@nghitrum/dsforge 0.1.5-alpha.1 → 0.1.5-alpha.10

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.
@@ -1,3 +1,15 @@
1
+ import {
2
+ PRESETS,
3
+ RADIUS_PRESETS,
4
+ SPACING_PRESETS,
5
+ buildSemanticSpacing,
6
+ isProUnlocked
7
+ } from "./chunk-YUPXTQZ5.js";
8
+ import {
9
+ COMPONENT_JSON_DEFINITIONS,
10
+ COMPONENT_METADATA_DEFINITIONS
11
+ } from "./chunk-A7VW6SII.js";
12
+
1
13
  // src/generators/showcase/types.ts
2
14
  function esc(s) {
3
15
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -32,31 +44,6 @@ function componentTokens(config, tokens) {
32
44
  };
33
45
  }
34
46
 
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
47
  // src/generators/showcase/foundations.ts
61
48
  function buildColorSection(config, tokens) {
62
49
  const groups = [];
@@ -117,26 +104,33 @@ function buildTypographySection(config) {
117
104
  }
118
105
  function buildSpacingSection(config) {
119
106
  const scale = config.spacing?.scale ?? {};
120
- const baseUnit = config.spacing?.baseUnit ?? 4;
121
- const row = (key, val) => `
107
+ const scaleRow = (key) => `
108
+ <div class="spacing-row">
109
+ <span class="spacing-key">spacing-${esc(key)}</span>
110
+ <div class="spacing-bar-wrap">
111
+ <div class="spacing-bar" style="width:min(calc(var(--spacing-${esc(key)}) * 2), 320px)"></div>
112
+ </div>
113
+ <span class="spacing-val" data-spacing-var="--spacing-${esc(key)}"></span>
114
+ </div>`;
115
+ const semanticRow = (key) => `
122
116
  <div class="spacing-row">
123
117
  <span class="spacing-key">${esc(key)}</span>
124
118
  <div class="spacing-bar-wrap">
125
- <div class="spacing-bar" style="width:${Math.min(Number(val) * 2, 320)}px"></div>
119
+ <div class="spacing-bar" style="width:min(calc(var(--${esc(key)}) * 2), 320px)"></div>
126
120
  </div>
127
- <span class="spacing-val">${val}px</span>
121
+ <span class="spacing-val" data-spacing-var="--${esc(key)}"></span>
128
122
  </div>`;
129
123
  return `
130
124
  <div class="section-block">
131
- <h3 class="group-title">Base Unit: ${baseUnit}px</h3>
125
+ <h3 class="group-title">Scale</h3>
132
126
  <div class="spacing-list">
133
- ${Object.entries(scale).map(([k, v]) => row(k, v)).join("")}
127
+ ${Object.keys(scale).map((k) => scaleRow(k)).join("")}
134
128
  </div>
135
129
  </div>
136
130
  <div class="section-block">
137
131
  <h3 class="group-title">Semantic Spacing</h3>
138
132
  <div class="spacing-list">
139
- ${Object.entries(config.spacing?.semantic ?? {}).map(([k, v]) => row(k, v)).join("")}
133
+ ${Object.keys(config.spacing?.semantic ?? {}).map((k) => semanticRow(k)).join("")}
140
134
  </div>
141
135
  </div>
142
136
  `;
@@ -147,12 +141,12 @@ function buildRadiusSection(config) {
147
141
  <div class="section-block">
148
142
  <h3 class="group-title">Border Radius</h3>
149
143
  <div class="radius-grid">
150
- ${Object.entries(radius).map(
151
- ([key, val]) => `
144
+ ${Object.keys(radius).map(
145
+ (key) => `
152
146
  <div class="radius-item">
153
- <div class="radius-box" style="border-radius:${val}px"></div>
147
+ <div class="radius-box" style="border-radius:var(--radius-${esc(key)})"></div>
154
148
  <span class="radius-key">${esc(key)}</span>
155
- <span class="radius-val">${val}px</span>
149
+ <span class="radius-val" data-spacing-var="--radius-${esc(key)}"></span>
156
150
  </div>
157
151
  `
158
152
  ).join("")}
@@ -228,11 +222,9 @@ var lockedPanel = (label) => `
228
222
  <p class="locked-desc">This tab is available with a dsforge Pro license.</p>
229
223
  <p class="locked-hint">Set the <code>DSFORGE_KEY</code> environment variable to unlock.</p>
230
224
  </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 = `
225
+ function buildPropsTable(props) {
226
+ const sorted = [...props].sort((a, b) => (b.required ? 1 : 0) - (a.required ? 1 : 0));
227
+ return `
236
228
  <table class="props-table">
237
229
  <thead>
238
230
  <tr>
@@ -244,7 +236,7 @@ function buildComponentPage(def, isPro) {
244
236
  </tr>
245
237
  </thead>
246
238
  <tbody>
247
- ${def.props.map(
239
+ ${sorted.map(
248
240
  (p) => `
249
241
  <tr>
250
242
  <td><code class="prop-name">${esc(p.name)}</code></td>
@@ -258,26 +250,83 @@ function buildComponentPage(def, isPro) {
258
250
  ).join("")}
259
251
  </tbody>
260
252
  </table>`;
261
- const examplesHtml = def.examples.map(
253
+ }
254
+ function buildExamplesHtml(id, examples) {
255
+ return examples.map(
262
256
  (ex, i) => `
263
257
  <div class="example-block">
264
258
  <div class="example-header">
265
259
  <div class="example-label">${esc(ex.label)}</div>
266
- <div class="example-desc">${esc(ex.description)}</div>
260
+ ${ex.description ? `<div class="example-desc">${esc(ex.description)}</div>` : ""}
267
261
  </div>
268
- <div class="example-preview">${ex.previewHtml}</div>
262
+ ${ex.previewHtml ? `<div class="example-preview">${ex.previewHtml}</div>` : ""}
269
263
  <div class="example-code-wrap">
270
264
  <div class="example-code-bar">
271
265
  <span>TSX</span>
272
- <button class="copy-btn" onclick="copyCode('${def.id}-ex-${i}', this)">Copy</button>
266
+ <button class="copy-btn" onclick="copyCode('${id}-ex-${i}', this)">Copy</button>
273
267
  </div>
274
- <pre class="example-code" id="${def.id}-ex-${i}">${esc(ex.code)}</pre>
268
+ <pre class="example-code" id="${id}-ex-${i}">${esc(ex.code)}</pre>
275
269
  </div>
276
270
  </div>`
277
271
  ).join("");
278
- const a11yHtml = `
272
+ }
273
+ function requirementBadge(val) {
274
+ if (typeof val === "boolean") {
275
+ return val ? '<span class="a11y-badge a11y-badge-aa">Yes</span>' : '<span class="a11y-badge" style="background:var(--color-bg-overlay,#f1f5f9);color:var(--color-text-secondary,#64748b);border:1px solid var(--color-border-default,#e2e8f0)">No</span>';
276
+ }
277
+ return `<span class="a11y-badge a11y-badge-aa">${esc(val)}</span>`;
278
+ }
279
+ function buildA11yHtml(contract, wcagItems) {
280
+ const requirements = [
281
+ {
282
+ label: "Keyboard operable",
283
+ value: contract.keyboard,
284
+ desc: "Component can be fully operated with keyboard alone. No mouse required."
285
+ },
286
+ {
287
+ label: "Focus ring",
288
+ value: contract.focusRing,
289
+ desc: "Visible focus indicator requirement when the element receives keyboard focus."
290
+ },
291
+ {
292
+ label: "aria-label",
293
+ value: contract.ariaLabel,
294
+ desc: "When a programmatic accessible label must be provided by the consumer."
295
+ }
296
+ ];
297
+ const requirementsHtml = `
298
+ <div class="a11y-requirements">
299
+ ${requirements.map(
300
+ (r) => `
301
+ <div class="a11y-req-row">
302
+ <div class="a11y-req-meta">
303
+ <span class="a11y-req-label">${esc(r.label)}</span>
304
+ ${requirementBadge(r.value)}
305
+ </div>
306
+ <p class="a11y-req-desc">${esc(r.desc)}</p>
307
+ </div>`
308
+ ).join("")}
309
+ ${contract.roles.length > 0 ? `<div class="a11y-req-row">
310
+ <div class="a11y-req-meta">
311
+ <span class="a11y-req-label">ARIA roles</span>
312
+ <span style="display:flex;gap:4px;flex-wrap:wrap">
313
+ ${contract.roles.map((r) => `<code class="a11y-role-chip">${esc(r)}</code>`).join("")}
314
+ </span>
315
+ </div>
316
+ <p class="a11y-req-desc">The semantic roles this component exposes to assistive technology.</p>
317
+ </div>` : ""}
318
+ ${contract.notes.map(
319
+ (note) => `
320
+ <div class="a11y-note">
321
+ <span class="a11y-note-icon">\u21B3</span>
322
+ <p class="a11y-note-text">${esc(note)}</p>
323
+ </div>`
324
+ ).join("")}
325
+ </div>`;
326
+ const wcagHtml = wcagItems.length > 0 ? `
327
+ <div class="group-title" style="margin-top:32px">WCAG Criteria</div>
279
328
  <div class="a11y-list">
280
- ${def.a11y.map(
329
+ ${wcagItems.map(
281
330
  (item) => `
282
331
  <div class="a11y-item">
283
332
  <div class="a11y-header">
@@ -287,45 +336,60 @@ function buildComponentPage(def, isPro) {
287
336
  <p class="a11y-desc">${esc(item.description)}</p>
288
337
  </div>`
289
338
  ).join("")}
290
- </div>`;
291
- const aiJson = JSON.stringify(def.aiMeta, null, 2);
292
- const aiHtml = `
339
+ </div>` : "";
340
+ return `
341
+ <div class="group-title">Requirements</div>
342
+ ${requirementsHtml}
343
+ ${wcagHtml}`;
344
+ }
345
+ function buildAiMetaHtml(id, meta) {
346
+ const metaJson = JSON.stringify(meta, null, 2);
347
+ return `
293
348
  <div class="ai-meta-intro">
294
- <p>This JSON contract is emitted to <code>dist-ds/metadata/${def.id}.json</code>.
349
+ <p>This JSON contract is emitted to <code>components/${esc(meta.name)}/${esc(meta.name)}.metadata.json</code>.
295
350
  AI coding assistants use it to understand when and how to use this component correctly.</p>
296
351
  </div>
297
352
  <div class="example-code-wrap" style="margin-top:16px">
298
353
  <div class="example-code-bar">
299
354
  <span>JSON</span>
300
- <button class="copy-btn" onclick="copyCode('${def.id}-ai-meta', this)">Copy</button>
355
+ <button class="copy-btn" onclick="copyCode('${id}-ai-meta', this)">Copy</button>
301
356
  </div>
302
- <pre class="example-code" id="${def.id}-ai-meta">${esc(aiJson)}</pre>
357
+ <pre class="example-code" id="${id}-ai-meta">${esc(metaJson)}</pre>
303
358
  </div>
304
359
  <div class="ai-guidance">
305
360
  <div class="group-title" style="margin-top:24px">AI usage guidance</div>
306
361
  <ul class="ai-guidance-list">
307
- ${def.aiMeta.aiGuidance.map((g) => `<li>${esc(g)}</li>`).join("")}
362
+ ${meta.aiGuidance.map((g) => `<li>${esc(g)}</li>`).join("")}
308
363
  </ul>
309
364
  </div>`;
365
+ }
366
+ function buildComponentPage(input) {
367
+ const { id, description, overviewHtml, json, showcaseExamples, a11yContract, a11yItems, metadata, isPro } = input;
368
+ const tabId = (tab) => `${id}-tab-${tab}`;
369
+ const panelId = (tab) => `${id}-panel-${tab}`;
370
+ const overviewContent = `
371
+ <div class="comp-overview">${overviewHtml}</div>
372
+ <p class="component-description">${esc(description)}</p>`;
373
+ const propsContent = buildPropsTable(json.props);
374
+ const examplesContent = buildExamplesHtml(id, showcaseExamples);
375
+ const a11yContent = a11yContract ? buildA11yHtml(a11yContract, a11yItems) : `<div class="a11y-list">${a11yItems.map((item) => `
376
+ <div class="a11y-item">
377
+ <div class="a11y-header">
378
+ <span class="a11y-criterion">${esc(item.criterion)}</span>
379
+ <span class="a11y-badge a11y-badge-${item.level.toLowerCase()}">WCAG ${item.level}</span>
380
+ </div>
381
+ <p class="a11y-desc">${esc(item.description)}</p>
382
+ </div>`).join("")}</div>`;
383
+ const aiContent = isPro && metadata ? buildAiMetaHtml(id, metadata) : lockedPanel("AI Metadata");
310
384
  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
- }
385
+ { id: "overview", label: "Overview", content: overviewContent, locked: false },
386
+ { id: "props", label: "Props", content: propsContent, locked: false },
387
+ { id: "examples", label: "Examples", content: examplesContent, locked: false },
388
+ { id: "accessibility", label: "Accessibility", content: a11yContent, locked: false },
389
+ { id: "ai-metadata", label: "AI Metadata", content: aiContent, locked: !isPro }
326
390
  ];
327
391
  return `
328
- <div class="comp-tabs" id="${def.id}-tabs">
392
+ <div class="comp-tabs" id="${id}-tabs">
329
393
  <div class="comp-tab-bar" role="tablist">
330
394
  ${tabs.map(
331
395
  (t, i) => `
@@ -335,7 +399,7 @@ function buildComponentPage(def, isPro) {
335
399
  role="tab"
336
400
  aria-selected="${i === 0}"
337
401
  aria-controls="${panelId(t.id)}"
338
- onclick="${t.locked ? "return false" : `switchTab('${def.id}', '${t.id}', this)`}"
402
+ onclick="${t.locked ? "return false" : `switchTab('${id}', '${t.id}', this)`}"
339
403
  ${t.locked ? 'title="Unlock with dsforge Pro"' : ""}
340
404
  >${esc(t.label)}${t.locked ? " &#x1F512;" : ""}</button>`
341
405
  ).join("")}
@@ -366,6 +430,9 @@ function buttonDef(config, tokens) {
366
430
  id: "button",
367
431
  label: "Button",
368
432
  description: "Triggers an action or event. Use for form submissions, dialogs, and in-page actions.",
433
+ usageExample: `<Button variant="primary" size="md" onClick={() => {}}>
434
+ Save changes
435
+ </Button>`,
369
436
  overviewHtml: `
370
437
  <div class="comp-overview-section">
371
438
  <div class="comp-overview-label">Variants</div>
@@ -552,6 +619,13 @@ function inputDef(config, tokens) {
552
619
  id: "input",
553
620
  label: "Input",
554
621
  description: "Single-line text field. Covers all standard input types with label, helper text, and validation states.",
622
+ usageExample: `<Input
623
+ label="Email"
624
+ placeholder="you@example.com"
625
+ value={email}
626
+ onChange={(e) => setEmail(e.target.value)}
627
+ error={emailError}
628
+ />`,
555
629
  overviewHtml: `
556
630
  <div class="comp-overview-section">
557
631
  <div class="comp-overview-label">States</div>
@@ -757,6 +831,10 @@ function cardDef(config, tokens) {
757
831
  id: "card",
758
832
  label: "Card",
759
833
  description: "A surface that groups related content. Supports header, body, and optional footer slots.",
834
+ usageExample: `<Card padding="lg">
835
+ <h2>Card title</h2>
836
+ <p>Card content goes here.</p>
837
+ </Card>`,
760
838
  overviewHtml: `
761
839
  <div class="comp-overview-section">
762
840
  <div class="comp-overview-label">Variants</div>
@@ -901,7 +979,7 @@ function cardDef(config, tokens) {
901
979
  // src/generators/showcase/components/badge.ts
902
980
  function badgeDef(config, tokens) {
903
981
  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>`;
982
+ const badgeHtml = (label, bg, color) => `<span style="display:inline-flex;align-items:center;font-family:${esc(ff)};font-size:var(--font-size-caption,0.75rem);font-weight:500;padding:2px var(--spacing-2,8px);border-radius:var(--radius-full,9999px);background:${bg};color:${color};white-space:nowrap">${label}</span>`;
905
983
  const variants = [
906
984
  { label: "Default", bg: "#f1f5f9", color: "#6b7280" },
907
985
  { label: "Success", bg: "#dcfce7", color: "#16a34a" },
@@ -913,6 +991,8 @@ function badgeDef(config, tokens) {
913
991
  id: "badge",
914
992
  label: "Badge",
915
993
  description: "Compact label for status, categories, or counts. Display-only \u2014 not interactive.",
994
+ usageExample: `<Badge variant="success">Active</Badge>
995
+ <Badge variant="warning">Pending</Badge>`,
916
996
  overviewHtml: `
917
997
  <div class="comp-overview-section">
918
998
  <div class="comp-overview-label">Variants</div>
@@ -923,10 +1003,10 @@ function badgeDef(config, tokens) {
923
1003
  <div class="comp-overview-section">
924
1004
  <div class="comp-overview-label">Sizes</div>
925
1005
  <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>
1006
+ <span style="display:inline-flex;align-items:center;font-family:${esc(ff)};font-size:var(--font-size-caption,0.75rem);font-weight:500;padding:1px var(--spacing-1,4px);border-radius:var(--radius-full,9999px);background:#dbeafe;color:#2563eb">Small</span>
1007
+ <span style="display:inline-flex;align-items:center;font-family:${esc(ff)};font-size:var(--font-size-caption,0.75rem);font-weight:500;padding:2px var(--spacing-2,8px);border-radius:var(--radius-full,9999px);background:#dbeafe;color:#2563eb">Medium</span>
1008
+ <span style="display:inline-flex;align-items:center;font-family:${esc(ff)};font-size:var(--font-size-body,1rem);font-weight:500;padding:var(--spacing-1,4px) var(--spacing-3,12px);border-radius:var(--radius-full,9999px);background:#dbeafe;color:#2563eb">Large</span>
1009
+ <span style="display:inline-flex;width:var(--spacing-2,8px);height:var(--spacing-2,8px);border-radius:50%;background:#16a34a" title="Dot mode"></span>
930
1010
  </div>
931
1011
  </div>`,
932
1012
  props: [
@@ -1047,25 +1127,30 @@ function checkboxDef(config, tokens) {
1047
1127
  const fill = checked || indeterminate ? "#2563eb" : C.bg;
1048
1128
  const borderColor = checked || indeterminate ? "#2563eb" : C.border;
1049
1129
  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>`;
1130
+ return `<span style="display:inline-flex;align-items:center;justify-content:center;width:var(--control-size-md,16px);height:var(--control-size-md,16px);border-radius:var(--radius-sm,2px);border:2px solid ${borderColor};background:${fill};flex-shrink:0">${mark}</span>`;
1051
1131
  };
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}">
1132
+ const checkboxHtml = (label, checked, opts = "", helper = "") => `<label style="display:inline-flex;align-items:flex-start;gap:var(--spacing-2,8px);cursor:pointer;font-family:${esc(ff)};${opts}">
1053
1133
  ${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>
1134
+ <span style="font-size:var(--font-size-small,0.875rem);color:${C.text};line-height:1.4">${label}${helper ? `<br><span style="font-size:var(--font-size-caption,0.75rem);color:${C.textSecondary}">${helper}</span>` : ""}</span>
1055
1135
  </label>`;
1056
1136
  return {
1057
1137
  id: "checkbox",
1058
1138
  label: "Checkbox",
1059
1139
  description: "Binary toggle for boolean values. Supports indeterminate state for partial selections.",
1140
+ usageExample: `<Checkbox
1141
+ label="Accept terms"
1142
+ checked={accepted}
1143
+ onChange={(e) => setAccepted(e.target.checked)}
1144
+ />`,
1060
1145
  overviewHtml: `
1061
1146
  <div class="comp-overview-section">
1062
1147
  <div class="comp-overview-label">States</div>
1063
1148
  <div class="comp-preview-col">
1064
1149
  ${checkboxHtml("Unchecked", false)}
1065
1150
  ${checkboxHtml("Checked", true)}
1066
- <label style="display:inline-flex;align-items:flex-start;gap:8px;cursor:pointer;font-family:${esc(ff)}">
1151
+ <label style="display:inline-flex;align-items:flex-start;gap:var(--spacing-2,8px);cursor:pointer;font-family:${esc(ff)}">
1067
1152
  ${boxHtml(false, true)}
1068
- <span style="font-size:14px;color:${C.text}">Indeterminate</span>
1153
+ <span style="font-size:var(--font-size-small,0.875rem);color:${C.text}">Indeterminate</span>
1069
1154
  </label>
1070
1155
  ${checkboxHtml("Disabled", false, "opacity:0.4;cursor:not-allowed")}
1071
1156
  </div>
@@ -1148,9 +1233,9 @@ function checkboxDef(config, tokens) {
1148
1233
  label="Select all (3 of 5)"
1149
1234
  indeterminate
1150
1235
  />`,
1151
- previewHtml: `<label style="display:inline-flex;align-items:center;gap:8px;cursor:pointer;font-family:${esc(ff)}">
1236
+ previewHtml: `<label style="display:inline-flex;align-items:center;gap:var(--spacing-2,8px);cursor:pointer;font-family:${esc(ff)}">
1152
1237
  ${boxHtml(false, true)}
1153
- <span style="font-size:14px;color:${C.text}">Select all (3 of 5)</span>
1238
+ <span style="font-size:var(--font-size-small,0.875rem);color:${C.text}">Select all (3 of 5)</span>
1154
1239
  </label>`
1155
1240
  },
1156
1241
  {
@@ -1227,24 +1312,26 @@ function radioDef(config, tokens) {
1227
1312
  };
1228
1313
  const circleHtml = (selected) => {
1229
1314
  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>` : ""}
1315
+ return `<span style="display:inline-flex;align-items:center;justify-content:center;width:var(--control-size-md,16px);height:var(--control-size-md,16px);border-radius:50%;border:2px solid ${borderColor};background:${C.bg};flex-shrink:0">
1316
+ ${selected ? `<span style="width:calc(var(--control-size-md,16px) / 2);height:calc(var(--control-size-md,16px) / 2);border-radius:50%;background:${C.action}"></span>` : ""}
1232
1317
  </span>`;
1233
1318
  };
1234
- const radioHtml = (label, selected, opts = "") => `<label style="display:inline-flex;align-items:center;gap:8px;cursor:pointer;font-family:${esc(ff)};${opts}">
1319
+ const radioHtml = (label, selected, opts = "") => `<label style="display:inline-flex;align-items:center;gap:var(--spacing-2,8px);cursor:pointer;font-family:${esc(ff)};${opts}">
1235
1320
  ${circleHtml(selected)}
1236
- <span style="font-size:14px;color:${C.text}">${label}</span>
1321
+ <span style="font-size:var(--font-size-small,0.875rem);color:${C.text}">${label}</span>
1237
1322
  </label>`;
1238
1323
  return {
1239
1324
  id: "radio",
1240
1325
  label: "Radio",
1241
1326
  description: "Single selection within a mutually exclusive group. Always pair Radio with RadioGroup.",
1327
+ usageExample: `<Radio label="Option A" name="choice" value="a" checked={choice === 'a'} onChange={() => setChoice('a')} />
1328
+ <Radio label="Option B" name="choice" value="b" checked={choice === 'b'} onChange={() => setChoice('b')} />`,
1242
1329
  overviewHtml: `
1243
1330
  <div class="comp-overview-section">
1244
1331
  <div class="comp-overview-label">RadioGroup (vertical)</div>
1245
1332
  <div class="comp-preview-col">
1246
1333
  <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>
1334
+ <legend style="font-size:var(--font-size-small,0.875rem);font-weight:600;color:${C.text};margin-bottom:var(--spacing-2,8px)">Notification preference</legend>
1248
1335
  <div class="comp-preview-col">
1249
1336
  ${radioHtml("Email", true)}
1250
1337
  ${radioHtml("SMS", false)}
@@ -1327,8 +1414,8 @@ function radioDef(config, tokens) {
1327
1414
  <Radio value="annual" label="Annual (save 20%)" />
1328
1415
  </RadioGroup>`,
1329
1416
  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">
1417
+ <legend style="font-size:var(--font-size-small,0.875rem);font-weight:600;color:${C.text};margin-bottom:var(--spacing-2,8px)">Billing cycle</legend>
1418
+ <div style="display:flex;flex-direction:column;gap:var(--spacing-2,8px)">
1332
1419
  ${radioHtml("Monthly", true)}
1333
1420
  ${radioHtml("Annual (save 20%)", false)}
1334
1421
  </div>
@@ -1343,8 +1430,8 @@ function radioDef(config, tokens) {
1343
1430
  <Radio value="lg" label="L" />
1344
1431
  </RadioGroup>`,
1345
1432
  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">
1433
+ <legend style="font-size:var(--font-size-small,0.875rem);font-weight:600;color:${C.text};margin-bottom:var(--spacing-2,8px)">Size</legend>
1434
+ <div style="display:flex;gap:var(--spacing-4,16px)">
1348
1435
  ${radioHtml("S", false)}
1349
1436
  ${radioHtml("M", true)}
1350
1437
  ${radioHtml("L", false)}
@@ -1425,6 +1512,12 @@ function selectDef(config, tokens) {
1425
1512
  id: "select",
1426
1513
  label: "Select",
1427
1514
  description: "Dropdown picker for selecting from a list of options. Wraps native <select> for full accessibility.",
1515
+ usageExample: `<Select
1516
+ label="Country"
1517
+ options={[{ label: 'Norway', value: 'no' }, { label: 'Sweden', value: 'se' }]}
1518
+ value={country}
1519
+ onChange={(e) => setCountry(e.target.value)}
1520
+ />`,
1428
1521
  overviewHtml: `
1429
1522
  <div class="comp-overview-section">
1430
1523
  <div class="comp-overview-label">States</div>
@@ -1599,17 +1692,23 @@ function toastDef(config, tokens) {
1599
1692
  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
1693
  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
1694
  };
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">
1695
+ const alertHtml = (v, title, body) => `<div role="alert" style="display:flex;gap:var(--spacing-3,12px);padding:var(--component-padding-sm,12px) var(--component-padding-md,16px);border-radius:${r};border:1px solid ${v.border};background:${v.bg};font-family:${esc(ff)};max-width:360px">
1603
1696
  <span style="color:${v.icon};flex-shrink:0;margin-top:1px">${icons[v.name]}</span>
1604
1697
  <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>
1698
+ <p style="margin:0;font-size:var(--font-size-small,0.875rem);font-weight:600;color:${v.icon}">${title}</p>
1699
+ <p style="margin:var(--spacing-1,4px) 0 0;font-size:var(--font-size-small,0.875rem);color:var(--color-text-secondary,#6b7280)">${body}</p>
1607
1700
  </div>
1608
1701
  </div>`;
1609
1702
  return {
1610
1703
  id: "toast",
1611
1704
  label: "Toast / Alert",
1612
1705
  description: "Feedback for user actions. Alert is inline and static; Toast is an overlay with auto-dismiss and a useToast() hook.",
1706
+ usageExample: `<Toast
1707
+ message="Changes saved successfully"
1708
+ variant="success"
1709
+ duration={3000}
1710
+ onDismiss={() => setToast(null)}
1711
+ />`,
1613
1712
  overviewHtml: `
1614
1713
  <div class="comp-overview-section">
1615
1714
  <div class="comp-overview-label">Alert \u2014 inline variants</div>
@@ -1685,11 +1784,11 @@ function toastDef(config, tokens) {
1685
1784
  >
1686
1785
  Upgrade to continue using all features.
1687
1786
  </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">
1787
+ previewHtml: `<div role="alert" style="display:flex;gap:var(--spacing-3,12px);padding:var(--component-padding-sm,12px) var(--component-padding-md,16px);border-radius:${r};border:1px solid ${vWarning.border};background:${vWarning.bg};font-family:${esc(ff)};max-width:360px">
1689
1788
  <span style="color:${vWarning.icon};flex-shrink:0;margin-top:1px">${icons["warning"]}</span>
1690
1789
  <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>
1790
+ <p style="margin:0;font-size:var(--font-size-small,0.875rem);font-weight:600;color:${vWarning.icon}">Your trial expires in 3 days</p>
1791
+ <p style="margin:var(--spacing-1,4px) 0 0;font-size:var(--font-size-small,0.875rem);color:var(--color-text-secondary,#6b7280)">Upgrade to continue using all features.</p>
1693
1792
  </div>
1694
1793
  <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
1794
  </div>`
@@ -1713,11 +1812,11 @@ toast.add({
1713
1812
  });`,
1714
1813
  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
1814
  <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)">
1815
+ <div style="display:flex;gap:var(--spacing-3,12px);padding:var(--component-padding-sm,12px) var(--component-padding-md,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
1816
  <span style="color:var(--color-success,#16a34a)">${icons["success"]}</span>
1718
1817
  <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>
1818
+ <p style="margin:0;font-size:var(--font-size-small,0.875rem);font-weight:600;color:var(--color-text-primary,#0f172a)">Saved</p>
1819
+ <p style="margin:var(--spacing-1,4px) 0 0;font-size:var(--font-size-small,0.875rem);color:var(--color-text-secondary,#6b7280)">Your changes have been saved.</p>
1721
1820
  </div>
1722
1821
  <button style="background:transparent;border:none;cursor:pointer;color:var(--color-text-secondary,#6b7280);padding:2px" aria-label="Dismiss">\u2715</button>
1723
1822
  </div>
@@ -1791,6 +1890,7 @@ function spinnerDef(config, tokens) {
1791
1890
  id: "spinner",
1792
1891
  label: "Spinner",
1793
1892
  description: "Loading indicator for async operations. Includes a visually hidden status label for screen readers.",
1893
+ usageExample: `<Spinner size="lg" label="Saving your changes" />`,
1794
1894
  overviewHtml: `
1795
1895
  <div class="comp-overview-section">
1796
1896
  <div class="comp-overview-label">Sizes</div>
@@ -1966,11 +2066,29 @@ var SHOWCASE_COMPONENTS = [
1966
2066
  ];
1967
2067
 
1968
2068
  // src/generators/showcase/html.ts
2069
+ function buildDensityCss() {
2070
+ const blocks = [];
2071
+ for (const preset of PRESETS) {
2072
+ const scale = SPACING_PRESETS[preset];
2073
+ const radius = RADIUS_PRESETS[preset];
2074
+ const semantic = buildSemanticSpacing(scale);
2075
+ const vars = [];
2076
+ for (const [k, v] of Object.entries(scale)) vars.push(` --spacing-${k}: ${v}px;`);
2077
+ for (const [k, v] of Object.entries(semantic)) vars.push(` --${k}: ${v}px;`);
2078
+ for (const [k, v] of Object.entries(radius))
2079
+ vars.push(` --radius-${k}: ${v === 9999 ? "9999px" : `${v}px`};`);
2080
+ blocks.push(` [data-density="${preset}"] {
2081
+ ${vars.join("\n")}
2082
+ }`);
2083
+ }
2084
+ return blocks.join("\n");
2085
+ }
1969
2086
  function generateShowcase(config, resolution) {
1970
2087
  const tokens = resolution.tokens;
1971
2088
  const name = config.meta?.name ?? "Design System";
1972
2089
  const version = config.meta?.version ?? "0.1.0";
1973
2090
  const themes = Object.keys(config.themes ?? {});
2091
+ const defaultDensity = config.meta?.preset ?? "comfortable";
1974
2092
  const foundationItems = [
1975
2093
  { id: "colors", label: "Colors" },
1976
2094
  { id: "typography", label: "Typography" },
@@ -1982,6 +2100,23 @@ function generateShowcase(config, resolution) {
1982
2100
  const componentItems = SHOWCASE_COMPONENTS.map(({ id, label }) => ({ id, label }));
1983
2101
  const allItems = [...foundationItems, ...componentItems];
1984
2102
  const isPro = isProUnlocked();
2103
+ const flatTokens = Object.fromEntries(
2104
+ Object.entries(tokens).map(([k, v]) => [
2105
+ k.replace(/^(global|semantic|component)\./, ""),
2106
+ v
2107
+ ])
2108
+ );
2109
+ const lightTheme = config.themes?.["light"] ?? {};
2110
+ const darkTheme = config.themes?.["dark"] ?? {};
2111
+ const lightCssVars = {
2112
+ ...Object.fromEntries(Object.entries(flatTokens).map(([k, v]) => [`--${k}`, v])),
2113
+ ...Object.fromEntries(Object.entries(lightTheme).map(([k, v]) => [`--${k}`, String(v)]))
2114
+ };
2115
+ const darkCssVars = {
2116
+ ...Object.fromEntries(Object.entries(flatTokens).map(([k, v]) => [`--${k}`, v])),
2117
+ ...Object.fromEntries(Object.entries(darkTheme).map(([k, v]) => [`--${k}`, String(v)]))
2118
+ };
2119
+ const resolvedCssVars = { light: lightCssVars, dark: darkCssVars };
1985
2120
  const sections = {
1986
2121
  colors: buildColorSection(config, tokens),
1987
2122
  typography: buildTypographySection(config),
@@ -1990,24 +2125,56 @@ function generateShowcase(config, resolution) {
1990
2125
  elevation: buildElevationSection(config),
1991
2126
  motion: buildMotionSection(config),
1992
2127
  ...Object.fromEntries(
1993
- SHOWCASE_COMPONENTS.map((entry) => [
1994
- entry.id,
1995
- buildComponentPage(entry.def(config, tokens), isPro)
1996
- ])
2128
+ SHOWCASE_COMPONENTS.map((entry) => {
2129
+ const defData = entry.def(config, tokens);
2130
+ const jsonDef = COMPONENT_JSON_DEFINITIONS[entry.label];
2131
+ const metaDef = COMPONENT_METADATA_DEFINITIONS[entry.label] ?? null;
2132
+ const componentJson = jsonDef ? { ...jsonDef, cssVars: resolvedCssVars } : {
2133
+ name: entry.label,
2134
+ description: entry.pageDescription,
2135
+ props: defData.props,
2136
+ examples: defData.examples.map((ex) => ({ label: ex.label, code: ex.code })),
2137
+ cssVars: resolvedCssVars
2138
+ };
2139
+ return [
2140
+ entry.id,
2141
+ buildComponentPage({
2142
+ id: entry.id,
2143
+ label: entry.label,
2144
+ description: componentJson.description,
2145
+ overviewHtml: defData.overviewHtml,
2146
+ json: componentJson,
2147
+ showcaseExamples: defData.examples,
2148
+ a11yContract: metaDef?.accessibilityContract ?? null,
2149
+ a11yItems: defData.a11y,
2150
+ metadata: isPro ? metaDef : null,
2151
+ isPro
2152
+ })
2153
+ ];
2154
+ })
1997
2155
  )
1998
2156
  };
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
2157
  const themeCssLight = Object.entries({ ...flatTokens, ...lightTheme }).map(([k, v]) => ` --${k}: ${v};`).join("\n");
2008
2158
  const themeCssDark = Object.entries({ ...flatTokens, ...darkTheme }).map(([k, v]) => ` --${k}: ${v};`).join("\n");
2159
+ const densityCss = buildDensityCss();
2160
+ const showcaseData = {
2161
+ systemName: name,
2162
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2163
+ isPro,
2164
+ components: SHOWCASE_COMPONENTS.map((entry) => {
2165
+ const jsonDef = COMPONENT_JSON_DEFINITIONS[entry.label];
2166
+ const metaDef = COMPONENT_METADATA_DEFINITIONS[entry.label] ?? null;
2167
+ return {
2168
+ json: jsonDef ? { ...jsonDef, cssVars: resolvedCssVars } : null,
2169
+ metadata: isPro ? metaDef : null
2170
+ };
2171
+ })
2172
+ };
2173
+ const dataScript = `<script>
2174
+ window.__DSFORGE__ = ${JSON.stringify(showcaseData)};
2175
+ </script>`;
2009
2176
  return `<!DOCTYPE html>
2010
- <html lang="en" data-theme="light">
2177
+ <html lang="en" data-theme="light" data-density="${defaultDensity}">
2011
2178
  <head>
2012
2179
  <meta charset="UTF-8" />
2013
2180
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -2022,6 +2189,9 @@ ${themeCssLight}
2022
2189
  ${themeCssDark}
2023
2190
  }
2024
2191
 
2192
+ /* \u2500\u2500 Density presets \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 */
2193
+ ${densityCss}
2194
+
2025
2195
  /* \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
2196
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
2027
2197
  html { font-size: 16px; }
@@ -2087,6 +2257,33 @@ ${themeCssDark}
2087
2257
  color: var(--color-text-primary, #0f172a); font-weight: 500;
2088
2258
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.06);
2089
2259
  }
2260
+ .density-toggle {
2261
+ display: flex; gap: 3px; align-items: center;
2262
+ background: var(--color-bg-subtle, #f8fafc);
2263
+ border: 1px solid var(--color-border-default, #e2e8f0);
2264
+ border-radius: 7px; padding: 3px;
2265
+ }
2266
+ .density-toggle.locked { opacity: 0.5; cursor: not-allowed; }
2267
+ .density-btn {
2268
+ padding: 3px 10px; border-radius: 4px; border: none;
2269
+ background: transparent; font-size: 12px; cursor: pointer;
2270
+ color: var(--color-text-secondary, #64748b);
2271
+ transition: background 120ms, color 120ms;
2272
+ }
2273
+ .density-btn:disabled { cursor: not-allowed; }
2274
+ .density-btn.active {
2275
+ background: var(--color-bg-default, #fff);
2276
+ color: var(--color-text-primary, #0f172a); font-weight: 500;
2277
+ box-shadow: 0 1px 2px rgb(0 0 0 / 0.06);
2278
+ }
2279
+ .density-lock {
2280
+ font-size: 10px; font-weight: 600; letter-spacing: 0.04em;
2281
+ color: var(--color-text-secondary, #64748b);
2282
+ padding: 2px 6px; border-radius: 4px;
2283
+ background: var(--color-bg-overlay, #f1f5f9);
2284
+ border: 1px solid var(--color-border-default, #e2e8f0);
2285
+ white-space: nowrap;
2286
+ }
2090
2287
  .content { padding: 36px 40px 80px; max-width: 860px; }
2091
2288
  .page { display: none; }
2092
2289
  .page.active { display: block; }
@@ -2257,6 +2454,20 @@ ${themeCssDark}
2257
2454
  }
2258
2455
 
2259
2456
  /* \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 */
2457
+ /* Requirements grid */
2458
+ .a11y-requirements { display: flex; flex-direction: column; gap: 0; border: 1px solid var(--color-border-default, #e2e8f0); border-radius: 10px; overflow: hidden; margin-bottom: 4px; }
2459
+ .a11y-req-row { padding: 14px 16px; border-bottom: 1px solid var(--color-border-default, #e2e8f0); background: var(--color-bg-default, #fff); }
2460
+ .a11y-req-row:last-child { border-bottom: none; }
2461
+ .a11y-req-meta { display: flex; align-items: center; gap: 10px; margin-bottom: 4px; flex-wrap: wrap; }
2462
+ .a11y-req-label { font-size: 13px; font-weight: 600; color: var(--color-text-primary, #0f172a); }
2463
+ .a11y-req-desc { font-size: 12px; color: var(--color-text-secondary, #64748b); line-height: 1.55; margin: 0; }
2464
+ .a11y-role-chip { font-family: monospace; font-size: 12px; background: var(--color-bg-overlay, #f1f5f9); border: 1px solid var(--color-border-default, #e2e8f0); border-radius: 4px; padding: 1px 7px; color: var(--color-action, #2563eb); }
2465
+ /* Notes */
2466
+ .a11y-note { display: flex; gap: 8px; padding: 10px 16px; border-bottom: 1px solid var(--color-border-default, #e2e8f0); background: var(--color-bg-subtle, #f8fafc); }
2467
+ .a11y-note:last-child { border-bottom: none; }
2468
+ .a11y-note-icon { font-size: 12px; color: var(--color-text-secondary, #64748b); flex-shrink: 0; padding-top: 1px; }
2469
+ .a11y-note-text { font-size: 12px; color: var(--color-text-secondary, #64748b); line-height: 1.55; margin: 0; }
2470
+ /* WCAG criteria list */
2260
2471
  .a11y-list { display: flex; flex-direction: column; gap: 1px; }
2261
2472
  .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
2473
  .a11y-header { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }
@@ -2290,21 +2501,43 @@ ${themeCssDark}
2290
2501
  .locked-hint code { background: var(--color-bg-overlay, #f1f5f9); padding: 1px 6px; border-radius: 4px; border: 1px solid var(--color-border-default, #e2e8f0); }
2291
2502
 
2292
2503
  /* \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; }
2504
+ .ds-btn { border: none; cursor: pointer; font-size: 14px; font-weight: 500; padding: var(--component-padding-sm, 8px) var(--component-padding-md, 16px); border-radius: var(--radius-md, 4px); transition: filter 120ms; }
2294
2505
  .ds-btn:hover:not(:disabled) { filter: brightness(0.92); }
2295
- .ds-field { display: flex; flex-direction: column; gap: 4px; }
2506
+ .ds-field { display: flex; flex-direction: column; gap: var(--component-padding-xs, 4px); }
2296
2507
  .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%; }
2508
+ .ds-input { border: 1.5px solid; padding: var(--component-padding-sm, 8px) var(--component-padding-sm, 12px); font-size: 14px; outline: none; border-radius: var(--radius-sm, 2px); transition: border-color 150ms, box-shadow 150ms; width: 100%; }
2298
2509
  .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; }
2510
+ .ds-card { border: 1px solid; overflow: hidden; width: 220px; border-radius: var(--radius-lg, 8px); }
2511
+ .ds-card-header { padding: var(--component-padding-sm, 12px) var(--component-padding-sm, 14px); font-size: 14px; font-weight: 600; }
2512
+ .ds-card-body { padding: var(--component-padding-sm, 12px) var(--component-padding-sm, 14px); }
2513
+ .ds-card-footer { padding: var(--component-padding-xs, 10px) var(--component-padding-sm, 14px); display: flex; justify-content: flex-end; }
2514
+
2515
+ /* \u2500\u2500 Component docs \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 */
2516
+ .component-docs { margin-top: 36px; }
2517
+ .component-description {
2518
+ font-size: 14px; color: var(--color-text-secondary, #64748b);
2519
+ line-height: 1.65; margin-bottom: 24px; max-width: 640px;
2520
+ }
2521
+ .component-docs h4 {
2522
+ font-size: 11px; font-weight: 600; text-transform: uppercase;
2523
+ letter-spacing: 0.07em; color: var(--color-text-secondary, #64748b);
2524
+ margin-bottom: 12px; padding-bottom: 8px;
2525
+ border-bottom: 1px solid var(--color-border-default, #e2e8f0);
2526
+ }
2527
+ .usage-example {
2528
+ margin: 0; padding: 16px 18px;
2529
+ background: var(--color-bg-default, #fff);
2530
+ color: var(--color-text-primary, #0f172a);
2531
+ border: 1px solid var(--color-border-default, #e2e8f0); border-radius: 8px;
2532
+ font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
2533
+ font-size: 12.5px; line-height: 1.65; overflow-x: auto; white-space: pre;
2534
+ }
2303
2535
 
2304
2536
  /* \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
2537
  @keyframes dsforge-spin { to { transform: rotate(360deg); } }
2306
2538
  @media (prefers-reduced-motion: reduce) { @keyframes dsforge-spin { to { transform: none; } } }
2307
2539
  </style>
2540
+ ${dataScript}
2308
2541
  </head>
2309
2542
  <body>
2310
2543
 
@@ -2337,6 +2570,16 @@ ${themeCssDark}
2337
2570
  ${esc(name)} / <span id="topbar-current">Colors</span>
2338
2571
  </div>
2339
2572
  <div class="topbar-actions">
2573
+ ${isPro ? `<div class="density-toggle" id="density-toggle">
2574
+ ${PRESETS.map((p) => `
2575
+ <button class="density-btn${p === defaultDensity ? " active" : ""}" onclick="setDensity('${p}', this)">${p.charAt(0).toUpperCase() + p.slice(1)}</button>
2576
+ `).join("")}
2577
+ </div>` : `<div class="density-toggle locked" title="Density switching requires dsforge Pro. Set DSFORGE_KEY to unlock.">
2578
+ ${PRESETS.map((p) => `
2579
+ <button class="density-btn${p === defaultDensity ? " active" : ""}" disabled>${p.charAt(0).toUpperCase() + p.slice(1)}</button>
2580
+ `).join("")}
2581
+ <span class="density-lock">\u2298 Pro</span>
2582
+ </div>`}
2340
2583
  ${themes.length >= 2 ? `
2341
2584
  <div class="theme-toggle">
2342
2585
  ${themes.map(
@@ -2377,6 +2620,22 @@ ${themeCssDark}
2377
2620
  btn.classList.add('active');
2378
2621
  }
2379
2622
 
2623
+ function setDensity(name, btn) {
2624
+ document.documentElement.setAttribute('data-density', name);
2625
+ document.querySelectorAll('.density-btn').forEach(b => b.classList.remove('active'));
2626
+ btn.classList.add('active');
2627
+ updateSpacingValues();
2628
+ }
2629
+
2630
+ function updateSpacingValues() {
2631
+ const style = getComputedStyle(document.documentElement);
2632
+ document.querySelectorAll('[data-spacing-var]').forEach(el => {
2633
+ const prop = el.getAttribute('data-spacing-var');
2634
+ const val = style.getPropertyValue(prop).trim();
2635
+ if (val) el.textContent = val;
2636
+ });
2637
+ }
2638
+
2380
2639
  function switchTab(compId, tabId, btn) {
2381
2640
  const tabs = document.querySelectorAll('#' + compId + '-tabs .comp-tab');
2382
2641
  const panels = document.querySelectorAll('#' + compId + '-tabs .comp-tab-panel');
@@ -2395,6 +2654,9 @@ ${themeCssDark}
2395
2654
  setTimeout(() => { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 2000);
2396
2655
  });
2397
2656
  }
2657
+
2658
+ // Populate spacing/radius value labels on load
2659
+ document.addEventListener('DOMContentLoaded', updateSpacingValues);
2398
2660
  </script>
2399
2661
  </body>
2400
2662
  </html>`;