sitesmith: AI-built pages render full-bleed (page root padding 0)

The page root Container's default 20px padding framed every AI-built section
in a white margin (visible in every eval screenshot). New AI page/site
replaces now set padding 0 on the page root unless the model set padding
itself. Saved pages, section inserts, patches and the Container default are
unchanged, so existing sites do not move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shadowdaoandClaude Opus 5.5 committed 2026-11-01 16:42:49 -08:00
1 parent 0fe7f07162
commit 0c9faf0495
3 files changed
+52

No files matched your search

+27
View File
@@ -88,3 +88,30 @@ describe('planReplace', () => {
expect(planReplace(patch, { userText: 'x', chromeDefault: true })).toEqual({ resp: patch, confirm: null });
});
});
import { fullBleedPageRoots } from './replace-plan';
describe('fullBleedPageRoots (AI-built pages have no white frame around every section)', () => {
const page = (rootProps: any): SitesmithResponse => ({ type: 'replace', scope: 'page', message: 'm', pages: [{ name: 'Home', tree: { type: { resolvedName: 'Container' }, props: rootProps, nodes: [] } as any }] });
test('a root Container without padding gets padding 0 (the Container default is 20px)', () => {
const out = fullBleedPageRoots(page({ tag: 'div', node_id: 'ai-root-1' })) as any;
expect(out.pages[0].tree.props.style).toEqual({ padding: '0px' });
const out2 = fullBleedPageRoots(page({ tag: 'div', style: { backgroundColor: '#000' } })) as any;
expect(out2.pages[0].tree.props.style).toEqual({ padding: '0px', backgroundColor: '#000' });
});
test('padding the model set on purpose is kept; non-Container roots, sections and patches are untouched', () => {
expect((fullBleedPageRoots(page({ style: { padding: '40px' } })) as any).pages[0].tree.props.style).toEqual({ padding: '40px' });
expect((fullBleedPageRoots(page({ style: { paddingTop: '8px' } })) as any).pages[0].tree.props.style).toEqual({ paddingTop: '8px' });
const sec: SitesmithResponse = { ...(page({}) as any), scope: 'section' };
expect(fullBleedPageRoots(sec)).toBe(sec);
const patch: SitesmithResponse = { type: 'patch', message: 'm', ops: [] };
expect(fullBleedPageRoots(patch)).toBe(patch);
const hero: SitesmithResponse = { type: 'replace', scope: 'page', message: 'm', pages: [{ name: 'Home', tree: { type: { resolvedName: 'HeroSimple' }, props: {}, nodes: [] } as any }] };
expect((fullBleedPageRoots(hero) as any).pages[0].tree.props.style).toBeUndefined();
});
test('site replace: every page root; the input is not mutated', () => {
const r: SitesmithResponse = { type: 'replace', scope: 'site', message: 'm', pages: [0, 1].map((i) => ({ name: `P${i}`, tree: { type: { resolvedName: 'Container' }, props: {}, nodes: [] } as any })) };
const out = fullBleedPageRoots(r) as any;
expect(out.pages.map((p: any) => p.tree.props.style)).toEqual([{ padding: '0px' }, { padding: '0px' }]);
expect((r as any).pages[0].tree.props.style).toBeUndefined();
});
});
+21
View File
@@ -99,3 +99,24 @@ export function withoutChrome(resp: SitesmithResponse): SitesmithResponse {
const { header: _h, footer: _f, ...rest } = resp;
return rest as SitesmithResponse;
}
const PADDING_KEYS = ['padding', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft', 'paddingBlock', 'paddingInline'];
/**
* AI-built pages render full-bleed: the page root Container gets padding 0 unless the model set padding
* itself. Without this the Container default (20px) framed every section in a white margin. Only new AI
* page/site replaces are touched -- saved pages and the Container default stay as they are.
*/
export function fullBleedPageRoots(resp: SitesmithResponse): SitesmithResponse {
if (resp.type !== 'replace' || (resp.scope !== 'page' && resp.scope !== 'site')) return resp;
return {
...resp,
pages: resp.pages.map((p) => {
const t = p.tree;
if (!t || t.type?.resolvedName !== 'Container') return p;
const style = (t.props?.style && typeof t.props.style === 'object' && !Array.isArray(t.props.style) ? t.props.style : {}) as Record<string, unknown>;
if (PADDING_KEYS.some((k) => style[k] !== undefined && style[k] !== '')) return p;
return { ...p, tree: { ...t, props: { ...(t.props ?? {}), style: { padding: '0px', ...style } } } };
}),
};
}
+4
View File
@@ -1,6 +1,7 @@
import { useEditor } from '@craftjs/core';
import { insertTargetOutsideInstance, isInsideInstance } from './global-sections';
import { usePages, treeToCraftState } from '../state/PageContext';
import { fullBleedPageRoots } from '../sitesmith/replace-plan';
import { canvasFingerprint, type CanvasFingerprint } from './canvas-fingerprint';
import { SitesmithResponse, SerializedTreeNode, SitesmithPatchOp } from '../types/sitesmith';
import { sanitizeAiTree, buildNodeTree } from './craft-tree';
@@ -123,6 +124,9 @@ export function useApplyAiResponse() {
return typeof name === 'string' ? name : undefined;
});
// AI-built pages are full-bleed (no 20px white frame from the Container default) -- new replaces only.
resp = fullBleedPageRoots(resp);
// Contrast auto-repair (design "Settled calls" 8): fix failing text colours before they reach the canvas.
let adjustments: ContrastAdjustment[] = [];
let notChecked = 0;