@lobehub/ui 5.24.0 → 5.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 (171) hide show
  1. package/es/A/index.mjs +2 -1
  2. package/es/A/index.mjs.map +1 -1
  3. package/es/Accordion/AccordionItem.mjs +0 -1
  4. package/es/Accordion/AccordionItem.mjs.map +1 -1
  5. package/es/ActionIcon/components/utils.mjs +7 -9
  6. package/es/ActionIcon/components/utils.mjs.map +1 -1
  7. package/es/Checkbox/CheckboxGroup.mjs +4 -2
  8. package/es/Checkbox/CheckboxGroup.mjs.map +1 -1
  9. package/es/CodeDiff/CodeDiff.mjs +20 -18
  10. package/es/CodeDiff/CodeDiff.mjs.map +1 -1
  11. package/es/CodeDiff/PatchDiff.mjs +18 -16
  12. package/es/CodeDiff/PatchDiff.mjs.map +1 -1
  13. package/es/CopyButton/CopyButton.mjs +2 -1
  14. package/es/CopyButton/CopyButton.mjs.map +1 -1
  15. package/es/Highlighter/LangSelect.mjs +41 -40
  16. package/es/Highlighter/LangSelect.mjs.map +1 -1
  17. package/es/Highlighter/SyntaxHighlighter/StaticRenderer.mjs +2 -1
  18. package/es/Highlighter/SyntaxHighlighter/StaticRenderer.mjs.map +1 -1
  19. package/es/HotkeyInput/HotkeyInput.mjs +3 -1
  20. package/es/HotkeyInput/HotkeyInput.mjs.map +1 -1
  21. package/es/HtmlPreview/const.mjs +1 -1
  22. package/es/HtmlPreview/const.mjs.map +1 -1
  23. package/es/Icon/components/utils.mjs +7 -9
  24. package/es/Icon/components/utils.mjs.map +1 -1
  25. package/es/Image/Image.mjs +138 -37
  26. package/es/Image/Image.mjs.map +1 -1
  27. package/es/Image/PreviewGroup.d.mts +1 -0
  28. package/es/Image/PreviewGroup.mjs +33 -10
  29. package/es/Image/PreviewGroup.mjs.map +1 -1
  30. package/es/Image/index.d.mts +2 -2
  31. package/es/Image/index.mjs +1 -1
  32. package/es/Image/style.mjs +126 -71
  33. package/es/Image/style.mjs.map +1 -1
  34. package/es/Image/type.d.mts +14 -17
  35. package/es/Image/viewer/GalleryNav.mjs +44 -0
  36. package/es/Image/viewer/GalleryNav.mjs.map +1 -0
  37. package/es/Image/viewer/ImageViewer.mjs +275 -0
  38. package/es/Image/viewer/ImageViewer.mjs.map +1 -0
  39. package/es/Image/viewer/PreviewOutlet.mjs +19 -0
  40. package/es/Image/viewer/PreviewOutlet.mjs.map +1 -0
  41. package/es/Image/viewer/Toolbar.mjs +184 -0
  42. package/es/Image/viewer/Toolbar.mjs.map +1 -0
  43. package/es/Image/viewer/ViewerChrome.mjs +54 -0
  44. package/es/Image/viewer/ViewerChrome.mjs.map +1 -0
  45. package/es/Image/viewer/geometry.mjs +85 -0
  46. package/es/Image/viewer/geometry.mjs.map +1 -0
  47. package/es/Image/viewer/registry.d.mts +2 -0
  48. package/es/Image/viewer/registry.mjs +52 -0
  49. package/es/Image/viewer/registry.mjs.map +1 -0
  50. package/es/Image/viewer/useChromeIdle.mjs +72 -0
  51. package/es/Image/viewer/useChromeIdle.mjs.map +1 -0
  52. package/es/Image/viewer/useFinalFocus.mjs +10 -0
  53. package/es/Image/viewer/useFinalFocus.mjs.map +1 -0
  54. package/es/Image/viewer/useFlipTransition.mjs +321 -0
  55. package/es/Image/viewer/useFlipTransition.mjs.map +1 -0
  56. package/es/Image/viewer/useGalleryNav.mjs +89 -0
  57. package/es/Image/viewer/useGalleryNav.mjs.map +1 -0
  58. package/es/Image/viewer/useRefitTransition.mjs +47 -0
  59. package/es/Image/viewer/useRefitTransition.mjs.map +1 -0
  60. package/es/Image/viewer/useViewerGestures.mjs +148 -0
  61. package/es/Image/viewer/useViewerGestures.mjs.map +1 -0
  62. package/es/Image/viewer/useZoomPan.mjs +346 -0
  63. package/es/Image/viewer/useZoomPan.mjs.map +1 -0
  64. package/es/Img/index.mjs +2 -1
  65. package/es/Img/index.mjs.map +1 -1
  66. package/es/Markdown/SyntaxMarkdown/StreamdownRender.mjs +2 -1
  67. package/es/Markdown/SyntaxMarkdown/StreamdownRender.mjs.map +1 -1
  68. package/es/Markdown/SyntaxMarkdown/useSmoothStreamContent.mjs +2 -2
  69. package/es/Markdown/SyntaxMarkdown/useSmoothStreamContent.mjs.map +1 -1
  70. package/es/Modal/RawModalStackItem.mjs +6 -5
  71. package/es/Modal/RawModalStackItem.mjs.map +1 -1
  72. package/es/Modal/imperative.mjs +4 -2
  73. package/es/Modal/imperative.mjs.map +1 -1
  74. package/es/NeuralNetworkLoading/NeuralNetworkLoading.mjs +1 -1
  75. package/es/NeuralNetworkLoading/NeuralNetworkLoading.mjs.map +1 -1
  76. package/es/ScrollShadow/ScrollShadow.mjs +13 -12
  77. package/es/ScrollShadow/ScrollShadow.mjs.map +1 -1
  78. package/es/Skeleton/SkeletonButton.mjs +2 -1
  79. package/es/Skeleton/SkeletonButton.mjs.map +1 -1
  80. package/es/SortableList/SortableList.mjs +5 -1
  81. package/es/SortableList/SortableList.mjs.map +1 -1
  82. package/es/Text/Text.mjs +22 -21
  83. package/es/Text/Text.mjs.map +1 -1
  84. package/es/ThemeProvider/ThemeProvider.mjs +3 -2
  85. package/es/ThemeProvider/ThemeProvider.mjs.map +1 -1
  86. package/es/awesome/GradientButton/GradientButton.mjs +1 -3
  87. package/es/awesome/GradientButton/GradientButton.mjs.map +1 -1
  88. package/es/awesome/Spotlight/Spotlight.mjs +2 -1
  89. package/es/awesome/Spotlight/Spotlight.mjs.map +1 -1
  90. package/es/base-ui/Button/Button.mjs +13 -11
  91. package/es/base-ui/Button/Button.mjs.map +1 -1
  92. package/es/base-ui/Button/SplitButton.mjs +2 -1
  93. package/es/base-ui/Button/SplitButton.mjs.map +1 -1
  94. package/es/base-ui/ContextMenu/ContextMenuTrigger.mjs +2 -1
  95. package/es/base-ui/ContextMenu/ContextMenuTrigger.mjs.map +1 -1
  96. package/es/base-ui/ContextMenu/renderItems.mjs +2 -1
  97. package/es/base-ui/ContextMenu/renderItems.mjs.map +1 -1
  98. package/es/base-ui/Drawer/Drawer.d.mts +6 -0
  99. package/es/base-ui/Drawer/Drawer.mjs +152 -0
  100. package/es/base-ui/Drawer/Drawer.mjs.map +1 -0
  101. package/es/base-ui/Drawer/DrawerLayerContext.mjs +10 -0
  102. package/es/base-ui/Drawer/DrawerLayerContext.mjs.map +1 -0
  103. package/es/base-ui/Drawer/DrawerPushContext.mjs +27 -0
  104. package/es/base-ui/Drawer/DrawerPushContext.mjs.map +1 -0
  105. package/es/base-ui/Drawer/atoms.d.mts +58 -0
  106. package/es/base-ui/Drawer/atoms.mjs +262 -0
  107. package/es/base-ui/Drawer/atoms.mjs.map +1 -0
  108. package/es/base-ui/Drawer/constants.d.mts +17 -0
  109. package/es/base-ui/Drawer/constants.mjs +72 -0
  110. package/es/base-ui/Drawer/constants.mjs.map +1 -0
  111. package/es/base-ui/Drawer/index.d.mts +5 -0
  112. package/es/base-ui/Drawer/style.mjs +295 -0
  113. package/es/base-ui/Drawer/style.mjs.map +1 -0
  114. package/es/base-ui/Drawer/type.d.mts +65 -0
  115. package/es/base-ui/DropdownMenu/DropdownMenu.mjs +2 -1
  116. package/es/base-ui/DropdownMenu/DropdownMenu.mjs.map +1 -1
  117. package/es/base-ui/DropdownMenu/atoms.mjs +2 -1
  118. package/es/base-ui/DropdownMenu/atoms.mjs.map +1 -1
  119. package/es/base-ui/DropdownMenu/renderItems.mjs +2 -1
  120. package/es/base-ui/DropdownMenu/renderItems.mjs.map +1 -1
  121. package/es/base-ui/FloatingSheet/FloatingSheet.mjs +4 -1
  122. package/es/base-ui/FloatingSheet/FloatingSheet.mjs.map +1 -1
  123. package/es/base-ui/Input/style.d.mts +1 -1
  124. package/es/base-ui/Modal/atoms.mjs +7 -5
  125. package/es/base-ui/Modal/atoms.mjs.map +1 -1
  126. package/es/base-ui/Modal/imperative.mjs +2 -1
  127. package/es/base-ui/Modal/imperative.mjs.map +1 -1
  128. package/es/base-ui/Popover/atoms.mjs +2 -1
  129. package/es/base-ui/Popover/atoms.mjs.map +1 -1
  130. package/es/base-ui/Popover/context.mjs +4 -2
  131. package/es/base-ui/Popover/context.mjs.map +1 -1
  132. package/es/base-ui/Select/atoms.mjs +2 -1
  133. package/es/base-ui/Select/atoms.mjs.map +1 -1
  134. package/es/base-ui/Toast/hostGuard.mjs +26 -0
  135. package/es/base-ui/Toast/hostGuard.mjs.map +1 -0
  136. package/es/base-ui/Toast/imperative.mjs +43 -20
  137. package/es/base-ui/Toast/imperative.mjs.map +1 -1
  138. package/es/base-ui/Toast/pendingQueue.mjs +30 -0
  139. package/es/base-ui/Toast/pendingQueue.mjs.map +1 -0
  140. package/es/base-ui/index.d.mts +6 -1
  141. package/es/base-ui/index.mjs +5 -2
  142. package/es/brand/LobeChat/index.mjs +0 -1
  143. package/es/brand/LobeChat/index.mjs.map +1 -1
  144. package/es/brand/LobeHub/index.mjs +0 -1
  145. package/es/brand/LobeHub/index.mjs.map +1 -1
  146. package/es/brand/Logo3d/index.mjs +2 -1
  147. package/es/brand/Logo3d/index.mjs.map +1 -1
  148. package/es/chat/ChatItem/components/Actions.mjs +5 -4
  149. package/es/chat/ChatItem/components/Actions.mjs.map +1 -1
  150. package/es/chat/ChatList/ChatList.mjs +2 -1
  151. package/es/chat/ChatList/ChatList.mjs.map +1 -1
  152. package/es/color/ColorScales/ScaleRow.mjs +0 -2
  153. package/es/color/ColorScales/ScaleRow.mjs.map +1 -1
  154. package/es/hooks/useMermaid.mjs +2 -1
  155. package/es/hooks/useMermaid.mjs.map +1 -1
  156. package/es/i18n/resources/en/image.d.mts +5 -0
  157. package/es/i18n/resources/en/image.mjs +6 -1
  158. package/es/i18n/resources/en/image.mjs.map +1 -1
  159. package/es/i18n/resources/zhCn/image.d.mts +5 -0
  160. package/es/i18n/resources/zhCn/image.mjs +6 -1
  161. package/es/i18n/resources/zhCn/image.mjs.map +1 -1
  162. package/es/index.mjs +8 -8
  163. package/package.json +1 -1
  164. package/es/Image/components/Preview.mjs +0 -23
  165. package/es/Image/components/Preview.mjs.map +0 -1
  166. package/es/Image/components/Toolbar.mjs +0 -128
  167. package/es/Image/components/Toolbar.mjs.map +0 -1
  168. package/es/Image/components/usePreview.mjs +0 -58
  169. package/es/Image/components/usePreview.mjs.map +0 -1
  170. package/es/Image/components/usePreviewGroup.mjs +0 -57
  171. package/es/Image/components/usePreviewGroup.mjs.map +0 -1
