@feugene/granularity 0.29.0 → 0.29.1
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-_jO3q1Le.js → config-3bQeQvqd.js} +1 -1
- package/dist/chunks/{config-_jO3q1Le.js.map → config-3bQeQvqd.js.map} +1 -1
- package/dist/chunks/{grIconStyles-B6BRqUld.js → grIconStyles-BwBrJY1E.js} +8 -5
- package/dist/chunks/grIconStyles-BwBrJY1E.js.map +1 -0
- package/dist/chunks/{granular-provider-Dds_519x.js → granular-provider-NtErwzYB.js} +1 -1
- package/dist/chunks/{granular-provider-Dds_519x.js.map → granular-provider-NtErwzYB.js.map} +1 -1
- package/dist/components/GrAutocomplete/chunks/{GrAutocomplete-BwhUTqmG.js → GrAutocomplete-CaHys_A_.js} +2 -2
- package/dist/components/GrAutocomplete/chunks/GrAutocomplete-CaHys_A_.js.map +1 -0
- package/dist/components/GrAutocomplete/index.js +1 -1
- package/dist/components/GrAutocomplete/styles.css +1 -1
- package/dist/components/GrBreadcrumbs/chunks/{GrBreadcrumbs-BKOvl8W1.js → GrBreadcrumbs-DRDNan_n.js} +1 -1
- package/dist/components/GrBreadcrumbs/chunks/{GrBreadcrumbs-BKOvl8W1.js.map → GrBreadcrumbs-DRDNan_n.js.map} +1 -1
- package/dist/components/GrBreadcrumbs/index.js +1 -1
- package/dist/components/GrCollapse/chunks/{GrCollapse-CytiA2Sx.js → GrCollapse-Bw9_JatF.js} +1 -1
- package/dist/components/GrCollapse/chunks/{GrCollapse-CytiA2Sx.js.map → GrCollapse-Bw9_JatF.js.map} +1 -1
- package/dist/components/GrCollapse/index.js +1 -1
- package/dist/components/GrDataTable/chunks/{GrDataTable-DU00V44y.js → GrDataTable-C8a9_nCg.js} +1 -1
- package/dist/components/GrDataTable/chunks/{GrDataTable-DU00V44y.js.map → GrDataTable-C8a9_nCg.js.map} +1 -1
- package/dist/components/GrDataTable/index.js +1 -1
- package/dist/components/GrDrawer/chunks/{GrDrawer-BCreL-eq.js → GrDrawer-B7PiAcbU.js} +1 -1
- package/dist/components/GrDrawer/chunks/{GrDrawer-BCreL-eq.js.map → GrDrawer-B7PiAcbU.js.map} +1 -1
- package/dist/components/GrDrawer/index.js +1 -1
- package/dist/components/GrEmptyState/chunks/{GrEmptyState-D4T5pzXA.js → GrEmptyState-RqsWvgI9.js} +1 -1
- package/dist/components/GrEmptyState/chunks/{GrEmptyState-D4T5pzXA.js.map → GrEmptyState-RqsWvgI9.js.map} +1 -1
- package/dist/components/GrEmptyState/index.js +1 -1
- package/dist/components/GrFileUpload/chunks/{GrFileUpload-FjbaYGjL.js → GrFileUpload-NsHkRRIa.js} +1 -1
- package/dist/components/GrFileUpload/chunks/{GrFileUpload-FjbaYGjL.js.map → GrFileUpload-NsHkRRIa.js.map} +1 -1
- package/dist/components/GrFileUpload/index.js +1 -1
- package/dist/components/GrFormFile/chunks/{GrFormFile-ChdKlN83.js → GrFormFile-CYtXU9HV.js} +1 -1
- package/dist/components/GrFormFile/chunks/{GrFormFile-ChdKlN83.js.map → GrFormFile-CYtXU9HV.js.map} +1 -1
- package/dist/components/GrFormFile/index.js +1 -1
- package/dist/components/GrIcon/chunks/{GrIcon-Bn1XEriL.js → GrIcon-Dmf0c9ba.js} +1 -1
- package/dist/components/GrIcon/chunks/{GrIcon-Bn1XEriL.js.map → GrIcon-Dmf0c9ba.js.map} +1 -1
- package/dist/components/GrIcon/index.js +2 -2
- package/dist/components/GrImageViewer/chunks/{GrImageViewer-COUcpOgR.js → GrImageViewer-CyVN6Zpc.js} +1 -1
- package/dist/components/GrImageViewer/chunks/{GrImageViewer-COUcpOgR.js.map → GrImageViewer-CyVN6Zpc.js.map} +1 -1
- package/dist/components/GrImageViewer/index.js +1 -1
- package/dist/components/GrInputTag/chunks/{GrInputTag-LP3smqOv.js → GrInputTag-5wVcB0zG.js} +1 -1
- package/dist/components/GrInputTag/chunks/{GrInputTag-LP3smqOv.js.map → GrInputTag-5wVcB0zG.js.map} +1 -1
- package/dist/components/GrInputTag/index.js +1 -1
- package/dist/components/GrLink/chunks/{GrLink-DAGCFYO7.js → GrLink-DYXw3W9w.js} +1 -1
- package/dist/components/GrLink/chunks/{GrLink-DAGCFYO7.js.map → GrLink-DYXw3W9w.js.map} +1 -1
- package/dist/components/GrLink/index.js +1 -1
- package/dist/components/GrLoading/chunks/{GrLoading-DDD8WAIH.js → GrLoading-CMD3Ri9P.js} +1 -1
- package/dist/components/GrLoading/chunks/{GrLoading-DDD8WAIH.js.map → GrLoading-CMD3Ri9P.js.map} +1 -1
- package/dist/components/GrLoading/index.js +1 -1
- package/dist/components/GrNavbar/chunks/{GrNavbar-DhswlYyh.js → GrNavbar-C5EjMKaC.js} +1 -1
- package/dist/components/GrNavbar/chunks/{GrNavbar-DhswlYyh.js.map → GrNavbar-C5EjMKaC.js.map} +1 -1
- package/dist/components/GrNavbar/index.js +1 -1
- package/dist/components/GrNumberInput/chunks/{GrNumberInput-Daud9Jdm.js → GrNumberInput-DQ59YICu.js} +1 -1
- package/dist/components/GrNumberInput/chunks/{GrNumberInput-Daud9Jdm.js.map → GrNumberInput-DQ59YICu.js.map} +1 -1
- package/dist/components/GrNumberInput/index.js +1 -1
- package/dist/components/GrPagination/chunks/{GrPagination-kQhWlfs-.js → GrPagination-CX9ZV8mR.js} +1 -1
- package/dist/components/GrPagination/chunks/{GrPagination-kQhWlfs-.js.map → GrPagination-CX9ZV8mR.js.map} +1 -1
- package/dist/components/GrPagination/index.js +1 -1
- package/dist/components/GrSelect/chunks/{GrSelect-D-KU9Lx_.js → GrSelect-DKoQu3w9.js} +3 -3
- package/dist/components/GrSelect/chunks/{GrSelect-D-KU9Lx_.js.map → GrSelect-DKoQu3w9.js.map} +1 -1
- package/dist/components/GrSelect/index.js +1 -1
- package/dist/components/GrSelect/styles.css +1 -1
- package/dist/components/GrSidebar/chunks/{GrSidebar-DxPhIiWs.js → GrSidebar-B8EQIlfW.js} +1 -1
- package/dist/components/GrSidebar/chunks/{GrSidebar-DxPhIiWs.js.map → GrSidebar-B8EQIlfW.js.map} +1 -1
- package/dist/components/GrSidebar/index.js +1 -1
- package/dist/components/GrToaster/chunks/{GrToaster-CUi3ldJ0.js → GrToaster-Dp7-4B8k.js} +1 -1
- package/dist/components/GrToaster/chunks/{GrToaster-CUi3ldJ0.js.map → GrToaster-Dp7-4B8k.js.map} +1 -1
- package/dist/components/GrToaster/index.js +1 -1
- package/dist/components/GrTooltip/chunks/{GrTooltip-BKFjqu0E.js → GrTooltip-uYETdbCD.js} +1 -1
- package/dist/components/GrTooltip/chunks/{GrTooltip-BKFjqu0E.js.map → GrTooltip-uYETdbCD.js.map} +1 -1
- package/dist/components/GrTooltip/index.js +1 -1
- package/dist/components/GrTreeSelect/chunks/{GrTreeSelect-B1GdEn5K.js → GrTreeSelect-CymO68Cv.js} +1 -1
- package/dist/components/GrTreeSelect/chunks/GrTreeSelect-CymO68Cv.js.map +1 -0
- package/dist/components/GrTreeSelect/index.js +1 -1
- package/dist/directives/loading.js +1 -1
- package/dist/granular-provider-node.js +1 -1
- package/dist/granular-provider.js +1 -1
- package/dist/index.js +22 -22
- package/dist/types/components/GrIcon/grIconStyles.d.ts +7 -4
- package/dist/web-types.json +1 -1
- package/package.json +2 -2
- package/dist/chunks/grIconStyles-B6BRqUld.js.map +0 -1
- package/dist/components/GrAutocomplete/chunks/GrAutocomplete-BwhUTqmG.js.map +0 -1
- package/dist/components/GrTreeSelect/chunks/GrTreeSelect-B1GdEn5K.js.map +0 -1
package/dist/components/GrInputTag/chunks/{GrInputTag-LP3smqOv.js.map → GrInputTag-5wVcB0zG.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GrInputTag-LP3smqOv.js","names":[],"sources":["../../../../src/components/GrInputTag/GrInputTag.vue","../../../../src/components/GrInputTag/GrInputTag.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, ref } from 'vue'\n\nimport type { GrBadgeRadius, GrBadgeSize, GrBadgeTone } from '../GrBadge'\nimport GrChip from '../GrChip/GrChip.vue'\nimport { chipSizeForBadgeScale } from '../GrChip/grChipStyles'\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport IconClose from '~icons/lucide/x'\nimport IconLoader from '~icons/lucide/loader-2'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { useControlAddons } from '../../composables/internal/useControlAddons'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useAnnouncer } from '../../composables/useAnnouncer'\nimport { useRovingFocus } from '../../composables/useRovingFocus'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useFocusWithin } from '../../composables/internal/useFocusWithin'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { isComposingEvent } from '../../internal/keyboard'\n\nimport {\n clearButtonClass,\n grInputTagInputClass,\n grInputTagWrapperClass,\n spinnerClass,\n type GrInputTagSize,\n type GrInputTagState,\n} from './grInputTagStyles'\n\nexport type { GrInputTagSize, GrInputTagState } from './grInputTagStyles'\n\nconst REGEX_SPECIAL_CHAR_RE = /[.*+?^${}()|[\\]\\\\]/g\n\n/**\n * Props for {@link GrInputTag}.\n * Tag-input primitive: renders a set of badges with an inline `<input>` for entering new tags.\n * Splits input by configurable `separators`, supports paste, Enter and Backspace editing.\n */\nexport interface GrInputTagProps {\n modelValue: string[]\n placeholder?: string\n disabled?: boolean\n readonly?: boolean\n invalid?: boolean\n /** Обязательное поле (`aria-required`). Складывается с `required` у `GrFormField`. */\n required?: boolean\n state?: GrInputTagState\n size?: GrInputTagSize\n separators?: string[]\n allowDuplicates?: boolean\n trim?: boolean\n max?: number\n addOnBlur?: boolean\n clearInputOnAdd?: boolean\n /**\n * Проверка тега перед добавлением. Может быть асинхронной (проверка на\n * сервере). Отклонённый тег не добавляется и уходит в событие `reject`.\n */\n beforeAdd?: (tag: string) => boolean | Promise<boolean>\n /** Кнопка «снести все теги». Настраивается через `GrConfigProvider`. */\n clearable?: boolean\n /** Фоновая работа: спиннер + `aria-busy`. Асинхронный `beforeAdd` поднимает его сам. */\n loading?: boolean\n tagTone?: GrBadgeTone\n tagDark?: boolean\n tagSize?: GrBadgeSize\n tagRadius?: GrBadgeRadius\n tagClosable?: boolean\n /** i18n-friendly aria-label for the per-tag remove button. */\n removeTagLabel?: string\n /** i18n aria-label кнопки «снести все». */\n clearAllLabel?: string\n /**\n * Имя контрола, когда он используется вне `GrFormField`. Внутри поля имя даёт\n * связка с его `<label for>` — там проп не нужен. Без того и другого у инпута\n * нет доступного имени вовсе: placeholder именем не считается.\n */\n ariaLabel?: string\n /** Имя для нативной формы: hidden input на каждый тег. */\n name?: string\n /**\n * Ширины аддонов `prefix`/`suffix` — общий контракт контролов пакета\n * (`docs/form-controls.md`).\n */\n prefixMinWidth?: string\n prefixMaxWidth?: string\n suffixMinWidth?: string\n suffixMaxWidth?: string\n prefixFixed?: boolean\n suffixFixed?: boolean\n}\n\nexport interface GrInputTagEmits {\n (e: 'update:modelValue', value: string[]): void\n (e: 'add', value: string): void\n (e: 'remove', value: string, index: number): void\n /** Тег не прошёл `beforeAdd`. */\n (e: 'reject', value: string): void\n /** Набор снесён кнопкой «очистить». */\n (e: 'clear'): void\n /** Набор тегов изменился. */\n (e: 'change', value: string[]): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrInputTagProps>(),\n {\n placeholder: undefined,\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n state: 'default',\n // Настраивается через `GrConfigProvider`; дефолт — в резолвере ниже.\n size: undefined,\n separators: () => [','],\n allowDuplicates: false,\n trim: true,\n max: undefined,\n addOnBlur: false,\n clearInputOnAdd: true,\n beforeAdd: undefined,\n clearable: undefined,\n loading: false,\n tagTone: 'neutral',\n tagDark: false,\n tagSize: 'md',\n tagRadius: 'round',\n tagClosable: true,\n removeTagLabel: undefined,\n clearAllLabel: undefined,\n ariaLabel: undefined,\n name: undefined,\n },\n)\n\nconst emit = defineEmits<GrInputTagEmits>()\ndefineSlots<{\n /** Аддон слева от чипов: иконка, метка. */\n prefix?: () => any\n /** Аддон справа, перед кнопкой очистки. */\n suffix?: () => any\n /** Свой чип: `remove` снимает значение. */\n tag?: (props: { tag: string, index: number, remove: () => void }) => any\n}>()\n\n\n// Fallback из контекста `GrFormField` (id/aria-describedby/invalid/required) —\n// как у GrInput и GrSelect: контрол не знает про форму, знает только про поле.\nconst field = useGrFormFieldContext()\nconst resolvedId = computed(() => field?.id.value)\nconst describedBy = computed(() => field?.describedById.value)\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n} = useGrFormControl(() => props)\n\n// Эффективные значения: локальный проп → `GrConfigProvider` → дефолт компонента.\n/** Ширина аддона по умолчанию — та же лестница, что у `GrInput`. */\nconst ADDON_MIN_WIDTH_BY_SIZE: Record<GrInputTagSize, string> = {\n xs: '2rem',\n sm: '2.25rem',\n md: '2.5rem',\n lg: '3rem',\n}\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrInputTag' })\n\n/**\n * Аддоны — флекс-соседи поля: оболочка тегового поля и так флекс-строка с\n * бейджами, поэтому места паддингом резервировать не нужно.\n */\nconst { hasPrefix, hasSuffix, prefixEl, suffixEl, prefixStyle, suffixStyle } = useControlAddons(() => props, {\n defaultMinWidth: () => ADDON_MIN_WIDTH_BY_SIZE[resolvedSize.value],\n paddingX: () => '0px',\n})\nconst resolvedClearable = useGrComponentProp('GrInputTag', 'clearable', () => props.clearable, false)\n\nconst { t } = useGranularityTranslations()\nconst resolvedClearAllLabel = computed(() => props.clearAllLabel ?? t('gr.inputTag.clearAll', 'Clear all tags'))\n\nfunction removeTagLabelFor(tag: string): string {\n // Безликое «Remove tag» на двадцати кнопках подряд не даёт выбрать нужную:\n // имя обязано называть сам тег.\n return props.removeTagLabel ?? t('gr.inputTag.removeTagNamed', 'Remove tag {tag}', { tag })\n}\n\nconst inputValue = ref('')\nconst inputEl = ref<HTMLInputElement | null>(null)\nconst rootEl = ref<HTMLElement | null>(null)\n\n// Внутри корня фокус ходит между полем и кнопками удаления чипов: без границы\n// каждое такое перемещение давало бы потребителю пару `blur` + `focus`.\nconst { onFocusIn, onFocusOut } = useFocusWithin(rootEl, {\n enter: event => emit('focus', event),\n leave: event => emit('blur', event),\n})\n/**\n * Инстансы чипов: у кольца цель — кнопка снятия внутри чипа, и достаётся она\n * через `defineExpose` (`removeEl`), а не селектором по DOM.\n */\nconst chipRefs = ref<Array<{ removeEl: HTMLButtonElement | null } | null>>([])\n\nfunction setChipRef(index: number) {\n return (el: unknown): void => {\n chipRefs.value[index] = el as { removeEl: HTMLButtonElement | null } | null\n }\n}\n\n/**\n * Проп `tagSize` объявлен по шкале бейджа и остаётся публичным контрактом:\n * `md` обязан означать для потребителя тот же кегль, что и раньше.\n */\nconst chipSize = computed(() => chipSizeForBadgeScale(props.tagSize))\n\nfunction focus(): void {\n inputEl.value?.focus()\n}\n\nfunction blur(): void {\n inputEl.value?.blur()\n}\n\nconst isMaxed = computed(() => props.max !== undefined && props.modelValue.length >= props.max)\nconst canEdit = computed(() => !isDisabled.value && !isReadonly.value)\nconst showRemove = computed(() => props.tagClosable && canEdit.value)\n\n// Добавление, удаление и достигнутый предел — события, а не состояние виджета:\n// показывать их негде, объявить нужно. Это работа общего живого региона.\nconst { announce } = useAnnouncer()\n\n/**\n * Roving tabindex по крестикам: двадцать тегов давали двадцать одну остановку\n * `Tab`. В таб-порядке ровно один крестик, между ними ходят стрелками.\n *\n * Адресация индексом, а не значением тега: теги допускают повторы, и по\n * значению два одинаковых чипа было бы не различить.\n */\nconst tagIndexes = computed(() => props.modelValue.map((_, index) => index))\n\nconst roving = useRovingFocus<number>({\n items: () => tagIndexes.value,\n elementFor: index => chipRefs.value[index]?.removeEl,\n orientation: () => 'horizontal',\n // Ряд чипов — не кольцо: у него есть край, и за краем стоит поле ввода.\n wrap: () => false,\n onOverflow: (edge) => {\n // За последним чипом — поле ввода: продолжение того же ряда. Слева от\n // первого — начало строки, фокус остаётся на месте.\n if (edge === 'end') focus()\n return true\n },\n // Удаление чипа перерисовывает ряд: без ожидания фокус уехал бы на узел,\n // которого уже нет.\n beforeFocus: () => nextTick(),\n})\n\nasync function focusTag(index: number): Promise<void> {\n const last = props.modelValue.length - 1\n if (last < 0) {\n focus()\n return\n }\n\n await roving.focusKey(Math.min(Math.max(index, 0), last))\n}\n\nfunction escapeRegexChar(ch: string): string {\n return ch.replace(REGEX_SPECIAL_CHAR_RE, '\\\\$&')\n}\n\nconst splitRegex = computed(() => {\n const seps = (props.separators ?? []).filter(Boolean).map(s => s[0]!)\n const uniq = [...new Set([...seps, '\\n', '\\r', '\\t'])]\n const body = uniq.map(escapeRegexChar).join('')\n return new RegExp(`[${body}]+`, 'g')\n})\n\nfunction normalizeTag(raw: string): string {\n return props.trim ? raw.trim() : raw\n}\n\nfunction splitToTags(raw: string): string[] {\n return raw\n .split(splitRegex.value)\n .map(v => normalizeTag(v))\n .filter(Boolean)\n}\n\nfunction addMany(rawTags: string[]): boolean {\n if (!canEdit.value || isMaxed.value)\n return false\n\n const next = props.modelValue.slice()\n const added: string[] = []\n\n for (const raw of rawTags) {\n const tag = normalizeTag(raw)\n if (!tag)\n continue\n\n if (!props.allowDuplicates && next.includes(tag))\n continue\n\n if (props.max !== undefined && next.length >= props.max)\n break\n\n next.push(tag)\n added.push(tag)\n emit('add', tag)\n }\n\n if (!added.length)\n return false\n\n emit('update:modelValue', next)\n emit('change', next)\n announce(added.length === 1\n ? t('gr.inputTag.added', 'Tag added: {tag}', { tag: added[0]! })\n : t('gr.inputTag.addedMany', '{count} tags added', { count: added.length }))\n\n return true\n}\n\n// Счётчик запуска: пока идёт асинхронная проверка, пользователь успевает нажать\n// Enter второй раз — результат устаревшей проверки дописывать нельзя.\nlet validationRun = 0\nconst validating = ref(false)\nconst isBusy = computed(() => props.loading || validating.value)\n\nasync function filterAllowed(tags: string[], run: number): Promise<string[] | null> {\n const check = props.beforeAdd\n if (!check)\n return tags\n\n const allowed: string[] = []\n\n for (const tag of tags) {\n const ok = await check(tag)\n if (run !== validationRun)\n return null\n\n if (ok)\n allowed.push(tag)\n else\n emit('reject', tag)\n }\n\n return allowed\n}\n\nasync function commitInput(): Promise<void> {\n if (!canEdit.value || !inputValue.value)\n return\n\n if (isMaxed.value) {\n announce(t('gr.inputTag.limitReached', 'Tag limit reached'))\n return\n }\n\n const raw = inputValue.value\n const tags = splitToTags(raw)\n if (!tags.length)\n return\n\n const run = ++validationRun\n validating.value = Boolean(props.beforeAdd)\n\n const allowed = await filterAllowed(tags, run)\n\n if (run === validationRun)\n validating.value = false\n\n if (allowed === null || !allowed.length)\n return\n\n const changed = addMany(allowed)\n // Значение могло измениться, пока шла проверка: стирать чужой ввод нельзя.\n if (changed && props.clearInputOnAdd && inputValue.value === raw)\n inputValue.value = ''\n}\n\nfunction removeAt(index: number): void {\n if (!canEdit.value)\n return\n\n const next = props.modelValue.slice()\n const removed = next[index]\n if (removed === undefined)\n return\n\n next.splice(index, 1)\n emit('update:modelValue', next)\n emit('change', next)\n emit('remove', removed, index)\n announce(t('gr.inputTag.removed', 'Tag removed: {tag}', { tag: removed }))\n}\n\n/** Удаление с клавиатуры: фокус переезжает на соседний чип, а не пропадает. */\nasync function removeAtFromKeyboard(index: number): Promise<void> {\n removeAt(index)\n await nextTick()\n\n if (props.modelValue.length === 0) {\n focus()\n return\n }\n\n await focusTag(Math.min(index, props.modelValue.length - 1))\n}\n\nfunction clearAll(): void {\n if (!canEdit.value || props.modelValue.length === 0)\n return\n\n emit('update:modelValue', [])\n emit('change', [])\n emit('clear')\n announce(t('gr.inputTag.cleared', 'All tags removed'))\n focus()\n}\n\nfunction onInput(e: Event): void {\n inputValue.value = (e.target as HTMLInputElement).value\n}\n\nfunction onKeydown(e: KeyboardEvent): void {\n // Enter/сепаратор во время IME-композиции коммитят её, а не тег.\n if (isComposingEvent(e))\n return\n if (!canEdit.value)\n return\n\n if (e.key === 'Enter') {\n e.preventDefault()\n void commitInput()\n return\n }\n\n if ((props.separators ?? []).includes(e.key)) {\n e.preventDefault()\n void commitInput()\n return\n }\n\n if (inputValue.value.length === 0 && props.modelValue.length > 0) {\n if (e.key === 'Backspace') {\n e.preventDefault()\n removeAt(props.modelValue.length - 1)\n return\n }\n\n // Из пустого поля стрелка влево уводит на последний чип — дальше по чипам\n // работают те же стрелки, `Tab` для этого не нужен.\n if (e.key === 'ArrowLeft') {\n e.preventDefault()\n void focusTag(props.modelValue.length - 1)\n }\n }\n}\n\nfunction onTagKeydown(e: KeyboardEvent, index: number): void {\n if (roving.handleNavigationKeys(e)) {\n return\n }\n\n if (e.key === 'Delete' || e.key === 'Backspace') {\n e.preventDefault()\n void removeAtFromKeyboard(index)\n }\n}\n\nfunction onBlur(): void {\n if (props.addOnBlur)\n void commitInput()\n}\n\nfunction onPaste(e: ClipboardEvent): void {\n if (!canEdit.value || isMaxed.value)\n return\n\n const pasted = e.clipboardData?.getData('text')\n if (!pasted)\n return\n\n const tags = splitToTags(pasted)\n if (tags.length <= 1)\n return\n\n e.preventDefault()\n\n const run = ++validationRun\n validating.value = Boolean(props.beforeAdd)\n\n void filterAllowed(tags, run).then((allowed) => {\n if (run === validationRun)\n validating.value = false\n\n if (allowed?.length)\n addMany(allowed)\n })\n}\n\nconst wrapperClassName = computed(() => {\n return grInputTagWrapperClass({\n size: resolvedSize.value,\n state: props.state,\n invalid: isInvalid.value,\n disabled: isDisabled.value,\n })\n})\n\nconst inputClassName = computed(() => grInputTagInputClass(resolvedSize.value))\n\nconst showClearAll = computed(() => resolvedClearable.value && canEdit.value && props.modelValue.length > 0)\n\nconst placeholderText = computed(() => props.modelValue.length > 0 ? undefined : props.placeholder)\n\ndefineExpose({ focus, blur, clear: clearAll })\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n data-gr-input-tag\n data-testid=\"gr-input-tag\"\n :class=\"wrapperClassName\"\n @click=\"focus\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n >\n <span\n v-if=\"hasPrefix\"\n ref=\"prefixEl\"\n data-gr-input-tag-prefix\n class=\"inline-flex shrink-0 items-center justify-center text-[var(--gr-muted-fg)]\"\n :style=\"prefixStyle\"\n >\n <slot name=\"prefix\" />\n </span>\n\n <!-- Нативная форма: hidden на каждый тег — стандартная сериализация набора. -->\n <template v-if=\"name\">\n <input\n v-for=\"(tag, tagIndex) in modelValue\"\n :key=\"`hidden-${tagIndex}`\"\n type=\"hidden\"\n :name=\"name\"\n :value=\"tag\"\n >\n </template>\n <!--\n `display: contents` — список нужен ради роли, а не ради раскладки: чипы\n обязаны переноситься в одном потоке с полем ввода.\n -->\n <span v-if=\"modelValue.length\" role=\"list\" class=\"contents\">\n <GrChip\n v-for=\"(tag, i) in modelValue\"\n :key=\"`${tag}-${i}`\"\n :ref=\"setChipRef(i)\"\n role=\"listitem\"\n :tone=\"tagTone\"\n :dark=\"tagDark\"\n :size=\"chipSize\"\n :radius=\"tagRadius\"\n :closable=\"showRemove\"\n :remove-label=\"removeTagLabelFor(tag)\"\n :remove-tabindex=\"roving.tabindexFor(i)\"\n data-gr-input-tag-item\n data-testid=\"gr-input-tag-item\"\n :data-index=\"i\"\n @remove=\"removeAt(i)\"\n @focusin=\"roving.setActive(i)\"\n @keydown=\"onTagKeydown($event, i)\"\n >\n <slot name=\"tag\" :tag=\"tag\" :index=\"i\" :remove=\"() => removeAt(i)\">\n <span class=\"truncate max-w-[18rem]\">{{ tag }}</span>\n </slot>\n </GrChip>\n </span>\n\n <input\n :id=\"resolvedId\"\n ref=\"inputEl\"\n data-gr-input-tag-input\n data-testid=\"gr-input-tag-input\"\n :value=\"inputValue\"\n :disabled=\"isDisabled\"\n :readonly=\"isReadonly\"\n :placeholder=\"placeholderText\"\n :aria-label=\"ariaLabel\"\n :aria-describedby=\"describedBy\"\n :aria-required=\"isRequired ? 'true' : undefined\"\n :aria-readonly=\"isReadonly ? 'true' : undefined\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-busy=\"isBusy ? 'true' : undefined\"\n class=\"flex-1 min-w-[120px] bg-transparent border-none outline-none placeholder:text-[var(--gr-muted-fg)]\"\n :class=\"inputClassName\"\n @input=\"onInput\"\n @keydown=\"onKeydown\"\n @blur=\"onBlur\"\n @paste=\"onPaste\"\n >\n\n <span\n v-if=\"hasSuffix\"\n ref=\"suffixEl\"\n data-gr-input-tag-suffix\n class=\"inline-flex shrink-0 items-center justify-center text-[var(--gr-muted-fg)]\"\n :style=\"suffixStyle\"\n >\n <slot name=\"suffix\" />\n </span>\n\n <span\n v-if=\"isBusy\"\n data-gr-input-tag-spinner\n :class=\"spinnerClass\"\n aria-hidden=\"true\"\n >\n <GrIcon size=\"sm\">\n <IconLoader class=\"animate-spin\" />\n </GrIcon>\n </span>\n\n <button\n v-if=\"showClearAll\"\n type=\"button\"\n data-gr-input-tag-clear\n :class=\"clearButtonClass\"\n :aria-label=\"resolvedClearAllLabel\"\n @mousedown.prevent.stop\n @click.stop=\"clearAll\"\n >\n <GrIcon size=\"sm\">\n <IconClose />\n </GrIcon>\n </button>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, nextTick, ref } from 'vue'\n\nimport type { GrBadgeRadius, GrBadgeSize, GrBadgeTone } from '../GrBadge'\nimport GrChip from '../GrChip/GrChip.vue'\nimport { chipSizeForBadgeScale } from '../GrChip/grChipStyles'\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport IconClose from '~icons/lucide/x'\nimport IconLoader from '~icons/lucide/loader-2'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { useControlAddons } from '../../composables/internal/useControlAddons'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useAnnouncer } from '../../composables/useAnnouncer'\nimport { useRovingFocus } from '../../composables/useRovingFocus'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useFocusWithin } from '../../composables/internal/useFocusWithin'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { isComposingEvent } from '../../internal/keyboard'\n\nimport {\n clearButtonClass,\n grInputTagInputClass,\n grInputTagWrapperClass,\n spinnerClass,\n type GrInputTagSize,\n type GrInputTagState,\n} from './grInputTagStyles'\n\nexport type { GrInputTagSize, GrInputTagState } from './grInputTagStyles'\n\nconst REGEX_SPECIAL_CHAR_RE = /[.*+?^${}()|[\\]\\\\]/g\n\n/**\n * Props for {@link GrInputTag}.\n * Tag-input primitive: renders a set of badges with an inline `<input>` for entering new tags.\n * Splits input by configurable `separators`, supports paste, Enter and Backspace editing.\n */\nexport interface GrInputTagProps {\n modelValue: string[]\n placeholder?: string\n disabled?: boolean\n readonly?: boolean\n invalid?: boolean\n /** Обязательное поле (`aria-required`). Складывается с `required` у `GrFormField`. */\n required?: boolean\n state?: GrInputTagState\n size?: GrInputTagSize\n separators?: string[]\n allowDuplicates?: boolean\n trim?: boolean\n max?: number\n addOnBlur?: boolean\n clearInputOnAdd?: boolean\n /**\n * Проверка тега перед добавлением. Может быть асинхронной (проверка на\n * сервере). Отклонённый тег не добавляется и уходит в событие `reject`.\n */\n beforeAdd?: (tag: string) => boolean | Promise<boolean>\n /** Кнопка «снести все теги». Настраивается через `GrConfigProvider`. */\n clearable?: boolean\n /** Фоновая работа: спиннер + `aria-busy`. Асинхронный `beforeAdd` поднимает его сам. */\n loading?: boolean\n tagTone?: GrBadgeTone\n tagDark?: boolean\n tagSize?: GrBadgeSize\n tagRadius?: GrBadgeRadius\n tagClosable?: boolean\n /** i18n-friendly aria-label for the per-tag remove button. */\n removeTagLabel?: string\n /** i18n aria-label кнопки «снести все». */\n clearAllLabel?: string\n /**\n * Имя контрола, когда он используется вне `GrFormField`. Внутри поля имя даёт\n * связка с его `<label for>` — там проп не нужен. Без того и другого у инпута\n * нет доступного имени вовсе: placeholder именем не считается.\n */\n ariaLabel?: string\n /** Имя для нативной формы: hidden input на каждый тег. */\n name?: string\n /**\n * Ширины аддонов `prefix`/`suffix` — общий контракт контролов пакета\n * (`docs/form-controls.md`).\n */\n prefixMinWidth?: string\n prefixMaxWidth?: string\n suffixMinWidth?: string\n suffixMaxWidth?: string\n prefixFixed?: boolean\n suffixFixed?: boolean\n}\n\nexport interface GrInputTagEmits {\n (e: 'update:modelValue', value: string[]): void\n (e: 'add', value: string): void\n (e: 'remove', value: string, index: number): void\n /** Тег не прошёл `beforeAdd`. */\n (e: 'reject', value: string): void\n /** Набор снесён кнопкой «очистить». */\n (e: 'clear'): void\n /** Набор тегов изменился. */\n (e: 'change', value: string[]): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrInputTagProps>(),\n {\n placeholder: undefined,\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n state: 'default',\n // Настраивается через `GrConfigProvider`; дефолт — в резолвере ниже.\n size: undefined,\n separators: () => [','],\n allowDuplicates: false,\n trim: true,\n max: undefined,\n addOnBlur: false,\n clearInputOnAdd: true,\n beforeAdd: undefined,\n clearable: undefined,\n loading: false,\n tagTone: 'neutral',\n tagDark: false,\n tagSize: 'md',\n tagRadius: 'round',\n tagClosable: true,\n removeTagLabel: undefined,\n clearAllLabel: undefined,\n ariaLabel: undefined,\n name: undefined,\n },\n)\n\nconst emit = defineEmits<GrInputTagEmits>()\ndefineSlots<{\n /** Аддон слева от чипов: иконка, метка. */\n prefix?: () => any\n /** Аддон справа, перед кнопкой очистки. */\n suffix?: () => any\n /** Свой чип: `remove` снимает значение. */\n tag?: (props: { tag: string, index: number, remove: () => void }) => any\n}>()\n\n\n// Fallback из контекста `GrFormField` (id/aria-describedby/invalid/required) —\n// как у GrInput и GrSelect: контрол не знает про форму, знает только про поле.\nconst field = useGrFormFieldContext()\nconst resolvedId = computed(() => field?.id.value)\nconst describedBy = computed(() => field?.describedById.value)\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n} = useGrFormControl(() => props)\n\n// Эффективные значения: локальный проп → `GrConfigProvider` → дефолт компонента.\n/** Ширина аддона по умолчанию — та же лестница, что у `GrInput`. */\nconst ADDON_MIN_WIDTH_BY_SIZE: Record<GrInputTagSize, string> = {\n xs: '2rem',\n sm: '2.25rem',\n md: '2.5rem',\n lg: '3rem',\n}\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrInputTag' })\n\n/**\n * Аддоны — флекс-соседи поля: оболочка тегового поля и так флекс-строка с\n * бейджами, поэтому места паддингом резервировать не нужно.\n */\nconst { hasPrefix, hasSuffix, prefixEl, suffixEl, prefixStyle, suffixStyle } = useControlAddons(() => props, {\n defaultMinWidth: () => ADDON_MIN_WIDTH_BY_SIZE[resolvedSize.value],\n paddingX: () => '0px',\n})\nconst resolvedClearable = useGrComponentProp('GrInputTag', 'clearable', () => props.clearable, false)\n\nconst { t } = useGranularityTranslations()\nconst resolvedClearAllLabel = computed(() => props.clearAllLabel ?? t('gr.inputTag.clearAll', 'Clear all tags'))\n\nfunction removeTagLabelFor(tag: string): string {\n // Безликое «Remove tag» на двадцати кнопках подряд не даёт выбрать нужную:\n // имя обязано называть сам тег.\n return props.removeTagLabel ?? t('gr.inputTag.removeTagNamed', 'Remove tag {tag}', { tag })\n}\n\nconst inputValue = ref('')\nconst inputEl = ref<HTMLInputElement | null>(null)\nconst rootEl = ref<HTMLElement | null>(null)\n\n// Внутри корня фокус ходит между полем и кнопками удаления чипов: без границы\n// каждое такое перемещение давало бы потребителю пару `blur` + `focus`.\nconst { onFocusIn, onFocusOut } = useFocusWithin(rootEl, {\n enter: event => emit('focus', event),\n leave: event => emit('blur', event),\n})\n/**\n * Инстансы чипов: у кольца цель — кнопка снятия внутри чипа, и достаётся она\n * через `defineExpose` (`removeEl`), а не селектором по DOM.\n */\nconst chipRefs = ref<Array<{ removeEl: HTMLButtonElement | null } | null>>([])\n\nfunction setChipRef(index: number) {\n return (el: unknown): void => {\n chipRefs.value[index] = el as { removeEl: HTMLButtonElement | null } | null\n }\n}\n\n/**\n * Проп `tagSize` объявлен по шкале бейджа и остаётся публичным контрактом:\n * `md` обязан означать для потребителя тот же кегль, что и раньше.\n */\nconst chipSize = computed(() => chipSizeForBadgeScale(props.tagSize))\n\nfunction focus(): void {\n inputEl.value?.focus()\n}\n\nfunction blur(): void {\n inputEl.value?.blur()\n}\n\nconst isMaxed = computed(() => props.max !== undefined && props.modelValue.length >= props.max)\nconst canEdit = computed(() => !isDisabled.value && !isReadonly.value)\nconst showRemove = computed(() => props.tagClosable && canEdit.value)\n\n// Добавление, удаление и достигнутый предел — события, а не состояние виджета:\n// показывать их негде, объявить нужно. Это работа общего живого региона.\nconst { announce } = useAnnouncer()\n\n/**\n * Roving tabindex по крестикам: двадцать тегов давали двадцать одну остановку\n * `Tab`. В таб-порядке ровно один крестик, между ними ходят стрелками.\n *\n * Адресация индексом, а не значением тега: теги допускают повторы, и по\n * значению два одинаковых чипа было бы не различить.\n */\nconst tagIndexes = computed(() => props.modelValue.map((_, index) => index))\n\nconst roving = useRovingFocus<number>({\n items: () => tagIndexes.value,\n elementFor: index => chipRefs.value[index]?.removeEl,\n orientation: () => 'horizontal',\n // Ряд чипов — не кольцо: у него есть край, и за краем стоит поле ввода.\n wrap: () => false,\n onOverflow: (edge) => {\n // За последним чипом — поле ввода: продолжение того же ряда. Слева от\n // первого — начало строки, фокус остаётся на месте.\n if (edge === 'end') focus()\n return true\n },\n // Удаление чипа перерисовывает ряд: без ожидания фокус уехал бы на узел,\n // которого уже нет.\n beforeFocus: () => nextTick(),\n})\n\nasync function focusTag(index: number): Promise<void> {\n const last = props.modelValue.length - 1\n if (last < 0) {\n focus()\n return\n }\n\n await roving.focusKey(Math.min(Math.max(index, 0), last))\n}\n\nfunction escapeRegexChar(ch: string): string {\n return ch.replace(REGEX_SPECIAL_CHAR_RE, '\\\\$&')\n}\n\nconst splitRegex = computed(() => {\n const seps = (props.separators ?? []).filter(Boolean).map(s => s[0]!)\n const uniq = [...new Set([...seps, '\\n', '\\r', '\\t'])]\n const body = uniq.map(escapeRegexChar).join('')\n return new RegExp(`[${body}]+`, 'g')\n})\n\nfunction normalizeTag(raw: string): string {\n return props.trim ? raw.trim() : raw\n}\n\nfunction splitToTags(raw: string): string[] {\n return raw\n .split(splitRegex.value)\n .map(v => normalizeTag(v))\n .filter(Boolean)\n}\n\nfunction addMany(rawTags: string[]): boolean {\n if (!canEdit.value || isMaxed.value)\n return false\n\n const next = props.modelValue.slice()\n const added: string[] = []\n\n for (const raw of rawTags) {\n const tag = normalizeTag(raw)\n if (!tag)\n continue\n\n if (!props.allowDuplicates && next.includes(tag))\n continue\n\n if (props.max !== undefined && next.length >= props.max)\n break\n\n next.push(tag)\n added.push(tag)\n emit('add', tag)\n }\n\n if (!added.length)\n return false\n\n emit('update:modelValue', next)\n emit('change', next)\n announce(added.length === 1\n ? t('gr.inputTag.added', 'Tag added: {tag}', { tag: added[0]! })\n : t('gr.inputTag.addedMany', '{count} tags added', { count: added.length }))\n\n return true\n}\n\n// Счётчик запуска: пока идёт асинхронная проверка, пользователь успевает нажать\n// Enter второй раз — результат устаревшей проверки дописывать нельзя.\nlet validationRun = 0\nconst validating = ref(false)\nconst isBusy = computed(() => props.loading || validating.value)\n\nasync function filterAllowed(tags: string[], run: number): Promise<string[] | null> {\n const check = props.beforeAdd\n if (!check)\n return tags\n\n const allowed: string[] = []\n\n for (const tag of tags) {\n const ok = await check(tag)\n if (run !== validationRun)\n return null\n\n if (ok)\n allowed.push(tag)\n else\n emit('reject', tag)\n }\n\n return allowed\n}\n\nasync function commitInput(): Promise<void> {\n if (!canEdit.value || !inputValue.value)\n return\n\n if (isMaxed.value) {\n announce(t('gr.inputTag.limitReached', 'Tag limit reached'))\n return\n }\n\n const raw = inputValue.value\n const tags = splitToTags(raw)\n if (!tags.length)\n return\n\n const run = ++validationRun\n validating.value = Boolean(props.beforeAdd)\n\n const allowed = await filterAllowed(tags, run)\n\n if (run === validationRun)\n validating.value = false\n\n if (allowed === null || !allowed.length)\n return\n\n const changed = addMany(allowed)\n // Значение могло измениться, пока шла проверка: стирать чужой ввод нельзя.\n if (changed && props.clearInputOnAdd && inputValue.value === raw)\n inputValue.value = ''\n}\n\nfunction removeAt(index: number): void {\n if (!canEdit.value)\n return\n\n const next = props.modelValue.slice()\n const removed = next[index]\n if (removed === undefined)\n return\n\n next.splice(index, 1)\n emit('update:modelValue', next)\n emit('change', next)\n emit('remove', removed, index)\n announce(t('gr.inputTag.removed', 'Tag removed: {tag}', { tag: removed }))\n}\n\n/** Удаление с клавиатуры: фокус переезжает на соседний чип, а не пропадает. */\nasync function removeAtFromKeyboard(index: number): Promise<void> {\n removeAt(index)\n await nextTick()\n\n if (props.modelValue.length === 0) {\n focus()\n return\n }\n\n await focusTag(Math.min(index, props.modelValue.length - 1))\n}\n\nfunction clearAll(): void {\n if (!canEdit.value || props.modelValue.length === 0)\n return\n\n emit('update:modelValue', [])\n emit('change', [])\n emit('clear')\n announce(t('gr.inputTag.cleared', 'All tags removed'))\n focus()\n}\n\nfunction onInput(e: Event): void {\n inputValue.value = (e.target as HTMLInputElement).value\n}\n\nfunction onKeydown(e: KeyboardEvent): void {\n // Enter/сепаратор во время IME-композиции коммитят её, а не тег.\n if (isComposingEvent(e))\n return\n if (!canEdit.value)\n return\n\n if (e.key === 'Enter') {\n e.preventDefault()\n void commitInput()\n return\n }\n\n if ((props.separators ?? []).includes(e.key)) {\n e.preventDefault()\n void commitInput()\n return\n }\n\n if (inputValue.value.length === 0 && props.modelValue.length > 0) {\n if (e.key === 'Backspace') {\n e.preventDefault()\n removeAt(props.modelValue.length - 1)\n return\n }\n\n // Из пустого поля стрелка влево уводит на последний чип — дальше по чипам\n // работают те же стрелки, `Tab` для этого не нужен.\n if (e.key === 'ArrowLeft') {\n e.preventDefault()\n void focusTag(props.modelValue.length - 1)\n }\n }\n}\n\nfunction onTagKeydown(e: KeyboardEvent, index: number): void {\n if (roving.handleNavigationKeys(e)) {\n return\n }\n\n if (e.key === 'Delete' || e.key === 'Backspace') {\n e.preventDefault()\n void removeAtFromKeyboard(index)\n }\n}\n\nfunction onBlur(): void {\n if (props.addOnBlur)\n void commitInput()\n}\n\nfunction onPaste(e: ClipboardEvent): void {\n if (!canEdit.value || isMaxed.value)\n return\n\n const pasted = e.clipboardData?.getData('text')\n if (!pasted)\n return\n\n const tags = splitToTags(pasted)\n if (tags.length <= 1)\n return\n\n e.preventDefault()\n\n const run = ++validationRun\n validating.value = Boolean(props.beforeAdd)\n\n void filterAllowed(tags, run).then((allowed) => {\n if (run === validationRun)\n validating.value = false\n\n if (allowed?.length)\n addMany(allowed)\n })\n}\n\nconst wrapperClassName = computed(() => {\n return grInputTagWrapperClass({\n size: resolvedSize.value,\n state: props.state,\n invalid: isInvalid.value,\n disabled: isDisabled.value,\n })\n})\n\nconst inputClassName = computed(() => grInputTagInputClass(resolvedSize.value))\n\nconst showClearAll = computed(() => resolvedClearable.value && canEdit.value && props.modelValue.length > 0)\n\nconst placeholderText = computed(() => props.modelValue.length > 0 ? undefined : props.placeholder)\n\ndefineExpose({ focus, blur, clear: clearAll })\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n data-gr-input-tag\n data-testid=\"gr-input-tag\"\n :class=\"wrapperClassName\"\n @click=\"focus\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n >\n <span\n v-if=\"hasPrefix\"\n ref=\"prefixEl\"\n data-gr-input-tag-prefix\n class=\"inline-flex shrink-0 items-center justify-center text-[var(--gr-muted-fg)]\"\n :style=\"prefixStyle\"\n >\n <slot name=\"prefix\" />\n </span>\n\n <!-- Нативная форма: hidden на каждый тег — стандартная сериализация набора. -->\n <template v-if=\"name\">\n <input\n v-for=\"(tag, tagIndex) in modelValue\"\n :key=\"`hidden-${tagIndex}`\"\n type=\"hidden\"\n :name=\"name\"\n :value=\"tag\"\n >\n </template>\n <!--\n `display: contents` — список нужен ради роли, а не ради раскладки: чипы\n обязаны переноситься в одном потоке с полем ввода.\n -->\n <span v-if=\"modelValue.length\" role=\"list\" class=\"contents\">\n <GrChip\n v-for=\"(tag, i) in modelValue\"\n :key=\"`${tag}-${i}`\"\n :ref=\"setChipRef(i)\"\n role=\"listitem\"\n :tone=\"tagTone\"\n :dark=\"tagDark\"\n :size=\"chipSize\"\n :radius=\"tagRadius\"\n :closable=\"showRemove\"\n :remove-label=\"removeTagLabelFor(tag)\"\n :remove-tabindex=\"roving.tabindexFor(i)\"\n data-gr-input-tag-item\n data-testid=\"gr-input-tag-item\"\n :data-index=\"i\"\n @remove=\"removeAt(i)\"\n @focusin=\"roving.setActive(i)\"\n @keydown=\"onTagKeydown($event, i)\"\n >\n <slot name=\"tag\" :tag=\"tag\" :index=\"i\" :remove=\"() => removeAt(i)\">\n <span class=\"truncate max-w-[18rem]\">{{ tag }}</span>\n </slot>\n </GrChip>\n </span>\n\n <input\n :id=\"resolvedId\"\n ref=\"inputEl\"\n data-gr-input-tag-input\n data-testid=\"gr-input-tag-input\"\n :value=\"inputValue\"\n :disabled=\"isDisabled\"\n :readonly=\"isReadonly\"\n :placeholder=\"placeholderText\"\n :aria-label=\"ariaLabel\"\n :aria-describedby=\"describedBy\"\n :aria-required=\"isRequired ? 'true' : undefined\"\n :aria-readonly=\"isReadonly ? 'true' : undefined\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-busy=\"isBusy ? 'true' : undefined\"\n class=\"flex-1 min-w-[120px] bg-transparent border-none outline-none placeholder:text-[var(--gr-muted-fg)]\"\n :class=\"inputClassName\"\n @input=\"onInput\"\n @keydown=\"onKeydown\"\n @blur=\"onBlur\"\n @paste=\"onPaste\"\n >\n\n <span\n v-if=\"hasSuffix\"\n ref=\"suffixEl\"\n data-gr-input-tag-suffix\n class=\"inline-flex shrink-0 items-center justify-center text-[var(--gr-muted-fg)]\"\n :style=\"suffixStyle\"\n >\n <slot name=\"suffix\" />\n </span>\n\n <span\n v-if=\"isBusy\"\n data-gr-input-tag-spinner\n :class=\"spinnerClass\"\n aria-hidden=\"true\"\n >\n <GrIcon size=\"sm\">\n <IconLoader class=\"animate-spin\" />\n </GrIcon>\n </span>\n\n <button\n v-if=\"showClearAll\"\n type=\"button\"\n data-gr-input-tag-clear\n :class=\"clearButtonClass\"\n :aria-label=\"resolvedClearAllLabel\"\n @mousedown.prevent.stop\n @click.stop=\"clearAll\"\n >\n <GrIcon size=\"sm\">\n <IconClose />\n </GrIcon>\n </button>\n </div>\n</template>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"GrInputTag-5wVcB0zG.js","names":[],"sources":["../../../../src/components/GrInputTag/GrInputTag.vue","../../../../src/components/GrInputTag/GrInputTag.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, ref } from 'vue'\n\nimport type { GrBadgeRadius, GrBadgeSize, GrBadgeTone } from '../GrBadge'\nimport GrChip from '../GrChip/GrChip.vue'\nimport { chipSizeForBadgeScale } from '../GrChip/grChipStyles'\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport IconClose from '~icons/lucide/x'\nimport IconLoader from '~icons/lucide/loader-2'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { useControlAddons } from '../../composables/internal/useControlAddons'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useAnnouncer } from '../../composables/useAnnouncer'\nimport { useRovingFocus } from '../../composables/useRovingFocus'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useFocusWithin } from '../../composables/internal/useFocusWithin'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { isComposingEvent } from '../../internal/keyboard'\n\nimport {\n clearButtonClass,\n grInputTagInputClass,\n grInputTagWrapperClass,\n spinnerClass,\n type GrInputTagSize,\n type GrInputTagState,\n} from './grInputTagStyles'\n\nexport type { GrInputTagSize, GrInputTagState } from './grInputTagStyles'\n\nconst REGEX_SPECIAL_CHAR_RE = /[.*+?^${}()|[\\]\\\\]/g\n\n/**\n * Props for {@link GrInputTag}.\n * Tag-input primitive: renders a set of badges with an inline `<input>` for entering new tags.\n * Splits input by configurable `separators`, supports paste, Enter and Backspace editing.\n */\nexport interface GrInputTagProps {\n modelValue: string[]\n placeholder?: string\n disabled?: boolean\n readonly?: boolean\n invalid?: boolean\n /** Обязательное поле (`aria-required`). Складывается с `required` у `GrFormField`. */\n required?: boolean\n state?: GrInputTagState\n size?: GrInputTagSize\n separators?: string[]\n allowDuplicates?: boolean\n trim?: boolean\n max?: number\n addOnBlur?: boolean\n clearInputOnAdd?: boolean\n /**\n * Проверка тега перед добавлением. Может быть асинхронной (проверка на\n * сервере). Отклонённый тег не добавляется и уходит в событие `reject`.\n */\n beforeAdd?: (tag: string) => boolean | Promise<boolean>\n /** Кнопка «снести все теги». Настраивается через `GrConfigProvider`. */\n clearable?: boolean\n /** Фоновая работа: спиннер + `aria-busy`. Асинхронный `beforeAdd` поднимает его сам. */\n loading?: boolean\n tagTone?: GrBadgeTone\n tagDark?: boolean\n tagSize?: GrBadgeSize\n tagRadius?: GrBadgeRadius\n tagClosable?: boolean\n /** i18n-friendly aria-label for the per-tag remove button. */\n removeTagLabel?: string\n /** i18n aria-label кнопки «снести все». */\n clearAllLabel?: string\n /**\n * Имя контрола, когда он используется вне `GrFormField`. Внутри поля имя даёт\n * связка с его `<label for>` — там проп не нужен. Без того и другого у инпута\n * нет доступного имени вовсе: placeholder именем не считается.\n */\n ariaLabel?: string\n /** Имя для нативной формы: hidden input на каждый тег. */\n name?: string\n /**\n * Ширины аддонов `prefix`/`suffix` — общий контракт контролов пакета\n * (`docs/form-controls.md`).\n */\n prefixMinWidth?: string\n prefixMaxWidth?: string\n suffixMinWidth?: string\n suffixMaxWidth?: string\n prefixFixed?: boolean\n suffixFixed?: boolean\n}\n\nexport interface GrInputTagEmits {\n (e: 'update:modelValue', value: string[]): void\n (e: 'add', value: string): void\n (e: 'remove', value: string, index: number): void\n /** Тег не прошёл `beforeAdd`. */\n (e: 'reject', value: string): void\n /** Набор снесён кнопкой «очистить». */\n (e: 'clear'): void\n /** Набор тегов изменился. */\n (e: 'change', value: string[]): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrInputTagProps>(),\n {\n placeholder: undefined,\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n state: 'default',\n // Настраивается через `GrConfigProvider`; дефолт — в резолвере ниже.\n size: undefined,\n separators: () => [','],\n allowDuplicates: false,\n trim: true,\n max: undefined,\n addOnBlur: false,\n clearInputOnAdd: true,\n beforeAdd: undefined,\n clearable: undefined,\n loading: false,\n tagTone: 'neutral',\n tagDark: false,\n tagSize: 'md',\n tagRadius: 'round',\n tagClosable: true,\n removeTagLabel: undefined,\n clearAllLabel: undefined,\n ariaLabel: undefined,\n name: undefined,\n },\n)\n\nconst emit = defineEmits<GrInputTagEmits>()\ndefineSlots<{\n /** Аддон слева от чипов: иконка, метка. */\n prefix?: () => any\n /** Аддон справа, перед кнопкой очистки. */\n suffix?: () => any\n /** Свой чип: `remove` снимает значение. */\n tag?: (props: { tag: string, index: number, remove: () => void }) => any\n}>()\n\n\n// Fallback из контекста `GrFormField` (id/aria-describedby/invalid/required) —\n// как у GrInput и GrSelect: контрол не знает про форму, знает только про поле.\nconst field = useGrFormFieldContext()\nconst resolvedId = computed(() => field?.id.value)\nconst describedBy = computed(() => field?.describedById.value)\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n} = useGrFormControl(() => props)\n\n// Эффективные значения: локальный проп → `GrConfigProvider` → дефолт компонента.\n/** Ширина аддона по умолчанию — та же лестница, что у `GrInput`. */\nconst ADDON_MIN_WIDTH_BY_SIZE: Record<GrInputTagSize, string> = {\n xs: '2rem',\n sm: '2.25rem',\n md: '2.5rem',\n lg: '3rem',\n}\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrInputTag' })\n\n/**\n * Аддоны — флекс-соседи поля: оболочка тегового поля и так флекс-строка с\n * бейджами, поэтому места паддингом резервировать не нужно.\n */\nconst { hasPrefix, hasSuffix, prefixEl, suffixEl, prefixStyle, suffixStyle } = useControlAddons(() => props, {\n defaultMinWidth: () => ADDON_MIN_WIDTH_BY_SIZE[resolvedSize.value],\n paddingX: () => '0px',\n})\nconst resolvedClearable = useGrComponentProp('GrInputTag', 'clearable', () => props.clearable, false)\n\nconst { t } = useGranularityTranslations()\nconst resolvedClearAllLabel = computed(() => props.clearAllLabel ?? t('gr.inputTag.clearAll', 'Clear all tags'))\n\nfunction removeTagLabelFor(tag: string): string {\n // Безликое «Remove tag» на двадцати кнопках подряд не даёт выбрать нужную:\n // имя обязано называть сам тег.\n return props.removeTagLabel ?? t('gr.inputTag.removeTagNamed', 'Remove tag {tag}', { tag })\n}\n\nconst inputValue = ref('')\nconst inputEl = ref<HTMLInputElement | null>(null)\nconst rootEl = ref<HTMLElement | null>(null)\n\n// Внутри корня фокус ходит между полем и кнопками удаления чипов: без границы\n// каждое такое перемещение давало бы потребителю пару `blur` + `focus`.\nconst { onFocusIn, onFocusOut } = useFocusWithin(rootEl, {\n enter: event => emit('focus', event),\n leave: event => emit('blur', event),\n})\n/**\n * Инстансы чипов: у кольца цель — кнопка снятия внутри чипа, и достаётся она\n * через `defineExpose` (`removeEl`), а не селектором по DOM.\n */\nconst chipRefs = ref<Array<{ removeEl: HTMLButtonElement | null } | null>>([])\n\nfunction setChipRef(index: number) {\n return (el: unknown): void => {\n chipRefs.value[index] = el as { removeEl: HTMLButtonElement | null } | null\n }\n}\n\n/**\n * Проп `tagSize` объявлен по шкале бейджа и остаётся публичным контрактом:\n * `md` обязан означать для потребителя тот же кегль, что и раньше.\n */\nconst chipSize = computed(() => chipSizeForBadgeScale(props.tagSize))\n\nfunction focus(): void {\n inputEl.value?.focus()\n}\n\nfunction blur(): void {\n inputEl.value?.blur()\n}\n\nconst isMaxed = computed(() => props.max !== undefined && props.modelValue.length >= props.max)\nconst canEdit = computed(() => !isDisabled.value && !isReadonly.value)\nconst showRemove = computed(() => props.tagClosable && canEdit.value)\n\n// Добавление, удаление и достигнутый предел — события, а не состояние виджета:\n// показывать их негде, объявить нужно. Это работа общего живого региона.\nconst { announce } = useAnnouncer()\n\n/**\n * Roving tabindex по крестикам: двадцать тегов давали двадцать одну остановку\n * `Tab`. В таб-порядке ровно один крестик, между ними ходят стрелками.\n *\n * Адресация индексом, а не значением тега: теги допускают повторы, и по\n * значению два одинаковых чипа было бы не различить.\n */\nconst tagIndexes = computed(() => props.modelValue.map((_, index) => index))\n\nconst roving = useRovingFocus<number>({\n items: () => tagIndexes.value,\n elementFor: index => chipRefs.value[index]?.removeEl,\n orientation: () => 'horizontal',\n // Ряд чипов — не кольцо: у него есть край, и за краем стоит поле ввода.\n wrap: () => false,\n onOverflow: (edge) => {\n // За последним чипом — поле ввода: продолжение того же ряда. Слева от\n // первого — начало строки, фокус остаётся на месте.\n if (edge === 'end') focus()\n return true\n },\n // Удаление чипа перерисовывает ряд: без ожидания фокус уехал бы на узел,\n // которого уже нет.\n beforeFocus: () => nextTick(),\n})\n\nasync function focusTag(index: number): Promise<void> {\n const last = props.modelValue.length - 1\n if (last < 0) {\n focus()\n return\n }\n\n await roving.focusKey(Math.min(Math.max(index, 0), last))\n}\n\nfunction escapeRegexChar(ch: string): string {\n return ch.replace(REGEX_SPECIAL_CHAR_RE, '\\\\$&')\n}\n\nconst splitRegex = computed(() => {\n const seps = (props.separators ?? []).filter(Boolean).map(s => s[0]!)\n const uniq = [...new Set([...seps, '\\n', '\\r', '\\t'])]\n const body = uniq.map(escapeRegexChar).join('')\n return new RegExp(`[${body}]+`, 'g')\n})\n\nfunction normalizeTag(raw: string): string {\n return props.trim ? raw.trim() : raw\n}\n\nfunction splitToTags(raw: string): string[] {\n return raw\n .split(splitRegex.value)\n .map(v => normalizeTag(v))\n .filter(Boolean)\n}\n\nfunction addMany(rawTags: string[]): boolean {\n if (!canEdit.value || isMaxed.value)\n return false\n\n const next = props.modelValue.slice()\n const added: string[] = []\n\n for (const raw of rawTags) {\n const tag = normalizeTag(raw)\n if (!tag)\n continue\n\n if (!props.allowDuplicates && next.includes(tag))\n continue\n\n if (props.max !== undefined && next.length >= props.max)\n break\n\n next.push(tag)\n added.push(tag)\n emit('add', tag)\n }\n\n if (!added.length)\n return false\n\n emit('update:modelValue', next)\n emit('change', next)\n announce(added.length === 1\n ? t('gr.inputTag.added', 'Tag added: {tag}', { tag: added[0]! })\n : t('gr.inputTag.addedMany', '{count} tags added', { count: added.length }))\n\n return true\n}\n\n// Счётчик запуска: пока идёт асинхронная проверка, пользователь успевает нажать\n// Enter второй раз — результат устаревшей проверки дописывать нельзя.\nlet validationRun = 0\nconst validating = ref(false)\nconst isBusy = computed(() => props.loading || validating.value)\n\nasync function filterAllowed(tags: string[], run: number): Promise<string[] | null> {\n const check = props.beforeAdd\n if (!check)\n return tags\n\n const allowed: string[] = []\n\n for (const tag of tags) {\n const ok = await check(tag)\n if (run !== validationRun)\n return null\n\n if (ok)\n allowed.push(tag)\n else\n emit('reject', tag)\n }\n\n return allowed\n}\n\nasync function commitInput(): Promise<void> {\n if (!canEdit.value || !inputValue.value)\n return\n\n if (isMaxed.value) {\n announce(t('gr.inputTag.limitReached', 'Tag limit reached'))\n return\n }\n\n const raw = inputValue.value\n const tags = splitToTags(raw)\n if (!tags.length)\n return\n\n const run = ++validationRun\n validating.value = Boolean(props.beforeAdd)\n\n const allowed = await filterAllowed(tags, run)\n\n if (run === validationRun)\n validating.value = false\n\n if (allowed === null || !allowed.length)\n return\n\n const changed = addMany(allowed)\n // Значение могло измениться, пока шла проверка: стирать чужой ввод нельзя.\n if (changed && props.clearInputOnAdd && inputValue.value === raw)\n inputValue.value = ''\n}\n\nfunction removeAt(index: number): void {\n if (!canEdit.value)\n return\n\n const next = props.modelValue.slice()\n const removed = next[index]\n if (removed === undefined)\n return\n\n next.splice(index, 1)\n emit('update:modelValue', next)\n emit('change', next)\n emit('remove', removed, index)\n announce(t('gr.inputTag.removed', 'Tag removed: {tag}', { tag: removed }))\n}\n\n/** Удаление с клавиатуры: фокус переезжает на соседний чип, а не пропадает. */\nasync function removeAtFromKeyboard(index: number): Promise<void> {\n removeAt(index)\n await nextTick()\n\n if (props.modelValue.length === 0) {\n focus()\n return\n }\n\n await focusTag(Math.min(index, props.modelValue.length - 1))\n}\n\nfunction clearAll(): void {\n if (!canEdit.value || props.modelValue.length === 0)\n return\n\n emit('update:modelValue', [])\n emit('change', [])\n emit('clear')\n announce(t('gr.inputTag.cleared', 'All tags removed'))\n focus()\n}\n\nfunction onInput(e: Event): void {\n inputValue.value = (e.target as HTMLInputElement).value\n}\n\nfunction onKeydown(e: KeyboardEvent): void {\n // Enter/сепаратор во время IME-композиции коммитят её, а не тег.\n if (isComposingEvent(e))\n return\n if (!canEdit.value)\n return\n\n if (e.key === 'Enter') {\n e.preventDefault()\n void commitInput()\n return\n }\n\n if ((props.separators ?? []).includes(e.key)) {\n e.preventDefault()\n void commitInput()\n return\n }\n\n if (inputValue.value.length === 0 && props.modelValue.length > 0) {\n if (e.key === 'Backspace') {\n e.preventDefault()\n removeAt(props.modelValue.length - 1)\n return\n }\n\n // Из пустого поля стрелка влево уводит на последний чип — дальше по чипам\n // работают те же стрелки, `Tab` для этого не нужен.\n if (e.key === 'ArrowLeft') {\n e.preventDefault()\n void focusTag(props.modelValue.length - 1)\n }\n }\n}\n\nfunction onTagKeydown(e: KeyboardEvent, index: number): void {\n if (roving.handleNavigationKeys(e)) {\n return\n }\n\n if (e.key === 'Delete' || e.key === 'Backspace') {\n e.preventDefault()\n void removeAtFromKeyboard(index)\n }\n}\n\nfunction onBlur(): void {\n if (props.addOnBlur)\n void commitInput()\n}\n\nfunction onPaste(e: ClipboardEvent): void {\n if (!canEdit.value || isMaxed.value)\n return\n\n const pasted = e.clipboardData?.getData('text')\n if (!pasted)\n return\n\n const tags = splitToTags(pasted)\n if (tags.length <= 1)\n return\n\n e.preventDefault()\n\n const run = ++validationRun\n validating.value = Boolean(props.beforeAdd)\n\n void filterAllowed(tags, run).then((allowed) => {\n if (run === validationRun)\n validating.value = false\n\n if (allowed?.length)\n addMany(allowed)\n })\n}\n\nconst wrapperClassName = computed(() => {\n return grInputTagWrapperClass({\n size: resolvedSize.value,\n state: props.state,\n invalid: isInvalid.value,\n disabled: isDisabled.value,\n })\n})\n\nconst inputClassName = computed(() => grInputTagInputClass(resolvedSize.value))\n\nconst showClearAll = computed(() => resolvedClearable.value && canEdit.value && props.modelValue.length > 0)\n\nconst placeholderText = computed(() => props.modelValue.length > 0 ? undefined : props.placeholder)\n\ndefineExpose({ focus, blur, clear: clearAll })\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n data-gr-input-tag\n data-testid=\"gr-input-tag\"\n :class=\"wrapperClassName\"\n @click=\"focus\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n >\n <span\n v-if=\"hasPrefix\"\n ref=\"prefixEl\"\n data-gr-input-tag-prefix\n class=\"inline-flex shrink-0 items-center justify-center text-[var(--gr-muted-fg)]\"\n :style=\"prefixStyle\"\n >\n <slot name=\"prefix\" />\n </span>\n\n <!-- Нативная форма: hidden на каждый тег — стандартная сериализация набора. -->\n <template v-if=\"name\">\n <input\n v-for=\"(tag, tagIndex) in modelValue\"\n :key=\"`hidden-${tagIndex}`\"\n type=\"hidden\"\n :name=\"name\"\n :value=\"tag\"\n >\n </template>\n <!--\n `display: contents` — список нужен ради роли, а не ради раскладки: чипы\n обязаны переноситься в одном потоке с полем ввода.\n -->\n <span v-if=\"modelValue.length\" role=\"list\" class=\"contents\">\n <GrChip\n v-for=\"(tag, i) in modelValue\"\n :key=\"`${tag}-${i}`\"\n :ref=\"setChipRef(i)\"\n role=\"listitem\"\n :tone=\"tagTone\"\n :dark=\"tagDark\"\n :size=\"chipSize\"\n :radius=\"tagRadius\"\n :closable=\"showRemove\"\n :remove-label=\"removeTagLabelFor(tag)\"\n :remove-tabindex=\"roving.tabindexFor(i)\"\n data-gr-input-tag-item\n data-testid=\"gr-input-tag-item\"\n :data-index=\"i\"\n @remove=\"removeAt(i)\"\n @focusin=\"roving.setActive(i)\"\n @keydown=\"onTagKeydown($event, i)\"\n >\n <slot name=\"tag\" :tag=\"tag\" :index=\"i\" :remove=\"() => removeAt(i)\">\n <span class=\"truncate max-w-[18rem]\">{{ tag }}</span>\n </slot>\n </GrChip>\n </span>\n\n <input\n :id=\"resolvedId\"\n ref=\"inputEl\"\n data-gr-input-tag-input\n data-testid=\"gr-input-tag-input\"\n :value=\"inputValue\"\n :disabled=\"isDisabled\"\n :readonly=\"isReadonly\"\n :placeholder=\"placeholderText\"\n :aria-label=\"ariaLabel\"\n :aria-describedby=\"describedBy\"\n :aria-required=\"isRequired ? 'true' : undefined\"\n :aria-readonly=\"isReadonly ? 'true' : undefined\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-busy=\"isBusy ? 'true' : undefined\"\n class=\"flex-1 min-w-[120px] bg-transparent border-none outline-none placeholder:text-[var(--gr-muted-fg)]\"\n :class=\"inputClassName\"\n @input=\"onInput\"\n @keydown=\"onKeydown\"\n @blur=\"onBlur\"\n @paste=\"onPaste\"\n >\n\n <span\n v-if=\"hasSuffix\"\n ref=\"suffixEl\"\n data-gr-input-tag-suffix\n class=\"inline-flex shrink-0 items-center justify-center text-[var(--gr-muted-fg)]\"\n :style=\"suffixStyle\"\n >\n <slot name=\"suffix\" />\n </span>\n\n <span\n v-if=\"isBusy\"\n data-gr-input-tag-spinner\n :class=\"spinnerClass\"\n aria-hidden=\"true\"\n >\n <GrIcon size=\"sm\">\n <IconLoader class=\"animate-spin\" />\n </GrIcon>\n </span>\n\n <button\n v-if=\"showClearAll\"\n type=\"button\"\n data-gr-input-tag-clear\n :class=\"clearButtonClass\"\n :aria-label=\"resolvedClearAllLabel\"\n @mousedown.prevent.stop\n @click.stop=\"clearAll\"\n >\n <GrIcon size=\"sm\">\n <IconClose />\n </GrIcon>\n </button>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, nextTick, ref } from 'vue'\n\nimport type { GrBadgeRadius, GrBadgeSize, GrBadgeTone } from '../GrBadge'\nimport GrChip from '../GrChip/GrChip.vue'\nimport { chipSizeForBadgeScale } from '../GrChip/grChipStyles'\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport IconClose from '~icons/lucide/x'\nimport IconLoader from '~icons/lucide/loader-2'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { useControlAddons } from '../../composables/internal/useControlAddons'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useAnnouncer } from '../../composables/useAnnouncer'\nimport { useRovingFocus } from '../../composables/useRovingFocus'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useFocusWithin } from '../../composables/internal/useFocusWithin'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { isComposingEvent } from '../../internal/keyboard'\n\nimport {\n clearButtonClass,\n grInputTagInputClass,\n grInputTagWrapperClass,\n spinnerClass,\n type GrInputTagSize,\n type GrInputTagState,\n} from './grInputTagStyles'\n\nexport type { GrInputTagSize, GrInputTagState } from './grInputTagStyles'\n\nconst REGEX_SPECIAL_CHAR_RE = /[.*+?^${}()|[\\]\\\\]/g\n\n/**\n * Props for {@link GrInputTag}.\n * Tag-input primitive: renders a set of badges with an inline `<input>` for entering new tags.\n * Splits input by configurable `separators`, supports paste, Enter and Backspace editing.\n */\nexport interface GrInputTagProps {\n modelValue: string[]\n placeholder?: string\n disabled?: boolean\n readonly?: boolean\n invalid?: boolean\n /** Обязательное поле (`aria-required`). Складывается с `required` у `GrFormField`. */\n required?: boolean\n state?: GrInputTagState\n size?: GrInputTagSize\n separators?: string[]\n allowDuplicates?: boolean\n trim?: boolean\n max?: number\n addOnBlur?: boolean\n clearInputOnAdd?: boolean\n /**\n * Проверка тега перед добавлением. Может быть асинхронной (проверка на\n * сервере). Отклонённый тег не добавляется и уходит в событие `reject`.\n */\n beforeAdd?: (tag: string) => boolean | Promise<boolean>\n /** Кнопка «снести все теги». Настраивается через `GrConfigProvider`. */\n clearable?: boolean\n /** Фоновая работа: спиннер + `aria-busy`. Асинхронный `beforeAdd` поднимает его сам. */\n loading?: boolean\n tagTone?: GrBadgeTone\n tagDark?: boolean\n tagSize?: GrBadgeSize\n tagRadius?: GrBadgeRadius\n tagClosable?: boolean\n /** i18n-friendly aria-label for the per-tag remove button. */\n removeTagLabel?: string\n /** i18n aria-label кнопки «снести все». */\n clearAllLabel?: string\n /**\n * Имя контрола, когда он используется вне `GrFormField`. Внутри поля имя даёт\n * связка с его `<label for>` — там проп не нужен. Без того и другого у инпута\n * нет доступного имени вовсе: placeholder именем не считается.\n */\n ariaLabel?: string\n /** Имя для нативной формы: hidden input на каждый тег. */\n name?: string\n /**\n * Ширины аддонов `prefix`/`suffix` — общий контракт контролов пакета\n * (`docs/form-controls.md`).\n */\n prefixMinWidth?: string\n prefixMaxWidth?: string\n suffixMinWidth?: string\n suffixMaxWidth?: string\n prefixFixed?: boolean\n suffixFixed?: boolean\n}\n\nexport interface GrInputTagEmits {\n (e: 'update:modelValue', value: string[]): void\n (e: 'add', value: string): void\n (e: 'remove', value: string, index: number): void\n /** Тег не прошёл `beforeAdd`. */\n (e: 'reject', value: string): void\n /** Набор снесён кнопкой «очистить». */\n (e: 'clear'): void\n /** Набор тегов изменился. */\n (e: 'change', value: string[]): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrInputTagProps>(),\n {\n placeholder: undefined,\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n state: 'default',\n // Настраивается через `GrConfigProvider`; дефолт — в резолвере ниже.\n size: undefined,\n separators: () => [','],\n allowDuplicates: false,\n trim: true,\n max: undefined,\n addOnBlur: false,\n clearInputOnAdd: true,\n beforeAdd: undefined,\n clearable: undefined,\n loading: false,\n tagTone: 'neutral',\n tagDark: false,\n tagSize: 'md',\n tagRadius: 'round',\n tagClosable: true,\n removeTagLabel: undefined,\n clearAllLabel: undefined,\n ariaLabel: undefined,\n name: undefined,\n },\n)\n\nconst emit = defineEmits<GrInputTagEmits>()\ndefineSlots<{\n /** Аддон слева от чипов: иконка, метка. */\n prefix?: () => any\n /** Аддон справа, перед кнопкой очистки. */\n suffix?: () => any\n /** Свой чип: `remove` снимает значение. */\n tag?: (props: { tag: string, index: number, remove: () => void }) => any\n}>()\n\n\n// Fallback из контекста `GrFormField` (id/aria-describedby/invalid/required) —\n// как у GrInput и GrSelect: контрол не знает про форму, знает только про поле.\nconst field = useGrFormFieldContext()\nconst resolvedId = computed(() => field?.id.value)\nconst describedBy = computed(() => field?.describedById.value)\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n} = useGrFormControl(() => props)\n\n// Эффективные значения: локальный проп → `GrConfigProvider` → дефолт компонента.\n/** Ширина аддона по умолчанию — та же лестница, что у `GrInput`. */\nconst ADDON_MIN_WIDTH_BY_SIZE: Record<GrInputTagSize, string> = {\n xs: '2rem',\n sm: '2.25rem',\n md: '2.5rem',\n lg: '3rem',\n}\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrInputTag' })\n\n/**\n * Аддоны — флекс-соседи поля: оболочка тегового поля и так флекс-строка с\n * бейджами, поэтому места паддингом резервировать не нужно.\n */\nconst { hasPrefix, hasSuffix, prefixEl, suffixEl, prefixStyle, suffixStyle } = useControlAddons(() => props, {\n defaultMinWidth: () => ADDON_MIN_WIDTH_BY_SIZE[resolvedSize.value],\n paddingX: () => '0px',\n})\nconst resolvedClearable = useGrComponentProp('GrInputTag', 'clearable', () => props.clearable, false)\n\nconst { t } = useGranularityTranslations()\nconst resolvedClearAllLabel = computed(() => props.clearAllLabel ?? t('gr.inputTag.clearAll', 'Clear all tags'))\n\nfunction removeTagLabelFor(tag: string): string {\n // Безликое «Remove tag» на двадцати кнопках подряд не даёт выбрать нужную:\n // имя обязано называть сам тег.\n return props.removeTagLabel ?? t('gr.inputTag.removeTagNamed', 'Remove tag {tag}', { tag })\n}\n\nconst inputValue = ref('')\nconst inputEl = ref<HTMLInputElement | null>(null)\nconst rootEl = ref<HTMLElement | null>(null)\n\n// Внутри корня фокус ходит между полем и кнопками удаления чипов: без границы\n// каждое такое перемещение давало бы потребителю пару `blur` + `focus`.\nconst { onFocusIn, onFocusOut } = useFocusWithin(rootEl, {\n enter: event => emit('focus', event),\n leave: event => emit('blur', event),\n})\n/**\n * Инстансы чипов: у кольца цель — кнопка снятия внутри чипа, и достаётся она\n * через `defineExpose` (`removeEl`), а не селектором по DOM.\n */\nconst chipRefs = ref<Array<{ removeEl: HTMLButtonElement | null } | null>>([])\n\nfunction setChipRef(index: number) {\n return (el: unknown): void => {\n chipRefs.value[index] = el as { removeEl: HTMLButtonElement | null } | null\n }\n}\n\n/**\n * Проп `tagSize` объявлен по шкале бейджа и остаётся публичным контрактом:\n * `md` обязан означать для потребителя тот же кегль, что и раньше.\n */\nconst chipSize = computed(() => chipSizeForBadgeScale(props.tagSize))\n\nfunction focus(): void {\n inputEl.value?.focus()\n}\n\nfunction blur(): void {\n inputEl.value?.blur()\n}\n\nconst isMaxed = computed(() => props.max !== undefined && props.modelValue.length >= props.max)\nconst canEdit = computed(() => !isDisabled.value && !isReadonly.value)\nconst showRemove = computed(() => props.tagClosable && canEdit.value)\n\n// Добавление, удаление и достигнутый предел — события, а не состояние виджета:\n// показывать их негде, объявить нужно. Это работа общего живого региона.\nconst { announce } = useAnnouncer()\n\n/**\n * Roving tabindex по крестикам: двадцать тегов давали двадцать одну остановку\n * `Tab`. В таб-порядке ровно один крестик, между ними ходят стрелками.\n *\n * Адресация индексом, а не значением тега: теги допускают повторы, и по\n * значению два одинаковых чипа было бы не различить.\n */\nconst tagIndexes = computed(() => props.modelValue.map((_, index) => index))\n\nconst roving = useRovingFocus<number>({\n items: () => tagIndexes.value,\n elementFor: index => chipRefs.value[index]?.removeEl,\n orientation: () => 'horizontal',\n // Ряд чипов — не кольцо: у него есть край, и за краем стоит поле ввода.\n wrap: () => false,\n onOverflow: (edge) => {\n // За последним чипом — поле ввода: продолжение того же ряда. Слева от\n // первого — начало строки, фокус остаётся на месте.\n if (edge === 'end') focus()\n return true\n },\n // Удаление чипа перерисовывает ряд: без ожидания фокус уехал бы на узел,\n // которого уже нет.\n beforeFocus: () => nextTick(),\n})\n\nasync function focusTag(index: number): Promise<void> {\n const last = props.modelValue.length - 1\n if (last < 0) {\n focus()\n return\n }\n\n await roving.focusKey(Math.min(Math.max(index, 0), last))\n}\n\nfunction escapeRegexChar(ch: string): string {\n return ch.replace(REGEX_SPECIAL_CHAR_RE, '\\\\$&')\n}\n\nconst splitRegex = computed(() => {\n const seps = (props.separators ?? []).filter(Boolean).map(s => s[0]!)\n const uniq = [...new Set([...seps, '\\n', '\\r', '\\t'])]\n const body = uniq.map(escapeRegexChar).join('')\n return new RegExp(`[${body}]+`, 'g')\n})\n\nfunction normalizeTag(raw: string): string {\n return props.trim ? raw.trim() : raw\n}\n\nfunction splitToTags(raw: string): string[] {\n return raw\n .split(splitRegex.value)\n .map(v => normalizeTag(v))\n .filter(Boolean)\n}\n\nfunction addMany(rawTags: string[]): boolean {\n if (!canEdit.value || isMaxed.value)\n return false\n\n const next = props.modelValue.slice()\n const added: string[] = []\n\n for (const raw of rawTags) {\n const tag = normalizeTag(raw)\n if (!tag)\n continue\n\n if (!props.allowDuplicates && next.includes(tag))\n continue\n\n if (props.max !== undefined && next.length >= props.max)\n break\n\n next.push(tag)\n added.push(tag)\n emit('add', tag)\n }\n\n if (!added.length)\n return false\n\n emit('update:modelValue', next)\n emit('change', next)\n announce(added.length === 1\n ? t('gr.inputTag.added', 'Tag added: {tag}', { tag: added[0]! })\n : t('gr.inputTag.addedMany', '{count} tags added', { count: added.length }))\n\n return true\n}\n\n// Счётчик запуска: пока идёт асинхронная проверка, пользователь успевает нажать\n// Enter второй раз — результат устаревшей проверки дописывать нельзя.\nlet validationRun = 0\nconst validating = ref(false)\nconst isBusy = computed(() => props.loading || validating.value)\n\nasync function filterAllowed(tags: string[], run: number): Promise<string[] | null> {\n const check = props.beforeAdd\n if (!check)\n return tags\n\n const allowed: string[] = []\n\n for (const tag of tags) {\n const ok = await check(tag)\n if (run !== validationRun)\n return null\n\n if (ok)\n allowed.push(tag)\n else\n emit('reject', tag)\n }\n\n return allowed\n}\n\nasync function commitInput(): Promise<void> {\n if (!canEdit.value || !inputValue.value)\n return\n\n if (isMaxed.value) {\n announce(t('gr.inputTag.limitReached', 'Tag limit reached'))\n return\n }\n\n const raw = inputValue.value\n const tags = splitToTags(raw)\n if (!tags.length)\n return\n\n const run = ++validationRun\n validating.value = Boolean(props.beforeAdd)\n\n const allowed = await filterAllowed(tags, run)\n\n if (run === validationRun)\n validating.value = false\n\n if (allowed === null || !allowed.length)\n return\n\n const changed = addMany(allowed)\n // Значение могло измениться, пока шла проверка: стирать чужой ввод нельзя.\n if (changed && props.clearInputOnAdd && inputValue.value === raw)\n inputValue.value = ''\n}\n\nfunction removeAt(index: number): void {\n if (!canEdit.value)\n return\n\n const next = props.modelValue.slice()\n const removed = next[index]\n if (removed === undefined)\n return\n\n next.splice(index, 1)\n emit('update:modelValue', next)\n emit('change', next)\n emit('remove', removed, index)\n announce(t('gr.inputTag.removed', 'Tag removed: {tag}', { tag: removed }))\n}\n\n/** Удаление с клавиатуры: фокус переезжает на соседний чип, а не пропадает. */\nasync function removeAtFromKeyboard(index: number): Promise<void> {\n removeAt(index)\n await nextTick()\n\n if (props.modelValue.length === 0) {\n focus()\n return\n }\n\n await focusTag(Math.min(index, props.modelValue.length - 1))\n}\n\nfunction clearAll(): void {\n if (!canEdit.value || props.modelValue.length === 0)\n return\n\n emit('update:modelValue', [])\n emit('change', [])\n emit('clear')\n announce(t('gr.inputTag.cleared', 'All tags removed'))\n focus()\n}\n\nfunction onInput(e: Event): void {\n inputValue.value = (e.target as HTMLInputElement).value\n}\n\nfunction onKeydown(e: KeyboardEvent): void {\n // Enter/сепаратор во время IME-композиции коммитят её, а не тег.\n if (isComposingEvent(e))\n return\n if (!canEdit.value)\n return\n\n if (e.key === 'Enter') {\n e.preventDefault()\n void commitInput()\n return\n }\n\n if ((props.separators ?? []).includes(e.key)) {\n e.preventDefault()\n void commitInput()\n return\n }\n\n if (inputValue.value.length === 0 && props.modelValue.length > 0) {\n if (e.key === 'Backspace') {\n e.preventDefault()\n removeAt(props.modelValue.length - 1)\n return\n }\n\n // Из пустого поля стрелка влево уводит на последний чип — дальше по чипам\n // работают те же стрелки, `Tab` для этого не нужен.\n if (e.key === 'ArrowLeft') {\n e.preventDefault()\n void focusTag(props.modelValue.length - 1)\n }\n }\n}\n\nfunction onTagKeydown(e: KeyboardEvent, index: number): void {\n if (roving.handleNavigationKeys(e)) {\n return\n }\n\n if (e.key === 'Delete' || e.key === 'Backspace') {\n e.preventDefault()\n void removeAtFromKeyboard(index)\n }\n}\n\nfunction onBlur(): void {\n if (props.addOnBlur)\n void commitInput()\n}\n\nfunction onPaste(e: ClipboardEvent): void {\n if (!canEdit.value || isMaxed.value)\n return\n\n const pasted = e.clipboardData?.getData('text')\n if (!pasted)\n return\n\n const tags = splitToTags(pasted)\n if (tags.length <= 1)\n return\n\n e.preventDefault()\n\n const run = ++validationRun\n validating.value = Boolean(props.beforeAdd)\n\n void filterAllowed(tags, run).then((allowed) => {\n if (run === validationRun)\n validating.value = false\n\n if (allowed?.length)\n addMany(allowed)\n })\n}\n\nconst wrapperClassName = computed(() => {\n return grInputTagWrapperClass({\n size: resolvedSize.value,\n state: props.state,\n invalid: isInvalid.value,\n disabled: isDisabled.value,\n })\n})\n\nconst inputClassName = computed(() => grInputTagInputClass(resolvedSize.value))\n\nconst showClearAll = computed(() => resolvedClearable.value && canEdit.value && props.modelValue.length > 0)\n\nconst placeholderText = computed(() => props.modelValue.length > 0 ? undefined : props.placeholder)\n\ndefineExpose({ focus, blur, clear: clearAll })\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n data-gr-input-tag\n data-testid=\"gr-input-tag\"\n :class=\"wrapperClassName\"\n @click=\"focus\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n >\n <span\n v-if=\"hasPrefix\"\n ref=\"prefixEl\"\n data-gr-input-tag-prefix\n class=\"inline-flex shrink-0 items-center justify-center text-[var(--gr-muted-fg)]\"\n :style=\"prefixStyle\"\n >\n <slot name=\"prefix\" />\n </span>\n\n <!-- Нативная форма: hidden на каждый тег — стандартная сериализация набора. -->\n <template v-if=\"name\">\n <input\n v-for=\"(tag, tagIndex) in modelValue\"\n :key=\"`hidden-${tagIndex}`\"\n type=\"hidden\"\n :name=\"name\"\n :value=\"tag\"\n >\n </template>\n <!--\n `display: contents` — список нужен ради роли, а не ради раскладки: чипы\n обязаны переноситься в одном потоке с полем ввода.\n -->\n <span v-if=\"modelValue.length\" role=\"list\" class=\"contents\">\n <GrChip\n v-for=\"(tag, i) in modelValue\"\n :key=\"`${tag}-${i}`\"\n :ref=\"setChipRef(i)\"\n role=\"listitem\"\n :tone=\"tagTone\"\n :dark=\"tagDark\"\n :size=\"chipSize\"\n :radius=\"tagRadius\"\n :closable=\"showRemove\"\n :remove-label=\"removeTagLabelFor(tag)\"\n :remove-tabindex=\"roving.tabindexFor(i)\"\n data-gr-input-tag-item\n data-testid=\"gr-input-tag-item\"\n :data-index=\"i\"\n @remove=\"removeAt(i)\"\n @focusin=\"roving.setActive(i)\"\n @keydown=\"onTagKeydown($event, i)\"\n >\n <slot name=\"tag\" :tag=\"tag\" :index=\"i\" :remove=\"() => removeAt(i)\">\n <span class=\"truncate max-w-[18rem]\">{{ tag }}</span>\n </slot>\n </GrChip>\n </span>\n\n <input\n :id=\"resolvedId\"\n ref=\"inputEl\"\n data-gr-input-tag-input\n data-testid=\"gr-input-tag-input\"\n :value=\"inputValue\"\n :disabled=\"isDisabled\"\n :readonly=\"isReadonly\"\n :placeholder=\"placeholderText\"\n :aria-label=\"ariaLabel\"\n :aria-describedby=\"describedBy\"\n :aria-required=\"isRequired ? 'true' : undefined\"\n :aria-readonly=\"isReadonly ? 'true' : undefined\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-busy=\"isBusy ? 'true' : undefined\"\n class=\"flex-1 min-w-[120px] bg-transparent border-none outline-none placeholder:text-[var(--gr-muted-fg)]\"\n :class=\"inputClassName\"\n @input=\"onInput\"\n @keydown=\"onKeydown\"\n @blur=\"onBlur\"\n @paste=\"onPaste\"\n >\n\n <span\n v-if=\"hasSuffix\"\n ref=\"suffixEl\"\n data-gr-input-tag-suffix\n class=\"inline-flex shrink-0 items-center justify-center text-[var(--gr-muted-fg)]\"\n :style=\"suffixStyle\"\n >\n <slot name=\"suffix\" />\n </span>\n\n <span\n v-if=\"isBusy\"\n data-gr-input-tag-spinner\n :class=\"spinnerClass\"\n aria-hidden=\"true\"\n >\n <GrIcon size=\"sm\">\n <IconLoader class=\"animate-spin\" />\n </GrIcon>\n </span>\n\n <button\n v-if=\"showClearAll\"\n type=\"button\"\n data-gr-input-tag-clear\n :class=\"clearButtonClass\"\n :aria-label=\"resolvedClearAllLabel\"\n @mousedown.prevent.stop\n @click.stop=\"clearAll\"\n >\n <GrIcon size=\"sm\">\n <IconClose />\n </GrIcon>\n </button>\n </div>\n</template>\n"],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { n as grInputTagSafelist, t as grInputTagConfig } from "../../chunks/config-BqT6PMzO.js";
|
|
2
|
-
import { t as GrInputTag_default } from "./chunks/GrInputTag-
|
|
2
|
+
import { t as GrInputTag_default } from "./chunks/GrInputTag-5wVcB0zG.js";
|
|
3
3
|
export { GrInputTag_default as GrInputTag, GrInputTag_default as default, grInputTagConfig, grInputTagSafelist };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { a as useGrComponentSize } from "../../../chunks/context-CTbuwcYs.js";
|
|
2
2
|
import { n as useGranularityTranslations } from "../../../chunks/granularityI18n-BC5zsj_d.js";
|
|
3
|
-
import { t as GrIcon_default } from "../../GrIcon/chunks/GrIcon-
|
|
3
|
+
import { t as GrIcon_default } from "../../GrIcon/chunks/GrIcon-Dmf0c9ba.js";
|
|
4
4
|
import { a as grLinkClass, i as focusRingClass, o as grLinkColorStyle, t as baseRootClass } from "../../../chunks/grLinkStyles-FME_qMpi.js";
|
|
5
5
|
import { computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, markRaw, mergeProps, openBlock, renderSlot, resolveDynamicComponent, toDisplayString, unref, useAttrs, withCtx } from "vue";
|
|
6
6
|
//#region ~icons/lucide/external-link
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GrLink-DAGCFYO7.js","names":[],"sources":["../../../../src/components/GrLink/GrLink.vue","../../../../src/components/GrLink/GrLink.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { useGrComponentSize } from '../GrConfigProvider/context'\n/**\n * GrLink — GR-примитив ссылки.\n *\n * Корневой тег зависит от пропов:\n * - кастомный компонент/тег из пропа `as` — когда `as` передан и не `disabled`\n * (например, `Link` от `@inertiajs/vue3` или `RouterLink` от Vue Router);\n * - `<a>` — когда задан `href` и не `disabled`;\n * - `<span>` — во всех остальных случаях (включая `disabled`, даже если задан\n * `href`/`as`).\n *\n * Это осознанное решение, чтобы отключённая ссылка не была кликабельной и\n * не участвовала в порядке табуляции. Учтите это в внешних CSS-селекторах.\n *\n * Проп `as` нужен для интеграции с роутерами (Inertia, Vue Router и пр.):\n * стили/поведение GR остаются едиными, а тег корня подменяется. Все\n * специфичные для роутера атрибуты (`method`, `replace`, `preserve-scroll` и т.п.)\n * проходят через fallthrough-attrs.\n */\nimport { computed, markRaw, useAttrs, type Component } from 'vue'\n\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport IconExternal from '~icons/lucide/external-link'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n\nimport {\n baseRootClass,\n focusRingClass,\n type GrLinkSize,\n type GrLinkTone,\n type GrLinkUnderline,\n type GrLinkVariant,\n grLinkClass,\n grLinkColorStyle,\n} from './grLinkStyles'\n\nexport interface GrLinkProps {\n /**\n * Кастомный корневой тег/компонент. Если передан и компонент не `disabled` —\n * рендерится через `<component :is=\"as\">`. Игнорируется при `disabled`.\n */\n as?: string | Component\n href?: string\n external?: boolean\n target?: string\n rel?: string\n disabled?: boolean\n ariaLabel?: string\n /** Семантический цвет ссылки из палитры `GrTone`. */\n tone?: GrLinkTone\n /** Уровень акцента: `default` (окрашен) или `muted` (приглушён, акцент на hover). */\n variant?: GrLinkVariant\n underline?: GrLinkUnderline\n size?: GrLinkSize\n /**\n * Иконка внешней ссылки. По умолчанию показывается у любой ссылки, которая\n * открывается в новой вкладке, — не только при `external`.\n */\n externalIcon?: boolean\n /** i18n: скрытая подсказка «откроется в новой вкладке». */\n newTabLabel?: string\n}\n\ndefineOptions({\n inheritAttrs: false,\n})\n\nconst props = withDefaults(defineProps<GrLinkProps>(), {\n as: undefined,\n href: undefined,\n external: false,\n target: undefined,\n rel: undefined,\n disabled: false,\n ariaLabel: undefined,\n tone: 'primary',\n variant: 'default',\n underline: 'auto',\n size: undefined,\n externalIcon: undefined,\n newTabLabel: undefined,\n})\n\nconst attrs = useAttrs()\n\n// Кастомный корень имеет приоритет над `<a>` — GR не пытается угадывать\n// «правильный» тег за пользователя, если он явно его указал.\nconst isInteractive = computed(() => !props.disabled && (!!props.as || !!props.href))\n\nconst renderAs = computed<string | Component>(() => {\n if (!isInteractive.value) return 'span'\n if (props.as) {\n // Если `as` — объект-компонент, помечаем его как нереактивный, чтобы избежать\n // оверхеда от Vue-прокси и предупреждений в консоли. Строки (нативные теги)\n // оставляем как есть.\n return typeof props.as === 'string' ? props.as : markRaw(props.as)\n }\n return 'a'\n})\n\nconst resolvedTarget = computed(() => {\n if (props.target)\n return props.target\n\n if (props.external)\n return '_blank'\n\n return undefined\n})\n\nconst resolvedRel = computed(() => {\n if (props.rel)\n return props.rel\n\n // Авто-защита для любых ссылок, открывающихся в новой вкладке,\n // а не только когда выставлен `external`.\n if (resolvedTarget.value === '_blank')\n return 'noopener noreferrer'\n\n return undefined\n})\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrLink' })\n\nconst { t } = useGranularityTranslations()\n\n/**\n * Смена контекста должна быть объявлена (WCAG 3.2.5). Условие — ссылка реально\n * открывается в новой вкладке, а не наличие пропа `external`: `target=\"_blank\"`\n * снаружи даёт ровно тот же сюрприз (та же генерализация, что у `rel`).\n */\nconst opensInNewTab = computed(() => isInteractive.value && resolvedTarget.value === '_blank')\nconst newTabHint = computed(() => props.newTabLabel ?? t('gr.link.opensInNewTab', 'opens in a new tab'))\n\nconst showExternalIcon = computed(() => props.externalIcon ?? opensInNewTab.value)\n\n/**\n * `aria-label` перекрывает содержимое целиком — вместе со скрытой подсказкой.\n * Поэтому, если имя задано руками, подсказка дописывается к нему.\n */\nconst resolvedAriaLabel = computed(() => {\n if (!props.ariaLabel)\n return undefined\n\n return opensInNewTab.value ? `${props.ariaLabel}, ${newTabHint.value}` : props.ariaLabel\n})\n\nconst iconSize = computed(() => (resolvedSize.value === 'lg' ? 'sm' : 'xs'))\n\nconst rootClass = computed(() => {\n const variantClass = grLinkClass({\n size: resolvedSize.value,\n underline: props.underline,\n disabled: props.disabled,\n })\n\n // На disabled-ветке (`<span>`) focus-ring не нужен: элемент не фокусируется,\n // а если снаружи ему навесят `tabindex`, фокус-рамка на «отключённой» ссылке смотрелась бы странно.\n return props.disabled\n ? `${baseRootClass} ${variantClass}`\n : `${baseRootClass} ${focusRingClass} ${variantClass}`\n})\n\n// Цвет (tone × variant) прокидываем через CSS-переменные, чтобы не плодить классы.\nconst colorStyle = computed(() => grLinkColorStyle({\n tone: props.tone,\n variant: props.variant,\n disabled: props.disabled,\n}))\n\ndefineSlots<{\n /** Текст ссылки. */\n default?: () => any\n}>()\n\n</script>\n\n<template>\n <component\n :is=\"renderAs\"\n v-bind=\"attrs\"\n :href=\"isInteractive ? href : undefined\"\n :target=\"isInteractive ? resolvedTarget : undefined\"\n :rel=\"isInteractive ? resolvedRel : undefined\"\n :aria-label=\"resolvedAriaLabel\"\n :aria-disabled=\"disabled ? 'true' : undefined\"\n :class=\"rootClass\"\n :style=\"colorStyle\"\n >\n <slot />\n\n <GrIcon v-if=\"showExternalIcon\" :size=\"iconSize\">\n <IconExternal />\n </GrIcon>\n\n <!-- Подсказка не дублируется, когда её уже вобрал `aria-label`. -->\n <span v-if=\"opensInNewTab && !ariaLabel\" class=\"sr-only\">({{ newTabHint }})</span>\n </component>\n</template>\n","<script setup lang=\"ts\">\nimport { useGrComponentSize } from '../GrConfigProvider/context'\n/**\n * GrLink — GR-примитив ссылки.\n *\n * Корневой тег зависит от пропов:\n * - кастомный компонент/тег из пропа `as` — когда `as` передан и не `disabled`\n * (например, `Link` от `@inertiajs/vue3` или `RouterLink` от Vue Router);\n * - `<a>` — когда задан `href` и не `disabled`;\n * - `<span>` — во всех остальных случаях (включая `disabled`, даже если задан\n * `href`/`as`).\n *\n * Это осознанное решение, чтобы отключённая ссылка не была кликабельной и\n * не участвовала в порядке табуляции. Учтите это в внешних CSS-селекторах.\n *\n * Проп `as` нужен для интеграции с роутерами (Inertia, Vue Router и пр.):\n * стили/поведение GR остаются едиными, а тег корня подменяется. Все\n * специфичные для роутера атрибуты (`method`, `replace`, `preserve-scroll` и т.п.)\n * проходят через fallthrough-attrs.\n */\nimport { computed, markRaw, useAttrs, type Component } from 'vue'\n\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport IconExternal from '~icons/lucide/external-link'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n\nimport {\n baseRootClass,\n focusRingClass,\n type GrLinkSize,\n type GrLinkTone,\n type GrLinkUnderline,\n type GrLinkVariant,\n grLinkClass,\n grLinkColorStyle,\n} from './grLinkStyles'\n\nexport interface GrLinkProps {\n /**\n * Кастомный корневой тег/компонент. Если передан и компонент не `disabled` —\n * рендерится через `<component :is=\"as\">`. Игнорируется при `disabled`.\n */\n as?: string | Component\n href?: string\n external?: boolean\n target?: string\n rel?: string\n disabled?: boolean\n ariaLabel?: string\n /** Семантический цвет ссылки из палитры `GrTone`. */\n tone?: GrLinkTone\n /** Уровень акцента: `default` (окрашен) или `muted` (приглушён, акцент на hover). */\n variant?: GrLinkVariant\n underline?: GrLinkUnderline\n size?: GrLinkSize\n /**\n * Иконка внешней ссылки. По умолчанию показывается у любой ссылки, которая\n * открывается в новой вкладке, — не только при `external`.\n */\n externalIcon?: boolean\n /** i18n: скрытая подсказка «откроется в новой вкладке». */\n newTabLabel?: string\n}\n\ndefineOptions({\n inheritAttrs: false,\n})\n\nconst props = withDefaults(defineProps<GrLinkProps>(), {\n as: undefined,\n href: undefined,\n external: false,\n target: undefined,\n rel: undefined,\n disabled: false,\n ariaLabel: undefined,\n tone: 'primary',\n variant: 'default',\n underline: 'auto',\n size: undefined,\n externalIcon: undefined,\n newTabLabel: undefined,\n})\n\nconst attrs = useAttrs()\n\n// Кастомный корень имеет приоритет над `<a>` — GR не пытается угадывать\n// «правильный» тег за пользователя, если он явно его указал.\nconst isInteractive = computed(() => !props.disabled && (!!props.as || !!props.href))\n\nconst renderAs = computed<string | Component>(() => {\n if (!isInteractive.value) return 'span'\n if (props.as) {\n // Если `as` — объект-компонент, помечаем его как нереактивный, чтобы избежать\n // оверхеда от Vue-прокси и предупреждений в консоли. Строки (нативные теги)\n // оставляем как есть.\n return typeof props.as === 'string' ? props.as : markRaw(props.as)\n }\n return 'a'\n})\n\nconst resolvedTarget = computed(() => {\n if (props.target)\n return props.target\n\n if (props.external)\n return '_blank'\n\n return undefined\n})\n\nconst resolvedRel = computed(() => {\n if (props.rel)\n return props.rel\n\n // Авто-защита для любых ссылок, открывающихся в новой вкладке,\n // а не только когда выставлен `external`.\n if (resolvedTarget.value === '_blank')\n return 'noopener noreferrer'\n\n return undefined\n})\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrLink' })\n\nconst { t } = useGranularityTranslations()\n\n/**\n * Смена контекста должна быть объявлена (WCAG 3.2.5). Условие — ссылка реально\n * открывается в новой вкладке, а не наличие пропа `external`: `target=\"_blank\"`\n * снаружи даёт ровно тот же сюрприз (та же генерализация, что у `rel`).\n */\nconst opensInNewTab = computed(() => isInteractive.value && resolvedTarget.value === '_blank')\nconst newTabHint = computed(() => props.newTabLabel ?? t('gr.link.opensInNewTab', 'opens in a new tab'))\n\nconst showExternalIcon = computed(() => props.externalIcon ?? opensInNewTab.value)\n\n/**\n * `aria-label` перекрывает содержимое целиком — вместе со скрытой подсказкой.\n * Поэтому, если имя задано руками, подсказка дописывается к нему.\n */\nconst resolvedAriaLabel = computed(() => {\n if (!props.ariaLabel)\n return undefined\n\n return opensInNewTab.value ? `${props.ariaLabel}, ${newTabHint.value}` : props.ariaLabel\n})\n\nconst iconSize = computed(() => (resolvedSize.value === 'lg' ? 'sm' : 'xs'))\n\nconst rootClass = computed(() => {\n const variantClass = grLinkClass({\n size: resolvedSize.value,\n underline: props.underline,\n disabled: props.disabled,\n })\n\n // На disabled-ветке (`<span>`) focus-ring не нужен: элемент не фокусируется,\n // а если снаружи ему навесят `tabindex`, фокус-рамка на «отключённой» ссылке смотрелась бы странно.\n return props.disabled\n ? `${baseRootClass} ${variantClass}`\n : `${baseRootClass} ${focusRingClass} ${variantClass}`\n})\n\n// Цвет (tone × variant) прокидываем через CSS-переменные, чтобы не плодить классы.\nconst colorStyle = computed(() => grLinkColorStyle({\n tone: props.tone,\n variant: props.variant,\n disabled: props.disabled,\n}))\n\ndefineSlots<{\n /** Текст ссылки. */\n default?: () => any\n}>()\n\n</script>\n\n<template>\n <component\n :is=\"renderAs\"\n v-bind=\"attrs\"\n :href=\"isInteractive ? href : undefined\"\n :target=\"isInteractive ? resolvedTarget : undefined\"\n :rel=\"isInteractive ? resolvedRel : undefined\"\n :aria-label=\"resolvedAriaLabel\"\n :aria-disabled=\"disabled ? 'true' : undefined\"\n :class=\"rootClass\"\n :style=\"colorStyle\"\n >\n <slot />\n\n <GrIcon v-if=\"showExternalIcon\" :size=\"iconSize\">\n <IconExternal />\n </GrIcon>\n\n <!-- Подсказка не дублируется, когда её уже вобрал `aria-label`. -->\n <span v-if=\"opensInNewTab && !ariaLabel\" class=\"sr-only\">({{ newTabHint }})</span>\n </component>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoEA,MAAM,QAAQ;EAgBd,MAAM,QAAQ,SAAS;EAIvB,MAAM,gBAAgB,eAAe,CAAC,MAAM,aAAa,CAAC,CAAC,MAAM,MAAM,CAAC,CAAC,MAAM,KAAK;EAEpF,MAAM,WAAW,eAAmC;GAClD,IAAI,CAAC,cAAc,OAAO,OAAO;GACjC,IAAI,MAAM,IAIR,OAAO,OAAO,MAAM,OAAO,WAAW,MAAM,KAAK,QAAQ,MAAM,EAAE;GAEnE,OAAO;EACT,CAAC;EAED,MAAM,iBAAiB,eAAe;GACpC,IAAI,MAAM,QACR,OAAO,MAAM;GAEf,IAAI,MAAM,UACR,OAAO;EAGX,CAAC;EAED,MAAM,cAAc,eAAe;GACjC,IAAI,MAAM,KACR,OAAO,MAAM;GAIf,IAAI,eAAe,UAAU,UAC3B,OAAO;EAGX,CAAC;EAED,MAAM,eAAe,yBAAyB,MAAM,MAAM,EAAE,WAAW,SAAS,CAAC;EAEjF,MAAM,EAAE,MAAM,2BAA2B;;;;;;EAOzC,MAAM,gBAAgB,eAAe,cAAc,SAAS,eAAe,UAAU,QAAQ;EAC7F,MAAM,aAAa,eAAe,MAAM,eAAe,EAAE,yBAAyB,oBAAoB,CAAC;EAEvG,MAAM,mBAAmB,eAAe,MAAM,gBAAgB,cAAc,KAAK;;;;;EAMjF,MAAM,oBAAoB,eAAe;GACvC,IAAI,CAAC,MAAM,WACT,OAAO,KAAA;GAET,OAAO,cAAc,QAAQ,GAAG,MAAM,UAAU,IAAI,WAAW,UAAU,MAAM;EACjF,CAAC;EAED,MAAM,WAAW,eAAgB,aAAa,UAAU,OAAO,OAAO,IAAK;EAE3E,MAAM,YAAY,eAAe;GAC/B,MAAM,eAAe,YAAY;IAC/B,MAAM,aAAa;IACnB,WAAW,MAAM;IACjB,UAAU,MAAM;GAClB,CAAC;GAID,OAAO,MAAM,WACT,GAAG,cAAc,GAAG,iBACpB,GAAG,cAAc,GAAG,eAAe,GAAG;EAC5C,CAAC;EAGD,MAAM,aAAa,eAAe,iBAAiB;GACjD,MAAM,MAAM;GACZ,SAAS,MAAM;GACf,UAAU,MAAM;EAClB,CAAC,CAAC;;uBAUA,YAmBY,wBAlBL,SAAA,KAAQ,GADf,WAEU,MAiBE,KAAA,GAjBG;IACZ,MAAM,cAAA,QAAgB,QAAA,OAAO,KAAA;IAC7B,QAAQ,cAAA,QAAgB,eAAA,QAAiB,KAAA;IACzC,KAAK,cAAA,QAAgB,YAAA,QAAc,KAAA;IACnC,cAAY,kBAAA;IACZ,iBAAe,QAAA,WAAQ,SAAY,KAAA;IACnC,OAAO,UAAA;IACP,OAAO,WAAA;;2BAEA;KAAR,WAAQ,KAAA,QAAA,SAAA;KAEM,iBAAA,SAAA,UAAA,GAAd,YAES,gBAAA;;MAFwB,MAAM,SAAA;;6BACrB,CAAhB,YAAgB,MAAA,qBAAA,CAAA,CAAA,CAAA;;;KAIN,cAAA,SAAa,CAAK,QAAA,aAAA,UAAA,GAA9B,mBAAkF,QAAlF,YAAyD,MAAC,gBAAG,WAAA,KAAU,IAAG,KAAC,CAAA,KAAA,mBAAA,IAAA,IAAA"}
|
|
1
|
+
{"version":3,"file":"GrLink-DYXw3W9w.js","names":[],"sources":["../../../../src/components/GrLink/GrLink.vue","../../../../src/components/GrLink/GrLink.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { useGrComponentSize } from '../GrConfigProvider/context'\n/**\n * GrLink — GR-примитив ссылки.\n *\n * Корневой тег зависит от пропов:\n * - кастомный компонент/тег из пропа `as` — когда `as` передан и не `disabled`\n * (например, `Link` от `@inertiajs/vue3` или `RouterLink` от Vue Router);\n * - `<a>` — когда задан `href` и не `disabled`;\n * - `<span>` — во всех остальных случаях (включая `disabled`, даже если задан\n * `href`/`as`).\n *\n * Это осознанное решение, чтобы отключённая ссылка не была кликабельной и\n * не участвовала в порядке табуляции. Учтите это в внешних CSS-селекторах.\n *\n * Проп `as` нужен для интеграции с роутерами (Inertia, Vue Router и пр.):\n * стили/поведение GR остаются едиными, а тег корня подменяется. Все\n * специфичные для роутера атрибуты (`method`, `replace`, `preserve-scroll` и т.п.)\n * проходят через fallthrough-attrs.\n */\nimport { computed, markRaw, useAttrs, type Component } from 'vue'\n\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport IconExternal from '~icons/lucide/external-link'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n\nimport {\n baseRootClass,\n focusRingClass,\n type GrLinkSize,\n type GrLinkTone,\n type GrLinkUnderline,\n type GrLinkVariant,\n grLinkClass,\n grLinkColorStyle,\n} from './grLinkStyles'\n\nexport interface GrLinkProps {\n /**\n * Кастомный корневой тег/компонент. Если передан и компонент не `disabled` —\n * рендерится через `<component :is=\"as\">`. Игнорируется при `disabled`.\n */\n as?: string | Component\n href?: string\n external?: boolean\n target?: string\n rel?: string\n disabled?: boolean\n ariaLabel?: string\n /** Семантический цвет ссылки из палитры `GrTone`. */\n tone?: GrLinkTone\n /** Уровень акцента: `default` (окрашен) или `muted` (приглушён, акцент на hover). */\n variant?: GrLinkVariant\n underline?: GrLinkUnderline\n size?: GrLinkSize\n /**\n * Иконка внешней ссылки. По умолчанию показывается у любой ссылки, которая\n * открывается в новой вкладке, — не только при `external`.\n */\n externalIcon?: boolean\n /** i18n: скрытая подсказка «откроется в новой вкладке». */\n newTabLabel?: string\n}\n\ndefineOptions({\n inheritAttrs: false,\n})\n\nconst props = withDefaults(defineProps<GrLinkProps>(), {\n as: undefined,\n href: undefined,\n external: false,\n target: undefined,\n rel: undefined,\n disabled: false,\n ariaLabel: undefined,\n tone: 'primary',\n variant: 'default',\n underline: 'auto',\n size: undefined,\n externalIcon: undefined,\n newTabLabel: undefined,\n})\n\nconst attrs = useAttrs()\n\n// Кастомный корень имеет приоритет над `<a>` — GR не пытается угадывать\n// «правильный» тег за пользователя, если он явно его указал.\nconst isInteractive = computed(() => !props.disabled && (!!props.as || !!props.href))\n\nconst renderAs = computed<string | Component>(() => {\n if (!isInteractive.value) return 'span'\n if (props.as) {\n // Если `as` — объект-компонент, помечаем его как нереактивный, чтобы избежать\n // оверхеда от Vue-прокси и предупреждений в консоли. Строки (нативные теги)\n // оставляем как есть.\n return typeof props.as === 'string' ? props.as : markRaw(props.as)\n }\n return 'a'\n})\n\nconst resolvedTarget = computed(() => {\n if (props.target)\n return props.target\n\n if (props.external)\n return '_blank'\n\n return undefined\n})\n\nconst resolvedRel = computed(() => {\n if (props.rel)\n return props.rel\n\n // Авто-защита для любых ссылок, открывающихся в новой вкладке,\n // а не только когда выставлен `external`.\n if (resolvedTarget.value === '_blank')\n return 'noopener noreferrer'\n\n return undefined\n})\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrLink' })\n\nconst { t } = useGranularityTranslations()\n\n/**\n * Смена контекста должна быть объявлена (WCAG 3.2.5). Условие — ссылка реально\n * открывается в новой вкладке, а не наличие пропа `external`: `target=\"_blank\"`\n * снаружи даёт ровно тот же сюрприз (та же генерализация, что у `rel`).\n */\nconst opensInNewTab = computed(() => isInteractive.value && resolvedTarget.value === '_blank')\nconst newTabHint = computed(() => props.newTabLabel ?? t('gr.link.opensInNewTab', 'opens in a new tab'))\n\nconst showExternalIcon = computed(() => props.externalIcon ?? opensInNewTab.value)\n\n/**\n * `aria-label` перекрывает содержимое целиком — вместе со скрытой подсказкой.\n * Поэтому, если имя задано руками, подсказка дописывается к нему.\n */\nconst resolvedAriaLabel = computed(() => {\n if (!props.ariaLabel)\n return undefined\n\n return opensInNewTab.value ? `${props.ariaLabel}, ${newTabHint.value}` : props.ariaLabel\n})\n\nconst iconSize = computed(() => (resolvedSize.value === 'lg' ? 'sm' : 'xs'))\n\nconst rootClass = computed(() => {\n const variantClass = grLinkClass({\n size: resolvedSize.value,\n underline: props.underline,\n disabled: props.disabled,\n })\n\n // На disabled-ветке (`<span>`) focus-ring не нужен: элемент не фокусируется,\n // а если снаружи ему навесят `tabindex`, фокус-рамка на «отключённой» ссылке смотрелась бы странно.\n return props.disabled\n ? `${baseRootClass} ${variantClass}`\n : `${baseRootClass} ${focusRingClass} ${variantClass}`\n})\n\n// Цвет (tone × variant) прокидываем через CSS-переменные, чтобы не плодить классы.\nconst colorStyle = computed(() => grLinkColorStyle({\n tone: props.tone,\n variant: props.variant,\n disabled: props.disabled,\n}))\n\ndefineSlots<{\n /** Текст ссылки. */\n default?: () => any\n}>()\n\n</script>\n\n<template>\n <component\n :is=\"renderAs\"\n v-bind=\"attrs\"\n :href=\"isInteractive ? href : undefined\"\n :target=\"isInteractive ? resolvedTarget : undefined\"\n :rel=\"isInteractive ? resolvedRel : undefined\"\n :aria-label=\"resolvedAriaLabel\"\n :aria-disabled=\"disabled ? 'true' : undefined\"\n :class=\"rootClass\"\n :style=\"colorStyle\"\n >\n <slot />\n\n <GrIcon v-if=\"showExternalIcon\" :size=\"iconSize\">\n <IconExternal />\n </GrIcon>\n\n <!-- Подсказка не дублируется, когда её уже вобрал `aria-label`. -->\n <span v-if=\"opensInNewTab && !ariaLabel\" class=\"sr-only\">({{ newTabHint }})</span>\n </component>\n</template>\n","<script setup lang=\"ts\">\nimport { useGrComponentSize } from '../GrConfigProvider/context'\n/**\n * GrLink — GR-примитив ссылки.\n *\n * Корневой тег зависит от пропов:\n * - кастомный компонент/тег из пропа `as` — когда `as` передан и не `disabled`\n * (например, `Link` от `@inertiajs/vue3` или `RouterLink` от Vue Router);\n * - `<a>` — когда задан `href` и не `disabled`;\n * - `<span>` — во всех остальных случаях (включая `disabled`, даже если задан\n * `href`/`as`).\n *\n * Это осознанное решение, чтобы отключённая ссылка не была кликабельной и\n * не участвовала в порядке табуляции. Учтите это в внешних CSS-селекторах.\n *\n * Проп `as` нужен для интеграции с роутерами (Inertia, Vue Router и пр.):\n * стили/поведение GR остаются едиными, а тег корня подменяется. Все\n * специфичные для роутера атрибуты (`method`, `replace`, `preserve-scroll` и т.п.)\n * проходят через fallthrough-attrs.\n */\nimport { computed, markRaw, useAttrs, type Component } from 'vue'\n\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport IconExternal from '~icons/lucide/external-link'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n\nimport {\n baseRootClass,\n focusRingClass,\n type GrLinkSize,\n type GrLinkTone,\n type GrLinkUnderline,\n type GrLinkVariant,\n grLinkClass,\n grLinkColorStyle,\n} from './grLinkStyles'\n\nexport interface GrLinkProps {\n /**\n * Кастомный корневой тег/компонент. Если передан и компонент не `disabled` —\n * рендерится через `<component :is=\"as\">`. Игнорируется при `disabled`.\n */\n as?: string | Component\n href?: string\n external?: boolean\n target?: string\n rel?: string\n disabled?: boolean\n ariaLabel?: string\n /** Семантический цвет ссылки из палитры `GrTone`. */\n tone?: GrLinkTone\n /** Уровень акцента: `default` (окрашен) или `muted` (приглушён, акцент на hover). */\n variant?: GrLinkVariant\n underline?: GrLinkUnderline\n size?: GrLinkSize\n /**\n * Иконка внешней ссылки. По умолчанию показывается у любой ссылки, которая\n * открывается в новой вкладке, — не только при `external`.\n */\n externalIcon?: boolean\n /** i18n: скрытая подсказка «откроется в новой вкладке». */\n newTabLabel?: string\n}\n\ndefineOptions({\n inheritAttrs: false,\n})\n\nconst props = withDefaults(defineProps<GrLinkProps>(), {\n as: undefined,\n href: undefined,\n external: false,\n target: undefined,\n rel: undefined,\n disabled: false,\n ariaLabel: undefined,\n tone: 'primary',\n variant: 'default',\n underline: 'auto',\n size: undefined,\n externalIcon: undefined,\n newTabLabel: undefined,\n})\n\nconst attrs = useAttrs()\n\n// Кастомный корень имеет приоритет над `<a>` — GR не пытается угадывать\n// «правильный» тег за пользователя, если он явно его указал.\nconst isInteractive = computed(() => !props.disabled && (!!props.as || !!props.href))\n\nconst renderAs = computed<string | Component>(() => {\n if (!isInteractive.value) return 'span'\n if (props.as) {\n // Если `as` — объект-компонент, помечаем его как нереактивный, чтобы избежать\n // оверхеда от Vue-прокси и предупреждений в консоли. Строки (нативные теги)\n // оставляем как есть.\n return typeof props.as === 'string' ? props.as : markRaw(props.as)\n }\n return 'a'\n})\n\nconst resolvedTarget = computed(() => {\n if (props.target)\n return props.target\n\n if (props.external)\n return '_blank'\n\n return undefined\n})\n\nconst resolvedRel = computed(() => {\n if (props.rel)\n return props.rel\n\n // Авто-защита для любых ссылок, открывающихся в новой вкладке,\n // а не только когда выставлен `external`.\n if (resolvedTarget.value === '_blank')\n return 'noopener noreferrer'\n\n return undefined\n})\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrLink' })\n\nconst { t } = useGranularityTranslations()\n\n/**\n * Смена контекста должна быть объявлена (WCAG 3.2.5). Условие — ссылка реально\n * открывается в новой вкладке, а не наличие пропа `external`: `target=\"_blank\"`\n * снаружи даёт ровно тот же сюрприз (та же генерализация, что у `rel`).\n */\nconst opensInNewTab = computed(() => isInteractive.value && resolvedTarget.value === '_blank')\nconst newTabHint = computed(() => props.newTabLabel ?? t('gr.link.opensInNewTab', 'opens in a new tab'))\n\nconst showExternalIcon = computed(() => props.externalIcon ?? opensInNewTab.value)\n\n/**\n * `aria-label` перекрывает содержимое целиком — вместе со скрытой подсказкой.\n * Поэтому, если имя задано руками, подсказка дописывается к нему.\n */\nconst resolvedAriaLabel = computed(() => {\n if (!props.ariaLabel)\n return undefined\n\n return opensInNewTab.value ? `${props.ariaLabel}, ${newTabHint.value}` : props.ariaLabel\n})\n\nconst iconSize = computed(() => (resolvedSize.value === 'lg' ? 'sm' : 'xs'))\n\nconst rootClass = computed(() => {\n const variantClass = grLinkClass({\n size: resolvedSize.value,\n underline: props.underline,\n disabled: props.disabled,\n })\n\n // На disabled-ветке (`<span>`) focus-ring не нужен: элемент не фокусируется,\n // а если снаружи ему навесят `tabindex`, фокус-рамка на «отключённой» ссылке смотрелась бы странно.\n return props.disabled\n ? `${baseRootClass} ${variantClass}`\n : `${baseRootClass} ${focusRingClass} ${variantClass}`\n})\n\n// Цвет (tone × variant) прокидываем через CSS-переменные, чтобы не плодить классы.\nconst colorStyle = computed(() => grLinkColorStyle({\n tone: props.tone,\n variant: props.variant,\n disabled: props.disabled,\n}))\n\ndefineSlots<{\n /** Текст ссылки. */\n default?: () => any\n}>()\n\n</script>\n\n<template>\n <component\n :is=\"renderAs\"\n v-bind=\"attrs\"\n :href=\"isInteractive ? href : undefined\"\n :target=\"isInteractive ? resolvedTarget : undefined\"\n :rel=\"isInteractive ? resolvedRel : undefined\"\n :aria-label=\"resolvedAriaLabel\"\n :aria-disabled=\"disabled ? 'true' : undefined\"\n :class=\"rootClass\"\n :style=\"colorStyle\"\n >\n <slot />\n\n <GrIcon v-if=\"showExternalIcon\" :size=\"iconSize\">\n <IconExternal />\n </GrIcon>\n\n <!-- Подсказка не дублируется, когда её уже вобрал `aria-label`. -->\n <span v-if=\"opensInNewTab && !ariaLabel\" class=\"sr-only\">({{ newTabHint }})</span>\n </component>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoEA,MAAM,QAAQ;EAgBd,MAAM,QAAQ,SAAS;EAIvB,MAAM,gBAAgB,eAAe,CAAC,MAAM,aAAa,CAAC,CAAC,MAAM,MAAM,CAAC,CAAC,MAAM,KAAK;EAEpF,MAAM,WAAW,eAAmC;GAClD,IAAI,CAAC,cAAc,OAAO,OAAO;GACjC,IAAI,MAAM,IAIR,OAAO,OAAO,MAAM,OAAO,WAAW,MAAM,KAAK,QAAQ,MAAM,EAAE;GAEnE,OAAO;EACT,CAAC;EAED,MAAM,iBAAiB,eAAe;GACpC,IAAI,MAAM,QACR,OAAO,MAAM;GAEf,IAAI,MAAM,UACR,OAAO;EAGX,CAAC;EAED,MAAM,cAAc,eAAe;GACjC,IAAI,MAAM,KACR,OAAO,MAAM;GAIf,IAAI,eAAe,UAAU,UAC3B,OAAO;EAGX,CAAC;EAED,MAAM,eAAe,yBAAyB,MAAM,MAAM,EAAE,WAAW,SAAS,CAAC;EAEjF,MAAM,EAAE,MAAM,2BAA2B;;;;;;EAOzC,MAAM,gBAAgB,eAAe,cAAc,SAAS,eAAe,UAAU,QAAQ;EAC7F,MAAM,aAAa,eAAe,MAAM,eAAe,EAAE,yBAAyB,oBAAoB,CAAC;EAEvG,MAAM,mBAAmB,eAAe,MAAM,gBAAgB,cAAc,KAAK;;;;;EAMjF,MAAM,oBAAoB,eAAe;GACvC,IAAI,CAAC,MAAM,WACT,OAAO,KAAA;GAET,OAAO,cAAc,QAAQ,GAAG,MAAM,UAAU,IAAI,WAAW,UAAU,MAAM;EACjF,CAAC;EAED,MAAM,WAAW,eAAgB,aAAa,UAAU,OAAO,OAAO,IAAK;EAE3E,MAAM,YAAY,eAAe;GAC/B,MAAM,eAAe,YAAY;IAC/B,MAAM,aAAa;IACnB,WAAW,MAAM;IACjB,UAAU,MAAM;GAClB,CAAC;GAID,OAAO,MAAM,WACT,GAAG,cAAc,GAAG,iBACpB,GAAG,cAAc,GAAG,eAAe,GAAG;EAC5C,CAAC;EAGD,MAAM,aAAa,eAAe,iBAAiB;GACjD,MAAM,MAAM;GACZ,SAAS,MAAM;GACf,UAAU,MAAM;EAClB,CAAC,CAAC;;uBAUA,YAmBY,wBAlBL,SAAA,KAAQ,GADf,WAEU,MAiBE,KAAA,GAjBG;IACZ,MAAM,cAAA,QAAgB,QAAA,OAAO,KAAA;IAC7B,QAAQ,cAAA,QAAgB,eAAA,QAAiB,KAAA;IACzC,KAAK,cAAA,QAAgB,YAAA,QAAc,KAAA;IACnC,cAAY,kBAAA;IACZ,iBAAe,QAAA,WAAQ,SAAY,KAAA;IACnC,OAAO,UAAA;IACP,OAAO,WAAA;;2BAEA;KAAR,WAAQ,KAAA,QAAA,SAAA;KAEM,iBAAA,SAAA,UAAA,GAAd,YAES,gBAAA;;MAFwB,MAAM,SAAA;;6BACrB,CAAhB,YAAgB,MAAA,qBAAA,CAAA,CAAA,CAAA;;;KAIN,cAAA,SAAa,CAAK,QAAA,aAAA,UAAA,GAA9B,mBAAkF,QAAlF,YAAyD,MAAC,gBAAG,WAAA,KAAU,IAAG,KAAC,CAAA,KAAA,mBAAA,IAAA,IAAA"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { t as GrLink_default } from "./chunks/GrLink-
|
|
1
|
+
import { t as GrLink_default } from "./chunks/GrLink-DYXw3W9w.js";
|
|
2
2
|
import { n as grLinkSafelist, t as grLinkConfig } from "../../chunks/config-CQLNjc9X2.js";
|
|
3
3
|
export { GrLink_default as GrLink, GrLink_default as default, grLinkConfig, grLinkSafelist };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as useGranularityTranslations } from "../../../chunks/granularityI18n-BC5zsj_d.js";
|
|
2
2
|
import { t as loader_circle_default } from "../../../chunks/loader-circle-CfVLz7lb.js";
|
|
3
|
-
import { t as GrIcon_default } from "../../GrIcon/chunks/GrIcon-
|
|
3
|
+
import { t as GrIcon_default } from "../../GrIcon/chunks/GrIcon-Dmf0c9ba.js";
|
|
4
4
|
import { n as grLoadingRootClass } from "../../../chunks/grLoadingStyles-CnQaf5ry.js";
|
|
5
5
|
import { computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, openBlock, ref, renderSlot, resolveDynamicComponent, toDisplayString, withCtx } from "vue";
|
|
6
6
|
//#region src/components/GrLoading/GrLoading.vue?vue&type=script&setup=true&lang.ts
|
package/dist/components/GrLoading/chunks/{GrLoading-DDD8WAIH.js.map → GrLoading-CMD3Ri9P.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GrLoading-DDD8WAIH.js","names":[],"sources":["../../../../src/components/GrLoading/GrLoading.vue","../../../../src/components/GrLoading/GrLoading.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, type Component } from 'vue'\n\nimport IconLoader from '~icons/lucide/loader-circle'\n\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport type { GrIconSize, GrIconTone } from '../GrIcon/grIconStyles'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { grLoadingRootClass } from './grLoadingStyles'\n\n/**\n * Пропы {@link GrLoading}.\n *\n * Оверлей загрузки: спиннер с подписью поверх соседнего контента. `fullscreen`\n * накрывает весь экран, иначе оверлей ложится на ближайшего позиционированного\n * предка.\n */\nexport interface GrLoadingProps {\n /** Подпись под спиннером. Пустая строка убирает её совсем. По умолчанию — из локали. */\n text?: string\n /** Свой компонент спиннера вместо иконки по умолчанию. */\n spinner?: Component\n /** Дополнительные классы обёртки спиннера. */\n spinnerClass?: string\n /** Размер спиннера: шкала пакета либо произвольный в пикселях. */\n spinnerSize?: GrIconSize | number\n /** Тон спиннера из палитры. */\n spinnerTone?: GrIconTone\n /** Вращение спиннера. По умолчанию включено. */\n animated?: boolean\n /** Свой `background-color`. Задан — дефолтный скрим `--gr-overlay-bg` снимается. */\n background?: string\n /** Накрыть весь экран (`position: fixed`) вместо ближайшего позиционированного предка. */\n fullscreen?: boolean\n /** Имя CSS-переменной слоя — escape-hatch мимо `--gr-z-loading`. */\n zIndexVar?: string\n /** Задержка показа в миллисекундах: короткая загрузка не мигает оверлеем. */\n delay?: number\n /** Дополнительные классы корня оверлея. */\n customClass?: string\n}\n\nexport interface GrLoadingEmits {\n /**\n * Оверлей появился на экране — сразу или по истечении `delay`. По этому\n * событию директива блокирует контент под оверлеем: до показа блокировать\n * нечего, иначе быстрый запрос молча «замораживал» бы форму.\n */\n (e: 'show'): void\n}\n\nconst props = withDefaults(\n defineProps<GrLoadingProps>(),\n {\n text: undefined,\n spinner: undefined,\n spinnerClass: undefined,\n // 28px не входит в шкалу иконок (14–20): спиннер загрузки — самостоятельный\n // акцент панели, а не иконка рядом с текстом.\n spinnerSize: 28,\n spinnerTone: 'neutral',\n animated: true,\n background: undefined,\n fullscreen: false,\n zIndexVar: undefined,\n delay: 0,\n customClass: undefined,\n },\n)\n\nconst emit = defineEmits<GrLoadingEmits>()\n\ndefineSlots<{\n /** Содержимое панели целиком вместо спиннера с подписью. */\n default?: () => any\n}>()\n\nconst { t } = useGranularityTranslations()\n\nconst Spinner = computed(() => props.spinner ?? IconLoader)\n\nconst displayText = computed(() => props.text ?? t('gr.loading.defaultText', 'Loading...'))\n\nconst rootClass = computed(() => {\n return grLoadingRootClass({\n fullscreen: props.fullscreen,\n hasBackground: props.background !== undefined,\n customClass: props.customClass,\n })\n})\n\nconst rootStyle = computed(() => {\n return {\n backgroundColor: props.background,\n zIndex: props.zIndexVar ? `var(${props.zIndexVar})` : undefined,\n } as Record<string, string | undefined>\n})\n\n// Оверлей на быстром запросе успевает мигнуть и раздражает сильнее, чем его\n// отсутствие. Отсчёт начинается с монтирования: компонент создаётся ровно в\n// момент старта загрузки — и в `v-if`, и в директиве.\nconst visible = ref(props.delay <= 0)\nlet delayTimer: ReturnType<typeof setTimeout> | undefined\n\nonMounted(() => {\n if (visible.value) {\n emit('show')\n return\n }\n\n delayTimer = setTimeout(() => {\n visible.value = true\n emit('show')\n }, props.delay)\n})\n\nonBeforeUnmount(() => {\n clearTimeout(delayTimer)\n})\n</script>\n\n<template>\n <div\n v-if=\"visible\"\n data-gr-loading\n class=\"flex items-center justify-center cursor-wait select-none pointer-events-auto\"\n :class=\"rootClass\"\n :style=\"rootStyle\"\n role=\"status\"\n aria-live=\"polite\"\n >\n <div\n data-gr-loading-panel\n class=\"flex flex-col items-center justify-center gap-2 text-center rounded-[var(--gr-radius-md)] bg-[var(--gr-bg)]/55 px-5 py-4 shadow-[var(--gr-shadow-2)]\"\n >\n <slot>\n <GrIcon\n data-gr-loading-spinner\n :size=\"spinnerSize\"\n :tone=\"spinnerTone\"\n :spin=\"animated\"\n :class=\"spinnerClass\"\n >\n <component :is=\"Spinner\" />\n </GrIcon>\n <div v-if=\"displayText\" data-gr-loading-text class=\"text-[length:var(--gr-text-sm)] leading-[var(--gr-leading-sm)] text-[var(--gr-muted-fg)]\">\n {{ displayText }}\n </div>\n </slot>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, type Component } from 'vue'\n\nimport IconLoader from '~icons/lucide/loader-circle'\n\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport type { GrIconSize, GrIconTone } from '../GrIcon/grIconStyles'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { grLoadingRootClass } from './grLoadingStyles'\n\n/**\n * Пропы {@link GrLoading}.\n *\n * Оверлей загрузки: спиннер с подписью поверх соседнего контента. `fullscreen`\n * накрывает весь экран, иначе оверлей ложится на ближайшего позиционированного\n * предка.\n */\nexport interface GrLoadingProps {\n /** Подпись под спиннером. Пустая строка убирает её совсем. По умолчанию — из локали. */\n text?: string\n /** Свой компонент спиннера вместо иконки по умолчанию. */\n spinner?: Component\n /** Дополнительные классы обёртки спиннера. */\n spinnerClass?: string\n /** Размер спиннера: шкала пакета либо произвольный в пикселях. */\n spinnerSize?: GrIconSize | number\n /** Тон спиннера из палитры. */\n spinnerTone?: GrIconTone\n /** Вращение спиннера. По умолчанию включено. */\n animated?: boolean\n /** Свой `background-color`. Задан — дефолтный скрим `--gr-overlay-bg` снимается. */\n background?: string\n /** Накрыть весь экран (`position: fixed`) вместо ближайшего позиционированного предка. */\n fullscreen?: boolean\n /** Имя CSS-переменной слоя — escape-hatch мимо `--gr-z-loading`. */\n zIndexVar?: string\n /** Задержка показа в миллисекундах: короткая загрузка не мигает оверлеем. */\n delay?: number\n /** Дополнительные классы корня оверлея. */\n customClass?: string\n}\n\nexport interface GrLoadingEmits {\n /**\n * Оверлей появился на экране — сразу или по истечении `delay`. По этому\n * событию директива блокирует контент под оверлеем: до показа блокировать\n * нечего, иначе быстрый запрос молча «замораживал» бы форму.\n */\n (e: 'show'): void\n}\n\nconst props = withDefaults(\n defineProps<GrLoadingProps>(),\n {\n text: undefined,\n spinner: undefined,\n spinnerClass: undefined,\n // 28px не входит в шкалу иконок (14–20): спиннер загрузки — самостоятельный\n // акцент панели, а не иконка рядом с текстом.\n spinnerSize: 28,\n spinnerTone: 'neutral',\n animated: true,\n background: undefined,\n fullscreen: false,\n zIndexVar: undefined,\n delay: 0,\n customClass: undefined,\n },\n)\n\nconst emit = defineEmits<GrLoadingEmits>()\n\ndefineSlots<{\n /** Содержимое панели целиком вместо спиннера с подписью. */\n default?: () => any\n}>()\n\nconst { t } = useGranularityTranslations()\n\nconst Spinner = computed(() => props.spinner ?? IconLoader)\n\nconst displayText = computed(() => props.text ?? t('gr.loading.defaultText', 'Loading...'))\n\nconst rootClass = computed(() => {\n return grLoadingRootClass({\n fullscreen: props.fullscreen,\n hasBackground: props.background !== undefined,\n customClass: props.customClass,\n })\n})\n\nconst rootStyle = computed(() => {\n return {\n backgroundColor: props.background,\n zIndex: props.zIndexVar ? `var(${props.zIndexVar})` : undefined,\n } as Record<string, string | undefined>\n})\n\n// Оверлей на быстром запросе успевает мигнуть и раздражает сильнее, чем его\n// отсутствие. Отсчёт начинается с монтирования: компонент создаётся ровно в\n// момент старта загрузки — и в `v-if`, и в директиве.\nconst visible = ref(props.delay <= 0)\nlet delayTimer: ReturnType<typeof setTimeout> | undefined\n\nonMounted(() => {\n if (visible.value) {\n emit('show')\n return\n }\n\n delayTimer = setTimeout(() => {\n visible.value = true\n emit('show')\n }, props.delay)\n})\n\nonBeforeUnmount(() => {\n clearTimeout(delayTimer)\n})\n</script>\n\n<template>\n <div\n v-if=\"visible\"\n data-gr-loading\n class=\"flex items-center justify-center cursor-wait select-none pointer-events-auto\"\n :class=\"rootClass\"\n :style=\"rootStyle\"\n role=\"status\"\n aria-live=\"polite\"\n >\n <div\n data-gr-loading-panel\n class=\"flex flex-col items-center justify-center gap-2 text-center rounded-[var(--gr-radius-md)] bg-[var(--gr-bg)]/55 px-5 py-4 shadow-[var(--gr-shadow-2)]\"\n >\n <slot>\n <GrIcon\n data-gr-loading-spinner\n :size=\"spinnerSize\"\n :tone=\"spinnerTone\"\n :spin=\"animated\"\n :class=\"spinnerClass\"\n >\n <component :is=\"Spinner\" />\n </GrIcon>\n <div v-if=\"displayText\" data-gr-loading-text class=\"text-[length:var(--gr-text-sm)] leading-[var(--gr-leading-sm)] text-[var(--gr-muted-fg)]\">\n {{ displayText }}\n </div>\n </slot>\n </div>\n </div>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmDA,MAAM,QAAQ;EAmBd,MAAM,OAAO;EAOb,MAAM,EAAE,MAAM,2BAA2B;EAEzC,MAAM,UAAU,eAAe,MAAM,WAAW,qBAAU;EAE1D,MAAM,cAAc,eAAe,MAAM,QAAQ,EAAE,0BAA0B,YAAY,CAAC;EAE1F,MAAM,YAAY,eAAe;GAC/B,OAAO,mBAAmB;IACxB,YAAY,MAAM;IAClB,eAAe,MAAM,eAAe,KAAA;IACpC,aAAa,MAAM;GACrB,CAAC;EACH,CAAC;EAED,MAAM,YAAY,eAAe;GAC/B,OAAO;IACL,iBAAiB,MAAM;IACvB,QAAQ,MAAM,YAAY,OAAO,MAAM,UAAU,KAAK,KAAA;GACxD;EACF,CAAC;EAKD,MAAM,UAAU,IAAI,MAAM,SAAS,CAAC;EACpC,IAAI;EAEJ,gBAAgB;GACd,IAAI,QAAQ,OAAO;IACjB,KAAK,MAAM;IACX;GACF;GAEA,aAAa,iBAAiB;IAC5B,QAAQ,QAAQ;IAChB,KAAK,MAAM;GACb,GAAG,MAAM,KAAK;EAChB,CAAC;EAED,sBAAsB;GACpB,aAAa,UAAU;EACzB,CAAC;;UAKS,QAAA,SAAA,UAAA,GADR,mBA4BM,OAAA;;IA1BJ,mBAAA;IACA,OAAK,eAAA,CAAC,gFACE,UAAA,KAAS,CAAA;IAChB,OAAK,eAAE,UAAA,KAAS;IACjB,MAAK;IACL,aAAU;OAEV,mBAkBM,OAlBN,YAkBM,CAdJ,WAaO,KAAA,QAAA,WAAA,CAAA,SAAA,CAZL,YAQS,gBAAA;IAPP,2BAAA;IACC,MAAM,QAAA;IACN,MAAM,QAAA;IACN,MAAM,QAAA;IACN,OAAK,eAAE,QAAA,YAAY;;2BAEO,EAAA,UAAA,GAA3B,YAA2B,wBAAX,QAAA,KAAO,CAAA,EAAA,CAAA;;;;;;;OAEd,YAAA,SAAA,UAAA,GAAX,mBAEM,OAFN,YAEM,gBADD,YAAA,KAAW,GAAA,CAAA,KAAA,mBAAA,IAAA,IAAA,CAAA,CAAA,CAAA,CAAA,CAAA,GAAA,CAAA,KAAA,mBAAA,IAAA,IAAA"}
|
|
1
|
+
{"version":3,"file":"GrLoading-CMD3Ri9P.js","names":[],"sources":["../../../../src/components/GrLoading/GrLoading.vue","../../../../src/components/GrLoading/GrLoading.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, type Component } from 'vue'\n\nimport IconLoader from '~icons/lucide/loader-circle'\n\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport type { GrIconSize, GrIconTone } from '../GrIcon/grIconStyles'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { grLoadingRootClass } from './grLoadingStyles'\n\n/**\n * Пропы {@link GrLoading}.\n *\n * Оверлей загрузки: спиннер с подписью поверх соседнего контента. `fullscreen`\n * накрывает весь экран, иначе оверлей ложится на ближайшего позиционированного\n * предка.\n */\nexport interface GrLoadingProps {\n /** Подпись под спиннером. Пустая строка убирает её совсем. По умолчанию — из локали. */\n text?: string\n /** Свой компонент спиннера вместо иконки по умолчанию. */\n spinner?: Component\n /** Дополнительные классы обёртки спиннера. */\n spinnerClass?: string\n /** Размер спиннера: шкала пакета либо произвольный в пикселях. */\n spinnerSize?: GrIconSize | number\n /** Тон спиннера из палитры. */\n spinnerTone?: GrIconTone\n /** Вращение спиннера. По умолчанию включено. */\n animated?: boolean\n /** Свой `background-color`. Задан — дефолтный скрим `--gr-overlay-bg` снимается. */\n background?: string\n /** Накрыть весь экран (`position: fixed`) вместо ближайшего позиционированного предка. */\n fullscreen?: boolean\n /** Имя CSS-переменной слоя — escape-hatch мимо `--gr-z-loading`. */\n zIndexVar?: string\n /** Задержка показа в миллисекундах: короткая загрузка не мигает оверлеем. */\n delay?: number\n /** Дополнительные классы корня оверлея. */\n customClass?: string\n}\n\nexport interface GrLoadingEmits {\n /**\n * Оверлей появился на экране — сразу или по истечении `delay`. По этому\n * событию директива блокирует контент под оверлеем: до показа блокировать\n * нечего, иначе быстрый запрос молча «замораживал» бы форму.\n */\n (e: 'show'): void\n}\n\nconst props = withDefaults(\n defineProps<GrLoadingProps>(),\n {\n text: undefined,\n spinner: undefined,\n spinnerClass: undefined,\n // 28px не входит в шкалу иконок (14–20): спиннер загрузки — самостоятельный\n // акцент панели, а не иконка рядом с текстом.\n spinnerSize: 28,\n spinnerTone: 'neutral',\n animated: true,\n background: undefined,\n fullscreen: false,\n zIndexVar: undefined,\n delay: 0,\n customClass: undefined,\n },\n)\n\nconst emit = defineEmits<GrLoadingEmits>()\n\ndefineSlots<{\n /** Содержимое панели целиком вместо спиннера с подписью. */\n default?: () => any\n}>()\n\nconst { t } = useGranularityTranslations()\n\nconst Spinner = computed(() => props.spinner ?? IconLoader)\n\nconst displayText = computed(() => props.text ?? t('gr.loading.defaultText', 'Loading...'))\n\nconst rootClass = computed(() => {\n return grLoadingRootClass({\n fullscreen: props.fullscreen,\n hasBackground: props.background !== undefined,\n customClass: props.customClass,\n })\n})\n\nconst rootStyle = computed(() => {\n return {\n backgroundColor: props.background,\n zIndex: props.zIndexVar ? `var(${props.zIndexVar})` : undefined,\n } as Record<string, string | undefined>\n})\n\n// Оверлей на быстром запросе успевает мигнуть и раздражает сильнее, чем его\n// отсутствие. Отсчёт начинается с монтирования: компонент создаётся ровно в\n// момент старта загрузки — и в `v-if`, и в директиве.\nconst visible = ref(props.delay <= 0)\nlet delayTimer: ReturnType<typeof setTimeout> | undefined\n\nonMounted(() => {\n if (visible.value) {\n emit('show')\n return\n }\n\n delayTimer = setTimeout(() => {\n visible.value = true\n emit('show')\n }, props.delay)\n})\n\nonBeforeUnmount(() => {\n clearTimeout(delayTimer)\n})\n</script>\n\n<template>\n <div\n v-if=\"visible\"\n data-gr-loading\n class=\"flex items-center justify-center cursor-wait select-none pointer-events-auto\"\n :class=\"rootClass\"\n :style=\"rootStyle\"\n role=\"status\"\n aria-live=\"polite\"\n >\n <div\n data-gr-loading-panel\n class=\"flex flex-col items-center justify-center gap-2 text-center rounded-[var(--gr-radius-md)] bg-[var(--gr-bg)]/55 px-5 py-4 shadow-[var(--gr-shadow-2)]\"\n >\n <slot>\n <GrIcon\n data-gr-loading-spinner\n :size=\"spinnerSize\"\n :tone=\"spinnerTone\"\n :spin=\"animated\"\n :class=\"spinnerClass\"\n >\n <component :is=\"Spinner\" />\n </GrIcon>\n <div v-if=\"displayText\" data-gr-loading-text class=\"text-[length:var(--gr-text-sm)] leading-[var(--gr-leading-sm)] text-[var(--gr-muted-fg)]\">\n {{ displayText }}\n </div>\n </slot>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, type Component } from 'vue'\n\nimport IconLoader from '~icons/lucide/loader-circle'\n\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport type { GrIconSize, GrIconTone } from '../GrIcon/grIconStyles'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { grLoadingRootClass } from './grLoadingStyles'\n\n/**\n * Пропы {@link GrLoading}.\n *\n * Оверлей загрузки: спиннер с подписью поверх соседнего контента. `fullscreen`\n * накрывает весь экран, иначе оверлей ложится на ближайшего позиционированного\n * предка.\n */\nexport interface GrLoadingProps {\n /** Подпись под спиннером. Пустая строка убирает её совсем. По умолчанию — из локали. */\n text?: string\n /** Свой компонент спиннера вместо иконки по умолчанию. */\n spinner?: Component\n /** Дополнительные классы обёртки спиннера. */\n spinnerClass?: string\n /** Размер спиннера: шкала пакета либо произвольный в пикселях. */\n spinnerSize?: GrIconSize | number\n /** Тон спиннера из палитры. */\n spinnerTone?: GrIconTone\n /** Вращение спиннера. По умолчанию включено. */\n animated?: boolean\n /** Свой `background-color`. Задан — дефолтный скрим `--gr-overlay-bg` снимается. */\n background?: string\n /** Накрыть весь экран (`position: fixed`) вместо ближайшего позиционированного предка. */\n fullscreen?: boolean\n /** Имя CSS-переменной слоя — escape-hatch мимо `--gr-z-loading`. */\n zIndexVar?: string\n /** Задержка показа в миллисекундах: короткая загрузка не мигает оверлеем. */\n delay?: number\n /** Дополнительные классы корня оверлея. */\n customClass?: string\n}\n\nexport interface GrLoadingEmits {\n /**\n * Оверлей появился на экране — сразу или по истечении `delay`. По этому\n * событию директива блокирует контент под оверлеем: до показа блокировать\n * нечего, иначе быстрый запрос молча «замораживал» бы форму.\n */\n (e: 'show'): void\n}\n\nconst props = withDefaults(\n defineProps<GrLoadingProps>(),\n {\n text: undefined,\n spinner: undefined,\n spinnerClass: undefined,\n // 28px не входит в шкалу иконок (14–20): спиннер загрузки — самостоятельный\n // акцент панели, а не иконка рядом с текстом.\n spinnerSize: 28,\n spinnerTone: 'neutral',\n animated: true,\n background: undefined,\n fullscreen: false,\n zIndexVar: undefined,\n delay: 0,\n customClass: undefined,\n },\n)\n\nconst emit = defineEmits<GrLoadingEmits>()\n\ndefineSlots<{\n /** Содержимое панели целиком вместо спиннера с подписью. */\n default?: () => any\n}>()\n\nconst { t } = useGranularityTranslations()\n\nconst Spinner = computed(() => props.spinner ?? IconLoader)\n\nconst displayText = computed(() => props.text ?? t('gr.loading.defaultText', 'Loading...'))\n\nconst rootClass = computed(() => {\n return grLoadingRootClass({\n fullscreen: props.fullscreen,\n hasBackground: props.background !== undefined,\n customClass: props.customClass,\n })\n})\n\nconst rootStyle = computed(() => {\n return {\n backgroundColor: props.background,\n zIndex: props.zIndexVar ? `var(${props.zIndexVar})` : undefined,\n } as Record<string, string | undefined>\n})\n\n// Оверлей на быстром запросе успевает мигнуть и раздражает сильнее, чем его\n// отсутствие. Отсчёт начинается с монтирования: компонент создаётся ровно в\n// момент старта загрузки — и в `v-if`, и в директиве.\nconst visible = ref(props.delay <= 0)\nlet delayTimer: ReturnType<typeof setTimeout> | undefined\n\nonMounted(() => {\n if (visible.value) {\n emit('show')\n return\n }\n\n delayTimer = setTimeout(() => {\n visible.value = true\n emit('show')\n }, props.delay)\n})\n\nonBeforeUnmount(() => {\n clearTimeout(delayTimer)\n})\n</script>\n\n<template>\n <div\n v-if=\"visible\"\n data-gr-loading\n class=\"flex items-center justify-center cursor-wait select-none pointer-events-auto\"\n :class=\"rootClass\"\n :style=\"rootStyle\"\n role=\"status\"\n aria-live=\"polite\"\n >\n <div\n data-gr-loading-panel\n class=\"flex flex-col items-center justify-center gap-2 text-center rounded-[var(--gr-radius-md)] bg-[var(--gr-bg)]/55 px-5 py-4 shadow-[var(--gr-shadow-2)]\"\n >\n <slot>\n <GrIcon\n data-gr-loading-spinner\n :size=\"spinnerSize\"\n :tone=\"spinnerTone\"\n :spin=\"animated\"\n :class=\"spinnerClass\"\n >\n <component :is=\"Spinner\" />\n </GrIcon>\n <div v-if=\"displayText\" data-gr-loading-text class=\"text-[length:var(--gr-text-sm)] leading-[var(--gr-leading-sm)] text-[var(--gr-muted-fg)]\">\n {{ displayText }}\n </div>\n </slot>\n </div>\n </div>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmDA,MAAM,QAAQ;EAmBd,MAAM,OAAO;EAOb,MAAM,EAAE,MAAM,2BAA2B;EAEzC,MAAM,UAAU,eAAe,MAAM,WAAW,qBAAU;EAE1D,MAAM,cAAc,eAAe,MAAM,QAAQ,EAAE,0BAA0B,YAAY,CAAC;EAE1F,MAAM,YAAY,eAAe;GAC/B,OAAO,mBAAmB;IACxB,YAAY,MAAM;IAClB,eAAe,MAAM,eAAe,KAAA;IACpC,aAAa,MAAM;GACrB,CAAC;EACH,CAAC;EAED,MAAM,YAAY,eAAe;GAC/B,OAAO;IACL,iBAAiB,MAAM;IACvB,QAAQ,MAAM,YAAY,OAAO,MAAM,UAAU,KAAK,KAAA;GACxD;EACF,CAAC;EAKD,MAAM,UAAU,IAAI,MAAM,SAAS,CAAC;EACpC,IAAI;EAEJ,gBAAgB;GACd,IAAI,QAAQ,OAAO;IACjB,KAAK,MAAM;IACX;GACF;GAEA,aAAa,iBAAiB;IAC5B,QAAQ,QAAQ;IAChB,KAAK,MAAM;GACb,GAAG,MAAM,KAAK;EAChB,CAAC;EAED,sBAAsB;GACpB,aAAa,UAAU;EACzB,CAAC;;UAKS,QAAA,SAAA,UAAA,GADR,mBA4BM,OAAA;;IA1BJ,mBAAA;IACA,OAAK,eAAA,CAAC,gFACE,UAAA,KAAS,CAAA;IAChB,OAAK,eAAE,UAAA,KAAS;IACjB,MAAK;IACL,aAAU;OAEV,mBAkBM,OAlBN,YAkBM,CAdJ,WAaO,KAAA,QAAA,WAAA,CAAA,SAAA,CAZL,YAQS,gBAAA;IAPP,2BAAA;IACC,MAAM,QAAA;IACN,MAAM,QAAA;IACN,MAAM,QAAA;IACN,OAAK,eAAE,QAAA,YAAY;;2BAEO,EAAA,UAAA,GAA3B,YAA2B,wBAAX,QAAA,KAAO,CAAA,EAAA,CAAA;;;;;;;OAEd,YAAA,SAAA,UAAA,GAAX,mBAEM,OAFN,YAEM,gBADD,YAAA,KAAW,GAAA,CAAA,KAAA,mBAAA,IAAA,IAAA,CAAA,CAAA,CAAA,CAAA,CAAA,GAAA,CAAA,KAAA,mBAAA,IAAA,IAAA"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { t as GrLoading_default } from "./chunks/GrLoading-
|
|
1
|
+
import { t as GrLoading_default } from "./chunks/GrLoading-CMD3Ri9P.js";
|
|
2
2
|
import { n as grLoadingSafelist, t as grLoadingConfig } from "../../chunks/config-Bjn30smH.js";
|
|
3
3
|
export { GrLoading_default as GrLoading, GrLoading_default as default, grLoadingConfig, grLoadingSafelist };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as useGranularityTranslations } from "../../../chunks/granularityI18n-BC5zsj_d.js";
|
|
2
|
-
import { t as GrIcon_default } from "../../GrIcon/chunks/GrIcon-
|
|
2
|
+
import { t as GrIcon_default } from "../../GrIcon/chunks/GrIcon-Dmf0c9ba.js";
|
|
3
3
|
import { t as GrButton_default } from "../../GrButton/chunks/GrButton-Bw2AH4uX.js";
|
|
4
4
|
import { a as navbarRightAlignClass, c as navbarTitleClass, i as navbarCenterClass, o as navbarSideClass, r as grNavbarRootClass, s as navbarSideGrowClass } from "../../../chunks/config-2ZHRsdOc.js";
|
|
5
5
|
import { computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, markRaw, normalizeClass, openBlock, renderSlot, toDisplayString, unref, useSlots, withCtx } from "vue";
|
package/dist/components/GrNavbar/chunks/{GrNavbar-DhswlYyh.js.map → GrNavbar-C5EjMKaC.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GrNavbar-
|
|
1
|
+
{"version":3,"file":"GrNavbar-C5EjMKaC.js","names":[],"sources":["../../../../src/components/GrNavbar/GrNavbar.vue","../../../../src/components/GrNavbar/GrNavbar.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nimport IconMenu from '~icons/lucide/menu'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrIcon from '../GrIcon/GrIcon.vue'\n\nimport {\n grNavbarRootClass,\n navbarCenterClass,\n navbarRightAlignClass,\n navbarSideClass,\n navbarSideGrowClass,\n navbarTitleClass,\n} from './grNavbarStyles'\n\n/**\n * GrNavbar — верхняя панель приложения (header) с тремя зонами: слева заголовок\n * и кнопка меню, по центру — свободная зона (поиск, хлебные крошки), справа —\n * действия и аватар.\n *\n * - Корень — `<header>` (landmark), без собственного `role`.\n * - `aria-label` кнопки меню локализуется через `useGranularityTranslations`.\n */\nexport interface GrNavbarProps {\n /** Заголовок строкой. Слот `#title` сильнее и позволяет обойтись без пропа. */\n title?: string\n showMenuButton?: boolean\n /** Extra classes applied to the menu button wrapper (e.g. `sm:hidden`). */\n menuButtonClass?: string\n /**\n * Панель прилипает к верху при прокрутке. Слой — `--gr-z-navbar`: он ниже\n * якорных панелей, чтобы открытый список не уезжал под шапку.\n */\n sticky?: boolean\n}\n\nexport interface GrNavbarEmits {\n (e: 'menu'): void\n}\n\nconst props = withDefaults(defineProps<GrNavbarProps>(), {\n title: undefined,\n showMenuButton: false,\n menuButtonClass: '',\n sticky: false,\n})\n\nconst slots = defineSlots<{\n /** Заголовок целиком — вместо строки `title`. */\n title?: () => unknown\n /** Зона сразу после заголовка: вкладки, переключатель раздела. */\n left?: () => unknown\n /** Центральная зона: поиск, хлебные крошки. */\n center?: () => unknown\n /** Правая зона: действия, аватар. */\n default?: () => unknown\n}>()\n\nconst emit = defineEmits<GrNavbarEmits>()\n\nconst { t } = useGranularityTranslations()\n\nconst menuAriaLabel = computed(() => t('gr.navbar.openMenu', 'Open menu'))\n\n// Пустой блок заголовка съедал бы отступ ряда, поэтому рендерим его только при\n// наличии содержимого — проп `title` для этого не обязателен.\nconst hasTitle = computed(() => Boolean(props.title || slots.title))\n\nconst rootClass = computed(() => grNavbarRootClass(props.sticky))\n\n// С центральной зоной боковые делят остаток поровну — иначе «центр» уезжает\n// вслед за более широким боком. Без неё правая просто прижата к краю.\nconst hasCenter = computed(() => Boolean(slots.center))\n\nconst sideClass = computed(() => [navbarSideClass, hasCenter.value ? navbarSideGrowClass : ''])\nconst rightClass = computed(() => [\n navbarSideClass,\n hasCenter.value ? `${navbarSideGrowClass} ${navbarRightAlignClass}` : 'ml-auto',\n])\n</script>\n\n<template>\n <header\n data-gr-navbar\n :data-sticky=\"sticky ? 'true' : undefined\"\n :class=\"rootClass\"\n >\n <div data-gr-navbar-left :class=\"sideClass\">\n <GrButton\n v-if=\"showMenuButton\"\n data-gr-navbar-menu\n variant=\"ghost\"\n size=\"sm\"\n square\n :aria-label=\"menuAriaLabel\"\n :class=\"menuButtonClass\"\n @click=\"emit('menu')\"\n >\n <GrIcon :size=\"16\">\n <IconMenu aria-hidden=\"true\" />\n </GrIcon>\n </GrButton>\n\n <div v-if=\"hasTitle\" data-gr-navbar-title :class=\"navbarTitleClass\">\n <slot name=\"title\">\n {{ title }}\n </slot>\n </div>\n\n <slot name=\"left\" />\n </div>\n\n <div v-if=\"hasCenter\" data-gr-navbar-center :class=\"navbarCenterClass\">\n <slot name=\"center\" />\n </div>\n\n <div data-gr-navbar-right :class=\"rightClass\">\n <slot />\n </div>\n </header>\n</template>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nimport IconMenu from '~icons/lucide/menu'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport GrButton from '../GrButton/GrButton.vue'\nimport GrIcon from '../GrIcon/GrIcon.vue'\n\nimport {\n grNavbarRootClass,\n navbarCenterClass,\n navbarRightAlignClass,\n navbarSideClass,\n navbarSideGrowClass,\n navbarTitleClass,\n} from './grNavbarStyles'\n\n/**\n * GrNavbar — верхняя панель приложения (header) с тремя зонами: слева заголовок\n * и кнопка меню, по центру — свободная зона (поиск, хлебные крошки), справа —\n * действия и аватар.\n *\n * - Корень — `<header>` (landmark), без собственного `role`.\n * - `aria-label` кнопки меню локализуется через `useGranularityTranslations`.\n */\nexport interface GrNavbarProps {\n /** Заголовок строкой. Слот `#title` сильнее и позволяет обойтись без пропа. */\n title?: string\n showMenuButton?: boolean\n /** Extra classes applied to the menu button wrapper (e.g. `sm:hidden`). */\n menuButtonClass?: string\n /**\n * Панель прилипает к верху при прокрутке. Слой — `--gr-z-navbar`: он ниже\n * якорных панелей, чтобы открытый список не уезжал под шапку.\n */\n sticky?: boolean\n}\n\nexport interface GrNavbarEmits {\n (e: 'menu'): void\n}\n\nconst props = withDefaults(defineProps<GrNavbarProps>(), {\n title: undefined,\n showMenuButton: false,\n menuButtonClass: '',\n sticky: false,\n})\n\nconst slots = defineSlots<{\n /** Заголовок целиком — вместо строки `title`. */\n title?: () => unknown\n /** Зона сразу после заголовка: вкладки, переключатель раздела. */\n left?: () => unknown\n /** Центральная зона: поиск, хлебные крошки. */\n center?: () => unknown\n /** Правая зона: действия, аватар. */\n default?: () => unknown\n}>()\n\nconst emit = defineEmits<GrNavbarEmits>()\n\nconst { t } = useGranularityTranslations()\n\nconst menuAriaLabel = computed(() => t('gr.navbar.openMenu', 'Open menu'))\n\n// Пустой блок заголовка съедал бы отступ ряда, поэтому рендерим его только при\n// наличии содержимого — проп `title` для этого не обязателен.\nconst hasTitle = computed(() => Boolean(props.title || slots.title))\n\nconst rootClass = computed(() => grNavbarRootClass(props.sticky))\n\n// С центральной зоной боковые делят остаток поровну — иначе «центр» уезжает\n// вслед за более широким боком. Без неё правая просто прижата к краю.\nconst hasCenter = computed(() => Boolean(slots.center))\n\nconst sideClass = computed(() => [navbarSideClass, hasCenter.value ? navbarSideGrowClass : ''])\nconst rightClass = computed(() => [\n navbarSideClass,\n hasCenter.value ? `${navbarSideGrowClass} ${navbarRightAlignClass}` : 'ml-auto',\n])\n</script>\n\n<template>\n <header\n data-gr-navbar\n :data-sticky=\"sticky ? 'true' : undefined\"\n :class=\"rootClass\"\n >\n <div data-gr-navbar-left :class=\"sideClass\">\n <GrButton\n v-if=\"showMenuButton\"\n data-gr-navbar-menu\n variant=\"ghost\"\n size=\"sm\"\n square\n :aria-label=\"menuAriaLabel\"\n :class=\"menuButtonClass\"\n @click=\"emit('menu')\"\n >\n <GrIcon :size=\"16\">\n <IconMenu aria-hidden=\"true\" />\n </GrIcon>\n </GrButton>\n\n <div v-if=\"hasTitle\" data-gr-navbar-title :class=\"navbarTitleClass\">\n <slot name=\"title\">\n {{ title }}\n </slot>\n </div>\n\n <slot name=\"left\" />\n </div>\n\n <div v-if=\"hasCenter\" data-gr-navbar-center :class=\"navbarCenterClass\">\n <slot name=\"center\" />\n </div>\n\n <div data-gr-navbar-right :class=\"rightClass\">\n <slot />\n </div>\n </header>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2CA,MAAM,QAAQ;EAOd,MAAM,QAAQ,SAAA;EAWd,MAAM,OAAO;EAEb,MAAM,EAAE,MAAM,2BAA2B;EAEzC,MAAM,gBAAgB,eAAe,EAAE,sBAAsB,WAAW,CAAC;EAIzE,MAAM,WAAW,eAAe,QAAQ,MAAM,SAAS,MAAM,KAAK,CAAC;EAEnE,MAAM,YAAY,eAAe,kBAAkB,MAAM,MAAM,CAAC;EAIhE,MAAM,YAAY,eAAe,QAAQ,MAAM,MAAM,CAAC;EAEtD,MAAM,YAAY,eAAe,CAAC,iBAAiB,UAAU,QAAQ,sBAAsB,EAAE,CAAC;EAC9F,MAAM,aAAa,eAAe,CAChC,iBACA,UAAU,QAAQ,GAAG,oBAAoB,GAAG,0BAA0B,SACxE,CAAC;;uBAIC,mBAqCS,UAAA;IApCP,kBAAA;IACC,eAAa,QAAA,SAAM,SAAY,KAAA;IAC/B,OAAK,eAAE,UAAA,KAAS;;IAEjB,mBAuBM,OAAA;KAvBD,uBAAA;KAAqB,OAAK,eAAE,UAAA,KAAS;;KAEhC,QAAA,kBAAA,UAAA,GADR,YAaW,kBAAA;;MAXT,uBAAA;MACA,SAAQ;MACR,MAAK;MACL,QAAA;MACC,cAAY,cAAA;MACZ,OAAK,eAAE,QAAA,eAAe;MACtB,SAAK,OAAA,OAAA,OAAA,MAAA,WAAE,KAAI,MAAA;;6BAIH,CAFT,YAES,gBAAA,EAFA,MAAM,GAAE,GAAA;8BACgB,CAA/B,YAA+B,MAAA,YAAA,GAAA,EAArB,eAAY,OAAM,CAAA,CAAA,CAAA;;;;;KAIrB,SAAA,SAAA,UAAA,GAAX,mBAIM,OAAA;;MAJe,wBAAA;MAAsB,OAAK,eAAE,MAAA,gBAAA,CAAgB;SAChE,WAEO,KAAA,QAAA,SAAA,CAAA,SAAA,CAAA,gBAAA,gBADF,QAAA,KAAK,GAAA,CAAA,CAAA,CAAA,CAAA,GAAA,CAAA,KAAA,mBAAA,IAAA,IAAA;KAIZ,WAAoB,KAAA,QAAA,MAAA;;IAGX,UAAA,SAAA,UAAA,GAAX,mBAEM,OAAA;;KAFgB,yBAAA;KAAuB,OAAK,eAAE,MAAA,iBAAA,CAAiB;QACnE,WAAsB,KAAA,QAAA,QAAA,CAAA,GAAA,CAAA,KAAA,mBAAA,IAAA,IAAA;IAGxB,mBAEM,OAAA;KAFD,wBAAA;KAAsB,OAAK,eAAE,WAAA,KAAU;QAC1C,WAAQ,KAAA,QAAA,SAAA,CAAA,GAAA,CAAA"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { t as GrNavbar_default } from "./chunks/GrNavbar-
|
|
1
|
+
import { t as GrNavbar_default } from "./chunks/GrNavbar-C5EjMKaC.js";
|
|
2
2
|
import { n as grNavbarSafelist, t as grNavbarConfig } from "../../chunks/config-2ZHRsdOc.js";
|
|
3
3
|
export { GrNavbar_default as GrNavbar, GrNavbar_default as default, grNavbarConfig, grNavbarSafelist };
|
package/dist/components/GrNumberInput/chunks/{GrNumberInput-Daud9Jdm.js → GrNumberInput-DQ59YICu.js}
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as x_default } from "../../../chunks/x-2KXnqxiD.js";
|
|
2
2
|
import { a as useGrComponentSize, i as useGrComponentProp } from "../../../chunks/context-CTbuwcYs.js";
|
|
3
3
|
import { n as useGranularityTranslations } from "../../../chunks/granularityI18n-BC5zsj_d.js";
|
|
4
|
-
import { t as GrIcon_default } from "../../GrIcon/chunks/GrIcon-
|
|
4
|
+
import { t as GrIcon_default } from "../../GrIcon/chunks/GrIcon-Dmf0c9ba.js";
|
|
5
5
|
import { n as useGrFormFieldContext } from "../../../chunks/context-DdNIxa1w.js";
|
|
6
6
|
import { useGrFormControl } from "../../../composables/useGrFormControl.js";
|
|
7
7
|
import { n as addLen, t as useControlAddons } from "../../../chunks/useControlAddons-asUr10Pf.js";
|