@feugene/granularity 0.34.2 → 0.35.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.
@@ -5,8 +5,8 @@ import { defineGranularComponent } from "@feugene/unocss-preset-granular/contrac
5
5
  var tabSizes = {
6
6
  xs: "h-7 px-2 text-[length:var(--gr-text-xs)] leading-[var(--gr-leading-xs)]",
7
7
  sm: "h-8 px-2.5 text-[length:var(--gr-text-xs)] leading-[var(--gr-leading-xs)]",
8
- md: "h-9 px-3 text-[length:var(--gr-text-sm)] leading-[var(--gr-leading-sm)]",
9
- lg: "h-10 px-4 text-[length:var(--gr-text-base)] leading-[var(--gr-leading-base)]"
8
+ md: "h-10 px-3 text-[length:var(--gr-text-sm)] leading-[var(--gr-leading-sm)]",
9
+ lg: "h-11 px-4 text-[length:var(--gr-text-base)] leading-[var(--gr-leading-base)]"
10
10
  };
11
11
  /** Счётчик у вкладки: на ступень мельче подписи. */
12
12
  var tabBadgeSizes = {
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-CMNfZO7S.js","names":[],"sources":["../../src/components/GrTabs/grTabsStyles.ts","../../src/components/GrTabs/safelist.ts","../../src/components/GrTabs/config.ts"],"sourcesContent":["import type { GrComponentSize } from '../shared/sizes'\n\nexport type GrTabsSize = GrComponentSize\n\n/**\n * `pills` — вкладки-таблетки в общей обойме, `line` — классический ряд с\n * подчёркиванием активной. Имя `pills` общее с `GrSegmented`: у компонентов\n * одна роль, и вид с таким названием у них совпадает.\n */\nexport const GR_TABS_VARIANTS = ['pills', 'line'] as const\nexport type GrTabsVariant = typeof GR_TABS_VARIANTS[number]\n\nexport type GrTabsOrientation = 'horizontal' | 'vertical'\n\n/** Высота вкладки повторяет шкалу `GrButton`: вкладки часто стоят с ним в один ряд. */\nexport const tabSizes: Record<GrTabsSize, string> = {\n xs: 'h-7 px-2 text-[length:var(--gr-text-xs)] leading-[var(--gr-leading-xs)]',\n sm: 'h-8 px-2.5 text-[length:var(--gr-text-xs)] leading-[var(--gr-leading-xs)]',\n md: 'h-10 px-3 text-[length:var(--gr-text-sm)] leading-[var(--gr-leading-sm)]',\n lg: 'h-11 px-4 text-[length:var(--gr-text-base)] leading-[var(--gr-leading-base)]',\n}\n\n/** Счётчик у вкладки: на ступень мельче подписи. */\nexport const tabBadgeSizes: Record<GrTabsSize, string> = {\n xs: 'text-[length:var(--gr-text-2xs)] leading-[var(--gr-leading-2xs)] px-1 py-0.5',\n sm: 'text-[length:var(--gr-text-2xs)] leading-[var(--gr-leading-2xs)] px-1.5 py-0.5',\n md: 'text-[length:var(--gr-text-2xs)] leading-[var(--gr-leading-2xs)] px-1.5 py-0.5',\n lg: 'text-[length:var(--gr-text-xs)] leading-[var(--gr-leading-xs)] px-2 py-0.5',\n}\n\n/** Обойма вокруг вкладок: на мелких ступенях рамка съедала бы высоту. */\nexport const tablistSizes: Record<GrTabsSize, string> = {\n xs: 'gap-0.5 p-0.5',\n sm: 'gap-1 p-1',\n md: 'gap-1 p-1',\n lg: 'gap-1 p-1.5',\n}\n\n/** `line` живёт без обоймы, поэтому и без внутренних отступов. */\nexport const tablistLineSizes: Record<GrTabsSize, string> = {\n xs: 'gap-1',\n sm: 'gap-2',\n md: 'gap-2',\n lg: 'gap-3',\n}\n\nexport const tablistBase = 'inline-flex max-w-full'\n\nexport const tablistVariants: Record<GrTabsVariant, string> = {\n pills: 'rounded-[var(--gr-radius-lg)] border border-[var(--gr-brd)] bg-[var(--gr-muted)]',\n line: 'border-b border-[var(--gr-brd)]',\n}\n\n/**\n * Вертикальный `line` подчёркивает вкладку сбоку, а не снизу: линия должна идти\n * вдоль списка, иначе она отделяет вкладки друг от друга вместо ряда целиком.\n */\nexport const tablistVerticalVariants: Record<GrTabsVariant, string> = {\n pills: '',\n line: 'border-b-0 border-r border-[var(--gr-brd)]',\n}\n\n/**\n * Переполнение прокручивается, а не переносится строкой: перенос ломает ряд и\n * уводит вкладки под панель. Полоса прокрутки скрыта — ряд ведёт клавиатура и\n * автоскролл к активной вкладке.\n */\nexport const tablistScrollClass = 'overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'\n\n/** Вертикальный ряд — колонка; прокрутки ему не нужно: он растёт вниз. */\nexport const tablistColumnClass = 'flex-col'\n\nexport const tabBase = 'shrink-0 font-600 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)]'\n\n// Цвет подчёркивания задаёт только состояние: `border-transparent` в базе имеет\n// ту же специфичность, что и цвет активной вкладки, и побеждал бы через раз —\n// по порядку правил в сгенерированном CSS, а не по порядку классов в атрибуте.\nexport const tabVariantBase: Record<GrTabsVariant, string> = {\n pills: 'rounded-[var(--gr-radius-md)]',\n line: 'rounded-[var(--gr-radius-md)] rounded-b-[var(--gr-radius-none)] border-b-2 -mb-px',\n}\n\nexport const tabStateClasses: Record<GrTabsVariant, { active: string, idle: string, disabled: string }> = {\n pills: {\n active: 'bg-[var(--gr-card)] text-[var(--gr-fg)] border border-[var(--gr-brd)]',\n idle: 'text-[var(--gr-muted-fg)] hover:text-[var(--gr-fg)] hover:bg-[color-mix(in_srgb,var(--gr-card)_70%,transparent)]',\n disabled: 'cursor-not-allowed text-[var(--gr-disabled-fg)]',\n },\n line: {\n active: 'border-[var(--gr-primary)] text-[var(--gr-fg)]',\n idle: 'border-transparent text-[var(--gr-muted-fg)] hover:text-[var(--gr-fg)] hover:border-[var(--gr-brd)]',\n disabled: 'border-transparent cursor-not-allowed text-[var(--gr-disabled-fg)]',\n },\n}\n\nexport const tabIconClass = 'h-4 w-4 shrink-0'\n\nexport const tabContentClass = 'inline-flex items-center gap-2'\n\n/**\n * Крестик закрытия. Кнопкой он быть не может: `role=\"tab\"` объявляет потомков\n * презентационными, и вложенный интерактив теряется у скринридера (axe:\n * `nested-interactive`). Поэтому это `<span>`, а клик по нему разбирает\n * обработчик самой вкладки.\n *\n * Своего фокус-кольца у него нет по той же причине — в таб-порядке он не стоит,\n * с клавиатуры закрытие идёт по `Delete`/`Backspace`.\n */\nexport const tabCloseBase = 'inline-flex items-center justify-center shrink-0 rounded-[var(--gr-radius-sm)] text-[var(--gr-muted-fg)] transition-colors hover:bg-[var(--gr-muted)] hover:text-[var(--gr-fg)]'\n\nexport const tabCloseSizes: Record<GrTabsSize, string> = {\n xs: 'h-4 w-4',\n sm: 'h-4 w-4',\n md: 'h-5 w-5',\n lg: 'h-5 w-5',\n}\n\nexport const tabCloseIconSizes: Record<GrTabsSize, string> = {\n xs: 'h-3 w-3',\n sm: 'h-3 w-3',\n md: 'h-4 w-4',\n lg: 'h-4 w-4',\n}\n\n/** Пустой ряд держит высоту вкладки: иначе соседние блоки прыгают при закрытии последней. */\nexport const tabsEmptyBase = 'inline-flex items-center text-[var(--gr-muted-fg)]'\n\nexport const tabBadgeBase = 'rounded-[var(--gr-radius-full)] bg-[var(--gr-secondary)] text-[var(--gr-secondary-fg)]'\n\nexport function grTabsListClass(options: {\n variant: GrTabsVariant\n size: GrTabsSize\n orientation: GrTabsOrientation\n}): string {\n const vertical = options.orientation === 'vertical'\n\n return [\n tablistBase,\n tablistVariants[options.variant],\n vertical ? tablistVerticalVariants[options.variant] : '',\n options.variant === 'pills' ? tablistSizes[options.size] : tablistLineSizes[options.size],\n vertical ? tablistColumnClass : tablistScrollClass,\n ].filter(Boolean).join(' ')\n}\n\nexport function grTabsTabClass(options: {\n variant: GrTabsVariant\n size: GrTabsSize\n active: boolean\n disabled: boolean\n}): string {\n const state = tabStateClasses[options.variant]\n\n return [\n tabBase,\n tabVariantBase[options.variant],\n tabSizes[options.size],\n options.disabled ? state.disabled : options.active ? state.active : state.idle,\n ].join(' ')\n}\n\nexport function grTabsBadgeClass(size: GrTabsSize): string {\n return [tabBadgeBase, tabBadgeSizes[size]].join(' ')\n}\n\nexport function grTabsCloseClass(size: GrTabsSize): string {\n return [tabCloseBase, tabCloseSizes[size]].join(' ')\n}\n\nexport function grTabsEmptyClass(size: GrTabsSize): string {\n return [tabsEmptyBase, tabSizes[size]].join(' ')\n}\n","import { splitClassTokens } from '../shared/classTokens'\nimport {\n tabBadgeBase,\n tabBadgeSizes,\n tabBase,\n tabCloseBase,\n tabCloseIconSizes,\n tabCloseSizes,\n tabContentClass,\n tabIconClass,\n tablistBase,\n tablistColumnClass,\n tablistLineSizes,\n tablistScrollClass,\n tablistSizes,\n tablistVariants,\n tablistVerticalVariants,\n tabsEmptyBase,\n tabSizes,\n tabStateClasses,\n tabVariantBase,\n} from './grTabsStyles'\n\nexport const grTabsSafelist = [...new Set([\n ...Object.values(tabSizes).flatMap(splitClassTokens),\n ...Object.values(tabBadgeSizes).flatMap(splitClassTokens),\n ...Object.values(tabCloseSizes).flatMap(splitClassTokens),\n ...Object.values(tabCloseIconSizes).flatMap(splitClassTokens),\n ...Object.values(tablistSizes).flatMap(splitClassTokens),\n ...Object.values(tablistLineSizes).flatMap(splitClassTokens),\n ...Object.values(tablistVariants).flatMap(splitClassTokens),\n ...Object.values(tablistVerticalVariants).flatMap(splitClassTokens),\n ...Object.values(tabVariantBase).flatMap(splitClassTokens),\n ...Object.values(tabStateClasses).flatMap(state => [\n ...splitClassTokens(state.active),\n ...splitClassTokens(state.idle),\n ...splitClassTokens(state.disabled),\n ]),\n ...splitClassTokens(tablistBase),\n ...splitClassTokens(tablistScrollClass),\n ...splitClassTokens(tablistColumnClass),\n ...splitClassTokens(tabBase),\n ...splitClassTokens(tabBadgeBase),\n ...splitClassTokens(tabContentClass),\n ...splitClassTokens(tabIconClass),\n ...splitClassTokens(tabCloseBase),\n ...splitClassTokens(tabsEmptyBase),\n])]\n","import { defineGranularComponent } from '@feugene/unocss-preset-granular/contract'\n\nimport { grTabsSafelist } from './safelist'\n\nexport const grTabsConfig = defineGranularComponent(import.meta.url, {\n name: 'GrTabs',\n safelist: grTabsSafelist,\n})\n"],"mappings":"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"}
@@ -68,7 +68,7 @@ import { t as grStepsConfig } from "./config-CLjjjPZj.js";
68
68
  import { t as grSwitchConfig } from "./config-Yy67uWHY.js";
69
69
  import { t as grTableConfig } from "./config-DCq4JySZ.js";
70
70
  import { t as grTabPanelsConfig } from "./config-eXB87DOi.js";
71
- import { t as grTabsConfig } from "./config-_QF0O1vq.js";
71
+ import { t as grTabsConfig } from "./config-CMNfZO7S.js";
72
72
  import { t as grTextareaConfig } from "./config-BhfNgyWL.js";
73
73
  import { t as grTimelineConfig } from "./config-Bd7QfiGj.js";
74
74
  import { t as grToasterConfig } from "./config-B5SBUa6u.js";
@@ -1 +1 @@
1
- {"version":3,"file":"granular-provider-BAy4APOX.js","names":[],"sources":["../../src/components/GrDialog/config.ts","../../src/components/GrResponseErrorBanner/config.ts","../../src/granular-provider/shared.ts","../../src/granular-provider/index.ts"],"sourcesContent":["import { defineGranularComponent } from '@feugene/unocss-preset-granular/contract'\n\nimport { grDialogSafelist } from './safelist'\n\nexport const grDialogConfig = defineGranularComponent(import.meta.url, {\n name: 'GrDialog',\n dependencies: ['GrButton', 'GrModal'],\n safelist: grDialogSafelist,\n})\n","import { defineGranularComponent } from '@feugene/unocss-preset-granular/contract'\n\nexport const grResponseErrorBannerConfig = defineGranularComponent(import.meta.url, {\n name: 'GrResponseErrorBanner',\n dependencies: ['GrAlert', 'GrButton'],\n})\n","// `id`, `theme.*`, `packageBaseUrl` и реестр компонентов провайдера.\n//\n// Все `new URL('../styles/...', import.meta.url)` обязаны лежать именно здесь:\n// `shared.ts` в одной директории с `index.ts` / `node.ts`, поэтому\n// относительные пути совпадают. Для браузера безопасно — бандлеры\n// транслируют такие литералы в статические asset-URL.\n//\n// `apps/showcase/scripts/generate-component-api.mjs` читает список компонентов\n// из `Object.keys(granularityComponentConfigs)` через vite SSR.\nimport {\n defineGranularProvider,\n type GranularComponentDescriptor,\n type GranularProvider,\n resolvePackageBaseUrl,\n} from '@feugene/unocss-preset-granular/contract'\n// <granularity:components:imports> — блок генерируется `yarn generate:registry`\nimport { grAlertConfig } from '../components/GrAlert/config'\nimport { grAutocompleteConfig } from '../components/GrAutocomplete/config'\nimport { grAvatarConfig } from '../components/GrAvatar/config'\nimport { grBadgeConfig } from '../components/GrBadge/config'\nimport { grBadgeWrapConfig } from '../components/GrBadgeWrap/config'\nimport { grBottomNavConfig } from '../components/GrBottomNav/config'\nimport { grBreadcrumbsConfig } from '../components/GrBreadcrumbs/config'\nimport { grButtonConfig } from '../components/GrButton/config'\nimport { grButtonGroupConfig } from '../components/GrButtonGroup/config'\nimport { grCardConfig } from '../components/GrCard/config'\nimport { grCheckboxConfig } from '../components/GrCheckbox/config'\nimport { grCheckboxGroupConfig } from '../components/GrCheckboxGroup/config'\nimport { grChipConfig } from '../components/GrChip/config'\nimport { grChipGroupConfig } from '../components/GrChipGroup/config'\nimport { grCodeBlockConfig } from '../components/GrCodeBlock/config'\nimport { grCollapseConfig } from '../components/GrCollapse/config'\nimport { grColorPickerConfig } from '../components/GrColorPicker/config'\nimport { grCommandPaletteConfig } from '../components/GrCommandPalette/config'\nimport { grConfigProviderConfig } from '../components/GrConfigProvider/config'\nimport { grConfirmDialogConfig } from '../components/GrConfirmDialog/config'\nimport { grContextMenuConfig } from '../components/GrContextMenu/config'\nimport { grDataTableConfig } from '../components/GrDataTable/config'\nimport { grDeltaConfig } from '../components/GrDelta/config'\nimport { grDescriptionListConfig } from '../components/GrDescriptionList/config'\nimport { grDialogConfig } from '../components/GrDialog/config'\nimport { grDialogServiceConfig } from '../components/GrDialogService/config'\nimport { grDividerConfig } from '../components/GrDivider/config'\nimport { grDrawerConfig } from '../components/GrDrawer/config'\nimport { grDropdownConfig } from '../components/GrDropdown/config'\nimport { grDropdownMenuConfig } from '../components/GrDropdownMenu/config'\nimport { grEmptyStateConfig } from '../components/GrEmptyState/config'\nimport { grFilePreviewConfig } from '../components/GrFilePreview/config'\nimport { grFileUploadConfig } from '../components/GrFileUpload/config'\nimport { grFormConfig } from '../components/GrForm/config'\nimport { grFormFieldConfig } from '../components/GrFormField/config'\nimport { grFormFileConfig } from '../components/GrFormFile/config'\nimport { grFormSectionConfig } from '../components/GrFormSection/config'\nimport { grIconConfig } from '../components/GrIcon/config'\nimport { grImageViewerConfig } from '../components/GrImageViewer/config'\nimport { grInputConfig } from '../components/GrInput/config'\nimport { grInputTagConfig } from '../components/GrInputTag/config'\nimport { grJsonViewerConfig } from '../components/GrJsonViewer/config'\nimport { grKbdConfig } from '../components/GrKbd/config'\nimport { grLinkConfig } from '../components/GrLink/config'\nimport { grListConfig } from '../components/GrList/config'\nimport { grLoadingConfig } from '../components/GrLoading/config'\nimport { grModalConfig } from '../components/GrModal/config'\nimport { grNavbarConfig } from '../components/GrNavbar/config'\nimport { grNumberInputConfig } from '../components/GrNumberInput/config'\nimport { grPaginationConfig } from '../components/GrPagination/config'\nimport { grPopoverConfig } from '../components/GrPopover/config'\nimport { grProgressBarConfig } from '../components/GrProgressBar/config'\nimport { grProgressCircleConfig } from '../components/GrProgressCircle/config'\nimport { grPromptDialogConfig } from '../components/GrPromptDialog/config'\nimport { grRadioConfig } from '../components/GrRadio/config'\nimport { grRadioGroupConfig } from '../components/GrRadioGroup/config'\nimport { grRatingConfig } from '../components/GrRating/config'\nimport { grResponseErrorBannerConfig } from '../components/GrResponseErrorBanner/config'\nimport { grSegmentedConfig } from '../components/GrSegmented/config'\nimport { grSelectConfig } from '../components/GrSelect/config'\nimport { grSidebarConfig } from '../components/GrSidebar/config'\nimport { grSkeletonConfig } from '../components/GrSkeleton/config'\nimport { grSliderConfig } from '../components/GrSlider/config'\nimport { grSortableListConfig } from '../components/GrSortableList/config'\nimport { grSplitterConfig } from '../components/GrSplitter/config'\nimport { grStatisticConfig } from '../components/GrStatistic/config'\nimport { grStepsConfig } from '../components/GrSteps/config'\nimport { grSwitchConfig } from '../components/GrSwitch/config'\nimport { grTableConfig } from '../components/GrTable/config'\nimport { grTabPanelsConfig } from '../components/GrTabPanels/config'\nimport { grTabsConfig } from '../components/GrTabs/config'\nimport { grTextareaConfig } from '../components/GrTextarea/config'\nimport { grTimelineConfig } from '../components/GrTimeline/config'\nimport { grToasterConfig } from '../components/GrToaster/config'\nimport { grTooltipConfig } from '../components/GrTooltip/config'\nimport { grTreeConfig } from '../components/GrTree/config'\nimport { grTreeSelectConfig } from '../components/GrTreeSelect/config'\nimport { grValueConfig } from '../components/GrValue/config'\n// </granularity:components:imports>\n\n/** Идентификатор провайдера — совпадает с именем пакета. */\nexport const GRANULARITY_PROVIDER_ID = '@feugene/granularity'\n\n// Не заменять на `new URL('..', import.meta.url)`: Vite и rolldown распознают\n// этот литерал и подставляют `data:`-URL, после чего scan-директории пустеют.\nconst packageBaseUrl = resolvePackageBaseUrl(import.meta.url)\n\n/** Встроенные темы пакета. Единственный источник правды о списке тем. */\nexport const granularityThemeNames = ['light', 'dark'] as const\nexport type GranularityThemeName = (typeof granularityThemeNames)[number]\n\n/** Темы, активные по умолчанию, если окружение не переопределяет выбор. */\nexport const granularityDefaultThemes: readonly GranularityThemeName[] = ['light']\n\nconst theme = {\n baseCssUrl: new URL('../styles/base.css', import.meta.url).href,\n tokensCssUrl: new URL('../styles/tokens.css', import.meta.url).href,\n themes: {\n light: new URL('../styles/themes/light.css', import.meta.url).href,\n dark: new URL('../styles/themes/dark.css', import.meta.url).href,\n },\n defaultThemes: granularityDefaultThemes,\n} as const\n\n/**\n * Реестр публичных компонентов. Ключи совпадают с subpath-экспортом\n * `@feugene/granularity/components/<Name>`.\n */\nexport const granularityComponentConfigs = {\n // <granularity:components:registry> — блок генерируется `yarn generate:registry`\n GrAlert: grAlertConfig,\n GrAutocomplete: grAutocompleteConfig,\n GrAvatar: grAvatarConfig,\n GrBadge: grBadgeConfig,\n GrBadgeWrap: grBadgeWrapConfig,\n GrBottomNav: grBottomNavConfig,\n GrBreadcrumbs: grBreadcrumbsConfig,\n GrButton: grButtonConfig,\n GrButtonGroup: grButtonGroupConfig,\n GrCard: grCardConfig,\n GrCheckbox: grCheckboxConfig,\n GrCheckboxGroup: grCheckboxGroupConfig,\n GrChip: grChipConfig,\n GrChipGroup: grChipGroupConfig,\n GrCodeBlock: grCodeBlockConfig,\n GrCollapse: grCollapseConfig,\n GrColorPicker: grColorPickerConfig,\n GrCommandPalette: grCommandPaletteConfig,\n GrConfigProvider: grConfigProviderConfig,\n GrConfirmDialog: grConfirmDialogConfig,\n GrContextMenu: grContextMenuConfig,\n GrDataTable: grDataTableConfig,\n GrDelta: grDeltaConfig,\n GrDescriptionList: grDescriptionListConfig,\n GrDialog: grDialogConfig,\n GrDialogService: grDialogServiceConfig,\n GrDivider: grDividerConfig,\n GrDrawer: grDrawerConfig,\n GrDropdown: grDropdownConfig,\n GrDropdownMenu: grDropdownMenuConfig,\n GrEmptyState: grEmptyStateConfig,\n GrFilePreview: grFilePreviewConfig,\n GrFileUpload: grFileUploadConfig,\n GrForm: grFormConfig,\n GrFormField: grFormFieldConfig,\n GrFormFile: grFormFileConfig,\n GrFormSection: grFormSectionConfig,\n GrIcon: grIconConfig,\n GrImageViewer: grImageViewerConfig,\n GrInput: grInputConfig,\n GrInputTag: grInputTagConfig,\n GrJsonViewer: grJsonViewerConfig,\n GrKbd: grKbdConfig,\n GrLink: grLinkConfig,\n GrList: grListConfig,\n GrLoading: grLoadingConfig,\n GrModal: grModalConfig,\n GrNavbar: grNavbarConfig,\n GrNumberInput: grNumberInputConfig,\n GrPagination: grPaginationConfig,\n GrPopover: grPopoverConfig,\n GrProgressBar: grProgressBarConfig,\n GrProgressCircle: grProgressCircleConfig,\n GrPromptDialog: grPromptDialogConfig,\n GrRadio: grRadioConfig,\n GrRadioGroup: grRadioGroupConfig,\n GrRating: grRatingConfig,\n GrResponseErrorBanner: grResponseErrorBannerConfig,\n GrSegmented: grSegmentedConfig,\n GrSelect: grSelectConfig,\n GrSidebar: grSidebarConfig,\n GrSkeleton: grSkeletonConfig,\n GrSlider: grSliderConfig,\n GrSortableList: grSortableListConfig,\n GrSplitter: grSplitterConfig,\n GrStatistic: grStatisticConfig,\n GrSteps: grStepsConfig,\n GrSwitch: grSwitchConfig,\n GrTable: grTableConfig,\n GrTabPanels: grTabPanelsConfig,\n GrTabs: grTabsConfig,\n GrTextarea: grTextareaConfig,\n GrTimeline: grTimelineConfig,\n GrToaster: grToasterConfig,\n GrTooltip: grTooltipConfig,\n GrTree: grTreeConfig,\n GrTreeSelect: grTreeSelectConfig,\n GrValue: grValueConfig,\n // </granularity:components:registry>\n}\n\nexport type GranularityComponentName = keyof typeof granularityComponentConfigs\n\n/** Базовый набор компонентов в порядке реестра. */\nconst baseComponents: readonly GranularComponentDescriptor[] = Object.values(\n granularityComponentConfigs,\n)\n\n/**\n * Собирает granular-provider пакета.\n *\n * `overrides` — точка расширения для потребителя: дескриптор с именем из\n * базового реестра заменяет его, остальные дописываются в конец.\n */\nexport function createGranularityProvider(\n overrides: readonly GranularComponentDescriptor[] = [],\n): GranularProvider {\n const overrideByName = new Map(\n overrides.map(component => [component.name, component]),\n )\n const components: GranularComponentDescriptor[] = [\n ...baseComponents.map(component => overrideByName.get(component.name) ?? component),\n ...overrides.filter(component => !baseComponents.some(base => base.name === component.name)),\n ]\n\n return defineGranularProvider({\n id: GRANULARITY_PROVIDER_ID,\n contractVersion: 1,\n packageBaseUrl,\n components,\n theme,\n })\n}\n","import { createGranularityProvider } from './shared'\n\nexport * from './shared'\n\nexport const granularityProvider = createGranularityProvider()\n\nexport default granularityProvider\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAIA,IAAa,iBAAiB,wBAAwB,OAAO,KAAK,KAAK;CACrE,MAAM;CACN,cAAc,CAAC,YAAY,SAAS;CACpC,UAAU;AACZ,CAAC;;;ACND,IAAa,8BAA8B,wBAAwB,OAAO,KAAK,KAAK;CAClF,MAAM;CACN,cAAc,CAAC,WAAW,UAAU;AACtC,CAAC;;;;AC4FD,IAAa,0BAA0B;AAIvC,IAAM,iBAAiB,sBAAsB,OAAO,KAAK,GAAG;;AAG5D,IAAa,wBAAwB,CAAC,SAAS,MAAM;;AAIrD,IAAa,2BAA4D,CAAC,OAAO;AAEjF,IAAM,QAAQ;CACZ,YAAY,IAAA,IAAA,q5KAAA,KAAA,OAAA,KAAA,GAAA,CAA8C,CAAC;CAC3D,cAAc,IAAA,IAAA,65YAAA,KAAA,OAAA,KAAA,GAAA,CAAgD,CAAC;CAC/D,QAAQ;EACN,OAAO,IAAA,IAAA,iyYAAA,KAAA,OAAA,KAAA,GAAA,CAAsD,CAAC;EAC9D,MAAM,IAAA,IAAA,y/SAAA,KAAA,OAAA,KAAA,GAAA,CAAqD,CAAC;CAC9D;CACA,eAAe;AACjB;;;;;AAMA,IAAa,8BAA8B;CAEzC,SAAS;CACT,gBAAgB;CAChB,UAAU;CACV,SAAS;CACT,aAAa;CACb,aAAa;CACb,eAAe;CACf,UAAU;CACV,eAAe;CACf,QAAQ;CACR,YAAY;CACZ,iBAAiB;CACjB,QAAQ;CACR,aAAa;CACb,aAAa;CACb,YAAY;CACZ,eAAe;CACf,kBAAkB;CAClB,kBAAkB;CAClB,iBAAiB;CACjB,eAAe;CACf,aAAa;CACb,SAAS;CACT,mBAAmB;CACnB,UAAU;CACV,iBAAiB;CACjB,WAAW;CACX,UAAU;CACV,YAAY;CACZ,gBAAgB;CAChB,cAAc;CACd,eAAe;CACf,cAAc;CACd,QAAQ;CACR,aAAa;CACb,YAAY;CACZ,eAAe;CACf,QAAQ;CACR,eAAe;CACf,SAAS;CACT,YAAY;CACZ,cAAc;CACd,OAAO;CACP,QAAQ;CACR,QAAQ;CACR,WAAW;CACX,SAAS;CACT,UAAU;CACV,eAAe;CACf,cAAc;CACd,WAAW;CACX,eAAe;CACf,kBAAkB;CAClB,gBAAgB;CAChB,SAAS;CACT,cAAc;CACd,UAAU;CACV,uBAAuB;CACvB,aAAa;CACb,UAAU;CACV,WAAW;CACX,YAAY;CACZ,UAAU;CACV,gBAAgB;CAChB,YAAY;CACZ,aAAa;CACb,SAAS;CACT,UAAU;CACV,SAAS;CACT,aAAa;CACb,QAAQ;CACR,YAAY;CACZ,YAAY;CACZ,WAAW;CACX,WAAW;CACX,QAAQ;CACR,cAAc;CACd,SAAS;AAEX;;AAKA,IAAM,iBAAyD,OAAO,OACpE,2BACF;;;;;;;AAQA,SAAgB,0BACd,YAAoD,CAAC,GACnC;CAClB,MAAM,iBAAiB,IAAI,IACzB,UAAU,KAAI,cAAa,CAAC,UAAU,MAAM,SAAS,CAAC,CACxD;CAMA,OAAO,uBAAuB;EAC5B,IAAI;EACJ,iBAAiB;EACjB;EACA,aARA,GAAG,eAAe,KAAI,cAAa,eAAe,IAAI,UAAU,IAAI,KAAK,SAAS,GAClF,GAAG,UAAU,QAAO,cAAa,CAAC,eAAe,MAAK,SAAQ,KAAK,SAAS,UAAU,IAAI,CAAC,CAO3F;EACA;CACF,CAAC;AACH;;;AC1OA,IAAa,sBAAsB,0BAA0B"}
1
+ {"version":3,"file":"granular-provider-DCn6fgEs.js","names":[],"sources":["../../src/components/GrDialog/config.ts","../../src/components/GrResponseErrorBanner/config.ts","../../src/granular-provider/shared.ts","../../src/granular-provider/index.ts"],"sourcesContent":["import { defineGranularComponent } from '@feugene/unocss-preset-granular/contract'\n\nimport { grDialogSafelist } from './safelist'\n\nexport const grDialogConfig = defineGranularComponent(import.meta.url, {\n name: 'GrDialog',\n dependencies: ['GrButton', 'GrModal'],\n safelist: grDialogSafelist,\n})\n","import { defineGranularComponent } from '@feugene/unocss-preset-granular/contract'\n\nexport const grResponseErrorBannerConfig = defineGranularComponent(import.meta.url, {\n name: 'GrResponseErrorBanner',\n dependencies: ['GrAlert', 'GrButton'],\n})\n","// `id`, `theme.*`, `packageBaseUrl` и реестр компонентов провайдера.\n//\n// Все `new URL('../styles/...', import.meta.url)` обязаны лежать именно здесь:\n// `shared.ts` в одной директории с `index.ts` / `node.ts`, поэтому\n// относительные пути совпадают. Для браузера безопасно — бандлеры\n// транслируют такие литералы в статические asset-URL.\n//\n// `apps/showcase/scripts/generate-component-api.mjs` читает список компонентов\n// из `Object.keys(granularityComponentConfigs)` через vite SSR.\nimport {\n defineGranularProvider,\n type GranularComponentDescriptor,\n type GranularProvider,\n resolvePackageBaseUrl,\n} from '@feugene/unocss-preset-granular/contract'\n// <granularity:components:imports> — блок генерируется `yarn generate:registry`\nimport { grAlertConfig } from '../components/GrAlert/config'\nimport { grAutocompleteConfig } from '../components/GrAutocomplete/config'\nimport { grAvatarConfig } from '../components/GrAvatar/config'\nimport { grBadgeConfig } from '../components/GrBadge/config'\nimport { grBadgeWrapConfig } from '../components/GrBadgeWrap/config'\nimport { grBottomNavConfig } from '../components/GrBottomNav/config'\nimport { grBreadcrumbsConfig } from '../components/GrBreadcrumbs/config'\nimport { grButtonConfig } from '../components/GrButton/config'\nimport { grButtonGroupConfig } from '../components/GrButtonGroup/config'\nimport { grCardConfig } from '../components/GrCard/config'\nimport { grCheckboxConfig } from '../components/GrCheckbox/config'\nimport { grCheckboxGroupConfig } from '../components/GrCheckboxGroup/config'\nimport { grChipConfig } from '../components/GrChip/config'\nimport { grChipGroupConfig } from '../components/GrChipGroup/config'\nimport { grCodeBlockConfig } from '../components/GrCodeBlock/config'\nimport { grCollapseConfig } from '../components/GrCollapse/config'\nimport { grColorPickerConfig } from '../components/GrColorPicker/config'\nimport { grCommandPaletteConfig } from '../components/GrCommandPalette/config'\nimport { grConfigProviderConfig } from '../components/GrConfigProvider/config'\nimport { grConfirmDialogConfig } from '../components/GrConfirmDialog/config'\nimport { grContextMenuConfig } from '../components/GrContextMenu/config'\nimport { grDataTableConfig } from '../components/GrDataTable/config'\nimport { grDeltaConfig } from '../components/GrDelta/config'\nimport { grDescriptionListConfig } from '../components/GrDescriptionList/config'\nimport { grDialogConfig } from '../components/GrDialog/config'\nimport { grDialogServiceConfig } from '../components/GrDialogService/config'\nimport { grDividerConfig } from '../components/GrDivider/config'\nimport { grDrawerConfig } from '../components/GrDrawer/config'\nimport { grDropdownConfig } from '../components/GrDropdown/config'\nimport { grDropdownMenuConfig } from '../components/GrDropdownMenu/config'\nimport { grEmptyStateConfig } from '../components/GrEmptyState/config'\nimport { grFilePreviewConfig } from '../components/GrFilePreview/config'\nimport { grFileUploadConfig } from '../components/GrFileUpload/config'\nimport { grFormConfig } from '../components/GrForm/config'\nimport { grFormFieldConfig } from '../components/GrFormField/config'\nimport { grFormFileConfig } from '../components/GrFormFile/config'\nimport { grFormSectionConfig } from '../components/GrFormSection/config'\nimport { grIconConfig } from '../components/GrIcon/config'\nimport { grImageViewerConfig } from '../components/GrImageViewer/config'\nimport { grInputConfig } from '../components/GrInput/config'\nimport { grInputTagConfig } from '../components/GrInputTag/config'\nimport { grJsonViewerConfig } from '../components/GrJsonViewer/config'\nimport { grKbdConfig } from '../components/GrKbd/config'\nimport { grLinkConfig } from '../components/GrLink/config'\nimport { grListConfig } from '../components/GrList/config'\nimport { grLoadingConfig } from '../components/GrLoading/config'\nimport { grModalConfig } from '../components/GrModal/config'\nimport { grNavbarConfig } from '../components/GrNavbar/config'\nimport { grNumberInputConfig } from '../components/GrNumberInput/config'\nimport { grPaginationConfig } from '../components/GrPagination/config'\nimport { grPopoverConfig } from '../components/GrPopover/config'\nimport { grProgressBarConfig } from '../components/GrProgressBar/config'\nimport { grProgressCircleConfig } from '../components/GrProgressCircle/config'\nimport { grPromptDialogConfig } from '../components/GrPromptDialog/config'\nimport { grRadioConfig } from '../components/GrRadio/config'\nimport { grRadioGroupConfig } from '../components/GrRadioGroup/config'\nimport { grRatingConfig } from '../components/GrRating/config'\nimport { grResponseErrorBannerConfig } from '../components/GrResponseErrorBanner/config'\nimport { grSegmentedConfig } from '../components/GrSegmented/config'\nimport { grSelectConfig } from '../components/GrSelect/config'\nimport { grSidebarConfig } from '../components/GrSidebar/config'\nimport { grSkeletonConfig } from '../components/GrSkeleton/config'\nimport { grSliderConfig } from '../components/GrSlider/config'\nimport { grSortableListConfig } from '../components/GrSortableList/config'\nimport { grSplitterConfig } from '../components/GrSplitter/config'\nimport { grStatisticConfig } from '../components/GrStatistic/config'\nimport { grStepsConfig } from '../components/GrSteps/config'\nimport { grSwitchConfig } from '../components/GrSwitch/config'\nimport { grTableConfig } from '../components/GrTable/config'\nimport { grTabPanelsConfig } from '../components/GrTabPanels/config'\nimport { grTabsConfig } from '../components/GrTabs/config'\nimport { grTextareaConfig } from '../components/GrTextarea/config'\nimport { grTimelineConfig } from '../components/GrTimeline/config'\nimport { grToasterConfig } from '../components/GrToaster/config'\nimport { grTooltipConfig } from '../components/GrTooltip/config'\nimport { grTreeConfig } from '../components/GrTree/config'\nimport { grTreeSelectConfig } from '../components/GrTreeSelect/config'\nimport { grValueConfig } from '../components/GrValue/config'\n// </granularity:components:imports>\n\n/** Идентификатор провайдера — совпадает с именем пакета. */\nexport const GRANULARITY_PROVIDER_ID = '@feugene/granularity'\n\n// Не заменять на `new URL('..', import.meta.url)`: Vite и rolldown распознают\n// этот литерал и подставляют `data:`-URL, после чего scan-директории пустеют.\nconst packageBaseUrl = resolvePackageBaseUrl(import.meta.url)\n\n/** Встроенные темы пакета. Единственный источник правды о списке тем. */\nexport const granularityThemeNames = ['light', 'dark'] as const\nexport type GranularityThemeName = (typeof granularityThemeNames)[number]\n\n/** Темы, активные по умолчанию, если окружение не переопределяет выбор. */\nexport const granularityDefaultThemes: readonly GranularityThemeName[] = ['light']\n\nconst theme = {\n baseCssUrl: new URL('../styles/base.css', import.meta.url).href,\n tokensCssUrl: new URL('../styles/tokens.css', import.meta.url).href,\n themes: {\n light: new URL('../styles/themes/light.css', import.meta.url).href,\n dark: new URL('../styles/themes/dark.css', import.meta.url).href,\n },\n defaultThemes: granularityDefaultThemes,\n} as const\n\n/**\n * Реестр публичных компонентов. Ключи совпадают с subpath-экспортом\n * `@feugene/granularity/components/<Name>`.\n */\nexport const granularityComponentConfigs = {\n // <granularity:components:registry> — блок генерируется `yarn generate:registry`\n GrAlert: grAlertConfig,\n GrAutocomplete: grAutocompleteConfig,\n GrAvatar: grAvatarConfig,\n GrBadge: grBadgeConfig,\n GrBadgeWrap: grBadgeWrapConfig,\n GrBottomNav: grBottomNavConfig,\n GrBreadcrumbs: grBreadcrumbsConfig,\n GrButton: grButtonConfig,\n GrButtonGroup: grButtonGroupConfig,\n GrCard: grCardConfig,\n GrCheckbox: grCheckboxConfig,\n GrCheckboxGroup: grCheckboxGroupConfig,\n GrChip: grChipConfig,\n GrChipGroup: grChipGroupConfig,\n GrCodeBlock: grCodeBlockConfig,\n GrCollapse: grCollapseConfig,\n GrColorPicker: grColorPickerConfig,\n GrCommandPalette: grCommandPaletteConfig,\n GrConfigProvider: grConfigProviderConfig,\n GrConfirmDialog: grConfirmDialogConfig,\n GrContextMenu: grContextMenuConfig,\n GrDataTable: grDataTableConfig,\n GrDelta: grDeltaConfig,\n GrDescriptionList: grDescriptionListConfig,\n GrDialog: grDialogConfig,\n GrDialogService: grDialogServiceConfig,\n GrDivider: grDividerConfig,\n GrDrawer: grDrawerConfig,\n GrDropdown: grDropdownConfig,\n GrDropdownMenu: grDropdownMenuConfig,\n GrEmptyState: grEmptyStateConfig,\n GrFilePreview: grFilePreviewConfig,\n GrFileUpload: grFileUploadConfig,\n GrForm: grFormConfig,\n GrFormField: grFormFieldConfig,\n GrFormFile: grFormFileConfig,\n GrFormSection: grFormSectionConfig,\n GrIcon: grIconConfig,\n GrImageViewer: grImageViewerConfig,\n GrInput: grInputConfig,\n GrInputTag: grInputTagConfig,\n GrJsonViewer: grJsonViewerConfig,\n GrKbd: grKbdConfig,\n GrLink: grLinkConfig,\n GrList: grListConfig,\n GrLoading: grLoadingConfig,\n GrModal: grModalConfig,\n GrNavbar: grNavbarConfig,\n GrNumberInput: grNumberInputConfig,\n GrPagination: grPaginationConfig,\n GrPopover: grPopoverConfig,\n GrProgressBar: grProgressBarConfig,\n GrProgressCircle: grProgressCircleConfig,\n GrPromptDialog: grPromptDialogConfig,\n GrRadio: grRadioConfig,\n GrRadioGroup: grRadioGroupConfig,\n GrRating: grRatingConfig,\n GrResponseErrorBanner: grResponseErrorBannerConfig,\n GrSegmented: grSegmentedConfig,\n GrSelect: grSelectConfig,\n GrSidebar: grSidebarConfig,\n GrSkeleton: grSkeletonConfig,\n GrSlider: grSliderConfig,\n GrSortableList: grSortableListConfig,\n GrSplitter: grSplitterConfig,\n GrStatistic: grStatisticConfig,\n GrSteps: grStepsConfig,\n GrSwitch: grSwitchConfig,\n GrTable: grTableConfig,\n GrTabPanels: grTabPanelsConfig,\n GrTabs: grTabsConfig,\n GrTextarea: grTextareaConfig,\n GrTimeline: grTimelineConfig,\n GrToaster: grToasterConfig,\n GrTooltip: grTooltipConfig,\n GrTree: grTreeConfig,\n GrTreeSelect: grTreeSelectConfig,\n GrValue: grValueConfig,\n // </granularity:components:registry>\n}\n\nexport type GranularityComponentName = keyof typeof granularityComponentConfigs\n\n/** Базовый набор компонентов в порядке реестра. */\nconst baseComponents: readonly GranularComponentDescriptor[] = Object.values(\n granularityComponentConfigs,\n)\n\n/**\n * Собирает granular-provider пакета.\n *\n * `overrides` — точка расширения для потребителя: дескриптор с именем из\n * базового реестра заменяет его, остальные дописываются в конец.\n */\nexport function createGranularityProvider(\n overrides: readonly GranularComponentDescriptor[] = [],\n): GranularProvider {\n const overrideByName = new Map(\n overrides.map(component => [component.name, component]),\n )\n const components: GranularComponentDescriptor[] = [\n ...baseComponents.map(component => overrideByName.get(component.name) ?? component),\n ...overrides.filter(component => !baseComponents.some(base => base.name === component.name)),\n ]\n\n return defineGranularProvider({\n id: GRANULARITY_PROVIDER_ID,\n contractVersion: 1,\n packageBaseUrl,\n components,\n theme,\n })\n}\n","import { createGranularityProvider } from './shared'\n\nexport * from './shared'\n\nexport const granularityProvider = createGranularityProvider()\n\nexport default granularityProvider\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAIA,IAAa,iBAAiB,wBAAwB,OAAO,KAAK,KAAK;CACrE,MAAM;CACN,cAAc,CAAC,YAAY,SAAS;CACpC,UAAU;AACZ,CAAC;;;ACND,IAAa,8BAA8B,wBAAwB,OAAO,KAAK,KAAK;CAClF,MAAM;CACN,cAAc,CAAC,WAAW,UAAU;AACtC,CAAC;;;;AC4FD,IAAa,0BAA0B;AAIvC,IAAM,iBAAiB,sBAAsB,OAAO,KAAK,GAAG;;AAG5D,IAAa,wBAAwB,CAAC,SAAS,MAAM;;AAIrD,IAAa,2BAA4D,CAAC,OAAO;AAEjF,IAAM,QAAQ;CACZ,YAAY,IAAA,IAAA,q5KAAA,KAAA,OAAA,KAAA,GAAA,CAA8C,CAAC;CAC3D,cAAc,IAAA,IAAA,65YAAA,KAAA,OAAA,KAAA,GAAA,CAAgD,CAAC;CAC/D,QAAQ;EACN,OAAO,IAAA,IAAA,iyYAAA,KAAA,OAAA,KAAA,GAAA,CAAsD,CAAC;EAC9D,MAAM,IAAA,IAAA,y/SAAA,KAAA,OAAA,KAAA,GAAA,CAAqD,CAAC;CAC9D;CACA,eAAe;AACjB;;;;;AAMA,IAAa,8BAA8B;CAEzC,SAAS;CACT,gBAAgB;CAChB,UAAU;CACV,SAAS;CACT,aAAa;CACb,aAAa;CACb,eAAe;CACf,UAAU;CACV,eAAe;CACf,QAAQ;CACR,YAAY;CACZ,iBAAiB;CACjB,QAAQ;CACR,aAAa;CACb,aAAa;CACb,YAAY;CACZ,eAAe;CACf,kBAAkB;CAClB,kBAAkB;CAClB,iBAAiB;CACjB,eAAe;CACf,aAAa;CACb,SAAS;CACT,mBAAmB;CACnB,UAAU;CACV,iBAAiB;CACjB,WAAW;CACX,UAAU;CACV,YAAY;CACZ,gBAAgB;CAChB,cAAc;CACd,eAAe;CACf,cAAc;CACd,QAAQ;CACR,aAAa;CACb,YAAY;CACZ,eAAe;CACf,QAAQ;CACR,eAAe;CACf,SAAS;CACT,YAAY;CACZ,cAAc;CACd,OAAO;CACP,QAAQ;CACR,QAAQ;CACR,WAAW;CACX,SAAS;CACT,UAAU;CACV,eAAe;CACf,cAAc;CACd,WAAW;CACX,eAAe;CACf,kBAAkB;CAClB,gBAAgB;CAChB,SAAS;CACT,cAAc;CACd,UAAU;CACV,uBAAuB;CACvB,aAAa;CACb,UAAU;CACV,WAAW;CACX,YAAY;CACZ,UAAU;CACV,gBAAgB;CAChB,YAAY;CACZ,aAAa;CACb,SAAS;CACT,UAAU;CACV,SAAS;CACT,aAAa;CACb,QAAQ;CACR,YAAY;CACZ,YAAY;CACZ,WAAW;CACX,WAAW;CACX,QAAQ;CACR,cAAc;CACd,SAAS;AAEX;;AAKA,IAAM,iBAAyD,OAAO,OACpE,2BACF;;;;;;;AAQA,SAAgB,0BACd,YAAoD,CAAC,GACnC;CAClB,MAAM,iBAAiB,IAAI,IACzB,UAAU,KAAI,cAAa,CAAC,UAAU,MAAM,SAAS,CAAC,CACxD;CAMA,OAAO,uBAAuB;EAC5B,IAAI;EACJ,iBAAiB;EACjB;EACA,aARA,GAAG,eAAe,KAAI,cAAa,eAAe,IAAI,UAAU,IAAI,KAAK,SAAS,GAClF,GAAG,UAAU,QAAO,cAAa,CAAC,eAAe,MAAK,SAAQ,KAAK,SAAS,UAAU,IAAI,CAAC,CAO3F;EACA;CACF,CAAC;AACH;;;AC1OA,IAAa,sBAAsB,0BAA0B"}
@@ -1,3 +1,4 @@
1
+ import { a as useGrComponentSize } from "../../../chunks/context-CTbuwcYs.js";
1
2
  import { n as useGranularityTranslations } from "../../../chunks/granularityI18n-BC5zsj_d.js";
2
3
  import { t as GrIcon_default } from "../../GrIcon/chunks/GrIcon-Dmf0c9ba.js";
3
4
  import { t as GrButton_default } from "../../GrButton/chunks/GrButton-7GP0zObj.js";
@@ -36,6 +37,7 @@ var GrNavbar_default = /* @__PURE__ */ defineComponent({
36
37
  type: Boolean,
37
38
  default: false
38
39
  },
40
+ size: { default: void 0 },
39
41
  menuButtonClass: { default: "" },
40
42
  sticky: {
41
43
  type: Boolean,
@@ -49,6 +51,10 @@ var GrNavbar_default = /* @__PURE__ */ defineComponent({
49
51
  const emit = __emit;
50
52
  const { t } = useGranularityTranslations();
51
53
  const menuAriaLabel = computed(() => t("gr.navbar.openMenu", "Open menu"));
54
+ const menuSize = useGrComponentSize(() => props.size, {
55
+ component: "GrNavbar",
56
+ fallback: "sm"
57
+ });
52
58
  const hasTitle = computed(() => Boolean(props.title || slots.title));
53
59
  const rootClass = computed(() => grNavbarRootClass(props.sticky));
54
60
  const hasCenter = computed(() => Boolean(slots.center));
@@ -68,18 +74,22 @@ var GrNavbar_default = /* @__PURE__ */ defineComponent({
68
74
  key: 0,
69
75
  "data-gr-navbar-menu": "",
70
76
  variant: "ghost",
71
- size: "sm",
77
+ size: unref(menuSize),
72
78
  square: "",
73
79
  "aria-label": menuAriaLabel.value,
74
80
  class: normalizeClass(__props.menuButtonClass),
75
81
  onClick: _cache[0] || (_cache[0] = ($event) => emit("menu"))
76
82
  }, {
77
- default: withCtx(() => [createVNode(GrIcon_default, { size: 16 }, {
83
+ default: withCtx(() => [createVNode(GrIcon_default, { size: unref(menuSize) }, {
78
84
  default: withCtx(() => [createVNode(unref(menu_default), { "aria-hidden": "true" })]),
79
85
  _: 1
80
- })]),
86
+ }, 8, ["size"])]),
81
87
  _: 1
82
- }, 8, ["aria-label", "class"])) : createCommentVNode("", true),
88
+ }, 8, [
89
+ "size",
90
+ "aria-label",
91
+ "class"
92
+ ])) : createCommentVNode("", true),
83
93
  hasTitle.value ? (openBlock(), createElementBlock("div", {
84
94
  key: 1,
85
95
  "data-gr-navbar-title": "",
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GrNavbar-Dr8e5kW-.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'\nimport { type GrComponentSize, useGrComponentSize } from '../GrConfigProvider/context'\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 /**\n * Ступень кнопки меню — и только её: остальное в шапке принадлежит\n * потребителю, а высоту ряда задаёт `--gr-navbar-height`.\n *\n * Фолбэк — `sm` (32px), а не общий для пакета `md`: кнопка была этого размера\n * с самого начала, и подъём по умолчанию сдвинул бы шапку у всех. Нужен\n * тач-таргет — `size=\"lg\"` даёт 44px, ровно то, чего требуют WCAG 2.5.5 и\n * Apple HIG; в 56px ряда он помещается.\n */\n size?: GrComponentSize\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 size: undefined,\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\nconst menuSize = useGrComponentSize(() => props.size, { component: 'GrNavbar', fallback: 'sm' })\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=\"menuSize\"\n square\n :aria-label=\"menuAriaLabel\"\n :class=\"menuButtonClass\"\n @click=\"emit('menu')\"\n >\n <GrIcon :size=\"menuSize\">\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'\nimport { type GrComponentSize, useGrComponentSize } from '../GrConfigProvider/context'\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 /**\n * Ступень кнопки меню — и только её: остальное в шапке принадлежит\n * потребителю, а высоту ряда задаёт `--gr-navbar-height`.\n *\n * Фолбэк — `sm` (32px), а не общий для пакета `md`: кнопка была этого размера\n * с самого начала, и подъём по умолчанию сдвинул бы шапку у всех. Нужен\n * тач-таргет — `size=\"lg\"` даёт 44px, ровно то, чего требуют WCAG 2.5.5 и\n * Apple HIG; в 56px ряда он помещается.\n */\n size?: GrComponentSize\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 size: undefined,\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\nconst menuSize = useGrComponentSize(() => props.size, { component: 'GrNavbar', fallback: 'sm' })\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=\"menuSize\"\n square\n :aria-label=\"menuAriaLabel\"\n :class=\"menuButtonClass\"\n @click=\"emit('menu')\"\n >\n <GrIcon :size=\"menuSize\">\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsDA,MAAM,QAAQ;EAQd,MAAM,QAAQ,SAAA;EAWd,MAAM,OAAO;EAEb,MAAM,EAAE,MAAM,2BAA2B;EAEzC,MAAM,gBAAgB,eAAe,EAAE,sBAAsB,WAAW,CAAC;EAEzE,MAAM,WAAW,yBAAyB,MAAM,MAAM;GAAE,WAAW;GAAY,UAAU;EAAK,CAAC;EAI/F,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;MACP,MAAM,MAAA,QAAA;MACP,QAAA;MACC,cAAY,cAAA;MACZ,OAAK,eAAE,QAAA,eAAe;MACtB,SAAK,OAAA,OAAA,OAAA,MAAA,WAAE,KAAI,MAAA;;6BAIH,CAFT,YAES,gBAAA,EAFA,MAAM,MAAA,QAAA,EAAQ,GAAA;8BACU,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-_rtHKwTf.js";
1
+ import { t as GrNavbar_default } from "./chunks/GrNavbar-Dr8e5kW-.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 };
@@ -3,7 +3,7 @@ import { a as useGrComponentSize, i as useGrComponentProp } from "../../../chunk
3
3
  import { n as useGranularityTranslations } from "../../../chunks/granularityI18n-BC5zsj_d.js";
4
4
  import { useRovingFocus } from "../../../composables/useRovingFocus.js";
5
5
  import { n as iconTag, t as iconClass } from "../../../chunks/icon-Dag2tIMs.js";
6
- import { a as grTabsEmptyClass, c as tabCloseIconSizes, i as grTabsCloseClass, l as tabContentClass, o as grTabsListClass, r as grTabsBadgeClass, s as grTabsTabClass, u as tabIconClass } from "../../../chunks/config-_QF0O1vq.js";
6
+ import { a as grTabsEmptyClass, c as tabCloseIconSizes, i as grTabsCloseClass, l as tabContentClass, o as grTabsListClass, r as grTabsBadgeClass, s as grTabsTabClass, u as tabIconClass } from "../../../chunks/config-CMNfZO7S.js";
7
7
  import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, nextTick, normalizeClass, onBeforeUnmount, onMounted, openBlock, ref, renderList, renderSlot, resolveDynamicComponent, toDisplayString, unref, watch } from "vue";
8
8
  import '../styles.css';//#region src/components/shared/scrollOverflow.ts
9
9
  /**
@@ -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":";;;;;;;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,sBACd,aACA,YACA,YACkB;CAClB,MAAM,SAAS,aAAa;CAE5B,IAAI,UAAU,GACZ,OAAO;CAET,MAAM,QAAQ,KAAK,IAAI,WAAW;CAClC,MAAM,UAAU,SAAS;CACzB,MAAM,QAAQ,SAAS,SAAS;CAEhC,IAAI,WAAW,OACb,OAAO;CAET,IAAI,SACF,OAAO;CAET,IAAI,OACF,OAAO;CAET,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ECgDA,MAAM,QAAQ;EAmBd,MAAM,OAAO;EAEb,MAAM,EAAE,MAAM,2BAA2B;EAEzC,MAAM,eAAe,yBAAyB,MAAM,MAAM,EAAE,WAAW,SAAS,CAAC;EACjF,MAAM,kBAAkB,mBAAmB,UAAU,iBAAiB,MAAM,SAAS,OAAO;EAE5F,MAAM,eAAe,eAAe,gBAAgB;GAClD,SAAS,gBAAgB;GACzB,MAAM,aAAa;GACnB,aAAa,MAAM;EACrB,CAAC,CAAC;EAEF,SAAS,SAAS,KAAoB;GACpC,OAAO,eAAe;IACpB,SAAS,gBAAgB;IACzB,MAAM,aAAa;IACnB,QAAQ,IAAI,UAAU,MAAM;IAC5B,UAAU,QAAQ,IAAI,QAAQ;GAChC,CAAC;EACH;EAEA,MAAM,aAAa,eAAe,iBAAiB,aAAa,KAAK,CAAC;EACtE,MAAM,aAAa,eAAe,iBAAiB,aAAa,KAAK,CAAC;EACtE,MAAM,iBAAiB,eAAe,kBAAkB,aAAa,MAAM;EAC3E,MAAM,aAAa,eAAe,iBAAiB,aAAa,KAAK,CAAC;;;;;;EAOtE,MAAM,UAAU,eAAe,MAAM,KAAK,WAAW,CAAC;EACtD,MAAM,oBAAoB,eAAe,MAAM,aAAa,EAAE,iBAAiB,kBAAkB,CAAC;EAClG,MAAM,aAAa,eAAe,EAAE,iBAAiB,WAAW,CAAC;;EAGjE,SAAS,WAAW,KAAqB;GACvC,OAAO,CAAC,IAAI,aAAa,IAAI,YAAY,MAAM;EACjD;EAEA,MAAM,aAAa,IAAyB,CAAC,CAAC;;;;;;EAO9C,SAAS,aAAa,IAAa,OAAqB;GACtD,IAAI,cAAc,mBAChB,WAAW,MAAM,SAAS;QAE1B,OAAO,WAAW,MAAM;GAE1B,IAAI,WAAW,MAAM,SAAS,MAAM,KAAK,QACvC,WAAW,MAAM,SAAS,MAAM,KAAK;EACzC;EAEA,MAAM,cAAc,eAAe,MAAM,KAAK,WAAU,MAAK,EAAE,UAAU,MAAM,UAAU,CAAC;EAE1F,SAAS,UAAU,KAAqB;GACtC,OAAO,CAAC,IAAI;EACd;EAEA,MAAM,aAAa,eAAe,MAAM,KAAK,KAAK,GAAG,UAAU,KAAK,CAAC;;;;;;;;;;;EAYrE,MAAM,SAAS,eAAuB;GACpC,aAAa,WAAW;GACxB,aAAY,UAAS,WAAW,MAAM;GACtC,aAAY,UAAS,QAAQ,MAAM,KAAK,MAAM,EAAE,QAAQ;GACxD,mBAAoB,MAAM,gBAAgB,aAAa,aAAa;GACpE,oBAAoB;;;;;;;;;;;GAWpB,kBAAmB,YAAY,SAAS,IAAI,YAAY,QAAQ,KAAA;;;;;;GAMhE,cAAa,UAAS,eAAe,KAAK;EAC5C,CAAC;;;;;;EAOD,eAAe,WAAW,OAA8B;GACtD,MAAM,SAAS;GACf,MAAM,OAAO,SAAS,KAAK;EAC7B;;;;;;EAOA,SAAS,eAAe,OAAqB;GAC3C,WAAW,MAAM,MAAM,EAAE,iBAAiB;IAAE,OAAO;IAAW,QAAQ;GAAU,CAAC;EACnF;EAEA,YAAY,MAAM,YAAY,YAAY;GACxC,MAAM,SAAS;GACf,IAAI,YAAY,SAAS,GACvB,eAAe,YAAY,KAAK;EACpC,CAAC;;;;;;;;;;;EAYD,MAAM,YAAY,IAAwB,IAAI;EAC9C,MAAM,iBAAiB,IAAsB,MAAM;EAEnD,IAAI,iBAAwC;EAC5C,IAAI,YAAY;EAEhB,SAAS,kBAAwB;GAC/B,MAAM,KAAK,UAAU;GAErB,IAAI,CAAC,MAAM,MAAM,gBAAgB,YAAY;IAC3C,eAAe,QAAQ;IACvB;GACF;GAEA,eAAe,QAAQ,sBAAsB,GAAG,YAAY,GAAG,aAAa,GAAG,WAAW;EAC5F;EAEA,SAAS,kBAAwB;GAC/B,IAAI,WACF;GAEF,YAAY;GACZ,eAAoB;IAClB,YAAY;IACZ,gBAAgB;GAClB,CAAC;EACH;EAEA,gBAAgB;GACd,gBAAgB;GAGhB,IAAI,OAAO,mBAAmB,aAC5B;GAEF,iBAAiB,IAAI,eAAe,eAAe;GACnD,IAAI,UAAU,OACZ,eAAe,QAAQ,UAAU,KAAK;EAC1C,CAAC;EAED,sBAAsB;GACpB,gBAAgB,WAAW;GAC3B,iBAAiB;EACnB,CAAC;EAGD,YAAY,CAAC,MAAM,KAAK,QAAQ,MAAM,WAAW,GAAG,eAAe;EAEnE,eAAe,cAAc,OAAe,QAAQ,OAAsB;GACxE,MAAM,MAAM,MAAM,KAAK;GACvB,IAAI,CAAC,OAAO,CAAC,UAAU,GAAG,GACxB;GACF,IAAI,IAAI,UAAU,MAAM,YACtB,KAAK,qBAAqB,IAAI,KAAK;GACrC,IAAI,OACF,MAAM,WAAW,KAAK;EAC1B;EAEA,SAAS,UAAU,OAA4B;GAC7C,IAAI,MAAM,KAAK,WAAW,GACxB;GAEF,MAAM,eAAe,OAAO,UAAU,SAAS;GAE/C,QAAQ,MAAM,KAAd;IACE,KAAK;IACL,KAAK;KAEH,IAAI,MAAM,mBAAmB,UAAU;MACrC,MAAM,eAAe;MACrB,cAAmB,cAAc,KAAK;KACxC;KACA;IACF,KAAK;IACL,KAAK,aAAa;KAEhB,MAAM,MAAM,MAAM,KAAK;KACvB,IAAI,OAAO,WAAW,GAAG,GAAG;MAC1B,MAAM,eAAe;MACrB,aAAa,YAAY;KAC3B;KACA;IACF;GACF;GAGA,IAAI,CAAC,OAAO,qBAAqB,KAAK,GACpC;GAGF,IAAI,MAAM,mBAAmB,UAC3B,cAAmB,OAAO,UAAU,SAAS,cAAc,KAAK;EACpE;;;;;;EAOA,IAAI,eAAwD;EAE5D,SAAS,aAAa,OAAqB;GACzC,MAAM,MAAM,MAAM,KAAK;GACvB,IAAI,CAAC,OAAO,CAAC,WAAW,GAAG,GACzB;GAGF,eADa,WAAW,MAAM,WAAW,SAAS,gBAC5B;IAAE;IAAO,OAAO,IAAI;GAAM,IAAI;GAEpD,KAAK,SAAS,IAAI,KAAK;EACzB;EAIA,YAAY,MAAM,KAAK,QAAQ,OAAO,MAAM,SAAS;GACnD,MAAM,UAAU;GAChB,eAAe;GAEf,IAAI,CAAC,WAAW,QAAQ,MACtB;GAEF,IAAI,MAAM,KAAK,MAAK,QAAO,IAAI,UAAU,QAAQ,KAAK,GACpD;GAEF,MAAM,SAAS;GAEf,MAAM,SAAS,KAAK,IAAI,QAAQ,OAAO,OAAO,CAAC;GAC/C,IAAI,UAAU,GACZ,WAAgB,MAAM;QAEtB,OAAO,MAAM;EACjB,CAAC;EAED,SAAS,QAAQ,OAAmB,KAAY,OAAqB;GACnE,IAAI,CAAC,UAAU,GAAG,GAChB;GAGF,IAAI,MAAM,kBAAkB,WAAW,MAAM,OAAO,QAAQ,qBAAqB,GAAG;IAClF,aAAa,KAAK;IAClB;GACF;GAEA,OAAO,UAAU,KAAK;GAEtB,IAAI,IAAI,UAAU,MAAM,YACtB,KAAK,qBAAqB,IAAI,KAAK;EACvC;;UAIa,QAAA,SAAA,UAAA,GAAX,mBAIM,OAAA;;IAJc,sBAAA;IAAoB,OAAK,eAAE,WAAA,KAAU;OACvD,WAEO,KAAA,QAAA,SAAA,CAAA,SAAA,CAAA,gBAAA,gBADF,kBAAA,KAAiB,GAAA,CAAA,CAAA,CAAA,CAAA,GAAA,CAAA,MAAA,UAAA,GAIxB,mBA0DM,OAAA;;aAxDA;IAAJ,KAAI;IACJ,MAAK;IACL,gBAAA;IACC,oBAAkB,QAAA;IAClB,iBAAe,QAAA,gBAAW,aAAkB,KAAA,IAAY,eAAA;IACxD,OAAK,eAAE,aAAA,KAAY;IACV;qBACO;yBAEjB,mBA8CS,UAAA,MAAA,WA7CgB,QAAA,OAAf,KAAK,UAAK;wBADpB,mBA8CS,UAAA;KA5CN,IAAI,QAAA,SAAM,GAAM,QAAA,OAAM,OAAQ,IAAI,UAAU,KAAA;KAC5C,KAAK,IAAI;;KACT,MAAK,OAAM,aAAa,IAAI,KAAK;KAClC,MAAK;KACL,MAAK;KACL,eAAA;KACC,iBAAe,QAAA,SAAM,GAAM,QAAA,OAAM,SAAU,IAAI,UAAU,KAAA;KACzD,iBAAe,IAAI,UAAU,QAAA,aAAU,SAAA;KACvC,iBAAe,IAAI,WAAQ,SAAY,KAAA;KACvC,qBAAmB,WAAW,GAAG,IAAA,WAAe,KAAA;KAChD,UAAU,MAAA,MAAA,CAAM,CAAC,YAAY,KAAK;KAClC,OAAK,eAAE,SAAS,GAAG,CAAA;KACnB,UAAK,WAAE,QAAQ,QAAQ,KAAK,KAAK;QAElC,mBA6BO,QAAA,EA7BA,OAAK,eAAE,MAAA,eAAA,CAAe,EAAA,GAAA,CAC3B,WAgBO,KAAA,QAAA,OAAA;KAdC;KACL,QAAQ,IAAI,UAAU,QAAA;KACtB,UAAU,QAAQ,IAAI,QAAQ;aAY1B;KARG,IAAI,QAAA,UAAA,GAFZ,YAKE,wBAJK,MAAA,OAAA,CAAO,CAAC,IAAI,IAAI,CAAA,GAAA;;MAEpB,OAAK,eAAA,CAAG,MAAA,YAAA,GAAc,MAAA,SAAA,CAAS,CAAC,IAAI,IAAI,CAAA,CAAA;MACzC,eAAY;;KAEd,mBAA4B,QAAA,MAAA,gBAAnB,IAAI,KAAK,GAAA,CAAA;KACN,IAAI,SAAA,UAAA,GAAhB,mBAEO,QAAA;;MAFiB,OAAK,eAAE,WAAA,KAAU;wBACpC,IAAI,KAAK,GAAA,CAAA,KAAA,mBAAA,IAAA,IAAA;QAMR,WAAW,GAAG,KAAA,UAAA,GADtB,mBAQO,QAAA;;KANL,qBAAA;KACC,OAAK,eAAE,WAAA,KAAU;KACjB,OAAO,WAAA;KACR,eAAY;QAEZ,YAAiC,MAAA,SAAA,GAAA,EAAzB,OAAK,eAAE,eAAA,KAAc,EAAA,GAAA,MAAA,GAAA,CAAA,OAAA,CAAA,CAAA,GAAA,IAAA,UAAA,KAAA,mBAAA,IAAA,IAAA,CAAA,GAAA,CAAA,CAAA,GAAA,IAAA,UAAA"}
@@ -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 };
@@ -1,2 +1,2 @@
1
- import { a as granularityDefaultThemes, i as granularityComponentConfigs, n as GRANULARITY_PROVIDER_ID, o as granularityThemeNames, r as createGranularityProvider, t as granularityProvider } from "./chunks/granular-provider-BAy4APOX.js";
1
+ import { a as granularityDefaultThemes, i as granularityComponentConfigs, n as GRANULARITY_PROVIDER_ID, o as granularityThemeNames, r as createGranularityProvider, t as granularityProvider } from "./chunks/granular-provider-DCn6fgEs.js";
2
2
  export { GRANULARITY_PROVIDER_ID, createGranularityProvider, granularityProvider as default, granularityProvider, granularityComponentConfigs, granularityDefaultThemes, granularityThemeNames };
@@ -1,2 +1,2 @@
1
- import { a as granularityDefaultThemes, i as granularityComponentConfigs, n as GRANULARITY_PROVIDER_ID, o as granularityThemeNames, r as createGranularityProvider, t as granularityProvider } from "./chunks/granular-provider-BAy4APOX.js";
1
+ import { a as granularityDefaultThemes, i as granularityComponentConfigs, n as GRANULARITY_PROVIDER_ID, o as granularityThemeNames, r as createGranularityProvider, t as granularityProvider } from "./chunks/granular-provider-DCn6fgEs.js";
2
2
  export { GRANULARITY_PROVIDER_ID, createGranularityProvider, granularityProvider as default, granularityProvider, granularityComponentConfigs, granularityDefaultThemes, granularityThemeNames };
package/dist/index.js CHANGED
@@ -145,7 +145,7 @@ import { n as grLoadingSafelist, t as grLoadingConfig } from "./chunks/config-Bj
145
145
  import "./components/GrLoading/index.js";
146
146
  import { n as grModalSafelist, t as grModalConfig } from "./chunks/config-mlheIMtV.js";
147
147
  import "./components/GrModal/index.js";
148
- import { t as GrNavbar_default } from "./components/GrNavbar/chunks/GrNavbar-_rtHKwTf.js";
148
+ import { t as GrNavbar_default } from "./components/GrNavbar/chunks/GrNavbar-Dr8e5kW-.js";
149
149
  import { n as grNavbarSafelist, t as grNavbarConfig } from "./chunks/config-2ZHRsdOc.js";
150
150
  import { n as grNumberInputSafelist, t as grNumberInputConfig } from "./chunks/config-MmPym-Z72.js";
151
151
  import { t as GrNumberInput_default } from "./components/GrNumberInput/chunks/GrNumberInput-DQ59YICu.js";
@@ -192,8 +192,8 @@ import { n as grTableSafelist, t as grTableConfig } from "./chunks/config-DCq4Jy
192
192
  import "./components/GrTable/index.js";
193
193
  import { n as GrTabPanels_default, t as GrTabPanel_default } from "./components/GrTabPanels/chunks/GrTabPanels-BZQS8-JM.js";
194
194
  import { t as grTabPanelsConfig } from "./chunks/config-eXB87DOi.js";
195
- import { t as GrTabs_default } from "./components/GrTabs/chunks/GrTabs-CEKT5tXv.js";
196
- import { n as grTabsSafelist, t as grTabsConfig } from "./chunks/config-_QF0O1vq.js";
195
+ import { t as GrTabs_default } from "./components/GrTabs/chunks/GrTabs-CT2nk_HV.js";
196
+ import { n as grTabsSafelist, t as grTabsConfig } from "./chunks/config-CMNfZO7S.js";
197
197
  import { n as grTextareaSafelist, t as grTextareaConfig } from "./chunks/config-BhfNgyWL.js";
198
198
  import "./components/GrTextarea/index.js";
199
199
  import { n as GrTimeline_default, t as GrTimelineItem_default } from "./components/GrTimeline/chunks/GrTimeline-BiHpArsg.js";
@@ -1,3 +1,4 @@
1
+ import { type GrComponentSize } from '../GrConfigProvider/context';
1
2
  /**
2
3
  * GrNavbar — верхняя панель приложения (header) с тремя зонами: слева заголовок
3
4
  * и кнопка меню, по центру — свободная зона (поиск, хлебные крошки), справа —
@@ -10,6 +11,16 @@ export interface GrNavbarProps {
10
11
  /** Заголовок строкой. Слот `#title` сильнее и позволяет обойтись без пропа. */
11
12
  title?: string;
12
13
  showMenuButton?: boolean;
14
+ /**
15
+ * Ступень кнопки меню — и только её: остальное в шапке принадлежит
16
+ * потребителю, а высоту ряда задаёт `--gr-navbar-height`.
17
+ *
18
+ * Фолбэк — `sm` (32px), а не общий для пакета `md`: кнопка была этого размера
19
+ * с самого начала, и подъём по умолчанию сдвинул бы шапку у всех. Нужен
20
+ * тач-таргет — `size="lg"` даёт 44px, ровно то, чего требуют WCAG 2.5.5 и
21
+ * Apple HIG; в 56px ряда он помещается.
22
+ */
23
+ size?: GrComponentSize;
13
24
  /** Extra classes applied to the menu button wrapper (e.g. `sm:hidden`). */
14
25
  menuButtonClass?: string;
15
26
  /**
@@ -34,6 +45,7 @@ type __VLS_Slots = {
34
45
  declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPropTypes<__VLS_WithDefaults<__VLS_TypePropsToOption<GrNavbarProps>, {
35
46
  title: undefined;
36
47
  showMenuButton: boolean;
48
+ size: undefined;
37
49
  menuButtonClass: string;
38
50
  sticky: boolean;
39
51
  }>>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
@@ -41,11 +53,13 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
41
53
  }, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<__VLS_WithDefaults<__VLS_TypePropsToOption<GrNavbarProps>, {
42
54
  title: undefined;
43
55
  showMenuButton: boolean;
56
+ size: undefined;
44
57
  menuButtonClass: string;
45
58
  sticky: boolean;
46
59
  }>>> & Readonly<{
47
60
  onMenu?: (() => any) | undefined;
48
61
  }>, {
62
+ size: GrComponentSize;
49
63
  title: string;
50
64
  sticky: boolean;
51
65
  showMenuButton: boolean;
@@ -0,0 +1,10 @@
1
+ import type { GrComponentSize } from '../GrConfigProvider/context';
2
+ /** Пропы `GrNavbar`, настраиваемые глобально через `componentDefaults`. */
3
+ export interface GrNavbarConfigurableProps {
4
+ size: GrComponentSize;
5
+ }
6
+ declare module '../../composables/useGrComponentConfig' {
7
+ interface GrComponentDefaultsRegistry {
8
+ GrNavbar: GrNavbarConfigurableProps;
9
+ }
10
+ }
@@ -1,6 +1,7 @@
1
1
  export { default } from './GrNavbar.vue';
2
2
  export { default as GrNavbar } from './GrNavbar.vue';
3
3
  export type { GrNavbarProps } from './GrNavbar.vue';
4
+ export type { GrNavbarConfigurableProps } from './defaults';
4
5
  export { grNavbarConfig } from './config';
5
6
  export { grNavbarSafelist } from './safelist';
6
7
  export type { GrNavbarEmits } from './GrNavbar.vue';
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/JetBrains/web-types/master/schema/web-types.json",
3
3
  "name": "@feugene/granularity",
4
- "version": "0.34.2",
4
+ "version": "0.35.0",
5
5
  "framework": "vue",
6
6
  "js-types-syntax": "typescript",
7
7
  "description-markup": "markdown",
@@ -6565,6 +6565,15 @@
6565
6565
  "type": "string | undefined"
6566
6566
  }
6567
6567
  },
6568
+ {
6569
+ "name": "size",
6570
+ "description": "Ступень кнопки меню — и только её: остальное в шапке принадлежит потребителю, а высоту ряда задаёт `--gr-navbar-height`. Фолбэк — `sm` (32px), а не общий для пакета `md`: кнопка была этого размера с самого начала, и подъём по умолчанию сдвинул бы шапку у всех. Нужен тач-таргет — `size=\"lg\"` даёт 44px, ровно то, чего требуют WCAG 2.5.5 и Apple HIG; в 56px ряда он помещается.",
6571
+ "default": "undefined",
6572
+ "value": {
6573
+ "kind": "expression",
6574
+ "type": "\"xs\" | \"sm\" | \"md\" | \"lg\" | undefined"
6575
+ }
6576
+ },
6568
6577
  {
6569
6578
  "name": "showMenuButton",
6570
6579
  "default": "false",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@feugene/granularity",
3
3
  "description": "Granularity design system package with Vue 3 components, consumed via the `@feugene/unocss-preset-granular` preset.",
4
- "version": "0.34.2",
4
+ "version": "0.35.0",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": {
7
7
  "name": "Evgeniy Fureev",
@@ -1 +0,0 @@
1
- {"version":3,"file":"config-_QF0O1vq.js","names":[],"sources":["../../src/components/GrTabs/grTabsStyles.ts","../../src/components/GrTabs/safelist.ts","../../src/components/GrTabs/config.ts"],"sourcesContent":["import type { GrComponentSize } from '../shared/sizes'\n\nexport type GrTabsSize = GrComponentSize\n\n/**\n * `pills` — вкладки-таблетки в общей обойме, `line` — классический ряд с\n * подчёркиванием активной. Имя `pills` общее с `GrSegmented`: у компонентов\n * одна роль, и вид с таким названием у них совпадает.\n */\nexport const GR_TABS_VARIANTS = ['pills', 'line'] as const\nexport type GrTabsVariant = typeof GR_TABS_VARIANTS[number]\n\nexport type GrTabsOrientation = 'horizontal' | 'vertical'\n\n/** Высота вкладки повторяет шкалу `GrButton`: вкладки часто стоят с ним в один ряд. */\nexport const tabSizes: Record<GrTabsSize, string> = {\n xs: 'h-7 px-2 text-[length:var(--gr-text-xs)] leading-[var(--gr-leading-xs)]',\n sm: 'h-8 px-2.5 text-[length:var(--gr-text-xs)] leading-[var(--gr-leading-xs)]',\n md: 'h-9 px-3 text-[length:var(--gr-text-sm)] leading-[var(--gr-leading-sm)]',\n lg: 'h-10 px-4 text-[length:var(--gr-text-base)] leading-[var(--gr-leading-base)]',\n}\n\n/** Счётчик у вкладки: на ступень мельче подписи. */\nexport const tabBadgeSizes: Record<GrTabsSize, string> = {\n xs: 'text-[length:var(--gr-text-2xs)] leading-[var(--gr-leading-2xs)] px-1 py-0.5',\n sm: 'text-[length:var(--gr-text-2xs)] leading-[var(--gr-leading-2xs)] px-1.5 py-0.5',\n md: 'text-[length:var(--gr-text-2xs)] leading-[var(--gr-leading-2xs)] px-1.5 py-0.5',\n lg: 'text-[length:var(--gr-text-xs)] leading-[var(--gr-leading-xs)] px-2 py-0.5',\n}\n\n/** Обойма вокруг вкладок: на мелких ступенях рамка съедала бы высоту. */\nexport const tablistSizes: Record<GrTabsSize, string> = {\n xs: 'gap-0.5 p-0.5',\n sm: 'gap-1 p-1',\n md: 'gap-1 p-1',\n lg: 'gap-1 p-1.5',\n}\n\n/** `line` живёт без обоймы, поэтому и без внутренних отступов. */\nexport const tablistLineSizes: Record<GrTabsSize, string> = {\n xs: 'gap-1',\n sm: 'gap-2',\n md: 'gap-2',\n lg: 'gap-3',\n}\n\nexport const tablistBase = 'inline-flex max-w-full'\n\nexport const tablistVariants: Record<GrTabsVariant, string> = {\n pills: 'rounded-[var(--gr-radius-lg)] border border-[var(--gr-brd)] bg-[var(--gr-muted)]',\n line: 'border-b border-[var(--gr-brd)]',\n}\n\n/**\n * Вертикальный `line` подчёркивает вкладку сбоку, а не снизу: линия должна идти\n * вдоль списка, иначе она отделяет вкладки друг от друга вместо ряда целиком.\n */\nexport const tablistVerticalVariants: Record<GrTabsVariant, string> = {\n pills: '',\n line: 'border-b-0 border-r border-[var(--gr-brd)]',\n}\n\n/**\n * Переполнение прокручивается, а не переносится строкой: перенос ломает ряд и\n * уводит вкладки под панель. Полоса прокрутки скрыта — ряд ведёт клавиатура и\n * автоскролл к активной вкладке.\n */\nexport const tablistScrollClass = 'overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'\n\n/** Вертикальный ряд — колонка; прокрутки ему не нужно: он растёт вниз. */\nexport const tablistColumnClass = 'flex-col'\n\nexport const tabBase = 'shrink-0 font-600 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)]'\n\n// Цвет подчёркивания задаёт только состояние: `border-transparent` в базе имеет\n// ту же специфичность, что и цвет активной вкладки, и побеждал бы через раз —\n// по порядку правил в сгенерированном CSS, а не по порядку классов в атрибуте.\nexport const tabVariantBase: Record<GrTabsVariant, string> = {\n pills: 'rounded-[var(--gr-radius-md)]',\n line: 'rounded-[var(--gr-radius-md)] rounded-b-[var(--gr-radius-none)] border-b-2 -mb-px',\n}\n\nexport const tabStateClasses: Record<GrTabsVariant, { active: string, idle: string, disabled: string }> = {\n pills: {\n active: 'bg-[var(--gr-card)] text-[var(--gr-fg)] border border-[var(--gr-brd)]',\n idle: 'text-[var(--gr-muted-fg)] hover:text-[var(--gr-fg)] hover:bg-[color-mix(in_srgb,var(--gr-card)_70%,transparent)]',\n disabled: 'cursor-not-allowed text-[var(--gr-disabled-fg)]',\n },\n line: {\n active: 'border-[var(--gr-primary)] text-[var(--gr-fg)]',\n idle: 'border-transparent text-[var(--gr-muted-fg)] hover:text-[var(--gr-fg)] hover:border-[var(--gr-brd)]',\n disabled: 'border-transparent cursor-not-allowed text-[var(--gr-disabled-fg)]',\n },\n}\n\nexport const tabIconClass = 'h-4 w-4 shrink-0'\n\nexport const tabContentClass = 'inline-flex items-center gap-2'\n\n/**\n * Крестик закрытия. Кнопкой он быть не может: `role=\"tab\"` объявляет потомков\n * презентационными, и вложенный интерактив теряется у скринридера (axe:\n * `nested-interactive`). Поэтому это `<span>`, а клик по нему разбирает\n * обработчик самой вкладки.\n *\n * Своего фокус-кольца у него нет по той же причине — в таб-порядке он не стоит,\n * с клавиатуры закрытие идёт по `Delete`/`Backspace`.\n */\nexport const tabCloseBase = 'inline-flex items-center justify-center shrink-0 rounded-[var(--gr-radius-sm)] text-[var(--gr-muted-fg)] transition-colors hover:bg-[var(--gr-muted)] hover:text-[var(--gr-fg)]'\n\nexport const tabCloseSizes: Record<GrTabsSize, string> = {\n xs: 'h-4 w-4',\n sm: 'h-4 w-4',\n md: 'h-5 w-5',\n lg: 'h-5 w-5',\n}\n\nexport const tabCloseIconSizes: Record<GrTabsSize, string> = {\n xs: 'h-3 w-3',\n sm: 'h-3 w-3',\n md: 'h-4 w-4',\n lg: 'h-4 w-4',\n}\n\n/** Пустой ряд держит высоту вкладки: иначе соседние блоки прыгают при закрытии последней. */\nexport const tabsEmptyBase = 'inline-flex items-center text-[var(--gr-muted-fg)]'\n\nexport const tabBadgeBase = 'rounded-[var(--gr-radius-full)] bg-[var(--gr-secondary)] text-[var(--gr-secondary-fg)]'\n\nexport function grTabsListClass(options: {\n variant: GrTabsVariant\n size: GrTabsSize\n orientation: GrTabsOrientation\n}): string {\n const vertical = options.orientation === 'vertical'\n\n return [\n tablistBase,\n tablistVariants[options.variant],\n vertical ? tablistVerticalVariants[options.variant] : '',\n options.variant === 'pills' ? tablistSizes[options.size] : tablistLineSizes[options.size],\n vertical ? tablistColumnClass : tablistScrollClass,\n ].filter(Boolean).join(' ')\n}\n\nexport function grTabsTabClass(options: {\n variant: GrTabsVariant\n size: GrTabsSize\n active: boolean\n disabled: boolean\n}): string {\n const state = tabStateClasses[options.variant]\n\n return [\n tabBase,\n tabVariantBase[options.variant],\n tabSizes[options.size],\n options.disabled ? state.disabled : options.active ? state.active : state.idle,\n ].join(' ')\n}\n\nexport function grTabsBadgeClass(size: GrTabsSize): string {\n return [tabBadgeBase, tabBadgeSizes[size]].join(' ')\n}\n\nexport function grTabsCloseClass(size: GrTabsSize): string {\n return [tabCloseBase, tabCloseSizes[size]].join(' ')\n}\n\nexport function grTabsEmptyClass(size: GrTabsSize): string {\n return [tabsEmptyBase, tabSizes[size]].join(' ')\n}\n","import { splitClassTokens } from '../shared/classTokens'\nimport {\n tabBadgeBase,\n tabBadgeSizes,\n tabBase,\n tabCloseBase,\n tabCloseIconSizes,\n tabCloseSizes,\n tabContentClass,\n tabIconClass,\n tablistBase,\n tablistColumnClass,\n tablistLineSizes,\n tablistScrollClass,\n tablistSizes,\n tablistVariants,\n tablistVerticalVariants,\n tabsEmptyBase,\n tabSizes,\n tabStateClasses,\n tabVariantBase,\n} from './grTabsStyles'\n\nexport const grTabsSafelist = [...new Set([\n ...Object.values(tabSizes).flatMap(splitClassTokens),\n ...Object.values(tabBadgeSizes).flatMap(splitClassTokens),\n ...Object.values(tabCloseSizes).flatMap(splitClassTokens),\n ...Object.values(tabCloseIconSizes).flatMap(splitClassTokens),\n ...Object.values(tablistSizes).flatMap(splitClassTokens),\n ...Object.values(tablistLineSizes).flatMap(splitClassTokens),\n ...Object.values(tablistVariants).flatMap(splitClassTokens),\n ...Object.values(tablistVerticalVariants).flatMap(splitClassTokens),\n ...Object.values(tabVariantBase).flatMap(splitClassTokens),\n ...Object.values(tabStateClasses).flatMap(state => [\n ...splitClassTokens(state.active),\n ...splitClassTokens(state.idle),\n ...splitClassTokens(state.disabled),\n ]),\n ...splitClassTokens(tablistBase),\n ...splitClassTokens(tablistScrollClass),\n ...splitClassTokens(tablistColumnClass),\n ...splitClassTokens(tabBase),\n ...splitClassTokens(tabBadgeBase),\n ...splitClassTokens(tabContentClass),\n ...splitClassTokens(tabIconClass),\n ...splitClassTokens(tabCloseBase),\n ...splitClassTokens(tabsEmptyBase),\n])]\n","import { defineGranularComponent } from '@feugene/unocss-preset-granular/contract'\n\nimport { grTabsSafelist } from './safelist'\n\nexport const grTabsConfig = defineGranularComponent(import.meta.url, {\n name: 'GrTabs',\n safelist: grTabsSafelist,\n})\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"GrNavbar-_rtHKwTf.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"}