├── .gitignore ├── .vscode └── settings.json ├── README.md ├── assets └── favicon.ico ├── ast ├── parse-code.test.ts ├── parse-code.ts ├── parse-props.ts ├── query-nodes.test.ts ├── query-nodes.ts └── walk-ast.ts ├── data └── blog │ ├── demo-post-1.md │ └── demo-post-2.md ├── default-theme.ts ├── deps.ts ├── ds ├── layouts │ ├── BlogIndex.tsx │ ├── BlogPage.tsx │ ├── ComponentPage.tsx │ └── Page.tsx ├── patterns │ ├── Accordion.tsx │ ├── Alert.tsx │ ├── CodeEditor.tsx │ ├── List.tsx │ ├── Navigation.tsx │ ├── Table.tsx │ ├── Tabs.tsx │ ├── Tag.tsx │ └── Toc.tsx └── primitives │ ├── Box.test.tsx │ ├── Box.tsx │ ├── Button.tsx │ ├── Flex.tsx │ ├── Heading.tsx │ ├── Link.tsx │ ├── Stack.tsx │ ├── Text.tsx │ └── _utils.ts ├── lib ├── elements.ts ├── gentleRpc │ ├── jsonRpc2Types.ts │ └── rpcClient.ts ├── jsx │ ├── element-types.d.ts │ ├── events.d.ts │ └── intrinsic-elements.d.ts └── prism │ ├── mod.ts │ └── prism.js ├── mod.ts ├── pages ├── _page.ts ├── blog │ ├── _pages.ts │ └── index.tsx ├── design-system │ ├── Collection.tsx │ ├── Colors.tsx │ ├── SpacingScale.tsx │ ├── Types.tsx │ ├── _page.ts │ ├── _pages.ts │ ├── evaluate-code.ts │ ├── evaluate-jsx.test.tsx │ ├── evaluate-jsx.ts │ └── index.tsx └── index.tsx ├── scripts.json ├── swc-server ├── index.js ├── package-lock.json └── package.json ├── tsconfig.json ├── types.ts ├── user-theme.ts └── utils ├── generate-meta.ts ├── get-component.test.ts ├── get-component.ts ├── get-components.test.ts ├── get-components.ts ├── get-pages.ts ├── get-urls.test.ts ├── get-urls.ts ├── process-markdown.ts ├── watch-directories.ts └── web-sockets.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | public/ 3 | stats.json 4 | tailwind.ts 5 | 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "[typescript]": { 4 | "editor.defaultFormatter": "denoland.vscode-deno", 5 | }, 6 | "[typescriptreact]": { 7 | "editor.defaultFormatter": "denoland.vscode-deno", 8 | }, 9 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **Deprecated** See [gustwind](https://github.com/survivejs/gustwind) for the project that replaced this one. 2 | 3 | # Tailspin - Site generator and design system in one 4 | 5 | **Tailspin** is a collection of utilities that implements the following stack: 6 | 7 | - [Oceanwind](https://www.npmjs.com/package/oceanwind) for styling. It uses [Tailwind.css](https://tailwindcss.com/) syntax underneath. 8 | - [Deno](https://deno.land/) for bundling 9 | - [typed-html](https://www.npmjs.com/package/typed-html) for templating and component model 10 | - [Sidewind](https://sidewindjs.com/) for state management 11 | 12 | ## Structure 13 | 14 | - `assets/` - Static assets such as favicons. 15 | - `ds/` - The design system of the project lives here and it contains **layouts**, **patterns**, and **primitives** used to construct the pages. 16 | - `pages/` - The system picks up each `index.tsx` from the hierarchy and constructs a page for each. It's possible to load dynamic content to a section of a site by writing a `_pages.ts` file which returns a `getPages` function resolving to page data and `layout` pointing to a layout through which to render each page. 17 | 18 | ## Usage 19 | 20 | Run the available commands through [denon](https://github.com/denosaurs/denon) or [velociraptor](https://github.com/umbopepato/velociraptor) (vr). 21 | -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/survivejs/tailspin/089bcc5da7ebd7a824d57d0187ce3f8afce81f34/assets/favicon.ico -------------------------------------------------------------------------------- /ast/parse-code.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "../deps.ts"; 2 | import parseCode from "./parse-code.ts"; 3 | 4 | Deno.test("parses simple source", async () => { 5 | const componentSource = "
demo
"; 6 | const source = await parseCode( 7 | { name: "Demo", source: `const Demo = () => ${componentSource};` }, 8 | ); 9 | 10 | assertEquals(source, componentSource); 11 | }); 12 | 13 | // TODO: Wait for a swc fix for this. 14 | /* 15 | Deno.test("parses source with attributes", async () => { 16 | const componentSource = `
demo
`; 17 | const source = await parseCode( 18 | { name: "Demo", source: `const Demo = () => ${componentSource};` }, 19 | ); 20 | 21 | assertEquals(source, componentSource); 22 | }); 23 | */ 24 | -------------------------------------------------------------------------------- /ast/parse-code.ts: -------------------------------------------------------------------------------- 1 | import { printAst } from "../deps.ts"; 2 | import queryNodes from "./query-nodes.ts"; 3 | 4 | async function parseCode( 5 | { name, source }: { name: string; source: string }, 6 | ) { 7 | const identifierNodes = await queryNodes({ 8 | source, 9 | query: { 10 | type: "Identifier", 11 | value: name, 12 | }, 13 | }); 14 | 15 | if (identifierNodes.length) { 16 | const identifierParent = identifierNodes[0].parent; 17 | 18 | const jsxNodes = await queryNodes({ 19 | node: identifierParent, 20 | query: { 21 | type: "JSXElement", 22 | }, 23 | }); 24 | 25 | if (jsxNodes.length) { 26 | return printAst(jsxNodes[0].node); 27 | } 28 | } 29 | 30 | return Promise.resolve(""); 31 | } 32 | 33 | export default parseCode; 34 | -------------------------------------------------------------------------------- /ast/parse-props.ts: -------------------------------------------------------------------------------- 1 | import { printAst } from "../deps.ts"; 2 | import queryNodes from "./query-nodes.ts"; 3 | 4 | async function parseProps({ 5 | componentPath, 6 | displayName, 7 | source, 8 | }: { 9 | componentPath: string; 10 | displayName: string; 11 | source: string; 12 | }): Promise<{ name: string; isOptional: boolean; type: string }[] | undefined> { 13 | // TODO 14 | return Promise.resolve([{ name: "demo", isOptional: false, type: "demo" }]); 15 | 16 | /* 17 | // This isn't fool proof. It would be better to find specifically a function 18 | // to avoid matching something else. 19 | const componentNodes = await queryNodes({ 20 | source, 21 | // TODO 22 | // query: `Identifier[name="${displayName}"]`, 23 | query: { type: displayName }, 24 | }); 25 | const componentNode = componentNodes[0]; 26 | 27 | if (!componentNode) { 28 | return; 29 | } 30 | 31 | // @ts-ignore: TODO: Add parents to AST nodes 32 | const componentSource = await printAst(componentNode.parent); 33 | const propNodes = await queryNodes({ 34 | source: componentSource, 35 | // query: "TypeLiteral PropertySignature", 36 | // TODO 37 | query: {}, 38 | }); 39 | 40 | if (propNodes.length) { 41 | // @ts-ignore TODO: Fix the type 42 | return parseProperties(propNodes); 43 | } 44 | 45 | // TODO: Likely it would be better to select the first parameter instead 46 | const typeReferenceNodes = await queryNodes({ 47 | source: componentSource, 48 | // query: `Identifier[name="props"] ~ TypeReference`, 49 | // TODO 50 | query: {}, 51 | }); 52 | const typeReferenceNode = typeReferenceNodes[0]; 53 | 54 | if (typeReferenceNode) { 55 | // @ts-ignore 56 | const referenceType = typeReferenceNode.getText(); 57 | const propertySignatureNodes = await queryNodes({ 58 | source: source, 59 | /*query: 60 | `Identifier[name="${referenceType}"] ~ TypeLiteral > PropertySignature`, 61 | // TODO 62 | query: {}, 63 | }); 64 | 65 | if (propertySignatureNodes.length) { 66 | // @ts-ignore TODO: Fix the type 67 | return parseProperties(propertySignatureNodes); 68 | } 69 | 70 | const identifierNodes = await queryNodes({ 71 | source, 72 | // query: `Identifier[name="${referenceType}"]`, 73 | // TODO 74 | query: {}, 75 | }); 76 | const identifierNode = identifierNodes[0]; 77 | 78 | if (!identifierNode) { 79 | return; 80 | } 81 | 82 | // TODO: Tidy up 83 | // @ts-ignore 84 | const moduleTarget = identifierNode?.parent?.parent?.parent?.parent 85 | ?.moduleSpecifier 86 | ?.getText() 87 | .replace(/"/g, ""); 88 | 89 | // TODO: Figure out why this can occur for Stack 90 | if (!moduleTarget) { 91 | // console.warn(`parseProps - Missing module target`, identifierNode); 92 | return; 93 | } 94 | 95 | // TODO 96 | return Promise.resolve(undefined); 97 | 98 | return parseProps({ 99 | componentDirectory, 100 | // @ts-ignore TODO: Type this properly 101 | displayName: await import(componentPath).displayName, 102 | source: Deno.readTextFileSync(componentPath), 103 | }); 104 | } 105 | */ 106 | } 107 | 108 | // @ts-ignore 109 | function parseProperties(nodes) { 110 | if (!nodes.length) { 111 | return; 112 | } 113 | 114 | return nodes.map( 115 | // @ts-ignore: Figure out the exact type 116 | ({ name: nameNode, questionToken, type: typeNode }) => { 117 | const name = nameNode.getText(); 118 | const isOptional = !!questionToken; 119 | const type = typeNode.getText(); 120 | 121 | return { name, isOptional, type }; 122 | }, 123 | ); 124 | } 125 | 126 | export default parseProps; 127 | -------------------------------------------------------------------------------- /ast/query-nodes.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "../deps.ts"; 2 | import queryNodes from "./query-nodes.ts"; 3 | 4 | const source = `const magic = 5; 5 | 6 | function f(n:any) { 7 | return n+n; 8 | } 9 | 10 | 11 | function g() { 12 | return f(magic); 13 | } 14 | 15 | console.log(g());`; 16 | 17 | Deno.test("finds a const", async () => { 18 | const type = "VariableDeclaration"; 19 | const kind = "const"; 20 | const matches = await queryNodes( 21 | { source, query: { type, kind } }, 22 | ); 23 | 24 | assertEquals(matches.length, 1); 25 | assertEquals(matches[0].node.type, type); 26 | assertEquals(matches[0].node.kind, kind); 27 | }); 28 | 29 | Deno.test("finds functions", async () => { 30 | const type = "FunctionDeclaration"; 31 | const matches = await queryNodes( 32 | { source, query: { type } }, 33 | ); 34 | 35 | assertEquals(matches.length, 2); 36 | }); 37 | 38 | Deno.test("finds component source", async () => { 39 | const source = "const Demo = () =>
demo
;"; 40 | const type = "ArrowFunctionExpression"; 41 | 42 | const matches = await queryNodes( 43 | { source, query: { type } }, 44 | ); 45 | 46 | assertEquals(matches.length, 1); 47 | }); 48 | -------------------------------------------------------------------------------- /ast/query-nodes.ts: -------------------------------------------------------------------------------- 1 | import { parseSource } from "../deps.ts"; 2 | import type { AstNode } from "../types.ts"; 3 | import walkAst from "./walk-ast.ts"; 4 | 5 | type Query = { [key in keyof AstNode]?: string }; 6 | 7 | async function queryNodes( 8 | { node, source, query }: { node?: AstNode; source?: string; query: Query }, 9 | ) { 10 | const matches: { node: AstNode; parent?: AstNode }[] = []; 11 | 12 | if (!node && !source) { 13 | return []; 14 | } 15 | 16 | walkAst({ 17 | // @ts-ignore: Find a better way to pass either node or source 18 | node: node || await parseSource(source), 19 | onNode: (node: AstNode, parent?: AstNode) => { 20 | if ( 21 | // @ts-ignore: Figure out how to type this 22 | Object.entries(query).every(([k, v]) => node[k] === v) 23 | ) { 24 | matches.push({ node, parent }); 25 | } 26 | }, 27 | }); 28 | 29 | return matches; 30 | } 31 | 32 | export default queryNodes; 33 | -------------------------------------------------------------------------------- /ast/walk-ast.ts: -------------------------------------------------------------------------------- 1 | import type { AstNode } from "../types.ts"; 2 | 3 | function walkAst( 4 | { node, onNode, parent }: { 5 | node: AstNode; 6 | onNode: (node: AstNode, parent?: AstNode) => void; 7 | parent?: AstNode; 8 | }, 9 | ) { 10 | onNode(node, parent); 11 | 12 | if (node.body) { 13 | if (Array.isArray(node.body)) { 14 | node.body.forEach((child) => { 15 | walkAst({ node: child, onNode, parent: node }); 16 | }); 17 | } else if (node.body) { 18 | walkAst({ node: node.body, onNode, parent: node }); 19 | } 20 | } 21 | if (node.declarations) { 22 | if (Array.isArray(node.declarations)) { 23 | node.declarations.forEach((child) => { 24 | walkAst({ node: child, onNode, parent: node }); 25 | }); 26 | } 27 | } 28 | if (node.declaration) { 29 | walkAst({ node: node.declaration, onNode, parent: node }); 30 | } 31 | if (node.expression) { 32 | walkAst({ node: node.expression, onNode, parent: node }); 33 | } 34 | if (node.id) { 35 | walkAst({ node: node.id, onNode, parent: node }); 36 | } 37 | if (node.init) { 38 | walkAst({ node: node.init, onNode, parent: node }); 39 | } 40 | } 41 | 42 | export default walkAst; 43 | -------------------------------------------------------------------------------- /data/blog/demo-post-1.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Brief Guide to Finnish vol. 1 — Basics 3 | description: >- 4 | They say Finnish is one of the most difficult languages in the world. It’s 5 | obviously the easiest one for me since I was grown into it but I… 6 | date: "2018-01-20T12:21:01.230Z" 7 | categories: ["demo"] 8 | slug: /demo-post-1/ 9 | author: "Juho Vepsäläinen" 10 | --- 11 | 12 | Demo post 1 content goes here. 13 | -------------------------------------------------------------------------------- /data/blog/demo-post-2.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Brief Guide to Finnish vol. 2 3 | description: >- 4 | While preparing for the upcoming React Finland (24–26.04) conference, you may 5 | have spent some time learning the basics of the Finnish… 6 | date: "2019-02-04T10:18:30.273Z" 7 | categories: [] 8 | keywords: [] 9 | slug: /demo-post-2/ 10 | author: "Juho Vepsäläinen" 11 | --- 12 | 13 | Demo post 2 content goes here. 14 | -------------------------------------------------------------------------------- /default-theme.ts: -------------------------------------------------------------------------------- 1 | // Default theme copied over from Oceanwind 2 | export default { 3 | font: { 4 | sans: '"Lato", Roboto, "Helvetica Neue", "Segoe UI", sans-serif', 5 | serif: 'Georgia, Cambria, "Times New Roman", Times, serif', 6 | mono: 7 | 'Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', 8 | }, 9 | 10 | fontMetrics: { 11 | "Segoe UI": { 12 | capHeight: 1455, 13 | ascent: 2200, 14 | descent: -480, 15 | lineGap: 0, 16 | unitsPerEm: 2048, 17 | }, 18 | Roboto: { 19 | capHeight: 1500, 20 | ascent: 1900, 21 | descent: -500, 22 | lineGap: 0, 23 | unitsPerEm: 2048, 24 | }, 25 | "Helvetica Neue": { 26 | capHeight: 1433, 27 | ascent: 1974, 28 | descent: -426, 29 | lineGap: 0, 30 | unitsPerEm: 2048, 31 | }, 32 | Lato: { 33 | capHeight: 1433, 34 | ascent: 1974, 35 | descent: -426, 36 | lineGap: 0, 37 | unitsPerEm: 2000, 38 | }, 39 | }, 40 | 41 | unit: { 42 | auto: "auto", 43 | full: "100%", 44 | px: "1px", 45 | "0": "0", 46 | "1": "0.25rem", 47 | "2": "0.5rem", 48 | "3": "0.75rem", 49 | "4": "1rem", 50 | "5": "1.25rem", 51 | "6": "1.5rem", 52 | "8": "2rem", 53 | "10": "2.5rem", 54 | "12": "3rem", 55 | "16": "4rem", 56 | "20": "5rem", 57 | "24": "6rem", 58 | "32": "8rem", 59 | "40": "10rem", 60 | "48": "12rem", 61 | "56": "15rem", 62 | "64": "16rem", 63 | }, 64 | 65 | width: { 66 | auto: "auto", 67 | xs: "20rem", 68 | sm: "24rem", 69 | md: "28rem", 70 | lg: "32rem", 71 | xl: "36rem", 72 | "2xl": "42rem", 73 | "3xl": "48rem", 74 | "4xl": "56rem", 75 | "5xl": "64rem", 76 | "6xl": "72rem", 77 | full: "100%", 78 | }, 79 | 80 | screen: { 81 | xs: "480px", 82 | sm: "640px", 83 | md: "768px", 84 | lg: "1024px", 85 | xl: "1280px", 86 | }, 87 | 88 | text: { 89 | xs: "0.75rem", 90 | sm: "0.875rem", 91 | base: "1rem", 92 | lg: "1.125rem", 93 | xl: "1.25rem", 94 | "2xl": "1.5rem", 95 | "3xl": "1.875rem", 96 | "4xl": "2.25rem", 97 | "5xl": "3rem", 98 | "6xl": "4rem", 99 | }, 100 | 101 | weight: { 102 | hairline: 100, 103 | thin: 200, 104 | light: 300, 105 | normal: 400, 106 | medium: 500, 107 | semibold: 600, 108 | bold: 700, 109 | extrabold: 800, 110 | black: 900, 111 | }, 112 | 113 | tracking: { 114 | tighter: "-0.05em", 115 | tight: "-0.025em", 116 | normal: "0em", 117 | wide: "0.025em", 118 | wider: "0.05em", 119 | widest: "0.1em", 120 | }, 121 | 122 | leading: { 123 | none: 1, 124 | tight: 1.25, 125 | snug: 1.375, 126 | normal: 1.5, 127 | relaxed: 1.625, 128 | loose: 2, 129 | "3": "0.75rem", 130 | "4": "1rem", 131 | "5": "1.25rem", 132 | "6": "1.5rem", 133 | "7": "1.75rem", 134 | "8": "2rem", 135 | }, 136 | 137 | rounded: { 138 | "": "0.25rem", 139 | none: "0", 140 | sm: "0.125rem", 141 | md: "0.375rem", 142 | lg: "0.5rem", 143 | xl: "1rem", 144 | "2xl": "2rem", 145 | full: "9999px", 146 | }, 147 | 148 | border: { 149 | "0": "0", 150 | "": "1px", 151 | "1": "1px", 152 | "2": "2px", 153 | "4": "4px", 154 | "8": "8px", 155 | "16": "16px", 156 | }, 157 | 158 | shadow: { 159 | xs: "0 0 0 1px rgba(0, 0, 0, 0.05)", 160 | sm: "0 1px 2px 0 rgba(0, 0, 0, 0.05)", 161 | "": "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)", 162 | md: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)", 163 | lg: 164 | "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)", 165 | xl: 166 | "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)", 167 | "2xl": "0 25px 50px -12px rgba(0, 0, 0, 0.25)", 168 | inner: "inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)", 169 | outline: "0 0 0 3px rgba(66, 153, 225, 0.5)", 170 | }, 171 | 172 | duration: { 173 | "75": "75", 174 | "100": "100", 175 | "150": "150", 176 | "200": "200", 177 | "300": "300", 178 | "500": "500", 179 | "700": "700", 180 | "1000": "1000", 181 | }, 182 | 183 | scale: { 184 | "0": "0", 185 | "50": ".5", 186 | "75": ".75", 187 | "90": ".9", 188 | "95": ".95", 189 | "100": "1", 190 | "105": "1.05", 191 | "110": "1.1", 192 | "125": "1.25", 193 | "150": "1.5", 194 | }, 195 | 196 | rotate: { 197 | "0": 0, 198 | "45": 45, 199 | "90": 90, 200 | "180": 180, 201 | "-180": -180, 202 | "-90": -90, 203 | "-45": -45, 204 | }, 205 | 206 | skew: { 207 | "0": "0", 208 | "3": "3deg", 209 | "6": "6deg", 210 | "12": "12deg", 211 | }, 212 | 213 | colors: { 214 | transparent: "transparent", 215 | current: "currentColor", 216 | black: "#000", 217 | white: "#fff", 218 | gray: { 219 | "100": "#F7FAFC", 220 | "200": "#EDF2F7", 221 | "300": "#E2E8F0", 222 | "400": "#CBD5E0", 223 | "500": "#A0AEC0", 224 | "600": "#718096", 225 | "700": "#4A5568", 226 | "800": "#2D3748", 227 | "900": "#1A202C", 228 | }, 229 | red: { 230 | "100": "#FFF5F5", 231 | "200": "#FED7D7", 232 | "300": "#FEB2B2", 233 | "400": "#FC8181", 234 | "500": "#F56565", 235 | "600": "#E53E3E", 236 | "700": "#C53030", 237 | "800": "#9B2C2C", 238 | "900": "#742A2A", 239 | }, 240 | orange: { 241 | "100": "#FFFAF0", 242 | "200": "#FEEBC8", 243 | "300": "#FBD38D", 244 | "400": "#F6AD55", 245 | "500": "#ED8936", 246 | "600": "#DD6B20", 247 | "700": "#C05621", 248 | "800": "#9C4221", 249 | "900": "#7B341E", 250 | }, 251 | yellow: { 252 | "100": "#FFFFF0", 253 | "200": "#FEFCBF", 254 | "300": "#FAF089", 255 | "400": "#F6E05E", 256 | "500": "#ECC94B", 257 | "600": "#D69E2E", 258 | "700": "#B7791F", 259 | "800": "#975A16", 260 | "900": "#744210", 261 | }, 262 | green: { 263 | "100": "#F0FFF4", 264 | "200": "#C6F6D5", 265 | "300": "#9AE6B4", 266 | "400": "#68D391", 267 | "500": "#48BB78", 268 | "600": "#38A169", 269 | "700": "#2F855A", 270 | "800": "#276749", 271 | "900": "#22543D", 272 | }, 273 | teal: { 274 | "100": "#E6FFFA", 275 | "200": "#B2F5EA", 276 | "300": "#81E6D9", 277 | "400": "#4FD1C5", 278 | "500": "#38B2AC", 279 | "600": "#319795", 280 | "700": "#2C7A7B", 281 | "800": "#285E61", 282 | "900": "#234E52", 283 | }, 284 | blue: { 285 | "100": "#EBF8FF", 286 | "200": "#BEE3F8", 287 | "300": "#90CDF4", 288 | "400": "#63B3ED", 289 | "500": "#4299E1", 290 | "600": "#3182CE", 291 | "700": "#2B6CB0", 292 | "800": "#2C5282", 293 | "900": "#2A4365", 294 | }, 295 | indigo: { 296 | "100": "#EBF4FF", 297 | "200": "#C3DAFE", 298 | "300": "#A3BFFA", 299 | "400": "#7F9CF5", 300 | "500": "#667EEA", 301 | "600": "#5A67D8", 302 | "700": "#4C51BF", 303 | "800": "#434190", 304 | "900": "#3C366B", 305 | }, 306 | purple: { 307 | "100": "#FAF5FF", 308 | "200": "#E9D8FD", 309 | "300": "#D6BCFA", 310 | "400": "#B794F4", 311 | "500": "#9F7AEA", 312 | "600": "#805AD5", 313 | "700": "#6B46C1", 314 | "800": "#553C9A", 315 | "900": "#44337A", 316 | }, 317 | pink: { 318 | "100": "#FFF5F7", 319 | "200": "#FED7E2", 320 | "300": "#FBB6CE", 321 | "400": "#F687B3", 322 | "500": "#ED64A6", 323 | "600": "#D53F8C", 324 | "700": "#B83280", 325 | "800": "#97266D", 326 | "900": "#702459", 327 | }, 328 | }, 329 | }; 330 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | import * as path from "https://deno.land/std/path/mod.ts"; 2 | import { expandGlobSync, existsSync } from "https://deno.land/std/fs/mod.ts"; 3 | import { assertEquals } from "https://deno.land/std@0.69.0/testing/asserts.ts"; 4 | import { 5 | setup as setupOceanwind, 6 | getStyleTag, 7 | VirtualInjector, 8 | themed, 9 | } from "https://unpkg.com/@bebraw/oceanwind@0.2.6"; 10 | import { Application } from "https://deno.land/x/oak@v6.2.0/mod.ts"; 11 | import * as elements from "./lib/elements.ts"; 12 | import parseCode from "./ast/parse-code.ts"; 13 | import parseProps from "./ast/parse-props.ts"; 14 | import queryNodes from "./ast/query-nodes.ts"; 15 | import getComponents from "./utils/get-components.ts"; 16 | import processMarkdown from "./utils/process-markdown.ts"; 17 | import type { AstNode } from "./types.ts"; 18 | import { createRemote } from "./lib/gentleRpc/rpcClient.ts"; 19 | import userTheme from "./user-theme.ts"; 20 | import Prism from "./lib/prism/mod.ts"; 21 | 22 | const remote = createRemote("http://0.0.0.0:4000"); 23 | 24 | const joinPath = path.posix.join; 25 | const getDirectory = path.posix.dirname; 26 | const getRelativePath = path.posix.relative; 27 | 28 | const printAst = async (ast: AstNode): Promise => 29 | // @ts-ignore 30 | remote.print(ast); 31 | 32 | const parseSource = async (source: string): Promise => 33 | // @ts-ignore 34 | remote.parse(source); 35 | 36 | const getStyleInjector = () => { 37 | const injector = VirtualInjector(); 38 | 39 | setupOceanwind({ injector }); 40 | 41 | return injector; 42 | }; 43 | 44 | const ow = themed(userTheme); 45 | 46 | // TODO: Check that given language exists 47 | const highlight = (str: string, language: string) => 48 | Prism.highlight(str, Prism.languages[language], language); 49 | 50 | export { 51 | ow, 52 | highlight, 53 | assertEquals, 54 | expandGlobSync, 55 | elements, 56 | joinPath, 57 | existsSync, 58 | getComponents, 59 | getDirectory, 60 | getRelativePath, 61 | parseCode, 62 | parseProps, 63 | parseSource, 64 | printAst, 65 | processMarkdown, 66 | queryNodes, 67 | getStyleInjector, 68 | getStyleTag, 69 | Application, 70 | }; 71 | -------------------------------------------------------------------------------- /ds/layouts/BlogIndex.tsx: -------------------------------------------------------------------------------- 1 | import { elements, joinPath } from "../../deps.ts"; 2 | import PageLayout from "./Page.tsx"; 3 | import Tag from "../patterns/Tag.tsx"; 4 | import Box from "../primitives/Box.tsx"; 5 | import Flex from "../primitives/Flex.tsx"; 6 | import Stack from "../primitives/Stack.tsx"; 7 | import Heading from "../primitives/Heading.tsx"; 8 | import Link from "../primitives/Link.tsx"; 9 | import type { BlogPage } from "./BlogPage.tsx"; 10 | 11 | export type BlogIndexLayoutProps = { 12 | url: string; 13 | pages: BlogPage[]; 14 | }; 15 | 16 | const BlogIndexLayout = ({ 17 | url, 18 | pages, 19 | }: BlogIndexLayoutProps) => ( 20 | 29 | 30 | Blog pages 31 | 32 | 33 | {pages 34 | .map(({ meta: { title, description, slug, categories } }) => ( 35 | 36 | 37 | {/* @ts-ignore */} 38 | {title} 39 | 40 | {description} 41 | 42 | {categories.map((category) => {category}).join("")} 43 | 44 | 45 | )) 46 | .join("")} 47 | 48 | } 49 | /> 50 | ); 51 | 52 | export const displayName = "BlogIndexLayout"; 53 | export const Example = () => ( 54 | 71 | ); 72 | 73 | export default BlogIndexLayout; 74 | -------------------------------------------------------------------------------- /ds/layouts/BlogPage.tsx: -------------------------------------------------------------------------------- 1 | import { elements } from "../../deps.ts"; 2 | import PageLayout from "./Page.tsx"; 3 | import Tag from "../patterns/Tag.tsx"; 4 | import Box from "../primitives/Box.tsx"; 5 | import Flex from "../primitives/Flex.tsx"; 6 | import Stack from "../primitives/Stack.tsx"; 7 | import Heading from "../primitives/Heading.tsx"; 8 | import Text from "../primitives/Text.tsx"; 9 | 10 | export type BlogPage = { 11 | url: string; 12 | content: string; 13 | meta: { 14 | title: string; 15 | categories: string[]; 16 | description: string; 17 | author: string; 18 | date: string; 19 | slug: string; 20 | }; 21 | }; 22 | 23 | const BlogPageLayout = ({ 24 | url, 25 | attributes, 26 | }: { 27 | url: string; 28 | attributes: BlogPage; 29 | }) => ( 30 | 33 | 34 | {attributes.meta.title} 35 | 36 | 37 | 38 | {attributes.meta.categories 39 | .map((category) => {category}) 40 | .join("")} 41 | 42 | {attributes.content} 43 | 44 | {attributes.meta.author} 45 | 46 | {/*new Intl.DateTimeFormat("en", { 47 | year: "numeric", 48 | month: "short", 49 | day: "2-digit", 50 | }).format(new Date(attributes.meta.date))*/} 51 | 52 | 53 | 54 | } 55 | /> 56 | ); 57 | 58 | export const displayName = "BlogPageLayout"; 59 | export const Example = () => ( 60 | 75 | ); 76 | 77 | export default BlogPageLayout; 78 | -------------------------------------------------------------------------------- /ds/layouts/ComponentPage.tsx: -------------------------------------------------------------------------------- 1 | import { elements } from "../../deps.ts"; 2 | import type { DesignSystemComponent } from "../../types.ts"; 3 | import PageLayout from "./Page.tsx"; 4 | import Tag, { 5 | Example as TagExample, 6 | description as tagDescription, 7 | } from "../patterns/Tag.tsx"; 8 | import Stack from "../primitives/Stack.tsx"; 9 | import Box from "../primitives/Box.tsx"; 10 | import Heading from "../primitives/Heading.tsx"; 11 | 12 | const ComponentPageLayout = ({ 13 | url, 14 | attributes: { 15 | component, 16 | }, 17 | }: { 18 | url: string; 19 | attributes: { 20 | component: DesignSystemComponent; 21 | }; 22 | }) => ( 23 | 26 | 27 | {component.displayName} 28 | 29 | 30 | {component.description} 31 | {component.Example()} 32 | 33 | } 34 | /> 35 | ); 36 | 37 | export const displayName = "BlogPageLayout"; 38 | export const Example = () => ( 39 | 50 | ); 51 | 52 | export default ComponentPageLayout; 53 | -------------------------------------------------------------------------------- /ds/layouts/Page.tsx: -------------------------------------------------------------------------------- 1 | import { elements } from "../../deps.ts"; 2 | import { Navigation, NavigationItem } from "../patterns/Navigation.tsx"; 3 | import Box from "../primitives/Box.tsx"; 4 | import Flex from "../primitives/Flex.tsx"; 5 | import Link from "../primitives/Link.tsx"; 6 | import Text from "../primitives/Text.tsx"; 7 | 8 | type PageLayoutProps = { body: string; url: string }; 9 | 10 | // TODO: Support fragments (<>) 11 | const PageLayout = ({ body, url }: PageLayoutProps) => ( 12 | 13 | 14 | 16 | tailspin 17 | } 18 | > 19 | 20 | 21 | Blog 22 | 23 | 27 | Design system 28 | 29 | 30 | 31 | 34 | 40 | Star at GitHub 41 | 42 | 43 | 44 | 45 | 46 | 47 | {body} 48 | 49 | 56 | 57 | Created by{" "} 58 | 59 | Juho Vepsäläinen 60 | 61 | 62 | 63 | 64 | ); 65 | 66 | export const displayName = "PageLayout"; 67 | export const Example = () => ; 68 | 69 | export default PageLayout; 70 | -------------------------------------------------------------------------------- /ds/patterns/Accordion.tsx: -------------------------------------------------------------------------------- 1 | import { elements, ow } from "../../deps.ts"; 2 | import Box from "../primitives/Box.tsx"; 3 | import Flex from "../primitives/Flex.tsx"; 4 | 5 | const Accordion = ({ title }: { title: string }, children: string[]) => ( 6 | 7 | 12 | {title} 13 | 14 | 15 |
16 | {children.join("")} 17 |
18 |
19 | ); 20 | 21 | export const description = 22 | "Use Accordion when you want to show a lot of information in a compact space."; 23 | export const displayName = "Accordion"; 24 | export const Example = () => ( 25 | 26 | Junior engineer description goes here 27 | 28 | ); 29 | export const showCodeEditor = true; 30 | 31 | export default Accordion; 32 | -------------------------------------------------------------------------------- /ds/patterns/Alert.tsx: -------------------------------------------------------------------------------- 1 | import { elements } from "../../deps.ts"; 2 | import Box from "../primitives/Box.tsx"; 3 | import Flex from "../primitives/Flex.tsx"; 4 | import Stack from "../primitives/Stack.tsx"; 5 | import type config from "../../tailwind.ts"; 6 | 7 | type ColorKeys = keyof typeof config.colors; 8 | type Variant = "info" | "warning" | "error" | "success"; 9 | 10 | // TODO: Generate examples using available variants 11 | // https://tailwindcss.com/components/alerts 12 | const Alert = ({ variant }: { variant: Variant }, children: string[]) => { 13 | const { border, color, bg, icon } = getStyle(variant); 14 | 15 | return ( 16 | 25 | 26 | {icon} 27 | 28 | {children.join("")} 29 | 30 | ); 31 | }; 32 | 33 | // Icons from https://heroicons.com/ 34 | function getStyle( 35 | variant: Variant, 36 | ): { border: ColorKeys; color: ColorKeys; bg: ColorKeys; icon: string } { 37 | switch (variant) { 38 | case "info": 39 | return { 40 | border: "blue-400", 41 | color: "blue-700", 42 | bg: "blue-100", 43 | icon: ( 44 | 52 | 59 | 60 | ), 61 | }; 62 | case "success": 63 | return { 64 | border: "green-400", 65 | color: "green-700", 66 | bg: "green-100", 67 | icon: ( 68 | 76 | 83 | 84 | ), 85 | }; 86 | case "warning": 87 | return { 88 | border: "yellow-400", 89 | color: "yellow-700", 90 | bg: "yellow-100", 91 | icon: ( 92 | 100 | 107 | 108 | ), 109 | }; 110 | case "error": 111 | return { 112 | border: "red-400", 113 | color: "red-700", 114 | bg: "red-100", 115 | icon: ( 116 | 124 | 131 | 132 | ), 133 | }; 134 | } 135 | } 136 | 137 | export const displayName = "Alert"; 138 | export const Example = () => ( 139 | 140 | This is an info alert 141 | This is a success alert 142 | This is a warning alert 143 | This is an error alert 144 | 145 | ); 146 | 147 | export default Alert; 148 | -------------------------------------------------------------------------------- /ds/patterns/CodeEditor.tsx: -------------------------------------------------------------------------------- 1 | import { elements, highlight } from "../../deps.ts"; 2 | import Box from "../primitives/Box.tsx"; 3 | import Text from "../primitives/Text.tsx"; 4 | 5 | const CodeContainer = ( 6 | { sources }: { sources: { [key: string]: string } }, 7 | children: string[], 8 | ) => { 9 | const state = `{ ${ 10 | Object.entries(sources) 11 | .map(([name, source]) => `${name}: atob('${btoa(source)}')`) 12 | .join(", ") 13 | } }`; 14 | 15 | return ( 16 | 17 | {children.join("")} 18 | 19 | ); 20 | }; 21 | 22 | // TODO: Textarea 23 | const CodeEditor = ({ 24 | parent = "this", 25 | value = "code", 26 | fallback, 27 | }: { 28 | parent?: string; 29 | value: string; 30 | fallback: string; 31 | }) => ( 32 | 38 | 39 | Editor 40 | 41 | 42 | 50 | {highlight(fallback, "tsx")} 51 | 52 | 65 | 66 | 67 | ); 68 | 69 | export const displayName = "CodeEditor"; 70 | export const Example = () => ( 71 | 72 | 73 | 74 | 75 | ); 76 | 77 | export { CodeContainer, CodeEditor }; 78 | -------------------------------------------------------------------------------- /ds/patterns/List.tsx: -------------------------------------------------------------------------------- 1 | import { elements } from "../../deps.ts"; 2 | import Box from "../primitives/Box.tsx"; 3 | 4 | type Variant = "none" | "disc" | "decimal"; 5 | 6 | // https://tailwindcss.com/components/alerts 7 | const List = ({ variant }: { variant: Variant }, children: string[]) => ( 8 | 9 | {children.join("")} 10 | 11 | ); 12 | 13 | const ListItem = ({}, children: string[]) => ( 14 | {children.join("")} 15 | ); 16 | 17 | export const displayName = "List"; 18 | export const Example = () => ( 19 | 20 | Red 21 | Yellow 22 | Green 23 | 24 | ); 25 | export const showCodeEditor = true; 26 | 27 | export { List, ListItem }; 28 | -------------------------------------------------------------------------------- /ds/patterns/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import { elements, ow } from "../../deps.ts"; 2 | import Flex from "../primitives/Flex.tsx"; 3 | import Box from "../primitives/Box.tsx"; 4 | import Link, { ExternalLinkProps } from "../primitives/Link.tsx"; 5 | 6 | // https://tailwindcss.com/components/navigation 7 | const Navigation = ({ logo }: { logo?: string }, children: string[]) => ( 8 | 15 | 16 | {logo} 17 | 18 | 19 | 27 | 33 | Menu 34 | 35 | 36 | 37 | 38 | 43 | {children.join("")} 44 | 45 | 46 | ); 47 | 48 | const NavigationItem = ( 49 | { href, isSelected }: ExternalLinkProps & { isSelected?: boolean }, 50 | label: string[], 51 | ) => ( 52 | 58 | {label} 59 | 60 | ); 61 | 62 | const displayName = "Navigation"; 63 | const Example = () => ( 64 | 65 | tailspin} 67 | > 68 | 69 | Blog 70 | 71 | Design system 72 | 73 | 74 | 75 | 78 | Star at GitHub 79 | 80 | 81 | 82 | 83 | ); 84 | export const showCodeEditor = true; 85 | 86 | export { Navigation, NavigationItem, displayName, Example }; 87 | -------------------------------------------------------------------------------- /ds/patterns/Table.tsx: -------------------------------------------------------------------------------- 1 | import { elements } from "../../deps.ts"; 2 | import Box from "../primitives/Box.tsx"; 3 | 4 | // https://tailwindcss.com/docs/display/#table 5 | const Table = ({}, children: string[]) => ( 6 | 7 | {children.join("")} 8 | 9 | ); 10 | 11 | const TableRow = ({}, children: string[]) => ( 12 | 13 | {children.join("")} 14 | 15 | ); 16 | 17 | const TableHeader = ({}, children: string[]) => ( 18 | {children.join("")} 19 | ); 20 | 21 | const TableHeaderCell = ({}, children: string[]) => ( 22 | 23 | {children} 24 | 25 | ); 26 | 27 | const TableBody = ({}, children: string[]) => ( 28 | {children.join("")} 29 | ); 30 | 31 | const TableBodyCell = ({}, children: string[]) => ( 32 | 33 | {children.join("")} 34 | 35 | ); 36 | 37 | export const displayName = "Table"; 38 | export const Example = () => ( 39 | 40 | 41 | 42 | Language 43 | Color 44 | 45 | 46 | 47 | 48 | JavaScript 49 | Yellow 50 | 51 | 52 | Go 53 | Blue 54 | 55 | 56 | Python 57 | Green 58 | 59 | 60 |
61 | ); 62 | export const showCodeEditor = true; 63 | 64 | export { 65 | Table, 66 | TableHeader, 67 | TableHeaderCell, 68 | TableBody, 69 | TableBodyCell, 70 | TableRow, 71 | }; 72 | -------------------------------------------------------------------------------- /ds/patterns/Tabs.tsx: -------------------------------------------------------------------------------- 1 | import { elements, ow } from "../../deps.ts"; 2 | import Box from "../primitives/Box.tsx"; 3 | import Flex from "../primitives/Flex.tsx"; 4 | 5 | // https://tailwindcss.com/components/navigation/#tabs 6 | const Tabs = ({ selectedTab }: { selectedTab: string }, children: string[]) => ( 7 | 8 | {children.join("")} 9 | 10 | ); 11 | const TabHeader = ({}, children: string[]) => ( 12 | 13 | {children.join("")} 14 | 15 | ); 16 | const TabHeaderItem = ({ tabId }: { tabId: string }, children: string[]) => ( 17 | 25 | {children.join("")} 26 | 27 | ); 28 | const TabBody = ({}, children: string[]) => ( 29 | 30 | {children.join("")} 31 | 32 | ); 33 | const TabBodyItem = ( 34 | { tabId }: { tabId: string; showAsFallback?: boolean }, 35 | children: string[], 36 | ) => ( 37 | 38 | {children.join("")} 39 | 40 | ); 41 | 42 | export const description = 43 | "Use Tabs when you have a limited amount of space and a related group of items to explain."; 44 | export const displayName = "Tabs"; 45 | export const Example = () => ( 46 | 47 | 48 | Animals 49 | Languages 50 | Colors 51 | 52 | 53 | Cats, dogs, monkeys 54 | German, Finnish, English 55 | blue, green, red 56 | 57 | 58 | ); 59 | export const showCodeEditor = true; 60 | 61 | export { Tabs, TabHeader, TabHeaderItem, TabBody, TabBodyItem }; 62 | -------------------------------------------------------------------------------- /ds/patterns/Tag.tsx: -------------------------------------------------------------------------------- 1 | import { elements } from "../../deps.ts"; 2 | import Box from "../primitives/Box.tsx"; 3 | import Stack from "../primitives/Stack.tsx"; 4 | import Link from "../primitives/Link.tsx"; 5 | import Text from "../primitives/Text.tsx"; 6 | 7 | // https://tailwindcss.com/components/alerts 8 | const Tag = ({}, children: string[]) => ( 9 | 10 | {children.join("")} 11 | 12 | ); 13 | 14 | export const description = "Use tag for metadata"; 15 | export const displayName = "Tag"; 16 | export const Example = () => ( 17 | 18 | Angular.js 19 | 20 | React 21 | 22 | Vue 23 | 24 | ); 25 | export const showCodeEditor = true; 26 | 27 | export default Tag; 28 | -------------------------------------------------------------------------------- /ds/patterns/Toc.tsx: -------------------------------------------------------------------------------- 1 | import { elements, ow } from "../../deps.ts"; 2 | import Box from "../primitives/Box.tsx"; 3 | import { List, ListItem } from "./List.tsx"; 4 | 5 | const Toc = () => ( 6 | 12 | 13 | 14 | 15 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | 32 | export const description = 33 | `Toc (table of contents) constructs its contents based on h2 and h3 elements while maintaining active state while the page is scrolled.`; 34 | export const displayName = "Toc"; 35 | export const Example = () => ; 36 | export const showCodeEditor = true; 37 | 38 | export default Toc; 39 | -------------------------------------------------------------------------------- /ds/primitives/Box.test.tsx: -------------------------------------------------------------------------------- 1 | import { assertEquals, elements } from "../../deps.ts"; 2 | import Box from "./Box.tsx"; 3 | 4 | // TODO: Figure out why Deno.test doesn't work in tsx 5 | // Deno.test("renders an empty div", async () => { 6 | assertEquals(hello, "
hello
"); 7 | // }); 8 | -------------------------------------------------------------------------------- /ds/primitives/Box.tsx: -------------------------------------------------------------------------------- 1 | import { elements } from "../../deps.ts"; 2 | import { constructTailwindClasses, tailwindKeys } from "./_utils.ts"; 3 | import type config from "../../tailwind.ts"; 4 | 5 | type ColorKeys = keyof typeof config.colors; 6 | type MarginKeys = keyof typeof config.unit; 7 | type PaddingKeys = keyof typeof config.unit; 8 | type WidthKeys = keyof typeof config.width; 9 | type MinWidthKeys = keyof typeof config.minWidth; 10 | type MaxWidthKeys = keyof typeof config.maxWidth; 11 | type HeightKeys = keyof typeof config.height; 12 | type MinHeightKeys = keyof typeof config.minHeight; 13 | type MaxHeightKeys = keyof typeof config.maxHeight; 14 | type ScreenKeys = keyof typeof config.screen; 15 | 16 | export type BoxProps = { 17 | as?: keyof JSX.IntrinsicElements; 18 | m?: MarginKeys | { [k in ScreenKeys | "default"]?: MarginKeys }; 19 | mb?: MarginKeys | { [k in ScreenKeys | "default"]?: MarginKeys }; 20 | mt?: MarginKeys | { [k in ScreenKeys | "default"]?: MarginKeys }; 21 | ml?: MarginKeys | { [k in ScreenKeys | "default"]?: MarginKeys }; 22 | mr?: MarginKeys | { [k in ScreenKeys | "default"]?: MarginKeys }; 23 | mx?: MarginKeys | { [k in ScreenKeys | "default"]?: MarginKeys }; 24 | my?: MarginKeys | { [k in ScreenKeys | "default"]?: MarginKeys }; 25 | p?: PaddingKeys | { [k in ScreenKeys | "default"]?: PaddingKeys }; 26 | pb?: PaddingKeys | { [k in ScreenKeys | "default"]?: PaddingKeys }; 27 | pt?: PaddingKeys | { [k in ScreenKeys | "default"]?: PaddingKeys }; 28 | pl?: PaddingKeys | { [k in ScreenKeys | "default"]?: PaddingKeys }; 29 | pr?: PaddingKeys | { [k in ScreenKeys | "default"]?: PaddingKeys }; 30 | px?: PaddingKeys | { [k in ScreenKeys | "default"]?: PaddingKeys }; 31 | py?: PaddingKeys | { [k in ScreenKeys | "default"]?: PaddingKeys }; 32 | color?: ColorKeys | { [k in ScreenKeys | "default"]?: ColorKeys }; 33 | bg?: ColorKeys | { [k in ScreenKeys | "default"]?: ColorKeys }; 34 | w?: WidthKeys | { [k in ScreenKeys | "default"]?: WidthKeys }; 35 | minw?: MinWidthKeys | { [k in ScreenKeys | "default"]?: MinWidthKeys }; 36 | maxw?: MaxWidthKeys | { [k in ScreenKeys | "default"]?: MaxWidthKeys }; 37 | h?: HeightKeys | { [k in ScreenKeys | "default"]?: HeightKeys }; 38 | minh?: MinHeightKeys | { [k in ScreenKeys | "default"]?: MinHeightKeys }; 39 | maxh?: MaxHeightKeys | { [k in ScreenKeys | "default"]?: MaxHeightKeys }; 40 | // Exposed attributes 41 | onclick?: string; 42 | role?: string; 43 | x?: string; 44 | style?: string; 45 | id?: string; 46 | // TODO: These are for svg -> push to a Svg component? 47 | d?: string; 48 | fill?: string; 49 | stroke?: string; 50 | viewBox?: string; 51 | xmlns?: string; 52 | // TODO: sx can be only tailwind classes so constraint to those 53 | sx?: string; 54 | // Shortcut for pure classes 55 | class?: string; 56 | }; 57 | 58 | // https://theme-ui.com/components/box 59 | const Box = (props: BoxProps = {}, children: string[]) => 60 | elements.createElement( 61 | props?.as || "div", 62 | attachAttributes(props), 63 | children.join(""), 64 | ); 65 | 66 | export const displayName = "Box"; 67 | export const Example = () => ( 68 | 75 | Beep 76 | 77 | ); 78 | 79 | export default Box; 80 | 81 | function attachAttributes(props?: {}): elements.Attributes { 82 | if (!props) { 83 | return {}; 84 | } 85 | 86 | const ret: { [key: string]: string } = {}; 87 | 88 | Object.entries(props).forEach(([k, v]) => { 89 | if (k === "as" || k === "sx") { 90 | return; 91 | } 92 | 93 | if (k.split("-").length > 1 || !tailwindKeys.includes(k)) { 94 | ret[k] = v as string; 95 | } 96 | }); 97 | 98 | const klass = constructTailwindClasses(props); 99 | 100 | if (klass) { 101 | ret["class"] = klass; 102 | } 103 | 104 | return ret; 105 | } 106 | -------------------------------------------------------------------------------- /ds/primitives/Button.tsx: -------------------------------------------------------------------------------- 1 | import { elements } from "../../deps.ts"; 2 | import Box from "./Box.tsx"; 3 | import Stack from "./Stack.tsx"; 4 | 5 | type ButtonProps = { onclick?: string; sx?: string }; 6 | type Variant = "primary" | "secondary"; 7 | 8 | // https://tailwindcss.com/components/buttons 9 | const Button = ( 10 | { onclick, sx, variant }: ButtonProps & { variant?: Variant }, 11 | children: string[], 12 | ) => ( 13 | 20 | {children} 21 | 22 | ); 23 | 24 | function getVariantClasses(variant?: Variant) { 25 | const sharedClasses = "font-bold rounded cursor-pointer"; 26 | 27 | switch (variant) { 28 | case "primary": 29 | return `${sharedClasses} bg-primary text-white hover:bg-secondary`; 30 | case "secondary": 31 | return `${sharedClasses} bg-secondary text-white hover:bg-primary`; 32 | } 33 | 34 | return sharedClasses; 35 | } 36 | 37 | export const displayName = "Button"; 38 | export const Example = () => ( 39 | 40 | 41 | 47 | 50 | 53 | 54 | ); 55 | 56 | export default Button; 57 | -------------------------------------------------------------------------------- /ds/primitives/Flex.tsx: -------------------------------------------------------------------------------- 1 | import { elements } from "../../deps.ts"; 2 | import Box, { BoxProps } from "./Box.tsx"; 3 | import type config from "../../tailwind.ts"; 4 | import { convertToClasses, omit } from "./_utils.ts"; 5 | 6 | type Direction = "column" | "row"; 7 | type ScreenKeys = keyof typeof config.screen; 8 | 9 | type FlexProps = BoxProps & { 10 | direction?: Direction | { [k in ScreenKeys | "default"]?: Direction }; 11 | }; 12 | 13 | // https://theme-ui.com/components/flex 14 | const Flex = ( 15 | props: FlexProps = { 16 | direction: "column", 17 | }, 18 | children: string[], 19 | ) => ( 20 | 26 | `${mediaQuery ? mediaQuery + ":" : ""}${prefix}-${ 27 | v === "column" ? "col" : "row" 28 | }`, 29 | )(props?.direction) 30 | } ${(props?.sx && props.sx) || ""}`.trim()} 31 | > 32 | {children.join("")} 33 | 34 | ); 35 | 36 | export const displayName = "Flex"; 37 | export const Example = () => ( 38 | 39 | 40 | 41 | Flex 42 | 43 | 44 | Box 45 | 46 | 47 | 48 | 49 | Flex 50 | 51 | 52 | Box 53 | 54 | 55 | 56 | ); 57 | 58 | export default Flex; 59 | -------------------------------------------------------------------------------- /ds/primitives/Heading.tsx: -------------------------------------------------------------------------------- 1 | import { elements } from "../../deps.ts"; 2 | import Box from "./Box.tsx"; 3 | import Flex from "./Flex.tsx"; 4 | import Link from "./Link.tsx"; 5 | import Text, { TextProps } from "./Text.tsx"; 6 | 7 | type HeadingLevel = 1 | 2 | 3 | 4; 8 | 9 | // https://theme-ui.com/components/heading 10 | // This one is more strict than the reference one and it enforced "as". 11 | const Heading = ( 12 | { 13 | level, 14 | size, 15 | withAnchor, 16 | }: { level: HeadingLevel; size: TextProps["size"]; withAnchor?: boolean }, 17 | children: string[], 18 | ) => 19 | withAnchor 20 | ? ( 21 | 22 | {children} 23 | 24 | ) 25 | : ( 26 | 27 | {children} 28 | 29 | ); 30 | 31 | const ids: { [key: string]: number } = {}; 32 | 33 | /* 34 | TODO: Restore after-hash for links 35 | 36 | .after-hash::after { 37 | content: "#"; 38 | } 39 | */ 40 | 41 | const HeadingWithAnchor = ( 42 | { level, size }: { level: HeadingLevel; size: TextProps["size"] }, 43 | children: string[], 44 | ) => { 45 | let id = slugify(children.join("")); 46 | 47 | if (ids[id]) { 48 | ids[id]++; 49 | 50 | id += `-${ids[id]}`; 51 | } else { 52 | ids[id] = 1; 53 | } 54 | 55 | return ( 56 | 57 | 62 | 68 | 69 | {/* @ts-ignore */} 70 | {children} 71 | 72 | 73 | ); 74 | }; 75 | 76 | const slugify = (idBase: string) => 77 | idBase 78 | .toLowerCase() 79 | .replace(/`/g, "") 80 | .replace(/[^\w]+/g, "-"); 81 | 82 | export const displayName = "Heading"; 83 | export const Example = () => ( 84 | 85 | 86 | h1 heading 87 | 88 | 89 | h2 heading 90 | 91 | 92 | h3 heading 93 | 94 | 95 | h4 heading 96 | 97 | 98 | h4 heading with anchor 99 | 100 | 101 | Responsive heading 102 | 103 | 104 | ); 105 | 106 | export default Heading; 107 | -------------------------------------------------------------------------------- /ds/primitives/Link.tsx: -------------------------------------------------------------------------------- 1 | import { elements } from "../../deps.ts"; 2 | import { constructTailwindClasses } from "./_utils.ts"; 3 | import type config from "../../tailwind.ts"; 4 | import Box from "./Box.tsx"; 5 | import Flex from "./Flex.tsx"; 6 | 7 | type InternalLinks = keyof typeof config.internalLinks; 8 | 9 | export type LinkProps = { href: InternalLinks; sx?: string }; 10 | 11 | const Link = (props: LinkProps, label: string[]) => ( 12 | {label} 13 | ); 14 | 15 | export type ExternalLinkProps = { href: string; sx?: string }; 16 | 17 | const LinkExternal = (props: ExternalLinkProps, label: string[]) => ( 18 | 19 | {label} 20 | 21 | ); 22 | Link.withExternal = LinkExternal; 23 | 24 | export const description = 25 | `Regular Links are meant to be used internally and they are type-checked. The external variant omits type-checking and you should check those links using another tool.`; 26 | export const displayName = "Link"; 27 | export const Example = () => ( 28 | 29 | 30 | Design system 31 | 32 | 33 | 36 | Star at GitHub 37 | 38 | 39 | 40 | ); 41 | 42 | export default Link; 43 | -------------------------------------------------------------------------------- /ds/primitives/Stack.tsx: -------------------------------------------------------------------------------- 1 | import { elements } from "../../deps.ts"; 2 | import Box, { BoxProps } from "./Box.tsx"; 3 | import type config from "../../tailwind.ts"; 4 | import { convertToClasses, omit } from "./_utils.ts"; 5 | 6 | type Direction = "column" | "row"; 7 | type SpacingKeys = keyof typeof config.spacing; 8 | type ScreenKeys = keyof typeof config.screen; 9 | 10 | type StackProps = BoxProps & { 11 | direction?: Direction | { [k in ScreenKeys | "default"]?: Direction }; 12 | spacing?: SpacingKeys | { [k in ScreenKeys | "default"]?: SpacingKeys }; 13 | }; 14 | 15 | const Stack = ( 16 | props: StackProps = { 17 | direction: "column", 18 | }, 19 | children: string[], 20 | ) => ( 21 | 27 | `${mediaQuery ? mediaQuery + ":" : ""}${prefix}-${ 28 | v === "column" ? "col" : "row" 29 | }`, 30 | )(props?.direction) 31 | } ${ 32 | parseSpacingClass( 33 | props?.direction, 34 | props?.spacing, 35 | ) 36 | } ${(props?.sx && props.sx) || ""}`.trim()} 37 | > 38 | {children.join("")} 39 | 40 | ); 41 | 42 | function parseSpacingClass( 43 | direction: StackProps["direction"], 44 | spacing: StackProps["spacing"], 45 | ) { 46 | if (!spacing) { 47 | return ""; 48 | } 49 | 50 | return convertToClasses("space", (mediaQuery, prefix, direction) => { 51 | const klass = `${mediaQuery ? mediaQuery + ":" : ""}${prefix}-${ 52 | direction === "row" ? "x" : "y" 53 | }-${spacing}`; 54 | const inverseClass = `${mediaQuery ? mediaQuery + ":" : ""}${prefix}-${ 55 | direction === "row" ? "y" : "x" 56 | }-0`; 57 | 58 | return `${klass} ${inverseClass}`; 59 | })(direction); 60 | } 61 | 62 | export const displayName = "Stack"; 63 | export const Example = () => ( 64 | 65 | 66 | First 67 | Second 68 | Third 69 | 70 | 76 | First 77 | Second 78 | Third 79 | 80 | 81 | ); 82 | 83 | export default Stack; 84 | -------------------------------------------------------------------------------- /ds/primitives/Text.tsx: -------------------------------------------------------------------------------- 1 | import { elements } from "../../deps.ts"; 2 | import Box, { BoxProps } from "./Box.tsx"; 3 | import type config from "../../tailwind.ts"; 4 | import { convertToClasses, omit } from "./_utils.ts"; 5 | 6 | type FontSize = keyof typeof config.fontSize; 7 | type FontWeight = keyof typeof config.fontWeight; 8 | type ScreenKeys = keyof typeof config.screen; 9 | 10 | export type TextProps = { 11 | as?: BoxProps["as"]; 12 | size?: FontSize | { [k in ScreenKeys | "default"]?: FontSize }; 13 | weight?: FontWeight; 14 | }; 15 | 16 | // TODO: Support responsive syntax 17 | // https://theme-ui.com/components/text 18 | const Text = (props: TextProps, children: string[]) => ( 19 | 27 | {children.join("")} 28 | 29 | ); 30 | 31 | export const description = "Text is a simple typographic primitive."; 32 | export const displayName = "Text"; 33 | export const Example = () => ( 34 | 35 | First 36 | Second 37 | Third 38 | 39 | ); 40 | 41 | export default Text; 42 | -------------------------------------------------------------------------------- /ds/primitives/_utils.ts: -------------------------------------------------------------------------------- 1 | import { ow } from "../../deps.ts"; 2 | 3 | const rules = { 4 | bg: convertToClasses("bg"), 5 | color: convertToClasses("text"), 6 | m: convertToClasses("m"), 7 | mx: convertToClasses("mx"), 8 | my: convertToClasses("my"), 9 | mb: convertToClasses("mb", supportNegative), 10 | mt: convertToClasses("mt", supportNegative), 11 | ml: convertToClasses("ml", supportNegative), 12 | mr: convertToClasses("mr", supportNegative), 13 | p: convertToClasses("p"), 14 | px: convertToClasses("px"), 15 | py: convertToClasses("py"), 16 | pb: convertToClasses("pb"), 17 | pt: convertToClasses("pt"), 18 | pl: convertToClasses("pl"), 19 | pr: convertToClasses("pr"), 20 | w: convertToClasses("w"), 21 | minw: convertToClasses("min-w"), 22 | maxw: convertToClasses("max-w"), 23 | h: convertToClasses("h"), 24 | minh: convertToClasses("min-h"), 25 | maxh: convertToClasses("max-h"), 26 | }; 27 | 28 | function supportNegative( 29 | mediaQuery: string, 30 | prefix: string, 31 | v: string | number, 32 | ) { 33 | return v > 0 34 | ? `${mediaQuery ? mediaQuery + ":" : ""}${prefix}-${v}` 35 | : `-${prefix}-${Math.abs(v as number)}`; 36 | } 37 | 38 | const tailwindKeys = Object.keys(rules); 39 | 40 | function constructTailwindClasses( 41 | props?: { class?: string; sx?: string }, 42 | classes?: string[], 43 | ): string { 44 | if (!props) { 45 | return ""; 46 | } 47 | 48 | const combinedClasses = Object.entries(props) 49 | // @ts-ignore TODO: Figure out how to type this. 50 | .map(([k, v]) => rules[k]?.(v)) 51 | .concat(props.sx ? props.sx.split(" ") : []) 52 | .concat(classes) 53 | .filter(Boolean); 54 | 55 | // Likely Oceanwind should be fine with an empty array 56 | return (combinedClasses.length ? ow([combinedClasses.join(" ")]) : "").concat( 57 | props.class ? " " + props.class.split(" ").join(" ") : "", 58 | ); 59 | } 60 | 61 | function convertToClasses(prefix: string, customizeValue = defaultValue) { 62 | return (value?: any) => { 63 | if (!value) { 64 | return ""; 65 | } 66 | 67 | if (isObject(value)) { 68 | return Object.entries(value).map(([k, v]) => 69 | customizeValue(k === "default" ? "" : k, prefix, v as string) 70 | ); 71 | } 72 | 73 | return customizeValue("", prefix, value); 74 | }; 75 | } 76 | 77 | function defaultValue( 78 | mediaQuery: string, 79 | prefix: string, 80 | value: string | number, 81 | ) { 82 | return `${mediaQuery ? mediaQuery + ":" : ""}${prefix}-${value}`; 83 | } 84 | 85 | const isObject = (a: any) => typeof a === "object"; 86 | 87 | // https://deno.land/x/30_seconds_of_typescript@v1.0.1/docs/omit.md 88 | const omit = (obj: { [key: string]: any }, ...arr: string[]) => 89 | Object.keys(obj) 90 | .filter((k) => !arr.includes(k)) 91 | .reduce( 92 | (acc, key) => ((acc[key] = obj[key]), acc), 93 | {} as { [key: string]: any }, 94 | ); 95 | 96 | export { 97 | constructTailwindClasses, 98 | convertToClasses, 99 | tailwindKeys, 100 | isObject, 101 | omit, 102 | }; 103 | -------------------------------------------------------------------------------- /lib/elements.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | type AttributeValue = number | string | Date | boolean | string[]; 6 | 7 | export interface Children { 8 | children?: AttributeValue; 9 | } 10 | 11 | export interface CustomElementHandler { 12 | (attributes: Attributes & Children, contents: string[]): string; 13 | } 14 | 15 | export interface Attributes { 16 | [key: string]: AttributeValue; 17 | } 18 | 19 | const capitalACharCode = "A".charCodeAt(0); 20 | const capitalZCharCode = "Z".charCodeAt(0); 21 | 22 | const isUpper = (input: string, index: number) => { 23 | const charCode = input.charCodeAt(index); 24 | return capitalACharCode <= charCode && capitalZCharCode >= charCode; 25 | }; 26 | 27 | const toKebabCase = (camelCased: string) => { 28 | let kebabCased = ""; 29 | for (let i = 0; i < camelCased.length; i++) { 30 | const prevUpperCased = i > 0 ? isUpper(camelCased, i - 1) : true; 31 | const currentUpperCased = isUpper(camelCased, i); 32 | const nextUpperCased = i < camelCased.length - 1 33 | ? isUpper(camelCased, i + 1) 34 | : true; 35 | if ( 36 | !prevUpperCased && currentUpperCased || 37 | currentUpperCased && !nextUpperCased 38 | ) { 39 | kebabCased += "-"; 40 | kebabCased += camelCased[i].toLowerCase(); 41 | } else { 42 | kebabCased += camelCased[i]; 43 | } 44 | } 45 | return kebabCased; 46 | }; 47 | 48 | const escapeAttrNodeValue = (value: string) => { 49 | return value.replace(/(&)|(")|(\u00A0)/g, function (_, amp, quote) { 50 | if (amp) return "&"; 51 | if (quote) return """; 52 | return " "; 53 | }); 54 | }; 55 | 56 | const attributeToString = (attributes: Attributes) => 57 | (name: string): string => { 58 | const value = attributes[name]; 59 | const formattedName = toKebabCase(name); 60 | const makeAttribute = (value: string) => `${formattedName}="${value}"`; 61 | if (value instanceof Date) { 62 | return makeAttribute(value.toISOString()); 63 | } else { 64 | switch (typeof value) { 65 | case "boolean": 66 | // https://www.w3.org/TR/2008/WD-html5-20080610/semantics.html#boolean 67 | if (value) { 68 | return formattedName; 69 | } else { 70 | return ""; 71 | } 72 | default: 73 | return makeAttribute(escapeAttrNodeValue(value.toString())); 74 | } 75 | } 76 | }; 77 | 78 | const attributesToString = (attributes: Attributes | undefined): string => { 79 | if (attributes) { 80 | const renderedAttributes = Object.keys(attributes) 81 | .filter((attribute) => attribute !== "children") // filter out children attributes 82 | .map(attributeToString(attributes)) 83 | .filter((attribute) => attribute.length) // filter out negative boolean attributes 84 | .join(" "); 85 | 86 | if (renderedAttributes.length) { 87 | return ` ${renderedAttributes}`; 88 | } 89 | 90 | return ""; 91 | } else { 92 | return ""; 93 | } 94 | }; 95 | 96 | const contentsToString = (contents: any[] | undefined) => { 97 | if (contents) { 98 | return contents 99 | .map((elements) => 100 | Array.isArray(elements) ? elements.join("\n") : elements 101 | ) 102 | .join("\n"); 103 | } else { 104 | return ""; 105 | } 106 | }; 107 | 108 | const isVoidElement = (tagName: string) => { 109 | return [ 110 | "area", 111 | "base", 112 | "br", 113 | "col", 114 | "command", 115 | "embed", 116 | "hr", 117 | "img", 118 | "input", 119 | "keygen", 120 | "link", 121 | "meta", 122 | "param", 123 | "source", 124 | "track", 125 | "wbr", 126 | ].indexOf(tagName) > -1; 127 | }; 128 | 129 | // TODO: Support only React-style children here! 130 | export function createElement( 131 | name: string | CustomElementHandler, 132 | attributes: Attributes & Children | undefined = {}, 133 | ...contents: string[] 134 | ) { 135 | const children = attributes && attributes.children || contents; 136 | 137 | if (typeof name === "function") { 138 | return name(children ? { children, ...attributes } : attributes, contents); 139 | } else { 140 | const tagName = toKebabCase(name); 141 | if (isVoidElement(tagName) && !contents.length) { 142 | return `<${tagName}${attributesToString(attributes)}>`; 143 | } else { 144 | return `<${tagName}${attributesToString(attributes)}>${ 145 | contentsToString(contents) 146 | }`; 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /lib/gentleRpc/jsonRpc2Types.ts: -------------------------------------------------------------------------------- 1 | // https://www.jsonrpc.org/specification 2 | export type JsonRpcVersion = "2.0"; 3 | export type JsonRpcReservedMethod = string; 4 | export type JsonRpcId = number | string | null; 5 | export type JsonRpcParams = JsonArray | JsonObject; 6 | export type JsonRpcMethod = string; 7 | 8 | export interface JsonRpcRequest { 9 | jsonrpc: JsonRpcVersion; 10 | method: JsonRpcMethod; 11 | id?: JsonRpcId; 12 | params?: JsonArray | JsonObject; 13 | } 14 | 15 | export type JsonRpcBatchRequest = JsonRpcRequest[]; 16 | export type JsonRpcBatchResponse = JsonRpcResponse[]; 17 | 18 | export type JsonRpcResponse = JsonRpcSuccess | JsonRpcFailure; 19 | 20 | export interface JsonRpcResponseBasis { 21 | jsonrpc: JsonRpcVersion; 22 | id: JsonRpcId; 23 | } 24 | 25 | export interface JsonRpcSuccess extends JsonRpcResponseBasis { 26 | result: JsonValue; 27 | } 28 | 29 | export interface JsonRpcFailure extends JsonRpcResponseBasis { 30 | error: JsonRpcError; 31 | } 32 | 33 | export interface JsonRpcError { 34 | code: number; 35 | message: string; 36 | data?: JsonValue; 37 | } 38 | 39 | export type JsonPrimitive = string | number | boolean | null; 40 | export type JsonValue = JsonPrimitive | JsonObject | JsonArray; 41 | export type JsonObject = { [member: string]: JsonValue }; 42 | export type JsonArray = JsonValue[]; -------------------------------------------------------------------------------- /lib/gentleRpc/rpcClient.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | JsonRpcRequest, 3 | JsonRpcBatchRequest, 4 | JsonRpcResponseBasis, 5 | JsonRpcSuccess, 6 | JsonRpcFailure, 7 | JsonRpcParams, 8 | JsonRpcMethod, 9 | JsonRpcId, 10 | JsonValue, 11 | } from "./jsonRpc2Types.ts"; 12 | 13 | type Resource = string | URL | Request; 14 | type Options = RequestInit & { 15 | isNotification?: boolean; 16 | id?: JsonRpcId; 17 | handleUnsuccessfulResponse?: (res: Response) => unknown; 18 | }; 19 | type BatchArrayOutput = (JsonValue | BadServerDataError)[]; 20 | type BatchObjectOutput = Record; 21 | type BatchOutput = BatchArrayOutput | BatchObjectOutput; 22 | type BatchArrayInput = [string, JsonRpcParams?][]; 23 | type BatchObjectInput = Record; 24 | type BatchInput = BatchArrayInput | BatchObjectInput; 25 | 26 | class BadServerDataError extends Error { 27 | name: string; 28 | code: number; 29 | data?: unknown; 30 | constructor(message: string, errorCode: number, data?: unknown) { 31 | super(message); 32 | this.name = this.constructor.name; 33 | this.code = errorCode; 34 | this.data = data; 35 | } 36 | } 37 | 38 | function send( 39 | resource: Resource, 40 | fetchInit: RequestInit, 41 | handleUnsuccessfulResponse?: (res: Response) => unknown, 42 | ) { 43 | return fetch(resource, fetchInit) 44 | .then((res: Response) => { 45 | if (res.ok) { 46 | // check if rpc was a notification 47 | return res.text().then((text) => (text ? JSON.parse(text) : undefined)); 48 | } else if (handleUnsuccessfulResponse) { 49 | return handleUnsuccessfulResponse(res); 50 | } else { 51 | return Promise.reject( 52 | new BadServerDataError(`${res.status} ${res.statusText}`, -32001), 53 | ); 54 | } 55 | }) 56 | .catch((err) => 57 | Promise.reject(new BadServerDataError(err.message, -32002)) 58 | ); 59 | } 60 | 61 | function createRpcRequestObj( 62 | methodName: string, 63 | params?: JsonRpcParams, 64 | id?: JsonRpcId, 65 | ): JsonRpcRequest { 66 | const rpcRequestObj: JsonRpcRequest = { 67 | jsonrpc: "2.0", 68 | method: methodName, 69 | }; 70 | if (params) rpcRequestObj.params = params; 71 | if (id !== undefined) rpcRequestObj.id = id; 72 | if (id === null) throw new TypeError("Setting the id to null is not allowed"); 73 | return rpcRequestObj; 74 | } 75 | 76 | function createRpcBatchObj( 77 | batchObj: BatchInput, 78 | isNotification = false, 79 | ): JsonRpcBatchRequest { 80 | return Array.isArray(batchObj) 81 | ? batchObj.map((el) => 82 | createRpcRequestObj( 83 | el[0], 84 | el[1], 85 | isNotification ? undefined : generateID(), 86 | ) 87 | ) 88 | : Object.entries(batchObj).map(([key, value]) => 89 | createRpcRequestObj(value[0], value[1], key) 90 | ); 91 | } 92 | 93 | function generateID(size = 7): string { 94 | const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz"; 95 | for (var str = "", i = 0; i < size; i += 1) { 96 | str += chars[Math.floor(Math.random() * chars.length)]; 97 | } 98 | return str; 99 | } 100 | 101 | function createRemote(resource: Resource, options: Options = {}) { 102 | const handler = { 103 | get(client: Client, name: JsonRpcMethod) { 104 | if ((client as any)[name] !== undefined) { 105 | return client[name as keyof Client]; 106 | } else { 107 | return (...args: JsonRpcParams[]) => 108 | client.makeRpcCall( 109 | JSON.stringify( 110 | createRpcRequestObj( 111 | name, 112 | args, 113 | options.isNotification ? undefined : options.id || generateID(), 114 | ), 115 | ), 116 | ); 117 | } 118 | }, 119 | }; 120 | const client = new Client( 121 | resource, 122 | options, 123 | options.handleUnsuccessfulResponse, 124 | ); 125 | return new Proxy(client, handler); 126 | } 127 | 128 | class Client { 129 | private url: Resource; 130 | private fetchInit: RequestInit; 131 | private isNotification = false; 132 | private handleUnsuccessfulResponse?: (res: Response) => unknown; 133 | [key: string]: unknown // necessary for es6 proxy 134 | constructor( 135 | url: Resource, 136 | options: Options = {}, 137 | handleUnsuccessfulResponse?: (res: Response) => unknown, 138 | ) { 139 | this.url = url; 140 | this.isNotification = options.isNotification || false; 141 | this.handleUnsuccessfulResponse = handleUnsuccessfulResponse; 142 | this.fetchInit = { 143 | ...options, 144 | method: "POST", 145 | headers: { ...options.headers, "Content-Type": "application/json" }, 146 | }; 147 | } 148 | 149 | async batch(batchObj: BatchArrayInput): Promise; 150 | async batch(batchObj: BatchObjectInput): Promise>; 151 | async batch(batchObj: BatchInput): Promise { 152 | if (Array.isArray(batchObj)) { 153 | const result = (await this.makeRpcCall( 154 | JSON.stringify(createRpcBatchObj(batchObj, this.isNotification)), 155 | Array.isArray(batchObj), 156 | )) as BatchArrayOutput | undefined; 157 | if (result instanceof BadServerDataError) return Promise.reject(result); 158 | else if (result === undefined || batchObj.length !== result.length) { 159 | return Promise.reject( 160 | new BadServerDataError("Something went wrong", -32004, result), 161 | ); 162 | } else if (result.find((el) => el instanceof BadServerDataError)) { 163 | return Promise.reject( 164 | result.find((el) => el instanceof BadServerDataError), 165 | ); 166 | } else return result; 167 | } else { 168 | const result = (await this.makeRpcCall( 169 | JSON.stringify(createRpcBatchObj(batchObj, this.isNotification)), 170 | Array.isArray(batchObj), 171 | )) as BatchObjectOutput | undefined; 172 | if (result instanceof BadServerDataError) return Promise.reject(result); 173 | else if ( 174 | result === undefined || 175 | Object.keys(batchObj).length !== Object.keys(result).length 176 | ) { 177 | return Promise.reject( 178 | new BadServerDataError("Something went wrong", -32004, result), 179 | ); 180 | } else if ( 181 | Object.values(result).find((el) => el instanceof BadServerDataError) 182 | ) { 183 | return Promise.reject( 184 | Object.values(result).find((el) => el instanceof BadServerDataError), 185 | ); 186 | } else return result; 187 | } 188 | } 189 | 190 | async makeRpcCall( 191 | stringifiedRpcRequestObj: string, 192 | shouldReturnBatchResultsAsArray = true, 193 | ): Promise { 194 | const rpcResponse = (await send( 195 | this.url, 196 | { 197 | ...this.fetchInit, 198 | body: stringifiedRpcRequestObj, 199 | }, 200 | this.handleUnsuccessfulResponse, 201 | )) as JsonValue | undefined; 202 | const result = rpcResponse === undefined 203 | ? undefined 204 | : this.handleResponseData(rpcResponse, shouldReturnBatchResultsAsArray); 205 | return result instanceof BadServerDataError 206 | ? Promise.reject(result) 207 | : result; 208 | } 209 | 210 | // public for tests 211 | handleResponseData( 212 | rpcResponseObjOrBatch: JsonValue, 213 | shouldReturnBatchResultsAsArray = true, 214 | ): JsonValue | BadServerDataError | BatchOutput { 215 | if (Array.isArray(rpcResponseObjOrBatch)) { 216 | return shouldReturnBatchResultsAsArray 217 | ? this.returnBatchAsArray(rpcResponseObjOrBatch) 218 | : this.returnBatchAsObject(rpcResponseObjOrBatch); 219 | } else { 220 | return this.validateRpcResponseObj(rpcResponseObjOrBatch); 221 | } 222 | } 223 | 224 | private returnBatchAsArray(rpcResponseBatch: JsonValue[]): BatchArrayOutput { 225 | return rpcResponseBatch.reduce<(JsonValue | BadServerDataError)[]>( 226 | (acc, rpcResponseObj) => { 227 | acc.push(this.validateRpcResponseObj(rpcResponseObj)); 228 | return acc; 229 | }, 230 | [], 231 | ); 232 | } 233 | 234 | private returnBatchAsObject( 235 | rpcResponseBatch: JsonValue[], 236 | ): BatchObjectOutput { 237 | return rpcResponseBatch.reduce((acc, rpcResponseObj) => { 238 | if ( 239 | this.validateJsonRpcBasis(rpcResponseObj) && 240 | rpcResponseObj.id !== null 241 | ) { 242 | acc[rpcResponseObj.id] = this.validateRpcResponseObj(rpcResponseObj); 243 | return acc; 244 | } else { 245 | // id might be null if an error occured on server side 246 | acc["null"] = this.validateRpcResponseObj(rpcResponseObj); 247 | return acc; 248 | } 249 | }, {}); 250 | } 251 | private isObject(obj: unknown): obj is object { 252 | return ( 253 | obj !== null && typeof obj === "object" && Array.isArray(obj) === false 254 | ); 255 | } 256 | 257 | private hasProperty( 258 | key: K, 259 | x: object, 260 | ): x is { [key in K]: unknown } { 261 | return key in x; 262 | } 263 | private validateJsonRpcBasis(data: unknown): data is JsonRpcResponseBasis { 264 | return ( 265 | this.isObject(data) && 266 | this.hasProperty("jsonrpc", data) && 267 | data.jsonrpc === "2.0" && 268 | this.hasProperty("id", data) && 269 | (typeof data.id === "number" || 270 | typeof data.id === "string" || 271 | data.id === null) 272 | ); 273 | } 274 | private validateJsonRpcSuccess( 275 | data: JsonRpcResponseBasis, 276 | ): data is JsonRpcSuccess { 277 | return this.hasProperty("result", data); 278 | } 279 | private validateJsonRpcFailure( 280 | data: JsonRpcResponseBasis, 281 | ): data is JsonRpcFailure { 282 | return ( 283 | this.hasProperty("error", data) && 284 | this.isObject(data.error) && 285 | this.hasProperty("code", data.error) && 286 | typeof data.error.code === "number" && 287 | this.hasProperty("message", data.error) && 288 | typeof data.error.message === "string" 289 | ); 290 | } 291 | 292 | private validateRpcResponseObj( 293 | data: JsonValue, 294 | ): JsonValue | BadServerDataError { 295 | if (this.validateJsonRpcBasis(data)) { 296 | if (this.validateJsonRpcSuccess(data)) return data.result; 297 | else if (this.validateJsonRpcFailure(data)) { 298 | return new BadServerDataError( 299 | data.error.message, 300 | data.error.code, 301 | data.error.data, 302 | ); 303 | } 304 | } 305 | return new BadServerDataError( 306 | "Received data is no RPC response object.", 307 | -32003, 308 | ); 309 | } 310 | } 311 | 312 | export { 313 | createRemote, 314 | send, 315 | createRpcRequestObj, 316 | createRpcBatchObj, 317 | Client, 318 | BadServerDataError, 319 | }; 320 | -------------------------------------------------------------------------------- /lib/jsx/element-types.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace JSX { 2 | interface HtmlTag { 3 | accesskey?: string; 4 | class?: string; 5 | contenteditable?: string; 6 | dir?: string; 7 | hidden?: string | boolean; 8 | id?: string; 9 | role?: string; 10 | lang?: string; 11 | draggable?: string | boolean; 12 | spellcheck?: string | boolean; 13 | style?: string; 14 | tabindex?: string; 15 | title?: string; 16 | translate?: string | boolean; 17 | 18 | // Custom 19 | x?: string; 20 | } 21 | interface HtmlAnchorTag extends HtmlTag { 22 | href?: string; 23 | target?: string; 24 | download?: string; 25 | ping?: string; 26 | rel?: string; 27 | media?: string; 28 | hreflang?: string; 29 | type?: string; 30 | } 31 | interface HtmlAreaTag extends HtmlTag { 32 | alt?: string; 33 | coords?: string; 34 | shape?: string; 35 | href?: string; 36 | target?: string; 37 | ping?: string; 38 | rel?: string; 39 | media?: string; 40 | hreflang?: string; 41 | type?: string; 42 | } 43 | interface HtmlAudioTag extends HtmlTag { 44 | src?: string; 45 | autobuffer?: string; 46 | autoplay?: string; 47 | loop?: string; 48 | controls?: string; 49 | } 50 | interface BaseTag extends HtmlTag { 51 | href?: string; 52 | target?: string; 53 | } 54 | interface HtmlQuoteTag extends HtmlTag { 55 | cite?: string; 56 | } 57 | interface HtmlBodyTag extends HtmlTag {} 58 | interface HtmlButtonTag extends HtmlTag { 59 | action?: string; 60 | autofocus?: string; 61 | disabled?: string; 62 | enctype?: string; 63 | form?: string; 64 | method?: string; 65 | name?: string; 66 | novalidate?: string | boolean; 67 | target?: string; 68 | type?: string; 69 | value?: string; 70 | } 71 | interface HtmlDataListTag extends HtmlTag {} 72 | interface HtmlCanvasTag extends HtmlTag { 73 | width?: string; 74 | height?: string; 75 | } 76 | interface HtmlTableColTag extends HtmlTag { 77 | span?: string; 78 | } 79 | interface HtmlTableSectionTag extends HtmlTag {} 80 | interface HtmlTableRowTag extends HtmlTag {} 81 | interface DataTag extends HtmlTag { 82 | value?: string; 83 | } 84 | interface HtmlEmbedTag extends HtmlTag { 85 | src?: string; 86 | type?: string; 87 | width?: string; 88 | height?: string; 89 | [anything: string]: string | boolean | undefined; 90 | } 91 | interface HtmlFieldSetTag extends HtmlTag { 92 | disabled?: string; 93 | form?: string; 94 | name?: string; 95 | } 96 | interface HtmlFormTag extends HtmlTag { 97 | acceptCharset?: string; 98 | action?: string; 99 | autocomplete?: string; 100 | enctype?: string; 101 | method?: string; 102 | name?: string; 103 | novalidate?: string | boolean; 104 | target?: string; 105 | } 106 | interface HtmlHtmlTag extends HtmlTag { 107 | manifest?: string; 108 | } 109 | interface HtmlIFrameTag extends HtmlTag { 110 | src?: string; 111 | srcdoc?: string; 112 | name?: string; 113 | sandbox?: string; 114 | seamless?: string; 115 | width?: string; 116 | height?: string; 117 | } 118 | interface HtmlImageTag extends HtmlTag { 119 | alt?: string; 120 | src?: string; 121 | crossorigin?: string; 122 | usemap?: string; 123 | ismap?: string; 124 | width?: string; 125 | height?: string; 126 | } 127 | interface HtmlInputTag extends HtmlTag { 128 | accept?: string; 129 | action?: string; 130 | alt?: string; 131 | autocomplete?: string; 132 | autofocus?: string; 133 | checked?: string | boolean; 134 | disabled?: string | boolean; 135 | enctype?: string; 136 | form?: string; 137 | height?: string; 138 | list?: string; 139 | max?: string; 140 | maxlength?: string; 141 | method?: string; 142 | min?: string; 143 | multiple?: string; 144 | name?: string; 145 | novalidate?: string | boolean; 146 | pattern?: string; 147 | placeholder?: string; 148 | readonly?: string; 149 | required?: string; 150 | size?: string; 151 | src?: string; 152 | step?: string; 153 | target?: string; 154 | type?: string; 155 | value?: string; 156 | width?: string; 157 | } 158 | interface HtmlModTag extends HtmlTag { 159 | cite?: string; 160 | datetime?: string | Date; 161 | } 162 | interface KeygenTag extends HtmlTag { 163 | autofocus?: string; 164 | challenge?: string; 165 | disabled?: string; 166 | form?: string; 167 | keytype?: string; 168 | name?: string; 169 | } 170 | interface HtmlLabelTag extends HtmlTag { 171 | form?: string; 172 | for?: string; 173 | } 174 | interface HtmlLITag extends HtmlTag { 175 | value?: string | number; 176 | } 177 | interface HtmlLinkTag extends HtmlTag { 178 | href?: string; 179 | crossorigin?: string; 180 | rel?: string; 181 | media?: string; 182 | hreflang?: string; 183 | type?: string; 184 | sizes?: string; 185 | integrity?: string; 186 | } 187 | interface HtmlMapTag extends HtmlTag { 188 | name?: string; 189 | } 190 | interface HtmlMetaTag extends HtmlTag { 191 | name?: string; 192 | httpEquiv?: string; 193 | content?: string; 194 | charset?: string; 195 | } 196 | interface HtmlMeterTag extends HtmlTag { 197 | value?: string | number; 198 | min?: string | number; 199 | max?: string | number; 200 | low?: string | number; 201 | high?: string | number; 202 | optimum?: string | number; 203 | } 204 | interface HtmlObjectTag extends HtmlTag { 205 | data?: string; 206 | type?: string; 207 | name?: string; 208 | usemap?: string; 209 | form?: string; 210 | width?: string; 211 | height?: string; 212 | } 213 | interface HtmlOListTag extends HtmlTag { 214 | reversed?: string; 215 | start?: string | number; 216 | } 217 | interface HtmlOptgroupTag extends HtmlTag { 218 | disabled?: string; 219 | label?: string; 220 | } 221 | interface HtmlOptionTag extends HtmlTag { 222 | disabled?: string; 223 | label?: string; 224 | selected?: string; 225 | value?: string; 226 | } 227 | interface HtmlOutputTag extends HtmlTag { 228 | for?: string; 229 | form?: string; 230 | name?: string; 231 | } 232 | interface HtmlParamTag extends HtmlTag { 233 | name?: string; 234 | value?: string; 235 | } 236 | interface HtmlProgressTag extends HtmlTag { 237 | value?: string | number; 238 | max?: string | number; 239 | } 240 | interface HtmlCommandTag extends HtmlTag { 241 | type?: string; 242 | label?: string; 243 | icon?: string; 244 | disabled?: string; 245 | checked?: string; 246 | radiogroup?: string; 247 | default?: string; 248 | } 249 | interface HtmlLegendTag extends HtmlTag {} 250 | interface HtmlBrowserButtonTag extends HtmlTag { 251 | type?: string; 252 | } 253 | interface HtmlMenuTag extends HtmlTag { 254 | type?: string; 255 | label?: string; 256 | } 257 | interface HtmlScriptTag extends HtmlTag { 258 | src?: string; 259 | type?: string; 260 | charset?: string; 261 | async?: string; 262 | defer?: string; 263 | crossorigin?: string; 264 | integrity?: string; 265 | text?: string; 266 | } 267 | interface HtmlDetailsTag extends HtmlTag { 268 | open?: string; 269 | } 270 | interface HtmlSelectTag extends HtmlTag { 271 | autofocus?: string; 272 | disabled?: string; 273 | form?: string; 274 | multiple?: string; 275 | name?: string; 276 | required?: string; 277 | size?: string; 278 | } 279 | interface HtmlSourceTag extends HtmlTag { 280 | src?: string; 281 | type?: string; 282 | media?: string; 283 | } 284 | interface HtmlStyleTag extends HtmlTag { 285 | media?: string; 286 | type?: string; 287 | disabled?: string; 288 | scoped?: string; 289 | } 290 | interface HtmlTableTag extends HtmlTag {} 291 | interface HtmlTableDataCellTag extends HtmlTag { 292 | colspan?: string | number; 293 | rowspan?: string | number; 294 | headers?: string; 295 | } 296 | interface HtmlTextAreaTag extends HtmlTag { 297 | autofocus?: string; 298 | cols?: string; 299 | dirname?: string; 300 | disabled?: string; 301 | form?: string; 302 | maxlength?: string; 303 | minlength?: string; 304 | name?: string; 305 | placeholder?: string; 306 | readonly?: string; 307 | required?: string; 308 | rows?: string; 309 | wrap?: string; 310 | oninput?: string; 311 | autocapitalize?: string; 312 | autocomplete?: string; 313 | autocorrect?: string; 314 | } 315 | interface HtmlTableHeaderCellTag extends HtmlTag { 316 | colspan?: string | number; 317 | rowspan?: string | number; 318 | headers?: string; 319 | scope?: string; 320 | } 321 | interface HtmlTimeTag extends HtmlTag { 322 | datetime?: string | Date; 323 | } 324 | interface HtmlTrackTag extends HtmlTag { 325 | default?: string; 326 | kind?: string; 327 | label?: string; 328 | src?: string; 329 | srclang?: string; 330 | } 331 | interface HtmlVideoTag extends HtmlTag { 332 | src?: string; 333 | poster?: string; 334 | autobuffer?: string; 335 | autoplay?: string; 336 | loop?: string; 337 | controls?: string; 338 | width?: string; 339 | height?: string; 340 | } 341 | 342 | // Custom 343 | interface Path { 344 | d: string; 345 | } 346 | interface Svg { 347 | class?: string; 348 | role?: string; 349 | width?: string; 350 | height?: string; 351 | fill?: string; 352 | stroke?: string; 353 | xmlns: string; 354 | viewBox: string; 355 | } 356 | interface IntrinsicElements { 357 | path: Path; 358 | svg: Svg; 359 | } 360 | } 361 | //# sourceMappingURL=element-types.d.ts.map 362 | -------------------------------------------------------------------------------- /lib/jsx/events.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace JSX { 2 | interface HtmlBodyTag { 3 | onafterprint?: string; 4 | onbeforeprint?: string; 5 | onbeforeonload?: string; 6 | onblur?: string; 7 | onerror?: string; 8 | onfocus?: string; 9 | onhaschange?: string; 10 | onload?: string; 11 | onmessage?: string; 12 | onoffline?: string; 13 | ononline?: string; 14 | onpagehide?: string; 15 | onpageshow?: string; 16 | onpopstate?: string; 17 | onredo?: string; 18 | onresize?: string; 19 | onstorage?: string; 20 | onundo?: string; 21 | onunload?: string; 22 | } 23 | interface HtmlTag { 24 | oncontextmenu?: string; 25 | onkeydown?: string; 26 | onkeypress?: string; 27 | onkeyup?: string; 28 | onclick?: string; 29 | ondblclick?: string; 30 | ondrag?: string; 31 | ondragend?: string; 32 | ondragenter?: string; 33 | ondragleave?: string; 34 | ondragover?: string; 35 | ondragstart?: string; 36 | ondrop?: string; 37 | onmousedown?: string; 38 | onmousemove?: string; 39 | onmouseout?: string; 40 | onmouseover?: string; 41 | onmouseup?: string; 42 | onmousewheel?: string; 43 | onscroll?: string; 44 | } 45 | interface FormEvents { 46 | onblur?: string; 47 | onchange?: string; 48 | onfocus?: string; 49 | onformchange?: string; 50 | onforminput?: string; 51 | oninput?: string; 52 | oninvalid?: string; 53 | onselect?: string; 54 | onsubmit?: string; 55 | } 56 | interface HtmlInputTag extends FormEvents { 57 | } 58 | interface HtmlFieldSetTag extends FormEvents { 59 | } 60 | interface HtmlFormTag extends FormEvents { 61 | } 62 | interface MediaEvents { 63 | onabort?: string; 64 | oncanplay?: string; 65 | oncanplaythrough?: string; 66 | ondurationchange?: string; 67 | onemptied?: string; 68 | onended?: string; 69 | onerror?: string; 70 | onloadeddata?: string; 71 | onloadedmetadata?: string; 72 | onloadstart?: string; 73 | onpause?: string; 74 | onplay?: string; 75 | onplaying?: string; 76 | onprogress?: string; 77 | onratechange?: string; 78 | onreadystatechange?: string; 79 | onseeked?: string; 80 | onseeking?: string; 81 | onstalled?: string; 82 | onsuspend?: string; 83 | ontimeupdate?: string; 84 | onvolumechange?: string; 85 | onwaiting?: string; 86 | } 87 | interface HtmlAudioTag extends MediaEvents { 88 | } 89 | interface HtmlEmbedTag extends MediaEvents { 90 | } 91 | interface HtmlImageTag extends MediaEvents { 92 | } 93 | interface HtmlObjectTag extends MediaEvents { 94 | } 95 | interface HtmlVideoTag extends MediaEvents { 96 | } 97 | } 98 | //# sourceMappingURL=events.d.ts.map 99 | -------------------------------------------------------------------------------- /lib/jsx/intrinsic-elements.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace JSX { 2 | type Element = string; 3 | interface IntrinsicElements { 4 | a: HtmlAnchorTag; 5 | abbr: HtmlTag; 6 | address: HtmlTag; 7 | area: HtmlAreaTag; 8 | article: HtmlTag; 9 | aside: HtmlTag; 10 | audio: HtmlAudioTag; 11 | b: HtmlTag; 12 | bb: HtmlBrowserButtonTag; 13 | base: BaseTag; 14 | bdi: HtmlTag; 15 | bdo: HtmlTag; 16 | blockquote: HtmlQuoteTag; 17 | body: HtmlBodyTag; 18 | br: HtmlTag; 19 | button: HtmlButtonTag; 20 | canvas: HtmlCanvasTag; 21 | caption: HtmlTag; 22 | cite: HtmlTag; 23 | code: HtmlTag; 24 | col: HtmlTableColTag; 25 | colgroup: HtmlTableColTag; 26 | commands: HtmlCommandTag; 27 | data: DataTag; 28 | datalist: HtmlDataListTag; 29 | dd: HtmlTag; 30 | del: HtmlModTag; 31 | details: HtmlDetailsTag; 32 | dfn: HtmlTag; 33 | div: HtmlTag; 34 | dl: HtmlTag; 35 | dt: HtmlTag; 36 | em: HtmlTag; 37 | embed: HtmlEmbedTag; 38 | fieldset: HtmlFieldSetTag; 39 | figcaption: HtmlTag; 40 | figure: HtmlTag; 41 | footer: HtmlTag; 42 | form: HtmlFormTag; 43 | h1: HtmlTag; 44 | h2: HtmlTag; 45 | h3: HtmlTag; 46 | h4: HtmlTag; 47 | h5: HtmlTag; 48 | h6: HtmlTag; 49 | head: HtmlTag; 50 | header: HtmlTag; 51 | hr: HtmlTag; 52 | html: HtmlHtmlTag; 53 | i: HtmlTag; 54 | iframe: HtmlIFrameTag; 55 | img: HtmlImageTag; 56 | input: HtmlInputTag; 57 | ins: HtmlModTag; 58 | kbd: HtmlTag; 59 | keygen: KeygenTag; 60 | label: HtmlLabelTag; 61 | legend: HtmlLegendTag; 62 | li: HtmlLITag; 63 | link: HtmlLinkTag; 64 | main: HtmlTag; 65 | map: HtmlMapTag; 66 | mark: HtmlTag; 67 | menu: HtmlMenuTag; 68 | meta: HtmlMetaTag; 69 | meter: HtmlMeterTag; 70 | nav: HtmlTag; 71 | noscript: HtmlTag; 72 | object: HtmlObjectTag; 73 | ol: HtmlOListTag; 74 | optgroup: HtmlOptgroupTag; 75 | option: HtmlOptionTag; 76 | output: HtmlOutputTag; 77 | p: HtmlTag; 78 | param: HtmlParamTag; 79 | pre: HtmlTag; 80 | progress: HtmlProgressTag; 81 | q: HtmlQuoteTag; 82 | rb: HtmlTag; 83 | rp: HtmlTag; 84 | rt: HtmlTag; 85 | rtc: HtmlTag; 86 | ruby: HtmlTag; 87 | s: HtmlTag; 88 | samp: HtmlTag; 89 | script: HtmlScriptTag; 90 | section: HtmlTag; 91 | select: HtmlSelectTag; 92 | small: HtmlTag; 93 | source: HtmlSourceTag; 94 | span: HtmlTag; 95 | strong: HtmlTag; 96 | style: HtmlStyleTag; 97 | sub: HtmlTag; 98 | sup: HtmlTag; 99 | table: HtmlTableTag; 100 | tbody: HtmlTag; 101 | td: HtmlTableDataCellTag; 102 | template: HtmlTag; 103 | textarea: HtmlTextAreaTag; 104 | tfoot: HtmlTableSectionTag; 105 | th: HtmlTableHeaderCellTag; 106 | thead: HtmlTableSectionTag; 107 | time: HtmlTimeTag; 108 | title: HtmlTag; 109 | tr: HtmlTableRowTag; 110 | track: HtmlTrackTag; 111 | u: HtmlTag; 112 | ul: HtmlTag; 113 | var: HtmlTag; 114 | video: HtmlVideoTag; 115 | wbr: HtmlTag; 116 | } 117 | } 118 | //# sourceMappingURL=intrinsic-elements.d.ts.map 119 | -------------------------------------------------------------------------------- /lib/prism/mod.ts: -------------------------------------------------------------------------------- 1 | import './prism.js'; 2 | 3 | const Prism = (window as any).Prism; 4 | export default Prism; -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | import { getStyleInjector, getStyleTag, Application } from "./deps.ts"; 2 | import getUrls from "./utils/get-urls.ts"; 3 | import getPages from "./utils/get-pages.ts"; 4 | import watchDirectories from "./utils/watch-directories.ts"; 5 | import { getWebsocketServer, websocketClient } from "./utils/web-sockets.ts"; 6 | import type { Pages, Page } from "./types.ts"; 7 | 8 | async function serve(port: number) { 9 | const app = new Application(); 10 | const pageContext: { 11 | _pages: Pages; 12 | init: () => void; 13 | getPage: (url: string) => Page; 14 | } = { 15 | _pages: {}, 16 | init: async function () { 17 | this._pages = await getPages(await getUrls()); 18 | }, 19 | getPage: function (url: string) { 20 | return this._pages[url]; 21 | }, 22 | }; 23 | await pageContext.init(); 24 | 25 | const wss = getWebsocketServer(); 26 | 27 | app.use(async (context) => { 28 | const url = context.request.url.pathname; 29 | const page = pageContext.getPage(url); 30 | 31 | if (!page) { 32 | // favicon and others fall here 33 | context.response.status = 404; 34 | 35 | return; 36 | } 37 | 38 | try { 39 | const injector = getStyleInjector(); 40 | 41 | const { module: { default: component }, pages, attributes } = page; 42 | 43 | const pageHtml = await Promise.resolve( 44 | component({ url, pages, attributes }), 45 | ); 46 | 47 | // @ts-ignore: TODO: Drop default in favor of simpler composition? 48 | const { title, meta } = component; 49 | 50 | const styleTag = getStyleTag(injector); 51 | 52 | context.response.headers.set("Content-Type", "text/html; charset=UTF-8"); 53 | context.response.body = new TextEncoder().encode(` 54 | 55 | 56 | ${title || ""} 57 | ${generateMeta(meta)} 58 | 59 | 60 | 61 | 62 | 63 | ${styleTag} 64 | 65 | 66 | ${pageHtml} 67 | 68 | 69 | 70 | `); 71 | } catch (err) { 72 | console.error(err); 73 | 74 | context.response.body = new TextEncoder().encode(err.stack); 75 | } 76 | }); 77 | 78 | console.log(`Serving at http://127.0.0.1:${port}`); 79 | app.listen({ port }); 80 | 81 | // TODO: Drop this as denon handles file watching - all we need to do 82 | // is to make the client reconnect and the force a refresh on reconnection 83 | watchDirectories( 84 | // Directories have to be relative to cwd 85 | // https://github.com/denoland/deno/issues/5742 86 | ["./ds", "./pages"], 87 | async () => { 88 | await pageContext.init(); 89 | 90 | wss.clients.forEach((socket) => { 91 | // 1 for open, https://developer.mozilla.org/en-US/docs/Web/API/WebSocket 92 | if (socket.state === 1) { 93 | console.log("watchDirectories - Refresh ws"); 94 | 95 | socket.send("refresh"); 96 | } 97 | }); 98 | }, 99 | ); 100 | } 101 | 102 | function generateMeta(meta?: { [key: string]: string }) { 103 | if (!meta) { 104 | return ""; 105 | } 106 | 107 | return Object.entries(meta).map(([key, value]) => 108 | `` 109 | ).join("\n"); 110 | } 111 | 112 | // TODO: Make port configurable 113 | const port = 3000; 114 | 115 | // Only development mode for now 116 | serve(port); 117 | -------------------------------------------------------------------------------- /pages/_page.ts: -------------------------------------------------------------------------------- 1 | console.log("Hello from index"); 2 | -------------------------------------------------------------------------------- /pages/blog/_pages.ts: -------------------------------------------------------------------------------- 1 | import { 2 | expandGlobSync, 3 | joinPath, 4 | processMarkdown, 5 | } from "../../deps.ts"; 6 | import BlogPageLayout from "../../ds/layouts/BlogPage.tsx"; 7 | import type { BlogPage } from "../../ds/layouts/BlogPage.tsx"; 8 | 9 | function getPages() { 10 | const ret: BlogPage[] = []; 11 | 12 | for ( 13 | const file of expandGlobSync( 14 | joinPath(Deno.cwd(), "data/blog/**/*.md"), 15 | ) 16 | ) { 17 | const page = processMarkdown(Deno.readTextFileSync(file.path)); 18 | 19 | ret.push({ ...page, url: page.meta.slug }); 20 | } 21 | 22 | return ret; 23 | } 24 | 25 | export { 26 | getPages, 27 | BlogPageLayout as layout, 28 | }; 29 | -------------------------------------------------------------------------------- /pages/blog/index.tsx: -------------------------------------------------------------------------------- 1 | import { elements } from "../../deps.ts"; 2 | import BlogIndexLayout from "../../ds/layouts/BlogIndex.tsx"; 3 | import type { BlogPage } from "../../ds/layouts/BlogPage.tsx"; 4 | 5 | // TODO: Figure out a good way to handle page typing (needs a generic) 6 | const BlogIndexPage = (props: { url: string; pages: BlogPage[] }) => ( 7 | 8 | ); 9 | 10 | BlogIndexPage.title = "Blog"; 11 | BlogIndexPage.meta = { 12 | description: "Blog index", 13 | }; 14 | 15 | export default BlogIndexPage; 16 | -------------------------------------------------------------------------------- /pages/design-system/Collection.tsx: -------------------------------------------------------------------------------- 1 | import { elements } from "../../deps.ts"; 2 | import type { Component } from "../../types.ts"; 3 | import { CodeContainer, CodeEditor } from "../../ds/patterns/CodeEditor.tsx"; 4 | import Box from "../../ds/primitives/Box.tsx"; 5 | import Stack from "../../ds/primitives/Stack.tsx"; 6 | import Heading from "../../ds/primitives/Heading.tsx"; 7 | import { 8 | Tabs, 9 | TabHeader, 10 | TabHeaderItem, 11 | TabBody, 12 | TabBodyItem, 13 | } from "../../ds/patterns/Tabs.tsx"; 14 | 15 | // TODO: Restore 16 | // import evaluateCode from "./evaluate-code.ts"; 17 | import Types from "./types.tsx"; 18 | 19 | const Collection = ({ items }: { items: Component[] }) => { 20 | const componentSources = getComponentSources(items); 21 | 22 | return items 23 | .map( 24 | ({ displayName, description, exampleSource, componentSource, props }) => ( 25 | 26 | 27 | 28 | {displayName} 29 | 30 | {description ? description : ""} 31 | 32 | 33 | 34 | Example source 35 | 36 | {componentSource 37 | ? ( 38 | 39 | Component source 40 | 41 | ) 42 | : ( 43 | "" 44 | )} 45 | {props?.length > 0 46 | ? ( 47 | Props 48 | ) 49 | : ( 50 | "" 51 | )} 52 | 53 | 54 | 55 | 60 | 61 | 62 | 67 | 68 | {props?.length > 0 69 | ? ( 70 | 71 | 72 | 73 | ) 74 | : ( 75 | "" 76 | )} 77 | 78 | 79 | {/* TODO: Add a fallback (evaluate code) here to work progressively */} 80 | 86 | {/*evaluateCode(componentSources, exampleSource, displayName)*/} 87 | 88 | 89 | 90 | ), 91 | ) 92 | .join(""); 93 | }; 94 | 95 | function getComponentSources(items: Component[]) { 96 | const ret: { [key: string]: () => string } = {}; 97 | 98 | items.forEach(({ displayName, default: def }) => { 99 | ret[displayName] = def; 100 | }); 101 | 102 | return ret; 103 | } 104 | 105 | export default Collection; 106 | -------------------------------------------------------------------------------- /pages/design-system/Colors.tsx: -------------------------------------------------------------------------------- 1 | import { elements } from "../../deps.ts"; 2 | import Box from "../../ds/primitives/Box.tsx"; 3 | import Flex from "../../ds/primitives/Flex.tsx"; 4 | 5 | // TODO: Replace with a standalone implementation 6 | // import { readableColor } from "https://unpkg.com/polished@3.6.6/dist/polished.cjs.js"; 7 | 8 | // TODO: Figure out how to handle polymorphism in TS 9 | // Likely this one is easier to solve against extendedColors 10 | const Colors = ({ 11 | items, 12 | parent, 13 | }: { 14 | items: { [key: string]: string | { [key: string]: string } }; 15 | parent?: string; 16 | }) => 17 | Object.entries(items) 18 | .map(([key, color]) => 19 | isObject(color) 20 | ? ( 21 | 22 | 23 | {key} 24 | 25 | {/* @ts-ignore */} 26 | 27 | 28 | ) 29 | : ( 30 | 36 | {key} 37 | 38 | ) 39 | ) 40 | .join(""); 41 | 42 | // TODO: Consume from _utils 43 | const isObject = (a: unknown) => typeof a === "object"; 44 | 45 | const getComplementary = (color: string) => 46 | tryTo(() => readableColor(color), "#000"); 47 | 48 | const readableColor = (color: string) => color; 49 | 50 | function tryTo(fn: () => unknown, defaultValue: string) { 51 | try { 52 | return fn(); 53 | } catch (err) { 54 | return defaultValue; 55 | } 56 | } 57 | 58 | export default Colors; 59 | -------------------------------------------------------------------------------- /pages/design-system/SpacingScale.tsx: -------------------------------------------------------------------------------- 1 | import { elements } from "../../deps.ts"; 2 | import Box from "../../ds/primitives/Box.tsx"; 3 | 4 | const SpacingScale = ({ items }: { items: string[] }) => 5 | items 6 | .map((key) => ( 7 | 8 | {key} 9 | 10 | )) 11 | .join(""); 12 | 13 | export default SpacingScale; 14 | -------------------------------------------------------------------------------- /pages/design-system/Types.tsx: -------------------------------------------------------------------------------- 1 | import { elements } from "../../deps.ts"; 2 | import { 3 | Table, 4 | TableHeader, 5 | TableHeaderCell, 6 | TableBody, 7 | TableBodyCell, 8 | TableRow, 9 | } from "../../ds/patterns/Table.tsx"; 10 | import Box from "../../ds/primitives/Box.tsx"; 11 | 12 | const Types = ({ 13 | props = [], 14 | }: { 15 | props: { 16 | name: string; 17 | isOptional: boolean; 18 | type: "string"; 19 | }[]; 20 | }) => 21 | props.length > 0 22 | ? ( 23 | 24 | 25 | 26 | Name 27 | Type 28 | Is optional 29 | 30 | 31 | 32 | {props 33 | .map(({ name, isOptional, type }) => ( 34 | 35 | 36 | {name} 37 | 38 | {type} 39 | {isOptional ? "✓" : ""} 40 | 41 | )) 42 | .join("")} 43 | 44 |
45 | ) 46 | : ( 47 | "" 48 | ); 49 | 50 | export default Types; 51 | -------------------------------------------------------------------------------- /pages/design-system/_page.ts: -------------------------------------------------------------------------------- 1 | import evaluateComponentCode from "./evaluate-code.ts"; 2 | import type { Components } from "./evaluate-jsx.ts"; 3 | 4 | const evaluateCode = ( 5 | exampleSource: string, 6 | componentName: string, 7 | componentSource?: string, 8 | ): string => 9 | evaluateComponentCode( 10 | window.components, 11 | exampleSource, 12 | componentName, 13 | componentSource, 14 | ); 15 | 16 | // TODO: Inject to a global at the host 17 | /* 18 | function loadComponents(context) { 19 | const ret = {}; 20 | 21 | context.keys().forEach((key) => { 22 | const { 23 | displayName, 24 | default: def, 25 | showCodeEditor, 26 | Example, 27 | ...rest 28 | } = context(key); 29 | 30 | if (def) { 31 | ret[displayName] = def; 32 | 33 | Object.keys(def).forEach((k) => { 34 | ret[`${displayName}.${k}`] = def[k]; 35 | }); 36 | } else { 37 | Object.entries(rest).forEach(([k, v]) => { 38 | ret[k] = v; 39 | }); 40 | } 41 | }); 42 | 43 | return ret; 44 | } 45 | */ 46 | 47 | declare global { 48 | interface Window { 49 | // Components should be injected to a global by the host 50 | components: Components; 51 | evaluateCode: typeof evaluateCode; 52 | } 53 | } 54 | 55 | window.evaluateCode = evaluateCode; 56 | -------------------------------------------------------------------------------- /pages/design-system/_pages.ts: -------------------------------------------------------------------------------- 1 | import { expandGlobSync, joinPath } from "../../deps.ts"; 2 | import type { DesignSystemComponent } from "../../types.ts"; 3 | import ComponentPageLayout from "../../ds/layouts/ComponentPage.tsx"; 4 | 5 | async function getPages() { 6 | const ret: { component: DesignSystemComponent; url: string }[] = []; 7 | 8 | for ( 9 | const file of expandGlobSync( 10 | joinPath(Deno.cwd(), "ds/**/*.tsx"), 11 | ) 12 | ) { 13 | const component = await import(file.path); 14 | 15 | ret.push({ component, url: component.displayName }); 16 | } 17 | 18 | return ret; 19 | } 20 | 21 | export { 22 | getPages, 23 | ComponentPageLayout as layout, 24 | }; 25 | -------------------------------------------------------------------------------- /pages/design-system/evaluate-code.ts: -------------------------------------------------------------------------------- 1 | import evaluateJSX from "./evaluate-jsx.ts"; 2 | import type { Components } from "./evaluate-jsx.ts"; 3 | 4 | const evaluateCode = ( 5 | componentSources: Components, 6 | exampleSource: string, 7 | componentName: string, 8 | componentSource?: string, 9 | ): string => { 10 | if (componentSource) { 11 | return evaluateJSX(exampleSource, { 12 | ...componentSources, 13 | [componentName]: (props: { [key: string]: any }, children: string[]) => 14 | evaluateJSX(componentSource, componentSources, { 15 | ...props, 16 | children, 17 | }), 18 | }); 19 | } 20 | 21 | return evaluateJSX(exampleSource, componentSources); 22 | }; 23 | 24 | export default evaluateCode; 25 | -------------------------------------------------------------------------------- /pages/design-system/evaluate-jsx.test.tsx: -------------------------------------------------------------------------------- 1 | import { assertEquals, elements } from "../../deps.ts"; 2 | import evaluateJSX from "./evaluate-jsx.ts"; 3 | 4 | Deno.test("should return empty string for empty string", () => { 5 | assertEquals(evaluateJSX("", {}), ""); 6 | }); 7 | 8 | Deno.test("should evaluate a component", () => { 9 | const ShowChildren = ({}, children: string[]) =>
{children}
; 10 | 11 | assertEquals( 12 | evaluateJSX("test", { 13 | ShowChildren, 14 | }), 15 | "
test
", 16 | ); 17 | }); 18 | 19 | Deno.test("should evaluate a boolean", () => { 20 | const ShowChildren = ( 21 | { withAnchor }: { withAnchor: boolean }, 22 | children: string[], 23 | ) => ( 24 |
{withAnchor ? "anchor" : children}
25 | ); 26 | 27 | assertEquals( 28 | evaluateJSX("test", { 29 | ShowChildren, 30 | }), 31 | "
anchor
", 32 | ); 33 | }); 34 | 35 | Deno.test("should evaluate components from components", () => { 36 | const Show = ({}, children: string[]) =>
parent: {children}
; 37 | const Children = ({}, children: string[]) =>
{children}
; 38 | Show.Children = Children; 39 | 40 | assertEquals( 41 | evaluateJSX("test", { 42 | Show, 43 | }), 44 | "
test
", 45 | ); 46 | }); 47 | 48 | Deno.test("should evaluate nested components", () => { 49 | const ShowChildren = ({}, children: string[]) =>
{children}
; 50 | 51 | assertEquals( 52 | evaluateJSX( 53 | "test", 54 | { 55 | ShowChildren, 56 | }, 57 | ), 58 | "
test
", 59 | ); 60 | }); 61 | 62 | Deno.test("should evaluate component props", () => { 63 | const PassProps = ({ test }: { test: string }, children: string[]) => ( 64 |
{test}
65 | ); 66 | 67 | assertEquals( 68 | evaluateJSX(``, { 69 | PassProps, 70 | }), 71 | "
test
", 72 | ); 73 | }); 74 | 75 | Deno.test("should evaluate arrays as component props", () => { 76 | const PassProps = ({ pages }: { pages: string[] }, children: string[]) => ( 77 |
{pages.join("")}
78 | ); 79 | 80 | assertEquals( 81 | evaluateJSX(``, { 82 | PassProps, 83 | }), 84 | "
foobarbaz
", 85 | ); 86 | }); 87 | 88 | Deno.test("should evaluate arrays within objects as component props", () => { 89 | const PassProps = ( 90 | { attributes }: { attributes: { pages: string[] } }, 91 | children: string[], 92 | ) =>
{attributes.pages.join("")}
; 93 | 94 | assertEquals( 95 | evaluateJSX( 96 | ``, 97 | { 98 | PassProps, 99 | }, 100 | ), 101 | "
foobarbaz
", 102 | ); 103 | }); 104 | 105 | Deno.test("should evaluate arrays of objects within objects as component props", () => { 106 | const PassProps = ( 107 | { attributes }: { attributes: { pages: { title: string }[] } }, 108 | children: string[], 109 | ) =>
{attributes.pages[0].title}
; 110 | 111 | assertEquals( 112 | evaluateJSX(``, { 113 | PassProps, 114 | }), 115 | "
Demo
", 116 | ); 117 | }); 118 | 119 | Deno.test("should evaluate numbers within objects as component props", () => { 120 | const PassProps = ( 121 | { attributes }: { attributes: { number: number } }, 122 | children: string[], 123 | ) =>
{attributes.number}
; 124 | 125 | assertEquals( 126 | evaluateJSX(``, { 127 | PassProps, 128 | }), 129 | "
21
", 130 | ); 131 | }); 132 | 133 | Deno.test("should evaluate strings within objects as component props", () => { 134 | const PassProps = ( 135 | { attributes }: { attributes: { str: string } }, 136 | children: string[], 137 | ) =>
{attributes.str}
; 138 | 139 | assertEquals( 140 | evaluateJSX(``, { 141 | PassProps, 142 | }), 143 | "
foo
", 144 | ); 145 | }); 146 | 147 | Deno.test("should evaluate objects within objects as component props", () => { 148 | const PassProps = ( 149 | { attributes }: { attributes: { page: { title: string } } }, 150 | children: string[], 151 | ) =>
{attributes.page.title}
; 152 | 153 | assertEquals( 154 | evaluateJSX(``, { 155 | PassProps, 156 | }), 157 | "
Demo
", 158 | ); 159 | }); 160 | 161 | Deno.test("should evaluate children", () => { 162 | const ShowChildren = ({}, children: string[]) =>
{children}
; 163 | 164 | assertEquals( 165 | evaluateJSX(`{1 + 1}`, { 166 | ShowChildren, 167 | }), 168 | "
2
", 169 | ); 170 | }); 171 | 172 | Deno.test("should evaluate expression props", () => { 173 | const PassProps = ({ test }: { test: string }, children: string[]) => ( 174 |
{test}
175 | ); 176 | 177 | assertEquals( 178 | evaluateJSX(``, { 179 | PassProps, 180 | }), 181 | "
2
", 182 | ); 183 | }); 184 | 185 | Deno.test("should evaluate components as props", () => { 186 | const Hello = () =>
hello
; 187 | const PassProps = ({ test }: { test: string }, children: string[]) => ( 188 |
{test}
189 | ); 190 | 191 | assertEquals( 192 | evaluateJSX(`} />`, { 193 | Hello, 194 | PassProps, 195 | }), 196 | "
hello
", 197 | ); 198 | }); 199 | 200 | Deno.test("should evaluate component children and props", () => { 201 | const PassChildrenAndProps = ( 202 | { test }: { test: string }, 203 | children: string[], 204 | ) => ( 205 |
206 | {test} 207 | {children} 208 |
209 | ); 210 | 211 | assertEquals( 212 | evaluateJSX( 213 | `children`, 214 | { 215 | PassChildrenAndProps, 216 | }, 217 | ), 218 | "
prop\nchildren
", 219 | ); 220 | }); 221 | 222 | Deno.test("should evaluate component with svg", () => { 223 | const svg = ( 224 | 230 | Close 231 | 232 | ); 233 | 234 | const ShowSvg = () =>
{svg}
; 235 | 236 | assertEquals( 237 | evaluateJSX("", { 238 | ShowSvg, 239 | }), 240 | `
Close
`, 241 | ); 242 | }); 243 | 244 | Deno.test("should evaluate component with svg and a custom component", () => { 245 | const PassChildren = ({}, children: string[]) =>
{children}
; 246 | const svg = ( 247 | 253 | Close 254 | 255 | ); 256 | 257 | const ShowSvg = () => ( 258 |
259 | {svg} 260 |
261 | ); 262 | 263 | assertEquals( 264 | evaluateJSX("", { 265 | ShowSvg, 266 | PassChildren, 267 | }), 268 | `
Close
`, 269 | ); 270 | }); 271 | 272 | Deno.test("should replace children", () => { 273 | const ShowChildren = ({}, children: string[]) =>
{children}
; 274 | 275 | assertEquals( 276 | evaluateJSX( 277 | "{children}", 278 | { 279 | ShowChildren, 280 | }, 281 | { 282 | children: ["replaced"], 283 | }, 284 | ), 285 | "
replaced
", 286 | ); 287 | }); 288 | 289 | Deno.test("should replace calling children", () => { 290 | const ShowChildren = ({}, children: string[]) =>
{children}
; 291 | 292 | assertEquals( 293 | evaluateJSX( 294 | "{children.join('')}", 295 | { 296 | ShowChildren, 297 | }, 298 | { 299 | children: ["replaced"], 300 | }, 301 | ), 302 | "
replaced
", 303 | ); 304 | }); 305 | 306 | Deno.test("should replace children with elements", () => { 307 | const ShowChildren = ({}, children: string[]) => ( 308 |
{children}
309 | ); 310 | 311 | assertEquals( 312 | evaluateJSX("
Demo
", { 313 | ShowChildren, 314 | }), 315 | "
Demo
", 316 | ); 317 | }); 318 | 319 | Deno.test("should replace children with elements and attributes", () => { 320 | const ShowChildren = ({}, children: string[]) => ( 321 |
{children}
322 | ); 323 | 324 | assertEquals( 325 | evaluateJSX(`
Demo
`, { 326 | ShowChildren, 327 | }), 328 | `
Demo
`, 329 | ); 330 | }); 331 | 332 | Deno.test("should replace children with multiple elements", () => { 333 | const ShowChildren = ({}, children: string[]) => ( 334 |
{children}
335 | ); 336 | 337 | assertEquals( 338 | evaluateJSX( 339 | "
Demo
Another demo
", 340 | { 341 | ShowChildren, 342 | }, 343 | ), 344 | "
Demo
Another demo
", 345 | ); 346 | }); 347 | 348 | Deno.test("should replace children with multiple children", () => { 349 | const ShowChildren = ({}, children: string[]) => ( 350 |
{children}
351 | ); 352 | 353 | assertEquals( 354 | evaluateJSX( 355 | "
FooBar
", 356 | { 357 | ShowChildren, 358 | }, 359 | ), 360 | "
FooBar
", 361 | ); 362 | }); 363 | -------------------------------------------------------------------------------- /pages/design-system/evaluate-jsx.ts: -------------------------------------------------------------------------------- 1 | // This module has been designed to work in the browser! 2 | import acorn, { Parser } from "https://cdn.skypack.dev/acorn@8.0.1?dts"; 3 | import jsx from "https://cdn.skypack.dev/acorn-jsx@5.3.1?dts"; 4 | import escodegen from "https://cdn.skypack.dev/escodegen@2.0.0?dts"; 5 | 6 | const generate = escodegen.generate; 7 | 8 | const JsxParser = Parser.extend(jsx()); 9 | 10 | type JSXNode = acorn.Node & { children: JSXNode[] }; 11 | export type Components = { 12 | [key: string]: (props: any, children: string[]) => string; 13 | }; 14 | type Replacements = { [key: string]: string[] }; 15 | 16 | function evaluateJSX( 17 | code: string, 18 | components: Components, 19 | replacements: Replacements = {}, 20 | ) { 21 | return ( 22 | evaluateJSXElement( 23 | findFirst( 24 | "JSXElement", 25 | // @ts-ignore: body property is missing from the root 26 | JsxParser.parse(code, { ecmaVersion: 2015 })?.body, 27 | ), 28 | components, 29 | replacements, 30 | ) || code 31 | ); 32 | } 33 | 34 | function evaluateJSXElement( 35 | JSXElement: JSXNode, 36 | components: Components, 37 | replacements: Replacements, 38 | ) { 39 | // @ts-ignore 40 | const firstJSXOpeningElement = JSXElement?.openingElement; 41 | const firstJSXElementAttributes = firstJSXOpeningElement?.attributes; 42 | const firstJSXElementName = resolveJSXElementName(firstJSXOpeningElement); 43 | 44 | if (firstJSXElementName) { 45 | const foundComponent = components[firstJSXElementName.name]; 46 | 47 | // TODO: Add a check to assert the found component is a function 48 | if (foundComponent) { 49 | // @ts-ignore 50 | return (firstJSXElementName.property 51 | ? // @ts-ignore 52 | foundComponent[firstJSXElementName.property] 53 | : foundComponent)( 54 | attributesToObject( 55 | firstJSXElementAttributes, 56 | components, 57 | replacements, 58 | ), 59 | childrenToString(JSXElement.children, components, replacements), 60 | ); 61 | } else { 62 | const attributesString = attributesToString( 63 | attributesToObject( 64 | firstJSXElementAttributes, 65 | components, 66 | replacements, 67 | ), 68 | ); 69 | 70 | return `<${firstJSXElementName.name}${ 71 | attributesString ? " " + attributesString : "" 72 | }>${ 73 | childrenToString( 74 | JSXElement.children, 75 | components, 76 | replacements, 77 | ) 78 | }`; 79 | } 80 | } 81 | 82 | return ""; 83 | } 84 | 85 | function attributesToString(attributes: { [key: string]: string }) { 86 | return Object.entries(attributes) 87 | .map(([key, value]) => `${key}="${value}"`) 88 | .join(" "); 89 | } 90 | 91 | function resolveJSXElementName(JSXElement: acorn.Node) { 92 | // @ts-ignore 93 | const name = JSXElement?.name; 94 | 95 | if (!name) { 96 | return; 97 | } 98 | 99 | return name.name ? { name: name.name } : { 100 | name: name.object.name, 101 | property: name.property.name, 102 | }; 103 | } 104 | 105 | function findFirst(type: string, nodes: acorn.Node[]) { 106 | for (let nodeIndex = 0; nodeIndex < nodes.length; nodeIndex++) { 107 | const node = nodes[nodeIndex]; 108 | 109 | if (node.type === type) { 110 | return node; 111 | } 112 | 113 | // @ts-ignore 114 | if (node?.expression?.type === type) { 115 | // @ts-ignore 116 | return node.expression; 117 | } 118 | } 119 | } 120 | 121 | function attributesToObject( 122 | attributes: acorn.Node[], 123 | components: Components, 124 | replacements: Replacements, 125 | ) { 126 | const ret = {}; 127 | 128 | attributes.forEach((attribute) => { 129 | // @ts-ignore 130 | if (attribute?.value?.expression) { 131 | // @ts-ignore 132 | const expression = attribute.value.expression; 133 | 134 | if (expression.type === "JSXElement") { 135 | // @ts-ignore 136 | ret[attribute?.name?.name] = evaluateJSXElement(expression, components); 137 | 138 | return; 139 | } 140 | 141 | if (expression.type === "ObjectExpression") { 142 | // @ts-ignore 143 | ret[attribute?.name?.name] = objectExpressionToObject(expression); 144 | 145 | return; 146 | } 147 | 148 | // @ts-ignore 149 | ret[attribute?.name?.name] = evaluate( 150 | generate(expression), 151 | replacements, 152 | ); 153 | // @ts.ignore 154 | } else { 155 | // @ts-ignore 156 | ret[attribute?.name?.name] = 157 | // @ts-ignore 158 | attribute?.value === null ? true : attribute?.value?.value; 159 | } 160 | }); 161 | 162 | return ret; 163 | } 164 | 165 | function objectExpressionToObject( 166 | node: acorn.Node, 167 | ) { 168 | const ret: { [key: string]: { [key: string]: string } } = {}; 169 | 170 | // @ts-ignore 171 | node.properties?.forEach((property) => { 172 | ret[property.key.name] = valueToObject(property.value); 173 | }); 174 | 175 | return ret; 176 | } 177 | 178 | function valueToObject(node: acorn.Node) { 179 | if (node.type === "ArrayExpression") { 180 | // @ts-ignore 181 | return node.elements.map(valueToObject); 182 | } 183 | 184 | if (node.type === "ObjectExpression") { 185 | return objectExpressionToObject(node); 186 | } 187 | 188 | if (node.type === "Literal") { 189 | // @ts-ignore 190 | return node.value; 191 | } 192 | 193 | throw new Error( 194 | `valueToObject - Node type ${node.type} has not been implemented yet`, 195 | ); 196 | } 197 | 198 | function childrenToString( 199 | children: JSXNode[], 200 | components: Components, 201 | replacements: Replacements, 202 | ): string { 203 | return children 204 | .map((child) => { 205 | if (child.type === "JSXElement") { 206 | return evaluateJSXElement(child, components, replacements); 207 | } 208 | 209 | if (child.type === "JSXExpressionContainer") { 210 | // @ts-ignore 211 | const expression = child?.expression; 212 | 213 | if (expression.type === "CallExpression") { 214 | return evaluate(generate(expression), replacements); 215 | } 216 | 217 | // @ts-ignore 218 | const expressionName = expression?.name; 219 | const replacement = replacements[expressionName]; 220 | 221 | if (!replacement) { 222 | // @ts-ignore 223 | return eval(generate(child.expression)); 224 | } 225 | 226 | return replacement; 227 | } 228 | 229 | // @ts-ignore 230 | if (child.expression) { 231 | // @ts-ignore 232 | return eval(generate(child.expression)); 233 | } 234 | 235 | // @ts-ignore 236 | return child.value; 237 | }) 238 | .join(""); 239 | } 240 | 241 | // TODO: Consume from sidewind? 242 | function evaluate(expression: string, replacements: Replacements) { 243 | try { 244 | return Function.apply( 245 | null, 246 | Object.keys(replacements).concat(`return ${expression}`), 247 | )(...Object.values(replacements)); 248 | } catch (err) { 249 | console.error("Failed to evaluate", expression, replacements, err); 250 | } 251 | } 252 | 253 | export default evaluateJSX; 254 | -------------------------------------------------------------------------------- /pages/design-system/index.tsx: -------------------------------------------------------------------------------- 1 | import { getComponents, elements } from "../../deps.ts"; 2 | import PageLayout from "../../ds/layouts/Page.tsx"; 3 | import Toc from "../../ds/patterns/Toc.tsx"; 4 | import Flex from "../../ds/primitives/Flex.tsx"; 5 | import Box from "../../ds/primitives/Box.tsx"; 6 | import Stack from "../../ds/primitives/Stack.tsx"; 7 | import Heading from "../../ds/primitives/Heading.tsx"; 8 | import config from "../../tailwind.ts"; 9 | import Colors from "./Colors.tsx"; 10 | import SpacingScale from "./SpacingScale.tsx"; 11 | import Collection from "./Collection.tsx"; 12 | 13 | const DesignSystemPage = async (props: { url: string }) => ( 14 | 17 | 18 | 19 | 20 | 27 | 28 | Design System 29 | 30 | 31 | 32 | 33 | 34 | Spacing scale 35 | 36 | 37 | 38 | 39 | 40 | Colors 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | Primitives 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | Patterns 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | Layouts 67 | 68 | 69 | 70 | 71 | 72 | 73 | } 74 | /> 75 | ); 76 | 77 | DesignSystemPage.title = "Design system"; 78 | DesignSystemPage.meta = { 79 | description: 80 | "You can find the different variants and components of the system on this page", 81 | }; 82 | 83 | export default DesignSystemPage; 84 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { elements, processMarkdown } from "../deps.ts"; 2 | import PageLayout from "../ds/layouts/Page.tsx"; 3 | import Alert from "../ds/patterns/Alert.tsx"; 4 | import Heading from "../ds/primitives/Heading.tsx"; 5 | import Box from "../ds/primitives/Box.tsx"; 6 | import Stack from "../ds/primitives/Stack.tsx"; 7 | import Button from "../ds/primitives/Button.tsx"; 8 | 9 | const readme = processMarkdown( 10 | Deno.readTextFileSync(Deno.cwd() + "/README.md"), 11 | ); 12 | 13 | const IndexPage = (props: { url: string }) => ( 14 | 17 | {readme.content} 18 | 19 | Demo 20 | 21 | 22 | 23 | Value: 24 | 25 | This is a demo alert 26 | 29 | 30 |
} 31 | /> 32 | ); 33 | 34 | IndexPage.title = "tailspin"; 35 | IndexPage.meta = { 36 | description: "tailspin is a site generator and design system in one", 37 | }; 38 | 39 | export default IndexPage; 40 | -------------------------------------------------------------------------------- /scripts.json: -------------------------------------------------------------------------------- 1 | { 2 | "allow": ["env", "plugin", "read", "write", "net"], 3 | "scripts": { 4 | "format": "deno fmt --unstable", 5 | "prebuild": "npm run generate-meta", 6 | "build": "npm run clean && TODO", 7 | "prestart": "npm run generate-meta", 8 | "start": "deno run --config ./tsconfig.json --unstable mod.ts", 9 | "clean": "rm -rf public", 10 | "generate-sitemap": "TODO", 11 | "check-links": "TODO", 12 | "generate-meta": "deno run --config ./tsconfig.json --unstable ./utils/generate-meta.ts", 13 | "test": "deno test --config ./tsconfig.json --failfast --unstable ./ast ./pages ./utils" 14 | } 15 | } -------------------------------------------------------------------------------- /swc-server/index.js: -------------------------------------------------------------------------------- 1 | const swc = require('@swc/wasm'); 2 | const jayson = require('jayson'); 3 | 4 | function serve(port) { 5 | const server = jayson.server({ 6 | parse: (args, cb) => { 7 | try { 8 | cb(null, parse(args[0])); 9 | } catch(err) { 10 | console.error(err); 11 | 12 | cb(err); 13 | } 14 | }, 15 | ping: (_, cb) => cb(null, 'pong'), 16 | print: (args, cb) => { 17 | try { 18 | cb(null, print(args[0])); 19 | } catch(err) { 20 | console.error(err); 21 | 22 | cb(err); 23 | } 24 | }, 25 | }); 26 | 27 | console.log(`swc-server - running at port ${port}`) 28 | server.http().listen(port); 29 | } 30 | 31 | function parse(source) { 32 | return swc.parseSync(source, { syntax: "typescript", tsx: true }); 33 | } 34 | 35 | function print(ast) { 36 | const ret = swc.printSync({ type: "Module", body: [{ type: "ExpressionStatement", expression: ast, span: { start: 0, end: 0, ctxt: 0 } }], span: { start: 0, end: 0, ctxt: 0 } }, {}).code.trim(); 37 | 38 | // TODO: Trim last ; 39 | return ret.slice(0, ret.length - 1); 40 | } 41 | 42 | serve(process.env.PORT || 4000); 43 | -------------------------------------------------------------------------------- /swc-server/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swc-server", 3 | "version": "0.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@swc/wasm": { 8 | "version": "1.2.30", 9 | "resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.2.30.tgz", 10 | "integrity": "sha512-4qupMM/PMVXIyglW8IOZLCXjdXuKfemGFjAMbr+2N671LrbJPpgY129j6g2HjXabVBKW1IoqTcohCXlHCC32Jg==" 11 | }, 12 | "@types/connect": { 13 | "version": "3.4.33", 14 | "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", 15 | "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==", 16 | "requires": { 17 | "@types/node": "*" 18 | } 19 | }, 20 | "@types/express-serve-static-core": { 21 | "version": "4.17.12", 22 | "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.12.tgz", 23 | "integrity": "sha512-EaEdY+Dty1jEU7U6J4CUWwxL+hyEGMkO5jan5gplfegUgCUsIUWqXxqw47uGjimeT4Qgkz/XUfwoau08+fgvKA==", 24 | "requires": { 25 | "@types/node": "*", 26 | "@types/qs": "*", 27 | "@types/range-parser": "*" 28 | } 29 | }, 30 | "@types/lodash": { 31 | "version": "4.14.161", 32 | "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.161.tgz", 33 | "integrity": "sha512-EP6O3Jkr7bXvZZSZYlsgt5DIjiGr0dXP1/jVEwVLTFgg0d+3lWVQkRavYVQszV7dYUwvg0B8R0MBDpcmXg7XIA==" 34 | }, 35 | "@types/node": { 36 | "version": "12.12.62", 37 | "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.62.tgz", 38 | "integrity": "sha512-qAfo81CsD7yQIM9mVyh6B/U47li5g7cfpVQEDMfQeF8pSZVwzbhwU3crc0qG4DmpsebpJPR49AKOExQyJ05Cpg==" 39 | }, 40 | "@types/qs": { 41 | "version": "6.9.5", 42 | "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz", 43 | "integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ==" 44 | }, 45 | "@types/range-parser": { 46 | "version": "1.2.3", 47 | "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", 48 | "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" 49 | }, 50 | "JSONStream": { 51 | "version": "1.3.5", 52 | "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", 53 | "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", 54 | "requires": { 55 | "jsonparse": "^1.2.0", 56 | "through": ">=2.2.7 <3" 57 | } 58 | }, 59 | "commander": { 60 | "version": "2.20.3", 61 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 62 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" 63 | }, 64 | "es6-promise": { 65 | "version": "4.2.8", 66 | "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", 67 | "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" 68 | }, 69 | "es6-promisify": { 70 | "version": "5.0.0", 71 | "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", 72 | "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", 73 | "requires": { 74 | "es6-promise": "^4.0.3" 75 | } 76 | }, 77 | "eyes": { 78 | "version": "0.1.8", 79 | "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", 80 | "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=" 81 | }, 82 | "jayson": { 83 | "version": "3.3.4", 84 | "resolved": "https://registry.npmjs.org/jayson/-/jayson-3.3.4.tgz", 85 | "integrity": "sha512-p2stl9m1C0vM8mdXM1m8shn0v5ECohD5gEDRzLD6CPv02pxRm1lv0jEz0HX6RvfJ/uO9z9Zzlzti7/uqq+Rh5g==", 86 | "requires": { 87 | "@types/connect": "^3.4.33", 88 | "@types/express-serve-static-core": "^4.17.9", 89 | "@types/lodash": "^4.14.159", 90 | "@types/node": "^12.12.54", 91 | "JSONStream": "^1.3.5", 92 | "commander": "^2.20.3", 93 | "es6-promisify": "^5.0.0", 94 | "eyes": "^0.1.8", 95 | "json-stringify-safe": "^5.0.1", 96 | "lodash": "^4.17.20", 97 | "uuid": "^3.4.0" 98 | } 99 | }, 100 | "json-stringify-safe": { 101 | "version": "5.0.1", 102 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 103 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 104 | }, 105 | "jsonparse": { 106 | "version": "1.3.1", 107 | "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", 108 | "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" 109 | }, 110 | "lodash": { 111 | "version": "4.17.20", 112 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", 113 | "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" 114 | }, 115 | "through": { 116 | "version": "2.3.8", 117 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 118 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" 119 | }, 120 | "uuid": { 121 | "version": "3.4.0", 122 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", 123 | "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /swc-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swc-server", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node ./index.js" 8 | }, 9 | "keywords": [], 10 | "author": "Juho Vepsäläinen", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@swc/wasm": "^1.2.30", 14 | "jayson": "^3.3.4" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["ds", "pages"], 3 | "compilerOptions": { 4 | "jsx": "react", 5 | "jsxFactory": "elements.createElement" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /types.ts: -------------------------------------------------------------------------------- 1 | // TODO: Disjointed union would work better here 2 | type AstNode = { 3 | type: string; 4 | value?: string; 5 | kind?: string; 6 | span: { 7 | start: number; 8 | end: number; 9 | ctxt: number; 10 | }; 11 | declare?: boolean; 12 | declaration?: AstNode; 13 | declarations?: AstNode[]; 14 | body?: AstNode[] | AstNode; 15 | expression?: AstNode; 16 | init?: AstNode; 17 | id?: AstNode; 18 | }; 19 | 20 | // TODO: Join these two and extract the generator specific type 21 | type Component = { 22 | displayName: string; 23 | description: string; 24 | default: (...args: any) => string; 25 | exampleSource: string; 26 | componentSource: string; 27 | // TODO: Prop type 28 | props: any[]; 29 | }; 30 | type DesignSystemComponent = { 31 | displayName: string; 32 | description: string; 33 | default: (...args: any) => string; 34 | Example: (...args: any) => string; 35 | }; 36 | type Pages = { 37 | [key: string]: Page; 38 | }; 39 | type Layout = ({ 40 | url, 41 | title, 42 | meta, 43 | pages, 44 | attributes, 45 | }: { 46 | url: string; 47 | title?: string; 48 | meta?: { [key: string]: string }; 49 | pages: DynamicPages; 50 | attributes: {}; 51 | }) => void; 52 | // TODO: Figure out a good way to type dynamic pages 53 | type DynamicPages = { layout: Layout; attributes: {} }[]; 54 | type Page = { 55 | module: { 56 | default: Layout; 57 | }; 58 | pages: DynamicPages; 59 | attributes: {}; 60 | }; 61 | type Urls = { 62 | [key: string]: { 63 | layout?: Layout; 64 | path: string | undefined; 65 | pages: DynamicPages; 66 | attributes: {}; 67 | }; 68 | }; 69 | 70 | export type { AstNode, Component, DesignSystemComponent, Pages, Page, Urls }; 71 | -------------------------------------------------------------------------------- /user-theme.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | colors: { 3 | primary: "#09b5c4", 4 | secondary: "#434343", 5 | action: "#6eedf8", 6 | muted: "#999", 7 | error: "#ec5334", 8 | warning: "#fdf2bb", 9 | tip: "#f5fffb", 10 | }, 11 | // Below properties belong to Oceanwind 12 | spacing: { 13 | "0": "0", 14 | "1": "0.25rem", 15 | "2": "0.5rem", 16 | "3": "0.75rem", 17 | "4": "1rem", 18 | "5": "1.25rem", 19 | "6": "1.5rem", 20 | "8": "2rem", 21 | "10": "2.5rem", 22 | "12": "3rem", 23 | "16": "4rem", 24 | "20": "5rem", 25 | "24": "6rem", 26 | "32": "8rem", 27 | "40": "10rem", 28 | "48": "12rem", 29 | "56": "14rem", 30 | "64": "16rem", 31 | px: "1px", 32 | }, 33 | width: { 34 | "0": "0", 35 | "1": "0.25rem", 36 | "2": "0.5rem", 37 | "3": "0.75rem", 38 | "4": "1rem", 39 | "5": "1.25rem", 40 | "6": "1.5rem", 41 | "8": "2rem", 42 | "10": "2.5rem", 43 | "12": "3rem", 44 | "16": "4rem", 45 | "20": "5rem", 46 | "24": "6rem", 47 | "32": "8rem", 48 | "40": "10rem", 49 | "48": "12rem", 50 | "56": "14rem", 51 | "64": "16rem", 52 | auto: "auto", 53 | px: "1px", 54 | "1/2": "50%", 55 | "1/3": "33.333333%", 56 | "2/3": "66.666667%", 57 | "1/4": "25%", 58 | "2/4": "50%", 59 | "3/4": "75%", 60 | "1/5": "20%", 61 | "2/5": "40%", 62 | "3/5": "60%", 63 | "4/5": "80%", 64 | "1/6": "16.666667%", 65 | "2/6": "33.333333%", 66 | "3/6": "50%", 67 | "4/6": "66.666667%", 68 | "5/6": "83.333333%", 69 | "1/12": "8.333333%", 70 | "2/12": "16.666667%", 71 | "3/12": "25%", 72 | "4/12": "33.333333%", 73 | "5/12": "41.666667%", 74 | "6/12": "50%", 75 | "7/12": "58.333333%", 76 | "8/12": "66.666667%", 77 | "9/12": "75%", 78 | "10/12": "83.333333%", 79 | "11/12": "91.666667%", 80 | full: "100%", 81 | screen: "100vw", 82 | }, 83 | height: { 84 | "0": "0", 85 | "1": "0.25rem", 86 | "2": "0.5rem", 87 | "3": "0.75rem", 88 | "4": "1rem", 89 | "5": "1.25rem", 90 | "6": "1.5rem", 91 | "8": "2rem", 92 | "10": "2.5rem", 93 | "12": "3rem", 94 | "16": "4rem", 95 | "20": "5rem", 96 | "24": "6rem", 97 | "32": "8rem", 98 | "40": "10rem", 99 | "48": "12rem", 100 | "56": "14rem", 101 | "64": "16rem", 102 | auto: "auto", 103 | px: "1px", 104 | full: "100%", 105 | screen: "100vh", 106 | }, 107 | maxHeight: { 108 | full: "100%", 109 | screen: "100vh", 110 | }, 111 | maxWidth: { 112 | none: "none", 113 | xs: "20rem", 114 | sm: "24rem", 115 | md: "28rem", 116 | lg: "32rem", 117 | xl: "36rem", 118 | "2xl": "42rem", 119 | "3xl": "48rem", 120 | "4xl": "56rem", 121 | "5xl": "64rem", 122 | "6xl": "72rem", 123 | full: "100%", 124 | "screen-sm": "640px", 125 | "screen-md": "768px", 126 | "screen-lg": "1024px", 127 | "screen-xl": "1280px", 128 | }, 129 | minHeight: { 130 | "0": "0", 131 | full: "100%", 132 | screen: "100vh", 133 | }, 134 | minWidth: { 135 | "0": "0", 136 | full: "100%", 137 | }, 138 | fontSize: { 139 | xs: "0.75rem", 140 | sm: "0.875rem", 141 | base: "1rem", 142 | lg: "1.125rem", 143 | xl: "1.25rem", 144 | "2xl": "1.5rem", 145 | "3xl": "1.875rem", 146 | "4xl": "2.25rem", 147 | "5xl": "3rem", 148 | "6xl": "4rem", 149 | }, 150 | fontWeight: { 151 | hairline: "100", 152 | thin: "200", 153 | light: "300", 154 | normal: "400", 155 | medium: "500", 156 | semibold: "600", 157 | bold: "700", 158 | extrabold: "800", 159 | black: "900", 160 | }, 161 | }; 162 | -------------------------------------------------------------------------------- /utils/generate-meta.ts: -------------------------------------------------------------------------------- 1 | import defaultTheme from "../default-theme.ts"; 2 | import userTheme from "../user-theme.ts"; 3 | import getUrls from "./get-urls.ts"; 4 | 5 | const isObject = (a: any) => typeof a === "object"; 6 | 7 | async function generateMeta() { 8 | await generateInitialMeta(); 9 | await generateAllMeta(); 10 | } 11 | 12 | async function generateInitialMeta() { 13 | try { 14 | // TODO: Do a proper merge here 15 | const expandedConfig = { 16 | ...defaultTheme, 17 | ...userTheme, 18 | extendedColors: { ...defaultTheme.colors, ...userTheme.colors }, 19 | colors: expandColors({ ...defaultTheme.colors, ...userTheme.colors }), 20 | // TODO: Find a way to generate the definition without executing code since the 21 | // code depends on it. Maybe it's better to push the check to the system instead of ts 22 | // as that can handle external links as well. 23 | internalLinks: { "/": {}, "/blog/": {}, "/design-system/": {} }, 24 | }; 25 | 26 | Deno.writeTextFileSync( 27 | Deno.cwd() + "/tailwind.ts", 28 | `export default ${JSON.stringify(expandedConfig, null, 2)};`, 29 | ); 30 | } catch (error) { 31 | console.error(error); 32 | } 33 | } 34 | 35 | async function generateAllMeta() { 36 | try { 37 | // TODO: Do a proper merge here 38 | const expandedConfig = { 39 | ...defaultTheme, 40 | ...userTheme, 41 | extendedColors: { ...defaultTheme.colors, ...userTheme.colors }, 42 | colors: expandColors({ ...defaultTheme.colors, ...userTheme.colors }), 43 | internalLinks: await getUrls(), 44 | }; 45 | 46 | Deno.writeTextFileSync( 47 | Deno.cwd() + "/tailwind.ts", 48 | `export default ${JSON.stringify(expandedConfig, null, 2)};`, 49 | ); 50 | } catch (error) { 51 | console.error(error); 52 | } 53 | } 54 | 55 | type Colors = { [key: string]: string | { [key: string]: string } }; 56 | 57 | function expandColors(colors: Colors) { 58 | const ret: Colors = {}; 59 | 60 | // This assumes one level of nesting so no recursion is needed 61 | Object.entries(colors).forEach(([key, value]) => { 62 | if (isObject(value)) { 63 | Object.entries(value).forEach(([k, v]) => { 64 | ret[`${key}-${k}`] = v; 65 | }); 66 | } else { 67 | ret[key] = value; 68 | } 69 | }); 70 | 71 | return ret; 72 | } 73 | 74 | // TODO: Detect if this is run from outside or exposed as a module 75 | generateMeta(); 76 | -------------------------------------------------------------------------------- /utils/get-component.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, joinPath } from "../deps.ts"; 2 | import getComponent from "./get-component.ts"; 3 | 4 | Deno.test("gets text", async () => { 5 | const text = await getComponent( 6 | joinPath(Deno.cwd(), "ds", "primitives", "Text.tsx"), 7 | ); 8 | 9 | assertEquals(text.displayName, "Text"); 10 | assertEquals(text.exampleSource.length, 111); 11 | }); 12 | -------------------------------------------------------------------------------- /utils/get-component.ts: -------------------------------------------------------------------------------- 1 | import { parseCode, parseProps } from "../deps.ts"; 2 | import type { Component } from "../types.ts"; 3 | 4 | async function getComponent( 5 | componentPath: string, 6 | ): Promise { 7 | const source = Deno.readTextFileSync(componentPath); 8 | const component = await import(componentPath); 9 | const { displayName } = component; 10 | 11 | return { 12 | ...component, 13 | source, 14 | componentSource: component.showCodeEditor 15 | ? await parseCode({ name: displayName, source }) 16 | : "", 17 | exampleSource: await parseCode({ name: "Example", source }), 18 | props: await parseProps({ 19 | componentPath, 20 | displayName, 21 | source, 22 | }), 23 | }; 24 | } 25 | 26 | export default getComponent; 27 | -------------------------------------------------------------------------------- /utils/get-components.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "../deps.ts"; 2 | import getComponents from "./get-components.ts"; 3 | 4 | Deno.test("gets primitives", async () => { 5 | const primitives = await getComponents("primitives"); 6 | 7 | assertEquals(primitives.length > 0, true); 8 | }); 9 | 10 | Deno.test("gets patterns", async () => { 11 | const patterns = await getComponents("patterns"); 12 | 13 | assertEquals(patterns.length > 0, true); 14 | }); 15 | 16 | Deno.test("gets layouts", async () => { 17 | const layouts = await getComponents("layouts"); 18 | 19 | assertEquals(layouts.length > 0, true); 20 | }); 21 | -------------------------------------------------------------------------------- /utils/get-components.ts: -------------------------------------------------------------------------------- 1 | import { expandGlobSync, joinPath } from "../deps.ts"; 2 | import type { Component } from "../types.ts"; 3 | import getComponent from "./get-component.ts"; 4 | 5 | async function getComponents(type: string) { 6 | // TODO: Expose as a parameter 7 | const componentDirectory = joinPath(Deno.cwd(), "ds", type); 8 | 9 | const ret: Component[] = []; 10 | 11 | for ( 12 | const file of expandGlobSync( 13 | joinPath(componentDirectory, "*.tsx"), 14 | ) 15 | ) { 16 | ret.push(await getComponent(file.path)); 17 | } 18 | 19 | return ret; 20 | } 21 | 22 | export default getComponents; 23 | -------------------------------------------------------------------------------- /utils/get-pages.ts: -------------------------------------------------------------------------------- 1 | import type { Pages, Urls } from "../types.ts"; 2 | 3 | async function getPages(urls: Urls) { 4 | const ret: Pages = {}; 5 | 6 | await Promise.all( 7 | Object.entries(urls).map( 8 | async ([url, { path, pages, attributes, layout }]) => { 9 | if (path) { 10 | // TODO: It's better to do 11 | // await import(`${path}.tsx#${Math.random()}`) 12 | // TODO: Maintain a counter per page instead of using a random number 13 | const module = await import(`${path}?version=${Math.random()}.tsx`); 14 | 15 | ret[url] = { module, pages, attributes }; 16 | 17 | return Promise.resolve(); 18 | } 19 | 20 | if (!layout) { 21 | console.warn( 22 | "Dynamic page is missing a layout", 23 | { url, path, pages, attributes }, 24 | ); 25 | 26 | return Promise.resolve(); 27 | } 28 | 29 | // TODO: Consider reloading the layout now given it might have received 30 | // changes. Likely this should be await import() 31 | ret[url] = { 32 | module: { 33 | "default": layout, 34 | }, 35 | pages, 36 | attributes, 37 | }; 38 | 39 | // TODO: Add logic to deal with dynamically generated pages 40 | return Promise.resolve(); 41 | }, 42 | ), 43 | ); 44 | 45 | return ret; 46 | } 47 | 48 | export default getPages; 49 | -------------------------------------------------------------------------------- /utils/get-urls.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "../deps.ts"; 2 | import getUrls from "./get-urls.ts"; 3 | 4 | Deno.test("gets urls", async () => { 5 | const urls = await getUrls(); 6 | 7 | assertEquals(Object.keys(urls).length > 0, true); 8 | }); 9 | -------------------------------------------------------------------------------- /utils/get-urls.ts: -------------------------------------------------------------------------------- 1 | import { 2 | joinPath, 3 | getDirectory, 4 | getRelativePath, 5 | expandGlobSync, 6 | existsSync, 7 | } from "../deps.ts"; 8 | import type { Urls } from "../types.ts"; 9 | 10 | async function getUrls() { 11 | const rootPath = joinPath(Deno.cwd(), "pages"); 12 | const ret: Urls = {}; 13 | 14 | for ( 15 | const file of expandGlobSync(joinPath(rootPath, "**/index.tsx")) 16 | ) { 17 | const fileDir = getDirectory(file.path); 18 | const relativePath = getRelativePath(rootPath, file.path); 19 | const link = relativePath 20 | .replace("/index.tsx", "") 21 | .replace("index.tsx", "/"); 22 | const resolvedUrl = link === "/" ? link : `/${link}/`; 23 | 24 | const extraPagesPath = joinPath(fileDir, "_pages.ts"); 25 | 26 | let pages = []; 27 | if (existsSync(extraPagesPath)) { 28 | const extraPages = await import(extraPagesPath); 29 | const layout = extraPages.layout; 30 | 31 | const extras = await Promise.resolve(extraPages.getPages()); 32 | pages = extras.map(( 33 | { url, ...attributes }: { url: string }, 34 | ) => { 35 | if (!url) { 36 | // TODO: Give a warning? 37 | return; 38 | } 39 | 40 | ret[joinPath(resolvedUrl, url)] = { 41 | layout, 42 | path: undefined, 43 | pages: [], 44 | attributes, 45 | }; 46 | 47 | return { ...attributes }; 48 | }).filter(Boolean); 49 | } 50 | 51 | ret[resolvedUrl] = { path: file.path, pages, attributes: {} }; 52 | } 53 | 54 | return ret; 55 | } 56 | 57 | export default getUrls; 58 | -------------------------------------------------------------------------------- /utils/process-markdown.ts: -------------------------------------------------------------------------------- 1 | // Reference: https://deno.land/x/pagic@v0.9.1/src/plugins/md.tsx 2 | import fm from "https://dev.jspm.io/front-matter@4.0.2"; 3 | import MarkdownIt from "https://dev.jspm.io/markdown-it@11.0.0"; 4 | 5 | // TODO: Customize and add highlighting (highlight.js?) 6 | // @ts-ignore 7 | const mdRenderer = new MarkdownIt({ 8 | html: true, 9 | }); 10 | 11 | function processMarkdown(source: string) { 12 | // @ts-ignore 13 | const { body, attributes: meta } = fm(source); 14 | 15 | return { content: mdRenderer.render(body).trim(), meta }; 16 | } 17 | 18 | export default processMarkdown; 19 | -------------------------------------------------------------------------------- /utils/watch-directories.ts: -------------------------------------------------------------------------------- 1 | async function watchDirectories( 2 | directories: string[], 3 | onModify: () => void, 4 | ) { 5 | const watcher = Deno.watchFs(directories, { recursive: true }); 6 | 7 | for await (const event of watcher) { 8 | console.log("watchDirectories - Detected a change", event); 9 | 10 | if (event.kind === "modify") { 11 | await onModify(); 12 | } 13 | } 14 | } 15 | 16 | export default watchDirectories; 17 | -------------------------------------------------------------------------------- /utils/web-sockets.ts: -------------------------------------------------------------------------------- 1 | import { 2 | WebSocketServer, 3 | } from "https://deno.land/x/websocket@v0.0.5/mod.ts"; 4 | import type { 5 | WebSocket, 6 | } from "https://deno.land/x/websocket@v0.0.5/mod.ts"; 7 | 8 | const getWebsocketServer = () => { 9 | const wss = new WebSocketServer(8080); 10 | 11 | wss.on("connection", (ws: WebSocket) => { 12 | console.log("wss - Connected"); 13 | 14 | ws.send("connected"); 15 | 16 | // Catch possible messages here 17 | /*ws.on("message", (message: string) => { 18 | console.log(message); 19 | ws.send(message); 20 | });*/ 21 | }); 22 | 23 | return wss; 24 | }; 25 | 26 | const websocketClient = `const socket = new WebSocket('ws://localhost:8080'); 27 | 28 | socket.addEventListener('message', (event) => { 29 | if (event.data === 'connected') { 30 | console.log('WebSocket - connected'); 31 | } 32 | 33 | if (event.data === 'refresh') { 34 | location.reload(); 35 | } 36 | });` 37 | .split("\n") 38 | .join(""); 39 | 40 | export { getWebsocketServer, websocketClient }; 41 | --------------------------------------------------------------------------------