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
@@ -1,13 +1,13 @@
1
- import { TOKENS, COMPONENT_DEFS, getThemeColors, getThemeRadius } from './tokens';
2
- import { parseColor, debug } from './colors';
1
+ import { TOKENS, COMPONENT_DEFS, getThemeColors, getThemeRadius } from '../tokens';
2
+ import { parseColor, debug } from '../tokens';
3
3
  import { getComponentDef } from './component-defs';
4
- import { tailwindClassesToStyle, applyTailwindStylesToFrame } from './tailwind';
5
- import { bindColorVariable, pxFromSizeToken } from './variables';
6
- import { createTextNode } from './text-builder';
7
- import { extractStatesFromClasses, mergeStatesWithDefinition, type StateInfo } from './state-analyzer';
8
- import { extractArbitraryValue, parseLength } from './utility-resolver';
9
-
10
- type RingInfo = { width: number; color: { r: number; g: number; b: number; a?: number } };
4
+ import type { ComponentAnalysis } from '../../scanner/types';
5
+ import type { ComponentDef } from './scanner-types';
6
+ import { tailwindClassesToStyle, applyTailwindStylesToFrame } from '../tailwind';
7
+ import { bindColorVariable, pxFromSizeToken } from '../tokens';
8
+ import { createTextNode } from '../text';
9
+ import { extractStatesFromClasses, mergeStatesWithDefinition, type StateInfo } from '../tailwind';
10
+ import { getRingInfoFromClasses, markRingNode, applyRingIfPossible } from '../layout';
11
11
 
