a heading can say which type role it is
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) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { useClient } from 'hooks/useClient';
|
import { useClient } from 'hooks/useClient';
|
||||||
import { readableError } from '../errors';
|
import { readableError } from '../errors';
|
||||||
import { useAutosave } from '../useAutosave';
|
import { useAutosave } from '../useAutosave';
|
||||||
|
import { TitleSize } from './TitleSize';
|
||||||
import { useFlash } from '../useFlash';
|
import { useFlash } from '../useFlash';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,7 +27,7 @@ export const BandSettings = ({
|
|||||||
blocks,
|
blocks,
|
||||||
}: {
|
}: {
|
||||||
pageId: number;
|
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. */
|
/** The builder re-reads its sections: the nav's row is this name, and the page draws this title. */
|
||||||
onSaved: () => void;
|
onSaved: () => void;
|
||||||
/** This band's blocks — `BandBlocks`, built by the panel that has the handlers. */
|
/** This band's blocks — `BandBlocks`, built by the panel that has the handlers. */
|
||||||
@@ -94,6 +95,18 @@ export const BandSettings = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Beside the heading it sizes. Its own request, like the rule below — a select has one value and
|
||||||
|
nothing to debounce. */}
|
||||||
|
<TitleSize
|
||||||
|
value={band.title_size}
|
||||||
|
disabled={!draft.title.trim()}
|
||||||
|
onChange={(next) => {
|
||||||
|
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
|
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.
|
debounce and waiting 600ms to see a line disappear reads as the control not working.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { TitleSize } from './TitleSize';
|
||||||
import { useClient } from 'hooks/useClient';
|
import { useClient } from 'hooks/useClient';
|
||||||
import { readableError } from '../errors';
|
import { readableError } from '../errors';
|
||||||
import { useAutosave } from '../useAutosave';
|
import { useAutosave } from '../useAutosave';
|
||||||
@@ -30,6 +31,7 @@ export const BlockSettings = ({
|
|||||||
block: {
|
block: {
|
||||||
id: number;
|
id: number;
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
|
title_size?: string | null;
|
||||||
width?: string | null;
|
width?: string | null;
|
||||||
padding?: Record<string, { x?: number; y?: number }>;
|
padding?: Record<string, { x?: number; y?: number }>;
|
||||||
};
|
};
|
||||||
@@ -83,14 +85,22 @@ export const BlockSettings = ({
|
|||||||
setDraft((was) => ({ ...was, padding: next }));
|
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<string, unknown>) => {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
// Empty is sent as an empty string, which the API reads as "take the heading off".
|
// Empty is sent as an empty string, which the API reads as "take the heading off".
|
||||||
await clientRef.current.patch(
|
await clientRef.current.patch(
|
||||||
`/fofos-admin/pages/${pageId}/sections/${sectionId}/components/${block.id}`,
|
`/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();
|
flashSaved();
|
||||||
onSaved();
|
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. */}
|
||||||
|
<TitleSize
|
||||||
|
value={block.title_size}
|
||||||
|
disabled={!draft.title.trim()}
|
||||||
|
onChange={(next) => void save({ title_size: next ?? '' })}
|
||||||
|
/>
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
How wide it is allowed to be.
|
How wide it is allowed to be.
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}) => (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-xs">Heading size</label>
|
||||||
|
<select
|
||||||
|
value={value ?? 'title'}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={(event) => {
|
||||||
|
const next = event.currentTarget.value;
|
||||||
|
// `title` IS the default, so choosing it stores nothing — a heading nobody has thought about
|
||||||
|
// and one deliberately set to Headings should not be two different rows.
|
||||||
|
onChange(next === 'title' ? null : next);
|
||||||
|
}}
|
||||||
|
className="h-9 rounded-md border border-input bg-background px-2 text-sm disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{ROLES.map((role) => (
|
||||||
|
<option key={role.size} value={role.size}>
|
||||||
|
{role.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user