@n-ivan/react-justified-gallery 1.0.2 → 2.0.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
@@ -1,12 +1,14 @@
1
1
  # react-justified-gallery
2
2
 
3
- A React component for creating beautiful justified image galleries with automatic layout calculations and responsive behavior.
3
+ A React component for creating beautiful justified image galleries with CSS flexbox layout.
4
4
 
5
5
  ## Features
6
6
 
7
7
  - **Perfect Justification** - Images fill the full container width while preserving aspect ratios
8
- - **Fully Responsive** - Automatically recalculates layout on container resize using ResizeObserver
8
+ - **Fully Responsive** - Pure CSS handles resize automatically
9
9
  - **Flexible Rendering** - Custom render prop for complete control over image display
10
+ - **SSR-Friendly** - No client-side measurement needed
11
+ - **Zero Re-renders** - No state changes on container resize
10
12
 
11
13
  ## Installation
12
14
 
@@ -17,7 +19,7 @@ npm install @n-ivan/react-justified-gallery
17
19
  ## Quick Start
18
20
 
19
21
  ```tsx
20
- import { JustifiedGallery } from 'react-justified-gallery';
22
+ import { JustifiedGallery } from '@n-ivan/react-justified-gallery';
21
23
 
22
24
  const images = [
23
25
  [
@@ -59,7 +61,6 @@ type GalleryData = ImageData[][];
59
61
  | `images` | `ImageData[][]` | **required** | 2D array of image data |
60
62
  | `gap` | `number` | `8` | Gap between images in pixels |
61
63
  | `renderImage` | `function` | - | Custom render function for images |
62
- | `resizeDebounce` | `number` | `150` | Debounce delay for resize (ms) |
63
64
  | `lazyLoad` | `boolean` | `true` | Enable native lazy loading |
64
65
  | `onImageLoad` | `function` | - | Callback when image loads |
65
66
  | `onImageError` | `function` | - | Callback when image fails |
@@ -72,30 +73,13 @@ When using the `renderImage` prop, you receive the following properties:
72
73
 
73
74
  ```typescript
74
75
  interface RenderImageProps {
75
- image: ImageData; // Original image data
76
- computedWidth: number; // Calculated width
77
- computedHeight: number; // Calculated height
78
- originalWidth: number; // Original image width
79
- originalHeight: number; // Original image height
80
- rowIndex: number; // Row index (0-based)
81
- imageIndex: number; // Image index in row (0-based)
82
- isFirstInRow: boolean; // First image in row
83
- isLastInRow: boolean; // Last image in row
76
+ image: ImageData; // Original image data
77
+ rowIndex: number; // Row index (0-based)
78
+ imageIndex: number; // Image index in row (0-based)
84
79
  }
