@weatherboard/gyde-design 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/catalogue.mjs ADDED
@@ -0,0 +1,506 @@
1
+ /**
2
+ * G-50 — the catalogue, generated rather than authored.
3
+ *
4
+ * @gyde-emits-source-for-another-repo
5
+ *
6
+ * WHY GENERATED.
7
+ *
8
+ * Both source repositories hand-authored one, and the larger of the two shows
9
+ * what that costs at scale: a 433-line registry whose `sourcePath` entries point
10
+ * at a superseded generation of its own components, whose Button lists four
11
+ * variants where the real one has nine, and which has **no coverage test**. It
12
+ * documents a system that no longer exists, confidently.
13
+ *
14
+ * Generating it makes "every component is catalogued" true by construction
15
+ * instead of true by a test somebody has to keep passing. The prior-art survey
16
+ * found Storybook does not enforce catalogue coverage either, so this is a real
17
+ * gap rather than a preference.
18
+ *
19
+ * WHAT IT MUST RENDER, AND WHY EACH ONE IS ON THE LIST.
20
+ *
21
+ * Every item here is a defect class a deployed catalogue has already caught once:
22
+ *
23
+ * every variant a Button that rendered nothing at all
24
+ * both themes three muted notes running together into one word
25
+ * a portalled surface a Select popup opening light against a dark panel
26
+ * a runtime-themed one the capability whose absence cost two apps 0% adoption
27
+ * the usage index a component reporting itself unused while demonstrating itself
28
+ *
29
+ * A catalogue that renders only first paint, in one theme, is a screenshot.
30
+ *
31
+ * THE HONESTY TEST, which is a gate rather than a convention:
32
+ *
33
+ * "It boots with nothing else running — no manager, no database, no network.
34
+ * That is not a convenience, it is the test of whether the catalogue is still
35
+ * honest… If this command ever grows a prerequisite, read the vite config
36
+ * before fixing it, because the prerequisite is the bug."
37
+ */
38
+
39
+ /**
40
+ * What each seed component can be shown as.
41
+ *
42
+ * Data, not JSX, so the coverage test can read it as text — the same reason
43
+ * System A keeps its entries in a declarative array. A test that
44
+ * imports the catalogue would resolve a typo'd component name to `undefined`
45
+ * and pass while covering nothing.
46
+ *
47
+ * `states` is deliberately plural everywhere. The defects worth catching are
48
+ * states, and none of them exists on load.
49
+ */
50
+ export const CATALOGUE_ENTRIES = {
51
+ Button: {
52
+ purpose: "The one action a view wants, and every lesser action beside it.",
53
+ replaces: ["a styled <button>", "an <a> that looks like a button"],
54
+ states: [
55
+ { label: "accent / filled", props: { tone: "accent", emphasis: "filled", size: "md" }, children: "Save changes" },
56
+ { label: "neutral / outline", props: { tone: "neutral", emphasis: "outline", size: "md" }, children: "Cancel" },
57
+ { label: "danger / filled", props: { tone: "danger", emphasis: "filled", size: "md" }, children: "Delete" },
58
+ { label: "ghost, small", props: { tone: "neutral", emphasis: "ghost", size: "sm" }, children: "Dismiss" },
59
+ { label: "disabled", props: { tone: "accent", emphasis: "filled", size: "md", disabled: true }, children: "Saving…" },
60
+ ],
61
+ },
62
+ Text: {
63
+ purpose: "Every piece of prose, at the role it plays rather than the size it is.",
64
+ replaces: ["a <p> with a font-size", "a <span> with a colour"],
65
+ states: [
66
+ { label: "display", props: { role: "display" }, children: "A page title" },
67
+ { label: "title", props: { role: "title" }, children: "A panel title" },
68
+ { label: "body", props: { role: "body" }, children: "Body copy, which is what most text is." },
69
+ { label: "muted", props: { role: "muted", as: "p" }, children: "Supporting text that is still information." },
70
+ { label: "faint", props: { role: "faint", as: "p" }, children: "Present, but not information." },
71
+ ],
72
+ },
73
+ Card: {
74
+ purpose: "A bordered surface. The only one, which is the point.",
75
+ replaces: ["a div with a border and a radius", "a panel", "a well"],
76
+ states: [
77
+ { label: "default", props: { tone: "default", inset: "md" }, children: "A surface." },
78
+ { label: "muted", props: { tone: "muted", inset: "md" }, children: "A surface sunk into another." },
79
+ { label: "danger", props: { tone: "danger", inset: "md" }, children: "Something failed here." },
80
+ { label: "no inset", props: { tone: "default", inset: "none" }, children: "Edge to edge." },
81
+ ],
82
+ },
83
+ Checkbox: {
84
+ purpose: "A binary choice that is part of a form.",
85
+ replaces: ["an <input type=checkbox> with a label beside it"],
86
+ controlled: { checked: false },
87
+ states: [
88
+ { label: "unchecked", props: { checked: false, label: "Include drafts", id: "cb-1" } },
89
+ { label: "checked", props: { checked: true, label: "Include drafts", id: "cb-2" } },
90
+ { label: "indeterminate", props: { checked: false, indeterminate: true, label: "Some selected", id: "cb-3" } },
91
+ { label: "disabled", props: { checked: true, disabled: true, label: "Locked", id: "cb-4" } },
92
+ ],
93
+ },
94
+ Select: {
95
+ purpose: "One of a known set of options.",
96
+ replaces: ["a <select>", "a dropdown built from a button and a list"],
97
+ controlled: { value: "b" },
98
+ /**
99
+ * The open state is the reason this component is in the seed set — the popup
100
+ * is portalled, and a portal is where inheritance-based theming stops
101
+ * reaching. It is also the state this catalogue CANNOT render: forcing it
102
+ * would need `defaultOpen`, which the wrapper deliberately does not expose.
103
+ *
104
+ * An earlier version listed an "open" example that rendered exactly the
105
+ * closed one. A label that lies is worse than a gap, so the states below are
106
+ * the ones the closed API can actually produce, and the gap is recorded in
107
+ * `notes` where a reader of the catalogue will see it.
108
+ */
109
+ notes: [
110
+ "Its open state is portalled and cannot be forced through the closed API. " +
111
+ "That is where theming across a portal boundary is proved, so it needs a " +
112
+ "rendered-artifact review rather than a static example (G-11).",
113
+ ],
114
+ states: [
115
+ { label: "a value selected", props: { label: "Sort by", options: [{ value: "a", label: "Newest" }, { value: "b", label: "Oldest" }] } },
116
+ { label: "a different value selected", props: { value: "a", label: "Sort by", options: [{ value: "a", label: "Newest" }, { value: "b", label: "Oldest" }] } },
117
+ { label: "disabled", props: { disabled: true, label: "Sort by", options: [{ value: "a", label: "Newest" }, { value: "b", label: "Oldest" }] } },
118
+ ],
119
+ },
120
+ };
121
+
122
+ const lit = (v) =>
123
+ typeof v === "string" ? JSON.stringify(v)
124
+ : typeof v === "boolean" || typeof v === "number" ? `{${v}}`
125
+ : `{${JSON.stringify(v)}}`;
126
+
127
+ /**
128
+ * Render one example as JSX.
129
+ *
130
+ * An explicit builder, not a chain of `.replace()` calls on a formatted string.
131
+ * The first version was the latter and shipped two bugs into a scaffolded
132
+ * repository: `Select` emitted without its required `value` prop, and every
133
+ * state of a controlled component rendered identically because the
134
+ * substitutions silently did not match.
135
+ *
136
+ * A generator whose output is assembled by patching text produces output nobody
137
+ * predicted, and the failure is invisible until somebody reads the emitted file.
138
+ */
139
+ function exampleJsx(name, state, entry) {
140
+ const props = { ...(entry.controlled ?? {}), ...state.props };
141
+ const attrs = Object.entries(props).map(([k, v]) => `${k}=${lit(v)}`);
142
+
143
+ // A controlled component needs a handler. The catalogue holds no state on
144
+ // purpose — what is being reviewed is the rendering, not the interaction.
145
+ if (entry.controlled) attrs.push("onChange={() => {}}");
146
+
147
+ const open = `<${name} ${attrs.join(" ")}`;
148
+ return state.children ? `${open}>${state.children}</${name}>` : `${open} />`;
149
+ }
150
+
151
+ /**
152
+ * A KNOWN GAP, stated rather than worked around.
153
+ *
154
+ * `Select`'s open state is the reason it is in the seed set: the popup is
155
+ * portalled, and a portal is where inheritance-based theming stops reaching.
156
+ * But the wrapper deliberately does not expose `defaultOpen` — the primitive
157
+ * has it, and forwarding it would be an escape hatch opened for the
158
+ * catalogue's convenience, which is exactly how closure erodes.
159
+ *
160
+ * So **a closed component set cannot demonstrate its own interactive states.**
161
+ * That is a real tension, not an oversight, and the catalogue says so at the
162
+ * call site rather than printing "open" beside a control that is closed. A
163
+ * label that lies is worse than an admission.
164
+ *
165
+ * The fix is a decision rather than a patch: either the set gains a reviewed
166
+ * way to force a state, or interactive states are explicitly out of the
167
+ * catalogue's scope and a rendered-artifact review (G-11) covers them instead.
168
+ */
169
+ export const INTERACTIVE_GAP = "a closed component set cannot force its own interactive states";
170
+
171
+ /**
172
+ * The entries module.
173
+ *
174
+ * Generated from the same list the barrel is generated from, which is what
175
+ * makes coverage structural: a component cannot be added to the set without
176
+ * appearing here, because both come from one array.
177
+ */
178
+ function entriesSource(components, systemPackage) {
179
+ const missing = components.filter((c) => !CATALOGUE_ENTRIES[c]);
180
+ if (missing.length) {
181
+ throw new Error(
182
+ `No catalogue entry for ${missing.join(", ")}. A component that cannot be ` +
183
+ "demonstrated cannot be reviewed — add states before emitting it, or the " +
184
+ "catalogue documents a set it does not cover.",
185
+ );
186
+ }
187
+
188
+ const blocks = components.map((name) => {
189
+ const e = CATALOGUE_ENTRIES[name];
190
+ const states = e.states.map((s) =>
191
+ ` { label: ${JSON.stringify(s.label)}, node: ${exampleJsx(name, s, e)} },`).join("\n");
192
+
193
+ return ` {
194
+ name: ${JSON.stringify(name)},
195
+ purpose: ${JSON.stringify(e.purpose)},
196
+ replaces: ${JSON.stringify(e.replaces)},
197
+ notes: ${JSON.stringify(e.notes ?? [])},
198
+ examples: [
199
+ ${states}
200
+ ],
201
+ },`;
202
+ }).join("\n");
203
+
204
+ return `/* GENERATED BY GYDE — regenerate rather than edit.
205
+ *
206
+ * Entries are DATA, not JSX scattered through a page, so the coverage test can
207
+ * read this file as text. A test that imported it would resolve a typo'd
208
+ * component name to \`undefined\` and pass while covering nothing.
209
+ */
210
+ import type { ReactNode } from "react";
211
+ import { ${components.join(", ")} } from "${systemPackage}";
212
+
213
+ export type Example = { label: string; node: ReactNode };
214
+ export type Entry = { name: string; purpose: string; replaces: readonly string[]; notes: readonly string[]; examples: readonly Example[] };
215
+
216
+ export const ENTRIES: readonly Entry[] = [
217
+ ${blocks}
218
+ ];
219
+ `;
220
+ }
221
+
222
+ const app = (systemPackage, tokensPackage) => `/* GENERATED BY GYDE — then yours.
223
+ *
224
+ * The shell. It renders every entry, in BOTH THEMES side by side, because
225
+ * \`[data-theme]\` is emitted as an unqualified selector and therefore works on a
226
+ * container. A catalogue that can only show one theme at a time is a catalogue
227
+ * where a theme regression is found by a person switching back and forth.
228
+ */
229
+ import { ENTRIES } from "./entries";
230
+ import { Tokens } from "./Tokens";
231
+ import { Usage } from "./Usage";
232
+
233
+ export function App() {
234
+ return (
235
+ <main className="dx-page">
236
+ <h1 className="dx-title">The component set</h1>
237
+ <p className="dx-note">
238
+ Every component, every state, both themes. Nothing else is running —
239
+ no database, no network. If this page ever needs something else to
240
+ boot, that prerequisite is the bug.
241
+ </p>
242
+
243
+ <Tokens />
244
+
245
+ {ENTRIES.map((entry) => (
246
+ <section key={entry.name} className="dx-entry">
247
+ <h2 className="dx-entry__name">{entry.name}</h2>
248
+ <p className="dx-note">{entry.purpose}</p>
249
+ <p className="dx-note">Replaces: {entry.replaces.join(", ")}</p>
250
+ {entry.notes.map((n) => (
251
+ <p key={n} className="dx-note dx-note--gap">Not shown here: {n}</p>
252
+ ))}
253
+ <Usage name={entry.name} />
254
+
255
+ {entry.examples.map((ex) => (
256
+ <div key={ex.label} className="dx-example">
257
+ <div className="dx-example__label">{ex.label}</div>
258
+ {/* Both themes, side by side, from one render. */}
259
+ <div className="dx-themes">
260
+ <div data-theme="light" className="dx-theme">{ex.node}</div>
261
+ <div data-theme="dark" className="dx-theme">{ex.node}</div>
262
+ </div>
263
+ </div>
264
+ ))}
265
+
266
+ {/* The runtime-theming path, which no static example exercises.
267
+ Its absence is what cost a real product two apps' adoption. */}
268
+ {entry.name === "Card" ? <ThemedExample /> : null}
269
+ </section>
270
+ ))}
271
+ </main>
272
+ );
273
+ }
274
+
275
+ import { Card, themeVars } from "${systemPackage}";
276
+
277
+ function ThemedExample() {
278
+ return (
279
+ <div className="dx-example">
280
+ <div className="dx-example__label">runtime theme — a per-tenant brand</div>
281
+ <div style={themeVars({ surface: "#1d2b53", text: "#f2f0e6", border: "#3b4d7a" })}>
282
+ <Card tone="themed" inset="md">
283
+ A surface taking colours resolved at request time, with the dictionary
284
+ as its fallback.
285
+ </Card>
286
+ </div>
287
+ </div>
288
+ );
289
+ }
290
+ `;
291
+
292
+ const tokensPage = (tokensPackage) => `/* GENERATED BY GYDE — then yours. */
293
+ import { tokens } from "${tokensPackage}/dictionary.mjs";
294
+
295
+ /**
296
+ * The dictionary, rendered.
297
+ *
298
+ * Every token with its ROLE beside it, because a swatch without a purpose is
299
+ * how a token gets used for the wrong thing. Both themes, for the same reason
300
+ * the components show both.
301
+ */
302
+ export function Tokens() {
303
+ return (
304
+ <section className="dx-entry">
305
+ <h2 className="dx-entry__name">Tokens</h2>
306
+ {Object.entries(tokens).map(([group, entries]) => (
307
+ <div key={group} className="dx-tokens">
308
+ <h3 className="dx-tokens__group">{group}</h3>
309
+ {Object.entries(entries as Record<string, { value: unknown; role: string }>).map(([name, token]) => (
310
+ <div key={name} className="dx-token">
311
+ <code className="dx-token__name">--{group}-{name}</code>
312
+ <span className="dx-token__role">{token.role}</span>
313
+ </div>
314
+ ))}
315
+ </div>
316
+ ))}
317
+ </section>
318
+ );
319
+ }
320
+ `;
321
+
322
+ const USAGE_PAGE = `/* GENERATED BY GYDE — regenerate rather than edit. */
323
+ import usage from "./usage.json";
324
+
325
+ /**
326
+ * Where each component is actually rendered.
327
+ *
328
+ * Read from a committed file because this is a browser app with no filesystem.
329
+ * The index that produced it excludes THIS catalogue's entries — a component
330
+ * demonstrating itself is not a use — but not the catalogue's own chrome, which
331
+ * is. A real index once reported a component unused while it sat at the top of
332
+ * a page demonstrating itself.
333
+ */
334
+ export function Usage({ name }: { name: string }) {
335
+ const sites: string[] = (usage.used as Record<string, string[]>)[name] ?? [];
336
+ if (sites.length === 0) {
337
+ return <p className="dx-note">Not rendered anywhere yet — either use it, or delete it.</p>;
338
+ }
339
+ return <p className="dx-note">Used in {sites.length} place{sites.length === 1 ? "" : "s"}.</p>;
340
+ }
341
+ `;
342
+
343
+ const CHROME_CSS = `/* GENERATED BY GYDE — then yours.
344
+ *
345
+ * The catalogue's own chrome, prefixed \`dx-\` so it is a DECLARED vocabulary
346
+ * rather than bespoke styling the audit has to guess about. A surface that is
347
+ * legitimately outside the component set says so at every call site, instead of
348
+ * being excluded by path in a config nobody reads.
349
+ */
350
+ .dx-page { padding: var(--space-6); font-family: var(--font-sans); background: var(--color-surface); color: var(--color-text); }
351
+ .dx-title { font-size: var(--text-display); line-height: var(--line-height-flush); }
352
+ .dx-note { color: var(--color-muted); font-size: var(--text-small); }
353
+ .dx-entry { margin-top: var(--space-7); border-top: var(--border-hair) solid var(--color-border); padding-top: var(--space-5); }
354
+ .dx-entry__name { font-size: var(--text-title); }
355
+ .dx-example { margin-top: var(--space-5); }
356
+ .dx-example__label { font-size: var(--text-micro); color: var(--color-faint); margin-bottom: var(--space-2); }
357
+ .dx-themes { display: flex; gap: var(--space-4); }
358
+ .dx-theme { flex: 1; padding: var(--space-4); background: var(--color-surface); border-radius: var(--radius-card); }
359
+ .dx-tokens { margin-top: var(--space-4); }
360
+ .dx-token { display: flex; gap: var(--space-4); font-size: var(--text-small); }
361
+ .dx-token__name { font-family: var(--font-mono); color: var(--color-muted); min-width: 16rem; }
362
+ .dx-token__role { color: var(--color-text); }
363
+ `;
364
+
365
+ const VITE_CONFIG = `/* GENERATED BY GYDE — then yours.
366
+ *
367
+ * DO NOT ADD A PREREQUISITE TO THIS FILE.
368
+ *
369
+ * The catalogue boots with nothing else running — no database, no network, no
370
+ * other app. That is not a convenience, it is the test of whether it is still
371
+ * honest: a catalogue that needs the product running can only show what the
372
+ * product's state allows, which is a screenshot with extra steps.
373
+ *
374
+ * If this config ever grows a proxy, an env requirement or a fixture server,
375
+ * the prerequisite is the bug. Gyde's gate fails on it.
376
+ */
377
+ import { defineConfig } from "vite";
378
+ import react from "@vitejs/plugin-react";
379
+
380
+ export default defineConfig({
381
+ plugins: [react()],
382
+ server: { port: 3300 },
383
+ build: { outDir: "dist" },
384
+ });
385
+ `;
386
+
387
+ const COVERAGE_TEST = `/* GENERATED BY GYDE — then yours. */
388
+ import { test } from "node:test";
389
+ import assert from "node:assert/strict";
390
+ import { readFileSync } from "node:fs";
391
+ import { fileURLToPath } from "node:url";
392
+ import { dirname, join } from "node:path";
393
+
394
+ const HERE = dirname(fileURLToPath(import.meta.url));
395
+
396
+ /**
397
+ * Coverage, read as TEXT rather than imported.
398
+ *
399
+ * An import resolves a typo'd name to \`undefined\`, so an import-based check
400
+ * passes while covering nothing. Both files are read as source for that reason.
401
+ *
402
+ * This test is a backstop, not the mechanism. Coverage is true by construction
403
+ * because the barrel and the entries are generated from one list — if this ever
404
+ * fails, something has been hand-edited, and that is worth knowing.
405
+ */
406
+ test("every exported component appears in the catalogue", () => {
407
+ const barrel = readFileSync(join(HERE, "../../../packages/design-system/src/index.ts"), "utf8");
408
+ const entries = readFileSync(join(HERE, "entries.tsx"), "utf8");
409
+
410
+ const exported = [...barrel.matchAll(/export \\{ (\\w+) \\}/g)].map((m) => m[1]);
411
+ assert.ok(exported.length > 0, "read no components out of the barrel");
412
+
413
+ for (const name of exported) {
414
+ assert.match(entries, new RegExp(\`name: "\${name}"\`), \`\${name} is exported and not catalogued\`);
415
+ }
416
+ });
417
+
418
+ test("the catalogue lists nothing that is not exported", () => {
419
+ const barrel = readFileSync(join(HERE, "../../../packages/design-system/src/index.ts"), "utf8");
420
+ const entries = readFileSync(join(HERE, "entries.tsx"), "utf8");
421
+ const exported = new Set([...barrel.matchAll(/export \\{ (\\w+) \\}/g)].map((m) => m[1]));
422
+
423
+ for (const m of entries.matchAll(/name: "(\\w+)"/g)) {
424
+ assert.ok(exported.has(m[1]), \`\${m[1]} is catalogued and not exported\`);
425
+ }
426
+ });
427
+
428
+ test("every entry shows more than one state", () => {
429
+ // A single example is a screenshot. The defects worth catching are states.
430
+ const entries = readFileSync(join(HERE, "entries.tsx"), "utf8");
431
+ for (const block of entries.split(/\\n \\{\\n/).slice(1)) {
432
+ const name = block.match(/name: "(\\w+)"/)?.[1];
433
+ if (!name) continue;
434
+ const labels = [...block.matchAll(/label: "/g)].length;
435
+ assert.ok(labels >= 2, \`\${name} shows \${labels} state(s); a single example proves nothing\`);
436
+ }
437
+ });
438
+ `;
439
+
440
+ /**
441
+ * Emit the catalogue.
442
+ *
443
+ * Throws when a component has no entry, rather than emitting a catalogue that
444
+ * silently covers less than the set. That is the whole guarantee: a component
445
+ * you cannot demonstrate is one nobody can review.
446
+ */
447
+ export function emitCatalogue({
448
+ components,
449
+ path = "apps/design",
450
+ /**
451
+ * The names the SCAFFOLDED workspace uses, not names Gyde invented.
452
+ *
453
+ * The first version hardcoded `@gyde-scaffold/*`, so every emitted import
454
+ * resolved to nothing and a scaffolded repository would not build. Every test
455
+ * passed, because each compared the emitted text against itself. The CLI now
456
+ * derives these from the workspace's own scope.
457
+ */
458
+ systemPackage = "@scaffold/design-system",
459
+ tokensPackage = "@scaffold/design-tokens",
460
+ cataloguePackage = "@scaffold/design-catalogue",
461
+ } = {}) {
462
+ return {
463
+ /**
464
+ * The catalogue is an app, so it needs a manifest.
465
+ *
466
+ * Found by Gyde's own scan reporting `apps/design — no package.json`:
467
+ * without one it cannot be started, cannot be filtered to by the package
468
+ * manager, and is invisible to workspace discovery — a catalogue nobody can
469
+ * run is a catalogue nobody looks at, which is the whole value gone.
470
+ *
471
+ * `dev` and `build` and nothing else. The moment this grows a script that
472
+ * needs another process, the honesty test in vite.config.ts is broken.
473
+ */
474
+ [`${path}/package.json`]: JSON.stringify({
475
+ name: `${cataloguePackage}`,
476
+ version: "0.0.0",
477
+ private: true,
478
+ type: "module",
479
+ scripts: { dev: "vite", build: "vite build", test: "node --test src/*.test.mjs" },
480
+ dependencies: { [systemPackage]: "workspace:*", [tokensPackage]: "workspace:*", react: "^19.0.0", "react-dom": "^19.0.0" },
481
+ devDependencies: { vite: "^6.0.0", "@vitejs/plugin-react": "^4.3.0" },
482
+ }, null, 2) + "\n",
483
+ [`${path}/src/entries.tsx`]: entriesSource(components, systemPackage),
484
+ [`${path}/src/App.tsx`]: app(systemPackage, tokensPackage),
485
+ [`${path}/src/Tokens.tsx`]: tokensPage(tokensPackage),
486
+ [`${path}/src/Usage.tsx`]: USAGE_PAGE,
487
+ [`${path}/src/chrome.css`]: CHROME_CSS,
488
+ [`${path}/src/usage.json`]: JSON.stringify({ used: {}, unused: components, leftovers: {} }, null, 2) + "\n",
489
+ [`${path}/src/coverage.test.mjs`]: COVERAGE_TEST,
490
+ [`${path}/vite.config.ts`]: VITE_CONFIG,
491
+ [`${path}/src/main.tsx`]: `/* GENERATED BY GYDE — then yours. */
492
+ import { createRoot } from "react-dom/client";
493
+ import "${systemPackage}/styles.css";
494
+ import "./chrome.css";
495
+ import { App } from "./App";
496
+
497
+ createRoot(document.getElementById("root")!).render(<App />);
498
+ `,
499
+ [`${path}/index.html`]: `<!doctype html>
500
+ <html lang="en">
501
+ <head><meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" /><title>Design system</title></head>
502
+ <body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body>
503
+ </html>
504
+ `,
505
+ };
506
+ }