@nghitrum/dsforge 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3324 @@
1
+ // src/types/index.ts
2
+ var TokenResolutionError = class extends Error {
3
+ constructor(message, path3, cause) {
4
+ super(message);
5
+ this.path = path3;
6
+ this.cause = cause;
7
+ this.name = "TokenResolutionError";
8
+ }
9
+ };
10
+ var CircularReferenceError = class extends TokenResolutionError {
11
+ constructor(cycle) {
12
+ const chain = cycle.join(" \u2192 ");
13
+ super(`Circular token reference detected: ${chain}`, cycle[0] ?? "");
14
+ this.cycle = cycle;
15
+ this.name = "CircularReferenceError";
16
+ }
17
+ };
18
+ var UnresolvedReferenceError = class extends TokenResolutionError {
19
+ constructor(refPath, fromToken) {
20
+ super(
21
+ `Token reference "{${refPath}}" in "${fromToken}" could not be resolved`,
22
+ fromToken
23
+ );
24
+ this.fromToken = fromToken;
25
+ this.name = "UnresolvedReferenceError";
26
+ }
27
+ };
28
+
29
+ // src/core/token-resolver.ts
30
+ var REF_PATTERN = /\{([^{}]+)\}/g;
31
+ var SOLE_REF_PATTERN = /^\{([^{}]+)\}$/;
32
+ function getByPath(obj, segments) {
33
+ let current = obj;
34
+ for (const segment of segments) {
35
+ if (current === null || typeof current !== "object") return void 0;
36
+ current = current[segment];
37
+ }
38
+ return current;
39
+ }
40
+ function lookupRef(refPath, config) {
41
+ const segments = refPath.split(".");
42
+ if (config.tokens) {
43
+ const fromTokens = getByPath(config.tokens, segments);
44
+ if (fromTokens !== void 0) {
45
+ return fromTokens;
46
+ }
47
+ }
48
+ const fromRoot = getByPath(config, segments);
49
+ if (fromRoot !== void 0) {
50
+ return fromRoot;
51
+ }
52
+ return void 0;
53
+ }
54
+ var TokenResolver = class {
55
+ constructor(config) {
56
+ /** LRU-style cache: refPath → resolved concrete string */
57
+ this.cache = /* @__PURE__ */ new Map();
58
+ /** Tracks the current resolution chain for cycle detection */
59
+ this.stack = [];
60
+ this.warnings = [];
61
+ this.config = config;
62
+ }
63
+ // ─── Public API ─────────────────────────────────────────────────────────────
64
+ /**
65
+ * Resolve all tokens in the config.
66
+ * Returns a flat map of every token path to its concrete value.
67
+ *
68
+ * Resolution order ensures correct layering:
69
+ * global → semantic → component
70
+ */
71
+ resolve() {
72
+ const tokens = {};
73
+ const layers = this.config.tokens;
74
+ if (!layers) {
75
+ return { tokens, warnings: this.warnings };
76
+ }
77
+ for (const [key, value] of Object.entries(layers.global ?? {})) {
78
+ const tokenPath = `global.${key}`;
79
+ try {
80
+ tokens[tokenPath] = this.resolveValue(String(value), tokenPath);
81
+ } catch (err) {
82
+ this.handleError(err, tokenPath);
83
+ }
84
+ }
85
+ for (const [key, value] of Object.entries(layers.semantic ?? {})) {
86
+ const tokenPath = `semantic.${key}`;
87
+ try {
88
+ tokens[tokenPath] = this.resolveValue(String(value), tokenPath);
89
+ } catch (err) {
90
+ this.handleError(err, tokenPath);
91
+ }
92
+ }
93
+ for (const [key, value] of Object.entries(layers.component ?? {})) {
94
+ const tokenPath = `component.${key}`;
95
+ try {
96
+ tokens[tokenPath] = this.resolveValue(String(value), tokenPath);
97
+ } catch (err) {
98
+ this.handleError(err, tokenPath);
99
+ }
100
+ }
101
+ return { tokens, warnings: this.warnings };
102
+ }
103
+ /**
104
+ * Resolve a single value in isolation.
105
+ * Useful for one-off resolution outside the full config context.
106
+ */
107
+ resolveOneValue(value, contextPath = "<anonymous>") {
108
+ return this.resolveValue(value, contextPath);
109
+ }
110
+ // ─── Core resolution logic ───────────────────────────────────────────────────
111
+ /**
112
+ * Resolve all {ref} patterns within a value string.
113
+ *
114
+ * Two modes:
115
+ * - SOLE reference: "{global.blue-600}" → return the resolved value directly
116
+ * (preserves numeric types when the referenced value is a number)
117
+ * - INTERPOLATED: "0 1px {shadow.color}" → replace refs inline, return string
118
+ */
119
+ resolveValue(value, currentPath) {
120
+ if (!value.includes("{")) {
121
+ return value;
122
+ }
123
+ const soleMatch = SOLE_REF_PATTERN.exec(value);
124
+ if (soleMatch?.[1] !== void 0) {
125
+ return this.resolveRef(soleMatch[1], currentPath);
126
+ }
127
+ return value.replace(REF_PATTERN, (_match, refPath) => {
128
+ try {
129
+ return this.resolveRef(refPath, currentPath);
130
+ } catch (err) {
131
+ if (err instanceof UnresolvedReferenceError) {
132
+ this.warnings.push({
133
+ type: "unresolved_ref",
134
+ path: currentPath,
135
+ message: err.message
136
+ });
137
+ return `{${refPath}}`;
138
+ }
139
+ throw err;
140
+ }
141
+ });
142
+ }
143
+ /**
144
+ * Resolve a single reference path to its concrete value.
145
+ * Implements:
146
+ * - Cache lookup (avoid redundant resolution)
147
+ * - Cycle detection via the resolution stack
148
+ * - Recursive resolution (a ref can point to another ref)
149
+ */
150
+ resolveRef(refPath, fromToken) {
151
+ const cached = this.cache.get(refPath);
152
+ if (cached !== void 0) {
153
+ return cached;
154
+ }
155
+ const cycleIndex = this.stack.indexOf(refPath);
156
+ if (cycleIndex !== -1) {
157
+ throw new CircularReferenceError([
158
+ ...this.stack.slice(cycleIndex),
159
+ refPath
160
+ ]);
161
+ }
162
+ const rawValue = lookupRef(refPath, this.config);
163
+ if (rawValue === void 0) {
164
+ throw new UnresolvedReferenceError(refPath, fromToken);
165
+ }
166
+ this.stack.push(refPath);
167
+ const resolved = this.resolveValue(String(rawValue), refPath);
168
+ this.stack.pop();
169
+ this.cache.set(refPath, resolved);
170
+ return resolved;
171
+ }
172
+ // ─── Error handling ──────────────────────────────────────────────────────────
173
+ handleError(err, path3) {
174
+ if (err instanceof CircularReferenceError) {
175
+ throw err;
176
+ }
177
+ if (err instanceof UnresolvedReferenceError) {
178
+ this.warnings.push({
179
+ type: "unresolved_ref",
180
+ path: path3,
181
+ message: err.message
182
+ });
183
+ return;
184
+ }
185
+ if (err instanceof Error) {
186
+ throw new TokenResolutionError(err.message, path3, err);
187
+ }
188
+ throw err;
189
+ }
190
+ };
191
+ function resolveTokens(config) {
192
+ return new TokenResolver(config).resolve();
193
+ }
194
+ function buildThemeCss(config, themeName) {
195
+ const theme = config.themes?.[themeName];
196
+ if (!theme) {
197
+ throw new Error(`Theme "${themeName}" not found in config.themes`);
198
+ }
199
+ const { tokens } = resolveTokens(config);
200
+ const lines = [];
201
+ for (const [tokenName, themeValue] of Object.entries(theme)) {
202
+ const cssVarName = `--${tokenName}`;
203
+ lines.push(` ${cssVarName}: ${String(themeValue)};`);
204
+ }
205
+ for (const [tokenPath, resolvedValue] of Object.entries(tokens)) {
206
+ if (tokenPath.startsWith("semantic.")) {
207
+ const varName = tokenPath.replace("semantic.", "").replace(/\./g, "-");
208
+ const cssVarName = `--${varName}`;
209
+ const themeKey = varName;
210
+ if (!(themeKey in theme)) {
211
+ lines.push(` ${cssVarName}: ${resolvedValue};`);
212
+ }
213
+ }
214
+ }
215
+ return `:root[data-theme="${themeName}"] {
216
+ ${lines.join("\n")}
217
+ }`;
218
+ }
219
+ function extractRefs(value) {
220
+ const refs = [];
221
+ let match;
222
+ const pattern = new RegExp(REF_PATTERN.source, "g");
223
+ while ((match = pattern.exec(value)) !== null) {
224
+ if (match[1]) refs.push(match[1]);
225
+ }
226
+ return refs;
227
+ }
228
+ function hasRefs(value) {
229
+ return REF_PATTERN.test(value);
230
+ }
231
+
232
+ // src/utils/fs.ts
233
+ import fs from "fs-extra";
234
+ import path from "path";
235
+
236
+ // src/schema/config.schema.ts
237
+ import { z } from "zod";
238
+ var RawValueSchema = z.union([z.string(), z.number()]);
239
+ var TokenValueSchema = RawValueSchema;
240
+ var CssLengthSchema = z.string().regex(
241
+ /^\d+(\.\d+)?(px|rem|em|%|vw|vh)?$/,
242
+ "Must be a valid CSS length (e.g. '4px', '0.25rem')"
243
+ );
244
+ var GlobalTokensSchema = z.record(z.string(), RawValueSchema).describe("Layer 1: primitive raw values only. No {references} allowed.");
245
+ var SemanticTokensSchema = z.record(z.string(), TokenValueSchema).describe("Layer 2: intent-named tokens. May reference {global.*} tokens.");
246
+ var ComponentTokensSchema = z.record(z.string(), TokenValueSchema).describe("Layer 3: component-specific. May reference global or semantic.");
247
+ var TokenLayersSchema = z.object({
248
+ global: GlobalTokensSchema.optional(),
249
+ semantic: SemanticTokensSchema.optional(),
250
+ component: ComponentTokensSchema.optional()
251
+ });
252
+ var ColorSurfaceSchema = z.object({
253
+ default: TokenValueSchema,
254
+ subtle: TokenValueSchema.optional(),
255
+ overlay: TokenValueSchema.optional(),
256
+ inverse: TokenValueSchema.optional()
257
+ });
258
+ var ColorBorderSchema = z.object({
259
+ default: TokenValueSchema,
260
+ strong: TokenValueSchema.optional(),
261
+ focus: TokenValueSchema.optional(),
262
+ subtle: TokenValueSchema.optional()
263
+ });
264
+ var ColorTextSchema = z.object({
265
+ primary: TokenValueSchema,
266
+ secondary: TokenValueSchema.optional(),
267
+ disabled: TokenValueSchema.optional(),
268
+ inverse: TokenValueSchema.optional(),
269
+ onColor: TokenValueSchema.optional()
270
+ });
271
+ var StatusColorSetSchema = z.object({
272
+ bg: TokenValueSchema,
273
+ fg: TokenValueSchema,
274
+ border: TokenValueSchema.optional()
275
+ });
276
+ var ColorStatusSchema = z.object({
277
+ success: StatusColorSetSchema.optional(),
278
+ warning: StatusColorSetSchema.optional(),
279
+ danger: StatusColorSetSchema.optional(),
280
+ info: StatusColorSetSchema.optional()
281
+ });
282
+ var InteractiveStateSetSchema = z.object({
283
+ rest: TokenValueSchema,
284
+ hover: TokenValueSchema,
285
+ active: TokenValueSchema,
286
+ disabled: TokenValueSchema,
287
+ selected: TokenValueSchema.optional()
288
+ });
289
+ var ColorInteractiveSchema = z.object({
290
+ primary: InteractiveStateSetSchema.optional(),
291
+ secondary: InteractiveStateSetSchema.optional(),
292
+ danger: InteractiveStateSetSchema.optional()
293
+ });
294
+ var ColorConfigSchema = z.object({
295
+ surface: ColorSurfaceSchema.optional(),
296
+ border: ColorBorderSchema.optional(),
297
+ text: ColorTextSchema.optional(),
298
+ status: ColorStatusSchema.optional(),
299
+ interactive: ColorInteractiveSchema.optional()
300
+ });
301
+ var FontWeightSchema = z.union([
302
+ z.literal(100),
303
+ z.literal(200),
304
+ z.literal(300),
305
+ z.literal(400),
306
+ z.literal(500),
307
+ z.literal(600),
308
+ z.literal(700),
309
+ z.literal(800),
310
+ z.literal(900)
311
+ ]);
312
+ var TypographyRoleSchema = z.object({
313
+ size: z.number().min(8).max(128).describe("Font size in px"),
314
+ weight: FontWeightSchema,
315
+ lineHeight: z.number().min(0.8).max(3).describe("Unitless line height multiplier"),
316
+ letterSpacing: z.union([z.string(), z.number()]).optional(),
317
+ fontFamily: z.string().optional()
318
+ });
319
+ var TypographyConfigSchema = z.object({
320
+ fontFamily: z.string().min(1),
321
+ roles: z.object({
322
+ display: TypographyRoleSchema.optional(),
323
+ h1: TypographyRoleSchema.optional(),
324
+ h2: TypographyRoleSchema.optional(),
325
+ h3: TypographyRoleSchema.optional(),
326
+ body: TypographyRoleSchema.optional(),
327
+ small: TypographyRoleSchema.optional(),
328
+ caption: TypographyRoleSchema.optional(),
329
+ label: TypographyRoleSchema.optional(),
330
+ code: TypographyRoleSchema.optional()
331
+ }).optional(),
332
+ /** Legacy — kept for backward compat; prefer roles */
333
+ scale: z.array(z.number().positive()).optional(),
334
+ fontWeights: z.array(FontWeightSchema).optional()
335
+ });
336
+ var SpacingConfigSchema = z.object({
337
+ baseUnit: z.number().positive().optional(),
338
+ scale: z.record(z.string(), z.number().nonnegative()).optional(),
339
+ semantic: z.record(z.string(), TokenValueSchema).optional()
340
+ });
341
+ var RadiusConfigSchema = z.record(z.string(), z.number().nonnegative()).describe(
342
+ "Named border-radius values in px. Keys: none, sm, md, lg, xl, full"
343
+ );
344
+ var ElevationConfigSchema = z.record(z.string(), z.string()).describe(
345
+ "Named shadow levels. Values are CSS box-shadow strings or 'none'."
346
+ );
347
+ var MotionDurationSchema = z.record(
348
+ z.string(),
349
+ z.number().nonnegative().describe("Duration in ms")
350
+ );
351
+ var MotionEasingSchema = z.record(
352
+ z.string(),
353
+ z.string().describe("CSS easing value: keyword or cubic-bezier(...)")
354
+ );
355
+ var MotionConfigSchema = z.object({
356
+ duration: MotionDurationSchema.optional(),
357
+ easing: MotionEasingSchema.optional()
358
+ });
359
+ var FocusRingSchema = z.object({
360
+ color: TokenValueSchema,
361
+ width: CssLengthSchema,
362
+ offset: CssLengthSchema,
363
+ style: z.enum(["solid", "dashed", "dotted"]).optional()
364
+ });
365
+ var StatesConfigSchema = z.object({
366
+ hoverOpacity: z.number().min(0).max(1).optional(),
367
+ activeOpacity: z.number().min(0).max(1).optional(),
368
+ disabledOpacity: z.number().min(0).max(1).optional(),
369
+ focusRing: FocusRingSchema.optional()
370
+ });
371
+ var BreakpointsSchema = z.record(
372
+ z.string(),
373
+ z.number().positive().describe("Breakpoint width in px")
374
+ );
375
+ var GridColumnsSchema = z.record(
376
+ z.string(),
377
+ z.number().int().positive().max(24)
378
+ );
379
+ var GridConfigSchema = z.object({
380
+ columns: GridColumnsSchema.optional(),
381
+ gutter: z.number().nonnegative().optional(),
382
+ margin: z.number().nonnegative().optional()
383
+ });
384
+ var LayoutConfigSchema = z.object({
385
+ breakpoints: BreakpointsSchema.optional(),
386
+ grid: GridConfigSchema.optional(),
387
+ container: z.record(z.string(), z.number().positive()).optional()
388
+ });
389
+ var ThemesConfigSchema = z.record(z.string(), z.record(z.string(), RawValueSchema)).describe(
390
+ "Named themes. Each is a flat map of semantic token overrides. Keys become CSS custom property names; values are concrete values."
391
+ );
392
+ var PhilosophyConfigSchema = z.object({
393
+ density: z.enum(["compact", "comfortable", "spacious"]).optional(),
394
+ elevation: z.enum(["minimal", "moderate", "expressive"]).optional(),
395
+ motion: z.enum(["none", "reduced", "full"]).optional()
396
+ });
397
+ var CustomizationConfigSchema = z.object({
398
+ extends: z.string().nullable().optional(),
399
+ overrides: z.record(z.string(), RawValueSchema).optional()
400
+ });
401
+ var SUPPORTED_TARGETS = ["react"];
402
+ var OutputConfigSchema = z.object({
403
+ /**
404
+ * The framework adapter to use when generating component files.
405
+ *
406
+ * @default "react"
407
+ *
408
+ * Supported values: "react"
409
+ * Future values: "vue", "svelte", "angular", "react-native"
410
+ *
411
+ * An unknown value here will fail validation with an actionable message
412
+ * rather than silently generating incorrect output.
413
+ */
414
+ target: z.enum(SUPPORTED_TARGETS, {
415
+ errorMap: () => ({
416
+ message: `output.target must be one of: ${SUPPORTED_TARGETS.join(", ")}. Got an unsupported value. If you are trying to use a framework that is not yet supported, remove the output.target field or set it to "react".`
417
+ })
418
+ }).default("react")
419
+ }).optional();
420
+ var MetaConfigSchema = z.object({
421
+ name: z.string().min(1).regex(
422
+ /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/,
423
+ "Must be a valid npm package name"
424
+ ),
425
+ version: z.string().regex(/^\d+\.\d+\.\d+/, "Must be a valid semver string (e.g. 0.1.0)"),
426
+ description: z.string().optional(),
427
+ preset: z.enum(["compact", "comfortable", "spacious"]).optional(),
428
+ npmScope: z.string().regex(
429
+ /^@[a-z0-9-~][a-z0-9-._~]*$/,
430
+ "Must be a valid npm scope (e.g. @myorg)"
431
+ ).optional()
432
+ });
433
+ var DesignSystemConfigSchema = z.object({
434
+ meta: MetaConfigSchema,
435
+ tokens: TokenLayersSchema.optional(),
436
+ themes: ThemesConfigSchema.optional(),
437
+ color: ColorConfigSchema.optional(),
438
+ typography: TypographyConfigSchema.optional(),
439
+ spacing: SpacingConfigSchema.optional(),
440
+ radius: RadiusConfigSchema.optional(),
441
+ elevation: ElevationConfigSchema.optional(),
442
+ motion: MotionConfigSchema.optional(),
443
+ states: StatesConfigSchema.optional(),
444
+ layout: LayoutConfigSchema.optional(),
445
+ philosophy: PhilosophyConfigSchema.optional(),
446
+ customization: CustomizationConfigSchema.optional(),
447
+ output: OutputConfigSchema
448
+ });
449
+ var A11yRuleSchema = z.object({
450
+ keyboard: z.boolean().optional(),
451
+ focusRing: z.boolean().optional(),
452
+ ariaLabel: z.enum(["required", "optional", "forbidden"]).optional(),
453
+ role: z.string().optional()
454
+ });
455
+ var ComponentRuleSchema = z.object({
456
+ allowedVariants: z.array(z.string()).optional(),
457
+ requiredProps: z.array(z.string()).optional(),
458
+ maxWidth: z.string().optional(),
459
+ allowedRadius: z.array(z.string()).optional(),
460
+ allowedShadows: z.array(z.string()).optional(),
461
+ colorPalette: z.array(z.string()).optional(),
462
+ tokens: z.record(z.string(), TokenValueSchema).optional(),
463
+ a11y: A11yRuleSchema.optional()
464
+ });
465
+ var RulesConfigSchema = z.record(z.string(), ComponentRuleSchema);
466
+
467
+ // src/utils/logger.ts
468
+ import chalk from "chalk";
469
+
470
+ // src/utils/contrast.ts
471
+ function parseHex(hex) {
472
+ const clean = hex.replace(/^#/, "");
473
+ if (clean.length === 3) {
474
+ const r = parseInt(clean[0] + clean[0], 16);
475
+ const g = parseInt(clean[1] + clean[1], 16);
476
+ const b = parseInt(clean[2] + clean[2], 16);
477
+ if (isNaN(r) || isNaN(g) || isNaN(b)) return null;
478
+ return [r, g, b];
479
+ }
480
+ if (clean.length === 6) {
481
+ const r = parseInt(clean.slice(0, 2), 16);
482
+ const g = parseInt(clean.slice(2, 4), 16);
483
+ const b = parseInt(clean.slice(4, 6), 16);
484
+ if (isNaN(r) || isNaN(g) || isNaN(b)) return null;
485
+ return [r, g, b];
486
+ }
487
+ return null;
488
+ }
489
+ function linearize(channel) {
490
+ const sRGB = channel / 255;
491
+ return sRGB <= 0.03928 ? sRGB / 12.92 : Math.pow((sRGB + 0.055) / 1.055, 2.4);
492
+ }
493
+ function relativeLuminance(r, g, b) {
494
+ return 0.2126 * linearize(r) + 0.7152 * linearize(g) + 0.0722 * linearize(b);
495
+ }
496
+ function contrastRatio(l1, l2) {
497
+ const lighter = Math.max(l1, l2);
498
+ const darker = Math.min(l1, l2);
499
+ return (lighter + 0.05) / (darker + 0.05);
500
+ }
501
+ function hexContrastRatio(foreground, background) {
502
+ const fg = parseHex(foreground);
503
+ const bg = parseHex(background);
504
+ if (!fg || !bg) return null;
505
+ const fgLum = relativeLuminance(...fg);
506
+ const bgLum = relativeLuminance(...bg);
507
+ return contrastRatio(fgLum, bgLum);
508
+ }
509
+ var WCAG_THRESHOLDS = {
510
+ AA: { normal: 4.5, large: 3 },
511
+ AAA: { normal: 7, large: 4.5 }
512
+ };
513
+ function checkContrast(foreground, background, level = "AA", textSize = "normal") {
514
+ const ratio = hexContrastRatio(foreground, background);
515
+ if (ratio === null) return null;
516
+ const required = WCAG_THRESHOLDS[level][textSize];
517
+ return {
518
+ ratio,
519
+ passes: ratio >= required,
520
+ level,
521
+ textSize,
522
+ required,
523
+ ratioDisplay: ratio.toFixed(2)
524
+ };
525
+ }
526
+ function suggestContrastFix(foreground, background, level = "AA", textSize = "normal") {
527
+ const bgParsed = parseHex(background);
528
+ if (!bgParsed) return null;
529
+ const bgLum = relativeLuminance(...bgParsed);
530
+ const required = WCAG_THRESHOLDS[level][textSize];
531
+ const shouldDarken = bgLum > 0.5;
532
+ const fgParsed = parseHex(foreground);
533
+ if (!fgParsed) return null;
534
+ let [r, g, b] = fgParsed;
535
+ for (let step = 0; step < 30; step++) {
536
+ const fgLum = relativeLuminance(r, g, b);
537
+ const ratio = contrastRatio(fgLum, bgLum);
538
+ if (ratio >= required) {
539
+ return rgbToHex(r, g, b);
540
+ }
541
+ if (shouldDarken) {
542
+ r = Math.max(0, Math.round(r * 0.88));
543
+ g = Math.max(0, Math.round(g * 0.88));
544
+ b = Math.max(0, Math.round(b * 0.88));
545
+ } else {
546
+ r = Math.min(255, Math.round(r + (255 - r) * 0.12));
547
+ g = Math.min(255, Math.round(g + (255 - g) * 0.12));
548
+ b = Math.min(255, Math.round(b + (255 - b) * 0.12));
549
+ }
550
+ }
551
+ return null;
552
+ }
553
+ function rgbToHex(r, g, b) {
554
+ return "#" + [r, g, b].map((v) => v.toString(16).padStart(2, "0")).join("");
555
+ }
556
+ function isHexColor(value) {
557
+ return /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(value);
558
+ }
559
+
560
+ // src/cli/commands/validate.ts
561
+ var HEALTH_CHECKS = [
562
+ // ── 1. Token architecture (15 pts) ──────────────────────────────────────────
563
+ {
564
+ id: "token-architecture",
565
+ label: "Token Architecture",
566
+ maxScore: 15,
567
+ run(config) {
568
+ const issues = [];
569
+ const layers = config.tokens;
570
+ if (!layers) {
571
+ issues.push({
572
+ severity: "error",
573
+ code: "TOKEN_ARCH_MISSING",
574
+ message: 'No "tokens" section defined. Add tokens.global, tokens.semantic, tokens.component.',
575
+ path: "tokens",
576
+ suggestion: 'Add a "tokens": { "global": {}, "semantic": {}, "component": {} } section.'
577
+ });
578
+ return issues;
579
+ }
580
+ if (!layers.global || Object.keys(layers.global).length === 0) {
581
+ issues.push({
582
+ severity: "error",
583
+ code: "TOKEN_GLOBAL_EMPTY",
584
+ message: "tokens.global is empty. Global tokens are the raw value source of truth.",
585
+ path: "tokens.global",
586
+ suggestion: 'Add your brand palette as global tokens: { "blue-600": "#2563eb" }'
587
+ });
588
+ }
589
+ if (!layers.semantic || Object.keys(layers.semantic).length === 0) {
590
+ issues.push({
591
+ severity: "error",
592
+ code: "TOKEN_SEMANTIC_EMPTY",
593
+ message: "tokens.semantic is empty. Semantic tokens give intent to raw values.",
594
+ path: "tokens.semantic",
595
+ suggestion: 'Add semantic tokens: { "color-action": "{global.blue-600}" }'
596
+ });
597
+ }
598
+ for (const [key, value] of Object.entries(layers.global ?? {})) {
599
+ const strValue = String(value);
600
+ if (extractRefs(strValue).length > 0) {
601
+ issues.push({
602
+ severity: "error",
603
+ code: "TOKEN_GLOBAL_REF",
604
+ message: `global.${key} contains a reference "${strValue}". Global tokens must be raw values.`,
605
+ path: `tokens.global.${key}`,
606
+ suggestion: "Replace the reference with the actual value."
607
+ });
608
+ }
609
+ }
610
+ return issues;
611
+ }
612
+ },
613
+ // ── 2. Color roles (15 pts) ─────────────────────────────────────────────────
614
+ {
615
+ id: "color-roles",
616
+ label: "Color Roles",
617
+ maxScore: 15,
618
+ run(config) {
619
+ const issues = [];
620
+ const color = config.color;
621
+ if (!color) {
622
+ issues.push({
623
+ severity: "error",
624
+ code: "COLOR_ROLES_MISSING",
625
+ message: 'No "color" section defined. Define semantic color roles (surface, border, text, status).',
626
+ path: "color"
627
+ });
628
+ return issues;
629
+ }
630
+ if (!color.surface?.default) {
631
+ issues.push({
632
+ severity: "error",
633
+ code: "COLOR_SURFACE_MISSING",
634
+ message: "color.surface.default is required.",
635
+ path: "color.surface"
636
+ });
637
+ }
638
+ if (!color.text?.primary) {
639
+ issues.push({
640
+ severity: "error",
641
+ code: "COLOR_TEXT_MISSING",
642
+ message: "color.text.primary is required.",
643
+ path: "color.text"
644
+ });
645
+ }
646
+ if (!color.border?.default) {
647
+ issues.push({
648
+ severity: "warning",
649
+ code: "COLOR_BORDER_MISSING",
650
+ message: "color.border.default is not defined.",
651
+ path: "color.border",
652
+ suggestion: "Add a default border color to keep borders consistent."
653
+ });
654
+ }
655
+ const statusVariants = ["success", "warning", "danger", "info"];
656
+ for (const variant of statusVariants) {
657
+ if (!color.status?.[variant]) {
658
+ issues.push({
659
+ severity: "warning",
660
+ code: `COLOR_STATUS_${variant.toUpperCase()}_MISSING`,
661
+ message: `color.status.${variant} is not defined.`,
662
+ path: `color.status.${variant}`,
663
+ suggestion: `Add bg, fg, and border values for ${variant} status.`
664
+ });
665
+ }
666
+ }
667
+ return issues;
668
+ }
669
+ },
670
+ // ── 3. Dark mode / theming (15 pts) ─────────────────────────────────────────
671
+ {
672
+ id: "dark-mode",
673
+ label: "Theme Support",
674
+ maxScore: 15,
675
+ run(config) {
676
+ const issues = [];
677
+ if (!config.themes) {
678
+ issues.push({
679
+ severity: "error",
680
+ code: "THEMES_MISSING",
681
+ message: 'No "themes" section defined. Without it, dark mode is impossible without a full rewrite.',
682
+ path: "themes",
683
+ suggestion: 'Add "themes": { "light": {...}, "dark": {...} } to your config.'
684
+ });
685
+ return issues;
686
+ }
687
+ if (!config.themes["light"]) {
688
+ issues.push({
689
+ severity: "error",
690
+ code: "THEME_LIGHT_MISSING",
691
+ message: "themes.light is not defined.",
692
+ path: "themes.light"
693
+ });
694
+ }
695
+ if (!config.themes["dark"]) {
696
+ issues.push({
697
+ severity: "error",
698
+ code: "THEME_DARK_MISSING",
699
+ message: "themes.dark is not defined. Dark mode requires a dark theme override map.",
700
+ path: "themes.dark",
701
+ suggestion: "Add a dark theme that remaps surface, text, and border semantic tokens."
702
+ });
703
+ }
704
+ return issues;
705
+ }
706
+ },
707
+ // ── 4. Typography roles (10 pts) ─────────────────────────────────────────────
708
+ {
709
+ id: "typography-roles",
710
+ label: "Typography Roles",
711
+ maxScore: 10,
712
+ run(config) {
713
+ const issues = [];
714
+ const typo = config.typography;
715
+ if (!typo) {
716
+ issues.push({
717
+ severity: "error",
718
+ code: "TYPOGRAPHY_MISSING",
719
+ message: 'No "typography" section defined.',
720
+ path: "typography"
721
+ });
722
+ return issues;
723
+ }
724
+ if (!typo.roles) {
725
+ issues.push({
726
+ severity: "error",
727
+ code: "TYPOGRAPHY_ROLES_MISSING",
728
+ message: "typography.roles is not defined. A raw scale array is insufficient \u2014 roles carry intent.",
729
+ path: "typography.roles",
730
+ suggestion: "Replace typography.scale with named roles: display, h1, h2, body, small, caption."
731
+ });
732
+ return issues;
733
+ }
734
+ const requiredRoles = ["body"];
735
+ const recommendedRoles = [
736
+ "display",
737
+ "h1",
738
+ "h2",
739
+ "small",
740
+ "caption"
741
+ ];
742
+ for (const role of requiredRoles) {
743
+ if (!typo.roles[role]) {
744
+ issues.push({
745
+ severity: "error",
746
+ code: `TYPOGRAPHY_ROLE_${role.toUpperCase()}_MISSING`,
747
+ message: `typography.roles.${role} is required.`,
748
+ path: `typography.roles.${role}`
749
+ });
750
+ }
751
+ }
752
+ for (const role of recommendedRoles) {
753
+ if (!typo.roles[role]) {
754
+ issues.push({
755
+ severity: "warning",
756
+ code: `TYPOGRAPHY_ROLE_${role.toUpperCase()}_MISSING`,
757
+ message: `typography.roles.${role} is not defined.`,
758
+ path: `typography.roles.${role}`,
759
+ suggestion: `Add a ${role} role with size, weight, and lineHeight.`
760
+ });
761
+ }
762
+ }
763
+ for (const [roleName, roleValue] of Object.entries(typo.roles ?? {})) {
764
+ if (roleValue && !roleValue.lineHeight) {
765
+ issues.push({
766
+ severity: "error",
767
+ code: "TYPOGRAPHY_ROLE_NO_LINE_HEIGHT",
768
+ message: `typography.roles.${roleName} is missing lineHeight. Line height is required for readability.`,
769
+ path: `typography.roles.${roleName}.lineHeight`,
770
+ suggestion: `Add "lineHeight": 1.5 (or similar) to typography.roles.${roleName}.`
771
+ });
772
+ }
773
+ }
774
+ return issues;
775
+ }
776
+ },
777
+ // ── 5. Spacing semantic scale (8 pts) ────────────────────────────────────────
778
+ {
779
+ id: "spacing-semantic",
780
+ label: "Spacing Semantic Scale",
781
+ maxScore: 8,
782
+ run(config) {
783
+ const issues = [];
784
+ const spacing = config.spacing;
785
+ if (!spacing) {
786
+ issues.push({
787
+ severity: "error",
788
+ code: "SPACING_MISSING",
789
+ message: 'No "spacing" section defined.',
790
+ path: "spacing"
791
+ });
792
+ return issues;
793
+ }
794
+ if (!spacing.scale || Object.keys(spacing.scale).length < 6) {
795
+ issues.push({
796
+ severity: "error",
797
+ code: "SPACING_SCALE_INSUFFICIENT",
798
+ message: `spacing.scale has fewer than 6 named steps (found ${Object.keys(spacing.scale ?? {}).length}). A robust scale needs at least 6.`,
799
+ path: "spacing.scale",
800
+ suggestion: 'Define at least: { "1": 4, "2": 8, "3": 12, "4": 16, "5": 24, "6": 32 }'
801
+ });
802
+ }
803
+ if (!spacing.semantic || Object.keys(spacing.semantic).length === 0) {
804
+ issues.push({
805
+ severity: "error",
806
+ code: "SPACING_SEMANTIC_MISSING",
807
+ message: "spacing.semantic is not defined. Without it, engineers arithmetic-derive spacing rather than using intent-named tokens.",
808
+ path: "spacing.semantic",
809
+ suggestion: 'Add "component-padding-md", "layout-gap-sm" etc. as semantic tokens.'
810
+ });
811
+ }
812
+ return issues;
813
+ }
814
+ },
815
+ // ── 6. Interactive states (8 pts) ────────────────────────────────────────────
816
+ {
817
+ id: "interactive-states",
818
+ label: "Interactive States",
819
+ maxScore: 8,
820
+ run(config) {
821
+ const issues = [];
822
+ if (!config.states) {
823
+ issues.push({
824
+ severity: "error",
825
+ code: "STATES_MISSING",
826
+ message: 'No "states" section defined. Without state tokens every component invents its own hover/focus styles.',
827
+ path: "states",
828
+ suggestion: 'Add "states": { "focusRing": {...}, "hoverOpacity": 0.08, "disabledOpacity": 0.4 }'
829
+ });
830
+ return issues;
831
+ }
832
+ if (!config.states.focusRing) {
833
+ issues.push({
834
+ severity: "error",
835
+ code: "STATES_FOCUS_RING_MISSING",
836
+ message: "states.focusRing is not defined. Focus rings are required for keyboard accessibility.",
837
+ path: "states.focusRing",
838
+ suggestion: 'Add "focusRing": { "color": "...", "width": "2px", "offset": "2px" }'
839
+ });
840
+ }
841
+ if (!config.color?.interactive?.primary) {
842
+ issues.push({
843
+ severity: "warning",
844
+ code: "STATES_INTERACTIVE_MISSING",
845
+ message: "color.interactive.primary is not defined. Without it, hover/active/disabled colors are undefined.",
846
+ path: "color.interactive.primary",
847
+ suggestion: 'Add rest/hover/active/disabled values to "color.interactive.primary".'
848
+ });
849
+ }
850
+ return issues;
851
+ }
852
+ },
853
+ // ── 7. Motion tokens (6 pts) ──────────────────────────────────────────────────
854
+ {
855
+ id: "motion-tokens",
856
+ label: "Motion Tokens",
857
+ maxScore: 6,
858
+ run(config) {
859
+ const issues = [];
860
+ const motion = config.motion;
861
+ if (!motion) {
862
+ issues.push({
863
+ severity: "warning",
864
+ code: "MOTION_MISSING",
865
+ message: 'No "motion" section defined. Animation durations and easing will be magic numbers.',
866
+ path: "motion",
867
+ suggestion: 'Add "motion": { "duration": { "fast": 100, "normal": 200 }, "easing": { "standard": "..." } }'
868
+ });
869
+ return issues;
870
+ }
871
+ const durationCount = Object.keys(motion.duration ?? {}).length;
872
+ if (durationCount < 2) {
873
+ issues.push({
874
+ severity: "warning",
875
+ code: "MOTION_DURATION_INSUFFICIENT",
876
+ message: `motion.duration has only ${durationCount} value(s). Define at least 3 (fast, normal, slow).`,
877
+ path: "motion.duration"
878
+ });
879
+ }
880
+ if (!motion.easing || Object.keys(motion.easing).length === 0) {
881
+ issues.push({
882
+ severity: "warning",
883
+ code: "MOTION_EASING_MISSING",
884
+ message: "motion.easing is not defined.",
885
+ path: "motion.easing",
886
+ suggestion: 'Add "standard" and "decelerate" easing curves.'
887
+ });
888
+ }
889
+ return issues;
890
+ }
891
+ },
892
+ // ── 8. Elevation (6 pts) ─────────────────────────────────────────────────────
893
+ {
894
+ id: "elevation",
895
+ label: "Elevation Scale",
896
+ maxScore: 6,
897
+ run(config) {
898
+ const issues = [];
899
+ const elevation = config.elevation;
900
+ if (!elevation || Object.keys(elevation).length === 0) {
901
+ issues.push({
902
+ severity: "warning",
903
+ code: "ELEVATION_MISSING",
904
+ message: 'No "elevation" section defined. Shadow values will be inconsistent.',
905
+ path: "elevation",
906
+ suggestion: 'Add at least 4 named shadow levels: { "0": "none", "1": "...", "2": "...", "3": "..." }'
907
+ });
908
+ return issues;
909
+ }
910
+ if (Object.keys(elevation).length < 3) {
911
+ issues.push({
912
+ severity: "warning",
913
+ code: "ELEVATION_INSUFFICIENT",
914
+ message: `elevation has only ${Object.keys(elevation).length} level(s). Define at least 3 for a usable scale.`,
915
+ path: "elevation"
916
+ });
917
+ }
918
+ return issues;
919
+ }
920
+ },
921
+ // ── 9. Layout & breakpoints (6 pts) ──────────────────────────────────────────
922
+ {
923
+ id: "layout-grid",
924
+ label: "Layout & Breakpoints",
925
+ maxScore: 6,
926
+ run(config) {
927
+ const issues = [];
928
+ const layout = config.layout;
929
+ if (!layout) {
930
+ issues.push({
931
+ severity: "warning",
932
+ code: "LAYOUT_MISSING",
933
+ message: 'No "layout" section defined. Responsive behavior will be undefined.',
934
+ path: "layout",
935
+ suggestion: 'Add "layout": { "breakpoints": { "sm": 640, "md": 768, "lg": 1024 }, "grid": {...} }'
936
+ });
937
+ return issues;
938
+ }
939
+ const bpCount = Object.keys(layout.breakpoints ?? {}).length;
940
+ if (bpCount < 3) {
941
+ issues.push({
942
+ severity: "warning",
943
+ code: "LAYOUT_BREAKPOINTS_INSUFFICIENT",
944
+ message: `layout.breakpoints has only ${bpCount} breakpoint(s). Define at least 3 for responsive design.`,
945
+ path: "layout.breakpoints"
946
+ });
947
+ }
948
+ if (!layout.grid?.columns) {
949
+ issues.push({
950
+ severity: "warning",
951
+ code: "LAYOUT_GRID_MISSING",
952
+ message: "layout.grid.columns is not defined. Grid column counts per breakpoint are needed for consistent layouts.",
953
+ path: "layout.grid.columns"
954
+ });
955
+ }
956
+ return issues;
957
+ }
958
+ }
959
+ ];
960
+ function runTokenResolutionCheck(config) {
961
+ const issues = [];
962
+ try {
963
+ const { warnings } = resolveTokens(config);
964
+ for (const w of warnings) {
965
+ issues.push({
966
+ severity: "error",
967
+ code: "TOKEN_UNRESOLVED_REF",
968
+ message: w.message,
969
+ path: w.path,
970
+ suggestion: "Check that the referenced token exists in tokens.global or tokens.semantic."
971
+ });
972
+ }
973
+ } catch (err) {
974
+ if (err instanceof CircularReferenceError) {
975
+ issues.push({
976
+ severity: "error",
977
+ code: "TOKEN_CIRCULAR_REF",
978
+ message: `Circular token reference: ${err.cycle.join(" \u2192 ")}`,
979
+ path: err.path,
980
+ suggestion: "Break the cycle by resolving one of the references to a raw value."
981
+ });
982
+ } else {
983
+ issues.push({
984
+ severity: "error",
985
+ code: "TOKEN_RESOLUTION_FAILED",
986
+ message: `Token resolution failed: ${err.message}`,
987
+ path: "tokens"
988
+ });
989
+ }
990
+ }
991
+ return issues;
992
+ }
993
+ function runGovernanceChecks(_config, rules) {
994
+ const issues = [];
995
+ for (const [component, rule] of Object.entries(rules)) {
996
+ for (const [tokenName, tokenValue] of Object.entries(rule.tokens ?? {})) {
997
+ const refs = extractRefs(String(tokenValue));
998
+ for (const ref of refs) {
999
+ if (!ref.includes(".")) {
1000
+ issues.push({
1001
+ severity: "warning",
1002
+ code: "RULE_INVALID_REF_FORMAT",
1003
+ message: `rules.${component}.tokens.${tokenName}: reference "{${ref}}" has no layer prefix (expected "layer.key").`,
1004
+ path: `rules.${component}.tokens.${tokenName}`,
1005
+ suggestion: `Use "{semantic.${ref}}" or "{global.${ref}}" format.`
1006
+ });
1007
+ }
1008
+ }
1009
+ }
1010
+ }
1011
+ return issues;
1012
+ }
1013
+ function runContrastChecks(config) {
1014
+ const issues = [];
1015
+ const { tokens } = resolveTokens(config);
1016
+ function resolveColor(value) {
1017
+ const str = String(value ?? "");
1018
+ if (!str) return null;
1019
+ const refMatch = /^\{([^{}]+)\}$/.exec(str);
1020
+ if (refMatch?.[1]) {
1021
+ const resolved = tokens[refMatch[1]];
1022
+ return resolved && isHexColor(resolved) ? resolved : null;
1023
+ }
1024
+ return isHexColor(str) ? str : null;
1025
+ }
1026
+ const pairs = [];
1027
+ const textPrimary = resolveColor(config.color?.text?.primary);
1028
+ const textSecondary = resolveColor(config.color?.text?.secondary);
1029
+ const textDisabled = resolveColor(config.color?.text?.disabled);
1030
+ const textInverse = resolveColor(config.color?.text?.inverse);
1031
+ const bgDefault = resolveColor(config.color?.surface?.default);
1032
+ const bgSubtle = resolveColor(config.color?.surface?.subtle);
1033
+ const bgInverse = resolveColor(config.color?.surface?.inverse);
1034
+ if (textPrimary && bgDefault)
1035
+ pairs.push([
1036
+ textPrimary,
1037
+ bgDefault,
1038
+ "color.text.primary on color.surface.default"
1039
+ ]);
1040
+ if (textSecondary && bgDefault)
1041
+ pairs.push([
1042
+ textSecondary,
1043
+ bgDefault,
1044
+ "color.text.secondary on color.surface.default"
1045
+ ]);
1046
+ if (textPrimary && bgSubtle)
1047
+ pairs.push([
1048
+ textPrimary,
1049
+ bgSubtle,
1050
+ "color.text.primary on color.surface.subtle"
1051
+ ]);
1052
+ if (textInverse && bgInverse)
1053
+ pairs.push([
1054
+ textInverse,
1055
+ bgInverse,
1056
+ "color.text.inverse on color.surface.inverse"
1057
+ ]);
1058
+ for (const [statusName, statusValue] of Object.entries(
1059
+ config.color?.status ?? {}
1060
+ )) {
1061
+ if (!statusValue) continue;
1062
+ const fg = resolveColor(statusValue.fg);
1063
+ const bg = resolveColor(statusValue.bg);
1064
+ if (fg && bg) pairs.push([fg, bg, `color.status.${statusName}.fg on .bg`]);
1065
+ }
1066
+ const actionBg = resolveColor(
1067
+ tokens["semantic.color-action"] ?? config.color?.interactive?.primary?.rest
1068
+ );
1069
+ const actionText = resolveColor(config.color?.text?.onColor);
1070
+ if (actionBg && actionText)
1071
+ pairs.push([
1072
+ actionText,
1073
+ actionBg,
1074
+ "color.text.onColor on color-action (button text)"
1075
+ ]);
1076
+ for (const [fg, bg, label] of pairs) {
1077
+ const result = checkContrast(fg, bg, "AA", "normal");
1078
+ if (!result) continue;
1079
+ if (!result.passes) {
1080
+ const fix = suggestContrastFix(fg, bg, "AA", "normal");
1081
+ issues.push({
1082
+ severity: "error",
1083
+ code: "A11Y_CONTRAST_FAIL",
1084
+ message: `${label}: contrast ${result.ratioDisplay}:1 (required ${result.required}:1 for WCAG AA)`,
1085
+ path: label,
1086
+ suggestion: fix ? `Adjust foreground to approximately ${fix}` : `Increase contrast between ${fg} and ${bg} to at least ${result.required}:1`
1087
+ });
1088
+ }
1089
+ }
1090
+ if (textDisabled && bgDefault) {
1091
+ const result = checkContrast(textDisabled, bgDefault, "AA", "normal");
1092
+ if (result && result.ratio < 1.5) {
1093
+ issues.push({
1094
+ severity: "warning",
1095
+ code: "A11Y_DISABLED_TOO_FAINT",
1096
+ message: `color.text.disabled has contrast ${result.ratioDisplay}:1 \u2014 disabled text should remain perceivable.`,
1097
+ path: "color.text.disabled",
1098
+ suggestion: "Keep disabled text contrast above 1.5:1 even though AA exemption applies."
1099
+ });
1100
+ }
1101
+ }
1102
+ return issues;
1103
+ }
1104
+ function computeScore(config, rules, issues) {
1105
+ let score = 0;
1106
+ for (const check of HEALTH_CHECKS) {
1107
+ const checkIssues = check.run(config, rules);
1108
+ const hasErrors = checkIssues.some((i) => i.severity === "error");
1109
+ const hasWarnings = checkIssues.some((i) => i.severity === "warning");
1110
+ if (!hasErrors && !hasWarnings) {
1111
+ score += check.maxScore;
1112
+ } else if (!hasErrors && hasWarnings) {
1113
+ score += Math.floor(check.maxScore * 0.6);
1114
+ }
1115
+ }
1116
+ void issues;
1117
+ return score;
1118
+ }
1119
+ function validateConfig(config, rules) {
1120
+ const allIssues = [];
1121
+ for (const check of HEALTH_CHECKS) {
1122
+ allIssues.push(...check.run(config, rules));
1123
+ }
1124
+ allIssues.push(...runTokenResolutionCheck(config));
1125
+ allIssues.push(...runGovernanceChecks(config, rules));
1126
+ allIssues.push(...runContrastChecks(config));
1127
+ const maxScore = HEALTH_CHECKS.reduce((sum, c) => sum + c.maxScore, 0);
1128
+ const score = computeScore(config, rules, allIssues);
1129
+ const valid = !allIssues.some((i) => i.severity === "error");
1130
+ return { valid, score, maxScore, issues: allIssues };
1131
+ }
1132
+
1133
+ // src/cli/commands/init.ts
1134
+ import path2 from "path";
1135
+ import fs2 from "fs-extra";
1136
+ import chalk3 from "chalk";
1137
+
1138
+ // src/cli/prompt.ts
1139
+ import readline from "readline";
1140
+ import chalk2 from "chalk";
1141
+ var rl = readline.createInterface({
1142
+ input: process.stdin,
1143
+ output: process.stdout
1144
+ });
1145
+
1146
+ // src/cli/commands/init.ts
1147
+ var SPACING_PRESETS = {
1148
+ compact: {
1149
+ "1": 2,
1150
+ "2": 4,
1151
+ "3": 8,
1152
+ "4": 12,
1153
+ "5": 16,
1154
+ "6": 24,
1155
+ "7": 32,
1156
+ "8": 48
1157
+ },
1158
+ comfortable: {
1159
+ "1": 4,
1160
+ "2": 8,
1161
+ "3": 12,
1162
+ "4": 16,
1163
+ "5": 24,
1164
+ "6": 32,
1165
+ "7": 48,
1166
+ "8": 64
1167
+ },
1168
+ spacious: {
1169
+ "1": 6,
1170
+ "2": 12,
1171
+ "3": 18,
1172
+ "4": 24,
1173
+ "5": 36,
1174
+ "6": 48,
1175
+ "7": 72,
1176
+ "8": 96
1177
+ }
1178
+ };
1179
+ var RADIUS_PRESETS = {
1180
+ compact: { none: 0, sm: 2, md: 3, lg: 6, xl: 10, full: 9999 },
1181
+ comfortable: { none: 0, sm: 2, md: 4, lg: 8, xl: 16, full: 9999 },
1182
+ spacious: { none: 0, sm: 3, md: 6, lg: 12, xl: 20, full: 9999 }
1183
+ };
1184
+ function buildInitialConfig(name, preset = "comfortable") {
1185
+ const spacing = SPACING_PRESETS[preset];
1186
+ const radius = RADIUS_PRESETS[preset];
1187
+ return {
1188
+ meta: {
1189
+ name,
1190
+ version: "0.1.0",
1191
+ description: `${name} design system`,
1192
+ preset
1193
+ },
1194
+ tokens: {
1195
+ global: {
1196
+ // ── Brand palette ──
1197
+ // Replace with your actual brand colors from Figma.
1198
+ "brand-50": "#eff6ff",
1199
+ "brand-100": "#dbeafe",
1200
+ "brand-500": "#3b82f6",
1201
+ "brand-600": "#2563eb",
1202
+ "brand-700": "#1d4ed8",
1203
+ "brand-900": "#1e3a8a",
1204
+ // ── Neutral palette ──
1205
+ "neutral-0": "#ffffff",
1206
+ "neutral-50": "#f8fafc",
1207
+ "neutral-100": "#f1f5f9",
1208
+ "neutral-200": "#e2e8f0",
1209
+ "neutral-300": "#cbd5e1",
1210
+ "neutral-400": "#94a3b8",
1211
+ "neutral-500": "#64748b",
1212
+ "neutral-600": "#475569",
1213
+ "neutral-700": "#334155",
1214
+ "neutral-800": "#1e293b",
1215
+ "neutral-900": "#0f172a",
1216
+ "neutral-950": "#020617",
1217
+ // ── Status ──
1218
+ "green-50": "#f0fdf4",
1219
+ "green-700": "#15803d",
1220
+ "green-300": "#86efac",
1221
+ "yellow-50": "#fffbeb",
1222
+ "yellow-700": "#b45309",
1223
+ "yellow-300": "#fcd34d",
1224
+ "red-50": "#fef2f2",
1225
+ "red-600": "#c22121",
1226
+ "red-300": "#fca5a5",
1227
+ "blue-50": "#eff6ff",
1228
+ "blue-700": "#1d4ed8",
1229
+ "blue-300": "#93c5fd"
1230
+ },
1231
+ semantic: {
1232
+ // ── Color action ──
1233
+ "color-action": "{global.brand-600}",
1234
+ "color-action-hover": "{global.brand-700}",
1235
+ "color-action-active": "{global.brand-900}",
1236
+ "color-action-disabled": "{global.brand-100}",
1237
+ "color-action-text": "{global.neutral-0}",
1238
+ // ── Surfaces ──
1239
+ "color-bg-default": "{global.neutral-0}",
1240
+ "color-bg-subtle": "{global.neutral-50}",
1241
+ "color-bg-overlay": "{global.neutral-100}",
1242
+ "color-bg-inverse": "{global.neutral-900}",
1243
+ // ── Borders ──
1244
+ "color-border-default": "{global.neutral-200}",
1245
+ "color-border-strong": "{global.neutral-400}",
1246
+ "color-border-focus": "{global.brand-600}",
1247
+ // ── Text ──
1248
+ "color-text-primary": "{global.neutral-900}",
1249
+ "color-text-secondary": "{global.neutral-500}",
1250
+ "color-text-disabled": "{global.neutral-400}",
1251
+ "color-text-inverse": "{global.neutral-0}",
1252
+ "color-text-on-color": "{global.neutral-0}"
1253
+ },
1254
+ component: {
1255
+ // These will be filled in during generate.
1256
+ // Example: "button-bg": "{semantic.color-action}"
1257
+ }
1258
+ },
1259
+ themes: {
1260
+ light: {
1261
+ "color-bg-default": "#ffffff",
1262
+ "color-bg-subtle": "#f8fafc",
1263
+ "color-text-primary": "#0f172a",
1264
+ "color-text-secondary": "#64748b",
1265
+ "color-border-default": "#e2e8f0",
1266
+ "color-action": "#2563eb",
1267
+ "color-action-hover": "#1d4ed8"
1268
+ },
1269
+ dark: {
1270
+ "color-bg-default": "#0f172a",
1271
+ "color-bg-subtle": "#1e293b",
1272
+ "color-text-primary": "#f1f5f9",
1273
+ "color-text-secondary": "#94a3b8",
1274
+ "color-border-default": "#334155",
1275
+ "color-action": "#60a5fa",
1276
+ "color-action-hover": "#93c5fd"
1277
+ }
1278
+ },
1279
+ color: {
1280
+ surface: {
1281
+ default: "{semantic.color-bg-default}",
1282
+ subtle: "{semantic.color-bg-subtle}",
1283
+ overlay: "{semantic.color-bg-overlay}",
1284
+ inverse: "{semantic.color-bg-inverse}"
1285
+ },
1286
+ border: {
1287
+ default: "{semantic.color-border-default}",
1288
+ strong: "{semantic.color-border-strong}",
1289
+ focus: "{semantic.color-border-focus}"
1290
+ },
1291
+ text: {
1292
+ primary: "{semantic.color-text-primary}",
1293
+ secondary: "{semantic.color-text-secondary}",
1294
+ disabled: "{semantic.color-text-disabled}",
1295
+ inverse: "{semantic.color-text-inverse}",
1296
+ onColor: "{semantic.color-text-on-color}"
1297
+ },
1298
+ status: {
1299
+ success: {
1300
+ bg: "{global.green-50}",
1301
+ fg: "{global.green-700}",
1302
+ border: "{global.green-300}"
1303
+ },
1304
+ warning: {
1305
+ bg: "{global.yellow-50}",
1306
+ fg: "{global.yellow-700}",
1307
+ border: "{global.yellow-300}"
1308
+ },
1309
+ danger: {
1310
+ bg: "{global.red-50}",
1311
+ fg: "{global.red-600}",
1312
+ border: "{global.red-300}"
1313
+ },
1314
+ info: {
1315
+ bg: "{global.blue-50}",
1316
+ fg: "{global.blue-700}",
1317
+ border: "{global.blue-300}"
1318
+ }
1319
+ },
1320
+ interactive: {
1321
+ primary: {
1322
+ rest: "{semantic.color-action}",
1323
+ hover: "{semantic.color-action-hover}",
1324
+ active: "{semantic.color-action-active}",
1325
+ disabled: "{semantic.color-action-disabled}"
1326
+ }
1327
+ }
1328
+ },
1329
+ typography: {
1330
+ fontFamily: "Inter, system-ui, sans-serif",
1331
+ roles: {
1332
+ display: {
1333
+ size: 32,
1334
+ weight: 700,
1335
+ lineHeight: 1.2,
1336
+ letterSpacing: "-0.02em"
1337
+ },
1338
+ h1: {
1339
+ size: 24,
1340
+ weight: 700,
1341
+ lineHeight: 1.3,
1342
+ letterSpacing: "-0.01em"
1343
+ },
1344
+ h2: {
1345
+ size: 20,
1346
+ weight: 600,
1347
+ lineHeight: 1.4,
1348
+ letterSpacing: "-0.005em"
1349
+ },
1350
+ h3: { size: 18, weight: 600, lineHeight: 1.4, letterSpacing: 0 },
1351
+ body: { size: 16, weight: 400, lineHeight: 1.6, letterSpacing: 0 },
1352
+ small: { size: 14, weight: 400, lineHeight: 1.5, letterSpacing: 0 },
1353
+ caption: {
1354
+ size: 12,
1355
+ weight: 400,
1356
+ lineHeight: 1.4,
1357
+ letterSpacing: "0.01em"
1358
+ },
1359
+ label: {
1360
+ size: 14,
1361
+ weight: 500,
1362
+ lineHeight: 1.4,
1363
+ letterSpacing: "0.01em"
1364
+ },
1365
+ code: {
1366
+ size: 13,
1367
+ weight: 400,
1368
+ lineHeight: 1.6,
1369
+ fontFamily: "ui-monospace, monospace"
1370
+ }
1371
+ }
1372
+ },
1373
+ spacing: {
1374
+ baseUnit: preset === "compact" ? 2 : preset === "spacious" ? 6 : 4,
1375
+ scale: spacing,
1376
+ semantic: {
1377
+ "component-padding-xs": `${spacing[1]}`,
1378
+ "component-padding-sm": `${spacing[2]}`,
1379
+ "component-padding-md": `${spacing[4]}`,
1380
+ "component-padding-lg": `${spacing[5]}`,
1381
+ "layout-gap-xs": `${spacing[2]}`,
1382
+ "layout-gap-sm": `${spacing[3]}`,
1383
+ "layout-gap-md": `${spacing[5]}`,
1384
+ "layout-gap-lg": `${spacing[6]}`,
1385
+ "layout-section": `${spacing[7]}`
1386
+ }
1387
+ },
1388
+ radius,
1389
+ elevation: {
1390
+ "0": "none",
1391
+ "1": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
1392
+ "2": "0 1px 3px 0 rgb(0 0 0 / 0.10), 0 1px 2px -1px rgb(0 0 0 / 0.10)",
1393
+ "3": "0 4px 6px -1px rgb(0 0 0 / 0.10), 0 2px 4px -2px rgb(0 0 0 / 0.10)",
1394
+ "4": "0 10px 15px -3px rgb(0 0 0 / 0.10), 0 4px 6px -4px rgb(0 0 0 / 0.10)"
1395
+ },
1396
+ motion: {
1397
+ duration: {
1398
+ instant: 50,
1399
+ fast: 100,
1400
+ normal: 200,
1401
+ slow: 300,
1402
+ deliberate: 500
1403
+ },
1404
+ easing: {
1405
+ standard: "cubic-bezier(0.2, 0, 0, 1)",
1406
+ decelerate: "cubic-bezier(0, 0, 0, 1)",
1407
+ accelerate: "cubic-bezier(0.3, 0, 1, 1)",
1408
+ spring: "cubic-bezier(0.34, 1.56, 0.64, 1)"
1409
+ }
1410
+ },
1411
+ states: {
1412
+ hoverOpacity: 0.08,
1413
+ activeOpacity: 0.12,
1414
+ disabledOpacity: 0.4,
1415
+ focusRing: {
1416
+ color: "{semantic.color-border-focus}",
1417
+ width: "2px",
1418
+ offset: "2px"
1419
+ }
1420
+ },
1421
+ layout: {
1422
+ breakpoints: {
1423
+ sm: 640,
1424
+ md: 768,
1425
+ lg: 1024,
1426
+ xl: 1280,
1427
+ "2xl": 1536
1428
+ },
1429
+ grid: {
1430
+ columns: { sm: 4, md: 8, lg: 12 },
1431
+ gutter: 16,
1432
+ margin: 24
1433
+ },
1434
+ container: {
1435
+ sm: 640,
1436
+ md: 768,
1437
+ lg: 1024,
1438
+ xl: 1280
1439
+ }
1440
+ },
1441
+ philosophy: {
1442
+ density: preset,
1443
+ elevation: "minimal",
1444
+ motion: "full"
1445
+ },
1446
+ customization: {
1447
+ extends: null,
1448
+ overrides: {}
1449
+ }
1450
+ };
1451
+ }
1452
+ function buildInitialRules() {
1453
+ return {
1454
+ button: {
1455
+ allowedVariants: ["primary", "secondary", "danger", "ghost"],
1456
+ requiredProps: ["aria-label"],
1457
+ maxWidth: "320px",
1458
+ tokens: {
1459
+ "button-bg": "{semantic.color-action}",
1460
+ "button-bg-hover": "{semantic.color-action-hover}",
1461
+ "button-text": "{semantic.color-text-on-color}",
1462
+ "button-radius": "{radius.md}"
1463
+ },
1464
+ a11y: {
1465
+ keyboard: true,
1466
+ focusRing: true,
1467
+ ariaLabel: "required"
1468
+ }
1469
+ },
1470
+ input: {
1471
+ allowedVariants: ["default", "error", "disabled"],
1472
+ requiredProps: ["aria-label"],
1473
+ tokens: {
1474
+ "input-border": "{semantic.color-border-default}",
1475
+ "input-border-focus": "{semantic.color-border-focus}",
1476
+ "input-bg": "{semantic.color-bg-default}"
1477
+ },
1478
+ a11y: {
1479
+ keyboard: true,
1480
+ focusRing: true,
1481
+ ariaLabel: "required"
1482
+ }
1483
+ },
1484
+ card: {
1485
+ allowedVariants: ["default", "elevated", "outlined"],
1486
+ maxWidth: "640px",
1487
+ allowedRadius: ["md", "lg"],
1488
+ allowedShadows: ["0", "1", "2", "3"],
1489
+ tokens: {
1490
+ "card-bg": "{semantic.color-bg-default}",
1491
+ "card-border": "{semantic.color-border-default}",
1492
+ "card-radius": "{radius.lg}"
1493
+ }
1494
+ }
1495
+ };
1496
+ }
1497
+
1498
+ // src/generators/tokens/css-vars.ts
1499
+ function cssVar(name) {
1500
+ return `--${name}`;
1501
+ }
1502
+ function emitBlock(selector, entries, comment) {
1503
+ if (entries.length === 0) return "";
1504
+ const lines = [];
1505
+ if (comment) lines.push(`/* ${comment} */`);
1506
+ lines.push(`${selector} {`);
1507
+ for (const [name, value] of entries) {
1508
+ lines.push(` ${cssVar(name)}: ${value};`);
1509
+ }
1510
+ lines.push("}");
1511
+ return lines.join("\n");
1512
+ }
1513
+ function emitBaseCss(config, resolution) {
1514
+ const sections = [];
1515
+ const header = [
1516
+ `/* \u2500\u2500\u2500 ${config.meta.name} \u2014 base tokens \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */`,
1517
+ `/* Generated by dsforge. Do not edit manually. */`,
1518
+ `/* Import this file, then add a theme file for light/dark mode. */`,
1519
+ ""
1520
+ ].join("\n");
1521
+ const semanticEntries = Object.entries(
1522
+ resolution.tokens
1523
+ ).filter(([k]) => k.startsWith("semantic.")).map(([k, v]) => [k.replace("semantic.", ""), v]);
1524
+ if (semanticEntries.length > 0) {
1525
+ sections.push(emitBlock(":root", semanticEntries, "Semantic tokens"));
1526
+ }
1527
+ const componentEntries = Object.entries(
1528
+ resolution.tokens
1529
+ ).filter(([k]) => k.startsWith("component.")).map(([k, v]) => [k.replace("component.", ""), v]);
1530
+ if (componentEntries.length > 0) {
1531
+ sections.push(emitBlock(":root", componentEntries, "Component tokens"));
1532
+ }
1533
+ const spacing = config.spacing;
1534
+ const spacingEntries = [];
1535
+ for (const [key, value] of Object.entries(spacing?.scale ?? {})) {
1536
+ spacingEntries.push([`spacing-${key}`, `${value}px`]);
1537
+ }
1538
+ for (const [key, value] of Object.entries(spacing?.semantic ?? {})) {
1539
+ spacingEntries.push([key, `${value}px`]);
1540
+ }
1541
+ if (spacingEntries.length > 0) {
1542
+ sections.push(emitBlock(":root", spacingEntries, "Spacing"));
1543
+ }
1544
+ const typo = config.typography;
1545
+ const typoEntries = [];
1546
+ if (typo?.fontFamily) {
1547
+ typoEntries.push(["font-family-base", typo.fontFamily]);
1548
+ }
1549
+ for (const [role, value] of Object.entries(typo?.roles ?? {})) {
1550
+ if (!value) continue;
1551
+ typoEntries.push([`font-size-${role}`, `${value.size}px`]);
1552
+ typoEntries.push([`font-weight-${role}`, String(value.weight)]);
1553
+ typoEntries.push([`line-height-${role}`, String(value.lineHeight)]);
1554
+ if (value.letterSpacing !== void 0 && value.letterSpacing !== 0) {
1555
+ typoEntries.push([`letter-spacing-${role}`, String(value.letterSpacing)]);
1556
+ }
1557
+ }
1558
+ if (typoEntries.length > 0) {
1559
+ sections.push(emitBlock(":root", typoEntries, "Typography"));
1560
+ }
1561
+ const radiusEntries = Object.entries(
1562
+ config.radius ?? {}
1563
+ ).filter(([, v]) => v !== void 0).map(([k, v]) => [`radius-${k}`, v === 9999 ? "9999px" : `${v}px`]);
1564
+ if (radiusEntries.length > 0) {
1565
+ sections.push(emitBlock(":root", radiusEntries, "Border radius"));
1566
+ }
1567
+ const elevEntries = Object.entries(
1568
+ config.elevation ?? {}
1569
+ ).map(([k, v]) => [`shadow-${k}`, v]);
1570
+ if (elevEntries.length > 0) {
1571
+ sections.push(emitBlock(":root", elevEntries, "Elevation / shadows"));
1572
+ }
1573
+ const motionEntries = [];
1574
+ for (const [key, ms] of Object.entries(config.motion?.duration ?? {})) {
1575
+ if (ms !== void 0) motionEntries.push([`duration-${key}`, `${ms}ms`]);
1576
+ }
1577
+ for (const [key, curve] of Object.entries(config.motion?.easing ?? {})) {
1578
+ if (curve !== void 0) motionEntries.push([`easing-${key}`, curve]);
1579
+ }
1580
+ if (motionEntries.length > 0) {
1581
+ sections.push(emitBlock(":root", motionEntries, "Motion"));
1582
+ }
1583
+ const states = config.states;
1584
+ const stateEntries = [];
1585
+ if (states?.hoverOpacity !== void 0)
1586
+ stateEntries.push(["state-hover-opacity", String(states.hoverOpacity)]);
1587
+ if (states?.activeOpacity !== void 0)
1588
+ stateEntries.push(["state-active-opacity", String(states.activeOpacity)]);
1589
+ if (states?.disabledOpacity !== void 0)
1590
+ stateEntries.push([
1591
+ "state-disabled-opacity",
1592
+ String(states.disabledOpacity)
1593
+ ]);
1594
+ if (states?.focusRing) {
1595
+ const fr = states.focusRing;
1596
+ const frColor = resolution.tokens[String(fr.color).replace(/[{}]/g, "")] ?? String(fr.color);
1597
+ stateEntries.push(["focus-ring-color", frColor]);
1598
+ stateEntries.push(["focus-ring-width", fr.width]);
1599
+ stateEntries.push(["focus-ring-offset", fr.offset]);
1600
+ }
1601
+ if (stateEntries.length > 0) {
1602
+ sections.push(emitBlock(":root", stateEntries, "Interactive states"));
1603
+ }
1604
+ return [header, ...sections].join("\n\n") + "\n";
1605
+ }
1606
+ function emitThemeCss(themeName, themeOverrides, config) {
1607
+ const lines = [
1608
+ `/* \u2500\u2500\u2500 ${config.meta.name} \u2014 ${themeName} theme \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */`,
1609
+ `/* Generated by dsforge. Do not edit manually. */`,
1610
+ `/* Usage: <html data-theme="${themeName}"> or ThemeProvider theme="${themeName}" */`,
1611
+ ""
1612
+ ];
1613
+ const entries = Object.entries(themeOverrides).map(
1614
+ ([k, v]) => [k, String(v)]
1615
+ );
1616
+ lines.push(emitBlock(`:root[data-theme="${themeName}"]`, entries));
1617
+ return lines.join("\n") + "\n";
1618
+ }
1619
+ function generateCssFiles(config, resolution) {
1620
+ const files = [];
1621
+ files.push({
1622
+ filename: "base.css",
1623
+ content: emitBaseCss(config, resolution)
1624
+ });
1625
+ for (const [themeName, themeOverrides] of Object.entries(
1626
+ config.themes ?? {}
1627
+ )) {
1628
+ files.push({
1629
+ filename: `${themeName}.css`,
1630
+ content: emitThemeCss(themeName, themeOverrides, config)
1631
+ });
1632
+ }
1633
+ if (!config.themes || Object.keys(config.themes).length === 0) {
1634
+ const fallbackEntries = Object.entries(
1635
+ resolution.tokens
1636
+ ).filter(([k]) => k.startsWith("semantic.")).map(([k, v]) => [k.replace("semantic.", ""), v]);
1637
+ files.push({
1638
+ filename: "light.css",
1639
+ content: [
1640
+ `/* ${config.meta.name} \u2014 default theme (no themes defined in config) */`,
1641
+ emitBlock(":root", fallbackEntries),
1642
+ ""
1643
+ ].join("\n")
1644
+ });
1645
+ }
1646
+ return files;
1647
+ }
1648
+
1649
+ // src/adapters/react/tokens/js-tokens.ts
1650
+ function safeKey(k) {
1651
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k) ? k : JSON.stringify(k);
1652
+ }
1653
+ function toJsIdentifier(tokenPath) {
1654
+ const withoutLayer = tokenPath.replace(/^(global|semantic|component)\./, "");
1655
+ return withoutLayer.split("-").map(
1656
+ (part, i) => i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)
1657
+ ).join("");
1658
+ }
1659
+ function formatValue(value) {
1660
+ return JSON.stringify(value);
1661
+ }
1662
+ function emitJsTokens(config, resolution) {
1663
+ const lines = [
1664
+ `// ${config.meta.name} \u2014 design tokens`,
1665
+ `// Generated by dsforge v${config.meta.version}. Do not edit manually.`,
1666
+ "",
1667
+ "// \u2500\u2500\u2500 Resolved token map \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
1668
+ "export const tokens = {"
1669
+ ];
1670
+ const layers = ["global", "semantic", "component"];
1671
+ for (const layer of layers) {
1672
+ const entries = Object.entries(resolution.tokens).filter(
1673
+ ([k]) => k.startsWith(`${layer}.`)
1674
+ );
1675
+ if (entries.length === 0) continue;
1676
+ lines.push(` // ${layer}`);
1677
+ for (const [tokenPath, value] of entries) {
1678
+ const identifier = toJsIdentifier(tokenPath);
1679
+ lines.push(` ${identifier}: ${formatValue(value)},`);
1680
+ }
1681
+ }
1682
+ lines.push("} as const;", "");
1683
+ lines.push("export type TokenKey = keyof typeof tokens;", "");
1684
+ const themes = config.themes ?? {};
1685
+ if (Object.keys(themes).length > 0) {
1686
+ lines.push(
1687
+ "// \u2500\u2500\u2500 Theme overrides \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"
1688
+ );
1689
+ lines.push("export const themes = {");
1690
+ for (const [themeName, overrides] of Object.entries(themes)) {
1691
+ lines.push(` "${themeName}": {`);
1692
+ for (const [k, v] of Object.entries(overrides)) {
1693
+ lines.push(` ${JSON.stringify(k)}: ${JSON.stringify(String(v))},`);
1694
+ }
1695
+ lines.push(" },");
1696
+ }
1697
+ lines.push("} as const;", "");
1698
+ lines.push("export type ThemeName = keyof typeof themes;", "");
1699
+ }
1700
+ const spacingScale = config.spacing?.scale ?? {};
1701
+ if (Object.keys(spacingScale).length > 0) {
1702
+ lines.push(
1703
+ "// \u2500\u2500\u2500 Spacing scale \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"
1704
+ );
1705
+ lines.push("export const spacing = {");
1706
+ for (const [key, value] of Object.entries(spacingScale)) {
1707
+ lines.push(` ${JSON.stringify(key)}: ${value}, // ${value}px`);
1708
+ }
1709
+ lines.push("} as const;", "");
1710
+ }
1711
+ const typoRoles = config.typography?.roles ?? {};
1712
+ if (Object.keys(typoRoles).length > 0) {
1713
+ lines.push(
1714
+ "// \u2500\u2500\u2500 Typography roles \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"
1715
+ );
1716
+ lines.push("export const typography = {");
1717
+ for (const [role, value] of Object.entries(typoRoles)) {
1718
+ if (!value) continue;
1719
+ lines.push(` ${role}: {`);
1720
+ lines.push(` size: ${value.size},`);
1721
+ lines.push(` weight: ${value.weight},`);
1722
+ lines.push(` lineHeight: ${value.lineHeight},`);
1723
+ if (value.letterSpacing !== void 0) {
1724
+ lines.push(
1725
+ ` letterSpacing: ${JSON.stringify(String(value.letterSpacing))},`
1726
+ );
1727
+ }
1728
+ if (value.fontFamily) {
1729
+ lines.push(` fontFamily: ${JSON.stringify(value.fontFamily)},`);
1730
+ }
1731
+ lines.push(" },");
1732
+ }
1733
+ lines.push("} as const;", "");
1734
+ }
1735
+ const radius = config.radius ?? {};
1736
+ if (Object.keys(radius).length > 0) {
1737
+ lines.push(
1738
+ "// \u2500\u2500\u2500 Border radius \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"
1739
+ );
1740
+ lines.push("export const radius = {");
1741
+ for (const [key, value] of Object.entries(radius)) {
1742
+ if (value === void 0) continue;
1743
+ const cssValue = value === 9999 ? "9999px" : `${value}px`;
1744
+ lines.push(` ${key}: ${JSON.stringify(cssValue)},`);
1745
+ }
1746
+ lines.push("} as const;", "");
1747
+ }
1748
+ const elevation = config.elevation ?? {};
1749
+ if (Object.keys(elevation).length > 0) {
1750
+ lines.push(
1751
+ "// \u2500\u2500\u2500 Elevation / shadows \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"
1752
+ );
1753
+ lines.push("export const elevation = {");
1754
+ for (const [key, value] of Object.entries(elevation)) {
1755
+ lines.push(` ${JSON.stringify(key)}: ${JSON.stringify(value)},`);
1756
+ }
1757
+ lines.push("} as const;", "");
1758
+ }
1759
+ return lines.join("\n");
1760
+ }
1761
+ function emitTailwindConfig(config) {
1762
+ const lines = [
1763
+ `// ${config.meta.name} \u2014 Tailwind CSS theme extension`,
1764
+ `// Generated by dsforge. Do not edit manually.`,
1765
+ `//`,
1766
+ `// Usage in tailwind.config.js:`,
1767
+ `// const ds = require('./${config.meta.name}/tokens/tailwind');`,
1768
+ `// module.exports = { theme: { extend: ds } };`,
1769
+ "",
1770
+ "module.exports = {"
1771
+ ];
1772
+ const interactive = config.color?.interactive?.primary;
1773
+ const surface = config.color?.surface;
1774
+ const border = config.color?.border;
1775
+ const text = config.color?.text;
1776
+ const status = config.color?.status ?? {};
1777
+ lines.push(" colors: {");
1778
+ if (interactive) {
1779
+ lines.push(" action: {");
1780
+ if (interactive.rest) lines.push(` DEFAULT: 'var(--color-action)',`);
1781
+ if (interactive.hover)
1782
+ lines.push(` hover: 'var(--color-action-hover)',`);
1783
+ if (interactive.active)
1784
+ lines.push(` active: 'var(--color-action-active)',`);
1785
+ if (interactive.disabled)
1786
+ lines.push(` disabled: 'var(--color-action-disabled)',`);
1787
+ lines.push(" },");
1788
+ }
1789
+ if (surface) {
1790
+ lines.push(" surface: {");
1791
+ if (surface.default)
1792
+ lines.push(` DEFAULT: 'var(--color-bg-default)',`);
1793
+ if (surface.subtle) lines.push(` subtle: 'var(--color-bg-subtle)',`);
1794
+ if (surface.overlay)
1795
+ lines.push(` overlay: 'var(--color-bg-overlay)',`);
1796
+ if (surface.inverse)
1797
+ lines.push(` inverse: 'var(--color-bg-inverse)',`);
1798
+ lines.push(" },");
1799
+ }
1800
+ if (border) {
1801
+ lines.push(" border: {");
1802
+ if (border.default)
1803
+ lines.push(` DEFAULT: 'var(--color-border-default)',`);
1804
+ if (border.strong)
1805
+ lines.push(` strong: 'var(--color-border-strong)',`);
1806
+ if (border.focus) lines.push(` focus: 'var(--color-border-focus)',`);
1807
+ lines.push(" },");
1808
+ }
1809
+ if (text) {
1810
+ lines.push(" text: {");
1811
+ if (text.primary) lines.push(` DEFAULT: 'var(--color-text-primary)',`);
1812
+ if (text.secondary)
1813
+ lines.push(` secondary: 'var(--color-text-secondary)',`);
1814
+ if (text.disabled)
1815
+ lines.push(` disabled: 'var(--color-text-disabled)',`);
1816
+ if (text.inverse) lines.push(` inverse: 'var(--color-text-inverse)',`);
1817
+ lines.push(" },");
1818
+ }
1819
+ for (const [statusName, statusValue] of Object.entries(status)) {
1820
+ if (!statusValue) continue;
1821
+ lines.push(` ${safeKey(statusName)}: {`);
1822
+ lines.push(` bg: 'var(--color-${statusName}-bg)',`);
1823
+ lines.push(` DEFAULT: 'var(--color-${statusName}-fg)',`);
1824
+ lines.push(` border: 'var(--color-${statusName}-border)',`);
1825
+ lines.push(" },");
1826
+ }
1827
+ lines.push(" },");
1828
+ const spacingScale = config.spacing?.scale ?? {};
1829
+ if (Object.keys(spacingScale).length > 0) {
1830
+ lines.push(" spacing: {");
1831
+ for (const [key, value] of Object.entries(spacingScale)) {
1832
+ lines.push(` ${JSON.stringify(key)}: '${value}px',`);
1833
+ }
1834
+ lines.push(" },");
1835
+ }
1836
+ const roles = config.typography?.roles ?? {};
1837
+ if (Object.keys(roles).length > 0) {
1838
+ lines.push(" fontSize: {");
1839
+ for (const [role, value] of Object.entries(roles)) {
1840
+ if (!value) continue;
1841
+ const ls = value.letterSpacing ? `, letterSpacing: '${value.letterSpacing}'` : "";
1842
+ lines.push(
1843
+ ` ${safeKey(role)}: ['${value.size}px', { lineHeight: '${value.lineHeight}'${ls} }],`
1844
+ );
1845
+ }
1846
+ lines.push(" },");
1847
+ }
1848
+ const weights = /* @__PURE__ */ new Set();
1849
+ for (const role of Object.values(config.typography?.roles ?? {})) {
1850
+ if (role) weights.add(role.weight);
1851
+ }
1852
+ if (weights.size > 0) {
1853
+ lines.push(" fontWeight: {");
1854
+ const weightNames = {
1855
+ 100: "thin",
1856
+ 200: "extralight",
1857
+ 300: "light",
1858
+ 400: "normal",
1859
+ 500: "medium",
1860
+ 600: "semibold",
1861
+ 700: "bold",
1862
+ 800: "extrabold",
1863
+ 900: "black"
1864
+ };
1865
+ for (const w of [...weights].sort()) {
1866
+ const name = weightNames[w] ?? String(w);
1867
+ lines.push(` ${safeKey(name)}: '${w}',`);
1868
+ }
1869
+ lines.push(" },");
1870
+ }
1871
+ const radiusConfig = config.radius ?? {};
1872
+ if (Object.keys(radiusConfig).length > 0) {
1873
+ lines.push(" borderRadius: {");
1874
+ for (const [key, value] of Object.entries(radiusConfig)) {
1875
+ if (value === void 0) continue;
1876
+ const cssValue = value === 9999 ? "9999px" : `${value}px`;
1877
+ const twKey = key === "none" ? "none" : key === "full" ? "full" : `ds-${key}`;
1878
+ lines.push(` ${safeKey(twKey)}: '${cssValue}',`);
1879
+ }
1880
+ lines.push(" },");
1881
+ }
1882
+ const elevationConfig = config.elevation ?? {};
1883
+ if (Object.keys(elevationConfig).length > 0) {
1884
+ lines.push(" boxShadow: {");
1885
+ for (const [key, value] of Object.entries(elevationConfig)) {
1886
+ lines.push(
1887
+ ` ${JSON.stringify(`ds-${key}`)}: ${JSON.stringify(value)},`
1888
+ );
1889
+ }
1890
+ lines.push(" },");
1891
+ }
1892
+ const breakpoints = config.layout?.breakpoints ?? {};
1893
+ if (Object.keys(breakpoints).length > 0) {
1894
+ lines.push(" screens: {");
1895
+ for (const [key, value] of Object.entries(breakpoints)) {
1896
+ if (value !== void 0) lines.push(` ${safeKey(key)}: '${value}px',`);
1897
+ }
1898
+ lines.push(" },");
1899
+ }
1900
+ const durations = config.motion?.duration ?? {};
1901
+ if (Object.keys(durations).length > 0) {
1902
+ lines.push(" transitionDuration: {");
1903
+ for (const [key, value] of Object.entries(durations)) {
1904
+ if (value !== void 0) lines.push(` ${safeKey(key)}: '${value}ms',`);
1905
+ }
1906
+ lines.push(" },");
1907
+ }
1908
+ const easing = config.motion?.easing ?? {};
1909
+ if (Object.keys(easing).length > 0) {
1910
+ lines.push(" transitionTimingFunction: {");
1911
+ for (const [key, value] of Object.entries(easing)) {
1912
+ if (value !== void 0)
1913
+ lines.push(` ${safeKey(key)}: ${JSON.stringify(value)},`);
1914
+ }
1915
+ lines.push(" },");
1916
+ }
1917
+ lines.push("};", "");
1918
+ return lines.join("\n");
1919
+ }
1920
+
1921
+ // src/adapters/react/components/button.ts
1922
+ function generateButton(config, rule) {
1923
+ const variants = rule?.allowedVariants ?? [
1924
+ "primary",
1925
+ "secondary",
1926
+ "danger",
1927
+ "ghost"
1928
+ ];
1929
+ const requiresAriaLabel = rule?.a11y?.ariaLabel === "required";
1930
+ const maxWidth = rule?.maxWidth ?? "none";
1931
+ const variantType = variants.map((v) => `"${v}"`).join(" | ");
1932
+ const variantStyles = generateVariantStyles(variants, config);
1933
+ return `/**
1934
+ * Button \u2014 ${config.meta.name}
1935
+ *
1936
+ * Generated by dsforge. Extend via design-system.rules.json or component token overrides.
1937
+ * Allowed variants: ${variants.join(", ")}
1938
+ */
1939
+
1940
+ import React from "react";
1941
+
1942
+ // \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1943
+
1944
+ export type ButtonVariant = ${variantType};
1945
+ export type ButtonSize = "sm" | "md" | "lg";
1946
+
1947
+ export interface ButtonProps
1948
+ extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "aria-label"> {
1949
+ /** Visual style variant */
1950
+ variant?: ButtonVariant;
1951
+ /** Size preset */
1952
+ size?: ButtonSize;
1953
+ /** Shows a loading spinner and disables interaction */
1954
+ loading?: boolean;
1955
+ /** Full-width block button */
1956
+ fullWidth?: boolean;
1957
+ /** Left icon slot */
1958
+ iconLeft?: React.ReactNode;
1959
+ /** Right icon slot */
1960
+ iconRight?: React.ReactNode;
1961
+ /**
1962
+ * Accessible label.${requiresAriaLabel ? "\n * @required Governance rule: buttons must have aria-label." : ""}
1963
+ */
1964
+ "aria-label": string;
1965
+ children?: React.ReactNode;
1966
+ /** Style overrides applied to the button element */
1967
+ style?: React.CSSProperties;
1968
+ }
1969
+
1970
+ // \u2500\u2500\u2500 Size styles \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1971
+
1972
+ const SIZE_STYLES: Record<ButtonSize, React.CSSProperties> = {
1973
+ sm: {
1974
+ fontSize: "var(--font-size-small, 0.875rem)",
1975
+ padding: "var(--spacing-2, 8px) var(--spacing-3, 12px)",
1976
+ gap: "var(--spacing-1, 4px)",
1977
+ },
1978
+ md: {
1979
+ fontSize: "var(--font-size-body, 1rem)",
1980
+ padding: "var(--component-padding-sm, 8px) var(--component-padding-md, 16px)",
1981
+ gap: "var(--spacing-2, 8px)",
1982
+ },
1983
+ lg: {
1984
+ fontSize: "var(--font-size-body, 1rem)",
1985
+ padding: "var(--component-padding-md, 16px) var(--spacing-6, 32px)",
1986
+ gap: "var(--spacing-2, 8px)",
1987
+ },
1988
+ };
1989
+
1990
+ // \u2500\u2500\u2500 Variant styles \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1991
+ ${variantStyles}
1992
+
1993
+ // \u2500\u2500\u2500 Component \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1994
+
1995
+ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
1996
+ function Button(
1997
+ {
1998
+ variant = "primary",
1999
+ size = "md",
2000
+ loading = false,
2001
+ fullWidth = false,
2002
+ iconLeft,
2003
+ iconRight,
2004
+ disabled,
2005
+ children,
2006
+ style,
2007
+ ...props
2008
+ },
2009
+ ref,
2010
+ ) {
2011
+ const isDisabled = disabled ?? loading;
2012
+
2013
+ const baseStyle: React.CSSProperties = {
2014
+ display: "inline-flex",
2015
+ alignItems: "center",
2016
+ justifyContent: "center",
2017
+ fontFamily: "var(--font-family-base, inherit)",
2018
+ fontWeight: "var(--font-weight-label, 500)" as React.CSSProperties["fontWeight"],
2019
+ lineHeight: 1,
2020
+ border: "1px solid transparent",
2021
+ borderRadius: "var(--button-radius, var(--radius-md, 4px))",
2022
+ cursor: isDisabled ? "not-allowed" : "pointer",
2023
+ opacity: isDisabled ? "var(--state-disabled-opacity, 0.4)" : 1,
2024
+ transition: [
2025
+ "background-color var(--duration-fast, 100ms) var(--easing-standard, ease)",
2026
+ "border-color var(--duration-fast, 100ms) var(--easing-standard, ease)",
2027
+ "box-shadow var(--duration-fast, 100ms) var(--easing-standard, ease)",
2028
+ "opacity var(--duration-fast, 100ms)",
2029
+ ].join(", "),
2030
+ maxWidth: "${maxWidth}",
2031
+ width: fullWidth ? "100%" : undefined,
2032
+ outline: "none",
2033
+ textDecoration: "none",
2034
+ whiteSpace: "nowrap",
2035
+ userSelect: "none",
2036
+ ...SIZE_STYLES[size],
2037
+ ...VARIANT_STYLES[variant],
2038
+ ...style,
2039
+ };
2040
+
2041
+ const focusStyle: React.CSSProperties = {
2042
+ boxShadow: [
2043
+ "0 0 0 var(--focus-ring-offset, 2px) var(--color-bg-default, #fff)",
2044
+ "0 0 0 calc(var(--focus-ring-offset, 2px) + var(--focus-ring-width, 2px)) var(--focus-ring-color, #2563eb)",
2045
+ ].join(", "),
2046
+ };
2047
+
2048
+ const [isFocusVisible, setIsFocusVisible] = React.useState(false);
2049
+
2050
+ const handleKeyUp = () => setIsFocusVisible(true);
2051
+ const handleMouseDown = () => setIsFocusVisible(false);
2052
+ const handleBlur = () => setIsFocusVisible(false);
2053
+
2054
+ return (
2055
+ <button
2056
+ ref={ref}
2057
+ disabled={isDisabled}
2058
+ style={{ ...baseStyle, ...(isFocusVisible ? focusStyle : {}) }}
2059
+ onKeyUp={handleKeyUp}
2060
+ onMouseDown={handleMouseDown}
2061
+ onBlur={handleBlur}
2062
+ {...props}
2063
+ >
2064
+ {loading && (
2065
+ <LoadingSpinner
2066
+ size={size === "sm" ? 14 : 16}
2067
+ aria-hidden="true"
2068
+ />
2069
+ )}
2070
+ {!loading && iconLeft && <span aria-hidden="true">{iconLeft}</span>}
2071
+ {children && <span>{children}</span>}
2072
+ {!loading && iconRight && <span aria-hidden="true">{iconRight}</span>}
2073
+ </button>
2074
+ );
2075
+ },
2076
+ );
2077
+
2078
+ Button.displayName = "Button";
2079
+
2080
+ // \u2500\u2500\u2500 Loading spinner \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2081
+
2082
+ function LoadingSpinner({ size, ...props }: { size: number } & React.SVGProps<SVGSVGElement>) {
2083
+ return (
2084
+ <svg
2085
+ width={size}
2086
+ height={size}
2087
+ viewBox="0 0 24 24"
2088
+ fill="none"
2089
+ stroke="currentColor"
2090
+ strokeWidth={2}
2091
+ strokeLinecap="round"
2092
+ style={{
2093
+ animation: "dsforge-spin 0.75s linear infinite",
2094
+ }}
2095
+ {...props}
2096
+ >
2097
+ <path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" />
2098
+ </svg>
2099
+ );
2100
+ }
2101
+
2102
+ // \u2500\u2500\u2500 Keyframe injection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2103
+
2104
+ if (typeof document !== "undefined") {
2105
+ const styleId = "dsforge-button-keyframes";
2106
+ if (!document.getElementById(styleId)) {
2107
+ const style = document.createElement("style");
2108
+ style.id = styleId;
2109
+ style.textContent = "@keyframes dsforge-spin { to { transform: rotate(360deg); } }";
2110
+ document.head.appendChild(style);
2111
+ }
2112
+ }
2113
+ `;
2114
+ }
2115
+ function generateVariantStyles(variants, _config) {
2116
+ const variantMap = {
2117
+ primary: ` primary: {
2118
+ background: "var(--button-bg, var(--color-action, #2563eb))",
2119
+ color: "var(--button-text, var(--color-text-inverse, #ffffff))",
2120
+ borderColor: "var(--button-bg, var(--color-action, #2563eb))",
2121
+ }`,
2122
+ secondary: ` secondary: {
2123
+ background: "var(--color-bg-subtle, #f8fafc)",
2124
+ color: "var(--color-text-primary, #111827)",
2125
+ borderColor: "var(--color-border-default, #e2e8f0)",
2126
+ }`,
2127
+ danger: ` danger: {
2128
+ background: "var(--color-danger-fg, #dc2626)",
2129
+ color: "#ffffff",
2130
+ borderColor: "var(--color-danger-fg, #dc2626)",
2131
+ }`,
2132
+ ghost: ` ghost: {
2133
+ background: "transparent",
2134
+ color: "var(--color-action, #2563eb)",
2135
+ borderColor: "transparent",
2136
+ }`,
2137
+ outline: ` outline: {
2138
+ background: "transparent",
2139
+ color: "var(--color-action, #2563eb)",
2140
+ borderColor: "var(--color-action, #2563eb)",
2141
+ }`
2142
+ };
2143
+ const entries = variants.map((v) => {
2144
+ return variantMap[v] ?? ` ${v}: {}`;
2145
+ });
2146
+ return `
2147
+ const VARIANT_STYLES: Record<ButtonVariant, React.CSSProperties> = {
2148
+ ${entries.join(",\n")},
2149
+ };
2150
+ `;
2151
+ }
2152
+
2153
+ // src/adapters/react/components/input.ts
2154
+ function generateInput(config, rule) {
2155
+ const variants = rule?.allowedVariants ?? ["default", "error", "disabled"];
2156
+ const variantType = variants.map((v) => `"${v}"`).join(" | ");
2157
+ return `/**
2158
+ * Input \u2014 ${config.meta.name}
2159
+ *
2160
+ * Generated by dsforge. Extend via design-system.rules.json or component token overrides.
2161
+ */
2162
+
2163
+ import React from "react";
2164
+
2165
+ // \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2166
+
2167
+ export type InputVariant = ${variantType};
2168
+
2169
+ export interface InputProps
2170
+ extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size" | "style"> {
2171
+ /** Visual/state variant */
2172
+ variant?: InputVariant;
2173
+ /** Label displayed above the input */
2174
+ label?: string;
2175
+ /** Helper text shown below the input */
2176
+ helperText?: string;
2177
+ /** Error message \u2014 also sets variant to "error" when present */
2178
+ errorMessage?: string;
2179
+ /** Size preset */
2180
+ size?: "sm" | "md" | "lg";
2181
+ /** Left adornment (icon or text) */
2182
+ startAdornment?: React.ReactNode;
2183
+ /** Right adornment (icon or text) */
2184
+ endAdornment?: React.ReactNode;
2185
+ /** Accessible label \u2014 required for inputs without a visible label */
2186
+ "aria-label"?: string;
2187
+ /** Style applied to the outer wrapper element */
2188
+ style?: React.CSSProperties;
2189
+ }
2190
+
2191
+ // \u2500\u2500\u2500 Styles \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2192
+
2193
+ const SIZE_PADDING: Record<"sm" | "md" | "lg", string> = {
2194
+ sm: "var(--spacing-1, 4px) var(--spacing-2, 8px)",
2195
+ md: "var(--component-padding-sm, 8px) var(--component-padding-md, 16px)",
2196
+ lg: "var(--component-padding-md, 16px) var(--component-padding-lg, 24px)",
2197
+ };
2198
+
2199
+ const SIZE_FONT: Record<"sm" | "md" | "lg", string> = {
2200
+ sm: "var(--font-size-small, 0.875rem)",
2201
+ md: "var(--font-size-body, 1rem)",
2202
+ lg: "var(--font-size-body, 1rem)",
2203
+ };
2204
+
2205
+ // \u2500\u2500\u2500 Component \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2206
+
2207
+ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
2208
+ function Input(
2209
+ {
2210
+ variant = "default",
2211
+ size = "md",
2212
+ label,
2213
+ helperText,
2214
+ errorMessage,
2215
+ startAdornment,
2216
+ endAdornment,
2217
+ style,
2218
+ id: propId,
2219
+ disabled,
2220
+ ...props
2221
+ },
2222
+ ref,
2223
+ ) {
2224
+ const id = propId ?? React.useId();
2225
+ const errorId = \`\${id}-error\`;
2226
+ const helperId = \`\${id}-helper\`;
2227
+
2228
+ const hasError = Boolean(errorMessage);
2229
+ const isDisabled = disabled ?? variant === "disabled";
2230
+ const resolvedVariant: InputVariant = hasError ? "error" : variant;
2231
+
2232
+ const borderColor =
2233
+ resolvedVariant === "error"
2234
+ ? "var(--color-danger-border, #fca5a5)"
2235
+ : "var(--input-border, var(--color-border-default, #e2e8f0))";
2236
+
2237
+ const wrapperStyle: React.CSSProperties = {
2238
+ display: "flex",
2239
+ flexDirection: "column",
2240
+ gap: "var(--spacing-1, 4px)",
2241
+ fontFamily: "var(--font-family-base, inherit)",
2242
+ opacity: isDisabled ? "var(--state-disabled-opacity, 0.4)" : 1,
2243
+ ...style,
2244
+ };
2245
+
2246
+ const inputWrapperStyle: React.CSSProperties = {
2247
+ display: "flex",
2248
+ alignItems: "center",
2249
+ background: "var(--input-bg, var(--color-bg-default, #ffffff))",
2250
+ border: \`1px solid \${borderColor}\`,
2251
+ borderRadius: "var(--input-radius, var(--radius-md, 4px))",
2252
+ transition: \`border-color var(--duration-fast, 100ms) var(--easing-standard, ease),
2253
+ box-shadow var(--duration-fast, 100ms) var(--easing-standard, ease)\`,
2254
+ cursor: isDisabled ? "not-allowed" : "text",
2255
+ };
2256
+
2257
+ const inputStyle: React.CSSProperties = {
2258
+ flex: 1,
2259
+ padding: SIZE_PADDING[size],
2260
+ fontSize: SIZE_FONT[size],
2261
+ lineHeight: "var(--line-height-body, 1.5)",
2262
+ color: "var(--color-text-primary, #111827)",
2263
+ background: "transparent",
2264
+ border: "none",
2265
+ outline: "none",
2266
+ cursor: isDisabled ? "not-allowed" : "text",
2267
+ };
2268
+
2269
+ const [isFocused, setIsFocused] = React.useState(false);
2270
+
2271
+ const focusShadow = isFocused
2272
+ ? [
2273
+ "0 0 0 var(--focus-ring-offset, 2px) var(--color-bg-default, #fff)",
2274
+ "0 0 0 calc(var(--focus-ring-offset, 2px) + var(--focus-ring-width, 2px)) var(--focus-ring-color, #2563eb)",
2275
+ ].join(", ")
2276
+ : undefined;
2277
+
2278
+ return (
2279
+ <div style={wrapperStyle}>
2280
+ {label && (
2281
+ <label
2282
+ htmlFor={id}
2283
+ style={{
2284
+ fontSize: "var(--font-size-label, 0.875rem)",
2285
+ fontWeight: "var(--font-weight-label, 500)" as React.CSSProperties["fontWeight"],
2286
+ color: "var(--color-text-primary, #111827)",
2287
+ lineHeight: "var(--line-height-label, 1.4)",
2288
+ }}
2289
+ >
2290
+ {label}
2291
+ </label>
2292
+ )}
2293
+
2294
+ <div
2295
+ style={{
2296
+ ...inputWrapperStyle,
2297
+ boxShadow: focusShadow,
2298
+ borderColor: isFocused
2299
+ ? "var(--input-border-focus, var(--color-border-focus, #2563eb))"
2300
+ : borderColor,
2301
+ }}
2302
+ >
2303
+ {startAdornment && (
2304
+ <span
2305
+ aria-hidden="true"
2306
+ style={{
2307
+ display: "flex",
2308
+ alignItems: "center",
2309
+ paddingLeft: "var(--spacing-3, 12px)",
2310
+ color: "var(--color-text-secondary, #6b7280)",
2311
+ flexShrink: 0,
2312
+ }}
2313
+ >
2314
+ {startAdornment}
2315
+ </span>
2316
+ )}
2317
+
2318
+ <input
2319
+ ref={ref}
2320
+ id={id}
2321
+ disabled={isDisabled}
2322
+ aria-invalid={hasError ? "true" : undefined}
2323
+ aria-describedby={
2324
+ [hasError ? errorId : null, helperText ? helperId : null]
2325
+ .filter(Boolean)
2326
+ .join(" ") || undefined
2327
+ }
2328
+ style={inputStyle}
2329
+ onFocus={() => setIsFocused(true)}
2330
+ onBlur={() => setIsFocused(false)}
2331
+ {...props}
2332
+ />
2333
+
2334
+ {endAdornment && (
2335
+ <span
2336
+ aria-hidden="true"
2337
+ style={{
2338
+ display: "flex",
2339
+ alignItems: "center",
2340
+ paddingRight: "var(--spacing-3, 12px)",
2341
+ color: "var(--color-text-secondary, #6b7280)",
2342
+ flexShrink: 0,
2343
+ }}
2344
+ >
2345
+ {endAdornment}
2346
+ </span>
2347
+ )}
2348
+ </div>
2349
+
2350
+ {hasError && (
2351
+ <span
2352
+ id={errorId}
2353
+ role="alert"
2354
+ style={{
2355
+ fontSize: "var(--font-size-caption, 0.75rem)",
2356
+ color: "var(--color-danger-fg, #dc2626)",
2357
+ lineHeight: "var(--line-height-caption, 1.4)",
2358
+ }}
2359
+ >
2360
+ {errorMessage}
2361
+ </span>
2362
+ )}
2363
+
2364
+ {helperText && !hasError && (
2365
+ <span
2366
+ id={helperId}
2367
+ style={{
2368
+ fontSize: "var(--font-size-caption, 0.75rem)",
2369
+ color: "var(--color-text-secondary, #6b7280)",
2370
+ lineHeight: "var(--line-height-caption, 1.4)",
2371
+ }}
2372
+ >
2373
+ {helperText}
2374
+ </span>
2375
+ )}
2376
+ </div>
2377
+ );
2378
+ },
2379
+ );
2380
+
2381
+ Input.displayName = "Input";
2382
+ `;
2383
+ }
2384
+
2385
+ // src/adapters/react/components/card.ts
2386
+ function generateCard(config, rule) {
2387
+ const variants = rule?.allowedVariants ?? ["default", "elevated", "outlined"];
2388
+ const maxWidth = rule?.maxWidth ?? "none";
2389
+ const variantType = variants.map((v) => `"${v}"`).join(" | ");
2390
+ return `/**
2391
+ * Card \u2014 ${config.meta.name}
2392
+ *
2393
+ * Generated by dsforge. Extend via design-system.rules.json or component token overrides.
2394
+ * Composable: use Card.Header, Card.Body, Card.Footer for structured content.
2395
+ */
2396
+
2397
+ import React from "react";
2398
+
2399
+ // \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2400
+
2401
+ export type CardVariant = ${variantType};
2402
+
2403
+ export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
2404
+ /** Visual style variant */
2405
+ variant?: CardVariant;
2406
+ /** Override max-width. Default: ${maxWidth} */
2407
+ maxWidth?: string | number;
2408
+ /** Remove all padding from the card body */
2409
+ noPadding?: boolean;
2410
+ children: React.ReactNode;
2411
+ /** Style overrides applied to the card container */
2412
+ style?: React.CSSProperties;
2413
+ }
2414
+
2415
+ export interface CardSectionProps extends React.HTMLAttributes<HTMLDivElement> {
2416
+ children: React.ReactNode;
2417
+ /** Style overrides applied to the section element */
2418
+ style?: React.CSSProperties;
2419
+ }
2420
+
2421
+ // \u2500\u2500\u2500 Variant styles \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2422
+
2423
+ const VARIANT_STYLES: Record<CardVariant, React.CSSProperties> = {
2424
+ ${generateVariantStylesCard(variants)}
2425
+ };
2426
+
2427
+ // \u2500\u2500\u2500 Card \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2428
+
2429
+ function CardRoot({
2430
+ variant = "default",
2431
+ maxWidth: maxWidthProp,
2432
+ noPadding = false,
2433
+ style,
2434
+ children,
2435
+ ...props
2436
+ }: CardProps) {
2437
+ const computedMaxWidth = maxWidthProp
2438
+ ? typeof maxWidthProp === "number"
2439
+ ? \`\${maxWidthProp}px\`
2440
+ : maxWidthProp
2441
+ : "${maxWidth}";
2442
+
2443
+ const baseStyle: React.CSSProperties = {
2444
+ display: "flex",
2445
+ flexDirection: "column",
2446
+ background: "var(--card-bg, var(--color-bg-default, #ffffff))",
2447
+ borderRadius: "var(--card-radius, var(--radius-lg, 8px))",
2448
+ overflow: "hidden",
2449
+ maxWidth: computedMaxWidth,
2450
+ width: "100%",
2451
+ fontFamily: "var(--font-family-base, inherit)",
2452
+ transition: "box-shadow var(--duration-normal, 200ms) var(--easing-standard, ease)",
2453
+ ...VARIANT_STYLES[variant],
2454
+ ...style,
2455
+ };
2456
+
2457
+ return (
2458
+ <div style={baseStyle} {...props}>
2459
+ {children}
2460
+ </div>
2461
+ );
2462
+ }
2463
+
2464
+ // \u2500\u2500\u2500 Card.Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2465
+
2466
+ function CardHeader({ style, children, ...props }: CardSectionProps) {
2467
+ return (
2468
+ <div
2469
+ style={{
2470
+ display: "flex",
2471
+ alignItems: "center",
2472
+ justifyContent: "space-between",
2473
+ padding: "var(--component-padding-md, 16px)",
2474
+ borderBottom: "1px solid var(--color-border-default, #e2e8f0)",
2475
+ ...style,
2476
+ }}
2477
+ {...props}
2478
+ >
2479
+ {children}
2480
+ </div>
2481
+ );
2482
+ }
2483
+
2484
+ // \u2500\u2500\u2500 Card.Body \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2485
+
2486
+ function CardBody({ style, children, ...props }: CardSectionProps) {
2487
+ return (
2488
+ <div
2489
+ style={{
2490
+ flex: 1,
2491
+ padding: "var(--component-padding-md, 16px)",
2492
+ color: "var(--color-text-primary, #111827)",
2493
+ fontSize: "var(--font-size-body, 1rem)",
2494
+ lineHeight: "var(--line-height-body, 1.5)",
2495
+ ...style,
2496
+ }}
2497
+ {...props}
2498
+ >
2499
+ {children}
2500
+ </div>
2501
+ );
2502
+ }
2503
+
2504
+ // \u2500\u2500\u2500 Card.Footer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2505
+
2506
+ function CardFooter({ style, children, ...props }: CardSectionProps) {
2507
+ return (
2508
+ <div
2509
+ style={{
2510
+ display: "flex",
2511
+ alignItems: "center",
2512
+ gap: "var(--spacing-3, 12px)",
2513
+ padding: "var(--component-padding-md, 16px)",
2514
+ borderTop: "1px solid var(--color-border-default, #e2e8f0)",
2515
+ background: "var(--color-bg-subtle, #f8fafc)",
2516
+ ...style,
2517
+ }}
2518
+ {...props}
2519
+ >
2520
+ {children}
2521
+ </div>
2522
+ );
2523
+ }
2524
+
2525
+ // \u2500\u2500\u2500 Composite export \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2526
+
2527
+ export const Card = Object.assign(CardRoot, {
2528
+ Header: CardHeader,
2529
+ Body: CardBody,
2530
+ Footer: CardFooter,
2531
+ });
2532
+ `;
2533
+ }
2534
+ function generateVariantStylesCard(variants) {
2535
+ const map = {
2536
+ default: ` default: {
2537
+ border: "1px solid var(--color-border-default, #e2e8f0)",
2538
+ boxShadow: "none",
2539
+ }`,
2540
+ elevated: ` elevated: {
2541
+ border: "1px solid transparent",
2542
+ boxShadow: "var(--shadow-2, 0 1px 3px 0 rgb(0 0 0 / 0.10))",
2543
+ }`,
2544
+ outlined: ` outlined: {
2545
+ border: "2px solid var(--color-border-strong, #94a3b8)",
2546
+ boxShadow: "none",
2547
+ }`,
2548
+ interactive: ` interactive: {
2549
+ border: "1px solid var(--color-border-default, #e2e8f0)",
2550
+ boxShadow: "var(--shadow-1, 0 1px 2px 0 rgb(0 0 0 / 0.05))",
2551
+ cursor: "pointer",
2552
+ }`
2553
+ };
2554
+ return variants.map((v) => map[v] ?? ` ${v}: {}`).join(",\n");
2555
+ }
2556
+
2557
+ // src/adapters/react/theme-provider.ts
2558
+ function generateThemeProvider(config) {
2559
+ const themeNames = Object.keys(config.themes ?? { light: {}, dark: {} });
2560
+ const defaultTheme = themeNames.includes("light") ? "light" : themeNames[0] ?? "light";
2561
+ const themeType = themeNames.map((t) => `"${t}"`).join(" | ");
2562
+ return `/**
2563
+ * ThemeProvider \u2014 ${config.meta.name}
2564
+ *
2565
+ * Applies the active theme by setting data-theme on the root element.
2566
+ * Consumers wrap their app (or a subtree) in ThemeProvider.
2567
+ *
2568
+ * Usage:
2569
+ * import "@${config.meta.name}/tokens/base.css";
2570
+ * import "@${config.meta.name}/tokens/light.css"; // or dark.css
2571
+ * import { ThemeProvider } from "@${config.meta.name}";
2572
+ *
2573
+ * <ThemeProvider theme="light">
2574
+ * <App />
2575
+ * </ThemeProvider>
2576
+ */
2577
+
2578
+ import React from "react";
2579
+
2580
+ // \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2581
+
2582
+ export type ThemeName = ${themeType};
2583
+
2584
+ export interface ThemeContextValue {
2585
+ theme: ThemeName;
2586
+ setTheme: (theme: ThemeName) => void;
2587
+ }
2588
+
2589
+ export interface ThemeProviderProps {
2590
+ /** Initial theme. Defaults to "${defaultTheme}". */
2591
+ theme?: ThemeName;
2592
+ /** Called when setTheme is invoked \u2014 use to persist theme preference. */
2593
+ onThemeChange?: (theme: ThemeName) => void;
2594
+ children: React.ReactNode;
2595
+ }
2596
+
2597
+ // \u2500\u2500\u2500 Context \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2598
+
2599
+ export const ThemeContext = React.createContext<ThemeContextValue>({
2600
+ theme: "${defaultTheme}",
2601
+ setTheme: () => undefined,
2602
+ });
2603
+
2604
+ /**
2605
+ * Hook to read and change the current theme.
2606
+ * Must be used inside a <ThemeProvider>.
2607
+ */
2608
+ export function useTheme(): ThemeContextValue {
2609
+ return React.useContext(ThemeContext);
2610
+ }
2611
+
2612
+ // \u2500\u2500\u2500 Provider \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2613
+
2614
+ export function ThemeProvider({
2615
+ theme: initialTheme = "${defaultTheme}",
2616
+ onThemeChange,
2617
+ children,
2618
+ }: ThemeProviderProps) {
2619
+ const [theme, setThemeState] = React.useState<ThemeName>(initialTheme);
2620
+
2621
+ React.useEffect(() => {
2622
+ setThemeState(initialTheme);
2623
+ }, [initialTheme]);
2624
+
2625
+ const setTheme = React.useCallback(
2626
+ (next: ThemeName) => {
2627
+ setThemeState(next);
2628
+ onThemeChange?.(next);
2629
+ },
2630
+ [onThemeChange],
2631
+ );
2632
+
2633
+ return (
2634
+ <ThemeContext.Provider value={{ theme, setTheme }}>
2635
+ <div data-theme={theme} style={{ display: "contents" }}>
2636
+ {children}
2637
+ </div>
2638
+ </ThemeContext.Provider>
2639
+ );
2640
+ }
2641
+ `;
2642
+ }
2643
+ function generateComponentIndex(config, componentNames) {
2644
+ const lines = [
2645
+ `/**`,
2646
+ ` * ${config.meta.name} \u2014 component library`,
2647
+ ` * Generated by dsforge v${config.meta.version}. Do not edit manually.`,
2648
+ ` *`,
2649
+ ` * Usage:`,
2650
+ ` * import { Button, Input, Card, ThemeProvider } from "${config.meta.npmScope ?? "@myorg"}/${config.meta.name}";`,
2651
+ ` */`,
2652
+ ""
2653
+ ];
2654
+ for (const name of componentNames) {
2655
+ lines.push(`export * from "./${name}";`);
2656
+ }
2657
+ lines.push(`export * from "./ThemeProvider";`);
2658
+ lines.push("");
2659
+ return lines.join("\n");
2660
+ }
2661
+
2662
+ // src/generators/metadata/generator.ts
2663
+ var COMPONENT_DEFAULTS = {
2664
+ button: {
2665
+ description: "Triggers an action or navigation. The primary interactive element.",
2666
+ role: "action-trigger",
2667
+ hierarchyLevel: "primary",
2668
+ interactionModel: "synchronous",
2669
+ layoutImpact: "inline",
2670
+ destructive: false,
2671
+ sizes: ["sm", "md", "lg"]
2672
+ },
2673
+ input: {
2674
+ description: "Accepts user text input. Use with a label for accessibility.",
2675
+ role: "data-entry",
2676
+ hierarchyLevel: "primary",
2677
+ interactionModel: "synchronous",
2678
+ layoutImpact: "block",
2679
+ destructive: false,
2680
+ sizes: ["sm", "md", "lg"]
2681
+ },
2682
+ card: {
2683
+ description: "Groups related content with optional header, body, and footer slots.",
2684
+ role: "content-container",
2685
+ hierarchyLevel: "utility",
2686
+ interactionModel: "none",
2687
+ layoutImpact: "block",
2688
+ destructive: false
2689
+ },
2690
+ badge: {
2691
+ description: "Compact label for status, categories, or counts. Display-only \u2014 not interactive.",
2692
+ role: "status-indicator",
2693
+ hierarchyLevel: "utility",
2694
+ interactionModel: "none",
2695
+ layoutImpact: "inline",
2696
+ destructive: false,
2697
+ sizes: ["sm", "md", "lg"]
2698
+ },
2699
+ checkbox: {
2700
+ description: "Binary toggle for boolean values. Supports indeterminate state for partial selections.",
2701
+ role: "data-entry",
2702
+ hierarchyLevel: "primary",
2703
+ interactionModel: "synchronous",
2704
+ layoutImpact: "inline",
2705
+ destructive: false,
2706
+ sizes: ["sm", "md", "lg"]
2707
+ },
2708
+ radio: {
2709
+ description: "Single selection within a mutually exclusive group. Always use inside RadioGroup.",
2710
+ role: "data-entry",
2711
+ hierarchyLevel: "primary",
2712
+ interactionModel: "synchronous",
2713
+ layoutImpact: "inline",
2714
+ destructive: false,
2715
+ sizes: ["sm", "md", "lg"]
2716
+ },
2717
+ select: {
2718
+ description: "Dropdown picker for selecting from a list of options. Wraps native <select> for accessibility.",
2719
+ role: "data-entry",
2720
+ hierarchyLevel: "primary",
2721
+ interactionModel: "synchronous",
2722
+ layoutImpact: "block",
2723
+ destructive: false,
2724
+ sizes: ["sm", "md", "lg"]
2725
+ },
2726
+ toast: {
2727
+ description: "Feedback messages for user actions. Alert is inline; Toast is an overlay with auto-dismiss.",
2728
+ role: "feedback",
2729
+ hierarchyLevel: "utility",
2730
+ interactionModel: "asynchronous",
2731
+ layoutImpact: "overlay",
2732
+ destructive: false
2733
+ },
2734
+ spinner: {
2735
+ description: "Loading indicator for async operations. Use with an accessible label for screen readers.",
2736
+ role: "loading-indicator",
2737
+ hierarchyLevel: "utility",
2738
+ interactionModel: "asynchronous",
2739
+ layoutImpact: "inline",
2740
+ destructive: false,
2741
+ sizes: ["xs", "sm", "md", "lg", "xl"]
2742
+ }
2743
+ };
2744
+ function buildComponentMetadata(componentName, rule, config) {
2745
+ const defaults = COMPONENT_DEFAULTS[componentName.toLowerCase()] ?? {};
2746
+ const variants = rule.allowedVariants ?? ["default"];
2747
+ const requiredProps = rule.requiredProps ?? [];
2748
+ const tokens = {};
2749
+ for (const [tokenName] of Object.entries(rule.tokens ?? {})) {
2750
+ tokens[tokenName] = `--${tokenName}`;
2751
+ }
2752
+ const meta = {
2753
+ component: pascalCase(componentName),
2754
+ version: config.meta.version,
2755
+ description: defaults.description ?? `A ${componentName} component.`,
2756
+ role: defaults.role ?? "ui-element",
2757
+ hierarchyLevel: defaults.hierarchyLevel ?? "utility",
2758
+ interactionModel: defaults.interactionModel ?? "none",
2759
+ layoutImpact: defaults.layoutImpact ?? "inline",
2760
+ destructive: componentName.toLowerCase().includes("delete") || variants.includes("danger"),
2761
+ allowedVariants: variants,
2762
+ defaultVariant: variants[0] ?? "default",
2763
+ requiredProps,
2764
+ optionalProps: buildOptionalProps(componentName, defaults),
2765
+ tokens,
2766
+ accessibilityContract: {
2767
+ keyboard: rule.a11y?.keyboard ?? true,
2768
+ focusRing: rule.a11y?.focusRing ?? true,
2769
+ ariaLabel: rule.a11y?.ariaLabel ?? "optional",
2770
+ ...rule.a11y?.role ? { role: rule.a11y.role } : {}
2771
+ },
2772
+ governanceRules: {
2773
+ ...rule.maxWidth ? { maxWidth: rule.maxWidth } : {},
2774
+ ...rule.allowedRadius ? { allowedRadius: rule.allowedRadius } : {},
2775
+ ...rule.allowedShadows ? { allowedShadows: rule.allowedShadows } : {},
2776
+ ...rule.colorPalette ? { colorPalette: rule.colorPalette } : {}
2777
+ }
2778
+ };
2779
+ if (defaults.sizes) {
2780
+ meta.sizes = defaults.sizes;
2781
+ }
2782
+ return meta;
2783
+ }
2784
+ function buildOptionalProps(componentName, _defaults) {
2785
+ const common = ["className", "style", "id", "data-testid"];
2786
+ const byComponent = {
2787
+ button: [
2788
+ "size",
2789
+ "loading",
2790
+ "disabled",
2791
+ "fullWidth",
2792
+ "iconLeft",
2793
+ "iconRight",
2794
+ "onClick"
2795
+ ],
2796
+ input: [
2797
+ "size",
2798
+ "disabled",
2799
+ "label",
2800
+ "helperText",
2801
+ "errorMessage",
2802
+ "placeholder",
2803
+ "startAdornment",
2804
+ "endAdornment",
2805
+ "onChange"
2806
+ ],
2807
+ card: ["maxWidth", "noPadding", "onClick"],
2808
+ badge: ["size", "dot"],
2809
+ checkbox: ["size", "disabled", "label", "helperText", "indeterminate", "checked", "onChange"],
2810
+ radio: ["size", "disabled", "label", "value", "onChange"],
2811
+ select: ["size", "disabled", "label", "helperText", "errorMessage", "placeholder", "options", "fullWidth", "onChange"],
2812
+ toast: ["variant", "title", "dismissible", "duration", "onDismiss"],
2813
+ spinner: ["size", "variant", "label"]
2814
+ };
2815
+ return [...byComponent[componentName.toLowerCase()] ?? [], ...common];
2816
+ }
2817
+ function pascalCase(str) {
2818
+ return str.charAt(0).toUpperCase() + str.slice(1);
2819
+ }
2820
+ function buildIndexMetadata(config, componentNames, tokenCount) {
2821
+ return {
2822
+ name: config.meta.name,
2823
+ version: config.meta.version,
2824
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2825
+ components: componentNames.map(pascalCase),
2826
+ tokenCount,
2827
+ themes: Object.keys(config.themes ?? {})
2828
+ };
2829
+ }
2830
+ function generateMetadata(config, rules, tokenCount) {
2831
+ const files = [];
2832
+ const componentNames = Object.keys(rules);
2833
+ for (const [componentName, rule] of Object.entries(rules)) {
2834
+ const metadata = buildComponentMetadata(componentName, rule, config);
2835
+ files.push({
2836
+ filename: `${componentName}.json`,
2837
+ content: JSON.stringify(metadata, null, 2)
2838
+ });
2839
+ }
2840
+ const index = buildIndexMetadata(config, componentNames, tokenCount);
2841
+ files.push({
2842
+ filename: "index.json",
2843
+ content: JSON.stringify(index, null, 2)
2844
+ });
2845
+ return files;
2846
+ }
2847
+
2848
+ // src/adapters/react/docs/mdx.ts
2849
+ function generateComponentDoc(metadata, config) {
2850
+ const {
2851
+ component,
2852
+ description,
2853
+ allowedVariants,
2854
+ sizes,
2855
+ accessibilityContract
2856
+ } = metadata;
2857
+ const inlineContentByComponent = {
2858
+ Button: (v) => v.charAt(0).toUpperCase() + v.slice(1)
2859
+ };
2860
+ const childContent = inlineContentByComponent[component] ?? (() => "");
2861
+ const variantStories = allowedVariants.map((v) => `<${component} variant="${v}" aria-label="${v} example">${childContent(v)}</${component}>`).join("\n");
2862
+ const sizeStories = sizes ? sizes.map((s) => `<${component} size="${s}" aria-label="${s} size example">${childContent(s)}</${component}>`).join("\n") : "";
2863
+ const requiredPropsNote = metadata.requiredProps.length > 0 ? `
2864
+
2865
+ > **Required props:** ${metadata.requiredProps.map((p) => `\`${p}\``).join(", ")}` : "";
2866
+ return `---
2867
+ title: ${component}
2868
+ description: "${description}"
2869
+ ---
2870
+
2871
+ import { ${component} } from '../components/${component}';
2872
+ import { ThemeProvider } from '../components/ThemeProvider';
2873
+
2874
+ # ${component}
2875
+
2876
+ ${description}${requiredPropsNote}
2877
+
2878
+ ## Overview
2879
+
2880
+ | Property | Value |
2881
+ |---|---|
2882
+ | Role | \`${metadata.role}\` |
2883
+ | Interaction | \`${metadata.interactionModel}\` |
2884
+ | Layout impact | \`${metadata.layoutImpact}\` |
2885
+ | Destructive | \`${metadata.destructive}\` |
2886
+ | Default variant | \`${metadata.defaultVariant}\` |
2887
+
2888
+ ## Variants
2889
+
2890
+ All allowed variants as defined in \`design-system.rules.json\`:
2891
+
2892
+ \`\`\`tsx
2893
+ ${allowedVariants.map((v) => `<${component} variant="${v}" aria-label="${v}">${childContent(v)}</${component}>`).join("\n")}
2894
+ \`\`\`
2895
+
2896
+ <ThemeProvider theme="light">
2897
+ <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
2898
+ ${variantStories}
2899
+ </div>
2900
+ </ThemeProvider>
2901
+
2902
+ ${sizeStories ? `## Sizes
2903
+
2904
+ \`\`\`tsx
2905
+ ${sizes.map((s) => `<${component} size="${s}" aria-label="${s}">${childContent(s)}</${component}>`).join("\n")}
2906
+ \`\`\`
2907
+
2908
+ <ThemeProvider theme="light">
2909
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
2910
+ ${sizeStories}
2911
+ </div>
2912
+ </ThemeProvider>
2913
+ ` : ""}
2914
+
2915
+ ## States
2916
+
2917
+ \`\`\`tsx
2918
+ ${component === "Button" ? `{/* Loading */}
2919
+ <Button variant="primary" loading aria-label="Saving">Saving...</Button>
2920
+
2921
+ ` : ""}{/* Disabled */}
2922
+ <${component} disabled aria-label="Disabled example">${childContent("disabled")}</${component}>
2923
+ \`\`\`
2924
+
2925
+ ## Accessibility
2926
+
2927
+ | Requirement | Status |
2928
+ |---|---|
2929
+ | Keyboard navigation | ${accessibilityContract.keyboard ? "\u2705 Required" : "\u26A0\uFE0F Optional"} |
2930
+ | Focus ring | ${accessibilityContract.focusRing ? "\u2705 Required" : "\u26A0\uFE0F Optional"} |
2931
+ | aria-label | ${accessibilityContract.ariaLabel === "required" ? "\u2705 Required" : "\u26A0\uFE0F Optional"} |
2932
+ ${accessibilityContract.role ? `| ARIA role | \`${accessibilityContract.role}\` |` : ""}
2933
+
2934
+ ## Tokens
2935
+
2936
+ The following CSS custom properties control the appearance of this component.
2937
+ Override these in \`design-system.rules.json\` under \`${component.toLowerCase()}.tokens\`.
2938
+
2939
+ | Token | CSS Property |
2940
+ |---|---|
2941
+ ${Object.entries(metadata.tokens).map(([name, cssVar2]) => `| \`${name}\` | \`${cssVar2}\` |`).join("\n")}
2942
+
2943
+ ## Usage
2944
+
2945
+ \`\`\`tsx
2946
+ import { ${component} } from "${config.meta.npmScope ?? "@myorg"}/${config.meta.name}";
2947
+ import "${config.meta.npmScope ?? "@myorg"}/${config.meta.name}/tokens/light.css";
2948
+
2949
+ // Basic usage
2950
+ <${component}${metadata.requiredProps.includes("aria-label") ? `
2951
+ aria-label="${component}"` : ""}${allowedVariants[0] ? `
2952
+ variant="${allowedVariants[0]}"` : ""}
2953
+ >
2954
+ ${childContent(allowedVariants[0] ?? "")}
2955
+ </${component}>
2956
+ \`\`\`
2957
+ `;
2958
+ }
2959
+ function generateIndexDoc(config, componentNames) {
2960
+ return `---
2961
+ title: ${config.meta.name}
2962
+ description: "${config.meta.description ?? `${config.meta.name} design system`}"
2963
+ ---
2964
+
2965
+ # ${config.meta.name}
2966
+
2967
+ > ${config.meta.description ?? "A generated design system."}
2968
+
2969
+ Generated by **dsforge** \u2014 config-driven, AI-native, publish-ready.
2970
+
2971
+ ## Installation
2972
+
2973
+ \`\`\`bash
2974
+ npm install ${config.meta.npmScope ?? "@myorg"}/${config.meta.name}
2975
+ \`\`\`
2976
+
2977
+ ## Setup
2978
+
2979
+ \`\`\`tsx
2980
+ // 1. Import base tokens (spacing, typography, radius, motion)
2981
+ import "${config.meta.npmScope ?? "@myorg"}/${config.meta.name}/tokens/base.css";
2982
+
2983
+ // 2. Import your theme (light or dark)
2984
+ import "${config.meta.npmScope ?? "@myorg"}/${config.meta.name}/tokens/light.css";
2985
+
2986
+ // 3. Wrap your app
2987
+ import { ThemeProvider } from "${config.meta.npmScope ?? "@myorg"}/${config.meta.name}";
2988
+
2989
+ function App() {
2990
+ return (
2991
+ <ThemeProvider theme="light">
2992
+ {/* your app */}
2993
+ </ThemeProvider>
2994
+ );
2995
+ }
2996
+ \`\`\`
2997
+
2998
+ ## Components
2999
+
3000
+ ${componentNames.map((name) => `- [${name.charAt(0).toUpperCase() + name.slice(1)}](./${name}.mdx)`).join("\n")}
3001
+
3002
+ ## Themes
3003
+
3004
+ ${Object.keys(config.themes ?? {}).map((t) => `- \`${t}\` \u2014 import \`tokens/${t}.css\``).join("\n")}
3005
+
3006
+ ## Token Architecture
3007
+
3008
+ This design system uses a 3-tier token architecture:
3009
+
3010
+ | Layer | Purpose | Example |
3011
+ |---|---|---|
3012
+ | **Global** | Raw values | \`blue-600: #2563eb\` |
3013
+ | **Semantic** | Intent-named | \`color-action: {global.blue-600}\` |
3014
+ | **Component** | Component-specific | \`button-bg: {semantic.color-action}\` |
3015
+
3016
+ Themes override only the **semantic layer**, making brand swapping and dark mode a single file change.
3017
+ `;
3018
+ }
3019
+ function generateDocs(config, rules, metadataMap) {
3020
+ const files = [];
3021
+ files.push({
3022
+ filename: "index.mdx",
3023
+ content: generateIndexDoc(config, Object.keys(rules))
3024
+ });
3025
+ for (const componentName of Object.keys(rules)) {
3026
+ const metadata = metadataMap[componentName];
3027
+ if (!metadata) continue;
3028
+ files.push({
3029
+ filename: `${componentName}.mdx`,
3030
+ content: generateComponentDoc(metadata, config)
3031
+ });
3032
+ }
3033
+ return files;
3034
+ }
3035
+
3036
+ // src/generators/package/emitter.ts
3037
+ function generatePackageJson(config, componentNames) {
3038
+ const scope = config.meta.npmScope ?? "@myorg";
3039
+ const name = `${scope}/${config.meta.name}`;
3040
+ const pkg = {
3041
+ name,
3042
+ version: config.meta.version,
3043
+ description: config.meta.description ?? `${config.meta.name} design system`,
3044
+ keywords: ["design-system", "tokens", "react", "typescript", "components"],
3045
+ license: "MIT",
3046
+ type: "module",
3047
+ main: "./dist/index.js",
3048
+ types: "./dist/index.d.ts",
3049
+ exports: {
3050
+ ".": {
3051
+ import: "./dist/index.js",
3052
+ types: "./dist/index.d.ts"
3053
+ },
3054
+ "./tokens/base.css": "./tokens/base.css",
3055
+ ...Object.fromEntries(
3056
+ Object.keys({}).concat(["light", "dark"]).map((t) => [`./tokens/${t}.css`, `./tokens/${t}.css`])
3057
+ ),
3058
+ "./tokens": "./tokens/tokens.js",
3059
+ "./tailwind": "./tokens/tailwind.js",
3060
+ "./metadata": "./metadata/index.json",
3061
+ ...Object.fromEntries(
3062
+ componentNames.map((c) => [`./metadata/${c}`, `./metadata/${c}.json`])
3063
+ )
3064
+ },
3065
+ files: ["dist", "tokens", "metadata", "docs", "CHANGELOG.md"],
3066
+ scripts: {
3067
+ build: "tsc",
3068
+ prepublishOnly: "npm run build"
3069
+ },
3070
+ devDependencies: {
3071
+ "@types/react": "^18.0.0",
3072
+ "@types/react-dom": "^18.0.0",
3073
+ typescript: "^5.0.0"
3074
+ },
3075
+ peerDependencies: {
3076
+ react: ">=17.0.0",
3077
+ "react-dom": ">=17.0.0"
3078
+ },
3079
+ peerDependenciesMeta: {
3080
+ react: { optional: false },
3081
+ "react-dom": { optional: false }
3082
+ },
3083
+ engines: {
3084
+ node: ">=18.0.0"
3085
+ },
3086
+ dsforge: {
3087
+ generatedBy: "dsforge",
3088
+ configVersion: config.meta.version,
3089
+ preset: config.meta.preset ?? "comfortable",
3090
+ components: componentNames,
3091
+ themes: []
3092
+ }
3093
+ };
3094
+ return JSON.stringify(pkg, null, 2);
3095
+ }
3096
+ function generateTsConfig() {
3097
+ return JSON.stringify(
3098
+ {
3099
+ compilerOptions: {
3100
+ target: "ES2020",
3101
+ module: "NodeNext",
3102
+ moduleResolution: "NodeNext",
3103
+ lib: ["ES2020", "DOM"],
3104
+ outDir: "./dist",
3105
+ rootDir: "./src",
3106
+ declaration: true,
3107
+ declarationMap: true,
3108
+ sourceMap: true,
3109
+ strict: true,
3110
+ esModuleInterop: true,
3111
+ skipLibCheck: true,
3112
+ jsx: "react-jsx"
3113
+ },
3114
+ include: ["src/**/*"],
3115
+ exclude: ["node_modules", "dist"]
3116
+ },
3117
+ null,
3118
+ 2
3119
+ );
3120
+ }
3121
+ function generateReadme(config, componentNames) {
3122
+ const scope = config.meta.npmScope ?? "@myorg";
3123
+ const pkgName = `${scope}/${config.meta.name}`;
3124
+ const themeNames = ["light", "dark"];
3125
+ return `# ${config.meta.name}
3126
+
3127
+ > ${config.meta.description ?? "A design system generated by dsforge."}
3128
+
3129
+ [![npm version](https://badge.fury.io/js/${encodeURIComponent(pkgName)}.svg)](https://www.npmjs.com/package/${pkgName})
3130
+
3131
+ ---
3132
+
3133
+ ## Installation
3134
+
3135
+ \`\`\`bash
3136
+ npm install ${pkgName}
3137
+ \`\`\`
3138
+
3139
+ ## Quick start
3140
+
3141
+ \`\`\`tsx
3142
+ // 1. Import base tokens (once, in your app root)
3143
+ import "${pkgName}/tokens/base.css";
3144
+ import "${pkgName}/tokens/light.css";
3145
+
3146
+ // 2. Wrap your app
3147
+ import { ThemeProvider, Button } from "${pkgName}";
3148
+
3149
+ function App() {
3150
+ return (
3151
+ <ThemeProvider theme="light">
3152
+ <Button variant="primary" aria-label="Get started">
3153
+ Get started
3154
+ </Button>
3155
+ </ThemeProvider>
3156
+ );
3157
+ }
3158
+ \`\`\`
3159
+
3160
+ ## Components
3161
+
3162
+ ${componentNames.map(
3163
+ (c) => `- **${c.charAt(0).toUpperCase() + c.slice(1)}** \u2014 see \`metadata/${c}.json\` for contract`
3164
+ ).join("\n")}
3165
+
3166
+ ## Themes
3167
+
3168
+ ${themeNames.map((t) => `- \`${t}\` \u2014 import \`${pkgName}/tokens/${t}.css\``).join("\n")}
3169
+
3170
+ ### Theme switching
3171
+
3172
+ \`\`\`tsx
3173
+ import { useTheme } from "${pkgName}";
3174
+
3175
+ function ThemeToggle() {
3176
+ const { theme, setTheme } = useTheme();
3177
+ return (
3178
+ <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
3179
+ Toggle theme
3180
+ </button>
3181
+ );
3182
+ }
3183
+ \`\`\`
3184
+
3185
+ ## Using with Tailwind
3186
+
3187
+ \`\`\`js
3188
+ // tailwind.config.js
3189
+ const ds = require("${pkgName}/tailwind");
3190
+ module.exports = {
3191
+ theme: { extend: ds },
3192
+ };
3193
+ \`\`\`
3194
+
3195
+ ## Without React (CSS tokens only)
3196
+
3197
+ \`\`\`html
3198
+ <link rel="stylesheet" href="node_modules/${pkgName}/tokens/base.css" />
3199
+ <link rel="stylesheet" href="node_modules/${pkgName}/tokens/light.css" />
3200
+
3201
+ <button class="my-button">Click me</button>
3202
+
3203
+ <style>
3204
+ .my-button {
3205
+ background: var(--color-action);
3206
+ color: var(--color-text-inverse);
3207
+ border-radius: var(--radius-md);
3208
+ padding: var(--component-padding-sm) var(--component-padding-md);
3209
+ }
3210
+ </style>
3211
+ \`\`\`
3212
+
3213
+ ## Token customization
3214
+
3215
+ All design tokens are defined in \`design-system.config.json\`. After editing,
3216
+ run \`dsforge generate\` to regenerate the package.
3217
+
3218
+ \`\`\`json
3219
+ // design-system.config.json (excerpt)
3220
+ {
3221
+ "tokens": {
3222
+ "global": {
3223
+ "color-brand-500": "#2563eb"
3224
+ },
3225
+ "semantic": {
3226
+ "color-action": "{global.color-brand-500}"
3227
+ }
3228
+ }
3229
+ }
3230
+ \`\`\`
3231
+
3232
+ Token references use \`{tier.name}\` syntax to alias values across tiers:
3233
+ global \u2192 semantic \u2192 component. Changing a global value propagates through
3234
+ every semantic and component token that references it.
3235
+
3236
+ ## AI tool integration
3237
+
3238
+ The \`metadata/\` directory contains machine-readable component contracts.
3239
+ AI coding assistants (Copilot, Cursor, Claude Code) can read these to
3240
+ generate UI that respects your governance rules automatically.
3241
+
3242
+ \`\`\`json
3243
+ // ${pkgName}/metadata/button.json
3244
+ {
3245
+ "component": "Button",
3246
+ "allowedVariants": ["primary", "secondary", "danger", "ghost"],
3247
+ "requiredProps": ["aria-label"],
3248
+ "accessibilityContract": { "keyboard": true, "focusRing": true }
3249
+ }
3250
+ \`\`\`
3251
+
3252
+ ---
3253
+
3254
+ Generated by [dsforge](https://github.com/nghitrum/dsforge) v${config.meta.version}.
3255
+ `;
3256
+ }
3257
+ function generateChangelog(config) {
3258
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3259
+ return `# Changelog
3260
+
3261
+ All notable changes to \`${config.meta.name}\` are documented here.
3262
+ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
3263
+
3264
+ ## [${config.meta.version}] \u2014 ${date}
3265
+
3266
+ ### Added
3267
+
3268
+ - Initial release generated by dsforge
3269
+ - Components: see \`metadata/index.json\` for full list
3270
+ - Token architecture: global \u2192 semantic \u2192 component (3-tier)
3271
+ - Themes: light, dark
3272
+ - CSS custom property output for all tokens
3273
+ - Tailwind theme extension
3274
+ - AI-consumable metadata per component
3275
+
3276
+ ---
3277
+
3278
+ _To add a new entry: run \`dsforge diff --from <prev-version>\` and paste the output here._
3279
+ `;
3280
+ }
3281
+ export {
3282
+ ColorConfigSchema,
3283
+ DesignSystemConfigSchema,
3284
+ ElevationConfigSchema,
3285
+ LayoutConfigSchema,
3286
+ MotionConfigSchema,
3287
+ OutputConfigSchema,
3288
+ RadiusConfigSchema,
3289
+ RulesConfigSchema,
3290
+ SUPPORTED_TARGETS,
3291
+ SpacingConfigSchema,
3292
+ StatesConfigSchema,
3293
+ ThemesConfigSchema,
3294
+ TokenLayersSchema,
3295
+ TokenResolver,
3296
+ TypographyConfigSchema,
3297
+ buildInitialConfig,
3298
+ buildInitialRules,
3299
+ buildThemeCss,
3300
+ checkContrast,
3301
+ emitBaseCss,
3302
+ emitJsTokens,
3303
+ emitTailwindConfig,
3304
+ emitThemeCss,
3305
+ extractRefs,
3306
+ generateButton,
3307
+ generateCard,
3308
+ generateChangelog,
3309
+ generateComponentIndex,
3310
+ generateCssFiles,
3311
+ generateDocs,
3312
+ generateInput,
3313
+ generateMetadata,
3314
+ generatePackageJson,
3315
+ generateReadme,
3316
+ generateThemeProvider,
3317
+ generateTsConfig,
3318
+ hasRefs,
3319
+ hexContrastRatio,
3320
+ isHexColor,
3321
+ resolveTokens,
3322
+ suggestContrastFix,
3323
+ validateConfig
3324
+ };