@gallerist/react 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +143 -2
- package/dist/index.cjs +320 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +141 -0
- package/dist/index.d.ts +141 -0
- package/dist/index.js +306 -0
- package/dist/index.js.map +1 -0
- package/package.json +77 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shafayet Ahmmed
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,144 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @gallerist/react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React adapter for Gallerist — <Gallerist> component, useGallerist() hook, JSX overlay/region portals.
|
|
4
|
+
|
|
5
|
+
Part of [Gallerist](https://github.com/shafayet9780/gallerist#readme) — a headless, framework-agnostic media viewer for grouped / nested galleries with image-space overlays and virtualized rendering.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i @gallerist/react
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Needs React and React DOM 18 or later (it uses `useSyncExternalStore`, `useId` and
|
|
14
|
+
`useInsertionEffect`).
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
import { Gallerist } from '@gallerist/react'
|
|
20
|
+
import '@gallerist/theme-default/gallerist.css'
|
|
21
|
+
|
|
22
|
+
<Gallerist items={[{ id: 'a', type: 'image', source: 'a.jpg' }]} open={open} onClose={() => setOpen(false)} />
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Props that update live: `items` / `groups` / `collection` (changed content is applied in place and
|
|
26
|
+
keeps the cursor on the same item; data rebuilt inline with the same content is ignored; an invalid
|
|
27
|
+
update, such as a duplicate id, is logged and the current data kept), `on`, `open`, `onClose`, `skin`,
|
|
28
|
+
`accent` and `scheme`. Everything else (`loop`, `startAt`, `plugins`, `overlays`, `preset`, `container`,
|
|
29
|
+
`label`, `returnFocus`, `preload`, `adapters`, …) is read once; change the component's `key` to apply
|
|
30
|
+
new values.
|
|
31
|
+
|
|
32
|
+
`preset` sets the layout, the thumbnails rail included: `thumbnails()` in `plugins` takes the preset's
|
|
33
|
+
rail options for any option you don't pass (`preset="reviewer"` gives the timeline and numbered items;
|
|
34
|
+
`thumbnails({ timeline: false })` keeps the numbers and drops the timeline).
|
|
35
|
+
|
|
36
|
+
`plugins`, `on` and render props are functions, so `<Gallerist>` is used from a client component. The
|
|
37
|
+
build carries a `"use client"` banner, so a Next.js App Router page can import it from inside a client
|
|
38
|
+
boundary (a file that starts with `"use client"`); a Server Component can't pass it those props.
|
|
39
|
+
|
|
40
|
+
### Opening at a position
|
|
41
|
+
|
|
42
|
+
`open` takes `true`, `false`, or a position `{ g, i }` (group index, optional item index):
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
const [at, setAt] = useState<{ g: number; i?: number } | false>(false)
|
|
46
|
+
|
|
47
|
+
<button onClick={() => setAt({ g: 3, i: 1 })}>Open the 4th group</button>
|
|
48
|
+
<Gallerist groups={groups} open={at} onClose={() => setAt(false)} />
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
A new position while the viewer is open moves the cursor. The position is applied when it changes,
|
|
52
|
+
not enforced: the user can navigate away from it, and an inline `open={{ g: 3 }}` re-rendered with the
|
|
53
|
+
same value doesn't pull them back. `open` is compared by value (`g` and `i`), so setting the same
|
|
54
|
+
`{ g, i }` again does nothing; to jump back to it, call `ref.current.open({ g, i })`. Going from a
|
|
55
|
+
position to `true` keeps the viewer where it is.
|
|
56
|
+
|
|
57
|
+
### Escape
|
|
58
|
+
|
|
59
|
+
The viewer listens for Escape on its own root and closes, unless a handler that ran first called
|
|
60
|
+
`event.preventDefault()`. A dialog or popover you open over the viewer should handle Escape itself:
|
|
61
|
+
if it is rendered inside the viewer (`<Gallerist.Overlay>`, `<Gallerist.Region>`), call
|
|
62
|
+
`preventDefault()` on the Escape that closes it, or the viewer closes too. A dialog library that
|
|
63
|
+
listens on `document` in the capture phase sees Escape before the viewer does; it keeps the viewer
|
|
64
|
+
open only if it calls `preventDefault()` (or `stopPropagation()`) on the key it handles.
|
|
65
|
+
|
|
66
|
+
### The imperative handle
|
|
67
|
+
|
|
68
|
+
The ref is the viewer itself (`ViewerApi` from `@gallerist/core`), for code that drives the viewer
|
|
69
|
+
without controlled props:
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
import type { ViewerApi } from '@gallerist/core'
|
|
73
|
+
|
|
74
|
+
const viewer = useRef<ViewerApi>(null)
|
|
75
|
+
|
|
76
|
+
<Gallerist ref={viewer} groups={groups} />
|
|
77
|
+
<button onClick={() => viewer.current?.open({ g: 2, i: 0 })}>Open</button>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`open({ g, i })`, `close()`, `next()` / `prev()`, `nextItem()` / `prevItem()`, `goTo(g, i)`,
|
|
81
|
+
`goToId(groupId, itemId)`, `zoomTo(scale)`, `resetZoom()`, `getState()`, `subscribe(fn)` and
|
|
82
|
+
`on(event, fn)` are all there. Leave `open` undefined when the ref drives visibility, or the next
|
|
83
|
+
change of `open` wins.
|
|
84
|
+
|
|
85
|
+
### Captions
|
|
86
|
+
|
|
87
|
+
`<Gallerist.Caption>` renders the current item's caption with React, into the renderer's caption
|
|
88
|
+
region: the caption bar under the stage, the editorial placard, or wherever the preset puts it. Item
|
|
89
|
+
data keeps its plain-text `caption`; the design lives here, once per viewer.
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
interface Shot {
|
|
93
|
+
app: string
|
|
94
|
+
capturedAt: string
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
<Gallerist groups={groups} open={open} onClose={close}>
|
|
98
|
+
<Gallerist.Caption>
|
|
99
|
+
{({ item, group, cursor }) => {
|
|
100
|
+
const shot = item.meta as Shot
|
|
101
|
+
return (
|
|
102
|
+
<div className="shot-caption">
|
|
103
|
+
<strong>{shot.app}</strong> <time>{shot.capturedAt}</time>
|
|
104
|
+
<span>
|
|
105
|
+
{cursor.i + 1} of {group.items.length}
|
|
106
|
+
</span>
|
|
107
|
+
</div>
|
|
108
|
+
)
|
|
109
|
+
}}
|
|
110
|
+
</Gallerist.Caption>
|
|
111
|
+
</Gallerist>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Returning `null`, `undefined`, `true` or `false` shows the item's text `caption` (or nothing), in a
|
|
115
|
+
`span.gallerist-caption-text` so it keeps the theme's caption font. The band grows with what you render
|
|
116
|
+
(the stage gives up the room), from the height of a two-line text caption up to 40% of the viewport
|
|
117
|
+
(less if the stage would drop below 36% of the viewer), then it scrolls. Its text is the viewer's
|
|
118
|
+
accessible description (`aria-describedby`); the announcement on navigation stays the position plus
|
|
119
|
+
the item's `alt`, so put what a screen reader user needs in `alt`. Without React, use
|
|
120
|
+
[`@gallerist/plugin-captions`](../plugin-captions/README.md).
|
|
121
|
+
|
|
122
|
+
### Hooks
|
|
123
|
+
|
|
124
|
+
- `useCurrentItem()` returns `{ item, group, cursor }` for the enclosing `<Gallerist>` (or pass it a
|
|
125
|
+
viewer). It re-renders on navigation and collection changes, not on zoom or pan.
|
|
126
|
+
- `useGallerist(viewer)` returns the whole state (`useSyncExternalStore`) and stable action functions.
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
function Details() {
|
|
130
|
+
const { item } = useCurrentItem()
|
|
131
|
+
return <p>{item?.alt}</p>
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Portals
|
|
136
|
+
|
|
137
|
+
`<Gallerist.Region name="toolbar">` renders JSX into a named region (`toolbar`, `aside`, `footer`,
|
|
138
|
+
`caption`, …); `<Gallerist.Overlay anchor={…}>` renders JSX as an overlay on the image or the viewport.
|
|
139
|
+
|
|
140
|
+
Full docs: the [monorepo README](https://github.com/shafayet9780/gallerist#readme) and [`docs/`](https://github.com/shafayet9780/gallerist/tree/main/docs).
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT © Shafayet Ahmmed
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
DEFAULT_STRINGS: () => import_renderer_dom2.DEFAULT_STRINGS,
|
|
25
|
+
Gallerist: () => Gallerist2,
|
|
26
|
+
GalleristContext: () => GalleristContext,
|
|
27
|
+
resolvePreset: () => import_renderer_dom2.resolvePreset,
|
|
28
|
+
useCurrentItem: () => useCurrentItem,
|
|
29
|
+
useGallerist: () => useGallerist,
|
|
30
|
+
useViewerContext: () => useViewerContext
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(index_exports);
|
|
33
|
+
var import_renderer_dom2 = require("@gallerist/renderer-dom");
|
|
34
|
+
|
|
35
|
+
// src/context.ts
|
|
36
|
+
var import_react = require("react");
|
|
37
|
+
var GalleristContext = (0, import_react.createContext)(null);
|
|
38
|
+
function useViewerContext() {
|
|
39
|
+
const viewer = (0, import_react.useContext)(GalleristContext);
|
|
40
|
+
if (!viewer)
|
|
41
|
+
throw new Error(
|
|
42
|
+
"Gallerist.Overlay / Gallerist.Region / Gallerist.Caption must be used inside <Gallerist>"
|
|
43
|
+
);
|
|
44
|
+
return viewer;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/useGallerist.ts
|
|
48
|
+
var import_react2 = require("react");
|
|
49
|
+
function useGallerist(viewer) {
|
|
50
|
+
const subscribe = (0, import_react2.useCallback)((cb) => viewer.subscribe(cb), [viewer]);
|
|
51
|
+
const getSnapshot = (0, import_react2.useCallback)(() => viewer.getState(), [viewer]);
|
|
52
|
+
const state = (0, import_react2.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
|
|
53
|
+
const actions = (0, import_react2.useMemo)(
|
|
54
|
+
() => ({
|
|
55
|
+
open: (at) => viewer.open(at),
|
|
56
|
+
close: () => viewer.close(),
|
|
57
|
+
next: () => viewer.next(),
|
|
58
|
+
prev: () => viewer.prev(),
|
|
59
|
+
nextItem: () => viewer.nextItem(),
|
|
60
|
+
prevItem: () => viewer.prevItem(),
|
|
61
|
+
goTo: (g, i) => viewer.goTo(g, i),
|
|
62
|
+
goToId: (g, i) => viewer.goToId(g, i),
|
|
63
|
+
zoomTo: (s, f) => viewer.zoomTo(s, f),
|
|
64
|
+
resetZoom: () => viewer.resetZoom()
|
|
65
|
+
}),
|
|
66
|
+
[viewer]
|
|
67
|
+
);
|
|
68
|
+
return { state, viewer, ...actions };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/useCurrentItem.ts
|
|
72
|
+
var import_react3 = require("react");
|
|
73
|
+
function useCurrentItem(viewer) {
|
|
74
|
+
const fromContext = (0, import_react3.useContext)(GalleristContext);
|
|
75
|
+
const v = viewer ?? fromContext;
|
|
76
|
+
if (!v) throw new Error("useCurrentItem() must be used inside <Gallerist> or given a viewer");
|
|
77
|
+
const last = (0, import_react3.useRef)(null);
|
|
78
|
+
const subscribe = (0, import_react3.useCallback)((cb) => v.subscribe(cb), [v]);
|
|
79
|
+
const getSnapshot = (0, import_react3.useCallback)(() => {
|
|
80
|
+
const { g, i } = v.getState().cursor;
|
|
81
|
+
const group = v.getCollection().groups[g];
|
|
82
|
+
const item = group?.items[i];
|
|
83
|
+
const prev = last.current;
|
|
84
|
+
if (prev && prev.item === item && prev.group === group && prev.cursor.g === g && prev.cursor.i === i)
|
|
85
|
+
return prev;
|
|
86
|
+
last.current = { item, group, cursor: { g, i } };
|
|
87
|
+
return last.current;
|
|
88
|
+
}, [v]);
|
|
89
|
+
return (0, import_react3.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/Gallerist.tsx
|
|
93
|
+
var import_core = require("@gallerist/core");
|
|
94
|
+
var import_renderer_dom = require("@gallerist/renderer-dom");
|
|
95
|
+
var import_react4 = require("react");
|
|
96
|
+
var import_react_dom = require("react-dom");
|
|
97
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
98
|
+
var EVENT_NAMES = Object.keys({
|
|
99
|
+
init: 1,
|
|
100
|
+
destroy: 1,
|
|
101
|
+
open: 1,
|
|
102
|
+
opened: 1,
|
|
103
|
+
close: 1,
|
|
104
|
+
closed: 1,
|
|
105
|
+
beforeSlide: 1,
|
|
106
|
+
slide: 1,
|
|
107
|
+
afterSlide: 1,
|
|
108
|
+
beforeItem: 1,
|
|
109
|
+
item: 1,
|
|
110
|
+
afterItem: 1,
|
|
111
|
+
zoom: 1,
|
|
112
|
+
zoomEnd: 1,
|
|
113
|
+
load: 1,
|
|
114
|
+
loaded: 1,
|
|
115
|
+
loadError: 1,
|
|
116
|
+
collectionChange: 1,
|
|
117
|
+
fullscreenChange: 1,
|
|
118
|
+
overlayClick: 1
|
|
119
|
+
});
|
|
120
|
+
var toCollection = (items, groups, collection) => collection ?? { groups: groups ?? (items ?? []).map((it) => ({ id: it.id, items: [it] })) };
|
|
121
|
+
var shallowSame = (a, b) => {
|
|
122
|
+
if (Object.is(a, b)) return true;
|
|
123
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
124
|
+
return a.length === b.length && a.every((v, i) => Object.is(v, b[i]));
|
|
125
|
+
}
|
|
126
|
+
if (!a || !b || typeof a !== "object" || typeof b !== "object") return false;
|
|
127
|
+
const ka = Object.keys(a);
|
|
128
|
+
const rb = b;
|
|
129
|
+
return ka.length === Object.keys(b).length && ka.every((k) => Object.is(a[k], rb[k]));
|
|
130
|
+
};
|
|
131
|
+
var sameRecord = (a, b, skip) => {
|
|
132
|
+
const ra = a;
|
|
133
|
+
const rb = b;
|
|
134
|
+
const ka = Object.keys(ra).filter((k) => k !== skip);
|
|
135
|
+
const kb = Object.keys(rb).filter((k) => k !== skip);
|
|
136
|
+
return ka.length === kb.length && ka.every((k) => shallowSame(ra[k], rb[k]));
|
|
137
|
+
};
|
|
138
|
+
var sameCollection = (a, b) => a.groups.length === b.groups.length && shallowSame(a.meta, b.meta) && a.groups.every((g, gi) => {
|
|
139
|
+
const h = b.groups[gi];
|
|
140
|
+
return !!h && sameRecord(g, h, "items") && g.items.length === h.items.length && g.items.every((it, ii) => {
|
|
141
|
+
const other = h.items[ii];
|
|
142
|
+
return !!other && sameRecord(it, other);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
var GalleristBase = (0, import_react4.forwardRef)(function Gallerist(props, ref) {
|
|
146
|
+
const {
|
|
147
|
+
open,
|
|
148
|
+
onClose,
|
|
149
|
+
container,
|
|
150
|
+
controls,
|
|
151
|
+
preset,
|
|
152
|
+
skin,
|
|
153
|
+
accent,
|
|
154
|
+
scheme,
|
|
155
|
+
arrows,
|
|
156
|
+
rail,
|
|
157
|
+
autoHide,
|
|
158
|
+
linear,
|
|
159
|
+
strings,
|
|
160
|
+
itemIndicator,
|
|
161
|
+
navigator,
|
|
162
|
+
frame,
|
|
163
|
+
groupAxis,
|
|
164
|
+
arrowLabels,
|
|
165
|
+
label,
|
|
166
|
+
returnFocus,
|
|
167
|
+
preload,
|
|
168
|
+
adapters,
|
|
169
|
+
children,
|
|
170
|
+
...options
|
|
171
|
+
} = props;
|
|
172
|
+
const rootRef = (0, import_react4.useRef)(null);
|
|
173
|
+
const { items, groups, collection, on } = options;
|
|
174
|
+
const onRef = (0, import_react4.useRef)(on);
|
|
175
|
+
(0, import_react4.useInsertionEffect)(() => {
|
|
176
|
+
onRef.current = on;
|
|
177
|
+
});
|
|
178
|
+
const [viewer] = (0, import_react4.useState)(() => {
|
|
179
|
+
const { plugins: _plugins, on: _on, ...rest } = options;
|
|
180
|
+
const forward = {};
|
|
181
|
+
for (const name of EVENT_NAMES) {
|
|
182
|
+
;
|
|
183
|
+
forward[name] = (payload) => onRef.current?.[name]?.(payload);
|
|
184
|
+
}
|
|
185
|
+
return (0, import_core.createViewer)({ ...rest, on: forward });
|
|
186
|
+
});
|
|
187
|
+
const appliedData = (0, import_react4.useRef)({ items, groups, collection });
|
|
188
|
+
(0, import_react4.useEffect)(() => {
|
|
189
|
+
const prev = appliedData.current;
|
|
190
|
+
if (prev.items === items && prev.groups === groups && prev.collection === collection) return;
|
|
191
|
+
appliedData.current = { items, groups, collection };
|
|
192
|
+
const next = toCollection(items, groups, collection);
|
|
193
|
+
if (sameCollection(next, viewer.getCollection())) return;
|
|
194
|
+
try {
|
|
195
|
+
viewer.setCollection(next);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.error("[gallerist] ignored an invalid collection update:", error);
|
|
198
|
+
}
|
|
199
|
+
}, [viewer, items, groups, collection]);
|
|
200
|
+
(0, import_react4.useImperativeHandle)(ref, () => viewer, [viewer]);
|
|
201
|
+
(0, import_react4.useEffect)(() => {
|
|
202
|
+
const disposers = (options.plugins ?? []).map((plugin) => viewer.use(plugin));
|
|
203
|
+
return () => {
|
|
204
|
+
for (const dispose of disposers) dispose();
|
|
205
|
+
};
|
|
206
|
+
}, [viewer]);
|
|
207
|
+
(0, import_react4.useEffect)(() => {
|
|
208
|
+
const handle = (0, import_renderer_dom.mount)(viewer, {
|
|
209
|
+
container: container ?? null,
|
|
210
|
+
controls,
|
|
211
|
+
preset,
|
|
212
|
+
arrows,
|
|
213
|
+
rail,
|
|
214
|
+
autoHide,
|
|
215
|
+
linear,
|
|
216
|
+
strings,
|
|
217
|
+
itemIndicator,
|
|
218
|
+
navigator,
|
|
219
|
+
frame,
|
|
220
|
+
groupAxis,
|
|
221
|
+
arrowLabels,
|
|
222
|
+
label,
|
|
223
|
+
returnFocus,
|
|
224
|
+
preload,
|
|
225
|
+
adapters,
|
|
226
|
+
skin,
|
|
227
|
+
accent,
|
|
228
|
+
scheme,
|
|
229
|
+
loop: options.loop
|
|
230
|
+
});
|
|
231
|
+
rootRef.current = handle.root;
|
|
232
|
+
return () => {
|
|
233
|
+
handle.destroy();
|
|
234
|
+
rootRef.current = null;
|
|
235
|
+
};
|
|
236
|
+
}, [viewer]);
|
|
237
|
+
(0, import_react4.useEffect)(() => {
|
|
238
|
+
const root = rootRef.current;
|
|
239
|
+
if (root) (0, import_renderer_dom.applyAppearance)(root, { skin, accent, scheme });
|
|
240
|
+
}, [skin, accent, scheme]);
|
|
241
|
+
const openG = typeof open === "object" && open ? open.g : void 0;
|
|
242
|
+
const openI = typeof open === "object" && open ? open.i : void 0;
|
|
243
|
+
const shown = open === void 0 ? void 0 : !!open;
|
|
244
|
+
(0, import_react4.useEffect)(() => {
|
|
245
|
+
if (shown === void 0) return;
|
|
246
|
+
if (shown) viewer.open(openG === void 0 ? void 0 : { g: openG, i: openI });
|
|
247
|
+
else viewer.close();
|
|
248
|
+
}, [shown, openG, openI, viewer]);
|
|
249
|
+
(0, import_react4.useEffect)(() => {
|
|
250
|
+
if (!onClose) return;
|
|
251
|
+
return viewer.on("closed", () => onClose());
|
|
252
|
+
}, [onClose, viewer]);
|
|
253
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GalleristContext.Provider, { value: viewer, children });
|
|
254
|
+
});
|
|
255
|
+
function Overlay(props) {
|
|
256
|
+
const viewer = useViewerContext();
|
|
257
|
+
const [host, setHost] = (0, import_react4.useState)(null);
|
|
258
|
+
const autoId = (0, import_react4.useId)();
|
|
259
|
+
const id = props.id ?? autoId;
|
|
260
|
+
const anchorKey = JSON.stringify(props.anchor);
|
|
261
|
+
const visibleAtKey = JSON.stringify(props.visibleAt ?? null);
|
|
262
|
+
(0, import_react4.useEffect)(() => {
|
|
263
|
+
const node = document.createElement("div");
|
|
264
|
+
setHost(node);
|
|
265
|
+
const spec = {
|
|
266
|
+
id,
|
|
267
|
+
anchor: props.anchor,
|
|
268
|
+
layer: props.layer,
|
|
269
|
+
interactive: props.interactive,
|
|
270
|
+
visibleAt: props.visibleAt,
|
|
271
|
+
class: props.className,
|
|
272
|
+
render: () => node
|
|
273
|
+
// stable host; React owns its children through the portal
|
|
274
|
+
};
|
|
275
|
+
const dispose = viewer.use(() => ({ name: `overlay:${id}`, overlays: [spec] }));
|
|
276
|
+
return () => {
|
|
277
|
+
dispose();
|
|
278
|
+
setHost(null);
|
|
279
|
+
};
|
|
280
|
+
}, [viewer, id, anchorKey, visibleAtKey, props.layer, props.interactive, props.className]);
|
|
281
|
+
return host ? (0, import_react_dom.createPortal)(props.children, host) : null;
|
|
282
|
+
}
|
|
283
|
+
function Region(props) {
|
|
284
|
+
const viewer = useViewerContext();
|
|
285
|
+
const [host, setHost] = (0, import_react4.useState)(null);
|
|
286
|
+
const { name } = props;
|
|
287
|
+
(0, import_react4.useEffect)(() => {
|
|
288
|
+
const node = document.createElement("div");
|
|
289
|
+
setHost(node);
|
|
290
|
+
const dispose = viewer.use(() => ({
|
|
291
|
+
name: `region:${name}`,
|
|
292
|
+
regions: { [name]: () => node }
|
|
293
|
+
}));
|
|
294
|
+
return () => {
|
|
295
|
+
dispose();
|
|
296
|
+
setHost(null);
|
|
297
|
+
};
|
|
298
|
+
}, [viewer, name]);
|
|
299
|
+
return host ? (0, import_react_dom.createPortal)(props.children, host) : null;
|
|
300
|
+
}
|
|
301
|
+
function Caption(props) {
|
|
302
|
+
const { item, group, cursor } = useCurrentItem();
|
|
303
|
+
const out = item && group ? props.children({ item, group, cursor }) : null;
|
|
304
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Region, { name: "caption", children: out == null || typeof out === "boolean" ? text(item?.caption) : out });
|
|
305
|
+
}
|
|
306
|
+
function text(caption) {
|
|
307
|
+
return typeof caption === "string" && caption ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "gallerist-caption-text", children: caption }) : null;
|
|
308
|
+
}
|
|
309
|
+
var Gallerist2 = Object.assign(GalleristBase, { Overlay, Region, Caption });
|
|
310
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
311
|
+
0 && (module.exports = {
|
|
312
|
+
DEFAULT_STRINGS,
|
|
313
|
+
Gallerist,
|
|
314
|
+
GalleristContext,
|
|
315
|
+
resolvePreset,
|
|
316
|
+
useCurrentItem,
|
|
317
|
+
useGallerist,
|
|
318
|
+
useViewerContext
|
|
319
|
+
});
|
|
320
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/context.ts","../src/useGallerist.ts","../src/useCurrentItem.ts","../src/Gallerist.tsx"],"sourcesContent":["// @gallerist/react — React adapter (spec §16). Thin binding over the headless store.\nexport {\n type Accent,\n type ArrowLayout,\n type ColorScheme,\n type GroupAxis,\n type ItemIndicator,\n type PresetName,\n type RailPlacement,\n type ResolvedPreset,\n type Skin,\n type Strings,\n DEFAULT_STRINGS,\n resolvePreset,\n} from '@gallerist/renderer-dom'\nexport { useViewerContext, GalleristContext } from './context'\nexport { type UseGallerist, useGallerist } from './useGallerist'\nexport { type CurrentItem, useCurrentItem } from './useCurrentItem'\nexport {\n Gallerist,\n type GalleristCaptionProps,\n type GalleristOpenAt,\n type GalleristOverlayProps,\n type GalleristProps,\n type GalleristRegionProps,\n} from './Gallerist'\n","import type { ViewerApi } from '@gallerist/core'\nimport { createContext, useContext } from 'react'\n\nexport const GalleristContext = createContext<ViewerApi | null>(null)\n\nexport function useViewerContext(): ViewerApi {\n const viewer = useContext(GalleristContext)\n if (!viewer)\n throw new Error(\n 'Gallerist.Overlay / Gallerist.Region / Gallerist.Caption must be used inside <Gallerist>',\n )\n return viewer\n}\n","import type { ViewerApi, ViewerState } from '@gallerist/core'\nimport { useCallback, useMemo, useSyncExternalStore } from 'react'\n\nexport interface UseGallerist {\n state: Readonly<ViewerState>\n viewer: ViewerApi\n open: ViewerApi['open']\n close: ViewerApi['close']\n next: ViewerApi['next']\n prev: ViewerApi['prev']\n nextItem: ViewerApi['nextItem']\n prevItem: ViewerApi['prevItem']\n goTo: ViewerApi['goTo']\n goToId: ViewerApi['goToId']\n zoomTo: ViewerApi['zoomTo']\n resetZoom: ViewerApi['resetZoom']\n}\n\n/** Bind a React component to a viewer's state (useSyncExternalStore) + its actions. */\nexport function useGallerist(viewer: ViewerApi): UseGallerist {\n const subscribe = useCallback((cb: () => void) => viewer.subscribe(cb), [viewer])\n const getSnapshot = useCallback(() => viewer.getState(), [viewer])\n const state = useSyncExternalStore(subscribe, getSnapshot, getSnapshot)\n\n // Stabilize the action closures so they keep a constant identity across re-renders (this hook\n // re-renders on every store commit). Otherwise each commit handed consumers fresh function\n // references, defeating React.memo / dependency-array memoization downstream (S-PERF4).\n const actions = useMemo(\n () => ({\n open: (at?: Parameters<ViewerApi['open']>[0]) => viewer.open(at),\n close: () => viewer.close(),\n next: () => viewer.next(),\n prev: () => viewer.prev(),\n nextItem: () => viewer.nextItem(),\n prevItem: () => viewer.prevItem(),\n goTo: (g: number, i?: number) => viewer.goTo(g, i),\n goToId: (g: string, i?: string) => viewer.goToId(g, i),\n zoomTo: (s: number, f?: Parameters<ViewerApi['zoomTo']>[1]) => viewer.zoomTo(s, f),\n resetZoom: () => viewer.resetZoom(),\n }),\n [viewer],\n )\n\n return { state, viewer, ...actions }\n}\n","import type { Cursor, Group, Item, ViewerApi } from '@gallerist/core'\nimport { useCallback, useContext, useRef, useSyncExternalStore } from 'react'\nimport { GalleristContext } from './context'\n\n/** The viewer's current item, its group and its position. */\nexport interface CurrentItem<GM = unknown, IM = unknown> {\n /** undefined only for an empty collection */\n item: Item<IM> | undefined\n group: Group<GM, IM> | undefined\n cursor: Readonly<Cursor>\n}\n\n/**\n * The current item, group and cursor of the enclosing `<Gallerist>` (or of `viewer`). Re-renders\n * when the cursor or the collection changes, not on zoom or pan.\n */\nexport function useCurrentItem<GM = unknown, IM = unknown>(\n viewer?: ViewerApi,\n): CurrentItem<GM, IM> {\n const fromContext = useContext(GalleristContext)\n const v = viewer ?? fromContext\n if (!v) throw new Error('useCurrentItem() must be used inside <Gallerist> or given a viewer')\n // useSyncExternalStore needs a stable snapshot: the same object until the item really changes\n const last = useRef<CurrentItem | null>(null)\n const subscribe = useCallback((cb: () => void) => v.subscribe(cb), [v])\n const getSnapshot = useCallback((): CurrentItem => {\n const { g, i } = v.getState().cursor\n const group = v.getCollection().groups[g]\n const item = group?.items[i]\n const prev = last.current\n if (\n prev &&\n prev.item === item &&\n prev.group === group &&\n prev.cursor.g === g &&\n prev.cursor.i === i\n )\n return prev\n last.current = { item, group, cursor: { g, i } }\n return last.current\n }, [v])\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot) as CurrentItem<GM, IM>\n}\n","import {\n type Collection,\n type Cursor,\n type EventHandlers,\n type EventMap,\n type Group,\n type Item,\n type OverlaySpec,\n type RegionName,\n type ViewerApi,\n type ViewerOptions,\n createViewer,\n} from '@gallerist/core'\nimport {\n type Accent,\n type ArrowLayout,\n type ColorScheme,\n type ItemIndicator,\n type MountOptions,\n type PresetName,\n type RailPlacement,\n type Skin,\n type Strings,\n applyAppearance,\n mount,\n} from '@gallerist/renderer-dom'\nimport {\n type ReactNode,\n forwardRef,\n useEffect,\n useId,\n useImperativeHandle,\n useInsertionEffect,\n useRef,\n useState,\n} from 'react'\nimport { createPortal } from 'react-dom'\nimport { GalleristContext, useViewerContext } from './context'\nimport { useCurrentItem } from './useCurrentItem'\n\n/** A position to open at: group `g`, item `i` (default: the group's remembered item). */\nexport interface GalleristOpenAt {\n g: number\n i?: number\n}\n\n/**\n * `items` / `groups` / `collection` and `on` are reactive. Changed data swaps the collection in\n * place (diffed by id, keeping the cursor); data rebuilt with the same content is ignored. Every other viewer option\n * (`loop`, `itemMemory`, `startAt`, `plugins`, `overlays`, …) and the layout props (`container`,\n * `preset`, `arrows`, `rail`, `label`, `preload`, `adapters`, …) are read once; change the\n * component's `key` to apply new ones. `skin`, `accent`, `scheme` and `open` are reactive.\n */\nexport interface GalleristProps extends ViewerOptions {\n /**\n * Controlled visibility. `true` opens at the current position; `{ g, i }` opens there, and a new\n * position while open moves the cursor (applied when the value changes, so the user can still\n * navigate away from it); `false` closes. Leave it undefined to drive the viewer through the ref.\n */\n open?: boolean | GalleristOpenAt\n onClose?: () => void\n container?: HTMLElement | null\n controls?: boolean\n /** layout preset (arrow placement, auto-hide, data-gallerist-preset, the thumbnails() defaults) — UX-E. */\n preset?: PresetName\n /** visual skin, default `gallery`; other skins need their stylesheet (reactive). */\n skin?: Skin\n /** the skin's accent by name, e.g. `mint` (Gallery) or `cyan` (Console) (reactive). */\n accent?: Accent\n /** colour scheme: `dark`, `light` or `auto`; unset, the skin's own default (reactive). */\n scheme?: ColorScheme\n /** within-group arrow placement (overrides the preset default) — UX-D. */\n arrows?: ArrowLayout\n /** thumbnail rail placement (overrides the preset default) — UX-E. */\n rail?: RailPlacement\n /** auto-hide chrome on idle (overrides the preset default) — UX-C. */\n autoHide?: boolean\n /** linear flat traversal: ←/→ walks the flattened item sequence; pairs with a flat thumbnail rail. */\n linear?: boolean\n /** UI text overrides (button labels, announcements) for translation; read once at mount. */\n strings?: Partial<Strings>\n /** item position indicator: `pills` (default), `dots`, `counter` or `placard`; read once at mount. */\n itemIndicator?: ItemIndicator\n /** zoom navigator while zoomed in (overrides the preset default); read once at mount. */\n navigator?: boolean\n /** framed media: the work hangs with room around it (overrides the preset); read once at mount. */\n frame?: boolean\n /** which arrows move between groups (overrides the rail and preset default); read once at mount. */\n groupAxis?: MountOptions['groupAxis']\n /** name the group arrows after their target group (overrides the preset); read once at mount. */\n arrowLabels?: MountOptions['arrowLabels']\n /** the overlay dialog's accessible name (default `strings.dialog`); read once at mount. */\n label?: string\n /** where focus returns when the overlay closes (default: what had focus when it opened); read\n * once at mount, so pass a getter for a target that changes. */\n returnFocus?: MountOptions['returnFocus']\n /** how many groups to keep mounted ahead of and behind the current one; read once at mount. */\n preload?: MountOptions['preload']\n /** media adapters, replacing the default image adapter; read once at mount. */\n adapters?: MountOptions['adapters']\n children?: ReactNode\n}\n\n// Every viewer event, checked against EventMap (a missing or unknown name fails to compile), so\n// the forwarders below can route each one to the latest `on` handlers.\nconst EVENT_NAMES = Object.keys({\n init: 1,\n destroy: 1,\n open: 1,\n opened: 1,\n close: 1,\n closed: 1,\n beforeSlide: 1,\n slide: 1,\n afterSlide: 1,\n beforeItem: 1,\n item: 1,\n afterItem: 1,\n zoom: 1,\n zoomEnd: 1,\n load: 1,\n loaded: 1,\n loadError: 1,\n collectionChange: 1,\n fullscreenChange: 1,\n overlayClick: 1,\n} satisfies Record<keyof EventMap, 1>) as (keyof EventMap)[]\n\n// The collection a data prop describes (flat items → singleton groups). setCollection validates it.\nconst toCollection = (items?: Item[], groups?: Group[], collection?: Collection): Collection =>\n collection ?? { groups: groups ?? (items ?? []).map((it) => ({ id: it.id, items: [it] })) }\n\n// Values equal at one level of depth: identical, or arrays/plain objects with identical entries\n// (a `source: { src }`, `naturalSize`, or a re-created `overlays` array of the same specs).\nconst shallowSame = (a: unknown, b: unknown): boolean => {\n if (Object.is(a, b)) return true\n if (Array.isArray(a) && Array.isArray(b)) {\n return a.length === b.length && a.every((v, i) => Object.is(v, b[i]))\n }\n if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return false\n const ka = Object.keys(a)\n const rb = b as Record<string, unknown>\n return (\n ka.length === Object.keys(b).length &&\n ka.every((k) => Object.is((a as Record<string, unknown>)[k], rb[k]))\n )\n}\nconst sameRecord = (a: object, b: object, skip?: string): boolean => {\n const ra = a as Record<string, unknown>\n const rb = b as Record<string, unknown>\n const ka = Object.keys(ra).filter((k) => k !== skip)\n const kb = Object.keys(rb).filter((k) => k !== skip)\n return ka.length === kb.length && ka.every((k) => shallowSame(ra[k], rb[k]))\n}\n// Same content (groups, items and their fields), even when rebuilt as fresh objects — so an\n// inline `items={photos.map(…)}` doesn't swap the collection (and reset load status) every render.\nconst sameCollection = (a: Collection, b: Readonly<Collection>): boolean =>\n a.groups.length === b.groups.length &&\n shallowSame(a.meta, b.meta) &&\n a.groups.every((g, gi) => {\n const h = b.groups[gi]\n return (\n !!h &&\n sameRecord(g, h, 'items') &&\n g.items.length === h.items.length &&\n g.items.every((it, ii) => {\n const other = h.items[ii]\n return !!other && sameRecord(it, other)\n })\n )\n })\n\nconst GalleristBase = forwardRef<ViewerApi, GalleristProps>(function Gallerist(props, ref) {\n const {\n open,\n onClose,\n container,\n controls,\n preset,\n skin,\n accent,\n scheme,\n arrows,\n rail,\n autoHide,\n linear,\n strings,\n itemIndicator,\n navigator,\n frame,\n groupAxis,\n arrowLabels,\n label,\n returnFocus,\n preload,\n adapters,\n children,\n ...options\n } = props\n const rootRef = useRef<HTMLElement | null>(null)\n const { items, groups, collection, on } = options\n\n // The viewer outlives renders, so its handlers must not capture the first render's `on`: they\n // forward to whatever `on` the latest committed render passed (a returned `false` still cancels).\n const onRef = useRef<EventHandlers | undefined>(on)\n useInsertionEffect(() => {\n onRef.current = on\n })\n // Create the viewer once (core is DOM-free → SSR-safe); options are read at construction. Lazy\n // useState (not useMemo, which React may discard) guarantees a single stable instance. Plugins\n // are deliberately NOT registered at construction — they're managed in an effect below so their\n // teardown runs on unmount (I-X2).\n const [viewer] = useState(() => {\n const { plugins: _plugins, on: _on, ...rest } = options\n const forward: EventHandlers = {}\n for (const name of EVENT_NAMES) {\n ;(forward as Record<string, (p: unknown) => unknown>)[name] = (payload) =>\n (onRef.current?.[name] as ((p: unknown) => unknown) | undefined)?.(payload)\n }\n return createViewer({ ...rest, on: forward })\n })\n\n // Data props are reactive: new data swaps the collection via setCollection (diffed by id, so the\n // cursor and zoom survive when the current item does). The first render's data is already in the\n // viewer; a new reference with the same content is ignored; invalid data (a duplicate id) is\n // reported and the viewer keeps its current collection instead of crashing the React tree.\n const appliedData = useRef({ items, groups, collection })\n useEffect(() => {\n const prev = appliedData.current\n if (prev.items === items && prev.groups === groups && prev.collection === collection) return\n appliedData.current = { items, groups, collection }\n const next = toCollection(items, groups, collection)\n if (sameCollection(next, viewer.getCollection())) return\n try {\n viewer.setCollection(next)\n } catch (error) {\n console.error('[gallerist] ignored an invalid collection update:', error)\n }\n }, [viewer, items, groups, collection])\n useImperativeHandle(ref, () => viewer, [viewer])\n\n // Register the construction-time plugins via use() and dispose them on unmount, so their\n // init()-created store subscriptions / observers / DOM don't leak on every remount (the viewer\n // itself is intentionally never destroyed — see the mount effect). Plugins are mount-time config.\n // biome-ignore lint/correctness/useExhaustiveDependencies: register once for the viewer's life\n useEffect(() => {\n const disposers = (options.plugins ?? []).map((plugin) => viewer.use(plugin))\n return () => {\n for (const dispose of disposers) dispose()\n }\n }, [viewer])\n\n // Mount the renderer once per viewer (client-side); container/controls are mount-time only. Only\n // the renderer (the DOM holder) is torn down here. The viewer is NOT destroyed on cleanup: under\n // StrictMode the simulated unmount would run viewer.destroy() on the stable useState instance,\n // permanently poisoning it (registerPlugin/nav short-circuit once destroyed) so re-registered\n // overlays/regions vanish. The viewer is plain JS (no DOM) and is reclaimed by GC once the\n // component unmounts and the renderer/hook/overlay subscriptions have all cleaned themselves up.\n // biome-ignore lint/correctness/useExhaustiveDependencies: intentional mount-once (layout opts read once)\n useEffect(() => {\n const handle = mount(viewer, {\n container: container ?? null,\n controls,\n preset,\n arrows,\n rail,\n autoHide,\n linear,\n strings,\n itemIndicator,\n navigator,\n frame,\n groupAxis,\n arrowLabels,\n label,\n returnFocus,\n preload,\n adapters,\n skin,\n accent,\n scheme,\n loop: options.loop,\n })\n rootRef.current = handle.root\n return () => {\n handle.destroy()\n rootRef.current = null\n }\n }, [viewer])\n\n // The appearance is reactive: the skin files key off root attributes, so no remount is needed.\n useEffect(() => {\n const root = rootRef.current\n if (root) applyAppearance(root, { skin, accent, scheme })\n }, [skin, accent, scheme])\n\n // `open` is compared by value, so an inline `open={{ g: 2 }}` re-applies only when g or i change\n const openG = typeof open === 'object' && open ? open.g : undefined\n const openI = typeof open === 'object' && open ? open.i : undefined\n const shown = open === undefined ? undefined : !!open\n useEffect(() => {\n if (shown === undefined) return\n // open({ g, i }) on a viewer already showing is a navigation (core)\n if (shown) viewer.open(openG === undefined ? undefined : { g: openG, i: openI })\n else viewer.close()\n }, [shown, openG, openI, viewer])\n\n useEffect(() => {\n if (!onClose) return\n return viewer.on('closed', () => onClose())\n }, [onClose, viewer])\n\n return <GalleristContext.Provider value={viewer}>{children}</GalleristContext.Provider>\n})\n\nexport interface GalleristOverlayProps {\n anchor: OverlaySpec['anchor']\n id?: string\n layer?: number\n interactive?: boolean\n visibleAt?: [number, number]\n className?: string\n children?: ReactNode\n}\n\n/** Render a React subtree as an image-space / viewport overlay via a portal (decision P0-G). */\nfunction Overlay(props: GalleristOverlayProps): ReactNode {\n const viewer = useViewerContext()\n const [host, setHost] = useState<HTMLElement | null>(null)\n // useId is SSR-deterministic (server + client agree), unlike a module-global counter.\n const autoId = useId()\n const id = props.id ?? autoId\n\n // Depend on the VALUE of the object/array props, not their identity. The common usage passes\n // inline literals (`anchor={{ kind: 'viewport', … }}`) that are a fresh reference every render;\n // keying on identity disposed+re-registered the overlay plugin on every parent render, forcing a\n // full overlay repaint. Serializing makes re-registration happen only on a real change (I-X3).\n const anchorKey = JSON.stringify(props.anchor)\n const visibleAtKey = JSON.stringify(props.visibleAt ?? null)\n // biome-ignore lint/correctness/useExhaustiveDependencies: anchor/visibleAt tracked by value key\n useEffect(() => {\n const node = document.createElement('div')\n setHost(node)\n const spec: OverlaySpec = {\n id,\n anchor: props.anchor,\n layer: props.layer,\n interactive: props.interactive,\n visibleAt: props.visibleAt,\n class: props.className,\n render: () => node, // stable host; React owns its children through the portal\n }\n const dispose = viewer.use(() => ({ name: `overlay:${id}`, overlays: [spec] }))\n return () => {\n dispose()\n setHost(null)\n }\n }, [viewer, id, anchorKey, visibleAtKey, props.layer, props.interactive, props.className])\n\n return host ? createPortal(props.children, host) : null\n}\n\nexport interface GalleristRegionProps {\n name: RegionName\n children?: ReactNode\n}\n\n/** Render a React subtree into a named renderer region (toolbar, footer, …) via a portal. */\nfunction Region(props: GalleristRegionProps): ReactNode {\n const viewer = useViewerContext()\n const [host, setHost] = useState<HTMLElement | null>(null)\n const { name } = props\n\n useEffect(() => {\n const node = document.createElement('div')\n setHost(node)\n const dispose = viewer.use(() => ({\n name: `region:${name}`,\n regions: { [name]: () => node },\n }))\n return () => {\n dispose()\n setHost(null)\n }\n }, [viewer, name])\n\n return host ? createPortal(props.children, host) : null\n}\n\nexport interface GalleristCaptionProps<GM = unknown, IM = unknown> {\n /**\n * the caption for the current item; `null`, `undefined`, `true` or `false` shows its text\n * `caption` (in a `span.gallerist-caption-text`, the theme's caption type)\n */\n children: (current: {\n item: Item<IM>\n group: Group<GM, IM>\n cursor: Readonly<Cursor>\n }) => ReactNode\n}\n\n/**\n * Render the caption for the current item with React, into the renderer's caption region (the\n * caption bar, the editorial placard, wherever the preset puts it). Re-renders on navigation and\n * collection changes. Its text is the viewer's accessible description, like a text caption.\n */\nfunction Caption<GM = unknown, IM = unknown>(props: GalleristCaptionProps<GM, IM>): ReactNode {\n const { item, group, cursor } = useCurrentItem<GM, IM>()\n const out = item && group ? props.children({ item, group, cursor }) : null\n return (\n <Region name=\"caption\">\n {out == null || typeof out === 'boolean' ? text(item?.caption) : out}\n </Region>\n )\n}\n\n// The item's own text caption, styled like the bar's text. Untyped data (JSON, a CDN page) can still\n// carry an old object-shaped caption, which React would throw on: anything but a string is none.\nfunction text(caption: unknown): ReactNode {\n return typeof caption === 'string' && caption ? (\n <span className=\"gallerist-caption-text\">{caption}</span>\n ) : null\n}\n\nexport const Gallerist = Object.assign(GalleristBase, { Overlay, Region, Caption })\n"],"mappings":"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","names":["Gallerist","import_renderer_dom","import_react","import_react","import_react","Gallerist"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { PresetName, Skin, Accent, ColorScheme, ArrowLayout, RailPlacement, Strings, ItemIndicator, MountOptions } from '@gallerist/renderer-dom';
|
|
2
|
+
export { Accent, ArrowLayout, ColorScheme, DEFAULT_STRINGS, GroupAxis, ItemIndicator, PresetName, RailPlacement, ResolvedPreset, Skin, Strings, resolvePreset } from '@gallerist/renderer-dom';
|
|
3
|
+
import * as react from 'react';
|
|
4
|
+
import { ReactNode } from 'react';
|
|
5
|
+
import { ViewerApi, ViewerState, Item, Group, Cursor, ViewerOptions, OverlaySpec, RegionName } from '@gallerist/core';
|
|
6
|
+
|
|
7
|
+
declare const GalleristContext: react.Context<ViewerApi | null>;
|
|
8
|
+
declare function useViewerContext(): ViewerApi;
|
|
9
|
+
|
|
10
|
+
interface UseGallerist {
|
|
11
|
+
state: Readonly<ViewerState>;
|
|
12
|
+
viewer: ViewerApi;
|
|
13
|
+
open: ViewerApi['open'];
|
|
14
|
+
close: ViewerApi['close'];
|
|
15
|
+
next: ViewerApi['next'];
|
|
16
|
+
prev: ViewerApi['prev'];
|
|
17
|
+
nextItem: ViewerApi['nextItem'];
|
|
18
|
+
prevItem: ViewerApi['prevItem'];
|
|
19
|
+
goTo: ViewerApi['goTo'];
|
|
20
|
+
goToId: ViewerApi['goToId'];
|
|
21
|
+
zoomTo: ViewerApi['zoomTo'];
|
|
22
|
+
resetZoom: ViewerApi['resetZoom'];
|
|
23
|
+
}
|
|
24
|
+
/** Bind a React component to a viewer's state (useSyncExternalStore) + its actions. */
|
|
25
|
+
declare function useGallerist(viewer: ViewerApi): UseGallerist;
|
|
26
|
+
|
|
27
|
+
/** The viewer's current item, its group and its position. */
|
|
28
|
+
interface CurrentItem<GM = unknown, IM = unknown> {
|
|
29
|
+
/** undefined only for an empty collection */
|
|
30
|
+
item: Item<IM> | undefined;
|
|
31
|
+
group: Group<GM, IM> | undefined;
|
|
32
|
+
cursor: Readonly<Cursor>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The current item, group and cursor of the enclosing `<Gallerist>` (or of `viewer`). Re-renders
|
|
36
|
+
* when the cursor or the collection changes, not on zoom or pan.
|
|
37
|
+
*/
|
|
38
|
+
declare function useCurrentItem<GM = unknown, IM = unknown>(viewer?: ViewerApi): CurrentItem<GM, IM>;
|
|
39
|
+
|
|
40
|
+
/** A position to open at: group `g`, item `i` (default: the group's remembered item). */
|
|
41
|
+
interface GalleristOpenAt {
|
|
42
|
+
g: number;
|
|
43
|
+
i?: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* `items` / `groups` / `collection` and `on` are reactive. Changed data swaps the collection in
|
|
47
|
+
* place (diffed by id, keeping the cursor); data rebuilt with the same content is ignored. Every other viewer option
|
|
48
|
+
* (`loop`, `itemMemory`, `startAt`, `plugins`, `overlays`, …) and the layout props (`container`,
|
|
49
|
+
* `preset`, `arrows`, `rail`, `label`, `preload`, `adapters`, …) are read once; change the
|
|
50
|
+
* component's `key` to apply new ones. `skin`, `accent`, `scheme` and `open` are reactive.
|
|
51
|
+
*/
|
|
52
|
+
interface GalleristProps extends ViewerOptions {
|
|
53
|
+
/**
|
|
54
|
+
* Controlled visibility. `true` opens at the current position; `{ g, i }` opens there, and a new
|
|
55
|
+
* position while open moves the cursor (applied when the value changes, so the user can still
|
|
56
|
+
* navigate away from it); `false` closes. Leave it undefined to drive the viewer through the ref.
|
|
57
|
+
*/
|
|
58
|
+
open?: boolean | GalleristOpenAt;
|
|
59
|
+
onClose?: () => void;
|
|
60
|
+
container?: HTMLElement | null;
|
|
61
|
+
controls?: boolean;
|
|
62
|
+
/** layout preset (arrow placement, auto-hide, data-gallerist-preset, the thumbnails() defaults) — UX-E. */
|
|
63
|
+
preset?: PresetName;
|
|
64
|
+
/** visual skin, default `gallery`; other skins need their stylesheet (reactive). */
|
|
65
|
+
skin?: Skin;
|
|
66
|
+
/** the skin's accent by name, e.g. `mint` (Gallery) or `cyan` (Console) (reactive). */
|
|
67
|
+
accent?: Accent;
|
|
68
|
+
/** colour scheme: `dark`, `light` or `auto`; unset, the skin's own default (reactive). */
|
|
69
|
+
scheme?: ColorScheme;
|
|
70
|
+
/** within-group arrow placement (overrides the preset default) — UX-D. */
|
|
71
|
+
arrows?: ArrowLayout;
|
|
72
|
+
/** thumbnail rail placement (overrides the preset default) — UX-E. */
|
|
73
|
+
rail?: RailPlacement;
|
|
74
|
+
/** auto-hide chrome on idle (overrides the preset default) — UX-C. */
|
|
75
|
+
autoHide?: boolean;
|
|
76
|
+
/** linear flat traversal: ←/→ walks the flattened item sequence; pairs with a flat thumbnail rail. */
|
|
77
|
+
linear?: boolean;
|
|
78
|
+
/** UI text overrides (button labels, announcements) for translation; read once at mount. */
|
|
79
|
+
strings?: Partial<Strings>;
|
|
80
|
+
/** item position indicator: `pills` (default), `dots`, `counter` or `placard`; read once at mount. */
|
|
81
|
+
itemIndicator?: ItemIndicator;
|
|
82
|
+
/** zoom navigator while zoomed in (overrides the preset default); read once at mount. */
|
|
83
|
+
navigator?: boolean;
|
|
84
|
+
/** framed media: the work hangs with room around it (overrides the preset); read once at mount. */
|
|
85
|
+
frame?: boolean;
|
|
86
|
+
/** which arrows move between groups (overrides the rail and preset default); read once at mount. */
|
|
87
|
+
groupAxis?: MountOptions['groupAxis'];
|
|
88
|
+
/** name the group arrows after their target group (overrides the preset); read once at mount. */
|
|
89
|
+
arrowLabels?: MountOptions['arrowLabels'];
|
|
90
|
+
/** the overlay dialog's accessible name (default `strings.dialog`); read once at mount. */
|
|
91
|
+
label?: string;
|
|
92
|
+
/** where focus returns when the overlay closes (default: what had focus when it opened); read
|
|
93
|
+
* once at mount, so pass a getter for a target that changes. */
|
|
94
|
+
returnFocus?: MountOptions['returnFocus'];
|
|
95
|
+
/** how many groups to keep mounted ahead of and behind the current one; read once at mount. */
|
|
96
|
+
preload?: MountOptions['preload'];
|
|
97
|
+
/** media adapters, replacing the default image adapter; read once at mount. */
|
|
98
|
+
adapters?: MountOptions['adapters'];
|
|
99
|
+
children?: ReactNode;
|
|
100
|
+
}
|
|
101
|
+
interface GalleristOverlayProps {
|
|
102
|
+
anchor: OverlaySpec['anchor'];
|
|
103
|
+
id?: string;
|
|
104
|
+
layer?: number;
|
|
105
|
+
interactive?: boolean;
|
|
106
|
+
visibleAt?: [number, number];
|
|
107
|
+
className?: string;
|
|
108
|
+
children?: ReactNode;
|
|
109
|
+
}
|
|
110
|
+
/** Render a React subtree as an image-space / viewport overlay via a portal (decision P0-G). */
|
|
111
|
+
declare function Overlay(props: GalleristOverlayProps): ReactNode;
|
|
112
|
+
interface GalleristRegionProps {
|
|
113
|
+
name: RegionName;
|
|
114
|
+
children?: ReactNode;
|
|
115
|
+
}
|
|
116
|
+
/** Render a React subtree into a named renderer region (toolbar, footer, …) via a portal. */
|
|
117
|
+
declare function Region(props: GalleristRegionProps): ReactNode;
|
|
118
|
+
interface GalleristCaptionProps<GM = unknown, IM = unknown> {
|
|
119
|
+
/**
|
|
120
|
+
* the caption for the current item; `null`, `undefined`, `true` or `false` shows its text
|
|
121
|
+
* `caption` (in a `span.gallerist-caption-text`, the theme's caption type)
|
|
122
|
+
*/
|
|
123
|
+
children: (current: {
|
|
124
|
+
item: Item<IM>;
|
|
125
|
+
group: Group<GM, IM>;
|
|
126
|
+
cursor: Readonly<Cursor>;
|
|
127
|
+
}) => ReactNode;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Render the caption for the current item with React, into the renderer's caption region (the
|
|
131
|
+
* caption bar, the editorial placard, wherever the preset puts it). Re-renders on navigation and
|
|
132
|
+
* collection changes. Its text is the viewer's accessible description, like a text caption.
|
|
133
|
+
*/
|
|
134
|
+
declare function Caption<GM = unknown, IM = unknown>(props: GalleristCaptionProps<GM, IM>): ReactNode;
|
|
135
|
+
declare const Gallerist: react.ForwardRefExoticComponent<GalleristProps & react.RefAttributes<ViewerApi>> & {
|
|
136
|
+
Overlay: typeof Overlay;
|
|
137
|
+
Region: typeof Region;
|
|
138
|
+
Caption: typeof Caption;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export { type CurrentItem, Gallerist, type GalleristCaptionProps, GalleristContext, type GalleristOpenAt, type GalleristOverlayProps, type GalleristProps, type GalleristRegionProps, type UseGallerist, useCurrentItem, useGallerist, useViewerContext };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { PresetName, Skin, Accent, ColorScheme, ArrowLayout, RailPlacement, Strings, ItemIndicator, MountOptions } from '@gallerist/renderer-dom';
|
|
2
|
+
export { Accent, ArrowLayout, ColorScheme, DEFAULT_STRINGS, GroupAxis, ItemIndicator, PresetName, RailPlacement, ResolvedPreset, Skin, Strings, resolvePreset } from '@gallerist/renderer-dom';
|
|
3
|
+
import * as react from 'react';
|
|
4
|
+
import { ReactNode } from 'react';
|
|
5
|
+
import { ViewerApi, ViewerState, Item, Group, Cursor, ViewerOptions, OverlaySpec, RegionName } from '@gallerist/core';
|
|
6
|
+
|
|
7
|
+
declare const GalleristContext: react.Context<ViewerApi | null>;
|
|
8
|
+
declare function useViewerContext(): ViewerApi;
|
|
9
|
+
|
|
10
|
+
interface UseGallerist {
|
|
11
|
+
state: Readonly<ViewerState>;
|
|
12
|
+
viewer: ViewerApi;
|
|
13
|
+
open: ViewerApi['open'];
|
|
14
|
+
close: ViewerApi['close'];
|
|
15
|
+
next: ViewerApi['next'];
|
|
16
|
+
prev: ViewerApi['prev'];
|
|
17
|
+
nextItem: ViewerApi['nextItem'];
|
|
18
|
+
prevItem: ViewerApi['prevItem'];
|
|
19
|
+
goTo: ViewerApi['goTo'];
|
|
20
|
+
goToId: ViewerApi['goToId'];
|
|
21
|
+
zoomTo: ViewerApi['zoomTo'];
|
|
22
|
+
resetZoom: ViewerApi['resetZoom'];
|
|
23
|
+
}
|
|
24
|
+
/** Bind a React component to a viewer's state (useSyncExternalStore) + its actions. */
|
|
25
|
+
declare function useGallerist(viewer: ViewerApi): UseGallerist;
|
|
26
|
+
|
|
27
|
+
/** The viewer's current item, its group and its position. */
|
|
28
|
+
interface CurrentItem<GM = unknown, IM = unknown> {
|
|
29
|
+
/** undefined only for an empty collection */
|
|
30
|
+
item: Item<IM> | undefined;
|
|
31
|
+
group: Group<GM, IM> | undefined;
|
|
32
|
+
cursor: Readonly<Cursor>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The current item, group and cursor of the enclosing `<Gallerist>` (or of `viewer`). Re-renders
|
|
36
|
+
* when the cursor or the collection changes, not on zoom or pan.
|
|
37
|
+
*/
|
|
38
|
+
declare function useCurrentItem<GM = unknown, IM = unknown>(viewer?: ViewerApi): CurrentItem<GM, IM>;
|
|
39
|
+
|
|
40
|
+
/** A position to open at: group `g`, item `i` (default: the group's remembered item). */
|
|
41
|
+
interface GalleristOpenAt {
|
|
42
|
+
g: number;
|
|
43
|
+
i?: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* `items` / `groups` / `collection` and `on` are reactive. Changed data swaps the collection in
|
|
47
|
+
* place (diffed by id, keeping the cursor); data rebuilt with the same content is ignored. Every other viewer option
|
|
48
|
+
* (`loop`, `itemMemory`, `startAt`, `plugins`, `overlays`, …) and the layout props (`container`,
|
|
49
|
+
* `preset`, `arrows`, `rail`, `label`, `preload`, `adapters`, …) are read once; change the
|
|
50
|
+
* component's `key` to apply new ones. `skin`, `accent`, `scheme` and `open` are reactive.
|
|
51
|
+
*/
|
|
52
|
+
interface GalleristProps extends ViewerOptions {
|
|
53
|
+
/**
|
|
54
|
+
* Controlled visibility. `true` opens at the current position; `{ g, i }` opens there, and a new
|
|
55
|
+
* position while open moves the cursor (applied when the value changes, so the user can still
|
|
56
|
+
* navigate away from it); `false` closes. Leave it undefined to drive the viewer through the ref.
|
|
57
|
+
*/
|
|
58
|
+
open?: boolean | GalleristOpenAt;
|
|
59
|
+
onClose?: () => void;
|
|
60
|
+
container?: HTMLElement | null;
|
|
61
|
+
controls?: boolean;
|
|
62
|
+
/** layout preset (arrow placement, auto-hide, data-gallerist-preset, the thumbnails() defaults) — UX-E. */
|
|
63
|
+
preset?: PresetName;
|
|
64
|
+
/** visual skin, default `gallery`; other skins need their stylesheet (reactive). */
|
|
65
|
+
skin?: Skin;
|
|
66
|
+
/** the skin's accent by name, e.g. `mint` (Gallery) or `cyan` (Console) (reactive). */
|
|
67
|
+
accent?: Accent;
|
|
68
|
+
/** colour scheme: `dark`, `light` or `auto`; unset, the skin's own default (reactive). */
|
|
69
|
+
scheme?: ColorScheme;
|
|
70
|
+
/** within-group arrow placement (overrides the preset default) — UX-D. */
|
|
71
|
+
arrows?: ArrowLayout;
|
|
72
|
+
/** thumbnail rail placement (overrides the preset default) — UX-E. */
|
|
73
|
+
rail?: RailPlacement;
|
|
74
|
+
/** auto-hide chrome on idle (overrides the preset default) — UX-C. */
|
|
75
|
+
autoHide?: boolean;
|
|
76
|
+
/** linear flat traversal: ←/→ walks the flattened item sequence; pairs with a flat thumbnail rail. */
|
|
77
|
+
linear?: boolean;
|
|
78
|
+
/** UI text overrides (button labels, announcements) for translation; read once at mount. */
|
|
79
|
+
strings?: Partial<Strings>;
|
|
80
|
+
/** item position indicator: `pills` (default), `dots`, `counter` or `placard`; read once at mount. */
|
|
81
|
+
itemIndicator?: ItemIndicator;
|
|
82
|
+
/** zoom navigator while zoomed in (overrides the preset default); read once at mount. */
|
|
83
|
+
navigator?: boolean;
|
|
84
|
+
/** framed media: the work hangs with room around it (overrides the preset); read once at mount. */
|
|
85
|
+
frame?: boolean;
|
|
86
|
+
/** which arrows move between groups (overrides the rail and preset default); read once at mount. */
|
|
87
|
+
groupAxis?: MountOptions['groupAxis'];
|
|
88
|
+
/** name the group arrows after their target group (overrides the preset); read once at mount. */
|
|
89
|
+
arrowLabels?: MountOptions['arrowLabels'];
|
|
90
|
+
/** the overlay dialog's accessible name (default `strings.dialog`); read once at mount. */
|
|
91
|
+
label?: string;
|
|
92
|
+
/** where focus returns when the overlay closes (default: what had focus when it opened); read
|
|
93
|
+
* once at mount, so pass a getter for a target that changes. */
|
|
94
|
+
returnFocus?: MountOptions['returnFocus'];
|
|
95
|
+
/** how many groups to keep mounted ahead of and behind the current one; read once at mount. */
|
|
96
|
+
preload?: MountOptions['preload'];
|
|
97
|
+
/** media adapters, replacing the default image adapter; read once at mount. */
|
|
98
|
+
adapters?: MountOptions['adapters'];
|
|
99
|
+
children?: ReactNode;
|
|
100
|
+
}
|
|
101
|
+
interface GalleristOverlayProps {
|
|
102
|
+
anchor: OverlaySpec['anchor'];
|
|
103
|
+
id?: string;
|
|
104
|
+
layer?: number;
|
|
105
|
+
interactive?: boolean;
|
|
106
|
+
visibleAt?: [number, number];
|
|
107
|
+
className?: string;
|
|
108
|
+
children?: ReactNode;
|
|
109
|
+
}
|
|
110
|
+
/** Render a React subtree as an image-space / viewport overlay via a portal (decision P0-G). */
|
|
111
|
+
declare function Overlay(props: GalleristOverlayProps): ReactNode;
|
|
112
|
+
interface GalleristRegionProps {
|
|
113
|
+
name: RegionName;
|
|
114
|
+
children?: ReactNode;
|
|
115
|
+
}
|
|
116
|
+
/** Render a React subtree into a named renderer region (toolbar, footer, …) via a portal. */
|
|
117
|
+
declare function Region(props: GalleristRegionProps): ReactNode;
|
|
118
|
+
interface GalleristCaptionProps<GM = unknown, IM = unknown> {
|
|
119
|
+
/**
|
|
120
|
+
* the caption for the current item; `null`, `undefined`, `true` or `false` shows its text
|
|
121
|
+
* `caption` (in a `span.gallerist-caption-text`, the theme's caption type)
|
|
122
|
+
*/
|
|
123
|
+
children: (current: {
|
|
124
|
+
item: Item<IM>;
|
|
125
|
+
group: Group<GM, IM>;
|
|
126
|
+
cursor: Readonly<Cursor>;
|
|
127
|
+
}) => ReactNode;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Render the caption for the current item with React, into the renderer's caption region (the
|
|
131
|
+
* caption bar, the editorial placard, wherever the preset puts it). Re-renders on navigation and
|
|
132
|
+
* collection changes. Its text is the viewer's accessible description, like a text caption.
|
|
133
|
+
*/
|
|
134
|
+
declare function Caption<GM = unknown, IM = unknown>(props: GalleristCaptionProps<GM, IM>): ReactNode;
|
|
135
|
+
declare const Gallerist: react.ForwardRefExoticComponent<GalleristProps & react.RefAttributes<ViewerApi>> & {
|
|
136
|
+
Overlay: typeof Overlay;
|
|
137
|
+
Region: typeof Region;
|
|
138
|
+
Caption: typeof Caption;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export { type CurrentItem, Gallerist, type GalleristCaptionProps, GalleristContext, type GalleristOpenAt, type GalleristOverlayProps, type GalleristProps, type GalleristRegionProps, type UseGallerist, useCurrentItem, useGallerist, useViewerContext };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_STRINGS,
|
|
6
|
+
resolvePreset
|
|
7
|
+
} from "@gallerist/renderer-dom";
|
|
8
|
+
|
|
9
|
+
// src/context.ts
|
|
10
|
+
import { createContext, useContext } from "react";
|
|
11
|
+
var GalleristContext = createContext(null);
|
|
12
|
+
function useViewerContext() {
|
|
13
|
+
const viewer = useContext(GalleristContext);
|
|
14
|
+
if (!viewer)
|
|
15
|
+
throw new Error(
|
|
16
|
+
"Gallerist.Overlay / Gallerist.Region / Gallerist.Caption must be used inside <Gallerist>"
|
|
17
|
+
);
|
|
18
|
+
return viewer;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/useGallerist.ts
|
|
22
|
+
import { useCallback, useMemo, useSyncExternalStore } from "react";
|
|
23
|
+
function useGallerist(viewer) {
|
|
24
|
+
const subscribe = useCallback((cb) => viewer.subscribe(cb), [viewer]);
|
|
25
|
+
const getSnapshot = useCallback(() => viewer.getState(), [viewer]);
|
|
26
|
+
const state = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
27
|
+
const actions = useMemo(
|
|
28
|
+
() => ({
|
|
29
|
+
open: (at) => viewer.open(at),
|
|
30
|
+
close: () => viewer.close(),
|
|
31
|
+
next: () => viewer.next(),
|
|
32
|
+
prev: () => viewer.prev(),
|
|
33
|
+
nextItem: () => viewer.nextItem(),
|
|
34
|
+
prevItem: () => viewer.prevItem(),
|
|
35
|
+
goTo: (g, i) => viewer.goTo(g, i),
|
|
36
|
+
goToId: (g, i) => viewer.goToId(g, i),
|
|
37
|
+
zoomTo: (s, f) => viewer.zoomTo(s, f),
|
|
38
|
+
resetZoom: () => viewer.resetZoom()
|
|
39
|
+
}),
|
|
40
|
+
[viewer]
|
|
41
|
+
);
|
|
42
|
+
return { state, viewer, ...actions };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/useCurrentItem.ts
|
|
46
|
+
import { useCallback as useCallback2, useContext as useContext2, useRef, useSyncExternalStore as useSyncExternalStore2 } from "react";
|
|
47
|
+
function useCurrentItem(viewer) {
|
|
48
|
+
const fromContext = useContext2(GalleristContext);
|
|
49
|
+
const v = viewer ?? fromContext;
|
|
50
|
+
if (!v) throw new Error("useCurrentItem() must be used inside <Gallerist> or given a viewer");
|
|
51
|
+
const last = useRef(null);
|
|
52
|
+
const subscribe = useCallback2((cb) => v.subscribe(cb), [v]);
|
|
53
|
+
const getSnapshot = useCallback2(() => {
|
|
54
|
+
const { g, i } = v.getState().cursor;
|
|
55
|
+
const group = v.getCollection().groups[g];
|
|
56
|
+
const item = group?.items[i];
|
|
57
|
+
const prev = last.current;
|
|
58
|
+
if (prev && prev.item === item && prev.group === group && prev.cursor.g === g && prev.cursor.i === i)
|
|
59
|
+
return prev;
|
|
60
|
+
last.current = { item, group, cursor: { g, i } };
|
|
61
|
+
return last.current;
|
|
62
|
+
}, [v]);
|
|
63
|
+
return useSyncExternalStore2(subscribe, getSnapshot, getSnapshot);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/Gallerist.tsx
|
|
67
|
+
import {
|
|
68
|
+
createViewer
|
|
69
|
+
} from "@gallerist/core";
|
|
70
|
+
import {
|
|
71
|
+
applyAppearance,
|
|
72
|
+
mount
|
|
73
|
+
} from "@gallerist/renderer-dom";
|
|
74
|
+
import {
|
|
75
|
+
forwardRef,
|
|
76
|
+
useEffect,
|
|
77
|
+
useId,
|
|
78
|
+
useImperativeHandle,
|
|
79
|
+
useInsertionEffect,
|
|
80
|
+
useRef as useRef2,
|
|
81
|
+
useState
|
|
82
|
+
} from "react";
|
|
83
|
+
import { createPortal } from "react-dom";
|
|
84
|
+
import { jsx } from "react/jsx-runtime";
|
|
85
|
+
var EVENT_NAMES = Object.keys({
|
|
86
|
+
init: 1,
|
|
87
|
+
destroy: 1,
|
|
88
|
+
open: 1,
|
|
89
|
+
opened: 1,
|
|
90
|
+
close: 1,
|
|
91
|
+
closed: 1,
|
|
92
|
+
beforeSlide: 1,
|
|
93
|
+
slide: 1,
|
|
94
|
+
afterSlide: 1,
|
|
95
|
+
beforeItem: 1,
|
|
96
|
+
item: 1,
|
|
97
|
+
afterItem: 1,
|
|
98
|
+
zoom: 1,
|
|
99
|
+
zoomEnd: 1,
|
|
100
|
+
load: 1,
|
|
101
|
+
loaded: 1,
|
|
102
|
+
loadError: 1,
|
|
103
|
+
collectionChange: 1,
|
|
104
|
+
fullscreenChange: 1,
|
|
105
|
+
overlayClick: 1
|
|
106
|
+
});
|
|
107
|
+
var toCollection = (items, groups, collection) => collection ?? { groups: groups ?? (items ?? []).map((it) => ({ id: it.id, items: [it] })) };
|
|
108
|
+
var shallowSame = (a, b) => {
|
|
109
|
+
if (Object.is(a, b)) return true;
|
|
110
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
111
|
+
return a.length === b.length && a.every((v, i) => Object.is(v, b[i]));
|
|
112
|
+
}
|
|
113
|
+
if (!a || !b || typeof a !== "object" || typeof b !== "object") return false;
|
|
114
|
+
const ka = Object.keys(a);
|
|
115
|
+
const rb = b;
|
|
116
|
+
return ka.length === Object.keys(b).length && ka.every((k) => Object.is(a[k], rb[k]));
|
|
117
|
+
};
|
|
118
|
+
var sameRecord = (a, b, skip) => {
|
|
119
|
+
const ra = a;
|
|
120
|
+
const rb = b;
|
|
121
|
+
const ka = Object.keys(ra).filter((k) => k !== skip);
|
|
122
|
+
const kb = Object.keys(rb).filter((k) => k !== skip);
|
|
123
|
+
return ka.length === kb.length && ka.every((k) => shallowSame(ra[k], rb[k]));
|
|
124
|
+
};
|
|
125
|
+
var sameCollection = (a, b) => a.groups.length === b.groups.length && shallowSame(a.meta, b.meta) && a.groups.every((g, gi) => {
|
|
126
|
+
const h = b.groups[gi];
|
|
127
|
+
return !!h && sameRecord(g, h, "items") && g.items.length === h.items.length && g.items.every((it, ii) => {
|
|
128
|
+
const other = h.items[ii];
|
|
129
|
+
return !!other && sameRecord(it, other);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
var GalleristBase = forwardRef(function Gallerist(props, ref) {
|
|
133
|
+
const {
|
|
134
|
+
open,
|
|
135
|
+
onClose,
|
|
136
|
+
container,
|
|
137
|
+
controls,
|
|
138
|
+
preset,
|
|
139
|
+
skin,
|
|
140
|
+
accent,
|
|
141
|
+
scheme,
|
|
142
|
+
arrows,
|
|
143
|
+
rail,
|
|
144
|
+
autoHide,
|
|
145
|
+
linear,
|
|
146
|
+
strings,
|
|
147
|
+
itemIndicator,
|
|
148
|
+
navigator,
|
|
149
|
+
frame,
|
|
150
|
+
groupAxis,
|
|
151
|
+
arrowLabels,
|
|
152
|
+
label,
|
|
153
|
+
returnFocus,
|
|
154
|
+
preload,
|
|
155
|
+
adapters,
|
|
156
|
+
children,
|
|
157
|
+
...options
|
|
158
|
+
} = props;
|
|
159
|
+
const rootRef = useRef2(null);
|
|
160
|
+
const { items, groups, collection, on } = options;
|
|
161
|
+
const onRef = useRef2(on);
|
|
162
|
+
useInsertionEffect(() => {
|
|
163
|
+
onRef.current = on;
|
|
164
|
+
});
|
|
165
|
+
const [viewer] = useState(() => {
|
|
166
|
+
const { plugins: _plugins, on: _on, ...rest } = options;
|
|
167
|
+
const forward = {};
|
|
168
|
+
for (const name of EVENT_NAMES) {
|
|
169
|
+
;
|
|
170
|
+
forward[name] = (payload) => onRef.current?.[name]?.(payload);
|
|
171
|
+
}
|
|
172
|
+
return createViewer({ ...rest, on: forward });
|
|
173
|
+
});
|
|
174
|
+
const appliedData = useRef2({ items, groups, collection });
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
const prev = appliedData.current;
|
|
177
|
+
if (prev.items === items && prev.groups === groups && prev.collection === collection) return;
|
|
178
|
+
appliedData.current = { items, groups, collection };
|
|
179
|
+
const next = toCollection(items, groups, collection);
|
|
180
|
+
if (sameCollection(next, viewer.getCollection())) return;
|
|
181
|
+
try {
|
|
182
|
+
viewer.setCollection(next);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error("[gallerist] ignored an invalid collection update:", error);
|
|
185
|
+
}
|
|
186
|
+
}, [viewer, items, groups, collection]);
|
|
187
|
+
useImperativeHandle(ref, () => viewer, [viewer]);
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
const disposers = (options.plugins ?? []).map((plugin) => viewer.use(plugin));
|
|
190
|
+
return () => {
|
|
191
|
+
for (const dispose of disposers) dispose();
|
|
192
|
+
};
|
|
193
|
+
}, [viewer]);
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
const handle = mount(viewer, {
|
|
196
|
+
container: container ?? null,
|
|
197
|
+
controls,
|
|
198
|
+
preset,
|
|
199
|
+
arrows,
|
|
200
|
+
rail,
|
|
201
|
+
autoHide,
|
|
202
|
+
linear,
|
|
203
|
+
strings,
|
|
204
|
+
itemIndicator,
|
|
205
|
+
navigator,
|
|
206
|
+
frame,
|
|
207
|
+
groupAxis,
|
|
208
|
+
arrowLabels,
|
|
209
|
+
label,
|
|
210
|
+
returnFocus,
|
|
211
|
+
preload,
|
|
212
|
+
adapters,
|
|
213
|
+
skin,
|
|
214
|
+
accent,
|
|
215
|
+
scheme,
|
|
216
|
+
loop: options.loop
|
|
217
|
+
});
|
|
218
|
+
rootRef.current = handle.root;
|
|
219
|
+
return () => {
|
|
220
|
+
handle.destroy();
|
|
221
|
+
rootRef.current = null;
|
|
222
|
+
};
|
|
223
|
+
}, [viewer]);
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
const root = rootRef.current;
|
|
226
|
+
if (root) applyAppearance(root, { skin, accent, scheme });
|
|
227
|
+
}, [skin, accent, scheme]);
|
|
228
|
+
const openG = typeof open === "object" && open ? open.g : void 0;
|
|
229
|
+
const openI = typeof open === "object" && open ? open.i : void 0;
|
|
230
|
+
const shown = open === void 0 ? void 0 : !!open;
|
|
231
|
+
useEffect(() => {
|
|
232
|
+
if (shown === void 0) return;
|
|
233
|
+
if (shown) viewer.open(openG === void 0 ? void 0 : { g: openG, i: openI });
|
|
234
|
+
else viewer.close();
|
|
235
|
+
}, [shown, openG, openI, viewer]);
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
if (!onClose) return;
|
|
238
|
+
return viewer.on("closed", () => onClose());
|
|
239
|
+
}, [onClose, viewer]);
|
|
240
|
+
return /* @__PURE__ */ jsx(GalleristContext.Provider, { value: viewer, children });
|
|
241
|
+
});
|
|
242
|
+
function Overlay(props) {
|
|
243
|
+
const viewer = useViewerContext();
|
|
244
|
+
const [host, setHost] = useState(null);
|
|
245
|
+
const autoId = useId();
|
|
246
|
+
const id = props.id ?? autoId;
|
|
247
|
+
const anchorKey = JSON.stringify(props.anchor);
|
|
248
|
+
const visibleAtKey = JSON.stringify(props.visibleAt ?? null);
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
const node = document.createElement("div");
|
|
251
|
+
setHost(node);
|
|
252
|
+
const spec = {
|
|
253
|
+
id,
|
|
254
|
+
anchor: props.anchor,
|
|
255
|
+
layer: props.layer,
|
|
256
|
+
interactive: props.interactive,
|
|
257
|
+
visibleAt: props.visibleAt,
|
|
258
|
+
class: props.className,
|
|
259
|
+
render: () => node
|
|
260
|
+
// stable host; React owns its children through the portal
|
|
261
|
+
};
|
|
262
|
+
const dispose = viewer.use(() => ({ name: `overlay:${id}`, overlays: [spec] }));
|
|
263
|
+
return () => {
|
|
264
|
+
dispose();
|
|
265
|
+
setHost(null);
|
|
266
|
+
};
|
|
267
|
+
}, [viewer, id, anchorKey, visibleAtKey, props.layer, props.interactive, props.className]);
|
|
268
|
+
return host ? createPortal(props.children, host) : null;
|
|
269
|
+
}
|
|
270
|
+
function Region(props) {
|
|
271
|
+
const viewer = useViewerContext();
|
|
272
|
+
const [host, setHost] = useState(null);
|
|
273
|
+
const { name } = props;
|
|
274
|
+
useEffect(() => {
|
|
275
|
+
const node = document.createElement("div");
|
|
276
|
+
setHost(node);
|
|
277
|
+
const dispose = viewer.use(() => ({
|
|
278
|
+
name: `region:${name}`,
|
|
279
|
+
regions: { [name]: () => node }
|
|
280
|
+
}));
|
|
281
|
+
return () => {
|
|
282
|
+
dispose();
|
|
283
|
+
setHost(null);
|
|
284
|
+
};
|
|
285
|
+
}, [viewer, name]);
|
|
286
|
+
return host ? createPortal(props.children, host) : null;
|
|
287
|
+
}
|
|
288
|
+
function Caption(props) {
|
|
289
|
+
const { item, group, cursor } = useCurrentItem();
|
|
290
|
+
const out = item && group ? props.children({ item, group, cursor }) : null;
|
|
291
|
+
return /* @__PURE__ */ jsx(Region, { name: "caption", children: out == null || typeof out === "boolean" ? text(item?.caption) : out });
|
|
292
|
+
}
|
|
293
|
+
function text(caption) {
|
|
294
|
+
return typeof caption === "string" && caption ? /* @__PURE__ */ jsx("span", { className: "gallerist-caption-text", children: caption }) : null;
|
|
295
|
+
}
|
|
296
|
+
var Gallerist2 = Object.assign(GalleristBase, { Overlay, Region, Caption });
|
|
297
|
+
export {
|
|
298
|
+
DEFAULT_STRINGS,
|
|
299
|
+
Gallerist2 as Gallerist,
|
|
300
|
+
GalleristContext,
|
|
301
|
+
resolvePreset,
|
|
302
|
+
useCurrentItem,
|
|
303
|
+
useGallerist,
|
|
304
|
+
useViewerContext
|
|
305
|
+
};
|
|
306
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/context.ts","../src/useGallerist.ts","../src/useCurrentItem.ts","../src/Gallerist.tsx"],"sourcesContent":["// @gallerist/react — React adapter (spec §16). Thin binding over the headless store.\nexport {\n type Accent,\n type ArrowLayout,\n type ColorScheme,\n type GroupAxis,\n type ItemIndicator,\n type PresetName,\n type RailPlacement,\n type ResolvedPreset,\n type Skin,\n type Strings,\n DEFAULT_STRINGS,\n resolvePreset,\n} from '@gallerist/renderer-dom'\nexport { useViewerContext, GalleristContext } from './context'\nexport { type UseGallerist, useGallerist } from './useGallerist'\nexport { type CurrentItem, useCurrentItem } from './useCurrentItem'\nexport {\n Gallerist,\n type GalleristCaptionProps,\n type GalleristOpenAt,\n type GalleristOverlayProps,\n type GalleristProps,\n type GalleristRegionProps,\n} from './Gallerist'\n","import type { ViewerApi } from '@gallerist/core'\nimport { createContext, useContext } from 'react'\n\nexport const GalleristContext = createContext<ViewerApi | null>(null)\n\nexport function useViewerContext(): ViewerApi {\n const viewer = useContext(GalleristContext)\n if (!viewer)\n throw new Error(\n 'Gallerist.Overlay / Gallerist.Region / Gallerist.Caption must be used inside <Gallerist>',\n )\n return viewer\n}\n","import type { ViewerApi, ViewerState } from '@gallerist/core'\nimport { useCallback, useMemo, useSyncExternalStore } from 'react'\n\nexport interface UseGallerist {\n state: Readonly<ViewerState>\n viewer: ViewerApi\n open: ViewerApi['open']\n close: ViewerApi['close']\n next: ViewerApi['next']\n prev: ViewerApi['prev']\n nextItem: ViewerApi['nextItem']\n prevItem: ViewerApi['prevItem']\n goTo: ViewerApi['goTo']\n goToId: ViewerApi['goToId']\n zoomTo: ViewerApi['zoomTo']\n resetZoom: ViewerApi['resetZoom']\n}\n\n/** Bind a React component to a viewer's state (useSyncExternalStore) + its actions. */\nexport function useGallerist(viewer: ViewerApi): UseGallerist {\n const subscribe = useCallback((cb: () => void) => viewer.subscribe(cb), [viewer])\n const getSnapshot = useCallback(() => viewer.getState(), [viewer])\n const state = useSyncExternalStore(subscribe, getSnapshot, getSnapshot)\n\n // Stabilize the action closures so they keep a constant identity across re-renders (this hook\n // re-renders on every store commit). Otherwise each commit handed consumers fresh function\n // references, defeating React.memo / dependency-array memoization downstream (S-PERF4).\n const actions = useMemo(\n () => ({\n open: (at?: Parameters<ViewerApi['open']>[0]) => viewer.open(at),\n close: () => viewer.close(),\n next: () => viewer.next(),\n prev: () => viewer.prev(),\n nextItem: () => viewer.nextItem(),\n prevItem: () => viewer.prevItem(),\n goTo: (g: number, i?: number) => viewer.goTo(g, i),\n goToId: (g: string, i?: string) => viewer.goToId(g, i),\n zoomTo: (s: number, f?: Parameters<ViewerApi['zoomTo']>[1]) => viewer.zoomTo(s, f),\n resetZoom: () => viewer.resetZoom(),\n }),\n [viewer],\n )\n\n return { state, viewer, ...actions }\n}\n","import type { Cursor, Group, Item, ViewerApi } from '@gallerist/core'\nimport { useCallback, useContext, useRef, useSyncExternalStore } from 'react'\nimport { GalleristContext } from './context'\n\n/** The viewer's current item, its group and its position. */\nexport interface CurrentItem<GM = unknown, IM = unknown> {\n /** undefined only for an empty collection */\n item: Item<IM> | undefined\n group: Group<GM, IM> | undefined\n cursor: Readonly<Cursor>\n}\n\n/**\n * The current item, group and cursor of the enclosing `<Gallerist>` (or of `viewer`). Re-renders\n * when the cursor or the collection changes, not on zoom or pan.\n */\nexport function useCurrentItem<GM = unknown, IM = unknown>(\n viewer?: ViewerApi,\n): CurrentItem<GM, IM> {\n const fromContext = useContext(GalleristContext)\n const v = viewer ?? fromContext\n if (!v) throw new Error('useCurrentItem() must be used inside <Gallerist> or given a viewer')\n // useSyncExternalStore needs a stable snapshot: the same object until the item really changes\n const last = useRef<CurrentItem | null>(null)\n const subscribe = useCallback((cb: () => void) => v.subscribe(cb), [v])\n const getSnapshot = useCallback((): CurrentItem => {\n const { g, i } = v.getState().cursor\n const group = v.getCollection().groups[g]\n const item = group?.items[i]\n const prev = last.current\n if (\n prev &&\n prev.item === item &&\n prev.group === group &&\n prev.cursor.g === g &&\n prev.cursor.i === i\n )\n return prev\n last.current = { item, group, cursor: { g, i } }\n return last.current\n }, [v])\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot) as CurrentItem<GM, IM>\n}\n","import {\n type Collection,\n type Cursor,\n type EventHandlers,\n type EventMap,\n type Group,\n type Item,\n type OverlaySpec,\n type RegionName,\n type ViewerApi,\n type ViewerOptions,\n createViewer,\n} from '@gallerist/core'\nimport {\n type Accent,\n type ArrowLayout,\n type ColorScheme,\n type ItemIndicator,\n type MountOptions,\n type PresetName,\n type RailPlacement,\n type Skin,\n type Strings,\n applyAppearance,\n mount,\n} from '@gallerist/renderer-dom'\nimport {\n type ReactNode,\n forwardRef,\n useEffect,\n useId,\n useImperativeHandle,\n useInsertionEffect,\n useRef,\n useState,\n} from 'react'\nimport { createPortal } from 'react-dom'\nimport { GalleristContext, useViewerContext } from './context'\nimport { useCurrentItem } from './useCurrentItem'\n\n/** A position to open at: group `g`, item `i` (default: the group's remembered item). */\nexport interface GalleristOpenAt {\n g: number\n i?: number\n}\n\n/**\n * `items` / `groups` / `collection` and `on` are reactive. Changed data swaps the collection in\n * place (diffed by id, keeping the cursor); data rebuilt with the same content is ignored. Every other viewer option\n * (`loop`, `itemMemory`, `startAt`, `plugins`, `overlays`, …) and the layout props (`container`,\n * `preset`, `arrows`, `rail`, `label`, `preload`, `adapters`, …) are read once; change the\n * component's `key` to apply new ones. `skin`, `accent`, `scheme` and `open` are reactive.\n */\nexport interface GalleristProps extends ViewerOptions {\n /**\n * Controlled visibility. `true` opens at the current position; `{ g, i }` opens there, and a new\n * position while open moves the cursor (applied when the value changes, so the user can still\n * navigate away from it); `false` closes. Leave it undefined to drive the viewer through the ref.\n */\n open?: boolean | GalleristOpenAt\n onClose?: () => void\n container?: HTMLElement | null\n controls?: boolean\n /** layout preset (arrow placement, auto-hide, data-gallerist-preset, the thumbnails() defaults) — UX-E. */\n preset?: PresetName\n /** visual skin, default `gallery`; other skins need their stylesheet (reactive). */\n skin?: Skin\n /** the skin's accent by name, e.g. `mint` (Gallery) or `cyan` (Console) (reactive). */\n accent?: Accent\n /** colour scheme: `dark`, `light` or `auto`; unset, the skin's own default (reactive). */\n scheme?: ColorScheme\n /** within-group arrow placement (overrides the preset default) — UX-D. */\n arrows?: ArrowLayout\n /** thumbnail rail placement (overrides the preset default) — UX-E. */\n rail?: RailPlacement\n /** auto-hide chrome on idle (overrides the preset default) — UX-C. */\n autoHide?: boolean\n /** linear flat traversal: ←/→ walks the flattened item sequence; pairs with a flat thumbnail rail. */\n linear?: boolean\n /** UI text overrides (button labels, announcements) for translation; read once at mount. */\n strings?: Partial<Strings>\n /** item position indicator: `pills` (default), `dots`, `counter` or `placard`; read once at mount. */\n itemIndicator?: ItemIndicator\n /** zoom navigator while zoomed in (overrides the preset default); read once at mount. */\n navigator?: boolean\n /** framed media: the work hangs with room around it (overrides the preset); read once at mount. */\n frame?: boolean\n /** which arrows move between groups (overrides the rail and preset default); read once at mount. */\n groupAxis?: MountOptions['groupAxis']\n /** name the group arrows after their target group (overrides the preset); read once at mount. */\n arrowLabels?: MountOptions['arrowLabels']\n /** the overlay dialog's accessible name (default `strings.dialog`); read once at mount. */\n label?: string\n /** where focus returns when the overlay closes (default: what had focus when it opened); read\n * once at mount, so pass a getter for a target that changes. */\n returnFocus?: MountOptions['returnFocus']\n /** how many groups to keep mounted ahead of and behind the current one; read once at mount. */\n preload?: MountOptions['preload']\n /** media adapters, replacing the default image adapter; read once at mount. */\n adapters?: MountOptions['adapters']\n children?: ReactNode\n}\n\n// Every viewer event, checked against EventMap (a missing or unknown name fails to compile), so\n// the forwarders below can route each one to the latest `on` handlers.\nconst EVENT_NAMES = Object.keys({\n init: 1,\n destroy: 1,\n open: 1,\n opened: 1,\n close: 1,\n closed: 1,\n beforeSlide: 1,\n slide: 1,\n afterSlide: 1,\n beforeItem: 1,\n item: 1,\n afterItem: 1,\n zoom: 1,\n zoomEnd: 1,\n load: 1,\n loaded: 1,\n loadError: 1,\n collectionChange: 1,\n fullscreenChange: 1,\n overlayClick: 1,\n} satisfies Record<keyof EventMap, 1>) as (keyof EventMap)[]\n\n// The collection a data prop describes (flat items → singleton groups). setCollection validates it.\nconst toCollection = (items?: Item[], groups?: Group[], collection?: Collection): Collection =>\n collection ?? { groups: groups ?? (items ?? []).map((it) => ({ id: it.id, items: [it] })) }\n\n// Values equal at one level of depth: identical, or arrays/plain objects with identical entries\n// (a `source: { src }`, `naturalSize`, or a re-created `overlays` array of the same specs).\nconst shallowSame = (a: unknown, b: unknown): boolean => {\n if (Object.is(a, b)) return true\n if (Array.isArray(a) && Array.isArray(b)) {\n return a.length === b.length && a.every((v, i) => Object.is(v, b[i]))\n }\n if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return false\n const ka = Object.keys(a)\n const rb = b as Record<string, unknown>\n return (\n ka.length === Object.keys(b).length &&\n ka.every((k) => Object.is((a as Record<string, unknown>)[k], rb[k]))\n )\n}\nconst sameRecord = (a: object, b: object, skip?: string): boolean => {\n const ra = a as Record<string, unknown>\n const rb = b as Record<string, unknown>\n const ka = Object.keys(ra).filter((k) => k !== skip)\n const kb = Object.keys(rb).filter((k) => k !== skip)\n return ka.length === kb.length && ka.every((k) => shallowSame(ra[k], rb[k]))\n}\n// Same content (groups, items and their fields), even when rebuilt as fresh objects — so an\n// inline `items={photos.map(…)}` doesn't swap the collection (and reset load status) every render.\nconst sameCollection = (a: Collection, b: Readonly<Collection>): boolean =>\n a.groups.length === b.groups.length &&\n shallowSame(a.meta, b.meta) &&\n a.groups.every((g, gi) => {\n const h = b.groups[gi]\n return (\n !!h &&\n sameRecord(g, h, 'items') &&\n g.items.length === h.items.length &&\n g.items.every((it, ii) => {\n const other = h.items[ii]\n return !!other && sameRecord(it, other)\n })\n )\n })\n\nconst GalleristBase = forwardRef<ViewerApi, GalleristProps>(function Gallerist(props, ref) {\n const {\n open,\n onClose,\n container,\n controls,\n preset,\n skin,\n accent,\n scheme,\n arrows,\n rail,\n autoHide,\n linear,\n strings,\n itemIndicator,\n navigator,\n frame,\n groupAxis,\n arrowLabels,\n label,\n returnFocus,\n preload,\n adapters,\n children,\n ...options\n } = props\n const rootRef = useRef<HTMLElement | null>(null)\n const { items, groups, collection, on } = options\n\n // The viewer outlives renders, so its handlers must not capture the first render's `on`: they\n // forward to whatever `on` the latest committed render passed (a returned `false` still cancels).\n const onRef = useRef<EventHandlers | undefined>(on)\n useInsertionEffect(() => {\n onRef.current = on\n })\n // Create the viewer once (core is DOM-free → SSR-safe); options are read at construction. Lazy\n // useState (not useMemo, which React may discard) guarantees a single stable instance. Plugins\n // are deliberately NOT registered at construction — they're managed in an effect below so their\n // teardown runs on unmount (I-X2).\n const [viewer] = useState(() => {\n const { plugins: _plugins, on: _on, ...rest } = options\n const forward: EventHandlers = {}\n for (const name of EVENT_NAMES) {\n ;(forward as Record<string, (p: unknown) => unknown>)[name] = (payload) =>\n (onRef.current?.[name] as ((p: unknown) => unknown) | undefined)?.(payload)\n }\n return createViewer({ ...rest, on: forward })\n })\n\n // Data props are reactive: new data swaps the collection via setCollection (diffed by id, so the\n // cursor and zoom survive when the current item does). The first render's data is already in the\n // viewer; a new reference with the same content is ignored; invalid data (a duplicate id) is\n // reported and the viewer keeps its current collection instead of crashing the React tree.\n const appliedData = useRef({ items, groups, collection })\n useEffect(() => {\n const prev = appliedData.current\n if (prev.items === items && prev.groups === groups && prev.collection === collection) return\n appliedData.current = { items, groups, collection }\n const next = toCollection(items, groups, collection)\n if (sameCollection(next, viewer.getCollection())) return\n try {\n viewer.setCollection(next)\n } catch (error) {\n console.error('[gallerist] ignored an invalid collection update:', error)\n }\n }, [viewer, items, groups, collection])\n useImperativeHandle(ref, () => viewer, [viewer])\n\n // Register the construction-time plugins via use() and dispose them on unmount, so their\n // init()-created store subscriptions / observers / DOM don't leak on every remount (the viewer\n // itself is intentionally never destroyed — see the mount effect). Plugins are mount-time config.\n // biome-ignore lint/correctness/useExhaustiveDependencies: register once for the viewer's life\n useEffect(() => {\n const disposers = (options.plugins ?? []).map((plugin) => viewer.use(plugin))\n return () => {\n for (const dispose of disposers) dispose()\n }\n }, [viewer])\n\n // Mount the renderer once per viewer (client-side); container/controls are mount-time only. Only\n // the renderer (the DOM holder) is torn down here. The viewer is NOT destroyed on cleanup: under\n // StrictMode the simulated unmount would run viewer.destroy() on the stable useState instance,\n // permanently poisoning it (registerPlugin/nav short-circuit once destroyed) so re-registered\n // overlays/regions vanish. The viewer is plain JS (no DOM) and is reclaimed by GC once the\n // component unmounts and the renderer/hook/overlay subscriptions have all cleaned themselves up.\n // biome-ignore lint/correctness/useExhaustiveDependencies: intentional mount-once (layout opts read once)\n useEffect(() => {\n const handle = mount(viewer, {\n container: container ?? null,\n controls,\n preset,\n arrows,\n rail,\n autoHide,\n linear,\n strings,\n itemIndicator,\n navigator,\n frame,\n groupAxis,\n arrowLabels,\n label,\n returnFocus,\n preload,\n adapters,\n skin,\n accent,\n scheme,\n loop: options.loop,\n })\n rootRef.current = handle.root\n return () => {\n handle.destroy()\n rootRef.current = null\n }\n }, [viewer])\n\n // The appearance is reactive: the skin files key off root attributes, so no remount is needed.\n useEffect(() => {\n const root = rootRef.current\n if (root) applyAppearance(root, { skin, accent, scheme })\n }, [skin, accent, scheme])\n\n // `open` is compared by value, so an inline `open={{ g: 2 }}` re-applies only when g or i change\n const openG = typeof open === 'object' && open ? open.g : undefined\n const openI = typeof open === 'object' && open ? open.i : undefined\n const shown = open === undefined ? undefined : !!open\n useEffect(() => {\n if (shown === undefined) return\n // open({ g, i }) on a viewer already showing is a navigation (core)\n if (shown) viewer.open(openG === undefined ? undefined : { g: openG, i: openI })\n else viewer.close()\n }, [shown, openG, openI, viewer])\n\n useEffect(() => {\n if (!onClose) return\n return viewer.on('closed', () => onClose())\n }, [onClose, viewer])\n\n return <GalleristContext.Provider value={viewer}>{children}</GalleristContext.Provider>\n})\n\nexport interface GalleristOverlayProps {\n anchor: OverlaySpec['anchor']\n id?: string\n layer?: number\n interactive?: boolean\n visibleAt?: [number, number]\n className?: string\n children?: ReactNode\n}\n\n/** Render a React subtree as an image-space / viewport overlay via a portal (decision P0-G). */\nfunction Overlay(props: GalleristOverlayProps): ReactNode {\n const viewer = useViewerContext()\n const [host, setHost] = useState<HTMLElement | null>(null)\n // useId is SSR-deterministic (server + client agree), unlike a module-global counter.\n const autoId = useId()\n const id = props.id ?? autoId\n\n // Depend on the VALUE of the object/array props, not their identity. The common usage passes\n // inline literals (`anchor={{ kind: 'viewport', … }}`) that are a fresh reference every render;\n // keying on identity disposed+re-registered the overlay plugin on every parent render, forcing a\n // full overlay repaint. Serializing makes re-registration happen only on a real change (I-X3).\n const anchorKey = JSON.stringify(props.anchor)\n const visibleAtKey = JSON.stringify(props.visibleAt ?? null)\n // biome-ignore lint/correctness/useExhaustiveDependencies: anchor/visibleAt tracked by value key\n useEffect(() => {\n const node = document.createElement('div')\n setHost(node)\n const spec: OverlaySpec = {\n id,\n anchor: props.anchor,\n layer: props.layer,\n interactive: props.interactive,\n visibleAt: props.visibleAt,\n class: props.className,\n render: () => node, // stable host; React owns its children through the portal\n }\n const dispose = viewer.use(() => ({ name: `overlay:${id}`, overlays: [spec] }))\n return () => {\n dispose()\n setHost(null)\n }\n }, [viewer, id, anchorKey, visibleAtKey, props.layer, props.interactive, props.className])\n\n return host ? createPortal(props.children, host) : null\n}\n\nexport interface GalleristRegionProps {\n name: RegionName\n children?: ReactNode\n}\n\n/** Render a React subtree into a named renderer region (toolbar, footer, …) via a portal. */\nfunction Region(props: GalleristRegionProps): ReactNode {\n const viewer = useViewerContext()\n const [host, setHost] = useState<HTMLElement | null>(null)\n const { name } = props\n\n useEffect(() => {\n const node = document.createElement('div')\n setHost(node)\n const dispose = viewer.use(() => ({\n name: `region:${name}`,\n regions: { [name]: () => node },\n }))\n return () => {\n dispose()\n setHost(null)\n }\n }, [viewer, name])\n\n return host ? createPortal(props.children, host) : null\n}\n\nexport interface GalleristCaptionProps<GM = unknown, IM = unknown> {\n /**\n * the caption for the current item; `null`, `undefined`, `true` or `false` shows its text\n * `caption` (in a `span.gallerist-caption-text`, the theme's caption type)\n */\n children: (current: {\n item: Item<IM>\n group: Group<GM, IM>\n cursor: Readonly<Cursor>\n }) => ReactNode\n}\n\n/**\n * Render the caption for the current item with React, into the renderer's caption region (the\n * caption bar, the editorial placard, wherever the preset puts it). Re-renders on navigation and\n * collection changes. Its text is the viewer's accessible description, like a text caption.\n */\nfunction Caption<GM = unknown, IM = unknown>(props: GalleristCaptionProps<GM, IM>): ReactNode {\n const { item, group, cursor } = useCurrentItem<GM, IM>()\n const out = item && group ? props.children({ item, group, cursor }) : null\n return (\n <Region name=\"caption\">\n {out == null || typeof out === 'boolean' ? text(item?.caption) : out}\n </Region>\n )\n}\n\n// The item's own text caption, styled like the bar's text. Untyped data (JSON, a CDN page) can still\n// carry an old object-shaped caption, which React would throw on: anything but a string is none.\nfunction text(caption: unknown): ReactNode {\n return typeof caption === 'string' && caption ? (\n <span className=\"gallerist-caption-text\">{caption}</span>\n ) : null\n}\n\nexport const Gallerist = Object.assign(GalleristBase, { Overlay, Region, Caption })\n"],"mappings":"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","names":["useCallback","useContext","useSyncExternalStore","useContext","useCallback","useSyncExternalStore","useRef","useRef","Gallerist"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,80 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gallerist/react",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "React adapter for Gallerist — <Gallerist> component, useGallerist() hook, JSX overlay/region portals.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"LICENSE",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"main": "./dist/index.cjs",
|
|
14
|
+
"module": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"import": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"default": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"require": {
|
|
23
|
+
"types": "./dist/index.d.cts",
|
|
24
|
+
"default": "./dist/index.cjs"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@gallerist/core": ">=0.3.0 <1",
|
|
30
|
+
"@gallerist/renderer-dom": ">=0.3.0 <1",
|
|
31
|
+
"react": ">=18",
|
|
32
|
+
"react-dom": ">=18"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@testing-library/react": "^16.1.0",
|
|
36
|
+
"@types/react": "^18.3.12",
|
|
37
|
+
"@types/react-dom": "^18.3.1",
|
|
38
|
+
"react": "^18.3.1",
|
|
39
|
+
"react-dom": "^18.3.1",
|
|
40
|
+
"tsup": "^8.3.5",
|
|
41
|
+
"typescript": "^5.7.2",
|
|
42
|
+
"@gallerist/core": "0.3.0",
|
|
43
|
+
"@gallerist/renderer-dom": "0.3.0"
|
|
44
|
+
},
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/shafayet9780/gallerist.git",
|
|
48
|
+
"directory": "packages/react"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://github.com/shafayet9780/gallerist#readme",
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/shafayet9780/gallerist/issues"
|
|
53
|
+
},
|
|
54
|
+
"author": "Shafayet Ahmmed",
|
|
55
|
+
"keywords": [
|
|
56
|
+
"lightbox",
|
|
57
|
+
"gallery",
|
|
58
|
+
"image-viewer",
|
|
59
|
+
"media-viewer",
|
|
60
|
+
"photo-gallery",
|
|
61
|
+
"carousel",
|
|
62
|
+
"zoom",
|
|
63
|
+
"pan",
|
|
64
|
+
"overlays",
|
|
65
|
+
"headless",
|
|
66
|
+
"framework-agnostic",
|
|
67
|
+
"virtualized",
|
|
68
|
+
"nested-gallery",
|
|
69
|
+
"react",
|
|
70
|
+
"react-component",
|
|
71
|
+
"hook"
|
|
72
|
+
],
|
|
73
|
+
"publishConfig": {
|
|
74
|
+
"access": "public"
|
|
75
|
+
},
|
|
76
|
+
"scripts": {
|
|
77
|
+
"build": "tsup",
|
|
78
|
+
"typecheck": "tsc --noEmit"
|
|
79
|
+
}
|
|
6
80
|
}
|