@lobehub/ui 5.26.0 → 5.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +6 -6
  2. package/es/Image/Image.mjs +3 -2
  3. package/es/Image/Image.mjs.map +1 -1
  4. package/es/Image/style.mjs +19 -13
  5. package/es/Image/style.mjs.map +1 -1
  6. package/es/Image/type.d.mts +3 -0
  7. package/es/Image/viewer/ActualSizeIcon.mjs +44 -0
  8. package/es/Image/viewer/ActualSizeIcon.mjs.map +1 -0
  9. package/es/Image/viewer/ImageViewer.mjs +12 -3
  10. package/es/Image/viewer/ImageViewer.mjs.map +1 -1
  11. package/es/Image/viewer/Toolbar.mjs +17 -13
  12. package/es/Image/viewer/Toolbar.mjs.map +1 -1
  13. package/es/Image/viewer/ViewerChrome.mjs.map +1 -1
  14. package/es/Image/viewer/geometry.d.mts +5 -0
  15. package/es/Image/viewer/geometry.mjs +13 -1
  16. package/es/Image/viewer/geometry.mjs.map +1 -1
  17. package/es/Image/viewer/registry.mjs.map +1 -1
  18. package/es/Image/viewer/useFlipTransition.mjs +7 -4
  19. package/es/Image/viewer/useFlipTransition.mjs.map +1 -1
  20. package/es/Image/viewer/useGalleryNav.mjs +6 -5
  21. package/es/Image/viewer/useGalleryNav.mjs.map +1 -1
  22. package/es/Image/viewer/useViewerGestures.mjs +2 -6
  23. package/es/Image/viewer/useViewerGestures.mjs.map +1 -1
  24. package/es/Image/viewer/useZoomPan.mjs +80 -23
  25. package/es/Image/viewer/useZoomPan.mjs.map +1 -1
  26. package/es/Markdown/SyntaxMarkdown/CachedMarkdown.mjs +1 -1
  27. package/es/Markdown/plugins/rehypeCustomFootnotes.mjs +1 -1
  28. package/es/Markdown/plugins/rehypeKatexDir.d.mts +1 -1
  29. package/es/Markdown/plugins/rehypeKatexDir.mjs +1 -1
  30. package/es/Markdown/plugins/rehypeStreamAnimated.d.mts +1 -1
  31. package/es/Markdown/plugins/rehypeStreamAnimated.mjs +1 -1
  32. package/es/Markdown/plugins/remarkBr.mjs +1 -1
  33. package/es/Markdown/plugins/remarkColor.mjs +1 -1
  34. package/es/Markdown/plugins/remarkCustomFootnotes.mjs +2 -2
  35. package/es/Markdown/plugins/remarkGfmPlus.mjs +1 -1
  36. package/es/Markdown/plugins/remarkVideo.mjs +1 -1
  37. package/es/base-ui/Toast/imperative.mjs +30 -15
  38. package/es/base-ui/Toast/imperative.mjs.map +1 -1
  39. package/es/base-ui/Toast/type.d.mts +7 -0
  40. package/es/i18n/resources/en/image.d.mts +2 -1
  41. package/es/i18n/resources/en/image.mjs +3 -2
  42. package/es/i18n/resources/en/image.mjs.map +1 -1
  43. package/es/i18n/resources/zhCn/image.d.mts +2 -1
  44. package/es/i18n/resources/zhCn/image.mjs +3 -2
  45. package/es/i18n/resources/zhCn/image.mjs.map +1 -1
  46. package/es/index.d.mts +2 -2
  47. package/es/mdx/mdxComponents/index.d.mts +1 -1
  48. package/es/node_modules/{.bun → .pnpm}/@types_hast@3.0.5/node_modules/@types/hast/index.d.mts +1 -1
  49. package/es/node_modules/{.bun → .pnpm}/@types_mdx@2.0.14/node_modules/@types/mdx/types.d.mts +1 -1
  50. package/es/node_modules/{.bun → .pnpm}/@types_unist@3.0.3/node_modules/@types/unist/index.d.mts +1 -1
  51. package/es/node_modules/{.bun → .pnpm}/unist-util-is@6.0.1/node_modules/unist-util-is/lib/index.mjs +1 -1
  52. package/es/node_modules/.pnpm/unist-util-is@6.0.1/node_modules/unist-util-is/lib/index.mjs.map +1 -0
  53. package/es/node_modules/.pnpm/unist-util-visit-parents@6.0.2/node_modules/unist-util-visit-parents/lib/color.node.mjs +12 -0
  54. 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
  55. package/es/node_modules/{.bun → .pnpm}/unist-util-visit-parents@6.0.2/node_modules/unist-util-visit-parents/lib/index.mjs +1 -1
  56. package/es/node_modules/.pnpm/unist-util-visit-parents@6.0.2/node_modules/unist-util-visit-parents/lib/index.mjs.map +1 -0
  57. package/es/node_modules/{.bun → .pnpm}/unist-util-visit@5.1.0/node_modules/unist-util-visit/lib/index.mjs +1 -1
  58. package/es/node_modules/.pnpm/unist-util-visit@5.1.0/node_modules/unist-util-visit/lib/index.mjs.map +1 -0
  59. package/package.json +4 -5
  60. package/es/node_modules/.bun/unist-util-is@6.0.1/node_modules/unist-util-is/lib/index.mjs.map +0 -1
  61. package/es/node_modules/.bun/unist-util-visit-parents@6.0.2/node_modules/unist-util-visit-parents/lib/color.node.mjs +0 -12
  62. 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
  63. package/es/node_modules/.bun/unist-util-visit-parents@6.0.2/node_modules/unist-util-visit-parents/lib/index.mjs.map +0 -1
  64. 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
