@fragments-sdk/cli 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/bin.js +18 -13
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{chunk-MUZ6CM66.js → chunk-5JNME72P.js} +3 -3
  4. package/dist/{chunk-MUZ6CM66.js.map → chunk-5JNME72P.js.map} +1 -1
  5. package/dist/{chunk-XHNKNI6J.js → chunk-AW7MWOUH.js} +9 -1
  6. package/dist/chunk-AW7MWOUH.js.map +1 -0
  7. package/dist/{chunk-LY2CFFPY.js → chunk-FYIYMXGA.js} +2 -2
  8. package/dist/{chunk-3OTEW66K.js → chunk-LDKNZ55O.js} +4 -4
  9. package/dist/{chunk-BSCG3IP7.js → chunk-NOTYONHY.js} +2 -2
  10. package/dist/{chunk-ACIDZOYW.js → chunk-ODXQAQQX.js} +21 -8
  11. package/dist/chunk-ODXQAQQX.js.map +1 -0
  12. package/dist/{chunk-PMGI7ATF.js → chunk-OZQ7Z6C3.js} +31 -2
  13. package/dist/chunk-OZQ7Z6C3.js.map +1 -0
  14. package/dist/{core-DWKLGY4N.js → core-F3VT277E.js} +5 -3
  15. package/dist/{generate-3LBZANQ3.js → generate-PNIUR75D.js} +4 -4
  16. package/dist/index.d.ts +18 -0
  17. package/dist/index.js +6 -6
  18. package/dist/{init-NKIUCYTG.js → init-ON6WYG66.js} +4 -4
  19. package/dist/mcp-bin.js +8 -3
  20. package/dist/mcp-bin.js.map +1 -1
  21. package/dist/scan-E6U644RS.js +12 -0
  22. package/dist/{service-QSZMZJBJ.js → service-U7AR2PC2.js} +4 -4
  23. package/dist/{static-viewer-MIPGZ4Z7.js → static-viewer-QL2SCWYB.js} +4 -4
  24. package/dist/{test-ZCTR4LBB.js → test-PBPKJ4WJ.js} +3 -3
  25. package/dist/{tokens-5JQ5IOR2.js → tokens-4J4PRIGT.js} +5 -5
  26. package/dist/{viewer-D7QC4GM2.js → viewer-6VCZMA3T.js} +13 -13
  27. package/package.json +1 -1
  28. package/src/bin.ts +7 -1
  29. package/src/build.ts +16 -0
  30. package/src/core/index.ts +4 -0
  31. package/src/core/parser.ts +54 -1
  32. package/src/core/schema.ts +11 -0
  33. package/src/core/types.ts +27 -0
  34. package/src/mcp/server.ts +11 -1
  35. package/src/migrate/bin.ts +7 -1
  36. package/src/migrate/report.ts +1 -1
  37. package/src/service/report.ts +1 -1
  38. package/src/theme/__tests__/generator.test.ts +412 -0
  39. package/src/theme/__tests__/presets.test.ts +169 -0
  40. package/src/theme/__tests__/schema.test.ts +463 -0
  41. package/src/theme/__tests__/serializer.test.ts +326 -0
  42. package/src/theme/generator.ts +355 -0
  43. package/src/theme/index.ts +61 -0
  44. package/src/theme/presets.ts +189 -0
  45. package/src/theme/schema.ts +193 -0
  46. package/src/theme/serializer.ts +123 -0
  47. package/src/theme/types.ts +210 -0
  48. package/src/viewer/styles/globals.css +1 -1
  49. package/dist/chunk-ACIDZOYW.js.map +0 -1
  50. package/dist/chunk-PMGI7ATF.js.map +0 -1
  51. package/dist/chunk-XHNKNI6J.js.map +0 -1
  52. package/dist/scan-3ZAOVO4U.js +0 -12
  53. /package/dist/{chunk-LY2CFFPY.js.map → chunk-FYIYMXGA.js.map} +0 -0
  54. /package/dist/{chunk-3OTEW66K.js.map → chunk-LDKNZ55O.js.map} +0 -0
  55. /package/dist/{chunk-BSCG3IP7.js.map → chunk-NOTYONHY.js.map} +0 -0
  56. /package/dist/{core-DWKLGY4N.js.map → core-F3VT277E.js.map} +0 -0
  57. /package/dist/{generate-3LBZANQ3.js.map → generate-PNIUR75D.js.map} +0 -0
  58. /package/dist/{init-NKIUCYTG.js.map → init-ON6WYG66.js.map} +0 -0
  59. /package/dist/{scan-3ZAOVO4U.js.map → scan-E6U644RS.js.map} +0 -0
  60. /package/dist/{service-QSZMZJBJ.js.map → service-U7AR2PC2.js.map} +0 -0
  61. /package/dist/{static-viewer-MIPGZ4Z7.js.map → static-viewer-QL2SCWYB.js.map} +0 -0
  62. /package/dist/{test-ZCTR4LBB.js.map → test-PBPKJ4WJ.js.map} +0 -0
  63. /package/dist/{tokens-5JQ5IOR2.js.map → tokens-4J4PRIGT.js.map} +0 -0
  64. /package/dist/{viewer-D7QC4GM2.js.map → viewer-6VCZMA3T.js.map} +0 -0
