@feugene/granularity 0.34.2 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/chunks/{config-_QF0O1vq.js → config-CMNfZO7S.js} +2 -2
  2. package/dist/chunks/config-CMNfZO7S.js.map +1 -0
  3. package/dist/chunks/{config-mlheIMtV.js → config-DBi0GVce.js} +2 -2
  4. package/dist/chunks/config-DBi0GVce.js.map +1 -0
  5. package/dist/chunks/{grModalStyles-EZfuEiw_.js → grModalStyles-BEkiFal_.js} +10 -1
  6. package/dist/chunks/grModalStyles-BEkiFal_.js.map +1 -0
  7. package/dist/chunks/{granular-provider-BAy4APOX.js → granular-provider-Bp_BCBnr.js} +2 -2
  8. package/dist/chunks/{granular-provider-BAy4APOX.js.map → granular-provider-Bp_BCBnr.js.map} +1 -1
  9. package/dist/components/GrColorPicker/chunks/{GrColorPicker-CIgdPXHn.js → GrColorPicker-B-L4CWMt.js} +2 -2
  10. package/dist/components/GrColorPicker/chunks/{GrColorPicker-CIgdPXHn.js.map → GrColorPicker-B-L4CWMt.js.map} +1 -1
  11. package/dist/components/GrColorPicker/index.js +1 -1
  12. package/dist/components/GrCommandPalette/chunks/{GrCommandPalette-BZHUKlux.js → GrCommandPalette-8HkYmMVP.js} +1 -1
  13. package/dist/components/GrCommandPalette/chunks/{GrCommandPalette-BZHUKlux.js.map → GrCommandPalette-8HkYmMVP.js.map} +1 -1
  14. package/dist/components/GrCommandPalette/index.js +1 -1
  15. package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-jRDs_E6C.js → GrConfirmDialog-CZEO2JR4.js} +1 -1
  16. package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-jRDs_E6C.js.map → GrConfirmDialog-CZEO2JR4.js.map} +1 -1
  17. package/dist/components/GrConfirmDialog/index.js +1 -1
  18. package/dist/components/GrDialog/chunks/{GrDialog-C01Hhk6G.js → GrDialog-CKNbnxwO.js} +1 -1
  19. package/dist/components/GrDialog/chunks/{GrDialog-C01Hhk6G.js.map → GrDialog-CKNbnxwO.js.map} +1 -1
  20. package/dist/components/GrDialog/index.js +1 -1
  21. package/dist/components/GrDialogService/chunks/{GrDialogService-CmsJNUmM.js → GrDialogService-Ut8iiBQh.js} +2 -2
  22. package/dist/components/GrDialogService/chunks/{GrDialogService-CmsJNUmM.js.map → GrDialogService-Ut8iiBQh.js.map} +1 -1
  23. package/dist/components/GrDialogService/index.js +1 -1
  24. package/dist/components/GrFileUpload/chunks/{GrFileUpload-LL8RZvC0.js → GrFileUpload-C0ROpmOY.js} +1 -1
  25. package/dist/components/GrFileUpload/chunks/{GrFileUpload-LL8RZvC0.js.map → GrFileUpload-C0ROpmOY.js.map} +1 -1
  26. package/dist/components/GrFileUpload/index.js +1 -1
  27. package/dist/components/GrInput/chunks/{GrInput-BLywRZUN.js → GrInput-CJ7eu3fR.js} +1 -0
  28. package/dist/components/GrInput/chunks/{GrInput-BLywRZUN.js.map → GrInput-CJ7eu3fR.js.map} +1 -1
  29. package/dist/components/GrInput/index.js +1 -1
  30. package/dist/components/GrJsonViewer/chunks/{GrJsonViewer-BCZ7lE2k.js → GrJsonViewer-BXG7DdTe.js} +1 -1
  31. package/dist/components/GrJsonViewer/chunks/{GrJsonViewer-BCZ7lE2k.js.map → GrJsonViewer-BXG7DdTe.js.map} +1 -1
  32. package/dist/components/GrJsonViewer/index.js +1 -1
  33. package/dist/components/GrModal/chunks/{GrModal-_JrDhon-.js → GrModal-DixnhJcu.js} +3 -2
  34. package/dist/components/GrModal/chunks/GrModal-DixnhJcu.js.map +1 -0
  35. package/dist/components/GrModal/index.js +2 -2
  36. package/dist/components/GrNavbar/chunks/{GrNavbar-_rtHKwTf.js → GrNavbar-Dr8e5kW-.js} +14 -4
  37. package/dist/components/GrNavbar/chunks/GrNavbar-Dr8e5kW-.js.map +1 -0
  38. package/dist/components/GrNavbar/index.js +1 -1
  39. package/dist/components/GrPagination/chunks/{GrPagination-gBoWVHk6.js → GrPagination-68iyAwQz.js} +24 -9
  40. package/dist/components/GrPagination/chunks/GrPagination-68iyAwQz.js.map +1 -0
  41. package/dist/components/GrPagination/index.js +1 -1
  42. package/dist/components/GrProgressBar/chunks/{GrProgressBar-hszUOC8f.js → GrProgressBar-Cd7AaJtR.js} +5 -2
  43. package/dist/components/GrProgressBar/chunks/GrProgressBar-Cd7AaJtR.js.map +1 -0
  44. package/dist/components/GrProgressBar/index.js +1 -1
  45. package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-CailXSqW.js → GrPromptDialog-Ckeombia.js} +2 -2
  46. package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-CailXSqW.js.map → GrPromptDialog-Ckeombia.js.map} +1 -1
  47. package/dist/components/GrPromptDialog/index.js +1 -1
  48. package/dist/components/GrSelect/chunks/{GrSelect-CZqV1Eg4.js → GrSelect-Bm3Jugx8.js} +3 -7
  49. package/dist/components/GrSelect/chunks/GrSelect-Bm3Jugx8.js.map +1 -0
  50. package/dist/components/GrSelect/index.js +1 -1
  51. package/dist/components/GrSelect/styles.css +1 -1
  52. package/dist/components/GrSlider/chunks/{GrSlider-DTTrZTHK.js → GrSlider-DM6W6U9c.js} +9 -3
  53. package/dist/components/GrSlider/chunks/GrSlider-DM6W6U9c.js.map +1 -0
  54. package/dist/components/GrSlider/index.js +1 -1
  55. package/dist/components/GrStatistic/chunks/{GrStatistic-qHVasK37.js → GrStatistic-BoLkXw7F.js} +7 -3
  56. package/dist/components/GrStatistic/chunks/GrStatistic-BoLkXw7F.js.map +1 -0
  57. package/dist/components/GrStatistic/index.js +1 -1
  58. package/dist/components/GrSteps/chunks/{GrSteps-C2jxpZoN.js → GrSteps-CpGHXLOc.js} +1 -1
  59. package/dist/components/GrSteps/chunks/{GrSteps-C2jxpZoN.js.map → GrSteps-CpGHXLOc.js.map} +1 -1
  60. package/dist/components/GrSteps/index.js +1 -1
  61. package/dist/components/GrTabs/chunks/{GrTabs-CEKT5tXv.js → GrTabs-CT2nk_HV.js} +1 -1
  62. package/dist/components/GrTabs/chunks/{GrTabs-CEKT5tXv.js.map → GrTabs-CT2nk_HV.js.map} +1 -1
  63. package/dist/components/GrTabs/index.js +2 -2
  64. package/dist/components/GrTreeSelect/chunks/{GrTreeSelect-DOXcpWq2.js → GrTreeSelect-MGsyvkJV.js} +1 -1
  65. package/dist/components/GrTreeSelect/chunks/{GrTreeSelect-DOXcpWq2.js.map → GrTreeSelect-MGsyvkJV.js.map} +1 -1
  66. package/dist/components/GrTreeSelect/index.js +1 -1
  67. package/dist/granular-provider-node.js +1 -1
  68. package/dist/granular-provider.js +1 -1
  69. package/dist/index.js +21 -21
  70. package/dist/types/components/GrModal/grModalStyles.d.ts +9 -0
  71. package/dist/types/components/GrNavbar/GrNavbar.vue.d.ts +14 -0
  72. package/dist/types/components/GrNavbar/defaults.d.ts +10 -0
  73. package/dist/types/components/GrNavbar/index.d.ts +1 -0
  74. package/dist/types/components/GrProgressBar/GrProgressBar.vue.d.ts +1 -1
  75. package/dist/web-types.json +11 -2
  76. package/package.json +3 -3
  77. package/dist/chunks/config-_QF0O1vq.js.map +0 -1
  78. package/dist/chunks/config-mlheIMtV.js.map +0 -1
  79. package/dist/chunks/grModalStyles-EZfuEiw_.js.map +0 -1
  80. package/dist/components/GrModal/chunks/GrModal-_JrDhon-.js.map +0 -1
  81. package/dist/components/GrNavbar/chunks/GrNavbar-_rtHKwTf.js.map +0 -1
  82. package/dist/components/GrPagination/chunks/GrPagination-gBoWVHk6.js.map +0 -1
  83. package/dist/components/GrProgressBar/chunks/GrProgressBar-hszUOC8f.js.map +0 -1
  84. package/dist/components/GrSelect/chunks/GrSelect-CZqV1Eg4.js.map +0 -1
  85. package/dist/components/GrSlider/chunks/GrSlider-DTTrZTHK.js.map +0 -1
  86. package/dist/components/GrStatistic/chunks/GrStatistic-qHVasK37.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"GrTabs-CEKT5tXv.js","names":[],"sources":["../../../../src/components/shared/scrollOverflow.ts","../../../../src/components/GrTabs/GrTabs.vue","../../../../src/components/GrTabs/GrTabs.vue"],"sourcesContent":["/** С какой стороны у скроллера осталось невидимое содержимое. */\nexport type GrScrollOverflow = 'none' | 'start' | 'end' | 'both'\n\n/**\n * Состояние переполнения скроллера — по трём числам, без DOM.\n *\n * Отдельным модулем, потому что в jsdom раскладки нет: `scrollWidth` и\n * `clientWidth` там нули, и через смонтированный компонент ни одна ветка не\n * проверяется. Здесь она проверяется вся.\n *\n * `scrollStart` — расстояние от начала ряда, а не `scrollLeft`. Это не\n * педантизм: в RTL современные браузеры отсчитывают `scrollLeft` от правого\n * края и уводят его в минус, поэтому физическое «слева» и логическое «в начале»\n * там не совпадают. Модуль знает только про начало и конец, а какой это край\n * экрана — дело CSS.\n *\n * Запас в пиксель — как в {@link titleWhenTruncated}: браузеры округляют\n * размеры вверх, и у ряда, влезающего целиком, разница бывает единичной.\n * Без запаса затухание висело бы на ровном месте.\n */\nexport function resolveScrollOverflow(\n scrollStart: number,\n scrollSize: number,\n clientSize: number,\n): GrScrollOverflow {\n const hidden = scrollSize - clientSize\n\n if (hidden <= 1)\n return 'none'\n\n const start = Math.abs(scrollStart)\n const atStart = start <= 1\n const atEnd = hidden - start <= 1\n\n if (atStart && atEnd)\n return 'none'\n\n if (atStart)\n return 'end'\n\n if (atEnd)\n return 'start'\n\n return 'both'\n}\n","<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, watch, type Component } from 'vue'\n\nimport IconX from '~icons/lucide/x'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { useRovingFocus } from '../../composables/useRovingFocus'\nimport { iconClass, iconTag } from '../shared/icon'\nimport { resolveScrollOverflow, type GrScrollOverflow } from '../shared/scrollOverflow'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport {\n grTabsBadgeClass,\n grTabsCloseClass,\n grTabsEmptyClass,\n grTabsListClass,\n grTabsTabClass,\n tabCloseIconSizes,\n tabContentClass,\n tabIconClass,\n type GrTabsOrientation,\n type GrTabsSize,\n type GrTabsVariant,\n} from './grTabsStyles'\n\nexport interface GrTab {\n value: string\n label: string\n badge?: string\n /**\n * Иконка слева от подписи: Vue-компонент либо класс иконки вашей UnoCSS-сборки\n * (`'i-lucide-user'` — тогда нужен ваш `presetIcons`, см. `docs/installation.md`).\n */\n icon?: string | Component\n disabled?: boolean\n /** Перекрывает `closable` ряда в обе стороны: закрепить вкладку или закрыть одну её. */\n closable?: boolean\n}\n\nexport interface GrTabsProps {\n modelValue: string\n tabs: GrTab[]\n /**\n * База id для ARIA-связки с `GrTabPanels`. Если задана, каждая вкладка\n * получает `id=\"<idBase>-tab-<value>\"` и `aria-controls=\"<idBase>-panel-<value>\"`.\n * Передайте тот же `idBase` в `GrTabPanels`, чтобы связать `tab`↔`tabpanel`.\n */\n idBase?: string\n size?: GrTabsSize\n /** Вид ряда: обойма с таблетками или ряд с подчёркиванием. */\n variant?: GrTabsVariant\n /**\n * `automatic` (по умолчанию) — стрелка сразу переключает вкладку;\n * `manual` — стрелка двигает только фокус, выбор подтверждается\n * `Enter`/`Space`. Второй режим для вкладок с тяжёлой загрузкой: перебор\n * стрелками иначе тянет каждую панель.\n */\n activationMode?: GrTabsActivationMode\n /** Горизонтальный (по умолчанию) или вертикальный список вкладок. */\n orientation?: GrTabsOrientation\n /**\n * Крестик на вкладках и закрытие по `Delete`/`Backspace`. Точечно снимается\n * `closable: false` у самой вкладки.\n */\n closable?: boolean\n /** Текст, когда список вкладок пуст. Слот `#empty` сильнее. */\n emptyText?: string\n}\n\nexport interface GrTabsEmits {\n (e: 'update:modelValue', value: string): void\n /**\n * Просьба закрыть вкладку. Компонент список не трогает: `tabs` — проп, и\n * закрытие может не состояться («сохранить изменения?»).\n */\n (e: 'close', value: string): void\n}\n\nexport type GrTabsActivationMode = 'automatic' | 'manual'\n\nexport type { GrTabsOrientation, GrTabsVariant } from './grTabsStyles'\n\n/**\n * GrTabs — горизонтальная группа вкладок с паттерном WAI-ARIA `tablist`.\n *\n * A11y:\n * - `role=\"tablist\"` на корне, `role=\"tab\"` на каждой вкладке.\n * - Roving `tabindex`: активная вкладка `0`, остальные `-1`.\n * - Клавиатура: `ArrowLeft`/`ArrowRight` — цикличный переход, `Home`/`End` — к первой/последней.\n * - При переключении стрелками DOM-фокус программно переносится на новую вкладку.\n *\n * Сам компонент не рендерит `tabpanel` — это ответственность консьюмера (по `aria-controls`/внешней разметке).\n */\nconst props = withDefaults(defineProps<GrTabsProps>(), {\n idBase: undefined,\n size: undefined,\n // Дефолт живёт в резолвере: Vue подставил бы свой раньше, чем компонент\n // заглянет в `GrConfigProvider`.\n variant: undefined,\n activationMode: 'automatic',\n orientation: 'horizontal',\n closable: false,\n emptyText: undefined,\n})\n\ndefineSlots<{\n /** Содержимое вкладки целиком — вместо подписи, иконки и счётчика. Крестик остаётся. */\n tab?: (props: { tab: GrTab, active: boolean, disabled: boolean }) => unknown\n /** Пустой ряд — вместо текста из локали. */\n empty?: () => unknown\n}>()\n\nconst emit = defineEmits<GrTabsEmits>()\n\nconst { t } = useGranularityTranslations()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrTabs' })\nconst resolvedVariant = useGrComponentProp('GrTabs', 'variant', () => props.variant, 'pills')\n\nconst tablistClass = computed(() => grTabsListClass({\n variant: resolvedVariant.value,\n size: resolvedSize.value,\n orientation: props.orientation,\n}))\n\nfunction tabClass(tab: GrTab): string {\n return grTabsTabClass({\n variant: resolvedVariant.value,\n size: resolvedSize.value,\n active: tab.value === props.modelValue,\n disabled: Boolean(tab.disabled),\n })\n}\n\nconst badgeClass = computed(() => grTabsBadgeClass(resolvedSize.value))\nconst closeClass = computed(() => grTabsCloseClass(resolvedSize.value))\nconst closeIconClass = computed(() => tabCloseIconSizes[resolvedSize.value])\nconst emptyClass = computed(() => grTabsEmptyClass(resolvedSize.value))\n\n/**\n * Пустой ряд — не пустой `tablist`: роль обязана владеть потомками `tab`,\n * и текст внутри неё был бы нарушением, а не пустым состоянием. Поэтому\n * пустое состояние рисуется отдельной веткой, а не содержимым `tablist`.\n */\nconst isEmpty = computed(() => props.tabs.length === 0)\nconst resolvedEmptyText = computed(() => props.emptyText ?? t('gr.tabs.empty', 'Nothing here yet'))\nconst closeTitle = computed(() => t('gr.tabs.close', 'Close tab'))\n\n/** Отключённая вкладка не принимает взаимодействия, а закрытие — тоже взаимодействие. */\nfunction isClosable(tab: GrTab): boolean {\n return !tab.disabled && (tab.closable ?? props.closable)\n}\n\nconst buttonRefs = ref<HTMLButtonElement[]>([])\n\n/**\n * Vue зовёт ref-функцию с `null`, когда узел исчез. Игнорировать такой вызов\n * нельзя: в массиве оставались отсоединённые от DOM кнопки, и `focus()` по\n * сократившемуся списку молча проваливался в `<body>`.\n */\nfunction setButtonRef(el: unknown, index: number): void {\n if (el instanceof HTMLButtonElement)\n buttonRefs.value[index] = el\n else\n delete buttonRefs.value[index]\n\n if (buttonRefs.value.length > props.tabs.length)\n buttonRefs.value.length = props.tabs.length\n}\n\nconst activeIndex = computed(() => props.tabs.findIndex(t => t.value === props.modelValue))\n\nfunction isEnabled(tab: GrTab): boolean {\n return !tab.disabled\n}\n\nconst tabIndexes = computed(() => props.tabs.map((_, index) => index))\n\n/**\n * Кольцо roving-фокуса. При `activationMode: 'manual'` фокус уезжает вперёд\n * выбора — это и есть разница между `activeKey` примитива и `initialKey`:\n * первый ведёт фокус, второй подставляет выбранную вкладку, пока фокус не\n * трогали.\n *\n * Выбор стрелка **не** переносит: этим заведует `onKeydown` по\n * `activationMode`. Через `onMove` это выразить нельзя — клик тоже зовёт\n * `focusKey`, и выбор эмитился бы дважды.\n */\nconst roving = useRovingFocus<number>({\n items: () => tabIndexes.value,\n elementFor: index => buttonRefs.value[index],\n isDisabled: index => Boolean(props.tabs[index]?.disabled),\n orientation: () => (props.orientation === 'vertical' ? 'vertical' : 'horizontal'),\n skipDisabled: () => true,\n /**\n * Пока фокус не трогали, остановку `Tab` держит выбранная вкладка. Если\n * `modelValue` не совпал ни с одной (пустое начальное значение, асинхронный\n * список, удалённая активная вкладка), примитив ставит её на первую\n * доступную — иначе tablist выпал бы из обхода целиком и молча.\n *\n * Выключенность здесь не проверяется намеренно: на кнопке `aria-disabled`, а\n * не нативный `disabled`, поэтому выключенная вкладка фокусируется и держать\n * остановку вправе.\n */\n initialKey: () => (activeIndex.value >= 0 ? activeIndex.value : undefined),\n /**\n * Прокрутка до фокуса, а не после: иначе браузер доскроллит сам по-своему, и\n * ряд дёрнется дважды. Синхронно — фокус обязан остаться в том же такте, что\n * и раньше: тик, где он нужен, добавляет `focusIndex`.\n */\n beforeFocus: index => scrollIntoView(index),\n})\n\n/**\n * Фокус после перерисовки — для путей, где список только что изменился\n * (закрытие вкладки). Клавиатура идёт через примитив напрямую: там ряд уже\n * отрисован, и лишний тик только отложил бы фокус.\n */\nasync function focusIndex(index: number): Promise<void> {\n await nextTick()\n await roving.focusKey(index)\n}\n\n/**\n * Ряд прокручивается, а не переносится, поэтому активная вкладка может уехать\n * за край — например, когда её выбрали снаружи. `scrollIntoView` нет в jsdom и\n * в старых движках, поэтому вызов необязательный.\n */\nfunction scrollIntoView(index: number): void {\n buttonRefs.value[index]?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' })\n}\n\nwatch(() => props.modelValue, async () => {\n await nextTick()\n if (activeIndex.value >= 0)\n scrollIntoView(activeIndex.value)\n})\n\n/**\n * Признак того, что ряд продолжается за краем.\n *\n * Полоса прокрутки у ряда скрыта намеренно, и без этого признака продолжение\n * не выдаёт себя ничем: вкладки за краем достижимы стрелками, но о том, что они\n * там есть, догадаться нечем. Затухание рисует CSS по `data-overflow`.\n *\n * Вертикальный ряд не скроллится вовсе — он растёт вниз, — поэтому состояние\n * считается только для горизонтального.\n */\nconst tablistEl = ref<HTMLElement | null>(null)\nconst scrollOverflow = ref<GrScrollOverflow>('none')\n\nlet resizeObserver: ResizeObserver | null = null\nlet scheduled = false\n\nfunction measureOverflow(): void {\n const el = tablistEl.value\n\n if (!el || props.orientation === 'vertical') {\n scrollOverflow.value = 'none'\n return\n }\n\n scrollOverflow.value = resolveScrollOverflow(el.scrollLeft, el.scrollWidth, el.clientWidth)\n}\n\nfunction scheduleMeasure(): void {\n if (scheduled)\n return\n\n scheduled = true\n void nextTick(() => {\n scheduled = false\n measureOverflow()\n })\n}\n\nonMounted(() => {\n scheduleMeasure()\n\n // На сервере и в jsdom `ResizeObserver` отсутствует — измерять там нечего.\n if (typeof ResizeObserver === 'undefined')\n return\n\n resizeObserver = new ResizeObserver(scheduleMeasure)\n if (tablistEl.value)\n resizeObserver.observe(tablistEl.value)\n})\n\nonBeforeUnmount(() => {\n resizeObserver?.disconnect()\n resizeObserver = null\n})\n\n// Состав ряда меняет его ширину, ориентация — само наличие прокрутки.\nwatch(() => [props.tabs.length, props.orientation], scheduleMeasure)\n\nasync function selectByIndex(index: number, focus = false): Promise<void> {\n const tab = props.tabs[index]\n if (!tab || !isEnabled(tab))\n return\n if (tab.value !== props.modelValue)\n emit('update:modelValue', tab.value)\n if (focus)\n await focusIndex(index)\n}\n\nfunction onKeydown(event: KeyboardEvent): void {\n if (props.tabs.length === 0)\n return\n\n const currentIndex = roving.rovingKey.value ?? 0\n\n switch (event.key) {\n case 'Enter':\n case ' ':\n // В ручном режиме выбор подтверждается явно — на той вкладке, где фокус.\n if (props.activationMode === 'manual') {\n event.preventDefault()\n void selectByIndex(currentIndex, false)\n }\n return\n case 'Delete':\n case 'Backspace': {\n // Единственный клавиатурный путь к закрытию: крестик в таб-порядке не стоит.\n const tab = props.tabs[currentIndex]\n if (tab && isClosable(tab)) {\n event.preventDefault()\n requestClose(currentIndex)\n }\n return\n }\n }\n\n // Стрелки, `Home`, `End` — фокус ведёт примитив по объявленной ориентации.\n if (!roving.handleNavigationKeys(event))\n return\n\n // `automatic`: выбор едет за фокусом. `manual`: ждёт `Enter`/`Space`.\n if (props.activationMode !== 'manual')\n void selectByIndex(roving.rovingKey.value ?? currentIndex, false)\n}\n\n/**\n * Закрытая вкладка, которая держала фокус. Компонент список не укорачивает —\n * это делает потребитель, — поэтому фокус возвращается только после того, как\n * `tabs` действительно стал короче.\n */\nlet pendingClose: { index: number, value: string } | null = null\n\nfunction requestClose(index: number): void {\n const tab = props.tabs[index]\n if (!tab || !isClosable(tab))\n return\n\n const held = buttonRefs.value[index] === document.activeElement\n pendingClose = held ? { index, value: tab.value } : null\n\n emit('close', tab.value)\n}\n\n// Без этого клавиатурное закрытие роняет фокус в `<body>`: кнопка, на которой\n// он стоял, исчезает из DOM вместе со вкладкой.\nwatch(() => props.tabs.length, async (next, prev) => {\n const pending = pendingClose\n pendingClose = null\n\n if (!pending || next >= prev)\n return\n // Ушла не та вкладка — потребитель поменял список по своей причине.\n if (props.tabs.some(tab => tab.value === pending.value))\n return\n\n await nextTick()\n\n const target = Math.min(pending.index, next - 1)\n if (target >= 0)\n void focusIndex(target)\n else\n roving.reset()\n})\n\nfunction onClick(event: MouseEvent, tab: GrTab, index: number): void {\n if (!isEnabled(tab))\n return\n\n // Крестик — не кнопка, поэтому его клик приходит сюда же и разбирается по цели.\n if (event.target instanceof Element && event.target.closest('[data-gr-tab-close]')) {\n requestClose(index)\n return\n }\n\n roving.setActive(index)\n\n if (tab.value !== props.modelValue)\n emit('update:modelValue', tab.value)\n}\n</script>\n\n<template>\n <div v-if=\"isEmpty\" data-gr-tabs-empty :class=\"emptyClass\">\n <slot name=\"empty\">\n {{ resolvedEmptyText }}\n </slot>\n </div>\n\n <div\n v-else\n ref=\"tablistEl\"\n role=\"tablist\"\n data-gr-tabs\n :aria-orientation=\"orientation\"\n :data-overflow=\"orientation === 'vertical' ? undefined : scrollOverflow\"\n :class=\"tablistClass\"\n @keydown=\"onKeydown\"\n @scroll.passive=\"scheduleMeasure\"\n >\n <button\n v-for=\"(tab, index) in tabs\"\n :id=\"idBase ? `${idBase}-tab-${tab.value}` : undefined\"\n :key=\"tab.value\"\n :ref=\"el => setButtonRef(el, index)\"\n type=\"button\"\n role=\"tab\"\n data-gr-tab\n :aria-controls=\"idBase ? `${idBase}-panel-${tab.value}` : undefined\"\n :aria-selected=\"tab.value === modelValue ? 'true' : 'false'\"\n :aria-disabled=\"tab.disabled ? 'true' : undefined\"\n :aria-keyshortcuts=\"isClosable(tab) ? 'Delete' : undefined\"\n :tabindex=\"roving.tabindexFor(index)\"\n :class=\"tabClass(tab)\"\n @click=\"onClick($event, tab, index)\"\n >\n <span :class=\"tabContentClass\">\n <slot\n name=\"tab\"\n :tab=\"tab\"\n :active=\"tab.value === modelValue\"\n :disabled=\"Boolean(tab.disabled)\"\n >\n <component\n :is=\"iconTag(tab.icon)\"\n v-if=\"tab.icon\"\n :class=\"[tabIconClass, iconClass(tab.icon)]\"\n aria-hidden=\"true\"\n />\n <span>{{ tab.label }}</span>\n <span v-if=\"tab.badge\" :class=\"badgeClass\">\n {{ tab.badge }}\n </span>\n </slot>\n\n <!-- Снаружи слота: своя разметка вкладки не должна отнимать закрытие. -->\n <span\n v-if=\"isClosable(tab)\"\n data-gr-tab-close\n :class=\"closeClass\"\n :title=\"closeTitle\"\n aria-hidden=\"true\"\n >\n <IconX :class=\"closeIconClass\" />\n </span>\n </span>\n </button>\n </div>\n</template>\n\n<style>\n/*\n * Затухание у края, за которым ряд продолжается.\n *\n * Здесь, а не утилитами, по двум причинам сразу. `mask-image` в `presetMini` не\n * выражается — это уже зафиксировано отказом в `GrImageCrop`. И маской, а не\n * градиентом-подложкой, потому что фон полосы у вариантов разный: `pills` несёт\n * свой непрозрачный `--gr-muted`, а `line` прозрачен и лежит на фоне родителя,\n * которого компонент не знает. Градиенту пришлось бы красить в цвет подложки —\n * для `line` его неоткуда взять. Маска гасит содержимое независимо от того, что\n * под ним.\n *\n * Состояние считается логически (начало/конец ряда), а направление градиента в\n * CSS выражается только физически — отсюда зеркальные правила для RTL.\n */\n\n[data-gr-tabs] {\n /*\n * `scrollIntoView({ inline: 'nearest' })` из кольца фокуса прижимает вкладку\n * вплотную к краю. Без отступа прокрутки кольцо фокуса у клавиатурного\n * пользователя оказалось бы ровно под затуханием.\n */\n scroll-padding-inline: var(--gr-tabs-scroll-fade, 1.5rem);\n}\n\n[data-gr-tabs][data-overflow='start'] {\n --gr-tabs-mask: linear-gradient(to right, transparent 0, #000 var(--gr-tabs-scroll-fade, 1.5rem));\n}\n\n[data-gr-tabs][data-overflow='end'] {\n --gr-tabs-mask: linear-gradient(to right, #000 calc(100% - var(--gr-tabs-scroll-fade, 1.5rem)), transparent 100%);\n}\n\n[data-gr-tabs][data-overflow='both'] {\n --gr-tabs-mask: linear-gradient(\n to right,\n transparent 0,\n #000 var(--gr-tabs-scroll-fade, 1.5rem),\n #000 calc(100% - var(--gr-tabs-scroll-fade, 1.5rem)),\n transparent 100%\n );\n}\n\n[dir='rtl'] [data-gr-tabs][data-overflow='start'] {\n --gr-tabs-mask: linear-gradient(to left, transparent 0, #000 var(--gr-tabs-scroll-fade, 1.5rem));\n}\n\n[dir='rtl'] [data-gr-tabs][data-overflow='end'] {\n --gr-tabs-mask: linear-gradient(to left, #000 calc(100% - var(--gr-tabs-scroll-fade, 1.5rem)), transparent 100%);\n}\n\n[data-gr-tabs]:is([data-overflow='start'], [data-overflow='end'], [data-overflow='both']) {\n -webkit-mask-image: var(--gr-tabs-mask);\n mask-image: var(--gr-tabs-mask);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, watch, type Component } from 'vue'\n\nimport IconX from '~icons/lucide/x'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { useRovingFocus } from '../../composables/useRovingFocus'\nimport { iconClass, iconTag } from '../shared/icon'\nimport { resolveScrollOverflow, type GrScrollOverflow } from '../shared/scrollOverflow'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport {\n grTabsBadgeClass,\n grTabsCloseClass,\n grTabsEmptyClass,\n grTabsListClass,\n grTabsTabClass,\n tabCloseIconSizes,\n tabContentClass,\n tabIconClass,\n type GrTabsOrientation,\n type GrTabsSize,\n type GrTabsVariant,\n} from './grTabsStyles'\n\nexport interface GrTab {\n value: string\n label: string\n badge?: string\n /**\n * Иконка слева от подписи: Vue-компонент либо класс иконки вашей UnoCSS-сборки\n * (`'i-lucide-user'` — тогда нужен ваш `presetIcons`, см. `docs/installation.md`).\n */\n icon?: string | Component\n disabled?: boolean\n /** Перекрывает `closable` ряда в обе стороны: закрепить вкладку или закрыть одну её. */\n closable?: boolean\n}\n\nexport interface GrTabsProps {\n modelValue: string\n tabs: GrTab[]\n /**\n * База id для ARIA-связки с `GrTabPanels`. Если задана, каждая вкладка\n * получает `id=\"<idBase>-tab-<value>\"` и `aria-controls=\"<idBase>-panel-<value>\"`.\n * Передайте тот же `idBase` в `GrTabPanels`, чтобы связать `tab`↔`tabpanel`.\n */\n idBase?: string\n size?: GrTabsSize\n /** Вид ряда: обойма с таблетками или ряд с подчёркиванием. */\n variant?: GrTabsVariant\n /**\n * `automatic` (по умолчанию) — стрелка сразу переключает вкладку;\n * `manual` — стрелка двигает только фокус, выбор подтверждается\n * `Enter`/`Space`. Второй режим для вкладок с тяжёлой загрузкой: перебор\n * стрелками иначе тянет каждую панель.\n */\n activationMode?: GrTabsActivationMode\n /** Горизонтальный (по умолчанию) или вертикальный список вкладок. */\n orientation?: GrTabsOrientation\n /**\n * Крестик на вкладках и закрытие по `Delete`/`Backspace`. Точечно снимается\n * `closable: false` у самой вкладки.\n */\n closable?: boolean\n /** Текст, когда список вкладок пуст. Слот `#empty` сильнее. */\n emptyText?: string\n}\n\nexport interface GrTabsEmits {\n (e: 'update:modelValue', value: string): void\n /**\n * Просьба закрыть вкладку. Компонент список не трогает: `tabs` — проп, и\n * закрытие может не состояться («сохранить изменения?»).\n */\n (e: 'close', value: string): void\n}\n\nexport type GrTabsActivationMode = 'automatic' | 'manual'\n\nexport type { GrTabsOrientation, GrTabsVariant } from './grTabsStyles'\n\n/**\n * GrTabs — горизонтальная группа вкладок с паттерном WAI-ARIA `tablist`.\n *\n * A11y:\n * - `role=\"tablist\"` на корне, `role=\"tab\"` на каждой вкладке.\n * - Roving `tabindex`: активная вкладка `0`, остальные `-1`.\n * - Клавиатура: `ArrowLeft`/`ArrowRight` — цикличный переход, `Home`/`End` — к первой/последней.\n * - При переключении стрелками DOM-фокус программно переносится на новую вкладку.\n *\n * Сам компонент не рендерит `tabpanel` — это ответственность консьюмера (по `aria-controls`/внешней разметке).\n */\nconst props = withDefaults(defineProps<GrTabsProps>(), {\n idBase: undefined,\n size: undefined,\n // Дефолт живёт в резолвере: Vue подставил бы свой раньше, чем компонент\n // заглянет в `GrConfigProvider`.\n variant: undefined,\n activationMode: 'automatic',\n orientation: 'horizontal',\n closable: false,\n emptyText: undefined,\n})\n\ndefineSlots<{\n /** Содержимое вкладки целиком — вместо подписи, иконки и счётчика. Крестик остаётся. */\n tab?: (props: { tab: GrTab, active: boolean, disabled: boolean }) => unknown\n /** Пустой ряд — вместо текста из локали. */\n empty?: () => unknown\n}>()\n\nconst emit = defineEmits<GrTabsEmits>()\n\nconst { t } = useGranularityTranslations()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrTabs' })\nconst resolvedVariant = useGrComponentProp('GrTabs', 'variant', () => props.variant, 'pills')\n\nconst tablistClass = computed(() => grTabsListClass({\n variant: resolvedVariant.value,\n size: resolvedSize.value,\n orientation: props.orientation,\n}))\n\nfunction tabClass(tab: GrTab): string {\n return grTabsTabClass({\n variant: resolvedVariant.value,\n size: resolvedSize.value,\n active: tab.value === props.modelValue,\n disabled: Boolean(tab.disabled),\n })\n}\n\nconst badgeClass = computed(() => grTabsBadgeClass(resolvedSize.value))\nconst closeClass = computed(() => grTabsCloseClass(resolvedSize.value))\nconst closeIconClass = computed(() => tabCloseIconSizes[resolvedSize.value])\nconst emptyClass = computed(() => grTabsEmptyClass(resolvedSize.value))\n\n/**\n * Пустой ряд — не пустой `tablist`: роль обязана владеть потомками `tab`,\n * и текст внутри неё был бы нарушением, а не пустым состоянием. Поэтому\n * пустое состояние рисуется отдельной веткой, а не содержимым `tablist`.\n */\nconst isEmpty = computed(() => props.tabs.length === 0)\nconst resolvedEmptyText = computed(() => props.emptyText ?? t('gr.tabs.empty', 'Nothing here yet'))\nconst closeTitle = computed(() => t('gr.tabs.close', 'Close tab'))\n\n/** Отключённая вкладка не принимает взаимодействия, а закрытие — тоже взаимодействие. */\nfunction isClosable(tab: GrTab): boolean {\n return !tab.disabled && (tab.closable ?? props.closable)\n}\n\nconst buttonRefs = ref<HTMLButtonElement[]>([])\n\n/**\n * Vue зовёт ref-функцию с `null`, когда узел исчез. Игнорировать такой вызов\n * нельзя: в массиве оставались отсоединённые от DOM кнопки, и `focus()` по\n * сократившемуся списку молча проваливался в `<body>`.\n */\nfunction setButtonRef(el: unknown, index: number): void {\n if (el instanceof HTMLButtonElement)\n buttonRefs.value[index] = el\n else\n delete buttonRefs.value[index]\n\n if (buttonRefs.value.length > props.tabs.length)\n buttonRefs.value.length = props.tabs.length\n}\n\nconst activeIndex = computed(() => props.tabs.findIndex(t => t.value === props.modelValue))\n\nfunction isEnabled(tab: GrTab): boolean {\n return !tab.disabled\n}\n\nconst tabIndexes = computed(() => props.tabs.map((_, index) => index))\n\n/**\n * Кольцо roving-фокуса. При `activationMode: 'manual'` фокус уезжает вперёд\n * выбора — это и есть разница между `activeKey` примитива и `initialKey`:\n * первый ведёт фокус, второй подставляет выбранную вкладку, пока фокус не\n * трогали.\n *\n * Выбор стрелка **не** переносит: этим заведует `onKeydown` по\n * `activationMode`. Через `onMove` это выразить нельзя — клик тоже зовёт\n * `focusKey`, и выбор эмитился бы дважды.\n */\nconst roving = useRovingFocus<number>({\n items: () => tabIndexes.value,\n elementFor: index => buttonRefs.value[index],\n isDisabled: index => Boolean(props.tabs[index]?.disabled),\n orientation: () => (props.orientation === 'vertical' ? 'vertical' : 'horizontal'),\n skipDisabled: () => true,\n /**\n * Пока фокус не трогали, остановку `Tab` держит выбранная вкладка. Если\n * `modelValue` не совпал ни с одной (пустое начальное значение, асинхронный\n * список, удалённая активная вкладка), примитив ставит её на первую\n * доступную — иначе tablist выпал бы из обхода целиком и молча.\n *\n * Выключенность здесь не проверяется намеренно: на кнопке `aria-disabled`, а\n * не нативный `disabled`, поэтому выключенная вкладка фокусируется и держать\n * остановку вправе.\n */\n initialKey: () => (activeIndex.value >= 0 ? activeIndex.value : undefined),\n /**\n * Прокрутка до фокуса, а не после: иначе браузер доскроллит сам по-своему, и\n * ряд дёрнется дважды. Синхронно — фокус обязан остаться в том же такте, что\n * и раньше: тик, где он нужен, добавляет `focusIndex`.\n */\n beforeFocus: index => scrollIntoView(index),\n})\n\n/**\n * Фокус после перерисовки — для путей, где список только что изменился\n * (закрытие вкладки). Клавиатура идёт через примитив напрямую: там ряд уже\n * отрисован, и лишний тик только отложил бы фокус.\n */\nasync function focusIndex(index: number): Promise<void> {\n await nextTick()\n await roving.focusKey(index)\n}\n\n/**\n * Ряд прокручивается, а не переносится, поэтому активная вкладка может уехать\n * за край — например, когда её выбрали снаружи. `scrollIntoView` нет в jsdom и\n * в старых движках, поэтому вызов необязательный.\n */\nfunction scrollIntoView(index: number): void {\n buttonRefs.value[index]?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' })\n}\n\nwatch(() => props.modelValue, async () => {\n await nextTick()\n if (activeIndex.value >= 0)\n scrollIntoView(activeIndex.value)\n})\n\n/**\n * Признак того, что ряд продолжается за краем.\n *\n * Полоса прокрутки у ряда скрыта намеренно, и без этого признака продолжение\n * не выдаёт себя ничем: вкладки за краем достижимы стрелками, но о том, что они\n * там есть, догадаться нечем. Затухание рисует CSS по `data-overflow`.\n *\n * Вертикальный ряд не скроллится вовсе — он растёт вниз, — поэтому состояние\n * считается только для горизонтального.\n */\nconst tablistEl = ref<HTMLElement | null>(null)\nconst scrollOverflow = ref<GrScrollOverflow>('none')\n\nlet resizeObserver: ResizeObserver | null = null\nlet scheduled = false\n\nfunction measureOverflow(): void {\n const el = tablistEl.value\n\n if (!el || props.orientation === 'vertical') {\n scrollOverflow.value = 'none'\n return\n }\n\n scrollOverflow.value = resolveScrollOverflow(el.scrollLeft, el.scrollWidth, el.clientWidth)\n}\n\nfunction scheduleMeasure(): void {\n if (scheduled)\n return\n\n scheduled = true\n void nextTick(() => {\n scheduled = false\n measureOverflow()\n })\n}\n\nonMounted(() => {\n scheduleMeasure()\n\n // На сервере и в jsdom `ResizeObserver` отсутствует — измерять там нечего.\n if (typeof ResizeObserver === 'undefined')\n return\n\n resizeObserver = new ResizeObserver(scheduleMeasure)\n if (tablistEl.value)\n resizeObserver.observe(tablistEl.value)\n})\n\nonBeforeUnmount(() => {\n resizeObserver?.disconnect()\n resizeObserver = null\n})\n\n// Состав ряда меняет его ширину, ориентация — само наличие прокрутки.\nwatch(() => [props.tabs.length, props.orientation], scheduleMeasure)\n\nasync function selectByIndex(index: number, focus = false): Promise<void> {\n const tab = props.tabs[index]\n if (!tab || !isEnabled(tab))\n return\n if (tab.value !== props.modelValue)\n emit('update:modelValue', tab.value)\n if (focus)\n await focusIndex(index)\n}\n\nfunction onKeydown(event: KeyboardEvent): void {\n if (props.tabs.length === 0)\n return\n\n const currentIndex = roving.rovingKey.value ?? 0\n\n switch (event.key) {\n case 'Enter':\n case ' ':\n // В ручном режиме выбор подтверждается явно — на той вкладке, где фокус.\n if (props.activationMode === 'manual') {\n event.preventDefault()\n void selectByIndex(currentIndex, false)\n }\n return\n case 'Delete':\n case 'Backspace': {\n // Единственный клавиатурный путь к закрытию: крестик в таб-порядке не стоит.\n const tab = props.tabs[currentIndex]\n if (tab && isClosable(tab)) {\n event.preventDefault()\n requestClose(currentIndex)\n }\n return\n }\n }\n\n // Стрелки, `Home`, `End` — фокус ведёт примитив по объявленной ориентации.\n if (!roving.handleNavigationKeys(event))\n return\n\n // `automatic`: выбор едет за фокусом. `manual`: ждёт `Enter`/`Space`.\n if (props.activationMode !== 'manual')\n void selectByIndex(roving.rovingKey.value ?? currentIndex, false)\n}\n\n/**\n * Закрытая вкладка, которая держала фокус. Компонент список не укорачивает —\n * это делает потребитель, — поэтому фокус возвращается только после того, как\n * `tabs` действительно стал короче.\n */\nlet pendingClose: { index: number, value: string } | null = null\n\nfunction requestClose(index: number): void {\n const tab = props.tabs[index]\n if (!tab || !isClosable(tab))\n return\n\n const held = buttonRefs.value[index] === document.activeElement\n pendingClose = held ? { index, value: tab.value } : null\n\n emit('close', tab.value)\n}\n\n// Без этого клавиатурное закрытие роняет фокус в `<body>`: кнопка, на которой\n// он стоял, исчезает из DOM вместе со вкладкой.\nwatch(() => props.tabs.length, async (next, prev) => {\n const pending = pendingClose\n pendingClose = null\n\n if (!pending || next >= prev)\n return\n // Ушла не та вкладка — потребитель поменял список по своей причине.\n if (props.tabs.some(tab => tab.value === pending.value))\n return\n\n await nextTick()\n\n const target = Math.min(pending.index, next - 1)\n if (target >= 0)\n void focusIndex(target)\n else\n roving.reset()\n})\n\nfunction onClick(event: MouseEvent, tab: GrTab, index: number): void {\n if (!isEnabled(tab))\n return\n\n // Крестик — не кнопка, поэтому его клик приходит сюда же и разбирается по цели.\n if (event.target instanceof Element && event.target.closest('[data-gr-tab-close]')) {\n requestClose(index)\n return\n }\n\n roving.setActive(index)\n\n if (tab.value !== props.modelValue)\n emit('update:modelValue', tab.value)\n}\n</script>\n\n<template>\n <div v-if=\"isEmpty\" data-gr-tabs-empty :class=\"emptyClass\">\n <slot name=\"empty\">\n {{ resolvedEmptyText }}\n </slot>\n </div>\n\n <div\n v-else\n ref=\"tablistEl\"\n role=\"tablist\"\n data-gr-tabs\n :aria-orientation=\"orientation\"\n :data-overflow=\"orientation === 'vertical' ? undefined : scrollOverflow\"\n :class=\"tablistClass\"\n @keydown=\"onKeydown\"\n @scroll.passive=\"scheduleMeasure\"\n >\n <button\n v-for=\"(tab, index) in tabs\"\n :id=\"idBase ? `${idBase}-tab-${tab.value}` : undefined\"\n :key=\"tab.value\"\n :ref=\"el => setButtonRef(el, index)\"\n type=\"button\"\n role=\"tab\"\n data-gr-tab\n :aria-controls=\"idBase ? `${idBase}-panel-${tab.value}` : undefined\"\n :aria-selected=\"tab.value === modelValue ? 'true' : 'false'\"\n :aria-disabled=\"tab.disabled ? 'true' : undefined\"\n :aria-keyshortcuts=\"isClosable(tab) ? 'Delete' : undefined\"\n :tabindex=\"roving.tabindexFor(index)\"\n :class=\"tabClass(tab)\"\n @click=\"onClick($event, tab, index)\"\n >\n <span :class=\"tabContentClass\">\n <slot\n name=\"tab\"\n :tab=\"tab\"\n :active=\"tab.value === modelValue\"\n :disabled=\"Boolean(tab.disabled)\"\n >\n <component\n :is=\"iconTag(tab.icon)\"\n v-if=\"tab.icon\"\n :class=\"[tabIconClass, iconClass(tab.icon)]\"\n aria-hidden=\"true\"\n />\n <span>{{ tab.label }}</span>\n <span v-if=\"tab.badge\" :class=\"badgeClass\">\n {{ tab.badge }}\n </span>\n </slot>\n\n <!-- Снаружи слота: своя разметка вкладки не должна отнимать закрытие. -->\n <span\n v-if=\"isClosable(tab)\"\n data-gr-tab-close\n :class=\"closeClass\"\n :title=\"closeTitle\"\n aria-hidden=\"true\"\n >\n <IconX :class=\"closeIconClass\" />\n </span>\n </span>\n </button>\n </div>\n</template>\n\n<style>\n/*\n * Затухание у края, за которым ряд продолжается.\n *\n * Здесь, а не утилитами, по двум причинам сразу. `mask-image` в `presetMini` не\n * выражается — это уже зафиксировано отказом в `GrImageCrop`. И маской, а не\n * градиентом-подложкой, потому что фон полосы у вариантов разный: `pills` несёт\n * свой непрозрачный `--gr-muted`, а `line` прозрачен и лежит на фоне родителя,\n * которого компонент не знает. Градиенту пришлось бы красить в цвет подложки —\n * для `line` его неоткуда взять. Маска гасит содержимое независимо от того, что\n * под ним.\n *\n * Состояние считается логически (начало/конец ряда), а направление градиента в\n * CSS выражается только физически — отсюда зеркальные правила для RTL.\n */\n\n[data-gr-tabs] {\n /*\n * `scrollIntoView({ inline: 'nearest' })` из кольца фокуса прижимает вкладку\n * вплотную к краю. Без отступа прокрутки кольцо фокуса у клавиатурного\n * пользователя оказалось бы ровно под затуханием.\n */\n scroll-padding-inline: var(--gr-tabs-scroll-fade, 1.5rem);\n}\n\n[data-gr-tabs][data-overflow='start'] {\n --gr-tabs-mask: linear-gradient(to right, transparent 0, #000 var(--gr-tabs-scroll-fade, 1.5rem));\n}\n\n[data-gr-tabs][data-overflow='end'] {\n --gr-tabs-mask: linear-gradient(to right, #000 calc(100% - var(--gr-tabs-scroll-fade, 1.5rem)), transparent 100%);\n}\n\n[data-gr-tabs][data-overflow='both'] {\n --gr-tabs-mask: linear-gradient(\n to right,\n transparent 0,\n #000 var(--gr-tabs-scroll-fade, 1.5rem),\n #000 calc(100% - var(--gr-tabs-scroll-fade, 1.5rem)),\n transparent 100%\n );\n}\n\n[dir='rtl'] [data-gr-tabs][data-overflow='start'] {\n --gr-tabs-mask: linear-gradient(to left, transparent 0, #000 var(--gr-tabs-scroll-fade, 1.5rem));\n}\n\n[dir='rtl'] [data-gr-tabs][data-overflow='end'] {\n --gr-tabs-mask: linear-gradient(to left, #000 calc(100% - var(--gr-tabs-scroll-fade, 1.5rem)), transparent 100%);\n}\n\n[data-gr-tabs]:is([data-overflow='start'], [data-overflow='end'], [data-overflow='both']) {\n -webkit-mask-image: var(--gr-tabs-mask);\n mask-image: var(--gr-tabs-mask);\n}\n</style>\n"],"mappings":"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"}
