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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user