From a0e05efda94eada4ebc6a1aa1a34d2a9b14bd0e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Tue, 1 Sep 2026 02:55:07 +0100 Subject: [PATCH] Layout's controls move to the editor panel too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Colours, Typefaces and Padding sat below the preview and took their height from it — the same trade the page builder's editors made, and undone for the same reason: this screen exists to be looked at, and the controls were eating the thing being judged. Announced through the same slot, so the panel still holds nothing and still knows nothing about what a layout is. One thing that does not survive the move: a JSX comment cannot be an argument. The block carried its own `{/* … */}` above the tabs and it had to become an ordinary comment on the effect, which is where the reasoning belongs anyway now that the element is announced rather than rendered in place. Co-Authored-By: Claude Opus 5 (1M context) --- web/sections/LayoutPanel.tsx | 305 ++++++++++++++++++----------------- 1 file changed, 161 insertions(+), 144 deletions(-) diff --git a/web/sections/LayoutPanel.tsx b/web/sections/LayoutPanel.tsx index 899c2b4..819d2a3 100644 --- a/web/sections/LayoutPanel.tsx +++ b/web/sections/LayoutPanel.tsx @@ -8,6 +8,7 @@ import { useSaveTarget } from '../SaveTarget'; import { usePreviewable, useViewMode } from '../ViewMode'; import { applyGutter, paintMeasure } from '../canvas'; import { SitePreview, useDevPort } from '../SitePreview'; +import { useEditorSlot } from '../useEditorSlot'; import { type LayoutBand } from './PaddingTab'; import { GutterTab, type Gutter } from './GutterTab'; @@ -252,6 +253,166 @@ export const LayoutPanel = () => { [gutter], ); + const [, setEditor] = useEditorSlot(); + const setEditorRef = useRef(setEditor); + setEditorRef.current = setEditor; + + /* + The layout's controls, handed to the editor panel. + + They were below the preview and took their height from it — the same trade the page builder's + editors made, and the same reason for undoing it: this screen exists to be looked at, and the + controls were eating the thing being judged. + + Announced rather than rendered, because the panel is a SIBLING — see useEditorSlot. The dependency + list is everything the tabs DISPLAY; an element captures the props it was built with, so one left + out here reads as a control that has stopped responding rather than as an error. + */ + useEffect(() => { + if (!draft) { + setEditorRef.current(null); + return; + } + + setEditorRef.current( + + + + Colours + + + Typefaces + + + Padding + + + + {/* + EVERY panel in the same grid cell, so the block is as tall as the TALLEST of them and stops + moving when the tab changes. + + Measuring the panels and setting a height would need a ResizeObserver, a re-measure on every + edit, and a wrong answer for one frame after each. `grid-area: 1/1` gets it from the layout + engine for nothing, and it stays right when a tab is added or one of them grows a row — + which is the case that made the first fix insufficient. + + `invisible` and not `hidden`: an invisible element still takes its space, which IS the + mechanism. It is also removed from the tab order, so the controls behind the front panel + cannot be reached by keyboard. + */} +
+
+ <> +
+ {COLOURS.map(({ key, label, fallback }) => ( +
+ {/* + The browser's own picker. It sits on the LEFT of a wide row here, which is the whole + reason it works: the same control at the right-hand edge of this panel opened its popup + off the side of the window. See OverlayEditor for the version that had to move. + */} + patch({ [key]: event.currentTarget.value } as Partial)} + className="size-8 shrink-0 cursor-pointer rounded border border-input bg-transparent p-0.5" + title={label} + /> +
+ {label} + +
+
+ ))} +
+ +
+ +
+ <> + {/* + Four across, one row. + + At `sm:` and not `lg:`. The breakpoint here is the BROWSER's, not the panel's — and this panel + is wide in every window anybody runs the admin in — but lg is 1024 and there was no reason to + make four short selects wait for it. + + Two columns meant Typefaces was twice the height of Colours, so switching tabs moved + everything below it — and the preview above it changed size to match. A tab strip whose + contents jump is a tab strip that has to be re-read every time it is used. + + The hint moves into the option list's own space rather than a line under each select, which + is where the second row of height was really going. + */} +
+ {ROLES.map(({ key, label, hint }) => ( +
+ + + {hint} +
+ ))} +
+ +
+ +
+ {/* + The site's gutter, not a per-band padding. + + PaddingTab is the per-band control and it stays — for the pages, where a band really + does want its own spacing. Here the three wrappers share one measure and should share + one number. + + All three widths at once, and deliberately NOT tied to the device toggle: a gutter is a + progression, 4 then 8 then 12, and it is judged as one. Reading it off three separate + visits to the same control is how a site ends up tighter on a tablet than on a phone. + */} + patch({ padding_x })} + /> +
+
+
+ ); + }, [draft, tab, device]); + + // Cleared on unmount, or the panel goes on offering Layout's controls over somebody else's page. + useEffect(() => () => setEditorRef.current(null), []); + if (!draft) return
{error ?? 'Loading…'}
; /* @@ -280,150 +441,6 @@ export const LayoutPanel = () => { would have gone on being wrong differently every time either side changed. */} - - {/* - The controls as TABS, and only in Edit. - - They were two stacked cards, which took as much height as both of them together — and every - pixel of it came off the preview, which is the thing being judged. One at a time costs a click - and gives the page back most of the panel. - - `shrink-0` so they never take height from the preview, and no `max-h` any more: one tab is short - enough that it does not need capping, which is the other thing this bought. - */} - - - - Colours - - - Typefaces - - - Padding - - - - {/* - EVERY panel in the same grid cell, so the block is as tall as the TALLEST of them and stops - moving when the tab changes. - - Measuring the panels and setting a height would need a ResizeObserver, a re-measure on every - edit, and a wrong answer for one frame after each. `grid-area: 1/1` gets it from the layout - engine for nothing, and it stays right when a tab is added or one of them grows a row — - which is the case that made the first fix insufficient. - - `invisible` and not `hidden`: an invisible element still takes its space, which IS the - mechanism. It is also removed from the tab order, so the controls behind the front panel - cannot be reached by keyboard. - */} -
-
-<> -
- {COLOURS.map(({ key, label, fallback }) => ( -
- {/* - The browser's own picker. It sits on the LEFT of a wide row here, which is the whole - reason it works: the same control at the right-hand edge of this panel opened its popup - off the side of the window. See OverlayEditor for the version that had to move. - */} - patch({ [key]: event.currentTarget.value } as Partial)} - className="size-8 shrink-0 cursor-pointer rounded border border-input bg-transparent p-0.5" - title={label} - /> -
- {label} - -
-
- ))} -
- -
- -
-<> - {/* - Four across, one row. - - At `sm:` and not `lg:`. The breakpoint here is the BROWSER's, not the panel's — and this panel - is wide in every window anybody runs the admin in — but lg is 1024 and there was no reason to - make four short selects wait for it. - - Two columns meant Typefaces was twice the height of Colours, so switching tabs moved - everything below it — and the preview above it changed size to match. A tab strip whose - contents jump is a tab strip that has to be re-read every time it is used. - - The hint moves into the option list's own space rather than a line under each select, which - is where the second row of height was really going. - */} -
- {ROLES.map(({ key, label, hint }) => ( -
- - - {hint} -
- ))} -
- -
- -
- {/* - The site's gutter, not a per-band padding. - - PaddingTab is the per-band control and it stays — for the pages, where a band really - does want its own spacing. Here the three wrappers share one measure and should share - one number. - - All three widths at once, and deliberately NOT tied to the device toggle: a gutter is a - progression, 4 then 8 then 12, and it is judged as one. Reading it off three separate - visits to the same control is how a site ends up tighter on a tablet than on a phone. - */} - patch({ padding_x })} - /> -
-
-
- ); };