85
80
  ```
86
81
 
87
- ## How It Works
88
-
89
- The component calculates the optimal height for each row based on:
90
-
91
- 1. **Total aspect ratio** of all images in the row
92
- 2. **Available width** (container width minus gaps)
93
- 3. **Individual aspect ratios** of each image
94
-
95
- This ensures:
96
- - All images in a row have the same height
97
- - Each image maintains its original aspect ratio
98
- - The row fills the complete container width
82
+ The wrapper div already has the correct `flex` and `aspect-ratio` styles applied, so your custom content just needs to fill `width: 100%` and `height: 100%`.
99
83
 
100
84
  ## Development
101
85
 
@@ -144,8 +128,8 @@ import type {
144
128
  ## Browser Support
145
129
 
146
130
  Modern browsers with support for:
147
- - ResizeObserver
148
- - ES2020+ features
131
+ - CSS `aspect-ratio` property (Chrome 88+, Firefox 89+, Safari 15+, Edge 88+)
132
+ - CSS Flexbox
149
133
  - React 18+
150
134
 
151
135
  ## License
@@ -1,2 +1,2 @@
1
1
  import type { JustifiedGalleryProps } from './types';
2
- export declare function JustifiedGallery({ images, gap, renderImage, resizeDebounce, lazyLoad, onImageLoad, onImageError, containerStyle, rowStyle, }: JustifiedGalleryProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function JustifiedGallery({ images, gap, renderImage, lazyLoad, onImageLoad, onImageError, containerStyle, rowStyle, }: JustifiedGalleryProps): import("react/jsx-runtime").JSX.Element;
@@ -1,171 +1,76 @@
1
- import { jsx as g } from "react/jsx-runtime";
2
- import { useRef as T, useState as C, useCallback as F, useEffect as G, useMemo as U } from "react";
3
- function k(o, t) {
4
- let e = null;
5
- const i = (...n) => {
6
- e !== null && clearTimeout(e), e = setTimeout(() => {
7
- o(...n), e = null;
8
- }, t);
9
- };
10
- return i.cancel = () => {
11
- e !== null && (clearTimeout(e), e = null);
12
- }, i;
13
- }
14
- function B(o) {
15
- const t = T(null), [e, i] = C(0), n = F(() => {
16
- if (t.current) {
17
- const l = t.current.getBoundingClientRect().width;
18
- i(l);
19
- }
20
- }, []);
21
- return G(() => {
22
- const l = t.current;
23
- if (!l) return;
24
- n();
25
- const h = k(n, o), c = new ResizeObserver(() => {
26
- h();
27
- });
28
- return c.observe(l), () => {
29
- c.disconnect(), h.cancel();
30
- };
31
- }, [n, o]), { containerRef: t, width: e };
32
- }
33
- function D(o) {
34
- for (let t = 0; t < o.length; t++) {
35
- const e = o[t];
36
- if (e.length === 0)
37
- throw new Error(`Row ${t} is empty. All rows must contain at least one image.`);
38
- for (let i = 0; i < e.length; i++) {
39
- const n = e[i];
40
- if (typeof n.width != "number" || n.width <= 0)
41
- throw new Error(
42
- `Image at row ${t}, index ${i} has invalid width. Expected positive number, got: ${n.width}`
43
- );
44
- if (typeof n.height != "number" || n.height <= 0)
45
- throw new Error(
46
- `Image at row ${t}, index ${i} has invalid height. Expected positive number, got: ${n.height}`
47
- );
48
- }
49
- }
50
- }
51
- function O(o, t, e) {
52
- return D(o), o.map((i) => {
53
- const n = e * (i.length - 1), l = t - n, h = i.reduce((a, s) => a + s.width / s.height, 0), c = l / h;
54
- return {
55
- images: i.map((a) => {
56
- const s = a.width / a.height, f = c * s;
57
- return {
58
- image: a,
59
- computedWidth: f,
60
- computedHeight: c
61
- };
62
- }),
63
- height: c
64
- };
65
- });
66
- }
67
- const _ = 8, x = 150;
68
- function P({
69
- images: o,
70
- gap: t = _,
71
- renderImage: e,
72
- resizeDebounce: i = x,
73
- lazyLoad: n = !0,
74
- onImageLoad: l,
75
- onImageError: h,
76
- containerStyle: c,
77
- rowStyle: w
1
+ import { jsx as r } from "react/jsx-runtime";
2
+ const o = 8;
3
+ function d({
4
+ images: $,
5
+ gap: y = o,
6
+ renderImage: p,
7
+ lazyLoad: f = !0,
8
+ onImageLoad: h,
9
+ onImageError: c,
10
+ containerStyle: u,
11
+ rowStyle: a
78
12
  }) {
79
- const { containerRef: a, width: s } = B(i), f = U(() => s === 0 || o.length === 0 ? [] : O(o, s, t), [o, s, t]), $ = (u) => (r) => {
80
- l == null || l(u, r);
81
- }, R = (u) => (r) => {
82
- h == null || h(u, r);
83
- }, E = (u) => {
84
- const { image: r, computedWidth: m, computedHeight: d } = u;
85
- return /* @__PURE__ */ g(
86
- "img",
87
- {
88
- src: r.src,
89
- alt: r.alt ?? "",
90
- width: m,
91
- height: d,
92
- loading: n ? "lazy" : void 0,
93
- onLoad: $(r),
94
- onError: R(r),
95
- style: {
96
- display: "block",
97
- width: m,
98
- height: d
99
- }
100
- }
101
- );
102
- };
103
- return s === 0 ? /* @__PURE__ */ g(
104
- "div",
105
- {
106
- ref: a,
107
- role: "region",
108
- "aria-label": "Image gallery",
109
- style: {
110
- width: "100%",
111
- ...c
112
- }
113
- }
114
- ) : /* @__PURE__ */ g(
13
+ return /* @__PURE__ */ r(
115
14
  "div",
116
15
  {
117
- ref: a,
118
16
  role: "region",
119
17
  "aria-label": "Image gallery",
120
18
  style: {
121
19
  width: "100%",
122
- ...c
20
+ ...u
123
21
  },
124
- children: f.map((u, r) => /* @__PURE__ */ g(
22
+ children: $.map((e, l) => /* @__PURE__ */ r(
125
23
  "div",
126
24
  {
127
25
  role: "group",
128
- "aria-label": `Gallery row ${r + 1}`,
26
+ "aria-label": `Gallery row ${l + 1}`,
129
27
  style: {
130
28
  display: "flex",
131
- gap: t,
132
- marginBottom: r < f.length - 1 ? t : 0,
133
- ...w
29
+ gap: y,
30
+ marginBottom: l < $.length - 1 ? y : 0,
31
+ ...a
134
32
  },
135
- children: u.images.map((m, d) => {
136
- const { image: p, computedWidth: b, computedHeight: y } = m, W = d === 0, A = d === u.images.length - 1, v = {
137
- image: p,
138
- computedWidth: b,
139
- computedHeight: y,
140
- originalWidth: p.width,
141
- originalHeight: p.height,
142
- rowIndex: r,
143
- imageIndex: d,
144
- isFirstInRow: W,
145
- isLastInRow: A
146
- }, H = e ? e(v) : E(v);
147
- return /* @__PURE__ */ g(
33
+ children: e.map((t, i) => {
34
+ const b = t.width / t.height;
35
+ return p ? /* @__PURE__ */ r(
148
36
  "div",
149
37
  {
150
38
  tabIndex: 0,
151
39
  role: "img",
152
- "aria-label": p.alt || `Image ${r + 1}-${d + 1}`,
40
+ "aria-label": t.alt || `Image ${l + 1}-${i + 1}`,
153
41
  style: {
154
- width: b,
155
- height: y,
156
- flexShrink: 0
42
+ flex: `${b} 1 0`,
43
+ aspectRatio: `${t.width} / ${t.height}`
157
44
  },
158
- children: H
45
+ children: p({ image: t, rowIndex: l, imageIndex: i })
46
+ },
47
+ i
48
+ ) : /* @__PURE__ */ r(
49
+ "img",
50
+ {
51
+ src: t.src,
52
+ alt: t.alt ?? "",
53
+ tabIndex: 0,
54
+ role: "img",
55
+ "aria-label": t.alt || `Image ${l + 1}-${i + 1}`,
56
+ loading: f ? "lazy" : void 0,
57
+ onLoad: (s) => h == null ? void 0 : h(t, s),
58
+ onError: (s) => c == null ? void 0 : c(t, s),
59
+ style: {
60
+ flex: `${b} 1 0`,
61
+ aspectRatio: `${t.width} / ${t.height}`,
62
+ objectFit: "cover"
63
+ }
159
64
  },
160
- `${r}-${d}`
65
+ i
161
66
  );
162
67
  })
163
68
  },
164
- r
69
+ l
165
70
  ))
166
71
  }
167
72
  );
168
73
  }
169
74
  export {
170
- P as JustifiedGallery
75
+ d as JustifiedGallery
171
76
  };
package/dist/types.d.ts CHANGED
@@ -8,32 +8,16 @@ export interface ImageData {
8
8
  }
9
9
  export interface RenderImageProps {
10
10
  image: ImageData;
11
- computedWidth: number;
12
- computedHeight: number;
13
- originalWidth: number;
14
- originalHeight: number;
15
11
  rowIndex: number;
16
12
  imageIndex: number;
17
- isFirstInRow: boolean;
18
- isLastInRow: boolean;
19
13
  }
20
14
  export interface JustifiedGalleryProps {
21
15
  images: ImageData[][];
22
16
  gap?: number;
23
17
  renderImage?: (props: RenderImageProps) => ReactNode;
24
- resizeDebounce?: number;
25
18
  lazyLoad?: boolean;
26
19
  onImageLoad?: (image: ImageData, event: SyntheticEvent<HTMLImageElement>) => void;
27
20
  onImageError?: (image: ImageData, event: SyntheticEvent<HTMLImageElement>) => void;
28
21
  containerStyle?: CSSProperties;
29
22
  rowStyle?: CSSProperties;
30
23
  }
31
- export interface ComputedImage {
32
- image: ImageData;
33
- computedWidth: number;
34
- computedHeight: number;
35
- }
36
- export interface ComputedRow {
37
- images: ComputedImage[];
38
- height: number;
39
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@n-ivan/react-justified-gallery",
3
- "version": "1.0.2",
3
+ "version": "2.0.0",
4
4
  "description": "A React component for creating justified image galleries",
5
5
  "keywords": [
6
6
  "react",
@@ -9,7 +9,7 @@
9
9
  "images",
10
10
  "layout"
11
11
  ],
12
- "homepage": "https://github.com/n-ivan/react-justified-gallery#readme",
12
+ "homepage": "https://n-ivan.github.io/react-justified-gallery/",
13
13
  "bugs": {
14
14
  "url": "https://github.com/n-ivan/react-justified-gallery/issues"
15
15
  },
@@ -33,6 +33,7 @@
33
33
  ],
34
34
  "scripts": {
35
35
  "build": "tsc --project tsconfig.build.json && vite build",
36
+ "build:demo": "vite build --config demo/vite.config.ts",
36
37
  "dev": "vite",
37
38
  "demo": "vite --config demo/vite.config.ts",
38
39
  "test": "vitest",