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>
This commit is contained in:
@@ -7,6 +7,7 @@ import { useSaveTarget } from '../SaveTarget';
|
||||
import { usePreviewable, useViewMode } from '../ViewMode';
|
||||
import { paintMeasure } from '../canvas';
|
||||
import { SitePreview, useDevPort } from '../SitePreview';
|
||||
import { PaddingTab, type LayoutBand, type SectionPadding } from './PaddingTab';
|
||||
|
||||
/**
|
||||
* The shell every page renders inside: the menu, the footer, the colours and the typefaces.
|
||||
@@ -49,6 +50,11 @@ type Layout = {
|
||||
font_display: string | null;
|
||||
/** What the picker may offer — sent by the API so this file holds no third copy of the list. */
|
||||
fonts: string[];
|
||||
/** The layout PAGE and its three bands. A different thing from the colours above — see the API. */
|
||||
layout_page_id: number | null;
|
||||
sections: LayoutBand[];
|
||||
/** The steps a padding may take, sent by the API so this file holds no copy of the scale. */
|
||||
padding_steps: number[];
|
||||
/**
|
||||
* Which face plays each role when nothing has been chosen, read from the site's own stylesheet.
|
||||
*
|
||||
@@ -116,6 +122,8 @@ export const LayoutPanel = () => {
|
||||
const devPort = useDevPort();
|
||||
/** Controlled, because the grid stack below decides visibility rather than Radix unmounting. */
|
||||
const [tab, setTab] = useState('colours');
|
||||
/** Which band the Padding tab is editing. Null until the payload has arrived. */
|
||||
const [band, setBand] = useState<number | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [saved, flashSaved] = useFlash();
|
||||
|
||||
@@ -140,11 +148,23 @@ export const LayoutPanel = () => {
|
||||
|
||||
/*
|
||||
Compared field by field rather than whole, because the payload carries something this does not own:
|
||||
`fonts` and `font_defaults` are the API describing the site — which faces the build subsets, and
|
||||
`fonts`, `font_defaults` and `padding_steps` are the API describing the site — which faces the build subsets, and
|
||||
which one plays each role today — not values anybody edits here. Including them would be harmless
|
||||
today and wrong the first time either changed: the panel would show unsaved changes for a decision
|
||||
made in a build script or a stylesheet.
|
||||
*/
|
||||
/*
|
||||
Padding is drafted here and written by the same Save, through the SECTIONS endpoint.
|
||||
|
||||
Two endpoints, one button. The alternative was a second Save for spacing, which would mean somebody
|
||||
changing a colour and a padding and having to press two things — and being told they had unsaved
|
||||
changes by only one of them.
|
||||
*/
|
||||
const setPadding = (id: number, padding: SectionPadding) =>
|
||||
setDraft((was) =>
|
||||
was ? { ...was, sections: was.sections.map((one) => (one.id === id ? { ...one, padding } : one)) } : was,
|
||||
);
|
||||
|
||||
const WRITABLE = [
|
||||
'background',
|
||||
'foreground',
|
||||
@@ -158,7 +178,9 @@ export const LayoutPanel = () => {
|
||||
const only = (source: Layout | null) =>
|
||||
source ? JSON.stringify(Object.fromEntries(WRITABLE.map((key) => [key, source[key]]))) : null;
|
||||
|
||||
const dirty = Boolean(stored && draft) && only(stored) !== only(draft);
|
||||
const paddings = (source: Layout | null) => JSON.stringify(source?.sections.map((one) => one.padding) ?? null);
|
||||
|
||||
const dirty = Boolean(stored && draft) && (only(stored) !== only(draft) || paddings(stored) !== paddings(draft));
|
||||
|
||||
const save = async () => {
|
||||
if (!draft) return;
|
||||
@@ -166,6 +188,21 @@ export const LayoutPanel = () => {
|
||||
setError(null);
|
||||
try {
|
||||
const body = Object.fromEntries(WRITABLE.map((key) => [key, draft[key]]));
|
||||
|
||||
// The bands that actually changed, before the layout write, so a failure on one leaves the other
|
||||
// untouched rather than half-applied.
|
||||
if (draft.layout_page_id) {
|
||||
for (const one of draft.sections) {
|
||||
const was = stored?.sections.find((other) => other.id === one.id);
|
||||
if (was && JSON.stringify(was.padding) !== JSON.stringify(one.padding)) {
|
||||
await clientRef.current.patch(
|
||||
`/fofos-admin/pages/${draft.layout_page_id}/sections/${one.id}`,
|
||||
{ padding: one.padding },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apply((await clientRef.current.patch<{ data: Layout }>('/fofos-admin/layout', body)).data);
|
||||
flashSaved();
|
||||
} catch (failure) {
|
||||
@@ -178,7 +215,7 @@ export const LayoutPanel = () => {
|
||||
useSaveTarget(dirty, busy, save);
|
||||
// This section has a preview, so the bar offers Edit/View and the three widths.
|
||||
usePreviewable();
|
||||
const { mode } = useViewMode();
|
||||
const { mode, device } = useViewMode();
|
||||
|
||||
/*
|
||||
The measure, and nothing else.
|
||||
@@ -243,6 +280,9 @@ export const LayoutPanel = () => {
|
||||
<TabsTrigger value="typefaces" className="text-xs">
|
||||
Typefaces
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="padding" className="text-xs">
|
||||
Padding
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/*
|
||||
@@ -340,6 +380,20 @@ export const LayoutPanel = () => {
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`col-start-1 row-start-1 ${tab === 'padding' ? '' : 'invisible'}`}
|
||||
aria-hidden={tab !== 'padding'}
|
||||
>
|
||||
<PaddingTab
|
||||
bands={draft.sections}
|
||||
device={device}
|
||||
steps={draft.padding_steps}
|
||||
selected={band}
|
||||
onSelect={setBand}
|
||||
onChange={setPadding}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user