@nghitrum/dsforge 0.1.1

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.
@@ -0,0 +1,2417 @@
1
+ // src/generators/showcase/types.ts
2
+ function esc(s) {
3
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
4
+ }
5
+ function isHex(v) {
6
+ return /^#[0-9a-fA-F]{3,8}$/.test(v);
7
+ }
8
+ function hexLuminance(hex) {
9
+ const c = hex.replace("#", "");
10
+ const full = c.length === 3 ? c.split("").map((x) => x + x).join("") : c;
11
+ const r = parseInt(full.slice(0, 2), 16) / 255;
12
+ const g = parseInt(full.slice(2, 4), 16) / 255;
13
+ const b = parseInt(full.slice(4, 6), 16) / 255;
14
+ const toLinear = (x) => x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4;
15
+ return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
16
+ }
17
+ function textOnColor(hex) {
18
+ return hexLuminance(hex) > 0.35 ? "#111827" : "#ffffff";
19
+ }
20
+ function componentTokens(config, tokens) {
21
+ return {
22
+ action: tokens["semantic.color-action"] ?? "#2563eb",
23
+ actionHover: tokens["semantic.color-action-hover"] ?? "#1d4ed8",
24
+ actionText: tokens["semantic.color-text-on-color"] ?? "#ffffff",
25
+ bg: tokens["semantic.color-bg-default"] ?? "#ffffff",
26
+ border: tokens["semantic.color-border-default"] ?? "#e2e8f0",
27
+ text: tokens["semantic.color-text-primary"] ?? "#0f172a",
28
+ textSecondary: tokens["semantic.color-text-secondary"] ?? "#64748b",
29
+ radiusMd: config.radius?.["md"] ?? 4,
30
+ radiusLg: config.radius?.["lg"] ?? 8,
31
+ ff: config.typography?.fontFamily ?? "system-ui, sans-serif"
32
+ };
33
+ }
34
+
35
+ // src/lib/license.ts
36
+ import { readFileSync } from "fs";
37
+ import { join } from "path";
38
+ function readKeyFromDotEnv() {
39
+ try {
40
+ const content = readFileSync(join(process.cwd(), ".env"), "utf8");
41
+ for (const raw of content.split("\n")) {
42
+ const line = raw.trim();
43
+ if (!line || line.startsWith("#")) continue;
44
+ const eq = line.indexOf("=");
45
+ if (eq === -1) continue;
46
+ const key = line.slice(0, eq).trim();
47
+ if (key !== "DSFORGE_KEY") continue;
48
+ const val = line.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
49
+ return val || void 0;
50
+ }
51
+ } catch {
52
+ }
53
+ return void 0;
54
+ }
55
+ function isProUnlocked() {
56
+ const key = process.env["DSFORGE_KEY"] ?? readKeyFromDotEnv();
57
+ return typeof key === "string" && key.length > 0;
58
+ }
59
+
60
+ // src/generators/showcase/foundations.ts
61
+ function buildColorSection(config, tokens) {
62
+ const groups = [];
63
+ const globalEntries = Object.entries(config.tokens?.global ?? {}).filter(([, v]) => isHex(String(v))).map(([k, v]) => ({ name: k, value: String(v) }));
64
+ if (globalEntries.length)
65
+ groups.push({ title: "Global Palette", items: globalEntries });
66
+ const semanticEntries = Object.entries(tokens).filter(([k, v]) => k.startsWith("semantic.color") && isHex(v)).map(([k, v]) => ({ name: k.replace("semantic.", ""), value: v }));
67
+ if (semanticEntries.length)
68
+ groups.push({ title: "Semantic Colors", items: semanticEntries });
69
+ return groups.map(
70
+ ({ title, items }) => `
71
+ <div class="section-block">
72
+ <h3 class="group-title">${esc(title)}</h3>
73
+ <div class="swatch-grid">
74
+ ${items.map(
75
+ ({ name, value }) => `
76
+ <div class="swatch" style="background:${value};color:${textOnColor(value)}">
77
+ <span class="swatch-name">${esc(name)}</span>
78
+ <span class="swatch-value">${esc(value)}</span>
79
+ </div>
80
+ `
81
+ ).join("")}
82
+ </div>
83
+ </div>
84
+ `
85
+ ).join("");
86
+ }
87
+ function buildTypographySection(config) {
88
+ const roles = config.typography?.roles ?? {};
89
+ const ff = config.typography?.fontFamily ?? "system-ui, sans-serif";
90
+ return `
91
+ <div class="section-block">
92
+ <h3 class="group-title">Font Family</h3>
93
+ <div class="type-family">${esc(ff)}</div>
94
+ </div>
95
+ <div class="section-block">
96
+ <h3 class="group-title">Type Scale</h3>
97
+ <div class="type-scale">
98
+ ${Object.entries(roles).map(([role, def]) => {
99
+ const size = typeof def === "object" && def !== null ? def["size"] : 16;
100
+ const weight = typeof def === "object" && def !== null ? def["weight"] : 400;
101
+ const lh = typeof def === "object" && def !== null ? def["lineHeight"] : 1.5;
102
+ return `
103
+ <div class="type-row">
104
+ <div class="type-meta">
105
+ <span class="type-role">${esc(role)}</span>
106
+ <span class="type-spec">${size}px / ${weight} / lh ${lh}</span>
107
+ </div>
108
+ <div class="type-sample" style="font-size:${size}px;font-weight:${weight};line-height:${lh};font-family:${esc(ff)}">
109
+ The quick brown fox
110
+ </div>
111
+ </div>
112
+ `;
113
+ }).join("")}
114
+ </div>
115
+ </div>
116
+ `;
117
+ }
118
+ function buildSpacingSection(config) {
119
+ const scale = config.spacing?.scale ?? {};
120
+ const baseUnit = config.spacing?.baseUnit ?? 4;
121
+ const row = (key, val) => `
122
+ <div class="spacing-row">
123
+ <span class="spacing-key">${esc(key)}</span>
124
+ <div class="spacing-bar-wrap">
125
+ <div class="spacing-bar" style="width:${Math.min(Number(val) * 2, 320)}px"></div>
126
+ </div>
127
+ <span class="spacing-val">${val}px</span>
128
+ </div>`;
129
+ return `
130
+ <div class="section-block">
131
+ <h3 class="group-title">Base Unit: ${baseUnit}px</h3>
132
+ <div class="spacing-list">
133
+ ${Object.entries(scale).map(([k, v]) => row(k, v)).join("")}
134
+ </div>
135
+ </div>
136
+ <div class="section-block">
137
+ <h3 class="group-title">Semantic Spacing</h3>
138
+ <div class="spacing-list">
139
+ ${Object.entries(config.spacing?.semantic ?? {}).map(([k, v]) => row(k, v)).join("")}
140
+ </div>
141
+ </div>
142
+ `;
143
+ }
144
+ function buildRadiusSection(config) {
145
+ const radius = config.radius ?? {};
146
+ return `
147
+ <div class="section-block">
148
+ <h3 class="group-title">Border Radius</h3>
149
+ <div class="radius-grid">
150
+ ${Object.entries(radius).map(
151
+ ([key, val]) => `
152
+ <div class="radius-item">
153
+ <div class="radius-box" style="border-radius:${val}px"></div>
154
+ <span class="radius-key">${esc(key)}</span>
155
+ <span class="radius-val">${val}px</span>
156
+ </div>
157
+ `
158
+ ).join("")}
159
+ </div>
160
+ </div>
161
+ `;
162
+ }
163
+ function buildElevationSection(config) {
164
+ const elevation = config.elevation ?? {};
165
+ return `
166
+ <div class="section-block">
167
+ <h3 class="group-title">Elevation</h3>
168
+ <div class="elevation-grid">
169
+ ${Object.entries(elevation).map(
170
+ ([key, val]) => `
171
+ <div class="elevation-item">
172
+ <div class="elevation-box" style="box-shadow:${val === "none" ? "none" : val}"></div>
173
+ <span class="elevation-key">Level ${esc(key)}</span>
174
+ <span class="elevation-val">${esc(String(val))}</span>
175
+ </div>
176
+ `
177
+ ).join("")}
178
+ </div>
179
+ </div>
180
+ `;
181
+ }
182
+ function buildMotionSection(config) {
183
+ const duration = config.motion?.duration ?? {};
184
+ const easing = config.motion?.easing ?? {};
185
+ const dot = (transitionStyle, onclick) => `
186
+ <div class="motion-track" onclick="${onclick}">
187
+ <div class="motion-dot" style="transition:${transitionStyle}"></div>
188
+ </div>`;
189
+ return `
190
+ <div class="section-block">
191
+ <h3 class="group-title">Duration</h3>
192
+ <div class="motion-grid">
193
+ ${Object.entries(duration).map(
194
+ ([key, val]) => `
195
+ <div class="motion-item" onclick="this.querySelector('.motion-dot').style.transform='translateX(120px)';setTimeout(()=>this.querySelector('.motion-dot').style.transform='',${val}+50)">
196
+ ${dot(`transform ${val}ms linear`, "")}
197
+ <span class="motion-key">${esc(key)}</span>
198
+ <span class="motion-val">${val}ms</span>
199
+ <span class="motion-hint">click to preview</span>
200
+ </div>
201
+ `
202
+ ).join("")}
203
+ </div>
204
+ </div>
205
+ <div class="section-block">
206
+ <h3 class="group-title">Easing</h3>
207
+ <div class="motion-grid">
208
+ ${Object.entries(easing).map(
209
+ ([key, val]) => `
210
+ <div class="motion-item" onclick="this.querySelector('.motion-dot').style.transform='translateX(120px)';setTimeout(()=>this.querySelector('.motion-dot').style.transform='',500)">
211
+ ${dot(`transform 500ms ${val}`, "")}
212
+ <span class="motion-key">${esc(key)}</span>
213
+ <span class="motion-val">${esc(String(val))}</span>
214
+ <span class="motion-hint">click to preview</span>
215
+ </div>
216
+ `
217
+ ).join("")}
218
+ </div>
219
+ </div>
220
+ `;
221
+ }
222
+
223
+ // src/generators/showcase/page.ts
224
+ var lockedPanel = (label) => `
225
+ <div class="locked-panel">
226
+ <div class="locked-icon">\u2298</div>
227
+ <div class="locked-title">${label} \u2014 dsforge Pro</div>
228
+ <p class="locked-desc">This tab is available with a dsforge Pro license.</p>
229
+ <p class="locked-hint">Set the <code>DSFORGE_KEY</code> environment variable to unlock.</p>
230
+ </div>`;
231
+ function buildComponentPage(def, isPro) {
232
+ const tabId = (tab) => `${def.id}-tab-${tab}`;
233
+ const panelId = (tab) => `${def.id}-panel-${tab}`;
234
+ const overviewHtml = `<div class="comp-overview">${def.overviewHtml}</div>`;
235
+ const propsTable = `
236
+ <table class="props-table">
237
+ <thead>
238
+ <tr>
239
+ <th>Prop</th>
240
+ <th>Required</th>
241
+ <th>Type</th>
242
+ <th>Default</th>
243
+ <th>Description</th>
244
+ </tr>
245
+ </thead>
246
+ <tbody>
247
+ ${def.props.map(
248
+ (p) => `
249
+ <tr>
250
+ <td><code class="prop-name">${esc(p.name)}</code></td>
251
+ <td class="prop-required-cell">
252
+ ${p.required ? '<span class="prop-required">Yes</span>' : '<span class="prop-optional">\u2014</span>'}
253
+ </td>
254
+ <td><code class="prop-type">${esc(p.type)}</code></td>
255
+ <td><code class="prop-default">${esc(p.default)}</code></td>
256
+ <td class="prop-desc">${esc(p.description)}</td>
257
+ </tr>`
258
+ ).join("")}
259
+ </tbody>
260
+ </table>`;
261
+ const examplesHtml = def.examples.map(
262
+ (ex, i) => `
263
+ <div class="example-block">
264
+ <div class="example-header">
265
+ <div class="example-label">${esc(ex.label)}</div>
266
+ <div class="example-desc">${esc(ex.description)}</div>
267
+ </div>
268
+ <div class="example-preview">${ex.previewHtml}</div>
269
+ <div class="example-code-wrap">
270
+ <div class="example-code-bar">
271
+ <span>TSX</span>
272
+ <button class="copy-btn" onclick="copyCode('${def.id}-ex-${i}', this)">Copy</button>
273
+ </div>
274
+ <pre class="example-code" id="${def.id}-ex-${i}">${esc(ex.code)}</pre>
275
+ </div>
276
+ </div>`
277
+ ).join("");
278
+ const a11yHtml = `
279
+ <div class="a11y-list">
280
+ ${def.a11y.map(
281
+ (item) => `
282
+ <div class="a11y-item">
283
+ <div class="a11y-header">
284
+ <span class="a11y-criterion">${esc(item.criterion)}</span>
285
+ <span class="a11y-badge a11y-badge-${item.level.toLowerCase()}">WCAG ${item.level}</span>
286
+ </div>
287
+ <p class="a11y-desc">${esc(item.description)}</p>
288
+ </div>`
289
+ ).join("")}
290
+ </div>`;
291
+ const aiJson = JSON.stringify(def.aiMeta, null, 2);
292
+ const aiHtml = `
293
+ <div class="ai-meta-intro">
294
+ <p>This JSON contract is emitted to <code>dist-ds/metadata/${def.id}.json</code>.
295
+ AI coding assistants use it to understand when and how to use this component correctly.</p>
296
+ </div>
297
+ <div class="example-code-wrap" style="margin-top:16px">
298
+ <div class="example-code-bar">
299
+ <span>JSON</span>
300
+ <button class="copy-btn" onclick="copyCode('${def.id}-ai-meta', this)">Copy</button>
301
+ </div>
302
+ <pre class="example-code" id="${def.id}-ai-meta">${esc(aiJson)}</pre>
303
+ </div>
304
+ <div class="ai-guidance">
305
+ <div class="group-title" style="margin-top:24px">AI usage guidance</div>
306
+ <ul class="ai-guidance-list">
307
+ ${def.aiMeta.aiGuidance.map((g) => `<li>${esc(g)}</li>`).join("")}
308
+ </ul>
309
+ </div>`;
310
+ const tabs = [
311
+ { id: "overview", label: "Overview", content: overviewHtml, locked: false },
312
+ { id: "props", label: "Props", content: propsTable, locked: false },
313
+ { id: "examples", label: "Examples", content: examplesHtml, locked: false },
314
+ {
315
+ id: "accessibility",
316
+ label: "Accessibility",
317
+ content: isPro ? a11yHtml : lockedPanel("Accessibility"),
318
+ locked: !isPro
319
+ },
320
+ {
321
+ id: "ai-metadata",
322
+ label: "AI Metadata",
323
+ content: isPro ? aiHtml : lockedPanel("AI Metadata"),
324
+ locked: !isPro
325
+ }
326
+ ];
327
+ return `
328
+ <div class="comp-tabs" id="${def.id}-tabs">
329
+ <div class="comp-tab-bar" role="tablist">
330
+ ${tabs.map(
331
+ (t, i) => `
332
+ <button
333
+ class="comp-tab${i === 0 ? " active" : ""}${t.locked ? " locked" : ""}"
334
+ id="${tabId(t.id)}"
335
+ role="tab"
336
+ aria-selected="${i === 0}"
337
+ aria-controls="${panelId(t.id)}"
338
+ onclick="${t.locked ? "return false" : `switchTab('${def.id}', '${t.id}', this)`}"
339
+ ${t.locked ? 'title="Unlock with dsforge Pro"' : ""}
340
+ >${esc(t.label)}${t.locked ? " &#x1F512;" : ""}</button>`
341
+ ).join("")}
342
+ </div>
343
+ ${tabs.map(
344
+ (t, i) => `
345
+ <div
346
+ class="comp-tab-panel${i === 0 ? " active" : ""}"
347
+ id="${panelId(t.id)}"
348
+ role="tabpanel"
349
+ aria-labelledby="${tabId(t.id)}"
350
+ >${t.content}</div>`
351
+ ).join("")}
352
+ </div>`;
353
+ }
354
+
355
+ // src/generators/showcase/components/button.ts
356
+ function buttonDef(config, tokens) {
357
+ const { radiusMd, ff } = componentTokens(config, tokens);
358
+ const r = `${radiusMd}px`;
359
+ const s = (extra = "") => `style="font-family:${esc(ff)};${extra}"`;
360
+ const C = {
361
+ action: "var(--color-action, #2563eb)",
362
+ actionText: "var(--color-text-on-color, #fff)",
363
+ text: "var(--color-text-primary, #0f172a)"
364
+ };
365
+ return {
366
+ id: "button",
367
+ label: "Button",
368
+ description: "Triggers an action or event. Use for form submissions, dialogs, and in-page actions.",
369
+ overviewHtml: `
370
+ <div class="comp-overview-section">
371
+ <div class="comp-overview-label">Variants</div>
372
+ <div class="comp-preview-row">
373
+ <button class="ds-btn" ${s(`background:${C.action};color:${C.actionText};border-radius:${r}`)}>Primary</button>
374
+ <button class="ds-btn" ${s(`background:transparent;color:${C.action};border:1.5px solid ${C.action};border-radius:${r}`)}>Secondary</button>
375
+ <button class="ds-btn" ${s(`background:#dc2626;color:#fff;border-radius:${r}`)}>Danger</button>
376
+ <button class="ds-btn" ${s(`background:transparent;color:${C.text};border-radius:${r}`)}>Ghost</button>
377
+ <button class="ds-btn" ${s(`background:${C.action};color:${C.actionText};border-radius:${r};opacity:0.4;cursor:not-allowed`)} disabled>Disabled</button>
378
+ </div>
379
+ </div>
380
+ <div class="comp-overview-section">
381
+ <div class="comp-overview-label">Sizes</div>
382
+ <div class="comp-preview-row" style="align-items:center">
383
+ <button class="ds-btn" ${s(`background:${C.action};color:${C.actionText};border-radius:${r};font-size:12px;padding:4px 12px`)}>Small</button>
384
+ <button class="ds-btn" ${s(`background:${C.action};color:${C.actionText};border-radius:${r};font-size:14px;padding:8px 16px`)}>Medium</button>
385
+ <button class="ds-btn" ${s(`background:${C.action};color:${C.actionText};border-radius:${r};font-size:16px;padding:12px 24px`)}>Large</button>
386
+ </div>
387
+ </div>`,
388
+ props: [
389
+ {
390
+ name: "variant",
391
+ type: '"primary" | "secondary" | "danger" | "ghost"',
392
+ default: '"primary"',
393
+ required: false,
394
+ description: "Visual style of the button."
395
+ },
396
+ {
397
+ name: "size",
398
+ type: '"sm" | "md" | "lg"',
399
+ default: '"md"',
400
+ required: false,
401
+ description: "Controls padding and font size."
402
+ },
403
+ {
404
+ name: "disabled",
405
+ type: "boolean",
406
+ default: "false",
407
+ required: false,
408
+ description: "Prevents interaction and applies reduced opacity."
409
+ },
410
+ {
411
+ name: "loading",
412
+ type: "boolean",
413
+ default: "false",
414
+ required: false,
415
+ description: "Shows a spinner and disables the button."
416
+ },
417
+ {
418
+ name: "onClick",
419
+ type: "() => void",
420
+ default: "\u2014",
421
+ required: false,
422
+ description: "Callback fired on click."
423
+ },
424
+ {
425
+ name: "type",
426
+ type: '"button" | "submit" | "reset"',
427
+ default: '"button"',
428
+ required: false,
429
+ description: "Native button type. Always set explicitly inside forms."
430
+ },
431
+ {
432
+ name: "children",
433
+ type: "React.ReactNode",
434
+ default: "\u2014",
435
+ required: true,
436
+ description: "Button label. Prefer plain text; icons should have aria-label."
437
+ },
438
+ {
439
+ name: "className",
440
+ type: "string",
441
+ default: "\u2014",
442
+ required: false,
443
+ description: "Additional CSS classes on the root element."
444
+ }
445
+ ],
446
+ examples: [
447
+ {
448
+ label: "Basic usage",
449
+ description: "The default primary button.",
450
+ code: `<Button onClick={() => console.log('clicked')}>
451
+ Save changes
452
+ </Button>`,
453
+ previewHtml: `<button class="ds-btn" ${s(`background:${C.action};color:${C.actionText};border-radius:${r}`)}>Save changes</button>`
454
+ },
455
+ {
456
+ label: "Danger action",
457
+ description: "Use the danger variant for destructive or irreversible actions.",
458
+ code: `<Button variant="danger" onClick={handleDelete}>
459
+ Delete account
460
+ </Button>`,
461
+ previewHtml: `<button class="ds-btn" ${s(`background:#dc2626;color:#fff;border-radius:${r}`)}>Delete account</button>`
462
+ },
463
+ {
464
+ label: "Loading state",
465
+ description: "Pass loading to show a spinner while an async action is in progress.",
466
+ code: `<Button loading onClick={handleSubmit}>
467
+ Saving\u2026
468
+ </Button>`,
469
+ previewHtml: `<button class="ds-btn" ${s(`background:${C.action};color:${C.actionText};border-radius:${r};opacity:0.7;cursor:not-allowed`)} disabled>\u27F3 &nbsp;Saving\u2026</button>`
470
+ },
471
+ {
472
+ label: "Button group",
473
+ description: "Combine secondary and primary for confirm/cancel pairs.",
474
+ code: `<div style={{ display: 'flex', gap: 8 }}>
475
+ <Button variant="secondary" onClick={onCancel}>Cancel</Button>
476
+ <Button onClick={onConfirm}>Confirm</Button>
477
+ </div>`,
478
+ previewHtml: `<div style="display:flex;gap:8px">
479
+ <button class="ds-btn" ${s(`background:transparent;color:${C.action};border:1.5px solid ${C.action};border-radius:${r}`)}>Cancel</button>
480
+ <button class="ds-btn" ${s(`background:${C.action};color:${C.actionText};border-radius:${r}`)}>Confirm</button>
481
+ </div>`
482
+ }
483
+ ],
484
+ a11y: [
485
+ {
486
+ criterion: "1.3.1 Info and Relationships",
487
+ level: "A",
488
+ description: "Button role is conveyed via the native <button> element \u2014 no extra role attribute needed."
489
+ },
490
+ {
491
+ criterion: "1.4.3 Contrast (Minimum)",
492
+ level: "AA",
493
+ description: "Primary and danger variants are validated against WCAG AA (4.5:1 for text, 3:1 for UI components)."
494
+ },
495
+ {
496
+ criterion: "2.1.1 Keyboard",
497
+ level: "A",
498
+ description: "Fully operable with keyboard. Enter and Space activate the button. Tab moves focus."
499
+ },
500
+ {
501
+ criterion: "2.4.7 Focus Visible",
502
+ level: "AA",
503
+ description: "A visible focus ring is applied on :focus-visible. Never suppressed with outline:none."
504
+ },
505
+ {
506
+ criterion: "4.1.2 Name, Role, Value",
507
+ level: "A",
508
+ description: "Icon-only buttons must receive an aria-label. Loading state sets aria-busy='true'."
509
+ }
510
+ ],
511
+ aiMeta: {
512
+ component: "Button",
513
+ role: "action-trigger",
514
+ hierarchyLevel: "primary",
515
+ interactionModel: "synchronous",
516
+ layoutImpact: "inline",
517
+ destructiveVariants: ["danger"],
518
+ accessibilityContract: {
519
+ keyboard: true,
520
+ focusRing: "required",
521
+ ariaLabel: "required-for-icon-only",
522
+ ariaBusy: "set-when-loading"
523
+ },
524
+ variants: ["primary", "secondary", "danger", "ghost"],
525
+ aiGuidance: [
526
+ "Use primary for the single most important action on a surface.",
527
+ "Never place two primary buttons side by side.",
528
+ "Use danger only for irreversible destructive actions \u2014 always pair with a confirmation dialog.",
529
+ "Ghost is suitable for tertiary actions inside dense UI (toolbars, table rows)."
530
+ ]
531
+ }
532
+ };
533
+ }
534
+
535
+ // src/generators/showcase/components/input.ts
536
+ function inputDef(config, tokens) {
537
+ const { radiusMd, ff } = componentTokens(config, tokens);
538
+ const r = `${radiusMd}px`;
539
+ const C = {
540
+ text: "var(--color-text-primary, #0f172a)",
541
+ textSecondary: "var(--color-text-secondary, #64748b)",
542
+ bg: "var(--color-bg-default, #fff)",
543
+ border: "var(--color-border-default, #e2e8f0)"
544
+ };
545
+ const fieldHtml = (label, opts, borderColor = C.border, helperHtml = "") => `
546
+ <div class="ds-field" style="font-family:${esc(ff)};max-width:320px">
547
+ <label class="ds-label" style="color:${C.text}">${label}</label>
548
+ <input class="ds-input" style="border-color:${borderColor};border-radius:${r};color:${C.text};background:${C.bg}" ${opts} />
549
+ ${helperHtml}
550
+ </div>`;
551
+ return {
552
+ id: "input",
553
+ label: "Input",
554
+ description: "Single-line text field. Covers all standard input types with label, helper text, and validation states.",
555
+ overviewHtml: `
556
+ <div class="comp-overview-section">
557
+ <div class="comp-overview-label">States</div>
558
+ <div class="comp-preview-col">
559
+ ${fieldHtml("Default", 'placeholder="Placeholder text"')}
560
+ ${fieldHtml("With value", 'value="Some input value"')}
561
+ ${fieldHtml(
562
+ "Error",
563
+ 'value="invalid@"',
564
+ "#dc2626",
565
+ `<span style="font-size:12px;color:#dc2626;margin-top:2px">Enter a valid email address</span>`
566
+ )}
567
+ ${fieldHtml("Disabled", 'disabled placeholder="Disabled" style="opacity:0.5"')}
568
+ </div>
569
+ </div>`,
570
+ props: [
571
+ {
572
+ name: "label",
573
+ type: "string",
574
+ default: "\u2014",
575
+ required: true,
576
+ description: "Visible label rendered above the field. Also used as the accessible name."
577
+ },
578
+ {
579
+ name: "value",
580
+ type: "string",
581
+ default: "\u2014",
582
+ required: false,
583
+ description: "Controlled value. Pair with onChange."
584
+ },
585
+ {
586
+ name: "onChange",
587
+ type: "(value: string) => void",
588
+ default: "\u2014",
589
+ required: false,
590
+ description: "Fires on every keystroke."
591
+ },
592
+ {
593
+ name: "placeholder",
594
+ type: "string",
595
+ default: "\u2014",
596
+ required: false,
597
+ description: "Hint text shown when value is empty. Not a substitute for label."
598
+ },
599
+ {
600
+ name: "type",
601
+ type: '"text" | "email" | "password" | "number" | "search" | "tel" | "url"',
602
+ default: '"text"',
603
+ required: false,
604
+ description: "HTML input type."
605
+ },
606
+ {
607
+ name: "error",
608
+ type: "string",
609
+ default: "\u2014",
610
+ required: false,
611
+ description: "Validation message shown below the field. Triggers error styling."
612
+ },
613
+ {
614
+ name: "disabled",
615
+ type: "boolean",
616
+ default: "false",
617
+ required: false,
618
+ description: "Makes the field non-interactive."
619
+ },
620
+ {
621
+ name: "required",
622
+ type: "boolean",
623
+ default: "false",
624
+ required: false,
625
+ description: "Marks the field as required and sets aria-required."
626
+ },
627
+ {
628
+ name: "helperText",
629
+ type: "string",
630
+ default: "\u2014",
631
+ required: false,
632
+ description: "Supplementary text below the field (shown when no error)."
633
+ }
634
+ ],
635
+ examples: [
636
+ {
637
+ label: "Controlled input",
638
+ description: "Typical controlled usage with value and onChange.",
639
+ code: `const [email, setEmail] = useState('');
640
+
641
+ <Input
642
+ label="Email address"
643
+ type="email"
644
+ value={email}
645
+ onChange={setEmail}
646
+ placeholder="you@example.com"
647
+ />`,
648
+ previewHtml: fieldHtml(
649
+ "Email address",
650
+ 'type="email" placeholder="you@example.com"'
651
+ )
652
+ },
653
+ {
654
+ label: "With validation error",
655
+ description: "Pass an error string to show inline validation feedback.",
656
+ code: `<Input
657
+ label="Username"
658
+ value="ab"
659
+ error="Username must be at least 3 characters."
660
+ />`,
661
+ previewHtml: fieldHtml(
662
+ "Username",
663
+ 'value="ab"',
664
+ "#dc2626",
665
+ `<span style="font-size:12px;color:#dc2626;margin-top:2px">Username must be at least 3 characters.</span>`
666
+ )
667
+ },
668
+ {
669
+ label: "Password field",
670
+ description: "type='password' masks the value automatically.",
671
+ code: `<Input
672
+ label="Password"
673
+ type="password"
674
+ required
675
+ helperText="Must be at least 8 characters."
676
+ />`,
677
+ previewHtml: `
678
+ <div class="ds-field" style="font-family:${esc(ff)};max-width:320px">
679
+ <label class="ds-label" style="color:${C.text}">Password <span style="color:#dc2626">*</span></label>
680
+ <input class="ds-input" type="password" style="border-color:${C.border};border-radius:${r};color:${C.text};background:${C.bg}" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" />
681
+ <span style="font-size:12px;color:${C.textSecondary};margin-top:2px">Must be at least 8 characters.</span>
682
+ </div>`
683
+ }
684
+ ],
685
+ a11y: [
686
+ {
687
+ criterion: "1.3.1 Info and Relationships",
688
+ level: "A",
689
+ description: "Every input is programmatically associated with its label via htmlFor/id. Never use placeholder as a label."
690
+ },
691
+ {
692
+ criterion: "1.4.3 Contrast (Minimum)",
693
+ level: "AA",
694
+ description: "Text, placeholder, and border colour all meet WCAG AA at the configured token values."
695
+ },
696
+ {
697
+ criterion: "3.3.1 Error Identification",
698
+ level: "A",
699
+ description: "When an error is present, the field receives aria-invalid='true' and aria-describedby pointing to the error message."
700
+ },
701
+ {
702
+ criterion: "3.3.2 Labels or Instructions",
703
+ level: "A",
704
+ description: "The label prop is always rendered as a visible <label> element. helperText is associated via aria-describedby."
705
+ },
706
+ {
707
+ criterion: "4.1.2 Name, Role, Value",
708
+ level: "A",
709
+ description: "required maps to aria-required. disabled maps to the native disabled attribute, preventing all interaction."
710
+ }
711
+ ],
712
+ aiMeta: {
713
+ component: "Input",
714
+ role: "data-entry",
715
+ hierarchyLevel: "atomic",
716
+ interactionModel: "continuous",
717
+ layoutImpact: "block",
718
+ destructiveVariants: [],
719
+ accessibilityContract: {
720
+ labelRequired: true,
721
+ ariaInvalidOnError: true,
722
+ ariaDescribedByForHelper: true,
723
+ ariaRequired: "mirrors-required-prop"
724
+ },
725
+ variants: ["default", "error", "disabled"],
726
+ aiGuidance: [
727
+ "Always provide a label \u2014 never rely on placeholder alone.",
728
+ "Use error to surface validation results inline, not via alert/toast.",
729
+ "For password fields always use type='password' \u2014 never type='text'.",
730
+ "Group related inputs inside a <fieldset> with a <legend> for screen readers."
731
+ ]
732
+ }
733
+ };
734
+ }
735
+
736
+ // src/generators/showcase/components/card.ts
737
+ function cardDef(config, tokens) {
738
+ const { radiusMd, radiusLg, ff } = componentTokens(config, tokens);
739
+ const rMd = `${radiusMd}px`;
740
+ const rLg = `${radiusLg}px`;
741
+ const C = {
742
+ action: "var(--color-action, #2563eb)",
743
+ actionText: "var(--color-text-on-color, #fff)",
744
+ text: "var(--color-text-primary, #0f172a)",
745
+ textSecondary: "var(--color-text-secondary, #64748b)",
746
+ bg: "var(--color-bg-default, #fff)",
747
+ border: "var(--color-border-default, #e2e8f0)"
748
+ };
749
+ const cardHtml = (title, body, extraStyle = "", footerHtml = "") => `
750
+ <div class="ds-card" style="border-color:${C.border};border-radius:${rLg};background:${C.bg};color:${C.text};${extraStyle}">
751
+ <div class="ds-card-header" style="border-bottom:1px solid ${C.border}"><strong>${title}</strong></div>
752
+ <div class="ds-card-body"><p style="color:${C.textSecondary};margin:0;font-size:14px;font-family:${esc(ff)}">${body}</p></div>
753
+ ${footerHtml ? `<div class="ds-card-footer" style="border-top:1px solid ${C.border}">${footerHtml}</div>` : ""}
754
+ </div>`;
755
+ const footerBtn = `<button class="ds-btn" style="background:${C.action};color:${C.actionText};border-radius:${rMd};font-size:13px;padding:6px 14px;font-family:${esc(ff)}">Action</button>`;
756
+ return {
757
+ id: "card",
758
+ label: "Card",
759
+ description: "A surface that groups related content. Supports header, body, and optional footer slots.",
760
+ overviewHtml: `
761
+ <div class="comp-overview-section">
762
+ <div class="comp-overview-label">Variants</div>
763
+ <div class="comp-preview-row" style="align-items:flex-start;flex-wrap:wrap">
764
+ ${cardHtml("Default", "Bordered surface, no shadow.", "", footerBtn)}
765
+ ${cardHtml("Elevated", "Shadow replaces border.", `border-color:transparent;box-shadow:0 4px 6px -1px rgb(0 0 0 / 0.10)`)}
766
+ ${cardHtml("Outlined", "Stronger border, no shadow.", `border:2px solid ${C.border}`)}
767
+ </div>
768
+ </div>`,
769
+ props: [
770
+ {
771
+ name: "variant",
772
+ type: '"default" | "elevated" | "outlined"',
773
+ default: '"default"',
774
+ required: false,
775
+ description: "Visual style. Elevated uses box-shadow; outlined uses a heavier border."
776
+ },
777
+ {
778
+ name: "padding",
779
+ type: '"none" | "sm" | "md" | "lg"',
780
+ default: '"md"',
781
+ required: false,
782
+ description: "Inner padding of the card body."
783
+ },
784
+ {
785
+ name: "header",
786
+ type: "React.ReactNode",
787
+ default: "\u2014",
788
+ required: false,
789
+ description: "Content rendered in the card header slot."
790
+ },
791
+ {
792
+ name: "footer",
793
+ type: "React.ReactNode",
794
+ default: "\u2014",
795
+ required: false,
796
+ description: "Content rendered in the card footer slot."
797
+ },
798
+ {
799
+ name: "children",
800
+ type: "React.ReactNode",
801
+ default: "\u2014",
802
+ required: true,
803
+ description: "Main body content."
804
+ },
805
+ {
806
+ name: "className",
807
+ type: "string",
808
+ default: "\u2014",
809
+ required: false,
810
+ description: "Additional CSS classes on the root element."
811
+ }
812
+ ],
813
+ examples: [
814
+ {
815
+ label: "Basic card",
816
+ description: "Default bordered card with header, body, and a footer action.",
817
+ code: `<Card
818
+ header={<strong>Card title</strong>}
819
+ footer={<Button size="sm">Action</Button>}
820
+ >
821
+ Card body content goes here.
822
+ </Card>`,
823
+ previewHtml: cardHtml(
824
+ "Card title",
825
+ "Card body content goes here.",
826
+ "",
827
+ footerBtn
828
+ )
829
+ },
830
+ {
831
+ label: "Elevated card",
832
+ description: "Use elevated for surfaces that float above the page background.",
833
+ code: `<Card variant="elevated" header={<strong>Elevated</strong>}>
834
+ Uses box-shadow instead of a border.
835
+ </Card>`,
836
+ previewHtml: cardHtml(
837
+ "Elevated",
838
+ "Uses box-shadow instead of a border.",
839
+ "border-color:transparent;box-shadow:0 4px 6px -1px rgb(0 0 0 / 0.10)"
840
+ )
841
+ },
842
+ {
843
+ label: "No header or footer",
844
+ description: "Header and footer are optional. Omit both for a simple content container.",
845
+ code: `<Card>
846
+ A minimal card with no header or footer \u2014 just body content.
847
+ </Card>`,
848
+ previewHtml: `
849
+ <div class="ds-card" style="border-color:${C.border};border-radius:${rLg};background:${C.bg};color:${C.text}">
850
+ <div class="ds-card-body">
851
+ <p style="color:${C.textSecondary};margin:0;font-size:14px;font-family:${esc(ff)}">A minimal card with no header or footer.</p>
852
+ </div>
853
+ </div>`
854
+ }
855
+ ],
856
+ a11y: [
857
+ {
858
+ criterion: "1.3.1 Info and Relationships",
859
+ level: "A",
860
+ description: "Card is a <div> container \u2014 it carries no implicit role. If the card is a meaningful landmark, add role='region' and an aria-label."
861
+ },
862
+ {
863
+ criterion: "1.4.3 Contrast (Minimum)",
864
+ level: "AA",
865
+ description: "Text and border colour tokens are validated against WCAG AA at generate time."
866
+ },
867
+ {
868
+ criterion: "2.4.6 Headings and Labels",
869
+ level: "AA",
870
+ description: "The header slot should contain a heading element (<h2>\u2013<h4>) rather than bold text to preserve document outline."
871
+ },
872
+ {
873
+ criterion: "2.1.1 Keyboard",
874
+ level: "A",
875
+ description: "The card itself is not focusable. Interactive children (buttons, links) are reachable and operable by keyboard."
876
+ }
877
+ ],
878
+ aiMeta: {
879
+ component: "Card",
880
+ role: "content-container",
881
+ hierarchyLevel: "composite",
882
+ interactionModel: "passive",
883
+ layoutImpact: "block",
884
+ destructiveVariants: [],
885
+ accessibilityContract: {
886
+ implicitRole: "none",
887
+ addRoleRegionForLandmarks: true,
888
+ headerSlotShouldUseHeading: true
889
+ },
890
+ variants: ["default", "elevated", "outlined"],
891
+ aiGuidance: [
892
+ "Do not nest interactive cards inside other interactive cards.",
893
+ "If the entire card is clickable, use a single <a> or <button> wrapping the content \u2014 not onClick on the div.",
894
+ "Use header slot for a concise title; keep it to one line where possible.",
895
+ "Elevated variant is best on coloured or image backgrounds where a border would be lost."
896
+ ]
897
+ }
898
+ };
899
+ }
900
+
901
+ // src/generators/showcase/components/badge.ts
902
+ function badgeDef(config, tokens) {
903
+ const { ff } = componentTokens(config, tokens);
904
+ const badgeHtml = (label, bg, color) => `<span style="display:inline-flex;align-items:center;font-family:${esc(ff)};font-size:12px;font-weight:500;padding:2px 8px;border-radius:9999px;background:${bg};color:${color};white-space:nowrap">${label}</span>`;
905
+ const variants = [
906
+ { label: "Default", bg: "#f1f5f9", color: "#6b7280" },
907
+ { label: "Success", bg: "#dcfce7", color: "#16a34a" },
908
+ { label: "Warning", bg: "#fef9c3", color: "#ca8a04" },
909
+ { label: "Danger", bg: "#fee2e2", color: "#dc2626" },
910
+ { label: "Info", bg: "#dbeafe", color: "#2563eb" }
911
+ ];
912
+ return {
913
+ id: "badge",
914
+ label: "Badge",
915
+ description: "Compact label for status, categories, or counts. Display-only \u2014 not interactive.",
916
+ overviewHtml: `
917
+ <div class="comp-overview-section">
918
+ <div class="comp-overview-label">Variants</div>
919
+ <div class="comp-preview-row">
920
+ ${variants.map((v) => badgeHtml(v.label, v.bg, v.color)).join("\n ")}
921
+ </div>
922
+ </div>
923
+ <div class="comp-overview-section">
924
+ <div class="comp-overview-label">Sizes</div>
925
+ <div class="comp-preview-row" style="align-items:center">
926
+ <span style="display:inline-flex;align-items:center;font-family:${esc(ff)};font-size:11px;font-weight:500;padding:1px 6px;border-radius:9999px;background:#dbeafe;color:#2563eb">Small</span>
927
+ <span style="display:inline-flex;align-items:center;font-family:${esc(ff)};font-size:12px;font-weight:500;padding:2px 8px;border-radius:9999px;background:#dbeafe;color:#2563eb">Medium</span>
928
+ <span style="display:inline-flex;align-items:center;font-family:${esc(ff)};font-size:14px;font-weight:500;padding:4px 12px;border-radius:9999px;background:#dbeafe;color:#2563eb">Large</span>
929
+ <span style="display:inline-flex;width:8px;height:8px;border-radius:50%;background:#16a34a" title="Dot mode"></span>
930
+ </div>
931
+ </div>`,
932
+ props: [
933
+ {
934
+ name: "variant",
935
+ type: '"default" | "success" | "warning" | "danger" | "info"',
936
+ default: '"default"',
937
+ required: false,
938
+ description: "Semantic color variant."
939
+ },
940
+ {
941
+ name: "size",
942
+ type: '"sm" | "md" | "lg"',
943
+ default: '"md"',
944
+ required: false,
945
+ description: "Controls font size and padding."
946
+ },
947
+ {
948
+ name: "dot",
949
+ type: "boolean",
950
+ default: "false",
951
+ required: false,
952
+ description: "Renders as a coloured dot with no text."
953
+ },
954
+ {
955
+ name: "children",
956
+ type: "React.ReactNode",
957
+ default: "\u2014",
958
+ required: false,
959
+ description: "Badge label text."
960
+ }
961
+ ],
962
+ examples: [
963
+ {
964
+ label: "Status badges",
965
+ description: "Use semantic variants to communicate status at a glance.",
966
+ code: `<Badge variant="success">Published</Badge>
967
+ <Badge variant="warning">Draft</Badge>
968
+ <Badge variant="danger">Archived</Badge>`,
969
+ previewHtml: `<div style="display:flex;gap:8px;flex-wrap:wrap">
970
+ ${badgeHtml("Published", "#dcfce7", "#16a34a")}
971
+ ${badgeHtml("Draft", "#fef9c3", "#ca8a04")}
972
+ ${badgeHtml("Archived", "#fee2e2", "#dc2626")}
973
+ </div>`
974
+ },
975
+ {
976
+ label: "Dot indicator",
977
+ description: "Use dot mode for presence or connection status indicators.",
978
+ code: `<span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
979
+ <Badge variant="success" dot />
980
+ Online
981
+ </span>`,
982
+ previewHtml: `<div style="display:flex;align-items:center;gap:6px;font-family:${esc(ff)};font-size:14px;color:var(--color-text-primary,#0f172a)">
983
+ <span style="display:inline-flex;width:8px;height:8px;border-radius:50%;background:#16a34a"></span>
984
+ Online
985
+ </div>`
986
+ },
987
+ {
988
+ label: "With a count",
989
+ description: "Pair with a label for notification counts.",
990
+ code: `<Badge variant="danger">3</Badge>`,
991
+ previewHtml: `<div style="display:flex;gap:8px">
992
+ ${badgeHtml("3", "#fee2e2", "#dc2626")}
993
+ ${badgeHtml("12", "#dbeafe", "#2563eb")}
994
+ </div>`
995
+ }
996
+ ],
997
+ a11y: [
998
+ {
999
+ criterion: "1.3.1 Info and Relationships",
1000
+ level: "A",
1001
+ description: "Badge is a <span> with no implicit role. If it conveys status, add role='status' or wrap in a live region."
1002
+ },
1003
+ {
1004
+ criterion: "1.4.3 Contrast (Minimum)",
1005
+ level: "AA",
1006
+ description: "All variant colour pairs are chosen to meet 4.5:1 contrast ratio for small text."
1007
+ },
1008
+ {
1009
+ criterion: "1.4.11 Non-text Contrast",
1010
+ level: "AA",
1011
+ description: "Dot badges must have sufficient contrast with their background. Pair with a text label for screen readers."
1012
+ }
1013
+ ],
1014
+ aiMeta: {
1015
+ component: "Badge",
1016
+ role: "status-indicator",
1017
+ hierarchyLevel: "utility",
1018
+ interactionModel: "none",
1019
+ layoutImpact: "inline",
1020
+ destructiveVariants: [],
1021
+ accessibilityContract: {
1022
+ implicitRole: "none",
1023
+ dotRequiresTextAlternative: true
1024
+ },
1025
+ variants: ["default", "success", "warning", "danger", "info"],
1026
+ aiGuidance: [
1027
+ "Use badge to convey status, not to trigger actions \u2014 use Button for actions.",
1028
+ "Dot badges are not perceivable by screen readers alone; always pair with adjacent text.",
1029
+ "Keep badge labels short \u2014 1\u20132 words or a count.",
1030
+ "Danger variant is appropriate for errors or destructive states, not general alerts."
1031
+ ]
1032
+ }
1033
+ };
1034
+ }
1035
+
1036
+ // src/generators/showcase/components/checkbox.ts
1037
+ function checkboxDef(config, tokens) {
1038
+ const { ff } = componentTokens(config, tokens);
1039
+ const C = {
1040
+ action: "var(--color-action, #2563eb)",
1041
+ text: "var(--color-text-primary, #0f172a)",
1042
+ textSecondary: "var(--color-text-secondary, #64748b)",
1043
+ border: "var(--color-border-default, #e2e8f0)",
1044
+ bg: "var(--color-bg-default, #fff)"
1045
+ };
1046
+ const boxHtml = (checked, indeterminate = false) => {
1047
+ const fill = checked || indeterminate ? "#2563eb" : C.bg;
1048
+ const borderColor = checked || indeterminate ? "#2563eb" : C.border;
1049
+ const mark = checked ? `<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1.5,5 4,7.5 8.5,2.5"/></svg>` : indeterminate ? `<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round"><line x1="2" y1="5" x2="8" y2="5"/></svg>` : "";
1050
+ return `<span style="display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border-radius:2px;border:2px solid ${borderColor};background:${fill};flex-shrink:0">${mark}</span>`;
1051
+ };
1052
+ const checkboxHtml = (label, checked, opts = "", helper = "") => `<label style="display:inline-flex;align-items:flex-start;gap:8px;cursor:pointer;font-family:${esc(ff)};${opts}">
1053
+ ${boxHtml(checked)}
1054
+ <span style="font-size:14px;color:${C.text};line-height:1.4">${label}${helper ? `<br><span style="font-size:12px;color:${C.textSecondary}">${helper}</span>` : ""}</span>
1055
+ </label>`;
1056
+ return {
1057
+ id: "checkbox",
1058
+ label: "Checkbox",
1059
+ description: "Binary toggle for boolean values. Supports indeterminate state for partial selections.",
1060
+ overviewHtml: `
1061
+ <div class="comp-overview-section">
1062
+ <div class="comp-overview-label">States</div>
1063
+ <div class="comp-preview-col">
1064
+ ${checkboxHtml("Unchecked", false)}
1065
+ ${checkboxHtml("Checked", true)}
1066
+ <label style="display:inline-flex;align-items:flex-start;gap:8px;cursor:pointer;font-family:${esc(ff)}">
1067
+ ${boxHtml(false, true)}
1068
+ <span style="font-size:14px;color:${C.text}">Indeterminate</span>
1069
+ </label>
1070
+ ${checkboxHtml("Disabled", false, "opacity:0.4;cursor:not-allowed")}
1071
+ </div>
1072
+ </div>`,
1073
+ props: [
1074
+ {
1075
+ name: "label",
1076
+ type: "string",
1077
+ default: "\u2014",
1078
+ required: false,
1079
+ description: "Visible label rendered next to the checkbox."
1080
+ },
1081
+ {
1082
+ name: "checked",
1083
+ type: "boolean",
1084
+ default: "\u2014",
1085
+ required: false,
1086
+ description: "Controlled checked state. Pair with onChange."
1087
+ },
1088
+ {
1089
+ name: "defaultChecked",
1090
+ type: "boolean",
1091
+ default: "false",
1092
+ required: false,
1093
+ description: "Initial checked state for uncontrolled usage."
1094
+ },
1095
+ {
1096
+ name: "indeterminate",
1097
+ type: "boolean",
1098
+ default: "false",
1099
+ required: false,
1100
+ description: "Partial selection state. Visually distinct from checked/unchecked."
1101
+ },
1102
+ {
1103
+ name: "disabled",
1104
+ type: "boolean",
1105
+ default: "false",
1106
+ required: false,
1107
+ description: "Prevents interaction and applies reduced opacity."
1108
+ },
1109
+ {
1110
+ name: "size",
1111
+ type: '"sm" | "md" | "lg"',
1112
+ default: '"md"',
1113
+ required: false,
1114
+ description: "Controls checkbox and label size."
1115
+ },
1116
+ {
1117
+ name: "helperText",
1118
+ type: "string",
1119
+ default: "\u2014",
1120
+ required: false,
1121
+ description: "Secondary text below the label."
1122
+ },
1123
+ {
1124
+ name: "onChange",
1125
+ type: "(e: React.ChangeEvent<HTMLInputElement>) => void",
1126
+ default: "\u2014",
1127
+ required: false,
1128
+ description: "Fires when the checked state changes."
1129
+ }
1130
+ ],
1131
+ examples: [
1132
+ {
1133
+ label: "Controlled checkbox",
1134
+ description: "Manage checked state externally with useState.",
1135
+ code: `const [agreed, setAgreed] = useState(false);
1136
+
1137
+ <Checkbox
1138
+ label="I accept the terms and conditions"
1139
+ checked={agreed}
1140
+ onChange={(e) => setAgreed(e.target.checked)}
1141
+ />`,
1142
+ previewHtml: checkboxHtml("I accept the terms and conditions", true)
1143
+ },
1144
+ {
1145
+ label: "Indeterminate (parent selection)",
1146
+ description: "Use indeterminate when some but not all children are selected.",
1147
+ code: `<Checkbox
1148
+ label="Select all (3 of 5)"
1149
+ indeterminate
1150
+ />`,
1151
+ previewHtml: `<label style="display:inline-flex;align-items:center;gap:8px;cursor:pointer;font-family:${esc(ff)}">
1152
+ ${boxHtml(false, true)}
1153
+ <span style="font-size:14px;color:${C.text}">Select all (3 of 5)</span>
1154
+ </label>`
1155
+ },
1156
+ {
1157
+ label: "With helper text",
1158
+ description: "helperText provides secondary context without replacing the label.",
1159
+ code: `<Checkbox
1160
+ label="Receive email notifications"
1161
+ helperText="We'll send summaries once a week."
1162
+ defaultChecked
1163
+ />`,
1164
+ previewHtml: checkboxHtml(
1165
+ "Receive email notifications",
1166
+ true,
1167
+ "",
1168
+ "We'll send summaries once a week."
1169
+ )
1170
+ }
1171
+ ],
1172
+ a11y: [
1173
+ {
1174
+ criterion: "1.3.1 Info and Relationships",
1175
+ level: "A",
1176
+ description: "A hidden native <input type=checkbox> is always present. The visible custom box is aria-hidden."
1177
+ },
1178
+ {
1179
+ criterion: "2.1.1 Keyboard",
1180
+ level: "A",
1181
+ description: "Focusable and toggleable with Space. Tab moves focus to/from the checkbox."
1182
+ },
1183
+ {
1184
+ criterion: "2.4.7 Focus Visible",
1185
+ level: "AA",
1186
+ description: "Focus ring renders on keyboard focus via :focus-visible detection."
1187
+ },
1188
+ {
1189
+ criterion: "4.1.2 Name, Role, Value",
1190
+ level: "A",
1191
+ description: "Role is conveyed by the native input element. Indeterminate state is set via the indeterminate property on the DOM node."
1192
+ }
1193
+ ],
1194
+ aiMeta: {
1195
+ component: "Checkbox",
1196
+ role: "data-entry",
1197
+ hierarchyLevel: "primary",
1198
+ interactionModel: "synchronous",
1199
+ layoutImpact: "inline",
1200
+ destructiveVariants: [],
1201
+ accessibilityContract: {
1202
+ keyboard: true,
1203
+ focusRing: "required",
1204
+ nativeInputPreserved: true,
1205
+ indeterminateDomProperty: true
1206
+ },
1207
+ variants: ["default", "error", "disabled"],
1208
+ aiGuidance: [
1209
+ "Always provide a label prop or a wrapping <label> \u2014 never rely on adjacent text alone.",
1210
+ "Use indeterminate for 'select all' patterns when partial selection exists.",
1211
+ "Group related checkboxes in a <fieldset> with a <legend>.",
1212
+ "For single boolean toggles, prefer Checkbox over a custom toggle switch."
1213
+ ]
1214
+ }
1215
+ };
1216
+ }
1217
+
1218
+ // src/generators/showcase/components/radio.ts
1219
+ function radioDef(config, tokens) {
1220
+ const { ff } = componentTokens(config, tokens);
1221
+ const C = {
1222
+ action: "#2563eb",
1223
+ text: "var(--color-text-primary, #0f172a)",
1224
+ textSecondary: "var(--color-text-secondary, #64748b)",
1225
+ border: "var(--color-border-default, #e2e8f0)",
1226
+ bg: "var(--color-bg-default, #fff)"
1227
+ };
1228
+ const circleHtml = (selected) => {
1229
+ const borderColor = selected ? C.action : C.border;
1230
+ return `<span style="display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border-radius:50%;border:2px solid ${borderColor};background:${C.bg};flex-shrink:0">
1231
+ ${selected ? `<span style="width:8px;height:8px;border-radius:50%;background:${C.action}"></span>` : ""}
1232
+ </span>`;
1233
+ };
1234
+ const radioHtml = (label, selected, opts = "") => `<label style="display:inline-flex;align-items:center;gap:8px;cursor:pointer;font-family:${esc(ff)};${opts}">
1235
+ ${circleHtml(selected)}
1236
+ <span style="font-size:14px;color:${C.text}">${label}</span>
1237
+ </label>`;
1238
+ return {
1239
+ id: "radio",
1240
+ label: "Radio",
1241
+ description: "Single selection within a mutually exclusive group. Always pair Radio with RadioGroup.",
1242
+ overviewHtml: `
1243
+ <div class="comp-overview-section">
1244
+ <div class="comp-overview-label">RadioGroup (vertical)</div>
1245
+ <div class="comp-preview-col">
1246
+ <fieldset style="border:none;padding:0;margin:0;font-family:${esc(ff)}">
1247
+ <legend style="font-size:13px;font-weight:600;color:${C.text};margin-bottom:8px">Notification preference</legend>
1248
+ <div class="comp-preview-col">
1249
+ ${radioHtml("Email", true)}
1250
+ ${radioHtml("SMS", false)}
1251
+ ${radioHtml("Push notification", false)}
1252
+ </div>
1253
+ </fieldset>
1254
+ </div>
1255
+ </div>
1256
+ <div class="comp-overview-section">
1257
+ <div class="comp-overview-label">Horizontal layout</div>
1258
+ <div class="comp-preview-row">
1259
+ ${radioHtml("Monthly", true)}
1260
+ ${radioHtml("Quarterly", false)}
1261
+ ${radioHtml("Annually", false)}
1262
+ </div>
1263
+ </div>`,
1264
+ props: [
1265
+ {
1266
+ name: "label",
1267
+ type: "string",
1268
+ default: "\u2014",
1269
+ required: false,
1270
+ description: "Visible label rendered next to the radio circle."
1271
+ },
1272
+ {
1273
+ name: "value",
1274
+ type: "string",
1275
+ default: "\u2014",
1276
+ required: false,
1277
+ description: "The value submitted when this radio is selected. Required when used inside RadioGroup."
1278
+ },
1279
+ {
1280
+ name: "size",
1281
+ type: '"sm" | "md" | "lg"',
1282
+ default: '"md"',
1283
+ required: false,
1284
+ description: "Controls radio and label size."
1285
+ },
1286
+ {
1287
+ name: "disabled",
1288
+ type: "boolean",
1289
+ default: "false",
1290
+ required: false,
1291
+ description: "Prevents interaction and applies reduced opacity."
1292
+ },
1293
+ {
1294
+ name: "legend",
1295
+ type: "string",
1296
+ default: "\u2014",
1297
+ required: true,
1298
+ description: "[RadioGroup] Accessible group label read by screen readers."
1299
+ },
1300
+ {
1301
+ name: "orientation",
1302
+ type: '"horizontal" | "vertical"',
1303
+ default: '"vertical"',
1304
+ required: false,
1305
+ description: "[RadioGroup] Layout direction of the radio items."
1306
+ },
1307
+ {
1308
+ name: "onChange",
1309
+ type: "(value: string) => void",
1310
+ default: "\u2014",
1311
+ required: false,
1312
+ description: "[RadioGroup] Fires when the selected value changes."
1313
+ }
1314
+ ],
1315
+ examples: [
1316
+ {
1317
+ label: "Controlled RadioGroup",
1318
+ description: "Wrap Radio buttons inside RadioGroup for proper grouping and keyboard navigation.",
1319
+ code: `const [plan, setPlan] = useState("monthly");
1320
+
1321
+ <RadioGroup
1322
+ legend="Billing cycle"
1323
+ value={plan}
1324
+ onChange={setPlan}
1325
+ >
1326
+ <Radio value="monthly" label="Monthly" />
1327
+ <Radio value="annual" label="Annual (save 20%)" />
1328
+ </RadioGroup>`,
1329
+ previewHtml: `<fieldset style="border:none;padding:0;margin:0;font-family:${esc(ff)}">
1330
+ <legend style="font-size:13px;font-weight:600;color:${C.text};margin-bottom:8px">Billing cycle</legend>
1331
+ <div style="display:flex;flex-direction:column;gap:8px">
1332
+ ${radioHtml("Monthly", true)}
1333
+ ${radioHtml("Annual (save 20%)", false)}
1334
+ </div>
1335
+ </fieldset>`
1336
+ },
1337
+ {
1338
+ label: "Horizontal orientation",
1339
+ description: "Use orientation='horizontal' for compact inline layouts.",
1340
+ code: `<RadioGroup legend="Size" orientation="horizontal" defaultValue="md">
1341
+ <Radio value="sm" label="S" />
1342
+ <Radio value="md" label="M" />
1343
+ <Radio value="lg" label="L" />
1344
+ </RadioGroup>`,
1345
+ previewHtml: `<fieldset style="border:none;padding:0;margin:0;font-family:${esc(ff)}">
1346
+ <legend style="font-size:13px;font-weight:600;color:${C.text};margin-bottom:8px">Size</legend>
1347
+ <div style="display:flex;gap:16px">
1348
+ ${radioHtml("S", false)}
1349
+ ${radioHtml("M", true)}
1350
+ ${radioHtml("L", false)}
1351
+ </div>
1352
+ </fieldset>`
1353
+ }
1354
+ ],
1355
+ a11y: [
1356
+ {
1357
+ criterion: "1.3.1 Info and Relationships",
1358
+ level: "A",
1359
+ description: "RadioGroup renders a native <fieldset> with <legend>. Individual radios are native <input type=radio> elements \u2014 role and grouping are implicit."
1360
+ },
1361
+ {
1362
+ criterion: "2.1.1 Keyboard",
1363
+ level: "A",
1364
+ description: "Arrow keys cycle through options within the group. Tab moves focus to/from the group."
1365
+ },
1366
+ {
1367
+ criterion: "2.4.7 Focus Visible",
1368
+ level: "AA",
1369
+ description: "Focus ring renders on keyboard focus via :focus-visible detection."
1370
+ },
1371
+ {
1372
+ criterion: "4.1.2 Name, Role, Value",
1373
+ level: "A",
1374
+ description: "Selected state is communicated via the native checked attribute. Name grouping is handled by RadioGroup context."
1375
+ }
1376
+ ],
1377
+ aiMeta: {
1378
+ component: "Radio",
1379
+ role: "data-entry",
1380
+ hierarchyLevel: "primary",
1381
+ interactionModel: "synchronous",
1382
+ layoutImpact: "inline",
1383
+ destructiveVariants: [],
1384
+ accessibilityContract: {
1385
+ keyboard: true,
1386
+ focusRing: "required",
1387
+ mustUseRadioGroup: true,
1388
+ nativeInputPreserved: true
1389
+ },
1390
+ variants: ["default", "disabled"],
1391
+ aiGuidance: [
1392
+ "Always wrap Radio inside RadioGroup \u2014 never render standalone Radio elements.",
1393
+ "Use radio buttons when the user must pick exactly one option from a small set (2\u20137 items).",
1394
+ "For longer lists, prefer Select instead.",
1395
+ "The legend prop is required on RadioGroup and must describe the choice being made."
1396
+ ]
1397
+ }
1398
+ };
1399
+ }
1400
+
1401
+ // src/generators/showcase/components/select.ts
1402
+ function selectDef(config, tokens) {
1403
+ const { radiusMd, ff } = componentTokens(config, tokens);
1404
+ const r = `${radiusMd}px`;
1405
+ const C = {
1406
+ text: "var(--color-text-primary, #0f172a)",
1407
+ textSecondary: "var(--color-text-secondary, #64748b)",
1408
+ bg: "var(--color-bg-default, #fff)",
1409
+ border: "var(--color-border-default, #e2e8f0)"
1410
+ };
1411
+ const chevron = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="${C.textSecondary}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>`;
1412
+ const selectHtml = (label, placeholder, options = [], borderColor = C.border, helperHtml = "", disabled = false) => `
1413
+ <div class="ds-field" style="font-family:${esc(ff)};max-width:320px;${disabled ? "opacity:0.5" : ""}">
1414
+ <label class="ds-label" style="color:${C.text}">${esc(label)}</label>
1415
+ <div style="position:relative;display:flex;align-items:center;background:${C.bg};border:1px solid ${borderColor};border-radius:${r}">
1416
+ <select style="flex:1;appearance:none;-webkit-appearance:none;background:${C.bg};border:none;outline:none;padding:8px 32px 8px 12px;font-size:14px;color:${C.text};cursor:${disabled ? "not-allowed" : "pointer"};width:100%" ${disabled ? "disabled" : ""}>
1417
+ <option value="" disabled selected style="color:${C.textSecondary}">${esc(placeholder)}</option>
1418
+ ${options.map((o) => `<option>${esc(o)}</option>`).join("")}
1419
+ </select>
1420
+ <span style="position:absolute;right:8px;display:flex;align-items:center;pointer-events:none">${chevron}</span>
1421
+ </div>
1422
+ ${helperHtml}
1423
+ </div>`;
1424
+ return {
1425
+ id: "select",
1426
+ label: "Select",
1427
+ description: "Dropdown picker for selecting from a list of options. Wraps native <select> for full accessibility.",
1428
+ overviewHtml: `
1429
+ <div class="comp-overview-section">
1430
+ <div class="comp-overview-label">States</div>
1431
+ <div class="comp-preview-col">
1432
+ ${selectHtml("Country", "Select a country\u2026", ["Norway", "Sweden", "Denmark"], C.border, `<span style="font-size:12px;color:${C.textSecondary};margin-top:2px">Select your country of residence.</span>`)}
1433
+ ${selectHtml("Role", "Select a role\u2026", ["Admin", "Editor", "Viewer"], C.border, `<span style="font-size:12px;color:${C.textSecondary};margin-top:2px">Role determines what permissions this user has.</span>`)}
1434
+ ${selectHtml("Priority", "Select a priority\u2026", ["Low", "Medium", "High"], "#dc2626", `<span style="font-size:12px;color:#dc2626;margin-top:2px">Please select a priority level.</span>`)}
1435
+ ${selectHtml("Status", "Select a status\u2026", ["Active", "Inactive"], C.border, "", true)}
1436
+ </div>
1437
+ </div>`,
1438
+ props: [
1439
+ {
1440
+ name: "label",
1441
+ type: "string",
1442
+ default: "\u2014",
1443
+ required: false,
1444
+ description: "Visible label rendered above the select."
1445
+ },
1446
+ {
1447
+ name: "options",
1448
+ type: "SelectOption[]",
1449
+ default: "\u2014",
1450
+ required: false,
1451
+ description: "Structured option list. Alternatively, pass <option> children directly."
1452
+ },
1453
+ {
1454
+ name: "placeholder",
1455
+ type: "string",
1456
+ default: "\u2014",
1457
+ required: false,
1458
+ description: "Disabled first option shown when no value is selected."
1459
+ },
1460
+ {
1461
+ name: "errorMessage",
1462
+ type: "string",
1463
+ default: "\u2014",
1464
+ required: false,
1465
+ description: "Validation message. Also applies error border styling."
1466
+ },
1467
+ {
1468
+ name: "helperText",
1469
+ type: "string",
1470
+ default: "\u2014",
1471
+ required: false,
1472
+ description: "Secondary text shown below the select (no error state)."
1473
+ },
1474
+ {
1475
+ name: "size",
1476
+ type: '"sm" | "md" | "lg"',
1477
+ default: '"md"',
1478
+ required: false,
1479
+ description: "Controls padding and font size."
1480
+ },
1481
+ {
1482
+ name: "fullWidth",
1483
+ type: "boolean",
1484
+ default: "false",
1485
+ required: false,
1486
+ description: "Expands the select to fill its container."
1487
+ },
1488
+ {
1489
+ name: "disabled",
1490
+ type: "boolean",
1491
+ default: "false",
1492
+ required: false,
1493
+ description: "Prevents interaction and applies reduced opacity."
1494
+ }
1495
+ ],
1496
+ examples: [
1497
+ {
1498
+ label: "Using options prop",
1499
+ description: "Pass a structured array for programmatic option lists.",
1500
+ code: `<Select
1501
+ label="Framework"
1502
+ placeholder="Choose a framework\u2026"
1503
+ options={[
1504
+ { value: "react", label: "React" },
1505
+ { value: "vue", label: "Vue" },
1506
+ { value: "svelte", label: "Svelte" },
1507
+ ]}
1508
+ onChange={(e) => console.log(e.target.value)}
1509
+ />`,
1510
+ previewHtml: `<div class="ds-field" style="font-family:${esc(ff)};max-width:320px">
1511
+ <label class="ds-label" style="color:${C.text}">Framework</label>
1512
+ <div style="position:relative;display:flex;align-items:center;background:${C.bg};border:1px solid ${C.border};border-radius:${r}">
1513
+ <select style="flex:1;appearance:none;-webkit-appearance:none;background:${C.bg};border:none;outline:none;padding:8px 32px 8px 12px;font-size:14px;color:${C.text};cursor:pointer;width:100%">
1514
+ <option value="" disabled selected style="color:${C.textSecondary}">Choose a framework\u2026</option>
1515
+ <option>React</option>
1516
+ <option>Vue</option>
1517
+ <option>Svelte</option>
1518
+ </select>
1519
+ <span style="position:absolute;right:8px;display:flex;align-items:center;pointer-events:none">${chevron}</span>
1520
+ </div>
1521
+ </div>`
1522
+ },
1523
+ {
1524
+ label: "With validation error",
1525
+ description: "Pass errorMessage to show inline validation and apply error styling.",
1526
+ code: `<Select
1527
+ label="Department"
1528
+ placeholder="Select department"
1529
+ errorMessage="Please select a department."
1530
+ />`,
1531
+ previewHtml: selectHtml(
1532
+ "Department",
1533
+ "Select a department\u2026",
1534
+ ["Engineering", "Design", "Product"],
1535
+ "#dc2626",
1536
+ `<span style="font-size:12px;color:#dc2626;margin-top:2px">Please select a department.</span>`
1537
+ )
1538
+ }
1539
+ ],
1540
+ a11y: [
1541
+ {
1542
+ criterion: "1.3.1 Info and Relationships",
1543
+ level: "A",
1544
+ description: "The native <select> is always rendered. The custom chevron icon is aria-hidden. Label is associated via htmlFor/id."
1545
+ },
1546
+ {
1547
+ criterion: "2.1.1 Keyboard",
1548
+ level: "A",
1549
+ description: "Native <select> is fully keyboard accessible. Arrow keys navigate options; Enter or Space opens the dropdown."
1550
+ },
1551
+ {
1552
+ criterion: "3.3.1 Error Identification",
1553
+ level: "A",
1554
+ description: "When errorMessage is present, the select receives aria-invalid='true' and aria-describedby pointing to the error."
1555
+ },
1556
+ {
1557
+ criterion: "4.1.2 Name, Role, Value",
1558
+ level: "A",
1559
+ description: "Role and value state are communicated by the native select element \u2014 no ARIA additions needed."
1560
+ }
1561
+ ],
1562
+ aiMeta: {
1563
+ component: "Select",
1564
+ role: "data-entry",
1565
+ hierarchyLevel: "primary",
1566
+ interactionModel: "synchronous",
1567
+ layoutImpact: "block",
1568
+ destructiveVariants: [],
1569
+ accessibilityContract: {
1570
+ keyboard: true,
1571
+ nativeSelectPreserved: true,
1572
+ ariaInvalidOnError: true
1573
+ },
1574
+ variants: ["default", "error", "disabled"],
1575
+ aiGuidance: [
1576
+ "Use Select for lists of 4+ options; use RadioGroup for 2\u20133 mutually exclusive choices.",
1577
+ "Always provide a label \u2014 never rely on placeholder alone.",
1578
+ "For multi-select, use native multiple attribute or a custom multi-select component.",
1579
+ "options prop and children are mutually exclusive \u2014 use one approach consistently."
1580
+ ]
1581
+ }
1582
+ };
1583
+ }
1584
+
1585
+ // src/generators/showcase/components/toast.ts
1586
+ function toastDef(config, tokens) {
1587
+ const { radiusMd, ff } = componentTokens(config, tokens);
1588
+ const r = `${radiusMd}px`;
1589
+ const vDefault = { name: "default", bg: "var(--color-bg-subtle, #f8fafc)", border: "var(--color-border-default, #e2e8f0)", icon: "var(--color-text-secondary, #6b7280)", label: "Default" };
1590
+ const vSuccess = { name: "success", bg: "var(--color-success-subtle, #dcfce7)", border: "var(--color-success-border, #86efac)", icon: "var(--color-success, #16a34a)", label: "Success" };
1591
+ const vWarning = { name: "warning", bg: "var(--color-warning-subtle, #fef9c3)", border: "var(--color-warning-border, #fde047)", icon: "var(--color-warning, #ca8a04)", label: "Warning" };
1592
+ const vDanger = { name: "danger", bg: "var(--color-danger-subtle, #fee2e2)", border: "var(--color-danger-border, #fca5a5)", icon: "var(--color-danger, #dc2626)", label: "Error" };
1593
+ const vInfo = { name: "info", bg: "var(--color-info-subtle, #dbeafe)", border: "var(--color-info-border, #93c5fd)", icon: "var(--color-info, #2563eb)", label: "Info" };
1594
+ const variants = [vDefault, vSuccess, vWarning, vDanger, vInfo];
1595
+ const icons = {
1596
+ default: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>`,
1597
+ success: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>`,
1598
+ warning: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`,
1599
+ danger: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>`,
1600
+ info: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>`
1601
+ };
1602
+ const alertHtml = (v, title, body) => `<div role="alert" style="display:flex;gap:12px;padding:12px 16px;border-radius:${r};border:1px solid ${v.border};background:${v.bg};font-family:${esc(ff)};max-width:360px">
1603
+ <span style="color:${v.icon};flex-shrink:0;margin-top:1px">${icons[v.name]}</span>
1604
+ <div>
1605
+ <p style="margin:0;font-size:13px;font-weight:600;color:${v.icon}">${title}</p>
1606
+ <p style="margin:4px 0 0;font-size:13px;color:var(--color-text-secondary,#6b7280)">${body}</p>
1607
+ </div>
1608
+ </div>`;
1609
+ return {
1610
+ id: "toast",
1611
+ label: "Toast / Alert",
1612
+ description: "Feedback for user actions. Alert is inline and static; Toast is an overlay with auto-dismiss and a useToast() hook.",
1613
+ overviewHtml: `
1614
+ <div class="comp-overview-section">
1615
+ <div class="comp-overview-label">Alert \u2014 inline variants</div>
1616
+ <div class="comp-preview-col">
1617
+ ${variants.map((v) => alertHtml(v, v.label, `This is a ${v.name} alert message.`)).join("\n ")}
1618
+ </div>
1619
+ </div>`,
1620
+ props: [
1621
+ {
1622
+ name: "variant",
1623
+ type: '"default" | "success" | "warning" | "danger" | "info"',
1624
+ default: '"default"',
1625
+ required: false,
1626
+ description: "Semantic color and icon variant."
1627
+ },
1628
+ {
1629
+ name: "title",
1630
+ type: "string",
1631
+ default: "\u2014",
1632
+ required: false,
1633
+ description: "Bold heading rendered above the message body."
1634
+ },
1635
+ {
1636
+ name: "dismissible",
1637
+ type: "boolean",
1638
+ default: "false",
1639
+ required: false,
1640
+ description: "[Alert] Shows a close button. Hides the alert on click."
1641
+ },
1642
+ {
1643
+ name: "onDismiss",
1644
+ type: "() => void",
1645
+ default: "\u2014",
1646
+ required: false,
1647
+ description: "[Alert] Called after the alert is dismissed."
1648
+ },
1649
+ {
1650
+ name: "message",
1651
+ type: "string",
1652
+ default: "\u2014",
1653
+ required: true,
1654
+ description: "[Toast] The notification message body."
1655
+ },
1656
+ {
1657
+ name: "duration",
1658
+ type: "number",
1659
+ default: "5000",
1660
+ required: false,
1661
+ description: "[Toast] Auto-dismiss delay in ms. Set to 0 to disable."
1662
+ }
1663
+ ],
1664
+ examples: [
1665
+ {
1666
+ label: "Inline Alert",
1667
+ description: "Alert renders in-place within the page flow.",
1668
+ code: `<Alert variant="success" title="Payment confirmed">
1669
+ Your order #1234 has been placed successfully.
1670
+ </Alert>`,
1671
+ previewHtml: alertHtml(
1672
+ vSuccess,
1673
+ "Payment confirmed",
1674
+ "Your order #1234 has been placed successfully."
1675
+ )
1676
+ },
1677
+ {
1678
+ label: "Dismissible Alert",
1679
+ description: "Add dismissible to show a close button.",
1680
+ code: `<Alert
1681
+ variant="warning"
1682
+ title="Your trial expires in 3 days"
1683
+ dismissible
1684
+ onDismiss={() => setShowBanner(false)}
1685
+ >
1686
+ Upgrade to continue using all features.
1687
+ </Alert>`,
1688
+ previewHtml: `<div role="alert" style="display:flex;gap:12px;padding:12px 16px;border-radius:${r};border:1px solid ${vWarning.border};background:${vWarning.bg};font-family:${esc(ff)};max-width:360px">
1689
+ <span style="color:${vWarning.icon};flex-shrink:0;margin-top:1px">${icons["warning"]}</span>
1690
+ <div style="flex:1">
1691
+ <p style="margin:0;font-size:13px;font-weight:600;color:${vWarning.icon}">Your trial expires in 3 days</p>
1692
+ <p style="margin:4px 0 0;font-size:13px;color:var(--color-text-secondary,#6b7280)">Upgrade to continue using all features.</p>
1693
+ </div>
1694
+ <button style="background:transparent;border:none;cursor:pointer;color:var(--color-text-secondary,#6b7280);padding:2px;flex-shrink:0" aria-label="Dismiss">\u2715</button>
1695
+ </div>`
1696
+ },
1697
+ {
1698
+ label: "Toast via useToast()",
1699
+ description: "Wrap your app in ToastProvider, then call toast.add() from anywhere.",
1700
+ code: `// Wrap your app once
1701
+ <ToastProvider>
1702
+ <App />
1703
+ </ToastProvider>
1704
+
1705
+ // Inside any component
1706
+ const toast = useToast();
1707
+
1708
+ toast.add({
1709
+ variant: "success",
1710
+ title: "Saved",
1711
+ message: "Your changes have been saved.",
1712
+ duration: 4000,
1713
+ });`,
1714
+ previewHtml: `<div style="position:relative;background:var(--color-bg-subtle,#f8fafc);border:1px dashed var(--color-border-default,#e2e8f0);border-radius:${r};padding:20px;min-height:80px;font-family:${esc(ff)}">
1715
+ <p style="font-size:12px;color:var(--color-text-secondary,#6b7280);margin:0 0 12px">Bottom-right overlay (fixed position)</p>
1716
+ <div style="display:flex;gap:12px;padding:12px 16px;border-radius:${r};border:1px solid var(--color-success-border,#86efac);background:var(--color-success-subtle,#dcfce7);box-shadow:0 4px 12px rgba(0,0,0,0.12)">
1717
+ <span style="color:var(--color-success,#16a34a)">${icons["success"]}</span>
1718
+ <div>
1719
+ <p style="margin:0;font-size:13px;font-weight:600;color:var(--color-text-primary,#0f172a)">Saved</p>
1720
+ <p style="margin:4px 0 0;font-size:13px;color:var(--color-text-secondary,#6b7280)">Your changes have been saved.</p>
1721
+ </div>
1722
+ <button style="background:transparent;border:none;cursor:pointer;color:var(--color-text-secondary,#6b7280);padding:2px" aria-label="Dismiss">\u2715</button>
1723
+ </div>
1724
+ </div>`
1725
+ }
1726
+ ],
1727
+ a11y: [
1728
+ {
1729
+ criterion: "4.1.3 Status Messages",
1730
+ level: "AA",
1731
+ description: "Alert uses role='alert' for immediate announcements. Toast container uses aria-live='polite' so screen readers announce without interrupting."
1732
+ },
1733
+ {
1734
+ criterion: "2.2.1 Timing Adjustable",
1735
+ level: "A",
1736
+ description: "duration prop controls auto-dismiss timing. Set to 0 to disable. Users can also dismiss manually with the close button."
1737
+ },
1738
+ {
1739
+ criterion: "2.1.1 Keyboard",
1740
+ level: "A",
1741
+ description: "The dismiss button is keyboard focusable and activatable. Toast overlay does not trap focus."
1742
+ },
1743
+ {
1744
+ criterion: "1.4.3 Contrast (Minimum)",
1745
+ level: "AA",
1746
+ description: "All variant text/background pairs are selected to meet 4.5:1 contrast for body text."
1747
+ }
1748
+ ],
1749
+ aiMeta: {
1750
+ component: "Toast",
1751
+ role: "feedback",
1752
+ hierarchyLevel: "utility",
1753
+ interactionModel: "asynchronous",
1754
+ layoutImpact: "overlay",
1755
+ destructiveVariants: [],
1756
+ accessibilityContract: {
1757
+ alertRoleForInline: true,
1758
+ ariaLivePoliteForToasts: true,
1759
+ dismissButtonRequired: "when-dismissible"
1760
+ },
1761
+ variants: ["default", "success", "warning", "danger", "info"],
1762
+ aiGuidance: [
1763
+ "Use Alert for inline, contextual feedback tied to a form or page section.",
1764
+ "Use Toast (via useToast) for transient system-level feedback after async actions.",
1765
+ "Never use Toast for error messages that the user must act on \u2014 use Alert instead.",
1766
+ "Keep toast messages under 80 characters \u2014 they are ephemeral."
1767
+ ]
1768
+ }
1769
+ };
1770
+ }
1771
+
1772
+ // src/generators/showcase/components/spinner.ts
1773
+ function spinnerDef(config, tokens) {
1774
+ const { ff } = componentTokens(config, tokens);
1775
+ const C = {
1776
+ action: "#2563eb",
1777
+ textSecondary: "#6b7280",
1778
+ bg: "var(--color-bg-default, #fff)"
1779
+ };
1780
+ const svgSpinner = (size, color, strokeWidth = 2) => {
1781
+ const r = size / 2 - strokeWidth;
1782
+ const c = 2 * Math.PI * r;
1783
+ return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" fill="none" style="color:${color};animation:dsforge-spin 0.75s linear infinite;display:block">
1784
+ <circle cx="${size / 2}" cy="${size / 2}" r="${r}" stroke="currentColor" stroke-width="${strokeWidth}" opacity="0.2"/>
1785
+ <circle cx="${size / 2}" cy="${size / 2}" r="${r}" stroke="currentColor" stroke-width="${strokeWidth}" stroke-linecap="round"
1786
+ stroke-dasharray="${c}" stroke-dashoffset="${c * 0.25}"
1787
+ transform="rotate(-90 ${size / 2} ${size / 2})"/>
1788
+ </svg>`;
1789
+ };
1790
+ return {
1791
+ id: "spinner",
1792
+ label: "Spinner",
1793
+ description: "Loading indicator for async operations. Includes a visually hidden status label for screen readers.",
1794
+ overviewHtml: `
1795
+ <div class="comp-overview-section">
1796
+ <div class="comp-overview-label">Sizes</div>
1797
+ <div class="comp-preview-row" style="align-items:center">
1798
+ ${svgSpinner(12, C.action, 2.5)}
1799
+ ${svgSpinner(16, C.action, 2.5)}
1800
+ ${svgSpinner(24, C.action, 2)}
1801
+ ${svgSpinner(32, C.action, 2)}
1802
+ ${svgSpinner(48, C.action, 1.5)}
1803
+ </div>
1804
+ </div>
1805
+ <div class="comp-overview-section">
1806
+ <div class="comp-overview-label">Variants</div>
1807
+ <div class="comp-preview-row" style="align-items:center">
1808
+ ${svgSpinner(24, C.textSecondary, 2)}
1809
+ ${svgSpinner(24, C.action, 2)}
1810
+ <span style="background:#1e293b;padding:8px;border-radius:6px;display:inline-flex">${svgSpinner(24, "#ffffff", 2)}</span>
1811
+ </div>
1812
+ </div>`,
1813
+ props: [
1814
+ {
1815
+ name: "size",
1816
+ type: '"xs" | "sm" | "md" | "lg" | "xl"',
1817
+ default: '"md"',
1818
+ required: false,
1819
+ description: "Controls the diameter and stroke width."
1820
+ },
1821
+ {
1822
+ name: "variant",
1823
+ type: '"default" | "primary" | "inverted"',
1824
+ default: '"default"',
1825
+ required: false,
1826
+ description: "Color variant. Use inverted on dark backgrounds."
1827
+ },
1828
+ {
1829
+ name: "label",
1830
+ type: "string",
1831
+ default: '"Loading\u2026"',
1832
+ required: false,
1833
+ description: "Screen reader announcement. Rendered as visually hidden text inside role='status'."
1834
+ }
1835
+ ],
1836
+ examples: [
1837
+ {
1838
+ label: "Inline loading state",
1839
+ description: "Use inside buttons or next to labels during async operations.",
1840
+ code: `<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
1841
+ <Spinner size="sm" label="Saving changes" />
1842
+ <span>Saving\u2026</span>
1843
+ </div>`,
1844
+ previewHtml: `<div style="display:flex;align-items:center;gap:8px;font-family:${esc(ff)};font-size:14px;color:var(--color-text-primary,#0f172a)">
1845
+ ${svgSpinner(16, C.action, 2.5)}
1846
+ <span>Saving\u2026</span>
1847
+ </div>`
1848
+ },
1849
+ {
1850
+ label: "Full-page loader",
1851
+ description: "Center a large spinner during initial data fetching.",
1852
+ code: `<div style={{ display: 'flex', justifyContent: 'center', padding: 48 }}>
1853
+ <Spinner size="lg" label="Loading dashboard" />
1854
+ </div>`,
1855
+ previewHtml: `<div style="display:flex;justify-content:center;padding:32px;background:var(--color-bg-subtle,#f8fafc);border-radius:6px;border:1px solid #e2e8f0">
1856
+ ${svgSpinner(32, C.action, 2)}
1857
+ </div>`
1858
+ },
1859
+ {
1860
+ label: "On dark background",
1861
+ description: "Use variant='inverted' when the spinner sits on a dark surface.",
1862
+ code: `<div style={{ background: '#1e293b', padding: 16, borderRadius: 8 }}>
1863
+ <Spinner variant="inverted" label="Loading" />
1864
+ </div>`,
1865
+ previewHtml: `<div style="background:#1e293b;padding:16px;border-radius:6px;display:inline-flex">
1866
+ ${svgSpinner(24, "#ffffff", 2)}
1867
+ </div>`
1868
+ }
1869
+ ],
1870
+ a11y: [
1871
+ {
1872
+ criterion: "4.1.3 Status Messages",
1873
+ level: "AA",
1874
+ description: "Spinner is wrapped in a <span role='status'>. The label prop is rendered as visually hidden text inside this element, ensuring screen readers announce the loading state."
1875
+ },
1876
+ {
1877
+ criterion: "1.4.3 Contrast (Minimum)",
1878
+ level: "AA",
1879
+ description: "The spinner arc contrasts with its track. default and primary variants are validated against the configured background token."
1880
+ },
1881
+ {
1882
+ criterion: "2.2.2 Pause, Stop, Hide",
1883
+ level: "A",
1884
+ description: "Spinner animations are driven by CSS. Users with prefers-reduced-motion can override these with a media query."
1885
+ }
1886
+ ],
1887
+ aiMeta: {
1888
+ component: "Spinner",
1889
+ role: "loading-indicator",
1890
+ hierarchyLevel: "utility",
1891
+ interactionModel: "asynchronous",
1892
+ layoutImpact: "inline",
1893
+ destructiveVariants: [],
1894
+ accessibilityContract: {
1895
+ roleStatus: true,
1896
+ visuallyHiddenLabel: "required",
1897
+ reducedMotionSupported: true
1898
+ },
1899
+ variants: ["default", "primary", "inverted"],
1900
+ aiGuidance: [
1901
+ "Always provide a meaningful label describing what is loading, not just 'Loading\u2026'.",
1902
+ "Use sm size inside buttons; md for inline states; lg/xl for full-page loaders.",
1903
+ "Pair with aria-busy='true' on the container that is loading for extra screen reader context.",
1904
+ "Use inverted variant on dark surfaces (dark backgrounds, colored buttons)."
1905
+ ]
1906
+ }
1907
+ };
1908
+ }
1909
+
1910
+ // src/generators/showcase/registry.ts
1911
+ var SHOWCASE_COMPONENTS = [
1912
+ {
1913
+ id: "button",
1914
+ label: "Button",
1915
+ pageDescription: "Triggers an action or event. Supports multiple variants, sizes, and loading state.",
1916
+ def: buttonDef
1917
+ },
1918
+ {
1919
+ id: "input",
1920
+ label: "Input",
1921
+ pageDescription: "Single-line text field with label, helper text, and validation states.",
1922
+ def: inputDef
1923
+ },
1924
+ {
1925
+ id: "card",
1926
+ label: "Card",
1927
+ pageDescription: "Surface that groups related content. Supports header, body, and footer slots.",
1928
+ def: cardDef
1929
+ },
1930
+ {
1931
+ id: "badge",
1932
+ label: "Badge",
1933
+ pageDescription: "Compact label for status, categories, or counts. Display-only \u2014 not interactive.",
1934
+ def: badgeDef
1935
+ },
1936
+ {
1937
+ id: "checkbox",
1938
+ label: "Checkbox",
1939
+ pageDescription: "Binary toggle for boolean values. Supports indeterminate state for partial selections.",
1940
+ def: checkboxDef
1941
+ },
1942
+ {
1943
+ id: "radio",
1944
+ label: "Radio",
1945
+ pageDescription: "Single selection within a mutually exclusive group. Always use inside RadioGroup.",
1946
+ def: radioDef
1947
+ },
1948
+ {
1949
+ id: "select",
1950
+ label: "Select",
1951
+ pageDescription: "Dropdown picker for selecting from a list of options. Wraps native <select> for full accessibility.",
1952
+ def: selectDef
1953
+ },
1954
+ {
1955
+ id: "toast",
1956
+ label: "Toast / Alert",
1957
+ pageDescription: "Feedback messages for user actions. Alert is inline; Toast is an overlay with auto-dismiss.",
1958
+ def: toastDef
1959
+ },
1960
+ {
1961
+ id: "spinner",
1962
+ label: "Spinner",
1963
+ pageDescription: "Loading indicator for async operations. Includes a visually hidden status label for screen readers.",
1964
+ def: spinnerDef
1965
+ }
1966
+ ];
1967
+
1968
+ // src/generators/showcase/html.ts
1969
+ function generateShowcase(config, resolution) {
1970
+ const tokens = resolution.tokens;
1971
+ const name = config.meta?.name ?? "Design System";
1972
+ const version = config.meta?.version ?? "0.1.0";
1973
+ const themes = Object.keys(config.themes ?? {});
1974
+ const foundationItems = [
1975
+ { id: "colors", label: "Colors" },
1976
+ { id: "typography", label: "Typography" },
1977
+ { id: "spacing", label: "Spacing" },
1978
+ { id: "radius", label: "Border Radius" },
1979
+ { id: "elevation", label: "Elevation" },
1980
+ { id: "motion", label: "Motion" }
1981
+ ];
1982
+ const componentItems = SHOWCASE_COMPONENTS.map(({ id, label }) => ({ id, label }));
1983
+ const allItems = [...foundationItems, ...componentItems];
1984
+ const isPro = isProUnlocked();
1985
+ const sections = {
1986
+ colors: buildColorSection(config, tokens),
1987
+ typography: buildTypographySection(config),
1988
+ spacing: buildSpacingSection(config),
1989
+ radius: buildRadiusSection(config),
1990
+ elevation: buildElevationSection(config),
1991
+ motion: buildMotionSection(config),
1992
+ ...Object.fromEntries(
1993
+ SHOWCASE_COMPONENTS.map((entry) => [
1994
+ entry.id,
1995
+ buildComponentPage(entry.def(config, tokens), isPro)
1996
+ ])
1997
+ )
1998
+ };
1999
+ const flatTokens = Object.fromEntries(
2000
+ Object.entries(tokens).map(([k, v]) => [
2001
+ k.replace(/^(global|semantic|component)\./, ""),
2002
+ v
2003
+ ])
2004
+ );
2005
+ const lightTheme = config.themes?.["light"] ?? {};
2006
+ const darkTheme = config.themes?.["dark"] ?? {};
2007
+ const themeCssLight = Object.entries({ ...flatTokens, ...lightTheme }).map(([k, v]) => ` --${k}: ${v};`).join("\n");
2008
+ const themeCssDark = Object.entries({ ...flatTokens, ...darkTheme }).map(([k, v]) => ` --${k}: ${v};`).join("\n");
2009
+ return `<!DOCTYPE html>
2010
+ <html lang="en" data-theme="light">
2011
+ <head>
2012
+ <meta charset="UTF-8" />
2013
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
2014
+ <title>${esc(name)} \u2014 Design System Docs</title>
2015
+ <link rel="icon" type="image/svg+xml" href="../assets/favicon.svg" />
2016
+ <style>
2017
+ /* \u2500\u2500 Theme tokens \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2018
+ [data-theme="light"] {
2019
+ ${themeCssLight}
2020
+ }
2021
+ [data-theme="dark"] {
2022
+ ${themeCssDark}
2023
+ }
2024
+
2025
+ /* \u2500\u2500 Reset + base \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2026
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
2027
+ html { font-size: 16px; }
2028
+ body {
2029
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
2030
+ background: var(--color-bg-subtle, #f8fafc);
2031
+ color: var(--color-text-primary, #0f172a);
2032
+ display: flex; min-height: 100vh; line-height: 1.5;
2033
+ }
2034
+
2035
+ /* \u2500\u2500 Sidebar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2036
+ .sidebar {
2037
+ width: 220px; min-width: 220px;
2038
+ background: var(--color-bg-default, #fff);
2039
+ border-right: 1px solid var(--color-border-default, #e2e8f0);
2040
+ height: 100vh; position: sticky; top: 0;
2041
+ display: flex; flex-direction: column; overflow-y: auto;
2042
+ }
2043
+ .sidebar-header { padding: 18px 16px 14px; border-bottom: 1px solid var(--color-border-default, #e2e8f0); }
2044
+ .sidebar-title { font-size: 14px; font-weight: 700; color: var(--color-text-primary, #0f172a); letter-spacing: -0.01em; }
2045
+ .sidebar-version { font-size: 11px; color: var(--color-text-secondary, #64748b); margin-top: 2px; }
2046
+ .sidebar-section { padding: 12px 8px 4px; }
2047
+ .sidebar-section-label {
2048
+ font-size: 10px; font-weight: 600; text-transform: uppercase;
2049
+ letter-spacing: 0.08em; color: var(--color-text-secondary, #64748b);
2050
+ padding: 0 8px; margin-bottom: 4px;
2051
+ }
2052
+ .nav-item {
2053
+ display: block; padding: 5px 8px; border-radius: 5px;
2054
+ font-size: 13px; color: var(--color-text-secondary, #64748b);
2055
+ cursor: pointer; text-decoration: none;
2056
+ transition: background 120ms, color 120ms;
2057
+ }
2058
+ .nav-item:hover { background: var(--color-bg-subtle, #f8fafc); color: var(--color-text-primary, #0f172a); }
2059
+ .nav-item.active { background: var(--color-bg-overlay, #f1f5f9); color: var(--color-action, #2563eb); font-weight: 500; }
2060
+
2061
+ /* \u2500\u2500 Main \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2062
+ .main { flex: 1; overflow-y: auto; }
2063
+ .topbar {
2064
+ position: sticky; top: 0; z-index: 10;
2065
+ background: var(--color-bg-default, #fff);
2066
+ border-bottom: 1px solid var(--color-border-default, #e2e8f0);
2067
+ padding: 10px 32px;
2068
+ display: flex; align-items: center; justify-content: space-between; gap: 16px;
2069
+ }
2070
+ .topbar-breadcrumb { font-size: 13px; color: var(--color-text-secondary, #64748b); }
2071
+ .topbar-breadcrumb span { color: var(--color-text-primary, #0f172a); font-weight: 500; }
2072
+ .topbar-actions { display: flex; gap: 8px; align-items: center; }
2073
+ .theme-toggle {
2074
+ display: flex; gap: 3px;
2075
+ background: var(--color-bg-subtle, #f8fafc);
2076
+ border: 1px solid var(--color-border-default, #e2e8f0);
2077
+ border-radius: 7px; padding: 3px;
2078
+ }
2079
+ .theme-btn {
2080
+ padding: 3px 10px; border-radius: 4px; border: none;
2081
+ background: transparent; font-size: 12px; cursor: pointer;
2082
+ color: var(--color-text-secondary, #64748b);
2083
+ transition: background 120ms, color 120ms;
2084
+ }
2085
+ .theme-btn.active {
2086
+ background: var(--color-bg-default, #fff);
2087
+ color: var(--color-text-primary, #0f172a); font-weight: 500;
2088
+ box-shadow: 0 1px 2px rgb(0 0 0 / 0.06);
2089
+ }
2090
+ .content { padding: 36px 40px 80px; max-width: 860px; }
2091
+ .page { display: none; }
2092
+ .page.active { display: block; }
2093
+ .page-title {
2094
+ font-size: 26px; font-weight: 700; letter-spacing: -0.02em;
2095
+ color: var(--color-text-primary, #0f172a); margin-bottom: 4px;
2096
+ }
2097
+ .page-desc {
2098
+ font-size: 14px; color: var(--color-text-secondary, #64748b);
2099
+ margin-bottom: 28px; line-height: 1.65; max-width: 640px;
2100
+ }
2101
+ .section-block { margin-bottom: 36px; }
2102
+ .group-title {
2103
+ font-size: 11px; font-weight: 600; text-transform: uppercase;
2104
+ letter-spacing: 0.07em; color: var(--color-text-secondary, #64748b);
2105
+ margin-bottom: 14px; padding-bottom: 8px;
2106
+ border-bottom: 1px solid var(--color-border-default, #e2e8f0);
2107
+ }
2108
+
2109
+ /* \u2500\u2500 Foundations: colors \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2110
+ .swatch-grid { display: flex; flex-wrap: wrap; gap: 8px; }
2111
+ .swatch {
2112
+ width: 88px; height: 72px; border-radius: 8px; padding: 8px;
2113
+ display: flex; flex-direction: column; justify-content: flex-end;
2114
+ font-size: 10px; line-height: 1.3; border: 1px solid rgb(0 0 0 / 0.06);
2115
+ }
2116
+ .swatch-name { font-weight: 600; word-break: break-all; }
2117
+ .swatch-value { opacity: 0.8; }
2118
+
2119
+ /* \u2500\u2500 Foundations: typography \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2120
+ .type-family {
2121
+ font-size: 13px; color: var(--color-text-secondary, #64748b); font-family: monospace;
2122
+ background: var(--color-bg-overlay, #f1f5f9);
2123
+ display: inline-block; padding: 4px 10px; border-radius: 6px; margin-bottom: 24px;
2124
+ }
2125
+ .type-scale { display: flex; flex-direction: column; gap: 2px; }
2126
+ .type-row { display: flex; align-items: center; gap: 24px; padding: 12px 0; border-bottom: 1px solid var(--color-border-default, #e2e8f0); }
2127
+ .type-meta { width: 140px; min-width: 140px; }
2128
+ .type-role { font-size: 12px; font-weight: 600; color: var(--color-action, #2563eb); display: block; }
2129
+ .type-spec { font-size: 11px; color: var(--color-text-secondary, #64748b); font-family: monospace; }
2130
+ .type-sample { flex: 1; color: var(--color-text-primary, #0f172a); }
2131
+
2132
+ /* \u2500\u2500 Foundations: spacing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2133
+ .spacing-list { display: flex; flex-direction: column; gap: 10px; }
2134
+ .spacing-row { display: flex; align-items: center; gap: 16px; }
2135
+ .spacing-key { width: 160px; min-width: 160px; font-size: 12px; font-family: monospace; color: var(--color-text-secondary, #64748b); }
2136
+ .spacing-bar-wrap { flex: 1; }
2137
+ .spacing-bar { height: 8px; background: var(--color-action, #2563eb); border-radius: 4px; min-width: 4px; opacity: 0.7; }
2138
+ .spacing-val { width: 48px; font-size: 12px; color: var(--color-text-secondary, #64748b); font-family: monospace; text-align: right; }
2139
+
2140
+ /* \u2500\u2500 Foundations: radius \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2141
+ .radius-grid { display: flex; flex-wrap: wrap; gap: 24px; }
2142
+ .radius-item { display: flex; flex-direction: column; align-items: center; gap: 8px; }
2143
+ .radius-box { width: 64px; height: 64px; background: var(--color-action, #2563eb); opacity: 0.15; border: 2px solid var(--color-action, #2563eb); }
2144
+ .radius-key { font-size: 12px; font-weight: 600; color: var(--color-text-primary, #0f172a); }
2145
+ .radius-val { font-size: 11px; color: var(--color-text-secondary, #64748b); font-family: monospace; }
2146
+
2147
+ /* \u2500\u2500 Foundations: elevation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2148
+ .elevation-grid { display: flex; flex-wrap: wrap; gap: 24px; align-items: flex-start; }
2149
+ .elevation-item { display: flex; flex-direction: column; align-items: center; gap: 12px; }
2150
+ .elevation-box { width: 80px; height: 80px; background: var(--color-bg-default, #fff); border-radius: 10px; border: 1px solid var(--color-border-default, #e2e8f0); }
2151
+ .elevation-key { font-size: 12px; font-weight: 600; color: var(--color-text-primary, #0f172a); }
2152
+ .elevation-val { font-size: 10px; color: var(--color-text-secondary, #64748b); font-family: monospace; text-align: center; max-width: 120px; word-break: break-all; }
2153
+
2154
+ /* \u2500\u2500 Foundations: motion \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2155
+ .motion-grid { display: flex; flex-direction: column; gap: 12px; }
2156
+ .motion-item {
2157
+ display: flex; align-items: center; gap: 16px; cursor: pointer;
2158
+ padding: 10px 12px; border-radius: 8px;
2159
+ border: 1px solid var(--color-border-default, #e2e8f0);
2160
+ background: var(--color-bg-default, #fff); transition: border-color 150ms;
2161
+ }
2162
+ .motion-item:hover { border-color: var(--color-action, #2563eb); }
2163
+ .motion-track { width: 140px; height: 12px; background: var(--color-bg-overlay, #f1f5f9); border-radius: 6px; position: relative; overflow: hidden; }
2164
+ .motion-dot { position: absolute; left: 4px; top: 2px; width: 8px; height: 8px; border-radius: 50%; background: var(--color-action, #2563eb); transform: translateX(0); }
2165
+ .motion-key { width: 100px; font-size: 12px; font-weight: 600; color: var(--color-text-primary, #0f172a); }
2166
+ .motion-val { flex: 1; font-size: 12px; font-family: monospace; color: var(--color-text-secondary, #64748b); }
2167
+ .motion-hint { font-size: 11px; color: var(--color-text-secondary, #64748b); margin-left: auto; }
2168
+
2169
+ /* \u2500\u2500 Component tab bar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2170
+ .comp-tab-bar {
2171
+ display: flex;
2172
+ border-bottom: 1px solid var(--color-border-default, #e2e8f0);
2173
+ margin-bottom: 28px;
2174
+ }
2175
+ .comp-tab {
2176
+ padding: 8px 16px;
2177
+ background: none; border: none; border-bottom: 2px solid transparent;
2178
+ font-size: 13px; font-weight: 500; cursor: pointer;
2179
+ color: var(--color-text-secondary, #64748b);
2180
+ margin-bottom: -1px; transition: color 120ms, border-color 120ms;
2181
+ }
2182
+ .comp-tab:hover { color: var(--color-text-primary, #0f172a); }
2183
+ .comp-tab.active { color: var(--color-action, #2563eb); border-bottom-color: var(--color-action, #2563eb); }
2184
+ .comp-tab-panel { display: none; }
2185
+ .comp-tab-panel.active { display: block; }
2186
+
2187
+ /* \u2500\u2500 Component overview \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2188
+ .comp-overview-section { margin-bottom: 28px; }
2189
+ .comp-overview-label {
2190
+ font-size: 11px; font-weight: 600; text-transform: uppercase;
2191
+ letter-spacing: 0.07em; color: var(--color-text-secondary, #64748b); margin-bottom: 12px;
2192
+ }
2193
+ .comp-preview-row {
2194
+ display: flex; flex-wrap: wrap; gap: 12px; align-items: center;
2195
+ padding: 24px; background: var(--color-bg-default, #fff);
2196
+ border: 1px solid var(--color-border-default, #e2e8f0); border-radius: 10px;
2197
+ }
2198
+ .comp-preview-col {
2199
+ display: flex; flex-direction: column; gap: 16px;
2200
+ padding: 24px; max-width: 400px;
2201
+ background: var(--color-bg-default, #fff);
2202
+ border: 1px solid var(--color-border-default, #e2e8f0); border-radius: 10px;
2203
+ }
2204
+
2205
+ /* \u2500\u2500 Props table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2206
+ .props-table { width: 100%; border-collapse: collapse; font-size: 13px; }
2207
+ .props-table th {
2208
+ text-align: left; padding: 8px 12px;
2209
+ font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em;
2210
+ color: var(--color-text-secondary, #64748b);
2211
+ border-bottom: 2px solid var(--color-border-default, #e2e8f0);
2212
+ }
2213
+ .props-table td { padding: 10px 12px; border-bottom: 1px solid var(--color-border-default, #e2e8f0); vertical-align: top; line-height: 1.5; }
2214
+ .props-table tr:last-child td { border-bottom: none; }
2215
+ .prop-name { font-family: monospace; font-size: 13px; color: var(--color-text-primary, #0f172a); font-weight: 600; }
2216
+ .prop-type { font-family: monospace; font-size: 12px; color: var(--color-action, #2563eb); }
2217
+ .prop-default { font-family: monospace; font-size: 12px; color: var(--color-text-secondary, #64748b); }
2218
+ .prop-desc { font-size: 13px; color: var(--color-text-secondary, #64748b); }
2219
+ .prop-required-cell { text-align: center; }
2220
+ .prop-required {
2221
+ display: inline-block; font-size: 10px; font-weight: 700;
2222
+ text-transform: uppercase; letter-spacing: 0.04em;
2223
+ color: #dc2626; background: #fef2f2;
2224
+ border: 1px solid #fecaca; border-radius: 4px; padding: 2px 6px;
2225
+ }
2226
+ .prop-optional { color: var(--color-text-secondary, #64748b); font-size: 13px; }
2227
+
2228
+ /* \u2500\u2500 Examples \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2229
+ .example-block { border: 1px solid var(--color-border-default, #e2e8f0); border-radius: 10px; overflow: hidden; margin-bottom: 20px; }
2230
+ .example-header { padding: 14px 16px; border-bottom: 1px solid var(--color-border-default, #e2e8f0); background: var(--color-bg-default, #fff); }
2231
+ .example-label { font-size: 13px; font-weight: 600; color: var(--color-text-primary, #0f172a); }
2232
+ .example-desc { font-size: 12px; color: var(--color-text-secondary, #64748b); margin-top: 2px; }
2233
+ .example-preview { padding: 28px 24px; background: var(--color-bg-subtle, #f8fafc); border-bottom: 1px solid var(--color-border-default, #e2e8f0); }
2234
+ .example-code-wrap { position: relative; }
2235
+ .example-code-bar {
2236
+ display: flex; justify-content: space-between; align-items: center;
2237
+ padding: 6px 14px;
2238
+ background: var(--color-bg-overlay, #f1f5f9);
2239
+ color: var(--color-text-secondary, #64748b);
2240
+ font-size: 11px; font-weight: 600; letter-spacing: 0.05em;
2241
+ border-bottom: 1px solid var(--color-border-default, #e2e8f0);
2242
+ }
2243
+ .copy-btn {
2244
+ background: transparent; border: 1px solid var(--color-border-default, #e2e8f0);
2245
+ color: var(--color-text-secondary, #64748b); font-size: 11px; padding: 3px 10px;
2246
+ border-radius: 4px; cursor: pointer; transition: background 120ms, color 120ms;
2247
+ }
2248
+ .copy-btn:hover { background: var(--color-bg-subtle, #f8fafc); color: var(--color-text-primary, #0f172a); }
2249
+ .copy-btn.copied { color: #16a34a; border-color: #16a34a; }
2250
+ .example-code {
2251
+ margin: 0; padding: 16px 18px;
2252
+ background: var(--color-bg-default, #fff);
2253
+ color: var(--color-text-primary, #0f172a);
2254
+ border: 1px solid var(--color-border-default, #e2e8f0); border-top: none;
2255
+ font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
2256
+ font-size: 12.5px; line-height: 1.65; overflow-x: auto; white-space: pre;
2257
+ }
2258
+
2259
+ /* \u2500\u2500 Accessibility \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2260
+ .a11y-list { display: flex; flex-direction: column; gap: 1px; }
2261
+ .a11y-item { padding: 16px; border: 1px solid var(--color-border-default, #e2e8f0); border-radius: 8px; margin-bottom: 10px; background: var(--color-bg-default, #fff); }
2262
+ .a11y-header { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }
2263
+ .a11y-criterion { font-size: 13px; font-weight: 600; color: var(--color-text-primary, #0f172a); }
2264
+ .a11y-badge { font-size: 10px; font-weight: 700; padding: 2px 7px; border-radius: 99px; letter-spacing: 0.04em; }
2265
+ .a11y-badge-a { background: #f0fdf4; color: #15803d; border: 1px solid #bbf7d0; }
2266
+ .a11y-badge-aa { background: #eff6ff; color: #1d4ed8; border: 1px solid #bfdbfe; }
2267
+ .a11y-badge-aaa { background: #faf5ff; color: #7e22ce; border: 1px solid #e9d5ff; }
2268
+ .a11y-desc { font-size: 13px; color: var(--color-text-secondary, #64748b); line-height: 1.6; }
2269
+
2270
+ /* \u2500\u2500 AI Metadata \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2271
+ .ai-meta-intro { padding: 14px 16px; background: var(--color-bg-overlay, #f1f5f9); border: 1px solid var(--color-border-default, #e2e8f0); border-radius: 8px; margin-bottom: 4px; }
2272
+ .ai-meta-intro p { font-size: 13px; color: var(--color-text-secondary, #64748b); line-height: 1.6; }
2273
+ .ai-meta-intro code { font-family: monospace; font-size: 12px; color: var(--color-action, #2563eb); }
2274
+ .ai-guidance-list { padding-left: 20px; display: flex; flex-direction: column; gap: 8px; margin-top: 10px; }
2275
+ .ai-guidance-list li { font-size: 13px; color: var(--color-text-secondary, #64748b); line-height: 1.6; }
2276
+
2277
+ /* \u2500\u2500 Locked tabs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2278
+ .comp-tab.locked { opacity: 0.45; cursor: default; }
2279
+ .comp-tab.locked:hover { color: var(--color-text-secondary, #64748b); }
2280
+ .locked-panel {
2281
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
2282
+ padding: 64px 32px; text-align: center;
2283
+ border: 1px dashed var(--color-border-default, #e2e8f0); border-radius: 10px;
2284
+ background: var(--color-bg-subtle, #f8fafc);
2285
+ }
2286
+ .locked-icon { font-size: 28px; color: var(--color-text-secondary, #64748b); margin-bottom: 12px; }
2287
+ .locked-title { font-size: 14px; font-weight: 600; color: var(--color-text-primary, #0f172a); margin-bottom: 8px; }
2288
+ .locked-desc { font-size: 13px; color: var(--color-text-secondary, #64748b); max-width: 360px; line-height: 1.6; margin-bottom: 6px; }
2289
+ .locked-hint { font-size: 12px; color: var(--color-text-secondary, #64748b); font-family: monospace; }
2290
+ .locked-hint code { background: var(--color-bg-overlay, #f1f5f9); padding: 1px 6px; border-radius: 4px; border: 1px solid var(--color-border-default, #e2e8f0); }
2291
+
2292
+ /* \u2500\u2500 Component primitives \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2293
+ .ds-btn { border: none; cursor: pointer; font-size: 14px; font-weight: 500; padding: 8px 16px; transition: filter 120ms; }
2294
+ .ds-btn:hover:not(:disabled) { filter: brightness(0.92); }
2295
+ .ds-field { display: flex; flex-direction: column; gap: 4px; }
2296
+ .ds-label { font-size: 13px; font-weight: 500; }
2297
+ .ds-input { border: 1.5px solid; padding: 8px 12px; font-size: 14px; outline: none; transition: border-color 150ms, box-shadow 150ms; width: 100%; }
2298
+ .ds-input:focus { box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-action, #2563eb) 20%, transparent); border-color: var(--color-action, #2563eb) !important; }
2299
+ .ds-card { border: 1px solid; overflow: hidden; width: 220px; }
2300
+ .ds-card-header { padding: 12px 14px; font-size: 14px; font-weight: 600; }
2301
+ .ds-card-body { padding: 12px 14px; }
2302
+ .ds-card-footer { padding: 10px 14px; display: flex; justify-content: flex-end; }
2303
+
2304
+ /* \u2500\u2500 Spinner animation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2305
+ @keyframes dsforge-spin { to { transform: rotate(360deg); } }
2306
+ @media (prefers-reduced-motion: reduce) { @keyframes dsforge-spin { to { transform: none; } } }
2307
+ </style>
2308
+ </head>
2309
+ <body>
2310
+
2311
+ <aside class="sidebar">
2312
+ <div class="sidebar-header">
2313
+ <div class="sidebar-title">${esc(name)}</div>
2314
+ <div class="sidebar-version">v${esc(version)}${themes.length ? " \xB7 " + themes.join(", ") : ""}</div>
2315
+ </div>
2316
+ <div class="sidebar-section">
2317
+ <div class="sidebar-section-label">Foundations</div>
2318
+ ${foundationItems.map(
2319
+ (item) => `
2320
+ <a class="nav-item${item.id === "colors" ? " active" : ""}" onclick="showPage('${item.id}', this)" href="#">${esc(item.label)}</a>
2321
+ `
2322
+ ).join("")}
2323
+ </div>
2324
+ <div class="sidebar-section">
2325
+ <div class="sidebar-section-label">Components</div>
2326
+ ${componentItems.map(
2327
+ (item) => `
2328
+ <a class="nav-item" onclick="showPage('${item.id}', this)" href="#">${esc(item.label)}</a>
2329
+ `
2330
+ ).join("")}
2331
+ </div>
2332
+ </aside>
2333
+
2334
+ <div class="main">
2335
+ <div class="topbar">
2336
+ <div class="topbar-breadcrumb">
2337
+ ${esc(name)} / <span id="topbar-current">Colors</span>
2338
+ </div>
2339
+ <div class="topbar-actions">
2340
+ ${themes.length >= 2 ? `
2341
+ <div class="theme-toggle">
2342
+ ${themes.map(
2343
+ (t, i) => `
2344
+ <button class="theme-btn${i === 0 ? " active" : ""}" onclick="setTheme('${t}', this)">${esc(t)}</button>
2345
+ `
2346
+ ).join("")}
2347
+ </div>
2348
+ ` : ""}
2349
+ </div>
2350
+ </div>
2351
+
2352
+ <div class="content">
2353
+ ${allItems.map(
2354
+ ({ id, label }) => `
2355
+ <div class="page${id === "colors" ? " active" : ""}" id="page-${id}">
2356
+ <h1 class="page-title">${esc(label)}</h1>
2357
+ <p class="page-desc">${esc(pageDesc(id, name))}</p>
2358
+ ${sections[id] ?? ""}
2359
+ </div>
2360
+ `
2361
+ ).join("")}
2362
+ </div>
2363
+ </div>
2364
+
2365
+ <script>
2366
+ function showPage(id, el) {
2367
+ document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
2368
+ document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
2369
+ document.getElementById('page-' + id).classList.add('active');
2370
+ el.classList.add('active');
2371
+ document.getElementById('topbar-current').textContent = el.textContent.trim();
2372
+ }
2373
+
2374
+ function setTheme(name, btn) {
2375
+ document.documentElement.setAttribute('data-theme', name);
2376
+ document.querySelectorAll('.theme-btn').forEach(b => b.classList.remove('active'));
2377
+ btn.classList.add('active');
2378
+ }
2379
+
2380
+ function switchTab(compId, tabId, btn) {
2381
+ const tabs = document.querySelectorAll('#' + compId + '-tabs .comp-tab');
2382
+ const panels = document.querySelectorAll('#' + compId + '-tabs .comp-tab-panel');
2383
+ tabs.forEach(t => { t.classList.remove('active'); t.setAttribute('aria-selected', 'false'); });
2384
+ panels.forEach(p => p.classList.remove('active'));
2385
+ btn.classList.add('active');
2386
+ btn.setAttribute('aria-selected', 'true');
2387
+ document.getElementById(compId + '-panel-' + tabId).classList.add('active');
2388
+ }
2389
+
2390
+ function copyCode(id, btn) {
2391
+ const code = document.getElementById(id).textContent;
2392
+ navigator.clipboard.writeText(code).then(() => {
2393
+ btn.textContent = 'Copied!';
2394
+ btn.classList.add('copied');
2395
+ setTimeout(() => { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 2000);
2396
+ });
2397
+ }
2398
+ </script>
2399
+ </body>
2400
+ </html>`;
2401
+ }
2402
+ function pageDesc(id, name) {
2403
+ const foundations = {
2404
+ colors: `The color tokens used across ${name}. Global palette tokens are the raw values; semantic tokens map intent to palette.`,
2405
+ typography: `Type scale and font settings for ${name}. All sizes, weights, and line heights.`,
2406
+ spacing: `Spacing scale based on the configured base unit. Used for padding, margin, and gap values.`,
2407
+ radius: `Border radius tokens. Applied to buttons, inputs, cards, and other surfaces.`,
2408
+ elevation: `Box shadow levels. Higher levels appear more elevated.`,
2409
+ motion: `Duration and easing tokens. Click any row to preview the animation.`
2410
+ };
2411
+ if (id in foundations) return foundations[id] ?? "";
2412
+ const entry = SHOWCASE_COMPONENTS.find((c) => c.id === id);
2413
+ return entry?.pageDescription ?? "";
2414
+ }
2415
+ export {
2416
+ generateShowcase
2417
+ };