@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.d.ts +440 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +831 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin/code.js +3803 -0
- package/dist/plugin/ui.html +1045 -0
- package/manifest.json +16 -0
- package/package.json +61 -0
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
|