@feugene/granularity 0.9.2 → 0.9.4
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/dist/chunks/grSelectStyles-pUt19VGy.js.map +1 -1
- package/dist/components/GrImageViewer/chunks/{GrImageViewer-Cn1X-sWS.js → GrImageViewer-CsvXQHM9.js} +203 -50
- package/dist/components/GrImageViewer/chunks/GrImageViewer-CsvXQHM9.js.map +1 -0
- package/dist/components/GrImageViewer/index.js +1 -1
- package/dist/components/GrPagination/chunks/{GrPagination-cD6rFO9W.js → GrPagination-BNDEqM8_.js} +1 -1
- package/dist/components/GrPagination/chunks/{GrPagination-cD6rFO9W.js.map → GrPagination-BNDEqM8_.js.map} +1 -1
- package/dist/components/GrPagination/index.js +1 -1
- package/dist/components/GrSelect/chunks/{GrSelect-W9lbAjRZ.js → GrSelect-CnHdbPlf.js} +108 -56
- package/dist/components/GrSelect/chunks/GrSelect-CnHdbPlf.js.map +1 -0
- package/dist/components/GrSelect/index.js +1 -1
- package/dist/index.js +3 -3
- package/dist/types/src/components/GrImageViewer/GrImageViewer.vue.d.ts +30 -0
- package/dist/types/src/components/GrSelect/GrSelect.vue.d.ts +5 -4
- package/dist/types/src/components/GrSelect/grSelectStyles.d.ts +7 -0
- package/dist/types/src/components/GrSelect/index.d.ts +1 -1
- package/package.json +1 -1
- package/dist/components/GrImageViewer/chunks/GrImageViewer-Cn1X-sWS.js.map +0 -1
- package/dist/components/GrSelect/chunks/GrSelect-W9lbAjRZ.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grSelectStyles-pUt19VGy.js","names":[],"sources":["../../src/components/GrSelect/grSelectStyles.ts"],"sourcesContent":["export type GrSelectView = 'default' | 'link'\nexport type GrSelectSize = 'xs' | 'sm' | 'md' | 'lg'\nexport type GrSelectVariant = 'primary' | 'default' | 'muted' | 'danger'\nexport type GrSelectUnderline = 'auto' | 'always' | 'none'\nexport type GrSelectOptionsView = 'native' | 'panel'\nexport type GrSelectOption = { value: string, label: string }\nexport type GrSelectModelValue = string | string[]\n\nexport const defaultBaseClass = 'w-full rounded-md border border-[var(--brd)] bg-[var(--bg)] text-[var(--fg)] transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)]'\nexport const linkBaseClass = 'cursor-pointer inline-block w-auto align-baseline appearance-none bg-transparent border border-transparent px-0 py-0 rounded-[6px] transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)]'\n\n\nexport const selectSizeClassBySize: Record<GrSelectSize, string> = {\n xs: 'h-7 px-2.5 text-[12px]',\n sm: 'h-8 px-3 text-[13px]',\n md: 'h-10 px-3 text-[14px]',\n lg: 'h-11 px-4 text-[16px]',\n}\n\nexport const selectLinkSizeClassBySize: Record<GrSelectSize, string> = {\n xs: 'text-[12px]',\n sm: 'text-[13px]',\n md: 'text-[14px]',\n lg: 'text-[16px]',\n}\n\nexport const selectLinkVariantClassByVariant: Record<GrSelectVariant, string> = {\n primary: 'text-[var(--primary)] hover:text-[var(--primary-hover)] active:text-[var(--primary-active)]',\n default: 'text-[var(--fg)] hover:text-[var(--primary)] active:text-[var(--primary-active)]',\n muted: 'text-[var(--muted-fg)] hover:text-[var(--fg)] active:text-[var(--fg)]',\n danger: 'text-[var(--ds-danger)] hover:text-[var(--ds-danger-hover)] active:text-[var(--ds-danger-active)]',\n}\n\n/**\n * Variant-классы для видимой метки в `view=\"link\"` + `optionsView=\"native\"`.\n * Hover-цвет приходит через `peer-hover:` от прозрачного `<select>`-overlay,\n * т.к. видимая метка имеет `pointer-events-none` и не получает hover напрямую.\n */\nexport const selectLinkNativeLabelVariantClassByVariant: Record<GrSelectVariant, string> = {\n primary: 'text-[var(--primary)] peer-hover:text-[var(--primary-hover)] peer-active:text-[var(--primary-active)]',\n default: 'text-[var(--fg)] peer-hover:text-[var(--primary)] peer-active:text-[var(--primary-active)]',\n muted: 'text-[var(--muted-fg)] peer-hover:text-[var(--fg)] peer-active:text-[var(--fg)]',\n danger: 'text-[var(--ds-danger)] peer-hover:text-[var(--ds-danger-hover)] peer-active:text-[var(--ds-danger-active)]',\n}\n\nfunction selectLinkUnderlineClass(options: { underline: GrSelectUnderline, disabled: boolean }): string {\n if (options.disabled) return 'no-underline'\n if (options.underline === 'always') return 'underline underline-offset-4'\n if (options.underline === 'none') return 'no-underline'\n return 'no-underline hover:underline hover:underline-offset-4'\n}\n\n/**\n * Underline-классы для видимой метки в `view=\"link\"` + `optionsView=\"native\"`.\n * Видимая метка имеет `pointer-events-none`, а hover получает прозрачный\n * `<select>`-overlay (peer), поэтому для `underline=\"auto\"` используем\n * `peer-hover:underline` вместо `hover:underline`.\n */\nfunction selectLinkNativeLabelUnderlineClass(options: { underline: GrSelectUnderline, disabled: boolean }): string {\n if (options.disabled) return 'no-underline'\n if (options.underline === 'always') return 'underline underline-offset-4'\n if (options.underline === 'none') return 'no-underline'\n return 'no-underline peer-hover:underline peer-hover:underline-offset-4'\n}\n\n/**\n * Классы прозрачного `<select>`-overlay для `view=\"link\"` + `optionsView=\"native\"`.\n * Нативный селект растягивается на всю обёртку, но визуально невидим — ширину обёртки\n * задаёт видимый `<span>` с выбранной меткой, благодаря чему компонент в закрытом\n * состоянии занимает столько места, сколько нужно текущей опции (а не самой длинной).\n */\nexport const grSelectLinkNativeOverlayClass = 'peer absolute inset-0 w-full h-full m-0 p-0 border-0 bg-transparent text-transparent appearance-none opacity-0 cursor-pointer disabled:cursor-not-allowed focus:outline-none'\n\n/**\n * Классы видимой метки для `view=\"link\"` + `optionsView=\"native\"` (поверх прозрачного `<select>`).\n * Получает все link-стили (size/variant/underline), а также focus-ring через `peer-focus-visible`.\n */\nexport function grSelectLinkNativeLabelClass(options: {\n size: GrSelectSize\n variant: GrSelectVariant\n underline: GrSelectUnderline\n disabled: boolean\n}): string {\n return [\n 'pointer-events-none inline-block whitespace-nowrap align-baseline rounded-[6px] transition-colors duration-150',\n selectLinkSizeClassBySize[options.size],\n selectLinkNativeLabelUnderlineClass({ underline: options.underline, disabled: options.disabled }),\n selectLinkNativeLabelVariantClassByVariant[options.variant],\n options.disabled ? 'opacity-60 text-[var(--muted-fg)] no-underline' : '',\n 'peer-focus-visible:outline-none peer-focus-visible:ring-2 peer-focus-visible:ring-[var(--ring)]',\n ]\n .filter(Boolean)\n .join(' ')\n}\n\nexport function grSelectClass(options: {\n view: GrSelectView\n size: GrSelectSize\n disabled: boolean\n variant: GrSelectVariant\n underline: GrSelectUnderline\n}): string {\n if (options.view === 'link') {\n return [\n selectLinkSizeClassBySize[options.size],\n selectLinkUnderlineClass({ underline: options.underline, disabled: options.disabled }),\n selectLinkVariantClassByVariant[options.variant],\n 'disabled:opacity-60 disabled:cursor-not-allowed disabled:text-[var(--muted-fg)] disabled:no-underline',\n ].join(' ')\n }\n\n return [\n selectSizeClassBySize[options.size],\n 'disabled:opacity-50 disabled:cursor-not-allowed',\n ].join(' ')\n}\n\nexport function grSelectNativeClass(options: {\n view: GrSelectView\n size: GrSelectSize\n disabled: boolean\n variant: GrSelectVariant\n underline: GrSelectUnderline\n showNativeChevron: boolean\n}): string {\n return [\n grSelectClass(options),\n options.showNativeChevron ? 'appearance-none pr-9' : '',\n ]\n .filter(Boolean)\n .join(' ')\n}\n\nexport function grSelectTriggerClass(options: {\n view: GrSelectView\n optionsView: GrSelectOptionsView\n size: GrSelectSize\n disabled: boolean\n variant: GrSelectVariant\n underline: GrSelectUnderline\n}): string {\n if (options.optionsView !== 'panel') {\n return grSelectClass(options)\n }\n\n if (options.view === 'link') {\n return [\n grSelectClass(options),\n 'inline-flex items-center gap-1 text-left',\n ].join(' ')\n }\n\n return [\n grSelectClass(options),\n 'flex items-center justify-between text-left',\n ].join(' ')\n}\n\nexport const grSelectPanelClasses = 'rounded-[var(--ds-radius-xl)] border border-[var(--brd)] bg-[var(--card)] text-[var(--card-fg)] shadow-[var(--ds-shadow-2)] overflow-hidden'\n"],"mappings":";
|
|
1
|
+
{"version":3,"file":"grSelectStyles-pUt19VGy.js","names":[],"sources":["../../src/components/GrSelect/grSelectStyles.ts"],"sourcesContent":["export type GrSelectView = 'default' | 'link'\nexport type GrSelectSize = 'xs' | 'sm' | 'md' | 'lg'\nexport type GrSelectVariant = 'primary' | 'default' | 'muted' | 'danger'\nexport type GrSelectUnderline = 'auto' | 'always' | 'none'\nexport type GrSelectOptionsView = 'native' | 'panel'\nexport type GrSelectOption = { value: string, label: string }\n/** Группа опций: заголовок `label` + вложенные опции `options`. */\nexport type GrSelectOptionGroup = { label: string, options: GrSelectOption[] }\n/** Элемент списка опций: либо одиночная опция, либо группа опций. */\nexport type GrSelectOptionOrGroup = GrSelectOption | GrSelectOptionGroup\nexport type GrSelectModelValue = string | string[]\n\nexport const defaultBaseClass = 'w-full rounded-md border border-[var(--brd)] bg-[var(--bg)] text-[var(--fg)] transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)]'\nexport const linkBaseClass = 'cursor-pointer inline-block w-auto align-baseline appearance-none bg-transparent border border-transparent px-0 py-0 rounded-[6px] transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)]'\n\n\nexport const selectSizeClassBySize: Record<GrSelectSize, string> = {\n xs: 'h-7 px-2.5 text-[12px]',\n sm: 'h-8 px-3 text-[13px]',\n md: 'h-10 px-3 text-[14px]',\n lg: 'h-11 px-4 text-[16px]',\n}\n\nexport const selectLinkSizeClassBySize: Record<GrSelectSize, string> = {\n xs: 'text-[12px]',\n sm: 'text-[13px]',\n md: 'text-[14px]',\n lg: 'text-[16px]',\n}\n\nexport const selectLinkVariantClassByVariant: Record<GrSelectVariant, string> = {\n primary: 'text-[var(--primary)] hover:text-[var(--primary-hover)] active:text-[var(--primary-active)]',\n default: 'text-[var(--fg)] hover:text-[var(--primary)] active:text-[var(--primary-active)]',\n muted: 'text-[var(--muted-fg)] hover:text-[var(--fg)] active:text-[var(--fg)]',\n danger: 'text-[var(--ds-danger)] hover:text-[var(--ds-danger-hover)] active:text-[var(--ds-danger-active)]',\n}\n\n/**\n * Variant-классы для видимой метки в `view=\"link\"` + `optionsView=\"native\"`.\n * Hover-цвет приходит через `peer-hover:` от прозрачного `<select>`-overlay,\n * т.к. видимая метка имеет `pointer-events-none` и не получает hover напрямую.\n */\nexport const selectLinkNativeLabelVariantClassByVariant: Record<GrSelectVariant, string> = {\n primary: 'text-[var(--primary)] peer-hover:text-[var(--primary-hover)] peer-active:text-[var(--primary-active)]',\n default: 'text-[var(--fg)] peer-hover:text-[var(--primary)] peer-active:text-[var(--primary-active)]',\n muted: 'text-[var(--muted-fg)] peer-hover:text-[var(--fg)] peer-active:text-[var(--fg)]',\n danger: 'text-[var(--ds-danger)] peer-hover:text-[var(--ds-danger-hover)] peer-active:text-[var(--ds-danger-active)]',\n}\n\nfunction selectLinkUnderlineClass(options: { underline: GrSelectUnderline, disabled: boolean }): string {\n if (options.disabled) return 'no-underline'\n if (options.underline === 'always') return 'underline underline-offset-4'\n if (options.underline === 'none') return 'no-underline'\n return 'no-underline hover:underline hover:underline-offset-4'\n}\n\n/**\n * Underline-классы для видимой метки в `view=\"link\"` + `optionsView=\"native\"`.\n * Видимая метка имеет `pointer-events-none`, а hover получает прозрачный\n * `<select>`-overlay (peer), поэтому для `underline=\"auto\"` используем\n * `peer-hover:underline` вместо `hover:underline`.\n */\nfunction selectLinkNativeLabelUnderlineClass(options: { underline: GrSelectUnderline, disabled: boolean }): string {\n if (options.disabled) return 'no-underline'\n if (options.underline === 'always') return 'underline underline-offset-4'\n if (options.underline === 'none') return 'no-underline'\n return 'no-underline peer-hover:underline peer-hover:underline-offset-4'\n}\n\n/**\n * Классы прозрачного `<select>`-overlay для `view=\"link\"` + `optionsView=\"native\"`.\n * Нативный селект растягивается на всю обёртку, но визуально невидим — ширину обёртки\n * задаёт видимый `<span>` с выбранной меткой, благодаря чему компонент в закрытом\n * состоянии занимает столько места, сколько нужно текущей опции (а не самой длинной).\n */\nexport const grSelectLinkNativeOverlayClass = 'peer absolute inset-0 w-full h-full m-0 p-0 border-0 bg-transparent text-transparent appearance-none opacity-0 cursor-pointer disabled:cursor-not-allowed focus:outline-none'\n\n/**\n * Классы видимой метки для `view=\"link\"` + `optionsView=\"native\"` (поверх прозрачного `<select>`).\n * Получает все link-стили (size/variant/underline), а также focus-ring через `peer-focus-visible`.\n */\nexport function grSelectLinkNativeLabelClass(options: {\n size: GrSelectSize\n variant: GrSelectVariant\n underline: GrSelectUnderline\n disabled: boolean\n}): string {\n return [\n 'pointer-events-none inline-block whitespace-nowrap align-baseline rounded-[6px] transition-colors duration-150',\n selectLinkSizeClassBySize[options.size],\n selectLinkNativeLabelUnderlineClass({ underline: options.underline, disabled: options.disabled }),\n selectLinkNativeLabelVariantClassByVariant[options.variant],\n options.disabled ? 'opacity-60 text-[var(--muted-fg)] no-underline' : '',\n 'peer-focus-visible:outline-none peer-focus-visible:ring-2 peer-focus-visible:ring-[var(--ring)]',\n ]\n .filter(Boolean)\n .join(' ')\n}\n\nexport function grSelectClass(options: {\n view: GrSelectView\n size: GrSelectSize\n disabled: boolean\n variant: GrSelectVariant\n underline: GrSelectUnderline\n}): string {\n if (options.view === 'link') {\n return [\n selectLinkSizeClassBySize[options.size],\n selectLinkUnderlineClass({ underline: options.underline, disabled: options.disabled }),\n selectLinkVariantClassByVariant[options.variant],\n 'disabled:opacity-60 disabled:cursor-not-allowed disabled:text-[var(--muted-fg)] disabled:no-underline',\n ].join(' ')\n }\n\n return [\n selectSizeClassBySize[options.size],\n 'disabled:opacity-50 disabled:cursor-not-allowed',\n ].join(' ')\n}\n\nexport function grSelectNativeClass(options: {\n view: GrSelectView\n size: GrSelectSize\n disabled: boolean\n variant: GrSelectVariant\n underline: GrSelectUnderline\n showNativeChevron: boolean\n}): string {\n return [\n grSelectClass(options),\n options.showNativeChevron ? 'appearance-none pr-9' : '',\n ]\n .filter(Boolean)\n .join(' ')\n}\n\nexport function grSelectTriggerClass(options: {\n view: GrSelectView\n optionsView: GrSelectOptionsView\n size: GrSelectSize\n disabled: boolean\n variant: GrSelectVariant\n underline: GrSelectUnderline\n}): string {\n if (options.optionsView !== 'panel') {\n return grSelectClass(options)\n }\n\n if (options.view === 'link') {\n return [\n grSelectClass(options),\n 'inline-flex items-center gap-1 text-left',\n ].join(' ')\n }\n\n return [\n grSelectClass(options),\n 'flex items-center justify-between text-left',\n ].join(' ')\n}\n\nexport const grSelectPanelClasses = 'rounded-[var(--ds-radius-xl)] border border-[var(--brd)] bg-[var(--card)] text-[var(--card-fg)] shadow-[var(--ds-shadow-2)] overflow-hidden'\n"],"mappings":";AAYA,IAAa,mBAAmB;AAChC,IAAa,gBAAgB;AAG7B,IAAa,wBAAsD;CACjE,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACL;AAED,IAAa,4BAA0D;CACrE,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACL;AAED,IAAa,kCAAmE;CAC9E,SAAS;CACT,SAAS;CACT,OAAO;CACP,QAAQ;CACT;;;;;;AAOD,IAAa,6CAA8E;CACzF,SAAS;CACT,SAAS;CACT,OAAO;CACP,QAAQ;CACT;AAED,SAAS,yBAAyB,SAAsE;AACtG,KAAI,QAAQ,SAAU,QAAO;AAC7B,KAAI,QAAQ,cAAc,SAAU,QAAO;AAC3C,KAAI,QAAQ,cAAc,OAAQ,QAAO;AACzC,QAAO;;;;;;;;AAST,SAAS,oCAAoC,SAAsE;AACjH,KAAI,QAAQ,SAAU,QAAO;AAC7B,KAAI,QAAQ,cAAc,SAAU,QAAO;AAC3C,KAAI,QAAQ,cAAc,OAAQ,QAAO;AACzC,QAAO;;;;;;;;AAST,IAAa,iCAAiC;;;;;AAM9C,SAAgB,6BAA6B,SAKlC;AACT,QAAO;EACL;EACA,0BAA0B,QAAQ;EAClC,oCAAoC;GAAE,WAAW,QAAQ;GAAW,UAAU,QAAQ;GAAU,CAAC;EACjG,2CAA2C,QAAQ;EACnD,QAAQ,WAAW,mDAAmD;EACtE;EACD,CACE,OAAO,QAAQ,CACf,KAAK,IAAI;;AAGd,SAAgB,cAAc,SAMnB;AACT,KAAI,QAAQ,SAAS,OACnB,QAAO;EACL,0BAA0B,QAAQ;EAClC,yBAAyB;GAAE,WAAW,QAAQ;GAAW,UAAU,QAAQ;GAAU,CAAC;EACtF,gCAAgC,QAAQ;EACxC;EACD,CAAC,KAAK,IAAI;AAGb,QAAO,CACL,sBAAsB,QAAQ,OAC9B,kDACD,CAAC,KAAK,IAAI;;AAGb,SAAgB,oBAAoB,SAOzB;AACT,QAAO,CACL,cAAc,QAAQ,EACtB,QAAQ,oBAAoB,yBAAyB,GACtD,CACE,OAAO,QAAQ,CACf,KAAK,IAAI;;AAGd,SAAgB,qBAAqB,SAO1B;AACT,KAAI,QAAQ,gBAAgB,QAC1B,QAAO,cAAc,QAAQ;AAG/B,KAAI,QAAQ,SAAS,OACnB,QAAO,CACL,cAAc,QAAQ,EACtB,2CACD,CAAC,KAAK,IAAI;AAGb,QAAO,CACL,cAAc,QAAQ,EACtB,8CACD,CAAC,KAAK,IAAI;;AAGb,IAAa,uBAAuB"}
|
package/dist/components/GrImageViewer/chunks/{GrImageViewer-Cn1X-sWS.js → GrImageViewer-CsvXQHM9.js}
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as he, o as Ye, r as Ge, t as Se } from "../../../chunks/transition-mJ3PZCHH.js";
|
|
2
|
-
import { Fragment, Teleport, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, normalizeStyle, openBlock, ref, renderSlot, toDisplayString, unref, watch, withCtx } from "vue";
|
|
2
|
+
import { Fragment, Teleport, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, guardReactiveProps, nextTick, normalizeClass, normalizeProps, normalizeStyle, onBeforeUnmount, openBlock, ref, renderSlot, toDisplayString, unref, watch, withCtx } from "vue";
|
|
3
3
|
//#region src/components/GrImageViewer/GrImageViewer.vue?vue&type=script&setup=true&lang.ts
|
|
4
4
|
var _hoisted_1 = { class: "fixed inset-0 overflow-hidden" };
|
|
5
5
|
var _hoisted_2 = { class: "pointer-events-none absolute inset-x-0 top-0 z-30 px-3 py-3 sm:px-6" };
|
|
@@ -16,29 +16,30 @@ var _hoisted_6 = {
|
|
|
16
16
|
class: "rounded-full bg-black/35 px-3 py-1 text-xs font-700 text-white/95 sm:text-sm"
|
|
17
17
|
};
|
|
18
18
|
var _hoisted_7 = ["aria-label"];
|
|
19
|
-
var _hoisted_8 =
|
|
19
|
+
var _hoisted_8 = ["aria-label"];
|
|
20
20
|
var _hoisted_9 = ["aria-label"];
|
|
21
|
-
var _hoisted_10 =
|
|
22
|
-
var _hoisted_11 =
|
|
23
|
-
var _hoisted_12 =
|
|
24
|
-
var _hoisted_13 = {
|
|
21
|
+
var _hoisted_10 = { class: "h-full w-full flex items-center justify-center" };
|
|
22
|
+
var _hoisted_11 = ["src"];
|
|
23
|
+
var _hoisted_12 = {
|
|
25
24
|
key: 1,
|
|
26
25
|
class: "rounded-[var(--ds-radius-xl)] border border-white/20 bg-black/25 px-4 py-3 text-sm text-white/80"
|
|
27
26
|
};
|
|
28
|
-
var
|
|
29
|
-
var
|
|
30
|
-
var
|
|
27
|
+
var _hoisted_13 = { class: "pointer-events-none absolute inset-x-0 bottom-0 z-30 px-3 pb-3 sm:px-6 sm:pb-6" };
|
|
28
|
+
var _hoisted_14 = { class: "pointer-events-auto mx-auto w-full max-w-max" };
|
|
29
|
+
var _hoisted_15 = { class: "rounded-full border border-white/20 bg-black/35 p-1 backdrop-blur-sm flex items-center gap-1" };
|
|
30
|
+
var _hoisted_16 = ["aria-label"];
|
|
31
31
|
var _hoisted_17 = ["aria-label"];
|
|
32
32
|
var _hoisted_18 = ["aria-label"];
|
|
33
|
-
var _hoisted_19 =
|
|
34
|
-
var _hoisted_20 = {
|
|
33
|
+
var _hoisted_19 = {
|
|
35
34
|
key: 1,
|
|
36
35
|
class: "mx-1 h-6 w-px bg-white/25",
|
|
37
36
|
"aria-hidden": "true"
|
|
38
37
|
};
|
|
38
|
+
var _hoisted_20 = ["aria-label"];
|
|
39
39
|
var _hoisted_21 = ["aria-label"];
|
|
40
|
-
var _hoisted_22 = ["aria-label"];
|
|
41
40
|
var DEFAULT_Z_INDEX = 2e3;
|
|
41
|
+
var WHEEL_ZOOM_SENSITIVITY = .0015;
|
|
42
|
+
var WHEEL_IDLE_MS = 120;
|
|
42
43
|
//#endregion
|
|
43
44
|
//#region src/components/GrImageViewer/GrImageViewer.vue
|
|
44
45
|
var GrImageViewer_default = /* @__PURE__ */ defineComponent({
|
|
@@ -66,6 +67,14 @@ var GrImageViewer_default = /* @__PURE__ */ defineComponent({
|
|
|
66
67
|
type: Boolean,
|
|
67
68
|
default: true
|
|
68
69
|
},
|
|
70
|
+
wheelZoom: {
|
|
71
|
+
type: Boolean,
|
|
72
|
+
default: true
|
|
73
|
+
},
|
|
74
|
+
draggable: {
|
|
75
|
+
type: Boolean,
|
|
76
|
+
default: false
|
|
77
|
+
},
|
|
69
78
|
zIndex: { default: void 0 },
|
|
70
79
|
closeLabel: { default: "Close image viewer" },
|
|
71
80
|
prevLabel: { default: "Previous image" },
|
|
@@ -95,6 +104,21 @@ var GrImageViewer_default = /* @__PURE__ */ defineComponent({
|
|
|
95
104
|
const rotation = ref(0);
|
|
96
105
|
const offsetX = ref(0);
|
|
97
106
|
const offsetY = ref(0);
|
|
107
|
+
const imageEl = ref(null);
|
|
108
|
+
const isWheelZooming = ref(false);
|
|
109
|
+
let pendingWheelDelta = 0;
|
|
110
|
+
let wheelFrameId = null;
|
|
111
|
+
let wheelIdleTimer = null;
|
|
112
|
+
const isDragging = ref(false);
|
|
113
|
+
let dragStartX = 0;
|
|
114
|
+
let dragStartY = 0;
|
|
115
|
+
let dragOffsetStartX = 0;
|
|
116
|
+
let dragOffsetStartY = 0;
|
|
117
|
+
const naturalWidth = ref(0);
|
|
118
|
+
const naturalHeight = ref(0);
|
|
119
|
+
const fittedWidth = ref(0);
|
|
120
|
+
const fittedHeight = ref(0);
|
|
121
|
+
let resizeObserver = null;
|
|
98
122
|
const currentUrl = computed(() => {
|
|
99
123
|
if (!hasImages.value) return;
|
|
100
124
|
return props.urlList[currentIndex.value];
|
|
@@ -105,11 +129,37 @@ var GrImageViewer_default = /* @__PURE__ */ defineComponent({
|
|
|
105
129
|
});
|
|
106
130
|
const viewerStyle = computed(() => ({ zIndex: String(Number.isFinite(props.zIndex) ? props.zIndex : DEFAULT_Z_INDEX) }));
|
|
107
131
|
const imageStyle = computed(() => ({ transform: `translate3d(${offsetX.value}px, ${offsetY.value}px, 0) scale(${scale.value}) rotate(${rotation.value}deg)` }));
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
if (
|
|
111
|
-
return
|
|
132
|
+
const imageTransitionClass = computed(() => isWheelZooming.value || isDragging.value ? "" : "transition-transform duration-150 ease-out");
|
|
133
|
+
const imageCursorClass = computed(() => {
|
|
134
|
+
if (!props.draggable) return "";
|
|
135
|
+
return isDragging.value ? "cursor-grabbing" : "cursor-grab";
|
|
136
|
+
});
|
|
137
|
+
function formatPercent(value) {
|
|
138
|
+
if (Number.isInteger(value)) return String(value);
|
|
139
|
+
return value.toFixed(1).replace(TRAILING_ZERO_DECIMAL_RE, "");
|
|
140
|
+
}
|
|
141
|
+
const zoomValueText = computed(() => formatPercent(scale.value * 100));
|
|
142
|
+
const renderedWidth = computed(() => Math.round(fittedWidth.value * scale.value));
|
|
143
|
+
const renderedHeight = computed(() => Math.round(fittedHeight.value * scale.value));
|
|
144
|
+
const realScale = computed(() => {
|
|
145
|
+
if (!naturalWidth.value) return 0;
|
|
146
|
+
return fittedWidth.value * scale.value / naturalWidth.value;
|
|
112
147
|
});
|
|
148
|
+
const realScalePercent = computed(() => formatPercent(realScale.value * 100));
|
|
149
|
+
const toolbarSlotProps = computed(() => ({
|
|
150
|
+
index: currentIndex.value,
|
|
151
|
+
displayIndex: displayIndex.value,
|
|
152
|
+
total: total.value,
|
|
153
|
+
scale: scale.value,
|
|
154
|
+
rotation: rotation.value,
|
|
155
|
+
naturalWidth: naturalWidth.value,
|
|
156
|
+
naturalHeight: naturalHeight.value,
|
|
157
|
+
renderedWidth: renderedWidth.value,
|
|
158
|
+
renderedHeight: renderedHeight.value,
|
|
159
|
+
realScale: realScale.value,
|
|
160
|
+
realScalePercent: realScalePercent.value,
|
|
161
|
+
actions: toolbarActions
|
|
162
|
+
}));
|
|
113
163
|
function normalizeIndex(value) {
|
|
114
164
|
if (!hasImages.value) return 0;
|
|
115
165
|
return ((Number.isFinite(value) ? Math.trunc(value) : 0) % total.value + total.value) % total.value;
|
|
@@ -119,12 +169,47 @@ var GrImageViewer_default = /* @__PURE__ */ defineComponent({
|
|
|
119
169
|
rotation.value = 0;
|
|
120
170
|
offsetX.value = 0;
|
|
121
171
|
offsetY.value = 0;
|
|
172
|
+
isDragging.value = false;
|
|
173
|
+
}
|
|
174
|
+
function resetImageMetrics() {
|
|
175
|
+
naturalWidth.value = 0;
|
|
176
|
+
naturalHeight.value = 0;
|
|
177
|
+
fittedWidth.value = 0;
|
|
178
|
+
fittedHeight.value = 0;
|
|
179
|
+
}
|
|
180
|
+
function measureFitted() {
|
|
181
|
+
const el = imageEl.value;
|
|
182
|
+
if (!el) return;
|
|
183
|
+
fittedWidth.value = el.offsetWidth;
|
|
184
|
+
fittedHeight.value = el.offsetHeight;
|
|
185
|
+
}
|
|
186
|
+
function onImageLoad(event) {
|
|
187
|
+
const el = event.target;
|
|
188
|
+
naturalWidth.value = el.naturalWidth;
|
|
189
|
+
naturalHeight.value = el.naturalHeight;
|
|
190
|
+
measureFitted();
|
|
191
|
+
}
|
|
192
|
+
function startObservingImage() {
|
|
193
|
+
const el = imageEl.value;
|
|
194
|
+
if (!el || typeof ResizeObserver === "undefined") return;
|
|
195
|
+
if (!resizeObserver) resizeObserver = new ResizeObserver(() => measureFitted());
|
|
196
|
+
resizeObserver.observe(el);
|
|
197
|
+
if (el.complete && el.naturalWidth) {
|
|
198
|
+
naturalWidth.value = el.naturalWidth;
|
|
199
|
+
naturalHeight.value = el.naturalHeight;
|
|
200
|
+
}
|
|
201
|
+
measureFitted();
|
|
202
|
+
}
|
|
203
|
+
function stopObservingImage() {
|
|
204
|
+
resizeObserver?.disconnect();
|
|
122
205
|
}
|
|
123
206
|
function setIndex(nextIndex, options) {
|
|
124
207
|
const normalizedIndex = normalizeIndex(nextIndex);
|
|
125
208
|
if (normalizedIndex === currentIndex.value) return;
|
|
126
209
|
currentIndex.value = normalizedIndex;
|
|
127
210
|
resetTransform();
|
|
211
|
+
resetImageMetrics();
|
|
212
|
+
endWheelZoom();
|
|
128
213
|
if (options?.emitSwitch !== false) emit("switch", normalizedIndex);
|
|
129
214
|
}
|
|
130
215
|
function syncIndexFromInitial() {
|
|
@@ -134,6 +219,8 @@ var GrImageViewer_default = /* @__PURE__ */ defineComponent({
|
|
|
134
219
|
}
|
|
135
220
|
currentIndex.value = normalizeIndex(props.initialIndex);
|
|
136
221
|
resetTransform();
|
|
222
|
+
resetImageMetrics();
|
|
223
|
+
endWheelZoom();
|
|
137
224
|
}
|
|
138
225
|
function preloadAt(index) {
|
|
139
226
|
const url = props.urlList[index];
|
|
@@ -152,11 +239,70 @@ var GrImageViewer_default = /* @__PURE__ */ defineComponent({
|
|
|
152
239
|
function next() {
|
|
153
240
|
setIndex(currentIndex.value + 1);
|
|
154
241
|
}
|
|
242
|
+
function setScale(value) {
|
|
243
|
+
const clamped = Math.min(props.maxScale, Math.max(props.minScale, value));
|
|
244
|
+
scale.value = Number(clamped.toFixed(4));
|
|
245
|
+
}
|
|
155
246
|
function zoomIn() {
|
|
156
|
-
|
|
247
|
+
setScale(scale.value * props.zoomRate);
|
|
157
248
|
}
|
|
158
249
|
function zoomOut() {
|
|
159
|
-
|
|
250
|
+
setScale(scale.value / props.zoomRate);
|
|
251
|
+
}
|
|
252
|
+
function cancelWheelFrame() {
|
|
253
|
+
if (wheelFrameId !== null) {
|
|
254
|
+
cancelAnimationFrame(wheelFrameId);
|
|
255
|
+
wheelFrameId = null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function endWheelZoom() {
|
|
259
|
+
if (wheelIdleTimer !== null) {
|
|
260
|
+
clearTimeout(wheelIdleTimer);
|
|
261
|
+
wheelIdleTimer = null;
|
|
262
|
+
}
|
|
263
|
+
isWheelZooming.value = false;
|
|
264
|
+
pendingWheelDelta = 0;
|
|
265
|
+
cancelWheelFrame();
|
|
266
|
+
}
|
|
267
|
+
function flushWheelZoom() {
|
|
268
|
+
wheelFrameId = null;
|
|
269
|
+
const delta = pendingWheelDelta;
|
|
270
|
+
pendingWheelDelta = 0;
|
|
271
|
+
if (!delta) return;
|
|
272
|
+
const factor = Math.exp(-delta * WHEEL_ZOOM_SENSITIVITY);
|
|
273
|
+
setScale(scale.value * factor);
|
|
274
|
+
}
|
|
275
|
+
function onWheel(event) {
|
|
276
|
+
if (!props.wheelZoom || !hasImages.value) return;
|
|
277
|
+
event.preventDefault();
|
|
278
|
+
isWheelZooming.value = true;
|
|
279
|
+
pendingWheelDelta += event.deltaY;
|
|
280
|
+
if (wheelFrameId === null) wheelFrameId = requestAnimationFrame(flushWheelZoom);
|
|
281
|
+
if (wheelIdleTimer !== null) clearTimeout(wheelIdleTimer);
|
|
282
|
+
wheelIdleTimer = setTimeout(() => {
|
|
283
|
+
wheelIdleTimer = null;
|
|
284
|
+
isWheelZooming.value = false;
|
|
285
|
+
}, WHEEL_IDLE_MS);
|
|
286
|
+
}
|
|
287
|
+
function onPointerDown(event) {
|
|
288
|
+
if (!props.draggable || !hasImages.value || event.button !== 0) return;
|
|
289
|
+
event.preventDefault();
|
|
290
|
+
isDragging.value = true;
|
|
291
|
+
dragStartX = event.clientX;
|
|
292
|
+
dragStartY = event.clientY;
|
|
293
|
+
dragOffsetStartX = offsetX.value;
|
|
294
|
+
dragOffsetStartY = offsetY.value;
|
|
295
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
296
|
+
}
|
|
297
|
+
function onPointerMove(event) {
|
|
298
|
+
if (!isDragging.value) return;
|
|
299
|
+
offsetX.value = dragOffsetStartX + (event.clientX - dragStartX);
|
|
300
|
+
offsetY.value = dragOffsetStartY + (event.clientY - dragStartY);
|
|
301
|
+
}
|
|
302
|
+
function onPointerUp(event) {
|
|
303
|
+
if (!isDragging.value) return;
|
|
304
|
+
isDragging.value = false;
|
|
305
|
+
event.currentTarget.releasePointerCapture?.(event.pointerId);
|
|
160
306
|
}
|
|
161
307
|
function rotateLeft() {
|
|
162
308
|
rotation.value -= 90;
|
|
@@ -214,9 +360,16 @@ var GrImageViewer_default = /* @__PURE__ */ defineComponent({
|
|
|
214
360
|
default:
|
|
215
361
|
}
|
|
216
362
|
}
|
|
217
|
-
watch(() => props.modelValue, (isOpen) => {
|
|
218
|
-
if (!isOpen)
|
|
363
|
+
watch(() => props.modelValue, async (isOpen) => {
|
|
364
|
+
if (!isOpen) {
|
|
365
|
+
stopObservingImage();
|
|
366
|
+
endWheelZoom();
|
|
367
|
+
isDragging.value = false;
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
219
370
|
syncIndexFromInitial();
|
|
371
|
+
await nextTick();
|
|
372
|
+
startObservingImage();
|
|
220
373
|
}, { immediate: true });
|
|
221
374
|
watch(() => props.initialIndex, () => {
|
|
222
375
|
if (!props.modelValue) return;
|
|
@@ -231,6 +384,10 @@ var GrImageViewer_default = /* @__PURE__ */ defineComponent({
|
|
|
231
384
|
preloadAt(normalizeIndex(index - 1));
|
|
232
385
|
preloadAt(normalizeIndex(index + 1));
|
|
233
386
|
}, { immediate: true });
|
|
387
|
+
onBeforeUnmount(() => {
|
|
388
|
+
stopObservingImage();
|
|
389
|
+
endWheelZoom();
|
|
390
|
+
});
|
|
234
391
|
return (_ctx, _cache) => {
|
|
235
392
|
return openBlock(), createBlock(Teleport, { to: "body" }, [createVNode(unref(Se), {
|
|
236
393
|
show: open.value,
|
|
@@ -286,7 +443,10 @@ var GrImageViewer_default = /* @__PURE__ */ defineComponent({
|
|
|
286
443
|
"aria-hidden": "true",
|
|
287
444
|
class: "text-lg leading-none"
|
|
288
445
|
}, "✕", -1)])], 8, _hoisted_7)])]),
|
|
289
|
-
createElementVNode("div",
|
|
446
|
+
createElementVNode("div", {
|
|
447
|
+
class: "relative z-10 h-full w-full flex items-center justify-center px-16 py-16 sm:px-24 sm:py-20",
|
|
448
|
+
onWheel
|
|
449
|
+
}, [
|
|
290
450
|
total.value > 1 ? (openBlock(), createElementBlock("button", {
|
|
291
451
|
key: 0,
|
|
292
452
|
type: "button",
|
|
@@ -297,7 +457,7 @@ var GrImageViewer_default = /* @__PURE__ */ defineComponent({
|
|
|
297
457
|
}, [..._cache[1] || (_cache[1] = [createElementVNode("span", {
|
|
298
458
|
"aria-hidden": "true",
|
|
299
459
|
class: "text-xl leading-none"
|
|
300
|
-
}, "‹", -1)])], 8,
|
|
460
|
+
}, "‹", -1)])], 8, _hoisted_8)) : createCommentVNode("", true),
|
|
301
461
|
total.value > 1 ? (openBlock(), createElementBlock("button", {
|
|
302
462
|
key: 1,
|
|
303
463
|
type: "button",
|
|
@@ -308,78 +468,71 @@ var GrImageViewer_default = /* @__PURE__ */ defineComponent({
|
|
|
308
468
|
}, [..._cache[2] || (_cache[2] = [createElementVNode("span", {
|
|
309
469
|
"aria-hidden": "true",
|
|
310
470
|
class: "text-xl leading-none"
|
|
311
|
-
}, "›", -1)])], 8,
|
|
312
|
-
createElementVNode("div",
|
|
471
|
+
}, "›", -1)])], 8, _hoisted_9)) : createCommentVNode("", true),
|
|
472
|
+
createElementVNode("div", _hoisted_10, [currentUrl.value ? (openBlock(), createElementBlock("img", {
|
|
313
473
|
key: 0,
|
|
474
|
+
ref_key: "imageEl",
|
|
475
|
+
ref: imageEl,
|
|
314
476
|
"data-ds-image-viewer-image": "",
|
|
315
477
|
src: currentUrl.value,
|
|
316
478
|
alt: "",
|
|
317
479
|
draggable: "false",
|
|
318
|
-
class: "max-h-full max-w-full select-none object-contain will-change-transform
|
|
319
|
-
style: normalizeStyle(imageStyle.value)
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
actions: toolbarActions
|
|
329
|
-
}, () => [createElementVNode("div", _hoisted_16, [
|
|
480
|
+
class: normalizeClass(["max-h-full max-w-full select-none object-contain will-change-transform", [imageTransitionClass.value, imageCursorClass.value]]),
|
|
481
|
+
style: normalizeStyle(imageStyle.value),
|
|
482
|
+
onLoad: onImageLoad,
|
|
483
|
+
onPointerdown: onPointerDown,
|
|
484
|
+
onPointermove: onPointerMove,
|
|
485
|
+
onPointerup: onPointerUp,
|
|
486
|
+
onPointercancel: onPointerUp
|
|
487
|
+
}, null, 46, _hoisted_11)) : (openBlock(), createElementBlock("div", _hoisted_12, toDisplayString(__props.emptyText), 1))])
|
|
488
|
+
], 32),
|
|
489
|
+
createElementVNode("div", _hoisted_13, [createElementVNode("div", _hoisted_14, [renderSlot(_ctx.$slots, "toolbar", normalizeProps(guardReactiveProps(toolbarSlotProps.value)), () => [createElementVNode("div", _hoisted_15, [
|
|
330
490
|
createElementVNode("button", {
|
|
331
491
|
type: "button",
|
|
332
492
|
"data-ds-image-viewer-zoom-out": "",
|
|
333
493
|
"aria-label": __props.zoomOutLabel,
|
|
334
494
|
class: "h-11 min-w-11 px-2 flex items-center justify-center rounded-full text-sm font-600 text-white transition-colors hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80",
|
|
335
495
|
onClick: zoomOut
|
|
336
|
-
}, " − ", 8,
|
|
496
|
+
}, " − ", 8, _hoisted_16),
|
|
337
497
|
createElementVNode("button", {
|
|
338
498
|
type: "button",
|
|
339
499
|
"data-ds-image-viewer-zoom-reset": "",
|
|
340
500
|
"aria-label": __props.resetZoomLabel,
|
|
341
501
|
class: "h-11 min-w-11 px-3 flex items-center justify-center rounded-full text-xs font-700 text-white transition-colors hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80",
|
|
342
502
|
onClick: resetTransform
|
|
343
|
-
}, " 100% ", 8,
|
|
503
|
+
}, " 100% ", 8, _hoisted_17),
|
|
344
504
|
createElementVNode("button", {
|
|
345
505
|
type: "button",
|
|
346
506
|
"data-ds-image-viewer-zoom-in": "",
|
|
347
507
|
"aria-label": __props.zoomInLabel,
|
|
348
508
|
class: "h-11 min-w-11 px-2 flex items-center justify-center rounded-full text-sm font-600 text-white transition-colors hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80",
|
|
349
509
|
onClick: zoomIn
|
|
350
|
-
}, " + ", 8,
|
|
510
|
+
}, " + ", 8, _hoisted_18),
|
|
351
511
|
_ctx.$slots["toolbar-actions"] ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [
|
|
352
512
|
_cache[3] || (_cache[3] = createElementVNode("div", {
|
|
353
513
|
class: "mx-1 h-6 w-px bg-white/25",
|
|
354
514
|
"aria-hidden": "true"
|
|
355
515
|
}, null, -1)),
|
|
356
|
-
renderSlot(_ctx.$slots, "toolbar-actions",
|
|
357
|
-
index: currentIndex.value,
|
|
358
|
-
displayIndex: displayIndex.value,
|
|
359
|
-
total: total.value,
|
|
360
|
-
scale: scale.value,
|
|
361
|
-
rotation: rotation.value,
|
|
362
|
-
actions: toolbarActions
|
|
363
|
-
}),
|
|
516
|
+
renderSlot(_ctx.$slots, "toolbar-actions", normalizeProps(guardReactiveProps(toolbarSlotProps.value))),
|
|
364
517
|
_cache[4] || (_cache[4] = createElementVNode("div", {
|
|
365
518
|
class: "mx-1 h-6 w-px bg-white/25",
|
|
366
519
|
"aria-hidden": "true"
|
|
367
520
|
}, null, -1))
|
|
368
|
-
], 64)) : (openBlock(), createElementBlock("div",
|
|
521
|
+
], 64)) : (openBlock(), createElementBlock("div", _hoisted_19)),
|
|
369
522
|
createElementVNode("button", {
|
|
370
523
|
type: "button",
|
|
371
524
|
"data-ds-image-viewer-rotate-left": "",
|
|
372
525
|
"aria-label": __props.rotateLeftLabel,
|
|
373
526
|
class: "h-11 min-w-11 px-2 flex items-center justify-center rounded-full text-sm font-600 text-white transition-colors hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80",
|
|
374
527
|
onClick: rotateLeft
|
|
375
|
-
}, " ↺ ", 8,
|
|
528
|
+
}, " ↺ ", 8, _hoisted_20),
|
|
376
529
|
createElementVNode("button", {
|
|
377
530
|
type: "button",
|
|
378
531
|
"data-ds-image-viewer-rotate-right": "",
|
|
379
532
|
"aria-label": __props.rotateRightLabel,
|
|
380
533
|
class: "h-11 min-w-11 px-2 flex items-center justify-center rounded-full text-sm font-600 text-white transition-colors hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80",
|
|
381
534
|
onClick: rotateRight
|
|
382
|
-
}, " ↻ ", 8,
|
|
535
|
+
}, " ↻ ", 8, _hoisted_21)
|
|
383
536
|
])])])])
|
|
384
537
|
]),
|
|
385
538
|
_: 3
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GrImageViewer-CsvXQHM9.js","names":["$slots"],"sources":["../../../../src/components/GrImageViewer/GrImageViewer.vue","../../../../src/components/GrImageViewer/GrImageViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'\nimport { Dialog, DialogPanel, TransitionChild, TransitionRoot } from '@headlessui/vue'\n\n/**\n * Usage:\n *\n * <GrImageViewer\n * v-model=\"open\"\n * :url-list=\"images\"\n * :initial-index=\"0\"\n * show-progress\n * hide-on-click-modal\n * />\n */\nconst props = withDefaults(\n defineProps<{\n modelValue: boolean\n urlList: string[]\n initialIndex?: number\n zoomRate?: number\n minScale?: number\n maxScale?: number\n hideOnClickModal?: boolean\n closeOnPressEscape?: boolean\n showProgress?: boolean\n showZoomValue?: boolean\n /** Включает масштабирование колесом мыши / жестом на трекпаде. По умолчанию включено. */\n wheelZoom?: boolean\n /** Включает перетаскивание (pan) картинки мышью. При наведении курсор «рука». По умолчанию выключено. */\n draggable?: boolean\n zIndex?: number\n /** i18n: aria-label кнопки закрытия. */\n closeLabel?: string\n /** i18n: aria-label кнопки «предыдущее изображение». */\n prevLabel?: string\n /** i18n: aria-label кнопки «следующее изображение». */\n nextLabel?: string\n /** i18n: aria-label кнопки «увеличить». */\n zoomInLabel?: string\n /** i18n: aria-label кнопки «уменьшить». */\n zoomOutLabel?: string\n /** i18n: aria-label кнопки «сбросить масштаб». */\n resetZoomLabel?: string\n /** i18n: aria-label кнопки «повернуть влево». */\n rotateLeftLabel?: string\n /** i18n: aria-label кнопки «повернуть вправо». */\n rotateRightLabel?: string\n /** i18n: текст в пустом состоянии (нет изображений). */\n emptyText?: string\n }>(),\n {\n initialIndex: 0,\n zoomRate: 1.2,\n minScale: 0.5,\n maxScale: 5,\n hideOnClickModal: false,\n closeOnPressEscape: true,\n showProgress: false,\n showZoomValue: true,\n wheelZoom: true,\n draggable: false,\n zIndex: undefined,\n closeLabel: 'Close image viewer',\n prevLabel: 'Previous image',\n nextLabel: 'Next image',\n zoomInLabel: 'Zoom in',\n zoomOutLabel: 'Zoom out',\n resetZoomLabel: 'Reset zoom',\n rotateLeftLabel: 'Rotate left',\n rotateRightLabel: 'Rotate right',\n emptyText: 'No image',\n },\n)\n\nconst emit = defineEmits<{\n (e: 'update:modelValue', value: boolean): void\n (e: 'close'): void\n (e: 'switch', newIndex: number): void\n (e: 'rotate', deg: number): void\n}>()\n\ntype GrImageViewerToolbarActions = {\n close: () => void\n prev: () => void\n next: () => void\n zoomIn: () => void\n zoomOut: () => void\n reset: () => void\n rotateLeft: () => void\n rotateRight: () => void\n}\n\ntype GrImageViewerSlotProps = {\n index: number\n displayIndex: number\n total: number\n scale: number\n rotation: number\n /** Натуральный (исходный) размер картинки, px. 0 пока изображение не загружено. */\n naturalWidth: number\n naturalHeight: number\n /** Фактический размер изображения на экране c учётом scale, px (footprint вдоль осей картинки). */\n renderedWidth: number\n renderedHeight: number\n /** Реальный масштаб относительно натурального размера (доля): renderedWidth / naturalWidth. */\n realScale: number\n /** Реальный масштаб в процентах, отформатированный (например `67`). */\n realScalePercent: string\n actions: GrImageViewerToolbarActions\n}\n\nconst DEFAULT_Z_INDEX = 2000\nconst TRAILING_ZERO_DECIMAL_RE = /\\.0$/\n// Чувствительность масштабирования колесом/трекпадом: чем больше — тем резче реакция.\nconst WHEEL_ZOOM_SENSITIVITY = 0.0015\n// Сколько ms простоя после последнего wheel-события считаем «зум завершён»\n// (после этого возвращаем плавный CSS-переход).\nconst WHEEL_IDLE_MS = 120\n\nconst open = computed(() => props.modelValue)\nconst total = computed(() => props.urlList.length)\nconst hasImages = computed(() => total.value > 0)\n\nconst currentIndex = ref(0)\nconst scale = ref(1)\nconst rotation = ref(0)\nconst offsetX = ref(0)\nconst offsetY = ref(0)\n\nconst imageEl = ref<HTMLImageElement | null>(null)\n\n// Флаг активного зума колесом/трекпадом. Пока true — CSS-переход отключён,\n// чтобы непрерывное масштабирование не «наслаивалось» и не дёргалось.\nconst isWheelZooming = ref(false)\n\n// Накопленный deltaY между кадрами и id запланированного rAF.\nlet pendingWheelDelta = 0\nlet wheelFrameId: number | null = null\nlet wheelIdleTimer: ReturnType<typeof setTimeout> | null = null\n\n// Флаг активного перетаскивания (pan) картинки мышью.\nconst isDragging = ref(false)\n\n// Координаты указателя и offset на момент начала перетаскивания.\nlet dragStartX = 0\nlet dragStartY = 0\nlet dragOffsetStartX = 0\nlet dragOffsetStartY = 0\n\n// Натуральный (исходный) размер картинки, читается из `<img>` при загрузке.\nconst naturalWidth = ref(0)\nconst naturalHeight = ref(0)\n\n// Layout-размер вписанного (`object-contain`) изображения без учёта CSS transform/scale.\nconst fittedWidth = ref(0)\nconst fittedHeight = ref(0)\n\nlet resizeObserver: ResizeObserver | null = null\n\nconst currentUrl = computed(() => {\n if (!hasImages.value) {\n return undefined\n }\n\n return props.urlList[currentIndex.value]\n})\n\nconst displayIndex = computed(() => {\n if (!hasImages.value) {\n return 0\n }\n\n return currentIndex.value + 1\n})\n\nconst viewerStyle = computed(() => ({\n zIndex: String(Number.isFinite(props.zIndex) ? props.zIndex : DEFAULT_Z_INDEX),\n}))\n\nconst imageStyle = computed(() => ({\n transform: `translate3d(${offsetX.value}px, ${offsetY.value}px, 0) scale(${scale.value}) rotate(${rotation.value}deg)`,\n}))\n\n// Плавный CSS-переход transform только для дискретных зумов (кнопки/клавиши).\n// При непрерывном зуме колесом и при перетаскивании переход отключаем —\n// иначе картинка «дёргается»/наслаивается и тянется за курсором с задержкой.\nconst imageTransitionClass = computed(() =>\n isWheelZooming.value || isDragging.value ? '' : 'transition-transform duration-150 ease-out',\n)\n\n// Курсор «рука» при наведении (grab) и при зажатии (grabbing), только если включён drag.\nconst imageCursorClass = computed(() => {\n if (!props.draggable) {\n return ''\n }\n\n return isDragging.value ? 'cursor-grabbing' : 'cursor-grab'\n})\n\nfunction formatPercent(value: number): string {\n if (Number.isInteger(value)) {\n return String(value)\n }\n\n return value.toFixed(1).replace(TRAILING_ZERO_DECIMAL_RE, '')\n}\n\nconst zoomValueText = computed(() => formatPercent(scale.value * 100))\n\n// Фактический размер изображения на экране вдоль его осей (footprint), c учётом scale.\nconst renderedWidth = computed(() => Math.round(fittedWidth.value * scale.value))\nconst renderedHeight = computed(() => Math.round(fittedHeight.value * scale.value))\n\n// Реальный масштаб относительно натурального размера (доля). Не зависит от rotation,\n// так как scale равномерный по осям картинки.\nconst realScale = computed(() => {\n if (!naturalWidth.value) {\n return 0\n }\n\n return (fittedWidth.value * scale.value) / naturalWidth.value\n})\n\nconst realScalePercent = computed(() => formatPercent(realScale.value * 100))\n\nconst toolbarSlotProps = computed<GrImageViewerSlotProps>(() => ({\n index: currentIndex.value,\n displayIndex: displayIndex.value,\n total: total.value,\n scale: scale.value,\n rotation: rotation.value,\n naturalWidth: naturalWidth.value,\n naturalHeight: naturalHeight.value,\n renderedWidth: renderedWidth.value,\n renderedHeight: renderedHeight.value,\n realScale: realScale.value,\n realScalePercent: realScalePercent.value,\n actions: toolbarActions,\n}))\n\nfunction normalizeIndex(value: number): number {\n if (!hasImages.value) {\n return 0\n }\n\n const normalized = Number.isFinite(value) ? Math.trunc(value) : 0\n\n return ((normalized % total.value) + total.value) % total.value\n}\n\nfunction resetTransform(): void {\n scale.value = 1\n rotation.value = 0\n offsetX.value = 0\n offsetY.value = 0\n isDragging.value = false\n}\n\nfunction resetImageMetrics(): void {\n naturalWidth.value = 0\n naturalHeight.value = 0\n fittedWidth.value = 0\n fittedHeight.value = 0\n}\n\nfunction measureFitted(): void {\n const el = imageEl.value\n\n if (!el) {\n return\n }\n\n // offsetWidth/Height — layout-размер вписанного изображения, без учёта CSS transform.\n fittedWidth.value = el.offsetWidth\n fittedHeight.value = el.offsetHeight\n}\n\nfunction onImageLoad(event: Event): void {\n const el = event.target as HTMLImageElement\n\n naturalWidth.value = el.naturalWidth\n naturalHeight.value = el.naturalHeight\n measureFitted()\n}\n\nfunction startObservingImage(): void {\n const el = imageEl.value\n\n if (!el || typeof ResizeObserver === 'undefined') {\n return\n }\n\n if (!resizeObserver) {\n resizeObserver = new ResizeObserver(() => measureFitted())\n }\n\n resizeObserver.observe(el)\n\n // Если картинка уже в кэше и `load` не сработал — считаем метрики сразу.\n if (el.complete && el.naturalWidth) {\n naturalWidth.value = el.naturalWidth\n naturalHeight.value = el.naturalHeight\n }\n\n measureFitted()\n}\n\nfunction stopObservingImage(): void {\n resizeObserver?.disconnect()\n}\n\nfunction setIndex(nextIndex: number, options?: { emitSwitch?: boolean }): void {\n const normalizedIndex = normalizeIndex(nextIndex)\n\n if (normalizedIndex === currentIndex.value) {\n return\n }\n\n currentIndex.value = normalizedIndex\n resetTransform()\n resetImageMetrics()\n endWheelZoom()\n\n if (options?.emitSwitch !== false) {\n emit('switch', normalizedIndex)\n }\n}\n\nfunction syncIndexFromInitial(): void {\n if (!hasImages.value) {\n currentIndex.value = 0\n return\n }\n\n currentIndex.value = normalizeIndex(props.initialIndex)\n resetTransform()\n resetImageMetrics()\n endWheelZoom()\n}\n\nfunction preloadAt(index: number): void {\n const url = props.urlList[index]\n\n if (!url) {\n return\n }\n\n const image = new Image()\n image.decoding = 'async'\n image.src = url\n}\n\nfunction closeViewer(): void {\n emit('update:modelValue', false)\n emit('close')\n}\n\nfunction prev(): void {\n setIndex(currentIndex.value - 1)\n}\n\nfunction next(): void {\n setIndex(currentIndex.value + 1)\n}\n\nfunction setScale(value: number): void {\n const clamped = Math.min(props.maxScale, Math.max(props.minScale, value))\n\n scale.value = Number(clamped.toFixed(4))\n}\n\nfunction zoomIn(): void {\n setScale(scale.value * props.zoomRate)\n}\n\nfunction zoomOut(): void {\n setScale(scale.value / props.zoomRate)\n}\n\nfunction cancelWheelFrame(): void {\n if (wheelFrameId !== null) {\n cancelAnimationFrame(wheelFrameId)\n wheelFrameId = null\n }\n}\n\nfunction endWheelZoom(): void {\n if (wheelIdleTimer !== null) {\n clearTimeout(wheelIdleTimer)\n wheelIdleTimer = null\n }\n\n isWheelZooming.value = false\n pendingWheelDelta = 0\n cancelWheelFrame()\n}\n\n// Применяем накопленный за кадр deltaY одним обновлением scale — так рендер\n// происходит не чаще одного раза в кадр (без «дёрганья» от частых обновлений).\nfunction flushWheelZoom(): void {\n wheelFrameId = null\n\n const delta = pendingWheelDelta\n pendingWheelDelta = 0\n\n if (!delta) {\n return\n }\n\n // Экспоненциальный шаг — плавно и одинаково ощущается и для колеса, и для трекпада.\n // delta < 0 (от себя / scroll up) — приближение, delta > 0 — отдаление.\n const factor = Math.exp(-delta * WHEEL_ZOOM_SENSITIVITY)\n\n setScale(scale.value * factor)\n}\n\nfunction onWheel(event: WheelEvent): void {\n if (!props.wheelZoom || !hasImages.value) {\n return\n }\n\n // Не даём странице/оверлею проскроллиться — колесо отдаём под зум.\n event.preventDefault()\n\n // Во время непрерывного зума отключаем CSS-переход: плавность даёт само\n // покадровое обновление, а transition на каждом микрошаге вызывает лаг/наслоение.\n isWheelZooming.value = true\n\n pendingWheelDelta += event.deltaY\n\n if (wheelFrameId === null) {\n wheelFrameId = requestAnimationFrame(flushWheelZoom)\n }\n\n if (wheelIdleTimer !== null) {\n clearTimeout(wheelIdleTimer)\n }\n\n // По завершении жеста возвращаем плавный переход для последующих дискретных зумов.\n wheelIdleTimer = setTimeout(() => {\n wheelIdleTimer = null\n isWheelZooming.value = false\n }, WHEEL_IDLE_MS)\n}\n\nfunction onPointerDown(event: PointerEvent): void {\n // Тянем только основной кнопкой и только если drag включён и есть картинка.\n if (!props.draggable || !hasImages.value || event.button !== 0) {\n return\n }\n\n event.preventDefault()\n\n isDragging.value = true\n dragStartX = event.clientX\n dragStartY = event.clientY\n dragOffsetStartX = offsetX.value\n dragOffsetStartY = offsetY.value\n\n // Захватываем указатель, чтобы движение/отпускание ловились даже за пределами картинки.\n ;(event.currentTarget as Element).setPointerCapture?.(event.pointerId)\n}\n\nfunction onPointerMove(event: PointerEvent): void {\n if (!isDragging.value) {\n return\n }\n\n offsetX.value = dragOffsetStartX + (event.clientX - dragStartX)\n offsetY.value = dragOffsetStartY + (event.clientY - dragStartY)\n}\n\nfunction onPointerUp(event: PointerEvent): void {\n if (!isDragging.value) {\n return\n }\n\n isDragging.value = false\n ;(event.currentTarget as Element).releasePointerCapture?.(event.pointerId)\n}\n\nfunction rotateLeft(): void {\n rotation.value -= 90\n emit('rotate', rotation.value)\n}\n\nfunction rotateRight(): void {\n rotation.value += 90\n emit('rotate', rotation.value)\n}\n\nconst toolbarActions: GrImageViewerToolbarActions = {\n close: closeViewer,\n prev,\n next,\n zoomIn,\n zoomOut,\n reset: resetTransform,\n rotateLeft,\n rotateRight,\n}\n\nfunction onBackdropClick(): void {\n if (!props.hideOnClickModal) {\n return\n }\n\n closeViewer()\n}\n\nfunction onKeydown(event: KeyboardEvent): void {\n switch (event.key) {\n case 'Escape':\n if (!props.closeOnPressEscape) {\n return\n }\n\n event.preventDefault()\n closeViewer()\n return\n\n case 'ArrowLeft':\n event.preventDefault()\n prev()\n return\n\n case 'ArrowRight':\n event.preventDefault()\n next()\n return\n\n case '+':\n case '=':\n case 'Add':\n event.preventDefault()\n zoomIn()\n return\n\n case '-':\n case '_':\n case 'Subtract':\n event.preventDefault()\n zoomOut()\n return\n\n case '0':\n event.preventDefault()\n resetTransform()\n break\n\n default:\n }\n}\n\nwatch(\n () => props.modelValue,\n async (isOpen) => {\n if (!isOpen) {\n stopObservingImage()\n endWheelZoom()\n isDragging.value = false\n return\n }\n\n syncIndexFromInitial()\n await nextTick()\n startObservingImage()\n },\n { immediate: true },\n)\n\nwatch(\n () => props.initialIndex,\n () => {\n if (!props.modelValue) {\n return\n }\n\n syncIndexFromInitial()\n },\n)\n\nwatch(\n () => props.urlList,\n () => {\n if (!props.modelValue) {\n return\n }\n\n syncIndexFromInitial()\n },\n { deep: true },\n)\n\nwatch(\n currentIndex,\n (index) => {\n if (!hasImages.value || total.value < 2) {\n return\n }\n\n preloadAt(normalizeIndex(index - 1))\n preloadAt(normalizeIndex(index + 1))\n },\n { immediate: true },\n)\n\nonBeforeUnmount(() => {\n stopObservingImage()\n endWheelZoom()\n})\n</script>\n\n<template>\n <teleport to=\"body\">\n <TransitionRoot :show=\"open\" as=\"template\">\n <Dialog\n as=\"div\"\n class=\"fixed inset-0\"\n :style=\"viewerStyle\"\n role=\"dialog\"\n aria-modal=\"true\"\n :static=\"true\"\n @keydown=\"onKeydown\"\n >\n <div class=\"fixed inset-0 overflow-hidden\">\n <TransitionChild\n as=\"template\"\n enter=\"duration-150 ease-out\"\n enter-from=\"opacity-0\"\n enter-to=\"opacity-100\"\n leave=\"duration-120 ease-in\"\n leave-from=\"opacity-100\"\n leave-to=\"opacity-0\"\n >\n <div\n data-ds-image-viewer-overlay\n class=\"absolute inset-0 bg-black/60 backdrop-blur-sm\"\n aria-hidden=\"true\"\n @click=\"onBackdropClick\"\n />\n </TransitionChild>\n\n <TransitionChild\n as=\"template\"\n enter=\"duration-180 ease-out\"\n enter-from=\"opacity-0 scale-98\"\n enter-to=\"opacity-100 scale-100\"\n leave=\"duration-130 ease-in\"\n leave-from=\"opacity-100 scale-100\"\n leave-to=\"opacity-0 scale-98\"\n >\n <DialogPanel\n data-ds-image-viewer-panel\n class=\"relative z-10 h-full w-full outline-none\"\n tabindex=\"-1\"\n >\n <div class=\"pointer-events-none absolute inset-x-0 top-0 z-30 px-3 py-3 sm:px-6\">\n <div class=\"pointer-events-auto flex items-center justify-between gap-3\">\n <div class=\"flex items-center gap-2\">\n <div\n v-if=\"showProgress\"\n data-ds-image-viewer-progress\n class=\"rounded-full bg-black/35 px-3 py-1 text-xs font-600 text-white/95 sm:text-sm\"\n >\n {{ displayIndex }} / {{ total }}\n </div>\n\n <div\n v-if=\"showZoomValue\"\n data-ds-image-viewer-zoom-value\n class=\"rounded-full bg-black/35 px-3 py-1 text-xs font-700 text-white/95 sm:text-sm\"\n >\n {{ zoomValueText }}%\n </div>\n </div>\n\n <button\n type=\"button\"\n data-ds-image-viewer-close\n :aria-label=\"closeLabel\"\n class=\"h-11 w-11 flex items-center justify-center rounded-full border border-white/20 bg-black/35 text-white transition-colors hover:bg-black/55 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80\"\n @click=\"closeViewer\"\n >\n <span aria-hidden=\"true\" class=\"text-lg leading-none\">✕</span>\n </button>\n </div>\n </div>\n\n <div\n class=\"relative z-10 h-full w-full flex items-center justify-center px-16 py-16 sm:px-24 sm:py-20\"\n @wheel=\"onWheel\"\n >\n <button\n v-if=\"total > 1\"\n type=\"button\"\n data-ds-image-viewer-prev\n :aria-label=\"prevLabel\"\n class=\"absolute left-3 top-1/2 z-20 h-12 w-12 -translate-y-1/2 flex items-center justify-center rounded-full border border-white/20 bg-black/35 text-white transition-colors hover:bg-black/55 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80 sm:left-6\"\n @click=\"prev\"\n >\n <span aria-hidden=\"true\" class=\"text-xl leading-none\">‹</span>\n </button>\n\n <button\n v-if=\"total > 1\"\n type=\"button\"\n data-ds-image-viewer-next\n :aria-label=\"nextLabel\"\n class=\"absolute right-3 top-1/2 z-20 h-12 w-12 -translate-y-1/2 flex items-center justify-center rounded-full border border-white/20 bg-black/35 text-white transition-colors hover:bg-black/55 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80 sm:right-6\"\n @click=\"next\"\n >\n <span aria-hidden=\"true\" class=\"text-xl leading-none\">›</span>\n </button>\n\n <div class=\"h-full w-full flex items-center justify-center\">\n <img\n v-if=\"currentUrl\"\n ref=\"imageEl\"\n data-ds-image-viewer-image\n :src=\"currentUrl\"\n alt=\"\"\n draggable=\"false\"\n class=\"max-h-full max-w-full select-none object-contain will-change-transform\"\n :class=\"[imageTransitionClass, imageCursorClass]\"\n :style=\"imageStyle\"\n @load=\"onImageLoad\"\n @pointerdown=\"onPointerDown\"\n @pointermove=\"onPointerMove\"\n @pointerup=\"onPointerUp\"\n @pointercancel=\"onPointerUp\"\n >\n\n <div\n v-else\n class=\"rounded-[var(--ds-radius-xl)] border border-white/20 bg-black/25 px-4 py-3 text-sm text-white/80\"\n >\n {{ emptyText }}\n </div>\n </div>\n </div>\n\n <div class=\"pointer-events-none absolute inset-x-0 bottom-0 z-30 px-3 pb-3 sm:px-6 sm:pb-6\">\n <div class=\"pointer-events-auto mx-auto w-full max-w-max\">\n <slot\n name=\"toolbar\"\n v-bind=\"toolbarSlotProps\"\n >\n <div class=\"rounded-full border border-white/20 bg-black/35 p-1 backdrop-blur-sm flex items-center gap-1\">\n <button\n type=\"button\"\n data-ds-image-viewer-zoom-out\n :aria-label=\"zoomOutLabel\"\n class=\"h-11 min-w-11 px-2 flex items-center justify-center rounded-full text-sm font-600 text-white transition-colors hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80\"\n @click=\"zoomOut\"\n >\n −\n </button>\n\n <button\n type=\"button\"\n data-ds-image-viewer-zoom-reset\n :aria-label=\"resetZoomLabel\"\n class=\"h-11 min-w-11 px-3 flex items-center justify-center rounded-full text-xs font-700 text-white transition-colors hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80\"\n @click=\"resetTransform\"\n >\n 100%\n </button>\n\n <button\n type=\"button\"\n data-ds-image-viewer-zoom-in\n :aria-label=\"zoomInLabel\"\n class=\"h-11 min-w-11 px-2 flex items-center justify-center rounded-full text-sm font-600 text-white transition-colors hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80\"\n @click=\"zoomIn\"\n >\n +\n </button>\n\n <template v-if=\"$slots['toolbar-actions']\">\n <div class=\"mx-1 h-6 w-px bg-white/25\" aria-hidden=\"true\" />\n\n <slot\n name=\"toolbar-actions\"\n v-bind=\"toolbarSlotProps\"\n />\n\n <div class=\"mx-1 h-6 w-px bg-white/25\" aria-hidden=\"true\" />\n </template>\n\n <div v-else class=\"mx-1 h-6 w-px bg-white/25\" aria-hidden=\"true\" />\n\n <button\n type=\"button\"\n data-ds-image-viewer-rotate-left\n :aria-label=\"rotateLeftLabel\"\n class=\"h-11 min-w-11 px-2 flex items-center justify-center rounded-full text-sm font-600 text-white transition-colors hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80\"\n @click=\"rotateLeft\"\n >\n ↺\n </button>\n\n <button\n type=\"button\"\n data-ds-image-viewer-rotate-right\n :aria-label=\"rotateRightLabel\"\n class=\"h-11 min-w-11 px-2 flex items-center justify-center rounded-full text-sm font-600 text-white transition-colors hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80\"\n @click=\"rotateRight\"\n >\n ↻\n </button>\n </div>\n </slot>\n </div>\n </div>\n </DialogPanel>\n </TransitionChild>\n </div>\n </Dialog>\n </TransitionRoot>\n </teleport>\n</template>","<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'\nimport { Dialog, DialogPanel, TransitionChild, TransitionRoot } from '@headlessui/vue'\n\n/**\n * Usage:\n *\n * <GrImageViewer\n * v-model=\"open\"\n * :url-list=\"images\"\n * :initial-index=\"0\"\n * show-progress\n * hide-on-click-modal\n * />\n */\nconst props = withDefaults(\n defineProps<{\n modelValue: boolean\n urlList: string[]\n initialIndex?: number\n zoomRate?: number\n minScale?: number\n maxScale?: number\n hideOnClickModal?: boolean\n closeOnPressEscape?: boolean\n showProgress?: boolean\n showZoomValue?: boolean\n /** Включает масштабирование колесом мыши / жестом на трекпаде. По умолчанию включено. */\n wheelZoom?: boolean\n /** Включает перетаскивание (pan) картинки мышью. При наведении курсор «рука». По умолчанию выключено. */\n draggable?: boolean\n zIndex?: number\n /** i18n: aria-label кнопки закрытия. */\n closeLabel?: string\n /** i18n: aria-label кнопки «предыдущее изображение». */\n prevLabel?: string\n /** i18n: aria-label кнопки «следующее изображение». */\n nextLabel?: string\n /** i18n: aria-label кнопки «увеличить». */\n zoomInLabel?: string\n /** i18n: aria-label кнопки «уменьшить». */\n zoomOutLabel?: string\n /** i18n: aria-label кнопки «сбросить масштаб». */\n resetZoomLabel?: string\n /** i18n: aria-label кнопки «повернуть влево». */\n rotateLeftLabel?: string\n /** i18n: aria-label кнопки «повернуть вправо». */\n rotateRightLabel?: string\n /** i18n: текст в пустом состоянии (нет изображений). */\n emptyText?: string\n }>(),\n {\n initialIndex: 0,\n zoomRate: 1.2,\n minScale: 0.5,\n maxScale: 5,\n hideOnClickModal: false,\n closeOnPressEscape: true,\n showProgress: false,\n showZoomValue: true,\n wheelZoom: true,\n draggable: false,\n zIndex: undefined,\n closeLabel: 'Close image viewer',\n prevLabel: 'Previous image',\n nextLabel: 'Next image',\n zoomInLabel: 'Zoom in',\n zoomOutLabel: 'Zoom out',\n resetZoomLabel: 'Reset zoom',\n rotateLeftLabel: 'Rotate left',\n rotateRightLabel: 'Rotate right',\n emptyText: 'No image',\n },\n)\n\nconst emit = defineEmits<{\n (e: 'update:modelValue', value: boolean): void\n (e: 'close'): void\n (e: 'switch', newIndex: number): void\n (e: 'rotate', deg: number): void\n}>()\n\ntype GrImageViewerToolbarActions = {\n close: () => void\n prev: () => void\n next: () => void\n zoomIn: () => void\n zoomOut: () => void\n reset: () => void\n rotateLeft: () => void\n rotateRight: () => void\n}\n\ntype GrImageViewerSlotProps = {\n index: number\n displayIndex: number\n total: number\n scale: number\n rotation: number\n /** Натуральный (исходный) размер картинки, px. 0 пока изображение не загружено. */\n naturalWidth: number\n naturalHeight: number\n /** Фактический размер изображения на экране c учётом scale, px (footprint вдоль осей картинки). */\n renderedWidth: number\n renderedHeight: number\n /** Реальный масштаб относительно натурального размера (доля): renderedWidth / naturalWidth. */\n realScale: number\n /** Реальный масштаб в процентах, отформатированный (например `67`). */\n realScalePercent: string\n actions: GrImageViewerToolbarActions\n}\n\nconst DEFAULT_Z_INDEX = 2000\nconst TRAILING_ZERO_DECIMAL_RE = /\\.0$/\n// Чувствительность масштабирования колесом/трекпадом: чем больше — тем резче реакция.\nconst WHEEL_ZOOM_SENSITIVITY = 0.0015\n// Сколько ms простоя после последнего wheel-события считаем «зум завершён»\n// (после этого возвращаем плавный CSS-переход).\nconst WHEEL_IDLE_MS = 120\n\nconst open = computed(() => props.modelValue)\nconst total = computed(() => props.urlList.length)\nconst hasImages = computed(() => total.value > 0)\n\nconst currentIndex = ref(0)\nconst scale = ref(1)\nconst rotation = ref(0)\nconst offsetX = ref(0)\nconst offsetY = ref(0)\n\nconst imageEl = ref<HTMLImageElement | null>(null)\n\n// Флаг активного зума колесом/трекпадом. Пока true — CSS-переход отключён,\n// чтобы непрерывное масштабирование не «наслаивалось» и не дёргалось.\nconst isWheelZooming = ref(false)\n\n// Накопленный deltaY между кадрами и id запланированного rAF.\nlet pendingWheelDelta = 0\nlet wheelFrameId: number | null = null\nlet wheelIdleTimer: ReturnType<typeof setTimeout> | null = null\n\n// Флаг активного перетаскивания (pan) картинки мышью.\nconst isDragging = ref(false)\n\n// Координаты указателя и offset на момент начала перетаскивания.\nlet dragStartX = 0\nlet dragStartY = 0\nlet dragOffsetStartX = 0\nlet dragOffsetStartY = 0\n\n// Натуральный (исходный) размер картинки, читается из `<img>` при загрузке.\nconst naturalWidth = ref(0)\nconst naturalHeight = ref(0)\n\n// Layout-размер вписанного (`object-contain`) изображения без учёта CSS transform/scale.\nconst fittedWidth = ref(0)\nconst fittedHeight = ref(0)\n\nlet resizeObserver: ResizeObserver | null = null\n\nconst currentUrl = computed(() => {\n if (!hasImages.value) {\n return undefined\n }\n\n return props.urlList[currentIndex.value]\n})\n\nconst displayIndex = computed(() => {\n if (!hasImages.value) {\n return 0\n }\n\n return currentIndex.value + 1\n})\n\nconst viewerStyle = computed(() => ({\n zIndex: String(Number.isFinite(props.zIndex) ? props.zIndex : DEFAULT_Z_INDEX),\n}))\n\nconst imageStyle = computed(() => ({\n transform: `translate3d(${offsetX.value}px, ${offsetY.value}px, 0) scale(${scale.value}) rotate(${rotation.value}deg)`,\n}))\n\n// Плавный CSS-переход transform только для дискретных зумов (кнопки/клавиши).\n// При непрерывном зуме колесом и при перетаскивании переход отключаем —\n// иначе картинка «дёргается»/наслаивается и тянется за курсором с задержкой.\nconst imageTransitionClass = computed(() =>\n isWheelZooming.value || isDragging.value ? '' : 'transition-transform duration-150 ease-out',\n)\n\n// Курсор «рука» при наведении (grab) и при зажатии (grabbing), только если включён drag.\nconst imageCursorClass = computed(() => {\n if (!props.draggable) {\n return ''\n }\n\n return isDragging.value ? 'cursor-grabbing' : 'cursor-grab'\n})\n\nfunction formatPercent(value: number): string {\n if (Number.isInteger(value)) {\n return String(value)\n }\n\n return value.toFixed(1).replace(TRAILING_ZERO_DECIMAL_RE, '')\n}\n\nconst zoomValueText = computed(() => formatPercent(scale.value * 100))\n\n// Фактический размер изображения на экране вдоль его осей (footprint), c учётом scale.\nconst renderedWidth = computed(() => Math.round(fittedWidth.value * scale.value))\nconst renderedHeight = computed(() => Math.round(fittedHeight.value * scale.value))\n\n// Реальный масштаб относительно натурального размера (доля). Не зависит от rotation,\n// так как scale равномерный по осям картинки.\nconst realScale = computed(() => {\n if (!naturalWidth.value) {\n return 0\n }\n\n return (fittedWidth.value * scale.value) / naturalWidth.value\n})\n\nconst realScalePercent = computed(() => formatPercent(realScale.value * 100))\n\nconst toolbarSlotProps = computed<GrImageViewerSlotProps>(() => ({\n index: currentIndex.value,\n displayIndex: displayIndex.value,\n total: total.value,\n scale: scale.value,\n rotation: rotation.value,\n naturalWidth: naturalWidth.value,\n naturalHeight: naturalHeight.value,\n renderedWidth: renderedWidth.value,\n renderedHeight: renderedHeight.value,\n realScale: realScale.value,\n realScalePercent: realScalePercent.value,\n actions: toolbarActions,\n}))\n\nfunction normalizeIndex(value: number): number {\n if (!hasImages.value) {\n return 0\n }\n\n const normalized = Number.isFinite(value) ? Math.trunc(value) : 0\n\n return ((normalized % total.value) + total.value) % total.value\n}\n\nfunction resetTransform(): void {\n scale.value = 1\n rotation.value = 0\n offsetX.value = 0\n offsetY.value = 0\n isDragging.value = false\n}\n\nfunction resetImageMetrics(): void {\n naturalWidth.value = 0\n naturalHeight.value = 0\n fittedWidth.value = 0\n fittedHeight.value = 0\n}\n\nfunction measureFitted(): void {\n const el = imageEl.value\n\n if (!el) {\n return\n }\n\n // offsetWidth/Height — layout-размер вписанного изображения, без учёта CSS transform.\n fittedWidth.value = el.offsetWidth\n fittedHeight.value = el.offsetHeight\n}\n\nfunction onImageLoad(event: Event): void {\n const el = event.target as HTMLImageElement\n\n naturalWidth.value = el.naturalWidth\n naturalHeight.value = el.naturalHeight\n measureFitted()\n}\n\nfunction startObservingImage(): void {\n const el = imageEl.value\n\n if (!el || typeof ResizeObserver === 'undefined') {\n return\n }\n\n if (!resizeObserver) {\n resizeObserver = new ResizeObserver(() => measureFitted())\n }\n\n resizeObserver.observe(el)\n\n // Если картинка уже в кэше и `load` не сработал — считаем метрики сразу.\n if (el.complete && el.naturalWidth) {\n naturalWidth.value = el.naturalWidth\n naturalHeight.value = el.naturalHeight\n }\n\n measureFitted()\n}\n\nfunction stopObservingImage(): void {\n resizeObserver?.disconnect()\n}\n\nfunction setIndex(nextIndex: number, options?: { emitSwitch?: boolean }): void {\n const normalizedIndex = normalizeIndex(nextIndex)\n\n if (normalizedIndex === currentIndex.value) {\n return\n }\n\n currentIndex.value = normalizedIndex\n resetTransform()\n resetImageMetrics()\n endWheelZoom()\n\n if (options?.emitSwitch !== false) {\n emit('switch', normalizedIndex)\n }\n}\n\nfunction syncIndexFromInitial(): void {\n if (!hasImages.value) {\n currentIndex.value = 0\n return\n }\n\n currentIndex.value = normalizeIndex(props.initialIndex)\n resetTransform()\n resetImageMetrics()\n endWheelZoom()\n}\n\nfunction preloadAt(index: number): void {\n const url = props.urlList[index]\n\n if (!url) {\n return\n }\n\n const image = new Image()\n image.decoding = 'async'\n image.src = url\n}\n\nfunction closeViewer(): void {\n emit('update:modelValue', false)\n emit('close')\n}\n\nfunction prev(): void {\n setIndex(currentIndex.value - 1)\n}\n\nfunction next(): void {\n setIndex(currentIndex.value + 1)\n}\n\nfunction setScale(value: number): void {\n const clamped = Math.min(props.maxScale, Math.max(props.minScale, value))\n\n scale.value = Number(clamped.toFixed(4))\n}\n\nfunction zoomIn(): void {\n setScale(scale.value * props.zoomRate)\n}\n\nfunction zoomOut(): void {\n setScale(scale.value / props.zoomRate)\n}\n\nfunction cancelWheelFrame(): void {\n if (wheelFrameId !== null) {\n cancelAnimationFrame(wheelFrameId)\n wheelFrameId = null\n }\n}\n\nfunction endWheelZoom(): void {\n if (wheelIdleTimer !== null) {\n clearTimeout(wheelIdleTimer)\n wheelIdleTimer = null\n }\n\n isWheelZooming.value = false\n pendingWheelDelta = 0\n cancelWheelFrame()\n}\n\n// Применяем накопленный за кадр deltaY одним обновлением scale — так рендер\n// происходит не чаще одного раза в кадр (без «дёрганья» от частых обновлений).\nfunction flushWheelZoom(): void {\n wheelFrameId = null\n\n const delta = pendingWheelDelta\n pendingWheelDelta = 0\n\n if (!delta) {\n return\n }\n\n // Экспоненциальный шаг — плавно и одинаково ощущается и для колеса, и для трекпада.\n // delta < 0 (от себя / scroll up) — приближение, delta > 0 — отдаление.\n const factor = Math.exp(-delta * WHEEL_ZOOM_SENSITIVITY)\n\n setScale(scale.value * factor)\n}\n\nfunction onWheel(event: WheelEvent): void {\n if (!props.wheelZoom || !hasImages.value) {\n return\n }\n\n // Не даём странице/оверлею проскроллиться — колесо отдаём под зум.\n event.preventDefault()\n\n // Во время непрерывного зума отключаем CSS-переход: плавность даёт само\n // покадровое обновление, а transition на каждом микрошаге вызывает лаг/наслоение.\n isWheelZooming.value = true\n\n pendingWheelDelta += event.deltaY\n\n if (wheelFrameId === null) {\n wheelFrameId = requestAnimationFrame(flushWheelZoom)\n }\n\n if (wheelIdleTimer !== null) {\n clearTimeout(wheelIdleTimer)\n }\n\n // По завершении жеста возвращаем плавный переход для последующих дискретных зумов.\n wheelIdleTimer = setTimeout(() => {\n wheelIdleTimer = null\n isWheelZooming.value = false\n }, WHEEL_IDLE_MS)\n}\n\nfunction onPointerDown(event: PointerEvent): void {\n // Тянем только основной кнопкой и только если drag включён и есть картинка.\n if (!props.draggable || !hasImages.value || event.button !== 0) {\n return\n }\n\n event.preventDefault()\n\n isDragging.value = true\n dragStartX = event.clientX\n dragStartY = event.clientY\n dragOffsetStartX = offsetX.value\n dragOffsetStartY = offsetY.value\n\n // Захватываем указатель, чтобы движение/отпускание ловились даже за пределами картинки.\n ;(event.currentTarget as Element).setPointerCapture?.(event.pointerId)\n}\n\nfunction onPointerMove(event: PointerEvent): void {\n if (!isDragging.value) {\n return\n }\n\n offsetX.value = dragOffsetStartX + (event.clientX - dragStartX)\n offsetY.value = dragOffsetStartY + (event.clientY - dragStartY)\n}\n\nfunction onPointerUp(event: PointerEvent): void {\n if (!isDragging.value) {\n return\n }\n\n isDragging.value = false\n ;(event.currentTarget as Element).releasePointerCapture?.(event.pointerId)\n}\n\nfunction rotateLeft(): void {\n rotation.value -= 90\n emit('rotate', rotation.value)\n}\n\nfunction rotateRight(): void {\n rotation.value += 90\n emit('rotate', rotation.value)\n}\n\nconst toolbarActions: GrImageViewerToolbarActions = {\n close: closeViewer,\n prev,\n next,\n zoomIn,\n zoomOut,\n reset: resetTransform,\n rotateLeft,\n rotateRight,\n}\n\nfunction onBackdropClick(): void {\n if (!props.hideOnClickModal) {\n return\n }\n\n closeViewer()\n}\n\nfunction onKeydown(event: KeyboardEvent): void {\n switch (event.key) {\n case 'Escape':\n if (!props.closeOnPressEscape) {\n return\n }\n\n event.preventDefault()\n closeViewer()\n return\n\n case 'ArrowLeft':\n event.preventDefault()\n prev()\n return\n\n case 'ArrowRight':\n event.preventDefault()\n next()\n return\n\n case '+':\n case '=':\n case 'Add':\n event.preventDefault()\n zoomIn()\n return\n\n case '-':\n case '_':\n case 'Subtract':\n event.preventDefault()\n zoomOut()\n return\n\n case '0':\n event.preventDefault()\n resetTransform()\n break\n\n default:\n }\n}\n\nwatch(\n () => props.modelValue,\n async (isOpen) => {\n if (!isOpen) {\n stopObservingImage()\n endWheelZoom()\n isDragging.value = false\n return\n }\n\n syncIndexFromInitial()\n await nextTick()\n startObservingImage()\n },\n { immediate: true },\n)\n\nwatch(\n () => props.initialIndex,\n () => {\n if (!props.modelValue) {\n return\n }\n\n syncIndexFromInitial()\n },\n)\n\nwatch(\n () => props.urlList,\n () => {\n if (!props.modelValue) {\n return\n }\n\n syncIndexFromInitial()\n },\n { deep: true },\n)\n\nwatch(\n currentIndex,\n (index) => {\n if (!hasImages.value || total.value < 2) {\n return\n }\n\n preloadAt(normalizeIndex(index - 1))\n preloadAt(normalizeIndex(index + 1))\n },\n { immediate: true },\n)\n\nonBeforeUnmount(() => {\n stopObservingImage()\n endWheelZoom()\n})\n</script>\n\n<template>\n <teleport to=\"body\">\n <TransitionRoot :show=\"open\" as=\"template\">\n <Dialog\n as=\"div\"\n class=\"fixed inset-0\"\n :style=\"viewerStyle\"\n role=\"dialog\"\n aria-modal=\"true\"\n :static=\"true\"\n @keydown=\"onKeydown\"\n >\n <div class=\"fixed inset-0 overflow-hidden\">\n <TransitionChild\n as=\"template\"\n enter=\"duration-150 ease-out\"\n enter-from=\"opacity-0\"\n enter-to=\"opacity-100\"\n leave=\"duration-120 ease-in\"\n leave-from=\"opacity-100\"\n leave-to=\"opacity-0\"\n >\n <div\n data-ds-image-viewer-overlay\n class=\"absolute inset-0 bg-black/60 backdrop-blur-sm\"\n aria-hidden=\"true\"\n @click=\"onBackdropClick\"\n />\n </TransitionChild>\n\n <TransitionChild\n as=\"template\"\n enter=\"duration-180 ease-out\"\n enter-from=\"opacity-0 scale-98\"\n enter-to=\"opacity-100 scale-100\"\n leave=\"duration-130 ease-in\"\n leave-from=\"opacity-100 scale-100\"\n leave-to=\"opacity-0 scale-98\"\n >\n <DialogPanel\n data-ds-image-viewer-panel\n class=\"relative z-10 h-full w-full outline-none\"\n tabindex=\"-1\"\n >\n <div class=\"pointer-events-none absolute inset-x-0 top-0 z-30 px-3 py-3 sm:px-6\">\n <div class=\"pointer-events-auto flex items-center justify-between gap-3\">\n <div class=\"flex items-center gap-2\">\n <div\n v-if=\"showProgress\"\n data-ds-image-viewer-progress\n class=\"rounded-full bg-black/35 px-3 py-1 text-xs font-600 text-white/95 sm:text-sm\"\n >\n {{ displayIndex }} / {{ total }}\n </div>\n\n <div\n v-if=\"showZoomValue\"\n data-ds-image-viewer-zoom-value\n class=\"rounded-full bg-black/35 px-3 py-1 text-xs font-700 text-white/95 sm:text-sm\"\n >\n {{ zoomValueText }}%\n </div>\n </div>\n\n <button\n type=\"button\"\n data-ds-image-viewer-close\n :aria-label=\"closeLabel\"\n class=\"h-11 w-11 flex items-center justify-center rounded-full border border-white/20 bg-black/35 text-white transition-colors hover:bg-black/55 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80\"\n @click=\"closeViewer\"\n >\n <span aria-hidden=\"true\" class=\"text-lg leading-none\">✕</span>\n </button>\n </div>\n </div>\n\n <div\n class=\"relative z-10 h-full w-full flex items-center justify-center px-16 py-16 sm:px-24 sm:py-20\"\n @wheel=\"onWheel\"\n >\n <button\n v-if=\"total > 1\"\n type=\"button\"\n data-ds-image-viewer-prev\n :aria-label=\"prevLabel\"\n class=\"absolute left-3 top-1/2 z-20 h-12 w-12 -translate-y-1/2 flex items-center justify-center rounded-full border border-white/20 bg-black/35 text-white transition-colors hover:bg-black/55 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80 sm:left-6\"\n @click=\"prev\"\n >\n <span aria-hidden=\"true\" class=\"text-xl leading-none\">‹</span>\n </button>\n\n <button\n v-if=\"total > 1\"\n type=\"button\"\n data-ds-image-viewer-next\n :aria-label=\"nextLabel\"\n class=\"absolute right-3 top-1/2 z-20 h-12 w-12 -translate-y-1/2 flex items-center justify-center rounded-full border border-white/20 bg-black/35 text-white transition-colors hover:bg-black/55 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80 sm:right-6\"\n @click=\"next\"\n >\n <span aria-hidden=\"true\" class=\"text-xl leading-none\">›</span>\n </button>\n\n <div class=\"h-full w-full flex items-center justify-center\">\n <img\n v-if=\"currentUrl\"\n ref=\"imageEl\"\n data-ds-image-viewer-image\n :src=\"currentUrl\"\n alt=\"\"\n draggable=\"false\"\n class=\"max-h-full max-w-full select-none object-contain will-change-transform\"\n :class=\"[imageTransitionClass, imageCursorClass]\"\n :style=\"imageStyle\"\n @load=\"onImageLoad\"\n @pointerdown=\"onPointerDown\"\n @pointermove=\"onPointerMove\"\n @pointerup=\"onPointerUp\"\n @pointercancel=\"onPointerUp\"\n >\n\n <div\n v-else\n class=\"rounded-[var(--ds-radius-xl)] border border-white/20 bg-black/25 px-4 py-3 text-sm text-white/80\"\n >\n {{ emptyText }}\n </div>\n </div>\n </div>\n\n <div class=\"pointer-events-none absolute inset-x-0 bottom-0 z-30 px-3 pb-3 sm:px-6 sm:pb-6\">\n <div class=\"pointer-events-auto mx-auto w-full max-w-max\">\n <slot\n name=\"toolbar\"\n v-bind=\"toolbarSlotProps\"\n >\n <div class=\"rounded-full border border-white/20 bg-black/35 p-1 backdrop-blur-sm flex items-center gap-1\">\n <button\n type=\"button\"\n data-ds-image-viewer-zoom-out\n :aria-label=\"zoomOutLabel\"\n class=\"h-11 min-w-11 px-2 flex items-center justify-center rounded-full text-sm font-600 text-white transition-colors hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80\"\n @click=\"zoomOut\"\n >\n −\n </button>\n\n <button\n type=\"button\"\n data-ds-image-viewer-zoom-reset\n :aria-label=\"resetZoomLabel\"\n class=\"h-11 min-w-11 px-3 flex items-center justify-center rounded-full text-xs font-700 text-white transition-colors hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80\"\n @click=\"resetTransform\"\n >\n 100%\n </button>\n\n <button\n type=\"button\"\n data-ds-image-viewer-zoom-in\n :aria-label=\"zoomInLabel\"\n class=\"h-11 min-w-11 px-2 flex items-center justify-center rounded-full text-sm font-600 text-white transition-colors hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80\"\n @click=\"zoomIn\"\n >\n +\n </button>\n\n <template v-if=\"$slots['toolbar-actions']\">\n <div class=\"mx-1 h-6 w-px bg-white/25\" aria-hidden=\"true\" />\n\n <slot\n name=\"toolbar-actions\"\n v-bind=\"toolbarSlotProps\"\n />\n\n <div class=\"mx-1 h-6 w-px bg-white/25\" aria-hidden=\"true\" />\n </template>\n\n <div v-else class=\"mx-1 h-6 w-px bg-white/25\" aria-hidden=\"true\" />\n\n <button\n type=\"button\"\n data-ds-image-viewer-rotate-left\n :aria-label=\"rotateLeftLabel\"\n class=\"h-11 min-w-11 px-2 flex items-center justify-center rounded-full text-sm font-600 text-white transition-colors hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80\"\n @click=\"rotateLeft\"\n >\n ↺\n </button>\n\n <button\n type=\"button\"\n data-ds-image-viewer-rotate-right\n :aria-label=\"rotateRightLabel\"\n class=\"h-11 min-w-11 px-2 flex items-center justify-center rounded-full text-sm font-600 text-white transition-colors hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80\"\n @click=\"rotateRight\"\n >\n ↻\n </button>\n </div>\n </slot>\n </div>\n </div>\n </DialogPanel>\n </TransitionChild>\n </div>\n </Dialog>\n </TransitionRoot>\n </teleport>\n</template>"],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { t as GrImageViewer_default } from "./chunks/GrImageViewer-
|
|
1
|
+
import { t as GrImageViewer_default } from "./chunks/GrImageViewer-CsvXQHM9.js";
|
|
2
2
|
import { t as grImageViewerConfig } from "../../chunks/config-CvGdaXvH.js";
|
|
3
3
|
export { GrImageViewer_default as GrImageViewer, GrImageViewer_default as default, grImageViewerConfig };
|
package/dist/components/GrPagination/chunks/{GrPagination-cD6rFO9W.js → GrPagination-BNDEqM8_.js}
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as GrButton_default } from "../../GrButton/chunks/GrButton-CPDKonoB.js";
|
|
2
2
|
import { t as useGranularityTranslations } from "../../../chunks/granularityI18n-LcJ88Vce.js";
|
|
3
|
-
import { t as GrSelect_default } from "../../GrSelect/chunks/GrSelect-
|
|
3
|
+
import { t as GrSelect_default } from "../../GrSelect/chunks/GrSelect-CnHdbPlf.js";
|
|
4
4
|
import { Fragment, computed, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, normalizeClass, openBlock, renderList, toDisplayString, unref, withCtx } from "vue";
|
|
5
5
|
//#region src/components/GrPagination/GrPagination.vue?vue&type=script&setup=true&lang.ts
|
|
6
6
|
var _hoisted_1 = ["aria-label"];
|