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.
- package/README.md +108 -25
- package/bin/inkbridge.mjs +354 -83
- package/code.js +40 -11802
- package/manifest.json +1 -0
- package/package.json +74 -23
- package/scanner/adapter-utils-regression.ts +159 -0
- package/scanner/aspect-percent-position-regression.ts +237 -0
- package/scanner/aspect-ratio-regression.ts +90 -0
- package/scanner/blob-placement-regression.ts +2 -2
- package/scanner/block-cache-regression.ts +195 -0
- package/scanner/bundle-size-regression.ts +50 -0
- package/scanner/child-sizing-matrix-regression.ts +303 -0
- package/scanner/cli.ts +342 -13
- package/scanner/component-scanner.ts +2108 -174
- package/scanner/component-sections-regression.ts +198 -0
- package/scanner/compound-classes-lookup-regression.ts +163 -0
- package/scanner/css-token-reader-regression.ts +7 -6
- package/scanner/css-token-reader.ts +152 -31
- package/scanner/cva-jsx-child-fallback-regression.ts +98 -0
- package/scanner/cva-master-icon-regression.ts +315 -0
- package/scanner/data-attr-prop-alias-regression.ts +129 -0
- package/scanner/explicit-size-root-regression.ts +102 -0
- package/scanner/font-family-extract-regression.ts +113 -0
- package/scanner/font-style-resolver-regression.ts +1 -1
- package/scanner/framework-adapter-shadcn-regression.ts +480 -0
- package/scanner/full-width-matrix-regression.ts +338 -0
- package/scanner/grid-cols-extraction-regression.ts +110 -0
- package/scanner/image-src-collector-regression.ts +204 -0
- package/scanner/inline-flex-regression.ts +235 -0
- package/scanner/input-range-regression.ts +217 -0
- package/scanner/instance-rendering-regression.ts +224 -0
- package/scanner/jsx-prop-unresolved-regression.ts +178 -0
- package/scanner/jsx-text-regression.ts +178 -0
- package/scanner/layout-alignment-regression.ts +108 -0
- package/scanner/layout-flex-regression.ts +90 -0
- package/scanner/layout-mode-regression.ts +71 -0
- package/scanner/layout-sizing-regression.ts +227 -0
- package/scanner/layout-spacing-regression.ts +135 -0
- package/scanner/local-const-className-regression.ts +331 -0
- package/scanner/percent-position-regression.ts +105 -0
- package/scanner/provider-cascade-regression.ts +224 -0
- package/scanner/provider-flatten-regression.ts +235 -0
- package/scanner/radial-gradient-regression.ts +1 -1
- package/scanner/render-prop-parser-regression.ts +161 -0
- package/scanner/ring-utility-regression.ts +153 -0
- package/scanner/sandbox-spread-regression.ts +125 -0
- package/scanner/selection-pressed-regression.ts +241 -0
- package/scanner/size-full-normalization-regression.ts +127 -0
- package/scanner/state-classification-regression.ts +175 -0
- package/scanner/story-diagnostics-regression.ts +216 -0
- package/scanner/story-dimensioning-regression.ts +298 -0
- package/scanner/story-render-strategy-regression.ts +205 -0
- package/scanner/stretch-to-parent-width-regression.ts +147 -0
- package/scanner/svg-fill-parent-regression.ts +98 -0
- package/scanner/svg-group-inheritance-regression.ts +166 -0
- package/scanner/svg-marker-inline-regression.ts +211 -0
- package/scanner/svg-marker-regression.ts +116 -0
- package/scanner/tailwind-parser.ts +46 -4
- package/scanner/text-resize-matrix-regression.ts +173 -0
- package/scanner/transform-math-regression.ts +1 -1
- package/scanner/types.ts +26 -2
- package/src/cache/frame-cache.ts +150 -0
- package/src/cache/index.ts +2 -0
- package/src/{component-defs.ts → components/component-defs.ts} +25 -10
- package/src/{component-gen.ts → components/component-gen.ts} +43 -116
- package/src/components/component-instance.ts +386 -0
- package/src/components/component-library.ts +44 -0
- package/src/components/component-lookup.ts +161 -0
- package/src/components/index.ts +7 -0
- package/src/components/scanner-types.ts +39 -0
- package/src/components/symbol-instance-policy.ts +312 -0
- package/src/design-system/block-cache.ts +130 -0
- package/src/design-system/component-sections.ts +107 -0
- package/src/design-system/cva-inference.ts +187 -0
- package/src/design-system/cva-master.ts +427 -0
- package/src/design-system/cva-utils.ts +29 -0
- package/src/design-system/design-system.ts +334 -0
- package/src/design-system/frame-stabilizers.ts +191 -0
- package/src/design-system/frame-utils.ts +46 -0
- package/src/design-system/generated-node.ts +84 -0
- package/src/design-system/icon-rendering.ts +229 -0
- package/src/design-system/index.ts +13 -0
- package/src/design-system/instance-rendering.ts +307 -0
- package/src/design-system/master-shared.ts +133 -0
- package/src/design-system/node-helpers.ts +237 -0
- package/src/design-system/node-variants.ts +196 -0
- package/src/design-system/non-cva-master.ts +104 -0
- package/src/design-system/portal-handling.ts +138 -0
- package/src/design-system/preview-builder.ts +738 -0
- package/src/{render-context.ts → design-system/render-context.ts} +32 -6
- package/src/design-system/render-prop-parser.ts +50 -0
- package/src/design-system/responsive-resolver.ts +180 -0
- package/src/design-system/selectable-state.ts +157 -0
- package/src/design-system/state-master.ts +267 -0
- package/src/design-system/state-utils.ts +15 -0
- package/src/design-system/story-builder-context.ts +40 -0
- package/src/design-system/story-builder.ts +1322 -0
- package/src/design-system/story-diagnostics.ts +80 -0
- package/src/design-system/story-dimensioning.ts +272 -0
- package/src/design-system/story-frames.ts +400 -0
- package/src/design-system/story-instance.ts +333 -0
- package/src/{story-layout.ts → design-system/story-layout.ts} +2 -2
- package/src/design-system/story-render-strategy.ts +150 -0
- package/src/design-system/story-tree-search.ts +110 -0
- package/src/design-system/symbol-fallback.ts +89 -0
- package/src/design-system/symbol-source.ts +172 -0
- package/src/design-system/table-helpers.ts +56 -0
- package/src/design-system/tag-predicates.ts +99 -0
- package/src/design-system/theme-context.ts +52 -0
- package/src/design-system/typography.ts +100 -0
- package/src/design-system/ui-builder.ts +2676 -0
- package/src/{clip-path-decorative.ts → effects/clip-path-decorative.ts} +11 -11
- package/src/effects/icon-builder.ts +1074 -0
- package/src/effects/index.ts +5 -0
- package/src/effects/portal-panel.ts +369 -0
- package/src/{radial-gradient.ts → effects/radial-gradient.ts} +1 -1
- package/src/framework-adapters/index.ts +47 -0
- package/src/framework-adapters/shadcn.ts +541 -0
- package/src/{github.ts → github/github.ts} +46 -21
- package/src/github/index.ts +1 -0
- package/src/layout/deferred-layout.ts +1556 -0
- package/src/layout/index.ts +24 -0
- package/src/layout/layout-parser.ts +375 -0
- package/src/{layout-utils.ts → layout/layout-utils.ts} +23 -17
- package/src/layout/parser/alignment.ts +54 -0
- package/src/layout/parser/flex.ts +59 -0
- package/src/layout/parser/index.ts +65 -0
- package/src/layout/parser/ir.ts +80 -0
- package/src/layout/parser/layout-mode.ts +57 -0
- package/src/layout/parser/sizing.ts +241 -0
- package/src/layout/parser/spacing-scale.ts +78 -0
- package/src/layout/parser/spacing.ts +134 -0
- package/src/layout/ring-utils.ts +120 -0
- package/src/layout/size-utils.ts +143 -0
- package/src/layout/text-resize-decision.ts +51 -0
- package/src/{width-solver.ts → layout/width-solver.ts} +168 -37
- package/src/main.ts +444 -162
- package/src/{config.ts → plugin/config.ts} +12 -12
- package/src/{dev-server.ts → plugin/dev-server.ts} +3 -3
- package/src/plugin/image-src-collector.ts +52 -0
- package/src/plugin/index.ts +3 -0
- package/src/plugin/packs/index.ts +2 -0
- package/src/{pack-provider.ts → plugin/packs/pack-provider.ts} +12 -12
- package/src/{packs.ts → plugin/packs/packs.ts} +22 -17
- package/src/render-engine-version.ts +2 -0
- package/src/tailwind/adapter-utils.ts +137 -0
- package/src/{class-utils.ts → tailwind/class-utils.ts} +33 -6
- package/src/tailwind/index.ts +8 -0
- package/src/tailwind/jsx-utils.ts +319 -0
- package/src/{node-ir.ts → tailwind/node-ir.ts} +208 -19
- package/src/{responsive-analyzer.ts → tailwind/responsive-analyzer.ts} +32 -2
- package/src/{state-analyzer.ts → tailwind/state-analyzer.ts} +71 -5
- package/src/{tailwind.ts → tailwind/tailwind.ts} +423 -674
- package/src/{utility-resolver.ts → tailwind/utility-resolver.ts} +27 -6
- package/src/{font-style-resolver.ts → text/font-style-resolver.ts} +0 -2
- package/src/text/index.ts +4 -0
- package/src/{inline-text.ts → text/inline-text.ts} +13 -13
- package/src/{text-builder.ts → text/text-builder.ts} +24 -7
- package/src/{text-line.ts → text/text-line.ts} +2 -2
- package/src/{change-detection.ts → tokens/change-detection.ts} +12 -12
- package/src/{color-resolver.ts → tokens/color-resolver.ts} +1 -6
- package/src/{colors.ts → tokens/colors.ts} +13 -6
- package/src/tokens/index.ts +6 -0
- package/src/{token-source.ts → tokens/token-source.ts} +4 -1
- package/src/{tokens.ts → tokens/tokens.ts} +116 -20
- package/src/{variables.ts → tokens/variables.ts} +447 -102
- package/templates/patch-tokens-route.ts +25 -6
- package/templates/scan-components-route.ts +26 -5
- package/ui.html +485 -37
- package/src/component-lookup.ts +0 -82
- package/src/design-system.ts +0 -59
- package/src/icon-builder.ts +0 -607
- package/src/layout-parser.ts +0 -667
- package/src/story-builder.ts +0 -1706
- package/src/ui-builder.ts +0 -1996
- /package/src/{image-cache.ts → cache/image-cache.ts} +0 -0
- /package/src/{blob-placement.ts → effects/blob-placement.ts} +0 -0
- /package/src/{transform-math.ts → tailwind/transform-math.ts} +0 -0
package/src/layout-parser.ts
DELETED
|
@@ -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
|
-
}
|