Skip to content

Commit 309c60e

Browse files
authored
fix(layout): suppress computed gap values when using SPACE_BETWEEN (#341)
When primaryAxisAlignItems or counterAxisAlignContent is SPACE_BETWEEN, Figma computes gaps dynamically but the API still returns stale spacing values. Suppress these and also add missing counterAxisSpacing support for wrapped flex layouts. Closes #169
1 parent b0f9efc commit 309c60e

2 files changed

Lines changed: 102 additions & 1 deletion

File tree

src/tests/layout-alignment.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,85 @@ describe("layout alignment", () => {
7272
});
7373
});
7474

75+
describe("gap suppression with SPACE_BETWEEN", () => {
76+
test("primary: itemSpacing suppressed when SPACE_BETWEEN", () => {
77+
const node = makeFrame({
78+
primaryAxisAlignItems: "SPACE_BETWEEN",
79+
itemSpacing: 10,
80+
});
81+
expect(buildSimplifiedLayout(node).gap).toBeUndefined();
82+
});
83+
84+
test("primary: itemSpacing preserved for other alignment modes", () => {
85+
const node = makeFrame({
86+
primaryAxisAlignItems: "MIN",
87+
itemSpacing: 10,
88+
});
89+
expect(buildSimplifiedLayout(node).gap).toBe("10px");
90+
});
91+
92+
test("counter: counterAxisSpacing suppressed when SPACE_BETWEEN", () => {
93+
const node = makeFrame({
94+
layoutWrap: "WRAP",
95+
counterAxisAlignContent: "SPACE_BETWEEN",
96+
counterAxisSpacing: 24,
97+
primaryAxisAlignItems: "SPACE_BETWEEN",
98+
itemSpacing: 10,
99+
});
100+
expect(buildSimplifiedLayout(node).gap).toBeUndefined();
101+
});
102+
103+
test("counter: counterAxisSpacing preserved when AUTO", () => {
104+
const node = makeFrame({
105+
layoutWrap: "WRAP",
106+
counterAxisAlignContent: "AUTO",
107+
counterAxisSpacing: 24,
108+
itemSpacing: 10,
109+
});
110+
expect(buildSimplifiedLayout(node).gap).toBe("24px 10px");
111+
});
112+
113+
test("wrapped row: both gaps emit CSS shorthand (row-gap column-gap)", () => {
114+
const node = makeFrame({
115+
layoutMode: "HORIZONTAL",
116+
layoutWrap: "WRAP",
117+
itemSpacing: 10,
118+
counterAxisSpacing: 24,
119+
});
120+
// row layout: counterAxisSpacing=row-gap, itemSpacing=column-gap
121+
expect(buildSimplifiedLayout(node).gap).toBe("24px 10px");
122+
});
123+
124+
test("wrapped column: both gaps emit CSS shorthand (row-gap column-gap)", () => {
125+
const node = makeFrame({
126+
layoutMode: "VERTICAL",
127+
layoutWrap: "WRAP",
128+
itemSpacing: 10,
129+
counterAxisSpacing: 24,
130+
});
131+
// column layout: itemSpacing=row-gap, counterAxisSpacing=column-gap
132+
expect(buildSimplifiedLayout(node).gap).toBe("10px 24px");
133+
});
134+
135+
test("wrapped: equal gaps collapse to single value", () => {
136+
const node = makeFrame({
137+
layoutWrap: "WRAP",
138+
itemSpacing: 16,
139+
counterAxisSpacing: 16,
140+
});
141+
expect(buildSimplifiedLayout(node).gap).toBe("16px");
142+
});
143+
144+
test("counterAxisSpacing ignored for non-wrapped layouts", () => {
145+
const node = makeFrame({
146+
layoutWrap: "NO_WRAP",
147+
itemSpacing: 10,
148+
counterAxisSpacing: 24,
149+
});
150+
expect(buildSimplifiedLayout(node).gap).toBe("10px");
151+
});
152+
});
153+
75154
describe("alignItems stretch detection", () => {
76155
test("row: all children fill cross axis → stretch", () => {
77156
const node = makeFrame({

src/transformers/layout.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,28 @@ function convertSelfAlign(align?: HasLayoutTrait["layoutAlign"]) {
103103
}
104104
}
105105

106+
// SPACE_BETWEEN computes gaps dynamically — the API returns stale spacing
107+
// values, but Figma's UI shows "Auto". Suppress the affected axis.
108+
function buildGap(n: HasFramePropertiesTrait, mode: "row" | "column"): string | undefined {
109+
const primaryGap = n.primaryAxisAlignItems === "SPACE_BETWEEN" ? undefined : n.itemSpacing;
110+
const counterGap =
111+
n.layoutWrap !== "WRAP" || n.counterAxisAlignContent === "SPACE_BETWEEN"
112+
? undefined
113+
: n.counterAxisSpacing;
114+
115+
// Map Figma's primary/counter axes to CSS's row/column axes
116+
const rowGap = mode === "row" ? counterGap : primaryGap;
117+
const colGap = mode === "row" ? primaryGap : counterGap;
118+
119+
return gapShorthand(rowGap, colGap);
120+
}
121+
122+
function gapShorthand(row?: number, col?: number): string | undefined {
123+
if (!row && !col) return undefined;
124+
if (row && col) return row === col ? `${row}px` : `${row}px ${col}px`;
125+
return `${(row ?? col)!}px`;
126+
}
127+
106128
// interpret sizing
107129
function convertSizing(
108130
s?: HasLayoutTrait["layoutSizingHorizontal"] | HasLayoutTrait["layoutSizingVertical"],
@@ -146,7 +168,7 @@ function buildSimplifiedFrameValues(n: FigmaDocumentNode): SimplifiedLayout | {
146168

147169
// Only include wrap if it's set to WRAP, since flex layouts don't default to wrapping
148170
frameValues.wrap = n.layoutWrap === "WRAP" ? true : undefined;
149-
frameValues.gap = n.itemSpacing ? `${n.itemSpacing ?? 0}px` : undefined;
171+
frameValues.gap = buildGap(n, frameValues.mode);
150172
// gather padding
151173
if (n.paddingTop || n.paddingBottom || n.paddingLeft || n.paddingRight) {
152174
frameValues.padding = generateCSSShorthand({

0 commit comments

Comments
 (0)