@feugene/granularity 0.9.1 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-DuUQA7nv.js → GrConfirmDialog-Br6WJkLE.js} +1 -1
  2. package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-DuUQA7nv.js.map → GrConfirmDialog-Br6WJkLE.js.map} +1 -1
  3. package/dist/components/GrConfirmDialog/index.js +1 -1
  4. package/dist/components/GrDialog/chunks/{GrDialog-SYcF-4op.js → GrDialog-C7XLxp8e.js} +1 -1
  5. package/dist/components/GrDialog/chunks/{GrDialog-SYcF-4op.js.map → GrDialog-C7XLxp8e.js.map} +1 -1
  6. package/dist/components/GrDialog/index.js +1 -1
  7. package/dist/components/GrImageViewer/chunks/{GrImageViewer-Cn1X-sWS.js → GrImageViewer-CsvXQHM9.js} +203 -50
  8. package/dist/components/GrImageViewer/chunks/GrImageViewer-CsvXQHM9.js.map +1 -0
  9. package/dist/components/GrImageViewer/index.js +1 -1
  10. package/dist/components/GrModal/chunks/{GrModal-2ALC3kwy.js → GrModal-lh0lOmoi.js} +60 -8
  11. package/dist/components/GrModal/chunks/GrModal-lh0lOmoi.js.map +1 -0
  12. package/dist/components/GrModal/index.js +1 -1
  13. package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-23x2nZvv.js → GrPromptDialog-BV4EW3XO.js} +1 -1
  14. package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-23x2nZvv.js.map → GrPromptDialog-BV4EW3XO.js.map} +1 -1
  15. package/dist/components/GrPromptDialog/index.js +1 -1
  16. package/dist/index.js +5 -5
  17. package/dist/types/src/components/GrImageViewer/GrImageViewer.vue.d.ts +30 -0
  18. package/dist/types/src/components/GrModal/GrModal.vue.d.ts +4 -4
  19. package/dist/types/src/components/GrModal/grModalTopStack.d.ts +31 -0
  20. package/package.json +1 -1
  21. package/dist/components/GrImageViewer/chunks/GrImageViewer-Cn1X-sWS.js.map +0 -1
  22. package/dist/components/GrModal/chunks/GrModal-2ALC3kwy.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GrImageViewer-CsvXQHM9.js","names":["$slots"],"sources":["../../../../src/components/GrImageViewer/GrImageViewer.vue","../../../../src/components/GrImageViewer/GrImageViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'\nimport { Dialog, DialogPanel, TransitionChild, TransitionRoot } from '@headlessui/vue'\n\n/**\n * Usage:\n *\n * <GrImageViewer\n * v-model=\"open\"\n * :url-list=\"images\"\n * :initial-index=\"0\"\n * show-progress\n * hide-on-click-modal\n * />\n */\nconst props = withDefaults(\n defineProps<{\n modelValue: boolean\n urlList: string[]\n initialIndex?: number\n zoomRate?: number\n minScale?: number\n maxScale?: number\n hideOnClickModal?: boolean\n closeOnPressEscape?: boolean\n showProgress?: boolean\n showZoomValue?: boolean\n /** Включает масштабирование колесом мыши / жестом на трекпаде. По умолчанию включено. */\n wheelZoom?: boolean\n /** Включает перетаскивание (pan) картинки мышью. При наведении курсор «рука». По умолчанию выключено. */\n draggable?: boolean\n zIndex?: number\n /** i18n: aria-label кнопки закрытия. */\n closeLabel?: string\n /** i18n: aria-label кнопки «предыдущее изображение». */\n prevLabel?: string\n /** i18n: aria-label кнопки «следующее изображение». */\n nextLabel?: string\n /** i18n: aria-label кнопки «увеличить». */\n zoomInLabel?: string\n /** i18n: aria-label кнопки «уменьшить». */\n zoomOutLabel?: string\n /** i18n: aria-label кнопки «сбросить масштаб». */\n resetZoomLabel?: string\n /** i18n: aria-label кнопки «повернуть влево». */\n rotateLeftLabel?: string\n /** i18n: aria-label кнопки «повернуть вправо». */\n rotateRightLabel?: string\n /** i18n: текст в пустом состоянии (нет изображений). */\n emptyText?: string\n }>(),\n {\n initialIndex: 0,\n zoomRate: 1.2,\n minScale: 0.5,\n maxScale: 5,\n hideOnClickModal: false,\n closeOnPressEscape: true,\n showProgress: false,\n showZoomValue: true,\n wheelZoom: true,\n draggable: false,\n zIndex: undefined,\n closeLabel: 'Close image viewer',\n prevLabel: 'Previous image',\n nextLabel: 'Next image',\n zoomInLabel: 'Zoom in',\n zoomOutLabel: 'Zoom out',\n resetZoomLabel: 'Reset zoom',\n rotateLeftLabel: 'Rotate left',\n rotateRightLabel: 'Rotate right',\n emptyText: 'No image',\n },\n)\n\nconst emit = defineEmits<{\n (e: 'update:modelValue', value: boolean): void\n (e: 'close'): void\n (e: 'switch', newIndex: number): void\n (e: 'rotate', deg: number): void\n}>()\n\ntype GrImageViewerToolbarActions = {\n close: () => void\n prev: () => void\n next: () => void\n zoomIn: () => void\n zoomOut: () => void\n reset: () => void\n rotateLeft: () => void\n rotateRight: () => void\n}\n\ntype GrImageViewerSlotProps = {\n index: number\n displayIndex: number\n total: number\n scale: number\n rotation: number\n /** Натуральный (исходный) размер картинки, px. 0 пока изображение не загружено. */\n naturalWidth: number\n naturalHeight: number\n /** Фактический размер изображения на экране c учётом scale, px (footprint вдоль осей картинки). */\n renderedWidth: number\n renderedHeight: number\n /** Реальный масштаб относительно натурального размера (доля): renderedWidth / naturalWidth. */\n realScale: number\n /** Реальный масштаб в процентах, отформатированный (например `67`). */\n realScalePercent: string\n actions: GrImageViewerToolbarActions\n}\n\nconst DEFAULT_Z_INDEX = 2000\nconst TRAILING_ZERO_DECIMAL_RE = /\\.0$/\n// Чувствительность масштабирования колесом/трекпадом: чем больше — тем резче реакция.\nconst WHEEL_ZOOM_SENSITIVITY = 0.0015\n// Сколько ms простоя после последнего wheel-события считаем «зум завершён»\n// (после этого возвращаем плавный CSS-переход).\nconst WHEEL_IDLE_MS = 120\n\nconst open = computed(() => props.modelValue)\nconst total = computed(() => props.urlList.length)\nconst hasImages = computed(() => total.value > 0)\n\nconst currentIndex = ref(0)\nconst scale = ref(1)\nconst rotation = ref(0)\nconst offsetX = ref(0)\nconst offsetY = ref(0)\n\nconst imageEl = ref<HTMLImageElement | null>(null)\n\n// Флаг активного зума колесом/трекпадом. Пока true — CSS-переход отключён,\n// чтобы непрерывное масштабирование не «наслаивалось» и не дёргалось.\nconst isWheelZooming = ref(false)\n\n// Накопленный deltaY между кадрами и id запланированного rAF.\nlet pendingWheelDelta = 0\nlet wheelFrameId: number | null = null\nlet wheelIdleTimer: ReturnType<typeof setTimeout> | null = null\n\n// Флаг активного перетаскивания (pan) картинки мышью.\nconst isDragging = ref(false)\n\n// Координаты указателя и offset на момент начала перетаскивания.\nlet dragStartX = 0\nlet dragStartY = 0\nlet dragOffsetStartX = 0\nlet dragOffsetStartY = 0\n\n// Натуральный (исходный) размер картинки, читается из `<img>` при загрузке.\nconst naturalWidth = ref(0)\nconst naturalHeight = ref(0)\n\n// Layout-размер вписанного (`object-contain`) изображения без учёта CSS transform/scale.\nconst fittedWidth = ref(0)\nconst fittedHeight = ref(0)\n\nlet resizeObserver: ResizeObserver | null = null\n\nconst currentUrl = computed(() => {\n if (!hasImages.value) {\n return undefined\n }\n\n return props.urlList[currentIndex.value]\n})\n\nconst displayIndex = computed(() => {\n if (!hasImages.value) {\n return 0\n }\n\n return currentIndex.value + 1\n})\n\nconst viewerStyle = computed(() => ({\n zIndex: String(Number.isFinite(props.zIndex) ? props.zIndex : DEFAULT_Z_INDEX),\n}))\n\nconst imageStyle = computed(() => ({\n transform: `translate3d(${offsetX.value}px, ${offsetY.value}px, 0) scale(${scale.value}) rotate(${rotation.value}deg)`,\n}))\n\n// Плавный CSS-переход transform только для дискретных зумов (кнопки/клавиши).\n// При непрерывном зуме колесом и при перетаскивании переход отключаем —\n// иначе картинка «дёргается»/наслаивается и тянется за курсором с задержкой.\nconst imageTransitionClass = computed(() =>\n isWheelZooming.value || isDragging.value ? '' : 'transition-transform duration-150 ease-out',\n)\n\n// Курсор «рука» при наведении (grab) и при зажатии (grabbing), только если включён drag.\nconst imageCursorClass = computed(() => {\n if (!props.draggable) {\n return ''\n }\n\n return isDragging.value ? 'cursor-grabbing' : 'cursor-grab'\n})\n\nfunction formatPercent(value: number): string {\n if (Number.isInteger(value)) {\n return String(value)\n }\n\n return value.toFixed(1).replace(TRAILING_ZERO_DECIMAL_RE, '')\n}\n\nconst zoomValueText = computed(() => formatPercent(scale.value * 100))\n\n// Фактический размер изображения на экране вдоль его осей (footprint), c учётом scale.\nconst renderedWidth = computed(() => Math.round(fittedWidth.value * scale.value))\nconst renderedHeight = computed(() => Math.round(fittedHeight.value * scale.value))\n\n// Реальный масштаб относительно натурального размера (доля). Не зависит от rotation,\n// так как scale равномерный по осям картинки.\nconst realScale = computed(() => {\n if (!naturalWidth.value) {\n return 0\n }\n\n return (fittedWidth.value * scale.value) / naturalWidth.value\n})\n\nconst realScalePercent = computed(() => formatPercent(realScale.value * 100))\n\nconst toolbarSlotProps = computed<GrImageViewerSlotProps>(() => ({\n index: currentIndex.value,\n displayIndex: displayIndex.value,\n total: total.value,\n scale: scale.value,\n rotation: rotation.value,\n naturalWidth: naturalWidth.value,\n naturalHeight: naturalHeight.value,\n renderedWidth: renderedWidth.value,\n renderedHeight: renderedHeight.value,\n realScale: realScale.value,\n realScalePercent: realScalePercent.value,\n actions: toolbarActions,\n}))\n\nfunction normalizeIndex(value: number): number {\n if (!hasImages.value) {\n return 0\n }\n\n const normalized = Number.isFinite(value) ? Math.trunc(value) : 0\n\n return ((normalized % total.value) + total.value) % total.value\n}\n\nfunction resetTransform(): void {\n scale.value = 1\n rotation.value = 0\n offsetX.value = 0\n offsetY.value = 0\n isDragging.value = false\n}\n\nfunction resetImageMetrics(): void {\n naturalWidth.value = 0\n naturalHeight.value = 0\n fittedWidth.value = 0\n fittedHeight.value = 0\n}\n\nfunction measureFitted(): void {\n const el = imageEl.value\n\n if (!el) {\n return\n }\n\n // offsetWidth/Height — layout-размер вписанного изображения, без учёта CSS transform.\n fittedWidth.value = el.offsetWidth\n fittedHeight.value = el.offsetHeight\n}\n\nfunction onImageLoad(event: Event): void {\n const el = event.target as HTMLImageElement\n\n naturalWidth.value = el.naturalWidth\n naturalHeight.value = el.naturalHeight\n measureFitted()\n}\n\nfunction startObservingImage(): void {\n const el = imageEl.value\n\n if (!el || typeof ResizeObserver === 'undefined') {\n return\n }\n\n if (!resizeObserver) {\n resizeObserver = new ResizeObserver(() => measureFitted())\n }\n\n resizeObserver.observe(el)\n\n // Если картинка уже в кэше и `load` не сработал — считаем метрики сразу.\n if (el.complete && el.naturalWidth) {\n naturalWidth.value = el.naturalWidth\n naturalHeight.value = el.naturalHeight\n }\n\n measureFitted()\n}\n\nfunction stopObservingImage(): void {\n resizeObserver?.disconnect()\n}\n\nfunction setIndex(nextIndex: number, options?: { emitSwitch?: boolean }): void {\n const normalizedIndex = normalizeIndex(nextIndex)\n\n if (normalizedIndex === currentIndex.value) {\n return\n }\n\n currentIndex.value = normalizedIndex\n resetTransform()\n resetImageMetrics()\n endWheelZoom()\n\n if (options?.emitSwitch !== false) {\n emit('switch', normalizedIndex)\n }\n}\n\nfunction syncIndexFromInitial(): void {\n if (!hasImages.value) {\n currentIndex.value = 0\n return\n }\n\n currentIndex.value = normalizeIndex(props.initialIndex)\n resetTransform()\n resetImageMetrics()\n endWheelZoom()\n}\n\nfunction preloadAt(index: number): void {\n const url = props.urlList[index]\n\n if (!url) {\n return\n }\n\n const image = new Image()\n image.decoding = 'async'\n image.src = url\n}\n\nfunction closeViewer(): void {\n emit('update:modelValue', false)\n emit('close')\n}\n\nfunction prev(): void {\n setIndex(currentIndex.value - 1)\n}\n\nfunction next(): void {\n setIndex(currentIndex.value + 1)\n}\n\nfunction setScale(value: number): void {\n const clamped = Math.min(props.maxScale, Math.max(props.minScale, value))\n\n scale.value = Number(clamped.toFixed(4))\n}\n\nfunction zoomIn(): void {\n setScale(scale.value * props.zoomRate)\n}\n\nfunction zoomOut(): void {\n setScale(scale.value / props.zoomRate)\n}\n\nfunction cancelWheelFrame(): void {\n if (wheelFrameId !== null) {\n cancelAnimationFrame(wheelFrameId)\n wheelFrameId = null\n }\n}\n\nfunction endWheelZoom(): void {\n if (wheelIdleTimer !== null) {\n clearTimeout(wheelIdleTimer)\n wheelIdleTimer = null\n }\n\n isWheelZooming.value = false\n pendingWheelDelta = 0\n cancelWheelFrame()\n}\n\n// Применяем накопленный за кадр deltaY одним обновлением scale — так рендер\n// происходит не чаще одного раза в кадр (без «дёрганья» от частых обновлений).\nfunction flushWheelZoom(): void {\n wheelFrameId = null\n\n const delta = pendingWheelDelta\n pendingWheelDelta = 0\n\n if (!delta) {\n return\n }\n\n // Экспоненциальный шаг — плавно и одинаково ощущается и для колеса, и для трекпада.\n // delta < 0 (от себя / scroll up) — приближение, delta > 0 — отдаление.\n const factor = Math.exp(-delta * WHEEL_ZOOM_SENSITIVITY)\n\n setScale(scale.value * factor)\n}\n\nfunction onWheel(event: WheelEvent): void {\n if (!props.wheelZoom || !hasImages.value) {\n return\n }\n\n // Не даём странице/оверлею проскроллиться — колесо отдаём под зум.\n event.preventDefault()\n\n // Во время непрерывного зума отключаем CSS-переход: плавность даёт само\n // покадровое обновление, а transition на каждом микрошаге вызывает лаг/наслоение.\n isWheelZooming.value = true\n\n pendingWheelDelta += event.deltaY\n\n if (wheelFrameId === null) {\n wheelFrameId = requestAnimationFrame(flushWheelZoom)\n }\n\n if (wheelIdleTimer !== null) {\n clearTimeout(wheelIdleTimer)\n }\n\n // По завершении жеста возвращаем плавный переход для последующих дискретных зумов.\n wheelIdleTimer = setTimeout(() => {\n wheelIdleTimer = null\n isWheelZooming.value = false\n }, WHEEL_IDLE_MS)\n}\n\nfunction onPointerDown(event: PointerEvent): void {\n // Тянем только основной кнопкой и только если drag включён и есть картинка.\n if (!props.draggable || !hasImages.value || event.button !== 0) {\n return\n }\n\n event.preventDefault()\n\n isDragging.value = true\n dragStartX = event.clientX\n dragStartY = event.clientY\n dragOffsetStartX = offsetX.value\n dragOffsetStartY = offsetY.value\n\n // Захватываем указатель, чтобы движение/отпускание ловились даже за пределами картинки.\n ;(event.currentTarget as Element).setPointerCapture?.(event.pointerId)\n}\n\nfunction onPointerMove(event: PointerEvent): void {\n if (!isDragging.value) {\n return\n }\n\n offsetX.value = dragOffsetStartX + (event.clientX - dragStartX)\n offsetY.value = dragOffsetStartY + (event.clientY - dragStartY)\n}\n\nfunction onPointerUp(event: PointerEvent): void {\n if (!isDragging.value) {\n return\n }\n\n isDragging.value = false\n ;(event.currentTarget as Element).releasePointerCapture?.(event.pointerId)\n}\n\nfunction rotateLeft(): void {\n rotation.value -= 90\n emit('rotate', rotation.value)\n}\n\nfunction rotateRight(): void {\n rotation.value += 90\n emit('rotate', rotation.value)\n}\n\nconst toolbarActions: GrImageViewerToolbarActions = {\n close: closeViewer,\n prev,\n next,\n zoomIn,\n zoomOut,\n reset: resetTransform,\n rotateLeft,\n rotateRight,\n}\n\nfunction onBackdropClick(): void {\n if (!props.hideOnClickModal) {\n return\n }\n\n closeViewer()\n}\n\nfunction onKeydown(event: KeyboardEvent): void {\n switch (event.key) {\n case 'Escape':\n if (!props.closeOnPressEscape) {\n return\n }\n\n event.preventDefault()\n closeViewer()\n return\n\n case 'ArrowLeft':\n event.preventDefault()\n prev()\n return\n\n case 'ArrowRight':\n event.preventDefault()\n next()\n return\n\n case '+':\n case '=':\n case 'Add':\n event.preventDefault()\n zoomIn()\n return\n\n case '-':\n case '_':\n case 'Subtract':\n event.preventDefault()\n zoomOut()\n return\n\n case '0':\n event.preventDefault()\n resetTransform()\n break\n\n default:\n }\n}\n\nwatch(\n () => props.modelValue,\n async (isOpen) => {\n if (!isOpen) {\n stopObservingImage()\n endWheelZoom()\n isDragging.value = false\n return\n }\n\n syncIndexFromInitial()\n await nextTick()\n startObservingImage()\n },\n { immediate: true },\n)\n\nwatch(\n () => props.initialIndex,\n () => {\n if (!props.modelValue) {\n return\n }\n\n syncIndexFromInitial()\n },\n)\n\nwatch(\n () => props.urlList,\n () => {\n if (!props.modelValue) {\n return\n }\n\n syncIndexFromInitial()\n },\n { deep: true },\n)\n\nwatch(\n currentIndex,\n (index) => {\n if (!hasImages.value || total.value < 2) {\n return\n }\n\n preloadAt(normalizeIndex(index - 1))\n preloadAt(normalizeIndex(index + 1))\n },\n { immediate: true },\n)\n\nonBeforeUnmount(() => {\n stopObservingImage()\n endWheelZoom()\n})\n</script>\n\n<template>\n <teleport to=\"body\">\n <TransitionRoot :show=\"open\" as=\"template\">\n <Dialog\n as=\"div\"\n class=\"fixed inset-0\"\n :style=\"viewerStyle\"\n role=\"dialog\"\n aria-modal=\"true\"\n :static=\"true\"\n @keydown=\"onKeydown\"\n >\n <div class=\"fixed inset-0 overflow-hidden\">\n <TransitionChild\n as=\"template\"\n enter=\"duration-150 ease-out\"\n enter-from=\"opacity-0\"\n enter-to=\"opacity-100\"\n leave=\"duration-120 ease-in\"\n leave-from=\"opacity-100\"\n leave-to=\"opacity-0\"\n >\n <div\n data-ds-image-viewer-overlay\n class=\"absolute inset-0 bg-black/60 backdrop-blur-sm\"\n aria-hidden=\"true\"\n @click=\"onBackdropClick\"\n />\n </TransitionChild>\n\n <TransitionChild\n as=\"template\"\n enter=\"duration-180 ease-out\"\n enter-from=\"opacity-0 scale-98\"\n enter-to=\"opacity-100 scale-100\"\n leave=\"duration-130 ease-in\"\n leave-from=\"opacity-100 scale-100\"\n leave-to=\"opacity-0 scale-98\"\n >\n <DialogPanel\n data-ds-image-viewer-panel\n class=\"relative z-10 h-full w-full outline-none\"\n tabindex=\"-1\"\n >\n <div class=\"pointer-events-none absolute inset-x-0 top-0 z-30 px-3 py-3 sm:px-6\">\n <div class=\"pointer-events-auto flex items-center justify-between gap-3\">\n <div class=\"flex items-center gap-2\">\n <div\n v-if=\"showProgress\"\n data-ds-image-viewer-progress\n class=\"rounded-full bg-black/35 px-3 py-1 text-xs font-600 text-white/95 sm:text-sm\"\n >\n {{ displayIndex }} / {{ total }}\n </div>\n\n <div\n v-if=\"showZoomValue\"\n data-ds-image-viewer-zoom-value\n class=\"rounded-full bg-black/35 px-3 py-1 text-xs font-700 text-white/95 sm:text-sm\"\n >\n {{ zoomValueText }}%\n </div>\n </div>\n\n <button\n type=\"button\"\n data-ds-image-viewer-close\n :aria-label=\"closeLabel\"\n class=\"h-11 w-11 flex items-center justify-center rounded-full border border-white/20 bg-black/35 text-white transition-colors hover:bg-black/55 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80\"\n @click=\"closeViewer\"\n >\n <span aria-hidden=\"true\" class=\"text-lg leading-none\">✕</span>\n </button>\n </div>\n </div>\n\n <div\n class=\"relative z-10 h-full w-full flex items-center justify-center px-16 py-16 sm:px-24 sm:py-20\"\n @wheel=\"onWheel\"\n >\n <button\n v-if=\"total > 1\"\n type=\"button\"\n data-ds-image-viewer-prev\n :aria-label=\"prevLabel\"\n class=\"absolute left-3 top-1/2 z-20 h-12 w-12 -translate-y-1/2 flex items-center justify-center rounded-full border border-white/20 bg-black/35 text-white transition-colors hover:bg-black/55 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80 sm:left-6\"\n @click=\"prev\"\n >\n <span aria-hidden=\"true\" class=\"text-xl leading-none\">‹</span>\n </button>\n\n <button\n v-if=\"total > 1\"\n type=\"button\"\n data-ds-image-viewer-next\n :aria-label=\"nextLabel\"\n class=\"absolute right-3 top-1/2 z-20 h-12 w-12 -translate-y-1/2 flex items-center justify-center rounded-full border border-white/20 bg-black/35 text-white transition-colors hover:bg-black/55 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80 sm:right-6\"\n @click=\"next\"\n >\n <span aria-hidden=\"true\" class=\"text-xl leading-none\">›</span>\n </button>\n\n <div class=\"h-full w-full flex items-center justify-center\">\n <img\n v-if=\"currentUrl\"\n ref=\"imageEl\"\n data-ds-image-viewer-image\n :src=\"currentUrl\"\n alt=\"\"\n draggable=\"false\"\n class=\"max-h-full max-w-full select-none object-contain will-change-transform\"\n :class=\"[imageTransitionClass, imageCursorClass]\"\n :style=\"imageStyle\"\n @load=\"onImageLoad\"\n @pointerdown=\"onPointerDown\"\n @pointermove=\"onPointerMove\"\n @pointerup=\"onPointerUp\"\n @pointercancel=\"onPointerUp\"\n >\n\n <div\n v-else\n class=\"rounded-[var(--ds-radius-xl)] border border-white/20 bg-black/25 px-4 py-3 text-sm text-white/80\"\n >\n {{ emptyText }}\n </div>\n </div>\n </div>\n\n <div class=\"pointer-events-none absolute inset-x-0 bottom-0 z-30 px-3 pb-3 sm:px-6 sm:pb-6\">\n <div class=\"pointer-events-auto mx-auto w-full max-w-max\">\n <slot\n name=\"toolbar\"\n v-bind=\"toolbarSlotProps\"\n >\n <div class=\"rounded-full border border-white/20 bg-black/35 p-1 backdrop-blur-sm flex items-center gap-1\">\n <button\n type=\"button\"\n data-ds-image-viewer-zoom-out\n :aria-label=\"zoomOutLabel\"\n class=\"h-11 min-w-11 px-2 flex items-center justify-center rounded-full text-sm font-600 text-white transition-colors hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80\"\n @click=\"zoomOut\"\n >\n −\n </button>\n\n <button\n type=\"button\"\n data-ds-image-viewer-zoom-reset\n :aria-label=\"resetZoomLabel\"\n class=\"h-11 min-w-11 px-3 flex items-center justify-center rounded-full text-xs font-700 text-white transition-colors hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80\"\n @click=\"resetTransform\"\n >\n 100%\n </button>\n\n <button\n type=\"button\"\n data-ds-image-viewer-zoom-in\n :aria-label=\"zoomInLabel\"\n class=\"h-11 min-w-11 px-2 flex items-center justify-center rounded-full text-sm font-600 text-white transition-colors hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80\"\n @click=\"zoomIn\"\n >\n +\n </button>\n\n <template v-if=\"$slots['toolbar-actions']\">\n <div class=\"mx-1 h-6 w-px bg-white/25\" aria-hidden=\"true\" />\n\n <slot\n name=\"toolbar-actions\"\n v-bind=\"toolbarSlotProps\"\n />\n\n <div class=\"mx-1 h-6 w-px bg-white/25\" aria-hidden=\"true\" />\n </template>\n\n <div v-else class=\"mx-1 h-6 w-px bg-white/25\" aria-hidden=\"true\" />\n\n <button\n type=\"button\"\n data-ds-image-viewer-rotate-left\n :aria-label=\"rotateLeftLabel\"\n class=\"h-11 min-w-11 px-2 flex items-center justify-center rounded-full text-sm font-600 text-white transition-colors hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80\"\n @click=\"rotateLeft\"\n >\n ↺\n </button>\n\n <button\n type=\"button\"\n data-ds-image-viewer-rotate-right\n :aria-label=\"rotateRightLabel\"\n class=\"h-11 min-w-11 px-2 flex items-center justify-center rounded-full text-sm font-600 text-white transition-colors hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80\"\n @click=\"rotateRight\"\n >\n ↻\n </button>\n </div>\n </slot>\n </div>\n </div>\n </DialogPanel>\n </TransitionChild>\n </div>\n </Dialog>\n </TransitionRoot>\n </teleport>\n</template>","<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'\nimport { Dialog, DialogPanel, TransitionChild, TransitionRoot } from '@headlessui/vue'\n\n/**\n * Usage:\n *\n * <GrImageViewer\n * v-model=\"open\"\n * :url-list=\"images\"\n * :initial-index=\"0\"\n * show-progress\n * hide-on-click-modal\n * />\n */\nconst props = withDefaults(\n defineProps<{\n modelValue: boolean\n urlList: string[]\n initialIndex?: number\n zoomRate?: number\n minScale?: number\n maxScale?: number\n hideOnClickModal?: boolean\n closeOnPressEscape?: boolean\n showProgress?: boolean\n showZoomValue?: boolean\n /** Включает масштабирование колесом мыши / жестом на трекпаде. По умолчанию включено. */\n wheelZoom?: boolean\n /** Включает перетаскивание (pan) картинки мышью. При наведении курсор «рука». По умолчанию выключено. */\n draggable?: boolean\n zIndex?: number\n /** i18n: aria-label кнопки закрытия. */\n closeLabel?: string\n /** i18n: aria-label кнопки «предыдущее изображение». */\n prevLabel?: string\n /** i18n: aria-label кнопки «следующее изображение». */\n nextLabel?: string\n /** i18n: aria-label кнопки «увеличить». */\n zoomInLabel?: string\n /** i18n: aria-label кнопки «уменьшить». */\n zoomOutLabel?: string\n /** i18n: aria-label кнопки «сбросить масштаб». */\n resetZoomLabel?: string\n /** i18n: aria-label кнопки «повернуть влево». */\n rotateLeftLabel?: string\n /** i18n: aria-label кнопки «повернуть вправо». */\n rotateRightLabel?: string\n /** i18n: текст в пустом состоянии (нет изображений). */\n emptyText?: string\n }>(),\n {\n initialIndex: 0,\n zoomRate: 1.2,\n minScale: 0.5,\n maxScale: 5,\n hideOnClickModal: false,\n closeOnPressEscape: true,\n showProgress: false,\n showZoomValue: true,\n wheelZoom: true,\n draggable: false,\n zIndex: undefined,\n closeLabel: 'Close image viewer',\n prevLabel: 'Previous image',\n nextLabel: 'Next image',\n zoomInLabel: 'Zoom in',\n zoomOutLabel: 'Zoom out',\n resetZoomLabel: 'Reset zoom',\n rotateLeftLabel: 'Rotate left',\n rotateRightLabel: 'Rotate right',\n emptyText: 'No image',\n },\n)\n\nconst emit = defineEmits<{\n (e: 'update:modelValue', value: boolean): void\n (e: 'close'): void\n (e: 'switch', newIndex: number): void\n (e: 'rotate', deg: number): void\n}>()\n\ntype GrImageViewerToolbarActions = {\n close: () => void\n prev: () => void\n next: () => void\n zoomIn: () => void\n zoomOut: () => void\n reset: () => void\n rotateLeft: () => void\n rotateRight: () => void\n}\n\ntype GrImageViewerSlotProps = {\n index: number\n displayIndex: number\n total: number\n scale: number\n rotation: number\n /** Натуральный (исходный) размер картинки, px. 0 пока изображение не загружено. */\n naturalWidth: number\n naturalHeight: number\n /** Фактический размер изображения на экране c учётом scale, px (footprint вдоль осей картинки). */\n renderedWidth: number\n renderedHeight: number\n /** Реальный масштаб относительно натурального размера (доля): renderedWidth / naturalWidth. */\n realScale: number\n /** Реальный масштаб в процентах, отформатированный (например `67`). */\n realScalePercent: string\n actions: GrImageViewerToolbarActions\n}\n\nconst DEFAULT_Z_INDEX = 2000\nconst TRAILING_ZERO_DECIMAL_RE = /\\.0$/\n// Чувствительность масштабирования колесом/трекпадом: чем больше — тем резче реакция.\nconst WHEEL_ZOOM_SENSITIVITY = 0.0015\n// Сколько ms простоя после последнего wheel-события считаем «зум завершён»\n// (после этого возвращаем плавный CSS-переход).\nconst WHEEL_IDLE_MS = 120\n\nconst open = computed(() => props.modelValue)\nconst total = computed(() => props.urlList.length)\nconst hasImages = computed(() => total.value > 0)\n\nconst currentIndex = ref(0)\nconst scale = ref(1)\nconst rotation = ref(0)\nconst offsetX = ref(0)\nconst offsetY = ref(0)\n\nconst imageEl = ref<HTMLImageElement | null>(null)\n\n// Флаг активного зума колесом/трекпадом. Пока true — CSS-переход отключён,\n// чтобы непрерывное масштабирование не «наслаивалось» и не дёргалось.\nconst isWheelZooming = ref(false)\n\n// Накопленный deltaY между кадрами и id запланированного rAF.\nlet pendingWheelDelta = 0\nlet wheelFrameId: number | null = null\nlet wheelIdleTimer: ReturnType<typeof setTimeout> | null = null\n\n// Флаг активного перетаскивания (pan) картинки мышью.\nconst isDragging = ref(false)\n\n// Координаты указателя и offset на момент начала перетаскивания.\nlet dragStartX = 0\nlet dragStartY = 0\nlet dragOffsetStartX = 0\nlet dragOffsetStartY = 0\n\n// Натуральный (исходный) размер картинки, читается из `<img>` при загрузке.\nconst naturalWidth = ref(0)\nconst naturalHeight = ref(0)\n\n// Layout-размер вписанного (`object-contain`) изображения без учёта CSS transform/scale.\nconst fittedWidth = ref(0)\nconst fittedHeight = ref(0)\n\nlet resizeObserver: ResizeObserver | null = null\n\nconst currentUrl = computed(() => {\n if (!hasImages.value) {\n return undefined\n }\n\n return props.urlList[currentIndex.value]\n})\n\nconst displayIndex = computed(() => {\n if (!hasImages.value) {\n return 0\n }\n\n return currentIndex.value + 1\n})\n\nconst viewerStyle = computed(() => ({\n zIndex: String(Number.isFinite(props.zIndex) ? props.zIndex : DEFAULT_Z_INDEX),\n}))\n\nconst imageStyle = computed(() => ({\n transform: `translate3d(${offsetX.value}px, ${offsetY.value}px, 0) scale(${scale.value}) rotate(${rotation.value}deg)`,\n}))\n\n// Плавный CSS-переход transform только для дискретных зумов (кнопки/клавиши).\n// При непрерывном зуме колесом и при перетаскивании переход отключаем —\n// иначе картинка «дёргается»/наслаивается и тянется за курсором с задержкой.\nconst imageTransitionClass = computed(() =>\n isWheelZooming.value || isDragging.value ? '' : 'transition-transform duration-150 ease-out',\n)\n\n// Курсор «рука» при наведении (grab) и при зажатии (grabbing), только если включён drag.\nconst imageCursorClass = computed(() => {\n if (!props.draggable) {\n return ''\n }\n\n return isDragging.value ? 'cursor-grabbing' : 'cursor-grab'\n})\n\nfunction formatPercent(value: number): string {\n if (Number.isInteger(value)) {\n return String(value)\n }\n\n return value.toFixed(1).replace(TRAILING_ZERO_DECIMAL_RE, '')\n}\n\nconst zoomValueText = computed(() => formatPercent(scale.value * 100))\n\n// Фактический размер изображения на экране вдоль его осей (footprint), c учётом scale.\nconst renderedWidth = computed(() => Math.round(fittedWidth.value * scale.value))\nconst renderedHeight = computed(() => Math.round(fittedHeight.value * scale.value))\n\n// Реальный масштаб относительно натурального размера (доля). Не зависит от rotation,\n// так как scale равномерный по осям картинки.\nconst realScale = computed(() => {\n if (!naturalWidth.value) {\n return 0\n }\n\n return (fittedWidth.value * scale.value) / naturalWidth.value\n})\n\nconst realScalePercent = computed(() => formatPercent(realScale.value * 100))\n\nconst toolbarSlotProps = computed<GrImageViewerSlotProps>(() => ({\n index: currentIndex.value,\n displayIndex: displayIndex.value,\n total: total.value,\n scale: scale.value,\n rotation: rotation.value,\n naturalWidth: naturalWidth.value,\n naturalHeight: naturalHeight.value,\n renderedWidth: renderedWidth.value,\n renderedHeight: renderedHeight.value,\n realScale: realScale.value,\n realScalePercent: realScalePercent.value,\n actions: toolbarActions,\n}))\n\nfunction normalizeIndex(value: number): number {\n if (!hasImages.value) {\n return 0\n }\n\n const normalized = Number.isFinite(value) ? Math.trunc(value) : 0\n\n return ((normalized % total.value) + total.value) % total.value\n}\n\nfunction resetTransform(): void {\n scale.value = 1\n rotation.value = 0\n offsetX.value = 0\n offsetY.value = 0\n isDragging.value = false\n}\n\nfunction resetImageMetrics(): void {\n naturalWidth.value = 0\n naturalHeight.value = 0\n fittedWidth.value = 0\n fittedHeight.value = 0\n}\n\nfunction measureFitted(): void {\n const el = imageEl.value\n\n if (!el) {\n return\n }\n\n // offsetWidth/Height — layout-размер вписанного изображения, без учёта CSS transform.\n fittedWidth.value = el.offsetWidth\n fittedHeight.value = el.offsetHeight\n}\n\nfunction onImageLoad(event: Event): void {\n const el = event.target as HTMLImageElement\n\n naturalWidth.value = el.naturalWidth\n naturalHeight.value = el.naturalHeight\n measureFitted()\n}\n\nfunction startObservingImage(): void {\n const el = imageEl.value\n\n if (!el || typeof ResizeObserver === 'undefined') {\n return\n }\n\n if (!resizeObserver) {\n resizeObserver = new ResizeObserver(() => measureFitted())\n }\n\n resizeObserver.observe(el)\n\n // Если картинка уже в кэше и `load` не сработал — считаем метрики сразу.\n if (el.complete && el.naturalWidth) {\n naturalWidth.value = el.naturalWidth\n naturalHeight.value = el.naturalHeight\n }\n\n measureFitted()\n}\n\nfunction stopObservingImage(): void {\n resizeObserver?.disconnect()\n}\n\nfunction setIndex(nextIndex: number, options?: { emitSwitch?: boolean }): void {\n const normalizedIndex = normalizeIndex(nextIndex)\n\n if (normalizedIndex === currentIndex.value) {\n return\n }\n\n currentIndex.value = normalizedIndex\n resetTransform()\n resetImageMetrics()\n endWheelZoom()\n\n if (options?.emitSwitch !== false) {\n emit('switch', normalizedIndex)\n }\n}\n\nfunction syncIndexFromInitial(): void {\n if (!hasImages.value) {\n currentIndex.value = 0\n return\n }\n\n currentIndex.value = normalizeIndex(props.initialIndex)\n resetTransform()\n resetImageMetrics()\n endWheelZoom()\n}\n\nfunction preloadAt(index: number): void {\n const url = props.urlList[index]\n\n if (!url) {\n return\n }\n\n const image = new Image()\n image.decoding = 'async'\n image.src = url\n}\n\nfunction closeViewer(): void {\n emit('update:modelValue', false)\n emit('close')\n}\n\nfunction prev(): void {\n setIndex(currentIndex.value - 1)\n}\n\nfunction next(): void {\n setIndex(currentIndex.value + 1)\n}\n\nfunction setScale(value: number): void {\n const clamped = Math.min(props.maxScale, Math.max(props.minScale, value))\n\n scale.value = Number(clamped.toFixed(4))\n}\n\nfunction zoomIn(): void {\n setScale(scale.value * props.zoomRate)\n}\n\nfunction zoomOut(): void {\n setScale(scale.value / props.zoomRate)\n}\n\nfunction cancelWheelFrame(): void {\n if (wheelFrameId !== null) {\n cancelAnimationFrame(wheelFrameId)\n wheelFrameId = null\n }\n}\n\nfunction endWheelZoom(): void {\n if (wheelIdleTimer !== null) {\n clearTimeout(wheelIdleTimer)\n wheelIdleTimer = null\n }\n\n isWheelZooming.value = false\n pendingWheelDelta = 0\n cancelWheelFrame()\n}\n\n// Применяем накопленный за кадр deltaY одним обновлением scale — так рендер\n// происходит не чаще одного раза в кадр (без «дёрганья» от частых обновлений).\nfunction flushWheelZoom(): void {\n wheelFrameId = null\n\n const delta = pendingWheelDelta\n pendingWheelDelta = 0\n\n if (!delta) {\n return\n }\n\n // Экспоненциальный шаг — плавно и одинаково ощущается и для колеса, и для трекпада.\n // delta < 0 (от себя / scroll up) — приближение, delta > 0 — отдаление.\n const factor = Math.exp(-delta * WHEEL_ZOOM_SENSITIVITY)\n\n setScale(scale.value * factor)\n}\n\nfunction onWheel(event: WheelEvent): void {\n if (!props.wheelZoom || !hasImages.value) {\n return\n }\n\n // Не даём странице/оверлею проскроллиться — колесо отдаём под зум.\n event.preventDefault()\n\n // Во время непрерывного зума отключаем CSS-переход: плавность даёт само\n // покадровое обновление, а transition на каждом микрошаге вызывает лаг/наслоение.\n isWheelZooming.value = true\n\n pendingWheelDelta += event.deltaY\n\n if (wheelFrameId === null) {\n wheelFrameId = requestAnimationFrame(flushWheelZoom)\n }\n\n if (wheelIdleTimer !== null) {\n clearTimeout(wheelIdleTimer)\n }\n\n // По завершении жеста возвращаем плавный переход для последующих дискретных зумов.\n wheelIdleTimer = setTimeout(() => {\n wheelIdleTimer = null\n isWheelZooming.value = false\n }, WHEEL_IDLE_MS)\n}\n\nfunction onPointerDown(event: PointerEvent): void {\n // Тянем только основной кнопкой и только если drag включён и есть картинка.\n if (!props.draggable || !hasImages.value || event.button !== 0) {\n return\n }\n\n event.preventDefault()\n\n isDragging.value = true\n dragStartX = event.clientX\n dragStartY = event.clientY\n dragOffsetStartX = offsetX.value\n dragOffsetStartY = offsetY.value\n\n // Захватываем указатель, чтобы движение/отпускание ловились даже за пределами картинки.\n ;(event.currentTarget as Element).setPointerCapture?.(event.pointerId)\n}\n\nfunction onPointerMove(event: PointerEvent): void {\n if (!isDragging.value) {\n return\n }\n\n offsetX.value = dragOffsetStartX + (event.clientX - dragStartX)\n offsetY.value = dragOffsetStartY + (event.clientY - dragStartY)\n}\n\nfunction onPointerUp(event: PointerEvent): void {\n if (!isDragging.value) {\n return\n }\n\n isDragging.value = false\n ;(event.currentTarget as Element).releasePointerCapture?.(event.pointerId)\n}\n\nfunction rotateLeft(): void {\n rotation.value -= 90\n emit('rotate', rotation.value)\n}\n\nfunction rotateRight(): void {\n rotation.value += 90\n emit('rotate', rotation.value)\n}\n\nconst toolbarActions: GrImageViewerToolbarActions = {\n close: closeViewer,\n prev,\n next,\n zoomIn,\n zoomOut,\n reset: resetTransform,\n rotateLeft,\n rotateRight,\n}\n\nfunction onBackdropClick(): void {\n if (!props.hideOnClickModal) {\n return\n }\n\n closeViewer()\n}\n\nfunction onKeydown(event: KeyboardEvent): void {\n switch (event.key) {\n case 'Escape':\n if (!props.closeOnPressEscape) {\n return\n }\n\n event.preventDefault()\n closeViewer()\n return\n\n case 'ArrowLeft':\n event.preventDefault()\n prev()\n return\n\n case 'ArrowRight':\n event.preventDefault()\n next()\n return\n\n case '+':\n case '=':\n case 'Add':\n event.preventDefault()\n zoomIn()\n return\n\n case '-':\n case '_':\n case 'Subtract':\n event.preventDefault()\n zoomOut()\n return\n\n case '0':\n event.preventDefault()\n resetTransform()\n break\n\n default:\n }\n}\n\nwatch(\n () => props.modelValue,\n async (isOpen) => {\n if (!isOpen) {\n stopObservingImage()\n endWheelZoom()\n isDragging.value = false\n return\n }\n\n syncIndexFromInitial()\n await nextTick()\n startObservingImage()\n },\n { immediate: true },\n)\n\nwatch(\n () => props.initialIndex,\n () => {\n if (!props.modelValue) {\n return\n }\n\n syncIndexFromInitial()\n },\n)\n\nwatch(\n () => props.urlList,\n () => {\n if (!props.modelValue) {\n return\n }\n\n syncIndexFromInitial()\n },\n { deep: true },\n)\n\nwatch(\n currentIndex,\n (index) => {\n if (!hasImages.value || total.value < 2) {\n return\n }\n\n preloadAt(normalizeIndex(index - 1))\n preloadAt(normalizeIndex(index + 1))\n },\n { immediate: true },\n)\n\nonBeforeUnmount(() => {\n stopObservingImage()\n endWheelZoom()\n})\n</script>\n\n<template>\n <teleport to=\"body\">\n <TransitionRoot :show=\"open\" as=\"template\">\n <Dialog\n as=\"div\"\n class=\"fixed inset-0\"\n :style=\"viewerStyle\"\n role=\"dialog\"\n aria-modal=\"true\"\n :static=\"true\"\n @keydown=\"onKeydown\"\n >\n <div class=\"fixed inset-0 overflow-hidden\">\n <TransitionChild\n as=\"template\"\n enter=\"duration-150 ease-out\"\n enter-from=\"opacity-0\"\n enter-to=\"opacity-100\"\n leave=\"duration-120 ease-in\"\n leave-from=\"opacity-100\"\n leave-to=\"opacity-0\"\n >\n <div\n data-ds-image-viewer-overlay\n class=\"absolute inset-0 bg-black/60 backdrop-blur-sm\"\n aria-hidden=\"true\"\n @click=\"onBackdropClick\"\n />\n </TransitionChild>\n\n <TransitionChild\n as=\"template\"\n enter=\"duration-180 ease-out\"\n enter-from=\"opacity-0 scale-98\"\n enter-to=\"opacity-100 scale-100\"\n leave=\"duration-130 ease-in\"\n leave-from=\"opacity-100 scale-100\"\n leave-to=\"opacity-0 scale-98\"\n >\n <DialogPanel\n data-ds-image-viewer-panel\n class=\"relative z-10 h-full w-full outline-none\"\n tabindex=\"-1\"\n >\n <div class=\"pointer-events-none absolute inset-x-0 top-0 z-30 px-3 py-3 sm:px-6\">\n <div class=\"pointer-events-auto flex items-center justify-between gap-3\">\n <div class=\"flex items-center gap-2\">\n <div\n v-if=\"showProgress\"\n data-ds-image-viewer-progress\n class=\"rounded-full bg-black/35 px-3 py-1 text-xs font-600 text-white/95 sm:text-sm\"\n >\n {{ displayIndex }} / {{ total }}\n </div>\n\n <div\n v-if=\"showZoomValue\"\n data-ds-image-viewer-zoom-value\n class=\"rounded-full bg-black/35 px-3 py-1 text-xs font-700 text-white/95 sm:text-sm\"\n >\n {{ zoomValueText }}%\n </div>\n </div>\n\n <button\n type=\"button\"\n data-ds-image-viewer-close\n :aria-label=\"closeLabel\"\n class=\"h-11 w-11 flex items-center justify-center rounded-full border border-white/20 bg-black/35 text-white transition-colors hover:bg-black/55 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80\"\n @click=\"closeViewer\"\n >\n <span aria-hidden=\"true\" class=\"text-lg leading-none\">✕</span>\n </button>\n </div>\n </div>\n\n <div\n class=\"relative z-10 h-full w-full flex items-center justify-center px-16 py-16 sm:px-24 sm:py-20\"\n @wheel=\"onWheel\"\n >\n <button\n v-if=\"total > 1\"\n type=\"button\"\n data-ds-image-viewer-prev\n :aria-label=\"prevLabel\"\n class=\"absolute left-3 top-1/2 z-20 h-12 w-12 -translate-y-1/2 flex items-center justify-center rounded-full border border-white/20 bg-black/35 text-white transition-colors hover:bg-black/55 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80 sm:left-6\"\n @click=\"prev\"\n >\n <span aria-hidden=\"true\" class=\"text-xl leading-none\">‹</span>\n </button>\n\n <button\n v-if=\"total > 1\"\n type=\"button\"\n data-ds-image-viewer-next\n :aria-label=\"nextLabel\"\n class=\"absolute right-3 top-1/2 z-20 h-12 w-12 -translate-y-1/2 flex items-center justify-center rounded-full border border-white/20 bg-black/35 text-white transition-colors hover:bg-black/55 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80 sm:right-6\"\n @click=\"next\"\n >\n <span aria-hidden=\"true\" class=\"text-xl leading-none\">›</span>\n </button>\n\n <div class=\"h-full w-full flex items-center justify-center\">\n <img\n v-if=\"currentUrl\"\n ref=\"imageEl\"\n data-ds-image-viewer-image\n :src=\"currentUrl\"\n alt=\"\"\n draggable=\"false\"\n class=\"max-h-full max-w-full select-none object-contain will-change-transform\"\n :class=\"[imageTransitionClass, imageCursorClass]\"\n :style=\"imageStyle\"\n @load=\"onImageLoad\"\n @pointerdown=\"onPointerDown\"\n @pointermove=\"onPointerMove\"\n @pointerup=\"onPointerUp\"\n @pointercancel=\"onPointerUp\"\n >\n\n <div\n v-else\n class=\"rounded-[var(--ds-radius-xl)] border border-white/20 bg-black/25 px-4 py-3 text-sm text-white/80\"\n >\n {{ emptyText }}\n </div>\n </div>\n </div>\n\n <div class=\"pointer-events-none absolute inset-x-0 bottom-0 z-30 px-3 pb-3 sm:px-6 sm:pb-6\">\n <div class=\"pointer-events-auto mx-auto w-full max-w-max\">\n <slot\n name=\"toolbar\"\n v-bind=\"toolbarSlotProps\"\n >\n <div class=\"rounded-full border border-white/20 bg-black/35 p-1 backdrop-blur-sm flex items-center gap-1\">\n <button\n type=\"button\"\n data-ds-image-viewer-zoom-out\n :aria-label=\"zoomOutLabel\"\n class=\"h-11 min-w-11 px-2 flex items-center justify-center rounded-full text-sm font-600 text-white transition-colors hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80\"\n @click=\"zoomOut\"\n >\n −\n </button>\n\n <button\n type=\"button\"\n data-ds-image-viewer-zoom-reset\n :aria-label=\"resetZoomLabel\"\n class=\"h-11 min-w-11 px-3 flex items-center justify-center rounded-full text-xs font-700 text-white transition-colors hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80\"\n @click=\"resetTransform\"\n >\n 100%\n </button>\n\n <button\n type=\"button\"\n data-ds-image-viewer-zoom-in\n :aria-label=\"zoomInLabel\"\n class=\"h-11 min-w-11 px-2 flex items-center justify-center rounded-full text-sm font-600 text-white transition-colors hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80\"\n @click=\"zoomIn\"\n >\n +\n </button>\n\n <template v-if=\"$slots['toolbar-actions']\">\n <div class=\"mx-1 h-6 w-px bg-white/25\" aria-hidden=\"true\" />\n\n <slot\n name=\"toolbar-actions\"\n v-bind=\"toolbarSlotProps\"\n />\n\n <div class=\"mx-1 h-6 w-px bg-white/25\" aria-hidden=\"true\" />\n </template>\n\n <div v-else class=\"mx-1 h-6 w-px bg-white/25\" aria-hidden=\"true\" />\n\n <button\n type=\"button\"\n data-ds-image-viewer-rotate-left\n :aria-label=\"rotateLeftLabel\"\n class=\"h-11 min-w-11 px-2 flex items-center justify-center rounded-full text-sm font-600 text-white transition-colors hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80\"\n @click=\"rotateLeft\"\n >\n ↺\n </button>\n\n <button\n type=\"button\"\n data-ds-image-viewer-rotate-right\n :aria-label=\"rotateRightLabel\"\n class=\"h-11 min-w-11 px-2 flex items-center justify-center rounded-full text-sm font-600 text-white transition-colors hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80\"\n @click=\"rotateRight\"\n >\n ↻\n </button>\n </div>\n </slot>\n </div>\n </div>\n </DialogPanel>\n </TransitionChild>\n </div>\n </Dialog>\n </TransitionRoot>\n </teleport>\n</template>"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
