@feugene/granularity 0.31.1 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/{config-Dv_r92GL.js → config-2vrOFn2f.js} +2 -2
- package/dist/chunks/{config-Dv_r92GL.js.map → config-2vrOFn2f.js.map} +1 -1
- package/dist/chunks/{config-g8-JGZ_Y.js → config-BscVxZR3.js} +1 -1
- package/dist/chunks/{config-g8-JGZ_Y.js.map → config-BscVxZR3.js.map} +1 -1
- package/dist/chunks/{grBadgeStyles-D8Mz73B5.js → grBadgeStyles-DV0DrAew.js} +17 -1
- package/dist/chunks/grBadgeStyles-DV0DrAew.js.map +1 -0
- package/dist/chunks/{grChipStyles-3JZApw03.js → grChipStyles-DcDyxAL0.js} +1 -1
- package/dist/chunks/{grChipStyles-3JZApw03.js.map → grChipStyles-DcDyxAL0.js.map} +1 -1
- package/dist/chunks/{granular-provider-Da2CJO6j.js → granular-provider-CrQua8PT.js} +2 -2
- package/dist/chunks/{granular-provider-Da2CJO6j.js.map → granular-provider-CrQua8PT.js.map} +1 -1
- package/dist/chunks/{overlayStack-C8cfsqXa.js → overlayStack-0CZS99Da.js} +40 -6
- package/dist/chunks/overlayStack-0CZS99Da.js.map +1 -0
- package/dist/chunks/{safelist-CtiHPgrj.js → safelist-x8USecK8.js} +3 -1
- package/dist/chunks/safelist-x8USecK8.js.map +1 -0
- package/dist/chunks/{useModalOverlay-Cv5hF63W.js → useModalOverlay-D__CqLGn.js} +3 -0
- package/dist/chunks/useModalOverlay-D__CqLGn.js.map +1 -0
- package/dist/components/GrAutocomplete/chunks/{GrAutocomplete-rYEY15UI.js → GrAutocomplete-CoQjEF8h.js} +2 -2
- package/dist/components/GrAutocomplete/chunks/{GrAutocomplete-rYEY15UI.js.map → GrAutocomplete-CoQjEF8h.js.map} +1 -1
- package/dist/components/GrAutocomplete/index.js +1 -1
- package/dist/components/GrBadge/chunks/{GrBadge-D2MfThPz.js → GrBadge-DpUDXKbq.js} +9 -4
- package/dist/components/GrBadge/chunks/GrBadge-DpUDXKbq.js.map +1 -0
- package/dist/components/GrBadge/index.js +3 -3
- package/dist/components/GrBadge/styles.css +1 -1
- package/dist/components/GrChip/chunks/{GrChip-C0vU69tf.js → GrChip-BTivtwff.js} +1 -1
- package/dist/components/GrChip/chunks/{GrChip-C0vU69tf.js.map → GrChip-BTivtwff.js.map} +1 -1
- package/dist/components/GrChip/index.js +2 -2
- package/dist/components/GrColorPicker/chunks/{GrColorPicker-B20gQ6AR.js → GrColorPicker-p2XH_8cE.js} +1 -1
- package/dist/components/GrColorPicker/chunks/{GrColorPicker-B20gQ6AR.js.map → GrColorPicker-p2XH_8cE.js.map} +1 -1
- package/dist/components/GrColorPicker/index.js +1 -1
- package/dist/components/GrCommandPalette/chunks/{GrCommandPalette-Cd0Yo3Dw.js → GrCommandPalette-BXbh3q7O.js} +1 -1
- package/dist/components/GrCommandPalette/chunks/{GrCommandPalette-Cd0Yo3Dw.js.map → GrCommandPalette-BXbh3q7O.js.map} +1 -1
- package/dist/components/GrCommandPalette/index.js +1 -1
- package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-DOsbRhoZ.js → GrConfirmDialog-CUP43q__.js} +1 -1
- package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-DOsbRhoZ.js.map → GrConfirmDialog-CUP43q__.js.map} +1 -1
- package/dist/components/GrConfirmDialog/index.js +1 -1
- package/dist/components/GrContextMenu/chunks/{GrContextMenu-_FKwQvuQ.js → GrContextMenu-ClkVjdcP.js} +1 -1
- package/dist/components/GrContextMenu/chunks/{GrContextMenu-_FKwQvuQ.js.map → GrContextMenu-ClkVjdcP.js.map} +1 -1
- package/dist/components/GrContextMenu/index.js +1 -1
- package/dist/components/GrDataTable/chunks/{GrDataTable-CAGgRk27.js → GrDataTable-4JAgOQ-7.js} +1 -1
- package/dist/components/GrDataTable/chunks/{GrDataTable-CAGgRk27.js.map → GrDataTable-4JAgOQ-7.js.map} +1 -1
- package/dist/components/GrDataTable/index.js +1 -1
- package/dist/components/GrDialog/chunks/{GrDialog-S1Vl-OhX.js → GrDialog-BiMJ9_T4.js} +1 -1
- package/dist/components/GrDialog/chunks/{GrDialog-S1Vl-OhX.js.map → GrDialog-BiMJ9_T4.js.map} +1 -1
- package/dist/components/GrDialog/index.js +1 -1
- package/dist/components/GrDialogService/chunks/{GrDialogService-Cuhc4YLp.js → GrDialogService-BxNK6crD.js} +2 -2
- package/dist/components/GrDialogService/chunks/{GrDialogService-Cuhc4YLp.js.map → GrDialogService-BxNK6crD.js.map} +1 -1
- package/dist/components/GrDialogService/index.js +1 -1
- package/dist/components/GrDrawer/chunks/{GrDrawer-C0YqVE9e.js → GrDrawer-Ce2l3zaG.js} +5 -4
- package/dist/components/GrDrawer/chunks/GrDrawer-Ce2l3zaG.js.map +1 -0
- package/dist/components/GrDrawer/index.js +1 -1
- package/dist/components/GrImageViewer/chunks/{GrImageViewer-CyVN6Zpc.js → GrImageViewer-CG4vUBg_.js} +4 -3
- package/dist/components/GrImageViewer/chunks/GrImageViewer-CG4vUBg_.js.map +1 -0
- package/dist/components/GrImageViewer/index.js +1 -1
- package/dist/components/GrInputTag/chunks/{GrInputTag-Cj4A9Xgs.js → GrInputTag-ByD9NugQ.js} +2 -2
- package/dist/components/GrInputTag/chunks/{GrInputTag-Cj4A9Xgs.js.map → GrInputTag-ByD9NugQ.js.map} +1 -1
- package/dist/components/GrInputTag/index.js +1 -1
- package/dist/components/GrModal/chunks/{GrModal-CCoWZuIl.js → GrModal-D0mf1SAx.js} +3 -2
- package/dist/components/GrModal/chunks/GrModal-D0mf1SAx.js.map +1 -0
- package/dist/components/GrModal/index.js +1 -1
- package/dist/components/GrPagination/chunks/{GrPagination-B_kAhoAV.js → GrPagination-FiiXzVyv.js} +1 -1
- package/dist/components/GrPagination/chunks/{GrPagination-B_kAhoAV.js.map → GrPagination-FiiXzVyv.js.map} +1 -1
- package/dist/components/GrPagination/index.js +1 -1
- package/dist/components/GrPopover/chunks/{GrPopover-CofG-Tfe.js → GrPopover-faJanmGs.js} +1 -1
- package/dist/components/GrPopover/chunks/{GrPopover-CofG-Tfe.js.map → GrPopover-faJanmGs.js.map} +1 -1
- package/dist/components/GrPopover/index.js +1 -1
- package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-BuJQMZLM.js → GrPromptDialog-C6QGC7QJ.js} +1 -1
- package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-BuJQMZLM.js.map → GrPromptDialog-C6QGC7QJ.js.map} +1 -1
- package/dist/components/GrPromptDialog/index.js +1 -1
- package/dist/components/GrSelect/chunks/{GrSelect-gCeRFgQo.js → GrSelect-Dt8iqruc.js} +2 -2
- package/dist/components/GrSelect/chunks/{GrSelect-gCeRFgQo.js.map → GrSelect-Dt8iqruc.js.map} +1 -1
- package/dist/components/GrSelect/index.js +1 -1
- package/dist/components/GrTable/chunks/{GrTable-Zcdz2St_.js → GrTable-jhUTczjv.js} +12 -1
- package/dist/components/GrTable/chunks/GrTable-jhUTczjv.js.map +1 -0
- package/dist/components/GrTable/index.js +1 -1
- package/dist/composables/useFloating.js +2 -22
- package/dist/composables/useFloating.js.map +1 -1
- package/dist/composables/useOverlayLayer.js +10 -3
- package/dist/composables/useOverlayLayer.js.map +1 -1
- package/dist/granular-provider-node.js +1 -1
- package/dist/granular-provider.js +1 -1
- package/dist/index.js +22 -22
- package/dist/types/components/GrBadge/GrBadge.vue.d.ts +11 -0
- package/dist/types/components/GrBadge/grBadgeStyles.d.ts +11 -0
- package/dist/types/composables/internal/overlayStack.d.ts +30 -0
- package/dist/types/composables/internal/useModalOverlay.d.ts +14 -0
- package/dist/types/composables/useOverlayLayer.d.ts +8 -0
- package/dist/web-types.json +5 -1
- package/package.json +3 -3
- package/dist/chunks/grBadgeStyles-D8Mz73B5.js.map +0 -1
- package/dist/chunks/overlayStack-C8cfsqXa.js.map +0 -1
- package/dist/chunks/safelist-CtiHPgrj.js.map +0 -1
- package/dist/chunks/useModalOverlay-Cv5hF63W.js.map +0 -1
- package/dist/components/GrBadge/chunks/GrBadge-D2MfThPz.js.map +0 -1
- package/dist/components/GrDrawer/chunks/GrDrawer-C0YqVE9e.js.map +0 -1
- package/dist/components/GrImageViewer/chunks/GrImageViewer-CyVN6Zpc.js.map +0 -1
- package/dist/components/GrModal/chunks/GrModal-CCoWZuIl.js.map +0 -1
- package/dist/components/GrTable/chunks/GrTable-Zcdz2St_.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feugene/granularity",
|
|
3
3
|
"description": "Granularity design system package with Vue 3 components, consumed via the `@feugene/unocss-preset-granular` preset.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.33.0",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Evgeniy Fureev",
|
|
@@ -591,7 +591,7 @@
|
|
|
591
591
|
"./package.json": "./package.json"
|
|
592
592
|
},
|
|
593
593
|
"peerDependencies": {
|
|
594
|
-
"@feugene/fint-i18n": "
|
|
594
|
+
"@feugene/fint-i18n": ">=0.6.0 <1.0.0",
|
|
595
595
|
"@feugene/unocss-preset-granular": "^0.11.0",
|
|
596
596
|
"@floating-ui/dom": "^1.8.0",
|
|
597
597
|
"vue": "^3.5.0"
|
|
@@ -606,7 +606,7 @@
|
|
|
606
606
|
},
|
|
607
607
|
"devDependencies": {
|
|
608
608
|
"@antfu/eslint-config": "^9.1.0",
|
|
609
|
-
"@feugene/fint-i18n": "^0.
|
|
609
|
+
"@feugene/fint-i18n": "^0.7.0",
|
|
610
610
|
"@feugene/granularity-test-kit": "^0.7.0",
|
|
611
611
|
"@feugene/unocss-preset-granular": "^0.11.0",
|
|
612
612
|
"@floating-ui/dom": "^1.8.0",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"grBadgeStyles-D8Mz73B5.js","names":[],"sources":["../../src/components/GrBadge/grBadgeStyles.ts"],"sourcesContent":["import type { GrComponentSize } from '../shared/sizes'\n\nimport type { GrTone } from '../shared/tones'\n\nexport type GrBadgeTone = GrTone\nexport type GrBadgeSize = GrComponentSize\nexport type GrBadgeRadius = 'square' | 'semi' | 'round'\nexport const sizeClassBySize: Record<GrBadgeSize, string> = {\n xs: 'px-2 py-0.4 text-[length:var(--gr-control-text-2xs)] leading-[var(--gr-control-leading-2xs)]',\n sm: 'px-2.5 py-0.5 text-[length:var(--gr-control-text-xs)] leading-[var(--gr-control-leading-xs)]',\n md: 'px-3 py-1 text-[length:var(--gr-control-text-sm)] leading-[var(--gr-control-leading-sm)]',\n lg: 'px-3.5 py-1.5 text-[length:var(--gr-control-text-md)] leading-[var(--gr-control-leading-md)]',\n}\nexport const semiRadiusClassBySize: Record<GrBadgeSize, string> = {\n xs: 'rounded-[var(--gr-badge-semi-radius-xs,3px)]',\n sm: 'rounded-[var(--gr-badge-semi-radius-sm,3px)]',\n md: 'rounded-[var(--gr-badge-semi-radius-md,5px)]',\n lg: 'rounded-[var(--gr-badge-semi-radius-lg,7px)]',\n}\nexport const lightToneClassByTone: Record<GrBadgeTone, string> = {\n neutral: 'bg-[var(--gr-muted)] text-[var(--gr-fg)] border-[var(--gr-brd)]',\n primary:\n 'bg-[var(--gr-accent)] text-[var(--gr-accent-fg)] border-[color-mix(in_srgb,var(--gr-primary)_30%,var(--gr-accent))]',\n // Текст на тонированной подложке — только `-text`, никогда не насыщенный тон:\n // `--gr-success` на `--gr-success-light` даёт 2.24:1, `-text` — 6.78:1.\n // slate/azure ниже уже были сделаны правильно.\n success: 'bg-[var(--gr-success-light)] text-[var(--gr-success-text)] border-[color-mix(in_srgb,var(--gr-success)_30%,var(--gr-success-light))]',\n warning: 'bg-[var(--gr-warning-light)] text-[var(--gr-warning-text)] border-[color-mix(in_srgb,var(--gr-warning)_30%,var(--gr-warning-light))]',\n danger: 'bg-[var(--gr-danger-light)] text-[var(--gr-danger-text)] border-[color-mix(in_srgb,var(--gr-danger)_30%,var(--gr-danger-light))]',\n info: 'bg-[var(--gr-info-light)] text-[var(--gr-info-text)] border-[color-mix(in_srgb,var(--gr-info)_30%,var(--gr-info-light))]',\n slate: 'bg-[var(--gr-slate-light)] text-[var(--gr-slate-text)] border-[color-mix(in_srgb,var(--gr-slate)_30%,var(--gr-slate-light))]',\n azure: 'bg-[var(--gr-azure-light)] text-[var(--gr-azure-text)] border-[color-mix(in_srgb,var(--gr-azure)_30%,var(--gr-azure-light))]',\n}\n/**\n * Заливка filled-бейджа идёт через покомпонентный слой `--gr-badge-{tone}-*`, а\n * не напрямую из роли тона, потому что нужный вес у тем разный.\n *\n * В светлой теме `--gr-{tone}` — это яркая заливка под **тёмный** текст (`-fg`\n * там обязан быть тёмным: белый на `--gr-success` даёт 2.54:1), и filled-бейдж\n * читался как тяжёлая почти чёрная плашка. Слой уводит его на `-solid`/`-solid-fg`\n * — заливку кнопочного веса под светлый текст, ту же, что у solid-кнопок.\n * В тёмной теме пастельная заливка с тёмным текстом — штатная конвенция, и слой\n * оставляет прежние роли. Значения — `themes/{light,dark}.css`.\n */\nexport const darkToneClassByTone: Record<GrBadgeTone, string> = {\n // Нейтральный вес — инверсия страницы, а не тон: `-solid`-роли у него нет.\n neutral: 'bg-[var(--gr-badge-neutral-bg,var(--gr-fg))] text-[var(--gr-badge-neutral-fg,var(--gr-bg))] border-[color-mix(in_srgb,var(--gr-fg)_35%,var(--gr-brd))]',\n primary: 'bg-[var(--gr-badge-primary-bg,var(--gr-primary-solid))] text-[var(--gr-badge-primary-fg,var(--gr-primary-solid-fg))] border-[var(--gr-badge-primary-bg,var(--gr-primary-solid))]',\n success: 'bg-[var(--gr-badge-success-bg,var(--gr-success-solid))] text-[var(--gr-badge-success-fg,var(--gr-success-solid-fg))] border-[var(--gr-badge-success-bg,var(--gr-success-solid))]',\n warning: 'bg-[var(--gr-badge-warning-bg,var(--gr-warning-solid))] text-[var(--gr-badge-warning-fg,var(--gr-warning-solid-fg))] border-[var(--gr-badge-warning-bg,var(--gr-warning-solid))]',\n danger: 'bg-[var(--gr-badge-danger-bg,var(--gr-danger-solid))] text-[var(--gr-badge-danger-fg,var(--gr-danger-solid-fg))] border-[var(--gr-badge-danger-bg,var(--gr-danger-solid))]',\n info: 'bg-[var(--gr-badge-info-bg,var(--gr-info-solid))] text-[var(--gr-badge-info-fg,var(--gr-info-solid-fg))] border-[var(--gr-badge-info-bg,var(--gr-info-solid))]',\n slate: 'bg-[var(--gr-badge-slate-bg,var(--gr-slate-solid))] text-[var(--gr-badge-slate-fg,var(--gr-slate-solid-fg))] border-[var(--gr-badge-slate-bg,var(--gr-slate-solid))]',\n azure: 'bg-[var(--gr-badge-azure-bg,var(--gr-azure-solid))] text-[var(--gr-badge-azure-fg,var(--gr-azure-solid-fg))] border-[var(--gr-badge-azure-bg,var(--gr-azure-solid))]',\n}\n/**\n * Экспортируются ради `GrChip`: у чипа те же три радиуса и та же палитра тонов,\n * и копия этих двух ветвлений разошлась бы с оригиналом молча. Свои у чипа\n * только размеры — он интерактивен, и цель нажатия у него не метки, а контрола.\n */\nexport function radiusClass(radius: GrBadgeRadius, size: GrBadgeSize): string {\n if (radius === 'square')\n return 'rounded-[var(--gr-radius-none)]'\n if (radius === 'semi')\n return semiRadiusClassBySize[size]\n return 'rounded-[var(--gr-radius-full)]'\n}\nexport function toneClass(tone: GrBadgeTone, dark: boolean): string {\n return dark ? darkToneClassByTone[tone] : lightToneClassByTone[tone]\n}\nexport function grBadgeClass(options: { tone: GrBadgeTone, dark: boolean, size: GrBadgeSize, radius: GrBadgeRadius }): string {\n return [\n radiusClass(options.radius, options.size),\n sizeClassBySize[options.size],\n toneClass(options.tone, options.dark),\n ].join(' ')\n}\n"],"mappings":";AAOA,IAAa,kBAA+C;CAC1D,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;AACN;AACA,IAAa,wBAAqD;CAChE,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;AACN;AACA,IAAa,uBAAoD;CAC/D,SAAS;CACT,SACE;CAIF,SAAS;CACT,SAAS;CACT,QAAQ;CACR,MAAM;CACN,OAAO;CACP,OAAO;AACT;;;;;;;;;;;;AAYA,IAAa,sBAAmD;CAE9D,SAAS;CACT,SAAS;CACT,SAAS;CACT,SAAS;CACT,QAAQ;CACR,MAAM;CACN,OAAO;CACP,OAAO;AACT;;;;;;AAMA,SAAgB,YAAY,QAAuB,MAA2B;CAC5E,IAAI,WAAW,UACb,OAAO;CACT,IAAI,WAAW,QACb,OAAO,sBAAsB;CAC/B,OAAO;AACT;AACA,SAAgB,UAAU,MAAmB,MAAuB;CAClE,OAAO,OAAO,oBAAoB,QAAQ,qBAAqB;AACjE;AACA,SAAgB,aAAa,SAAiG;CAC5H,OAAO;EACL,YAAY,QAAQ,QAAQ,QAAQ,IAAI;EACxC,gBAAgB,QAAQ;EACxB,UAAU,QAAQ,MAAM,QAAQ,IAAI;CACtC,CAAC,CAAC,KAAK,GAAG;AACZ"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"overlayStack-C8cfsqXa.js","names":[],"sources":["../../src/composables/internal/overlayStack.ts"],"sourcesContent":["/**\n * Единый стек слоёв оверлеев: модалки, drawer'ы, просмотрщик, панели селектов,\n * дропдауны, подсказки — всё, что открывается поверх страницы.\n *\n * Список один, и порядок регистрации совпадает с порядком открытия, но из него\n * выводятся **две разные вершины**:\n *\n * - **Esc** → последний слой любого рода. Дропдаун, открытый внутри модалки,\n * обязан закрыться первым — пользователь видит верхним именно его.\n * - **`inert`** → ставится на модальные слои ниже последнего **модального**.\n * Немодальный слой сверху модалку не понижает: иначе окно ушло бы в `inert`\n * вместе со своим же открытым дропдауном и перестало отвечать.\n * - **высота** → `openModalCount()` поднимает floating-панель над окном, из\n * которого её открыли: телепортированная панель лежит рядом с корнем окна,\n * и статический `z-index` дропдауна оставлял её под ним.\n *\n * Вести эти две вершины в двух независимых реестрах нельзя: реестры описывают\n * один и тот же порядок и расходятся молча — достаточно оверлея, который\n * зарегистрировался в одном и забыл про другой.\n *\n * Ловушку фокуса стек не реализует сам, но питает её (`useFocusTrap`): он\n * видит слои из **разных деревьев рендера** (диалоги `useDialogService`\n * монтируются отдельным `render()` в `body`) и знает их корни.\n */\n\nimport { isComposingEvent } from '../../internal/keyboard'\n\nexport interface OverlayLayer {\n /** Уникальный id зарегистрированного слоя. */\n id: number\n /**\n * Модальный слой: блокирует страницу и участвует в вычислении `inert`.\n * Немодальные (поповеры, меню, подсказки) — только в очереди Esc.\n */\n modal: boolean\n /** Закрывать ли слой по Esc (реактивный геттер: `closeOnEsc` и подобные). */\n shouldClose: () => boolean\n /** Закрыть слой. */\n close: () => void\n /** Уведомление «этот модальный слой сейчас верхний». Только для `modal`. */\n setTopmost?: (isTopmost: boolean) => void\n /**\n * Корень слоя. Ловушка фокуса нижнего слоя обязана считать его своим:\n * панель селекта, открытого внутри модалки, телепортирована в `body` и лежит\n * вне DOM-поддерева окна, но для пользователя это тот же слой.\n */\n root?: () => HTMLElement | null\n}\n\nconst stack: OverlayLayer[] = []\nlet listening = false\nlet nextId = 1\n\n/** Пересчитывает верхний **модальный** слой и уведомляет все модальные. */\nfunction syncTopmost(): void {\n const modals = stack.filter(layer => layer.modal)\n const top = modals[modals.length - 1]\n for (const layer of modals) layer.setTopmost?.(layer === top)\n}\n\nfunction handleKeydown(event: KeyboardEvent): void {\n if (event.key !== 'Escape')\n return\n // Esc во время IME-композиции отменяет её, а не закрывает слой.\n if (isComposingEvent(event))\n return\n if (stack.length === 0)\n return\n\n const top = stack[stack.length - 1]\n\n // Гасим всегда, даже если верхний слой не закрывается по Esc: иначе нажатие\n // «провалится» на слой ниже и закроет не то, что видит пользователь.\n event.preventDefault()\n event.stopPropagation()\n if (typeof event.stopImmediatePropagation === 'function')\n event.stopImmediatePropagation()\n\n if (top.shouldClose())\n top.close()\n}\n\nfunction startListening(): void {\n if (listening)\n return\n if (typeof window === 'undefined')\n return\n // Capture-фаза опережает любые локальные обработчики: Esc обязан достаться\n // верхнему слою, а не тому, кто первым подписался.\n window.addEventListener('keydown', handleKeydown, true)\n listening = true\n}\n\nfunction stopListening(): void {\n if (!listening)\n return\n if (typeof window === 'undefined')\n return\n window.removeEventListener('keydown', handleKeydown, true)\n listening = false\n}\n\n/** Регистрирует слой как верхний и возвращает его id. */\nexport function pushOverlayLayer(layer: Omit<OverlayLayer, 'id'>): number {\n const id = nextId++\n stack.push({ ...layer, id })\n startListening()\n syncTopmost()\n return id\n}\n\n/** Снимает слой со стека (при закрытии или размонтировании). */\nexport function removeOverlayLayer(id: number): void {\n const index = stack.findIndex(item => item.id === id)\n if (index >= 0)\n stack.splice(index, 1)\n if (stack.length === 0)\n stopListening()\n syncTopmost()\n}\n\n/**\n * Корни слоёв, открытых **поверх** данного.\n *\n * Ими ловушка фокуса дополняет свой контейнер: всё, что открыто изнутри слоя\n * (панель селекта, меню, подсказка), лежит в `body` отдельным поддеревом, и\n * без этого списка ловушка утаскивала бы из него фокус обратно в окно.\n */\nexport function layerRootsAbove(id: number): HTMLElement[] {\n const index = stack.findIndex(layer => layer.id === id)\n if (index < 0)\n return []\n\n return stack\n .slice(index + 1)\n .map(layer => layer.root?.() ?? null)\n .filter((root): root is HTMLElement => root !== null)\n}\n\n/** Корни всех зарегистрированных слоёв. */\nexport function allLayerRoots(): HTMLElement[] {\n return stack\n .map(layer => layer.root?.() ?? null)\n .filter((root): root is HTMLElement => root !== null)\n}\n\n/** Сколько слоёв открыто. Для тестов и диагностики. */\nexport function overlayStackSize(): number {\n return stack.length\n}\n\n/**\n * Сколько **модальных** слоёв открыто прямо сейчас.\n *\n * Читает `useFloating`, когда считает высоту панели: панель телепортирована в\n * общий портал и лежит рядом с корнем окна, а не внутри него, поэтому свой\n * стек-контекст её не спасает — высоту приходится брать из порядка слоёв.\n */\nexport function openModalCount(): number {\n return stack.reduce((count, layer) => count + (layer.modal ? 1 : 0), 0)\n}\n\n/** Тестовая/служебная очистка стека. */\nexport function resetOverlayStack(): void {\n stack.splice(0, stack.length)\n stopListening()\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAiDA,IAAM,QAAwB,CAAC;AAC/B,IAAI,YAAY;AAChB,IAAI,SAAS;;AAGb,SAAS,cAAoB;CAC3B,MAAM,SAAS,MAAM,QAAO,UAAS,MAAM,KAAK;CAChD,MAAM,MAAM,OAAO,OAAO,SAAS;CACnC,KAAK,MAAM,SAAS,QAAQ,MAAM,aAAa,UAAU,GAAG;AAC9D;AAEA,SAAS,cAAc,OAA4B;CACjD,IAAI,MAAM,QAAQ,UAChB;CAEF,IAAI,iBAAiB,KAAK,GACxB;CACF,IAAI,MAAM,WAAW,GACnB;CAEF,MAAM,MAAM,MAAM,MAAM,SAAS;CAIjC,MAAM,eAAe;CACrB,MAAM,gBAAgB;CACtB,IAAI,OAAO,MAAM,6BAA6B,YAC5C,MAAM,yBAAyB;CAEjC,IAAI,IAAI,YAAY,GAClB,IAAI,MAAM;AACd;AAEA,SAAS,iBAAuB;CAC9B,IAAI,WACF;CACF,IAAI,OAAO,WAAW,aACpB;CAGF,OAAO,iBAAiB,WAAW,eAAe,IAAI;CACtD,YAAY;AACd;AAEA,SAAS,gBAAsB;CAC7B,IAAI,CAAC,WACH;CACF,IAAI,OAAO,WAAW,aACpB;CACF,OAAO,oBAAoB,WAAW,eAAe,IAAI;CACzD,YAAY;AACd;;AAGA,SAAgB,iBAAiB,OAAyC;CACxE,MAAM,KAAK;CACX,MAAM,KAAK;EAAE,GAAG;EAAO;CAAG,CAAC;CAC3B,eAAe;CACf,YAAY;CACZ,OAAO;AACT;;AAGA,SAAgB,mBAAmB,IAAkB;CACnD,MAAM,QAAQ,MAAM,WAAU,SAAQ,KAAK,OAAO,EAAE;CACpD,IAAI,SAAS,GACX,MAAM,OAAO,OAAO,CAAC;CACvB,IAAI,MAAM,WAAW,GACnB,cAAc;CAChB,YAAY;AACd;;;;;;;;AASA,SAAgB,gBAAgB,IAA2B;CACzD,MAAM,QAAQ,MAAM,WAAU,UAAS,MAAM,OAAO,EAAE;CACtD,IAAI,QAAQ,GACV,OAAO,CAAC;CAEV,OAAO,MACJ,MAAM,QAAQ,CAAC,CAAC,CAChB,KAAI,UAAS,MAAM,OAAO,KAAK,IAAI,CAAC,CACpC,QAAQ,SAA8B,SAAS,IAAI;AACxD;;;;;;;;AAqBA,SAAgB,iBAAyB;CACvC,OAAO,MAAM,QAAQ,OAAO,UAAU,SAAS,MAAM,QAAQ,IAAI,IAAI,CAAC;AACxE"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"safelist-CtiHPgrj.js","names":[],"sources":["../../src/components/GrBadge/safelist.ts"],"sourcesContent":["import { splitClassTokens } from '../shared/classTokens'\nimport {\n darkToneClassByTone,\n lightToneClassByTone,\n semiRadiusClassBySize,\n sizeClassBySize,\n} from './grBadgeStyles'\n\nexport const grBadgeSafelist = [...new Set([\n ...Object.values(sizeClassBySize).flatMap(splitClassTokens),\n ...Object.values(semiRadiusClassBySize).flatMap(splitClassTokens),\n 'rounded-[var(--gr-radius-none)]',\n 'rounded-[var(--gr-radius-full)]',\n ...Object.values(lightToneClassByTone).flatMap(splitClassTokens),\n ...Object.values(darkToneClassByTone).flatMap(splitClassTokens),\n])]\n"],"mappings":";;;AAQA,IAAa,kBAAkB,CAAC,mBAAG,IAAI,IAAI;CACzC,GAAG,OAAO,OAAO,eAAe,CAAC,CAAC,QAAQ,gBAAgB;CAC1D,GAAG,OAAO,OAAO,qBAAqB,CAAC,CAAC,QAAQ,gBAAgB;CAChE;CACA;CACA,GAAG,OAAO,OAAO,oBAAoB,CAAC,CAAC,QAAQ,gBAAgB;CAC/D,GAAG,OAAO,OAAO,mBAAmB,CAAC,CAAC,QAAQ,gBAAgB;AAChE,CAAC,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"useModalOverlay-Cv5hF63W.js","names":[],"sources":["../../src/composables/internal/useInertOthers.ts","../../src/composables/internal/useOverlayPresence.ts","../../src/composables/internal/useScrollLock.ts","../../src/composables/internal/useModalOverlay.ts"],"sourcesContent":["import type { Ref } from 'vue'\nimport { onScopeDispose, watch } from 'vue'\n\nimport { inertableOutside, markInert } from './inert'\n\n/**\n * Пока слой активен, всё вне его ветки помечено `inert`: от корня оверлея вверх\n * до `<body>`, на каждом уровне — соседи.\n *\n * Это вторая половина модальности: `z-index` перекрывает страницу глазами,\n * `inert` — клавиатурой и скринридером.\n */\nexport function useInertOthers(root: Ref<HTMLElement | null>, active: () => boolean): void {\n let release: (() => void) | undefined\n\n function apply(): void {\n release?.()\n release = markInert(inertableOutside(root.value))\n }\n\n function clear(): void {\n release?.()\n release = undefined\n }\n\n watch(\n () => Boolean(active()) && Boolean(root.value),\n (isActive) => {\n if (isActive)\n apply()\n else clear()\n },\n { immediate: true, flush: 'post' },\n )\n\n onScopeDispose(clear)\n}\n","import type { Ref } from 'vue'\nimport { ref, watch } from 'vue'\n\nexport interface OverlayPresence {\n /** Смонтировано ли поддерево слоя: держится до конца leave-анимации. */\n mounted: Ref<boolean>\n /** Показан ли слой: этим переключаются `<Transition>` внутри. */\n visible: Ref<boolean>\n /** Вызывается из `@after-leave` панели. */\n onPanelAfterLeave: () => void\n}\n\n/**\n * Присутствие оверлея в DOM отдельно от его видимости.\n *\n * Штатный `<Transition>` анимирует свой элемент, но не координирует соседей:\n * убери обёртку по `v-if` — и подложка с панелью исчезнут мгновенно, без\n * leave-анимации. Поэтому присутствие поддерева и видимость слоя — два разных\n * флага: первый снимается только по концу leave-анимации панели.\n *\n * Видимость включается **в том же такте**, что и монтирование: содержимое окна\n * обязано появляться в DOM сразу после открытия — на этом стоит и код\n * потребителя, и внутренние компоненты, которые ищут в панели свои элементы.\n * Enter-анимацию это не отменяет: `<Transition appear>` проигрывает её и на\n * первом рендере ребёнка.\n */\nexport function useOverlayPresence(open: Ref<boolean>): OverlayPresence {\n const mounted = ref(open.value)\n const visible = ref(open.value)\n\n watch(open, (isOpen) => {\n if (isOpen)\n mounted.value = true\n visible.value = isOpen\n })\n\n function onPanelAfterLeave(): void {\n // Пока панель уезжала, слой могли открыть заново — тогда размонтировать\n // нечего и незачем.\n if (!open.value)\n mounted.value = false\n }\n\n return { mounted, visible, onPanelAfterLeave }\n}\n","/**\n * useScrollLock — общий **reference-counted** lock скролла `<body>` для оверлеев\n * (GrModal, GrDrawer, GrImageViewer).\n *\n * Зачем счётчик: состояние `<body>` принадлежит не оверлею, а их совокупности.\n * Держи каждый свой `savedBodyOverflow`, и закрытие A при ещё открытой B вернуло\n * бы странице прокрутку под открытым окном — закрывают не всегда в порядке\n * открытия. Поэтому состояние сохраняется один раз (переход счётчика 0→1) и\n * восстанавливается только когда закрыт последний оверлей (1→0).\n *\n * Плюс компенсация ширины скроллбара: при `overflow: hidden` скроллбар исчезает и\n * контент дёргается вправо — добавляем `padding-right` на его ширину.\n *\n * Отдельная ветка — iOS Safari: там `overflow: hidden` фон не держит, страница\n * продолжает «резинить» под открытым окном. Лечится тем же, чем лечил\n * HeadlessUI (`handleIOSLocking`): гасим `touchmove` вне скроллящихся областей\n * оверлея и возвращаем позицию прокрутки после снятия лока.\n */\n\ninterface SavedBodyStyle {\n overflow: string\n paddingRight: string\n overscrollBehavior: string\n}\n\nlet lockCount = 0\nlet saved: SavedBodyStyle | null = null\nlet savedScrollY = 0\nlet releaseTouchGuard: (() => void) | null = null\n\n/**\n * iOS определяем по платформе, а не по ширине экрана: лечение специфично для\n * движка, а не для размера. `maxTouchPoints` отличает iPadOS, который со времён\n * 13-й версии представляется десктопным Mac.\n */\nfunction isIOS(): boolean {\n if (typeof navigator === 'undefined')\n return false\n return /iP(?:hone|ad|od)/.test(navigator.platform)\n || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)\n}\n\n/** Прокручиваемая область оверлея — единственное, чему `touchmove` разрешён. */\nfunction isScrollableOverlayArea(target: EventTarget | null): boolean {\n if (!(target instanceof Element))\n return false\n\n for (let node: Element | null = target; node; node = node.parentElement) {\n if (!(node instanceof HTMLElement))\n continue\n if (node.hasAttribute('data-gr-overlay-root'))\n return true\n }\n\n return false\n}\n\nfunction applyTouchGuard(): void {\n if (!isIOS())\n return\n\n const onTouchMove = (event: TouchEvent): void => {\n // Внутри оверлея скролл законен (длинное окно, лента изображений), а всё\n // остальное — это и есть фон, который лок обязан держать.\n if (isScrollableOverlayArea(event.target))\n return\n if (event.cancelable)\n event.preventDefault()\n }\n\n document.addEventListener('touchmove', onTouchMove, { passive: false })\n releaseTouchGuard = () => document.removeEventListener('touchmove', onTouchMove)\n}\n\nfunction getScrollbarWidth(): number {\n return window.innerWidth - document.documentElement.clientWidth\n}\n\nfunction applyLock(): void {\n const body = document.body\n saved = {\n overflow: body.style.overflow,\n paddingRight: body.style.paddingRight,\n overscrollBehavior: body.style.overscrollBehavior,\n }\n savedScrollY = window.scrollY\n\n // Ширину скроллбара считаем ДО `overflow: hidden` (после — `clientWidth` меняется).\n const scrollbarWidth = getScrollbarWidth()\n\n body.style.overflow = 'hidden'\n // Прокрутка, «доехавшая» до края внутри окна, не должна продолжаться фоном.\n body.style.overscrollBehavior = 'contain'\n if (scrollbarWidth > 0) {\n const currentPadding = Number.parseFloat(getComputedStyle(body).paddingRight) || 0\n body.style.paddingRight = `${currentPadding + scrollbarWidth}px`\n }\n\n applyTouchGuard()\n}\n\nfunction releaseLock(): void {\n releaseTouchGuard?.()\n releaseTouchGuard = null\n\n if (!saved)\n return\n document.body.style.overflow = saved.overflow\n document.body.style.paddingRight = saved.paddingRight\n document.body.style.overscrollBehavior = saved.overscrollBehavior\n saved = null\n\n // iOS сдвигает страницу, пока фон заблокирован; возвращаем её на место.\n if (isIOS() && window.scrollY !== savedScrollY)\n window.scrollTo(0, savedScrollY)\n}\n\n/**\n * Возвращает пару `lock`/`unlock`, идемпотентную **в пределах одного инстанса**\n * (повторный `lock()` без `unlock()` не увеличивает глобальный счётчик дважды).\n * Глобальный счётчик разделяется всеми оверлеями.\n */\nexport function useScrollLock() {\n const isClient = typeof window !== 'undefined'\n let held = false\n\n function lock(): void {\n if (!isClient || held)\n return\n held = true\n lockCount += 1\n if (lockCount === 1)\n applyLock()\n }\n\n function unlock(): void {\n if (!isClient || !held)\n return\n held = false\n lockCount = Math.max(0, lockCount - 1)\n if (lockCount === 0)\n releaseLock()\n }\n\n return { lock, unlock }\n}\n\n/** Тестовая/служебная очистка глобального состояния. */\nexport function resetScrollLock(): void {\n lockCount = 0\n if (typeof document !== 'undefined')\n releaseLock()\n saved = null\n}\n","import type { ComputedRef, Ref } from 'vue'\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\n\nimport { useGrThemeAttrs } from '../../components/GrConfigProvider/context'\nimport { useFocusTrap } from '../useFocusTrap'\nimport { useOverlayLayer } from '../useOverlayLayer'\nimport { usePortalTarget } from '../usePortalTarget'\nimport { useInertOthers } from './useInertOthers'\nimport { useOverlayPresence } from './useOverlayPresence'\nimport { useScrollLock } from './useScrollLock'\n\n/**\n * Модальный слой целиком: одна сборка на весь пакет.\n *\n * Модальность собиралась вручную в трёх компонентах (`GrModal`, `GrDrawer`,\n * `GrImageViewer`) из шести примитивов, и сборка повторялась дословно. Дело не\n * в объёме: в этой проводке сидят инварианты, каждый из которых при пропуске\n * даёт тихий баг доступности, а не заметную поломку. Здесь они собраны в одном\n * месте — забыть часть модального слоя больше нельзя.\n *\n * Что именно неочевидно:\n *\n * - **`restoreFocus: false` у ловушки.** Возврат фокуса принадлежит стеку\n * слоёв: у него правило строже — вернуть, только если на момент закрытия\n * фокус ещё был внутри слоя. Две системы возврата подрались бы за фокус.\n * - **`containers: layer.rootsAbove`.** Панель селекта, открытого ВНУТРИ окна,\n * телепортирована в `body` и лежит вне поддерева панели. Без этого списка\n * ловушка считала бы её «снаружи» и отбирала фокус.\n * - **Ловушка и `inert` работают, только пока слой верхний.** Иначе нижнее\n * окно ворует фокус у открытого поверх него, а `inert` гасит собственную\n * панель вместе со страницей.\n * - **Клик по подложке считается только тот, что на ней и начался.** Иначе\n * выделение текста мышью или перетаскивание, отпущенное за пределами панели,\n * закрывает слой.\n *\n * Чего композабл НЕ делает: не рисует разметку. Корневой `div`, подложка и\n * панель у трёх компонентов различаются по существу (обёртки скролла у окна,\n * сквозные клики у немодальной панели, слои хрома у просмотрщика), а классами\n * общий модуль владеть не вправе — пресет гранулярности подмешивает safelist\n * только по директориям выбранных компонентов.\n */\nexport interface UseModalOverlayOptions {\n /**\n * Модальный ли слой: блокирует страницу, держит фокус, гасит нижние окна.\n * Геттером — там, где модальность включается пропом (`GrDrawer`).\n */\n modal?: boolean | (() => boolean)\n /** Закрывать ли по Esc. Геттер — проп компонента реактивен. */\n closeOnEscape?: () => boolean\n /** Закрывать ли по клику в подложку; читается обработчиками `backdrop`. */\n closeOnBackdrop?: () => boolean\n /** Панель слоя: контейнер ловушки фокуса и её запасной приёмник. */\n panel: Ref<HTMLElement | null>\n /**\n * Свой корень слоя. У якорного оверлея (`GrPopover`) корень и панель — один\n * элемент: в портал уезжает сама панель, отдельной обёртки нет. Не задан —\n * композабл заводит корень сам, как у центрированных окон.\n */\n root?: Ref<HTMLElement | null>\n /**\n * Точечное переопределение точки монтирования (проп `teleportTo`). По\n * умолчанию — общий портал оверлеев.\n */\n teleportTo?: () => string | HTMLElement | undefined\n /**\n * Кому отдать фокус при открытии. Не задан — первому фокусируемому внутри\n * панели (правило `useFocusTrap`): просмотрщику нужна именно кнопка тулбара,\n * а окну — сама панель, и оно передаёт её явно.\n */\n initialFocus?: () => HTMLElement | null | undefined\n /**\n * Блокировать ли скролл страницы. По умолчанию — пока слой открыт и модален:\n * немодальная панель существует ровно ради работы со страницей под ней.\n */\n lockScroll?: () => boolean\n}\n\nexport interface ModalOverlay {\n /** Корень слоя: цель `ref` в шаблоне, хозяин `inert` и корень для стека слоёв. */\n rootEl: Ref<HTMLElement | null>\n /** Смонтировано ли поддерево: держится до конца leave-анимации панели. */\n isMounted: Ref<boolean>\n /** Показан ли слой: этим переключаются `<Transition>` внутри. */\n isVisible: Ref<boolean>\n /** Верхний ли это слой сейчас. */\n isTopmost: Ref<boolean>\n /** Значение атрибута `inert` для корня: слой перестал быть верхним. */\n inertAttr: ComputedRef<true | undefined>\n portalTarget: ComputedRef<string | HTMLElement>\n teleportEnabled: ComputedRef<boolean>\n /** `data-theme` на телепортированный корень: в DOM он вне обёртки провайдера. */\n themeAttrs: ComputedRef<{ 'data-theme'?: string }>\n /** Для `@after-leave` панели. Компонент оборачивает, если ему нужен эмит `closed`. */\n onPanelAfterLeave: () => void\n /**\n * Обработчики подложки вместе с guard'ом «клик начался на ней». Имена — под\n * `v-on=\"backdrop\"`: объектный синтаксис ждёт имена событий, а не пропов.\n */\n backdrop: {\n mousedown: (event: MouseEvent) => void\n click: (event: MouseEvent) => void\n }\n}\n\nexport function useModalOverlay(\n open: Ref<boolean>,\n onDismiss: () => void,\n options: UseModalOverlayOptions,\n): ModalOverlay {\n const isModal = (): boolean =>\n typeof options.modal === 'function' ? options.modal() : options.modal ?? true\n\n const rootEl = options.root ?? ref<HTMLElement | null>(null)\n const isTopmost = ref(true)\n\n const inertAttr = computed(() => (open.value && isModal() && !isTopmost.value ? true : undefined))\n\n // Телепорт включается только ПОСЛЕ монтирования: иначе первый клиентский\n // рендер не совпадёт с серверным и сломается гидрация.\n const { target: portalTarget, enabled: teleportEnabled } = usePortalTarget(options.teleportTo)\n\n // Панель уезжает в `body`, то есть вне обёртки провайдера, и `data-theme` с\n // неё не наследуется. В дереве компонентов слой остаётся внутри — `inject`\n // доходит, и тему он ставит себе сам.\n const themeAttrs = useGrThemeAttrs()\n\n const { mounted: isMounted, visible: isVisible, onPanelAfterLeave } = useOverlayPresence(open)\n\n const layer = useOverlayLayer(open, onDismiss, {\n modal: isModal,\n closeOnEscape: options.closeOnEscape,\n onTopmostChange: (value) => { isTopmost.value = value },\n root: rootEl,\n })\n\n useFocusTrap(options.panel, {\n active: () => open.value && isModal() && isTopmost.value,\n initialFocus: options.initialFocus,\n fallbackFocus: () => options.panel.value,\n containers: layer.rootsAbove,\n restoreFocus: false,\n })\n\n useInertOthers(rootEl, () => open.value && isModal() && isTopmost.value)\n\n // Общий reference-counted lock: несколько открытых оверлеев снимают блокировку\n // независимо от порядка закрытия, ширина скроллбара компенсируется.\n const { lock, unlock } = useScrollLock()\n\n watch(\n () => options.lockScroll?.() ?? (open.value && isModal()),\n (locked) => {\n if (locked)\n lock()\n else unlock()\n },\n { immediate: true },\n )\n\n onBeforeUnmount(unlock)\n\n let pointerDownOnBackdrop = false\n\n return {\n rootEl,\n isMounted,\n isVisible,\n isTopmost,\n inertAttr,\n portalTarget,\n teleportEnabled,\n themeAttrs,\n onPanelAfterLeave,\n backdrop: {\n mousedown(event: MouseEvent): void {\n pointerDownOnBackdrop = event.target === event.currentTarget\n },\n click(event: MouseEvent): void {\n const startedHere = pointerDownOnBackdrop\n pointerDownOnBackdrop = false\n\n if (!startedHere || event.target !== event.currentTarget)\n return\n if (options.closeOnBackdrop?.() === false)\n return\n\n onDismiss()\n },\n },\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"GrBadge-D2MfThPz.js","names":[],"sources":["../../../../src/components/GrBadge/GrBadge.vue","../../../../src/components/GrBadge/GrBadge.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, useAttrs } from 'vue'\n\nimport { useGrComponentProp } from '../GrConfigProvider/context'\n\nexport type { GrBadgeRadius, GrBadgeSize, GrBadgeTone } from './grBadgeStyles'\n\nimport { warnRenamedProp } from '../shared/renamedProp'\n\nimport {\n grBadgeClass,\n type GrBadgeRadius,\n type GrBadgeSize,\n type GrBadgeTone,\n} from './grBadgeStyles'\n\nexport interface GrBadgeProps {\n tone?: GrBadgeTone\n dark?: boolean\n size?: GrBadgeSize\n radius?: GrBadgeRadius\n}\n\nconst props = withDefaults(\n defineProps<GrBadgeProps>(),\n {\n // `tone`/`size`/`radius` настраиваются через `GrConfigProvider`, поэтому\n // их дефолты живут в резолверах ниже, а не в `withDefaults`.\n tone: undefined,\n dark: false,\n size: undefined,\n radius: undefined,\n },\n)\n\n// Эффективные значения: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedTone = useGrComponentProp('GrBadge', 'tone', () => props.tone, 'neutral')\nconst resolvedSize = useGrComponentProp('GrBadge', 'size', () => props.size, 'sm')\nconst resolvedRadius = useGrComponentProp('GrBadge', 'radius', () => props.radius, 'round')\n\n// `variant` переименован в `tone`: незнакомый атрибут Vue сажает на корневой\n// узел, поэтому старое имя не роняет ни типы, ни рантайм — компонент молча\n// рисуется дефолтным тоном.\nwarnRenamedProp('GrBadge', useAttrs(), { variant: 'tone' })\n\nconst className = computed(() => {\n return grBadgeClass({\n tone: resolvedTone.value,\n dark: props.dark,\n size: resolvedSize.value,\n radius: resolvedRadius.value,\n })\n})\n\ndefineSlots<{\n /** Содержимое метки. */\n default?: () => any\n}>()\n</script>\n\n<template>\n <span\n class=\"gr-badge inline-block border whitespace-nowrap leading-none\"\n :class=\"className\"\n >\n <span class=\"gr-badge__label\">\n <span class=\"gr-badge__text\">\n <slot />\n </span>\n </span>\n </span>\n</template>\n\n<style scoped>\n/*\n * Оптически центрируем текст по вертикали независимо от регистра,\n * не уменьшая высоту самого бейджа.\n *\n * Высоту держит обёртка `.gr-badge__label` (`min-height: 1lh` — высота\n * line-box при `leading-none`), а текст (`.gr-badge__text`) обрезается до\n * cap-height и центрируется внутри этой высоты через flex.\n *\n * Обрезка обязана жить на вложенном узле, а не на самом тексте: `text-box-trim`\n * уменьшает line-box, и повесь его прямо на текст — вместе с ним просядет\n * высота контента, а за ней и весь бейдж.\n */\n.gr-badge__label {\n display: flex;\n align-items: center;\n min-height: 1lh;\n}\n\n.gr-badge__text {\n display: block;\n text-box-trim: trim-both;\n text-box-edge: cap alphabetic;\n /* Шорткат для браузеров с поддержкой нового синтаксиса. */\n text-box: trim-both cap alphabetic;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, useAttrs } from 'vue'\n\nimport { useGrComponentProp } from '../GrConfigProvider/context'\n\nexport type { GrBadgeRadius, GrBadgeSize, GrBadgeTone } from './grBadgeStyles'\n\nimport { warnRenamedProp } from '../shared/renamedProp'\n\nimport {\n grBadgeClass,\n type GrBadgeRadius,\n type GrBadgeSize,\n type GrBadgeTone,\n} from './grBadgeStyles'\n\nexport interface GrBadgeProps {\n tone?: GrBadgeTone\n dark?: boolean\n size?: GrBadgeSize\n radius?: GrBadgeRadius\n}\n\nconst props = withDefaults(\n defineProps<GrBadgeProps>(),\n {\n // `tone`/`size`/`radius` настраиваются через `GrConfigProvider`, поэтому\n // их дефолты живут в резолверах ниже, а не в `withDefaults`.\n tone: undefined,\n dark: false,\n size: undefined,\n radius: undefined,\n },\n)\n\n// Эффективные значения: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedTone = useGrComponentProp('GrBadge', 'tone', () => props.tone, 'neutral')\nconst resolvedSize = useGrComponentProp('GrBadge', 'size', () => props.size, 'sm')\nconst resolvedRadius = useGrComponentProp('GrBadge', 'radius', () => props.radius, 'round')\n\n// `variant` переименован в `tone`: незнакомый атрибут Vue сажает на корневой\n// узел, поэтому старое имя не роняет ни типы, ни рантайм — компонент молча\n// рисуется дефолтным тоном.\nwarnRenamedProp('GrBadge', useAttrs(), { variant: 'tone' })\n\nconst className = computed(() => {\n return grBadgeClass({\n tone: resolvedTone.value,\n dark: props.dark,\n size: resolvedSize.value,\n radius: resolvedRadius.value,\n })\n})\n\ndefineSlots<{\n /** Содержимое метки. */\n default?: () => any\n}>()\n</script>\n\n<template>\n <span\n class=\"gr-badge inline-block border whitespace-nowrap leading-none\"\n :class=\"className\"\n >\n <span class=\"gr-badge__label\">\n <span class=\"gr-badge__text\">\n <slot />\n </span>\n </span>\n </span>\n</template>\n\n<style scoped>\n/*\n * Оптически центрируем текст по вертикали независимо от регистра,\n * не уменьшая высоту самого бейджа.\n *\n * Высоту держит обёртка `.gr-badge__label` (`min-height: 1lh` — высота\n * line-box при `leading-none`), а текст (`.gr-badge__text`) обрезается до\n * cap-height и центрируется внутри этой высоты через flex.\n *\n * Обрезка обязана жить на вложенном узле, а не на самом тексте: `text-box-trim`\n * уменьшает line-box, и повесь его прямо на текст — вместе с ним просядет\n * высота контента, а за ней и весь бейдж.\n */\n.gr-badge__label {\n display: flex;\n align-items: center;\n min-height: 1lh;\n}\n\n.gr-badge__text {\n display: block;\n text-box-trim: trim-both;\n text-box-edge: cap alphabetic;\n /* Шорткат для браузеров с поддержкой нового синтаксиса. */\n text-box: trim-both cap alphabetic;\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;EAuBA,MAAM,QAAQ;EAad,MAAM,eAAe,mBAAmB,WAAW,cAAc,MAAM,MAAM,SAAS;EACtF,MAAM,eAAe,mBAAmB,WAAW,cAAc,MAAM,MAAM,IAAI;EACjF,MAAM,iBAAiB,mBAAmB,WAAW,gBAAgB,MAAM,QAAQ,OAAO;EAK1F,gBAAgB,WAAW,SAAS,GAAG,EAAE,SAAS,OAAO,CAAC;EAE1D,MAAM,YAAY,eAAe;GAC/B,OAAO,aAAa;IAClB,MAAM,aAAa;IACnB,MAAM,MAAM;IACZ,MAAM,aAAa;IACnB,QAAQ,eAAe;GACzB,CAAC;EACH,CAAC;;uBASC,mBASO,QAAA,EARH,OAAK,eAAA,CAAC,+DACE,UAAA,KAAS,CAAA,EAAA,GAAA,CAEnB,mBAIO,QAJP,YAIO,CAHL,mBAEO,QAFP,YAEO,CADL,WAAQ,KAAA,QAAA,WAAA,CAAA,GAAA,KAAA,GAAA,IAAA,CAAA,CAAA,CAAA,CAAA,CAAA,GAAA,CAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"GrDrawer-C0YqVE9e.js","names":["$slots"],"sources":["../../../../src/components/GrDrawer/GrDrawer.vue","../../../../src/components/GrDrawer/GrDrawer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref, useId, watch } from 'vue'\n\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport { useGrComponentProp } from '../GrConfigProvider/context'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n// Модальный слой целиком: стек (Esc верхнему, `inert` нижним модалкам), ловушка\n// фокуса, скролл-лок, портал. Немодальная панель регистрируется в стеке как\n// обычный dismissible-слой и страницу под собой не трогает.\nimport { useModalOverlay } from '../../composables/internal/useModalOverlay'\nimport {\n DEFAULT_GR_DRAWER_BODY_CONFIG,\n DEFAULT_GR_DRAWER_FOOTER_CONFIG,\n DEFAULT_GR_DRAWER_HEADER_CONFIG,\n footerBorderClass,\n grDrawerAxis,\n grDrawerPanelClass,\n grDrawerPanelEnterFrom,\n headerBorderClass,\n overlayClass,\n resolveGrDrawerSectionConfig,\n rootClass,\n rootPassThroughClass,\n srOnlyTitleClass,\n titleClass,\n type GrDrawerSectionConfig,\n type GrDrawerSide,\n type GrDrawerSize,\n} from './grDrawerStyles'\n\nimport IconClose from '~icons/lucide/x'\n\nimport './defaults'\n\nexport interface GrDrawerProps {\n /** Контроль открытия через v-model. */\n modelValue: boolean\n /** Заголовок; если передан — покажется в хедере. Можно переопределить слотом `#title`. */\n title?: string\n /**\n * Модальная панель: подложка, блокировка скролла, `inert` остальной странице\n * и ловушка фокуса. `false` — панель живёт рядом со страницей: с ней работают,\n * не закрывая, а Tab уходит наружу.\n */\n modal?: boolean\n /** Закрывать при клике по бэкдропу. В немодальном режиме подложки нет. */\n closeOnBackdrop?: boolean\n /** Закрывать по Esc. */\n closeOnEsc?: boolean\n /**\n * Запрет закрытия «мягкими» способами (бэкдроп, Esc) — на время операции,\n * которую нельзя бросить на полпути. Кнопка закрытия при этом остаётся:\n * панель без единого выхода — ловушка.\n */\n persistent?: boolean\n /** Сторона, с которой выезжает панель. */\n side?: GrDrawerSide\n /** Размер панели по её оси: ширина у боковых, высота у верхней и нижней. */\n size?: GrDrawerSize\n /** Произвольная ширина боковой панели. Число трактуется как пиксели; сильнее `size`. */\n width?: string | number\n /** Произвольная высота верхней или нижней панели. Число трактуется как пиксели. */\n height?: string | number\n /** Рендерить ли хедер (заголовок + кнопка закрытия). */\n showHeader?: boolean\n /** Рендерить ли кнопку закрытия в хедере. */\n showCloseButton?: boolean\n /** Паддинги и рамка секций — как у `GrDialog`. */\n headerConfig?: GrDrawerSectionConfig\n bodyConfig?: GrDrawerSectionConfig\n footerConfig?: GrDrawerSectionConfig\n /** Элемент, получающий фокус при открытии. По умолчанию — сама панель. */\n initialFocus?: HTMLElement | null\n /** i18n-friendly aria-label для кнопки закрытия. */\n closeLabel?: string\n}\n\nexport interface GrDrawerEmits {\n (e: 'update:modelValue', value: boolean): void\n /** Панель выехала и анимация закончилась. */\n (e: 'opened'): void\n /** Панель уехала и анимация закончилась. */\n (e: 'closed'): void\n}\n\n// Дефолты оформления намеренно `undefined`: «настоящий» дефолт живёт в\n// `useGrComponentProp`, иначе `GrConfigProvider` не отличит заданный\n// пользователем проп от подставленного Vue.\nconst props = withDefaults(defineProps<GrDrawerProps>(), {\n title: undefined,\n modal: true,\n closeOnBackdrop: true,\n closeOnEsc: true,\n persistent: false,\n side: undefined,\n size: undefined,\n width: undefined,\n height: undefined,\n showHeader: true,\n showCloseButton: true,\n headerConfig: undefined,\n bodyConfig: undefined,\n footerConfig: undefined,\n initialFocus: null,\n closeLabel: undefined,\n})\n\nconst { t } = useGranularityTranslations()\nconst resolvedCloseLabel = computed(() => props.closeLabel ?? t('gr.common.close', 'Close'))\n\nconst emit = defineEmits<GrDrawerEmits>()\n\nconst slots = defineSlots<{\n default?: () => any\n title?: () => any\n /** Своя шапка целиком: заголовок, кнопка закрытия и всё, что нужно рядом. */\n header?: (props: { title?: string, close: () => void }) => any\n footer?: () => any\n}>()\n\nconst titleId = useId()\n\nconst size = useGrComponentProp('GrDrawer', 'size', () => props.size, 'md')\nconst side = useGrComponentProp('GrDrawer', 'side', () => props.side, 'right')\n\nconst axis = computed(() => grDrawerAxis(side.value))\n\n// Пустой/пробельный заголовок считается отсутствующим — иначе хедер занимал бы\n// место ради строки-заглушки.\nconst resolvedTitle = computed(() => props.title?.trim() || undefined)\nconst hasTitle = computed(() => Boolean(resolvedTitle.value) || Boolean(slots.title))\nconst hasCustomHeader = computed(() => Boolean(slots.header))\nconst hasHeader = computed(() =>\n props.showHeader && (hasCustomHeader.value || hasTitle.value || props.showCloseButton),\n)\n\n/**\n * Видимый заголовок рисуется только в стандартной шапке. Со своей шапкой и без\n * шапки вовсе заголовок всё равно нужен — но скрытым: имя слоя обязано остаться\n * осмысленным, а не свалиться на обобщённое «Drawer» из локали.\n */\nconst showVisibleTitle = computed(() =>\n props.showHeader && !hasCustomHeader.value && hasTitle.value,\n)\nconst showSrOnlyTitle = computed(() => hasTitle.value && !showVisibleTitle.value)\n\n// Имя модального слоя обязательно: без него axe роняет `aria-dialog-name`.\n// Есть заголовок — связываем `aria-labelledby`, иначе даём `aria-label` из i18n.\nconst labelledBy = computed(() => (hasTitle.value ? titleId : undefined))\nconst ariaLabel = computed(() => (labelledBy.value ? undefined : t('gr.drawer.title', 'Drawer')))\n\nconst panelEl = ref<HTMLElement | null>(null)\n\n/** Произвольный размер — только по оси панели: у боковой ширина, у нижней высота. */\nconst customLength = computed(() => (axis.value === 'horizontal' ? props.width : props.height))\n\nconst panelClass = computed(() => grDrawerPanelClass({\n side: side.value,\n size: size.value,\n hasCustomLength: customLength.value !== undefined,\n}))\n\nconst panelStyle = computed(() => {\n const length = customLength.value\n if (length === undefined)\n return undefined\n\n const value = typeof length === 'number' ? `${length}px` : length\n return axis.value === 'horizontal' ? { width: value } : { height: value }\n})\n\n// Проп не своей оси молча не работал бы — а выглядит это как баг компонента.\nif (__GR_DEV__) {\n watch(\n [axis, () => props.width, () => props.height],\n ([currentAxis, width, height]) => {\n const ignored = currentAxis === 'horizontal' ? 'height' : 'width'\n const value = currentAxis === 'horizontal' ? height : width\n if (value === undefined)\n return\n\n console.warn(\n `[GrDrawer] Проп \\`${ignored}\\` не применяется к стороне \"${side.value}\": `\n + `панель по этой оси растянута. Задайте \\`${ignored === 'height' ? 'width' : 'height'}\\`.`,\n )\n },\n { immediate: true },\n )\n}\n\nconst panelEnterFrom = computed(() => grDrawerPanelEnterFrom(side.value))\n\nconst headerSection = computed(() => resolveGrDrawerSectionConfig(props.headerConfig, DEFAULT_GR_DRAWER_HEADER_CONFIG))\nconst bodySection = computed(() => resolveGrDrawerSectionConfig(props.bodyConfig, DEFAULT_GR_DRAWER_BODY_CONFIG))\nconst footerSection = computed(() => resolveGrDrawerSectionConfig(props.footerConfig, DEFAULT_GR_DRAWER_FOOTER_CONFIG))\n\nfunction sectionClass(\n section: { paddingX: string, paddingY: string, bordered: boolean },\n borderClass: string,\n): string[] {\n return [section.paddingX, section.paddingY, section.bordered ? borderClass : '']\n}\n\nfunction close(): void {\n emit('update:modelValue', false)\n}\n\n/** Закрытие «мягким» способом: бэкдроп или Esc. `persistent` их и запрещает. */\nfunction closeSoftly(): void {\n if (props.persistent)\n return\n\n close()\n}\n\nconst rootClasses = computed(() => [rootClass, props.modal ? '' : rootPassThroughClass].filter(Boolean))\n\n// Немодальная панель остаётся в очереди Esc, но модалки под собой в `inert` не\n// отправляет и скролл не блокирует: она существует ровно ради работы со\n// страницей под ней, и Tab обязан уводить фокус туда.\nconst {\n rootEl,\n isMounted,\n isVisible,\n inertAttr,\n portalTarget,\n teleportEnabled,\n themeAttrs,\n onPanelAfterLeave: releasePresence,\n backdrop,\n} = useModalOverlay(computed(() => props.modelValue), closeSoftly, {\n panel: panelEl,\n modal: () => props.modal,\n closeOnEscape: () => props.closeOnEsc && !props.persistent,\n closeOnBackdrop: () => props.closeOnBackdrop,\n initialFocus: () => props.initialFocus ?? panelEl.value,\n})\n\nfunction onPanelAfterLeave(): void {\n releasePresence()\n emit('closed')\n}\n\ndefineExpose({\n /** Закрыть панель (эквивалент `v-model = false`), минуя `persistent`. */\n close,\n /** Вернуть фокус на панель — например после операции, уведшей его наружу. */\n focus: () => panelEl.value?.focus(),\n})\n</script>\n\n<template>\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <div\n v-if=\"teleportEnabled && isMounted\"\n ref=\"rootEl\"\n data-gr-drawer\n data-gr-overlay-root\n role=\"dialog\"\n :aria-modal=\"modal ? 'true' : undefined\"\n :class=\"rootClasses\"\n :inert=\"inertAttr\"\n :aria-labelledby=\"labelledBy\"\n :aria-label=\"ariaLabel\"\n >\n <Transition\n appear\n enter-active-class=\"duration-[var(--gr-duration-base)] ease-[var(--gr-ease-out)]\"\n enter-from-class=\"opacity-0\"\n leave-active-class=\"duration-[var(--gr-duration-fast)] ease-[var(--gr-ease-in)]\"\n leave-to-class=\"opacity-0\"\n >\n <div\n v-if=\"isVisible && modal\"\n data-gr-drawer-overlay\n :class=\"overlayClass\"\n aria-hidden=\"true\"\n v-on=\"backdrop\"\n />\n </Transition>\n\n <Transition\n appear\n enter-active-class=\"duration-[var(--gr-duration-base)] ease-[var(--gr-ease-out)]\"\n :enter-from-class=\"panelEnterFrom\"\n leave-active-class=\"duration-[var(--gr-duration-fast)] ease-[var(--gr-ease-in)]\"\n :leave-to-class=\"panelEnterFrom\"\n @after-enter=\"emit('opened')\"\n @after-leave=\"onPanelAfterLeave\"\n >\n <div\n v-if=\"isVisible\"\n ref=\"panelEl\"\n v-bind=\"themeAttrs\"\n data-gr-drawer-panel\n tabindex=\"-1\"\n :class=\"panelClass\"\n :style=\"panelStyle\"\n >\n <!-- Заголовок без видимой шапки: имя слоя обязано остаться своим. -->\n <div\n v-if=\"showSrOnlyTitle\"\n :id=\"titleId\"\n data-gr-drawer-title\n :class=\"srOnlyTitleClass\"\n >\n <slot name=\"title\">\n {{ resolvedTitle }}\n </slot>\n </div>\n\n <div\n v-if=\"hasHeader\"\n data-gr-drawer-header\n :class=\"[hasCustomHeader ? '' : 'flex items-center justify-between gap-4', sectionClass(headerSection, headerBorderClass)]\"\n >\n <slot name=\"header\" :title=\"resolvedTitle\" :close=\"close\">\n <div\n v-if=\"showVisibleTitle\"\n :id=\"titleId\"\n data-gr-drawer-title\n :class=\"titleClass\"\n >\n <slot name=\"title\">\n {{ resolvedTitle }}\n </slot>\n </div>\n <div v-else class=\"min-w-0 flex-1\" />\n\n <GrButton\n v-if=\"showCloseButton\"\n variant=\"ghost\"\n size=\"sm\"\n square\n data-gr-drawer-close\n :aria-label=\"resolvedCloseLabel\"\n @click=\"close\"\n >\n <GrIcon size=\"sm\">\n <IconClose />\n </GrIcon>\n </GrButton>\n </slot>\n </div>\n\n <!-- Тело скроллится, поэтому обязано попадать в таб-порядок: длинный\n текст без единого фокусируемого элемента иначе не прокрутить с\n клавиатуры (axe: `scrollable-region-focusable`). -->\n <div\n data-gr-drawer-body\n tabindex=\"0\"\n class=\"min-h-0 flex-1 overflow-y-auto\"\n >\n <div :class=\"sectionClass(bodySection, '')\">\n <slot />\n </div>\n </div>\n\n <div\n v-if=\"$slots.footer\"\n data-gr-drawer-footer\n :class=\"sectionClass(footerSection, footerBorderClass)\"\n >\n <slot name=\"footer\" />\n </div>\n </div>\n </Transition>\n </div>\n </teleport>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, ref, useId, watch } from 'vue'\n\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport { useGrComponentProp } from '../GrConfigProvider/context'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n// Модальный слой целиком: стек (Esc верхнему, `inert` нижним модалкам), ловушка\n// фокуса, скролл-лок, портал. Немодальная панель регистрируется в стеке как\n// обычный dismissible-слой и страницу под собой не трогает.\nimport { useModalOverlay } from '../../composables/internal/useModalOverlay'\nimport {\n DEFAULT_GR_DRAWER_BODY_CONFIG,\n DEFAULT_GR_DRAWER_FOOTER_CONFIG,\n DEFAULT_GR_DRAWER_HEADER_CONFIG,\n footerBorderClass,\n grDrawerAxis,\n grDrawerPanelClass,\n grDrawerPanelEnterFrom,\n headerBorderClass,\n overlayClass,\n resolveGrDrawerSectionConfig,\n rootClass,\n rootPassThroughClass,\n srOnlyTitleClass,\n titleClass,\n type GrDrawerSectionConfig,\n type GrDrawerSide,\n type GrDrawerSize,\n} from './grDrawerStyles'\n\nimport IconClose from '~icons/lucide/x'\n\nimport './defaults'\n\nexport interface GrDrawerProps {\n /** Контроль открытия через v-model. */\n modelValue: boolean\n /** Заголовок; если передан — покажется в хедере. Можно переопределить слотом `#title`. */\n title?: string\n /**\n * Модальная панель: подложка, блокировка скролла, `inert` остальной странице\n * и ловушка фокуса. `false` — панель живёт рядом со страницей: с ней работают,\n * не закрывая, а Tab уходит наружу.\n */\n modal?: boolean\n /** Закрывать при клике по бэкдропу. В немодальном режиме подложки нет. */\n closeOnBackdrop?: boolean\n /** Закрывать по Esc. */\n closeOnEsc?: boolean\n /**\n * Запрет закрытия «мягкими» способами (бэкдроп, Esc) — на время операции,\n * которую нельзя бросить на полпути. Кнопка закрытия при этом остаётся:\n * панель без единого выхода — ловушка.\n */\n persistent?: boolean\n /** Сторона, с которой выезжает панель. */\n side?: GrDrawerSide\n /** Размер панели по её оси: ширина у боковых, высота у верхней и нижней. */\n size?: GrDrawerSize\n /** Произвольная ширина боковой панели. Число трактуется как пиксели; сильнее `size`. */\n width?: string | number\n /** Произвольная высота верхней или нижней панели. Число трактуется как пиксели. */\n height?: string | number\n /** Рендерить ли хедер (заголовок + кнопка закрытия). */\n showHeader?: boolean\n /** Рендерить ли кнопку закрытия в хедере. */\n showCloseButton?: boolean\n /** Паддинги и рамка секций — как у `GrDialog`. */\n headerConfig?: GrDrawerSectionConfig\n bodyConfig?: GrDrawerSectionConfig\n footerConfig?: GrDrawerSectionConfig\n /** Элемент, получающий фокус при открытии. По умолчанию — сама панель. */\n initialFocus?: HTMLElement | null\n /** i18n-friendly aria-label для кнопки закрытия. */\n closeLabel?: string\n}\n\nexport interface GrDrawerEmits {\n (e: 'update:modelValue', value: boolean): void\n /** Панель выехала и анимация закончилась. */\n (e: 'opened'): void\n /** Панель уехала и анимация закончилась. */\n (e: 'closed'): void\n}\n\n// Дефолты оформления намеренно `undefined`: «настоящий» дефолт живёт в\n// `useGrComponentProp`, иначе `GrConfigProvider` не отличит заданный\n// пользователем проп от подставленного Vue.\nconst props = withDefaults(defineProps<GrDrawerProps>(), {\n title: undefined,\n modal: true,\n closeOnBackdrop: true,\n closeOnEsc: true,\n persistent: false,\n side: undefined,\n size: undefined,\n width: undefined,\n height: undefined,\n showHeader: true,\n showCloseButton: true,\n headerConfig: undefined,\n bodyConfig: undefined,\n footerConfig: undefined,\n initialFocus: null,\n closeLabel: undefined,\n})\n\nconst { t } = useGranularityTranslations()\nconst resolvedCloseLabel = computed(() => props.closeLabel ?? t('gr.common.close', 'Close'))\n\nconst emit = defineEmits<GrDrawerEmits>()\n\nconst slots = defineSlots<{\n default?: () => any\n title?: () => any\n /** Своя шапка целиком: заголовок, кнопка закрытия и всё, что нужно рядом. */\n header?: (props: { title?: string, close: () => void }) => any\n footer?: () => any\n}>()\n\nconst titleId = useId()\n\nconst size = useGrComponentProp('GrDrawer', 'size', () => props.size, 'md')\nconst side = useGrComponentProp('GrDrawer', 'side', () => props.side, 'right')\n\nconst axis = computed(() => grDrawerAxis(side.value))\n\n// Пустой/пробельный заголовок считается отсутствующим — иначе хедер занимал бы\n// место ради строки-заглушки.\nconst resolvedTitle = computed(() => props.title?.trim() || undefined)\nconst hasTitle = computed(() => Boolean(resolvedTitle.value) || Boolean(slots.title))\nconst hasCustomHeader = computed(() => Boolean(slots.header))\nconst hasHeader = computed(() =>\n props.showHeader && (hasCustomHeader.value || hasTitle.value || props.showCloseButton),\n)\n\n/**\n * Видимый заголовок рисуется только в стандартной шапке. Со своей шапкой и без\n * шапки вовсе заголовок всё равно нужен — но скрытым: имя слоя обязано остаться\n * осмысленным, а не свалиться на обобщённое «Drawer» из локали.\n */\nconst showVisibleTitle = computed(() =>\n props.showHeader && !hasCustomHeader.value && hasTitle.value,\n)\nconst showSrOnlyTitle = computed(() => hasTitle.value && !showVisibleTitle.value)\n\n// Имя модального слоя обязательно: без него axe роняет `aria-dialog-name`.\n// Есть заголовок — связываем `aria-labelledby`, иначе даём `aria-label` из i18n.\nconst labelledBy = computed(() => (hasTitle.value ? titleId : undefined))\nconst ariaLabel = computed(() => (labelledBy.value ? undefined : t('gr.drawer.title', 'Drawer')))\n\nconst panelEl = ref<HTMLElement | null>(null)\n\n/** Произвольный размер — только по оси панели: у боковой ширина, у нижней высота. */\nconst customLength = computed(() => (axis.value === 'horizontal' ? props.width : props.height))\n\nconst panelClass = computed(() => grDrawerPanelClass({\n side: side.value,\n size: size.value,\n hasCustomLength: customLength.value !== undefined,\n}))\n\nconst panelStyle = computed(() => {\n const length = customLength.value\n if (length === undefined)\n return undefined\n\n const value = typeof length === 'number' ? `${length}px` : length\n return axis.value === 'horizontal' ? { width: value } : { height: value }\n})\n\n// Проп не своей оси молча не работал бы — а выглядит это как баг компонента.\nif (__GR_DEV__) {\n watch(\n [axis, () => props.width, () => props.height],\n ([currentAxis, width, height]) => {\n const ignored = currentAxis === 'horizontal' ? 'height' : 'width'\n const value = currentAxis === 'horizontal' ? height : width\n if (value === undefined)\n return\n\n console.warn(\n `[GrDrawer] Проп \\`${ignored}\\` не применяется к стороне \"${side.value}\": `\n + `панель по этой оси растянута. Задайте \\`${ignored === 'height' ? 'width' : 'height'}\\`.`,\n )\n },\n { immediate: true },\n )\n}\n\nconst panelEnterFrom = computed(() => grDrawerPanelEnterFrom(side.value))\n\nconst headerSection = computed(() => resolveGrDrawerSectionConfig(props.headerConfig, DEFAULT_GR_DRAWER_HEADER_CONFIG))\nconst bodySection = computed(() => resolveGrDrawerSectionConfig(props.bodyConfig, DEFAULT_GR_DRAWER_BODY_CONFIG))\nconst footerSection = computed(() => resolveGrDrawerSectionConfig(props.footerConfig, DEFAULT_GR_DRAWER_FOOTER_CONFIG))\n\nfunction sectionClass(\n section: { paddingX: string, paddingY: string, bordered: boolean },\n borderClass: string,\n): string[] {\n return [section.paddingX, section.paddingY, section.bordered ? borderClass : '']\n}\n\nfunction close(): void {\n emit('update:modelValue', false)\n}\n\n/** Закрытие «мягким» способом: бэкдроп или Esc. `persistent` их и запрещает. */\nfunction closeSoftly(): void {\n if (props.persistent)\n return\n\n close()\n}\n\nconst rootClasses = computed(() => [rootClass, props.modal ? '' : rootPassThroughClass].filter(Boolean))\n\n// Немодальная панель остаётся в очереди Esc, но модалки под собой в `inert` не\n// отправляет и скролл не блокирует: она существует ровно ради работы со\n// страницей под ней, и Tab обязан уводить фокус туда.\nconst {\n rootEl,\n isMounted,\n isVisible,\n inertAttr,\n portalTarget,\n teleportEnabled,\n themeAttrs,\n onPanelAfterLeave: releasePresence,\n backdrop,\n} = useModalOverlay(computed(() => props.modelValue), closeSoftly, {\n panel: panelEl,\n modal: () => props.modal,\n closeOnEscape: () => props.closeOnEsc && !props.persistent,\n closeOnBackdrop: () => props.closeOnBackdrop,\n initialFocus: () => props.initialFocus ?? panelEl.value,\n})\n\nfunction onPanelAfterLeave(): void {\n releasePresence()\n emit('closed')\n}\n\ndefineExpose({\n /** Закрыть панель (эквивалент `v-model = false`), минуя `persistent`. */\n close,\n /** Вернуть фокус на панель — например после операции, уведшей его наружу. */\n focus: () => panelEl.value?.focus(),\n})\n</script>\n\n<template>\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <div\n v-if=\"teleportEnabled && isMounted\"\n ref=\"rootEl\"\n data-gr-drawer\n data-gr-overlay-root\n role=\"dialog\"\n :aria-modal=\"modal ? 'true' : undefined\"\n :class=\"rootClasses\"\n :inert=\"inertAttr\"\n :aria-labelledby=\"labelledBy\"\n :aria-label=\"ariaLabel\"\n >\n <Transition\n appear\n enter-active-class=\"duration-[var(--gr-duration-base)] ease-[var(--gr-ease-out)]\"\n enter-from-class=\"opacity-0\"\n leave-active-class=\"duration-[var(--gr-duration-fast)] ease-[var(--gr-ease-in)]\"\n leave-to-class=\"opacity-0\"\n >\n <div\n v-if=\"isVisible && modal\"\n data-gr-drawer-overlay\n :class=\"overlayClass\"\n aria-hidden=\"true\"\n v-on=\"backdrop\"\n />\n </Transition>\n\n <Transition\n appear\n enter-active-class=\"duration-[var(--gr-duration-base)] ease-[var(--gr-ease-out)]\"\n :enter-from-class=\"panelEnterFrom\"\n leave-active-class=\"duration-[var(--gr-duration-fast)] ease-[var(--gr-ease-in)]\"\n :leave-to-class=\"panelEnterFrom\"\n @after-enter=\"emit('opened')\"\n @after-leave=\"onPanelAfterLeave\"\n >\n <div\n v-if=\"isVisible\"\n ref=\"panelEl\"\n v-bind=\"themeAttrs\"\n data-gr-drawer-panel\n tabindex=\"-1\"\n :class=\"panelClass\"\n :style=\"panelStyle\"\n >\n <!-- Заголовок без видимой шапки: имя слоя обязано остаться своим. -->\n <div\n v-if=\"showSrOnlyTitle\"\n :id=\"titleId\"\n data-gr-drawer-title\n :class=\"srOnlyTitleClass\"\n >\n <slot name=\"title\">\n {{ resolvedTitle }}\n </slot>\n </div>\n\n <div\n v-if=\"hasHeader\"\n data-gr-drawer-header\n :class=\"[hasCustomHeader ? '' : 'flex items-center justify-between gap-4', sectionClass(headerSection, headerBorderClass)]\"\n >\n <slot name=\"header\" :title=\"resolvedTitle\" :close=\"close\">\n <div\n v-if=\"showVisibleTitle\"\n :id=\"titleId\"\n data-gr-drawer-title\n :class=\"titleClass\"\n >\n <slot name=\"title\">\n {{ resolvedTitle }}\n </slot>\n </div>\n <div v-else class=\"min-w-0 flex-1\" />\n\n <GrButton\n v-if=\"showCloseButton\"\n variant=\"ghost\"\n size=\"sm\"\n square\n data-gr-drawer-close\n :aria-label=\"resolvedCloseLabel\"\n @click=\"close\"\n >\n <GrIcon size=\"sm\">\n <IconClose />\n </GrIcon>\n </GrButton>\n </slot>\n </div>\n\n <!-- Тело скроллится, поэтому обязано попадать в таб-порядок: длинный\n текст без единого фокусируемого элемента иначе не прокрутить с\n клавиатуры (axe: `scrollable-region-focusable`). -->\n <div\n data-gr-drawer-body\n tabindex=\"0\"\n class=\"min-h-0 flex-1 overflow-y-auto\"\n >\n <div :class=\"sectionClass(bodySection, '')\">\n <slot />\n </div>\n </div>\n\n <div\n v-if=\"$slots.footer\"\n data-gr-drawer-footer\n :class=\"sectionClass(footerSection, footerBorderClass)\"\n >\n <slot name=\"footer\" />\n </div>\n </div>\n </Transition>\n </div>\n </teleport>\n</template>\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"GrImageViewer-CyVN6Zpc.js","names":["$slots"],"sources":["../../../../src/components/GrImageViewer/composables/useZoomPan.ts","../../../../src/components/GrImageViewer/composables/useWheelGesture.ts","../../../../src/components/GrImageViewer/composables/usePointerGestures.ts","../../../../src/components/GrImageViewer/composables/useViewerKeyboard.ts","../../../../src/components/GrImageViewer/GrImageViewer.vue","../../../../src/components/GrImageViewer/GrImageViewer.vue"],"sourcesContent":["import { computed, ref, type Ref } from 'vue'\n\nimport { useDragGesture } from '../../../composables/useDragGesture'\n\nconst TRAILING_ZERO_DECIMAL_RE = /\\.0$/\n\nexport function formatPercent(value: number): string {\n if (Number.isInteger(value))\n return String(value)\n return value.toFixed(1).replace(TRAILING_ZERO_DECIMAL_RE, '')\n}\n\n/** Точка в координатах вьюпорта — курсор или середина между пальцами. */\nexport interface ZoomAnchor {\n clientX: number\n clientY: number\n}\n\nexport interface UseZoomPanOptions {\n minScale: () => number\n maxScale: () => number\n zoomRate: () => number\n /** Тянуть можно и на неувеличенном кадре. Увеличенный тянется всегда. */\n draggable: () => boolean\n /** Ссылка на `<img>` — для измерения layout-размера и подписки ResizeObserver. */\n imageEl: Ref<HTMLImageElement | null>\n /** Область кадра: задаёт систему координат для якорного зума и границы пана. */\n viewportEl: Ref<HTMLElement | null>\n /** Колбэк на поворот (эмит `rotate`). */\n onRotate: (deg: number) => void\n}\n\n/**\n * useZoomPan — состояние и логика масштаба/поворота/панорамирования картинки,\n * а также метрики (натуральный/вписанный/фактический размер, реальный масштаб).\n */\nexport function useZoomPan(options: UseZoomPanOptions) {\n const scale = ref(1)\n const rotation = ref(0)\n const offsetX = ref(0)\n const offsetY = ref(0)\n\n const isDragging = ref(false)\n let dragStartX = 0\n let dragStartY = 0\n let dragOffsetStartX = 0\n let dragOffsetStartY = 0\n\n // Натуральный размер (из `<img>`) и layout-размер вписанного (`object-contain`) изображения.\n const naturalWidth = ref(0)\n const naturalHeight = ref(0)\n const fittedWidth = ref(0)\n const fittedHeight = ref(0)\n\n let resizeObserver: ResizeObserver | null = null\n\n const imageStyle = computed(() => ({\n transform: `translate3d(${offsetX.value}px, ${offsetY.value}px, 0) scale(${scale.value}) rotate(${rotation.value}deg)`,\n }))\n\n const zoomValueText = computed(() => formatPercent(scale.value * 100))\n\n // Фактический размер изображения на экране вдоль его осей (footprint), c учётом scale.\n const renderedWidth = computed(() => Math.round(fittedWidth.value * scale.value))\n const renderedHeight = computed(() => Math.round(fittedHeight.value * scale.value))\n\n // Реальный масштаб относительно натурального размера (доля). Не зависит от rotation.\n const realScale = computed(() => {\n if (!naturalWidth.value)\n return 0\n return (fittedWidth.value * scale.value) / naturalWidth.value\n })\n const realScalePercent = computed(() => formatPercent(realScale.value * 100))\n\n /** Увеличенный кадр тянется всегда: иначе до его краёв не добраться вовсе. */\n const isPannable = computed(() => options.draggable() || scale.value > 1)\n\n /**\n * Граница смещения по осям экрана.\n *\n * Тянуть имеет смысл ровно настолько, насколько кадр вылез за область\n * просмотра, — дальше пользователь утаскивает картинку в пустоту и достаёт её\n * только сбросом. Поворот на 90°/270° меняет оси местами, поэтому границы\n * считаются по bounding box повёрнутого footprint'а.\n */\n function panBounds(): { x: number, y: number } {\n const viewport = options.viewportEl.value\n if (!viewport)\n return { x: 0, y: 0 }\n\n const quarterTurns = Math.abs(Math.round(rotation.value / 90) % 2)\n const width = quarterTurns === 1 ? renderedHeight.value : renderedWidth.value\n const height = quarterTurns === 1 ? renderedWidth.value : renderedHeight.value\n\n return {\n x: Math.max(0, (width - viewport.clientWidth) / 2),\n y: Math.max(0, (height - viewport.clientHeight) / 2),\n }\n }\n\n function clampOffsets(): void {\n const bounds = panBounds()\n offsetX.value = Math.min(bounds.x, Math.max(-bounds.x, offsetX.value))\n offsetY.value = Math.min(bounds.y, Math.max(-bounds.y, offsetY.value))\n }\n\n function clampScale(value: number): number {\n const clamped = Math.min(options.maxScale(), Math.max(options.minScale(), value))\n return Number(clamped.toFixed(4))\n }\n\n function setScale(value: number): void {\n scale.value = clampScale(value)\n clampOffsets()\n }\n\n /**\n * Масштаб с якорем: точка под курсором (или между пальцами) остаётся на\n * месте. Без этого зум всегда идёт от центра, и увеличить нужный угол кадра\n * можно только зумом с последующим таскиванием.\n */\n function setScaleAt(value: number, anchor?: ZoomAnchor): void {\n const viewport = options.viewportEl.value\n const previous = scale.value\n const next = clampScale(value)\n\n if (next === previous)\n return\n\n if (anchor && viewport) {\n const rect = viewport.getBoundingClientRect()\n // Координаты якоря относительно центра области — там же начало отсчёта\n // трансформации картинки.\n const anchorX = anchor.clientX - (rect.left + rect.width / 2)\n const anchorY = anchor.clientY - (rect.top + rect.height / 2)\n const ratio = next / previous\n\n offsetX.value = anchorX - (anchorX - offsetX.value) * ratio\n offsetY.value = anchorY - (anchorY - offsetY.value) * ratio\n }\n\n scale.value = next\n clampOffsets()\n }\n\n function zoomIn(anchor?: ZoomAnchor): void {\n setScaleAt(scale.value * options.zoomRate(), anchor)\n }\n\n function zoomOut(anchor?: ZoomAnchor): void {\n setScaleAt(scale.value / options.zoomRate(), anchor)\n }\n\n function rotateLeft(): void {\n rotation.value -= 90\n clampOffsets()\n options.onRotate(rotation.value)\n }\n\n function rotateRight(): void {\n rotation.value += 90\n clampOffsets()\n options.onRotate(rotation.value)\n }\n\n /**\n * Масштаб «один к одному»: пиксель картинки на пиксель экрана.\n *\n * Считается от вписанного размера, потому что `scale` номинальный — единица\n * означает «вписано в окно», а не «натуральный размер». У крупного кадра\n * реальные 100% приходятся на номинальные 6–8×, и вручную такую кнопку\n * потребителю пришлось бы собирать из `naturalWidth`/`renderedWidth`.\n *\n * Потолок `maxScale` действует и здесь: он ограничивает зум сознательно, и\n * «один к одному» — не повод его обойти. Если кадру нужно больше, поднимите\n * `maxScale`.\n */\n function zoomToNatural(): void {\n if (!naturalWidth.value || !fittedWidth.value)\n return\n\n setScale(naturalWidth.value / fittedWidth.value)\n }\n\n function resetTransform(): void {\n scale.value = 1\n rotation.value = 0\n offsetX.value = 0\n offsetY.value = 0\n isDragging.value = false\n }\n\n function resetImageMetrics(): void {\n naturalWidth.value = 0\n naturalHeight.value = 0\n fittedWidth.value = 0\n fittedHeight.value = 0\n }\n\n function measureFitted(): void {\n const el = options.imageEl.value\n if (!el)\n return\n // offsetWidth/Height — layout-размер вписанного изображения, без CSS transform.\n fittedWidth.value = el.offsetWidth\n fittedHeight.value = el.offsetHeight\n clampOffsets()\n }\n\n function onImageLoad(event: Event): void {\n const el = event.target as HTMLImageElement\n naturalWidth.value = el.naturalWidth\n naturalHeight.value = el.naturalHeight\n measureFitted()\n }\n\n function startObservingImage(): void {\n const el = options.imageEl.value\n if (!el || typeof ResizeObserver === 'undefined')\n return\n\n if (!resizeObserver)\n resizeObserver = new ResizeObserver(() => measureFitted())\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\n function stopObservingImage(): void {\n resizeObserver?.disconnect()\n }\n\n /** Начало перетаскивания: используется и мышью, и жестами. */\n function startPan(x: number, y: number): void {\n isDragging.value = true\n dragStartX = x\n dragStartY = y\n dragOffsetStartX = offsetX.value\n dragOffsetStartY = offsetY.value\n }\n\n function movePan(x: number, y: number): void {\n if (!isDragging.value)\n return\n offsetX.value = dragOffsetStartX + (x - dragStartX)\n offsetY.value = dragOffsetStartY + (y - dragStartY)\n clampOffsets()\n }\n\n function endPan(): void {\n isDragging.value = false\n }\n\n /**\n * Перетаскивание мышью и пером — на общем примитиве жеста.\n *\n * Слушатели живут на `window`, а не на захвате указателя: захват привязан к\n * `<img>`, и смена кадра посреди жеста подменяла элемент — отпускание не\n * приходило никуда, а `isDragging` оставался поднятым.\n */\n const drag = useDragGesture({\n disabled: () => !isPannable.value,\n onStart: (event) => {\n // Тач ведёт `usePointerGestures`: у него свои pinch и свайп, и второй\n // владелец жеста тянул бы кадр дважды.\n if (event.pointerType === 'touch')\n return false\n\n event.preventDefault()\n startPan(event.clientX, event.clientY)\n },\n onMove: event => movePan(event.clientX, event.clientY),\n onEnd: endPan,\n // Обрыв не откатывает панораму: кадр уже сдвинут пользователем, и возврат\n // к исходной точке читался бы как рывок, а не как отмена.\n onCancel: endPan,\n })\n\n /** Принудительно закончить перетаскивание — закрытие и смена кадра. */\n function cancelPan(): void {\n drag.stop()\n endPan()\n }\n\n return {\n scale,\n rotation,\n offsetX,\n offsetY,\n isDragging,\n isPannable,\n naturalWidth,\n naturalHeight,\n fittedWidth,\n fittedHeight,\n imageStyle,\n zoomValueText,\n renderedWidth,\n renderedHeight,\n realScale,\n realScalePercent,\n setScale,\n setScaleAt,\n zoomIn,\n zoomOut,\n zoomToNatural,\n rotateLeft,\n rotateRight,\n resetTransform,\n resetImageMetrics,\n measureFitted,\n clampOffsets,\n onImageLoad,\n startObservingImage,\n stopObservingImage,\n startPan,\n movePan,\n endPan,\n cancelPan,\n onPointerDown: drag.start,\n }\n}\n","import { ref } from 'vue'\n\nimport type { ZoomAnchor } from './useZoomPan'\n\n// Чувствительность масштабирования колесом/трекпадом: чем больше — тем резче реакция.\nconst WHEEL_ZOOM_SENSITIVITY = 0.0015\n// Сколько ms простоя после последнего wheel-события считаем «зум завершён»\n// (после этого возвращаем плавный CSS-переход).\nconst WHEEL_IDLE_MS = 120\n\nexport interface UseWheelGestureOptions {\n /** Разрешён ли зум колесом (проп `wheelZoom` + наличие изображений). */\n enabled: () => boolean\n /** Применить множитель масштаба (накопленный за кадр) с якорем в точке курсора. */\n applyZoomFactor: (factor: number, anchor: ZoomAnchor) => void\n}\n\n/**\n * useWheelGesture — масштабирование колесом мыши / жестом трекпада с батчингом\n * по `requestAnimationFrame` (рендер не чаще кадра — без «дёрганья»/наслоения) и\n * временным отключением CSS-перехода на время жеста (`isWheelZooming`).\n */\nexport function useWheelGesture(options: UseWheelGestureOptions) {\n const isWheelZooming = ref(false)\n\n let pendingWheelDelta = 0\n let wheelFrameId: number | null = null\n let wheelIdleTimer: ReturnType<typeof setTimeout> | null = null\n // Якорь берётся из последнего события кадра: зум обязан идти в точку под\n // курсором, а не от центра — иначе до угла увеличенного кадра не добраться.\n let anchor: ZoomAnchor = { clientX: 0, clientY: 0 }\n\n function cancelWheelFrame(): void {\n if (wheelFrameId !== null) {\n cancelAnimationFrame(wheelFrameId)\n wheelFrameId = null\n }\n }\n\n function 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 function flushWheelZoom(): void {\n wheelFrameId = null\n\n const delta = pendingWheelDelta\n pendingWheelDelta = 0\n\n if (!delta)\n return\n\n // Экспоненциальный шаг — плавно и одинаково для колеса и трекпада.\n // delta < 0 (scroll up) — приближение, delta > 0 — отдаление.\n const factor = Math.exp(-delta * WHEEL_ZOOM_SENSITIVITY)\n options.applyZoomFactor(factor, anchor)\n }\n\n function onWheel(event: WheelEvent): void {\n if (!options.enabled())\n return\n\n // Не даём странице/оверлею проскроллиться — колесо отдаём под зум.\n event.preventDefault()\n\n // Во время непрерывного зума отключаем CSS-переход: плавность даёт само\n // покадровое обновление, а transition на микрошагах вызывает лаг/наслоение.\n isWheelZooming.value = true\n\n pendingWheelDelta += event.deltaY\n anchor = { clientX: event.clientX, clientY: event.clientY }\n\n if (wheelFrameId === null)\n wheelFrameId = requestAnimationFrame(flushWheelZoom)\n\n if (wheelIdleTimer !== null)\n clearTimeout(wheelIdleTimer)\n\n // По завершении жеста возвращаем плавный переход для дискретных зумов.\n wheelIdleTimer = setTimeout(() => {\n wheelIdleTimer = null\n isWheelZooming.value = false\n }, WHEEL_IDLE_MS)\n }\n\n return { isWheelZooming, onWheel, endWheelZoom }\n}\n","import { ref } from 'vue'\n\nimport type { ZoomAnchor } from './useZoomPan'\n\n/** Сколько пикселей должен пройти палец, чтобы это считалось свайпом. */\nconst SWIPE_THRESHOLD_PX = 60\n/** Во сколько раз горизонталь должна превысить вертикаль: иначе это не листание. */\nconst SWIPE_AXIS_RATIO = 1.5\n\nexport interface UsePointerGesturesOptions {\n /** Включены ли жесты вовсе (есть кадры). */\n enabled: () => boolean\n /** Текущий масштаб: на неувеличенном кадре одиночный жест листает, а не тянет. */\n scale: () => number\n /** Есть ли куда листать (кадров больше одного). */\n canSwipe: () => boolean\n /** Масштаб с якорем — тот же, что у колеса. */\n setScaleAt: (value: number, anchor: ZoomAnchor) => void\n pan: {\n start: (x: number, y: number) => void\n move: (x: number, y: number) => void\n end: () => void\n }\n onSwipeLeft: () => void\n onSwipeRight: () => void\n}\n\ninterface TrackedPointer {\n x: number\n y: number\n}\n\n/**\n * usePointerGestures — сенсорные жесты просмотрщика: pinch-zoom двумя пальцами\n * и горизонтальный свайп для листания.\n *\n * Живёт на тех же pointer-событиях, что и перетаскивание мышью: разделение идёт\n * по числу активных указателей, а не по типу устройства — трекпад и стилус\n * ведут себя как мышь, палец как палец, отдельных веток на `touch*` не нужно.\n *\n * Что делает одиночный жест, решает масштаб: на увеличенном кадре тянуть важнее\n * (иначе до краёв не добраться), на вписанном — листать.\n */\nexport function usePointerGestures(options: UsePointerGesturesOptions) {\n const pointers = new Map<number, TrackedPointer>()\n\n /** Идёт ли сейчас pinch: на время жеста компонент гасит CSS-переход. */\n const isPinching = ref(false)\n\n let pinchStartDistance = 0\n let pinchStartScale = 1\n\n let swipeStartX = 0\n let swipeStartY = 0\n let isSwipeCandidate = false\n\n function distance(a: TrackedPointer, b: TrackedPointer): number {\n return Math.hypot(a.x - b.x, a.y - b.y)\n }\n\n function midpoint(a: TrackedPointer, b: TrackedPointer): ZoomAnchor {\n return { clientX: (a.x + b.x) / 2, clientY: (a.y + b.y) / 2 }\n }\n\n function activePair(): [TrackedPointer, TrackedPointer] | null {\n if (pointers.size < 2)\n return null\n const [first, second] = [...pointers.values()]\n return [first, second]\n }\n\n function onPointerDown(event: PointerEvent): void {\n if (!options.enabled() || event.pointerType === 'mouse')\n return\n\n pointers.set(event.pointerId, { x: event.clientX, y: event.clientY })\n\n const pair = activePair()\n if (pair) {\n // Второй палец превращает начатое перетаскивание в pinch.\n options.pan.end()\n isSwipeCandidate = false\n isPinching.value = true\n pinchStartDistance = distance(pair[0], pair[1]) || 1\n pinchStartScale = options.scale()\n return\n }\n\n if (options.scale() > 1) {\n options.pan.start(event.clientX, event.clientY)\n return\n }\n\n isSwipeCandidate = options.canSwipe()\n swipeStartX = event.clientX\n swipeStartY = event.clientY\n }\n\n function onPointerMove(event: PointerEvent): void {\n if (!pointers.has(event.pointerId))\n return\n\n pointers.set(event.pointerId, { x: event.clientX, y: event.clientY })\n\n const pair = activePair()\n if (pair && isPinching.value) {\n const current = distance(pair[0], pair[1])\n if (!current)\n return\n\n options.setScaleAt(pinchStartScale * (current / pinchStartDistance), midpoint(pair[0], pair[1]))\n return\n }\n\n if (!isSwipeCandidate)\n options.pan.move(event.clientX, event.clientY)\n }\n\n function finishSwipe(event: PointerEvent): void {\n const deltaX = event.clientX - swipeStartX\n const deltaY = event.clientY - swipeStartY\n\n isSwipeCandidate = false\n\n // Вертикальное движение — это скролл или случайное дрожание, листать по\n // нему нельзя: пользователь целился не туда.\n if (Math.abs(deltaX) < SWIPE_THRESHOLD_PX)\n return\n if (Math.abs(deltaX) < Math.abs(deltaY) * SWIPE_AXIS_RATIO)\n return\n\n if (deltaX < 0)\n options.onSwipeLeft()\n else options.onSwipeRight()\n }\n\n function onPointerUp(event: PointerEvent): void {\n if (!pointers.has(event.pointerId))\n return\n\n const wasSwipeCandidate = isSwipeCandidate\n pointers.delete(event.pointerId)\n\n if (isPinching.value && pointers.size < 2) {\n isPinching.value = false\n // Оставшийся палец продолжает жест уже как перетаскивание.\n const [remaining] = [...pointers.values()]\n if (remaining && options.scale() > 1)\n options.pan.start(remaining.x, remaining.y)\n return\n }\n\n if (wasSwipeCandidate) {\n finishSwipe(event)\n return\n }\n\n options.pan.end()\n }\n\n /**\n * Обрыв — не отпускание: браузер забрал указатель (системный жест, звонок,\n * потеря окна). Пройденное расстояние при этом не считается свайпом, иначе\n * прерванный жест листает кадр, которого пользователь не листал.\n */\n function onPointerCancel(event: PointerEvent): void {\n if (!pointers.has(event.pointerId))\n return\n\n pointers.delete(event.pointerId)\n isSwipeCandidate = false\n isPinching.value = false\n options.pan.end()\n }\n\n /** Сброс при закрытии, смене кадра и размонтировании. */\n function reset(): void {\n pointers.clear()\n isPinching.value = false\n isSwipeCandidate = false\n }\n\n return { isPinching, onPointerDown, onPointerMove, onPointerUp, onPointerCancel, reset }\n}\n","export interface ViewerKeyboardActions {\n close: () => void\n prev: () => void\n next: () => void\n zoomIn: () => void\n zoomOut: () => void\n reset: () => void\n}\n\nexport interface UseViewerKeyboardOptions {\n actions: ViewerKeyboardActions\n}\n\n/**\n * useViewerKeyboard — клавиатурное управление просмотрщиком: стрелки —\n * переключение, `+`/`-`/`0` — зум/сброс.\n *\n * Esc сюда не входит: он идёт через общий стек dismissible-слоёв\n * (`useDismissible`), иначе просмотрщик, открытый поверх модалки, закрывал бы\n * не себя. Локальный `@keydown` до него всё равно не дошёл бы — стек гасит\n * событие в capture-фазе.\n */\nexport function useViewerKeyboard(options: UseViewerKeyboardOptions) {\n const { actions } = options\n\n function onKeydown(event: KeyboardEvent): void {\n switch (event.key) {\n case 'ArrowLeft':\n event.preventDefault()\n actions.prev()\n return\n\n case 'ArrowRight':\n event.preventDefault()\n actions.next()\n return\n\n case '+':\n case '=':\n case 'Add':\n event.preventDefault()\n actions.zoomIn()\n return\n\n case '-':\n case '_':\n case 'Subtract':\n event.preventDefault()\n actions.zoomOut()\n return\n\n case '0':\n event.preventDefault()\n actions.reset()\n break\n\n default:\n }\n }\n\n return { onKeydown }\n}\n","<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'\n\nimport { useAnnouncer } from '../../composables/useAnnouncer'\nimport { useModalOverlay } from '../../composables/internal/useModalOverlay'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport IconChevronLeft from '~icons/lucide/chevron-left'\nimport IconChevronRight from '~icons/lucide/chevron-right'\nimport IconDownload from '~icons/lucide/download'\nimport IconMinus from '~icons/lucide/minus'\nimport IconPlus from '~icons/lucide/plus'\nimport IconRotateCcw from '~icons/lucide/rotate-ccw'\nimport IconRotateCw from '~icons/lucide/rotate-cw'\nimport IconX from '~icons/lucide/x'\n\nimport GrIcon from '../GrIcon/GrIcon.vue'\n\nimport {\n badgeClass,\n chromeButtonClass,\n emptyStateClass,\n scrimClass,\n toolbarButtonClass,\n toolbarSeparatorClass,\n toolbarShellClass,\n} from './grImageViewerStyles'\nimport { useZoomPan } from './composables/useZoomPan'\nimport { useWheelGesture } from './composables/useWheelGesture'\nimport { usePointerGestures } from './composables/usePointerGestures'\nimport { useViewerKeyboard } from './composables/useViewerKeyboard'\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 *\n * Логика вынесена в composables: `useZoomPan` (масштаб/поворот/пан + метрики),\n * `useWheelGesture` (зум колесом/трекпадом с rAF-батчингом), `useViewerKeyboard`\n * (клавиатура). Сам SFC отвечает за оверлей, индекс изображений и композицию.\n */\n/** Кадр просмотрщика: только адрес или адрес с альтернативным текстом. */\nexport type GrImageViewerItem = { src: string, alt?: string }\nexport type GrImageViewerSource = string | GrImageViewerItem\n\nexport interface GrImageViewerProps {\n modelValue: boolean\n /**\n * Кадры. Строка — только адрес; объект `{ src, alt }` даёт изображению\n * альтернативный текст: без него просмотрщик пуст для незрячего\n * пользователя, а придумать текст за потребителя компонент не может.\n */\n urlList: GrImageViewerSource[]\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 /**\n * Имя CSS-переменной слоя — escape-hatch мимо `--gr-z-modal`. Сырое число\n * компонент не принимает: слой задаётся шкалой, см. `docs/z-index.md`.\n */\n zIndexVar?: string\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 zoomToNaturalLabel?: string\n /** i18n: aria-label кнопки «повернуть влево». */\n rotateLeftLabel?: string\n /** i18n: aria-label кнопки «повернуть вправо». */\n rotateRightLabel?: string\n /** Показывать кнопку скачивания текущего кадра. */\n showDownload?: boolean\n /** i18n: aria-label кнопки «скачать». */\n downloadLabel?: string\n /** i18n: текст в пустом состоянии (нет изображений). */\n emptyText?: string\n /**\n * Доступное имя слоя. Модальный диалог без имени — нарушение\n * `aria-dialog-name`: диктор объявит «диалог» и замолчит.\n */\n ariaLabel?: string\n}\n\nexport interface GrImageViewerEmits {\n (e: 'update:modelValue', value: boolean): void\n (e: 'close'): void\n /** Показан другой кадр. */\n (e: 'change', newIndex: number): void\n (e: 'rotate', deg: number): void\n /** Нажата кнопка скачивания. Само скачивание компонент уже запустил. */\n (e: 'download', payload: { src: string, alt: string, index: number }): void\n}\n\nconst props = withDefaults(\n defineProps<GrImageViewerProps>(),\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 zIndexVar: undefined,\n closeLabel: undefined,\n prevLabel: undefined,\n nextLabel: undefined,\n zoomInLabel: undefined,\n zoomOutLabel: undefined,\n resetZoomLabel: undefined,\n zoomToNaturalLabel: undefined,\n rotateLeftLabel: undefined,\n rotateRightLabel: undefined,\n showDownload: false,\n downloadLabel: undefined,\n emptyText: undefined,\n ariaLabel: undefined,\n },\n)\n\nconst { t } = useGranularityTranslations()\nconst resolvedCloseLabel = computed(() => props.closeLabel ?? t('gr.imageViewer.close', 'Close image viewer'))\nconst resolvedPrevLabel = computed(() => props.prevLabel ?? t('gr.imageViewer.prev', 'Previous image'))\nconst resolvedNextLabel = computed(() => props.nextLabel ?? t('gr.imageViewer.next', 'Next image'))\nconst resolvedZoomInLabel = computed(() => props.zoomInLabel ?? t('gr.imageViewer.zoomIn', 'Zoom in'))\nconst resolvedZoomOutLabel = computed(() => props.zoomOutLabel ?? t('gr.imageViewer.zoomOut', 'Zoom out'))\nconst resolvedResetZoomLabel = computed(() => props.resetZoomLabel ?? t('gr.imageViewer.resetZoom', 'Reset zoom'))\nconst resolvedZoomToNaturalLabel = computed(() => props.zoomToNaturalLabel ?? t('gr.imageViewer.zoomToNatural', 'Actual size (1:1)'))\nconst resolvedRotateLeftLabel = computed(() => props.rotateLeftLabel ?? t('gr.imageViewer.rotateLeft', 'Rotate left'))\nconst resolvedRotateRightLabel = computed(() => props.rotateRightLabel ?? t('gr.imageViewer.rotateRight', 'Rotate right'))\nconst resolvedDownloadLabel = computed(() => props.downloadLabel ?? t('gr.imageViewer.download', 'Download image'))\nconst resolvedEmptyText = computed(() => props.emptyText ?? t('gr.imageViewer.empty', 'No image'))\nconst resolvedAriaLabel = computed(() => props.ariaLabel ?? t('gr.imageViewer.label', 'Image viewer'))\n\nconst emit = defineEmits<GrImageViewerEmits>()\n\ntype GrImageViewerToolbarActions = {\n close: () => void\n prev: () => void\n next: () => void\n zoomIn: () => void\n zoomOut: () => void\n /** Масштаб «один к одному»: реальные 100%, а не номинальные. */\n zoomToNatural: () => void\n reset: () => void\n rotateLeft: () => void\n rotateRight: () => void\n /** Скачать текущий кадр — то же, что делает кнопка тулбара. */\n download: () => void\n}\n\ntype GrImageViewerSlotProps = {\n index: number\n /** Текущий кадр — адрес и альтернативный текст. */\n src?: string\n alt: string\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 open = computed(() => props.modelValue)\n\nconst items = computed<GrImageViewerItem[]>(() =>\n props.urlList.map(item => (typeof item === 'string' ? { src: item } : item)),\n)\n\nconst total = computed(() => items.value.length)\nconst hasImages = computed(() => total.value > 0)\n\nconst currentIndex = ref(0)\nconst imageEl = ref<HTMLImageElement | null>(null)\n// Область кадра: система координат для якорного зума и границы перетаскивания.\nconst stageEl = ref<HTMLElement | null>(null)\n\n// Масштаб / поворот / панорамирование + метрики изображения.\nconst {\n scale,\n rotation,\n isDragging,\n naturalWidth,\n naturalHeight,\n renderedWidth,\n renderedHeight,\n realScale,\n realScalePercent,\n imageStyle,\n zoomValueText,\n zoomIn,\n zoomOut,\n zoomToNatural,\n rotateLeft,\n rotateRight,\n resetTransform,\n resetImageMetrics,\n onImageLoad,\n startObservingImage,\n stopObservingImage,\n isPannable,\n setScaleAt,\n startPan,\n movePan,\n endPan,\n cancelPan,\n onPointerDown: onPanPointerDown,\n} = useZoomPan({\n minScale: () => props.minScale,\n maxScale: () => props.maxScale,\n zoomRate: () => props.zoomRate,\n draggable: () => props.draggable,\n imageEl,\n viewportEl: stageEl,\n onRotate: deg => emit('rotate', deg),\n})\n\n// Зум колесом мыши / жестом трекпада — с якорем в точке под курсором.\nconst { isWheelZooming, onWheel, endWheelZoom } = useWheelGesture({\n enabled: () => props.wheelZoom && hasImages.value,\n applyZoomFactor: (factor, anchor) => setScaleAt(scale.value * factor, anchor),\n})\n\nconst currentItem = computed(() => (hasImages.value ? items.value[currentIndex.value] : undefined))\nconst currentUrl = computed(() => currentItem.value?.src)\nconst currentAlt = computed(() => currentItem.value?.alt ?? '')\nconst displayIndex = computed(() => (hasImages.value ? currentIndex.value + 1 : 0))\n\n/**\n * Позицию объявляем только на смене кадра: при открытии её скажет имя диалога.\n *\n * Регион общий и живёт вне просмотрщика — он помечен `data-gr-live-region` и\n * потому не гасится `inert`, которым просмотрщик накрывает страницу.\n */\nconst { announce } = useAnnouncer()\n\nwatch(displayIndex, (index) => {\n if (!hasImages.value)\n return\n\n announce(t('gr.imageViewer.position', 'Image {index} of {total}', { index, total: total.value }))\n})\n\n// Просмотрщик — оверлей модального класса, значит и слой у него модальный.\n// `zIndexVar` подменяет переменную слоя своей — тот же escape-hatch, что у\n// `useFloating` и `GrLoading`.\nconst viewerStyle = computed(() => ({\n zIndex: `var(${props.zIndexVar ?? '--gr-z-modal'})`,\n}))\n\n// Сенсорные жесты: pinch двумя пальцами и свайп для листания. Мышь идёт мимо —\n// у неё своя ветка перетаскивания в `useZoomPan`.\nconst {\n isPinching,\n onPointerDown: onGesturePointerDown,\n onPointerMove: onGesturePointerMove,\n onPointerUp: onGesturePointerUp,\n onPointerCancel: onGesturePointerCancel,\n reset: resetGestures,\n} = usePointerGestures({\n enabled: () => hasImages.value,\n scale: () => scale.value,\n canSwipe: () => total.value > 1,\n setScaleAt,\n pan: { start: startPan, move: movePan, end: endPan },\n onSwipeLeft: () => next(),\n onSwipeRight: () => prev(),\n})\n\nfunction onPointerDown(event: PointerEvent): void {\n onGesturePointerDown(event)\n onPanPointerDown(event)\n}\n\n// Движение и отпускание — только сенсорная ветка: панораму мышью и пером ведёт\n// `useDragGesture` со своими слушателями на `window`.\nfunction onPointerMove(event: PointerEvent): void {\n onGesturePointerMove(event)\n}\n\nfunction onPointerUp(event: PointerEvent): void {\n onGesturePointerUp(event)\n}\n\nfunction onPointerCancel(event: PointerEvent): void {\n onGesturePointerCancel(event)\n}\n\n// Плавный CSS-переход только для дискретных зумов; при wheel-зуме/перетаскивании отключаем.\nconst imageTransitionClass = computed(() =>\n isWheelZooming.value || isDragging.value || isPinching.value\n ? ''\n : 'transition-transform duration-[var(--gr-duration-fast)] ease-[var(--gr-ease-out)]',\n)\n\n// Курсор «рука» (grab/grabbing), только если включён drag.\nconst imageCursorClass = computed(() => {\n if (!isPannable.value)\n return ''\n return isDragging.value ? 'cursor-grabbing' : 'cursor-grab'\n})\n\nfunction normalizeIndex(value: number): number {\n if (!hasImages.value)\n return 0\n const normalized = Number.isFinite(value) ? Math.trunc(value) : 0\n return ((normalized % total.value) + total.value) % total.value\n}\n\nfunction setIndex(nextIndex: number, options?: { emitSwitch?: boolean }): void {\n const normalizedIndex = normalizeIndex(nextIndex)\n if (normalizedIndex === currentIndex.value)\n return\n\n currentIndex.value = normalizedIndex\n resetTransform()\n resetImageMetrics()\n endWheelZoom()\n\n if (options?.emitSwitch !== false)\n emit('change', normalizedIndex)\n}\n\nfunction syncIndexFromInitial(): void {\n if (!hasImages.value) {\n currentIndex.value = 0\n return\n }\n currentIndex.value = normalizeIndex(props.initialIndex)\n resetTransform()\n resetImageMetrics()\n endWheelZoom()\n}\n\n/**\n * Предзагруженные соседи. Держим ссылки, чтобы загрузку можно было оборвать:\n * сброс `src` у `Image` отменяет незавершённый запрос. Без этого быстрое\n * перелистывание галереи копит запросы, которые уже никому не нужны.\n */\nlet preloadedImages: HTMLImageElement[] = []\n\nfunction cancelPreload(): void {\n for (const image of preloadedImages) image.src = ''\n preloadedImages = []\n}\n\nfunction preloadAt(index: number): void {\n const url = items.value[index]?.src\n if (!url)\n return\n const image = new Image()\n image.decoding = 'async'\n image.src = url\n preloadedImages.push(image)\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\nconst toolbarActions: GrImageViewerToolbarActions = {\n close: closeViewer,\n prev,\n next,\n zoomIn,\n zoomOut,\n zoomToNatural,\n reset: resetTransform,\n rotateLeft,\n rotateRight,\n download,\n}\n\n// Наружу отдаём тот же набор, что получает слот тулбара. Открытия здесь нет\n// намеренно: оно принадлежит `v-model`, и вторая точка входа рассинхронизировала\n// бы состояние с моделью.\ndefineExpose(toolbarActions)\n\nconst toolbarSlotProps = computed<GrImageViewerSlotProps>(() => ({\n index: currentIndex.value,\n src: currentUrl.value,\n alt: currentAlt.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\n/**\n * Скачивание текущего кадра.\n *\n * Компонент делает очевидное — `<a download>` на текущий `src` — и сообщает о\n * нажатии событием. Кросс-доменный адрес браузер всё равно скачает не всегда\n * (атрибут `download` он там игнорирует), поэтому подписанные ссылки и свои\n * запросы делаются иначе: `showDownload: false` плюс своя кнопка в слоте\n * `#toolbar-actions`.\n */\nfunction download(): void {\n const item = currentItem.value\n if (!item?.src)\n return\n\n emit('download', { src: item.src, alt: item.alt ?? '', index: currentIndex.value })\n\n if (typeof document === 'undefined')\n return\n\n const link = document.createElement('a')\n link.href = item.src\n link.download = ''\n link.rel = 'noopener'\n document.body.appendChild(link)\n link.click()\n link.remove()\n}\n\nconst { onKeydown } = useViewerKeyboard({\n actions: { close: closeViewer, prev, next, zoomIn, zoomOut, reset: resetTransform },\n})\n\n// Просмотрщик — модальный класс: бэкдроп, scroll-lock, ловушка фокуса, место в\n// общем стеке. Открытый поверх модалки, он закрывает по Esc себя, а не её.\nconst panelEl = ref<HTMLElement | null>(null)\n\nconst {\n rootEl,\n isMounted,\n isVisible,\n inertAttr,\n portalTarget,\n teleportEnabled,\n themeAttrs,\n onPanelAfterLeave: releasePresence,\n backdrop,\n} = useModalOverlay(open, closeViewer, {\n panel: panelEl,\n closeOnEscape: () => props.closeOnPressEscape,\n closeOnBackdrop: () => props.hideOnClickModal,\n})\n\nwatch(\n () => props.modelValue,\n async (isOpen) => {\n if (!isOpen) {\n stopObservingImage()\n endWheelZoom()\n resetGestures()\n cancelPan()\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 syncIndexFromInitial()\n },\n)\n\n/**\n * Список кадров меняется у живой галереи постоянно: догрузилась следующая\n * страница, заменили один url. Дёрни на любое такое изменение\n * `syncIndexFromInitial()` — пользователя выбросит на `initialIndex` вместе со\n * сброшенным зумом и поворотом.\n *\n * Держимся кадра, а не позиции: если текущий `src` остался в наборе, идём за\n * ним (и трансформации не трогаем). Исчез — остаёмся на своём месте в списке,\n * прижав индекс к границам, и только тогда сбрасываем трансформации: показываем\n * уже другую картинку.\n */\nwatch(\n items,\n (next, prev) => {\n if (!props.modelValue)\n return\n\n if (!next.length) {\n currentIndex.value = 0\n resetTransform()\n resetImageMetrics()\n endWheelZoom()\n return\n }\n\n const shownSrc = prev?.[currentIndex.value]?.src\n const keptIndex = shownSrc ? next.findIndex(item => item.src === shownSrc) : -1\n\n if (keptIndex >= 0) {\n currentIndex.value = keptIndex\n return\n }\n\n currentIndex.value = Math.min(currentIndex.value, next.length - 1)\n resetTransform()\n resetImageMetrics()\n endWheelZoom()\n },\n { deep: true },\n)\n\n/**\n * Соседние кадры греются только у **открытого** просмотрщика: закрытый на\n * странице не должен тянуть два полноразмерных изображения просто потому, что\n * он там есть. `new Image()` существует только в браузере — на сервере эта\n * ветка не выполняется, потому что закрытый просмотрщик до неё не доходит.\n */\nfunction preloadNeighbours(index: number): void {\n cancelPreload()\n\n if (!props.modelValue || !hasImages.value || total.value < 2)\n return\n\n preloadAt(normalizeIndex(index - 1))\n preloadAt(normalizeIndex(index + 1))\n}\n\nwatch([currentIndex, open], ([index]) => preloadNeighbours(index), { immediate: true })\n\nonBeforeUnmount(() => {\n stopObservingImage()\n endWheelZoom()\n cancelPreload()\n resetGestures()\n cancelPan()\n})\n\ndefineSlots<{\n /** Панель инструментов целиком вместо встроенной. */\n 'toolbar'?: (props: GrImageViewerSlotProps) => any\n /** Свои кнопки рядом со встроенными — поворот, скачивание, печать. */\n 'toolbar-actions'?: (props: GrImageViewerSlotProps) => any\n}>()\n</script>\n\n<template>\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <div\n v-if=\"teleportEnabled && isMounted\"\n ref=\"rootEl\"\n v-bind=\"themeAttrs\"\n data-gr-overlay-root\n :inert=\"inertAttr\"\n class=\"fixed inset-0\"\n :style=\"viewerStyle\"\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-label=\"resolvedAriaLabel\"\n @keydown=\"onKeydown\"\n >\n <div class=\"fixed inset-0 overflow-hidden\">\n <Transition\n appear\n enter-active-class=\"duration-[var(--gr-duration-fast)] ease-[var(--gr-ease-out)]\"\n enter-from-class=\"opacity-0\"\n leave-active-class=\"duration-[var(--gr-duration-fast)] ease-[var(--gr-ease-in)]\"\n leave-to-class=\"opacity-0\"\n >\n <div\n v-if=\"isVisible\"\n data-gr-image-viewer-overlay\n :class=\"scrimClass\"\n aria-hidden=\"true\"\n v-on=\"backdrop\"\n />\n </Transition>\n\n <Transition\n appear\n enter-active-class=\"duration-[var(--gr-duration-base)] ease-[var(--gr-ease-out)]\"\n enter-from-class=\"opacity-0 scale-98\"\n leave-active-class=\"duration-[var(--gr-duration-fast)] ease-[var(--gr-ease-in)]\"\n leave-to-class=\"opacity-0 scale-98\"\n @after-leave=\"releasePresence\"\n >\n <div\n v-if=\"isVisible\"\n ref=\"panelEl\"\n data-gr-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-gr-image-viewer-progress\n class=\"font-600\"\n :class=\"badgeClass\"\n >\n {{ displayIndex }} / {{ total }}\n </div>\n\n <div\n v-if=\"showZoomValue\"\n data-gr-image-viewer-zoom-value\n class=\"font-700\"\n :class=\"badgeClass\"\n >\n {{ zoomValueText }}%\n </div>\n </div>\n\n <button\n type=\"button\"\n data-gr-image-viewer-close\n :aria-label=\"resolvedCloseLabel\"\n class=\"h-11 w-11\"\n :class=\"chromeButtonClass\"\n @click=\"closeViewer\"\n >\n <GrIcon size=\"md\">\n <IconX />\n </GrIcon>\n </button>\n </div>\n </div>\n\n <div\n ref=\"stageEl\"\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-gr-image-viewer-prev\n :aria-label=\"resolvedPrevLabel\"\n class=\"absolute left-3 top-1/2 z-20 h-12 w-12 -translate-y-1/2 sm:left-6\"\n :class=\"chromeButtonClass\"\n @click=\"prev\"\n >\n <GrIcon size=\"lg\">\n <IconChevronLeft />\n </GrIcon>\n </button>\n\n <button\n v-if=\"total > 1\"\n type=\"button\"\n data-gr-image-viewer-next\n :aria-label=\"resolvedNextLabel\"\n class=\"absolute right-3 top-1/2 z-20 h-12 w-12 -translate-y-1/2 sm:right-6\"\n :class=\"chromeButtonClass\"\n @click=\"next\"\n >\n <GrIcon size=\"lg\">\n <IconChevronRight />\n </GrIcon>\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-gr-image-viewer-image\n :src=\"currentUrl\"\n :alt=\"currentAlt\"\n draggable=\"false\"\n class=\"max-h-full max-w-full select-none object-contain will-change-transform [touch-action:none]\"\n :class=\"[imageTransitionClass, imageCursorClass]\"\n :style=\"imageStyle\"\n @load=\"onImageLoad\"\n @pointerdown=\"onPointerDown\"\n @pointermove=\"onPointerMove\"\n @pointerup=\"onPointerUp\"\n @pointercancel=\"onPointerCancel\"\n >\n\n <div\n v-else\n :class=\"emptyStateClass\"\n >\n {{ resolvedEmptyText }}\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=\"toolbarShellClass\">\n <button\n type=\"button\"\n data-gr-image-viewer-zoom-out\n :aria-label=\"resolvedZoomOutLabel\"\n class=\"h-11 min-w-11 px-2 text-[length:var(--gr-text-sm)] leading-[var(--gr-leading-sm)] font-600\"\n :class=\"toolbarButtonClass\"\n @click=\"zoomOut\"\n >\n <GrIcon size=\"sm\">\n <IconMinus />\n </GrIcon>\n </button>\n\n <button\n type=\"button\"\n data-gr-image-viewer-zoom-reset\n :aria-label=\"resolvedResetZoomLabel\"\n class=\"h-11 min-w-11 px-3 text-[length:var(--gr-text-xs)] leading-[var(--gr-leading-xs)] font-700\"\n :class=\"toolbarButtonClass\"\n @click=\"resetTransform\"\n >\n 100%\n </button>\n\n <!--\n «1:1» рядом с «100%» не дубль: сброс возвращает вписанный в\n окно кадр (номинальные 100%), а это — пиксель в пиксель, то\n есть реальные 100%. У крупной фотографии между ними разница\n в разы, и собирать эту кнопку из `naturalWidth`/`renderedWidth`\n каждому потребителю незачем.\n -->\n <button\n type=\"button\"\n data-gr-image-viewer-zoom-natural\n :aria-label=\"resolvedZoomToNaturalLabel\"\n class=\"h-11 min-w-11 px-3 text-[length:var(--gr-text-xs)] leading-[var(--gr-leading-xs)] font-700\"\n :class=\"toolbarButtonClass\"\n @click=\"zoomToNatural\"\n >\n 1:1\n </button>\n\n <button\n type=\"button\"\n data-gr-image-viewer-zoom-in\n :aria-label=\"resolvedZoomInLabel\"\n class=\"h-11 min-w-11 px-2 text-[length:var(--gr-text-sm)] leading-[var(--gr-leading-sm)] font-600\"\n :class=\"toolbarButtonClass\"\n @click=\"zoomIn\"\n >\n <GrIcon size=\"sm\">\n <IconPlus />\n </GrIcon>\n </button>\n\n <button\n v-if=\"showDownload\"\n type=\"button\"\n data-gr-image-viewer-download\n :aria-label=\"resolvedDownloadLabel\"\n class=\"h-11 min-w-11 px-2 text-[length:var(--gr-text-sm)] leading-[var(--gr-leading-sm)] font-600\"\n :class=\"toolbarButtonClass\"\n @click=\"download\"\n >\n <GrIcon size=\"sm\">\n <IconDownload />\n </GrIcon>\n </button>\n\n <template v-if=\"$slots['toolbar-actions']\">\n <div :class=\"toolbarSeparatorClass\" aria-hidden=\"true\" />\n\n <slot\n name=\"toolbar-actions\"\n v-bind=\"toolbarSlotProps\"\n />\n\n <div :class=\"toolbarSeparatorClass\" aria-hidden=\"true\" />\n </template>\n\n <div v-else :class=\"toolbarSeparatorClass\" aria-hidden=\"true\" />\n\n <button\n type=\"button\"\n data-gr-image-viewer-rotate-left\n :aria-label=\"resolvedRotateLeftLabel\"\n class=\"h-11 min-w-11 px-2 text-[length:var(--gr-text-sm)] leading-[var(--gr-leading-sm)] font-600\"\n :class=\"toolbarButtonClass\"\n @click=\"rotateLeft\"\n >\n <GrIcon size=\"sm\">\n <IconRotateCcw />\n </GrIcon>\n </button>\n\n <button\n type=\"button\"\n data-gr-image-viewer-rotate-right\n :aria-label=\"resolvedRotateRightLabel\"\n class=\"h-11 min-w-11 px-2 text-[length:var(--gr-text-sm)] leading-[var(--gr-leading-sm)] font-600\"\n :class=\"toolbarButtonClass\"\n @click=\"rotateRight\"\n >\n <GrIcon size=\"sm\">\n <IconRotateCw />\n </GrIcon>\n </button>\n </div>\n </slot>\n </div>\n </div>\n </div>\n </Transition>\n </div>\n </div>\n </teleport>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'\n\nimport { useAnnouncer } from '../../composables/useAnnouncer'\nimport { useModalOverlay } from '../../composables/internal/useModalOverlay'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport IconChevronLeft from '~icons/lucide/chevron-left'\nimport IconChevronRight from '~icons/lucide/chevron-right'\nimport IconDownload from '~icons/lucide/download'\nimport IconMinus from '~icons/lucide/minus'\nimport IconPlus from '~icons/lucide/plus'\nimport IconRotateCcw from '~icons/lucide/rotate-ccw'\nimport IconRotateCw from '~icons/lucide/rotate-cw'\nimport IconX from '~icons/lucide/x'\n\nimport GrIcon from '../GrIcon/GrIcon.vue'\n\nimport {\n badgeClass,\n chromeButtonClass,\n emptyStateClass,\n scrimClass,\n toolbarButtonClass,\n toolbarSeparatorClass,\n toolbarShellClass,\n} from './grImageViewerStyles'\nimport { useZoomPan } from './composables/useZoomPan'\nimport { useWheelGesture } from './composables/useWheelGesture'\nimport { usePointerGestures } from './composables/usePointerGestures'\nimport { useViewerKeyboard } from './composables/useViewerKeyboard'\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 *\n * Логика вынесена в composables: `useZoomPan` (масштаб/поворот/пан + метрики),\n * `useWheelGesture` (зум колесом/трекпадом с rAF-батчингом), `useViewerKeyboard`\n * (клавиатура). Сам SFC отвечает за оверлей, индекс изображений и композицию.\n */\n/** Кадр просмотрщика: только адрес или адрес с альтернативным текстом. */\nexport type GrImageViewerItem = { src: string, alt?: string }\nexport type GrImageViewerSource = string | GrImageViewerItem\n\nexport interface GrImageViewerProps {\n modelValue: boolean\n /**\n * Кадры. Строка — только адрес; объект `{ src, alt }` даёт изображению\n * альтернативный текст: без него просмотрщик пуст для незрячего\n * пользователя, а придумать текст за потребителя компонент не может.\n */\n urlList: GrImageViewerSource[]\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 /**\n * Имя CSS-переменной слоя — escape-hatch мимо `--gr-z-modal`. Сырое число\n * компонент не принимает: слой задаётся шкалой, см. `docs/z-index.md`.\n */\n zIndexVar?: string\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 zoomToNaturalLabel?: string\n /** i18n: aria-label кнопки «повернуть влево». */\n rotateLeftLabel?: string\n /** i18n: aria-label кнопки «повернуть вправо». */\n rotateRightLabel?: string\n /** Показывать кнопку скачивания текущего кадра. */\n showDownload?: boolean\n /** i18n: aria-label кнопки «скачать». */\n downloadLabel?: string\n /** i18n: текст в пустом состоянии (нет изображений). */\n emptyText?: string\n /**\n * Доступное имя слоя. Модальный диалог без имени — нарушение\n * `aria-dialog-name`: диктор объявит «диалог» и замолчит.\n */\n ariaLabel?: string\n}\n\nexport interface GrImageViewerEmits {\n (e: 'update:modelValue', value: boolean): void\n (e: 'close'): void\n /** Показан другой кадр. */\n (e: 'change', newIndex: number): void\n (e: 'rotate', deg: number): void\n /** Нажата кнопка скачивания. Само скачивание компонент уже запустил. */\n (e: 'download', payload: { src: string, alt: string, index: number }): void\n}\n\nconst props = withDefaults(\n defineProps<GrImageViewerProps>(),\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 zIndexVar: undefined,\n closeLabel: undefined,\n prevLabel: undefined,\n nextLabel: undefined,\n zoomInLabel: undefined,\n zoomOutLabel: undefined,\n resetZoomLabel: undefined,\n zoomToNaturalLabel: undefined,\n rotateLeftLabel: undefined,\n rotateRightLabel: undefined,\n showDownload: false,\n downloadLabel: undefined,\n emptyText: undefined,\n ariaLabel: undefined,\n },\n)\n\nconst { t } = useGranularityTranslations()\nconst resolvedCloseLabel = computed(() => props.closeLabel ?? t('gr.imageViewer.close', 'Close image viewer'))\nconst resolvedPrevLabel = computed(() => props.prevLabel ?? t('gr.imageViewer.prev', 'Previous image'))\nconst resolvedNextLabel = computed(() => props.nextLabel ?? t('gr.imageViewer.next', 'Next image'))\nconst resolvedZoomInLabel = computed(() => props.zoomInLabel ?? t('gr.imageViewer.zoomIn', 'Zoom in'))\nconst resolvedZoomOutLabel = computed(() => props.zoomOutLabel ?? t('gr.imageViewer.zoomOut', 'Zoom out'))\nconst resolvedResetZoomLabel = computed(() => props.resetZoomLabel ?? t('gr.imageViewer.resetZoom', 'Reset zoom'))\nconst resolvedZoomToNaturalLabel = computed(() => props.zoomToNaturalLabel ?? t('gr.imageViewer.zoomToNatural', 'Actual size (1:1)'))\nconst resolvedRotateLeftLabel = computed(() => props.rotateLeftLabel ?? t('gr.imageViewer.rotateLeft', 'Rotate left'))\nconst resolvedRotateRightLabel = computed(() => props.rotateRightLabel ?? t('gr.imageViewer.rotateRight', 'Rotate right'))\nconst resolvedDownloadLabel = computed(() => props.downloadLabel ?? t('gr.imageViewer.download', 'Download image'))\nconst resolvedEmptyText = computed(() => props.emptyText ?? t('gr.imageViewer.empty', 'No image'))\nconst resolvedAriaLabel = computed(() => props.ariaLabel ?? t('gr.imageViewer.label', 'Image viewer'))\n\nconst emit = defineEmits<GrImageViewerEmits>()\n\ntype GrImageViewerToolbarActions = {\n close: () => void\n prev: () => void\n next: () => void\n zoomIn: () => void\n zoomOut: () => void\n /** Масштаб «один к одному»: реальные 100%, а не номинальные. */\n zoomToNatural: () => void\n reset: () => void\n rotateLeft: () => void\n rotateRight: () => void\n /** Скачать текущий кадр — то же, что делает кнопка тулбара. */\n download: () => void\n}\n\ntype GrImageViewerSlotProps = {\n index: number\n /** Текущий кадр — адрес и альтернативный текст. */\n src?: string\n alt: string\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 open = computed(() => props.modelValue)\n\nconst items = computed<GrImageViewerItem[]>(() =>\n props.urlList.map(item => (typeof item === 'string' ? { src: item } : item)),\n)\n\nconst total = computed(() => items.value.length)\nconst hasImages = computed(() => total.value > 0)\n\nconst currentIndex = ref(0)\nconst imageEl = ref<HTMLImageElement | null>(null)\n// Область кадра: система координат для якорного зума и границы перетаскивания.\nconst stageEl = ref<HTMLElement | null>(null)\n\n// Масштаб / поворот / панорамирование + метрики изображения.\nconst {\n scale,\n rotation,\n isDragging,\n naturalWidth,\n naturalHeight,\n renderedWidth,\n renderedHeight,\n realScale,\n realScalePercent,\n imageStyle,\n zoomValueText,\n zoomIn,\n zoomOut,\n zoomToNatural,\n rotateLeft,\n rotateRight,\n resetTransform,\n resetImageMetrics,\n onImageLoad,\n startObservingImage,\n stopObservingImage,\n isPannable,\n setScaleAt,\n startPan,\n movePan,\n endPan,\n cancelPan,\n onPointerDown: onPanPointerDown,\n} = useZoomPan({\n minScale: () => props.minScale,\n maxScale: () => props.maxScale,\n zoomRate: () => props.zoomRate,\n draggable: () => props.draggable,\n imageEl,\n viewportEl: stageEl,\n onRotate: deg => emit('rotate', deg),\n})\n\n// Зум колесом мыши / жестом трекпада — с якорем в точке под курсором.\nconst { isWheelZooming, onWheel, endWheelZoom } = useWheelGesture({\n enabled: () => props.wheelZoom && hasImages.value,\n applyZoomFactor: (factor, anchor) => setScaleAt(scale.value * factor, anchor),\n})\n\nconst currentItem = computed(() => (hasImages.value ? items.value[currentIndex.value] : undefined))\nconst currentUrl = computed(() => currentItem.value?.src)\nconst currentAlt = computed(() => currentItem.value?.alt ?? '')\nconst displayIndex = computed(() => (hasImages.value ? currentIndex.value + 1 : 0))\n\n/**\n * Позицию объявляем только на смене кадра: при открытии её скажет имя диалога.\n *\n * Регион общий и живёт вне просмотрщика — он помечен `data-gr-live-region` и\n * потому не гасится `inert`, которым просмотрщик накрывает страницу.\n */\nconst { announce } = useAnnouncer()\n\nwatch(displayIndex, (index) => {\n if (!hasImages.value)\n return\n\n announce(t('gr.imageViewer.position', 'Image {index} of {total}', { index, total: total.value }))\n})\n\n// Просмотрщик — оверлей модального класса, значит и слой у него модальный.\n// `zIndexVar` подменяет переменную слоя своей — тот же escape-hatch, что у\n// `useFloating` и `GrLoading`.\nconst viewerStyle = computed(() => ({\n zIndex: `var(${props.zIndexVar ?? '--gr-z-modal'})`,\n}))\n\n// Сенсорные жесты: pinch двумя пальцами и свайп для листания. Мышь идёт мимо —\n// у неё своя ветка перетаскивания в `useZoomPan`.\nconst {\n isPinching,\n onPointerDown: onGesturePointerDown,\n onPointerMove: onGesturePointerMove,\n onPointerUp: onGesturePointerUp,\n onPointerCancel: onGesturePointerCancel,\n reset: resetGestures,\n} = usePointerGestures({\n enabled: () => hasImages.value,\n scale: () => scale.value,\n canSwipe: () => total.value > 1,\n setScaleAt,\n pan: { start: startPan, move: movePan, end: endPan },\n onSwipeLeft: () => next(),\n onSwipeRight: () => prev(),\n})\n\nfunction onPointerDown(event: PointerEvent): void {\n onGesturePointerDown(event)\n onPanPointerDown(event)\n}\n\n// Движение и отпускание — только сенсорная ветка: панораму мышью и пером ведёт\n// `useDragGesture` со своими слушателями на `window`.\nfunction onPointerMove(event: PointerEvent): void {\n onGesturePointerMove(event)\n}\n\nfunction onPointerUp(event: PointerEvent): void {\n onGesturePointerUp(event)\n}\n\nfunction onPointerCancel(event: PointerEvent): void {\n onGesturePointerCancel(event)\n}\n\n// Плавный CSS-переход только для дискретных зумов; при wheel-зуме/перетаскивании отключаем.\nconst imageTransitionClass = computed(() =>\n isWheelZooming.value || isDragging.value || isPinching.value\n ? ''\n : 'transition-transform duration-[var(--gr-duration-fast)] ease-[var(--gr-ease-out)]',\n)\n\n// Курсор «рука» (grab/grabbing), только если включён drag.\nconst imageCursorClass = computed(() => {\n if (!isPannable.value)\n return ''\n return isDragging.value ? 'cursor-grabbing' : 'cursor-grab'\n})\n\nfunction normalizeIndex(value: number): number {\n if (!hasImages.value)\n return 0\n const normalized = Number.isFinite(value) ? Math.trunc(value) : 0\n return ((normalized % total.value) + total.value) % total.value\n}\n\nfunction setIndex(nextIndex: number, options?: { emitSwitch?: boolean }): void {\n const normalizedIndex = normalizeIndex(nextIndex)\n if (normalizedIndex === currentIndex.value)\n return\n\n currentIndex.value = normalizedIndex\n resetTransform()\n resetImageMetrics()\n endWheelZoom()\n\n if (options?.emitSwitch !== false)\n emit('change', normalizedIndex)\n}\n\nfunction syncIndexFromInitial(): void {\n if (!hasImages.value) {\n currentIndex.value = 0\n return\n }\n currentIndex.value = normalizeIndex(props.initialIndex)\n resetTransform()\n resetImageMetrics()\n endWheelZoom()\n}\n\n/**\n * Предзагруженные соседи. Держим ссылки, чтобы загрузку можно было оборвать:\n * сброс `src` у `Image` отменяет незавершённый запрос. Без этого быстрое\n * перелистывание галереи копит запросы, которые уже никому не нужны.\n */\nlet preloadedImages: HTMLImageElement[] = []\n\nfunction cancelPreload(): void {\n for (const image of preloadedImages) image.src = ''\n preloadedImages = []\n}\n\nfunction preloadAt(index: number): void {\n const url = items.value[index]?.src\n if (!url)\n return\n const image = new Image()\n image.decoding = 'async'\n image.src = url\n preloadedImages.push(image)\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\nconst toolbarActions: GrImageViewerToolbarActions = {\n close: closeViewer,\n prev,\n next,\n zoomIn,\n zoomOut,\n zoomToNatural,\n reset: resetTransform,\n rotateLeft,\n rotateRight,\n download,\n}\n\n// Наружу отдаём тот же набор, что получает слот тулбара. Открытия здесь нет\n// намеренно: оно принадлежит `v-model`, и вторая точка входа рассинхронизировала\n// бы состояние с моделью.\ndefineExpose(toolbarActions)\n\nconst toolbarSlotProps = computed<GrImageViewerSlotProps>(() => ({\n index: currentIndex.value,\n src: currentUrl.value,\n alt: currentAlt.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\n/**\n * Скачивание текущего кадра.\n *\n * Компонент делает очевидное — `<a download>` на текущий `src` — и сообщает о\n * нажатии событием. Кросс-доменный адрес браузер всё равно скачает не всегда\n * (атрибут `download` он там игнорирует), поэтому подписанные ссылки и свои\n * запросы делаются иначе: `showDownload: false` плюс своя кнопка в слоте\n * `#toolbar-actions`.\n */\nfunction download(): void {\n const item = currentItem.value\n if (!item?.src)\n return\n\n emit('download', { src: item.src, alt: item.alt ?? '', index: currentIndex.value })\n\n if (typeof document === 'undefined')\n return\n\n const link = document.createElement('a')\n link.href = item.src\n link.download = ''\n link.rel = 'noopener'\n document.body.appendChild(link)\n link.click()\n link.remove()\n}\n\nconst { onKeydown } = useViewerKeyboard({\n actions: { close: closeViewer, prev, next, zoomIn, zoomOut, reset: resetTransform },\n})\n\n// Просмотрщик — модальный класс: бэкдроп, scroll-lock, ловушка фокуса, место в\n// общем стеке. Открытый поверх модалки, он закрывает по Esc себя, а не её.\nconst panelEl = ref<HTMLElement | null>(null)\n\nconst {\n rootEl,\n isMounted,\n isVisible,\n inertAttr,\n portalTarget,\n teleportEnabled,\n themeAttrs,\n onPanelAfterLeave: releasePresence,\n backdrop,\n} = useModalOverlay(open, closeViewer, {\n panel: panelEl,\n closeOnEscape: () => props.closeOnPressEscape,\n closeOnBackdrop: () => props.hideOnClickModal,\n})\n\nwatch(\n () => props.modelValue,\n async (isOpen) => {\n if (!isOpen) {\n stopObservingImage()\n endWheelZoom()\n resetGestures()\n cancelPan()\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 syncIndexFromInitial()\n },\n)\n\n/**\n * Список кадров меняется у живой галереи постоянно: догрузилась следующая\n * страница, заменили один url. Дёрни на любое такое изменение\n * `syncIndexFromInitial()` — пользователя выбросит на `initialIndex` вместе со\n * сброшенным зумом и поворотом.\n *\n * Держимся кадра, а не позиции: если текущий `src` остался в наборе, идём за\n * ним (и трансформации не трогаем). Исчез — остаёмся на своём месте в списке,\n * прижав индекс к границам, и только тогда сбрасываем трансформации: показываем\n * уже другую картинку.\n */\nwatch(\n items,\n (next, prev) => {\n if (!props.modelValue)\n return\n\n if (!next.length) {\n currentIndex.value = 0\n resetTransform()\n resetImageMetrics()\n endWheelZoom()\n return\n }\n\n const shownSrc = prev?.[currentIndex.value]?.src\n const keptIndex = shownSrc ? next.findIndex(item => item.src === shownSrc) : -1\n\n if (keptIndex >= 0) {\n currentIndex.value = keptIndex\n return\n }\n\n currentIndex.value = Math.min(currentIndex.value, next.length - 1)\n resetTransform()\n resetImageMetrics()\n endWheelZoom()\n },\n { deep: true },\n)\n\n/**\n * Соседние кадры греются только у **открытого** просмотрщика: закрытый на\n * странице не должен тянуть два полноразмерных изображения просто потому, что\n * он там есть. `new Image()` существует только в браузере — на сервере эта\n * ветка не выполняется, потому что закрытый просмотрщик до неё не доходит.\n */\nfunction preloadNeighbours(index: number): void {\n cancelPreload()\n\n if (!props.modelValue || !hasImages.value || total.value < 2)\n return\n\n preloadAt(normalizeIndex(index - 1))\n preloadAt(normalizeIndex(index + 1))\n}\n\nwatch([currentIndex, open], ([index]) => preloadNeighbours(index), { immediate: true })\n\nonBeforeUnmount(() => {\n stopObservingImage()\n endWheelZoom()\n cancelPreload()\n resetGestures()\n cancelPan()\n})\n\ndefineSlots<{\n /** Панель инструментов целиком вместо встроенной. */\n 'toolbar'?: (props: GrImageViewerSlotProps) => any\n /** Свои кнопки рядом со встроенными — поворот, скачивание, печать. */\n 'toolbar-actions'?: (props: GrImageViewerSlotProps) => any\n}>()\n</script>\n\n<template>\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <div\n v-if=\"teleportEnabled && isMounted\"\n ref=\"rootEl\"\n v-bind=\"themeAttrs\"\n data-gr-overlay-root\n :inert=\"inertAttr\"\n class=\"fixed inset-0\"\n :style=\"viewerStyle\"\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-label=\"resolvedAriaLabel\"\n @keydown=\"onKeydown\"\n >\n <div class=\"fixed inset-0 overflow-hidden\">\n <Transition\n appear\n enter-active-class=\"duration-[var(--gr-duration-fast)] ease-[var(--gr-ease-out)]\"\n enter-from-class=\"opacity-0\"\n leave-active-class=\"duration-[var(--gr-duration-fast)] ease-[var(--gr-ease-in)]\"\n leave-to-class=\"opacity-0\"\n >\n <div\n v-if=\"isVisible\"\n data-gr-image-viewer-overlay\n :class=\"scrimClass\"\n aria-hidden=\"true\"\n v-on=\"backdrop\"\n />\n </Transition>\n\n <Transition\n appear\n enter-active-class=\"duration-[var(--gr-duration-base)] ease-[var(--gr-ease-out)]\"\n enter-from-class=\"opacity-0 scale-98\"\n leave-active-class=\"duration-[var(--gr-duration-fast)] ease-[var(--gr-ease-in)]\"\n leave-to-class=\"opacity-0 scale-98\"\n @after-leave=\"releasePresence\"\n >\n <div\n v-if=\"isVisible\"\n ref=\"panelEl\"\n data-gr-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-gr-image-viewer-progress\n class=\"font-600\"\n :class=\"badgeClass\"\n >\n {{ displayIndex }} / {{ total }}\n </div>\n\n <div\n v-if=\"showZoomValue\"\n data-gr-image-viewer-zoom-value\n class=\"font-700\"\n :class=\"badgeClass\"\n >\n {{ zoomValueText }}%\n </div>\n </div>\n\n <button\n type=\"button\"\n data-gr-image-viewer-close\n :aria-label=\"resolvedCloseLabel\"\n class=\"h-11 w-11\"\n :class=\"chromeButtonClass\"\n @click=\"closeViewer\"\n >\n <GrIcon size=\"md\">\n <IconX />\n </GrIcon>\n </button>\n </div>\n </div>\n\n <div\n ref=\"stageEl\"\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-gr-image-viewer-prev\n :aria-label=\"resolvedPrevLabel\"\n class=\"absolute left-3 top-1/2 z-20 h-12 w-12 -translate-y-1/2 sm:left-6\"\n :class=\"chromeButtonClass\"\n @click=\"prev\"\n >\n <GrIcon size=\"lg\">\n <IconChevronLeft />\n </GrIcon>\n </button>\n\n <button\n v-if=\"total > 1\"\n type=\"button\"\n data-gr-image-viewer-next\n :aria-label=\"resolvedNextLabel\"\n class=\"absolute right-3 top-1/2 z-20 h-12 w-12 -translate-y-1/2 sm:right-6\"\n :class=\"chromeButtonClass\"\n @click=\"next\"\n >\n <GrIcon size=\"lg\">\n <IconChevronRight />\n </GrIcon>\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-gr-image-viewer-image\n :src=\"currentUrl\"\n :alt=\"currentAlt\"\n draggable=\"false\"\n class=\"max-h-full max-w-full select-none object-contain will-change-transform [touch-action:none]\"\n :class=\"[imageTransitionClass, imageCursorClass]\"\n :style=\"imageStyle\"\n @load=\"onImageLoad\"\n @pointerdown=\"onPointerDown\"\n @pointermove=\"onPointerMove\"\n @pointerup=\"onPointerUp\"\n @pointercancel=\"onPointerCancel\"\n >\n\n <div\n v-else\n :class=\"emptyStateClass\"\n >\n {{ resolvedEmptyText }}\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=\"toolbarShellClass\">\n <button\n type=\"button\"\n data-gr-image-viewer-zoom-out\n :aria-label=\"resolvedZoomOutLabel\"\n class=\"h-11 min-w-11 px-2 text-[length:var(--gr-text-sm)] leading-[var(--gr-leading-sm)] font-600\"\n :class=\"toolbarButtonClass\"\n @click=\"zoomOut\"\n >\n <GrIcon size=\"sm\">\n <IconMinus />\n </GrIcon>\n </button>\n\n <button\n type=\"button\"\n data-gr-image-viewer-zoom-reset\n :aria-label=\"resolvedResetZoomLabel\"\n class=\"h-11 min-w-11 px-3 text-[length:var(--gr-text-xs)] leading-[var(--gr-leading-xs)] font-700\"\n :class=\"toolbarButtonClass\"\n @click=\"resetTransform\"\n >\n 100%\n </button>\n\n <!--\n «1:1» рядом с «100%» не дубль: сброс возвращает вписанный в\n окно кадр (номинальные 100%), а это — пиксель в пиксель, то\n есть реальные 100%. У крупной фотографии между ними разница\n в разы, и собирать эту кнопку из `naturalWidth`/`renderedWidth`\n каждому потребителю незачем.\n -->\n <button\n type=\"button\"\n data-gr-image-viewer-zoom-natural\n :aria-label=\"resolvedZoomToNaturalLabel\"\n class=\"h-11 min-w-11 px-3 text-[length:var(--gr-text-xs)] leading-[var(--gr-leading-xs)] font-700\"\n :class=\"toolbarButtonClass\"\n @click=\"zoomToNatural\"\n >\n 1:1\n </button>\n\n <button\n type=\"button\"\n data-gr-image-viewer-zoom-in\n :aria-label=\"resolvedZoomInLabel\"\n class=\"h-11 min-w-11 px-2 text-[length:var(--gr-text-sm)] leading-[var(--gr-leading-sm)] font-600\"\n :class=\"toolbarButtonClass\"\n @click=\"zoomIn\"\n >\n <GrIcon size=\"sm\">\n <IconPlus />\n </GrIcon>\n </button>\n\n <button\n v-if=\"showDownload\"\n type=\"button\"\n data-gr-image-viewer-download\n :aria-label=\"resolvedDownloadLabel\"\n class=\"h-11 min-w-11 px-2 text-[length:var(--gr-text-sm)] leading-[var(--gr-leading-sm)] font-600\"\n :class=\"toolbarButtonClass\"\n @click=\"download\"\n >\n <GrIcon size=\"sm\">\n <IconDownload />\n </GrIcon>\n </button>\n\n <template v-if=\"$slots['toolbar-actions']\">\n <div :class=\"toolbarSeparatorClass\" aria-hidden=\"true\" />\n\n <slot\n name=\"toolbar-actions\"\n v-bind=\"toolbarSlotProps\"\n />\n\n <div :class=\"toolbarSeparatorClass\" aria-hidden=\"true\" />\n </template>\n\n <div v-else :class=\"toolbarSeparatorClass\" aria-hidden=\"true\" />\n\n <button\n type=\"button\"\n data-gr-image-viewer-rotate-left\n :aria-label=\"resolvedRotateLeftLabel\"\n class=\"h-11 min-w-11 px-2 text-[length:var(--gr-text-sm)] leading-[var(--gr-leading-sm)] font-600\"\n :class=\"toolbarButtonClass\"\n @click=\"rotateLeft\"\n >\n <GrIcon size=\"sm\">\n <IconRotateCcw />\n </GrIcon>\n </button>\n\n <button\n type=\"button\"\n data-gr-image-viewer-rotate-right\n :aria-label=\"resolvedRotateRightLabel\"\n class=\"h-11 min-w-11 px-2 text-[length:var(--gr-text-sm)] leading-[var(--gr-leading-sm)] font-600\"\n :class=\"toolbarButtonClass\"\n @click=\"rotateRight\"\n >\n <GrIcon size=\"sm\">\n <IconRotateCw />\n </GrIcon>\n </button>\n </div>\n </slot>\n </div>\n </div>\n </div>\n </Transition>\n </div>\n </div>\n </teleport>\n</template>\n"],"mappings":"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"}
|