diff --git a/web/EditorPanel.tsx b/web/EditorPanel.tsx
index dbb7437..2186794 100644
--- a/web/EditorPanel.tsx
+++ b/web/EditorPanel.tsx
@@ -40,9 +40,16 @@ export const EditorPanel = () => {
Above the device toggle, because the toggle scopes what is under it to one width and none of these
three have widths.
*/
- const { section, item } = useNav();
+ const { section, item, part } = useNav();
const { pages } = usePages();
- const page = section === 'pages' ? pageFromParam(pages, item) : null;
+ /*
+ Only when the PAGE row itself is selected.
+
+ A band or a block selected means the editor below is about that, and the page's own name and
+ address sitting above it read as belonging to whatever is open. They are reached by pressing the
+ page again, which is what the nav already implies.
+ */
+ const page = section === 'pages' && !part ? pageFromParam(pages, item) : null;
return (
diff --git a/web/sections/BlockSettings.tsx b/web/sections/BlockSettings.tsx
index eb485b0..aa327d5 100644
--- a/web/sections/BlockSettings.tsx
+++ b/web/sections/BlockSettings.tsx
@@ -4,6 +4,7 @@ import { useClient } from 'hooks/useClient';
import { readableError } from '../errors';
import { useAutosave } from '../useAutosave';
import { useFlash } from '../useFlash';
+import { useDevice } from '../DeviceToggle';
/**
* A block's own heading. Optional, and above whatever editor the block has.
@@ -21,24 +22,54 @@ export const BlockSettings = ({
pageId,
sectionId,
block,
+ steps,
onSaved,
}: {
pageId: number;
sectionId: number;
- block: { id: number; title?: string | null };
+ block: { id: number; title?: string | null; padding?: Record };
+ /** The padding scale, sent by the API so this file holds no copy of it. */
+ steps: number[];
onSaved: () => void;
}) => {
const client = useClient();
const clientRef = useRef(client);
clientRef.current = client;
- const [draft, setDraft] = useState(block.title ?? '');
+ const [device] = useDevice();
+ const breakpoint = ({ desktop: 'lg', tablet: 'md', mobile: 'base' } as const)[device];
+
+ const [draft, setDraft] = useState({ title: block.title ?? '', padding: block.padding ?? {} });
const [busy, setBusy] = useState(false);
const [error, setError] = useState(null);
const [saved, flashSaved] = useFlash();
// Keyed on the id, so re-reading the same block after a save does not throw away what is being typed.
- useEffect(() => setDraft(block.title ?? ''), [block.id]);
+ useEffect(() => setDraft({ title: block.title ?? '', padding: block.padding ?? {} }), [block.id]);
+
+ /** What an axis falls back to at this width: the nearest smaller one that has a value. */
+ const inherited = (axis: 'x' | 'y'): number | null => {
+ const below = { base: [], md: ['base'], lg: ['md', 'base'] } as const;
+ for (const smaller of below[breakpoint]) {
+ const step = draft.padding[smaller]?.[axis];
+ if (typeof step === 'number') return step;
+ }
+ return null;
+ };
+
+ const setPadding = (axis: 'x' | 'y', value: string) => {
+ const at = { ...(draft.padding[breakpoint] ?? {}) };
+ if (value === '') delete at[axis];
+ else at[axis] = Number(value);
+
+ const next = { ...draft.padding };
+ // An empty breakpoint is removed rather than kept as `{}`, so "inherit everything" stays an
+ // absence instead of a thing that has to be told apart from one later.
+ if (Object.keys(at).length) next[breakpoint] = at;
+ else delete next[breakpoint];
+
+ setDraft((was) => ({ ...was, padding: next }));
+ };
const save = async () => {
setBusy(true);
@@ -47,7 +78,7 @@ export const BlockSettings = ({
// Empty is sent as an empty string, which the API reads as "take the heading off".
await clientRef.current.patch(
`/fofos-admin/pages/${pageId}/sections/${sectionId}/components/${block.id}`,
- { title: draft },
+ { title: draft.title, padding: draft.padding },
);
flashSaved();
onSaved();
@@ -58,12 +89,62 @@ export const BlockSettings = ({
}
};
- useAutosave(draft !== (block.title ?? '') ? draft : null, busy, save);
+ const stored = JSON.stringify({ title: block.title ?? '', padding: block.padding ?? {} });
+ const pending = JSON.stringify(draft);
+
+ useAutosave(pending !== stored ? pending : null, busy, save);
return (
- setDraft(event.currentTarget.value)} />
+ setDraft((was) => ({ ...was, title: event.currentTarget.value }))}
+ />
+
+ {/*
+ The block's own spacing, at the width the toggle above is showing.
+
+ It had a Spacing link in the chrome painted onto the page, which went with the rest of it. It
+ belongs here: it is a property of the selected block, like the heading, and one of the two is
+ not worth a separate place to go.
+
+ Unset inherits the smaller width, and 0 is the other instruction — none HERE even though the
+ phone has some. So they are separate options.
+ */}
+