Files
fofos-admin/web/sections/GutterTab.tsx
T
pastilhasandClaude Opus 5 2b2d60f45c the gutter shows all three widths at once
One control, three values, horizontal only — Mobile, Tablet, Desktop on one row, smallest first because
that is the direction they cascade.

Not tied to the device toggle, and the difference from the per-band control is the reason. That one has
two axes and three bands, where showing every width would be eighteen inputs, so it writes to whichever
width the preview is showing. Here there is one axis and one value, so all three fit — and seeing them
together is the point. A gutter is a progression, 4 then 8 then 12, judged as a whole; reading it off
three separate visits to the same control is how a site ends up tighter on a tablet than on a phone.

Inherit still says what it is inheriting, and Mobile still says "None", because there is nothing below
it to inherit from.

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

108 lines
4.3 KiB
TypeScript

/**
* The site's horizontal gutter: how far the content sits from the edge of the window.
*
* ── One setting, three places ──
*
* The header row, the content column and the footer's inner block all share the site's measure, and all
* three want the same space at the edges. That is what makes this a GUTTER rather than three paddings
* that happen to agree — and setting it once is the only arrangement in which they cannot drift.
*
* A section's own padding still exists and composes on top of this. This is the floor everything starts
* from; `PaddingTab` is the per-band control, and it stays for the pages.
*
* ── All three widths at once, and NOT tied to the device toggle ──
*
* The per-band control does it the other way: one set of inputs writing to whichever width the preview
* is showing. That is right when there are two axes and three bands, where showing everything would be
* eighteen inputs.
*
* Here there is one axis and one value, so all three fit on a row — and seeing them together is the
* point. A gutter is a progression, 4 then 8 then 12, and it is judged as one: reading it off three
* separate visits to the same control is how a site ends up tighter on a tablet than on a phone.
*/
export type Gutter = Partial<Record<'base' | 'md' | 'lg', number>>;
/**
* The three widths, smallest first, with the name the admin uses for each.
*
* Smallest first because that is the direction they cascade: `md` inherits from `base`, `lg` from `md`.
* Read left to right, the row is the progression.
*/
const WIDTHS = [
{ key: 'base' as const, label: 'Mobile', hint: 'every width' },
{ key: 'md' as const, label: 'Tablet', hint: '768 and up' },
{ key: 'lg' as const, label: 'Desktop', hint: '1024 and up' },
];
/** What a width falls back to: the nearest smaller one that has a value. */
const inherited = (gutter: Gutter, key: 'base' | 'md' | 'lg'): number | null => {
const below = { base: [], md: ['base'], lg: ['md', 'base'] } as const;
for (const smaller of below[key]) {
const step = gutter[smaller];
if (typeof step === 'number') return step;
}
return null;
};
export const GutterTab = ({
gutter,
steps,
onChange,
}: {
gutter: Gutter;
/** The scale, sent by the API so this file holds no copy of it. */
steps: number[];
onChange: (gutter: Gutter) => void;
}) => {
const set = (key: 'base' | 'md' | 'lg', value: string) => {
const next = { ...gutter };
// Removed rather than stored as null: an absence is what "inherit" means, and a marker for it would
// leave two ways to say the same thing.
if (value === '') delete next[key];
else next[key] = Number(value);
onChange(next);
};
return (
<div className="flex flex-col gap-3">
<div className="flex flex-wrap items-baseline gap-2">
<h4 className="text-xs font-medium">Horizontal padding</h4>
<span className="text-[10px] text-muted-foreground">
The space between the content and the edge of the window — the header, the page and the footer
</span>
</div>
<div className="grid grid-cols-3 gap-3 sm:grid-cols-4">
{WIDTHS.map(({ key, label, hint }) => {
const own = gutter[key];
const from = inherited(gutter, key);
return (
<div key={key} className="flex flex-col gap-1">
<label className="text-xs">{label}</label>
<select
value={own === undefined ? '' : String(own)}
onChange={(event) => set(key, 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 the empty
option means what it actually does there: none. */}
<option value="">
{key === 'base' ? 'None' : from === null ? 'Inherit (none)' : `Inherit (${from})`}
</option>
{steps.map((step) => (
<option key={step} value={String(step)}>
{step}
</option>
))}
</select>
<span className="truncate text-[10px] text-muted-foreground">{hint}</span>
</div>
);
})}
</div>
</div>
);
};