1+ ---
2+ // src/components/Figure.astro
3+ import { Image } from ' astro:assets' ;
4+
5+ type Layout = ' bottom-center' | ' bottom-left' | ' bottom-right' | ' top-hover' | ' left' | ' right' ;
6+
7+ interface Props {
8+ src: string ;
9+ alt: string ;
10+ width: number ;
11+ height: number ;
12+ class? : string ;
13+ loading? : ' eager' | ' lazy' ;
14+ quality? : number | ' low' | ' mid' | ' high' | ' max' ;
15+ format? : ' avif' | ' png' | ' jpeg' | ' svg' | ' webp' ;
16+ figureClass? : string ;
17+ layout? : Layout ;
18+ zoomable? : boolean ;
19+ }
20+
21+ const {
22+ figureClass,
23+ layout = ' bottom-center' ,
24+ zoomable = false ,
25+ src,
26+ alt,
27+ width,
28+ height,
29+ class : className,
30+ loading,
31+ quality,
32+ format,
33+ } = Astro .props ;
34+
35+ const isRowLayout = layout === ' left' || layout === ' right' ;
36+ ---
37+
38+ <figure
39+ class:list ={ [
40+ ' group' ,
41+ figureClass ,
42+ {
43+ ' relative' : ! isRowLayout ,
44+ ' flex items-center gap-4' : isRowLayout ,
45+ ' flex-row-reverse' : layout === ' left' ,
46+ ' flex-row' : layout === ' right' ,
47+ },
48+ ]}
49+ data-zoomable ={ zoomable ? ' true' : undefined }
50+ >
51+ { isRowLayout ? (
52+ <div class = " flex-shrink-0 w-1/2" >
53+ <Image { src } { alt } { width } { height } class = { className } { loading } { quality } { format } />
54+ </div >
55+ ) : (
56+ <Image { src } { alt } { width } { height } class = { className } { loading } { quality } { format } />
57+ )}
58+
59+ { Astro .slots .has (' caption' ) && (
60+ <figcaption
61+ class :list = { [
62+ ' transition-all duration-300' ,
63+ {
64+ ' mt-2 text-sm' : layout .startsWith (' bottom' ),
65+ ' text-center' : layout === ' bottom-center' ,
66+ ' text-left' : layout === ' bottom-left' ,
67+ ' text-right' : layout === ' bottom-right' ,
68+ ' flex-1' : isRowLayout ,
69+ ' absolute inset-0 flex items-center justify-center p-4 bg-black/60 text-white opacity-0 group-hover:opacity-100' : layout === ' top-hover' ,
70+ },
71+ ]}
72+ >
73+ <slot name = " caption" />
74+ </figcaption >
75+ )}
76+ </figure >
77+
78+ <script >
79+ function initializeImageViewer() {
80+ const figures = document.querySelectorAll('figure[data-zoomable="true"]');
81+
82+ figures.forEach(figure => {
83+ const img = figure.querySelector('img');
84+ if (img instanceof HTMLElement) {
85+ if (img.dataset.viewerInitialized) return;
86+ img.dataset.viewerInitialized = 'true';
87+ img.style.cursor = 'zoom-in';
88+
89+ img.addEventListener('click', () => openViewer(img as HTMLImageElement));
90+ }
91+ });
92+
93+ function openViewer(img: HTMLImageElement) {
94+ const overlay = document.createElement('div');
95+ overlay.id = 'image-viewer-overlay';
96+
97+ const content = document.createElement('img');
98+ content.id = 'image-viewer-content';
99+ content.src = img.src;
100+
101+ overlay.appendChild(content);
102+ document.body.appendChild(overlay);
103+ document.body.style.overflow = 'hidden'; // Keep this to prevent background scroll
104+
105+ let scale = 1;
106+ let isDragging = false;
107+ let startX = 0, startY = 0;
108+ let translateX = 0, translateY = 0;
109+
110+ const updateTransform = () => {
111+ content.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
112+ };
113+
114+ const wheelHandler = (e: WheelEvent) => {
115+ e.preventDefault();
116+ const scaleAmount = e.deltaY > 0 ? -0.1 : 0.1;
117+ scale = Math.max(0.5, Math.min(scale + scaleAmount, 5));
118+ updateTransform();
119+ };
120+
121+ const mouseDownHandler = (e: MouseEvent) => {
122+ e.preventDefault();
123+ isDragging = true;
124+ startX = e.clientX - translateX;
125+ startY = e.clientY - translateY;
126+ content.style.cursor = 'grabbing';
127+ };
128+
129+ const mouseMoveHandler = (e: MouseEvent) => {
130+ if (!isDragging) return;
131+ translateX = e.clientX - startX;
132+ translateY = e.clientY - startY;
133+ updateTransform();
134+ };
135+
136+ const mouseUpHandler = () => {
137+ isDragging = false;
138+ content.style.cursor = 'grab';
139+ };
140+
141+ const closeModal = () => {
142+ document.body.style.overflow = 'auto';
143+ overlay.remove();
144+ window.removeEventListener('mousemove', mouseMoveHandler);
145+ window.removeEventListener('mouseup', mouseUpHandler);
146+ };
147+
148+ overlay.addEventListener('click', closeModal);
149+ content.addEventListener('click', (e) => e.stopPropagation());
150+ overlay.addEventListener('wheel', wheelHandler);
151+ content.addEventListener('mousedown', mouseDownHandler);
152+ window.addEventListener('mousemove', mouseMoveHandler);
153+ window.addEventListener('mouseup', mouseUpHandler);
154+ }
155+ }
156+
157+ initializeImageViewer();
158+ document.addEventListener('astro:after-swap', initializeImageViewer);
159+ </script >
160+
161+ <style is:global >
162+ #image-viewer-overlay {
163+ position: fixed;
164+ inset: 0;
165+ z-index: 9999;
166+ display: flex;
167+ align-items: center;
168+ justify-content: center;
169+ padding: 2rem;
170+ background-color: rgba(255, 255, 255, 0.85);
171+ backdrop-filter: blur(8px);
172+ /* Removed transition for opacity */
173+ }
174+
175+ html.dark #image-viewer-overlay {
176+ background-color: rgba(10, 10, 10, 0.85);
177+ }
178+
179+ #image-viewer-content {
180+ max-width: 100%;
181+ max-height: 100%;
182+ object-fit: contain;
183+ border-radius: 8px;
184+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
185+ transform-origin: center center;
186+ transition: transform 0.15s linear;
187+ cursor: grab;
188+ }
189+ </style >
0 commit comments