inkbridge 0.1.0-beta.2 → 0.1.0-beta.21

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 (178) hide show
  1. package/README.md +108 -25
  2. package/bin/inkbridge.mjs +354 -83
  3. package/code.js +40 -11802
  4. package/manifest.json +1 -0
  5. package/package.json +74 -23
  6. package/scanner/adapter-utils-regression.ts +159 -0
  7. package/scanner/aspect-percent-position-regression.ts +237 -0
  8. package/scanner/aspect-ratio-regression.ts +90 -0
  9. package/scanner/blob-placement-regression.ts +2 -2
  10. package/scanner/block-cache-regression.ts +195 -0
  11. package/scanner/bundle-size-regression.ts +50 -0
  12. package/scanner/child-sizing-matrix-regression.ts +303 -0
  13. package/scanner/cli.ts +342 -13
  14. package/scanner/component-scanner.ts +2108 -174
  15. package/scanner/component-sections-regression.ts +198 -0
  16. package/scanner/compound-classes-lookup-regression.ts +163 -0
  17. package/scanner/css-token-reader-regression.ts +7 -6
  18. package/scanner/css-token-reader.ts +152 -31
  19. package/scanner/cva-jsx-child-fallback-regression.ts +98 -0
  20. package/scanner/cva-master-icon-regression.ts +315 -0
  21. package/scanner/data-attr-prop-alias-regression.ts +129 -0
  22. package/scanner/explicit-size-root-regression.ts +102 -0
  23. package/scanner/font-family-extract-regression.ts +113 -0
  24. package/scanner/font-style-resolver-regression.ts +1 -1
  25. package/scanner/framework-adapter-shadcn-regression.ts +480 -0
  26. package/scanner/full-width-matrix-regression.ts +338 -0
  27. package/scanner/grid-cols-extraction-regression.ts +110 -0
  28. package/scanner/image-src-collector-regression.ts +204 -0
  29. package/scanner/inline-flex-regression.ts +235 -0
  30. package/scanner/input-range-regression.ts +217 -0
  31. package/scanner/instance-rendering-regression.ts +224 -0
  32. package/scanner/jsx-prop-unresolved-regression.ts +178 -0
  33. package/scanner/jsx-text-regression.ts +178 -0
  34. package/scanner/layout-alignment-regression.ts +108 -0
  35. package/scanner/layout-flex-regression.ts +90 -0
  36. package/scanner/layout-mode-regression.ts +71 -0
  37. package/scanner/layout-sizing-regression.ts +227 -0
  38. package/scanner/layout-spacing-regression.ts +135 -0
  39. package/scanner/local-const-className-regression.ts +331 -0
  40. package/scanner/percent-position-regression.ts +105 -0
  41. package/scanner/provider-cascade-regression.ts +224 -0
  42. package/scanner/provider-flatten-regression.ts +235 -0
  43. package/scanner/radial-gradient-regression.ts +1 -1
  44. package/scanner/render-prop-parser-regression.ts +161 -0
  45. package/scanner/ring-utility-regression.ts +153 -0
  46. package/scanner/sandbox-spread-regression.ts +125 -0
  47. package/scanner/selection-pressed-regression.ts +241 -0
  48. package/scanner/size-full-normalization-regression.ts +127 -0
  49. package/scanner/state-classification-regression.ts +175 -0
  50. package/scanner/story-diagnostics-regression.ts +216 -0
  51. package/scanner/story-dimensioning-regression.ts +298 -0
  52. package/scanner/story-render-strategy-regression.ts +205 -0
  53. package/scanner/stretch-to-parent-width-regression.ts +147 -0
  54. package/scanner/svg-fill-parent-regression.ts +98 -0
  55. package/scanner/svg-group-inheritance-regression.ts +166 -0
  56. package/scanner/svg-marker-inline-regression.ts +211 -0
  57. package/scanner/svg-marker-regression.ts +116 -0
  58. package/scanner/tailwind-parser.ts +46 -4
  59. package/scanner/text-resize-matrix-regression.ts +173 -0
  60. package/scanner/transform-math-regression.ts +1 -1
  61. package/scanner/types.ts +26 -2
  62. package/src/cache/frame-cache.ts +150 -0
  63. package/src/cache/index.ts +2 -0
  64. package/src/{component-defs.ts → components/component-defs.ts} +25 -10
  65. package/src/{component-gen.ts → components/component-gen.ts} +43 -116
  66. package/src/components/component-instance.ts +386 -0
  67. package/src/components/component-library.ts +44 -0
  68. package/src/components/component-lookup.ts +161 -0
  69. package/src/components/index.ts +7 -0
  70. package/src/components/scanner-types.ts +39 -0
  71. package/src/components/symbol-instance-policy.ts +312 -0
  72. package/src/design-system/block-cache.ts +130 -0
  73. package/src/design-system/component-sections.ts +107 -0
  74. package/src/design-system/cva-inference.ts +187 -0
  75. package/src/design-system/cva-master.ts +427 -0
  76. package/src/design-system/cva-utils.ts +29 -0
  77. package/src/design-system/design-system.ts +334 -0
  78. package/src/design-system/frame-stabilizers.ts +191 -0
  79. package/src/design-system/frame-utils.ts +46 -0
  80. package/src/design-system/generated-node.ts +84 -0
  81. package/src/design-system/icon-rendering.ts +229 -0
  82. package/src/design-system/index.ts +13 -0
  83. package/src/design-system/instance-rendering.ts +307 -0
  84. package/src/design-system/master-shared.ts +133 -0
  85. package/src/design-system/node-helpers.ts +237 -0
  86. package/src/design-system/node-variants.ts +196 -0
  87. package/src/design-system/non-cva-master.ts +104 -0
  88. package/src/design-system/portal-handling.ts +138 -0
  89. package/src/design-system/preview-builder.ts +738 -0
  90. package/src/{render-context.ts → design-system/render-context.ts} +32 -6
  91. package/src/design-system/render-prop-parser.ts +50 -0
  92. package/src/design-system/responsive-resolver.ts +180 -0
  93. package/src/design-system/selectable-state.ts +157 -0
  94. package/src/design-system/state-master.ts +267 -0
  95. package/src/design-system/state-utils.ts +15 -0
  96. package/src/design-system/story-builder-context.ts +40 -0
  97. package/src/design-system/story-builder.ts +1322 -0
  98. package/src/design-system/story-diagnostics.ts +80 -0
  99. package/src/design-system/story-dimensioning.ts +272 -0
  100. package/src/design-system/story-frames.ts +400 -0
  101. package/src/design-system/story-instance.ts +333 -0
  102. package/src/{story-layout.ts → design-system/story-layout.ts} +2 -2
  103. package/src/design-system/story-render-strategy.ts +150 -0
  104. package/src/design-system/story-tree-search.ts +110 -0
  105. package/src/design-system/symbol-fallback.ts +89 -0
  106. package/src/design-system/symbol-source.ts +172 -0
  107. package/src/design-system/table-helpers.ts +56 -0
  108. package/src/design-system/tag-predicates.ts +99 -0
  109. package/src/design-system/theme-context.ts +52 -0
  110. package/src/design-system/typography.ts +100 -0
  111. package/src/design-system/ui-builder.ts +2676 -0
  112. package/src/{clip-path-decorative.ts → effects/clip-path-decorative.ts} +11 -11
  113. package/src/effects/icon-builder.ts +1074 -0
  114. package/src/effects/index.ts +5 -0
  115. package/src/effects/portal-panel.ts +369 -0
  116. package/src/{radial-gradient.ts → effects/radial-gradient.ts} +1 -1
  117. package/src/framework-adapters/index.ts +47 -0
  118. package/src/framework-adapters/shadcn.ts +541 -0
  119. package/src/{github.ts → github/github.ts} +46 -21
  120. package/src/github/index.ts +1 -0
  121. package/src/layout/deferred-layout.ts +1556 -0
  122. package/src/layout/index.ts +24 -0
  123. package/src/layout/layout-parser.ts +375 -0
  124. package/src/{layout-utils.ts → layout/layout-utils.ts} +23 -17
  125. package/src/layout/parser/alignment.ts +54 -0
  126. package/src/layout/parser/flex.ts +59 -0
  127. package/src/layout/parser/index.ts +65 -0
  128. package/src/layout/parser/ir.ts +80 -0
  129. package/src/layout/parser/layout-mode.ts +57 -0
  130. package/src/layout/parser/sizing.ts +241 -0
  131. package/src/layout/parser/spacing-scale.ts +78 -0
  132. package/src/layout/parser/spacing.ts +134 -0
  133. package/src/layout/ring-utils.ts +120 -0
  134. package/src/layout/size-utils.ts +143 -0
  135. package/src/layout/text-resize-decision.ts +51 -0
  136. package/src/{width-solver.ts → layout/width-solver.ts} +168 -37
  137. package/src/main.ts +444 -162
  138. package/src/{config.ts → plugin/config.ts} +12 -12
  139. package/src/{dev-server.ts → plugin/dev-server.ts} +3 -3
  140. package/src/plugin/image-src-collector.ts +52 -0
  141. package/src/plugin/index.ts +3 -0
  142. package/src/plugin/packs/index.ts +2 -0
  143. package/src/{pack-provider.ts → plugin/packs/pack-provider.ts} +12 -12
  144. package/src/{packs.ts → plugin/packs/packs.ts} +22 -17
  145. package/src/render-engine-version.ts +2 -0
  146. package/src/tailwind/adapter-utils.ts +137 -0
  147. package/src/{class-utils.ts → tailwind/class-utils.ts} +33 -6
  148. package/src/tailwind/index.ts +8 -0
  149. package/src/tailwind/jsx-utils.ts +319 -0
  150. package/src/{node-ir.ts → tailwind/node-ir.ts} +208 -19
  151. package/src/{responsive-analyzer.ts → tailwind/responsive-analyzer.ts} +32 -2
  152. package/src/{state-analyzer.ts → tailwind/state-analyzer.ts} +71 -5
  153. package/src/{tailwind.ts → tailwind/tailwind.ts} +423 -674
  154. package/src/{utility-resolver.ts → tailwind/utility-resolver.ts} +27 -6
  155. package/src/{font-style-resolver.ts → text/font-style-resolver.ts} +0 -2
  156. package/src/text/index.ts +4 -0
  157. package/src/{inline-text.ts → text/inline-text.ts} +13 -13
  158. package/src/{text-builder.ts → text/text-builder.ts} +24 -7
  159. package/src/{text-line.ts → text/text-line.ts} +2 -2
  160. package/src/{change-detection.ts → tokens/change-detection.ts} +12 -12
  161. package/src/{color-resolver.ts → tokens/color-resolver.ts} +1 -6
  162. package/src/{colors.ts → tokens/colors.ts} +13 -6
  163. package/src/tokens/index.ts +6 -0
  164. package/src/{token-source.ts → tokens/token-source.ts} +4 -1
  165. package/src/{tokens.ts → tokens/tokens.ts} +116 -20
  166. package/src/{variables.ts → tokens/variables.ts} +447 -102
  167. package/templates/patch-tokens-route.ts +25 -6
  168. package/templates/scan-components-route.ts +26 -5
  169. package/ui.html +485 -37
  170. package/src/component-lookup.ts +0 -82
  171. package/src/design-system.ts +0 -59
  172. package/src/icon-builder.ts +0 -607
  173. package/src/layout-parser.ts +0 -667
  174. package/src/story-builder.ts +0 -1706
  175. package/src/ui-builder.ts +0 -1996
  176. /package/src/{image-cache.ts → cache/image-cache.ts} +0 -0
  177. /package/src/{blob-placement.ts → effects/blob-placement.ts} +0 -0
  178. /package/src/{transform-math.ts → tailwind/transform-math.ts} +0 -0
