@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.
@@ -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":";AAQA,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"}
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"}
@@ -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 = { class: "relative z-10 h-full w-full flex items-center justify-center px-16 py-16 sm:px-24 sm:py-20" };
19
+ var _hoisted_8 = ["aria-label"];
20
20
  var _hoisted_9 = ["aria-label"];
21
- var _hoisted_10 = ["aria-label"];
22
- var _hoisted_11 = { class: "h-full w-full flex items-center justify-center" };
23
- var _hoisted_12 = ["src"];
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 _hoisted_14 = { class: "pointer-events-none absolute inset-x-0 bottom-0 z-30 px-3 pb-3 sm:px-6 sm:pb-6" };
29
- var _hoisted_15 = { class: "pointer-events-auto mx-auto w-full max-w-max" };
30
- var _hoisted_16 = { class: "rounded-full border border-white/20 bg-black/35 p-1 backdrop-blur-sm flex items-center gap-1" };
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 = ["aria-label"];
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 zoomValueText = computed(() => {
109
- const zoomPercent = scale.value * 100;
110
- if (Number.isInteger(zoomPercent)) return String(zoomPercent);
111
- return zoomPercent.toFixed(1).replace(TRAILING_ZERO_DECIMAL_RE, "");
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
- scale.value = Math.min(props.maxScale, Number((scale.value * props.zoomRate).toFixed(4)));
247
+ setScale(scale.value * props.zoomRate);
157
248
  }
158
249
  function zoomOut() {
159
- scale.value = Math.max(props.minScale, Number((scale.value / props.zoomRate).toFixed(4)));
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) return;
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", _hoisted_8, [
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, _hoisted_9)) : createCommentVNode("", true),
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, _hoisted_10)) : createCommentVNode("", true),
312
- createElementVNode("div", _hoisted_11, [currentUrl.value ? (openBlock(), createElementBlock("img", {
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 transition-transform duration-150 ease-out",
319
- style: normalizeStyle(imageStyle.value)
320
- }, null, 12, _hoisted_12)) : (openBlock(), createElementBlock("div", _hoisted_13, toDisplayString(__props.emptyText), 1))])
321
- ]),
322
- createElementVNode("div", _hoisted_14, [createElementVNode("div", _hoisted_15, [renderSlot(_ctx.$slots, "toolbar", {
323
- index: currentIndex.value,
324
- displayIndex: displayIndex.value,
325
- total: total.value,
326
- scale: scale.value,
327
- rotation: rotation.value,
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, _hoisted_17),
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, _hoisted_18),
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, _hoisted_19),
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", _hoisted_20)),
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, _hoisted_21),
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, _hoisted_22)
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-Cn1X-sWS.js";
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 };
@@ -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-W9lbAjRZ.js";
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"];