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