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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/README.md +108 -25
  2. package/bin/inkbridge.mjs +354 -83
  3. package/code.js +40 -11802
  4. package/manifest.json +1 -0
  5. package/package.json +74 -23
  6. package/scanner/adapter-utils-regression.ts +159 -0
  7. package/scanner/aspect-percent-position-regression.ts +237 -0
  8. package/scanner/aspect-ratio-regression.ts +90 -0
  9. package/scanner/blob-placement-regression.ts +2 -2
  10. package/scanner/block-cache-regression.ts +195 -0
  11. package/scanner/bundle-size-regression.ts +50 -0
  12. package/scanner/child-sizing-matrix-regression.ts +303 -0
  13. package/scanner/cli.ts +342 -13
  14. package/scanner/component-scanner.ts +2108 -174
  15. package/scanner/component-sections-regression.ts +198 -0
  16. package/scanner/compound-classes-lookup-regression.ts +163 -0
  17. package/scanner/css-token-reader-regression.ts +7 -6
  18. package/scanner/css-token-reader.ts +152 -31
  19. package/scanner/cva-jsx-child-fallback-regression.ts +98 -0
  20. package/scanner/cva-master-icon-regression.ts +315 -0
  21. package/scanner/data-attr-prop-alias-regression.ts +129 -0
  22. package/scanner/explicit-size-root-regression.ts +102 -0
  23. package/scanner/font-family-extract-regression.ts +113 -0
  24. package/scanner/font-style-resolver-regression.ts +1 -1
  25. package/scanner/framework-adapter-shadcn-regression.ts +480 -0
  26. package/scanner/full-width-matrix-regression.ts +338 -0
  27. package/scanner/grid-cols-extraction-regression.ts +110 -0
  28. package/scanner/image-src-collector-regression.ts +204 -0
  29. package/scanner/inline-flex-regression.ts +235 -0
  30. package/scanner/input-range-regression.ts +217 -0
  31. package/scanner/instance-rendering-regression.ts +224 -0
  32. package/scanner/jsx-prop-unresolved-regression.ts +178 -0
  33. package/scanner/jsx-text-regression.ts +178 -0
  34. package/scanner/layout-alignment-regression.ts +108 -0
  35. package/scanner/layout-flex-regression.ts +90 -0
  36. package/scanner/layout-mode-regression.ts +71 -0
  37. package/scanner/layout-sizing-regression.ts +227 -0
  38. package/scanner/layout-spacing-regression.ts +135 -0
  39. package/scanner/local-const-className-regression.ts +331 -0
  40. package/scanner/percent-position-regression.ts +105 -0
  41. package/scanner/provider-cascade-regression.ts +224 -0
  42. package/scanner/provider-flatten-regression.ts +235 -0
  43. package/scanner/radial-gradient-regression.ts +1 -1
  44. package/scanner/render-prop-parser-regression.ts +161 -0
  45. package/scanner/ring-utility-regression.ts +153 -0
  46. package/scanner/sandbox-spread-regression.ts +125 -0
  47. package/scanner/selection-pressed-regression.ts +241 -0
  48. package/scanner/size-full-normalization-regression.ts +127 -0
  49. package/scanner/state-classification-regression.ts +175 -0
  50. package/scanner/story-diagnostics-regression.ts +216 -0
  51. package/scanner/story-dimensioning-regression.ts +298 -0
  52. package/scanner/story-render-strategy-regression.ts +205 -0
  53. package/scanner/stretch-to-parent-width-regression.ts +147 -0
  54. package/scanner/svg-fill-parent-regression.ts +98 -0
  55. package/scanner/svg-group-inheritance-regression.ts +166 -0
  56. package/scanner/svg-marker-inline-regression.ts +211 -0
  57. package/scanner/svg-marker-regression.ts +116 -0
  58. package/scanner/tailwind-parser.ts +46 -4
  59. package/scanner/text-resize-matrix-regression.ts +173 -0
  60. package/scanner/transform-math-regression.ts +1 -1
  61. package/scanner/types.ts +26 -2
  62. package/src/cache/frame-cache.ts +150 -0
  63. package/src/cache/index.ts +2 -0
  64. package/src/{component-defs.ts → components/component-defs.ts} +25 -10
  65. package/src/{component-gen.ts → components/component-gen.ts} +43 -116
  66. package/src/components/component-instance.ts +386 -0
  67. package/src/components/component-library.ts +44 -0
  68. package/src/components/component-lookup.ts +161 -0
  69. package/src/components/index.ts +7 -0
  70. package/src/components/scanner-types.ts +39 -0
  71. package/src/components/symbol-instance-policy.ts +312 -0
  72. package/src/design-system/block-cache.ts +130 -0
  73. package/src/design-system/component-sections.ts +107 -0
  74. package/src/design-system/cva-inference.ts +187 -0
  75. package/src/design-system/cva-master.ts +427 -0
  76. package/src/design-system/cva-utils.ts +29 -0
  77. package/src/design-system/design-system.ts +334 -0
  78. package/src/design-system/frame-stabilizers.ts +191 -0
  79. package/src/design-system/frame-utils.ts +46 -0
  80. package/src/design-system/generated-node.ts +84 -0
  81. package/src/design-system/icon-rendering.ts +229 -0
  82. package/src/design-system/index.ts +13 -0
  83. package/src/design-system/instance-rendering.ts +307 -0
  84. package/src/design-system/master-shared.ts +133 -0
  85. package/src/design-system/node-helpers.ts +237 -0
  86. package/src/design-system/node-variants.ts +196 -0
  87. package/src/design-system/non-cva-master.ts +104 -0
  88. package/src/design-system/portal-handling.ts +138 -0
  89. package/src/design-system/preview-builder.ts +738 -0
  90. package/src/{render-context.ts → design-system/render-context.ts} +32 -6
  91. package/src/design-system/render-prop-parser.ts +50 -0
  92. package/src/design-system/responsive-resolver.ts +180 -0
  93. package/src/design-system/selectable-state.ts +157 -0
  94. package/src/design-system/state-master.ts +267 -0
  95. package/src/design-system/state-utils.ts +15 -0
  96. package/src/design-system/story-builder-context.ts +40 -0
  97. package/src/design-system/story-builder.ts +1322 -0
  98. package/src/design-system/story-diagnostics.ts +80 -0
  99. package/src/design-system/story-dimensioning.ts +272 -0
  100. package/src/design-system/story-frames.ts +400 -0
  101. package/src/design-system/story-instance.ts +333 -0
  102. package/src/{story-layout.ts → design-system/story-layout.ts} +2 -2
  103. package/src/design-system/story-render-strategy.ts +150 -0
  104. package/src/design-system/story-tree-search.ts +110 -0
  105. package/src/design-system/symbol-fallback.ts +89 -0
  106. package/src/design-system/symbol-source.ts +172 -0
  107. package/src/design-system/table-helpers.ts +56 -0
  108. package/src/design-system/tag-predicates.ts +99 -0
  109. package/src/design-system/theme-context.ts +52 -0
  110. package/src/design-system/typography.ts +100 -0
  111. package/src/design-system/ui-builder.ts +2676 -0
  112. package/src/{clip-path-decorative.ts → effects/clip-path-decorative.ts} +11 -11
  113. package/src/effects/icon-builder.ts +1074 -0
  114. package/src/effects/index.ts +5 -0
  115. package/src/effects/portal-panel.ts +369 -0
  116. package/src/{radial-gradient.ts → effects/radial-gradient.ts} +1 -1
  117. package/src/framework-adapters/index.ts +47 -0
  118. package/src/framework-adapters/shadcn.ts +541 -0
  119. package/src/{github.ts → github/github.ts} +46 -21
  120. package/src/github/index.ts +1 -0
  121. package/src/layout/deferred-layout.ts +1556 -0
  122. package/src/layout/index.ts +24 -0
  123. package/src/layout/layout-parser.ts +375 -0
  124. package/src/{layout-utils.ts → layout/layout-utils.ts} +23 -17
  125. package/src/layout/parser/alignment.ts +54 -0
  126. package/src/layout/parser/flex.ts +59 -0
  127. package/src/layout/parser/index.ts +65 -0
  128. package/src/layout/parser/ir.ts +80 -0
  129. package/src/layout/parser/layout-mode.ts +57 -0
  130. package/src/layout/parser/sizing.ts +241 -0
  131. package/src/layout/parser/spacing-scale.ts +78 -0
  132. package/src/layout/parser/spacing.ts +134 -0
  133. package/src/layout/ring-utils.ts +120 -0
  134. package/src/layout/size-utils.ts +143 -0
  135. package/src/layout/text-resize-decision.ts +51 -0
  136. package/src/{width-solver.ts → layout/width-solver.ts} +168 -37
  137. package/src/main.ts +444 -162
  138. package/src/{config.ts → plugin/config.ts} +12 -12
  139. package/src/{dev-server.ts → plugin/dev-server.ts} +3 -3
  140. package/src/plugin/image-src-collector.ts +52 -0
  141. package/src/plugin/index.ts +3 -0
  142. package/src/plugin/packs/index.ts +2 -0
  143. package/src/{pack-provider.ts → plugin/packs/pack-provider.ts} +12 -12
  144. package/src/{packs.ts → plugin/packs/packs.ts} +22 -17
  145. package/src/render-engine-version.ts +2 -0
  146. package/src/tailwind/adapter-utils.ts +137 -0
  147. package/src/{class-utils.ts → tailwind/class-utils.ts} +33 -6
  148. package/src/tailwind/index.ts +8 -0
  149. package/src/tailwind/jsx-utils.ts +319 -0
  150. package/src/{node-ir.ts → tailwind/node-ir.ts} +208 -19
  151. package/src/{responsive-analyzer.ts → tailwind/responsive-analyzer.ts} +32 -2
  152. package/src/{state-analyzer.ts → tailwind/state-analyzer.ts} +71 -5
  153. package/src/{tailwind.ts → tailwind/tailwind.ts} +423 -674
  154. package/src/{utility-resolver.ts → tailwind/utility-resolver.ts} +27 -6
  155. package/src/{font-style-resolver.ts → text/font-style-resolver.ts} +0 -2
  156. package/src/text/index.ts +4 -0
  157. package/src/{inline-text.ts → text/inline-text.ts} +13 -13
  158. package/src/{text-builder.ts → text/text-builder.ts} +24 -7
  159. package/src/{text-line.ts → text/text-line.ts} +2 -2
  160. package/src/{change-detection.ts → tokens/change-detection.ts} +12 -12
  161. package/src/{color-resolver.ts → tokens/color-resolver.ts} +1 -6
  162. package/src/{colors.ts → tokens/colors.ts} +13 -6
  163. package/src/tokens/index.ts +6 -0
  164. package/src/{token-source.ts → tokens/token-source.ts} +4 -1
  165. package/src/{tokens.ts → tokens/tokens.ts} +116 -20
  166. package/src/{variables.ts → tokens/variables.ts} +447 -102
  167. package/templates/patch-tokens-route.ts +25 -6
  168. package/templates/scan-components-route.ts +26 -5
  169. package/ui.html +485 -37
  170. package/src/component-lookup.ts +0 -82
  171. package/src/design-system.ts +0 -59
  172. package/src/icon-builder.ts +0 -607
  173. package/src/layout-parser.ts +0 -667
  174. package/src/story-builder.ts +0 -1706
  175. package/src/ui-builder.ts +0 -1996
  176. /package/src/{image-cache.ts → cache/image-cache.ts} +0 -0
  177. /package/src/{blob-placement.ts → effects/blob-placement.ts} +0 -0
  178. /package/src/{transform-math.ts → tailwind/transform-math.ts} +0 -0
