@enigmax/primitives 0.25.0 → 0.26.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.
Files changed (48) hide show
  1. package/dist/chunk-4SZ5N7BA.js +121 -0
  2. package/dist/chunk-EYZ366LP.js +189 -0
  3. package/dist/chunk-ISXB5RUH.js +380 -0
  4. package/dist/chunk-JJVZYW5C.js +946 -0
  5. package/dist/chunk-QIIV2QSV.js +108 -0
  6. package/dist/chunk-RUEQ3TGP.js +155 -0
  7. package/dist/clipboard-menu-BvXJCSFN.d.ts +93 -0
  8. package/dist/context-BfjYGPnH.d.ts +54 -0
  9. package/dist/{clipboard-menu-B_ouitfS.d.ts → context-menu-D3FtTn7v.d.ts} +1 -91
  10. package/dist/index.d.ts +266 -2
  11. package/dist/index.js +4 -2
  12. package/dist/menu-MSQSE6U5.js +54 -0
  13. package/dist/menu-PNQRTRNV.js +33 -0
  14. package/dist/next/index.d.ts +6 -2
  15. package/dist/next/index.js +10 -6
  16. package/dist/react/context-menu.d.ts +8 -54
  17. package/dist/react/image.d.ts +109 -0
  18. package/dist/react/image.js +3 -0
  19. package/dist/react/index.d.ts +6 -2
  20. package/dist/react/index.js +10 -6
  21. package/dist/react/input.js +1 -1
  22. package/dist/react/video.d.ts +120 -0
  23. package/dist/react/video.js +3 -0
  24. package/dist/react-router/index.d.ts +6 -2
  25. package/dist/react-router/index.js +10 -6
  26. package/dist/viewer-73LSE6RR.js +3 -0
  27. package/package.json +15 -2
  28. package/recipes/image/styles.css +182 -0
  29. package/recipes/video/styles.css +154 -0
  30. package/registry.json +465 -0
  31. package/src/core/image-viewer.ts +222 -0
  32. package/src/core/player.ts +303 -0
  33. package/src/index.ts +52 -0
  34. package/src/react/image/icons.tsx +55 -0
  35. package/src/react/image/index.tsx +185 -0
  36. package/src/react/image/menu.tsx +76 -0
  37. package/src/react/image/styles.ts +213 -0
  38. package/src/react/image/types.ts +113 -0
  39. package/src/react/image/viewer.tsx +571 -0
  40. package/src/react/index.ts +3 -0
  41. package/src/react/video/icons.tsx +135 -0
  42. package/src/react/video/index.tsx +724 -0
  43. package/src/react/video/menu.tsx +66 -0
  44. package/src/react/video/rail.tsx +99 -0
  45. package/src/react/video/styles.ts +161 -0
  46. package/src/react/video/types.ts +135 -0
  47. /package/dist/{chunk-3MGBZOAU.js → chunk-RMNXVWQ4.js} +0 -0
  48. /package/dist/{chunk-MSOCCQGH.js → chunk-T7I6WJSN.js} +0 -0
