@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.
- package/README.md +22 -29
- package/dist/chunk-A7VW6SII.js +436 -0
- package/dist/chunk-YUPXTQZ5.js +118 -0
- package/dist/{chunk-RI3XDGKU.js → chunk-ZZRPNO6Z.js} +12 -17
- package/dist/cli/index.js +292 -310
- package/dist/componentDefinitions-5LFCNFQY.js +8 -0
- package/dist/{emitter-ZNRPJ4D6.js → emitter-IC77G4QF.js} +1 -1
- package/dist/generateAiFolder-3OOFWBH7.js +70 -0
- package/dist/generateComponentJson-XBEUWCW6.js +16 -0
- package/dist/generateComponentMetadata-2L5VNERD.js +13 -0
- package/dist/generateRegistry-3MEZDJAJ.js +19 -0
- package/dist/{html-6SIG34W5.js → html-XGJ22SXB.js} +385 -123
- package/dist/index.d.ts +12 -0
- package/dist/index.js +150 -46
- package/package.json +1 -1
|
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -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
|
|
121
|
-
|
|
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
|
|
119
|
+
<div class="spacing-bar" style="width:min(calc(var(--${esc(key)}) * 2), 320px)"></div>
|
|
126
120
|
</div>
|
|
127
|
-
<span class="spacing-val"
|
|
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">
|
|
125
|
+
<h3 class="group-title">Scale</h3>
|
|
132
126
|
<div class="spacing-list">
|
|
133
|
-
${Object.
|
|
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.
|
|
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.
|
|
151
|
-
(
|
|
144
|
+
${Object.keys(radius).map(
|
|
145
|
+
(key) => `
|
|
152
146
|
<div class="radius-item">
|
|
153
|
-
<div class="radius-box" style="border-radius
|
|
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"
|
|
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
|
|
232
|
-
const
|
|
233
|
-
|
|
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
|
-
${
|
|
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
|
-
|
|
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
|
-
|
|
260
|
+
${ex.description ? `<div class="example-desc">${esc(ex.description)}</div>` : ""}
|
|
267
261
|
</div>
|
|
268
|
-
|
|
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('${
|
|
266
|
+
<button class="copy-btn" onclick="copyCode('${id}-ex-${i}', this)">Copy</button>
|
|
273
267
|
</div>
|
|
274
|
-
<pre class="example-code" id="${
|
|
268
|
+
<pre class="example-code" id="${id}-ex-${i}">${esc(ex.code)}</pre>
|
|
275
269
|
</div>
|
|
276
270
|
</div>`
|
|
277
271
|
).join("");
|
|
278
|
-
|
|
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
|
-
${
|
|
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
|
-
|
|
292
|
-
|
|
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>
|
|
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('${
|
|
355
|
+
<button class="copy-btn" onclick="copyCode('${id}-ai-meta', this)">Copy</button>
|
|
301
356
|
</div>
|
|
302
|
-
<pre class="example-code" id="${
|
|
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
|
-
${
|
|
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:
|
|
312
|
-
{ id: "props", label: "Props", content:
|
|
313
|
-
{ id: "examples", label: "Examples", content:
|
|
314
|
-
{
|
|
315
|
-
|
|
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="${
|
|
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('${
|
|
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 ? " 🔒" : ""}</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:
|
|
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:
|
|
927
|
-
<span style="display:inline-flex;align-items:center;font-family:${esc(ff)};font-size:
|
|
928
|
-
<span style="display:inline-flex;align-items:center;font-family:${esc(ff)};font-size:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
1606
|
-
<p style="margin:4px 0 0;font-size:
|
|
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:
|
|
1692
|
-
<p style="margin:4px 0 0;font-size:
|
|
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:
|
|
1720
|
-
<p style="margin:4px 0 0;font-size:
|
|
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.
|
|
1995
|
-
|
|
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>`;
|