@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
@@ -0,0 +1,326 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ encodeThemeToUrl,
4
+ decodeThemeFromUrl,
5
+ compressTheme,
6
+ decompressTheme,
7
+ } from "../serializer.js";
8
+ import type { ThemeConfig } from "../types.js";
9
+
10
+ describe("Theme Serializer", () => {
11
+ describe("compression", () => {
12
+ it("should compress large theme to shorter string than JSON", () => {
13
+ // Use a large theme to demonstrate compression benefit
14
+ // (small themes may be larger after base64 encoding)
15
+ const theme: ThemeConfig = {
16
+ name: "Complete Theme",
17
+ version: "1.0.0",
18
+ extends: "default",
19
+ colors: {
20
+ accent: "#6366f1",
21
+ accentHover: "#4f46e5",
22
+ accentActive: "#4338ca",
23
+ danger: "#ef4444",
24
+ dangerHover: "#dc2626",
25
+ success: "#22c55e",
26
+ warning: "#f59e0b",
27
+ info: "#3b82f6",
28
+ dangerBg: "rgba(239, 68, 68, 0.1)",
29
+ successBg: "rgba(34, 197, 94, 0.1)",
30
+ warningBg: "rgba(245, 158, 11, 0.1)",
31
+ infoBg: "rgba(59, 130, 246, 0.1)",
32
+ },
33
+ surfaces: {
34
+ bgPrimary: "#ffffff",
35
+ bgSecondary: "#f8fafc",
36
+ bgTertiary: "#f1f5f9",
37
+ bgElevated: "#ffffff",
38
+ bgHover: "rgba(0, 0, 0, 0.04)",
39
+ bgActive: "rgba(0, 0, 0, 0.06)",
40
+ },
41
+ text: {
42
+ primary: "#0f172a",
43
+ secondary: "#64748b",
44
+ tertiary: "#94a3b8",
45
+ inverse: "#ffffff",
46
+ },
47
+ borders: {
48
+ default: "#e2e8f0",
49
+ strong: "#cbd5e1",
50
+ },
51
+ };
52
+
53
+ const jsonString = JSON.stringify(theme);
54
+ const compressed = compressTheme(theme);
55
+
56
+ // For large themes, compression should help
57
+ expect(compressed.length).toBeLessThan(jsonString.length);
58
+ });
59
+
60
+ it("should round-trip decompress to original theme", () => {
61
+ const theme: ThemeConfig = {
62
+ name: "Test Theme",
63
+ colors: {
64
+ accent: "#6366f1",
65
+ },
66
+ };
67
+
68
+ const compressed = compressTheme(theme);
69
+ const decompressed = decompressTheme(compressed);
70
+
71
+ expect(decompressed).toEqual(theme);
72
+ });
73
+
74
+ it("should handle minimal theme (name only)", () => {
75
+ const theme: ThemeConfig = {
76
+ name: "Minimal",
77
+ };
78
+
79
+ const compressed = compressTheme(theme);
80
+ const decompressed = decompressTheme(compressed);
81
+
82
+ expect(decompressed).toEqual(theme);
83
+ });
84
+
85
+ it("should handle complex theme with all fields", () => {
86
+ const theme: ThemeConfig = {
87
+ name: "Complete Theme",
88
+ version: "1.0.0",
89
+ extends: "default",
90
+ colors: {
91
+ accent: "#6366f1",
92
+ accentHover: "#4f46e5",
93
+ accentActive: "#4338ca",
94
+ danger: "#ef4444",
95
+ dangerHover: "#dc2626",
96
+ success: "#22c55e",
97
+ warning: "#f59e0b",
98
+ info: "#3b82f6",
99
+ dangerBg: "rgba(239, 68, 68, 0.1)",
100
+ successBg: "rgba(34, 197, 94, 0.1)",
101
+ warningBg: "rgba(245, 158, 11, 0.1)",
102
+ infoBg: "rgba(59, 130, 246, 0.1)",
103
+ },
104
+ surfaces: {
105
+ bgPrimary: "#ffffff",
106
+ bgSecondary: "#f8fafc",
107
+ bgTertiary: "#f1f5f9",
108
+ bgElevated: "#ffffff",
109
+ bgHover: "rgba(0, 0, 0, 0.04)",
110
+ bgActive: "rgba(0, 0, 0, 0.06)",
111
+ },
112
+ text: {
113
+ primary: "#0f172a",
114
+ secondary: "#64748b",
115
+ tertiary: "#94a3b8",
116
+ inverse: "#ffffff",
117
+ },
118
+ borders: {
119
+ default: "#e2e8f0",
120
+ strong: "#cbd5e1",
121
+ },
122
+ typography: {
123
+ fontSans: "system-ui, -apple-system, sans-serif",
124
+ fontMono: "'SF Mono', monospace",
125
+ fontWeightNormal: 400,
126
+ fontWeightMedium: 500,
127
+ fontWeightSemibold: 600,
128
+ },
129
+ radius: {
130
+ sm: "0.25rem",
131
+ md: "0.375rem",
132
+ lg: "0.5rem",
133
+ full: "9999px",
134
+ },
135
+ shadows: {
136
+ sm: "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
137
+ md: "0 4px 6px -1px rgba(0, 0, 0, 0.1)",
138
+ },
139
+ dark: {
140
+ surfaces: {
141
+ bgPrimary: "#0f172a",
142
+ bgSecondary: "#1e293b",
143
+ },
144
+ text: {
145
+ primary: "#f8fafc",
146
+ secondary: "#94a3b8",
147
+ },
148
+ },
149
+ };
150
+
151
+ const compressed = compressTheme(theme);
152
+ const decompressed = decompressTheme(compressed);
153
+
154
+ expect(decompressed).toEqual(theme);
155
+ });
156
+ });
157
+
158
+ describe("URL encoding", () => {
159
+ it("should produce URL-safe base64 (no +, /, =)", () => {
160
+ const theme: ThemeConfig = {
161
+ name: "Test Theme",
162
+ colors: {
163
+ accent: "#6366f1",
164
+ },
165
+ };
166
+
167
+ const compressed = compressTheme(theme);
168
+
169
+ // base64url should not contain +, /, or =
170
+ expect(compressed).not.toMatch(/[+/=]/);
171
+ // Should only contain alphanumeric, -, and _
172
+ expect(compressed).toMatch(/^[A-Za-z0-9_-]+$/);
173
+ });
174
+
175
+ it("should decode URL parameter back to theme", () => {
176
+ const theme: ThemeConfig = {
177
+ name: "Test Theme",
178
+ colors: {
179
+ accent: "#6366f1",
180
+ },
181
+ };
182
+
183
+ const compressed = compressTheme(theme);
184
+ const decoded = decompressTheme(compressed);
185
+
186
+ expect(decoded).toEqual(theme);
187
+ });
188
+
189
+ it("should return null for invalid encoded string", () => {
190
+ const result = decompressTheme("not-valid-base64-at-all!!!");
191
+ expect(result).toBeNull();
192
+ });
193
+
194
+ it("should return null for corrupted/truncated data", () => {
195
+ const theme: ThemeConfig = {
196
+ name: "Test Theme",
197
+ };
198
+
199
+ const compressed = compressTheme(theme);
200
+ // Truncate the compressed string
201
+ const truncated = compressed.slice(0, 10);
202
+
203
+ const result = decompressTheme(truncated);
204
+ expect(result).toBeNull();
205
+ });
206
+
207
+ it("should return null for valid base64 but invalid JSON", () => {
208
+ // Create a valid base64url string that doesn't decode to valid JSON
209
+ const result = decompressTheme("YWJj"); // "abc" in base64
210
+ expect(result).toBeNull();
211
+ });
212
+ });
213
+
214
+ describe("full URL format", () => {
215
+ it("should generate complete URL with ?preset= param", () => {
216
+ const theme: ThemeConfig = {
217
+ name: "Test Theme",
218
+ colors: {
219
+ accent: "#6366f1",
220
+ },
221
+ };
222
+
223
+ const url = encodeThemeToUrl(theme, "https://fragments.dev/create");
224
+
225
+ expect(url).toMatch(/^https:\/\/fragments\.dev\/create\?preset=/);
226
+ });
227
+
228
+ it("should extract theme from full URL", () => {
229
+ const theme: ThemeConfig = {
230
+ name: "Test Theme",
231
+ colors: {
232
+ accent: "#6366f1",
233
+ },
234
+ };
235
+
236
+ const url = encodeThemeToUrl(theme, "https://fragments.dev/create");
237
+ const decoded = decodeThemeFromUrl(url);
238
+
239
+ expect(decoded).toEqual(theme);
240
+ });
241
+
242
+ it("should handle URL with other query params", () => {
243
+ const theme: ThemeConfig = {
244
+ name: "Test Theme",
245
+ };
246
+
247
+ // Create a URL with preset param
248
+ const url = encodeThemeToUrl(theme, "https://fragments.dev/create");
249
+ // Add another param manually
250
+ const urlWithExtra = url + "&foo=bar";
251
+
252
+ const decoded = decodeThemeFromUrl(urlWithExtra);
253
+ expect(decoded).toEqual(theme);
254
+ });
255
+
256
+ it("should decode raw encoded string (not in URL)", () => {
257
+ const theme: ThemeConfig = {
258
+ name: "Test Theme",
259
+ };
260
+
261
+ const compressed = compressTheme(theme);
262
+ const decoded = decodeThemeFromUrl(compressed);
263
+
264
+ expect(decoded).toEqual(theme);
265
+ });
266
+
267
+ it("should return null for URL without preset param", () => {
268
+ const url = "https://fragments.dev/create?foo=bar";
269
+ const decoded = decodeThemeFromUrl(url);
270
+
271
+ expect(decoded).toBeNull();
272
+ });
273
+
274
+ it("should return null for invalid URL", () => {
275
+ const decoded = decodeThemeFromUrl("not a valid url or encoded string!!!");
276
+ expect(decoded).toBeNull();
277
+ });
278
+
279
+ it("should use default base URL when not provided", () => {
280
+ const theme: ThemeConfig = {
281
+ name: "Test Theme",
282
+ };
283
+
284
+ const url = encodeThemeToUrl(theme);
285
+ expect(url).toMatch(/^https:\/\/fragments\.dev\/init\?preset=/);
286
+ });
287
+ });
288
+
289
+ describe("edge cases", () => {
290
+ it("should handle theme names with special characters", () => {
291
+ const theme: ThemeConfig = {
292
+ name: "Theme with 'quotes' and \"double quotes\"",
293
+ };
294
+
295
+ const compressed = compressTheme(theme);
296
+ const decompressed = decompressTheme(compressed);
297
+
298
+ expect(decompressed).toEqual(theme);
299
+ });
300
+
301
+ it("should handle unicode characters", () => {
302
+ const theme: ThemeConfig = {
303
+ name: "Theme with emoji 🎨 and unicode",
304
+ };
305
+
306
+ const compressed = compressTheme(theme);
307
+ const decompressed = decompressTheme(compressed);
308
+
309
+ expect(decompressed).toEqual(theme);
310
+ });
311
+
312
+ it("should handle empty optional fields", () => {
313
+ const theme: ThemeConfig = {
314
+ name: "Test",
315
+ colors: {},
316
+ surfaces: {},
317
+ dark: {},
318
+ };
319
+
320
+ const compressed = compressTheme(theme);
321
+ const decompressed = decompressTheme(compressed);
322
+
323
+ expect(decompressed).toEqual(theme);
324
+ });
325
+ });
326
+ });
@@ -0,0 +1,355 @@
1
+ /**
2
+ * Theme Token Generator
3
+ *
4
+ * Generates SCSS and CSS token files from theme configurations
5
+ */
6
+
7
+ import { mkdir, writeFile } from "node:fs/promises";
8
+ import { join } from "node:path";
9
+ import type {
10
+ ThemeConfig,
11
+ TokenGeneratorOptions,
12
+ TokenGeneratorResult,
13
+ } from "./types.js";
14
+
15
+ const DEFAULT_FILE_PREFIX = "_theme-tokens";
16
+
17
+ /**
18
+ * Token mapping from ThemeConfig structure to SCSS variable names
19
+ */
20
+ const TOKEN_MAPPINGS = {
21
+ colors: {
22
+ accent: "fui-color-accent",
23
+ accentHover: "fui-color-accent-hover",
24
+ accentActive: "fui-color-accent-active",
25
+ danger: "fui-color-danger",
26
+ dangerHover: "fui-color-danger-hover",
27
+ success: "fui-color-success",
28
+ warning: "fui-color-warning",
29
+ info: "fui-color-info",
30
+ dangerBg: "fui-color-danger-bg",
31
+ successBg: "fui-color-success-bg",
32
+ warningBg: "fui-color-warning-bg",
33
+ infoBg: "fui-color-info-bg",
34
+ },
35
+ surfaces: {
36
+ bgPrimary: "fui-bg-primary",
37
+ bgSecondary: "fui-bg-secondary",
38
+ bgTertiary: "fui-bg-tertiary",
39
+ bgElevated: "fui-bg-elevated",
40
+ bgHover: "fui-bg-hover",
41
+ bgActive: "fui-bg-active",
42
+ },
43
+ text: {
44
+ primary: "fui-text-primary",
45
+ secondary: "fui-text-secondary",
46
+ tertiary: "fui-text-tertiary",
47
+ inverse: "fui-text-inverse",
48
+ },
49
+ borders: {
50
+ default: "fui-border",
51
+ strong: "fui-border-strong",
52
+ },
53
+ typography: {
54
+ fontSans: "fui-font-sans",
55
+ fontMono: "fui-font-mono",
56
+ fontWeightNormal: "fui-font-weight-normal",
57
+ fontWeightMedium: "fui-font-weight-medium",
58
+ fontWeightSemibold: "fui-font-weight-semibold",
59
+ },
60
+ radius: {
61
+ sm: "fui-radius-sm",
62
+ md: "fui-radius-md",
63
+ lg: "fui-radius-lg",
64
+ full: "fui-radius-full",
65
+ },
66
+ shadows: {
67
+ sm: "fui-shadow-sm",
68
+ md: "fui-shadow-md",
69
+ },
70
+ } as const;
71
+
72
+ /**
73
+ * Dark mode token mappings
74
+ */
75
+ const DARK_TOKEN_MAPPINGS = {
76
+ surfaces: {
77
+ bgPrimary: "fui-dark-bg-primary",
78
+ bgSecondary: "fui-dark-bg-secondary",
79
+ bgTertiary: "fui-dark-bg-tertiary",
80
+ bgElevated: "fui-dark-bg-elevated",
81
+ bgHover: "fui-dark-bg-hover",
82
+ bgActive: "fui-dark-bg-active",
83
+ },
84
+ text: {
85
+ primary: "fui-dark-text-primary",
86
+ secondary: "fui-dark-text-secondary",
87
+ tertiary: "fui-dark-text-tertiary",
88
+ inverse: "fui-dark-text-inverse",
89
+ },
90
+ borders: {
91
+ default: "fui-dark-border",
92
+ strong: "fui-dark-border-strong",
93
+ },
94
+ shadows: {
95
+ sm: "fui-dark-shadow-sm",
96
+ md: "fui-dark-shadow-md",
97
+ },
98
+ // Direct dark mode properties
99
+ dangerBg: "fui-dark-color-danger-bg",
100
+ successBg: "fui-dark-color-success-bg",
101
+ warningBg: "fui-dark-color-warning-bg",
102
+ infoBg: "fui-dark-color-info-bg",
103
+ backdrop: "fui-dark-backdrop",
104
+ } as const;
105
+
106
+ /**
107
+ * Generate tokens for a category
108
+ */
109
+ function generateCategoryTokens(
110
+ config: ThemeConfig,
111
+ categoryKey: keyof typeof TOKEN_MAPPINGS,
112
+ format: "scss" | "css"
113
+ ): string[] {
114
+ const category = config[categoryKey];
115
+ if (!category) return [];
116
+
117
+ const mappings = TOKEN_MAPPINGS[categoryKey];
118
+ const tokens: string[] = [];
119
+
120
+ for (const [key, varName] of Object.entries(mappings)) {
121
+ const value = (category as Record<string, unknown>)[key];
122
+ if (value !== undefined) {
123
+ if (format === "scss") {
124
+ tokens.push(`$${varName}: ${value} !default;`);
125
+ } else {
126
+ tokens.push(` --${varName}: ${value};`);
127
+ }
128
+ }
129
+ }
130
+
131
+ return tokens;
132
+ }
133
+
134
+ /**
135
+ * Generate dark mode tokens
136
+ */
137
+ function generateDarkTokens(
138
+ config: ThemeConfig,
139
+ format: "scss" | "css"
140
+ ): string[] {
141
+ if (!config.dark) return [];
142
+
143
+ const tokens: string[] = [];
144
+
145
+ // Handle nested categories
146
+ const nestedCategories = ["surfaces", "text", "borders", "shadows"] as const;
147
+ for (const category of nestedCategories) {
148
+ const categoryData = config.dark[category];
149
+ if (!categoryData) continue;
150
+
151
+ const mappings = DARK_TOKEN_MAPPINGS[category];
152
+ for (const [key, varName] of Object.entries(mappings)) {
153
+ const value = (categoryData as Record<string, unknown>)[key];
154
+ if (value !== undefined) {
155
+ if (format === "scss") {
156
+ tokens.push(`$${varName}: ${value} !default;`);
157
+ } else {
158
+ tokens.push(` --${varName.replace("fui-dark-", "fui-")}: ${value};`);
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ // Handle direct dark mode properties
165
+ const directProps = ["dangerBg", "successBg", "warningBg", "infoBg", "backdrop"] as const;
166
+ for (const prop of directProps) {
167
+ const value = config.dark[prop];
168
+ if (value !== undefined) {
169
+ const varName = DARK_TOKEN_MAPPINGS[prop];
170
+ if (format === "scss") {
171
+ tokens.push(`$${varName}: ${value} !default;`);
172
+ } else {
173
+ // For CSS, use the light mode variable name in dark context
174
+ const cssVarName = varName.replace("fui-dark-color-", "fui-color-").replace("fui-dark-", "fui-");
175
+ tokens.push(` --${cssVarName}: ${value};`);
176
+ }
177
+ }
178
+ }
179
+
180
+ return tokens;
181
+ }
182
+
183
+ /**
184
+ * Generate SCSS tokens from a theme configuration
185
+ *
186
+ * @param config - Theme configuration
187
+ * @returns SCSS content string
188
+ */
189
+ export function generateScssTokens(config: ThemeConfig): string {
190
+ const lines: string[] = [];
191
+
192
+ // Header
193
+ lines.push("// Auto-generated by @fragments-sdk/cli");
194
+ lines.push(`// Theme: ${config.name}`);
195
+ if (config.version) {
196
+ lines.push(`// Version: ${config.version}`);
197
+ }
198
+ lines.push("");
199
+
200
+ // Colors
201
+ const colorTokens = generateCategoryTokens(config, "colors", "scss");
202
+ if (colorTokens.length > 0) {
203
+ lines.push("// Colors");
204
+ lines.push(...colorTokens);
205
+ lines.push("");
206
+ }
207
+
208
+ // Surfaces
209
+ const surfaceTokens = generateCategoryTokens(config, "surfaces", "scss");
210
+ if (surfaceTokens.length > 0) {
211
+ lines.push("// Surfaces");
212
+ lines.push(...surfaceTokens);
213
+ lines.push("");
214
+ }
215
+
216
+ // Text
217
+ const textTokens = generateCategoryTokens(config, "text", "scss");
218
+ if (textTokens.length > 0) {
219
+ lines.push("// Text");
220
+ lines.push(...textTokens);
221
+ lines.push("");
222
+ }
223
+
224
+ // Borders
225
+ const borderTokens = generateCategoryTokens(config, "borders", "scss");
226
+ if (borderTokens.length > 0) {
227
+ lines.push("// Borders");
228
+ lines.push(...borderTokens);
229
+ lines.push("");
230
+ }
231
+
232
+ // Typography
233
+ const typographyTokens = generateCategoryTokens(config, "typography", "scss");
234
+ if (typographyTokens.length > 0) {
235
+ lines.push("// Typography");
236
+ lines.push(...typographyTokens);
237
+ lines.push("");
238
+ }
239
+
240
+ // Border Radius
241
+ const radiusTokens = generateCategoryTokens(config, "radius", "scss");
242
+ if (radiusTokens.length > 0) {
243
+ lines.push("// Border Radius");
244
+ lines.push(...radiusTokens);
245
+ lines.push("");
246
+ }
247
+
248
+ // Shadows
249
+ const shadowTokens = generateCategoryTokens(config, "shadows", "scss");
250
+ if (shadowTokens.length > 0) {
251
+ lines.push("// Shadows");
252
+ lines.push(...shadowTokens);
253
+ lines.push("");
254
+ }
255
+
256
+ // Dark mode
257
+ const darkTokens = generateDarkTokens(config, "scss");
258
+ if (darkTokens.length > 0) {
259
+ lines.push("// Dark Mode");
260
+ lines.push(...darkTokens);
261
+ lines.push("");
262
+ }
263
+
264
+ return lines.join("\n");
265
+ }
266
+
267
+ /**
268
+ * Generate CSS custom properties from a theme configuration
269
+ *
270
+ * @param config - Theme configuration
271
+ * @returns CSS content string
272
+ */
273
+ export function generateCssTokens(config: ThemeConfig): string {
274
+ const lines: string[] = [];
275
+
276
+ // Header
277
+ lines.push("/* Auto-generated by @fragments-sdk/cli */");
278
+ lines.push(`/* Theme: ${config.name} */`);
279
+ if (config.version) {
280
+ lines.push(`/* Version: ${config.version} */`);
281
+ }
282
+ lines.push("");
283
+
284
+ // Collect all light mode tokens
285
+ const lightTokens: string[] = [];
286
+
287
+ const categories = ["colors", "surfaces", "text", "borders", "typography", "radius", "shadows"] as const;
288
+ for (const category of categories) {
289
+ const tokens = generateCategoryTokens(config, category, "css");
290
+ lightTokens.push(...tokens);
291
+ }
292
+
293
+ if (lightTokens.length > 0) {
294
+ lines.push(":root {");
295
+ lines.push(...lightTokens);
296
+ lines.push("}");
297
+ lines.push("");
298
+ }
299
+
300
+ // Dark mode
301
+ const darkTokens = generateDarkTokens(config, "css");
302
+ if (darkTokens.length > 0) {
303
+ lines.push(':root.dark,');
304
+ lines.push(':root[data-theme="dark"] {');
305
+ lines.push(...darkTokens);
306
+ lines.push("}");
307
+ lines.push("");
308
+ }
309
+
310
+ return lines.join("\n");
311
+ }
312
+
313
+ /**
314
+ * Generate token files from a theme configuration
315
+ *
316
+ * @param config - Theme configuration
317
+ * @param options - Generator options
318
+ * @returns Result with file paths or error
319
+ */
320
+ export async function generateTokenFiles(
321
+ config: ThemeConfig,
322
+ options: TokenGeneratorOptions
323
+ ): Promise<TokenGeneratorResult> {
324
+ const { format, outputDir, filePrefix = DEFAULT_FILE_PREFIX } = options;
325
+
326
+ try {
327
+ // Ensure output directory exists
328
+ await mkdir(outputDir, { recursive: true });
329
+
330
+ const result: TokenGeneratorResult = { success: true };
331
+
332
+ // Generate SCSS if requested
333
+ if (format === "scss" || format === "both") {
334
+ const scssContent = generateScssTokens(config);
335
+ const scssPath = join(outputDir, `${filePrefix}.scss`);
336
+ await writeFile(scssPath, scssContent, "utf-8");
337
+ result.scssPath = scssPath;
338
+ }
339
+
340
+ // Generate CSS if requested
341
+ if (format === "css" || format === "both") {
342
+ const cssContent = generateCssTokens(config);
343
+ const cssPath = join(outputDir, `${filePrefix}.css`);
344
+ await writeFile(cssPath, cssContent, "utf-8");
345
+ result.cssPath = cssPath;
346
+ }
347
+
348
+ return result;
349
+ } catch (error) {
350
+ return {
351
+ success: false,
352
+ error: error instanceof Error ? error.message : "Unknown error",
353
+ };
354
+ }
355
+ }