1
+ {"version":3,"file":"GrTabs-CT2nk_HV.js","names":[],"sources":["../../../../src/components/shared/scrollOverflow.ts","../../../../src/components/GrTabs/GrTabs.vue","../../../../src/components/GrTabs/GrTabs.vue"],"sourcesContent":["/** С какой стороны у скроллера осталось невидимое содержимое. */\nexport type GrScrollOverflow = 'none' | 'start' | 'end' | 'both'\n\n/**\n * Состояние переполнения скроллера — по трём числам, без DOM.\n *\n * Отдельным модулем, потому что в jsdom раскладки нет: `scrollWidth` и\n * `clientWidth` там нули, и через смонтированный компонент ни одна ветка не\n * проверяется. Здесь она проверяется вся.\n *\n * `scrollStart` — расстояние от начала ряда, а не `scrollLeft`. Это не\n * педантизм: в RTL современные браузеры отсчитывают `scrollLeft` от правого\n * края и уводят его в минус, поэтому физическое «слева» и логическое «в начале»\n * там не совпадают. Модуль знает только про начало и конец, а какой это край\n * экрана — дело CSS.\n *\n * Запас в пиксель — как в {@link titleWhenTruncated}: браузеры округляют\n * размеры вверх, и у ряда, влезающего целиком, разница бывает единичной.\n * Без запаса затухание висело бы на ровном месте.\n */\nexport function resolveScrollOverflow(\n scrollStart: number,\n scrollSize: number,\n clientSize: number,\n): GrScrollOverflow {\n const hidden = scrollSize - clientSize\n\n if (hidden <= 1)\n return 'none'\n\n const start = Math.abs(scrollStart)\n const atStart = start <= 1\n const atEnd = hidden - start <= 1\n\n if (atStart && atEnd)\n return 'none'\n\n if (atStart)\n return 'end'\n\n if (atEnd)\n return 'start'\n\n return 'both'\n}\n","<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, watch, type Component } from 'vue'\n\nimport IconX from '~icons/lucide/x'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { useRovingFocus } from '../../composables/useRovingFocus'\nimport { iconClass, iconTag } from '../shared/icon'\nimport { resolveScrollOverflow, type GrScrollOverflow } from '../shared/scrollOverflow'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport {\n grTabsBadgeClass,\n grTabsCloseClass,\n grTabsEmptyClass,\n grTabsListClass,\n grTabsTabClass,\n tabCloseIconSizes,\n tabContentClass,\n tabIconClass,\n type GrTabsOrientation,\n type GrTabsSize,\n type GrTabsVariant,\n} from './grTabsStyles'\n\nexport interface GrTab {\n value: string\n label: string\n badge?: string\n /**\n * Иконка слева от подписи: Vue-компонент либо класс иконки вашей UnoCSS-сборки\n * (`'i-lucide-user'` — тогда нужен ваш `presetIcons`, см. `docs/installation.md`).\n */\n icon?: string | Component\n disabled?: boolean\n /** Перекрывает `closable` ряда в обе стороны: закрепить вкладку или закрыть одну её. */\n closable?: boolean\n}\n\nexport interface GrTabsProps {\n modelValue: string\n tabs: GrTab[]\n /**\n * База id для ARIA-связки с `GrTabPanels`. Если задана, каждая вкладка\n * получает `id=\"<idBase>-tab-<value>\"` и `aria-controls=\"<idBase>-panel-<value>\"`.\n * Передайте тот же `idBase` в `GrTabPanels`, чтобы связать `tab`↔`tabpanel`.\n */\n idBase?: string\n size?: GrTabsSize\n /** Вид ряда: обойма с таблетками или ряд с подчёркиванием. */\n variant?: GrTabsVariant\n /**\n * `automatic` (по умолчанию) — стрелка сразу переключает вкладку;\n * `manual` — стрелка двигает только фокус, выбор подтверждается\n * `Enter`/`Space`. Второй режим для вкладок с тяжёлой загрузкой: перебор\n * стрелками иначе тянет каждую панель.\n */\n activationMode?: GrTabsActivationMode\n /** Горизонтальный (по умолчанию) или вертикальный список вкладок. */\n orientation?: GrTabsOrientation\n /**\n * Крестик на вкладках и закрытие по `Delete`/`Backspace`. Точечно снимается\n * `closable: false` у самой вкладки.\n */\n closable?: boolean\n /** Текст, когда список вкладок пуст. Слот `#empty` сильнее. */\n emptyText?: string\n}\n\nexport interface GrTabsEmits {\n (e: 'update:modelValue', value: string): void\n /**\n * Просьба закрыть вкладку. Компонент список не трогает: `tabs` — проп, и\n * закрытие может не состояться («сохранить изменения?»).\n */\n (e: 'close', value: string): void\n}\n\nexport type GrTabsActivationMode = 'automatic' | 'manual'\n\nexport type { GrTabsOrientation, GrTabsVariant } from './grTabsStyles'\n\n/**\n * GrTabs — горизонтальная группа вкладок с паттерном WAI-ARIA `tablist`.\n *\n * A11y:\n * - `role=\"tablist\"` на корне, `role=\"tab\"` на каждой вкладке.\n * - Roving `tabindex`: активная вкладка `0`, остальные `-1`.\n * - Клавиатура: `ArrowLeft`/`ArrowRight` — цикличный переход, `Home`/`End` — к первой/последней.\n * - При переключении стрелками DOM-фокус программно переносится на новую вкладку.\n *\n * Сам компонент не рендерит `tabpanel` — это ответственность консьюмера (по `aria-controls`/внешней разметке).\n */\nconst props = withDefaults(defineProps<GrTabsProps>(), {\n idBase: undefined,\n size: undefined,\n // Дефолт живёт в резолвере: Vue подставил бы свой раньше, чем компонент\n // заглянет в `GrConfigProvider`.\n variant: undefined,\n activationMode: 'automatic',\n orientation: 'horizontal',\n closable: false,\n emptyText: undefined,\n})\n\ndefineSlots<{\n /** Содержимое вкладки целиком — вместо подписи, иконки и счётчика. Крестик остаётся. */\n tab?: (props: { tab: GrTab, active: boolean, disabled: boolean }) => unknown\n /** Пустой ряд — вместо текста из локали. */\n empty?: () => unknown\n}>()\n\nconst emit = defineEmits<GrTabsEmits>()\n\nconst { t } = useGranularityTranslations()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrTabs' })\nconst resolvedVariant = useGrComponentProp('GrTabs', 'variant', () => props.variant, 'pills')\n\nconst tablistClass = computed(() => grTabsListClass({\n variant: resolvedVariant.value,\n size: resolvedSize.value,\n orientation: props.orientation,\n}))\n\nfunction tabClass(tab: GrTab): string {\n return grTabsTabClass({\n variant: resolvedVariant.value,\n size: resolvedSize.value,\n active: tab.value === props.modelValue,\n disabled: Boolean(tab.disabled),\n })\n}\n\nconst badgeClass = computed(() => grTabsBadgeClass(resolvedSize.value))\nconst closeClass = computed(() => grTabsCloseClass(resolvedSize.value))\nconst closeIconClass = computed(() => tabCloseIconSizes[resolvedSize.value])\nconst emptyClass = computed(() => grTabsEmptyClass(resolvedSize.value))\n\n/**\n * Пустой ряд — не пустой `tablist`: роль обязана владеть потомками `tab`,\n * и текст внутри неё был бы нарушением, а не пустым состоянием. Поэтому\n * пустое состояние рисуется отдельной веткой, а не содержимым `tablist`.\n */\nconst isEmpty = computed(() => props.tabs.length === 0)\nconst resolvedEmptyText = computed(() => props.emptyText ?? t('gr.tabs.empty', 'Nothing here yet'))\nconst closeTitle = computed(() => t('gr.tabs.close', 'Close tab'))\n\n/** Отключённая вкладка не принимает взаимодействия, а закрытие — тоже взаимодействие. */\nfunction isClosable(tab: GrTab): boolean {\n return !tab.disabled && (tab.closable ?? props.closable)\n}\n\nconst buttonRefs = ref<HTMLButtonElement[]>([])\n\n/**\n * Vue зовёт ref-функцию с `null`, когда узел исчез. Игнорировать такой вызов\n * нельзя: в массиве оставались отсоединённые от DOM кнопки, и `focus()` по\n * сократившемуся списку молча проваливался в `<body>`.\n */\nfunction setButtonRef(el: unknown, index: number): void {\n if (el instanceof HTMLButtonElement)\n buttonRefs.value[index] = el\n else\n delete buttonRefs.value[index]\n\n if (buttonRefs.value.length > props.tabs.length)\n buttonRefs.value.length = props.tabs.length\n}\n\nconst activeIndex = computed(() => props.tabs.findIndex(t => t.value === props.modelValue))\n\nfunction isEnabled(tab: GrTab): boolean {\n return !tab.disabled\n}\n\nconst tabIndexes = computed(() => props.tabs.map((_, index) => index))\n\n/**\n * Кольцо roving-фокуса. При `activationMode: 'manual'` фокус уезжает вперёд\n * выбора — это и есть разница между `activeKey` примитива и `initialKey`:\n * первый ведёт фокус, второй подставляет выбранную вкладку, пока фокус не\n * трогали.\n *\n * Выбор стрелка **не** переносит: этим заведует `onKeydown` по\n * `activationMode`. Через `onMove` это выразить нельзя — клик тоже зовёт\n * `focusKey`, и выбор эмитился бы дважды.\n */\nconst roving = useRovingFocus<number>({\n items: () => tabIndexes.value,\n elementFor: index => buttonRefs.value[index],\n isDisabled: index => Boolean(props.tabs[index]?.disabled),\n orientation: () => (props.orientation === 'vertical' ? 'vertical' : 'horizontal'),\n skipDisabled: () => true,\n /**\n * Пока фокус не трогали, остановку `Tab` держит выбранная вкладка. Если\n * `modelValue` не совпал ни с одной (пустое начальное значение, асинхронный\n * список, удалённая активная вкладка), примитив ставит её на первую\n * доступную — иначе tablist выпал бы из обхода целиком и молча.\n *\n * Выключенность здесь не проверяется намеренно: на кнопке `aria-disabled`, а\n * не нативный `disabled`, поэтому выключенная вкладка фокусируется и держать\n * остановку вправе.\n */\n initialKey: () => (activeIndex.value >= 0 ? activeIndex.value : undefined),\n /**\n * Прокрутка до фокуса, а не после: иначе браузер доскроллит сам по-своему, и\n * ряд дёрнется дважды. Синхронно — фокус обязан остаться в том же такте, что\n * и раньше: тик, где он нужен, добавляет `focusIndex`.\n */\n beforeFocus: index => scrollIntoView(index),\n})\n\n/**\n * Фокус после перерисовки — для путей, где список только что изменился\n * (закрытие вкладки). Клавиатура идёт через примитив напрямую: там ряд уже\n * отрисован, и лишний тик только отложил бы фокус.\n */\nasync function focusIndex(index: number): Promise<void> {\n await nextTick()\n await roving.focusKey(index)\n}\n\n/**\n * Ряд прокручивается, а не переносится, поэтому активная вкладка может уехать\n * за край — например, когда её выбрали снаружи. `scrollIntoView` нет в jsdom и\n * в старых движках, поэтому вызов необязательный.\n */\nfunction scrollIntoView(index: number): void {\n buttonRefs.value[index]?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' })\n}\n\nwatch(() => props.modelValue, async () => {\n await nextTick()\n if (activeIndex.value >= 0)\n scrollIntoView(activeIndex.value)\n})\n\n/**\n * Признак того, что ряд продолжается за краем.\n *\n * Полоса прокрутки у ряда скрыта намеренно, и без этого признака продолжение\n * не выдаёт себя ничем: вкладки за краем достижимы стрелками, но о том, что они\n * там есть, догадаться нечем. Затухание рисует CSS по `data-overflow`.\n *\n * Вертикальный ряд не скроллится вовсе — он растёт вниз, — поэтому состояние\n * считается только для горизонтального.\n */\nconst tablistEl = ref<HTMLElement | null>(null)\nconst scrollOverflow = ref<GrScrollOverflow>('none')\n\nlet resizeObserver: ResizeObserver | null = null\nlet scheduled = false\n\nfunction measureOverflow(): void {\n const el = tablistEl.value\n\n if (!el || props.orientation === 'vertical') {\n scrollOverflow.value = 'none'\n return\n }\n\n scrollOverflow.value = resolveScrollOverflow(el.scrollLeft, el.scrollWidth, el.clientWidth)\n}\n\nfunction scheduleMeasure(): void {\n if (scheduled)\n return\n\n scheduled = true\n void nextTick(() => {\n scheduled = false\n measureOverflow()\n })\n}\n\nonMounted(() => {\n scheduleMeasure()\n\n // На сервере и в jsdom `ResizeObserver` отсутствует — измерять там нечего.\n if (typeof ResizeObserver === 'undefined')\n return\n\n resizeObserver = new ResizeObserver(scheduleMeasure)\n if (tablistEl.value)\n resizeObserver.observe(tablistEl.value)\n})\n\nonBeforeUnmount(() => {\n resizeObserver?.disconnect()\n resizeObserver = null\n})\n\n// Состав ряда меняет его ширину, ориентация — само наличие прокрутки.\nwatch(() => [props.tabs.length, props.orientation], scheduleMeasure)\n\nasync function selectByIndex(index: number, focus = false): Promise<void> {\n const tab = props.tabs[index]\n if (!tab || !isEnabled(tab))\n return\n if (tab.value !== props.modelValue)\n emit('update:modelValue', tab.value)\n if (focus)\n await focusIndex(index)\n}\n\nfunction onKeydown(event: KeyboardEvent): void {\n if (props.tabs.length === 0)\n return\n\n const currentIndex = roving.rovingKey.value ?? 0\n\n switch (event.key) {\n case 'Enter':\n case ' ':\n // В ручном режиме выбор подтверждается явно — на той вкладке, где фокус.\n if (props.activationMode === 'manual') {\n event.preventDefault()\n void selectByIndex(currentIndex, false)\n }\n return\n case 'Delete':\n case 'Backspace': {\n // Единственный клавиатурный путь к закрытию: крестик в таб-порядке не стоит.\n const tab = props.tabs[currentIndex]\n if (tab && isClosable(tab)) {\n event.preventDefault()\n requestClose(currentIndex)\n }\n return\n }\n }\n\n // Стрелки, `Home`, `End` — фокус ведёт примитив по объявленной ориентации.\n if (!roving.handleNavigationKeys(event))\n return\n\n // `automatic`: выбор едет за фокусом. `manual`: ждёт `Enter`/`Space`.\n if (props.activationMode !== 'manual')\n void selectByIndex(roving.rovingKey.value ?? currentIndex, false)\n}\n\n/**\n * Закрытая вкладка, которая держала фокус. Компонент список не укорачивает —\n * это делает потребитель, — поэтому фокус возвращается только после того, как\n * `tabs` действительно стал короче.\n */\nlet pendingClose: { index: number, value: string } | null = null\n\nfunction requestClose(index: number): void {\n const tab = props.tabs[index]\n if (!tab || !isClosable(tab))\n return\n\n const held = buttonRefs.value[index] === document.activeElement\n pendingClose = held ? { index, value: tab.value } : null\n\n emit('close', tab.value)\n}\n\n// Без этого клавиатурное закрытие роняет фокус в `<body>`: кнопка, на которой\n// он стоял, исчезает из DOM вместе со вкладкой.\nwatch(() => props.tabs.length, async (next, prev) => {\n const pending = pendingClose\n pendingClose = null\n\n if (!pending || next >= prev)\n return\n // Ушла не та вкладка — потребитель поменял список по своей причине.\n if (props.tabs.some(tab => tab.value === pending.value))\n return\n\n await nextTick()\n\n const target = Math.min(pending.index, next - 1)\n if (target >= 0)\n void focusIndex(target)\n else\n roving.reset()\n})\n\nfunction onClick(event: MouseEvent, tab: GrTab, index: number): void {\n if (!isEnabled(tab))\n return\n\n // Крестик — не кнопка, поэтому его клик приходит сюда же и разбирается по цели.\n if (event.target instanceof Element && event.target.closest('[data-gr-tab-close]')) {\n requestClose(index)\n return\n }\n\n roving.setActive(index)\n\n if (tab.value !== props.modelValue)\n emit('update:modelValue', tab.value)\n}\n</script>\n\n<template>\n <div v-if=\"isEmpty\" data-gr-tabs-empty :class=\"emptyClass\">\n <slot name=\"empty\">\n {{ resolvedEmptyText }}\n </slot>\n </div>\n\n <div\n v-else\n ref=\"tablistEl\"\n role=\"tablist\"\n data-gr-tabs\n :aria-orientation=\"orientation\"\n :data-overflow=\"orientation === 'vertical' ? undefined : scrollOverflow\"\n :class=\"tablistClass\"\n @keydown=\"onKeydown\"\n @scroll.passive=\"scheduleMeasure\"\n >\n <button\n v-for=\"(tab, index) in tabs\"\n :id=\"idBase ? `${idBase}-tab-${tab.value}` : undefined\"\n :key=\"tab.value\"\n :ref=\"el => setButtonRef(el, index)\"\n type=\"button\"\n role=\"tab\"\n data-gr-tab\n :aria-controls=\"idBase ? `${idBase}-panel-${tab.value}` : undefined\"\n :aria-selected=\"tab.value === modelValue ? 'true' : 'false'\"\n :aria-disabled=\"tab.disabled ? 'true' : undefined\"\n :aria-keyshortcuts=\"isClosable(tab) ? 'Delete' : undefined\"\n :tabindex=\"roving.tabindexFor(index)\"\n :class=\"tabClass(tab)\"\n @click=\"onClick($event, tab, index)\"\n >\n <span :class=\"tabContentClass\">\n <slot\n name=\"tab\"\n :tab=\"tab\"\n :active=\"tab.value === modelValue\"\n :disabled=\"Boolean(tab.disabled)\"\n >\n <component\n :is=\"iconTag(tab.icon)\"\n v-if=\"tab.icon\"\n :class=\"[tabIconClass, iconClass(tab.icon)]\"\n aria-hidden=\"true\"\n />\n <span>{{ tab.label }}</span>\n <span v-if=\"tab.badge\" :class=\"badgeClass\">\n {{ tab.badge }}\n </span>\n </slot>\n\n <!-- Снаружи слота: своя разметка вкладки не должна отнимать закрытие. -->\n <span\n v-if=\"isClosable(tab)\"\n data-gr-tab-close\n :class=\"closeClass\"\n :title=\"closeTitle\"\n aria-hidden=\"true\"\n >\n <IconX :class=\"closeIconClass\" />\n </span>\n </span>\n </button>\n </div>\n</template>\n\n<style>\n/*\n * Затухание у края, за которым ряд продолжается.\n *\n * Здесь, а не утилитами, по двум причинам сразу. `mask-image` в `presetMini` не\n * выражается — это уже зафиксировано отказом в `GrImageCrop`. И маской, а не\n * градиентом-подложкой, потому что фон полосы у вариантов разный: `pills` несёт\n * свой непрозрачный `--gr-muted`, а `line` прозрачен и лежит на фоне родителя,\n * которого компонент не знает. Градиенту пришлось бы красить в цвет подложки —\n * для `line` его неоткуда взять. Маска гасит содержимое независимо от того, что\n * под ним.\n *\n * Состояние считается логически (начало/конец ряда), а направление градиента в\n * CSS выражается только физически — отсюда зеркальные правила для RTL.\n */\n\n[data-gr-tabs] {\n /*\n * `scrollIntoView({ inline: 'nearest' })` из кольца фокуса прижимает вкладку\n * вплотную к краю. Без отступа прокрутки кольцо фокуса у клавиатурного\n * пользователя оказалось бы ровно под затуханием.\n */\n scroll-padding-inline: var(--gr-tabs-scroll-fade, 1.5rem);\n}\n\n[data-gr-tabs][data-overflow='start'] {\n --gr-tabs-mask: linear-gradient(to right, transparent 0, #000 var(--gr-tabs-scroll-fade, 1.5rem));\n}\n\n[data-gr-tabs][data-overflow='end'] {\n --gr-tabs-mask: linear-gradient(to right, #000 calc(100% - var(--gr-tabs-scroll-fade, 1.5rem)), transparent 100%);\n}\n\n[data-gr-tabs][data-overflow='both'] {\n --gr-tabs-mask: linear-gradient(\n to right,\n transparent 0,\n #000 var(--gr-tabs-scroll-fade, 1.5rem),\n #000 calc(100% - var(--gr-tabs-scroll-fade, 1.5rem)),\n transparent 100%\n );\n}\n\n[dir='rtl'] [data-gr-tabs][data-overflow='start'] {\n --gr-tabs-mask: linear-gradient(to left, transparent 0, #000 var(--gr-tabs-scroll-fade, 1.5rem));\n}\n\n[dir='rtl'] [data-gr-tabs][data-overflow='end'] {\n --gr-tabs-mask: linear-gradient(to left, #000 calc(100% - var(--gr-tabs-scroll-fade, 1.5rem)), transparent 100%);\n}\n\n[data-gr-tabs]:is([data-overflow='start'], [data-overflow='end'], [data-overflow='both']) {\n -webkit-mask-image: var(--gr-tabs-mask);\n mask-image: var(--gr-tabs-mask);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, watch, type Component } from 'vue'\n\nimport IconX from '~icons/lucide/x'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { useRovingFocus } from '../../composables/useRovingFocus'\nimport { iconClass, iconTag } from '../shared/icon'\nimport { resolveScrollOverflow, type GrScrollOverflow } from '../shared/scrollOverflow'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport {\n grTabsBadgeClass,\n grTabsCloseClass,\n grTabsEmptyClass,\n grTabsListClass,\n grTabsTabClass,\n tabCloseIconSizes,\n tabContentClass,\n tabIconClass,\n type GrTabsOrientation,\n type GrTabsSize,\n type GrTabsVariant,\n} from './grTabsStyles'\n\nexport interface GrTab {\n value: string\n label: string\n badge?: string\n /**\n * Иконка слева от подписи: Vue-компонент либо класс иконки вашей UnoCSS-сборки\n * (`'i-lucide-user'` — тогда нужен ваш `presetIcons`, см. `docs/installation.md`).\n */\n icon?: string | Component\n disabled?: boolean\n /** Перекрывает `closable` ряда в обе стороны: закрепить вкладку или закрыть одну её. */\n closable?: boolean\n}\n\nexport interface GrTabsProps {\n modelValue: string\n tabs: GrTab[]\n /**\n * База id для ARIA-связки с `GrTabPanels`. Если задана, каждая вкладка\n * получает `id=\"<idBase>-tab-<value>\"` и `aria-controls=\"<idBase>-panel-<value>\"`.\n * Передайте тот же `idBase` в `GrTabPanels`, чтобы связать `tab`↔`tabpanel`.\n */\n idBase?: string\n size?: GrTabsSize\n /** Вид ряда: обойма с таблетками или ряд с подчёркиванием. */\n variant?: GrTabsVariant\n /**\n * `automatic` (по умолчанию) — стрелка сразу переключает вкладку;\n * `manual` — стрелка двигает только фокус, выбор подтверждается\n * `Enter`/`Space`. Второй режим для вкладок с тяжёлой загрузкой: перебор\n * стрелками иначе тянет каждую панель.\n */\n activationMode?: GrTabsActivationMode\n /** Горизонтальный (по умолчанию) или вертикальный список вкладок. */\n orientation?: GrTabsOrientation\n /**\n * Крестик на вкладках и закрытие по `Delete`/`Backspace`. Точечно снимается\n * `closable: false` у самой вкладки.\n */\n closable?: boolean\n /** Текст, когда список вкладок пуст. Слот `#empty` сильнее. */\n emptyText?: string\n}\n\nexport interface GrTabsEmits {\n (e: 'update:modelValue', value: string): void\n /**\n * Просьба закрыть вкладку. Компонент список не трогает: `tabs` — проп, и\n * закрытие может не состояться («сохранить изменения?»).\n */\n (e: 'close', value: string): void\n}\n\nexport type GrTabsActivationMode = 'automatic' | 'manual'\n\nexport type { GrTabsOrientation, GrTabsVariant } from './grTabsStyles'\n\n/**\n * GrTabs — горизонтальная группа вкладок с паттерном WAI-ARIA `tablist`.\n *\n * A11y:\n * - `role=\"tablist\"` на корне, `role=\"tab\"` на каждой вкладке.\n * - Roving `tabindex`: активная вкладка `0`, остальные `-1`.\n * - Клавиатура: `ArrowLeft`/`ArrowRight` — цикличный переход, `Home`/`End` — к первой/последней.\n * - При переключении стрелками DOM-фокус программно переносится на новую вкладку.\n *\n * Сам компонент не рендерит `tabpanel` — это ответственность консьюмера (по `aria-controls`/внешней разметке).\n */\nconst props = withDefaults(defineProps<GrTabsProps>(), {\n idBase: undefined,\n size: undefined,\n // Дефолт живёт в резолвере: Vue подставил бы свой раньше, чем компонент\n // заглянет в `GrConfigProvider`.\n variant: undefined,\n activationMode: 'automatic',\n orientation: 'horizontal',\n closable: false,\n emptyText: undefined,\n})\n\ndefineSlots<{\n /** Содержимое вкладки целиком — вместо подписи, иконки и счётчика. Крестик остаётся. */\n tab?: (props: { tab: GrTab, active: boolean, disabled: boolean }) => unknown\n /** Пустой ряд — вместо текста из локали. */\n empty?: () => unknown\n}>()\n\nconst emit = defineEmits<GrTabsEmits>()\n\nconst { t } = useGranularityTranslations()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrTabs' })\nconst resolvedVariant = useGrComponentProp('GrTabs', 'variant', () => props.variant, 'pills')\n\nconst tablistClass = computed(() => grTabsListClass({\n variant: resolvedVariant.value,\n size: resolvedSize.value,\n orientation: props.orientation,\n}))\n\nfunction tabClass(tab: GrTab): string {\n return grTabsTabClass({\n variant: resolvedVariant.value,\n size: resolvedSize.value,\n active: tab.value === props.modelValue,\n disabled: Boolean(tab.disabled),\n })\n}\n\nconst badgeClass = computed(() => grTabsBadgeClass(resolvedSize.value))\nconst closeClass = computed(() => grTabsCloseClass(resolvedSize.value))\nconst closeIconClass = computed(() => tabCloseIconSizes[resolvedSize.value])\nconst emptyClass = computed(() => grTabsEmptyClass(resolvedSize.value))\n\n/**\n * Пустой ряд — не пустой `tablist`: роль обязана владеть потомками `tab`,\n * и текст внутри неё был бы нарушением, а не пустым состоянием. Поэтому\n * пустое состояние рисуется отдельной веткой, а не содержимым `tablist`.\n */\nconst isEmpty = computed(() => props.tabs.length === 0)\nconst resolvedEmptyText = computed(() => props.emptyText ?? t('gr.tabs.empty', 'Nothing here yet'))\nconst closeTitle = computed(() => t('gr.tabs.close', 'Close tab'))\n\n/** Отключённая вкладка не принимает взаимодействия, а закрытие — тоже взаимодействие. */\nfunction isClosable(tab: GrTab): boolean {\n return !tab.disabled && (tab.closable ?? props.closable)\n}\n\nconst buttonRefs = ref<HTMLButtonElement[]>([])\n\n/**\n * Vue зовёт ref-функцию с `null`, когда узел исчез. Игнорировать такой вызов\n * нельзя: в массиве оставались отсоединённые от DOM кнопки, и `focus()` по\n * сократившемуся списку молча проваливался в `<body>`.\n */\nfunction setButtonRef(el: unknown, index: number): void {\n if (el instanceof HTMLButtonElement)\n buttonRefs.value[index] = el\n else\n delete buttonRefs.value[index]\n\n if (buttonRefs.value.length > props.tabs.length)\n buttonRefs.value.length = props.tabs.length\n}\n\nconst activeIndex = computed(() => props.tabs.findIndex(t => t.value === props.modelValue))\n\nfunction isEnabled(tab: GrTab): boolean {\n return !tab.disabled\n}\n\nconst tabIndexes = computed(() => props.tabs.map((_, index) => index))\n\n/**\n * Кольцо roving-фокуса. При `activationMode: 'manual'` фокус уезжает вперёд\n * выбора — это и есть разница между `activeKey` примитива и `initialKey`:\n * первый ведёт фокус, второй подставляет выбранную вкладку, пока фокус не\n * трогали.\n *\n * Выбор стрелка **не** переносит: этим заведует `onKeydown` по\n * `activationMode`. Через `onMove` это выразить нельзя — клик тоже зовёт\n * `focusKey`, и выбор эмитился бы дважды.\n */\nconst roving = useRovingFocus<number>({\n items: () => tabIndexes.value,\n elementFor: index => buttonRefs.value[index],\n isDisabled: index => Boolean(props.tabs[index]?.disabled),\n orientation: () => (props.orientation === 'vertical' ? 'vertical' : 'horizontal'),\n skipDisabled: () => true,\n /**\n * Пока фокус не трогали, остановку `Tab` держит выбранная вкладка. Если\n * `modelValue` не совпал ни с одной (пустое начальное значение, асинхронный\n * список, удалённая активная вкладка), примитив ставит её на первую\n * доступную — иначе tablist выпал бы из обхода целиком и молча.\n *\n * Выключенность здесь не проверяется намеренно: на кнопке `aria-disabled`, а\n * не нативный `disabled`, поэтому выключенная вкладка фокусируется и держать\n * остановку вправе.\n */\n initialKey: () => (activeIndex.value >= 0 ? activeIndex.value : undefined),\n /**\n * Прокрутка до фокуса, а не после: иначе браузер доскроллит сам по-своему, и\n * ряд дёрнется дважды. Синхронно — фокус обязан остаться в том же такте, что\n * и раньше: тик, где он нужен, добавляет `focusIndex`.\n */\n beforeFocus: index => scrollIntoView(index),\n})\n\n/**\n * Фокус после перерисовки — для путей, где список только что изменился\n * (закрытие вкладки). Клавиатура идёт через примитив напрямую: там ряд уже\n * отрисован, и лишний тик только отложил бы фокус.\n */\nasync function focusIndex(index: number): Promise<void> {\n await nextTick()\n await roving.focusKey(index)\n}\n\n/**\n * Ряд прокручивается, а не переносится, поэтому активная вкладка может уехать\n * за край — например, когда её выбрали снаружи. `scrollIntoView` нет в jsdom и\n * в старых движках, поэтому вызов необязательный.\n */\nfunction scrollIntoView(index: number): void {\n buttonRefs.value[index]?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' })\n}\n\nwatch(() => props.modelValue, async () => {\n await nextTick()\n if (activeIndex.value >= 0)\n scrollIntoView(activeIndex.value)\n})\n\n/**\n * Признак того, что ряд продолжается за краем.\n *\n * Полоса прокрутки у ряда скрыта намеренно, и без этого признака продолжение\n * не выдаёт себя ничем: вкладки за краем достижимы стрелками, но о том, что они\n * там есть, догадаться нечем. Затухание рисует CSS по `data-overflow`.\n *\n * Вертикальный ряд не скроллится вовсе — он растёт вниз, — поэтому состояние\n * считается только для горизонтального.\n */\nconst tablistEl = ref<HTMLElement | null>(null)\nconst scrollOverflow = ref<GrScrollOverflow>('none')\n\nlet resizeObserver: ResizeObserver | null = null\nlet scheduled = false\n\nfunction measureOverflow(): void {\n const el = tablistEl.value\n\n if (!el || props.orientation === 'vertical') {\n scrollOverflow.value = 'none'\n return\n }\n\n scrollOverflow.value = resolveScrollOverflow(el.scrollLeft, el.scrollWidth, el.clientWidth)\n}\n\nfunction scheduleMeasure(): void {\n if (scheduled)\n return\n\n scheduled = true\n void nextTick(() => {\n scheduled = false\n measureOverflow()\n })\n}\n\nonMounted(() => {\n scheduleMeasure()\n\n // На сервере и в jsdom `ResizeObserver` отсутствует — измерять там нечего.\n if (typeof ResizeObserver === 'undefined')\n return\n\n resizeObserver = new ResizeObserver(scheduleMeasure)\n if (tablistEl.value)\n resizeObserver.observe(tablistEl.value)\n})\n\nonBeforeUnmount(() => {\n resizeObserver?.disconnect()\n resizeObserver = null\n})\n\n// Состав ряда меняет его ширину, ориентация — само наличие прокрутки.\nwatch(() => [props.tabs.length, props.orientation], scheduleMeasure)\n\nasync function selectByIndex(index: number, focus = false): Promise<void> {\n const tab = props.tabs[index]\n if (!tab || !isEnabled(tab))\n return\n if (tab.value !== props.modelValue)\n emit('update:modelValue', tab.value)\n if (focus)\n await focusIndex(index)\n}\n\nfunction onKeydown(event: KeyboardEvent): void {\n if (props.tabs.length === 0)\n return\n\n const currentIndex = roving.rovingKey.value ?? 0\n\n switch (event.key) {\n case 'Enter':\n case ' ':\n // В ручном режиме выбор подтверждается явно — на той вкладке, где фокус.\n if (props.activationMode === 'manual') {\n event.preventDefault()\n void selectByIndex(currentIndex, false)\n }\n return\n case 'Delete':\n case 'Backspace': {\n // Единственный клавиатурный путь к закрытию: крестик в таб-порядке не стоит.\n const tab = props.tabs[currentIndex]\n if (tab && isClosable(tab)) {\n event.preventDefault()\n requestClose(currentIndex)\n }\n return\n }\n }\n\n // Стрелки, `Home`, `End` — фокус ведёт примитив по объявленной ориентации.\n if (!roving.handleNavigationKeys(event))\n return\n\n // `automatic`: выбор едет за фокусом. `manual`: ждёт `Enter`/`Space`.\n if (props.activationMode !== 'manual')\n void selectByIndex(roving.rovingKey.value ?? currentIndex, false)\n}\n\n/**\n * Закрытая вкладка, которая держала фокус. Компонент список не укорачивает —\n * это делает потребитель, — поэтому фокус возвращается только после того, как\n * `tabs` действительно стал короче.\n */\nlet pendingClose: { index: number, value: string } | null = null\n\nfunction requestClose(index: number): void {\n const tab = props.tabs[index]\n if (!tab || !isClosable(tab))\n return\n\n const held = buttonRefs.value[index] === document.activeElement\n pendingClose = held ? { index, value: tab.value } : null\n\n emit('close', tab.value)\n}\n\n// Без этого клавиатурное закрытие роняет фокус в `<body>`: кнопка, на которой\n// он стоял, исчезает из DOM вместе со вкладкой.\nwatch(() => props.tabs.length, async (next, prev) => {\n const pending = pendingClose\n pendingClose = null\n\n if (!pending || next >= prev)\n return\n // Ушла не та вкладка — потребитель поменял список по своей причине.\n if (props.tabs.some(tab => tab.value === pending.value))\n return\n\n await nextTick()\n\n const target = Math.min(pending.index, next - 1)\n if (target >= 0)\n void focusIndex(target)\n else\n roving.reset()\n})\n\nfunction onClick(event: MouseEvent, tab: GrTab, index: number): void {\n if (!isEnabled(tab))\n return\n\n // Крестик — не кнопка, поэтому его клик приходит сюда же и разбирается по цели.\n if (event.target instanceof Element && event.target.closest('[data-gr-tab-close]')) {\n requestClose(index)\n return\n }\n\n roving.setActive(index)\n\n if (tab.value !== props.modelValue)\n emit('update:modelValue', tab.value)\n}\n</script>\n\n<template>\n <div v-if=\"isEmpty\" data-gr-tabs-empty :class=\"emptyClass\">\n <slot name=\"empty\">\n {{ resolvedEmptyText }}\n </slot>\n </div>\n\n <div\n v-else\n ref=\"tablistEl\"\n role=\"tablist\"\n data-gr-tabs\n :aria-orientation=\"orientation\"\n :data-overflow=\"orientation === 'vertical' ? undefined : scrollOverflow\"\n :class=\"tablistClass\"\n @keydown=\"onKeydown\"\n @scroll.passive=\"scheduleMeasure\"\n >\n <button\n v-for=\"(tab, index) in tabs\"\n :id=\"idBase ? `${idBase}-tab-${tab.value}` : undefined\"\n :key=\"tab.value\"\n :ref=\"el => setButtonRef(el, index)\"\n type=\"button\"\n role=\"tab\"\n data-gr-tab\n :aria-controls=\"idBase ? `${idBase}-panel-${tab.value}` : undefined\"\n :aria-selected=\"tab.value === modelValue ? 'true' : 'false'\"\n :aria-disabled=\"tab.disabled ? 'true' : undefined\"\n :aria-keyshortcuts=\"isClosable(tab) ? 'Delete' : undefined\"\n :tabindex=\"roving.tabindexFor(index)\"\n :class=\"tabClass(tab)\"\n @click=\"onClick($event, tab, index)\"\n >\n <span :class=\"tabContentClass\">\n <slot\n name=\"tab\"\n :tab=\"tab\"\n :active=\"tab.value === modelValue\"\n :disabled=\"Boolean(tab.disabled)\"\n >\n <component\n :is=\"iconTag(tab.icon)\"\n v-if=\"tab.icon\"\n :class=\"[tabIconClass, iconClass(tab.icon)]\"\n aria-hidden=\"true\"\n />\n <span>{{ tab.label }}</span>\n <span v-if=\"tab.badge\" :class=\"badgeClass\">\n {{ tab.badge }}\n </span>\n </slot>\n\n <!-- Снаружи слота: своя разметка вкладки не должна отнимать закрытие. -->\n <span\n v-if=\"isClosable(tab)\"\n data-gr-tab-close\n :class=\"closeClass\"\n :title=\"closeTitle\"\n aria-hidden=\"true\"\n >\n <IconX :class=\"closeIconClass\" />\n </span>\n </span>\n </button>\n </div>\n</template>\n\n<style>\n/*\n * Затухание у края, за которым ряд продолжается.\n *\n * Здесь, а не утилитами, по двум причинам сразу. `mask-image` в `presetMini` не\n * выражается — это уже зафиксировано отказом в `GrImageCrop`. И маской, а не\n * градиентом-подложкой, потому что фон полосы у вариантов разный: `pills` несёт\n * свой непрозрачный `--gr-muted`, а `line` прозрачен и лежит на фоне родителя,\n * которого компонент не знает. Градиенту пришлось бы красить в цвет подложки —\n * для `line` его неоткуда взять. Маска гасит содержимое независимо от того, что\n * под ним.\n *\n * Состояние считается логически (начало/конец ряда), а направление градиента в\n * CSS выражается только физически — отсюда зеркальные правила для RTL.\n */\n\n[data-gr-tabs] {\n /*\n * `scrollIntoView({ inline: 'nearest' })` из кольца фокуса прижимает вкладку\n * вплотную к краю. Без отступа прокрутки кольцо фокуса у клавиатурного\n * пользователя оказалось бы ровно под затуханием.\n */\n scroll-padding-inline: var(--gr-tabs-scroll-fade, 1.5rem);\n}\n\n[data-gr-tabs][data-overflow='start'] {\n --gr-tabs-mask: linear-gradient(to right, transparent 0, #000 var(--gr-tabs-scroll-fade, 1.5rem));\n}\n\n[data-gr-tabs][data-overflow='end'] {\n --gr-tabs-mask: linear-gradient(to right, #000 calc(100% - var(--gr-tabs-scroll-fade, 1.5rem)), transparent 100%);\n}\n\n[data-gr-tabs][data-overflow='both'] {\n --gr-tabs-mask: linear-gradient(\n to right,\n transparent 0,\n #000 var(--gr-tabs-scroll-fade, 1.5rem),\n #000 calc(100% - var(--gr-tabs-scroll-fade, 1.5rem)),\n transparent 100%\n );\n}\n\n[dir='rtl'] [data-gr-tabs][data-overflow='start'] {\n --gr-tabs-mask: linear-gradient(to left, transparent 0, #000 var(--gr-tabs-scroll-fade, 1.5rem));\n}\n\n[dir='rtl'] [data-gr-tabs][data-overflow='end'] {\n --gr-tabs-mask: linear-gradient(to left, #000 calc(100% - var(--gr-tabs-scroll-fade, 1.5rem)), transparent 100%);\n}\n\n[data-gr-tabs]:is([data-overflow='start'], [data-overflow='end'], [data-overflow='both']) {\n -webkit-mask-image: var(--gr-tabs-mask);\n mask-image: var(--gr-tabs-mask);\n}\n</style>\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
- import { t as GrTabs_default } from "./chunks/GrTabs-CEKT5tXv.js";
2
- import { n as grTabsSafelist, t as grTabsConfig } from "../../chunks/config-_QF0O1vq.js";
1
+ import { t as GrTabs_default } from "./chunks/GrTabs-CT2nk_HV.js";
2
+ import { n as grTabsSafelist, t as grTabsConfig } from "../../chunks/config-CMNfZO7S.js";
3
3
  export { GrTabs_default as GrTabs, GrTabs_default as default, grTabsConfig, grTabsSafelist };
@@ -14,7 +14,7 @@ import { t as useControlAddons } from "../../../chunks/useControlAddons-asUr10Pf
14
14
  import { n as panelPopTransition } from "../../../chunks/overlayPanelSurface-BKeMhRx4.js";
15
15
  import { t as check_default } from "../../../chunks/check-BZI24Qs4.js";
16
16
  import { t as chevron_down_default } from "../../../chunks/chevron-down-CQS2OG_s.js";
17
- import { t as GrInput_default } from "../../GrInput/chunks/GrInput-BLywRZUN.js";
17
+ import { t as GrInput_default } from "../../GrInput/chunks/GrInput-CJ7eu3fR.js";
18
18
  import { t as GrTree_default } from "../../GrTree/chunks/GrTree-Ci6CZfKR.js";
19
19
  import { a as grTreeSelectStateClass, i as grTreeSelectPanelClass, o as paddingX, r as grTreeSelectClass, s as trailingZoneWidth } from "../../../chunks/config-DUNH02Fg.js";
20
20
  import { Fragment, Teleport, Transition, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, mergeProps, nextTick, normalizeClass, normalizeStyle, openBlock, ref, renderList, renderSlot, toDisplayString, unref, useId, useSlots, vShow, watch, withCtx, withDirectives, withModifiers } from "vue";