inkhouse 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +201 -0
  2. package/bin/inkhouse.mjs +171 -0
  3. package/code.js +11802 -0
  4. package/manifest.json +30 -0
  5. package/package.json +45 -0
  6. package/scanner/blob-placement-regression.ts +132 -0
  7. package/scanner/class-collector.ts +69 -0
  8. package/scanner/cli.ts +336 -0
  9. package/scanner/component-scanner.ts +2876 -0
  10. package/scanner/css-patch-regression.ts +112 -0
  11. package/scanner/css-token-reader-regression.ts +92 -0
  12. package/scanner/css-token-reader.ts +477 -0
  13. package/scanner/font-style-resolver-regression.ts +32 -0
  14. package/scanner/index.ts +9 -0
  15. package/scanner/radial-gradient-regression.ts +53 -0
  16. package/scanner/style-map.ts +145 -0
  17. package/scanner/tailwind-parser.ts +644 -0
  18. package/scanner/transform-math-regression.ts +42 -0
  19. package/scanner/types.ts +298 -0
  20. package/src/blob-placement.ts +111 -0
  21. package/src/change-detection.ts +204 -0
  22. package/src/class-utils.ts +105 -0
  23. package/src/clip-path-decorative.ts +194 -0
  24. package/src/color-resolver.ts +98 -0
  25. package/src/colors.ts +196 -0
  26. package/src/component-defs.ts +54 -0
  27. package/src/component-gen.ts +561 -0
  28. package/src/component-lookup.ts +82 -0
  29. package/src/config.ts +115 -0
  30. package/src/design-system.ts +59 -0
  31. package/src/dev-server.ts +173 -0
  32. package/src/figma-globals.d.ts +3 -0
  33. package/src/font-style-resolver.ts +171 -0
  34. package/src/github.ts +1465 -0
  35. package/src/icon-builder.ts +607 -0
  36. package/src/image-cache.ts +22 -0
  37. package/src/inline-text.ts +271 -0
  38. package/src/layout-parser.ts +667 -0
  39. package/src/layout-utils.ts +155 -0
  40. package/src/main.ts +687 -0
  41. package/src/node-ir.ts +595 -0
  42. package/src/pack-provider.ts +148 -0
  43. package/src/packs.ts +126 -0
  44. package/src/radial-gradient.ts +84 -0
  45. package/src/render-context.ts +138 -0
  46. package/src/responsive-analyzer.ts +139 -0
  47. package/src/state-analyzer.ts +143 -0
  48. package/src/story-builder.ts +1706 -0
  49. package/src/story-layout.ts +38 -0
  50. package/src/tailwind.ts +2379 -0
  51. package/src/text-builder.ts +116 -0
  52. package/src/text-line.ts +42 -0
  53. package/src/token-source.ts +43 -0
  54. package/src/tokens.ts +717 -0
  55. package/src/transform-math.ts +44 -0
  56. package/src/ui-builder.ts +1996 -0
  57. package/src/utility-resolver.ts +125 -0
  58. package/src/variables.ts +1042 -0
  59. package/src/width-solver.ts +466 -0
  60. package/templates/patch-tokens-route.ts +165 -0
  61. package/templates/scan-components-route.ts +57 -0
  62. package/ui.html +1222 -0
