the open page lists its bands in the nav

A third level, under the selected page only — every page has bands, and listing
all of them would make the nav longer than the site.

Announced by the builder rather than fetched again: it already reads them to know
what it is looking at in the document, and it mounts exactly when a page is open.
Cleared on unmount, or the nav keeps listing the last page's bands.

Clicking one writes a third URL segment and the preview scrolls to it, keyed on
the render count too so the position survives a save replacing the frame.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-01 09:45:07 +01:00
co-authored by Claude Opus 5
parent 2eb9142484
commit 7c4c3f48a8
4 changed files with 89 additions and 10 deletions
+24 -2
View File
@@ -35,7 +35,7 @@ import { useEditorSlot } from '../useEditorSlot';
import { AddComponentDialog, type ComponentKind } from './AddComponentDialog';
import { MediaPicker } from '../MediaPicker';
import { GalleryPicker } from '../GalleryPicker';
import { pageFromParam, usePages, type Page } from '../usePages';
import { pageFromParam, usePages, useSections, type Page } from '../usePages';
/**
* One page, being built.
@@ -57,10 +57,14 @@ export const PageBuilderPanel = () => {
const clientRef = useRef(client);
clientRef.current = client;
const { item: slug } = useNav();
const { item: slug, part } = useNav();
const devPort = useDevPort();
const { pages, announce } = usePages();
const { announceSections } = useSections();
// Cleared when this unmounts, or the nav goes on listing the last page's bands under whatever is
// selected next.
useEffect(() => () => announceSections(null), [announceSections]);
// This section has a preview, so the bar offers Edit/View and the three widths — the same opt-in
// Layout makes. Pages, the list, does not: there is nothing there to view at tablet width.
@@ -160,6 +164,21 @@ export const PageBuilderPanel = () => {
*/
const frameDoc = useRef<Document | null>(null);
/*
The band named by the third URL segment, brought into view.
That segment is what the nav's rows under the open page write, and this is what makes them do
something. `rendered` is a dependency as well as `part`: the frame is replaced on every write, and
a scroll issued against the old document would be lost — this way the position survives a save.
The band is found by `data-section-id`, the marker the site already carries for the admin.
*/
useEffect(() => {
if (!part) return;
const band = frameDoc.current?.querySelector(`[data-section-id="${CSS.escape(part)}"]`);
band?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, [part, rendered]);
const onDocument = useCallback(
(frameDocument: Document) => {
frameDoc.current = frameDocument;
@@ -246,6 +265,9 @@ export const PageBuilderPanel = () => {
.get<{ data: CanvasSection[]; steps?: number[] }>(`/fofos-admin/pages/${page.id}/sections`)
.then((response) => {
setSections(response.data);
// The nav draws these as rows under the open page — see useSections for why it is announced
// rather than fetched a second time.
announceSections(response.data.map((section) => ({ id: section.id, name: section.name })));
if (response.steps) setSteps(response.steps);
})
.catch((failure: unknown) => setError(readableError(failure)));