@@ -5,16 +5,17 @@ import { parseColor, colorToLabel, debug, normalizeThemeName, normalizeGroupName
5
5
  // Module-level state
6
6
  // ---------------------------------------------------------------------------
7
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
8
+ let _variableCollection: VariableCollection | null = null; // Single-collection (paid) or default-theme collection (free)
9
+ let _themeCollections: Record<string, VariableCollection> = {}; // Free-plan: one collection per theme
10
10
  let _variableModeIds: Record<string, string> = {}; // Multi-mode: theme -> modeId
11
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 …)
12
+ let _colorVariables: Record<string, Variable> = {}; // default theme tokenKey -> Variable
13
+ let _themeColorVariables: Record<string, Record<string, Variable>> = {}; // theme -> tokenKey -> Variable
14
+ let _radiusVariables: Record<string, Variable> = {}; // key -> FLOAT Variable (radius/sm, radius/md …)
15
+ let _fontVariables: Record<string, Variable> = {}; // key -> STRING Variable (font/sans …)
16
+ let _spacingVariables: Record<string, Variable> = {}; // key -> FLOAT Variable (spacing/sm, spacing/lg …)
17
+ let _fontSizeVariables: Record<string, Variable> = {}; // key -> FLOAT Variable (fontSize/sm, fontSize/xl …)
18
+ let _breakpointVariables: Record<string, Variable> = {}; // key -> FLOAT Variable (breakpoint/sm, breakpoint/lg …)
18
19
  let _useMultiMode = false; // true if paid plan (multi-mode available)
19
20
 
20
21
  // ---------------------------------------------------------------------------
@@ -46,12 +47,13 @@ function getThemeDisplayName(theme: string): string {
46
47
  }
47
48
 