@@ -0,0 +1,1042 @@
1
+ import { TOKENS } from './tokens';
2
+ import { parseColor, colorToLabel, debug, normalizeThemeName, normalizeGroupName, normalizeSizeValue } from './colors';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Module-level state
6
+ // ---------------------------------------------------------------------------
7
+
8
+ let _variableCollection: any = null; // Single-collection (paid) or default-theme collection (free)
9
+ let _themeCollections: Record<string, any> = {}; // Free-plan: one collection per theme
10
+ let _variableModeIds: Record<string, string> = {}; // Multi-mode: theme -> modeId
11
+ let _defaultThemeName = 'primary';
12
+ let _colorVariables: Record<string, any> = {}; // default theme tokenKey -> Variable
13
+ let _themeColorVariables: Record<string, Record<string, any>> = {}; // theme -> tokenKey -> Variable
14
+ let _radiusVariables: Record<string, any> = {}; // key -> FLOAT Variable (radius/sm, radius/md …)
15
+ let _fontVariables: Record<string, any> = {}; // key -> STRING Variable (font/sans …)
16
+ let _spacingVariables: Record<string, any> = {}; // key -> FLOAT Variable (spacing/sm, spacing/lg …)
17
+ let _fontSizeVariables: Record<string, any> = {}; // key -> FLOAT Variable (fontSize/sm, fontSize/xl …)
18
+ let _useMultiMode = false; // true if paid plan (multi-mode available)
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Functions
22
+ // ---------------------------------------------------------------------------
23
+
24
+ function normalizeCustomThemeName(value: unknown): string | null {
25
+ const raw = String(value || '').trim();
26
+ if (!raw) return null;
27
+ const known = normalizeThemeName(raw);
28
+ if (known) return known;
29
+ const slug = raw
30
+ .toLowerCase()
31
+ .replace(/[^a-z0-9_-]+/g, '-')
32
+ .replace(/-+/g, '-')
33
+ .replace(/^-|-$/g, '');
34
+ if (!slug) return null;
35
+ if (/^mode[-_ ]?\d+$/i.test(slug)) return null;
36
+ if (slug === 'default') return 'primary';
37
+ return slug;
38
+ }
39
+
40
+ function getThemeDisplayName(theme: string): string {
41
+ return String(theme || '')
42
+ .split(/[-_\s]+/)
43
+ .filter(Boolean)
44
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
45
+ .join(' ');
46
+ }
47
+
48
+ function getThemeNamesFromTokens(): string[] {
49
+ const names = Object.keys(TOKENS || {}).filter((key) => {
50
+ if (key === 'core') return false;
51
+ const block = (TOKENS as any)[key];
52
+ if (!block || typeof block !== 'object') return false;
53
+ return Boolean(
54
+ block.color || block.radius || block.font || block.spacing || block.fontSize || block.shadow
55
+ );
56
+ });
57
+ if (names.length === 0) return ['primary'];
58
+ if (!names.includes('primary')) return names;
59
+ return ['primary'].concat(names.filter((name) => name !== 'primary'));
60
+ }
61
+
62
+ function getThemeGroup(theme: string, group: string): Record<string, any> {
63
+ const block = ((TOKENS as any)[theme] && typeof (TOKENS as any)[theme] === 'object')
64
+ ? (TOKENS as any)[theme]
65
+ : {};
66
+ const value = block[group];
67
+ return value && typeof value === 'object' ? value : {};
68
+ }
69
+
70
+ function findCollectionByName(collections: any[], name: string): any {
71
+ const needle = String(name || '').toLowerCase();
72
+ return collections.find((c: any) => String(c.name || '').toLowerCase() === needle) || null;
73
+ }
74
+
75
+ export function resolveVariableValue(value: any, modeId?: string, visited?: Record<string, boolean>): any {
76
+ if (!value) return null;
77
+ if (value.type === 'VARIABLE_ALIAS' && value.id) {
78
+ if (!visited) visited = {};
79
+ if (visited[value.id]) return null;
80
+ visited[value.id] = true;
81
+ if (!figma.variables || !figma.variables.getVariableById) return null;
82
+ const target = figma.variables.getVariableById(value.id);
83
+ if (!target || !target.valuesByMode) return null;
84
+ const vbm = target.valuesByMode;
85
+ const direct = (modeId && vbm[modeId] !== undefined) ? vbm[modeId] : vbm[Object.keys(vbm)[0]];
86
+ return resolveVariableValue(direct, modeId, visited);
87
+ }
88
+ return value;
89
+ }
90
+
91
+ export function readVariableTokens(): any {
92
+ if (!figma.variables || !figma.variables.getLocalVariables) {
93
+ debug('Variables API unavailable, using embedded tokens.');
94
+ return null;
95
+ }
96
+ const vars = figma.variables.getLocalVariables();
97
+ if (!vars || !vars.length) {
98
+ debug('No local variables found, using embedded tokens.');
99
+ return null;
100
+ }
101
+ const collections = figma.variables.getLocalVariableCollections ? figma.variables.getLocalVariableCollections() : [];
102
+ const collectionById: Record<string, any> = {};
103
+ const modeNameById: Record<string, string> = {};
104
+ for (const col of collections) {
105
+ collectionById[col.id] = col;
106
+ if (col.modes) {
107
+ for (const mode of col.modes) {
108
+ modeNameById[mode.modeId] = mode.name;
109
+ }
110
+ }
111
+ }
112
+
113
+ // Full token structure read back from Figma Variables.
114
+ // Keep this dynamic: theme keys can be arbitrary (not just primary/secondary).
115
+ const out: Record<string, any> = {
116
+ core: { font: {} },
117
+ primary: { color: {}, radius: {}, font: {}, spacing: {}, fontSize: {} },
118
+ };
119
+ let wrote = false;
120
+
121
+ function ensureTheme(themeName: string): Record<string, any> {
122
+ if (!out[themeName] || typeof out[themeName] !== 'object') {
123
+ out[themeName] = {};
124
+ }
125
+ const block = out[themeName] as Record<string, any>;
126
+ if (!block.color || typeof block.color !== 'object') block.color = {};
127
+ if (!block.radius || typeof block.radius !== 'object') block.radius = {};
128
+ if (!block.font || typeof block.font !== 'object') block.font = {};
129
+ if (!block.spacing || typeof block.spacing !== 'object') block.spacing = {};
130
+ if (!block.fontSize || typeof block.fontSize !== 'object') block.fontSize = {};
131
+ return block;
132
+ }
133
+
134
+ function assign(theme: string, group: string, parts: string[], value: any): void {
135
+ if (!value) return;
136
+ const key = parts && parts.length ? parts.join('-') : null;
137
+ if (!key) return;
138
+
139
+ const resolvedTheme = theme && theme !== 'core' ? theme : 'primary';
140
+ let target: Record<string, any>;
141
+
142
+ if (group === 'color') {
143
+ target = ensureTheme(resolvedTheme).color;
144
+ target[key] = value;
145
+ wrote = true;
146
+ return;
147
+ }
148
+ if (group === 'radius') {
149
+ target = ensureTheme(resolvedTheme).radius;
150
+ target[key] = normalizeSizeValue(value);
151
+ wrote = true;
152
+ return;
153
+ }
154
+ if (group === 'font') {
155
+ // Route unscoped fonts into core, scoped fonts into their theme block.
156
+ if (!theme || theme === 'core') target = out.core.font;
157
+ else target = ensureTheme(theme).font;
158
+ target[key] = String(value);
159
+ wrote = true;
160
+ return;
161
+ }
162
+ if (group === 'spacing') {
163
+ target = ensureTheme(resolvedTheme).spacing;
164
+ target[key] = normalizeSizeValue(value);
165
+ wrote = true;
166
+ return;
167
+ }
168
+ if (group === 'fontsize') {
169
+ target = ensureTheme(resolvedTheme).fontSize;
170
+ target[key] = normalizeSizeValue(value);
171
+ wrote = true;
172
+ return;
173
+ }
174
+ // Ignore fontWeight, tracking, typography - these come from Tailwind
175
+ }
176
+
177
+ for (const variable of vars) {
178
+ const rawName = String(variable.name || '');
179
+ if (!rawName) continue;
180
+ const parts = rawName.split(/[./]/).filter(Boolean);
181
+ if (!parts.length) continue;
182
+ const firstPartIsGroup = !!normalizeGroupName(parts[0]);
183
+ let theme = firstPartIsGroup ? null : (normalizeThemeName(parts[0]) || normalizeCustomThemeName(parts[0]));
184
+ let cursor = parts.slice(theme ? 1 : 0);
185
+ let group = normalizeGroupName(cursor[0]);
186
+
187
+ if (!group) {
188
+ // Backward compatibility: older variable sets used flat color names like
189
+ // `primary`, `foreground` (without `color/` prefix). Infer group by type.
190
+ if (String((variable as any).resolvedType || '').toUpperCase() === 'COLOR') {
191
+ group = 'color';
192
+ if (cursor.length === 0) {
193
+ theme = null;
194
+ cursor = parts.slice(0);
195
+ }
196
+ } else {
197
+ continue;
198
+ }
199
+ } else {
200
+ cursor = cursor.slice(1);
201
+ }
202
+
203
+ const valuesByMode = variable.valuesByMode || {};
204
+ const modeIds = Object.keys(valuesByMode);
205
+ if (!modeIds.length) continue;
206
+
207
+ const collection = collectionById[variable.variableCollectionId];
208
+ const isSingleModeCollection = !!(collection && Array.isArray(collection.modes) && collection.modes.length === 1);
209
+ for (const modeId of modeIds) {
210
+ const modeName = modeNameById[modeId];
211
+ const modeTheme =
212
+ theme ||
213
+ normalizeThemeName(modeName) ||
214
+ normalizeCustomThemeName(modeName) ||
215
+ (isSingleModeCollection
216
+ ? (
217
+ normalizeThemeName(collection && collection.name) ||
218
+ normalizeCustomThemeName(collection && collection.name)
219
+ )
220
+ : null);
221
+ if (!modeTheme && !isSingleModeCollection) {
222
+ // Ignore unknown/legacy extra modes in multi-mode collections.
223
+ continue;
224
+ }
225
+ const value = resolveVariableValue(valuesByMode[modeId], modeId);
226
+ const finalTheme = modeTheme || (group === 'font' ? 'core' : 'primary');
227
+ assign(finalTheme, group, cursor, value);
228
+ if (theme) break;
229
+ }
230
+ }
231
+
232
+ return wrote ? out : null;
233
+ }
234
+
235
+ export function upsertPaintStyle(name: string, color: { r: number; g: number; b: number; a?: number }): any {
236
+ const styles = figma.getLocalPaintStyles();
237
+ const existing = styles.find((s: any) => s.name === name);
238
+ const paint = { type: 'SOLID' as const, color: { r: color.r, g: color.g, b: color.b }, opacity: (color.a == null ? 1 : color.a) };
239
+ if (existing) {
240
+ existing.paints = [paint];
241
+ return existing;
242
+ } else {
243
+ const s = figma.createPaintStyle();
244
+ s.name = name;
245
+ s.paints = [paint];
246
+ return s;
247
+ }
248
+ }
249
+
250
+ // ---------------------------------------------------------------------------
251
+ // Shadow Effect Styles
252
+ // ---------------------------------------------------------------------------
253
+
254
+ interface ShadowLayer {
255
+ inset: boolean;
256
+ x: number;
257
+ y: number;
258
+ blur: number;
259
+ spread: number;
260
+ r: number;
261
+ g: number;
262
+ b: number;
263
+ a: number;
264
+ }
265
+
266
+ /**
267
+ * Parse a CSS `rgb(r g b / a)` or `rgba(r, g, b, a)` color string into 0–1 floats.
268
+ */
269
+ function parseCssShadowColor(raw: string): { r: number; g: number; b: number; a: number } {
270
+ // rgb(r g b / a) — CSS Color Level 4
271
+ const lvl4 = raw.match(/^rgb\(\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)\s*\/\s*([\d.]+)\s*\)$/);
272
+ if (lvl4) {
273
+ return {
274
+ r: parseFloat(lvl4[1]) / 255,
275
+ g: parseFloat(lvl4[2]) / 255,
276
+ b: parseFloat(lvl4[3]) / 255,
277
+ a: parseFloat(lvl4[4]),
278
+ };
279
+ }
280
+ // rgba(r, g, b, a)
281
+ const legacy = raw.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)$/);
282
+ if (legacy) {
283
+ return {
284
+ r: parseFloat(legacy[1]) / 255,
285
+ g: parseFloat(legacy[2]) / 255,
286
+ b: parseFloat(legacy[3]) / 255,
287
+ a: legacy[4] != null ? parseFloat(legacy[4]) : 1,
288
+ };
289
+ }
290
+ return { r: 0, g: 0, b: 0, a: 0.1 };
291
+ }
292
+
293
+ /**
294
+ * Parse a CSS box-shadow value string (possibly multi-layer, comma-separated)
295
+ * into an array of ShadowLayer objects.
296
+ * Format per layer: [inset] <offset-x> <offset-y> [blur] [spread] <color>
297
+ */
298
+ function parseCssBoxShadow(value: string): ShadowLayer[] {
299
+ // Split on commas that are NOT inside parentheses
300
+ const layers: string[] = [];
301
+ let depth = 0;
302
+ let start = 0;
303
+ for (let i = 0; i < value.length; i++) {
304
+ if (value[i] === '(') depth++;
305
+ else if (value[i] === ')') depth--;
306
+ else if (value[i] === ',' && depth === 0) {
307
+ layers.push(value.slice(start, i).trim());
308
+ start = i + 1;
309
+ }
310
+ }
311
+ layers.push(value.slice(start).trim());
312
+
313
+ const result: ShadowLayer[] = [];
314
+ for (const layer of layers) {
315
+ if (!layer) continue;
316
+ const inset = /\binset\b/.test(layer);
317
+ const withoutInset = layer.replace(/\binset\b/, '').trim();
318
+
319
+ // Extract color: find rgb/rgba(...) or hex
320
+ const colorMatch = withoutInset.match(/(rgba?\([^)]+\)|#[0-9a-fA-F]{3,8})/);
321
+ const colorStr = colorMatch ? colorMatch[1] : 'rgb(0 0 0 / 0.1)';
322
+ const withoutColor = withoutInset.replace(colorStr, '').trim();
323
+
324
+ const lengths = withoutColor.split(/\s+/).filter(Boolean);
325
+ const px = (s: string): number => {
326
+ if (!s) return 0;
327
+ if (s.endsWith('px')) return parseFloat(s);
328
+ if (s === '0') return 0;
329
+ return parseFloat(s) || 0;
330
+ };
331
+
332
+ const color = parseCssShadowColor(colorStr);
333
+ result.push({
334
+ inset,
335
+ x: px(lengths[0] || '0'),
336
+ y: px(lengths[1] || '0'),
337
+ blur: px(lengths[2] || '0'),
338
+ spread: px(lengths[3] || '0'),
339
+ r: color.r,
340
+ g: color.g,
341
+ b: color.b,
342
+ a: color.a,
343
+ });
344
+ }
345
+ return result;
346
+ }
347
+
348
+ /**
349
+ * Create or update a Figma Effect Style for a shadow token.
350
+ * Name format: "shadow/sm", "shadow/DEFAULT" → "shadow", etc.
351
+ */
352
+ function upsertEffectStyle(name: string, cssValue: string): void {
353
+ if (!figma.getLocalEffectStyles) return;
354
+ const layers = parseCssBoxShadow(cssValue);
355
+ if (!layers.length) return;
356
+
357
+ const effects: any[] = [];
358
+ for (const l of layers) {
359
+ effects.push({
360
+ type: l.inset ? 'INNER_SHADOW' : 'DROP_SHADOW',
361
+ color: { r: l.r, g: l.g, b: l.b, a: l.a },
362
+ offset: { x: l.x, y: l.y },
363
+ radius: l.blur,
364
+ spread: l.spread,
365
+ visible: true,
366
+ blendMode: 'NORMAL',
367
+ });
368
+ }
369
+
370
+ const existing = figma.getLocalEffectStyles().find((s: any) => s.name === name);
371
+ if (existing) {
372
+ existing.effects = effects;
373
+ } else {
374
+ const s = figma.createEffectStyle();
375
+ s.name = name;
376
+ s.effects = effects;
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Create/update Figma Effect Styles for all shadow tokens.
382
+ */
383
+ export function populateShadowStyles(shadowTokens: Record<string, string>): void {
384
+ for (const key in shadowTokens) {
385
+ const styleName = key === 'DEFAULT' ? 'shadow' : 'shadow/' + key;
386
+ upsertEffectStyle(styleName, shadowTokens[key]);
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Remove stale paint styles from previous runs (LIGHT, DARK, PRIMARY, SECONDARY groups)
392
+ */
393
+ export function removeStaleColorStyles(): void {
394
+ const styles = figma.getLocalPaintStyles();
395
+ const stalePrefix = ['LIGHT/', 'DARK/', 'PRIMARY/', 'SECONDARY/'];
396
+ for (const s of styles) {
397
+ if (stalePrefix.some(p => s.name.startsWith(p))) {
398
+ s.remove();
399
+ }
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Populate variables in a collection for a given theme's color tokens.
405
+ */
406
+ export function populateColorVariables(collection: any, modeId: string, colorTokens: Record<string, any>, outMap: Record<string, any>): void {
407
+ // Index existing variables in this collection
408
+ const existingVars = figma.variables.getLocalVariables('COLOR');
409
+ const existingByName: Record<string, any> = {};
410
+ for (const v of existingVars) {
411
+ if (v.variableCollectionId === collection.id) {
412
+ existingByName[v.name] = v;
413
+ }
414
+ }
415
+
416
+ for (const key in colorTokens) {
417
+ const varName = 'color/' + key;
418
+ let variable = existingByName[varName];
419
+ if (!variable) {
420
+ variable = figma.variables.createVariable(varName, collection, 'COLOR');
421
+ }
422
+ const rgb = parseColor(colorTokens[key]);
423
+ variable.setValueForMode(modeId, { r: rgb.r, g: rgb.g, b: rgb.b, a: rgb.a == null ? 1 : rgb.a });
424
+ outMap[key] = variable;
425
+ }
426
+ }
427
+
428
+ /**
429
+ * Populate FLOAT radius variables in a collection for a given mode.
430
+ */
431
+ export function populateRadiusVariables(collection: any, modeId: string, radiusTokens: Record<string, string>, outMap: Record<string, any>): void {
432
+ const existingVars = figma.variables.getLocalVariables('FLOAT');
433
+ const existingByName: Record<string, any> = {};
434
+ for (const v of existingVars) {
435
+ if (v.variableCollectionId === collection.id) {
436
+ existingByName[v.name] = v;
437
+ }
438
+ }
439
+ for (const key in radiusTokens) {
440
+ const varName = 'radius/' + key;
441
+ let variable = existingByName[varName];
442
+ if (!variable) {
443
+ variable = figma.variables.createVariable(varName, collection, 'FLOAT');
444
+ }
445
+ variable.setValueForMode(modeId, pxFromSizeToken(radiusTokens[key]));
446
+ outMap[key] = variable;
447
+ }
448
+ }
449
+
450
+ /**
451
+ * Populate STRING font-family variables in a collection for a given mode.
452
+ */
453
+ export function populateFontVariables(collection: any, modeId: string, fontTokens: Record<string, string>, outMap: Record<string, any>): void {
454
+ const existingVars = figma.variables.getLocalVariables('STRING');
455
+ const existingByName: Record<string, any> = {};
456
+ for (const v of existingVars) {
457
+ if (v.variableCollectionId === collection.id) {
458
+ existingByName[v.name] = v;
459
+ }
460
+ }
461
+ for (const key in fontTokens) {
462
+ const varName = 'font/' + key;
463
+ let variable = existingByName[varName];
464
+ if (!variable) {
465
+ variable = figma.variables.createVariable(varName, collection, 'STRING');
466
+ }
467
+ // Store the first family name only (no fallback stack in Figma)
468
+ const first = String(fontTokens[key]).split(',')[0].trim().replace(/^["']|["']$/g, '');
469
+ variable.setValueForMode(modeId, first);
470
+ outMap[key] = variable;
471
+ }
472
+ }
473
+
474
+ /**
475
+ * Populate FLOAT spacing variables in a collection for a given mode.
476
+ */
477
+ export function populateSpacingVariables(collection: any, modeId: string, spacingTokens: Record<string, string>, outMap: Record<string, any>): void {
478
+ const existingVars = figma.variables.getLocalVariables('FLOAT');
479
+ const existingByName: Record<string, any> = {};
480
+ for (const v of existingVars) {
481
+ if (v.variableCollectionId === collection.id) existingByName[v.name] = v;
482
+ }
483
+ for (const key in spacingTokens) {
484
+ const varName = 'spacing/' + key;
485
+ let variable = existingByName[varName];
486
+ if (!variable) {
487
+ variable = figma.variables.createVariable(varName, collection, 'FLOAT');
488
+ }
489
+ variable.setValueForMode(modeId, pxFromSizeToken(spacingTokens[key]));
490
+ outMap[key] = variable;
491
+ }
492
+ }
493
+
494
+ /**
495
+ * Populate FLOAT font-size variables in a collection for a given mode.
496
+ */
497
+ export function populateFontSizeVariables(collection: any, modeId: string, fontSizeTokens: Record<string, string>, outMap: Record<string, any>): void {
498
+ const existingVars = figma.variables.getLocalVariables('FLOAT');
499
+ const existingByName: Record<string, any> = {};
500
+ for (const v of existingVars) {
501
+ if (v.variableCollectionId === collection.id) existingByName[v.name] = v;
502
+ }
503
+ for (const key in fontSizeTokens) {
504
+ const varName = 'fontSize/' + key;
505
+ let variable = existingByName[varName];
506
+ if (!variable) {
507
+ variable = figma.variables.createVariable(varName, collection, 'FLOAT');
508
+ }
509
+ variable.setValueForMode(modeId, pxFromSizeToken(fontSizeTokens[key]));
510
+ outMap[key] = variable;
511
+ }
512
+ }
513
+
514
+ /**
515
+ * Create or update color variables.
516
+ * Tries multi-mode first (paid plans). Falls back to two collections (free plan).
517
+ */
518
+ export function createOrUpdateVariables(): void {
519
+ if (!figma.variables || !figma.variables.createVariableCollection) {
520
+ debug('Variables API not available, falling back to paint styles');
521
+ createOrUpdateStylesFallback();
522
+ return;
523
+ }
524
+
525
+ const themeNames = getThemeNamesFromTokens();
526
+ _defaultThemeName = themeNames.includes('primary') ? 'primary' : themeNames[0];
527
+ const defaultTheme = _defaultThemeName;
528
+ const defaultDisplayName = getThemeDisplayName(defaultTheme);
529
+
530
+ const themeColors: Record<string, Record<string, any>> = {};
531
+ const themeRadius: Record<string, Record<string, any>> = {};
532
+ const themeFont: Record<string, Record<string, any>> = {};
533
+ const themeSpacing: Record<string, Record<string, any>> = {};
534
+ const themeFontSize: Record<string, Record<string, any>> = {};
535
+ for (const theme of themeNames) {
536
+ themeColors[theme] = getThemeGroup(theme, 'color');
537
+ themeRadius[theme] = getThemeGroup(theme, 'radius');
538
+ themeFont[theme] = getThemeGroup(theme, 'font');
539
+ themeSpacing[theme] = getThemeGroup(theme, 'spacing');
540
+ themeFontSize[theme] = getThemeGroup(theme, 'fontSize');
541
+ }
542
+
543
+ _themeCollections = {};
544
+ _themeColorVariables = {};
545
+ _variableModeIds = {};
546
+ _colorVariables = {};
547
+ _radiusVariables = {};
548
+ _fontVariables = {};
549
+ _spacingVariables = {};
550
+ _fontSizeVariables = {};
551
+
552
+ const collections = figma.variables.getLocalVariableCollections();
553
+
554
+ function ensureThemeModes(collection: any): boolean {
555
+ const map: Record<string, string> = {};
556
+ const modes = Array.isArray(collection.modes) ? collection.modes.slice() : [];
557
+ const unclaimed: string[] = modes.map((mode: any) => mode.modeId);
558
+ const removeUnclaimed = (modeId: string): void => {
559
+ const idx = unclaimed.indexOf(modeId);
560
+ if (idx >= 0) unclaimed.splice(idx, 1);
561
+ };
562
+
563
+ for (const mode of modes) {
564
+ const mapped = normalizeThemeName(mode.name) || normalizeCustomThemeName(mode.name);
565
+ if (mapped && themeNames.includes(mapped) && !map[mapped]) {
566
+ map[mapped] = mode.modeId;
567
+ removeUnclaimed(mode.modeId);
568
+ }
569
+ }
570
+
571
+ for (const theme of themeNames) {
572
+ if (map[theme]) continue;
573
+ if (unclaimed.length > 0) {
574
+ const modeId = unclaimed.shift() as string;
575
+ try {
576
+ collection.renameMode(modeId, getThemeDisplayName(theme));
577
+ } catch (_e) {}
578
+ map[theme] = modeId;
579
+ continue;
580
+ }
581
+ try {
582
+ map[theme] = collection.addMode(getThemeDisplayName(theme));
583
+ } catch (e: any) {
584
+ debug('Cannot add mode (free plan): ' + (e.message || e));
585
+ return false;
586
+ }
587
+ }
588
+
589
+ for (const theme of themeNames) {
590
+ const modeId = map[theme];
591
+ if (!modeId) continue;
592
+ try {
593
+ collection.renameMode(modeId, getThemeDisplayName(theme));
594
+ } catch (_e) {}
595
+ }
596
+
597
+ // Rename leftover modes to neutral names so they don't map to semantic themes.
598
+ let legacyIndex = 1;
599
+ for (const modeId of unclaimed) {
600
+ try {
601
+ collection.renameMode(modeId, 'Mode ' + legacyIndex++);
602
+ } catch (_e) {}
603
+ }
604
+
605
+ _variableModeIds = map;
606
+ return true;
607
+ }
608
+
609
+ _variableCollection =
610
+ findCollectionByName(collections, 'Design Tokens') ||
611
+ findCollectionByName(collections, defaultDisplayName) ||
612
+ findCollectionByName(collections, 'Primary');
613
+
614
+ _useMultiMode = false;
615
+ if (_variableCollection) {
616
+ _useMultiMode = ensureThemeModes(_variableCollection);
617
+ } else {
618
+ _variableCollection = figma.variables.createVariableCollection('Design Tokens');
619
+ _useMultiMode = ensureThemeModes(_variableCollection);
620
+ }
621
+
622
+ if (_useMultiMode) {
623
+ const allColorKeys: Record<string, boolean> = {};
624
+ const allRadiusKeys: Record<string, boolean> = {};
625
+ const allFontKeys: Record<string, boolean> = {};
626
+ const allSpacingKeys: Record<string, boolean> = {};
627
+ const allFontSizeKeys: Record<string, boolean> = {};
628
+ for (const theme of themeNames) {
629
+ for (const key in themeColors[theme]) allColorKeys[key] = true;
630
+ for (const key in themeRadius[theme]) allRadiusKeys[key] = true;
631
+ for (const key in themeFont[theme]) allFontKeys[key] = true;
632
+ for (const key in themeSpacing[theme]) allSpacingKeys[key] = true;
633
+ for (const key in themeFontSize[theme]) allFontSizeKeys[key] = true;
634
+ }
635
+
636
+ const existingColorVars = figma.variables.getLocalVariables('COLOR');
637
+ const existingColorByName: Record<string, any> = {};
638
+ for (const v of existingColorVars) {
639
+ if (v.variableCollectionId === _variableCollection.id) {
640
+ existingColorByName[v.name] = v;
641
+ }
642
+ }
643
+
644
+ const existingFloatVars = figma.variables.getLocalVariables('FLOAT');
645
+ const existingFloatByName: Record<string, any> = {};
646
+ for (const v of existingFloatVars) {
647
+ if (v.variableCollectionId === _variableCollection.id) {
648
+ existingFloatByName[v.name] = v;
649
+ }
650
+ }
651
+
652
+ const existingStringVars = figma.variables.getLocalVariables('STRING');
653
+ const existingStringByName: Record<string, any> = {};
654
+ for (const v of existingStringVars) {
655
+ if (v.variableCollectionId === _variableCollection.id) {
656
+ existingStringByName[v.name] = v;
657
+ }
658
+ }
659
+
660
+ for (const theme of themeNames) _themeColorVariables[theme] = {};
661
+
662
+ const defaultColors = themeColors[defaultTheme] || {};
663
+ for (const key in allColorKeys) {
664
+ const varName = 'color/' + key;
665
+ let variable = existingColorByName[varName];
666
+ if (!variable) variable = figma.variables.createVariable(varName, _variableCollection, 'COLOR');
667
+
668
+ for (const theme of themeNames) {
669
+ const modeId = _variableModeIds[theme];
670
+ if (!modeId) continue;
671
+ const value = (themeColors[theme] && themeColors[theme][key] !== undefined)
672
+ ? themeColors[theme][key]
673
+ : defaultColors[key];
674
+ if (value === undefined) continue;
675
+ const rgb = parseColor(value);
676
+ variable.setValueForMode(modeId, { r: rgb.r, g: rgb.g, b: rgb.b, a: rgb.a == null ? 1 : rgb.a });
677
+ _themeColorVariables[theme][key] = variable;
678
+ }
679
+ }
680
+ _colorVariables = _themeColorVariables[defaultTheme] || {};
681
+
682
+ const defaultRadius = themeRadius[defaultTheme] || {};
683
+ for (const key in allRadiusKeys) {
684
+ const varName = 'radius/' + key;
685
+ let variable = existingFloatByName[varName];
686
+ if (!variable) variable = figma.variables.createVariable(varName, _variableCollection, 'FLOAT');
687
+
688
+ for (const theme of themeNames) {
689
+ const modeId = _variableModeIds[theme];
690
+ if (!modeId) continue;
691
+ const value = (themeRadius[theme] && themeRadius[theme][key] !== undefined)
692
+ ? themeRadius[theme][key]
693
+ : defaultRadius[key];
694
+ if (value === undefined) continue;
695
+ variable.setValueForMode(modeId, pxFromSizeToken(value));
696
+ }
697
+ _radiusVariables[key] = variable;
698
+ }
699
+
700
+ const defaultFont = themeFont[defaultTheme] || {};
701
+ for (const key in allFontKeys) {
702
+ const varName = 'font/' + key;
703
+ let variable = existingStringByName[varName];
704
+ if (!variable) variable = figma.variables.createVariable(varName, _variableCollection, 'STRING');
705
+
706
+ for (const theme of themeNames) {
707
+ const modeId = _variableModeIds[theme];
708
+ if (!modeId) continue;
709
+ const value = (themeFont[theme] && themeFont[theme][key] !== undefined)
710
+ ? themeFont[theme][key]
711
+ : defaultFont[key];
712
+ if (value === undefined) continue;
713
+ const first = String(value).split(',')[0].trim().replace(/^["']|["']$/g, '');
714
+ variable.setValueForMode(modeId, first);
715
+ }
716
+ _fontVariables[key] = variable;
717
+ }
718
+
719
+ const defaultSpacing = themeSpacing[defaultTheme] || {};
720
+ for (const key in allSpacingKeys) {
721
+ const varName = 'spacing/' + key;
722
+ let variable = existingFloatByName[varName];
723
+ if (!variable) variable = figma.variables.createVariable(varName, _variableCollection, 'FLOAT');
724
+
725
+ for (const theme of themeNames) {
726
+ const modeId = _variableModeIds[theme];
727
+ if (!modeId) continue;
728
+ const value = (themeSpacing[theme] && themeSpacing[theme][key] !== undefined)
729
+ ? themeSpacing[theme][key]
730
+ : defaultSpacing[key];
731
+ if (value === undefined) continue;
732
+ variable.setValueForMode(modeId, pxFromSizeToken(value));
733
+ }
734
+ _spacingVariables[key] = variable;
735
+ }
736
+
737
+ const defaultFontSize = themeFontSize[defaultTheme] || {};
738
+ for (const key in allFontSizeKeys) {
739
+ const varName = 'fontSize/' + key;
740
+ let variable = existingFloatByName[varName];
741
+ if (!variable) variable = figma.variables.createVariable(varName, _variableCollection, 'FLOAT');
742
+
743
+ for (const theme of themeNames) {
744
+ const modeId = _variableModeIds[theme];
745
+ if (!modeId) continue;
746
+ const value = (themeFontSize[theme] && themeFontSize[theme][key] !== undefined)
747
+ ? themeFontSize[theme][key]
748
+ : defaultFontSize[key];
749
+ if (value === undefined) continue;
750
+ variable.setValueForMode(modeId, pxFromSizeToken(value));
751
+ }
752
+ _fontSizeVariables[key] = variable;
753
+ }
754
+
755
+ for (const col of collections) {
756
+ if (!col || col.id === _variableCollection.id) continue;
757
+ const mapped = normalizeThemeName(col.name) || normalizeCustomThemeName(col.name);
758
+ if (mapped && themeNames.includes(mapped)) {
759
+ try { col.remove(); } catch (_e) {}
760
+ }
761
+ }
762
+
763
+ debug(
764
+ 'Multi-mode: ' +
765
+ themeNames.length +
766
+ ' themes, ' +
767
+ Object.keys(_colorVariables).length + ' color + ' +
768
+ Object.keys(_radiusVariables).length + ' radius + ' +
769
+ Object.keys(_fontVariables).length + ' font variables'
770
+ );
771
+ return;
772
+ }
773
+
774
+ // --- Free plan fallback: one collection per theme (color), default theme for shared scales ---
775
+ const existingCollections = figma.variables.getLocalVariableCollections();
776
+ for (const theme of themeNames) {
777
+ const displayName = getThemeDisplayName(theme);
778
+ let collection = findCollectionByName(existingCollections, displayName);
779
+ if (!collection && theme === defaultTheme && _variableCollection) {
780
+ collection = _variableCollection;
781
+ }
782
+ if (!collection) {
783
+ collection = figma.variables.createVariableCollection(displayName);
784
+ } else if (collection.name !== displayName) {
785
+ collection.name = displayName;
786
+ }
787
+ _themeCollections[theme] = collection;
788
+ _themeColorVariables[theme] = {};
789
+
790
+ const modeId = collection.modes[0].modeId;
791
+ populateColorVariables(collection, modeId, themeColors[theme] || {}, _themeColorVariables[theme]);
792
+
793
+ if (theme === defaultTheme) {
794
+ _variableCollection = collection;
795
+ _variableModeIds[theme] = modeId;
796
+ _colorVariables = _themeColorVariables[theme];
797
+ populateRadiusVariables(collection, modeId, themeRadius[theme] || {}, _radiusVariables);
798
+ populateFontVariables(collection, modeId, themeFont[theme] || {}, _fontVariables);
799
+ populateSpacingVariables(collection, modeId, themeSpacing[theme] || {}, _spacingVariables);
800
+ populateFontSizeVariables(collection, modeId, themeFontSize[theme] || {}, _fontSizeVariables);
801
+ }
802
+ }
803
+
804
+ // Remove stale plugin theme collections no longer present in tokens.
805
+ for (const collection of existingCollections) {
806
+ if (!collection) continue;
807
+ const mapped = normalizeThemeName(collection.name) || normalizeCustomThemeName(collection.name);
808
+ if (!mapped) continue;
809
+ if (themeNames.includes(mapped)) continue;
810
+ if (collection.id === (_variableCollection && _variableCollection.id)) continue;
811
+ const varsInCollection = figma.variables.getLocalVariables().filter((v: any) => v.variableCollectionId === collection.id);
812
+ const looksLikePluginThemeCollection = varsInCollection.some((v: any) =>
813
+ /^color\//.test(String(v.name || '')) ||
814
+ /^radius\//.test(String(v.name || '')) ||
815
+ /^font\//.test(String(v.name || '')) ||
816
+ /^spacing\//.test(String(v.name || '')) ||
817
+ /^fontSize\//.test(String(v.name || ''))
818
+ );
819
+ if (!looksLikePluginThemeCollection) continue;
820
+ try { collection.remove(); } catch (_e) {}
821
+ }
822
+
823
+ debug(
824
+ 'Free plan: ' +
825
+ themeNames.length +
826
+ ' theme collections, ' +
827
+ Object.keys(_colorVariables).length + ' default-theme colors'
828
+ );
829
+ }
830
+
831
+ /**
832
+ * Fallback: create paint styles if Variables API isn't available
833
+ */
834
+ export function createOrUpdateStylesFallback(): void {
835
+ const themes = getThemeNamesFromTokens();
836
+ for (const theme of themes) {
837
+ const col: Record<string, any> = getThemeGroup(theme, 'color');
838
+ for (const [key, val] of Object.entries(col)) {
839
+ const rgb = parseColor(val);
840
+ upsertPaintStyle(`${theme.toUpperCase()}/Color/${key}`, rgb);
841
+ }
842
+ }
843
+ }
844
+
845
+ /**
846
+ * Bind a frame's fill to a color variable (semantic token).
847
+ */
848
+ export function bindColorVariable(node: any, tokenKey: string, fillOrStroke: string, theme?: string): boolean {
849
+ const resolvedTheme =
850
+ (theme && (_themeColorVariables[theme] || _variableModeIds[theme])) ? theme :
851
+ _defaultThemeName;
852
+ const variable =
853
+ (_themeColorVariables[resolvedTheme] && _themeColorVariables[resolvedTheme][tokenKey]) ||
854
+ (_themeColorVariables[_defaultThemeName] && _themeColorVariables[_defaultThemeName][tokenKey]) ||
855
+ _colorVariables[tokenKey] ||
856
+ null;
857
+ if (!variable) return false;
858
+
859
+ try {
860
+ if (fillOrStroke === 'fill') {
861
+ const fills = JSON.parse(JSON.stringify(node.fills || []));
862
+ if (fills.length === 0) {
863
+ fills.push({ type: 'SOLID', color: { r: 1, g: 1, b: 1 }, opacity: 1 });
864
+ }
865
+ fills[0] = figma.variables.setBoundVariableForPaint(fills[0], 'color', variable);
866
+ node.fills = fills;
867
+ } else if (fillOrStroke === 'stroke') {
868
+ const strokes = JSON.parse(JSON.stringify(node.strokes || []));
869
+ if (strokes.length === 0) {
870
+ strokes.push({ type: 'SOLID', color: { r: 0.9, g: 0.9, b: 0.9 } });
871
+ }
872
+ strokes[0] = figma.variables.setBoundVariableForPaint(strokes[0], 'color', variable);
873
+ node.strokes = strokes;
874
+ }
875
+ return true;
876
+ } catch (e: any) {
877
+ debug('Failed to bind variable ' + tokenKey + ': ' + (e.message || e));
878
+ return false;
879
+ }
880
+ }
881
+
882
+ /**
883
+ * Set the theme mode on a frame (only works on paid plans with multi-mode).
884
+ * On free plan this is a no-op (themes are separate collections instead).
885
+ */
886
+ export function setThemeMode(frame: any, theme: string): void {
887
+ if (!_useMultiMode || !_variableCollection) return;
888
+ const normalized = normalizeThemeName(theme) || normalizeCustomThemeName(theme);
889
+ const resolvedTheme = (theme && _variableModeIds[theme]) ? theme :
890
+ ((normalized && _variableModeIds[normalized]) ? normalized : _defaultThemeName);
891
+ const modeId = _variableModeIds[resolvedTheme];
892
+ if (!modeId) return;
893
+ try {
894
+ frame.setExplicitVariableModeForCollection(_variableCollection, modeId);
895
+ } catch (e: any) {
896
+ debug('Failed to set theme mode: ' + (e.message || e));
897
+ }
898
+ }
899
+
900
+ export function isMultiModeEnabled(): boolean {
901
+ return _useMultiMode;
902
+ }
903
+
904
+ export function pxFromSizeToken(v: unknown): number {
905
+ const baseRem = 16;
906
+ const toPx = (t: string): number => {
907
+ t = String(t).trim();
908
+ if (t.endsWith('rem')) return parseFloat(t) * baseRem;
909
+ if (t.endsWith('px')) return parseFloat(t);
910
+ return parseFloat(t);
911
+ };
912
+ const sv = String(v || '').trim();
913
+ if (sv.startsWith('calc(') && sv.endsWith(')')) {
914
+ const inner = sv.slice(5, -1);
915
+ let total = 0;
916
+ let cur = '';
917
+ let op = '+';
918
+ for (const ch of inner) {
919
+ if (ch === '+' || ch === '-') {
920
+ if (cur.trim()) {
921
+ const val = toPx(cur);
922
+ total = op === '+' ? total + val : total - val;
923
+ cur = '';
924
+ }
925
+ op = ch;
926
+ } else {
927
+ cur += ch;
928
+ }
929
+ }
930
+ if (cur.trim()) {
931
+ const val = toPx(cur);
932
+ total = op === '+' ? total + val : total - val;
933
+ }
934
+ return Math.max(0, Math.round(total));
935
+ }
936
+ return Math.max(0, Math.round(toPx(sv)));
937
+ }
938
+
939
+ /**
940
+ * Bind a frame's cornerRadius corners to a radius variable.
941
+ * radiusKey is a token key like 'md', 'lg', 'base', etc.
942
+ */
943
+ export function bindRadiusVariable(node: any, radiusKey: string, _theme?: string): boolean {
944
+ const variable = _radiusVariables[radiusKey];
945
+ if (!variable) return false;
946
+ try {
947
+ node.setBoundVariable('topLeftRadius', variable);
948
+ node.setBoundVariable('topRightRadius', variable);
949
+ node.setBoundVariable('bottomLeftRadius', variable);
950
+ node.setBoundVariable('bottomRightRadius', variable);
951
+ return true;
952
+ } catch (e: any) {
953
+ debug('Failed to bind radius variable ' + radiusKey + ': ' + (e.message || e));
954
+ return false;
955
+ }
956
+ }
957
+
958
+ export function createOrUpdateStyles(): void {
959
+ // Remove stale paint styles (LIGHT/DARK/PRIMARY/SECONDARY)
960
+ removeStaleColorStyles();
961
+ // Create Figma Variables with dynamic theme modes/collections.
962
+ createOrUpdateVariables();
963
+ // Create Figma Effect Styles for shadow tokens
964
+ const shadowTokens = getThemeGroup(_defaultThemeName, 'shadow');
965
+ if (Object.keys(shadowTokens).length) {
966
+ populateShadowStyles(shadowTokens as Record<string, string>);
967
+ }
968
+ }
969
+
970
+ export function ensureTokensPage(): any {
971
+ let page = figma.root.children.find((p: any) => p.name === 'Design Tokens');
972
+ if (!page) {
973
+ page = figma.createPage();
974
+ page.name = 'Design Tokens';
975
+ }
976
+ figma.currentPage = page;
977
+ return page;
978
+ }
979
+
980
+ export function demoFrameColors(theme: string): any {
981
+ const frame = figma.createFrame();
982
+ frame.name = theme.toUpperCase() + ' Colors';
983
+ frame.layoutMode = 'VERTICAL';
984
+ frame.primaryAxisSizingMode = 'AUTO';
985
+ frame.counterAxisSizingMode = 'AUTO';
986
+ frame.itemSpacing = 12;
987
+ frame.paddingLeft = frame.paddingRight = frame.paddingTop = frame.paddingBottom = 16;
988
+ frame.fills = [];
989
+ frame.strokes = [{ type: 'SOLID', color: { r: 0.9, g: 0.9, b: 0.9 } }];
990
+ const col: Record<string, any> = ((TOKENS as any)[theme] && (TOKENS as any)[theme].color) ? (TOKENS as any)[theme].color : {};
991
+ let idx = 0;
992
+ for (const [key, val] of Object.entries(col)) {
993
+ const row = figma.createFrame();
994
+ row.layoutMode = 'HORIZONTAL';
995
+ row.primaryAxisSizingMode = 'AUTO';
996
+ row.counterAxisSizingMode = 'AUTO';
997
+ row.itemSpacing = 12;
998
+ row.name = key;
999
+
1000
+ const swatch = figma.createRectangle();
1001
+ swatch.resize(48, 32);
1002
+ const rgb = parseColor(val);
1003
+ swatch.fills = [{ type: 'SOLID', color: { r: rgb.r, g: rgb.g, b: rgb.b }, opacity: (rgb.a == null ? 1 : rgb.a) }];
1004
+ // Bind to Figma Variable so swatch updates when variable changes
1005
+ bindColorVariable(swatch, key, 'fill', theme);
1006
+ swatch.strokes = [];
1007
+
1008
+ const label = figma.createText();
1009
+ label.characters = key + ' \u2014 ' + colorToLabel(val);
1010
+ // Load default font for text placement
1011
+ // Note: plugin will await font load asynchronously below
1012
+ row.appendChild(swatch);
1013
+ row.appendChild(label);
1014
+ frame.appendChild(row);
1015
+ idx++;
1016
+ }
1017
+ return frame;
1018
+ }
1019
+
1020
+ export function demoFrameRadii(): any {
1021
+ const frame = figma.createFrame();
1022
+ frame.name = 'Radii';
1023
+ frame.layoutMode = 'HORIZONTAL';
1024
+ frame.primaryAxisSizingMode = 'AUTO';
1025
+ frame.counterAxisSizingMode = 'AUTO';
1026
+ frame.itemSpacing = 16;
1027
+ frame.paddingLeft = frame.paddingRight = frame.paddingTop = frame.paddingBottom = 16;
1028
+ frame.fills = [];
1029
+ frame.strokes = [{ type: 'SOLID', color: { r: 0.9, g: 0.9, b: 0.9 } }];
1030
+ const r: Record<string, any> = getThemeGroup(_defaultThemeName, 'radius');
1031
+ const keys = Object.keys(r);
1032
+ for (const k of keys) {
1033
+ const rect = figma.createRectangle();
1034
+ rect.resize(96, 64);
1035
+ rect.cornerRadius = pxFromSizeToken(r[k]);
1036
+ rect.fills = [{ type: 'SOLID', color: { r: 0.9, g: 0.9, b: 0.9 } }];
1037
+ rect.strokes = [{ type: 'SOLID', color: { r: 0.75, g: 0.75, b: 0.75 } }];
1038
+ rect.name = 'radius/' + k;
1039
+ frame.appendChild(rect);
1040
+ }
1041
+ return frame;
1042
+ }