@lobehub/ui 5.25.0 → 5.27.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 (63) hide show
  1. package/es/Image/Image.mjs +138 -37
  2. package/es/Image/Image.mjs.map +1 -1
  3. package/es/Image/PreviewGroup.d.mts +1 -0
  4. package/es/Image/PreviewGroup.mjs +33 -10
  5. package/es/Image/PreviewGroup.mjs.map +1 -1
  6. package/es/Image/index.d.mts +2 -2
  7. package/es/Image/index.mjs +1 -1
  8. package/es/Image/style.mjs +126 -71
  9. package/es/Image/style.mjs.map +1 -1
  10. package/es/Image/type.d.mts +14 -17
  11. package/es/Image/viewer/GalleryNav.mjs +44 -0
  12. package/es/Image/viewer/GalleryNav.mjs.map +1 -0
  13. package/es/Image/viewer/ImageViewer.mjs +275 -0
  14. package/es/Image/viewer/ImageViewer.mjs.map +1 -0
  15. package/es/Image/viewer/PreviewOutlet.mjs +19 -0
  16. package/es/Image/viewer/PreviewOutlet.mjs.map +1 -0
  17. package/es/Image/viewer/Toolbar.mjs +184 -0
  18. package/es/Image/viewer/Toolbar.mjs.map +1 -0
  19. package/es/Image/viewer/ViewerChrome.mjs +54 -0
  20. package/es/Image/viewer/ViewerChrome.mjs.map +1 -0
  21. package/es/Image/viewer/geometry.mjs +85 -0
  22. package/es/Image/viewer/geometry.mjs.map +1 -0
  23. package/es/Image/viewer/registry.d.mts +2 -0
  24. package/es/Image/viewer/registry.mjs +52 -0
  25. package/es/Image/viewer/registry.mjs.map +1 -0
  26. package/es/Image/viewer/useChromeIdle.mjs +72 -0
  27. package/es/Image/viewer/useChromeIdle.mjs.map +1 -0
  28. package/es/Image/viewer/useFinalFocus.mjs +10 -0
  29. package/es/Image/viewer/useFinalFocus.mjs.map +1 -0
  30. package/es/Image/viewer/useFlipTransition.mjs +321 -0
  31. package/es/Image/viewer/useFlipTransition.mjs.map +1 -0
  32. package/es/Image/viewer/useGalleryNav.mjs +89 -0
  33. package/es/Image/viewer/useGalleryNav.mjs.map +1 -0
  34. package/es/Image/viewer/useRefitTransition.mjs +47 -0
  35. package/es/Image/viewer/useRefitTransition.mjs.map +1 -0
  36. package/es/Image/viewer/useViewerGestures.mjs +148 -0
  37. package/es/Image/viewer/useViewerGestures.mjs.map +1 -0
  38. package/es/Image/viewer/useZoomPan.mjs +346 -0
  39. package/es/Image/viewer/useZoomPan.mjs.map +1 -0
  40. package/es/base-ui/Toast/hostGuard.mjs +26 -0
  41. package/es/base-ui/Toast/hostGuard.mjs.map +1 -0
  42. package/es/base-ui/Toast/imperative.mjs +69 -31
  43. package/es/base-ui/Toast/imperative.mjs.map +1 -1
  44. package/es/base-ui/Toast/pendingQueue.mjs +30 -0
  45. package/es/base-ui/Toast/pendingQueue.mjs.map +1 -0
  46. package/es/base-ui/Toast/type.d.mts +7 -0
  47. package/es/base-ui/index.mjs +1 -1
  48. package/es/i18n/resources/en/image.d.mts +5 -0
  49. package/es/i18n/resources/en/image.mjs +6 -1
  50. package/es/i18n/resources/en/image.mjs.map +1 -1
  51. package/es/i18n/resources/zhCn/image.d.mts +5 -0
  52. package/es/i18n/resources/zhCn/image.mjs +6 -1
  53. package/es/i18n/resources/zhCn/image.mjs.map +1 -1
  54. package/es/index.mjs +8 -8
  55. package/package.json +1 -1
  56. package/es/Image/components/Preview.mjs +0 -23
  57. package/es/Image/components/Preview.mjs.map +0 -1
  58. package/es/Image/components/Toolbar.mjs +0 -129
  59. package/es/Image/components/Toolbar.mjs.map +0 -1
  60. package/es/Image/components/usePreview.mjs +0 -58
  61. package/es/Image/components/usePreview.mjs.map +0 -1
  62. package/es/Image/components/usePreviewGroup.mjs +0 -57
  63. package/es/Image/components/usePreviewGroup.mjs.map +0 -1
