@nghitrum/dsforge 0.1.5-alpha.9 → 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.
@@ -0,0 +1,70 @@
1
+ // src/adapters/react/generateAiFolder.ts
2
+ function generateSystemPrompt(systemName, tokens, componentNames) {
3
+ const tokenLines = Object.entries(tokens).map(([cssVar, values]) => `${cssVar.padEnd(35)} ${values.light} (dark: ${values.dark})`).join("\n");
4
+ const pkgName = systemName.toLowerCase().replace(/\s+/g, "-");
5
+ return `# ${systemName} \u2014 AI Context
6
+
7
+ You are helping a developer build UI using the ${systemName} design system, generated by dsforge.
8
+
9
+ ## Rules
10
+ - Never hardcode colours, spacing, or typography values
11
+ - Always use CSS custom properties from this design system
12
+ - Always import components from '${pkgName}'
13
+ - Never install or suggest third-party component libraries
14
+
15
+ ## Tokens
16
+ ${tokenLines}
17
+
18
+ ## Available Components
19
+ ${componentNames.join(", ")}
20
+
21
+ ## Usage
22
+ import { ${componentNames.slice(0, 3).join(", ")} } from '${pkgName}'
23
+ import '${pkgName}/tokens/base.css'
24
+ `;
25
+ }
26
+ function generateComponentsJson(systemName, components, metadatas) {
27
+ const metaMap = Object.fromEntries(metadatas.map((m) => [m.name, m]));
28
+ const pkgName = systemName.toLowerCase().replace(/\s+/g, "-");
29
+ const combined = components.map((c) => ({
30
+ name: c.name,
31
+ import: `import { ${c.name} } from '${pkgName}'`,
32
+ description: c.description,
33
+ props: c.props,
34
+ examples: c.examples.slice(0, 2),
35
+ tokens: Object.keys(c.cssVars.light),
36
+ aiGuidance: metaMap[c.name]?.aiGuidance ?? []
37
+ }));
38
+ return JSON.stringify({ system: systemName, components: combined }, null, 2);
39
+ }
40
+ function generateCursorContext(systemName) {
41
+ return `# Design System Context for Cursor
42
+
43
+ This project uses the ${systemName} design system. When generating any UI code:
44
+
45
+ 1. Import components from '${systemName.toLowerCase().replace(/\s+/g, "-")}'
46
+ 2. Use CSS variables from tokens/base.css for all visual values
47
+ 3. Refer to ai/components.json for the full component API and usage rules
48
+
49
+ @file ../ai/system-prompt.md
50
+ @file ../ai/components.json
51
+ `;
52
+ }
53
+ function generateCopilotInstructions(systemName) {
54
+ const pkgName = systemName.toLowerCase().replace(/\s+/g, "-");
55
+ return `# GitHub Copilot Instructions
56
+
57
+ This project uses the ${systemName} design system.
58
+
59
+ - Always import components from '${pkgName}'
60
+ - Never hardcode colour, spacing, or typography values \u2014 use CSS custom properties
61
+ - Refer to ai/components.json for component APIs, props, and usage guidance
62
+ - Refer to ai/system-prompt.md for design system rules
63
+ `;
64
+ }
65
+ export {
66
+ generateComponentsJson,
67
+ generateCopilotInstructions,
68
+ generateCursorContext,
69
+ generateSystemPrompt
70
+ };
@@ -0,0 +1,16 @@
1
+ import {
2
+ COMPONENT_JSON_DEFINITIONS
3
+ } from "./chunk-A7VW6SII.js";
4
+
5
+ // src/adapters/react/generateComponentJson.ts
6
+ function generateComponentJson(name, resolvedCssVars) {
7
+ const definition = COMPONENT_JSON_DEFINITIONS[name];
8
+ if (!definition) throw new Error(`No JSON definition found for component: ${name}`);
9
+ return {
10
+ ...definition,
11
+ cssVars: resolvedCssVars
12
+ };
13
+ }
14
+ export {
15
+ generateComponentJson
16
+ };
@@ -0,0 +1,13 @@
1
+ import {
2
+ COMPONENT_METADATA_DEFINITIONS
3
+ } from "./chunk-A7VW6SII.js";
4
+
5
+ // src/adapters/react/generateComponentMetadata.ts
6
+ function generateComponentMetadata(name) {
7
+ const definition = COMPONENT_METADATA_DEFINITIONS[name];
8
+ if (!definition) throw new Error(`No metadata definition found for component: ${name}`);
9
+ return definition;
10
+ }
11
+ export {
12
+ generateComponentMetadata
13
+ };
@@ -0,0 +1,19 @@
1
+ // src/adapters/react/generateRegistry.ts
2
+ function generateRegistry(systemName, version, components) {
3
+ return {
4
+ $schema: "https://ui.shadcn.com/schema/registry.json",
5
+ name: systemName,
6
+ version,
7
+ description: `Design system generated by dsforge`,
8
+ items: components.map((c) => ({
9
+ name: c.name.toLowerCase(),
10
+ type: "registry:ui",
11
+ description: c.description,
12
+ files: [{ path: `components/${c.name}/${c.name}.tsx`, type: "registry:component" }],
13
+ cssVars: c.cssVars
14
+ }))
15
+ };
16
+ }
17
+ export {
18
+ generateRegistry
19
+ };
@@ -2,9 +2,12 @@ import {
2
2
  PRESETS,
3
3
  RADIUS_PRESETS,
4
4
  SPACING_PRESETS,
5
- buildSemanticSpacing,
6
- isProUnlocked
7
- } from "./chunk-YUPXTQZ5.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(
@@ -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(