a media slot can be framed by dragging and zooming
A photograph is rarely the shape of the slot it goes in, so something decides which part survives. It was a centred crop and no way to change it. Dragging the picture slides the window; the wheel zooms, anchored to the pointer so the thing under the cursor stays under it. Both write the site's own format — `x,y,w,h` in percentages of the ORIGINAL — which is resolution-independent and survives the file being re-encoded. Zooming in is the window getting smaller: w and h shrink together, keeping the slot's shape. Fit is the floor, because you cannot show more of a photograph than there is, and at exactly fit the crop is cleared rather than written as the centred value — a null crop and a crop that happens to be centred should not be two states. ── The preview is not object-cover ── It draws the rectangle: the picture scaled to `100/w` of the box and offset so the chosen corner lands at the origin. object-cover shows the largest window of the slot's shape that fits and nothing else — it can be panned and it cannot be zoomed, so a preview built on it would silently ignore half of what this control produces. ── Also here ── buildSite now hands the site an av-club URL and user. That is the first piece of publishing images cropped rather than cropping them in the browser with CSS: the plugin knows the sidecar registry, the site knows a URL. Inert until the site uses it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -55,6 +55,7 @@ export const AboutPanel = () => {
|
||||
const [about, setAbout] = useState<About | null>(null);
|
||||
const [headerUrl, setHeaderUrl] = useState('');
|
||||
const [phrase, setPhrase] = useState('');
|
||||
const [headerCrop, setHeaderCrop] = useState<string | null>(null);
|
||||
const [aboutText, setAboutText] = useState('');
|
||||
const [productVideo, setProductVideo] = useState('');
|
||||
const [productText, setProductText] = useState('');
|
||||
@@ -67,6 +68,7 @@ export const AboutPanel = () => {
|
||||
const apply = useCallback((data: About) => {
|
||||
setAbout(data);
|
||||
setHeaderUrl(data.header.url ?? '');
|
||||
setHeaderCrop(data.header.crop);
|
||||
setPhrase(data.header.phrase ?? '');
|
||||
setAboutText(data.about_text ?? '');
|
||||
setProductVideo(data.product_video_url ?? '');
|
||||
@@ -92,6 +94,7 @@ export const AboutPanel = () => {
|
||||
/** Every field this page owns, as the API spells them. One place, so save and dirty cannot disagree. */
|
||||
const edited = {
|
||||
header_url: orNull(headerUrl),
|
||||
header_crop: headerCrop,
|
||||
header_phrase: orNull(phrase),
|
||||
about_text: orNull(aboutText),
|
||||
product_video_url: orNull(productVideo),
|
||||
@@ -101,6 +104,7 @@ export const AboutPanel = () => {
|
||||
|
||||
const stored = about && {
|
||||
header_url: about.header.url,
|
||||
header_crop: about.header.crop,
|
||||
header_phrase: about.header.phrase,
|
||||
about_text: about.about_text,
|
||||
product_video_url: about.product_video_url,
|
||||
@@ -153,7 +157,19 @@ export const AboutPanel = () => {
|
||||
{/* 21/9 because that is what the page uses — `aspect-[21/9]` in about/partials.tsx. The two
|
||||
have to agree or the admin shows a shape the site does not have, which is worse than showing
|
||||
no shape at all: it looks decided. */}
|
||||
<MediaField value={headerUrl || null} onChange={(path) => setHeaderUrl(path ?? '')} aspect="21/9" label="header photograph" />
|
||||
<MediaField
|
||||
value={headerUrl || null}
|
||||
onChange={(path) => {
|
||||
setHeaderUrl(path ?? '');
|
||||
// A framing belongs to the picture it was made for. Keeping it across a change would apply
|
||||
// one photograph's decision to another one's composition.
|
||||
setHeaderCrop(null);
|
||||
}}
|
||||
crop={headerCrop}
|
||||
onCropChange={setHeaderCrop}
|
||||
aspect="21/9"
|
||||
label="header photograph"
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs text-muted-foreground">Phrase</label>
|
||||
|
||||
Reference in New Issue
Block a user