The Shape control it got last commit did nothing, and the reason is the trap that
file already warns about: `contentFor` is a per-kind allowlist and everything it
does not name is dropped. `subject-cover` was not in it, so the panel wrote a
ratio, got a 200, and the API stored `{}`. Its own comment says that is "exactly
what silently threw `ratio` away when the admin first sent one".
Now it holds a ratio and a crop, and the block gets the same framing surface the
About Us hero has.
Two things it does NOT get, both deliberate:
No Change and no Remove — `fixed` on MediaField. The picture is the subject's
first carousel item and this block cannot store a different one. Framing is its
decision; which photograph is not.
No local knowledge of which picture that is. The site answers
`/pages/:id/subject-lead`, because which template draws which section, which row
is the example and which of its pictures leads are four facts that live in
templates.ts — a second copy here is how the two drift when a fifth template
arrives.
And it says out loud that the crop applies to every row the template draws. It is
framed against one photograph and used on all of them, which is the one
surprising thing about cropping a template rather than a page.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
59 lines
1.8 KiB
TypeScript
59 lines
1.8 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { MediaField } from '../MediaField';
|
|
import { useAutosave } from '../useAutosave';
|
|
|
|
/**
|
|
* A photograph block's picture and its framing.
|
|
*
|
|
* ── Why the crop is held here ──
|
|
*
|
|
* `MediaField` is controlled: it reports every pointer move and draws whatever it is handed back. Wired
|
|
* straight to the block, that meant a PATCH and a full reload of the preview PER MOVE — the page went
|
|
* white while somebody dragged, which is what this exists to stop.
|
|
*
|
|
* So the drag runs against local state, which is instant, and the write is debounced by `useAutosave`
|
|
* — one request when the hand stops, not sixty on the way there. The same trade every other control in
|
|
* this admin makes, and the same hook.
|
|
*
|
|
* ── Reset by IDENTITY, not by value ──
|
|
*
|
|
* The effect keys on the url and the shape rather than on the crop. Keyed on the crop it would fight
|
|
* the drag: the parent's value is the last SAVED one, so every move would be pulled back to it until
|
|
* the save landed.
|
|
*/
|
|
export const BlockMedia = ({
|
|
url,
|
|
crop,
|
|
aspect,
|
|
onUrl,
|
|
onCrop,
|
|
fixed = false,
|
|
}: {
|
|
url: string | null;
|
|
crop: string | null;
|
|
/** Tailwind's ratio, as the page spells it — `21/9`, `1/1`. */
|
|
aspect: string;
|
|
onUrl: (url: string | null) => void;
|
|
onCrop: (crop: string | null) => void;
|
|
/** The picture is fixed and only its framing is editable — see MediaField. */
|
|
fixed?: boolean;
|
|
}) => {
|
|
const [draft, setDraft] = useState<string | null>(crop);
|
|
|
|
useEffect(() => setDraft(crop), [url, aspect]);
|
|
|
|
useAutosave(draft !== crop ? (draft ?? '') : null, false, () => onCrop(draft));
|
|
|
|
return (
|
|
<MediaField
|
|
value={url}
|
|
onChange={onUrl}
|
|
crop={draft}
|
|
onCropChange={setDraft}
|
|
aspect={aspect}
|
|
label="Choose a photograph"
|
|
fixed={fixed}
|
|
/>
|
|
);
|
|
};
|