├── src ├── vite-env.d.ts ├── default-board.css ├── utils.ts └── spring-board-element.ts ├── vite.config.ts ├── tests ├── test-constants.js ├── fixtures │ └── index.html ├── server.js └── tests.spec.ts ├── tsconfig.json ├── .github └── workflows │ └── ci.yaml ├── package.json ├── LICENSE ├── examples ├── index.html └── viewer.html ├── README.md ├── playwright.config.ts └── .gitignore /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | 3 | export default defineConfig({ 4 | build: { 5 | lib: { 6 | entry: 'src/spring-board-element.ts', 7 | formats: ['es', 'umd'], 8 | name: 'SpringBoardElement', 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /tests/test-constants.js: -------------------------------------------------------------------------------- 1 | export const publicKey = 2 | '41299b80b09f0bd623f3ce230328a7bab374975a7c497a5afb298b30183e0623'; 3 | export const privateKey = 4 | '52d117b99f320a1a66df787a6d86c7adb77ae4f881bdd5705c1bbbbdcd50f9c5'; 5 | export const generateBoardHTML = () => 6 | Buffer.from( 7 | `

Foo bar blee. Here's a number to prove it's me: 83.

`, 8 | ); 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "moduleResolution": "Node", 8 | "strict": true, 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "noEmit": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "skipLibCheck": true 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /src/default-board.css: -------------------------------------------------------------------------------- 1 | /* 2 | This is imported into spring-board-element.ts and used to build the template 3 | for the default board styles. 4 | 5 | Based on the Spring '83 spec's default style expectations: 6 | https://github.com/robinsloan/spring-83/blob/main/draft-20220629.md#boards-in-the-client 7 | */ 8 | 9 | :host { 10 | background-color: var(--board-background-color); 11 | box-sizing: border-box; 12 | display: block; 13 | padding: 2rem; 14 | } 15 | time { 16 | display: none; 17 | } 18 | p, 19 | h1, 20 | h2, 21 | h3, 22 | h4, 23 | h5 { 24 | margin: 0 0 2rem 0; 25 | } 26 | -------------------------------------------------------------------------------- /tests/fixtures/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | spring-board-element 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | tests: 7 | runs-on: ubuntu-latest 8 | 9 | env: 10 | CI: true 11 | 12 | steps: 13 | - name: Clone repository 14 | uses: actions/checkout@v3 15 | 16 | - name: Setup Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: '18.5.x' 20 | cache: 'npm' 21 | 22 | - name: Install dependencies 23 | run: npm ci 24 | 25 | - name: Install Playwright 26 | run: npx playwright install --with-deps 27 | 28 | - name: Run tests 29 | run: npm test 30 | 31 | - uses: actions/upload-artifact@v3 32 | if: always() 33 | with: 34 | name: playwright-report 35 | path: playwright-report/ 36 | retention-days: 30 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spring-board-element", 3 | "type": "module", 4 | "version": "0.3.0", 5 | "files": [ 6 | "dist" 7 | ], 8 | "main": "dist/spring-board-element.umd.cjs", 9 | "module": "dist/spring-board-element.js", 10 | "exports": { 11 | ".": { 12 | "import": "./dist/spring-board-element.js", 13 | "require": "./dist/spring-board-element.umd.cjs" 14 | } 15 | }, 16 | "scripts": { 17 | "build": "tsc && vite build", 18 | "dev": "vite", 19 | "preview": "vite preview", 20 | "test": "playwright test", 21 | "test:dev": "node tests/server.js" 22 | }, 23 | "devDependencies": { 24 | "@noble/ed25519": "^1.6.1", 25 | "@playwright/test": "^1.23.3", 26 | "@rdm/prettier-config": "^3.0.0", 27 | "prettier": "^2.7.1", 28 | "typescript": "^4.5.4", 29 | "vite": "^3.0.0" 30 | }, 31 | "prettier": "@rdm/prettier-config" 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ryan Murphy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Ensures that all links within a board open in a new tab. 3 | * 4 | * @private 5 | * @param event 6 | */ 7 | export function openLinksInNewTabs(event: MouseEvent) { 8 | const target = event.target as HTMLElement; 9 | 10 | if (target.matches('a')) { 11 | target.setAttribute('target', '_blank'); 12 | target.setAttribute('rel', 'noopener'); 13 | } 14 | } 15 | 16 | /** 17 | * Makes properties lazy. Enables board creation via document.createElement. 18 | * https://web.dev/custom-elements-best-practices/#make-properties-lazy 19 | * 20 | * @private 21 | * @param object 22 | * @param property 23 | */ 24 | export function upgradeProperty(object: any, property: string) { 25 | if (object.hasOwnProperty(property)) { 26 | let value = object[property]; 27 | delete object[property]; 28 | object[property] = value; 29 | } 30 | } 31 | 32 | export const enum States { 33 | Pending = 'pending', 34 | Fulfilled = 'fulfilled', 35 | Rejected = 'rejected', 36 | } 37 | 38 | export class Deferred { 39 | declare state: States; 40 | declare promise: Promise; 41 | declare resolve: (value: T) => void; 42 | declare reject: (value: any) => void; 43 | 44 | constructor() { 45 | this.state = States.Pending; 46 | 47 | this.promise = new Promise((resolve, reject) => { 48 | this.resolve = (value: T) => { 49 | this.state = States.Fulfilled; 50 | resolve(value); 51 | }; 52 | this.reject = (value: any) => { 53 | this.state = States.Rejected; 54 | reject(value); 55 | }; 56 | }); 57 | } 58 | 59 | get pending() { 60 | return this.state === States.Pending; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/server.js: -------------------------------------------------------------------------------- 1 | // packages 2 | import { sign } from '@noble/ed25519'; 3 | import { createServer } from 'vite'; 4 | 5 | // local 6 | import { generateBoardHTML, privateKey, publicKey } from './test-constants.js'; 7 | 8 | const BASE_URL = 'http://localhost'; 9 | 10 | async function run() { 11 | const server = await createServer({ 12 | plugins: [ 13 | { 14 | name: 'vite-micro-spring-server', 15 | async configureServer(server) { 16 | const boardHTML = generateBoardHTML(); 17 | const signatureBytes = await sign(boardHTML, privateKey); 18 | const signatureHex = Buffer.from(signatureBytes).toString('hex'); 19 | server.middlewares.use((request, response, next) => { 20 | const url = new URL(request.url, BASE_URL); 21 | if (url.pathname === '/') { 22 | response.statusCode = 200; 23 | response.end('OK'); 24 | } else if (url.pathname === `/${publicKey}`) { 25 | response.statusCode = 200; 26 | response.setHeader('Content-Type', 'text/html'); 27 | response.setHeader('Spring-Verson', '83'); 28 | response.setHeader('Spring-Signature', signatureHex); 29 | response.setHeader( 30 | 'Access-Control-Allow-Methods', 31 | 'GET, OPTIONS', 32 | ); 33 | response.setHeader('Access-Control-Allow-Origin', '*'); 34 | response.setHeader( 35 | 'Access-Control-Allow-Headers', 36 | 'Content-Type, If-Modified-Since, Spring-Signature, Spring-Version', 37 | ); 38 | response.setHeader( 39 | 'Access-Control-Expose-Headers', 40 | 'Content-Type, Last-Modified, Spring-Signature, Spring-Version', 41 | ); 42 | response.end(boardHTML); 43 | } else { 44 | next(); 45 | } 46 | }); 47 | }, 48 | }, 49 | ], 50 | }); 51 | 52 | server.listen(7000); 53 | } 54 | 55 | run().catch(console.error); 56 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | spring-board-element 12 | 21 | 29 | 36 | 37 | 38 | 39 | 42 | 45 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `` element 2 | 3 | A custom element that makes it simple to embed [Spring '83 boards](https://github.com/robinsloan/spring-83)! 4 | 5 | ## Usage 6 | 7 | If you are using `` in a client-side framework you'll likely want to install it via [npm](https://www.npmjs.com/), [Yarn](https://yarnpkg.com/) or [pnpm](https://pnpm.js.org/). 8 | 9 | ```sh 10 | npm install spring-board-element 11 | # or 12 | yarn add spring-board-element 13 | # or 14 | pnpm install spring-board-element 15 | ``` 16 | 17 | Then in your bundle, import the element: 18 | 19 | ```js 20 | import 'spring-board-element'; 21 | ``` 22 | 23 | However, a simple ` 29 | 30 | 31 | 32 | 33 | 34 | ``` 35 | 36 | ## Attributes 37 | 38 | `` has one optional attribute: 39 | 40 | - `href`: The URL of the board to embed. When this is changed a new board will be loaded. 41 | 42 | ## Properties 43 | 44 | `` has three properties: 45 | 46 | - `href`: The URL of the board to embed. Used to get or set the board URL. 47 | - `loaded`: A pending `Promise` that resolves when the board has loaded. Each time the `href` property is changed the `loaded` property will reference a new `Promise`. 48 | - `pubdate`: A `Date` object representing the date and time the board was published. This is retrieved from the board's `