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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/README.md +108 -25
  2. package/bin/inkbridge.mjs +354 -83
  3. package/code.js +40 -11802
  4. package/manifest.json +1 -0
  5. package/package.json +74 -23
  6. package/scanner/adapter-utils-regression.ts +159 -0
  7. package/scanner/aspect-percent-position-regression.ts +237 -0
  8. package/scanner/aspect-ratio-regression.ts +90 -0
  9. package/scanner/blob-placement-regression.ts +2 -2
  10. package/scanner/block-cache-regression.ts +195 -0
  11. package/scanner/bundle-size-regression.ts +50 -0
  12. package/scanner/child-sizing-matrix-regression.ts +303 -0
  13. package/scanner/cli.ts +342 -13
  14. package/scanner/component-scanner.ts +2108 -174
  15. package/scanner/component-sections-regression.ts +198 -0
  16. package/scanner/compound-classes-lookup-regression.ts +163 -0
  17. package/scanner/css-token-reader-regression.ts +7 -6
  18. package/scanner/css-token-reader.ts +152 -31
  19. package/scanner/cva-jsx-child-fallback-regression.ts +98 -0
  20. package/scanner/cva-master-icon-regression.ts +315 -0
  21. package/scanner/data-attr-prop-alias-regression.ts +129 -0
  22. package/scanner/explicit-size-root-regression.ts +102 -0
  23. package/scanner/font-family-extract-regression.ts +113 -0
  24. package/scanner/font-style-resolver-regression.ts +1 -1
  25. package/scanner/framework-adapter-shadcn-regression.ts +480 -0
  26. package/scanner/full-width-matrix-regression.ts +338 -0
  27. package/scanner/grid-cols-extraction-regression.ts +110 -0
  28. package/scanner/image-src-collector-regression.ts +204 -0
  29. package/scanner/inline-flex-regression.ts +235 -0
  30. package/scanner/input-range-regression.ts +217 -0
  31. package/scanner/instance-rendering-regression.ts +224 -0
  32. package/scanner/jsx-prop-unresolved-regression.ts +178 -0
  33. package/scanner/jsx-text-regression.ts +178 -0
  34. package/scanner/layout-alignment-regression.ts +108 -0
  35. package/scanner/layout-flex-regression.ts +90 -0
  36. package/scanner/layout-mode-regression.ts +71 -0
  37. package/scanner/layout-sizing-regression.ts +227 -0
  38. package/scanner/layout-spacing-regression.ts +135 -0
  39. package/scanner/local-const-className-regression.ts +331 -0
  40. package/scanner/percent-position-regression.ts +105 -0
  41. package/scanner/provider-cascade-regression.ts +224 -0
  42. package/scanner/provider-flatten-regression.ts +235 -0
  43. package/scanner/radial-gradient-regression.ts +1 -1
  44. package/scanner/render-prop-parser-regression.ts +161 -0
  45. package/scanner/ring-utility-regression.ts +153 -0
  46. package/scanner/sandbox-spread-regression.ts +125 -0
  47. package/scanner/selection-pressed-regression.ts +241 -0
  48. package/scanner/size-full-normalization-regression.ts +127 -0
  49. package/scanner/state-classification-regression.ts +175 -0
  50. package/scanner/story-diagnostics-regression.ts +216 -0
  51. package/scanner/story-dimensioning-regression.ts +298 -0
  52. package/scanner/story-render-strategy-regression.ts +205 -0
  53. package/scanner/stretch-to-parent-width-regression.ts +147 -0
  54. package/scanner/svg-fill-parent-regression.ts +98 -0
  55. package/scanner/svg-group-inheritance-regression.ts +166 -0
  56. package/scanner/svg-marker-inline-regression.ts +211 -0
  57. package/scanner/svg-marker-regression.ts +116 -0
  58. package/scanner/tailwind-parser.ts +46 -4
  59. package/scanner/text-resize-matrix-regression.ts +173 -0
  60. package/scanner/transform-math-regression.ts +1 -1
  61. package/scanner/types.ts +26 -2
  62. package/src/cache/frame-cache.ts +150 -0
  63. package/src/cache/index.ts +2 -0
  64. package/src/{component-defs.ts → components/component-defs.ts} +25 -10
  65. package/src/{component-gen.ts → components/component-gen.ts} +43 -116
  66. package/src/components/component-instance.ts +386 -0
  67. package/src/components/component-library.ts +44 -0
  68. package/src/components/component-lookup.ts +161 -0
  69. package/src/components/index.ts +7 -0
  70. package/src/components/scanner-types.ts +39 -0
  71. package/src/components/symbol-instance-policy.ts +312 -0
  72. package/src/design-system/block-cache.ts +130 -0
  73. package/src/design-system/component-sections.ts +107 -0
  74. package/src/design-system/cva-inference.ts +187 -0
  75. package/src/design-system/cva-master.ts +427 -0
  76. package/src/design-system/cva-utils.ts +29 -0
  77. package/src/design-system/design-system.ts +334 -0
  78. package/src/design-system/frame-stabilizers.ts +191 -0
  79. package/src/design-system/frame-utils.ts +46 -0
  80. package/src/design-system/generated-node.ts +84 -0
  81. package/src/design-system/icon-rendering.ts +229 -0
  82. package/src/design-system/index.ts +13 -0
  83. package/src/design-system/instance-rendering.ts +307 -0
  84. package/src/design-system/master-shared.ts +133 -0
  85. package/src/design-system/node-helpers.ts +237 -0
  86. package/src/design-system/node-variants.ts +196 -0
  87. package/src/design-system/non-cva-master.ts +104 -0
  88. package/src/design-system/portal-handling.ts +138 -0
  89. package/src/design-system/preview-builder.ts +738 -0
  90. package/src/{render-context.ts → design-system/render-context.ts} +32 -6
  91. package/src/design-system/render-prop-parser.ts +50 -0
  92. package/src/design-system/responsive-resolver.ts +180 -0
  93. package/src/design-system/selectable-state.ts +157 -0
  94. package/src/design-system/state-master.ts +267 -0
  95. package/src/design-system/state-utils.ts +15 -0
  96. package/src/design-system/story-builder-context.ts +40 -0
  97. package/src/design-system/story-builder.ts +1322 -0
  98. package/src/design-system/story-diagnostics.ts +80 -0
  99. package/src/design-system/story-dimensioning.ts +272 -0
  100. package/src/design-system/story-frames.ts +400 -0
  101. package/src/design-system/story-instance.ts +333 -0
  102. package/src/{story-layout.ts → design-system/story-layout.ts} +2 -2
  103. package/src/design-system/story-render-strategy.ts +150 -0
  104. package/src/design-system/story-tree-search.ts +110 -0
  105. package/src/design-system/symbol-fallback.ts +89 -0
  106. package/src/design-system/symbol-source.ts +172 -0
  107. package/src/design-system/table-helpers.ts +56 -0
  108. package/src/design-system/tag-predicates.ts +99 -0
  109. package/src/design-system/theme-context.ts +52 -0
  110. package/src/design-system/typography.ts +100 -0
  111. package/src/design-system/ui-builder.ts +2676 -0
  112. package/src/{clip-path-decorative.ts → effects/clip-path-decorative.ts} +11 -11
  113. package/src/effects/icon-builder.ts +1074 -0
  114. package/src/effects/index.ts +5 -0
  115. package/src/effects/portal-panel.ts +369 -0
  116. package/src/{radial-gradient.ts → effects/radial-gradient.ts} +1 -1
  117. package/src/framework-adapters/index.ts +47 -0
  118. package/src/framework-adapters/shadcn.ts +541 -0
  119. package/src/{github.ts → github/github.ts} +46 -21
  120. package/src/github/index.ts +1 -0
  121. package/src/layout/deferred-layout.ts +1556 -0
  122. package/src/layout/index.ts +24 -0
  123. package/src/layout/layout-parser.ts +375 -0
  124. package/src/{layout-utils.ts → layout/layout-utils.ts} +23 -17
  125. package/src/layout/parser/alignment.ts +54 -0
  126. package/src/layout/parser/flex.ts +59 -0
  127. package/src/layout/parser/index.ts +65 -0
  128. package/src/layout/parser/ir.ts +80 -0
  129. package/src/layout/parser/layout-mode.ts +57 -0
  130. package/src/layout/parser/sizing.ts +241 -0
  131. package/src/layout/parser/spacing-scale.ts +78 -0
  132. package/src/layout/parser/spacing.ts +134 -0
  133. package/src/layout/ring-utils.ts +120 -0
  134. package/src/layout/size-utils.ts +143 -0
  135. package/src/layout/text-resize-decision.ts +51 -0
  136. package/src/{width-solver.ts → layout/width-solver.ts} +168 -37
  137. package/src/main.ts +444 -162
  138. package/src/{config.ts → plugin/config.ts} +12 -12
  139. package/src/{dev-server.ts → plugin/dev-server.ts} +3 -3
  140. package/src/plugin/image-src-collector.ts +52 -0
  141. package/src/plugin/index.ts +3 -0
  142. package/src/plugin/packs/index.ts +2 -0
  143. package/src/{pack-provider.ts → plugin/packs/pack-provider.ts} +12 -12
  144. package/src/{packs.ts → plugin/packs/packs.ts} +22 -17
  145. package/src/render-engine-version.ts +2 -0
  146. package/src/tailwind/adapter-utils.ts +137 -0
  147. package/src/{class-utils.ts → tailwind/class-utils.ts} +33 -6
  148. package/src/tailwind/index.ts +8 -0
  149. package/src/tailwind/jsx-utils.ts +319 -0
  150. package/src/{node-ir.ts → tailwind/node-ir.ts} +208 -19
  151. package/src/{responsive-analyzer.ts → tailwind/responsive-analyzer.ts} +32 -2
  152. package/src/{state-analyzer.ts → tailwind/state-analyzer.ts} +71 -5
  153. package/src/{tailwind.ts → tailwind/tailwind.ts} +423 -674
  154. package/src/{utility-resolver.ts → tailwind/utility-resolver.ts} +27 -6
  155. package/src/{font-style-resolver.ts → text/font-style-resolver.ts} +0 -2
  156. package/src/text/index.ts +4 -0
  157. package/src/{inline-text.ts → text/inline-text.ts} +13 -13
  158. package/src/{text-builder.ts → text/text-builder.ts} +24 -7
  159. package/src/{text-line.ts → text/text-line.ts} +2 -2
  160. package/src/{change-detection.ts → tokens/change-detection.ts} +12 -12
  161. package/src/{color-resolver.ts → tokens/color-resolver.ts} +1 -6
  162. package/src/{colors.ts → tokens/colors.ts} +13 -6
  163. package/src/tokens/index.ts +6 -0
  164. package/src/{token-source.ts → tokens/token-source.ts} +4 -1
  165. package/src/{tokens.ts → tokens/tokens.ts} +116 -20
  166. package/src/{variables.ts → tokens/variables.ts} +447 -102
  167. package/templates/patch-tokens-route.ts +25 -6
  168. package/templates/scan-components-route.ts +26 -5
  169. package/ui.html +485 -37
  170. package/src/component-lookup.ts +0 -82
  171. package/src/design-system.ts +0 -59
  172. package/src/icon-builder.ts +0 -607
  173. package/src/layout-parser.ts +0 -667
  174. package/src/story-builder.ts +0 -1706
  175. package/src/ui-builder.ts +0 -1996
  176. /package/src/{image-cache.ts → cache/image-cache.ts} +0 -0
  177. /package/src/{blob-placement.ts → effects/blob-placement.ts} +0 -0
  178. /package/src/{transform-math.ts → tailwind/transform-math.ts} +0 -0
