Skip to content

Commit 76137dd

Browse files
Add image lightbox with Radix UI Dialog and improved image handling
Co-authored-by: paul.jaffre <[email protected]>
1 parent 5b3f314 commit 76137dd

File tree

5 files changed

+198
-15
lines changed

5 files changed

+198
-15
lines changed

app/globals.css

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,60 @@ body {
180180
content: "Step " counter(onboarding-step) ": ";
181181
font-weight: inherit;
182182
}
183+
184+
/* Lightbox animations */
185+
@keyframes dialog-content-show {
186+
from {
187+
opacity: 0;
188+
transform: translate(-50%, -48%) scale(0.96);
189+
}
190+
to {
191+
opacity: 1;
192+
transform: translate(-50%, -50%) scale(1);
193+
}
194+
}
195+
196+
@keyframes dialog-content-hide {
197+
from {
198+
opacity: 1;
199+
transform: translate(-50%, -50%) scale(1);
200+
}
201+
to {
202+
opacity: 0;
203+
transform: translate(-50%, -48%) scale(0.96);
204+
}
205+
}
206+
207+
@keyframes dialog-overlay-show {
208+
from {
209+
opacity: 0;
210+
}
211+
to {
212+
opacity: 1;
213+
}
214+
}
215+
216+
@keyframes dialog-overlay-hide {
217+
from {
218+
opacity: 1;
219+
}
220+
to {
221+
opacity: 0;
222+
}
223+
}
224+
225+
[data-state="open"] {
226+
animation: dialog-content-show 200ms cubic-bezier(0.16, 1, 0.3, 1);
227+
}
228+
229+
[data-state="closed"] {
230+
animation: dialog-content-hide 200ms cubic-bezier(0.16, 1, 0.3, 1);
231+
}
232+
233+
.dialog-overlay[data-state="open"] {
234+
animation: dialog-overlay-show 200ms cubic-bezier(0.16, 1, 0.3, 1);
235+
}
236+
237+
.dialog-overlay[data-state="closed"] {
238+
animation: dialog-overlay-hide 200ms cubic-bezier(0.16, 1, 0.3, 1);
239+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"@prettier/plugin-xml": "^3.3.1",
5050
"@radix-ui/colors": "^3.0.0",
5151
"@radix-ui/react-collapsible": "^1.1.1",
52+
"@radix-ui/react-dialog": "^1.1.2",
5253
"@radix-ui/react-dropdown-menu": "^2.1.2",
5354
"@radix-ui/react-icons": "^1.3.2",
5455
"@radix-ui/react-tabs": "^1.1.1",

src/components/docImage.tsx

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import path from 'path';
22

3-
import Image from 'next/image';
4-
53
import {serverContext} from 'sentry-docs/serverContext';
4+
import {DocImageClient} from './docImageClient';
65

76
export default function DocImage({
87
src,
@@ -40,18 +39,14 @@ export default function DocImage({
4039
.map(s => parseInt(s, 10));
4140

4241
return (
43-
<a href={imgPath} target="_blank" rel="noreferrer">
44-
<Image
45-
{...props}
46-
src={src}
47-
width={width}
48-
height={height}
49-
style={{
50-
width: '100%',
51-
height: 'auto',
52-
}}
53-
alt={props.alt ?? ''}
54-
/>
55-
</a>
42+
<DocImageClient
43+
src={src}
44+
imgPath={imgPath}
45+
width={width}
46+
height={height}
47+
alt={props.alt ?? ''}
48+
style={props.style}
49+
className={props.className}
50+
/>
5651
);
5752
}

src/components/docImageClient.tsx

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
'use client';
2+
3+
import Image from 'next/image';
4+
import {ImageLightbox} from './imageLightbox';
5+
6+
interface DocImageClientProps {
7+
src: string;
8+
imgPath: string;
9+
width: number;
10+
height: number;
11+
alt: string;
12+
style?: React.CSSProperties;
13+
className?: string;
14+
}
15+
16+
export function DocImageClient({
17+
src,
18+
imgPath,
19+
width,
20+
height,
21+
alt,
22+
style,
23+
className,
24+
}: DocImageClientProps) {
25+
const handleContextMenu = (e: React.MouseEvent) => {
26+
// Allow right-click to open in new tab
27+
const link = document.createElement('a');
28+
link.href = imgPath;
29+
link.target = '_blank';
30+
link.rel = 'noreferrer';
31+
link.click();
32+
};
33+
34+
const handleClick = (e: React.MouseEvent) => {
35+
// If Ctrl/Cmd+click, open in new tab instead of lightbox
36+
if (e.ctrlKey || e.metaKey) {
37+
e.preventDefault();
38+
e.stopPropagation();
39+
window.open(imgPath, '_blank', 'noreferrer');
40+
}
41+
};
42+
43+
return (
44+
<div onContextMenu={handleContextMenu} onClick={handleClick}>
45+
<ImageLightbox
46+
src={src}
47+
alt={alt}
48+
width={width}
49+
height={height}
50+
>
51+
<Image
52+
src={src}
53+
width={width}
54+
height={height}
55+
style={{
56+
width: '100%',
57+
height: 'auto',
58+
...style,
59+
}}
60+
className={className}
61+
alt={alt}
62+
/>
63+
</ImageLightbox>
64+
</div>
65+
);
66+
}

src/components/imageLightbox.tsx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
'use client';
2+
3+
import {useState} from 'react';
4+
import * as Dialog from '@radix-ui/react-dialog';
5+
import {X} from 'react-feather';
6+
import Image from 'next/image';
7+
8+
interface ImageLightboxProps {
9+
src: string;
10+
alt: string;
11+
width: number;
12+
height: number;
13+
children: React.ReactNode;
14+
}
15+
16+
export function ImageLightbox({src, alt, width, height, children}: ImageLightboxProps) {
17+
const [open, setOpen] = useState(false);
18+
19+
return (
20+
<Dialog.Root open={open} onOpenChange={setOpen}>
21+
<Dialog.Trigger asChild>
22+
<button className="cursor-pointer border-none bg-transparent p-0 block w-full">
23+
{children}
24+
</button>
25+
</Dialog.Trigger>
26+
27+
<Dialog.Portal>
28+
<Dialog.Overlay className="dialog-overlay fixed inset-0 bg-black/80 backdrop-blur-sm z-50" />
29+
30+
<Dialog.Content className="fixed left-[50%] top-[50%] z-50 max-h-[90vh] max-w-[90vw] translate-x-[-50%] translate-y-[-50%]">
31+
32+
{/* Close button */}
33+
<Dialog.Close className="absolute right-4 top-4 z-10 rounded-sm bg-black/50 p-2 text-white opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2">
34+
<X className="h-4 w-4" />
35+
<span className="sr-only">Close</span>
36+
</Dialog.Close>
37+
38+
{/* Image container */}
39+
<div className="relative flex items-center justify-center">
40+
<Image
41+
src={src}
42+
alt={alt}
43+
width={width}
44+
height={height}
45+
className="max-h-[90vh] max-w-[90vw] object-contain"
46+
style={{
47+
width: 'auto',
48+
height: 'auto',
49+
}}
50+
priority
51+
/>
52+
</div>
53+
54+
{/* Image caption */}
55+
{alt && (
56+
<div className="absolute bottom-0 left-0 right-0 bg-black/75 p-4 text-center text-white">
57+
<p className="text-sm">{alt}</p>
58+
</div>
59+
)}
60+
</Dialog.Content>
61+
</Dialog.Portal>
62+
</Dialog.Root>
63+
);
64+
}

0 commit comments

Comments
 (0)