@lobehub/ui 5.27.0 → 5.28.1
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/README.md +6 -6
- package/es/Image/Image.mjs +3 -2
- package/es/Image/Image.mjs.map +1 -1
- package/es/Image/style.mjs +19 -13
- package/es/Image/style.mjs.map +1 -1
- package/es/Image/type.d.mts +3 -0
- package/es/Image/viewer/ActualSizeIcon.mjs +44 -0
- package/es/Image/viewer/ActualSizeIcon.mjs.map +1 -0
- package/es/Image/viewer/ImageViewer.mjs +12 -3
- package/es/Image/viewer/ImageViewer.mjs.map +1 -1
- package/es/Image/viewer/Toolbar.mjs +17 -13
- package/es/Image/viewer/Toolbar.mjs.map +1 -1
- package/es/Image/viewer/ViewerChrome.mjs.map +1 -1
- package/es/Image/viewer/geometry.d.mts +5 -0
- package/es/Image/viewer/geometry.mjs +13 -1
- package/es/Image/viewer/geometry.mjs.map +1 -1
- package/es/Image/viewer/registry.mjs.map +1 -1
- package/es/Image/viewer/useFlipTransition.mjs +7 -4
- package/es/Image/viewer/useFlipTransition.mjs.map +1 -1
- package/es/Image/viewer/useGalleryNav.mjs +6 -5
- package/es/Image/viewer/useGalleryNav.mjs.map +1 -1
- package/es/Image/viewer/useViewerGestures.mjs +2 -6
- package/es/Image/viewer/useViewerGestures.mjs.map +1 -1
- package/es/Image/viewer/useZoomPan.mjs +80 -23
- package/es/Image/viewer/useZoomPan.mjs.map +1 -1
- package/es/Markdown/SyntaxMarkdown/CachedMarkdown.mjs +1 -1
- package/es/Markdown/plugins/rehypeCustomFootnotes.mjs +1 -1
- package/es/Markdown/plugins/rehypeKatexDir.d.mts +1 -1
- package/es/Markdown/plugins/rehypeKatexDir.mjs +1 -1
- package/es/Markdown/plugins/rehypeStreamAnimated.d.mts +1 -1
- package/es/Markdown/plugins/rehypeStreamAnimated.mjs +1 -1
- package/es/Markdown/plugins/remarkBr.mjs +1 -1
- package/es/Markdown/plugins/remarkColor.mjs +1 -1
- package/es/Markdown/plugins/remarkCustomFootnotes.mjs +2 -2
- package/es/Markdown/plugins/remarkGfmPlus.mjs +1 -1
- package/es/Markdown/plugins/remarkVideo.mjs +1 -1
- package/es/base-ui/Drawer/style.mjs +43 -21
- package/es/base-ui/Drawer/style.mjs.map +1 -1
- package/es/eslint/index.mjs +1 -0
- package/es/eslint/index.mjs.map +1 -1
- package/es/i18n/resources/en/image.d.mts +2 -1
- package/es/i18n/resources/en/image.mjs +3 -2
- package/es/i18n/resources/en/image.mjs.map +1 -1
- package/es/i18n/resources/zhCn/image.d.mts +2 -1
- package/es/i18n/resources/zhCn/image.mjs +3 -2
- package/es/i18n/resources/zhCn/image.mjs.map +1 -1
- package/es/index.d.mts +2 -2
- package/es/mdx/mdxComponents/index.d.mts +1 -1
- package/es/node_modules/{.bun → .pnpm}/@types_hast@3.0.5/node_modules/@types/hast/index.d.mts +1 -1
- package/es/node_modules/{.bun → .pnpm}/@types_mdx@2.0.14/node_modules/@types/mdx/types.d.mts +1 -1
- package/es/node_modules/{.bun → .pnpm}/@types_unist@3.0.3/node_modules/@types/unist/index.d.mts +1 -1
- package/es/node_modules/{.bun → .pnpm}/unist-util-is@6.0.1/node_modules/unist-util-is/lib/index.mjs +1 -1
- package/es/node_modules/.pnpm/unist-util-is@6.0.1/node_modules/unist-util-is/lib/index.mjs.map +1 -0
- package/es/node_modules/.pnpm/unist-util-visit-parents@6.0.2/node_modules/unist-util-visit-parents/lib/color.node.mjs +12 -0
- package/es/node_modules/.pnpm/unist-util-visit-parents@6.0.2/node_modules/unist-util-visit-parents/lib/color.node.mjs.map +1 -0
- package/es/node_modules/{.bun → .pnpm}/unist-util-visit-parents@6.0.2/node_modules/unist-util-visit-parents/lib/index.mjs +1 -1
- package/es/node_modules/.pnpm/unist-util-visit-parents@6.0.2/node_modules/unist-util-visit-parents/lib/index.mjs.map +1 -0
- package/es/node_modules/{.bun → .pnpm}/unist-util-visit@5.1.0/node_modules/unist-util-visit/lib/index.mjs +1 -1
- package/es/node_modules/.pnpm/unist-util-visit@5.1.0/node_modules/unist-util-visit/lib/index.mjs.map +1 -0
- package/package.json +4 -5
- package/es/node_modules/.bun/unist-util-is@6.0.1/node_modules/unist-util-is/lib/index.mjs.map +0 -1
- package/es/node_modules/.bun/unist-util-visit-parents@6.0.2/node_modules/unist-util-visit-parents/lib/color.node.mjs +0 -12
- package/es/node_modules/.bun/unist-util-visit-parents@6.0.2/node_modules/unist-util-visit-parents/lib/color.node.mjs.map +0 -1
- package/es/node_modules/.bun/unist-util-visit-parents@6.0.2/node_modules/unist-util-visit-parents/lib/index.mjs.map +0 -1
- package/es/node_modules/.bun/unist-util-visit@5.1.0/node_modules/unist-util-visit/lib/index.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -60,10 +60,10 @@ Lobe UI is an open-source UI component library for building _AIGC_ web apps
|
|
|
60
60
|
|
|
61
61
|
To install Lobe UI, run the following command:
|
|
62
62
|
|
|
63
|
-
[![][
|
|
63
|
+
[![][pnpm-shield]][pnpm-link]
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
|
-
$
|
|
66
|
+
$ pnpm add @lobehub/ui
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
### Compile with NextJS
|
|
@@ -166,8 +166,8 @@ Or clone it for local development:
|
|
|
166
166
|
```bash
|
|
167
167
|
$ git clone https://github.com/lobehub/lobe-ui.git
|
|
168
168
|
$ cd lobe-ui
|
|
169
|
-
$
|
|
170
|
-
$
|
|
169
|
+
$ pnpm install
|
|
170
|
+
$ pnpm start
|
|
171
171
|
```
|
|
172
172
|
|
|
173
173
|
<div align="right">
|
|
@@ -249,8 +249,8 @@ This project is [MIT](./LICENSE) licensed.
|
|
|
249
249
|
|
|
250
250
|
[back-to-top]: https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square
|
|
251
251
|
[banner]: https://github-production-user-asset-6210df.s3.amazonaws.com/17870709/268452017-960ab8a1-e4b7-4648-beb1-77daf4b6034a.png
|
|
252
|
-
[
|
|
253
|
-
[
|
|
252
|
+
[pnpm-link]: https://pnpm.io
|
|
253
|
+
[pnpm-shield]: https://img.shields.io/badge/-pnpm-F69220?logo=pnpm&style=for-the-badge
|
|
254
254
|
[codespaces-link]: https://codespaces.new/lobehub/lobe-ui
|
|
255
255
|
[codespaces-shield]: https://github.com/codespaces/badge.svg
|
|
256
256
|
[contributors-contrib]: https://contrib.rocks/image?repo=lobehub/lobe-ui
|
package/es/Image/Image.mjs
CHANGED
|
@@ -9,7 +9,6 @@ import { memo, useCallback, useEffect, useId, useMemo, useRef, useState } from "
|
|
|
9
9
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
10
|
import { cx, useThemeMode } from "antd-style";
|
|
11
11
|
//#region src/Image/Image.tsx
|
|
12
|
-
const DEFAULT_MAX_SCALE = 8;
|
|
13
12
|
const resolvePreview = (groupPreview, imagePreview) => {
|
|
14
13
|
if (imagePreview === false) return false;
|
|
15
14
|
if (imagePreview === void 0 && groupPreview === false) return false;
|
|
@@ -37,7 +36,9 @@ const Image = memo(({ ref, style, preview, isLoading, maxHeight = "100%", maxWid
|
|
|
37
36
|
const resolvedPreview = resolvePreview(group?.preview, preview);
|
|
38
37
|
const previewEnabled = resolvedPreview !== false;
|
|
39
38
|
const resolvedOptions = useMemo(() => previewEnabled ? {
|
|
40
|
-
|
|
39
|
+
autoZoomThreshold: 2,
|
|
40
|
+
defaultZoom: "auto",
|
|
41
|
+
maxScale: 8,
|
|
41
42
|
...resolvedPreview
|
|
42
43
|
} : void 0, [previewEnabled, resolvedPreview]);
|
|
43
44
|
const previewSrc = resolvedOptions?.src;
|
package/es/Image/Image.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Image.mjs","names":["Flexbox"],"sources":["../../src/Image/Image.tsx"],"sourcesContent":["'use client';\n\nimport { cx, useThemeMode } from 'antd-style';\nimport {\n memo,\n type MouseEvent,\n type PointerEvent,\n type SyntheticEvent,\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n} from 'react';\n\nimport { Flexbox } from '@/Flex';\nimport { SkeletonAvatar } from '@/Skeleton';\n\nimport { usePreviewGroupContext } from './PreviewGroup';\nimport { FALLBACK_DARK, FALLBACK_LIGHT, styles, variants } from './style';\nimport type { ImagePreviewOptions, ImageProps } from './type';\nimport PreviewOutlet from './viewer/PreviewOutlet';\nimport { openPreview, type PreviewEntry } from './viewer/registry';\n\nconst
|
|
1
|
+
{"version":3,"file":"Image.mjs","names":["Flexbox"],"sources":["../../src/Image/Image.tsx"],"sourcesContent":["'use client';\n\nimport { cx, useThemeMode } from 'antd-style';\nimport {\n memo,\n type MouseEvent,\n type PointerEvent,\n type SyntheticEvent,\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n} from 'react';\n\nimport { Flexbox } from '@/Flex';\nimport { SkeletonAvatar } from '@/Skeleton';\n\nimport { usePreviewGroupContext } from './PreviewGroup';\nimport { FALLBACK_DARK, FALLBACK_LIGHT, styles, variants } from './style';\nimport type { ImagePreviewOptions, ImageProps } from './type';\nimport { DEFAULT_AUTO_ZOOM_THRESHOLD, DEFAULT_MAX_SCALE } from './viewer/geometry';\nimport PreviewOutlet from './viewer/PreviewOutlet';\nimport { openPreview, type PreviewEntry } from './viewer/registry';\n\nconst resolvePreview = (\n groupPreview: boolean | ImagePreviewOptions | undefined,\n imagePreview: boolean | ImagePreviewOptions | undefined,\n): ImagePreviewOptions | false => {\n if (imagePreview === false) return false;\n if (imagePreview === undefined && groupPreview === false) return false;\n\n const groupOptions = typeof groupPreview === 'object' ? groupPreview : undefined;\n const imageOptions = typeof imagePreview === 'object' ? imagePreview : undefined;\n\n return { ...groupOptions, ...imageOptions };\n};\n\nconst Image = memo<ImageProps>(\n ({\n ref,\n style,\n preview,\n isLoading,\n maxHeight = '100%',\n maxWidth = '100%',\n minHeight,\n minWidth,\n actions,\n className,\n alwaysShowActions,\n variant = 'filled',\n objectFit = 'cover',\n classNames,\n styles: customStyles,\n onClick,\n onError,\n onPointerDown,\n width,\n height,\n size,\n src,\n alt,\n loading = 'lazy',\n ...rest\n }) => {\n const { isDarkMode } = useThemeMode();\n const [hasError, setHasError] = useState(false);\n const lastSrcRef = useRef(src);\n const imgRef = useRef<HTMLImageElement>(null);\n const preOpenFocusRef = useRef<HTMLElement | null>(null);\n const id = useId();\n const group = usePreviewGroupContext();\n\n if (lastSrcRef.current !== src) {\n lastSrcRef.current = src;\n if (hasError) setHasError(false);\n }\n\n const resolvedWidth = width ?? size;\n const resolvedHeight = height ?? size;\n\n const resolvedPreview = resolvePreview(group?.preview, preview);\n const previewEnabled = resolvedPreview !== false;\n const resolvedOptions = useMemo(\n () =>\n previewEnabled\n ? {\n autoZoomThreshold: DEFAULT_AUTO_ZOOM_THRESHOLD,\n defaultZoom: 'auto' as const,\n maxScale: DEFAULT_MAX_SCALE,\n ...resolvedPreview,\n }\n : undefined,\n [previewEnabled, resolvedPreview],\n );\n const previewSrc = resolvedOptions?.src;\n\n useEffect(() => {\n if (!group) return;\n return group.register({\n getElement: () => imgRef.current,\n id,\n options: resolvedOptions,\n previewSrc,\n src: src ?? '',\n });\n }, [group, id, src, previewSrc, resolvedOptions]);\n\n const handleError = useCallback(\n (event: SyntheticEvent<HTMLImageElement>) => {\n setHasError(true);\n onError?.(event);\n },\n [onError],\n );\n\n const handlePointerDown = useCallback(\n (event: PointerEvent<HTMLImageElement>) => {\n onPointerDown?.(event);\n const active = document.activeElement;\n preOpenFocusRef.current =\n active instanceof HTMLElement &&\n active !== document.body &&\n active !== document.documentElement\n ? active\n : null;\n },\n [onPointerDown],\n );\n\n const handleClick = useCallback(\n (event: MouseEvent<HTMLImageElement>) => {\n onClick?.(event);\n if (!previewEnabled || !imgRef.current || !resolvedOptions) return;\n const clickedEntry: PreviewEntry = {\n element: imgRef.current,\n options: resolvedOptions,\n previewSrc,\n src: imgRef.current.currentSrc || imgRef.current.src,\n };\n const openerFocusElement = preOpenFocusRef.current;\n\n if (!group) {\n openPreview(clickedEntry, undefined, 0, openerFocusElement);\n return;\n }\n\n const galleryEntries: PreviewEntry[] = [];\n let clickedIndex = -1;\n for (const groupEntry of group.getEntries()) {\n const element = groupEntry.getElement();\n if (!element || !groupEntry.options) continue;\n if (element === imgRef.current) clickedIndex = galleryEntries.length;\n galleryEntries.push({\n element,\n options: groupEntry.options,\n previewSrc: groupEntry.previewSrc,\n src: element.currentSrc || element.src,\n });\n }\n openPreview(\n clickedEntry,\n galleryEntries,\n clickedIndex >= 0 ? clickedIndex : 0,\n openerFocusElement,\n );\n },\n [onClick, previewEnabled, resolvedOptions, previewSrc, group],\n );\n\n const actionsClassName = alwaysShowActions ? styles.actionsVisible : styles.actionsHidden;\n\n if (isLoading)\n return (\n <div onClick={onClick}>\n <SkeletonAvatar\n active\n height={resolvedHeight}\n style={{ maxHeight, maxWidth, minHeight, minWidth }}\n width={resolvedWidth}\n />\n </div>\n );\n\n return (\n <Flexbox className={cx(variants({ variant }), className)} ref={ref} style={style}>\n {actions && (\n <div className={cx(actionsClassName, alwaysShowActions ? '' : 'actions-hidden')}>\n {actions}\n </div>\n )}\n <div className={cx(styles.wrapper, classNames?.wrapper)} style={customStyles?.wrapper}>\n <img\n alt={alt}\n className={cx(styles.image, previewEnabled && styles.previewable, classNames?.image)}\n height={resolvedHeight}\n loading={loading}\n ref={imgRef}\n src={hasError ? (isDarkMode ? FALLBACK_DARK : FALLBACK_LIGHT) : src}\n width={resolvedWidth}\n style={{\n height: resolvedHeight,\n maxHeight,\n maxWidth,\n minHeight,\n minWidth,\n objectFit,\n width: resolvedWidth,\n ...customStyles?.image,\n }}\n onClick={handleClick}\n onError={handleError}\n onPointerDown={handlePointerDown}\n {...rest}\n />\n </div>\n {previewEnabled && <PreviewOutlet elementRef={imgRef} />}\n </Flexbox>\n );\n },\n);\n\nImage.displayName = 'Image';\n\nexport default Image;\n"],"mappings":"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"}
|
package/es/Image/style.mjs
CHANGED
|
@@ -76,28 +76,34 @@ const styles = createStaticStyles(({ css, cssVar }) => {
|
|
|
76
76
|
|
|
77
77
|
background: color-mix(in srgb, ${cssVar.colorBgLayout} 60%, transparent);
|
|
78
78
|
backdrop-filter: blur(12px);
|
|
79
|
+
`,
|
|
80
|
+
actualSizeCorner: css`
|
|
81
|
+
/* view-box, so the per-corner transform-origin below is read in the 24x24
|
|
82
|
+
user space the path coordinates are written in rather than the rendered
|
|
83
|
+
pixel box. */
|
|
84
|
+
transform-box: view-box;
|
|
85
|
+
transition: transform 300ms ${cssVar.motionEaseInOut};
|
|
86
|
+
|
|
87
|
+
[data-actual-size='fit'] & {
|
|
88
|
+
transform: rotate(180deg);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@media (prefers-reduced-motion: reduce) {
|
|
92
|
+
transition: none;
|
|
93
|
+
}
|
|
79
94
|
`,
|
|
80
95
|
toolbarPercentage: css`
|
|
81
|
-
cursor: pointer;
|
|
82
96
|
user-select: none;
|
|
83
97
|
|
|
84
|
-
min-width:
|
|
98
|
+
/* Fixed, not min-width: this is a readout that changes digit count as it
|
|
99
|
+
zooms (9% → 100% → 800%), and letting it size to content shifts every
|
|
100
|
+
control beside it on each wheel tick. Wide enough for four digits. */
|
|
101
|
+
width: 56px;
|
|
85
102
|
height: 36px;
|
|
86
|
-
padding-inline: 8px;
|
|
87
|
-
border-radius: 999px;
|
|
88
103
|
|
|
89
104
|
font-size: 12px;
|
|
90
105
|
font-variant-numeric: tabular-nums;
|
|
91
106
|
color: ${cssVar.colorTextTertiary};
|
|
92
|
-
|
|
93
|
-
transition:
|
|
94
|
-
color 400ms ${cssVar.motionEaseOut},
|
|
95
|
-
background 100ms ${cssVar.motionEaseOut};
|
|
96
|
-
|
|
97
|
-
&:hover {
|
|
98
|
-
color: ${cssVar.colorTextSecondary};
|
|
99
|
-
background: ${cssVar.colorFillTertiary};
|
|
100
|
-
}
|
|
101
107
|
`,
|
|
102
108
|
viewerBackdrop: css`
|
|
103
109
|
position: fixed;
|
package/es/Image/style.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"style.mjs","names":["lobeStaticStylish"],"sources":["../../src/Image/style.ts"],"sourcesContent":["import { createStaticStyles } from 'antd-style';\nimport { cva } from 'class-variance-authority';\n\nimport { lobeStaticStylish } from '@/styles';\n\nexport const styles = createStaticStyles(({ css, cssVar }) => {\n // Bare viewer controls float directly over arbitrary image content; the\n // theme-side halo (light halo around dark icons in light mode and vice\n // versa) is what keeps them readable without a container panel.\n const controlHalo = `drop-shadow(0 0 2px ${cssVar.colorBgLayout}) drop-shadow(0 1px 6px ${cssVar.colorBgLayout})`;\n return {\n actionsHidden: css`\n cursor: pointer;\n\n position: absolute;\n z-index: 1;\n inset-block-start: 0;\n inset-inline-end: 0;\n\n opacity: 0;\n `,\n actionsVisible: css`\n cursor: pointer;\n\n position: absolute;\n z-index: 1;\n inset-block-start: 0;\n inset-inline-end: 0;\n\n opacity: 1;\n `,\n borderless: lobeStaticStylish.variantBorderlessWithoutHover,\n filled: css`\n ${lobeStaticStylish.variantOutlinedWithoutHover};\n ${lobeStaticStylish.variantFilledWithoutHover};\n `,\n image: css`\n display: flex;\n align-items: center;\n justify-content: center;\n\n width: auto;\n height: auto;\n `,\n outlined: lobeStaticStylish.variantOutlinedWithoutHover,\n previewable: css`\n cursor: zoom-in;\n `,\n root: css`\n cursor: pointer;\n user-select: none;\n\n position: relative;\n\n overflow: hidden;\n\n width: fit-content;\n border-radius: ${cssVar.borderRadius};\n\n line-height: 1;\n\n &:hover {\n .actions-hidden {\n opacity: 1;\n }\n }\n `,\n toolbar: css`\n pointer-events: auto;\n\n position: absolute;\n inset-block-end: 16px;\n inset-inline-start: 50%;\n transform: translateX(-50%);\n `,\n // The glass lives on the icon row, not the toolbar itself: tooltips and\n // the more-menu portal into the toolbar element, and an ancestor\n // backdrop-filter/filter would distort those popups too.\n toolbarRow: css`\n padding-block: 4px;\n padding-inline: 6px;\n border-radius: 999px;\n\n background: color-mix(in srgb, ${cssVar.colorBgLayout} 60%, transparent);\n backdrop-filter: blur(12px);\n `,\n
|
|
1
|
+
{"version":3,"file":"style.mjs","names":["lobeStaticStylish"],"sources":["../../src/Image/style.ts"],"sourcesContent":["import { createStaticStyles } from 'antd-style';\nimport { cva } from 'class-variance-authority';\n\nimport { lobeStaticStylish } from '@/styles';\n\nexport const styles = createStaticStyles(({ css, cssVar }) => {\n // Bare viewer controls float directly over arbitrary image content; the\n // theme-side halo (light halo around dark icons in light mode and vice\n // versa) is what keeps them readable without a container panel.\n const controlHalo = `drop-shadow(0 0 2px ${cssVar.colorBgLayout}) drop-shadow(0 1px 6px ${cssVar.colorBgLayout})`;\n return {\n actionsHidden: css`\n cursor: pointer;\n\n position: absolute;\n z-index: 1;\n inset-block-start: 0;\n inset-inline-end: 0;\n\n opacity: 0;\n `,\n actionsVisible: css`\n cursor: pointer;\n\n position: absolute;\n z-index: 1;\n inset-block-start: 0;\n inset-inline-end: 0;\n\n opacity: 1;\n `,\n borderless: lobeStaticStylish.variantBorderlessWithoutHover,\n filled: css`\n ${lobeStaticStylish.variantOutlinedWithoutHover};\n ${lobeStaticStylish.variantFilledWithoutHover};\n `,\n image: css`\n display: flex;\n align-items: center;\n justify-content: center;\n\n width: auto;\n height: auto;\n `,\n outlined: lobeStaticStylish.variantOutlinedWithoutHover,\n previewable: css`\n cursor: zoom-in;\n `,\n root: css`\n cursor: pointer;\n user-select: none;\n\n position: relative;\n\n overflow: hidden;\n\n width: fit-content;\n border-radius: ${cssVar.borderRadius};\n\n line-height: 1;\n\n &:hover {\n .actions-hidden {\n opacity: 1;\n }\n }\n `,\n toolbar: css`\n pointer-events: auto;\n\n position: absolute;\n inset-block-end: 16px;\n inset-inline-start: 50%;\n transform: translateX(-50%);\n `,\n // The glass lives on the icon row, not the toolbar itself: tooltips and\n // the more-menu portal into the toolbar element, and an ancestor\n // backdrop-filter/filter would distort those popups too.\n toolbarRow: css`\n padding-block: 4px;\n padding-inline: 6px;\n border-radius: 999px;\n\n background: color-mix(in srgb, ${cssVar.colorBgLayout} 60%, transparent);\n backdrop-filter: blur(12px);\n `,\n actualSizeCorner: css`\n /* view-box, so the per-corner transform-origin below is read in the 24x24\n user space the path coordinates are written in rather than the rendered\n pixel box. */\n transform-box: view-box;\n transition: transform 300ms ${cssVar.motionEaseInOut};\n\n [data-actual-size='fit'] & {\n transform: rotate(180deg);\n }\n\n @media (prefers-reduced-motion: reduce) {\n transition: none;\n }\n `,\n\n toolbarPercentage: css`\n user-select: none;\n\n /* Fixed, not min-width: this is a readout that changes digit count as it\n zooms (9% → 100% → 800%), and letting it size to content shifts every\n control beside it on each wheel tick. Wide enough for four digits. */\n width: 56px;\n height: 36px;\n\n font-size: 12px;\n font-variant-numeric: tabular-nums;\n color: ${cssVar.colorTextTertiary};\n `,\n\n viewerBackdrop: css`\n position: fixed;\n inset: 0;\n\n opacity: 0;\n background: color-mix(in srgb, ${cssVar.colorBgLayout} 90%, transparent);\n backdrop-filter: blur(8px);\n `,\n viewerChrome: css`\n pointer-events: none;\n position: absolute;\n inset: 0;\n opacity: 0;\n `,\n viewerChromeIdle: css`\n pointer-events: none;\n position: absolute;\n inset: 0;\n transition:\n opacity 200ms ${cssVar.motionEaseOut},\n visibility 200ms;\n\n &[data-idle-hidden] {\n visibility: hidden;\n opacity: 0;\n }\n\n @media (prefers-reduced-motion: reduce) {\n transition: none;\n }\n `,\n viewerClose: css`\n pointer-events: auto;\n\n position: absolute;\n inset-block-start: 16px;\n inset-inline-end: 16px;\n\n filter: ${controlHalo};\n `,\n viewerCounter: css`\n pointer-events: none;\n\n position: absolute;\n inset-block-start: 16px;\n inset-inline-start: 50%;\n transform: translateX(-50%);\n\n padding-block: 4px;\n padding-inline: 12px;\n border-radius: 999px;\n\n font-size: 12px;\n font-variant-numeric: tabular-nums;\n color: ${cssVar.colorTextSecondary};\n\n background: color-mix(in srgb, ${cssVar.colorBgLayout} 60%, transparent);\n backdrop-filter: blur(12px);\n `,\n viewerImage: css`\n will-change: transform;\n cursor: zoom-out;\n user-select: none;\n\n position: absolute;\n transform-origin: center center;\n\n object-fit: contain;\n\n -webkit-user-drag: none;\n `,\n viewerNavButton: css`\n pointer-events: auto;\n\n position: absolute;\n inset-block-start: 50%;\n transform: translateY(-50%);\n\n filter: ${controlHalo};\n `,\n viewerNavNext: css`\n inset-inline-end: 16px;\n `,\n viewerNavPrev: css`\n inset-inline-start: 16px;\n `,\n viewerPopup: css`\n position: fixed;\n inset: 0;\n overflow: hidden;\n outline: none;\n `,\n wrapper: css`\n position: relative;\n overflow: hidden;\n max-width: 100%;\n height: auto;\n `,\n };\n});\n\nexport const variants = cva(styles.root, {\n defaultVariants: {\n variant: 'filled',\n },\n\n variants: {\n variant: {\n filled: styles.filled,\n outlined: styles.outlined,\n borderless: styles.borderless,\n },\n },\n});\n\nexport const FALLBACK_DARK =\n 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjU2IiBoZWlnaHQ9IjI1NiIgdmlld0JveD0iMCAwIDI1NiAyNTYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyNTYiIGhlaWdodD0iMjU2IiBmaWxsPSIjM0IzQjNCIi8+CjxwYXRoIGQ9Ik0xNTYuODg4IDkxLjAwMkgxMDAuMTEyQzk1LjYzMjkgOTEuMDAyIDkyLjAwMTUgOTQuNjMzNCA5Mi4wMDE1IDk5LjExMjdWMTU1Ljg4OEM5Mi4wMDE1IDE2MC4zNjcgOTUuNjMyOSAxNjMuOTk5IDEwMC4xMTIgMTYzLjk5OUgxNTYuODg4QzE2MS4zNjcgMTYzLjk5OSAxNjQuOTk4IDE2MC4zNjcgMTY0Ljk5OCAxNTUuODg4Vjk5LjExMjdDMTY0Ljk5OCA5NC42MzM0IDE2MS4zNjcgOTEuMDAyIDE1Ni44ODggOTEuMDAyWiIgc3Ryb2tlPSIjNjI2MjYyIiBzdHJva2Utd2lkdGg9IjguMTEwNzciIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8cGF0aCBkPSJNMTY0Ljk5OCAxMzkuNjY4TDE1Mi40ODQgMTI3LjE1M0MxNTAuOTYyIDEyNS42MzIgMTQ4LjkgMTI0Ljc3OCAxNDYuNzQ5IDEyNC43NzhDMTQ0LjU5OSAxMjQuNzc4IDE0Mi41MzYgMTI1LjYzMiAxNDEuMDE1IDEyNy4xNTNMMTA0LjE2OCAxNjRNMTE2LjMzNCAxMjMuNDQ1QzEyMC44MTMgMTIzLjQ0NSAxMjQuNDQ1IDExOS44MTQgMTI0LjQ0NSAxMTUuMzM0QzEyNC40NDUgMTEwLjg1NSAxMjAuODEzIDEwNy4yMjQgMTE2LjMzNCAxMDcuMjI0QzExMS44NTUgMTA3LjIyNCAxMDguMjIzIDExMC44NTUgMTA4LjIyMyAxMTUuMzM0QzEwOC4yMjMgMTE5LjgxNCAxMTEuODU1IDEyMy40NDUgMTE2LjMzNCAxMjMuNDQ1WiIgc3Ryb2tlPSIjNjI2MjYyIiBzdHJva2Utd2lkdGg9IjguMTEwNzciIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8L3N2Zz4K';\nexport const FALLBACK_LIGHT =\n 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjU2IiBoZWlnaHQ9IjI1NiIgdmlld0JveD0iMCAwIDI1NiAyNTYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyNTYiIGhlaWdodD0iMjU2IiBmaWxsPSIjRUNFQ0VDIi8+CjxwYXRoIGQ9Ik0xNTYuODg4IDkxLjAwMkgxMDAuMTEyQzk1LjYzMjkgOTEuMDAyIDkyLjAwMTUgOTQuNjMzNCA5Mi4wMDE1IDk5LjExMjdWMTU1Ljg4OEM5Mi4wMDE1IDE2MC4zNjcgOTUuNjMyOSAxNjMuOTk5IDEwMC4xMTIgMTYzLjk5OUgxNTYuODg4QzE2MS4zNjcgMTYzLjk5OSAxNjQuOTk4IDE2MC4zNjcgMTY0Ljk5OCAxNTUuODg4Vjk5LjExMjdDMTY0Ljk5OCA5NC42MzM0IDE2MS4zNjcgOTEuMDAyIDE1Ni44ODggOTEuMDAyWiIgc3Ryb2tlPSIjRDdEN0Q3IiBzdHJva2Utd2lkdGg9IjguMTEwNzciIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8cGF0aCBkPSJNMTY0Ljk5OCAxMzkuNjY4TDE1Mi40ODQgMTI3LjE1M0MxNTAuOTYyIDEyNS42MzIgMTQ4LjkgMTI0Ljc3OCAxNDYuNzQ5IDEyNC43NzhDMTQ0LjU5OSAxMjQuNzc4IDE0Mi41MzYgMTI1LjYzMiAxNDEuMDE1IDEyNy4xNTNMMTA0LjE2OCAxNjRNMTE2LjMzNCAxMjMuNDQ1QzEyMC44MTMgMTIzLjQ0NSAxMjQuNDQ1IDExOS44MTQgMTI0LjQ0NSAxMTUuMzM0QzEyNC40NDUgMTEwLjg1NSAxMjAuODEzIDEwNy4yMjQgMTE2LjMzNCAxMDcuMjI0QzExMS44NTUgMTA3LjIyNCAxMDguMjIzIDExMC44NTUgMTA4LjIyMyAxMTUuMzM0QzEwOC4yMjMgMTE5LjgxNCAxMTEuODU1IDEyMy40NDUgMTE2LjMzNCAxMjMuNDQ1WiIgc3Ryb2tlPSIjRDdEN0Q3IiBzdHJva2Utd2lkdGg9IjguMTEwNzciIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8L3N2Zz4K';\n"],"mappings":";;;;AAKA,MAAa,SAAS,oBAAoB,EAAE,KAAK,aAAa;CAI5D,MAAM,cAAc,uBAAuB,OAAO,cAAc,0BAA0B,OAAO,cAAc;CAC/G,OAAO;EACL,eAAe,GAAG;;;;;;;;;;EAUlB,gBAAgB,GAAG;;;;;;;;;;EAUnB,YAAYA,cAAkB;EAC9B,QAAQ,GAAG;QACPA,cAAkB,4BAA4B;QAC9CA,cAAkB,0BAA0B;;EAEhD,OAAO,GAAG;;;;;;;;EAQV,UAAUA,cAAkB;EAC5B,aAAa,GAAG;;;EAGhB,MAAM,GAAG;;;;;;;;;uBASU,OAAO,aAAa;;;;;;;;;;EAUvC,SAAS,GAAG;;;;;;;;EAWZ,YAAY,GAAG;;;;;uCAKoB,OAAO,cAAc;;;EAGxD,kBAAkB,GAAG;;;;;oCAKW,OAAO,gBAAgB;;;;;;;;;;EAWvD,mBAAmB,GAAG;;;;;;;;;;;eAWX,OAAO,kBAAkB;;EAGpC,gBAAgB,GAAG;;;;;uCAKgB,OAAO,cAAc;;;EAGxD,cAAc,GAAG;;;;;;EAMjB,kBAAkB,GAAG;;;;;wBAKD,OAAO,cAAc;;;;;;;;;;;;EAYzC,aAAa,GAAG;;;;;;;gBAOJ,YAAY;;EAExB,eAAe,GAAG;;;;;;;;;;;;;;eAcP,OAAO,mBAAmB;;uCAEF,OAAO,cAAc;;;EAGxD,aAAa,GAAG;;;;;;;;;;;;EAYhB,iBAAiB,GAAG;;;;;;;gBAOR,YAAY;;EAExB,eAAe,GAAG;;;EAGlB,eAAe,GAAG;;;EAGlB,aAAa,GAAG;;;;;;EAMhB,SAAS,GAAG;;;;;;CAMd;AACF,CAAC;AAED,MAAa,WAAW,IAAI,OAAO,MAAM;CACvC,iBAAiB,EACf,SAAS,SACX;CAEA,UAAU,EACR,SAAS;EACP,QAAQ,OAAO;EACf,UAAU,OAAO;EACjB,YAAY,OAAO;CACrB,EACF;AACF,CAAC;AAED,MAAa,gBACX;AACF,MAAa,iBACX"}
|
package/es/Image/type.d.mts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { ZoomPolicy } from "./viewer/geometry.mjs";
|
|
1
2
|
import { CSSProperties, ComponentProps, ReactNode, Ref } from "react";
|
|
2
3
|
//#region src/Image/type.d.ts
|
|
3
4
|
interface ImagePreviewOptions {
|
|
5
|
+
autoZoomThreshold?: number;
|
|
6
|
+
defaultZoom?: ZoomPolicy;
|
|
4
7
|
maxScale?: number;
|
|
5
8
|
onOpenChange?: (open: boolean) => void;
|
|
6
9
|
src?: string;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { styles } from "../style.mjs";
|
|
3
|
+
import { memo } from "react";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
//#region src/Image/viewer/ActualSizeIcon.tsx
|
|
6
|
+
const CORNERS = [
|
|
7
|
+
{
|
|
8
|
+
d: "m21 21-6-6m6 6v-4.8m0 4.8h-4.8",
|
|
9
|
+
origin: "18px 18px"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
d: "M3 16.2V21m0 0h4.8M3 21l6-6",
|
|
13
|
+
origin: "6px 18px"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
d: "M21 7.8V3m0 0h-4.8M21 3l-6 6",
|
|
17
|
+
origin: "18px 6px"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
d: "M3 7.8V3m0 0h4.8M3 3l6 6",
|
|
21
|
+
origin: "6px 6px"
|
|
22
|
+
}
|
|
23
|
+
];
|
|
24
|
+
const ActualSizeIcon = memo(({ color = "currentColor", fill = "none", height = 24, strokeWidth = 2, width = 24 }) => /* @__PURE__ */ jsx("svg", {
|
|
25
|
+
fill,
|
|
26
|
+
height,
|
|
27
|
+
stroke: color,
|
|
28
|
+
strokeLinecap: "round",
|
|
29
|
+
strokeLinejoin: "round",
|
|
30
|
+
strokeWidth,
|
|
31
|
+
viewBox: "0 0 24 24",
|
|
32
|
+
width,
|
|
33
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
34
|
+
children: CORNERS.map(({ d, origin }) => /* @__PURE__ */ jsx("path", {
|
|
35
|
+
className: styles.actualSizeCorner,
|
|
36
|
+
d,
|
|
37
|
+
style: { transformOrigin: origin }
|
|
38
|
+
}, d))
|
|
39
|
+
}));
|
|
40
|
+
ActualSizeIcon.displayName = "ActualSizeIcon";
|
|
41
|
+
//#endregion
|
|
42
|
+
export { ActualSizeIcon as default };
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=ActualSizeIcon.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ActualSizeIcon.mjs","names":[],"sources":["../../../src/Image/viewer/ActualSizeIcon.tsx"],"sourcesContent":["'use client';\n\nimport { memo } from 'react';\n\nimport { styles } from '../style';\n\n// lucide's Expand, kept as four separate corners so each can be transformed on\n// its own. Expand and Shrink are not two unrelated glyphs: rotating each of\n// these corners 180° about its own quadrant centre maps Expand onto Shrink\n// exactly, coordinate for coordinate. That makes the toggle a transform\n// animation rather than a path interpolation, so it needs no morphing library\n// and stays on the compositor.\nconst CORNERS = [\n { d: 'm21 21-6-6m6 6v-4.8m0 4.8h-4.8', origin: '18px 18px' },\n { d: 'M3 16.2V21m0 0h4.8M3 21l6-6', origin: '6px 18px' },\n { d: 'M21 7.8V3m0 0h-4.8M21 3l-6 6', origin: '18px 6px' },\n { d: 'M3 7.8V3m0 0h4.8M3 3l6 6', origin: '6px 6px' },\n];\n\nexport interface ActualSizeIconProps {\n color?: string;\n fill?: string;\n height?: number | string;\n strokeWidth?: number | string;\n width?: number | string;\n}\n\nconst ActualSizeIcon = memo<ActualSizeIconProps>(\n ({ color = 'currentColor', fill = 'none', height = 24, strokeWidth = 2, width = 24 }) => (\n <svg\n fill={fill}\n height={height}\n stroke={color}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={strokeWidth}\n viewBox=\"0 0 24 24\"\n width={width}\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n {CORNERS.map(({ d, origin }) => (\n <path\n className={styles.actualSizeCorner}\n d={d}\n key={d}\n style={{ transformOrigin: origin }}\n />\n ))}\n </svg>\n ),\n);\n\nActualSizeIcon.displayName = 'ActualSizeIcon';\n\nexport default ActualSizeIcon;\n"],"mappings":";;;;;AAYA,MAAM,UAAU;CACd;EAAE,GAAG;EAAkC,QAAQ;CAAY;CAC3D;EAAE,GAAG;EAA+B,QAAQ;CAAW;CACvD;EAAE,GAAG;EAAgC,QAAQ;CAAW;CACxD;EAAE,GAAG;EAA4B,QAAQ;CAAU;AACrD;AAUA,MAAM,iBAAiB,MACpB,EAAE,QAAQ,gBAAgB,OAAO,QAAQ,SAAS,IAAI,cAAc,GAAG,QAAQ,SAC9E,oBAAC,OAAD;CACQ;CACE;CACR,QAAQ;CACR,eAAc;CACd,gBAAe;CACF;CACb,SAAQ;CACD;CACP,OAAM;CAEL,UAAA,QAAQ,KAAK,EAAE,GAAG,aACjB,oBAAC,QAAD;EACE,WAAW,OAAO;EACf;EAEH,OAAO,EAAE,iBAAiB,OAAO;CAClC,GAFM,CAEN,CACF;AACE,CAAA,CAET;AAEA,eAAe,cAAc"}
|
|
@@ -3,8 +3,8 @@ import { MotionComponent } from "../../MotionProvider/index.mjs";
|
|
|
3
3
|
import { useAppElement } from "../../ThemeProvider/AppElementContext.mjs";
|
|
4
4
|
import { useLayerZIndex } from "../../base-ui/zIndex/useLayerZIndex.mjs";
|
|
5
5
|
import { styles } from "../style.mjs";
|
|
6
|
-
import { ToastHost } from "../../base-ui/Toast/imperative.mjs";
|
|
7
6
|
import { computeFit, unrotatedRect } from "./geometry.mjs";
|
|
7
|
+
import { ToastHost } from "../../base-ui/Toast/imperative.mjs";
|
|
8
8
|
import { beginClosePreview, endClosePreview } from "./registry.mjs";
|
|
9
9
|
import { useFinalFocus } from "./useFinalFocus.mjs";
|
|
10
10
|
import { useFlipTransition } from "./useFlipTransition.mjs";
|
|
@@ -40,10 +40,15 @@ const ImageViewer = memo(({ entries, index, openerFocusElement, token }) => {
|
|
|
40
40
|
const requestClose = useCallback(() => closeRef.current?.(), []);
|
|
41
41
|
const isClosingIndirectRef = useRef(() => false);
|
|
42
42
|
const isClosingIndirect = useCallback(() => isClosingIndirectRef.current(), []);
|
|
43
|
+
const isTransitioningIndirectRef = useRef(() => true);
|
|
44
|
+
const isTransitioningIndirect = useCallback(() => isTransitioningIndirectRef.current(), []);
|
|
43
45
|
const fillViewport = Boolean(currentEntry.previewSrc && currentEntry.previewSrc !== currentEntry.src);
|
|
44
|
-
const { canZoomIn, canZoomOut, dragBy, dragEnd, escIntent, flipHorizontal, flipVertical, flipX, flipY, handleDoubleClick, handleWheel, isClean, isZoomed, reset, resetCloseTracking, rotate, rotateLeft, rotateRight, rotation, scale, setNatural: syncZoomNatural, setViewport: syncZoomViewport, x, y, zoomIn, zoomOut } = useZoomPan({
|
|
46
|
+
const { canZoomIn, canZoomOut, dragBy, dragEnd, escIntent, flipHorizontal, flipVertical, flipX, flipY, getInitialScale, handleDoubleClick, handleWheel, isClean, isZoomed, reset, resetCloseTracking, rotate, rotateLeft, rotateRight, rotation, scale, setNatural: syncZoomNatural, setViewport: syncZoomViewport, toggleActualSize, x, y, zoomIn, zoomOut } = useZoomPan({
|
|
47
|
+
autoZoomThreshold: currentEntry.options.autoZoomThreshold,
|
|
48
|
+
defaultZoom: currentEntry.options.defaultZoom,
|
|
45
49
|
fillViewport,
|
|
46
50
|
isClosing: isClosingIndirect,
|
|
51
|
+
isTransitioning: isTransitioningIndirect,
|
|
47
52
|
maxScale: currentEntry.options.maxScale,
|
|
48
53
|
natural,
|
|
49
54
|
onCloseRequest: requestClose,
|
|
@@ -83,12 +88,14 @@ const ImageViewer = memo(({ entries, index, openerFocusElement, token }) => {
|
|
|
83
88
|
animated,
|
|
84
89
|
getCloseSource,
|
|
85
90
|
getFitRect,
|
|
91
|
+
getInitialScale,
|
|
86
92
|
getViewportWidth,
|
|
87
93
|
onClosed: handleClosed,
|
|
88
94
|
source: openerEntry.element,
|
|
89
95
|
transform
|
|
90
96
|
});
|
|
91
97
|
isClosingIndirectRef.current = isClosing;
|
|
98
|
+
isTransitioningIndirectRef.current = isTransitioning;
|
|
92
99
|
useRefitTransition({
|
|
93
100
|
animated,
|
|
94
101
|
fillViewport,
|
|
@@ -141,6 +148,7 @@ const ImageViewer = memo(({ entries, index, openerFocusElement, token }) => {
|
|
|
141
148
|
entries,
|
|
142
149
|
flipX,
|
|
143
150
|
flipY,
|
|
151
|
+
getInitialScale,
|
|
144
152
|
resetCloseTracking,
|
|
145
153
|
rotate,
|
|
146
154
|
scale,
|
|
@@ -204,6 +212,7 @@ const ImageViewer = memo(({ entries, index, openerFocusElement, token }) => {
|
|
|
204
212
|
container: appElement ?? void 0,
|
|
205
213
|
children: [
|
|
206
214
|
/* @__PURE__ */ jsx(Dialog.Backdrop, {
|
|
215
|
+
forceRender: true,
|
|
207
216
|
className: styles.viewerBackdrop,
|
|
208
217
|
ref: backdropRef,
|
|
209
218
|
style: zIndex === void 0 ? void 0 : { zIndex },
|
|
@@ -250,12 +259,12 @@ const ImageViewer = memo(({ entries, index, openerFocusElement, token }) => {
|
|
|
250
259
|
natural,
|
|
251
260
|
next,
|
|
252
261
|
prev,
|
|
253
|
-
reset,
|
|
254
262
|
rotateLeft,
|
|
255
263
|
rotateRight,
|
|
256
264
|
rotation,
|
|
257
265
|
scale,
|
|
258
266
|
source,
|
|
267
|
+
toggleActualSize,
|
|
259
268
|
toolbarAddon: currentEntry.options.toolbarAddon,
|
|
260
269
|
total: entries.length,
|
|
261
270
|
zoomIn,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImageViewer.mjs","names":[],"sources":["../../../src/Image/viewer/ImageViewer.tsx"],"sourcesContent":["'use client';\n\nimport { Dialog, type DialogRootProps } from '@base-ui/react/dialog';\nimport {\n memo,\n type SyntheticEvent,\n use,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport { useMergeRefs } from 'react-merge-refs';\n\nimport { ToastHost } from '@/base-ui/Toast';\nimport { useLayerZIndex } from '@/base-ui/zIndex';\nimport { MotionComponent } from '@/MotionProvider';\nimport { useAppElement } from '@/ThemeProvider';\n\nimport { styles } from '../style';\nimport { computeFit, type Size, unrotatedRect } from './geometry';\nimport { beginClosePreview, endClosePreview, type PreviewEntry } from './registry';\nimport { useFinalFocus } from './useFinalFocus';\nimport { useFlipTransition } from './useFlipTransition';\nimport { readNatural, useGalleryNav } from './useGalleryNav';\nimport { useRefitTransition } from './useRefitTransition';\nimport { useViewerGestures } from './useViewerGestures';\nimport { useZoomPan } from './useZoomPan';\nimport ViewerChrome from './ViewerChrome';\n\nexport interface ImageViewerProps {\n entries: PreviewEntry[];\n index: number;\n openerFocusElement: HTMLElement | null;\n token: number;\n}\n\nconst readViewport = (): Size => ({ height: window.innerHeight, width: window.innerWidth });\n\nconst prefersReducedMotion = () =>\n window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;\n\nconst ImageViewer = memo<ImageViewerProps>(({ entries, index, openerFocusElement, token }) => {\n const openerEntry = entries[index];\n const appElement = useAppElement();\n const motionComponent = use(MotionComponent);\n const [animated] = useState(() => !!motionComponent && !prefersReducedMotion());\n\n const { ref: layerRef, zIndex } = useLayerZIndex<HTMLDivElement>('modal');\n\n const [currentIndex, setCurrentIndex] = useState(index);\n const currentEntry = entries[currentIndex];\n const currentEntryRef = useRef(currentEntry);\n currentEntryRef.current = currentEntry;\n\n const [source, setSource] = useState(openerEntry.src);\n const [natural, setNatural] = useState<Size>(() => readNatural(openerEntry.element));\n const [viewport, setViewport] = useState<Size>(readViewport);\n\n const closeRef = useRef<(() => void) | null>(null);\n const requestClose = useCallback(() => closeRef.current?.(), []);\n\n // useZoomPan needs to gate its wheel/toolbar entry points on close-in-flight,\n // but that state only exists once useFlipTransition (below) has run — and it\n // needs useZoomPan's own transform values first. Break the cycle with a\n // stable indirection: the ref is repointed at the real isClosing once it's\n // available, written directly during render like the other *Ref fields here.\n const isClosingIndirectRef = useRef<() => boolean>(() => false);\n const isClosingIndirect = useCallback(() => isClosingIndirectRef.current(), []);\n\n const fillViewport = Boolean(\n currentEntry.previewSrc && currentEntry.previewSrc !== currentEntry.src,\n );\n\n const {\n canZoomIn,\n canZoomOut,\n dragBy,\n dragEnd,\n escIntent,\n flipHorizontal,\n flipVertical,\n flipX,\n flipY,\n handleDoubleClick,\n handleWheel,\n isClean,\n isZoomed,\n reset,\n resetCloseTracking,\n rotate,\n rotateLeft,\n rotateRight,\n rotation,\n scale,\n setNatural: syncZoomNatural,\n setViewport: syncZoomViewport,\n x,\n y,\n zoomIn,\n zoomOut,\n } = useZoomPan({\n fillViewport,\n isClosing: isClosingIndirect,\n maxScale: currentEntry.options.maxScale,\n natural,\n onCloseRequest: requestClose,\n viewport,\n });\n\n const fitRect = useMemo(\n () => computeFit(natural, viewport, rotation, fillViewport),\n [natural, viewport, rotation, fillViewport],\n );\n const fitRectRef = useRef(fitRect);\n fitRectRef.current = fitRect;\n const getFitRect = useCallback(() => fitRectRef.current, []);\n\n const imageRect = useMemo(() => unrotatedRect(fitRect, rotation), [fitRect, rotation]);\n\n const viewportRef = useRef(viewport);\n viewportRef.current = viewport;\n const getViewportWidth = useCallback(() => viewportRef.current.width, []);\n\n const getCloseSource = useCallback(() => currentEntryRef.current.element, []);\n\n const transform = useMemo(\n () => ({ flipX, flipY, rotate, scale, x, y }),\n [flipX, flipY, rotate, scale, x, y],\n );\n const handleClosed = useCallback(() => endClosePreview(token), [token]);\n\n const {\n backdropRef,\n chromeRef,\n close,\n imageRef: flipImageRef,\n isClosing,\n isTransitioning,\n switchTo,\n } = useFlipTransition({\n animated,\n getCloseSource,\n getFitRect,\n getViewportWidth,\n onClosed: handleClosed,\n source: openerEntry.element,\n transform,\n });\n isClosingIndirectRef.current = isClosing;\n\n useRefitTransition({\n animated,\n fillViewport,\n isTransitioning,\n natural,\n rotation,\n scale,\n viewport,\n x,\n y,\n });\n const finalFocus = useFinalFocus(openerFocusElement);\n\n const isCleanRef = useRef(isClean);\n isCleanRef.current = isClean;\n\n const handleClose = useCallback(() => {\n beginClosePreview(token);\n close({ fade: !isCleanRef.current });\n }, [close, token]);\n closeRef.current = handleClose;\n\n const handleDialogOpenChange = useCallback<NonNullable<DialogRootProps['onOpenChange']>>(\n (open, eventDetails) => {\n if (open) return;\n // Base UI fires this on every Esc regardless of whether we're already\n // mid-close (our Dialog.Root's open prop never actually flips to\n // false), so without this guard a second Esc during the close window\n // reads escIntent() off the close spring's mid-flight scale, decides\n // 'reset', and cancels the close animation out from under itself.\n if (isClosing()) return;\n if (eventDetails.reason === 'escape-key' && escIntent() === 'reset') {\n eventDetails.cancel();\n reset();\n return;\n }\n handleClose();\n },\n [escIntent, handleClose, isClosing, reset],\n );\n\n const gestures = useViewerGestures({\n dragBy,\n dragEnd,\n handleDoubleClick,\n handleWheel,\n isClean,\n isZoomed,\n onClose: handleClose,\n reset,\n zoomIn,\n zoomOut,\n });\n\n const { hasNext, hasPrev, next, prev } = useGalleryNav({\n cancelPendingClose: gestures.cancelPendingClose,\n currentIndex,\n entries,\n flipX,\n flipY,\n resetCloseTracking,\n rotate,\n scale,\n setCurrentIndex,\n setNatural,\n setSource,\n switchTo,\n syncZoomNatural,\n x,\n y,\n });\n\n const popupNodeRef = useRef<HTMLDivElement | null>(null);\n const popupRef = useMergeRefs<HTMLDivElement>([layerRef, gestures.popupRef, popupNodeRef]);\n const imageRef = useMergeRefs<HTMLImageElement>([flipImageRef, gestures.imageRef]);\n\n const handleLoad = useCallback(\n (event: SyntheticEvent<HTMLImageElement>) => {\n const loaded = event.currentTarget;\n if (!loaded.naturalWidth || !loaded.naturalHeight) return;\n const next = { height: loaded.naturalHeight, width: loaded.naturalWidth };\n setNatural((prev) =>\n prev.width === next.width && prev.height === next.height ? prev : next,\n );\n syncZoomNatural(next);\n },\n [syncZoomNatural],\n );\n\n useEffect(() => {\n const handleResize = () => {\n const next = readViewport();\n setViewport(next);\n syncZoomViewport(next);\n };\n window.addEventListener('resize', handleResize);\n return () => window.removeEventListener('resize', handleResize);\n }, [syncZoomViewport]);\n\n useEffect(() => {\n const { previewSrc, src } = currentEntry;\n if (!previewSrc || previewSrc === src) return;\n let cancelled = false;\n const loader = new window.Image();\n const handleLoaded = () => {\n if (!cancelled) setSource(previewSrc);\n };\n loader.addEventListener('load', handleLoaded);\n loader.src = previewSrc;\n return () => {\n cancelled = true;\n loader.removeEventListener('load', handleLoaded);\n };\n }, [currentEntry]);\n\n useEffect(\n () => () => {\n const thumbnailWasRemoved = !openerEntry.element.isConnected;\n if (thumbnailWasRemoved) endClosePreview(token);\n },\n [openerEntry, token],\n );\n\n return (\n <Dialog.Root modal open onOpenChange={handleDialogOpenChange}>\n <Dialog.Portal container={appElement ?? undefined}>\n <Dialog.Backdrop\n className={styles.viewerBackdrop}\n ref={backdropRef}\n style={zIndex === undefined ? undefined : { zIndex }}\n onClick={gestures.onSurfaceClick}\n />\n <Dialog.Popup\n aria-label={currentEntry.element.alt || undefined}\n className={styles.viewerPopup}\n finalFocus={finalFocus}\n // Focus the popup itself instead of Base UI's default (the first\n // tabbable — the close button): a focus-visible close button pins\n // the idle auto-hide open forever and pops its tooltip on open.\n initialFocus={popupNodeRef}\n ref={popupRef}\n style={zIndex === undefined ? undefined : { zIndex: zIndex + 1 }}\n tabIndex={-1}\n onClick={gestures.onSurfaceClick}\n onPointerCancel={gestures.onPointerFinish}\n onPointerDown={gestures.onPointerDown}\n onPointerMove={gestures.onPointerMove}\n onPointerUp={gestures.onPointerFinish}\n >\n <img\n alt={currentEntry.element.alt}\n className={styles.viewerImage}\n ref={imageRef}\n src={source}\n style={{\n cursor: gestures.cursor,\n height: imageRect.height,\n left: imageRect.x,\n top: imageRect.y,\n width: imageRect.width,\n }}\n onClick={gestures.onImageClick}\n onDoubleClick={gestures.onImageDoubleClick}\n onLoad={handleLoad}\n />\n <ViewerChrome\n canZoomIn={canZoomIn}\n canZoomOut={canZoomOut}\n chromeRef={chromeRef}\n current={currentIndex}\n fitRect={fitRect}\n flipHorizontal={flipHorizontal}\n flipVertical={flipVertical}\n hasNext={hasNext}\n hasPrev={hasPrev}\n natural={natural}\n next={next}\n prev={prev}\n reset={reset}\n rotateLeft={rotateLeft}\n rotateRight={rotateRight}\n rotation={rotation}\n scale={scale}\n source={source}\n toolbarAddon={currentEntry.options.toolbarAddon}\n total={entries.length}\n zoomIn={zoomIn}\n zoomOut={zoomOut}\n onClose={handleClose}\n />\n </Dialog.Popup>\n <ToastHost root={appElement ?? undefined} />\n </Dialog.Portal>\n </Dialog.Root>\n );\n});\n\nImageViewer.displayName = 'ImageViewer';\n\nexport default ImageViewer;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ImageViewer.mjs","names":[],"sources":["../../../src/Image/viewer/ImageViewer.tsx"],"sourcesContent":["'use client';\n\nimport { Dialog, type DialogRootProps } from '@base-ui/react/dialog';\nimport {\n memo,\n type SyntheticEvent,\n use,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport { useMergeRefs } from 'react-merge-refs';\n\nimport { ToastHost } from '@/base-ui/Toast';\nimport { useLayerZIndex } from '@/base-ui/zIndex';\nimport { MotionComponent } from '@/MotionProvider';\nimport { useAppElement } from '@/ThemeProvider';\n\nimport { styles } from '../style';\nimport { computeFit, type Size, unrotatedRect } from './geometry';\nimport { beginClosePreview, endClosePreview, type PreviewEntry } from './registry';\nimport { useFinalFocus } from './useFinalFocus';\nimport { useFlipTransition } from './useFlipTransition';\nimport { readNatural, useGalleryNav } from './useGalleryNav';\nimport { useRefitTransition } from './useRefitTransition';\nimport { useViewerGestures } from './useViewerGestures';\nimport { useZoomPan } from './useZoomPan';\nimport ViewerChrome from './ViewerChrome';\n\nexport interface ImageViewerProps {\n entries: PreviewEntry[];\n index: number;\n openerFocusElement: HTMLElement | null;\n token: number;\n}\n\nconst readViewport = (): Size => ({ height: window.innerHeight, width: window.innerWidth });\n\nconst prefersReducedMotion = () =>\n window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;\n\nconst ImageViewer = memo<ImageViewerProps>(({ entries, index, openerFocusElement, token }) => {\n const openerEntry = entries[index];\n const appElement = useAppElement();\n const motionComponent = use(MotionComponent);\n const [animated] = useState(() => !!motionComponent && !prefersReducedMotion());\n\n const { ref: layerRef, zIndex } = useLayerZIndex<HTMLDivElement>('modal');\n\n const [currentIndex, setCurrentIndex] = useState(index);\n const currentEntry = entries[currentIndex];\n const currentEntryRef = useRef(currentEntry);\n currentEntryRef.current = currentEntry;\n\n const [source, setSource] = useState(openerEntry.src);\n const [natural, setNatural] = useState<Size>(() => readNatural(openerEntry.element));\n const [viewport, setViewport] = useState<Size>(readViewport);\n\n const closeRef = useRef<(() => void) | null>(null);\n const requestClose = useCallback(() => closeRef.current?.(), []);\n\n // useZoomPan needs to gate its wheel/toolbar entry points on close-in-flight,\n // but that state only exists once useFlipTransition (below) has run — and it\n // needs useZoomPan's own transform values first. Break the cycle with a\n // stable indirection: the ref is repointed at the real isClosing once it's\n // available, written directly during render like the other *Ref fields here.\n const isClosingIndirectRef = useRef<() => boolean>(() => false);\n const isClosingIndirect = useCallback(() => isClosingIndirectRef.current(), []);\n\n const isTransitioningIndirectRef = useRef<() => boolean>(() => true);\n const isTransitioningIndirect = useCallback(() => isTransitioningIndirectRef.current(), []);\n\n const fillViewport = Boolean(\n currentEntry.previewSrc && currentEntry.previewSrc !== currentEntry.src,\n );\n\n const {\n canZoomIn,\n canZoomOut,\n dragBy,\n dragEnd,\n escIntent,\n flipHorizontal,\n flipVertical,\n flipX,\n flipY,\n getInitialScale,\n handleDoubleClick,\n handleWheel,\n isClean,\n isZoomed,\n reset,\n resetCloseTracking,\n rotate,\n rotateLeft,\n rotateRight,\n rotation,\n scale,\n setNatural: syncZoomNatural,\n setViewport: syncZoomViewport,\n toggleActualSize,\n x,\n y,\n zoomIn,\n zoomOut,\n } = useZoomPan({\n autoZoomThreshold: currentEntry.options.autoZoomThreshold,\n defaultZoom: currentEntry.options.defaultZoom,\n fillViewport,\n isClosing: isClosingIndirect,\n isTransitioning: isTransitioningIndirect,\n maxScale: currentEntry.options.maxScale,\n natural,\n onCloseRequest: requestClose,\n viewport,\n });\n\n const fitRect = useMemo(\n () => computeFit(natural, viewport, rotation, fillViewport),\n [natural, viewport, rotation, fillViewport],\n );\n const fitRectRef = useRef(fitRect);\n fitRectRef.current = fitRect;\n const getFitRect = useCallback(() => fitRectRef.current, []);\n\n const imageRect = useMemo(() => unrotatedRect(fitRect, rotation), [fitRect, rotation]);\n\n const viewportRef = useRef(viewport);\n viewportRef.current = viewport;\n const getViewportWidth = useCallback(() => viewportRef.current.width, []);\n\n const getCloseSource = useCallback(() => currentEntryRef.current.element, []);\n\n const transform = useMemo(\n () => ({ flipX, flipY, rotate, scale, x, y }),\n [flipX, flipY, rotate, scale, x, y],\n );\n const handleClosed = useCallback(() => endClosePreview(token), [token]);\n\n const {\n backdropRef,\n chromeRef,\n close,\n imageRef: flipImageRef,\n isClosing,\n isTransitioning,\n switchTo,\n } = useFlipTransition({\n animated,\n getCloseSource,\n getFitRect,\n getInitialScale,\n getViewportWidth,\n onClosed: handleClosed,\n source: openerEntry.element,\n transform,\n });\n isClosingIndirectRef.current = isClosing;\n isTransitioningIndirectRef.current = isTransitioning;\n\n useRefitTransition({\n animated,\n fillViewport,\n isTransitioning,\n natural,\n rotation,\n scale,\n viewport,\n x,\n y,\n });\n const finalFocus = useFinalFocus(openerFocusElement);\n\n const isCleanRef = useRef(isClean);\n isCleanRef.current = isClean;\n\n const handleClose = useCallback(() => {\n beginClosePreview(token);\n close({ fade: !isCleanRef.current });\n }, [close, token]);\n closeRef.current = handleClose;\n\n const handleDialogOpenChange = useCallback<NonNullable<DialogRootProps['onOpenChange']>>(\n (open, eventDetails) => {\n if (open) return;\n // Base UI fires this on every Esc regardless of whether we're already\n // mid-close (our Dialog.Root's open prop never actually flips to\n // false), so without this guard a second Esc during the close window\n // reads escIntent() off the close spring's mid-flight scale, decides\n // 'reset', and cancels the close animation out from under itself.\n if (isClosing()) return;\n if (eventDetails.reason === 'escape-key' && escIntent() === 'reset') {\n eventDetails.cancel();\n reset();\n return;\n }\n handleClose();\n },\n [escIntent, handleClose, isClosing, reset],\n );\n\n const gestures = useViewerGestures({\n dragBy,\n dragEnd,\n handleDoubleClick,\n handleWheel,\n isClean,\n isZoomed,\n onClose: handleClose,\n reset,\n zoomIn,\n zoomOut,\n });\n\n const { hasNext, hasPrev, next, prev } = useGalleryNav({\n cancelPendingClose: gestures.cancelPendingClose,\n currentIndex,\n entries,\n flipX,\n flipY,\n getInitialScale,\n resetCloseTracking,\n rotate,\n scale,\n setCurrentIndex,\n setNatural,\n setSource,\n switchTo,\n syncZoomNatural,\n x,\n y,\n });\n\n const popupNodeRef = useRef<HTMLDivElement | null>(null);\n const popupRef = useMergeRefs<HTMLDivElement>([layerRef, gestures.popupRef, popupNodeRef]);\n const imageRef = useMergeRefs<HTMLImageElement>([flipImageRef, gestures.imageRef]);\n\n const handleLoad = useCallback(\n (event: SyntheticEvent<HTMLImageElement>) => {\n const loaded = event.currentTarget;\n if (!loaded.naturalWidth || !loaded.naturalHeight) return;\n const next = { height: loaded.naturalHeight, width: loaded.naturalWidth };\n setNatural((prev) =>\n prev.width === next.width && prev.height === next.height ? prev : next,\n );\n syncZoomNatural(next);\n },\n [syncZoomNatural],\n );\n\n useEffect(() => {\n const handleResize = () => {\n const next = readViewport();\n setViewport(next);\n syncZoomViewport(next);\n };\n window.addEventListener('resize', handleResize);\n return () => window.removeEventListener('resize', handleResize);\n }, [syncZoomViewport]);\n\n useEffect(() => {\n const { previewSrc, src } = currentEntry;\n if (!previewSrc || previewSrc === src) return;\n let cancelled = false;\n const loader = new window.Image();\n const handleLoaded = () => {\n if (!cancelled) setSource(previewSrc);\n };\n loader.addEventListener('load', handleLoaded);\n loader.src = previewSrc;\n return () => {\n cancelled = true;\n loader.removeEventListener('load', handleLoaded);\n };\n }, [currentEntry]);\n\n useEffect(\n () => () => {\n const thumbnailWasRemoved = !openerEntry.element.isConnected;\n if (thumbnailWasRemoved) endClosePreview(token);\n },\n [openerEntry, token],\n );\n\n return (\n <Dialog.Root modal open onOpenChange={handleDialogOpenChange}>\n <Dialog.Portal container={appElement ?? undefined}>\n {/* Base UI drops a nested dialog's backdrop so overlays don't stack. The viewer is\n not an overlay on the surface it opened from — it replaces the screen — so it\n keeps its own scrim, or a viewer opened from a maskless drawer would show the\n page straight through the letterbox around the image. */}\n <Dialog.Backdrop\n forceRender\n className={styles.viewerBackdrop}\n ref={backdropRef}\n style={zIndex === undefined ? undefined : { zIndex }}\n onClick={gestures.onSurfaceClick}\n />\n <Dialog.Popup\n aria-label={currentEntry.element.alt || undefined}\n className={styles.viewerPopup}\n finalFocus={finalFocus}\n // Focus the popup itself instead of Base UI's default (the first\n // tabbable — the close button): a focus-visible close button pins\n // the idle auto-hide open forever and pops its tooltip on open.\n initialFocus={popupNodeRef}\n ref={popupRef}\n style={zIndex === undefined ? undefined : { zIndex: zIndex + 1 }}\n tabIndex={-1}\n onClick={gestures.onSurfaceClick}\n onPointerCancel={gestures.onPointerFinish}\n onPointerDown={gestures.onPointerDown}\n onPointerMove={gestures.onPointerMove}\n onPointerUp={gestures.onPointerFinish}\n >\n <img\n alt={currentEntry.element.alt}\n className={styles.viewerImage}\n ref={imageRef}\n src={source}\n style={{\n cursor: gestures.cursor,\n height: imageRect.height,\n left: imageRect.x,\n top: imageRect.y,\n width: imageRect.width,\n }}\n onClick={gestures.onImageClick}\n onDoubleClick={gestures.onImageDoubleClick}\n onLoad={handleLoad}\n />\n <ViewerChrome\n canZoomIn={canZoomIn}\n canZoomOut={canZoomOut}\n chromeRef={chromeRef}\n current={currentIndex}\n fitRect={fitRect}\n flipHorizontal={flipHorizontal}\n flipVertical={flipVertical}\n hasNext={hasNext}\n hasPrev={hasPrev}\n natural={natural}\n next={next}\n prev={prev}\n rotateLeft={rotateLeft}\n rotateRight={rotateRight}\n rotation={rotation}\n scale={scale}\n source={source}\n toggleActualSize={toggleActualSize}\n toolbarAddon={currentEntry.options.toolbarAddon}\n total={entries.length}\n zoomIn={zoomIn}\n zoomOut={zoomOut}\n onClose={handleClose}\n />\n </Dialog.Popup>\n <ToastHost root={appElement ?? undefined} />\n </Dialog.Portal>\n </Dialog.Root>\n );\n});\n\nImageViewer.displayName = 'ImageViewer';\n\nexport default ImageViewer;\n"],"mappings":"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"}
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import FlexBasic_default from "../../Flex/FlexBasic.mjs";
|
|
3
3
|
import Center from "../../Flex/Center.mjs";
|
|
4
|
-
import { Tooltip } from "../../base-ui/Tooltip/Tooltip.mjs";
|
|
5
4
|
import TooltipGroup from "../../base-ui/Tooltip/TooltipGroup.mjs";
|
|
6
5
|
import ActionIcon from "../../ActionIcon/ActionIcon.mjs";
|
|
7
6
|
import DropdownMenu from "../../base-ui/DropdownMenu/DropdownMenu.mjs";
|
|
8
7
|
import { useTranslation } from "../../i18n/useTranslation.mjs";
|
|
9
8
|
import { downloadBlob } from "../../utils/downloadBlob.mjs";
|
|
10
9
|
import { styles } from "../style.mjs";
|
|
11
|
-
import { toast } from "../../base-ui/Toast/imperative.mjs";
|
|
12
10
|
import { naturalScale } from "./geometry.mjs";
|
|
11
|
+
import { toast } from "../../base-ui/Toast/imperative.mjs";
|
|
13
12
|
import image_default from "../../i18n/resources/en/image.mjs";
|
|
14
13
|
import { getClipboardBlob } from "../../utils/blobToPng.mjs";
|
|
14
|
+
import ActualSizeIcon from "./ActualSizeIcon.mjs";
|
|
15
15
|
import { memo, useCallback, useMemo, useState, useSyncExternalStore } from "react";
|
|
16
16
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
17
17
|
import { Copy, Download, Ellipsis, FlipHorizontal, FlipVertical, RotateCcw, RotateCw, ZoomIn, ZoomOut } from "lucide-react";
|
|
@@ -39,13 +39,15 @@ const usePercentage = (scale, matchScale) => {
|
|
|
39
39
|
const getSnapshot = useCallback(() => Math.round(scale.get() / matchScale * 100), [matchScale, scale]);
|
|
40
40
|
return useSyncExternalStore(subscribe, getSnapshot);
|
|
41
41
|
};
|
|
42
|
-
const Toolbar = memo(({ canZoomIn, canZoomOut, fitRect, flipHorizontal, flipVertical, natural, onMoreOpenChange,
|
|
42
|
+
const Toolbar = memo(({ canZoomIn, canZoomOut, fitRect, flipHorizontal, flipVertical, natural, onMoreOpenChange, rotateLeft, rotateRight, rotation, scale, source, toggleActualSize, toolbarAddon, zoomIn, zoomOut }) => {
|
|
43
43
|
const { t } = useTranslation(image_default);
|
|
44
44
|
const [containerEl, setContainerEl] = useState(null);
|
|
45
45
|
const [copyLoading, setCopyLoading] = useState(false);
|
|
46
46
|
const [downloadLoading, setDownloadLoading] = useState(false);
|
|
47
47
|
const matchScale = naturalScale(natural, fitRect, rotation);
|
|
48
48
|
const percentage = usePercentage(scale, matchScale);
|
|
49
|
+
const canToggleActualSize = Math.abs(matchScale - 1) > .01;
|
|
50
|
+
const showFitAffordance = canToggleActualSize && percentage === 100;
|
|
49
51
|
const handleDownload = useCallback(async () => {
|
|
50
52
|
setDownloadLoading(true);
|
|
51
53
|
try {
|
|
@@ -134,16 +136,10 @@ const Toolbar = memo(({ canZoomIn, canZoomOut, fitRect, flipHorizontal, flipVert
|
|
|
134
136
|
title: t("image.zoomOut"),
|
|
135
137
|
onClick: zoomOut
|
|
136
138
|
}),
|
|
137
|
-
/* @__PURE__ */
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
className: styles.toolbarPercentage,
|
|
142
|
-
role: "button",
|
|
143
|
-
tabIndex: 0,
|
|
144
|
-
onClick: reset,
|
|
145
|
-
children: [percentage, "%"]
|
|
146
|
-
})
|
|
139
|
+
/* @__PURE__ */ jsxs(Center, {
|
|
140
|
+
horizontal: true,
|
|
141
|
+
className: styles.toolbarPercentage,
|
|
142
|
+
children: [percentage, "%"]
|
|
147
143
|
}),
|
|
148
144
|
/* @__PURE__ */ jsx(ActionIcon, {
|
|
149
145
|
disabled: !canZoomIn,
|
|
@@ -152,6 +148,14 @@ const Toolbar = memo(({ canZoomIn, canZoomOut, fitRect, flipHorizontal, flipVert
|
|
|
152
148
|
title: t("image.zoomIn"),
|
|
153
149
|
onClick: zoomIn
|
|
154
150
|
}),
|
|
151
|
+
/* @__PURE__ */ jsx(ActionIcon, {
|
|
152
|
+
"data-actual-size": showFitAffordance ? "fit" : "actual",
|
|
153
|
+
disabled: !canToggleActualSize,
|
|
154
|
+
icon: ActualSizeIcon,
|
|
155
|
+
style: { borderRadius: 999 },
|
|
156
|
+
title: showFitAffordance ? t("image.fitToScreen") : t("image.actualSize"),
|
|
157
|
+
onClick: toggleActualSize
|
|
158
|
+
}),
|
|
155
159
|
/* @__PURE__ */ jsx(ActionIcon, {
|
|
156
160
|
icon: Download,
|
|
157
161
|
loading: downloadLoading,
|
|
@@ -1 +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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"}
|
|
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 { TooltipGroup } from '@/Tooltip';\nimport { getClipboardBlob } from '@/utils/blobToPng';\nimport { downloadBlob } from '@/utils/downloadBlob';\n\nimport { styles } from '../style';\nimport ActualSizeIcon from './ActualSizeIcon';\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 rotateLeft: () => void;\n rotateRight: () => void;\n rotation: Rotation;\n scale: MotionValue<number>;\n source: string;\n toggleActualSize: () => void;\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 rotateLeft,\n rotateRight,\n rotation,\n scale,\n source,\n toggleActualSize,\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 // An image small enough that computeFit never scaled it down is already at\n // 100% when fitted, so the two states the toggle switches between are the\n // same state and the control has nothing to do.\n const canToggleActualSize = Math.abs(matchScale - 1) > 0.01;\n // The control reads as \"go to actual size\" by default, and only flips to\n // the fit affordance when it can actually take you back — an image that\n // fits at 100% is at both states at once and would otherwise offer to\n // \"fit to screen\" while already fitted.\n const showFitAffordance = canToggleActualSize && percentage === 100;\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 <Center horizontal className={styles.toolbarPercentage}>\n {percentage}%\n </Center>\n <ActionIcon\n disabled={!canZoomIn}\n icon={ZoomIn}\n style={{ borderRadius: 999 }}\n title={t('image.zoomIn')}\n onClick={zoomIn}\n />\n <ActionIcon\n data-actual-size={showFitAffordance ? 'fit' : 'actual'}\n disabled={!canToggleActualSize}\n icon={ActualSizeIcon}\n style={{ borderRadius: 999 }}\n title={showFitAffordance ? t('image.fitToScreen') : t('image.actualSize')}\n onClick={toggleActualSize}\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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"}
|
|
@@ -1 +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
|
|
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 rotateLeft: () => void;\n rotateRight: () => void;\n rotation: Rotation;\n scale: MotionValue<number>;\n source: string;\n toggleActualSize: () => void;\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"}
|
|
@@ -74,12 +74,24 @@ const rubberBand = (value, min, max, factor = DEFAULT_RUBBER_BAND_FACTOR) => {
|
|
|
74
74
|
};
|
|
75
75
|
const clampScale = (scale, maxScale = 8) => Math.min(Math.max(scale, 1), maxScale);
|
|
76
76
|
const wheelZoomFactor = (deltaY) => Math.exp(-deltaY * WHEEL_ZOOM_SENSITIVITY);
|
|
77
|
+
const normalizeWheelDelta = (deltaY, deltaMode = 0, viewportHeight = 0) => {
|
|
78
|
+
if (deltaMode === 1) return deltaY * 16;
|
|
79
|
+
if (deltaMode === 2) return deltaY * (viewportHeight || 640);
|
|
80
|
+
return deltaY;
|
|
81
|
+
};
|
|
77
82
|
const naturalScale = (natural, fitRect, rotate) => effectiveSize(natural, rotate).width / fitRect.width;
|
|
83
|
+
const resolveInitialScale = (policy, natural, fitRect, rotate, threshold = 2) => {
|
|
84
|
+
if (policy === "fit") return 1;
|
|
85
|
+
const target = naturalScale(natural, fitRect, rotate);
|
|
86
|
+
if (!Number.isFinite(target) || target <= 0) return 1;
|
|
87
|
+
if (policy === "auto" && target > threshold) return 1;
|
|
88
|
+
return Math.max(1, target);
|
|
89
|
+
};
|
|
78
90
|
const doubleClickTarget = (currentScale, natural, fitRect, rotate) => {
|
|
79
91
|
if (currentScale > 1) return 1;
|
|
80
92
|
return Math.max(2, naturalScale(natural, fitRect, rotate));
|
|
81
93
|
};
|
|
82
94
|
//#endregion
|
|
83
|
-
export { DEFAULT_RUBBER_BAND_FACTOR, WHEEL_ZOOM_SENSITIVITY, anchoredZoom, clampPan, clampScale, computeFit, doubleClickTarget, naturalScale, normalizeRotation, panBounds, rubberBand, unrotatedRect, wheelZoomFactor };
|
|
95
|
+
export { DEFAULT_RUBBER_BAND_FACTOR, WHEEL_ZOOM_SENSITIVITY, anchoredZoom, clampPan, clampScale, computeFit, doubleClickTarget, naturalScale, normalizeRotation, normalizeWheelDelta, panBounds, resolveInitialScale, rubberBand, unrotatedRect, wheelZoomFactor };
|
|
84
96
|
|
|
85
97
|
//# sourceMappingURL=geometry.mjs.map
|