Files
fofos-admin/web/sections/FontSizesTab.tsx
T
pastilhasandClaude Opus 5 905e2e3325 one list of roles, named for what they are
Fonts and Font Sizes are the same four things, so they now read in the same order
under the same names — Menu, Titles, Headings, Body — from one list.

font_display is the largest statements and font_title is a section heading, which
is the reverse of what the words suggest. The labels say what the reader sees.

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

94 lines
3.1 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 option names what it is inheriting rather than leaving it to be
* worked out.
*/
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,
onChange,
}: {
sizes: FontSizes;
/** The scale, sent by the API so this file holds no copy of it. */
steps: 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-2 gap-3">
{ROLES.map((role) => {
const current = sizes[role.size];
const own = current?.[key];
const from = inherited(current, key);
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 there is nothing below it to inherit from — and nothing
chosen there means the stylesheet's own size. */}
<option value="">
{key === 'base' ? 'Default' : from === null ? 'Inherit (default)' : `Inherit (${from})`}
</option>
{steps.map((step) => (
<option key={step} value={step}>
{step}
</option>
))}
</select>
</div>
);
})}
</div>
);
};