From 96cecbc6bf2fb58be56d7b64bc28e35a17a2b126 Mon Sep 17 00:00:00 2001 From: Hein van Vlastuin Date: Tue, 18 Aug 2026 23:40:11 +0200 Subject: [PATCH] Fix HUG sizing collapsing to FIXED for TEXT nodes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A TEXT node never has a children array — its content is `characters`, a string, not child elements. The "HUG with nothing to hug falls back to FIXED" check read that absence as "nothing to hug" and forced every HUG-sized text node to FIXED, baking in whatever pixel width the text happened to render at in the source file. That width then breaks the moment surrounding layout differs from that original context (a different viewport, different sibling content, a wrapped label). Extracted the check into hugSizingIsMeaningless() so it's unit-testable without a live Figma document, and made it explicitly false for TEXT regardless of children. --- .../src/altNodes/jsonNodeConversion.test.ts | 20 +++++++++++ .../src/altNodes/jsonNodeConversion.ts | 35 ++++++++++++++----- 2 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 packages/backend/src/altNodes/jsonNodeConversion.test.ts diff --git a/packages/backend/src/altNodes/jsonNodeConversion.test.ts b/packages/backend/src/altNodes/jsonNodeConversion.test.ts new file mode 100644 index 00000000..1b8b489f --- /dev/null +++ b/packages/backend/src/altNodes/jsonNodeConversion.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; +import { hugSizingIsMeaningless } from "./jsonNodeConversion"; + +describe("hugSizingIsMeaningless", () => { + it("is false for a TEXT node with no children — it hugs its own characters", () => { + expect(hugSizingIsMeaningless({ type: "TEXT" })).toBe(false); + expect(hugSizingIsMeaningless({ type: "TEXT", children: [] })).toBe(false); + }); + + it("is true for an empty non-text frame — nothing to hug around", () => { + expect(hugSizingIsMeaningless({ type: "FRAME" })).toBe(true); + expect(hugSizingIsMeaningless({ type: "FRAME", children: [] })).toBe(true); + }); + + it("is false for a non-text frame with children — hugs its child content", () => { + expect( + hugSizingIsMeaningless({ type: "FRAME", children: [{}] }) + ).toBe(false); + }); +}); diff --git a/packages/backend/src/altNodes/jsonNodeConversion.ts b/packages/backend/src/altNodes/jsonNodeConversion.ts index d9b23f32..c96bdd6d 100644 --- a/packages/backend/src/altNodes/jsonNodeConversion.ts +++ b/packages/backend/src/altNodes/jsonNodeConversion.ts @@ -255,6 +255,24 @@ function adjustChildrenOrder(node: any) { node.children = [...absoluteChildren, ...fixedChildren]; } +/** + * True when a node's HUG sizing has nothing to hug — an empty frame, where + * HUG-with-no-content is meaningless and falling back to FIXED is correct. + * + * A TEXT node never has a `children` array (its content is `characters`, a + * string, not child elements), so a plain "does it have children" check + * reads every TEXT node as having nothing to hug and forces it to FIXED — + * wrong, since text hugs its own characters. TEXT is therefore never + * "meaningless" here regardless of `children`. + */ +export function hugSizingIsMeaningless(node: { + type: string; + children?: unknown[]; +}): boolean { + if (node.type === "TEXT") return false; + return !Array.isArray(node.children) || node.children.length === 0; +} + /** * Recursively process both JSON node and Figma node to update with data not available in JSON * This now includes the functionality from convertNodeToAltNode @@ -537,17 +555,16 @@ const processNodePair = async ( jsonNode.counterAxisAlignItems = "MIN"; } - // If layout sizing is HUG but there are no children, set it to FIXED - const hasChildren = - "children" in jsonNode && - jsonNode.children && - Array.isArray(jsonNode.children) && - jsonNode.children.length > 0; - - if (jsonNode.layoutSizingHorizontal === "HUG" && !hasChildren) { + if ( + jsonNode.layoutSizingHorizontal === "HUG" && + hugSizingIsMeaningless(jsonNode) + ) { jsonNode.layoutSizingHorizontal = "FIXED"; } - if (jsonNode.layoutSizingVertical === "HUG" && !hasChildren) { + if ( + jsonNode.layoutSizingVertical === "HUG" && + hugSizingIsMeaningless(jsonNode) + ) { jsonNode.layoutSizingVertical = "FIXED"; }