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,738 @@
1
+ // External modules.
2
+ import { jsxTreeHasNestedDataSlots } from './story-render-strategy';
3
+ import {
4
+ applyTailwindStylesToFrame,
5
+ buildCvaClassesForVariant,
6
+ cloneJsxNodeForBreakpoint,
7
+ collectTreeClasses,
8
+ extractBreakpointsFromClasses,
9
+ extractRootGridColsFromTree,
10
+ extractStatesFromClasses,
11
+ getBreakpointLabel,
12
+ getClassesForBreakpoint,
13
+ getResponsivePreviewWidth,
14
+ hasSignificantResponsiveChanges,
15
+ isHiddenAtBreakpoint,
16
+ mergeStatesWithDefinition,
17
+ parseUtilityClass,
18
+ propagateChildSelectorClasses,
19
+ splitClassName,
20
+ tailwindClassesToStyle,
21
+ treeClassSignature,
22
+ treeHasResponsiveClasses,
23
+ uniqueClassSignature,
24
+ type JsxNode,
25
+ type JsxElement,
26
+ type StateInfo,
27
+ } from '../tailwind';
28
+ import { parseColor } from '../tokens';
29
+ import { createTextNode, type CreateTextOptions } from '../text';
30
+ import { getRingInfoFromClasses, markRingNode, applyRingIfPossible } from '../layout';
31
+ import type {
32
+ ComponentDef,
33
+ ComponentStory,
34
+ } from '../components/scanner-types';
35
+
36
+ // Sibling design-system modules.
37
+ import { defaultGridWidth, normalizeLayoutClasses } from './story-layout';
38
+ import { tagGeneratedSubtree } from './generated-node';
39
+ import { type StoryBuilderContext } from './story-builder-context';
40
+ import { renderStandaloneStory } from './story-builder';
41
+ import {
42
+ buildStatePreviewLabel,
43
+ buildStoryStateClasses,
44
+ buildStoryInstanceClasses,
45
+ createStateStoryInstance,
46
+ } from './story-instance';
47
+ import { findMatchingInstance } from './story-tree-search';
48
+ import { getThemeContext, BOARD_LAYOUT } from './theme-context';
49
+ import { resolvePortalAwareStoryTree } from './portal-handling';
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // State-preview helpers (private to this module)
53
+ // ---------------------------------------------------------------------------
54
+
55
+ function getStateEntry(states: StateInfo[], name: string): StateInfo | null {
56
+ for (let i = 0; i < states.length; i++) {
57
+ if (states[i].name === name) return states[i];
58
+ }
59
+ return null;
60
+ }
61
+
62
+ function getStateNames(states: StateInfo[]): string[] {
63
+ const names: string[] = ['default'];
64
+ for (let i = 0; i < states.length; i++) {
65
+ const name = states[i].name;
66
+ if (name === 'default') continue;
67
+ names.push(name);
68
+ }
69
+ return names;
70
+ }
71
+
72
+ function buildStateClasses(states: StateInfo[], name: string): string[] {
73
+ const defaultEntry = getStateEntry(states, 'default');
74
+ const baseClasses = defaultEntry ? defaultEntry.classes.slice() : [];
75
+ if (name === 'default') return baseClasses;
76
+ const entry = getStateEntry(states, name);
77
+ if (!entry) return baseClasses;
78
+ return baseClasses.concat(entry.classes);
79
+ }
80
+
81
+ function isVisualStateUtility(cls: string): boolean {
82
+ const atom = parseUtilityClass(cls);
83
+ const utility = atom.utility || cls;
84
+ if (!utility) return false;
85
+ if (utility.startsWith('text-')) return false;
86
+ if (utility.startsWith('font-')) return false;
87
+ if (utility === 'underline' || utility === 'no-underline') return false;
88
+ if (utility.startsWith('decoration-')) return false;
89
+
90
+ return (
91
+ utility.startsWith('bg-')
92
+ || utility.startsWith('border')
93
+ || utility.startsWith('ring')
94
+ || utility.startsWith('shadow')
95
+ || utility.startsWith('opacity-')
96
+ || utility.startsWith('outline')
97
+ || utility.startsWith('cursor-')
98
+ || utility.startsWith('pointer-events-')
99
+ || utility.startsWith('scale-')
100
+ || utility.startsWith('translate-')
101
+ || utility.startsWith('rotate-')
102
+ || utility.startsWith('skew-')
103
+ );
104
+ }
105
+
106
+ function shouldSkipStatePreview(def: ComponentDef): boolean {
107
+ if (!def || def.type !== 'state') return false;
108
+ const states = def.states || {};
109
+ const names = Object.keys(states).filter(name => name !== 'default');
110
+ if (names.length === 0) return true;
111
+
112
+ let hasVisualState = false;
113
+ for (let i = 0; i < names.length; i++) {
114
+ const state = states[names[i]];
115
+ const classes = state && state.classes ? state.classes : [];
116
+ for (let j = 0; j < classes.length; j++) {
117
+ if (isVisualStateUtility(classes[j])) {
118
+ hasVisualState = true;
119
+ break;
120
+ }
121
+ }
122
+ if (hasVisualState) break;
123
+ }
124
+ if (hasVisualState) return false;
125
+
126
+ const stories = def.stories || [];
127
+ for (let i = 0; i < stories.length; i++) {
128
+ const tree = stories[i] && stories[i].jsxTree ? stories[i].jsxTree : null;
129
+ if (!tree || tree.type !== 'element') continue;
130
+ const tagName = String((tree as JsxElement).tagName || '').toLowerCase();
131
+ if (
132
+ tagName === 'div'
133
+ || tagName === 'section'
134
+ || tagName === 'header'
135
+ || tagName === 'footer'
136
+ || tagName === 'main'
137
+ || tagName === 'article'
138
+ || tagName === 'nav'
139
+ ) {
140
+ return true;
141
+ }
142
+ }
143
+ return false;
144
+ }
145
+
146
+ import { getPreviewTypography } from './typography';
147
+ export { getPreviewTypography };
148
+
149
+ export function measureTextWidth(label: string, options?: CreateTextOptions): number {
150
+ const node = createTextNode(label, options);
151
+ const width = node.width;
152
+ try {
153
+ node.remove();
154
+ } catch (_err) {
155
+ // ignore
156
+ }
157
+ return width;
158
+ }
159
+
160
+ export function createPreviewLabelNode(label: string, classes: string[], colorGroup: Record<string, string>): TextNode {
161
+ const style = tailwindClassesToStyle(classes, 'default', colorGroup);
162
+ const textColor = style.text ? parseColor(style.text) :
163
+ (colorGroup.foreground ? parseColor(colorGroup.foreground) : { r: 0, g: 0, b: 0 });
164
+ const typography = getPreviewTypography(classes);
165
+ const text = createTextNode(label, { bold: typography.bold, fontSize: typography.fontSize, fill: textColor });
166
+ if (style.underline && text.textDecoration !== undefined) {
167
+ text.textDecoration = 'UNDERLINE';
168
+ }
169
+ return text;
170
+ }
171
+
172
+ // ---------------------------------------------------------------------------
173
+ // Public: state preview block
174
+ // ---------------------------------------------------------------------------
175
+
176
+ export function createStatePreviewBlock(
177
+ def: ComponentDef,
178
+ story: ComponentStory,
179
+ theme: string,
180
+ ctx: StoryBuilderContext
181
+ ): FrameNode | null {
182
+ if (!def || (def.type !== 'cva' && def.type !== 'state')) return null;
183
+ if (shouldSkipStatePreview(def)) return null;
184
+ const themeContext = getThemeContext(theme);
185
+ const colorGroup = themeContext.colorGroup;
186
+ const radiusGroup = themeContext.radiusGroup;
187
+ const instance = findMatchingInstance(def, story);
188
+ const props = instance && instance.props ? instance.props : {};
189
+
190
+ const matrixTitleStyle = { fontSize: 18, bold: true, opacity: 0.95 };
191
+ const axisLabelStyle = { fontSize: 13, bold: true, opacity: 0.78 };
192
+ const itemLabelStyle = { fontSize: 13, bold: true, opacity: 0.72 };
193
+
194
+ const label = buildStatePreviewLabel(def, instance);
195
+
196
+ function createStateCell(stateName: string, classes: string[], columnLabel: string): FrameNode {
197
+ if (String(def && def.type ? def.type : '').toLowerCase() === 'state') {
198
+ const baseProps = Object.assign({}, props || {});
199
+ const originalExtraClasses = splitClassName(props && props.className);
200
+ delete baseProps.className;
201
+ delete baseProps.state;
202
+ delete baseProps['data-state'];
203
+ delete baseProps.disabled;
204
+ delete baseProps['aria-disabled'];
205
+ delete baseProps['aria-invalid'];
206
+ delete baseProps.checked;
207
+ delete baseProps.defaultChecked;
208
+ delete baseProps['aria-checked'];
209
+
210
+ const normalizedState = String(stateName || 'default').trim().toLowerCase();
211
+ let visualState = normalizedState || 'default';
212
+ let checked = false;
213
+
214
+ if (visualState === 'checked') {
215
+ visualState = 'default';
216
+ checked = true;
217
+ }
218
+
219
+ // Keep only story-level extras (e.g. width wrappers) here.
220
+ // Visual state classes are applied in createStateStoryFrame via def.states.
221
+ if (originalExtraClasses.length > 0) {
222
+ baseProps.className = originalExtraClasses.join(' ');
223
+ }
224
+
225
+ if (checked) {
226
+ baseProps.checked = 'true';
227
+ }
228
+ if (visualState && visualState !== 'default') {
229
+ baseProps.state = visualState;
230
+ }
231
+ if (visualState === 'disabled') {
232
+ baseProps.disabled = 'true';
233
+ } else if (visualState === 'error') {
234
+ baseProps['aria-invalid'] = 'true';
235
+ } else if (visualState === 'open') {
236
+ baseProps['data-state'] = 'open';
237
+ }
238
+
239
+ const stateNode = createStateStoryInstance(
240
+ def,
241
+ { props: baseProps },
242
+ theme,
243
+ ctx,
244
+ story
245
+ );
246
+
247
+ const cell = figma.createFrame();
248
+ cell.name = def.name + '/' + columnLabel + '/' + stateName;
249
+ cell.layoutMode = 'HORIZONTAL';
250
+ cell.primaryAxisSizingMode = 'AUTO';
251
+ cell.counterAxisSizingMode = 'AUTO';
252
+ cell.primaryAxisAlignItems = 'CENTER';
253
+ cell.counterAxisAlignItems = 'CENTER';
254
+ cell.fills = [];
255
+ cell.strokes = [];
256
+ ctx.applyClipBehavior(cell, []);
257
+ cell.appendChild(stateNode);
258
+ return cell;
259
+ }
260
+
261
+ const comp = figma.createFrame();
262
+ comp.name = def.name + '/' + columnLabel + '/' + stateName;
263
+ comp.primaryAxisSizingMode = 'AUTO';
264
+ comp.counterAxisSizingMode = 'AUTO';
265
+ comp.counterAxisAlignItems = 'CENTER';
266
+ comp.primaryAxisAlignItems = 'CENTER';
267
+ comp.itemSpacing = 8;
268
+ comp.fills = [];
269
+ comp.strokes = [];
270
+ ctx.applyClipBehavior(comp, classes);
271
+
272
+ applyTailwindStylesToFrame(comp, classes, colorGroup, radiusGroup, theme);
273
+ if (label && String(label).trim() !== '') {
274
+ comp.appendChild(createPreviewLabelNode(label, classes, colorGroup));
275
+ }
276
+ // Append any icon-like JSX-element child from the matched instance so
277
+ // icon-only components (e.g. Toggle with `<Bold />`) show their icon in
278
+ // every matrix cell instead of being invisible (default variant) or
279
+ // empty-bordered (outline variant). Use the first element child as a
280
+ // representative sample — the matrix is a state × variant grid, not a
281
+ // story-by-story render, so a single icon across cells is fine.
282
+ const jsxChildren = props && Array.isArray((props as { __jsxChildren?: unknown }).__jsxChildren)
283
+ ? ((props as { __jsxChildren?: JsxNode[] }).__jsxChildren as JsxNode[])
284
+ : [];
285
+ for (let i = 0; i < jsxChildren.length; i++) {
286
+ const child = jsxChildren[i];
287
+ if (!child || (child as { type?: string }).type !== 'element') continue;
288
+ const iconNode = ctx.renderJsxTree(
289
+ child,
290
+ colorGroup,
291
+ radiusGroup,
292
+ theme,
293
+ 0,
294
+ { parentLayout: 'HORIZONTAL' }
295
+ );
296
+ if (iconNode) {
297
+ comp.appendChild(iconNode);
298
+ }
299
+ break; // one sample is enough
300
+ }
301
+ const ringInfo = getRingInfoFromClasses(classes, colorGroup);
302
+ if (ringInfo) markRingNode(comp, ringInfo);
303
+
304
+ const cell = figma.createFrame();
305
+ cell.name = def.name + '/' + columnLabel + '/' + stateName + '/Cell';
306
+ cell.layoutMode = 'HORIZONTAL';
307
+ cell.primaryAxisSizingMode = 'AUTO';
308
+ cell.counterAxisSizingMode = 'AUTO';
309
+ cell.primaryAxisAlignItems = 'CENTER';
310
+ cell.counterAxisAlignItems = 'CENTER';
311
+ cell.fills = [];
312
+ cell.strokes = [];
313
+ ctx.applyClipBehavior(cell, []);
314
+ cell.appendChild(comp);
315
+ // Comp is now in the cell — its dimensions are final, so we can safely
316
+ // apply the marked ring overlay. The host-FIXED-toggle inside
317
+ // applyRingIfPossible ensures appending the overlay can't inflate comp.
318
+ applyRingIfPossible(comp, cell);
319
+ return cell;
320
+ }
321
+
322
+ type VariantColumn = { label: string; states: StateInfo[] };
323
+ const columns: VariantColumn[] = [];
324
+
325
+ if (def.type === 'cva' && def.variants && Object.keys(def.variants).length > 0) {
326
+ const variantKeys = Object.keys(def.variants);
327
+ const primaryKey = def.variants.variant ? 'variant' : variantKeys[0];
328
+ const primaryValues = def.variants[primaryKey] || [];
329
+ for (let i = 0; i < primaryValues.length; i++) {
330
+ const value = primaryValues[i];
331
+ const classes = buildCvaClassesForVariant(def, props, primaryKey, value);
332
+ const states = mergeStatesWithDefinition(extractStatesFromClasses(classes), def);
333
+ const labelText = value ? value.charAt(0).toUpperCase() + value.slice(1) : '';
334
+ columns.push({ label: labelText, states: states });
335
+ }
336
+ } else {
337
+ const classes = buildStoryStateClasses(def, instance);
338
+ if (classes.length === 0) return null;
339
+ const states = mergeStatesWithDefinition(extractStatesFromClasses(classes), def);
340
+ columns.push({ label: def.name || 'Component', states: states });
341
+ }
342
+
343
+ if (columns.length === 0) return null;
344
+
345
+ const stateNames: string[] = columns.length > 0 ? getStateNames(columns[0].states) : [];
346
+ for (let i = 1; i < columns.length; i++) {
347
+ const names = getStateNames(columns[i].states);
348
+ for (let j = 0; j < names.length; j++) {
349
+ const name = names[j];
350
+ if (stateNames.indexOf(name) === -1) {
351
+ stateNames.push(name);
352
+ }
353
+ }
354
+ }
355
+ if (stateNames.length <= 1) return null;
356
+
357
+ const block = figma.createFrame();
358
+ block.name = 'States';
359
+ block.layoutMode = 'VERTICAL';
360
+ block.itemSpacing = BOARD_LAYOUT.stateMatrixGap;
361
+ block.primaryAxisSizingMode = 'AUTO';
362
+ block.counterAxisSizingMode = 'AUTO';
363
+ block.fills = [];
364
+ ctx.applyClipBehavior(block, []);
365
+ block.appendChild(createTextNode('State Matrix', matrixTitleStyle));
366
+
367
+ const cellMatrix: FrameNode[][] = [];
368
+ const columnWidths: number[] = [];
369
+ for (let ci = 0; ci < columns.length; ci++) {
370
+ const column = columns[ci];
371
+ let maxWidth = measureTextWidth(column.label, itemLabelStyle);
372
+ const cells: FrameNode[] = [];
373
+ for (let si = 0; si < stateNames.length; si++) {
374
+ const stateName = stateNames[si];
375
+ const classes = buildStateClasses(column.states, stateName);
376
+ const cell = createStateCell(stateName, classes, column.label || def.name);
377
+ if (cell.width > maxWidth) maxWidth = cell.width;
378
+ cells.push(cell);
379
+ }
380
+ cellMatrix.push(cells);
381
+ columnWidths.push(maxWidth);
382
+ }
383
+
384
+ let legendWidth = measureTextWidth('State', itemLabelStyle);
385
+ for (let i = 0; i < stateNames.length; i++) {
386
+ const width = measureTextWidth(stateNames[i], itemLabelStyle);
387
+ if (width > legendWidth) legendWidth = width;
388
+ }
389
+ legendWidth += 24;
390
+
391
+ function createLegendCell(textValue: string, style: CreateTextOptions): FrameNode {
392
+ const cell = figma.createFrame();
393
+ cell.layoutMode = 'VERTICAL';
394
+ cell.primaryAxisSizingMode = 'AUTO';
395
+ cell.counterAxisSizingMode = 'AUTO';
396
+ cell.fills = [];
397
+ cell.appendChild(createTextNode(textValue, style));
398
+ try {
399
+ cell.resize(legendWidth, cell.height);
400
+ cell.counterAxisSizingMode = 'FIXED';
401
+ } catch (_err) {
402
+ // ignore
403
+ }
404
+ return cell;
405
+ }
406
+
407
+ const axesRow = figma.createFrame();
408
+ axesRow.name = 'State Axes';
409
+ axesRow.layoutMode = 'HORIZONTAL';
410
+ axesRow.itemSpacing = BOARD_LAYOUT.stateMatrixAxisGap;
411
+ axesRow.primaryAxisSizingMode = 'AUTO';
412
+ axesRow.counterAxisSizingMode = 'AUTO';
413
+ axesRow.fills = [];
414
+ ctx.applyClipBehavior(axesRow, []);
415
+ axesRow.appendChild(createLegendCell('States', axisLabelStyle));
416
+ axesRow.appendChild(createTextNode('Variants', axisLabelStyle));
417
+ block.appendChild(axesRow);
418
+
419
+ const table = figma.createFrame();
420
+ table.name = 'State Table';
421
+ table.layoutMode = 'VERTICAL';
422
+ table.itemSpacing = BOARD_LAYOUT.stateMatrixGap;
423
+ table.primaryAxisSizingMode = 'AUTO';
424
+ table.counterAxisSizingMode = 'AUTO';
425
+ table.fills = [];
426
+ ctx.applyClipBehavior(table, []);
427
+
428
+ const tableHeader = figma.createFrame();
429
+ tableHeader.name = 'State Table Header';
430
+ tableHeader.layoutMode = 'HORIZONTAL';
431
+ tableHeader.itemSpacing = BOARD_LAYOUT.stateMatrixAxisGap;
432
+ tableHeader.primaryAxisSizingMode = 'AUTO';
433
+ tableHeader.counterAxisSizingMode = 'AUTO';
434
+ tableHeader.fills = [];
435
+ tableHeader.appendChild(createLegendCell('State', itemLabelStyle));
436
+ for (let ci = 0; ci < columns.length; ci++) {
437
+ const headerCell = figma.createFrame();
438
+ headerCell.layoutMode = 'VERTICAL';
439
+ headerCell.primaryAxisSizingMode = 'AUTO';
440
+ headerCell.counterAxisSizingMode = 'AUTO';
441
+ headerCell.fills = [];
442
+ headerCell.appendChild(createTextNode(columns[ci].label, itemLabelStyle));
443
+ try {
444
+ headerCell.resize(columnWidths[ci], headerCell.height);
445
+ headerCell.counterAxisSizingMode = 'FIXED';
446
+ } catch (_err) {
447
+ // ignore
448
+ }
449
+ tableHeader.appendChild(headerCell);
450
+ }
451
+ table.appendChild(tableHeader);
452
+
453
+ for (let si = 0; si < stateNames.length; si++) {
454
+ const row = figma.createFrame();
455
+ row.name = 'State Row/' + stateNames[si];
456
+ row.layoutMode = 'HORIZONTAL';
457
+ row.itemSpacing = BOARD_LAYOUT.stateMatrixAxisGap;
458
+ row.primaryAxisSizingMode = 'AUTO';
459
+ row.counterAxisSizingMode = 'AUTO';
460
+ row.counterAxisAlignItems = 'CENTER';
461
+ row.fills = [];
462
+ ctx.applyClipBehavior(row, []);
463
+ row.appendChild(createLegendCell(stateNames[si], itemLabelStyle));
464
+ for (let ci = 0; ci < columns.length; ci++) {
465
+ const cell = cellMatrix[ci][si];
466
+ try {
467
+ cell.resize(columnWidths[ci], cell.height);
468
+ if (cell.layoutMode === 'HORIZONTAL') {
469
+ cell.primaryAxisSizingMode = 'FIXED';
470
+ } else if (cell.layoutMode === 'VERTICAL') {
471
+ cell.counterAxisSizingMode = 'FIXED';
472
+ }
473
+ } catch (_err) {
474
+ // ignore
475
+ }
476
+ row.appendChild(cell);
477
+ }
478
+ table.appendChild(row);
479
+ }
480
+
481
+ block.appendChild(table);
482
+ tagGeneratedSubtree(block, 'state-preview-block:' + def.name + ':' + (story && story.name ? story.name : 'default'));
483
+ return block;
484
+ }
485
+
486
+ // ---------------------------------------------------------------------------
487
+ // Public: responsive preview block
488
+ // ---------------------------------------------------------------------------
489
+
490
+ export function createResponsivePreviewBlock(
491
+ def: ComponentDef,
492
+ story: ComponentStory,
493
+ theme: string,
494
+ ctx: StoryBuilderContext
495
+ ): FrameNode | null {
496
+ if (!def) return null;
497
+ const instance = findMatchingInstance(def, story);
498
+ const layoutClasses = normalizeLayoutClasses(story && story.layoutClasses);
499
+ const layoutHasResponsive = hasSignificantResponsiveChanges(layoutClasses);
500
+ // Use the same portal-filtering logic as main rendering so trigger-only stories
501
+ // don't pick up sm: classes from portal content (e.g. Dialog's sm:max-w-lg).
502
+ const effectiveJsxTree = story && story.jsxTree
503
+ ? resolvePortalAwareStoryTree(story.jsxTree)
504
+ : null;
505
+ const treeHasResponsive = effectiveJsxTree ? treeHasResponsiveClasses(effectiveJsxTree as JsxNode) : false;
506
+ const instanceClasses = buildStoryInstanceClasses(def, instance);
507
+ const instanceHasResponsive = hasSignificantResponsiveChanges(instanceClasses);
508
+ if (!layoutHasResponsive && !treeHasResponsive && !instanceHasResponsive) return null;
509
+
510
+ const sourceClasses: string[] = [];
511
+ if (layoutHasResponsive && layoutClasses.length > 0) {
512
+ for (let i = 0; i < layoutClasses.length; i++) sourceClasses.push(layoutClasses[i]);
513
+ }
514
+ if (treeHasResponsive && effectiveJsxTree) {
515
+ collectTreeClasses(effectiveJsxTree as JsxNode, sourceClasses);
516
+ }
517
+ if (sourceClasses.length === 0) {
518
+ for (let i = 0; i < instanceClasses.length; i++) sourceClasses.push(instanceClasses[i]);
519
+ }
520
+ const breakpoints = extractBreakpointsFromClasses(sourceClasses);
521
+ if (breakpoints.length <= 1) return null;
522
+ const entries: Array<{
523
+ bp: { name: string; minWidth: number };
524
+ layoutOverride: string[];
525
+ treeOverride: JsxNode | null;
526
+ instanceOverride: string[];
527
+ signature: string;
528
+ }> = [];
529
+ for (let i = 0; i < breakpoints.length; i++) {
530
+ const bp = breakpoints[i];
531
+ const layoutOverride = layoutClasses.length > 0 ? getClassesForBreakpoint(layoutClasses, bp.name) : layoutClasses;
532
+ let treeOverride: JsxNode | null = effectiveJsxTree ?? null;
533
+ if (treeHasResponsive && effectiveJsxTree) {
534
+ // Clone for this breakpoint, then propagate *:X child-wildcard classes so
535
+ // the renderer sees explicit per-element classes (e.g. *:w-full on a flex-col
536
+ // footer → w-full on each button at base, *:w-auto → w-auto at sm+).
537
+ // Propagation runs here — after breakpoint mapping — so the main story render
538
+ // path is unaffected and keeps its original auto-sizing behaviour.
539
+ treeOverride = propagateChildSelectorClasses(
540
+ cloneJsxNodeForBreakpoint(effectiveJsxTree as JsxNode, bp.name)
541
+ );
542
+ }
543
+ const instanceOverride = getClassesForBreakpoint(instanceClasses, bp.name);
544
+ let signature = 'I:' + uniqueClassSignature(instanceOverride);
545
+ if (layoutHasResponsive || treeHasResponsive) {
546
+ signature = 'L:' + uniqueClassSignature(layoutOverride) + '|T:' + treeClassSignature(treeOverride as JsxNode);
547
+ }
548
+ entries.push({
549
+ bp: { name: bp.name, minWidth: bp.minWidth },
550
+ layoutOverride: layoutOverride,
551
+ treeOverride: treeOverride,
552
+ instanceOverride: instanceOverride,
553
+ signature: signature,
554
+ });
555
+ }
556
+ if (entries.length <= 1) return null;
557
+ // Drop breakpoints where the root element resolves to `display: none` at
558
+ // that breakpoint (e.g. `md:hidden` hides the whole component at md+). Walk
559
+ // the leading single-child spine so a decorator-wrapped component still
560
+ // exposes the inner root's visibility intent; deep-child `hidden` on a
561
+ // sibling branch is ignored.
562
+ const rootClassSet: string[] = [];
563
+ let spineNode: JsxNode | null | undefined = effectiveJsxTree;
564
+ let spineDepth = 0;
565
+ while (spineNode && spineNode.type === 'element' && spineDepth < 4) {
566
+ const el = spineNode as JsxElement;
567
+ const cls = el.props && el.props.className;
568
+ if (typeof cls === 'string') {
569
+ const list = splitClassName(cls);
570
+ for (let i = 0; i < list.length; i++) rootClassSet.push(list[i]);
571
+ }
572
+ const children = el.children || [];
573
+ if (children.length !== 1 || !children[0] || children[0].type !== 'element') break;
574
+ spineNode = children[0];
575
+ spineDepth++;
576
+ }
577
+ for (let i = 0; i < layoutClasses.length; i++) rootClassSet.push(layoutClasses[i]);
578
+ const visibleEntries = rootClassSet.length > 0
579
+ ? entries.filter(e => !isHiddenAtBreakpoint(rootClassSet, e.bp.name))
580
+ : entries;
581
+ if (visibleEntries.length <= 1) return null;
582
+ const filteredEntries: typeof entries = [];
583
+ const baseSignature = visibleEntries[0].signature;
584
+ const seenSignatures: Record<string, boolean> = {};
585
+ seenSignatures[baseSignature] = true;
586
+ filteredEntries.push(visibleEntries[0]);
587
+ for (let i = 1; i < visibleEntries.length; i++) {
588
+ const entry = visibleEntries[i];
589
+ if (entry.signature === baseSignature) continue;
590
+ if (seenSignatures[entry.signature]) continue;
591
+ seenSignatures[entry.signature] = true;
592
+ filteredEntries.push(entry);
593
+ }
594
+ if (filteredEntries.length <= 1) return null;
595
+
596
+ const themeContext = getThemeContext(theme);
597
+ const colorGroup = themeContext.colorGroup;
598
+ const radiusGroup = themeContext.radiusGroup;
599
+ const label = buildStatePreviewLabel(def, instance);
600
+
601
+ const block = figma.createFrame();
602
+ block.name = 'Responsive';
603
+ block.layoutMode = 'VERTICAL';
604
+ block.itemSpacing = BOARD_LAYOUT.responsiveBlockGap;
605
+ block.primaryAxisSizingMode = 'AUTO';
606
+ block.counterAxisSizingMode = 'AUTO';
607
+ block.fills = [];
608
+ ctx.applyClipBehavior(block, []);
609
+
610
+ block.appendChild(createTextNode('Responsive', { fontSize: 16, lineHeight: 22, opacity: 0.6 }));
611
+
612
+ const row = figma.createFrame();
613
+ row.name = 'Responsive Strip';
614
+ row.layoutMode = 'HORIZONTAL';
615
+ row.itemSpacing = BOARD_LAYOUT.responsiveColumnGap;
616
+ row.primaryAxisSizingMode = 'AUTO';
617
+ row.counterAxisSizingMode = 'AUTO';
618
+ row.fills = [];
619
+ ctx.applyClipBehavior(row, []);
620
+
621
+ for (let i = 0; i < filteredEntries.length; i++) {
622
+ const entry = filteredEntries[i];
623
+ const bp = entry.bp;
624
+ const viewportWidth = getResponsivePreviewWidth(bp.name, bp.minWidth);
625
+
626
+ const col = figma.createFrame();
627
+ col.name = getBreakpointLabel(bp.name, bp.minWidth);
628
+ col.layoutMode = 'VERTICAL';
629
+ col.itemSpacing = BOARD_LAYOUT.responsiveLabelGap;
630
+ col.primaryAxisSizingMode = 'AUTO';
631
+ col.counterAxisSizingMode = 'AUTO';
632
+ col.fills = [];
633
+
634
+ col.appendChild(createTextNode(getBreakpointLabel(bp.name, bp.minWidth), { fontSize: 12, bold: true, opacity: 0.6 }));
635
+ const viewport = figma.createFrame();
636
+ viewport.name = 'Viewport';
637
+ viewport.layoutMode = 'VERTICAL';
638
+ viewport.itemSpacing = 0;
639
+ viewport.primaryAxisSizingMode = 'AUTO';
640
+ viewport.counterAxisSizingMode = 'FIXED';
641
+ viewport.fills = [];
642
+ viewport.strokes = [];
643
+ // Keep responsive previews fully visible vertically; clipping here caused
644
+ // taller panels (e.g. Dialog OpenPanel/Confirm) to appear truncated.
645
+ viewport.clipsContent = false;
646
+ viewport.resize(viewportWidth, viewport.height);
647
+
648
+ if (layoutHasResponsive || treeHasResponsive) {
649
+ const layoutOverride = entry.layoutOverride;
650
+ const treeOverride = entry.treeOverride;
651
+ const storyOverride = Object.assign({}, story, { layoutClasses: layoutOverride, jsxTree: treeOverride });
652
+ const preview = renderStandaloneStory(storyOverride, theme, ctx, viewportWidth);
653
+ if (preview) {
654
+ const colsOverride = treeOverride ? extractRootGridColsFromTree(treeOverride as JsxNode) : null;
655
+ // The `cols > 1` part of the gate now lives in
656
+ // `applyGridColumnsIfPossible`, so a `cols === 1` forward is a
657
+ // safe no-op. Keep the `preview.children.length === 1` check —
658
+ // that's preview-builder-specific (only reflow when the
659
+ // standalone-story render produced a single root frame to
660
+ // re-grid).
661
+ if (colsOverride != null && preview.children.length === 1) {
662
+ const target = preview.children[0];
663
+ if ('layoutMode' in target) {
664
+ const previewPadding = (preview.paddingLeft || 0) + (preview.paddingRight || 0);
665
+ const minWidth = defaultGridWidth(colsOverride);
666
+ let previewWidth = Number.isFinite(preview.width) ? preview.width - previewPadding : undefined;
667
+ if (!Number.isFinite(previewWidth) || (previewWidth != null && previewWidth < minWidth)) {
668
+ previewWidth = minWidth;
669
+ }
670
+ ctx.applyGridColumnsWithReflow(target as FrameNode, previewWidth, colsOverride);
671
+ }
672
+ }
673
+ if ('layoutAlign' in preview) {
674
+ preview.layoutAlign = 'STRETCH';
675
+ }
676
+ viewport.appendChild(preview);
677
+ }
678
+ } else {
679
+ const bpClasses = entry.instanceOverride;
680
+ const comp = figma.createFrame();
681
+ comp.name = def.name + '/' + bp.name;
682
+ comp.primaryAxisSizingMode = 'AUTO';
683
+ comp.counterAxisSizingMode = 'AUTO';
684
+ comp.counterAxisAlignItems = 'CENTER';
685
+ comp.primaryAxisAlignItems = 'CENTER';
686
+ comp.itemSpacing = 8;
687
+ comp.fills = [];
688
+ comp.strokes = [];
689
+ ctx.applyClipBehavior(comp, bpClasses);
690
+
691
+ applyTailwindStylesToFrame(comp, bpClasses, colorGroup, radiusGroup, theme);
692
+ comp.appendChild(createPreviewLabelNode(label, bpClasses, colorGroup));
693
+
694
+ if ('layoutAlign' in comp) {
695
+ comp.layoutAlign = 'STRETCH';
696
+ }
697
+ viewport.appendChild(comp);
698
+ }
699
+ col.appendChild(viewport);
700
+ row.appendChild(col);
701
+ }
702
+
703
+ block.appendChild(row);
704
+ tagGeneratedSubtree(block, 'responsive-preview-block:' + def.name + ':' + (story && story.name ? story.name : 'default'));
705
+ return block;
706
+ }
707
+
708
+ // ---------------------------------------------------------------------------
709
+ // Public: gate functions (decide whether to render previews)
710
+ // ---------------------------------------------------------------------------
711
+
712
+ export function shouldRenderStatesForStory(def: ComponentDef, story: ComponentStory, storyIndex: number): boolean {
713
+ // Slider / Progress / future Tabs are mis-classified as `type:'state'` by
714
+ // the scanner but are structurally compound — the State Matrix renderer
715
+ // (createStateStoryFrame) can't reconstruct their geometry and would
716
+ // emit empty rows ("default" / "focus" / "disabled" with nothing in
717
+ // them). The same nested-data-slot signal we use to gate the
718
+ // tree-vs-instance choice also gates this preview block.
719
+ if (story && jsxTreeHasNestedDataSlots(story.jsxTree as JsxNode)) {
720
+ return false;
721
+ }
722
+ const name = String(story && story.name ? story.name : '').toLowerCase();
723
+ if (name && (name === 'default' || name.indexOf('default') !== -1)) {
724
+ return true;
725
+ }
726
+ const stories = def && def.stories ? def.stories : [];
727
+ let hasDefault = false;
728
+ for (let i = 0; i < stories.length; i++) {
729
+ const storyName = String(stories[i] && stories[i].name ? stories[i].name : '').toLowerCase();
730
+ if (storyName && (storyName === 'default' || storyName.indexOf('default') !== -1)) {
731
+ hasDefault = true;
732
+ break;
733
+ }
734
+ }
735
+ if (hasDefault) return false;
736
+ return storyIndex === 0;
737
+ }
738
+