an editor opens beside the page, not under it
The spacing and free-text editors rendered below the preview, so they took their height from the thing being edited: choosing a spacing shrank the page you were choosing it for, and on a laptop two dropdowns cost a third of the frame. They are a third panel now, to the right of the preview. The workspace already knows how to size and move panels; a drawer was a second, worse layout system living inside one of them. The panel holds nothing. Whichever panel owns a selection BUILDS the editor and announces it through a `useGlobal` slot — the two are siblings and neither may own the other, and a provider around both would mean something above them knowing they exist. The slot carries the ELEMENT, not a description of one. A descriptor plus a switch would move every editor's loading, saving and cancelling into a panel that would have to learn what a section is — a second place that understands the page builder, kept in step by hand. SaveTarget makes the same trade for the same reason. The hazard is real and worth naming: an element captures the props it was built with, so the announcing effect must depend on everything the editor DISPLAYS. The save handlers sit in refs so they cannot drag that list wider — depending on them would re-announce on every keystroke, and omitting them without refs would announce a stale one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,7 @@ const PLACES_ONLY: string[] = ['contact-form', 'partners'];
|
||||
import { AddSectionDialog, type SectionLayout } from './AddSectionDialog';
|
||||
import { FreeTextEditor } from './FreeTextEditor';
|
||||
import { SpacingEditor, type Padding } from './SpacingEditor';
|
||||
import { useEditorSlot } from '../useEditorSlot';
|
||||
import { AddComponentDialog, type ComponentKind } from './AddComponentDialog';
|
||||
import { MediaPicker } from '../MediaPicker';
|
||||
import { GalleryPicker } from '../GalleryPicker';
|
||||
@@ -477,6 +478,68 @@ export const PageBuilderPanel = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const [, setEditor] = useEditorSlot();
|
||||
const setEditorRef = useRef(setEditor);
|
||||
setEditorRef.current = setEditor;
|
||||
const saveSpacingRef = useRef(saveSpacing);
|
||||
saveSpacingRef.current = saveSpacing;
|
||||
const saveTextRef = useRef(saveText);
|
||||
saveTextRef.current = saveText;
|
||||
|
||||
/*
|
||||
Hand the open editor to the editor panel.
|
||||
|
||||
Announced rather than rendered, because the panel is a SIBLING — see useEditorSlot. The dependency
|
||||
list is everything either editor DISPLAYS, which is the part that is easy to get wrong: an element
|
||||
captures the props it was built with, so one left out here shows up as a control that has quietly
|
||||
stopped responding rather than as an error.
|
||||
|
||||
The handlers are in refs for that reason. They are rebuilt every render, and depending on them
|
||||
would re-announce on every keystroke; leaving them out without refs would announce a stale one.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (spacing) {
|
||||
setEditorRef.current(
|
||||
<SpacingEditor
|
||||
label={spacing.label}
|
||||
padding={spacing.padding}
|
||||
device={device}
|
||||
steps={steps}
|
||||
busy={false}
|
||||
onChange={(padding) => void saveSpacingRef.current(padding)}
|
||||
onClose={() => setSpacing(null)}
|
||||
/>,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (editing) {
|
||||
setEditorRef.current(
|
||||
<FreeTextEditor
|
||||
html={editing.html}
|
||||
busy={creating}
|
||||
onChange={(html) => setEditing((was) => (was ? { ...was, html } : was))}
|
||||
/*
|
||||
Cancelling reloads the frame rather than putting the old markup back by hand. The node has
|
||||
been written into on every keystroke, and restoring it correctly means remembering what was
|
||||
there before the first one — a reload is the same answer with nothing to get wrong.
|
||||
*/
|
||||
onCancel={() => {
|
||||
setEditing(null);
|
||||
setRendered((n) => n + 1);
|
||||
}}
|
||||
onSave={() => void saveTextRef.current()}
|
||||
/>,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setEditorRef.current(null);
|
||||
}, [spacing, editing, creating, device, steps]);
|
||||
|
||||
// Cleared on unmount, or the panel goes on offering an editor for a page nobody is looking at.
|
||||
useEffect(() => () => setEditorRef.current(null), []);
|
||||
|
||||
handlersRef.current = {
|
||||
add: () => setAdding(true),
|
||||
remove: (sectionId: number) => void deleteSection(sectionId),
|
||||
@@ -532,35 +595,6 @@ export const PageBuilderPanel = () => {
|
||||
selected={choosingGallery?.galleryId ?? null}
|
||||
/>
|
||||
|
||||
{spacing && (
|
||||
<SpacingEditor
|
||||
label={spacing.label}
|
||||
padding={spacing.padding}
|
||||
device={device}
|
||||
steps={steps}
|
||||
busy={false}
|
||||
onChange={(padding) => void saveSpacing(padding)}
|
||||
onClose={() => setSpacing(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{editing && (
|
||||
<FreeTextEditor
|
||||
html={editing.html}
|
||||
busy={creating}
|
||||
onChange={(html) => setEditing((was) => (was ? { ...was, html } : was))}
|
||||
/*
|
||||
Cancelling reloads the frame rather than putting the old markup back by hand. The node has
|
||||
been written into on every keystroke, and restoring it correctly means remembering what was
|
||||
there before the first one — a reload is the same answer with nothing to get wrong.
|
||||
*/
|
||||
onCancel={() => {
|
||||
setEditing(null);
|
||||
setRendered((n) => n + 1);
|
||||
}}
|
||||
onSave={() => void saveText()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user