inkbridge 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +201 -0
  2. package/bin/inkhouse.mjs +171 -0
  3. package/code.js +11802 -0
  4. package/manifest.json +30 -0
  5. package/package.json +45 -0
  6. package/scanner/blob-placement-regression.ts +132 -0
  7. package/scanner/class-collector.ts +69 -0
  8. package/scanner/cli.ts +336 -0
  9. package/scanner/component-scanner.ts +2876 -0
  10. package/scanner/css-patch-regression.ts +112 -0
  11. package/scanner/css-token-reader-regression.ts +92 -0
  12. package/scanner/css-token-reader.ts +477 -0
  13. package/scanner/font-style-resolver-regression.ts +32 -0
  14. package/scanner/index.ts +9 -0
  15. package/scanner/radial-gradient-regression.ts +53 -0
  16. package/scanner/style-map.ts +149 -0
  17. package/scanner/tailwind-parser.ts +644 -0
  18. package/scanner/transform-math-regression.ts +42 -0
  19. package/scanner/types.ts +298 -0
  20. package/src/blob-placement.ts +111 -0
  21. package/src/change-detection.ts +204 -0
  22. package/src/class-utils.ts +105 -0
  23. package/src/clip-path-decorative.ts +194 -0
  24. package/src/color-resolver.ts +98 -0
  25. package/src/colors.ts +196 -0
  26. package/src/component-defs.ts +54 -0
  27. package/src/component-gen.ts +561 -0
  28. package/src/component-lookup.ts +82 -0
  29. package/src/config.ts +115 -0
  30. package/src/design-system.ts +59 -0
  31. package/src/dev-server.ts +173 -0
  32. package/src/figma-globals.d.ts +3 -0
  33. package/src/font-style-resolver.ts +171 -0
  34. package/src/github.ts +1465 -0
  35. package/src/icon-builder.ts +607 -0
  36. package/src/image-cache.ts +22 -0
  37. package/src/inline-text.ts +271 -0
  38. package/src/layout-parser.ts +667 -0
  39. package/src/layout-utils.ts +155 -0
  40. package/src/main.ts +687 -0
  41. package/src/node-ir.ts +595 -0
  42. package/src/pack-provider.ts +148 -0
  43. package/src/packs.ts +126 -0
  44. package/src/radial-gradient.ts +84 -0
  45. package/src/render-context.ts +138 -0
  46. package/src/responsive-analyzer.ts +139 -0
  47. package/src/state-analyzer.ts +143 -0
  48. package/src/story-builder.ts +1706 -0
  49. package/src/story-layout.ts +38 -0
  50. package/src/tailwind.ts +2379 -0
  51. package/src/text-builder.ts +116 -0
  52. package/src/text-line.ts +42 -0
  53. package/src/token-source.ts +43 -0
  54. package/src/tokens.ts +717 -0
  55. package/src/transform-math.ts +44 -0
  56. package/src/ui-builder.ts +1996 -0
  57. package/src/utility-resolver.ts +125 -0
  58. package/src/variables.ts +1042 -0
  59. package/src/width-solver.ts +466 -0
  60. package/templates/patch-tokens-route.ts +165 -0
  61. package/templates/scan-components-route.ts +57 -0
  62. package/ui.html +1222 -0
