diff --git a/web/canvas.ts b/web/canvas.ts index 481b0af..6ab8f1d 100644 --- a/web/canvas.ts +++ b/web/canvas.ts @@ -55,6 +55,8 @@ export type CanvasComponent = { kind: string; /** The heading the page draws above the block, or null. Every kind may have one. */ title?: string | null; + /** `main`, `page` or null — how wide it is allowed to be. See the site's schema. */ + width?: string | null; /* One optional field per kind that the panel reads back. Not `Record`: every reader would then need a cast, and the point of naming them is that the compiler knows `free-text` holds diff --git a/web/sections/BlockSettings.tsx b/web/sections/BlockSettings.tsx index 9788aaa..bc55b2f 100644 --- a/web/sections/BlockSettings.tsx +++ b/web/sections/BlockSettings.tsx @@ -27,7 +27,12 @@ export const BlockSettings = ({ }: { pageId: number; sectionId: number; - block: { id: number; title?: string | null; padding?: Record }; + block: { + id: number; + title?: string | null; + width?: string | null; + padding?: Record; + }; /** The padding scale, sent by the API so this file holds no copy of it. */ steps: number[]; onSaved: () => void; @@ -39,13 +44,20 @@ export const BlockSettings = ({ const [device] = useDevice(); const breakpoint = ({ desktop: 'lg', tablet: 'md', mobile: 'base' } as const)[device]; - const [draft, setDraft] = useState({ title: block.title ?? '', padding: block.padding ?? {} }); + const [draft, setDraft] = useState({ + title: block.title ?? '', + width: block.width ?? '', + padding: block.padding ?? {}, + }); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [saved, flashSaved] = useFlash(); // Keyed on the id, so re-reading the same block after a save does not throw away what is being typed. - useEffect(() => setDraft({ title: block.title ?? '', padding: block.padding ?? {} }), [block.id]); + useEffect( + () => setDraft({ title: block.title ?? '', width: block.width ?? '', padding: block.padding ?? {} }), + [block.id], + ); /** What an axis falls back to at this width: the nearest smaller one that has a value. */ const inherited = (axis: 'x' | 'y'): number | null => { @@ -78,7 +90,7 @@ export const BlockSettings = ({ // Empty is sent as an empty string, which the API reads as "take the heading off". await clientRef.current.patch( `/fofos-admin/pages/${pageId}/sections/${sectionId}/components/${block.id}`, - { title: draft.title, padding: draft.padding }, + { title: draft.title, width: draft.width || null, padding: draft.padding }, ); flashSaved(); onSaved(); @@ -89,7 +101,11 @@ export const BlockSettings = ({ } }; - const stored = JSON.stringify({ title: block.title ?? '', padding: block.padding ?? {} }); + const stored = JSON.stringify({ + title: block.title ?? '', + width: block.width ?? '', + padding: block.padding ?? {}, + }); const pending = JSON.stringify(draft); useAutosave(pending !== stored ? pending : null, busy, save); @@ -108,6 +124,29 @@ export const BlockSettings = ({ }} /> + {/* + How wide it is allowed to be. + + It was a NEGATIVE horizontal padding, which worked and asked the wrong thing: you had to know + the band's gutter and type its opposite, and it broke quietly the day somebody changed the + gutter. These two say what is wanted, and the page works out the number. + */} +
+ + +
+ {/* The block's own spacing, at the width the toggle above is showing. @@ -139,20 +178,7 @@ export const BlockSettings = ({ - {/* - Horizontal offers NEGATIVES, vertical does not. - - A negative horizontal is how a block escapes the gutter its band applied — a video - that runs to the edge of the window inside an inset band. It becomes `-mx-*`, which - cancels the band's `px-*` exactly, so the value to pick is the band's own number. - - Vertically there is nothing to cancel: a band's vertical padding is the room around - the whole band, and a block pulling itself into the one above is a bug. - */} - {(axis === 'x' - ? [...steps.filter((step) => step > 0).map((step) => -step).reverse(), ...steps] - : steps - ).map((step) => ( + {steps.map((step) => (