@@ -0,0 +1,24 @@
1
+ export * from './layout-parser';
2
+ // Direct access to the free parsing API + IR types — preferred for
3
+ // new code (the LayoutParser class re-exported above remains for
4
+ // existing consumers).
5
+ export {
6
+ parseToIR,
7
+ parseLayoutMode,
8
+ parseGap,
9
+ parsePadding,
10
+ parseSpacing,
11
+ parseSizing,
12
+ parseAlignment,
13
+ parseFlexChildren,
14
+ parseWrap,
15
+ resolveSpacing,
16
+ makeEmptyIR,
17
+ } from './parser';
18
+ export type { LayoutIR, SizingMode } from './parser';
19
+ export * from './width-solver';
20
+ export * from './deferred-layout';
21
+ export * from './layout-utils';
22
+ export * from './size-utils';
23
+ export * from './ring-utils';
24
+ export * from './text-resize-decision';
@@ -0,0 +1,375 @@
1
+ /**
2
+ * LayoutParser — IR → Figma Auto-layout application.
3
+ *
4
+ * Pipeline: Tailwind utilities → LayoutIR → Figma Auto-layout.
5
+ *
6
+ * The PARSING side (Tailwind → IR) lives under `./parser/`, with one
7
+ * file per Tailwind axis (layout-mode, spacing, sizing, alignment,
8
+ * flex). Each is independently regression-tested. The orchestrator
9
+ * `parseToIR(classes)` is exported from `./parser/index.ts`.
10
+ *
11
+ * This file owns the APPLICATION side (IR → Figma frame) — i.e.
12
+ * `applyToFrame` (container) and `applyChildProperties` (per-child
13
+ * layoutGrow / layoutAlign / etc). `LayoutParser.parseToIR` is kept
14
+ * here as a thin delegate so existing consumers don't need to migrate
15
+ * their call sites; new code should `import { parseToIR } from './parser'`.
16
+ *
17
+ * (The class is still named `LayoutParser` for backward-compat with
18
+ * its consumers in `width-solver.ts` / `ui-builder.ts`. A future
19
+ * cleanup pass could rename it / this file to `LayoutApplier` /
20
+ * `layout-applier.ts` to match the current responsibility, but that's
21
+ * mostly cosmetic and would touch every call site.)
22
+ */
23
+
24
+ import { parseToIR as parseToIRImpl } from './parser';
25
+ import type { LayoutIR, SizingMode } from './parser/ir';
26
+ export type { LayoutIR, SizingMode };
27
+
28
+ const FRAME_CROSS_ALIGN = new WeakMap<FrameNode, LayoutIR['crossAlign']>();
29
+ const FRAME_FROM_BLOCK_FLOW = new WeakMap<FrameNode, boolean>();
30
+ // Block-flow parents that carry `text-align: center` (or right). CSS centers
31
+ // inline content inside such a container — `text-center` on a div centers
32
+ // inline-flex pills / inline-block buttons inside, even though they're not
33
+ // flex parents. Without this signal, `applyChildProperties` defaults the
34
+ // child to MIN cross-align and pills end up left-aligned.
35
+ const FRAME_INLINE_ALIGN = new WeakMap<FrameNode, 'CENTER' | 'MAX' | 'MIN'>();
36
+
37
+ /**
38
+ * Record a flex/grid parent's cross-axis intent (STRETCH | MIN | CENTER | MAX | BASELINE).
39
+ * `applyChildProperties` reads this to emulate CSS `align-items: stretch` — the default
40
+ * for flex/grid containers — since Figma has no parent-level STRETCH value.
41
+ * Call whenever a frame's layoutMode or counterAxisAlignItems is set outside LayoutParser
42
+ * (notably from `applySemanticUtilitiesToFrame` / `applyCssDeclarationsToFrame`).
43
+ */
44
+ export function setFrameCrossAlign(
45
+ frame: FrameNode,
46
+ align: NonNullable<LayoutIR['crossAlign']>
47
+ ): void {
48
+ FRAME_CROSS_ALIGN.set(frame, align);
49
+ }
50
+
51
+ /**
52
+ * Mark a frame as coming from block-flow (a regular `<div>` / `<section>`
53
+ * promoted to a VERTICAL auto-layout container, vs. a true flex/grid
54
+ * parent). Used by `applyChildProperties` to differentiate CSS semantics:
55
+ * block-flow parents emulate `align-items: stretch` for block children but
56
+ * leave inline children alone; real flex parents stretch all children
57
+ * (including inline-flex) per CSS spec.
58
+ */
59
+ export function setFrameFromBlockFlow(frame: FrameNode, fromBlockFlow: boolean): void {
60
+ FRAME_FROM_BLOCK_FLOW.set(frame, fromBlockFlow);
61
+ }
62
+
63
+ /**
64
+ * Record a block-flow parent's `text-align` so `applyChildProperties` can
65
+ * center / right-align inline-display children inside (CSS centers inline
66
+ * content via the parent's text-align). Without this, an inline-flex pill
67
+ * inside `<div class="text-center">` ends up left-aligned because the
68
+ * plugin defaults the child's counterAxisAlignSelf to MIN.
69
+ */
70
+ export function setFrameInlineAlign(frame: FrameNode, align: 'CENTER' | 'MAX' | 'MIN'): void {
71
+ FRAME_INLINE_ALIGN.set(frame, align);
72
+ }
73
+
74
+ export class LayoutParser {
75
+ /**
76
+ * Parse Tailwind classes into a LayoutIR. Thin delegate to
77
+ * `parseToIR` in `./parser` — kept on the class for backward-
78
+ * compatibility with the two existing call sites
79
+ * (`width-solver.ts`, `ui-builder.ts`). New code should import
80
+ * the free function directly.
81
+ */
82
+ static parseToIR(classes: string[]): LayoutIR {
83
+ return parseToIRImpl(classes);
84
+ }
85
+
86
+ /**
87
+ * Apply IR to a Figma frame (container properties)
88
+ */
89
+ static applyToFrame(frame: FrameNode, ir: LayoutIR): void {
90
+ // Set layout mode - default to VERTICAL for standard document flow
91
+ const layoutMode = ir.layoutMode !== 'NONE' ? ir.layoutMode : 'VERTICAL';
92
+ frame.layoutMode = layoutMode;
93
+
94
+ // Set base sizing modes to AUTO (hug content)
95
+ frame.primaryAxisSizingMode = 'AUTO';
96
+ frame.counterAxisSizingMode = 'AUTO';
97
+
98
+ // Apply alignment
99
+ frame.primaryAxisAlignItems = ir.mainAlign;
100
+ frame.counterAxisAlignItems = ir.crossAlign === 'STRETCH' ? 'MIN' : ir.crossAlign;
101
+ // Keep original Tailwind cross-axis intent available for child layout resolution.
102
+ FRAME_CROSS_ALIGN.set(frame, ir.crossAlign);
103
+ // Track whether this frame came from a non-flex/non-grid source (block flow).
104
+ FRAME_FROM_BLOCK_FLOW.set(frame, ir.layoutMode === 'NONE');
105
+
106
+ // Apply gap
107
+ frame.itemSpacing = ir.gap;
108
+
109
+ // Apply padding
110
+ frame.paddingTop = ir.paddingTop;
111
+ frame.paddingRight = ir.paddingRight;
112
+ frame.paddingBottom = ir.paddingBottom;
113
+ frame.paddingLeft = ir.paddingLeft;
114
+
115
+ // Apply fixed dimensions if specified
116
+ if (ir.widthMode === 'FIXED' && ir.fixedWidth !== undefined) {
117
+ try {
118
+ frame.resize(ir.fixedWidth, frame.height);
119
+ // Width is fixed - set appropriate axis sizing
120
+ if (layoutMode === 'HORIZONTAL') {
121
+ frame.primaryAxisSizingMode = 'FIXED';
122
+ } else {
123
+ frame.counterAxisSizingMode = 'FIXED';
124
+ }
125
+ } catch (_e) {
126
+ // Ignore resize errors
127
+ }
128
+ }
129
+
130
+ if (ir.heightMode === 'FIXED' && ir.fixedHeight !== undefined) {
131
+ try {
132
+ frame.resize(frame.width, ir.fixedHeight);
133
+ // Height is fixed - set appropriate axis sizing
134
+ if (layoutMode === 'VERTICAL') {
135
+ frame.primaryAxisSizingMode = 'FIXED';
136
+ } else {
137
+ frame.counterAxisSizingMode = 'FIXED';
138
+ }
139
+ } catch (_e) {
140
+ // Ignore resize errors
141
+ }
142
+ }
143
+
144
+ // Min-height / Min-width floors. Figma's auto-layout exposes these
145
+ // as native properties on FrameNode; `layoutMode` here is always
146
+ // HORIZONTAL or VERTICAL (the NONE case was rewritten to VERTICAL
147
+ // above), so the auto-layout property is safe to set. Resize too in
148
+ // case the frame hasn't grown yet.
149
+ if (ir.minHeight !== undefined && ir.minHeight > 0) {
150
+ try {
151
+ if ('minHeight' in frame) {
152
+ (frame as { minHeight: number }).minHeight = ir.minHeight;
153
+ }
154
+ if (frame.height < ir.minHeight) {
155
+ frame.resize(frame.width, ir.minHeight);
156
+ }
157
+ } catch (_e) { /* ignore */ }
158
+ }
159
+ if (ir.minWidth !== undefined && ir.minWidth > 0) {
160
+ try {
161
+ if ('minWidth' in frame) {
162
+ (frame as { minWidth: number }).minWidth = ir.minWidth;
163
+ }
164
+ if (frame.width < ir.minWidth) {
165
+ frame.resize(ir.minWidth, frame.height);
166
+ }
167
+ } catch (_e) { /* ignore */ }
168
+ }
169
+
170
+ // Apply wrap if supported
171
+ if (ir.wrap && 'layoutWrap' in frame) {
172
+ frame.layoutWrap = 'WRAP';
173
+ if (ir.gapY !== undefined && 'counterAxisSpacing' in frame) {
174
+ frame.counterAxisSpacing = ir.gapY;
175
+ }
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Apply child properties based on parent context
181
+ * MUST be called after child is appended to parent
182
+ */
183
+ static applyChildProperties(
184
+ child: SceneNode,
185
+ childClasses: string[],
186
+ parent: FrameNode
187
+ ): void {
188
+ const ir = this.parseToIR(childClasses);
189
+ const parentLayout = parent.layoutMode;
190
+
191
+ // Skip if parent has no auto-layout
192
+ if (parentLayout === 'NONE') return;
193
+
194
+ // Guardrail: in HUG/AUTO parents, Figma's layoutGrow can collapse children.
195
+ // When a child with layoutGrow=1 is appended to an AUTO parent, Figma immediately
196
+ // resizes it to fill (= 1px minimum). Reset BOTH layoutGrow AND primaryAxisSizingMode
197
+ // so Figma re-expands the child to hug its content again.
198
+ if ('layoutGrow' in child && parent.primaryAxisSizingMode !== 'FIXED') {
199
+ const prevGrow = child.layoutGrow;
200
+ child.layoutGrow = 0;
201
+ if (prevGrow > 0 && 'primaryAxisSizingMode' in child && 'layoutMode' in child) {
202
+ const childLayout = child.layoutMode;
203
+ if (childLayout === 'VERTICAL' || childLayout === 'HORIZONTAL') {
204
+ // Undo the FILL mode Figma internally set when the child was appended with layoutGrow>0.
205
+ child.primaryAxisSizingMode = 'AUTO';
206
+ }
207
+ }
208
+ }
209
+
210
+ // Apply grow (flex-1)
211
+ // layoutGrow only makes sense when the parent has a FIXED primary axis size.
212
+ // If the parent is AUTO-sized, layoutGrow collapses the child to height/width=0 in Figma.
213
+ // Reset any layoutGrow that may have been set earlier (e.g. by applyTailwindStylesToFrame).
214
+ if (ir.grow > 0 && 'layoutGrow' in child) {
215
+ if (parent.primaryAxisSizingMode === 'FIXED') {
216
+ child.layoutGrow = ir.grow;
217
+ } else {
218
+ child.layoutGrow = 0;
219
+ }
220
+ }
221
+ // shrink-0 explicitly pins layoutGrow = 0 so a viewport-anchored parent
222
+ // distributes remaining space only to real grow siblings.
223
+ if (ir.shrinkZero && 'layoutGrow' in child) {
224
+ child.layoutGrow = 0;
225
+ }
226
+
227
+ // Apply self alignment
228
+ if (ir.selfAlign !== undefined) {
229
+ if (ir.selfAlign === 'STRETCH') {
230
+ if ('layoutAlign' in child) child.layoutAlign = 'STRETCH';
231
+ } else if ('counterAxisAlignSelf' in child) {
232
+ // MIN / CENTER / MAX: use counterAxisAlignSelf (layoutAlign = MIN/CENTER/MAX is deprecated)
233
+ try { child.counterAxisAlignSelf = ir.selfAlign; } catch (_err) { /* ignore */ }
234
+ }
235
+ }
236
+
237
+ // CSS flex/grid default is `align-items: stretch` when no explicit items-* is provided.
238
+ // Figma has no parent-level STRETCH value, so emulate it by stretching children in
239
+ // vertical auto-layout containers unless the child explicitly sets a fixed width.
240
+ //
241
+ // CSS distinguishes two cases for inline-display children:
242
+ // (a) Inline-display child in a BLOCK-FLOW parent (plugin treats as VERTICAL
243
+ // auto-layout for convenience, but CSS-wise it's inline flow): the child
244
+ // hugs content like any other inline element. NEVER stretch. This is the
245
+ // recurring "pill renders full width" bug — see how-it-works / round-trip
246
+ // pills, scanner/inline-flex-regression.ts positive control.
247
+ // (b) Inline-display child in a real FLEX parent (`flex flex-col` etc.):
248
+ // CSS treats it as a flex item, and `align-items: stretch` (the CSS
249
+ // default) applies — child stretches. This is the dialog footer at base
250
+ // breakpoint: `<DialogFooter className="flex flex-col-reverse">` with
251
+ // inline-flex buttons that should stretch to full width on mobile.
252
+ // So the inline-display skip applies ONLY to the block-flow MIN branch.
253
+ const parentCrossAlign = FRAME_CROSS_ALIGN.get(parent);
254
+ const parentFromBlockFlow = FRAME_FROM_BLOCK_FLOW.get(parent) === true;
255
+ const isOutOfFlowChild = childClasses.includes('absolute') || childClasses.includes('fixed');
256
+ const childHasInlineDisplay = this.hasInlineDisplay(childClasses);
257
+ const fromBlockFlowMinBranch = parentFromBlockFlow && parentCrossAlign === 'MIN';
258
+ const fromFlexStretchBranch = parentCrossAlign === 'STRETCH' && !parentFromBlockFlow;
259
+ const parentWantsStretch =
260
+ fromFlexStretchBranch
261
+ || (fromBlockFlowMinBranch && !childHasInlineDisplay);
262
+ const shouldImplicitStretch = parentLayout === 'VERTICAL'
263
+ && parentWantsStretch
264
+ && ir.selfAlign === undefined
265
+ && !isOutOfFlowChild;
266
+ if (shouldImplicitStretch && 'layoutAlign' in child && !this.hasNonAutoWidthConstraint(childClasses)) {
267
+ child.layoutAlign = 'STRETCH';
268
+ }
269
+
270
+ // CSS `text-align` on a block container centers/aligns inline content
271
+ // INSIDE it — that includes inline-flex pills, inline-block buttons,
272
+ // and inline text. The plugin emulates this for inline-display flex
273
+ // children: when the block-flow parent recorded a non-MIN inline
274
+ // alignment, set the child's counterAxisAlignSelf so it sits at that
275
+ // horizontal position (CENTER for `text-center`, MAX for `text-right`).
276
+ // Doesn't apply to block-level children — they take full parent width
277
+ // via the implicit-stretch path above and their OWN textAlign handles
278
+ // text rendering inside.
279
+ if (
280
+ parentLayout === 'VERTICAL'
281
+ && childHasInlineDisplay
282
+ && parentFromBlockFlow
283
+ && ir.selfAlign === undefined
284
+ && !isOutOfFlowChild
285
+ ) {
286
+ const inlineAlign = FRAME_INLINE_ALIGN.get(parent);
287
+ if (inlineAlign && inlineAlign !== 'MIN' && 'counterAxisAlignSelf' in child) {
288
+ // Override the parent's `counterAxisAlignItems` for this child.
289
+ // `layoutAlign` only accepts 'INHERIT' | 'STRETCH' in current Figma;
290
+ // 'MIN' / 'CENTER' / 'MAX' are removed and trigger console warnings
291
+ // ("CENTER is no longer a supported value for layoutAlign").
292
+ try { child.counterAxisAlignSelf = inlineAlign; } catch (_e) { /* ignore */ }
293
+ }
294
+ }
295
+
296
+ // Handle w-full based on parent layout direction
297
+ if (childClasses.includes('w-full')) {
298
+ if (parentLayout === 'HORIZONTAL') {
299
+ // In horizontal parent, w-full means grow along primary axis
300
+ if ('layoutGrow' in child) {
301
+ child.layoutGrow = 1;
302
+ }
303
+ } else if (parentLayout === 'VERTICAL') {
304
+ // In vertical parent, w-full means stretch along counter axis
305
+ if ('layoutAlign' in child) {
306
+ child.layoutAlign = 'STRETCH';
307
+ }
308
+ }
309
+ }
310
+
311
+ // Handle h-full based on parent layout direction
312
+ if (childClasses.includes('h-full')) {
313
+ if (parentLayout === 'VERTICAL') {
314
+ // In vertical parent, h-full only makes sense when parent has a fixed height
315
+ if ('layoutGrow' in child && parent.primaryAxisSizingMode === 'FIXED') {
316
+ child.layoutGrow = 1;
317
+ }
318
+ } else if (parentLayout === 'HORIZONTAL') {
319
+ // In horizontal parent, h-full means stretch along counter axis
320
+ if ('layoutAlign' in child) {
321
+ child.layoutAlign = 'STRETCH';
322
+ }
323
+ }
324
+ }
325
+
326
+ // Handle fractional widths (w-1/2, w-1/3, etc.)
327
+ // In horizontal parent, use layoutGrow to distribute space proportionally
328
+ if (ir.widthFraction !== undefined && parentLayout === 'HORIZONTAL') {
329
+ if ('layoutGrow' in child) {
330
+ // Use fraction as layoutGrow value (scaled to work with other siblings)
331
+ // This gives proportional distribution when combined with other fractional widths
332
+ child.layoutGrow = ir.widthFraction;
333
+ }
334
+ }
335
+
336
+ // Handle fractional heights (h-1/2, h-1/3, etc.) in vertical parent
337
+ if (ir.heightFraction !== undefined && parentLayout === 'VERTICAL') {
338
+ if ('layoutGrow' in child) {
339
+ child.layoutGrow = ir.heightFraction;
340
+ }
341
+ }
342
+ }
343
+
344
+ // ===========================================================================
345
+ // Parsing helpers — every parser now lives under `./parser/`. Only
346
+ // class-level utilities (hasNonAutoWidthConstraint / hasInlineDisplay)
347
+ // remain here because they are used by `applyChildProperties` (the
348
+ // IR → Figma frame side, out of scope for the parser split).
349
+ // ===========================================================================
350
+
351
+ private static hasNonAutoWidthConstraint(classes: string[]): boolean {
352
+ for (const cls of classes) {
353
+ if (cls.startsWith('min-w-') || cls.startsWith('max-w-')) return true;
354
+ if (!cls.startsWith('w-')) continue;
355
+ if (cls === 'w-auto' || cls === 'w-full') continue;
356
+ return true;
357
+ }
358
+ return false;
359
+ }
360
+
361
+ private static hasInlineDisplay(classes: string[]): boolean {
362
+ for (const cls of classes) {
363
+ if (cls === 'inline' || cls === 'inline-block' || cls === 'inline-flex' || cls === 'inline-grid') {
364
+ return true;
365
+ }
366
+ }
367
+ return false;
368
+ }
369
+
370
+ // parseSizing now lives in `parser/sizing.ts` and is imported above.
371
+
372
+ // parseChildProperties (grow/shrink) and parseWrap now live in
373
+ // `parser/flex.ts` and are imported above.
374
+
375
+ }
@@ -1,7 +1,5 @@
1
- import { applyFullWidthIfPossible, applyGridColumnsIfPossible, getGridColumnsNode, hasGridColumnsNode, getColSpanNode } from './tailwind';
2
- import { getBaseClass } from './width-solver';
3
-
4
- declare const figma: any;
1
+ import { applyAspectRatioIfPossible, applyFullWidthIfPossible, applyRingIfPossible, applyGridColumnsIfPossible, getGridColumnsNode, hasGridColumnsNode, getColSpanNode, markFullWidthNode } from './deferred-layout';
2
+ import { getBaseClass } from '../tailwind';
5
3
 
6
4
  export type LayoutWrapContext = {
7
5
  maxWidth?: number;
@@ -45,6 +43,8 @@ function reflowFullWidthInSubtree(parent: FrameNode): void {
45
43
 
46
44
  for (const child of parent.children) {
47
45
  applyFullWidthIfPossible(child, parent, options);
46
+ applyRingIfPossible(child, parent);
47
+ applyAspectRatioIfPossible(child);
48
48
  if ('layoutMode' in child) {
49
49
  const childFrame = child as FrameNode;
50
50
  if (hasGridColumnsNode(childFrame)) {
@@ -75,7 +75,7 @@ function stretchGridRowHeights(frame: FrameNode, colsOverride?: number): void {
75
75
  if (!cols || cols <= 0) return;
76
76
  const flowChildren: SceneNode[] = [];
77
77
  for (const child of frame.children) {
78
- if ((child as any).layoutPositioning === 'ABSOLUTE') continue;
78
+ if ('layoutPositioning' in child && child.layoutPositioning === 'ABSOLUTE') continue;
79
79
  if (!('resize' in child)) continue;
80
80
  flowChildren.push(child);
81
81
  }
@@ -89,7 +89,8 @@ function stretchGridRowHeights(frame: FrameNode, colsOverride?: number): void {
89
89
  const span = Math.min(getColSpanNode(flowChildren[i]), cols);
90
90
  const rowIndex = Math.floor(slotIndex / cols);
91
91
  rowIndices.push(rowIndex);
92
- const height = (flowChildren[i] as any).height;
92
+ const child = flowChildren[i];
93
+ const height = 'height' in child ? child.height : NaN;
93
94
  if (Number.isFinite(height)) {
94
95
  const current = rowHeights[rowIndex];
95
96
  if (current == null || height > current) rowHeights[rowIndex] = height;
@@ -101,7 +102,8 @@ function stretchGridRowHeights(frame: FrameNode, colsOverride?: number): void {
101
102
  const rowIndex = rowIndices[i];
102
103
  const targetHeight = rowHeights[rowIndex];
103
104
  if (!Number.isFinite(targetHeight) || targetHeight <= 0) continue;
104
- const child = flowChildren[i] as any;
105
+ const child = flowChildren[i];
106
+ if (!('resize' in child) || !('width' in child)) continue;
105
107
  try {
106
108
  child.resize(child.width, targetHeight);
107
109
  if ('layoutMode' in child) {
@@ -137,19 +139,23 @@ export function maybeWrapMxAuto(
137
139
  wrapper.fills = [];
138
140
  wrapper.strokes = [];
139
141
  wrapper.clipsContent = false;
140
- if ('layoutAlign' in wrapper) (wrapper as any).layoutAlign = 'STRETCH';
141
- wrapper.appendChild(node);
142
- if (typeof context.maxWidth === 'number' && context.maxWidth > 0 && 'resize' in wrapper) {
142
+ if ('layoutAlign' in wrapper) wrapper.layoutAlign = 'STRETCH';
143
+ // Guard against downstream layout passes leaving this wrapper in HUG width.
144
+ // If parent content width is known at creation time, pin wrapper width now.
145
+ const parentContentWidth = context.maxWidth;
146
+ if (typeof parentContentWidth === 'number' && Number.isFinite(parentContentWidth) && parentContentWidth > 0) {
143
147
  try {
144
- if ((wrapper as any).width < context.maxWidth) {
145
- (wrapper as any).resize(context.maxWidth, (wrapper as any).height);
146
- if ('primaryAxisSizingMode' in wrapper) {
147
- (wrapper as any).primaryAxisSizingMode = 'FIXED';
148
- }
149
- }
148
+ wrapper.resize(parentContentWidth, Math.max(1, wrapper.height));
149
+ wrapper.primaryAxisSizingMode = 'FIXED';
150
+ if ('layoutSizingHorizontal' in wrapper) wrapper.layoutSizingHorizontal = 'FIXED';
150
151
  } catch (_err) {
151
- // ignore resize errors
152
+ // ignore
152
153
  }
153
154
  }
155
+ if ('counterAxisAlignSelf' in wrapper) {
156
+ try { wrapper.counterAxisAlignSelf = 'AUTO'; } catch (_err) { /* ignore */ }
157
+ }
158
+ markFullWidthNode(wrapper);
159
+ wrapper.appendChild(node);
154
160
  return wrapper;
155
161
  }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Tailwind alignment utilities → LayoutIR.
3
+ *
4
+ * Owns:
5
+ * - Container alignment: justify-* (mainAlign) and items-* (crossAlign)
6
+ * - Self alignment (a child-on-parent override): self-start / self-center
7
+ * / self-end / self-stretch / self-auto
8
+ *
9
+ * The grow / shrink utilities (flex-1, grow, shrink-0, etc.) are flex-
10
+ * specific child behaviour and stay with the upcoming flex parser in
11
+ * Phase 5 — not here.
12
+ *
13
+ * Behaviour preserved 1:1 from the original `parseAlignment` static
14
+ * method on `LayoutParser` and the self-* portion of `parseChildProperties`.
15
+ * Locked in by the alignment regression test.
16
+ */
17
+
18
+ import type { LayoutIR } from './ir';
19
+
20
+ /**
21
+ * Parse container + self alignment utilities into the IR.
22
+ *
23
+ * Note: when the IR has a flex layoutMode set, the default crossAlign
24
+ * is STRETCH (CSS default for flex containers). That default is applied
25
+ * here when no explicit `items-*` overrides it. `LayoutParser.parseToIR`
26
+ * sets `ir.layoutMode` BEFORE calling this function, so the check below
27
+ * sees the right value.
28
+ */
29
+ export function parseAlignment(classes: string[], ir: LayoutIR): void {
30
+ if (ir.layoutMode !== 'NONE') {
31
+ ir.crossAlign = 'STRETCH';
32
+ }
33
+ for (const cls of classes) {
34
+ // Primary axis (justify-*)
35
+ if (cls === 'justify-start') ir.mainAlign = 'MIN';
36
+ else if (cls === 'justify-center') ir.mainAlign = 'CENTER';
37
+ else if (cls === 'justify-end') ir.mainAlign = 'MAX';
38
+ else if (cls === 'justify-between') ir.mainAlign = 'SPACE_BETWEEN';
39
+
40
+ // Counter axis (items-*)
41
+ if (cls === 'items-start') ir.crossAlign = 'MIN';
42
+ else if (cls === 'items-center') ir.crossAlign = 'CENTER';
43
+ else if (cls === 'items-end') ir.crossAlign = 'MAX';
44
+ else if (cls === 'items-stretch') ir.crossAlign = 'STRETCH';
45
+ else if (cls === 'items-baseline') ir.crossAlign = 'BASELINE';
46
+
47
+ // Self alignment (overrides crossAlign for this child only)
48
+ if (cls === 'self-start') ir.selfAlign = 'MIN';
49
+ else if (cls === 'self-center') ir.selfAlign = 'CENTER';
50
+ else if (cls === 'self-end') ir.selfAlign = 'MAX';
51
+ else if (cls === 'self-stretch') ir.selfAlign = 'STRETCH';
52
+ else if (cls === 'self-auto') ir.selfAlign = undefined;
53
+ }
54
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Tailwind flex-related utilities → LayoutIR.
3
+ *
4
+ * Owns the two flex-specific groups that don't fit elsewhere:
5
+ * 1. Flex-child grow / shrink: flex-1, flex-grow, grow,
6
+ * flex-grow-0, grow-0, shrink-0, flex-shrink-0
7
+ * 2. Flex-wrap: flex-wrap, flex-wrap-reverse
8
+ *
9
+ * NOT handled here (each lives in its own module):
10
+ * - flex / flex-col / flex-row / inline-flex (layoutMode detection,
11
+ * handled by `parseLayoutMode` in `layout-parser.ts` because it's
12
+ * coupled to grid detection — both produce HORIZONTAL/VERTICAL/NONE)
13
+ * - justify-* / items-* / self-* (alignment.ts)
14
+ * - gap-* / space-x-* / space-y-* (spacing.ts)
15
+ *
16
+ * Behaviour preserved 1:1 from the original `parseChildProperties` and
17
+ * `parseWrap` static methods on `LayoutParser`. Locked in by the flex
18
+ * regression test.
19
+ */
20
+
21
+ import type { LayoutIR } from './ir';
22
+
23
+ /**
24
+ * Parse flex-child grow / shrink utilities into `ir.grow` / `ir.shrinkZero`.
25
+ *
26
+ * Flex grow / shrink semantics in Figma's auto-layout model:
27
+ * - `flex-1` / `flex-grow` / `grow` → grow = 1 (claim remaining space)
28
+ * - `flex-grow-0` / `grow-0` → grow = 0 (don't claim space)
29
+ * - `shrink-0` / `flex-shrink-0` → shrinkZero = true (force layoutGrow = 0
30
+ * even when the parent has remaining space; otherwise viewport-anchored
31
+ * parents would distribute leftover space across non-shrink siblings)
32
+ */
33
+ export function parseFlexChildren(classes: string[], ir: LayoutIR): void {
34
+ for (const cls of classes) {
35
+ if (cls === 'flex-1' || cls === 'flex-grow' || cls === 'grow') {
36
+ ir.grow = 1;
37
+ continue;
38
+ }
39
+ if (cls === 'flex-grow-0' || cls === 'grow-0') {
40
+ ir.grow = 0;
41
+ continue;
42
+ }
43
+ if (cls === 'shrink-0' || cls === 'flex-shrink-0') {
44
+ ir.shrinkZero = true;
45
+ continue;
46
+ }
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Parse flex-wrap utilities into `ir.wrap`.
52
+ * `flex-wrap-reverse` is treated as `flex-wrap` — Figma auto-layout has
53
+ * no reverse-wrap option, so we collapse the intent to plain wrap.
54
+ */
55
+ export function parseWrap(classes: string[], ir: LayoutIR): void {
56
+ if (classes.includes('flex-wrap') || classes.includes('flex-wrap-reverse')) {
57
+ ir.wrap = true;
58
+ }
59
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Layout parser — Tailwind class lists → LayoutIR.
3
+ *
4
+ * This barrel is the canonical entry point for parsing. Sub-parsers
5
+ * live one file each in this folder (one Tailwind axis per file):
6
+ *
7
+ * - layout-mode.ts → ir.layoutMode (flex/grid/none + direction)
8
+ * - spacing.ts → ir.gap / paddingT/R/B/L (gap-*, p-*, space-*-*)
9
+ * - sizing.ts → ir.widthMode/heightMode/fixedWidth/heightFraction/...
10
+ * - alignment.ts → ir.mainAlign / crossAlign / selfAlign
11
+ * - flex.ts → ir.grow / shrinkZero / wrap
12
+ *
13
+ * Each sub-parser is independently regression-tested under
14
+ * `scanner/layout-*-regression.ts`.
15
+ *
16
+ * The IR-to-Figma APPLICATION side (`applyToFrame`, `applyChildProperties`)
17
+ * lives on `LayoutParser` in `../layout-parser.ts`. That file is named
18
+ * for the original LayoutParser class; it keeps `LayoutParser.parseToIR`
19
+ * as a thin delegate to `parseToIR` here so existing consumers don't
20
+ * need to migrate their call sites.
21
+ */
22
+
23
+ import { LayoutIR, makeEmptyIR } from './ir';
24
+ import { parseLayoutMode } from './layout-mode';
25
+ import { parseGap, parsePadding } from './spacing';
26
+ import { parseSizing } from './sizing';
27
+ import { parseAlignment } from './alignment';
28
+ import { parseFlexChildren, parseWrap } from './flex';
29
+
30
+ /**
31
+ * Parse a Tailwind class list into a LayoutIR. Pure function — no
32
+ * Figma runtime needed, no I/O, no shared state. The orchestrator
33
+ * sequences sub-parsers; class-order semantics are preserved
34
+ * (later utilities override earlier ones, so responsive variants
35
+ * resolved at a breakpoint produce the right precedence).
36
+ */
37
+ export function parseToIR(classes: string[]): LayoutIR {
38
+ const ir = makeEmptyIR();
39
+
40
+ // Pass 1: layout mode (flex/grid detection — sets ir.layoutMode).
41
+ // Must run before parseAlignment, which applies an implicit STRETCH
42
+ // crossAlign default when layoutMode is set.
43
+ ir.layoutMode = parseLayoutMode(classes);
44
+
45
+ // Pass 2: per-axis parsers, each writing its own subset of IR fields.
46
+ parseGap(classes, ir);
47
+ parsePadding(classes, ir);
48
+ parseAlignment(classes, ir);
49
+ parseSizing(classes, ir);
50
+ parseFlexChildren(classes, ir);
51
+ parseWrap(classes, ir);
52
+
53
+ return ir;
54
+ }
55
+
56
+ // Re-exports — give consumers a single import surface for IR types
57
+ // AND each individual sub-parser (handy for tests + future uses).
58
+ export type { LayoutIR, SizingMode } from './ir';
59
+ export { makeEmptyIR } from './ir';
60
+ export { parseLayoutMode } from './layout-mode';
61
+ export { parseGap, parsePadding, parseSpacing } from './spacing';
62
+ export { parseSizing } from './sizing';
63
+ export { parseAlignment } from './alignment';
64
+ export { parseFlexChildren, parseWrap } from './flex';
65
+ export { resolveSpacing } from './spacing-scale';