@@ -0,0 +1,1706 @@
1
+ import { TOKENS, COMPONENT_DEFS, getThemeColors, getThemeNames, getThemeRadius } from './tokens';
2
+ import { parseColor, debug, type RGB } from './colors';
3
+ import { bindColorVariable, setThemeMode, isMultiModeEnabled } from './variables';
4
+ import { tailwindClassesToStyle, applyTailwindStylesToFrame, applyFullWidthIfPossible } from './tailwind';
5
+ import { createCompoundComponent } from './component-gen';
6
+ import { getActivePack } from './packs';
7
+ import { extractTextColorToken, resolveTextColorValue } from './color-resolver';
8
+ import { defaultGridWidth, extractGridBreakpointWidthFromTree, extractGridColumnsFromTree, normalizeLayoutClasses } from './story-layout';
9
+ import { extractFixedWidth, extractMaxWidth, extractGridBreakpointWidth, extractGridColumns, shouldSkipFullWidthForClasses } from './width-solver';
10
+ import { treeHasFullWidth } from './class-utils';
11
+ import { createTextNode } from './text-builder';
12
+ import { type JsxNode, splitClassName } from './node-ir';
13
+ import { extractStatesFromClasses, mergeStatesWithDefinition, type StateInfo } from './state-analyzer';
14
+ import { extractBreakpointsFromClasses, getClassesForBreakpoint, hasSignificantResponsiveChanges, getBreakpointLabel } from './responsive-analyzer';
15
+ import { extractArbitraryValue, parseLength, parseUtilityClass } from './utility-resolver';
16
+
17
+ declare const figma: any;
18
+
19
+ export type StoryRenderContext = {
20
+ maxWidth?: number;
21
+ textAlign?: 'LEFT' | 'CENTER' | 'RIGHT';
22
+ parentLayout?: 'HORIZONTAL' | 'VERTICAL';
23
+ textColor?: string | RGB | null;
24
+ textColorToken?: string;
25
+ parentCompoundDef?: any;
26
+ };
27
+
28
+ type ThemeContext = {
29
+ theme: string;
30
+ colorGroup: Record<string, string>;
31
+ radiusGroup: Record<string, string> | null;
32
+ };
33
+
34
+ export type StoryBuilderContext = {
35
+ getComponentDefByName: (name: string) => any | null;
36
+ normalizeComponentDef: (raw: any) => any;
37
+ applyClipBehavior: (frame: FrameNode, classes: string[] | undefined) => void;
38
+ applyLayoutClasses: (
39
+ frame: FrameNode,
40
+ classes: string[] | undefined,
41
+ colorGroup: Record<string, string>,
42
+ radiusGroup: Record<string, string> | null,
43
+ theme: string
44
+ ) => void;
45
+ hasExplicitHeight: (classes: string[] | undefined) => boolean;
46
+ renderJsxTree: (
47
+ node: JsxNode,
48
+ colorGroup: Record<string, string>,
49
+ radiusGroup: Record<string, string> | null,
50
+ theme: string,
51
+ depth?: number,
52
+ context?: StoryRenderContext
53
+ ) => SceneNode | null;
54
+ applyAbsoluteIfAllowed: (child: SceneNode, parent: FrameNode, allowAbsolute: boolean) => void;
55
+ applyGridColumnsWithReflow: (frame: FrameNode, widthOverride?: number, colsOverride?: number) => void;
56
+ hasWidthHintInClasses: (classes: string[]) => boolean;
57
+ propsContainWidthHint: (props: Record<string, any> | undefined) => boolean;
58
+ };
59
+
60
+ const THEME_CONTEXT_CACHE: Record<string, ThemeContext> = {};
61
+
62
+ function getThemeContext(theme: string): ThemeContext {
63
+ if (THEME_CONTEXT_CACHE[theme]) return THEME_CONTEXT_CACHE[theme];
64
+ const colorGroup = getThemeColors(TOKENS, theme);
65
+ const radiusGroup = getThemeRadius(TOKENS, theme);
66
+ const ctx: ThemeContext = {
67
+ theme: theme,
68
+ colorGroup: colorGroup,
69
+ radiusGroup: radiusGroup,
70
+ };
71
+ THEME_CONTEXT_CACHE[theme] = ctx;
72
+ return ctx;
73
+ }
74
+
75
+ function applyStyleToFrame(frame: any, style: any, theme: string): void {
76
+ if (!style) return;
77
+
78
+ if (style.bg) {
79
+ const bgBound = style.bgToken && bindColorVariable(frame, style.bgToken, 'fill', theme);
80
+ if (!bgBound) {
81
+ const bg = parseColor(style.bg);
82
+ const bgOpacity = style.bgOpacity != null ? style.bgOpacity : (bg.a == null ? 1 : bg.a);
83
+ frame.fills = [{ type: 'SOLID', color: { r: bg.r, g: bg.g, b: bg.b }, opacity: bgOpacity }];
84
+ }
85
+ }
86
+
87
+ if (style.border) {
88
+ const borderBound = style.borderToken && bindColorVariable(frame, style.borderToken, 'stroke', theme);
89
+ if (!borderBound) {
90
+ const borderColor = parseColor(style.border);
91
+ frame.strokes = [{ type: 'SOLID', color: { r: borderColor.r, g: borderColor.g, b: borderColor.b } }];
92
+ }
93
+ frame.strokeWeight = 1;
94
+ }
95
+
96
+ if (style.opacity != null) {
97
+ frame.opacity = style.opacity;
98
+ }
99
+ }
100
+
101
+ type RingInfo = { width: number; color: { r: number; g: number; b: number; a?: number } };
102
+
103
+ function getStateEntry(states: StateInfo[], name: string): StateInfo | null {
104
+ for (let i = 0; i < states.length; i++) {
105
+ if (states[i].name === name) return states[i];
106
+ }
107
+ return null;
108
+ }
109
+
110
+ function getStateNames(states: StateInfo[]): string[] {
111
+ const names: string[] = ['default'];
112
+ for (let i = 0; i < states.length; i++) {
113
+ const name = states[i].name;
114
+ if (name === 'default') continue;
115
+ names.push(name);
116
+ }
117
+ return names;
118
+ }
119
+
120
+ function buildStateClasses(states: StateInfo[], name: string): string[] {
121
+ const defaultEntry = getStateEntry(states, 'default');
122
+ const baseClasses = defaultEntry ? defaultEntry.classes.slice() : [];
123
+ if (name === 'default') return baseClasses;
124
+ const entry = getStateEntry(states, name);
125
+ if (!entry) return baseClasses;
126
+ return baseClasses.concat(entry.classes);
127
+ }
128
+
129
+ function isVisualStateUtility(cls: string): boolean {
130
+ const atom = parseUtilityClass(cls);
131
+ const utility = atom.utility || cls;
132
+ if (!utility) return false;
133
+ if (utility.startsWith('text-')) return false;
134
+ if (utility.startsWith('font-')) return false;
135
+ if (utility === 'underline' || utility === 'no-underline') return false;
136
+ if (utility.startsWith('decoration-')) return false;
137
+
138
+ return (
139
+ utility.startsWith('bg-')
140
+ || utility.startsWith('border')
141
+ || utility.startsWith('ring')
142
+ || utility.startsWith('shadow')
143
+ || utility.startsWith('opacity-')
144
+ || utility.startsWith('outline')
145
+ || utility.startsWith('cursor-')
146
+ || utility.startsWith('pointer-events-')
147
+ || utility.startsWith('scale-')
148
+ || utility.startsWith('translate-')
149
+ || utility.startsWith('rotate-')
150
+ || utility.startsWith('skew-')
151
+ );
152
+ }
153
+
154
+ function shouldSkipStatePreview(def: any): boolean {
155
+ if (!def || def.type !== 'state') return false;
156
+ const states = def.states || {};
157
+ const names = Object.keys(states).filter(name => name !== 'default');
158
+ if (names.length === 0) return true;
159
+
160
+ let hasVisualState = false;
161
+ for (let i = 0; i < names.length; i++) {
162
+ const state = states[names[i]];
163
+ const classes = state && state.classes ? state.classes : [];
164
+ for (let j = 0; j < classes.length; j++) {
165
+ if (isVisualStateUtility(classes[j])) {
166
+ hasVisualState = true;
167
+ break;
168
+ }
169
+ }
170
+ if (hasVisualState) break;
171
+ }
172
+ if (hasVisualState) return false;
173
+
174
+ const stories = def.stories || [];
175
+ for (let i = 0; i < stories.length; i++) {
176
+ const tree = stories[i] && stories[i].jsxTree ? stories[i].jsxTree : null;
177
+ if (!tree || tree.type !== 'element') continue;
178
+ const tagName = String((tree as any).tagName || '').toLowerCase();
179
+ if (
180
+ tagName === 'div'
181
+ || tagName === 'section'
182
+ || tagName === 'header'
183
+ || tagName === 'footer'
184
+ || tagName === 'main'
185
+ || tagName === 'article'
186
+ || tagName === 'nav'
187
+ ) {
188
+ return true;
189
+ }
190
+ }
191
+ return false;
192
+ }
193
+
194
+ function parseRingWidth(utility: string): number | null {
195
+ if (utility === 'ring') return 3;
196
+ if (!utility.startsWith('ring-')) return null;
197
+ const token = utility.substring(5);
198
+ if (token === 'inset' || token.startsWith('offset-')) return null;
199
+ if (token.startsWith('[')) {
200
+ const arbitrary = extractArbitraryValue(utility);
201
+ if (!arbitrary) return null;
202
+ return parseLength(arbitrary);
203
+ }
204
+ const num = parseFloat(token);
205
+ if (!Number.isNaN(num) && String(num) === token) return num;
206
+ return null;
207
+ }
208
+
209
+ function parseRingColor(utility: string, colorGroup: Record<string, string>): { r: number; g: number; b: number; a?: number } | null {
210
+ if (!utility.startsWith('ring-')) return null;
211
+ const token = utility.substring(5);
212
+ if (token === 'inset' || token.startsWith('offset-')) return null;
213
+ if (token.startsWith('[')) return null;
214
+ const num = parseFloat(token);
215
+ if (!Number.isNaN(num) && String(num) === token) return null;
216
+ const resolved = colorGroup[token];
217
+ if (!resolved) return null;
218
+ return parseColor(resolved);
219
+ }
220
+
221
+ function getRingInfoFromClasses(classes: string[], colorGroup: Record<string, string>): RingInfo | null {
222
+ let width: number | null = null;
223
+ let color: { r: number; g: number; b: number; a?: number } | null = null;
224
+
225
+ for (let i = 0; i < classes.length; i++) {
226
+ const cls = classes[i];
227
+ const nextWidth = parseRingWidth(cls);
228
+ if (nextWidth != null) width = nextWidth;
229
+ const nextColor = parseRingColor(cls, colorGroup);
230
+ if (nextColor) color = nextColor;
231
+ }
232
+
233
+ if (width == null && color == null) return null;
234
+ if (width == null) width = 3;
235
+ if (!color) {
236
+ const fallback = colorGroup.ring || colorGroup.primary;
237
+ if (!fallback) return null;
238
+ color = parseColor(fallback);
239
+ }
240
+ if (!width || width <= 0) return null;
241
+ return { width, color };
242
+ }
243
+
244
+ function hasVisibleFill(node: FrameNode): boolean {
245
+ const fills = (node as any).fills;
246
+ if (!Array.isArray(fills)) return false;
247
+ for (let i = 0; i < fills.length; i++) {
248
+ const fill = fills[i];
249
+ if (!fill || fill.visible === false) continue;
250
+ if (fill.opacity != null && fill.opacity <= 0) continue;
251
+ return true;
252
+ }
253
+ return false;
254
+ }
255
+
256
+ function applyRingEffect(node: FrameNode, classes: string[], colorGroup: Record<string, string>): void {
257
+ const ring = getRingInfoFromClasses(classes, colorGroup);
258
+ if (!ring) return;
259
+ const useSpread = (node as any).clipsContent === true && hasVisibleFill(node);
260
+ if (!useSpread) {
261
+ const strokeWeight = typeof node.strokeWeight === 'number' ? node.strokeWeight : 0;
262
+ node.strokes = [{
263
+ type: 'SOLID',
264
+ color: { r: ring.color.r, g: ring.color.g, b: ring.color.b },
265
+ opacity: ring.color.a == null ? 1 : ring.color.a,
266
+ }];
267
+ node.strokeWeight = Math.max(strokeWeight, ring.width);
268
+ return;
269
+ }
270
+ const effect: DropShadowEffect = {
271
+ type: 'DROP_SHADOW',
272
+ color: { r: ring.color.r, g: ring.color.g, b: ring.color.b, a: ring.color.a == null ? 1 : ring.color.a },
273
+ offset: { x: 0, y: 0 },
274
+ radius: 0,
275
+ spread: ring.width,
276
+ visible: true,
277
+ blendMode: 'NORMAL'
278
+ };
279
+ const effects: Effect[] = [];
280
+ if (node.effects && node.effects.length > 0) {
281
+ for (let i = 0; i < node.effects.length; i++) effects.push(node.effects[i]);
282
+ }
283
+ effects.push(effect);
284
+ node.effects = effects;
285
+ }
286
+
287
+ function normalizeComponentName(name: string | undefined): string {
288
+ return String(name || '').toLowerCase().replace(/-/g, '');
289
+ }
290
+
291
+ function findMatchingInstance(def: any, story: any): any | null {
292
+ const instances = story && story.instances ? story.instances : [];
293
+ const defKey = normalizeComponentName(def && def.name);
294
+ for (let i = 0; i < instances.length; i++) {
295
+ const instance = instances[i];
296
+ if (!instance || !instance.componentName) continue;
297
+ if (normalizeComponentName(instance.componentName) === defKey) return instance;
298
+ }
299
+ return null;
300
+ }
301
+
302
+ function buildStatePreviewLabel(def: any, instance: any): string {
303
+ const props = instance && instance.props ? instance.props : {};
304
+ if (instance && instance.children) return String(instance.children);
305
+ if (props.children) return String(props.children);
306
+ if (props.placeholder) return String(props.placeholder);
307
+ if (props.defaultValue) return String(props.defaultValue);
308
+ return def && def.name ? String(def.name) : 'Component';
309
+ }
310
+
311
+ function buildStoryStateClasses(def: any, instance: any): string[] {
312
+ const props = instance && instance.props ? instance.props : {};
313
+ const classes: string[] = [];
314
+
315
+ if (def && def.baseClasses) {
316
+ for (let i = 0; i < def.baseClasses.length; i++) classes.push(def.baseClasses[i]);
317
+ }
318
+
319
+ if (def && def.type === 'cva') {
320
+ const variantKeys = Object.keys(def.variants || {});
321
+ for (let i = 0; i < variantKeys.length; i++) {
322
+ const key = variantKeys[i];
323
+ const value = props[key] || (def.defaultVariants && def.defaultVariants[key]) || (def.variants[key] && def.variants[key][0]);
324
+ if (value && def.variantClasses && def.variantClasses[key] && def.variantClasses[key][value]) {
325
+ const variantClasses = def.variantClasses[key][value];
326
+ for (let j = 0; j < variantClasses.length; j++) classes.push(variantClasses[j]);
327
+ }
328
+ }
329
+ }
330
+
331
+ const extra = splitClassName(props.className);
332
+ for (let i = 0; i < extra.length; i++) classes.push(extra[i]);
333
+ return classes;
334
+ }
335
+
336
+ function getPreviewTypography(classes: string[]): { fontSize: number; bold: boolean } {
337
+ const sizeMap: Record<string, number> = {
338
+ 'text-xs': 12,
339
+ 'text-sm': 14,
340
+ 'text-base': 16,
341
+ 'text-lg': 18,
342
+ 'text-xl': 20,
343
+ 'text-2xl': 24,
344
+ 'text-3xl': 30,
345
+ 'text-4xl': 36,
346
+ 'text-5xl': 48,
347
+ };
348
+ let fontSize = 14;
349
+ let bold = false;
350
+ for (let i = 0; i < classes.length; i++) {
351
+ const atom = parseUtilityClass(classes[i]);
352
+ const utility = atom.utility || '';
353
+ if (sizeMap[utility] != null) fontSize = sizeMap[utility];
354
+ if (utility === 'font-bold' || utility === 'font-semibold') bold = true;
355
+ if (utility === 'font-normal' || utility === 'font-light' || utility === 'font-extralight') bold = false;
356
+ }
357
+ return { fontSize: fontSize, bold: bold };
358
+ }
359
+
360
+ function measureTextWidth(label: string, options?: any): number {
361
+ const node = createTextNode(label, options);
362
+ const width = node.width;
363
+ try {
364
+ node.remove();
365
+ } catch (_err) {
366
+ // ignore
367
+ }
368
+ return width;
369
+ }
370
+
371
+ function createPreviewLabelNode(label: string, classes: string[], colorGroup: Record<string, string>): TextNode {
372
+ const style = tailwindClassesToStyle(classes, 'default', colorGroup);
373
+ const textColor = style.text ? parseColor(style.text) :
374
+ (colorGroup.foreground ? parseColor(colorGroup.foreground) : { r: 0, g: 0, b: 0 });
375
+ const typography = getPreviewTypography(classes);
376
+ const text = createTextNode(label, { bold: typography.bold, fontSize: typography.fontSize, fill: textColor });
377
+ if (style.underline && text.textDecoration !== undefined) {
378
+ text.textDecoration = 'UNDERLINE';
379
+ }
380
+ return text;
381
+ }
382
+
383
+ function buildStoryInstanceClasses(def: any, instance: any): string[] {
384
+ const props = instance && instance.props ? instance.props : {};
385
+ let classes: string[] = def && def.baseClasses ? def.baseClasses.slice() : [];
386
+
387
+ if (def && def.type === 'cva') {
388
+ const variantKeys = Object.keys(def.variants || {});
389
+ for (let i = 0; i < variantKeys.length; i++) {
390
+ const key = variantKeys[i];
391
+ const value = props[key]
392
+ || (def.defaultVariants && def.defaultVariants[key])
393
+ || (def.variants && def.variants[key] && def.variants[key][0]);
394
+ if (value && def.variantClasses && def.variantClasses[key] && def.variantClasses[key][value]) {
395
+ classes = classes.concat(def.variantClasses[key][value]);
396
+ }
397
+ }
398
+ }
399
+
400
+ const extra = splitClassName(props.className);
401
+ if (extra.length > 0) {
402
+ classes = classes.concat(extra);
403
+ }
404
+
405
+ return classes;
406
+ }
407
+
408
+ function collectTreeClasses(node: JsxNode | undefined, output: string[]): void {
409
+ if (!node) return;
410
+ if (node.type === 'element') {
411
+ const el = node as any;
412
+ const className = el.props && el.props.className ? String(el.props.className) : '';
413
+ if (className) {
414
+ const list = splitClassName(className);
415
+ for (let i = 0; i < list.length; i++) output.push(list[i]);
416
+ }
417
+ const children = el.children || [];
418
+ for (let i = 0; i < children.length; i++) {
419
+ collectTreeClasses(children[i], output);
420
+ }
421
+ }
422
+ }
423
+
424
+ function treeHasResponsiveClasses(node: JsxNode | undefined): boolean {
425
+ const list: string[] = [];
426
+ collectTreeClasses(node, list);
427
+ return list.length > 0 && hasSignificantResponsiveChanges(list);
428
+ }
429
+
430
+ function mapClassNameForBreakpoint(value: string | undefined, breakpoint: string): string | undefined {
431
+ if (!value) return value;
432
+ const list = splitClassName(value);
433
+ if (list.length === 0) return value;
434
+ const mapped = getClassesForBreakpoint(list, breakpoint);
435
+ let hasGrid = false;
436
+ let hasGridCols = false;
437
+ for (let i = 0; i < mapped.length; i++) {
438
+ const cls = mapped[i];
439
+ if (cls === 'grid' || cls === 'inline-grid') hasGrid = true;
440
+ if (cls.startsWith('grid-cols-')) hasGridCols = true;
441
+ }
442
+ if (hasGrid && !hasGridCols) mapped.push('grid-cols-1');
443
+ return mapped.join(' ');
444
+ }
445
+
446
+ function cloneJsxNodeForBreakpoint(node: JsxNode, breakpoint: string): JsxNode {
447
+ if (!node || node.type !== 'element') return node;
448
+ const el: any = node;
449
+ const nextProps = el.props ? Object.assign({}, el.props) : {};
450
+ if (nextProps.className) {
451
+ nextProps.className = mapClassNameForBreakpoint(String(nextProps.className), breakpoint);
452
+ }
453
+ const nextChildren: any[] = [];
454
+ const children = el.children || [];
455
+ for (let i = 0; i < children.length; i++) {
456
+ nextChildren.push(cloneJsxNodeForBreakpoint(children[i], breakpoint));
457
+ }
458
+ return Object.assign({}, el, { props: nextProps, children: nextChildren });
459
+ }
460
+
461
+ function extractGridColsFromTree(node: JsxNode | undefined): number | null {
462
+ if (!node || node.type !== 'element') return null;
463
+ const el = node as any;
464
+ const className = el.props && el.props.className ? String(el.props.className) : '';
465
+ const list = className ? splitClassName(className) : [];
466
+ let hasGrid = false;
467
+ for (let i = 0; i < list.length; i++) {
468
+ const cls = list[i];
469
+ if (cls === 'grid' || cls === 'inline-grid') hasGrid = true;
470
+ const match = cls.match(/^grid-cols-(\d+)$/);
471
+ if (match) return parseInt(match[1], 10);
472
+ }
473
+ const children = el.children || [];
474
+ for (let i = 0; i < children.length; i++) {
475
+ const found = extractGridColsFromTree(children[i]);
476
+ if (found != null) return found;
477
+ }
478
+ return hasGrid ? 1 : null;
479
+ }
480
+
481
+ function extractLeadingContainerMaxWidthFromTree(node: JsxNode | undefined): number | null {
482
+ let current: JsxNode | undefined = node;
483
+ let constrainedWidth: number | null = null;
484
+
485
+ while (current && current.type === 'element') {
486
+ const element = current as any;
487
+ const className = element.props && element.props.className ? String(element.props.className) : '';
488
+ const classes = className ? splitClassName(className) : [];
489
+ const hasFullWidth = classes.indexOf('w-full') !== -1;
490
+ const maxWidth = extractMaxWidth(classes);
491
+
492
+ if (hasFullWidth && maxWidth != null) {
493
+ constrainedWidth = constrainedWidth == null ? maxWidth : Math.min(constrainedWidth, maxWidth);
494
+ }
495
+
496
+ const children = element.children || [];
497
+ if (children.length !== 1 || !children[0] || children[0].type !== 'element') {
498
+ break;
499
+ }
500
+ current = children[0];
501
+ }
502
+
503
+ return constrainedWidth;
504
+ }
505
+
506
+ function uniqueClassSignature(classes: string[] | undefined): string {
507
+ const seen: Record<string, boolean> = {};
508
+ const output: string[] = [];
509
+ const list = classes || [];
510
+ for (let i = 0; i < list.length; i++) {
511
+ const cls = list[i];
512
+ if (!cls || seen[cls]) continue;
513
+ seen[cls] = true;
514
+ output.push(cls);
515
+ }
516
+ return output.join(' ');
517
+ }
518
+
519
+ function treeClassSignature(node: JsxNode | undefined): string {
520
+ const classes: string[] = [];
521
+ collectTreeClasses(node, classes);
522
+ return uniqueClassSignature(classes);
523
+ }
524
+
525
+ function buildCvaClassesForVariant(def: any, props: Record<string, any>, primaryKey: string, primaryValue: string): string[] {
526
+ const classes: string[] = [];
527
+ if (def && def.baseClasses) {
528
+ for (let i = 0; i < def.baseClasses.length; i++) classes.push(def.baseClasses[i]);
529
+ }
530
+ const variants = def && def.variants ? def.variants : {};
531
+ const variantKeys = Object.keys(variants);
532
+ for (let i = 0; i < variantKeys.length; i++) {
533
+ const key = variantKeys[i];
534
+ let value: string | null = null;
535
+ if (key === primaryKey) {
536
+ value = primaryValue;
537
+ } else if (props && props[key]) {
538
+ value = props[key];
539
+ } else if (def && def.defaultVariants && def.defaultVariants[key]) {
540
+ value = def.defaultVariants[key];
541
+ } else if (variants[key] && variants[key][0]) {
542
+ value = variants[key][0];
543
+ }
544
+ if (value && def.variantClasses && def.variantClasses[key] && def.variantClasses[key][value]) {
545
+ const variantClasses = def.variantClasses[key][value];
546
+ for (let j = 0; j < variantClasses.length; j++) classes.push(variantClasses[j]);
547
+ }
548
+ }
549
+ const extra = splitClassName(props && props.className);
550
+ for (let i = 0; i < extra.length; i++) classes.push(extra[i]);
551
+ return classes;
552
+ }
553
+
554
+ function createStatePreviewBlock(
555
+ def: any,
556
+ story: any,
557
+ theme: string,
558
+ ctx: StoryBuilderContext
559
+ ): FrameNode | null {
560
+ if (!def || (def.type !== 'cva' && def.type !== 'state')) return null;
561
+ if (shouldSkipStatePreview(def)) return null;
562
+ const themeContext = getThemeContext(theme);
563
+ const colorGroup = themeContext.colorGroup;
564
+ const radiusGroup = themeContext.radiusGroup;
565
+ const instance = findMatchingInstance(def, story);
566
+ const props = instance && instance.props ? instance.props : {};
567
+
568
+ const block = figma.createFrame();
569
+ block.name = 'States';
570
+ block.layoutMode = 'VERTICAL';
571
+ block.itemSpacing = 16;
572
+ block.primaryAxisSizingMode = 'AUTO';
573
+ block.counterAxisSizingMode = 'AUTO';
574
+ block.fills = [];
575
+ ctx.applyClipBehavior(block, []);
576
+
577
+ const matrixTitleStyle = { fontSize: 18, bold: true, opacity: 0.95 };
578
+ const axisLabelStyle = { fontSize: 13, bold: true, opacity: 0.78 };
579
+ const itemLabelStyle = { fontSize: 13, bold: true, opacity: 0.72 };
580
+
581
+ block.appendChild(createTextNode('State Matrix', matrixTitleStyle));
582
+
583
+ const label = buildStatePreviewLabel(def, instance);
584
+
585
+ function createStateCell(stateName: string, classes: string[], columnLabel: string): FrameNode {
586
+ const comp = figma.createFrame();
587
+ comp.name = def.name + '/' + columnLabel + '/' + stateName;
588
+ comp.primaryAxisSizingMode = 'AUTO';
589
+ comp.counterAxisSizingMode = 'AUTO';
590
+ comp.counterAxisAlignItems = 'CENTER';
591
+ comp.primaryAxisAlignItems = 'CENTER';
592
+ comp.itemSpacing = 8;
593
+ comp.fills = [];
594
+ comp.strokes = [];
595
+ ctx.applyClipBehavior(comp, classes);
596
+
597
+ applyTailwindStylesToFrame(comp, classes, colorGroup, radiusGroup, theme);
598
+ comp.appendChild(createPreviewLabelNode(label, classes, colorGroup));
599
+ applyRingEffect(comp, classes, colorGroup);
600
+
601
+ return comp;
602
+ }
603
+
604
+ type VariantColumn = { label: string; states: StateInfo[] };
605
+ const columns: VariantColumn[] = [];
606
+
607
+ if (def.type === 'cva' && def.variants && Object.keys(def.variants).length > 0) {
608
+ const variantKeys = Object.keys(def.variants);
609
+ const primaryKey = def.variants.variant ? 'variant' : variantKeys[0];
610
+ const primaryValues = def.variants[primaryKey] || [];
611
+ for (let i = 0; i < primaryValues.length; i++) {
612
+ const value = primaryValues[i];
613
+ const classes = buildCvaClassesForVariant(def, props, primaryKey, value);
614
+ const states = mergeStatesWithDefinition(extractStatesFromClasses(classes), def);
615
+ const labelText = value ? value.charAt(0).toUpperCase() + value.slice(1) : '';
616
+ columns.push({ label: labelText, states: states });
617
+ }
618
+ } else {
619
+ const classes = buildStoryStateClasses(def, instance);
620
+ if (classes.length === 0) return null;
621
+ const states = mergeStatesWithDefinition(extractStatesFromClasses(classes), def);
622
+ columns.push({ label: def.name || 'Component', states: states });
623
+ }
624
+
625
+ if (columns.length === 0) return null;
626
+
627
+ const stateNames: string[] = columns.length > 0 ? getStateNames(columns[0].states) : [];
628
+ for (let i = 1; i < columns.length; i++) {
629
+ const names = getStateNames(columns[i].states);
630
+ for (let j = 0; j < names.length; j++) {
631
+ const name = names[j];
632
+ if (stateNames.indexOf(name) === -1) {
633
+ stateNames.push(name);
634
+ }
635
+ }
636
+ }
637
+ if (stateNames.length <= 1) return null;
638
+
639
+ const cellMatrix: FrameNode[][] = [];
640
+ const columnWidths: number[] = [];
641
+ for (let ci = 0; ci < columns.length; ci++) {
642
+ const column = columns[ci];
643
+ let maxWidth = measureTextWidth(column.label, itemLabelStyle);
644
+ const cells: FrameNode[] = [];
645
+ for (let si = 0; si < stateNames.length; si++) {
646
+ const stateName = stateNames[si];
647
+ const classes = buildStateClasses(column.states, stateName);
648
+ const cell = createStateCell(stateName, classes, column.label || def.name);
649
+ if (cell.width > maxWidth) maxWidth = cell.width;
650
+ cells.push(cell);
651
+ }
652
+ cellMatrix.push(cells);
653
+ columnWidths.push(maxWidth);
654
+ }
655
+
656
+ let legendWidth = measureTextWidth('State', itemLabelStyle);
657
+ for (let i = 0; i < stateNames.length; i++) {
658
+ const width = measureTextWidth(stateNames[i], itemLabelStyle);
659
+ if (width > legendWidth) legendWidth = width;
660
+ }
661
+ legendWidth += 24;
662
+
663
+ function createLegendCell(textValue: string, style: any): FrameNode {
664
+ const cell = figma.createFrame();
665
+ cell.layoutMode = 'VERTICAL';
666
+ cell.primaryAxisSizingMode = 'AUTO';
667
+ cell.counterAxisSizingMode = 'AUTO';
668
+ cell.fills = [];
669
+ cell.appendChild(createTextNode(textValue, style));
670
+ try {
671
+ cell.resize(legendWidth, cell.height);
672
+ cell.counterAxisSizingMode = 'FIXED';
673
+ } catch (_err) {
674
+ // ignore
675
+ }
676
+ return cell;
677
+ }
678
+
679
+ const axesRow = figma.createFrame();
680
+ axesRow.name = 'State Axes';
681
+ axesRow.layoutMode = 'HORIZONTAL';
682
+ axesRow.itemSpacing = 28;
683
+ axesRow.primaryAxisSizingMode = 'AUTO';
684
+ axesRow.counterAxisSizingMode = 'AUTO';
685
+ axesRow.fills = [];
686
+ ctx.applyClipBehavior(axesRow, []);
687
+ axesRow.appendChild(createLegendCell('States', axisLabelStyle));
688
+ axesRow.appendChild(createTextNode('Variants', axisLabelStyle));
689
+ block.appendChild(axesRow);
690
+
691
+ const table = figma.createFrame();
692
+ table.name = 'State Table';
693
+ table.layoutMode = 'VERTICAL';
694
+ table.itemSpacing = 12;
695
+ table.primaryAxisSizingMode = 'AUTO';
696
+ table.counterAxisSizingMode = 'AUTO';
697
+ table.fills = [];
698
+ ctx.applyClipBehavior(table, []);
699
+
700
+ const tableHeader = figma.createFrame();
701
+ tableHeader.name = 'State Table Header';
702
+ tableHeader.layoutMode = 'HORIZONTAL';
703
+ tableHeader.itemSpacing = 28;
704
+ tableHeader.primaryAxisSizingMode = 'AUTO';
705
+ tableHeader.counterAxisSizingMode = 'AUTO';
706
+ tableHeader.fills = [];
707
+ tableHeader.appendChild(createLegendCell('State', itemLabelStyle));
708
+ for (let ci = 0; ci < columns.length; ci++) {
709
+ const headerCell = figma.createFrame();
710
+ headerCell.layoutMode = 'VERTICAL';
711
+ headerCell.primaryAxisSizingMode = 'AUTO';
712
+ headerCell.counterAxisSizingMode = 'AUTO';
713
+ headerCell.fills = [];
714
+ headerCell.appendChild(createTextNode(columns[ci].label, itemLabelStyle));
715
+ try {
716
+ headerCell.resize(columnWidths[ci], headerCell.height);
717
+ headerCell.counterAxisSizingMode = 'FIXED';
718
+ } catch (_err) {
719
+ // ignore
720
+ }
721
+ tableHeader.appendChild(headerCell);
722
+ }
723
+ table.appendChild(tableHeader);
724
+
725
+ for (let si = 0; si < stateNames.length; si++) {
726
+ const row = figma.createFrame();
727
+ row.name = 'State Row/' + stateNames[si];
728
+ row.layoutMode = 'HORIZONTAL';
729
+ row.itemSpacing = 28;
730
+ row.primaryAxisSizingMode = 'AUTO';
731
+ row.counterAxisSizingMode = 'AUTO';
732
+ row.counterAxisAlignItems = 'CENTER';
733
+ row.fills = [];
734
+ row.appendChild(createLegendCell(stateNames[si], itemLabelStyle));
735
+ for (let ci = 0; ci < columns.length; ci++) {
736
+ const cell = cellMatrix[ci][si];
737
+ try {
738
+ cell.resize(columnWidths[ci], cell.height);
739
+ if (cell.layoutMode === 'HORIZONTAL') {
740
+ cell.primaryAxisSizingMode = 'FIXED';
741
+ } else if (cell.layoutMode === 'VERTICAL') {
742
+ cell.counterAxisSizingMode = 'FIXED';
743
+ }
744
+ } catch (_err) {
745
+ // ignore
746
+ }
747
+ row.appendChild(cell);
748
+ }
749
+ table.appendChild(row);
750
+ }
751
+
752
+ block.appendChild(table);
753
+ return block;
754
+ }
755
+
756
+ function createResponsivePreviewBlock(
757
+ def: any,
758
+ story: any,
759
+ theme: string,
760
+ ctx: StoryBuilderContext
761
+ ): FrameNode | null {
762
+ if (!def) return null;
763
+ const instance = findMatchingInstance(def, story);
764
+ const layoutClasses = normalizeLayoutClasses(story && story.layoutClasses);
765
+ const layoutHasResponsive = hasSignificantResponsiveChanges(layoutClasses);
766
+ const treeHasResponsive = story && story.jsxTree ? treeHasResponsiveClasses(story.jsxTree as any) : false;
767
+ const instanceClasses = buildStoryInstanceClasses(def, instance);
768
+ const instanceHasResponsive = hasSignificantResponsiveChanges(instanceClasses);
769
+ if (!layoutHasResponsive && !treeHasResponsive && !instanceHasResponsive) return null;
770
+
771
+ const sourceClasses: string[] = [];
772
+ if (layoutHasResponsive && layoutClasses.length > 0) {
773
+ for (let i = 0; i < layoutClasses.length; i++) sourceClasses.push(layoutClasses[i]);
774
+ }
775
+ if (treeHasResponsive && story && story.jsxTree) {
776
+ collectTreeClasses(story.jsxTree as any, sourceClasses);
777
+ }
778
+ if (sourceClasses.length === 0) {
779
+ for (let i = 0; i < instanceClasses.length; i++) sourceClasses.push(instanceClasses[i]);
780
+ }
781
+ const breakpoints = extractBreakpointsFromClasses(sourceClasses);
782
+ if (breakpoints.length <= 1) return null;
783
+ const entries: Array<{
784
+ bp: { name: string; minWidth: number };
785
+ layoutOverride: string[];
786
+ treeOverride: any;
787
+ instanceOverride: string[];
788
+ signature: string;
789
+ }> = [];
790
+ for (let i = 0; i < breakpoints.length; i++) {
791
+ const bp = breakpoints[i];
792
+ const layoutOverride = layoutClasses.length > 0 ? getClassesForBreakpoint(layoutClasses, bp.name) : layoutClasses;
793
+ let treeOverride: any = story && story.jsxTree ? story.jsxTree : null;
794
+ if (treeHasResponsive && story && story.jsxTree) {
795
+ treeOverride = cloneJsxNodeForBreakpoint(story.jsxTree as any, bp.name);
796
+ }
797
+ const instanceOverride = getClassesForBreakpoint(instanceClasses, bp.name);
798
+ let signature = 'I:' + uniqueClassSignature(instanceOverride);
799
+ if (layoutHasResponsive || treeHasResponsive) {
800
+ signature = 'L:' + uniqueClassSignature(layoutOverride) + '|T:' + treeClassSignature(treeOverride as any);
801
+ }
802
+ entries.push({
803
+ bp: { name: bp.name, minWidth: bp.minWidth },
804
+ layoutOverride: layoutOverride,
805
+ treeOverride: treeOverride,
806
+ instanceOverride: instanceOverride,
807
+ signature: signature,
808
+ });
809
+ }
810
+ if (entries.length <= 1) return null;
811
+ const filteredEntries: typeof entries = [];
812
+ const baseSignature = entries[0].signature;
813
+ const seenSignatures: Record<string, boolean> = {};
814
+ seenSignatures[baseSignature] = true;
815
+ filteredEntries.push(entries[0]);
816
+ for (let i = 1; i < entries.length; i++) {
817
+ const entry = entries[i];
818
+ if (entry.signature === baseSignature) continue;
819
+ if (seenSignatures[entry.signature]) continue;
820
+ seenSignatures[entry.signature] = true;
821
+ filteredEntries.push(entry);
822
+ }
823
+ if (filteredEntries.length <= 1) return null;
824
+
825
+ const themeContext = getThemeContext(theme);
826
+ const colorGroup = themeContext.colorGroup;
827
+ const radiusGroup = themeContext.radiusGroup;
828
+ const label = buildStatePreviewLabel(def, instance);
829
+
830
+ const block = figma.createFrame();
831
+ block.name = 'Responsive';
832
+ block.layoutMode = 'VERTICAL';
833
+ block.itemSpacing = 8;
834
+ block.primaryAxisSizingMode = 'AUTO';
835
+ block.counterAxisSizingMode = 'AUTO';
836
+ block.fills = [];
837
+ ctx.applyClipBehavior(block, []);
838
+
839
+ block.appendChild(createTextNode('Responsive', { fontSize: 12, bold: true, opacity: 0.7 }));
840
+
841
+ const row = figma.createFrame();
842
+ row.name = 'Responsive Strip';
843
+ row.layoutMode = 'HORIZONTAL';
844
+ row.itemSpacing = 16;
845
+ row.primaryAxisSizingMode = 'AUTO';
846
+ row.counterAxisSizingMode = 'AUTO';
847
+ row.fills = [];
848
+ ctx.applyClipBehavior(row, []);
849
+
850
+ for (let i = 0; i < filteredEntries.length; i++) {
851
+ const entry = filteredEntries[i];
852
+ const bp = entry.bp;
853
+ const viewportWidth = getResponsivePreviewWidth(bp.name, bp.minWidth);
854
+
855
+ const col = figma.createFrame();
856
+ col.name = getBreakpointLabel(bp.name, bp.minWidth);
857
+ col.layoutMode = 'VERTICAL';
858
+ col.itemSpacing = 6;
859
+ col.primaryAxisSizingMode = 'AUTO';
860
+ col.counterAxisSizingMode = 'AUTO';
861
+ col.fills = [];
862
+
863
+ col.appendChild(createTextNode(getBreakpointLabel(bp.name, bp.minWidth), { fontSize: 12, bold: true, opacity: 0.6 }));
864
+ const viewport = figma.createFrame();
865
+ viewport.name = 'Viewport';
866
+ viewport.layoutMode = 'VERTICAL';
867
+ viewport.itemSpacing = 0;
868
+ viewport.primaryAxisSizingMode = 'AUTO';
869
+ viewport.counterAxisSizingMode = 'FIXED';
870
+ viewport.fills = [];
871
+ viewport.strokes = [];
872
+ viewport.clipsContent = true;
873
+ viewport.resize(viewportWidth, viewport.height);
874
+
875
+ if (layoutHasResponsive || treeHasResponsive) {
876
+ const layoutOverride = entry.layoutOverride;
877
+ const treeOverride = entry.treeOverride;
878
+ const storyOverride = Object.assign({}, story, { layoutClasses: layoutOverride, jsxTree: treeOverride });
879
+ const preview = renderStandaloneStory(storyOverride, theme, ctx, viewportWidth);
880
+ if (preview) {
881
+ const colsOverride = treeOverride ? extractGridColsFromTree(treeOverride as any) : null;
882
+ if (colsOverride != null && colsOverride > 0 && preview.children.length === 1) {
883
+ const target = preview.children[0];
884
+ if ('layoutMode' in target) {
885
+ const previewPadding = (preview.paddingLeft || 0) + (preview.paddingRight || 0);
886
+ const minWidth = defaultGridWidth(colsOverride);
887
+ let previewWidth = Number.isFinite(preview.width) ? preview.width - previewPadding : undefined;
888
+ if (!Number.isFinite(previewWidth) || (previewWidth != null && previewWidth < minWidth)) {
889
+ previewWidth = minWidth;
890
+ }
891
+ ctx.applyGridColumnsWithReflow(target as FrameNode, previewWidth, colsOverride);
892
+ }
893
+ }
894
+ if ('layoutAlign' in preview) {
895
+ (preview as any).layoutAlign = 'STRETCH';
896
+ }
897
+ viewport.appendChild(preview);
898
+ }
899
+ } else {
900
+ const bpClasses = entry.instanceOverride;
901
+ const comp = figma.createFrame();
902
+ comp.name = def.name + '/' + bp.name;
903
+ comp.primaryAxisSizingMode = 'AUTO';
904
+ comp.counterAxisSizingMode = 'AUTO';
905
+ comp.counterAxisAlignItems = 'CENTER';
906
+ comp.primaryAxisAlignItems = 'CENTER';
907
+ comp.itemSpacing = 8;
908
+ comp.fills = [];
909
+ comp.strokes = [];
910
+ ctx.applyClipBehavior(comp, bpClasses);
911
+
912
+ applyTailwindStylesToFrame(comp, bpClasses, colorGroup, radiusGroup, theme);
913
+ comp.appendChild(createPreviewLabelNode(label, bpClasses, colorGroup));
914
+
915
+ if ('layoutAlign' in comp) {
916
+ (comp as any).layoutAlign = 'STRETCH';
917
+ }
918
+ viewport.appendChild(comp);
919
+ }
920
+ col.appendChild(viewport);
921
+ row.appendChild(col);
922
+ }
923
+
924
+ block.appendChild(row);
925
+ return block;
926
+ }
927
+
928
+ function getResponsivePreviewWidth(breakpointName: string, minWidth: number): number {
929
+ if (breakpointName === 'base') return 390;
930
+ if (Number.isFinite(minWidth) && minWidth > 0) return minWidth;
931
+ return 390;
932
+ }
933
+
934
+ function shouldRenderStatesForStory(def: any, story: any, storyIndex: number): boolean {
935
+ const name = String(story && story.name ? story.name : '').toLowerCase();
936
+ if (name && (name === 'default' || name.indexOf('default') !== -1)) {
937
+ return true;
938
+ }
939
+ const stories = def && def.stories ? def.stories : [];
940
+ let hasDefault = false;
941
+ for (let i = 0; i < stories.length; i++) {
942
+ const storyName = String(stories[i] && stories[i].name ? stories[i].name : '').toLowerCase();
943
+ if (storyName && (storyName === 'default' || storyName.indexOf('default') !== -1)) {
944
+ hasDefault = true;
945
+ break;
946
+ }
947
+ }
948
+ if (hasDefault) return false;
949
+ return storyIndex === 0;
950
+ }
951
+
952
+ function shouldRenderResponsiveForStory(def: any, story: any, storyIndex: number): boolean {
953
+ const layoutClasses = normalizeLayoutClasses(story && story.layoutClasses);
954
+ if (hasSignificantResponsiveChanges(layoutClasses)) return true;
955
+ if (story && story.jsxTree && treeHasResponsiveClasses(story.jsxTree as any)) return true;
956
+ const instance = findMatchingInstance(def, story);
957
+ const instanceClasses = buildStoryInstanceClasses(def, instance);
958
+ if (hasSignificantResponsiveChanges(instanceClasses)) return true;
959
+
960
+ const name = String(story && story.name ? story.name : '').toLowerCase();
961
+ if (name && (name === 'default' || name.indexOf('default') !== -1)) {
962
+ return true;
963
+ }
964
+ const stories = def && def.stories ? def.stories : [];
965
+ let hasDefault = false;
966
+ for (let i = 0; i < stories.length; i++) {
967
+ const storyName = String(stories[i] && stories[i].name ? stories[i].name : '').toLowerCase();
968
+ if (storyName && (storyName === 'default' || storyName.indexOf('default') !== -1)) {
969
+ hasDefault = true;
970
+ break;
971
+ }
972
+ }
973
+ if (hasDefault) return false;
974
+ return storyIndex === 0;
975
+ }
976
+
977
+ export function createCVAStoryInstance(def: any, instance: any, theme: string, ctx: StoryBuilderContext): any {
978
+ const themeContext = getThemeContext(theme);
979
+ const colorGroup = themeContext.colorGroup;
980
+ const radiusGroup = themeContext.radiusGroup;
981
+ const props = instance.props || {};
982
+
983
+ let classes = def.baseClasses ? def.baseClasses.slice() : [];
984
+ const variantKeys = Object.keys(def.variants || {});
985
+ for (const key of variantKeys) {
986
+ const value = props[key] || (def.defaultVariants && def.defaultVariants[key]) || (def.variants[key] && def.variants[key][0]);
987
+ if (value && def.variantClasses && def.variantClasses[key] && def.variantClasses[key][value]) {
988
+ classes = classes.concat(def.variantClasses[key][value]);
989
+ }
990
+ }
991
+ classes = classes.concat(splitClassName(props.className));
992
+
993
+ const comp = figma.createFrame();
994
+ comp.name = def.name;
995
+ comp.primaryAxisSizingMode = 'AUTO';
996
+ comp.counterAxisSizingMode = 'AUTO';
997
+ comp.counterAxisAlignItems = 'CENTER';
998
+ comp.primaryAxisAlignItems = 'CENTER';
999
+ comp.itemSpacing = 8;
1000
+ comp.fills = [];
1001
+ comp.strokes = [];
1002
+ ctx.applyClipBehavior(comp, classes);
1003
+
1004
+ applyTailwindStylesToFrame(comp, classes, colorGroup, radiusGroup, theme);
1005
+
1006
+ const state = props.disabled === 'true' ? 'disabled' : 'default';
1007
+ const style = tailwindClassesToStyle(classes, state, colorGroup);
1008
+ applyStyleToFrame(comp, style, theme);
1009
+
1010
+ const label = instance.children || props.children || def.name;
1011
+ const typography = getPreviewTypography(classes);
1012
+ const textColor = style.text ? parseColor(style.text) :
1013
+ (colorGroup.foreground ? parseColor(colorGroup.foreground) : { r: 0, g: 0, b: 0 });
1014
+ const text = createTextNode(label, { bold: typography.bold, fontSize: typography.fontSize, fill: textColor });
1015
+ if (style.underline && text.textDecoration !== undefined) {
1016
+ text.textDecoration = 'UNDERLINE';
1017
+ }
1018
+ comp.appendChild(text);
1019
+
1020
+ return comp;
1021
+ }
1022
+
1023
+ export function createStateStoryInstance(def: any, instance: any, theme: string, ctx: StoryBuilderContext): any {
1024
+ const themeContext = getThemeContext(theme);
1025
+ const colorGroup = themeContext.colorGroup;
1026
+ const radiusGroup = themeContext.radiusGroup;
1027
+ const props = instance.props || {};
1028
+
1029
+ let classes = def.baseClasses ? def.baseClasses.slice() : [];
1030
+ classes = classes.concat(splitClassName(props.className));
1031
+
1032
+ const comp = figma.createFrame();
1033
+ comp.name = def.name;
1034
+ comp.primaryAxisSizingMode = 'AUTO';
1035
+ comp.counterAxisSizingMode = 'AUTO';
1036
+ comp.counterAxisAlignItems = 'CENTER';
1037
+ comp.itemSpacing = 8;
1038
+ comp.fills = [];
1039
+ comp.strokes = [];
1040
+ ctx.applyClipBehavior(comp, classes);
1041
+
1042
+ applyTailwindStylesToFrame(comp, classes, colorGroup, radiusGroup, theme);
1043
+
1044
+ let stateName = 'default';
1045
+ if (props['aria-invalid'] === 'true') stateName = 'error';
1046
+ if (props.disabled === 'true') stateName = 'disabled';
1047
+
1048
+ if (stateName !== 'default' && def.states && def.states[stateName]) {
1049
+ applyTailwindStylesToFrame(comp, def.states[stateName].classes || [], colorGroup, radiusGroup, theme);
1050
+ }
1051
+
1052
+ const label = instance.children || props.placeholder || props.defaultValue || def.name;
1053
+ const textColor = colorGroup['muted-foreground'] ? parseColor(colorGroup['muted-foreground']) : { r: 0.4, g: 0.4, b: 0.4 };
1054
+ const text = createTextNode(label, { fontSize: 14, fill: textColor });
1055
+ comp.appendChild(text);
1056
+
1057
+ return comp;
1058
+ }
1059
+
1060
+ export function createSimpleStoryInstance(def: any, instance: any, theme: string, ctx: StoryBuilderContext): any {
1061
+ const themeContext = getThemeContext(theme);
1062
+ const colorGroup = themeContext.colorGroup;
1063
+ const radiusGroup = themeContext.radiusGroup;
1064
+ const props = instance.props || {};
1065
+
1066
+ let classes = def.classes ? def.classes.slice() : [];
1067
+ classes = classes.concat(splitClassName(props.className));
1068
+
1069
+ const frame = figma.createFrame();
1070
+ frame.name = def.name;
1071
+ frame.primaryAxisSizingMode = 'AUTO';
1072
+ frame.counterAxisSizingMode = 'AUTO';
1073
+ frame.fills = [];
1074
+ ctx.applyClipBehavior(frame, classes);
1075
+
1076
+ applyTailwindStylesToFrame(frame, classes, colorGroup, radiusGroup, theme);
1077
+
1078
+ const label = instance.children || props.children;
1079
+ if (label) {
1080
+ frame.appendChild(createTextNode(label, { fontSize: 12 }));
1081
+ }
1082
+
1083
+ return frame;
1084
+ }
1085
+
1086
+ function renderStoryInstances(
1087
+ layout: any,
1088
+ story: any,
1089
+ theme: string,
1090
+ colorGroup: Record<string, string>,
1091
+ radiusGroup: Record<string, string> | null,
1092
+ ctx: StoryBuilderContext
1093
+ ): number {
1094
+ let added = 0;
1095
+ for (const instance of story.instances || []) {
1096
+ if (!instance || !instance.componentName) continue;
1097
+ const compDef = ctx.getComponentDefByName(instance.componentName);
1098
+ if (compDef) {
1099
+ const analysis = ctx.normalizeComponentDef(compDef);
1100
+ if (analysis.type === 'cva') {
1101
+ layout.appendChild(createCVAStoryInstance(analysis, instance, theme, ctx));
1102
+ added++;
1103
+ continue;
1104
+ }
1105
+ if (analysis.type === 'state') {
1106
+ layout.appendChild(createStateStoryInstance(analysis, instance, theme, ctx));
1107
+ added++;
1108
+ continue;
1109
+ }
1110
+ if (analysis.type === 'simple') {
1111
+ layout.appendChild(createSimpleStoryInstance(analysis, instance, theme, ctx));
1112
+ added++;
1113
+ continue;
1114
+ }
1115
+ if (analysis.type === 'compound') {
1116
+ const comp = createCompoundComponent(layout, analysis, theme);
1117
+ const extra = splitClassName(instance.props && instance.props.className);
1118
+ if (comp && extra.length > 0) {
1119
+ applyTailwindStylesToFrame(comp, extra, colorGroup, radiusGroup, theme);
1120
+ }
1121
+ added++;
1122
+ continue;
1123
+ }
1124
+ }
1125
+
1126
+ const fallback = figma.createFrame();
1127
+ fallback.name = instance.componentName || 'Component';
1128
+ fallback.layoutMode = 'VERTICAL';
1129
+ fallback.primaryAxisSizingMode = 'AUTO';
1130
+ fallback.counterAxisSizingMode = 'AUTO';
1131
+ fallback.fills = [];
1132
+ const extra = splitClassName(instance.props && instance.props.className);
1133
+ ctx.applyClipBehavior(fallback, extra);
1134
+ if (extra.length > 0) {
1135
+ applyTailwindStylesToFrame(fallback, extra, colorGroup, radiusGroup, theme);
1136
+ }
1137
+ const label = instance.children || instance.props?.children || instance.componentName;
1138
+ if (label) {
1139
+ fallback.appendChild(createTextNode(String(label), { fontSize: 12, opacity: 0.7 }));
1140
+ }
1141
+ layout.appendChild(fallback);
1142
+ added++;
1143
+ }
1144
+ return added;
1145
+ }
1146
+
1147
+ export function renderStandaloneStory(
1148
+ story: any,
1149
+ theme: string,
1150
+ ctx: StoryBuilderContext,
1151
+ viewportWidth?: number
1152
+ ): any {
1153
+ const themeContext = getThemeContext(theme);
1154
+ const colorGroup = themeContext.colorGroup;
1155
+ const radiusGroup = themeContext.radiusGroup;
1156
+ const layoutClasses = normalizeLayoutClasses(story.layoutClasses);
1157
+ const skipLayoutFullWidth = shouldSkipFullWidthForClasses(layoutClasses);
1158
+
1159
+ const layout = figma.createFrame();
1160
+ layout.name = 'Story Layout';
1161
+ layout.primaryAxisSizingMode = 'AUTO';
1162
+ layout.counterAxisSizingMode = 'AUTO';
1163
+ layout.fills = [];
1164
+ ctx.applyClipBehavior(layout, layoutClasses);
1165
+
1166
+ ctx.applyLayoutClasses(layout, layoutClasses, colorGroup, radiusGroup, theme);
1167
+
1168
+ const fixedWidth = extractFixedWidth(layoutClasses);
1169
+ const maxWidthFromLayout = extractMaxWidth(layoutClasses);
1170
+ const maxWidthFromTree = extractLeadingContainerMaxWidthFromTree(story.jsxTree);
1171
+ const constrainedMaxWidth = (
1172
+ maxWidthFromLayout != null && maxWidthFromTree != null
1173
+ ? Math.min(maxWidthFromLayout, maxWidthFromTree)
1174
+ : (maxWidthFromTree != null ? maxWidthFromTree : maxWidthFromLayout)
1175
+ );
1176
+ const fallbackWidth = !fixedWidth && story.jsxTree && treeHasFullWidth(story.jsxTree, null, {
1177
+ getComponentDefByName: ctx.getComponentDefByName,
1178
+ normalizeComponentDef: ctx.normalizeComponentDef,
1179
+ hasWidthHintInClasses: ctx.hasWidthHintInClasses,
1180
+ propsContainWidthHint: ctx.propsContainWidthHint,
1181
+ })
1182
+ ? (constrainedMaxWidth || 900)
1183
+ : null;
1184
+ let effectiveWidth = fixedWidth || fallbackWidth;
1185
+ const treeGridCols = story.jsxTree ? extractGridColumnsFromTree(story.jsxTree) : null;
1186
+ const treeGridMinWidth = story.jsxTree ? extractGridBreakpointWidthFromTree(story.jsxTree) : null;
1187
+ const layoutGridCols = extractGridColumns(layoutClasses);
1188
+ const layoutGridMinWidth = extractGridBreakpointWidth(layoutClasses);
1189
+ const gridCols = layoutGridCols || treeGridCols;
1190
+ if (!effectiveWidth && gridCols) {
1191
+ effectiveWidth = defaultGridWidth(gridCols);
1192
+ } else if (effectiveWidth && layoutGridCols) {
1193
+ // Only let root-level grid-cols override effectiveWidth — not deeply nested grid-cols inside the story tree
1194
+ const minGridWidth = defaultGridWidth(layoutGridCols);
1195
+ if (effectiveWidth < minGridWidth) {
1196
+ effectiveWidth = minGridWidth;
1197
+ }
1198
+ }
1199
+ const gridMinWidth = Math.max(layoutGridMinWidth || 0, treeGridMinWidth || 0);
1200
+ if (gridMinWidth > 0) {
1201
+ if (!effectiveWidth || effectiveWidth < gridMinWidth) {
1202
+ effectiveWidth = gridMinWidth;
1203
+ }
1204
+ }
1205
+ // Universal default: give stories without a detected width a sensible canvas size
1206
+ if (!effectiveWidth) {
1207
+ effectiveWidth = 900;
1208
+ }
1209
+ if (viewportWidth != null && Number.isFinite(viewportWidth) && viewportWidth > 0) {
1210
+ effectiveWidth = viewportWidth;
1211
+ }
1212
+ if (effectiveWidth) {
1213
+ layout.resize(effectiveWidth, layout.height);
1214
+ if (layout.layoutMode === 'HORIZONTAL') {
1215
+ layout.primaryAxisSizingMode = 'FIXED';
1216
+ } else {
1217
+ layout.counterAxisSizingMode = 'FIXED';
1218
+ }
1219
+ if (!ctx.hasExplicitHeight(layoutClasses)) {
1220
+ layout.resize(effectiveWidth, 1);
1221
+ if (layout.layoutMode === 'HORIZONTAL') {
1222
+ layout.counterAxisSizingMode = 'AUTO';
1223
+ } else {
1224
+ layout.primaryAxisSizingMode = 'AUTO';
1225
+ }
1226
+ }
1227
+ }
1228
+
1229
+ const layoutPadding = (layout.paddingLeft || 0) + (layout.paddingRight || 0);
1230
+ const treeContentWidth = effectiveWidth ? effectiveWidth - layoutPadding : undefined;
1231
+ const treeContext: StoryRenderContext = {
1232
+ maxWidth: treeContentWidth,
1233
+ parentLayout: layout.layoutMode === 'HORIZONTAL' ? 'HORIZONTAL' : 'VERTICAL',
1234
+ };
1235
+
1236
+ let added = 0;
1237
+ const allowAbsolute = ctx.hasExplicitHeight(layoutClasses);
1238
+ if (story.jsxTree) {
1239
+ let rendered = false;
1240
+ const rootNode = story.jsxTree as any;
1241
+ if (layoutClasses.length > 0 && rootNode && rootNode.type === 'element' && rootNode.tagName === 'div') {
1242
+ const rootClasses = splitClassName(rootNode.props && rootNode.props.className);
1243
+ const sameClasses = rootClasses.length > 0 && rootClasses.join(' ') === layoutClasses.join(' ');
1244
+ if (sameClasses) {
1245
+ const rootStyle = tailwindClassesToStyle(rootClasses, 'default', colorGroup);
1246
+ const rootTextToken = rootStyle.textToken || extractTextColorToken(rootClasses) || undefined;
1247
+ const rootTextColor = resolveTextColorValue(rootStyle.text, rootTextToken, colorGroup, theme) || undefined;
1248
+ let gridChildWidth: number | undefined;
1249
+ const rootGridCols = extractGridColumns(rootClasses, treeContentWidth);
1250
+ const gridColsForChildren = layoutGridCols || rootGridCols;
1251
+ if (gridColsForChildren && treeContentWidth != null && treeContentWidth > 0) {
1252
+ const gap = layout.itemSpacing || 0;
1253
+ const available = treeContentWidth - gap * Math.max(0, gridColsForChildren - 1);
1254
+ if (available > 0) {
1255
+ gridChildWidth = Math.max(0, available / gridColsForChildren);
1256
+ }
1257
+ }
1258
+ const rootContext: StoryRenderContext = {
1259
+ textColor: rootTextColor,
1260
+ textColorToken: rootTextToken,
1261
+ parentLayout: layout.layoutMode === 'HORIZONTAL' ? 'HORIZONTAL' : 'VERTICAL',
1262
+ maxWidth: gridChildWidth != null ? gridChildWidth : treeContentWidth,
1263
+ };
1264
+ for (let i = 0; i < (rootNode.children || []).length; i++) {
1265
+ const child = rootNode.children[i];
1266
+ const childNode = ctx.renderJsxTree(child, colorGroup, radiusGroup, theme, 0, rootContext);
1267
+ if (!childNode) continue;
1268
+ layout.appendChild(childNode);
1269
+ ctx.applyAbsoluteIfAllowed(childNode, layout, allowAbsolute);
1270
+ const fullWidthOptions = (skipLayoutFullWidth || treeContentWidth != null)
1271
+ ? { skipFullWidth: skipLayoutFullWidth, widthOverride: treeContentWidth }
1272
+ : undefined;
1273
+ applyFullWidthIfPossible(childNode, layout, fullWidthOptions);
1274
+ if ('layoutMode' in childNode) {
1275
+ ctx.applyGridColumnsWithReflow(childNode as FrameNode, treeContentWidth);
1276
+ }
1277
+ rendered = true;
1278
+ }
1279
+ }
1280
+ }
1281
+ if (!rendered) {
1282
+ const treeNode = ctx.renderJsxTree(story.jsxTree, colorGroup, radiusGroup, theme, 0, treeContext);
1283
+ if (treeNode) {
1284
+ layout.appendChild(treeNode);
1285
+ ctx.applyAbsoluteIfAllowed(treeNode, layout, allowAbsolute);
1286
+ const fullWidthOptions = (skipLayoutFullWidth || treeContentWidth != null)
1287
+ ? { skipFullWidth: skipLayoutFullWidth, widthOverride: treeContentWidth }
1288
+ : undefined;
1289
+ applyFullWidthIfPossible(treeNode, layout, fullWidthOptions);
1290
+ if ('layoutMode' in treeNode) {
1291
+ ctx.applyGridColumnsWithReflow(treeNode as FrameNode, treeContentWidth);
1292
+ }
1293
+ added++;
1294
+ }
1295
+ } else {
1296
+ added++;
1297
+ }
1298
+ }
1299
+ if (layoutGridCols) {
1300
+ ctx.applyGridColumnsWithReflow(layout, effectiveWidth || layout.width, layoutGridCols);
1301
+ }
1302
+
1303
+ if (added === 0 && story.instances && story.instances.length > 0) {
1304
+ added = renderStoryInstances(layout, story, theme, colorGroup, radiusGroup, ctx);
1305
+ }
1306
+
1307
+ if (added === 0) {
1308
+ layout.appendChild(createTextNode(story.name || 'Story', { fontSize: 12, opacity: 0.6 }));
1309
+ }
1310
+
1311
+ return layout;
1312
+ }
1313
+
1314
+ export function createUIComponents(parent: any, opts: any, ctx: StoryBuilderContext): any {
1315
+ const options = {
1316
+ primary: true,
1317
+ secondary: true,
1318
+ themeNames: getThemeNames(TOKENS),
1319
+ ...(opts || {}),
1320
+ };
1321
+ debug('createUIComponents start', { opts: options });
1322
+ const section = figma.createFrame();
1323
+ const sectionTitle: string = options.sectionTitle || 'UI Components';
1324
+ section.name = sectionTitle;
1325
+ section.layoutMode = 'VERTICAL';
1326
+ section.itemSpacing = 32;
1327
+ section.primaryAxisSizingMode = 'AUTO';
1328
+ section.counterAxisSizingMode = 'AUTO';
1329
+ section.paddingLeft = section.paddingRight = 24;
1330
+ section.paddingTop = section.paddingBottom = 24;
1331
+ section.fills = [];
1332
+ ctx.applyClipBehavior(section, []);
1333
+ const offsetY = typeof options.yOffset === 'number' ? options.yOffset : 0;
1334
+ const offsetX = typeof options.xOffset === 'number' ? options.xOffset : 0;
1335
+ section.x = offsetX;
1336
+ section.y = offsetY;
1337
+ parent.appendChild(section);
1338
+
1339
+ section.appendChild(createTextNode(sectionTitle, { fontSize: 28, bold: true }));
1340
+
1341
+ function formatThemeLabel(theme: string): string {
1342
+ if (!theme) return 'Theme';
1343
+ return theme.charAt(0).toUpperCase() + theme.slice(1);
1344
+ }
1345
+
1346
+ function addColumn(label: string, theme: string): any {
1347
+ const colFrame = figma.createFrame();
1348
+ colFrame.name = label + ' Column';
1349
+ colFrame.layoutMode = 'VERTICAL';
1350
+ colFrame.itemSpacing = 24;
1351
+ colFrame.primaryAxisSizingMode = 'AUTO';
1352
+ colFrame.counterAxisSizingMode = 'AUTO';
1353
+ colFrame.fills = [];
1354
+ ctx.applyClipBehavior(colFrame, []);
1355
+
1356
+ const header = createTextNode(label + ' Theme', { fontSize: 18, bold: true });
1357
+ colFrame.appendChild(header);
1358
+
1359
+ const pack = getActivePack();
1360
+ const packStories = pack && pack.stories ? pack.stories : [];
1361
+ const storyTagFilter = options && options.storyTagFilter ? String(options.storyTagFilter) : '';
1362
+ for (const story of packStories) {
1363
+ if (storyTagFilter) {
1364
+ const tags = story.tags || [];
1365
+ if (tags.indexOf(storyTagFilter) === -1) continue;
1366
+ }
1367
+ const storyBlock = figma.createFrame();
1368
+ storyBlock.name = story.name || 'Story';
1369
+ storyBlock.layoutMode = 'VERTICAL';
1370
+ storyBlock.itemSpacing = 12;
1371
+ storyBlock.primaryAxisSizingMode = 'AUTO';
1372
+ storyBlock.counterAxisSizingMode = 'AUTO';
1373
+ storyBlock.fills = [];
1374
+ ctx.applyClipBehavior(storyBlock, []);
1375
+ storyBlock.appendChild(createTextNode(story.name || 'Story', { fontSize: 16, bold: true }));
1376
+ storyBlock.appendChild(renderStandaloneStory(story, theme, ctx));
1377
+ colFrame.appendChild(storyBlock);
1378
+ }
1379
+
1380
+ const componentList = figma.createFrame();
1381
+ componentList.name = 'Component Blocks';
1382
+ componentList.layoutMode = 'VERTICAL';
1383
+ componentList.primaryAxisSizingMode = 'AUTO';
1384
+ componentList.counterAxisSizingMode = 'AUTO';
1385
+ componentList.itemSpacing = colFrame.itemSpacing * 6;
1386
+ componentList.fills = [];
1387
+ ctx.applyClipBehavior(componentList, []);
1388
+
1389
+ const defsRaw = (COMPONENT_DEFS && COMPONENT_DEFS.components) ? COMPONENT_DEFS.components : [];
1390
+ const onlyComponents: string[] | undefined = options.onlyComponents;
1391
+ const excludeComponents: string[] | undefined = options.excludeComponents;
1392
+ const defs = defsRaw
1393
+ .map(ctx.normalizeComponentDef)
1394
+ .filter((d: any) => {
1395
+ if (!d || !d.stories || d.stories.length === 0) return false;
1396
+ if (onlyComponents && onlyComponents.length > 0) return onlyComponents.includes(d.name);
1397
+ if (excludeComponents && excludeComponents.length > 0) return !excludeComponents.includes(d.name);
1398
+ return true;
1399
+ })
1400
+ .sort((a: any, b: any) => a.name.localeCompare(b.name));
1401
+
1402
+ for (const def of defs) {
1403
+ const block = figma.createFrame();
1404
+ block.name = def.name;
1405
+ block.layoutMode = 'VERTICAL';
1406
+ block.itemSpacing = 12;
1407
+ block.primaryAxisSizingMode = 'AUTO';
1408
+ block.counterAxisSizingMode = 'AUTO';
1409
+ block.fills = [];
1410
+ ctx.applyClipBehavior(block, []);
1411
+
1412
+ block.appendChild(createTextNode(def.name, { fontSize: 16, bold: true }));
1413
+
1414
+ const storyList = def.stories || [];
1415
+ for (let storyIndex = 0; storyIndex < storyList.length; storyIndex++) {
1416
+ const story = storyList[storyIndex];
1417
+ const storyWrap = figma.createFrame();
1418
+ storyWrap.name = story.name;
1419
+ storyWrap.layoutMode = 'VERTICAL';
1420
+ storyWrap.itemSpacing = 8;
1421
+ storyWrap.primaryAxisSizingMode = 'AUTO';
1422
+ storyWrap.counterAxisSizingMode = 'AUTO';
1423
+ storyWrap.counterAxisAlignItems = 'MIN';
1424
+ storyWrap.fills = [];
1425
+ ctx.applyClipBehavior(storyWrap, []);
1426
+
1427
+ storyWrap.appendChild(createTextNode(story.name, { fontSize: 12, opacity: 0.6, textAlignHorizontal: 'LEFT' }));
1428
+
1429
+ const layout = figma.createFrame();
1430
+ layout.name = 'Story Layout';
1431
+ layout.primaryAxisSizingMode = 'AUTO';
1432
+ layout.counterAxisSizingMode = 'AUTO';
1433
+ layout.fills = [];
1434
+
1435
+ const themeContext = getThemeContext(theme);
1436
+ const colorGroup = themeContext.colorGroup;
1437
+ const radiusGroup = themeContext.radiusGroup;
1438
+ const layoutClasses = normalizeLayoutClasses(story.layoutClasses);
1439
+ const skipLayoutFullWidth = shouldSkipFullWidthForClasses(layoutClasses);
1440
+ ctx.applyClipBehavior(layout, layoutClasses);
1441
+ ctx.applyLayoutClasses(layout, layoutClasses, colorGroup, radiusGroup, theme);
1442
+
1443
+ const fixedWidth = extractFixedWidth(layoutClasses);
1444
+ const maxWidthFromLayout = extractMaxWidth(layoutClasses);
1445
+ const maxWidthFromTree = extractLeadingContainerMaxWidthFromTree(story.jsxTree);
1446
+ const constrainedMaxWidth = (
1447
+ maxWidthFromLayout != null && maxWidthFromTree != null
1448
+ ? Math.min(maxWidthFromLayout, maxWidthFromTree)
1449
+ : (maxWidthFromTree != null ? maxWidthFromTree : maxWidthFromLayout)
1450
+ );
1451
+ const fallbackWidth = !fixedWidth && story.jsxTree && treeHasFullWidth(story.jsxTree, null, {
1452
+ getComponentDefByName: ctx.getComponentDefByName,
1453
+ normalizeComponentDef: ctx.normalizeComponentDef,
1454
+ hasWidthHintInClasses: ctx.hasWidthHintInClasses,
1455
+ propsContainWidthHint: ctx.propsContainWidthHint,
1456
+ })
1457
+ ? (constrainedMaxWidth || 900)
1458
+ : null;
1459
+ let effectiveWidth = fixedWidth || fallbackWidth;
1460
+ const treeGridCols = story.jsxTree ? extractGridColumnsFromTree(story.jsxTree) : null;
1461
+ const treeGridMinWidth = story.jsxTree ? extractGridBreakpointWidthFromTree(story.jsxTree) : null;
1462
+ const layoutGridCols = extractGridColumns(layoutClasses);
1463
+ const layoutGridMinWidth = extractGridBreakpointWidth(layoutClasses);
1464
+ const gridCols = layoutGridCols || treeGridCols;
1465
+ if (!effectiveWidth && gridCols) {
1466
+ effectiveWidth = defaultGridWidth(gridCols);
1467
+ } else if (effectiveWidth && layoutGridCols) {
1468
+ // Only let root-level grid-cols override effectiveWidth — not deeply nested grid-cols inside the story tree
1469
+ const minGridWidth = defaultGridWidth(layoutGridCols);
1470
+ if (effectiveWidth < minGridWidth) {
1471
+ effectiveWidth = minGridWidth;
1472
+ }
1473
+ }
1474
+ const gridMinWidth = Math.max(layoutGridMinWidth || 0, treeGridMinWidth || 0);
1475
+ if (gridMinWidth > 0) {
1476
+ if (!effectiveWidth || effectiveWidth < gridMinWidth) {
1477
+ effectiveWidth = gridMinWidth;
1478
+ }
1479
+ }
1480
+ // Universal default: give stories without a detected width a sensible canvas size
1481
+ if (!effectiveWidth) {
1482
+ effectiveWidth = 900;
1483
+ }
1484
+ if (effectiveWidth) {
1485
+ layout.resize(effectiveWidth, layout.height);
1486
+ if (layout.layoutMode === 'HORIZONTAL') {
1487
+ layout.primaryAxisSizingMode = 'FIXED';
1488
+ } else {
1489
+ layout.counterAxisSizingMode = 'FIXED';
1490
+ }
1491
+ if (!ctx.hasExplicitHeight(layoutClasses)) {
1492
+ layout.resize(effectiveWidth, 1);
1493
+ if (layout.layoutMode === 'HORIZONTAL') {
1494
+ layout.counterAxisSizingMode = 'AUTO';
1495
+ } else {
1496
+ layout.primaryAxisSizingMode = 'AUTO';
1497
+ }
1498
+ }
1499
+ } else {
1500
+ layout.primaryAxisSizingMode = 'AUTO';
1501
+ layout.counterAxisSizingMode = 'AUTO';
1502
+ }
1503
+
1504
+ let added = 0;
1505
+
1506
+ // For compound/simple components with jsxTree, render the full tree
1507
+ if ((def.type === 'compound' || def.type === 'simple') && story.jsxTree) {
1508
+ let rendered = false;
1509
+ const allowAbsolute = ctx.hasExplicitHeight(layoutClasses);
1510
+ const rootNode = story.jsxTree as any;
1511
+ if (layoutClasses.length > 0 && rootNode && rootNode.type === 'element' && rootNode.tagName === 'div') {
1512
+ const rootClasses = splitClassName(rootNode.props && rootNode.props.className);
1513
+ const sameClasses = rootClasses.length > 0 && rootClasses.join(' ') === layoutClasses.join(' ');
1514
+ if (sameClasses) {
1515
+ const rootStyle = tailwindClassesToStyle(rootClasses, 'default', colorGroup);
1516
+ const rootTextToken = rootStyle.textToken || extractTextColorToken(rootClasses) || undefined;
1517
+ const rootTextColor = resolveTextColorValue(rootStyle.text, rootTextToken, colorGroup, theme) || undefined;
1518
+ // Calculate content width accounting for padding
1519
+ const layoutPadding = (layout.paddingLeft || 0) + (layout.paddingRight || 0);
1520
+ const contentWidth = effectiveWidth ? effectiveWidth - layoutPadding : undefined;
1521
+ let gridChildWidth: number | undefined;
1522
+ const rootGridCols = extractGridColumns(rootClasses, contentWidth);
1523
+ const gridColsForChildren = layoutGridCols || rootGridCols;
1524
+ if (gridColsForChildren && contentWidth != null && contentWidth > 0) {
1525
+ const gap = layout.itemSpacing || 0;
1526
+ const available = contentWidth - gap * Math.max(0, gridColsForChildren - 1);
1527
+ if (available > 0) {
1528
+ gridChildWidth = Math.max(0, available / gridColsForChildren);
1529
+ }
1530
+ }
1531
+ const rootContext: StoryRenderContext = {
1532
+ textColor: rootTextColor,
1533
+ textColorToken: rootTextToken,
1534
+ parentLayout: layout.layoutMode === 'HORIZONTAL' ? 'HORIZONTAL' : 'VERTICAL',
1535
+ maxWidth: gridChildWidth != null ? gridChildWidth : contentWidth,
1536
+ };
1537
+ for (const child of rootNode.children || []) {
1538
+ const childNode = ctx.renderJsxTree(child, colorGroup, radiusGroup, theme, 0, rootContext);
1539
+ if (childNode) {
1540
+ layout.appendChild(childNode);
1541
+ ctx.applyAbsoluteIfAllowed(childNode, layout, allowAbsolute);
1542
+ const fullWidthOptions = (skipLayoutFullWidth || contentWidth != null)
1543
+ ? { skipFullWidth: skipLayoutFullWidth, widthOverride: contentWidth }
1544
+ : undefined;
1545
+ applyFullWidthIfPossible(childNode, layout, fullWidthOptions);
1546
+ if ('layoutMode' in childNode) {
1547
+ ctx.applyGridColumnsWithReflow(childNode as FrameNode, contentWidth);
1548
+ }
1549
+ rendered = true;
1550
+ }
1551
+ }
1552
+ if (rendered && layoutGridCols) {
1553
+ ctx.applyGridColumnsWithReflow(layout, effectiveWidth || layout.width, layoutGridCols);
1554
+ }
1555
+ }
1556
+ }
1557
+
1558
+ if (!rendered) {
1559
+ // Calculate content width for the tree context
1560
+ const layoutPadding = (layout.paddingLeft || 0) + (layout.paddingRight || 0);
1561
+ const treeContentWidth = effectiveWidth ? effectiveWidth - layoutPadding : undefined;
1562
+ const treeContext: StoryRenderContext = {
1563
+ maxWidth: treeContentWidth,
1564
+ parentLayout: layout.layoutMode === 'HORIZONTAL' ? 'HORIZONTAL' : 'VERTICAL',
1565
+ };
1566
+ const treeNode = ctx.renderJsxTree(story.jsxTree, colorGroup, radiusGroup, theme, 0, treeContext);
1567
+ if (treeNode) {
1568
+ layout.appendChild(treeNode);
1569
+ ctx.applyAbsoluteIfAllowed(treeNode, layout, allowAbsolute);
1570
+ const fullWidthOptions = (skipLayoutFullWidth || treeContentWidth != null)
1571
+ ? { skipFullWidth: skipLayoutFullWidth, widthOverride: treeContentWidth }
1572
+ : undefined;
1573
+ applyFullWidthIfPossible(treeNode, layout, fullWidthOptions);
1574
+ if ('layoutMode' in treeNode) {
1575
+ ctx.applyGridColumnsWithReflow(treeNode as FrameNode, treeContentWidth);
1576
+ }
1577
+
1578
+ if (effectiveWidth && rootNode && rootNode.type === 'element') {
1579
+ const rootClasses = splitClassName(rootNode.props && rootNode.props.className);
1580
+ const wantsFullWidth = rootClasses.includes('w-full');
1581
+ if (wantsFullWidth && 'resize' in treeNode) {
1582
+ try {
1583
+ (treeNode as any).resize(layout.width, (treeNode as any).height);
1584
+ const treeLayout = ('layoutMode' in treeNode) ? (treeNode as any).layoutMode : 'VERTICAL';
1585
+ if (treeLayout === 'HORIZONTAL' && 'primaryAxisSizingMode' in treeNode) {
1586
+ (treeNode as any).primaryAxisSizingMode = 'FIXED';
1587
+ } else if (treeLayout === 'VERTICAL' && 'counterAxisSizingMode' in treeNode) {
1588
+ (treeNode as any).counterAxisSizingMode = 'FIXED';
1589
+ }
1590
+ } catch (_err) {
1591
+ // ignore resize errors
1592
+ }
1593
+ }
1594
+ }
1595
+ added++;
1596
+ }
1597
+ } else {
1598
+ added++;
1599
+ }
1600
+ }
1601
+ if (added > 0) {
1602
+ }
1603
+
1604
+ // For CVA/state components OR when no jsxTree, use flat instance rendering
1605
+ if (added === 0) {
1606
+ for (const instance of story.instances || []) {
1607
+ if (!instance || !instance.componentName) continue;
1608
+ // Normalize names for comparison: lowercase and remove hyphens
1609
+ // This handles cases like "Mobile-nav" (from filename) vs "MobileNav" (from JS identifier)
1610
+ const normalizedInstanceName = instance.componentName.toLowerCase().replace(/-/g, '');
1611
+ const normalizedDefName = def.name.toLowerCase().replace(/-/g, '');
1612
+ if (normalizedInstanceName !== normalizedDefName) continue;
1613
+
1614
+ if (def.type === 'cva') {
1615
+ layout.appendChild(createCVAStoryInstance(def, instance, theme, ctx));
1616
+ added++;
1617
+ } else if (def.type === 'state') {
1618
+ layout.appendChild(createStateStoryInstance(def, instance, theme, ctx));
1619
+ added++;
1620
+ } else if (def.type === 'compound') {
1621
+ const comp = createCompoundComponent(layout, def, theme);
1622
+ const extra = splitClassName(instance.props && instance.props.className);
1623
+ if (comp && extra.length > 0) {
1624
+ applyTailwindStylesToFrame(comp, extra, colorGroup, radiusGroup, theme);
1625
+ }
1626
+ added++;
1627
+ } else if (def.type === 'simple') {
1628
+ layout.appendChild(createSimpleStoryInstance(def, instance, theme, ctx));
1629
+ added++;
1630
+ }
1631
+ }
1632
+ }
1633
+
1634
+ // Fallback if nothing was rendered
1635
+ if (added === 0) {
1636
+ if (def.type === 'compound') {
1637
+ createCompoundComponent(layout, def, theme);
1638
+ } else if (def.type === 'simple') {
1639
+ layout.appendChild(createSimpleStoryInstance(def, { props: {} }, theme, ctx));
1640
+ } else if (def.type === 'cva') {
1641
+ layout.appendChild(createCVAStoryInstance(def, { props: {} }, theme, ctx));
1642
+ } else if (def.type === 'state') {
1643
+ layout.appendChild(createStateStoryInstance(def, { props: {} }, theme, ctx));
1644
+ }
1645
+ }
1646
+
1647
+ storyWrap.appendChild(layout);
1648
+ if (shouldRenderStatesForStory(def, story, storyIndex)) {
1649
+ const statesBlock = createStatePreviewBlock(def, story, theme, ctx);
1650
+ if (statesBlock) {
1651
+ storyWrap.appendChild(statesBlock);
1652
+ }
1653
+ }
1654
+ if (shouldRenderResponsiveForStory(def, story, storyIndex)) {
1655
+ const responsiveBlock = createResponsivePreviewBlock(def, story, theme, ctx);
1656
+ if (responsiveBlock) {
1657
+ storyWrap.appendChild(responsiveBlock);
1658
+ }
1659
+ }
1660
+ block.appendChild(storyWrap);
1661
+ }
1662
+
1663
+ componentList.appendChild(block);
1664
+ }
1665
+
1666
+ if (componentList.children.length > 0) {
1667
+ colFrame.appendChild(componentList);
1668
+ }
1669
+
1670
+ return colFrame;
1671
+ }
1672
+
1673
+ const columns = figma.createFrame();
1674
+ columns.layoutMode = 'HORIZONTAL';
1675
+ columns.itemSpacing = 32;
1676
+ columns.primaryAxisSizingMode = 'AUTO';
1677
+ columns.counterAxisSizingMode = 'AUTO';
1678
+ columns.fills = [];
1679
+ ctx.applyClipBehavior(columns, []);
1680
+ const requestedThemes = Array.isArray(options.themeNames)
1681
+ ? options.themeNames.filter((theme: any, index: number, list: any[]) =>
1682
+ typeof theme === 'string' &&
1683
+ theme.trim().length > 0 &&
1684
+ list.indexOf(theme) === index)
1685
+ : [];
1686
+ if (requestedThemes.length === 0) {
1687
+ if (options.primary) requestedThemes.push('primary');
1688
+ if (options.secondary) requestedThemes.push('secondary');
1689
+ }
1690
+ const multiMode = isMultiModeEnabled();
1691
+ if (multiMode) {
1692
+ const activeTheme = requestedThemes[0] || 'primary';
1693
+ const singleCol = addColumn('Theme', activeTheme);
1694
+ setThemeMode(singleCol, activeTheme);
1695
+ columns.appendChild(singleCol);
1696
+ } else {
1697
+ for (const themeName of requestedThemes) {
1698
+ const themeCol = addColumn(formatThemeLabel(themeName), themeName);
1699
+ setThemeMode(themeCol, themeName);
1700
+ columns.appendChild(themeCol);
1701
+ }
1702
+ }
1703
+ section.appendChild(columns);
1704
+ debug("UI section built", { columns: columns.children.length });
1705
+ return section;
1706
+ }