@feugene/granularity 0.31.1 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/{config-Dv_r92GL.js → config-2vrOFn2f.js} +2 -2
- package/dist/chunks/{config-Dv_r92GL.js.map → config-2vrOFn2f.js.map} +1 -1
- package/dist/chunks/{config-g8-JGZ_Y.js → config-BscVxZR3.js} +1 -1
- package/dist/chunks/{config-g8-JGZ_Y.js.map → config-BscVxZR3.js.map} +1 -1
- package/dist/chunks/{grBadgeStyles-D8Mz73B5.js → grBadgeStyles-DV0DrAew.js} +17 -1
- package/dist/chunks/grBadgeStyles-DV0DrAew.js.map +1 -0
- package/dist/chunks/{grChipStyles-3JZApw03.js → grChipStyles-DcDyxAL0.js} +1 -1
- package/dist/chunks/{grChipStyles-3JZApw03.js.map → grChipStyles-DcDyxAL0.js.map} +1 -1
- package/dist/chunks/{granular-provider-Da2CJO6j.js → granular-provider-CrQua8PT.js} +2 -2
- package/dist/chunks/{granular-provider-Da2CJO6j.js.map → granular-provider-CrQua8PT.js.map} +1 -1
- package/dist/chunks/{overlayStack-C8cfsqXa.js → overlayStack-0CZS99Da.js} +40 -6
- package/dist/chunks/overlayStack-0CZS99Da.js.map +1 -0
- package/dist/chunks/{safelist-CtiHPgrj.js → safelist-x8USecK8.js} +3 -1
- package/dist/chunks/safelist-x8USecK8.js.map +1 -0
- package/dist/chunks/{useModalOverlay-Cv5hF63W.js → useModalOverlay-D__CqLGn.js} +3 -0
- package/dist/chunks/useModalOverlay-D__CqLGn.js.map +1 -0
- package/dist/components/GrAutocomplete/chunks/{GrAutocomplete-rYEY15UI.js → GrAutocomplete-CoQjEF8h.js} +2 -2
- package/dist/components/GrAutocomplete/chunks/{GrAutocomplete-rYEY15UI.js.map → GrAutocomplete-CoQjEF8h.js.map} +1 -1
- package/dist/components/GrAutocomplete/index.js +1 -1
- package/dist/components/GrBadge/chunks/{GrBadge-D2MfThPz.js → GrBadge-DpUDXKbq.js} +9 -4
- package/dist/components/GrBadge/chunks/GrBadge-DpUDXKbq.js.map +1 -0
- package/dist/components/GrBadge/index.js +3 -3
- package/dist/components/GrBadge/styles.css +1 -1
- package/dist/components/GrChip/chunks/{GrChip-C0vU69tf.js → GrChip-BTivtwff.js} +1 -1
- package/dist/components/GrChip/chunks/{GrChip-C0vU69tf.js.map → GrChip-BTivtwff.js.map} +1 -1
- package/dist/components/GrChip/index.js +2 -2
- package/dist/components/GrColorPicker/chunks/{GrColorPicker-B20gQ6AR.js → GrColorPicker-p2XH_8cE.js} +1 -1
- package/dist/components/GrColorPicker/chunks/{GrColorPicker-B20gQ6AR.js.map → GrColorPicker-p2XH_8cE.js.map} +1 -1
- package/dist/components/GrColorPicker/index.js +1 -1
- package/dist/components/GrCommandPalette/chunks/{GrCommandPalette-Cd0Yo3Dw.js → GrCommandPalette-BXbh3q7O.js} +1 -1
- package/dist/components/GrCommandPalette/chunks/{GrCommandPalette-Cd0Yo3Dw.js.map → GrCommandPalette-BXbh3q7O.js.map} +1 -1
- package/dist/components/GrCommandPalette/index.js +1 -1
- package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-DOsbRhoZ.js → GrConfirmDialog-CUP43q__.js} +1 -1
- package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-DOsbRhoZ.js.map → GrConfirmDialog-CUP43q__.js.map} +1 -1
- package/dist/components/GrConfirmDialog/index.js +1 -1
- package/dist/components/GrContextMenu/chunks/{GrContextMenu-_FKwQvuQ.js → GrContextMenu-ClkVjdcP.js} +1 -1
- package/dist/components/GrContextMenu/chunks/{GrContextMenu-_FKwQvuQ.js.map → GrContextMenu-ClkVjdcP.js.map} +1 -1
- package/dist/components/GrContextMenu/index.js +1 -1
- package/dist/components/GrDataTable/chunks/{GrDataTable-CAGgRk27.js → GrDataTable-4JAgOQ-7.js} +1 -1
- package/dist/components/GrDataTable/chunks/{GrDataTable-CAGgRk27.js.map → GrDataTable-4JAgOQ-7.js.map} +1 -1
- package/dist/components/GrDataTable/index.js +1 -1
- package/dist/components/GrDialog/chunks/{GrDialog-S1Vl-OhX.js → GrDialog-BiMJ9_T4.js} +1 -1
- package/dist/components/GrDialog/chunks/{GrDialog-S1Vl-OhX.js.map → GrDialog-BiMJ9_T4.js.map} +1 -1
- package/dist/components/GrDialog/index.js +1 -1
- package/dist/components/GrDialogService/chunks/{GrDialogService-Cuhc4YLp.js → GrDialogService-BxNK6crD.js} +2 -2
- package/dist/components/GrDialogService/chunks/{GrDialogService-Cuhc4YLp.js.map → GrDialogService-BxNK6crD.js.map} +1 -1
- package/dist/components/GrDialogService/index.js +1 -1
- package/dist/components/GrDrawer/chunks/{GrDrawer-C0YqVE9e.js → GrDrawer-Ce2l3zaG.js} +5 -4
- package/dist/components/GrDrawer/chunks/GrDrawer-Ce2l3zaG.js.map +1 -0
- package/dist/components/GrDrawer/index.js +1 -1
- package/dist/components/GrImageViewer/chunks/{GrImageViewer-CyVN6Zpc.js → GrImageViewer-CG4vUBg_.js} +4 -3
- package/dist/components/GrImageViewer/chunks/GrImageViewer-CG4vUBg_.js.map +1 -0
- package/dist/components/GrImageViewer/index.js +1 -1
- package/dist/components/GrInputTag/chunks/{GrInputTag-Cj4A9Xgs.js → GrInputTag-ByD9NugQ.js} +2 -2
- package/dist/components/GrInputTag/chunks/{GrInputTag-Cj4A9Xgs.js.map → GrInputTag-ByD9NugQ.js.map} +1 -1
- package/dist/components/GrInputTag/index.js +1 -1
- package/dist/components/GrModal/chunks/{GrModal-CCoWZuIl.js → GrModal-D0mf1SAx.js} +3 -2
- package/dist/components/GrModal/chunks/GrModal-D0mf1SAx.js.map +1 -0
- package/dist/components/GrModal/index.js +1 -1
- package/dist/components/GrPagination/chunks/{GrPagination-B_kAhoAV.js → GrPagination-FiiXzVyv.js} +1 -1
- package/dist/components/GrPagination/chunks/{GrPagination-B_kAhoAV.js.map → GrPagination-FiiXzVyv.js.map} +1 -1
- package/dist/components/GrPagination/index.js +1 -1
- package/dist/components/GrPopover/chunks/{GrPopover-CofG-Tfe.js → GrPopover-faJanmGs.js} +1 -1
- package/dist/components/GrPopover/chunks/{GrPopover-CofG-Tfe.js.map → GrPopover-faJanmGs.js.map} +1 -1
- package/dist/components/GrPopover/index.js +1 -1
- package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-BuJQMZLM.js → GrPromptDialog-C6QGC7QJ.js} +1 -1
- package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-BuJQMZLM.js.map → GrPromptDialog-C6QGC7QJ.js.map} +1 -1
- package/dist/components/GrPromptDialog/index.js +1 -1
- package/dist/components/GrSelect/chunks/{GrSelect-gCeRFgQo.js → GrSelect-Dt8iqruc.js} +2 -2
- package/dist/components/GrSelect/chunks/{GrSelect-gCeRFgQo.js.map → GrSelect-Dt8iqruc.js.map} +1 -1
- package/dist/components/GrSelect/index.js +1 -1
- package/dist/components/GrTable/chunks/{GrTable-Zcdz2St_.js → GrTable-jhUTczjv.js} +12 -1
- package/dist/components/GrTable/chunks/GrTable-jhUTczjv.js.map +1 -0
- package/dist/components/GrTable/index.js +1 -1
- package/dist/composables/useFloating.js +2 -22
- package/dist/composables/useFloating.js.map +1 -1
- package/dist/composables/useOverlayLayer.js +10 -3
- package/dist/composables/useOverlayLayer.js.map +1 -1
- package/dist/granular-provider-node.js +1 -1
- package/dist/granular-provider.js +1 -1
- package/dist/index.js +22 -22
- package/dist/types/components/GrBadge/GrBadge.vue.d.ts +11 -0
- package/dist/types/components/GrBadge/grBadgeStyles.d.ts +11 -0
- package/dist/types/composables/internal/overlayStack.d.ts +30 -0
- package/dist/types/composables/internal/useModalOverlay.d.ts +14 -0
- package/dist/types/composables/useOverlayLayer.d.ts +8 -0
- package/dist/web-types.json +5 -1
- package/package.json +3 -3
- package/dist/chunks/grBadgeStyles-D8Mz73B5.js.map +0 -1
- package/dist/chunks/overlayStack-C8cfsqXa.js.map +0 -1
- package/dist/chunks/safelist-CtiHPgrj.js.map +0 -1
- package/dist/chunks/useModalOverlay-Cv5hF63W.js.map +0 -1
- package/dist/components/GrBadge/chunks/GrBadge-D2MfThPz.js.map +0 -1
- package/dist/components/GrDrawer/chunks/GrDrawer-C0YqVE9e.js.map +0 -1
- package/dist/components/GrImageViewer/chunks/GrImageViewer-CyVN6Zpc.js.map +0 -1
- package/dist/components/GrModal/chunks/GrModal-CCoWZuIl.js.map +0 -1
- package/dist/components/GrTable/chunks/GrTable-Zcdz2St_.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GrPagination-B_kAhoAV.js","names":[],"sources":["../../../../src/components/GrPagination/GrPagination.vue","../../../../src/components/GrPagination/GrPagination.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref, watch } from 'vue'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrSelect from '../GrSelect/GrSelect.vue'\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport {\n type GrPaginationSize,\n ellipsisSizes,\n jumperSizes,\n labelSizes,\n navButtonSizes,\n pageListGaps,\n pageSizes,\n pageSizeSelectWidths,\n rowGaps,\n} from './grPaginationStyles'\n\n/**\n * Пропы пагинации.\n *\n * `total` интерпретируется как общее количество элементов; число страниц\n * вычисляется из `total` и `pageSize`. Номера усекаются многоточием\n * (алгоритм boundary/sibling, как у MUI): всегда видны первая/последняя страница\n * и `siblingCount` соседей вокруг текущей.\n */\nexport interface GrPaginationProps {\n page: number\n pageSize: number\n total: number\n pageSizes?: number[]\n /** Сколько соседних страниц показывать вокруг текущей. По умолчанию `1`. */\n siblingCount?: number\n /** Сколько крайних страниц всегда показывать с каждого края. По умолчанию `1`. */\n boundaryCount?: number\n /**\n * Компактный вариант: вместо нумерованных страниц показывается индикатор\n * «текущая / всего» — удобно для узких мест (мобайл, тулбары таблиц).\n */\n compact?: boolean\n /** Показывать поле «перейти к странице» с быстрым переходом по вводу номера. */\n showJumper?: boolean\n /** Показывать селект размера страницы. */\n showPageSize?: boolean\n /** Показывать диапазон показанных элементов — «1–20 из 137». Слот `#total` сильнее. */\n showTotal?: boolean\n /** i18n-подпись перед полем перехода. По умолчанию — `gr.pagination.jumpTo`. */\n jumperLabel?: string\n /**\n * Имя навигационного лендмарка. По умолчанию — `gr.pagination.label`; задавать\n * стоит там, где пагинаций на странице несколько и их надо различать.\n */\n ariaLabel?: string\n /** Гасит всю пагинацию: номера, кнопки, селект размера и поле перехода. */\n disabled?: boolean\n size?: GrPaginationSize\n}\n\nexport interface GrPaginationEmits {\n (e: 'update:page', value: number): void\n (e: 'update:pageSize', value: number): void\n}\n\nconst { t } = useGranularityTranslations()\n\nconst props = withDefaults(defineProps<GrPaginationProps>(), {\n pageSizes: () => [10, 20, 50],\n siblingCount: 1,\n boundaryCount: 1,\n compact: false,\n showJumper: false,\n showPageSize: false,\n showTotal: false,\n jumperLabel: undefined,\n ariaLabel: undefined,\n disabled: false,\n size: undefined,\n})\n\ndefineSlots<{\n /** Диапазон показанных элементов целиком — вместо строки из локали. */\n total?: (props: { from: number, to: number, total: number }) => unknown\n}>()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrPagination' })\n\nconst rowClass = computed(() => rowGaps[resolvedSize.value])\nconst pageListClass = computed(() => pageListGaps[resolvedSize.value])\nconst pageClass = computed(() => pageSizes[resolvedSize.value])\nconst jumperClass = computed(() => jumperSizes[resolvedSize.value])\nconst ellipsisClass = computed(() => ellipsisSizes[resolvedSize.value])\nconst labelClass = computed(() => labelSizes[resolvedSize.value])\nconst selectWrapClass = computed(() => pageSizeSelectWidths[resolvedSize.value])\nconst navButtonSize = computed(() => navButtonSizes[resolvedSize.value])\n\nconst emit = defineEmits<GrPaginationEmits>()\n\n// Делитель клампится: `pageSize: 0` дал бы `Infinity` страниц и бесконечный\n// цикл в `range()`.\nconst resolvedPageSize = computed(() => Math.max(1, Math.trunc(props.pageSize)))\nconst pageCount = computed(() => Math.max(1, Math.ceil(props.total / resolvedPageSize.value)))\n\n// Рендер идёт от зажатого номера: вотчер ниже просит родителя подтянуть `page`,\n// но на первом рендере он молчит, и без клампа пагинация осталась бы вовсе без\n// активной страницы.\nconst currentPage = computed(() => Math.min(Math.max(Math.trunc(props.page), 1), pageCount.value))\n\n// Контролируемый компонент: если `total`/`pageSize` уменьшились и текущая `page`\n// вышла за диапазон — просим родителя подтянуть её к последней странице.\nwatch(pageCount, (count) => {\n if (props.page > count)\n emit('update:page', count)\n})\n\nconst pageSizeModel = computed({\n get: () => String(props.pageSize),\n set: (value: string) => emit('update:pageSize', Number(value)),\n})\n\nconst pageSizeOptions = computed(() =>\n props.pageSizes.map(pageSize => ({\n value: String(pageSize),\n label: String(pageSize),\n })),\n)\n\nconst rangeFrom = computed(() => (props.total === 0 ? 0 : (currentPage.value - 1) * resolvedPageSize.value + 1))\nconst rangeTo = computed(() => Math.min(currentPage.value * resolvedPageSize.value, props.total))\n\ntype PaginationItem = number | 'ellipsis-start' | 'ellipsis-end'\n\nfunction range(start: number, end: number): number[] {\n const list: number[] = []\n for (let page = start; page <= end; page += 1)\n list.push(page)\n return list\n}\n\n// Алгоритм usePagination как в MUI: крайние `boundaryCount` страниц + `siblingCount`\n// соседей вокруг текущей, разрывы схлопываются в многоточие. Одиночный «пропущенный»\n// номер показываем как номер, а не как «…» (визуально ровнее).\nconst items = computed<PaginationItem[]>(() => {\n const count = pageCount.value\n const current = currentPage.value\n const boundaryCount = Math.max(0, props.boundaryCount)\n const siblingCount = Math.max(0, props.siblingCount)\n\n const startPages = range(1, Math.min(boundaryCount, count))\n const endPages = range(Math.max(count - boundaryCount + 1, boundaryCount + 1), count)\n\n const siblingsStart = Math.max(\n Math.min(current - siblingCount, count - boundaryCount - siblingCount * 2 - 1),\n boundaryCount + 2,\n )\n const siblingsEnd = Math.min(\n Math.max(current + siblingCount, boundaryCount + siblingCount * 2 + 2),\n endPages.length > 0 ? endPages[0] - 2 : count - 1,\n )\n\n return [\n ...startPages,\n ...(siblingsStart > boundaryCount + 2\n ? ['ellipsis-start' as const]\n : boundaryCount + 1 < count - boundaryCount\n ? [boundaryCount + 1]\n : []),\n ...range(siblingsStart, siblingsEnd),\n ...(siblingsEnd < count - boundaryCount - 1\n ? ['ellipsis-end' as const]\n : count - boundaryCount > boundaryCount\n ? [count - boundaryCount]\n : []),\n ...endPages,\n ]\n})\n\nfunction goTo(page: number): void {\n if (props.disabled)\n return\n\n emit('update:page', page)\n}\n\nfunction prev(): void {\n goTo(Math.max(1, currentPage.value - 1))\n}\n\nfunction next(): void {\n goTo(Math.min(pageCount.value, currentPage.value + 1))\n}\n\nfunction first(): void {\n goTo(1)\n}\n\nfunction last(): void {\n goTo(pageCount.value)\n}\n\n// «Перейти к странице»: клампим введённый номер к диапазону [1, pageCount]\n// и сбрасываем поле после перехода.\nconst jumperValue = ref('')\n\nfunction submitJumper(): void {\n const parsed = Number.parseInt(jumperValue.value, 10)\n jumperValue.value = ''\n if (Number.isNaN(parsed))\n return\n\n goTo(Math.min(pageCount.value, Math.max(1, parsed)))\n}\n\nconst resolvedJumperLabel = computed(() => props.jumperLabel ?? t('gr.pagination.jumpTo', 'Go to'))\nconst resolvedAriaLabel = computed(() => props.ariaLabel ?? t('gr.pagination.label', 'Pagination'))\nconst statusText = computed(() =>\n t('gr.pagination.status', 'Page {page} of {count}', { page: currentPage.value, count: pageCount.value }),\n)\nconst totalText = computed(() =>\n t('gr.pagination.total', '{from}–{to} of {total}', { from: rangeFrom.value, to: rangeTo.value, total: props.total }),\n)\n</script>\n\n<template>\n <div\n class=\"flex flex-wrap items-center justify-end\"\n :class=\"rowClass\"\n data-gr-pagination\n role=\"navigation\"\n :aria-label=\"resolvedAriaLabel\"\n >\n <div\n v-if=\"showTotal\"\n data-gr-pagination-total\n class=\"text-[var(--gr-muted-fg)] tabular-nums\"\n :class=\"labelClass\"\n >\n <slot name=\"total\" :from=\"rangeFrom\" :to=\"rangeTo\" :total=\"total\">\n {{ totalText }}\n </slot>\n </div>\n\n <div v-if=\"showPageSize\" :class=\"selectWrapClass\">\n <GrSelect\n v-model=\"pageSizeModel\"\n :options=\"pageSizeOptions\"\n :size=\"resolvedSize\"\n :disabled=\"disabled\"\n :aria-label=\"t('gr.pagination.pageSize', 'Page size')\"\n />\n </div>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage <= 1\" :aria-label=\"t('gr.pagination.first', 'First page')\" data-gr-pagination-first @click=\"first\">\n «\n </GrButton>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage <= 1\" data-gr-pagination-prev @click=\"prev\">\n {{ t('gr.pagination.prev', 'Prev') }}\n </GrButton>\n\n <!-- Компактный вариант: индикатор «текущая / всего» вместо нумерованных страниц. -->\n <div\n v-if=\"compact\"\n data-gr-pagination-compact\n class=\"px-2 text-[var(--gr-fg)] tabular-nums\"\n :class=\"labelClass\"\n role=\"status\"\n >\n {{ currentPage }} / {{ pageCount }}\n </div>\n\n <template v-else>\n <ul class=\"flex items-center [list-style:none]\" :class=\"pageListClass\" data-gr-pagination-pages role=\"list\">\n <li v-for=\"(item, index) in items\" :key=\"index\" :aria-hidden=\"typeof item === 'string' ? 'true' : undefined\">\n <span\n v-if=\"item === 'ellipsis-start' || item === 'ellipsis-end'\"\n data-gr-pagination-ellipsis\n class=\"grid place-items-center text-[var(--gr-muted-fg)]\"\n :class=\"ellipsisClass\"\n >…</span>\n <button\n v-else\n type=\"button\"\n data-gr-pagination-page\n :disabled=\"disabled\"\n :aria-current=\"item === currentPage ? 'page' : undefined\"\n :aria-label=\"t('gr.pagination.page', 'Page {n}', { n: item })\"\n class=\"rounded-[var(--gr-radius-control)] font-600 transition-colors duration-[var(--gr-duration-fast)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)] disabled:cursor-not-allowed\"\n :class=\"[\n pageClass,\n item === currentPage\n ? 'bg-[var(--gr-primary)] text-[var(--gr-primary-fg)]'\n // Подложка тональная, а не `--gr-muted`: серый на белом фоне даёт\n // 1.10:1 и на светлой теме hover был практически не виден, хотя у\n // соседних «назад/вперёд» (ghost-кнопки) он тонирован. Ряд обязан\n // читаться одним контролом.\n : 'text-[var(--gr-muted-fg)] hover:bg-[var(--gr-accent)] hover:text-[var(--gr-accent-fg)]',\n ]\"\n @click=\"goTo(item)\"\n >\n {{ item }}\n </button>\n </li>\n </ul>\n\n <!-- В компактном режиме смену страницы объявляет видимый индикатор. -->\n <div data-gr-pagination-status role=\"status\" class=\"sr-only\">\n {{ statusText }}\n </div>\n </template>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage >= pageCount\" data-gr-pagination-next @click=\"next\">\n {{ t('gr.pagination.next', 'Next') }}\n </GrButton>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage >= pageCount\" :aria-label=\"t('gr.pagination.last', 'Last page')\" data-gr-pagination-last @click=\"last\">\n »\n </GrButton>\n\n <!-- «Перейти к странице»: Enter или blur применяют введённый номер. -->\n <div v-if=\"showJumper\" class=\"flex items-center gap-2 text-[var(--gr-muted-fg)]\" :class=\"labelClass\">\n <span>{{ resolvedJumperLabel }}</span>\n <input\n v-model=\"jumperValue\"\n type=\"number\"\n min=\"1\"\n :max=\"pageCount\"\n inputmode=\"numeric\"\n data-gr-pagination-jumper\n :disabled=\"disabled\"\n :aria-label=\"resolvedJumperLabel\"\n class=\"rounded-[var(--gr-radius-control)] border border-[var(--gr-brd)] bg-[var(--gr-bg)] px-2 text-center text-[var(--gr-fg)] tabular-nums transition-colors duration-[var(--gr-duration-fast)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)] disabled:cursor-not-allowed\"\n :class=\"jumperClass\"\n @keydown.enter=\"submitJumper\"\n @blur=\"submitJumper\"\n >\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, ref, watch } from 'vue'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrSelect from '../GrSelect/GrSelect.vue'\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport {\n type GrPaginationSize,\n ellipsisSizes,\n jumperSizes,\n labelSizes,\n navButtonSizes,\n pageListGaps,\n pageSizes,\n pageSizeSelectWidths,\n rowGaps,\n} from './grPaginationStyles'\n\n/**\n * Пропы пагинации.\n *\n * `total` интерпретируется как общее количество элементов; число страниц\n * вычисляется из `total` и `pageSize`. Номера усекаются многоточием\n * (алгоритм boundary/sibling, как у MUI): всегда видны первая/последняя страница\n * и `siblingCount` соседей вокруг текущей.\n */\nexport interface GrPaginationProps {\n page: number\n pageSize: number\n total: number\n pageSizes?: number[]\n /** Сколько соседних страниц показывать вокруг текущей. По умолчанию `1`. */\n siblingCount?: number\n /** Сколько крайних страниц всегда показывать с каждого края. По умолчанию `1`. */\n boundaryCount?: number\n /**\n * Компактный вариант: вместо нумерованных страниц показывается индикатор\n * «текущая / всего» — удобно для узких мест (мобайл, тулбары таблиц).\n */\n compact?: boolean\n /** Показывать поле «перейти к странице» с быстрым переходом по вводу номера. */\n showJumper?: boolean\n /** Показывать селект размера страницы. */\n showPageSize?: boolean\n /** Показывать диапазон показанных элементов — «1–20 из 137». Слот `#total` сильнее. */\n showTotal?: boolean\n /** i18n-подпись перед полем перехода. По умолчанию — `gr.pagination.jumpTo`. */\n jumperLabel?: string\n /**\n * Имя навигационного лендмарка. По умолчанию — `gr.pagination.label`; задавать\n * стоит там, где пагинаций на странице несколько и их надо различать.\n */\n ariaLabel?: string\n /** Гасит всю пагинацию: номера, кнопки, селект размера и поле перехода. */\n disabled?: boolean\n size?: GrPaginationSize\n}\n\nexport interface GrPaginationEmits {\n (e: 'update:page', value: number): void\n (e: 'update:pageSize', value: number): void\n}\n\nconst { t } = useGranularityTranslations()\n\nconst props = withDefaults(defineProps<GrPaginationProps>(), {\n pageSizes: () => [10, 20, 50],\n siblingCount: 1,\n boundaryCount: 1,\n compact: false,\n showJumper: false,\n showPageSize: false,\n showTotal: false,\n jumperLabel: undefined,\n ariaLabel: undefined,\n disabled: false,\n size: undefined,\n})\n\ndefineSlots<{\n /** Диапазон показанных элементов целиком — вместо строки из локали. */\n total?: (props: { from: number, to: number, total: number }) => unknown\n}>()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrPagination' })\n\nconst rowClass = computed(() => rowGaps[resolvedSize.value])\nconst pageListClass = computed(() => pageListGaps[resolvedSize.value])\nconst pageClass = computed(() => pageSizes[resolvedSize.value])\nconst jumperClass = computed(() => jumperSizes[resolvedSize.value])\nconst ellipsisClass = computed(() => ellipsisSizes[resolvedSize.value])\nconst labelClass = computed(() => labelSizes[resolvedSize.value])\nconst selectWrapClass = computed(() => pageSizeSelectWidths[resolvedSize.value])\nconst navButtonSize = computed(() => navButtonSizes[resolvedSize.value])\n\nconst emit = defineEmits<GrPaginationEmits>()\n\n// Делитель клампится: `pageSize: 0` дал бы `Infinity` страниц и бесконечный\n// цикл в `range()`.\nconst resolvedPageSize = computed(() => Math.max(1, Math.trunc(props.pageSize)))\nconst pageCount = computed(() => Math.max(1, Math.ceil(props.total / resolvedPageSize.value)))\n\n// Рендер идёт от зажатого номера: вотчер ниже просит родителя подтянуть `page`,\n// но на первом рендере он молчит, и без клампа пагинация осталась бы вовсе без\n// активной страницы.\nconst currentPage = computed(() => Math.min(Math.max(Math.trunc(props.page), 1), pageCount.value))\n\n// Контролируемый компонент: если `total`/`pageSize` уменьшились и текущая `page`\n// вышла за диапазон — просим родителя подтянуть её к последней странице.\nwatch(pageCount, (count) => {\n if (props.page > count)\n emit('update:page', count)\n})\n\nconst pageSizeModel = computed({\n get: () => String(props.pageSize),\n set: (value: string) => emit('update:pageSize', Number(value)),\n})\n\nconst pageSizeOptions = computed(() =>\n props.pageSizes.map(pageSize => ({\n value: String(pageSize),\n label: String(pageSize),\n })),\n)\n\nconst rangeFrom = computed(() => (props.total === 0 ? 0 : (currentPage.value - 1) * resolvedPageSize.value + 1))\nconst rangeTo = computed(() => Math.min(currentPage.value * resolvedPageSize.value, props.total))\n\ntype PaginationItem = number | 'ellipsis-start' | 'ellipsis-end'\n\nfunction range(start: number, end: number): number[] {\n const list: number[] = []\n for (let page = start; page <= end; page += 1)\n list.push(page)\n return list\n}\n\n// Алгоритм usePagination как в MUI: крайние `boundaryCount` страниц + `siblingCount`\n// соседей вокруг текущей, разрывы схлопываются в многоточие. Одиночный «пропущенный»\n// номер показываем как номер, а не как «…» (визуально ровнее).\nconst items = computed<PaginationItem[]>(() => {\n const count = pageCount.value\n const current = currentPage.value\n const boundaryCount = Math.max(0, props.boundaryCount)\n const siblingCount = Math.max(0, props.siblingCount)\n\n const startPages = range(1, Math.min(boundaryCount, count))\n const endPages = range(Math.max(count - boundaryCount + 1, boundaryCount + 1), count)\n\n const siblingsStart = Math.max(\n Math.min(current - siblingCount, count - boundaryCount - siblingCount * 2 - 1),\n boundaryCount + 2,\n )\n const siblingsEnd = Math.min(\n Math.max(current + siblingCount, boundaryCount + siblingCount * 2 + 2),\n endPages.length > 0 ? endPages[0] - 2 : count - 1,\n )\n\n return [\n ...startPages,\n ...(siblingsStart > boundaryCount + 2\n ? ['ellipsis-start' as const]\n : boundaryCount + 1 < count - boundaryCount\n ? [boundaryCount + 1]\n : []),\n ...range(siblingsStart, siblingsEnd),\n ...(siblingsEnd < count - boundaryCount - 1\n ? ['ellipsis-end' as const]\n : count - boundaryCount > boundaryCount\n ? [count - boundaryCount]\n : []),\n ...endPages,\n ]\n})\n\nfunction goTo(page: number): void {\n if (props.disabled)\n return\n\n emit('update:page', page)\n}\n\nfunction prev(): void {\n goTo(Math.max(1, currentPage.value - 1))\n}\n\nfunction next(): void {\n goTo(Math.min(pageCount.value, currentPage.value + 1))\n}\n\nfunction first(): void {\n goTo(1)\n}\n\nfunction last(): void {\n goTo(pageCount.value)\n}\n\n// «Перейти к странице»: клампим введённый номер к диапазону [1, pageCount]\n// и сбрасываем поле после перехода.\nconst jumperValue = ref('')\n\nfunction submitJumper(): void {\n const parsed = Number.parseInt(jumperValue.value, 10)\n jumperValue.value = ''\n if (Number.isNaN(parsed))\n return\n\n goTo(Math.min(pageCount.value, Math.max(1, parsed)))\n}\n\nconst resolvedJumperLabel = computed(() => props.jumperLabel ?? t('gr.pagination.jumpTo', 'Go to'))\nconst resolvedAriaLabel = computed(() => props.ariaLabel ?? t('gr.pagination.label', 'Pagination'))\nconst statusText = computed(() =>\n t('gr.pagination.status', 'Page {page} of {count}', { page: currentPage.value, count: pageCount.value }),\n)\nconst totalText = computed(() =>\n t('gr.pagination.total', '{from}–{to} of {total}', { from: rangeFrom.value, to: rangeTo.value, total: props.total }),\n)\n</script>\n\n<template>\n <div\n class=\"flex flex-wrap items-center justify-end\"\n :class=\"rowClass\"\n data-gr-pagination\n role=\"navigation\"\n :aria-label=\"resolvedAriaLabel\"\n >\n <div\n v-if=\"showTotal\"\n data-gr-pagination-total\n class=\"text-[var(--gr-muted-fg)] tabular-nums\"\n :class=\"labelClass\"\n >\n <slot name=\"total\" :from=\"rangeFrom\" :to=\"rangeTo\" :total=\"total\">\n {{ totalText }}\n </slot>\n </div>\n\n <div v-if=\"showPageSize\" :class=\"selectWrapClass\">\n <GrSelect\n v-model=\"pageSizeModel\"\n :options=\"pageSizeOptions\"\n :size=\"resolvedSize\"\n :disabled=\"disabled\"\n :aria-label=\"t('gr.pagination.pageSize', 'Page size')\"\n />\n </div>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage <= 1\" :aria-label=\"t('gr.pagination.first', 'First page')\" data-gr-pagination-first @click=\"first\">\n «\n </GrButton>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage <= 1\" data-gr-pagination-prev @click=\"prev\">\n {{ t('gr.pagination.prev', 'Prev') }}\n </GrButton>\n\n <!-- Компактный вариант: индикатор «текущая / всего» вместо нумерованных страниц. -->\n <div\n v-if=\"compact\"\n data-gr-pagination-compact\n class=\"px-2 text-[var(--gr-fg)] tabular-nums\"\n :class=\"labelClass\"\n role=\"status\"\n >\n {{ currentPage }} / {{ pageCount }}\n </div>\n\n <template v-else>\n <ul class=\"flex items-center [list-style:none]\" :class=\"pageListClass\" data-gr-pagination-pages role=\"list\">\n <li v-for=\"(item, index) in items\" :key=\"index\" :aria-hidden=\"typeof item === 'string' ? 'true' : undefined\">\n <span\n v-if=\"item === 'ellipsis-start' || item === 'ellipsis-end'\"\n data-gr-pagination-ellipsis\n class=\"grid place-items-center text-[var(--gr-muted-fg)]\"\n :class=\"ellipsisClass\"\n >…</span>\n <button\n v-else\n type=\"button\"\n data-gr-pagination-page\n :disabled=\"disabled\"\n :aria-current=\"item === currentPage ? 'page' : undefined\"\n :aria-label=\"t('gr.pagination.page', 'Page {n}', { n: item })\"\n class=\"rounded-[var(--gr-radius-control)] font-600 transition-colors duration-[var(--gr-duration-fast)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)] disabled:cursor-not-allowed\"\n :class=\"[\n pageClass,\n item === currentPage\n ? 'bg-[var(--gr-primary)] text-[var(--gr-primary-fg)]'\n // Подложка тональная, а не `--gr-muted`: серый на белом фоне даёт\n // 1.10:1 и на светлой теме hover был практически не виден, хотя у\n // соседних «назад/вперёд» (ghost-кнопки) он тонирован. Ряд обязан\n // читаться одним контролом.\n : 'text-[var(--gr-muted-fg)] hover:bg-[var(--gr-accent)] hover:text-[var(--gr-accent-fg)]',\n ]\"\n @click=\"goTo(item)\"\n >\n {{ item }}\n </button>\n </li>\n </ul>\n\n <!-- В компактном режиме смену страницы объявляет видимый индикатор. -->\n <div data-gr-pagination-status role=\"status\" class=\"sr-only\">\n {{ statusText }}\n </div>\n </template>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage >= pageCount\" data-gr-pagination-next @click=\"next\">\n {{ t('gr.pagination.next', 'Next') }}\n </GrButton>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage >= pageCount\" :aria-label=\"t('gr.pagination.last', 'Last page')\" data-gr-pagination-last @click=\"last\">\n »\n </GrButton>\n\n <!-- «Перейти к странице»: Enter или blur применяют введённый номер. -->\n <div v-if=\"showJumper\" class=\"flex items-center gap-2 text-[var(--gr-muted-fg)]\" :class=\"labelClass\">\n <span>{{ resolvedJumperLabel }}</span>\n <input\n v-model=\"jumperValue\"\n type=\"number\"\n min=\"1\"\n :max=\"pageCount\"\n inputmode=\"numeric\"\n data-gr-pagination-jumper\n :disabled=\"disabled\"\n :aria-label=\"resolvedJumperLabel\"\n class=\"rounded-[var(--gr-radius-control)] border border-[var(--gr-brd)] bg-[var(--gr-bg)] px-2 text-center text-[var(--gr-fg)] tabular-nums transition-colors duration-[var(--gr-duration-fast)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)] disabled:cursor-not-allowed\"\n :class=\"jumperClass\"\n @keydown.enter=\"submitJumper\"\n @blur=\"submitJumper\"\n >\n </div>\n </div>\n</template>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"GrPagination-FiiXzVyv.js","names":[],"sources":["../../../../src/components/GrPagination/GrPagination.vue","../../../../src/components/GrPagination/GrPagination.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref, watch } from 'vue'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrSelect from '../GrSelect/GrSelect.vue'\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport {\n type GrPaginationSize,\n ellipsisSizes,\n jumperSizes,\n labelSizes,\n navButtonSizes,\n pageListGaps,\n pageSizes,\n pageSizeSelectWidths,\n rowGaps,\n} from './grPaginationStyles'\n\n/**\n * Пропы пагинации.\n *\n * `total` интерпретируется как общее количество элементов; число страниц\n * вычисляется из `total` и `pageSize`. Номера усекаются многоточием\n * (алгоритм boundary/sibling, как у MUI): всегда видны первая/последняя страница\n * и `siblingCount` соседей вокруг текущей.\n */\nexport interface GrPaginationProps {\n page: number\n pageSize: number\n total: number\n pageSizes?: number[]\n /** Сколько соседних страниц показывать вокруг текущей. По умолчанию `1`. */\n siblingCount?: number\n /** Сколько крайних страниц всегда показывать с каждого края. По умолчанию `1`. */\n boundaryCount?: number\n /**\n * Компактный вариант: вместо нумерованных страниц показывается индикатор\n * «текущая / всего» — удобно для узких мест (мобайл, тулбары таблиц).\n */\n compact?: boolean\n /** Показывать поле «перейти к странице» с быстрым переходом по вводу номера. */\n showJumper?: boolean\n /** Показывать селект размера страницы. */\n showPageSize?: boolean\n /** Показывать диапазон показанных элементов — «1–20 из 137». Слот `#total` сильнее. */\n showTotal?: boolean\n /** i18n-подпись перед полем перехода. По умолчанию — `gr.pagination.jumpTo`. */\n jumperLabel?: string\n /**\n * Имя навигационного лендмарка. По умолчанию — `gr.pagination.label`; задавать\n * стоит там, где пагинаций на странице несколько и их надо различать.\n */\n ariaLabel?: string\n /** Гасит всю пагинацию: номера, кнопки, селект размера и поле перехода. */\n disabled?: boolean\n size?: GrPaginationSize\n}\n\nexport interface GrPaginationEmits {\n (e: 'update:page', value: number): void\n (e: 'update:pageSize', value: number): void\n}\n\nconst { t } = useGranularityTranslations()\n\nconst props = withDefaults(defineProps<GrPaginationProps>(), {\n pageSizes: () => [10, 20, 50],\n siblingCount: 1,\n boundaryCount: 1,\n compact: false,\n showJumper: false,\n showPageSize: false,\n showTotal: false,\n jumperLabel: undefined,\n ariaLabel: undefined,\n disabled: false,\n size: undefined,\n})\n\ndefineSlots<{\n /** Диапазон показанных элементов целиком — вместо строки из локали. */\n total?: (props: { from: number, to: number, total: number }) => unknown\n}>()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrPagination' })\n\nconst rowClass = computed(() => rowGaps[resolvedSize.value])\nconst pageListClass = computed(() => pageListGaps[resolvedSize.value])\nconst pageClass = computed(() => pageSizes[resolvedSize.value])\nconst jumperClass = computed(() => jumperSizes[resolvedSize.value])\nconst ellipsisClass = computed(() => ellipsisSizes[resolvedSize.value])\nconst labelClass = computed(() => labelSizes[resolvedSize.value])\nconst selectWrapClass = computed(() => pageSizeSelectWidths[resolvedSize.value])\nconst navButtonSize = computed(() => navButtonSizes[resolvedSize.value])\n\nconst emit = defineEmits<GrPaginationEmits>()\n\n// Делитель клампится: `pageSize: 0` дал бы `Infinity` страниц и бесконечный\n// цикл в `range()`.\nconst resolvedPageSize = computed(() => Math.max(1, Math.trunc(props.pageSize)))\nconst pageCount = computed(() => Math.max(1, Math.ceil(props.total / resolvedPageSize.value)))\n\n// Рендер идёт от зажатого номера: вотчер ниже просит родителя подтянуть `page`,\n// но на первом рендере он молчит, и без клампа пагинация осталась бы вовсе без\n// активной страницы.\nconst currentPage = computed(() => Math.min(Math.max(Math.trunc(props.page), 1), pageCount.value))\n\n// Контролируемый компонент: если `total`/`pageSize` уменьшились и текущая `page`\n// вышла за диапазон — просим родителя подтянуть её к последней странице.\nwatch(pageCount, (count) => {\n if (props.page > count)\n emit('update:page', count)\n})\n\nconst pageSizeModel = computed({\n get: () => String(props.pageSize),\n set: (value: string) => emit('update:pageSize', Number(value)),\n})\n\nconst pageSizeOptions = computed(() =>\n props.pageSizes.map(pageSize => ({\n value: String(pageSize),\n label: String(pageSize),\n })),\n)\n\nconst rangeFrom = computed(() => (props.total === 0 ? 0 : (currentPage.value - 1) * resolvedPageSize.value + 1))\nconst rangeTo = computed(() => Math.min(currentPage.value * resolvedPageSize.value, props.total))\n\ntype PaginationItem = number | 'ellipsis-start' | 'ellipsis-end'\n\nfunction range(start: number, end: number): number[] {\n const list: number[] = []\n for (let page = start; page <= end; page += 1)\n list.push(page)\n return list\n}\n\n// Алгоритм usePagination как в MUI: крайние `boundaryCount` страниц + `siblingCount`\n// соседей вокруг текущей, разрывы схлопываются в многоточие. Одиночный «пропущенный»\n// номер показываем как номер, а не как «…» (визуально ровнее).\nconst items = computed<PaginationItem[]>(() => {\n const count = pageCount.value\n const current = currentPage.value\n const boundaryCount = Math.max(0, props.boundaryCount)\n const siblingCount = Math.max(0, props.siblingCount)\n\n const startPages = range(1, Math.min(boundaryCount, count))\n const endPages = range(Math.max(count - boundaryCount + 1, boundaryCount + 1), count)\n\n const siblingsStart = Math.max(\n Math.min(current - siblingCount, count - boundaryCount - siblingCount * 2 - 1),\n boundaryCount + 2,\n )\n const siblingsEnd = Math.min(\n Math.max(current + siblingCount, boundaryCount + siblingCount * 2 + 2),\n endPages.length > 0 ? endPages[0] - 2 : count - 1,\n )\n\n return [\n ...startPages,\n ...(siblingsStart > boundaryCount + 2\n ? ['ellipsis-start' as const]\n : boundaryCount + 1 < count - boundaryCount\n ? [boundaryCount + 1]\n : []),\n ...range(siblingsStart, siblingsEnd),\n ...(siblingsEnd < count - boundaryCount - 1\n ? ['ellipsis-end' as const]\n : count - boundaryCount > boundaryCount\n ? [count - boundaryCount]\n : []),\n ...endPages,\n ]\n})\n\nfunction goTo(page: number): void {\n if (props.disabled)\n return\n\n emit('update:page', page)\n}\n\nfunction prev(): void {\n goTo(Math.max(1, currentPage.value - 1))\n}\n\nfunction next(): void {\n goTo(Math.min(pageCount.value, currentPage.value + 1))\n}\n\nfunction first(): void {\n goTo(1)\n}\n\nfunction last(): void {\n goTo(pageCount.value)\n}\n\n// «Перейти к странице»: клампим введённый номер к диапазону [1, pageCount]\n// и сбрасываем поле после перехода.\nconst jumperValue = ref('')\n\nfunction submitJumper(): void {\n const parsed = Number.parseInt(jumperValue.value, 10)\n jumperValue.value = ''\n if (Number.isNaN(parsed))\n return\n\n goTo(Math.min(pageCount.value, Math.max(1, parsed)))\n}\n\nconst resolvedJumperLabel = computed(() => props.jumperLabel ?? t('gr.pagination.jumpTo', 'Go to'))\nconst resolvedAriaLabel = computed(() => props.ariaLabel ?? t('gr.pagination.label', 'Pagination'))\nconst statusText = computed(() =>\n t('gr.pagination.status', 'Page {page} of {count}', { page: currentPage.value, count: pageCount.value }),\n)\nconst totalText = computed(() =>\n t('gr.pagination.total', '{from}–{to} of {total}', { from: rangeFrom.value, to: rangeTo.value, total: props.total }),\n)\n</script>\n\n<template>\n <div\n class=\"flex flex-wrap items-center justify-end\"\n :class=\"rowClass\"\n data-gr-pagination\n role=\"navigation\"\n :aria-label=\"resolvedAriaLabel\"\n >\n <div\n v-if=\"showTotal\"\n data-gr-pagination-total\n class=\"text-[var(--gr-muted-fg)] tabular-nums\"\n :class=\"labelClass\"\n >\n <slot name=\"total\" :from=\"rangeFrom\" :to=\"rangeTo\" :total=\"total\">\n {{ totalText }}\n </slot>\n </div>\n\n <div v-if=\"showPageSize\" :class=\"selectWrapClass\">\n <GrSelect\n v-model=\"pageSizeModel\"\n :options=\"pageSizeOptions\"\n :size=\"resolvedSize\"\n :disabled=\"disabled\"\n :aria-label=\"t('gr.pagination.pageSize', 'Page size')\"\n />\n </div>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage <= 1\" :aria-label=\"t('gr.pagination.first', 'First page')\" data-gr-pagination-first @click=\"first\">\n «\n </GrButton>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage <= 1\" data-gr-pagination-prev @click=\"prev\">\n {{ t('gr.pagination.prev', 'Prev') }}\n </GrButton>\n\n <!-- Компактный вариант: индикатор «текущая / всего» вместо нумерованных страниц. -->\n <div\n v-if=\"compact\"\n data-gr-pagination-compact\n class=\"px-2 text-[var(--gr-fg)] tabular-nums\"\n :class=\"labelClass\"\n role=\"status\"\n >\n {{ currentPage }} / {{ pageCount }}\n </div>\n\n <template v-else>\n <ul class=\"flex items-center [list-style:none]\" :class=\"pageListClass\" data-gr-pagination-pages role=\"list\">\n <li v-for=\"(item, index) in items\" :key=\"index\" :aria-hidden=\"typeof item === 'string' ? 'true' : undefined\">\n <span\n v-if=\"item === 'ellipsis-start' || item === 'ellipsis-end'\"\n data-gr-pagination-ellipsis\n class=\"grid place-items-center text-[var(--gr-muted-fg)]\"\n :class=\"ellipsisClass\"\n >…</span>\n <button\n v-else\n type=\"button\"\n data-gr-pagination-page\n :disabled=\"disabled\"\n :aria-current=\"item === currentPage ? 'page' : undefined\"\n :aria-label=\"t('gr.pagination.page', 'Page {n}', { n: item })\"\n class=\"rounded-[var(--gr-radius-control)] font-600 transition-colors duration-[var(--gr-duration-fast)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)] disabled:cursor-not-allowed\"\n :class=\"[\n pageClass,\n item === currentPage\n ? 'bg-[var(--gr-primary)] text-[var(--gr-primary-fg)]'\n // Подложка тональная, а не `--gr-muted`: серый на белом фоне даёт\n // 1.10:1 и на светлой теме hover был практически не виден, хотя у\n // соседних «назад/вперёд» (ghost-кнопки) он тонирован. Ряд обязан\n // читаться одним контролом.\n : 'text-[var(--gr-muted-fg)] hover:bg-[var(--gr-accent)] hover:text-[var(--gr-accent-fg)]',\n ]\"\n @click=\"goTo(item)\"\n >\n {{ item }}\n </button>\n </li>\n </ul>\n\n <!-- В компактном режиме смену страницы объявляет видимый индикатор. -->\n <div data-gr-pagination-status role=\"status\" class=\"sr-only\">\n {{ statusText }}\n </div>\n </template>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage >= pageCount\" data-gr-pagination-next @click=\"next\">\n {{ t('gr.pagination.next', 'Next') }}\n </GrButton>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage >= pageCount\" :aria-label=\"t('gr.pagination.last', 'Last page')\" data-gr-pagination-last @click=\"last\">\n »\n </GrButton>\n\n <!-- «Перейти к странице»: Enter или blur применяют введённый номер. -->\n <div v-if=\"showJumper\" class=\"flex items-center gap-2 text-[var(--gr-muted-fg)]\" :class=\"labelClass\">\n <span>{{ resolvedJumperLabel }}</span>\n <input\n v-model=\"jumperValue\"\n type=\"number\"\n min=\"1\"\n :max=\"pageCount\"\n inputmode=\"numeric\"\n data-gr-pagination-jumper\n :disabled=\"disabled\"\n :aria-label=\"resolvedJumperLabel\"\n class=\"rounded-[var(--gr-radius-control)] border border-[var(--gr-brd)] bg-[var(--gr-bg)] px-2 text-center text-[var(--gr-fg)] tabular-nums transition-colors duration-[var(--gr-duration-fast)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)] disabled:cursor-not-allowed\"\n :class=\"jumperClass\"\n @keydown.enter=\"submitJumper\"\n @blur=\"submitJumper\"\n >\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, ref, watch } from 'vue'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrSelect from '../GrSelect/GrSelect.vue'\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport {\n type GrPaginationSize,\n ellipsisSizes,\n jumperSizes,\n labelSizes,\n navButtonSizes,\n pageListGaps,\n pageSizes,\n pageSizeSelectWidths,\n rowGaps,\n} from './grPaginationStyles'\n\n/**\n * Пропы пагинации.\n *\n * `total` интерпретируется как общее количество элементов; число страниц\n * вычисляется из `total` и `pageSize`. Номера усекаются многоточием\n * (алгоритм boundary/sibling, как у MUI): всегда видны первая/последняя страница\n * и `siblingCount` соседей вокруг текущей.\n */\nexport interface GrPaginationProps {\n page: number\n pageSize: number\n total: number\n pageSizes?: number[]\n /** Сколько соседних страниц показывать вокруг текущей. По умолчанию `1`. */\n siblingCount?: number\n /** Сколько крайних страниц всегда показывать с каждого края. По умолчанию `1`. */\n boundaryCount?: number\n /**\n * Компактный вариант: вместо нумерованных страниц показывается индикатор\n * «текущая / всего» — удобно для узких мест (мобайл, тулбары таблиц).\n */\n compact?: boolean\n /** Показывать поле «перейти к странице» с быстрым переходом по вводу номера. */\n showJumper?: boolean\n /** Показывать селект размера страницы. */\n showPageSize?: boolean\n /** Показывать диапазон показанных элементов — «1–20 из 137». Слот `#total` сильнее. */\n showTotal?: boolean\n /** i18n-подпись перед полем перехода. По умолчанию — `gr.pagination.jumpTo`. */\n jumperLabel?: string\n /**\n * Имя навигационного лендмарка. По умолчанию — `gr.pagination.label`; задавать\n * стоит там, где пагинаций на странице несколько и их надо различать.\n */\n ariaLabel?: string\n /** Гасит всю пагинацию: номера, кнопки, селект размера и поле перехода. */\n disabled?: boolean\n size?: GrPaginationSize\n}\n\nexport interface GrPaginationEmits {\n (e: 'update:page', value: number): void\n (e: 'update:pageSize', value: number): void\n}\n\nconst { t } = useGranularityTranslations()\n\nconst props = withDefaults(defineProps<GrPaginationProps>(), {\n pageSizes: () => [10, 20, 50],\n siblingCount: 1,\n boundaryCount: 1,\n compact: false,\n showJumper: false,\n showPageSize: false,\n showTotal: false,\n jumperLabel: undefined,\n ariaLabel: undefined,\n disabled: false,\n size: undefined,\n})\n\ndefineSlots<{\n /** Диапазон показанных элементов целиком — вместо строки из локали. */\n total?: (props: { from: number, to: number, total: number }) => unknown\n}>()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrPagination' })\n\nconst rowClass = computed(() => rowGaps[resolvedSize.value])\nconst pageListClass = computed(() => pageListGaps[resolvedSize.value])\nconst pageClass = computed(() => pageSizes[resolvedSize.value])\nconst jumperClass = computed(() => jumperSizes[resolvedSize.value])\nconst ellipsisClass = computed(() => ellipsisSizes[resolvedSize.value])\nconst labelClass = computed(() => labelSizes[resolvedSize.value])\nconst selectWrapClass = computed(() => pageSizeSelectWidths[resolvedSize.value])\nconst navButtonSize = computed(() => navButtonSizes[resolvedSize.value])\n\nconst emit = defineEmits<GrPaginationEmits>()\n\n// Делитель клампится: `pageSize: 0` дал бы `Infinity` страниц и бесконечный\n// цикл в `range()`.\nconst resolvedPageSize = computed(() => Math.max(1, Math.trunc(props.pageSize)))\nconst pageCount = computed(() => Math.max(1, Math.ceil(props.total / resolvedPageSize.value)))\n\n// Рендер идёт от зажатого номера: вотчер ниже просит родителя подтянуть `page`,\n// но на первом рендере он молчит, и без клампа пагинация осталась бы вовсе без\n// активной страницы.\nconst currentPage = computed(() => Math.min(Math.max(Math.trunc(props.page), 1), pageCount.value))\n\n// Контролируемый компонент: если `total`/`pageSize` уменьшились и текущая `page`\n// вышла за диапазон — просим родителя подтянуть её к последней странице.\nwatch(pageCount, (count) => {\n if (props.page > count)\n emit('update:page', count)\n})\n\nconst pageSizeModel = computed({\n get: () => String(props.pageSize),\n set: (value: string) => emit('update:pageSize', Number(value)),\n})\n\nconst pageSizeOptions = computed(() =>\n props.pageSizes.map(pageSize => ({\n value: String(pageSize),\n label: String(pageSize),\n })),\n)\n\nconst rangeFrom = computed(() => (props.total === 0 ? 0 : (currentPage.value - 1) * resolvedPageSize.value + 1))\nconst rangeTo = computed(() => Math.min(currentPage.value * resolvedPageSize.value, props.total))\n\ntype PaginationItem = number | 'ellipsis-start' | 'ellipsis-end'\n\nfunction range(start: number, end: number): number[] {\n const list: number[] = []\n for (let page = start; page <= end; page += 1)\n list.push(page)\n return list\n}\n\n// Алгоритм usePagination как в MUI: крайние `boundaryCount` страниц + `siblingCount`\n// соседей вокруг текущей, разрывы схлопываются в многоточие. Одиночный «пропущенный»\n// номер показываем как номер, а не как «…» (визуально ровнее).\nconst items = computed<PaginationItem[]>(() => {\n const count = pageCount.value\n const current = currentPage.value\n const boundaryCount = Math.max(0, props.boundaryCount)\n const siblingCount = Math.max(0, props.siblingCount)\n\n const startPages = range(1, Math.min(boundaryCount, count))\n const endPages = range(Math.max(count - boundaryCount + 1, boundaryCount + 1), count)\n\n const siblingsStart = Math.max(\n Math.min(current - siblingCount, count - boundaryCount - siblingCount * 2 - 1),\n boundaryCount + 2,\n )\n const siblingsEnd = Math.min(\n Math.max(current + siblingCount, boundaryCount + siblingCount * 2 + 2),\n endPages.length > 0 ? endPages[0] - 2 : count - 1,\n )\n\n return [\n ...startPages,\n ...(siblingsStart > boundaryCount + 2\n ? ['ellipsis-start' as const]\n : boundaryCount + 1 < count - boundaryCount\n ? [boundaryCount + 1]\n : []),\n ...range(siblingsStart, siblingsEnd),\n ...(siblingsEnd < count - boundaryCount - 1\n ? ['ellipsis-end' as const]\n : count - boundaryCount > boundaryCount\n ? [count - boundaryCount]\n : []),\n ...endPages,\n ]\n})\n\nfunction goTo(page: number): void {\n if (props.disabled)\n return\n\n emit('update:page', page)\n}\n\nfunction prev(): void {\n goTo(Math.max(1, currentPage.value - 1))\n}\n\nfunction next(): void {\n goTo(Math.min(pageCount.value, currentPage.value + 1))\n}\n\nfunction first(): void {\n goTo(1)\n}\n\nfunction last(): void {\n goTo(pageCount.value)\n}\n\n// «Перейти к странице»: клампим введённый номер к диапазону [1, pageCount]\n// и сбрасываем поле после перехода.\nconst jumperValue = ref('')\n\nfunction submitJumper(): void {\n const parsed = Number.parseInt(jumperValue.value, 10)\n jumperValue.value = ''\n if (Number.isNaN(parsed))\n return\n\n goTo(Math.min(pageCount.value, Math.max(1, parsed)))\n}\n\nconst resolvedJumperLabel = computed(() => props.jumperLabel ?? t('gr.pagination.jumpTo', 'Go to'))\nconst resolvedAriaLabel = computed(() => props.ariaLabel ?? t('gr.pagination.label', 'Pagination'))\nconst statusText = computed(() =>\n t('gr.pagination.status', 'Page {page} of {count}', { page: currentPage.value, count: pageCount.value }),\n)\nconst totalText = computed(() =>\n t('gr.pagination.total', '{from}–{to} of {total}', { from: rangeFrom.value, to: rangeTo.value, total: props.total }),\n)\n</script>\n\n<template>\n <div\n class=\"flex flex-wrap items-center justify-end\"\n :class=\"rowClass\"\n data-gr-pagination\n role=\"navigation\"\n :aria-label=\"resolvedAriaLabel\"\n >\n <div\n v-if=\"showTotal\"\n data-gr-pagination-total\n class=\"text-[var(--gr-muted-fg)] tabular-nums\"\n :class=\"labelClass\"\n >\n <slot name=\"total\" :from=\"rangeFrom\" :to=\"rangeTo\" :total=\"total\">\n {{ totalText }}\n </slot>\n </div>\n\n <div v-if=\"showPageSize\" :class=\"selectWrapClass\">\n <GrSelect\n v-model=\"pageSizeModel\"\n :options=\"pageSizeOptions\"\n :size=\"resolvedSize\"\n :disabled=\"disabled\"\n :aria-label=\"t('gr.pagination.pageSize', 'Page size')\"\n />\n </div>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage <= 1\" :aria-label=\"t('gr.pagination.first', 'First page')\" data-gr-pagination-first @click=\"first\">\n «\n </GrButton>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage <= 1\" data-gr-pagination-prev @click=\"prev\">\n {{ t('gr.pagination.prev', 'Prev') }}\n </GrButton>\n\n <!-- Компактный вариант: индикатор «текущая / всего» вместо нумерованных страниц. -->\n <div\n v-if=\"compact\"\n data-gr-pagination-compact\n class=\"px-2 text-[var(--gr-fg)] tabular-nums\"\n :class=\"labelClass\"\n role=\"status\"\n >\n {{ currentPage }} / {{ pageCount }}\n </div>\n\n <template v-else>\n <ul class=\"flex items-center [list-style:none]\" :class=\"pageListClass\" data-gr-pagination-pages role=\"list\">\n <li v-for=\"(item, index) in items\" :key=\"index\" :aria-hidden=\"typeof item === 'string' ? 'true' : undefined\">\n <span\n v-if=\"item === 'ellipsis-start' || item === 'ellipsis-end'\"\n data-gr-pagination-ellipsis\n class=\"grid place-items-center text-[var(--gr-muted-fg)]\"\n :class=\"ellipsisClass\"\n >…</span>\n <button\n v-else\n type=\"button\"\n data-gr-pagination-page\n :disabled=\"disabled\"\n :aria-current=\"item === currentPage ? 'page' : undefined\"\n :aria-label=\"t('gr.pagination.page', 'Page {n}', { n: item })\"\n class=\"rounded-[var(--gr-radius-control)] font-600 transition-colors duration-[var(--gr-duration-fast)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)] disabled:cursor-not-allowed\"\n :class=\"[\n pageClass,\n item === currentPage\n ? 'bg-[var(--gr-primary)] text-[var(--gr-primary-fg)]'\n // Подложка тональная, а не `--gr-muted`: серый на белом фоне даёт\n // 1.10:1 и на светлой теме hover был практически не виден, хотя у\n // соседних «назад/вперёд» (ghost-кнопки) он тонирован. Ряд обязан\n // читаться одним контролом.\n : 'text-[var(--gr-muted-fg)] hover:bg-[var(--gr-accent)] hover:text-[var(--gr-accent-fg)]',\n ]\"\n @click=\"goTo(item)\"\n >\n {{ item }}\n </button>\n </li>\n </ul>\n\n <!-- В компактном режиме смену страницы объявляет видимый индикатор. -->\n <div data-gr-pagination-status role=\"status\" class=\"sr-only\">\n {{ statusText }}\n </div>\n </template>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage >= pageCount\" data-gr-pagination-next @click=\"next\">\n {{ t('gr.pagination.next', 'Next') }}\n </GrButton>\n\n <GrButton variant=\"ghost\" :size=\"navButtonSize\" :disabled=\"disabled || currentPage >= pageCount\" :aria-label=\"t('gr.pagination.last', 'Last page')\" data-gr-pagination-last @click=\"last\">\n »\n </GrButton>\n\n <!-- «Перейти к странице»: Enter или blur применяют введённый номер. -->\n <div v-if=\"showJumper\" class=\"flex items-center gap-2 text-[var(--gr-muted-fg)]\" :class=\"labelClass\">\n <span>{{ resolvedJumperLabel }}</span>\n <input\n v-model=\"jumperValue\"\n type=\"number\"\n min=\"1\"\n :max=\"pageCount\"\n inputmode=\"numeric\"\n data-gr-pagination-jumper\n :disabled=\"disabled\"\n :aria-label=\"resolvedJumperLabel\"\n class=\"rounded-[var(--gr-radius-control)] border border-[var(--gr-brd)] bg-[var(--gr-bg)] px-2 text-center text-[var(--gr-fg)] tabular-nums transition-colors duration-[var(--gr-duration-fast)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)] disabled:cursor-not-allowed\"\n :class=\"jumperClass\"\n @keydown.enter=\"submitJumper\"\n @blur=\"submitJumper\"\n >\n </div>\n </div>\n</template>\n"],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { n as grPaginationSafelist, t as grPaginationConfig } from "../../chunks/config-CjKpUcmy.js";
|
|
2
|
-
import { t as GrPagination_default } from "./chunks/GrPagination-
|
|
2
|
+
import { t as GrPagination_default } from "./chunks/GrPagination-FiiXzVyv.js";
|
|
3
3
|
export { GrPagination_default as GrPagination, GrPagination_default as default, grPaginationConfig, grPaginationSafelist };
|
|
@@ -3,7 +3,7 @@ import { vClickOutside } from "../../../directives/clickOutside.js";
|
|
|
3
3
|
import { createFloatingAnchor, useFloating } from "../../../composables/useFloating.js";
|
|
4
4
|
import { t as useControlledOpen } from "../../../chunks/useControlledOpen-DV3SqLTP.js";
|
|
5
5
|
import { t as panelPopTransition } from "../../../chunks/overlayTransition-0Fj5tQbq.js";
|
|
6
|
-
import { t as useModalOverlay } from "../../../chunks/useModalOverlay-
|
|
6
|
+
import { t as useModalOverlay } from "../../../chunks/useModalOverlay-D__CqLGn.js";
|
|
7
7
|
import { t as grPopoverPanelClass } from "../../../chunks/grPopoverStyles-Djh5RNhn.js";
|
|
8
8
|
import { Teleport, Transition, computed, createBlock, createCommentVNode, createElementBlock, createVNode, defineComponent, mergeProps, nextTick, openBlock, ref, renderSlot, unref, useId, useSlots, vShow, watch, withCtx, withDirectives } from "vue";
|
|
9
9
|
//#region src/components/GrPopover/GrPopover.vue?vue&type=script&setup=true&lang.ts
|
package/dist/components/GrPopover/chunks/{GrPopover-CofG-Tfe.js.map → GrPopover-faJanmGs.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GrPopover-CofG-Tfe.js","names":[],"sources":["../../../../src/components/GrPopover/GrPopover.vue","../../../../src/components/GrPopover/GrPopover.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * GrPopover — якорный немодальный оверлей с произвольным содержимым.\n *\n * Роль «универсального поповера» до сих пор насильно исполнял `GrDropdown`, но\n * он про меню: жёстко объявляет `role=\"menu\"`, `aria-haspopup=\"menu\"` и водит\n * фокус по пунктам. Для формы, подтверждения или палитры это неверная семантика.\n *\n * Здесь наоборот: позиционирование, слой и закрытие берёт на себя примитив, а\n * клавиатурный паттерн внутри панели остаётся за потребителем — поэтому `role`\n * задаётся пропом. На этом собираются `GrMenu` / `GrContextMenu` /\n * `GrColorPicker` / Popconfirm.\n *\n * A11y:\n * - триггер получает `aria-haspopup`/`aria-expanded`/`aria-controls` через\n * `triggerProps` (биндится на реальный фокусируемый элемент в слоте);\n * - панель — `role=\"dialog\"` по умолчанию и обязательное доступное имя\n * (`ariaLabel` либо `aria-labelledby` через `labelledBy`);\n * - Esc закрывает верхний слой стека, клик вне — закрывает по точке попадания;\n * - **по умолчанию фокус-ловушки нет намеренно**: Tab обязан уводить фокус\n * наружу, иначе немодальный слой запирает пользователя на странице, которая\n * не заблокирована. Проп `modal` включает второй режим — см. ниже.\n */\nimport { computed, nextTick, ref, useId, useSlots, watch } from 'vue'\n\nimport { useControlledOpen } from '../../composables/internal/useControlledOpen'\nimport { useModalOverlay } from '../../composables/internal/useModalOverlay'\nimport { createFloatingAnchor, type GrFloatingAnchorRect, useFloating, type UseFloatingPlacement } from '../../composables/useFloating'\nimport { vClickOutside } from '../../directives'\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport { panelPopTransition } from '../shared/overlayTransition'\nimport {\n type GrPopoverPadding,\n type GrPopoverRole,\n type GrPopoverSize,\n grPopoverPanelClass,\n} from './grPopoverStyles'\n\nexport interface GrPopoverProps {\n /**\n * Открыт ли поповер. Без этого пропа компонент ведёт состояние сам\n * (uncontrolled), с ним — слушайте `update:open`.\n */\n open?: boolean\n placement?: UseFloatingPlacement\n /** Зазор между триггером и панелью, px. */\n offsetPx?: number\n size?: GrPopoverSize\n /** Роль панели. Меняется теми, кто строит поверх примитива своё меню/список. */\n role?: GrPopoverRole\n /**\n * Модальный режим: фон уходит в `inert`, Tab ходит по кругу внутри панели,\n * скролл страницы блокируется, а слой встаёт в стек модальным — как окно.\n * Нужен поповеру с формой или подтверждением внутри: без изоляции фона\n * пользователь уводит фокус на страницу, к которой поповер и относится.\n *\n * В этом режиме фокус переносится в панель независимо от `autoFocus`: фон\n * недоступен, и слой без фокуса внутри стал бы клавиатурной ловушкой.\n */\n modal?: boolean\n /** Доступное имя панели. Обязательно для `role=\"dialog\"` без видимого заголовка. */\n ariaLabel?: string\n /** `id` видимого заголовка внутри панели — альтернатива `ariaLabel`. */\n labelledBy?: string\n /**\n * `manual` — открывать только программно (через `v-model:open`). Нужно для\n * контекстного меню и подтверждений, где момент открытия решает потребитель.\n */\n trigger?: 'click' | 'manual'\n closeOnEsc?: boolean\n closeOnClickOutside?: boolean\n /** Закрывать по клику внутри панели — удобно для меню, вредно для формы. */\n closeOnContentClick?: boolean\n /**\n * Переносить фокус на панель при открытии. Именно на панель, а не на первый\n * контрол внутри: сфокусировать поле ввода за пользователя — решение\n * содержимого, а не оболочки.\n */\n autoFocus?: boolean\n /**\n * Точечное переопределение точки монтирования. По умолчанию — общий портал\n * оверлеев (`#gr-portal` либо `portalTarget` из `GrConfigProvider`).\n */\n teleportTo?: string | HTMLElement\n contentClass?: string\n disabled?: boolean\n /**\n * Якорь-прямоугольник в координатах вьюпорта вместо обёртки слота `#trigger`:\n * панель встаёт у точки курсора или у строки списка. Нужен контекстному меню,\n * у которого триггера-элемента нет вовсе.\n *\n * `triggerProps` в этом режиме некому потребить, и своих ARIA-атрибутов\n * поповер никуда не вешает: `aria-haspopup` невалиден вне интерактивного\n * элемента, а `aria-expanded` без хозяина — шум. Связь с содержимым страницы\n * объявляет тот, кто открывает.\n */\n anchor?: GrFloatingAnchorRect | null\n /** Поле панели. `none` — содержимое рисует своё (меню, список опций). */\n padding?: GrPopoverPadding\n}\n\nexport interface GrPopoverEmits {\n (e: 'update:open', value: boolean): void\n}\n\nconst props = withDefaults(defineProps<GrPopoverProps>(), {\n open: undefined,\n placement: 'bottom-start',\n offsetPx: 8,\n size: undefined,\n role: 'dialog',\n modal: false,\n ariaLabel: undefined,\n labelledBy: undefined,\n trigger: 'click',\n closeOnEsc: true,\n closeOnClickOutside: true,\n closeOnContentClick: false,\n autoFocus: true,\n teleportTo: undefined,\n contentClass: undefined,\n disabled: false,\n anchor: undefined,\n padding: 'default',\n})\n\nconst emit = defineEmits<GrPopoverEmits>()\n\nconst slots = useSlots()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrPopover' })\n\nconst { open: isOpen, setOpen } = useControlledOpen(\n () => props.open,\n next => emit('update:open', next),\n)\n\nconst rootEl = ref<HTMLElement | null>(null)\nconst panelEl = ref<HTMLElement | null>(null)\nconst panelId = useId()\n\nconst clickOutsideExclude = [() => panelEl.value]\n\nfunction open(): void {\n if (props.disabled)\n return\n setOpen(true)\n}\n\nfunction close(): void {\n setOpen(false)\n}\n\nfunction toggle(): void {\n isOpen.value ? close() : open()\n}\n\n/**\n * Якорь создаётся один раз и читает координаты геттером: `useFloating`\n * пересоздаёт подписку при смене самой ссылки, а точка меняется на каждый\n * вызов — новый объект рвал бы `autoUpdate` без нужды.\n */\nconst anchorEl = createFloatingAnchor(() => props.anchor)\n\n/** Обёртка триггера существует только вместе со слотом — см. проп `anchor`. */\nconst hasTrigger = computed(() => Boolean(slots.trigger))\n\nconst { floatingStyle, resolvedPlacement, update: updateFloatingPosition } = useFloating(\n () => (props.anchor ? anchorEl : rootEl.value),\n panelEl,\n isOpen,\n {\n placement: () => props.placement,\n offsetPx: props.offsetPx,\n // Своего слоя у поповера нет намеренно: он и дропдаун — один класс якорных\n // немодальных оверлеев и обязаны лежать на одной высоте, иначе начнут\n // перекрывать друг друга в зависимости от порядка открытия.\n zIndexVar: '--gr-z-dropdown',\n },\n)\n\nwatch(() => props.placement, () => {\n if (isOpen.value)\n updateFloatingPosition()\n})\n\n// Объект якоря стабилен, поэтому смену координат `useFloating` сам не заметит:\n// повторный вызов у другой точки — это пересчёт позиции, а не переоткрытие.\nwatch(() => props.anchor, () => {\n if (isOpen.value)\n updateFloatingPosition()\n}, { deep: true })\n\n/**\n * Слой стека оверлеев: очередь Esc общая с модалками, поэтому поповер, открытый\n * внутри модалки, по Esc закрывает себя, а не её. Возврат фокуса на триггер\n * тоже отсюда — по правилу «только если на момент закрытия фокус ещё внутри».\n *\n * Модальность включается пропом и приходит той же сборкой, что у окна и drawer'а\n * (`useModalOverlay`): ловушка фокуса, `inert` фона, блокировка скролла. При\n * `modal: false` не включается ничего из этого — дефолтный поповер немодален.\n *\n * Корень слоя и панель здесь один элемент: якорный оверлей уезжает в портал сам,\n * без обёртки.\n */\nconst { inertAttr, portalTarget, teleportEnabled, themeAttrs } = useModalOverlay(isOpen, close, {\n modal: () => props.modal,\n closeOnEscape: () => props.closeOnEsc,\n panel: panelEl,\n root: panelEl,\n teleportTo: () => props.teleportTo,\n initialFocus: () => panelEl.value,\n})\n\n// Фокус переносим после отрисовки панели: до `nextTick` её ещё нет в DOM.\n// В модальном режиме перенос обязателен: фон в `inert`, и фокус, оставленный\n// снаружи, попал бы в недоступное поддерево.\nwatch(isOpen, async (next) => {\n if (!next || !(props.autoFocus || props.modal))\n return\n\n await nextTick()\n panelEl.value?.focus()\n})\n\n/** Роли, которые `aria-haspopup` действительно понимает. */\nconst HASPOPUP_ROLES = ['dialog', 'menu', 'listbox', 'grid'] as const\n\n/**\n * Пропсы для реального фокусируемого триггера: `<button v-bind=\"triggerProps\">`.\n * Клик вешается на обёртку, а ARIA — сюда, потому что `aria-expanded` обязан\n * жить на самом интерактивном элементе, а не на `div` вокруг него.\n */\nconst triggerProps = computed(() => ({\n // `aria-haspopup` принимает не любую роль: `group` в его списке нет, и\n // объявленный им триггер отдавал бы невалидный атрибут, который AT просто\n // отбрасывает. Роли вне списка сообщают о панели только через `aria-expanded`.\n 'aria-haspopup': HASPOPUP_ROLES.includes(props.role as (typeof HASPOPUP_ROLES)[number])\n ? (props.role as (typeof HASPOPUP_ROLES)[number])\n : undefined,\n 'aria-expanded': isOpen.value,\n 'aria-controls': isOpen.value ? panelId : undefined,\n 'disabled': props.disabled || undefined,\n}))\n\nfunction onTriggerClick(): void {\n if (props.trigger === 'click')\n toggle()\n}\n\nfunction onContentClick(): void {\n if (props.closeOnContentClick)\n close()\n}\n\nconst panelClasses = computed(() =>\n grPopoverPanelClass(resolvedSize.value, resolvedPlacement.value, props.contentClass, props.padding),\n)\n\ndefineExpose({ open, close, toggle })\n\ndefineSlots<{\n /**\n * Триггер панели. `triggerProps` обязаны попасть на сам интерактивный\n * элемент, а не на обёртку вокруг него: `aria-expanded` и `aria-controls`\n * читаются с того узла, который получает фокус.\n */\n trigger?: (props: {\n open: boolean\n toggle: () => void\n close: () => void\n triggerProps: Record<string, unknown>\n }) => any\n /** Содержимое панели. */\n content?: (props: { close: () => void }) => any\n}>()\n</script>\n\n<template>\n <div data-gr-popover>\n <div\n v-if=\"hasTrigger\"\n ref=\"rootEl\"\n v-click-outside=\"{ handler: close, enabled: isOpen && closeOnClickOutside, exclude: clickOutsideExclude }\"\n data-gr-popover-trigger\n class=\"inline-block max-w-full\"\n @click=\"onTriggerClick\"\n >\n <slot\n name=\"trigger\"\n :open=\"isOpen\"\n :toggle=\"toggle\"\n :close=\"close\"\n :trigger-props=\"triggerProps\"\n />\n </div>\n\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <transition\n :enter-active-class=\"panelPopTransition.enter\"\n :enter-from-class=\"panelPopTransition.enterFrom\"\n :enter-to-class=\"panelPopTransition.enterTo\"\n :leave-active-class=\"panelPopTransition.leave\"\n :leave-from-class=\"panelPopTransition.leaveFrom\"\n :leave-to-class=\"panelPopTransition.leaveTo\"\n >\n <div\n v-show=\"isOpen\"\n :id=\"panelId\"\n ref=\"panelEl\"\n v-click-outside=\"{ handler: close, enabled: isOpen && closeOnClickOutside && !hasTrigger }\"\n v-bind=\"themeAttrs\"\n data-gr-popover-panel\n data-gr-overlay-root\n :role=\"role === 'none' ? undefined : role\"\n :aria-modal=\"modal ? 'true' : undefined\"\n :inert=\"inertAttr\"\n :aria-label=\"labelledBy ? undefined : ariaLabel\"\n :aria-labelledby=\"labelledBy\"\n tabindex=\"-1\"\n :class=\"panelClasses\"\n :style=\"floatingStyle\"\n @click=\"onContentClick\"\n >\n <slot name=\"content\" :close=\"close\" />\n </div>\n </transition>\n </teleport>\n </div>\n</template>\n","<script setup lang=\"ts\">\n/**\n * GrPopover — якорный немодальный оверлей с произвольным содержимым.\n *\n * Роль «универсального поповера» до сих пор насильно исполнял `GrDropdown`, но\n * он про меню: жёстко объявляет `role=\"menu\"`, `aria-haspopup=\"menu\"` и водит\n * фокус по пунктам. Для формы, подтверждения или палитры это неверная семантика.\n *\n * Здесь наоборот: позиционирование, слой и закрытие берёт на себя примитив, а\n * клавиатурный паттерн внутри панели остаётся за потребителем — поэтому `role`\n * задаётся пропом. На этом собираются `GrMenu` / `GrContextMenu` /\n * `GrColorPicker` / Popconfirm.\n *\n * A11y:\n * - триггер получает `aria-haspopup`/`aria-expanded`/`aria-controls` через\n * `triggerProps` (биндится на реальный фокусируемый элемент в слоте);\n * - панель — `role=\"dialog\"` по умолчанию и обязательное доступное имя\n * (`ariaLabel` либо `aria-labelledby` через `labelledBy`);\n * - Esc закрывает верхний слой стека, клик вне — закрывает по точке попадания;\n * - **по умолчанию фокус-ловушки нет намеренно**: Tab обязан уводить фокус\n * наружу, иначе немодальный слой запирает пользователя на странице, которая\n * не заблокирована. Проп `modal` включает второй режим — см. ниже.\n */\nimport { computed, nextTick, ref, useId, useSlots, watch } from 'vue'\n\nimport { useControlledOpen } from '../../composables/internal/useControlledOpen'\nimport { useModalOverlay } from '../../composables/internal/useModalOverlay'\nimport { createFloatingAnchor, type GrFloatingAnchorRect, useFloating, type UseFloatingPlacement } from '../../composables/useFloating'\nimport { vClickOutside } from '../../directives'\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport { panelPopTransition } from '../shared/overlayTransition'\nimport {\n type GrPopoverPadding,\n type GrPopoverRole,\n type GrPopoverSize,\n grPopoverPanelClass,\n} from './grPopoverStyles'\n\nexport interface GrPopoverProps {\n /**\n * Открыт ли поповер. Без этого пропа компонент ведёт состояние сам\n * (uncontrolled), с ним — слушайте `update:open`.\n */\n open?: boolean\n placement?: UseFloatingPlacement\n /** Зазор между триггером и панелью, px. */\n offsetPx?: number\n size?: GrPopoverSize\n /** Роль панели. Меняется теми, кто строит поверх примитива своё меню/список. */\n role?: GrPopoverRole\n /**\n * Модальный режим: фон уходит в `inert`, Tab ходит по кругу внутри панели,\n * скролл страницы блокируется, а слой встаёт в стек модальным — как окно.\n * Нужен поповеру с формой или подтверждением внутри: без изоляции фона\n * пользователь уводит фокус на страницу, к которой поповер и относится.\n *\n * В этом режиме фокус переносится в панель независимо от `autoFocus`: фон\n * недоступен, и слой без фокуса внутри стал бы клавиатурной ловушкой.\n */\n modal?: boolean\n /** Доступное имя панели. Обязательно для `role=\"dialog\"` без видимого заголовка. */\n ariaLabel?: string\n /** `id` видимого заголовка внутри панели — альтернатива `ariaLabel`. */\n labelledBy?: string\n /**\n * `manual` — открывать только программно (через `v-model:open`). Нужно для\n * контекстного меню и подтверждений, где момент открытия решает потребитель.\n */\n trigger?: 'click' | 'manual'\n closeOnEsc?: boolean\n closeOnClickOutside?: boolean\n /** Закрывать по клику внутри панели — удобно для меню, вредно для формы. */\n closeOnContentClick?: boolean\n /**\n * Переносить фокус на панель при открытии. Именно на панель, а не на первый\n * контрол внутри: сфокусировать поле ввода за пользователя — решение\n * содержимого, а не оболочки.\n */\n autoFocus?: boolean\n /**\n * Точечное переопределение точки монтирования. По умолчанию — общий портал\n * оверлеев (`#gr-portal` либо `portalTarget` из `GrConfigProvider`).\n */\n teleportTo?: string | HTMLElement\n contentClass?: string\n disabled?: boolean\n /**\n * Якорь-прямоугольник в координатах вьюпорта вместо обёртки слота `#trigger`:\n * панель встаёт у точки курсора или у строки списка. Нужен контекстному меню,\n * у которого триггера-элемента нет вовсе.\n *\n * `triggerProps` в этом режиме некому потребить, и своих ARIA-атрибутов\n * поповер никуда не вешает: `aria-haspopup` невалиден вне интерактивного\n * элемента, а `aria-expanded` без хозяина — шум. Связь с содержимым страницы\n * объявляет тот, кто открывает.\n */\n anchor?: GrFloatingAnchorRect | null\n /** Поле панели. `none` — содержимое рисует своё (меню, список опций). */\n padding?: GrPopoverPadding\n}\n\nexport interface GrPopoverEmits {\n (e: 'update:open', value: boolean): void\n}\n\nconst props = withDefaults(defineProps<GrPopoverProps>(), {\n open: undefined,\n placement: 'bottom-start',\n offsetPx: 8,\n size: undefined,\n role: 'dialog',\n modal: false,\n ariaLabel: undefined,\n labelledBy: undefined,\n trigger: 'click',\n closeOnEsc: true,\n closeOnClickOutside: true,\n closeOnContentClick: false,\n autoFocus: true,\n teleportTo: undefined,\n contentClass: undefined,\n disabled: false,\n anchor: undefined,\n padding: 'default',\n})\n\nconst emit = defineEmits<GrPopoverEmits>()\n\nconst slots = useSlots()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrPopover' })\n\nconst { open: isOpen, setOpen } = useControlledOpen(\n () => props.open,\n next => emit('update:open', next),\n)\n\nconst rootEl = ref<HTMLElement | null>(null)\nconst panelEl = ref<HTMLElement | null>(null)\nconst panelId = useId()\n\nconst clickOutsideExclude = [() => panelEl.value]\n\nfunction open(): void {\n if (props.disabled)\n return\n setOpen(true)\n}\n\nfunction close(): void {\n setOpen(false)\n}\n\nfunction toggle(): void {\n isOpen.value ? close() : open()\n}\n\n/**\n * Якорь создаётся один раз и читает координаты геттером: `useFloating`\n * пересоздаёт подписку при смене самой ссылки, а точка меняется на каждый\n * вызов — новый объект рвал бы `autoUpdate` без нужды.\n */\nconst anchorEl = createFloatingAnchor(() => props.anchor)\n\n/** Обёртка триггера существует только вместе со слотом — см. проп `anchor`. */\nconst hasTrigger = computed(() => Boolean(slots.trigger))\n\nconst { floatingStyle, resolvedPlacement, update: updateFloatingPosition } = useFloating(\n () => (props.anchor ? anchorEl : rootEl.value),\n panelEl,\n isOpen,\n {\n placement: () => props.placement,\n offsetPx: props.offsetPx,\n // Своего слоя у поповера нет намеренно: он и дропдаун — один класс якорных\n // немодальных оверлеев и обязаны лежать на одной высоте, иначе начнут\n // перекрывать друг друга в зависимости от порядка открытия.\n zIndexVar: '--gr-z-dropdown',\n },\n)\n\nwatch(() => props.placement, () => {\n if (isOpen.value)\n updateFloatingPosition()\n})\n\n// Объект якоря стабилен, поэтому смену координат `useFloating` сам не заметит:\n// повторный вызов у другой точки — это пересчёт позиции, а не переоткрытие.\nwatch(() => props.anchor, () => {\n if (isOpen.value)\n updateFloatingPosition()\n}, { deep: true })\n\n/**\n * Слой стека оверлеев: очередь Esc общая с модалками, поэтому поповер, открытый\n * внутри модалки, по Esc закрывает себя, а не её. Возврат фокуса на триггер\n * тоже отсюда — по правилу «только если на момент закрытия фокус ещё внутри».\n *\n * Модальность включается пропом и приходит той же сборкой, что у окна и drawer'а\n * (`useModalOverlay`): ловушка фокуса, `inert` фона, блокировка скролла. При\n * `modal: false` не включается ничего из этого — дефолтный поповер немодален.\n *\n * Корень слоя и панель здесь один элемент: якорный оверлей уезжает в портал сам,\n * без обёртки.\n */\nconst { inertAttr, portalTarget, teleportEnabled, themeAttrs } = useModalOverlay(isOpen, close, {\n modal: () => props.modal,\n closeOnEscape: () => props.closeOnEsc,\n panel: panelEl,\n root: panelEl,\n teleportTo: () => props.teleportTo,\n initialFocus: () => panelEl.value,\n})\n\n// Фокус переносим после отрисовки панели: до `nextTick` её ещё нет в DOM.\n// В модальном режиме перенос обязателен: фон в `inert`, и фокус, оставленный\n// снаружи, попал бы в недоступное поддерево.\nwatch(isOpen, async (next) => {\n if (!next || !(props.autoFocus || props.modal))\n return\n\n await nextTick()\n panelEl.value?.focus()\n})\n\n/** Роли, которые `aria-haspopup` действительно понимает. */\nconst HASPOPUP_ROLES = ['dialog', 'menu', 'listbox', 'grid'] as const\n\n/**\n * Пропсы для реального фокусируемого триггера: `<button v-bind=\"triggerProps\">`.\n * Клик вешается на обёртку, а ARIA — сюда, потому что `aria-expanded` обязан\n * жить на самом интерактивном элементе, а не на `div` вокруг него.\n */\nconst triggerProps = computed(() => ({\n // `aria-haspopup` принимает не любую роль: `group` в его списке нет, и\n // объявленный им триггер отдавал бы невалидный атрибут, который AT просто\n // отбрасывает. Роли вне списка сообщают о панели только через `aria-expanded`.\n 'aria-haspopup': HASPOPUP_ROLES.includes(props.role as (typeof HASPOPUP_ROLES)[number])\n ? (props.role as (typeof HASPOPUP_ROLES)[number])\n : undefined,\n 'aria-expanded': isOpen.value,\n 'aria-controls': isOpen.value ? panelId : undefined,\n 'disabled': props.disabled || undefined,\n}))\n\nfunction onTriggerClick(): void {\n if (props.trigger === 'click')\n toggle()\n}\n\nfunction onContentClick(): void {\n if (props.closeOnContentClick)\n close()\n}\n\nconst panelClasses = computed(() =>\n grPopoverPanelClass(resolvedSize.value, resolvedPlacement.value, props.contentClass, props.padding),\n)\n\ndefineExpose({ open, close, toggle })\n\ndefineSlots<{\n /**\n * Триггер панели. `triggerProps` обязаны попасть на сам интерактивный\n * элемент, а не на обёртку вокруг него: `aria-expanded` и `aria-controls`\n * читаются с того узла, который получает фокус.\n */\n trigger?: (props: {\n open: boolean\n toggle: () => void\n close: () => void\n triggerProps: Record<string, unknown>\n }) => any\n /** Содержимое панели. */\n content?: (props: { close: () => void }) => any\n}>()\n</script>\n\n<template>\n <div data-gr-popover>\n <div\n v-if=\"hasTrigger\"\n ref=\"rootEl\"\n v-click-outside=\"{ handler: close, enabled: isOpen && closeOnClickOutside, exclude: clickOutsideExclude }\"\n data-gr-popover-trigger\n class=\"inline-block max-w-full\"\n @click=\"onTriggerClick\"\n >\n <slot\n name=\"trigger\"\n :open=\"isOpen\"\n :toggle=\"toggle\"\n :close=\"close\"\n :trigger-props=\"triggerProps\"\n />\n </div>\n\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <transition\n :enter-active-class=\"panelPopTransition.enter\"\n :enter-from-class=\"panelPopTransition.enterFrom\"\n :enter-to-class=\"panelPopTransition.enterTo\"\n :leave-active-class=\"panelPopTransition.leave\"\n :leave-from-class=\"panelPopTransition.leaveFrom\"\n :leave-to-class=\"panelPopTransition.leaveTo\"\n >\n <div\n v-show=\"isOpen\"\n :id=\"panelId\"\n ref=\"panelEl\"\n v-click-outside=\"{ handler: close, enabled: isOpen && closeOnClickOutside && !hasTrigger }\"\n v-bind=\"themeAttrs\"\n data-gr-popover-panel\n data-gr-overlay-root\n :role=\"role === 'none' ? undefined : role\"\n :aria-modal=\"modal ? 'true' : undefined\"\n :inert=\"inertAttr\"\n :aria-label=\"labelledBy ? undefined : ariaLabel\"\n :aria-labelledby=\"labelledBy\"\n tabindex=\"-1\"\n :class=\"panelClasses\"\n :style=\"floatingStyle\"\n @click=\"onContentClick\"\n >\n <slot name=\"content\" :close=\"close\" />\n </div>\n </transition>\n </teleport>\n </div>\n</template>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"GrPopover-faJanmGs.js","names":[],"sources":["../../../../src/components/GrPopover/GrPopover.vue","../../../../src/components/GrPopover/GrPopover.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * GrPopover — якорный немодальный оверлей с произвольным содержимым.\n *\n * Роль «универсального поповера» до сих пор насильно исполнял `GrDropdown`, но\n * он про меню: жёстко объявляет `role=\"menu\"`, `aria-haspopup=\"menu\"` и водит\n * фокус по пунктам. Для формы, подтверждения или палитры это неверная семантика.\n *\n * Здесь наоборот: позиционирование, слой и закрытие берёт на себя примитив, а\n * клавиатурный паттерн внутри панели остаётся за потребителем — поэтому `role`\n * задаётся пропом. На этом собираются `GrMenu` / `GrContextMenu` /\n * `GrColorPicker` / Popconfirm.\n *\n * A11y:\n * - триггер получает `aria-haspopup`/`aria-expanded`/`aria-controls` через\n * `triggerProps` (биндится на реальный фокусируемый элемент в слоте);\n * - панель — `role=\"dialog\"` по умолчанию и обязательное доступное имя\n * (`ariaLabel` либо `aria-labelledby` через `labelledBy`);\n * - Esc закрывает верхний слой стека, клик вне — закрывает по точке попадания;\n * - **по умолчанию фокус-ловушки нет намеренно**: Tab обязан уводить фокус\n * наружу, иначе немодальный слой запирает пользователя на странице, которая\n * не заблокирована. Проп `modal` включает второй режим — см. ниже.\n */\nimport { computed, nextTick, ref, useId, useSlots, watch } from 'vue'\n\nimport { useControlledOpen } from '../../composables/internal/useControlledOpen'\nimport { useModalOverlay } from '../../composables/internal/useModalOverlay'\nimport { createFloatingAnchor, type GrFloatingAnchorRect, useFloating, type UseFloatingPlacement } from '../../composables/useFloating'\nimport { vClickOutside } from '../../directives'\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport { panelPopTransition } from '../shared/overlayTransition'\nimport {\n type GrPopoverPadding,\n type GrPopoverRole,\n type GrPopoverSize,\n grPopoverPanelClass,\n} from './grPopoverStyles'\n\nexport interface GrPopoverProps {\n /**\n * Открыт ли поповер. Без этого пропа компонент ведёт состояние сам\n * (uncontrolled), с ним — слушайте `update:open`.\n */\n open?: boolean\n placement?: UseFloatingPlacement\n /** Зазор между триггером и панелью, px. */\n offsetPx?: number\n size?: GrPopoverSize\n /** Роль панели. Меняется теми, кто строит поверх примитива своё меню/список. */\n role?: GrPopoverRole\n /**\n * Модальный режим: фон уходит в `inert`, Tab ходит по кругу внутри панели,\n * скролл страницы блокируется, а слой встаёт в стек модальным — как окно.\n * Нужен поповеру с формой или подтверждением внутри: без изоляции фона\n * пользователь уводит фокус на страницу, к которой поповер и относится.\n *\n * В этом режиме фокус переносится в панель независимо от `autoFocus`: фон\n * недоступен, и слой без фокуса внутри стал бы клавиатурной ловушкой.\n */\n modal?: boolean\n /** Доступное имя панели. Обязательно для `role=\"dialog\"` без видимого заголовка. */\n ariaLabel?: string\n /** `id` видимого заголовка внутри панели — альтернатива `ariaLabel`. */\n labelledBy?: string\n /**\n * `manual` — открывать только программно (через `v-model:open`). Нужно для\n * контекстного меню и подтверждений, где момент открытия решает потребитель.\n */\n trigger?: 'click' | 'manual'\n closeOnEsc?: boolean\n closeOnClickOutside?: boolean\n /** Закрывать по клику внутри панели — удобно для меню, вредно для формы. */\n closeOnContentClick?: boolean\n /**\n * Переносить фокус на панель при открытии. Именно на панель, а не на первый\n * контрол внутри: сфокусировать поле ввода за пользователя — решение\n * содержимого, а не оболочки.\n */\n autoFocus?: boolean\n /**\n * Точечное переопределение точки монтирования. По умолчанию — общий портал\n * оверлеев (`#gr-portal` либо `portalTarget` из `GrConfigProvider`).\n */\n teleportTo?: string | HTMLElement\n contentClass?: string\n disabled?: boolean\n /**\n * Якорь-прямоугольник в координатах вьюпорта вместо обёртки слота `#trigger`:\n * панель встаёт у точки курсора или у строки списка. Нужен контекстному меню,\n * у которого триггера-элемента нет вовсе.\n *\n * `triggerProps` в этом режиме некому потребить, и своих ARIA-атрибутов\n * поповер никуда не вешает: `aria-haspopup` невалиден вне интерактивного\n * элемента, а `aria-expanded` без хозяина — шум. Связь с содержимым страницы\n * объявляет тот, кто открывает.\n */\n anchor?: GrFloatingAnchorRect | null\n /** Поле панели. `none` — содержимое рисует своё (меню, список опций). */\n padding?: GrPopoverPadding\n}\n\nexport interface GrPopoverEmits {\n (e: 'update:open', value: boolean): void\n}\n\nconst props = withDefaults(defineProps<GrPopoverProps>(), {\n open: undefined,\n placement: 'bottom-start',\n offsetPx: 8,\n size: undefined,\n role: 'dialog',\n modal: false,\n ariaLabel: undefined,\n labelledBy: undefined,\n trigger: 'click',\n closeOnEsc: true,\n closeOnClickOutside: true,\n closeOnContentClick: false,\n autoFocus: true,\n teleportTo: undefined,\n contentClass: undefined,\n disabled: false,\n anchor: undefined,\n padding: 'default',\n})\n\nconst emit = defineEmits<GrPopoverEmits>()\n\nconst slots = useSlots()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrPopover' })\n\nconst { open: isOpen, setOpen } = useControlledOpen(\n () => props.open,\n next => emit('update:open', next),\n)\n\nconst rootEl = ref<HTMLElement | null>(null)\nconst panelEl = ref<HTMLElement | null>(null)\nconst panelId = useId()\n\nconst clickOutsideExclude = [() => panelEl.value]\n\nfunction open(): void {\n if (props.disabled)\n return\n setOpen(true)\n}\n\nfunction close(): void {\n setOpen(false)\n}\n\nfunction toggle(): void {\n isOpen.value ? close() : open()\n}\n\n/**\n * Якорь создаётся один раз и читает координаты геттером: `useFloating`\n * пересоздаёт подписку при смене самой ссылки, а точка меняется на каждый\n * вызов — новый объект рвал бы `autoUpdate` без нужды.\n */\nconst anchorEl = createFloatingAnchor(() => props.anchor)\n\n/** Обёртка триггера существует только вместе со слотом — см. проп `anchor`. */\nconst hasTrigger = computed(() => Boolean(slots.trigger))\n\nconst { floatingStyle, resolvedPlacement, update: updateFloatingPosition } = useFloating(\n () => (props.anchor ? anchorEl : rootEl.value),\n panelEl,\n isOpen,\n {\n placement: () => props.placement,\n offsetPx: props.offsetPx,\n // Своего слоя у поповера нет намеренно: он и дропдаун — один класс якорных\n // немодальных оверлеев и обязаны лежать на одной высоте, иначе начнут\n // перекрывать друг друга в зависимости от порядка открытия.\n zIndexVar: '--gr-z-dropdown',\n },\n)\n\nwatch(() => props.placement, () => {\n if (isOpen.value)\n updateFloatingPosition()\n})\n\n// Объект якоря стабилен, поэтому смену координат `useFloating` сам не заметит:\n// повторный вызов у другой точки — это пересчёт позиции, а не переоткрытие.\nwatch(() => props.anchor, () => {\n if (isOpen.value)\n updateFloatingPosition()\n}, { deep: true })\n\n/**\n * Слой стека оверлеев: очередь Esc общая с модалками, поэтому поповер, открытый\n * внутри модалки, по Esc закрывает себя, а не её. Возврат фокуса на триггер\n * тоже отсюда — по правилу «только если на момент закрытия фокус ещё внутри».\n *\n * Модальность включается пропом и приходит той же сборкой, что у окна и drawer'а\n * (`useModalOverlay`): ловушка фокуса, `inert` фона, блокировка скролла. При\n * `modal: false` не включается ничего из этого — дефолтный поповер немодален.\n *\n * Корень слоя и панель здесь один элемент: якорный оверлей уезжает в портал сам,\n * без обёртки.\n */\nconst { inertAttr, portalTarget, teleportEnabled, themeAttrs } = useModalOverlay(isOpen, close, {\n modal: () => props.modal,\n closeOnEscape: () => props.closeOnEsc,\n panel: panelEl,\n root: panelEl,\n teleportTo: () => props.teleportTo,\n initialFocus: () => panelEl.value,\n})\n\n// Фокус переносим после отрисовки панели: до `nextTick` её ещё нет в DOM.\n// В модальном режиме перенос обязателен: фон в `inert`, и фокус, оставленный\n// снаружи, попал бы в недоступное поддерево.\nwatch(isOpen, async (next) => {\n if (!next || !(props.autoFocus || props.modal))\n return\n\n await nextTick()\n panelEl.value?.focus()\n})\n\n/** Роли, которые `aria-haspopup` действительно понимает. */\nconst HASPOPUP_ROLES = ['dialog', 'menu', 'listbox', 'grid'] as const\n\n/**\n * Пропсы для реального фокусируемого триггера: `<button v-bind=\"triggerProps\">`.\n * Клик вешается на обёртку, а ARIA — сюда, потому что `aria-expanded` обязан\n * жить на самом интерактивном элементе, а не на `div` вокруг него.\n */\nconst triggerProps = computed(() => ({\n // `aria-haspopup` принимает не любую роль: `group` в его списке нет, и\n // объявленный им триггер отдавал бы невалидный атрибут, который AT просто\n // отбрасывает. Роли вне списка сообщают о панели только через `aria-expanded`.\n 'aria-haspopup': HASPOPUP_ROLES.includes(props.role as (typeof HASPOPUP_ROLES)[number])\n ? (props.role as (typeof HASPOPUP_ROLES)[number])\n : undefined,\n 'aria-expanded': isOpen.value,\n 'aria-controls': isOpen.value ? panelId : undefined,\n 'disabled': props.disabled || undefined,\n}))\n\nfunction onTriggerClick(): void {\n if (props.trigger === 'click')\n toggle()\n}\n\nfunction onContentClick(): void {\n if (props.closeOnContentClick)\n close()\n}\n\nconst panelClasses = computed(() =>\n grPopoverPanelClass(resolvedSize.value, resolvedPlacement.value, props.contentClass, props.padding),\n)\n\ndefineExpose({ open, close, toggle })\n\ndefineSlots<{\n /**\n * Триггер панели. `triggerProps` обязаны попасть на сам интерактивный\n * элемент, а не на обёртку вокруг него: `aria-expanded` и `aria-controls`\n * читаются с того узла, который получает фокус.\n */\n trigger?: (props: {\n open: boolean\n toggle: () => void\n close: () => void\n triggerProps: Record<string, unknown>\n }) => any\n /** Содержимое панели. */\n content?: (props: { close: () => void }) => any\n}>()\n</script>\n\n<template>\n <div data-gr-popover>\n <div\n v-if=\"hasTrigger\"\n ref=\"rootEl\"\n v-click-outside=\"{ handler: close, enabled: isOpen && closeOnClickOutside, exclude: clickOutsideExclude }\"\n data-gr-popover-trigger\n class=\"inline-block max-w-full\"\n @click=\"onTriggerClick\"\n >\n <slot\n name=\"trigger\"\n :open=\"isOpen\"\n :toggle=\"toggle\"\n :close=\"close\"\n :trigger-props=\"triggerProps\"\n />\n </div>\n\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <transition\n :enter-active-class=\"panelPopTransition.enter\"\n :enter-from-class=\"panelPopTransition.enterFrom\"\n :enter-to-class=\"panelPopTransition.enterTo\"\n :leave-active-class=\"panelPopTransition.leave\"\n :leave-from-class=\"panelPopTransition.leaveFrom\"\n :leave-to-class=\"panelPopTransition.leaveTo\"\n >\n <div\n v-show=\"isOpen\"\n :id=\"panelId\"\n ref=\"panelEl\"\n v-click-outside=\"{ handler: close, enabled: isOpen && closeOnClickOutside && !hasTrigger }\"\n v-bind=\"themeAttrs\"\n data-gr-popover-panel\n data-gr-overlay-root\n :role=\"role === 'none' ? undefined : role\"\n :aria-modal=\"modal ? 'true' : undefined\"\n :inert=\"inertAttr\"\n :aria-label=\"labelledBy ? undefined : ariaLabel\"\n :aria-labelledby=\"labelledBy\"\n tabindex=\"-1\"\n :class=\"panelClasses\"\n :style=\"floatingStyle\"\n @click=\"onContentClick\"\n >\n <slot name=\"content\" :close=\"close\" />\n </div>\n </transition>\n </teleport>\n </div>\n</template>\n","<script setup lang=\"ts\">\n/**\n * GrPopover — якорный немодальный оверлей с произвольным содержимым.\n *\n * Роль «универсального поповера» до сих пор насильно исполнял `GrDropdown`, но\n * он про меню: жёстко объявляет `role=\"menu\"`, `aria-haspopup=\"menu\"` и водит\n * фокус по пунктам. Для формы, подтверждения или палитры это неверная семантика.\n *\n * Здесь наоборот: позиционирование, слой и закрытие берёт на себя примитив, а\n * клавиатурный паттерн внутри панели остаётся за потребителем — поэтому `role`\n * задаётся пропом. На этом собираются `GrMenu` / `GrContextMenu` /\n * `GrColorPicker` / Popconfirm.\n *\n * A11y:\n * - триггер получает `aria-haspopup`/`aria-expanded`/`aria-controls` через\n * `triggerProps` (биндится на реальный фокусируемый элемент в слоте);\n * - панель — `role=\"dialog\"` по умолчанию и обязательное доступное имя\n * (`ariaLabel` либо `aria-labelledby` через `labelledBy`);\n * - Esc закрывает верхний слой стека, клик вне — закрывает по точке попадания;\n * - **по умолчанию фокус-ловушки нет намеренно**: Tab обязан уводить фокус\n * наружу, иначе немодальный слой запирает пользователя на странице, которая\n * не заблокирована. Проп `modal` включает второй режим — см. ниже.\n */\nimport { computed, nextTick, ref, useId, useSlots, watch } from 'vue'\n\nimport { useControlledOpen } from '../../composables/internal/useControlledOpen'\nimport { useModalOverlay } from '../../composables/internal/useModalOverlay'\nimport { createFloatingAnchor, type GrFloatingAnchorRect, useFloating, type UseFloatingPlacement } from '../../composables/useFloating'\nimport { vClickOutside } from '../../directives'\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport { panelPopTransition } from '../shared/overlayTransition'\nimport {\n type GrPopoverPadding,\n type GrPopoverRole,\n type GrPopoverSize,\n grPopoverPanelClass,\n} from './grPopoverStyles'\n\nexport interface GrPopoverProps {\n /**\n * Открыт ли поповер. Без этого пропа компонент ведёт состояние сам\n * (uncontrolled), с ним — слушайте `update:open`.\n */\n open?: boolean\n placement?: UseFloatingPlacement\n /** Зазор между триггером и панелью, px. */\n offsetPx?: number\n size?: GrPopoverSize\n /** Роль панели. Меняется теми, кто строит поверх примитива своё меню/список. */\n role?: GrPopoverRole\n /**\n * Модальный режим: фон уходит в `inert`, Tab ходит по кругу внутри панели,\n * скролл страницы блокируется, а слой встаёт в стек модальным — как окно.\n * Нужен поповеру с формой или подтверждением внутри: без изоляции фона\n * пользователь уводит фокус на страницу, к которой поповер и относится.\n *\n * В этом режиме фокус переносится в панель независимо от `autoFocus`: фон\n * недоступен, и слой без фокуса внутри стал бы клавиатурной ловушкой.\n */\n modal?: boolean\n /** Доступное имя панели. Обязательно для `role=\"dialog\"` без видимого заголовка. */\n ariaLabel?: string\n /** `id` видимого заголовка внутри панели — альтернатива `ariaLabel`. */\n labelledBy?: string\n /**\n * `manual` — открывать только программно (через `v-model:open`). Нужно для\n * контекстного меню и подтверждений, где момент открытия решает потребитель.\n */\n trigger?: 'click' | 'manual'\n closeOnEsc?: boolean\n closeOnClickOutside?: boolean\n /** Закрывать по клику внутри панели — удобно для меню, вредно для формы. */\n closeOnContentClick?: boolean\n /**\n * Переносить фокус на панель при открытии. Именно на панель, а не на первый\n * контрол внутри: сфокусировать поле ввода за пользователя — решение\n * содержимого, а не оболочки.\n */\n autoFocus?: boolean\n /**\n * Точечное переопределение точки монтирования. По умолчанию — общий портал\n * оверлеев (`#gr-portal` либо `portalTarget` из `GrConfigProvider`).\n */\n teleportTo?: string | HTMLElement\n contentClass?: string\n disabled?: boolean\n /**\n * Якорь-прямоугольник в координатах вьюпорта вместо обёртки слота `#trigger`:\n * панель встаёт у точки курсора или у строки списка. Нужен контекстному меню,\n * у которого триггера-элемента нет вовсе.\n *\n * `triggerProps` в этом режиме некому потребить, и своих ARIA-атрибутов\n * поповер никуда не вешает: `aria-haspopup` невалиден вне интерактивного\n * элемента, а `aria-expanded` без хозяина — шум. Связь с содержимым страницы\n * объявляет тот, кто открывает.\n */\n anchor?: GrFloatingAnchorRect | null\n /** Поле панели. `none` — содержимое рисует своё (меню, список опций). */\n padding?: GrPopoverPadding\n}\n\nexport interface GrPopoverEmits {\n (e: 'update:open', value: boolean): void\n}\n\nconst props = withDefaults(defineProps<GrPopoverProps>(), {\n open: undefined,\n placement: 'bottom-start',\n offsetPx: 8,\n size: undefined,\n role: 'dialog',\n modal: false,\n ariaLabel: undefined,\n labelledBy: undefined,\n trigger: 'click',\n closeOnEsc: true,\n closeOnClickOutside: true,\n closeOnContentClick: false,\n autoFocus: true,\n teleportTo: undefined,\n contentClass: undefined,\n disabled: false,\n anchor: undefined,\n padding: 'default',\n})\n\nconst emit = defineEmits<GrPopoverEmits>()\n\nconst slots = useSlots()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrPopover' })\n\nconst { open: isOpen, setOpen } = useControlledOpen(\n () => props.open,\n next => emit('update:open', next),\n)\n\nconst rootEl = ref<HTMLElement | null>(null)\nconst panelEl = ref<HTMLElement | null>(null)\nconst panelId = useId()\n\nconst clickOutsideExclude = [() => panelEl.value]\n\nfunction open(): void {\n if (props.disabled)\n return\n setOpen(true)\n}\n\nfunction close(): void {\n setOpen(false)\n}\n\nfunction toggle(): void {\n isOpen.value ? close() : open()\n}\n\n/**\n * Якорь создаётся один раз и читает координаты геттером: `useFloating`\n * пересоздаёт подписку при смене самой ссылки, а точка меняется на каждый\n * вызов — новый объект рвал бы `autoUpdate` без нужды.\n */\nconst anchorEl = createFloatingAnchor(() => props.anchor)\n\n/** Обёртка триггера существует только вместе со слотом — см. проп `anchor`. */\nconst hasTrigger = computed(() => Boolean(slots.trigger))\n\nconst { floatingStyle, resolvedPlacement, update: updateFloatingPosition } = useFloating(\n () => (props.anchor ? anchorEl : rootEl.value),\n panelEl,\n isOpen,\n {\n placement: () => props.placement,\n offsetPx: props.offsetPx,\n // Своего слоя у поповера нет намеренно: он и дропдаун — один класс якорных\n // немодальных оверлеев и обязаны лежать на одной высоте, иначе начнут\n // перекрывать друг друга в зависимости от порядка открытия.\n zIndexVar: '--gr-z-dropdown',\n },\n)\n\nwatch(() => props.placement, () => {\n if (isOpen.value)\n updateFloatingPosition()\n})\n\n// Объект якоря стабилен, поэтому смену координат `useFloating` сам не заметит:\n// повторный вызов у другой точки — это пересчёт позиции, а не переоткрытие.\nwatch(() => props.anchor, () => {\n if (isOpen.value)\n updateFloatingPosition()\n}, { deep: true })\n\n/**\n * Слой стека оверлеев: очередь Esc общая с модалками, поэтому поповер, открытый\n * внутри модалки, по Esc закрывает себя, а не её. Возврат фокуса на триггер\n * тоже отсюда — по правилу «только если на момент закрытия фокус ещё внутри».\n *\n * Модальность включается пропом и приходит той же сборкой, что у окна и drawer'а\n * (`useModalOverlay`): ловушка фокуса, `inert` фона, блокировка скролла. При\n * `modal: false` не включается ничего из этого — дефолтный поповер немодален.\n *\n * Корень слоя и панель здесь один элемент: якорный оверлей уезжает в портал сам,\n * без обёртки.\n */\nconst { inertAttr, portalTarget, teleportEnabled, themeAttrs } = useModalOverlay(isOpen, close, {\n modal: () => props.modal,\n closeOnEscape: () => props.closeOnEsc,\n panel: panelEl,\n root: panelEl,\n teleportTo: () => props.teleportTo,\n initialFocus: () => panelEl.value,\n})\n\n// Фокус переносим после отрисовки панели: до `nextTick` её ещё нет в DOM.\n// В модальном режиме перенос обязателен: фон в `inert`, и фокус, оставленный\n// снаружи, попал бы в недоступное поддерево.\nwatch(isOpen, async (next) => {\n if (!next || !(props.autoFocus || props.modal))\n return\n\n await nextTick()\n panelEl.value?.focus()\n})\n\n/** Роли, которые `aria-haspopup` действительно понимает. */\nconst HASPOPUP_ROLES = ['dialog', 'menu', 'listbox', 'grid'] as const\n\n/**\n * Пропсы для реального фокусируемого триггера: `<button v-bind=\"triggerProps\">`.\n * Клик вешается на обёртку, а ARIA — сюда, потому что `aria-expanded` обязан\n * жить на самом интерактивном элементе, а не на `div` вокруг него.\n */\nconst triggerProps = computed(() => ({\n // `aria-haspopup` принимает не любую роль: `group` в его списке нет, и\n // объявленный им триггер отдавал бы невалидный атрибут, который AT просто\n // отбрасывает. Роли вне списка сообщают о панели только через `aria-expanded`.\n 'aria-haspopup': HASPOPUP_ROLES.includes(props.role as (typeof HASPOPUP_ROLES)[number])\n ? (props.role as (typeof HASPOPUP_ROLES)[number])\n : undefined,\n 'aria-expanded': isOpen.value,\n 'aria-controls': isOpen.value ? panelId : undefined,\n 'disabled': props.disabled || undefined,\n}))\n\nfunction onTriggerClick(): void {\n if (props.trigger === 'click')\n toggle()\n}\n\nfunction onContentClick(): void {\n if (props.closeOnContentClick)\n close()\n}\n\nconst panelClasses = computed(() =>\n grPopoverPanelClass(resolvedSize.value, resolvedPlacement.value, props.contentClass, props.padding),\n)\n\ndefineExpose({ open, close, toggle })\n\ndefineSlots<{\n /**\n * Триггер панели. `triggerProps` обязаны попасть на сам интерактивный\n * элемент, а не на обёртку вокруг него: `aria-expanded` и `aria-controls`\n * читаются с того узла, который получает фокус.\n */\n trigger?: (props: {\n open: boolean\n toggle: () => void\n close: () => void\n triggerProps: Record<string, unknown>\n }) => any\n /** Содержимое панели. */\n content?: (props: { close: () => void }) => any\n}>()\n</script>\n\n<template>\n <div data-gr-popover>\n <div\n v-if=\"hasTrigger\"\n ref=\"rootEl\"\n v-click-outside=\"{ handler: close, enabled: isOpen && closeOnClickOutside, exclude: clickOutsideExclude }\"\n data-gr-popover-trigger\n class=\"inline-block max-w-full\"\n @click=\"onTriggerClick\"\n >\n <slot\n name=\"trigger\"\n :open=\"isOpen\"\n :toggle=\"toggle\"\n :close=\"close\"\n :trigger-props=\"triggerProps\"\n />\n </div>\n\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <transition\n :enter-active-class=\"panelPopTransition.enter\"\n :enter-from-class=\"panelPopTransition.enterFrom\"\n :enter-to-class=\"panelPopTransition.enterTo\"\n :leave-active-class=\"panelPopTransition.leave\"\n :leave-from-class=\"panelPopTransition.leaveFrom\"\n :leave-to-class=\"panelPopTransition.leaveTo\"\n >\n <div\n v-show=\"isOpen\"\n :id=\"panelId\"\n ref=\"panelEl\"\n v-click-outside=\"{ handler: close, enabled: isOpen && closeOnClickOutside && !hasTrigger }\"\n v-bind=\"themeAttrs\"\n data-gr-popover-panel\n data-gr-overlay-root\n :role=\"role === 'none' ? undefined : role\"\n :aria-modal=\"modal ? 'true' : undefined\"\n :inert=\"inertAttr\"\n :aria-label=\"labelledBy ? undefined : ariaLabel\"\n :aria-labelledby=\"labelledBy\"\n tabindex=\"-1\"\n :class=\"panelClasses\"\n :style=\"floatingStyle\"\n @click=\"onContentClick\"\n >\n <slot name=\"content\" :close=\"close\" />\n </div>\n </transition>\n </teleport>\n </div>\n</template>\n"],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { t as GrPopover_default } from "./chunks/GrPopover-
|
|
1
|
+
import { t as GrPopover_default } from "./chunks/GrPopover-faJanmGs.js";
|
|
2
2
|
import { n as grPopoverSafelist, t as grPopoverConfig } from "../../chunks/config-BTwYMQaz2.js";
|
|
3
3
|
export { GrPopover_default as GrPopover, GrPopover_default as default, grPopoverConfig, grPopoverSafelist };
|
|
@@ -2,7 +2,7 @@ import { i as useGrComponentProp } from "../../../chunks/context-CTbuwcYs.js";
|
|
|
2
2
|
import { n as useGranularityTranslations } from "../../../chunks/granularityI18n-BC5zsj_d.js";
|
|
3
3
|
import { t as GrButton_default } from "../../GrButton/chunks/GrButton-7GP0zObj.js";
|
|
4
4
|
import { t as GrInput_default } from "../../GrInput/chunks/GrInput-BLywRZUN.js";
|
|
5
|
-
import { t as GrDialog_default } from "../../GrDialog/chunks/GrDialog-
|
|
5
|
+
import { t as GrDialog_default } from "../../GrDialog/chunks/GrDialog-BiMJ9_T4.js";
|
|
6
6
|
import { t as GrResponseErrorBanner_default } from "../../GrResponseErrorBanner/chunks/GrResponseErrorBanner-DMeOWlzj.js";
|
|
7
7
|
import { t as GrFormField_default } from "../../GrFormField/chunks/GrFormField-DSylU7IU.js";
|
|
8
8
|
import { t as GrTextarea_default } from "../../GrTextarea/chunks/GrTextarea-DinXG12-.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GrPromptDialog-BuJQMZLM.js","names":[],"sources":["../../../../src/components/GrPromptDialog/GrPromptDialog.vue","../../../../src/components/GrPromptDialog/GrPromptDialog.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * GrPromptDialog — GR-примитив диалога ввода значения поверх `GrDialog`.\n *\n * Синхронизирован с `GrDialog` по набору проп-проксирования:\n * `size`, `closeOnBackdrop`, `closeOnEsc`, `showHeader`, `showCloseButton`,\n * `headerConfig`, `footerConfig`, `bodyConfig`, `closeLabel`.\n *\n * Поведение `required=true` (по умолчанию): пустое значение подсвечивается\n * ошибкой только после первого «касания» (blur или попытка confirm),\n * чтобы не шуметь при открытии.\n *\n * Проверки сверх `required` описываются пропом `rules` — тем же движком, что и\n * у `GrForm`: третий частный случай валидации в пакете заводить незачем.\n */\nimport { useGrComponentProp } from '../GrConfigProvider/context'\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 GrTextarea from '../GrTextarea/GrTextarea.vue'\nimport GrResponseErrorBanner from '../GrResponseErrorBanner/GrResponseErrorBanner.vue'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n// Именованные импорты чистых функций: разметки не тянут, поэтому в\n// `config.dependencies` не объявляются (см. правило про зависимости).\nimport { createGrFormMessageResolver, rulesForTrigger, runFieldRules } from '../GrForm/validation'\nimport type { GrFormRule, GrFormTrigger } from '../GrForm/validation'\nimport type { GrButtonSize, GrButtonTone, GrButtonVariant } from '../GrButton'\nimport type { GrDialogSectionConfig, GrDialogSize } from '../GrDialog'\n\n/**\n * Тип поля выводим из самого `GrInput`, а не заводим ему именованный алиас:\n * алиас в пропе `GrInput` заменил бы в таблице API витрины список допустимых\n * значений на своё имя — документация стала бы беднее ради удобства обёртки.\n */\ntype GrInputType = NonNullable<InstanceType<typeof GrInput>['$props']['type']>\nimport type { ResponseErrorInfo } from '../GrResponseErrorBanner'\nimport type { InputHTMLAttributes } from 'vue'\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 inputType?: GrInputType\n /** Программная клавиатура на мобильных. */\n inputmode?: InputHTMLAttributes['inputmode']\n /** Ограничение длины; со `showCount` рисуется счётчик. */\n maxlength?: number\n showCount?: boolean\n /** Многострочный ввод: вместо `GrInput` рисуется `GrTextarea`. */\n multiline?: boolean\n /** Высота многострочного поля в строках. */\n rows?: number\n /** Автоподбор высоты многострочного поля под содержимое. */\n autosize?: boolean\n /**\n * Правила проверки значения — те же, что у `GrForm` (`required`, `type`,\n * `min`/`max`/`len`, `pattern`, свой в т.ч. асинхронный `validator`).\n * Прогоняются на blur (после первого касания) и на подтверждении.\n */\n rules?: GrFormRule | GrFormRule[]\n /**\n * Структура ошибки ответа сервера для показа общим блоком в теле диалога\n * (через `GrResponseErrorBanner`). `null` — блок скрыт.\n */\n error?: ResponseErrorInfo | null\n /**\n * Внешняя ошибка поля ввода (например, серверная валидация). Имеет приоритет\n * над встроенной проверкой. `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 * Запрет закрытия «мягкими» способами (Esc, клик по бэкдропу), пока идёт\n * подтверждение или проверка `rules`. Кнопка закрытия и «Отмена» остаются:\n * окно без единого выхода — ловушка.\n */\n persistent?: boolean\n}\n\nexport interface GrPromptDialogEmits {\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\nimport './defaults'\n\nconst props = withDefaults(defineProps<GrPromptDialogProps>(), {\n title: undefined,\n description: undefined,\n label: undefined,\n placeholder: undefined,\n closeOnBackdrop: true,\n closeOnEsc: true,\n showHeader: true,\n showCloseButton: true,\n // Дефолт `size` живёт в резолвере ниже, а не здесь: Vue подставил бы его\n // до того, как компонент заглянет в `GrConfigProvider`.\n size: undefined,\n headerConfig: undefined,\n footerConfig: undefined,\n bodyConfig: undefined,\n closeLabel: undefined,\n buttonSize: undefined,\n confirmText: undefined,\n cancelText: undefined,\n requiredErrorText: undefined,\n confirmVariant: 'primary',\n confirmTone: 'primary',\n required: true,\n inputType: 'text',\n inputmode: undefined,\n maxlength: undefined,\n showCount: false,\n multiline: false,\n rows: undefined,\n autosize: false,\n rules: undefined,\n error: null,\n fieldError: null,\n confirmLoading: false,\n confirmDisabled: false,\n closeOnConfirm: true,\n persistent: false,\n})\n\n// Эффективный размер: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentProp('GrPromptDialog', 'size', () => props.size, 'md')\n\nconst emit = defineEmits<GrPromptDialogEmits>()\n\n// Дефолты берём из общего i18n-блока пакета (fallback — англ.), а не хардкодим.\nconst { t } = useGranularityTranslations()\nconst resolvedTitle = computed(() => props.title ?? t('gr.dialog.prompt.title', 'Prompt'))\nconst resolvedLabel = computed(() => props.label ?? t('gr.dialog.prompt.label', 'Value'))\nconst resolvedRequiredErrorText = computed(() => props.requiredErrorText ?? t('gr.dialog.prompt.required', 'Enter a value.'))\nconst resolvedCloseLabel = computed(() => props.closeLabel ?? t('gr.common.close', 'Close'))\nconst resolvedConfirmText = computed(() => props.confirmText ?? t('gr.common.confirm', 'Confirm'))\nconst resolvedCancelText = computed(() => props.cancelText ?? t('gr.common.cancel', 'Cancel'))\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)\nconst textareaRef = ref<InstanceType<typeof GrTextarea> | null>(null)\n\n/** Ошибка последнего прогона `rules`; `required` считается отдельно и мгновенно. */\nconst ruleError = ref<string | undefined>(undefined)\nconst validating = ref(false)\n\n// Сторож устаревших прогонов: асинхронное правило может ответить после того,\n// как значение уже сменилось, и дописать чужую ошибку.\nlet validationRun = 0\n\nwatch(\n () => props.modelValue,\n (isOpen) => {\n if (!isOpen)\n return\n\n touched.value = false\n ruleError.value = undefined\n validationRun += 1\n },\n)\n\nconst resolveMessage = createGrFormMessageResolver(t)\n\nconst normalizedRules = computed<GrFormRule[]>(() => {\n if (!props.rules)\n return []\n return Array.isArray(props.rules) ? props.rules : [props.rules]\n})\n\nconst requiredError = computed(() => {\n if (!props.required)\n return undefined\n return valueModel.value.trim().length > 0 ? undefined : resolvedRequiredErrorText.value\n})\n\nconst canConfirm = computed(() => !requiredError.value)\n\nconst validationError = computed(() => {\n if (!touched.value)\n return undefined\n return requiredError.value ?? ruleError.value\n})\n\n// Внешняя ошибка поля (серверная валидация) имеет приоритет над встроенной.\nconst fieldErrorMessage = computed(() => props.fieldError ?? validationError.value ?? undefined)\n\n/**\n * Прогоняет `rules` для триггера. Возвращает сообщение или `undefined`.\n * `required` здесь не участвует: он синхронный и считается отдельно, чтобы\n * пустое поле блокировало кнопку без похода в асинхронный движок.\n */\nasync function runRules(trigger: GrFormTrigger): Promise<string | undefined> {\n const rules = rulesForTrigger(normalizedRules.value, trigger)\n if (!rules.length)\n return undefined\n\n const run = ++validationRun\n validating.value = true\n try {\n const message = await runFieldRules(valueModel.value, rules, { value: valueModel.value }, resolveMessage)\n // Ответ устарел — значение или сам диалог успели смениться.\n if (run !== validationRun)\n return undefined\n\n ruleError.value = message\n return message\n }\n finally {\n if (run === validationRun)\n validating.value = false\n }\n}\n\nfunction onBlur(): void {\n touched.value = true\n void runRules('blur')\n}\n\n// Пока подтверждение или проверка в полёте, случайное движение не должно\n// оборвать операцию.\nconst softCloseBlocked = computed(() => props.persistent && (props.confirmLoading || validating.value))\nconst resolvedCloseOnBackdrop = computed(() => (softCloseBlocked.value ? false : props.closeOnBackdrop))\nconst resolvedCloseOnEsc = computed(() => (softCloseBlocked.value ? false : props.closeOnEsc))\n\nfunction onCancel(): void {\n emit('cancel')\n emit('update:modelValue', false)\n}\n\nfunction focusField(): void {\n if (props.multiline)\n textareaRef.value?.focus()\n else inputRef.value?.focus()\n}\n\nasync function onConfirm(): Promise<void> {\n touched.value = true\n\n if (!canConfirm.value) {\n focusField()\n return\n }\n\n if (await runRules('submit')) {\n focusField()\n return\n }\n\n emit('confirm', valueModel.value)\n if (props.closeOnConfirm)\n emit('update:modelValue', false)\n}\n\n/**\n * `Enter` подтверждает — базовое ожидание от окна, единственный смысл которого\n * ввести одно значение. В многострочном режиме `Enter` остаётся переводом\n * строки, поэтому обработчик висит только на однострочном поле.\n */\nfunction onEnter(): void {\n void onConfirm()\n}\n\n// Фокус в поле, а не на панель: диалог существует ровно ради ввода.\n//\n// Через `initialFocus` у `GrModal` это не решается: элемент рождается внутри\n// поддерева диалога, и возврат его же пропом наверх замыкает рендер в цикл\n// («Maximum recursive updates» — проверено). Поэтому фокус ставит содержимое,\n// после отрисовки (`flush: 'post'` + кадр), то есть заведомо позже, чем\n// ловушка фокуса `GrModal` переводит фокус на панель.\n// Источник — не только `modelValue`, но и само поле: содержимое панели\n// появляется на такт позже открытия, и фокус по фиксированному `nextTick`\n// приходился на момент, когда фокусировать ещё нечего.\nwatch(\n [() => props.modelValue, inputRef, textareaRef],\n () => {\n if (!props.modelValue)\n return\n focusField()\n },\n // `immediate`: окно могут смонтировать уже открытым — тогда смены пропа не\n // будет вовсе, и без этого фокус остался бы на панели.\n { immediate: true, flush: 'post' },\n)\n\ndefineSlots<{\n /** Содержимое диалога вместо пропа `message`. */\n default?: () => any\n /** Разбор ошибки вместо встроенного баннера. */\n error?: (props: { error: ResponseErrorInfo | null }) => any\n /** Кнопки диалога вместо пары «отмена и подтверждение». */\n footer?: () => any\n}>()\n</script>\n\n<template>\n <GrDialog\n v-model=\"open\"\n :title=\"resolvedTitle\"\n :size=\"resolvedSize\"\n :close-on-backdrop=\"resolvedCloseOnBackdrop\"\n :close-on-esc=\"resolvedCloseOnEsc\"\n :show-header=\"showHeader\"\n :show-close-button=\"showCloseButton\"\n :header-config=\"headerConfig\"\n :footer-config=\"footerConfig\"\n :body-config=\"bodyConfig\"\n :close-label=\"resolvedCloseLabel\"\n >\n <div class=\"grid gap-4\">\n <slot>\n <div v-if=\"description\" class=\"text-[length:var(--gr-control-text-md)] leading-[var(--gr-control-leading-md)] text-[var(--gr-muted-fg)]\">\n {{ description }}\n </div>\n </slot>\n\n <!-- `id` полю не задаём: `GrFormField` генерирует его сам, а поле читает\n из контекста. Литеральный id ломал два открытых диалога сразу. -->\n <GrFormField :label=\"resolvedLabel\" :error=\"fieldErrorMessage\">\n <GrTextarea\n v-if=\"multiline\"\n ref=\"textareaRef\"\n v-model=\"valueModel\"\n data-testid=\"gr-prompt-input\"\n :placeholder=\"placeholder\"\n :rows=\"rows\"\n :autosize=\"autosize\"\n :maxlength=\"maxlength\"\n :show-count=\"showCount\"\n :invalid=\"!!fieldErrorMessage\"\n @blur=\"onBlur\"\n />\n <GrInput\n v-else\n ref=\"inputRef\"\n v-model=\"valueModel\"\n data-testid=\"gr-prompt-input\"\n :type=\"inputType\"\n :inputmode=\"inputmode\"\n :placeholder=\"placeholder\"\n :maxlength=\"maxlength\"\n :show-count=\"showCount\"\n :invalid=\"!!fieldErrorMessage\"\n @blur=\"onBlur\"\n @keydown.enter=\"onEnter\"\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=\"gr-prompt-cancel\" variant=\"outline\" :size=\"buttonSize\" @click=\"onCancel\">\n {{ resolvedCancelText }}\n </GrButton>\n <GrButton\n data-testid=\"gr-prompt-confirm\"\n :variant=\"confirmVariant\"\n :tone=\"confirmTone\"\n :size=\"buttonSize\"\n :loading=\"confirmLoading || validating\"\n :disabled=\"confirmDisabled || (touched && !canConfirm)\"\n @click=\"onConfirm\"\n >\n {{ resolvedConfirmText }}\n </GrButton>\n </div>\n </slot>\n </template>\n </GrDialog>\n</template>\n","<script setup lang=\"ts\">\n/**\n * GrPromptDialog — GR-примитив диалога ввода значения поверх `GrDialog`.\n *\n * Синхронизирован с `GrDialog` по набору проп-проксирования:\n * `size`, `closeOnBackdrop`, `closeOnEsc`, `showHeader`, `showCloseButton`,\n * `headerConfig`, `footerConfig`, `bodyConfig`, `closeLabel`.\n *\n * Поведение `required=true` (по умолчанию): пустое значение подсвечивается\n * ошибкой только после первого «касания» (blur или попытка confirm),\n * чтобы не шуметь при открытии.\n *\n * Проверки сверх `required` описываются пропом `rules` — тем же движком, что и\n * у `GrForm`: третий частный случай валидации в пакете заводить незачем.\n */\nimport { useGrComponentProp } from '../GrConfigProvider/context'\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 GrTextarea from '../GrTextarea/GrTextarea.vue'\nimport GrResponseErrorBanner from '../GrResponseErrorBanner/GrResponseErrorBanner.vue'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n// Именованные импорты чистых функций: разметки не тянут, поэтому в\n// `config.dependencies` не объявляются (см. правило про зависимости).\nimport { createGrFormMessageResolver, rulesForTrigger, runFieldRules } from '../GrForm/validation'\nimport type { GrFormRule, GrFormTrigger } from '../GrForm/validation'\nimport type { GrButtonSize, GrButtonTone, GrButtonVariant } from '../GrButton'\nimport type { GrDialogSectionConfig, GrDialogSize } from '../GrDialog'\n\n/**\n * Тип поля выводим из самого `GrInput`, а не заводим ему именованный алиас:\n * алиас в пропе `GrInput` заменил бы в таблице API витрины список допустимых\n * значений на своё имя — документация стала бы беднее ради удобства обёртки.\n */\ntype GrInputType = NonNullable<InstanceType<typeof GrInput>['$props']['type']>\nimport type { ResponseErrorInfo } from '../GrResponseErrorBanner'\nimport type { InputHTMLAttributes } from 'vue'\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 inputType?: GrInputType\n /** Программная клавиатура на мобильных. */\n inputmode?: InputHTMLAttributes['inputmode']\n /** Ограничение длины; со `showCount` рисуется счётчик. */\n maxlength?: number\n showCount?: boolean\n /** Многострочный ввод: вместо `GrInput` рисуется `GrTextarea`. */\n multiline?: boolean\n /** Высота многострочного поля в строках. */\n rows?: number\n /** Автоподбор высоты многострочного поля под содержимое. */\n autosize?: boolean\n /**\n * Правила проверки значения — те же, что у `GrForm` (`required`, `type`,\n * `min`/`max`/`len`, `pattern`, свой в т.ч. асинхронный `validator`).\n * Прогоняются на blur (после первого касания) и на подтверждении.\n */\n rules?: GrFormRule | GrFormRule[]\n /**\n * Структура ошибки ответа сервера для показа общим блоком в теле диалога\n * (через `GrResponseErrorBanner`). `null` — блок скрыт.\n */\n error?: ResponseErrorInfo | null\n /**\n * Внешняя ошибка поля ввода (например, серверная валидация). Имеет приоритет\n * над встроенной проверкой. `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 * Запрет закрытия «мягкими» способами (Esc, клик по бэкдропу), пока идёт\n * подтверждение или проверка `rules`. Кнопка закрытия и «Отмена» остаются:\n * окно без единого выхода — ловушка.\n */\n persistent?: boolean\n}\n\nexport interface GrPromptDialogEmits {\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\nimport './defaults'\n\nconst props = withDefaults(defineProps<GrPromptDialogProps>(), {\n title: undefined,\n description: undefined,\n label: undefined,\n placeholder: undefined,\n closeOnBackdrop: true,\n closeOnEsc: true,\n showHeader: true,\n showCloseButton: true,\n // Дефолт `size` живёт в резолвере ниже, а не здесь: Vue подставил бы его\n // до того, как компонент заглянет в `GrConfigProvider`.\n size: undefined,\n headerConfig: undefined,\n footerConfig: undefined,\n bodyConfig: undefined,\n closeLabel: undefined,\n buttonSize: undefined,\n confirmText: undefined,\n cancelText: undefined,\n requiredErrorText: undefined,\n confirmVariant: 'primary',\n confirmTone: 'primary',\n required: true,\n inputType: 'text',\n inputmode: undefined,\n maxlength: undefined,\n showCount: false,\n multiline: false,\n rows: undefined,\n autosize: false,\n rules: undefined,\n error: null,\n fieldError: null,\n confirmLoading: false,\n confirmDisabled: false,\n closeOnConfirm: true,\n persistent: false,\n})\n\n// Эффективный размер: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentProp('GrPromptDialog', 'size', () => props.size, 'md')\n\nconst emit = defineEmits<GrPromptDialogEmits>()\n\n// Дефолты берём из общего i18n-блока пакета (fallback — англ.), а не хардкодим.\nconst { t } = useGranularityTranslations()\nconst resolvedTitle = computed(() => props.title ?? t('gr.dialog.prompt.title', 'Prompt'))\nconst resolvedLabel = computed(() => props.label ?? t('gr.dialog.prompt.label', 'Value'))\nconst resolvedRequiredErrorText = computed(() => props.requiredErrorText ?? t('gr.dialog.prompt.required', 'Enter a value.'))\nconst resolvedCloseLabel = computed(() => props.closeLabel ?? t('gr.common.close', 'Close'))\nconst resolvedConfirmText = computed(() => props.confirmText ?? t('gr.common.confirm', 'Confirm'))\nconst resolvedCancelText = computed(() => props.cancelText ?? t('gr.common.cancel', 'Cancel'))\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)\nconst textareaRef = ref<InstanceType<typeof GrTextarea> | null>(null)\n\n/** Ошибка последнего прогона `rules`; `required` считается отдельно и мгновенно. */\nconst ruleError = ref<string | undefined>(undefined)\nconst validating = ref(false)\n\n// Сторож устаревших прогонов: асинхронное правило может ответить после того,\n// как значение уже сменилось, и дописать чужую ошибку.\nlet validationRun = 0\n\nwatch(\n () => props.modelValue,\n (isOpen) => {\n if (!isOpen)\n return\n\n touched.value = false\n ruleError.value = undefined\n validationRun += 1\n },\n)\n\nconst resolveMessage = createGrFormMessageResolver(t)\n\nconst normalizedRules = computed<GrFormRule[]>(() => {\n if (!props.rules)\n return []\n return Array.isArray(props.rules) ? props.rules : [props.rules]\n})\n\nconst requiredError = computed(() => {\n if (!props.required)\n return undefined\n return valueModel.value.trim().length > 0 ? undefined : resolvedRequiredErrorText.value\n})\n\nconst canConfirm = computed(() => !requiredError.value)\n\nconst validationError = computed(() => {\n if (!touched.value)\n return undefined\n return requiredError.value ?? ruleError.value\n})\n\n// Внешняя ошибка поля (серверная валидация) имеет приоритет над встроенной.\nconst fieldErrorMessage = computed(() => props.fieldError ?? validationError.value ?? undefined)\n\n/**\n * Прогоняет `rules` для триггера. Возвращает сообщение или `undefined`.\n * `required` здесь не участвует: он синхронный и считается отдельно, чтобы\n * пустое поле блокировало кнопку без похода в асинхронный движок.\n */\nasync function runRules(trigger: GrFormTrigger): Promise<string | undefined> {\n const rules = rulesForTrigger(normalizedRules.value, trigger)\n if (!rules.length)\n return undefined\n\n const run = ++validationRun\n validating.value = true\n try {\n const message = await runFieldRules(valueModel.value, rules, { value: valueModel.value }, resolveMessage)\n // Ответ устарел — значение или сам диалог успели смениться.\n if (run !== validationRun)\n return undefined\n\n ruleError.value = message\n return message\n }\n finally {\n if (run === validationRun)\n validating.value = false\n }\n}\n\nfunction onBlur(): void {\n touched.value = true\n void runRules('blur')\n}\n\n// Пока подтверждение или проверка в полёте, случайное движение не должно\n// оборвать операцию.\nconst softCloseBlocked = computed(() => props.persistent && (props.confirmLoading || validating.value))\nconst resolvedCloseOnBackdrop = computed(() => (softCloseBlocked.value ? false : props.closeOnBackdrop))\nconst resolvedCloseOnEsc = computed(() => (softCloseBlocked.value ? false : props.closeOnEsc))\n\nfunction onCancel(): void {\n emit('cancel')\n emit('update:modelValue', false)\n}\n\nfunction focusField(): void {\n if (props.multiline)\n textareaRef.value?.focus()\n else inputRef.value?.focus()\n}\n\nasync function onConfirm(): Promise<void> {\n touched.value = true\n\n if (!canConfirm.value) {\n focusField()\n return\n }\n\n if (await runRules('submit')) {\n focusField()\n return\n }\n\n emit('confirm', valueModel.value)\n if (props.closeOnConfirm)\n emit('update:modelValue', false)\n}\n\n/**\n * `Enter` подтверждает — базовое ожидание от окна, единственный смысл которого\n * ввести одно значение. В многострочном режиме `Enter` остаётся переводом\n * строки, поэтому обработчик висит только на однострочном поле.\n */\nfunction onEnter(): void {\n void onConfirm()\n}\n\n// Фокус в поле, а не на панель: диалог существует ровно ради ввода.\n//\n// Через `initialFocus` у `GrModal` это не решается: элемент рождается внутри\n// поддерева диалога, и возврат его же пропом наверх замыкает рендер в цикл\n// («Maximum recursive updates» — проверено). Поэтому фокус ставит содержимое,\n// после отрисовки (`flush: 'post'` + кадр), то есть заведомо позже, чем\n// ловушка фокуса `GrModal` переводит фокус на панель.\n// Источник — не только `modelValue`, но и само поле: содержимое панели\n// появляется на такт позже открытия, и фокус по фиксированному `nextTick`\n// приходился на момент, когда фокусировать ещё нечего.\nwatch(\n [() => props.modelValue, inputRef, textareaRef],\n () => {\n if (!props.modelValue)\n return\n focusField()\n },\n // `immediate`: окно могут смонтировать уже открытым — тогда смены пропа не\n // будет вовсе, и без этого фокус остался бы на панели.\n { immediate: true, flush: 'post' },\n)\n\ndefineSlots<{\n /** Содержимое диалога вместо пропа `message`. */\n default?: () => any\n /** Разбор ошибки вместо встроенного баннера. */\n error?: (props: { error: ResponseErrorInfo | null }) => any\n /** Кнопки диалога вместо пары «отмена и подтверждение». */\n footer?: () => any\n}>()\n</script>\n\n<template>\n <GrDialog\n v-model=\"open\"\n :title=\"resolvedTitle\"\n :size=\"resolvedSize\"\n :close-on-backdrop=\"resolvedCloseOnBackdrop\"\n :close-on-esc=\"resolvedCloseOnEsc\"\n :show-header=\"showHeader\"\n :show-close-button=\"showCloseButton\"\n :header-config=\"headerConfig\"\n :footer-config=\"footerConfig\"\n :body-config=\"bodyConfig\"\n :close-label=\"resolvedCloseLabel\"\n >\n <div class=\"grid gap-4\">\n <slot>\n <div v-if=\"description\" class=\"text-[length:var(--gr-control-text-md)] leading-[var(--gr-control-leading-md)] text-[var(--gr-muted-fg)]\">\n {{ description }}\n </div>\n </slot>\n\n <!-- `id` полю не задаём: `GrFormField` генерирует его сам, а поле читает\n из контекста. Литеральный id ломал два открытых диалога сразу. -->\n <GrFormField :label=\"resolvedLabel\" :error=\"fieldErrorMessage\">\n <GrTextarea\n v-if=\"multiline\"\n ref=\"textareaRef\"\n v-model=\"valueModel\"\n data-testid=\"gr-prompt-input\"\n :placeholder=\"placeholder\"\n :rows=\"rows\"\n :autosize=\"autosize\"\n :maxlength=\"maxlength\"\n :show-count=\"showCount\"\n :invalid=\"!!fieldErrorMessage\"\n @blur=\"onBlur\"\n />\n <GrInput\n v-else\n ref=\"inputRef\"\n v-model=\"valueModel\"\n data-testid=\"gr-prompt-input\"\n :type=\"inputType\"\n :inputmode=\"inputmode\"\n :placeholder=\"placeholder\"\n :maxlength=\"maxlength\"\n :show-count=\"showCount\"\n :invalid=\"!!fieldErrorMessage\"\n @blur=\"onBlur\"\n @keydown.enter=\"onEnter\"\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=\"gr-prompt-cancel\" variant=\"outline\" :size=\"buttonSize\" @click=\"onCancel\">\n {{ resolvedCancelText }}\n </GrButton>\n <GrButton\n data-testid=\"gr-prompt-confirm\"\n :variant=\"confirmVariant\"\n :tone=\"confirmTone\"\n :size=\"buttonSize\"\n :loading=\"confirmLoading || validating\"\n :disabled=\"confirmDisabled || (touched && !canConfirm)\"\n @click=\"onConfirm\"\n >\n {{ resolvedConfirmText }}\n </GrButton>\n </div>\n </slot>\n </template>\n </GrDialog>\n</template>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"GrPromptDialog-C6QGC7QJ.js","names":[],"sources":["../../../../src/components/GrPromptDialog/GrPromptDialog.vue","../../../../src/components/GrPromptDialog/GrPromptDialog.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * GrPromptDialog — GR-примитив диалога ввода значения поверх `GrDialog`.\n *\n * Синхронизирован с `GrDialog` по набору проп-проксирования:\n * `size`, `closeOnBackdrop`, `closeOnEsc`, `showHeader`, `showCloseButton`,\n * `headerConfig`, `footerConfig`, `bodyConfig`, `closeLabel`.\n *\n * Поведение `required=true` (по умолчанию): пустое значение подсвечивается\n * ошибкой только после первого «касания» (blur или попытка confirm),\n * чтобы не шуметь при открытии.\n *\n * Проверки сверх `required` описываются пропом `rules` — тем же движком, что и\n * у `GrForm`: третий частный случай валидации в пакете заводить незачем.\n */\nimport { useGrComponentProp } from '../GrConfigProvider/context'\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 GrTextarea from '../GrTextarea/GrTextarea.vue'\nimport GrResponseErrorBanner from '../GrResponseErrorBanner/GrResponseErrorBanner.vue'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n// Именованные импорты чистых функций: разметки не тянут, поэтому в\n// `config.dependencies` не объявляются (см. правило про зависимости).\nimport { createGrFormMessageResolver, rulesForTrigger, runFieldRules } from '../GrForm/validation'\nimport type { GrFormRule, GrFormTrigger } from '../GrForm/validation'\nimport type { GrButtonSize, GrButtonTone, GrButtonVariant } from '../GrButton'\nimport type { GrDialogSectionConfig, GrDialogSize } from '../GrDialog'\n\n/**\n * Тип поля выводим из самого `GrInput`, а не заводим ему именованный алиас:\n * алиас в пропе `GrInput` заменил бы в таблице API витрины список допустимых\n * значений на своё имя — документация стала бы беднее ради удобства обёртки.\n */\ntype GrInputType = NonNullable<InstanceType<typeof GrInput>['$props']['type']>\nimport type { ResponseErrorInfo } from '../GrResponseErrorBanner'\nimport type { InputHTMLAttributes } from 'vue'\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 inputType?: GrInputType\n /** Программная клавиатура на мобильных. */\n inputmode?: InputHTMLAttributes['inputmode']\n /** Ограничение длины; со `showCount` рисуется счётчик. */\n maxlength?: number\n showCount?: boolean\n /** Многострочный ввод: вместо `GrInput` рисуется `GrTextarea`. */\n multiline?: boolean\n /** Высота многострочного поля в строках. */\n rows?: number\n /** Автоподбор высоты многострочного поля под содержимое. */\n autosize?: boolean\n /**\n * Правила проверки значения — те же, что у `GrForm` (`required`, `type`,\n * `min`/`max`/`len`, `pattern`, свой в т.ч. асинхронный `validator`).\n * Прогоняются на blur (после первого касания) и на подтверждении.\n */\n rules?: GrFormRule | GrFormRule[]\n /**\n * Структура ошибки ответа сервера для показа общим блоком в теле диалога\n * (через `GrResponseErrorBanner`). `null` — блок скрыт.\n */\n error?: ResponseErrorInfo | null\n /**\n * Внешняя ошибка поля ввода (например, серверная валидация). Имеет приоритет\n * над встроенной проверкой. `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 * Запрет закрытия «мягкими» способами (Esc, клик по бэкдропу), пока идёт\n * подтверждение или проверка `rules`. Кнопка закрытия и «Отмена» остаются:\n * окно без единого выхода — ловушка.\n */\n persistent?: boolean\n}\n\nexport interface GrPromptDialogEmits {\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\nimport './defaults'\n\nconst props = withDefaults(defineProps<GrPromptDialogProps>(), {\n title: undefined,\n description: undefined,\n label: undefined,\n placeholder: undefined,\n closeOnBackdrop: true,\n closeOnEsc: true,\n showHeader: true,\n showCloseButton: true,\n // Дефолт `size` живёт в резолвере ниже, а не здесь: Vue подставил бы его\n // до того, как компонент заглянет в `GrConfigProvider`.\n size: undefined,\n headerConfig: undefined,\n footerConfig: undefined,\n bodyConfig: undefined,\n closeLabel: undefined,\n buttonSize: undefined,\n confirmText: undefined,\n cancelText: undefined,\n requiredErrorText: undefined,\n confirmVariant: 'primary',\n confirmTone: 'primary',\n required: true,\n inputType: 'text',\n inputmode: undefined,\n maxlength: undefined,\n showCount: false,\n multiline: false,\n rows: undefined,\n autosize: false,\n rules: undefined,\n error: null,\n fieldError: null,\n confirmLoading: false,\n confirmDisabled: false,\n closeOnConfirm: true,\n persistent: false,\n})\n\n// Эффективный размер: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentProp('GrPromptDialog', 'size', () => props.size, 'md')\n\nconst emit = defineEmits<GrPromptDialogEmits>()\n\n// Дефолты берём из общего i18n-блока пакета (fallback — англ.), а не хардкодим.\nconst { t } = useGranularityTranslations()\nconst resolvedTitle = computed(() => props.title ?? t('gr.dialog.prompt.title', 'Prompt'))\nconst resolvedLabel = computed(() => props.label ?? t('gr.dialog.prompt.label', 'Value'))\nconst resolvedRequiredErrorText = computed(() => props.requiredErrorText ?? t('gr.dialog.prompt.required', 'Enter a value.'))\nconst resolvedCloseLabel = computed(() => props.closeLabel ?? t('gr.common.close', 'Close'))\nconst resolvedConfirmText = computed(() => props.confirmText ?? t('gr.common.confirm', 'Confirm'))\nconst resolvedCancelText = computed(() => props.cancelText ?? t('gr.common.cancel', 'Cancel'))\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)\nconst textareaRef = ref<InstanceType<typeof GrTextarea> | null>(null)\n\n/** Ошибка последнего прогона `rules`; `required` считается отдельно и мгновенно. */\nconst ruleError = ref<string | undefined>(undefined)\nconst validating = ref(false)\n\n// Сторож устаревших прогонов: асинхронное правило может ответить после того,\n// как значение уже сменилось, и дописать чужую ошибку.\nlet validationRun = 0\n\nwatch(\n () => props.modelValue,\n (isOpen) => {\n if (!isOpen)\n return\n\n touched.value = false\n ruleError.value = undefined\n validationRun += 1\n },\n)\n\nconst resolveMessage = createGrFormMessageResolver(t)\n\nconst normalizedRules = computed<GrFormRule[]>(() => {\n if (!props.rules)\n return []\n return Array.isArray(props.rules) ? props.rules : [props.rules]\n})\n\nconst requiredError = computed(() => {\n if (!props.required)\n return undefined\n return valueModel.value.trim().length > 0 ? undefined : resolvedRequiredErrorText.value\n})\n\nconst canConfirm = computed(() => !requiredError.value)\n\nconst validationError = computed(() => {\n if (!touched.value)\n return undefined\n return requiredError.value ?? ruleError.value\n})\n\n// Внешняя ошибка поля (серверная валидация) имеет приоритет над встроенной.\nconst fieldErrorMessage = computed(() => props.fieldError ?? validationError.value ?? undefined)\n\n/**\n * Прогоняет `rules` для триггера. Возвращает сообщение или `undefined`.\n * `required` здесь не участвует: он синхронный и считается отдельно, чтобы\n * пустое поле блокировало кнопку без похода в асинхронный движок.\n */\nasync function runRules(trigger: GrFormTrigger): Promise<string | undefined> {\n const rules = rulesForTrigger(normalizedRules.value, trigger)\n if (!rules.length)\n return undefined\n\n const run = ++validationRun\n validating.value = true\n try {\n const message = await runFieldRules(valueModel.value, rules, { value: valueModel.value }, resolveMessage)\n // Ответ устарел — значение или сам диалог успели смениться.\n if (run !== validationRun)\n return undefined\n\n ruleError.value = message\n return message\n }\n finally {\n if (run === validationRun)\n validating.value = false\n }\n}\n\nfunction onBlur(): void {\n touched.value = true\n void runRules('blur')\n}\n\n// Пока подтверждение или проверка в полёте, случайное движение не должно\n// оборвать операцию.\nconst softCloseBlocked = computed(() => props.persistent && (props.confirmLoading || validating.value))\nconst resolvedCloseOnBackdrop = computed(() => (softCloseBlocked.value ? false : props.closeOnBackdrop))\nconst resolvedCloseOnEsc = computed(() => (softCloseBlocked.value ? false : props.closeOnEsc))\n\nfunction onCancel(): void {\n emit('cancel')\n emit('update:modelValue', false)\n}\n\nfunction focusField(): void {\n if (props.multiline)\n textareaRef.value?.focus()\n else inputRef.value?.focus()\n}\n\nasync function onConfirm(): Promise<void> {\n touched.value = true\n\n if (!canConfirm.value) {\n focusField()\n return\n }\n\n if (await runRules('submit')) {\n focusField()\n return\n }\n\n emit('confirm', valueModel.value)\n if (props.closeOnConfirm)\n emit('update:modelValue', false)\n}\n\n/**\n * `Enter` подтверждает — базовое ожидание от окна, единственный смысл которого\n * ввести одно значение. В многострочном режиме `Enter` остаётся переводом\n * строки, поэтому обработчик висит только на однострочном поле.\n */\nfunction onEnter(): void {\n void onConfirm()\n}\n\n// Фокус в поле, а не на панель: диалог существует ровно ради ввода.\n//\n// Через `initialFocus` у `GrModal` это не решается: элемент рождается внутри\n// поддерева диалога, и возврат его же пропом наверх замыкает рендер в цикл\n// («Maximum recursive updates» — проверено). Поэтому фокус ставит содержимое,\n// после отрисовки (`flush: 'post'` + кадр), то есть заведомо позже, чем\n// ловушка фокуса `GrModal` переводит фокус на панель.\n// Источник — не только `modelValue`, но и само поле: содержимое панели\n// появляется на такт позже открытия, и фокус по фиксированному `nextTick`\n// приходился на момент, когда фокусировать ещё нечего.\nwatch(\n [() => props.modelValue, inputRef, textareaRef],\n () => {\n if (!props.modelValue)\n return\n focusField()\n },\n // `immediate`: окно могут смонтировать уже открытым — тогда смены пропа не\n // будет вовсе, и без этого фокус остался бы на панели.\n { immediate: true, flush: 'post' },\n)\n\ndefineSlots<{\n /** Содержимое диалога вместо пропа `message`. */\n default?: () => any\n /** Разбор ошибки вместо встроенного баннера. */\n error?: (props: { error: ResponseErrorInfo | null }) => any\n /** Кнопки диалога вместо пары «отмена и подтверждение». */\n footer?: () => any\n}>()\n</script>\n\n<template>\n <GrDialog\n v-model=\"open\"\n :title=\"resolvedTitle\"\n :size=\"resolvedSize\"\n :close-on-backdrop=\"resolvedCloseOnBackdrop\"\n :close-on-esc=\"resolvedCloseOnEsc\"\n :show-header=\"showHeader\"\n :show-close-button=\"showCloseButton\"\n :header-config=\"headerConfig\"\n :footer-config=\"footerConfig\"\n :body-config=\"bodyConfig\"\n :close-label=\"resolvedCloseLabel\"\n >\n <div class=\"grid gap-4\">\n <slot>\n <div v-if=\"description\" class=\"text-[length:var(--gr-control-text-md)] leading-[var(--gr-control-leading-md)] text-[var(--gr-muted-fg)]\">\n {{ description }}\n </div>\n </slot>\n\n <!-- `id` полю не задаём: `GrFormField` генерирует его сам, а поле читает\n из контекста. Литеральный id ломал два открытых диалога сразу. -->\n <GrFormField :label=\"resolvedLabel\" :error=\"fieldErrorMessage\">\n <GrTextarea\n v-if=\"multiline\"\n ref=\"textareaRef\"\n v-model=\"valueModel\"\n data-testid=\"gr-prompt-input\"\n :placeholder=\"placeholder\"\n :rows=\"rows\"\n :autosize=\"autosize\"\n :maxlength=\"maxlength\"\n :show-count=\"showCount\"\n :invalid=\"!!fieldErrorMessage\"\n @blur=\"onBlur\"\n />\n <GrInput\n v-else\n ref=\"inputRef\"\n v-model=\"valueModel\"\n data-testid=\"gr-prompt-input\"\n :type=\"inputType\"\n :inputmode=\"inputmode\"\n :placeholder=\"placeholder\"\n :maxlength=\"maxlength\"\n :show-count=\"showCount\"\n :invalid=\"!!fieldErrorMessage\"\n @blur=\"onBlur\"\n @keydown.enter=\"onEnter\"\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=\"gr-prompt-cancel\" variant=\"outline\" :size=\"buttonSize\" @click=\"onCancel\">\n {{ resolvedCancelText }}\n </GrButton>\n <GrButton\n data-testid=\"gr-prompt-confirm\"\n :variant=\"confirmVariant\"\n :tone=\"confirmTone\"\n :size=\"buttonSize\"\n :loading=\"confirmLoading || validating\"\n :disabled=\"confirmDisabled || (touched && !canConfirm)\"\n @click=\"onConfirm\"\n >\n {{ resolvedConfirmText }}\n </GrButton>\n </div>\n </slot>\n </template>\n </GrDialog>\n</template>\n","<script setup lang=\"ts\">\n/**\n * GrPromptDialog — GR-примитив диалога ввода значения поверх `GrDialog`.\n *\n * Синхронизирован с `GrDialog` по набору проп-проксирования:\n * `size`, `closeOnBackdrop`, `closeOnEsc`, `showHeader`, `showCloseButton`,\n * `headerConfig`, `footerConfig`, `bodyConfig`, `closeLabel`.\n *\n * Поведение `required=true` (по умолчанию): пустое значение подсвечивается\n * ошибкой только после первого «касания» (blur или попытка confirm),\n * чтобы не шуметь при открытии.\n *\n * Проверки сверх `required` описываются пропом `rules` — тем же движком, что и\n * у `GrForm`: третий частный случай валидации в пакете заводить незачем.\n */\nimport { useGrComponentProp } from '../GrConfigProvider/context'\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 GrTextarea from '../GrTextarea/GrTextarea.vue'\nimport GrResponseErrorBanner from '../GrResponseErrorBanner/GrResponseErrorBanner.vue'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n// Именованные импорты чистых функций: разметки не тянут, поэтому в\n// `config.dependencies` не объявляются (см. правило про зависимости).\nimport { createGrFormMessageResolver, rulesForTrigger, runFieldRules } from '../GrForm/validation'\nimport type { GrFormRule, GrFormTrigger } from '../GrForm/validation'\nimport type { GrButtonSize, GrButtonTone, GrButtonVariant } from '../GrButton'\nimport type { GrDialogSectionConfig, GrDialogSize } from '../GrDialog'\n\n/**\n * Тип поля выводим из самого `GrInput`, а не заводим ему именованный алиас:\n * алиас в пропе `GrInput` заменил бы в таблице API витрины список допустимых\n * значений на своё имя — документация стала бы беднее ради удобства обёртки.\n */\ntype GrInputType = NonNullable<InstanceType<typeof GrInput>['$props']['type']>\nimport type { ResponseErrorInfo } from '../GrResponseErrorBanner'\nimport type { InputHTMLAttributes } from 'vue'\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 inputType?: GrInputType\n /** Программная клавиатура на мобильных. */\n inputmode?: InputHTMLAttributes['inputmode']\n /** Ограничение длины; со `showCount` рисуется счётчик. */\n maxlength?: number\n showCount?: boolean\n /** Многострочный ввод: вместо `GrInput` рисуется `GrTextarea`. */\n multiline?: boolean\n /** Высота многострочного поля в строках. */\n rows?: number\n /** Автоподбор высоты многострочного поля под содержимое. */\n autosize?: boolean\n /**\n * Правила проверки значения — те же, что у `GrForm` (`required`, `type`,\n * `min`/`max`/`len`, `pattern`, свой в т.ч. асинхронный `validator`).\n * Прогоняются на blur (после первого касания) и на подтверждении.\n */\n rules?: GrFormRule | GrFormRule[]\n /**\n * Структура ошибки ответа сервера для показа общим блоком в теле диалога\n * (через `GrResponseErrorBanner`). `null` — блок скрыт.\n */\n error?: ResponseErrorInfo | null\n /**\n * Внешняя ошибка поля ввода (например, серверная валидация). Имеет приоритет\n * над встроенной проверкой. `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 * Запрет закрытия «мягкими» способами (Esc, клик по бэкдропу), пока идёт\n * подтверждение или проверка `rules`. Кнопка закрытия и «Отмена» остаются:\n * окно без единого выхода — ловушка.\n */\n persistent?: boolean\n}\n\nexport interface GrPromptDialogEmits {\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\nimport './defaults'\n\nconst props = withDefaults(defineProps<GrPromptDialogProps>(), {\n title: undefined,\n description: undefined,\n label: undefined,\n placeholder: undefined,\n closeOnBackdrop: true,\n closeOnEsc: true,\n showHeader: true,\n showCloseButton: true,\n // Дефолт `size` живёт в резолвере ниже, а не здесь: Vue подставил бы его\n // до того, как компонент заглянет в `GrConfigProvider`.\n size: undefined,\n headerConfig: undefined,\n footerConfig: undefined,\n bodyConfig: undefined,\n closeLabel: undefined,\n buttonSize: undefined,\n confirmText: undefined,\n cancelText: undefined,\n requiredErrorText: undefined,\n confirmVariant: 'primary',\n confirmTone: 'primary',\n required: true,\n inputType: 'text',\n inputmode: undefined,\n maxlength: undefined,\n showCount: false,\n multiline: false,\n rows: undefined,\n autosize: false,\n rules: undefined,\n error: null,\n fieldError: null,\n confirmLoading: false,\n confirmDisabled: false,\n closeOnConfirm: true,\n persistent: false,\n})\n\n// Эффективный размер: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentProp('GrPromptDialog', 'size', () => props.size, 'md')\n\nconst emit = defineEmits<GrPromptDialogEmits>()\n\n// Дефолты берём из общего i18n-блока пакета (fallback — англ.), а не хардкодим.\nconst { t } = useGranularityTranslations()\nconst resolvedTitle = computed(() => props.title ?? t('gr.dialog.prompt.title', 'Prompt'))\nconst resolvedLabel = computed(() => props.label ?? t('gr.dialog.prompt.label', 'Value'))\nconst resolvedRequiredErrorText = computed(() => props.requiredErrorText ?? t('gr.dialog.prompt.required', 'Enter a value.'))\nconst resolvedCloseLabel = computed(() => props.closeLabel ?? t('gr.common.close', 'Close'))\nconst resolvedConfirmText = computed(() => props.confirmText ?? t('gr.common.confirm', 'Confirm'))\nconst resolvedCancelText = computed(() => props.cancelText ?? t('gr.common.cancel', 'Cancel'))\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)\nconst textareaRef = ref<InstanceType<typeof GrTextarea> | null>(null)\n\n/** Ошибка последнего прогона `rules`; `required` считается отдельно и мгновенно. */\nconst ruleError = ref<string | undefined>(undefined)\nconst validating = ref(false)\n\n// Сторож устаревших прогонов: асинхронное правило может ответить после того,\n// как значение уже сменилось, и дописать чужую ошибку.\nlet validationRun = 0\n\nwatch(\n () => props.modelValue,\n (isOpen) => {\n if (!isOpen)\n return\n\n touched.value = false\n ruleError.value = undefined\n validationRun += 1\n },\n)\n\nconst resolveMessage = createGrFormMessageResolver(t)\n\nconst normalizedRules = computed<GrFormRule[]>(() => {\n if (!props.rules)\n return []\n return Array.isArray(props.rules) ? props.rules : [props.rules]\n})\n\nconst requiredError = computed(() => {\n if (!props.required)\n return undefined\n return valueModel.value.trim().length > 0 ? undefined : resolvedRequiredErrorText.value\n})\n\nconst canConfirm = computed(() => !requiredError.value)\n\nconst validationError = computed(() => {\n if (!touched.value)\n return undefined\n return requiredError.value ?? ruleError.value\n})\n\n// Внешняя ошибка поля (серверная валидация) имеет приоритет над встроенной.\nconst fieldErrorMessage = computed(() => props.fieldError ?? validationError.value ?? undefined)\n\n/**\n * Прогоняет `rules` для триггера. Возвращает сообщение или `undefined`.\n * `required` здесь не участвует: он синхронный и считается отдельно, чтобы\n * пустое поле блокировало кнопку без похода в асинхронный движок.\n */\nasync function runRules(trigger: GrFormTrigger): Promise<string | undefined> {\n const rules = rulesForTrigger(normalizedRules.value, trigger)\n if (!rules.length)\n return undefined\n\n const run = ++validationRun\n validating.value = true\n try {\n const message = await runFieldRules(valueModel.value, rules, { value: valueModel.value }, resolveMessage)\n // Ответ устарел — значение или сам диалог успели смениться.\n if (run !== validationRun)\n return undefined\n\n ruleError.value = message\n return message\n }\n finally {\n if (run === validationRun)\n validating.value = false\n }\n}\n\nfunction onBlur(): void {\n touched.value = true\n void runRules('blur')\n}\n\n// Пока подтверждение или проверка в полёте, случайное движение не должно\n// оборвать операцию.\nconst softCloseBlocked = computed(() => props.persistent && (props.confirmLoading || validating.value))\nconst resolvedCloseOnBackdrop = computed(() => (softCloseBlocked.value ? false : props.closeOnBackdrop))\nconst resolvedCloseOnEsc = computed(() => (softCloseBlocked.value ? false : props.closeOnEsc))\n\nfunction onCancel(): void {\n emit('cancel')\n emit('update:modelValue', false)\n}\n\nfunction focusField(): void {\n if (props.multiline)\n textareaRef.value?.focus()\n else inputRef.value?.focus()\n}\n\nasync function onConfirm(): Promise<void> {\n touched.value = true\n\n if (!canConfirm.value) {\n focusField()\n return\n }\n\n if (await runRules('submit')) {\n focusField()\n return\n }\n\n emit('confirm', valueModel.value)\n if (props.closeOnConfirm)\n emit('update:modelValue', false)\n}\n\n/**\n * `Enter` подтверждает — базовое ожидание от окна, единственный смысл которого\n * ввести одно значение. В многострочном режиме `Enter` остаётся переводом\n * строки, поэтому обработчик висит только на однострочном поле.\n */\nfunction onEnter(): void {\n void onConfirm()\n}\n\n// Фокус в поле, а не на панель: диалог существует ровно ради ввода.\n//\n// Через `initialFocus` у `GrModal` это не решается: элемент рождается внутри\n// поддерева диалога, и возврат его же пропом наверх замыкает рендер в цикл\n// («Maximum recursive updates» — проверено). Поэтому фокус ставит содержимое,\n// после отрисовки (`flush: 'post'` + кадр), то есть заведомо позже, чем\n// ловушка фокуса `GrModal` переводит фокус на панель.\n// Источник — не только `modelValue`, но и само поле: содержимое панели\n// появляется на такт позже открытия, и фокус по фиксированному `nextTick`\n// приходился на момент, когда фокусировать ещё нечего.\nwatch(\n [() => props.modelValue, inputRef, textareaRef],\n () => {\n if (!props.modelValue)\n return\n focusField()\n },\n // `immediate`: окно могут смонтировать уже открытым — тогда смены пропа не\n // будет вовсе, и без этого фокус остался бы на панели.\n { immediate: true, flush: 'post' },\n)\n\ndefineSlots<{\n /** Содержимое диалога вместо пропа `message`. */\n default?: () => any\n /** Разбор ошибки вместо встроенного баннера. */\n error?: (props: { error: ResponseErrorInfo | null }) => any\n /** Кнопки диалога вместо пары «отмена и подтверждение». */\n footer?: () => any\n}>()\n</script>\n\n<template>\n <GrDialog\n v-model=\"open\"\n :title=\"resolvedTitle\"\n :size=\"resolvedSize\"\n :close-on-backdrop=\"resolvedCloseOnBackdrop\"\n :close-on-esc=\"resolvedCloseOnEsc\"\n :show-header=\"showHeader\"\n :show-close-button=\"showCloseButton\"\n :header-config=\"headerConfig\"\n :footer-config=\"footerConfig\"\n :body-config=\"bodyConfig\"\n :close-label=\"resolvedCloseLabel\"\n >\n <div class=\"grid gap-4\">\n <slot>\n <div v-if=\"description\" class=\"text-[length:var(--gr-control-text-md)] leading-[var(--gr-control-leading-md)] text-[var(--gr-muted-fg)]\">\n {{ description }}\n </div>\n </slot>\n\n <!-- `id` полю не задаём: `GrFormField` генерирует его сам, а поле читает\n из контекста. Литеральный id ломал два открытых диалога сразу. -->\n <GrFormField :label=\"resolvedLabel\" :error=\"fieldErrorMessage\">\n <GrTextarea\n v-if=\"multiline\"\n ref=\"textareaRef\"\n v-model=\"valueModel\"\n data-testid=\"gr-prompt-input\"\n :placeholder=\"placeholder\"\n :rows=\"rows\"\n :autosize=\"autosize\"\n :maxlength=\"maxlength\"\n :show-count=\"showCount\"\n :invalid=\"!!fieldErrorMessage\"\n @blur=\"onBlur\"\n />\n <GrInput\n v-else\n ref=\"inputRef\"\n v-model=\"valueModel\"\n data-testid=\"gr-prompt-input\"\n :type=\"inputType\"\n :inputmode=\"inputmode\"\n :placeholder=\"placeholder\"\n :maxlength=\"maxlength\"\n :show-count=\"showCount\"\n :invalid=\"!!fieldErrorMessage\"\n @blur=\"onBlur\"\n @keydown.enter=\"onEnter\"\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=\"gr-prompt-cancel\" variant=\"outline\" :size=\"buttonSize\" @click=\"onCancel\">\n {{ resolvedCancelText }}\n </GrButton>\n <GrButton\n data-testid=\"gr-prompt-confirm\"\n :variant=\"confirmVariant\"\n :tone=\"confirmTone\"\n :size=\"buttonSize\"\n :loading=\"confirmLoading || validating\"\n :disabled=\"confirmDisabled || (touched && !canConfirm)\"\n @click=\"onConfirm\"\n >\n {{ resolvedConfirmText }}\n </GrButton>\n </div>\n </slot>\n </template>\n </GrDialog>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyHA,MAAM,QAAQ;EAwCd,MAAM,eAAe,mBAAmB,kBAAkB,cAAc,MAAM,MAAM,IAAI;EAExF,MAAM,OAAO;EAGb,MAAM,EAAE,MAAM,2BAA2B;EACzC,MAAM,gBAAgB,eAAe,MAAM,SAAS,EAAE,0BAA0B,QAAQ,CAAC;EACzF,MAAM,gBAAgB,eAAe,MAAM,SAAS,EAAE,0BAA0B,OAAO,CAAC;EACxF,MAAM,4BAA4B,eAAe,MAAM,qBAAqB,EAAE,6BAA6B,gBAAgB,CAAC;EAC5H,MAAM,qBAAqB,eAAe,MAAM,cAAc,EAAE,mBAAmB,OAAO,CAAC;EAC3F,MAAM,sBAAsB,eAAe,MAAM,eAAe,EAAE,qBAAqB,SAAS,CAAC;EACjG,MAAM,qBAAqB,eAAe,MAAM,cAAc,EAAE,oBAAoB,QAAQ,CAAC;EAE7F,MAAM,OAAO,SAAS;GACpB,WAAW,MAAM;GACjB,MAAM,MAAe,KAAK,qBAAqB,CAAC;EAClD,CAAC;EAED,MAAM,aAAa,SAAS;GAC1B,WAAW,MAAM;GACjB,MAAM,MAAc,KAAK,gBAAgB,CAAC;EAC5C,CAAC;EAED,MAAM,UAAU,IAAI,KAAK;EACzB,MAAM,WAAW,IAAyC,IAAI;EAC9D,MAAM,cAAc,IAA4C,IAAI;;EAGpE,MAAM,YAAY,IAAwB,KAAA,CAAS;EACnD,MAAM,aAAa,IAAI,KAAK;EAI5B,IAAI,gBAAgB;EAEpB,YACQ,MAAM,aACX,WAAW;GACV,IAAI,CAAC,QACH;GAEF,QAAQ,QAAQ;GAChB,UAAU,QAAQ,KAAA;GAClB,iBAAiB;EACnB,CACF;EAEA,MAAM,iBAAiB,4BAA4B,CAAC;EAEpD,MAAM,kBAAkB,eAA6B;GACnD,IAAI,CAAC,MAAM,OACT,OAAO,CAAC;GACV,OAAO,MAAM,QAAQ,MAAM,KAAK,IAAI,MAAM,QAAQ,CAAC,MAAM,KAAK;EAChE,CAAC;EAED,MAAM,gBAAgB,eAAe;GACnC,IAAI,CAAC,MAAM,UACT,OAAO,KAAA;GACT,OAAO,WAAW,MAAM,KAAK,CAAC,CAAC,SAAS,IAAI,KAAA,IAAY,0BAA0B;EACpF,CAAC;EAED,MAAM,aAAa,eAAe,CAAC,cAAc,KAAK;EAEtD,MAAM,kBAAkB,eAAe;GACrC,IAAI,CAAC,QAAQ,OACX,OAAO,KAAA;GACT,OAAO,cAAc,SAAS,UAAU;EAC1C,CAAC;EAGD,MAAM,oBAAoB,eAAe,MAAM,cAAc,gBAAgB,SAAS,KAAA,CAAS;;;;;;EAO/F,eAAe,SAAS,SAAqD;GAC3E,MAAM,QAAQ,gBAAgB,gBAAgB,OAAO,OAAO;GAC5D,IAAI,CAAC,MAAM,QACT,OAAO,KAAA;GAET,MAAM,MAAM,EAAE;GACd,WAAW,QAAQ;GACnB,IAAI;IACF,MAAM,UAAU,MAAM,cAAc,WAAW,OAAO,OAAO,EAAE,OAAO,WAAW,MAAM,GAAG,cAAc;IAExG,IAAI,QAAQ,eACV,OAAO,KAAA;IAET,UAAU,QAAQ;IAClB,OAAO;GACT,UACQ;IACN,IAAI,QAAQ,eACV,WAAW,QAAQ;GACvB;EACF;EAEA,SAAS,SAAe;GACtB,QAAQ,QAAQ;GAChB,SAAc,MAAM;EACtB;EAIA,MAAM,mBAAmB,eAAe,MAAM,eAAe,MAAM,kBAAkB,WAAW,MAAM;EACtG,MAAM,0BAA0B,eAAgB,iBAAiB,QAAQ,QAAQ,MAAM,eAAgB;EACvG,MAAM,qBAAqB,eAAgB,iBAAiB,QAAQ,QAAQ,MAAM,UAAW;EAE7F,SAAS,WAAiB;GACxB,KAAK,QAAQ;GACb,KAAK,qBAAqB,KAAK;EACjC;EAEA,SAAS,aAAmB;GAC1B,IAAI,MAAM,WACR,YAAY,OAAO,MAAM;QACtB,SAAS,OAAO,MAAM;EAC7B;EAEA,eAAe,YAA2B;GACxC,QAAQ,QAAQ;GAEhB,IAAI,CAAC,WAAW,OAAO;IACrB,WAAW;IACX;GACF;GAEA,IAAI,MAAM,SAAS,QAAQ,GAAG;IAC5B,WAAW;IACX;GACF;GAEA,KAAK,WAAW,WAAW,KAAK;GAChC,IAAI,MAAM,gBACR,KAAK,qBAAqB,KAAK;EACnC;;;;;;EAOA,SAAS,UAAgB;GACvB,UAAe;EACjB;EAYA,MACE;SAAO,MAAM;GAAY;GAAU;EAAW,SACxC;GACJ,IAAI,CAAC,MAAM,YACT;GACF,WAAW;EACb,GAGA;GAAE,WAAW;GAAM,OAAO;EAAO,CACnC;;uBAaE,YA6EW,kBAAA;gBA5EA,KAAA;sEAAI,QAAA;IACZ,OAAO,cAAA;IACP,MAAM,MAAA,YAAA;IACN,qBAAmB,wBAAA;IACnB,gBAAc,mBAAA;IACd,eAAa,QAAA;IACb,qBAAmB,QAAA;IACnB,iBAAe,QAAA;IACf,iBAAe,QAAA;IACf,eAAa,QAAA;IACb,eAAa,mBAAA;;IA8CH,QAAM,cAkBR,CAjBP,WAiBO,KAAA,QAAA,UAAA,CAAA,SAAA,CAhBL,mBAeM,OAfN,YAeM,CAdJ,YAEW,kBAAA;KAFD,eAAY;KAAmB,SAAQ;KAAW,MAAM,QAAA;KAAa,SAAO;;4BAC5D,CAAA,gBAAA,gBAArB,mBAAA,KAAkB,GAAA,CAAA,CAAA,CAAA;;qBAEvB,YAUW,kBAAA;KATT,eAAY;KACX,SAAS,QAAA;KACT,MAAM,QAAA;KACN,MAAM,QAAA;KACN,SAAS,QAAA,kBAAkB,WAAA;KAC3B,UAAU,QAAA,mBAAoB,QAAA,SAAO,CAAK,WAAA;KAC1C,SAAO;;4BAEiB,CAAA,gBAAA,gBAAtB,oBAAA,KAAmB,GAAA,CAAA,CAAA,CAAA;;;;;;;;;2BAjBxB,CA1CN,mBA0CM,OA1CN,YA0CM;KAzCJ,WAIO,KAAA,QAAA,WAAA,CAAA,SAAA,CAHM,QAAA,eAAA,UAAA,GAAX,mBAEM,OAFN,YAEM,gBADD,QAAA,WAAW,GAAA,CAAA,KAAA,mBAAA,IAAA,IAAA,CAAA,CAAA;KAMlB,YA4Bc,qBAAA;MA5BA,OAAO,cAAA;MAAgB,OAAO,kBAAA;;6BAaxC,CAXM,QAAA,aAAA,UAAA,GADR,YAYE,oBAAA;;gBAVI;OAAJ,KAAI;mBACK,WAAA;+EAAU,QAAA;OACnB,eAAY;OACX,aAAa,QAAA;OACb,MAAM,QAAA;OACN,UAAU,QAAA;OACV,WAAW,QAAA;OACX,cAAY,QAAA;OACZ,SAAO,CAAA,CAAI,kBAAA;OACL;;;;;;;;;0BAET,YAaE,iBAAA;;gBAXI;OAAJ,KAAI;mBACK,WAAA;+EAAU,QAAA;OACnB,eAAY;OACX,MAAM,QAAA;OACN,WAAW,QAAA;OACX,aAAa,QAAA;OACb,WAAW,QAAA;OACX,cAAY,QAAA;OACZ,SAAO,CAAA,CAAI,kBAAA;OACL;OACN,WAAO,SAAQ,SAAO,CAAA,OAAA,CAAA;;;;;;;;;;;;KAI3B,WAEO,KAAA,QAAA,SAAA,EAFa,OAAO,QAAA,MAAK,SAEzB,CADwB,QAAA,SAAA,UAAA,GAA7B,YAA0E,+BAAA;;MAArC,OAAO,QAAA;MAAQ,eAAa"}
|