package/src/build.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { readFile, writeFile, mkdir } from "node:fs/promises";
2
2
  import { resolve, join } from "node:path";
3
+ import { existsSync } from "node:fs";
3
4
  import type {
4
5
  SegmentsConfig,
5
6
  CompiledSegmentsFile,
@@ -110,6 +111,8 @@ export async function buildSegments(
110
111
  ...(v.figma && { figma: v.figma }),
111
112
  ...(v.args && { args: v.args }),
112
113
  })),
114
+ // Include AI metadata if present
115
+ ...(parsed.ai && { ai: parsed.ai }),
113
116
  };
114
117
 
115
118
  segments[parsed.meta.name] = compiled;
@@ -150,9 +153,22 @@ export async function buildSegments(
150
153
  // Recipe discovery failure is non-fatal
151
154
  }
152
155
 
156
+ // Read package name for import statements
157
+ let packageName: string | undefined;
158
+ const pkgJsonPath = resolve(configDir, "package.json");
159
+ if (existsSync(pkgJsonPath)) {
160
+ try {
161
+ const pkg = JSON.parse(await readFile(pkgJsonPath, "utf-8"));
162
+ if (pkg.name) packageName = pkg.name;
163
+ } catch {
164
+ // Non-fatal
165
+ }
166
+ }
167
+
153
168
  const output: CompiledSegmentsFile = {
154
169
  version: "1.0.0",
155
170
  generatedAt: new Date().toISOString(),
171
+ ...(packageName && { packageName }),
156
172
  segments,
157
173
  ...(Object.keys(recipes).length > 0 && { recipes }),
158
174
  };
package/src/core/index.ts CHANGED
@@ -32,6 +32,8 @@ export type {
32
32
  // Contract and provenance types
33
33
  SegmentContract,
34
34
  SegmentGenerated,
35
+ // AI metadata type
36
+ AIMetadata,
35
37
  // Screenshot types
36
38
  ScreenshotConfig,
37
39
  ServiceConfig,
@@ -87,6 +89,8 @@ export {
87
89
  segmentGeneratedSchema,
88
90
  segmentBanSchema,
89
91
  recipeDefinitionSchema,
92
+ // AI metadata schema
93
+ aiMetadataSchema,
90
94
  } from "./schema.js";
91
95
 
92
96
  // Main API
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import ts from "typescript";
11
- import type { SegmentMeta, SegmentUsage, PropDefinition } from "./types.js";
11
+ import type { SegmentMeta, SegmentUsage, PropDefinition, AIMetadata } from "./types.js";
12
12
 
13
13
  /**
14
14
  * Parsed segment metadata (extracted statically from AST)
@@ -45,6 +45,9 @@ export interface ParsedSegmentMetadata {
45
45
  note: string;
46
46
  }>;
47
47
 
48
+ /** AI-specific metadata for playground context generation */
49
+ ai?: AIMetadata;
50
+
48
51
  /** Parse warnings */
49
52
  warnings: string[];
50
53
  }
