@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.
- package/dist/bin.js +18 -13
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-MUZ6CM66.js → chunk-5JNME72P.js} +3 -3
- package/dist/{chunk-MUZ6CM66.js.map → chunk-5JNME72P.js.map} +1 -1
- package/dist/{chunk-XHNKNI6J.js → chunk-AW7MWOUH.js} +9 -1
- package/dist/chunk-AW7MWOUH.js.map +1 -0
- package/dist/{chunk-LY2CFFPY.js → chunk-FYIYMXGA.js} +2 -2
- package/dist/{chunk-3OTEW66K.js → chunk-LDKNZ55O.js} +4 -4
- package/dist/{chunk-BSCG3IP7.js → chunk-NOTYONHY.js} +2 -2
- package/dist/{chunk-ACIDZOYW.js → chunk-ODXQAQQX.js} +21 -8
- package/dist/chunk-ODXQAQQX.js.map +1 -0
- package/dist/{chunk-PMGI7ATF.js → chunk-OZQ7Z6C3.js} +31 -2
- package/dist/chunk-OZQ7Z6C3.js.map +1 -0
- package/dist/{core-DWKLGY4N.js → core-F3VT277E.js} +5 -3
- package/dist/{generate-3LBZANQ3.js → generate-PNIUR75D.js} +4 -4
- package/dist/index.d.ts +18 -0
- package/dist/index.js +6 -6
- package/dist/{init-NKIUCYTG.js → init-ON6WYG66.js} +4 -4
- package/dist/mcp-bin.js +8 -3
- package/dist/mcp-bin.js.map +1 -1
- package/dist/scan-E6U644RS.js +12 -0
- package/dist/{service-QSZMZJBJ.js → service-U7AR2PC2.js} +4 -4
- package/dist/{static-viewer-MIPGZ4Z7.js → static-viewer-QL2SCWYB.js} +4 -4
- package/dist/{test-ZCTR4LBB.js → test-PBPKJ4WJ.js} +3 -3
- package/dist/{tokens-5JQ5IOR2.js → tokens-4J4PRIGT.js} +5 -5
- package/dist/{viewer-D7QC4GM2.js → viewer-6VCZMA3T.js} +13 -13
- package/package.json +1 -1
- package/src/bin.ts +7 -1
- package/src/build.ts +16 -0
- package/src/core/index.ts +4 -0
- package/src/core/parser.ts +54 -1
- package/src/core/schema.ts +11 -0
- package/src/core/types.ts +27 -0
- package/src/mcp/server.ts +11 -1
- package/src/migrate/bin.ts +7 -1
- package/src/migrate/report.ts +1 -1
- package/src/service/report.ts +1 -1
- package/src/theme/__tests__/generator.test.ts +412 -0
- package/src/theme/__tests__/presets.test.ts +169 -0
- package/src/theme/__tests__/schema.test.ts +463 -0
- package/src/theme/__tests__/serializer.test.ts +326 -0
- package/src/theme/generator.ts +355 -0
- package/src/theme/index.ts +61 -0
- package/src/theme/presets.ts +189 -0
- package/src/theme/schema.ts +193 -0
- package/src/theme/serializer.ts +123 -0
- package/src/theme/types.ts +210 -0
- package/src/viewer/styles/globals.css +1 -1
- package/dist/chunk-ACIDZOYW.js.map +0 -1
- package/dist/chunk-PMGI7ATF.js.map +0 -1
- package/dist/chunk-XHNKNI6J.js.map +0 -1
- package/dist/scan-3ZAOVO4U.js +0 -12
- /package/dist/{chunk-LY2CFFPY.js.map → chunk-FYIYMXGA.js.map} +0 -0
- /package/dist/{chunk-3OTEW66K.js.map → chunk-LDKNZ55O.js.map} +0 -0
- /package/dist/{chunk-BSCG3IP7.js.map → chunk-NOTYONHY.js.map} +0 -0
- /package/dist/{core-DWKLGY4N.js.map → core-F3VT277E.js.map} +0 -0
- /package/dist/{generate-3LBZANQ3.js.map → generate-PNIUR75D.js.map} +0 -0
- /package/dist/{init-NKIUCYTG.js.map → init-ON6WYG66.js.map} +0 -0
- /package/dist/{scan-3ZAOVO4U.js.map → scan-E6U644RS.js.map} +0 -0
- /package/dist/{service-QSZMZJBJ.js.map → service-U7AR2PC2.js.map} +0 -0
- /package/dist/{static-viewer-MIPGZ4Z7.js.map → static-viewer-QL2SCWYB.js.map} +0 -0
- /package/dist/{test-ZCTR4LBB.js.map → test-PBPKJ4WJ.js.map} +0 -0
- /package/dist/{tokens-5JQ5IOR2.js.map → tokens-4J4PRIGT.js.map} +0 -0
- /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
|
+
}
|