Files
fofos-admin/web/sections/FontSizesTab.tsx
T
pastilhasandClaude Opus 5 aab536059b the empty size option names the size
"Inherit (default)" answers a question nobody asked. What somebody wants from
that dropdown is what it is now — and a label that will not say makes them change
the setting to find out, which is the one way to lose the answer.

It reads Default (4xl) at the smallest width and Inherit (2xl) above it. The
stylesheet's own sizes come from the API, which reads them out of styles.css.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 08:48:51 +01:00

106 lines
3.8 KiB
TypeScript

import { useDevice } from '../DeviceToggle';
import { ROLES } from './roles';
/**
* How big each role is, at the width being edited.
*
* ── The same four roles as Fonts ──
*
* A template asks for a role and gets both the face and the size, so this section is the one above it
* with numbers instead of names. What that costs is worth knowing: a role used at two sizes on purpose
* collapses to one — `font-sub` was three sizes across the site and is now a single choice.
*
* ── One value per field ──
*
* The tabs at the top of the panel say which width. Unset inherits the smaller one, so "the same
* everywhere" is one choice.
*
* ── The empty option NAMES the size ──
*
* It said "Inherit (default)", which answers a question nobody asked. What somebody wants from that
* dropdown is "what is it now", and a label that does not say makes them change the setting to find
* out — which is the one way to lose the answer.
*
* So it names the size that will actually apply: the nearest smaller width that has one, or the
* stylesheet's own, which the API reads out of styles.css rather than this file guessing.
*/
export type FontSizes = Partial<Record<'title' | 'body' | 'sub' | 'display', Partial<Record<'base' | 'md' | 'lg', string>>>>;
const BREAKPOINT = { desktop: 'lg', tablet: 'md', mobile: 'base' } as const;
const inherited = (
steps: Partial<Record<'base' | 'md' | 'lg', string>> | undefined,
key: 'base' | 'md' | 'lg',
): string | null => {
const below = { base: [], md: ['base'], lg: ['md', 'base'] } as const;
for (const smaller of below[key]) {
const step = steps?.[smaller];
if (step) return step;
}
return null;
};
export const FontSizesTab = ({
sizes,
steps,
defaults,
onChange,
}: {
sizes: FontSizes;
/** The scale, sent by the API so this file holds no copy of it. */
steps: string[];
/** What each role is with nothing chosen, read from the site's stylesheet. */
defaults: Record<string, string>;
onChange: (sizes: FontSizes) => void;
}) => {
const [device] = useDevice();
const key = BREAKPOINT[device];
const set = (role: string, value: string) => {
const at = { ...((sizes as Record<string, Partial<Record<string, string>>>)[role] ?? {}) };
// Removed rather than stored empty: an absence is what "inherit" means, all the way up.
if (value === '') delete at[key];
else at[key] = value;
const next = { ...(sizes as Record<string, unknown>) };
if (Object.keys(at).length) next[role] = at;
else delete next[role];
onChange(next as FontSizes);
};
return (
<div className="grid grid-cols-1 gap-3 @[20rem]:grid-cols-2">
{ROLES.map((role) => {
const current = sizes[role.size];
const own = current?.[key];
// The nearest smaller width that has one, or the stylesheet's. Either way, a size to read.
const from = inherited(current, key) ?? defaults[role.size];
return (
<div key={role.size} className="flex flex-col gap-1">
<label className="text-xs">{role.label}</label>
<select
value={own ?? ''}
onChange={(event) => set(role.size, event.currentTarget.value)}
className="h-8 rounded-md border border-input bg-transparent px-2 text-xs"
>
{/* Mobile is the smallest, so nothing below it: what it falls back to is the
stylesheet's own size rather than another breakpoint's. */}
<option value="">
{from ? `${key === 'base' ? 'Default' : 'Inherit'} (${from})` : 'Default'}
</option>
{steps.map((step) => (
<option key={step} value={step}>
{step}
</option>
))}
</select>
</div>
);
})}
</div>
);
};