@@ -129,6 +132,9 @@ export function parseSegmentFile(
129
132
  // Extract relations
130
133
  const relations = extractRelations(arg, warnings);
131
134
 
135
+ // Extract AI metadata
136
+ const ai = extractAIMetadata(arg, warnings);
137
+
132
138
  return {
133
139
  componentImport,
134
140
  componentName,
@@ -137,6 +143,7 @@ export function parseSegmentFile(
137
143
  props,
138
144
  variants,
139
145
  relations,
146
+ ai,
140
147
  warnings,
141
148
  };
142
149
  }
@@ -460,6 +467,52 @@ function extractRelations(
460
467
  return relations;
461
468
  }
462
469
 
470
+ /**
471
+ * Extract AI metadata from defineSegment call.
472
+ */
473
+ function extractAIMetadata(
474
+ arg: ts.ObjectLiteralExpression,
475
+ warnings: string[]
476
+ ): AIMetadata | undefined {
477
+ const aiProp = findProperty(arg, "ai");
478
+ if (!aiProp || !ts.isObjectLiteralExpression(aiProp)) {
479
+ return undefined;
480
+ }
481
+
482
+ const ai: AIMetadata = {};
483
+
484
+ // Extract compositionPattern
485
+ const compositionPattern = extractStringProperty(aiProp, "compositionPattern");
486
+ if (compositionPattern && ['compound', 'simple', 'controlled'].includes(compositionPattern)) {
487
+ ai.compositionPattern = compositionPattern as AIMetadata['compositionPattern'];
488
+ }
489
+
490
+ // Extract subComponents array
491
+ const subComponents = extractStringArray(aiProp, "subComponents");
492
+ if (subComponents.length > 0) {
493
+ ai.subComponents = subComponents;
494
+ }
495
+
496
+ // Extract requiredChildren array
497
+ const requiredChildren = extractStringArray(aiProp, "requiredChildren");
498
+ if (requiredChildren.length > 0) {
499
+ ai.requiredChildren = requiredChildren;
500
+ }
501
+
502
+ // Extract commonPatterns array
503
+ const commonPatterns = extractStringArray(aiProp, "commonPatterns");
504
+ if (commonPatterns.length > 0) {
505
+ ai.commonPatterns = commonPatterns;
506
+ }
507
+
508
+ // Only return if we have any fields
509
+ if (Object.keys(ai).length > 0) {
510
+ return ai;
511
+ }
512
+
513
+ return undefined;
514
+ }
515
+
463
516
  /**
464
517
  * Extract a string property from an object literal.
465
518
  */
@@ -138,6 +138,16 @@ export const segmentGeneratedSchema = z.object({
138
138
  timestamp: z.string().datetime().optional(),
139
139
  });
140
140
 
141
+ /**
142
+ * Schema for AI-specific metadata for playground context generation
143
+ */
144
+ export const aiMetadataSchema = z.object({
145
+ compositionPattern: z.enum(['compound', 'simple', 'controlled']).optional(),
146
+ subComponents: z.array(z.string()).optional(),
147
+ requiredChildren: z.array(z.string()).optional(),
148
+ commonPatterns: z.array(z.string()).optional(),
149
+ });
150
+
141
151
  /**
142
152
  * Schema for recipe definitions
143
153
  */
@@ -158,6 +168,7 @@ export const segmentDefinitionSchema = z.object({
158
168
  relations: z.array(componentRelationSchema).optional(),
159
169
  variants: z.array(segmentVariantSchema), // Allow empty variants array
160
170
  contract: segmentContractSchema.optional(),
171
+ ai: aiMetadataSchema.optional(),
161
172
  _generated: segmentGeneratedSchema.optional(),
162
173
  });
163
174
 
package/src/core/types.ts CHANGED
@@ -314,6 +314,24 @@ export interface SegmentGenerated {
314
314
  timestamp?: string;
315
315
  }
316
316
 
317
+ /**
318
+ * AI-specific metadata for playground context generation
319
+ * Provides hints for AI code generation about component composition
320
+ */
321
+ export interface AIMetadata {
322
+ /** How this component is composed with others */
323
+ compositionPattern?: "compound" | "simple" | "controlled";
324
+
325
+ /** Sub-component names (without parent prefix, e.g., "Header" not "Card.Header") */
326
+ subComponents?: string[];
327
+
328
+ /** Sub-components that must be present for valid composition */
329
+ requiredChildren?: string[];
330
+
331
+ /** Common usage patterns as JSX strings for AI reference */
332
+ commonPatterns?: string[];
333
+ }
334
+
317
335
  /**
318
336
  * Complete segment definition
319
337
  */
@@ -339,6 +357,9 @@ export interface SegmentDefinition<TProps = unknown> {
339
357
  /** Agent-optimized contract metadata */
340
358
  contract?: SegmentContract;
341
359
 
360
+ /** AI-specific metadata for playground context generation */
361
+ ai?: AIMetadata;
362
+
342
363
  /** Provenance tracking (for generated segments) */
343
364
  _generated?: SegmentGenerated;
344
365
  }
@@ -706,6 +727,9 @@ export interface CompiledSegment {
706
727
  /** Agent-optimized contract metadata */
707
728
  contract?: SegmentContract;
708
729
 
730
+ /** AI-specific metadata for playground context generation */
731
+ ai?: AIMetadata;
732
+
709
733
  /** Provenance tracking (for generated segments) */
710
734
  _generated?: SegmentGenerated;
711
735
  }
@@ -746,6 +770,9 @@ export interface CompiledSegmentsFile {
746
770
  /** When this file was generated */
747
771
  generatedAt: string;
748
772
 
773
+ /** Package name for import statements (read from package.json at build time) */
774
+ packageName?: string;
775
+
749
776
  /** All compiled segments indexed by component name */
750
777
  segments: Record<string, CompiledSegment>;
751
778
 
package/src/mcp/server.ts CHANGED
@@ -366,13 +366,23 @@ export function createMcpServer(config: McpServerConfig): Server {
366
366
  }
367
367
 
368
368
  /**
369
- * Get the package name from package.json for import statements
369
+ * Get the package name for import statements.
370
+ * Prefers packageName from fragments.json (set at build time),
371
+ * falls back to the project's package.json name.
370
372
  */
371
373
  async function getPackageName(): Promise<string> {
372
374
  if (packageName) {
373
375
  return packageName;
374
376
  }
375
377
 
378
+ // Prefer packageName from compiled fragments.json
379
+ const data = await loadSegments();
380
+ if (data.packageName) {
381
+ packageName = data.packageName;
382
+ return packageName;
383
+ }
384
+
385
+ // Fallback to project package.json
376
386
  const packageJsonPath = join(config.projectRoot, 'package.json');
377
387
  if (existsSync(packageJsonPath)) {
378
388
  try {
@@ -7,15 +7,21 @@
7
7
 
8
8
  import { Command } from "commander";
9
9
  import pc from "picocolors";
10
+ import { readFileSync } from "node:fs";
11
+ import { fileURLToPath } from "node:url";
12
+ import { dirname, join } from "node:path";
10
13
  import { BRAND } from "../core/index.js";
11
14
  import { migrate } from "./migrate.js";
12
15
 
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+ const pkg = JSON.parse(readFileSync(join(__dirname, "../../package.json"), "utf-8")) as { version: string };
18
+
13
19
  const program = new Command();
14
20
 
15
21
  program
16
22
  .name("segments-migrate")
17
23
  .description(`${BRAND.name} Storybook Migration Tool`)
18
- .version("0.0.1");
24
+ .version(pkg.version);
19
25
 
20
26
  program
21
27
  .command("migrate")
@@ -49,7 +49,7 @@ function getStyles(): string {
49
49
  --bg-warning: rgba(234, 179, 8, 0.1);
50
50
  --bg-error: rgba(239, 68, 68, 0.1);
51
51
  --border: #262626;
52
- --text: #fafafa;
52
+ --text: #f2f2f2;
53
53
  --text-secondary: #a1a1aa;
54
54
  --text-muted: #71717a;
55
55
  --accent: #8b5cf6;
@@ -61,7 +61,7 @@ function getStyles(): string {
61
61
  --bg-card: #141414;
62
62
  --bg-hover: #1a1a1a;
63
63
  --border: #262626;
64
- --text: #fafafa;
64
+ --text: #f2f2f2;
65
65
  --text-secondary: #a1a1aa;
66
66
  --text-muted: #71717a;
67
67
  --accent: #10b981;
@@ -0,0 +1,412 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdir, rm, readFile, access } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import {
6
+ generateScssTokens,
7
+ generateCssTokens,
8
+ generateTokenFiles,
9
+ } from "../generator.js";
10
+ import type { ThemeConfig } from "../types.js";
11
+
12
+ describe("Theme Generator", () => {
13
+ let testDir: string;
14
+
15
+ beforeEach(async () => {
16
+ testDir = join(
17
+ tmpdir(),
18
+ `theme-generator-test-${Date.now()}-${Math.random().toString(36).slice(2)}`
19
+ );
20
+ await mkdir(testDir, { recursive: true });
21
+ });
22
+
23
+ afterEach(async () => {
24
+ try {
25
+ await rm(testDir, { recursive: true, force: true });
26
+ } catch {
27
+ // Ignore cleanup errors
28
+ }
29
+ });
30
+
31
+ describe("SCSS generation", () => {
32
+ it("should generate valid SCSS with custom accent color", () => {
33
+ const theme: ThemeConfig = {
34
+ name: "Test Theme",
35
+ colors: {
36
+ accent: "#ff6600",
37
+ },
38
+ };
39
+
40
+ const scss = generateScssTokens(theme);
41
+
42
+ expect(scss).toContain("$fui-color-accent: #ff6600 !default;");
43
+ });
44
+
45
+ it("should include !default flag for all variables", () => {
46
+ const theme: ThemeConfig = {
47
+ name: "Test Theme",
48
+ colors: {
49
+ accent: "#6366f1",
50
+ danger: "#ef4444",
51
+ },
52
+ radius: {
53
+ sm: "4px",
54
+ },
55
+ };
56
+
57
+ const scss = generateScssTokens(theme);
58
+
59
+ // All variable definitions should have !default
60
+ const lines = scss.split("\n").filter((l) => l.includes("$fui-"));
61
+ for (const line of lines) {
62
+ expect(line).toContain("!default;");
63
+ }
64
+ });
65
+
66
+ it("should generate dark mode variables", () => {
67
+ const theme: ThemeConfig = {
68
+ name: "Test Theme",
69
+ dark: {
70
+ surfaces: {
71
+ bgPrimary: "#0f172a",
72
+ },
73
+ text: {
74
+ primary: "#f8fafc",
75
+ },
76
+ },
77
+ };
78
+
79
+ const scss = generateScssTokens(theme);
80
+
81
+ expect(scss).toContain("$fui-dark-bg-primary: #0f172a !default;");
82
+ expect(scss).toContain("$fui-dark-text-primary: #f8fafc !default;");
83
+ });
84
+
85
+ it("should include header comment with theme name", () => {
86
+ const theme: ThemeConfig = {
87
+ name: "My Custom Theme",
88
+ };
89
+
90
+ const scss = generateScssTokens(theme);
91
+
92
+ expect(scss).toContain("// Theme: My Custom Theme");
93
+ expect(scss).toContain("// Auto-generated by @fragments-sdk/cli");
94
+ });
95
+
96
+ it("should generate all token categories", () => {
97
+ const theme: ThemeConfig = {
98
+ name: "Complete Theme",
99
+ colors: {
100
+ accent: "#6366f1",
101
+ },
102
+ surfaces: {
103
+ bgPrimary: "#ffffff",
104
+ },
105
+ text: {
106
+ primary: "#0f172a",
107
+ },
108
+ borders: {
109
+ default: "#e2e8f0",
110
+ },
111
+ typography: {
112
+ fontSans: "system-ui, sans-serif",
113
+ fontWeightNormal: 400,
114
+ },
115
+ radius: {
116
+ sm: "4px",
117
+ },
118
+ shadows: {
119
+ sm: "0 1px 2px rgba(0,0,0,0.05)",
120
+ },
121
+ };
122
+
123
+ const scss = generateScssTokens(theme);
124
+
125
+ // Check for section comments
126
+ expect(scss).toContain("// Colors");
127
+ expect(scss).toContain("// Surfaces");
128
+ expect(scss).toContain("// Text");
129
+ expect(scss).toContain("// Borders");
130
+ expect(scss).toContain("// Typography");
131
+ expect(scss).toContain("// Border Radius");
132
+ expect(scss).toContain("// Shadows");
133
+ });
134
+
135
+ it("should only include overridden tokens", () => {
136
+ const theme: ThemeConfig = {
137
+ name: "Minimal Theme",
138
+ colors: {
139
+ accent: "#ff0000",
140
+ },
141
+ };
142
+
143
+ const scss = generateScssTokens(theme);
144
+
145
+ // Should include accent
146
+ expect(scss).toContain("$fui-color-accent: #ff0000");
147
+
148
+ // Should NOT include non-overridden tokens
149
+ expect(scss).not.toContain("$fui-color-danger:");
150
+ expect(scss).not.toContain("$fui-bg-primary:");
151
+ });
152
+ });
153
+
154
+ describe("CSS generation", () => {
155
+ it("should generate CSS custom properties", () => {
156
+ const theme: ThemeConfig = {
157
+ name: "Test Theme",
158
+ colors: {
159
+ accent: "#ff6600",
160
+ },
161
+ };
162
+
163
+ const css = generateCssTokens(theme);
164
+
165
+ expect(css).toContain("--fui-color-accent: #ff6600;");
166
+ });
167
+
168
+ it("should wrap in :root selector", () => {
169
+ const theme: ThemeConfig = {
170
+ name: "Test Theme",
171
+ colors: {
172
+ accent: "#6366f1",
173
+ },
174
+ };
175
+
176
+ const css = generateCssTokens(theme);
177
+
178
+ expect(css).toContain(":root {");
179
+ expect(css).toContain("}");
180
+ });
181
+
182
+ it("should generate dark mode with .dark and [data-theme] selectors", () => {
183
+ const theme: ThemeConfig = {
184
+ name: "Test Theme",
185
+ dark: {
186
+ surfaces: {
187
+ bgPrimary: "#0f172a",
188
+ },
189
+ },
190
+ };
191
+
192
+ const css = generateCssTokens(theme);
193
+
194
+ expect(css).toContain(':root.dark,\n:root[data-theme="dark"] {');
195
+ expect(css).toContain("--fui-bg-primary: #0f172a;");
196
+ });
197
+
198
+ it("should include header comment with theme name", () => {
199
+ const theme: ThemeConfig = {
200
+ name: "My CSS Theme",
201
+ };
202
+
203
+ const css = generateCssTokens(theme);
204
+
205
+ expect(css).toContain("/* Theme: My CSS Theme */");
206
+ expect(css).toContain("/* Auto-generated by @fragments-sdk/cli */");
207
+ });
208
+ });
209
+
210
+ describe("file generation", () => {
211
+ it("should create SCSS file at specified path", async () => {
212
+ const theme: ThemeConfig = {
213
+ name: "Test Theme",
214
+ colors: {
215
+ accent: "#6366f1",
216
+ },
217
+ };
218
+
219
+ const result = await generateTokenFiles(theme, {
220
+ format: "scss",
221
+ outputDir: testDir,
222
+ });
223
+
224
+ expect(result.success).toBe(true);
225
+ expect(result.scssPath).toBeDefined();
226
+
227
+ // Verify file exists and contains content
228
+ const content = await readFile(result.scssPath!, "utf-8");
229
+ expect(content).toContain("$fui-color-accent: #6366f1");
230
+ });
231
+
232
+ it("should create CSS file at specified path", async () => {
233
+ const theme: ThemeConfig = {
234
+ name: "Test Theme",
235
+ colors: {
236
+ accent: "#6366f1",
237
+ },
238
+ };
239
+
240
+ const result = await generateTokenFiles(theme, {
241
+ format: "css",
242
+ outputDir: testDir,
243
+ });
244
+
245
+ expect(result.success).toBe(true);
246
+ expect(result.cssPath).toBeDefined();
247
+
248
+ // Verify file exists and contains content
249
+ const content = await readFile(result.cssPath!, "utf-8");
250
+ expect(content).toContain("--fui-color-accent: #6366f1");
251
+ });
252
+
253
+ it('should create both when format is "both"', async () => {
254
+ const theme: ThemeConfig = {
255
+ name: "Test Theme",
256
+ colors: {
257
+ accent: "#6366f1",
258
+ },
259
+ };
260
+
261
+ const result = await generateTokenFiles(theme, {
262
+ format: "both",
263
+ outputDir: testDir,
264
+ });
265
+
266
+ expect(result.success).toBe(true);
267
+ expect(result.scssPath).toBeDefined();
268
+ expect(result.cssPath).toBeDefined();
269
+
270
+ // Verify both files exist
271
+ await expect(access(result.scssPath!)).resolves.toBeUndefined();
272
+ await expect(access(result.cssPath!)).resolves.toBeUndefined();
273
+ });
274
+
275
+ it("should use custom file prefix", async () => {
276
+ const theme: ThemeConfig = {
277
+ name: "Test Theme",
278
+ };
279
+
280
+ const result = await generateTokenFiles(theme, {
281
+ format: "scss",
282
+ outputDir: testDir,
283
+ filePrefix: "_custom-tokens",
284
+ });
285
+
286
+ expect(result.success).toBe(true);
287
+ expect(result.scssPath).toContain("_custom-tokens.scss");
288
+ });
289
+
290
+ it("should create output directory if needed", async () => {
291
+ const theme: ThemeConfig = {
292
+ name: "Test Theme",
293
+ };
294
+
295
+ const nestedDir = join(testDir, "nested", "path");
296
+
297
+ const result = await generateTokenFiles(theme, {
298
+ format: "scss",
299
+ outputDir: nestedDir,
300
+ });
301
+
302
+ expect(result.success).toBe(true);
303
+ await expect(access(nestedDir)).resolves.toBeUndefined();
304
+ });
305
+
306
+ it("should use default file prefix when not specified", async () => {
307
+ const theme: ThemeConfig = {
308
+ name: "Test Theme",
309
+ };
310
+
311
+ const result = await generateTokenFiles(theme, {
312
+ format: "scss",
313
+ outputDir: testDir,
314
+ });
315
+
316
+ expect(result.success).toBe(true);
317
+ expect(result.scssPath).toContain("_theme-tokens.scss");
318
+ });
319
+ });
320
+
321
+ describe("token mapping", () => {
322
+ it("should map colors.accent to $fui-color-accent", () => {
323
+ const theme: ThemeConfig = {
324
+ name: "Test",
325
+ colors: { accent: "#123456" },
326
+ };
327
+
328
+ const scss = generateScssTokens(theme);
329
+ expect(scss).toContain("$fui-color-accent: #123456");
330
+ });
331
+
332
+ it("should map surfaces.bgPrimary to $fui-bg-primary", () => {
333
+ const theme: ThemeConfig = {
334
+ name: "Test",
335
+ surfaces: { bgPrimary: "#ffffff" },
336
+ };
337
+
338
+ const scss = generateScssTokens(theme);
339
+ expect(scss).toContain("$fui-bg-primary: #ffffff");
340
+ });
341
+
342
+ it("should map text.primary to $fui-text-primary", () => {
343
+ const theme: ThemeConfig = {
344
+ name: "Test",
345
+ text: { primary: "#000000" },
346
+ };
347
+
348
+ const scss = generateScssTokens(theme);
349
+ expect(scss).toContain("$fui-text-primary: #000000");
350
+ });
351
+
352
+ it("should map borders.default to $fui-border", () => {
353
+ const theme: ThemeConfig = {
354
+ name: "Test",
355
+ borders: { default: "#cccccc" },
356
+ };
357
+
358
+ const scss = generateScssTokens(theme);
359
+ expect(scss).toContain("$fui-border: #cccccc");
360
+ });
361
+
362
+ it("should map typography.fontSans to $fui-font-sans", () => {
363
+ const theme: ThemeConfig = {
364
+ name: "Test",
365
+ typography: { fontSans: "Arial, sans-serif" },
366
+ };
367
+
368
+ const scss = generateScssTokens(theme);
369
+ expect(scss).toContain("$fui-font-sans: Arial, sans-serif");
370
+ });
371
+
372
+ it("should map radius.sm to $fui-radius-sm", () => {
373
+ const theme: ThemeConfig = {
374
+ name: "Test",
375
+ radius: { sm: "2px" },
376
+ };
377
+
378
+ const scss = generateScssTokens(theme);
379
+ expect(scss).toContain("$fui-radius-sm: 2px");
380
+ });
381
+
382
+ it("should map shadows.md to $fui-shadow-md", () => {
383
+ const theme: ThemeConfig = {
384
+ name: "Test",
385
+ shadows: { md: "0 4px 8px rgba(0,0,0,0.1)" },
386
+ };
387
+
388
+ const scss = generateScssTokens(theme);
389
+ expect(scss).toContain("$fui-shadow-md: 0 4px 8px rgba(0,0,0,0.1)");
390
+ });
391
+
392
+ it("should map dark.surfaces.bgPrimary to $fui-dark-bg-primary", () => {
393
+ const theme: ThemeConfig = {
394
+ name: "Test",
395
+ dark: { surfaces: { bgPrimary: "#1a1a1a" } },
396
+ };
397
+
398
+ const scss = generateScssTokens(theme);
399
+ expect(scss).toContain("$fui-dark-bg-primary: #1a1a1a");
400
+ });
401
+
402
+ it("should map dark.dangerBg to $fui-dark-color-danger-bg", () => {
403
+ const theme: ThemeConfig = {
404
+ name: "Test",
405
+ dark: { dangerBg: "rgba(239, 68, 68, 0.15)" },
406
+ };
407
+
408
+ const scss = generateScssTokens(theme);
409
+ expect(scss).toContain("$fui-dark-color-danger-bg: rgba(239, 68, 68, 0.15)");
410
+ });
411
+ });
412
+ });