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
@@ -0,0 +1,333 @@
1
+ import { splitClassName, applyTailwindStylesToFrame, tailwindClassesToStyle } from '../tailwind';
2
+ import { parseColor } from '../tokens';
3
+ import { createTextNode } from '../text';
4
+ import { enforceFixedBoxSizingAfterLayout } from '../layout';
5
+ import {
6
+ analyzeSymbolPolicy,
7
+ extractCompoundSlotTextOverridesFromProps,
8
+ getInstantiableComponent,
9
+ tryCreateCvaComponentInstance as tryCreateCvaComponentInstanceShared,
10
+ tryCreateNonCvaComponentInstance as tryCreateNonCvaComponentInstanceShared,
11
+ } from '../components';
12
+ import { setGeneratedFallbackReason, setGeneratedSymbolDebugData } from './generated-node';
13
+ import { getThemeContext } from './theme-context';
14
+ import {
15
+ toFigmaVariantPropertyName,
16
+ toFigmaVariantPropertyValue,
17
+ ENABLE_SYMBOL_MASTERS,
18
+ } from './master-shared';
19
+ import {
20
+ ensureStateComponentSet,
21
+ isCheckedInstance,
22
+ hasCheckedStateVariant,
23
+ getStateVariantForInstance,
24
+ } from './state-master';
25
+ import {
26
+ ensureCvaComponentSet,
27
+ getCvaSelectionFromInstance,
28
+ isTruthyVariantValue,
29
+ applyStyleToFrame,
30
+ getCvaSymbolFallbackReason,
31
+ } from './cva-master';
32
+ import { buildCvaClassesWithSelection } from './cva-utils';
33
+ import { getPreviewTypography } from './typography';
34
+ import { createStateStoryFrame, createSimpleStoryFrame } from './story-frames';
35
+ import { ensureNonCvaComponentMaster } from './non-cva-master';
36
+ import {
37
+ getInstanceTextOverride,
38
+ applyTextOverrideToInstance,
39
+ getNonCvaSymbolFallbackReason,
40
+ getInstanceChildrenText,
41
+ } from './symbol-fallback';
42
+ import {
43
+ normalizeComponentName,
44
+ findBestStoryElementForClasses,
45
+ findFirstStoryText,
46
+ } from './story-tree-search';
47
+ import type { StoryBuilderContext } from './story-builder-context';
48
+
49
+ /**
50
+ * Top-level story-instance entrypoints. The createX functions try the
51
+ * symbol-instance path first (via tryCreateXComponentInstance) and fall
52
+ * back to a freshly-rendered frame when no master is available.
53
+ *
54
+ * `componentInstanceBackend` is the wiring object passed to those
55
+ * try-create functions; it bundles all the predicates / accessors they
56
+ * need to read state from a def and apply text overrides to instances.
57
+ *
58
+ * The buildStoryX helpers are used by preview-builder for the state matrix
59
+ * and responsive previews.
60
+ */
61
+
62
+ type CompoundTextOverrides = {
63
+ bySubcomponent: Record<string, string>;
64
+ bySlot: Record<string, string>;
65
+ };
66
+
67
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
68
+ function buildCompoundTextOverrides(def: any, story: any, instance?: any): CompoundTextOverrides {
69
+ const bySubcomponent: Record<string, string> = {};
70
+ const bySlot = extractCompoundSlotTextOverridesFromProps(def, instance && instance.props ? instance.props : {});
71
+ if (!def || def.type !== 'compound') return { bySubcomponent, bySlot };
72
+
73
+ const subComponents = Array.isArray(def.subComponents) ? def.subComponents : [];
74
+ const instances = (story && Array.isArray(story.instances)) ? story.instances : [];
75
+
76
+ for (let i = 0; i < subComponents.length; i++) {
77
+ const sub = subComponents[i];
78
+ const subName = normalizeComponentName(sub && sub.name ? sub.name : '');
79
+ const slotName = String(sub && sub.slot ? sub.slot : '').toLowerCase();
80
+ if (!subName) continue;
81
+ if (slotName && bySlot[slotName]) {
82
+ bySubcomponent[subName] = bySlot[slotName];
83
+ continue;
84
+ }
85
+
86
+ for (let j = 0; j < instances.length; j++) {
87
+ const instance = instances[j];
88
+ const instanceName = normalizeComponentName(instance && instance.componentName ? instance.componentName : '');
89
+ if (!instanceName || instanceName !== subName) continue;
90
+ const text = getInstanceChildrenText(instance);
91
+ if (!text) continue;
92
+ bySubcomponent[subName] = text;
93
+ if (slotName && !bySlot[slotName]) bySlot[slotName] = text;
94
+ break;
95
+ }
96
+ }
97
+
98
+ if (story && story.jsxTree) {
99
+ for (let i = 0; i < subComponents.length; i++) {
100
+ const sub = subComponents[i];
101
+ const subName = normalizeComponentName(sub && sub.name ? sub.name : '');
102
+ const slotName = String(sub && sub.slot ? sub.slot : '').toLowerCase();
103
+ const subClasses = Array.isArray(sub && sub.classes) ? sub.classes : [];
104
+ if (!subName || bySubcomponent[subName]) continue;
105
+ const match = findBestStoryElementForClasses(story.jsxTree, subClasses);
106
+ const text = findFirstStoryText(match);
107
+ if (!text) continue;
108
+ bySubcomponent[subName] = text;
109
+ if (slotName && !bySlot[slotName]) bySlot[slotName] = text;
110
+ }
111
+ }
112
+
113
+ return { bySubcomponent, bySlot };
114
+ }
115
+
116
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
117
+ function findFirstTextByAncestorPredicate(node: any, predicate: (name: string) => boolean, ancestry?: string[]): any | null {
118
+ if (!node) return null;
119
+ const trail = ancestry ? ancestry.slice() : [];
120
+ const nodeName = String(node.name || '');
121
+ if (nodeName) trail.push(nodeName);
122
+
123
+ if (node.type === 'TEXT') {
124
+ for (let i = 0; i < trail.length; i++) {
125
+ if (predicate(trail[i])) return node;
126
+ }
127
+ }
128
+
129
+ if (!Array.isArray(node.children)) return null;
130
+ for (let i = 0; i < node.children.length; i++) {
131
+ const found = findFirstTextByAncestorPredicate(node.children[i], predicate, trail);
132
+ if (found) return found;
133
+ }
134
+ return null;
135
+ }
136
+
137
+ function applyCompoundTextOverridesToInstance(
138
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
139
+ def: any,
140
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
141
+ story: any,
142
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
143
+ storyInstance: any,
144
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
145
+ instanceNode: any
146
+ ): { bySubcomponent: Record<string, string>; bySlot: Record<string, string> } {
147
+ const overrides = buildCompoundTextOverrides(def, story, storyInstance);
148
+ const appliedBySubcomponent: Record<string, string> = {};
149
+ const appliedBySlot: Record<string, string> = {};
150
+ const subComponents = Array.isArray(def && def.subComponents) ? def.subComponents : [];
151
+
152
+ for (let i = 0; i < subComponents.length; i++) {
153
+ const sub = subComponents[i];
154
+ const subNameRaw = String(sub && sub.name ? sub.name : '');
155
+ const subName = normalizeComponentName(subNameRaw);
156
+ const slotName = String(sub && sub.slot ? sub.slot : '').toLowerCase();
157
+ const value = overrides.bySubcomponent[subName] || (slotName ? overrides.bySlot[slotName] : '');
158
+ if (!value) continue;
159
+
160
+ let textNode = findFirstTextByAncestorPredicate(
161
+ instanceNode,
162
+ function(name: string): boolean {
163
+ return normalizeComponentName(name) === subName;
164
+ }
165
+ );
166
+
167
+ if (!textNode && slotName) {
168
+ textNode = findFirstTextByAncestorPredicate(
169
+ instanceNode,
170
+ function(name: string): boolean {
171
+ return normalizeComponentName(name).indexOf(normalizeComponentName(slotName)) !== -1;
172
+ }
173
+ );
174
+ }
175
+
176
+ if (!textNode) continue;
177
+ try {
178
+ textNode.characters = String(value);
179
+ if (subName) appliedBySubcomponent[subName] = String(value);
180
+ if (slotName) appliedBySlot[slotName] = String(value);
181
+ } catch (_error) {
182
+ // If override is blocked by component structure, keep default text.
183
+ }
184
+ }
185
+ return { bySubcomponent: appliedBySubcomponent, bySlot: appliedBySlot };
186
+ }
187
+
188
+ export const componentInstanceBackend = {
189
+ enableSymbolMasters: ENABLE_SYMBOL_MASTERS,
190
+ splitClassName,
191
+ ensureCvaComponentSet,
192
+ ensureStateComponentSet,
193
+ ensureNonCvaComponentMaster,
194
+ getInstantiableComponent,
195
+ getCvaSelectionFromInstance,
196
+ toFigmaVariantPropertyName,
197
+ toFigmaVariantPropertyValue,
198
+ getStateVariantForInstance,
199
+ isCheckedInstance,
200
+ hasCheckedStateVariant,
201
+ getInstanceTextOverride,
202
+ applyTextOverrideToInstance,
203
+ applyCompoundTextOverridesToInstance,
204
+ analyzeSymbolPolicy,
205
+ setGeneratedSymbolDebugData,
206
+ };
207
+
208
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
209
+ export function buildStatePreviewLabel(def: any, instance: any): string {
210
+ const props = instance && instance.props ? instance.props : {};
211
+ if (instance && instance.children) return String(instance.children);
212
+ if (props.children) return String(props.children);
213
+ if (props.placeholder) return String(props.placeholder);
214
+ if (props.defaultValue) return String(props.defaultValue);
215
+ return '';
216
+ }
217
+
218
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
219
+ export function buildStoryStateClasses(def: any, instance: any): string[] {
220
+ const props = instance && instance.props ? instance.props : {};
221
+ const classes: string[] = [];
222
+
223
+ if (def && def.baseClasses) {
224
+ for (let i = 0; i < def.baseClasses.length; i++) classes.push(def.baseClasses[i]);
225
+ }
226
+
227
+ if (def && def.type === 'cva') {
228
+ const variantKeys = Object.keys(def.variants || {});
229
+ for (let i = 0; i < variantKeys.length; i++) {
230
+ const key = variantKeys[i];
231
+ const value = props[key] || (def.defaultVariants && def.defaultVariants[key]) || (def.variants[key] && def.variants[key][0]);
232
+ if (value && def.variantClasses && def.variantClasses[key] && def.variantClasses[key][value]) {
233
+ const variantClasses = def.variantClasses[key][value];
234
+ for (let j = 0; j < variantClasses.length; j++) classes.push(variantClasses[j]);
235
+ }
236
+ }
237
+ }
238
+
239
+ const extra = splitClassName(props.className);
240
+ for (let i = 0; i < extra.length; i++) classes.push(extra[i]);
241
+ return classes;
242
+ }
243
+
244
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
245
+ export function buildStoryInstanceClasses(def: any, instance: any): string[] {
246
+ const props = instance && instance.props ? instance.props : {};
247
+ let classes: string[] = def && def.baseClasses ? def.baseClasses.slice() : [];
248
+
249
+ if (def && def.type === 'cva') {
250
+ const variantKeys = Object.keys(def.variants || {});
251
+ for (let i = 0; i < variantKeys.length; i++) {
252
+ const key = variantKeys[i];
253
+ const value = props[key]
254
+ || (def.defaultVariants && def.defaultVariants[key])
255
+ || (def.variants && def.variants[key] && def.variants[key][0]);
256
+ if (value && def.variantClasses && def.variantClasses[key] && def.variantClasses[key][value]) {
257
+ classes = classes.concat(def.variantClasses[key][value]);
258
+ }
259
+ }
260
+ }
261
+
262
+ const extra = splitClassName(props.className);
263
+ if (extra.length > 0) {
264
+ classes = classes.concat(extra);
265
+ }
266
+
267
+ return classes;
268
+ }
269
+
270
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
271
+ export function createCVAStoryInstance(def: any, instance: any, theme: string, ctx: StoryBuilderContext): any {
272
+ const componentInstance = tryCreateCvaComponentInstanceShared(def, instance, theme, ctx, componentInstanceBackend);
273
+ if (componentInstance) {
274
+ return componentInstance;
275
+ }
276
+
277
+ const themeContext = getThemeContext(theme);
278
+ const colorGroup = themeContext.colorGroup;
279
+ const radiusGroup = themeContext.radiusGroup;
280
+ const props = instance.props || {};
281
+
282
+ const selection = getCvaSelectionFromInstance(def, instance);
283
+ const classes = buildCvaClassesWithSelection(def, selection, props.className);
284
+
285
+ const comp = figma.createFrame();
286
+ comp.name = def.name;
287
+ comp.primaryAxisSizingMode = 'AUTO';
288
+ comp.counterAxisSizingMode = 'AUTO';
289
+ comp.counterAxisAlignItems = 'CENTER';
290
+ comp.primaryAxisAlignItems = 'CENTER';
291
+ comp.itemSpacing = 8;
292
+ comp.fills = [];
293
+ comp.strokes = [];
294
+ ctx.applyClipBehavior(comp, classes);
295
+
296
+ applyTailwindStylesToFrame(comp, classes, colorGroup, radiusGroup, theme);
297
+
298
+ const state = isTruthyVariantValue(props.disabled) ? 'disabled' : 'default';
299
+ const style = tailwindClassesToStyle(classes, state, colorGroup);
300
+ applyStyleToFrame(comp, style, theme);
301
+
302
+ const label = instance.children || props.children || def.name;
303
+ const typography = getPreviewTypography(classes);
304
+ const textColor = style.text ? parseColor(style.text) :
305
+ (colorGroup.foreground ? parseColor(colorGroup.foreground) : { r: 0, g: 0, b: 0 });
306
+ const text = createTextNode(label, { bold: typography.bold, fontSize: typography.fontSize, fill: textColor });
307
+ if (style.underline && text.textDecoration !== undefined) {
308
+ text.textDecoration = 'UNDERLINE';
309
+ }
310
+ comp.appendChild(text);
311
+ enforceFixedBoxSizingAfterLayout(comp, classes);
312
+ setGeneratedFallbackReason(comp, getCvaSymbolFallbackReason(def, instance));
313
+
314
+ return comp;
315
+ }
316
+
317
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
318
+ export function createStateStoryInstance(def: any, instance: any, theme: string, ctx: StoryBuilderContext, story?: any): any {
319
+ const symbolInstance = tryCreateNonCvaComponentInstanceShared(def, instance, theme, ctx, componentInstanceBackend, story);
320
+ if (symbolInstance) {
321
+ return symbolInstance;
322
+ }
323
+ return createStateStoryFrame(def, instance, theme, ctx, getNonCvaSymbolFallbackReason(def, instance), story);
324
+ }
325
+
326
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
327
+ export function createSimpleStoryInstance(def: any, instance: any, theme: string, ctx: StoryBuilderContext, story?: any): any {
328
+ const symbolInstance = tryCreateNonCvaComponentInstanceShared(def, instance, theme, ctx, componentInstanceBackend, story);
329
+ if (symbolInstance) {
330
+ return symbolInstance;
331
+ }
332
+ return createSimpleStoryFrame(def, instance, theme, ctx, getNonCvaSymbolFallbackReason(def, instance));
333
+ }
@@ -1,5 +1,5 @@
1
- import { type JsxNode, type JsxElement, splitClassName } from './node-ir';
2
- import { extractGridColumns, extractGridBreakpointWidth } from './width-solver';
1
+ import { type JsxNode, type JsxElement, splitClassName } from '../tailwind';
2
+ import { extractGridColumns, extractGridBreakpointWidth } from '../layout';
3
3
 
