react-pic-gallery 2.0.2 → 2.1.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.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  Small, accessible React image gallery and lightbox with a polished default UI and typed escape hatches for custom controls.
4
4
 
5
5
  [![NPM](https://img.shields.io/npm/v/react-pic-gallery.svg)](https://www.npmjs.com/package/react-pic-gallery)
6
- [![Minified + gzip size](https://badgen.net/static/minified%20%2B%20gzip/7.7%20KB/blue)](https://bundlephobia.com/package/react-pic-gallery@2.0.2)
6
+ [![Minified + gzip size](https://badgen.net/static/minified%20%2B%20gzip/9.6%20KB/blue)](https://bundlephobia.com/package/react-pic-gallery)
7
7
 
8
8
  [Live playground and docs](https://marcelrsoub.github.io/react-pic-gallery/)
9
9
 
@@ -23,17 +23,17 @@ bun add react-pic-gallery
23
23
 
24
24
  React 18.3+ and React 19 are supported. The [Quick start docs](https://marcelrsoub.github.io/react-pic-gallery/docs/getting-started/) provide these commands in a package-manager switcher.
25
25
 
26
- The package is intentionally lightweight: it has no runtime dependencies and React is a peer dependency. The built JavaScript and CSS together are about **7.7 KB gzipped** (5.55 KB JS + 2.16 KB CSS).
26
+ The package is intentionally lightweight: it has no runtime dependencies and React is a peer dependency. The built JavaScript and CSS together are about **9.6 KB gzipped** (7.20 KB JS + 2.40 KB CSS).
27
27
 
28
28
  ## Why react-pic-gallery
29
29
 
30
- - **Tiny by design** — no runtime dependencies, no context providers, no polyfills; ~7.7 KB gzipped total.
30
+ - **Tiny by design** — no runtime dependencies, no context providers, no polyfills; ~9.6 KB gzipped total.
31
31
  - **Simple by default** — one component, one stylesheet import, sensible accessible defaults.
32
32
  - **Yours when needed** — typed `renderActions`, `renderCaption`, and `renderControls` callbacks let you add custom UI without rebuilding the lightbox; extra fields on your image objects flow through fully typed.
33
33
  - **Accessible** — native modal `<dialog>`, focus containment and restoration, Escape to close, screen-reader announcements, `prefers-reduced-motion` support.
34
34
  - **Keyboard and touch friendly** — arrow-key navigation in the grid (roving tabindex) and in the lightbox, Enter to open, swipe navigation on touch devices.
35
35
  - **Themeable** — namespaced classes and CSS variables (`--gallery-accent`, `--gallery-overlay`, `--gallery-motion`, …).
36
- - **Three layouts** — choose a uniform grid, proportional justified rows, or an editorial mosaic.
36
+ - **Four layouts** — choose a uniform grid, proportional justified rows, an editorial mosaic, or an inline carousel.
37
37
 
38
38
  ## Quick start
39
39
 
@@ -43,8 +43,12 @@ import 'react-pic-gallery/styles.css'
43
43
 
44
44
  const images = [
45
45
  {
46
- src: 'https://example.com/photo-large.jpg',
47
- thumbnailSrc: 'https://example.com/photo-thumb.jpg',
46
+ src: 'https://example.com/mountain-1600.jpg',
47
+ srcSet: 'https://example.com/mountain-960.jpg 960w, https://example.com/mountain-1600.jpg 1600w',
48
+ sizes: '(max-width: 48rem) 100vw, 80rem',
49
+ thumbnailSrc: 'https://example.com/mountain-thumb-480.jpg',
50
+ thumbnailSrcSet: 'https://example.com/mountain-thumb-320.jpg 320w, https://example.com/mountain-thumb-640.jpg 640w',
51
+ thumbnailSizes: '(max-width: 48rem) 100vw, 33vw',
48
52
  alt: 'A mountain reflected in a lake',
49
53
  caption: 'Morning at the lake'
50
54
  }
@@ -55,7 +59,7 @@ export function App() {
55
59
  }
56
60
  ```
57
61
 
58
- `src` and `alt` are required. `id`, `thumbnailSrc`, `caption`, `width`, and `height` are optional. Image objects can include application-specific fields; those fields remain available in renderer callbacks when using TypeScript generics.
62
+ `src` and `alt` are required. `srcSet` and `sizes` describe responsive full-size sources; `thumbnailSrc`, `thumbnailSrcSet`, and `thumbnailSizes` optionally provide a separate responsive thumbnail family. If neither `thumbnailSrc` nor `thumbnailSrcSet` is provided, thumbnails use the full-size source family. `id`, `caption`, `width`, and `height` are optional. Image objects can include application-specific fields; those fields remain available in renderer callbacks when using TypeScript generics.
59
63
 
60
64
  ## Gallery layouts
61
65
 
@@ -64,13 +68,17 @@ The default is the existing three-column grid. Choose another layout with `layou
64
68
  ```tsx
65
69
  <PicGallery images={images} layout='justified' />
66
70
  <PicGallery images={images} layout='mosaic' />
71
+ <PicGallery images={images} layout='carousel' />
67
72
  ```
68
73
 
69
74
  - **`grid`** — consistent tiles with a configurable fixed column count.
70
75
  - **`justified`** — proportional photos arranged in aligned rows; the final row stays left-aligned.
71
76
  - **`mosaic`** — alternating featured photos and smaller supporting tiles.
77
+ - **`carousel`** — one image at a time with inline previous/next controls.
72
78
 
73
- Image `width` and `height` are recommended, but optional. Justified rows use them when supplied and fall back to 3:2 when either dimension is missing or invalid. Accurate dimensions give the most faithful layout and help reserve space while images load. `rowHeight` sets the tile height for the grid, the base row height for the mosaic, and the target row height for justified galleries. `columns` applies to the grid only.
79
+ The default appearance is `framed`; use `appearance='bare'` to remove the outer surface while retaining tile spacing and rounded corners. Image `width` and `height` are recommended, but optional. Justified rows use them when supplied and fall back to 3:2 when either dimension is missing or invalid. Accurate dimensions give the most faithful layout and help reserve space while images load. `rowHeight` sets the tile height for the grid, the base row height for the mosaic, and the target row height for justified galleries. `columns` applies to the grid only and defaults to three.
80
+
81
+ Responsive image selection uses native `srcSet` and `sizes`. A dedicated thumbnail family is selected when either `thumbnailSrc` or `thumbnailSrcSet` is set; otherwise thumbnails use `src`, `srcSet`, and `sizes`. For example, `srcSet` alone works as a responsive source for both thumbnails and the lightbox. If `thumbnailSrcSet` is supplied without `thumbnailSrc`, `src` is used as its fallback URL.
74
82
 
75
83
  ## Custom UI
76
84
 
@@ -131,6 +139,7 @@ The lightbox uses the native modal `<dialog>` element and includes:
131
139
  - Keyboard navigation, Escape-to-close, focus containment, and focus restoration.
132
140
  - Backdrop closing with stable page width while scrolling is locked.
133
141
  - Native lazy loading for thumbnails and explicit image loading/error states.
142
+ - Adjacent full-size images are preloaded while the viewer is active, using responsive sources and low fetch priority.
134
143
  - Horizontal swipe navigation on touch devices.
135
144
  - Instagram-Stories-style edge taps on touch screens: tap the right edge of the image to go forward, the left edge to go back (navigation buttons are hidden on small screens where tap zones take over).
136
145
  - Reduced-motion support, safe-area padding, rounded image surfaces, and animated transitions.
@@ -139,21 +148,34 @@ Gallery tiles use roving focus. Left/Right follow image order; in the grid, Up/D
139
148
 
140
149
  Native modal behavior targets modern browsers: Chrome 37+, Edge 79+, Firefox 98+, and Safari/iOS 15.4+. The package does not ship a dialog polyfill.
141
150
 
151
+ Adjacent preloading is enabled by default and considers only the immediate previous and next full-size images while the lightbox is open or the inline carousel is mounted. The shared tracker is capped at 32 entries. Set `preloadAdjacent={false}` to opt out. It uses the browser image cache and does not guarantee reuse; cache headers, browser policy, and network conditions still apply.
152
+
142
153
  ## Styling
143
154
 
144
- The stylesheet uses namespaced classes and CSS variables. Import it once, then override variables globally or on the lightbox class:
155
+ The stylesheet uses namespaced classes and CSS variables. Import it once. Set gallery variables on the `PicGallery` wrapper (for example, through its `className`) so the layout container inherits them; the lightbox is portaled, so lightbox colors should be set globally or on `.react-pic-gallery__lightbox`.
145
156
 
146
157
  ```css
147
- :root,
148
- .react-pic-gallery__lightbox {
158
+ .brand-gallery {
159
+ --gallery-background: #111a24;
160
+ --gallery-padding: 0.65rem;
161
+ --gallery-border: 1px solid rgba(148, 163, 184, 0.18);
162
+ --gallery-frame-radius: 1.1rem;
149
163
  --gallery-accent: #ff7a59;
164
+ }
165
+
166
+ .react-pic-gallery__lightbox {
150
167
  --gallery-overlay: rgba(10, 10, 14, 0.98);
151
168
  --gallery-control-size: 3rem;
152
169
  --gallery-motion: 240ms;
153
170
  }
154
171
  ```
155
172
 
156
- The default grid uses three columns. Pass `columns` for a different fixed count. `rowHeight` accepts CSS height values or numeric pixels; its meaning depends on the selected layout as described above.
173
+ ```tsx
174
+ <PicGallery className='brand-gallery' images={images} />
175
+ <PicGallery className='brand-gallery' images={images} appearance='bare' />
176
+ ```
177
+
178
+ The default frame variables are `--gallery-background`, `--gallery-padding`, `--gallery-border`, and `--gallery-frame-radius`. The default grid uses three columns. Pass `columns` for a different fixed count. `rowHeight` accepts CSS height values or numeric pixels; its meaning depends on the selected layout as described above.
157
179
 
158
180
  ## API
159
181
 
@@ -162,23 +184,40 @@ The default grid uses three columns. Pass `columns` for a different fixed count.
162
184
  | Prop | Type | Description |
163
185
  | --- | --- | --- |
164
186
  | `images` | `readonly GalleryImage[]` | Images to display. |
165
- | `layout` | `'grid' \| 'justified' \| 'mosaic'` | Defaults to `'grid'`. |
187
+ | `layout` | `'grid' \| 'justified' \| 'mosaic' \| 'carousel'` | Defaults to `'grid'`. |
188
+ | `appearance` | `'framed' \| 'bare'` | Defaults to `'framed'`. |
166
189
  | `columns` | `number` | Optional fixed grid column count. Defaults to three; only applies to `grid`. |
167
190
  | `rowHeight` | `CSSProperties['height']` | Grid tile height, mosaic base row height, or justified target row height. |
191
+ | `preloadAdjacent` | `boolean` | Defaults to `true`. Preloads adjacent full-size images while the lightbox is open, and for the inline carousel while it is mounted. |
168
192
  | `renderActions` | `LightboxRenderer` | Adds controls to the default toolbar. |
169
193
  | `renderCaption` | `LightboxRenderer` | Replaces the current caption. |
170
194
  | `renderControls` | `LightboxRenderer` | Replaces the complete default control layer. |
171
195
  | `showCounter` | `boolean` | Shows the current image count. Defaults to `true`. |
172
196
  | `showNavigation` | `boolean` | Shows previous/next controls. Defaults to `true`. |
173
197
 
174
- `Gallery` accepts `images`, `onImageClick`, `layout`, `columns`, `rowHeight`, `className`, and `style`.
198
+ `Gallery` accepts `images`, `onImageClick`, `layout`, `appearance`, `columns`, `rowHeight`, `preloadAdjacent`, `className`, and `style`. Its appearance defaults to `framed`; adjacent-image preloading applies only when `layout='carousel'`.
199
+
200
+ `Lightbox` accepts `images`, controlled `index`, `onIndexChange`, the renderer props, `showCounter`, `showNavigation`, `preloadAdjacent`, and `className`.
201
+
202
+ `PicGallery` accepts `appearance` (`'framed' | 'bare'`, default `'framed'`) and `preloadAdjacent` in addition to the props above. Adjacent preloading is enabled by default while the lightbox is open or the inline carousel is mounted. It preloads only the immediate previous and next full-size responsive sources, uses low fetch priority, and tracks at most 32 preload entries. Set `preloadAdjacent={false}` to opt out. Browser caching and network policy determine whether a prefetched response is reused.
203
+
204
+ ### `GalleryImage`
175
205
 
176
- `Lightbox` accepts `images`, controlled `index`, `onIndexChange`, the renderer props, `showCounter`, `showNavigation`, and `className`.
206
+ | Field | Type | Description |
207
+ | --- | --- | --- |
208
+ | `src` | `string` | Required full-size fallback source. |
209
+ | `srcSet` | `string` | Optional responsive full-size candidates, passed to the browser natively. |
210
+ | `sizes` | `string` | Optional rendered-size hint for `srcSet`. |
211
+ | `thumbnailSrc` | `string` | Optional dedicated thumbnail source. |
212
+ | `thumbnailSrcSet` | `string` | Optional responsive candidates for a dedicated thumbnail family. |
213
+ | `thumbnailSizes` | `string` | Optional rendered-size hint for `thumbnailSrcSet`. |
177
214
 
178
215
  ## Migrating from v1
179
216
 
180
217
  v2 intentionally removes the old configuration and external lightbox workaround.
181
218
 
219
+ `Gallery` and `PicGallery` now use the built-in framed appearance by default. If an existing card or `.gallery-frame` wrapper adds its own padding, background, border, or rounded surface, remove those duplicate surface styles and keep only layout or margin rules. Set `appearance='bare'` when you want the previous unframed look; tile spacing and tile corner radii remain in place.
220
+
182
221
  | v1 | v2 |
183
222
  | --- | --- |
184
223
  | `imgList` | `images` |
@@ -222,6 +261,7 @@ npm install
222
261
  npm run dev
223
262
  npm run dev:library
224
263
  npm test
264
+ npm run test:visual
225
265
  npm run typecheck
226
266
  npm run build
227
267
  npm run build:docs
@@ -230,6 +270,22 @@ npm pack --dry-run
230
270
 
231
271
  `npm run dev` starts the Astro playground and docs site. `npm run dev:library` starts the standalone Vite library demo. Publishing runs the package build automatically through `prepack`.
232
272
 
273
+ ### Visual regression tests
274
+
275
+ `npm run test:visual` renders a deterministic fixture (`visual/`) across every layout, the standalone components, and the lightbox at desktop and mobile widths, then compares screenshots and layout metrics against the committed baselines in `visual/baselines/`. `npm run test:visual:update` regenerates those baselines after an intentional visual change.
276
+
277
+ The harness drives a real Chromium over the DevTools protocol, so a browser must be reachable. It defaults to `ws://127.0.0.1:9222/`; override with `VISUAL_CDP`. In this repository's environment, run the browserless Chromium container and set `VISUAL_BASE_HOST` to the host address the container can reach (the container's default gateway, e.g. `192.168.16.1`):
278
+
279
+ ```bash
280
+ VISUAL_BASE_HOST=192.168.16.1 npm run test:visual
281
+ ```
282
+
283
+ `npm run test:visual:cross` renders the current working tree and a prior git ref with the `bare` appearance, which matches the pre-`framed` default, and diffs them to confirm rendering did not change. It accepts `--ref <ref>` (default `HEAD`) and `--tolerance <ratio>` (default `0.0005`, i.e. 0.05% of pixels) for sub-pixel text antialiasing:
284
+
285
+ ```bash
286
+ VISUAL_BASE_HOST=192.168.16.1 npm run test:visual:cross -- --ref HEAD
287
+ ```
288
+
233
289
  ## License
234
290
 
235
291
  MIT © [marcelrsoub](https://github.com/marcelrsoub)
@@ -0,0 +1,19 @@
1
+ import { ReactNode } from 'react';
2
+ import { GalleryImage, LightboxRenderer } from './types';
3
+ import { UseCarouselNavigationResult } from './useCarouselNavigation';
4
+ export type CarouselProps<T extends GalleryImage = GalleryImage> = {
5
+ images: readonly T[];
6
+ index: number;
7
+ navigation: UseCarouselNavigationResult;
8
+ renderActions?: LightboxRenderer<T>;
9
+ renderCaption?: LightboxRenderer<T>;
10
+ renderControls?: LightboxRenderer<T>;
11
+ showCounter?: boolean;
12
+ showNavigation?: boolean;
13
+ preloadAdjacent?: boolean;
14
+ close?: () => void;
15
+ onEmptyAreaClick?: () => void;
16
+ onImageClick?: () => void;
17
+ className?: string;
18
+ };
19
+ export declare function Carousel<T extends GalleryImage>(props: CarouselProps<T>): ReactNode;
package/dist/Gallery.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import { GalleryImage, GalleryProps } from './types';
2
- declare function GalleryComponent<T extends GalleryImage>({ images, onImageClick, layout, columns, rowHeight, className, style }: GalleryProps<T>): import('react').JSX.Element;
2
+ declare function GalleryComponent<T extends GalleryImage>({ images, onImageClick, layout, appearance, columns, rowHeight, className, style, renderCaption, showCounter, showNavigation, preloadAdjacent }: GalleryProps<T>): import('react').JSX.Element;
3
3
  export declare const Gallery: typeof GalleryComponent;
4
4
  export {};
package/dist/Image.d.ts CHANGED
@@ -1,16 +1,19 @@
1
- import { AnimationEventHandler, CSSProperties } from 'react';
1
+ import { AnimationEventHandler, CSSProperties, MouseEventHandler } from 'react';
2
2
  type ImageProps = {
3
3
  src: string;
4
+ srcSet?: string;
5
+ sizes?: string;
4
6
  alt: string;
5
7
  className?: string;
6
8
  imageClassName?: string;
7
9
  style?: CSSProperties;
8
10
  imageStyle?: CSSProperties;
9
11
  onAnimationEnd?: AnimationEventHandler<HTMLSpanElement>;
12
+ onClick?: MouseEventHandler<HTMLSpanElement>;
10
13
  eager?: boolean;
11
14
  objectFit?: 'cover' | 'contain';
12
15
  width?: number;
13
16
  height?: number;
14
17
  };
15
- export declare function Image({ src, alt, className, imageClassName, style, imageStyle, onAnimationEnd, eager, objectFit, width, height }: ImageProps): import('react').JSX.Element;
18
+ export declare function Image({ src, srcSet, sizes, alt, className, imageClassName, style, imageStyle, onAnimationEnd, onClick, eager, objectFit, width, height }: ImageProps): import('react').JSX.Element;
16
19
  export {};
@@ -1,2 +1,2 @@
1
1
  import { GalleryImage, LightboxProps } from './types';
2
- export declare function Lightbox<T extends GalleryImage>({ images, index, onIndexChange, renderActions, renderCaption, renderControls, showCounter, showNavigation, className }: LightboxProps<T>): import('react').ReactPortal | null;
2
+ export declare function Lightbox<T extends GalleryImage>({ images, index, onIndexChange, renderActions, renderCaption, renderControls, showCounter, showNavigation, preloadAdjacent, className }: LightboxProps<T>): import('react').ReactPortal | null;
@@ -1,2 +1,2 @@
1
1
  import { GalleryImage, PicGalleryProps } from './types';
2
- export declare function PicGallery<T extends GalleryImage>({ images, layout, columns, rowHeight, className, galleryClassName, style, renderActions, renderCaption, renderControls, showCounter, showNavigation }: PicGalleryProps<T>): import('react').JSX.Element;
2
+ export declare function PicGallery<T extends GalleryImage>({ images, layout, appearance, columns, rowHeight, className, galleryClassName, style, renderActions, renderCaption, renderControls, showCounter, showNavigation, preloadAdjacent }: PicGalleryProps<T>): import('react').JSX.Element;
@@ -0,0 +1,9 @@
1
+ import { GalleryImage } from './types';
2
+ export type ImageSource = {
3
+ src: string;
4
+ srcSet?: string;
5
+ sizes?: string;
6
+ };
7
+ export declare function getFullImageSource(image: GalleryImage): ImageSource;
8
+ export declare function getThumbnailImageSource(image: GalleryImage): ImageSource;
9
+ export declare function getImageSourceKey(source: ImageSource): string;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { Gallery } from './Gallery';
2
2
  export { Lightbox } from './Lightbox';
3
3
  export { PicGallery } from './PicGallery';
4
- export type { GalleryImage, GalleryLayout, GalleryProps, LightboxContext, LightboxProps, LightboxRenderer, PicGalleryProps } from './types';
4
+ export type { GalleryImage, GalleryAppearance, GalleryLayout, GalleryProps, LightboxContext, LightboxProps, LightboxRenderer, PicGalleryProps } from './types';
5
5
  export { PicGallery as default } from './PicGallery';