Files
pastilhasandClaude Opus 5 2e8eabbc6b a Padding tab, where the device toggle picks the breakpoint
Choose a band — Header, Page, Footer — and set its horizontal and vertical padding. Which WIDTH you are
setting is decided by the Desktop / Tablet / Mobile buttons already in the bar:

  Mobile   390   base   every width
  Tablet   834   md     768 and up
  Desktop  1440  lg     1024 and up

That mapping is Tailwind's own and it lines up with the three preview widths exactly. Editing all three
at once would be six controls per section, five of them for a width you are not looking at; this way
you switch to Mobile, see the phone, and change the padding you can see changing.

INHERIT AND ZERO ARE DIFFERENT OPTIONS, because they are different instructions. Unset takes the
smaller breakpoint's value — which is what makes "one padding everywhere" a single choice rather than
three — and zero means no padding HERE even though the smaller width has some. The select says which
value is being inherited rather than leaving it to be worked out, and at the smallest width it says
"None", since there is nothing below to inherit from.

An emptied breakpoint is REMOVED rather than stored as `{}`, so "inherit everything" stays an absence
instead of becoming a thing that has to be told apart from one later.

One Save for both endpoints. The alternative was a second Save for spacing, which means somebody
changing a colour and a padding presses two buttons and is told about unsaved work by one of them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 05:24:30 +01:00

143 lines
5.1 KiB
TypeScript

import { Button } from '@/components/ui/button';
import { DEVICES, type DeviceName } from '../ViewMode';
/**
* Padding, per section, per breakpoint.
*
* ── The device toggle IS the breakpoint selector ──
*
* Editing all three widths at once would be six controls per section, and five of them would be for a
* width you are not looking at. Instead the bar's Desktop / Tablet / Mobile decides which breakpoint
* these two controls write to — so you switch to Mobile, see the phone, and change the padding you can
* see changing.
*
* The mapping is Tailwind's, and it lines up with the three preview widths exactly:
*
* Mobile 390 base every width
* Tablet 834 md 768 and up
* Desktop 1440 lg 1024 and up
*
* ── Inherit is a real value, and not the same as zero ──
*
* They cascade, so an unset breakpoint takes the smaller one's value. That is what makes the common
* case — one padding everywhere — a single choice rather than three. Zero is the opposite instruction:
* no padding HERE even though the smaller width has some. Both have to be expressible, so "Inherit"
* and "0" are separate options rather than one meaning both.
*/
export type SectionPadding = Partial<Record<'base' | 'md' | 'lg', { x?: number; y?: number }>>;
export type LayoutBand = { id: number; name: string; padding: SectionPadding };
/** Which breakpoint a device is looking at. See the table above. */
export const BREAKPOINT: Record<DeviceName, 'base' | 'md' | 'lg'> = {
mobile: 'base',
tablet: 'md',
desktop: 'lg',
};
/** What a breakpoint inherits when an axis is unset: the nearest smaller one that has a value. */
const inherited = (padding: SectionPadding, breakpoint: 'base' | 'md' | 'lg', axis: 'x' | 'y'): number | null => {
const below = { base: [], md: ['base'], lg: ['md', 'base'] } as const;
for (const smaller of below[breakpoint]) {
const step = padding[smaller]?.[axis];
if (typeof step === 'number') return step;
}
return null;
};
export const PaddingTab = ({
bands,
device,
steps,
selected,
onSelect,
onChange,
}: {
bands: LayoutBand[];
device: DeviceName;
/** The scale, sent by the API so this file holds no copy of it. */
steps: number[];
selected: number | null;
onSelect: (id: number) => void;
onChange: (id: number, padding: SectionPadding) => void;
}) => {
const breakpoint = BREAKPOINT[device];
const band = bands.find((one) => one.id === selected) ?? bands[0];
if (!band) {
// Before the layout has been seeded. Said plainly rather than shown as an empty form somebody
// would try to use.
return <p className="text-xs text-muted-foreground">The layout has no sections yet.</p>;
}
const set = (axis: 'x' | 'y', value: string) => {
const at = { ...(band.padding[breakpoint] ?? {}) };
if (value === '') delete at[axis];
else at[axis] = Number(value);
const next = { ...band.padding };
// An empty breakpoint is REMOVED rather than left as `{}` — otherwise "inherit everything" would
// be stored as a thing rather than as an absence, and the two would have to be told apart later.
if (Object.keys(at).length) next[breakpoint] = at;
else delete next[breakpoint];
onChange(band.id, next);
};
const axis = (name: 'x' | 'y', label: string) => {
const own = band.padding[breakpoint]?.[name];
const from = inherited(band.padding, breakpoint, name);
return (
<div className="flex flex-col gap-1">
<label className="text-xs">{label}</label>
<select
value={own === undefined ? '' : String(own)}
onChange={(event) => set(name, 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 from, so the empty option means
what it actually does there: no padding. */}
<option value="">
{breakpoint === 'base' ? 'None' : from === null ? 'Inherit (none)' : `Inherit (${from})`}
</option>
{steps.map((step) => (
<option key={step} value={String(step)}>
{step}
</option>
))}
</select>
</div>
);
};
return (
<div className="flex flex-col gap-3">
<div className="flex flex-wrap items-center gap-2">
{bands.map((one) => (
<Button
key={one.id}
size="sm"
variant={one.id === band.id ? 'secondary' : 'ghost'}
className="h-7 px-2 text-xs"
onClick={() => onSelect(one.id)}
>
{one.name}
</Button>
))}
{/* Which width is being edited, said out loud. 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">
{axis('x', 'Horizontal')}
{axis('y', 'Vertical')}
</div>
</div>
);
};