- [![][bun-shield]][bun-link]
63
+ [![][pnpm-shield]][pnpm-link]
64
64
 
65
65
  ```bash
66
- $ bun add @lobehub/ui
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
- $ bun install
170
- $ bun start
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
- [bun-link]: https://bun.sh
253
- [bun-shield]: https://img.shields.io/badge/-speedup%20with%20bun-black?logo=bun&style=for-the-badge
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
@@ -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
- maxScale: DEFAULT_MAX_SCALE,
39
+ autoZoomThreshold: 2,
40
+ defaultZoom: "auto",
41
+ maxScale: 8,
41
42
  ...resolvedPreview
42
43
  } : void 0, [previewEnabled, resolvedPreview]);
43
44
  const previewSrc = resolvedOptions?.src;
@@ -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 DEFAULT_MAX_SCALE = 8;\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 () => (previewEnabled ? { maxScale: DEFAULT_MAX_SCALE, ...resolvedPreview } : 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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"}
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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"}
@@ -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: 44px;
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;
@@ -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 toolbarPercentage: css`\n cursor: pointer;\n user-select: none;\n\n min-width: 44px;\n height: 36px;\n padding-inline: 8px;\n border-radius: 999px;\n\n font-size: 12px;\n font-variant-numeric: tabular-nums;\n color: ${cssVar.colorTextTertiary};\n\n transition:\n color 400ms ${cssVar.motionEaseOut},\n background 100ms ${cssVar.motionEaseOut};\n\n &:hover {\n color: ${cssVar.colorTextSecondary};\n background: ${cssVar.colorFillTertiary};\n }\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,mBAAmB,GAAG;;;;;;;;;;;eAWX,OAAO,kBAAkB;;;sBAGlB,OAAO,cAAc;2BAChB,OAAO,cAAc;;;iBAG/B,OAAO,mBAAmB;sBACrB,OAAO,kBAAkB;;;EAI3C,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"}
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"}
@@ -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, reset, rotateLeft, rotateRight, rotation, scale, source, toolbarAddon, zoomIn, zoomOut }) => {
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__ */ 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
- })
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 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"}
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"}
@@ -0,0 +1,5 @@
1
+ //#region src/Image/viewer/geometry.d.ts
2
+ type ZoomPolicy = 'auto' | 'actual' | 'fit';
3
+ //#endregion
4
+ export { ZoomPolicy };
5
+ //# sourceMappingURL=geometry.d.mts.map
@@ -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