astro-image-zoom 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,150 @@
1
+ ---
2
+ /**
3
+ * ImageZoom component for Astro
4
+ * Medium-style zoom overlay with accessibility and performance features
5
+ */
6
+
7
+ // Global stylesheet: selectors are prefixed and live in @layer astro-image-zoom
8
+ import "./zoom.css";
9
+ import { wrapImages } from "./wrapImages";
10
+
11
+ export interface Props {
12
+ /**
13
+ * Theme configuration
14
+ */
15
+ theme?: {
16
+ backgroundColor?: string;
17
+ closeButtonColor?: string;
18
+ navigationColor?: string;
19
+ };
20
+
21
+ /**
22
+ * Animation duration in milliseconds. Without it, the duration comes from the
23
+ * --zoom-animation-duration CSS variable (300ms by default)
24
+ */
25
+ animationDuration?: number;
26
+
27
+ /**
28
+ * Enable keyboard navigation
29
+ */
30
+ keyboardNavigation?: boolean;
31
+
32
+ /**
33
+ * Close on backdrop click
34
+ */
35
+ closeOnBackdrop?: boolean;
36
+
37
+ /**
38
+ * Close on image click
39
+ */
40
+ closeOnImage?: boolean;
41
+
42
+ /**
43
+ * Close on scroll/wheel
44
+ */
45
+ closeOnScroll?: boolean;
46
+
47
+ /**
48
+ * Show navigation arrows for galleries
49
+ */
50
+ showNavigation?: boolean;
51
+
52
+ /**
53
+ * Navigation arrows in a bar at the bottom, with the counter, or at the sides of the screen
54
+ */
55
+ navigationLayout?: "bar" | "sides";
56
+
57
+ /**
58
+ * Show the position in the gallery, such as "3 / 8"
59
+ */
60
+ showCounter?: boolean;
61
+
62
+ /**
63
+ * Show the caption of the zoomed image
64
+ */
65
+ showCaption?: boolean;
66
+
67
+ /**
68
+ * Where the caption sits: at the bottom or the top of the screen
69
+ */
70
+ captionPosition?: "bottom" | "top";
71
+
72
+ /**
73
+ * Custom CSS class for the zoom container
74
+ */
75
+ class?: string;
76
+ }
77
+
78
+ const {
79
+ theme = {},
80
+ animationDuration,
81
+ keyboardNavigation = true,
82
+ closeOnBackdrop = true,
83
+ closeOnImage = true,
84
+ closeOnScroll = true,
85
+ showNavigation = true,
86
+ navigationLayout = "bar",
87
+ showCounter = true,
88
+ showCaption = true,
89
+ captionPosition = "bottom",
90
+ class: className = "",
91
+ } = Astro.props;
92
+
93
+ // The theme and the duration become --zoom-* variables on the element, which the overlay copies
94
+ // when it opens: they win over the variables of the site, set on this element or around it
95
+ const variables = Object.entries({
96
+ "--zoom-bg": theme.backgroundColor,
97
+ "--zoom-close-color": theme.closeButtonColor,
98
+ "--zoom-nav-color": theme.navigationColor,
99
+ "--zoom-animation-duration": animationDuration === undefined ? undefined : `${animationDuration}ms`,
100
+ })
101
+ .filter(([, value]) => value)
102
+ .map(([name, value]) => `${name}: ${value}`)
103
+ .join("; ");
104
+ const wrapperClasses = ["astro-image-zoom-wrapper", className]
105
+ .filter(Boolean)
106
+ .join(" ");
107
+
108
+ // Wrap the slot's images in accessible links
109
+ const slotContent = wrapImages(await Astro.slots.render("default"));
110
+ ---
111
+
112
+ <astro-image-zoom
113
+ class={wrapperClasses}
114
+ data-keyboard={keyboardNavigation ? "true" : "false"}
115
+ data-close-backdrop={closeOnBackdrop ? "true" : "false"}
116
+ data-close-image={closeOnImage ? "true" : "false"}
117
+ data-close-scroll={closeOnScroll ? "true" : "false"}
118
+ data-show-nav={showNavigation ? "true" : "false"}
119
+ data-navigation-layout={navigationLayout}
120
+ data-show-counter={showCounter ? "true" : "false"}
121
+ data-show-caption={showCaption ? "true" : "false"}
122
+ data-caption-position={captionPosition}
123
+ style={variables || undefined}
124
+ >
125
+ <Fragment set:html={slotContent} />
126
+ </astro-image-zoom>
127
+
128
+ <script>
129
+ import { Zoom } from "./zoom";
130
+
131
+ // Each <astro-image-zoom> gets its own Zoom instance; they share one overlay,
132
+ // which zoom.ts creates on first use.
133
+ class AstroImageZoomElement extends HTMLElement {
134
+ private zoom: Zoom | null = null;
135
+
136
+ connectedCallback() {
137
+ this.zoom = new Zoom(this);
138
+ }
139
+
140
+ disconnectedCallback() {
141
+ this.zoom?.destroy();
142
+ this.zoom = null;
143
+ }
144
+ }
145
+
146
+ // Register custom element (only runs once even with multiple instances)
147
+ if (!customElements.get("astro-image-zoom")) {
148
+ customElements.define("astro-image-zoom", AstroImageZoomElement);
149
+ }
150
+ </script>
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Antonio Hidalgo Gómez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,467 @@
1
+ # astro-image-zoom
2
+
3
+ Medium-style image zoom for Astro: a click grows each image from its place on the page to fill the
4
+ screen, and the images of a gallery become a carousel you can swipe.
5
+
6
+ > **Beta.** The API may still change before 1.0. Feedback and bug reports are welcome in the
7
+ > [issues](https://github.com/antoniohg/astro-image-zoom/issues).
8
+
9
+ ## Features
10
+
11
+ - **Zoom from the page**: FLIP animations with CSS transforms and `clip-path`, from the thumbnail to
12
+ the full image and back, crops included.
13
+ - **Accessible**: a native modal `<dialog>` that traps and restores focus, keyboard navigation,
14
+ labelled controls, reduced motion and forced colors.
15
+ - **Galleries**: a native scroll-snap carousel with touch and touchpad swipes, arrow keys, buttons and
16
+ a counter.
17
+ - **Isolated**: the overlay lives in a shadow root, so the CSS of your site can't break it.
18
+ - **Customizable**: `--zoom-*` CSS variables, per gallery if you want, `::part()` and props.
19
+ - **Light and dark**: follows the color scheme of the page, or the one you set.
20
+ - **No dependencies**: TypeScript and CSS only; without JavaScript, each image links to its full size.
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ pnpm add astro-image-zoom@beta
26
+ # or: npm install astro-image-zoom@beta
27
+ ```
28
+
29
+ ## Quick Start
30
+
31
+ ### Basic Usage
32
+
33
+ Wrap your images in `<ImageZoom>`. You can mix Astro's `<Image>` and `<Picture>` with plain `<img>`
34
+ tags, local or remote.
35
+
36
+ `<Image>` and `<Picture>` are recommended for performance: Astro resizes and compresses them, so the
37
+ page loads light images and the zoom fetches the full size only when it opens (see
38
+ [Which image the zoom shows](#which-image-the-zoom-shows)). Plain `<img>` tags work too, but they ship
39
+ whatever file you give them.
40
+
41
+ ```astro
42
+ ---
43
+ import ImageZoom from 'astro-image-zoom/ImageZoom.astro';
44
+ import { Image, getImage } from 'astro:assets';
45
+ import photo from '../assets/photo.jpg';
46
+
47
+ // Full-size version for the zoom (see "Which image the zoom shows")
48
+ const fullSize = await getImage({ src: photo, width: 1920 });
49
+ ---
50
+
51
+ <ImageZoom>
52
+ <!-- Optimized Astro image -->
53
+ <Image src={photo} alt="Mountain landscape at sunset" width={400} data-zoom-src={fullSize.src} />
54
+
55
+ <!-- Plain image from public/ -->
56
+ <img src="/photos/city.jpg" alt="City skyline at night" />
57
+
58
+ <!-- Remote image -->
59
+ <img src="https://example.com/photos/forest.jpg" alt="Misty forest at dawn" />
60
+ </ImageZoom>
61
+ ```
62
+
63
+ ### Which image the zoom shows
64
+
65
+ The zoom opens, in this order:
66
+
67
+ 1. The URL in `data-zoom-src`, if the image has it.
68
+ 2. The `href` of a link around the image (`<a href="…" data-zoom>`).
69
+ 3. The image's own `src`.
70
+
71
+ Plain images work out of the box: `<img src="/photo.jpg">` opens that same file, at full size.
72
+
73
+ > **Warning: resized images look blurry when zoomed.** If the image on the page is a smaller
74
+ > version (a thumbnail, or Astro's `<Image width={400}>`, which generates a 400 px file), the zoom
75
+ > enlarges that small file. Add `data-zoom-src` with the full-size version, as shown in
76
+ > [High-Resolution Images](#high-resolution-images) and
77
+ > [Using with Astro Assets](#using-with-astro-assets-optimized-images).
78
+
79
+ ### With Custom Captions
80
+
81
+ ```astro
82
+ <ImageZoom>
83
+ <img
84
+ src="/image1.jpg"
85
+ alt="Beautiful landscape"
86
+ data-zoom-caption="Sunset over the mountains"
87
+ />
88
+ <img
89
+ src="/image2.jpg"
90
+ alt="City skyline"
91
+ data-zoom-caption="Downtown at night"
92
+ />
93
+ </ImageZoom>
94
+ ```
95
+
96
+ ### Gallery with Links
97
+
98
+ ```astro
99
+ <ImageZoom>
100
+ <a href="/full-res-image1.jpg" data-zoom>
101
+ <img src="/thumbnail1.jpg" alt="Thumbnail 1" />
102
+ </a>
103
+ <a href="/full-res-image2.jpg" data-zoom>
104
+ <img src="/thumbnail2.jpg" alt="Thumbnail 2" />
105
+ </a>
106
+ </ImageZoom>
107
+ ```
108
+
109
+ ### High-Resolution Images
110
+
111
+ Use `data-zoom-src` to load higher resolution images in the zoom:
112
+
113
+ ```astro
114
+ <ImageZoom>
115
+ <img
116
+ src="/thumbnail.jpg"
117
+ data-zoom-src="/full-resolution.jpg"
118
+ alt="High quality image"
119
+ />
120
+ </ImageZoom>
121
+ ```
122
+
123
+ ### Using with Astro Assets (Optimized Images)
124
+
125
+ To use optimized Astro images, you can use the `<Image />` component. For the zoom overlay image (which requires a URL string), use the `getImage()` helper.
126
+
127
+ ```astro
128
+ ---
129
+ import { Image, getImage } from 'astro:assets';
130
+ import ImageZoom from 'astro-image-zoom/ImageZoom.astro';
131
+
132
+ import myImage from '../assets/my-image.jpg';
133
+
134
+ // Optimize the full-resolution image for the zoom overlay
135
+ const optimizedImage = await getImage({
136
+ src: myImage,
137
+ format: 'webp',
138
+ width: 1920 // Optional: limit width for better performance
139
+ });
140
+ ---
141
+
142
+ <ImageZoom>
143
+ <Image
144
+ src={myImage}
145
+ alt="A beautiful optimized image"
146
+ width={600}
147
+ data-zoom-src={optimizedImage.src}
148
+ />
149
+ </ImageZoom>
150
+ ```
151
+
152
+ > **Note:** Do not pass the `Image` component directly to `data-zoom-src` or call it as a function. The zoom script expects a string URL for the `data-zoom-src` attribute.
153
+
154
+
155
+ ## Configuration
156
+
157
+ ### Component Props
158
+
159
+ ```astro
160
+ <ImageZoom
161
+ theme={{
162
+ backgroundColor: 'rgba(0, 0, 0, 0.95)',
163
+ closeButtonColor: '#ffffff',
164
+ navigationColor: '#ffffff'
165
+ }}
166
+ animationDuration={300}
167
+ keyboardNavigation={true}
168
+ closeOnBackdrop={true}
169
+ showNavigation={true}
170
+ class="my-custom-class"
171
+ />
172
+ ```
173
+
174
+ #### Props Reference
175
+
176
+ | Prop | Type | Default | Description |
177
+ |------|------|---------|-------------|
178
+ | `theme` | `object` | `{}` | Theme configuration object |
179
+ | `theme.backgroundColor` | `string` | light or dark, following the page | Overlay background color (`--zoom-bg`) |
180
+ | `theme.closeButtonColor` | `string` | light or dark, following the page | Close button color (`--zoom-close-color`) |
181
+ | `theme.navigationColor` | `string` | light or dark, following the page | Arrows and counter color (`--zoom-nav-color`) |
182
+ | `animationDuration` | `number` | — | Animation duration in milliseconds; overrides `--zoom-animation-duration` (300ms by default) |
183
+ | `keyboardNavigation` | `boolean` | `true` | Enable keyboard shortcuts |
184
+ | `closeOnBackdrop` | `boolean` | `true` | Close when clicking backdrop |
185
+ | `closeOnImage` | `boolean` | `true` | Close when clicking the zoomed image |
186
+ | `closeOnScroll` | `boolean` | `true` | Close when scrolling/wheeling |
187
+ | `showNavigation` | `boolean` | `true` | Show navigation arrows |
188
+ | `navigationLayout` | `'bar' \| 'sides'` | `'bar'` | Arrows and counter in a bar at the bottom, or arrows at the sides |
189
+ | `showCounter` | `boolean` | `true` | Show the position in the gallery, such as "3 / 8" |
190
+ | `showCaption` | `boolean` | `true` | Show the caption of the zoomed image |
191
+ | `captionPosition` | `'bottom' \| 'top'` | `'bottom'` | Where the caption sits on the screen |
192
+ | `class` | `string` | `''` | Custom CSS class |
193
+
194
+ ### Custom Styling
195
+
196
+ You can override the default styles using CSS variables:
197
+
198
+ ```css
199
+ :root {
200
+ --zoom-bg: rgba(20, 20, 30, 0.98);
201
+ --zoom-close-color: #ff6b6b;
202
+ --zoom-nav-color: #4ecdc4; /* arrows and counter */
203
+ --zoom-close-bg: rgba(255, 255, 255, 0.1);
204
+ --zoom-nav-bg: rgba(255, 255, 255, 0.1);
205
+ --zoom-caption-color: #fff;
206
+ --zoom-caption-bg: rgba(0, 0, 0, 0.9);
207
+ --zoom-animation-duration: 400ms; /* ms or s; the animationDuration prop wins over it */
208
+
209
+ /* Layout */
210
+ --zoom-padding: 0; /* space between the zoomed image and the screen edges */
211
+ --zoom-image-radius: 0; /* corners of the zoomed image */
212
+ --zoom-button-size: 44px; /* arrows and close button */
213
+ --zoom-button-radius: 999px; /* shape of the buttons and the navigation bar */
214
+ --zoom-controls-offset: 20px; /* distance from the controls and the caption to the edges */
215
+ --zoom-caption-max-width: 70%; /* 90% on phones */
216
+ --zoom-caption-font: inherit; /* the font of your site */
217
+ --zoom-caption-font-size: 13px;
218
+ --zoom-color-scheme: light dark; /* set "dark" or "light" to follow your own theme toggle */
219
+ --zoom-caption-radius: 6px;
220
+
221
+ /* Focus ring drawn on the image when its link has keyboard focus */
222
+ --zoom-focus-outline: 3px solid rebeccapurple; /* default: 2px solid currentColor */
223
+ --zoom-focus-offset: 3px; /* use a negative value if a parent with overflow: hidden clips it */
224
+ }
225
+ ```
226
+
227
+ Set them on `:root` for the whole site, or on any element around an `<ImageZoom>` for that gallery
228
+ only: when a gallery opens, the overlay takes the values found around it. The `theme` prop wins over
229
+ the variables.
230
+
231
+ ```css
232
+ .portfolio {
233
+ --zoom-padding: 5vmin;
234
+ --zoom-image-radius: 12px;
235
+ }
236
+ ```
237
+
238
+ #### Parts
239
+
240
+ The zoom overlay lives in a [shadow root](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM),
241
+ so the CSS of your site can't break it: rules such as `button { all: unset }` or `svg { width: 1em }`
242
+ never reach it. The variables above cross into it. For anything they don't cover, style the exposed
243
+ parts with `::part()`:
244
+
245
+ ```css
246
+ astro-image-zoom-overlay::part(caption) {
247
+ text-transform: uppercase;
248
+ }
249
+ ```
250
+
251
+ | Part | Element |
252
+ |------|---------|
253
+ | `overlay` | The `<dialog>` |
254
+ | `backdrop` | The background behind the image |
255
+ | `track` | The carousel that holds the slides |
256
+ | `slide`, `image` | Each slide of the carousel and its image |
257
+ | `caption` | The caption |
258
+ | `close` | The close button |
259
+ | `toolbar` | The navigation bar |
260
+ | `nav`, `prev`, `next` | The arrows (`nav` matches both) |
261
+ | `counter` | The position in the gallery |
262
+
263
+ The component loads its own stylesheet, so you don't need to import `zoom.css` yourself. Import
264
+ `astro-image-zoom/zoom.css` directly only if you use `ZoomClass` without the `<ImageZoom>` component.
265
+
266
+ #### Cascade layer
267
+
268
+ The styles for the images on your page (the zoom cursor and the focus ring) live in the
269
+ `astro-image-zoom` [cascade layer](https://developer.mozilla.org/en-US/docs/Web/CSS/@layer).
270
+ Any unlayered CSS on your page overrides them, with no specificity tricks or `!important`.
271
+
272
+ If your site uses its own layers, add `astro-image-zoom` to your layer order so you decide what wins.
273
+ Declare the order before any stylesheet loads (for example, in an inline `<style>` at the top of
274
+ `<head>`), because the first time a layer appears fixes its position:
275
+
276
+ ```html
277
+ <style is:inline>
278
+ @layer reset, base, astro-image-zoom, components;
279
+ </style>
280
+ ```
281
+
282
+ With this order your reset can't break the zoom cursor or the focus ring, and your `components`
283
+ layer can still customize them. Class names (`.astro-image-zoom-*`) are prefixed, so the styles don't
284
+ clash with the rest of your site.
285
+
286
+ To style the images of one gallery on the page, pass a class:
287
+
288
+ ```astro
289
+ <ImageZoom class="custom-zoom">
290
+ <!-- Your images -->
291
+ </ImageZoom>
292
+
293
+ <style>
294
+ /* Global: the wrapper is rendered by the component, outside the scope of your page */
295
+ :global(.custom-zoom img) {
296
+ border-radius: 8px;
297
+ }
298
+ </style>
299
+ ```
300
+
301
+ ## Keyboard Shortcuts
302
+
303
+ - **Escape** - Close zoom
304
+ - **Arrow Left** - Previous image
305
+ - **Arrow Right** - Next image
306
+ - **Tab / Shift+Tab** - Navigate between controls
307
+
308
+ ## Touch Gestures
309
+
310
+ - **Swipe left** - Next image (also a two-finger swipe on a touchpad)
311
+ - **Swipe right** - Previous image
312
+ - **Swipe up or down** - Close the zoom and keep scrolling the page
313
+ - **Tap the backdrop** - Close the zoom
314
+
315
+ ## Accessibility
316
+
317
+ - A native modal `<dialog>`: focus moves to the close button, stays inside while it is open and
318
+ returns to the image when it closes.
319
+ - Each image becomes a link, reachable with the keyboard, with a visible focus ring.
320
+ - Labelled buttons; the caption is announced when the image changes.
321
+ - Reduced motion turns the animations off; forced colors (Windows high contrast) keep the controls
322
+ visible.
323
+
324
+ ## Performance
325
+
326
+ - The page loads the images you give it; the full-size version loads only when the zoom opens, and
327
+ the neighbors of a gallery are preloaded.
328
+ - Animations are CSS only (transforms, `clip-path` and opacity); the script measures positions and
329
+ waits for them to end.
330
+ - One overlay shared by every gallery on the page, and one delegated click listener per gallery.
331
+
332
+ ## Advanced Usage
333
+
334
+ ### Multiple Zoom Instances
335
+
336
+ ```astro
337
+ <ImageZoom>
338
+ <img src="/gallery1-image1.jpg" alt="Gallery 1" />
339
+ <img src="/gallery1-image2.jpg" alt="Gallery 1" />
340
+ </ImageZoom>
341
+
342
+ <ImageZoom>
343
+ <img src="/gallery2-image1.jpg" alt="Gallery 2" />
344
+ <img src="/gallery2-image2.jpg" alt="Gallery 2" />
345
+ </ImageZoom>
346
+ ```
347
+
348
+ ### Custom Theme
349
+
350
+ ```astro
351
+ <ImageZoom
352
+ theme={{
353
+ backgroundColor: 'rgba(26, 32, 44, 0.95)',
354
+ closeButtonColor: '#f7fafc',
355
+ navigationColor: '#63b3ed'
356
+ }}
357
+ animationDuration={400}
358
+ >
359
+ <img src="/image.jpg" alt="Custom themed image" />
360
+ </ImageZoom>
361
+ ```
362
+
363
+ ### Programmatic Control (Advanced)
364
+
365
+ ```astro
366
+ <ImageZoom class="my-gallery" />
367
+
368
+ <script>
369
+ import { ZoomClass } from 'astro-image-zoom';
370
+
371
+ const wrapper = document.querySelector('.my-gallery');
372
+ const zoom = new ZoomClass(wrapper);
373
+
374
+ // Later, if needed:
375
+ // zoom.destroy();
376
+ </script>
377
+ ```
378
+
379
+ `ZoomClass` reads the options from the `data-*` attributes that `<ImageZoom>` renders; the theme and
380
+ the duration come from the `--zoom-*` variables.
381
+
382
+ ## Examples
383
+
384
+ ### Blog Post Images
385
+
386
+ ```astro
387
+ ---
388
+ import ImageZoom from 'astro-image-zoom/ImageZoom.astro';
389
+ ---
390
+
391
+ <article>
392
+ <h1>My Blog Post</h1>
393
+ <p>Check out these amazing photos from my trip:</p>
394
+
395
+ <ImageZoom>
396
+ <figure>
397
+ <img
398
+ src="/trip-photo-1.jpg"
399
+ alt="Mountain landscape"
400
+ data-zoom-caption="The view from the summit"
401
+ />
402
+ <figcaption>Summit view</figcaption>
403
+ </figure>
404
+
405
+ <figure>
406
+ <img
407
+ src="/trip-photo-2.jpg"
408
+ alt="Lake reflection"
409
+ data-zoom-caption="Perfect morning reflection"
410
+ />
411
+ <figcaption>Lake reflection</figcaption>
412
+ </figure>
413
+ </ImageZoom>
414
+ </article>
415
+ ```
416
+
417
+ ### Portfolio Grid
418
+
419
+ ```astro
420
+ ---
421
+ import ImageZoom from 'astro-image-zoom/ImageZoom.astro';
422
+
423
+ const projects = [
424
+ { thumb: '/thumb1.jpg', full: '/full1.jpg', title: 'Project 1' },
425
+ { thumb: '/thumb2.jpg', full: '/full2.jpg', title: 'Project 2' },
426
+ { thumb: '/thumb3.jpg', full: '/full3.jpg', title: 'Project 3' },
427
+ ];
428
+ ---
429
+
430
+ <ImageZoom>
431
+ <div class="portfolio-grid">
432
+ {projects.map(project => (
433
+ <img
434
+ src={project.thumb}
435
+ data-zoom-src={project.full}
436
+ alt={project.title}
437
+ data-zoom-caption={project.title}
438
+ />
439
+ ))}
440
+ </div>
441
+ </ImageZoom>
442
+
443
+ <style>
444
+ .portfolio-grid {
445
+ display: grid;
446
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
447
+ gap: 1rem;
448
+ }
449
+ </style>
450
+ ```
451
+
452
+ ## Browser Support
453
+
454
+ Current browsers: the overlay uses `<dialog>`, shadow DOM, `:has()` and `light-dark()` (Chrome and
455
+ Edge 123, Firefox 120, Safari 17.5 and later). During the beta it has been tested in Chromium; reports
456
+ from Firefox and Safari, desktop or mobile, are welcome.
457
+
458
+ ## License
459
+
460
+ [MIT](https://github.com/antoniohg/astro-image-zoom/blob/main/LICENSE)
461
+
462
+ ## Contributing
463
+
464
+ Found a bug or have an idea? Open an [issue](https://github.com/antoniohg/astro-image-zoom/issues) or
465
+ a pull request.
466
+
467
+ Inspired by Medium's image viewer.
package/env.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ // Vite imports a stylesheet with ?inline as a string, minified in production builds.
2
+ // Astro projects get this from astro/client; declared here so zoom.ts type-checks on its own.
3
+ declare module '*.css?inline' {
4
+ const css: string;
5
+ export default css;
6
+ }
package/index.js ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * astro-image-zoom
3
+ * A Medium-style zoom component for Astro
4
+ */
5
+
6
+ // Export the TypeScript module
7
+ export { Zoom as ZoomClass } from './zoom';