Files
fofos-admin/web/sections/GutterTab.tsx
T

84 lines
3.2 KiB
TypeScript

import { useDevice } from '../DeviceToggle';
/**
* 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 device toggle's names, and the breakpoint each one writes. */
const BREAKPOINT = { desktop: 'lg', tablet: 'md', mobile: 'base' } as const;
/** 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 [device] = useDevice();
const key = BREAKPOINT[device];
const set = (value: string) => {
const next = { ...gutter };
// Removed rather than stored as null: an absence is what "inherit" means.
if (value === '') delete next[key];
else next[key] = Number(value);
onChange(next);
};
const own = gutter[key];
const from = inherited(gutter, key);
return (
<div className="flex flex-col gap-1">
<h4 className="text-xs font-medium">Top Nav</h4>
<label className="text-xs text-muted-foreground">Horizontal padding</label>
<select
value={own === undefined ? '' : String(own)}
onChange={(event) => set(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. */}
<option value="">{key === 'base' ? 'None' : from === null ? 'Inherit (none)' : `Inherit (${from})`}</option>
{steps.map((step) => (
<option key={step} value={String(step)}>
{step}
</option>
))}
</select>
</div>
);
};