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
|
@@ -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 './
|
|
2
|
-
import { getBaseClass } from '
|
|
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 (
|
|
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
|
|
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]
|
|
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)
|
|
141
|
-
wrapper.
|
|
142
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
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';
|