@@ -0,0 +1,321 @@
1
+ import { useCallback, useLayoutEffect, useMemo, useRef, useState } from "react";
2
+ import { animate, motionValue } from "motion/react";
3
+ //#region src/Image/viewer/useFlipTransition.ts
4
+ const OPEN_SPRING = {
5
+ bounce: .15,
6
+ type: "spring",
7
+ visualDuration: .3
8
+ };
9
+ const PUSH_SPRING = {
10
+ bounce: 0,
11
+ type: "spring",
12
+ visualDuration: .3
13
+ };
14
+ const FADE = {
15
+ duration: .15,
16
+ ease: "easeOut"
17
+ };
18
+ const CHROME_FADE = {
19
+ delay: .15,
20
+ duration: .2,
21
+ ease: "easeOut"
22
+ };
23
+ const FADE_SCALE = .92;
24
+ const SWITCH_FADE_SCALE = .96;
25
+ const SETTLE_FALLBACK_MS = 1e3;
26
+ const measureSource = (element) => {
27
+ if (!element.isConnected) return null;
28
+ const rect = element.getBoundingClientRect();
29
+ if (rect.width <= 0 || rect.height <= 0) return null;
30
+ if (rect.bottom <= 0 || rect.right <= 0) return null;
31
+ if (rect.top >= window.innerHeight || rect.left >= window.innerWidth) return null;
32
+ return rect;
33
+ };
34
+ const sourceTransform = (rect, fit) => ({
35
+ scale: fit.width > 0 && fit.height > 0 ? Math.max(rect.width / fit.width, rect.height / fit.height) : 1,
36
+ x: rect.left + rect.width / 2 - (fit.x + fit.width / 2),
37
+ y: rect.top + rect.height / 2 - (fit.y + fit.height / 2)
38
+ });
39
+ const readTransform = (transform) => [
40
+ `translate3d(${transform.x.get()}px, ${transform.y.get()}px, 0)`,
41
+ `scale(${transform.scale.get()})`,
42
+ `rotate(${transform.rotate.get()}deg)`,
43
+ `scaleX(${transform.flipX.get() ? -1 : 1})`,
44
+ `scaleY(${transform.flipY.get() ? -1 : 1})`
45
+ ].join(" ");
46
+ const useBoundNode = (apply) => {
47
+ const nodeRef = useRef(null);
48
+ const applyRef = useRef(apply);
49
+ applyRef.current = apply;
50
+ const setNode = useCallback((node) => {
51
+ nodeRef.current = node;
52
+ if (node) applyRef.current(node);
53
+ }, []);
54
+ const refresh = useCallback(() => {
55
+ const node = nodeRef.current;
56
+ if (node) applyRef.current(node);
57
+ }, []);
58
+ const getNode = useCallback(() => nodeRef.current, []);
59
+ return useMemo(() => ({
60
+ getNode,
61
+ refresh,
62
+ setNode
63
+ }), [
64
+ getNode,
65
+ refresh,
66
+ setNode
67
+ ]);
68
+ };
69
+ const useFlipTransition = ({ animated, getCloseSource, getFitRect, getViewportWidth, onClosed, source, transform }) => {
70
+ const [opacity] = useState(() => ({
71
+ backdrop: motionValue(0),
72
+ chrome: motionValue(0),
73
+ image: motionValue(animated ? 1 : 0)
74
+ }));
75
+ const image = useBoundNode(useCallback((node) => {
76
+ node.style.transform = readTransform(transform);
77
+ node.style.opacity = String(opacity.image.get());
78
+ }, [opacity, transform]));
79
+ const backdrop = useBoundNode(useCallback((node) => {
80
+ node.style.opacity = String(opacity.backdrop.get());
81
+ }, [opacity]));
82
+ const chrome = useBoundNode(useCallback((node) => {
83
+ node.style.opacity = String(opacity.chrome.get());
84
+ }, [opacity]));
85
+ const runningRef = useRef([]);
86
+ const closingRef = useRef(false);
87
+ const transitioningRef = useRef(true);
88
+ const onClosedRef = useRef(onClosed);
89
+ onClosedRef.current = onClosed;
90
+ const getFitRectRef = useRef(getFitRect);
91
+ getFitRectRef.current = getFitRect;
92
+ const getCloseSourceRef = useRef(getCloseSource);
93
+ getCloseSourceRef.current = getCloseSource;
94
+ const getViewportWidthRef = useRef(getViewportWidth);
95
+ getViewportWidthRef.current = getViewportWidth;
96
+ const ghostRef = useRef(null);
97
+ const removeGhost = useCallback(() => {
98
+ ghostRef.current?.remove();
99
+ ghostRef.current = null;
100
+ }, []);
101
+ const run = useCallback((animations) => {
102
+ runningRef.current = [...runningRef.current, ...animations];
103
+ }, []);
104
+ const stopAll = useCallback(() => {
105
+ for (const animation of runningRef.current) animation.stop();
106
+ runningRef.current = [];
107
+ }, []);
108
+ const isTransitioning = useCallback(() => transitioningRef.current, []);
109
+ const isClosing = useCallback(() => closingRef.current, []);
110
+ const trackTimeout = useCallback((callback, ms) => {
111
+ const id = window.setTimeout(callback, ms);
112
+ run([{ stop: () => window.clearTimeout(id) }]);
113
+ }, [run]);
114
+ const animateSettling = useCallback((axes, onSettled) => {
115
+ let pending = axes.length;
116
+ let settled = false;
117
+ const finish = () => {
118
+ if (settled) return;
119
+ settled = true;
120
+ onSettled();
121
+ };
122
+ const onAxisComplete = () => {
123
+ pending -= 1;
124
+ if (pending === 0) finish();
125
+ };
126
+ run(axes.map(([value, target]) => animate(value, target, {
127
+ ...OPEN_SPRING,
128
+ onComplete: onAxisComplete
129
+ })));
130
+ trackTimeout(finish, SETTLE_FALLBACK_MS);
131
+ }, [run, trackTimeout]);
132
+ useLayoutEffect(() => {
133
+ const unsubscribes = [
134
+ ...[
135
+ transform.flipX,
136
+ transform.flipY,
137
+ transform.rotate,
138
+ transform.scale,
139
+ transform.x,
140
+ transform.y,
141
+ opacity.image
142
+ ].map((value) => value.on("change", image.refresh)),
143
+ opacity.backdrop.on("change", backdrop.refresh),
144
+ opacity.chrome.on("change", chrome.refresh)
145
+ ];
146
+ return () => {
147
+ for (const unsubscribe of unsubscribes) unsubscribe();
148
+ };
149
+ }, [
150
+ backdrop,
151
+ chrome,
152
+ image,
153
+ opacity,
154
+ transform
155
+ ]);
156
+ useLayoutEffect(() => {
157
+ const rect = animated ? measureSource(source) : null;
158
+ transitioningRef.current = true;
159
+ const markSettled = () => {
160
+ transitioningRef.current = false;
161
+ };
162
+ if (rect) {
163
+ const start = sourceTransform(rect, getFitRectRef.current());
164
+ transform.scale.set(start.scale);
165
+ transform.x.set(start.x);
166
+ transform.y.set(start.y);
167
+ opacity.image.set(1);
168
+ animateSettling([
169
+ [transform.scale, 1],
170
+ [transform.x, 0],
171
+ [transform.y, 0]
172
+ ], markSettled);
173
+ } else {
174
+ opacity.image.set(0);
175
+ if (animated) transform.scale.set(FADE_SCALE);
176
+ run([animate(opacity.image, 1, {
177
+ ...FADE,
178
+ onComplete: markSettled
179
+ })]);
180
+ if (animated) run([animate(transform.scale, 1, FADE)]);
181
+ trackTimeout(markSettled, SETTLE_FALLBACK_MS);
182
+ }
183
+ run([animate(opacity.backdrop, 1, FADE), animate(opacity.chrome, 1, CHROME_FADE)]);
184
+ return () => {
185
+ stopAll();
186
+ removeGhost();
187
+ };
188
+ }, [
189
+ animateSettling,
190
+ animated,
191
+ opacity,
192
+ removeGhost,
193
+ run,
194
+ source,
195
+ stopAll,
196
+ trackTimeout,
197
+ transform
198
+ ]);
199
+ const close = useCallback((options) => {
200
+ if (closingRef.current) return;
201
+ closingRef.current = true;
202
+ transitioningRef.current = true;
203
+ stopAll();
204
+ removeGhost();
205
+ let finished = false;
206
+ const finish = () => {
207
+ if (finished) return;
208
+ finished = true;
209
+ transitioningRef.current = false;
210
+ onClosedRef.current();
211
+ };
212
+ const rect = animated && !options?.fade ? measureSource(getCloseSourceRef.current()) : null;
213
+ run([animate(opacity.backdrop, 0, FADE), animate(opacity.chrome, 0, FADE)]);
214
+ if (rect) {
215
+ const target = sourceTransform(rect, getFitRectRef.current());
216
+ animateSettling([
217
+ [transform.x, target.x],
218
+ [transform.y, target.y],
219
+ [transform.scale, target.scale]
220
+ ], finish);
221
+ return;
222
+ }
223
+ if (animated) run([animate(transform.scale, transform.scale.get() * FADE_SCALE, FADE)]);
224
+ run([animate(opacity.image, 0, {
225
+ ...FADE,
226
+ onComplete: finish
227
+ })]);
228
+ trackTimeout(finish, SETTLE_FALLBACK_MS);
229
+ }, [
230
+ animateSettling,
231
+ animated,
232
+ opacity,
233
+ run,
234
+ stopAll,
235
+ trackTimeout,
236
+ transform
237
+ ]);
238
+ const switchTo = useCallback((apply, direction) => {
239
+ if (closingRef.current) return;
240
+ stopAll();
241
+ removeGhost();
242
+ transitioningRef.current = true;
243
+ const markSettled = () => {
244
+ transitioningRef.current = false;
245
+ };
246
+ const node = image.getNode();
247
+ const width = getViewportWidthRef.current();
248
+ if (animated && direction && node && width > 0) {
249
+ const ghost = node.cloneNode();
250
+ ghost.dataset.ghost = "true";
251
+ ghost.setAttribute("aria-hidden", "true");
252
+ ghost.style.pointerEvents = "none";
253
+ const base = ghost.style.transform;
254
+ node.parentElement?.insertBefore(ghost, node);
255
+ ghostRef.current = ghost;
256
+ const progress = motionValue(0);
257
+ const unsubscribe = progress.on("change", (p) => {
258
+ ghost.style.transform = `translateX(${-direction * width * p}px) ${base}`;
259
+ });
260
+ const finishGhost = () => {
261
+ unsubscribe();
262
+ if (ghostRef.current === ghost) removeGhost();
263
+ else ghost.remove();
264
+ };
265
+ apply();
266
+ transform.x.jump(direction * width);
267
+ run([
268
+ animate(progress, 1, {
269
+ ...PUSH_SPRING,
270
+ onComplete: finishGhost
271
+ }),
272
+ animate(transform.x, 0, {
273
+ ...PUSH_SPRING,
274
+ onComplete: markSettled
275
+ }),
276
+ { stop: finishGhost }
277
+ ]);
278
+ trackTimeout(() => {
279
+ finishGhost();
280
+ markSettled();
281
+ }, SETTLE_FALLBACK_MS);
282
+ return;
283
+ }
284
+ const tasks = [];
285
+ if (animated) tasks.push(animate(transform.scale, SWITCH_FADE_SCALE, FADE));
286
+ tasks.push(animate(opacity.image, 0, {
287
+ ...FADE,
288
+ onComplete: () => {
289
+ apply();
290
+ run([animate(opacity.image, 1, {
291
+ ...FADE,
292
+ onComplete: markSettled
293
+ })]);
294
+ }
295
+ }));
296
+ run(tasks);
297
+ trackTimeout(markSettled, SETTLE_FALLBACK_MS);
298
+ }, [
299
+ animated,
300
+ image,
301
+ opacity,
302
+ removeGhost,
303
+ run,
304
+ stopAll,
305
+ trackTimeout,
306
+ transform
307
+ ]);
308
+ return {
309
+ backdropRef: backdrop.setNode,
310
+ chromeRef: chrome.setNode,
311
+ close,
312
+ imageRef: image.setNode,
313
+ isClosing,
314
+ isTransitioning,
315
+ switchTo
316
+ };
317
+ };
318
+ //#endregion
319
+ export { OPEN_SPRING, useFlipTransition };
320
+
321
+ //# sourceMappingURL=useFlipTransition.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFlipTransition.mjs","names":[],"sources":["../../../src/Image/viewer/useFlipTransition.ts"],"sourcesContent":["import type { MotionValue } from 'motion/react';\nimport { animate, motionValue } from 'motion/react';\nimport { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';\n\nimport type { Rect } from './geometry';\n\nexport const OPEN_SPRING = { bounce: 0.15, type: 'spring' as const, visualDuration: 0.3 };\n// bounce: 0 — a full-viewport push with overshoot would visibly slide past\n// the resting position and snap back.\nconst PUSH_SPRING = { bounce: 0, type: 'spring' as const, visualDuration: 0.3 };\nconst FADE = { duration: 0.15, ease: 'easeOut' as const };\nconst CHROME_FADE = { delay: 0.15, duration: 0.2, ease: 'easeOut' as const };\nconst FADE_SCALE = 0.92;\nconst SWITCH_FADE_SCALE = 0.96;\nconst SETTLE_FALLBACK_MS = 1000;\n\ninterface Animation {\n stop: () => void;\n}\n\nexport interface FlipTransformValues {\n flipX: MotionValue<boolean>;\n flipY: MotionValue<boolean>;\n rotate: MotionValue<number>;\n scale: MotionValue<number>;\n x: MotionValue<number>;\n y: MotionValue<number>;\n}\n\nexport interface UseFlipTransitionOptions {\n animated: boolean;\n getCloseSource: () => HTMLImageElement;\n getFitRect: () => Rect;\n getViewportWidth: () => number;\n onClosed: () => void;\n source: HTMLImageElement;\n transform: FlipTransformValues;\n}\n\nexport interface CloseTransitionOptions {\n fade?: boolean;\n}\n\nexport interface UseFlipTransitionResult {\n backdropRef: (node: HTMLElement | null) => void;\n chromeRef: (node: HTMLElement | null) => void;\n close: (options?: CloseTransitionOptions) => void;\n imageRef: (node: HTMLImageElement | null) => void;\n isClosing: () => boolean;\n isTransitioning: () => boolean;\n switchTo: (apply: () => void, direction?: 1 | -1) => void;\n}\n\nconst measureSource = (element: HTMLImageElement): DOMRect | null => {\n if (!element.isConnected) return null;\n const rect = element.getBoundingClientRect();\n if (rect.width <= 0 || rect.height <= 0) return null;\n if (rect.bottom <= 0 || rect.right <= 0) return null;\n if (rect.top >= window.innerHeight || rect.left >= window.innerWidth) return null;\n return rect;\n};\n\nconst sourceTransform = (rect: DOMRect, fit: Rect) => ({\n scale:\n fit.width > 0 && fit.height > 0\n ? Math.max(rect.width / fit.width, rect.height / fit.height)\n : 1,\n x: rect.left + rect.width / 2 - (fit.x + fit.width / 2),\n y: rect.top + rect.height / 2 - (fit.y + fit.height / 2),\n});\n\nconst readTransform = (transform: FlipTransformValues) =>\n [\n `translate3d(${transform.x.get()}px, ${transform.y.get()}px, 0)`,\n `scale(${transform.scale.get()})`,\n `rotate(${transform.rotate.get()}deg)`,\n `scaleX(${transform.flipX.get() ? -1 : 1})`,\n `scaleY(${transform.flipY.get() ? -1 : 1})`,\n ].join(' ');\n\nconst useBoundNode = <T extends HTMLElement>(apply: (node: T) => void) => {\n const nodeRef = useRef<T | null>(null);\n const applyRef = useRef(apply);\n applyRef.current = apply;\n\n const setNode = useCallback((node: T | null) => {\n nodeRef.current = node;\n if (node) applyRef.current(node);\n }, []);\n\n const refresh = useCallback(() => {\n const node = nodeRef.current;\n if (node) applyRef.current(node);\n }, []);\n\n const getNode = useCallback(() => nodeRef.current, []);\n\n return useMemo(() => ({ getNode, refresh, setNode }), [getNode, refresh, setNode]);\n};\n\nexport const useFlipTransition = ({\n animated,\n getCloseSource,\n getFitRect,\n getViewportWidth,\n onClosed,\n source,\n transform,\n}: UseFlipTransitionOptions): UseFlipTransitionResult => {\n const [opacity] = useState(() => ({\n backdrop: motionValue(0),\n chrome: motionValue(0),\n image: motionValue(animated ? 1 : 0),\n }));\n\n const image = useBoundNode<HTMLImageElement>(\n useCallback(\n (node) => {\n node.style.transform = readTransform(transform);\n node.style.opacity = String(opacity.image.get());\n },\n [opacity, transform],\n ),\n );\n\n const backdrop = useBoundNode<HTMLElement>(\n useCallback(\n (node) => {\n node.style.opacity = String(opacity.backdrop.get());\n },\n [opacity],\n ),\n );\n\n const chrome = useBoundNode<HTMLElement>(\n useCallback(\n (node) => {\n node.style.opacity = String(opacity.chrome.get());\n },\n [opacity],\n ),\n );\n\n const runningRef = useRef<Animation[]>([]);\n const closingRef = useRef(false);\n const transitioningRef = useRef(true);\n\n const onClosedRef = useRef(onClosed);\n onClosedRef.current = onClosed;\n\n const getFitRectRef = useRef(getFitRect);\n getFitRectRef.current = getFitRect;\n\n const getCloseSourceRef = useRef(getCloseSource);\n getCloseSourceRef.current = getCloseSource;\n\n const getViewportWidthRef = useRef(getViewportWidth);\n getViewportWidthRef.current = getViewportWidth;\n\n const ghostRef = useRef<HTMLImageElement | null>(null);\n const removeGhost = useCallback(() => {\n ghostRef.current?.remove();\n ghostRef.current = null;\n }, []);\n\n const run = useCallback((animations: Animation[]) => {\n runningRef.current = [...runningRef.current, ...animations];\n }, []);\n\n const stopAll = useCallback(() => {\n for (const animation of runningRef.current) animation.stop();\n runningRef.current = [];\n }, []);\n\n const isTransitioning = useCallback(() => transitioningRef.current, []);\n const isClosing = useCallback(() => closingRef.current, []);\n\n // stopAll() only stops tracked Animation objects; a bare window.setTimeout\n // has no way to be cancelled by it. Track a fallback timer the same way, so\n // a later stopAll() (a new open/close/switch superseding an interrupted one)\n // clears the old timer along with the animations it was backing — otherwise\n // it can fire mid-way through the new phase and flip transitioningRef false\n // while something else is legitimately still in flight.\n const trackTimeout = useCallback(\n (callback: () => void, ms: number) => {\n const id = window.setTimeout(callback, ms);\n run([{ stop: () => window.clearTimeout(id) }]);\n },\n [run],\n );\n\n // A future animate() call on one of these shared motion values (e.g. a refit\n // triggered by a natural-size change) silently cancels whichever animation is\n // already running on it — motion's animate() stops the prior one without ever\n // firing its onComplete. Track completion by count for the common case, but\n // always back it with a fallback timer so onSettled fires exactly once no\n // matter what: a cancelled axis can never leave this permanently unfired.\n const animateSettling = useCallback(\n (axes: [MotionValue<number>, number][], onSettled: () => void) => {\n let pending = axes.length;\n let settled = false;\n const finish = () => {\n if (settled) return;\n settled = true;\n onSettled();\n };\n const onAxisComplete = () => {\n pending -= 1;\n if (pending === 0) finish();\n };\n run(\n axes.map(([value, target]) =>\n animate(value, target, { ...OPEN_SPRING, onComplete: onAxisComplete }),\n ),\n );\n trackTimeout(finish, SETTLE_FALLBACK_MS);\n },\n [run, trackTimeout],\n );\n\n useLayoutEffect(() => {\n const unsubscribes = [\n ...[\n transform.flipX,\n transform.flipY,\n transform.rotate,\n transform.scale,\n transform.x,\n transform.y,\n opacity.image,\n ].map((value) => value.on('change', image.refresh)),\n opacity.backdrop.on('change', backdrop.refresh),\n opacity.chrome.on('change', chrome.refresh),\n ];\n\n return () => {\n for (const unsubscribe of unsubscribes) unsubscribe();\n };\n }, [backdrop, chrome, image, opacity, transform]);\n\n useLayoutEffect(() => {\n const rect = animated ? measureSource(source) : null;\n transitioningRef.current = true;\n const markSettled = () => {\n transitioningRef.current = false;\n };\n\n if (rect) {\n const start = sourceTransform(rect, getFitRectRef.current());\n transform.scale.set(start.scale);\n transform.x.set(start.x);\n transform.y.set(start.y);\n opacity.image.set(1);\n animateSettling(\n [\n [transform.scale, 1],\n [transform.x, 0],\n [transform.y, 0],\n ],\n markSettled,\n );\n } else {\n opacity.image.set(0);\n if (animated) transform.scale.set(FADE_SCALE);\n run([animate(opacity.image, 1, { ...FADE, onComplete: markSettled })]);\n if (animated) run([animate(transform.scale, 1, FADE)]);\n trackTimeout(markSettled, SETTLE_FALLBACK_MS);\n }\n\n run([animate(opacity.backdrop, 1, FADE), animate(opacity.chrome, 1, CHROME_FADE)]);\n\n return () => {\n stopAll();\n removeGhost();\n };\n }, [\n animateSettling,\n animated,\n opacity,\n removeGhost,\n run,\n source,\n stopAll,\n trackTimeout,\n transform,\n ]);\n\n const close = useCallback(\n (options?: CloseTransitionOptions) => {\n if (closingRef.current) return;\n closingRef.current = true;\n transitioningRef.current = true;\n stopAll();\n removeGhost();\n\n let finished = false;\n const finish = () => {\n if (finished) return;\n finished = true;\n transitioningRef.current = false;\n onClosedRef.current();\n };\n const rect = animated && !options?.fade ? measureSource(getCloseSourceRef.current()) : null;\n\n run([animate(opacity.backdrop, 0, FADE), animate(opacity.chrome, 0, FADE)]);\n\n if (rect) {\n const target = sourceTransform(rect, getFitRectRef.current());\n animateSettling(\n [\n [transform.x, target.x],\n [transform.y, target.y],\n [transform.scale, target.scale],\n ],\n finish,\n );\n return;\n }\n\n if (animated) run([animate(transform.scale, transform.scale.get() * FADE_SCALE, FADE)]);\n run([animate(opacity.image, 0, { ...FADE, onComplete: finish })]);\n trackTimeout(finish, SETTLE_FALLBACK_MS);\n },\n [animateSettling, animated, opacity, run, stopAll, trackTimeout, transform],\n );\n\n const switchTo = useCallback(\n (apply: () => void, direction?: 1 | -1) => {\n if (closingRef.current) return;\n stopAll();\n removeGhost();\n transitioningRef.current = true;\n\n const markSettled = () => {\n transitioningRef.current = false;\n };\n\n const node = image.getNode();\n const width = getViewportWidthRef.current();\n\n if (animated && direction && node && width > 0) {\n // The clone snapshots the outgoing visual state wholesale — src,\n // fitRect positioning and the current zoom/rotate/flip transform —\n // so the live <img> stays the single element every other system\n // (gestures, FLIP close, refit) keeps operating on.\n const ghost = node.cloneNode() as HTMLImageElement;\n ghost.dataset.ghost = 'true';\n ghost.setAttribute('aria-hidden', 'true');\n ghost.style.pointerEvents = 'none';\n const base = ghost.style.transform;\n node.parentElement?.insertBefore(ghost, node);\n ghostRef.current = ghost;\n\n const progress = motionValue(0);\n const unsubscribe = progress.on('change', (p) => {\n ghost.style.transform = `translateX(${-direction * width * p}px) ${base}`;\n });\n const finishGhost = () => {\n unsubscribe();\n if (ghostRef.current === ghost) removeGhost();\n else ghost.remove();\n };\n\n apply();\n transform.x.jump(direction * width);\n\n run([\n animate(progress, 1, { ...PUSH_SPRING, onComplete: finishGhost }),\n animate(transform.x, 0, { ...PUSH_SPRING, onComplete: markSettled }),\n { stop: finishGhost },\n ]);\n trackTimeout(() => {\n finishGhost();\n markSettled();\n }, SETTLE_FALLBACK_MS);\n return;\n }\n\n const tasks: Animation[] = [];\n if (animated) tasks.push(animate(transform.scale, SWITCH_FADE_SCALE, FADE));\n tasks.push(\n animate(opacity.image, 0, {\n ...FADE,\n onComplete: () => {\n apply();\n run([animate(opacity.image, 1, { ...FADE, onComplete: markSettled })]);\n },\n }),\n );\n run(tasks);\n trackTimeout(markSettled, SETTLE_FALLBACK_MS);\n },\n [animated, image, opacity, removeGhost, run, stopAll, trackTimeout, transform],\n );\n\n return {\n backdropRef: backdrop.setNode,\n chromeRef: chrome.setNode,\n close,\n imageRef: image.setNode,\n isClosing,\n isTransitioning,\n switchTo,\n };\n};\n"],"mappings":"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"}
@@ -0,0 +1,89 @@
1
+ import { isTypingTarget } from "./useViewerGestures.mjs";
2
+ import { useCallback, useEffect, useRef } from "react";
3
+ //#region src/Image/viewer/useGalleryNav.ts
4
+ const readNatural = (element) => {
5
+ if (element.naturalWidth > 0 && element.naturalHeight > 0) return {
6
+ height: element.naturalHeight,
7
+ width: element.naturalWidth
8
+ };
9
+ const rect = element.getBoundingClientRect();
10
+ return {
11
+ height: Math.max(rect.height, 1),
12
+ width: Math.max(rect.width, 1)
13
+ };
14
+ };
15
+ const useGalleryNav = ({ cancelPendingClose, currentIndex, entries, flipX, flipY, resetCloseTracking, rotate, scale, setCurrentIndex, setNatural, setSource, switchTo, syncZoomNatural, x, y }) => {
16
+ const indexRef = useRef(currentIndex);
17
+ indexRef.current = currentIndex;
18
+ const switchingRef = useRef(false);
19
+ const switchToRef = useRef(switchTo);
20
+ switchToRef.current = switchTo;
21
+ const cancelPendingCloseRef = useRef(cancelPendingClose);
22
+ cancelPendingCloseRef.current = cancelPendingClose;
23
+ const resetCloseTrackingRef = useRef(resetCloseTracking);
24
+ resetCloseTrackingRef.current = resetCloseTracking;
25
+ const goTo = useCallback((nextIndex) => {
26
+ if (switchingRef.current) return;
27
+ if (nextIndex < 0 || nextIndex >= entries.length || nextIndex === indexRef.current) return;
28
+ switchingRef.current = true;
29
+ cancelPendingCloseRef.current();
30
+ resetCloseTrackingRef.current();
31
+ switchToRef.current(() => {
32
+ const nextEntry = entries[nextIndex];
33
+ setCurrentIndex(nextIndex);
34
+ setSource(nextEntry.src);
35
+ const nextNatural = readNatural(nextEntry.element);
36
+ setNatural(nextNatural);
37
+ syncZoomNatural(nextNatural);
38
+ scale.jump(1);
39
+ x.jump(0);
40
+ y.jump(0);
41
+ rotate.jump(0);
42
+ flipX.jump(false);
43
+ flipY.jump(false);
44
+ switchingRef.current = false;
45
+ }, nextIndex > indexRef.current ? 1 : -1);
46
+ }, [
47
+ entries,
48
+ flipX,
49
+ flipY,
50
+ rotate,
51
+ scale,
52
+ setCurrentIndex,
53
+ setNatural,
54
+ setSource,
55
+ syncZoomNatural,
56
+ x,
57
+ y
58
+ ]);
59
+ const prev = useCallback(() => goTo(indexRef.current - 1), [goTo]);
60
+ const next = useCallback(() => goTo(indexRef.current + 1), [goTo]);
61
+ const isGallery = entries.length > 1;
62
+ useEffect(() => {
63
+ if (!isGallery) return;
64
+ const listener = (event) => {
65
+ if (event.altKey || event.ctrlKey || event.metaKey || event.defaultPrevented) return;
66
+ if (isTypingTarget(event.target)) return;
67
+ if (event.key === "ArrowLeft") prev();
68
+ else if (event.key === "ArrowRight") next();
69
+ else return;
70
+ event.preventDefault();
71
+ };
72
+ document.addEventListener("keydown", listener);
73
+ return () => document.removeEventListener("keydown", listener);
74
+ }, [
75
+ isGallery,
76
+ next,
77
+ prev
78
+ ]);
79
+ return {
80
+ hasNext: currentIndex < entries.length - 1,
81
+ hasPrev: currentIndex > 0,
82
+ next,
83
+ prev
84
+ };
85
+ };
86
+ //#endregion
87
+ export { readNatural, useGalleryNav };
88
+
89
+ //# sourceMappingURL=useGalleryNav.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useGalleryNav.mjs","names":[],"sources":["../../../src/Image/viewer/useGalleryNav.ts"],"sourcesContent":["import type { MotionValue } from 'motion/react';\nimport { useCallback, useEffect, useRef } from 'react';\n\nimport type { Size } from './geometry';\nimport type { PreviewEntry } from './registry';\nimport { isTypingTarget } from './useViewerGestures';\n\nexport const readNatural = (element: HTMLImageElement): Size => {\n if (element.naturalWidth > 0 && element.naturalHeight > 0)\n return { height: element.naturalHeight, width: element.naturalWidth };\n const rect = element.getBoundingClientRect();\n return { height: Math.max(rect.height, 1), width: Math.max(rect.width, 1) };\n};\n\nexport interface UseGalleryNavOptions {\n cancelPendingClose: () => void;\n currentIndex: number;\n entries: PreviewEntry[];\n flipX: MotionValue<boolean>;\n flipY: MotionValue<boolean>;\n resetCloseTracking: () => void;\n rotate: MotionValue<number>;\n scale: MotionValue<number>;\n setCurrentIndex: (index: number) => void;\n setNatural: (natural: Size) => void;\n setSource: (src: string) => void;\n switchTo: (apply: () => void, direction?: 1 | -1) => void;\n syncZoomNatural: (natural: Size) => void;\n x: MotionValue<number>;\n y: MotionValue<number>;\n}\n\nexport interface UseGalleryNavResult {\n hasNext: boolean;\n hasPrev: boolean;\n next: () => void;\n prev: () => void;\n}\n\nexport const useGalleryNav = ({\n cancelPendingClose,\n currentIndex,\n entries,\n flipX,\n flipY,\n resetCloseTracking,\n rotate,\n scale,\n setCurrentIndex,\n setNatural,\n setSource,\n switchTo,\n syncZoomNatural,\n x,\n y,\n}: UseGalleryNavOptions): UseGalleryNavResult => {\n const indexRef = useRef(currentIndex);\n indexRef.current = currentIndex;\n\n const switchingRef = useRef(false);\n\n const switchToRef = useRef(switchTo);\n switchToRef.current = switchTo;\n\n const cancelPendingCloseRef = useRef(cancelPendingClose);\n cancelPendingCloseRef.current = cancelPendingClose;\n\n const resetCloseTrackingRef = useRef(resetCloseTracking);\n resetCloseTrackingRef.current = resetCloseTracking;\n\n const goTo = useCallback(\n (nextIndex: number) => {\n if (switchingRef.current) return;\n if (nextIndex < 0 || nextIndex >= entries.length || nextIndex === indexRef.current) return;\n switchingRef.current = true;\n // A switch changes the displayed image out from under any pending\n // click-to-close or wheel-close accumulation armed for the old image,\n // so both must clear at this single choke point every switch goes through.\n cancelPendingCloseRef.current();\n resetCloseTrackingRef.current();\n switchToRef.current(\n () => {\n const nextEntry = entries[nextIndex];\n setCurrentIndex(nextIndex);\n setSource(nextEntry.src);\n const nextNatural = readNatural(nextEntry.element);\n setNatural(nextNatural);\n syncZoomNatural(nextNatural);\n // .jump(), not .set(): a surviving spring from the outgoing image (a\n // dragEnd clamp-back, a wheel snap-back, or reset()) targets the OLD\n // image's bounds and would otherwise silently overwrite this reset on\n // its next tick, since motion's set() never stops an active animation.\n scale.jump(1);\n x.jump(0);\n y.jump(0);\n rotate.jump(0);\n flipX.jump(false);\n flipY.jump(false);\n switchingRef.current = false;\n },\n nextIndex > indexRef.current ? 1 : -1,\n );\n },\n [\n entries,\n flipX,\n flipY,\n rotate,\n scale,\n setCurrentIndex,\n setNatural,\n setSource,\n syncZoomNatural,\n x,\n y,\n ],\n );\n\n const prev = useCallback(() => goTo(indexRef.current - 1), [goTo]);\n const next = useCallback(() => goTo(indexRef.current + 1), [goTo]);\n\n const isGallery = entries.length > 1;\n\n useEffect(() => {\n if (!isGallery) return;\n const listener = (event: KeyboardEvent) => {\n if (event.altKey || event.ctrlKey || event.metaKey || event.defaultPrevented) return;\n if (isTypingTarget(event.target)) return;\n if (event.key === 'ArrowLeft') prev();\n else if (event.key === 'ArrowRight') next();\n else return;\n event.preventDefault();\n };\n document.addEventListener('keydown', listener);\n return () => document.removeEventListener('keydown', listener);\n }, [isGallery, next, prev]);\n\n return {\n hasNext: currentIndex < entries.length - 1,\n hasPrev: currentIndex > 0,\n next,\n prev,\n };\n};\n"],"mappings":";;;AAOA,MAAa,eAAe,YAAoC;CAC9D,IAAI,QAAQ,eAAe,KAAK,QAAQ,gBAAgB,GACtD,OAAO;EAAE,QAAQ,QAAQ;EAAe,OAAO,QAAQ;CAAa;CACtE,MAAM,OAAO,QAAQ,sBAAsB;CAC3C,OAAO;EAAE,QAAQ,KAAK,IAAI,KAAK,QAAQ,CAAC;EAAG,OAAO,KAAK,IAAI,KAAK,OAAO,CAAC;CAAE;AAC5E;AA2BA,MAAa,iBAAiB,EAC5B,oBACA,cACA,SACA,OACA,OACA,oBACA,QACA,OACA,iBACA,YACA,WACA,UACA,iBACA,GACA,QAC+C;CAC/C,MAAM,WAAW,OAAO,YAAY;CACpC,SAAS,UAAU;CAEnB,MAAM,eAAe,OAAO,KAAK;CAEjC,MAAM,cAAc,OAAO,QAAQ;CACnC,YAAY,UAAU;CAEtB,MAAM,wBAAwB,OAAO,kBAAkB;CACvD,sBAAsB,UAAU;CAEhC,MAAM,wBAAwB,OAAO,kBAAkB;CACvD,sBAAsB,UAAU;CAEhC,MAAM,OAAO,aACV,cAAsB;EACrB,IAAI,aAAa,SAAS;EAC1B,IAAI,YAAY,KAAK,aAAa,QAAQ,UAAU,cAAc,SAAS,SAAS;EACpF,aAAa,UAAU;EAIvB,sBAAsB,QAAQ;EAC9B,sBAAsB,QAAQ;EAC9B,YAAY,cACJ;GACJ,MAAM,YAAY,QAAQ;GAC1B,gBAAgB,SAAS;GACzB,UAAU,UAAU,GAAG;GACvB,MAAM,cAAc,YAAY,UAAU,OAAO;GACjD,WAAW,WAAW;GACtB,gBAAgB,WAAW;GAK3B,MAAM,KAAK,CAAC;GACZ,EAAE,KAAK,CAAC;GACR,EAAE,KAAK,CAAC;GACR,OAAO,KAAK,CAAC;GACb,MAAM,KAAK,KAAK;GAChB,MAAM,KAAK,KAAK;GAChB,aAAa,UAAU;EACzB,GACA,YAAY,SAAS,UAAU,IAAI,EACrC;CACF,GACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,CACF;CAEA,MAAM,OAAO,kBAAkB,KAAK,SAAS,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC;CACjE,MAAM,OAAO,kBAAkB,KAAK,SAAS,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC;CAEjE,MAAM,YAAY,QAAQ,SAAS;CAEnC,gBAAgB;EACd,IAAI,CAAC,WAAW;EAChB,MAAM,YAAY,UAAyB;GACzC,IAAI,MAAM,UAAU,MAAM,WAAW,MAAM,WAAW,MAAM,kBAAkB;GAC9E,IAAI,eAAe,MAAM,MAAM,GAAG;GAClC,IAAI,MAAM,QAAQ,aAAa,KAAK;QAC/B,IAAI,MAAM,QAAQ,cAAc,KAAK;QACrC;GACL,MAAM,eAAe;EACvB;EACA,SAAS,iBAAiB,WAAW,QAAQ;EAC7C,aAAa,SAAS,oBAAoB,WAAW,QAAQ;CAC/D,GAAG;EAAC;EAAW;EAAM;CAAI,CAAC;CAE1B,OAAO;EACL,SAAS,eAAe,QAAQ,SAAS;EACzC,SAAS,eAAe;EACxB;EACA;CACF;AACF"}
@@ -0,0 +1,47 @@
1
+ import { computeFit } from "./geometry.mjs";
2
+ import { OPEN_SPRING } from "./useFlipTransition.mjs";
3
+ import { useLayoutEffect, useRef } from "react";
4
+ import { animate } from "motion/react";
5
+ //#region src/Image/viewer/useRefitTransition.ts
6
+ const useRefitTransition = ({ animated, fillViewport, isTransitioning, natural, rotation, scale, viewport, x, y }) => {
7
+ const naturalRef = useRef(natural);
8
+ useLayoutEffect(() => {
9
+ const previousNatural = naturalRef.current;
10
+ naturalRef.current = natural;
11
+ if (previousNatural.width === natural.width && previousNatural.height === natural.height) return;
12
+ if (!animated) return;
13
+ if (isTransitioning()) return;
14
+ const previousFit = computeFit(previousNatural, viewport, rotation, fillViewport);
15
+ const nextFit = computeFit(natural, viewport, rotation, fillViewport);
16
+ if (previousFit.width === nextFit.width && previousFit.height === nextFit.height && previousFit.x === nextFit.x && previousFit.y === nextFit.y) return;
17
+ const ratio = nextFit.width > 0 ? previousFit.width / nextFit.width : 1;
18
+ if (!Number.isFinite(ratio) || ratio <= 0) return;
19
+ const previousCenterX = previousFit.x + previousFit.width / 2;
20
+ const previousCenterY = previousFit.y + previousFit.height / 2;
21
+ const nextCenterX = nextFit.x + nextFit.width / 2;
22
+ const nextCenterY = nextFit.y + nextFit.height / 2;
23
+ const targetScale = scale.get();
24
+ const targetX = x.get();
25
+ const targetY = y.get();
26
+ scale.set(targetScale * ratio);
27
+ x.set(targetX + (previousCenterX - nextCenterX));
28
+ y.set(targetY + (previousCenterY - nextCenterY));
29
+ animate(scale, targetScale, OPEN_SPRING);
30
+ animate(x, targetX, OPEN_SPRING);
31
+ animate(y, targetY, OPEN_SPRING);
32
+ }, [
33
+ animated,
34
+ fillViewport,
35
+ isTransitioning,
36
+ natural,
37
+ rotation,
38
+ scale,
39
+ viewport,
40
+ x,
41
+ y
42
+ ]);
43
+ };
44
+ //#endregion
45
+ export { useRefitTransition };
46
+
47
+ //# sourceMappingURL=useRefitTransition.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useRefitTransition.mjs","names":[],"sources":["../../../src/Image/viewer/useRefitTransition.ts"],"sourcesContent":["import type { MotionValue } from 'motion/react';\nimport { animate } from 'motion/react';\nimport { useLayoutEffect, useRef } from 'react';\n\nimport { computeFit, type Rotation, type Size } from './geometry';\nimport { OPEN_SPRING } from './useFlipTransition';\n\nexport interface UseRefitTransitionOptions {\n animated: boolean;\n fillViewport?: boolean;\n isTransitioning: () => boolean;\n natural: Size;\n rotation: Rotation;\n scale: MotionValue<number>;\n viewport: Size;\n x: MotionValue<number>;\n y: MotionValue<number>;\n}\n\nexport const useRefitTransition = ({\n animated,\n fillViewport,\n isTransitioning,\n natural,\n rotation,\n scale,\n viewport,\n x,\n y,\n}: UseRefitTransitionOptions): void => {\n const naturalRef = useRef(natural);\n\n useLayoutEffect(() => {\n const previousNatural = naturalRef.current;\n naturalRef.current = natural;\n if (previousNatural.width === natural.width && previousNatural.height === natural.height)\n return;\n if (!animated) return;\n // A FLIP open/close spring or a gallery switch is already animating these\n // same scale/x/y values. Calling animate() on them here would silently\n // cancel that in-flight animation (motion's animate() stops the prior one\n // without firing its onComplete), which can park the viewer mid-transform\n // on open or, worse, permanently wedge an invisible dialog open on close.\n // Skip outright: the open spring always targets identity relative to the\n // *current* fit rect, so it self-corrects once it settles; a switch resets\n // the transform itself, and instant re-fit is correct there.\n if (isTransitioning()) return;\n\n const previousFit = computeFit(previousNatural, viewport, rotation, fillViewport);\n const nextFit = computeFit(natural, viewport, rotation, fillViewport);\n if (\n previousFit.width === nextFit.width &&\n previousFit.height === nextFit.height &&\n previousFit.x === nextFit.x &&\n previousFit.y === nextFit.y\n )\n return;\n\n const ratio = nextFit.width > 0 ? previousFit.width / nextFit.width : 1;\n if (!Number.isFinite(ratio) || ratio <= 0) return;\n\n const previousCenterX = previousFit.x + previousFit.width / 2;\n const previousCenterY = previousFit.y + previousFit.height / 2;\n const nextCenterX = nextFit.x + nextFit.width / 2;\n const nextCenterY = nextFit.y + nextFit.height / 2;\n\n const targetScale = scale.get();\n const targetX = x.get();\n const targetY = y.get();\n\n scale.set(targetScale * ratio);\n x.set(targetX + (previousCenterX - nextCenterX));\n y.set(targetY + (previousCenterY - nextCenterY));\n\n animate(scale, targetScale, OPEN_SPRING);\n animate(x, targetX, OPEN_SPRING);\n animate(y, targetY, OPEN_SPRING);\n }, [animated, fillViewport, isTransitioning, natural, rotation, scale, viewport, x, y]);\n};\n"],"mappings":";;;;;AAmBA,MAAa,sBAAsB,EACjC,UACA,cACA,iBACA,SACA,UACA,OACA,UACA,GACA,QACqC;CACrC,MAAM,aAAa,OAAO,OAAO;CAEjC,sBAAsB;EACpB,MAAM,kBAAkB,WAAW;EACnC,WAAW,UAAU;EACrB,IAAI,gBAAgB,UAAU,QAAQ,SAAS,gBAAgB,WAAW,QAAQ,QAChF;EACF,IAAI,CAAC,UAAU;EASf,IAAI,gBAAgB,GAAG;EAEvB,MAAM,cAAc,WAAW,iBAAiB,UAAU,UAAU,YAAY;EAChF,MAAM,UAAU,WAAW,SAAS,UAAU,UAAU,YAAY;EACpE,IACE,YAAY,UAAU,QAAQ,SAC9B,YAAY,WAAW,QAAQ,UAC/B,YAAY,MAAM,QAAQ,KAC1B,YAAY,MAAM,QAAQ,GAE1B;EAEF,MAAM,QAAQ,QAAQ,QAAQ,IAAI,YAAY,QAAQ,QAAQ,QAAQ;EACtE,IAAI,CAAC,OAAO,SAAS,KAAK,KAAK,SAAS,GAAG;EAE3C,MAAM,kBAAkB,YAAY,IAAI,YAAY,QAAQ;EAC5D,MAAM,kBAAkB,YAAY,IAAI,YAAY,SAAS;EAC7D,MAAM,cAAc,QAAQ,IAAI,QAAQ,QAAQ;EAChD,MAAM,cAAc,QAAQ,IAAI,QAAQ,SAAS;EAEjD,MAAM,cAAc,MAAM,IAAI;EAC9B,MAAM,UAAU,EAAE,IAAI;EACtB,MAAM,UAAU,EAAE,IAAI;EAEtB,MAAM,IAAI,cAAc,KAAK;EAC7B,EAAE,IAAI,WAAW,kBAAkB,YAAY;EAC/C,EAAE,IAAI,WAAW,kBAAkB,YAAY;EAE/C,QAAQ,OAAO,aAAa,WAAW;EACvC,QAAQ,GAAG,SAAS,WAAW;EAC/B,QAAQ,GAAG,SAAS,WAAW;CACjC,GAAG;EAAC;EAAU;EAAc;EAAiB;EAAS;EAAU;EAAO;EAAU;EAAG;CAAC,CAAC;AACxF"}
@@ -0,0 +1,148 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ //#region src/Image/viewer/useViewerGestures.ts
3
+ const DRAG_THRESHOLD = 4;
4
+ const DOUBLE_CLICK_WINDOW = 250;
5
+ const capturePointer = (node, pointerId) => {
6
+ if (typeof node.setPointerCapture === "function") node.setPointerCapture(pointerId);
7
+ };
8
+ const isTypingTarget = (target) => target instanceof HTMLElement && (target.isContentEditable || [
9
+ "INPUT",
10
+ "SELECT",
11
+ "TEXTAREA"
12
+ ].includes(target.tagName));
13
+ const useViewerGestures = ({ dragBy, dragEnd, handleDoubleClick, handleWheel, isClean, isZoomed, onClose, reset, zoomIn, zoomOut }) => {
14
+ const [popup, setPopup] = useState(null);
15
+ const [panning, setPanning] = useState(false);
16
+ const imageNodeRef = useRef(null);
17
+ const dragRef = useRef(null);
18
+ const movedRef = useRef(false);
19
+ const pendingCloseRef = useRef(null);
20
+ const isCleanRef = useRef(isClean);
21
+ isCleanRef.current = isClean;
22
+ const cancelPendingClose = useCallback(() => {
23
+ if (pendingCloseRef.current === null) return;
24
+ clearTimeout(pendingCloseRef.current);
25
+ pendingCloseRef.current = null;
26
+ }, []);
27
+ useEffect(() => cancelPendingClose, [cancelPendingClose]);
28
+ const imageRef = useCallback((node) => {
29
+ imageNodeRef.current = node;
30
+ }, []);
31
+ useEffect(() => {
32
+ if (!popup) return;
33
+ const listener = (event) => handleWheel(event);
34
+ popup.addEventListener("wheel", listener, { passive: false });
35
+ return () => popup.removeEventListener("wheel", listener);
36
+ }, [handleWheel, popup]);
37
+ useEffect(() => {
38
+ const listener = (event) => {
39
+ if (event.altKey || event.ctrlKey || event.metaKey || event.defaultPrevented) return;
40
+ if (isTypingTarget(event.target)) return;
41
+ switch (event.key) {
42
+ case "+":
43
+ case "=":
44
+ zoomIn();
45
+ break;
46
+ case "-":
47
+ zoomOut();
48
+ break;
49
+ case "0":
50
+ reset();
51
+ break;
52
+ default: return;
53
+ }
54
+ event.preventDefault();
55
+ };
56
+ document.addEventListener("keydown", listener);
57
+ return () => document.removeEventListener("keydown", listener);
58
+ }, [
59
+ reset,
60
+ zoomIn,
61
+ zoomOut
62
+ ]);
63
+ const onPointerDown = useCallback((event) => {
64
+ if (dragRef.current?.panning) return;
65
+ if (event.button !== 0) return;
66
+ cancelPendingClose();
67
+ const start = {
68
+ x: event.clientX,
69
+ y: event.clientY
70
+ };
71
+ const panEligible = isZoomed && event.target === imageNodeRef.current;
72
+ movedRef.current = false;
73
+ dragRef.current = {
74
+ id: event.pointerId,
75
+ last: start,
76
+ panning: panEligible,
77
+ start
78
+ };
79
+ if (!panEligible) return;
80
+ capturePointer(event.target, event.pointerId);
81
+ setPanning(true);
82
+ }, [cancelPendingClose, isZoomed]);
83
+ const onPointerMove = useCallback((event) => {
84
+ const drag = dragRef.current;
85
+ if (!drag || drag.id !== event.pointerId) return;
86
+ const point = {
87
+ x: event.clientX,
88
+ y: event.clientY
89
+ };
90
+ if (Math.hypot(point.x - drag.start.x, point.y - drag.start.y) > DRAG_THRESHOLD) movedRef.current = true;
91
+ if (drag.panning) dragBy({
92
+ x: point.x - drag.last.x,
93
+ y: point.y - drag.last.y
94
+ });
95
+ drag.last = point;
96
+ }, [dragBy]);
97
+ const onPointerFinish = useCallback((event) => {
98
+ const drag = dragRef.current;
99
+ if (!drag || drag.id !== event.pointerId) return;
100
+ dragRef.current = null;
101
+ if (!drag.panning) return;
102
+ setPanning(false);
103
+ dragEnd();
104
+ }, [dragEnd]);
105
+ const onSurfaceClick = useCallback(() => {
106
+ if (movedRef.current) return;
107
+ cancelPendingClose();
108
+ onClose();
109
+ }, [cancelPendingClose, onClose]);
110
+ const onImageClick = useCallback((event) => {
111
+ event.stopPropagation();
112
+ cancelPendingClose();
113
+ if (movedRef.current || isZoomed) return;
114
+ pendingCloseRef.current = setTimeout(() => {
115
+ pendingCloseRef.current = null;
116
+ if (!isCleanRef.current) return;
117
+ onClose();
118
+ }, DOUBLE_CLICK_WINDOW);
119
+ }, [
120
+ cancelPendingClose,
121
+ isZoomed,
122
+ onClose
123
+ ]);
124
+ const onImageDoubleClick = useCallback((event) => {
125
+ event.stopPropagation();
126
+ cancelPendingClose();
127
+ handleDoubleClick({
128
+ x: event.clientX,
129
+ y: event.clientY
130
+ });
131
+ }, [cancelPendingClose, handleDoubleClick]);
132
+ return {
133
+ cancelPendingClose,
134
+ cursor: isZoomed ? panning ? "grabbing" : "grab" : "zoom-out",
135
+ imageRef,
136
+ onImageClick,
137
+ onImageDoubleClick,
138
+ onPointerDown,
139
+ onPointerFinish,
140
+ onPointerMove,
141
+ onSurfaceClick,
142
+ popupRef: setPopup
143
+ };
144
+ };
145
+ //#endregion
146
+ export { isTypingTarget, useViewerGestures };
147
+
148
+ //# sourceMappingURL=useViewerGestures.mjs.map