Layout's Padding tab sets the site's gutter
Not a per-band control. The header, the content column and the footer share one measure and should share one number at its edges — three sections' worth of controls would be three ways to disagree about the same decision. PaddingTab is untouched and stays. It is the per-band control and it is right for the pages, where a band genuinely does want its own spacing. This panel just is not where that decision belongs. Same interaction as everything else here: the device toggle picks the breakpoint, Inherit and 0 are separate options because they are separate instructions, and the select says which value it is inheriting rather than leaving it to be worked out. Saved with the colours and the typefaces, because it is a field of the same row — so the per-band write path this panel had briefly is gone with it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import { DEVICES, type DeviceName } from '../ViewMode';
|
||||
|
||||
/**
|
||||
* The site's horizontal gutter: how far the content sits from the edge of the window.
|
||||
*
|
||||
* ── One setting, not one per band ──
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* ── The device toggle picks the width ──
|
||||
*
|
||||
* Same as everywhere else in this panel: Mobile writes `base`, Tablet `md`, Desktop `lg`, and they
|
||||
* cascade — so one gutter at every width is one choice rather than three.
|
||||
*/
|
||||
|
||||
export type Gutter = Partial<Record<'base' | 'md' | 'lg', number>>;
|
||||
|
||||
const BREAKPOINT: Record<DeviceName, 'base' | 'md' | 'lg'> = { mobile: 'base', tablet: 'md', desktop: 'lg' };
|
||||
|
||||
/** What this breakpoint falls back to: the nearest smaller one that has a value. */
|
||||
const inherited = (gutter: Gutter, breakpoint: 'base' | 'md' | 'lg'): number | null => {
|
||||
const below = { base: [], md: ['base'], lg: ['md', 'base'] } as const;
|
||||
for (const smaller of below[breakpoint]) {
|
||||
const step = gutter[smaller];
|
||||
if (typeof step === 'number') return step;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const GutterTab = ({
|
||||
gutter,
|
||||
device,
|
||||
steps,
|
||||
onChange,
|
||||
}: {
|
||||
gutter: Gutter;
|
||||
device: DeviceName;
|
||||
steps: number[];
|
||||
onChange: (gutter: Gutter) => void;
|
||||
}) => {
|
||||
const breakpoint = BREAKPOINT[device];
|
||||
const own = gutter[breakpoint];
|
||||
const from = inherited(gutter, breakpoint);
|
||||
|
||||
const set = (value: string) => {
|
||||
const next = { ...gutter };
|
||||
// Removed rather than stored as null: an absence is what "inherit" means, and storing a marker for
|
||||
// it would leave two ways to say the same thing.
|
||||
if (value === '') delete next[breakpoint];
|
||||
else next[breakpoint] = 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
|
||||
</span>
|
||||
{/* Which width is being set, said here because the device buttons are at the top of the panel
|
||||
and the effect of pressing one is not obvious from down here. */}
|
||||
<span className="ml-auto text-[10px] text-muted-foreground">
|
||||
editing {DEVICES[device].label.toLowerCase()} — {breakpoint}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs">Gutter</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"
|
||||
>
|
||||
{/* At the smallest width there is nothing below to inherit, so the empty option means what
|
||||
it does there: none. */}
|
||||
<option value="">
|
||||
{breakpoint === '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">
|
||||
Applies to the header, the page and the footer
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user