diff --git a/packages/core/src/render/renderer.ts b/packages/core/src/render/renderer.ts index 306218ddd..2b809abb9 100644 --- a/packages/core/src/render/renderer.ts +++ b/packages/core/src/render/renderer.ts @@ -201,7 +201,8 @@ async function renderNode(graph: SceneGraph, tree: TreeNode, parentId: string): const parentLayout = parent?.layoutMode ?? 'NONE' const isText = nodeType === 'TEXT' - const overrides = propsToOverrides(tree.props, isText, parentLayout) + const childCount = tree.children.filter((c) => typeof c !== 'string' && isTreeNode(c)).length + const overrides = propsToOverrides(tree.props, isText, parentLayout, childCount) if (isText) { const textContent = tree.children.filter((c): c is string => typeof c === 'string').join('') @@ -414,13 +415,25 @@ function applyAutoLayoutSizing( if (counterDim === 'hug') o.counterAxisSizing = 'HUG' } +function shouldEnableAutoLayout( + props: Record, + isText: boolean, + childCount: number +): boolean { + if (props.flex !== undefined) return true + if (!isText && hasAutoLayoutTriggerProps(props)) return true + if (!isText && !props.grid && childCount >= 2) return true + return false +} + function applyLayoutOverrides( props: Record, o: Partial, w: unknown, h: unknown, isText: boolean, - parentLayout: SceneNode['layoutMode'] + parentLayout: SceneNode['layoutMode'], + childCount: number ): void { if (props.grid) { applyGridOverrides(props, o, w, h) @@ -433,9 +446,7 @@ function applyLayoutOverrides( applyGridChildOverrides(props, o) } - const needsAutoLayout = props.flex !== undefined || (!isText && hasAutoLayoutTriggerProps(props)) - - if (needsAutoLayout) { + if (shouldEnableAutoLayout(props, isText, childCount)) { applyAutoLayoutSizing(o, props, w, h) } @@ -569,7 +580,8 @@ function applyShapeAndEffectOverrides(props: Record, o: Partial function propsToOverrides( props: Record, isText: boolean, - parentLayout: SceneNode['layoutMode'] + parentLayout: SceneNode['layoutMode'], + childCount = 0 ): Partial { const o: Partial = {} @@ -577,7 +589,7 @@ function propsToOverrides( const { w, h } = applySizeOverrides(props, o, parentLayout) applyVisualOverrides(props, o) - applyLayoutOverrides(props, o, w, h, isText, parentLayout) + applyLayoutOverrides(props, o, w, h, isText, parentLayout, childCount) if (isText) applyTextOverrides(props, o, parentLayout) applyShapeAndEffectOverrides(props, o) diff --git a/packages/core/src/tools/describe-layout-issues.ts b/packages/core/src/tools/describe-layout-issues.ts index e9a466ec8..7e0f16aac 100644 --- a/packages/core/src/tools/describe-layout-issues.ts +++ b/packages/core/src/tools/describe-layout-issues.ts @@ -101,9 +101,16 @@ function checkDividerOrientation(ctx: LayoutContext): void { } } +function willGetConcreteSize(node: SceneNode, isRow: boolean, graph: SceneGraph): boolean { + return node.layoutGrow > 0 + || node.layoutAlignSelf === 'STRETCH' + || isEffectivelyFilling(node, isRow, graph) +} + function checkGrowInHug(ctx: LayoutContext): void { - const { node, children, issues } = ctx + const { node, isRow, graph, children, issues } = ctx if (node.primaryAxisSizing !== 'HUG') return + if (willGetConcreteSize(node, isRow, graph)) return for (const child of children) { if (child.layoutGrow > 0) { issues.push({ @@ -164,9 +171,9 @@ function checkChildOverflow(ctx: LayoutContext): void { } function checkHugCollapse(ctx: LayoutContext): void { - const { node, isRow, children, issues } = ctx + const { node, isRow, graph, children, issues } = ctx if (children.length === 0) return - if (node.primaryAxisSizing === 'HUG' && children.every((c) => c.layoutGrow > 0)) { + if (node.primaryAxisSizing === 'HUG' && !willGetConcreteSize(node, isRow, graph) && children.every((c) => c.layoutGrow > 0)) { issues.push({ message: `"${node.name}" is HUG but all children use grow — collapses to zero`, suggestion: 'Give at least one child a fixed size, or set parent to fixed' @@ -234,7 +241,8 @@ function checkTextOverflow(ctx: LayoutContext): void { } function checkSiblingHeightConsistency(ctx: LayoutContext): void { - const { isRow, children, issues } = ctx + const { node, isRow, children, issues } = ctx + if (node.counterAxisAlign === 'CENTER') return const containers = children.filter((c) => CONTAINER_TYPES.has(c.type)) if (containers.length < 2) return const dim = isRow ? 'height' : 'width' @@ -290,6 +298,7 @@ function checkAbsoluteInFlex(_ctx: LayoutContext): void { function checkNestedFlexWithoutFill(ctx: LayoutContext): void { const { node, isRow, children, issues } = ctx if (node.layoutMode === 'NONE') return + if (node.primaryAxisAlign === 'SPACE_BETWEEN' || node.primaryAxisAlign === 'CENTER') return for (const child of children) { if (child.layoutMode === 'NONE') continue const crossDim = isRow ? child.width : child.height diff --git a/src/ai/system-prompt.md b/src/ai/system-prompt.md index d7adbe292..e2c599d01 100644 --- a/src/ai/system-prompt.md +++ b/src/ai/system-prompt.md @@ -32,7 +32,9 @@ These are ALL available props. Nothing else exists. ## Layout rules -⚠ **Every parent with children using `w="fill"` or `h="fill"` MUST have `flex="col"` or `flex="row"`.** Without flex, fill is ignored. This is the #1 layout bug. +⚠ **Every Frame with 2+ children needs `flex="col"` or `flex="row"`.** Without it, children stack at (0,0). Card with photo + info → `flex="col"`. Row of buttons → `flex="row"`. Only omit for decorative layers with explicit x/y positioning. + +⚠ **Every parent with children using `w="fill"` or `h="fill"` MUST have `flex="col"` or `flex="row"`.** Without flex, fill is ignored. justify/items require flex. The value is "between", not "space-between". @@ -82,6 +84,8 @@ No style={{}}, className, CSS. No named colors or rgb(). No percentage values. N **Don't mix `w={N}` and `grow={N}`** — grow overrides width. +**Tab bar / Bottom nav:** Outer frame `flex="row" w="fill" justify="between" px={32}`. Each tab `flex="col" items="center" gap={4}`. Tab items are HUG-width — `justify="between"` distributes them. Don't use `grow` on individual tabs. + **Dividers:** Vertical `w={1} h="fill"` inside flex="row". Horizontal `h={1} w="fill"` inside flex="col". # Workflow (MANDATORY) @@ -102,7 +106,6 @@ Typically **3 renders + 3–4 describes**. `describe` the root with `depth=2` - "gap N not on 8px grid" → fix the gap - "grow inside HUG parent" → set parent to fixed size or use h="fill" -- "nested flex may collapse" → add w="fill" or grow - "duplicate sibling names" → rename - "near-invisible fill" → increase alpha