From 7d77201e5a430e491a4c7562cdc16b75cdfd0a2e Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Sun, 1 Nov 2026 09:14:28 -0800 Subject: [PATCH] fix(site-builder): Container 40px drop-zone min-height only when empty Co-Authored-By: Claude Sonnet 5.5 --- .../layout/Container.dropZone.test.tsx | 33 +++++++++++++++++++ craft/src/components/layout/Container.tsx | 5 ++- 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 craft/src/components/layout/Container.dropZone.test.tsx diff --git a/craft/src/components/layout/Container.dropZone.test.tsx b/craft/src/components/layout/Container.dropZone.test.tsx new file mode 100644 index 0000000..3621b4e --- /dev/null +++ b/craft/src/components/layout/Container.dropZone.test.tsx @@ -0,0 +1,33 @@ +import { describe, test, expect, afterEach } from 'vitest'; +import { renderEditorHarness, EditorHarness } from '../../test-utils/editorHarness'; + +// The 40px min-height is an editor-only DROP ZONE for EMPTY containers. A container that has +// content must lay out like the published page (no injected min-height). +const node = (type: string, props: Record, nodes: string[] = [], extra: Record = {}) => ({ + type: { resolvedName: type }, isCanvas: type === 'Container' || type === 'Text', props, displayName: type, custom: {}, hidden: false, nodes, linkedNodes: {}, ...extra, +}); +let h: EditorHarness | null = null; +afterEach(() => { h?.unmount(); h = null; }); +const mount = (inner: Record, withChild: boolean) => { + h = renderEditorHarness({ + initialState: JSON.stringify({ + ROOT: node('Container', { tag: 'div', style: {} }, ['c1']), + c1: node('Container', { tag: 'section', ...inner }, withChild ? ['t1'] : [], { parent: 'ROOT' }), + ...(withChild ? { t1: node('TextBlock', { text: 'hi', style: {} }, [], { parent: 'c1' }) } : {}), + }), + }); + return h.container.querySelector('section') as HTMLElement; +}; + +describe('Container empty-only drop zone', () => { + test('empty container keeps the 40px drop zone', () => { + expect(mount({ style: {} }, false).style.minHeight).toBe('40px'); + }); + test('non-empty container does NOT get an injected min-height', () => { + expect(mount({ style: {} }, true).style.minHeight).toBe(''); + }); + test('an explicit style.minHeight always wins', () => { + expect(mount({ style: { minHeight: '100px' } }, true).style.minHeight).toBe('100px'); + expect(mount({ style: { minHeight: '100px' } }, false).style.minHeight).toBe('100px'); + }); +}); diff --git a/craft/src/components/layout/Container.tsx b/craft/src/components/layout/Container.tsx index 44ec28d..4797f83 100644 --- a/craft/src/components/layout/Container.tsx +++ b/craft/src/components/layout/Container.tsx @@ -95,8 +95,11 @@ export const Container: UserComponent = ({ // follows the current device's Alignment / Vertical alignment. const layout = containerLayout(style, needsBoxedWrapper); + // 40px min-height = editor-only DROP ZONE, so only an EMPTY container gets it. A container with + // content must size like the published page (toHtml adds no min-height). + const isEmpty = React.Children.count(children) === 0; const outerStyle: CSSProperties = { - minHeight: '40px', + ...(isEmpty ? { minHeight: '40px' } : {}), ...style, ...(fullWidth ? { width: '100vw', marginLeft: 'calc(-50vw + 50%)' } : {}), ...layout.outer,