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>
94 lines
3.1 KiB
TypeScript
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>
|
|
);
|
|
};
|