@lobehub/ui 5.24.0 → 5.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/es/A/index.mjs +2 -1
- package/es/A/index.mjs.map +1 -1
- package/es/Accordion/AccordionItem.mjs +0 -1
- package/es/Accordion/AccordionItem.mjs.map +1 -1
- package/es/ActionIcon/components/utils.mjs +7 -9
- package/es/ActionIcon/components/utils.mjs.map +1 -1
- package/es/Checkbox/CheckboxGroup.mjs +4 -2
- package/es/Checkbox/CheckboxGroup.mjs.map +1 -1
- package/es/CodeDiff/CodeDiff.mjs +20 -18
- package/es/CodeDiff/CodeDiff.mjs.map +1 -1
- package/es/CodeDiff/PatchDiff.mjs +18 -16
- package/es/CodeDiff/PatchDiff.mjs.map +1 -1
- package/es/CopyButton/CopyButton.mjs +2 -1
- package/es/CopyButton/CopyButton.mjs.map +1 -1
- package/es/Highlighter/LangSelect.mjs +41 -40
- package/es/Highlighter/LangSelect.mjs.map +1 -1
- package/es/Highlighter/SyntaxHighlighter/StaticRenderer.mjs +2 -1
- package/es/Highlighter/SyntaxHighlighter/StaticRenderer.mjs.map +1 -1
- package/es/HotkeyInput/HotkeyInput.mjs +3 -1
- package/es/HotkeyInput/HotkeyInput.mjs.map +1 -1
- package/es/HtmlPreview/const.mjs +1 -1
- package/es/HtmlPreview/const.mjs.map +1 -1
- package/es/Icon/components/utils.mjs +7 -9
- package/es/Icon/components/utils.mjs.map +1 -1
- package/es/Image/Image.mjs +138 -37
- package/es/Image/Image.mjs.map +1 -1
- package/es/Image/PreviewGroup.d.mts +1 -0
- package/es/Image/PreviewGroup.mjs +33 -10
- package/es/Image/PreviewGroup.mjs.map +1 -1
- package/es/Image/index.d.mts +2 -2
- package/es/Image/index.mjs +1 -1
- package/es/Image/style.mjs +126 -71
- package/es/Image/style.mjs.map +1 -1
- package/es/Image/type.d.mts +14 -17
- package/es/Image/viewer/GalleryNav.mjs +44 -0
- package/es/Image/viewer/GalleryNav.mjs.map +1 -0
- package/es/Image/viewer/ImageViewer.mjs +275 -0
- package/es/Image/viewer/ImageViewer.mjs.map +1 -0
- package/es/Image/viewer/PreviewOutlet.mjs +19 -0
- package/es/Image/viewer/PreviewOutlet.mjs.map +1 -0
- package/es/Image/viewer/Toolbar.mjs +184 -0
- package/es/Image/viewer/Toolbar.mjs.map +1 -0
- package/es/Image/viewer/ViewerChrome.mjs +54 -0
- package/es/Image/viewer/ViewerChrome.mjs.map +1 -0
- package/es/Image/viewer/geometry.mjs +85 -0
- package/es/Image/viewer/geometry.mjs.map +1 -0
- package/es/Image/viewer/registry.d.mts +2 -0
- package/es/Image/viewer/registry.mjs +52 -0
- package/es/Image/viewer/registry.mjs.map +1 -0
- package/es/Image/viewer/useChromeIdle.mjs +72 -0
- package/es/Image/viewer/useChromeIdle.mjs.map +1 -0
- package/es/Image/viewer/useFinalFocus.mjs +10 -0
- package/es/Image/viewer/useFinalFocus.mjs.map +1 -0
- package/es/Image/viewer/useFlipTransition.mjs +321 -0
- package/es/Image/viewer/useFlipTransition.mjs.map +1 -0
- package/es/Image/viewer/useGalleryNav.mjs +89 -0
- package/es/Image/viewer/useGalleryNav.mjs.map +1 -0
- package/es/Image/viewer/useRefitTransition.mjs +47 -0
- package/es/Image/viewer/useRefitTransition.mjs.map +1 -0
- package/es/Image/viewer/useViewerGestures.mjs +148 -0
- package/es/Image/viewer/useViewerGestures.mjs.map +1 -0
- package/es/Image/viewer/useZoomPan.mjs +346 -0
- package/es/Image/viewer/useZoomPan.mjs.map +1 -0
- package/es/Img/index.mjs +2 -1
- package/es/Img/index.mjs.map +1 -1
- package/es/Markdown/SyntaxMarkdown/StreamdownRender.mjs +2 -1
- package/es/Markdown/SyntaxMarkdown/StreamdownRender.mjs.map +1 -1
- package/es/Markdown/SyntaxMarkdown/useSmoothStreamContent.mjs +2 -2
- package/es/Markdown/SyntaxMarkdown/useSmoothStreamContent.mjs.map +1 -1
- package/es/Modal/RawModalStackItem.mjs +6 -5
- package/es/Modal/RawModalStackItem.mjs.map +1 -1
- package/es/Modal/imperative.mjs +4 -2
- package/es/Modal/imperative.mjs.map +1 -1
- package/es/NeuralNetworkLoading/NeuralNetworkLoading.mjs +1 -1
- package/es/NeuralNetworkLoading/NeuralNetworkLoading.mjs.map +1 -1
- package/es/ScrollShadow/ScrollShadow.mjs +13 -12
- package/es/ScrollShadow/ScrollShadow.mjs.map +1 -1
- package/es/Skeleton/SkeletonButton.mjs +2 -1
- package/es/Skeleton/SkeletonButton.mjs.map +1 -1
- package/es/SortableList/SortableList.mjs +5 -1
- package/es/SortableList/SortableList.mjs.map +1 -1
- package/es/Text/Text.mjs +22 -21
- package/es/Text/Text.mjs.map +1 -1
- package/es/ThemeProvider/ThemeProvider.mjs +3 -2
- package/es/ThemeProvider/ThemeProvider.mjs.map +1 -1
- package/es/awesome/GradientButton/GradientButton.mjs +1 -3
- package/es/awesome/GradientButton/GradientButton.mjs.map +1 -1
- package/es/awesome/Spotlight/Spotlight.mjs +2 -1
- package/es/awesome/Spotlight/Spotlight.mjs.map +1 -1
- package/es/base-ui/Button/Button.mjs +13 -11
- package/es/base-ui/Button/Button.mjs.map +1 -1
- package/es/base-ui/Button/SplitButton.mjs +2 -1
- package/es/base-ui/Button/SplitButton.mjs.map +1 -1
- package/es/base-ui/ContextMenu/ContextMenuTrigger.mjs +2 -1
- package/es/base-ui/ContextMenu/ContextMenuTrigger.mjs.map +1 -1
- package/es/base-ui/ContextMenu/renderItems.mjs +2 -1
- package/es/base-ui/ContextMenu/renderItems.mjs.map +1 -1
- package/es/base-ui/Drawer/Drawer.d.mts +6 -0
- package/es/base-ui/Drawer/Drawer.mjs +152 -0
- package/es/base-ui/Drawer/Drawer.mjs.map +1 -0
- package/es/base-ui/Drawer/DrawerLayerContext.mjs +10 -0
- package/es/base-ui/Drawer/DrawerLayerContext.mjs.map +1 -0
- package/es/base-ui/Drawer/DrawerPushContext.mjs +27 -0
- package/es/base-ui/Drawer/DrawerPushContext.mjs.map +1 -0
- package/es/base-ui/Drawer/atoms.d.mts +58 -0
- package/es/base-ui/Drawer/atoms.mjs +262 -0
- package/es/base-ui/Drawer/atoms.mjs.map +1 -0
- package/es/base-ui/Drawer/constants.d.mts +17 -0
- package/es/base-ui/Drawer/constants.mjs +72 -0
- package/es/base-ui/Drawer/constants.mjs.map +1 -0
- package/es/base-ui/Drawer/index.d.mts +5 -0
- package/es/base-ui/Drawer/style.mjs +295 -0
- package/es/base-ui/Drawer/style.mjs.map +1 -0
- package/es/base-ui/Drawer/type.d.mts +65 -0
- package/es/base-ui/DropdownMenu/DropdownMenu.mjs +2 -1
- package/es/base-ui/DropdownMenu/DropdownMenu.mjs.map +1 -1
- package/es/base-ui/DropdownMenu/atoms.mjs +2 -1
- package/es/base-ui/DropdownMenu/atoms.mjs.map +1 -1
- package/es/base-ui/DropdownMenu/renderItems.mjs +2 -1
- package/es/base-ui/DropdownMenu/renderItems.mjs.map +1 -1
- package/es/base-ui/FloatingSheet/FloatingSheet.mjs +4 -1
- package/es/base-ui/FloatingSheet/FloatingSheet.mjs.map +1 -1
- package/es/base-ui/Input/style.d.mts +1 -1
- package/es/base-ui/Modal/atoms.mjs +7 -5
- package/es/base-ui/Modal/atoms.mjs.map +1 -1
- package/es/base-ui/Modal/imperative.mjs +2 -1
- package/es/base-ui/Modal/imperative.mjs.map +1 -1
- package/es/base-ui/Popover/atoms.mjs +2 -1
- package/es/base-ui/Popover/atoms.mjs.map +1 -1
- package/es/base-ui/Popover/context.mjs +4 -2
- package/es/base-ui/Popover/context.mjs.map +1 -1
- package/es/base-ui/Select/atoms.mjs +2 -1
- package/es/base-ui/Select/atoms.mjs.map +1 -1
- package/es/base-ui/Toast/hostGuard.mjs +26 -0
- package/es/base-ui/Toast/hostGuard.mjs.map +1 -0
- package/es/base-ui/Toast/imperative.mjs +43 -20
- package/es/base-ui/Toast/imperative.mjs.map +1 -1
- package/es/base-ui/Toast/pendingQueue.mjs +30 -0
- package/es/base-ui/Toast/pendingQueue.mjs.map +1 -0
- package/es/base-ui/index.d.mts +6 -1
- package/es/base-ui/index.mjs +5 -2
- package/es/brand/LobeChat/index.mjs +0 -1
- package/es/brand/LobeChat/index.mjs.map +1 -1
- package/es/brand/LobeHub/index.mjs +0 -1
- package/es/brand/LobeHub/index.mjs.map +1 -1
- package/es/brand/Logo3d/index.mjs +2 -1
- package/es/brand/Logo3d/index.mjs.map +1 -1
- package/es/chat/ChatItem/components/Actions.mjs +5 -4
- package/es/chat/ChatItem/components/Actions.mjs.map +1 -1
- package/es/chat/ChatList/ChatList.mjs +2 -1
- package/es/chat/ChatList/ChatList.mjs.map +1 -1
- package/es/color/ColorScales/ScaleRow.mjs +0 -2
- package/es/color/ColorScales/ScaleRow.mjs.map +1 -1
- package/es/hooks/useMermaid.mjs +2 -1
- package/es/hooks/useMermaid.mjs.map +1 -1
- package/es/i18n/resources/en/image.d.mts +5 -0
- package/es/i18n/resources/en/image.mjs +6 -1
- package/es/i18n/resources/en/image.mjs.map +1 -1
- package/es/i18n/resources/zhCn/image.d.mts +5 -0
- package/es/i18n/resources/zhCn/image.mjs +6 -1
- package/es/i18n/resources/zhCn/image.mjs.map +1 -1
- package/es/index.mjs +8 -8
- package/package.json +1 -1
- package/es/Image/components/Preview.mjs +0 -23
- package/es/Image/components/Preview.mjs.map +0 -1
- package/es/Image/components/Toolbar.mjs +0 -128
- package/es/Image/components/Toolbar.mjs.map +0 -1
- package/es/Image/components/usePreview.mjs +0 -58
- package/es/Image/components/usePreview.mjs.map +0 -1
- package/es/Image/components/usePreviewGroup.mjs +0 -57
- package/es/Image/components/usePreviewGroup.mjs.map +0 -1
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import FlexBasic_default from "../../Flex/FlexBasic.mjs";
|
|
3
|
+
import Center from "../../Flex/Center.mjs";
|
|
4
|
+
import { Tooltip } from "../../base-ui/Tooltip/Tooltip.mjs";
|
|
5
|
+
import TooltipGroup from "../../base-ui/Tooltip/TooltipGroup.mjs";
|
|
6
|
+
import ActionIcon from "../../ActionIcon/ActionIcon.mjs";
|
|
7
|
+
import DropdownMenu from "../../base-ui/DropdownMenu/DropdownMenu.mjs";
|
|
8
|
+
import { useTranslation } from "../../i18n/useTranslation.mjs";
|
|
9
|
+
import { downloadBlob } from "../../utils/downloadBlob.mjs";
|
|
10
|
+
import { styles } from "../style.mjs";
|
|
11
|
+
import { toast } from "../../base-ui/Toast/imperative.mjs";
|
|
12
|
+
import { naturalScale } from "./geometry.mjs";
|
|
13
|
+
import image_default from "../../i18n/resources/en/image.mjs";
|
|
14
|
+
import { getClipboardBlob } from "../../utils/blobToPng.mjs";
|
|
15
|
+
import { memo, useCallback, useMemo, useState, useSyncExternalStore } from "react";
|
|
16
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
17
|
+
import { Copy, Download, Ellipsis, FlipHorizontal, FlipVertical, RotateCcw, RotateCw, ZoomIn, ZoomOut } from "lucide-react";
|
|
18
|
+
//#region src/Image/viewer/Toolbar.tsx
|
|
19
|
+
const getFileNameFromUrl = (url) => {
|
|
20
|
+
try {
|
|
21
|
+
const match = new URL(url).pathname.match(/\/([^/]+)$/);
|
|
22
|
+
return match ? decodeURIComponent(match[1]) : "image";
|
|
23
|
+
} catch {
|
|
24
|
+
return "image";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const getExtensionFromMimeType = (mimeType) => {
|
|
28
|
+
return {
|
|
29
|
+
"image/svg+xml": "svg",
|
|
30
|
+
"image/png": "png",
|
|
31
|
+
"image/jpeg": "jpg",
|
|
32
|
+
"image/jpg": "jpg",
|
|
33
|
+
"image/webp": "webp",
|
|
34
|
+
"image/gif": "gif"
|
|
35
|
+
}[mimeType?.toLowerCase()] || mimeType?.split("/")[1]?.split("+")[0] || "png";
|
|
36
|
+
};
|
|
37
|
+
const usePercentage = (scale, matchScale) => {
|
|
38
|
+
const subscribe = useCallback((onChange) => scale.on("change", onChange), [scale]);
|
|
39
|
+
const getSnapshot = useCallback(() => Math.round(scale.get() / matchScale * 100), [matchScale, scale]);
|
|
40
|
+
return useSyncExternalStore(subscribe, getSnapshot);
|
|
41
|
+
};
|
|
42
|
+
const Toolbar = memo(({ canZoomIn, canZoomOut, fitRect, flipHorizontal, flipVertical, natural, onMoreOpenChange, reset, rotateLeft, rotateRight, rotation, scale, source, toolbarAddon, zoomIn, zoomOut }) => {
|
|
43
|
+
const { t } = useTranslation(image_default);
|
|
44
|
+
const [containerEl, setContainerEl] = useState(null);
|
|
45
|
+
const [copyLoading, setCopyLoading] = useState(false);
|
|
46
|
+
const [downloadLoading, setDownloadLoading] = useState(false);
|
|
47
|
+
const matchScale = naturalScale(natural, fitRect, rotation);
|
|
48
|
+
const percentage = usePercentage(scale, matchScale);
|
|
49
|
+
const handleDownload = useCallback(async () => {
|
|
50
|
+
setDownloadLoading(true);
|
|
51
|
+
try {
|
|
52
|
+
const blob = await (await fetch(source, { mode: "cors" })).blob();
|
|
53
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
54
|
+
let fileName = getFileNameFromUrl(source);
|
|
55
|
+
const ext = getExtensionFromMimeType(blob.type);
|
|
56
|
+
if (!fileName.includes(".")) fileName = `${fileName}.${ext}`;
|
|
57
|
+
else if (fileName.endsWith(".svg+xml")) fileName = fileName.replace(/\.svg\+xml$/i, ".svg");
|
|
58
|
+
await downloadBlob(blobUrl, fileName);
|
|
59
|
+
URL.revokeObjectURL(blobUrl);
|
|
60
|
+
toast.success(t("image.downloadSuccess"));
|
|
61
|
+
} catch {
|
|
62
|
+
toast.error(t("image.downloadFailed"));
|
|
63
|
+
} finally {
|
|
64
|
+
setDownloadLoading(false);
|
|
65
|
+
}
|
|
66
|
+
}, [source, t]);
|
|
67
|
+
const handleCopy = useCallback(async () => {
|
|
68
|
+
setCopyLoading(true);
|
|
69
|
+
try {
|
|
70
|
+
const blob = await (await fetch(source, { mode: "cors" })).blob();
|
|
71
|
+
const clipboardBlob = await getClipboardBlob(blob);
|
|
72
|
+
await navigator.clipboard.write([new ClipboardItem(clipboardBlob)]);
|
|
73
|
+
toast.success(t("image.copySuccess"));
|
|
74
|
+
} catch {
|
|
75
|
+
toast.error(t("image.copyFailed"));
|
|
76
|
+
} finally {
|
|
77
|
+
setCopyLoading(false);
|
|
78
|
+
}
|
|
79
|
+
}, [source, t]);
|
|
80
|
+
const moreItems = useMemo(() => [
|
|
81
|
+
{
|
|
82
|
+
icon: FlipHorizontal,
|
|
83
|
+
key: "flip-horizontal",
|
|
84
|
+
label: t("image.flipHorizontal"),
|
|
85
|
+
onClick: flipHorizontal
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
icon: FlipVertical,
|
|
89
|
+
key: "flip-vertical",
|
|
90
|
+
label: t("image.flipVertical"),
|
|
91
|
+
onClick: flipVertical
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
icon: RotateCcw,
|
|
95
|
+
key: "rotate-left",
|
|
96
|
+
label: t("image.rotateLeft"),
|
|
97
|
+
onClick: rotateLeft
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
icon: RotateCw,
|
|
101
|
+
key: "rotate-right",
|
|
102
|
+
label: t("image.rotateRight"),
|
|
103
|
+
onClick: rotateRight
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
icon: Copy,
|
|
107
|
+
key: "copy",
|
|
108
|
+
label: t("image.copy"),
|
|
109
|
+
onClick: handleCopy
|
|
110
|
+
}
|
|
111
|
+
], [
|
|
112
|
+
flipHorizontal,
|
|
113
|
+
flipVertical,
|
|
114
|
+
handleCopy,
|
|
115
|
+
rotateLeft,
|
|
116
|
+
rotateRight,
|
|
117
|
+
t
|
|
118
|
+
]);
|
|
119
|
+
return /* @__PURE__ */ jsx(TooltipGroup, {
|
|
120
|
+
popupContainer: containerEl ?? void 0,
|
|
121
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
122
|
+
className: styles.toolbar,
|
|
123
|
+
ref: setContainerEl,
|
|
124
|
+
children: /* @__PURE__ */ jsxs(FlexBasic_default, {
|
|
125
|
+
horizontal: true,
|
|
126
|
+
align: "center",
|
|
127
|
+
className: styles.toolbarRow,
|
|
128
|
+
gap: 8,
|
|
129
|
+
children: [
|
|
130
|
+
/* @__PURE__ */ jsx(ActionIcon, {
|
|
131
|
+
disabled: !canZoomOut,
|
|
132
|
+
icon: ZoomOut,
|
|
133
|
+
style: { borderRadius: 999 },
|
|
134
|
+
title: t("image.zoomOut"),
|
|
135
|
+
onClick: zoomOut
|
|
136
|
+
}),
|
|
137
|
+
/* @__PURE__ */ jsx(Tooltip, {
|
|
138
|
+
title: t("image.zoomReset"),
|
|
139
|
+
children: /* @__PURE__ */ jsxs(Center, {
|
|
140
|
+
horizontal: true,
|
|
141
|
+
className: styles.toolbarPercentage,
|
|
142
|
+
role: "button",
|
|
143
|
+
tabIndex: 0,
|
|
144
|
+
onClick: reset,
|
|
145
|
+
children: [percentage, "%"]
|
|
146
|
+
})
|
|
147
|
+
}),
|
|
148
|
+
/* @__PURE__ */ jsx(ActionIcon, {
|
|
149
|
+
disabled: !canZoomIn,
|
|
150
|
+
icon: ZoomIn,
|
|
151
|
+
style: { borderRadius: 999 },
|
|
152
|
+
title: t("image.zoomIn"),
|
|
153
|
+
onClick: zoomIn
|
|
154
|
+
}),
|
|
155
|
+
/* @__PURE__ */ jsx(ActionIcon, {
|
|
156
|
+
icon: Download,
|
|
157
|
+
loading: downloadLoading,
|
|
158
|
+
style: { borderRadius: 999 },
|
|
159
|
+
title: t("image.download"),
|
|
160
|
+
onClick: handleDownload
|
|
161
|
+
}),
|
|
162
|
+
/* @__PURE__ */ jsx(DropdownMenu, {
|
|
163
|
+
items: moreItems,
|
|
164
|
+
placement: "top",
|
|
165
|
+
portalProps: { container: containerEl ?? void 0 },
|
|
166
|
+
onOpenChange: (open) => onMoreOpenChange?.(open),
|
|
167
|
+
children: /* @__PURE__ */ jsx(ActionIcon, {
|
|
168
|
+
icon: Ellipsis,
|
|
169
|
+
loading: copyLoading,
|
|
170
|
+
style: { borderRadius: 999 },
|
|
171
|
+
title: t("image.more")
|
|
172
|
+
})
|
|
173
|
+
}),
|
|
174
|
+
toolbarAddon
|
|
175
|
+
]
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
Toolbar.displayName = "Toolbar";
|
|
181
|
+
//#endregion
|
|
182
|
+
export { Toolbar as default };
|
|
183
|
+
|
|
184
|
+
//# sourceMappingURL=Toolbar.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Toolbar.mjs","names":["imageMessages","Flexbox"],"sources":["../../../src/Image/viewer/Toolbar.tsx"],"sourcesContent":["'use client';\n\nimport {\n Copy,\n Download,\n Ellipsis,\n FlipHorizontal,\n FlipVertical,\n RotateCcw,\n RotateCw,\n ZoomIn,\n ZoomOut,\n} from 'lucide-react';\nimport type { MotionValue } from 'motion/react';\nimport { memo, type ReactNode, useCallback, useMemo, useState, useSyncExternalStore } from 'react';\n\nimport ActionIcon from '@/ActionIcon';\nimport DropdownMenu, { type DropdownItem } from '@/base-ui/DropdownMenu';\nimport { toast } from '@/base-ui/Toast';\nimport { Center, Flexbox } from '@/Flex';\nimport imageMessages from '@/i18n/resources/en/image';\nimport { useTranslation } from '@/i18n/useTranslation';\nimport Tooltip, { TooltipGroup } from '@/Tooltip';\nimport { getClipboardBlob } from '@/utils/blobToPng';\nimport { downloadBlob } from '@/utils/downloadBlob';\n\nimport { styles } from '../style';\nimport { naturalScale, type Rect, type Rotation, type Size } from './geometry';\n\nconst getFileNameFromUrl = (url: string): string => {\n try {\n const pathname = new URL(url).pathname;\n const match = pathname.match(/\\/([^/]+)$/);\n return match ? decodeURIComponent(match[1]) : 'image';\n } catch {\n return 'image';\n }\n};\n\nconst getExtensionFromMimeType = (mimeType: string): string => {\n const map: Record<string, string> = {\n 'image/svg+xml': 'svg',\n 'image/png': 'png',\n 'image/jpeg': 'jpg',\n 'image/jpg': 'jpg',\n 'image/webp': 'webp',\n 'image/gif': 'gif',\n };\n return map[mimeType?.toLowerCase()] || mimeType?.split('/')[1]?.split('+')[0] || 'png';\n};\n\nconst usePercentage = (scale: MotionValue<number>, matchScale: number): number => {\n const subscribe = useCallback((onChange: () => void) => scale.on('change', onChange), [scale]);\n const getSnapshot = useCallback(\n () => Math.round((scale.get() / matchScale) * 100),\n [matchScale, scale],\n );\n\n return useSyncExternalStore(subscribe, getSnapshot);\n};\n\nexport interface ToolbarProps {\n canZoomIn: boolean;\n canZoomOut: boolean;\n fitRect: Rect;\n flipHorizontal: () => void;\n flipVertical: () => void;\n natural: Size;\n onMoreOpenChange?: (open: boolean) => void;\n reset: () => void;\n rotateLeft: () => void;\n rotateRight: () => void;\n rotation: Rotation;\n scale: MotionValue<number>;\n source: string;\n toolbarAddon?: ReactNode;\n zoomIn: () => void;\n zoomOut: () => void;\n}\n\nconst Toolbar = memo<ToolbarProps>(\n ({\n canZoomIn,\n canZoomOut,\n fitRect,\n flipHorizontal,\n flipVertical,\n natural,\n onMoreOpenChange,\n reset,\n rotateLeft,\n rotateRight,\n rotation,\n scale,\n source,\n toolbarAddon,\n zoomIn,\n zoomOut,\n }) => {\n const { t } = useTranslation(imageMessages);\n const [containerEl, setContainerEl] = useState<HTMLElement | null>(null);\n const [copyLoading, setCopyLoading] = useState(false);\n const [downloadLoading, setDownloadLoading] = useState(false);\n\n const matchScale = naturalScale(natural, fitRect, rotation);\n const percentage = usePercentage(scale, matchScale);\n\n const handleDownload = useCallback(async () => {\n setDownloadLoading(true);\n try {\n const response = await fetch(source, { mode: 'cors' });\n const blob = await response.blob();\n const blobUrl = URL.createObjectURL(blob);\n let fileName = getFileNameFromUrl(source);\n const ext = getExtensionFromMimeType(blob.type);\n if (!fileName.includes('.')) {\n fileName = `${fileName}.${ext}`;\n } else if (fileName.endsWith('.svg+xml')) {\n fileName = fileName.replace(/\\.svg\\+xml$/i, '.svg');\n }\n await downloadBlob(blobUrl, fileName);\n URL.revokeObjectURL(blobUrl);\n toast.success(t('image.downloadSuccess'));\n } catch {\n toast.error(t('image.downloadFailed'));\n } finally {\n setDownloadLoading(false);\n }\n }, [source, t]);\n\n const handleCopy = useCallback(async () => {\n setCopyLoading(true);\n try {\n const response = await fetch(source, { mode: 'cors' });\n const blob = await response.blob();\n const clipboardBlob = await getClipboardBlob(blob);\n await navigator.clipboard.write([new ClipboardItem(clipboardBlob)]);\n toast.success(t('image.copySuccess'));\n } catch {\n toast.error(t('image.copyFailed'));\n } finally {\n setCopyLoading(false);\n }\n }, [source, t]);\n\n const moreItems = useMemo<DropdownItem[]>(\n () => [\n {\n icon: FlipHorizontal,\n key: 'flip-horizontal',\n label: t('image.flipHorizontal'),\n onClick: flipHorizontal,\n },\n {\n icon: FlipVertical,\n key: 'flip-vertical',\n label: t('image.flipVertical'),\n onClick: flipVertical,\n },\n { icon: RotateCcw, key: 'rotate-left', label: t('image.rotateLeft'), onClick: rotateLeft },\n {\n icon: RotateCw,\n key: 'rotate-right',\n label: t('image.rotateRight'),\n onClick: rotateRight,\n },\n { icon: Copy, key: 'copy', label: t('image.copy'), onClick: handleCopy },\n ],\n [flipHorizontal, flipVertical, handleCopy, rotateLeft, rotateRight, t],\n );\n\n return (\n <TooltipGroup popupContainer={containerEl ?? undefined}>\n <div className={styles.toolbar} ref={setContainerEl}>\n <Flexbox horizontal align=\"center\" className={styles.toolbarRow} gap={8}>\n <ActionIcon\n disabled={!canZoomOut}\n icon={ZoomOut}\n style={{ borderRadius: 999 }}\n title={t('image.zoomOut')}\n onClick={zoomOut}\n />\n <Tooltip title={t('image.zoomReset')}>\n <Center\n horizontal\n className={styles.toolbarPercentage}\n role=\"button\"\n tabIndex={0}\n onClick={reset}\n >\n {percentage}%\n </Center>\n </Tooltip>\n <ActionIcon\n disabled={!canZoomIn}\n icon={ZoomIn}\n style={{ borderRadius: 999 }}\n title={t('image.zoomIn')}\n onClick={zoomIn}\n />\n <ActionIcon\n icon={Download}\n loading={downloadLoading}\n style={{ borderRadius: 999 }}\n title={t('image.download')}\n onClick={handleDownload}\n />\n <DropdownMenu\n items={moreItems}\n placement=\"top\"\n portalProps={{ container: containerEl ?? undefined }}\n onOpenChange={(open) => onMoreOpenChange?.(open)}\n >\n <ActionIcon\n icon={Ellipsis}\n loading={copyLoading}\n style={{ borderRadius: 999 }}\n title={t('image.more')}\n />\n </DropdownMenu>\n {toolbarAddon}\n </Flexbox>\n </div>\n </TooltipGroup>\n );\n },\n);\n\nToolbar.displayName = 'Toolbar';\n\nexport default Toolbar;\n"],"mappings":"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"}
|
|
@@ -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,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"}
|