react-motion-gallery 2.0.19 → 2.0.20
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/LICENSE.md +13 -3
- package/README.md +677 -286
- package/THIRD_PARTY_NOTICES.md +31 -0
- package/dist/FullscreenRuntime-GNH22QHC.mjs +4 -0
- package/dist/FullscreenRuntime-QX6YELBN.css +1 -0
- package/dist/GridSkeleton-Dn7N-TEh.d.mts +23 -0
- package/dist/MasonrySkeleton-ftJdtDLs.d.mts +29 -0
- package/dist/chunk-3YENO5LQ.mjs +1 -0
- package/dist/{chunk-NABNX5HB.mjs → chunk-4NT4UVB5.mjs} +1 -1
- package/dist/chunk-4VHNCVVB.mjs +0 -0
- package/dist/chunk-6CJ7JFOA.mjs +1 -0
- package/dist/chunk-6FFRWH2C.mjs +1 -0
- package/dist/chunk-6ZNRPJC2.mjs +6 -0
- package/dist/chunk-ADIHG7AT.mjs +1 -0
- package/dist/chunk-ASH5AOA4.mjs +1 -0
- package/dist/chunk-AX2FSVFD.mjs +2 -0
- package/dist/chunk-B4CC5AGE.mjs +1 -0
- package/dist/chunk-BJBHSWMF.mjs +1 -0
- package/dist/chunk-C6PKH3FH.mjs +1 -0
- package/dist/chunk-CR2MZG3Q.mjs +1 -0
- package/dist/chunk-D3T6HIS2.mjs +1 -0
- package/dist/chunk-DCUCXQHE.mjs +3 -0
- package/dist/chunk-DCY4ZVYI.mjs +1 -0
- package/dist/chunk-EFXHC36P.mjs +0 -0
- package/dist/chunk-GUNIA4DZ.mjs +1 -0
- package/dist/chunk-H6XFG3CJ.mjs +1 -0
- package/dist/chunk-HGY3QLCE.mjs +1 -0
- package/dist/chunk-HK2DPKES.mjs +1 -0
- package/dist/chunk-IT7HWE4G.mjs +1 -0
- package/dist/chunk-J2ZX5I7E.mjs +5 -0
- package/dist/chunk-JJMFOLJZ.mjs +1 -0
- package/dist/chunk-L2HRIINV.mjs +1 -0
- package/dist/chunk-L4TRPKGX.mjs +4 -0
- package/dist/chunk-NHIKOJLU.mjs +1 -0
- package/dist/chunk-NQI246HG.mjs +1 -0
- package/dist/chunk-OHD2HQP7.mjs +4 -0
- package/dist/chunk-OWKZOHPK.mjs +3 -0
- package/dist/chunk-PFEGIWQJ.mjs +1 -0
- package/dist/chunk-Q5WUKZ2J.mjs +4 -0
- package/dist/chunk-R6EGYRTJ.mjs +6 -0
- package/dist/chunk-RLT5FULN.mjs +0 -0
- package/dist/chunk-RNLUNA5L.mjs +2 -0
- package/dist/chunk-STRS7UNJ.mjs +1 -0
- package/dist/chunk-TIQVSK5S.mjs +1 -0
- package/dist/chunk-UML6FCOQ.mjs +1 -0
- package/dist/chunk-UP6P6CQS.mjs +2 -0
- package/dist/chunk-UUAWLGWO.mjs +1 -0
- package/dist/chunk-V25YIPLC.mjs +1 -0
- package/dist/chunk-V7DPXRZF.mjs +1 -0
- package/dist/chunk-VEXMXZJM.mjs +4 -0
- package/dist/chunk-VGXO2IAF.mjs +1 -0
- package/dist/chunk-VWEQRZ24.mjs +1 -0
- package/dist/chunk-VXMW2JT5.mjs +1 -0
- package/dist/chunk-WGVWASZM.mjs +1 -0
- package/dist/chunk-WMG2LTLR.mjs +1 -0
- package/dist/chunk-X4HEGEZV.mjs +1 -0
- package/dist/chunk-XUQO5F2F.mjs +1 -0
- package/dist/chunk-Y7NUGXTR.mjs +1 -0
- package/dist/chunk-Z34PSRMG.mjs +1 -0
- package/dist/chunk-ZCCYTID7.mjs +1 -0
- package/dist/chunk-ZOFTC6YV.mjs +1 -0
- package/dist/core.d.mts +22 -22
- package/dist/core.mjs +1 -1
- package/dist/entries.css +1 -1
- package/dist/entries.d.mts +50 -17
- package/dist/entries.mjs +1 -1
- package/dist/force-C5m1QpdF.d.mts +7 -0
- package/dist/fullscreen-captions.css +1 -0
- package/dist/fullscreen-captions.d.mts +19 -0
- package/dist/fullscreen-captions.mjs +1 -0
- package/dist/fullscreen-controls.d.mts +19 -0
- package/dist/fullscreen-controls.mjs +1 -0
- package/dist/fullscreen-crossfade.d.mts +19 -0
- package/dist/fullscreen-crossfade.mjs +1 -0
- package/dist/fullscreen-lazy-load.css +1 -0
- package/dist/fullscreen-lazy-load.d.mts +19 -0
- package/dist/fullscreen-lazy-load.mjs +1 -0
- package/dist/fullscreen-slider.css +1 -0
- package/dist/fullscreen-slider.d.mts +19 -0
- package/dist/fullscreen-slider.mjs +1 -0
- package/dist/fullscreen-thumbnails.d.mts +19 -0
- package/dist/fullscreen-thumbnails.mjs +1 -0
- package/dist/fullscreen-video.css +1 -0
- package/dist/fullscreen-video.d.mts +19 -0
- package/dist/fullscreen-video.mjs +1 -0
- package/dist/fullscreen-zoom-pan.d.mts +19 -0
- package/dist/fullscreen-zoom-pan.mjs +1 -0
- package/dist/fullscreen.css +1 -1
- package/dist/fullscreen.d.mts +27 -67
- package/dist/fullscreen.mjs +1 -1
- package/dist/fullscreenThumbnails.css +1 -1
- package/dist/fullscreenThumbnails.d.mts +8 -8
- package/dist/fullscreenThumbnails.mjs +1 -1
- package/dist/grid-ready.d.mts +13 -0
- package/dist/grid-ready.mjs +1 -0
- package/dist/grid.css +1 -1
- package/dist/grid.d.mts +9 -29
- package/dist/grid.mjs +1 -1
- package/dist/index-Cp40fdvU.d.mts +21 -0
- package/dist/{index-CwwxTQKa.d.mts → index-YphJztdR.d.mts} +5 -6
- package/dist/index.css +1 -1
- package/dist/index.d.mts +26 -28
- package/dist/index.mjs +1 -1
- package/dist/layout-DoYnPD0I.d.mts +137 -0
- package/dist/lazy-dGoYpcRa.d.mts +14 -0
- package/dist/masonry-ready.d.mts +13 -0
- package/dist/masonry-ready.mjs +1 -0
- package/dist/masonry.css +1 -1
- package/dist/masonry.d.mts +15 -13
- package/dist/masonry.mjs +1 -1
- package/dist/{plyrTypes-Cq4C3ul5.d.mts → media.d.mts} +1 -8
- package/dist/media.mjs +1 -0
- package/dist/metafile-esm.json +1 -1
- package/dist/plyrTypes-DhzgHNfX.d.mts +9 -0
- package/dist/responsive-Ch5b4LC-.d.mts +526 -0
- package/dist/responsive.d.mts +15 -0
- package/dist/responsive.mjs +1 -0
- package/dist/responsiveNumber-CouEMJ9O.d.mts +5 -0
- package/dist/skeleton-base.css +1 -0
- package/dist/skeleton-base.d.mts +52 -0
- package/dist/skeleton-base.mjs +1 -0
- package/dist/skeleton-grid.css +1 -0
- package/dist/skeleton-grid.d.mts +64 -0
- package/dist/skeleton-grid.mjs +1 -0
- package/dist/skeleton-masonry.css +1 -0
- package/dist/skeleton-masonry.d.mts +63 -0
- package/dist/skeleton-masonry.mjs +1 -0
- package/dist/skeleton-slider.css +1 -0
- package/dist/skeleton-slider.d.mts +208 -0
- package/dist/skeleton-slider.mjs +40 -0
- package/dist/slider-arrows.d.mts +9 -0
- package/dist/slider-arrows.mjs +1 -0
- package/dist/slider-auto-height.d.mts +9 -0
- package/dist/slider-auto-height.mjs +1 -0
- package/dist/slider-auto-play.d.mts +9 -0
- package/dist/slider-auto-play.mjs +1 -0
- package/dist/slider-auto-scroll.d.mts +9 -0
- package/dist/slider-auto-scroll.mjs +1 -0
- package/dist/slider-crossfade.d.mts +9 -0
- package/dist/slider-crossfade.mjs +1 -0
- package/dist/slider-dots.css +1 -0
- package/dist/slider-dots.d.mts +9 -0
- package/dist/slider-dots.mjs +1 -0
- package/dist/slider-fade.d.mts +9 -0
- package/dist/slider-fade.mjs +1 -0
- package/dist/slider-fullscreen.d.mts +9 -0
- package/dist/slider-fullscreen.mjs +1 -0
- package/dist/slider-lazy-load.css +1 -0
- package/dist/slider-lazy-load.d.mts +9 -0
- package/dist/slider-lazy-load.mjs +1 -0
- package/dist/slider-loading.css +1 -0
- package/dist/slider-loading.d.mts +9 -0
- package/dist/slider-loading.mjs +1 -0
- package/dist/slider-parallax.d.mts +9 -0
- package/dist/slider-parallax.mjs +1 -0
- package/dist/slider-progress.d.mts +9 -0
- package/dist/slider-progress.mjs +1 -0
- package/dist/slider-ready.d.mts +14 -0
- package/dist/slider-ready.mjs +1 -0
- package/dist/slider-ripple.d.mts +9 -0
- package/dist/slider-ripple.mjs +1 -0
- package/dist/slider-scale.d.mts +9 -0
- package/dist/slider-scale.mjs +1 -0
- package/dist/slider-scrollbar.css +1 -0
- package/dist/slider-scrollbar.d.mts +9 -0
- package/dist/slider-scrollbar.mjs +1 -0
- package/dist/slider.css +1 -1
- package/dist/slider.d.mts +7 -81
- package/dist/slider.mjs +1 -1
- package/dist/text-BBcRGVzn.d.mts +10 -0
- package/dist/thumbnails.css +1 -1
- package/dist/thumbnails.d.mts +8 -11
- package/dist/thumbnails.mjs +1 -1
- package/dist/transitions-DU3ftmIq.d.mts +6 -0
- package/dist/{types-ROPjU8Nl.d.mts → types-B7AiQJkM.d.mts} +3 -2
- package/dist/types-BlFwyRVQ.d.mts +43 -0
- package/dist/types-CfvTYIyd.d.mts +450 -0
- package/dist/{types-CHUayqcj.d.mts → types-DP7ogmr4.d.mts} +5 -3
- package/dist/types-DWzjXjYR.d.mts +48 -0
- package/dist/video.css +1 -1
- package/dist/video.d.mts +2 -1
- package/dist/video.mjs +1 -1
- package/dist/zoomPan.d.mts +2 -4
- package/dist/zoomPan.mjs +1 -1
- package/package.json +135 -6
- package/dist/chunk-5HIHJGIV.mjs +0 -45
- package/dist/chunk-6TPHLAUP.mjs +0 -1
- package/dist/chunk-BIDZ4WZB.mjs +0 -2
- package/dist/chunk-DBIFLX6Y.mjs +0 -6
- package/dist/chunk-ECQ74X24.mjs +0 -1
- package/dist/chunk-FJYYM5TH.mjs +0 -1
- package/dist/chunk-GSEIEFRW.mjs +0 -1
- package/dist/chunk-GT6IL37J.mjs +0 -1
- package/dist/chunk-J4E4PKE5.mjs +0 -1
- package/dist/chunk-JD3VAF3N.mjs +0 -4
- package/dist/chunk-JMFDRKTX.mjs +0 -2
- package/dist/chunk-K6PQU6HF.mjs +0 -1
- package/dist/chunk-KSOQWCCL.mjs +0 -6
- package/dist/chunk-NEJ27O2B.mjs +0 -2
- package/dist/chunk-Q2PY6ZMU.mjs +0 -2
- package/dist/chunk-TKPLWDPW.mjs +0 -7
- package/dist/chunk-UV2SUN5V.mjs +0 -1
- package/dist/chunk-VXSRNAH4.mjs +0 -1
- package/dist/chunk-WLWVKQPL.mjs +0 -4
- package/dist/chunk-WZWMG4ZT.mjs +0 -1
- package/dist/chunk-XOS5AXSR.mjs +0 -4
- package/dist/chunk-ZX5E327W.mjs +0 -1
- package/dist/controls-SpWg1Kgt.d.mts +0 -44
- package/dist/layout-CR6f2aPH.d.mts +0 -95
- package/dist/responsive-D_xhZmVI.d.mts +0 -186
- package/dist/sliderSub-Bo6Y8as_.d.mts +0 -45
- package/dist/text-Cl2tR8oO.d.mts +0 -4
- package/dist/types-DY058l5M.d.mts +0 -301
- package/dist/types-VULXzSa2.d.mts +0 -68
- package/dist/types-XEr8LRal.d.mts +0 -65
- package/dist/types-_1D0QtfD.d.mts +0 -174
package/README.md
CHANGED
|
@@ -1,24 +1,54 @@
|
|
|
1
1
|
# React Motion Gallery
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Composable React media gallery primitives for production interfaces: sliders, grids, masonry, structured entries, fullscreen, thumbnails, video, zoom/pan, and loading states that are designed around the layout they protect.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The package stays close to React composition. `Slider`, `Grid`, and `Masonry` render children directly; `Entries` renders structured data; `GalleryCore` coordinates fullscreen state; `Video` handles Plyr-backed media; `ZoomPanImage` gives you a standalone zoom surface; and `Skeleton` can be used inside or outside gallery layouts. For loading-state precision, the repo also includes a development-time browser measurement workflow that turns real rendered text into stable skeleton text authoring data, including reflow-sensitive layouts such as masonry.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Runtime Gzip Sizes
|
|
8
|
+
|
|
9
|
+
This table reports local gzip measurements for selected runtime surfaces. Type-only imports are erased and add no JS; the responsive row measures `BREAKPOINT_MAP`, and feature subpath rows measure only that feature entry point. The script rebundles one export at a time from its published ESM entry point, excludes peer and runtime externals, and gzips the resulting JS bundle. If a surface creates async chunks, the row reports initial plus async JS. Run `npm run build && npm run size:readme` in `packages/react-motion-gallery` to refresh it.
|
|
8
10
|
|
|
9
11
|
<!-- bundle-size:start -->
|
|
10
|
-
|
|
|
12
|
+
| Surface | JS gzip |
|
|
11
13
|
| --- | --- |
|
|
12
|
-
| `Entries` |
|
|
13
|
-
| `FullscreenThumbnailSlider` |
|
|
14
|
-
| `GalleryCore` | 2.
|
|
15
|
-
| `Grid` |
|
|
16
|
-
| `
|
|
17
|
-
| `
|
|
18
|
-
| `
|
|
19
|
-
| `
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
14
|
+
| `Entries` | 13.0kB |
|
|
15
|
+
| `FullscreenThumbnailSlider` | 20.3kB |
|
|
16
|
+
| `GalleryCore` | 2.6kB |
|
|
17
|
+
| `Grid` | 8.8kB |
|
|
18
|
+
| `grid/ready` | 323.0B |
|
|
19
|
+
| `Masonry` | 8.9kB |
|
|
20
|
+
| `masonry/ready` | 323.0B |
|
|
21
|
+
| `Skeleton base` | 8.1kB |
|
|
22
|
+
| `skeleton/slider` | 16.5kB |
|
|
23
|
+
| `skeleton/grid` | 10.4kB |
|
|
24
|
+
| `skeleton/masonry` | 17.8kB |
|
|
25
|
+
| `Slider core` | 18.4kB |
|
|
26
|
+
| `slider/ready` | 894.0B |
|
|
27
|
+
| `slider/arrows` | 1.2kB |
|
|
28
|
+
| `slider/dots` | 932.0B |
|
|
29
|
+
| `slider/progress` | 892.0B |
|
|
30
|
+
| `slider/scrollbar` | 1.2kB |
|
|
31
|
+
| `slider/auto-height` | 1.3kB |
|
|
32
|
+
| `slider/lazy-load` | 3.9kB |
|
|
33
|
+
| `slider/parallax` | 1.4kB |
|
|
34
|
+
| `slider/scale` | 1.2kB |
|
|
35
|
+
| `slider/fade` | 1.2kB |
|
|
36
|
+
| `slider/crossfade` | 2.8kB |
|
|
37
|
+
| `slider/fullscreen` | 959.0B |
|
|
38
|
+
| `ThumbnailSlider` | 18.9kB |
|
|
39
|
+
| `useFullscreenController` | 4.9kB |
|
|
40
|
+
| `fullscreen/slider` | 35.3kB |
|
|
41
|
+
| `fullscreen/controls` | 173.0B |
|
|
42
|
+
| `fullscreen/captions` | 13.1kB |
|
|
43
|
+
| `fullscreen/zoom-pan` | 9.9kB |
|
|
44
|
+
| `fullscreen/video` | 16.3kB |
|
|
45
|
+
| `fullscreen/lazy-load` | 13.1kB |
|
|
46
|
+
| `fullscreen/crossfade` | 181.0B |
|
|
47
|
+
| `fullscreen/thumbnails` | 160.0B |
|
|
48
|
+
| `Video` | 12.7kB |
|
|
49
|
+
| `ZoomPanImage` | 8.7kB |
|
|
50
|
+
| `media / toMediaItems` | 260.0B |
|
|
51
|
+
| `responsive / BREAKPOINT_MAP` | 85.0B |
|
|
22
52
|
<!-- bundle-size:end -->
|
|
23
53
|
|
|
24
54
|
## Overview
|
|
@@ -43,6 +73,7 @@ Mental model:
|
|
|
43
73
|
- `GalleryCore` and `useFullscreenController` power fullscreen behavior.
|
|
44
74
|
- `Video` is the gallery-ready video primitive.
|
|
45
75
|
- `ZoomPanImage` attaches click-to-zoom, drag pan, ctrl-wheel pinch, and touch pinch to one clipped image surface.
|
|
76
|
+
- `Skeleton` renders standalone placeholders or wraps real content with shared loading-layer timing.
|
|
46
77
|
|
|
47
78
|
`MediaItem` accepts three shapes:
|
|
48
79
|
|
|
@@ -54,7 +85,8 @@ Mental model:
|
|
|
54
85
|
|
|
55
86
|
```typescript
|
|
56
87
|
import "react-motion-gallery/styles.css";
|
|
57
|
-
import {
|
|
88
|
+
import { toMediaItems, type MediaItem } from "react-motion-gallery/media";
|
|
89
|
+
import { Slider } from "react-motion-gallery/slider";
|
|
58
90
|
|
|
59
91
|
const items: MediaItem[] = toMediaItems([
|
|
60
92
|
"https://picsum.photos/id/1015/1600/900",
|
|
@@ -84,7 +116,93 @@ export function QuickStart() {
|
|
|
84
116
|
|
|
85
117
|
Responsive numeric props in this package accept either a plain number or a breakpoint map like `{ 0: 1, md: 2, 1200: 3 }`. Named breakpoints resolve from the internal map: `xs: 0`, `sm: 600`, `md: 900`, `lg: 1200`, `xl: 1536`.
|
|
86
118
|
|
|
87
|
-
The package root
|
|
119
|
+
The package root exports the primary public components, helper functions, and companion prop types. Use it when one module needs several gallery surfaces. Prefer subpaths for routes or components that only need one surface, such as `react-motion-gallery/media` or `react-motion-gallery/slider`.
|
|
120
|
+
|
|
121
|
+
Subpaths give bundlers a smaller graph than the root. Less JS to transfer, parse, evaluate, and hydrate can improve first loads, cache misses, slower devices, and perceived speed.
|
|
122
|
+
|
|
123
|
+
| Entry point | Main surface |
|
|
124
|
+
| --- | --- |
|
|
125
|
+
| `react-motion-gallery/media` | `toMediaItems`, `MediaItem`, `MediaInput` |
|
|
126
|
+
| `react-motion-gallery/responsive` | `BREAKPOINT_MAP` and responsive value types |
|
|
127
|
+
| `react-motion-gallery/core` | `GalleryCore`, `GalleryCoreProvider`, `useGalleryCore` |
|
|
128
|
+
| `react-motion-gallery/slider` | `Slider`, `createSliderIndexChannel`, slider types |
|
|
129
|
+
| `react-motion-gallery/slider/ready` | `useSliderReady` |
|
|
130
|
+
| `react-motion-gallery/slider/arrows` | `sliderArrows` |
|
|
131
|
+
| `react-motion-gallery/slider/dots` | `sliderDots` |
|
|
132
|
+
| `react-motion-gallery/slider/progress` | `sliderProgress` |
|
|
133
|
+
| `react-motion-gallery/slider/scrollbar` | `sliderScrollbar` |
|
|
134
|
+
| `react-motion-gallery/slider/ripple` | `sliderRipple` |
|
|
135
|
+
| `react-motion-gallery/slider/auto-play` | `sliderAutoPlay` |
|
|
136
|
+
| `react-motion-gallery/slider/auto-scroll` | `sliderAutoScroll` |
|
|
137
|
+
| `react-motion-gallery/slider/auto-height` | `sliderAutoHeight` |
|
|
138
|
+
| `react-motion-gallery/slider/lazy-load` | `sliderLazyLoad` |
|
|
139
|
+
| `react-motion-gallery/slider/parallax` | `sliderParallax` |
|
|
140
|
+
| `react-motion-gallery/slider/scale` | `sliderScale` |
|
|
141
|
+
| `react-motion-gallery/slider/fade` | `sliderFade` |
|
|
142
|
+
| `react-motion-gallery/slider/crossfade` | `sliderCrossfade` |
|
|
143
|
+
| `react-motion-gallery/slider/fullscreen` | `sliderFullscreen` |
|
|
144
|
+
| `react-motion-gallery/slider/loading` | `sliderLoading` |
|
|
145
|
+
| `react-motion-gallery/grid` | `Grid`, `Grid.Item`, grid types |
|
|
146
|
+
| `react-motion-gallery/grid/ready` | `useGridReady` |
|
|
147
|
+
| `react-motion-gallery/masonry` | `Masonry`, `Masonry.Item`, masonry types |
|
|
148
|
+
| `react-motion-gallery/masonry/ready` | `useMasonryReady` |
|
|
149
|
+
| `react-motion-gallery/entries` | `Entries`, `flattenEntries`, entry media container helpers |
|
|
150
|
+
| `react-motion-gallery/skeleton/base` | Standalone `Skeleton` and generic skeleton authoring types |
|
|
151
|
+
| `react-motion-gallery/skeleton/slider` | `SliderSkeleton` and slider skeleton authoring types |
|
|
152
|
+
| `react-motion-gallery/skeleton/grid` | `GridSkeleton` and grid skeleton authoring types |
|
|
153
|
+
| `react-motion-gallery/skeleton/masonry` | `MasonrySkeleton` and masonry skeleton authoring types |
|
|
154
|
+
| `react-motion-gallery/fullscreen` | `useFullscreenController` and fullscreen types |
|
|
155
|
+
| `react-motion-gallery/fullscreen/slider` | `fullscreenSlider` |
|
|
156
|
+
| `react-motion-gallery/fullscreen/controls` | `fullscreenControls` |
|
|
157
|
+
| `react-motion-gallery/fullscreen/captions` | `fullscreenCaptions` |
|
|
158
|
+
| `react-motion-gallery/fullscreen/zoom-pan` | `fullscreenZoomPan` |
|
|
159
|
+
| `react-motion-gallery/fullscreen/video` | `fullscreenVideo` |
|
|
160
|
+
| `react-motion-gallery/fullscreen/lazy-load` | `fullscreenLazyLoad` |
|
|
161
|
+
| `react-motion-gallery/fullscreen/crossfade` | `fullscreenCrossfade` |
|
|
162
|
+
| `react-motion-gallery/fullscreen/thumbnails` | `fullscreenThumbnails` |
|
|
163
|
+
| `react-motion-gallery/thumbnails` | `ThumbnailSlider`, thumbnail sync helpers |
|
|
164
|
+
| `react-motion-gallery/fullscreenThumbnails` | `FullscreenThumbnailSlider` |
|
|
165
|
+
| `react-motion-gallery/video` | `Video` and optional Plyr-backed video types |
|
|
166
|
+
| `react-motion-gallery/zoomPan` | `ZoomPanImage` and zoom/pan types |
|
|
167
|
+
|
|
168
|
+
## Acknowledgements
|
|
169
|
+
|
|
170
|
+
React Motion Gallery's slider engine includes portions of code derived from [Embla Carousel](https://github.com/davidjerleke/embla-carousel), which is MIT licensed. Those portions have been substantially adapted for React Motion Gallery's React architecture, public API, transition system, fullscreen integration, loading layers, and media workflows.
|
|
171
|
+
|
|
172
|
+
See [`THIRD_PARTY_NOTICES.md`](./THIRD_PARTY_NOTICES.md) for the preserved Embla Carousel copyright and MIT license notice.
|
|
173
|
+
|
|
174
|
+
## Core
|
|
175
|
+
|
|
176
|
+
`GalleryCore` is the shared state boundary for fullscreen-aware galleries. Wrap a layout in it when you need shared breakpoints, a normalized fullscreen media list, fullscreen-open state, or programmatic fullscreen opening. `useGalleryCore()` is the public hook for reading that core state from descendants.
|
|
177
|
+
|
|
178
|
+
### `GalleryCore` props
|
|
179
|
+
|
|
180
|
+
| Option | Type | Default | Notes |
|
|
181
|
+
| --- | --- | --- | --- |
|
|
182
|
+
| `children` | `React.ReactNode` | `—` | The gallery tree using the shared core. |
|
|
183
|
+
| `layout` | `"slider" \| "grid" \| "masonry" \| "entries"` | `—` | Declares the owning base layout. Omit it for standalone fullscreen/core usage. |
|
|
184
|
+
| `breakpoints` | `Record<string, number>` | `xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536` | Breakpoint map shared with descendants. |
|
|
185
|
+
| `fullscreenItems` | `MediaItem[] \| string[]` | `[]` | Normalized fullscreen media list. |
|
|
186
|
+
| `nodes` | `ReactNode \| ReactNode[]` | `—` | Advanced initial node list used by the slider-backed imperative state. |
|
|
187
|
+
|
|
188
|
+
### `useGalleryCore` API
|
|
189
|
+
|
|
190
|
+
`GalleryApi` is the public alias for `GalleryCoreApi`. It covers core fullscreen state and programmatic fullscreen opening. Slider item mutation lives on `SliderHandle` and `SliderApi`.
|
|
191
|
+
|
|
192
|
+
| Field / Method | Type | Notes |
|
|
193
|
+
| --- | --- | --- |
|
|
194
|
+
| `layout` | `"slider" \| "grid" \| "masonry" \| "entries" \| null` | Current owning layout, or `null` for standalone fullscreen/core usage. |
|
|
195
|
+
| `effectiveBreakpoints` | `Record<string, number>` | Breakpoint map after merging custom `GalleryCore.breakpoints` with defaults. |
|
|
196
|
+
| `normalizedItems` | `MediaItem[]` | Fullscreen item list normalized from `fullscreenItems`. |
|
|
197
|
+
| `fsEnabled` | `boolean` | `true` when a mounted fullscreen controller has enabled fullscreen behavior. |
|
|
198
|
+
| `setFsEnabled` | `(enabled: boolean) => void` | Enables or disables fullscreen behavior. Usually handled by `useFullscreenController`. |
|
|
199
|
+
| `isFullscreenOpen` | `boolean` | `true` while fullscreen is open. |
|
|
200
|
+
| `isFullscreenOpenRef` | `React.RefObject<boolean>` | Ref mirror for handlers that need the current fullscreen-open state. |
|
|
201
|
+
| `setFullscreenOpen` | `(open: boolean) => void` | Updates fullscreen-open state. Usually handled by the fullscreen runtime. |
|
|
202
|
+
| `openFullscreenAt` | `({ index, method?, event? }) => void` | Opens fullscreen at a normalized fullscreen item index. Pass the source event for scale-origin detection. |
|
|
203
|
+
| `notifyBaseVisibleIndex` | `(index: number) => void` | Emits the visible base media index for fullscreen lazy-load/prewarm coordination. |
|
|
204
|
+
| `notifyFsVisibleIndex` | `(index: number) => void` | Emits the active fullscreen index back to base media. |
|
|
205
|
+
| `registerExpandableImage` | `(index: number, node: HTMLElement \| null) => void` | Registers an origin surface for layoutless scale transitions. |
|
|
88
206
|
|
|
89
207
|
## ZoomPanImage
|
|
90
208
|
|
|
@@ -108,10 +226,68 @@ export function ZoomPanCard() {
|
|
|
108
226
|
|
|
109
227
|
`ZoomPanImage` is the lightweight standalone zoom surface. The component root is the clipping container, so border radius, aspect ratio, and overflow all live on the same element.
|
|
110
228
|
|
|
229
|
+
## Skeleton
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
import { Skeleton, type SkeletonNode } from "react-motion-gallery/skeleton/base";
|
|
233
|
+
|
|
234
|
+
const shellSkeleton: SkeletonNode = {
|
|
235
|
+
kind: "rect",
|
|
236
|
+
style: { width: "100%", height: 320 },
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
export function LoadingShell({ ready, children }: { ready: boolean; children: React.ReactNode }) {
|
|
240
|
+
return (
|
|
241
|
+
<Skeleton
|
|
242
|
+
layout={shellSkeleton}
|
|
243
|
+
ready={ready}
|
|
244
|
+
timing={{ exitMs: 520, minVisibleMs: 220 }}
|
|
245
|
+
force={false}
|
|
246
|
+
ariaLabel={ready ? undefined : "Loading content"}
|
|
247
|
+
>
|
|
248
|
+
{children}
|
|
249
|
+
</Skeleton>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
`Skeleton` can render a standalone placeholder by itself, or it can wrap real content and own the loading transition. Wrapper mode is enabled when `children` are provided.
|
|
255
|
+
|
|
256
|
+
| Option | Type | Default | Notes |
|
|
257
|
+
| --- | --- | --- | --- |
|
|
258
|
+
| `layout` | `SkeletonNode` | `—` | Structured placeholder layout tree. |
|
|
259
|
+
| `children` | `React.ReactNode` | `—` | Real content. When present, `Skeleton` renders content and loading layers. |
|
|
260
|
+
| `ready` | `boolean` | `false` | Reveals content and exits the skeleton once true. |
|
|
261
|
+
| `enabled` | `boolean` | `true` | Set false to render content immediately with no skeleton layer. |
|
|
262
|
+
| `force` | `boolean \| { enabled?: boolean; showContent?: boolean; skeletonOpacity?: number }` | `false` | Keeps the skeleton visible. Set `showContent: true` to preview ready content under the skeleton, and tune the overlay with `skeletonOpacity`. |
|
|
263
|
+
| `timing.exitMs` | `number` | `600` | Keeps the skeleton layer mounted for this long after exit starts and controls the opacity transition. |
|
|
264
|
+
| `timing.minVisibleMs` | `number` | `220` | Minimum time the skeleton stays visible before exit can begin. |
|
|
265
|
+
| `shellClassName` / `shellStyle` | `string` / `CSSProperties` | `—` | Wrapper-layer class and style for content+skeleton mode. |
|
|
266
|
+
| `contentClassName` / `contentStyle` | `string` / `CSSProperties` | `—` | Content-layer class and style for wrapper mode. |
|
|
267
|
+
|
|
268
|
+
The wrapper timing model matches the gallery loading layers: content begins fading in as soon as the skeleton exit starts; it does not wait for the skeleton to unmount.
|
|
269
|
+
|
|
270
|
+
### Browser-measured skeleton text authoring
|
|
271
|
+
|
|
272
|
+
Responsive text is one of the easiest places for a polished loading state to drift away from the real UI. React Motion Gallery's skeleton text workflow measures real DOM text in a live page with headless Chrome, then emits `lines`, `barWidth`, `lastBarWidth`, and optional `barHeight`/`lineHeight` values for the skeleton `text` nodes used by `Slider`, `Grid`, `Masonry`, `Entries`, and standalone `Skeleton` layouts.
|
|
273
|
+
|
|
274
|
+
This is development-time authoring support, not production client code. It is especially useful for multiline cards, responsive grids, equal-height sliders, and reflow-sensitive masonry surfaces where a generic text placeholder can otherwise change row height, item height, or column packing when real content appears.
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
npm run --silent generate:skeleton-text-module -- \
|
|
278
|
+
--input ./path/to/example.skeleton-text.browser.manifest.json \
|
|
279
|
+
--analysis-output ./path/to/example.skeleton-text.measurements.json
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Use `responsiveBy: "container"` when text wrapping follows the card or cell width more closely than the viewport. For equal-height card sliders, the browser analyzer can also measure all canonical slider items and emit `rowHeightCompensation` so unseen cards cannot surprise the skeleton row height. See [`docs/skeleton-text-authoring.md`](./docs/skeleton-text-authoring.md) for manifest fields, command options, and the Codex-friendly workflow.
|
|
283
|
+
|
|
111
284
|
## Slider
|
|
112
285
|
|
|
286
|
+
The default `Slider` is the small synchronous core: children, drag, wheel navigation, snapping, grouping, looping, index channels, intro, and the imperative ref API. Heavier behavior is opt-in through first-party plugins, so importing one feature, such as arrows or parallax, does not pull in the rest of the slider feature set. Structured slider skeletons and restore behavior are owned by `SliderSkeleton`, composed with `useSliderReady()`.
|
|
287
|
+
|
|
113
288
|
```typescript
|
|
114
|
-
import { Slider } from "react-motion-gallery";
|
|
289
|
+
import { Slider } from "react-motion-gallery/slider";
|
|
290
|
+
import { sliderArrows } from "react-motion-gallery/slider/arrows";
|
|
115
291
|
|
|
116
292
|
const slides = [
|
|
117
293
|
"https://picsum.photos/id/1015/1600/900",
|
|
@@ -121,7 +297,7 @@ const slides = [
|
|
|
121
297
|
|
|
122
298
|
export function BasicSlider() {
|
|
123
299
|
return (
|
|
124
|
-
<Slider>
|
|
300
|
+
<Slider plugins={[sliderArrows()]}>
|
|
125
301
|
{slides.map((src, index) => (
|
|
126
302
|
<img key={src} src={src} alt={`Slide ${index + 1}`} style={{ width: "100%" }} />
|
|
127
303
|
))}
|
|
@@ -135,97 +311,89 @@ export function BasicSlider() {
|
|
|
135
311
|
| Option | Type | Default | Notes |
|
|
136
312
|
| --- | --- | --- | --- |
|
|
137
313
|
| `children` | `React.ReactNode` | `—` | Slide content rendered in order. |
|
|
314
|
+
| `initialIndex` | `number` | `0` | Selects the slide index used for the first layout and intro fade-in. |
|
|
138
315
|
| `breakpoints` | `Record<string, number>` | `xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536` | Merged with the internal breakpoint map for responsive values. |
|
|
139
|
-
| `expandableImageRefs` | `React.RefObject<(HTMLImageElement | null)[]>` | internal ref | Supplies origin images for fullscreen scale transitions. |
|
|
140
316
|
| `indexChannel` | `SliderIndexChannel` | internal channel | Share index state with thumbnails or sibling sliders. |
|
|
317
|
+
| `plugins` | `SliderPlugin[]` | `[]` | Explicit first-party slider features such as arrows, dots, auto-height, effects, fullscreen, or lazy-load. |
|
|
141
318
|
|
|
142
319
|
### Slider layout and scroll options
|
|
143
320
|
|
|
144
321
|
| Option | Type | Default | Notes |
|
|
145
322
|
| --- | --- | --- | --- |
|
|
146
|
-
| `layout.gap` | `number
|
|
323
|
+
| `layout.gap` | `number \| Record<string, number>` | `20` | Responsive gap between cells. |
|
|
147
324
|
| `layout.cellsPerSlide` | `number \| Record<string, number>` | `—` | Groups multiple cells into a slide page. |
|
|
148
325
|
| `direction.dir` | `"ltr" \| "rtl"` | `"ltr"` | Text direction and arrow direction. |
|
|
149
326
|
| `direction.axis` | `"x" \| "y"` | `"x"` | Horizontal or vertical slider axis. |
|
|
150
327
|
| `align` | `"start" \| "center"` | `"start"` | Slide alignment inside the viewport. |
|
|
151
328
|
| `scroll.groupCells` | `boolean` | `false` | Scrolls by grouped cells instead of every cell. |
|
|
152
|
-
| `scroll.skipSnaps` | `boolean` | `false` | Allows momentum to skip snap points. |
|
|
329
|
+
| `scroll.skipSnaps` | `boolean \| { enabled?: boolean; threshold?: number }` | `false` | Allows momentum to skip snap points. Object form enables skip snaps by default and `threshold` requires release force to reach a multiple of the adjacent snap distance before multi-snap momentum is used. |
|
|
330
|
+
| `scroll.strictSnaps` | `boolean` | `false` | Prevents one drag release from settling more than one snap away from where the drag started. Overrides `scroll.skipSnaps`. |
|
|
153
331
|
| `scroll.freeScroll` | `boolean` | `false` | Enables free dragging instead of strict snapping. |
|
|
154
332
|
| `scroll.loop` | `boolean` | `false` | Wraps around at the ends. |
|
|
155
333
|
|
|
156
|
-
### Slider element and
|
|
334
|
+
### Slider element and plugin options
|
|
335
|
+
|
|
336
|
+
`elements`, `motion`, and `transitions.intro` stay in the core slider. Controls, autoplay, lazy media, effects, auto-height, fullscreen, and loading overlays are explicit plugin imports.
|
|
157
337
|
|
|
158
338
|
| Option | Type | Default | Notes |
|
|
159
339
|
| --- | --- | --- | --- |
|
|
160
340
|
| `elements.viewport` | `ElementStyle` | `—` | Class and inline style for the viewport element. |
|
|
161
341
|
| `elements.container` | `ElementStyle` | `—` | Class and inline style for the moving slider container. |
|
|
162
|
-
| `lazyLoad.enabled` | `boolean` | `false` | Enables slide-level lazy image and video loading. |
|
|
163
|
-
| `lazyLoad.spinner` | `boolean \| ReactNode \| ((args) => ReactNode)` | `true` | `false` disables the built-in spinner. |
|
|
164
|
-
| `lazyLoad.spinnerClassName` | `string` | `""` | Applied to the spinner wrapper. |
|
|
165
|
-
| `lazyLoad.spinnerStyle` | `React.CSSProperties` | `{}` | Inline styles for the spinner wrapper. |
|
|
166
|
-
|
|
167
|
-
### Slider control options
|
|
168
|
-
|
|
169
|
-
| Option | Type | Default | Notes |
|
|
170
|
-
| --- | --- | --- | --- |
|
|
171
|
-
| `controls.arrows.enabled` | `boolean` | `true` | Toggles previous and next arrows. |
|
|
172
|
-
| `controls.arrows.arrow` | `ElementStyle` | `{}` | Shared arrow class and style. |
|
|
173
|
-
| `controls.arrows.prev` | `ElementStyle` | `{}` | Previous-arrow override. |
|
|
174
|
-
| `controls.arrows.next` | `ElementStyle` | `{}` | Next-arrow override. |
|
|
175
|
-
| `controls.arrows.render` | `(args) => ReactNode` | `—` | Custom renderer for both arrows. |
|
|
176
|
-
| `controls.arrows.renderPrev` | `(args) => ReactNode` | `—` | Custom previous arrow. |
|
|
177
|
-
| `controls.arrows.renderNext` | `(args) => ReactNode` | `—` | Custom next arrow. |
|
|
178
|
-
| `controls.dots.enabled` | `boolean` | `true` | Toggles pagination dots. |
|
|
179
|
-
| `controls.dots.root` | `ElementStyle` | `{}` | Dot container class and style. |
|
|
180
|
-
| `controls.dots.dot` | `ElementStyle` | `{}` | Individual dot class and style. |
|
|
181
|
-
| `controls.dots.render` | `(args) => ReactNode` | `—` | Full custom dots UI. |
|
|
182
|
-
| `controls.progress.enabled` | `boolean` | `false` | Toggles the progress bar. |
|
|
183
|
-
| `controls.progress.root` | `ElementStyle` | `{}` | Progress track class and style. |
|
|
184
|
-
| `controls.progress.bar` | `ElementStyle` | `{}` | Progress fill class and style. |
|
|
185
|
-
| `controls.progress.render` | `(args) => ReactNode` | `—` | Full custom progress UI. |
|
|
186
|
-
| `controls.ripple.enabled` | `boolean` | `true` | Toggles control ripple feedback. |
|
|
187
|
-
| `controls.ripple.className` | `string` | `""` | Custom ripple class. |
|
|
188
|
-
|
|
189
|
-
### Slider auto and transition options
|
|
190
|
-
|
|
191
|
-
| Option | Type | Default | Notes |
|
|
192
|
-
| --- | --- | --- | --- |
|
|
193
|
-
| `auto.play.enabled` | `boolean` | `false` | Timed slide changes. |
|
|
194
|
-
| `auto.play.speedMs` | `number` | `3000` | Delay between autoplay advances. |
|
|
195
|
-
| `auto.play.pauseMs` | `number` | `1000` | Delay after interaction before autoplay resumes. |
|
|
196
|
-
| `auto.play.pauseOnHover` | `boolean` | `true` | Pauses autoplay while hovering. |
|
|
197
|
-
| `auto.scroll.enabled` | `boolean` | `false` | Continuous timed scrolling. |
|
|
198
|
-
| `auto.scroll.speedMs` | `number` | `0.3` | Continuous auto-scroll speed. |
|
|
199
|
-
| `auto.scroll.pauseMs` | `number` | `1000` | Delay after interaction before auto-scroll resumes. |
|
|
200
|
-
| `auto.scroll.pauseOnHover` | `boolean` | `true` | Pauses while hovering. |
|
|
201
|
-
| `transitions.loading.enabled` | `boolean` | `—` | Enables the loading skeleton layer. |
|
|
202
|
-
| `transitions.loading.force` | `boolean` | `—` | Forces the loading layer to stay visible. |
|
|
203
|
-
| `transitions.loading.skeletonCount` | `number \| Record<string, number>` | `—` | Responsive skeleton slot count. |
|
|
204
|
-
| `transitions.loading.renderLoading` | `({ count }) => ReactNode` | `—` | Custom loading renderer. |
|
|
205
|
-
| `transitions.loading.skeleton` | `SliderSkeletonSpec` | `—` | Built-in skeleton spec, including per-slot overrides with `layout.slots` and centered peek support via `centering: "first"`. |
|
|
206
|
-
| `transitions.loading.timing.exitMs` | `number` | `600` | Keeps the loading layer mounted for this long after exit starts. |
|
|
207
|
-
| `transitions.loading.timing.minVisibleMs` | `number` | `220` | Minimum time the loading layer stays visible before exit can begin. |
|
|
208
342
|
| `transitions.intro.renderIntro` | `({ active, containerProps }, content) => ReactNode` | `—` | Custom intro wrapper. |
|
|
209
343
|
| `transitions.intro.staggerMs` | `number` | `—` | Delay between item fade-ins. |
|
|
210
344
|
| `transitions.intro.durationMs` | `number` | `—` | Intro fade duration. |
|
|
211
345
|
| `transitions.intro.easing` | `string` | `—` | Intro fade easing. |
|
|
212
346
|
|
|
347
|
+
### Slider plugins
|
|
348
|
+
|
|
349
|
+
Each plugin is imported from its own subpath and passed to `plugins`. There is no aggregate controls or effects helper; this keeps one-feature imports as small as possible.
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
import { Slider } from "react-motion-gallery/slider";
|
|
353
|
+
import { sliderArrows } from "react-motion-gallery/slider/arrows";
|
|
354
|
+
import { sliderParallax } from "react-motion-gallery/slider/parallax";
|
|
355
|
+
|
|
356
|
+
<Slider plugins={[sliderArrows(), sliderParallax({ bleedPct: "8%" })]}>
|
|
357
|
+
{slides}
|
|
358
|
+
</Slider>;
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
| Import | Factory | Notes |
|
|
362
|
+
| --- | --- | --- |
|
|
363
|
+
| `react-motion-gallery/slider/arrows` | `sliderArrows(options)` | Previous/next arrows. |
|
|
364
|
+
| `react-motion-gallery/slider/dots` | `sliderDots(options)` | Pagination dots. |
|
|
365
|
+
| `react-motion-gallery/slider/progress` | `sliderProgress(options)` | Progress bar or custom progress renderer. |
|
|
366
|
+
| `react-motion-gallery/slider/scrollbar` | `sliderScrollbar(options)` | Range-style position control. |
|
|
367
|
+
| `react-motion-gallery/slider/ripple` | `sliderRipple(options)` | Enables ripple feedback for controls that call `createRipple`. |
|
|
368
|
+
| `react-motion-gallery/slider/auto-play` | `sliderAutoPlay(options)` | Timed slide changes. |
|
|
369
|
+
| `react-motion-gallery/slider/auto-scroll` | `sliderAutoScroll(options)` | Timed continuous advancement. |
|
|
370
|
+
| `react-motion-gallery/slider/auto-height` | `sliderAutoHeight(options)` | Measures active slide height and gates slider readiness until measured. |
|
|
371
|
+
| `react-motion-gallery/slider/lazy-load` | `sliderLazyLoad(options)` | Adds lazy media attributes to slide images and videos. |
|
|
372
|
+
| `react-motion-gallery/slider/parallax` | `sliderParallax(options)` | Parallax slide wrapper. |
|
|
373
|
+
| `react-motion-gallery/slider/scale` | `sliderScale(options)` | Scales non-active slides. |
|
|
374
|
+
| `react-motion-gallery/slider/fade` | `sliderFade(options)` | Fades non-active slides. |
|
|
375
|
+
| `react-motion-gallery/slider/crossfade` | `sliderCrossfade(options)` | Enables crossfade-aware control navigation. |
|
|
376
|
+
| `react-motion-gallery/slider/fullscreen` | `sliderFullscreen()` | Bridges a `GalleryCore layout="slider"` slider to fullscreen. |
|
|
377
|
+
| `react-motion-gallery/slider/loading` | `sliderLoading(options)` | Basic custom loading overlay. Prefer `SliderSkeleton` for structured skeleton and restore. |
|
|
378
|
+
|
|
213
379
|
### Slider loading skeletons
|
|
214
380
|
|
|
215
|
-
`
|
|
381
|
+
Use `SliderSkeleton` to own slider loading. `useSliderReady()` exposes the slider ref plus a settled `ready` flag; `isSlidesBuilt()` remains a lower-level DOM-built signal and is not the right fade-out trigger.
|
|
216
382
|
|
|
217
383
|
`layout.slots` is the per-slide override system. Define the shared placeholder once with `layout.item` and `layout.itemWrapStyle`, then override any individual slot with `slots[index]`. Slot `itemWrapStyle` values merge on top of the base wrap style, while `slot.item` can replace the placeholder node entirely for that slot.
|
|
218
384
|
|
|
219
385
|
`itemWrapStyle` now supports wrapper-only `border` and `boxShadow` values. Wrapper `width`, `height`, and `aspectRatio` are treated as outer border-box dimensions, so the inner placeholder shrinks by the border thickness. Use simple uniform border shorthands such as `1px solid #cbd5e1` when you want the built-in sizing math to account for the border width.
|
|
220
386
|
|
|
221
|
-
`text` nodes render one skeleton bar per `lines` value. `lines` can be a single number or a numeric min-width map such as `{ 0: 3, 767: 2, 1200: 1 }`. Use `
|
|
387
|
+
`text` nodes render one skeleton bar per `lines` value. `barHeight` controls the bar height and can be a single number or a numeric min-width map. `lineHeight` remains the full line-box multiplier and now accepts the same numeric min-width maps. `lines` can be a single number or a numeric min-width map such as `{ 0: 3, 767: 2, 1200: 1 }`. Use `lastBarWidth` to override the shortened trailing bar width; it defaults to `68%` of the text block width and can also be responsive with numeric min-width keys.
|
|
222
388
|
|
|
223
|
-
`centering: "first"` is designed for center-aligned peek sliders. When the real slider uses `align="center"` and the skeleton uses `mode: "peek"` with `layout.kind: "slider"`, the
|
|
389
|
+
`centering: "first"` is designed for center-aligned peek sliders. When the real slider uses `align="center"` and the skeleton uses `mode: "peek"` with `layout.kind: "slider"`, the skeleton renderer inserts the leading spacer needed to center the first visible placeholder. You should not add that spacer manually.
|
|
224
390
|
|
|
225
|
-
When you provide `
|
|
391
|
+
When you provide `SliderSkeleton.timing`, `exitMs` controls both how long the loading layer remains mounted after exit starts and its opacity transition duration.
|
|
226
392
|
|
|
227
393
|
```typescript
|
|
228
|
-
import {
|
|
394
|
+
import { SliderSkeleton } from "react-motion-gallery/skeleton/slider";
|
|
395
|
+
import { Slider } from "react-motion-gallery/slider";
|
|
396
|
+
import { useSliderReady } from "react-motion-gallery/slider/ready";
|
|
229
397
|
|
|
230
398
|
const slides = [
|
|
231
399
|
{ src: "https://picsum.photos/id/1020/660/960", width: 220, height: 320 },
|
|
@@ -234,47 +402,47 @@ const slides = [
|
|
|
234
402
|
];
|
|
235
403
|
|
|
236
404
|
export function VariableWidthSkeletonSlider() {
|
|
405
|
+
const { ref: sliderRef, ready: sliderReady } = useSliderReady();
|
|
406
|
+
|
|
237
407
|
return (
|
|
238
|
-
<
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
height: "100%",
|
|
255
|
-
borderRadius: 12,
|
|
256
|
-
},
|
|
257
|
-
},
|
|
258
|
-
slots: slides.map((slide) => ({
|
|
259
|
-
itemWrapStyle: {
|
|
260
|
-
width: slide.width,
|
|
261
|
-
height: slide.height,
|
|
262
|
-
},
|
|
263
|
-
})),
|
|
408
|
+
<SliderSkeleton
|
|
409
|
+
ready={sliderReady}
|
|
410
|
+
layout={{
|
|
411
|
+
mode: "peek",
|
|
412
|
+
centering: "first",
|
|
413
|
+
visibleCount: 2,
|
|
414
|
+
layout: {
|
|
415
|
+
kind: "slider",
|
|
416
|
+
direction: "row",
|
|
417
|
+
style: { gap: 20 },
|
|
418
|
+
item: {
|
|
419
|
+
kind: "rect",
|
|
420
|
+
style: {
|
|
421
|
+
width: "100%",
|
|
422
|
+
height: "100%",
|
|
423
|
+
borderRadius: 12,
|
|
264
424
|
},
|
|
265
425
|
},
|
|
426
|
+
slots: slides.map((slide) => ({
|
|
427
|
+
itemWrapStyle: {
|
|
428
|
+
width: slide.width,
|
|
429
|
+
height: slide.height,
|
|
430
|
+
},
|
|
431
|
+
})),
|
|
266
432
|
},
|
|
267
433
|
}}
|
|
268
434
|
>
|
|
269
|
-
{
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
435
|
+
<Slider ref={sliderRef} align="center">
|
|
436
|
+
{slides.map((slide, index) => (
|
|
437
|
+
<img
|
|
438
|
+
key={slide.src}
|
|
439
|
+
src={slide.src}
|
|
440
|
+
alt={`Slide ${index + 1}`}
|
|
441
|
+
style={{ width: slide.width, height: slide.height, objectFit: "cover" }}
|
|
442
|
+
/>
|
|
443
|
+
))}
|
|
444
|
+
</Slider>
|
|
445
|
+
</SliderSkeleton>
|
|
278
446
|
);
|
|
279
447
|
}
|
|
280
448
|
```
|
|
@@ -285,6 +453,7 @@ export function VariableWidthSkeletonSlider() {
|
|
|
285
453
|
| --- | --- | --- |
|
|
286
454
|
| `mode` | `"fit" \| "peek"` | `"peek"` preserves partial next or previous slide visibility in the loading state. |
|
|
287
455
|
| `centering` | `"first"` | Adds the leading spacer needed for the first visible slot when using the built-in centered peek skeleton flow. |
|
|
456
|
+
| `visibleCount` | `number \| Record<string, number>` | Responsive count of visible skeleton slots. |
|
|
288
457
|
| `className` | `string \| undefined` | Applied to the skeleton overlay root. |
|
|
289
458
|
| `style` | `React.CSSProperties \| undefined` | Inline styles for the skeleton overlay root. |
|
|
290
459
|
| `layout` | `SliderSkeletonNode \| undefined` | Structured placeholder layout tree. Use `kind: "slider"` to model slide tracks. |
|
|
@@ -298,7 +467,7 @@ export function VariableWidthSkeletonSlider() {
|
|
|
298
467
|
| --- | --- | --- |
|
|
299
468
|
| `kind` | `"slider"` | Slider-specific skeleton layout root. |
|
|
300
469
|
| `style` | `SkeletonContainerStyle \| Record<string, SkeletonContainerStyle>` | Track-level container styles such as `gap`, `padding`, `align`, `justify`, `width`, and `maxWidth`. |
|
|
301
|
-
| `count` | `number \| undefined` | Optional explicit slot count for the layout. Falls back to `
|
|
470
|
+
| `count` | `number \| undefined` | Optional explicit slot count for the layout. Falls back to `visibleCount` on the surrounding slider skeleton spec. |
|
|
302
471
|
| `item` | `SkeletonNode` | Default placeholder node rendered in each slot. |
|
|
303
472
|
| `itemWrapStyle` | `SliderSkeletonWrapStyle \| undefined` | Shared wrapper size, margin, border, and box-shadow rules for every slot. Border sizing is border-box. |
|
|
304
473
|
| `slots` | `SliderSkeletonSlot[] \| undefined` | Per-slot overrides for variable widths, heights, aspect ratios, or custom placeholder nodes. |
|
|
@@ -312,23 +481,15 @@ export function VariableWidthSkeletonSlider() {
|
|
|
312
481
|
| `item` | `SkeletonNode \| undefined` | Replaces the base `layout.item` for one slot. |
|
|
313
482
|
| `itemWrapStyle` | `SliderSkeletonWrapStyle \| undefined` | Merges on top of the base `layout.itemWrapStyle` for one slot, including wrapper borders and shadows. |
|
|
314
483
|
|
|
315
|
-
`SkeletonNode` supports these building blocks: `rect`, `square`, `circle`, `text`, `media`, `row`, `col`, and `stack`. `text.lines` controls how many wrapped skeleton rows render for that text block, and `text.
|
|
484
|
+
`SkeletonNode` supports these building blocks: `rect`, `square`, `circle`, `text`, `media`, `row`, `col`, and `stack`. `text.barHeight` controls the bar height, `text.lines` controls how many wrapped skeleton rows render for that text block, and `text.lastBarWidth` controls the trailing bar width.
|
|
316
485
|
|
|
317
|
-
### Slider motion
|
|
486
|
+
### Slider motion options
|
|
318
487
|
|
|
319
488
|
| Option | Type | Default | Notes |
|
|
320
489
|
| --- | --- | --- | --- |
|
|
321
490
|
| `motion.selectDuration` | `number` | `25` | Duration for snapped selection motion. |
|
|
322
491
|
| `motion.freeScrollDuration` | `number` | `43` | Duration for free-scroll settling. |
|
|
323
492
|
| `motion.friction` | `number` | `0.68` | Drag and settling friction. |
|
|
324
|
-
| `effects.parallax.enabled` | `boolean` | `—` | Enables the parallax slide treatment. |
|
|
325
|
-
| `effects.parallax.bleedPct` | `string` | `—` | Extra image bleed around the viewport. |
|
|
326
|
-
| `effects.parallax.borderRadius` | `string` | `—` | Radius for the parallax frame. |
|
|
327
|
-
| `effects.parallax.sideWidth` | `string` | `—` | Side crop width used by the effect. |
|
|
328
|
-
| `effects.scale.enabled` | `boolean` | `—` | Scales neighboring slides. |
|
|
329
|
-
| `effects.scale.amount` | `number` | `—` | Scale multiplier for the scale effect. |
|
|
330
|
-
| `effects.fade.enabled` | `boolean` | `—` | Fades slides based on position. |
|
|
331
|
-
| `effects.fade.minOpacity` | `number` | `0.36` | Minimum opacity used for the fade effect, clamped from `0` to `1`. |
|
|
332
493
|
|
|
333
494
|
### Slider render callback args
|
|
334
495
|
|
|
@@ -387,12 +548,22 @@ export function VariableWidthSkeletonSlider() {
|
|
|
387
548
|
| `onSlidesBuilt` | `(cb: (nodes: HTMLElement[]) => void) => () => void` | Runs when slide nodes are ready. |
|
|
388
549
|
| `whenSlidesBuilt` | `() => Promise<HTMLElement[]>` | Promise form of `onSlidesBuilt`. |
|
|
389
550
|
| `isSlidesBuilt` | `() => boolean` | `true` once the slide list is ready. |
|
|
551
|
+
| `onReady` | `(cb: (nodes: HTMLElement[]) => void) => () => void` | Runs when the slider has built, measured, committed its index, and all plugin ready gates have cleared. |
|
|
552
|
+
| `whenReady` | `() => Promise<HTMLElement[]>` | Promise form of `onReady`. |
|
|
553
|
+
| `isReady` | `() => boolean` | `true` once the settled slider ready signal has fired. |
|
|
390
554
|
| `scrollNext` | `(mode?: IndexMode) => void` | Advances one step. |
|
|
391
555
|
| `scrollPrev` | `(mode?: IndexMode) => void` | Moves backward one step. |
|
|
392
556
|
| `canScrollNext` | `() => boolean` | Whether next navigation is available. |
|
|
393
557
|
| `canScrollPrev` | `() => boolean` | Whether previous navigation is available. |
|
|
394
558
|
| `scrollProgress` | `() => number` | Current progress from `0` to `1`. |
|
|
395
559
|
| `cellsInView` | `() => number[]` | Canonical cell indexes currently visible. |
|
|
560
|
+
| `append` | `(nodes: ReactNode \| ReactNode[]) => number` | Appends nodes and returns the new total count. |
|
|
561
|
+
| `prepend` | `(nodes: ReactNode \| ReactNode[]) => number` | Prepends nodes and returns the new total count. |
|
|
562
|
+
| `insert` | `(index: number, nodes: ReactNode \| ReactNode[]) => number` | Inserts nodes and returns the new total count. |
|
|
563
|
+
| `remove` | `(indexOrPredicate: number \| ((i: number) => boolean)) => number` | Removes items and returns the new total count. |
|
|
564
|
+
| `replace` | `(index: number, node: ReactNode) => void` | Replaces a node at an index. |
|
|
565
|
+
| `setItems` | `(nodes: ReactNode[]) => number` | Replaces all nodes and returns the new total count. |
|
|
566
|
+
| `onIndexChange` | `(cb: (i: number, meta: { mode: IndexMode }) => void) => () => void` | Subscribes to index changes. |
|
|
396
567
|
| `getInternals` | `() => { slides, slider, visibleImages, selectedIndex, sliderX, sliderVelocity, isWrapping }` | Low-level internals used by fullscreen and advanced sync code. |
|
|
397
568
|
|
|
398
569
|
### `createSliderIndexChannel`
|
|
@@ -473,7 +644,7 @@ export function SliderWithThumbnails() {
|
|
|
473
644
|
}
|
|
474
645
|
```
|
|
475
646
|
|
|
476
|
-
The component forwards a ref to its outer thumbnail shell.
|
|
647
|
+
The component forwards a ref to its outer thumbnail shell.
|
|
477
648
|
|
|
478
649
|
### ThumbnailSlider component props
|
|
479
650
|
|
|
@@ -532,7 +703,7 @@ The component forwards a ref to its outer thumbnail shell. The explicit `layout`
|
|
|
532
703
|
| Option | Type | Default | Notes |
|
|
533
704
|
| --- | --- | --- | --- |
|
|
534
705
|
| `transitions.loading.enabled` | `boolean` | `true` | Enables the thumbnail loading layer. |
|
|
535
|
-
| `transitions.loading.force` | `boolean` | `false` | Forces the loading layer to remain visible. |
|
|
706
|
+
| `transitions.loading.force` | `boolean \| { enabled?: boolean; showContent?: boolean; skeletonOpacity?: number }` | `false` | Forces the loading layer to remain visible. Set `showContent: true` to preview the real thumbnails under the skeleton, and tune the loading overlay with `skeletonOpacity`. |
|
|
536
707
|
| `transitions.loading.skeletonCount` | `number \| Record<string, number>` | `—` | Responsive count for the built-in loading placeholders. |
|
|
537
708
|
| `transitions.loading.mode` | `"fit" \| "peek"` | `"peek"` | `"peek"` keeps fixed-size thumbnail placeholders when width or height is explicitly set; `"fit"` divides the rail evenly across the visible count. |
|
|
538
709
|
| `transitions.loading.elements.container` | `ElementStyle` | `—` | Class and inline style for the built-in loading overlay container. |
|
|
@@ -588,16 +759,28 @@ export function BasicGrid() {
|
|
|
588
759
|
|
|
589
760
|
| Option | Type | Default | Notes |
|
|
590
761
|
| --- | --- | --- | --- |
|
|
591
|
-
| `children` | `React.ReactNode` | `—` | Grid items rendered in order. |
|
|
762
|
+
| `children` | `React.ReactNode` | `—` | Grid items rendered in order. Wrap individual cards in `Grid.Item` when they need custom spans or wrapper props. |
|
|
592
763
|
| `breakpoints` | `Record<string, number>` | `xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536` | Used to resolve responsive columns and gaps. |
|
|
593
764
|
| `gridItemBaseClass` | `string` | `"rmg__grid-item"` | Internal item base class override. |
|
|
594
765
|
| `renderMode` | `"wrap" \| "passthrough"` | `"wrap"` | `wrap` adds an item wrapper; `passthrough` keeps child structure closer to the source node. |
|
|
595
766
|
|
|
767
|
+
### Grid.Item props
|
|
768
|
+
|
|
769
|
+
`Grid.Item` is a metadata wrapper. It renders only its children, while Grid reads the wrapper props and applies them to the generated item shell.
|
|
770
|
+
|
|
771
|
+
| Option | Type | Default | Notes |
|
|
772
|
+
| --- | --- | --- | --- |
|
|
773
|
+
| `children` | `React.ReactNode` | `—` | The grid card content. |
|
|
774
|
+
| `span` | `number \| "full" \| Record<string, number \| "full">` | `1` | Per-item track span. `"full"` renders `grid-column: 1 / -1`; numeric values render `grid-column: span n / span n`. |
|
|
775
|
+
| `className` | `string` | `—` | Extra class name merged onto the grid item wrapper. |
|
|
776
|
+
| `style` | `React.CSSProperties` | `—` | Inline styles merged onto the grid item wrapper. |
|
|
777
|
+
|
|
596
778
|
### Grid options
|
|
597
779
|
|
|
598
780
|
| Option | Type | Default | Notes |
|
|
599
781
|
| --- | --- | --- | --- |
|
|
600
782
|
| `columns` | `number \| Record<string, number>` | `—` | Fixed responsive column count. When omitted, Grid auto-fits using `minColumnWidth`. |
|
|
783
|
+
| `templateColumns` | `string \| Record<string, string>` | `—` | Explicit `grid-template-columns` value. Takes precedence over `columns` and `minColumnWidth`. |
|
|
601
784
|
| `minColumnWidth` | `number \| string` | `160` | Minimum width used by auto-fit mode. |
|
|
602
785
|
| `gap` | `number \| Record<string, number>` | `8` | Responsive grid gap. |
|
|
603
786
|
| `rootClassName` | `string` | `—` | Class name for the grid root. |
|
|
@@ -607,15 +790,9 @@ export function BasicGrid() {
|
|
|
607
790
|
| `lazyLoad.spinner` | `boolean \| ReactNode \| ((args) => ReactNode)` | `—` | Spinner override for lazy items. |
|
|
608
791
|
| `lazyLoad.spinnerClassName` | `string` | `—` | Spinner wrapper class. |
|
|
609
792
|
| `lazyLoad.spinnerStyle` | `React.CSSProperties` | `—` | Spinner wrapper style. |
|
|
610
|
-
| `loading.enabled` | `boolean` | `—` | Enables the loading layer. |
|
|
611
|
-
| `loading.force` | `boolean` | `—` | Keeps the loading layer visible even when media is ready. |
|
|
612
|
-
| `loading.renderLoading` | `({ count }) => ReactNode` | `—` | Custom loading renderer. |
|
|
613
|
-
| `loading.skeleton` | `GridSkeletonSpec` | `—` | Built-in grid skeleton spec. |
|
|
614
|
-
| `loading.timing.exitMs` | `number` | `600` | Keeps the loading layer mounted for this long after exit starts. |
|
|
615
|
-
| `loading.timing.minVisibleMs` | `number` | `220` | Minimum time the loading layer stays visible before exit can begin. |
|
|
616
793
|
| `intro.renderIntro` | `({ active, containerProps }, content) => ReactNode` | `—` | Custom intro wrapper. |
|
|
617
|
-
| `intro.staggerMs` | `number` | `
|
|
618
|
-
| `intro.durationMs` | `number` | `
|
|
794
|
+
| `intro.staggerMs` | `number` | `60` | Reveal stagger for the fade-in. |
|
|
795
|
+
| `intro.durationMs` | `number` | `600` | Intro fade duration. |
|
|
619
796
|
| `intro.easing` | `string` | `"cubic-bezier(.2,.7,.2,1)"` | Intro fade easing. |
|
|
620
797
|
| `intro.staggerLimit` | `number` | `—` | Optional cap on how many items stagger. |
|
|
621
798
|
|
|
@@ -623,9 +800,92 @@ When `lazyLoad.enabled` is true, Grid rewrites trackable image `src` values into
|
|
|
623
800
|
|
|
624
801
|
Grid fullscreen behavior is provided by `GalleryCore` and `useFullscreenController`; Grid itself does not expose a ref-based imperative API.
|
|
625
802
|
|
|
626
|
-
|
|
803
|
+
Wrap a card in `Grid.Item` when it should span tracks or needs wrapper styling:
|
|
804
|
+
|
|
805
|
+
```typescript
|
|
806
|
+
<Grid columns={{ 0: 1, 720: 6, 1100: 12 }} gap={{ 0: 12, 1100: 18 }}>
|
|
807
|
+
<Grid.Item span={{ 0: "full", 720: 3, 1100: 6 }} className="feature-card">
|
|
808
|
+
<FeatureCard />
|
|
809
|
+
</Grid.Item>
|
|
810
|
+
<Grid.Item span={{ 0: "full", 720: 3, 1100: 3 }}>
|
|
811
|
+
<ProductCard />
|
|
812
|
+
</Grid.Item>
|
|
813
|
+
<Grid.Item span="full">
|
|
814
|
+
<WideEditorialCard />
|
|
815
|
+
</Grid.Item>
|
|
816
|
+
</Grid>
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
Grid spans require explicit tracks: use `columns` or `templateColumns`. If Grid is in auto-fit mode through `minColumnWidth`, item spans are ignored because there is no stable track count to span. Responsive span maps use the same breakpoint keys as responsive numeric props, so named keys such as `md` and numeric keys such as `900` are both valid.
|
|
820
|
+
|
|
821
|
+
Use `templateColumns` when the tracks themselves need custom proportions:
|
|
822
|
+
|
|
823
|
+
```typescript
|
|
824
|
+
<Grid
|
|
825
|
+
templateColumns={{
|
|
826
|
+
0: "1fr",
|
|
827
|
+
900: "minmax(0, 1.4fr) minmax(0, 1fr)",
|
|
828
|
+
1200: "minmax(0, 2fr) repeat(2, minmax(0, 1fr))",
|
|
829
|
+
}}
|
|
830
|
+
gap={{ 0: 12, 1200: 18 }}
|
|
831
|
+
>
|
|
832
|
+
<Grid.Item span={{ 0: "full", 900: 2 }}>
|
|
833
|
+
<FeatureCard />
|
|
834
|
+
</Grid.Item>
|
|
835
|
+
</Grid>
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
Grid no longer owns loading UI. Use `useGridReady` and wrap Grid with `GridSkeleton`, the same composition pattern used by Slider and Masonry.
|
|
839
|
+
|
|
840
|
+
Grid skeletons live in `react-motion-gallery/skeleton/grid`. Their `text` nodes use the same wrapped-line treatment as slider skeletons, including responsive `barHeight` and `lines` maps plus the configurable trailing `lastBarWidth`.
|
|
627
841
|
|
|
628
|
-
Grid
|
|
842
|
+
Grid skeletons inherit real item spans by default. Slot overrides in the Skeleton layout can change individual placeholder nodes or wrapper styles without losing the span applied by `Grid.Item`.
|
|
843
|
+
|
|
844
|
+
When Grid is wrapped in `GridSkeleton`, `GridSkeleton.timing.exitMs` controls both how long the loading layer stays mounted after exit starts and its opacity transition, and the real grid intro begins as soon as exit starts.
|
|
845
|
+
|
|
846
|
+
```typescript
|
|
847
|
+
import { Grid, useGridReady } from "react-motion-gallery";
|
|
848
|
+
import { GridSkeleton, type GridSkeletonSpec } from "react-motion-gallery/skeleton/grid";
|
|
849
|
+
|
|
850
|
+
const gridSkeleton: GridSkeletonSpec = {
|
|
851
|
+
radius: 14,
|
|
852
|
+
layout: {
|
|
853
|
+
kind: "grid",
|
|
854
|
+
count: 6,
|
|
855
|
+
item: {
|
|
856
|
+
kind: "rect",
|
|
857
|
+
style: { aspectRatio: "4 / 5" },
|
|
858
|
+
},
|
|
859
|
+
},
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
function GridWithSkeleton({ images }: { images: { src: string; alt: string }[] }) {
|
|
863
|
+
const { ref: gridRef, ready: gridReady } = useGridReady();
|
|
864
|
+
|
|
865
|
+
return (
|
|
866
|
+
<GridSkeleton
|
|
867
|
+
layout={gridSkeleton}
|
|
868
|
+
ready={gridReady}
|
|
869
|
+
timing={{ minVisibleMs: 220, exitMs: 600 }}
|
|
870
|
+
grid={{
|
|
871
|
+
count: images.length,
|
|
872
|
+
columns: { 0: 1, 640: 2, 960: 3 },
|
|
873
|
+
gap: { 0: 12, 960: 20 },
|
|
874
|
+
}}
|
|
875
|
+
>
|
|
876
|
+
<Grid
|
|
877
|
+
ref={gridRef}
|
|
878
|
+
columns={{ 0: 1, 640: 2, 960: 3 }}
|
|
879
|
+
gap={{ 0: 12, 960: 20 }}
|
|
880
|
+
>
|
|
881
|
+
{images.map((image) => (
|
|
882
|
+
<img key={image.src} src={image.src} alt={image.alt} />
|
|
883
|
+
))}
|
|
884
|
+
</Grid>
|
|
885
|
+
</GridSkeleton>
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
```
|
|
629
889
|
|
|
630
890
|
## Masonry
|
|
631
891
|
|
|
@@ -654,144 +914,154 @@ export function BasicMasonry() {
|
|
|
654
914
|
|
|
655
915
|
| Option | Type | Default | Notes |
|
|
656
916
|
| --- | --- | --- | --- |
|
|
657
|
-
| `children` | `React.ReactNode` | `—` | Masonry items rendered in order. |
|
|
917
|
+
| `children` | `React.ReactNode` | `—` | Masonry items rendered in order. Wrap individual cards in `Masonry.Item` when they need custom spans or wrapper props. |
|
|
658
918
|
| `breakpoints` | `Record<string, number>` | `xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536` | Used to resolve responsive columns and gaps. |
|
|
659
919
|
|
|
920
|
+
### Masonry.Item props
|
|
921
|
+
|
|
922
|
+
| Option | Type | Default | Notes |
|
|
923
|
+
| --- | --- | --- | --- |
|
|
924
|
+
| `children` | `React.ReactNode` | `—` | The masonry card content. |
|
|
925
|
+
| `span` | `number \| "full" \| Record<string, number \| "full">` | `1` | Per-item track span. `"full"` resolves to the active column count and numeric values clamp to the current track count. |
|
|
926
|
+
| `className` | `string` | `—` | Extra class name merged onto the masonry item wrapper. |
|
|
927
|
+
| `style` | `React.CSSProperties` | `—` | Inline styles merged onto the masonry item wrapper. |
|
|
928
|
+
|
|
660
929
|
### Masonry options
|
|
661
930
|
|
|
662
931
|
| Option | Type | Default | Notes |
|
|
663
932
|
| --- | --- | --- | --- |
|
|
664
933
|
| `columns` | `number \| Record<string, number>` | `—` | Responsive column count. |
|
|
665
934
|
| `gap` | `number \| Record<string, number>` | `—` | Responsive gap between columns and items. |
|
|
666
|
-
| `placement` | `"balanced" \| "roundRobin"` | `"balanced"` | `balanced`
|
|
667
|
-
| `
|
|
935
|
+
| `placement` | `"balanced" \| "roundRobin" \| "horizontalOrder"` | `"balanced"` | `balanced` packs into the shortest fitting column group, `roundRobin` cycles start columns deterministically, and `horizontalOrder` preserves a stronger left-to-right scan when spans are involved. |
|
|
936
|
+
| `fullscreenTrigger` | `"item" \| "media"` | `"media"` | Opens fullscreen from the clicked media node or the entire masonry item shell. |
|
|
668
937
|
| `itemWrapClassName` | `string` | `—` | Class name added to the masonry item wrapper. |
|
|
669
938
|
| `itemWrapStyle` | `React.CSSProperties` | `—` | Inline styles applied to the masonry item wrapper. |
|
|
670
939
|
| `as` | `React.ElementType` | `"div"` | Root HTML element or custom component. |
|
|
671
940
|
| `rootRef` | `React.Ref<HTMLDivElement>` | `—` | Ref to the masonry root. |
|
|
672
941
|
| `classNames.root` | `string` | `—` | Root class name. |
|
|
673
|
-
| `classNames.column` | `string` | `—` |
|
|
942
|
+
| `classNames.column` | `string` | `—` | Retained for backwards compatibility with the legacy column-wrapper renderer. |
|
|
674
943
|
| `classNames.item` | `string` | `—` | Item class name. |
|
|
675
944
|
| `lazyLoad.enabled` | `boolean` | `—` | Enables lazy media loading. |
|
|
676
945
|
| `lazyLoad.spinner` | `boolean \| ReactNode \| ((args) => ReactNode)` | `—` | Spinner override for lazy items. |
|
|
677
946
|
| `lazyLoad.spinnerClassName` | `string` | `—` | Spinner wrapper class. |
|
|
678
947
|
| `lazyLoad.spinnerStyle` | `React.CSSProperties` | `—` | Spinner wrapper style. |
|
|
679
|
-
| `loading.enabled` | `boolean` | `—` | Enables the loading layer. |
|
|
680
|
-
| `loading.force` | `boolean` | `—` | Forces the loading layer to stay visible. |
|
|
681
|
-
| `loading.renderLoading` | `({ count }) => ReactNode` | `—` | Custom loading renderer. |
|
|
682
|
-
| `loading.skeleton` | `MasonrySkeletonSpec` | `—` | Built-in masonry skeleton spec. |
|
|
683
948
|
| `intro.renderIntro` | `({ active, containerProps }, content) => ReactNode` | `—` | Custom intro wrapper. |
|
|
684
|
-
| `intro.staggerMs` | `number` | `
|
|
685
|
-
| `intro.durationMs` | `number` | `
|
|
949
|
+
| `intro.staggerMs` | `number` | `160` | Reveal stagger for the fade-in. |
|
|
950
|
+
| `intro.durationMs` | `number` | `600` | Intro fade duration. |
|
|
686
951
|
| `intro.easing` | `string` | `"cubic-bezier(.2,.7,.2,1)"` | Intro fade easing. |
|
|
687
952
|
| `intro.staggerLimit` | `number` | `—` | Optional cap on how many items stagger. |
|
|
688
953
|
|
|
689
954
|
When `lazyLoad.enabled` is true, Masonry uses the same image shell behavior as Slider: trackable image `src` values move into `data-rmg-lazy-src`, the real images load on intersection, and the item only fades in after decode and spinner exit.
|
|
690
955
|
|
|
691
|
-
Masonry already accepts arbitrary React children, including text-containing JSX. The
|
|
956
|
+
Masonry already accepts arbitrary React children, including text-containing JSX. The wrapper props are only for styling the built-in masonry item shell.
|
|
692
957
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
`layout.slots` gives Masonry the same per-card override escape hatch that slider skeletons have. Use a slot when one card needs a different placeholder tree, wrapper styling, or outer height. `slot.ratio` maps to Masonry's card-height rhythm, while `slot.heightPx` lets you pin a specific shell height when you need an exact placeholder.
|
|
958
|
+
Wrap a card in `Masonry.Item` when it needs its own span, wrapper `className`, or wrapper `style`:
|
|
696
959
|
|
|
697
960
|
```typescript
|
|
698
961
|
<Masonry
|
|
699
|
-
columns={{ 0: 1,
|
|
700
|
-
gap={{ 0: 12,
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
962
|
+
columns={{ 0: 1, 760: 2, 1160: 4 }}
|
|
963
|
+
gap={{ 0: 12, 1160: 18 }}
|
|
964
|
+
placement="horizontalOrder"
|
|
965
|
+
>
|
|
966
|
+
<Masonry.Item span={{ 0: 1, 760: 2, 1160: 2 }}>
|
|
967
|
+
<FeatureCard />
|
|
968
|
+
</Masonry.Item>
|
|
969
|
+
<Masonry.Item span={1}>
|
|
970
|
+
<StandardCard />
|
|
971
|
+
</Masonry.Item>
|
|
972
|
+
</Masonry>
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
Choose a placement based on what should feel stable:
|
|
976
|
+
|
|
977
|
+
- `balanced`: best when visual balance and the shortest overall columns matter most.
|
|
978
|
+
- `roundRobin`: best when deterministic column assignment matters more than tight packing.
|
|
979
|
+
- `horizontalOrder`: best when wider cards should still read in a mostly left-to-right order.
|
|
980
|
+
|
|
981
|
+
Masonry no longer owns loading UI. Use `useMasonryReady` and wrap Masonry with `MasonrySkeleton`, the same composition pattern used by Slider and Grid.
|
|
982
|
+
|
|
983
|
+
Masonry skeletons live in `react-motion-gallery/skeleton/masonry` and can use a structured `layout` spec with the same inner node vocabulary as Grid skeletons, including `text` nodes and `itemWrapStyle`.
|
|
984
|
+
|
|
985
|
+
Live Masonry content mounts invisibly until the current item set has completed an initial measurement pass. The Skeleton wrapper stays visible during that handoff, so the first revealed layout is based on measured DOM geometry rather than approximate height hints.
|
|
986
|
+
|
|
987
|
+
`layout.slots` gives Masonry the same per-card override escape hatch that slider skeletons have. Use a slot when one card needs a different placeholder tree, wrapper styling, span, or outer height. `slot.span` can override the corresponding `Masonry.Item` span for the placeholder, `slot.ratio` maps to Masonry's card-height rhythm, and `slot.heightPx` lets you pin a specific shell height when you need an exact placeholder.
|
|
988
|
+
|
|
989
|
+
```typescript
|
|
990
|
+
import { Masonry, useMasonryReady } from "react-motion-gallery";
|
|
991
|
+
import {
|
|
992
|
+
MasonrySkeleton,
|
|
993
|
+
type MasonrySkeletonSpec,
|
|
994
|
+
} from "react-motion-gallery/skeleton/masonry";
|
|
995
|
+
|
|
996
|
+
const masonrySkeleton: MasonrySkeletonSpec = {
|
|
997
|
+
ratios: [118, 126, 102, 146],
|
|
998
|
+
layout: {
|
|
999
|
+
kind: "masonry",
|
|
1000
|
+
itemWrapStyle: {
|
|
1001
|
+
padding: 14,
|
|
1002
|
+
borderRadius: 20,
|
|
1003
|
+
boxShadow: "0 18px 36px rgba(15, 23, 42, 0.08)",
|
|
1004
|
+
},
|
|
1005
|
+
item: {
|
|
1006
|
+
kind: "col",
|
|
1007
|
+
style: { gap: 12 },
|
|
1008
|
+
children: [
|
|
1009
|
+
{
|
|
1010
|
+
kind: "rect",
|
|
1011
|
+
style: { width: "100%", height: 180, borderRadius: 16 },
|
|
1012
|
+
},
|
|
1013
|
+
{
|
|
1014
|
+
kind: "text",
|
|
1015
|
+
barHeight: 14,
|
|
1016
|
+
lineHeight: 1.55,
|
|
1017
|
+
lines: 3,
|
|
1018
|
+
lastBarWidth: "74%",
|
|
1019
|
+
style: { width: "100%" },
|
|
715
1020
|
},
|
|
1021
|
+
],
|
|
1022
|
+
},
|
|
1023
|
+
slots: [
|
|
1024
|
+
{
|
|
1025
|
+
ratio: 182,
|
|
1026
|
+
span: { 0: 1, 1100: 2 },
|
|
716
1027
|
item: {
|
|
717
|
-
kind: "
|
|
718
|
-
style: {
|
|
719
|
-
children: [
|
|
720
|
-
{
|
|
721
|
-
kind: "rect",
|
|
722
|
-
style: { width: "100%", height: 180, borderRadius: 16 },
|
|
723
|
-
},
|
|
724
|
-
{
|
|
725
|
-
kind: "text",
|
|
726
|
-
fontSize: 12,
|
|
727
|
-
lineHeight: 1.4,
|
|
728
|
-
lines: 1,
|
|
729
|
-
lineWidth: "36%",
|
|
730
|
-
style: { width: "34%", borderRadius: 999 },
|
|
731
|
-
},
|
|
732
|
-
{
|
|
733
|
-
kind: "text",
|
|
734
|
-
fontSize: 18,
|
|
735
|
-
lineHeight: 1.35,
|
|
736
|
-
lines: { 0: 2, 900: 1 },
|
|
737
|
-
lineWidth: "64%",
|
|
738
|
-
style: { width: "88%" },
|
|
739
|
-
},
|
|
740
|
-
{
|
|
741
|
-
kind: "text",
|
|
742
|
-
fontSize: 14,
|
|
743
|
-
lineHeight: 1.55,
|
|
744
|
-
lines: 3,
|
|
745
|
-
lineWidth: "74%",
|
|
746
|
-
style: { width: "100%" },
|
|
747
|
-
},
|
|
748
|
-
],
|
|
1028
|
+
kind: "rect",
|
|
1029
|
+
style: { width: "100%", aspectRatio: "3 / 5", borderRadius: 16 },
|
|
749
1030
|
},
|
|
750
|
-
slots: [
|
|
751
|
-
{
|
|
752
|
-
ratio: 182,
|
|
753
|
-
item: {
|
|
754
|
-
kind: "col",
|
|
755
|
-
style: { gap: 12 },
|
|
756
|
-
children: [
|
|
757
|
-
{
|
|
758
|
-
kind: "rect",
|
|
759
|
-
style: { width: "100%", aspectRatio: "3 / 5", borderRadius: 16 },
|
|
760
|
-
},
|
|
761
|
-
{
|
|
762
|
-
kind: "text",
|
|
763
|
-
fontSize: 12,
|
|
764
|
-
lineHeight: 1.4,
|
|
765
|
-
lines: 1,
|
|
766
|
-
lineWidth: "36%",
|
|
767
|
-
style: { width: "28%", borderRadius: 999 },
|
|
768
|
-
},
|
|
769
|
-
{
|
|
770
|
-
kind: "text",
|
|
771
|
-
fontSize: 18,
|
|
772
|
-
lineHeight: 1.35,
|
|
773
|
-
lines: 1,
|
|
774
|
-
lineWidth: "64%",
|
|
775
|
-
style: { width: "72%" },
|
|
776
|
-
},
|
|
777
|
-
{
|
|
778
|
-
kind: "text",
|
|
779
|
-
fontSize: 14,
|
|
780
|
-
lineHeight: 1.55,
|
|
781
|
-
lines: 2,
|
|
782
|
-
lineWidth: "78%",
|
|
783
|
-
style: { width: "100%" },
|
|
784
|
-
},
|
|
785
|
-
],
|
|
786
|
-
},
|
|
787
|
-
},
|
|
788
|
-
],
|
|
789
1031
|
},
|
|
790
|
-
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
1032
|
+
],
|
|
1033
|
+
},
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
function MasonryWithSkeleton({ items }: { items: React.ReactNode[] }) {
|
|
1037
|
+
const { ref: masonryRef, ready: masonryReady } = useMasonryReady();
|
|
1038
|
+
|
|
1039
|
+
return (
|
|
1040
|
+
<MasonrySkeleton
|
|
1041
|
+
layout={masonrySkeleton}
|
|
1042
|
+
ready={masonryReady}
|
|
1043
|
+
timing={{ minVisibleMs: 220, exitMs: 600 }}
|
|
1044
|
+
masonry={{
|
|
1045
|
+
count: items.length,
|
|
1046
|
+
columns: { 0: 1, 700: 2, 1100: 3 },
|
|
1047
|
+
gap: { 0: 12, 1100: 20 },
|
|
1048
|
+
placement: "balanced",
|
|
1049
|
+
}}
|
|
1050
|
+
>
|
|
1051
|
+
<Masonry
|
|
1052
|
+
ref={masonryRef}
|
|
1053
|
+
columns={{ 0: 1, 700: 2, 1100: 3 }}
|
|
1054
|
+
gap={{ 0: 12, 1100: 20 }}
|
|
1055
|
+
itemWrapStyle={{
|
|
1056
|
+
padding: "6px",
|
|
1057
|
+
borderRadius: "28px",
|
|
1058
|
+
}}
|
|
1059
|
+
>
|
|
1060
|
+
{items}
|
|
1061
|
+
</Masonry>
|
|
1062
|
+
</MasonrySkeleton>
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
795
1065
|
```
|
|
796
1066
|
|
|
797
1067
|
## Entries
|
|
@@ -863,6 +1133,16 @@ export function EntryGallery() {
|
|
|
863
1133
|
}
|
|
864
1134
|
```
|
|
865
1135
|
|
|
1136
|
+
### Entry loading, decode, and reveal flow
|
|
1137
|
+
|
|
1138
|
+
When `loading.enabled` is true, entries use two viewport gates instead of one generic fade-in. `loading.nearMargin` marks a row as near the viewport, mounts the real entry content, and starts the entry media work early. `loading.viewMargin` and `loading.threshold` record when the row has actually entered view.
|
|
1139
|
+
|
|
1140
|
+
With `loading.waitForDecode` enabled, an entry does not reveal as soon as it intersects. The built-in gate waits for every trackable media URL in that entry to load and decode; in the current entry-level gate, that means image media in the entry’s `media` array. It falls back after `loading.decodeTimeoutMs`, and entries without image media are decode-ready immediately. The row fades from skeleton to content only after both conditions are true: the row has entered view and the entry media decode gate is ready.
|
|
1141
|
+
|
|
1142
|
+
Reveal timing is assigned when each entry becomes ready, so entries fade in by actual load/decode completion order as well as viewport intersection. A later row that loads quickly can take the next reveal slot while a slower row keeps its skeleton visible until its media is ready.
|
|
1143
|
+
|
|
1144
|
+
Fullscreen close has a matching entry-aware path. If the user closes fullscreen from a slide whose owning entry has not been viewed yet, the runtime resolves the flattened fullscreen index back to the owner entry, shows a temporary loading spinner while that row mounts and decodes, scrolls the owner entry into view, forces the skeleton/content layers to their final revealed state, and then runs the close animation back to the now-visible entry media. This keeps the close animation from landing on an unrevealed skeleton or an offscreen row.
|
|
1145
|
+
|
|
866
1146
|
### `Entries` component props
|
|
867
1147
|
|
|
868
1148
|
| Option | Type | Default | Notes |
|
|
@@ -886,11 +1166,14 @@ export function EntryGallery() {
|
|
|
886
1166
|
| `mediaLayout` | `"slider" \| "grid" \| "masonry"` | `"slider"` | Declares the intended media layout. |
|
|
887
1167
|
| `render.card` | `({ entry, entryIndex, media }) => ReactNode` | `—` | Wraps the media container in custom card UI. |
|
|
888
1168
|
| `render.media` | `({ entry, entryIndex, media, mediaIndex }) => ReactNode` | `—` | Custom media renderer per media item. |
|
|
889
|
-
| `render.overlay` | `({ entry, entryIndex, mediaIndex, link, opacity, fsIndex, style, containerProps }) => ReactNode` | `—` | Renders fullscreen overlay content for the active entry slide. |
|
|
1169
|
+
| `render.overlay` | `({ entry, entryIndex, media, mediaIndex, link, opacity, fsIndex, style, containerProps }) => ReactNode` | `—` | Renders fullscreen overlay content for the active entry slide. |
|
|
890
1170
|
| `render.skeleton` | `({ entry, entryIndex }) => ReactNode` | `—` | Declared in the type, but the current runtime uses `loading.skeleton` instead. |
|
|
891
1171
|
| `overlay` | `ElementStyle` | `—` | Styles the fullscreen overlay container that wraps `render.overlay`. |
|
|
1172
|
+
| `overlay.overlayCrossfadeTarget` | `"content" \| "overlay"` | `"overlay"` | Selects whether fullscreen entry changes fade only the rendered overlay content or the whole overlay layer. |
|
|
1173
|
+
| `overlay.overlayCrossfadeDurationMs` | `number` | `300` | Duration for fullscreen entry overlay crossfades. |
|
|
1174
|
+
| `overlay.overlayCrossfadeEasing` | `string` | `"cubic-bezier(.4,0,.22,1)"` | Easing for fullscreen entry overlay crossfades. |
|
|
892
1175
|
| `loading.enabled` | `boolean` | `—` | Enables entry loading and decode gating. |
|
|
893
|
-
| `loading.force` | `boolean` | `—` | Forces entry skeletons to remain visible. |
|
|
1176
|
+
| `loading.force` | `boolean \| { enabled?: boolean; showContent?: boolean; skeletonOpacity?: number }` | `—` | Forces entry skeletons to remain visible. Set `showContent: true` to preview mounted, ready entry content under the skeleton, and tune the loading overlay with `skeletonOpacity`. |
|
|
894
1177
|
| `loading.skeleton` | `EntrySkeletonSpec \| ((args) => EntrySkeletonSpec \| null \| undefined)` | `—` | Built-in skeleton spec or resolver. |
|
|
895
1178
|
| `loading.minHeight` | `number \| string` | `"260px"` | Minimum reserved height while loading. |
|
|
896
1179
|
| `loading.nearMargin` | `string` | `"700px 0px"` | Preload margin used before entries enter view. |
|
|
@@ -907,7 +1190,7 @@ export function EntryGallery() {
|
|
|
907
1190
|
| `entryList` | `ElementStyle` | `—` | Styles the entry list container. |
|
|
908
1191
|
| `entryRow` | `ElementStyle` | `—` | Styles each entry row container. |
|
|
909
1192
|
|
|
910
|
-
Entry skeleton `text` nodes also render wrapped line bars via `lines`, matching the slider and grid skeleton behavior, including responsive line counts
|
|
1193
|
+
Entry skeleton `text` nodes also render wrapped line bars via `lines`, matching the slider and grid skeleton behavior, including responsive `barHeight` and line counts plus configurable trailing `lastBarWidth`.
|
|
911
1194
|
|
|
912
1195
|
### Entry-related callback and helper types
|
|
913
1196
|
|
|
@@ -941,6 +1224,7 @@ Entry skeleton `text` nodes also render wrapped line bars via `lines`, matching
|
|
|
941
1224
|
| --- | --- | --- |
|
|
942
1225
|
| `entry` | `EntryItem` | Entry owning the active fullscreen slide. |
|
|
943
1226
|
| `entryIndex` | `number` | Entry index. |
|
|
1227
|
+
| `media` | `MediaItem \| null` | Media item for the active fullscreen slide, when available. |
|
|
944
1228
|
| `mediaIndex` | `number \| null` | Media index inside the entry when available. |
|
|
945
1229
|
| `link` | `MediaEntryLink \| null` | Flattened link back to the entry/media pair. |
|
|
946
1230
|
| `opacity` | `number` | Overlay opacity supplied by the runtime. |
|
|
@@ -981,9 +1265,86 @@ Entry skeleton `text` nodes also render wrapped line bars via `lines`, matching
|
|
|
981
1265
|
|
|
982
1266
|
Fullscreen is compositional. `GalleryCore` owns the normalized fullscreen item list, your layout opens slides through that core, and `useFullscreenController` renders the portal UI.
|
|
983
1267
|
|
|
1268
|
+
### Standalone fullscreen
|
|
1269
|
+
|
|
1270
|
+
Use `GalleryCore` without a `layout` prop when your own markup owns the visible surface. Call `openFullscreenAt` with the matching item index, and render the fullscreen portal once inside the core.
|
|
1271
|
+
|
|
1272
|
+
```typescript
|
|
1273
|
+
import * as React from "react";
|
|
1274
|
+
import { GalleryCore, useGalleryCore } from "react-motion-gallery/core";
|
|
1275
|
+
import { useFullscreenController } from "react-motion-gallery/fullscreen";
|
|
1276
|
+
import { fullscreenSlider } from "react-motion-gallery/fullscreen/slider";
|
|
1277
|
+
import { toMediaItems } from "react-motion-gallery/media";
|
|
1278
|
+
|
|
1279
|
+
const images = [
|
|
1280
|
+
{
|
|
1281
|
+
src: "https://picsum.photos/id/1015/1600/900",
|
|
1282
|
+
alt: "Mountain lake",
|
|
1283
|
+
},
|
|
1284
|
+
{
|
|
1285
|
+
src: "https://picsum.photos/id/1018/1600/900",
|
|
1286
|
+
alt: "Forest path",
|
|
1287
|
+
},
|
|
1288
|
+
];
|
|
1289
|
+
|
|
1290
|
+
const fullscreenItems = toMediaItems(images);
|
|
1291
|
+
|
|
1292
|
+
function FullscreenPortal() {
|
|
1293
|
+
const { fullscreenNode } = useFullscreenController({
|
|
1294
|
+
plugins: [fullscreenSlider()],
|
|
1295
|
+
fullscreen: { enabled: true },
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
return <>{fullscreenNode}</>;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function ImageButton(props: {
|
|
1302
|
+
image: (typeof images)[number];
|
|
1303
|
+
index: number;
|
|
1304
|
+
}) {
|
|
1305
|
+
const gallery = useGalleryCore();
|
|
1306
|
+
|
|
1307
|
+
const open = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
1308
|
+
gallery.openFullscreenAt({
|
|
1309
|
+
index: props.index,
|
|
1310
|
+
event: event.nativeEvent,
|
|
1311
|
+
});
|
|
1312
|
+
};
|
|
1313
|
+
|
|
1314
|
+
return (
|
|
1315
|
+
<button type="button" onClick={open}>
|
|
1316
|
+
<img
|
|
1317
|
+
src={props.image.src}
|
|
1318
|
+
alt={props.image.alt}
|
|
1319
|
+
style={{
|
|
1320
|
+
display: "block",
|
|
1321
|
+
width: 180,
|
|
1322
|
+
aspectRatio: "16 / 9",
|
|
1323
|
+
objectFit: "cover",
|
|
1324
|
+
}}
|
|
1325
|
+
/>
|
|
1326
|
+
</button>
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
export function StandaloneFullscreen() {
|
|
1331
|
+
return (
|
|
1332
|
+
<GalleryCore fullscreenItems={fullscreenItems}>
|
|
1333
|
+
{images.map((image, index) => (
|
|
1334
|
+
<ImageButton key={image.src} image={image} index={index} />
|
|
1335
|
+
))}
|
|
1336
|
+
<FullscreenPortal />
|
|
1337
|
+
</GalleryCore>
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
```
|
|
1341
|
+
|
|
1342
|
+
### Slider fullscreen
|
|
1343
|
+
|
|
984
1344
|
```typescript
|
|
985
1345
|
import * as React from "react";
|
|
986
1346
|
import { GalleryCore, Slider, useFullscreenController } from "react-motion-gallery";
|
|
1347
|
+
import { fullscreenSlider } from "react-motion-gallery/fullscreen/slider";
|
|
987
1348
|
|
|
988
1349
|
const slides = [
|
|
989
1350
|
"https://picsum.photos/id/1015/1600/900",
|
|
@@ -993,6 +1354,7 @@ const slides = [
|
|
|
993
1354
|
|
|
994
1355
|
function FullscreenAddon() {
|
|
995
1356
|
const { fullscreenNode } = useFullscreenController({
|
|
1357
|
+
plugins: [fullscreenSlider()],
|
|
996
1358
|
fullscreen: { enabled: true },
|
|
997
1359
|
});
|
|
998
1360
|
|
|
@@ -1013,13 +1375,28 @@ export function SliderWithFullscreen() {
|
|
|
1013
1375
|
}
|
|
1014
1376
|
```
|
|
1015
1377
|
|
|
1378
|
+
### Fullscreen lazy-load handshake
|
|
1379
|
+
|
|
1380
|
+
Fullscreen keeps the base layout and fullscreen surface as separate render trees joined by one canonical index. The base layout can render thumbnails, cropped images, cards, or entries while `GalleryCore.fullscreenItems` provides the media that fullscreen renders for the same positions.
|
|
1381
|
+
|
|
1382
|
+
That index is also the communication channel for lazy loading. When a base item becomes visible, `GalleryCore` emits a base-visible event. If `fullscreen.lazyLoad.images.enabled` or `fullscreen.lazyLoad.videos.enabled` is active through `fullscreenLazyLoad()`, the fullscreen runtime listens for that event and prewarms the matching fullscreen media: images are fetched and decoded with high priority, and videos can prewarm their poster/source before being force-mounted.
|
|
1383
|
+
|
|
1384
|
+
Once the modal is open, the fullscreen slider index becomes the live gate. `fsSub` changes recompute which canonical image or video is allowed to mount or apply its source, then notify the lazy slide listeners. The active slide is always allowed; decoded images and prepared videos stay warm, and videos that were prewarmed from the base layout remain in the allowed set so navigation can land on prepared media.
|
|
1385
|
+
|
|
1386
|
+
Fullscreen also emits its visible index back through `GalleryCore`. Base media primitives use the core fullscreen state to suspend while fullscreen is active, and can use the visible fullscreen index to prewarm their matching media. Captions, overlays, and thumbnail rails stay synchronized through the same index contract.
|
|
1387
|
+
|
|
1388
|
+
For custom fullscreen images, `fullscreen.renderImage` must render a real descendant `<img>`. With `fullscreenLazyLoad({ images: { enabled: true } })`, that custom renderer participates in the same mount, spinner, load, and decode flow instead of mounting every fullscreen image eagerly.
|
|
1389
|
+
|
|
1016
1390
|
Add fullscreen thumbnails by rendering `FullscreenThumbnailSlider` with the bridge returned from `useFullscreenController`.
|
|
1017
1391
|
|
|
1018
1392
|
```typescript
|
|
1019
1393
|
import { FullscreenThumbnailSlider, useFullscreenController } from "react-motion-gallery";
|
|
1394
|
+
import { fullscreenSlider } from "react-motion-gallery/fullscreen/slider";
|
|
1395
|
+
import { fullscreenThumbnails } from "react-motion-gallery/fullscreen/thumbnails";
|
|
1020
1396
|
|
|
1021
1397
|
function FullscreenWithThumbs({ thumbs }: { thumbs: string[] }) {
|
|
1022
1398
|
const { fullscreenNode, fullscreenThumbnailBridge } = useFullscreenController({
|
|
1399
|
+
plugins: [fullscreenSlider(), fullscreenThumbnails()],
|
|
1023
1400
|
fullscreen: {
|
|
1024
1401
|
enabled: true,
|
|
1025
1402
|
slider: {
|
|
@@ -1047,6 +1424,7 @@ Set `fullscreen.slider.direction` when fullscreen should mirror RTL interaction:
|
|
|
1047
1424
|
|
|
1048
1425
|
```typescript
|
|
1049
1426
|
useFullscreenController({
|
|
1427
|
+
plugins: [fullscreenSlider()],
|
|
1050
1428
|
fullscreen: {
|
|
1051
1429
|
enabled: true,
|
|
1052
1430
|
slider: {
|
|
@@ -1056,22 +1434,52 @@ useFullscreenController({
|
|
|
1056
1434
|
});
|
|
1057
1435
|
```
|
|
1058
1436
|
|
|
1059
|
-
|
|
1437
|
+
Set `fullscreen.slider.gap` to add space between fullscreen slides. It accepts the same responsive number form as the base slider, using the `GalleryCore.breakpoints` map for named breakpoint keys:
|
|
1060
1438
|
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1439
|
+
```typescript
|
|
1440
|
+
useFullscreenController({
|
|
1441
|
+
plugins: [fullscreenSlider()],
|
|
1442
|
+
fullscreen: {
|
|
1443
|
+
enabled: true,
|
|
1444
|
+
slider: {
|
|
1445
|
+
gap: { 0: 12, md: 20, 1200: 28 },
|
|
1446
|
+
},
|
|
1447
|
+
},
|
|
1448
|
+
});
|
|
1449
|
+
```
|
|
1450
|
+
|
|
1451
|
+
Import `fullscreenVideo` from `react-motion-gallery/fullscreen/video` for fullscreen video slides. Set `fullscreen.video.playOnOpen` to start a Plyr-backed fullscreen video when fullscreen opens directly onto that video slide:
|
|
1452
|
+
|
|
1453
|
+
```typescript
|
|
1454
|
+
useFullscreenController({
|
|
1455
|
+
plugins: [fullscreenSlider(), fullscreenVideo()],
|
|
1456
|
+
fullscreen: {
|
|
1457
|
+
enabled: true,
|
|
1458
|
+
video: {
|
|
1459
|
+
playOnOpen: true,
|
|
1460
|
+
},
|
|
1461
|
+
},
|
|
1462
|
+
});
|
|
1463
|
+
```
|
|
1068
1464
|
|
|
1069
1465
|
### `useFullscreenController` args
|
|
1070
1466
|
|
|
1071
1467
|
| Option | Type | Default | Notes |
|
|
1072
1468
|
| --- | --- | --- | --- |
|
|
1469
|
+
| `plugins` | `FullscreenPlugin[]` | `[]` | Explicit first-party fullscreen features. At minimum, import `fullscreenSlider()` to mount the fullscreen runtime. |
|
|
1073
1470
|
| `fullscreen` | `FullscreenOptions` | `—` | Fullscreen behavior and rendering options. |
|
|
1074
1471
|
|
|
1472
|
+
| Import | Factory | Notes |
|
|
1473
|
+
| --- | --- | --- |
|
|
1474
|
+
| `react-motion-gallery/fullscreen/slider` | `fullscreenSlider(options)` | Mounts the fullscreen slider runtime and accepts `fullscreen.slider` options. |
|
|
1475
|
+
| `react-motion-gallery/fullscreen/controls` | `fullscreenControls(options)` | Option plugin for close, arrows, and counter options. Use with `fullscreenSlider()`. |
|
|
1476
|
+
| `react-motion-gallery/fullscreen/captions` | `fullscreenCaptions(options)` | Adds caption rendering, placement, and caption motion runtime. Use with `fullscreenSlider()`. |
|
|
1477
|
+
| `react-motion-gallery/fullscreen/zoom-pan` | `fullscreenZoomPan(options)` | Adds fullscreen click zoom, pan, and pinch runtime. Use with `fullscreenSlider()`. |
|
|
1478
|
+
| `react-motion-gallery/fullscreen/video` | `fullscreenVideo(options)` | Adds fullscreen Plyr rendering, source/options, and `playOnOpen` runtime. Use with `fullscreenSlider()`. |
|
|
1479
|
+
| `react-motion-gallery/fullscreen/lazy-load` | `fullscreenLazyLoad(options)` | Adds fullscreen image and video lazy-load gates. Use with `fullscreenSlider()`. |
|
|
1480
|
+
| `react-motion-gallery/fullscreen/crossfade` | `fullscreenCrossfade(options)` | Option plugin for fullscreen crossfade controls, drag, and wheel behavior. Use with `fullscreenSlider()`. |
|
|
1481
|
+
| `react-motion-gallery/fullscreen/thumbnails` | `fullscreenThumbnails()` | Option-only plugin for fullscreen thumbnail bridge behavior. Use with `fullscreenSlider()`. |
|
|
1482
|
+
|
|
1075
1483
|
### Recommended `useFullscreenController` return values
|
|
1076
1484
|
|
|
1077
1485
|
| Field | Type | Notes |
|
|
@@ -1092,9 +1500,10 @@ The hook returns additional refs and setters for the internal fullscreen runtime
|
|
|
1092
1500
|
| --- | --- | --- | --- |
|
|
1093
1501
|
| `enabled` | `boolean` | `false` | Master switch for fullscreen UI. |
|
|
1094
1502
|
| `items` | `MediaItem[] \| string[]` | `—` | Declared in the type, but current fullscreen media resolution comes from `GalleryCore.fullscreenItems`. |
|
|
1095
|
-
| `renderImage` | `({ item, index, isZoomed, className, baseStyle }) => ReactNode` | `—` | Custom fullscreen image renderer. Must render a real descendant `<img>`. |
|
|
1503
|
+
| `renderImage` | `({ item, index, isZoomed, className, baseStyle }) => ReactNode` | `—` | Custom fullscreen image renderer. Must render a real descendant `<img>`. With `lazyLoad.images.enabled`, the renderer is mounted only when the slide is allowed and the runtime watches that descendant image for load/decode readiness. |
|
|
1096
1504
|
| `video.source` | `(item: MediaItem, index: number) => Plyr.SourceInfo` | `—` | Builds fullscreen Plyr sources for video items. |
|
|
1097
1505
|
| `video.options` | `Plyr.Options \| ((item: MediaItem, index: number) => Plyr.Options)` | `—` | Builds fullscreen Plyr options. |
|
|
1506
|
+
| `video.playOnOpen` | `boolean` | `false` | Attempts to play the fullscreen Plyr video when fullscreen opens directly onto a video slide. Browser autoplay rules still apply. |
|
|
1098
1507
|
| `video.style` | `React.CSSProperties` | `—` | Fullscreen player inline style. |
|
|
1099
1508
|
| `video.className` | `string` | `—` | Fullscreen player class. |
|
|
1100
1509
|
| `controls.close.enabled` | `boolean` | `true` | Toggles the close button. |
|
|
@@ -1120,6 +1529,9 @@ The hook returns additional refs and setters for the internal fullscreen runtime
|
|
|
1120
1529
|
| `caption.breakpoint` | `number` | `—` | Viewport cutoff for switching placement logic. |
|
|
1121
1530
|
| `caption.render` | `({ item, index, isZoomed }) => ReactNode` | `—` | Custom caption renderer. |
|
|
1122
1531
|
| `caption.layout` | `"overlay" \| "slide"` | `—` | Chooses whether the caption overlays the media or lives in the slide layout. |
|
|
1532
|
+
| `caption.overlayCrossfadeTarget` | `"content" \| "overlay"` | `"content"` | Selects whether overlay caption changes fade only the rendered caption content or the whole overlay layer. |
|
|
1533
|
+
| `caption.overlayCrossfadeDurationMs` | `number` | `300` | Duration for fullscreen overlay caption crossfades. |
|
|
1534
|
+
| `caption.overlayCrossfadeEasing` | `string` | `"cubic-bezier(.4,0,.22,1)"` | Easing for fullscreen overlay caption crossfades. |
|
|
1123
1535
|
| `caption.zoomFade` | `boolean` | `true` | Fades captions out on fullscreen zoom-in and back in on zoom-out. |
|
|
1124
1536
|
| `caption.zoomFadeDurationMs` | `number` | `300` | Duration for fullscreen caption zoom fades. |
|
|
1125
1537
|
| `caption.zoomFadeEasing` | `string` | `"cubic-bezier(.4,0,.22,1)"` | Easing for fullscreen caption zoom fades. |
|
|
@@ -1128,6 +1540,7 @@ The hook returns additional refs and setters for the internal fullscreen runtime
|
|
|
1128
1540
|
| `slider.duration` | `number` | `25` | Fullscreen slider motion duration. |
|
|
1129
1541
|
| `slider.friction` | `number` | `0.68` | Fullscreen slider friction. |
|
|
1130
1542
|
| `slider.direction` | `"ltr" \| "rtl"` | `"ltr"` | Fullscreen slider interaction direction. |
|
|
1543
|
+
| `slider.gap` | `number \| Record<string, number>` | `0` | Responsive pixel gap between fullscreen slides. Named keys resolve from `GalleryCore.breakpoints`. |
|
|
1131
1544
|
| `zoom.clickZoomLevel` | `number` | `2.5` | Zoom level used for click-to-zoom. |
|
|
1132
1545
|
| `zoom.maxZoomLevel` | `number` | `3` | Maximum allowed zoom level. |
|
|
1133
1546
|
| `zoom.panDuration` | `number` | `43` | Pan settling duration. |
|
|
@@ -1135,18 +1548,27 @@ The hook returns additional refs and setters for the internal fullscreen runtime
|
|
|
1135
1548
|
| `effects.introDuration` | `number` | `300` | Open animation duration. |
|
|
1136
1549
|
| `effects.introEasing` | `string` | `"cubic-bezier(.4,0,.22,1)"` | Open animation easing. |
|
|
1137
1550
|
| `effects.introFade` | `boolean` | `false` | Forces fade intro behavior. |
|
|
1138
|
-
| `effects.
|
|
1139
|
-
| `effects.
|
|
1140
|
-
| `effects.
|
|
1141
|
-
| `
|
|
1551
|
+
| `effects.crossfade.controls` | `boolean` | `false` | Uses crossfade transitions for fullscreen arrow navigation and animated slide requests. Also enables wheel crossfade unless `effects.crossfade.wheel` is provided. |
|
|
1552
|
+
| `effects.crossfade.drag` | `boolean` | `false` | Scrubs adjacent fullscreen slides with crossfade during drag instead of moving the track. |
|
|
1553
|
+
| `effects.crossfade.wheel` | `boolean \| CrossFadeWheelOptions` | `effects.crossfade.controls` | Uses wheel or touchpad travel as a one-slide-at-a-time fullscreen crossfade gesture. Set `false` to keep arrow crossfades while using normal wheel scrolling. |
|
|
1554
|
+
| `effects.crossfade.wheel.enabled` | `boolean` | `true` when object form is used | Enables or disables fullscreen wheel crossfade when using the object form. |
|
|
1555
|
+
| `effects.crossfade.wheel.sensitivity` | `number` | `5` | Multiplies wheel delta into virtual drag progress. Higher values reach the commit threshold sooner. |
|
|
1556
|
+
| `effects.crossfade.wheel.commitThreshold` | `number` | `0.38` | Progress needed to commit to the previous or next fullscreen slide. Values are clamped from `0` to below `0.5`. |
|
|
1557
|
+
| `effects.crossfade.wheel.durationMs` | `number` | `effects.crossfade.durationMs` | Fade duration after fullscreen wheel crossfade commits. |
|
|
1558
|
+
| `effects.crossfade.wheel.sessionGapMs` | `number` | `24` | Short quiet window used to distinguish same-direction touchpad tail from a fresh fullscreen wheel gesture after a committed wheel crossfade. |
|
|
1559
|
+
| `effects.crossfade.durationMs` | `number` | `120` | Shared fullscreen crossfade duration for controls, drag release, and wheel commit unless wheel overrides it. |
|
|
1560
|
+
| `effects.crossfade.easing` | `string` | `"cubic-bezier(.4,0,.22,1)"` | Shared fullscreen crossfade easing. |
|
|
1561
|
+
| `lazyLoad.images.enabled` | `boolean` | `—` | Enables fullscreen image lazy loading. Base-visible indices predecode matching fullscreen images, and fullscreen index changes allow the active image slide to mount or apply its source. |
|
|
1142
1562
|
| `lazyLoad.images.spinner` | `boolean \| ReactNode \| ((args) => ReactNode)` | `—` | Spinner override for fullscreen images. |
|
|
1143
1563
|
| `lazyLoad.images.spinnerClassName` | `string` | `—` | Spinner class for image slides. |
|
|
1144
1564
|
| `lazyLoad.images.spinnerStyle` | `React.CSSProperties` | `—` | Spinner style for image slides. |
|
|
1145
|
-
| `lazyLoad.videos.enabled` | `boolean` | `—` |
|
|
1565
|
+
| `lazyLoad.videos.enabled` | `boolean` | `—` | Opts fullscreen videos into lazy mounting. Base-visible indices prewarm matching video posters/sources and fullscreen index changes mount the active or already-prepared video slide. By default fullscreen Plyr videos mount eagerly in the hidden fullscreen tree. |
|
|
1146
1566
|
| `lazyLoad.videos.spinner` | `boolean \| ReactNode \| ((args) => ReactNode)` | `—` | Spinner override for fullscreen videos. |
|
|
1147
1567
|
| `lazyLoad.videos.spinnerClassName` | `string` | `—` | Spinner class for video slides. |
|
|
1148
1568
|
| `lazyLoad.videos.spinnerStyle` | `React.CSSProperties` | `—` | Spinner style for video slides. |
|
|
1149
1569
|
|
|
1570
|
+
Fullscreen `effects.crossfade.wheel` uses the same `true`, `false`, or object form as slider wheel crossfade. Its `durationMs` default follows fullscreen `effects.crossfade.durationMs`, which defaults to `120`.
|
|
1571
|
+
|
|
1150
1572
|
### Fullscreen callback and helper types
|
|
1151
1573
|
|
|
1152
1574
|
#### `FsCounterArgs`
|
|
@@ -1255,37 +1677,6 @@ The hook returns additional refs and setters for the internal fullscreen runtime
|
|
|
1255
1677
|
| `fadeDurationMs` | `number \| undefined` | Slot fade duration. |
|
|
1256
1678
|
| `fadeEasing` | `string \| undefined` | Slot fade easing. |
|
|
1257
1679
|
|
|
1258
|
-
### `GalleryApi`
|
|
1259
|
-
|
|
1260
|
-
`GalleryApi` is exported as a type from the package root. The package also exposes `GalleryCore` and `useGalleryCore()` for core-context access, but it does not expose a dedicated hook that returns a `GalleryApi`-typed instance directly.
|
|
1261
|
-
|
|
1262
|
-
| Method | Signature | Notes |
|
|
1263
|
-
| --- | --- | --- |
|
|
1264
|
-
| `rootNode` | `() => HTMLElement \| null` | Gallery root node. |
|
|
1265
|
-
| `containerNode` | `() => HTMLElement \| null` | Moving or content container node. |
|
|
1266
|
-
| `getViewportNode` | `() => HTMLDivElement \| null` | Viewport node. |
|
|
1267
|
-
| `slideNodes` | `() => HTMLElement[]` | Current slide elements. |
|
|
1268
|
-
| `onReady` | `(cb: (nodes: HTMLElement[]) => void) => () => void` | Subscribes to readiness. |
|
|
1269
|
-
| `whenReady` | `() => Promise<HTMLElement[]>` | Promise form of readiness. |
|
|
1270
|
-
| `isReady` | `() => boolean` | `true` after readiness resolves. |
|
|
1271
|
-
| `scrollTo` | `(index: number, jump?: boolean) => void` | Navigates to a slide. |
|
|
1272
|
-
| `scrollNext` | `(jump?: boolean) => void` | Advances to the next slide. |
|
|
1273
|
-
| `scrollPrev` | `(jump?: boolean) => void` | Moves to the previous slide. |
|
|
1274
|
-
| `canScrollNext` | `() => boolean` | Whether next navigation is available. |
|
|
1275
|
-
| `canScrollPrev` | `() => boolean` | Whether previous navigation is available. |
|
|
1276
|
-
| `getIndex` | `() => number` | Current active index. |
|
|
1277
|
-
| `selectCell` | `(index: number, jump?: boolean) => void` | Selects a cell by canonical index. |
|
|
1278
|
-
| `scrollProgress` | `() => number` | Scroll progress from `0` to `1`. |
|
|
1279
|
-
| `cellsInView` | `() => number[]` | Canonical cells currently visible. |
|
|
1280
|
-
| `append` | `(nodes: ReactNode \| ReactNode[]) => number` | Appends nodes and returns the new total count. |
|
|
1281
|
-
| `prepend` | `(nodes: ReactNode \| ReactNode[]) => number` | Prepends nodes and returns the new total count. |
|
|
1282
|
-
| `insert` | `(index: number, nodes: ReactNode \| ReactNode[]) => number` | Inserts nodes and returns the new total count. |
|
|
1283
|
-
| `remove` | `(indexOrPredicate: number \| ((i: number) => boolean)) => number` | Removes items and returns the new total count. |
|
|
1284
|
-
| `replace` | `(index: number, node: ReactNode) => void` | Replaces a node at an index. |
|
|
1285
|
-
| `setItems` | `(nodes: ReactNode[]) => number` | Replaces all nodes and returns the new total count. |
|
|
1286
|
-
| `onIndexChange` | `(cb: (i: number, meta: { mode: IndexMode }) => void) => () => void` | Subscribes to index changes. |
|
|
1287
|
-
| `openFullscreenAt` | `({ index, method?, event? }) => void` | Programmatically opens fullscreen at an index. |
|
|
1288
|
-
|
|
1289
1680
|
## Video
|
|
1290
1681
|
|
|
1291
1682
|
`Video` is the gallery-aware video primitive. It mounts Plyr lazily, syncs with gallery visibility, and can be used inside `Slider`, `Grid`, `Masonry`, `Entries`, and fullscreen flows.
|