each row picks its own main image, and frames it

The shape is one decision for the whole section and stays on the template — every
collab is a 21:9 band or none of them is. Which part of THIS photograph survives
that shape is a decision about this photograph, and there is one per row. A single
rectangle chosen against one lead and applied to all of them is how a portrait
ends up cropped through somebody's head, which is what the block-level crop did
and why it is gone.

So a row's settings now carry Main image: a picture and a cropping frame in the
proportions the page asked for. Empty means the first item of its carousel, which
is the ordinary state and is not stored — reorder the carousel and the lead
follows. Choosing one is the deliberate act of saying "not that one", and it may
be a picture the carousel does not contain at all.

`cover_shape` rides along with each row so this panel can draw the right frame
without knowing how a template is assembled. SHAPES moved out of PageBuilderPanel
because two screens offer them now, and two lists is two places to add the next
ratio.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-05 15:41:37 +01:00
co-authored by Claude Opus 5
parent c6cebc5000
commit 5968cb5d83
5 changed files with 105 additions and 19 deletions
+11 -2
View File
@@ -116,9 +116,18 @@ export const SubjectPanel = <T extends SubjectRecord>({ section }: { section: Su
useEffect(() => {
setEditorRef.current(
selected ? <SubjectFields section={section} record={selected} onSaved={() => void load()} /> : null,
selected ? (
<SubjectFields
section={section}
record={selected}
// What the cover falls back to when the row has not chosen one — the same first item the
// grid below is already showing, so the two cannot disagree about which picture that is.
firstPhoto={photos?.[0]?.path ?? null}
onSaved={() => void load()}
/>
) : null,
);
}, [selected, load, section]);
}, [selected, load, section, photos]);
useEffect(() => () => setEditorRef.current(null), []);