@feugene/granularity 0.31.1 → 0.32.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/{overlayStack-C8cfsqXa.js → overlayStack-0CZS99Da.js} +40 -6
- package/dist/chunks/overlayStack-0CZS99Da.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/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/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/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/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/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/index.js +11 -11
- 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 +1 -1
- package/package.json +1 -1
- package/dist/chunks/overlayStack-C8cfsqXa.js.map +0 -1
- package/dist/chunks/useModalOverlay-Cv5hF63W.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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GrImageViewer-CG4vUBg_.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// Сенсорные жесты: 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 layerZIndex,\n portalTarget,\n teleportEnabled,\n themeAttrs,\n onPanelAfterLeave: releasePresence,\n backdrop,\n} = useModalOverlay(open, closeViewer, {\n // Escape-hatch доходит до расчёта высоты: глубина слоя прибавляется к той\n // переменной, которую задал потребитель, а не к `--gr-z-modal` мимо неё.\n zIndexVar: () => props.zIndexVar,\n panel: panelEl,\n closeOnEscape: () => props.closeOnPressEscape,\n closeOnBackdrop: () => props.hideOnClickModal,\n})\n\n// Просмотрщик — оверлей модального класса, значит и слой у него модальный.\n// `zIndexVar` подменяет переменную слоя своей — тот же escape-hatch, что у\n// `useFloating` и `GrLoading`.\nconst viewerStyle = computed(() => ({\n zIndex: layerZIndex.value,\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// Сенсорные жесты: 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 layerZIndex,\n portalTarget,\n teleportEnabled,\n themeAttrs,\n onPanelAfterLeave: releasePresence,\n backdrop,\n} = useModalOverlay(open, closeViewer, {\n // Escape-hatch доходит до расчёта высоты: глубина слоя прибавляется к той\n // переменной, которую задал потребитель, а не к `--gr-z-modal` мимо неё.\n zIndexVar: () => props.zIndexVar,\n panel: panelEl,\n closeOnEscape: () => props.closeOnPressEscape,\n closeOnBackdrop: () => props.hideOnClickModal,\n})\n\n// Просмотрщик — оверлей модального класса, значит и слой у него модальный.\n// `zIndexVar` подменяет переменную слоя своей — тот же escape-hatch, что у\n// `useFloating` и `GrLoading`.\nconst viewerStyle = computed(() => ({\n zIndex: layerZIndex.value,\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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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { t as GrImageViewer_default } from "./chunks/GrImageViewer-
|
|
1
|
+
import { t as GrImageViewer_default } from "./chunks/GrImageViewer-CG4vUBg_.js";
|
|
2
2
|
import { n as grImageViewerSafelist, t as grImageViewerConfig } from "../../chunks/config-BDSaD-5p.js";
|
|
3
3
|
export { GrImageViewer_default as GrImageViewer, GrImageViewer_default as default, grImageViewerConfig, grImageViewerSafelist };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { i as useGrComponentProp } from "../../../chunks/context-CTbuwcYs.js";
|
|
2
2
|
import { n as useGranularityTranslations } from "../../../chunks/granularityI18n-BC5zsj_d.js";
|
|
3
|
-
import { t as useModalOverlay } from "../../../chunks/useModalOverlay-
|
|
3
|
+
import { t as useModalOverlay } from "../../../chunks/useModalOverlay-D__CqLGn.js";
|
|
4
4
|
import { a as overlayTransition, d as panelSectionClass, f as panelTransition, i as overlay, m as root, n as getGrModalShellClass, r as layoutByScroll, s as panelBodyScrollClass, t as getGrModalPanelClass } from "../../../chunks/grModalStyles-EZfuEiw_.js";
|
|
5
5
|
import { Teleport, Transition, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, inject, mergeProps, normalizeClass, onScopeDispose, openBlock, provide, ref, renderSlot, toHandlers, unref, useId, useSlots, watch, withCtx } from "vue";
|
|
6
6
|
//#region src/components/GrModal/context.ts
|
|
@@ -191,7 +191,7 @@ var GrModal_default = /* @__PURE__ */ defineComponent({
|
|
|
191
191
|
function toggle() {
|
|
192
192
|
emit("update:modelValue", !props.modelValue);
|
|
193
193
|
}
|
|
194
|
-
const { rootEl, isMounted, isVisible, inertAttr, portalTarget, teleportEnabled, themeAttrs, onPanelAfterLeave: releasePresence, backdrop } = useModalOverlay(computed(() => props.modelValue), close, {
|
|
194
|
+
const { rootEl, isMounted, isVisible, inertAttr, layerZIndex, portalTarget, teleportEnabled, themeAttrs, onPanelAfterLeave: releasePresence, backdrop } = useModalOverlay(computed(() => props.modelValue), close, {
|
|
195
195
|
panel: panelRef,
|
|
196
196
|
closeOnEscape: () => props.closeOnEsc,
|
|
197
197
|
closeOnBackdrop: () => props.closeOnBackdrop,
|
|
@@ -224,6 +224,7 @@ var GrModal_default = /* @__PURE__ */ defineComponent({
|
|
|
224
224
|
role: "dialog",
|
|
225
225
|
"aria-modal": "true",
|
|
226
226
|
class: unref(root),
|
|
227
|
+
style: { zIndex: unref(layerZIndex) },
|
|
227
228
|
inert: unref(inertAttr),
|
|
228
229
|
"aria-labelledby": labelledBy.value,
|
|
229
230
|
"aria-describedby": describedBy.value,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GrModal-D0mf1SAx.js","names":["$slots"],"sources":["../../../../src/components/GrModal/context.ts","../../../../src/components/GrModal/GrModal.vue","../../../../src/components/GrModal/GrModal.vue"],"sourcesContent":["import type { InjectionKey } from 'vue'\nimport { inject, onScopeDispose, provide, watch } from 'vue'\n\nexport interface GrModalContext {\n /** Id, который обязан оказаться на элементе заголовка окна. */\n titleId: string\n /** Id, который обязан оказаться на элементе описания окна. */\n descriptionId: string\n /** «Заголовок отрисован» — окно ставит `aria-labelledby`. Возвращает отказ от заявки. */\n claimTitle: () => () => void\n /** То же для описания и `aria-describedby`. */\n claimDescription: () => () => void\n}\n\nexport const GR_MODAL_CONTEXT_KEY: InjectionKey<GrModalContext> = Symbol('GrModalContext')\n\nexport function provideGrModalContext(context: GrModalContext): void {\n provide(GR_MODAL_CONTEXT_KEY, context)\n}\n\n/**\n * Контекст окна для его частей.\n *\n * Именно контекст, а не проп: заголовок рисует `GrDialogHeader` — внук\n * `GrModal` через слот, — а `aria-labelledby` стоит на корне окна.\n *\n * **Id принадлежит окну, а не заголовку.** Заголовок только сообщает, что\n * отрисовался. Обратный порядок (id рождается у потомка и уезжает наверх)\n * означает мутацию состояния окна во время его собственного патча: Vue уходит\n * в рекурсивный ре-рендер и монтирует заголовок заново, пока не упрётся в\n * лимит. Стабильный id этого класса ошибок не допускает по построению.\n *\n * `null` вне окна — законное состояние: `GrDialogHeader` используют и отдельно.\n */\nexport function useGrModalContext(): GrModalContext | null {\n return inject(GR_MODAL_CONTEXT_KEY, null)\n}\n\n/**\n * Объявляет элемент заголовком окна, пока `active` истинно.\n *\n * Возвращает id для элемента — или `undefined` вне окна: тогда заголовок просто\n * рисуется без связи.\n */\nexport function useGrModalTitle(active: () => boolean): { titleId: string | undefined } {\n const context = useGrModalContext()\n if (!context)\n return { titleId: undefined }\n\n let release: (() => void) | undefined\n\n watch(\n active,\n (isActive) => {\n release?.()\n release = isActive ? context.claimTitle() : undefined\n },\n // `post`: заявка меняет состояние окна, а заголовок — его потомок, и в\n // `pre`-фазе мутация пришлась бы на патч самого окна.\n { immediate: true, flush: 'post' },\n )\n\n onScopeDispose(() => release?.())\n\n return { titleId: context.titleId }\n}\n","<script setup lang=\"ts\">\n/**\n * GrModal — GR-примитив модального окна.\n *\n * Публичный API:\n * - `modelValue` (v-model) — открыто/закрыто;\n * - `size` — размер панели (`sm | md | lg | xl | full`);\n * - `scrollBehavior` — кто скроллится при длинном содержимом;\n * - `closeOnBackdrop` (default: true) — закрывать при клике по оверлею;\n * - `closeOnEsc` (default: true) — закрывать по Esc;\n * - слоты `#title` / `#description` — получают id и связываются с окном через\n * `aria-labelledby` / `aria-describedby`;\n * - слоты `#header` / `#footer` — пустые layout-области вне скроллящегося тела:\n * при `inside` они остаются на месте. Своей разметки примитив в них не\n * добавляет — шапку и подвал рисует `GrDialog`.\n *\n * A11y: **имя у модального слоя обязательно**. Имя берётся из `#title` (или из\n * заголовка `GrDialog`, который заявляет себя тем же контекстом), иначе из\n * пропа `ariaLabel`, иначе — из i18n, чтобы безымянного окна не получилось ни\n * при каком употреблении (axe: `aria-dialog-name`).\n *\n * Механика слоя — `useModalOverlay`: стек (Esc верхнему, `inert` нижним окнам,\n * возврат фокуса), ловушка фокуса, блокировка скролла, портал и присутствие в\n * DOM до конца leave-анимации. Инварианты этой сборки описаны там же.\n */\nimport { useGrComponentProp } from '../GrConfigProvider/context'\nimport { computed, ref, useId, useSlots, watch } from 'vue'\n\nimport { useModalOverlay } from '../../composables/internal/useModalOverlay'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { provideGrModalContext } from './context'\n\nimport {\n type GrModalScrollBehavior,\n type GrModalSize,\n getGrModalPanelClass,\n getGrModalShellClass,\n layoutByScroll,\n overlay as overlayClass,\n overlayTransition,\n panelBodyScrollClass,\n panelSectionClass,\n panelTransition,\n root as rootClass,\n} from './grModalStyles'\n\nexport type { GrModalScrollBehavior }\n\nexport interface GrModalProps {\n modelValue: boolean\n closeOnBackdrop?: boolean\n closeOnEsc?: boolean\n size?: GrModalSize\n /**\n * Кто скроллится при длинном содержимом: весь оверлей (`outside`) или сама\n * панель (`inside` — окно остаётся на месте, шапка и подвал на виду).\n */\n scrollBehavior?: GrModalScrollBehavior\n /**\n * Доступное имя окна, когда заголовка в слоте `#title` нет. Слот сильнее:\n * при нём имя даёт `aria-labelledby`.\n */\n ariaLabel?: string\n /**\n * Элемент, получающий фокус при открытии. По умолчанию — сама панель: она\n * фокусируема программно (`tabindex=\"-1\"`), диктор объявляет окно целиком, а\n * первый Tab приводит в начало содержимого.\n */\n initialFocus?: HTMLElement | null\n}\n\nexport interface GrModalEmits {\n (e: 'update:modelValue', value: boolean): void\n /** Окно открылось и анимация закончилась. */\n (e: 'opened'): void\n /** Окно закрылось и анимация закончилась — содержимое можно размонтировать. */\n (e: 'closed'): void\n}\n\nimport './defaults'\n\nconst props = withDefaults(defineProps<GrModalProps>(), {\n closeOnBackdrop: true,\n closeOnEsc: true,\n // Дефолт `size` живёт в резолвере ниже, а не здесь: Vue подставил бы его\n // до того, как компонент заглянет в `GrConfigProvider`.\n size: undefined,\n scrollBehavior: 'outside',\n ariaLabel: undefined,\n initialFocus: null,\n})\n\nconst emit = defineEmits<GrModalEmits>()\n\ndefineSlots<{\n default?: () => any\n title?: () => any\n description?: () => any\n /** Закреплённая шапка: при `inside` остаётся на месте, скроллится только тело. */\n header?: () => any\n /** Закреплённый подвал: там же, где и шапка, — вне скроллящегося тела. */\n footer?: () => any\n}>()\n\nconst slots = useSlots()\n\n// Эффективный размер: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentProp('GrModal', 'size', () => props.size, 'md')\n\nconst panelClass = computed(() => getGrModalPanelClass(resolvedSize.value, props.scrollBehavior))\nconst shellClass = computed(() => getGrModalShellClass(resolvedSize.value, props.scrollBehavior))\nconst layoutClass = computed(() => layoutByScroll[props.scrollBehavior])\n\n// Тело скроллится только при `inside`, и тогда же обязано попадать в таб-порядок:\n// длинное содержимое без единого фокусируемого элемента иначе недостижимо с\n// клавиатуры (axe: `scrollable-region-focusable`).\nconst isBodyScrollable = computed(() => props.scrollBehavior === 'inside')\n\n// Панель фокусируема программно (`tabindex=\"-1\"`) — это и приёмник фокуса по\n// умолчанию, и запасной, когда фокусировать внутри нечего.\nconst panelRef = ref<HTMLElement | null>(null)\n\nconst { t } = useGranularityTranslations()\n\n// ————— Имя и описание окна.\n// Id принадлежат окну и раздаются вниз контекстом: заголовок рисует либо слот\n// `#title`, либо `GrDialogHeader` глубже по дереву, и оба лишь заявляют, что\n// отрисовались. Обратное владение (id рождается у заголовка) мутировало бы\n// состояние окна во время его же патча — см. `context.ts`.\nconst titleId = useId()\nconst descriptionId = useId()\n\nconst hasTitle = ref(false)\nconst hasDescription = ref(false)\n\n// Счётчики намеренно не реактивные: наружу торчит только «заголовок есть».\n// Пересоздание содержимого слота меняет счётчик, но не флаг, и лишнего рендера\n// окна не случается.\nlet titleClaims = 0\nlet descriptionClaims = 0\n\nfunction claim(add: () => void, remove: () => void): () => void {\n add()\n let released = false\n return () => {\n if (released)\n return\n released = true\n remove()\n }\n}\n\nfunction claimTitle(): () => void {\n return claim(\n () => {\n titleClaims += 1\n hasTitle.value = true\n },\n () => {\n titleClaims -= 1\n hasTitle.value = titleClaims > 0\n },\n )\n}\n\nfunction claimDescription(): () => void {\n return claim(\n () => {\n descriptionClaims += 1\n hasDescription.value = true\n },\n () => {\n descriptionClaims -= 1\n hasDescription.value = descriptionClaims > 0\n },\n )\n}\n\nprovideGrModalContext({ titleId, descriptionId, claimTitle, claimDescription })\n\n// Собственные слоты заявляются тем же путём, что и заголовок `GrDialog`.\nlet releaseOwnTitle: (() => void) | undefined\nlet releaseOwnDescription: (() => void) | undefined\n\nwatch(\n () => Boolean(slots.title),\n (has) => {\n releaseOwnTitle?.()\n releaseOwnTitle = has ? claimTitle() : undefined\n },\n { immediate: true },\n)\n\nwatch(\n () => Boolean(slots.description),\n (has) => {\n releaseOwnDescription?.()\n releaseOwnDescription = has ? claimDescription() : undefined\n },\n { immediate: true },\n)\n\nconst labelledBy = computed(() => (hasTitle.value ? titleId : undefined))\nconst describedBy = computed(() => (hasDescription.value ? descriptionId : undefined))\n\n// Собственный `aria-label` ставим только в отсутствие заголовка: при обоих\n// атрибутах `aria-labelledby` всё равно сильнее, и второе имя было бы шумом.\nconst ariaLabelAttr = computed(() => {\n if (labelledBy.value)\n return undefined\n return props.ariaLabel || t('gr.modal.title', 'Dialog')\n})\n\n// Обобщённое имя из локали спасает от безымянного окна, но осмысленное имя\n// знает только автор. Предупреждаем при первом открытии, а не при монтировании:\n// закрытая модалка ещё ничего не нарушила, и ругаться на неё рано.\nlet missingNameWarned = false\n\nfunction warnMissingAccessibleName(): void {\n if (missingNameWarned)\n return\n if (!__GR_DEV__)\n return\n if (slots.title || props.ariaLabel)\n return\n\n missingNameWarned = true\n console.warn(\n '[GrModal] Окно без доступного имени: передайте слот #title или проп ariaLabel. '\n + 'Пока используется обобщённое имя из локали.',\n )\n}\n\n// Два заголовка на окно — это разъехавшийся `aria-labelledby`: диктор прочитает\n// первый, а автор будет уверен во втором.\nfunction warnDuplicateTitle(): void {\n if (!__GR_DEV__)\n return\n if (titleClaims <= 1)\n return\n\n console.warn(\n '[GrModal] На окно заявлено несколько заголовков — имя берётся от первого. '\n + 'Оставьте один: слот #title у GrModal либо заголовок GrDialog.',\n )\n}\n\nfunction open(): void {\n emit('update:modelValue', true)\n}\n\nfunction close(): void {\n emit('update:modelValue', false)\n}\n\nfunction toggle(): void {\n emit('update:modelValue', !props.modelValue)\n}\n\n// ————— Модальный слой целиком: стек, ловушка фокуса, `inert`, скролл-лок,\n// портал, присутствие в DOM и guard подложки.\nconst {\n rootEl,\n isMounted,\n isVisible,\n inertAttr,\n layerZIndex,\n portalTarget,\n teleportEnabled,\n themeAttrs,\n onPanelAfterLeave: releasePresence,\n backdrop,\n} = useModalOverlay(computed(() => props.modelValue), close, {\n panel: panelRef,\n closeOnEscape: () => props.closeOnEsc,\n closeOnBackdrop: () => props.closeOnBackdrop,\n // Окно фокусирует саму панель, когда внутри фокусировать нечего и когда\n // потребитель не указал цель явно.\n initialFocus: () => props.initialFocus ?? panelRef.value,\n})\n\nfunction onPanelAfterLeave(): void {\n releasePresence()\n emit('closed')\n}\n\nwatch(\n () => props.modelValue,\n (value) => {\n if (!value)\n return\n warnMissingAccessibleName()\n warnDuplicateTitle()\n },\n { immediate: true },\n)\n\ndefineExpose({ open, close, toggle })\n</script>\n\n<template>\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <div\n v-if=\"teleportEnabled && isMounted\"\n ref=\"rootEl\"\n data-gr-overlay-root\n v-bind=\"themeAttrs\"\n role=\"dialog\"\n aria-modal=\"true\"\n :class=\"rootClass\"\n :style=\"{ zIndex: layerZIndex }\"\n :inert=\"inertAttr\"\n :aria-labelledby=\"labelledBy\"\n :aria-describedby=\"describedBy\"\n :aria-label=\"ariaLabelAttr\"\n >\n <div :class=\"shellClass\">\n <div :class=\"layoutClass\">\n <Transition\n appear\n :enter-active-class=\"overlayTransition.enter\"\n :enter-from-class=\"overlayTransition.enterFrom\"\n :enter-to-class=\"overlayTransition.enterTo\"\n :leave-active-class=\"overlayTransition.leave\"\n :leave-from-class=\"overlayTransition.leaveFrom\"\n :leave-to-class=\"overlayTransition.leaveTo\"\n >\n <div\n v-if=\"isVisible\"\n data-gr-modal-overlay\n :class=\"overlayClass\"\n aria-hidden=\"true\"\n v-on=\"backdrop\"\n />\n </Transition>\n\n <!-- `opened`/`closed` вешаем на транзишн панели, а не на корневой:\n анимация живёт здесь, и только её конец что-то значит для\n потребителя, который ждёт момента для размонтирования. -->\n <Transition\n appear\n :enter-active-class=\"panelTransition.enter\"\n :enter-from-class=\"panelTransition.enterFrom\"\n :enter-to-class=\"panelTransition.enterTo\"\n :leave-active-class=\"panelTransition.leave\"\n :leave-from-class=\"panelTransition.leaveFrom\"\n :leave-to-class=\"panelTransition.leaveTo\"\n @after-enter=\"emit('opened')\"\n @after-leave=\"onPanelAfterLeave\"\n >\n <div\n v-if=\"isVisible\"\n ref=\"panelRef\"\n data-gr-modal-panel\n tabindex=\"-1\"\n :class=\"panelClass\"\n >\n <div v-if=\"$slots.title\" :id=\"titleId\" data-gr-modal-title>\n <slot name=\"title\" />\n </div>\n <div v-if=\"$slots.description\" :id=\"descriptionId\" data-gr-modal-description>\n <slot name=\"description\" />\n </div>\n\n <div v-if=\"$slots.header\" data-gr-modal-header :class=\"panelSectionClass\">\n <slot name=\"header\" />\n </div>\n\n <!-- При `inside` тело едет отдельно от шапки и подвала: иначе\n режим ничем не отличался бы от скролла всей панели. -->\n <div\n v-if=\"isBodyScrollable\"\n data-gr-modal-body\n tabindex=\"0\"\n :class=\"panelBodyScrollClass\"\n >\n <slot />\n </div>\n <slot v-else />\n\n <div v-if=\"$slots.footer\" data-gr-modal-footer :class=\"panelSectionClass\">\n <slot name=\"footer\" />\n </div>\n </div>\n </Transition>\n </div>\n </div>\n </div>\n </teleport>\n</template>\n","<script setup lang=\"ts\">\n/**\n * GrModal — GR-примитив модального окна.\n *\n * Публичный API:\n * - `modelValue` (v-model) — открыто/закрыто;\n * - `size` — размер панели (`sm | md | lg | xl | full`);\n * - `scrollBehavior` — кто скроллится при длинном содержимом;\n * - `closeOnBackdrop` (default: true) — закрывать при клике по оверлею;\n * - `closeOnEsc` (default: true) — закрывать по Esc;\n * - слоты `#title` / `#description` — получают id и связываются с окном через\n * `aria-labelledby` / `aria-describedby`;\n * - слоты `#header` / `#footer` — пустые layout-области вне скроллящегося тела:\n * при `inside` они остаются на месте. Своей разметки примитив в них не\n * добавляет — шапку и подвал рисует `GrDialog`.\n *\n * A11y: **имя у модального слоя обязательно**. Имя берётся из `#title` (или из\n * заголовка `GrDialog`, который заявляет себя тем же контекстом), иначе из\n * пропа `ariaLabel`, иначе — из i18n, чтобы безымянного окна не получилось ни\n * при каком употреблении (axe: `aria-dialog-name`).\n *\n * Механика слоя — `useModalOverlay`: стек (Esc верхнему, `inert` нижним окнам,\n * возврат фокуса), ловушка фокуса, блокировка скролла, портал и присутствие в\n * DOM до конца leave-анимации. Инварианты этой сборки описаны там же.\n */\nimport { useGrComponentProp } from '../GrConfigProvider/context'\nimport { computed, ref, useId, useSlots, watch } from 'vue'\n\nimport { useModalOverlay } from '../../composables/internal/useModalOverlay'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { provideGrModalContext } from './context'\n\nimport {\n type GrModalScrollBehavior,\n type GrModalSize,\n getGrModalPanelClass,\n getGrModalShellClass,\n layoutByScroll,\n overlay as overlayClass,\n overlayTransition,\n panelBodyScrollClass,\n panelSectionClass,\n panelTransition,\n root as rootClass,\n} from './grModalStyles'\n\nexport type { GrModalScrollBehavior }\n\nexport interface GrModalProps {\n modelValue: boolean\n closeOnBackdrop?: boolean\n closeOnEsc?: boolean\n size?: GrModalSize\n /**\n * Кто скроллится при длинном содержимом: весь оверлей (`outside`) или сама\n * панель (`inside` — окно остаётся на месте, шапка и подвал на виду).\n */\n scrollBehavior?: GrModalScrollBehavior\n /**\n * Доступное имя окна, когда заголовка в слоте `#title` нет. Слот сильнее:\n * при нём имя даёт `aria-labelledby`.\n */\n ariaLabel?: string\n /**\n * Элемент, получающий фокус при открытии. По умолчанию — сама панель: она\n * фокусируема программно (`tabindex=\"-1\"`), диктор объявляет окно целиком, а\n * первый Tab приводит в начало содержимого.\n */\n initialFocus?: HTMLElement | null\n}\n\nexport interface GrModalEmits {\n (e: 'update:modelValue', value: boolean): void\n /** Окно открылось и анимация закончилась. */\n (e: 'opened'): void\n /** Окно закрылось и анимация закончилась — содержимое можно размонтировать. */\n (e: 'closed'): void\n}\n\nimport './defaults'\n\nconst props = withDefaults(defineProps<GrModalProps>(), {\n closeOnBackdrop: true,\n closeOnEsc: true,\n // Дефолт `size` живёт в резолвере ниже, а не здесь: Vue подставил бы его\n // до того, как компонент заглянет в `GrConfigProvider`.\n size: undefined,\n scrollBehavior: 'outside',\n ariaLabel: undefined,\n initialFocus: null,\n})\n\nconst emit = defineEmits<GrModalEmits>()\n\ndefineSlots<{\n default?: () => any\n title?: () => any\n description?: () => any\n /** Закреплённая шапка: при `inside` остаётся на месте, скроллится только тело. */\n header?: () => any\n /** Закреплённый подвал: там же, где и шапка, — вне скроллящегося тела. */\n footer?: () => any\n}>()\n\nconst slots = useSlots()\n\n// Эффективный размер: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentProp('GrModal', 'size', () => props.size, 'md')\n\nconst panelClass = computed(() => getGrModalPanelClass(resolvedSize.value, props.scrollBehavior))\nconst shellClass = computed(() => getGrModalShellClass(resolvedSize.value, props.scrollBehavior))\nconst layoutClass = computed(() => layoutByScroll[props.scrollBehavior])\n\n// Тело скроллится только при `inside`, и тогда же обязано попадать в таб-порядок:\n// длинное содержимое без единого фокусируемого элемента иначе недостижимо с\n// клавиатуры (axe: `scrollable-region-focusable`).\nconst isBodyScrollable = computed(() => props.scrollBehavior === 'inside')\n\n// Панель фокусируема программно (`tabindex=\"-1\"`) — это и приёмник фокуса по\n// умолчанию, и запасной, когда фокусировать внутри нечего.\nconst panelRef = ref<HTMLElement | null>(null)\n\nconst { t } = useGranularityTranslations()\n\n// ————— Имя и описание окна.\n// Id принадлежат окну и раздаются вниз контекстом: заголовок рисует либо слот\n// `#title`, либо `GrDialogHeader` глубже по дереву, и оба лишь заявляют, что\n// отрисовались. Обратное владение (id рождается у заголовка) мутировало бы\n// состояние окна во время его же патча — см. `context.ts`.\nconst titleId = useId()\nconst descriptionId = useId()\n\nconst hasTitle = ref(false)\nconst hasDescription = ref(false)\n\n// Счётчики намеренно не реактивные: наружу торчит только «заголовок есть».\n// Пересоздание содержимого слота меняет счётчик, но не флаг, и лишнего рендера\n// окна не случается.\nlet titleClaims = 0\nlet descriptionClaims = 0\n\nfunction claim(add: () => void, remove: () => void): () => void {\n add()\n let released = false\n return () => {\n if (released)\n return\n released = true\n remove()\n }\n}\n\nfunction claimTitle(): () => void {\n return claim(\n () => {\n titleClaims += 1\n hasTitle.value = true\n },\n () => {\n titleClaims -= 1\n hasTitle.value = titleClaims > 0\n },\n )\n}\n\nfunction claimDescription(): () => void {\n return claim(\n () => {\n descriptionClaims += 1\n hasDescription.value = true\n },\n () => {\n descriptionClaims -= 1\n hasDescription.value = descriptionClaims > 0\n },\n )\n}\n\nprovideGrModalContext({ titleId, descriptionId, claimTitle, claimDescription })\n\n// Собственные слоты заявляются тем же путём, что и заголовок `GrDialog`.\nlet releaseOwnTitle: (() => void) | undefined\nlet releaseOwnDescription: (() => void) | undefined\n\nwatch(\n () => Boolean(slots.title),\n (has) => {\n releaseOwnTitle?.()\n releaseOwnTitle = has ? claimTitle() : undefined\n },\n { immediate: true },\n)\n\nwatch(\n () => Boolean(slots.description),\n (has) => {\n releaseOwnDescription?.()\n releaseOwnDescription = has ? claimDescription() : undefined\n },\n { immediate: true },\n)\n\nconst labelledBy = computed(() => (hasTitle.value ? titleId : undefined))\nconst describedBy = computed(() => (hasDescription.value ? descriptionId : undefined))\n\n// Собственный `aria-label` ставим только в отсутствие заголовка: при обоих\n// атрибутах `aria-labelledby` всё равно сильнее, и второе имя было бы шумом.\nconst ariaLabelAttr = computed(() => {\n if (labelledBy.value)\n return undefined\n return props.ariaLabel || t('gr.modal.title', 'Dialog')\n})\n\n// Обобщённое имя из локали спасает от безымянного окна, но осмысленное имя\n// знает только автор. Предупреждаем при первом открытии, а не при монтировании:\n// закрытая модалка ещё ничего не нарушила, и ругаться на неё рано.\nlet missingNameWarned = false\n\nfunction warnMissingAccessibleName(): void {\n if (missingNameWarned)\n return\n if (!__GR_DEV__)\n return\n if (slots.title || props.ariaLabel)\n return\n\n missingNameWarned = true\n console.warn(\n '[GrModal] Окно без доступного имени: передайте слот #title или проп ariaLabel. '\n + 'Пока используется обобщённое имя из локали.',\n )\n}\n\n// Два заголовка на окно — это разъехавшийся `aria-labelledby`: диктор прочитает\n// первый, а автор будет уверен во втором.\nfunction warnDuplicateTitle(): void {\n if (!__GR_DEV__)\n return\n if (titleClaims <= 1)\n return\n\n console.warn(\n '[GrModal] На окно заявлено несколько заголовков — имя берётся от первого. '\n + 'Оставьте один: слот #title у GrModal либо заголовок GrDialog.',\n )\n}\n\nfunction open(): void {\n emit('update:modelValue', true)\n}\n\nfunction close(): void {\n emit('update:modelValue', false)\n}\n\nfunction toggle(): void {\n emit('update:modelValue', !props.modelValue)\n}\n\n// ————— Модальный слой целиком: стек, ловушка фокуса, `inert`, скролл-лок,\n// портал, присутствие в DOM и guard подложки.\nconst {\n rootEl,\n isMounted,\n isVisible,\n inertAttr,\n layerZIndex,\n portalTarget,\n teleportEnabled,\n themeAttrs,\n onPanelAfterLeave: releasePresence,\n backdrop,\n} = useModalOverlay(computed(() => props.modelValue), close, {\n panel: panelRef,\n closeOnEscape: () => props.closeOnEsc,\n closeOnBackdrop: () => props.closeOnBackdrop,\n // Окно фокусирует саму панель, когда внутри фокусировать нечего и когда\n // потребитель не указал цель явно.\n initialFocus: () => props.initialFocus ?? panelRef.value,\n})\n\nfunction onPanelAfterLeave(): void {\n releasePresence()\n emit('closed')\n}\n\nwatch(\n () => props.modelValue,\n (value) => {\n if (!value)\n return\n warnMissingAccessibleName()\n warnDuplicateTitle()\n },\n { immediate: true },\n)\n\ndefineExpose({ open, close, toggle })\n</script>\n\n<template>\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <div\n v-if=\"teleportEnabled && isMounted\"\n ref=\"rootEl\"\n data-gr-overlay-root\n v-bind=\"themeAttrs\"\n role=\"dialog\"\n aria-modal=\"true\"\n :class=\"rootClass\"\n :style=\"{ zIndex: layerZIndex }\"\n :inert=\"inertAttr\"\n :aria-labelledby=\"labelledBy\"\n :aria-describedby=\"describedBy\"\n :aria-label=\"ariaLabelAttr\"\n >\n <div :class=\"shellClass\">\n <div :class=\"layoutClass\">\n <Transition\n appear\n :enter-active-class=\"overlayTransition.enter\"\n :enter-from-class=\"overlayTransition.enterFrom\"\n :enter-to-class=\"overlayTransition.enterTo\"\n :leave-active-class=\"overlayTransition.leave\"\n :leave-from-class=\"overlayTransition.leaveFrom\"\n :leave-to-class=\"overlayTransition.leaveTo\"\n >\n <div\n v-if=\"isVisible\"\n data-gr-modal-overlay\n :class=\"overlayClass\"\n aria-hidden=\"true\"\n v-on=\"backdrop\"\n />\n </Transition>\n\n <!-- `opened`/`closed` вешаем на транзишн панели, а не на корневой:\n анимация живёт здесь, и только её конец что-то значит для\n потребителя, который ждёт момента для размонтирования. -->\n <Transition\n appear\n :enter-active-class=\"panelTransition.enter\"\n :enter-from-class=\"panelTransition.enterFrom\"\n :enter-to-class=\"panelTransition.enterTo\"\n :leave-active-class=\"panelTransition.leave\"\n :leave-from-class=\"panelTransition.leaveFrom\"\n :leave-to-class=\"panelTransition.leaveTo\"\n @after-enter=\"emit('opened')\"\n @after-leave=\"onPanelAfterLeave\"\n >\n <div\n v-if=\"isVisible\"\n ref=\"panelRef\"\n data-gr-modal-panel\n tabindex=\"-1\"\n :class=\"panelClass\"\n >\n <div v-if=\"$slots.title\" :id=\"titleId\" data-gr-modal-title>\n <slot name=\"title\" />\n </div>\n <div v-if=\"$slots.description\" :id=\"descriptionId\" data-gr-modal-description>\n <slot name=\"description\" />\n </div>\n\n <div v-if=\"$slots.header\" data-gr-modal-header :class=\"panelSectionClass\">\n <slot name=\"header\" />\n </div>\n\n <!-- При `inside` тело едет отдельно от шапки и подвала: иначе\n режим ничем не отличался бы от скролла всей панели. -->\n <div\n v-if=\"isBodyScrollable\"\n data-gr-modal-body\n tabindex=\"0\"\n :class=\"panelBodyScrollClass\"\n >\n <slot />\n </div>\n <slot v-else />\n\n <div v-if=\"$slots.footer\" data-gr-modal-footer :class=\"panelSectionClass\">\n <slot name=\"footer\" />\n </div>\n </div>\n </Transition>\n </div>\n </div>\n </div>\n </teleport>\n</template>\n"],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { t as GrModal_default } from "./chunks/GrModal-
|
|
1
|
+
import { t as GrModal_default } from "./chunks/GrModal-D0mf1SAx.js";
|
|
2
2
|
import { n as grModalSafelist, t as grModalConfig } from "../../chunks/config-mlheIMtV.js";
|
|
3
3
|
export { GrModal_default as GrModal, GrModal_default as default, grModalConfig, grModalSafelist };
|
|
@@ -3,7 +3,7 @@ import { vClickOutside } from "../../../directives/clickOutside.js";
|
|
|
3
3
|
import { createFloatingAnchor, useFloating } from "../../../composables/useFloating.js";
|
|
4
4
|
import { t as useControlledOpen } from "../../../chunks/useControlledOpen-DV3SqLTP.js";
|
|
5
5
|
import { t as panelPopTransition } from "../../../chunks/overlayTransition-0Fj5tQbq.js";
|
|
6
|
-
import { t as useModalOverlay } from "../../../chunks/useModalOverlay-
|
|
6
|
+
import { t as useModalOverlay } from "../../../chunks/useModalOverlay-D__CqLGn.js";
|
|
7
7
|
import { t as grPopoverPanelClass } from "../../../chunks/grPopoverStyles-Djh5RNhn.js";
|
|
8
8
|
import { Teleport, Transition, computed, createBlock, createCommentVNode, createElementBlock, createVNode, defineComponent, mergeProps, nextTick, openBlock, ref, renderSlot, unref, useId, useSlots, vShow, watch, withCtx, withDirectives } from "vue";
|
|
9
9
|
//#region src/components/GrPopover/GrPopover.vue?vue&type=script&setup=true&lang.ts
|
package/dist/components/GrPopover/chunks/{GrPopover-CofG-Tfe.js.map → GrPopover-faJanmGs.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GrPopover-CofG-Tfe.js","names":[],"sources":["../../../../src/components/GrPopover/GrPopover.vue","../../../../src/components/GrPopover/GrPopover.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * GrPopover — якорный немодальный оверлей с произвольным содержимым.\n *\n * Роль «универсального поповера» до сих пор насильно исполнял `GrDropdown`, но\n * он про меню: жёстко объявляет `role=\"menu\"`, `aria-haspopup=\"menu\"` и водит\n * фокус по пунктам. Для формы, подтверждения или палитры это неверная семантика.\n *\n * Здесь наоборот: позиционирование, слой и закрытие берёт на себя примитив, а\n * клавиатурный паттерн внутри панели остаётся за потребителем — поэтому `role`\n * задаётся пропом. На этом собираются `GrMenu` / `GrContextMenu` /\n * `GrColorPicker` / Popconfirm.\n *\n * A11y:\n * - триггер получает `aria-haspopup`/`aria-expanded`/`aria-controls` через\n * `triggerProps` (биндится на реальный фокусируемый элемент в слоте);\n * - панель — `role=\"dialog\"` по умолчанию и обязательное доступное имя\n * (`ariaLabel` либо `aria-labelledby` через `labelledBy`);\n * - Esc закрывает верхний слой стека, клик вне — закрывает по точке попадания;\n * - **по умолчанию фокус-ловушки нет намеренно**: Tab обязан уводить фокус\n * наружу, иначе немодальный слой запирает пользователя на странице, которая\n * не заблокирована. Проп `modal` включает второй режим — см. ниже.\n */\nimport { computed, nextTick, ref, useId, useSlots, watch } from 'vue'\n\nimport { useControlledOpen } from '../../composables/internal/useControlledOpen'\nimport { useModalOverlay } from '../../composables/internal/useModalOverlay'\nimport { createFloatingAnchor, type GrFloatingAnchorRect, useFloating, type UseFloatingPlacement } from '../../composables/useFloating'\nimport { vClickOutside } from '../../directives'\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport { panelPopTransition } from '../shared/overlayTransition'\nimport {\n type GrPopoverPadding,\n type GrPopoverRole,\n type GrPopoverSize,\n grPopoverPanelClass,\n} from './grPopoverStyles'\n\nexport interface GrPopoverProps {\n /**\n * Открыт ли поповер. Без этого пропа компонент ведёт состояние сам\n * (uncontrolled), с ним — слушайте `update:open`.\n */\n open?: boolean\n placement?: UseFloatingPlacement\n /** Зазор между триггером и панелью, px. */\n offsetPx?: number\n size?: GrPopoverSize\n /** Роль панели. Меняется теми, кто строит поверх примитива своё меню/список. */\n role?: GrPopoverRole\n /**\n * Модальный режим: фон уходит в `inert`, Tab ходит по кругу внутри панели,\n * скролл страницы блокируется, а слой встаёт в стек модальным — как окно.\n * Нужен поповеру с формой или подтверждением внутри: без изоляции фона\n * пользователь уводит фокус на страницу, к которой поповер и относится.\n *\n * В этом режиме фокус переносится в панель независимо от `autoFocus`: фон\n * недоступен, и слой без фокуса внутри стал бы клавиатурной ловушкой.\n */\n modal?: boolean\n /** Доступное имя панели. Обязательно для `role=\"dialog\"` без видимого заголовка. */\n ariaLabel?: string\n /** `id` видимого заголовка внутри панели — альтернатива `ariaLabel`. */\n labelledBy?: string\n /**\n * `manual` — открывать только программно (через `v-model:open`). Нужно для\n * контекстного меню и подтверждений, где момент открытия решает потребитель.\n */\n trigger?: 'click' | 'manual'\n closeOnEsc?: boolean\n closeOnClickOutside?: boolean\n /** Закрывать по клику внутри панели — удобно для меню, вредно для формы. */\n closeOnContentClick?: boolean\n /**\n * Переносить фокус на панель при открытии. Именно на панель, а не на первый\n * контрол внутри: сфокусировать поле ввода за пользователя — решение\n * содержимого, а не оболочки.\n */\n autoFocus?: boolean\n /**\n * Точечное переопределение точки монтирования. По умолчанию — общий портал\n * оверлеев (`#gr-portal` либо `portalTarget` из `GrConfigProvider`).\n */\n teleportTo?: string | HTMLElement\n contentClass?: string\n disabled?: boolean\n /**\n * Якорь-прямоугольник в координатах вьюпорта вместо обёртки слота `#trigger`:\n * панель встаёт у точки курсора или у строки списка. Нужен контекстному меню,\n * у которого триггера-элемента нет вовсе.\n *\n * `triggerProps` в этом режиме некому потребить, и своих ARIA-атрибутов\n * поповер никуда не вешает: `aria-haspopup` невалиден вне интерактивного\n * элемента, а `aria-expanded` без хозяина — шум. Связь с содержимым страницы\n * объявляет тот, кто открывает.\n */\n anchor?: GrFloatingAnchorRect | null\n /** Поле панели. `none` — содержимое рисует своё (меню, список опций). */\n padding?: GrPopoverPadding\n}\n\nexport interface GrPopoverEmits {\n (e: 'update:open', value: boolean): void\n}\n\nconst props = withDefaults(defineProps<GrPopoverProps>(), {\n open: undefined,\n placement: 'bottom-start',\n offsetPx: 8,\n size: undefined,\n role: 'dialog',\n modal: false,\n ariaLabel: undefined,\n labelledBy: undefined,\n trigger: 'click',\n closeOnEsc: true,\n closeOnClickOutside: true,\n closeOnContentClick: false,\n autoFocus: true,\n teleportTo: undefined,\n contentClass: undefined,\n disabled: false,\n anchor: undefined,\n padding: 'default',\n})\n\nconst emit = defineEmits<GrPopoverEmits>()\n\nconst slots = useSlots()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrPopover' })\n\nconst { open: isOpen, setOpen } = useControlledOpen(\n () => props.open,\n next => emit('update:open', next),\n)\n\nconst rootEl = ref<HTMLElement | null>(null)\nconst panelEl = ref<HTMLElement | null>(null)\nconst panelId = useId()\n\nconst clickOutsideExclude = [() => panelEl.value]\n\nfunction open(): void {\n if (props.disabled)\n return\n setOpen(true)\n}\n\nfunction close(): void {\n setOpen(false)\n}\n\nfunction toggle(): void {\n isOpen.value ? close() : open()\n}\n\n/**\n * Якорь создаётся один раз и читает координаты геттером: `useFloating`\n * пересоздаёт подписку при смене самой ссылки, а точка меняется на каждый\n * вызов — новый объект рвал бы `autoUpdate` без нужды.\n */\nconst anchorEl = createFloatingAnchor(() => props.anchor)\n\n/** Обёртка триггера существует только вместе со слотом — см. проп `anchor`. */\nconst hasTrigger = computed(() => Boolean(slots.trigger))\n\nconst { floatingStyle, resolvedPlacement, update: updateFloatingPosition } = useFloating(\n () => (props.anchor ? anchorEl : rootEl.value),\n panelEl,\n isOpen,\n {\n placement: () => props.placement,\n offsetPx: props.offsetPx,\n // Своего слоя у поповера нет намеренно: он и дропдаун — один класс якорных\n // немодальных оверлеев и обязаны лежать на одной высоте, иначе начнут\n // перекрывать друг друга в зависимости от порядка открытия.\n zIndexVar: '--gr-z-dropdown',\n },\n)\n\nwatch(() => props.placement, () => {\n if (isOpen.value)\n updateFloatingPosition()\n})\n\n// Объект якоря стабилен, поэтому смену координат `useFloating` сам не заметит:\n// повторный вызов у другой точки — это пересчёт позиции, а не переоткрытие.\nwatch(() => props.anchor, () => {\n if (isOpen.value)\n updateFloatingPosition()\n}, { deep: true })\n\n/**\n * Слой стека оверлеев: очередь Esc общая с модалками, поэтому поповер, открытый\n * внутри модалки, по Esc закрывает себя, а не её. Возврат фокуса на триггер\n * тоже отсюда — по правилу «только если на момент закрытия фокус ещё внутри».\n *\n * Модальность включается пропом и приходит той же сборкой, что у окна и drawer'а\n * (`useModalOverlay`): ловушка фокуса, `inert` фона, блокировка скролла. При\n * `modal: false` не включается ничего из этого — дефолтный поповер немодален.\n *\n * Корень слоя и панель здесь один элемент: якорный оверлей уезжает в портал сам,\n * без обёртки.\n */\nconst { inertAttr, portalTarget, teleportEnabled, themeAttrs } = useModalOverlay(isOpen, close, {\n modal: () => props.modal,\n closeOnEscape: () => props.closeOnEsc,\n panel: panelEl,\n root: panelEl,\n teleportTo: () => props.teleportTo,\n initialFocus: () => panelEl.value,\n})\n\n// Фокус переносим после отрисовки панели: до `nextTick` её ещё нет в DOM.\n// В модальном режиме перенос обязателен: фон в `inert`, и фокус, оставленный\n// снаружи, попал бы в недоступное поддерево.\nwatch(isOpen, async (next) => {\n if (!next || !(props.autoFocus || props.modal))\n return\n\n await nextTick()\n panelEl.value?.focus()\n})\n\n/** Роли, которые `aria-haspopup` действительно понимает. */\nconst HASPOPUP_ROLES = ['dialog', 'menu', 'listbox', 'grid'] as const\n\n/**\n * Пропсы для реального фокусируемого триггера: `<button v-bind=\"triggerProps\">`.\n * Клик вешается на обёртку, а ARIA — сюда, потому что `aria-expanded` обязан\n * жить на самом интерактивном элементе, а не на `div` вокруг него.\n */\nconst triggerProps = computed(() => ({\n // `aria-haspopup` принимает не любую роль: `group` в его списке нет, и\n // объявленный им триггер отдавал бы невалидный атрибут, который AT просто\n // отбрасывает. Роли вне списка сообщают о панели только через `aria-expanded`.\n 'aria-haspopup': HASPOPUP_ROLES.includes(props.role as (typeof HASPOPUP_ROLES)[number])\n ? (props.role as (typeof HASPOPUP_ROLES)[number])\n : undefined,\n 'aria-expanded': isOpen.value,\n 'aria-controls': isOpen.value ? panelId : undefined,\n 'disabled': props.disabled || undefined,\n}))\n\nfunction onTriggerClick(): void {\n if (props.trigger === 'click')\n toggle()\n}\n\nfunction onContentClick(): void {\n if (props.closeOnContentClick)\n close()\n}\n\nconst panelClasses = computed(() =>\n grPopoverPanelClass(resolvedSize.value, resolvedPlacement.value, props.contentClass, props.padding),\n)\n\ndefineExpose({ open, close, toggle })\n\ndefineSlots<{\n /**\n * Триггер панели. `triggerProps` обязаны попасть на сам интерактивный\n * элемент, а не на обёртку вокруг него: `aria-expanded` и `aria-controls`\n * читаются с того узла, который получает фокус.\n */\n trigger?: (props: {\n open: boolean\n toggle: () => void\n close: () => void\n triggerProps: Record<string, unknown>\n }) => any\n /** Содержимое панели. */\n content?: (props: { close: () => void }) => any\n}>()\n</script>\n\n<template>\n <div data-gr-popover>\n <div\n v-if=\"hasTrigger\"\n ref=\"rootEl\"\n v-click-outside=\"{ handler: close, enabled: isOpen && closeOnClickOutside, exclude: clickOutsideExclude }\"\n data-gr-popover-trigger\n class=\"inline-block max-w-full\"\n @click=\"onTriggerClick\"\n >\n <slot\n name=\"trigger\"\n :open=\"isOpen\"\n :toggle=\"toggle\"\n :close=\"close\"\n :trigger-props=\"triggerProps\"\n />\n </div>\n\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <transition\n :enter-active-class=\"panelPopTransition.enter\"\n :enter-from-class=\"panelPopTransition.enterFrom\"\n :enter-to-class=\"panelPopTransition.enterTo\"\n :leave-active-class=\"panelPopTransition.leave\"\n :leave-from-class=\"panelPopTransition.leaveFrom\"\n :leave-to-class=\"panelPopTransition.leaveTo\"\n >\n <div\n v-show=\"isOpen\"\n :id=\"panelId\"\n ref=\"panelEl\"\n v-click-outside=\"{ handler: close, enabled: isOpen && closeOnClickOutside && !hasTrigger }\"\n v-bind=\"themeAttrs\"\n data-gr-popover-panel\n data-gr-overlay-root\n :role=\"role === 'none' ? undefined : role\"\n :aria-modal=\"modal ? 'true' : undefined\"\n :inert=\"inertAttr\"\n :aria-label=\"labelledBy ? undefined : ariaLabel\"\n :aria-labelledby=\"labelledBy\"\n tabindex=\"-1\"\n :class=\"panelClasses\"\n :style=\"floatingStyle\"\n @click=\"onContentClick\"\n >\n <slot name=\"content\" :close=\"close\" />\n </div>\n </transition>\n </teleport>\n </div>\n</template>\n","<script setup lang=\"ts\">\n/**\n * GrPopover — якорный немодальный оверлей с произвольным содержимым.\n *\n * Роль «универсального поповера» до сих пор насильно исполнял `GrDropdown`, но\n * он про меню: жёстко объявляет `role=\"menu\"`, `aria-haspopup=\"menu\"` и водит\n * фокус по пунктам. Для формы, подтверждения или палитры это неверная семантика.\n *\n * Здесь наоборот: позиционирование, слой и закрытие берёт на себя примитив, а\n * клавиатурный паттерн внутри панели остаётся за потребителем — поэтому `role`\n * задаётся пропом. На этом собираются `GrMenu` / `GrContextMenu` /\n * `GrColorPicker` / Popconfirm.\n *\n * A11y:\n * - триггер получает `aria-haspopup`/`aria-expanded`/`aria-controls` через\n * `triggerProps` (биндится на реальный фокусируемый элемент в слоте);\n * - панель — `role=\"dialog\"` по умолчанию и обязательное доступное имя\n * (`ariaLabel` либо `aria-labelledby` через `labelledBy`);\n * - Esc закрывает верхний слой стека, клик вне — закрывает по точке попадания;\n * - **по умолчанию фокус-ловушки нет намеренно**: Tab обязан уводить фокус\n * наружу, иначе немодальный слой запирает пользователя на странице, которая\n * не заблокирована. Проп `modal` включает второй режим — см. ниже.\n */\nimport { computed, nextTick, ref, useId, useSlots, watch } from 'vue'\n\nimport { useControlledOpen } from '../../composables/internal/useControlledOpen'\nimport { useModalOverlay } from '../../composables/internal/useModalOverlay'\nimport { createFloatingAnchor, type GrFloatingAnchorRect, useFloating, type UseFloatingPlacement } from '../../composables/useFloating'\nimport { vClickOutside } from '../../directives'\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport { panelPopTransition } from '../shared/overlayTransition'\nimport {\n type GrPopoverPadding,\n type GrPopoverRole,\n type GrPopoverSize,\n grPopoverPanelClass,\n} from './grPopoverStyles'\n\nexport interface GrPopoverProps {\n /**\n * Открыт ли поповер. Без этого пропа компонент ведёт состояние сам\n * (uncontrolled), с ним — слушайте `update:open`.\n */\n open?: boolean\n placement?: UseFloatingPlacement\n /** Зазор между триггером и панелью, px. */\n offsetPx?: number\n size?: GrPopoverSize\n /** Роль панели. Меняется теми, кто строит поверх примитива своё меню/список. */\n role?: GrPopoverRole\n /**\n * Модальный режим: фон уходит в `inert`, Tab ходит по кругу внутри панели,\n * скролл страницы блокируется, а слой встаёт в стек модальным — как окно.\n * Нужен поповеру с формой или подтверждением внутри: без изоляции фона\n * пользователь уводит фокус на страницу, к которой поповер и относится.\n *\n * В этом режиме фокус переносится в панель независимо от `autoFocus`: фон\n * недоступен, и слой без фокуса внутри стал бы клавиатурной ловушкой.\n */\n modal?: boolean\n /** Доступное имя панели. Обязательно для `role=\"dialog\"` без видимого заголовка. */\n ariaLabel?: string\n /** `id` видимого заголовка внутри панели — альтернатива `ariaLabel`. */\n labelledBy?: string\n /**\n * `manual` — открывать только программно (через `v-model:open`). Нужно для\n * контекстного меню и подтверждений, где момент открытия решает потребитель.\n */\n trigger?: 'click' | 'manual'\n closeOnEsc?: boolean\n closeOnClickOutside?: boolean\n /** Закрывать по клику внутри панели — удобно для меню, вредно для формы. */\n closeOnContentClick?: boolean\n /**\n * Переносить фокус на панель при открытии. Именно на панель, а не на первый\n * контрол внутри: сфокусировать поле ввода за пользователя — решение\n * содержимого, а не оболочки.\n */\n autoFocus?: boolean\n /**\n * Точечное переопределение точки монтирования. По умолчанию — общий портал\n * оверлеев (`#gr-portal` либо `portalTarget` из `GrConfigProvider`).\n */\n teleportTo?: string | HTMLElement\n contentClass?: string\n disabled?: boolean\n /**\n * Якорь-прямоугольник в координатах вьюпорта вместо обёртки слота `#trigger`:\n * панель встаёт у точки курсора или у строки списка. Нужен контекстному меню,\n * у которого триггера-элемента нет вовсе.\n *\n * `triggerProps` в этом режиме некому потребить, и своих ARIA-атрибутов\n * поповер никуда не вешает: `aria-haspopup` невалиден вне интерактивного\n * элемента, а `aria-expanded` без хозяина — шум. Связь с содержимым страницы\n * объявляет тот, кто открывает.\n */\n anchor?: GrFloatingAnchorRect | null\n /** Поле панели. `none` — содержимое рисует своё (меню, список опций). */\n padding?: GrPopoverPadding\n}\n\nexport interface GrPopoverEmits {\n (e: 'update:open', value: boolean): void\n}\n\nconst props = withDefaults(defineProps<GrPopoverProps>(), {\n open: undefined,\n placement: 'bottom-start',\n offsetPx: 8,\n size: undefined,\n role: 'dialog',\n modal: false,\n ariaLabel: undefined,\n labelledBy: undefined,\n trigger: 'click',\n closeOnEsc: true,\n closeOnClickOutside: true,\n closeOnContentClick: false,\n autoFocus: true,\n teleportTo: undefined,\n contentClass: undefined,\n disabled: false,\n anchor: undefined,\n padding: 'default',\n})\n\nconst emit = defineEmits<GrPopoverEmits>()\n\nconst slots = useSlots()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrPopover' })\n\nconst { open: isOpen, setOpen } = useControlledOpen(\n () => props.open,\n next => emit('update:open', next),\n)\n\nconst rootEl = ref<HTMLElement | null>(null)\nconst panelEl = ref<HTMLElement | null>(null)\nconst panelId = useId()\n\nconst clickOutsideExclude = [() => panelEl.value]\n\nfunction open(): void {\n if (props.disabled)\n return\n setOpen(true)\n}\n\nfunction close(): void {\n setOpen(false)\n}\n\nfunction toggle(): void {\n isOpen.value ? close() : open()\n}\n\n/**\n * Якорь создаётся один раз и читает координаты геттером: `useFloating`\n * пересоздаёт подписку при смене самой ссылки, а точка меняется на каждый\n * вызов — новый объект рвал бы `autoUpdate` без нужды.\n */\nconst anchorEl = createFloatingAnchor(() => props.anchor)\n\n/** Обёртка триггера существует только вместе со слотом — см. проп `anchor`. */\nconst hasTrigger = computed(() => Boolean(slots.trigger))\n\nconst { floatingStyle, resolvedPlacement, update: updateFloatingPosition } = useFloating(\n () => (props.anchor ? anchorEl : rootEl.value),\n panelEl,\n isOpen,\n {\n placement: () => props.placement,\n offsetPx: props.offsetPx,\n // Своего слоя у поповера нет намеренно: он и дропдаун — один класс якорных\n // немодальных оверлеев и обязаны лежать на одной высоте, иначе начнут\n // перекрывать друг друга в зависимости от порядка открытия.\n zIndexVar: '--gr-z-dropdown',\n },\n)\n\nwatch(() => props.placement, () => {\n if (isOpen.value)\n updateFloatingPosition()\n})\n\n// Объект якоря стабилен, поэтому смену координат `useFloating` сам не заметит:\n// повторный вызов у другой точки — это пересчёт позиции, а не переоткрытие.\nwatch(() => props.anchor, () => {\n if (isOpen.value)\n updateFloatingPosition()\n}, { deep: true })\n\n/**\n * Слой стека оверлеев: очередь Esc общая с модалками, поэтому поповер, открытый\n * внутри модалки, по Esc закрывает себя, а не её. Возврат фокуса на триггер\n * тоже отсюда — по правилу «только если на момент закрытия фокус ещё внутри».\n *\n * Модальность включается пропом и приходит той же сборкой, что у окна и drawer'а\n * (`useModalOverlay`): ловушка фокуса, `inert` фона, блокировка скролла. При\n * `modal: false` не включается ничего из этого — дефолтный поповер немодален.\n *\n * Корень слоя и панель здесь один элемент: якорный оверлей уезжает в портал сам,\n * без обёртки.\n */\nconst { inertAttr, portalTarget, teleportEnabled, themeAttrs } = useModalOverlay(isOpen, close, {\n modal: () => props.modal,\n closeOnEscape: () => props.closeOnEsc,\n panel: panelEl,\n root: panelEl,\n teleportTo: () => props.teleportTo,\n initialFocus: () => panelEl.value,\n})\n\n// Фокус переносим после отрисовки панели: до `nextTick` её ещё нет в DOM.\n// В модальном режиме перенос обязателен: фон в `inert`, и фокус, оставленный\n// снаружи, попал бы в недоступное поддерево.\nwatch(isOpen, async (next) => {\n if (!next || !(props.autoFocus || props.modal))\n return\n\n await nextTick()\n panelEl.value?.focus()\n})\n\n/** Роли, которые `aria-haspopup` действительно понимает. */\nconst HASPOPUP_ROLES = ['dialog', 'menu', 'listbox', 'grid'] as const\n\n/**\n * Пропсы для реального фокусируемого триггера: `<button v-bind=\"triggerProps\">`.\n * Клик вешается на обёртку, а ARIA — сюда, потому что `aria-expanded` обязан\n * жить на самом интерактивном элементе, а не на `div` вокруг него.\n */\nconst triggerProps = computed(() => ({\n // `aria-haspopup` принимает не любую роль: `group` в его списке нет, и\n // объявленный им триггер отдавал бы невалидный атрибут, который AT просто\n // отбрасывает. Роли вне списка сообщают о панели только через `aria-expanded`.\n 'aria-haspopup': HASPOPUP_ROLES.includes(props.role as (typeof HASPOPUP_ROLES)[number])\n ? (props.role as (typeof HASPOPUP_ROLES)[number])\n : undefined,\n 'aria-expanded': isOpen.value,\n 'aria-controls': isOpen.value ? panelId : undefined,\n 'disabled': props.disabled || undefined,\n}))\n\nfunction onTriggerClick(): void {\n if (props.trigger === 'click')\n toggle()\n}\n\nfunction onContentClick(): void {\n if (props.closeOnContentClick)\n close()\n}\n\nconst panelClasses = computed(() =>\n grPopoverPanelClass(resolvedSize.value, resolvedPlacement.value, props.contentClass, props.padding),\n)\n\ndefineExpose({ open, close, toggle })\n\ndefineSlots<{\n /**\n * Триггер панели. `triggerProps` обязаны попасть на сам интерактивный\n * элемент, а не на обёртку вокруг него: `aria-expanded` и `aria-controls`\n * читаются с того узла, который получает фокус.\n */\n trigger?: (props: {\n open: boolean\n toggle: () => void\n close: () => void\n triggerProps: Record<string, unknown>\n }) => any\n /** Содержимое панели. */\n content?: (props: { close: () => void }) => any\n}>()\n</script>\n\n<template>\n <div data-gr-popover>\n <div\n v-if=\"hasTrigger\"\n ref=\"rootEl\"\n v-click-outside=\"{ handler: close, enabled: isOpen && closeOnClickOutside, exclude: clickOutsideExclude }\"\n data-gr-popover-trigger\n class=\"inline-block max-w-full\"\n @click=\"onTriggerClick\"\n >\n <slot\n name=\"trigger\"\n :open=\"isOpen\"\n :toggle=\"toggle\"\n :close=\"close\"\n :trigger-props=\"triggerProps\"\n />\n </div>\n\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <transition\n :enter-active-class=\"panelPopTransition.enter\"\n :enter-from-class=\"panelPopTransition.enterFrom\"\n :enter-to-class=\"panelPopTransition.enterTo\"\n :leave-active-class=\"panelPopTransition.leave\"\n :leave-from-class=\"panelPopTransition.leaveFrom\"\n :leave-to-class=\"panelPopTransition.leaveTo\"\n >\n <div\n v-show=\"isOpen\"\n :id=\"panelId\"\n ref=\"panelEl\"\n v-click-outside=\"{ handler: close, enabled: isOpen && closeOnClickOutside && !hasTrigger }\"\n v-bind=\"themeAttrs\"\n data-gr-popover-panel\n data-gr-overlay-root\n :role=\"role === 'none' ? undefined : role\"\n :aria-modal=\"modal ? 'true' : undefined\"\n :inert=\"inertAttr\"\n :aria-label=\"labelledBy ? undefined : ariaLabel\"\n :aria-labelledby=\"labelledBy\"\n tabindex=\"-1\"\n :class=\"panelClasses\"\n :style=\"floatingStyle\"\n @click=\"onContentClick\"\n >\n <slot name=\"content\" :close=\"close\" />\n </div>\n </transition>\n </teleport>\n </div>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyGA,MAAM,QAAQ;EAqBd,MAAM,OAAO;EAEb,MAAM,QAAQ,SAAS;EAEvB,MAAM,eAAe,yBAAyB,MAAM,MAAM,EAAE,WAAW,YAAY,CAAC;EAEpF,MAAM,EAAE,MAAM,QAAQ,YAAY,wBAC1B,MAAM,OACZ,SAAQ,KAAK,eAAe,IAAI,CAClC;EAEA,MAAM,SAAS,IAAwB,IAAI;EAC3C,MAAM,UAAU,IAAwB,IAAI;EAC5C,MAAM,UAAU,MAAM;EAEtB,MAAM,sBAAsB,OAAO,QAAQ,KAAK;EAEhD,SAAS,OAAa;GACpB,IAAI,MAAM,UACR;GACF,QAAQ,IAAI;EACd;EAEA,SAAS,QAAc;GACrB,QAAQ,KAAK;EACf;EAEA,SAAS,SAAe;GACtB,OAAO,QAAQ,MAAM,IAAI,KAAK;EAChC;;;;;;EAOA,MAAM,WAAW,2BAA2B,MAAM,MAAM;;EAGxD,MAAM,aAAa,eAAe,QAAQ,MAAM,OAAO,CAAC;EAExD,MAAM,EAAE,eAAe,mBAAmB,QAAQ,2BAA2B,kBACpE,MAAM,SAAS,WAAW,OAAO,OACxC,SACA,QACA;GACE,iBAAiB,MAAM;GACvB,UAAU,MAAM;GAIhB,WAAW;EACb,CACF;EAEA,YAAY,MAAM,iBAAiB;GACjC,IAAI,OAAO,OACT,uBAAuB;EAC3B,CAAC;EAID,YAAY,MAAM,cAAc;GAC9B,IAAI,OAAO,OACT,uBAAuB;EAC3B,GAAG,EAAE,MAAM,KAAK,CAAC;;;;;;;;;;;;;EAcjB,MAAM,EAAE,WAAW,cAAc,iBAAiB,eAAe,gBAAgB,QAAQ,OAAO;GAC9F,aAAa,MAAM;GACnB,qBAAqB,MAAM;GAC3B,OAAO;GACP,MAAM;GACN,kBAAkB,MAAM;GACxB,oBAAoB,QAAQ;EAC9B,CAAC;EAKD,MAAM,QAAQ,OAAO,SAAS;GAC5B,IAAI,CAAC,QAAQ,EAAE,MAAM,aAAa,MAAM,QACtC;GAEF,MAAM,SAAS;GACf,QAAQ,OAAO,MAAM;EACvB,CAAC;;EAGD,MAAM,iBAAiB;GAAC;GAAU;GAAQ;GAAW;EAAM;;;;;;EAO3D,MAAM,eAAe,gBAAgB;GAInC,iBAAiB,eAAe,SAAS,MAAM,IAAuC,IACjF,MAAM,OACP,KAAA;GACJ,iBAAiB,OAAO;GACxB,iBAAiB,OAAO,QAAQ,UAAU,KAAA;GAC1C,YAAY,MAAM,YAAY,KAAA;EAChC,EAAE;EAEF,SAAS,iBAAuB;GAC9B,IAAI,MAAM,YAAY,SACpB,OAAO;EACX;EAEA,SAAS,iBAAuB;GAC9B,IAAI,MAAM,qBACR,MAAM;EACV;EAEA,MAAM,eAAe,eACnB,oBAAoB,aAAa,OAAO,kBAAkB,OAAO,MAAM,cAAc,MAAM,OAAO,CACpG;EAEA,SAAa;GAAE;GAAM;GAAO;EAAO,CAAC;;uBAoBlC,mBAiDM,OAjDN,YAiDM,CA/CI,WAAA,QAAA,gBAAA,UAAA,GADR,mBAeM,OAAA;;aAbA;IAAJ,KAAI;IAEJ,2BAAA;IACA,OAAM;IACL,SAAO;OAER,WAME,KAAA,QAAA,WAAA;IAJC,MAAM,MAAA,MAAA;IACE;IACD;IACP,cAAe,aAAA;;aAVU;IAAK,SAAW,MAAA,MAAA,KAAU,QAAA;IAAmB,SAAW;GAAmB,CAAA,CAAA,CAAA,IAAA,mBAAA,IAAA,IAAA,IAAA,UAAA,GAczG,YA8BW,UAAA;IA9BA,IAAI,MAAA,YAAA;IAAe,UAAQ,CAAG,MAAA,eAAA;OACvC,YA4Ba,YAAA;IA3BV,sBAAoB,MAAA,kBAAA,CAAkB,CAAC;IACvC,oBAAkB,MAAA,kBAAA,CAAkB,CAAC;IACrC,kBAAgB,MAAA,kBAAA,CAAkB,CAAC;IACnC,sBAAoB,MAAA,kBAAA,CAAkB,CAAC;IACvC,oBAAkB,MAAA,kBAAA,CAAkB,CAAC;IACrC,kBAAgB,MAAA,kBAAA,CAAkB,CAAC;;2BAqB9B,CAAA,gBAAA,UAAA,GAnBN,mBAmBM,OAnBN,WAmBM;KAjBH,IAAI,MAAA,OAAA;cACD;KAAJ,KAAI;OAEI,MAAA,UAAA,GAAU;KAClB,yBAAA;KACA,wBAAA;KACC,MAAM,QAAA,SAAI,SAAc,KAAA,IAAY,QAAA;KACpC,cAAY,QAAA,QAAK,SAAY,KAAA;KAC7B,OAAO,MAAA,SAAA;KACP,cAAY,QAAA,aAAa,KAAA,IAAY,QAAA;KACrC,mBAAiB,QAAA;KAClB,UAAS;KACR,OAAO,aAAA;KACP,OAAO,MAAA,aAAA;KACP,SAAO;SAER,WAAsC,KAAA,QAAA,WAAA,EAAT,MAAK,CAAA,CAAA,GAAA,IAAA,UAAA,IAAA,CAAA,CAAA,OAjB1B,MAAA,MAAA,CAAM,GAAA,CAAA,MAAA,aAAA,GAAA;cAGc;KAAK,SAAW,MAAA,MAAA,KAAU,QAAA,uBAAmB,CAAK,WAAA;IAAU,CAAA,CAAA,CAAA,CAAA,CAAA"}
|
|
1
|
+
{"version":3,"file":"GrPopover-faJanmGs.js","names":[],"sources":["../../../../src/components/GrPopover/GrPopover.vue","../../../../src/components/GrPopover/GrPopover.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * GrPopover — якорный немодальный оверлей с произвольным содержимым.\n *\n * Роль «универсального поповера» до сих пор насильно исполнял `GrDropdown`, но\n * он про меню: жёстко объявляет `role=\"menu\"`, `aria-haspopup=\"menu\"` и водит\n * фокус по пунктам. Для формы, подтверждения или палитры это неверная семантика.\n *\n * Здесь наоборот: позиционирование, слой и закрытие берёт на себя примитив, а\n * клавиатурный паттерн внутри панели остаётся за потребителем — поэтому `role`\n * задаётся пропом. На этом собираются `GrMenu` / `GrContextMenu` /\n * `GrColorPicker` / Popconfirm.\n *\n * A11y:\n * - триггер получает `aria-haspopup`/`aria-expanded`/`aria-controls` через\n * `triggerProps` (биндится на реальный фокусируемый элемент в слоте);\n * - панель — `role=\"dialog\"` по умолчанию и обязательное доступное имя\n * (`ariaLabel` либо `aria-labelledby` через `labelledBy`);\n * - Esc закрывает верхний слой стека, клик вне — закрывает по точке попадания;\n * - **по умолчанию фокус-ловушки нет намеренно**: Tab обязан уводить фокус\n * наружу, иначе немодальный слой запирает пользователя на странице, которая\n * не заблокирована. Проп `modal` включает второй режим — см. ниже.\n */\nimport { computed, nextTick, ref, useId, useSlots, watch } from 'vue'\n\nimport { useControlledOpen } from '../../composables/internal/useControlledOpen'\nimport { useModalOverlay } from '../../composables/internal/useModalOverlay'\nimport { createFloatingAnchor, type GrFloatingAnchorRect, useFloating, type UseFloatingPlacement } from '../../composables/useFloating'\nimport { vClickOutside } from '../../directives'\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport { panelPopTransition } from '../shared/overlayTransition'\nimport {\n type GrPopoverPadding,\n type GrPopoverRole,\n type GrPopoverSize,\n grPopoverPanelClass,\n} from './grPopoverStyles'\n\nexport interface GrPopoverProps {\n /**\n * Открыт ли поповер. Без этого пропа компонент ведёт состояние сам\n * (uncontrolled), с ним — слушайте `update:open`.\n */\n open?: boolean\n placement?: UseFloatingPlacement\n /** Зазор между триггером и панелью, px. */\n offsetPx?: number\n size?: GrPopoverSize\n /** Роль панели. Меняется теми, кто строит поверх примитива своё меню/список. */\n role?: GrPopoverRole\n /**\n * Модальный режим: фон уходит в `inert`, Tab ходит по кругу внутри панели,\n * скролл страницы блокируется, а слой встаёт в стек модальным — как окно.\n * Нужен поповеру с формой или подтверждением внутри: без изоляции фона\n * пользователь уводит фокус на страницу, к которой поповер и относится.\n *\n * В этом режиме фокус переносится в панель независимо от `autoFocus`: фон\n * недоступен, и слой без фокуса внутри стал бы клавиатурной ловушкой.\n */\n modal?: boolean\n /** Доступное имя панели. Обязательно для `role=\"dialog\"` без видимого заголовка. */\n ariaLabel?: string\n /** `id` видимого заголовка внутри панели — альтернатива `ariaLabel`. */\n labelledBy?: string\n /**\n * `manual` — открывать только программно (через `v-model:open`). Нужно для\n * контекстного меню и подтверждений, где момент открытия решает потребитель.\n */\n trigger?: 'click' | 'manual'\n closeOnEsc?: boolean\n closeOnClickOutside?: boolean\n /** Закрывать по клику внутри панели — удобно для меню, вредно для формы. */\n closeOnContentClick?: boolean\n /**\n * Переносить фокус на панель при открытии. Именно на панель, а не на первый\n * контрол внутри: сфокусировать поле ввода за пользователя — решение\n * содержимого, а не оболочки.\n */\n autoFocus?: boolean\n /**\n * Точечное переопределение точки монтирования. По умолчанию — общий портал\n * оверлеев (`#gr-portal` либо `portalTarget` из `GrConfigProvider`).\n */\n teleportTo?: string | HTMLElement\n contentClass?: string\n disabled?: boolean\n /**\n * Якорь-прямоугольник в координатах вьюпорта вместо обёртки слота `#trigger`:\n * панель встаёт у точки курсора или у строки списка. Нужен контекстному меню,\n * у которого триггера-элемента нет вовсе.\n *\n * `triggerProps` в этом режиме некому потребить, и своих ARIA-атрибутов\n * поповер никуда не вешает: `aria-haspopup` невалиден вне интерактивного\n * элемента, а `aria-expanded` без хозяина — шум. Связь с содержимым страницы\n * объявляет тот, кто открывает.\n */\n anchor?: GrFloatingAnchorRect | null\n /** Поле панели. `none` — содержимое рисует своё (меню, список опций). */\n padding?: GrPopoverPadding\n}\n\nexport interface GrPopoverEmits {\n (e: 'update:open', value: boolean): void\n}\n\nconst props = withDefaults(defineProps<GrPopoverProps>(), {\n open: undefined,\n placement: 'bottom-start',\n offsetPx: 8,\n size: undefined,\n role: 'dialog',\n modal: false,\n ariaLabel: undefined,\n labelledBy: undefined,\n trigger: 'click',\n closeOnEsc: true,\n closeOnClickOutside: true,\n closeOnContentClick: false,\n autoFocus: true,\n teleportTo: undefined,\n contentClass: undefined,\n disabled: false,\n anchor: undefined,\n padding: 'default',\n})\n\nconst emit = defineEmits<GrPopoverEmits>()\n\nconst slots = useSlots()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrPopover' })\n\nconst { open: isOpen, setOpen } = useControlledOpen(\n () => props.open,\n next => emit('update:open', next),\n)\n\nconst rootEl = ref<HTMLElement | null>(null)\nconst panelEl = ref<HTMLElement | null>(null)\nconst panelId = useId()\n\nconst clickOutsideExclude = [() => panelEl.value]\n\nfunction open(): void {\n if (props.disabled)\n return\n setOpen(true)\n}\n\nfunction close(): void {\n setOpen(false)\n}\n\nfunction toggle(): void {\n isOpen.value ? close() : open()\n}\n\n/**\n * Якорь создаётся один раз и читает координаты геттером: `useFloating`\n * пересоздаёт подписку при смене самой ссылки, а точка меняется на каждый\n * вызов — новый объект рвал бы `autoUpdate` без нужды.\n */\nconst anchorEl = createFloatingAnchor(() => props.anchor)\n\n/** Обёртка триггера существует только вместе со слотом — см. проп `anchor`. */\nconst hasTrigger = computed(() => Boolean(slots.trigger))\n\nconst { floatingStyle, resolvedPlacement, update: updateFloatingPosition } = useFloating(\n () => (props.anchor ? anchorEl : rootEl.value),\n panelEl,\n isOpen,\n {\n placement: () => props.placement,\n offsetPx: props.offsetPx,\n // Своего слоя у поповера нет намеренно: он и дропдаун — один класс якорных\n // немодальных оверлеев и обязаны лежать на одной высоте, иначе начнут\n // перекрывать друг друга в зависимости от порядка открытия.\n zIndexVar: '--gr-z-dropdown',\n },\n)\n\nwatch(() => props.placement, () => {\n if (isOpen.value)\n updateFloatingPosition()\n})\n\n// Объект якоря стабилен, поэтому смену координат `useFloating` сам не заметит:\n// повторный вызов у другой точки — это пересчёт позиции, а не переоткрытие.\nwatch(() => props.anchor, () => {\n if (isOpen.value)\n updateFloatingPosition()\n}, { deep: true })\n\n/**\n * Слой стека оверлеев: очередь Esc общая с модалками, поэтому поповер, открытый\n * внутри модалки, по Esc закрывает себя, а не её. Возврат фокуса на триггер\n * тоже отсюда — по правилу «только если на момент закрытия фокус ещё внутри».\n *\n * Модальность включается пропом и приходит той же сборкой, что у окна и drawer'а\n * (`useModalOverlay`): ловушка фокуса, `inert` фона, блокировка скролла. При\n * `modal: false` не включается ничего из этого — дефолтный поповер немодален.\n *\n * Корень слоя и панель здесь один элемент: якорный оверлей уезжает в портал сам,\n * без обёртки.\n */\nconst { inertAttr, portalTarget, teleportEnabled, themeAttrs } = useModalOverlay(isOpen, close, {\n modal: () => props.modal,\n closeOnEscape: () => props.closeOnEsc,\n panel: panelEl,\n root: panelEl,\n teleportTo: () => props.teleportTo,\n initialFocus: () => panelEl.value,\n})\n\n// Фокус переносим после отрисовки панели: до `nextTick` её ещё нет в DOM.\n// В модальном режиме перенос обязателен: фон в `inert`, и фокус, оставленный\n// снаружи, попал бы в недоступное поддерево.\nwatch(isOpen, async (next) => {\n if (!next || !(props.autoFocus || props.modal))\n return\n\n await nextTick()\n panelEl.value?.focus()\n})\n\n/** Роли, которые `aria-haspopup` действительно понимает. */\nconst HASPOPUP_ROLES = ['dialog', 'menu', 'listbox', 'grid'] as const\n\n/**\n * Пропсы для реального фокусируемого триггера: `<button v-bind=\"triggerProps\">`.\n * Клик вешается на обёртку, а ARIA — сюда, потому что `aria-expanded` обязан\n * жить на самом интерактивном элементе, а не на `div` вокруг него.\n */\nconst triggerProps = computed(() => ({\n // `aria-haspopup` принимает не любую роль: `group` в его списке нет, и\n // объявленный им триггер отдавал бы невалидный атрибут, который AT просто\n // отбрасывает. Роли вне списка сообщают о панели только через `aria-expanded`.\n 'aria-haspopup': HASPOPUP_ROLES.includes(props.role as (typeof HASPOPUP_ROLES)[number])\n ? (props.role as (typeof HASPOPUP_ROLES)[number])\n : undefined,\n 'aria-expanded': isOpen.value,\n 'aria-controls': isOpen.value ? panelId : undefined,\n 'disabled': props.disabled || undefined,\n}))\n\nfunction onTriggerClick(): void {\n if (props.trigger === 'click')\n toggle()\n}\n\nfunction onContentClick(): void {\n if (props.closeOnContentClick)\n close()\n}\n\nconst panelClasses = computed(() =>\n grPopoverPanelClass(resolvedSize.value, resolvedPlacement.value, props.contentClass, props.padding),\n)\n\ndefineExpose({ open, close, toggle })\n\ndefineSlots<{\n /**\n * Триггер панели. `triggerProps` обязаны попасть на сам интерактивный\n * элемент, а не на обёртку вокруг него: `aria-expanded` и `aria-controls`\n * читаются с того узла, который получает фокус.\n */\n trigger?: (props: {\n open: boolean\n toggle: () => void\n close: () => void\n triggerProps: Record<string, unknown>\n }) => any\n /** Содержимое панели. */\n content?: (props: { close: () => void }) => any\n}>()\n</script>\n\n<template>\n <div data-gr-popover>\n <div\n v-if=\"hasTrigger\"\n ref=\"rootEl\"\n v-click-outside=\"{ handler: close, enabled: isOpen && closeOnClickOutside, exclude: clickOutsideExclude }\"\n data-gr-popover-trigger\n class=\"inline-block max-w-full\"\n @click=\"onTriggerClick\"\n >\n <slot\n name=\"trigger\"\n :open=\"isOpen\"\n :toggle=\"toggle\"\n :close=\"close\"\n :trigger-props=\"triggerProps\"\n />\n </div>\n\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <transition\n :enter-active-class=\"panelPopTransition.enter\"\n :enter-from-class=\"panelPopTransition.enterFrom\"\n :enter-to-class=\"panelPopTransition.enterTo\"\n :leave-active-class=\"panelPopTransition.leave\"\n :leave-from-class=\"panelPopTransition.leaveFrom\"\n :leave-to-class=\"panelPopTransition.leaveTo\"\n >\n <div\n v-show=\"isOpen\"\n :id=\"panelId\"\n ref=\"panelEl\"\n v-click-outside=\"{ handler: close, enabled: isOpen && closeOnClickOutside && !hasTrigger }\"\n v-bind=\"themeAttrs\"\n data-gr-popover-panel\n data-gr-overlay-root\n :role=\"role === 'none' ? undefined : role\"\n :aria-modal=\"modal ? 'true' : undefined\"\n :inert=\"inertAttr\"\n :aria-label=\"labelledBy ? undefined : ariaLabel\"\n :aria-labelledby=\"labelledBy\"\n tabindex=\"-1\"\n :class=\"panelClasses\"\n :style=\"floatingStyle\"\n @click=\"onContentClick\"\n >\n <slot name=\"content\" :close=\"close\" />\n </div>\n </transition>\n </teleport>\n </div>\n</template>\n","<script setup lang=\"ts\">\n/**\n * GrPopover — якорный немодальный оверлей с произвольным содержимым.\n *\n * Роль «универсального поповера» до сих пор насильно исполнял `GrDropdown`, но\n * он про меню: жёстко объявляет `role=\"menu\"`, `aria-haspopup=\"menu\"` и водит\n * фокус по пунктам. Для формы, подтверждения или палитры это неверная семантика.\n *\n * Здесь наоборот: позиционирование, слой и закрытие берёт на себя примитив, а\n * клавиатурный паттерн внутри панели остаётся за потребителем — поэтому `role`\n * задаётся пропом. На этом собираются `GrMenu` / `GrContextMenu` /\n * `GrColorPicker` / Popconfirm.\n *\n * A11y:\n * - триггер получает `aria-haspopup`/`aria-expanded`/`aria-controls` через\n * `triggerProps` (биндится на реальный фокусируемый элемент в слоте);\n * - панель — `role=\"dialog\"` по умолчанию и обязательное доступное имя\n * (`ariaLabel` либо `aria-labelledby` через `labelledBy`);\n * - Esc закрывает верхний слой стека, клик вне — закрывает по точке попадания;\n * - **по умолчанию фокус-ловушки нет намеренно**: Tab обязан уводить фокус\n * наружу, иначе немодальный слой запирает пользователя на странице, которая\n * не заблокирована. Проп `modal` включает второй режим — см. ниже.\n */\nimport { computed, nextTick, ref, useId, useSlots, watch } from 'vue'\n\nimport { useControlledOpen } from '../../composables/internal/useControlledOpen'\nimport { useModalOverlay } from '../../composables/internal/useModalOverlay'\nimport { createFloatingAnchor, type GrFloatingAnchorRect, useFloating, type UseFloatingPlacement } from '../../composables/useFloating'\nimport { vClickOutside } from '../../directives'\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport { panelPopTransition } from '../shared/overlayTransition'\nimport {\n type GrPopoverPadding,\n type GrPopoverRole,\n type GrPopoverSize,\n grPopoverPanelClass,\n} from './grPopoverStyles'\n\nexport interface GrPopoverProps {\n /**\n * Открыт ли поповер. Без этого пропа компонент ведёт состояние сам\n * (uncontrolled), с ним — слушайте `update:open`.\n */\n open?: boolean\n placement?: UseFloatingPlacement\n /** Зазор между триггером и панелью, px. */\n offsetPx?: number\n size?: GrPopoverSize\n /** Роль панели. Меняется теми, кто строит поверх примитива своё меню/список. */\n role?: GrPopoverRole\n /**\n * Модальный режим: фон уходит в `inert`, Tab ходит по кругу внутри панели,\n * скролл страницы блокируется, а слой встаёт в стек модальным — как окно.\n * Нужен поповеру с формой или подтверждением внутри: без изоляции фона\n * пользователь уводит фокус на страницу, к которой поповер и относится.\n *\n * В этом режиме фокус переносится в панель независимо от `autoFocus`: фон\n * недоступен, и слой без фокуса внутри стал бы клавиатурной ловушкой.\n */\n modal?: boolean\n /** Доступное имя панели. Обязательно для `role=\"dialog\"` без видимого заголовка. */\n ariaLabel?: string\n /** `id` видимого заголовка внутри панели — альтернатива `ariaLabel`. */\n labelledBy?: string\n /**\n * `manual` — открывать только программно (через `v-model:open`). Нужно для\n * контекстного меню и подтверждений, где момент открытия решает потребитель.\n */\n trigger?: 'click' | 'manual'\n closeOnEsc?: boolean\n closeOnClickOutside?: boolean\n /** Закрывать по клику внутри панели — удобно для меню, вредно для формы. */\n closeOnContentClick?: boolean\n /**\n * Переносить фокус на панель при открытии. Именно на панель, а не на первый\n * контрол внутри: сфокусировать поле ввода за пользователя — решение\n * содержимого, а не оболочки.\n */\n autoFocus?: boolean\n /**\n * Точечное переопределение точки монтирования. По умолчанию — общий портал\n * оверлеев (`#gr-portal` либо `portalTarget` из `GrConfigProvider`).\n */\n teleportTo?: string | HTMLElement\n contentClass?: string\n disabled?: boolean\n /**\n * Якорь-прямоугольник в координатах вьюпорта вместо обёртки слота `#trigger`:\n * панель встаёт у точки курсора или у строки списка. Нужен контекстному меню,\n * у которого триггера-элемента нет вовсе.\n *\n * `triggerProps` в этом режиме некому потребить, и своих ARIA-атрибутов\n * поповер никуда не вешает: `aria-haspopup` невалиден вне интерактивного\n * элемента, а `aria-expanded` без хозяина — шум. Связь с содержимым страницы\n * объявляет тот, кто открывает.\n */\n anchor?: GrFloatingAnchorRect | null\n /** Поле панели. `none` — содержимое рисует своё (меню, список опций). */\n padding?: GrPopoverPadding\n}\n\nexport interface GrPopoverEmits {\n (e: 'update:open', value: boolean): void\n}\n\nconst props = withDefaults(defineProps<GrPopoverProps>(), {\n open: undefined,\n placement: 'bottom-start',\n offsetPx: 8,\n size: undefined,\n role: 'dialog',\n modal: false,\n ariaLabel: undefined,\n labelledBy: undefined,\n trigger: 'click',\n closeOnEsc: true,\n closeOnClickOutside: true,\n closeOnContentClick: false,\n autoFocus: true,\n teleportTo: undefined,\n contentClass: undefined,\n disabled: false,\n anchor: undefined,\n padding: 'default',\n})\n\nconst emit = defineEmits<GrPopoverEmits>()\n\nconst slots = useSlots()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrPopover' })\n\nconst { open: isOpen, setOpen } = useControlledOpen(\n () => props.open,\n next => emit('update:open', next),\n)\n\nconst rootEl = ref<HTMLElement | null>(null)\nconst panelEl = ref<HTMLElement | null>(null)\nconst panelId = useId()\n\nconst clickOutsideExclude = [() => panelEl.value]\n\nfunction open(): void {\n if (props.disabled)\n return\n setOpen(true)\n}\n\nfunction close(): void {\n setOpen(false)\n}\n\nfunction toggle(): void {\n isOpen.value ? close() : open()\n}\n\n/**\n * Якорь создаётся один раз и читает координаты геттером: `useFloating`\n * пересоздаёт подписку при смене самой ссылки, а точка меняется на каждый\n * вызов — новый объект рвал бы `autoUpdate` без нужды.\n */\nconst anchorEl = createFloatingAnchor(() => props.anchor)\n\n/** Обёртка триггера существует только вместе со слотом — см. проп `anchor`. */\nconst hasTrigger = computed(() => Boolean(slots.trigger))\n\nconst { floatingStyle, resolvedPlacement, update: updateFloatingPosition } = useFloating(\n () => (props.anchor ? anchorEl : rootEl.value),\n panelEl,\n isOpen,\n {\n placement: () => props.placement,\n offsetPx: props.offsetPx,\n // Своего слоя у поповера нет намеренно: он и дропдаун — один класс якорных\n // немодальных оверлеев и обязаны лежать на одной высоте, иначе начнут\n // перекрывать друг друга в зависимости от порядка открытия.\n zIndexVar: '--gr-z-dropdown',\n },\n)\n\nwatch(() => props.placement, () => {\n if (isOpen.value)\n updateFloatingPosition()\n})\n\n// Объект якоря стабилен, поэтому смену координат `useFloating` сам не заметит:\n// повторный вызов у другой точки — это пересчёт позиции, а не переоткрытие.\nwatch(() => props.anchor, () => {\n if (isOpen.value)\n updateFloatingPosition()\n}, { deep: true })\n\n/**\n * Слой стека оверлеев: очередь Esc общая с модалками, поэтому поповер, открытый\n * внутри модалки, по Esc закрывает себя, а не её. Возврат фокуса на триггер\n * тоже отсюда — по правилу «только если на момент закрытия фокус ещё внутри».\n *\n * Модальность включается пропом и приходит той же сборкой, что у окна и drawer'а\n * (`useModalOverlay`): ловушка фокуса, `inert` фона, блокировка скролла. При\n * `modal: false` не включается ничего из этого — дефолтный поповер немодален.\n *\n * Корень слоя и панель здесь один элемент: якорный оверлей уезжает в портал сам,\n * без обёртки.\n */\nconst { inertAttr, portalTarget, teleportEnabled, themeAttrs } = useModalOverlay(isOpen, close, {\n modal: () => props.modal,\n closeOnEscape: () => props.closeOnEsc,\n panel: panelEl,\n root: panelEl,\n teleportTo: () => props.teleportTo,\n initialFocus: () => panelEl.value,\n})\n\n// Фокус переносим после отрисовки панели: до `nextTick` её ещё нет в DOM.\n// В модальном режиме перенос обязателен: фон в `inert`, и фокус, оставленный\n// снаружи, попал бы в недоступное поддерево.\nwatch(isOpen, async (next) => {\n if (!next || !(props.autoFocus || props.modal))\n return\n\n await nextTick()\n panelEl.value?.focus()\n})\n\n/** Роли, которые `aria-haspopup` действительно понимает. */\nconst HASPOPUP_ROLES = ['dialog', 'menu', 'listbox', 'grid'] as const\n\n/**\n * Пропсы для реального фокусируемого триггера: `<button v-bind=\"triggerProps\">`.\n * Клик вешается на обёртку, а ARIA — сюда, потому что `aria-expanded` обязан\n * жить на самом интерактивном элементе, а не на `div` вокруг него.\n */\nconst triggerProps = computed(() => ({\n // `aria-haspopup` принимает не любую роль: `group` в его списке нет, и\n // объявленный им триггер отдавал бы невалидный атрибут, который AT просто\n // отбрасывает. Роли вне списка сообщают о панели только через `aria-expanded`.\n 'aria-haspopup': HASPOPUP_ROLES.includes(props.role as (typeof HASPOPUP_ROLES)[number])\n ? (props.role as (typeof HASPOPUP_ROLES)[number])\n : undefined,\n 'aria-expanded': isOpen.value,\n 'aria-controls': isOpen.value ? panelId : undefined,\n 'disabled': props.disabled || undefined,\n}))\n\nfunction onTriggerClick(): void {\n if (props.trigger === 'click')\n toggle()\n}\n\nfunction onContentClick(): void {\n if (props.closeOnContentClick)\n close()\n}\n\nconst panelClasses = computed(() =>\n grPopoverPanelClass(resolvedSize.value, resolvedPlacement.value, props.contentClass, props.padding),\n)\n\ndefineExpose({ open, close, toggle })\n\ndefineSlots<{\n /**\n * Триггер панели. `triggerProps` обязаны попасть на сам интерактивный\n * элемент, а не на обёртку вокруг него: `aria-expanded` и `aria-controls`\n * читаются с того узла, который получает фокус.\n */\n trigger?: (props: {\n open: boolean\n toggle: () => void\n close: () => void\n triggerProps: Record<string, unknown>\n }) => any\n /** Содержимое панели. */\n content?: (props: { close: () => void }) => any\n}>()\n</script>\n\n<template>\n <div data-gr-popover>\n <div\n v-if=\"hasTrigger\"\n ref=\"rootEl\"\n v-click-outside=\"{ handler: close, enabled: isOpen && closeOnClickOutside, exclude: clickOutsideExclude }\"\n data-gr-popover-trigger\n class=\"inline-block max-w-full\"\n @click=\"onTriggerClick\"\n >\n <slot\n name=\"trigger\"\n :open=\"isOpen\"\n :toggle=\"toggle\"\n :close=\"close\"\n :trigger-props=\"triggerProps\"\n />\n </div>\n\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <transition\n :enter-active-class=\"panelPopTransition.enter\"\n :enter-from-class=\"panelPopTransition.enterFrom\"\n :enter-to-class=\"panelPopTransition.enterTo\"\n :leave-active-class=\"panelPopTransition.leave\"\n :leave-from-class=\"panelPopTransition.leaveFrom\"\n :leave-to-class=\"panelPopTransition.leaveTo\"\n >\n <div\n v-show=\"isOpen\"\n :id=\"panelId\"\n ref=\"panelEl\"\n v-click-outside=\"{ handler: close, enabled: isOpen && closeOnClickOutside && !hasTrigger }\"\n v-bind=\"themeAttrs\"\n data-gr-popover-panel\n data-gr-overlay-root\n :role=\"role === 'none' ? undefined : role\"\n :aria-modal=\"modal ? 'true' : undefined\"\n :inert=\"inertAttr\"\n :aria-label=\"labelledBy ? undefined : ariaLabel\"\n :aria-labelledby=\"labelledBy\"\n tabindex=\"-1\"\n :class=\"panelClasses\"\n :style=\"floatingStyle\"\n @click=\"onContentClick\"\n >\n <slot name=\"content\" :close=\"close\" />\n </div>\n </transition>\n </teleport>\n </div>\n</template>\n"],"mappings":"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"}
|