4
4
  export function extractGridColumnsFromTree(node: JsxNode | undefined): number | null {
5
5
  if (!node || node.type !== 'element') return null;
@@ -0,0 +1,150 @@
1
+ import { splitClassName, type JsxElement, type JsxNode } from '../tailwind';
2
+ import type { ComponentDef, ComponentStory } from '../components';
3
+ import { findMatchingInstance, normalizeComponentName } from './story-tree-search';
4
+ import { hasCheckedStateVariant } from './state-master';
5
+
6
+ /**
7
+ * Components that prefer instance rendering even when a JSX tree is available.
8
+ * `switch` lives here because the input element + indicator slot are hard to
9
+ * reconstruct correctly from the JSX tree alone — the symbol master is more
10
+ * reliable than the tree expansion.
11
+ */
12
+ export const INSTANCE_FALLBACK_COMPONENTS = new Set(['switch']);
13
+
14
+ // Tree-rendering budget. Stories that exceed any of these limits fall back to
15
+ // symbol-instance rendering. Tuned empirically so the realistic component
16
+ // catalogue still renders as a tree, but pathological deep stories (giant
17
+ // marketing pages, accidentally-recursive trees) skip the tree path and render
18
+ // as a labelled instance frame instead.
19
+ const STORY_JSX_NODE_LIMIT = 320;
20
+ const STORY_JSX_CLASS_TOKEN_LIMIT = 1800;
21
+ const STORY_JSX_DEPTH_LIMIT = 36;
22
+
23
+ /**
24
+ * Walks the JSX tree once, counting nodes, max depth, and total class tokens.
25
+ * Returns true as soon as any limit is exceeded. Pure data — the only side
26
+ * effect is the early return.
27
+ */
28
+ export function exceedsStoryJsxComplexityLimits(tree: JsxNode | null | undefined): boolean {
29
+ if (!tree || typeof tree !== 'object') return false;
30
+ let nodeCount = 0;
31
+ let classTokenCount = 0;
32
+ let maxDepth = 0;
33
+ const stack: Array<{ node: JsxNode; depth: number }> = [{ node: tree, depth: 0 }];
34
+
35
+ while (stack.length > 0) {
36
+ const current = stack.pop();
37
+ if (!current || !current.node) continue;
38
+
39
+ nodeCount += 1;
40
+ if (current.depth > maxDepth) maxDepth = current.depth;
41
+ if (nodeCount > STORY_JSX_NODE_LIMIT) return true;
42
+ if (maxDepth > STORY_JSX_DEPTH_LIMIT) return true;
43
+
44
+ if (current.node.type === 'element') {
45
+ const el = current.node as JsxElement;
46
+ const rawClassName = el.props ? el.props.className : '';
47
+ if (rawClassName) {
48
+ classTokenCount += splitClassName(String(rawClassName)).length;
49
+ if (classTokenCount > STORY_JSX_CLASS_TOKEN_LIMIT) return true;
50
+ }
51
+
52
+ const children = Array.isArray(el.children) ? el.children : [];
53
+ for (let i = 0; i < children.length; i++) {
54
+ stack.push({ node: children[i], depth: current.depth + 1 });
55
+ }
56
+ }
57
+ }
58
+
59
+ return false;
60
+ }
61
+
62
+ /**
63
+ * Returns true when story-tree rendering should be skipped in favour of the
64
+ * instance path. Two reasons:
65
+ * - The component is on the explicit instance-fallback list (e.g. switch).
66
+ * - The tree exceeds the complexity budget (`exceedsStoryJsxComplexityLimits`).
67
+ */
68
+ export function shouldSkipStoryJsxTree(def: ComponentDef, story: ComponentStory): boolean {
69
+ if (!story || !story.jsxTree) return false;
70
+ const defName = normalizeComponentName(def && def.name ? def.name : '');
71
+ if (defName && INSTANCE_FALLBACK_COMPONENTS.has(defName)) return true;
72
+ return exceedsStoryJsxComplexityLimits(story.jsxTree);
73
+ }
74
+
75
+ /**
76
+ * Returns true when instance rendering should be preferred even though a JSX
77
+ * tree exists. Used by CVA and (most) state components, which represent
78
+ * variant matrices more compactly as instances than as expanded trees.
79
+ * Skipped when the story passes JSX children (manual content overrides),
80
+ * or when the def is on the explicit fallback list (those need true instance
81
+ * rendering, not the symbol-instance shortcut).
82
+ */
83
+ export function shouldPreferInstanceRendering(def: ComponentDef, story: ComponentStory): boolean {
84
+ if (!def || !story) return false;
85
+ const defKey = normalizeComponentName(def && def.name ? def.name : '');
86
+ if (!defKey) return false;
87
+ const storyInstances = Array.isArray(story.instances) ? story.instances : [];
88
+ if (storyInstances.length > 0) {
89
+ const rootKey = normalizeComponentName(storyInstances[0] && storyInstances[0].componentName ? storyInstances[0].componentName : '');
90
+ // Do not treat nested subcomponents as the root story preview source.
91
+ if (rootKey && rootKey !== defKey) return false;
92
+ }
93
+ const matched = findMatchingInstance(def, story);
94
+ if (!matched) return false;
95
+ const normalizedName = defKey;
96
+ if (INSTANCE_FALLBACK_COMPONENTS.has(normalizedName)) return false;
97
+ const matchedProps = matched && matched.props ? matched.props : null;
98
+ if (matchedProps && Array.isArray(matchedProps.__jsxChildren) && matchedProps.__jsxChildren.length > 0) {
99
+ return false;
100
+ }
101
+ const analysis = def && def.analysis ? def.analysis : def;
102
+ if (!analysis) return false;
103
+ const type = String(analysis.type || '').toLowerCase();
104
+ if (type === 'cva') return true;
105
+ if (type === 'state') {
106
+ // The scanner classifies a component as `state` based on the presence
107
+ // of a `data-[disabled]:` / `aria-invalid:` / `data-[state=…]:` modifier
108
+ // somewhere in its className. That's a good signal for input-shaped
109
+ // primitives (one element, a few state variants) but a false positive
110
+ // for compound primitives like Slider whose body is a track + thumb +
111
+ // indicator structure — the state-matrix renderer can't represent the
112
+ // geometry. If the story's JSX tree contains multiple nested
113
+ // `data-slot` markers, the component is structurally compound; fall
114
+ // back to tree rendering so the layout actually renders.
115
+ if (jsxTreeHasNestedDataSlots(story.jsxTree)) return false;
116
+ const childrenText = matched && matched.children != null ? String(matched.children).trim() : '';
117
+ if (childrenText !== '' && hasCheckedStateVariant(analysis)) {
118
+ return false;
119
+ }
120
+ return true;
121
+ }
122
+ // For simple/compound components, prefer story JSX tree rendering so layout/content
123
+ // fidelity is preserved (e.g. portal wrappers, centered hero content, nested sections).
124
+ // Nested atoms are still rendered as symbol instances by renderJsxTree.
125
+ return false;
126
+ }
127
+
128
+ /**
129
+ * True when the story's JSX tree has 2+ elements carrying a `data-slot`
130
+ * marker. A `state`-classified component with this shape is actually
131
+ * compound (Slider, Progress, future Tabs, …) and the state-matrix
132
+ * renderer can't produce a faithful preview from it.
133
+ */
134
+ export function jsxTreeHasNestedDataSlots(tree: JsxNode | null | undefined): boolean {
135
+ if (!tree || typeof tree !== 'object') return false;
136
+ let slotsFound = 0;
137
+ const stack: JsxNode[] = [tree];
138
+ while (stack.length > 0) {
139
+ const node = stack.pop();
140
+ if (!node || typeof node !== 'object' || node.type !== 'element') continue;
141
+ const el = node as JsxElement;
142
+ if (el.props && typeof el.props['data-slot'] === 'string' && el.props['data-slot'].length > 0) {
143
+ slotsFound += 1;
144
+ if (slotsFound >= 2) return true;
145
+ }
146
+ const children = Array.isArray(el.children) ? el.children : [];
147
+ for (let i = 0; i < children.length; i++) stack.push(children[i]);
148
+ }
149
+ return false;
150
+ }
@@ -0,0 +1,110 @@
1
+ import { splitClassName } from '../tailwind';
2
+
3
+ /**
4
+ * Pure tree-search and component-name helpers used across the design-system
5
+ * page builder. Kept in their own module so story-instance and the master
6
+ * builders can share them without re-importing from story-builder.ts (which
7
+ * would otherwise create cycles via cva-master / state-master callbacks).
8
+ *
9
+ * No file-level state, no Figma API calls — just JSX-tree traversal,
10
+ * class-set comparison, and name normalization.
11
+ */
12
+
13
+ export function normalizeComponentName(name: string | undefined): string {
14
+ return String(name || '').toLowerCase().replace(/-/g, '');
15
+ }
16
+
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
+ export function collectStoryJsxElements(node: any, out: any[]): void {
19
+ if (!node || node.type !== 'element') return;
20
+ out.push(node);
21
+ const children = Array.isArray(node.children) ? node.children : [];
22
+ for (let i = 0; i < children.length; i++) {
23
+ collectStoryJsxElements(children[i], out);
24
+ }
25
+ }
26
+
27
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
+ export function findFirstStoryText(node: any): string | null {
29
+ if (!node) return null;
30
+ if (node.type === 'text') {
31
+ const content = String(node.content || '').trim();
32
+ return content || null;
33
+ }
34
+ if (node.type !== 'element' || !Array.isArray(node.children)) return null;
35
+ for (let i = 0; i < node.children.length; i++) {
36
+ const value = findFirstStoryText(node.children[i]);
37
+ if (value) return value;
38
+ }
39
+ return null;
40
+ }
41
+
42
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
43
+ export function findBestStoryElementForClasses(jsxTree: any, classes: string[]): any | null {
44
+ if (!jsxTree || !Array.isArray(classes) || classes.length === 0) return null;
45
+ const targets = new Set(classes);
46
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
47
+ const elements: any[] = [];
48
+ collectStoryJsxElements(jsxTree, elements);
49
+
50
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
51
+ let best: any | null = null;
52
+ let bestOverlap = 0;
53
+ for (let i = 0; i < elements.length; i++) {
54
+ const el = elements[i];
55
+ const tokens = splitClassName(el && el.props ? el.props.className : '');
56
+ if (tokens.length === 0) continue;
57
+ let overlap = 0;
58
+ for (let j = 0; j < tokens.length; j++) {
59
+ if (targets.has(tokens[j])) overlap++;
60
+ }
61
+ if (overlap > bestOverlap) {
62
+ bestOverlap = overlap;
63
+ best = el;
64
+ }
65
+ }
66
+ if (!best || bestOverlap === 0) return null;
67
+ return best;
68
+ }
69
+
70
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
71
+ export function getStoriesWithJsxTree(def: any): any[] {
72
+ const stories = def && Array.isArray(def.stories) ? def.stories : [];
73
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
74
+ const out: any[] = [];
75
+ for (let i = 0; i < stories.length; i++) {
76
+ const story = stories[i];
77
+ if (story && story.jsxTree) out.push(story);
78
+ }
79
+ return out;
80
+ }
81
+
82
+ export function countClassOverlap(classes: string[], targetSet: Set<string>): number {
83
+ let overlap = 0;
84
+ for (let i = 0; i < classes.length; i++) {
85
+ if (targetSet.has(classes[i])) overlap++;
86
+ }
87
+ return overlap;
88
+ }
89
+
90
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
91
+ export function findMatchingInstance(def: any, story: any): any | null {
92
+ const instances = story && story.instances ? story.instances : [];
93
+ const defKey = normalizeComponentName(def && def.name);
94
+ for (let i = 0; i < instances.length; i++) {
95
+ const instance = instances[i];
96
+ if (!instance || !instance.componentName) continue;
97
+ if (normalizeComponentName(instance.componentName) === defKey) return instance;
98
+ }
99
+ return null;
100
+ }
101
+
102
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
103
+ export function findFirstMatchingInstance(def: any): any | null {
104
+ const stories = def && Array.isArray(def.stories) ? def.stories : [];
105
+ for (let i = 0; i < stories.length; i++) {
106
+ const instance = findMatchingInstance(def, stories[i]);
107
+ if (instance) return instance;
108
+ }
109
+ return null;
110
+ }
@@ -0,0 +1,89 @@
1
+ import { getNonDefaultSymbolPropReason, inferSafeTextOverridePropKeys } from '../components';
2
+
3
+ /**
4
+ * Helpers for resolving the text content that should override a symbol
5
+ * instance, plus the "why we can't use a symbol" reason strings the design
6
+ * system page records when it falls back to raw rendering.
7
+ *
8
+ * The cva-flavoured fallback reason (and the symbol-master warming pass)
9
+ * still live in story-builder.ts because they read CVA-master constants;
10
+ * they'll move out alongside the cva-master extraction.
11
+ */
12
+
13
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
+ export function getInstanceTextOverride(def: any, instance: any): string | null {
15
+ const props = instance && instance.props ? instance.props : {};
16
+ if (instance && instance.children != null && String(instance.children).trim() !== '') {
17
+ return String(instance.children);
18
+ }
19
+ if (props.children != null && String(props.children).trim() !== '') {
20
+ return String(props.children);
21
+ }
22
+ if (def && def.type !== 'compound') {
23
+ const inferredTextKeys = inferSafeTextOverridePropKeys(def);
24
+ for (let i = 0; i < inferredTextKeys.length; i++) {
25
+ const key = inferredTextKeys[i];
26
+ if (props[key] != null && String(props[key]).trim() !== '') {
27
+ return String(props[key]);
28
+ }
29
+ }
30
+ }
31
+ if (def && def.type === 'state') {
32
+ if (props.placeholder != null && String(props.placeholder).trim() !== '') {
33
+ return String(props.placeholder);
34
+ }
35
+ if (props.defaultValue != null && String(props.defaultValue).trim() !== '') {
36
+ return String(props.defaultValue);
37
+ }
38
+ }
39
+ return null;
40
+ }
41
+
42
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
43
+ export function getNonCvaSymbolFallbackReason(def: any, instance: any): string {
44
+ if (!def || def.type === 'cva') return 'non_cva_not_applicable';
45
+ if (def.symbolCandidate === false) return 'symbol_candidate_disabled';
46
+ const propReason = getNonDefaultSymbolPropReason(def, instance);
47
+ if (propReason) return propReason;
48
+ return 'master_or_instance_unavailable';
49
+ }
50
+
51
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
+ export function findFirstTextDescendant(node: any): any | null {
53
+ if (!node) return null;
54
+ if (node.type === 'TEXT') return node;
55
+ if (!Array.isArray(node.children)) return null;
56
+ for (let i = 0; i < node.children.length; i++) {
57
+ const found = findFirstTextDescendant(node.children[i]);
58
+ if (found) return found;
59
+ }
60
+ return null;
61
+ }
62
+
63
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
64
+ export function applyTextOverrideToInstance(instanceNode: any, value: string): boolean {
65
+ const text = String(value || '').trim();
66
+ if (!text) return false;
67
+ const textNode = findFirstTextDescendant(instanceNode);
68
+ if (!textNode) return false;
69
+ try {
70
+ textNode.characters = text;
71
+ return true;
72
+ } catch (_error) {
73
+ // If override is blocked by component structure, keep default text.
74
+ return false;
75
+ }
76
+ }
77
+
78
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
79
+ export function getInstanceChildrenText(instance: any): string | null {
80
+ if (!instance) return null;
81
+ if (instance.children != null && String(instance.children).trim() !== '') {
82
+ return String(instance.children);
83
+ }
84
+ const props = instance.props || {};
85
+ if (props.children != null && String(props.children).trim() !== '') {
86
+ return String(props.children);
87
+ }
88
+ return null;
89
+ }