The three columns became one select. Which breakpoint it writes comes from the device control above it, so the value you see is the value for the view you are looking at.
83 lines
3.1 KiB
TypeScript
83 lines
3.1 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">
|
|
<label className="text-xs">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>
|
|
);
|
|
};
|