From 44a907f3b15e7ab7c3f9011863740ee8c8eef379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Fri, 4 Sep 2026 16:49:01 +0100 Subject: [PATCH] a heading can say which type role it is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every heading — a band's and a block's — was `type-title`, so the only way to make one bigger was to resize Headings for the whole site. A ROLE rather than a size. The four are named and sized under Layout, so a heading set to Titles follows whatever Titles is set to and keeps following it. Offering point sizes here would be a second place to change one, and the two would part company the first time somebody used the panel that exists for it. Same `ROLES` list Fonts and Font Sizes read, so all three name the same four things in the same order. Choosing Headings stores nothing. A heading nobody has thought about and one deliberately set to the default should not be two different rows. Disabled with no heading, because the size of a heading that does not exist is not a question — and written on its own rather than through the draft, because a select has one value and waiting 600ms to watch a heading change size reads as a control that has not responded. Co-Authored-By: Claude Opus 5 (1M context) --- web/sections/BandSettings.tsx | 15 ++++++++++- web/sections/BlockSettings.tsx | 23 ++++++++++++++-- web/sections/TitleSize.tsx | 48 ++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 web/sections/TitleSize.tsx diff --git a/web/sections/BandSettings.tsx b/web/sections/BandSettings.tsx index 55f297f..4fa2545 100644 --- a/web/sections/BandSettings.tsx +++ b/web/sections/BandSettings.tsx @@ -3,6 +3,7 @@ import { Input } from '@/components/ui/input'; import { useClient } from 'hooks/useClient'; import { readableError } from '../errors'; import { useAutosave } from '../useAutosave'; +import { TitleSize } from './TitleSize'; import { useFlash } from '../useFlash'; /** @@ -26,7 +27,7 @@ export const BandSettings = ({ blocks, }: { pageId: number; - band: { id: number; name: string; title?: string | null; divider?: boolean }; + band: { id: number; name: string; title?: string | null; title_size?: string | null; divider?: boolean }; /** The builder re-reads its sections: the nav's row is this name, and the page draws this title. */ onSaved: () => void; /** This band's blocks — `BandBlocks`, built by the panel that has the handlers. */ @@ -94,6 +95,18 @@ export const BandSettings = ({ /> + {/* Beside the heading it sizes. Its own request, like the rule below — a select has one value and + nothing to debounce. */} + { + void client + .patch(`/fofos-admin/pages/${pageId}/sections/${band.id}`, { title_size: next ?? '' }) + .then(onSaved); + }} + /> + {/* Written on its own rather than through the draft, because it is a switch: there is nothing to debounce and waiting 600ms to see a line disappear reads as the control not working. diff --git a/web/sections/BlockSettings.tsx b/web/sections/BlockSettings.tsx index bc55b2f..97e0fd9 100644 --- a/web/sections/BlockSettings.tsx +++ b/web/sections/BlockSettings.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef, useState } from 'react'; import { Input } from '@/components/ui/input'; +import { TitleSize } from './TitleSize'; import { useClient } from 'hooks/useClient'; import { readableError } from '../errors'; import { useAutosave } from '../useAutosave'; @@ -30,6 +31,7 @@ export const BlockSettings = ({ block: { id: number; title?: string | null; + title_size?: string | null; width?: string | null; padding?: Record; }; @@ -83,14 +85,22 @@ export const BlockSettings = ({ setDraft((was) => ({ ...was, padding: next })); }; - const save = async () => { + /** + * @param extra Fields written alongside the draft, for controls that are not part of it. + * + * The heading SIZE is one: a select has a single value and nothing to debounce, so waiting 600ms to + * watch a heading change size would read as a control that has not responded. It still goes through + * this function rather than its own request, so the draft is not left behind by a write that + * happened next to it. + */ + const save = async (extra?: Record) => { setBusy(true); setError(null); try { // 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, width: draft.width || null, padding: draft.padding }, + { title: draft.title, width: draft.width || null, padding: draft.padding, ...extra }, ); flashSaved(); onSaved(); @@ -124,6 +134,15 @@ export const BlockSettings = ({ }} /> + {/* Beside the heading it sizes. Its own request rather than part of the draft: a select has one + value and nothing to debounce, and waiting to see a heading change size reads as a control + that has not responded. */} + void save({ title_size: next ?? '' })} + /> + {/* How wide it is allowed to be. diff --git a/web/sections/TitleSize.tsx b/web/sections/TitleSize.tsx new file mode 100644 index 0000000..e3c7207 --- /dev/null +++ b/web/sections/TitleSize.tsx @@ -0,0 +1,48 @@ +import { ROLES } from './roles'; + +/** + * Which type role a heading is drawn at. + * + * ── A role, not a size ── + * + * The four are named and sized under Layout, so a heading set to Titles follows whatever Titles is set + * to and keeps following it. Offering point sizes here would be a second place to change one, and the + * two would part company the first time somebody used the panel that exists for it. + * + * The same `ROLES` list Fonts and Font Sizes read, so all three name the same four things in the same + * order — which is the only reason somebody moving between them can trust what they see. + * + * Disabled with no heading, because the size of a heading that does not exist is not a question. The + * field above it is where one starts. + */ +export const TitleSize = ({ + value, + disabled, + onChange, +}: { + /** `display`, `title`, `body`, `sub` — or null for the default, which is Headings. */ + value: string | null | undefined; + disabled?: boolean; + onChange: (next: string | null) => void; +}) => ( +
+ + +
+);