a band lists its blocks, and selection is the URL
A fourth level under the open band only — a page of six bands with four blocks each is thirty rows, and the nav stops being somewhere you can find anything. Blocks are named by the same list the picker offers, so a thing is called the same word wherever it appears. Clicking a block in the PAGE now writes that address too, rather than calling the handler directly. Both ways of picking a block are the same act, so neither can disagree with the other about what is selected — and closing it is the band row. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -40,7 +40,7 @@ type Choice = {
|
||||
* inventing a roadmap. Gallery stopped being one of them on 2026-08-31: the set, its ordering and the
|
||||
* carousel that draws it were all already built, so the block is a reference to one.
|
||||
*/
|
||||
const CHOICES: Choice[] = [
|
||||
export const CHOICES: Choice[] = [
|
||||
{ kind: 'free-text', label: 'Text', hint: 'Words, headings and links', Icon: Type },
|
||||
/*
|
||||
One entry, either kind, because the block does not store which.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useNav } from '../routes';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { href, useNav } from '../routes';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { readableError } from '../errors';
|
||||
import { SitePreview, useDevPort } from '../SitePreview';
|
||||
@@ -35,7 +36,7 @@ import { useEditorSlot } from '../useEditorSlot';
|
||||
import { AddComponentDialog, type ComponentKind } from './AddComponentDialog';
|
||||
import { MediaPicker } from '../MediaPicker';
|
||||
import { GalleryPicker } from '../GalleryPicker';
|
||||
import { pageFromParam, usePages, useSections, type Page } from '../usePages';
|
||||
import { pageFromParam, pageParam, usePages, useSections, type Page } from '../usePages';
|
||||
|
||||
/**
|
||||
* One page, being built.
|
||||
@@ -57,7 +58,7 @@ export const PageBuilderPanel = () => {
|
||||
const clientRef = useRef(client);
|
||||
clientRef.current = client;
|
||||
|
||||
const { item: slug, part } = useNav();
|
||||
const { item: slug, part, piece } = useNav();
|
||||
|
||||
const devPort = useDevPort();
|
||||
const { pages, announce } = usePages();
|
||||
@@ -164,6 +165,31 @@ export const PageBuilderPanel = () => {
|
||||
*/
|
||||
const frameDoc = useRef<Document | null>(null);
|
||||
|
||||
/*
|
||||
Picking a block writes the fourth path segment; this opens the editor for whatever it names.
|
||||
|
||||
Through a ref for the same reason the other handlers are: `onDocument` must depend on the sections
|
||||
and nothing else, or it repaints on every render.
|
||||
*/
|
||||
const navigate = useNavigate();
|
||||
const navigateRef = useRef<(component: CanvasComponent) => void>(() => {});
|
||||
navigateRef.current = (component) => {
|
||||
if (!page) return;
|
||||
const owner = sections.find((section) => section.components.some((one) => one.id === component.id));
|
||||
if (!owner) return;
|
||||
navigate(href('pages', pageParam(page), String(owner.id), String(component.id)), { replace: true });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!piece) {
|
||||
// The band row was pressed, or the page was. Nothing is selected, so nothing should be open.
|
||||
setEditing(null);
|
||||
return;
|
||||
}
|
||||
const found = sections.flatMap((section) => section.components).find((one) => String(one.id) === piece);
|
||||
if (found) editComponentRef.current(found);
|
||||
}, [piece, sections]);
|
||||
|
||||
/*
|
||||
The band named by the third URL segment, brought into view.
|
||||
|
||||
@@ -187,7 +213,14 @@ export const PageBuilderPanel = () => {
|
||||
onAddSection: () => handlersRef.current.add(),
|
||||
onDeleteSection: (section) => handlersRef.current.remove(section.id),
|
||||
onAddComponent: (section, column) => handlersRef.current.addComponent(section, column),
|
||||
onEditComponent: (component) => handlersRef.current.editComponent(component),
|
||||
/*
|
||||
Selection goes through the URL, not straight to the handler.
|
||||
|
||||
The nav's rows write the same address, so both ways of picking a block are the same act and
|
||||
neither can disagree with the other about what is selected. The effect below is what opens
|
||||
the editor, whichever wrote it.
|
||||
*/
|
||||
onEditComponent: (component) => navigateRef.current(component),
|
||||
// Nothing to edit, so no button. See PLACES_ONLY.
|
||||
canEditComponent: (component) => !PLACES_ONLY.includes(component.kind),
|
||||
onDeleteComponent: (component) => handlersRef.current.removeComponent(component),
|
||||
@@ -267,7 +300,13 @@ export const PageBuilderPanel = () => {
|
||||
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 })));
|
||||
announceSections(
|
||||
response.data.map((section) => ({
|
||||
id: section.id,
|
||||
name: section.name,
|
||||
components: section.components.map((component) => ({ id: component.id, kind: component.kind })),
|
||||
})),
|
||||
);
|
||||
if (response.steps) setSteps(response.steps);
|
||||
})
|
||||
.catch((failure: unknown) => setError(readableError(failure)));
|
||||
@@ -371,6 +410,9 @@ export const PageBuilderPanel = () => {
|
||||
}
|
||||
};
|
||||
|
||||
/* Held so the effect that reads the URL can call it without depending on its identity. */
|
||||
const editComponentRef = useRef<(component: CanvasComponent) => void>(() => {});
|
||||
|
||||
const editComponent = (component: CanvasComponent) => {
|
||||
const owner = sections.find((section) => section.components.some((one) => one.id === component.id));
|
||||
if (!owner) return;
|
||||
@@ -561,6 +603,9 @@ export const PageBuilderPanel = () => {
|
||||
// Cleared on unmount, or the panel goes on offering an editor for a page nobody is looking at.
|
||||
useEffect(() => () => setEditorRef.current(null), []);
|
||||
|
||||
// Both refs are assigned during render, so the effects above always call the current closures.
|
||||
editComponentRef.current = editComponent;
|
||||
|
||||
handlersRef.current = {
|
||||
add: () => setAdding(true),
|
||||
remove: (sectionId: number) => void deleteSection(sectionId),
|
||||
|
||||
Reference in New Issue
Block a user