@nghitrum/dsforge 0.1.5-alpha.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,12 @@ import {
2
2
  PRESETS,
3
3
  RADIUS_PRESETS,
4
4
  SPACING_PRESETS,
5
- buildSemanticSpacing,
6
- isProUnlocked
7
- } from "./chunk-JUMR3N5J.js";
5
+ buildSemanticSpacing
6
+ } from "./chunk-5YT3VNE6.js";
7
+ import {
8
+ COMPONENT_JSON_DEFINITIONS,
9
+ COMPONENT_METADATA_DEFINITIONS
10
+ } from "./chunk-A7VW6SII.js";
8
11
 
9
12
  // src/generators/showcase/types.ts
10
13
  function esc(s) {
@@ -211,20 +214,9 @@ function buildMotionSection(config) {
211
214
  }
212
215
 
213
216
  // src/generators/showcase/page.ts
214
- var lockedPanel = (label) => `
215
- <div class="locked-panel">
216
- <div class="locked-icon">\u2298</div>
217
- <div class="locked-title">${label} \u2014 dsforge Pro</div>
218
- <p class="locked-desc">This tab is available with a dsforge Pro license.</p>
219
- <p class="locked-hint">Set the <code>DSFORGE_KEY</code> environment variable to unlock.</p>
220
- </div>`;
221
- function buildComponentPage(def, isPro) {
222
- const tabId = (tab) => `${def.id}-tab-${tab}`;
223
- const panelId = (tab) => `${def.id}-panel-${tab}`;
224
- const overviewHtml = `
225
- <div class="comp-overview">${def.overviewHtml}</div>
226
- <p class="component-description">${esc(def.description)}</p>`;
227
- const propsTable = `
217
+ function buildPropsTable(props) {
218
+ const sorted = [...props].sort((a, b) => (b.required ? 1 : 0) - (a.required ? 1 : 0));
219
+ return `
228
220
  <table class="props-table">
229
221
  <thead>
230
222
  <tr>
@@ -236,7 +228,7 @@ function buildComponentPage(def, isPro) {
236
228
  </tr>
237
229
  </thead>
238
230
  <tbody>
239
- ${def.props.map(
231
+ ${sorted.map(
240
232
  (p) => `
241
233
  <tr>
242
234
  <td><code class="prop-name">${esc(p.name)}</code></td>
@@ -250,26 +242,83 @@ function buildComponentPage(def, isPro) {
250
242
  ).join("")}
251
243
  </tbody>
252
244
  </table>`;
253
- const examplesHtml = def.examples.map(
245
+ }
246
+ function buildExamplesHtml(id, examples) {
247
+ return examples.map(
254
248
  (ex, i) => `
255
249
  <div class="example-block">
256
250
  <div class="example-header">
257
251
  <div class="example-label">${esc(ex.label)}</div>
258
- <div class="example-desc">${esc(ex.description)}</div>
252
+ ${ex.description ? `<div class="example-desc">${esc(ex.description)}</div>` : ""}
259
253
  </div>
260
- <div class="example-preview">${ex.previewHtml}</div>
254
+ ${ex.previewHtml ? `<div class="example-preview">${ex.previewHtml}</div>` : ""}
261
255
  <div class="example-code-wrap">
262
256
  <div class="example-code-bar">
263
257
  <span>TSX</span>
264
- <button class="copy-btn" onclick="copyCode('${def.id}-ex-${i}', this)">Copy</button>
258
+ <button class="copy-btn" onclick="copyCode('${id}-ex-${i}', this)">Copy</button>
265
259
  </div>
266
- <pre class="example-code" id="${def.id}-ex-${i}">${esc(ex.code)}</pre>
260
+ <pre class="example-code" id="${id}-ex-${i}">${esc(ex.code)}</pre>
267
261
  </div>
268
262
  </div>`
269
263
  ).join("");
270
- const a11yHtml = `
264
+ }
265
+ function requirementBadge(val) {
266
+ if (typeof val === "boolean") {
267
+ 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>';
268
+ }
269
+ return `<span class="a11y-badge a11y-badge-aa">${esc(val)}</span>`;
270
+ }
271
+ function buildA11yHtml(contract, wcagItems) {
272
+ const requirements = [
273
+ {
274
+ label: "Keyboard operable",
275
+ value: contract.keyboard,
276
+ desc: "Component can be fully operated with keyboard alone. No mouse required."
277
+ },
278
+ {
279
+ label: "Focus ring",
280
+ value: contract.focusRing,
281
+ desc: "Visible focus indicator requirement when the element receives keyboard focus."
282
+ },
283
+ {
284
+ label: "aria-label",
285
+ value: contract.ariaLabel,
286
+ desc: "When a programmatic accessible label must be provided by the consumer."
287
+ }
288
+ ];
289
+ const requirementsHtml = `
290
+ <div class="a11y-requirements">
291
+ ${requirements.map(
292
+ (r) => `
293
+ <div class="a11y-req-row">
294
+ <div class="a11y-req-meta">
295
+ <span class="a11y-req-label">${esc(r.label)}</span>
296
+ ${requirementBadge(r.value)}
297
+ </div>
298
+ <p class="a11y-req-desc">${esc(r.desc)}</p>
299
+ </div>`
300
+ ).join("")}
301
+ ${contract.roles.length > 0 ? `<div class="a11y-req-row">
302
+ <div class="a11y-req-meta">
303
+ <span class="a11y-req-label">ARIA roles</span>
304
+ <span style="display:flex;gap:4px;flex-wrap:wrap">
305
+ ${contract.roles.map((r) => `<code class="a11y-role-chip">${esc(r)}</code>`).join("")}
306
+ </span>
307
+ </div>
308
+ <p class="a11y-req-desc">The semantic roles this component exposes to assistive technology.</p>
309
+ </div>` : ""}
310
+ ${contract.notes.map(
311
+ (note) => `
312
+ <div class="a11y-note">
313
+ <span class="a11y-note-icon">\u21B3</span>
314
+ <p class="a11y-note-text">${esc(note)}</p>
315
+ </div>`
316
+ ).join("")}
317
+ </div>`;
318
+ const wcagHtml = wcagItems.length > 0 ? `
319
+ <div class="group-title" style="margin-top:32px">WCAG Criteria</div>
271
320
  <div class="a11y-list">
272
- ${def.a11y.map(
321
+ ${wcagItems.map(
273
322
  (item) => `
274
323
  <div class="a11y-item">
275
324
  <div class="a11y-header">
@@ -279,57 +328,71 @@ function buildComponentPage(def, isPro) {
279
328
  <p class="a11y-desc">${esc(item.description)}</p>
280
329
  </div>`
281
330
  ).join("")}
282
- </div>`;
283
- const aiJson = JSON.stringify(def.aiMeta, null, 2);
284
- const aiHtml = `
331
+ </div>` : "";
332
+ return `
333
+ <div class="group-title">Requirements</div>
334
+ ${requirementsHtml}
335
+ ${wcagHtml}`;
336
+ }
337
+ function buildAiMetaHtml(id, meta) {
338
+ const metaJson = JSON.stringify(meta, null, 2);
339
+ return `
285
340
  <div class="ai-meta-intro">
286
- <p>This JSON contract is emitted to <code>dist-ds/metadata/${def.id}.json</code>.
341
+ <p>This JSON contract is emitted to <code>components/${esc(meta.name)}/${esc(meta.name)}.metadata.json</code>.
287
342
  AI coding assistants use it to understand when and how to use this component correctly.</p>
288
343
  </div>
289
344
  <div class="example-code-wrap" style="margin-top:16px">
290
345
  <div class="example-code-bar">
291
346
  <span>JSON</span>
292
- <button class="copy-btn" onclick="copyCode('${def.id}-ai-meta', this)">Copy</button>
347
+ <button class="copy-btn" onclick="copyCode('${id}-ai-meta', this)">Copy</button>
293
348
  </div>
294
- <pre class="example-code" id="${def.id}-ai-meta">${esc(aiJson)}</pre>
349
+ <pre class="example-code" id="${id}-ai-meta">${esc(metaJson)}</pre>
295
350
  </div>
296
351
  <div class="ai-guidance">
297
352
  <div class="group-title" style="margin-top:24px">AI usage guidance</div>
298
353
  <ul class="ai-guidance-list">
299
- ${def.aiMeta.aiGuidance.map((g) => `<li>${esc(g)}</li>`).join("")}
354
+ ${meta.aiGuidance.map((g) => `<li>${esc(g)}</li>`).join("")}
300
355
  </ul>
301
356
  </div>`;
357
+ }
358
+ function buildComponentPage(input) {
359
+ const { id, description, overviewHtml, json, showcaseExamples, a11yContract, a11yItems, metadata } = input;
360
+ const tabId = (tab) => `${id}-tab-${tab}`;
361
+ const panelId = (tab) => `${id}-panel-${tab}`;
362
+ const overviewContent = `
363
+ <div class="comp-overview">${overviewHtml}</div>
364
+ <p class="component-description">${esc(description)}</p>`;
365
+ const propsContent = buildPropsTable(json.props);
366
+ const examplesContent = buildExamplesHtml(id, showcaseExamples);
367
+ const a11yContent = a11yContract ? buildA11yHtml(a11yContract, a11yItems) : `<div class="a11y-list">${a11yItems.map((item) => `
368
+ <div class="a11y-item">
369
+ <div class="a11y-header">
370
+ <span class="a11y-criterion">${esc(item.criterion)}</span>
371
+ <span class="a11y-badge a11y-badge-${item.level.toLowerCase()}">WCAG ${item.level}</span>
372
+ </div>
373
+ <p class="a11y-desc">${esc(item.description)}</p>
374
+ </div>`).join("")}</div>`;
375
+ const aiContent = metadata ? buildAiMetaHtml(id, metadata) : "";
302
376
  const tabs = [
303
- { id: "overview", label: "Overview", content: overviewHtml, locked: false },
304
- { id: "props", label: "Props", content: propsTable, locked: false },
305
- { id: "examples", label: "Examples", content: examplesHtml, locked: false },
306
- {
307
- id: "accessibility",
308
- label: "Accessibility",
309
- content: isPro ? a11yHtml : lockedPanel("Accessibility"),
310
- locked: !isPro
311
- },
312
- {
313
- id: "ai-metadata",
314
- label: "AI Metadata",
315
- content: isPro ? aiHtml : lockedPanel("AI Metadata"),
316
- locked: !isPro
317
- }
377
+ { id: "overview", label: "Overview", content: overviewContent },
378
+ { id: "props", label: "Props", content: propsContent },
379
+ { id: "examples", label: "Examples", content: examplesContent },
380
+ { id: "accessibility", label: "Accessibility", content: a11yContent },
381
+ { id: "ai-metadata", label: "AI Metadata", content: aiContent }
318
382
  ];
319
383
  return `
320
- <div class="comp-tabs" id="${def.id}-tabs">
384
+ <div class="comp-tabs" id="${id}-tabs">
321
385
  <div class="comp-tab-bar" role="tablist">
322
386
  ${tabs.map(
323
387
  (t, i) => `
324
388
  <button
325
- class="comp-tab${i === 0 ? " active" : ""}${t.locked ? " locked" : ""}"
389
+ class="comp-tab${i === 0 ? " active" : ""}"
326
390
  id="${tabId(t.id)}"
327
391
  role="tab"
328
392
  aria-selected="${i === 0}"
329
393
  aria-controls="${panelId(t.id)}"
330
- onclick="${t.locked ? "return false" : `switchTab('${def.id}', '${t.id}', this)`}"
331
- ${t.locked ? 'title="Unlock with dsforge Pro"' : ""}
332
- >${esc(t.label)}${t.locked ? " &#x1F512;" : ""}</button>`
394
+ onclick="switchTab('${id}', '${t.id}', this)"
395
+ >${esc(t.label)}</button>`
333
396
  ).join("")}
334
397
  </div>
335
398
  ${tabs.map(
@@ -907,7 +970,7 @@ function cardDef(config, tokens) {
907
970
  // src/generators/showcase/components/badge.ts
908
971
  function badgeDef(config, tokens) {
909
972
  const { ff } = componentTokens(config, tokens);
910
- 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>`;
973
+ 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>`;
911
974
  const variants = [
912
975
  { label: "Default", bg: "#f1f5f9", color: "#6b7280" },
913
976
  { label: "Success", bg: "#dcfce7", color: "#16a34a" },
@@ -931,10 +994,10 @@ function badgeDef(config, tokens) {
931
994
  <div class="comp-overview-section">
932
995
  <div class="comp-overview-label">Sizes</div>
933
996
  <div class="comp-preview-row" style="align-items:center">
934
- <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>
935
- <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>
936
- <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>
937
- <span style="display:inline-flex;width:8px;height:8px;border-radius:50%;background:#16a34a" title="Dot mode"></span>
997
+ <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>
998
+ <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>
999
+ <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>
1000
+ <span style="display:inline-flex;width:var(--spacing-2,8px);height:var(--spacing-2,8px);border-radius:50%;background:#16a34a" title="Dot mode"></span>
938
1001
  </div>
939
1002
  </div>`,
940
1003
  props: [
@@ -1055,11 +1118,11 @@ function checkboxDef(config, tokens) {
1055
1118
  const fill = checked || indeterminate ? "#2563eb" : C.bg;
1056
1119
  const borderColor = checked || indeterminate ? "#2563eb" : C.border;
1057
1120
  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>` : "";
1058
- 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>`;
1121
+ 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>`;
1059
1122
  };
1060
- const checkboxHtml = (label, checked, opts = "", helper = "") => `<label style="display:inline-flex;align-items:flex-start;gap:8px;cursor:pointer;font-family:${esc(ff)};${opts}">
1123
+ 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}">
1061
1124
  ${boxHtml(checked)}
1062
- <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>
1125
+ <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>
1063
1126
  </label>`;
1064
1127
  return {
1065
1128
  id: "checkbox",
@@ -1076,9 +1139,9 @@ function checkboxDef(config, tokens) {
1076
1139
  <div class="comp-preview-col">
1077
1140
  ${checkboxHtml("Unchecked", false)}
1078
1141
  ${checkboxHtml("Checked", true)}
1079
- <label style="display:inline-flex;align-items:flex-start;gap:8px;cursor:pointer;font-family:${esc(ff)}">
1142
+ <label style="display:inline-flex;align-items:flex-start;gap:var(--spacing-2,8px);cursor:pointer;font-family:${esc(ff)}">
1080
1143
  ${boxHtml(false, true)}
1081
- <span style="font-size:14px;color:${C.text}">Indeterminate</span>
1144
+ <span style="font-size:var(--font-size-small,0.875rem);color:${C.text}">Indeterminate</span>
1082
1145
  </label>
1083
1146
  ${checkboxHtml("Disabled", false, "opacity:0.4;cursor:not-allowed")}
1084
1147
  </div>
@@ -1161,9 +1224,9 @@ function checkboxDef(config, tokens) {
1161
1224
  label="Select all (3 of 5)"
1162
1225
  indeterminate
1163
1226
  />`,
1164
- previewHtml: `<label style="display:inline-flex;align-items:center;gap:8px;cursor:pointer;font-family:${esc(ff)}">
1227
+ previewHtml: `<label style="display:inline-flex;align-items:center;gap:var(--spacing-2,8px);cursor:pointer;font-family:${esc(ff)}">
1165
1228
  ${boxHtml(false, true)}
1166
- <span style="font-size:14px;color:${C.text}">Select all (3 of 5)</span>
1229
+ <span style="font-size:var(--font-size-small,0.875rem);color:${C.text}">Select all (3 of 5)</span>
1167
1230
  </label>`
1168
1231
  },
1169
1232
  {
@@ -1240,13 +1303,13 @@ function radioDef(config, tokens) {
1240
1303
  };
1241
1304
  const circleHtml = (selected) => {
1242
1305
  const borderColor = selected ? C.action : C.border;
1243
- 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">
1244
- ${selected ? `<span style="width:8px;height:8px;border-radius:50%;background:${C.action}"></span>` : ""}
1306
+ 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">
1307
+ ${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>` : ""}
1245
1308
  </span>`;
1246
1309
  };
1247
- const radioHtml = (label, selected, opts = "") => `<label style="display:inline-flex;align-items:center;gap:8px;cursor:pointer;font-family:${esc(ff)};${opts}">
1310
+ 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}">
1248
1311
  ${circleHtml(selected)}
1249
- <span style="font-size:14px;color:${C.text}">${label}</span>
1312
+ <span style="font-size:var(--font-size-small,0.875rem);color:${C.text}">${label}</span>
1250
1313
  </label>`;
1251
1314
  return {
1252
1315
  id: "radio",
@@ -1259,7 +1322,7 @@ function radioDef(config, tokens) {
1259
1322
  <div class="comp-overview-label">RadioGroup (vertical)</div>
1260
1323
  <div class="comp-preview-col">
1261
1324
  <fieldset style="border:none;padding:0;margin:0;font-family:${esc(ff)}">
1262
- <legend style="font-size:13px;font-weight:600;color:${C.text};margin-bottom:8px">Notification preference</legend>
1325
+ <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>
1263
1326
  <div class="comp-preview-col">
1264
1327
  ${radioHtml("Email", true)}
1265
1328
  ${radioHtml("SMS", false)}
@@ -1342,8 +1405,8 @@ function radioDef(config, tokens) {
1342
1405
  <Radio value="annual" label="Annual (save 20%)" />
1343
1406
  </RadioGroup>`,
1344
1407
  previewHtml: `<fieldset style="border:none;padding:0;margin:0;font-family:${esc(ff)}">
1345
- <legend style="font-size:13px;font-weight:600;color:${C.text};margin-bottom:8px">Billing cycle</legend>
1346
- <div style="display:flex;flex-direction:column;gap:8px">
1408
+ <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>
1409
+ <div style="display:flex;flex-direction:column;gap:var(--spacing-2,8px)">
1347
1410
  ${radioHtml("Monthly", true)}
1348
1411
  ${radioHtml("Annual (save 20%)", false)}
1349
1412
  </div>
@@ -1358,8 +1421,8 @@ function radioDef(config, tokens) {
1358
1421
  <Radio value="lg" label="L" />
1359
1422
  </RadioGroup>`,
1360
1423
  previewHtml: `<fieldset style="border:none;padding:0;margin:0;font-family:${esc(ff)}">
1361
- <legend style="font-size:13px;font-weight:600;color:${C.text};margin-bottom:8px">Size</legend>
1362
- <div style="display:flex;gap:16px">
1424
+ <legend style="font-size:var(--font-size-small,0.875rem);font-weight:600;color:${C.text};margin-bottom:var(--spacing-2,8px)">Size</legend>
1425
+ <div style="display:flex;gap:var(--spacing-4,16px)">
1363
1426
  ${radioHtml("S", false)}
1364
1427
  ${radioHtml("M", true)}
1365
1428
  ${radioHtml("L", false)}
@@ -1620,11 +1683,11 @@ function toastDef(config, tokens) {
1620
1683
  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>`,
1621
1684
  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>`
1622
1685
  };
1623
- 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">
1686
+ 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">
1624
1687
  <span style="color:${v.icon};flex-shrink:0;margin-top:1px">${icons[v.name]}</span>
1625
1688
  <div>
1626
- <p style="margin:0;font-size:13px;font-weight:600;color:${v.icon}">${title}</p>
1627
- <p style="margin:4px 0 0;font-size:13px;color:var(--color-text-secondary,#6b7280)">${body}</p>
1689
+ <p style="margin:0;font-size:var(--font-size-small,0.875rem);font-weight:600;color:${v.icon}">${title}</p>
1690
+ <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>
1628
1691
  </div>
1629
1692
  </div>`;
1630
1693
  return {
@@ -1712,11 +1775,11 @@ function toastDef(config, tokens) {
1712
1775
  >
1713
1776
  Upgrade to continue using all features.
1714
1777
  </Alert>`,
1715
- 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">
1778
+ 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">
1716
1779
  <span style="color:${vWarning.icon};flex-shrink:0;margin-top:1px">${icons["warning"]}</span>
1717
1780
  <div style="flex:1">
1718
- <p style="margin:0;font-size:13px;font-weight:600;color:${vWarning.icon}">Your trial expires in 3 days</p>
1719
- <p style="margin:4px 0 0;font-size:13px;color:var(--color-text-secondary,#6b7280)">Upgrade to continue using all features.</p>
1781
+ <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>
1782
+ <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>
1720
1783
  </div>
1721
1784
  <button style="background:transparent;border:none;cursor:pointer;color:var(--color-text-secondary,#6b7280);padding:2px;flex-shrink:0" aria-label="Dismiss">\u2715</button>
1722
1785
  </div>`
@@ -1740,11 +1803,11 @@ toast.add({
1740
1803
  });`,
1741
1804
  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)}">
1742
1805
  <p style="font-size:12px;color:var(--color-text-secondary,#6b7280);margin:0 0 12px">Bottom-right overlay (fixed position)</p>
1743
- <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)">
1806
+ <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)">
1744
1807
  <span style="color:var(--color-success,#16a34a)">${icons["success"]}</span>
1745
1808
  <div>
1746
- <p style="margin:0;font-size:13px;font-weight:600;color:var(--color-text-primary,#0f172a)">Saved</p>
1747
- <p style="margin:4px 0 0;font-size:13px;color:var(--color-text-secondary,#6b7280)">Your changes have been saved.</p>
1809
+ <p style="margin:0;font-size:var(--font-size-small,0.875rem);font-weight:600;color:var(--color-text-primary,#0f172a)">Saved</p>
1810
+ <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>
1748
1811
  </div>
1749
1812
  <button style="background:transparent;border:none;cursor:pointer;color:var(--color-text-secondary,#6b7280);padding:2px" aria-label="Dismiss">\u2715</button>
1750
1813
  </div>
@@ -2027,7 +2090,23 @@ function generateShowcase(config, resolution) {
2027
2090
  ];
2028
2091
  const componentItems = SHOWCASE_COMPONENTS.map(({ id, label }) => ({ id, label }));
2029
2092
  const allItems = [...foundationItems, ...componentItems];
2030
- const isPro = isProUnlocked();
2093
+ const flatTokens = Object.fromEntries(
2094
+ Object.entries(tokens).map(([k, v]) => [
2095
+ k.replace(/^(global|semantic|component)\./, ""),
2096
+ v
2097
+ ])
2098
+ );
2099
+ const lightTheme = config.themes?.["light"] ?? {};
2100
+ const darkTheme = config.themes?.["dark"] ?? {};
2101
+ const lightCssVars = {
2102
+ ...Object.fromEntries(Object.entries(flatTokens).map(([k, v]) => [`--${k}`, v])),
2103
+ ...Object.fromEntries(Object.entries(lightTheme).map(([k, v]) => [`--${k}`, String(v)]))
2104
+ };
2105
+ const darkCssVars = {
2106
+ ...Object.fromEntries(Object.entries(flatTokens).map(([k, v]) => [`--${k}`, v])),
2107
+ ...Object.fromEntries(Object.entries(darkTheme).map(([k, v]) => [`--${k}`, String(v)]))
2108
+ };
2109
+ const resolvedCssVars = { light: lightCssVars, dark: darkCssVars };
2031
2110
  const sections = {
2032
2111
  colors: buildColorSection(config, tokens),
2033
2112
  typography: buildTypographySection(config),
@@ -2036,23 +2115,52 @@ function generateShowcase(config, resolution) {
2036
2115
  elevation: buildElevationSection(config),
2037
2116
  motion: buildMotionSection(config),
2038
2117
  ...Object.fromEntries(
2039
- SHOWCASE_COMPONENTS.map((entry) => [
2040
- entry.id,
2041
- buildComponentPage(entry.def(config, tokens), isPro)
2042
- ])
2118
+ SHOWCASE_COMPONENTS.map((entry) => {
2119
+ const defData = entry.def(config, tokens);
2120
+ const jsonDef = COMPONENT_JSON_DEFINITIONS[entry.label];
2121
+ const metaDef = COMPONENT_METADATA_DEFINITIONS[entry.label] ?? null;
2122
+ const componentJson = jsonDef ? { ...jsonDef, cssVars: resolvedCssVars } : {
2123
+ name: entry.label,
2124
+ description: entry.pageDescription,
2125
+ props: defData.props,
2126
+ examples: defData.examples.map((ex) => ({ label: ex.label, code: ex.code })),
2127
+ cssVars: resolvedCssVars
2128
+ };
2129
+ return [
2130
+ entry.id,
2131
+ buildComponentPage({
2132
+ id: entry.id,
2133
+ label: entry.label,
2134
+ description: componentJson.description,
2135
+ overviewHtml: defData.overviewHtml,
2136
+ json: componentJson,
2137
+ showcaseExamples: defData.examples,
2138
+ a11yContract: metaDef?.accessibilityContract ?? null,
2139
+ a11yItems: defData.a11y,
2140
+ metadata: metaDef
2141
+ })
2142
+ ];
2143
+ })
2043
2144
  )
2044
2145
  };
2045
- const flatTokens = Object.fromEntries(
2046
- Object.entries(tokens).map(([k, v]) => [
2047
- k.replace(/^(global|semantic|component)\./, ""),
2048
- v
2049
- ])
2050
- );
2051
- const lightTheme = config.themes?.["light"] ?? {};
2052
- const darkTheme = config.themes?.["dark"] ?? {};
2053
2146
  const themeCssLight = Object.entries({ ...flatTokens, ...lightTheme }).map(([k, v]) => ` --${k}: ${v};`).join("\n");
2054
2147
  const themeCssDark = Object.entries({ ...flatTokens, ...darkTheme }).map(([k, v]) => ` --${k}: ${v};`).join("\n");
2055
2148
  const densityCss = buildDensityCss();
2149
+ const showcaseData = {
2150
+ systemName: name,
2151
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2152
+ components: SHOWCASE_COMPONENTS.map((entry) => {
2153
+ const jsonDef = COMPONENT_JSON_DEFINITIONS[entry.label];
2154
+ const metaDef = COMPONENT_METADATA_DEFINITIONS[entry.label] ?? null;
2155
+ return {
2156
+ json: jsonDef ? { ...jsonDef, cssVars: resolvedCssVars } : null,
2157
+ metadata: metaDef
2158
+ };
2159
+ })
2160
+ };
2161
+ const dataScript = `<script>
2162
+ window.__DSFORGE__ = ${JSON.stringify(showcaseData)};
2163
+ </script>`;
2056
2164
  return `<!DOCTYPE html>
2057
2165
  <html lang="en" data-theme="light" data-density="${defaultDensity}">
2058
2166
  <head>
@@ -2143,27 +2251,17 @@ ${densityCss}
2143
2251
  border: 1px solid var(--color-border-default, #e2e8f0);
2144
2252
  border-radius: 7px; padding: 3px;
2145
2253
  }
2146
- .density-toggle.locked { opacity: 0.5; cursor: not-allowed; }
2147
2254
  .density-btn {
2148
2255
  padding: 3px 10px; border-radius: 4px; border: none;
2149
2256
  background: transparent; font-size: 12px; cursor: pointer;
2150
2257
  color: var(--color-text-secondary, #64748b);
2151
2258
  transition: background 120ms, color 120ms;
2152
2259
  }
2153
- .density-btn:disabled { cursor: not-allowed; }
2154
2260
  .density-btn.active {
2155
2261
  background: var(--color-bg-default, #fff);
2156
2262
  color: var(--color-text-primary, #0f172a); font-weight: 500;
2157
2263
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.06);
2158
2264
  }
2159
- .density-lock {
2160
- font-size: 10px; font-weight: 600; letter-spacing: 0.04em;
2161
- color: var(--color-text-secondary, #64748b);
2162
- padding: 2px 6px; border-radius: 4px;
2163
- background: var(--color-bg-overlay, #f1f5f9);
2164
- border: 1px solid var(--color-border-default, #e2e8f0);
2165
- white-space: nowrap;
2166
- }
2167
2265
  .content { padding: 36px 40px 80px; max-width: 860px; }
2168
2266
  .page { display: none; }
2169
2267
  .page.active { display: block; }
@@ -2334,6 +2432,20 @@ ${densityCss}
2334
2432
  }
2335
2433
 
2336
2434
  /* \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 */
2435
+ /* Requirements grid */
2436
+ .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; }
2437
+ .a11y-req-row { padding: 14px 16px; border-bottom: 1px solid var(--color-border-default, #e2e8f0); background: var(--color-bg-default, #fff); }
2438
+ .a11y-req-row:last-child { border-bottom: none; }
2439
+ .a11y-req-meta { display: flex; align-items: center; gap: 10px; margin-bottom: 4px; flex-wrap: wrap; }
2440
+ .a11y-req-label { font-size: 13px; font-weight: 600; color: var(--color-text-primary, #0f172a); }
2441
+ .a11y-req-desc { font-size: 12px; color: var(--color-text-secondary, #64748b); line-height: 1.55; margin: 0; }
2442
+ .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); }
2443
+ /* Notes */
2444
+ .a11y-note { display: flex; gap: 8px; padding: 10px 16px; border-bottom: 1px solid var(--color-border-default, #e2e8f0); background: var(--color-bg-subtle, #f8fafc); }
2445
+ .a11y-note:last-child { border-bottom: none; }
2446
+ .a11y-note-icon { font-size: 12px; color: var(--color-text-secondary, #64748b); flex-shrink: 0; padding-top: 1px; }
2447
+ .a11y-note-text { font-size: 12px; color: var(--color-text-secondary, #64748b); line-height: 1.55; margin: 0; }
2448
+ /* WCAG criteria list */
2337
2449
  .a11y-list { display: flex; flex-direction: column; gap: 1px; }
2338
2450
  .a11y-item { padding: 16px; border: 1px solid var(--color-border-default, #e2e8f0); border-radius: 8px; margin-bottom: 10px; background: var(--color-bg-default, #fff); }
2339
2451
  .a11y-header { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }
@@ -2351,21 +2463,6 @@ ${densityCss}
2351
2463
  .ai-guidance-list { padding-left: 20px; display: flex; flex-direction: column; gap: 8px; margin-top: 10px; }
2352
2464
  .ai-guidance-list li { font-size: 13px; color: var(--color-text-secondary, #64748b); line-height: 1.6; }
2353
2465
 
2354
- /* \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 */
2355
- .comp-tab.locked { opacity: 0.45; cursor: default; }
2356
- .comp-tab.locked:hover { color: var(--color-text-secondary, #64748b); }
2357
- .locked-panel {
2358
- display: flex; flex-direction: column; align-items: center; justify-content: center;
2359
- padding: 64px 32px; text-align: center;
2360
- border: 1px dashed var(--color-border-default, #e2e8f0); border-radius: 10px;
2361
- background: var(--color-bg-subtle, #f8fafc);
2362
- }
2363
- .locked-icon { font-size: 28px; color: var(--color-text-secondary, #64748b); margin-bottom: 12px; }
2364
- .locked-title { font-size: 14px; font-weight: 600; color: var(--color-text-primary, #0f172a); margin-bottom: 8px; }
2365
- .locked-desc { font-size: 13px; color: var(--color-text-secondary, #64748b); max-width: 360px; line-height: 1.6; margin-bottom: 6px; }
2366
- .locked-hint { font-size: 12px; color: var(--color-text-secondary, #64748b); font-family: monospace; }
2367
- .locked-hint code { background: var(--color-bg-overlay, #f1f5f9); padding: 1px 6px; border-radius: 4px; border: 1px solid var(--color-border-default, #e2e8f0); }
2368
-
2369
2466
  /* \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 */
2370
2467
  .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; }
2371
2468
  .ds-btn:hover:not(:disabled) { filter: brightness(0.92); }
@@ -2403,6 +2500,7 @@ ${densityCss}
2403
2500
  @keyframes dsforge-spin { to { transform: rotate(360deg); } }
2404
2501
  @media (prefers-reduced-motion: reduce) { @keyframes dsforge-spin { to { transform: none; } } }
2405
2502
  </style>
2503
+ ${dataScript}
2406
2504
  </head>
2407
2505
  <body>
2408
2506
 
@@ -2435,16 +2533,11 @@ ${densityCss}
2435
2533
  ${esc(name)} / <span id="topbar-current">Colors</span>
2436
2534
  </div>
2437
2535
  <div class="topbar-actions">
2438
- ${isPro ? `<div class="density-toggle" id="density-toggle">
2439
- ${PRESETS.map((p) => `
2440
- <button class="density-btn${p === defaultDensity ? " active" : ""}" onclick="setDensity('${p}', this)">${p.charAt(0).toUpperCase() + p.slice(1)}</button>
2441
- `).join("")}
2442
- </div>` : `<div class="density-toggle locked" title="Density switching requires dsforge Pro. Set DSFORGE_KEY to unlock.">
2443
- ${PRESETS.map((p) => `
2444
- <button class="density-btn${p === defaultDensity ? " active" : ""}" disabled>${p.charAt(0).toUpperCase() + p.slice(1)}</button>
2445
- `).join("")}
2446
- <span class="density-lock">\u2298 Pro</span>
2447
- </div>`}
2536
+ <div class="density-toggle" id="density-toggle">
2537
+ ${PRESETS.map((p) => `
2538
+ <button class="density-btn${p === defaultDensity ? " active" : ""}" onclick="setDensity('${p}', this)">${p.charAt(0).toUpperCase() + p.slice(1)}</button>
2539
+ `).join("")}
2540
+ </div>
2448
2541
  ${themes.length >= 2 ? `
2449
2542
  <div class="theme-toggle">
2450
2543
  ${themes.map(