48
49
  function getThemeNamesFromTokens(): string[] {
49
- const names = Object.keys(TOKENS || {}).filter((key) => {
50
+ const tokensRecord = TOKENS as unknown as Record<string, Record<string, unknown> | undefined>;
51
+ const names = Object.keys(tokensRecord || {}).filter((key) => {
50
52
  if (key === 'core') return false;
51
- const block = (TOKENS as any)[key];
53
+ const block = tokensRecord[key];
52
54
  if (!block || typeof block !== 'object') return false;
53
55
  return Boolean(
54
- block.color || block.radius || block.font || block.spacing || block.fontSize || block.shadow
56
+ block.color || block.radius || block.font || block.spacing || block.fontSize || block.shadow || block.breakpoint
55
57
  );
56
58
  });
57
59
  if (names.length === 0) return ['primary'];
@@ -59,22 +61,23 @@ function getThemeNamesFromTokens(): string[] {
59
61
  return ['primary'].concat(names.filter((name) => name !== 'primary'));
60
62
  }
61
63
 
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]
64
+ function getThemeGroup(theme: string, group: string): Record<string, string> {
65
+ const tokensRecord = TOKENS as unknown as Record<string, Record<string, unknown> | undefined>;
66
+ const block = (tokensRecord[theme] && typeof tokensRecord[theme] === 'object')
67
+ ? tokensRecord[theme]!
65
68
  : {};
66
69
  const value = block[group];
67
- return value && typeof value === 'object' ? value : {};
70
+ return value && typeof value === 'object' ? (value as Record<string, string>) : {};
68
71
  }
69
72
 
70
- function findCollectionByName(collections: any[], name: string): any {
73
+ function findCollectionByName(collections: VariableCollection[], name: string): VariableCollection | null {
71
74
  const needle = String(name || '').toLowerCase();
72
- return collections.find((c: any) => String(c.name || '').toLowerCase() === needle) || null;
75
+ return collections.find((c) => String(c.name || '').toLowerCase() === needle) || null;
73
76
  }
74
77
 
75
- export function resolveVariableValue(value: any, modeId?: string, visited?: Record<string, boolean>): any {
78
+ export function resolveVariableValue(value: VariableValue | null | undefined, modeId?: string, visited?: Record<string, boolean>): VariableValue | null {
76
79
  if (!value) return null;
77
- if (value.type === 'VARIABLE_ALIAS' && value.id) {
80
+ if (typeof value === 'object' && 'type' in value && value.type === 'VARIABLE_ALIAS' && value.id) {
78
81
  if (!visited) visited = {};
79
82
  if (visited[value.id]) return null;
80
83
  visited[value.id] = true;
@@ -88,7 +91,16 @@ export function resolveVariableValue(value: any, modeId?: string, visited?: Reco
88
91
  return value;
89
92
  }
90
93
 
91
- export function readVariableTokens(): any {
94
+ type VariableTokenBlock = {
95
+ color?: Record<string, string>;
96
+ radius?: Record<string, string>;
97
+ font?: Record<string, string>;
98
+ spacing?: Record<string, string>;
99
+ fontSize?: Record<string, string>;
100
+ };
101
+ type VariableTokens = Record<string, VariableTokenBlock>;
102
+
103
+ export function readVariableTokens(): VariableTokens | null {
92
104
  if (!figma.variables || !figma.variables.getLocalVariables) {
93
105
  debug('Variables API unavailable, using embedded tokens.');
94
106
  return null;
@@ -99,7 +111,7 @@ export function readVariableTokens(): any {
99
111
  return null;
100
112
  }
101
113
  const collections = figma.variables.getLocalVariableCollections ? figma.variables.getLocalVariableCollections() : [];
102
- const collectionById: Record<string, any> = {};
114
+ const collectionById: Record<string, VariableCollection> = {};
103
115
  const modeNameById: Record<string, string> = {};
104
116
  for (const col of collections) {
105
117
  collectionById[col.id] = col;
@@ -112,36 +124,36 @@ export function readVariableTokens(): any {
112
124
 
113
125
  // Full token structure read back from Figma Variables.
114
126
  // Keep this dynamic: theme keys can be arbitrary (not just primary/secondary).
115
- const out: Record<string, any> = {
127
+ const out: VariableTokens = {
116
128
  core: { font: {} },
117
129
  primary: { color: {}, radius: {}, font: {}, spacing: {}, fontSize: {} },
118
130
  };
119
131
  let wrote = false;
120
132
 
121
- function ensureTheme(themeName: string): Record<string, any> {
133
+ function ensureTheme(themeName: string): Required<VariableTokenBlock> {
122
134
  if (!out[themeName] || typeof out[themeName] !== 'object') {
123
135
  out[themeName] = {};
124
136
  }
125
- const block = out[themeName] as Record<string, any>;
137
+ const block = out[themeName];
126
138
  if (!block.color || typeof block.color !== 'object') block.color = {};
127
139
  if (!block.radius || typeof block.radius !== 'object') block.radius = {};
128
140
  if (!block.font || typeof block.font !== 'object') block.font = {};
129
141
  if (!block.spacing || typeof block.spacing !== 'object') block.spacing = {};
130
142
  if (!block.fontSize || typeof block.fontSize !== 'object') block.fontSize = {};
131
- return block;
143
+ return block as Required<VariableTokenBlock>;
132
144
  }
133
145
 
134
- function assign(theme: string, group: string, parts: string[], value: any): void {
146
+ function assign(theme: string, group: string, parts: string[], value: VariableValue | null): void {
135
147
  if (!value) return;
136
148
  const key = parts && parts.length ? parts.join('-') : null;
137
149
  if (!key) return;
138
150
 
139
151
  const resolvedTheme = theme && theme !== 'core' ? theme : 'primary';
140
- let target: Record<string, any>;
152
+ let target: Record<string, string>;
141
153
 
142
154
  if (group === 'color') {
143
155
  target = ensureTheme(resolvedTheme).color;
144
- target[key] = value;
156
+ target[key] = String(value);
145
157
  wrote = true;
146
158
  return;
147
159
  }
@@ -153,7 +165,7 @@ export function readVariableTokens(): any {
153
165
  }
154
166
  if (group === 'font') {
155
167
  // Route unscoped fonts into core, scoped fonts into their theme block.
156
- if (!theme || theme === 'core') target = out.core.font;
168
+ if (!theme || theme === 'core') target = out.core.font!;
157
169
  else target = ensureTheme(theme).font;
158
170
  target[key] = String(value);
159
171
  wrote = true;
@@ -187,7 +199,7 @@ export function readVariableTokens(): any {
187
199
  if (!group) {
188
200
  // Backward compatibility: older variable sets used flat color names like
189
201
  // `primary`, `foreground` (without `color/` prefix). Infer group by type.
190
- if (String((variable as any).resolvedType || '').toUpperCase() === 'COLOR') {
202
+ if (String(variable.resolvedType || '').toUpperCase() === 'COLOR') {
191
203
  group = 'color';
192
204
  if (cursor.length === 0) {
193
205
  theme = null;
@@ -232,9 +244,9 @@ export function readVariableTokens(): any {
232
244
  return wrote ? out : null;
233
245
  }
234
246
 
235
- export function upsertPaintStyle(name: string, color: { r: number; g: number; b: number; a?: number }): any {
247
+ export function upsertPaintStyle(name: string, color: { r: number; g: number; b: number; a?: number }): PaintStyle {
236
248
  const styles = figma.getLocalPaintStyles();
237
- const existing = styles.find((s: any) => s.name === name);
249
+ const existing = styles.find((s) => s.name === name);
238
250
  const paint = { type: 'SOLID' as const, color: { r: color.r, g: color.g, b: color.b }, opacity: (color.a == null ? 1 : color.a) };
239
251
  if (existing) {
240
252
  existing.paints = [paint];
@@ -354,7 +366,7 @@ function upsertEffectStyle(name: string, cssValue: string): void {
354
366
  const layers = parseCssBoxShadow(cssValue);
355
367
  if (!layers.length) return;
356
368
 
357
- const effects: any[] = [];
369
+ const effects: Effect[] = [];
358
370
  for (const l of layers) {
359
371
  effects.push({
360
372
  type: l.inset ? 'INNER_SHADOW' : 'DROP_SHADOW',
@@ -367,7 +379,7 @@ function upsertEffectStyle(name: string, cssValue: string): void {
367
379
  });
368
380
  }
369
381
 
370
- const existing = figma.getLocalEffectStyles().find((s: any) => s.name === name);
382
+ const existing = figma.getLocalEffectStyles().find((s) => s.name === name);
371
383
  if (existing) {
372
384
  existing.effects = effects;
373
385
  } else {
@@ -403,10 +415,10 @@ export function removeStaleColorStyles(): void {
403
415
  /**
404
416
  * Populate variables in a collection for a given theme's color tokens.
405
417
  */
406
- export function populateColorVariables(collection: any, modeId: string, colorTokens: Record<string, any>, outMap: Record<string, any>): void {
418
+ export function populateColorVariables(collection: VariableCollection, modeId: string, colorTokens: Record<string, string>, outMap: Record<string, Variable>): void {
407
419
  // Index existing variables in this collection
408
420
  const existingVars = figma.variables.getLocalVariables('COLOR');
409
- const existingByName: Record<string, any> = {};
421
+ const existingByName: Record<string, Variable> = {};
410
422
  for (const v of existingVars) {
411
423
  if (v.variableCollectionId === collection.id) {
412
424
  existingByName[v.name] = v;
@@ -428,9 +440,9 @@ export function populateColorVariables(collection: any, modeId: string, colorTok
428
440
  /**
429
441
  * Populate FLOAT radius variables in a collection for a given mode.
430
442
  */
431
- export function populateRadiusVariables(collection: any, modeId: string, radiusTokens: Record<string, string>, outMap: Record<string, any>): void {
443
+ export function populateRadiusVariables(collection: VariableCollection, modeId: string, radiusTokens: Record<string, string>, outMap: Record<string, Variable>): void {
432
444
  const existingVars = figma.variables.getLocalVariables('FLOAT');
433
- const existingByName: Record<string, any> = {};
445
+ const existingByName: Record<string, Variable> = {};
434
446
  for (const v of existingVars) {
435
447
  if (v.variableCollectionId === collection.id) {
436
448
  existingByName[v.name] = v;
@@ -450,9 +462,9 @@ export function populateRadiusVariables(collection: any, modeId: string, radiusT
450
462
  /**
451
463
  * Populate STRING font-family variables in a collection for a given mode.
452
464
  */
453
- export function populateFontVariables(collection: any, modeId: string, fontTokens: Record<string, string>, outMap: Record<string, any>): void {
465
+ export function populateFontVariables(collection: VariableCollection, modeId: string, fontTokens: Record<string, string>, outMap: Record<string, Variable>): void {
454
466
  const existingVars = figma.variables.getLocalVariables('STRING');
455
- const existingByName: Record<string, any> = {};
467
+ const existingByName: Record<string, Variable> = {};
456
468
  for (const v of existingVars) {
457
469
  if (v.variableCollectionId === collection.id) {
458
470
  existingByName[v.name] = v;
@@ -474,9 +486,9 @@ export function populateFontVariables(collection: any, modeId: string, fontToken
474
486
  /**
475
487
  * Populate FLOAT spacing variables in a collection for a given mode.
476
488
  */
477
- export function populateSpacingVariables(collection: any, modeId: string, spacingTokens: Record<string, string>, outMap: Record<string, any>): void {
489
+ export function populateSpacingVariables(collection: VariableCollection, modeId: string, spacingTokens: Record<string, string>, outMap: Record<string, Variable>): void {
478
490
  const existingVars = figma.variables.getLocalVariables('FLOAT');
479
- const existingByName: Record<string, any> = {};
491
+ const existingByName: Record<string, Variable> = {};
480
492
  for (const v of existingVars) {
481
493
  if (v.variableCollectionId === collection.id) existingByName[v.name] = v;
482
494
  }
@@ -494,9 +506,9 @@ export function populateSpacingVariables(collection: any, modeId: string, spacin
494
506
  /**
495
507
  * Populate FLOAT font-size variables in a collection for a given mode.
496
508
  */
497
- export function populateFontSizeVariables(collection: any, modeId: string, fontSizeTokens: Record<string, string>, outMap: Record<string, any>): void {
509
+ export function populateFontSizeVariables(collection: VariableCollection, modeId: string, fontSizeTokens: Record<string, string>, outMap: Record<string, Variable>): void {
498
510
  const existingVars = figma.variables.getLocalVariables('FLOAT');
499
- const existingByName: Record<string, any> = {};
511
+ const existingByName: Record<string, Variable> = {};
500
512
  for (const v of existingVars) {
501
513
  if (v.variableCollectionId === collection.id) existingByName[v.name] = v;
502
514
  }
@@ -511,6 +523,26 @@ export function populateFontSizeVariables(collection: any, modeId: string, fontS
511
523
  }
512
524
  }
513
525
 
526
+ /**
527
+ * Populate FLOAT breakpoint variables in a collection for a given mode.
528
+ */
529
+ export function populateBreakpointVariables(collection: VariableCollection, modeId: string, breakpointTokens: Record<string, string>, outMap: Record<string, Variable>): void {
530
+ const existingVars = figma.variables.getLocalVariables('FLOAT');
531
+ const existingByName: Record<string, Variable> = {};
532
+ for (const v of existingVars) {
533
+ if (v.variableCollectionId === collection.id) existingByName[v.name] = v;
534
+ }
535
+ for (const key in breakpointTokens) {
536
+ const varName = 'breakpoint/' + key;
537
+ let variable = existingByName[varName];
538
+ if (!variable) {
539
+ variable = figma.variables.createVariable(varName, collection, 'FLOAT');
540
+ }
541
+ variable.setValueForMode(modeId, pxFromSizeToken(breakpointTokens[key]));
542
+ outMap[key] = variable;
543
+ }
544
+ }
545
+
514
546
  /**
515
547
  * Create or update color variables.
516
548
  * Tries multi-mode first (paid plans). Falls back to two collections (free plan).
@@ -527,17 +559,19 @@ export function createOrUpdateVariables(): void {
527
559
  const defaultTheme = _defaultThemeName;
528
560
  const defaultDisplayName = getThemeDisplayName(defaultTheme);
529
561
 
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>> = {};
562
+ const themeColors: Record<string, Record<string, string>> = {};
563
+ const themeRadius: Record<string, Record<string, string>> = {};
564
+ const themeFont: Record<string, Record<string, string>> = {};
565
+ const themeSpacing: Record<string, Record<string, string>> = {};
566
+ const themeFontSize: Record<string, Record<string, string>> = {};
567
+ const themeBreakpoint: Record<string, Record<string, string>> = {};
535
568
  for (const theme of themeNames) {
536
569
  themeColors[theme] = getThemeGroup(theme, 'color');
537
570
  themeRadius[theme] = getThemeGroup(theme, 'radius');
538
571
  themeFont[theme] = getThemeGroup(theme, 'font');
539
572
  themeSpacing[theme] = getThemeGroup(theme, 'spacing');
540
573
  themeFontSize[theme] = getThemeGroup(theme, 'fontSize');
574
+ themeBreakpoint[theme] = getThemeGroup(theme, 'breakpoint');
541
575
  }
542
576
 
543
577
  _themeCollections = {};
@@ -548,13 +582,14 @@ export function createOrUpdateVariables(): void {
548
582
  _fontVariables = {};
549
583
  _spacingVariables = {};
550
584
  _fontSizeVariables = {};
585
+ _breakpointVariables = {};
551
586
 
552
587
  const collections = figma.variables.getLocalVariableCollections();
553
588
 
554
- function ensureThemeModes(collection: any): boolean {
589
+ function ensureThemeModes(collection: VariableCollection): boolean {
555
590
  const map: Record<string, string> = {};
556
591
  const modes = Array.isArray(collection.modes) ? collection.modes.slice() : [];
557
- const unclaimed: string[] = modes.map((mode: any) => mode.modeId);
592
+ const unclaimed: string[] = modes.map((mode) => mode.modeId);
558
593
  const removeUnclaimed = (modeId: string): void => {
559
594
  const idx = unclaimed.indexOf(modeId);
560
595
  if (idx >= 0) unclaimed.splice(idx, 1);
@@ -580,8 +615,8 @@ export function createOrUpdateVariables(): void {
580
615
  }
581
616
  try {
582
617
  map[theme] = collection.addMode(getThemeDisplayName(theme));
583
- } catch (e: any) {
584
- debug('Cannot add mode (free plan): ' + (e.message || e));
618
+ } catch (e) {
619
+ debug('Cannot add mode (free plan): ' + (e instanceof Error ? e.message : String(e)));
585
620
  return false;
586
621
  }
587
622
  }
@@ -625,16 +660,18 @@ export function createOrUpdateVariables(): void {
625
660
  const allFontKeys: Record<string, boolean> = {};
626
661
  const allSpacingKeys: Record<string, boolean> = {};
627
662
  const allFontSizeKeys: Record<string, boolean> = {};
663
+ const allBreakpointKeys: Record<string, boolean> = {};
628
664
  for (const theme of themeNames) {
629
665
  for (const key in themeColors[theme]) allColorKeys[key] = true;
630
666
  for (const key in themeRadius[theme]) allRadiusKeys[key] = true;
631
667
  for (const key in themeFont[theme]) allFontKeys[key] = true;
632
668
  for (const key in themeSpacing[theme]) allSpacingKeys[key] = true;
633
669
  for (const key in themeFontSize[theme]) allFontSizeKeys[key] = true;
670
+ for (const key in themeBreakpoint[theme]) allBreakpointKeys[key] = true;
634
671
  }
635
672
 
636
673
  const existingColorVars = figma.variables.getLocalVariables('COLOR');
637
- const existingColorByName: Record<string, any> = {};
674
+ const existingColorByName: Record<string, Variable> = {};
638
675
  for (const v of existingColorVars) {
639
676
  if (v.variableCollectionId === _variableCollection.id) {
640
677
  existingColorByName[v.name] = v;
@@ -642,7 +679,7 @@ export function createOrUpdateVariables(): void {
642
679
  }
643
680
 
644
681
  const existingFloatVars = figma.variables.getLocalVariables('FLOAT');
645
- const existingFloatByName: Record<string, any> = {};
682
+ const existingFloatByName: Record<string, Variable> = {};
646
683
  for (const v of existingFloatVars) {
647
684
  if (v.variableCollectionId === _variableCollection.id) {
648
685
  existingFloatByName[v.name] = v;
@@ -650,7 +687,7 @@ export function createOrUpdateVariables(): void {
650
687
  }
651
688
 
652
689
  const existingStringVars = figma.variables.getLocalVariables('STRING');
653
- const existingStringByName: Record<string, any> = {};
690
+ const existingStringByName: Record<string, Variable> = {};
654
691
  for (const v of existingStringVars) {
655
692
  if (v.variableCollectionId === _variableCollection.id) {
656
693
  existingStringByName[v.name] = v;
@@ -752,6 +789,24 @@ export function createOrUpdateVariables(): void {
752
789
  _fontSizeVariables[key] = variable;
753
790
  }
754
791
 
792
+ const defaultBreakpoint = themeBreakpoint[defaultTheme] || {};
793
+ for (const key in allBreakpointKeys) {
794
+ const varName = 'breakpoint/' + key;
795
+ let variable = existingFloatByName[varName];
796
+ if (!variable) variable = figma.variables.createVariable(varName, _variableCollection, 'FLOAT');
797
+
798
+ for (const theme of themeNames) {
799
+ const modeId = _variableModeIds[theme];
800
+ if (!modeId) continue;
801
+ const value = (themeBreakpoint[theme] && themeBreakpoint[theme][key] !== undefined)
802
+ ? themeBreakpoint[theme][key]
803
+ : defaultBreakpoint[key];
804
+ if (value === undefined) continue;
805
+ variable.setValueForMode(modeId, pxFromSizeToken(value));
806
+ }
807
+ _breakpointVariables[key] = variable;
808
+ }
809
+
755
810
  for (const col of collections) {
756
811
  if (!col || col.id === _variableCollection.id) continue;
757
812
  const mapped = normalizeThemeName(col.name) || normalizeCustomThemeName(col.name);
@@ -798,6 +853,7 @@ export function createOrUpdateVariables(): void {
798
853
  populateFontVariables(collection, modeId, themeFont[theme] || {}, _fontVariables);
799
854
  populateSpacingVariables(collection, modeId, themeSpacing[theme] || {}, _spacingVariables);
800
855
  populateFontSizeVariables(collection, modeId, themeFontSize[theme] || {}, _fontSizeVariables);
856
+ populateBreakpointVariables(collection, modeId, themeBreakpoint[theme] || {}, _breakpointVariables);
801
857
  }
802
858
  }
803
859
 
@@ -808,13 +864,14 @@ export function createOrUpdateVariables(): void {
808
864
  if (!mapped) continue;
809
865
  if (themeNames.includes(mapped)) continue;
810
866
  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) =>
867
+ const varsInCollection = figma.variables.getLocalVariables().filter((v) => v.variableCollectionId === collection.id);
868
+ const looksLikePluginThemeCollection = varsInCollection.some((v) =>
813
869
  /^color\//.test(String(v.name || '')) ||
814
870
  /^radius\//.test(String(v.name || '')) ||
815
871
  /^font\//.test(String(v.name || '')) ||
816
872
  /^spacing\//.test(String(v.name || '')) ||
817
- /^fontSize\//.test(String(v.name || ''))
873
+ /^fontSize\//.test(String(v.name || '')) ||
874
+ /^breakpoint\//.test(String(v.name || ''))
818
875
  );
819
876
  if (!looksLikePluginThemeCollection) continue;
820
877
  try { collection.remove(); } catch (_e) {}
@@ -834,7 +891,7 @@ export function createOrUpdateVariables(): void {
834
891
  export function createOrUpdateStylesFallback(): void {
835
892
  const themes = getThemeNamesFromTokens();
836
893
  for (const theme of themes) {
837
- const col: Record<string, any> = getThemeGroup(theme, 'color');
894
+ const col: Record<string, string> = getThemeGroup(theme, 'color');
838
895
  for (const [key, val] of Object.entries(col)) {
839
896
  const rgb = parseColor(val);
840
897
  upsertPaintStyle(`${theme.toUpperCase()}/Color/${key}`, rgb);
@@ -845,7 +902,7 @@ export function createOrUpdateStylesFallback(): void {
845
902
  /**
846
903
  * Bind a frame's fill to a color variable (semantic token).
847
904
  */
848
- export function bindColorVariable(node: any, tokenKey: string, fillOrStroke: string, theme?: string): boolean {
905
+ export function bindColorVariable(node: SceneNode, tokenKey: string, fillOrStroke: string, theme?: string): boolean {
849
906
  const resolvedTheme =
850
907
  (theme && (_themeColorVariables[theme] || _variableModeIds[theme])) ? theme :
851
908
  _defaultThemeName;
@@ -857,24 +914,24 @@ export function bindColorVariable(node: any, tokenKey: string, fillOrStroke: str
857
914
  if (!variable) return false;
858
915
 
859
916
  try {
860
- if (fillOrStroke === 'fill') {
861
- const fills = JSON.parse(JSON.stringify(node.fills || []));
917
+ if (fillOrStroke === 'fill' && 'fills' in node) {
918
+ const fills: Paint[] = JSON.parse(JSON.stringify(node.fills || []));
862
919
  if (fills.length === 0) {
863
920
  fills.push({ type: 'SOLID', color: { r: 1, g: 1, b: 1 }, opacity: 1 });
864
921
  }
865
- fills[0] = figma.variables.setBoundVariableForPaint(fills[0], 'color', variable);
922
+ fills[0] = figma.variables.setBoundVariableForPaint(fills[0] as SolidPaint, 'color', variable);
866
923
  node.fills = fills;
867
- } else if (fillOrStroke === 'stroke') {
868
- const strokes = JSON.parse(JSON.stringify(node.strokes || []));
924
+ } else if (fillOrStroke === 'stroke' && 'strokes' in node) {
925
+ const strokes: Paint[] = JSON.parse(JSON.stringify(node.strokes || []));
869
926
  if (strokes.length === 0) {
870
927
  strokes.push({ type: 'SOLID', color: { r: 0.9, g: 0.9, b: 0.9 } });
871
928
  }
872
- strokes[0] = figma.variables.setBoundVariableForPaint(strokes[0], 'color', variable);
929
+ strokes[0] = figma.variables.setBoundVariableForPaint(strokes[0] as SolidPaint, 'color', variable);
873
930
  node.strokes = strokes;
874
931
  }
875
932
  return true;
876
- } catch (e: any) {
877
- debug('Failed to bind variable ' + tokenKey + ': ' + (e.message || e));
933
+ } catch (e) {
934
+ debug('Failed to bind variable ' + tokenKey + ': ' + (e instanceof Error ? e.message : String(e)));
878
935
  return false;
879
936
  }
880
937
  }
@@ -883,7 +940,7 @@ export function bindColorVariable(node: any, tokenKey: string, fillOrStroke: str
883
940
  * Set the theme mode on a frame (only works on paid plans with multi-mode).
884
941
  * On free plan this is a no-op (themes are separate collections instead).
885
942
  */
886
- export function setThemeMode(frame: any, theme: string): void {
943
+ export function setThemeMode(frame: SceneNode, theme: string): void {
887
944
  if (!_useMultiMode || !_variableCollection) return;
888
945
  const normalized = normalizeThemeName(theme) || normalizeCustomThemeName(theme);
889
946
  const resolvedTheme = (theme && _variableModeIds[theme]) ? theme :
@@ -892,8 +949,8 @@ export function setThemeMode(frame: any, theme: string): void {
892
949
  if (!modeId) return;
893
950
  try {
894
951
  frame.setExplicitVariableModeForCollection(_variableCollection, modeId);
895
- } catch (e: any) {
896
- debug('Failed to set theme mode: ' + (e.message || e));
952
+ } catch (e) {
953
+ debug('Failed to set theme mode: ' + (e instanceof Error ? e.message : String(e)));
897
954
  }
898
955
  }
899
956
 
@@ -940,17 +997,18 @@ export function pxFromSizeToken(v: unknown): number {
940
997
  * Bind a frame's cornerRadius corners to a radius variable.
941
998
  * radiusKey is a token key like 'md', 'lg', 'base', etc.
942
999
  */
943
- export function bindRadiusVariable(node: any, radiusKey: string, _theme?: string): boolean {
1000
+ export function bindRadiusVariable(node: SceneNode, radiusKey: string, _theme?: string): boolean {
944
1001
  const variable = _radiusVariables[radiusKey];
945
1002
  if (!variable) return false;
1003
+ if (!('setBoundVariable' in node)) return false;
946
1004
  try {
947
1005
  node.setBoundVariable('topLeftRadius', variable);
948
1006
  node.setBoundVariable('topRightRadius', variable);
949
1007
  node.setBoundVariable('bottomLeftRadius', variable);
950
1008
  node.setBoundVariable('bottomRightRadius', variable);
951
1009
  return true;
952
- } catch (e: any) {
953
- debug('Failed to bind radius variable ' + radiusKey + ': ' + (e.message || e));
1010
+ } catch (e) {
1011
+ debug('Failed to bind radius variable ' + radiusKey + ': ' + (e instanceof Error ? e.message : String(e)));
954
1012
  return false;
955
1013
  }
956
1014
  }
@@ -967,8 +1025,8 @@ export function createOrUpdateStyles(): void {
967
1025
  }
968
1026
  }
969
1027
 
970
- export function ensureTokensPage(): any {
971
- let page = figma.root.children.find((p: any) => p.name === 'Design Tokens');
1028
+ export function ensureTokensPage(): PageNode {
1029
+ let page = figma.root.children.find((p) => p.name === 'Design Tokens');
972
1030
  if (!page) {
973
1031
  page = figma.createPage();
974
1032
  page.name = 'Design Tokens';
@@ -977,9 +1035,96 @@ export function ensureTokensPage(): any {
977
1035
  return page;
978
1036
  }
979
1037
 
980
- export function demoFrameColors(theme: string): any {
1038
+ function makeSectionTitle(text: string): TextNode {
1039
+ const node = figma.createText();
1040
+ try {
1041
+ node.fontName = { family: 'Inter', style: 'Bold' };
1042
+ } catch (_e) {
1043
+ // Bold not loaded; fall back to whatever style createText assigns.
1044
+ }
1045
+ node.characters = text;
1046
+ node.fontSize = 14;
1047
+ return node;
1048
+ }
1049
+
1050
+ function makeCategoryHeading(text: string): TextNode {
1051
+ const node = figma.createText();
1052
+ try {
1053
+ node.fontName = { family: 'Inter', style: 'Bold' };
1054
+ } catch (_e) {
1055
+ // Bold not loaded; fall back to whatever style createText assigns.
1056
+ }
1057
+ node.characters = text;
1058
+ node.fontSize = 18;
1059
+ return node;
1060
+ }
1061
+
1062
+ // Equal-width column widths so the side-by-side theme frames don't drift in
1063
+ // width based on content (mirrors the per-story column treatment used in the
1064
+ // component grid).
1065
+ const TOKENS_THEME_COLUMN_WIDTH = 460;
1066
+ const TOKENS_THEME_COLUMN_GAP = 24;
1067
+
1068
+ export function buildTokensCategorySection(label: string, frames: Array<FrameNode | null>): FrameNode | null {
1069
+ const visible: FrameNode[] = frames.filter((f): f is FrameNode => !!f);
1070
+ if (visible.length === 0) return null;
1071
+
1072
+ const section = figma.createFrame();
1073
+ section.name = label;
1074
+ section.layoutMode = 'VERTICAL';
1075
+ section.primaryAxisSizingMode = 'AUTO';
1076
+ section.counterAxisSizingMode = 'AUTO';
1077
+ section.itemSpacing = 12;
1078
+ section.fills = [];
1079
+ section.strokes = [];
1080
+ section.appendChild(makeCategoryHeading(label));
1081
+
1082
+ if (visible.length === 1) {
1083
+ section.appendChild(visible[0]);
1084
+ return section;
1085
+ }
1086
+
1087
+ const row = figma.createFrame();
1088
+ row.name = label + ' / themes';
1089
+ row.layoutMode = 'HORIZONTAL';
1090
+ row.primaryAxisSizingMode = 'AUTO';
1091
+ row.counterAxisSizingMode = 'AUTO';
1092
+ row.itemSpacing = TOKENS_THEME_COLUMN_GAP;
1093
+ row.counterAxisAlignItems = 'MIN';
1094
+ row.fills = [];
1095
+ row.strokes = [];
1096
+
1097
+ for (const frame of visible) {
1098
+ row.appendChild(frame);
1099
+ // Lock each theme demo's width so Primary and Secondary columns stay
1100
+ // identical regardless of label or swatch content. counterAxisSizingMode
1101
+ // is the horizontal sizing for a vertical autolayout frame.
1102
+ if ('counterAxisSizingMode' in frame) {
1103
+ frame.counterAxisSizingMode = 'FIXED';
1104
+ }
1105
+ frame.resize(TOKENS_THEME_COLUMN_WIDTH, Math.max(frame.height || 1, 1));
1106
+ }
1107
+ section.appendChild(row);
1108
+ return section;
1109
+ }
1110
+
1111
+ function makeBodyText(text: string, options?: { family?: string; style?: string; size?: number }): TextNode {
1112
+ const node = figma.createText();
1113
+ const family = (options && options.family) || 'Inter';
1114
+ const style = (options && options.style) || 'Regular';
1115
+ try {
1116
+ node.fontName = { family, style };
1117
+ } catch (_e) {
1118
+ // Requested font not loaded; fall back to default font.
1119
+ }
1120
+ node.characters = text;
1121
+ if (options && options.size) node.fontSize = options.size;
1122
+ return node;
1123
+ }
1124
+
1125
+ function makeSectionFrame(name: string): FrameNode {
981
1126
  const frame = figma.createFrame();
982
- frame.name = theme.toUpperCase() + ' Colors';
1127
+ frame.name = name;
983
1128
  frame.layoutMode = 'VERTICAL';
984
1129
  frame.primaryAxisSizingMode = 'AUTO';
985
1130
  frame.counterAxisSizingMode = 'AUTO';
@@ -987,8 +1132,13 @@ export function demoFrameColors(theme: string): any {
987
1132
  frame.paddingLeft = frame.paddingRight = frame.paddingTop = frame.paddingBottom = 16;
988
1133
  frame.fills = [];
989
1134
  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;
1135
+ return frame;
1136
+ }
1137
+
1138
+ export function demoFrameColors(theme: string): FrameNode {
1139
+ const frame = makeSectionFrame(theme.toUpperCase() + ' Colors');
1140
+ frame.appendChild(makeSectionTitle(getThemeDisplayName(theme)));
1141
+ const col: Record<string, string> = getThemeGroup(theme, 'color');
992
1142
  for (const [key, val] of Object.entries(col)) {
993
1143
  const row = figma.createFrame();
994
1144
  row.layoutMode = 'HORIZONTAL';
@@ -1001,42 +1151,237 @@ export function demoFrameColors(theme: string): any {
1001
1151
  swatch.resize(48, 32);
1002
1152
  const rgb = parseColor(val);
1003
1153
  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
1154
  bindColorVariable(swatch, key, 'fill', theme);
1006
1155
  swatch.strokes = [];
1007
1156
 
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
1157
+ const label = makeBodyText(key + ' \u2014 ' + colorToLabel(val));
1012
1158
  row.appendChild(swatch);
1013
1159
  row.appendChild(label);
1014
1160
  frame.appendChild(row);
1015
- idx++;
1016
1161
  }
1017
1162
  return frame;
1018
1163
  }
1019
1164
 
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) {
1165
+ export function demoFrameRadii(): FrameNode {
1166
+ const frame = makeSectionFrame('Radii');
1167
+
1168
+ const row = figma.createFrame();
1169
+ row.layoutMode = 'HORIZONTAL';
1170
+ row.primaryAxisSizingMode = 'AUTO';
1171
+ row.counterAxisSizingMode = 'AUTO';
1172
+ row.itemSpacing = 16;
1173
+ row.fills = [];
1174
+ row.strokes = [];
1175
+
1176
+ const r: Record<string, string> = getThemeGroup(_defaultThemeName, 'radius');
1177
+ for (const k of Object.keys(r)) {
1178
+ const cell = figma.createFrame();
1179
+ cell.layoutMode = 'VERTICAL';
1180
+ cell.primaryAxisSizingMode = 'AUTO';
1181
+ cell.counterAxisSizingMode = 'AUTO';
1182
+ cell.itemSpacing = 6;
1183
+ cell.fills = [];
1184
+ cell.strokes = [];
1185
+ cell.name = 'radius/' + k;
1186
+
1033
1187
  const rect = figma.createRectangle();
1034
1188
  rect.resize(96, 64);
1035
1189
  rect.cornerRadius = pxFromSizeToken(r[k]);
1036
1190
  rect.fills = [{ type: 'SOLID', color: { r: 0.9, g: 0.9, b: 0.9 } }];
1037
1191
  rect.strokes = [{ type: 'SOLID', color: { r: 0.75, g: 0.75, b: 0.75 } }];
1038
- rect.name = 'radius/' + k;
1039
- frame.appendChild(rect);
1192
+ rect.name = 'swatch';
1193
+
1194
+ cell.appendChild(rect);
1195
+ cell.appendChild(makeBodyText(k, { size: 12 }));
1196
+ row.appendChild(cell);
1197
+ }
1198
+ frame.appendChild(row);
1199
+ return frame;
1200
+ }
1201
+
1202
+ export function demoFrameFonts(theme: string): FrameNode | null {
1203
+ const fonts: Record<string, string> = getThemeGroup(theme, 'font');
1204
+ const keys = Object.keys(fonts);
1205
+ if (keys.length === 0) return null;
1206
+
1207
+ const frame = makeSectionFrame(theme.toUpperCase() + ' Fonts');
1208
+ frame.appendChild(makeSectionTitle(getThemeDisplayName(theme)));
1209
+
1210
+ for (const key of keys) {
1211
+ const family = (function () {
1212
+ const raw = String(fonts[key] || '');
1213
+ const parts = raw.split(',');
1214
+ for (const part of parts) {
1215
+ const trimmed = part.trim();
1216
+ const varMatch = trimmed.match(/^var\(--font-([a-z0-9-]+)\)/i);
1217
+ if (varMatch) {
1218
+ return varMatch[1].split('-').map((w: string) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
1219
+ }
1220
+ const lower = trimmed.toLowerCase().replace(/^["']|["']$/g, '');
1221
+ if (lower === 'ui-sans-serif' || lower === 'system-ui' || lower === 'sans-serif' || lower === 'serif' || lower === 'monospace' || lower === 'ui-monospace') continue;
1222
+ const cleaned = trimmed.replace(/^["']|["']$/g, '');
1223
+ if (cleaned) return cleaned;
1224
+ }
1225
+ return 'Inter';
1226
+ })();
1227
+
1228
+ const cell = figma.createFrame();
1229
+ cell.layoutMode = 'VERTICAL';
1230
+ cell.primaryAxisSizingMode = 'AUTO';
1231
+ cell.counterAxisSizingMode = 'AUTO';
1232
+ cell.itemSpacing = 4;
1233
+ cell.fills = [];
1234
+ cell.strokes = [];
1235
+ cell.name = 'font/' + key;
1236
+
1237
+ cell.appendChild(makeBodyText(key + ' \u2014 ' + family, { size: 12 }));
1238
+ cell.appendChild(makeBodyText('The quick brown fox jumps over the lazy dog', { family, size: 18 }));
1239
+ frame.appendChild(cell);
1240
+ }
1241
+ return frame;
1242
+ }
1243
+
1244
+ export function demoFrameSpacing(): FrameNode | null {
1245
+ const spacing: Record<string, string> = getThemeGroup(_defaultThemeName, 'spacing');
1246
+ const keys = Object.keys(spacing);
1247
+ if (keys.length === 0) return null;
1248
+
1249
+ const frame = makeSectionFrame('Spacing');
1250
+
1251
+ for (const k of keys) {
1252
+ const px = pxFromSizeToken(spacing[k]);
1253
+ const row = figma.createFrame();
1254
+ row.layoutMode = 'HORIZONTAL';
1255
+ row.primaryAxisSizingMode = 'AUTO';
1256
+ row.counterAxisSizingMode = 'AUTO';
1257
+ row.itemSpacing = 12;
1258
+ row.counterAxisAlignItems = 'CENTER';
1259
+ row.fills = [];
1260
+ row.strokes = [];
1261
+ row.name = 'spacing/' + k;
1262
+
1263
+ const bar = figma.createRectangle();
1264
+ bar.resize(Math.max(1, px), 16);
1265
+ bar.fills = [{ type: 'SOLID', color: { r: 0.5, g: 0.5, b: 0.5 } }];
1266
+ bar.strokes = [];
1267
+
1268
+ row.appendChild(bar);
1269
+ row.appendChild(makeBodyText(k + ' \u2014 ' + Math.round(px) + 'px', { size: 12 }));
1270
+ frame.appendChild(row);
1271
+ }
1272
+ return frame;
1273
+ }
1274
+
1275
+ export function demoFrameFontSizes(): FrameNode | null {
1276
+ const sizes: Record<string, string> = getThemeGroup(_defaultThemeName, 'fontSize');
1277
+ const keys = Object.keys(sizes);
1278
+ if (keys.length === 0) return null;
1279
+
1280
+ const frame = makeSectionFrame('Font sizes');
1281
+
1282
+ for (const k of keys) {
1283
+ const px = pxFromSizeToken(sizes[k]);
1284
+ frame.appendChild(makeBodyText(k + ' \u2014 ' + Math.round(px) + 'px sample', { size: px }));
1285
+ }
1286
+ return frame;
1287
+ }
1288
+
1289
+ export function demoFrameBreakpoints(): FrameNode | null {
1290
+ const breakpoints: Record<string, string> = getThemeGroup(_defaultThemeName, 'breakpoint');
1291
+ const keys = Object.keys(breakpoints);
1292
+ if (keys.length === 0) return null;
1293
+
1294
+ const sorted = keys
1295
+ .map((k) => ({ key: k, px: pxFromSizeToken(breakpoints[k]) }))
1296
+ .filter((entry) => Number.isFinite(entry.px))
1297
+ .sort((a, b) => a.px - b.px);
1298
+ if (sorted.length === 0) return null;
1299
+
1300
+ const frame = makeSectionFrame('Breakpoints');
1301
+ const maxPx = sorted[sorted.length - 1].px;
1302
+ // Cap the visualization width so a 96rem breakpoint doesn't blow out the
1303
+ // canvas; everything scales relative to the largest breakpoint.
1304
+ const maxBarWidth = 480;
1305
+ const scale = maxPx > 0 ? maxBarWidth / maxPx : 0;
1306
+
1307
+ for (const entry of sorted) {
1308
+ const row = figma.createFrame();
1309
+ row.layoutMode = 'HORIZONTAL';
1310
+ row.primaryAxisSizingMode = 'AUTO';
1311
+ row.counterAxisSizingMode = 'AUTO';
1312
+ row.itemSpacing = 12;
1313
+ row.counterAxisAlignItems = 'CENTER';
1314
+ row.fills = [];
1315
+ row.strokes = [];
1316
+ row.name = 'breakpoint/' + entry.key;
1317
+
1318
+ const bar = figma.createRectangle();
1319
+ bar.resize(Math.max(1, Math.round(entry.px * scale)), 12);
1320
+ bar.fills = [{ type: 'SOLID', color: { r: 0.5, g: 0.5, b: 0.5 } }];
1321
+ bar.strokes = [];
1322
+
1323
+ row.appendChild(bar);
1324
+ row.appendChild(makeBodyText(entry.key + ' \u2014 ' + Math.round(entry.px) + 'px', { size: 12 }));
1325
+ frame.appendChild(row);
1326
+ }
1327
+ return frame;
1328
+ }
1329
+
1330
+ // Sized scale we want to surface in the design tokens overview, in ascending
1331
+ // visual weight. Anything else (DEFAULT alias, inner/drop/text variants) is
1332
+ // skipped — they pollute the order and inset shadows render nothing on a
1333
+ // solid-fill rect.
1334
+ const SHADOW_SIZE_ORDER = ['2xs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl'];
1335
+
1336
+ export function demoFrameShadows(theme: string): FrameNode | null {
1337
+ const shadows: Record<string, string> = getThemeGroup(theme, 'shadow');
1338
+ const ordered = SHADOW_SIZE_ORDER.filter((k) => shadows[k]);
1339
+ if (ordered.length === 0) return null;
1340
+
1341
+ const frame = makeSectionFrame(theme.toUpperCase() + ' Shadows');
1342
+ // Larger gap so big shadows (shadow-xl, shadow-2xl) don't visually overlap
1343
+ // the next swatch — the default itemSpacing is too tight for shadow-2xl
1344
+ // which extends ~38px below its rect.
1345
+ frame.itemSpacing = 56;
1346
+ // Keep shadows from being clipped by the wrapper's bounds.
1347
+ frame.clipsContent = false;
1348
+ frame.appendChild(makeSectionTitle(getThemeDisplayName(theme)));
1349
+
1350
+ for (const k of ordered) {
1351
+ const cell = figma.createFrame();
1352
+ cell.layoutMode = 'VERTICAL';
1353
+ cell.primaryAxisSizingMode = 'AUTO';
1354
+ cell.counterAxisSizingMode = 'AUTO';
1355
+ cell.itemSpacing = 8;
1356
+ cell.fills = [];
1357
+ cell.strokes = [];
1358
+ cell.clipsContent = false;
1359
+ cell.name = 'shadow/' + k;
1360
+
1361
+ const swatch = figma.createRectangle();
1362
+ swatch.resize(96, 48);
1363
+ swatch.cornerRadius = 6;
1364
+ swatch.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
1365
+ swatch.strokes = [];
1366
+
1367
+ const layers = parseCssBoxShadow(String(shadows[k] || ''));
1368
+ if (layers.length > 0) {
1369
+ swatch.effects = layers
1370
+ .filter((l) => !l.inset)
1371
+ .map((l) => ({
1372
+ type: 'DROP_SHADOW' as const,
1373
+ color: { r: l.r, g: l.g, b: l.b, a: l.a },
1374
+ offset: { x: l.x, y: l.y },
1375
+ radius: l.blur,
1376
+ spread: l.spread,
1377
+ visible: true,
1378
+ blendMode: 'NORMAL' as const,
1379
+ }));
1380
+ }
1381
+
1382
+ cell.appendChild(swatch);
1383
+ cell.appendChild(makeBodyText(k, { size: 12 }));
1384
+ frame.appendChild(cell);
1040
1385
  }
1041
1386
  return frame;
1042
1387
  }