@styleframe/figma 1.0.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/index.js ADDED
@@ -0,0 +1,831 @@
1
+ import { formatHex, formatHex8, parse } from "culori";
2
+
3
+ //#region src/converters/name-mapping.ts
4
+ /**
5
+ * Convert Styleframe variable name to Figma format
6
+ * @example "color.primary" → "color/primary"
7
+ */
8
+ function styleframeToFigmaName(name) {
9
+ return name.replace(/\./g, "/");
10
+ }
11
+ /**
12
+ * Convert Figma variable name to Styleframe format
13
+ * @example "color/primary" → "color.primary"
14
+ */
15
+ function figmaToStyleframeName(name) {
16
+ return name.replace(/\//g, ".");
17
+ }
18
+ /**
19
+ * Extract the category (first segment) from a variable name
20
+ * @example "color.primary.500" → "color"
21
+ * @example "color/primary/500" → "color"
22
+ */
23
+ function extractCategory(name) {
24
+ const separator = name.includes("/") ? "/" : ".";
25
+ return name.split(separator)[0] ?? "";
26
+ }
27
+ /**
28
+ * Extract the key (last segment) from a variable name
29
+ * @example "color.primary.500" → "500"
30
+ * @example "spacing.md" → "md"
31
+ */
32
+ function extractKey(name) {
33
+ const separator = name.includes("/") ? "/" : ".";
34
+ const parts = name.split(separator);
35
+ return parts[parts.length - 1] ?? "";
36
+ }
37
+ /**
38
+ * Convert a Styleframe variable name to a camelCase JavaScript identifier
39
+ * @example "color.primary" → "colorPrimary"
40
+ * @example "spacing.md" → "spacingMd"
41
+ */
42
+ function toVariableIdentifier(name) {
43
+ return (name.includes("/") ? figmaToStyleframeName(name) : name).split(".").map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join("");
44
+ }
45
+ /**
46
+ * Convert a CSS custom property name to Styleframe format
47
+ * @example "--color--primary" → "color.primary"
48
+ */
49
+ function cssVariableToStyleframeName(cssName) {
50
+ return cssName.replace(/^--/, "").replace(/--/g, ".");
51
+ }
52
+ /**
53
+ * Convert a Styleframe name to CSS custom property format
54
+ * @example "color.primary" → "--color--primary"
55
+ */
56
+ function styleframeToCssVariable(name) {
57
+ return `--${name.replace(/\./g, "--")}`;
58
+ }
59
+
60
+ //#endregion
61
+ //#region src/converters/value-parsing.ts
62
+ /**
63
+ * Convert a CSS color string to Figma RGBA format (0-1 range)
64
+ * Supports: hex, rgb(), rgba(), hsl(), hsla(), named colors
65
+ */
66
+ function cssColorToFigma(value) {
67
+ const parsed = parse(value);
68
+ if (!parsed) return null;
69
+ const rgb = parsed;
70
+ return {
71
+ r: rgb.r ?? 0,
72
+ g: rgb.g ?? 0,
73
+ b: rgb.b ?? 0,
74
+ a: rgb.alpha ?? 1
75
+ };
76
+ }
77
+ /**
78
+ * Convert Figma RGBA format (0-1 range) to CSS hex color
79
+ */
80
+ function figmaColorToCss(rgba) {
81
+ const color = {
82
+ mode: "rgb",
83
+ r: rgba.r,
84
+ g: rgba.g,
85
+ b: rgba.b,
86
+ alpha: rgba.a ?? 1
87
+ };
88
+ if (rgba.a !== void 0 && rgba.a < 1) return formatHex8(color) ?? "#000000";
89
+ return formatHex(color) ?? "#000000";
90
+ }
91
+ function parseDimension(value) {
92
+ const match = value.match(/^(-?\d*\.?\d+)(px|rem|em|%|vh|vw|vmin|vmax|ch)?$/);
93
+ if (!match) return null;
94
+ return {
95
+ value: Number.parseFloat(match[1] ?? "0"),
96
+ unit: match[2] ?? ""
97
+ };
98
+ }
99
+ /**
100
+ * Convert a CSS dimension to a pixel number
101
+ */
102
+ function dimensionToPixels(value, baseFontSize = 16) {
103
+ const parsed = parseDimension(value);
104
+ if (!parsed) return null;
105
+ switch (parsed.unit) {
106
+ case "px":
107
+ case "": return parsed.value;
108
+ case "rem":
109
+ case "em": return parsed.value * baseFontSize;
110
+ case "%": return parsed.value;
111
+ default: return null;
112
+ }
113
+ }
114
+ /**
115
+ * Convert a pixel value to a CSS dimension
116
+ */
117
+ function pixelsToDimension(pixels, unit = "px", baseFontSize = 16) {
118
+ switch (unit) {
119
+ case "rem": return `${pixels / baseFontSize}rem`;
120
+ case "em": return `${pixels / baseFontSize}em`;
121
+ case "%": return `${pixels}%`;
122
+ case "px":
123
+ default: return `${pixels}px`;
124
+ }
125
+ }
126
+ /**
127
+ * Detect if a string value is a color
128
+ */
129
+ function isColorValue(value) {
130
+ if (value.startsWith("#")) return true;
131
+ if (value.startsWith("rgb")) return true;
132
+ if (value.startsWith("hsl")) return true;
133
+ if (value.startsWith("oklch")) return true;
134
+ if (value.startsWith("oklab")) return true;
135
+ if (value.startsWith("lch")) return true;
136
+ if (value.startsWith("lab")) return true;
137
+ return parse(value) !== void 0;
138
+ }
139
+ /**
140
+ * Detect if a string value is a dimension
141
+ */
142
+ function isDimensionValue(value) {
143
+ return parseDimension(value) !== null;
144
+ }
145
+ /**
146
+ * Detect the Figma variable type from a Styleframe value
147
+ */
148
+ function detectFigmaType(value) {
149
+ if (typeof value === "boolean") return "BOOLEAN";
150
+ if (typeof value === "number") return "FLOAT";
151
+ if (typeof value === "string") {
152
+ if (isColorValue(value)) return "COLOR";
153
+ if (isDimensionValue(value)) return "FLOAT";
154
+ return "STRING";
155
+ }
156
+ return "STRING";
157
+ }
158
+ /**
159
+ * Convert a Styleframe token value to a Figma-compatible value
160
+ */
161
+ function styleframeValueToFigma(value, type, baseFontSize = 16) {
162
+ if (typeof value === "boolean") return value;
163
+ if (typeof value === "number") return value;
164
+ if (typeof value !== "string") return null;
165
+ switch (type) {
166
+ case "COLOR": return cssColorToFigma(value);
167
+ case "FLOAT": return dimensionToPixels(value, baseFontSize);
168
+ case "BOOLEAN": return value === "true";
169
+ case "STRING":
170
+ default: return value;
171
+ }
172
+ }
173
+ /**
174
+ * Convert a Figma variable value to a Styleframe-compatible CSS value
175
+ */
176
+ function figmaValueToStyleframe(value, type, useRem = false, baseFontSize = 16) {
177
+ switch (type) {
178
+ case "COLOR":
179
+ if (typeof value === "object" && "r" in value) return figmaColorToCss(value);
180
+ return String(value);
181
+ case "FLOAT":
182
+ if (typeof value === "number") {
183
+ if (useRem) return `${value / baseFontSize}rem`;
184
+ return `${value}px`;
185
+ }
186
+ return typeof value === "string" ? value : String(value);
187
+ case "BOOLEAN": return Boolean(value);
188
+ case "STRING":
189
+ default: return String(value);
190
+ }
191
+ }
192
+
193
+ //#endregion
194
+ //#region src/converters/codegen.ts
195
+ /**
196
+ * Known composable categories and their function names
197
+ */
198
+ const COMPOSABLE_MAP = {
199
+ color: "useColor",
200
+ spacing: "useSpacing",
201
+ fontSize: "useFontSize",
202
+ fontWeight: "useFontWeight",
203
+ fontFamily: "useFontFamily",
204
+ lineHeight: "useLineHeight",
205
+ letterSpacing: "useLetterSpacing",
206
+ borderWidth: "useBorderWidth",
207
+ borderRadius: "useBorderRadius",
208
+ boxShadow: "useBoxShadow"
209
+ };
210
+ /**
211
+ * Check if a value is a Figma variable alias
212
+ */
213
+ function isVariableAlias(value) {
214
+ return typeof value === "object" && value !== null && "type" in value && value.type === "VARIABLE_ALIAS";
215
+ }
216
+ /**
217
+ * Check if a value is a Figma RGBA color
218
+ */
219
+ function isFigmaColor(value) {
220
+ return typeof value === "object" && value !== null && "r" in value && "g" in value && "b" in value;
221
+ }
222
+ /**
223
+ * Parse Figma export format into structured data for code generation
224
+ */
225
+ function parseVariables(data, options) {
226
+ const { useRem = false, baseFontSize = 16, defaultModeName } = options;
227
+ const defaultMode = defaultModeName || data.modes[0] || "Default";
228
+ const themeNames = data.modes.filter((m) => m !== defaultMode);
229
+ const nameMap = /* @__PURE__ */ new Map();
230
+ for (const v of data.variables) nameMap.set(v.name, figmaToStyleframeName(v.name));
231
+ const variables = [];
232
+ const themeVariablesMap = /* @__PURE__ */ new Map();
233
+ for (const themeName of themeNames) themeVariablesMap.set(themeName, []);
234
+ for (const variable of data.variables) {
235
+ const styleframeName = figmaToStyleframeName(variable.name);
236
+ const category = extractCategory(styleframeName);
237
+ const defaultValue = variable.values[defaultMode];
238
+ if (defaultValue === void 0) continue;
239
+ let type = "string";
240
+ if (variable.type === "COLOR") type = "color";
241
+ else if (variable.type === "FLOAT") type = "number";
242
+ else if (variable.type === "BOOLEAN") type = "boolean";
243
+ const isReference = isVariableAlias(defaultValue);
244
+ let referenceTo;
245
+ let value;
246
+ if (isReference) {
247
+ referenceTo = variable.aliasTo ? figmaToStyleframeName(variable.aliasTo) : void 0;
248
+ value = "";
249
+ } else if (isFigmaColor(defaultValue)) value = figmaValueToStyleframe(defaultValue, "COLOR", useRem, baseFontSize);
250
+ else if (typeof defaultValue === "number") value = figmaValueToStyleframe(defaultValue, "FLOAT", useRem, baseFontSize);
251
+ else if (typeof defaultValue === "boolean") value = defaultValue;
252
+ else value = String(defaultValue);
253
+ variables.push({
254
+ name: styleframeName,
255
+ value,
256
+ type,
257
+ category,
258
+ isReference,
259
+ referenceTo
260
+ });
261
+ for (const themeName of themeNames) {
262
+ const themeValue = variable.values[themeName];
263
+ if (themeValue === void 0) continue;
264
+ const isThemeReference = isVariableAlias(themeValue);
265
+ let themeStyleframeValue;
266
+ if (isThemeReference) continue;
267
+ else if (isFigmaColor(themeValue)) themeStyleframeValue = figmaValueToStyleframe(themeValue, "COLOR", useRem, baseFontSize);
268
+ else if (typeof themeValue === "number") themeStyleframeValue = figmaValueToStyleframe(themeValue, "FLOAT", useRem, baseFontSize);
269
+ else if (typeof themeValue === "boolean") themeStyleframeValue = themeValue;
270
+ else themeStyleframeValue = String(themeValue);
271
+ if (themeStyleframeValue !== value) {
272
+ const themeVariables = themeVariablesMap.get(themeName) || [];
273
+ themeVariables.push({
274
+ name: styleframeName,
275
+ value: themeStyleframeValue,
276
+ type,
277
+ category,
278
+ isReference: false
279
+ });
280
+ themeVariablesMap.set(themeName, themeVariables);
281
+ }
282
+ }
283
+ }
284
+ const themes = [];
285
+ for (const [name, vars] of themeVariablesMap) if (vars.length > 0) themes.push({
286
+ name: name.toLowerCase(),
287
+ variables: vars
288
+ });
289
+ return {
290
+ variables,
291
+ themes
292
+ };
293
+ }
294
+ /**
295
+ * Group variables by category
296
+ */
297
+ function groupByCategory(variables) {
298
+ const groups = /* @__PURE__ */ new Map();
299
+ for (const v of variables) {
300
+ const existing = groups.get(v.category) || [];
301
+ existing.push(v);
302
+ groups.set(v.category, existing);
303
+ }
304
+ return groups;
305
+ }
306
+ /**
307
+ * Generate code using composables
308
+ */
309
+ function generateComposableCode(category, variables, instanceName) {
310
+ const composableName = COMPOSABLE_MAP[category];
311
+ if (!composableName) return null;
312
+ const nonRefVars = variables.filter((v) => !v.isReference);
313
+ if (nonRefVars.length === 0) return null;
314
+ const identifiers = [];
315
+ const valueEntries = [];
316
+ for (const v of nonRefVars) {
317
+ const key = v.name.split(".").slice(1).join(".") || v.name;
318
+ const identifier = toVariableIdentifier(v.name);
319
+ identifiers.push(identifier);
320
+ const valueStr = typeof v.value === "string" ? `"${v.value}"` : String(v.value);
321
+ valueEntries.push(`\t${key}: ${valueStr},`);
322
+ }
323
+ return {
324
+ code: `const { ${identifiers.join(", ")} } = ${composableName}(${instanceName}, {\n${valueEntries.join("\n")}\n});`,
325
+ identifiers,
326
+ composable: composableName
327
+ };
328
+ }
329
+ /**
330
+ * Generate variable declaration code
331
+ * Returns null if the variable cannot be generated (e.g., reference with unknown target)
332
+ */
333
+ function generateVariableCode(variable) {
334
+ const identifier = toVariableIdentifier(variable.name);
335
+ if (variable.isReference) {
336
+ if (variable.referenceTo) {
337
+ const refTarget = toVariableIdentifier(variable.referenceTo);
338
+ return `const ${identifier} = variable("${variable.name}", ref(${refTarget}));`;
339
+ }
340
+ return null;
341
+ }
342
+ const valueStr = typeof variable.value === "string" ? `"${variable.value}"` : String(variable.value);
343
+ return `const ${identifier} = variable("${variable.name}", ${valueStr});`;
344
+ }
345
+ /**
346
+ * Generate theme block code
347
+ */
348
+ function generateThemeCode(theme, instanceName) {
349
+ const lines = [];
350
+ lines.push(`theme("${theme.name}", (ctx) => {`);
351
+ for (const v of theme.variables) {
352
+ const identifier = toVariableIdentifier(v.name);
353
+ const valueStr = typeof v.value === "string" ? `"${v.value}"` : String(v.value);
354
+ lines.push(`\tctx.variable(${identifier}, ${valueStr});`);
355
+ }
356
+ lines.push("});");
357
+ return lines.join("\n");
358
+ }
359
+ /**
360
+ * Generate Styleframe TypeScript code from Figma export format
361
+ */
362
+ function generateStyleframeCode(data, options = {}) {
363
+ const { useComposables = true, instanceName = "s" } = options;
364
+ const { variables, themes } = parseVariables(data, options);
365
+ const grouped = groupByCategory(variables);
366
+ const imports = ["styleframe"];
367
+ const usedComposables = /* @__PURE__ */ new Set();
368
+ const lines = [];
369
+ const generatedIdentifiers = /* @__PURE__ */ new Set();
370
+ for (const [category, vars] of grouped) {
371
+ lines.push(`\n// ${category.charAt(0).toUpperCase() + category.slice(1)} variables`);
372
+ if (useComposables) {
373
+ const composableResult = generateComposableCode(category, vars, instanceName);
374
+ if (composableResult) {
375
+ lines.push(composableResult.code);
376
+ usedComposables.add(composableResult.composable);
377
+ for (const id of composableResult.identifiers) generatedIdentifiers.add(id);
378
+ }
379
+ }
380
+ for (const v of vars) {
381
+ const identifier = toVariableIdentifier(v.name);
382
+ if (generatedIdentifiers.has(identifier)) continue;
383
+ if (!useComposables || v.isReference || !COMPOSABLE_MAP[category]) {
384
+ const code$1 = generateVariableCode(v);
385
+ if (code$1) {
386
+ lines.push(code$1);
387
+ generatedIdentifiers.add(identifier);
388
+ }
389
+ }
390
+ }
391
+ }
392
+ if (themes.length > 0) {
393
+ lines.push("");
394
+ for (const theme of themes) lines.push(generateThemeCode(theme, instanceName));
395
+ }
396
+ const composableImports = Array.from(usedComposables).sort();
397
+ let code = `import { styleframe } from "styleframe";\n`;
398
+ if (composableImports.length > 0) {
399
+ code += `import { ${composableImports.join(", ")} } from "@styleframe/theme";\n`;
400
+ imports.push(...composableImports);
401
+ }
402
+ code += `\nconst ${instanceName} = styleframe();\n`;
403
+ code += `const { variable, ref, theme } = ${instanceName};\n`;
404
+ code += lines.join("\n");
405
+ code += `\n\nexport default ${instanceName};\n`;
406
+ return {
407
+ code,
408
+ variables,
409
+ themes,
410
+ imports
411
+ };
412
+ }
413
+
414
+ //#endregion
415
+ //#region src/converters/dtcg/types.ts
416
+ /**
417
+ * Check if a value is a DTCG alias
418
+ */
419
+ function isDTCGAlias(value) {
420
+ return typeof value === "string" && value.startsWith("{") && value.endsWith("}");
421
+ }
422
+ /**
423
+ * Type guard to check if a value is a DTCGToken (has $value property)
424
+ */
425
+ function isDTCGToken(value) {
426
+ return typeof value === "object" && value !== null && "$value" in value;
427
+ }
428
+ /**
429
+ * Type guard to check if a value is a DTCGGroup (object without $value)
430
+ */
431
+ function isDTCGGroup(value) {
432
+ return typeof value === "object" && value !== null && !("$value" in value) && !Array.isArray(value);
433
+ }
434
+
435
+ //#endregion
436
+ //#region src/converters/dtcg/type-mapping.ts
437
+ /**
438
+ * Map Figma variable types to DTCG types
439
+ */
440
+ function figmaTypeToDTCG(figmaType) {
441
+ switch (figmaType) {
442
+ case "COLOR": return "color";
443
+ case "FLOAT": return "dimension";
444
+ case "STRING": return "string";
445
+ case "BOOLEAN": return "string";
446
+ default: return "string";
447
+ }
448
+ }
449
+ /**
450
+ * Map DTCG types to Figma variable types
451
+ */
452
+ function dtcgTypeToFigma(dtcgType) {
453
+ switch (dtcgType) {
454
+ case "color": return "COLOR";
455
+ case "dimension":
456
+ case "number":
457
+ case "fontWeight":
458
+ case "duration": return "FLOAT";
459
+ case "fontFamily":
460
+ case "string": return "STRING";
461
+ case "cubicBezier": return "STRING";
462
+ default: return "STRING";
463
+ }
464
+ }
465
+ /**
466
+ * Detect DTCG type from a value
467
+ */
468
+ function detectDTCGType(value) {
469
+ if (typeof value === "number") return "number";
470
+ if (typeof value === "string") {
471
+ if (value.startsWith("#") || value.startsWith("rgb") || value.startsWith("hsl") || value.startsWith("oklch") || value.startsWith("oklab")) return "color";
472
+ if (/^-?\d*\.?\d+(px|rem|em|%|vh|vw|vmin|vmax|ch)$/.test(value)) return "dimension";
473
+ if (/^\d*\.?\d+(ms|s)$/.test(value)) return "duration";
474
+ }
475
+ if (Array.isArray(value) && value.length === 4) {
476
+ if (value.every((v) => typeof v === "number")) return "cubicBezier";
477
+ }
478
+ return "string";
479
+ }
480
+
481
+ //#endregion
482
+ //#region src/converters/dtcg/alias-parsing.ts
483
+ /**
484
+ * Parse a DTCG alias string to get the referenced path
485
+ * @example "{color.primary}" → "color.primary"
486
+ */
487
+ function parseAlias(alias) {
488
+ return alias.slice(1, -1);
489
+ }
490
+ /**
491
+ * Create a DTCG alias string from a token path
492
+ * @example "color.primary" → "{color.primary}"
493
+ */
494
+ function createAlias(path) {
495
+ return `{${path.replace(/\//g, ".")}}`;
496
+ }
497
+ /**
498
+ * Convert Figma slash notation to DTCG dot notation
499
+ * @example "color/primary" → "color.primary"
500
+ */
501
+ function figmaPathToDTCG(figmaPath) {
502
+ return figmaPath.replace(/\//g, ".");
503
+ }
504
+ /**
505
+ * Convert DTCG dot notation to Figma slash notation
506
+ * @example "color.primary" → "color/primary"
507
+ */
508
+ function dtcgPathToFigma(dtcgPath) {
509
+ return dtcgPath.replace(/\./g, "/");
510
+ }
511
+ /**
512
+ * Check if a value is an alias and resolve it to Figma format
513
+ */
514
+ function resolveAliasForFigma(value) {
515
+ if (isDTCGAlias(value)) return {
516
+ isAlias: true,
517
+ target: dtcgPathToFigma(parseAlias(value))
518
+ };
519
+ return { isAlias: false };
520
+ }
521
+ /**
522
+ * Get the segments of a token path
523
+ * @example "color.primary.500" → ["color", "primary", "500"]
524
+ */
525
+ function getPathSegments(path) {
526
+ return path.split(/[./]/);
527
+ }
528
+ /**
529
+ * Join path segments into a DTCG path
530
+ * @example ["color", "primary", "500"] → "color.primary.500"
531
+ */
532
+ function joinPath(segments) {
533
+ return segments.join(".");
534
+ }
535
+
536
+ //#endregion
537
+ //#region src/converters/dtcg/to-dtcg.ts
538
+ const DTCG_SCHEMA_URL = "https://design-tokens.github.io/community-group/format/";
539
+ /**
540
+ * Convert FigmaExportFormat to DTCG format
541
+ */
542
+ function toDTCG(data, options = {}) {
543
+ const { includeSchema = true, schemaUrl = DTCG_SCHEMA_URL, useModifiers = true, themeNames } = options;
544
+ const doc = {};
545
+ if (includeSchema) doc.$schema = schemaUrl;
546
+ doc.$extensions = { "dev.styleframe": {
547
+ collection: data.collection,
548
+ modes: data.modes
549
+ } };
550
+ const defaultMode = data.modes[0] || "Default";
551
+ const themeModes = themeNames ? data.modes.filter((m) => themeNames.includes(m) && m !== defaultMode) : data.modes.slice(1);
552
+ const modeOverrides = /* @__PURE__ */ new Map();
553
+ for (const mode of themeModes) modeOverrides.set(mode, /* @__PURE__ */ new Map());
554
+ for (const variable of data.variables) {
555
+ const { token, overrides } = variableToTokenWithOverrides(variable, data.modes, useModifiers, themeModes);
556
+ setNestedToken(doc, variable.name, token);
557
+ if (useModifiers) for (const [modeName, value] of overrides) {
558
+ const modeMap = modeOverrides.get(modeName);
559
+ if (modeMap) {
560
+ const tokenPath = figmaPathToDTCG(variable.name);
561
+ modeMap.set(tokenPath, value);
562
+ }
563
+ }
564
+ }
565
+ if (useModifiers && themeModes.length > 0 && hasAnyOverrides(modeOverrides)) doc.$modifiers = { theme: {
566
+ $type: "modifier",
567
+ contexts: buildModifierContexts(modeOverrides)
568
+ } };
569
+ return doc;
570
+ }
571
+ /**
572
+ * Convert a single Figma variable to a DTCG token, returning overrides separately
573
+ */
574
+ function variableToTokenWithOverrides(variable, modes, useModifiers, themeModes) {
575
+ const defaultMode = modes[0] || "Default";
576
+ const defaultValue = variable.values[defaultMode];
577
+ const dtcgType = figmaTypeToDTCG(variable.type);
578
+ const overrides = /* @__PURE__ */ new Map();
579
+ if (variable.aliasTo) {
580
+ const token$1 = {
581
+ $value: createAlias(variable.aliasTo),
582
+ $type: dtcgType
583
+ };
584
+ if (variable.description) token$1.$description = variable.description;
585
+ return {
586
+ token: token$1,
587
+ overrides
588
+ };
589
+ }
590
+ const $value = convertValueToDTCG(defaultValue, variable.type);
591
+ const token = {
592
+ $value,
593
+ $type: dtcgType
594
+ };
595
+ if (variable.description) token.$description = variable.description;
596
+ if (modes.length > 1) {
597
+ const modeValues = {};
598
+ let hasModeOverrides = false;
599
+ for (const mode of modes) {
600
+ if (mode === defaultMode) continue;
601
+ const modeValue = variable.values[mode];
602
+ if (modeValue !== void 0) {
603
+ const convertedValue = convertValueToDTCG(modeValue, variable.type);
604
+ if (convertedValue !== $value) {
605
+ const isThemeMode = themeModes.includes(mode);
606
+ if (useModifiers && isThemeMode) overrides.set(mode, convertedValue);
607
+ else if (!useModifiers || !isThemeMode) {
608
+ modeValues[mode] = convertedValue;
609
+ hasModeOverrides = true;
610
+ }
611
+ }
612
+ }
613
+ }
614
+ if (hasModeOverrides) token.$extensions = { "dev.styleframe": { modes: modeValues } };
615
+ }
616
+ return {
617
+ token,
618
+ overrides
619
+ };
620
+ }
621
+ /**
622
+ * Convert a Figma value to DTCG format
623
+ */
624
+ function convertValueToDTCG(value, type) {
625
+ if (type === "COLOR" && typeof value === "object" && value !== null && "r" in value) return figmaColorToCss(value);
626
+ if (type === "FLOAT" && typeof value === "number") return `${value}px`;
627
+ if (typeof value === "boolean") return value.toString();
628
+ return String(value);
629
+ }
630
+ /**
631
+ * Set a token at a nested path in the document
632
+ * @example "color/primary" creates { color: { primary: token } }
633
+ */
634
+ function setNestedToken(doc, path, token) {
635
+ const segments = getPathSegments(path);
636
+ let current = doc;
637
+ for (let i = 0; i < segments.length - 1; i++) {
638
+ const segment = segments[i];
639
+ if (!current[segment] || typeof current[segment] !== "object") current[segment] = {};
640
+ current = current[segment];
641
+ }
642
+ const leafKey = segments[segments.length - 1];
643
+ current[leafKey] = token;
644
+ }
645
+ /**
646
+ * Check if any mode has overrides
647
+ */
648
+ function hasAnyOverrides(modeOverrides) {
649
+ for (const overrides of modeOverrides.values()) if (overrides.size > 0) return true;
650
+ return false;
651
+ }
652
+ /**
653
+ * Build modifier contexts from collected overrides
654
+ */
655
+ function buildModifierContexts(modeOverrides) {
656
+ const contexts = {};
657
+ for (const [modeName, tokenOverrides] of modeOverrides) {
658
+ if (tokenOverrides.size === 0) continue;
659
+ const context = {};
660
+ for (const [tokenPath, value] of tokenOverrides) setNestedValue(context, tokenPath, { $value: value });
661
+ contexts[modeName] = context;
662
+ }
663
+ return contexts;
664
+ }
665
+ /**
666
+ * Set a value at a nested path using dot notation
667
+ */
668
+ function setNestedValue(obj, path, value) {
669
+ const segments = path.split(".");
670
+ let current = obj;
671
+ for (let i = 0; i < segments.length - 1; i++) {
672
+ const segment = segments[i];
673
+ if (!current[segment] || typeof current[segment] !== "object" || "$value" in current[segment]) current[segment] = {};
674
+ current = current[segment];
675
+ }
676
+ current[segments[segments.length - 1]] = value;
677
+ }
678
+
679
+ //#endregion
680
+ //#region src/converters/dtcg/from-dtcg.ts
681
+ /**
682
+ * Convert DTCG format to FigmaExportFormat
683
+ */
684
+ function fromDTCG(doc, options = {}) {
685
+ const { defaultModeName = "Default", collectionName, preferredModifier = "theme" } = options;
686
+ const { modes } = extractModes(doc, defaultModeName, preferredModifier);
687
+ const sfExtension = doc.$extensions?.["dev.styleframe"];
688
+ const collection = collectionName || sfExtension?.collection || "Design Tokens";
689
+ const variables = [];
690
+ const modifierOverrides = extractModifierOverrides(doc, preferredModifier);
691
+ walkTokens(doc, "", modes[0] || defaultModeName, (path, token, inheritedType) => {
692
+ const variable = tokenToVariableWithModifiers(path, token, inheritedType, modes, modes[0] || defaultModeName, modifierOverrides);
693
+ if (variable) variables.push(variable);
694
+ });
695
+ return {
696
+ collection,
697
+ modes,
698
+ variables
699
+ };
700
+ }
701
+ /**
702
+ * Walk all tokens in a DTCG document
703
+ */
704
+ function walkTokens(node, parentPath, defaultModeName, callback, inheritedType) {
705
+ const currentType = node.$type || inheritedType;
706
+ for (const [key, value] of Object.entries(node)) {
707
+ if (key.startsWith("$")) continue;
708
+ const path = parentPath ? `${parentPath}/${key}` : key;
709
+ if (isDTCGToken(value)) callback(path, value, currentType);
710
+ else if (isDTCGGroup(value)) walkTokens(value, path, defaultModeName, callback, currentType);
711
+ }
712
+ }
713
+ /**
714
+ * Extract modes from document, preferring modifiers over legacy format
715
+ */
716
+ function extractModes(doc, defaultModeName, preferredModifier) {
717
+ const modifier = doc.$modifiers?.[preferredModifier];
718
+ if (modifier && modifier.contexts) {
719
+ const contextNames = Object.keys(modifier.contexts);
720
+ if (contextNames.length > 0) {
721
+ const sfExtension$1 = doc.$extensions?.["dev.styleframe"];
722
+ const defaultMode = modifier.$default || sfExtension$1?.modes?.[0] || defaultModeName;
723
+ return {
724
+ modes: [defaultMode, ...contextNames.filter((n) => n !== defaultMode)],
725
+ modeSource: "modifier"
726
+ };
727
+ }
728
+ }
729
+ const sfExtension = doc.$extensions?.["dev.styleframe"];
730
+ if (sfExtension?.modes && sfExtension.modes.length > 0) return {
731
+ modes: sfExtension.modes,
732
+ modeSource: "extension"
733
+ };
734
+ return {
735
+ modes: [defaultModeName],
736
+ modeSource: "default"
737
+ };
738
+ }
739
+ /**
740
+ * Extract all modifier overrides into a lookup map
741
+ * Returns: Map<tokenPath, Map<contextName, value>>
742
+ */
743
+ function extractModifierOverrides(doc, preferredModifier) {
744
+ const overrides = /* @__PURE__ */ new Map();
745
+ const modifier = doc.$modifiers?.[preferredModifier];
746
+ if (!modifier?.contexts) return overrides;
747
+ for (const [contextName, context] of Object.entries(modifier.contexts)) walkModifierContext(context, "", (tokenPath, value) => {
748
+ if (!overrides.has(tokenPath)) overrides.set(tokenPath, /* @__PURE__ */ new Map());
749
+ overrides.get(tokenPath).set(contextName, value);
750
+ });
751
+ return overrides;
752
+ }
753
+ /**
754
+ * Walk a modifier context to extract token overrides
755
+ */
756
+ function walkModifierContext(context, parentPath, callback) {
757
+ for (const [key, value] of Object.entries(context)) {
758
+ const path = parentPath ? `${parentPath}.${key}` : key;
759
+ if (value && typeof value === "object" && "$value" in value) callback(path, value.$value);
760
+ else if (value && typeof value === "object") walkModifierContext(value, path, callback);
761
+ }
762
+ }
763
+ /**
764
+ * Convert a DTCG token to FigmaExportVariable, applying modifier overrides
765
+ */
766
+ function tokenToVariableWithModifiers(path, token, inheritedType, modes, defaultModeName, modifierOverrides) {
767
+ const figmaType = dtcgTypeToFigma(token.$type || inheritedType || "string");
768
+ const isAlias = isDTCGAlias(token.$value);
769
+ let aliasTo;
770
+ const values = {};
771
+ if (isAlias) {
772
+ aliasTo = parseAlias(token.$value);
773
+ values[defaultModeName] = {
774
+ type: "VARIABLE_ALIAS",
775
+ id: dtcgPathToFigma(aliasTo)
776
+ };
777
+ } else {
778
+ values[defaultModeName] = convertValueToFigma(token.$value, figmaType);
779
+ const tokenPath = path.replace(/\//g, ".");
780
+ const pathOverrides = modifierOverrides.get(tokenPath);
781
+ if (pathOverrides) for (const [modeName, modeValue] of pathOverrides) if (isDTCGAlias(modeValue)) values[modeName] = {
782
+ type: "VARIABLE_ALIAS",
783
+ id: dtcgPathToFigma(parseAlias(modeValue))
784
+ };
785
+ else values[modeName] = convertValueToFigma(modeValue, figmaType);
786
+ if (!pathOverrides || pathOverrides.size === 0) {
787
+ const legacyModeOverrides = token.$extensions?.["dev.styleframe"]?.modes;
788
+ if (legacyModeOverrides) for (const [modeName, modeValue] of Object.entries(legacyModeOverrides)) if (isDTCGAlias(modeValue)) values[modeName] = {
789
+ type: "VARIABLE_ALIAS",
790
+ id: dtcgPathToFigma(parseAlias(modeValue))
791
+ };
792
+ else values[modeName] = convertValueToFigma(modeValue, figmaType);
793
+ }
794
+ }
795
+ return {
796
+ name: path,
797
+ styleframeName: path.replace(/\//g, "."),
798
+ type: figmaType,
799
+ values,
800
+ aliasTo,
801
+ description: token.$description
802
+ };
803
+ }
804
+ /**
805
+ * Convert a DTCG value to Figma format
806
+ */
807
+ function convertValueToFigma(value, figmaType) {
808
+ if (figmaType === "COLOR" && typeof value === "string") {
809
+ const rgba = cssColorToFigma(value);
810
+ if (rgba) return rgba;
811
+ }
812
+ if (figmaType === "FLOAT") {
813
+ if (typeof value === "number") return value;
814
+ if (typeof value === "string") {
815
+ const pixels = dimensionToPixels(value);
816
+ if (pixels !== null) return pixels;
817
+ const num = Number.parseFloat(value);
818
+ if (!Number.isNaN(num)) return num;
819
+ }
820
+ }
821
+ if (figmaType === "BOOLEAN") {
822
+ if (typeof value === "boolean") return value;
823
+ if (value === "true") return true;
824
+ if (value === "false") return false;
825
+ }
826
+ return String(value);
827
+ }
828
+
829
+ //#endregion
830
+ export { createAlias, cssColorToFigma, cssVariableToStyleframeName, detectDTCGType, detectFigmaType, dimensionToPixels, dtcgPathToFigma, dtcgTypeToFigma, extractCategory, extractKey, figmaColorToCss, figmaPathToDTCG, figmaToStyleframeName, figmaTypeToDTCG, figmaValueToStyleframe, fromDTCG, generateStyleframeCode, getPathSegments, isColorValue, isDTCGAlias, isDTCGGroup, isDTCGToken, isDimensionValue, joinPath, parseAlias, parseDimension, pixelsToDimension, resolveAliasForFigma, styleframeToCssVariable, styleframeToFigmaName, styleframeValueToFigma, toDTCG, toVariableIdentifier };
831
+ //# sourceMappingURL=index.js.map