@@ -0,0 +1,346 @@
1
+ import { anchoredZoom, clampPan, clampScale, computeFit, doubleClickTarget, normalizeRotation, panBounds, rubberBand, wheelZoomFactor } from "./geometry.mjs";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ import { animate, motionValue } from "motion/react";
4
+ //#region src/Image/viewer/useZoomPan.ts
5
+ const WHEEL_CLOSE_THRESHOLD = 100;
6
+ const WHEEL_IDLE_MS = 300;
7
+ const ZOOM_STEP = 1.5;
8
+ const RESET_TRANSITION = {
9
+ damping: 30,
10
+ stiffness: 300,
11
+ type: "spring"
12
+ };
13
+ const CLEAN_EPSILON = .01;
14
+ const useZoomPan = ({ natural, viewport, maxScale, onCloseRequest, isClosing, fillViewport }) => {
15
+ const [{ scale, x, y, rotate, flipX, flipY }] = useState(() => ({
16
+ flipX: motionValue(false),
17
+ flipY: motionValue(false),
18
+ rotate: motionValue(0),
19
+ scale: motionValue(1),
20
+ x: motionValue(0),
21
+ y: motionValue(0)
22
+ }));
23
+ const naturalRef = useRef(natural);
24
+ const viewportRef = useRef(viewport);
25
+ const maxScaleRef = useRef(maxScale ?? 8);
26
+ maxScaleRef.current = maxScale ?? 8;
27
+ const fillViewportRef = useRef(fillViewport ?? false);
28
+ fillViewportRef.current = fillViewport ?? false;
29
+ const onCloseRequestRef = useRef(onCloseRequest);
30
+ onCloseRequestRef.current = onCloseRequest;
31
+ const isClosingRef = useRef(isClosing);
32
+ isClosingRef.current = isClosing;
33
+ const closeAccumRef = useRef(0);
34
+ const disarmedRef = useRef(false);
35
+ const idleTimerRef = useRef(null);
36
+ const resetCloseTracking = useCallback(() => {
37
+ if (idleTimerRef.current !== null) {
38
+ clearTimeout(idleTimerRef.current);
39
+ idleTimerRef.current = null;
40
+ }
41
+ closeAccumRef.current = 0;
42
+ disarmedRef.current = false;
43
+ }, []);
44
+ const closing = useCallback(() => isClosingRef.current?.() ?? false, []);
45
+ const isCleanState = useCallback(() => Math.abs(scale.get() - 1) < CLEAN_EPSILON && Math.abs(rotate.get()) < CLEAN_EPSILON && !flipX.get() && !flipY.get(), [
46
+ flipX,
47
+ flipY,
48
+ rotate,
49
+ scale
50
+ ]);
51
+ const computeDerived = useCallback(() => {
52
+ const currentScale = scale.get();
53
+ return {
54
+ canZoomIn: currentScale < maxScaleRef.current,
55
+ canZoomOut: currentScale > 1,
56
+ isClean: isCleanState(),
57
+ isZoomed: currentScale > 1,
58
+ rotation: normalizeRotation(rotate.get())
59
+ };
60
+ }, [
61
+ isCleanState,
62
+ rotate,
63
+ scale
64
+ ]);
65
+ const [derived, setDerived] = useState(computeDerived);
66
+ const syncDerived = useCallback(() => {
67
+ setDerived((prev) => {
68
+ const next = computeDerived();
69
+ if (prev.canZoomIn === next.canZoomIn && prev.canZoomOut === next.canZoomOut && prev.isClean === next.isClean && prev.isZoomed === next.isZoomed && prev.rotation === next.rotation) return prev;
70
+ return next;
71
+ });
72
+ }, [computeDerived]);
73
+ useEffect(() => {
74
+ const unsubscribers = [
75
+ scale,
76
+ rotate,
77
+ flipX,
78
+ flipY
79
+ ].map((value) => value.on("change", syncDerived));
80
+ syncDerived();
81
+ return () => {
82
+ for (const unsubscribe of unsubscribers) unsubscribe();
83
+ };
84
+ }, [
85
+ flipX,
86
+ flipY,
87
+ rotate,
88
+ scale,
89
+ syncDerived
90
+ ]);
91
+ const getFitRect = useCallback(() => computeFit(naturalRef.current, viewportRef.current, normalizeRotation(rotate.get()), fillViewportRef.current), [rotate]);
92
+ const applyTransform = useCallback((next, fitRect) => {
93
+ const clamped = clampPan(next, fitRect, viewportRef.current);
94
+ scale.jump(next.scale);
95
+ x.jump(clamped.x);
96
+ y.jump(clamped.y);
97
+ }, [
98
+ scale,
99
+ x,
100
+ y
101
+ ]);
102
+ const animateTransform = useCallback((next, fitRect) => {
103
+ const clamped = clampPan(next, fitRect, viewportRef.current);
104
+ animate(scale, next.scale, RESET_TRANSITION);
105
+ animate(x, clamped.x, RESET_TRANSITION);
106
+ animate(y, clamped.y, RESET_TRANSITION);
107
+ }, [
108
+ scale,
109
+ x,
110
+ y
111
+ ]);
112
+ const zoomBy = useCallback((factor) => {
113
+ if (closing()) return;
114
+ const currentScale = scale.get();
115
+ const targetScale = clampScale(currentScale * factor, maxScaleRef.current);
116
+ const fitRect = getFitRect();
117
+ const anchor = {
118
+ x: viewportRef.current.width / 2,
119
+ y: viewportRef.current.height / 2
120
+ };
121
+ const next = anchoredZoom({
122
+ scale: currentScale,
123
+ x: x.get(),
124
+ y: y.get()
125
+ }, targetScale, anchor, fitRect);
126
+ animateTransform(next, fitRect);
127
+ }, [
128
+ animateTransform,
129
+ closing,
130
+ getFitRect,
131
+ scale,
132
+ x,
133
+ y
134
+ ]);
135
+ const zoomIn = useCallback(() => zoomBy(ZOOM_STEP), [zoomBy]);
136
+ const zoomOut = useCallback(() => zoomBy(1 / ZOOM_STEP), [zoomBy]);
137
+ const handleDoubleClick = useCallback((point) => {
138
+ if (closing()) return;
139
+ const currentScale = scale.get();
140
+ const fitRect = getFitRect();
141
+ const target = doubleClickTarget(currentScale, naturalRef.current, fitRect, normalizeRotation(rotate.get()));
142
+ const targetScale = clampScale(target, maxScaleRef.current);
143
+ const next = anchoredZoom({
144
+ scale: currentScale,
145
+ x: x.get(),
146
+ y: y.get()
147
+ }, targetScale, point, fitRect);
148
+ animateTransform(next, fitRect);
149
+ }, [
150
+ animateTransform,
151
+ closing,
152
+ getFitRect,
153
+ rotate,
154
+ scale,
155
+ x,
156
+ y
157
+ ]);
158
+ const handleWheel = useCallback((event) => {
159
+ event.preventDefault?.();
160
+ if (closing()) return;
161
+ if (idleTimerRef.current !== null) clearTimeout(idleTimerRef.current);
162
+ idleTimerRef.current = setTimeout(() => {
163
+ idleTimerRef.current = null;
164
+ closeAccumRef.current = 0;
165
+ disarmedRef.current = false;
166
+ }, WHEEL_IDLE_MS);
167
+ const currentScale = scale.get();
168
+ if (!!(event.ctrlKey || event.metaKey) || currentScale > 1) {
169
+ disarmedRef.current = true;
170
+ closeAccumRef.current = 0;
171
+ const rawTarget = currentScale * wheelZoomFactor(event.deltaY);
172
+ if (rawTarget <= 1) {
173
+ animate(scale, 1, RESET_TRANSITION);
174
+ animate(x, 0, RESET_TRANSITION);
175
+ animate(y, 0, RESET_TRANSITION);
176
+ return;
177
+ }
178
+ const targetScale = clampScale(rawTarget, maxScaleRef.current);
179
+ const fitRect = getFitRect();
180
+ const anchor = {
181
+ x: event.clientX,
182
+ y: event.clientY
183
+ };
184
+ const next = anchoredZoom({
185
+ scale: currentScale,
186
+ x: x.get(),
187
+ y: y.get()
188
+ }, targetScale, anchor, fitRect);
189
+ applyTransform(next, fitRect);
190
+ return;
191
+ }
192
+ if (disarmedRef.current || !isCleanState()) return;
193
+ closeAccumRef.current += Math.abs(event.deltaY);
194
+ if (closeAccumRef.current >= WHEEL_CLOSE_THRESHOLD) {
195
+ closeAccumRef.current = 0;
196
+ onCloseRequestRef.current?.();
197
+ }
198
+ }, [
199
+ applyTransform,
200
+ closing,
201
+ getFitRect,
202
+ isCleanState,
203
+ scale,
204
+ x,
205
+ y
206
+ ]);
207
+ const rotateBy = useCallback((delta) => {
208
+ if (closing()) return;
209
+ rotate.jump(normalizeRotation(rotate.get() + delta));
210
+ scale.jump(1);
211
+ x.jump(0);
212
+ y.jump(0);
213
+ }, [
214
+ closing,
215
+ rotate,
216
+ scale,
217
+ x,
218
+ y
219
+ ]);
220
+ const rotateLeft = useCallback(() => rotateBy(-90), [rotateBy]);
221
+ const rotateRight = useCallback(() => rotateBy(90), [rotateBy]);
222
+ const flipHorizontal = useCallback(() => {
223
+ if (closing()) return;
224
+ flipX.set(!flipX.get());
225
+ }, [closing, flipX]);
226
+ const flipVertical = useCallback(() => {
227
+ if (closing()) return;
228
+ flipY.set(!flipY.get());
229
+ }, [closing, flipY]);
230
+ const reset = useCallback(() => {
231
+ if (closing()) return;
232
+ animate(scale, 1, RESET_TRANSITION);
233
+ animate(x, 0, RESET_TRANSITION);
234
+ animate(y, 0, RESET_TRANSITION);
235
+ animate(rotate, 0, RESET_TRANSITION);
236
+ flipX.set(false);
237
+ flipY.set(false);
238
+ }, [
239
+ closing,
240
+ flipX,
241
+ flipY,
242
+ rotate,
243
+ scale,
244
+ x,
245
+ y
246
+ ]);
247
+ const dragBy = useCallback((delta) => {
248
+ if (closing()) return;
249
+ const fitRect = getFitRect();
250
+ const bounds = panBounds({
251
+ scale: scale.get(),
252
+ x: 0,
253
+ y: 0
254
+ }, fitRect, viewportRef.current);
255
+ x.set(rubberBand(x.get() + delta.x, bounds.x.min, bounds.x.max));
256
+ y.set(rubberBand(y.get() + delta.y, bounds.y.min, bounds.y.max));
257
+ }, [
258
+ closing,
259
+ getFitRect,
260
+ scale,
261
+ x,
262
+ y
263
+ ]);
264
+ const dragEnd = useCallback(() => {
265
+ if (closing()) return;
266
+ const fitRect = getFitRect();
267
+ const clamped = clampPan({
268
+ scale: scale.get(),
269
+ x: x.get(),
270
+ y: y.get()
271
+ }, fitRect, viewportRef.current);
272
+ animate(x, clamped.x, RESET_TRANSITION);
273
+ animate(y, clamped.y, RESET_TRANSITION);
274
+ }, [
275
+ closing,
276
+ getFitRect,
277
+ scale,
278
+ x,
279
+ y
280
+ ]);
281
+ const escIntent = useCallback(() => isCleanState() ? "close" : "reset", [isCleanState]);
282
+ const setViewport = useCallback((next) => {
283
+ viewportRef.current = next;
284
+ const fitRect = computeFit(naturalRef.current, next, normalizeRotation(rotate.get()), fillViewportRef.current);
285
+ const clamped = clampPan({
286
+ scale: scale.get(),
287
+ x: x.get(),
288
+ y: y.get()
289
+ }, fitRect, next);
290
+ x.set(clamped.x);
291
+ y.set(clamped.y);
292
+ }, [
293
+ rotate,
294
+ scale,
295
+ x,
296
+ y
297
+ ]);
298
+ const setNatural = useCallback((next) => {
299
+ naturalRef.current = next;
300
+ const fitRect = computeFit(next, viewportRef.current, normalizeRotation(rotate.get()), fillViewportRef.current);
301
+ const clamped = clampPan({
302
+ scale: scale.get(),
303
+ x: x.get(),
304
+ y: y.get()
305
+ }, fitRect, viewportRef.current);
306
+ x.set(clamped.x);
307
+ y.set(clamped.y);
308
+ }, [
309
+ rotate,
310
+ scale,
311
+ x,
312
+ y
313
+ ]);
314
+ return {
315
+ canZoomIn: derived.canZoomIn,
316
+ canZoomOut: derived.canZoomOut,
317
+ dragBy,
318
+ dragEnd,
319
+ escIntent,
320
+ flipHorizontal,
321
+ flipVertical,
322
+ flipX,
323
+ flipY,
324
+ handleDoubleClick,
325
+ handleWheel,
326
+ isClean: derived.isClean,
327
+ isZoomed: derived.isZoomed,
328
+ reset,
329
+ resetCloseTracking,
330
+ rotate,
331
+ rotateLeft,
332
+ rotateRight,
333
+ rotation: derived.rotation,
334
+ scale,
335
+ setNatural,
336
+ setViewport,
337
+ x,
338
+ y,
339
+ zoomIn,
340
+ zoomOut
341
+ };
342
+ };
343
+ //#endregion
344
+ export { useZoomPan };
345
+
346
+ //# sourceMappingURL=useZoomPan.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useZoomPan.mjs","names":[],"sources":["../../../src/Image/viewer/useZoomPan.ts"],"sourcesContent":["import type { MotionValue } from 'motion/react';\nimport { animate, motionValue } from 'motion/react';\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport type { Point, Rect, Rotation, Size, TransformState } from './geometry';\nimport {\n anchoredZoom,\n clampPan,\n clampScale,\n computeFit,\n DEFAULT_MAX_SCALE,\n doubleClickTarget,\n normalizeRotation,\n panBounds,\n rubberBand,\n wheelZoomFactor,\n} from './geometry';\n\nconst WHEEL_CLOSE_THRESHOLD = 100;\nconst WHEEL_IDLE_MS = 300;\nconst ZOOM_STEP = 1.5;\nconst RESET_TRANSITION = { damping: 30, stiffness: 300, type: 'spring' as const };\nconst CLEAN_EPSILON = 0.01;\n\nexport interface WheelLikeEvent {\n clientX: number;\n clientY: number;\n ctrlKey?: boolean;\n deltaY: number;\n metaKey?: boolean;\n preventDefault?: () => void;\n}\n\nexport interface UseZoomPanOptions {\n fillViewport?: boolean;\n isClosing?: () => boolean;\n maxScale?: number;\n natural: Size;\n onCloseRequest?: () => void;\n viewport: Size;\n}\n\nexport interface UseZoomPanResult {\n canZoomIn: boolean;\n canZoomOut: boolean;\n dragBy: (delta: Point) => void;\n dragEnd: () => void;\n escIntent: () => 'reset' | 'close';\n flipHorizontal: () => void;\n flipVertical: () => void;\n flipX: MotionValue<boolean>;\n flipY: MotionValue<boolean>;\n handleDoubleClick: (point: Point) => void;\n handleWheel: (event: WheelLikeEvent) => void;\n isClean: boolean;\n isZoomed: boolean;\n reset: () => void;\n resetCloseTracking: () => void;\n rotate: MotionValue<number>;\n rotateLeft: () => void;\n rotateRight: () => void;\n rotation: Rotation;\n scale: MotionValue<number>;\n setNatural: (natural: Size) => void;\n setViewport: (viewport: Size) => void;\n x: MotionValue<number>;\n y: MotionValue<number>;\n zoomIn: () => void;\n zoomOut: () => void;\n}\n\ninterface DerivedState {\n canZoomIn: boolean;\n canZoomOut: boolean;\n isClean: boolean;\n isZoomed: boolean;\n rotation: Rotation;\n}\n\nexport const useZoomPan = ({\n natural,\n viewport,\n maxScale,\n onCloseRequest,\n isClosing,\n fillViewport,\n}: UseZoomPanOptions): UseZoomPanResult => {\n const [{ scale, x, y, rotate, flipX, flipY }] = useState(() => ({\n flipX: motionValue(false),\n flipY: motionValue(false),\n rotate: motionValue(0),\n scale: motionValue(1),\n x: motionValue(0),\n y: motionValue(0),\n }));\n\n const naturalRef = useRef(natural);\n const viewportRef = useRef(viewport);\n const maxScaleRef = useRef(maxScale ?? DEFAULT_MAX_SCALE);\n maxScaleRef.current = maxScale ?? DEFAULT_MAX_SCALE;\n const fillViewportRef = useRef(fillViewport ?? false);\n fillViewportRef.current = fillViewport ?? false;\n\n const onCloseRequestRef = useRef(onCloseRequest);\n onCloseRequestRef.current = onCloseRequest;\n\n const isClosingRef = useRef(isClosing);\n isClosingRef.current = isClosing;\n\n const closeAccumRef = useRef(0);\n const disarmedRef = useRef(false);\n const idleTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const resetCloseTracking = useCallback(() => {\n if (idleTimerRef.current !== null) {\n clearTimeout(idleTimerRef.current);\n idleTimerRef.current = null;\n }\n closeAccumRef.current = 0;\n disarmedRef.current = false;\n }, []);\n\n // The close FLIP spring animates these same scale/x/y values. Any of\n // these entry points still firing during the ~300ms close window would\n // either fight that spring visually or, for wheel/reset specifically,\n // race escIntent()/isCleanState() into cancelling the close outright.\n const closing = useCallback(() => isClosingRef.current?.() ?? false, []);\n\n const isCleanState = useCallback(\n () =>\n Math.abs(scale.get() - 1) < CLEAN_EPSILON &&\n Math.abs(rotate.get()) < CLEAN_EPSILON &&\n !flipX.get() &&\n !flipY.get(),\n [flipX, flipY, rotate, scale],\n );\n\n const computeDerived = useCallback((): DerivedState => {\n const currentScale = scale.get();\n return {\n canZoomIn: currentScale < maxScaleRef.current,\n canZoomOut: currentScale > 1,\n isClean: isCleanState(),\n isZoomed: currentScale > 1,\n rotation: normalizeRotation(rotate.get()),\n };\n }, [isCleanState, rotate, scale]);\n\n const [derived, setDerived] = useState<DerivedState>(computeDerived);\n\n const syncDerived = useCallback(() => {\n setDerived((prev) => {\n const next = computeDerived();\n if (\n prev.canZoomIn === next.canZoomIn &&\n prev.canZoomOut === next.canZoomOut &&\n prev.isClean === next.isClean &&\n prev.isZoomed === next.isZoomed &&\n prev.rotation === next.rotation\n ) {\n return prev;\n }\n return next;\n });\n }, [computeDerived]);\n\n useEffect(() => {\n const unsubscribers = [scale, rotate, flipX, flipY].map((value) =>\n value.on('change', syncDerived),\n );\n syncDerived();\n return () => {\n for (const unsubscribe of unsubscribers) unsubscribe();\n };\n }, [flipX, flipY, rotate, scale, syncDerived]);\n\n const getFitRect = useCallback(\n () =>\n computeFit(\n naturalRef.current,\n viewportRef.current,\n normalizeRotation(rotate.get()),\n fillViewportRef.current,\n ),\n [rotate],\n );\n\n const applyTransform = useCallback(\n (next: TransformState, fitRect: Rect) => {\n const clamped = clampPan(next, fitRect, viewportRef.current);\n // .jump(), not .set(): a surviving spring on these same values (e.g. a\n // dragEnd clamp-back or wheel snap-back still in flight) would otherwise\n // keep animating toward its own target and silently overwrite this\n // instant write on its next tick — motion's set() never stops an\n // active animation, only jump() does.\n scale.jump(next.scale);\n x.jump(clamped.x);\n y.jump(clamped.y);\n },\n [scale, x, y],\n );\n\n // Wheel/pinch stays on the instant applyTransform for 1:1 gesture tracking;\n // stepped inputs (toolbar/keyboard zoom, double-click) animate instead.\n const animateTransform = useCallback(\n (next: TransformState, fitRect: Rect) => {\n const clamped = clampPan(next, fitRect, viewportRef.current);\n animate(scale, next.scale, RESET_TRANSITION);\n animate(x, clamped.x, RESET_TRANSITION);\n animate(y, clamped.y, RESET_TRANSITION);\n },\n [scale, x, y],\n );\n\n const zoomBy = useCallback(\n (factor: number) => {\n if (closing()) return;\n const currentScale = scale.get();\n const targetScale = clampScale(currentScale * factor, maxScaleRef.current);\n const fitRect = getFitRect();\n const anchor: Point = { x: viewportRef.current.width / 2, y: viewportRef.current.height / 2 };\n const next = anchoredZoom(\n { scale: currentScale, x: x.get(), y: y.get() },\n targetScale,\n anchor,\n fitRect,\n );\n animateTransform(next, fitRect);\n },\n [animateTransform, closing, getFitRect, scale, x, y],\n );\n\n const zoomIn = useCallback(() => zoomBy(ZOOM_STEP), [zoomBy]);\n const zoomOut = useCallback(() => zoomBy(1 / ZOOM_STEP), [zoomBy]);\n\n const handleDoubleClick = useCallback(\n (point: Point) => {\n if (closing()) return;\n const currentScale = scale.get();\n const fitRect = getFitRect();\n const target = doubleClickTarget(\n currentScale,\n naturalRef.current,\n fitRect,\n normalizeRotation(rotate.get()),\n );\n const targetScale = clampScale(target, maxScaleRef.current);\n const next = anchoredZoom(\n { scale: currentScale, x: x.get(), y: y.get() },\n targetScale,\n point,\n fitRect,\n );\n animateTransform(next, fitRect);\n },\n [animateTransform, closing, getFitRect, rotate, scale, x, y],\n );\n\n const handleWheel = useCallback(\n (event: WheelLikeEvent) => {\n event.preventDefault?.();\n if (closing()) return;\n\n if (idleTimerRef.current !== null) clearTimeout(idleTimerRef.current);\n idleTimerRef.current = setTimeout(() => {\n idleTimerRef.current = null;\n closeAccumRef.current = 0;\n disarmedRef.current = false;\n }, WHEEL_IDLE_MS);\n\n const currentScale = scale.get();\n const isPinch = !!(event.ctrlKey || event.metaKey);\n const zoomed = currentScale > 1;\n\n if (isPinch || zoomed) {\n disarmedRef.current = true;\n closeAccumRef.current = 0;\n\n const rawTarget = currentScale * wheelZoomFactor(event.deltaY);\n\n if (rawTarget <= 1) {\n animate(scale, 1, RESET_TRANSITION);\n animate(x, 0, RESET_TRANSITION);\n animate(y, 0, RESET_TRANSITION);\n return;\n }\n\n const targetScale = clampScale(rawTarget, maxScaleRef.current);\n const fitRect = getFitRect();\n const anchor: Point = { x: event.clientX, y: event.clientY };\n const next = anchoredZoom(\n { scale: currentScale, x: x.get(), y: y.get() },\n targetScale,\n anchor,\n fitRect,\n );\n applyTransform(next, fitRect);\n return;\n }\n\n if (disarmedRef.current || !isCleanState()) return;\n\n closeAccumRef.current += Math.abs(event.deltaY);\n if (closeAccumRef.current >= WHEEL_CLOSE_THRESHOLD) {\n closeAccumRef.current = 0;\n onCloseRequestRef.current?.();\n }\n },\n [applyTransform, closing, getFitRect, isCleanState, scale, x, y],\n );\n\n const rotateBy = useCallback(\n (delta: 90 | -90) => {\n if (closing()) return;\n rotate.jump(normalizeRotation(rotate.get() + delta));\n scale.jump(1);\n x.jump(0);\n y.jump(0);\n },\n [closing, rotate, scale, x, y],\n );\n\n const rotateLeft = useCallback(() => rotateBy(-90), [rotateBy]);\n const rotateRight = useCallback(() => rotateBy(90), [rotateBy]);\n\n const flipHorizontal = useCallback(() => {\n if (closing()) return;\n flipX.set(!flipX.get());\n }, [closing, flipX]);\n\n const flipVertical = useCallback(() => {\n if (closing()) return;\n flipY.set(!flipY.get());\n }, [closing, flipY]);\n\n const reset = useCallback(() => {\n if (closing()) return;\n animate(scale, 1, RESET_TRANSITION);\n animate(x, 0, RESET_TRANSITION);\n animate(y, 0, RESET_TRANSITION);\n animate(rotate, 0, RESET_TRANSITION);\n flipX.set(false);\n flipY.set(false);\n }, [closing, flipX, flipY, rotate, scale, x, y]);\n\n const dragBy = useCallback(\n (delta: Point) => {\n // A pointer can still be down when close starts (e.g. pinch to just\n // over 1x — inside isClean's epsilon, so Esc/wheel reads 'close' —\n // while the same gesture is still panning). Gated for consistency\n // with the other entry points even though .set() here can't cancel\n // the close spring outright, only cause a self-correcting flicker.\n if (closing()) return;\n const fitRect = getFitRect();\n const bounds = panBounds({ scale: scale.get(), x: 0, y: 0 }, fitRect, viewportRef.current);\n x.set(rubberBand(x.get() + delta.x, bounds.x.min, bounds.x.max));\n y.set(rubberBand(y.get() + delta.y, bounds.y.min, bounds.y.max));\n },\n [closing, getFitRect, scale, x, y],\n );\n\n const dragEnd = useCallback(() => {\n if (closing()) return;\n const fitRect = getFitRect();\n const clamped = clampPan(\n { scale: scale.get(), x: x.get(), y: y.get() },\n fitRect,\n viewportRef.current,\n );\n animate(x, clamped.x, RESET_TRANSITION);\n animate(y, clamped.y, RESET_TRANSITION);\n }, [closing, getFitRect, scale, x, y]);\n\n const escIntent = useCallback(\n (): 'reset' | 'close' => (isCleanState() ? 'close' : 'reset'),\n [isCleanState],\n );\n\n const setViewport = useCallback(\n (next: Size) => {\n viewportRef.current = next;\n const fitRect = computeFit(\n naturalRef.current,\n next,\n normalizeRotation(rotate.get()),\n fillViewportRef.current,\n );\n const clamped = clampPan({ scale: scale.get(), x: x.get(), y: y.get() }, fitRect, next);\n x.set(clamped.x);\n y.set(clamped.y);\n },\n [rotate, scale, x, y],\n );\n\n const setNatural = useCallback(\n (next: Size) => {\n naturalRef.current = next;\n const fitRect = computeFit(\n next,\n viewportRef.current,\n normalizeRotation(rotate.get()),\n fillViewportRef.current,\n );\n const clamped = clampPan(\n { scale: scale.get(), x: x.get(), y: y.get() },\n fitRect,\n viewportRef.current,\n );\n x.set(clamped.x);\n y.set(clamped.y);\n },\n [rotate, scale, x, y],\n );\n\n return {\n canZoomIn: derived.canZoomIn,\n canZoomOut: derived.canZoomOut,\n dragBy,\n dragEnd,\n escIntent,\n flipHorizontal,\n flipVertical,\n flipX,\n flipY,\n handleDoubleClick,\n handleWheel,\n isClean: derived.isClean,\n isZoomed: derived.isZoomed,\n reset,\n resetCloseTracking,\n rotate,\n rotateLeft,\n rotateRight,\n rotation: derived.rotation,\n scale,\n setNatural,\n setViewport,\n x,\n y,\n zoomIn,\n zoomOut,\n };\n};\n\nexport type { Point, Rotation, Size } from './geometry';\n"],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ //#region src/base-ui/Toast/hostGuard.ts
2
+ const hostOrder = [];
3
+ const listeners = /* @__PURE__ */ new Set();
4
+ const emit = () => {
5
+ for (const listener of listeners) listener();
6
+ };
7
+ const registerToastHost = (id) => {
8
+ hostOrder.push(id);
9
+ emit();
10
+ return () => {
11
+ const index = hostOrder.indexOf(id);
12
+ if (index !== -1) hostOrder.splice(index, 1);
13
+ emit();
14
+ };
15
+ };
16
+ const isActiveToastHost = (id) => hostOrder[0] === id;
17
+ const subscribeToastHost = (listener) => {
18
+ listeners.add(listener);
19
+ return () => {
20
+ listeners.delete(listener);
21
+ };
22
+ };
23
+ //#endregion
24
+ export { isActiveToastHost, registerToastHost, subscribeToastHost };
25
+
26
+ //# sourceMappingURL=hostGuard.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hostGuard.mjs","names":[],"sources":["../../../src/base-ui/Toast/hostGuard.ts"],"sourcesContent":["const hostOrder: string[] = [];\nconst listeners = new Set<() => void>();\n\nconst emit = (): void => {\n for (const listener of listeners) listener();\n};\n\nexport const registerToastHost = (id: string): (() => void) => {\n hostOrder.push(id);\n emit();\n return () => {\n const index = hostOrder.indexOf(id);\n if (index !== -1) hostOrder.splice(index, 1);\n emit();\n };\n};\n\nexport const isActiveToastHost = (id: string): boolean => hostOrder[0] === id;\n\nexport const subscribeToastHost = (listener: () => void): (() => void) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n};\n\nexport const __resetToastHostRegistryForTests = (): void => {\n hostOrder.length = 0;\n};\n"],"mappings":";AAAA,MAAM,YAAsB,CAAC;AAC7B,MAAM,4BAAY,IAAI,IAAgB;AAEtC,MAAM,aAAmB;CACvB,KAAK,MAAM,YAAY,WAAW,SAAS;AAC7C;AAEA,MAAa,qBAAqB,OAA6B;CAC7D,UAAU,KAAK,EAAE;CACjB,KAAK;CACL,aAAa;EACX,MAAM,QAAQ,UAAU,QAAQ,EAAE;EAClC,IAAI,UAAU,IAAI,UAAU,OAAO,OAAO,CAAC;EAC3C,KAAK;CACP;AACF;AAEA,MAAa,qBAAqB,OAAwB,UAAU,OAAO;AAE3E,MAAa,sBAAsB,aAAuC;CACxE,UAAU,IAAI,QAAQ;CACtB,aAAa;EACX,UAAU,OAAO,QAAQ;CAC3B;AACF"}
@@ -3,9 +3,11 @@ import { useIsClient } from "../../hooks/useIsClient.mjs";
3
3
  import { useAppElement } from "../../ThemeProvider/AppElementContext.mjs";
