uix-image-magnify 0.2.2 → 0.2.4

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.
package/README.md CHANGED
@@ -2,7 +2,40 @@
2
2
 
3
3
  **Fast, accessible, SSR-safe image magnifier and product gallery for React & Next.js.**
4
4
 
5
- A lightweight, fully-typed React component set for zooming images with a lens, a side-by-side pane, inner pan-and-zoom, or a follow-style magnifier. Ships with a product-style gallery (thumbnails + arrows + counter), a headless slider hook, and SSR-safe variants that prevent hydration mismatches in Next.js App Router, Remix, etc.
5
+ A lightweight, fully-typed React component set for zooming images with a lens, a side-by-side pane, inner pan-and-zoom, or a follow-style magnifier. Ships with a product-style gallery (thumbnails + arrows + counter), headless hooks, and SSR-safe variants that prevent hydration mismatches in Next.js App Router, Remix, and any other SSR framework.
6
+
7
+ ---
8
+
9
+ ## Table of Contents
10
+
11
+ - [Features](#features)
12
+ - [Installation](#installation)
13
+ - [Quick Start](#quick-start)
14
+ - [Single Image Magnifier](#single-image-magnifier)
15
+ - [Zoom modes](#zoom-modes)
16
+ - [Zoom triggers](#zoom-triggers)
17
+ - [Inner mode panning](#inner-mode-panning)
18
+ - [Wheel zoom](#wheel-zoom)
19
+ - [Responsive behaviour](#responsive-behaviour)
20
+ - [Product Gallery](#product-gallery)
21
+ - [Thumbnails](#thumbnails)
22
+ - [Controlled gallery](#controlled-gallery)
23
+ - [Custom arrows](#custom-arrows)
24
+ - [Disable features](#disable-features)
25
+ - [Side-by-side pane inside the gallery](#side-by-side-pane-inside-the-gallery)
26
+ - [SSR & Next.js](#ssr--nextjs)
27
+ - [Hooks](#hooks)
28
+ - [useImageSlider](#useimageslider)
29
+ - [useImageMagnify](#useimagemagnify)
30
+ - [Props Reference](#props-reference)
31
+ - [ImageMagnifyProps](#imagemagnifyprops)
32
+ - [ImageMagnifyGalleryProps](#imagemagnifygalleryprops)
33
+ - [Styling](#styling)
34
+ - [Accessibility](#accessibility)
35
+ - [TypeScript](#typescript)
36
+ - [Package Exports](#package-exports)
37
+ - [Browser Support](#browser-support)
38
+ - [License](#license)
6
39
 
7
40
  ---
8
41
 
@@ -11,13 +44,14 @@ A lightweight, fully-typed React component set for zooming images with a lens, a
11
44
  - 🔍 **4 zoom modes** — `lens`, `side-by-side`, `inner`, `follow`
12
45
  - 🖱️ **6 zoom triggers** — `hover`, `click`, `double-click`, `hold`, `wheel`, `none`
13
46
  - 🖼️ **Product gallery** with thumbnails, arrows, counter, and loop
14
- - ♿ **Accessible** — keyboard nav, ARIA roles, screen-reader live region
47
+ - ♿ **Accessible** — keyboard navigation, ARIA roles, live region, `aria-current` thumbnails
15
48
  - 🧠 **SSR-safe** — dedicated `/ssr` entry point eliminates hydration mismatches
16
49
  - 📱 **Responsive** — zoom pane automatically moves below the image on mobile
17
50
  - 🎨 **Customizable** — every part is styleable; arrows accept custom React nodes
18
51
  - 🪝 **Headless hooks** — `useImageMagnify`, `useImageSlider`
19
52
  - 🧩 **TypeScript-first** with strict prop types
20
53
  - 🌲 **Tree-shakeable** — `sideEffects: false`, ESM + CJS builds
54
+ - ⚡ **Performance** — `requestAnimationFrame`-batched DOM writes; zero re-renders during zoom
21
55
 
22
56
  ---
23
57
 
@@ -33,6 +67,8 @@ pnpm add uix-image-magnify
33
67
 
34
68
  `react` and `react-dom` (`>=17`) are peer dependencies. `swiper` is installed automatically as a dependency for the gallery.
35
69
 
70
+ **Requirements:** Node 18+ for the build toolchain (not for runtime). Modern evergreen browsers (see [Browser Support](#browser-support)).
71
+
36
72
  ---
37
73
 
38
74
  ## Quick Start
@@ -76,7 +112,7 @@ import { ImageMagnify } from "uix-image-magnify";
76
112
  lensShape="circle"
77
113
  lensSize={160}
78
114
  lazy
79
- />
115
+ />;
80
116
  ```
81
117
 
82
118
  ### Zoom modes
@@ -88,27 +124,27 @@ import { ImageMagnify } from "uix-image-magnify";
88
124
  | `inner` | The image itself scales and pans inside its own container. |
89
125
  | `follow` | A lens follows the cursor with an optional offset. |
90
126
 
91
- > **Mobile:** on `side-by-side` + `panePosition="bottom"`, the pane flows below the image instead of being absolutely positioned — no overlap, no clipping.
92
-
93
127
  ### Zoom triggers
94
128
 
95
- | Trigger | Behavior |
96
- | -------------- | ------------------------------------------------------ |
97
- | `hover` | Zooms while the pointer is over the image. |
98
- | `click` | Toggles zoom on click. |
99
- | `double-click` | Toggles zoom on double-click. |
100
- | `hold` | Zooms only while the pointer is pressed down. |
101
- | `wheel` | Scroll to zoom; supports `minZoomScale`/`maxZoomScale`.|
102
- | `none` | Disables interaction (renders a plain image). |
129
+ | Trigger | Behavior |
130
+ | -------------- | --------------------------------------------------------- |
131
+ | `hover` | Zooms while the pointer is over the image. |
132
+ | `click` | Toggles zoom on click. |
133
+ | `double-click` | Toggles zoom on double-click. |
134
+ | `hold` | Zooms only while the pointer is pressed down. |
135
+ | `wheel` | Scroll to zoom; supports `minZoomScale` / `maxZoomScale`. |
136
+ | `none` | Disables interaction (renders a plain image). |
103
137
 
104
- > On touch devices, interaction behaves as tap-to-toggle regardless of `zoomType` (except `none`).
138
+ > **Touch devices:** interaction behaves as tap-to-toggle regardless of `zoomType` (except `none`).
139
+ >
140
+ > **Wheel mode:** on desktop, zoom is driven by the wheel event only — a plain click does not toggle zoom.
105
141
 
106
- ### `inner` mode panning
142
+ ### Inner mode panning
107
143
 
108
144
  When `mode="inner"`, set `dragMode`:
109
145
 
110
146
  - `dragMode="follow"` (default) — the pixel under the cursor stays under the cursor.
111
- - `dragMode="pan"` — drag to pan around the zoomed image.
147
+ - `dragMode="pan"` — drag to pan around the zoomed image. The image is **centered** the first time you zoom in.
112
148
 
113
149
  ```tsx
114
150
  <ImageMagnify
@@ -135,11 +171,17 @@ When `mode="inner"`, set `dragMode`:
135
171
  />
136
172
  ```
137
173
 
174
+ ### Responsive behaviour
175
+
176
+ - On screens `≤ 768px` wide, `width` is forced to `"100%"`.
177
+ - When `mode="side-by-side"` and the pane would render to the left/right on mobile, it automatically flows **below** the image instead — no overflow, no clipping.
178
+ - The `useMediaQuery` hook is SSR-safe: it returns `false` during SSR and the first client render, then updates before paint.
179
+
138
180
  ---
139
181
 
140
182
  ## Product Gallery
141
183
 
142
- `ImageMagnifyGallery` combines a Swiper-powered slider with the magnifier. It supports thumbnails, arrows, a counter, loop, keyboard navigation, and adjacent-image preloading.
184
+ `ImageMagnifyGallery` combines a Swiper-powered main slider with the magnifier and a lightweight thumbnail strip. It supports thumbnails, arrows, a counter, loop, keyboard navigation, and adjacent-image preloading.
143
185
 
144
186
  ```tsx
145
187
  import { ImageMagnifyGallery } from "uix-image-magnify";
@@ -162,6 +204,27 @@ const images = [
162
204
  thumbnailSize={72}
163
205
  thumbnailGap={10}
164
206
  onIndexChange={(i) => console.log("active:", i)}
207
+ />;
208
+ ```
209
+
210
+ ### Thumbnails
211
+
212
+ Thumbnails are rendered as a **plain scrollable flex strip** of `<button>` elements — not a nested Swiper. This is significantly faster (no second carousel instance, no duplicated magnifier instances) and gives you full control over styling.
213
+
214
+ - `thumbnailSize` — thumbnail width/height in px (default `80`).
215
+ - `thumbnailGap` — gap between thumbnails in px (default `8`).
216
+ - Clicking a thumbnail navigates the main slider.
217
+ - The active thumbnail receives `aria-current="true"` and merges `thumbnailActiveStyle` over `thumbnailStyle`.
218
+
219
+ ```tsx
220
+ <ImageMagnifyGallery
221
+ images={images}
222
+ alt="Product"
223
+ thumbnailSize={64}
224
+ thumbnailGap={12}
225
+ thumbnailContainerStyle={{ justifyContent: "center" }}
226
+ thumbnailStyle={{ borderRadius: 8 }}
227
+ thumbnailActiveStyle={{ borderColor: "#0af" }}
165
228
  />
166
229
  ```
167
230
 
@@ -175,9 +238,11 @@ const [active, setActive] = useState(0);
175
238
  alt="Product"
176
239
  activeIndex={active}
177
240
  onIndexChange={setActive}
178
- />
241
+ />;
179
242
  ```
180
243
 
244
+ When `activeIndex` is provided, the gallery becomes **controlled**: `onIndexChange` fires on every slide change, and external updates to `activeIndex` call `swiper.slideTo(...)`.
245
+
181
246
  ### Custom arrows
182
247
 
183
248
  ```tsx
@@ -191,6 +256,8 @@ const [active, setActive] = useState(0);
191
256
  />
192
257
  ```
193
258
 
259
+ Arrow buttons are automatically disabled (`disabled` + `aria-disabled`) at the ends when `loop={false}`.
260
+
194
261
  ### Disable features
195
262
 
196
263
  ```tsx
@@ -206,22 +273,61 @@ const [active, setActive] = useState(0);
206
273
  />
207
274
  ```
208
275
 
276
+ ### Side-by-side pane inside the gallery
277
+
278
+ Swiper applies `overflow: hidden` to its wrapper, so a `side-by-side` pane positioned `left` or `right` of the image will be **clipped** by the slide boundary. Options:
279
+
280
+ 1. Use `mode="lens"`, `mode="inner"`, or `mode="follow"` — these render inside the image bounds.
281
+ 2. Use `mode="side-by-side"` with `panePosition="bottom"` and give the slide enough vertical space.
282
+ 3. Pass `swiperStyle={{ overflow: "visible" }}` to let the pane escape the slide — note that adjacent slides will then be visible too.
283
+
284
+ ```tsx
285
+ // Option 1: use lens mode
286
+ <ImageMagnifyGallery images={images} alt="Product" mode="lens" />
287
+
288
+ // Option 2: bottom pane
289
+ <ImageMagnifyGallery
290
+ images={images}
291
+ alt="Product"
292
+ mode="side-by-side"
293
+ panePosition="bottom"
294
+ paneHeight={320}
295
+ />
296
+
297
+ // Option 3: opt out of clipping
298
+ <ImageMagnifyGallery
299
+ images={images}
300
+ alt="Product"
301
+ mode="side-by-side"
302
+ panePosition="right"
303
+ swiperStyle={{ overflow: "visible" }}
304
+ />
305
+ ```
306
+
209
307
  ---
210
308
 
211
309
  ## SSR & Next.js
212
310
 
213
- The magnifier touches `window`, `matchMedia`, and pointer events. To avoid hydration mismatches, this package ships **SSR-safe wrappers** under the `uix-image-magnify/ssr` entry point:
311
+ The magnifier touches `window`, `matchMedia`, `ResizeObserver`, and pointer events. To avoid hydration mismatches, this package ships **SSR-safe wrappers** under the `uix-image-magnify/ssr` entry point:
214
312
 
215
313
  - `ImageMagnifySSR` — plain `<img>` on server + first client render, swaps to `<ImageMagnify>` after hydration.
216
314
  - `ImageMagnifyGallerySSR` — same approach for the gallery.
217
315
 
316
+ Both non-SSR components are also re-exported from this entry point for convenience.
317
+
218
318
  ### Option 1 — Use the SSR wrappers (recommended)
219
319
 
220
320
  ```tsx
221
321
  // app/products/[id]/page.tsx (Server Component-safe import)
222
322
  import { ImageMagnifySSR } from "uix-image-magnify/ssr";
223
323
 
224
- export default function ProductImage({ src, alt }: { src: string; alt: string }) {
324
+ export default function ProductImage({
325
+ src,
326
+ alt,
327
+ }: {
328
+ src: string;
329
+ alt: string;
330
+ }) {
225
331
  return (
226
332
  <ImageMagnifySSR
227
333
  src={src}
@@ -247,11 +353,11 @@ import dynamic from "next/dynamic";
247
353
 
248
354
  const ImageMagnify = dynamic(
249
355
  () => import("uix-image-magnify").then((m) => m.ImageMagnify),
250
- { ssr: false }
356
+ { ssr: false },
251
357
  );
252
358
  ```
253
359
 
254
- Both approaches produce zero hydration warnings. Option 1 keeps the SSR-rendered `<img>` visible before hydration so users see the image immediately.
360
+ Both approaches produce zero hydration warnings. Option 1 keeps the SSR-rendered `<img>` visible before hydration so users see the image immediately (no layout shift, no blank frame).
255
361
 
256
362
  ### Remix / React Router
257
363
 
@@ -278,15 +384,26 @@ const { index, setIndex, prev, next, canPrev, canNext } = useImageSlider({
278
384
 
279
385
  **Options**
280
386
 
281
- | Option | Type | Default | Description |
282
- | --------------- | ------------------------- | ------- | ------------------------------ |
283
- | `length` | `number` | — | Total items. |
284
- | `defaultIndex` | `number` | `0` | Initial index (uncontrolled). |
285
- | `index` | `number` | — | Controlled index. |
286
- | `onIndexChange` | `(index: number) => void` | — | Fired when the index changes. |
287
- | `loop` | `boolean` | `true` | Wrap around at the ends. |
387
+ | Option | Type | Default | Description |
388
+ | --------------- | ------------------------- | ------- | ----------------------------- |
389
+ | `length` | `number` | — | Total items. |
390
+ | `defaultIndex` | `number` | `0` | Initial index (uncontrolled). |
391
+ | `index` | `number` | — | Controlled index. |
392
+ | `onIndexChange` | `(index: number) => void` | — | Fired when the index changes. |
393
+ | `loop` | `boolean` | `true` | Wrap around at the ends. |
394
+
395
+ **Returns**
288
396
 
289
- **Returns:** `{ index, setIndex, prev, next, canPrev, canNext }`.
397
+ | Field | Type | Description |
398
+ | ---------- | ------------------------ | ----------------------------------------------------------- |
399
+ | `index` | `number` | Current (clamped) index. |
400
+ | `canPrev` | `boolean` | Whether a previous item exists (always `true` when `loop`). |
401
+ | `canNext` | `boolean` | Whether a next item exists (always `true` when `loop`). |
402
+ | `setIndex` | `(next: number) => void` | Go to a specific index (wraps when `loop`). |
403
+ | `prev` | `() => void` | Go to previous item. |
404
+ | `next` | `() => void` | Go to next item. |
405
+
406
+ `defaultIndex` is clamped to `[0, length - 1]`.
290
407
 
291
408
  ### `useImageMagnify`
292
409
 
@@ -312,8 +429,47 @@ const {
312
429
  zoomType: "hover",
313
430
  zoomScale: 2.5,
314
431
  });
432
+
433
+ return (
434
+ <div ref={containerRef} {...handlers} style={{ position: "relative" }}>
435
+ <img
436
+ ref={imageRef}
437
+ src="/thumb.jpg"
438
+ alt=""
439
+ style={{ display: "block", width: "100%" }}
440
+ />
441
+ {isZoomed && (
442
+ <div
443
+ ref={paneRef}
444
+ style={{
445
+ position: "absolute",
446
+ left: "100%",
447
+ top: 0,
448
+ width: 400,
449
+ height: 400,
450
+ backgroundImage: bgImage ? `url("${bgImage}")` : undefined,
451
+ backgroundRepeat: "no-repeat",
452
+ }}
453
+ />
454
+ )}
455
+ </div>
456
+ );
315
457
  ```
316
458
 
459
+ **Return value**
460
+
461
+ | Field | Type | Description |
462
+ | -------------- | ----------------------------- | ----------------------------------------------- |
463
+ | `isZoomed` | `boolean` | Whether zoom is currently active. |
464
+ | `zoomScale` | `number` | Current zoom scale. |
465
+ | `bgImage` | `string \| undefined` | URL to use as the zoomed background. |
466
+ | `containerRef` | `RefObject<HTMLDivElement>` | Attach to the wrapper element. |
467
+ | `imageRef` | `RefObject<HTMLImageElement>` | Attach to the `<img>`. |
468
+ | `lensRef` | `RefObject<HTMLDivElement>` | Attach to the lens element (lens/follow modes). |
469
+ | `paneRef` | `RefObject<HTMLDivElement>` | Attach to the side-by-side pane. |
470
+ | `cursorRef` | `RefObject<HTMLDivElement>` | Attach to the side-by-side cursor dot. |
471
+ | `handlers` | `object` | Spread on the container element. |
472
+
317
473
  **Option highlights**
318
474
 
319
475
  | Option | Type | Default |
@@ -345,57 +501,58 @@ const {
345
501
 
346
502
  Extends `UseImageMagnifyOptions` **plus**:
347
503
 
348
- | Prop | Type | Default | Description |
349
- | ---------------- | ------------------------------ | ------- | ------------------------------------- |
350
- | `src` | `string` | — | Regular image URL. |
351
- | `alt` | `string` | — | Accessible label. |
352
- | `width` | `number \| string` | — | Outer width. On mobile, forced to `100%`. |
353
- | `className` | `string` | — | Outer wrapper class. |
354
- | `style` | `CSSProperties` | — | Outer wrapper style. |
355
- | `imageClassName` | `string` | — | `<img>` class. |
356
- | `imageStyle` | `CSSProperties` | — | `<img>` style. |
357
- | `lensClassName` | `string` | — | Lens element class. |
358
- | `lensStyle` | `CSSProperties` | — | Lens element style. |
359
- | `paneClassName` | `string` | — | Side-by-side pane class. |
360
- | `paneStyle` | `CSSProperties` | — | Side-by-side pane style. |
361
- | `paneWidth` | `number \| string` | `400` | Pane width. |
362
- | `paneHeight` | `number \| string` | `400` | Pane height. |
363
- | `lazy` | `boolean` | — | Use `loading="lazy"`. |
364
- | `imageProps` | `ImgHTMLAttributes` | — | Extra props forwarded to the `<img>`. |
365
- | `onLoad` | `ImgHTMLAttributes["onLoad"]` | — | Fired on image load. |
366
- | `onError` | `ImgHTMLAttributes["onError"]` | — | Fired on image error. |
504
+ | Prop | Type | Default | Description |
505
+ | ---------------- | ------------------------------ | ------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
506
+ | `src` | `string` | — | Regular image URL. |
507
+ | `alt` | `string` | — | Accessible label. |
508
+ | `width` | `number \| string` | — | Outer width. On mobile, forced to `100%`. |
509
+ | `className` | `string` | — | Outer wrapper class. |
510
+ | `style` | `CSSProperties` | — | Outer wrapper style. |
511
+ | `imageClassName` | `string` | — | `<img>` class. |
512
+ | `imageStyle` | `CSSProperties` | — | `<img>` style. |
513
+ | `lensClassName` | `string` | — | Lens element class. |
514
+ | `lensStyle` | `CSSProperties` | — | Lens element style. |
515
+ | `paneClassName` | `string` | — | Side-by-side pane class. |
516
+ | `paneStyle` | `CSSProperties` | — | Side-by-side pane style. |
517
+ | `paneWidth` | `number \| string` | `400` | Pane width. |
518
+ | `paneHeight` | `number \| string` | `400` | Pane height. |
519
+ | `lazy` | `boolean` | — | Use `loading="lazy"` on the `<img>`. |
520
+ | `imageProps` | `ImgHTMLAttributes` | — | Extra props forwarded to the `<img>`. Excludes `src`, `alt`, `style`, `className`, `onLoad`, `onError` (use the dedicated props instead). |
521
+ | `onLoad` | `ImgHTMLAttributes["onLoad"]` | — | Fired on image load. |
522
+ | `onError` | `ImgHTMLAttributes["onError"]` | — | Fired on image error. |
367
523
 
368
524
  ### `ImageMagnifyGalleryProps`
369
525
 
370
526
  Extends `ImageMagnifyProps` **minus** `src` / `onLoad` / `onError`, plus:
371
527
 
372
- | Prop | Type | Default | Description |
373
- | ----------------------------- | ------------------------- | ------- | ------------------------------------ |
374
- | `images` | `string[]` | — | Image URLs. |
375
- | `alt` | `string` | — | Alt text (shared). |
376
- | `activeIndex` | `number` | — | Controlled index. |
377
- | `defaultIndex` | `number` | `0` | Initial index (uncontrolled). |
378
- | `onIndexChange` | `(index: number) => void` | — | Fired when the active slide changes. |
379
- | `showArrows` | `boolean` | `true` | Prev/next arrows. |
380
- | `showThumbnails` | `boolean` | `true` | Thumbnail strip. |
381
- | `showCounter` | `boolean` | `false` | "1 / N" counter overlay. |
382
- | `loop` | `boolean` | `true` | Infinite loop. |
383
- | `enableKeyboard` | `boolean` | `true` | Arrow-key navigation. |
384
- | `preloadAdjacent` | `boolean` | `true` | Preload next/prev images. |
385
- | `viewportClassName` | `string` | — | Main carousel viewport class. |
386
- | `viewportStyle` | `CSSProperties` | — | Main carousel viewport style. |
387
- | `thumbnailContainerClassName` | `string` | — | Thumbnail container class. |
388
- | `thumbnailContainerStyle` | `CSSProperties` | — | Thumbnail container style. |
389
- | `thumbnailClassName` | `string` | — | Thumbnail button class. |
390
- | `thumbnailActiveClassName` | `string` | — | Active thumbnail class. |
391
- | `thumbnailStyle` | `CSSProperties` | — | Thumbnail button style. |
392
- | `thumbnailActiveStyle` | `CSSProperties` | — | Active thumbnail style. |
393
- | `arrowClassName` | `string` | — | Arrow button class. |
394
- | `arrowStyle` | `CSSProperties` | — | Arrow button style. |
395
- | `thumbnailSize` | `number` | `80` | Thumbnail size (px). |
396
- | `thumbnailGap` | `number` | `8` | Thumbnail gap (px). |
397
- | `prevArrow` | `ReactNode` | — | Custom previous arrow. |
398
- | `nextArrow` | `ReactNode` | — | Custom next arrow. |
528
+ | Prop | Type | Default | Description |
529
+ | ----------------------------- | ------------------------- | ------- | --------------------------------------------- |
530
+ | `images` | `string[]` | — | Image URLs. |
531
+ | `alt` | `string` | — | Alt text (shared; per-slide alt is appended). |
532
+ | `activeIndex` | `number` | — | Controlled index. |
533
+ | `defaultIndex` | `number` | `0` | Initial index (uncontrolled). |
534
+ | `onIndexChange` | `(index: number) => void` | — | Fired when the active slide changes. |
535
+ | `showArrows` | `boolean` | `true` | Prev/next arrows. |
536
+ | `showThumbnails` | `boolean` | `true` | Thumbnail strip. |
537
+ | `showCounter` | `boolean` | `false` | "1 / N" counter overlay. |
538
+ | `loop` | `boolean` | `true` | Infinite loop. |
539
+ | `enableKeyboard` | `boolean` | `true` | Arrow-key navigation. |
540
+ | `preloadAdjacent` | `boolean` | `true` | Preload next/prev images (respects `loop`). |
541
+ | `viewportClassName` | `string` | — | Main carousel viewport class. |
542
+ | `viewportStyle` | `CSSProperties` | — | Main carousel viewport style. |
543
+ | `swiperStyle` | `CSSProperties` | — | Style applied to the inner Swiper element. |
544
+ | `thumbnailContainerClassName` | `string` | — | Thumbnail strip class. |
545
+ | `thumbnailContainerStyle` | `CSSProperties` | — | Thumbnail strip style. |
546
+ | `thumbnailClassName` | `string` | — | Thumbnail button class. |
547
+ | `thumbnailActiveClassName` | `string` | — | Added to the active thumbnail button. |
548
+ | `thumbnailStyle` | `CSSProperties` | — | Thumbnail button style. |
549
+ | `thumbnailActiveStyle` | `CSSProperties` | — | Merged over `thumbnailStyle` when active. |
550
+ | `arrowClassName` | `string` | — | Arrow button class. |
551
+ | `arrowStyle` | `CSSProperties` | — | Arrow button style. |
552
+ | `thumbnailSize` | `number` | `80` | Thumbnail width/height (px). |
553
+ | `thumbnailGap` | `number` | `8` | Thumbnail gap (px). |
554
+ | `prevArrow` | `ReactNode` | — | Custom previous arrow. |
555
+ | `nextArrow` | `ReactNode` | — | Custom next arrow. |
399
556
 
400
557
  ---
401
558
 
@@ -441,27 +598,38 @@ The gallery ships with inline defaults for arrows and thumbnails that you can ov
441
598
  alt="Product"
442
599
  thumbnailSize={64}
443
600
  thumbnailGap={12}
601
+ thumbnailContainerStyle={{ justifyContent: "center" }}
444
602
  thumbnailStyle={{ borderRadius: 8 }}
445
603
  thumbnailActiveStyle={{ borderColor: "#0af" }}
446
604
  arrowStyle={{ background: "#111", color: "#fff" }}
447
605
  />
448
606
  ```
449
607
 
608
+ Because the gallery does not import any CSS from `swiper/css`, only the base Swiper runtime is used (no default arrow/thumbnail styles leak in). All visuals come from inline styles + your classes.
609
+
450
610
  ---
451
611
 
452
612
  ## Accessibility
453
613
 
454
614
  - Root gallery uses `role="region"`, `aria-roledescription="carousel"`, `aria-label`.
455
- - Each image container is `role="img"`, `tabIndex={0}`, `aria-label={alt}`.
615
+ - Each image container is `role="img"` with `aria-label={alt}`. It is focusable (`tabIndex={0}`) **only when interaction is enabled** (`zoomType !== "none"` and not `disabled`).
456
616
  - Hidden `aria-live="polite"` region announces **"Image X of Y"** on slide change.
457
- - Keyboard support on the container:
458
- - **← / → / ↑ / ↓** — move the zoom pointer.
459
- - **+ / = / -** — zoom in/out (when `zoomType="wheel"`).
460
- - **Enter / Space** — toggle zoom.
461
- - **Esc** — reset zoom.
617
+ - Thumbnails are `<button>` elements with `aria-label` and `aria-current="true"` for the active one; the strip is a `role="group"` labelled "Image thumbnails".
618
+ - Arrow buttons get `aria-disabled` and `disabled` at the ends when `loop={false}`.
462
619
  - Disabled state exposes `aria-disabled`.
620
+ - Swiper's `A11y` module is enabled for the main slider.
463
621
  - Custom arrows accept any `ReactNode`; default icons are `<svg aria-hidden="true">` with `aria-label` on the buttons.
464
622
 
623
+ ### Keyboard controls
624
+
625
+ | Key | Action |
626
+ | ----------------- | ------------------------------------------------- |
627
+ | **← / → / ↑ / ↓** | Move the zoom pointer (image container). |
628
+ | **+ / = / -** | Zoom in/out (when `zoomType="wheel"`). |
629
+ | **Enter / Space** | Toggle zoom. |
630
+ | **Esc** | Reset zoom. |
631
+ | **← / →** | Navigate slides (gallery, when `enableKeyboard`). |
632
+
465
633
  ---
466
634
 
467
635
  ## TypeScript
@@ -487,10 +655,12 @@ import type {
487
655
 
488
656
  ## Package Exports
489
657
 
490
- | Import path | Contents |
491
- | ----------------------- | ---------------------------------------------------------------------------------------- |
492
- | `uix-image-magnify` | `ImageMagnify`, `ImageMagnifyGallery`, `useImageMagnify`, `useImageSlider` + all types |
493
- | `uix-image-magnify/ssr` | `ImageMagnifySSR`, `ImageMagnifyGallerySSR` (and their non-SSR re-exports) |
658
+ | Import path | Contents |
659
+ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
660
+ | `uix-image-magnify` | `ImageMagnify`, `ImageMagnifyGallery`, `useImageMagnify`, `useImageSlider` + all types |
661
+ | `uix-image-magnify/ssr` | `ImageMagnifySSR`, `ImageMagnifyGallerySSR`, plus non-SSR re-exports (`ImageMagnify`, `ImageMagnifyGallery`) and their types |
662
+
663
+ Both entry points are dual-published (ESM + CJS) with bundled TypeScript declarations.
494
664
 
495
665
  ---
496
666
 
@@ -503,7 +673,7 @@ Modern evergreen browsers (Chrome, Edge, Firefox, Safari). Requires:
503
673
  - Pointer Events
504
674
  - CSS `transform` / `background-position`
505
675
 
506
- SSR wrappers render plain `<img>` on the server for maximum compatibility.
676
+ SSR wrappers render plain `<img>` on the server for maximum compatibility. The library uses React `>=17` and has no other runtime peer dependencies besides `swiper`.
507
677
 
508
678
  ---
509
679
 
@@ -513,6 +683,13 @@ MIT © uix-image-magnify contributors
513
683
 
514
684
  ---
515
685
 
516
- ### Contributing
686
+ ## Contributing
687
+
688
+ Issues and PRs are welcome. Before submitting, please run:
689
+
690
+ ```bash
691
+ npm run typecheck
692
+ npm run build
693
+ ```
517
694
 
518
- Issues and PRs welcome. Please run `npm run typecheck` and `npm run build` before submitting.
695
+ Both must pass cleanly.
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { CSSProperties, ImgHTMLAttributes } from 'react';
2
+ import { CSSProperties, ImgHTMLAttributes, ReactNode } from 'react';
3
3
 
4
4
  type ZoomType = "hover" | "click" | "double-click" | "hold" | "wheel" | "none";
5
5
  type PanePosition = "left" | "right" | "top" | "bottom";
@@ -7,35 +7,20 @@ type LensShape = "circle" | "square";
7
7
  type MagnifyMode = "lens" | "side-by-side" | "inner" | "follow";
8
8
  type DragMode = "follow" | "pan";
9
9
  interface UseImageMagnifyOptions {
10
- /** Base image URL (required when used standalone). */
11
10
  src?: string;
12
- /** High-resolution image used for the zoomed view. Defaults to `src`. */
13
11
  zoomSrc?: string;
14
- /** Magnification factor (2 = 200%). */
15
12
  zoomScale?: number;
16
- /** Minimum scale for wheel/keyboard zoom. Default `1.5`. */
17
13
  minZoomScale?: number;
18
- /** Maximum scale for wheel/keyboard zoom. Default `5`. */
19
14
  maxZoomScale?: number;
20
- /** Interaction that activates zoom. */
21
15
  zoomType?: ZoomType;
22
- /** How the zoomed content is displayed. */
23
16
  mode?: MagnifyMode;
24
- /** Drag interaction for `inner` mode. */
25
17
  dragMode?: DragMode;
26
- /** Where the side-by-side pane sits relative to the image. */
27
18
  panePosition?: PanePosition;
28
- /** Diameter / side length of the lens in px. */
29
19
  lensSize?: number;
30
- /** Lens shape. */
31
20
  lensShape?: LensShape;
32
- /** Offset (both axes, px) for `mode="follow"`. */
33
21
  followOffset?: number;
34
- /** Disable all interaction. */
35
22
  disabled?: boolean;
36
- /** Preload the high-res image to avoid a flash. */
37
23
  zoomPreload?: boolean;
38
- /** Reset zoom when the pointer leaves the container. */
39
24
  resetOnLeave?: boolean;
40
25
  onZoomStart?: () => void;
41
26
  onZoomEnd?: () => void;
@@ -79,10 +64,42 @@ interface ImageMagnifyProps extends UseImageMagnifyOptions {
79
64
  paneWidth?: number | string;
80
65
  paneHeight?: number | string;
81
66
  lazy?: boolean;
82
- imageProps?: Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt" | "style">;
67
+ imageProps?: Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt" | "style" | "className" | "onLoad" | "onError">;
83
68
  onLoad?: ImgHTMLAttributes<HTMLImageElement>["onLoad"];
84
69
  onError?: ImgHTMLAttributes<HTMLImageElement>["onError"];
85
70
  }
86
71
  declare const ImageMagnify: React.ForwardRefExoticComponent<ImageMagnifyProps & React.RefAttributes<HTMLDivElement>>;
87
72
 
88
- export { type DragMode as D, type ImageMagnifyProps as I, type LensShape as L, type MagnifyMode as M, type PanePosition as P, type UseImageMagnifyOptions as U, type ZoomType as Z, ImageMagnify as a, useImageMagnify as u };
73
+ interface ImageMagnifyGalleryProps extends Omit<ImageMagnifyProps, "src" | "onLoad" | "onError"> {
74
+ images: string[];
75
+ alt: string;
76
+ activeIndex?: number;
77
+ defaultIndex?: number;
78
+ onIndexChange?: (index: number) => void;
79
+ showArrows?: boolean;
80
+ showThumbnails?: boolean;
81
+ showCounter?: boolean;
82
+ loop?: boolean;
83
+ enableKeyboard?: boolean;
84
+ preloadAdjacent?: boolean;
85
+ className?: string;
86
+ style?: CSSProperties;
87
+ viewportClassName?: string;
88
+ viewportStyle?: CSSProperties;
89
+ swiperStyle?: CSSProperties;
90
+ thumbnailContainerClassName?: string;
91
+ thumbnailContainerStyle?: CSSProperties;
92
+ thumbnailClassName?: string;
93
+ thumbnailActiveClassName?: string;
94
+ thumbnailStyle?: CSSProperties;
95
+ thumbnailActiveStyle?: CSSProperties;
96
+ arrowClassName?: string;
97
+ arrowStyle?: CSSProperties;
98
+ thumbnailSize?: number;
99
+ thumbnailGap?: number;
100
+ prevArrow?: ReactNode;
101
+ nextArrow?: ReactNode;
102
+ }
103
+ declare const ImageMagnifyGallery: React.ForwardRefExoticComponent<ImageMagnifyGalleryProps & React.RefAttributes<HTMLDivElement>>;
104
+
105
+ export { type DragMode as D, ImageMagnify as I, type LensShape as L, type MagnifyMode as M, type PanePosition as P, type UseImageMagnifyOptions as U, type ZoomType as Z, ImageMagnifyGallery as a, type ImageMagnifyGalleryProps as b, type ImageMagnifyProps as c, useImageMagnify as u };