84 lines
3.2 KiB
TypeScript
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>
|
|
);
|
|
};
|