@gallerist/plugin-pins 0.0.0-stage → 0.3.1
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 +203 -2
- package/dist/index.cjs +491 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +130 -0
- package/dist/index.d.ts +130 -0
- package/dist/index.js +487 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -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,204 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @gallerist/plugin-pins
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Gallerist plugin — pins anchored to the image: comment threads, product tags or feature notes.
|
|
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/plugin-pins
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
A pin is a point on an item, as a fraction of the image (`space: 'normalized'`, the default) or
|
|
16
|
+
in image pixels (`space: 'natural'`). It follows pan and zoom at a constant screen size, and
|
|
17
|
+
clicking it opens a card beside it. By default pins are read from `item.meta.pins`:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { createViewer } from '@gallerist/core'
|
|
21
|
+
import { pins } from '@gallerist/plugin-pins'
|
|
22
|
+
import { mount } from '@gallerist/renderer-dom'
|
|
23
|
+
import '@gallerist/theme-default/gallerist.css' // includes the pin styles
|
|
24
|
+
|
|
25
|
+
const viewer = createViewer({
|
|
26
|
+
items: [
|
|
27
|
+
{
|
|
28
|
+
id: 'living-room',
|
|
29
|
+
type: 'image',
|
|
30
|
+
source: 'room.jpg',
|
|
31
|
+
meta: {
|
|
32
|
+
pins: [{ id: 'sofa', x: 0.5, y: 0.47, label: 'Marlow sofa', meta: '$2,490', href: '/sofa' }],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
plugins: [pins({ presentation: 'tag' })],
|
|
37
|
+
})
|
|
38
|
+
mount(viewer)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
| Presentation | Marker | Card |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `tag` (default) | a frosted ring | `label`, `meta` (price), `body`, a link (`href`, `linkLabel`) |
|
|
44
|
+
| `comment` | a numbered disc in the accent; muted when `resolved` | `label` (author), `meta` (time), `body`, `replies`, a resolved badge |
|
|
45
|
+
| `feature` | a numbered disc | `label`, `meta`, `body`, an optional link |
|
|
46
|
+
|
|
47
|
+
The default card links only to `http:`, `https:`, `mailto:` and `tel:` URLs, and to relative ones
|
|
48
|
+
(judged as http, whatever the page's own scheme). Any other `href` (`javascript:`, `data:`, `file:`,
|
|
49
|
+
`blob:` …) shows the card without its link. A `renderCard` card is yours to check.
|
|
50
|
+
|
|
51
|
+
| Option | Default | |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| `presentation` | `'tag'` | how pins look and what the card holds |
|
|
54
|
+
| `pins(item, group)` | `item.meta.pins` | where pins come from |
|
|
55
|
+
| `visible` | `true` | start shown; the toolbar toggle and `p` flip it |
|
|
56
|
+
| `toggle` | `true` | add the show/hide button to the toolbar |
|
|
57
|
+
| `renderCard(pin, ctx)` | — | replace the card's contents; return `null` to keep the default card for that pin. Called when the card opens, and again when an open card is redrawn |
|
|
58
|
+
| `onCardRemove(pin, node)` | — | a node `renderCard` returned left its card: unmount what you rendered there (once per node; never for default cards) |
|
|
59
|
+
| `onOpenChange(pin \| null, ctx)` | — | a card opened (`pin`) or closed (`null`); `ctx.previous` is the pin whose card was open until then (the one that closed) |
|
|
60
|
+
| `onPlace(at, ctx)` | — | turn on placing: the "Add a comment" button (or `c`) arms one click on the image, reported as `{ x, y }` in 0..1 |
|
|
61
|
+
| `strings` | English | every label, for translation |
|
|
62
|
+
|
|
63
|
+
The returned plugin also has `refresh()` (re-read pins you changed in place), `setVisible(bool)` and
|
|
64
|
+
`open(id | null, { focus })`: `focus: true` moves focus into the card as it opens (once its pin is
|
|
65
|
+
painted, if it isn't yet, unless focus moved elsewhere meanwhile), or to the pin as it closes. An
|
|
66
|
+
`open(id)` before the plugin has joined a viewer waits and opens on the first paint; after `destroy()`
|
|
67
|
+
it does nothing. Until the card is drawn, a click elsewhere doesn't cancel it (the click that creates
|
|
68
|
+
or opens the viewer). An overlay viewer moves focus into itself as it opens, which counts as focus
|
|
69
|
+
having moved: an `open(id, { focus: true })` made before `viewer.open()` may open the card without
|
|
70
|
+
focusing it. To focus the card, call it once the viewer is open.
|
|
71
|
+
|
|
72
|
+
Use one `pins()` per viewer: the plugin keeps the open card, its listeners and its viewer in the
|
|
73
|
+
instance, so two live viewers sharing one would close each other's cards. Each gallery gets its own
|
|
74
|
+
`pins({...})`; the same instance can move to a new viewer once the old one is destroyed (a React remount). Escape closes an open card (or cancels placing)
|
|
75
|
+
without closing the viewer, wherever focus is; a field in a `renderCard` card keeps Escape for itself
|
|
76
|
+
by calling `event.preventDefault()` in its own `keydown` handler. A card opens beside its pin toward
|
|
77
|
+
the side of the stage with more room and stays inside the stage on both axes: level with its pin when
|
|
78
|
+
it fits, else slid along it (across too, over the pin, when the room beside it is narrow), never
|
|
79
|
+
under a band floated over the stage (immersive) or the docked item navigator, and never wider or
|
|
80
|
+
taller than the room. A taller card scrolls: it then joins the tab order, ↑ ↓ Home End Page Up/Down
|
|
81
|
+
scroll it rather than navigate, a wheel over it scrolls it, and its close button stays in view. A
|
|
82
|
+
wheel over something in a card that scrolls on the wheel's axis (the card, or a textarea or list of
|
|
83
|
+
yours in it) scrolls that; over anything else, or a pinch (ctrl+wheel) anywhere on a card, it zooms
|
|
84
|
+
the image as anywhere on the stage. A touch in a card never pans or zooms the page (a textarea or
|
|
85
|
+
select of yours pans vertically). The card lays out its contents in normal block flow, as your page
|
|
86
|
+
would (a fragment's inline siblings share a line, a button keeps its width); the close button comes
|
|
87
|
+
first in it, in reading and tab order too. A card is placed again
|
|
88
|
+
after a zoom or pan and as its contents resize, so a host may render into it late. While placing, a
|
|
89
|
+
pan or a click beside the image keeps the mode armed; a click outside the stage cancels it.
|
|
90
|
+
|
|
91
|
+
Focus never falls to `<body>` (where the next Escape would close the viewer): when a card closes or is
|
|
92
|
+
redrawn while focus is inside it, focus goes to its pin if that is still on the current item, else to
|
|
93
|
+
the "Add a comment" button, else to the viewer. Focus already on `<body>` is moved too in an open overlay
|
|
94
|
+
viewer, but not in an inline one, where it may have come from a control of your page. The same holds when a
|
|
95
|
+
repaint drops the pin that has focus (you removed it, say an empty draft in `onOpenChange` followed by
|
|
96
|
+
`refresh()`): focus moves on once the repaint is done. Focus the plugin moves shows the focus ring
|
|
97
|
+
when the input that led to it was a key (Escape, Enter on ×; not a shortcut with meta, ctrl or alt) or
|
|
98
|
+
a click without a pointer (assistive tech), and not after a pointer (`focusVisible: true` / `false`; a
|
|
99
|
+
browser without the option decides for itself).
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
const commentPins = pins({
|
|
103
|
+
presentation: 'comment',
|
|
104
|
+
onPlace: (at, { item }) => {
|
|
105
|
+
item.meta.pins.push({ id: crypto.randomUUID(), ...at, label: 'You', body: '' })
|
|
106
|
+
commentPins.refresh()
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`renderCard` can replace some cards and keep the rest, or build on the default: `defaultPinCard(pin,
|
|
112
|
+
{ presentation, strings })` returns the contents `pins()` draws (a `DocumentFragment`; text is set as
|
|
113
|
+
text). Pass the same `presentation` as the plugin (default `tag`).
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { defaultPinCard, pins } from '@gallerist/plugin-pins'
|
|
117
|
+
|
|
118
|
+
pins({
|
|
119
|
+
presentation: 'comment',
|
|
120
|
+
renderCard: (pin) => {
|
|
121
|
+
if (!pin.meta?.startsWith('v2')) return null // the default card
|
|
122
|
+
const card = document.createElement('div')
|
|
123
|
+
card.append(defaultPinCard(pin, { presentation: 'comment' }), versionBadge(pin))
|
|
124
|
+
return card
|
|
125
|
+
},
|
|
126
|
+
})
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Host-rendered cards (React)
|
|
130
|
+
|
|
131
|
+
A card holds its contents only while it is open: `renderCard` runs as it opens, and again when an open
|
|
132
|
+
card is redrawn (`refresh()`, new data for its item), and the node it returned is handed back to
|
|
133
|
+
`onCardRemove` once it has left the card: the card closed (its pin, Escape, `open()`, another pin, a
|
|
134
|
+
click outside), moved to another item, its pin left the data, the redraw replaced it, or the plugin or
|
|
135
|
+
viewer was destroyed. Exactly once per node, so a portal or root can follow it. Return an element (a
|
|
136
|
+
`DocumentFragment` empties as it is inserted).
|
|
137
|
+
|
|
138
|
+
- Returning the same node again is supported (say, one cached per pin): a redraw that gets it back
|
|
139
|
+
doesn't hand it to `onCardRemove`; it is handed back when it really leaves, once each time it does.
|
|
140
|
+
- If `renderCard` throws, that pin gets the default card and the error is reported (`reportError`, so
|
|
141
|
+
`window.onerror`; else `console.error`) once the card has opened. The same goes for a callback that
|
|
142
|
+
throws: the rest still run.
|
|
143
|
+
- `onCardRemove` and `onOpenChange` run after the change has settled, in order. Calling `open()` or
|
|
144
|
+
`refresh()` from them is fine (its own events follow); from `renderCard` it isn't. So when one
|
|
145
|
+
does, an earlier `onOpenChange` may describe a state that a later change has already replaced:
|
|
146
|
+
the last one is current.
|
|
147
|
+
- A callback that calls `viewer.destroy()` doesn't stop the ones queued after it: they still run,
|
|
148
|
+
with the destroyed viewer's `ctx`. Once the plugin has left its viewer, `open()` does nothing (no
|
|
149
|
+
card the next viewer would open unprompted).
|
|
150
|
+
|
|
151
|
+
A portal keeps the card in your React tree (context, state, event handlers). Give each node its own
|
|
152
|
+
key (a counter, not `pin.id`: a redraw briefly has two nodes for the same pin):
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
import type { Collection } from '@gallerist/core'
|
|
156
|
+
import { type Pin, pins } from '@gallerist/plugin-pins'
|
|
157
|
+
import { Gallerist } from '@gallerist/react'
|
|
158
|
+
import { useMemo, useState } from 'react'
|
|
159
|
+
import { createPortal } from 'react-dom'
|
|
160
|
+
|
|
161
|
+
type Card = { key: number; pin: Pin; node: HTMLElement }
|
|
162
|
+
let nextKey = 0
|
|
163
|
+
|
|
164
|
+
function ReviewPins(props: { collection: Collection; onSave: (pin: Pin, text: string) => void }) {
|
|
165
|
+
const { collection, onSave } = props
|
|
166
|
+
const [cards, setCards] = useState<Card[]>([])
|
|
167
|
+
const plugin = useMemo(
|
|
168
|
+
() =>
|
|
169
|
+
pins({
|
|
170
|
+
presentation: 'comment',
|
|
171
|
+
renderCard: (pin) => {
|
|
172
|
+
const node = document.createElement('div')
|
|
173
|
+
setCards((list) => [...list, { key: ++nextKey, pin, node }])
|
|
174
|
+
return node
|
|
175
|
+
},
|
|
176
|
+
onCardRemove: (_pin, node) => setCards((list) => list.filter((c) => c.node !== node)),
|
|
177
|
+
}),
|
|
178
|
+
[],
|
|
179
|
+
)
|
|
180
|
+
return (
|
|
181
|
+
<>
|
|
182
|
+
<Gallerist collection={collection} plugins={[plugin]} preset="reviewer" skin="console" />
|
|
183
|
+
{cards.map(({ key, pin, node }) =>
|
|
184
|
+
createPortal(<CommentForm pin={pin} onSave={onSave} />, node, String(key)),
|
|
185
|
+
)}
|
|
186
|
+
</>
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
After saving, update your data and call `plugin.refresh()` (or pass a new collection): the card is
|
|
192
|
+
redrawn from the new data, focus is on its pin, and Escape closes the card rather than the viewer.
|
|
193
|
+
With a root per card instead of a portal: `createRoot(node).render(…)` in `renderCard`, and
|
|
194
|
+
`root.unmount()` in `onCardRemove` (keep the roots in a `Map` by node).
|
|
195
|
+
|
|
196
|
+
Pins take their colours from the skin (`--gallerist-pin-*`, the accent, the panel surface), so they
|
|
197
|
+
match Gallery, Console and the light schemes. A live demo of all three presentations is in
|
|
198
|
+
[`examples/hotspots`](https://github.com/shafayet9780/gallerist/tree/main/examples/hotspots).
|
|
199
|
+
|
|
200
|
+
Full docs: the [monorepo README](https://github.com/shafayet9780/gallerist#readme) and [`docs/`](https://github.com/shafayet9780/gallerist/tree/main/docs).
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
MIT © Shafayet Ahmmed
|