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 })}
- />
-