@lobehub/ui 5.25.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 (62) 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 +43 -20
  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/index.mjs +1 -1
  47. package/es/i18n/resources/en/image.d.mts +5 -0
  48. package/es/i18n/resources/en/image.mjs +6 -1
  49. package/es/i18n/resources/en/image.mjs.map +1 -1
  50. package/es/i18n/resources/zhCn/image.d.mts +5 -0
  51. package/es/i18n/resources/zhCn/image.mjs +6 -1
  52. package/es/i18n/resources/zhCn/image.mjs.map +1 -1
  53. package/es/index.mjs +8 -8
  54. package/package.json +1 -1
  55. package/es/Image/components/Preview.mjs +0 -23
  56. package/es/Image/components/Preview.mjs.map +0 -1
  57. package/es/Image/components/Toolbar.mjs +0 -129
  58. package/es/Image/components/Toolbar.mjs.map +0 -1
  59. package/es/Image/components/usePreview.mjs +0 -58
  60. package/es/Image/components/usePreview.mjs.map +0 -1
  61. package/es/Image/components/usePreviewGroup.mjs +0 -57
  62. package/es/Image/components/usePreviewGroup.mjs.map +0 -1
@@ -0,0 +1,54 @@
1
+ "use client";
2
+ import ActionIcon from "../../ActionIcon/ActionIcon.mjs";
3
+ import { useTranslation } from "../../i18n/useTranslation.mjs";
4
+ import { styles } from "../style.mjs";
5
+ import image_default from "../../i18n/resources/en/image.mjs";
6
+ import GalleryNav from "./GalleryNav.mjs";
7
+ import Toolbar from "./Toolbar.mjs";
8
+ import { useChromeIdle } from "./useChromeIdle.mjs";
9
+ import { memo, useCallback } from "react";
10
+ import { jsx, jsxs } from "react/jsx-runtime";
11
+ import { X } from "lucide-react";
12
+ //#region src/Image/viewer/ViewerChrome.tsx
13
+ const ViewerChrome = memo(({ chromeRef, current, hasNext, hasPrev, next, onClose, prev, total, ...toolbarProps }) => {
14
+ const { t } = useTranslation(image_default);
15
+ const idle = useChromeIdle();
16
+ const handleChromeClick = useCallback((event) => {
17
+ event.stopPropagation();
18
+ }, []);
19
+ return /* @__PURE__ */ jsx("div", {
20
+ className: styles.viewerChrome,
21
+ ref: chromeRef,
22
+ onClick: handleChromeClick,
23
+ children: /* @__PURE__ */ jsxs("div", {
24
+ className: styles.viewerChromeIdle,
25
+ "data-idle-hidden": idle.hidden ? "" : void 0,
26
+ ref: idle.ref,
27
+ children: [
28
+ /* @__PURE__ */ jsx(ActionIcon, {
29
+ className: styles.viewerClose,
30
+ icon: X,
31
+ title: t("image.close"),
32
+ onClick: onClose
33
+ }),
34
+ total > 1 && /* @__PURE__ */ jsx(GalleryNav, {
35
+ current,
36
+ hasNext,
37
+ hasPrev,
38
+ next,
39
+ prev,
40
+ total
41
+ }),
42
+ /* @__PURE__ */ jsx(Toolbar, {
43
+ ...toolbarProps,
44
+ onMoreOpenChange: idle.setHeld
45
+ })
46
+ ]
47
+ })
48
+ });
49
+ });
50
+ ViewerChrome.displayName = "ViewerChrome";
51
+ //#endregion
52
+ export { ViewerChrome as default };
53
+
54
+ //# sourceMappingURL=ViewerChrome.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ViewerChrome.mjs","names":["imageMessages"],"sources":["../../../src/Image/viewer/ViewerChrome.tsx"],"sourcesContent":["'use client';\n\nimport { X } from 'lucide-react';\nimport type { MotionValue } from 'motion/react';\nimport { memo, type MouseEvent, type ReactNode, useCallback } from 'react';\n\nimport ActionIcon from '@/ActionIcon';\nimport imageMessages from '@/i18n/resources/en/image';\nimport { useTranslation } from '@/i18n/useTranslation';\n\nimport { styles } from '../style';\nimport GalleryNav from './GalleryNav';\nimport type { Rect, Rotation, Size } from './geometry';\nimport Toolbar from './Toolbar';\nimport { useChromeIdle } from './useChromeIdle';\n\nexport interface ViewerChromeProps {\n canZoomIn: boolean;\n canZoomOut: boolean;\n chromeRef: (node: HTMLElement | null) => void;\n current: number;\n fitRect: Rect;\n flipHorizontal: () => void;\n flipVertical: () => void;\n hasNext: boolean;\n hasPrev: boolean;\n natural: Size;\n next: () => void;\n onClose: () => void;\n prev: () => void;\n reset: () => void;\n rotateLeft: () => void;\n rotateRight: () => void;\n rotation: Rotation;\n scale: MotionValue<number>;\n source: string;\n toolbarAddon?: ReactNode;\n total: number;\n zoomIn: () => void;\n zoomOut: () => void;\n}\n\nconst ViewerChrome = memo<ViewerChromeProps>(\n ({ chromeRef, current, hasNext, hasPrev, next, onClose, prev, total, ...toolbarProps }) => {\n const { t } = useTranslation(imageMessages);\n const idle = useChromeIdle();\n\n const handleChromeClick = useCallback((event: MouseEvent<HTMLDivElement>) => {\n event.stopPropagation();\n }, []);\n\n return (\n <div className={styles.viewerChrome} ref={chromeRef} onClick={handleChromeClick}>\n <div\n className={styles.viewerChromeIdle}\n data-idle-hidden={idle.hidden ? '' : undefined}\n ref={idle.ref}\n >\n <ActionIcon\n className={styles.viewerClose}\n icon={X}\n title={t('image.close')}\n onClick={onClose}\n />\n {total > 1 && (\n <GalleryNav\n current={current}\n hasNext={hasNext}\n hasPrev={hasPrev}\n next={next}\n prev={prev}\n total={total}\n />\n )}\n <Toolbar {...toolbarProps} onMoreOpenChange={idle.setHeld} />\n </div>\n </div>\n );\n },\n);\n\nViewerChrome.displayName = 'ViewerChrome';\n\nexport default ViewerChrome;\n"],"mappings":";;;;;;;;;;;;AA0CA,MAAM,eAAe,MAClB,EAAE,WAAW,SAAS,SAAS,SAAS,MAAM,SAAS,MAAM,OAAO,GAAG,mBAAmB;CACzF,MAAM,EAAE,MAAM,eAAeA,aAAa;CAC1C,MAAM,OAAO,cAAc;CAE3B,MAAM,oBAAoB,aAAa,UAAsC;EAC3E,MAAM,gBAAgB;CACxB,GAAG,CAAC,CAAC;CAEL,OACE,oBAAC,OAAD;EAAK,WAAW,OAAO;EAAc,KAAK;EAAW,SAAS;EAC5D,UAAA,qBAAC,OAAD;GACE,WAAW,OAAO;GAClB,oBAAkB,KAAK,SAAS,KAAK,KAAA;GACrC,KAAK,KAAK;GAHZ,UAAA;IAKE,oBAAC,YAAD;KACE,WAAW,OAAO;KAClB,MAAM;KACN,OAAO,EAAE,aAAa;KACtB,SAAS;IACV,CAAA;IACA,QAAQ,KACP,oBAAC,YAAD;KACW;KACA;KACA;KACH;KACA;KACC;IACR,CAAA;IAEH,oBAAC,SAAD;KAAS,GAAI;KAAc,kBAAkB,KAAK;IAAU,CAAA;GACzD;;CACF,CAAA;AAET,CACF;AAEA,aAAa,cAAc"}
@@ -0,0 +1,85 @@
1
+ const DEFAULT_RUBBER_BAND_FACTOR = .15;
2
+ const WHEEL_ZOOM_SENSITIVITY = .002;
3
+ const normalizeRotation = (degrees) => (degrees % 360 + 360) % 360;
4
+ const effectiveSize = (natural, rotate) => rotate === 90 || rotate === 270 ? {
5
+ height: natural.width,
6
+ width: natural.height
7
+ } : natural;
8
+ const computeFit = (natural, viewport, rotate, fillViewport = false) => {
9
+ const effective = effectiveSize(natural, rotate);
10
+ const availableWidth = Math.max(viewport.width - 48, 0);
11
+ const availableHeight = Math.max(viewport.height - 48, 0);
12
+ const containScale = Math.min(availableWidth / effective.width, availableHeight / effective.height, ...fillViewport ? [] : [1]);
13
+ const width = effective.width * containScale;
14
+ const height = effective.height * containScale;
15
+ return {
16
+ height,
17
+ width,
18
+ x: (viewport.width - width) / 2,
19
+ y: (viewport.height - height) / 2
20
+ };
21
+ };
22
+ const unrotatedRect = (fit, rotate) => {
23
+ if (rotate !== 90 && rotate !== 270) return fit;
24
+ const centerX = fit.x + fit.width / 2;
25
+ const centerY = fit.y + fit.height / 2;
26
+ return {
27
+ height: fit.width,
28
+ width: fit.height,
29
+ x: centerX - fit.height / 2,
30
+ y: centerY - fit.width / 2
31
+ };
32
+ };
33
+ const anchoredZoom = (current, targetScale, anchor, fitRect) => {
34
+ const centerX = fitRect.x + fitRect.width / 2;
35
+ const centerY = fitRect.y + fitRect.height / 2;
36
+ const ratio = targetScale / current.scale;
37
+ return {
38
+ scale: targetScale,
39
+ x: anchor.x - centerX - ratio * (anchor.x - centerX - current.x),
40
+ y: anchor.y - centerY - ratio * (anchor.y - centerY - current.y)
41
+ };
42
+ };
43
+ const panBounds = (state, fitRect, viewport) => {
44
+ const centerX = fitRect.x + fitRect.width / 2;
45
+ const centerY = fitRect.y + fitRect.height / 2;
46
+ const scaledWidth = fitRect.width * state.scale;
47
+ const scaledHeight = fitRect.height * state.scale;
48
+ const axis = (center, scaledSize, viewportSize) => {
49
+ if (scaledSize - viewportSize <= 0) return {
50
+ max: 0,
51
+ min: 0
52
+ };
53
+ return {
54
+ max: scaledSize / 2 - center,
55
+ min: viewportSize - center - scaledSize / 2
56
+ };
57
+ };
58
+ return {
59
+ x: axis(centerX, scaledWidth, viewport.width),
60
+ y: axis(centerY, scaledHeight, viewport.height)
61
+ };
62
+ };
63
+ const clampPan = (state, fitRect, viewport) => {
64
+ const bounds = panBounds(state, fitRect, viewport);
65
+ return {
66
+ x: Math.min(Math.max(state.x, bounds.x.min), bounds.x.max),
67
+ y: Math.min(Math.max(state.y, bounds.y.min), bounds.y.max)
68
+ };
69
+ };
70
+ const rubberBand = (value, min, max, factor = DEFAULT_RUBBER_BAND_FACTOR) => {
71
+ if (value < min) return min - (min - value) * factor;
72
+ if (value > max) return max + (value - max) * factor;
73
+ return value;
74
+ };
75
+ const clampScale = (scale, maxScale = 8) => Math.min(Math.max(scale, 1), maxScale);
76
+ const wheelZoomFactor = (deltaY) => Math.exp(-deltaY * WHEEL_ZOOM_SENSITIVITY);
77
+ const naturalScale = (natural, fitRect, rotate) => effectiveSize(natural, rotate).width / fitRect.width;
78
+ const doubleClickTarget = (currentScale, natural, fitRect, rotate) => {
79
+ if (currentScale > 1) return 1;
80
+ return Math.max(2, naturalScale(natural, fitRect, rotate));
81
+ };
82
+ //#endregion
83
+ export { DEFAULT_RUBBER_BAND_FACTOR, WHEEL_ZOOM_SENSITIVITY, anchoredZoom, clampPan, clampScale, computeFit, doubleClickTarget, naturalScale, normalizeRotation, panBounds, rubberBand, unrotatedRect, wheelZoomFactor };
84
+
85
+ //# sourceMappingURL=geometry.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geometry.mjs","names":[],"sources":["../../../src/Image/viewer/geometry.ts"],"sourcesContent":["export interface Size {\n height: number;\n width: number;\n}\n\nexport interface Point {\n x: number;\n y: number;\n}\n\nexport interface Rect {\n height: number;\n width: number;\n x: number;\n y: number;\n}\n\nexport type Rotation = 0 | 90 | 180 | 270;\n\nexport interface TransformState {\n scale: number;\n x: number;\n y: number;\n}\n\nexport interface AxisBounds {\n max: number;\n min: number;\n}\n\nexport const VIEWPORT_MARGIN = 24;\nexport const MIN_SCALE = 1;\nexport const DEFAULT_MAX_SCALE = 8;\nexport const DEFAULT_RUBBER_BAND_FACTOR = 0.15;\nexport const WHEEL_ZOOM_SENSITIVITY = 0.002;\n\nexport const normalizeRotation = (degrees: number): Rotation =>\n (((degrees % 360) + 360) % 360) as Rotation;\n\nconst effectiveSize = (natural: Size, rotate: Rotation): Size =>\n rotate === 90 || rotate === 270 ? { height: natural.width, width: natural.height } : natural;\n\nexport const computeFit = (\n natural: Size,\n viewport: Size,\n rotate: Rotation,\n fillViewport = false,\n): Rect => {\n const effective = effectiveSize(natural, rotate);\n const availableWidth = Math.max(viewport.width - VIEWPORT_MARGIN * 2, 0);\n const availableHeight = Math.max(viewport.height - VIEWPORT_MARGIN * 2, 0);\n // fillViewport drops the no-upscale cap: a dual-source thumbnail is a\n // stand-in for the hi-res image, so it must open at the viewport fit the\n // hi-res will land in — otherwise the swap re-fits mid-open.\n const containScale = Math.min(\n availableWidth / effective.width,\n availableHeight / effective.height,\n ...(fillViewport ? [] : [1]),\n );\n const width = effective.width * containScale;\n const height = effective.height * containScale;\n\n return {\n height,\n width,\n x: (viewport.width - width) / 2,\n y: (viewport.height - height) / 2,\n };\n};\n\n// computeFit returns the box the image occupies *after* the CSS rotation, but\n// the element it is applied to is sized before that rotation runs. For quarter\n// turns the two disagree, so the element box has to be the transpose about the\n// same center — otherwise `object-fit: contain` letterboxes the image down to\n// the short side and everything downstream (pan bounds, natural scale) is\n// measuring a box the viewer never actually paints.\nexport const unrotatedRect = (fit: Rect, rotate: Rotation): Rect => {\n if (rotate !== 90 && rotate !== 270) return fit;\n\n const centerX = fit.x + fit.width / 2;\n const centerY = fit.y + fit.height / 2;\n\n return {\n height: fit.width,\n width: fit.height,\n x: centerX - fit.height / 2,\n y: centerY - fit.width / 2,\n };\n};\n\nexport const anchoredZoom = (\n current: TransformState,\n targetScale: number,\n anchor: Point,\n fitRect: Rect,\n): TransformState => {\n const centerX = fitRect.x + fitRect.width / 2;\n const centerY = fitRect.y + fitRect.height / 2;\n const ratio = targetScale / current.scale;\n\n return {\n scale: targetScale,\n x: anchor.x - centerX - ratio * (anchor.x - centerX - current.x),\n y: anchor.y - centerY - ratio * (anchor.y - centerY - current.y),\n };\n};\n\nexport const panBounds = (\n state: TransformState,\n fitRect: Rect,\n viewport: Size,\n): { x: AxisBounds; y: AxisBounds } => {\n const centerX = fitRect.x + fitRect.width / 2;\n const centerY = fitRect.y + fitRect.height / 2;\n const scaledWidth = fitRect.width * state.scale;\n const scaledHeight = fitRect.height * state.scale;\n\n const axis = (center: number, scaledSize: number, viewportSize: number): AxisBounds => {\n const overflow = scaledSize - viewportSize;\n if (overflow <= 0) return { max: 0, min: 0 };\n return { max: scaledSize / 2 - center, min: viewportSize - center - scaledSize / 2 };\n };\n\n return {\n x: axis(centerX, scaledWidth, viewport.width),\n y: axis(centerY, scaledHeight, viewport.height),\n };\n};\n\nexport const clampPan = (state: TransformState, fitRect: Rect, viewport: Size): Point => {\n const bounds = panBounds(state, fitRect, viewport);\n return {\n x: Math.min(Math.max(state.x, bounds.x.min), bounds.x.max),\n y: Math.min(Math.max(state.y, bounds.y.min), bounds.y.max),\n };\n};\n\nexport const rubberBand = (\n value: number,\n min: number,\n max: number,\n factor: number = DEFAULT_RUBBER_BAND_FACTOR,\n): number => {\n if (value < min) return min - (min - value) * factor;\n if (value > max) return max + (value - max) * factor;\n return value;\n};\n\nexport const clampScale = (scale: number, maxScale: number = DEFAULT_MAX_SCALE): number =>\n Math.min(Math.max(scale, MIN_SCALE), maxScale);\n\nexport const wheelZoomFactor = (deltaY: number): number =>\n Math.exp(-deltaY * WHEEL_ZOOM_SENSITIVITY);\n\nexport const naturalScale = (natural: Size, fitRect: Rect, rotate: Rotation): number =>\n effectiveSize(natural, rotate).width / fitRect.width;\n\nexport const doubleClickTarget = (\n currentScale: number,\n natural: Size,\n fitRect: Rect,\n rotate: Rotation,\n): number => {\n if (currentScale > 1) return MIN_SCALE;\n return Math.max(2, naturalScale(natural, fitRect, rotate));\n};\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import "../type.mjs";
2
+ import "react";
@@ -0,0 +1,52 @@
1
+ import { useCallback, useSyncExternalStore } from "react";
2
+ //#region src/Image/viewer/registry.ts
3
+ let state = null;
4
+ let nextToken = 0;
5
+ const listeners = /* @__PURE__ */ new Set();
6
+ const emit = () => {
7
+ for (const listener of listeners) listener();
8
+ };
9
+ const flushClosed = () => {
10
+ if (!state || state.closing) return;
11
+ state.closing = true;
12
+ state.onOpenChange?.(false);
13
+ };
14
+ const openPreview = (entry, entries, index = 0, openerFocusElement = null) => {
15
+ flushClosed();
16
+ nextToken += 1;
17
+ state = {
18
+ closing: false,
19
+ entries: entries && entries.length > 0 ? entries : [entry],
20
+ index: entries && entries.length > 0 ? Math.min(Math.max(index, 0), entries.length - 1) : 0,
21
+ onOpenChange: entry.options.onOpenChange,
22
+ openerFocusElement,
23
+ token: nextToken
24
+ };
25
+ emit();
26
+ entry.options.onOpenChange?.(true);
27
+ };
28
+ const beginClosePreview = (token) => {
29
+ if (state?.token !== token) return;
30
+ flushClosed();
31
+ };
32
+ const endClosePreview = (token) => {
33
+ if (state?.token !== token) return;
34
+ flushClosed();
35
+ state = null;
36
+ emit();
37
+ };
38
+ const subscribe = (listener) => {
39
+ listeners.add(listener);
40
+ return () => {
41
+ listeners.delete(listener);
42
+ };
43
+ };
44
+ const getServerSnapshot = () => null;
45
+ const usePreviewSession = (elementRef) => {
46
+ const getSnapshot = useCallback(() => state && state.entries[state.index]?.element === elementRef.current ? state : null, [elementRef]);
47
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
48
+ };
49
+ //#endregion
50
+ export { beginClosePreview, endClosePreview, openPreview, usePreviewSession };
51
+
52
+ //# sourceMappingURL=registry.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.mjs","names":[],"sources":["../../../src/Image/viewer/registry.ts"],"sourcesContent":["import { type RefObject, useCallback, useSyncExternalStore } from 'react';\n\nimport type { ImagePreviewOptions } from '../type';\n\nexport interface ResolvedPreviewOptions extends Omit<ImagePreviewOptions, 'maxScale'> {\n maxScale: number;\n}\n\nexport interface PreviewEntry {\n element: HTMLImageElement;\n options: ResolvedPreviewOptions;\n previewSrc?: string;\n src: string;\n}\n\nexport interface PreviewSession {\n entries: PreviewEntry[];\n index: number;\n openerFocusElement: HTMLElement | null;\n token: number;\n}\n\ninterface HostState extends PreviewSession {\n closing: boolean;\n onOpenChange?: (open: boolean) => void;\n}\n\nlet state: HostState | null = null;\nlet nextToken = 0;\n\nconst listeners = new Set<() => void>();\n\nconst emit = () => {\n for (const listener of listeners) listener();\n};\n\nconst flushClosed = () => {\n if (!state || state.closing) return;\n state.closing = true;\n state.onOpenChange?.(false);\n};\n\n// openerFocusElement must be captured by the caller at pointerdown, not read here:\n// clicking a non-focusable thumbnail already blurs to document.body before its\n// click handler (and therefore this call) runs.\nexport const openPreview = (\n entry: PreviewEntry,\n entries?: PreviewEntry[],\n index = 0,\n openerFocusElement: HTMLElement | null = null,\n): void => {\n flushClosed();\n nextToken += 1;\n const resolvedEntries = entries && entries.length > 0 ? entries : [entry];\n const resolvedIndex =\n entries && entries.length > 0 ? Math.min(Math.max(index, 0), entries.length - 1) : 0;\n state = {\n closing: false,\n entries: resolvedEntries,\n index: resolvedIndex,\n onOpenChange: entry.options.onOpenChange,\n openerFocusElement,\n token: nextToken,\n };\n emit();\n entry.options.onOpenChange?.(true);\n};\n\nexport const beginClosePreview = (token: number): void => {\n if (state?.token !== token) return;\n flushClosed();\n};\n\nexport const endClosePreview = (token: number): void => {\n if (state?.token !== token) return;\n flushClosed();\n state = null;\n emit();\n};\n\nconst subscribe = (listener: () => void) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n};\n\nconst getServerSnapshot = (): PreviewSession | null => null;\n\nexport const usePreviewSession = (\n elementRef: RefObject<HTMLImageElement | null>,\n): PreviewSession | null => {\n const getSnapshot = useCallback(\n () => (state && state.entries[state.index]?.element === elementRef.current ? state : null),\n [elementRef],\n );\n\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n};\n"],"mappings":";;AA2BA,IAAI,QAA0B;AAC9B,IAAI,YAAY;AAEhB,MAAM,4BAAY,IAAI,IAAgB;AAEtC,MAAM,aAAa;CACjB,KAAK,MAAM,YAAY,WAAW,SAAS;AAC7C;AAEA,MAAM,oBAAoB;CACxB,IAAI,CAAC,SAAS,MAAM,SAAS;CAC7B,MAAM,UAAU;CAChB,MAAM,eAAe,KAAK;AAC5B;AAKA,MAAa,eACX,OACA,SACA,QAAQ,GACR,qBAAyC,SAChC;CACT,YAAY;CACZ,aAAa;CAIb,QAAQ;EACN,SAAS;EACT,SALsB,WAAW,QAAQ,SAAS,IAAI,UAAU,CAAC,KAAK;EAMtE,OAJA,WAAW,QAAQ,SAAS,IAAI,KAAK,IAAI,KAAK,IAAI,OAAO,CAAC,GAAG,QAAQ,SAAS,CAAC,IAAI;EAKnF,cAAc,MAAM,QAAQ;EAC5B;EACA,OAAO;CACT;CACA,KAAK;CACL,MAAM,QAAQ,eAAe,IAAI;AACnC;AAEA,MAAa,qBAAqB,UAAwB;CACxD,IAAI,OAAO,UAAU,OAAO;CAC5B,YAAY;AACd;AAEA,MAAa,mBAAmB,UAAwB;CACtD,IAAI,OAAO,UAAU,OAAO;CAC5B,YAAY;CACZ,QAAQ;CACR,KAAK;AACP;AAEA,MAAM,aAAa,aAAyB;CAC1C,UAAU,IAAI,QAAQ;CACtB,aAAa;EACX,UAAU,OAAO,QAAQ;CAC3B;AACF;AAEA,MAAM,0BAAiD;AAEvD,MAAa,qBACX,eAC0B;CAC1B,MAAM,cAAc,kBACX,SAAS,MAAM,QAAQ,MAAM,MAAM,EAAE,YAAY,WAAW,UAAU,QAAQ,MACrF,CAAC,UAAU,CACb;CAEA,OAAO,qBAAqB,WAAW,aAAa,iBAAiB;AACvE"}
@@ -0,0 +1,72 @@
1
+ "use client";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ //#region src/Image/viewer/useChromeIdle.ts
4
+ const IDLE_HIDE_MS = 2e3;
5
+ const useChromeIdle = () => {
6
+ const [hidden, setHidden] = useState(false);
7
+ const nodeRef = useRef(null);
8
+ const overChromeRef = useRef(false);
9
+ const heldRef = useRef(false);
10
+ const timerRef = useRef(null);
11
+ const clearTimer = useCallback(() => {
12
+ if (timerRef.current === null) return;
13
+ window.clearTimeout(timerRef.current);
14
+ timerRef.current = null;
15
+ }, []);
16
+ const arm = useCallback(() => {
17
+ clearTimer();
18
+ timerRef.current = window.setTimeout(function fire() {
19
+ timerRef.current = null;
20
+ const node = nodeRef.current;
21
+ const active = document.activeElement;
22
+ const focusWithin = Boolean(node && active && node.contains(active));
23
+ if (overChromeRef.current || heldRef.current || focusWithin) {
24
+ timerRef.current = window.setTimeout(fire, IDLE_HIDE_MS);
25
+ return;
26
+ }
27
+ setHidden(true);
28
+ }, IDLE_HIDE_MS);
29
+ }, [clearTimer]);
30
+ const wake = useCallback(() => {
31
+ setHidden(false);
32
+ arm();
33
+ }, [arm]);
34
+ useEffect(() => {
35
+ const handlePointerMove = (event) => {
36
+ overChromeRef.current = Boolean(event.target instanceof Node && nodeRef.current?.contains(event.target));
37
+ wake();
38
+ };
39
+ const handleActivity = () => wake();
40
+ document.addEventListener("pointermove", handlePointerMove);
41
+ document.addEventListener("pointerdown", handleActivity);
42
+ document.addEventListener("wheel", handleActivity, { passive: true });
43
+ document.addEventListener("keydown", handleActivity);
44
+ arm();
45
+ return () => {
46
+ document.removeEventListener("pointermove", handlePointerMove);
47
+ document.removeEventListener("pointerdown", handleActivity);
48
+ document.removeEventListener("wheel", handleActivity);
49
+ document.removeEventListener("keydown", handleActivity);
50
+ clearTimer();
51
+ };
52
+ }, [
53
+ arm,
54
+ clearTimer,
55
+ wake
56
+ ]);
57
+ const setHeld = useCallback((held) => {
58
+ heldRef.current = held;
59
+ if (held) wake();
60
+ }, [wake]);
61
+ return {
62
+ hidden,
63
+ ref: useCallback((node) => {
64
+ nodeRef.current = node;
65
+ }, []),
66
+ setHeld
67
+ };
68
+ };
69
+ //#endregion
70
+ export { useChromeIdle };
71
+
72
+ //# sourceMappingURL=useChromeIdle.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useChromeIdle.mjs","names":[],"sources":["../../../src/Image/viewer/useChromeIdle.ts"],"sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nconst IDLE_HIDE_MS = 2000;\n\nexport interface UseChromeIdleResult {\n hidden: boolean;\n ref: (node: HTMLDivElement | null) => void;\n setHeld: (held: boolean) => void;\n}\n\nexport const useChromeIdle = (): UseChromeIdleResult => {\n const [hidden, setHidden] = useState(false);\n const nodeRef = useRef<HTMLDivElement | null>(null);\n const overChromeRef = useRef(false);\n const heldRef = useRef(false);\n const timerRef = useRef<number | null>(null);\n\n const clearTimer = useCallback(() => {\n if (timerRef.current === null) return;\n window.clearTimeout(timerRef.current);\n timerRef.current = null;\n }, []);\n\n const arm = useCallback(() => {\n clearTimer();\n timerRef.current = window.setTimeout(function fire() {\n timerRef.current = null;\n const node = nodeRef.current;\n const active = document.activeElement;\n const focusWithin = Boolean(node && active && node.contains(active));\n if (overChromeRef.current || heldRef.current || focusWithin) {\n timerRef.current = window.setTimeout(fire, IDLE_HIDE_MS);\n return;\n }\n setHidden(true);\n }, IDLE_HIDE_MS);\n }, [clearTimer]);\n\n const wake = useCallback(() => {\n setHidden(false);\n arm();\n }, [arm]);\n\n useEffect(() => {\n const handlePointerMove = (event: PointerEvent) => {\n overChromeRef.current = Boolean(\n event.target instanceof Node && nodeRef.current?.contains(event.target),\n );\n wake();\n };\n const handleActivity = () => wake();\n\n document.addEventListener('pointermove', handlePointerMove);\n document.addEventListener('pointerdown', handleActivity);\n document.addEventListener('wheel', handleActivity, { passive: true });\n document.addEventListener('keydown', handleActivity);\n arm();\n\n return () => {\n document.removeEventListener('pointermove', handlePointerMove);\n document.removeEventListener('pointerdown', handleActivity);\n document.removeEventListener('wheel', handleActivity);\n document.removeEventListener('keydown', handleActivity);\n clearTimer();\n };\n }, [arm, clearTimer, wake]);\n\n const setHeld = useCallback(\n (held: boolean) => {\n heldRef.current = held;\n if (held) wake();\n },\n [wake],\n );\n\n const ref = useCallback((node: HTMLDivElement | null) => {\n nodeRef.current = node;\n }, []);\n\n return { hidden, ref, setHeld };\n};\n"],"mappings":";;;AAIA,MAAM,eAAe;AAQrB,MAAa,sBAA2C;CACtD,MAAM,CAAC,QAAQ,aAAa,SAAS,KAAK;CAC1C,MAAM,UAAU,OAA8B,IAAI;CAClD,MAAM,gBAAgB,OAAO,KAAK;CAClC,MAAM,UAAU,OAAO,KAAK;CAC5B,MAAM,WAAW,OAAsB,IAAI;CAE3C,MAAM,aAAa,kBAAkB;EACnC,IAAI,SAAS,YAAY,MAAM;EAC/B,OAAO,aAAa,SAAS,OAAO;EACpC,SAAS,UAAU;CACrB,GAAG,CAAC,CAAC;CAEL,MAAM,MAAM,kBAAkB;EAC5B,WAAW;EACX,SAAS,UAAU,OAAO,WAAW,SAAS,OAAO;GACnD,SAAS,UAAU;GACnB,MAAM,OAAO,QAAQ;GACrB,MAAM,SAAS,SAAS;GACxB,MAAM,cAAc,QAAQ,QAAQ,UAAU,KAAK,SAAS,MAAM,CAAC;GACnE,IAAI,cAAc,WAAW,QAAQ,WAAW,aAAa;IAC3D,SAAS,UAAU,OAAO,WAAW,MAAM,YAAY;IACvD;GACF;GACA,UAAU,IAAI;EAChB,GAAG,YAAY;CACjB,GAAG,CAAC,UAAU,CAAC;CAEf,MAAM,OAAO,kBAAkB;EAC7B,UAAU,KAAK;EACf,IAAI;CACN,GAAG,CAAC,GAAG,CAAC;CAER,gBAAgB;EACd,MAAM,qBAAqB,UAAwB;GACjD,cAAc,UAAU,QACtB,MAAM,kBAAkB,QAAQ,QAAQ,SAAS,SAAS,MAAM,MAAM,CACxE;GACA,KAAK;EACP;EACA,MAAM,uBAAuB,KAAK;EAElC,SAAS,iBAAiB,eAAe,iBAAiB;EAC1D,SAAS,iBAAiB,eAAe,cAAc;EACvD,SAAS,iBAAiB,SAAS,gBAAgB,EAAE,SAAS,KAAK,CAAC;EACpE,SAAS,iBAAiB,WAAW,cAAc;EACnD,IAAI;EAEJ,aAAa;GACX,SAAS,oBAAoB,eAAe,iBAAiB;GAC7D,SAAS,oBAAoB,eAAe,cAAc;GAC1D,SAAS,oBAAoB,SAAS,cAAc;GACpD,SAAS,oBAAoB,WAAW,cAAc;GACtD,WAAW;EACb;CACF,GAAG;EAAC;EAAK;EAAY;CAAI,CAAC;CAE1B,MAAM,UAAU,aACb,SAAkB;EACjB,QAAQ,UAAU;EAClB,IAAI,MAAM,KAAK;CACjB,GACA,CAAC,IAAI,CACP;CAMA,OAAO;EAAE;EAAQ,KAJL,aAAa,SAAgC;GACvD,QAAQ,UAAU;EACpB,GAAG,CAAC,CAEe;EAAG;CAAQ;AAChC"}
@@ -0,0 +1,10 @@
1
+ import { useCallback } from "react";
2
+ //#region src/Image/viewer/useFinalFocus.ts
3
+ const useFinalFocus = (openerFocusElement) => useCallback(() => {
4
+ if (openerFocusElement?.isConnected) return openerFocusElement;
5
+ return false;
6
+ }, [openerFocusElement]);
7
+ //#endregion
8
+ export { useFinalFocus };
9
+
10
+ //# sourceMappingURL=useFinalFocus.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFinalFocus.mjs","names":[],"sources":["../../../src/Image/viewer/useFinalFocus.ts"],"sourcesContent":["import { useCallback } from 'react';\n\nexport const useFinalFocus = (openerFocusElement: HTMLElement | null) =>\n useCallback((): HTMLElement | false => {\n if (openerFocusElement?.isConnected) return openerFocusElement;\n return false;\n }, [openerFocusElement]);\n"],"mappings":";;AAEA,MAAa,iBAAiB,uBAC5B,kBAAuC;CACrC,IAAI,oBAAoB,aAAa,OAAO;CAC5C,OAAO;AACT,GAAG,CAAC,kBAAkB,CAAC"}
@@ -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