@@ -1,667 +0,0 @@
1
- /**
2
- * LayoutParser - Tailwind → Figma Auto-layout mapping
3
- *
4
- * Approach: Tailwind utilities → Layout IR → Figma Auto-layout
5
- *
6
- * The IR captures:
7
- * - layoutMode: NONE | HORIZONTAL | VERTICAL
8
- * - wrap: boolean
9
- * - gap: number
10
- * - padding: t/r/b/l
11
- * - mainAlign: MIN | CENTER | MAX | SPACE_BETWEEN
12
- * - crossAlign: MIN | CENTER | MAX | STRETCH
13
- * - sizing: FIXED | HUG | FILL (resolved per-axis based on parent context)
14
- */
15
-
16
- declare const figma: any;
17
-
18
- // Tailwind spacing scale (in px)
19
- // Based on official Tailwind CSS v3 documentation
20
- // 1rem = 16px, values use 0.25rem (4px) base unit
21
- const SPACING_SCALE: Record<string, number> = {
22
- 'px': 1, // Special 1px value
23
- '0': 0,
24
- '0.5': 2, // 0.125rem
25
- '1': 4,
26
- '1.5': 6,
27
- '2': 8,
28
- '2.5': 10,
29
- '3': 12,
30
- '3.5': 14,
31
- '4': 16,
32
- '5': 20,
33
- '6': 24,
34
- '7': 28,
35
- '8': 32,
36
- '9': 36,
37
- '10': 40,
38
- '11': 44,
39
- '12': 48,
40
- '14': 56,
41
- '16': 64,
42
- '20': 80,
43
- '24': 96,
44
- '28': 112,
45
- '32': 128,
46
- '36': 144,
47
- '40': 160,
48
- '44': 176,
49
- '48': 192,
50
- '52': 208,
51
- '56': 224,
52
- '60': 240,
53
- '64': 256,
54
- '72': 288,
55
- '80': 320,
56
- '96': 384,
57
- };
58
-
59
- // Tailwind fractional width/height utilities
60
- // These map to percentages in CSS, handled as proportional grow in Figma
61
- const FRACTIONAL_SIZES: Record<string, number> = {
62
- // Halves
63
- '1/2': 0.5,
64
- // Thirds
65
- '1/3': 1 / 3,
66
- '2/3': 2 / 3,
67
- // Quarters
68
- '1/4': 0.25,
69
- '2/4': 0.5,
70
- '3/4': 0.75,
71
- // Fifths
72
- '1/5': 0.2,
73
- '2/5': 0.4,
74
- '3/5': 0.6,
75
- '4/5': 0.8,
76
- // Sixths
77
- '1/6': 1 / 6,
78
- '2/6': 2 / 6,
79
- '3/6': 0.5,
80
- '4/6': 4 / 6,
81
- '5/6': 5 / 6,
82
- // Twelfths
83
- '1/12': 1 / 12,
84
- '2/12': 2 / 12,
85
- '3/12': 0.25,
86
- '4/12': 4 / 12,
87
- '5/12': 5 / 12,
88
- '6/12': 0.5,
89
- '7/12': 7 / 12,
90
- '8/12': 8 / 12,
91
- '9/12': 0.75,
92
- '10/12': 10 / 12,
93
- '11/12': 11 / 12,
94
- // Full
95
- 'full': 1.0,
96
- };
97
-
98
- /**
99
- * Sizing mode for a dimension
100
- * - FIXED: explicit size (w-20, h-10)
101
- * - HUG: shrink to content (default for text, no size class)
102
- * - FILL: expand to fill available space (flex-1, w-full in flex parent)
103
- */
104
- export type SizingMode = 'FIXED' | 'HUG' | 'FILL';
105
-
106
- /**
107
- * Intermediate Representation for layout
108
- * This captures the Tailwind intent before applying to Figma
109
- */
110
- export interface LayoutIR {
111
- // Container properties
112
- layoutMode: 'HORIZONTAL' | 'VERTICAL' | 'NONE';
113
- wrap: boolean;
114
- gap: number;
115
- gapX?: number;
116
- gapY?: number;
117
-
118
- // Padding
119
- paddingTop: number;
120
- paddingRight: number;
121
- paddingBottom: number;
122
- paddingLeft: number;
123
-
124
- // Alignment (for containers)
125
- mainAlign: 'MIN' | 'CENTER' | 'MAX' | 'SPACE_BETWEEN';
126
- crossAlign: 'MIN' | 'CENTER' | 'MAX' | 'STRETCH' | 'BASELINE';
127
-
128
- // Sizing (resolved based on context)
129
- widthMode: SizingMode;
130
- heightMode: SizingMode;
131
- fixedWidth?: number;
132
- fixedHeight?: number;
133
- widthFraction?: number; // 0.0-1.0 for fractional widths (w-1/2, w-1/3, etc.)
134
- heightFraction?: number; // 0.0-1.0 for fractional heights
135
-
136
- // Child-specific (applied when this node is a child of a flex parent)
137
- grow: number; // 0 = don't grow, 1 = grow
138
- selfAlign?: 'MIN' | 'CENTER' | 'MAX' | 'STRETCH';
139
- }
140
-
141
- export class LayoutParser {
142
- /**
143
- * Parse Tailwind classes into a Layout IR
144
- */
145
- static parseToIR(classes: string[]): LayoutIR {
146
- const ir: LayoutIR = {
147
- layoutMode: 'NONE',
148
- wrap: false,
149
- gap: 0,
150
- paddingTop: 0,
151
- paddingRight: 0,
152
- paddingBottom: 0,
153
- paddingLeft: 0,
154
- mainAlign: 'MIN',
155
- crossAlign: 'MIN',
156
- widthMode: 'HUG',
157
- heightMode: 'HUG',
158
- grow: 0,
159
- };
160
-
161
- // Pass 1: Determine layout mode
162
- ir.layoutMode = this.parseLayoutMode(classes);
163
-
164
- // Pass 2: Parse all properties
165
- this.parseGap(classes, ir);
166
- this.parsePadding(classes, ir);
167
- this.parseAlignment(classes, ir);
168
- this.parseSizing(classes, ir);
169
- this.parseChildProperties(classes, ir);
170
- this.parseWrap(classes, ir);
171
-
172
- return ir;
173
- }
174
-
175
- /**
176
- * Apply IR to a Figma frame (container properties)
177
- */
178
- static applyToFrame(frame: FrameNode, ir: LayoutIR): void {
179
- // Set layout mode - default to VERTICAL for standard document flow
180
- const layoutMode = ir.layoutMode !== 'NONE' ? ir.layoutMode : 'VERTICAL';
181
- frame.layoutMode = layoutMode;
182
-
183
- // Set base sizing modes to AUTO (hug content)
184
- frame.primaryAxisSizingMode = 'AUTO';
185
- frame.counterAxisSizingMode = 'AUTO';
186
-
187
- // Apply alignment
188
- frame.primaryAxisAlignItems = ir.mainAlign;
189
- frame.counterAxisAlignItems = ir.crossAlign === 'STRETCH' ? 'MIN' : ir.crossAlign;
190
-
191
- // Apply gap
192
- frame.itemSpacing = ir.gap;
193
-
194
- // Apply padding
195
- frame.paddingTop = ir.paddingTop;
196
- frame.paddingRight = ir.paddingRight;
197
- frame.paddingBottom = ir.paddingBottom;
198
- frame.paddingLeft = ir.paddingLeft;
199
-
200
- // Apply fixed dimensions if specified
201
- if (ir.widthMode === 'FIXED' && ir.fixedWidth !== undefined) {
202
- try {
203
- frame.resize(ir.fixedWidth, frame.height);
204
- // Width is fixed - set appropriate axis sizing
205
- if (layoutMode === 'HORIZONTAL') {
206
- frame.primaryAxisSizingMode = 'FIXED';
207
- } else {
208
- frame.counterAxisSizingMode = 'FIXED';
209
- }
210
- } catch (_e) {
211
- // Ignore resize errors
212
- }
213
- }
214
-
215
- if (ir.heightMode === 'FIXED' && ir.fixedHeight !== undefined) {
216
- try {
217
- frame.resize(frame.width, ir.fixedHeight);
218
- // Height is fixed - set appropriate axis sizing
219
- if (layoutMode === 'VERTICAL') {
220
- frame.primaryAxisSizingMode = 'FIXED';
221
- } else {
222
- frame.counterAxisSizingMode = 'FIXED';
223
- }
224
- } catch (_e) {
225
- // Ignore resize errors
226
- }
227
- }
228
-
229
- // Apply wrap if supported
230
- if (ir.wrap && 'layoutWrap' in frame) {
231
- (frame as any).layoutWrap = 'WRAP';
232
- if (ir.gapY !== undefined && 'counterAxisSpacing' in frame) {
233
- (frame as any).counterAxisSpacing = ir.gapY;
234
- }
235
- }
236
- }
237
-
238
- /**
239
- * Apply child properties based on parent context
240
- * MUST be called after child is appended to parent
241
- */
242
- static applyChildProperties(
243
- child: SceneNode,
244
- childClasses: string[],
245
- parent: FrameNode
246
- ): void {
247
- const ir = this.parseToIR(childClasses);
248
- const parentLayout = parent.layoutMode;
249
-
250
- // Skip if parent has no auto-layout
251
- if (parentLayout === 'NONE') return;
252
-
253
- // Guardrail: in HUG/AUTO parents, Figma's layoutGrow can collapse children.
254
- // When a child with layoutGrow=1 is appended to an AUTO parent, Figma immediately
255
- // resizes it to fill (= 1px minimum). Reset BOTH layoutGrow AND primaryAxisSizingMode
256
- // so Figma re-expands the child to hug its content again.
257
- if ('layoutGrow' in child && parent.primaryAxisSizingMode !== 'FIXED') {
258
- const prevGrow = (child as any).layoutGrow as number;
259
- (child as any).layoutGrow = 0;
260
- if (prevGrow > 0 && 'primaryAxisSizingMode' in child && 'layoutMode' in child) {
261
- const childLayout = (child as any).layoutMode;
262
- if (childLayout === 'VERTICAL' || childLayout === 'HORIZONTAL') {
263
- // Undo the FILL mode Figma internally set when the child was appended with layoutGrow>0.
264
- (child as any).primaryAxisSizingMode = 'AUTO';
265
- }
266
- }
267
- }
268
-
269
- // Apply grow (flex-1)
270
- // layoutGrow only makes sense when the parent has a FIXED primary axis size.
271
- // If the parent is AUTO-sized, layoutGrow collapses the child to height/width=0 in Figma.
272
- // Reset any layoutGrow that may have been set earlier (e.g. by applyTailwindStylesToFrame).
273
- if (ir.grow > 0 && 'layoutGrow' in child) {
274
- if (parent.primaryAxisSizingMode === 'FIXED') {
275
- (child as any).layoutGrow = ir.grow;
276
- } else {
277
- (child as any).layoutGrow = 0;
278
- }
279
- }
280
-
281
- // Apply self alignment
282
- if (ir.selfAlign !== undefined) {
283
- if (ir.selfAlign === 'STRETCH') {
284
- if ('layoutAlign' in child) (child as any).layoutAlign = 'STRETCH';
285
- } else {
286
- // MIN / CENTER / MAX: use counterAxisAlignSelf (layoutAlign = MIN/CENTER/MAX is deprecated)
287
- try { (child as any).counterAxisAlignSelf = ir.selfAlign; } catch (_err) { /* ignore */ }
288
- }
289
- }
290
-
291
- // Handle w-full based on parent layout direction
292
- if (childClasses.includes('w-full')) {
293
- if (parentLayout === 'HORIZONTAL') {
294
- // In horizontal parent, w-full means grow along primary axis
295
- if ('layoutGrow' in child) {
296
- (child as any).layoutGrow = 1;
297
- }
298
- } else if (parentLayout === 'VERTICAL') {
299
- // In vertical parent, w-full means stretch along counter axis
300
- if ('layoutAlign' in child) {
301
- (child as any).layoutAlign = 'STRETCH';
302
- }
303
- }
304
- }
305
-
306
- // Handle h-full based on parent layout direction
307
- if (childClasses.includes('h-full')) {
308
- if (parentLayout === 'VERTICAL') {
309
- // In vertical parent, h-full only makes sense when parent has a fixed height
310
- if ('layoutGrow' in child && parent.primaryAxisSizingMode === 'FIXED') {
311
- (child as any).layoutGrow = 1;
312
- }
313
- } else if (parentLayout === 'HORIZONTAL') {
314
- // In horizontal parent, h-full means stretch along counter axis
315
- if ('layoutAlign' in child) {
316
- (child as any).layoutAlign = 'STRETCH';
317
- }
318
- }
319
- }
320
-
321
- // Handle fractional widths (w-1/2, w-1/3, etc.)
322
- // In horizontal parent, use layoutGrow to distribute space proportionally
323
- if (ir.widthFraction !== undefined && parentLayout === 'HORIZONTAL') {
324
- if ('layoutGrow' in child) {
325
- // Use fraction as layoutGrow value (scaled to work with other siblings)
326
- // This gives proportional distribution when combined with other fractional widths
327
- (child as any).layoutGrow = ir.widthFraction;
328
- }
329
- }
330
-
331
- // Handle fractional heights (h-1/2, h-1/3, etc.) in vertical parent
332
- if (ir.heightFraction !== undefined && parentLayout === 'VERTICAL') {
333
- if ('layoutGrow' in child) {
334
- (child as any).layoutGrow = ir.heightFraction;
335
- }
336
- }
337
- }
338
-
339
- // ===========================================================================
340
- // Parsing helpers
341
- // ===========================================================================
342
-
343
- private static parseLayoutMode(classes: string[]): 'HORIZONTAL' | 'VERTICAL' | 'NONE' {
344
- // Keep class-order semantics so later responsive utilities override base ones.
345
- // Example: "flex flex-col sm:flex-row" becomes ["flex","flex-col","flex-row"] at sm.
346
- let mode: 'HORIZONTAL' | 'VERTICAL' | 'NONE' = 'NONE';
347
- for (const cls of classes) {
348
- if (cls === 'flex' || cls === 'inline-flex') {
349
- mode = 'HORIZONTAL'; // flex defaults to row
350
- continue;
351
- }
352
- if (cls === 'grid' || cls === 'inline-grid') {
353
- // grid without grid-cols-N is a single-column layout (like flex-col)
354
- // grid WITH grid-cols-N wraps horizontally — handled separately by markGridColumnsNode
355
- const hasColumns = classes.some(c => /^grid-cols-\d+$/.test(c));
356
- mode = hasColumns ? 'HORIZONTAL' : 'VERTICAL';
357
- continue;
358
- }
359
- if (cls === 'flex-col' || cls === 'flex-col-reverse') {
360
- mode = 'VERTICAL';
361
- continue;
362
- }
363
- if (cls === 'flex-row' || cls === 'flex-row-reverse') {
364
- mode = 'HORIZONTAL';
365
- }
366
- }
367
- return mode;
368
- }
369
-
370
- private static parseGap(classes: string[], ir: LayoutIR): void {
371
- for (const cls of classes) {
372
- // gap-* (both axes)
373
- const gapMatch = cls.match(/^gap-(\d+(?:\.\d+)?|\[.+\])$/);
374
- if (gapMatch) {
375
- const val = this.resolveSpacing(gapMatch[1]);
376
- ir.gap = val;
377
- ir.gapX = val;
378
- ir.gapY = val;
379
- continue;
380
- }
381
-
382
- // gap-x-* (horizontal gap)
383
- const gapXMatch = cls.match(/^gap-x-(\d+(?:\.\d+)?|\[.+\])$/);
384
- if (gapXMatch) {
385
- ir.gapX = this.resolveSpacing(gapXMatch[1]);
386
- // Use gap-x for horizontal layout primary spacing
387
- if (ir.layoutMode === 'HORIZONTAL') {
388
- ir.gap = ir.gapX;
389
- }
390
- continue;
391
- }
392
-
393
- // gap-y-* (vertical gap)
394
- const gapYMatch = cls.match(/^gap-y-(\d+(?:\.\d+)?|\[.+\])$/);
395
- if (gapYMatch) {
396
- ir.gapY = this.resolveSpacing(gapYMatch[1]);
397
- // Use gap-y for vertical layout primary spacing
398
- if (ir.layoutMode === 'VERTICAL') {
399
- ir.gap = ir.gapY;
400
- }
401
- continue;
402
- }
403
-
404
- // space-x-* → convert to gap (Tailwind uses child margins, but for Figma use gap)
405
- const spaceXMatch = cls.match(/^space-x-(\d+(?:\.\d+)?|\[.+\])$/);
406
- if (spaceXMatch && ir.layoutMode === 'HORIZONTAL') {
407
- ir.gap = this.resolveSpacing(spaceXMatch[1]);
408
- continue;
409
- }
410
-
411
- // space-y-* → convert to gap
412
- const spaceYMatch = cls.match(/^space-y-(\d+(?:\.\d+)?|\[.+\])$/);
413
- if (spaceYMatch && ir.layoutMode === 'VERTICAL') {
414
- ir.gap = this.resolveSpacing(spaceYMatch[1]);
415
- continue;
416
- }
417
- }
418
- }
419
-
420
- private static parsePadding(classes: string[], ir: LayoutIR): void {
421
- for (const cls of classes) {
422
- // p-* (all sides)
423
- const pMatch = cls.match(/^p-(\d+(?:\.\d+)?|\[.+\])$/);
424
- if (pMatch) {
425
- const val = this.resolveSpacing(pMatch[1]);
426
- ir.paddingTop = val;
427
- ir.paddingRight = val;
428
- ir.paddingBottom = val;
429
- ir.paddingLeft = val;
430
- continue;
431
- }
432
-
433
- // px-* (horizontal)
434
- const pxMatch = cls.match(/^px-(\d+(?:\.\d+)?|\[.+\])$/);
435
- if (pxMatch) {
436
- const val = this.resolveSpacing(pxMatch[1]);
437
- ir.paddingLeft = val;
438
- ir.paddingRight = val;
439
- continue;
440
- }
441
-
442
- // py-* (vertical)
443
- const pyMatch = cls.match(/^py-(\d+(?:\.\d+)?|\[.+\])$/);
444
- if (pyMatch) {
445
- const val = this.resolveSpacing(pyMatch[1]);
446
- ir.paddingTop = val;
447
- ir.paddingBottom = val;
448
- continue;
449
- }
450
-
451
- // Individual padding
452
- const ptMatch = cls.match(/^pt-(\d+(?:\.\d+)?|\[.+\])$/);
453
- if (ptMatch) {
454
- ir.paddingTop = this.resolveSpacing(ptMatch[1]);
455
- continue;
456
- }
457
- const prMatch = cls.match(/^pr-(\d+(?:\.\d+)?|\[.+\])$/);
458
- if (prMatch) {
459
- ir.paddingRight = this.resolveSpacing(prMatch[1]);
460
- continue;
461
- }
462
- const pbMatch = cls.match(/^pb-(\d+(?:\.\d+)?|\[.+\])$/);
463
- if (pbMatch) {
464
- ir.paddingBottom = this.resolveSpacing(pbMatch[1]);
465
- continue;
466
- }
467
- const plMatch = cls.match(/^pl-(\d+(?:\.\d+)?|\[.+\])$/);
468
- if (plMatch) {
469
- ir.paddingLeft = this.resolveSpacing(plMatch[1]);
470
- continue;
471
- }
472
- }
473
- }
474
-
475
- private static parseAlignment(classes: string[], ir: LayoutIR): void {
476
- for (const cls of classes) {
477
- // Primary axis (justify-*)
478
- if (cls === 'justify-start') ir.mainAlign = 'MIN';
479
- else if (cls === 'justify-center') ir.mainAlign = 'CENTER';
480
- else if (cls === 'justify-end') ir.mainAlign = 'MAX';
481
- else if (cls === 'justify-between') ir.mainAlign = 'SPACE_BETWEEN';
482
-
483
- // Counter axis (items-*)
484
- if (cls === 'items-start') ir.crossAlign = 'MIN';
485
- else if (cls === 'items-center') ir.crossAlign = 'CENTER';
486
- else if (cls === 'items-end') ir.crossAlign = 'MAX';
487
- else if (cls === 'items-stretch') ir.crossAlign = 'STRETCH';
488
- else if (cls === 'items-baseline') ir.crossAlign = 'BASELINE';
489
- }
490
- }
491
-
492
- private static parseSizing(classes: string[], ir: LayoutIR): void {
493
- for (const cls of classes) {
494
- // Fixed width: w-20 (scale), w-[100px] (arbitrary)
495
- const wScaleMatch = cls.match(/^w-(\d+(?:\.\d+)?)$/);
496
- if (wScaleMatch) {
497
- const val = this.resolveSpacing(wScaleMatch[1]);
498
- if (val > 0) {
499
- ir.widthMode = 'FIXED';
500
- ir.fixedWidth = val;
501
- }
502
- continue;
503
- }
504
-
505
- const wArbitraryMatch = cls.match(/^w-\[(\d+(?:\.\d+)?)(px|rem|em)?\]$/);
506
- if (wArbitraryMatch) {
507
- let val = parseFloat(wArbitraryMatch[1]);
508
- const unit = wArbitraryMatch[2] || 'px';
509
- if (unit === 'rem' || unit === 'em') val *= 16;
510
- ir.widthMode = 'FIXED';
511
- ir.fixedWidth = val;
512
- continue;
513
- }
514
-
515
- // Fractional width: w-1/2, w-1/3, w-2/3, etc.
516
- const wFractionMatch = cls.match(/^w-(\d+\/\d+)$/);
517
- if (wFractionMatch) {
518
- const fraction = FRACTIONAL_SIZES[wFractionMatch[1]];
519
- if (fraction !== undefined) {
520
- ir.widthMode = 'FILL';
521
- ir.widthFraction = fraction;
522
- }
523
- continue;
524
- }
525
-
526
- // Fixed height: h-20 (scale), h-[100px] (arbitrary)
527
- const hScaleMatch = cls.match(/^h-(\d+(?:\.\d+)?)$/);
528
- if (hScaleMatch) {
529
- const val = this.resolveSpacing(hScaleMatch[1]);
530
- if (val > 0) {
531
- ir.heightMode = 'FIXED';
532
- ir.fixedHeight = val;
533
- }
534
- continue;
535
- }
536
-
537
- const hArbitraryMatch = cls.match(/^h-\[(\d+(?:\.\d+)?)(px|rem|em)?\]$/);
538
- if (hArbitraryMatch) {
539
- let val = parseFloat(hArbitraryMatch[1]);
540
- const unit = hArbitraryMatch[2] || 'px';
541
- if (unit === 'rem' || unit === 'em') val *= 16;
542
- ir.heightMode = 'FIXED';
543
- ir.fixedHeight = val;
544
- continue;
545
- }
546
-
547
- // Fractional height: h-1/2, h-1/3, h-2/3, etc.
548
- const hFractionMatch = cls.match(/^h-(\d+\/\d+)$/);
549
- if (hFractionMatch) {
550
- const fraction = FRACTIONAL_SIZES[hFractionMatch[1]];
551
- if (fraction !== undefined) {
552
- ir.heightMode = 'FILL';
553
- ir.heightFraction = fraction;
554
- }
555
- continue;
556
- }
557
-
558
- // Size (both dimensions): size-20
559
- const sizeMatch = cls.match(/^size-(\d+(?:\.\d+)?)$/);
560
- if (sizeMatch) {
561
- const val = this.resolveSpacing(sizeMatch[1]);
562
- if (val > 0) {
563
- ir.widthMode = 'FIXED';
564
- ir.heightMode = 'FIXED';
565
- ir.fixedWidth = val;
566
- ir.fixedHeight = val;
567
- }
568
- continue;
569
- }
570
-
571
- // Fill width (determined by parent context in applyChildProperties)
572
- // Just mark the intent here
573
- if (cls === 'w-full') {
574
- ir.widthMode = 'FILL';
575
- }
576
- if (cls === 'h-full') {
577
- ir.heightMode = 'FILL';
578
- }
579
-
580
- // Max-width with fixed value
581
- const maxWMatch = cls.match(/^max-w-(\d+(?:\.\d+)?)$/);
582
- if (maxWMatch) {
583
- const val = this.resolveSpacing(maxWMatch[1]);
584
- if (val > 0) {
585
- // Treat max-w as a fixed width constraint for Figma
586
- ir.widthMode = 'FIXED';
587
- ir.fixedWidth = val;
588
- }
589
- continue;
590
- }
591
-
592
- // Named max-width (max-w-xl, max-w-2xl, etc.)
593
- const maxWNamedMatch = cls.match(/^max-w-(xs|sm|md|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|full|prose|screen-sm|screen-md|screen-lg|screen-xl|screen-2xl)$/);
594
- if (maxWNamedMatch) {
595
- const maxWidths: Record<string, number> = {
596
- 'xs': 320, 'sm': 384, 'md': 448, 'lg': 512, 'xl': 576,
597
- '2xl': 672, '3xl': 768, '4xl': 896, '5xl': 1024, '6xl': 1152, '7xl': 1280,
598
- 'prose': 640,
599
- 'screen-sm': 640, 'screen-md': 768, 'screen-lg': 1024, 'screen-xl': 1280, 'screen-2xl': 1536,
600
- };
601
- const val = maxWidths[maxWNamedMatch[1]];
602
- if (val) {
603
- ir.widthMode = 'FIXED';
604
- ir.fixedWidth = val;
605
- }
606
- continue;
607
- }
608
- }
609
- }
610
-
611
- private static parseChildProperties(classes: string[], ir: LayoutIR): void {
612
- for (const cls of classes) {
613
- // Grow: flex-1, flex-grow, grow
614
- if (cls === 'flex-1' || cls === 'flex-grow' || cls === 'grow') {
615
- ir.grow = 1;
616
- continue;
617
- }
618
- if (cls === 'flex-grow-0' || cls === 'grow-0') {
619
- ir.grow = 0;
620
- continue;
621
- }
622
-
623
- // Self alignment
624
- if (cls === 'self-start') ir.selfAlign = 'MIN';
625
- else if (cls === 'self-center') ir.selfAlign = 'CENTER';
626
- else if (cls === 'self-end') ir.selfAlign = 'MAX';
627
- else if (cls === 'self-stretch') ir.selfAlign = 'STRETCH';
628
- else if (cls === 'self-auto') ir.selfAlign = undefined;
629
- }
630
- }
631
-
632
- private static parseWrap(classes: string[], ir: LayoutIR): void {
633
- if (classes.includes('flex-wrap') || classes.includes('flex-wrap-reverse')) {
634
- ir.wrap = true;
635
- }
636
- }
637
-
638
- /**
639
- * Resolve a Tailwind spacing value to pixels
640
- */
641
- private static resolveSpacing(value: string): number {
642
- // Arbitrary value [Xpx]
643
- if (value.startsWith('[') && value.endsWith(']')) {
644
- const inner = value.slice(1, -1);
645
- const match = inner.match(/^(\d+(?:\.\d+)?)(px|rem|em)?$/);
646
- if (match) {
647
- let num = parseFloat(match[1]);
648
- const unit = match[2] || 'px';
649
- if (unit === 'rem' || unit === 'em') num *= 16;
650
- return num;
651
- }
652
- }
653
-
654
- // Scale value
655
- if (SPACING_SCALE[value] !== undefined) {
656
- return SPACING_SCALE[value];
657
- }
658
-
659
- // Parse as number and multiply by 4 (Tailwind default)
660
- const num = parseFloat(value);
661
- if (!isNaN(num)) {
662
- return num * 4;
663
- }
664
-
665
- return 0;
666
- }
667
- }