- import { t as GrImageViewer_default } from "./chunks/GrImageViewer-Cn1X-sWS.js";
1
+ import { t as GrImageViewer_default } from "./chunks/GrImageViewer-CsvXQHM9.js";
2
2
  import { t as grImageViewerConfig } from "../../chunks/config-CvGdaXvH.js";
3
3
  export { GrImageViewer_default as GrImageViewer, GrImageViewer_default as default, grImageViewerConfig };
@@ -2,13 +2,13 @@ import { a as Ve, i as Je, n as he, o as Ye, r as Ge, t as Se } from "../../../c
2
2
  import { c as panelTransition, d as shell, i as overlayTransition, n as layout, r as overlay, t as getGrModalPanelClass, u as root } from "../../../chunks/grModalStyles-DS1Vl6EW.js";
3
3
  import { Teleport, computed, createBlock, createCommentVNode, createElementVNode, createVNode, defineComponent, normalizeClass, onBeforeUnmount, openBlock, ref, renderSlot, unref, watch, withCtx } from "vue";
4
4
  //#region src/components/GrModal/grModalEscStack.ts
5
- var stack = [];
5
+ var stack$1 = [];
6
6
  var listening = false;
7
- var nextId = 1;
7
+ var nextId$1 = 1;
8
8
  function handleKeydown(event) {
9
9
  if (event.key !== "Escape") return;
10
- if (stack.length === 0) return;
11
- const top = stack[stack.length - 1];
10
+ if (stack$1.length === 0) return;
11
+ const top = stack$1[stack$1.length - 1];
12
12
  event.preventDefault();
13
13
  event.stopPropagation();
14
14
  if (typeof event.stopImmediatePropagation === "function") event.stopImmediatePropagation();
@@ -28,8 +28,8 @@ function stopListening() {
28
28
  }
29
29
  /** Регистрирует открытую модалку как верхнюю и возвращает её id. */
30
30
  function pushGrModalEsc(entry) {
31
- const id = nextId++;
32
- stack.push({
31
+ const id = nextId$1++;
32
+ stack$1.push({
33
33
  ...entry,
34
34
  id
35
35
  });
@@ -38,9 +38,34 @@ function pushGrModalEsc(entry) {
38
38
  }
39
39
  /** Снимает модалку со стека (при закрытии или размонтировании). */
40
40
  function removeGrModalEsc(id) {
41
+ const index = stack$1.findIndex((item) => item.id === id);
42
+ if (index >= 0) stack$1.splice(index, 1);
43
+ if (stack$1.length === 0) stopListening();
44
+ }
45
+ //#endregion
46
+ //#region src/components/GrModal/grModalTopStack.ts
47
+ var stack = [];
48
+ var nextId = 1;
49
+ /** Пересчитывает «верхнее» окно и уведомляет все зарегистрированные модалки. */
50
+ function sync() {
51
+ const lastIndex = stack.length - 1;
52
+ stack.forEach((entry, index) => entry.setTopmost(index === lastIndex));
53
+ }
54
+ /** Регистрирует открытую модалку как верхнюю и возвращает её id. */
55
+ function pushGrModalTop(entry) {
56
+ const id = nextId++;
57
+ stack.push({
58
+ ...entry,
59
+ id
60
+ });
61
+ sync();
62
+ return id;
63
+ }
64
+ /** Снимает модалку со стека (при закрытии или размонтировании). */
65
+ function removeGrModalTop(id) {
41
66
  const index = stack.findIndex((item) => item.id === id);
42
67
  if (index >= 0) stack.splice(index, 1);
43
- if (stack.length === 0) stopListening();
68
+ sync();
44
69
  }
45
70
  //#endregion
46
71
  //#region src/components/GrModal/GrModal.vue
@@ -83,6 +108,9 @@ var GrModal_default = /* @__PURE__ */ defineComponent({
83
108
  const props = __props;
84
109
  const emit = __emit;
85
110
  const panelClass = computed(() => getGrModalPanelClass(props.size));
111
+ const panelRef = ref(null);
112
+ const isTopmost = ref(true);
113
+ const inertAttr = computed(() => props.modelValue && !isTopmost.value ? "" : void 0);
86
114
  const isClient = typeof window !== "undefined";
87
115
  const closeReason = ref(null);
88
116
  function close() {
@@ -101,6 +129,7 @@ var GrModal_default = /* @__PURE__ */ defineComponent({
101
129
  if (event.key === "Escape") closeReason.value = "esc";
102
130
  }
103
131
  let escEntryId = null;
132
+ let topEntryId = null;
104
133
  function registerEsc() {
105
134
  if (escEntryId !== null) return;
106
135
  escEntryId = pushGrModalEsc({
@@ -113,6 +142,18 @@ var GrModal_default = /* @__PURE__ */ defineComponent({
113
142
  removeGrModalEsc(escEntryId);
114
143
  escEntryId = null;
115
144
  }
145
+ function registerTop() {
146
+ if (topEntryId !== null) return;
147
+ topEntryId = pushGrModalTop({ setTopmost: (value) => {
148
+ isTopmost.value = value;
149
+ } });
150
+ }
151
+ function unregisterTop() {
152
+ if (topEntryId === null) return;
153
+ removeGrModalTop(topEntryId);
154
+ topEntryId = null;
155
+ isTopmost.value = true;
156
+ }
116
157
  function onOverlayPointerDown() {
117
158
  closeReason.value = "backdrop";
118
159
  }
@@ -133,14 +174,17 @@ var GrModal_default = /* @__PURE__ */ defineComponent({
133
174
  if (value) {
134
175
  lockBodyScroll();
135
176
  registerEsc();
177
+ registerTop();
136
178
  } else {
137
179
  unlockBodyScroll();
138
180
  unregisterEsc();
181
+ unregisterTop();
139
182
  }
140
183
  }, { immediate: true });
141
184
  onBeforeUnmount(() => {
142
185
  unlockBodyScroll();
143
186
  unregisterEsc();
187
+ unregisterTop();
144
188
  });
145
189
  return (_ctx, _cache) => {
146
190
  return openBlock(), createBlock(Teleport, {
@@ -153,6 +197,8 @@ var GrModal_default = /* @__PURE__ */ defineComponent({
153
197
  default: withCtx(() => [createVNode(unref(Ye), {
154
198
  as: "div",
155
199
  class: normalizeClass(unref(root)),
200
+ "initial-focus": panelRef.value,
201
+ inert: inertAttr.value,
156
202
  onClose: onDialogClose,
157
203
  onKeydownCapture: onRootKeydown
158
204
  }, {
@@ -189,6 +235,8 @@ var GrModal_default = /* @__PURE__ */ defineComponent({
189
235
  "leave-to": unref(panelTransition).leaveTo
190
236
  }, {
191
237
  default: withCtx(() => [createVNode(unref(Ge), {
238
+ ref_key: "panelRef",
239
+ ref: panelRef,
192
240
  "data-ds-modal-panel": "",
193
241
  tabindex: "-1",
194
242
  class: normalizeClass(panelClass.value)
@@ -224,7 +272,11 @@ var GrModal_default = /* @__PURE__ */ defineComponent({
224
272
  "leave-to"
225
273
  ])], 2)], 2)]),
226
274
  _: 3
227
- }, 8, ["class"])]),
275
+ }, 8, [
276
+ "class",
277
+ "initial-focus",
278
+ "inert"
279
+ ])]),
228
280
  _: 3
229
281
  }, 8, ["show"])], 8, ["disabled"]);
230
282
  };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GrModal-lh0lOmoi.js","names":["$slots"],"sources":["../../../../src/components/GrModal/grModalEscStack.ts","../../../../src/components/GrModal/grModalTopStack.ts","../../../../src/components/GrModal/GrModal.vue","../../../../src/components/GrModal/GrModal.vue"],"sourcesContent":["/**\n * grModalEscStack — общий стек открытых `GrModal` для корректной обработки Esc.\n *\n * Зачем нужен:\n * HeadlessUI вешает обработчик Escape на `window` у каждого открытого `<Dialog>`.\n * Когда модалки рендерятся в разных деревьях приложения (например, диалоги\n * `useDialogService` монтируются отдельным `render()` в `document.body`), их\n * HeadlessUI-стек не общий, и каждый `<Dialog>` считает себя верхним. В итоге\n * Esc перехватывает обработчик той модалки, что смонтирована раньше (нижней),\n * а реально верхнее окно (последнее открытое) остаётся.\n *\n * Решение: единый capture-обработчик на `window`, который срабатывает раньше\n * обработчиков HeadlessUI, гасит событие (`stopImmediatePropagation`) и\n * закрывает только верхнюю (последнюю зарегистрированную) модалку. Так Esc\n * всегда адресуется верхнему окну независимо от дерева рендера.\n */\n\nexport interface GrModalEscEntry {\n /** Уникальный id зарегистрированной модалки. */\n id: number\n /** Должна ли модалка закрываться по Esc (реактивный геттер `closeOnEsc`). */\n shouldClose: () => boolean\n /** Закрыть модалку (эмит `update:modelValue=false`). */\n close: () => void\n}\n\nconst stack: GrModalEscEntry[] = []\nlet listening = false\nlet nextId = 1\n\nfunction handleKeydown(event: KeyboardEvent): void {\n if (event.key !== 'Escape') return\n if (stack.length === 0) return\n\n // Верхняя модалка — последняя зарегистрированная.\n const top = stack[stack.length - 1]\n\n // Перехватываем Esc раньше HeadlessUI и нижних модалок: даже если верхнее\n // окно не закрывается (`closeOnEsc=false`), событие не должно «проваливаться»\n // к нижним окнам.\n event.preventDefault()\n event.stopPropagation()\n if (typeof event.stopImmediatePropagation === 'function')\n event.stopImmediatePropagation()\n\n if (top.shouldClose()) top.close()\n}\n\nfunction startListening(): void {\n if (listening) return\n if (typeof window === 'undefined') return\n // Capture-фаза, чтобы опередить window-обработчик Escape HeadlessUI.\n window.addEventListener('keydown', handleKeydown, true)\n listening = true\n}\n\nfunction stopListening(): void {\n if (!listening) return\n if (typeof window === 'undefined') return\n window.removeEventListener('keydown', handleKeydown, true)\n listening = false\n}\n\n/** Регистрирует открытую модалку как верхнюю и возвращает её id. */\nexport function pushGrModalEsc(entry: Omit<GrModalEscEntry, 'id'>): number {\n const id = nextId++\n stack.push({ ...entry, id })\n startListening()\n return id\n}\n\n/** Снимает модалку со стека (при закрытии или размонтировании). */\nexport function removeGrModalEsc(id: number): void {\n const index = stack.findIndex(item => item.id === id)\n if (index >= 0) stack.splice(index, 1)\n if (stack.length === 0) stopListening()\n}\n\n/** Тестовая/служебная очистка стека. */\nexport function resetGrModalEscStack(): void {\n stack.splice(0, stack.length)\n stopListening()\n}\n","/**\n * grModalTopStack — общий стек открытых `GrModal` для вычисления «верхнего»\n * (последнего открытого) окна независимо от дерева рендера.\n *\n * Зачем нужен:\n * HeadlessUI ловит фокус (`FocusTrap` с `FocusLock`) у каждого открытого\n * `<Dialog>` и помечает остальную часть страницы как inert. Когда модалки\n * рендерятся в разных деревьях (например, диалоги `useDialogService`\n * монтируются отдельным `render()` в `document.body`), их HeadlessUI-стек не\n * общий: каждый `<Dialog>` считает себя верхним и продолжает удерживать фокус.\n * В итоге нижнее окно «ворует» фокус у верхнего диалога — нельзя, например,\n * сфокусировать input в открытом поверх `prompt`.\n *\n * Решение: единый стек регистраций. Верхней считается последняя\n * зарегистрированная модалка. Все нижние окна уведомляются (`setTopmost(false)`)\n * и помечают свой корень `inert`, освобождая фокус для верхнего окна. Когда\n * верхнее окно закрывается, следующая по стеку модалка снова становится\n * верхней (`setTopmost(true)`) и снимает `inert`.\n */\n\nexport interface GrModalTopEntry {\n /** Уникальный id зарегистрированной модалки. */\n id: number\n /** Колбэк-уведомление: является ли модалка сейчас верхней. */\n setTopmost: (isTopmost: boolean) => void\n}\n\nconst stack: GrModalTopEntry[] = []\nlet nextId = 1\n\n/** Пересчитывает «верхнее» окно и уведомляет все зарегистрированные модалки. */\nfunction sync(): void {\n const lastIndex = stack.length - 1\n stack.forEach((entry, index) => entry.setTopmost(index === lastIndex))\n}\n\n/** Регистрирует открытую модалку как верхнюю и возвращает её id. */\nexport function pushGrModalTop(entry: Omit<GrModalTopEntry, 'id'>): number {\n const id = nextId++\n stack.push({ ...entry, id })\n sync()\n return id\n}\n\n/** Снимает модалку со стека (при закрытии или размонтировании). */\nexport function removeGrModalTop(id: number): void {\n const index = stack.findIndex(item => item.id === id)\n if (index >= 0) stack.splice(index, 1)\n sync()\n}\n\n/** Тестовая/служебная очистка стека. */\nexport function resetGrModalTopStack(): void {\n stack.splice(0, stack.length)\n}\n","<script setup lang=\"ts\">\n/**\n * GrModal — DS-примитив модального окна на базе `@headlessui/vue`.\n *\n * Публичный API:\n * - `modelValue` (v-model) — открыто/закрыто;\n * - `size` — размер панели (`sm | md | lg | xl | full`);\n * - `closeOnBackdrop` (default: true) — закрывать при клике по оверлею;\n * - `closeOnEsc` (default: true) — закрывать по Esc;\n * - слоты `#title` / `#description` — если переданы, оборачиваются в\n * `DialogTitle` / `DialogDescription` (связь через `aria-labelledby` /\n * `aria-describedby` ставится HeadlessUI автоматически).\n *\n * Esc обрабатывается через общий стек открытых модалок (`grModalEscStack`):\n * единый capture-обработчик на `window` закрывает только верхнюю (последнюю\n * открытую) модалку и опережает window-обработчик Escape HeadlessUI. Это\n * чинит кейс, когда поверх `GrModal` открыт диалог `useDialogService` (другое\n * дерево рендера): Esc закрывает именно верхний диалог, а не нижнее окно.\n * Клик по оверлею (backdrop) по-прежнему идёт через `@close` HeadlessUI;\n * источник закрытия различается через ref `closeReason`.\n */\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { Dialog, DialogDescription, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue'\n\nimport { pushGrModalEsc, removeGrModalEsc } from './grModalEscStack'\nimport { pushGrModalTop, removeGrModalTop } from './grModalTopStack'\n\nimport {\n type GrModalSize,\n getGrModalPanelClass,\n layout,\n overlay as overlayClass,\n overlayTransition,\n panelTransition,\n root as rootClass,\n shell,\n} from './grModalStyles'\n\nexport interface GrModalProps {\n modelValue: boolean\n closeOnBackdrop?: boolean\n closeOnEsc?: boolean\n size?: GrModalSize\n}\n\nconst props = withDefaults(defineProps<GrModalProps>(), {\n closeOnBackdrop: true,\n closeOnEsc: true,\n size: 'md',\n})\n\nconst emit = defineEmits<{\n (e: 'update:modelValue', value: boolean): void\n}>()\n\nconst panelClass = computed(() => getGrModalPanelClass(props.size))\n\n// Ссылка на панель используется как `initialFocus` для HeadlessUI `<Dialog>`:\n// панель имеет `tabindex=\"-1\"`, поэтому всегда фокусируема программно. Это\n// убирает предупреждение «There are no focusable elements inside the\n// <FocusTrap />», которое HeadlessUI выводит, когда не находит фокусируемого\n// элемента (в т.ч. когда контент окна временно помечен `inert`).\nconst panelRef = ref<HTMLElement | null>(null)\n\n// Является ли это окно верхним (последним открытым) в общем стеке модалок.\n// Когда поверх открыт другой `GrModal`/диалог сервиса, окно перестаёт быть\n// верхним и помечается `inert`, чтобы не «воровать» фокус у верхнего окна.\nconst isTopmost = ref(true)\n\n// `inert` ставится только на нижние (не верхние) открытые окна. Inert-поддерево\n// не интерактивно и не фокусируемо, поэтому FocusLock нижнего `<Dialog>`\n// перестаёт возвращать фокус себе — верхний диалог удерживает фокус сам.\nconst inertAttr = computed(() => (props.modelValue && !isTopmost.value ? '' : undefined))\n\n// SSR-guard: на сервере `document.body` недоступен — отключаем teleport,\n// а в клиенте включаем после маунта.\nconst isClient = typeof window !== 'undefined'\n\n// Источник закрытия от HeadlessUI `<Dialog>` — либо Esc, либо клик по оверлею.\n// Различаем их, чтобы `closeOnBackdrop` и `closeOnEsc` работали независимо.\nconst closeReason = ref<'backdrop' | 'esc' | null>(null)\n\nfunction close(): void {\n emit('update:modelValue', false)\n}\n\nfunction onDialogClose(): void {\n const reason = closeReason.value\n closeReason.value = null\n\n if (reason === 'esc') {\n if (props.closeOnEsc) close()\n return\n }\n\n // По умолчанию (клик по оверлею или программный вызов `@close`)\n // уважаем `closeOnBackdrop`.\n if (props.closeOnBackdrop) close()\n}\n\nfunction onRootKeydown(event: KeyboardEvent): void {\n if (event.key === 'Escape') closeReason.value = 'esc'\n}\n\n// ————— Esc-стек: гарантирует, что Esc закрывает именно верхнюю (последнюю\n// открытую) модалку, даже если окна живут в разных деревьях рендера\n// (например, диалоги `useDialogService` поверх `GrModal`).\nlet escEntryId: number | null = null\nlet topEntryId: number | null = null\n\nfunction registerEsc(): void {\n if (escEntryId !== null) return\n escEntryId = pushGrModalEsc({\n shouldClose: () => props.closeOnEsc,\n close,\n })\n}\n\nfunction unregisterEsc(): void {\n if (escEntryId === null) return\n removeGrModalEsc(escEntryId)\n escEntryId = null\n}\n\nfunction registerTop(): void {\n if (topEntryId !== null) return\n topEntryId = pushGrModalTop({\n setTopmost: (value) => { isTopmost.value = value },\n })\n}\n\nfunction unregisterTop(): void {\n if (topEntryId === null) return\n removeGrModalTop(topEntryId)\n topEntryId = null\n isTopmost.value = true\n}\n\nfunction onOverlayPointerDown(): void {\n closeReason.value = 'backdrop'\n}\n\n// ————— Scroll lock на `<body>` на время открытия.\n// HeadlessUI Vue этого не делает автоматически, а фон скроллится —\n// для DS-примитива это мешающий UX.\nlet savedBodyOverflow: string | null = null\n\nfunction lockBodyScroll(): void {\n if (!isClient) return\n if (savedBodyOverflow !== null) return\n savedBodyOverflow = document.body.style.overflow\n document.body.style.overflow = 'hidden'\n}\n\nfunction unlockBodyScroll(): void {\n if (!isClient) return\n if (savedBodyOverflow === null) return\n document.body.style.overflow = savedBodyOverflow\n savedBodyOverflow = null\n}\n\nwatch(\n () => props.modelValue,\n (value) => {\n if (value) {\n lockBodyScroll()\n registerEsc()\n registerTop()\n }\n else {\n unlockBodyScroll()\n unregisterEsc()\n unregisterTop()\n }\n },\n { immediate: true },\n)\n\nonBeforeUnmount(() => {\n unlockBodyScroll()\n unregisterEsc()\n unregisterTop()\n})\n\n</script>\n\n<template>\n <teleport to=\"body\" :disabled=\"!isClient\">\n <TransitionRoot :show=\"modelValue\" as=\"template\">\n <Dialog\n as=\"div\"\n :class=\"rootClass\"\n :initial-focus=\"panelRef\"\n :inert=\"inertAttr\"\n @close=\"onDialogClose\"\n @keydown.capture=\"onRootKeydown\"\n >\n <div :class=\"shell\">\n <div :class=\"layout\">\n <TransitionChild\n as=\"template\"\n :enter=\"overlayTransition.enter\"\n :enter-from=\"overlayTransition.enterFrom\"\n :enter-to=\"overlayTransition.enterTo\"\n :leave=\"overlayTransition.leave\"\n :leave-from=\"overlayTransition.leaveFrom\"\n :leave-to=\"overlayTransition.leaveTo\"\n >\n <div\n data-ds-modal-overlay\n :class=\"overlayClass\"\n aria-hidden=\"true\"\n @pointerdown=\"onOverlayPointerDown\"\n />\n </TransitionChild>\n\n <TransitionChild\n as=\"template\"\n :enter=\"panelTransition.enter\"\n :enter-from=\"panelTransition.enterFrom\"\n :enter-to=\"panelTransition.enterTo\"\n :leave=\"panelTransition.leave\"\n :leave-from=\"panelTransition.leaveFrom\"\n :leave-to=\"panelTransition.leaveTo\"\n >\n <DialogPanel\n ref=\"panelRef\"\n data-ds-modal-panel\n tabindex=\"-1\"\n :class=\"panelClass\"\n >\n <DialogTitle v-if=\"$slots.title\" as=\"div\" data-ds-modal-title>\n <slot name=\"title\" />\n </DialogTitle>\n <DialogDescription v-if=\"$slots.description\" as=\"div\" data-ds-modal-description>\n <slot name=\"description\" />\n </DialogDescription>\n <slot />\n </DialogPanel>\n </TransitionChild>\n </div>\n </div>\n </Dialog>\n </TransitionRoot>\n </teleport>\n</template>\n","<script setup lang=\"ts\">\n/**\n * GrModal — DS-примитив модального окна на базе `@headlessui/vue`.\n *\n * Публичный API:\n * - `modelValue` (v-model) — открыто/закрыто;\n * - `size` — размер панели (`sm | md | lg | xl | full`);\n * - `closeOnBackdrop` (default: true) — закрывать при клике по оверлею;\n * - `closeOnEsc` (default: true) — закрывать по Esc;\n * - слоты `#title` / `#description` — если переданы, оборачиваются в\n * `DialogTitle` / `DialogDescription` (связь через `aria-labelledby` /\n * `aria-describedby` ставится HeadlessUI автоматически).\n *\n * Esc обрабатывается через общий стек открытых модалок (`grModalEscStack`):\n * единый capture-обработчик на `window` закрывает только верхнюю (последнюю\n * открытую) модалку и опережает window-обработчик Escape HeadlessUI. Это\n * чинит кейс, когда поверх `GrModal` открыт диалог `useDialogService` (другое\n * дерево рендера): Esc закрывает именно верхний диалог, а не нижнее окно.\n * Клик по оверлею (backdrop) по-прежнему идёт через `@close` HeadlessUI;\n * источник закрытия различается через ref `closeReason`.\n */\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { Dialog, DialogDescription, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue'\n\nimport { pushGrModalEsc, removeGrModalEsc } from './grModalEscStack'\nimport { pushGrModalTop, removeGrModalTop } from './grModalTopStack'\n\nimport {\n type GrModalSize,\n getGrModalPanelClass,\n layout,\n overlay as overlayClass,\n overlayTransition,\n panelTransition,\n root as rootClass,\n shell,\n} from './grModalStyles'\n\nexport interface GrModalProps {\n modelValue: boolean\n closeOnBackdrop?: boolean\n closeOnEsc?: boolean\n size?: GrModalSize\n}\n\nconst props = withDefaults(defineProps<GrModalProps>(), {\n closeOnBackdrop: true,\n closeOnEsc: true,\n size: 'md',\n})\n\nconst emit = defineEmits<{\n (e: 'update:modelValue', value: boolean): void\n}>()\n\nconst panelClass = computed(() => getGrModalPanelClass(props.size))\n\n// Ссылка на панель используется как `initialFocus` для HeadlessUI `<Dialog>`:\n// панель имеет `tabindex=\"-1\"`, поэтому всегда фокусируема программно. Это\n// убирает предупреждение «There are no focusable elements inside the\n// <FocusTrap />», которое HeadlessUI выводит, когда не находит фокусируемого\n// элемента (в т.ч. когда контент окна временно помечен `inert`).\nconst panelRef = ref<HTMLElement | null>(null)\n\n// Является ли это окно верхним (последним открытым) в общем стеке модалок.\n// Когда поверх открыт другой `GrModal`/диалог сервиса, окно перестаёт быть\n// верхним и помечается `inert`, чтобы не «воровать» фокус у верхнего окна.\nconst isTopmost = ref(true)\n\n// `inert` ставится только на нижние (не верхние) открытые окна. Inert-поддерево\n// не интерактивно и не фокусируемо, поэтому FocusLock нижнего `<Dialog>`\n// перестаёт возвращать фокус себе — верхний диалог удерживает фокус сам.\nconst inertAttr = computed(() => (props.modelValue && !isTopmost.value ? '' : undefined))\n\n// SSR-guard: на сервере `document.body` недоступен — отключаем teleport,\n// а в клиенте включаем после маунта.\nconst isClient = typeof window !== 'undefined'\n\n// Источник закрытия от HeadlessUI `<Dialog>` — либо Esc, либо клик по оверлею.\n// Различаем их, чтобы `closeOnBackdrop` и `closeOnEsc` работали независимо.\nconst closeReason = ref<'backdrop' | 'esc' | null>(null)\n\nfunction close(): void {\n emit('update:modelValue', false)\n}\n\nfunction onDialogClose(): void {\n const reason = closeReason.value\n closeReason.value = null\n\n if (reason === 'esc') {\n if (props.closeOnEsc) close()\n return\n }\n\n // По умолчанию (клик по оверлею или программный вызов `@close`)\n // уважаем `closeOnBackdrop`.\n if (props.closeOnBackdrop) close()\n}\n\nfunction onRootKeydown(event: KeyboardEvent): void {\n if (event.key === 'Escape') closeReason.value = 'esc'\n}\n\n// ————— Esc-стек: гарантирует, что Esc закрывает именно верхнюю (последнюю\n// открытую) модалку, даже если окна живут в разных деревьях рендера\n// (например, диалоги `useDialogService` поверх `GrModal`).\nlet escEntryId: number | null = null\nlet topEntryId: number | null = null\n\nfunction registerEsc(): void {\n if (escEntryId !== null) return\n escEntryId = pushGrModalEsc({\n shouldClose: () => props.closeOnEsc,\n close,\n })\n}\n\nfunction unregisterEsc(): void {\n if (escEntryId === null) return\n removeGrModalEsc(escEntryId)\n escEntryId = null\n}\n\nfunction registerTop(): void {\n if (topEntryId !== null) return\n topEntryId = pushGrModalTop({\n setTopmost: (value) => { isTopmost.value = value },\n })\n}\n\nfunction unregisterTop(): void {\n if (topEntryId === null) return\n removeGrModalTop(topEntryId)\n topEntryId = null\n isTopmost.value = true\n}\n\nfunction onOverlayPointerDown(): void {\n closeReason.value = 'backdrop'\n}\n\n// ————— Scroll lock на `<body>` на время открытия.\n// HeadlessUI Vue этого не делает автоматически, а фон скроллится —\n// для DS-примитива это мешающий UX.\nlet savedBodyOverflow: string | null = null\n\nfunction lockBodyScroll(): void {\n if (!isClient) return\n if (savedBodyOverflow !== null) return\n savedBodyOverflow = document.body.style.overflow\n document.body.style.overflow = 'hidden'\n}\n\nfunction unlockBodyScroll(): void {\n if (!isClient) return\n if (savedBodyOverflow === null) return\n document.body.style.overflow = savedBodyOverflow\n savedBodyOverflow = null\n}\n\nwatch(\n () => props.modelValue,\n (value) => {\n if (value) {\n lockBodyScroll()\n registerEsc()\n registerTop()\n }\n else {\n unlockBodyScroll()\n unregisterEsc()\n unregisterTop()\n }\n },\n { immediate: true },\n)\n\nonBeforeUnmount(() => {\n unlockBodyScroll()\n unregisterEsc()\n unregisterTop()\n})\n\n</script>\n\n<template>\n <teleport to=\"body\" :disabled=\"!isClient\">\n <TransitionRoot :show=\"modelValue\" as=\"template\">\n <Dialog\n as=\"div\"\n :class=\"rootClass\"\n :initial-focus=\"panelRef\"\n :inert=\"inertAttr\"\n @close=\"onDialogClose\"\n @keydown.capture=\"onRootKeydown\"\n >\n <div :class=\"shell\">\n <div :class=\"layout\">\n <TransitionChild\n as=\"template\"\n :enter=\"overlayTransition.enter\"\n :enter-from=\"overlayTransition.enterFrom\"\n :enter-to=\"overlayTransition.enterTo\"\n :leave=\"overlayTransition.leave\"\n :leave-from=\"overlayTransition.leaveFrom\"\n :leave-to=\"overlayTransition.leaveTo\"\n >\n <div\n data-ds-modal-overlay\n :class=\"overlayClass\"\n aria-hidden=\"true\"\n @pointerdown=\"onOverlayPointerDown\"\n />\n </TransitionChild>\n\n <TransitionChild\n as=\"template\"\n :enter=\"panelTransition.enter\"\n :enter-from=\"panelTransition.enterFrom\"\n :enter-to=\"panelTransition.enterTo\"\n :leave=\"panelTransition.leave\"\n :leave-from=\"panelTransition.leaveFrom\"\n :leave-to=\"panelTransition.leaveTo\"\n >\n <DialogPanel\n ref=\"panelRef\"\n data-ds-modal-panel\n tabindex=\"-1\"\n :class=\"panelClass\"\n >\n <DialogTitle v-if=\"$slots.title\" as=\"div\" data-ds-modal-title>\n <slot name=\"title\" />\n </DialogTitle>\n <DialogDescription v-if=\"$slots.description\" as=\"div\" data-ds-modal-description>\n <slot name=\"description\" />\n </DialogDescription>\n <slot />\n </DialogPanel>\n </TransitionChild>\n </div>\n </div>\n </Dialog>\n </TransitionRoot>\n </teleport>\n</template>\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { t as GrModal_default } from "./chunks/GrModal-2ALC3kwy.js";
1
+ import { t as GrModal_default } from "./chunks/GrModal-lh0lOmoi.js";
2
2
  import { t as getGrModalPanelClass } from "../../chunks/grModalStyles-DS1Vl6EW.js";
3
3
  import { n as grModalClassTokens, r as grModalSafelist, t as grModalConfig } from "../../chunks/config-C5OwJM3-.js";
4
4
  export { GrModal_default as GrModal, GrModal_default as default, getGrModalPanelClass, grModalClassTokens, grModalConfig, grModalSafelist };
@@ -1,5 +1,5 @@
1
1
  import { t as GrButton_default } from "../../GrButton/chunks/GrButton-CPDKonoB.js";
2
- import { t as GrDialog_default } from "../../GrDialog/chunks/GrDialog-SYcF-4op.js";
2
+ import { t as GrDialog_default } from "../../GrDialog/chunks/GrDialog-C7XLxp8e.js";
3
3
  import { t as GrResponseErrorBanner_default } from "../../GrResponseErrorBanner/chunks/GrResponseErrorBanner-CzdTh4gd.js";
4
4
  import { t as GrFormField_default } from "../../GrFormField/chunks/GrFormField-U7c_aDPJ.js";
5
5
  import { t as GrInput_default } from "../../GrInput/chunks/GrInput-gwFQ1sk4.js";
@@ -1 +1 @@
1
- {"version":3,"file":"GrPromptDialog-23x2nZvv.js","names":[],"sources":["../../../../src/components/GrPromptDialog/GrPromptDialog.vue","../../../../src/components/GrPromptDialog/GrPromptDialog.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * GrPromptDialog — DS-примитив диалога ввода значения поверх `GrDialog`.\n *\n * Синхронизирован с `GrDialog` по набору проп-проксирования:\n * `size`, `closeOnBackdrop`, `closeOnEsc`, `showHeader`, `showCloseButton`,\n * `headerConfig`, `footerConfig`, `bodyConfig`, `closeLabel`.\n *\n * Поведение `required=true` (по умолчанию): пустое значение подсвечивается\n * ошибкой только после первого «касания» (blur или попытка confirm),\n * чтобы не шуметь при открытии.\n */\nimport { computed, ref, watch } from 'vue'\n\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrDialog from '../GrDialog/GrDialog.vue'\nimport GrFormField from '../GrFormField/GrFormField.vue'\nimport GrInput from '../GrInput/GrInput.vue'\nimport GrResponseErrorBanner from '../GrResponseErrorBanner/GrResponseErrorBanner.vue'\nimport type { GrButtonSize, GrButtonTone, GrButtonVariant } from '../GrButton'\nimport type { GrDialogSectionConfig, GrDialogSize } from '../GrDialog'\nimport type { ResponseErrorInfo } from '../GrResponseErrorBanner'\n\nexport interface GrPromptDialogProps {\n modelValue: boolean\n value: string\n title?: string\n description?: string\n label?: string\n placeholder?: string\n closeOnBackdrop?: boolean\n closeOnEsc?: boolean\n showHeader?: boolean\n showCloseButton?: boolean\n size?: GrDialogSize\n headerConfig?: GrDialogSectionConfig\n footerConfig?: GrDialogSectionConfig\n bodyConfig?: GrDialogSectionConfig\n /** A11y-лейбл кнопки закрытия (i18n). */\n closeLabel?: string\n buttonSize?: GrButtonSize\n confirmText?: string\n cancelText?: string\n /** Текст ошибки для пустого значения при `required=true` (i18n). */\n requiredErrorText?: string\n confirmVariant?: GrButtonVariant\n confirmTone?: GrButtonTone\n required?: boolean\n /**\n * Структура ошибки ответа сервера для показа общим блоком в теле диалога\n * (через `GrResponseErrorBanner`). `null` — блок скрыт.\n */\n error?: ResponseErrorInfo | null\n /**\n * Внешняя ошибка поля ввода (например, серверная валидация). Имеет приоритет\n * над встроенной проверкой `required`. `null`/`undefined` — нет внешней ошибки.\n */\n fieldError?: string | null\n /** Состояние загрузки кнопки Confirm (async-`onConfirm` in-flight). */\n confirmLoading?: boolean\n /** Принудительно дизейблит кнопку Confirm. */\n confirmDisabled?: boolean\n /**\n * Закрывать ли диалог автоматически по клику Confirm. По умолчанию `true`.\n * `false` — отдаёт управление закрытием наружу (нужно `useDialogService`).\n */\n closeOnConfirm?: boolean\n}\n\nconst props = withDefaults(defineProps<GrPromptDialogProps>(), {\n title: 'Prompt',\n description: undefined,\n label: 'Value',\n placeholder: undefined,\n closeOnBackdrop: true,\n closeOnEsc: true,\n showHeader: true,\n showCloseButton: true,\n size: 'md',\n headerConfig: undefined,\n footerConfig: undefined,\n bodyConfig: undefined,\n closeLabel: 'Close',\n buttonSize: undefined,\n confirmText: 'Confirm',\n cancelText: 'Cancel',\n requiredErrorText: 'Enter a value.',\n confirmVariant: 'primary',\n confirmTone: 'primary',\n required: true,\n error: null,\n fieldError: null,\n confirmLoading: false,\n confirmDisabled: false,\n closeOnConfirm: true,\n})\n\nconst emit = defineEmits<{\n (e: 'update:modelValue', value: boolean): void\n (e: 'update:value', value: string): void\n (e: 'confirm', value: string): void\n (e: 'cancel'): void\n}>()\n\nconst open = computed({\n get: () => props.modelValue,\n set: (v: boolean) => emit('update:modelValue', v),\n})\n\nconst valueModel = computed({\n get: () => props.value,\n set: (v: string) => emit('update:value', v),\n})\n\nconst touched = ref(false)\nconst inputRef = ref<InstanceType<typeof GrInput> | null>(null)\n\nwatch(\n () => props.modelValue,\n (v) => {\n if (v) touched.value = false\n },\n)\n\nconst canConfirm = computed(() => {\n if (!props.required) return true\n return valueModel.value.trim().length > 0\n})\n\nconst validationError = computed(() => {\n if (!props.required) return undefined\n if (!touched.value) return undefined\n return valueModel.value.trim().length > 0\n ? undefined\n : props.requiredErrorText\n})\n\n// Внешняя ошибка поля (серверная валидация) имеет приоритет над встроенной.\nconst fieldErrorMessage = computed(() => props.fieldError ?? validationError.value ?? undefined)\n\nfunction onCancel(): void {\n emit('cancel')\n emit('update:modelValue', false)\n}\n\nfunction onConfirm(): void {\n if (!canConfirm.value) {\n touched.value = true\n inputRef.value?.focus()\n return\n }\n\n emit('confirm', valueModel.value)\n if (props.closeOnConfirm)\n emit('update:modelValue', false)\n}\n</script>\n\n<template>\n <GrDialog\n v-model=\"open\"\n :title=\"title\"\n :size=\"size\"\n :close-on-backdrop=\"closeOnBackdrop\"\n :close-on-esc=\"closeOnEsc\"\n :show-header=\"showHeader\"\n :show-close-button=\"showCloseButton\"\n :header-config=\"headerConfig\"\n :footer-config=\"footerConfig\"\n :body-config=\"bodyConfig\"\n :close-label=\"closeLabel\"\n >\n <div class=\"grid gap-4\">\n <slot>\n <div v-if=\"description\" class=\"text-[14px] text-[var(--muted-fg)]\">\n {{ description }}\n </div>\n </slot>\n\n <GrFormField :label=\"label\" :error=\"fieldErrorMessage\" for-id=\"ds-prompt-input\">\n <GrInput\n id=\"ds-prompt-input\"\n ref=\"inputRef\"\n v-model=\"valueModel\"\n data-testid=\"ds-prompt-input\"\n :placeholder=\"placeholder\"\n :invalid=\"!!fieldErrorMessage\"\n @blur=\"touched = true\"\n />\n </GrFormField>\n\n <slot name=\"error\" :error=\"error\">\n <GrResponseErrorBanner v-if=\"error\" :error=\"error\" :can-dismiss=\"false\" />\n </slot>\n </div>\n\n <template #footer>\n <slot name=\"footer\">\n <div class=\"flex items-center justify-end gap-3\">\n <GrButton data-testid=\"ds-prompt-cancel\" variant=\"outline\" :size=\"buttonSize\" @click=\"onCancel\">\n {{ cancelText }}\n </GrButton>\n <GrButton\n data-testid=\"ds-prompt-confirm\"\n :variant=\"confirmVariant\"\n :tone=\"confirmTone\"\n :size=\"buttonSize\"\n :loading=\"confirmLoading\"\n :disabled=\"confirmDisabled || (required && touched && !canConfirm)\"\n @click=\"onConfirm\"\n >\n {{ confirmText }}\n </GrButton>\n </div>\n </slot>\n </template>\n </GrDialog>\n</template>\n","<script setup lang=\"ts\">\n/**\n * GrPromptDialog — DS-примитив диалога ввода значения поверх `GrDialog`.\n *\n * Синхронизирован с `GrDialog` по набору проп-проксирования:\n * `size`, `closeOnBackdrop`, `closeOnEsc`, `showHeader`, `showCloseButton`,\n * `headerConfig`, `footerConfig`, `bodyConfig`, `closeLabel`.\n *\n * Поведение `required=true` (по умолчанию): пустое значение подсвечивается\n * ошибкой только после первого «касания» (blur или попытка confirm),\n * чтобы не шуметь при открытии.\n */\nimport { computed, ref, watch } from 'vue'\n\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrDialog from '../GrDialog/GrDialog.vue'\nimport GrFormField from '../GrFormField/GrFormField.vue'\nimport GrInput from '../GrInput/GrInput.vue'\nimport GrResponseErrorBanner from '../GrResponseErrorBanner/GrResponseErrorBanner.vue'\nimport type { GrButtonSize, GrButtonTone, GrButtonVariant } from '../GrButton'\nimport type { GrDialogSectionConfig, GrDialogSize } from '../GrDialog'\nimport type { ResponseErrorInfo } from '../GrResponseErrorBanner'\n\nexport interface GrPromptDialogProps {\n modelValue: boolean\n value: string\n title?: string\n description?: string\n label?: string\n placeholder?: string\n closeOnBackdrop?: boolean\n closeOnEsc?: boolean\n showHeader?: boolean\n showCloseButton?: boolean\n size?: GrDialogSize\n headerConfig?: GrDialogSectionConfig\n footerConfig?: GrDialogSectionConfig\n bodyConfig?: GrDialogSectionConfig\n /** A11y-лейбл кнопки закрытия (i18n). */\n closeLabel?: string\n buttonSize?: GrButtonSize\n confirmText?: string\n cancelText?: string\n /** Текст ошибки для пустого значения при `required=true` (i18n). */\n requiredErrorText?: string\n confirmVariant?: GrButtonVariant\n confirmTone?: GrButtonTone\n required?: boolean\n /**\n * Структура ошибки ответа сервера для показа общим блоком в теле диалога\n * (через `GrResponseErrorBanner`). `null` — блок скрыт.\n */\n error?: ResponseErrorInfo | null\n /**\n * Внешняя ошибка поля ввода (например, серверная валидация). Имеет приоритет\n * над встроенной проверкой `required`. `null`/`undefined` — нет внешней ошибки.\n */\n fieldError?: string | null\n /** Состояние загрузки кнопки Confirm (async-`onConfirm` in-flight). */\n confirmLoading?: boolean\n /** Принудительно дизейблит кнопку Confirm. */\n confirmDisabled?: boolean\n /**\n * Закрывать ли диалог автоматически по клику Confirm. По умолчанию `true`.\n * `false` — отдаёт управление закрытием наружу (нужно `useDialogService`).\n */\n closeOnConfirm?: boolean\n}\n\nconst props = withDefaults(defineProps<GrPromptDialogProps>(), {\n title: 'Prompt',\n description: undefined,\n label: 'Value',\n placeholder: undefined,\n closeOnBackdrop: true,\n closeOnEsc: true,\n showHeader: true,\n showCloseButton: true,\n size: 'md',\n headerConfig: undefined,\n footerConfig: undefined,\n bodyConfig: undefined,\n closeLabel: 'Close',\n buttonSize: undefined,\n confirmText: 'Confirm',\n cancelText: 'Cancel',\n requiredErrorText: 'Enter a value.',\n confirmVariant: 'primary',\n confirmTone: 'primary',\n required: true,\n error: null,\n fieldError: null,\n confirmLoading: false,\n confirmDisabled: false,\n closeOnConfirm: true,\n})\n\nconst emit = defineEmits<{\n (e: 'update:modelValue', value: boolean): void\n (e: 'update:value', value: string): void\n (e: 'confirm', value: string): void\n (e: 'cancel'): void\n}>()\n\nconst open = computed({\n get: () => props.modelValue,\n set: (v: boolean) => emit('update:modelValue', v),\n})\n\nconst valueModel = computed({\n get: () => props.value,\n set: (v: string) => emit('update:value', v),\n})\n\nconst touched = ref(false)\nconst inputRef = ref<InstanceType<typeof GrInput> | null>(null)\n\nwatch(\n () => props.modelValue,\n (v) => {\n if (v) touched.value = false\n },\n)\n\nconst canConfirm = computed(() => {\n if (!props.required) return true\n return valueModel.value.trim().length > 0\n})\n\nconst validationError = computed(() => {\n if (!props.required) return undefined\n if (!touched.value) return undefined\n return valueModel.value.trim().length > 0\n ? undefined\n : props.requiredErrorText\n})\n\n// Внешняя ошибка поля (серверная валидация) имеет приоритет над встроенной.\nconst fieldErrorMessage = computed(() => props.fieldError ?? validationError.value ?? undefined)\n\nfunction onCancel(): void {\n emit('cancel')\n emit('update:modelValue', false)\n}\n\nfunction onConfirm(): void {\n if (!canConfirm.value) {\n touched.value = true\n inputRef.value?.focus()\n return\n }\n\n emit('confirm', valueModel.value)\n if (props.closeOnConfirm)\n emit('update:modelValue', false)\n}\n</script>\n\n<template>\n <GrDialog\n v-model=\"open\"\n :title=\"title\"\n :size=\"size\"\n :close-on-backdrop=\"closeOnBackdrop\"\n :close-on-esc=\"closeOnEsc\"\n :show-header=\"showHeader\"\n :show-close-button=\"showCloseButton\"\n :header-config=\"headerConfig\"\n :footer-config=\"footerConfig\"\n :body-config=\"bodyConfig\"\n :close-label=\"closeLabel\"\n >\n <div class=\"grid gap-4\">\n <slot>\n <div v-if=\"description\" class=\"text-[14px] text-[var(--muted-fg)]\">\n {{ description }}\n </div>\n </slot>\n\n <GrFormField :label=\"label\" :error=\"fieldErrorMessage\" for-id=\"ds-prompt-input\">\n <GrInput\n id=\"ds-prompt-input\"\n ref=\"inputRef\"\n v-model=\"valueModel\"\n data-testid=\"ds-prompt-input\"\n :placeholder=\"placeholder\"\n :invalid=\"!!fieldErrorMessage\"\n @blur=\"touched = true\"\n />\n </GrFormField>\n\n <slot name=\"error\" :error=\"error\">\n <GrResponseErrorBanner v-if=\"error\" :error=\"error\" :can-dismiss=\"false\" />\n </slot>\n </div>\n\n <template #footer>\n <slot name=\"footer\">\n <div class=\"flex items-center justify-end gap-3\">\n <GrButton data-testid=\"ds-prompt-cancel\" variant=\"outline\" :size=\"buttonSize\" @click=\"onCancel\">\n {{ cancelText }}\n </GrButton>\n <GrButton\n data-testid=\"ds-prompt-confirm\"\n :variant=\"confirmVariant\"\n :tone=\"confirmTone\"\n :size=\"buttonSize\"\n :loading=\"confirmLoading\"\n :disabled=\"confirmDisabled || (required && touched && !canConfirm)\"\n @click=\"onConfirm\"\n >\n {{ confirmText }}\n </GrButton>\n </div>\n </slot>\n </template>\n </GrDialog>\n</template>\n"],"mappings":"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"}
1
+ {"version":3,"file":"GrPromptDialog-BV4EW3XO.js","names":[],"sources":["../../../../src/components/GrPromptDialog/GrPromptDialog.vue","../../../../src/components/GrPromptDialog/GrPromptDialog.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * GrPromptDialog — DS-примитив диалога ввода значения поверх `GrDialog`.\n *\n * Синхронизирован с `GrDialog` по набору проп-проксирования:\n * `size`, `closeOnBackdrop`, `closeOnEsc`, `showHeader`, `showCloseButton`,\n * `headerConfig`, `footerConfig`, `bodyConfig`, `closeLabel`.\n *\n * Поведение `required=true` (по умолчанию): пустое значение подсвечивается\n * ошибкой только после первого «касания» (blur или попытка confirm),\n * чтобы не шуметь при открытии.\n */\nimport { computed, ref, watch } from 'vue'\n\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrDialog from '../GrDialog/GrDialog.vue'\nimport GrFormField from '../GrFormField/GrFormField.vue'\nimport GrInput from '../GrInput/GrInput.vue'\nimport GrResponseErrorBanner from '../GrResponseErrorBanner/GrResponseErrorBanner.vue'\nimport type { GrButtonSize, GrButtonTone, GrButtonVariant } from '../GrButton'\nimport type { GrDialogSectionConfig, GrDialogSize } from '../GrDialog'\nimport type { ResponseErrorInfo } from '../GrResponseErrorBanner'\n\nexport interface GrPromptDialogProps {\n modelValue: boolean\n value: string\n title?: string\n description?: string\n label?: string\n placeholder?: string\n closeOnBackdrop?: boolean\n closeOnEsc?: boolean\n showHeader?: boolean\n showCloseButton?: boolean\n size?: GrDialogSize\n headerConfig?: GrDialogSectionConfig\n footerConfig?: GrDialogSectionConfig\n bodyConfig?: GrDialogSectionConfig\n /** A11y-лейбл кнопки закрытия (i18n). */\n closeLabel?: string\n buttonSize?: GrButtonSize\n confirmText?: string\n cancelText?: string\n /** Текст ошибки для пустого значения при `required=true` (i18n). */\n requiredErrorText?: string\n confirmVariant?: GrButtonVariant\n confirmTone?: GrButtonTone\n required?: boolean\n /**\n * Структура ошибки ответа сервера для показа общим блоком в теле диалога\n * (через `GrResponseErrorBanner`). `null` — блок скрыт.\n */\n error?: ResponseErrorInfo | null\n /**\n * Внешняя ошибка поля ввода (например, серверная валидация). Имеет приоритет\n * над встроенной проверкой `required`. `null`/`undefined` — нет внешней ошибки.\n */\n fieldError?: string | null\n /** Состояние загрузки кнопки Confirm (async-`onConfirm` in-flight). */\n confirmLoading?: boolean\n /** Принудительно дизейблит кнопку Confirm. */\n confirmDisabled?: boolean\n /**\n * Закрывать ли диалог автоматически по клику Confirm. По умолчанию `true`.\n * `false` — отдаёт управление закрытием наружу (нужно `useDialogService`).\n */\n closeOnConfirm?: boolean\n}\n\nconst props = withDefaults(defineProps<GrPromptDialogProps>(), {\n title: 'Prompt',\n description: undefined,\n label: 'Value',\n placeholder: undefined,\n closeOnBackdrop: true,\n closeOnEsc: true,\n showHeader: true,\n showCloseButton: true,\n size: 'md',\n headerConfig: undefined,\n footerConfig: undefined,\n bodyConfig: undefined,\n closeLabel: 'Close',\n buttonSize: undefined,\n confirmText: 'Confirm',\n cancelText: 'Cancel',\n requiredErrorText: 'Enter a value.',\n confirmVariant: 'primary',\n confirmTone: 'primary',\n required: true,\n error: null,\n fieldError: null,\n confirmLoading: false,\n confirmDisabled: false,\n closeOnConfirm: true,\n})\n\nconst emit = defineEmits<{\n (e: 'update:modelValue', value: boolean): void\n (e: 'update:value', value: string): void\n (e: 'confirm', value: string): void\n (e: 'cancel'): void\n}>()\n\nconst open = computed({\n get: () => props.modelValue,\n set: (v: boolean) => emit('update:modelValue', v),\n})\n\nconst valueModel = computed({\n get: () => props.value,\n set: (v: string) => emit('update:value', v),\n})\n\nconst touched = ref(false)\nconst inputRef = ref<InstanceType<typeof GrInput> | null>(null)\n\nwatch(\n () => props.modelValue,\n (v) => {\n if (v) touched.value = false\n },\n)\n\nconst canConfirm = computed(() => {\n if (!props.required) return true\n return valueModel.value.trim().length > 0\n})\n\nconst validationError = computed(() => {\n if (!props.required) return undefined\n if (!touched.value) return undefined\n return valueModel.value.trim().length > 0\n ? undefined\n : props.requiredErrorText\n})\n\n// Внешняя ошибка поля (серверная валидация) имеет приоритет над встроенной.\nconst fieldErrorMessage = computed(() => props.fieldError ?? validationError.value ?? undefined)\n\nfunction onCancel(): void {\n emit('cancel')\n emit('update:modelValue', false)\n}\n\nfunction onConfirm(): void {\n if (!canConfirm.value) {\n touched.value = true\n inputRef.value?.focus()\n return\n }\n\n emit('confirm', valueModel.value)\n if (props.closeOnConfirm)\n emit('update:modelValue', false)\n}\n</script>\n\n<template>\n <GrDialog\n v-model=\"open\"\n :title=\"title\"\n :size=\"size\"\n :close-on-backdrop=\"closeOnBackdrop\"\n :close-on-esc=\"closeOnEsc\"\n :show-header=\"showHeader\"\n :show-close-button=\"showCloseButton\"\n :header-config=\"headerConfig\"\n :footer-config=\"footerConfig\"\n :body-config=\"bodyConfig\"\n :close-label=\"closeLabel\"\n >\n <div class=\"grid gap-4\">\n <slot>\n <div v-if=\"description\" class=\"text-[14px] text-[var(--muted-fg)]\">\n {{ description }}\n </div>\n </slot>\n\n <GrFormField :label=\"label\" :error=\"fieldErrorMessage\" for-id=\"ds-prompt-input\">\n <GrInput\n id=\"ds-prompt-input\"\n ref=\"inputRef\"\n v-model=\"valueModel\"\n data-testid=\"ds-prompt-input\"\n :placeholder=\"placeholder\"\n :invalid=\"!!fieldErrorMessage\"\n @blur=\"touched = true\"\n />\n </GrFormField>\n\n <slot name=\"error\" :error=\"error\">\n <GrResponseErrorBanner v-if=\"error\" :error=\"error\" :can-dismiss=\"false\" />\n </slot>\n </div>\n\n <template #footer>\n <slot name=\"footer\">\n <div class=\"flex items-center justify-end gap-3\">\n <GrButton data-testid=\"ds-prompt-cancel\" variant=\"outline\" :size=\"buttonSize\" @click=\"onCancel\">\n {{ cancelText }}\n </GrButton>\n <GrButton\n data-testid=\"ds-prompt-confirm\"\n :variant=\"confirmVariant\"\n :tone=\"confirmTone\"\n :size=\"buttonSize\"\n :loading=\"confirmLoading\"\n :disabled=\"confirmDisabled || (required && touched && !canConfirm)\"\n @click=\"onConfirm\"\n >\n {{ confirmText }}\n </GrButton>\n </div>\n </slot>\n </template>\n </GrDialog>\n</template>\n","<script setup lang=\"ts\">\n/**\n * GrPromptDialog — DS-примитив диалога ввода значения поверх `GrDialog`.\n *\n * Синхронизирован с `GrDialog` по набору проп-проксирования:\n * `size`, `closeOnBackdrop`, `closeOnEsc`, `showHeader`, `showCloseButton`,\n * `headerConfig`, `footerConfig`, `bodyConfig`, `closeLabel`.\n *\n * Поведение `required=true` (по умолчанию): пустое значение подсвечивается\n * ошибкой только после первого «касания» (blur или попытка confirm),\n * чтобы не шуметь при открытии.\n */\nimport { computed, ref, watch } from 'vue'\n\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrDialog from '../GrDialog/GrDialog.vue'\nimport GrFormField from '../GrFormField/GrFormField.vue'\nimport GrInput from '../GrInput/GrInput.vue'\nimport GrResponseErrorBanner from '../GrResponseErrorBanner/GrResponseErrorBanner.vue'\nimport type { GrButtonSize, GrButtonTone, GrButtonVariant } from '../GrButton'\nimport type { GrDialogSectionConfig, GrDialogSize } from '../GrDialog'\nimport type { ResponseErrorInfo } from '../GrResponseErrorBanner'\n\nexport interface GrPromptDialogProps {\n modelValue: boolean\n value: string\n title?: string\n description?: string\n label?: string\n placeholder?: string\n closeOnBackdrop?: boolean\n closeOnEsc?: boolean\n showHeader?: boolean\n showCloseButton?: boolean\n size?: GrDialogSize\n headerConfig?: GrDialogSectionConfig\n footerConfig?: GrDialogSectionConfig\n bodyConfig?: GrDialogSectionConfig\n /** A11y-лейбл кнопки закрытия (i18n). */\n closeLabel?: string\n buttonSize?: GrButtonSize\n confirmText?: string\n cancelText?: string\n /** Текст ошибки для пустого значения при `required=true` (i18n). */\n requiredErrorText?: string\n confirmVariant?: GrButtonVariant\n confirmTone?: GrButtonTone\n required?: boolean\n /**\n * Структура ошибки ответа сервера для показа общим блоком в теле диалога\n * (через `GrResponseErrorBanner`). `null` — блок скрыт.\n */\n error?: ResponseErrorInfo | null\n /**\n * Внешняя ошибка поля ввода (например, серверная валидация). Имеет приоритет\n * над встроенной проверкой `required`. `null`/`undefined` — нет внешней ошибки.\n */\n fieldError?: string | null\n /** Состояние загрузки кнопки Confirm (async-`onConfirm` in-flight). */\n confirmLoading?: boolean\n /** Принудительно дизейблит кнопку Confirm. */\n confirmDisabled?: boolean\n /**\n * Закрывать ли диалог автоматически по клику Confirm. По умолчанию `true`.\n * `false` — отдаёт управление закрытием наружу (нужно `useDialogService`).\n */\n closeOnConfirm?: boolean\n}\n\nconst props = withDefaults(defineProps<GrPromptDialogProps>(), {\n title: 'Prompt',\n description: undefined,\n label: 'Value',\n placeholder: undefined,\n closeOnBackdrop: true,\n closeOnEsc: true,\n showHeader: true,\n showCloseButton: true,\n size: 'md',\n headerConfig: undefined,\n footerConfig: undefined,\n bodyConfig: undefined,\n closeLabel: 'Close',\n buttonSize: undefined,\n confirmText: 'Confirm',\n cancelText: 'Cancel',\n requiredErrorText: 'Enter a value.',\n confirmVariant: 'primary',\n confirmTone: 'primary',\n required: true,\n error: null,\n fieldError: null,\n confirmLoading: false,\n confirmDisabled: false,\n closeOnConfirm: true,\n})\n\nconst emit = defineEmits<{\n (e: 'update:modelValue', value: boolean): void\n (e: 'update:value', value: string): void\n (e: 'confirm', value: string): void\n (e: 'cancel'): void\n}>()\n\nconst open = computed({\n get: () => props.modelValue,\n set: (v: boolean) => emit('update:modelValue', v),\n})\n\nconst valueModel = computed({\n get: () => props.value,\n set: (v: string) => emit('update:value', v),\n})\n\nconst touched = ref(false)\nconst inputRef = ref<InstanceType<typeof GrInput> | null>(null)\n\nwatch(\n () => props.modelValue,\n (v) => {\n if (v) touched.value = false\n },\n)\n\nconst canConfirm = computed(() => {\n if (!props.required) return true\n return valueModel.value.trim().length > 0\n})\n\nconst validationError = computed(() => {\n if (!props.required) return undefined\n if (!touched.value) return undefined\n return valueModel.value.trim().length > 0\n ? undefined\n : props.requiredErrorText\n})\n\n// Внешняя ошибка поля (серверная валидация) имеет приоритет над встроенной.\nconst fieldErrorMessage = computed(() => props.fieldError ?? validationError.value ?? undefined)\n\nfunction onCancel(): void {\n emit('cancel')\n emit('update:modelValue', false)\n}\n\nfunction onConfirm(): void {\n if (!canConfirm.value) {\n touched.value = true\n inputRef.value?.focus()\n return\n }\n\n emit('confirm', valueModel.value)\n if (props.closeOnConfirm)\n emit('update:modelValue', false)\n}\n</script>\n\n<template>\n <GrDialog\n v-model=\"open\"\n :title=\"title\"\n :size=\"size\"\n :close-on-backdrop=\"closeOnBackdrop\"\n :close-on-esc=\"closeOnEsc\"\n :show-header=\"showHeader\"\n :show-close-button=\"showCloseButton\"\n :header-config=\"headerConfig\"\n :footer-config=\"footerConfig\"\n :body-config=\"bodyConfig\"\n :close-label=\"closeLabel\"\n >\n <div class=\"grid gap-4\">\n <slot>\n <div v-if=\"description\" class=\"text-[14px] text-[var(--muted-fg)]\">\n {{ description }}\n </div>\n </slot>\n\n <GrFormField :label=\"label\" :error=\"fieldErrorMessage\" for-id=\"ds-prompt-input\">\n <GrInput\n id=\"ds-prompt-input\"\n ref=\"inputRef\"\n v-model=\"valueModel\"\n data-testid=\"ds-prompt-input\"\n :placeholder=\"placeholder\"\n :invalid=\"!!fieldErrorMessage\"\n @blur=\"touched = true\"\n />\n </GrFormField>\n\n <slot name=\"error\" :error=\"error\">\n <GrResponseErrorBanner v-if=\"error\" :error=\"error\" :can-dismiss=\"false\" />\n </slot>\n </div>\n\n <template #footer>\n <slot name=\"footer\">\n <div class=\"flex items-center justify-end gap-3\">\n <GrButton data-testid=\"ds-prompt-cancel\" variant=\"outline\" :size=\"buttonSize\" @click=\"onCancel\">\n {{ cancelText }}\n </GrButton>\n <GrButton\n data-testid=\"ds-prompt-confirm\"\n :variant=\"confirmVariant\"\n :tone=\"confirmTone\"\n :size=\"buttonSize\"\n :loading=\"confirmLoading\"\n :disabled=\"confirmDisabled || (required && touched && !canConfirm)\"\n @click=\"onConfirm\"\n >\n {{ confirmText }}\n </GrButton>\n </div>\n </slot>\n </template>\n </GrDialog>\n</template>\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
- import { t as GrPromptDialog_default } from "./chunks/GrPromptDialog-23x2nZvv.js";
1
+ import { t as GrPromptDialog_default } from "./chunks/GrPromptDialog-BV4EW3XO.js";
2
2
  import { t as grPromptDialogConfig } from "../../chunks/config-B1kFmhpM.js";
3
3
  export { GrPromptDialog_default as GrPromptDialog, GrPromptDialog_default as default, grPromptDialogConfig };
package/dist/index.js CHANGED
@@ -25,12 +25,12 @@ import { n as GrCollapse_default, r as GR_COLLAPSE_CONTEXT, t as GrCollapseItem_
25
25
  import { t as GrIcon_default } from "./components/GrIcon/chunks/GrIcon-BV7RBU_0.js";
26
26
  import { t as grIconConfig } from "./chunks/config-DbghHyY_.js";
27
27
  import { t as grCollapseConfig } from "./chunks/config-D5ek7VSi.js";
28
- import { t as GrModal_default } from "./components/GrModal/chunks/GrModal-2ALC3kwy.js";
28
+ import { t as GrModal_default } from "./components/GrModal/chunks/GrModal-lh0lOmoi.js";
29
29
  import { t as getGrModalPanelClass } from "./chunks/grModalStyles-DS1Vl6EW.js";
30
30
  import { a as resolveGrDialogTitle, i as resolveGrDialogSectionConfig, n as DEFAULT_GR_DIALOG_FOOTER_CONFIG, r as DEFAULT_GR_DIALOG_HEADER_CONFIG, t as DEFAULT_GR_DIALOG_BODY_CONFIG } from "./chunks/dialogShared-BtXUk6Zc.js";
31
- import { i as GrDialogFooter_default, n as GrDialogHeader_default, r as GrDialogCloseButton_default, t as GrDialog_default } from "./components/GrDialog/chunks/GrDialog-SYcF-4op.js";
31
+ import { i as GrDialogFooter_default, n as GrDialogHeader_default, r as GrDialogCloseButton_default, t as GrDialog_default } from "./components/GrDialog/chunks/GrDialog-C7XLxp8e.js";
32
32
  import { _ as DEFAULT_AUTO_HIDE_KINDS, a as extendDefaultParsers, c as problemDetailsParser, d as networkErrorParser, f as laravelValidationParser, g as abortErrorParser, h as fileValidationParser, i as defaultResponseErrorParsers, l as plainMessageParser, m as httpStatusParser, n as coreResponseErrorParsers, o as resolveResponseErrorTitle, p as jsonApiErrorParser, r as createResponseErrorClassifier, s as responseErrorParserPresets, t as GrResponseErrorBanner_default, u as normalizeError, v as DEFAULT_RESPONSE_ERROR_TEXTS, y as DEFAULT_TONE_BY_KIND } from "./components/GrResponseErrorBanner/chunks/GrResponseErrorBanner-CzdTh4gd.js";
33
- import { t as GrConfirmDialog_default } from "./components/GrConfirmDialog/chunks/GrConfirmDialog-DuUQA7nv.js";
33
+ import { t as GrConfirmDialog_default } from "./components/GrConfirmDialog/chunks/GrConfirmDialog-Br6WJkLE.js";
34
34
  import { t as grConfirmDialogConfig } from "./chunks/config-BduXEXjt.js";
35
35
  import { t as GrTable_default } from "./components/GrTable/chunks/GrTable-dTWUxzRm.js";
36
36
  import { t as GrDataTable_default } from "./components/GrDataTable/chunks/GrDataTable-ClcYiyc8.js";
@@ -39,7 +39,7 @@ import { t as grDialogSafelist } from "./chunks/safelist-_UVzJrgE2.js";
39
39
  import "./components/GrDialog/index.js";
40
40
  import { t as GrFormField_default } from "./components/GrFormField/chunks/GrFormField-U7c_aDPJ.js";
41
41
  import { t as GrInput_default } from "./components/GrInput/chunks/GrInput-gwFQ1sk4.js";
42
- import { t as GrPromptDialog_default } from "./components/GrPromptDialog/chunks/GrPromptDialog-23x2nZvv.js";
42
+ import { t as GrPromptDialog_default } from "./components/GrPromptDialog/chunks/GrPromptDialog-BV4EW3XO.js";
43
43
  import { n as GrFormErrorBanner_default, r as GrUploadErrorBanner_default, t as useResponseError } from "./components/GrResponseErrorBanner/chunks/GrResponseErrorBanner-BMvXREcv.js";
44
44
  import { n as grDrawerSafelist, t as grDrawerConfig } from "./chunks/config-C6zT56Jc2.js";
45
45
  import { t as GrDrawer_default } from "./components/GrDrawer/chunks/GrDrawer-BW3wtI7h.js";
@@ -67,7 +67,7 @@ import { t as GrFormFile_default } from "./components/GrFormFile/chunks/GrFormFi
67
67
  import { t as grFormFileConfig } from "./chunks/config-BbKroFtR.js";
68
68
  import { t as GrFormSection_default } from "./components/GrFormSection/chunks/GrFormSection-DpGhY7MX.js";
69
69
  import { t as grFormSectionConfig } from "./chunks/config-DoBk_CzG.js";
70
- import { t as GrImageViewer_default } from "./components/GrImageViewer/chunks/GrImageViewer-Cn1X-sWS.js";
70
+ import { t as GrImageViewer_default } from "./components/GrImageViewer/chunks/GrImageViewer-CsvXQHM9.js";
71
71
  import { t as grImageViewerConfig } from "./chunks/config-CvGdaXvH.js";
72
72
  import { n as grInputSafelist, t as grInputConfig } from "./chunks/config-D4s_QWVP.js";
73
73
  import "./components/GrInput/index.js";
@@ -20,6 +20,10 @@ type __VLS_Props = {
20
20
  closeOnPressEscape?: boolean;
21
21
  showProgress?: boolean;
22
22
  showZoomValue?: boolean;
23
+ /** Включает масштабирование колесом мыши / жестом на трекпаде. По умолчанию включено. */
24
+ wheelZoom?: boolean;
25
+ /** Включает перетаскивание (pan) картинки мышью. При наведении курсор «рука». По умолчанию выключено. */
26
+ draggable?: boolean;
23
27
  zIndex?: number;
24
28
  /** i18n: aria-label кнопки закрытия. */
25
29
  closeLabel?: string;
@@ -56,6 +60,16 @@ declare var __VLS_40: {
56
60
  total: number;
57
61
  scale: number;
58
62
  rotation: number;
63
+ /** Натуральный (исходный) размер картинки, px. 0 пока изображение не загружено. */
64
+ naturalWidth: number;
65
+ naturalHeight: number;
66
+ /** Фактический размер изображения на экране c учётом scale, px (footprint вдоль осей картинки). */
67
+ renderedWidth: number;
68
+ renderedHeight: number;
69
+ /** Реальный масштаб относительно натурального размера (доля): renderedWidth / naturalWidth. */
70
+ realScale: number;
71
+ /** Реальный масштаб в процентах, отформатированный (например `67`). */
72
+ realScalePercent: string;
59
73
  actions: GrImageViewerToolbarActions;
60
74
  }, __VLS_42: {
61
75
  index: number;
@@ -63,6 +77,16 @@ declare var __VLS_40: {
63
77
  total: number;
64
78
  scale: number;
65
79
  rotation: number;
80
+ /** Натуральный (исходный) размер картинки, px. 0 пока изображение не загружено. */
81
+ naturalWidth: number;
82
+ naturalHeight: number;
83
+ /** Фактический размер изображения на экране c учётом scale, px (footprint вдоль осей картинки). */
84
+ renderedWidth: number;
85
+ renderedHeight: number;
86
+ /** Реальный масштаб относительно натурального размера (доля): renderedWidth / naturalWidth. */
87
+ realScale: number;
88
+ /** Реальный масштаб в процентах, отформатированный (например `67`). */
89
+ realScalePercent: string;
66
90
  actions: GrImageViewerToolbarActions;
67
91
  };
68
92
  type __VLS_Slots = {} & {
@@ -79,6 +103,8 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
79
103
  closeOnPressEscape: boolean;
80
104
  showProgress: boolean;
81
105
  showZoomValue: boolean;
106
+ wheelZoom: boolean;
107
+ draggable: boolean;
82
108
  zIndex: undefined;
83
109
  closeLabel: string;
84
110
  prevLabel: string;
@@ -103,6 +129,8 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
103
129
  closeOnPressEscape: boolean;
104
130
  showProgress: boolean;
105
131
  showZoomValue: boolean;
132
+ wheelZoom: boolean;
133
+ draggable: boolean;
106
134
  zIndex: undefined;
107
135
  closeLabel: string;
108
136
  prevLabel: string;
@@ -129,6 +157,8 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
129
157
  hideOnClickModal: boolean;
130
158
  closeOnPressEscape: boolean;
131
159
  showZoomValue: boolean;
160
+ wheelZoom: boolean;
161
+ draggable: boolean;
132
162
  prevLabel: string;
133
163
  nextLabel: string;
134
164
  zoomInLabel: string;