@feugene/granularity 0.34.2 → 0.36.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-_QF0O1vq.js → config-CMNfZO7S.js} +2 -2
- package/dist/chunks/config-CMNfZO7S.js.map +1 -0
- package/dist/chunks/{config-mlheIMtV.js → config-DBi0GVce.js} +2 -2
- package/dist/chunks/config-DBi0GVce.js.map +1 -0
- package/dist/chunks/{grModalStyles-EZfuEiw_.js → grModalStyles-BEkiFal_.js} +10 -1
- package/dist/chunks/grModalStyles-BEkiFal_.js.map +1 -0
- package/dist/chunks/{granular-provider-BAy4APOX.js → granular-provider-Bp_BCBnr.js} +2 -2
- package/dist/chunks/{granular-provider-BAy4APOX.js.map → granular-provider-Bp_BCBnr.js.map} +1 -1
- package/dist/components/GrColorPicker/chunks/{GrColorPicker-CIgdPXHn.js → GrColorPicker-B-L4CWMt.js} +2 -2
- package/dist/components/GrColorPicker/chunks/{GrColorPicker-CIgdPXHn.js.map → GrColorPicker-B-L4CWMt.js.map} +1 -1
- package/dist/components/GrColorPicker/index.js +1 -1
- package/dist/components/GrCommandPalette/chunks/{GrCommandPalette-BZHUKlux.js → GrCommandPalette-8HkYmMVP.js} +1 -1
- package/dist/components/GrCommandPalette/chunks/{GrCommandPalette-BZHUKlux.js.map → GrCommandPalette-8HkYmMVP.js.map} +1 -1
- package/dist/components/GrCommandPalette/index.js +1 -1
- package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-jRDs_E6C.js → GrConfirmDialog-CZEO2JR4.js} +1 -1
- package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-jRDs_E6C.js.map → GrConfirmDialog-CZEO2JR4.js.map} +1 -1
- package/dist/components/GrConfirmDialog/index.js +1 -1
- package/dist/components/GrDialog/chunks/{GrDialog-C01Hhk6G.js → GrDialog-CKNbnxwO.js} +1 -1
- package/dist/components/GrDialog/chunks/{GrDialog-C01Hhk6G.js.map → GrDialog-CKNbnxwO.js.map} +1 -1
- package/dist/components/GrDialog/index.js +1 -1
- package/dist/components/GrDialogService/chunks/{GrDialogService-CmsJNUmM.js → GrDialogService-Ut8iiBQh.js} +2 -2
- package/dist/components/GrDialogService/chunks/{GrDialogService-CmsJNUmM.js.map → GrDialogService-Ut8iiBQh.js.map} +1 -1
- package/dist/components/GrDialogService/index.js +1 -1
- package/dist/components/GrFileUpload/chunks/{GrFileUpload-LL8RZvC0.js → GrFileUpload-C0ROpmOY.js} +1 -1
- package/dist/components/GrFileUpload/chunks/{GrFileUpload-LL8RZvC0.js.map → GrFileUpload-C0ROpmOY.js.map} +1 -1
- package/dist/components/GrFileUpload/index.js +1 -1
- package/dist/components/GrInput/chunks/{GrInput-BLywRZUN.js → GrInput-CJ7eu3fR.js} +1 -0
- package/dist/components/GrInput/chunks/{GrInput-BLywRZUN.js.map → GrInput-CJ7eu3fR.js.map} +1 -1
- package/dist/components/GrInput/index.js +1 -1
- package/dist/components/GrJsonViewer/chunks/{GrJsonViewer-BCZ7lE2k.js → GrJsonViewer-BXG7DdTe.js} +1 -1
- package/dist/components/GrJsonViewer/chunks/{GrJsonViewer-BCZ7lE2k.js.map → GrJsonViewer-BXG7DdTe.js.map} +1 -1
- package/dist/components/GrJsonViewer/index.js +1 -1
- package/dist/components/GrModal/chunks/{GrModal-_JrDhon-.js → GrModal-DixnhJcu.js} +3 -2
- package/dist/components/GrModal/chunks/GrModal-DixnhJcu.js.map +1 -0
- package/dist/components/GrModal/index.js +2 -2
- package/dist/components/GrNavbar/chunks/{GrNavbar-_rtHKwTf.js → GrNavbar-Dr8e5kW-.js} +14 -4
- package/dist/components/GrNavbar/chunks/GrNavbar-Dr8e5kW-.js.map +1 -0
- package/dist/components/GrNavbar/index.js +1 -1
- package/dist/components/GrPagination/chunks/{GrPagination-gBoWVHk6.js → GrPagination-68iyAwQz.js} +24 -9
- package/dist/components/GrPagination/chunks/GrPagination-68iyAwQz.js.map +1 -0
- package/dist/components/GrPagination/index.js +1 -1
- package/dist/components/GrProgressBar/chunks/{GrProgressBar-hszUOC8f.js → GrProgressBar-Cd7AaJtR.js} +5 -2
- package/dist/components/GrProgressBar/chunks/GrProgressBar-Cd7AaJtR.js.map +1 -0
- package/dist/components/GrProgressBar/index.js +1 -1
- package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-CailXSqW.js → GrPromptDialog-Ckeombia.js} +2 -2
- package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-CailXSqW.js.map → GrPromptDialog-Ckeombia.js.map} +1 -1
- package/dist/components/GrPromptDialog/index.js +1 -1
- package/dist/components/GrSelect/chunks/{GrSelect-CZqV1Eg4.js → GrSelect-Bm3Jugx8.js} +3 -7
- package/dist/components/GrSelect/chunks/GrSelect-Bm3Jugx8.js.map +1 -0
- package/dist/components/GrSelect/index.js +1 -1
- package/dist/components/GrSelect/styles.css +1 -1
- package/dist/components/GrSlider/chunks/{GrSlider-DTTrZTHK.js → GrSlider-DM6W6U9c.js} +9 -3
- package/dist/components/GrSlider/chunks/GrSlider-DM6W6U9c.js.map +1 -0
- package/dist/components/GrSlider/index.js +1 -1
- package/dist/components/GrStatistic/chunks/{GrStatistic-qHVasK37.js → GrStatistic-BoLkXw7F.js} +7 -3
- package/dist/components/GrStatistic/chunks/GrStatistic-BoLkXw7F.js.map +1 -0
- package/dist/components/GrStatistic/index.js +1 -1
- package/dist/components/GrSteps/chunks/{GrSteps-C2jxpZoN.js → GrSteps-CpGHXLOc.js} +1 -1
- package/dist/components/GrSteps/chunks/{GrSteps-C2jxpZoN.js.map → GrSteps-CpGHXLOc.js.map} +1 -1
- package/dist/components/GrSteps/index.js +1 -1
- package/dist/components/GrTabs/chunks/{GrTabs-CEKT5tXv.js → GrTabs-CT2nk_HV.js} +1 -1
- package/dist/components/GrTabs/chunks/{GrTabs-CEKT5tXv.js.map → GrTabs-CT2nk_HV.js.map} +1 -1
- package/dist/components/GrTabs/index.js +2 -2
- package/dist/components/GrTreeSelect/chunks/{GrTreeSelect-DOXcpWq2.js → GrTreeSelect-MGsyvkJV.js} +1 -1
- package/dist/components/GrTreeSelect/chunks/{GrTreeSelect-DOXcpWq2.js.map → GrTreeSelect-MGsyvkJV.js.map} +1 -1
- package/dist/components/GrTreeSelect/index.js +1 -1
- package/dist/granular-provider-node.js +1 -1
- package/dist/granular-provider.js +1 -1
- package/dist/index.js +21 -21
- package/dist/types/components/GrModal/grModalStyles.d.ts +9 -0
- package/dist/types/components/GrNavbar/GrNavbar.vue.d.ts +14 -0
- package/dist/types/components/GrNavbar/defaults.d.ts +10 -0
- package/dist/types/components/GrNavbar/index.d.ts +1 -0
- package/dist/types/components/GrProgressBar/GrProgressBar.vue.d.ts +1 -1
- package/dist/web-types.json +11 -2
- package/package.json +3 -3
- package/dist/chunks/config-_QF0O1vq.js.map +0 -1
- package/dist/chunks/config-mlheIMtV.js.map +0 -1
- package/dist/chunks/grModalStyles-EZfuEiw_.js.map +0 -1
- package/dist/components/GrModal/chunks/GrModal-_JrDhon-.js.map +0 -1
- package/dist/components/GrNavbar/chunks/GrNavbar-_rtHKwTf.js.map +0 -1
- package/dist/components/GrPagination/chunks/GrPagination-gBoWVHk6.js.map +0 -1
- package/dist/components/GrProgressBar/chunks/GrProgressBar-hszUOC8f.js.map +0 -1
- package/dist/components/GrSelect/chunks/GrSelect-CZqV1Eg4.js.map +0 -1
- package/dist/components/GrSlider/chunks/GrSlider-DTTrZTHK.js.map +0 -1
- package/dist/components/GrStatistic/chunks/GrStatistic-qHVasK37.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GrInput-BLywRZUN.js","names":["$slots","$attrs"],"sources":["../../../../src/components/GrInput/GrInput.vue","../../../../src/components/GrInput/GrInput.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport type { GrComponentSize } from '../shared/sizes'\nimport { computed, ref, useId } from 'vue'\n\nimport { useControlAddons } from '../../composables/internal/useControlAddons'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport type { InputHTMLAttributes } from 'vue'\n\nimport { addonInlinePrefixClass, addonInlineSuffixClass, addonSegmentPrefixClass, addonSegmentSuffixClass, grInputFieldClass, grInputShellClass, paddingX } from './grInputStyles'\n\nimport IconLoader from '~icons/lucide/loader-2'\nimport IconX from '~icons/lucide/x'\nimport IconEye from '~icons/lucide/eye'\nimport IconEyeOff from '~icons/lucide/eye-off'\n\nexport interface GrInputProps {\n /**\n * Значение поля. Необязательное: без `v-model` поле рисуется пустым.\n * Дефолт — пустая строка, а не `undefined`: длина значения читается\n * напрямую (`showClear`), и `undefined` уронил бы рендер.\n */\n modelValue?: string\n type?: 'text' | 'email' | 'password' | 'number' | 'search' | 'tel' | 'url'\n placeholder?: string\n autocomplete?: string\n inputmode?: InputHTMLAttributes['inputmode']\n disabled?: boolean\n /** Только для чтения: значение видно и выделяемо, но не редактируется. */\n readonly?: boolean\n invalid?: boolean\n /** Обязательное поле (`aria-required`). Складывается с `required` у `GrFormField`. */\n required?: boolean\n /** Доступное имя вне `GrFormField`. */\n ariaLabel?: string\n state?: 'default' | 'success' | 'warning' | 'danger'\n name?: string\n id?: string\n size?: GrInputSize\n\n /** Показывать кнопку очистки, когда есть значение (и не disabled/readonly). */\n clearable?: boolean\n /** i18n aria-label кнопки очистки. */\n clearLabel?: string\n /** Ограничение длины + основа для счётчика символов. */\n maxlength?: number\n /** Показывать счётчик символов (`len` или `len/maxlength`). */\n showCount?: boolean\n /**\n * Фоновая работа по полю (проверка занятости логина, автосохранение):\n * спиннер в trailing-области + `aria-busy`. Ввод не блокируется —\n * для этого есть `disabled`/`readonly`.\n */\n loading?: boolean\n /** Кнопка показать/скрыть пароль (только при `type=\"password\"`). */\n passwordToggle?: boolean\n /** i18n aria-label кнопки показать/скрыть пароль. */\n passwordShowLabel?: string\n passwordHideLabel?: string\n\n textAlign?: GrInputTextAlign\n\n prefixMinWidth?: string\n prefixMaxWidth?: string\n suffixMinWidth?: string\n suffixMaxWidth?: string\n /**\n * Фиксированная ширина у prefix/suffix: аддон получает жёсткую ширину\n * (из `*MaxWidth` → `*MinWidth` → дефолт), а контент обрезается по краю\n * (prefix — справа, suffix — слева). По умолчанию аддоны «растягиваются»\n * под контент (в пределах min/max), а излишек клипается оболочкой.\n */\n prefixFixed?: boolean\n suffixFixed?: boolean\n /**\n * Как выглядят аддоны `#prefix`/`#suffix`.\n *\n * `segment` (по умолчанию) — отдельный отсек, отрезанный рамкой и выровненный\n * по ступени размера: так поле с «₽» и поле с «USD» стоят в колонку.\n * `inline` — украшение внутри рамки: ни разделителя, ни своей ширины.\n *\n * Разница не косметическая. Поисковая строка с лупой в сегменте читается\n * составным элементом — полем с приклеенной кнопкой, — а не одним полем;\n * именно поэтому иконку внутри рамки нельзя было выразить аддоном, и\n * потребители отказывались от неё вовсе.\n */\n addon?: 'segment' | 'inline'\n}\n\nexport interface GrInputEmits {\n (e: 'update:modelValue', value: string): void\n /** Значение зафиксировано нативным `change` — по `blur` или `Enter`. */\n (e: 'change', value: string): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n /**\n * Значение стёрто кнопкой очистки. Отдельное событие потому, что по\n * `update:modelValue` программную очистку от ручного стирания не отличить.\n */\n (e: 'clear'): void\n}\n\ndefineOptions({\n inheritAttrs: false,\n})\n\nexport type GrInputSize = GrComponentSize\nexport type GrInputTextAlign = 'left' | 'center' | 'right'\n\nconst props = withDefaults(\n defineProps<GrInputProps>(),\n {\n modelValue: '',\n type: 'text',\n placeholder: undefined,\n autocomplete: undefined,\n inputmode: undefined,\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n ariaLabel: undefined,\n state: 'default',\n name: undefined,\n id: undefined,\n size: undefined,\n\n // Настраивается через `GrConfigProvider`; дефолт — в резолвере ниже.\n clearable: undefined,\n clearLabel: undefined,\n maxlength: undefined,\n showCount: false,\n loading: false,\n passwordToggle: false,\n passwordShowLabel: undefined,\n passwordHideLabel: undefined,\n\n textAlign: 'left',\n\n prefixMinWidth: undefined,\n prefixMaxWidth: undefined,\n suffixMinWidth: undefined,\n suffixMaxWidth: undefined,\n prefixFixed: false,\n suffixFixed: false,\n addon: 'segment',\n },\n)\n\nconst emit = defineEmits<GrInputEmits>()\ndefineSlots<{\n /** Аддон слева от поля: иконка, код валюты, метка. */\n prefix?: () => any\n /** Аддон справа от поля: единица измерения, подсказка. */\n suffix?: () => any\n}>()\n\n// Контекст `GrFormField` (если инпут внутри него): даёт id/aria-describedby/\n// invalid/required как fallback, чтобы не прокидывать `forId` вручную.\nconst field = useGrFormFieldContext()\n\n// Эффективные значения: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrInput' })\nconst resolvedClearable = useGrComponentProp('GrInput', 'clearable', () => props.clearable, false)\n\nconst resolvedId = computed(() => props.id ?? field?.id.value)\n\n// Счётчик обязан быть частью описания поля: иначе «12 / 60» видно глазами, но\n// не слышно — при том, что ограничение длины и есть смысл счётчика.\nconst countId = useId()\nconst describedBy = computed(() =>\n [field?.describedById.value, props.showCount ? countId : undefined]\n .filter(Boolean)\n .join(' ') || undefined,\n)\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n} = useGrFormControl(() => props)\n\nconst inputEl = ref<HTMLInputElement | null>(null)\n\nfunction focus(): void {\n inputEl.value?.focus()\n}\n\nfunction blur(): void {\n inputEl.value?.blur()\n}\n\n/** Выделить содержимое — спутник `focus()` для «подставили значение, перезапишите». */\nfunction select(): void {\n inputEl.value?.select()\n}\n\ndefineExpose({\n focus,\n blur,\n select,\n})\n\nconst ADDON_MIN_WIDTH_BY_SIZE: Record<GrInputSize, string> = {\n xs: '2rem', // w-8\n sm: '2.25rem', // w-9\n md: '2.5rem', // w-10\n lg: '3rem', // w-12\n}\n\nconst passwordVisible = ref(false)\n// Тип поля с учётом переключателя пароля.\nconst resolvedType = computed(() => (props.type === 'password' && passwordVisible.value ? 'text' : props.type))\n\nconst showPasswordToggle = computed(() => props.passwordToggle && props.type === 'password' && !isDisabled.value)\nconst showClear = computed(() => resolvedClearable.value && props.modelValue.length > 0 && !isDisabled.value && !isReadonly.value)\n\nconst trailingCount = computed(() => (showClear.value ? 1 : 0) + (showPasswordToggle.value ? 1 : 0) + (props.loading ? 1 : 0))\nconst trailingReserve = computed(() => (trailingCount.value > 0 ? `${trailingCount.value * 28}px` : '0px'))\n\nconst isInlineAddon = computed(() => props.addon === 'inline')\n\nconst prefixAddonClass = computed(() => (isInlineAddon.value ? addonInlinePrefixClass : addonSegmentPrefixClass))\nconst suffixAddonClass = computed(() => (isInlineAddon.value ? addonInlineSuffixClass : addonSegmentSuffixClass))\n\nconst {\n prefixEl,\n suffixEl,\n prefixStyle,\n suffixStyle,\n fieldPadding: inputStyle,\n} = useControlAddons(() => props, {\n // У украшения своей ширины нет — иначе иконка висела бы в пустом отсеке\n // шириной со ступень размера.\n defaultMinWidth: () => (isInlineAddon.value ? '0px' : ADDON_MIN_WIDTH_BY_SIZE[resolvedSize.value]),\n paddingX: () => paddingX[resolvedSize.value],\n trailingReserve: () => trailingReserve.value,\n})\n\n// Border/ring/disabled — на оболочке (`focus-within`), размеры/выравнивание — на инпуте.\nconst shellClass = computed(() => grInputShellClass({\n state: props.state,\n invalid: isInvalid.value,\n disabled: isDisabled.value,\n}))\n\nconst className = computed(() => grInputFieldClass({\n size: resolvedSize.value,\n align: props.textAlign,\n}))\n\nfunction onInput(e: Event): void {\n emit('update:modelValue', (e.target as HTMLInputElement).value)\n}\n\n// Объявленный emit уходит из `$attrs`, поэтому нативные события переизлучаем\n// руками — иначе `@change`/`@focus`/`@blur` у потребителя перестали бы работать.\nfunction onChange(e: Event): void {\n emit('change', (e.target as HTMLInputElement).value)\n}\n\nfunction onFocus(e: FocusEvent): void {\n emit('focus', e)\n}\n\nfunction onBlur(e: FocusEvent): void {\n emit('blur', e)\n}\n\n// ————— Trailing-контролы: очистка, переключатель пароля, счётчик символов.\nconst { t } = useGranularityTranslations()\nconst resolvedClearLabel = computed(() => props.clearLabel ?? t('gr.input.clear', 'Clear'))\nconst resolvedPasswordShowLabel = computed(() => props.passwordShowLabel ?? t('gr.input.showPassword', 'Show password'))\nconst resolvedPasswordHideLabel = computed(() => props.passwordHideLabel ?? t('gr.input.hidePassword', 'Hide password'))\n\n// Счётчик символов: `len` или `len / maxlength`.\nconst countText = computed(() =>\n props.maxlength !== undefined ? `${props.modelValue.length} / ${props.maxlength}` : String(props.modelValue.length),\n)\n\n/**\n * Живой регион объявляет только исчерпание лимита. Читать вслух каждый символ\n * нельзя — диктор захлебнётся, а сам счётчик и так связан с полем через\n * `aria-describedby` и читается при фокусе.\n */\nconst limitReached = computed(() =>\n props.maxlength !== undefined && props.modelValue.length >= props.maxlength,\n)\nconst liveMessage = computed(() =>\n limitReached.value ? t('gr.input.limitReached', 'Character limit reached') : '',\n)\n\nfunction clear(): void {\n emit('update:modelValue', '')\n emit('clear')\n inputEl.value?.focus()\n}\n\nfunction togglePassword(): void {\n passwordVisible.value = !passwordVisible.value\n inputEl.value?.focus()\n}\n</script>\n\n<template>\n <div data-gr-input class=\"w-full\">\n <div :class=\"shellClass\">\n <div\n v-if=\"$slots.prefix\"\n ref=\"prefixEl\"\n data-testid=\"gr-input-prefix\"\n class=\"absolute inset-y-0 left-0 flex items-center justify-center text-[var(--gr-muted-fg)] pointer-events-none select-none truncate\"\n :class=\"prefixAddonClass\"\n :style=\"prefixStyle\"\n aria-hidden=\"true\"\n >\n <slot name=\"prefix\" />\n </div>\n\n <input\n :id=\"resolvedId\"\n ref=\"inputEl\"\n v-bind=\"$attrs\"\n :name=\"props.name\"\n :type=\"resolvedType\"\n :inputmode=\"props.inputmode\"\n :autocomplete=\"props.autocomplete\"\n :placeholder=\"props.placeholder\"\n :disabled=\"isDisabled\"\n :readonly=\"isReadonly\"\n :maxlength=\"props.maxlength\"\n :value=\"props.modelValue\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-describedby=\"describedBy\"\n :aria-required=\"isRequired ? 'true' : undefined\"\n :aria-readonly=\"isReadonly ? 'true' : undefined\"\n :aria-label=\"ariaLabel\"\n :aria-busy=\"props.loading ? 'true' : undefined\"\n class=\"w-full bg-transparent text-[var(--gr-fg)] placeholder:text-[var(--gr-muted-fg)] focus:placeholder:text-transparent focus:outline-none disabled:cursor-not-allowed disabled:text-[var(--gr-muted-fg)]\"\n :class=\"className\"\n :style=\"inputStyle\"\n @input=\"onInput\"\n @change=\"onChange\"\n @focus=\"onFocus\"\n @blur=\"onBlur\"\n >\n\n <div\n v-if=\"trailingCount > 0\"\n data-gr-input-trailing\n class=\"absolute inset-y-0 right-1 flex items-center gap-0.5\"\n >\n <span\n v-if=\"loading\"\n data-gr-input-spinner\n class=\"flex h-6 w-6 items-center justify-center text-[var(--gr-muted-fg)]\"\n aria-hidden=\"true\"\n >\n <IconLoader class=\"h-4 w-4 animate-spin\" />\n </span>\n\n <button\n v-if=\"showClear\"\n type=\"button\"\n data-gr-input-clear\n :aria-label=\"resolvedClearLabel\"\n class=\"flex h-6 w-6 items-center justify-center rounded-[var(--gr-radius-sm)] text-[var(--gr-muted-fg)] transition-colors hover:bg-[var(--gr-muted)] hover:text-[var(--gr-fg)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)]\"\n @click=\"clear\"\n >\n <IconX class=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n\n <button\n v-if=\"showPasswordToggle\"\n type=\"button\"\n data-gr-input-password-toggle\n :aria-label=\"passwordVisible ? resolvedPasswordHideLabel : resolvedPasswordShowLabel\"\n :aria-pressed=\"passwordVisible ? 'true' : 'false'\"\n class=\"flex h-6 w-6 items-center justify-center rounded-[var(--gr-radius-sm)] text-[var(--gr-muted-fg)] transition-colors hover:bg-[var(--gr-muted)] hover:text-[var(--gr-fg)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)]\"\n @click=\"togglePassword\"\n >\n <IconEyeOff v-if=\"passwordVisible\" class=\"h-4 w-4\" aria-hidden=\"true\" />\n <IconEye v-else class=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n </div>\n\n <div\n v-if=\"$slots.suffix\"\n ref=\"suffixEl\"\n data-testid=\"gr-input-suffix\"\n class=\"absolute inset-y-0 right-0 flex items-center justify-center text-[var(--gr-muted-fg)] pointer-events-none select-none truncate\"\n :class=\"[suffixAddonClass, suffixFixed ? '[direction:rtl]' : '']\"\n :style=\"suffixStyle\"\n aria-hidden=\"true\"\n >\n <slot name=\"suffix\" />\n </div>\n </div>\n\n <div\n v-if=\"showCount\"\n :id=\"countId\"\n data-gr-input-count\n class=\"mt-1 text-right text-[length:var(--gr-control-text-xs)] leading-[var(--gr-leading-xs)] text-[var(--gr-muted-fg)] tabular-nums\"\n >\n {{ countText }}\n </div>\n\n <span\n v-if=\"showCount\"\n data-gr-input-live\n class=\"sr-only\"\n role=\"status\"\n aria-live=\"polite\"\n >{{ liveMessage }}</span>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport type { GrComponentSize } from '../shared/sizes'\nimport { computed, ref, useId } from 'vue'\n\nimport { useControlAddons } from '../../composables/internal/useControlAddons'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport type { InputHTMLAttributes } from 'vue'\n\nimport { addonInlinePrefixClass, addonInlineSuffixClass, addonSegmentPrefixClass, addonSegmentSuffixClass, grInputFieldClass, grInputShellClass, paddingX } from './grInputStyles'\n\nimport IconLoader from '~icons/lucide/loader-2'\nimport IconX from '~icons/lucide/x'\nimport IconEye from '~icons/lucide/eye'\nimport IconEyeOff from '~icons/lucide/eye-off'\n\nexport interface GrInputProps {\n /**\n * Значение поля. Необязательное: без `v-model` поле рисуется пустым.\n * Дефолт — пустая строка, а не `undefined`: длина значения читается\n * напрямую (`showClear`), и `undefined` уронил бы рендер.\n */\n modelValue?: string\n type?: 'text' | 'email' | 'password' | 'number' | 'search' | 'tel' | 'url'\n placeholder?: string\n autocomplete?: string\n inputmode?: InputHTMLAttributes['inputmode']\n disabled?: boolean\n /** Только для чтения: значение видно и выделяемо, но не редактируется. */\n readonly?: boolean\n invalid?: boolean\n /** Обязательное поле (`aria-required`). Складывается с `required` у `GrFormField`. */\n required?: boolean\n /** Доступное имя вне `GrFormField`. */\n ariaLabel?: string\n state?: 'default' | 'success' | 'warning' | 'danger'\n name?: string\n id?: string\n size?: GrInputSize\n\n /** Показывать кнопку очистки, когда есть значение (и не disabled/readonly). */\n clearable?: boolean\n /** i18n aria-label кнопки очистки. */\n clearLabel?: string\n /** Ограничение длины + основа для счётчика символов. */\n maxlength?: number\n /** Показывать счётчик символов (`len` или `len/maxlength`). */\n showCount?: boolean\n /**\n * Фоновая работа по полю (проверка занятости логина, автосохранение):\n * спиннер в trailing-области + `aria-busy`. Ввод не блокируется —\n * для этого есть `disabled`/`readonly`.\n */\n loading?: boolean\n /** Кнопка показать/скрыть пароль (только при `type=\"password\"`). */\n passwordToggle?: boolean\n /** i18n aria-label кнопки показать/скрыть пароль. */\n passwordShowLabel?: string\n passwordHideLabel?: string\n\n textAlign?: GrInputTextAlign\n\n prefixMinWidth?: string\n prefixMaxWidth?: string\n suffixMinWidth?: string\n suffixMaxWidth?: string\n /**\n * Фиксированная ширина у prefix/suffix: аддон получает жёсткую ширину\n * (из `*MaxWidth` → `*MinWidth` → дефолт), а контент обрезается по краю\n * (prefix — справа, suffix — слева). По умолчанию аддоны «растягиваются»\n * под контент (в пределах min/max), а излишек клипается оболочкой.\n */\n prefixFixed?: boolean\n suffixFixed?: boolean\n /**\n * Как выглядят аддоны `#prefix`/`#suffix`.\n *\n * `segment` (по умолчанию) — отдельный отсек, отрезанный рамкой и выровненный\n * по ступени размера: так поле с «₽» и поле с «USD» стоят в колонку.\n * `inline` — украшение внутри рамки: ни разделителя, ни своей ширины.\n *\n * Разница не косметическая. Поисковая строка с лупой в сегменте читается\n * составным элементом — полем с приклеенной кнопкой, — а не одним полем;\n * именно поэтому иконку внутри рамки нельзя было выразить аддоном, и\n * потребители отказывались от неё вовсе.\n */\n addon?: 'segment' | 'inline'\n}\n\nexport interface GrInputEmits {\n (e: 'update:modelValue', value: string): void\n /** Значение зафиксировано нативным `change` — по `blur` или `Enter`. */\n (e: 'change', value: string): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n /**\n * Значение стёрто кнопкой очистки. Отдельное событие потому, что по\n * `update:modelValue` программную очистку от ручного стирания не отличить.\n */\n (e: 'clear'): void\n}\n\ndefineOptions({\n inheritAttrs: false,\n})\n\nexport type GrInputSize = GrComponentSize\nexport type GrInputTextAlign = 'left' | 'center' | 'right'\n\nconst props = withDefaults(\n defineProps<GrInputProps>(),\n {\n modelValue: '',\n type: 'text',\n placeholder: undefined,\n autocomplete: undefined,\n inputmode: undefined,\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n ariaLabel: undefined,\n state: 'default',\n name: undefined,\n id: undefined,\n size: undefined,\n\n // Настраивается через `GrConfigProvider`; дефолт — в резолвере ниже.\n clearable: undefined,\n clearLabel: undefined,\n maxlength: undefined,\n showCount: false,\n loading: false,\n passwordToggle: false,\n passwordShowLabel: undefined,\n passwordHideLabel: undefined,\n\n textAlign: 'left',\n\n prefixMinWidth: undefined,\n prefixMaxWidth: undefined,\n suffixMinWidth: undefined,\n suffixMaxWidth: undefined,\n prefixFixed: false,\n suffixFixed: false,\n addon: 'segment',\n },\n)\n\nconst emit = defineEmits<GrInputEmits>()\ndefineSlots<{\n /** Аддон слева от поля: иконка, код валюты, метка. */\n prefix?: () => any\n /** Аддон справа от поля: единица измерения, подсказка. */\n suffix?: () => any\n}>()\n\n// Контекст `GrFormField` (если инпут внутри него): даёт id/aria-describedby/\n// invalid/required как fallback, чтобы не прокидывать `forId` вручную.\nconst field = useGrFormFieldContext()\n\n// Эффективные значения: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrInput' })\nconst resolvedClearable = useGrComponentProp('GrInput', 'clearable', () => props.clearable, false)\n\nconst resolvedId = computed(() => props.id ?? field?.id.value)\n\n// Счётчик обязан быть частью описания поля: иначе «12 / 60» видно глазами, но\n// не слышно — при том, что ограничение длины и есть смысл счётчика.\nconst countId = useId()\nconst describedBy = computed(() =>\n [field?.describedById.value, props.showCount ? countId : undefined]\n .filter(Boolean)\n .join(' ') || undefined,\n)\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n} = useGrFormControl(() => props)\n\nconst inputEl = ref<HTMLInputElement | null>(null)\n\nfunction focus(): void {\n inputEl.value?.focus()\n}\n\nfunction blur(): void {\n inputEl.value?.blur()\n}\n\n/** Выделить содержимое — спутник `focus()` для «подставили значение, перезапишите». */\nfunction select(): void {\n inputEl.value?.select()\n}\n\ndefineExpose({\n focus,\n blur,\n select,\n})\n\nconst ADDON_MIN_WIDTH_BY_SIZE: Record<GrInputSize, string> = {\n xs: '2rem', // w-8\n sm: '2.25rem', // w-9\n md: '2.5rem', // w-10\n lg: '3rem', // w-12\n}\n\nconst passwordVisible = ref(false)\n// Тип поля с учётом переключателя пароля.\nconst resolvedType = computed(() => (props.type === 'password' && passwordVisible.value ? 'text' : props.type))\n\nconst showPasswordToggle = computed(() => props.passwordToggle && props.type === 'password' && !isDisabled.value)\nconst showClear = computed(() => resolvedClearable.value && props.modelValue.length > 0 && !isDisabled.value && !isReadonly.value)\n\nconst trailingCount = computed(() => (showClear.value ? 1 : 0) + (showPasswordToggle.value ? 1 : 0) + (props.loading ? 1 : 0))\nconst trailingReserve = computed(() => (trailingCount.value > 0 ? `${trailingCount.value * 28}px` : '0px'))\n\nconst isInlineAddon = computed(() => props.addon === 'inline')\n\nconst prefixAddonClass = computed(() => (isInlineAddon.value ? addonInlinePrefixClass : addonSegmentPrefixClass))\nconst suffixAddonClass = computed(() => (isInlineAddon.value ? addonInlineSuffixClass : addonSegmentSuffixClass))\n\nconst {\n prefixEl,\n suffixEl,\n prefixStyle,\n suffixStyle,\n fieldPadding: inputStyle,\n} = useControlAddons(() => props, {\n // У украшения своей ширины нет — иначе иконка висела бы в пустом отсеке\n // шириной со ступень размера.\n defaultMinWidth: () => (isInlineAddon.value ? '0px' : ADDON_MIN_WIDTH_BY_SIZE[resolvedSize.value]),\n paddingX: () => paddingX[resolvedSize.value],\n trailingReserve: () => trailingReserve.value,\n})\n\n// Border/ring/disabled — на оболочке (`focus-within`), размеры/выравнивание — на инпуте.\nconst shellClass = computed(() => grInputShellClass({\n state: props.state,\n invalid: isInvalid.value,\n disabled: isDisabled.value,\n}))\n\nconst className = computed(() => grInputFieldClass({\n size: resolvedSize.value,\n align: props.textAlign,\n}))\n\nfunction onInput(e: Event): void {\n emit('update:modelValue', (e.target as HTMLInputElement).value)\n}\n\n// Объявленный emit уходит из `$attrs`, поэтому нативные события переизлучаем\n// руками — иначе `@change`/`@focus`/`@blur` у потребителя перестали бы работать.\nfunction onChange(e: Event): void {\n emit('change', (e.target as HTMLInputElement).value)\n}\n\nfunction onFocus(e: FocusEvent): void {\n emit('focus', e)\n}\n\nfunction onBlur(e: FocusEvent): void {\n emit('blur', e)\n}\n\n// ————— Trailing-контролы: очистка, переключатель пароля, счётчик символов.\nconst { t } = useGranularityTranslations()\nconst resolvedClearLabel = computed(() => props.clearLabel ?? t('gr.input.clear', 'Clear'))\nconst resolvedPasswordShowLabel = computed(() => props.passwordShowLabel ?? t('gr.input.showPassword', 'Show password'))\nconst resolvedPasswordHideLabel = computed(() => props.passwordHideLabel ?? t('gr.input.hidePassword', 'Hide password'))\n\n// Счётчик символов: `len` или `len / maxlength`.\nconst countText = computed(() =>\n props.maxlength !== undefined ? `${props.modelValue.length} / ${props.maxlength}` : String(props.modelValue.length),\n)\n\n/**\n * Живой регион объявляет только исчерпание лимита. Читать вслух каждый символ\n * нельзя — диктор захлебнётся, а сам счётчик и так связан с полем через\n * `aria-describedby` и читается при фокусе.\n */\nconst limitReached = computed(() =>\n props.maxlength !== undefined && props.modelValue.length >= props.maxlength,\n)\nconst liveMessage = computed(() =>\n limitReached.value ? t('gr.input.limitReached', 'Character limit reached') : '',\n)\n\nfunction clear(): void {\n emit('update:modelValue', '')\n emit('clear')\n inputEl.value?.focus()\n}\n\nfunction togglePassword(): void {\n passwordVisible.value = !passwordVisible.value\n inputEl.value?.focus()\n}\n</script>\n\n<template>\n <div data-gr-input class=\"w-full\">\n <div :class=\"shellClass\">\n <div\n v-if=\"$slots.prefix\"\n ref=\"prefixEl\"\n data-testid=\"gr-input-prefix\"\n class=\"absolute inset-y-0 left-0 flex items-center justify-center text-[var(--gr-muted-fg)] pointer-events-none select-none truncate\"\n :class=\"prefixAddonClass\"\n :style=\"prefixStyle\"\n aria-hidden=\"true\"\n >\n <slot name=\"prefix\" />\n </div>\n\n <input\n :id=\"resolvedId\"\n ref=\"inputEl\"\n v-bind=\"$attrs\"\n :name=\"props.name\"\n :type=\"resolvedType\"\n :inputmode=\"props.inputmode\"\n :autocomplete=\"props.autocomplete\"\n :placeholder=\"props.placeholder\"\n :disabled=\"isDisabled\"\n :readonly=\"isReadonly\"\n :maxlength=\"props.maxlength\"\n :value=\"props.modelValue\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-describedby=\"describedBy\"\n :aria-required=\"isRequired ? 'true' : undefined\"\n :aria-readonly=\"isReadonly ? 'true' : undefined\"\n :aria-label=\"ariaLabel\"\n :aria-busy=\"props.loading ? 'true' : undefined\"\n class=\"w-full bg-transparent text-[var(--gr-fg)] placeholder:text-[var(--gr-muted-fg)] focus:placeholder:text-transparent focus:outline-none disabled:cursor-not-allowed disabled:text-[var(--gr-muted-fg)]\"\n :class=\"className\"\n :style=\"inputStyle\"\n @input=\"onInput\"\n @change=\"onChange\"\n @focus=\"onFocus\"\n @blur=\"onBlur\"\n >\n\n <div\n v-if=\"trailingCount > 0\"\n data-gr-input-trailing\n class=\"absolute inset-y-0 right-1 flex items-center gap-0.5\"\n >\n <span\n v-if=\"loading\"\n data-gr-input-spinner\n class=\"flex h-6 w-6 items-center justify-center text-[var(--gr-muted-fg)]\"\n aria-hidden=\"true\"\n >\n <IconLoader class=\"h-4 w-4 animate-spin\" />\n </span>\n\n <button\n v-if=\"showClear\"\n type=\"button\"\n data-gr-input-clear\n :aria-label=\"resolvedClearLabel\"\n class=\"flex h-6 w-6 items-center justify-center rounded-[var(--gr-radius-sm)] text-[var(--gr-muted-fg)] transition-colors hover:bg-[var(--gr-muted)] hover:text-[var(--gr-fg)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)]\"\n @click=\"clear\"\n >\n <IconX class=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n\n <button\n v-if=\"showPasswordToggle\"\n type=\"button\"\n data-gr-input-password-toggle\n :aria-label=\"passwordVisible ? resolvedPasswordHideLabel : resolvedPasswordShowLabel\"\n :aria-pressed=\"passwordVisible ? 'true' : 'false'\"\n class=\"flex h-6 w-6 items-center justify-center rounded-[var(--gr-radius-sm)] text-[var(--gr-muted-fg)] transition-colors hover:bg-[var(--gr-muted)] hover:text-[var(--gr-fg)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)]\"\n @click=\"togglePassword\"\n >\n <IconEyeOff v-if=\"passwordVisible\" class=\"h-4 w-4\" aria-hidden=\"true\" />\n <IconEye v-else class=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n </div>\n\n <div\n v-if=\"$slots.suffix\"\n ref=\"suffixEl\"\n data-testid=\"gr-input-suffix\"\n class=\"absolute inset-y-0 right-0 flex items-center justify-center text-[var(--gr-muted-fg)] pointer-events-none select-none truncate\"\n :class=\"[suffixAddonClass, suffixFixed ? '[direction:rtl]' : '']\"\n :style=\"suffixStyle\"\n aria-hidden=\"true\"\n >\n <slot name=\"suffix\" />\n </div>\n </div>\n\n <div\n v-if=\"showCount\"\n :id=\"countId\"\n data-gr-input-count\n class=\"mt-1 text-right text-[length:var(--gr-control-text-xs)] leading-[var(--gr-leading-xs)] text-[var(--gr-muted-fg)] tabular-nums\"\n >\n {{ countText }}\n </div>\n\n <span\n v-if=\"showCount\"\n data-gr-input-live\n class=\"sr-only\"\n role=\"status\"\n aria-live=\"polite\"\n >{{ liveMessage }}</span>\n </div>\n</template>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"GrInput-CJ7eu3fR.js","names":["$slots","$attrs"],"sources":["../../../../src/components/GrInput/GrInput.vue","../../../../src/components/GrInput/GrInput.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport type { GrComponentSize } from '../shared/sizes'\nimport { computed, ref, useId } from 'vue'\n\nimport { useControlAddons } from '../../composables/internal/useControlAddons'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport type { InputHTMLAttributes } from 'vue'\n\nimport { addonInlinePrefixClass, addonInlineSuffixClass, addonSegmentPrefixClass, addonSegmentSuffixClass, grInputFieldClass, grInputShellClass, paddingX } from './grInputStyles'\n\nimport IconLoader from '~icons/lucide/loader-2'\nimport IconX from '~icons/lucide/x'\nimport IconEye from '~icons/lucide/eye'\nimport IconEyeOff from '~icons/lucide/eye-off'\n\nexport interface GrInputProps {\n /**\n * Значение поля. Необязательное: без `v-model` поле рисуется пустым.\n * Дефолт — пустая строка, а не `undefined`: длина значения читается\n * напрямую (`showClear`), и `undefined` уронил бы рендер.\n */\n modelValue?: string\n type?: 'text' | 'email' | 'password' | 'number' | 'search' | 'tel' | 'url'\n placeholder?: string\n autocomplete?: string\n inputmode?: InputHTMLAttributes['inputmode']\n disabled?: boolean\n /** Только для чтения: значение видно и выделяемо, но не редактируется. */\n readonly?: boolean\n invalid?: boolean\n /** Обязательное поле (`aria-required`). Складывается с `required` у `GrFormField`. */\n required?: boolean\n /** Доступное имя вне `GrFormField`. */\n ariaLabel?: string\n state?: 'default' | 'success' | 'warning' | 'danger'\n name?: string\n id?: string\n size?: GrInputSize\n\n /** Показывать кнопку очистки, когда есть значение (и не disabled/readonly). */\n clearable?: boolean\n /** i18n aria-label кнопки очистки. */\n clearLabel?: string\n /** Ограничение длины + основа для счётчика символов. */\n maxlength?: number\n /** Показывать счётчик символов (`len` или `len/maxlength`). */\n showCount?: boolean\n /**\n * Фоновая работа по полю (проверка занятости логина, автосохранение):\n * спиннер в trailing-области + `aria-busy`. Ввод не блокируется —\n * для этого есть `disabled`/`readonly`.\n */\n loading?: boolean\n /** Кнопка показать/скрыть пароль (только при `type=\"password\"`). */\n passwordToggle?: boolean\n /** i18n aria-label кнопки показать/скрыть пароль. */\n passwordShowLabel?: string\n passwordHideLabel?: string\n\n textAlign?: GrInputTextAlign\n\n prefixMinWidth?: string\n prefixMaxWidth?: string\n suffixMinWidth?: string\n suffixMaxWidth?: string\n /**\n * Фиксированная ширина у prefix/suffix: аддон получает жёсткую ширину\n * (из `*MaxWidth` → `*MinWidth` → дефолт), а контент обрезается по краю\n * (prefix — справа, suffix — слева). По умолчанию аддоны «растягиваются»\n * под контент (в пределах min/max), а излишек клипается оболочкой.\n */\n prefixFixed?: boolean\n suffixFixed?: boolean\n /**\n * Как выглядят аддоны `#prefix`/`#suffix`.\n *\n * `segment` (по умолчанию) — отдельный отсек, отрезанный рамкой и выровненный\n * по ступени размера: так поле с «₽» и поле с «USD» стоят в колонку.\n * `inline` — украшение внутри рамки: ни разделителя, ни своей ширины.\n *\n * Разница не косметическая. Поисковая строка с лупой в сегменте читается\n * составным элементом — полем с приклеенной кнопкой, — а не одним полем;\n * именно поэтому иконку внутри рамки нельзя было выразить аддоном, и\n * потребители отказывались от неё вовсе.\n */\n addon?: 'segment' | 'inline'\n}\n\nexport interface GrInputEmits {\n (e: 'update:modelValue', value: string): void\n /** Значение зафиксировано нативным `change` — по `blur` или `Enter`. */\n (e: 'change', value: string): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n /**\n * Значение стёрто кнопкой очистки. Отдельное событие потому, что по\n * `update:modelValue` программную очистку от ручного стирания не отличить.\n */\n (e: 'clear'): void\n}\n\ndefineOptions({\n inheritAttrs: false,\n})\n\nexport type GrInputSize = GrComponentSize\nexport type GrInputTextAlign = 'left' | 'center' | 'right'\n\nconst props = withDefaults(\n defineProps<GrInputProps>(),\n {\n modelValue: '',\n type: 'text',\n placeholder: undefined,\n autocomplete: undefined,\n inputmode: undefined,\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n ariaLabel: undefined,\n state: 'default',\n name: undefined,\n id: undefined,\n size: undefined,\n\n // Настраивается через `GrConfigProvider`; дефолт — в резолвере ниже.\n clearable: undefined,\n clearLabel: undefined,\n maxlength: undefined,\n showCount: false,\n loading: false,\n passwordToggle: false,\n passwordShowLabel: undefined,\n passwordHideLabel: undefined,\n\n textAlign: 'left',\n\n prefixMinWidth: undefined,\n prefixMaxWidth: undefined,\n suffixMinWidth: undefined,\n suffixMaxWidth: undefined,\n prefixFixed: false,\n suffixFixed: false,\n addon: 'segment',\n },\n)\n\nconst emit = defineEmits<GrInputEmits>()\ndefineSlots<{\n /** Аддон слева от поля: иконка, код валюты, метка. */\n prefix?: () => any\n /** Аддон справа от поля: единица измерения, подсказка. */\n suffix?: () => any\n}>()\n\n// Контекст `GrFormField` (если инпут внутри него): даёт id/aria-describedby/\n// invalid/required как fallback, чтобы не прокидывать `forId` вручную.\nconst field = useGrFormFieldContext()\n\n// Эффективные значения: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrInput' })\nconst resolvedClearable = useGrComponentProp('GrInput', 'clearable', () => props.clearable, false)\n\nconst resolvedId = computed(() => props.id ?? field?.id.value)\n\n// Счётчик обязан быть частью описания поля: иначе «12 / 60» видно глазами, но\n// не слышно — при том, что ограничение длины и есть смысл счётчика.\nconst countId = useId()\nconst describedBy = computed(() =>\n [field?.describedById.value, props.showCount ? countId : undefined]\n .filter(Boolean)\n .join(' ') || undefined,\n)\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n} = useGrFormControl(() => props)\n\nconst inputEl = ref<HTMLInputElement | null>(null)\n\nfunction focus(): void {\n inputEl.value?.focus()\n}\n\nfunction blur(): void {\n inputEl.value?.blur()\n}\n\n/** Выделить содержимое — спутник `focus()` для «подставили значение, перезапишите». */\nfunction select(): void {\n inputEl.value?.select()\n}\n\ndefineExpose({\n focus,\n blur,\n select,\n})\n\nconst ADDON_MIN_WIDTH_BY_SIZE: Record<GrInputSize, string> = {\n xs: '2rem', // w-8\n sm: '2.25rem', // w-9\n md: '2.5rem', // w-10\n lg: '3rem', // w-12\n}\n\nconst passwordVisible = ref(false)\n// Тип поля с учётом переключателя пароля.\nconst resolvedType = computed(() => (props.type === 'password' && passwordVisible.value ? 'text' : props.type))\n\nconst showPasswordToggle = computed(() => props.passwordToggle && props.type === 'password' && !isDisabled.value)\nconst showClear = computed(() => resolvedClearable.value && props.modelValue.length > 0 && !isDisabled.value && !isReadonly.value)\n\nconst trailingCount = computed(() => (showClear.value ? 1 : 0) + (showPasswordToggle.value ? 1 : 0) + (props.loading ? 1 : 0))\nconst trailingReserve = computed(() => (trailingCount.value > 0 ? `${trailingCount.value * 28}px` : '0px'))\n\nconst isInlineAddon = computed(() => props.addon === 'inline')\n\nconst prefixAddonClass = computed(() => (isInlineAddon.value ? addonInlinePrefixClass : addonSegmentPrefixClass))\nconst suffixAddonClass = computed(() => (isInlineAddon.value ? addonInlineSuffixClass : addonSegmentSuffixClass))\n\nconst {\n prefixEl,\n suffixEl,\n prefixStyle,\n suffixStyle,\n fieldPadding: inputStyle,\n} = useControlAddons(() => props, {\n // У украшения своей ширины нет — иначе иконка висела бы в пустом отсеке\n // шириной со ступень размера.\n defaultMinWidth: () => (isInlineAddon.value ? '0px' : ADDON_MIN_WIDTH_BY_SIZE[resolvedSize.value]),\n paddingX: () => paddingX[resolvedSize.value],\n trailingReserve: () => trailingReserve.value,\n})\n\n// Border/ring/disabled — на оболочке (`focus-within`), размеры/выравнивание — на инпуте.\nconst shellClass = computed(() => grInputShellClass({\n state: props.state,\n invalid: isInvalid.value,\n disabled: isDisabled.value,\n}))\n\nconst className = computed(() => grInputFieldClass({\n size: resolvedSize.value,\n align: props.textAlign,\n}))\n\nfunction onInput(e: Event): void {\n emit('update:modelValue', (e.target as HTMLInputElement).value)\n}\n\n// Объявленный emit уходит из `$attrs`, поэтому нативные события переизлучаем\n// руками — иначе `@change`/`@focus`/`@blur` у потребителя перестали бы работать.\nfunction onChange(e: Event): void {\n emit('change', (e.target as HTMLInputElement).value)\n}\n\nfunction onFocus(e: FocusEvent): void {\n emit('focus', e)\n}\n\nfunction onBlur(e: FocusEvent): void {\n emit('blur', e)\n}\n\n// ————— Trailing-контролы: очистка, переключатель пароля, счётчик символов.\nconst { t } = useGranularityTranslations()\nconst resolvedClearLabel = computed(() => props.clearLabel ?? t('gr.input.clear', 'Clear'))\nconst resolvedPasswordShowLabel = computed(() => props.passwordShowLabel ?? t('gr.input.showPassword', 'Show password'))\nconst resolvedPasswordHideLabel = computed(() => props.passwordHideLabel ?? t('gr.input.hidePassword', 'Hide password'))\n\n// Счётчик символов: `len` или `len / maxlength`.\nconst countText = computed(() =>\n props.maxlength !== undefined ? `${props.modelValue.length} / ${props.maxlength}` : String(props.modelValue.length),\n)\n\n/**\n * Живой регион объявляет только исчерпание лимита. Читать вслух каждый символ\n * нельзя — диктор захлебнётся, а сам счётчик и так связан с полем через\n * `aria-describedby` и читается при фокусе.\n */\nconst limitReached = computed(() =>\n props.maxlength !== undefined && props.modelValue.length >= props.maxlength,\n)\nconst liveMessage = computed(() =>\n limitReached.value ? t('gr.input.limitReached', 'Character limit reached') : '',\n)\n\nfunction clear(): void {\n emit('update:modelValue', '')\n // Очистка — такая же фиксация значения, как уход фокуса: без `change`\n // подписка «значение установилось» пропускала бы ровно её.\n emit('change', '')\n emit('clear')\n inputEl.value?.focus()\n}\n\nfunction togglePassword(): void {\n passwordVisible.value = !passwordVisible.value\n inputEl.value?.focus()\n}\n</script>\n\n<template>\n <div data-gr-input class=\"w-full\">\n <div :class=\"shellClass\">\n <div\n v-if=\"$slots.prefix\"\n ref=\"prefixEl\"\n data-testid=\"gr-input-prefix\"\n class=\"absolute inset-y-0 left-0 flex items-center justify-center text-[var(--gr-muted-fg)] pointer-events-none select-none truncate\"\n :class=\"prefixAddonClass\"\n :style=\"prefixStyle\"\n aria-hidden=\"true\"\n >\n <slot name=\"prefix\" />\n </div>\n\n <input\n :id=\"resolvedId\"\n ref=\"inputEl\"\n v-bind=\"$attrs\"\n :name=\"props.name\"\n :type=\"resolvedType\"\n :inputmode=\"props.inputmode\"\n :autocomplete=\"props.autocomplete\"\n :placeholder=\"props.placeholder\"\n :disabled=\"isDisabled\"\n :readonly=\"isReadonly\"\n :maxlength=\"props.maxlength\"\n :value=\"props.modelValue\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-describedby=\"describedBy\"\n :aria-required=\"isRequired ? 'true' : undefined\"\n :aria-readonly=\"isReadonly ? 'true' : undefined\"\n :aria-label=\"ariaLabel\"\n :aria-busy=\"props.loading ? 'true' : undefined\"\n class=\"w-full bg-transparent text-[var(--gr-fg)] placeholder:text-[var(--gr-muted-fg)] focus:placeholder:text-transparent focus:outline-none disabled:cursor-not-allowed disabled:text-[var(--gr-muted-fg)]\"\n :class=\"className\"\n :style=\"inputStyle\"\n @input=\"onInput\"\n @change=\"onChange\"\n @focus=\"onFocus\"\n @blur=\"onBlur\"\n >\n\n <div\n v-if=\"trailingCount > 0\"\n data-gr-input-trailing\n class=\"absolute inset-y-0 right-1 flex items-center gap-0.5\"\n >\n <span\n v-if=\"loading\"\n data-gr-input-spinner\n class=\"flex h-6 w-6 items-center justify-center text-[var(--gr-muted-fg)]\"\n aria-hidden=\"true\"\n >\n <IconLoader class=\"h-4 w-4 animate-spin\" />\n </span>\n\n <button\n v-if=\"showClear\"\n type=\"button\"\n data-gr-input-clear\n :aria-label=\"resolvedClearLabel\"\n class=\"flex h-6 w-6 items-center justify-center rounded-[var(--gr-radius-sm)] text-[var(--gr-muted-fg)] transition-colors hover:bg-[var(--gr-muted)] hover:text-[var(--gr-fg)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)]\"\n @click=\"clear\"\n >\n <IconX class=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n\n <button\n v-if=\"showPasswordToggle\"\n type=\"button\"\n data-gr-input-password-toggle\n :aria-label=\"passwordVisible ? resolvedPasswordHideLabel : resolvedPasswordShowLabel\"\n :aria-pressed=\"passwordVisible ? 'true' : 'false'\"\n class=\"flex h-6 w-6 items-center justify-center rounded-[var(--gr-radius-sm)] text-[var(--gr-muted-fg)] transition-colors hover:bg-[var(--gr-muted)] hover:text-[var(--gr-fg)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)]\"\n @click=\"togglePassword\"\n >\n <IconEyeOff v-if=\"passwordVisible\" class=\"h-4 w-4\" aria-hidden=\"true\" />\n <IconEye v-else class=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n </div>\n\n <div\n v-if=\"$slots.suffix\"\n ref=\"suffixEl\"\n data-testid=\"gr-input-suffix\"\n class=\"absolute inset-y-0 right-0 flex items-center justify-center text-[var(--gr-muted-fg)] pointer-events-none select-none truncate\"\n :class=\"[suffixAddonClass, suffixFixed ? '[direction:rtl]' : '']\"\n :style=\"suffixStyle\"\n aria-hidden=\"true\"\n >\n <slot name=\"suffix\" />\n </div>\n </div>\n\n <div\n v-if=\"showCount\"\n :id=\"countId\"\n data-gr-input-count\n class=\"mt-1 text-right text-[length:var(--gr-control-text-xs)] leading-[var(--gr-leading-xs)] text-[var(--gr-muted-fg)] tabular-nums\"\n >\n {{ countText }}\n </div>\n\n <span\n v-if=\"showCount\"\n data-gr-input-live\n class=\"sr-only\"\n role=\"status\"\n aria-live=\"polite\"\n >{{ liveMessage }}</span>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport type { GrComponentSize } from '../shared/sizes'\nimport { computed, ref, useId } from 'vue'\n\nimport { useControlAddons } from '../../composables/internal/useControlAddons'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport type { InputHTMLAttributes } from 'vue'\n\nimport { addonInlinePrefixClass, addonInlineSuffixClass, addonSegmentPrefixClass, addonSegmentSuffixClass, grInputFieldClass, grInputShellClass, paddingX } from './grInputStyles'\n\nimport IconLoader from '~icons/lucide/loader-2'\nimport IconX from '~icons/lucide/x'\nimport IconEye from '~icons/lucide/eye'\nimport IconEyeOff from '~icons/lucide/eye-off'\n\nexport interface GrInputProps {\n /**\n * Значение поля. Необязательное: без `v-model` поле рисуется пустым.\n * Дефолт — пустая строка, а не `undefined`: длина значения читается\n * напрямую (`showClear`), и `undefined` уронил бы рендер.\n */\n modelValue?: string\n type?: 'text' | 'email' | 'password' | 'number' | 'search' | 'tel' | 'url'\n placeholder?: string\n autocomplete?: string\n inputmode?: InputHTMLAttributes['inputmode']\n disabled?: boolean\n /** Только для чтения: значение видно и выделяемо, но не редактируется. */\n readonly?: boolean\n invalid?: boolean\n /** Обязательное поле (`aria-required`). Складывается с `required` у `GrFormField`. */\n required?: boolean\n /** Доступное имя вне `GrFormField`. */\n ariaLabel?: string\n state?: 'default' | 'success' | 'warning' | 'danger'\n name?: string\n id?: string\n size?: GrInputSize\n\n /** Показывать кнопку очистки, когда есть значение (и не disabled/readonly). */\n clearable?: boolean\n /** i18n aria-label кнопки очистки. */\n clearLabel?: string\n /** Ограничение длины + основа для счётчика символов. */\n maxlength?: number\n /** Показывать счётчик символов (`len` или `len/maxlength`). */\n showCount?: boolean\n /**\n * Фоновая работа по полю (проверка занятости логина, автосохранение):\n * спиннер в trailing-области + `aria-busy`. Ввод не блокируется —\n * для этого есть `disabled`/`readonly`.\n */\n loading?: boolean\n /** Кнопка показать/скрыть пароль (только при `type=\"password\"`). */\n passwordToggle?: boolean\n /** i18n aria-label кнопки показать/скрыть пароль. */\n passwordShowLabel?: string\n passwordHideLabel?: string\n\n textAlign?: GrInputTextAlign\n\n prefixMinWidth?: string\n prefixMaxWidth?: string\n suffixMinWidth?: string\n suffixMaxWidth?: string\n /**\n * Фиксированная ширина у prefix/suffix: аддон получает жёсткую ширину\n * (из `*MaxWidth` → `*MinWidth` → дефолт), а контент обрезается по краю\n * (prefix — справа, suffix — слева). По умолчанию аддоны «растягиваются»\n * под контент (в пределах min/max), а излишек клипается оболочкой.\n */\n prefixFixed?: boolean\n suffixFixed?: boolean\n /**\n * Как выглядят аддоны `#prefix`/`#suffix`.\n *\n * `segment` (по умолчанию) — отдельный отсек, отрезанный рамкой и выровненный\n * по ступени размера: так поле с «₽» и поле с «USD» стоят в колонку.\n * `inline` — украшение внутри рамки: ни разделителя, ни своей ширины.\n *\n * Разница не косметическая. Поисковая строка с лупой в сегменте читается\n * составным элементом — полем с приклеенной кнопкой, — а не одним полем;\n * именно поэтому иконку внутри рамки нельзя было выразить аддоном, и\n * потребители отказывались от неё вовсе.\n */\n addon?: 'segment' | 'inline'\n}\n\nexport interface GrInputEmits {\n (e: 'update:modelValue', value: string): void\n /** Значение зафиксировано нативным `change` — по `blur` или `Enter`. */\n (e: 'change', value: string): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n /**\n * Значение стёрто кнопкой очистки. Отдельное событие потому, что по\n * `update:modelValue` программную очистку от ручного стирания не отличить.\n */\n (e: 'clear'): void\n}\n\ndefineOptions({\n inheritAttrs: false,\n})\n\nexport type GrInputSize = GrComponentSize\nexport type GrInputTextAlign = 'left' | 'center' | 'right'\n\nconst props = withDefaults(\n defineProps<GrInputProps>(),\n {\n modelValue: '',\n type: 'text',\n placeholder: undefined,\n autocomplete: undefined,\n inputmode: undefined,\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n ariaLabel: undefined,\n state: 'default',\n name: undefined,\n id: undefined,\n size: undefined,\n\n // Настраивается через `GrConfigProvider`; дефолт — в резолвере ниже.\n clearable: undefined,\n clearLabel: undefined,\n maxlength: undefined,\n showCount: false,\n loading: false,\n passwordToggle: false,\n passwordShowLabel: undefined,\n passwordHideLabel: undefined,\n\n textAlign: 'left',\n\n prefixMinWidth: undefined,\n prefixMaxWidth: undefined,\n suffixMinWidth: undefined,\n suffixMaxWidth: undefined,\n prefixFixed: false,\n suffixFixed: false,\n addon: 'segment',\n },\n)\n\nconst emit = defineEmits<GrInputEmits>()\ndefineSlots<{\n /** Аддон слева от поля: иконка, код валюты, метка. */\n prefix?: () => any\n /** Аддон справа от поля: единица измерения, подсказка. */\n suffix?: () => any\n}>()\n\n// Контекст `GrFormField` (если инпут внутри него): даёт id/aria-describedby/\n// invalid/required как fallback, чтобы не прокидывать `forId` вручную.\nconst field = useGrFormFieldContext()\n\n// Эффективные значения: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrInput' })\nconst resolvedClearable = useGrComponentProp('GrInput', 'clearable', () => props.clearable, false)\n\nconst resolvedId = computed(() => props.id ?? field?.id.value)\n\n// Счётчик обязан быть частью описания поля: иначе «12 / 60» видно глазами, но\n// не слышно — при том, что ограничение длины и есть смысл счётчика.\nconst countId = useId()\nconst describedBy = computed(() =>\n [field?.describedById.value, props.showCount ? countId : undefined]\n .filter(Boolean)\n .join(' ') || undefined,\n)\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n} = useGrFormControl(() => props)\n\nconst inputEl = ref<HTMLInputElement | null>(null)\n\nfunction focus(): void {\n inputEl.value?.focus()\n}\n\nfunction blur(): void {\n inputEl.value?.blur()\n}\n\n/** Выделить содержимое — спутник `focus()` для «подставили значение, перезапишите». */\nfunction select(): void {\n inputEl.value?.select()\n}\n\ndefineExpose({\n focus,\n blur,\n select,\n})\n\nconst ADDON_MIN_WIDTH_BY_SIZE: Record<GrInputSize, string> = {\n xs: '2rem', // w-8\n sm: '2.25rem', // w-9\n md: '2.5rem', // w-10\n lg: '3rem', // w-12\n}\n\nconst passwordVisible = ref(false)\n// Тип поля с учётом переключателя пароля.\nconst resolvedType = computed(() => (props.type === 'password' && passwordVisible.value ? 'text' : props.type))\n\nconst showPasswordToggle = computed(() => props.passwordToggle && props.type === 'password' && !isDisabled.value)\nconst showClear = computed(() => resolvedClearable.value && props.modelValue.length > 0 && !isDisabled.value && !isReadonly.value)\n\nconst trailingCount = computed(() => (showClear.value ? 1 : 0) + (showPasswordToggle.value ? 1 : 0) + (props.loading ? 1 : 0))\nconst trailingReserve = computed(() => (trailingCount.value > 0 ? `${trailingCount.value * 28}px` : '0px'))\n\nconst isInlineAddon = computed(() => props.addon === 'inline')\n\nconst prefixAddonClass = computed(() => (isInlineAddon.value ? addonInlinePrefixClass : addonSegmentPrefixClass))\nconst suffixAddonClass = computed(() => (isInlineAddon.value ? addonInlineSuffixClass : addonSegmentSuffixClass))\n\nconst {\n prefixEl,\n suffixEl,\n prefixStyle,\n suffixStyle,\n fieldPadding: inputStyle,\n} = useControlAddons(() => props, {\n // У украшения своей ширины нет — иначе иконка висела бы в пустом отсеке\n // шириной со ступень размера.\n defaultMinWidth: () => (isInlineAddon.value ? '0px' : ADDON_MIN_WIDTH_BY_SIZE[resolvedSize.value]),\n paddingX: () => paddingX[resolvedSize.value],\n trailingReserve: () => trailingReserve.value,\n})\n\n// Border/ring/disabled — на оболочке (`focus-within`), размеры/выравнивание — на инпуте.\nconst shellClass = computed(() => grInputShellClass({\n state: props.state,\n invalid: isInvalid.value,\n disabled: isDisabled.value,\n}))\n\nconst className = computed(() => grInputFieldClass({\n size: resolvedSize.value,\n align: props.textAlign,\n}))\n\nfunction onInput(e: Event): void {\n emit('update:modelValue', (e.target as HTMLInputElement).value)\n}\n\n// Объявленный emit уходит из `$attrs`, поэтому нативные события переизлучаем\n// руками — иначе `@change`/`@focus`/`@blur` у потребителя перестали бы работать.\nfunction onChange(e: Event): void {\n emit('change', (e.target as HTMLInputElement).value)\n}\n\nfunction onFocus(e: FocusEvent): void {\n emit('focus', e)\n}\n\nfunction onBlur(e: FocusEvent): void {\n emit('blur', e)\n}\n\n// ————— Trailing-контролы: очистка, переключатель пароля, счётчик символов.\nconst { t } = useGranularityTranslations()\nconst resolvedClearLabel = computed(() => props.clearLabel ?? t('gr.input.clear', 'Clear'))\nconst resolvedPasswordShowLabel = computed(() => props.passwordShowLabel ?? t('gr.input.showPassword', 'Show password'))\nconst resolvedPasswordHideLabel = computed(() => props.passwordHideLabel ?? t('gr.input.hidePassword', 'Hide password'))\n\n// Счётчик символов: `len` или `len / maxlength`.\nconst countText = computed(() =>\n props.maxlength !== undefined ? `${props.modelValue.length} / ${props.maxlength}` : String(props.modelValue.length),\n)\n\n/**\n * Живой регион объявляет только исчерпание лимита. Читать вслух каждый символ\n * нельзя — диктор захлебнётся, а сам счётчик и так связан с полем через\n * `aria-describedby` и читается при фокусе.\n */\nconst limitReached = computed(() =>\n props.maxlength !== undefined && props.modelValue.length >= props.maxlength,\n)\nconst liveMessage = computed(() =>\n limitReached.value ? t('gr.input.limitReached', 'Character limit reached') : '',\n)\n\nfunction clear(): void {\n emit('update:modelValue', '')\n // Очистка — такая же фиксация значения, как уход фокуса: без `change`\n // подписка «значение установилось» пропускала бы ровно её.\n emit('change', '')\n emit('clear')\n inputEl.value?.focus()\n}\n\nfunction togglePassword(): void {\n passwordVisible.value = !passwordVisible.value\n inputEl.value?.focus()\n}\n</script>\n\n<template>\n <div data-gr-input class=\"w-full\">\n <div :class=\"shellClass\">\n <div\n v-if=\"$slots.prefix\"\n ref=\"prefixEl\"\n data-testid=\"gr-input-prefix\"\n class=\"absolute inset-y-0 left-0 flex items-center justify-center text-[var(--gr-muted-fg)] pointer-events-none select-none truncate\"\n :class=\"prefixAddonClass\"\n :style=\"prefixStyle\"\n aria-hidden=\"true\"\n >\n <slot name=\"prefix\" />\n </div>\n\n <input\n :id=\"resolvedId\"\n ref=\"inputEl\"\n v-bind=\"$attrs\"\n :name=\"props.name\"\n :type=\"resolvedType\"\n :inputmode=\"props.inputmode\"\n :autocomplete=\"props.autocomplete\"\n :placeholder=\"props.placeholder\"\n :disabled=\"isDisabled\"\n :readonly=\"isReadonly\"\n :maxlength=\"props.maxlength\"\n :value=\"props.modelValue\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-describedby=\"describedBy\"\n :aria-required=\"isRequired ? 'true' : undefined\"\n :aria-readonly=\"isReadonly ? 'true' : undefined\"\n :aria-label=\"ariaLabel\"\n :aria-busy=\"props.loading ? 'true' : undefined\"\n class=\"w-full bg-transparent text-[var(--gr-fg)] placeholder:text-[var(--gr-muted-fg)] focus:placeholder:text-transparent focus:outline-none disabled:cursor-not-allowed disabled:text-[var(--gr-muted-fg)]\"\n :class=\"className\"\n :style=\"inputStyle\"\n @input=\"onInput\"\n @change=\"onChange\"\n @focus=\"onFocus\"\n @blur=\"onBlur\"\n >\n\n <div\n v-if=\"trailingCount > 0\"\n data-gr-input-trailing\n class=\"absolute inset-y-0 right-1 flex items-center gap-0.5\"\n >\n <span\n v-if=\"loading\"\n data-gr-input-spinner\n class=\"flex h-6 w-6 items-center justify-center text-[var(--gr-muted-fg)]\"\n aria-hidden=\"true\"\n >\n <IconLoader class=\"h-4 w-4 animate-spin\" />\n </span>\n\n <button\n v-if=\"showClear\"\n type=\"button\"\n data-gr-input-clear\n :aria-label=\"resolvedClearLabel\"\n class=\"flex h-6 w-6 items-center justify-center rounded-[var(--gr-radius-sm)] text-[var(--gr-muted-fg)] transition-colors hover:bg-[var(--gr-muted)] hover:text-[var(--gr-fg)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)]\"\n @click=\"clear\"\n >\n <IconX class=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n\n <button\n v-if=\"showPasswordToggle\"\n type=\"button\"\n data-gr-input-password-toggle\n :aria-label=\"passwordVisible ? resolvedPasswordHideLabel : resolvedPasswordShowLabel\"\n :aria-pressed=\"passwordVisible ? 'true' : 'false'\"\n class=\"flex h-6 w-6 items-center justify-center rounded-[var(--gr-radius-sm)] text-[var(--gr-muted-fg)] transition-colors hover:bg-[var(--gr-muted)] hover:text-[var(--gr-fg)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)]\"\n @click=\"togglePassword\"\n >\n <IconEyeOff v-if=\"passwordVisible\" class=\"h-4 w-4\" aria-hidden=\"true\" />\n <IconEye v-else class=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n </div>\n\n <div\n v-if=\"$slots.suffix\"\n ref=\"suffixEl\"\n data-testid=\"gr-input-suffix\"\n class=\"absolute inset-y-0 right-0 flex items-center justify-center text-[var(--gr-muted-fg)] pointer-events-none select-none truncate\"\n :class=\"[suffixAddonClass, suffixFixed ? '[direction:rtl]' : '']\"\n :style=\"suffixStyle\"\n aria-hidden=\"true\"\n >\n <slot name=\"suffix\" />\n </div>\n </div>\n\n <div\n v-if=\"showCount\"\n :id=\"countId\"\n data-gr-input-count\n class=\"mt-1 text-right text-[length:var(--gr-control-text-xs)] leading-[var(--gr-leading-xs)] text-[var(--gr-muted-fg)] tabular-nums\"\n >\n {{ countText }}\n </div>\n\n <span\n v-if=\"showCount\"\n data-gr-input-live\n class=\"sr-only\"\n role=\"status\"\n aria-live=\"polite\"\n >{{ liveMessage }}</span>\n </div>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+GA,MAAM,QAAQ;EAwCd,MAAM,OAAO;EAUb,MAAM,QAAQ,sBAAsB;EAGpC,MAAM,eAAe,yBAAyB,MAAM,MAAM,EAAE,WAAW,UAAU,CAAC;EAClF,MAAM,oBAAoB,mBAAmB,WAAW,mBAAmB,MAAM,WAAW,KAAK;EAEjG,MAAM,aAAa,eAAe,MAAM,MAAM,OAAO,GAAG,KAAK;EAI7D,MAAM,UAAU,MAAM;EACtB,MAAM,cAAc,eAClB,CAAC,OAAO,cAAc,OAAO,MAAM,YAAY,UAAU,KAAA,CAAS,CAAA,CAC/D,OAAO,OAAO,CAAA,CACd,KAAK,GAAG,KAAK,KAAA,CAClB;EACA,MAAM,EACJ,UAAU,YACV,SAAS,WACT,UAAU,YACV,UAAU,eACR,uBAAuB,KAAK;EAEhC,MAAM,UAAU,IAA6B,IAAI;EAEjD,SAAS,QAAc;GACrB,QAAQ,OAAO,MAAM;EACvB;EAEA,SAAS,OAAa;GACpB,QAAQ,OAAO,KAAK;EACtB;;EAGA,SAAS,SAAe;GACtB,QAAQ,OAAO,OAAO;EACxB;EAEA,SAAa;GACX;GACA;GACA;EACF,CAAC;EAED,MAAM,0BAAuD;GAC3D,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;EACN;EAEA,MAAM,kBAAkB,IAAI,KAAK;EAEjC,MAAM,eAAe,eAAgB,MAAM,SAAS,cAAc,gBAAgB,QAAQ,SAAS,MAAM,IAAK;EAE9G,MAAM,qBAAqB,eAAe,MAAM,kBAAkB,MAAM,SAAS,cAAc,CAAC,WAAW,KAAK;EAChH,MAAM,YAAY,eAAe,kBAAkB,SAAS,MAAM,WAAW,SAAS,KAAK,CAAC,WAAW,SAAS,CAAC,WAAW,KAAK;EAEjI,MAAM,gBAAgB,gBAAgB,UAAU,QAAQ,IAAI,MAAM,mBAAmB,QAAQ,IAAI,MAAM,MAAM,UAAU,IAAI,EAAE;EAC7H,MAAM,kBAAkB,eAAgB,cAAc,QAAQ,IAAI,GAAG,cAAc,QAAQ,GAAG,MAAM,KAAM;EAE1G,MAAM,gBAAgB,eAAe,MAAM,UAAU,QAAQ;EAE7D,MAAM,mBAAmB,eAAgB,cAAc,QAAQ,yBAAyB,uBAAwB;EAChH,MAAM,mBAAmB,eAAgB,cAAc,QAAQ,yBAAyB,uBAAwB;EAEhH,MAAM,EACJ,UACA,UACA,aACA,aACA,cAAc,eACZ,uBAAuB,OAAO;GAGhC,uBAAwB,cAAc,QAAQ,QAAQ,wBAAwB,aAAa;GAC3F,gBAAgB,SAAS,aAAa;GACtC,uBAAuB,gBAAgB;EACzC,CAAC;EAGD,MAAM,aAAa,eAAe,kBAAkB;GAClD,OAAO,MAAM;GACb,SAAS,UAAU;GACnB,UAAU,WAAW;EACvB,CAAC,CAAC;EAEF,MAAM,YAAY,eAAe,kBAAkB;GACjD,MAAM,aAAa;GACnB,OAAO,MAAM;EACf,CAAC,CAAC;EAEF,SAAS,QAAQ,GAAgB;GAC/B,KAAK,qBAAsB,EAAE,OAA4B,KAAK;EAChE;EAIA,SAAS,SAAS,GAAgB;GAChC,KAAK,UAAW,EAAE,OAA4B,KAAK;EACrD;EAEA,SAAS,QAAQ,GAAqB;GACpC,KAAK,SAAS,CAAC;EACjB;EAEA,SAAS,OAAO,GAAqB;GACnC,KAAK,QAAQ,CAAC;EAChB;EAGA,MAAM,EAAE,MAAM,2BAA2B;EACzC,MAAM,qBAAqB,eAAe,MAAM,cAAc,EAAE,kBAAkB,OAAO,CAAC;EAC1F,MAAM,4BAA4B,eAAe,MAAM,qBAAqB,EAAE,yBAAyB,eAAe,CAAC;EACvH,MAAM,4BAA4B,eAAe,MAAM,qBAAqB,EAAE,yBAAyB,eAAe,CAAC;EAGvH,MAAM,YAAY,eAChB,MAAM,cAAc,KAAA,IAAY,GAAG,MAAM,WAAW,OAAO,KAAK,MAAM,cAAc,OAAO,MAAM,WAAW,MAAM,CACpH;;;;;;EAOA,MAAM,eAAe,eACnB,MAAM,cAAc,KAAA,KAAa,MAAM,WAAW,UAAU,MAAM,SACpE;EACA,MAAM,cAAc,eAClB,aAAa,QAAQ,EAAE,yBAAyB,yBAAyB,IAAI,EAC/E;EAEA,SAAS,QAAc;GACrB,KAAK,qBAAqB,EAAE;GAG5B,KAAK,UAAU,EAAE;GACjB,KAAK,OAAO;GACZ,QAAQ,OAAO,MAAM;EACvB;EAEA,SAAS,iBAAuB;GAC9B,gBAAgB,QAAQ,CAAC,gBAAgB;GACzC,QAAQ,OAAO,MAAM;EACvB;;uBAIE,mBA8GM,OA9GN,YA8GM;IA7GJ,mBA2FM,OAAA,EA3FA,OAAK,eAAE,WAAA,KAAU,EAAA,GAAA;KAEXA,KAAAA,OAAO,UAAA,UAAA,GADjB,mBAUM,OAAA;;eARE;MAAJ,KAAI;MACJ,eAAY;MACZ,OAAK,eAAA,CAAC,iIACE,iBAAA,KAAgB,CAAA;MACvB,OAAK,eAAE,MAAA,WAAA,CAAW;MACnB,eAAY;SAEd,WAAsB,KAAA,QAAA,QAAA,CAAA,GAAA,CAAA,KAAA,mBAAA,IAAA,IAAA;KAGxB,mBA0BC,SA1BD,WA0BC;MAzBI,IAAI,WAAA;eACD;MAAJ,KAAI;QACIC,KAAAA,QAAM;MACb,MAAM,MAAM;MACZ,MAAM,aAAA;MACN,WAAW,MAAM;MACjB,cAAc,MAAM;MACpB,aAAa,MAAM;MACnB,UAAU,MAAA,UAAA;MACV,UAAU,MAAA,UAAA;MACV,WAAW,MAAM;MACjB,OAAO,MAAM;MACb,gBAAc,MAAA,SAAA,IAAS,SAAY,KAAA;MACnC,oBAAkB,YAAA;MAClB,iBAAe,MAAA,UAAA,IAAU,SAAY,KAAA;MACrC,iBAAe,MAAA,UAAA,IAAU,SAAY,KAAA;MACrC,cAAY,QAAA;MACZ,aAAW,MAAM,UAAO,SAAY,KAAA;MACrC,OAAK,CAAC,wMACE,UAAA,KAAS;MAChB,OAAO,MAAA,UAAA;MACA;MACC;MACD;MACD;;KAID,cAAA,QAAa,KAAA,UAAA,GADvB,mBAqCM,OArCN,YAqCM;MA/BM,QAAA,WAAA,UAAA,GADV,mBAOO,QAPP,YAOO,CADL,YAA2C,MAAA,gBAAA,GAAA,EAA/B,OAAM,uBAAsB,CAAA,CAAA,CAAA,KAAA,mBAAA,IAAA,IAAA;MAIhC,UAAA,SAAA,UAAA,GADV,mBASS,UAAA;;OAPL,MAAK;OACL,uBAAA;OACC,cAAY,mBAAA;OACb,OAAM;OACL,SAAO;UAEV,YAA4C,MAAA,SAAA,GAAA;OAArC,OAAM;OAAU,eAAY;;MAI3B,mBAAA,SAAA,UAAA,GADV,mBAWS,UAAA;;OATL,MAAK;OACL,iCAAA;OACC,cAAY,gBAAA,QAAkB,0BAAA,QAA4B,0BAAA;OAC1D,gBAAc,gBAAA,QAAe,SAAA;OAC9B,OAAM;OACL,SAAO;UAEQ,gBAAA,SAAA,UAAA,GAAlB,YAAwE,MAAA,eAAA,GAAA;;OAArC,OAAM;OAAU,eAAY;0BAC/D,YAAqD,MAAA,WAAA,GAAA;;OAArC,OAAM;OAAU,eAAY;;;KAKtCD,KAAAA,OAAO,UAAA,UAAA,GADjB,mBAUM,OAAA;;eARE;MAAJ,KAAI;MACJ,eAAY;MACZ,OAAK,eAAA,CAAC,kIAAgI,CAC7H,iBAAA,OAAkB,QAAA,cAAW,oBAAA,EAAA,CAAA,CAAA;MACrC,OAAK,eAAE,MAAA,WAAA,CAAW;MACnB,eAAY;SAEd,WAAsB,KAAA,QAAA,QAAA,CAAA,GAAA,CAAA,KAAA,mBAAA,IAAA,IAAA;;IAKhB,QAAA,aAAA,UAAA,GADV,mBAOM,OAAA;;KALD,IAAI,MAAA,OAAA;KACL,uBAAA;KACA,OAAM;uBAEL,UAAA,KAAS,GAAA,GAAA,UAAA,KAAA,mBAAA,IAAA,IAAA;IAIJ,QAAA,aAAA,UAAA,GADV,mBAMyB,QANzB,YAMyB,gBAArB,YAAA,KAAW,GAAA,CAAA,KAAA,mBAAA,IAAA,IAAA"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { t as GrInput_default } from "./chunks/GrInput-
|
|
1
|
+
import { t as GrInput_default } from "./chunks/GrInput-CJ7eu3fR.js";
|
|
2
2
|
import { n as grInputSafelist, t as grInputConfig } from "../../chunks/config-CIkdsWUk.js";
|
|
3
3
|
export { GrInput_default as GrInput, GrInput_default as default, grInputConfig, grInputSafelist };
|
package/dist/components/GrJsonViewer/chunks/{GrJsonViewer-BCZ7lE2k.js → GrJsonViewer-BXG7DdTe.js}
RENAMED
|
@@ -4,7 +4,7 @@ import { t as check_default } from "../../../chunks/check-BZI24Qs4.js";
|
|
|
4
4
|
import { t as GrButton_default } from "../../GrButton/chunks/GrButton-7GP0zObj.js";
|
|
5
5
|
import { useAnnouncer } from "../../../composables/useAnnouncer.js";
|
|
6
6
|
import { t as copy_default } from "../../../chunks/copy-DChChcq-.js";
|
|
7
|
-
import { t as GrInput_default } from "../../GrInput/chunks/GrInput-
|
|
7
|
+
import { t as GrInput_default } from "../../GrInput/chunks/GrInput-CJ7eu3fR.js";
|
|
8
8
|
import { t as GrTree_default } from "../../GrTree/chunks/GrTree-Ci6CZfKR.js";
|
|
9
9
|
import { a as jsonViewerCopyClass, c as jsonViewerMutedClass, d as jsonViewerRowClass, f as jsonViewerSearchClass, i as jsonViewerContentClass, l as jsonViewerPunctuationClass, m as jsonViewerValueClass, o as jsonViewerGroupClass, p as jsonViewerToolbarClass, r as jsonValueClass, s as jsonViewerKeyClass, u as jsonViewerRootClass } from "../../../chunks/config-DpAYidyY2.js";
|
|
10
10
|
import { computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, normalizeClass, onBeforeUnmount, onMounted, openBlock, ref, renderSlot, resolveDynamicComponent, toDisplayString, unref, watch, withCtx, withModifiers } from "vue";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GrJsonViewer-BCZ7lE2k.js","names":[],"sources":["../../../../src/components/GrJsonViewer/jsonToNodes.ts","../../../../src/components/GrJsonViewer/GrJsonViewer.vue","../../../../src/components/GrJsonViewer/GrJsonViewer.vue"],"sourcesContent":["/**\n * Разбор произвольного значения в узлы дерева.\n *\n * Компонент показывает данные из БД и от чужих сервисов, поэтому обход обязан\n * пережить всё: цикл, `BigInt`, функцию в поле, массив на десять тысяч записей,\n * строку в миллион символов. Уронить страницу он права не имеет.\n */\n\n/** Что за значение в узле. Решает и цвет, и то, раскрывается ли узел. */\nexport type GrJsonNodeKind\n = | 'object'\n | 'array'\n | 'string'\n | 'number'\n | 'boolean'\n | 'null'\n /** Значения, которых в JSON не бывает: функция, символ, `undefined`. */\n | 'unsupported'\n /** Хвост обрезанного массива — «ещё N элементов». Своего значения не имеет. */\n | 'truncation'\n\nexport type GrJsonNode = {\n /** Читаемый адрес узла (`$.items[3].name`). Он же ключ для `GrTree`. */\n path: string\n /** Имя ключа или индекс. Показывается слева от двоеточия. */\n label: string\n kind: GrJsonNodeKind\n /** Что видно в строке. У ветки — счётчик, у листа — значение, возможно обрезанное. */\n preview: string\n /** Полное значение. Копирование берёт его, а не `preview`. */\n value: unknown\n /** Показ обрезан: копирование всё равно отдаёт целое. */\n truncated: boolean\n isLeaf: boolean\n children?: GrJsonNode[]\n}\n\nexport type GrJsonToNodesOptions = {\n /** Подпись корня. */\n rootLabel?: string\n /** Длина строки, после которой значение обрезается в показе. */\n maxStringLength?: number\n /** Сколько элементов массива разбирать до заглушки «ещё N». */\n maxArrayItems?: number\n}\n\n/** Маркер ветки, которая уже встречалась выше по этой же цепочке предков. */\nexport const CIRCULAR_MARKER = '[Circular]'\n\nconst DEFAULT_ROOT_LABEL = '$'\nconst DEFAULT_MAX_STRING_LENGTH = 200\nconst DEFAULT_MAX_ARRAY_ITEMS = 100\n\n/** Ключ, который можно дописать через точку, не сломав адрес. */\nconst PLAIN_KEY = /^[A-Za-z_$][\\w$]*$/\n\nfunction appendKey(parentPath: string, key: string): string {\n return PLAIN_KEY.test(key) ? `${parentPath}.${key}` : `${parentPath}[${JSON.stringify(key)}]`\n}\n\n/**\n * Счётчик у ветки вместо её содержимого: свёрнутый узел обязан сказать, стоит\n * ли его раскрывать. Слово не склоняется — компонент не знает языка потребителя,\n * а число само по себе читается в любом.\n */\nfunction branchPreview(kind: 'object' | 'array', count: number): string {\n return kind === 'array' ? `[${count}]` : `{${count}}`\n}\n\nfunction scalarPreview(value: unknown, maxStringLength: number): { preview: string, truncated: boolean } {\n if (typeof value === 'string') {\n if (value.length <= maxStringLength)\n return { preview: JSON.stringify(value), truncated: false }\n\n // Кавычка открывается и не закрывается намеренно: закрытая читалась бы как\n // конец строки, а строка продолжается — её просто не показывают целиком.\n return { preview: `${JSON.stringify(value.slice(0, maxStringLength))}…`, truncated: true }\n }\n\n // `BigInt` представления в JSON не имеет, и `JSON.stringify` на нём бросает.\n if (typeof value === 'bigint')\n return { preview: `${value}n`, truncated: false }\n\n if (typeof value === 'function' || typeof value === 'symbol' || value === undefined)\n return { preview: String(value), truncated: false }\n\n return { preview: JSON.stringify(value) ?? String(value), truncated: false }\n}\n\nfunction kindOf(value: unknown): GrJsonNodeKind {\n if (value === null)\n return 'null'\n if (Array.isArray(value))\n return 'array'\n\n switch (typeof value) {\n case 'string': return 'string'\n case 'number': return 'number'\n case 'bigint': return 'number'\n case 'boolean': return 'boolean'\n case 'object': return 'object'\n // `undefined`, функция и символ представления в JSON не имеют — узел\n // остаётся, чтобы ключ не исчез из показа молча.\n case 'undefined':\n case 'function':\n case 'symbol': return 'unsupported'\n }\n\n // Недостижимо: ветки выше покрывают `typeof` целиком. Оставлено ради вывода\n // типов — без него TS не считает функцию всегда возвращающей.\n return 'unsupported'\n}\n\ntype BuildContext = {\n maxStringLength: number\n maxArrayItems: number\n /**\n * Цепочка предков текущего узла, а не все встреченные объекты.\n *\n * Разница принципиальна и отличает обход дерева от `replacer` у\n * `JSON.stringify`: тому стек недоступен, поэтому он метит `[Circular]` любую\n * повторную ссылку — в том числе объект, честно положенный в данные дважды.\n * Здесь стек есть, и маркер достаётся только настоящему циклу.\n */\n ancestors: Set<object>\n}\n\nfunction buildNode(label: string, path: string, value: unknown, context: BuildContext): GrJsonNode {\n const kind = kindOf(value)\n\n if (kind !== 'object' && kind !== 'array') {\n const { preview, truncated } = scalarPreview(value, context.maxStringLength)\n\n return { path, label, kind, preview, value, truncated, isLeaf: true }\n }\n\n const container = value as object\n\n if (context.ancestors.has(container)) {\n return { path, label, kind, preview: CIRCULAR_MARKER, value, truncated: false, isLeaf: true }\n }\n\n const entries = Array.isArray(value)\n ? value.map((item, index) => [String(index), item] as const)\n : Object.entries(container)\n\n // Пустой объект и пустой массив — листья: раскрывать нечего, а ветка без\n // детей рисует стрелку, которая ничего не делает.\n if (entries.length === 0) {\n return {\n path,\n label,\n kind,\n preview: Array.isArray(value) ? '[]' : '{}',\n value,\n truncated: false,\n isLeaf: true,\n }\n }\n\n context.ancestors.add(container)\n\n const limit = Math.max(0, context.maxArrayItems)\n const isOverLimit = Array.isArray(value) && entries.length > limit\n const visible = isOverLimit ? entries.slice(0, limit) : entries\n\n const children: GrJsonNode[] = visible.map(([key, item]) => buildNode(\n key,\n Array.isArray(value) ? `${path}[${key}]` : appendKey(path, key),\n item,\n context,\n ))\n\n if (isOverLimit) {\n const rest = entries.length - limit\n\n children.push({\n path: `${path}[…]`,\n label: '…',\n kind: 'truncation',\n preview: String(rest),\n value: (value as unknown[]).slice(limit),\n truncated: true,\n isLeaf: true,\n })\n }\n\n context.ancestors.delete(container)\n\n return {\n path,\n label,\n kind,\n preview: branchPreview(kind, entries.length),\n value,\n truncated: isOverLimit,\n isLeaf: false,\n children,\n }\n}\n\n/**\n * Значение → узлы дерева. Корень всегда один: даже скаляр на входе — это узел,\n * иначе дерево пришлось бы то показывать, то нет.\n */\nexport function jsonToNodes(value: unknown, options: GrJsonToNodesOptions = {}): GrJsonNode[] {\n const context: BuildContext = {\n maxStringLength: options.maxStringLength ?? DEFAULT_MAX_STRING_LENGTH,\n maxArrayItems: options.maxArrayItems ?? DEFAULT_MAX_ARRAY_ITEMS,\n ancestors: new Set(),\n }\n\n return [buildNode(options.rootLabel ?? DEFAULT_ROOT_LABEL, DEFAULT_ROOT_LABEL, value, context)]\n}\n\n/** Пути всех узлов до заданной глубины — ими задаётся начальная свёртка. */\nexport function pathsToDepth(nodes: GrJsonNode[], depth: number, level = 0): string[] {\n if (level >= depth)\n return []\n\n return nodes.flatMap(node => (\n node.children ? [node.path, ...pathsToDepth(node.children, depth, level + 1)] : []\n ))\n}\n\n/** Пути всех веток — для «раскрыть всё». */\nexport function branchPaths(nodes: GrJsonNode[]): string[] {\n return nodes.flatMap(node => (node.children ? [node.path, ...branchPaths(node.children)] : []))\n}\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'\n\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrInput from '../GrInput/GrInput.vue'\nimport GrTree from '../GrTree/GrTree.vue'\nimport { useAnnouncer } from '../../composables/useAnnouncer'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\n\nimport IconCheck from '~icons/lucide/check'\nimport IconCopy from '~icons/lucide/copy'\n\nimport { branchPaths, jsonToNodes, pathsToDepth, type GrJsonNode } from './jsonToNodes'\nimport {\n jsonValueClass,\n jsonViewerContentClass,\n jsonViewerCopyClass,\n jsonViewerGroupClass,\n jsonViewerKeyClass,\n jsonViewerMutedClass,\n jsonViewerPunctuationClass,\n jsonViewerRootClass,\n jsonViewerRowClass,\n jsonViewerSearchClass,\n jsonViewerToolbarClass,\n jsonViewerValueClass,\n} from './grJsonViewerStyles'\n\nimport type { GrComponentSize } from '../shared/sizes'\n\nexport type { GrJsonNode, GrJsonNodeKind } from './jsonToNodes'\n\n/**\n * JSON со свёрткой по узлам, поиском по ключу и значению и копированием узла.\n *\n * Дерево, клавиатуру и виртуализацию несёт `GrTree`; здесь — разбор `unknown` в\n * узлы и разметка строки. Плоский текст целиком, который копируют в тикет, —\n * `GrCodeBlock`.\n */\nexport interface GrJsonViewerProps {\n /** Показываемое значение. `unknown`, потому что приходит из БД или чужого сервиса. */\n value: unknown\n /** Подпись корня. По умолчанию `$` — так адресуют путь JSONPath и dev-tools. */\n rootLabel?: string\n /** Сколько уровней раскрыто изначально. */\n defaultExpandDepth?: number\n /**\n * Длина строкового значения, после которой показ обрезается.\n *\n * Не косметика: запрос к модели с картинкой в base64 — это один лист на\n * сотни тысяч символов, и ни свёртка, ни виртуализация по строкам его не\n * берут. Копирование при этом отдаёт значение целиком.\n */\n maxStringLength?: number\n /** Сколько элементов массива разбирать до заглушки «ещё N». */\n maxArrayItems?: number\n /** Поле поиска над деревом. Выключено — поиск остаётся на `filter()` из `defineExpose`. */\n searchable?: boolean\n /** Кнопка копирования узла в строке. */\n copyable?: boolean\n /** Оставлять в DOM только окно вокруг вьюпорта. Требует `maxHeight`. */\n virtual?: boolean\n /** Высота области просмотра: число — пиксели, строка — как есть. */\n maxHeight?: string | number\n /** Имя области для скринридера. Безымянное дерево объявляется просто «дерево». */\n ariaLabel?: string\n size?: GrComponentSize\n}\n\nexport interface GrJsonViewerEmits {\n /** Узел скопирован: путь для тикета и полное значение. */\n (e: 'copy', payload: { path: string, value: unknown }): void\n}\n\nconst props = withDefaults(defineProps<GrJsonViewerProps>(), {\n // Дефолты живут в резолверах ниже: Vue подставил бы свой раньше, чем\n // компонент заглянет в `GrConfigProvider`.\n rootLabel: undefined,\n defaultExpandDepth: undefined,\n maxStringLength: undefined,\n maxArrayItems: undefined,\n searchable: undefined,\n copyable: undefined,\n virtual: false,\n maxHeight: undefined,\n ariaLabel: undefined,\n size: undefined,\n})\n\nconst emit = defineEmits<GrJsonViewerEmits>()\n\ndefineSlots<{\n /** Значение листа: ссылка, дата, денежная сумма — оформление знает приложение. */\n leaf?: (props: { node: GrJsonNode }) => unknown\n}>()\n\nconst { t } = useGranularityTranslations()\nconst { announce } = useAnnouncer()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrJsonViewer' })\nconst resolvedDepth = useGrComponentProp('GrJsonViewer', 'defaultExpandDepth', () => props.defaultExpandDepth, 1)\nconst resolvedMaxString = useGrComponentProp('GrJsonViewer', 'maxStringLength', () => props.maxStringLength, 200)\nconst resolvedMaxArray = useGrComponentProp('GrJsonViewer', 'maxArrayItems', () => props.maxArrayItems, 100)\nconst resolvedSearchable = useGrComponentProp('GrJsonViewer', 'searchable', () => props.searchable, true)\nconst resolvedCopyable = useGrComponentProp('GrJsonViewer', 'copyable', () => props.copyable, true)\n\nconst nodes = computed(() => jsonToNodes(props.value, {\n rootLabel: props.rootLabel,\n maxStringLength: resolvedMaxString.value,\n maxArrayItems: resolvedMaxArray.value,\n}))\n\n/**\n * Раскрытие ведётся подстановкой набора путей.\n *\n * Контролируемого `expandedKeys` у `GrTree` нет, но `defaultExpandedKeys` —\n * не «начальное значение»: дерево следит за пропом и заменяет набор целиком.\n * Этого хватает и глубине по умолчанию, и кнопкам «раскрыть/свернуть всё»;\n * ручное раскрытие пользователя при этом сбрасывается — от этих кнопок ровно\n * того и ждут.\n */\nconst expandedPaths = ref<string[]>([])\n\nwatch(\n [nodes, resolvedDepth],\n ([list, depth]) => { expandedPaths.value = pathsToDepth(list, depth) },\n { immediate: true },\n)\n\nfunction expandAll(): void {\n expandedPaths.value = branchPaths(nodes.value)\n}\n\nfunction collapseAll(): void {\n expandedPaths.value = []\n}\n\nconst query = ref('')\nconst treeRef = ref<{ filter: (value: string) => void } | null>(null)\n\nwatch(query, (value) => {\n treeRef.value?.filter(value)\n})\n\nfunction filter(value: string): void {\n query.value = value\n}\n\n/** Поиск идёт и по ключу, и по значению: в чужом ответе ищут то одно, то другое. */\nfunction matchKeyOrValue(value: string, data: GrJsonNode): boolean {\n if (!value)\n return true\n\n const needle = value.toLowerCase()\n\n return data.label.toLowerCase().includes(needle) || data.preview.toLowerCase().includes(needle)\n}\n\n/**\n * Наличие буфера уточняется после монтирования: `navigator` в теле `setup` либо\n * роняет серверный рендер, либо расходится с ним. До этого момента кнопки нет —\n * ровно то же, что отдаёт сервер, поэтому гидрация совпадает.\n */\nconst canCopy = ref(false)\n\nonMounted(() => {\n canCopy.value = typeof navigator !== 'undefined' && typeof navigator.clipboard?.writeText === 'function'\n\n if (props.virtual && props.maxHeight === undefined && __GR_DEV__) {\n console.warn('[GrJsonViewer] `virtual` без `maxHeight` не включает виртуализацию: окно считается от высоты области.')\n }\n})\n\nconst showCopy = computed(() => resolvedCopyable.value && canCopy.value)\n\nconst copiedPath = ref<string | null>(null)\nlet copiedTimer: number | null = null\n\nfunction resetCopiedTimer(): void {\n if (copiedTimer !== null) {\n clearTimeout(copiedTimer)\n copiedTimer = null\n }\n}\n\nonBeforeUnmount(resetCopiedTimer)\n\n/**\n * В буфер уходит **полное** значение узла, а не то, что видно: обрезка\n * принадлежит показу, и вставить её обратно нельзя.\n */\nasync function copyNode(node: GrJsonNode): Promise<void> {\n const text = typeof node.value === 'string' ? node.value : JSON.stringify(node.value, null, 2) ?? String(node.value)\n\n try {\n await navigator.clipboard.writeText(text)\n }\n catch {\n announce(t('gr.jsonViewer.copyFailed', 'Could not copy value'))\n return\n }\n\n copiedPath.value = node.path\n resetCopiedTimer()\n copiedTimer = window.setTimeout(() => {\n copiedPath.value = null\n copiedTimer = null\n }, 1200)\n\n announce(t('gr.jsonViewer.copied', 'Value copied'))\n emit('copy', { path: node.path, value: node.value })\n}\n\ndefineExpose({ filter, expandAll, collapseAll })\n</script>\n\n<template>\n <div data-gr-json-viewer :class=\"jsonViewerRootClass\">\n <div v-if=\"resolvedSearchable\" data-gr-json-viewer-toolbar :class=\"jsonViewerToolbarClass\">\n <GrInput\n v-model=\"query\"\n :class=\"jsonViewerSearchClass\"\n :size=\"resolvedSize\"\n clearable\n :placeholder=\"t('gr.jsonViewer.search', 'Search key or value')\"\n :aria-label=\"t('gr.jsonViewer.search', 'Search key or value')\"\n />\n\n <GrButton variant=\"ghost\" :size=\"resolvedSize\" @click=\"expandAll\">\n {{ t('gr.jsonViewer.expandAll', 'Expand all') }}\n </GrButton>\n <GrButton variant=\"ghost\" :size=\"resolvedSize\" @click=\"collapseAll\">\n {{ t('gr.jsonViewer.collapseAll', 'Collapse all') }}\n </GrButton>\n </div>\n\n <GrTree\n ref=\"treeRef\"\n :data=\"nodes\"\n node-key=\"path\"\n :props=\"{ children: 'children', label: 'label', isLeaf: 'isLeaf' }\"\n :default-expanded-keys=\"expandedPaths\"\n :filter-node-method=\"matchKeyOrValue\"\n :content-class=\"jsonViewerContentClass\"\n :virtual=\"virtual\"\n :max-height=\"maxHeight\"\n :size=\"resolvedSize\"\n :aria-label=\"ariaLabel\"\n >\n <template #default=\"{ data }\">\n <span data-gr-json-viewer-row :class=\"[jsonViewerRowClass, jsonViewerGroupClass]\">\n <span data-gr-json-viewer-key :class=\"jsonViewerKeyClass\">{{ data.label }}</span>\n <span :class=\"jsonViewerPunctuationClass\">:</span>\n\n <span\n data-gr-json-viewer-value\n :class=\"[jsonViewerValueClass, jsonValueClass[(data as GrJsonNode).kind]]\"\n >\n <slot name=\"leaf\" :node=\"data as GrJsonNode\">{{ (data as GrJsonNode).preview }}</slot>\n </span>\n\n <span\n v-if=\"(data as GrJsonNode).kind === 'truncation'\"\n :class=\"jsonViewerMutedClass\"\n >{{ t('gr.jsonViewer.moreItems', 'more') }}</span>\n\n <GrButton\n v-if=\"showCopy && (data as GrJsonNode).kind !== 'truncation'\"\n data-gr-json-viewer-copy\n variant=\"ghost\"\n size=\"xs\"\n square\n :class=\"jsonViewerCopyClass\"\n :aria-label=\"t('gr.jsonViewer.copy', 'Copy value')\"\n :title=\"t('gr.jsonViewer.copy', 'Copy value')\"\n @click.stop=\"copyNode(data as GrJsonNode)\"\n >\n <component\n :is=\"copiedPath === (data as GrJsonNode).path ? IconCheck : IconCopy\"\n class=\"h-3.5 w-3.5\"\n aria-hidden=\"true\"\n />\n </GrButton>\n </span>\n </template>\n </GrTree>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'\n\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrInput from '../GrInput/GrInput.vue'\nimport GrTree from '../GrTree/GrTree.vue'\nimport { useAnnouncer } from '../../composables/useAnnouncer'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\n\nimport IconCheck from '~icons/lucide/check'\nimport IconCopy from '~icons/lucide/copy'\n\nimport { branchPaths, jsonToNodes, pathsToDepth, type GrJsonNode } from './jsonToNodes'\nimport {\n jsonValueClass,\n jsonViewerContentClass,\n jsonViewerCopyClass,\n jsonViewerGroupClass,\n jsonViewerKeyClass,\n jsonViewerMutedClass,\n jsonViewerPunctuationClass,\n jsonViewerRootClass,\n jsonViewerRowClass,\n jsonViewerSearchClass,\n jsonViewerToolbarClass,\n jsonViewerValueClass,\n} from './grJsonViewerStyles'\n\nimport type { GrComponentSize } from '../shared/sizes'\n\nexport type { GrJsonNode, GrJsonNodeKind } from './jsonToNodes'\n\n/**\n * JSON со свёрткой по узлам, поиском по ключу и значению и копированием узла.\n *\n * Дерево, клавиатуру и виртуализацию несёт `GrTree`; здесь — разбор `unknown` в\n * узлы и разметка строки. Плоский текст целиком, который копируют в тикет, —\n * `GrCodeBlock`.\n */\nexport interface GrJsonViewerProps {\n /** Показываемое значение. `unknown`, потому что приходит из БД или чужого сервиса. */\n value: unknown\n /** Подпись корня. По умолчанию `$` — так адресуют путь JSONPath и dev-tools. */\n rootLabel?: string\n /** Сколько уровней раскрыто изначально. */\n defaultExpandDepth?: number\n /**\n * Длина строкового значения, после которой показ обрезается.\n *\n * Не косметика: запрос к модели с картинкой в base64 — это один лист на\n * сотни тысяч символов, и ни свёртка, ни виртуализация по строкам его не\n * берут. Копирование при этом отдаёт значение целиком.\n */\n maxStringLength?: number\n /** Сколько элементов массива разбирать до заглушки «ещё N». */\n maxArrayItems?: number\n /** Поле поиска над деревом. Выключено — поиск остаётся на `filter()` из `defineExpose`. */\n searchable?: boolean\n /** Кнопка копирования узла в строке. */\n copyable?: boolean\n /** Оставлять в DOM только окно вокруг вьюпорта. Требует `maxHeight`. */\n virtual?: boolean\n /** Высота области просмотра: число — пиксели, строка — как есть. */\n maxHeight?: string | number\n /** Имя области для скринридера. Безымянное дерево объявляется просто «дерево». */\n ariaLabel?: string\n size?: GrComponentSize\n}\n\nexport interface GrJsonViewerEmits {\n /** Узел скопирован: путь для тикета и полное значение. */\n (e: 'copy', payload: { path: string, value: unknown }): void\n}\n\nconst props = withDefaults(defineProps<GrJsonViewerProps>(), {\n // Дефолты живут в резолверах ниже: Vue подставил бы свой раньше, чем\n // компонент заглянет в `GrConfigProvider`.\n rootLabel: undefined,\n defaultExpandDepth: undefined,\n maxStringLength: undefined,\n maxArrayItems: undefined,\n searchable: undefined,\n copyable: undefined,\n virtual: false,\n maxHeight: undefined,\n ariaLabel: undefined,\n size: undefined,\n})\n\nconst emit = defineEmits<GrJsonViewerEmits>()\n\ndefineSlots<{\n /** Значение листа: ссылка, дата, денежная сумма — оформление знает приложение. */\n leaf?: (props: { node: GrJsonNode }) => unknown\n}>()\n\nconst { t } = useGranularityTranslations()\nconst { announce } = useAnnouncer()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrJsonViewer' })\nconst resolvedDepth = useGrComponentProp('GrJsonViewer', 'defaultExpandDepth', () => props.defaultExpandDepth, 1)\nconst resolvedMaxString = useGrComponentProp('GrJsonViewer', 'maxStringLength', () => props.maxStringLength, 200)\nconst resolvedMaxArray = useGrComponentProp('GrJsonViewer', 'maxArrayItems', () => props.maxArrayItems, 100)\nconst resolvedSearchable = useGrComponentProp('GrJsonViewer', 'searchable', () => props.searchable, true)\nconst resolvedCopyable = useGrComponentProp('GrJsonViewer', 'copyable', () => props.copyable, true)\n\nconst nodes = computed(() => jsonToNodes(props.value, {\n rootLabel: props.rootLabel,\n maxStringLength: resolvedMaxString.value,\n maxArrayItems: resolvedMaxArray.value,\n}))\n\n/**\n * Раскрытие ведётся подстановкой набора путей.\n *\n * Контролируемого `expandedKeys` у `GrTree` нет, но `defaultExpandedKeys` —\n * не «начальное значение»: дерево следит за пропом и заменяет набор целиком.\n * Этого хватает и глубине по умолчанию, и кнопкам «раскрыть/свернуть всё»;\n * ручное раскрытие пользователя при этом сбрасывается — от этих кнопок ровно\n * того и ждут.\n */\nconst expandedPaths = ref<string[]>([])\n\nwatch(\n [nodes, resolvedDepth],\n ([list, depth]) => { expandedPaths.value = pathsToDepth(list, depth) },\n { immediate: true },\n)\n\nfunction expandAll(): void {\n expandedPaths.value = branchPaths(nodes.value)\n}\n\nfunction collapseAll(): void {\n expandedPaths.value = []\n}\n\nconst query = ref('')\nconst treeRef = ref<{ filter: (value: string) => void } | null>(null)\n\nwatch(query, (value) => {\n treeRef.value?.filter(value)\n})\n\nfunction filter(value: string): void {\n query.value = value\n}\n\n/** Поиск идёт и по ключу, и по значению: в чужом ответе ищут то одно, то другое. */\nfunction matchKeyOrValue(value: string, data: GrJsonNode): boolean {\n if (!value)\n return true\n\n const needle = value.toLowerCase()\n\n return data.label.toLowerCase().includes(needle) || data.preview.toLowerCase().includes(needle)\n}\n\n/**\n * Наличие буфера уточняется после монтирования: `navigator` в теле `setup` либо\n * роняет серверный рендер, либо расходится с ним. До этого момента кнопки нет —\n * ровно то же, что отдаёт сервер, поэтому гидрация совпадает.\n */\nconst canCopy = ref(false)\n\nonMounted(() => {\n canCopy.value = typeof navigator !== 'undefined' && typeof navigator.clipboard?.writeText === 'function'\n\n if (props.virtual && props.maxHeight === undefined && __GR_DEV__) {\n console.warn('[GrJsonViewer] `virtual` без `maxHeight` не включает виртуализацию: окно считается от высоты области.')\n }\n})\n\nconst showCopy = computed(() => resolvedCopyable.value && canCopy.value)\n\nconst copiedPath = ref<string | null>(null)\nlet copiedTimer: number | null = null\n\nfunction resetCopiedTimer(): void {\n if (copiedTimer !== null) {\n clearTimeout(copiedTimer)\n copiedTimer = null\n }\n}\n\nonBeforeUnmount(resetCopiedTimer)\n\n/**\n * В буфер уходит **полное** значение узла, а не то, что видно: обрезка\n * принадлежит показу, и вставить её обратно нельзя.\n */\nasync function copyNode(node: GrJsonNode): Promise<void> {\n const text = typeof node.value === 'string' ? node.value : JSON.stringify(node.value, null, 2) ?? String(node.value)\n\n try {\n await navigator.clipboard.writeText(text)\n }\n catch {\n announce(t('gr.jsonViewer.copyFailed', 'Could not copy value'))\n return\n }\n\n copiedPath.value = node.path\n resetCopiedTimer()\n copiedTimer = window.setTimeout(() => {\n copiedPath.value = null\n copiedTimer = null\n }, 1200)\n\n announce(t('gr.jsonViewer.copied', 'Value copied'))\n emit('copy', { path: node.path, value: node.value })\n}\n\ndefineExpose({ filter, expandAll, collapseAll })\n</script>\n\n<template>\n <div data-gr-json-viewer :class=\"jsonViewerRootClass\">\n <div v-if=\"resolvedSearchable\" data-gr-json-viewer-toolbar :class=\"jsonViewerToolbarClass\">\n <GrInput\n v-model=\"query\"\n :class=\"jsonViewerSearchClass\"\n :size=\"resolvedSize\"\n clearable\n :placeholder=\"t('gr.jsonViewer.search', 'Search key or value')\"\n :aria-label=\"t('gr.jsonViewer.search', 'Search key or value')\"\n />\n\n <GrButton variant=\"ghost\" :size=\"resolvedSize\" @click=\"expandAll\">\n {{ t('gr.jsonViewer.expandAll', 'Expand all') }}\n </GrButton>\n <GrButton variant=\"ghost\" :size=\"resolvedSize\" @click=\"collapseAll\">\n {{ t('gr.jsonViewer.collapseAll', 'Collapse all') }}\n </GrButton>\n </div>\n\n <GrTree\n ref=\"treeRef\"\n :data=\"nodes\"\n node-key=\"path\"\n :props=\"{ children: 'children', label: 'label', isLeaf: 'isLeaf' }\"\n :default-expanded-keys=\"expandedPaths\"\n :filter-node-method=\"matchKeyOrValue\"\n :content-class=\"jsonViewerContentClass\"\n :virtual=\"virtual\"\n :max-height=\"maxHeight\"\n :size=\"resolvedSize\"\n :aria-label=\"ariaLabel\"\n >\n <template #default=\"{ data }\">\n <span data-gr-json-viewer-row :class=\"[jsonViewerRowClass, jsonViewerGroupClass]\">\n <span data-gr-json-viewer-key :class=\"jsonViewerKeyClass\">{{ data.label }}</span>\n <span :class=\"jsonViewerPunctuationClass\">:</span>\n\n <span\n data-gr-json-viewer-value\n :class=\"[jsonViewerValueClass, jsonValueClass[(data as GrJsonNode).kind]]\"\n >\n <slot name=\"leaf\" :node=\"data as GrJsonNode\">{{ (data as GrJsonNode).preview }}</slot>\n </span>\n\n <span\n v-if=\"(data as GrJsonNode).kind === 'truncation'\"\n :class=\"jsonViewerMutedClass\"\n >{{ t('gr.jsonViewer.moreItems', 'more') }}</span>\n\n <GrButton\n v-if=\"showCopy && (data as GrJsonNode).kind !== 'truncation'\"\n data-gr-json-viewer-copy\n variant=\"ghost\"\n size=\"xs\"\n square\n :class=\"jsonViewerCopyClass\"\n :aria-label=\"t('gr.jsonViewer.copy', 'Copy value')\"\n :title=\"t('gr.jsonViewer.copy', 'Copy value')\"\n @click.stop=\"copyNode(data as GrJsonNode)\"\n >\n <component\n :is=\"copiedPath === (data as GrJsonNode).path ? IconCheck : IconCopy\"\n class=\"h-3.5 w-3.5\"\n aria-hidden=\"true\"\n />\n </GrButton>\n </span>\n </template>\n </GrTree>\n </div>\n</template>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"GrJsonViewer-BXG7DdTe.js","names":[],"sources":["../../../../src/components/GrJsonViewer/jsonToNodes.ts","../../../../src/components/GrJsonViewer/GrJsonViewer.vue","../../../../src/components/GrJsonViewer/GrJsonViewer.vue"],"sourcesContent":["/**\n * Разбор произвольного значения в узлы дерева.\n *\n * Компонент показывает данные из БД и от чужих сервисов, поэтому обход обязан\n * пережить всё: цикл, `BigInt`, функцию в поле, массив на десять тысяч записей,\n * строку в миллион символов. Уронить страницу он права не имеет.\n */\n\n/** Что за значение в узле. Решает и цвет, и то, раскрывается ли узел. */\nexport type GrJsonNodeKind\n = | 'object'\n | 'array'\n | 'string'\n | 'number'\n | 'boolean'\n | 'null'\n /** Значения, которых в JSON не бывает: функция, символ, `undefined`. */\n | 'unsupported'\n /** Хвост обрезанного массива — «ещё N элементов». Своего значения не имеет. */\n | 'truncation'\n\nexport type GrJsonNode = {\n /** Читаемый адрес узла (`$.items[3].name`). Он же ключ для `GrTree`. */\n path: string\n /** Имя ключа или индекс. Показывается слева от двоеточия. */\n label: string\n kind: GrJsonNodeKind\n /** Что видно в строке. У ветки — счётчик, у листа — значение, возможно обрезанное. */\n preview: string\n /** Полное значение. Копирование берёт его, а не `preview`. */\n value: unknown\n /** Показ обрезан: копирование всё равно отдаёт целое. */\n truncated: boolean\n isLeaf: boolean\n children?: GrJsonNode[]\n}\n\nexport type GrJsonToNodesOptions = {\n /** Подпись корня. */\n rootLabel?: string\n /** Длина строки, после которой значение обрезается в показе. */\n maxStringLength?: number\n /** Сколько элементов массива разбирать до заглушки «ещё N». */\n maxArrayItems?: number\n}\n\n/** Маркер ветки, которая уже встречалась выше по этой же цепочке предков. */\nexport const CIRCULAR_MARKER = '[Circular]'\n\nconst DEFAULT_ROOT_LABEL = '$'\nconst DEFAULT_MAX_STRING_LENGTH = 200\nconst DEFAULT_MAX_ARRAY_ITEMS = 100\n\n/** Ключ, который можно дописать через точку, не сломав адрес. */\nconst PLAIN_KEY = /^[A-Za-z_$][\\w$]*$/\n\nfunction appendKey(parentPath: string, key: string): string {\n return PLAIN_KEY.test(key) ? `${parentPath}.${key}` : `${parentPath}[${JSON.stringify(key)}]`\n}\n\n/**\n * Счётчик у ветки вместо её содержимого: свёрнутый узел обязан сказать, стоит\n * ли его раскрывать. Слово не склоняется — компонент не знает языка потребителя,\n * а число само по себе читается в любом.\n */\nfunction branchPreview(kind: 'object' | 'array', count: number): string {\n return kind === 'array' ? `[${count}]` : `{${count}}`\n}\n\nfunction scalarPreview(value: unknown, maxStringLength: number): { preview: string, truncated: boolean } {\n if (typeof value === 'string') {\n if (value.length <= maxStringLength)\n return { preview: JSON.stringify(value), truncated: false }\n\n // Кавычка открывается и не закрывается намеренно: закрытая читалась бы как\n // конец строки, а строка продолжается — её просто не показывают целиком.\n return { preview: `${JSON.stringify(value.slice(0, maxStringLength))}…`, truncated: true }\n }\n\n // `BigInt` представления в JSON не имеет, и `JSON.stringify` на нём бросает.\n if (typeof value === 'bigint')\n return { preview: `${value}n`, truncated: false }\n\n if (typeof value === 'function' || typeof value === 'symbol' || value === undefined)\n return { preview: String(value), truncated: false }\n\n return { preview: JSON.stringify(value) ?? String(value), truncated: false }\n}\n\nfunction kindOf(value: unknown): GrJsonNodeKind {\n if (value === null)\n return 'null'\n if (Array.isArray(value))\n return 'array'\n\n switch (typeof value) {\n case 'string': return 'string'\n case 'number': return 'number'\n case 'bigint': return 'number'\n case 'boolean': return 'boolean'\n case 'object': return 'object'\n // `undefined`, функция и символ представления в JSON не имеют — узел\n // остаётся, чтобы ключ не исчез из показа молча.\n case 'undefined':\n case 'function':\n case 'symbol': return 'unsupported'\n }\n\n // Недостижимо: ветки выше покрывают `typeof` целиком. Оставлено ради вывода\n // типов — без него TS не считает функцию всегда возвращающей.\n return 'unsupported'\n}\n\ntype BuildContext = {\n maxStringLength: number\n maxArrayItems: number\n /**\n * Цепочка предков текущего узла, а не все встреченные объекты.\n *\n * Разница принципиальна и отличает обход дерева от `replacer` у\n * `JSON.stringify`: тому стек недоступен, поэтому он метит `[Circular]` любую\n * повторную ссылку — в том числе объект, честно положенный в данные дважды.\n * Здесь стек есть, и маркер достаётся только настоящему циклу.\n */\n ancestors: Set<object>\n}\n\nfunction buildNode(label: string, path: string, value: unknown, context: BuildContext): GrJsonNode {\n const kind = kindOf(value)\n\n if (kind !== 'object' && kind !== 'array') {\n const { preview, truncated } = scalarPreview(value, context.maxStringLength)\n\n return { path, label, kind, preview, value, truncated, isLeaf: true }\n }\n\n const container = value as object\n\n if (context.ancestors.has(container)) {\n return { path, label, kind, preview: CIRCULAR_MARKER, value, truncated: false, isLeaf: true }\n }\n\n const entries = Array.isArray(value)\n ? value.map((item, index) => [String(index), item] as const)\n : Object.entries(container)\n\n // Пустой объект и пустой массив — листья: раскрывать нечего, а ветка без\n // детей рисует стрелку, которая ничего не делает.\n if (entries.length === 0) {\n return {\n path,\n label,\n kind,\n preview: Array.isArray(value) ? '[]' : '{}',\n value,\n truncated: false,\n isLeaf: true,\n }\n }\n\n context.ancestors.add(container)\n\n const limit = Math.max(0, context.maxArrayItems)\n const isOverLimit = Array.isArray(value) && entries.length > limit\n const visible = isOverLimit ? entries.slice(0, limit) : entries\n\n const children: GrJsonNode[] = visible.map(([key, item]) => buildNode(\n key,\n Array.isArray(value) ? `${path}[${key}]` : appendKey(path, key),\n item,\n context,\n ))\n\n if (isOverLimit) {\n const rest = entries.length - limit\n\n children.push({\n path: `${path}[…]`,\n label: '…',\n kind: 'truncation',\n preview: String(rest),\n value: (value as unknown[]).slice(limit),\n truncated: true,\n isLeaf: true,\n })\n }\n\n context.ancestors.delete(container)\n\n return {\n path,\n label,\n kind,\n preview: branchPreview(kind, entries.length),\n value,\n truncated: isOverLimit,\n isLeaf: false,\n children,\n }\n}\n\n/**\n * Значение → узлы дерева. Корень всегда один: даже скаляр на входе — это узел,\n * иначе дерево пришлось бы то показывать, то нет.\n */\nexport function jsonToNodes(value: unknown, options: GrJsonToNodesOptions = {}): GrJsonNode[] {\n const context: BuildContext = {\n maxStringLength: options.maxStringLength ?? DEFAULT_MAX_STRING_LENGTH,\n maxArrayItems: options.maxArrayItems ?? DEFAULT_MAX_ARRAY_ITEMS,\n ancestors: new Set(),\n }\n\n return [buildNode(options.rootLabel ?? DEFAULT_ROOT_LABEL, DEFAULT_ROOT_LABEL, value, context)]\n}\n\n/** Пути всех узлов до заданной глубины — ими задаётся начальная свёртка. */\nexport function pathsToDepth(nodes: GrJsonNode[], depth: number, level = 0): string[] {\n if (level >= depth)\n return []\n\n return nodes.flatMap(node => (\n node.children ? [node.path, ...pathsToDepth(node.children, depth, level + 1)] : []\n ))\n}\n\n/** Пути всех веток — для «раскрыть всё». */\nexport function branchPaths(nodes: GrJsonNode[]): string[] {\n return nodes.flatMap(node => (node.children ? [node.path, ...branchPaths(node.children)] : []))\n}\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'\n\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrInput from '../GrInput/GrInput.vue'\nimport GrTree from '../GrTree/GrTree.vue'\nimport { useAnnouncer } from '../../composables/useAnnouncer'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\n\nimport IconCheck from '~icons/lucide/check'\nimport IconCopy from '~icons/lucide/copy'\n\nimport { branchPaths, jsonToNodes, pathsToDepth, type GrJsonNode } from './jsonToNodes'\nimport {\n jsonValueClass,\n jsonViewerContentClass,\n jsonViewerCopyClass,\n jsonViewerGroupClass,\n jsonViewerKeyClass,\n jsonViewerMutedClass,\n jsonViewerPunctuationClass,\n jsonViewerRootClass,\n jsonViewerRowClass,\n jsonViewerSearchClass,\n jsonViewerToolbarClass,\n jsonViewerValueClass,\n} from './grJsonViewerStyles'\n\nimport type { GrComponentSize } from '../shared/sizes'\n\nexport type { GrJsonNode, GrJsonNodeKind } from './jsonToNodes'\n\n/**\n * JSON со свёрткой по узлам, поиском по ключу и значению и копированием узла.\n *\n * Дерево, клавиатуру и виртуализацию несёт `GrTree`; здесь — разбор `unknown` в\n * узлы и разметка строки. Плоский текст целиком, который копируют в тикет, —\n * `GrCodeBlock`.\n */\nexport interface GrJsonViewerProps {\n /** Показываемое значение. `unknown`, потому что приходит из БД или чужого сервиса. */\n value: unknown\n /** Подпись корня. По умолчанию `$` — так адресуют путь JSONPath и dev-tools. */\n rootLabel?: string\n /** Сколько уровней раскрыто изначально. */\n defaultExpandDepth?: number\n /**\n * Длина строкового значения, после которой показ обрезается.\n *\n * Не косметика: запрос к модели с картинкой в base64 — это один лист на\n * сотни тысяч символов, и ни свёртка, ни виртуализация по строкам его не\n * берут. Копирование при этом отдаёт значение целиком.\n */\n maxStringLength?: number\n /** Сколько элементов массива разбирать до заглушки «ещё N». */\n maxArrayItems?: number\n /** Поле поиска над деревом. Выключено — поиск остаётся на `filter()` из `defineExpose`. */\n searchable?: boolean\n /** Кнопка копирования узла в строке. */\n copyable?: boolean\n /** Оставлять в DOM только окно вокруг вьюпорта. Требует `maxHeight`. */\n virtual?: boolean\n /** Высота области просмотра: число — пиксели, строка — как есть. */\n maxHeight?: string | number\n /** Имя области для скринридера. Безымянное дерево объявляется просто «дерево». */\n ariaLabel?: string\n size?: GrComponentSize\n}\n\nexport interface GrJsonViewerEmits {\n /** Узел скопирован: путь для тикета и полное значение. */\n (e: 'copy', payload: { path: string, value: unknown }): void\n}\n\nconst props = withDefaults(defineProps<GrJsonViewerProps>(), {\n // Дефолты живут в резолверах ниже: Vue подставил бы свой раньше, чем\n // компонент заглянет в `GrConfigProvider`.\n rootLabel: undefined,\n defaultExpandDepth: undefined,\n maxStringLength: undefined,\n maxArrayItems: undefined,\n searchable: undefined,\n copyable: undefined,\n virtual: false,\n maxHeight: undefined,\n ariaLabel: undefined,\n size: undefined,\n})\n\nconst emit = defineEmits<GrJsonViewerEmits>()\n\ndefineSlots<{\n /** Значение листа: ссылка, дата, денежная сумма — оформление знает приложение. */\n leaf?: (props: { node: GrJsonNode }) => unknown\n}>()\n\nconst { t } = useGranularityTranslations()\nconst { announce } = useAnnouncer()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrJsonViewer' })\nconst resolvedDepth = useGrComponentProp('GrJsonViewer', 'defaultExpandDepth', () => props.defaultExpandDepth, 1)\nconst resolvedMaxString = useGrComponentProp('GrJsonViewer', 'maxStringLength', () => props.maxStringLength, 200)\nconst resolvedMaxArray = useGrComponentProp('GrJsonViewer', 'maxArrayItems', () => props.maxArrayItems, 100)\nconst resolvedSearchable = useGrComponentProp('GrJsonViewer', 'searchable', () => props.searchable, true)\nconst resolvedCopyable = useGrComponentProp('GrJsonViewer', 'copyable', () => props.copyable, true)\n\nconst nodes = computed(() => jsonToNodes(props.value, {\n rootLabel: props.rootLabel,\n maxStringLength: resolvedMaxString.value,\n maxArrayItems: resolvedMaxArray.value,\n}))\n\n/**\n * Раскрытие ведётся подстановкой набора путей.\n *\n * Контролируемого `expandedKeys` у `GrTree` нет, но `defaultExpandedKeys` —\n * не «начальное значение»: дерево следит за пропом и заменяет набор целиком.\n * Этого хватает и глубине по умолчанию, и кнопкам «раскрыть/свернуть всё»;\n * ручное раскрытие пользователя при этом сбрасывается — от этих кнопок ровно\n * того и ждут.\n */\nconst expandedPaths = ref<string[]>([])\n\nwatch(\n [nodes, resolvedDepth],\n ([list, depth]) => { expandedPaths.value = pathsToDepth(list, depth) },\n { immediate: true },\n)\n\nfunction expandAll(): void {\n expandedPaths.value = branchPaths(nodes.value)\n}\n\nfunction collapseAll(): void {\n expandedPaths.value = []\n}\n\nconst query = ref('')\nconst treeRef = ref<{ filter: (value: string) => void } | null>(null)\n\nwatch(query, (value) => {\n treeRef.value?.filter(value)\n})\n\nfunction filter(value: string): void {\n query.value = value\n}\n\n/** Поиск идёт и по ключу, и по значению: в чужом ответе ищут то одно, то другое. */\nfunction matchKeyOrValue(value: string, data: GrJsonNode): boolean {\n if (!value)\n return true\n\n const needle = value.toLowerCase()\n\n return data.label.toLowerCase().includes(needle) || data.preview.toLowerCase().includes(needle)\n}\n\n/**\n * Наличие буфера уточняется после монтирования: `navigator` в теле `setup` либо\n * роняет серверный рендер, либо расходится с ним. До этого момента кнопки нет —\n * ровно то же, что отдаёт сервер, поэтому гидрация совпадает.\n */\nconst canCopy = ref(false)\n\nonMounted(() => {\n canCopy.value = typeof navigator !== 'undefined' && typeof navigator.clipboard?.writeText === 'function'\n\n if (props.virtual && props.maxHeight === undefined && __GR_DEV__) {\n console.warn('[GrJsonViewer] `virtual` без `maxHeight` не включает виртуализацию: окно считается от высоты области.')\n }\n})\n\nconst showCopy = computed(() => resolvedCopyable.value && canCopy.value)\n\nconst copiedPath = ref<string | null>(null)\nlet copiedTimer: number | null = null\n\nfunction resetCopiedTimer(): void {\n if (copiedTimer !== null) {\n clearTimeout(copiedTimer)\n copiedTimer = null\n }\n}\n\nonBeforeUnmount(resetCopiedTimer)\n\n/**\n * В буфер уходит **полное** значение узла, а не то, что видно: обрезка\n * принадлежит показу, и вставить её обратно нельзя.\n */\nasync function copyNode(node: GrJsonNode): Promise<void> {\n const text = typeof node.value === 'string' ? node.value : JSON.stringify(node.value, null, 2) ?? String(node.value)\n\n try {\n await navigator.clipboard.writeText(text)\n }\n catch {\n announce(t('gr.jsonViewer.copyFailed', 'Could not copy value'))\n return\n }\n\n copiedPath.value = node.path\n resetCopiedTimer()\n copiedTimer = window.setTimeout(() => {\n copiedPath.value = null\n copiedTimer = null\n }, 1200)\n\n announce(t('gr.jsonViewer.copied', 'Value copied'))\n emit('copy', { path: node.path, value: node.value })\n}\n\ndefineExpose({ filter, expandAll, collapseAll })\n</script>\n\n<template>\n <div data-gr-json-viewer :class=\"jsonViewerRootClass\">\n <div v-if=\"resolvedSearchable\" data-gr-json-viewer-toolbar :class=\"jsonViewerToolbarClass\">\n <GrInput\n v-model=\"query\"\n :class=\"jsonViewerSearchClass\"\n :size=\"resolvedSize\"\n clearable\n :placeholder=\"t('gr.jsonViewer.search', 'Search key or value')\"\n :aria-label=\"t('gr.jsonViewer.search', 'Search key or value')\"\n />\n\n <GrButton variant=\"ghost\" :size=\"resolvedSize\" @click=\"expandAll\">\n {{ t('gr.jsonViewer.expandAll', 'Expand all') }}\n </GrButton>\n <GrButton variant=\"ghost\" :size=\"resolvedSize\" @click=\"collapseAll\">\n {{ t('gr.jsonViewer.collapseAll', 'Collapse all') }}\n </GrButton>\n </div>\n\n <GrTree\n ref=\"treeRef\"\n :data=\"nodes\"\n node-key=\"path\"\n :props=\"{ children: 'children', label: 'label', isLeaf: 'isLeaf' }\"\n :default-expanded-keys=\"expandedPaths\"\n :filter-node-method=\"matchKeyOrValue\"\n :content-class=\"jsonViewerContentClass\"\n :virtual=\"virtual\"\n :max-height=\"maxHeight\"\n :size=\"resolvedSize\"\n :aria-label=\"ariaLabel\"\n >\n <template #default=\"{ data }\">\n <span data-gr-json-viewer-row :class=\"[jsonViewerRowClass, jsonViewerGroupClass]\">\n <span data-gr-json-viewer-key :class=\"jsonViewerKeyClass\">{{ data.label }}</span>\n <span :class=\"jsonViewerPunctuationClass\">:</span>\n\n <span\n data-gr-json-viewer-value\n :class=\"[jsonViewerValueClass, jsonValueClass[(data as GrJsonNode).kind]]\"\n >\n <slot name=\"leaf\" :node=\"data as GrJsonNode\">{{ (data as GrJsonNode).preview }}</slot>\n </span>\n\n <span\n v-if=\"(data as GrJsonNode).kind === 'truncation'\"\n :class=\"jsonViewerMutedClass\"\n >{{ t('gr.jsonViewer.moreItems', 'more') }}</span>\n\n <GrButton\n v-if=\"showCopy && (data as GrJsonNode).kind !== 'truncation'\"\n data-gr-json-viewer-copy\n variant=\"ghost\"\n size=\"xs\"\n square\n :class=\"jsonViewerCopyClass\"\n :aria-label=\"t('gr.jsonViewer.copy', 'Copy value')\"\n :title=\"t('gr.jsonViewer.copy', 'Copy value')\"\n @click.stop=\"copyNode(data as GrJsonNode)\"\n >\n <component\n :is=\"copiedPath === (data as GrJsonNode).path ? IconCheck : IconCopy\"\n class=\"h-3.5 w-3.5\"\n aria-hidden=\"true\"\n />\n </GrButton>\n </span>\n </template>\n </GrTree>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'\n\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrInput from '../GrInput/GrInput.vue'\nimport GrTree from '../GrTree/GrTree.vue'\nimport { useAnnouncer } from '../../composables/useAnnouncer'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\n\nimport IconCheck from '~icons/lucide/check'\nimport IconCopy from '~icons/lucide/copy'\n\nimport { branchPaths, jsonToNodes, pathsToDepth, type GrJsonNode } from './jsonToNodes'\nimport {\n jsonValueClass,\n jsonViewerContentClass,\n jsonViewerCopyClass,\n jsonViewerGroupClass,\n jsonViewerKeyClass,\n jsonViewerMutedClass,\n jsonViewerPunctuationClass,\n jsonViewerRootClass,\n jsonViewerRowClass,\n jsonViewerSearchClass,\n jsonViewerToolbarClass,\n jsonViewerValueClass,\n} from './grJsonViewerStyles'\n\nimport type { GrComponentSize } from '../shared/sizes'\n\nexport type { GrJsonNode, GrJsonNodeKind } from './jsonToNodes'\n\n/**\n * JSON со свёрткой по узлам, поиском по ключу и значению и копированием узла.\n *\n * Дерево, клавиатуру и виртуализацию несёт `GrTree`; здесь — разбор `unknown` в\n * узлы и разметка строки. Плоский текст целиком, который копируют в тикет, —\n * `GrCodeBlock`.\n */\nexport interface GrJsonViewerProps {\n /** Показываемое значение. `unknown`, потому что приходит из БД или чужого сервиса. */\n value: unknown\n /** Подпись корня. По умолчанию `$` — так адресуют путь JSONPath и dev-tools. */\n rootLabel?: string\n /** Сколько уровней раскрыто изначально. */\n defaultExpandDepth?: number\n /**\n * Длина строкового значения, после которой показ обрезается.\n *\n * Не косметика: запрос к модели с картинкой в base64 — это один лист на\n * сотни тысяч символов, и ни свёртка, ни виртуализация по строкам его не\n * берут. Копирование при этом отдаёт значение целиком.\n */\n maxStringLength?: number\n /** Сколько элементов массива разбирать до заглушки «ещё N». */\n maxArrayItems?: number\n /** Поле поиска над деревом. Выключено — поиск остаётся на `filter()` из `defineExpose`. */\n searchable?: boolean\n /** Кнопка копирования узла в строке. */\n copyable?: boolean\n /** Оставлять в DOM только окно вокруг вьюпорта. Требует `maxHeight`. */\n virtual?: boolean\n /** Высота области просмотра: число — пиксели, строка — как есть. */\n maxHeight?: string | number\n /** Имя области для скринридера. Безымянное дерево объявляется просто «дерево». */\n ariaLabel?: string\n size?: GrComponentSize\n}\n\nexport interface GrJsonViewerEmits {\n /** Узел скопирован: путь для тикета и полное значение. */\n (e: 'copy', payload: { path: string, value: unknown }): void\n}\n\nconst props = withDefaults(defineProps<GrJsonViewerProps>(), {\n // Дефолты живут в резолверах ниже: Vue подставил бы свой раньше, чем\n // компонент заглянет в `GrConfigProvider`.\n rootLabel: undefined,\n defaultExpandDepth: undefined,\n maxStringLength: undefined,\n maxArrayItems: undefined,\n searchable: undefined,\n copyable: undefined,\n virtual: false,\n maxHeight: undefined,\n ariaLabel: undefined,\n size: undefined,\n})\n\nconst emit = defineEmits<GrJsonViewerEmits>()\n\ndefineSlots<{\n /** Значение листа: ссылка, дата, денежная сумма — оформление знает приложение. */\n leaf?: (props: { node: GrJsonNode }) => unknown\n}>()\n\nconst { t } = useGranularityTranslations()\nconst { announce } = useAnnouncer()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrJsonViewer' })\nconst resolvedDepth = useGrComponentProp('GrJsonViewer', 'defaultExpandDepth', () => props.defaultExpandDepth, 1)\nconst resolvedMaxString = useGrComponentProp('GrJsonViewer', 'maxStringLength', () => props.maxStringLength, 200)\nconst resolvedMaxArray = useGrComponentProp('GrJsonViewer', 'maxArrayItems', () => props.maxArrayItems, 100)\nconst resolvedSearchable = useGrComponentProp('GrJsonViewer', 'searchable', () => props.searchable, true)\nconst resolvedCopyable = useGrComponentProp('GrJsonViewer', 'copyable', () => props.copyable, true)\n\nconst nodes = computed(() => jsonToNodes(props.value, {\n rootLabel: props.rootLabel,\n maxStringLength: resolvedMaxString.value,\n maxArrayItems: resolvedMaxArray.value,\n}))\n\n/**\n * Раскрытие ведётся подстановкой набора путей.\n *\n * Контролируемого `expandedKeys` у `GrTree` нет, но `defaultExpandedKeys` —\n * не «начальное значение»: дерево следит за пропом и заменяет набор целиком.\n * Этого хватает и глубине по умолчанию, и кнопкам «раскрыть/свернуть всё»;\n * ручное раскрытие пользователя при этом сбрасывается — от этих кнопок ровно\n * того и ждут.\n */\nconst expandedPaths = ref<string[]>([])\n\nwatch(\n [nodes, resolvedDepth],\n ([list, depth]) => { expandedPaths.value = pathsToDepth(list, depth) },\n { immediate: true },\n)\n\nfunction expandAll(): void {\n expandedPaths.value = branchPaths(nodes.value)\n}\n\nfunction collapseAll(): void {\n expandedPaths.value = []\n}\n\nconst query = ref('')\nconst treeRef = ref<{ filter: (value: string) => void } | null>(null)\n\nwatch(query, (value) => {\n treeRef.value?.filter(value)\n})\n\nfunction filter(value: string): void {\n query.value = value\n}\n\n/** Поиск идёт и по ключу, и по значению: в чужом ответе ищут то одно, то другое. */\nfunction matchKeyOrValue(value: string, data: GrJsonNode): boolean {\n if (!value)\n return true\n\n const needle = value.toLowerCase()\n\n return data.label.toLowerCase().includes(needle) || data.preview.toLowerCase().includes(needle)\n}\n\n/**\n * Наличие буфера уточняется после монтирования: `navigator` в теле `setup` либо\n * роняет серверный рендер, либо расходится с ним. До этого момента кнопки нет —\n * ровно то же, что отдаёт сервер, поэтому гидрация совпадает.\n */\nconst canCopy = ref(false)\n\nonMounted(() => {\n canCopy.value = typeof navigator !== 'undefined' && typeof navigator.clipboard?.writeText === 'function'\n\n if (props.virtual && props.maxHeight === undefined && __GR_DEV__) {\n console.warn('[GrJsonViewer] `virtual` без `maxHeight` не включает виртуализацию: окно считается от высоты области.')\n }\n})\n\nconst showCopy = computed(() => resolvedCopyable.value && canCopy.value)\n\nconst copiedPath = ref<string | null>(null)\nlet copiedTimer: number | null = null\n\nfunction resetCopiedTimer(): void {\n if (copiedTimer !== null) {\n clearTimeout(copiedTimer)\n copiedTimer = null\n }\n}\n\nonBeforeUnmount(resetCopiedTimer)\n\n/**\n * В буфер уходит **полное** значение узла, а не то, что видно: обрезка\n * принадлежит показу, и вставить её обратно нельзя.\n */\nasync function copyNode(node: GrJsonNode): Promise<void> {\n const text = typeof node.value === 'string' ? node.value : JSON.stringify(node.value, null, 2) ?? String(node.value)\n\n try {\n await navigator.clipboard.writeText(text)\n }\n catch {\n announce(t('gr.jsonViewer.copyFailed', 'Could not copy value'))\n return\n }\n\n copiedPath.value = node.path\n resetCopiedTimer()\n copiedTimer = window.setTimeout(() => {\n copiedPath.value = null\n copiedTimer = null\n }, 1200)\n\n announce(t('gr.jsonViewer.copied', 'Value copied'))\n emit('copy', { path: node.path, value: node.value })\n}\n\ndefineExpose({ filter, expandAll, collapseAll })\n</script>\n\n<template>\n <div data-gr-json-viewer :class=\"jsonViewerRootClass\">\n <div v-if=\"resolvedSearchable\" data-gr-json-viewer-toolbar :class=\"jsonViewerToolbarClass\">\n <GrInput\n v-model=\"query\"\n :class=\"jsonViewerSearchClass\"\n :size=\"resolvedSize\"\n clearable\n :placeholder=\"t('gr.jsonViewer.search', 'Search key or value')\"\n :aria-label=\"t('gr.jsonViewer.search', 'Search key or value')\"\n />\n\n <GrButton variant=\"ghost\" :size=\"resolvedSize\" @click=\"expandAll\">\n {{ t('gr.jsonViewer.expandAll', 'Expand all') }}\n </GrButton>\n <GrButton variant=\"ghost\" :size=\"resolvedSize\" @click=\"collapseAll\">\n {{ t('gr.jsonViewer.collapseAll', 'Collapse all') }}\n </GrButton>\n </div>\n\n <GrTree\n ref=\"treeRef\"\n :data=\"nodes\"\n node-key=\"path\"\n :props=\"{ children: 'children', label: 'label', isLeaf: 'isLeaf' }\"\n :default-expanded-keys=\"expandedPaths\"\n :filter-node-method=\"matchKeyOrValue\"\n :content-class=\"jsonViewerContentClass\"\n :virtual=\"virtual\"\n :max-height=\"maxHeight\"\n :size=\"resolvedSize\"\n :aria-label=\"ariaLabel\"\n >\n <template #default=\"{ data }\">\n <span data-gr-json-viewer-row :class=\"[jsonViewerRowClass, jsonViewerGroupClass]\">\n <span data-gr-json-viewer-key :class=\"jsonViewerKeyClass\">{{ data.label }}</span>\n <span :class=\"jsonViewerPunctuationClass\">:</span>\n\n <span\n data-gr-json-viewer-value\n :class=\"[jsonViewerValueClass, jsonValueClass[(data as GrJsonNode).kind]]\"\n >\n <slot name=\"leaf\" :node=\"data as GrJsonNode\">{{ (data as GrJsonNode).preview }}</slot>\n </span>\n\n <span\n v-if=\"(data as GrJsonNode).kind === 'truncation'\"\n :class=\"jsonViewerMutedClass\"\n >{{ t('gr.jsonViewer.moreItems', 'more') }}</span>\n\n <GrButton\n v-if=\"showCopy && (data as GrJsonNode).kind !== 'truncation'\"\n data-gr-json-viewer-copy\n variant=\"ghost\"\n size=\"xs\"\n square\n :class=\"jsonViewerCopyClass\"\n :aria-label=\"t('gr.jsonViewer.copy', 'Copy value')\"\n :title=\"t('gr.jsonViewer.copy', 'Copy value')\"\n @click.stop=\"copyNode(data as GrJsonNode)\"\n >\n <component\n :is=\"copiedPath === (data as GrJsonNode).path ? IconCheck : IconCopy\"\n class=\"h-3.5 w-3.5\"\n aria-hidden=\"true\"\n />\n </GrButton>\n </span>\n </template>\n </GrTree>\n </div>\n</template>\n"],"mappings":";;;;;;;;;;;;AA+CA,IAAa,kBAAkB;AAE/B,IAAM,qBAAqB;AAC3B,IAAM,4BAA4B;AAClC,IAAM,0BAA0B;;AAGhC,IAAM,YAAY;AAElB,SAAS,UAAU,YAAoB,KAAqB;CAC1D,OAAO,UAAU,KAAK,GAAG,IAAI,GAAG,WAAW,GAAG,QAAQ,GAAG,WAAW,GAAG,KAAK,UAAU,GAAG,EAAE;AAC7F;;;;;;AAOA,SAAS,cAAc,MAA0B,OAAuB;CACtE,OAAO,SAAS,UAAU,IAAI,MAAM,KAAK,IAAI,MAAM;AACrD;AAEA,SAAS,cAAc,OAAgB,iBAAkE;CACvG,IAAI,OAAO,UAAU,UAAU;EAC7B,IAAI,MAAM,UAAU,iBAClB,OAAO;GAAE,SAAS,KAAK,UAAU,KAAK;GAAG,WAAW;EAAM;EAI5D,OAAO;GAAE,SAAS,GAAG,KAAK,UAAU,MAAM,MAAM,GAAG,eAAe,CAAC,EAAE;GAAI,WAAW;EAAK;CAC3F;CAGA,IAAI,OAAO,UAAU,UACnB,OAAO;EAAE,SAAS,GAAG,MAAM;EAAI,WAAW;CAAM;CAElD,IAAI,OAAO,UAAU,cAAc,OAAO,UAAU,YAAY,UAAU,KAAA,GACxE,OAAO;EAAE,SAAS,OAAO,KAAK;EAAG,WAAW;CAAM;CAEpD,OAAO;EAAE,SAAS,KAAK,UAAU,KAAK,KAAK,OAAO,KAAK;EAAG,WAAW;CAAM;AAC7E;AAEA,SAAS,OAAO,OAAgC;CAC9C,IAAI,UAAU,MACZ,OAAO;CACT,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO;CAET,QAAQ,OAAO,OAAf;EACE,KAAK,UAAU,OAAO;EACtB,KAAK,UAAU,OAAO;EACtB,KAAK,UAAU,OAAO;EACtB,KAAK,WAAW,OAAO;EACvB,KAAK,UAAU,OAAO;EAGtB,KAAK;EACL,KAAK;EACL,KAAK,UAAU,OAAO;CACxB;CAIA,OAAO;AACT;AAgBA,SAAS,UAAU,OAAe,MAAc,OAAgB,SAAmC;CACjG,MAAM,OAAO,OAAO,KAAK;CAEzB,IAAI,SAAS,YAAY,SAAS,SAAS;EACzC,MAAM,EAAE,SAAS,cAAc,cAAc,OAAO,QAAQ,eAAe;EAE3E,OAAO;GAAE;GAAM;GAAO;GAAM;GAAS;GAAO;GAAW,QAAQ;EAAK;CACtE;CAEA,MAAM,YAAY;CAElB,IAAI,QAAQ,UAAU,IAAI,SAAS,GACjC,OAAO;EAAE;EAAM;EAAO;EAAM,SAAS;EAAiB;EAAO,WAAW;EAAO,QAAQ;CAAK;CAG9F,MAAM,UAAU,MAAM,QAAQ,KAAK,IAC/B,MAAM,KAAK,MAAM,UAAU,CAAC,OAAO,KAAK,GAAG,IAAI,CAAU,IACzD,OAAO,QAAQ,SAAS;CAI5B,IAAI,QAAQ,WAAW,GACrB,OAAO;EACL;EACA;EACA;EACA,SAAS,MAAM,QAAQ,KAAK,IAAI,OAAO;EACvC;EACA,WAAW;EACX,QAAQ;CACV;CAGF,QAAQ,UAAU,IAAI,SAAS;CAE/B,MAAM,QAAQ,KAAK,IAAI,GAAG,QAAQ,aAAa;CAC/C,MAAM,cAAc,MAAM,QAAQ,KAAK,KAAK,QAAQ,SAAS;CAG7D,MAAM,YAFU,cAAc,QAAQ,MAAM,GAAG,KAAK,IAAI,QAAA,CAEjB,KAAK,CAAC,KAAK,UAAU,UAC1D,KACA,MAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,GAAG,IAAI,KAAK,UAAU,MAAM,GAAG,GAC9D,MACA,OACF,CAAC;CAED,IAAI,aAAa;EACf,MAAM,OAAO,QAAQ,SAAS;EAE9B,SAAS,KAAK;GACZ,MAAM,GAAG,KAAK;GACd,OAAO;GACP,MAAM;GACN,SAAS,OAAO,IAAI;GACpB,OAAQ,MAAoB,MAAM,KAAK;GACvC,WAAW;GACX,QAAQ;EACV,CAAC;CACH;CAEA,QAAQ,UAAU,OAAO,SAAS;CAElC,OAAO;EACL;EACA;EACA;EACA,SAAS,cAAc,MAAM,QAAQ,MAAM;EAC3C;EACA,WAAW;EACX,QAAQ;EACR;CACF;AACF;;;;;AAMA,SAAgB,YAAY,OAAgB,UAAgC,CAAC,GAAiB;CAC5F,MAAM,UAAwB;EAC5B,iBAAiB,QAAQ,mBAAmB;EAC5C,eAAe,QAAQ,iBAAiB;EACxC,2BAAW,IAAI,IAAI;CACrB;CAEA,OAAO,CAAC,UAAU,QAAQ,aAAa,oBAAoB,oBAAoB,OAAO,OAAO,CAAC;AAChG;;AAGA,SAAgB,aAAa,OAAqB,OAAe,QAAQ,GAAa;CACpF,IAAI,SAAS,OACX,OAAO,CAAC;CAEV,OAAO,MAAM,SAAQ,SACnB,KAAK,WAAW,CAAC,KAAK,MAAM,GAAG,aAAa,KAAK,UAAU,OAAO,QAAQ,CAAC,CAAC,IAAI,CAAC,CAClF;AACH;;AAGA,SAAgB,YAAY,OAA+B;CACzD,OAAO,MAAM,SAAQ,SAAS,KAAK,WAAW,CAAC,KAAK,MAAM,GAAG,YAAY,KAAK,QAAQ,CAAC,IAAI,CAAC,CAAE;AAChG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ECzJA,MAAM,QAAQ;EAed,MAAM,OAAO;EAOb,MAAM,EAAE,MAAM,2BAA2B;EACzC,MAAM,EAAE,aAAa,aAAa;EAElC,MAAM,eAAe,yBAAyB,MAAM,MAAM,EAAE,WAAW,eAAe,CAAC;EACvF,MAAM,gBAAgB,mBAAmB,gBAAgB,4BAA4B,MAAM,oBAAoB,CAAC;EAChH,MAAM,oBAAoB,mBAAmB,gBAAgB,yBAAyB,MAAM,iBAAiB,GAAG;EAChH,MAAM,mBAAmB,mBAAmB,gBAAgB,uBAAuB,MAAM,eAAe,GAAG;EAC3G,MAAM,qBAAqB,mBAAmB,gBAAgB,oBAAoB,MAAM,YAAY,IAAI;EACxG,MAAM,mBAAmB,mBAAmB,gBAAgB,kBAAkB,MAAM,UAAU,IAAI;EAElG,MAAM,QAAQ,eAAe,YAAY,MAAM,OAAO;GACpD,WAAW,MAAM;GACjB,iBAAiB,kBAAkB;GACnC,eAAe,iBAAiB;EAClC,CAAC,CAAC;;;;;;;;;;EAWF,MAAM,gBAAgB,IAAc,CAAC,CAAC;EAEtC,MACE,CAAC,OAAO,aAAa,IACpB,CAAC,MAAM,WAAW;GAAE,cAAc,QAAQ,aAAa,MAAM,KAAK;EAAE,GACrE,EAAE,WAAW,KAAK,CACpB;EAEA,SAAS,YAAkB;GACzB,cAAc,QAAQ,YAAY,MAAM,KAAK;EAC/C;EAEA,SAAS,cAAoB;GAC3B,cAAc,QAAQ,CAAC;EACzB;EAEA,MAAM,QAAQ,IAAI,EAAE;EACpB,MAAM,UAAU,IAAgD,IAAI;EAEpE,MAAM,QAAQ,UAAU;GACtB,QAAQ,OAAO,OAAO,KAAK;EAC7B,CAAC;EAED,SAAS,OAAO,OAAqB;GACnC,MAAM,QAAQ;EAChB;;EAGA,SAAS,gBAAgB,OAAe,MAA2B;GACjE,IAAI,CAAC,OACH,OAAO;GAET,MAAM,SAAS,MAAM,YAAY;GAEjC,OAAO,KAAK,MAAM,YAAY,CAAC,CAAC,SAAS,MAAM,KAAK,KAAK,QAAQ,YAAY,CAAC,CAAC,SAAS,MAAM;EAChG;;;;;;EAOA,MAAM,UAAU,IAAI,KAAK;EAEzB,gBAAgB;GACd,QAAQ,QAAQ,OAAO,cAAc,eAAe,OAAO,UAAU,WAAW,cAAc;GAE9F,IAAI,MAAM,WAAW,MAAM,cAAc,KAAA,KAAA,OAAA,YAAA,eAAA,QAAA,IAAA,aAAA,cACvC,QAAQ,KAAK,uGAAuG;EAExH,CAAC;EAED,MAAM,WAAW,eAAe,iBAAiB,SAAS,QAAQ,KAAK;EAEvE,MAAM,aAAa,IAAmB,IAAI;EAC1C,IAAI,cAA6B;EAEjC,SAAS,mBAAyB;GAChC,IAAI,gBAAgB,MAAM;IACxB,aAAa,WAAW;IACxB,cAAc;GAChB;EACF;EAEA,gBAAgB,gBAAgB;;;;;EAMhC,eAAe,SAAS,MAAiC;GACvD,MAAM,OAAO,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ,KAAK,UAAU,KAAK,OAAO,MAAM,CAAC,KAAK,OAAO,KAAK,KAAK;GAEnH,IAAI;IACF,MAAM,UAAU,UAAU,UAAU,IAAI;GAC1C,QACM;IACJ,SAAS,EAAE,4BAA4B,sBAAsB,CAAC;IAC9D;GACF;GAEA,WAAW,QAAQ,KAAK;GACxB,iBAAiB;GACjB,cAAc,OAAO,iBAAiB;IACpC,WAAW,QAAQ;IACnB,cAAc;GAChB,GAAG,IAAI;GAEP,SAAS,EAAE,wBAAwB,cAAc,CAAC;GAClD,KAAK,QAAQ;IAAE,MAAM,KAAK;IAAM,OAAO,KAAK;GAAM,CAAC;EACrD;EAEA,SAAa;GAAE;GAAQ;GAAW;EAAY,CAAC;;uBAI7C,mBAqEM,OAAA;IArED,uBAAA;IAAqB,OAAK,eAAE,MAAA,mBAAA,CAAmB;OACvC,MAAA,kBAAA,KAAA,UAAA,GAAX,mBAgBM,OAAA;;IAhByB,+BAAA;IAA6B,OAAK,eAAE,MAAA,sBAAA,CAAsB;;IACvF,YAOE,iBAAA;iBANS,MAAA;wEAAK,QAAA;KACb,OAAK,eAAE,MAAA,qBAAA,CAAqB;KAC5B,MAAM,MAAA,YAAA;KACP,WAAA;KACC,aAAa,MAAA,CAAA,CAAC,CAAA,wBAAA,qBAAA;KACd,cAAY,MAAA,CAAA,CAAC,CAAA,wBAAA,qBAAA;;;;;;;;IAGhB,YAEW,kBAAA;KAFD,SAAQ;KAAS,MAAM,MAAA,YAAA;KAAe,SAAO;;4BACL,CAAA,gBAAA,gBAA7C,MAAA,CAAA,CAAC,CAAA,2BAAA,YAAA,CAAA,GAAA,CAAA,CAAA,CAAA;;;IAEN,YAEW,kBAAA;KAFD,SAAQ;KAAS,MAAM,MAAA,YAAA;KAAe,SAAO;;4BACD,CAAA,gBAAA,gBAAjD,MAAA,CAAA,CAAC,CAAA,6BAAA,cAAA,CAAA,GAAA,CAAA,CAAA,CAAA;;;0CAIR,YAiDS,gBAAA;aAhDH;IAAJ,KAAI;IACH,MAAM,MAAA;IACP,YAAS;IACR,OAAO;KAAA,UAAA;KAAA,OAAA;KAAA,QAAA;IAAA;IACP,yBAAuB,cAAA;IACvB,sBAAoB;IACpB,iBAAe,MAAA,sBAAA;IACf,SAAS,QAAA;IACT,cAAY,QAAA;IACZ,MAAM,MAAA,YAAA;IACN,cAAY,QAAA;;IAEF,SAAO,SAkCT,EAlCa,WAAI,CACxB,mBAiCO,QAAA;KAjCD,2BAAA;KAAyB,OAAK,eAAA,CAAG,MAAA,kBAAA,GAAoB,MAAA,oBAAA,CAAoB,CAAA;;KAC7E,mBAAiF,QAAA;MAA3E,2BAAA;MAAyB,OAAK,eAAE,MAAA,kBAAA,CAAkB;wBAAK,KAAK,KAAK,GAAA,CAAA;KACvE,mBAAkD,QAAA,EAA3C,OAAK,eAAE,MAAA,0BAAA,CAA0B,EAAA,GAAE,KAAC,CAAA;KAE3C,mBAKO,QAAA;MAJL,6BAAA;MACC,OAAK,eAAA,CAAG,MAAA,oBAAA,GAAsB,MAAA,cAAA,CAAc,CAAE,KAAoB,KAAI,CAAA;SAEvE,WAAsF,KAAA,QAAA,QAAA,EAAnE,MAAM,KAAA,SAA6D,CAAA,gBAAA,gBAArC,KAAoB,OAAO,GAAA,CAAA,CAAA,CAAA,CAAA,GAAA,CAAA;KAIrE,KAAoB,SAAI,gBAAA,UAAA,GADjC,mBAGkD,QAAA;;MAD/C,OAAK,eAAE,MAAA,oBAAA,CAAoB;wBAC1B,MAAA,CAAA,CAAC,CAAA,2BAAA,MAAA,CAAA,GAAA,CAAA,KAAA,mBAAA,IAAA,IAAA;KAGG,SAAA,SAAa,KAAoB,SAAI,gBAAA,UAAA,GAD7C,YAgBW,kBAAA;;MAdT,4BAAA;MACA,SAAQ;MACR,MAAK;MACL,QAAA;MACC,OAAK,eAAE,MAAA,mBAAA,CAAmB;MAC1B,cAAY,MAAA,CAAA,CAAC,CAAA,sBAAA,YAAA;MACb,OAAO,MAAA,CAAA,CAAC,CAAA,sBAAA,YAAA;MACR,SAAK,eAAA,WAAO,SAAS,IAAI,GAAA,CAAA,MAAA,CAAA;;6BAMxB,EAAA,UAAA,GAJF,YAIE,wBAHK,WAAA,UAAgB,KAAoB,OAAO,MAAA,aAAA,IAAY,MAAA,YAAA,CAAQ,GAAA;OACpE,OAAM;OACN,eAAY"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { t as GrJsonViewer_default } from "./chunks/GrJsonViewer-
|
|
1
|
+
import { t as GrJsonViewer_default } from "./chunks/GrJsonViewer-BXG7DdTe.js";
|
|
2
2
|
import { n as grJsonViewerSafelist, t as grJsonViewerConfig } from "../../chunks/config-DpAYidyY2.js";
|
|
3
3
|
export { GrJsonViewer_default as GrJsonViewer, GrJsonViewer_default as default, grJsonViewerConfig, grJsonViewerSafelist };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
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 useModalOverlay } from "../../../chunks/useModalOverlay-BnAGZpcI.js";
|
|
4
|
-
import { a as overlayTransition, d as panelSectionClass, f as panelTransition, i as overlay, m as root, n as getGrModalShellClass, r as layoutByScroll, s as panelBodyScrollClass, t as getGrModalPanelClass } from "../../../chunks/grModalStyles-
|
|
4
|
+
import { a as overlayTransition, d as panelSectionClass, f as panelTransition, h as rootClosingClass, i as overlay, m as root, n as getGrModalShellClass, r as layoutByScroll, s as panelBodyScrollClass, t as getGrModalPanelClass } from "../../../chunks/grModalStyles-BEkiFal_.js";
|
|
5
5
|
import { Teleport, Transition, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, inject, mergeProps, normalizeClass, onScopeDispose, openBlock, provide, ref, renderSlot, toHandlers, unref, useId, useSlots, watch, withCtx } from "vue";
|
|
6
6
|
//#region src/components/GrModal/context.ts
|
|
7
7
|
var GR_MODAL_CONTEXT_KEY = Symbol("GrModalContext");
|
|
@@ -197,6 +197,7 @@ var GrModal_default = /* @__PURE__ */ defineComponent({
|
|
|
197
197
|
closeOnBackdrop: () => props.closeOnBackdrop,
|
|
198
198
|
initialFocus: () => props.initialFocus ?? panelRef.value
|
|
199
199
|
});
|
|
200
|
+
const rootClass = computed(() => [root, isMounted.value && !isVisible.value ? rootClosingClass : ""].filter(Boolean));
|
|
200
201
|
function onPanelAfterLeave() {
|
|
201
202
|
releasePresence();
|
|
202
203
|
emit("closed");
|
|
@@ -223,7 +224,7 @@ var GrModal_default = /* @__PURE__ */ defineComponent({
|
|
|
223
224
|
}, unref(themeAttrs), {
|
|
224
225
|
role: "dialog",
|
|
225
226
|
"aria-modal": "true",
|
|
226
|
-
class:
|
|
227
|
+
class: rootClass.value,
|
|
227
228
|
style: { zIndex: unref(layerZIndex) },
|
|
228
229
|
inert: unref(inertAttr),
|
|
229
230
|
"aria-labelledby": labelledBy.value,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GrModal-DixnhJcu.js","names":["$slots"],"sources":["../../../../src/components/GrModal/context.ts","../../../../src/components/GrModal/GrModal.vue","../../../../src/components/GrModal/GrModal.vue"],"sourcesContent":["import type { InjectionKey } from 'vue'\nimport { inject, onScopeDispose, provide, watch } from 'vue'\n\nexport interface GrModalContext {\n /** Id, который обязан оказаться на элементе заголовка окна. */\n titleId: string\n /** Id, который обязан оказаться на элементе описания окна. */\n descriptionId: string\n /** «Заголовок отрисован» — окно ставит `aria-labelledby`. Возвращает отказ от заявки. */\n claimTitle: () => () => void\n /** То же для описания и `aria-describedby`. */\n claimDescription: () => () => void\n}\n\nexport const GR_MODAL_CONTEXT_KEY: InjectionKey<GrModalContext> = Symbol('GrModalContext')\n\nexport function provideGrModalContext(context: GrModalContext): void {\n provide(GR_MODAL_CONTEXT_KEY, context)\n}\n\n/**\n * Контекст окна для его частей.\n *\n * Именно контекст, а не проп: заголовок рисует `GrDialogHeader` — внук\n * `GrModal` через слот, — а `aria-labelledby` стоит на корне окна.\n *\n * **Id принадлежит окну, а не заголовку.** Заголовок только сообщает, что\n * отрисовался. Обратный порядок (id рождается у потомка и уезжает наверх)\n * означает мутацию состояния окна во время его собственного патча: Vue уходит\n * в рекурсивный ре-рендер и монтирует заголовок заново, пока не упрётся в\n * лимит. Стабильный id этого класса ошибок не допускает по построению.\n *\n * `null` вне окна — законное состояние: `GrDialogHeader` используют и отдельно.\n */\nexport function useGrModalContext(): GrModalContext | null {\n return inject(GR_MODAL_CONTEXT_KEY, null)\n}\n\n/**\n * Объявляет элемент заголовком окна, пока `active` истинно.\n *\n * Возвращает id для элемента — или `undefined` вне окна: тогда заголовок просто\n * рисуется без связи.\n */\nexport function useGrModalTitle(active: () => boolean): { titleId: string | undefined } {\n const context = useGrModalContext()\n if (!context)\n return { titleId: undefined }\n\n let release: (() => void) | undefined\n\n watch(\n active,\n (isActive) => {\n release?.()\n release = isActive ? context.claimTitle() : undefined\n },\n // `post`: заявка меняет состояние окна, а заголовок — его потомок, и в\n // `pre`-фазе мутация пришлась бы на патч самого окна.\n { immediate: true, flush: 'post' },\n )\n\n onScopeDispose(() => release?.())\n\n return { titleId: context.titleId }\n}\n","<script setup lang=\"ts\">\n/**\n * GrModal — GR-примитив модального окна.\n *\n * Публичный API:\n * - `modelValue` (v-model) — открыто/закрыто;\n * - `size` — размер панели (`sm | md | lg | xl | full`);\n * - `scrollBehavior` — кто скроллится при длинном содержимом;\n * - `closeOnBackdrop` (default: true) — закрывать при клике по оверлею;\n * - `closeOnEsc` (default: true) — закрывать по Esc;\n * - слоты `#title` / `#description` — получают id и связываются с окном через\n * `aria-labelledby` / `aria-describedby`;\n * - слоты `#header` / `#footer` — пустые layout-области вне скроллящегося тела:\n * при `inside` они остаются на месте. Своей разметки примитив в них не\n * добавляет — шапку и подвал рисует `GrDialog`.\n *\n * A11y: **имя у модального слоя обязательно**. Имя берётся из `#title` (или из\n * заголовка `GrDialog`, который заявляет себя тем же контекстом), иначе из\n * пропа `ariaLabel`, иначе — из i18n, чтобы безымянного окна не получилось ни\n * при каком употреблении (axe: `aria-dialog-name`).\n *\n * Механика слоя — `useModalOverlay`: стек (Esc верхнему, `inert` нижним окнам,\n * возврат фокуса), ловушка фокуса, блокировка скролла, портал и присутствие в\n * DOM до конца leave-анимации. Инварианты этой сборки описаны там же.\n */\nimport { useGrComponentProp } from '../GrConfigProvider/context'\nimport { computed, ref, useId, useSlots, watch } from 'vue'\n\nimport { useModalOverlay } from '../../composables/internal/useModalOverlay'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { provideGrModalContext } from './context'\n\nimport {\n type GrModalScrollBehavior,\n type GrModalSize,\n getGrModalPanelClass,\n getGrModalShellClass,\n layoutByScroll,\n overlay as overlayClass,\n overlayTransition,\n panelBodyScrollClass,\n panelSectionClass,\n panelTransition,\n root as rootBaseClass,\n rootClosingClass,\n} from './grModalStyles'\n\nexport type { GrModalScrollBehavior }\n\nexport interface GrModalProps {\n modelValue: boolean\n closeOnBackdrop?: boolean\n closeOnEsc?: boolean\n size?: GrModalSize\n /**\n * Кто скроллится при длинном содержимом: весь оверлей (`outside`) или сама\n * панель (`inside` — окно остаётся на месте, шапка и подвал на виду).\n */\n scrollBehavior?: GrModalScrollBehavior\n /**\n * Доступное имя окна, когда заголовка в слоте `#title` нет. Слот сильнее:\n * при нём имя даёт `aria-labelledby`.\n */\n ariaLabel?: string\n /**\n * Элемент, получающий фокус при открытии. По умолчанию — сама панель: она\n * фокусируема программно (`tabindex=\"-1\"`), диктор объявляет окно целиком, а\n * первый Tab приводит в начало содержимого.\n */\n initialFocus?: HTMLElement | null\n}\n\nexport interface GrModalEmits {\n (e: 'update:modelValue', value: boolean): void\n /** Окно открылось и анимация закончилась. */\n (e: 'opened'): void\n /** Окно закрылось и анимация закончилась — содержимое можно размонтировать. */\n (e: 'closed'): void\n}\n\nimport './defaults'\n\nconst props = withDefaults(defineProps<GrModalProps>(), {\n closeOnBackdrop: true,\n closeOnEsc: true,\n // Дефолт `size` живёт в резолвере ниже, а не здесь: Vue подставил бы его\n // до того, как компонент заглянет в `GrConfigProvider`.\n size: undefined,\n scrollBehavior: 'outside',\n ariaLabel: undefined,\n initialFocus: null,\n})\n\nconst emit = defineEmits<GrModalEmits>()\n\ndefineSlots<{\n default?: () => any\n title?: () => any\n description?: () => any\n /** Закреплённая шапка: при `inside` остаётся на месте, скроллится только тело. */\n header?: () => any\n /** Закреплённый подвал: там же, где и шапка, — вне скроллящегося тела. */\n footer?: () => any\n}>()\n\nconst slots = useSlots()\n\n// Эффективный размер: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentProp('GrModal', 'size', () => props.size, 'md')\n\nconst panelClass = computed(() => getGrModalPanelClass(resolvedSize.value, props.scrollBehavior))\nconst shellClass = computed(() => getGrModalShellClass(resolvedSize.value, props.scrollBehavior))\nconst layoutClass = computed(() => layoutByScroll[props.scrollBehavior])\n\n// Тело скроллится только при `inside`, и тогда же обязано попадать в таб-порядок:\n// длинное содержимое без единого фокусируемого элемента иначе недостижимо с\n// клавиатуры (axe: `scrollable-region-focusable`).\nconst isBodyScrollable = computed(() => props.scrollBehavior === 'inside')\n\n// Панель фокусируема программно (`tabindex=\"-1\"`) — это и приёмник фокуса по\n// умолчанию, и запасной, когда фокусировать внутри нечего.\nconst panelRef = ref<HTMLElement | null>(null)\n\nconst { t } = useGranularityTranslations()\n\n// ————— Имя и описание окна.\n// Id принадлежат окну и раздаются вниз контекстом: заголовок рисует либо слот\n// `#title`, либо `GrDialogHeader` глубже по дереву, и оба лишь заявляют, что\n// отрисовались. Обратное владение (id рождается у заголовка) мутировало бы\n// состояние окна во время его же патча — см. `context.ts`.\nconst titleId = useId()\nconst descriptionId = useId()\n\nconst hasTitle = ref(false)\nconst hasDescription = ref(false)\n\n// Счётчики намеренно не реактивные: наружу торчит только «заголовок есть».\n// Пересоздание содержимого слота меняет счётчик, но не флаг, и лишнего рендера\n// окна не случается.\nlet titleClaims = 0\nlet descriptionClaims = 0\n\nfunction claim(add: () => void, remove: () => void): () => void {\n add()\n let released = false\n return () => {\n if (released)\n return\n released = true\n remove()\n }\n}\n\nfunction claimTitle(): () => void {\n return claim(\n () => {\n titleClaims += 1\n hasTitle.value = true\n },\n () => {\n titleClaims -= 1\n hasTitle.value = titleClaims > 0\n },\n )\n}\n\nfunction claimDescription(): () => void {\n return claim(\n () => {\n descriptionClaims += 1\n hasDescription.value = true\n },\n () => {\n descriptionClaims -= 1\n hasDescription.value = descriptionClaims > 0\n },\n )\n}\n\nprovideGrModalContext({ titleId, descriptionId, claimTitle, claimDescription })\n\n// Собственные слоты заявляются тем же путём, что и заголовок `GrDialog`.\nlet releaseOwnTitle: (() => void) | undefined\nlet releaseOwnDescription: (() => void) | undefined\n\nwatch(\n () => Boolean(slots.title),\n (has) => {\n releaseOwnTitle?.()\n releaseOwnTitle = has ? claimTitle() : undefined\n },\n { immediate: true },\n)\n\nwatch(\n () => Boolean(slots.description),\n (has) => {\n releaseOwnDescription?.()\n releaseOwnDescription = has ? claimDescription() : undefined\n },\n { immediate: true },\n)\n\nconst labelledBy = computed(() => (hasTitle.value ? titleId : undefined))\nconst describedBy = computed(() => (hasDescription.value ? descriptionId : undefined))\n\n// Собственный `aria-label` ставим только в отсутствие заголовка: при обоих\n// атрибутах `aria-labelledby` всё равно сильнее, и второе имя было бы шумом.\nconst ariaLabelAttr = computed(() => {\n if (labelledBy.value)\n return undefined\n return props.ariaLabel || t('gr.modal.title', 'Dialog')\n})\n\n// Обобщённое имя из локали спасает от безымянного окна, но осмысленное имя\n// знает только автор. Предупреждаем при первом открытии, а не при монтировании:\n// закрытая модалка ещё ничего не нарушила, и ругаться на неё рано.\nlet missingNameWarned = false\n\nfunction warnMissingAccessibleName(): void {\n if (missingNameWarned)\n return\n if (!__GR_DEV__)\n return\n if (slots.title || props.ariaLabel)\n return\n\n missingNameWarned = true\n console.warn(\n '[GrModal] Окно без доступного имени: передайте слот #title или проп ariaLabel. '\n + 'Пока используется обобщённое имя из локали.',\n )\n}\n\n// Два заголовка на окно — это разъехавшийся `aria-labelledby`: диктор прочитает\n// первый, а автор будет уверен во втором.\nfunction warnDuplicateTitle(): void {\n if (!__GR_DEV__)\n return\n if (titleClaims <= 1)\n return\n\n console.warn(\n '[GrModal] На окно заявлено несколько заголовков — имя берётся от первого. '\n + 'Оставьте один: слот #title у GrModal либо заголовок GrDialog.',\n )\n}\n\nfunction open(): void {\n emit('update:modelValue', true)\n}\n\nfunction close(): void {\n emit('update:modelValue', false)\n}\n\nfunction toggle(): void {\n emit('update:modelValue', !props.modelValue)\n}\n\n// ————— Модальный слой целиком: стек, ловушка фокуса, `inert`, скролл-лок,\n// портал, присутствие в DOM и guard подложки.\nconst {\n rootEl,\n isMounted,\n isVisible,\n inertAttr,\n layerZIndex,\n portalTarget,\n teleportEnabled,\n themeAttrs,\n onPanelAfterLeave: releasePresence,\n backdrop,\n} = useModalOverlay(computed(() => props.modelValue), close, {\n panel: panelRef,\n closeOnEscape: () => props.closeOnEsc,\n closeOnBackdrop: () => props.closeOnBackdrop,\n // Окно фокусирует саму панель, когда внутри фокусировать нечего и когда\n // потребитель не указал цель явно.\n initialFocus: () => props.initialFocus ?? panelRef.value,\n})\n\n// Поддерево слоя живёт до конца leave-анимации панели, а корень растянут на\n// вьюпорт — значит всё это время он и перехватывает клики.\nconst rootClass = computed(() => [\n rootBaseClass,\n isMounted.value && !isVisible.value ? rootClosingClass : '',\n].filter(Boolean))\n\nfunction onPanelAfterLeave(): void {\n releasePresence()\n emit('closed')\n}\n\nwatch(\n () => props.modelValue,\n (value) => {\n if (!value)\n return\n warnMissingAccessibleName()\n warnDuplicateTitle()\n },\n { immediate: true },\n)\n\ndefineExpose({ open, close, toggle })\n</script>\n\n<template>\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <div\n v-if=\"teleportEnabled && isMounted\"\n ref=\"rootEl\"\n data-gr-overlay-root\n v-bind=\"themeAttrs\"\n role=\"dialog\"\n aria-modal=\"true\"\n :class=\"rootClass\"\n :style=\"{ zIndex: layerZIndex }\"\n :inert=\"inertAttr\"\n :aria-labelledby=\"labelledBy\"\n :aria-describedby=\"describedBy\"\n :aria-label=\"ariaLabelAttr\"\n >\n <div :class=\"shellClass\">\n <div :class=\"layoutClass\">\n <Transition\n appear\n :enter-active-class=\"overlayTransition.enter\"\n :enter-from-class=\"overlayTransition.enterFrom\"\n :enter-to-class=\"overlayTransition.enterTo\"\n :leave-active-class=\"overlayTransition.leave\"\n :leave-from-class=\"overlayTransition.leaveFrom\"\n :leave-to-class=\"overlayTransition.leaveTo\"\n >\n <div\n v-if=\"isVisible\"\n data-gr-modal-overlay\n :class=\"overlayClass\"\n aria-hidden=\"true\"\n v-on=\"backdrop\"\n />\n </Transition>\n\n <!-- `opened`/`closed` вешаем на транзишн панели, а не на корневой:\n анимация живёт здесь, и только её конец что-то значит для\n потребителя, который ждёт момента для размонтирования. -->\n <Transition\n appear\n :enter-active-class=\"panelTransition.enter\"\n :enter-from-class=\"panelTransition.enterFrom\"\n :enter-to-class=\"panelTransition.enterTo\"\n :leave-active-class=\"panelTransition.leave\"\n :leave-from-class=\"panelTransition.leaveFrom\"\n :leave-to-class=\"panelTransition.leaveTo\"\n @after-enter=\"emit('opened')\"\n @after-leave=\"onPanelAfterLeave\"\n >\n <div\n v-if=\"isVisible\"\n ref=\"panelRef\"\n data-gr-modal-panel\n tabindex=\"-1\"\n :class=\"panelClass\"\n >\n <div v-if=\"$slots.title\" :id=\"titleId\" data-gr-modal-title>\n <slot name=\"title\" />\n </div>\n <div v-if=\"$slots.description\" :id=\"descriptionId\" data-gr-modal-description>\n <slot name=\"description\" />\n </div>\n\n <div v-if=\"$slots.header\" data-gr-modal-header :class=\"panelSectionClass\">\n <slot name=\"header\" />\n </div>\n\n <!-- При `inside` тело едет отдельно от шапки и подвала: иначе\n режим ничем не отличался бы от скролла всей панели. -->\n <div\n v-if=\"isBodyScrollable\"\n data-gr-modal-body\n tabindex=\"0\"\n :class=\"panelBodyScrollClass\"\n >\n <slot />\n </div>\n <slot v-else />\n\n <div v-if=\"$slots.footer\" data-gr-modal-footer :class=\"panelSectionClass\">\n <slot name=\"footer\" />\n </div>\n </div>\n </Transition>\n </div>\n </div>\n </div>\n </teleport>\n</template>\n","<script setup lang=\"ts\">\n/**\n * GrModal — GR-примитив модального окна.\n *\n * Публичный API:\n * - `modelValue` (v-model) — открыто/закрыто;\n * - `size` — размер панели (`sm | md | lg | xl | full`);\n * - `scrollBehavior` — кто скроллится при длинном содержимом;\n * - `closeOnBackdrop` (default: true) — закрывать при клике по оверлею;\n * - `closeOnEsc` (default: true) — закрывать по Esc;\n * - слоты `#title` / `#description` — получают id и связываются с окном через\n * `aria-labelledby` / `aria-describedby`;\n * - слоты `#header` / `#footer` — пустые layout-области вне скроллящегося тела:\n * при `inside` они остаются на месте. Своей разметки примитив в них не\n * добавляет — шапку и подвал рисует `GrDialog`.\n *\n * A11y: **имя у модального слоя обязательно**. Имя берётся из `#title` (или из\n * заголовка `GrDialog`, который заявляет себя тем же контекстом), иначе из\n * пропа `ariaLabel`, иначе — из i18n, чтобы безымянного окна не получилось ни\n * при каком употреблении (axe: `aria-dialog-name`).\n *\n * Механика слоя — `useModalOverlay`: стек (Esc верхнему, `inert` нижним окнам,\n * возврат фокуса), ловушка фокуса, блокировка скролла, портал и присутствие в\n * DOM до конца leave-анимации. Инварианты этой сборки описаны там же.\n */\nimport { useGrComponentProp } from '../GrConfigProvider/context'\nimport { computed, ref, useId, useSlots, watch } from 'vue'\n\nimport { useModalOverlay } from '../../composables/internal/useModalOverlay'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { provideGrModalContext } from './context'\n\nimport {\n type GrModalScrollBehavior,\n type GrModalSize,\n getGrModalPanelClass,\n getGrModalShellClass,\n layoutByScroll,\n overlay as overlayClass,\n overlayTransition,\n panelBodyScrollClass,\n panelSectionClass,\n panelTransition,\n root as rootBaseClass,\n rootClosingClass,\n} from './grModalStyles'\n\nexport type { GrModalScrollBehavior }\n\nexport interface GrModalProps {\n modelValue: boolean\n closeOnBackdrop?: boolean\n closeOnEsc?: boolean\n size?: GrModalSize\n /**\n * Кто скроллится при длинном содержимом: весь оверлей (`outside`) или сама\n * панель (`inside` — окно остаётся на месте, шапка и подвал на виду).\n */\n scrollBehavior?: GrModalScrollBehavior\n /**\n * Доступное имя окна, когда заголовка в слоте `#title` нет. Слот сильнее:\n * при нём имя даёт `aria-labelledby`.\n */\n ariaLabel?: string\n /**\n * Элемент, получающий фокус при открытии. По умолчанию — сама панель: она\n * фокусируема программно (`tabindex=\"-1\"`), диктор объявляет окно целиком, а\n * первый Tab приводит в начало содержимого.\n */\n initialFocus?: HTMLElement | null\n}\n\nexport interface GrModalEmits {\n (e: 'update:modelValue', value: boolean): void\n /** Окно открылось и анимация закончилась. */\n (e: 'opened'): void\n /** Окно закрылось и анимация закончилась — содержимое можно размонтировать. */\n (e: 'closed'): void\n}\n\nimport './defaults'\n\nconst props = withDefaults(defineProps<GrModalProps>(), {\n closeOnBackdrop: true,\n closeOnEsc: true,\n // Дефолт `size` живёт в резолвере ниже, а не здесь: Vue подставил бы его\n // до того, как компонент заглянет в `GrConfigProvider`.\n size: undefined,\n scrollBehavior: 'outside',\n ariaLabel: undefined,\n initialFocus: null,\n})\n\nconst emit = defineEmits<GrModalEmits>()\n\ndefineSlots<{\n default?: () => any\n title?: () => any\n description?: () => any\n /** Закреплённая шапка: при `inside` остаётся на месте, скроллится только тело. */\n header?: () => any\n /** Закреплённый подвал: там же, где и шапка, — вне скроллящегося тела. */\n footer?: () => any\n}>()\n\nconst slots = useSlots()\n\n// Эффективный размер: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentProp('GrModal', 'size', () => props.size, 'md')\n\nconst panelClass = computed(() => getGrModalPanelClass(resolvedSize.value, props.scrollBehavior))\nconst shellClass = computed(() => getGrModalShellClass(resolvedSize.value, props.scrollBehavior))\nconst layoutClass = computed(() => layoutByScroll[props.scrollBehavior])\n\n// Тело скроллится только при `inside`, и тогда же обязано попадать в таб-порядок:\n// длинное содержимое без единого фокусируемого элемента иначе недостижимо с\n// клавиатуры (axe: `scrollable-region-focusable`).\nconst isBodyScrollable = computed(() => props.scrollBehavior === 'inside')\n\n// Панель фокусируема программно (`tabindex=\"-1\"`) — это и приёмник фокуса по\n// умолчанию, и запасной, когда фокусировать внутри нечего.\nconst panelRef = ref<HTMLElement | null>(null)\n\nconst { t } = useGranularityTranslations()\n\n// ————— Имя и описание окна.\n// Id принадлежат окну и раздаются вниз контекстом: заголовок рисует либо слот\n// `#title`, либо `GrDialogHeader` глубже по дереву, и оба лишь заявляют, что\n// отрисовались. Обратное владение (id рождается у заголовка) мутировало бы\n// состояние окна во время его же патча — см. `context.ts`.\nconst titleId = useId()\nconst descriptionId = useId()\n\nconst hasTitle = ref(false)\nconst hasDescription = ref(false)\n\n// Счётчики намеренно не реактивные: наружу торчит только «заголовок есть».\n// Пересоздание содержимого слота меняет счётчик, но не флаг, и лишнего рендера\n// окна не случается.\nlet titleClaims = 0\nlet descriptionClaims = 0\n\nfunction claim(add: () => void, remove: () => void): () => void {\n add()\n let released = false\n return () => {\n if (released)\n return\n released = true\n remove()\n }\n}\n\nfunction claimTitle(): () => void {\n return claim(\n () => {\n titleClaims += 1\n hasTitle.value = true\n },\n () => {\n titleClaims -= 1\n hasTitle.value = titleClaims > 0\n },\n )\n}\n\nfunction claimDescription(): () => void {\n return claim(\n () => {\n descriptionClaims += 1\n hasDescription.value = true\n },\n () => {\n descriptionClaims -= 1\n hasDescription.value = descriptionClaims > 0\n },\n )\n}\n\nprovideGrModalContext({ titleId, descriptionId, claimTitle, claimDescription })\n\n// Собственные слоты заявляются тем же путём, что и заголовок `GrDialog`.\nlet releaseOwnTitle: (() => void) | undefined\nlet releaseOwnDescription: (() => void) | undefined\n\nwatch(\n () => Boolean(slots.title),\n (has) => {\n releaseOwnTitle?.()\n releaseOwnTitle = has ? claimTitle() : undefined\n },\n { immediate: true },\n)\n\nwatch(\n () => Boolean(slots.description),\n (has) => {\n releaseOwnDescription?.()\n releaseOwnDescription = has ? claimDescription() : undefined\n },\n { immediate: true },\n)\n\nconst labelledBy = computed(() => (hasTitle.value ? titleId : undefined))\nconst describedBy = computed(() => (hasDescription.value ? descriptionId : undefined))\n\n// Собственный `aria-label` ставим только в отсутствие заголовка: при обоих\n// атрибутах `aria-labelledby` всё равно сильнее, и второе имя было бы шумом.\nconst ariaLabelAttr = computed(() => {\n if (labelledBy.value)\n return undefined\n return props.ariaLabel || t('gr.modal.title', 'Dialog')\n})\n\n// Обобщённое имя из локали спасает от безымянного окна, но осмысленное имя\n// знает только автор. Предупреждаем при первом открытии, а не при монтировании:\n// закрытая модалка ещё ничего не нарушила, и ругаться на неё рано.\nlet missingNameWarned = false\n\nfunction warnMissingAccessibleName(): void {\n if (missingNameWarned)\n return\n if (!__GR_DEV__)\n return\n if (slots.title || props.ariaLabel)\n return\n\n missingNameWarned = true\n console.warn(\n '[GrModal] Окно без доступного имени: передайте слот #title или проп ariaLabel. '\n + 'Пока используется обобщённое имя из локали.',\n )\n}\n\n// Два заголовка на окно — это разъехавшийся `aria-labelledby`: диктор прочитает\n// первый, а автор будет уверен во втором.\nfunction warnDuplicateTitle(): void {\n if (!__GR_DEV__)\n return\n if (titleClaims <= 1)\n return\n\n console.warn(\n '[GrModal] На окно заявлено несколько заголовков — имя берётся от первого. '\n + 'Оставьте один: слот #title у GrModal либо заголовок GrDialog.',\n )\n}\n\nfunction open(): void {\n emit('update:modelValue', true)\n}\n\nfunction close(): void {\n emit('update:modelValue', false)\n}\n\nfunction toggle(): void {\n emit('update:modelValue', !props.modelValue)\n}\n\n// ————— Модальный слой целиком: стек, ловушка фокуса, `inert`, скролл-лок,\n// портал, присутствие в DOM и guard подложки.\nconst {\n rootEl,\n isMounted,\n isVisible,\n inertAttr,\n layerZIndex,\n portalTarget,\n teleportEnabled,\n themeAttrs,\n onPanelAfterLeave: releasePresence,\n backdrop,\n} = useModalOverlay(computed(() => props.modelValue), close, {\n panel: panelRef,\n closeOnEscape: () => props.closeOnEsc,\n closeOnBackdrop: () => props.closeOnBackdrop,\n // Окно фокусирует саму панель, когда внутри фокусировать нечего и когда\n // потребитель не указал цель явно.\n initialFocus: () => props.initialFocus ?? panelRef.value,\n})\n\n// Поддерево слоя живёт до конца leave-анимации панели, а корень растянут на\n// вьюпорт — значит всё это время он и перехватывает клики.\nconst rootClass = computed(() => [\n rootBaseClass,\n isMounted.value && !isVisible.value ? rootClosingClass : '',\n].filter(Boolean))\n\nfunction onPanelAfterLeave(): void {\n releasePresence()\n emit('closed')\n}\n\nwatch(\n () => props.modelValue,\n (value) => {\n if (!value)\n return\n warnMissingAccessibleName()\n warnDuplicateTitle()\n },\n { immediate: true },\n)\n\ndefineExpose({ open, close, toggle })\n</script>\n\n<template>\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <div\n v-if=\"teleportEnabled && isMounted\"\n ref=\"rootEl\"\n data-gr-overlay-root\n v-bind=\"themeAttrs\"\n role=\"dialog\"\n aria-modal=\"true\"\n :class=\"rootClass\"\n :style=\"{ zIndex: layerZIndex }\"\n :inert=\"inertAttr\"\n :aria-labelledby=\"labelledBy\"\n :aria-describedby=\"describedBy\"\n :aria-label=\"ariaLabelAttr\"\n >\n <div :class=\"shellClass\">\n <div :class=\"layoutClass\">\n <Transition\n appear\n :enter-active-class=\"overlayTransition.enter\"\n :enter-from-class=\"overlayTransition.enterFrom\"\n :enter-to-class=\"overlayTransition.enterTo\"\n :leave-active-class=\"overlayTransition.leave\"\n :leave-from-class=\"overlayTransition.leaveFrom\"\n :leave-to-class=\"overlayTransition.leaveTo\"\n >\n <div\n v-if=\"isVisible\"\n data-gr-modal-overlay\n :class=\"overlayClass\"\n aria-hidden=\"true\"\n v-on=\"backdrop\"\n />\n </Transition>\n\n <!-- `opened`/`closed` вешаем на транзишн панели, а не на корневой:\n анимация живёт здесь, и только её конец что-то значит для\n потребителя, который ждёт момента для размонтирования. -->\n <Transition\n appear\n :enter-active-class=\"panelTransition.enter\"\n :enter-from-class=\"panelTransition.enterFrom\"\n :enter-to-class=\"panelTransition.enterTo\"\n :leave-active-class=\"panelTransition.leave\"\n :leave-from-class=\"panelTransition.leaveFrom\"\n :leave-to-class=\"panelTransition.leaveTo\"\n @after-enter=\"emit('opened')\"\n @after-leave=\"onPanelAfterLeave\"\n >\n <div\n v-if=\"isVisible\"\n ref=\"panelRef\"\n data-gr-modal-panel\n tabindex=\"-1\"\n :class=\"panelClass\"\n >\n <div v-if=\"$slots.title\" :id=\"titleId\" data-gr-modal-title>\n <slot name=\"title\" />\n </div>\n <div v-if=\"$slots.description\" :id=\"descriptionId\" data-gr-modal-description>\n <slot name=\"description\" />\n </div>\n\n <div v-if=\"$slots.header\" data-gr-modal-header :class=\"panelSectionClass\">\n <slot name=\"header\" />\n </div>\n\n <!-- При `inside` тело едет отдельно от шапки и подвала: иначе\n режим ничем не отличался бы от скролла всей панели. -->\n <div\n v-if=\"isBodyScrollable\"\n data-gr-modal-body\n tabindex=\"0\"\n :class=\"panelBodyScrollClass\"\n >\n <slot />\n </div>\n <slot v-else />\n\n <div v-if=\"$slots.footer\" data-gr-modal-footer :class=\"panelSectionClass\">\n <slot name=\"footer\" />\n </div>\n </div>\n </Transition>\n </div>\n </div>\n </div>\n </teleport>\n</template>\n"],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { t as GrModal_default } from "./chunks/GrModal-
|
|
2
|
-
import { n as grModalSafelist, t as grModalConfig } from "../../chunks/config-
|
|
1
|
+
import { t as GrModal_default } from "./chunks/GrModal-DixnhJcu.js";
|
|
2
|
+
import { n as grModalSafelist, t as grModalConfig } from "../../chunks/config-DBi0GVce.js";
|
|
3
3
|
export { GrModal_default as GrModal, GrModal_default as default, grModalConfig, grModalSafelist };
|