draft the crop instead of saving every pointer move
Panning went berserk. Wired straight to MediaField, `onCropChange` fired on every move, each move saved, each save reloaded the row, and the reload handed the crop back as the last SAVED value — yanking the picture out from under the drag. BlockMedia exists for exactly this and I went around it. It keeps the crop in a local draft, saves once when the drag stops, and resets on the url and the shape rather than on the crop — its own note says why, and the failure it describes is the one I reproduced. Also: an empty picture slot looked the same whether the carousel was still being read or genuinely had nothing in it. `undefined` now means loading and `null` means empty, and the line underneath says which. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { RichText } from '../RichText';
|
||||
import { useAutosave } from '../useAutosave';
|
||||
import { MediaField } from '../MediaField';
|
||||
import { BlockMedia } from './BlockMedia';
|
||||
import { SHAPES } from './shapes';
|
||||
import type { SubjectRecord, SubjectSection } from './subjects';
|
||||
|
||||
@@ -106,18 +106,29 @@ export const SubjectFields = <T extends SubjectRecord>({
|
||||
*/}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs text-muted-foreground">Main image</label>
|
||||
<MediaField
|
||||
value={cover}
|
||||
onChange={(path) => save({ cover_url: path, ...(path ? {} : { cover_crop: null }) })}
|
||||
{/*
|
||||
`BlockMedia`, not `MediaField` directly.
|
||||
|
||||
It holds the crop in a local draft and saves once, when the drag stops. Wired straight to
|
||||
`MediaField` this saved on every pointer move — and each save reloaded the row, which handed
|
||||
the crop back as the last SAVED value and yanked the picture out from under the drag. That is
|
||||
the exact failure the note at the top of BlockMedia describes, and the reason it exists.
|
||||
*/}
|
||||
<BlockMedia
|
||||
url={cover}
|
||||
crop={record.cover_crop}
|
||||
onCropChange={(crop) => save({ cover_crop: crop })}
|
||||
aspect={aspect}
|
||||
label="Choose the main image"
|
||||
onUrl={(path) => save({ cover_url: path, ...(path ? {} : { cover_crop: null }) })}
|
||||
onCrop={(crop) => save({ cover_crop: crop })}
|
||||
/>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
{record.cover_url
|
||||
? 'Chosen for this one. Remove it to go back to the first picture in its carousel.'
|
||||
: 'From the first picture in its carousel. Choose one to override that.'}
|
||||
: firstPhoto === undefined
|
||||
? 'Reading its carousel…'
|
||||
: firstPhoto
|
||||
? 'From the first picture in its carousel. Choose one to override that.'
|
||||
: 'Its carousel is empty. Choose a picture, or add some in Carousels.'}
|
||||
{record.cover_shape === null && ' No shape is set on the page, so it draws at its own.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -122,7 +122,9 @@ export const SubjectPanel = <T extends SubjectRecord>({ section }: { section: Su
|
||||
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}
|
||||
// `undefined` while the carousel is still being read, `null` when it genuinely has none.
|
||||
// The two look identical in an empty picture slot, and only one of them is a problem.
|
||||
firstPhoto={photos === null ? undefined : (photos[0]?.path ?? null)}
|
||||
onSaved={() => void load()}
|
||||
/>
|
||||
) : null,
|
||||
|
||||
Reference in New Issue
Block a user