4
4
  import { acquireLayerZIndex } from "../zIndex/manager.mjs";
5
5
  import { ToastContext } from "./context.mjs";
6
+ import { isActiveToastHost, registerToastHost, subscribeToastHost } from "./hostGuard.mjs";
7
+ import { markToastHostNotReady, markToastHostReady, runWhenToastHostReady } from "./pendingQueue.mjs";
6
8
  import { viewportVariants } from "./style.mjs";
7
9
  import ToastItem from "./Toast.mjs";
8
- import { memo, useEffect, useState } from "react";
10
+ import { memo, useEffect, useId, useState, useSyncExternalStore } from "react";
9
11
  import { jsx } from "react/jsx-runtime";
10
12
  import { cx } from "antd-style";
11
13
  import { Toast } from "@base-ui/react/toast";
@@ -32,15 +34,19 @@ const toastManagers = {
32
34
  "top-left": Toast.createToastManager(),
33
35
  "top-right": Toast.createToastManager()
34
36
  };
35
- const activeToastIds = {
36
- "bottom": /* @__PURE__ */ new Set(),
37
- "bottom-left": /* @__PURE__ */ new Set(),
38
- "bottom-right": /* @__PURE__ */ new Set(),
39
- "top": /* @__PURE__ */ new Set(),
40
- "top-left": /* @__PURE__ */ new Set(),
41
- "top-right": /* @__PURE__ */ new Set()
37
+ const activeToasts = {
38
+ "bottom": /* @__PURE__ */ new Map(),
39
+ "bottom-left": /* @__PURE__ */ new Map(),
40
+ "bottom-right": /* @__PURE__ */ new Map(),
41
+ "top": /* @__PURE__ */ new Map(),
42
+ "top-left": /* @__PURE__ */ new Map(),
43
+ "top-right": /* @__PURE__ */ new Map()
42
44
  };
43
45
  const getManager = (position) => toastManagers[position];
46
+ let toastIdCounter = 0;
47
+ const generateToastId = () => `toast-${Date.now().toString(36)}-${(toastIdCounter++).toString(36)}`;
48
+ const findActivePosition = (id) => ALL_POSITIONS.find((pos) => activeToasts[pos].has(id));
49
+ const isFrontMost = (position, id) => Array.from(activeToasts[position].keys()).at(-1) === id;
44
50
  const normalizeOptions = (optionsOrMessage, type) => {
45
51
  if (typeof optionsOrMessage === "string") return {
46
52
  description: optionsOrMessage,
@@ -52,43 +58,62 @@ const normalizeOptions = (optionsOrMessage, type) => {
52
58
  };
53
59
  };
54
60
  const createToastInstance = (id, position) => ({
55
- close: () => getManager(position).close(id),
61
+ close: () => runWhenToastHostReady(() => getManager(position).close(id)),
56
62
  id,
57
63
  update: (options) => {
58
- getManager(position).update(id, {
64
+ runWhenToastHostReady(() => getManager(position).update(id, {
59
65
  data: options,
60
66
  description: options.description,
61
67
  title: options.title
62
- });
68
+ }));
63
69
  }
64
70
  });
65
71
  const addToast = (options) => {
66
72
  const position = options.placement ?? globalState.position;
67
73
  const manager = getManager(position);
68
- const onRemove = options.onRemove;
69
- const id = manager.add({
70
- data: options,
71
- description: options.description,
72
- onClose: options.onClose,
73
- onRemove: () => {
74
- activeToastIds[position].delete(id);
75
- onRemove?.();
76
- },
77
- timeout: options.duration ?? globalState.duration,
78
- title: options.title
74
+ const { id: dedupeId, onClose, onRemove } = options;
75
+ if (dedupeId) {
76
+ const prevPosition = findActivePosition(dedupeId);
77
+ if (prevPosition && !(prevPosition === position && isFrontMost(position, dedupeId))) {
78
+ activeToasts[prevPosition].get(dedupeId).superseded = true;
79
+ activeToasts[prevPosition].delete(dedupeId);
80
+ runWhenToastHostReady(() => getManager(prevPosition).close(dedupeId));
81
+ }
82
+ }
83
+ const id = dedupeId ?? generateToastId();
84
+ const active = { superseded: false };
85
+ activeToasts[position].set(id, active);
86
+ runWhenToastHostReady(() => {
87
+ manager.add({
88
+ id,
89
+ data: options,
90
+ description: options.description,
91
+ onClose: () => {
92
+ if (active.superseded) return;
93
+ onClose?.();
94
+ },
95
+ onRemove: () => {
96
+ if (active.superseded) return;
97
+ activeToasts[position].delete(id);
98
+ onRemove?.();
99
+ },
100
+ timeout: options.duration ?? globalState.duration,
101
+ title: options.title
102
+ });
79
103
  });
80
- activeToastIds[position].add(id);
81
104
  return createToastInstance(id, position);
82
105
  };
83
106
  const dismissToast = (id) => {
84
107
  if (id) for (const [position, manager] of Object.entries(toastManagers)) {
85
- activeToastIds[position].delete(id);
86
- manager.close(id);
108
+ activeToasts[position].delete(id);
109
+ runWhenToastHostReady(() => manager.close(id));
87
110
  }
88
111
  else for (const [position, manager] of Object.entries(toastManagers)) {
89
- const ids = Array.from(activeToastIds[position]);
90
- for (const toastId of ids) manager.close(toastId);
91
- activeToastIds[position].clear();
112
+ const ids = Array.from(activeToasts[position].keys());
113
+ activeToasts[position].clear();
114
+ runWhenToastHostReady(() => {
115
+ for (const toastId of ids) manager.close(toastId);
116
+ });
92
117
  }
93
118
  };
94
119
  const createSuccessToast = (optionsOrMessage) => {
@@ -169,7 +194,11 @@ const ToastHost = memo(({ root, className, duration = 5e3, limit = 5, position =
169
194
  const isClient = useIsClient();
170
195
  const appElement = useAppElement();
171
196
  const [viewportZIndex, setViewportZIndex] = useState(void 0);
197
+ const hostId = useId();
198
+ useEffect(() => registerToastHost(hostId), [hostId]);
199
+ const isActive = useSyncExternalStore(subscribeToastHost, () => isActiveToastHost(hostId), () => false);
172
200
  useEffect(() => {
201
+ if (!isActive) return;
173
202
  globalState = {
174
203
  duration,
175
204
  limit,
@@ -180,12 +209,21 @@ const ToastHost = memo(({ root, className, duration = 5e3, limit = 5, position =
180
209
  duration,
181
210
  limit,
182
211
  position,
183
- swipeDirection
212
+ swipeDirection,
213
+ isActive
184
214
  ]);
185
215
  useEffect(() => {
216
+ if (!isActive) return;
186
217
  setViewportZIndex(acquireLayerZIndex("toast"));
187
- }, []);
188
- if (!isClient) return null;
218
+ }, [isActive]);
219
+ useEffect(() => {
220
+ if (!isActive || !isClient) return void 0;
221
+ markToastHostReady();
222
+ return () => {
223
+ markToastHostNotReady();
224
+ };
225
+ }, [isActive, isClient]);
226
+ if (!isClient || !isActive) return null;
189
227
  const container = root ?? appElement ?? document.body;
190
228
  return ALL_POSITIONS.map((pos) => /* @__PURE__ */ jsx(ToastContext, {
191
229
  value: {
@@ -1 +1 @@
1
- {"version":3,"file":"imperative.mjs","names":["BaseToast"],"sources":["../../../src/base-ui/Toast/imperative.tsx"],"sourcesContent":["'use client';\n\nimport { Toast as BaseToast } from '@base-ui/react/toast';\nimport { cx } from 'antd-style';\nimport { memo, useEffect, useState } from 'react';\n\nimport { useIsClient } from '@/hooks/useIsClient';\nimport { useAppElement } from '@/ThemeProvider';\n\nimport { acquireLayerZIndex } from '../zIndex';\nimport { ToastContext } from './context';\nimport { viewportVariants } from './style';\nimport ToastItem from './Toast';\nimport {\n type ToastAPI,\n type ToastInstance,\n type ToastOptions,\n type ToastPosition,\n type ToastPromiseOptions,\n type ToastType,\n} from './type';\n\n// All possible positions\nconst ALL_POSITIONS: ToastPosition[] = [\n 'top',\n 'top-left',\n 'top-right',\n 'bottom',\n 'bottom-left',\n 'bottom-right',\n];\n\n// Global state management\ninterface ToastState {\n duration: number;\n limit: number;\n position: ToastPosition;\n swipeDirection: ('left' | 'right' | 'up' | 'down') | ('left' | 'right' | 'up' | 'down')[];\n}\n\nlet globalState: ToastState = {\n duration: 5000,\n limit: 5,\n position: 'bottom-right',\n swipeDirection: ['down', 'right'],\n};\n\n// Toast managers for each position\nconst toastManagers: Record<ToastPosition, ReturnType<typeof BaseToast.createToastManager>> = {\n 'bottom': BaseToast.createToastManager(),\n 'bottom-left': BaseToast.createToastManager(),\n 'bottom-right': BaseToast.createToastManager(),\n 'top': BaseToast.createToastManager(),\n 'top-left': BaseToast.createToastManager(),\n 'top-right': BaseToast.createToastManager(),\n};\n\nconst activeToastIds: Record<ToastPosition, Set<string>> = {\n 'bottom': new Set(),\n 'bottom-left': new Set(),\n 'bottom-right': new Set(),\n 'top': new Set(),\n 'top-left': new Set(),\n 'top-right': new Set(),\n};\n\nconst getManager = (position: ToastPosition) => toastManagers[position];\n\nconst normalizeOptions = (\n optionsOrMessage: Omit<ToastOptions, 'type'> | string,\n type: ToastType,\n): ToastOptions => {\n if (typeof optionsOrMessage === 'string') {\n return {\n description: optionsOrMessage,\n type,\n };\n }\n return {\n ...optionsOrMessage,\n type,\n };\n};\n\nconst createToastInstance = (id: string, position: ToastPosition): ToastInstance => ({\n close: () => getManager(position).close(id),\n id,\n update: (options) => {\n getManager(position).update(id, {\n data: options,\n description: options.description,\n title: options.title,\n });\n },\n});\n\nconst addToast = (options: ToastOptions): ToastInstance => {\n const position = options.placement ?? globalState.position;\n const manager = getManager(position);\n const onRemove = options.onRemove;\n const id = manager.add({\n data: options,\n description: options.description,\n onClose: options.onClose,\n onRemove: () => {\n activeToastIds[position].delete(id);\n onRemove?.();\n },\n timeout: options.duration ?? globalState.duration,\n title: options.title,\n });\n activeToastIds[position].add(id);\n return createToastInstance(id, position);\n};\n\nconst dismissToast = (id?: string) => {\n if (id) {\n // Try to close from all managers since we don't know which position the toast is in\n for (const [position, manager] of Object.entries(toastManagers)) {\n activeToastIds[position as ToastPosition].delete(id);\n manager.close(id);\n }\n } else {\n // Clear all toasts\n for (const [position, manager] of Object.entries(toastManagers)) {\n const ids = Array.from(activeToastIds[position as ToastPosition]);\n for (const toastId of ids) {\n manager.close(toastId);\n }\n activeToastIds[position as ToastPosition].clear();\n }\n }\n};\n\nconst createSuccessToast = (\n optionsOrMessage: Omit<ToastOptions, 'type'> | string,\n): ToastInstance => {\n return addToast(normalizeOptions(optionsOrMessage, 'success'));\n};\n\nconst createErrorToast = (optionsOrMessage: Omit<ToastOptions, 'type'> | string): ToastInstance => {\n return addToast(normalizeOptions(optionsOrMessage, 'error'));\n};\n\nconst createInfoToast = (optionsOrMessage: Omit<ToastOptions, 'type'> | string): ToastInstance => {\n return addToast(normalizeOptions(optionsOrMessage, 'info'));\n};\n\nconst createWarningToast = (\n optionsOrMessage: Omit<ToastOptions, 'type'> | string,\n): ToastInstance => {\n return addToast(normalizeOptions(optionsOrMessage, 'warning'));\n};\n\nconst createLoadingToast = (\n optionsOrMessage: Omit<ToastOptions, 'type'> | string,\n): ToastInstance => {\n const options = normalizeOptions(optionsOrMessage, 'loading');\n // Loading toasts don't auto-dismiss by default\n return addToast({ duration: 0, ...options });\n};\n\nasync function promiseToast<T>(promise: Promise<T>, options: ToastPromiseOptions<T>): Promise<T> {\n const loadingOptions =\n typeof options.loading === 'string'\n ? { description: options.loading }\n : (options.loading as ToastOptions);\n\n const loadingToast = addToast({\n closable: false,\n duration: 0,\n type: 'loading',\n ...loadingOptions,\n });\n\n try {\n const result = await promise;\n\n loadingToast.close();\n\n const successOptions = (() => {\n if (typeof options.success === 'string') {\n return { description: options.success };\n }\n if (typeof options.success === 'function') {\n return { description: options.success(result) };\n }\n return options.success as ToastOptions;\n })();\n\n addToast({ type: 'success', ...successOptions });\n\n return result;\n } catch (error) {\n loadingToast.close();\n\n const errorOptions = (() => {\n if (typeof options.error === 'string') {\n return { description: options.error };\n }\n if (typeof options.error === 'function') {\n return { description: options.error(error as Error) };\n }\n return options.error as ToastOptions;\n })();\n\n addToast({ type: 'error', ...errorOptions });\n\n throw error;\n }\n}\n\n// Base toast function\nconst baseToast = (options: ToastOptions): ToastInstance => {\n return addToast({ type: 'default', ...options });\n};\n\n// Toast API\nexport const toast: ToastAPI = Object.assign(baseToast, {\n dismiss: dismissToast,\n error: createErrorToast,\n info: createInfoToast,\n loading: createLoadingToast,\n promise: promiseToast,\n success: createSuccessToast,\n warning: createWarningToast,\n});\n\n// Toast List Component\nconst ToastList = memo(() => {\n const { toasts } = BaseToast.useToastManager();\n return toasts.map((t) => <ToastItem key={t.id} toast={t} />);\n});\n\nToastList.displayName = 'ToastList';\n\nexport interface ToastHostProps {\n className?: string;\n /**\n * Default duration for toasts\n * @default 5000\n */\n duration?: number;\n /**\n * Maximum number of toasts\n * @default 5\n */\n limit?: number;\n /**\n * Toast position\n * @default 'bottom-right'\n */\n position?: ToastPosition;\n /**\n * Root element for portal\n */\n root?: HTMLElement | ShadowRoot | null;\n /**\n * Swipe direction to dismiss\n * @default ['down', 'right']\n */\n swipeDirection?: ('left' | 'right' | 'up' | 'down') | ('left' | 'right' | 'up' | 'down')[];\n}\n\nexport const ToastHost = memo(\n ({\n root,\n className,\n duration = 5000,\n limit = 5,\n position = 'bottom-right',\n swipeDirection = ['down', 'right'],\n }: ToastHostProps) => {\n const isClient = useIsClient();\n const appElement = useAppElement();\n const [viewportZIndex, setViewportZIndex] = useState<number | undefined>(undefined);\n\n useEffect(() => {\n globalState = {\n duration,\n limit,\n position,\n swipeDirection,\n };\n }, [duration, limit, position, swipeDirection]);\n\n useEffect(() => {\n setViewportZIndex(acquireLayerZIndex('toast'));\n }, []);\n\n if (!isClient) return null;\n\n const container = root ?? appElement ?? document.body;\n\n return ALL_POSITIONS.map((pos) => (\n <ToastContext key={pos} value={{ position: pos, swipeDirection }}>\n <BaseToast.Provider limit={limit} timeout={duration} toastManager={getManager(pos)}>\n <BaseToast.Portal container={container}>\n <BaseToast.Viewport\n className={cx(viewportVariants({ position: pos }), className)}\n style={{ zIndex: viewportZIndex }}\n >\n <ToastList />\n </BaseToast.Viewport>\n </BaseToast.Portal>\n </BaseToast.Provider>\n </ToastContext>\n ));\n },\n);\n\nToastHost.displayName = 'ToastHost';\n\n// Hook to use toast manager\nexport const useToast = () => toast;\n"],"mappings":"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"}
1
+ {"version":3,"file":"imperative.mjs","names":["BaseToast"],"sources":["../../../src/base-ui/Toast/imperative.tsx"],"sourcesContent":["'use client';\n\nimport { Toast as BaseToast } from '@base-ui/react/toast';\nimport { cx } from 'antd-style';\nimport { memo, useEffect, useId, useState, useSyncExternalStore } from 'react';\n\nimport { useIsClient } from '@/hooks/useIsClient';\nimport { useAppElement } from '@/ThemeProvider';\n\nimport { acquireLayerZIndex } from '../zIndex';\nimport { ToastContext } from './context';\nimport { isActiveToastHost, registerToastHost, subscribeToastHost } from './hostGuard';\nimport {\n __resetPendingToastQueueForTests,\n markToastHostNotReady,\n markToastHostReady,\n runWhenToastHostReady,\n} from './pendingQueue';\nimport { viewportVariants } from './style';\nimport ToastItem from './Toast';\nimport {\n type ToastAPI,\n type ToastInstance,\n type ToastOptions,\n type ToastPosition,\n type ToastPromiseOptions,\n type ToastType,\n} from './type';\n\n// All possible positions\nconst ALL_POSITIONS: ToastPosition[] = [\n 'top',\n 'top-left',\n 'top-right',\n 'bottom',\n 'bottom-left',\n 'bottom-right',\n];\n\n// Global state management\ninterface ToastState {\n duration: number;\n limit: number;\n position: ToastPosition;\n swipeDirection: ('left' | 'right' | 'up' | 'down') | ('left' | 'right' | 'up' | 'down')[];\n}\n\nlet globalState: ToastState = {\n duration: 5000,\n limit: 5,\n position: 'bottom-right',\n swipeDirection: ['down', 'right'],\n};\n\n// Toast managers for each position\nconst toastManagers: Record<ToastPosition, ReturnType<typeof BaseToast.createToastManager>> = {\n 'bottom': BaseToast.createToastManager(),\n 'bottom-left': BaseToast.createToastManager(),\n 'bottom-right': BaseToast.createToastManager(),\n 'top': BaseToast.createToastManager(),\n 'top-left': BaseToast.createToastManager(),\n 'top-right': BaseToast.createToastManager(),\n};\n\ninterface ActiveToast {\n superseded: boolean;\n}\n\nconst activeToasts: Record<ToastPosition, Map<string, ActiveToast>> = {\n 'bottom': new Map(),\n 'bottom-left': new Map(),\n 'bottom-right': new Map(),\n 'top': new Map(),\n 'top-left': new Map(),\n 'top-right': new Map(),\n};\n\nconst getManager = (position: ToastPosition) => toastManagers[position];\n\nlet toastIdCounter = 0;\nconst generateToastId = (): string =>\n `toast-${Date.now().toString(36)}-${(toastIdCounter++).toString(36)}`;\n\nconst findActivePosition = (id: string) => ALL_POSITIONS.find((pos) => activeToasts[pos].has(id));\n\n// Base UI prepends every new toast, so the last entry we registered is the one\n// currently rendered at the front of the stack.\nconst isFrontMost = (position: ToastPosition, id: string) =>\n Array.from(activeToasts[position].keys()).at(-1) === id;\n\nconst normalizeOptions = (\n optionsOrMessage: Omit<ToastOptions, 'type'> | string,\n type: ToastType,\n): ToastOptions => {\n if (typeof optionsOrMessage === 'string') {\n return {\n description: optionsOrMessage,\n type,\n };\n }\n return {\n ...optionsOrMessage,\n type,\n };\n};\n\nconst createToastInstance = (id: string, position: ToastPosition): ToastInstance => ({\n close: () => runWhenToastHostReady(() => getManager(position).close(id)),\n id,\n update: (options) => {\n runWhenToastHostReady(() =>\n getManager(position).update(id, {\n data: options,\n description: options.description,\n title: options.title,\n }),\n );\n },\n});\n\n// createToastManager() is a stateless emitter (see @base-ui/react/toast/createToastManager):\n// add/close/update just broadcast to whatever is currently subscribed, and the\n// ToastProvider that owns the real toast state only subscribes from a passive\n// useEffect. Calls made while no host is ready (e.g. mid-handoff between the\n// previously-active ToastHost unmounting and its successor's Provider\n// subscribing) would otherwise be silently dropped, so every manager call is\n// routed through runWhenToastHostReady to queue until a host is listening.\nconst addToast = (options: ToastOptions): ToastInstance => {\n const position = options.placement ?? globalState.position;\n const manager = getManager(position);\n const { id: dedupeId, onClose, onRemove } = options;\n\n if (dedupeId) {\n const prevPosition = findActivePosition(dedupeId);\n // Already the front-most toast: let Base UI upsert it in place so it only\n // refreshes its content and timer, without replaying the slide-in animation.\n const shouldPromote =\n prevPosition && !(prevPosition === position && isFrontMost(position, dedupeId));\n\n if (shouldPromote) {\n // Closing marks the toast as `ending`, which makes the following `add`\n // drop it and prepend a fresh one — Base UI's own upsert keeps the\n // original slot instead.\n activeToasts[prevPosition].get(dedupeId)!.superseded = true;\n activeToasts[prevPosition].delete(dedupeId);\n runWhenToastHostReady(() => getManager(prevPosition).close(dedupeId));\n }\n }\n\n const id = dedupeId ?? generateToastId();\n const active: ActiveToast = { superseded: false };\n activeToasts[position].set(id, active);\n runWhenToastHostReady(() => {\n manager.add({\n id,\n data: options,\n description: options.description,\n onClose: () => {\n if (active.superseded) return;\n onClose?.();\n },\n onRemove: () => {\n if (active.superseded) return;\n activeToasts[position].delete(id);\n onRemove?.();\n },\n timeout: options.duration ?? globalState.duration,\n title: options.title,\n });\n });\n return createToastInstance(id, position);\n};\n\nconst dismissToast = (id?: string) => {\n if (id) {\n // Try to close from all managers since we don't know which position the toast is in\n for (const [position, manager] of Object.entries(toastManagers)) {\n activeToasts[position as ToastPosition].delete(id);\n runWhenToastHostReady(() => manager.close(id));\n }\n } else {\n // Clear all toasts\n for (const [position, manager] of Object.entries(toastManagers)) {\n const ids = Array.from(activeToasts[position as ToastPosition].keys());\n activeToasts[position as ToastPosition].clear();\n runWhenToastHostReady(() => {\n for (const toastId of ids) {\n manager.close(toastId);\n }\n });\n }\n }\n};\n\nconst createSuccessToast = (\n optionsOrMessage: Omit<ToastOptions, 'type'> | string,\n): ToastInstance => {\n return addToast(normalizeOptions(optionsOrMessage, 'success'));\n};\n\nconst createErrorToast = (optionsOrMessage: Omit<ToastOptions, 'type'> | string): ToastInstance => {\n return addToast(normalizeOptions(optionsOrMessage, 'error'));\n};\n\nconst createInfoToast = (optionsOrMessage: Omit<ToastOptions, 'type'> | string): ToastInstance => {\n return addToast(normalizeOptions(optionsOrMessage, 'info'));\n};\n\nconst createWarningToast = (\n optionsOrMessage: Omit<ToastOptions, 'type'> | string,\n): ToastInstance => {\n return addToast(normalizeOptions(optionsOrMessage, 'warning'));\n};\n\nconst createLoadingToast = (\n optionsOrMessage: Omit<ToastOptions, 'type'> | string,\n): ToastInstance => {\n const options = normalizeOptions(optionsOrMessage, 'loading');\n // Loading toasts don't auto-dismiss by default\n return addToast({ duration: 0, ...options });\n};\n\nasync function promiseToast<T>(promise: Promise<T>, options: ToastPromiseOptions<T>): Promise<T> {\n const loadingOptions =\n typeof options.loading === 'string'\n ? { description: options.loading }\n : (options.loading as ToastOptions);\n\n const loadingToast = addToast({\n closable: false,\n duration: 0,\n type: 'loading',\n ...loadingOptions,\n });\n\n try {\n const result = await promise;\n\n loadingToast.close();\n\n const successOptions = (() => {\n if (typeof options.success === 'string') {\n return { description: options.success };\n }\n if (typeof options.success === 'function') {\n return { description: options.success(result) };\n }\n return options.success as ToastOptions;\n })();\n\n addToast({ type: 'success', ...successOptions });\n\n return result;\n } catch (error) {\n loadingToast.close();\n\n const errorOptions = (() => {\n if (typeof options.error === 'string') {\n return { description: options.error };\n }\n if (typeof options.error === 'function') {\n return { description: options.error(error as Error) };\n }\n return options.error as ToastOptions;\n })();\n\n addToast({ type: 'error', ...errorOptions });\n\n throw error;\n }\n}\n\n// Base toast function\nconst baseToast = (options: ToastOptions): ToastInstance => {\n return addToast({ type: 'default', ...options });\n};\n\n// Toast API\nexport const toast: ToastAPI = Object.assign(baseToast, {\n dismiss: dismissToast,\n error: createErrorToast,\n info: createInfoToast,\n loading: createLoadingToast,\n promise: promiseToast,\n success: createSuccessToast,\n warning: createWarningToast,\n});\n\n// Toast List Component\nconst ToastList = memo(() => {\n const { toasts } = BaseToast.useToastManager();\n return toasts.map((t) => <ToastItem key={t.id} toast={t} />);\n});\n\nToastList.displayName = 'ToastList';\n\nexport interface ToastHostProps {\n className?: string;\n /**\n * Default duration for toasts\n * @default 5000\n */\n duration?: number;\n /**\n * Maximum number of toasts\n * @default 5\n */\n limit?: number;\n /**\n * Toast position\n * @default 'bottom-right'\n */\n position?: ToastPosition;\n /**\n * Root element for portal\n */\n root?: HTMLElement | ShadowRoot | null;\n /**\n * Swipe direction to dismiss\n * @default ['down', 'right']\n */\n swipeDirection?: ('left' | 'right' | 'up' | 'down') | ('left' | 'right' | 'up' | 'down')[];\n}\n\nexport const ToastHost = memo(\n ({\n root,\n className,\n duration = 5000,\n limit = 5,\n position = 'bottom-right',\n swipeDirection = ['down', 'right'],\n }: ToastHostProps) => {\n const isClient = useIsClient();\n const appElement = useAppElement();\n const [viewportZIndex, setViewportZIndex] = useState<number | undefined>(undefined);\n const hostId = useId();\n\n useEffect(() => registerToastHost(hostId), [hostId]);\n\n const isActive = useSyncExternalStore(\n subscribeToastHost,\n () => isActiveToastHost(hostId),\n () => false,\n );\n\n useEffect(() => {\n if (!isActive) return;\n globalState = {\n duration,\n limit,\n position,\n swipeDirection,\n };\n }, [duration, limit, position, swipeDirection, isActive]);\n\n useEffect(() => {\n if (!isActive) return;\n setViewportZIndex(acquireLayerZIndex('toast'));\n }, [isActive]);\n\n useEffect(() => {\n if (!isActive || !isClient) return undefined;\n // Runs after the six BaseToast.Provider children below have committed\n // and subscribed (child effects run before the parent's), so the\n // managers are guaranteed to have a live listener once this fires.\n markToastHostReady();\n return () => {\n markToastHostNotReady();\n };\n }, [isActive, isClient]);\n\n if (!isClient || !isActive) return null;\n\n const container = root ?? appElement ?? document.body;\n\n return ALL_POSITIONS.map((pos) => (\n <ToastContext key={pos} value={{ position: pos, swipeDirection }}>\n <BaseToast.Provider limit={limit} timeout={duration} toastManager={getManager(pos)}>\n <BaseToast.Portal container={container}>\n <BaseToast.Viewport\n className={cx(viewportVariants({ position: pos }), className)}\n style={{ zIndex: viewportZIndex }}\n >\n <ToastList />\n </BaseToast.Viewport>\n </BaseToast.Portal>\n </BaseToast.Provider>\n </ToastContext>\n ));\n },\n);\n\nToastHost.displayName = 'ToastHost';\n\n// Hook to use toast manager\nexport const useToast = () => toast;\n\nexport const __resetToastStateForTests = (): void => {\n globalState = {\n duration: 5000,\n limit: 5,\n position: 'bottom-right',\n swipeDirection: ['down', 'right'],\n };\n for (const position of ALL_POSITIONS) {\n toastManagers[position] = BaseToast.createToastManager();\n activeToasts[position].clear();\n }\n __resetPendingToastQueueForTests();\n};\n"],"mappings":"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"}