├── 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 `