@@ -0,0 +1,380 @@
1
+ import { IDENTITY, prefersReducedMotion, ZOOM_LIMITS, flightFrom, nextIndex, fittedSize, clampPan, zoomAt, pinchDistance, flightMs, ZOOM_STEP, ZOOM_DOUBLE, wheelFactor } from './chunk-QIIV2QSV.js';
2
+ import { injectImageStyles } from './chunk-EYZ366LP.js';
3
+ import { createPortal } from 'react-dom';
4
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
+ import { lazy, useLayoutEffect, useRef, useState, useMemo, useCallback, useEffect, Suspense } from 'react';
6
+
7
+ function Glyph({ children }) {
8
+ return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", width: "1.125em", height: "1.125em", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children });
9
+ }
10
+ function Close() {
11
+ return /* @__PURE__ */ jsx(Glyph, { children: /* @__PURE__ */ jsx("path", { d: "M6 6 18 18M18 6 6 18" }) });
12
+ }
13
+ function Plus() {
14
+ return /* @__PURE__ */ jsx(Glyph, { children: /* @__PURE__ */ jsx("path", { d: "M12 5v14M5 12h14" }) });
15
+ }
16
+ function Minus() {
17
+ return /* @__PURE__ */ jsx(Glyph, { children: /* @__PURE__ */ jsx("path", { d: "M5 12h14" }) });
18
+ }
19
+ function ChevronLeft() {
20
+ return /* @__PURE__ */ jsx(Glyph, { children: /* @__PURE__ */ jsx("path", { d: "m15 5-7 7 7 7" }) });
21
+ }
22
+ function ChevronRight() {
23
+ return /* @__PURE__ */ jsx(Glyph, { children: /* @__PURE__ */ jsx("path", { d: "m9 5 7 7-7 7" }) });
24
+ }
25
+ function Trash() {
26
+ return /* @__PURE__ */ jsx(Glyph, { children: /* @__PURE__ */ jsx("path", { d: "M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13" }) });
27
+ }
28
+ function Dots() {
29
+ return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", width: "1.125em", height: "1.125em", fill: "currentColor", "aria-hidden": "true", children: [
30
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "5", r: "1.75" }),
31
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "1.75" }),
32
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "19", r: "1.75" })
33
+ ] });
34
+ }
35
+ function Download() {
36
+ return /* @__PURE__ */ jsx(Glyph, { children: /* @__PURE__ */ jsx("path", { d: "M12 3v12M7 11l5 5 5-5M4 20h16" }) });
37
+ }
38
+ var ImageMenu = lazy(() => import('./menu-MSQSE6U5.js').then((module) => ({ default: module.ImageMenu })));
39
+ function ImageViewer({
40
+ items,
41
+ index,
42
+ onIndex,
43
+ onClose,
44
+ zoom,
45
+ navigation,
46
+ thumbnails,
47
+ menu,
48
+ discardable,
49
+ onDiscard,
50
+ loop,
51
+ caption,
52
+ origin,
53
+ animate,
54
+ styles,
55
+ labels
56
+ }) {
57
+ useLayoutEffect(() => {
58
+ if (styles) injectImageStyles();
59
+ }, [styles]);
60
+ const frameRef = useRef(null);
61
+ const stripRef = useRef(null);
62
+ const imageRef = useRef(null);
63
+ const [transform, setTransform] = useState(IDENTITY);
64
+ const [loading, setLoading] = useState(true);
65
+ const [panning, setPanning] = useState(false);
66
+ const [phase, setPhase] = useState(() => animate && origin && !prefersReducedMotion() ? "measuring" : "open");
67
+ const [flight, setFlight] = useState(null);
68
+ const [discarded, setDiscarded] = useState(() => /* @__PURE__ */ new Set());
69
+ const limits = useMemo(() => ({ min: zoom?.min ?? ZOOM_LIMITS.min, max: zoom?.max ?? ZOOM_LIMITS.max }), [zoom]);
70
+ const item = items[index];
71
+ const zoomed = transform.scale > limits.min;
72
+ const remaining = items.length - discarded.size;
73
+ const position = useMemo(() => {
74
+ let seen = 0;
75
+ for (let at = 0; at <= index && at < items.length; at += 1) if (!discarded.has(at)) seen += 1;
76
+ return seen;
77
+ }, [index, items.length, discarded]);
78
+ const layoutBox = useCallback(() => {
79
+ const frame = frameRef.current;
80
+ const element = imageRef.current;
81
+ if (!frame || !element?.offsetWidth) return null;
82
+ const box = frame.getBoundingClientRect();
83
+ return { left: box.left + element.offsetLeft, top: box.top + element.offsetTop, width: element.offsetWidth, height: element.offsetHeight };
84
+ }, []);
85
+ const requestClose = useCallback(() => {
86
+ if (phase === "closing" || phase === "measuring") return onClose();
87
+ const from = origin?.();
88
+ const to = layoutBox();
89
+ const back = animate && !prefersReducedMotion() && from && to ? flightFrom(from, to) : null;
90
+ if (!back) return onClose();
91
+ setPhase("closing");
92
+ setFlight(back);
93
+ }, [phase, origin, layoutBox, animate, onClose]);
94
+ const go = useCallback((step) => {
95
+ const next = nextIndex(index, items.length, step, { loop, skip: discarded });
96
+ if (next === -1 || next === index) return;
97
+ onIndex(next);
98
+ }, [index, items.length, loop, discarded, onIndex]);
99
+ const showAt = useCallback((next) => {
100
+ if (next === index || discarded.has(next)) return;
101
+ onIndex(next);
102
+ }, [index, discarded, onIndex]);
103
+ const discard = useCallback(() => {
104
+ if (!discardable || !item) return;
105
+ const next = nextIndex(index, items.length, 1, { loop: true, skip: /* @__PURE__ */ new Set([...discarded, index]) });
106
+ setDiscarded((current) => new Set(current).add(index));
107
+ onDiscard?.(item, index);
108
+ if (next === -1) requestClose();
109
+ else onIndex(next);
110
+ }, [discardable, item, index, items.length, discarded, onDiscard, onIndex, requestClose]);
111
+ const reset = useCallback(() => setTransform(IDENTITY), []);
112
+ useEffect(() => {
113
+ setTransform(IDENTITY);
114
+ setLoading(true);
115
+ }, [item?.src]);
116
+ const frameBox = useCallback(() => {
117
+ const box = frameRef.current?.getBoundingClientRect();
118
+ return box ? { left: box.left, top: box.top, width: box.width, height: box.height } : null;
119
+ }, []);
120
+ const bounded = useCallback((next) => {
121
+ const box = frameBox();
122
+ const element = imageRef.current;
123
+ if (!box || !element) return next;
124
+ const fitted = fittedSize({ width: element.naturalWidth, height: element.naturalHeight }, box);
125
+ return clampPan(next, box, fitted);
126
+ }, [frameBox]);
127
+ const zoomBy = useCallback((factor, point) => {
128
+ const box = frameBox();
129
+ if (!box) return;
130
+ const at = point ?? { x: box.left + box.width / 2, y: box.top + box.height / 2 };
131
+ setTransform((current) => bounded(zoomAt(current, factor, at, box, limits)));
132
+ }, [frameBox, bounded, limits]);
133
+ useEffect(() => {
134
+ const element = frameRef.current;
135
+ if (!element || !zoom?.wheel) return;
136
+ const onWheel = (event) => {
137
+ event.preventDefault();
138
+ zoomBy(wheelFactor(event.deltaY, event.deltaMode), { x: event.clientX, y: event.clientY });
139
+ };
140
+ element.addEventListener("wheel", onWheel, { passive: false });
141
+ return () => element.removeEventListener("wheel", onWheel);
142
+ }, [zoom?.wheel, zoomBy]);
143
+ const pointers = useRef(/* @__PURE__ */ new Map());
144
+ const pinch = useRef(null);
145
+ const backdropPress = useRef(null);
146
+ const onPointerDown = useCallback((event) => {
147
+ if (event.button !== 0 && event.pointerType === "mouse") return;
148
+ const element = event.currentTarget;
149
+ backdropPress.current = event.target === element && !zoomed ? { x: event.clientX, y: event.clientY } : null;
150
+ pointers.current.set(event.pointerId, { x: event.clientX, y: event.clientY });
151
+ if (pointers.current.size === 2) {
152
+ const [a, b] = [...pointers.current.values()];
153
+ pinch.current = { distance: pinchDistance(a, b), scale: transform.scale };
154
+ setPanning(false);
155
+ return;
156
+ }
157
+ if (!zoomed) return;
158
+ event.preventDefault();
159
+ setPanning(true);
160
+ try {
161
+ element.setPointerCapture(event.pointerId);
162
+ } catch {
163
+ }
164
+ }, [zoomed, transform.scale]);
165
+ const onPointerMove = useCallback((event) => {
166
+ const previous = pointers.current.get(event.pointerId);
167
+ if (!previous) return;
168
+ pointers.current.set(event.pointerId, { x: event.clientX, y: event.clientY });
169
+ if (pointers.current.size === 2 && pinch.current) {
170
+ const [a, b] = [...pointers.current.values()];
171
+ const distance = pinchDistance(a, b);
172
+ if (!pinch.current.distance) return;
173
+ const box = frameBox();
174
+ if (!box) return;
175
+ const factor = distance / pinch.current.distance * (pinch.current.scale / transform.scale);
176
+ setTransform((current) => bounded(zoomAt(current, factor, { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }, box, limits)));
177
+ return;
178
+ }
179
+ if (!panning) return;
180
+ setTransform((current) => bounded({ ...current, x: current.x + (event.clientX - previous.x), y: current.y + (event.clientY - previous.y) }));
181
+ }, [panning, bounded, frameBox, limits, transform.scale]);
182
+ const endPointer = useCallback((event) => {
183
+ pointers.current.delete(event.pointerId);
184
+ if (pointers.current.size < 2) pinch.current = null;
185
+ if (pointers.current.size === 0) setPanning(false);
186
+ const started = backdropPress.current;
187
+ backdropPress.current = null;
188
+ if (!started || event.target !== event.currentTarget) return;
189
+ if (Math.abs(event.clientX - started.x) < 4 && Math.abs(event.clientY - started.y) < 4) requestClose();
190
+ }, [requestClose]);
191
+ useLayoutEffect(() => {
192
+ if (phase !== "measuring") return;
193
+ const element = imageRef.current;
194
+ const from = origin?.();
195
+ if (!element || !from) {
196
+ setPhase("open");
197
+ return;
198
+ }
199
+ if (!element.complete) return;
200
+ const to = layoutBox();
201
+ const start = to ? flightFrom(from, to) : null;
202
+ if (!start) {
203
+ setPhase("open");
204
+ return;
205
+ }
206
+ setFlight(start);
207
+ let second = 0;
208
+ const first = requestAnimationFrame(() => {
209
+ second = requestAnimationFrame(() => {
210
+ setFlight(null);
211
+ setPhase("flying");
212
+ });
213
+ });
214
+ return () => {
215
+ cancelAnimationFrame(first);
216
+ cancelAnimationFrame(second);
217
+ };
218
+ }, [phase, origin, layoutBox, loading]);
219
+ useEffect(() => {
220
+ if (phase !== "flying" && phase !== "closing") return;
221
+ const element = imageRef.current;
222
+ const done = (event) => {
223
+ if (event && event.propertyName !== "transform") return;
224
+ if (phase === "closing") onClose();
225
+ else setPhase("open");
226
+ };
227
+ element?.addEventListener("transitionend", done);
228
+ const timer = window.setTimeout(() => done(), flightMs(element) + 80);
229
+ return () => {
230
+ element?.removeEventListener("transitionend", done);
231
+ window.clearTimeout(timer);
232
+ };
233
+ }, [phase, onClose]);
234
+ useEffect(() => {
235
+ const onKeyDown = (event) => {
236
+ if (event.defaultPrevented) return;
237
+ const key = event.key;
238
+ if (key === "Escape") return void (event.preventDefault(), requestClose());
239
+ if (navigation && (key === "ArrowRight" || key === "ArrowLeft") && !zoomed) {
240
+ event.preventDefault();
241
+ return go(key === "ArrowRight" ? 1 : -1);
242
+ }
243
+ if (zoom && (key === "+" || key === "=")) return void (event.preventDefault(), zoomBy(ZOOM_STEP));
244
+ if (zoom && key === "-") return void (event.preventDefault(), zoomBy(1 / ZOOM_STEP));
245
+ if (zoom && key === "0") return void (event.preventDefault(), reset());
246
+ if (zoomed && key.startsWith("Arrow")) {
247
+ event.preventDefault();
248
+ const step = event.shiftKey ? 120 : 40;
249
+ const by = key === "ArrowLeft" ? [step, 0] : key === "ArrowRight" ? [-step, 0] : key === "ArrowUp" ? [0, step] : [0, -step];
250
+ return setTransform((current) => bounded({ ...current, x: current.x + by[0], y: current.y + by[1] }));
251
+ }
252
+ if (discardable && (key === "Delete" || key === "Backspace")) return void (event.preventDefault(), discard());
253
+ };
254
+ document.addEventListener("keydown", onKeyDown);
255
+ return () => document.removeEventListener("keydown", onKeyDown);
256
+ }, [requestClose, navigation, zoom, zoomed, go, zoomBy, reset, bounded, discardable, discard]);
257
+ useEffect(() => {
258
+ frameRef.current?.focus();
259
+ const { body } = document;
260
+ const overflow = body.style.overflow;
261
+ body.style.overflow = "hidden";
262
+ return () => {
263
+ body.style.overflow = overflow;
264
+ };
265
+ }, []);
266
+ useEffect(() => {
267
+ if (!thumbnails) return;
268
+ stripRef.current?.querySelector("[aria-current=true]")?.scrollIntoView({ block: "nearest", inline: "center" });
269
+ }, [thumbnails, index]);
270
+ if (!item) return null;
271
+ const drawn = flight ?? transform;
272
+ const style = { transform: `translate(${drawn.x}px, ${drawn.y}px) scale(${drawn.scale})` };
273
+ const counter = (labels.counter ?? "{index} of {total}").replace("{index}", String(position)).replace("{total}", String(remaining));
274
+ return createPortal(
275
+ /* @__PURE__ */ jsxs(
276
+ "div",
277
+ {
278
+ "data-enigma-image-viewer": "",
279
+ "data-state": phase === "measuring" || phase === "flying" ? "opening" : phase,
280
+ role: "dialog",
281
+ "aria-modal": "true",
282
+ "aria-label": labels.viewer ?? "Image viewer",
283
+ onPointerDown: (event) => {
284
+ if (event.target === event.currentTarget) requestClose();
285
+ },
286
+ children: [
287
+ /* @__PURE__ */ jsxs("div", { "data-enigma-image-bar": "", children: [
288
+ navigation && /* @__PURE__ */ jsx("span", { "data-enigma-image-counter": "", children: counter }),
289
+ !navigation && /* @__PURE__ */ jsx("span", { "data-enigma-image-counter": "" }),
290
+ zoom && /* @__PURE__ */ jsxs(Fragment, { children: [
291
+ /* @__PURE__ */ jsx("button", { type: "button", "data-enigma-image-button": "", "aria-label": labels.zoomOut ?? "Zoom out", title: labels.zoomOut ?? "Zoom out", disabled: transform.scale <= limits.min, onClick: () => zoomBy(1 / ZOOM_STEP), children: /* @__PURE__ */ jsx(Minus, {}) }),
292
+ /* @__PURE__ */ jsx("button", { type: "button", "data-enigma-image-button": "", "aria-label": labels.zoomIn ?? "Zoom in", title: labels.zoomIn ?? "Zoom in", disabled: transform.scale >= limits.max, onClick: () => zoomBy(ZOOM_STEP), children: /* @__PURE__ */ jsx(Plus, {}) })
293
+ ] }),
294
+ discardable && /* @__PURE__ */ jsx("button", { type: "button", "data-enigma-image-button": "", "aria-label": labels.discard ?? "Discard this image", title: labels.discard ?? "Discard this image", onClick: discard, children: /* @__PURE__ */ jsx(Trash, {}) }),
295
+ menu && /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx(MenuButton, { label: labels.menu ?? "More", onPress: void 0 }), children: /* @__PURE__ */ jsx(ImageMenu, { item, index, options: menu, labels }) }),
296
+ /* @__PURE__ */ jsx("button", { type: "button", "data-enigma-image-button": "", "aria-label": labels.close ?? "Close", title: labels.close ?? "Close", onClick: requestClose, children: /* @__PURE__ */ jsx(Close, {}) })
297
+ ] }),
298
+ /* @__PURE__ */ jsxs(
299
+ "div",
300
+ {
301
+ ref: frameRef,
302
+ "data-enigma-image-frame": "",
303
+ "data-zoom": zoom ? "" : void 0,
304
+ "data-flying": phase === "open" ? void 0 : phase === "measuring" ? "pending" : "moving",
305
+ "data-zoomed": zoomed ? "" : void 0,
306
+ "data-panning": panning ? "" : void 0,
307
+ "data-loading": loading ? "" : void 0,
308
+ tabIndex: -1,
309
+ onPointerDown,
310
+ onPointerMove,
311
+ onPointerUp: endPointer,
312
+ onPointerCancel: endPointer,
313
+ onDoubleClick: (event) => {
314
+ if (!zoom?.doubleClick || phase !== "open") return;
315
+ if (zoomed) return reset();
316
+ zoomBy(ZOOM_DOUBLE / transform.scale, { x: event.clientX, y: event.clientY });
317
+ },
318
+ children: [
319
+ /* @__PURE__ */ jsx(
320
+ "img",
321
+ {
322
+ ref: imageRef,
323
+ src: item.src,
324
+ alt: item.alt ?? "",
325
+ style,
326
+ draggable: false,
327
+ onLoad: () => setLoading(false),
328
+ onError: () => setLoading(false)
329
+ },
330
+ item.src
331
+ ),
332
+ loading && /* @__PURE__ */ jsx("span", { "data-enigma-image-spinner": "", role: "progressbar", "aria-label": labels.viewer ?? "Image viewer" }),
333
+ navigation && remaining > 1 && /* @__PURE__ */ jsxs(Fragment, { children: [
334
+ /* @__PURE__ */ jsx("button", { type: "button", "data-enigma-image-button": "", "data-enigma-image-nav": "previous", "aria-label": labels.previous ?? "Previous image", title: labels.previous ?? "Previous image", onClick: () => go(-1), children: /* @__PURE__ */ jsx(ChevronLeft, {}) }),
335
+ /* @__PURE__ */ jsx("button", { type: "button", "data-enigma-image-button": "", "data-enigma-image-nav": "next", "aria-label": labels.next ?? "Next image", title: labels.next ?? "Next image", onClick: () => go(1), children: /* @__PURE__ */ jsx(ChevronRight, {}) })
336
+ ] })
337
+ ]
338
+ }
339
+ ),
340
+ /* @__PURE__ */ jsxs("div", { "data-enigma-image-foot": "", children: [
341
+ (item.caption ?? caption) && /* @__PURE__ */ jsx("p", { "data-enigma-image-caption": "", children: item.caption ?? caption }),
342
+ thumbnails && remaining > 1 && /* @__PURE__ */ jsx("div", { ref: stripRef, "data-enigma-image-strip": "", role: "tablist", "aria-label": labels.thumbnails ?? "Images", children: items.map((entry, at) => discarded.has(at) ? null : /* @__PURE__ */ jsx(
343
+ "button",
344
+ {
345
+ type: "button",
346
+ role: "tab",
347
+ "data-enigma-image-thumb": "",
348
+ "aria-current": at === index,
349
+ "aria-selected": at === index,
350
+ "aria-label": entry.alt ?? `${at + 1}`,
351
+ onClick: () => showAt(at),
352
+ children: /* @__PURE__ */ jsx("img", { src: entry.thumbnail ?? entry.src, alt: "", loading: "lazy", draggable: false })
353
+ },
354
+ entry.src
355
+ )) })
356
+ ] })
357
+ ]
358
+ }
359
+ ),
360
+ document.body
361
+ );
362
+ }
363
+ function MenuButton({ label, onPress, expanded }) {
364
+ return /* @__PURE__ */ jsx(
365
+ "button",
366
+ {
367
+ type: "button",
368
+ "data-enigma-image-button": "",
369
+ "data-enigma-image-menu": "",
370
+ "aria-haspopup": "menu",
371
+ "aria-expanded": expanded ?? false,
372
+ "aria-label": label,
373
+ title: label,
374
+ onClick: onPress,
375
+ children: /* @__PURE__ */ jsx(Dots, {})
376
+ }
377
+ );
378
+ }
379
+
380
+ export { Download, ImageViewer, MenuButton };