diff --git a/web/sections/GutterTab.tsx b/web/sections/GutterTab.tsx index 85f3bc1..65f1d47 100644 --- a/web/sections/GutterTab.tsx +++ b/web/sections/GutterTab.tsx @@ -1,9 +1,7 @@ -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 ── + * ── 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 @@ -12,20 +10,35 @@ import { DEVICES, type DeviceName } from '../ViewMode'; * 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 ── + * ── All three widths at once, and NOT tied to the device toggle ── * - * 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. + * 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>; -const BREAKPOINT: Record = { mobile: 'base', tablet: 'md', desktop: 'lg' }; +/** + * 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 this breakpoint falls back to: the nearest smaller one that has a value. */ -const inherited = (gutter: Gutter, breakpoint: 'base' | 'md' | 'lg'): number | null => { +/** 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[breakpoint]) { + for (const smaller of below[key]) { const step = gutter[smaller]; if (typeof step === 'number') return step; } @@ -34,25 +47,20 @@ const inherited = (gutter: Gutter, breakpoint: 'base' | 'md' | 'lg'): number | n export const GutterTab = ({ gutter, - device, steps, onChange, }: { gutter: Gutter; - device: DeviceName; + /** The scale, sent by the API so this file holds no copy of it. */ steps: number[]; onChange: (gutter: Gutter) => void; }) => { - const breakpoint = BREAKPOINT[device]; - const own = gutter[breakpoint]; - const from = inherited(gutter, breakpoint); - - const set = (value: string) => { + const set = (key: 'base' | 'md' | 'lg', 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); + // 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); }; @@ -61,38 +69,38 @@ export const GutterTab = ({

Horizontal padding

- The space between the content and the edge of the window - - {/* 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. */} - - editing {DEVICES[device].label.toLowerCase()} — {breakpoint} + The space between the content and the edge of the window — the header, the page and the footer
-
-
- - - - Applies to the header, the page and the footer - -
+
+ {WIDTHS.map(({ key, label, hint }) => { + const own = gutter[key]; + const from = inherited(gutter, key); + + return ( +
+ + + {hint} +
+ ); + })}
); diff --git a/web/sections/LayoutPanel.tsx b/web/sections/LayoutPanel.tsx index 5936e59..bc16ce2 100644 --- a/web/sections/LayoutPanel.tsx +++ b/web/sections/LayoutPanel.tsx @@ -364,11 +364,14 @@ export const LayoutPanel = () => { PaddingTab is the per-band control and it stays — for the pages, where a band really does want its own spacing. Here the three wrappers share one measure and should share - one number, so this is one select rather than three sections' worth. + one number. + + All three widths at once, and deliberately NOT tied to the device toggle: 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. */} patch({ padding_x })} />