12
12
  function getStateEntry(states: StateInfo[], name: string): StateInfo | null {
13
13
  for (let i = 0; i < states.length; i++) {
@@ -35,100 +35,7 @@ function buildStateClasses(states: StateInfo[], name: string): string[] {
35
35
  return baseClasses.concat(entry.classes);
36
36
  }
37
37
 
38
- function parseRingWidth(utility: string): number | null {
39
- if (utility === 'ring') return 3;
40
- if (!utility.startsWith('ring-')) return null;
41
- const token = utility.substring(5);
42
- if (token === 'inset' || token.startsWith('offset-')) return null;
43
- if (token.startsWith('[')) {
44
- const arbitrary = extractArbitraryValue(utility);
45
- if (!arbitrary) return null;
46
- return parseLength(arbitrary);
47
- }
48
- const num = parseFloat(token);
49
- if (!Number.isNaN(num) && String(num) === token) return num;
50
- return null;
51
- }
52
-
53
- function parseRingColor(utility: string, colorGroup: Record<string, string>): { r: number; g: number; b: number; a?: number } | null {
54
- if (!utility.startsWith('ring-')) return null;
55
- const token = utility.substring(5);
56
- if (token === 'inset' || token.startsWith('offset-')) return null;
57
- if (token.startsWith('[')) return null;
58
- const num = parseFloat(token);
59
- if (!Number.isNaN(num) && String(num) === token) return null;
60
- const resolved = colorGroup[token];
61
- if (!resolved) return null;
62
- return parseColor(resolved);
63
- }
64
-
65
- function getRingInfoFromClasses(classes: string[], colorGroup: Record<string, string>): RingInfo | null {
66
- let width: number | null = null;
67
- let color: { r: number; g: number; b: number; a?: number } | null = null;
68
-
69
- for (let i = 0; i < classes.length; i++) {
70
- const cls = classes[i];
71
- const nextWidth = parseRingWidth(cls);
72
- if (nextWidth != null) width = nextWidth;
73
- const nextColor = parseRingColor(cls, colorGroup);
74
- if (nextColor) color = nextColor;
75
- }
76
-
77
- if (width == null && color == null) return null;
78
- if (width == null) width = 3;
79
- if (!color) {
80
- const fallback = colorGroup.ring || colorGroup.primary;
81
- if (!fallback) return null;
82
- color = parseColor(fallback);
83
- }
84
- if (!width || width <= 0) return null;
85
- return { width, color };
86
- }
87
-
88
- function hasVisibleFill(node: FrameNode): boolean {
89
- const fills = (node as any).fills;
90
- if (!Array.isArray(fills)) return false;
91
- for (let i = 0; i < fills.length; i++) {
92
- const fill = fills[i];
93
- if (!fill || fill.visible === false) continue;
94
- if (fill.opacity != null && fill.opacity <= 0) continue;
95
- return true;
96
- }
97
- return false;
98
- }
99
-
100
- function applyRingEffect(node: FrameNode, classes: string[], colorGroup: Record<string, string>): void {
101
- const ring = getRingInfoFromClasses(classes, colorGroup);
102
- if (!ring) return;
103
- const useSpread = (node as any).clipsContent === true && hasVisibleFill(node);
104
- if (!useSpread) {
105
- const strokeWeight = typeof node.strokeWeight === 'number' ? node.strokeWeight : 0;
106
- node.strokes = [{
107
- type: 'SOLID',
108
- color: { r: ring.color.r, g: ring.color.g, b: ring.color.b },
109
- opacity: ring.color.a == null ? 1 : ring.color.a,
110
- }];
111
- node.strokeWeight = Math.max(strokeWeight, ring.width);
112
- return;
113
- }
114
- const effect: DropShadowEffect = {
115
- type: 'DROP_SHADOW',
116
- color: { r: ring.color.r, g: ring.color.g, b: ring.color.b, a: ring.color.a == null ? 1 : ring.color.a },
117
- offset: { x: 0, y: 0 },
118
- radius: 0,
119
- spread: ring.width,
120
- visible: true,
121
- blendMode: 'NORMAL'
122
- };
123
- const effects: Effect[] = [];
124
- if (node.effects && node.effects.length > 0) {
125
- for (let i = 0; i < node.effects.length; i++) effects.push(node.effects[i]);
126
- }
127
- effects.push(effect);
128
- node.effects = effects;
129
- }
130
-
131
- export function createCVAComponentSet(parent: any, def: any, theme: string): any {
38
+ export function createCVAComponentSet(parent: FrameNode | PageNode, def: ComponentDef, theme: string): SceneNode | null {
132
39
  const colorGroup = getThemeColors(TOKENS, theme);
133
40
  const radiusGroup = getThemeRadius(TOKENS, theme);
134
41
 
@@ -210,7 +117,13 @@ export function createCVAComponentSet(parent: any, def: any, theme: string): any
210
117
  // Apply background - try variable binding first, fall back to raw color
211
118
  if (style.bg) {
212
119
  const bgBound = style.bgToken && bindColorVariable(comp, style.bgToken, 'fill', theme);
213
- if (!bgBound) {
120
+ if (bgBound) {
121
+ if (style.bgOpacity != null && Array.isArray(comp.fills) && comp.fills.length > 0) {
122
+ const nextFills = JSON.parse(JSON.stringify(comp.fills));
123
+ nextFills[0].opacity = style.bgOpacity;
124
+ comp.fills = nextFills;
125
+ }
126
+ } else {
214
127
  const bg = parseColor(style.bg);
215
128
  const bgOpacity = style.bgOpacity != null ? style.bgOpacity : (bg.a == null ? 1 : bg.a);
216
129
  comp.fills = [{ type: 'SOLID', color: { r: bg.r, g: bg.g, b: bg.b }, opacity: bgOpacity }];
@@ -242,8 +155,14 @@ export function createCVAComponentSet(parent: any, def: any, theme: string): any
242
155
  }
243
156
 
244
157
  comp.appendChild(text);
245
- applyRingEffect(comp, stateClasses, colorGroup);
158
+ const ringInfo = getRingInfoFromClasses(stateClasses, colorGroup);
159
+ if (ringInfo) markRingNode(comp, ringInfo);
246
160
  stateRow.appendChild(comp);
161
+ // Comp's children are in place and it's now in the parent's auto-layout
162
+ // — apply ring (no-op if not marked). `applyRingIfPossible` locks the
163
+ // comp's sizing modes during the overlay append so inflation is
164
+ // impossible.
165
+ applyRingIfPossible(comp, stateRow);
247
166
  }
248
167
 
249
168
  variantSection.appendChild(stateRow);
@@ -254,7 +173,7 @@ export function createCVAComponentSet(parent: any, def: any, theme: string): any
254
173
  return container;
255
174
  }
256
175
 
257
- export function createStateComponentSet(parent: any, def: any, theme: string): any {
176
+ export function createStateComponentSet(parent: FrameNode | PageNode, def: ComponentDef, theme: string): SceneNode | null {
258
177
  const colorGroup = getThemeColors(TOKENS, theme);
259
178
  const radiusGroup = getThemeRadius(TOKENS, theme);
260
179
 
@@ -304,7 +223,13 @@ export function createStateComponentSet(parent: any, def: any, theme: string): a
304
223
 
305
224
  if (style.bg) {
306
225
  const bgBound = style.bgToken && bindColorVariable(comp, style.bgToken, 'fill', theme);
307
- if (!bgBound) {
226
+ if (bgBound) {
227
+ if (style.bgOpacity != null && Array.isArray(comp.fills) && comp.fills.length > 0) {
228
+ const nextFills = JSON.parse(JSON.stringify(comp.fills));
229
+ nextFills[0].opacity = style.bgOpacity;
230
+ comp.fills = nextFills;
231
+ }
232
+ } else {
308
233
  const bg = parseColor(style.bg);
309
234
  const bgOpacity = style.bgOpacity != null ? style.bgOpacity : (bg.a == null ? 1 : bg.a);
310
235
  comp.fills = [{ type: 'SOLID', color: { r: bg.r, g: bg.g, b: bg.b }, opacity: bgOpacity }];
@@ -349,9 +274,11 @@ export function createStateComponentSet(parent: any, def: any, theme: string): a
349
274
  fill: { r: placeholderColor.r, g: placeholderColor.g, b: placeholderColor.b }
350
275
  });
351
276
  comp.appendChild(placeholder);
352
- applyRingEffect(comp, stateClasses, colorGroup);
277
+ const ringInfo = getRingInfoFromClasses(stateClasses, colorGroup);
278
+ if (ringInfo) markRingNode(comp, ringInfo);
353
279
 
354
280
  stateRow.appendChild(comp);
281
+ applyRingIfPossible(comp, stateRow);
355
282
  }
356
283
 
357
284
  container.appendChild(stateRow);
@@ -359,7 +286,7 @@ export function createStateComponentSet(parent: any, def: any, theme: string): a
359
286
  return container;
360
287
  }
361
288
 
362
- export function createCompoundComponent(parent: any, def: any, theme: string): any {
289
+ export function createCompoundComponent(parent: FrameNode | PageNode, def: ComponentDef, theme: string): SceneNode | null {
363
290
  const colorGroup = getThemeColors(TOKENS, theme);
364
291
  const radiusGroup = getThemeRadius(TOKENS, theme);
365
292
 
@@ -372,7 +299,7 @@ export function createCompoundComponent(parent: any, def: any, theme: string): a
372
299
  container.strokes = [];
373
300
 
374
301
  // Find the main container sub-component
375
- let mainSub: any = null;
302
+ let mainSub: ComponentDef = null;
376
303
  const subComponents = def.subComponents || [];
377
304
  for (let i = 0; i < subComponents.length; i++) {
378
305
  if (subComponents[i].slot === 'container' || subComponents[i].name === def.name) {
@@ -391,7 +318,7 @@ export function createCompoundComponent(parent: any, def: any, theme: string): a
391
318
  container.primaryAxisSizingMode = 'AUTO';
392
319
 
393
320
  // Helper to find a sub-component by slot
394
- function findSub(slot: string): any {
321
+ function findSub(slot: string): ComponentDef {
395
322
  for (let j = 0; j < subComponents.length; j++) {
396
323
  if (subComponents[j].slot === slot) return subComponents[j];
397
324
  }
@@ -491,7 +418,7 @@ export function createCompoundComponent(parent: any, def: any, theme: string): a
491
418
  return container;
492
419
  }
493
420
 
494
- export function createSimpleComponent(parent: any, def: any, theme: string): any {
421
+ export function createSimpleComponent(parent: FrameNode | PageNode, def: ComponentDef, theme: string): SceneNode | null {
495
422
  const colorGroup = getThemeColors(TOKENS, theme);
496
423
  const radiusGroup = getThemeRadius(TOKENS, theme);
497
424
 
@@ -512,7 +439,7 @@ export function createSimpleComponent(parent: any, def: any, theme: string): any
512
439
  return frame;
513
440
  }
514
441
 
515
- export function createComponentFromDef(parent: any, componentName: string, theme: string, options?: any): any {
442
+ export function createComponentFromDef(parent: FrameNode | PageNode, componentName: string, theme: string, _options?: { types?: string[]; exclude?: string[] }): SceneNode | null {
516
443
  const def = getComponentDef(componentName);
517
444
  if (!def) {
518
445
  debug('Component not found in COMPONENT_DEFS:', componentName);
@@ -531,18 +458,18 @@ export function createComponentFromDef(parent: any, componentName: string, theme
531
458
  case 'simple':
532
459
  return createSimpleComponent(parent, def, theme);
533
460
  default:
534
- debug('Unknown component type:', def.type);
461
+ debug('Unknown component type:', (def as ComponentAnalysis).type);
535
462
  return null;
536
463
  }
537
464
  }
538
465
 
539
- export function createAllComponentsFromDefs(parent: any, theme: string, options?: any): any[] {
466
+ export function createAllComponentsFromDefs(parent: FrameNode | PageNode, theme: string, options?: { types?: string[]; exclude?: string[] }): SceneNode[] {
540
467
  options = options || {};
541
468
  const allowedTypes = options.types || ['cva', 'compound', 'state', 'simple'];
542
469
  const excludeNames = options.exclude || [];
543
470
 
544
471
  const components = COMPONENT_DEFS && COMPONENT_DEFS.components ? COMPONENT_DEFS.components : [];
545
- const created: any[] = [];
472
+ const created: SceneNode[] = [];
546
473
 
547
474
  for (let i = 0; i < components.length; i++) {
548
475
  const def = components[i];
@@ -0,0 +1,386 @@
1
+ import type { BackendCtx, ComponentDef, ComponentInstance, ComponentStory } from './scanner-types';
2
+
3
+ /**
4
+ * Whether a CVA instance carries non-text JSX children that the symbol-instance
5
+ * path can't honour. Figma's component-property API can override TEXT and
6
+ * boolean/instance-swap properties on an instance — but it can't swap arbitrary
7
+ * SVG vectors. So when the consumer writes
8
+ * `<Toggle><AlignLeft /></Toggle>` inside a `<ToggleGroup>`, the Toggle master
9
+ * (built from Toggle's own stories) has a fixed icon, and every cloned instance
10
+ * inherits it. Result: all 3 ToggleGroup items render the same icon.
11
+ *
12
+ * The plugin's symmetric guard at the STORY root (`shouldUseInstance` in
13
+ * story-render-strategy.ts) already falls back to JSX-tree rendering when the
14
+ * root instance has `__jsxChildren`. This is the parallel guard for NESTED
15
+ * CVA instances inside a JSX tree (e.g. ToggleGroup items, icon-bearing
16
+ * Buttons inside another component's story). Returning null causes the JSX-
17
+ * tree renderer to build a frame for the instance instead, with its own
18
+ * per-instance children preserved.
19
+ *
20
+ * Text-only `__jsxChildren` (e.g. `<Button>Click me</Button>`) is NOT a
21
+ * reason to fall back — the text-override path handles it correctly. We
22
+ * only fall back when there's at least one element-type child.
23
+ */
24
+ export function cvaInstanceHasOverridingJsxChildren(props: { __jsxChildren?: unknown } | null | undefined): boolean {
25
+ if (!props) return false;
26
+ const children = props.__jsxChildren;
27
+ if (!Array.isArray(children)) return false;
28
+ for (let i = 0; i < children.length; i++) {
29
+ const c = children[i];
30
+ if (c && typeof c === 'object' && (c as { type?: string }).type === 'element') return true;
31
+ }
32
+ return false;
33
+ }
34
+
35
+ /**
36
+ * State props that, when truthy on a CVA instance, require frame-render
37
+ * fall-back instead of a symbol-instance reuse. The reason: Figma's
38
+ * component instance API can't per-instance-activate data-attribute CSS
39
+ * variants. So even if the icon matches the master, an instance with
40
+ * `defaultPressed=true` wouldn't get the master's pressed styling unless
41
+ * the master itself was built from a pressed story — which is non-
42
+ * deterministic and brittle. Frame rendering lets the variant engine
43
+ * activate `data-[pressed]:*` / `data-[checked]:*` per-instance correctly.
44
+ *
45
+ * The set mirrors DATA_ATTR_PROP_ALIASES in node-variants.ts. Only the
46
+ * "user explicitly wants this state" props (defaultX / X) are included —
47
+ * not the `aria-*` mirrors, which are accessibility metadata and don't
48
+ * by themselves indicate a visual state requested by the consumer.
49
+ */
50
+ const STATE_OVERRIDING_PROP_NAMES = [
51
+ 'defaultPressed',
52
+ 'pressed',
53
+ 'defaultChecked',
54
+ 'checked',
55
+ 'disabled',
56
+ 'defaultOpen',
57
+ 'open',
58
+ 'selected',
59
+ ] as const;
60
+
61
+ function isStateTruthy(value: unknown): boolean {
62
+ if (value === true) return true;
63
+ if (typeof value === 'string') {
64
+ const lower = value.trim().toLowerCase();
65
+ return lower === 'true' || lower === 'on' || lower === '';
66
+ }
67
+ return false;
68
+ }
69
+
70
+ /**
71
+ * Whether a CVA instance carries any truthy state-conveying prop that
72
+ * the master-instance path can't honour. See STATE_OVERRIDING_PROP_NAMES.
73
+ *
74
+ * `disabled=false` etc. return false (the prop is set but to a falsy
75
+ * value, which doesn't change the visual). Only truthy values trigger
76
+ * fall-back.
77
+ */
78
+ export function cvaInstanceHasOverridingState(
79
+ props: Record<string, unknown> | null | undefined
80
+ ): boolean {
81
+ if (!props) return false;
82
+ for (let i = 0; i < STATE_OVERRIDING_PROP_NAMES.length; i++) {
83
+ if (isStateTruthy(props[STATE_OVERRIDING_PROP_NAMES[i]])) return true;
84
+ }
85
+ return false;
86
+ }
87
+
88
+ /**
89
+ * Extract the tagName of the first JSX-element child of an instance.
90
+ * Used by the smart fall-back in `tryCreateCvaComponentInstance` to decide
91
+ * whether the instance's icon matches the master's recorded icon.
92
+ *
93
+ * Returns null when there's no element child (text-only or empty).
94
+ */
95
+ export function getFirstElementJsxChildTagName(jsxChildren: unknown): string | null {
96
+ if (!Array.isArray(jsxChildren)) return null;
97
+ for (let i = 0; i < jsxChildren.length; i++) {
98
+ const c = jsxChildren[i];
99
+ if (c && typeof c === 'object' && (c as { type?: string }).type === 'element') {
100
+ const tag = (c as { tagName?: string }).tagName;
101
+ if (typeof tag === 'string' && tag.length > 0) return tag;
102
+ }
103
+ }
104
+ return null;
105
+ }
106
+
107
+ /**
108
+ * The plugin-data key under which `cva-master` records the icon name a
109
+ * CVA variant master was built with. `tryCreateCvaComponentInstance` reads
110
+ * it to decide whether an instance with element children can safely reuse
111
+ * the master (icons match) or must fall back to frame rendering
112
+ * (icons differ).
113
+ */
114
+ export const MASTER_ICON_NAME_KEY = 'inkbridge:master-icon-name';
115
+
116
+ function copyVariantFillsToInstance(componentSet: SceneNode | null, instanceNode: InstanceNode | null, properties: Record<string, string>): void {
117
+ if (!componentSet || !('children' in componentSet) || !Array.isArray(componentSet.children)) return;
118
+ if (!instanceNode) return;
119
+ const keys = Object.keys(properties);
120
+ if (keys.length === 0) return;
121
+
122
+ // Match variant name: `Key1=Value1, Key2=Value2`. Split on ', ' and compare
123
+ // as an unordered set so order differences don't break the lookup.
124
+ const targetTokens: Record<string, string> = {};
125
+ for (let i = 0; i < keys.length; i++) {
126
+ targetTokens[keys[i]] = properties[keys[i]];
127
+ }
128
+ let match: ComponentNode | null = null;
129
+ const children = componentSet.children;
130
+ for (let i = 0; i < children.length; i++) {
131
+ const child = children[i];
132
+ if (!child || typeof child.name !== 'string') continue;
133
+ const parts = child.name.split(', ');
134
+ const tokens: Record<string, string> = {};
135
+ for (let j = 0; j < parts.length; j++) {
136
+ const eq = parts[j].indexOf('=');
137
+ if (eq > 0) tokens[parts[j].slice(0, eq)] = parts[j].slice(eq + 1);
138
+ }
139
+ let allMatch = true;
140
+ for (let j = 0; j < keys.length; j++) {
141
+ if (tokens[keys[j]] !== targetTokens[keys[j]]) { allMatch = false; break; }
142
+ }
143
+ if (allMatch && child.type === 'COMPONENT') { match = child; break; }
144
+ }
145
+ if (!match) return;
146
+ if (!Array.isArray(match.fills) || match.fills.length === 0) return;
147
+
148
+ try {
149
+ instanceNode.fills = JSON.parse(JSON.stringify(match.fills));
150
+ } catch (_error) {
151
+ // Leave instance fills as-is if the override fails.
152
+ }
153
+ }
154
+
155
+ export type InstanceBackend = {
156
+ enableSymbolMasters: boolean;
157
+ splitClassName: (value: string | undefined) => string[];
158
+ ensureCvaComponentSet: (def: ComponentDef, theme: string, ctx: BackendCtx) => SceneNode | null;
159
+ ensureStateComponentSet: (def: ComponentDef, theme: string, ctx: BackendCtx) => SceneNode | null;
160
+ ensureNonCvaComponentMaster: (def: ComponentDef, theme: string, ctx: BackendCtx) => SceneNode | null;
161
+ getInstantiableComponent: (node: SceneNode) => ComponentNode | null;
162
+ getCvaSelectionFromInstance: (def: ComponentDef, instance: ComponentInstance) => Record<string, string>;
163
+ toFigmaVariantPropertyName: (key: string) => string;
164
+ toFigmaVariantPropertyValue: (value: string) => string;
165
+ getStateVariantForInstance: (def: ComponentDef, instance: ComponentInstance) => string;
166
+ isCheckedInstance: (instance: ComponentInstance) => boolean;
167
+ hasCheckedStateVariant: (def: ComponentDef) => boolean;
168
+ getInstanceTextOverride: (def: ComponentDef, instance: ComponentInstance) => string | null;
169
+ applyTextOverrideToInstance: (instanceNode: InstanceNode, value: string) => boolean;
170
+ applyCompoundTextOverridesToInstance: (def: ComponentDef, story: ComponentStory, instance: ComponentInstance, instanceNode: InstanceNode) => {
171
+ bySlot: Record<string, string>;
172
+ bySubcomponent: Record<string, string>;
173
+ };
174
+ analyzeSymbolPolicy: (def: ComponentDef, instance: ComponentInstance) => {
175
+ reason?: string | null;
176
+ ignoredProps: string[];
177
+ slotPropMappings: Record<string, string>;
178
+ };
179
+ setGeneratedSymbolDebugData: (node: SceneNode, data: Record<string, unknown>) => void;
180
+ };
181
+
182
+ export function tryCreateCvaComponentInstance(
183
+ def: ComponentDef,
184
+ instance: ComponentInstance,
185
+ theme: string,
186
+ ctx: BackendCtx,
187
+ backend: InstanceBackend
188
+ ): InstanceNode | null {
189
+ if (!backend.enableSymbolMasters) return null;
190
+ const props = instance && instance.props ? instance.props : {};
191
+ if (backend.splitClassName(props.className).length > 0) {
192
+ return null;
193
+ }
194
+
195
+ const variantKeys = Object.keys((def && def.variants) || {});
196
+ if (variantKeys.length === 0) return null;
197
+
198
+ const componentSet = backend.ensureCvaComponentSet(def, theme, ctx);
199
+ if (!componentSet) return null;
200
+
201
+ const sourceComponent = backend.getInstantiableComponent(componentSet);
202
+ if (!sourceComponent) return null;
203
+
204
+ // Smart fall-back: when the instance has overriding JSX-element children
205
+ // (e.g. an icon child like `<Bold />`), the symbol-instance path can only
206
+ // produce a faithful render if the master happens to have been built with
207
+ // the SAME icon — Figma's instance API can't per-instance-swap SVG
208
+ // vectors. So compare the master's recorded icon name (stamped by
209
+ // cva-master when the variant was built) against the instance's first
210
+ // element-child tagName:
211
+ // - match → master would render the same icon → proceed (symbol instance)
212
+ // - differ → master would render the WRONG icon → return null (caller
213
+ // falls through to JSX-tree frame rendering so the actual
214
+ // instance icon is preserved)
215
+ // For text-only children (e.g. `<Button>Click me</Button>`), the text-
216
+ // override path handles per-instance content and this guard is a no-op.
217
+ if (cvaInstanceHasOverridingJsxChildren(props)) {
218
+ const masterIconName = typeof sourceComponent.getPluginData === 'function'
219
+ ? sourceComponent.getPluginData(MASTER_ICON_NAME_KEY)
220
+ : '';
221
+ const instanceIconName = getFirstElementJsxChildTagName(props.__jsxChildren);
222
+ if (!masterIconName || !instanceIconName || masterIconName !== instanceIconName) {
223
+ return null;
224
+ }
225
+ }
226
+
227
+ // Secondary fall-back: even when the icon matches, an instance carrying
228
+ // a truthy state prop (`defaultPressed`, `defaultChecked`, etc.) can't
229
+ // be expressed as a symbol-instance reuse — the master was built once
230
+ // from one story whose state is fixed in its baked-in classes, and
231
+ // Figma's instance API doesn't apply per-instance `data-[pressed]:*`
232
+ // resolution. Fall back to frame rendering so the variant engine
233
+ // (DATA_ATTR_PROP_ALIASES in node-variants.ts) activates the right
234
+ // state styling at render time.
235
+ if (cvaInstanceHasOverridingState(props)) {
236
+ return null;
237
+ }
238
+
239
+ let figmaInstance: InstanceNode | null = null;
240
+ try {
241
+ figmaInstance = sourceComponent.createInstance();
242
+ } catch (_error) {
243
+ return null;
244
+ }
245
+ if (!figmaInstance) return null;
246
+
247
+ const selection = backend.getCvaSelectionFromInstance(def, instance);
248
+ const properties: Record<string, string> = {};
249
+ for (let i = 0; i < variantKeys.length; i++) {
250
+ const key = variantKeys[i];
251
+ if (!selection[key]) continue;
252
+ properties[backend.toFigmaVariantPropertyName(key)] = backend.toFigmaVariantPropertyValue(selection[key]);
253
+ }
254
+
255
+ if (Object.keys(properties).length > 0) {
256
+ try {
257
+ figmaInstance.setProperties(properties);
258
+ } catch (_error) {
259
+ // Keep the default instance when property mapping fails.
260
+ }
261
+ // Figma's variant swap inherits boundVariables.color from the target variant
262
+ // but drops paint.opacity — so for tokens with a Tailwind alpha modifier
263
+ // (e.g. bg-primary/10), the instance renders at full opacity. Copy the
264
+ // matching variant master's fills onto the instance to restore opacity.
265
+ copyVariantFillsToInstance(componentSet, figmaInstance, properties);
266
+ }
267
+
268
+ const textOverrides: Record<string, string> = {};
269
+ const textOverride = backend.getInstanceTextOverride(def, instance);
270
+ if (textOverride) {
271
+ const applied = backend.applyTextOverrideToInstance(figmaInstance, textOverride);
272
+ if (applied) {
273
+ textOverrides.primary = textOverride;
274
+ }
275
+ }
276
+
277
+ backend.setGeneratedSymbolDebugData(figmaInstance, {
278
+ decision: 'cva-instance',
279
+ ignoredProps: [],
280
+ slotPropMappings: {},
281
+ textOverrides,
282
+ });
283
+
284
+ return figmaInstance;
285
+ }
286
+
287
+ export function tryCreateNonCvaComponentInstance(
288
+ def: ComponentDef,
289
+ instance: ComponentInstance,
290
+ theme: string,
291
+ ctx: BackendCtx,
292
+ backend: InstanceBackend,
293
+ story?: ComponentStory
294
+ ): InstanceNode | null {
295
+ if (!backend.enableSymbolMasters) return null;
296
+ if (!def || def.type === 'cva') return null;
297
+ if (def.symbolCandidate === false) return null;
298
+ const props = instance && instance.props ? instance.props : {};
299
+
300
+ if (def.type === 'state' && backend.splitClassName(props.className).length === 0) {
301
+ const stateSet = backend.ensureStateComponentSet(def, theme, ctx);
302
+ const sourceComponent = stateSet ? backend.getInstantiableComponent(stateSet) : null;
303
+ if (sourceComponent) {
304
+ try {
305
+ const figmaInstance = sourceComponent.createInstance();
306
+ const requestedState = backend.getStateVariantForInstance(def, instance);
307
+ const requestedChecked = backend.isCheckedInstance(instance);
308
+ try {
309
+ const stateProps: Record<string, string> = {
310
+ [backend.toFigmaVariantPropertyName('state')]: backend.toFigmaVariantPropertyValue(requestedState),
311
+ };
312
+ if (backend.hasCheckedStateVariant(def)) {
313
+ stateProps[backend.toFigmaVariantPropertyName('checked')] =
314
+ backend.toFigmaVariantPropertyValue(String(requestedChecked));
315
+ }
316
+ figmaInstance.setProperties(stateProps);
317
+ } catch (_setPropertiesError) {
318
+ // Keep default instance when mapping fails.
319
+ }
320
+
321
+ const textOverrides: Record<string, string> = {};
322
+ // Checked state controls (e.g. checkbox/switch) should not absorb story labels
323
+ // into the symbol itself; labels belong to surrounding layout.
324
+ if (!backend.hasCheckedStateVariant(def)) {
325
+ const textOverride = backend.getInstanceTextOverride(def, instance);
326
+ if (textOverride) {
327
+ const applied = backend.applyTextOverrideToInstance(figmaInstance, textOverride);
328
+ if (applied) textOverrides.primary = textOverride;
329
+ }
330
+ }
331
+ backend.setGeneratedSymbolDebugData(figmaInstance, {
332
+ decision: 'state-instance',
333
+ ignoredProps: [],
334
+ slotPropMappings: {},
335
+ textOverrides,
336
+ });
337
+ return figmaInstance;
338
+ } catch (_error) {
339
+ // Fall back to raw rendering below.
340
+ }
341
+ }
342
+ }
343
+
344
+ const policy = backend.analyzeSymbolPolicy(def, instance);
345
+ if (policy.reason) return null;
346
+
347
+ const master = backend.ensureNonCvaComponentMaster(def, theme, ctx);
348
+ if (!master || master.type !== 'COMPONENT') return null;
349
+
350
+ try {
351
+ const figmaInstance = master.createInstance();
352
+ const textOverrides: Record<string, string> = {};
353
+ if (def.type === 'compound' && story) {
354
+ const applied = backend.applyCompoundTextOverridesToInstance(def, story, instance, figmaInstance);
355
+ const appliedSlots = Object.keys(applied.bySlot);
356
+ if (appliedSlots.length > 0) {
357
+ for (let i = 0; i < appliedSlots.length; i++) {
358
+ const key = appliedSlots[i];
359
+ textOverrides[key] = applied.bySlot[key];
360
+ }
361
+ } else {
362
+ const appliedSubs = Object.keys(applied.bySubcomponent);
363
+ for (let i = 0; i < appliedSubs.length; i++) {
364
+ const key = appliedSubs[i];
365
+ textOverrides[key] = applied.bySubcomponent[key];
366
+ }
367
+ }
368
+ } else {
369
+ const textOverride = backend.getInstanceTextOverride(def, instance);
370
+ if (textOverride) {
371
+ const applied = backend.applyTextOverrideToInstance(figmaInstance, textOverride);
372
+ if (applied) textOverrides.primary = textOverride;
373
+ }
374
+ }
375
+
376
+ backend.setGeneratedSymbolDebugData(figmaInstance, {
377
+ decision: 'instance',
378
+ ignoredProps: policy.ignoredProps,
379
+ slotPropMappings: policy.slotPropMappings,
380
+ textOverrides,
381
+ });
382
+ return figmaInstance;
383
+ } catch (_error) {
384
+ return null;
385
+ }
386
+ }
@@ -0,0 +1,44 @@
1
+ import { findChildByName } from '../cache';
2
+
3
+ export const DESIGN_SYSTEM_PAGE_NAME = 'Design System';
4
+ export const COMPONENT_LIBRARY_ROOT_NAME = '__Inkbridge Component Library';
5
+
6
+ export function getInstantiableComponent(node: BaseNode | null): ComponentNode | null {
7
+ if (!node) return null;
8
+ if (node.type === 'COMPONENT') return node;
9
+ if (node.type === 'COMPONENT_SET') {
10
+ if (node.defaultVariant) return node.defaultVariant;
11
+ for (const candidate of node.children) {
12
+ if (candidate.type === 'COMPONENT') return candidate;
13
+ }
14
+ }
15
+ return null;
16
+ }
17
+
18
+ export function findExistingComponentLibraryRoot(): FrameNode | null {
19
+ const pages = figma.root && Array.isArray(figma.root.children) ? figma.root.children : [];
20
+ for (let i = 0; i < pages.length; i++) {
21
+ const page = pages[i];
22
+ if (!page || page.type !== 'PAGE' || !Array.isArray(page.children)) {
23
+ continue;
24
+ }
25
+ const root = findChildByName(page, COMPONENT_LIBRARY_ROOT_NAME);
26
+ if (root && root.type === 'FRAME') return root;
27
+ }
28
+ return null;
29
+ }
30
+
31
+ // findChildByName returns `any` at the boundary; the caller type-narrows.
32
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
+ export function findExistingThemeComponentLibrary(theme: string): any | null {
34
+ const root = findExistingComponentLibraryRoot();
35
+ if (!root) return null;
36
+ return findChildByName(root, String(theme || 'primary') + ' Masters');
37
+ }
38
+
39
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
+ export function findExistingThemedComponentNode(theme: string, name: string): any | null {
41
+ const themeLibrary = findExistingThemeComponentLibrary(theme);
42
+ if (!themeLibrary) return null;
43
+ return findChildByName(themeLibrary, name);
44
+ }