fix(media-gallery): harden download filename + zoom/pan from review

- mediaFilename: only skip appending an extension when the body already ends in
  a plausible short alphanumeric extension, so "Screenshot 2024.01.05" still
  gets a real extension appended for the saved file.
- Lightbox pan: divide the translate by zoom (it runs nested inside scale), so
  dragging tracks the cursor 1:1 instead of moving `zoom`x too far.
- Wheel: ignore deltaY === 0 (pure horizontal scroll no longer zooms out).
- Zoom-out button disables at the real reachable minimum (0.2, not dead 0.1).
- Zoom-controls group gets role="group" so its aria-label is announced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 15:28:27 -04:00
co-authored by Claude Opus 4.8
parent 8a92821a48
commit f9af363dd6
+24 -6
View File
@@ -135,10 +135,17 @@ function formatBytes(bytes: number): string {
}
// A sensible download filename: prefer the event body/filename; if it has no
// extension, append one derived from the mimetype so the saved file opens.
// plausible extension already, append one derived from the mimetype so the
// saved file opens. "Plausible" = a short alphanumeric tail after the last dot,
// so "Screenshot 2024.01.05" still gets a real extension appended.
function hasFileExtension(name: string): boolean {
const dot = name.lastIndexOf('.');
if (dot <= 0 || dot === name.length - 1) return false;
return /^[a-z0-9]{1,5}$/i.test(name.slice(dot + 1));
}
function mediaFilename(body: string, mimeType?: string): string {
const name = body.trim() || 'media';
if (name.includes('.')) return name;
if (hasFileExtension(name)) return name;
const ext = mimeType ? mimeTypeToExt(mimeType) : '';
return ext ? `${name}.${ext}` : name;
}
@@ -254,7 +261,12 @@ function LightboxMedia({
borderRadius: config.radii.R300,
display: 'block',
cursor,
transform: `scale(${zoom}) translate(${pan.translateX}px, ${pan.translateY}px)`,
// translate is nested inside scale(), so it runs in scaled space —
// divide by zoom so a dragged pixel moves the image one screen pixel
// (1:1 with the cursor) rather than `zoom` pixels.
transform: `scale(${zoom}) translate(${pan.translateX / zoom}px, ${
pan.translateY / zoom
}px)`,
transition: cursor === 'grabbing' ? 'none' : 'transform 120ms ease-out',
willChange: 'transform',
}}
@@ -313,7 +325,7 @@ function Lightbox({
(e: React.WheelEvent) => {
if (!isImage) return;
if (e.deltaY < 0) zoomIn();
else zoomOut();
else if (e.deltaY > 0) zoomOut();
},
[isImage, zoomIn, zoomOut],
);
@@ -372,14 +384,20 @@ function Lightbox({
{index + 1} / {items.length}
</Text>
{isImage && (
<Box shrink="No" alignItems="Center" gap="100" aria-label="Zoom controls">
<Box
shrink="No"
alignItems="Center"
gap="100"
role="group"
aria-label="Zoom controls"
>
<IconButton
variant="Surface"
size="300"
radii="300"
aria-label="Zoom out"
onClick={zoomOut}
disabled={zoom <= 0.1}
disabled={zoom <= 0.2}
>
<Icon size="50" src={Icons.Minus} />
</IconButton>