├── .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 | }${tagName}>`;
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 | "",
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 | "",
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 |
232 | );
233 |
234 | const ShowSvg = () => {svg}
;
235 |
236 | assertEquals(
237 | evaluateJSX("", {
238 | ShowSvg,
239 | }),
240 | ``,
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 |
255 | );
256 |
257 | const ShowSvg = () => (
258 |
261 | );
262 |
263 | assertEquals(
264 | evaluateJSX("", {
265 | ShowSvg,
266 | PassChildren,
267 | }),
268 | ``,
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 | "",
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 | ``,
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 | "",
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 | "",
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 | }${firstJSXElementName.name}>`;
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 |
--------------------------------------------------------------------------------