inkbridge 0.1.0-beta.21 → 0.1.0-beta.22
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/code.js +10 -10
- package/package.json +5 -2
- package/scanner/component-scanner.ts +132 -2
- package/scanner/imported-array-map-regression.ts +195 -0
- package/scanner/intrinsic-sizing-regression.ts +333 -0
- package/scanner/story-args-resolution-regression.ts +270 -0
- package/src/design-system/story-builder.ts +12 -0
- package/src/layout/index.ts +2 -0
- package/src/layout/intrinsic-applier.ts +105 -0
- package/src/layout/intrinsic-sizing.ts +183 -0
- package/src/layout/parser/layout-mode.ts +14 -4
- package/src/render-engine-version.ts +1 -1
- package/src/tailwind/tailwind.ts +29 -8
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intrinsic-sizing applier.
|
|
3
|
+
*
|
|
4
|
+
* Walks a built Figma tree and rewrites the auto-layout properties
|
|
5
|
+
* that would otherwise leave HUG containers stuck at Figma's frame-
|
|
6
|
+
* creation default (~100px) due to the STRETCH-in-HUG-chain deadlock
|
|
7
|
+
* described in `intrinsic-sizing.ts`.
|
|
8
|
+
*
|
|
9
|
+
* ## Why we transform alignment, not size
|
|
10
|
+
*
|
|
11
|
+
* Per the Figma plugin API:
|
|
12
|
+
*
|
|
13
|
+
* > AUTO: The primary axis length is determined by the size of the
|
|
14
|
+
* > children.
|
|
15
|
+
* > AUTO: The counter axis length is determined by the size of the
|
|
16
|
+
* > children. If set, the auto-layout frame will automatically resize
|
|
17
|
+
* > along the counter axis to fit its children.
|
|
18
|
+
*
|
|
19
|
+
* Calling `frame.resize(w, h)` on a HUG frame does NOT stick — the
|
|
20
|
+
* engine recomputes from children on the next pass. So we can't force
|
|
21
|
+
* a width on a HUG container without flipping `layoutMode` to `'NONE'`
|
|
22
|
+
* (which destroys designer auto-layout editability).
|
|
23
|
+
*
|
|
24
|
+
* Instead, we change the *child* contribution to the parent's HUG
|
|
25
|
+
* computation. For each STRETCH child whose parent's chain isn't
|
|
26
|
+
* width-anchored:
|
|
27
|
+
*
|
|
28
|
+
* 1. Flip the child's `layoutAlign` from `'STRETCH'` to `'INHERIT'`
|
|
29
|
+
* (drop the cross-axis stretch directive).
|
|
30
|
+
* 2. Reset the sizing axis that the STRETCH side-effect had silently
|
|
31
|
+
* flipped to FIXED back to `'AUTO'`. Parent is VERTICAL (per the
|
|
32
|
+
* predicate), so the affected child axis is its horizontal axis:
|
|
33
|
+
* `primaryAxisSizingMode` for a HORIZONTAL child,
|
|
34
|
+
* `counterAxisSizingMode` for a VERTICAL child.
|
|
35
|
+
*
|
|
36
|
+
* After the flip, the child hugs its own content, the parent's HUG
|
|
37
|
+
* resolves to max-of-children — the exact result CSS produces via
|
|
38
|
+
* max-content. Designer affordance is preserved: if the designer
|
|
39
|
+
* later anchors the chain by setting an ancestor to FIXED, they can
|
|
40
|
+
* re-enable Fill / STRETCH on the children manually and Figma's
|
|
41
|
+
* STRETCH cascade will work correctly against the new anchor.
|
|
42
|
+
*
|
|
43
|
+
* ## Phase
|
|
44
|
+
*
|
|
45
|
+
* Phase 3 (deferred layout). Runs once per built story-layout tree
|
|
46
|
+
* from `populateStoryLayout`, before `reflowDeferredAbsolutePositioningTree`.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
import { isUnanchoredStretchChild } from './intrinsic-sizing';
|
|
50
|
+
|
|
51
|
+
export interface IntrinsicApplierStats {
|
|
52
|
+
/** Number of children whose layoutAlign was flipped STRETCH → INHERIT. */
|
|
53
|
+
alignmentFlips: number;
|
|
54
|
+
/** Number of nodes walked. */
|
|
55
|
+
nodesWalked: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Walk the tree rooted at `root`. For every STRETCH child of a VERTICAL
|
|
60
|
+
* parent whose chain doesn't reach a width-anchored ancestor, flip the
|
|
61
|
+
* child's `layoutAlign` to `'INHERIT'` and reset its horizontal sizing
|
|
62
|
+
* axis to `'AUTO'`. Children then hug their own content; the parent's
|
|
63
|
+
* HUG resolves to max-of-children.
|
|
64
|
+
*
|
|
65
|
+
* Returns a stats object for observability / tests.
|
|
66
|
+
*/
|
|
67
|
+
export function breakHugStretchDeadlocks(root: SceneNode): IntrinsicApplierStats {
|
|
68
|
+
const stats: IntrinsicApplierStats = { alignmentFlips: 0, nodesWalked: 0 };
|
|
69
|
+
walk(root, stats);
|
|
70
|
+
return stats;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function walk(node: SceneNode, stats: IntrinsicApplierStats): void {
|
|
74
|
+
stats.nodesWalked += 1;
|
|
75
|
+
|
|
76
|
+
if (isUnanchoredStretchChild(node)) {
|
|
77
|
+
try {
|
|
78
|
+
const frame = node as FrameNode;
|
|
79
|
+
// Order matters per Figma API: AUTO is not valid on an axis where
|
|
80
|
+
// layoutAlign === 'STRETCH'. Drop the STRETCH first, then reset
|
|
81
|
+
// the sizing axis that the STRETCH side-effect had flipped to
|
|
82
|
+
// FIXED. Parent is VERTICAL (guaranteed by the predicate), so the
|
|
83
|
+
// affected axis is child's HORIZONTAL: primary for a HORIZONTAL
|
|
84
|
+
// child, counter for a VERTICAL child.
|
|
85
|
+
frame.layoutAlign = 'INHERIT';
|
|
86
|
+
if ('layoutMode' in frame) {
|
|
87
|
+
if (frame.layoutMode === 'HORIZONTAL') {
|
|
88
|
+
frame.primaryAxisSizingMode = 'AUTO';
|
|
89
|
+
} else if (frame.layoutMode === 'VERTICAL') {
|
|
90
|
+
frame.counterAxisSizingMode = 'AUTO';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
stats.alignmentFlips += 1;
|
|
94
|
+
} catch (_err) {
|
|
95
|
+
// Some node types may not accept the assignment in edge cases;
|
|
96
|
+
// ignore and continue — best-effort transform.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if ('children' in node && Array.isArray(node.children)) {
|
|
101
|
+
for (const child of node.children) {
|
|
102
|
+
walk(child, stats);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intrinsic-sizing predicates.
|
|
3
|
+
*
|
|
4
|
+
* Pure inspectors that detect where Figma's auto-layout reaches the
|
|
5
|
+
* intrinsic-sizing deadlock that CSS resolves via its two-pass
|
|
6
|
+
* algorithm (max-content bottom-up, then constraints top-down).
|
|
7
|
+
*
|
|
8
|
+
* ## The deadlock
|
|
9
|
+
*
|
|
10
|
+
* Consider a chain where a HUG container has STRETCH children:
|
|
11
|
+
*
|
|
12
|
+
* parent VERTICAL HUG counter ← width = max(children widths)
|
|
13
|
+
* └── child VERTICAL layoutAlign=STRETCH ← width = parent.width
|
|
14
|
+
*
|
|
15
|
+
* CSS resolves this by computing the child's intrinsic max-content,
|
|
16
|
+
* making that the parent's HUG width, then stretching the child back
|
|
17
|
+
* to that width. Figma's layout engine doesn't compute max-content —
|
|
18
|
+
* the dependency is circular, and Figma falls back to the frame-
|
|
19
|
+
* creation default (~100px) at the top, which then cascades down as
|
|
20
|
+
* "stretch to 100" through every level of the chain.
|
|
21
|
+
*
|
|
22
|
+
* ## What "width propagation" means here
|
|
23
|
+
*
|
|
24
|
+
* A frame's horizontal width is "definite" (i.e. its source is not
|
|
25
|
+
* one of its own children) if EITHER:
|
|
26
|
+
*
|
|
27
|
+
* 1. It is the root frame with an explicit width (Story Layout etc.).
|
|
28
|
+
* 2. It is a STRETCH child of a VERTICAL parent whose width is
|
|
29
|
+
* definite — the parent's width flows down to the child's
|
|
30
|
+
* horizontal axis.
|
|
31
|
+
* 3. It is a GROW (layoutGrow=1) child of a HORIZONTAL parent whose
|
|
32
|
+
* primary axis (horizontal) is definite — parent's primary width
|
|
33
|
+
* minus siblings flows down.
|
|
34
|
+
*
|
|
35
|
+
* Otherwise the frame's width is HUG-of-children — i.e. determined by
|
|
36
|
+
* walking into the subtree. If every ancestor in turn is also HUG-of-
|
|
37
|
+
* children, the chain is unanchored and the deadlock applies.
|
|
38
|
+
*
|
|
39
|
+
* ## Why we don't trust `primaryAxisSizingMode === 'FIXED'` alone
|
|
40
|
+
*
|
|
41
|
+
* Per the Figma plugin API, `layoutAlign='STRETCH'` cannot coexist
|
|
42
|
+
* with `primaryAxisSizingMode='AUTO'` on the relevant axis — Figma
|
|
43
|
+
* silently flips it to FIXED. So a HORIZONTAL frame inside a VERTICAL
|
|
44
|
+
* parent with STRETCH appears as `primaryAxisSizingMode=FIXED` even
|
|
45
|
+
* when the user wrote no explicit width utility. Reading the mode
|
|
46
|
+
* alone tells us only that Figma's resolver decided FIXED, not whether
|
|
47
|
+
* the width was authored by intent. The propagation walk is the
|
|
48
|
+
* authoritative check.
|
|
49
|
+
*
|
|
50
|
+
* ## Phase
|
|
51
|
+
*
|
|
52
|
+
* Phase 3 (deferred layout). The predicates need full ancestor context
|
|
53
|
+
* — they can only run after `appendChild`. Use from
|
|
54
|
+
* `reflowDeferredAbsolutePositioningTree` or another post-build pass.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
const WIDTH_DEFINITE_CACHE = new WeakMap<SceneNode, boolean>();
|
|
58
|
+
|
|
59
|
+
interface ChainRule {
|
|
60
|
+
/** Walks parent → child to see if the child's width is propagated from the parent. */
|
|
61
|
+
childWidthPropagates: (parent: FrameNode, child: SceneNode) => boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const VERTICAL_PARENT_RULE: ChainRule = {
|
|
65
|
+
childWidthPropagates(_parent, child) {
|
|
66
|
+
// In a VERTICAL parent the child's horizontal axis is the parent's
|
|
67
|
+
// counter axis. The child gets the parent's width only if it
|
|
68
|
+
// explicitly stretches.
|
|
69
|
+
return 'layoutAlign' in child && child.layoutAlign === 'STRETCH';
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const HORIZONTAL_PARENT_RULE: ChainRule = {
|
|
74
|
+
childWidthPropagates(_parent, child) {
|
|
75
|
+
// In a HORIZONTAL parent the child's horizontal axis is the parent's
|
|
76
|
+
// primary axis. The child gets a definite slice of the parent's
|
|
77
|
+
// width only if it grows to fill remaining primary space.
|
|
78
|
+
return 'layoutGrow' in child && child.layoutGrow === 1;
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function getParentRule(parent: FrameNode): ChainRule | null {
|
|
83
|
+
if (parent.layoutMode === 'VERTICAL') return VERTICAL_PARENT_RULE;
|
|
84
|
+
if (parent.layoutMode === 'HORIZONTAL') return HORIZONTAL_PARENT_RULE;
|
|
85
|
+
// NONE / freeform parent does not propagate sizing through auto-layout.
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isAuthoredRoot(node: SceneNode): boolean {
|
|
90
|
+
// Top-level frames (no parent or a non-frame parent like PageNode) are
|
|
91
|
+
// treated as definite — their width came from upstream code that knows
|
|
92
|
+
// what it's doing (Story Layout, component master, etc.).
|
|
93
|
+
const parent = node.parent;
|
|
94
|
+
if (!parent) return true;
|
|
95
|
+
if (!('layoutMode' in parent)) return true;
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Does this node's horizontal width come from somewhere other than its
|
|
101
|
+
* own children? Memoized — a tree walk evaluates each node at most once.
|
|
102
|
+
*
|
|
103
|
+
* NOTE: callers must invalidate the cache (call `clearWidthDefiniteCache`)
|
|
104
|
+
* if they mutate any property the predicate reads (layoutAlign,
|
|
105
|
+
* layoutGrow, layoutMode) between calls. Within a single Phase-3 pass
|
|
106
|
+
* the tree is stable; this is the intended use.
|
|
107
|
+
*/
|
|
108
|
+
export function hasDefiniteWidth(node: SceneNode): boolean {
|
|
109
|
+
const cached = WIDTH_DEFINITE_CACHE.get(node);
|
|
110
|
+
if (cached !== undefined) return cached;
|
|
111
|
+
const result = computeHasDefiniteWidth(node, new Set());
|
|
112
|
+
WIDTH_DEFINITE_CACHE.set(node, result);
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function computeHasDefiniteWidth(node: SceneNode, seen: Set<SceneNode>): boolean {
|
|
117
|
+
// Cycle guard for pathological trees.
|
|
118
|
+
if (seen.has(node)) return false;
|
|
119
|
+
seen.add(node);
|
|
120
|
+
|
|
121
|
+
if (isAuthoredRoot(node)) return true;
|
|
122
|
+
|
|
123
|
+
// Trust an explicit FIXED width set by the plugin's own code paths
|
|
124
|
+
// (applyFullWidthIfPossible, applyGridColumnsIfPossible, w-[Npx], etc.)
|
|
125
|
+
// when it's NOT a layoutAlign=STRETCH / layoutGrow=1 side-effect.
|
|
126
|
+
// Figma silently flips primaryAxisSizingMode to FIXED when those
|
|
127
|
+
// child-side directives are set, but the resulting "FIXED" width may
|
|
128
|
+
// be the frame-creation default (~100) — we can't trust those. When
|
|
129
|
+
// the child is neither STRETCH nor GROW, a FIXED axis means the value
|
|
130
|
+
// was authored.
|
|
131
|
+
if (hasAuthoredFixedWidth(node)) return true;
|
|
132
|
+
|
|
133
|
+
const parent = node.parent as FrameNode;
|
|
134
|
+
const rule = getParentRule(parent);
|
|
135
|
+
if (rule == null) return false;
|
|
136
|
+
if (!rule.childWidthPropagates(parent, node)) return false;
|
|
137
|
+
return computeHasDefiniteWidth(parent, seen);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function hasAuthoredFixedWidth(node: SceneNode): boolean {
|
|
141
|
+
// STRETCH / GROW side-effects: the FIXED axis came from Figma silently
|
|
142
|
+
// upgrading the sizing mode, not from authored intent.
|
|
143
|
+
if ('layoutAlign' in node && node.layoutAlign === 'STRETCH') return false;
|
|
144
|
+
if ('layoutGrow' in node && node.layoutGrow === 1) return false;
|
|
145
|
+
if (!('layoutMode' in node)) return false;
|
|
146
|
+
const frame = node as FrameNode;
|
|
147
|
+
if (!('width' in frame) || !Number.isFinite(frame.width) || frame.width <= 0) return false;
|
|
148
|
+
if (frame.layoutMode === 'HORIZONTAL') {
|
|
149
|
+
return frame.primaryAxisSizingMode === 'FIXED';
|
|
150
|
+
}
|
|
151
|
+
if (frame.layoutMode === 'VERTICAL') {
|
|
152
|
+
return frame.counterAxisSizingMode === 'FIXED';
|
|
153
|
+
}
|
|
154
|
+
// NONE freeform — width is just whatever the user / plugin set.
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Is this node a STRETCH child of a VERTICAL parent whose width is not
|
|
160
|
+
* definite — i.e. the STRETCH cascades into the frame-creation default
|
|
161
|
+
* and produces a ~100px stuck frame width?
|
|
162
|
+
*
|
|
163
|
+
* Restricted to VERTICAL parents because that's the width-deadlock
|
|
164
|
+
* shape: in a VERTICAL parent, `layoutAlign=STRETCH` on a child means
|
|
165
|
+
* "stretch horizontally to parent's width". If parent's width is HUG
|
|
166
|
+
* and unanchored, the STRETCH has nothing to stretch to.
|
|
167
|
+
*
|
|
168
|
+
* (HORIZONTAL parents have an analogous height-deadlock for
|
|
169
|
+
* `layoutAlign=STRETCH` and a width-deadlock for `layoutGrow=1`, but
|
|
170
|
+
* those are separate concerns covered by other heuristics in the
|
|
171
|
+
* pipeline. Width deadlocks via STRETCH in a VERTICAL chain are the
|
|
172
|
+
* canonical pattern this module addresses.)
|
|
173
|
+
*/
|
|
174
|
+
export function isUnanchoredStretchChild(node: SceneNode): boolean {
|
|
175
|
+
if (!('layoutAlign' in node)) return false;
|
|
176
|
+
if (node.layoutAlign !== 'STRETCH') return false;
|
|
177
|
+
const parent = node.parent as SceneNode | null;
|
|
178
|
+
if (!parent || !('layoutMode' in parent)) return false;
|
|
179
|
+
const parentFrame = parent as FrameNode;
|
|
180
|
+
if (parentFrame.layoutMode !== 'VERTICAL') return false;
|
|
181
|
+
return !hasDefiniteWidth(parent);
|
|
182
|
+
}
|
|
183
|
+
|
|
@@ -36,10 +36,20 @@ export function parseLayoutMode(classes: string[]): 'HORIZONTAL' | 'VERTICAL' |
|
|
|
36
36
|
}
|
|
37
37
|
if (cls === 'grid' || cls === 'inline-grid') {
|
|
38
38
|
// grid without grid-cols-N is a single-column layout (like flex-col).
|
|
39
|
-
// grid WITH grid-cols-N wraps horizontally — the column count
|
|
40
|
-
// applied later by markGridColumnsNode, not stored in the IR.
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
// grid WITH grid-cols-N (N >= 2) wraps horizontally — the column count
|
|
40
|
+
// is applied later by markGridColumnsNode, not stored in the IR.
|
|
41
|
+
//
|
|
42
|
+
// `grid-cols-1` is special: one column in CSS means children stack
|
|
43
|
+
// vertically (single column = no horizontal flow). Treating it as
|
|
44
|
+
// HORIZONTAL — like grid-cols-2+ — flips the layout, eats the
|
|
45
|
+
// primary-axis gap (no vertical itemSpacing), and forces wrap. Comes
|
|
46
|
+
// up via `mapClassNameForBreakpoint`'s grid-cols-1 injection at the
|
|
47
|
+
// base breakpoint when the source had a responsive `sm:grid-cols-N`.
|
|
48
|
+
const hasMultiColumns = classes.some(c => {
|
|
49
|
+
const match = c.match(/^grid-cols-(\d+)$/);
|
|
50
|
+
return match != null && parseInt(match[1], 10) >= 2;
|
|
51
|
+
});
|
|
52
|
+
mode = hasMultiColumns ? 'HORIZONTAL' : 'VERTICAL';
|
|
43
53
|
directionSet = true;
|
|
44
54
|
continue;
|
|
45
55
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by build.mjs. Do not edit manually.
|
|
2
|
-
export const RENDER_ENGINE_VERSION = '
|
|
2
|
+
export const RENDER_ENGINE_VERSION = '40e3e9df6d50c02c';
|
package/src/tailwind/tailwind.ts
CHANGED
|
@@ -328,15 +328,20 @@ function applyCssDeclarationsToFrame(frame: FrameNode, declarations: Record<stri
|
|
|
328
328
|
continue;
|
|
329
329
|
}
|
|
330
330
|
if (prop === 'display' && (value === 'grid' || value === 'inline-grid')) {
|
|
331
|
-
|
|
331
|
+
// `grid-cols-1` (1 explicit column) is CSS-semantically a vertical
|
|
332
|
+
// stack — same as `grid` without any column class. Only flip to
|
|
333
|
+
// HORIZONTAL+WRAP when there are >=2 columns; otherwise route through
|
|
334
|
+
// the vertical-flow path so itemSpacing (primary axis = vertical)
|
|
335
|
+
// applies correctly and children don't get a stale horizontal layout.
|
|
336
|
+
if (declaredGridColumns != null && declaredGridColumns >= 2) {
|
|
332
337
|
frame.layoutMode = 'HORIZONTAL';
|
|
333
338
|
markGridColumnsNode(frame, declaredGridColumns);
|
|
334
339
|
if (frame.layoutWrap !== undefined) {
|
|
335
340
|
frame.layoutWrap = 'WRAP';
|
|
336
341
|
}
|
|
337
342
|
} else {
|
|
338
|
-
// CSS grid without explicit template columns
|
|
339
|
-
//
|
|
343
|
+
// CSS grid without explicit template columns (or with exactly 1
|
|
344
|
+
// column) behaves like a single-column vertical flow.
|
|
340
345
|
frame.layoutMode = 'VERTICAL';
|
|
341
346
|
markCssGridVerticalFrame(frame);
|
|
342
347
|
}
|
|
@@ -842,6 +847,13 @@ function applySemanticUtilitiesToFrame(
|
|
|
842
847
|
const atom = parseUtilityClass(cls);
|
|
843
848
|
if (!atom.utility) continue;
|
|
844
849
|
|
|
850
|
+
// Filter breakpoint-prefixed atoms FIRST so utilities like `sm:grid-cols-2`
|
|
851
|
+
// don't leak through to `markGridColumnsNode` at the base breakpoint.
|
|
852
|
+
// The responsive-analyzer normally strips breakpoint prefixes before
|
|
853
|
+
// calling here, but defense-in-depth: any atom carrying a non-active
|
|
854
|
+
// variant must be skipped before any structural side-effects run.
|
|
855
|
+
if (!shouldApplyAtom(atom, 'default')) continue;
|
|
856
|
+
|
|
845
857
|
const utility = atom.utility;
|
|
846
858
|
|
|
847
859
|
const gridColsMatch = utility.match(/^grid-cols-(\d+)$/);
|
|
@@ -860,8 +872,6 @@ function applySemanticUtilitiesToFrame(
|
|
|
860
872
|
continue;
|
|
861
873
|
}
|
|
862
874
|
|
|
863
|
-
if (!shouldApplyAtom(atom, 'default')) continue;
|
|
864
|
-
|
|
865
875
|
if (utility === 'flex' || utility === 'inline-flex') {
|
|
866
876
|
// Bare `flex` enables flex layout but does not set direction. Only write
|
|
867
877
|
// HORIZONTAL when the direction hasn't been declared yet by a prior
|
|
@@ -906,14 +916,25 @@ function applySemanticUtilitiesToFrame(
|
|
|
906
916
|
continue;
|
|
907
917
|
}
|
|
908
918
|
if (utility === 'grid' || utility === 'inline-grid') {
|
|
909
|
-
|
|
910
|
-
|
|
919
|
+
// Match parseLayoutMode + applyCssDeclarationsToFrame: only flip
|
|
920
|
+
// to HORIZONTAL+WRAP when there are >=2 columns. `grid-cols-1` is
|
|
921
|
+
// CSS-semantically a vertical stack (single column = no horizontal
|
|
922
|
+
// flow) and must route through the VERTICAL path so itemSpacing
|
|
923
|
+
// applies on the right axis. The `mapClassNameForBreakpoint`
|
|
924
|
+
// helper injects `grid-cols-1` at the base breakpoint when the
|
|
925
|
+
// source had a responsive `sm:grid-cols-N`, so this case is hit
|
|
926
|
+
// any time a grid with responsive columns renders at base.
|
|
927
|
+
const hasMultiColumns = classes.some((c: string) => {
|
|
928
|
+
const match = c.match(/^grid-cols-(\d+)$/);
|
|
929
|
+
return match != null && parseInt(match[1], 10) >= 2;
|
|
930
|
+
});
|
|
931
|
+
if (hasMultiColumns) {
|
|
911
932
|
frame.layoutMode = 'HORIZONTAL';
|
|
912
933
|
if (frame.layoutWrap !== undefined) {
|
|
913
934
|
frame.layoutWrap = 'WRAP';
|
|
914
935
|
}
|
|
915
936
|
} else {
|
|
916
|
-
// Single-column implicit grid → VERTICAL; children stretch to fill (CSS default align-items: stretch)
|
|
937
|
+
// Single-column implicit grid or `grid-cols-1` → VERTICAL; children stretch to fill (CSS default align-items: stretch)
|
|
917
938
|
frame.layoutMode = 'VERTICAL';
|
|
918
939
|
markCssGridVerticalFrame(frame);
|
|
919
940
|
}
|