From ac0347ae5f19e702ca72dd9171b745eca266f070 Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Sun, 24 May 2026 15:35:05 -0700 Subject: [PATCH] sitesmith: fix blank canvas on Replace site treeToState() was setting isCanvas:true on every node, including leaf components (Heading, TextBlock, ButtonLink, Spacer, ImageBlock). Craft.js then renders those as empty drop-canvas wrappers instead of their actual content, so the canvas appears blank after applying an AI-generated 'replace' response. Now uses a CANVAS_TYPES set matching the apply-ai-response utility: only the layout wrappers (Container, Section, ColumnLayout, Hero/Features/ CTA sections, FormContainer, Navbar, Footer, etc.) are canvases. ROOT is forced to be a canvas regardless of source type so children render. Also defensively normalizes props.style: AI sometimes emits an empty array instead of an object, which can confuse downstream consumers. --- craft/src/state/PageContext.tsx | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/craft/src/state/PageContext.tsx b/craft/src/state/PageContext.tsx index b5a82f1..1bf67df 100644 --- a/craft/src/state/PageContext.tsx +++ b/craft/src/state/PageContext.tsx @@ -4,6 +4,17 @@ import { PageData } from '../types'; import { SerializedTreeNode } from '../types/sitesmith'; import { useSiteDesign, SiteDesign } from './SiteDesignContext'; +/** Layout components that accept children. Must match the .craft.rules.canMoveIn + * config of each component. Leaf components (Heading, TextBlock, ButtonLink, …) + * are NOT canvases and must serialize with isCanvas:false or their rendered + * content gets swallowed by an empty drop-target wrapper. */ +const CANVAS_TYPES = new Set([ + 'Container', 'Section', 'ColumnLayout', 'BackgroundSection', + 'HeaderZone', 'FooterZone', + 'HeroSimple', 'FeaturesGrid', 'CTASection', + 'FormContainer', 'Navbar', 'Footer', +]); + interface PageContextValue { pages: PageData[]; headerPage: PageData; @@ -290,11 +301,21 @@ export const PageProvider: React.FC<{ children: ReactNode }> = ({ children }) => const walk = (node: SerializedTreeNode, parent: string | null): string => { const id = (node.props.node_id as string | undefined) || `ai-auto-${counter++}`; const childIds: string[] = []; + const typeName = node.type?.resolvedName; + // Normalize props: the AI sometimes emits `style: []` instead of `{}`. + // React/Craft.js choke when a CSSProperties slot is an array — normalize it. + const rawProps = node.props ?? {}; + const props: Record = { ...rawProps }; + if (Array.isArray(props.style)) props.style = {}; nodes[id] = { type: node.type, - isCanvas: true, - props: node.props, - displayName: node.type.resolvedName, + // isCanvas must match the component's craft.rules — only layout + // wrappers accept children. Setting it true on leaf components + // (Heading, TextBlock, ButtonLink, etc) makes Craft.js render them + // as empty drop-canvas wrappers and the actual content disappears. + isCanvas: typeName ? CANVAS_TYPES.has(typeName) : false, + props, + displayName: typeName, custom: {}, hidden: false, parent, @@ -311,6 +332,8 @@ export const PageProvider: React.FC<{ children: ReactNode }> = ({ children }) => if (rootId !== 'ROOT') { nodes['ROOT'] = nodes[rootId]; (nodes['ROOT'] as any).parent = null; + // ROOT must be a canvas regardless of component type so children render. + (nodes['ROOT'] as any).isCanvas = true; delete nodes[rootId]; // Fix up parent references from ROOT's children for (const childId of (nodes['ROOT'] as any).nodes) {