@feugene/granularity 0.40.0 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/LICENSE +13 -162
  2. package/README.md +6 -5
  3. package/dist/chunks/{config-BRESvs9g.js → config-Ca8qr8Q3.js} +1 -1
  4. package/dist/chunks/{config-BRESvs9g.js.map → config-Ca8qr8Q3.js.map} +1 -1
  5. package/dist/chunks/{en-NpP9e7lC.js → en-B9E3jO6m.js} +5 -7
  6. package/dist/chunks/{en-NpP9e7lC.js.map → en-B9E3jO6m.js.map} +1 -1
  7. package/dist/chunks/{es-Do2CNrC2.js → es-DCkqHTlU.js} +3 -3
  8. package/dist/chunks/{es-Do2CNrC2.js.map → es-DCkqHTlU.js.map} +1 -1
  9. package/dist/chunks/{es-DaeA2Ql-.js → es-QpzdIEut.js} +5 -7
  10. package/dist/chunks/{es-DaeA2Ql-.js.map → es-QpzdIEut.js.map} +1 -1
  11. package/dist/chunks/{generated-BcfXQY4j.js → generated-B95IBXs6.js} +0 -56
  12. package/dist/chunks/{generated-BcfXQY4j.js.map → generated-B95IBXs6.js.map} +1 -1
  13. package/dist/chunks/{grKbdStyles-DENu8pUK.js → grKbdStyles-ByMsX-lQ.js} +17 -2
  14. package/dist/chunks/grKbdStyles-ByMsX-lQ.js.map +1 -0
  15. package/dist/chunks/{granular-provider-vVZ-76bP.js → granular-provider-BG_2XqkS.js} +1 -3
  16. package/dist/chunks/granular-provider-BG_2XqkS.js.map +1 -0
  17. package/dist/chunks/{ru-Bg9-DPmg.js → ru-D_1Ya489.js} +5 -7
  18. package/dist/chunks/{ru-Bg9-DPmg.js.map → ru-D_1Ya489.js.map} +1 -1
  19. package/dist/components/GrBreadcrumbs/chunks/{GrBreadcrumbs-DOpwY2EF.js → GrBreadcrumbs-D0qPry_R.js} +4 -1
  20. package/dist/components/GrBreadcrumbs/chunks/GrBreadcrumbs-D0qPry_R.js.map +1 -0
  21. package/dist/components/GrBreadcrumbs/index.js +1 -1
  22. package/dist/components/GrCommandPalette/chunks/{GrCommandPalette-CnzPuoIM.js → GrCommandPalette-Ha4s3DMT.js} +1 -1
  23. package/dist/components/GrCommandPalette/chunks/{GrCommandPalette-CnzPuoIM.js.map → GrCommandPalette-Ha4s3DMT.js.map} +1 -1
  24. package/dist/components/GrCommandPalette/index.js +1 -1
  25. package/dist/components/GrDataTable/chunks/{GrDataTable-BYYG_XZA.js → GrDataTable-BBe7-uUe.js} +6 -2
  26. package/dist/components/GrDataTable/chunks/GrDataTable-BBe7-uUe.js.map +1 -0
  27. package/dist/components/GrDataTable/index.js +1 -1
  28. package/dist/components/GrDelta/chunks/{GrDelta-BpfqebEV.js → GrDelta-CgTvfoIN.js} +0 -0
  29. package/dist/components/GrDelta/chunks/GrDelta-CgTvfoIN.js.map +1 -0
  30. package/dist/components/GrDelta/index.js +1 -1
  31. package/dist/components/GrDescriptionList/chunks/{GrDescriptionList-D8ANFpn_.js → GrDescriptionList-Dz2Jj1if.js} +26 -11
  32. package/dist/components/GrDescriptionList/chunks/GrDescriptionList-Dz2Jj1if.js.map +1 -0
  33. package/dist/components/GrDescriptionList/index.js +1 -1
  34. package/dist/components/GrFilePreview/chunks/{GrFilePreview-PabkLgYX.js → GrFilePreview-2kVOg1C7.js} +40 -5
  35. package/dist/components/GrFilePreview/chunks/GrFilePreview-2kVOg1C7.js.map +1 -0
  36. package/dist/components/GrFilePreview/index.js +1 -1
  37. package/dist/components/GrImageViewer/chunks/{GrImageViewer-BV38WM3x.js.map → GrImageViewer-Dd2gJ-j_.js.map} +1 -1
  38. package/dist/components/GrImageViewer/index.js +1 -1
  39. package/dist/components/GrInputTag/chunks/{GrInputTag-DhfUNOA5.js → GrInputTag-BKzm1PKw.js} +5 -2
  40. package/dist/components/GrInputTag/chunks/GrInputTag-BKzm1PKw.js.map +1 -0
  41. package/dist/components/GrInputTag/index.js +1 -1
  42. package/dist/components/GrJsonViewer/chunks/{GrJsonViewer-CdtmUpZ3.js → GrJsonViewer-BLCEWlwg.js} +62 -10
  43. package/dist/components/GrJsonViewer/chunks/GrJsonViewer-BLCEWlwg.js.map +1 -0
  44. package/dist/components/GrJsonViewer/index.js +1 -1
  45. package/dist/components/GrKbd/chunks/{GrKbd-D_a1mPLe.js → GrKbd-BNMeoaLV.js} +1 -1
  46. package/dist/components/GrKbd/chunks/{GrKbd-D_a1mPLe.js.map → GrKbd-BNMeoaLV.js.map} +1 -1
  47. package/dist/components/GrKbd/index.js +2 -2
  48. package/dist/components/GrNumberInput/chunks/{GrNumberInput-BqPydXpK.js → GrNumberInput-Bm-V59TG.js} +4 -1
  49. package/dist/components/GrNumberInput/chunks/GrNumberInput-Bm-V59TG.js.map +1 -0
  50. package/dist/components/GrNumberInput/index.js +1 -1
  51. package/dist/components/GrSegmented/chunks/{GrSegmented-NKTi9bMG.js → GrSegmented-CuDaKA6S.js} +5 -1
  52. package/dist/components/GrSegmented/chunks/GrSegmented-CuDaKA6S.js.map +1 -0
  53. package/dist/components/GrSegmented/index.js +1 -1
  54. package/dist/components/GrSortableList/chunks/{GrSortableList-CQCaWivn.js → GrSortableList-ce1psLL0.js} +4 -1
  55. package/dist/components/GrSortableList/chunks/GrSortableList-ce1psLL0.js.map +1 -0
  56. package/dist/components/GrSortableList/index.js +1 -1
  57. package/dist/components/GrSteps/chunks/{GrSteps-DXj7m-WO.js → GrSteps-Cng-Wrjk.js} +5 -1
  58. package/dist/components/GrSteps/chunks/GrSteps-Cng-Wrjk.js.map +1 -0
  59. package/dist/components/GrSteps/index.js +1 -1
  60. package/dist/components/GrTabs/chunks/{GrTabs-DHfv67cp.js → GrTabs-BArefUFv.js} +5 -1
  61. package/dist/components/GrTabs/chunks/GrTabs-BArefUFv.js.map +1 -0
  62. package/dist/components/GrTabs/index.js +1 -1
  63. package/dist/components/GrTree/chunks/{GrTree-CkELQwR8.js.map → GrTree-BFWFc0Bm.js.map} +1 -1
  64. package/dist/components/GrTree/index.js +1 -1
  65. package/dist/components/GrTreeSelect/chunks/{GrTreeSelect-BTb8e1az.js → GrTreeSelect-DJa2lYGe.js} +1 -1
  66. package/dist/components/GrTreeSelect/chunks/{GrTreeSelect-BTb8e1az.js.map → GrTreeSelect-DJa2lYGe.js.map} +1 -1
  67. package/dist/components/GrTreeSelect/index.js +1 -1
  68. package/dist/granular-provider-node.js +1 -1
  69. package/dist/granular-provider.js +1 -1
  70. package/dist/i18n/all.js +1 -1
  71. package/dist/i18n/index.js +1 -1
  72. package/dist/index.js +20 -22
  73. package/dist/theme.js +1 -1
  74. package/dist/tokens.js +1 -1
  75. package/dist/types/components/GrFilePreview/GrFilePreview.vue.d.ts +14 -0
  76. package/dist/types/components/GrJsonViewer/GrJsonViewer.vue.d.ts +1 -1
  77. package/dist/types/components/GrKbd/grKbdStyles.d.ts +17 -2
  78. package/dist/types/granular-provider/shared.d.ts +0 -1
  79. package/dist/types/i18n/messages/en.d.ts +12 -10
  80. package/dist/types/i18n/messages/es.d.ts +12 -10
  81. package/dist/types/i18n/messages/ru.d.ts +12 -10
  82. package/dist/types/index.d.ts +0 -1
  83. package/dist/web-types.json +55 -122
  84. package/package.json +3 -7
  85. package/dist/chunks/config-DolULhjJ.js +0 -92
  86. package/dist/chunks/config-DolULhjJ.js.map +0 -1
  87. package/dist/chunks/copy-DChChcq-.js +0 -29
  88. package/dist/chunks/copy-DChChcq-.js.map +0 -1
  89. package/dist/chunks/grKbdStyles-DENu8pUK.js.map +0 -1
  90. package/dist/chunks/granular-provider-vVZ-76bP.js.map +0 -1
  91. package/dist/components/GrBreadcrumbs/chunks/GrBreadcrumbs-DOpwY2EF.js.map +0 -1
  92. package/dist/components/GrCodeBlock/chunks/GrCodeBlock-BUOwKwQE.js +0 -315
  93. package/dist/components/GrCodeBlock/chunks/GrCodeBlock-BUOwKwQE.js.map +0 -1
  94. package/dist/components/GrCodeBlock/index.js +0 -3
  95. package/dist/components/GrCodeBlock/styles.css +0 -1
  96. package/dist/components/GrDataTable/chunks/GrDataTable-BYYG_XZA.js.map +0 -1
  97. package/dist/components/GrDelta/chunks/GrDelta-BpfqebEV.js.map +0 -1
  98. package/dist/components/GrDescriptionList/chunks/GrDescriptionList-D8ANFpn_.js.map +0 -1
  99. package/dist/components/GrFilePreview/chunks/GrFilePreview-PabkLgYX.js.map +0 -1
  100. package/dist/components/GrInputTag/chunks/GrInputTag-DhfUNOA5.js.map +0 -1
  101. package/dist/components/GrJsonViewer/chunks/GrJsonViewer-CdtmUpZ3.js.map +0 -1
  102. package/dist/components/GrNumberInput/chunks/GrNumberInput-BqPydXpK.js.map +0 -1
  103. package/dist/components/GrSegmented/chunks/GrSegmented-NKTi9bMG.js.map +0 -1
  104. package/dist/components/GrSortableList/chunks/GrSortableList-CQCaWivn.js.map +0 -1
  105. package/dist/components/GrSteps/chunks/GrSteps-DXj7m-WO.js.map +0 -1
  106. package/dist/components/GrTabs/chunks/GrTabs-DHfv67cp.js.map +0 -1
  107. package/dist/types/components/GrCodeBlock/GrCodeBlock.vue.d.ts +0 -79
  108. package/dist/types/components/GrCodeBlock/config.d.ts +0 -1
  109. package/dist/types/components/GrCodeBlock/defaults.d.ts +0 -21
  110. package/dist/types/components/GrCodeBlock/grCodeBlockStyles.d.ts +0 -53
  111. package/dist/types/components/GrCodeBlock/index.d.ts +0 -6
  112. package/dist/types/components/GrCodeBlock/safelist.d.ts +0 -1
  113. package/dist/types/components/GrCodeBlock/serializeCode.d.ts +0 -19
  114. package/dist/types/components/GrCodeBlock/tokenizeJson.d.ts +0 -22
  115. package/dist/components/GrImageViewer/chunks/{GrImageViewer-BV38WM3x.js → GrImageViewer-Dd2gJ-j_.js} +1 -1
  116. package/dist/components/GrTree/chunks/{GrTree-CkELQwR8.js → GrTree-BFWFc0Bm.js} +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GrNumberInput-Bm-V59TG.js","names":["$slots","$attrs"],"sources":["../../../../src/components/GrNumberInput/numberInputMath.ts","../../../../src/components/GrNumberInput/GrNumberInput.vue","../../../../src/components/GrNumberInput/GrNumberInput.vue"],"sourcesContent":["export { bigStep } from '../shared/numericStep'\n\n/**\n * Экспоненциальная запись разбирается отдельно: `String(1e-7)` — это `\"1e-7\"`,\n * и поиск точки насчитал бы ноль знаков, после чего округление съело бы всё\n * значение.\n */\nexport function decimalsOf(value: number): number {\n if (!Number.isFinite(value))\n return 0\n\n const text = String(Math.abs(value))\n\n const exponential = text.match(/^\\d+(?:\\.(\\d+))?e-(\\d+)$/)\n if (exponential) {\n const [, fraction = '', exponent] = exponential\n return fraction.length + Number(exponent)\n }\n\n const dot = text.indexOf('.')\n return dot === -1 ? 0 : text.length - dot - 1\n}\n\n/**\n * Двоичная дробь копит ошибку, поэтому сумма округляется. Разрядность — по\n * большему из операндов: шаг `0.5` от значения `1.25` обязан дать `1.75`.\n */\nexport function addStep(current: number, step: number): number {\n const decimals = Math.max(decimalsOf(current), decimalsOf(step))\n const sum = current + step\n\n // `toFixed` за пределами 100 знаков бросает.\n return decimals > 100 ? sum : Number(sum.toFixed(decimals))\n}\n","<script setup lang=\"ts\">\nimport type { InputHTMLAttributes } from 'vue'\n\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { computed, onBeforeUnmount, ref, watchEffect } from 'vue'\n\nimport {\n clearButtonClass,\n stepperCompactClass,\n stepperWideClass,\n grNumberInputInputClass,\n grNumberInputShellClass,\n type GrNumberInputControlsDirection,\n type GrNumberInputSize,\n type GrNumberInputState,\n type GrNumberInputTextAlign,\n} from './grNumberInputStyles'\nimport { addStep, bigStep } from './numberInputMath'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport IconChevronDown from '~icons/lucide/chevron-down'\nimport IconChevronLeft from '~icons/lucide/chevron-left'\nimport IconChevronRight from '~icons/lucide/chevron-right'\nimport IconChevronUp from '~icons/lucide/chevron-up'\nimport IconX from '~icons/lucide/x'\nimport { addLen } from '../../composables/internal/useAddonMeasurement'\nimport { useControlAddons } from '../../composables/internal/useControlAddons'\n\ndefineOptions({\n inheritAttrs: false,\n})\n\nconst ADDON_PX_BY_SIZE: Record<GrNumberInputSize, number> = {\n xs: 32,\n sm: 36,\n md: 40,\n lg: 48,\n}\n\nconst BASE_PADDING_X_LEN_BY_SIZE: Record<GrNumberInputSize, string> = {\n xs: '10px',\n sm: '12px',\n md: '12px',\n lg: '16px',\n}\n\nfunction px(n: number): string {\n return `${n}px`\n}\n\nexport interface GrNumberInputProps {\n /**\n * Значение поля. `null` — пусто.\n *\n * Незавершённый ввод («-», «1,») числом не является и в модель не попадает:\n * пока он набирается, поле держит его во внутреннем черновике, а модель\n * честно говорит «числа пока нет».\n */\n modelValue: number | null\n placeholder?: string\n autocomplete?: string\n inputmode?: InputHTMLAttributes['inputmode']\n disabled?: boolean\n /** Быстрый флаг невалидности; эквивалент `state='danger'` + `aria-invalid`. */\n invalid?: boolean\n /** Только для чтения: значение видно и уходит в форму, но не редактируется. */\n readonly?: boolean\n /** Обязательное поле (`aria-required`). Складывается с `required` у `GrFormField`. */\n required?: boolean\n /** Доступное имя вне `GrFormField`. */\n ariaLabel?: string\n state?: GrNumberInputState\n name?: string\n id?: string\n size?: GrNumberInputSize\n\n textAlign?: GrNumberInputTextAlign\n\n decimalSeparator?: string\n step?: number\n min?: number\n max?: number\n precision?: number\n\n /** Кнопка очистки значения. */\n clearable?: boolean\n /** A11y-подпись кнопки очистки. */\n clearLabel?: string\n /**\n * BCP-47 локаль для отображения значения (группировка разрядов и разделители\n * через `Intl.NumberFormat`). Работает вместе с `useGrouping`.\n */\n locale?: string\n /**\n * Группировать разряды при отображении (когда поле не в фокусе). При фокусе\n * показывается «сырое» значение для редактирования. По умолчанию выключено.\n */\n useGrouping?: boolean\n\n /** Показывать кнопки +/-. */\n controls?: boolean\n controlsDirection?: GrNumberInputControlsDirection\n\n prefixMinWidth?: string\n prefixMaxWidth?: string\n suffixMinWidth?: string\n suffixMaxWidth?: string\n /**\n * Фиксированная ширина у prefix/suffix: жёсткая ширина (из `*MaxWidth` →\n * `*MinWidth` → дефолт) + обрезка контента по краю (prefix — справа,\n * suffix — слева). По умолчанию аддоны растягиваются под контент.\n */\n prefixFixed?: boolean\n suffixFixed?: boolean\n\n /** i18n-friendly aria-label для кнопки \"увеличить\". */\n increaseLabel?: string\n /** i18n-friendly aria-label для кнопки \"уменьшить\". */\n decreaseLabel?: string\n}\n\nexport interface GrNumberInputEmits {\n (e: 'update:modelValue', value: number | null): void\n (e: 'change', value: number | null): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n /** Значение стёрто кнопкой очистки. */\n (e: 'clear'): void\n}\n\n// Контекст `GrFormField` — fallback для id/описания/невалидности, как в `GrInput`.\nconst field = useGrFormFieldContext()\n\nconst props = withDefaults(defineProps<GrNumberInputProps>(), {\n placeholder: undefined,\n autocomplete: undefined,\n inputmode: 'decimal',\n disabled: false,\n invalid: false,\n readonly: false,\n required: false,\n ariaLabel: undefined,\n state: 'default',\n name: undefined,\n id: undefined,\n size: undefined,\n\n textAlign: 'left',\n\n decimalSeparator: '.',\n step: 1,\n min: undefined,\n max: undefined,\n precision: undefined,\n locale: undefined,\n useGrouping: false,\n clearable: undefined,\n clearLabel: undefined,\n\n controls: false,\n controlsDirection: 'vertical',\n\n prefixMinWidth: undefined,\n prefixMaxWidth: undefined,\n suffixMinWidth: undefined,\n suffixMaxWidth: undefined,\n prefixFixed: false,\n suffixFixed: false,\n\n increaseLabel: undefined,\n decreaseLabel: undefined,\n})\n\n// Эффективный размер: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrNumberInput' })\n\nconst resolvedId = computed(() => props.id ?? field?.id.value)\nconst describedBy = computed(() => field?.describedById.value)\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n} = useGrFormControl(() => props)\n\nconst emit = defineEmits<GrNumberInputEmits>()\ndefineSlots<{\n /** Аддон слева от поля: знак валюты, иконка. */\n prefix?: () => any\n /** Аддон справа от поля: единица измерения. */\n suffix?: () => any\n}>()\n\nconst inputEl = ref<HTMLInputElement | null>(null)\n\n// В фокусе поле показывает «сырое» значение: группировка мешала бы правке.\nconst isFocused = ref(false)\n\n/**\n * Строка ровно в том виде, в каком её набирают. Модель к этому моменту может\n * быть `null` («-», «1,» — не числа), и без черновика поле стирало бы под\n * пальцами то, что печатают. Коммит (`change`/`blur`) черновик снимает.\n */\nconst draft = ref<string | null>(null)\n\nfunction focus(): void {\n inputEl.value?.focus()\n}\n\nfunction blur(): void {\n inputEl.value?.blur()\n}\n\ndefineExpose({ focus, blur })\n\nconst { t, locale } = useGranularityTranslations()\n\n/**\n * Локаль форматирования: свой проп → локаль i18n-адаптера → окружение. Без\n * второго шага мультиязычное приложение передавало бы `locale` каждому полю.\n */\nconst resolvedLocale = computed(() => props.locale ?? locale.value)\nconst resolvedIncreaseLabel = computed(() => props.increaseLabel ?? t('gr.numberInput.increase', 'Increase'))\nconst resolvedDecreaseLabel = computed(() => props.decreaseLabel ?? t('gr.numberInput.decrease', 'Decrease'))\n\nconst hasHorizontalControls = computed(() => props.controls && props.controlsDirection === 'horizontal')\nconst hasVerticalControls = computed(() => props.controls && props.controlsDirection === 'vertical')\n\nconst addonPx = computed(() => ADDON_PX_BY_SIZE[resolvedSize.value])\nconst addonLen = computed(() => px(addonPx.value))\nconst basePaddingXLen = computed(() => BASE_PADDING_X_LEN_BY_SIZE[resolvedSize.value])\n\n/** Кнопки ± справа: вертикальный стек — одна колонка, горизонтальный — вторая. */\nconst rightControlsCount = computed(() =>\n (hasHorizontalControls.value ? 1 : 0) + (hasVerticalControls.value ? 1 : 0))\n\nconst {\n prefixEl,\n suffixEl,\n prefixLen,\n suffixLen,\n prefixStyle,\n suffixStyle,\n fieldPadding: inputStyle,\n} = useControlAddons(() => props, {\n defaultMinWidth: () => addonLen.value,\n paddingX: () => basePaddingXLen.value,\n leadingReserve: () => (hasHorizontalControls.value ? addonLen.value : '0px'),\n trailingReserve: () => (rightControlsCount.value > 0 ? px(addonPx.value * rightControlsCount.value) : '0px'),\n // Суффикс позиционируется абсолютно: без якоря он уехал бы от правого края.\n anchorSuffixRight: true,\n})\n\nfunction addonStyle(side: 'left' | 'right', offset: string): Record<string, string> {\n return {\n width: addonLen.value,\n [side]: offset,\n }\n}\n\nconst verticalControlsStyle = computed(() => addonStyle('right', suffixLen.value))\n\n/**\n * Крестик очистки живёт левее правой зоны (суффикс + кнопки ±) — иначе он лёг бы\n * на них. Ширины там же, где их считает поле, поэтому переиспользуем расчёт.\n */\nconst clearButtonStyle = computed(() => {\n const controls = rightControlsCount.value > 0 ? px(addonPx.value * rightControlsCount.value) : '0px'\n\n return { right: addLen(addLen(suffixLen.value, controls), '6px') }\n})\nconst horizontalLeftControlsStyle = computed(() => addonStyle('left', prefixLen.value))\nconst horizontalRightControlsStyle = computed(() => addonStyle('right', suffixLen.value))\n\nconst shellClassName = computed(() => {\n return grNumberInputShellClass({\n disabled: isDisabled.value,\n state: props.state,\n invalid: isInvalid.value,\n })\n})\n\nconst inputClassName = computed(() => {\n return grNumberInputInputClass({\n size: resolvedSize.value,\n textAlign: props.textAlign,\n })\n})\n\nfunction isDigit(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\nfunction sanitize(raw: string): string {\n const sep = props.decimalSeparator || '.'\n\n let out = ''\n let hasSepLocal = false\n\n for (let i = 0; i < raw.length; i++) {\n const ch = raw[i]\n\n // Ведущий минус: разрешаем один в начале, чтобы можно было ввести\n // отрицательное значение с клавиатуры (границы проверит `clamp` на change).\n if (ch === '-' && out.length === 0) {\n out += '-'\n continue\n }\n\n if (isDigit(ch)) {\n out += ch\n continue\n }\n\n if ((ch === sep || ch === '.' || ch === ',') && !hasSepLocal) {\n out += sep\n hasSepLocal = true\n }\n }\n\n return out\n}\n\nfunction toNumber(value: string): number | null {\n if (value.trim() === '')\n return null\n const sep = props.decimalSeparator || '.'\n const normalized = sep === '.' ? value : value.replace(sep, '.')\n const n = Number(normalized)\n if (!Number.isFinite(n))\n return null\n return n\n}\n\nfunction clamp(v: number): number {\n if (props.min !== undefined)\n v = Math.max(props.min, v)\n if (props.max !== undefined)\n v = Math.min(props.max, v)\n return v\n}\n\nfunction normalize(v: number): number {\n if (props.precision === undefined)\n return v\n const p = Math.max(0, props.precision)\n return Number(v.toFixed(p))\n}\n\nfunction format(n: number): string {\n const sep = props.decimalSeparator || '.'\n const p = props.precision\n\n // Без `precision` избегаем экспоненциальной записи (`String(1e21)` → `\"1e+21\"`),\n // форматируя через `Intl` без группировки разрядов.\n let s = p === undefined\n ? n.toLocaleString('en-US', { useGrouping: false, maximumFractionDigits: 20 })\n : n.toFixed(Math.max(0, p))\n if (sep !== '.')\n s = s.replace('.', sep)\n return s\n}\n\nfunction onInput(e: Event): void {\n const el = e.target as HTMLInputElement\n const raw = el.value\n const next = sanitize(raw)\n\n if (raw !== next) {\n // Сохраняем позицию каретки: считаем, сколько валидных символов было ДО\n // каретки, и ставим её после стольких же в очищенной строке — иначе при\n // редактировании середины числа каретка прыгала в конец.\n const caret = el.selectionStart ?? raw.length\n const keptBefore = sanitize(raw.slice(0, caret)).length\n el.value = next\n el.setSelectionRange(keptBefore, keptBefore)\n }\n\n draft.value = next\n\n const num = toNumber(next)\n if (num !== props.modelValue)\n emit('update:modelValue', num)\n}\n\n/**\n * Коммит набранного: границы и точность применяются здесь, а не на каждом\n * нажатии, — иначе `min=10` не дал бы набрать «1» как начало «15».\n */\nfunction commit(): void {\n // Без черновика коммитится сама модель: значение вне границ обязано\n // выправиться и тогда, когда его выставили снаружи, а не набрали руками.\n const num = draft.value !== null ? toNumber(draft.value) : props.modelValue\n const next = num === null ? null : clamp(normalize(num))\n\n draft.value = null\n\n if (next !== props.modelValue)\n emit('update:modelValue', next)\n emit('change', next)\n}\n\nfunction onChange(): void {\n // Клампинг границ (`min`/`max`) и нормализация — на `change`/`blur`, а не только\n // в кнопках: иначе ручной ввод «999» при `max=10` останется невалидным.\n commit()\n}\n\nfunction setValue(n: number): void {\n const next = clamp(normalize(n))\n // Шаг завершает набор: черновик снимается, показ считается от модели.\n draft.value = null\n emit('update:modelValue', next)\n emit('change', next)\n}\n\n/**\n * Шаг значения. Фокус не трогаем: кнопка ± обязана оставаться под фокусом,\n * иначе клавиатурный пользователь после первого Enter теряет её и повторно\n * нажать не может. Поле фокусирует тот, кто шагает от него самого.\n */\nfunction stepBy(dir: 1 | -1, step: number = props.step ?? 1): void {\n if (isDisabled.value || isReadonly.value)\n return\n\n const current = props.modelValue ?? 0\n setValue(addStep(current, step * dir))\n}\n\n// Интервал уже шагал: финальный `click` при отпускании — не намерение\n// пользователя сделать ещё один шаг, а хвост того же жеста.\nlet repeatFired = false\n\n/** Клик кнопки «±»: шаг, если это не хвост только что завершённого удержания. */\nfunction onStepClick(dir: 1 | -1, event: MouseEvent): void {\n // Enter/Space на кнопке дают click без указателя (`detail === 0`) — хвостом\n // жеста он быть не может. Иначе удержание, кончившееся без клика (отпускание\n // вне кнопки, `pointercancel` на таче), оставляло бы флаг взведённым, и\n // спинбаттон молча терял бы нажатие у клавиатуры и вспомогательных технологий.\n if (event.detail === 0) {\n repeatFired = false\n stepBy(dir)\n return\n }\n\n if (repeatFired) {\n repeatFired = false\n return\n }\n stepBy(dir)\n}\n\nfunction onKeydown(e: KeyboardEvent): void {\n if (isDisabled.value)\n return\n // Readonly-поле ведёт себя как текст: стрелки и Home/End отдаются нативной\n // каретке, значение не меняется.\n if (isReadonly.value)\n return\n\n switch (e.key) {\n case 'ArrowUp':\n e.preventDefault()\n stepBy(1)\n break\n case 'ArrowDown':\n e.preventDefault()\n stepBy(-1)\n break\n case 'PageUp':\n e.preventDefault()\n stepBy(1, bigStep(props.step ?? 1, props.min, props.max))\n break\n case 'PageDown':\n e.preventDefault()\n stepBy(-1, bigStep(props.step ?? 1, props.min, props.max))\n break\n case 'Home':\n if (props.min !== undefined) {\n e.preventDefault()\n setValue(props.min)\n }\n break\n case 'End':\n if (props.max !== undefined) {\n e.preventDefault()\n setValue(props.max)\n }\n break\n }\n}\n\n// `aria-valuenow` — числовое значение для скринридеров; отсутствует, если поле пусто.\nconst ariaValueNow = computed(() => props.modelValue ?? undefined)\n\n// На пределе кнопка ± гаснет: активная кнопка, у которой `clamp` съедает\n// результат, читается как сломанная.\nconst numericValue = computed(() => props.modelValue)\n\nconst canIncrease = computed(() => {\n if (props.max === undefined)\n return true\n const current = numericValue.value\n return current === null || normalize(current) < props.max\n})\n\nconst canDecrease = computed(() => {\n if (props.min === undefined)\n return true\n const current = numericValue.value\n return current === null || normalize(current) > props.min\n})\n\n// Локале-зависимое отображение: при фокусе показываем «сырое» значение для\n// редактирования, иначе — сгруппированное через `Intl.NumberFormat`.\nfunction formatGrouped(num: number): string {\n const sep = props.decimalSeparator || '.'\n // formatToParts позволяет оставить локале-зависимый групповой разделитель,\n // но принудительно подставить наш десятичный разделитель (`decimalSeparator`),\n // корректно работая для любой локали (в т.ч. de-DE, где группа — '.').\n return new Intl.NumberFormat(resolvedLocale.value, {\n useGrouping: true,\n minimumFractionDigits: props.precision ?? 0,\n maximumFractionDigits: props.precision ?? 20,\n })\n .formatToParts(num)\n .map(part => (part.type === 'decimal' ? sep : part.value))\n .join('')\n}\n\n/**\n * `aria-valuetext` нужен только при группировке: без неё `aria-valuenow` уже\n * несёт то же самое число, и дублировать его текстом незачем.\n */\nconst ariaValueText = computed(() => {\n if (!props.useGrouping)\n return undefined\n return props.modelValue === null ? undefined : formatGrouped(props.modelValue)\n})\n\n/**\n * Что показывает поле: набираемый черновик, пока он есть, иначе — модель.\n * Группировка только вне фокуса: при правке разделители разрядов мешают.\n */\nconst displayValue = computed(() => {\n if (draft.value !== null)\n return draft.value\n if (props.modelValue === null)\n return ''\n if (props.useGrouping && !isFocused.value)\n return formatGrouped(props.modelValue)\n return format(props.modelValue)\n})\n\nfunction onFocus(event: FocusEvent): void {\n isFocused.value = true\n emit('focus', event)\n}\n\nfunction onBlur(event: FocusEvent): void {\n isFocused.value = false\n // `change` до `blur` доходит не всегда (значение могло вернуться к исходному),\n // а черновик обязан сняться в любом случае — иначе поле застынет на нём.\n draft.value = null\n emit('blur', event)\n stopRepeat()\n}\n\n// ————— Удержание кнопки ±: шаг повторяется, пока кнопку держат.\nconst REPEAT_DELAY_MS = 400\nconst REPEAT_INTERVAL_MS = 60\n\nlet repeatDelay: ReturnType<typeof setTimeout> | null = null\nlet repeatTimer: ReturnType<typeof setInterval> | null = null\n\nfunction stopRepeat(): void {\n if (repeatDelay !== null) {\n clearTimeout(repeatDelay)\n repeatDelay = null\n }\n if (repeatTimer !== null) {\n clearInterval(repeatTimer)\n repeatTimer = null\n }\n}\n\nfunction canStep(dir: 1 | -1): boolean {\n return dir === 1 ? canIncrease.value : canDecrease.value\n}\n\n/**\n * `pointerdown`, а не `mousedown`: на тач-устройствах второго не бывает.\n * Повтор останавливается на границе — иначе таймер крутился бы вхолостую.\n */\nfunction startRepeat(dir: 1 | -1): void {\n if (isDisabled.value || isReadonly.value)\n return\n\n stopRepeat()\n repeatFired = false\n repeatDelay = setTimeout(() => {\n repeatTimer = setInterval(() => {\n if (!canStep(dir)) {\n stopRepeat()\n return\n }\n repeatFired = true\n stepBy(dir)\n }, REPEAT_INTERVAL_MS)\n }, REPEAT_DELAY_MS)\n}\n\nonBeforeUnmount(stopRepeat)\n\n// ————— Очистка значения.\nconst resolvedClearable = useGrComponentProp('GrNumberInput', 'clearable', () => props.clearable, false)\nconst resolvedClearLabel = computed(() => props.clearLabel ?? t('gr.input.clear', 'Clear'))\n\nconst clearVisible = computed(() =>\n resolvedClearable.value && !isDisabled.value && !isReadonly.value && props.modelValue !== null,\n)\n\nfunction clear(): void {\n draft.value = null\n emit('update:modelValue', null)\n emit('change', null)\n emit('clear')\n focus()\n}\n\nif (__GR_DEV__) {\n watchEffect(() => {\n if (props.modelValue === undefined) {\n console.warn(\n `[granularity] GrNumberInput: обязательный проп \\`modelValue\\` не передан — получено ${String(props.modelValue)}.`,\n )\n }\n })\n}\n</script>\n\n<template>\n <div\n data-gr-number-input\n class=\"relative w-full overflow-hidden rounded-[var(--gr-radius-control)] border bg-[var(--gr-bg)] transition-colors duration-[var(--gr-duration-fast)] focus-within:ring-2 focus-within:ring-[var(--gr-ring)]\"\n :class=\"shellClassName\"\n >\n <div\n v-if=\"$slots.prefix\"\n ref=\"prefixEl\"\n data-testid=\"number-input-prefix\"\n data-gr-number-input-prefix\n class=\"absolute inset-y-0 left-0 flex items-center justify-center border-r border-[var(--gr-brd)] px-2 text-[var(--gr-muted-fg)] pointer-events-none select-none truncate\"\n :style=\"prefixStyle\"\n aria-hidden=\"true\"\n >\n <slot name=\"prefix\" />\n </div>\n\n <input\n :id=\"resolvedId\"\n ref=\"inputEl\"\n v-bind=\"$attrs\"\n :name=\"name\"\n type=\"text\"\n :inputmode=\"inputmode\"\n :autocomplete=\"autocomplete\"\n :placeholder=\"placeholder\"\n :disabled=\"isDisabled\"\n :value=\"displayValue\"\n role=\"spinbutton\"\n :aria-valuenow=\"ariaValueNow\"\n :aria-valuemin=\"min\"\n :aria-valuemax=\"max\"\n :aria-valuetext=\"ariaValueText\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-describedby=\"describedBy\"\n :aria-required=\"isRequired ? 'true' : undefined\"\n :aria-readonly=\"isReadonly ? 'true' : undefined\"\n :aria-label=\"ariaLabel\"\n :readonly=\"isReadonly\"\n class=\"w-full bg-transparent text-[var(--gr-fg)] placeholder:text-[var(--gr-muted-fg)] focus:placeholder:text-transparent focus:outline-none disabled:cursor-not-allowed\"\n :class=\"inputClassName\"\n :style=\"inputStyle\"\n @input=\"onInput\"\n @change=\"onChange\"\n @keydown=\"onKeydown\"\n @focus=\"onFocus\"\n @blur=\"onBlur\"\n >\n\n <div\n v-if=\"$slots.suffix\"\n ref=\"suffixEl\"\n data-testid=\"number-input-suffix\"\n data-gr-number-input-suffix\n class=\"absolute inset-y-0 flex items-center justify-center border-l border-[var(--gr-brd)] px-2 text-[var(--gr-muted-fg)] pointer-events-none select-none truncate\"\n :class=\"suffixFixed ? '[direction:rtl]' : ''\"\n :style=\"suffixStyle\"\n aria-hidden=\"true\"\n >\n <slot name=\"suffix\" />\n </div>\n\n <button\n v-if=\"clearVisible\"\n type=\"button\"\n data-gr-number-input-clear\n :class=\"clearButtonClass\"\n :style=\"clearButtonStyle\"\n :aria-label=\"resolvedClearLabel\"\n @mousedown.prevent\n @click=\"clear\"\n >\n <GrIcon :size=\"14\">\n <IconX />\n </GrIcon>\n </button>\n\n <div\n v-if=\"hasVerticalControls\"\n data-testid=\"number-input-controls-vertical\"\n data-gr-number-input-controls=\"vertical\"\n class=\"absolute inset-y-0 flex items-center justify-center border-l border-[var(--gr-brd)]\"\n :style=\"verticalControlsStyle\"\n >\n <div class=\"flex flex-col justify-center gap-1\">\n <button\n type=\"button\"\n data-gr-number-input-increase\n :class=\"stepperCompactClass\"\n :disabled=\"isDisabled || isReadonly || !canIncrease\"\n :aria-label=\"resolvedIncreaseLabel\"\n @mousedown.prevent\n @pointerdown=\"startRepeat(1)\"\n @pointerup=\"stopRepeat\"\n @pointerleave=\"stopRepeat\"\n @pointercancel=\"stopRepeat\"\n @click=\"onStepClick(1, $event)\"\n >\n <GrIcon :size=\"12\">\n <IconChevronUp />\n </GrIcon>\n </button>\n <button\n type=\"button\"\n data-gr-number-input-decrease\n :class=\"stepperCompactClass\"\n :disabled=\"isDisabled || isReadonly || !canDecrease\"\n :aria-label=\"resolvedDecreaseLabel\"\n @mousedown.prevent\n @pointerdown=\"startRepeat(-1)\"\n @pointerup=\"stopRepeat\"\n @pointerleave=\"stopRepeat\"\n @pointercancel=\"stopRepeat\"\n @click=\"onStepClick(-1, $event)\"\n >\n <GrIcon :size=\"12\">\n <IconChevronDown />\n </GrIcon>\n </button>\n </div>\n </div>\n\n <div\n v-if=\"hasHorizontalControls\"\n data-testid=\"number-input-controls-horizontal-left\"\n data-gr-number-input-controls=\"horizontal-left\"\n class=\"absolute inset-y-0 flex items-stretch justify-center border-r border-[var(--gr-brd)]\"\n :style=\"horizontalLeftControlsStyle\"\n >\n <button\n type=\"button\"\n data-gr-number-input-decrease\n :class=\"stepperWideClass\"\n :disabled=\"isDisabled || isReadonly || !canDecrease\"\n :aria-label=\"resolvedDecreaseLabel\"\n @mousedown.prevent\n @pointerdown=\"startRepeat(-1)\"\n @pointerup=\"stopRepeat\"\n @pointerleave=\"stopRepeat\"\n @pointercancel=\"stopRepeat\"\n @click=\"onStepClick(-1, $event)\"\n >\n <GrIcon :size=\"12\">\n <IconChevronLeft />\n </GrIcon>\n </button>\n </div>\n\n <div\n v-if=\"hasHorizontalControls\"\n data-testid=\"number-input-controls-horizontal-right\"\n data-gr-number-input-controls=\"horizontal-right\"\n class=\"absolute inset-y-0 flex items-stretch justify-center border-l border-[var(--gr-brd)]\"\n :style=\"horizontalRightControlsStyle\"\n >\n <button\n type=\"button\"\n data-gr-number-input-increase\n :class=\"stepperWideClass\"\n :disabled=\"isDisabled || isReadonly || !canIncrease\"\n :aria-label=\"resolvedIncreaseLabel\"\n @mousedown.prevent\n @pointerdown=\"startRepeat(1)\"\n @pointerup=\"stopRepeat\"\n @pointerleave=\"stopRepeat\"\n @pointercancel=\"stopRepeat\"\n @click=\"onStepClick(1, $event)\"\n >\n <GrIcon :size=\"12\">\n <IconChevronRight />\n </GrIcon>\n </button>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport type { InputHTMLAttributes } from 'vue'\n\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { computed, onBeforeUnmount, ref, watchEffect } from 'vue'\n\nimport {\n clearButtonClass,\n stepperCompactClass,\n stepperWideClass,\n grNumberInputInputClass,\n grNumberInputShellClass,\n type GrNumberInputControlsDirection,\n type GrNumberInputSize,\n type GrNumberInputState,\n type GrNumberInputTextAlign,\n} from './grNumberInputStyles'\nimport { addStep, bigStep } from './numberInputMath'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport GrIcon from '../GrIcon/GrIcon.vue'\nimport IconChevronDown from '~icons/lucide/chevron-down'\nimport IconChevronLeft from '~icons/lucide/chevron-left'\nimport IconChevronRight from '~icons/lucide/chevron-right'\nimport IconChevronUp from '~icons/lucide/chevron-up'\nimport IconX from '~icons/lucide/x'\nimport { addLen } from '../../composables/internal/useAddonMeasurement'\nimport { useControlAddons } from '../../composables/internal/useControlAddons'\n\ndefineOptions({\n inheritAttrs: false,\n})\n\nconst ADDON_PX_BY_SIZE: Record<GrNumberInputSize, number> = {\n xs: 32,\n sm: 36,\n md: 40,\n lg: 48,\n}\n\nconst BASE_PADDING_X_LEN_BY_SIZE: Record<GrNumberInputSize, string> = {\n xs: '10px',\n sm: '12px',\n md: '12px',\n lg: '16px',\n}\n\nfunction px(n: number): string {\n return `${n}px`\n}\n\nexport interface GrNumberInputProps {\n /**\n * Значение поля. `null` — пусто.\n *\n * Незавершённый ввод («-», «1,») числом не является и в модель не попадает:\n * пока он набирается, поле держит его во внутреннем черновике, а модель\n * честно говорит «числа пока нет».\n */\n modelValue: number | null\n placeholder?: string\n autocomplete?: string\n inputmode?: InputHTMLAttributes['inputmode']\n disabled?: boolean\n /** Быстрый флаг невалидности; эквивалент `state='danger'` + `aria-invalid`. */\n invalid?: boolean\n /** Только для чтения: значение видно и уходит в форму, но не редактируется. */\n readonly?: boolean\n /** Обязательное поле (`aria-required`). Складывается с `required` у `GrFormField`. */\n required?: boolean\n /** Доступное имя вне `GrFormField`. */\n ariaLabel?: string\n state?: GrNumberInputState\n name?: string\n id?: string\n size?: GrNumberInputSize\n\n textAlign?: GrNumberInputTextAlign\n\n decimalSeparator?: string\n step?: number\n min?: number\n max?: number\n precision?: number\n\n /** Кнопка очистки значения. */\n clearable?: boolean\n /** A11y-подпись кнопки очистки. */\n clearLabel?: string\n /**\n * BCP-47 локаль для отображения значения (группировка разрядов и разделители\n * через `Intl.NumberFormat`). Работает вместе с `useGrouping`.\n */\n locale?: string\n /**\n * Группировать разряды при отображении (когда поле не в фокусе). При фокусе\n * показывается «сырое» значение для редактирования. По умолчанию выключено.\n */\n useGrouping?: boolean\n\n /** Показывать кнопки +/-. */\n controls?: boolean\n controlsDirection?: GrNumberInputControlsDirection\n\n prefixMinWidth?: string\n prefixMaxWidth?: string\n suffixMinWidth?: string\n suffixMaxWidth?: string\n /**\n * Фиксированная ширина у prefix/suffix: жёсткая ширина (из `*MaxWidth` →\n * `*MinWidth` → дефолт) + обрезка контента по краю (prefix — справа,\n * suffix — слева). По умолчанию аддоны растягиваются под контент.\n */\n prefixFixed?: boolean\n suffixFixed?: boolean\n\n /** i18n-friendly aria-label для кнопки \"увеличить\". */\n increaseLabel?: string\n /** i18n-friendly aria-label для кнопки \"уменьшить\". */\n decreaseLabel?: string\n}\n\nexport interface GrNumberInputEmits {\n (e: 'update:modelValue', value: number | null): void\n (e: 'change', value: number | null): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n /** Значение стёрто кнопкой очистки. */\n (e: 'clear'): void\n}\n\n// Контекст `GrFormField` — fallback для id/описания/невалидности, как в `GrInput`.\nconst field = useGrFormFieldContext()\n\nconst props = withDefaults(defineProps<GrNumberInputProps>(), {\n placeholder: undefined,\n autocomplete: undefined,\n inputmode: 'decimal',\n disabled: false,\n invalid: false,\n readonly: false,\n required: false,\n ariaLabel: undefined,\n state: 'default',\n name: undefined,\n id: undefined,\n size: undefined,\n\n textAlign: 'left',\n\n decimalSeparator: '.',\n step: 1,\n min: undefined,\n max: undefined,\n precision: undefined,\n locale: undefined,\n useGrouping: false,\n clearable: undefined,\n clearLabel: undefined,\n\n controls: false,\n controlsDirection: 'vertical',\n\n prefixMinWidth: undefined,\n prefixMaxWidth: undefined,\n suffixMinWidth: undefined,\n suffixMaxWidth: undefined,\n prefixFixed: false,\n suffixFixed: false,\n\n increaseLabel: undefined,\n decreaseLabel: undefined,\n})\n\n// Эффективный размер: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrNumberInput' })\n\nconst resolvedId = computed(() => props.id ?? field?.id.value)\nconst describedBy = computed(() => field?.describedById.value)\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n} = useGrFormControl(() => props)\n\nconst emit = defineEmits<GrNumberInputEmits>()\ndefineSlots<{\n /** Аддон слева от поля: знак валюты, иконка. */\n prefix?: () => any\n /** Аддон справа от поля: единица измерения. */\n suffix?: () => any\n}>()\n\nconst inputEl = ref<HTMLInputElement | null>(null)\n\n// В фокусе поле показывает «сырое» значение: группировка мешала бы правке.\nconst isFocused = ref(false)\n\n/**\n * Строка ровно в том виде, в каком её набирают. Модель к этому моменту может\n * быть `null` («-», «1,» — не числа), и без черновика поле стирало бы под\n * пальцами то, что печатают. Коммит (`change`/`blur`) черновик снимает.\n */\nconst draft = ref<string | null>(null)\n\nfunction focus(): void {\n inputEl.value?.focus()\n}\n\nfunction blur(): void {\n inputEl.value?.blur()\n}\n\ndefineExpose({ focus, blur })\n\nconst { t, locale } = useGranularityTranslations()\n\n/**\n * Локаль форматирования: свой проп → локаль i18n-адаптера → окружение. Без\n * второго шага мультиязычное приложение передавало бы `locale` каждому полю.\n */\nconst resolvedLocale = computed(() => props.locale ?? locale.value)\nconst resolvedIncreaseLabel = computed(() => props.increaseLabel ?? t('gr.numberInput.increase', 'Increase'))\nconst resolvedDecreaseLabel = computed(() => props.decreaseLabel ?? t('gr.numberInput.decrease', 'Decrease'))\n\nconst hasHorizontalControls = computed(() => props.controls && props.controlsDirection === 'horizontal')\nconst hasVerticalControls = computed(() => props.controls && props.controlsDirection === 'vertical')\n\nconst addonPx = computed(() => ADDON_PX_BY_SIZE[resolvedSize.value])\nconst addonLen = computed(() => px(addonPx.value))\nconst basePaddingXLen = computed(() => BASE_PADDING_X_LEN_BY_SIZE[resolvedSize.value])\n\n/** Кнопки ± справа: вертикальный стек — одна колонка, горизонтальный — вторая. */\nconst rightControlsCount = computed(() =>\n (hasHorizontalControls.value ? 1 : 0) + (hasVerticalControls.value ? 1 : 0))\n\nconst {\n prefixEl,\n suffixEl,\n prefixLen,\n suffixLen,\n prefixStyle,\n suffixStyle,\n fieldPadding: inputStyle,\n} = useControlAddons(() => props, {\n defaultMinWidth: () => addonLen.value,\n paddingX: () => basePaddingXLen.value,\n leadingReserve: () => (hasHorizontalControls.value ? addonLen.value : '0px'),\n trailingReserve: () => (rightControlsCount.value > 0 ? px(addonPx.value * rightControlsCount.value) : '0px'),\n // Суффикс позиционируется абсолютно: без якоря он уехал бы от правого края.\n anchorSuffixRight: true,\n})\n\nfunction addonStyle(side: 'left' | 'right', offset: string): Record<string, string> {\n return {\n width: addonLen.value,\n [side]: offset,\n }\n}\n\nconst verticalControlsStyle = computed(() => addonStyle('right', suffixLen.value))\n\n/**\n * Крестик очистки живёт левее правой зоны (суффикс + кнопки ±) — иначе он лёг бы\n * на них. Ширины там же, где их считает поле, поэтому переиспользуем расчёт.\n */\nconst clearButtonStyle = computed(() => {\n const controls = rightControlsCount.value > 0 ? px(addonPx.value * rightControlsCount.value) : '0px'\n\n return { right: addLen(addLen(suffixLen.value, controls), '6px') }\n})\nconst horizontalLeftControlsStyle = computed(() => addonStyle('left', prefixLen.value))\nconst horizontalRightControlsStyle = computed(() => addonStyle('right', suffixLen.value))\n\nconst shellClassName = computed(() => {\n return grNumberInputShellClass({\n disabled: isDisabled.value,\n state: props.state,\n invalid: isInvalid.value,\n })\n})\n\nconst inputClassName = computed(() => {\n return grNumberInputInputClass({\n size: resolvedSize.value,\n textAlign: props.textAlign,\n })\n})\n\nfunction isDigit(ch: string): boolean {\n return ch >= '0' && ch <= '9'\n}\n\nfunction sanitize(raw: string): string {\n const sep = props.decimalSeparator || '.'\n\n let out = ''\n let hasSepLocal = false\n\n for (let i = 0; i < raw.length; i++) {\n const ch = raw[i]\n\n // Ведущий минус: разрешаем один в начале, чтобы можно было ввести\n // отрицательное значение с клавиатуры (границы проверит `clamp` на change).\n if (ch === '-' && out.length === 0) {\n out += '-'\n continue\n }\n\n if (isDigit(ch)) {\n out += ch\n continue\n }\n\n if ((ch === sep || ch === '.' || ch === ',') && !hasSepLocal) {\n out += sep\n hasSepLocal = true\n }\n }\n\n return out\n}\n\nfunction toNumber(value: string): number | null {\n if (value.trim() === '')\n return null\n const sep = props.decimalSeparator || '.'\n const normalized = sep === '.' ? value : value.replace(sep, '.')\n const n = Number(normalized)\n if (!Number.isFinite(n))\n return null\n return n\n}\n\nfunction clamp(v: number): number {\n if (props.min !== undefined)\n v = Math.max(props.min, v)\n if (props.max !== undefined)\n v = Math.min(props.max, v)\n return v\n}\n\nfunction normalize(v: number): number {\n if (props.precision === undefined)\n return v\n const p = Math.max(0, props.precision)\n return Number(v.toFixed(p))\n}\n\nfunction format(n: number): string {\n const sep = props.decimalSeparator || '.'\n const p = props.precision\n\n // Без `precision` избегаем экспоненциальной записи (`String(1e21)` → `\"1e+21\"`),\n // форматируя через `Intl` без группировки разрядов.\n let s = p === undefined\n ? n.toLocaleString('en-US', { useGrouping: false, maximumFractionDigits: 20 })\n : n.toFixed(Math.max(0, p))\n if (sep !== '.')\n s = s.replace('.', sep)\n return s\n}\n\nfunction onInput(e: Event): void {\n const el = e.target as HTMLInputElement\n const raw = el.value\n const next = sanitize(raw)\n\n if (raw !== next) {\n // Сохраняем позицию каретки: считаем, сколько валидных символов было ДО\n // каретки, и ставим её после стольких же в очищенной строке — иначе при\n // редактировании середины числа каретка прыгала в конец.\n const caret = el.selectionStart ?? raw.length\n const keptBefore = sanitize(raw.slice(0, caret)).length\n el.value = next\n el.setSelectionRange(keptBefore, keptBefore)\n }\n\n draft.value = next\n\n const num = toNumber(next)\n if (num !== props.modelValue)\n emit('update:modelValue', num)\n}\n\n/**\n * Коммит набранного: границы и точность применяются здесь, а не на каждом\n * нажатии, — иначе `min=10` не дал бы набрать «1» как начало «15».\n */\nfunction commit(): void {\n // Без черновика коммитится сама модель: значение вне границ обязано\n // выправиться и тогда, когда его выставили снаружи, а не набрали руками.\n const num = draft.value !== null ? toNumber(draft.value) : props.modelValue\n const next = num === null ? null : clamp(normalize(num))\n\n draft.value = null\n\n if (next !== props.modelValue)\n emit('update:modelValue', next)\n emit('change', next)\n}\n\nfunction onChange(): void {\n // Клампинг границ (`min`/`max`) и нормализация — на `change`/`blur`, а не только\n // в кнопках: иначе ручной ввод «999» при `max=10` останется невалидным.\n commit()\n}\n\nfunction setValue(n: number): void {\n const next = clamp(normalize(n))\n // Шаг завершает набор: черновик снимается, показ считается от модели.\n draft.value = null\n emit('update:modelValue', next)\n emit('change', next)\n}\n\n/**\n * Шаг значения. Фокус не трогаем: кнопка ± обязана оставаться под фокусом,\n * иначе клавиатурный пользователь после первого Enter теряет её и повторно\n * нажать не может. Поле фокусирует тот, кто шагает от него самого.\n */\nfunction stepBy(dir: 1 | -1, step: number = props.step ?? 1): void {\n if (isDisabled.value || isReadonly.value)\n return\n\n const current = props.modelValue ?? 0\n setValue(addStep(current, step * dir))\n}\n\n// Интервал уже шагал: финальный `click` при отпускании — не намерение\n// пользователя сделать ещё один шаг, а хвост того же жеста.\nlet repeatFired = false\n\n/** Клик кнопки «±»: шаг, если это не хвост только что завершённого удержания. */\nfunction onStepClick(dir: 1 | -1, event: MouseEvent): void {\n // Enter/Space на кнопке дают click без указателя (`detail === 0`) — хвостом\n // жеста он быть не может. Иначе удержание, кончившееся без клика (отпускание\n // вне кнопки, `pointercancel` на таче), оставляло бы флаг взведённым, и\n // спинбаттон молча терял бы нажатие у клавиатуры и вспомогательных технологий.\n if (event.detail === 0) {\n repeatFired = false\n stepBy(dir)\n return\n }\n\n if (repeatFired) {\n repeatFired = false\n return\n }\n stepBy(dir)\n}\n\nfunction onKeydown(e: KeyboardEvent): void {\n if (isDisabled.value)\n return\n // Readonly-поле ведёт себя как текст: стрелки и Home/End отдаются нативной\n // каретке, значение не меняется.\n if (isReadonly.value)\n return\n\n switch (e.key) {\n case 'ArrowUp':\n e.preventDefault()\n stepBy(1)\n break\n case 'ArrowDown':\n e.preventDefault()\n stepBy(-1)\n break\n case 'PageUp':\n e.preventDefault()\n stepBy(1, bigStep(props.step ?? 1, props.min, props.max))\n break\n case 'PageDown':\n e.preventDefault()\n stepBy(-1, bigStep(props.step ?? 1, props.min, props.max))\n break\n case 'Home':\n if (props.min !== undefined) {\n e.preventDefault()\n setValue(props.min)\n }\n break\n case 'End':\n if (props.max !== undefined) {\n e.preventDefault()\n setValue(props.max)\n }\n break\n }\n}\n\n// `aria-valuenow` — числовое значение для скринридеров; отсутствует, если поле пусто.\nconst ariaValueNow = computed(() => props.modelValue ?? undefined)\n\n// На пределе кнопка ± гаснет: активная кнопка, у которой `clamp` съедает\n// результат, читается как сломанная.\nconst numericValue = computed(() => props.modelValue)\n\nconst canIncrease = computed(() => {\n if (props.max === undefined)\n return true\n const current = numericValue.value\n return current === null || normalize(current) < props.max\n})\n\nconst canDecrease = computed(() => {\n if (props.min === undefined)\n return true\n const current = numericValue.value\n return current === null || normalize(current) > props.min\n})\n\n// Локале-зависимое отображение: при фокусе показываем «сырое» значение для\n// редактирования, иначе — сгруппированное через `Intl.NumberFormat`.\nfunction formatGrouped(num: number): string {\n const sep = props.decimalSeparator || '.'\n // formatToParts позволяет оставить локале-зависимый групповой разделитель,\n // но принудительно подставить наш десятичный разделитель (`decimalSeparator`),\n // корректно работая для любой локали (в т.ч. de-DE, где группа — '.').\n return new Intl.NumberFormat(resolvedLocale.value, {\n useGrouping: true,\n minimumFractionDigits: props.precision ?? 0,\n maximumFractionDigits: props.precision ?? 20,\n })\n .formatToParts(num)\n .map(part => (part.type === 'decimal' ? sep : part.value))\n .join('')\n}\n\n/**\n * `aria-valuetext` нужен только при группировке: без неё `aria-valuenow` уже\n * несёт то же самое число, и дублировать его текстом незачем.\n */\nconst ariaValueText = computed(() => {\n if (!props.useGrouping)\n return undefined\n return props.modelValue === null ? undefined : formatGrouped(props.modelValue)\n})\n\n/**\n * Что показывает поле: набираемый черновик, пока он есть, иначе — модель.\n * Группировка только вне фокуса: при правке разделители разрядов мешают.\n */\nconst displayValue = computed(() => {\n if (draft.value !== null)\n return draft.value\n if (props.modelValue === null)\n return ''\n if (props.useGrouping && !isFocused.value)\n return formatGrouped(props.modelValue)\n return format(props.modelValue)\n})\n\nfunction onFocus(event: FocusEvent): void {\n isFocused.value = true\n emit('focus', event)\n}\n\nfunction onBlur(event: FocusEvent): void {\n isFocused.value = false\n // `change` до `blur` доходит не всегда (значение могло вернуться к исходному),\n // а черновик обязан сняться в любом случае — иначе поле застынет на нём.\n draft.value = null\n emit('blur', event)\n stopRepeat()\n}\n\n// ————— Удержание кнопки ±: шаг повторяется, пока кнопку держат.\nconst REPEAT_DELAY_MS = 400\nconst REPEAT_INTERVAL_MS = 60\n\nlet repeatDelay: ReturnType<typeof setTimeout> | null = null\nlet repeatTimer: ReturnType<typeof setInterval> | null = null\n\nfunction stopRepeat(): void {\n if (repeatDelay !== null) {\n clearTimeout(repeatDelay)\n repeatDelay = null\n }\n if (repeatTimer !== null) {\n clearInterval(repeatTimer)\n repeatTimer = null\n }\n}\n\nfunction canStep(dir: 1 | -1): boolean {\n return dir === 1 ? canIncrease.value : canDecrease.value\n}\n\n/**\n * `pointerdown`, а не `mousedown`: на тач-устройствах второго не бывает.\n * Повтор останавливается на границе — иначе таймер крутился бы вхолостую.\n */\nfunction startRepeat(dir: 1 | -1): void {\n if (isDisabled.value || isReadonly.value)\n return\n\n stopRepeat()\n repeatFired = false\n repeatDelay = setTimeout(() => {\n repeatTimer = setInterval(() => {\n if (!canStep(dir)) {\n stopRepeat()\n return\n }\n repeatFired = true\n stepBy(dir)\n }, REPEAT_INTERVAL_MS)\n }, REPEAT_DELAY_MS)\n}\n\nonBeforeUnmount(stopRepeat)\n\n// ————— Очистка значения.\nconst resolvedClearable = useGrComponentProp('GrNumberInput', 'clearable', () => props.clearable, false)\nconst resolvedClearLabel = computed(() => props.clearLabel ?? t('gr.input.clear', 'Clear'))\n\nconst clearVisible = computed(() =>\n resolvedClearable.value && !isDisabled.value && !isReadonly.value && props.modelValue !== null,\n)\n\nfunction clear(): void {\n draft.value = null\n emit('update:modelValue', null)\n emit('change', null)\n emit('clear')\n focus()\n}\n\nif (__GR_DEV__) {\n watchEffect(() => {\n if (props.modelValue === undefined) {\n console.warn(\n `[granularity] GrNumberInput: обязательный проп \\`modelValue\\` не передан — получено ${String(props.modelValue)}.`,\n )\n }\n })\n}\n</script>\n\n<template>\n <div\n data-gr-number-input\n class=\"relative w-full overflow-hidden rounded-[var(--gr-radius-control)] border bg-[var(--gr-bg)] transition-colors duration-[var(--gr-duration-fast)] focus-within:ring-2 focus-within:ring-[var(--gr-ring)]\"\n :class=\"shellClassName\"\n >\n <div\n v-if=\"$slots.prefix\"\n ref=\"prefixEl\"\n data-testid=\"number-input-prefix\"\n data-gr-number-input-prefix\n class=\"absolute inset-y-0 left-0 flex items-center justify-center border-r border-[var(--gr-brd)] px-2 text-[var(--gr-muted-fg)] pointer-events-none select-none truncate\"\n :style=\"prefixStyle\"\n aria-hidden=\"true\"\n >\n <slot name=\"prefix\" />\n </div>\n\n <input\n :id=\"resolvedId\"\n ref=\"inputEl\"\n v-bind=\"$attrs\"\n :name=\"name\"\n type=\"text\"\n :inputmode=\"inputmode\"\n :autocomplete=\"autocomplete\"\n :placeholder=\"placeholder\"\n :disabled=\"isDisabled\"\n :value=\"displayValue\"\n role=\"spinbutton\"\n :aria-valuenow=\"ariaValueNow\"\n :aria-valuemin=\"min\"\n :aria-valuemax=\"max\"\n :aria-valuetext=\"ariaValueText\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-describedby=\"describedBy\"\n :aria-required=\"isRequired ? 'true' : undefined\"\n :aria-readonly=\"isReadonly ? 'true' : undefined\"\n :aria-label=\"ariaLabel\"\n :readonly=\"isReadonly\"\n class=\"w-full bg-transparent text-[var(--gr-fg)] placeholder:text-[var(--gr-muted-fg)] focus:placeholder:text-transparent focus:outline-none disabled:cursor-not-allowed\"\n :class=\"inputClassName\"\n :style=\"inputStyle\"\n @input=\"onInput\"\n @change=\"onChange\"\n @keydown=\"onKeydown\"\n @focus=\"onFocus\"\n @blur=\"onBlur\"\n >\n\n <div\n v-if=\"$slots.suffix\"\n ref=\"suffixEl\"\n data-testid=\"number-input-suffix\"\n data-gr-number-input-suffix\n class=\"absolute inset-y-0 flex items-center justify-center border-l border-[var(--gr-brd)] px-2 text-[var(--gr-muted-fg)] pointer-events-none select-none truncate\"\n :class=\"suffixFixed ? '[direction:rtl]' : ''\"\n :style=\"suffixStyle\"\n aria-hidden=\"true\"\n >\n <slot name=\"suffix\" />\n </div>\n\n <button\n v-if=\"clearVisible\"\n type=\"button\"\n data-gr-number-input-clear\n :class=\"clearButtonClass\"\n :style=\"clearButtonStyle\"\n :aria-label=\"resolvedClearLabel\"\n @mousedown.prevent\n @click=\"clear\"\n >\n <GrIcon :size=\"14\">\n <IconX />\n </GrIcon>\n </button>\n\n <div\n v-if=\"hasVerticalControls\"\n data-testid=\"number-input-controls-vertical\"\n data-gr-number-input-controls=\"vertical\"\n class=\"absolute inset-y-0 flex items-center justify-center border-l border-[var(--gr-brd)]\"\n :style=\"verticalControlsStyle\"\n >\n <div class=\"flex flex-col justify-center gap-1\">\n <button\n type=\"button\"\n data-gr-number-input-increase\n :class=\"stepperCompactClass\"\n :disabled=\"isDisabled || isReadonly || !canIncrease\"\n :aria-label=\"resolvedIncreaseLabel\"\n @mousedown.prevent\n @pointerdown=\"startRepeat(1)\"\n @pointerup=\"stopRepeat\"\n @pointerleave=\"stopRepeat\"\n @pointercancel=\"stopRepeat\"\n @click=\"onStepClick(1, $event)\"\n >\n <GrIcon :size=\"12\">\n <IconChevronUp />\n </GrIcon>\n </button>\n <button\n type=\"button\"\n data-gr-number-input-decrease\n :class=\"stepperCompactClass\"\n :disabled=\"isDisabled || isReadonly || !canDecrease\"\n :aria-label=\"resolvedDecreaseLabel\"\n @mousedown.prevent\n @pointerdown=\"startRepeat(-1)\"\n @pointerup=\"stopRepeat\"\n @pointerleave=\"stopRepeat\"\n @pointercancel=\"stopRepeat\"\n @click=\"onStepClick(-1, $event)\"\n >\n <GrIcon :size=\"12\">\n <IconChevronDown />\n </GrIcon>\n </button>\n </div>\n </div>\n\n <div\n v-if=\"hasHorizontalControls\"\n data-testid=\"number-input-controls-horizontal-left\"\n data-gr-number-input-controls=\"horizontal-left\"\n class=\"absolute inset-y-0 flex items-stretch justify-center border-r border-[var(--gr-brd)]\"\n :style=\"horizontalLeftControlsStyle\"\n >\n <button\n type=\"button\"\n data-gr-number-input-decrease\n :class=\"stepperWideClass\"\n :disabled=\"isDisabled || isReadonly || !canDecrease\"\n :aria-label=\"resolvedDecreaseLabel\"\n @mousedown.prevent\n @pointerdown=\"startRepeat(-1)\"\n @pointerup=\"stopRepeat\"\n @pointerleave=\"stopRepeat\"\n @pointercancel=\"stopRepeat\"\n @click=\"onStepClick(-1, $event)\"\n >\n <GrIcon :size=\"12\">\n <IconChevronLeft />\n </GrIcon>\n </button>\n </div>\n\n <div\n v-if=\"hasHorizontalControls\"\n data-testid=\"number-input-controls-horizontal-right\"\n data-gr-number-input-controls=\"horizontal-right\"\n class=\"absolute inset-y-0 flex items-stretch justify-center border-l border-[var(--gr-brd)]\"\n :style=\"horizontalRightControlsStyle\"\n >\n <button\n type=\"button\"\n data-gr-number-input-increase\n :class=\"stepperWideClass\"\n :disabled=\"isDisabled || isReadonly || !canIncrease\"\n :aria-label=\"resolvedIncreaseLabel\"\n @mousedown.prevent\n @pointerdown=\"startRepeat(1)\"\n @pointerup=\"stopRepeat\"\n @pointerleave=\"stopRepeat\"\n @pointercancel=\"stopRepeat\"\n @click=\"onStepClick(1, $event)\"\n >\n <GrIcon :size=\"12\">\n <IconChevronRight />\n </GrIcon>\n </button>\n </div>\n </div>\n</template>\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
1
  import { n as grNumberInputSafelist, t as grNumberInputConfig } from "../../chunks/config-MmPym-Z7.js";
2
- import { t as GrNumberInput_default } from "./chunks/GrNumberInput-BqPydXpK.js";
2
+ import { t as GrNumberInput_default } from "./chunks/GrNumberInput-Bm-V59TG.js";
3
3
  export { GrNumberInput_default as GrNumberInput, GrNumberInput_default as default, grNumberInputConfig, grNumberInputSafelist };
@@ -5,7 +5,7 @@ import { useGrFormControl } from "../../../composables/useGrFormControl.js";
5
5
  import { t as useFocusWithin } from "../../../chunks/useFocusWithin-BwHE0rzm.js";
6
6
  import { useRovingFocus } from "../../../composables/useRovingFocus.js";
7
7
  import { a as grSegmentedItemIconClass, c as grSegmentedRootClass, i as grSegmentedItemClass, l as grSegmentedRootStyle, o as grSegmentedItemLabelClass, r as grSegmentedIndicatorClass, s as grSegmentedItemSpinnerClass } from "../../../chunks/config-rLkc9Zn2.js";
8
- import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, defineComponent, nextTick, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, openBlock, ref, renderList, renderSlot, resolveDynamicComponent, toDisplayString, unref, useId, watch } from "vue";
8
+ import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, defineComponent, nextTick, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, openBlock, ref, renderList, renderSlot, resolveDynamicComponent, toDisplayString, unref, useId, watch, watchEffect } from "vue";
9
9
  //#region src/components/shared/truncationTitle.ts
10
10
  /**
11
11
  * Полный текст обрезанной строки — нативной подсказкой по наведению.
@@ -328,6 +328,10 @@ var GrSegmented_default = /* @__PURE__ */ defineComponent({
328
328
  observedRoot = null;
329
329
  observedItem = null;
330
330
  });
331
+ if (process.env.NODE_ENV !== "production") watchEffect(() => {
332
+ if (props.modelValue === void 0) console.warn(`[granularity] GrSegmented: обязательный проп \`modelValue\` не передан — получено ${String(props.modelValue)}.`);
333
+ if (!Array.isArray(props.options)) console.warn(`[granularity] GrSegmented: обязательный проп \`options\` должен быть массивом — получено ${String(props.options)}.`);
334
+ });
331
335
  return (_ctx, _cache) => {
332
336
  return openBlock(), createElementBlock("div", {
333
337
  id: fieldId.value,
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GrSegmented-CuDaKA6S.js","names":[],"sources":["../../../../src/components/shared/truncationTitle.ts","../../../../src/components/GrSegmented/GrSegmented.vue","../../../../src/components/GrSegmented/GrSegmented.vue"],"sourcesContent":["/**\n * Полный текст обрезанной строки — нативной подсказкой по наведению.\n *\n * `truncate` прячет хвост **визуально**: в DOM и для скринридера подпись\n * остаётся целой, поэтому теряет её ровно один читатель — тот, кто смотрит\n * глазами. Он видит «Включе…» и узнать остальное не может ничем.\n *\n * Подсказка ставится **по факту обрезки**, а не всегда: тултип, дублирующий\n * видимую целиком подпись, — шум, которого никто не просил. Замер делается на\n * самом наведении, поэтому ни наблюдателей, ни подписок заводить не нужно.\n *\n * Ширина сравнивается с запасом в пиксель: браузеры считают `scrollWidth`\n * округлением вверх, и у необрезанной строки он бывает на единицу больше\n * `clientWidth` — без запаса подсказка появлялась бы на ровном месте.\n */\nexport function titleWhenTruncated(event: Event): void {\n const element = event.currentTarget\n\n if (!(element instanceof HTMLElement))\n return\n\n // У поля ввода видимая строка живёт в `value`, а не в содержимом узла.\n const source = element instanceof HTMLInputElement ? element.value : element.textContent\n const text = source?.trim() ?? ''\n\n if (text && element.scrollWidth > element.clientWidth + 1)\n element.setAttribute('title', text)\n else\n element.removeAttribute('title')\n}\n","<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, useId, watch, watchEffect } from 'vue'\n\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useRovingFocus } from '../../composables/useRovingFocus'\nimport { useFocusWithin } from '../../composables/internal/useFocusWithin'\nimport { titleWhenTruncated } from '../shared/truncationTitle'\n\nimport type { ComponentPublicInstance } from 'vue'\nimport IconLoader from '~icons/lucide/loader-circle'\n\nimport {\n grSegmentedIndicatorClass,\n grSegmentedItemClass,\n grSegmentedItemIconClass,\n grSegmentedItemLabelClass,\n grSegmentedItemSpinnerClass,\n grSegmentedRootClass,\n grSegmentedRootStyle,\n type GrSegmentedOption,\n type GrSegmentedOrientation,\n type GrSegmentedSize,\n type GrSegmentedValue,\n type GrSegmentedVariant,\n} from './grSegmentedStyles'\n\ntype IndicatorGeometry = {\n width: number\n height: number\n x: number\n y: number\n}\n\n/**\n * Пропсы публичного GR-примитива «Segmented».\n */\nexport interface GrSegmentedProps {\n modelValue: GrSegmentedValue\n options: GrSegmentedOption[]\n variant?: GrSegmentedVariant\n size?: GrSegmentedSize\n /**\n * Направление ряда. Вертикаль — боковые фильтры; индикатор к ней готов по\n * построению, он двумерный.\n */\n orientation?: GrSegmentedOrientation\n /** Длительность анимации индикатора в мс. */\n indicatorDuration?: number\n /** Растягивать сегмент на всю ширину контейнера. */\n block?: boolean\n disabled?: boolean\n /** Только для чтения: выбор видно, но он не меняется. */\n readonly?: boolean\n /** Визуальное и ARIA-состояние ошибки. */\n invalid?: boolean\n /** Обязательное поле (`aria-required`). */\n required?: boolean\n /** Имя скрытого поля, которым выбранное значение уходит в нативную форму. */\n name?: string\n ariaLabel?: string\n}\n\nexport interface GrSegmentedEmits {\n (e: 'update:modelValue', value: GrSegmentedValue): void\n (e: 'change', value: GrSegmentedValue, option: GrSegmentedOption): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrSegmentedProps>(),\n {\n variant: undefined,\n size: undefined,\n orientation: 'horizontal',\n indicatorDuration: 300,\n block: false,\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n name: undefined,\n ariaLabel: undefined,\n },\n)\n\n// Эффективный размер: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrSegmented' })\nconst resolvedVariant = useGrComponentProp('GrSegmented', 'variant', () => props.variant, 'pills')\n\nconst emit = defineEmits<GrSegmentedEmits>()\ndefineSlots<{\n /** Содержимое сегмента вместо подписи из `options`. */\n default?: (props: {\n option: GrSegmentedOption\n selected: boolean\n disabled: boolean\n loading: boolean\n }) => any\n}>()\n\n// `useId()` стабилен между сервером и клиентом, в отличие от `instance.uid`.\nconst fallbackName = `gr-segmented-${useId()}`\n\n// Контекст `GrFormField` + общий контракт форм-контрола.\nconst field = useGrFormFieldContext()\nconst fieldId = computed(() => field?.id.value)\nconst describedBy = computed(() => field?.describedById.value)\nconst labelledBy = computed(() => (props.ariaLabel ? undefined : field?.labelId.value))\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n} = useGrFormControl(() => props)\n\nconst rootRef = ref<HTMLElement | null>(null)\n\n// Фокус ходит между сегментами: без границы каждая стрелка давала бы\n// потребителю пару `blur` + `focus`.\nconst { onFocusIn, onFocusOut } = useFocusWithin(rootRef, {\n enter: event => emit('focus', event),\n leave: event => emit('blur', event),\n})\n\ndefineExpose({ focus, blur })\nconst itemRefs = ref(new Map<string, HTMLElement>())\nconst indicatorGeometry = ref<IndicatorGeometry | null>(null)\nconst indicatorReady = ref(false)\n\nlet resizeObserver: ResizeObserver | null = null\nlet observedRoot: HTMLElement | null = null\nlet observedItem: HTMLElement | null = null\nlet scheduled = false\n\nconst selectedIndex = computed(() => props.options.findIndex(option => option.value === props.modelValue))\nconst selectedOption = computed(() => props.options[selectedIndex.value] ?? null)\nconst indicatorDuration = computed(() => Math.max(0, Math.round(props.indicatorDuration)))\nconst isVertical = computed(() => props.orientation === 'vertical')\nconst rootClassName = computed(() => grSegmentedRootClass({\n variant: resolvedVariant.value,\n orientation: props.orientation,\n block: props.block,\n disabled: isDisabled.value,\n}))\n\n/**\n * Раскладка задаётся треками, а не классами: число сегментов известно только в\n * рантайме. По вертикали колонка одна — сегменты одинаковой ширины по\n * построению, и `block` решает лишь, занимать ли ширину контейнера.\n */\nconst rootStyle = computed<Record<string, string>>(() => {\n const empty = props.options.length === 0\n const track = () => props.block && !isVertical.value ? 'minmax(0,1fr)' : 'minmax(0,max-content)'\n\n return {\n ...grSegmentedRootStyle({\n variant: resolvedVariant.value,\n size: resolvedSize.value,\n orientation: props.orientation,\n }),\n gridTemplateColumns: isVertical.value\n ? 'minmax(0,1fr)'\n : empty ? 'none' : props.options.map(track).join(' '),\n ...(isVertical.value\n ? { gridTemplateRows: empty ? 'none' : props.options.map(track).join(' ') }\n : {}),\n }\n})\nconst indicatorClassName = computed(() => grSegmentedIndicatorClass(resolvedVariant.value))\nconst resolvedName = computed(() => props.name ?? fallbackName)\nconst hasIndicator = computed(() => selectedIndex.value !== -1 && indicatorGeometry.value !== null)\n\nconst indicatorStyle = computed<Record<string, string>>(() => {\n const geometry = indicatorGeometry.value\n if (!geometry) {\n return {\n opacity: '0',\n width: '0px',\n height: '0px',\n transitionDuration: `${indicatorDuration.value}ms`,\n transform: 'translate3d(0px, 0px, 0px)',\n }\n }\n\n return {\n opacity: indicatorReady.value ? '1' : '0',\n width: `${geometry.width}px`,\n height: `${geometry.height}px`,\n transitionDuration: `${indicatorDuration.value}ms`,\n transform: `translate3d(${geometry.x}px, ${geometry.y}px, 0)`,\n }\n})\n\nfunction getOptionKey(value: GrSegmentedValue): string {\n return String(value)\n}\n\nfunction resolveOptionDisabled(option: GrSegmentedOption): boolean {\n return isDisabled.value || Boolean(option.disabled)\n}\n\n/**\n * Занятый сегмент не «недоступен», а работает: нативный `disabled` и\n * `aria-disabled` ему не ставим — состояние несёт `aria-busy`. Но выбор он не\n * принимает, поэтому блокировка считается отдельно от `disabled`.\n */\nfunction isOptionBusy(option: GrSegmentedOption): boolean {\n return Boolean(option.loading)\n}\n\nfunction isOptionBlocked(option: GrSegmentedOption): boolean {\n return resolveOptionDisabled(option) || isOptionBusy(option)\n}\n\nfunction isOptionSelected(option: GrSegmentedOption): boolean {\n return option.value === props.modelValue\n}\n\nfunction isIconOnlyOption(option: GrSegmentedOption): boolean {\n return Boolean(option.icon) && !option.label\n}\n\nfunction setItemRef(value: GrSegmentedValue, element: Element | ComponentPublicInstance | null): void {\n const key = getOptionKey(value)\n\n if (!(element instanceof HTMLElement)) {\n itemRefs.value.delete(key)\n return\n }\n\n itemRefs.value.set(key, element)\n}\n\nfunction isSameGeometry(left: IndicatorGeometry | null, right: IndicatorGeometry | null): boolean {\n if (left === right) {\n return true\n }\n\n if (!left || !right) {\n return false\n }\n\n return left.width === right.width\n && left.height === right.height\n && left.x === right.x\n && left.y === right.y\n}\n\nfunction measureIndicator(): void {\n const root = rootRef.value\n const option = selectedOption.value\n if (!root || !option) {\n if (indicatorGeometry.value !== null)\n indicatorGeometry.value = null\n\n if (indicatorReady.value)\n indicatorReady.value = false\n\n return\n }\n\n const selectedItem = itemRefs.value.get(getOptionKey(option.value))\n if (!selectedItem) {\n return\n }\n\n const rootRect = root.getBoundingClientRect()\n const itemRect = selectedItem.getBoundingClientRect()\n\n const nextGeometry = {\n width: Math.max(Math.round(itemRect.width), 0),\n height: Math.max(Math.round(itemRect.height), 0),\n x: Math.round(itemRect.left - rootRect.left),\n y: Math.max(Math.round(itemRect.top - rootRect.top) - 1, 0),\n } satisfies IndicatorGeometry\n\n if (!isSameGeometry(indicatorGeometry.value, nextGeometry)) {\n indicatorGeometry.value = nextGeometry\n }\n\n const nextReady = nextGeometry.width > 0 && nextGeometry.height > 0\n if (indicatorReady.value !== nextReady) {\n indicatorReady.value = nextReady\n }\n}\n\nfunction refreshObserver(): void {\n if (typeof ResizeObserver === 'undefined') {\n return\n }\n\n const root = rootRef.value\n const option = selectedOption.value\n const selectedItem = option\n ? itemRefs.value.get(getOptionKey(option.value)) ?? null\n : null\n\n if (!resizeObserver) {\n resizeObserver = new ResizeObserver(() => scheduleMeasure())\n }\n\n if (!root) {\n resizeObserver.disconnect()\n observedRoot = null\n observedItem = null\n return\n }\n\n if (observedRoot === root && observedItem === selectedItem) {\n return\n }\n\n resizeObserver.disconnect()\n resizeObserver.observe(root)\n\n if (selectedItem) {\n resizeObserver.observe(selectedItem)\n }\n\n observedRoot = root\n observedItem = selectedItem\n}\n\nfunction scheduleMeasure(): void {\n if (scheduled) {\n return\n }\n\n scheduled = true\n void nextTick(() => {\n scheduled = false\n measureIndicator()\n refreshObserver()\n })\n}\n\nfunction emitValue(option: GrSegmentedOption): void {\n // Один guard на все пути: проверяй `readonly` только клавиатура, и клик по\n // сегменту менял бы значение вопреки заявленному контракту.\n if (isReadonly.value || isOptionBlocked(option) || option.value === props.modelValue) {\n return\n }\n\n emit('update:modelValue', option.value)\n emit('change', option.value, option)\n}\n\nfunction optionOf(value: GrSegmentedValue): GrSegmentedOption | undefined {\n return props.options.find(option => option.value === value)\n}\n\n/**\n * Кольцо roving-фокуса. Обе оси работают всегда: `orientation` у сегмента\n * описывает раскладку и уходит в `aria-orientation`, а не сужает клавиатуру —\n * вертикальный набор листается и стрелками влево/вправо.\n */\nconst roving = useRovingFocus<GrSegmentedValue>({\n items: () => props.options.map(option => option.value),\n elementFor: value => itemRefs.value.get(getOptionKey(value)),\n isDisabled: (value) => {\n const option = optionOf(value)\n return option ? isOptionBlocked(option) : true\n },\n orientation: () => 'both',\n skipDisabled: () => true,\n /**\n * Выбранный сегмент держит остановку `Tab` — кроме выключенного: на нём\n * стоит нативный `disabled`, и фокус на него не встанет вовсе. Загружающийся\n * остановку сохраняет: он не «недоступен», а занят.\n */\n initialKey: () => {\n const selected = selectedOption.value\n return selected && !resolveOptionDisabled(selected) ? selected.value : undefined\n },\n // Стрелка в `radiogroup` переносит и выбор, а не только фокус.\n onMove: (value) => {\n const option = optionOf(value)\n if (option)\n emitValue(option)\n },\n})\n\nfunction onItemClick(option: GrSegmentedOption): void {\n emitValue(option)\n // Клик тоже переносит остановку `Tab`: иначе она осталась бы там, куда её\n // увела последняя стрелка.\n if (!isOptionBlocked(option))\n roving.setActive(option.value)\n}\n\n/** Императивный фокус ведёт на текущую остановку `Tab`, а не на первый сегмент. */\nfunction rovingElement(): HTMLElement | undefined {\n const key = roving.rovingKey.value\n return key === undefined ? undefined : itemRefs.value.get(getOptionKey(key))\n}\n\nfunction focus(): void {\n rovingElement()?.focus()\n}\n\nfunction blur(): void {\n rovingElement()?.blur()\n}\n\nfunction onKeydown(event: KeyboardEvent, index: number): void {\n if (isDisabled.value || isReadonly.value) {\n return\n }\n\n if (roving.handleNavigationKeys(event)) {\n return\n }\n\n if (event.key === ' ' || event.key === 'Enter') {\n event.preventDefault()\n const option = props.options[index]\n if (option) {\n emitValue(option)\n }\n }\n}\n\nwatch(() => props.modelValue, () => scheduleMeasure())\nwatch(() => props.options, () => scheduleMeasure(), { deep: true })\nwatch(resolvedVariant, () => scheduleMeasure())\nwatch(resolvedSize, () => scheduleMeasure())\nwatch(() => props.block, () => scheduleMeasure())\n// Смена ориентации переставляет сегменты: без пересчёта индикатор остался бы в\n// координатах прежней раскладки.\nwatch(() => props.orientation, () => scheduleMeasure())\n\nonMounted(() => scheduleMeasure())\nonBeforeUnmount(() => {\n resizeObserver?.disconnect()\n observedRoot = null\n observedItem = null\n})\n\nif (__GR_DEV__) {\n watchEffect(() => {\n if (props.modelValue === undefined) {\n console.warn(\n `[granularity] GrSegmented: обязательный проп \\`modelValue\\` не передан — получено ${String(props.modelValue)}.`,\n )\n }\n\n if (!Array.isArray(props.options)) {\n console.warn(\n `[granularity] GrSegmented: обязательный проп \\`options\\` должен быть массивом — получено ${String(props.options)}.`,\n )\n }\n })\n}\n</script>\n\n<template>\n <div\n :id=\"fieldId\"\n ref=\"rootRef\"\n data-gr-segmented\n :data-variant=\"resolvedVariant\"\n role=\"radiogroup\"\n :aria-orientation=\"orientation\"\n :aria-label=\"ariaLabel\"\n :aria-labelledby=\"labelledBy\"\n :aria-describedby=\"describedBy\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-required=\"isRequired ? 'true' : undefined\"\n :aria-readonly=\"isReadonly ? 'true' : undefined\"\n :aria-disabled=\"isDisabled ? 'true' : undefined\"\n :class=\"rootClassName\"\n :style=\"rootStyle\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n >\n <!-- Значение для нативной формы уходит одним скрытым полем рядом с\n сегментами, а не вложенным в каждый `role=\"radio\"`: роль объявляет своих\n потомков презентационными, и вложенный интерактивный контрол ломает\n виджет для скринридеров (axe: `nested-interactive`). Скрытое поле не\n фокусируется и интерактивным не считается. -->\n <input\n v-if=\"selectedOption && !resolveOptionDisabled(selectedOption)\"\n type=\"hidden\"\n :name=\"resolvedName\"\n :value=\"String(selectedOption.value)\"\n >\n\n <span\n v-if=\"hasIndicator\"\n data-gr-segmented-indicator\n aria-hidden=\"true\"\n :class=\"indicatorClassName\"\n :style=\"indicatorStyle\"\n />\n\n <button\n v-for=\"(option, index) in options\"\n :key=\"option.value\"\n :ref=\"element => setItemRef(option.value, element)\"\n data-gr-segmented-item\n :data-value=\"String(option.value)\"\n type=\"button\"\n role=\"radio\"\n :aria-label=\"option.ariaLabel\"\n :aria-checked=\"isOptionSelected(option) ? 'true' : 'false'\"\n :aria-disabled=\"resolveOptionDisabled(option) ? 'true' : undefined\"\n :aria-busy=\"isOptionBusy(option) ? 'true' : undefined\"\n :disabled=\"resolveOptionDisabled(option)\"\n :tabindex=\"roving.tabindexFor(option.value)\"\n :class=\"grSegmentedItemClass({\n variant: resolvedVariant,\n selected: isOptionSelected(option),\n disabled: resolveOptionDisabled(option),\n iconOnly: isIconOnlyOption(option),\n })\"\n @click=\"onItemClick(option)\"\n @keydown=\"onKeydown($event, index)\"\n >\n <slot\n :option=\"option\"\n :selected=\"isOptionSelected(option)\"\n :disabled=\"resolveOptionDisabled(option)\"\n :loading=\"isOptionBusy(option)\"\n >\n <!-- Спиннер занимает место иконки: две крутилки рядом читались бы как ошибка. -->\n <IconLoader\n v-if=\"isOptionBusy(option)\"\n data-gr-segmented-spinner\n aria-hidden=\"true\"\n :class=\"grSegmentedItemSpinnerClass\"\n />\n <component\n :is=\"option.icon\"\n v-else-if=\"option.icon\"\n aria-hidden=\"true\"\n :class=\"grSegmentedItemIconClass\"\n />\n <!-- Подпись сегмента обрезается по ширине трека, и «Включе…» без\n подсказки прочитать нечем: полный текст отдаётся по наведению. -->\n <span\n v-if=\"option.label\"\n :class=\"grSegmentedItemLabelClass\"\n @pointerenter=\"titleWhenTruncated\"\n >\n {{ option.label }}\n </span>\n </slot>\n </button>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, useId, watch, watchEffect } from 'vue'\n\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useRovingFocus } from '../../composables/useRovingFocus'\nimport { useFocusWithin } from '../../composables/internal/useFocusWithin'\nimport { titleWhenTruncated } from '../shared/truncationTitle'\n\nimport type { ComponentPublicInstance } from 'vue'\nimport IconLoader from '~icons/lucide/loader-circle'\n\nimport {\n grSegmentedIndicatorClass,\n grSegmentedItemClass,\n grSegmentedItemIconClass,\n grSegmentedItemLabelClass,\n grSegmentedItemSpinnerClass,\n grSegmentedRootClass,\n grSegmentedRootStyle,\n type GrSegmentedOption,\n type GrSegmentedOrientation,\n type GrSegmentedSize,\n type GrSegmentedValue,\n type GrSegmentedVariant,\n} from './grSegmentedStyles'\n\ntype IndicatorGeometry = {\n width: number\n height: number\n x: number\n y: number\n}\n\n/**\n * Пропсы публичного GR-примитива «Segmented».\n */\nexport interface GrSegmentedProps {\n modelValue: GrSegmentedValue\n options: GrSegmentedOption[]\n variant?: GrSegmentedVariant\n size?: GrSegmentedSize\n /**\n * Направление ряда. Вертикаль — боковые фильтры; индикатор к ней готов по\n * построению, он двумерный.\n */\n orientation?: GrSegmentedOrientation\n /** Длительность анимации индикатора в мс. */\n indicatorDuration?: number\n /** Растягивать сегмент на всю ширину контейнера. */\n block?: boolean\n disabled?: boolean\n /** Только для чтения: выбор видно, но он не меняется. */\n readonly?: boolean\n /** Визуальное и ARIA-состояние ошибки. */\n invalid?: boolean\n /** Обязательное поле (`aria-required`). */\n required?: boolean\n /** Имя скрытого поля, которым выбранное значение уходит в нативную форму. */\n name?: string\n ariaLabel?: string\n}\n\nexport interface GrSegmentedEmits {\n (e: 'update:modelValue', value: GrSegmentedValue): void\n (e: 'change', value: GrSegmentedValue, option: GrSegmentedOption): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrSegmentedProps>(),\n {\n variant: undefined,\n size: undefined,\n orientation: 'horizontal',\n indicatorDuration: 300,\n block: false,\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n name: undefined,\n ariaLabel: undefined,\n },\n)\n\n// Эффективный размер: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrSegmented' })\nconst resolvedVariant = useGrComponentProp('GrSegmented', 'variant', () => props.variant, 'pills')\n\nconst emit = defineEmits<GrSegmentedEmits>()\ndefineSlots<{\n /** Содержимое сегмента вместо подписи из `options`. */\n default?: (props: {\n option: GrSegmentedOption\n selected: boolean\n disabled: boolean\n loading: boolean\n }) => any\n}>()\n\n// `useId()` стабилен между сервером и клиентом, в отличие от `instance.uid`.\nconst fallbackName = `gr-segmented-${useId()}`\n\n// Контекст `GrFormField` + общий контракт форм-контрола.\nconst field = useGrFormFieldContext()\nconst fieldId = computed(() => field?.id.value)\nconst describedBy = computed(() => field?.describedById.value)\nconst labelledBy = computed(() => (props.ariaLabel ? undefined : field?.labelId.value))\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n} = useGrFormControl(() => props)\n\nconst rootRef = ref<HTMLElement | null>(null)\n\n// Фокус ходит между сегментами: без границы каждая стрелка давала бы\n// потребителю пару `blur` + `focus`.\nconst { onFocusIn, onFocusOut } = useFocusWithin(rootRef, {\n enter: event => emit('focus', event),\n leave: event => emit('blur', event),\n})\n\ndefineExpose({ focus, blur })\nconst itemRefs = ref(new Map<string, HTMLElement>())\nconst indicatorGeometry = ref<IndicatorGeometry | null>(null)\nconst indicatorReady = ref(false)\n\nlet resizeObserver: ResizeObserver | null = null\nlet observedRoot: HTMLElement | null = null\nlet observedItem: HTMLElement | null = null\nlet scheduled = false\n\nconst selectedIndex = computed(() => props.options.findIndex(option => option.value === props.modelValue))\nconst selectedOption = computed(() => props.options[selectedIndex.value] ?? null)\nconst indicatorDuration = computed(() => Math.max(0, Math.round(props.indicatorDuration)))\nconst isVertical = computed(() => props.orientation === 'vertical')\nconst rootClassName = computed(() => grSegmentedRootClass({\n variant: resolvedVariant.value,\n orientation: props.orientation,\n block: props.block,\n disabled: isDisabled.value,\n}))\n\n/**\n * Раскладка задаётся треками, а не классами: число сегментов известно только в\n * рантайме. По вертикали колонка одна — сегменты одинаковой ширины по\n * построению, и `block` решает лишь, занимать ли ширину контейнера.\n */\nconst rootStyle = computed<Record<string, string>>(() => {\n const empty = props.options.length === 0\n const track = () => props.block && !isVertical.value ? 'minmax(0,1fr)' : 'minmax(0,max-content)'\n\n return {\n ...grSegmentedRootStyle({\n variant: resolvedVariant.value,\n size: resolvedSize.value,\n orientation: props.orientation,\n }),\n gridTemplateColumns: isVertical.value\n ? 'minmax(0,1fr)'\n : empty ? 'none' : props.options.map(track).join(' '),\n ...(isVertical.value\n ? { gridTemplateRows: empty ? 'none' : props.options.map(track).join(' ') }\n : {}),\n }\n})\nconst indicatorClassName = computed(() => grSegmentedIndicatorClass(resolvedVariant.value))\nconst resolvedName = computed(() => props.name ?? fallbackName)\nconst hasIndicator = computed(() => selectedIndex.value !== -1 && indicatorGeometry.value !== null)\n\nconst indicatorStyle = computed<Record<string, string>>(() => {\n const geometry = indicatorGeometry.value\n if (!geometry) {\n return {\n opacity: '0',\n width: '0px',\n height: '0px',\n transitionDuration: `${indicatorDuration.value}ms`,\n transform: 'translate3d(0px, 0px, 0px)',\n }\n }\n\n return {\n opacity: indicatorReady.value ? '1' : '0',\n width: `${geometry.width}px`,\n height: `${geometry.height}px`,\n transitionDuration: `${indicatorDuration.value}ms`,\n transform: `translate3d(${geometry.x}px, ${geometry.y}px, 0)`,\n }\n})\n\nfunction getOptionKey(value: GrSegmentedValue): string {\n return String(value)\n}\n\nfunction resolveOptionDisabled(option: GrSegmentedOption): boolean {\n return isDisabled.value || Boolean(option.disabled)\n}\n\n/**\n * Занятый сегмент не «недоступен», а работает: нативный `disabled` и\n * `aria-disabled` ему не ставим — состояние несёт `aria-busy`. Но выбор он не\n * принимает, поэтому блокировка считается отдельно от `disabled`.\n */\nfunction isOptionBusy(option: GrSegmentedOption): boolean {\n return Boolean(option.loading)\n}\n\nfunction isOptionBlocked(option: GrSegmentedOption): boolean {\n return resolveOptionDisabled(option) || isOptionBusy(option)\n}\n\nfunction isOptionSelected(option: GrSegmentedOption): boolean {\n return option.value === props.modelValue\n}\n\nfunction isIconOnlyOption(option: GrSegmentedOption): boolean {\n return Boolean(option.icon) && !option.label\n}\n\nfunction setItemRef(value: GrSegmentedValue, element: Element | ComponentPublicInstance | null): void {\n const key = getOptionKey(value)\n\n if (!(element instanceof HTMLElement)) {\n itemRefs.value.delete(key)\n return\n }\n\n itemRefs.value.set(key, element)\n}\n\nfunction isSameGeometry(left: IndicatorGeometry | null, right: IndicatorGeometry | null): boolean {\n if (left === right) {\n return true\n }\n\n if (!left || !right) {\n return false\n }\n\n return left.width === right.width\n && left.height === right.height\n && left.x === right.x\n && left.y === right.y\n}\n\nfunction measureIndicator(): void {\n const root = rootRef.value\n const option = selectedOption.value\n if (!root || !option) {\n if (indicatorGeometry.value !== null)\n indicatorGeometry.value = null\n\n if (indicatorReady.value)\n indicatorReady.value = false\n\n return\n }\n\n const selectedItem = itemRefs.value.get(getOptionKey(option.value))\n if (!selectedItem) {\n return\n }\n\n const rootRect = root.getBoundingClientRect()\n const itemRect = selectedItem.getBoundingClientRect()\n\n const nextGeometry = {\n width: Math.max(Math.round(itemRect.width), 0),\n height: Math.max(Math.round(itemRect.height), 0),\n x: Math.round(itemRect.left - rootRect.left),\n y: Math.max(Math.round(itemRect.top - rootRect.top) - 1, 0),\n } satisfies IndicatorGeometry\n\n if (!isSameGeometry(indicatorGeometry.value, nextGeometry)) {\n indicatorGeometry.value = nextGeometry\n }\n\n const nextReady = nextGeometry.width > 0 && nextGeometry.height > 0\n if (indicatorReady.value !== nextReady) {\n indicatorReady.value = nextReady\n }\n}\n\nfunction refreshObserver(): void {\n if (typeof ResizeObserver === 'undefined') {\n return\n }\n\n const root = rootRef.value\n const option = selectedOption.value\n const selectedItem = option\n ? itemRefs.value.get(getOptionKey(option.value)) ?? null\n : null\n\n if (!resizeObserver) {\n resizeObserver = new ResizeObserver(() => scheduleMeasure())\n }\n\n if (!root) {\n resizeObserver.disconnect()\n observedRoot = null\n observedItem = null\n return\n }\n\n if (observedRoot === root && observedItem === selectedItem) {\n return\n }\n\n resizeObserver.disconnect()\n resizeObserver.observe(root)\n\n if (selectedItem) {\n resizeObserver.observe(selectedItem)\n }\n\n observedRoot = root\n observedItem = selectedItem\n}\n\nfunction scheduleMeasure(): void {\n if (scheduled) {\n return\n }\n\n scheduled = true\n void nextTick(() => {\n scheduled = false\n measureIndicator()\n refreshObserver()\n })\n}\n\nfunction emitValue(option: GrSegmentedOption): void {\n // Один guard на все пути: проверяй `readonly` только клавиатура, и клик по\n // сегменту менял бы значение вопреки заявленному контракту.\n if (isReadonly.value || isOptionBlocked(option) || option.value === props.modelValue) {\n return\n }\n\n emit('update:modelValue', option.value)\n emit('change', option.value, option)\n}\n\nfunction optionOf(value: GrSegmentedValue): GrSegmentedOption | undefined {\n return props.options.find(option => option.value === value)\n}\n\n/**\n * Кольцо roving-фокуса. Обе оси работают всегда: `orientation` у сегмента\n * описывает раскладку и уходит в `aria-orientation`, а не сужает клавиатуру —\n * вертикальный набор листается и стрелками влево/вправо.\n */\nconst roving = useRovingFocus<GrSegmentedValue>({\n items: () => props.options.map(option => option.value),\n elementFor: value => itemRefs.value.get(getOptionKey(value)),\n isDisabled: (value) => {\n const option = optionOf(value)\n return option ? isOptionBlocked(option) : true\n },\n orientation: () => 'both',\n skipDisabled: () => true,\n /**\n * Выбранный сегмент держит остановку `Tab` — кроме выключенного: на нём\n * стоит нативный `disabled`, и фокус на него не встанет вовсе. Загружающийся\n * остановку сохраняет: он не «недоступен», а занят.\n */\n initialKey: () => {\n const selected = selectedOption.value\n return selected && !resolveOptionDisabled(selected) ? selected.value : undefined\n },\n // Стрелка в `radiogroup` переносит и выбор, а не только фокус.\n onMove: (value) => {\n const option = optionOf(value)\n if (option)\n emitValue(option)\n },\n})\n\nfunction onItemClick(option: GrSegmentedOption): void {\n emitValue(option)\n // Клик тоже переносит остановку `Tab`: иначе она осталась бы там, куда её\n // увела последняя стрелка.\n if (!isOptionBlocked(option))\n roving.setActive(option.value)\n}\n\n/** Императивный фокус ведёт на текущую остановку `Tab`, а не на первый сегмент. */\nfunction rovingElement(): HTMLElement | undefined {\n const key = roving.rovingKey.value\n return key === undefined ? undefined : itemRefs.value.get(getOptionKey(key))\n}\n\nfunction focus(): void {\n rovingElement()?.focus()\n}\n\nfunction blur(): void {\n rovingElement()?.blur()\n}\n\nfunction onKeydown(event: KeyboardEvent, index: number): void {\n if (isDisabled.value || isReadonly.value) {\n return\n }\n\n if (roving.handleNavigationKeys(event)) {\n return\n }\n\n if (event.key === ' ' || event.key === 'Enter') {\n event.preventDefault()\n const option = props.options[index]\n if (option) {\n emitValue(option)\n }\n }\n}\n\nwatch(() => props.modelValue, () => scheduleMeasure())\nwatch(() => props.options, () => scheduleMeasure(), { deep: true })\nwatch(resolvedVariant, () => scheduleMeasure())\nwatch(resolvedSize, () => scheduleMeasure())\nwatch(() => props.block, () => scheduleMeasure())\n// Смена ориентации переставляет сегменты: без пересчёта индикатор остался бы в\n// координатах прежней раскладки.\nwatch(() => props.orientation, () => scheduleMeasure())\n\nonMounted(() => scheduleMeasure())\nonBeforeUnmount(() => {\n resizeObserver?.disconnect()\n observedRoot = null\n observedItem = null\n})\n\nif (__GR_DEV__) {\n watchEffect(() => {\n if (props.modelValue === undefined) {\n console.warn(\n `[granularity] GrSegmented: обязательный проп \\`modelValue\\` не передан — получено ${String(props.modelValue)}.`,\n )\n }\n\n if (!Array.isArray(props.options)) {\n console.warn(\n `[granularity] GrSegmented: обязательный проп \\`options\\` должен быть массивом — получено ${String(props.options)}.`,\n )\n }\n })\n}\n</script>\n\n<template>\n <div\n :id=\"fieldId\"\n ref=\"rootRef\"\n data-gr-segmented\n :data-variant=\"resolvedVariant\"\n role=\"radiogroup\"\n :aria-orientation=\"orientation\"\n :aria-label=\"ariaLabel\"\n :aria-labelledby=\"labelledBy\"\n :aria-describedby=\"describedBy\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-required=\"isRequired ? 'true' : undefined\"\n :aria-readonly=\"isReadonly ? 'true' : undefined\"\n :aria-disabled=\"isDisabled ? 'true' : undefined\"\n :class=\"rootClassName\"\n :style=\"rootStyle\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n >\n <!-- Значение для нативной формы уходит одним скрытым полем рядом с\n сегментами, а не вложенным в каждый `role=\"radio\"`: роль объявляет своих\n потомков презентационными, и вложенный интерактивный контрол ломает\n виджет для скринридеров (axe: `nested-interactive`). Скрытое поле не\n фокусируется и интерактивным не считается. -->\n <input\n v-if=\"selectedOption && !resolveOptionDisabled(selectedOption)\"\n type=\"hidden\"\n :name=\"resolvedName\"\n :value=\"String(selectedOption.value)\"\n >\n\n <span\n v-if=\"hasIndicator\"\n data-gr-segmented-indicator\n aria-hidden=\"true\"\n :class=\"indicatorClassName\"\n :style=\"indicatorStyle\"\n />\n\n <button\n v-for=\"(option, index) in options\"\n :key=\"option.value\"\n :ref=\"element => setItemRef(option.value, element)\"\n data-gr-segmented-item\n :data-value=\"String(option.value)\"\n type=\"button\"\n role=\"radio\"\n :aria-label=\"option.ariaLabel\"\n :aria-checked=\"isOptionSelected(option) ? 'true' : 'false'\"\n :aria-disabled=\"resolveOptionDisabled(option) ? 'true' : undefined\"\n :aria-busy=\"isOptionBusy(option) ? 'true' : undefined\"\n :disabled=\"resolveOptionDisabled(option)\"\n :tabindex=\"roving.tabindexFor(option.value)\"\n :class=\"grSegmentedItemClass({\n variant: resolvedVariant,\n selected: isOptionSelected(option),\n disabled: resolveOptionDisabled(option),\n iconOnly: isIconOnlyOption(option),\n })\"\n @click=\"onItemClick(option)\"\n @keydown=\"onKeydown($event, index)\"\n >\n <slot\n :option=\"option\"\n :selected=\"isOptionSelected(option)\"\n :disabled=\"resolveOptionDisabled(option)\"\n :loading=\"isOptionBusy(option)\"\n >\n <!-- Спиннер занимает место иконки: две крутилки рядом читались бы как ошибка. -->\n <IconLoader\n v-if=\"isOptionBusy(option)\"\n data-gr-segmented-spinner\n aria-hidden=\"true\"\n :class=\"grSegmentedItemSpinnerClass\"\n />\n <component\n :is=\"option.icon\"\n v-else-if=\"option.icon\"\n aria-hidden=\"true\"\n :class=\"grSegmentedItemIconClass\"\n />\n <!-- Подпись сегмента обрезается по ширине трека, и «Включе…» без\n подсказки прочитать нечем: полный текст отдаётся по наведению. -->\n <span\n v-if=\"option.label\"\n :class=\"grSegmentedItemLabelClass\"\n @pointerenter=\"titleWhenTruncated\"\n >\n {{ option.label }}\n </span>\n </slot>\n </button>\n </div>\n</template>\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
- import { t as GrSegmented_default } from "./chunks/GrSegmented-NKTi9bMG.js";
1
+ import { t as GrSegmented_default } from "./chunks/GrSegmented-CuDaKA6S.js";
2
2
  import { n as grSegmentedSafelist, t as grSegmentedConfig } from "../../chunks/config-rLkc9Zn2.js";
3
3
  export { GrSegmented_default as GrSegmented, GrSegmented_default as default, grSegmentedConfig, grSegmentedSafelist };
@@ -5,7 +5,7 @@ import { useAnnouncer } from "../../../composables/useAnnouncer.js";
5
5
  import { n as insertionIndex, r as moveItem, t as useDragSort } from "../../../chunks/useDragSort-0l47h4yb.js";
6
6
  import { t as grip_vertical_default } from "../../../chunks/grip-vertical-lRha2S7q.js";
7
7
  import { a as emptyClass, c as indicatorAfterClass, d as rowDisabledClass, f as rowDraggingClass, h as rowLayoutClass, i as dividedClass, l as indicatorBeforeClass, m as rowGrabbedClass, o as handleClass, p as rowFocusClass, r as contentClass, s as handleDisabledClass, u as listClass } from "../../../chunks/config-CbEbLyDE.js";
8
- import { Fragment, computed, createBlock, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, nextTick, normalizeClass, normalizeStyle, openBlock, ref, renderList, renderSlot, toDisplayString, unref, withCtx, withModifiers } from "vue";
8
+ import { Fragment, computed, createBlock, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, nextTick, normalizeClass, normalizeStyle, openBlock, ref, renderList, renderSlot, toDisplayString, unref, watchEffect, withCtx, withModifiers } from "vue";
9
9
  //#region src/components/GrSortableList/GrSortableList.vue?vue&type=script&setup=true&lang.ts
10
10
  var _hoisted_1 = ["aria-label", "aria-disabled"];
11
11
  var _hoisted_2 = [
@@ -213,6 +213,9 @@ var GrSortableList_default = /* @__PURE__ */ defineComponent({
213
213
  move: (from, to) => applyMove(from, to),
214
214
  focusItem: (index) => roving.focusKey(keys.value[index])
215
215
  });
216
+ if (process.env.NODE_ENV !== "production") watchEffect(() => {
217
+ if (!Array.isArray(props.modelValue)) console.warn(`[granularity] GrSortableList: обязательный проп \`modelValue\` должен быть массивом — получено ${String(props.modelValue)}.`);
218
+ });
216
219
  return (_ctx, _cache) => {
217
220
  return openBlock(), createBlock(unref(GrCard_default), { variant: __props.variant }, {
218
221
  default: withCtx(() => [createElementVNode("div", {
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GrSortableList-ce1psLL0.js","names":[],"sources":["../../../../src/components/GrSortableList/GrSortableList.vue","../../../../src/components/GrSortableList/GrSortableList.vue"],"sourcesContent":["<script setup lang=\"ts\" generic=\"T\">\nimport { computed, nextTick, ref, watchEffect } from 'vue'\n\nimport GrCard, { type GrCardVariant } from '../GrCard'\nimport { useAnnouncer } from '../../composables/useAnnouncer'\nimport { useDragSort } from '../../composables/useDragSort'\nimport { useRovingFocus } from '../../composables/useRovingFocus'\nimport { insertionIndex, moveItem } from '../../composables/internal/dragSortGeometry'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n\nimport type { GrSortableOrientation } from './grSortableListStyles'\nimport {\n contentClass,\n dividedClass,\n emptyClass,\n handleClass,\n handleDisabledClass,\n indicatorAfterClass,\n indicatorBeforeClass,\n listClass,\n rowDisabledClass,\n rowDraggingClass,\n rowFocusClass,\n rowGrabbedClass,\n rowLayoutClass,\n} from './grSortableListStyles'\n\nimport IconGripVertical from '~icons/lucide/grip-vertical'\n\nexport interface GrSortableListProps<TItem = unknown> {\n /** Набор в текущем порядке. `v-model`: наружу уходит новый массив, вход не мутируется. */\n modelValue: TItem[]\n /** Ключ элемента: имя поля или функция. Без него — индекс. */\n itemKey?: string | ((item: TItem, index: number) => string | number)\n /** Список только для чтения: ни указателем, ни с клавиатуры. */\n disabled?: boolean\n /** Ось переноса. `horizontal` — ряд с переносом по ширине. */\n orientation?: GrSortableOrientation\n /** Поверхность под списком — вариант карточки. */\n variant?: GrCardVariant\n /** Разделители между строками. */\n divided?: boolean\n /** Тянуть можно только за ручку. Выключено — тянется вся строка. */\n handleOnly?: boolean\n /** Высота видимой части: список становится скроллером с автопрокруткой при переносе. */\n maxHeight?: number | string\n /** Текст пустого состояния. Слот `#empty` сильнее. */\n emptyText?: string\n /** Имя списка для скринридера. Не задано — берётся из локали. */\n ariaLabel?: string\n}\n\nexport interface GrSortableListEmits<TItem = unknown> {\n (e: 'update:modelValue', value: TItem[]): void\n (e: 'move', from: number, to: number): void\n (e: 'change', value: TItem[]): void\n}\n\nconst props = withDefaults(defineProps<GrSortableListProps<T>>(), {\n itemKey: undefined,\n disabled: false,\n orientation: 'vertical',\n variant: undefined,\n divided: true,\n handleOnly: true,\n maxHeight: undefined,\n emptyText: undefined,\n ariaLabel: undefined,\n})\n\nconst emit = defineEmits<GrSortableListEmits<T>>()\n\ndefineSlots<{\n item?: (props: { item: T, index: number, dragging: boolean, grabbed: boolean }) => any\n handle?: (props: { item: T, index: number, disabled: boolean }) => any\n empty?: () => any\n}>()\n\nconst { t } = useGranularityTranslations()\nconst { announce } = useAnnouncer()\n\nconst listEl = ref<HTMLElement | null>(null)\nconst rowEls = new Map<string, HTMLElement>()\n\nconst count = computed(() => props.modelValue.length)\nconst isEmpty = computed(() => count.value === 0)\nconst isVertical = computed(() => props.orientation === 'vertical')\n\nfunction keyOf(item: T, index: number): string {\n if (typeof props.itemKey === 'function')\n return String(props.itemKey(item, index))\n if (typeof props.itemKey === 'string')\n return String((item as Record<string, unknown>)[props.itemKey])\n\n return String(index)\n}\n\nconst keys = computed(() => props.modelValue.map((item, index) => keyOf(item, index)))\n\nfunction indexOfKey(key: string): number {\n return keys.value.indexOf(key)\n}\n\nfunction registerRow(key: string, el: unknown): void {\n if (el instanceof HTMLElement) {\n rowEls.set(key, el)\n return\n }\n\n // Перестановка размонтирует старый узел уже после того, как встал новый:\n // снимаем запись, только если она протухла (приём из `GrTree`).\n const previous = rowEls.get(key)\n if (previous && !previous.isConnected)\n rowEls.delete(key)\n}\n\nconst roving = useRovingFocus<string>({\n items: () => keys.value,\n elementFor: key => rowEls.get(key) ?? null,\n orientation: () => (isVertical.value ? 'vertical' : 'horizontal'),\n wrap: () => false,\n})\n\nfunction applyMove(from: number, to: number): void {\n if (from < 0 || to < 0 || from === to)\n return\n\n const next = moveItem(props.modelValue, from, to)\n emit('update:modelValue', next)\n emit('move', from, to)\n emit('change', next)\n}\n\nconst sort = useDragSort<string, number>({\n items: () => keys.value,\n elementFor: key => rowEls.get(key) ?? null,\n orientation: () => props.orientation,\n scroller: () => (props.maxHeight === undefined ? null : listEl.value),\n disabled: () => props.disabled,\n resolveTarget: (hit, source) => insertionIndex(hit, indexOfKey(source), count.value),\n onDrop: (source, to) => applyMove(indexOfKey(source), to),\n onUpdate: (source, target, mode) => {\n // Объявляется только клавиатурный перенос: у мыши всё видно на экране, а\n // поток объявлений на каждом движении указателя делает диктор бесполезным.\n if (mode !== 'keyboard' || source === null)\n return\n\n const position = (target ?? indexOfKey(source)) + 1\n announce(t('gr.sortable.moved', 'Position {position} of {count}', { position, count: count.value }))\n },\n})\n\nconst activeIndex = computed(() => (sort.source.value === null ? -1 : indexOfKey(sort.source.value)))\n\n/** Куда встанет строка: индикатор рисуется на строке-соседе, а не отдельным узлом. */\nfunction indicatorFor(index: number): 'before' | 'after' | null {\n const target = sort.target.value\n if (target === null || !sort.isActive.value || index === activeIndex.value)\n return null\n\n if (target === index)\n return target < activeIndex.value || activeIndex.value === -1 ? 'before' : 'after'\n\n return null\n}\n\nfunction rowClass(index: number, key: string): string[] {\n const grabbed = sort.mode.value === 'keyboard' && sort.source.value === key\n const dragging = sort.mode.value === 'pointer' && sort.source.value === key\n const indicator = indicatorFor(index)\n\n return [\n rowLayoutClass,\n rowFocusClass,\n props.disabled ? rowDisabledClass : '',\n grabbed ? rowGrabbedClass : '',\n dragging ? rowDraggingClass : '',\n indicator === 'before' ? indicatorBeforeClass : '',\n indicator === 'after' ? indicatorAfterClass : '',\n ]\n}\n\nfunction startPointer(key: string, event: PointerEvent): void {\n if (props.disabled)\n return\n\n sort.startFrom(key)(event)\n}\n\nfunction grab(key: string): void {\n sort.grab(key)\n if (sort.mode.value !== 'keyboard')\n return\n\n // Цель ставится до объявления: `setTarget` сам объявляет позицию, и обратный\n // порядок стёр бы более полное сообщение о захвате.\n sort.setTarget(indexOfKey(key))\n announce(t('gr.sortable.grabbed', 'Grabbed. Position {position} of {count}', {\n position: indexOfKey(key) + 1,\n count: count.value,\n }))\n}\n\nasync function drop(key: string): Promise<void> {\n const target = sort.target.value\n sort.drop()\n\n announce(t('gr.sortable.dropped', 'Dropped at position {position} of {count}', {\n position: (target ?? 0) + 1,\n count: count.value,\n }))\n\n // Фокус остаётся на перенесённой строке: иначе после переноса он падает на\n // тело документа и клавиатурный сценарий обрывается на первом же шаге.\n await nextTick()\n await roving.focusKey(key)\n}\n\nfunction cancel(): void {\n if (!sort.isActive.value)\n return\n\n sort.cancel()\n announce(t('gr.sortable.cancelled', 'Move cancelled'))\n}\n\nfunction moveTargetBy(delta: number): void {\n const current = sort.target.value ?? activeIndex.value\n const next = Math.min(count.value - 1, Math.max(0, current + delta))\n\n if (next !== current)\n sort.setTarget(next)\n}\n\nfunction onRowKeydown(event: KeyboardEvent, key: string): void {\n if (props.disabled)\n return\n\n const forward = isVertical.value ? 'ArrowDown' : 'ArrowRight'\n const backward = isVertical.value ? 'ArrowUp' : 'ArrowLeft'\n const grabbed = sort.mode.value === 'keyboard'\n\n if (event.key === 'Escape') {\n if (!grabbed)\n return\n\n event.preventDefault()\n cancel()\n return\n }\n\n if (event.key === ' ' || event.key === 'Enter') {\n event.preventDefault()\n if (grabbed)\n void drop(key)\n else grab(key)\n return\n }\n\n if (grabbed) {\n // Взятая строка забирает стрелки себе: двигается она, а не фокус.\n if (event.key === forward || event.key === backward) {\n event.preventDefault()\n moveTargetBy(event.key === forward ? 1 : -1)\n }\n return\n }\n\n roving.handleNavigationKeys(event)\n}\n\n/** Фокус ушёл из списка — взятая строка не может остаться взятой навсегда. */\nfunction onFocusout(event: FocusEvent): void {\n if (sort.mode.value !== 'keyboard')\n return\n\n const next = event.relatedTarget as Node | null\n if (next && listEl.value?.contains(next))\n return\n\n cancel()\n}\n\nconst resolvedEmptyText = computed(() => props.emptyText ?? t('gr.sortable.empty', 'Nothing to sort yet'))\nconst resolvedLabel = computed(() => props.ariaLabel ?? t('gr.sortable.label', 'Sortable list'))\nconst handleLabel = computed(() => t('gr.sortable.handle', 'Reorder'))\n\nconst listStyle = computed(() => (props.maxHeight === undefined\n ? undefined\n : { maxHeight: typeof props.maxHeight === 'number' ? `${props.maxHeight}px` : props.maxHeight, overflow: 'auto' }))\n\ndefineExpose({\n /** Программная перестановка — та же, что делает перенос. */\n move: (from: number, to: number) => applyMove(from, to),\n focusItem: (index: number) => roving.focusKey(keys.value[index]),\n})\n\nif (__GR_DEV__) {\n watchEffect(() => {\n if (!Array.isArray(props.modelValue)) {\n console.warn(\n `[granularity] GrSortableList: обязательный проп \\`modelValue\\` должен быть массивом — получено ${String(props.modelValue)}.`,\n )\n }\n })\n}\n</script>\n\n<template>\n <GrCard :variant=\"variant\">\n <div\n ref=\"listEl\"\n role=\"list\"\n data-gr-sortable\n :aria-label=\"resolvedLabel\"\n :aria-disabled=\"disabled ? 'true' : undefined\"\n :class=\"[listClass[orientation], divided ? dividedClass : '']\"\n :style=\"listStyle\"\n @focusout=\"onFocusout\"\n >\n <div\n v-if=\"isEmpty\"\n data-gr-sortable-empty\n :class=\"emptyClass\"\n >\n <slot name=\"empty\">\n {{ resolvedEmptyText }}\n </slot>\n </div>\n\n <div\n v-for=\"(item, index) in modelValue\"\n v-else\n :key=\"keys[index]\"\n :ref=\"el => registerRow(keys[index], el)\"\n role=\"listitem\"\n data-gr-sortable-item\n :tabindex=\"disabled ? -1 : roving.tabindexFor(keys[index])\"\n :aria-roledescription=\"handleLabel\"\n :aria-posinset=\"index + 1\"\n :aria-setsize=\"count\"\n :aria-grabbed=\"sort.mode.value === 'keyboard' && sort.source.value === keys[index] ? 'true' : undefined\"\n :class=\"rowClass(index, keys[index])\"\n @keydown=\"onRowKeydown($event, keys[index])\"\n @focus=\"roving.setActive(keys[index])\"\n @pointerdown=\"handleOnly ? undefined : startPointer(keys[index], $event)\"\n >\n <button\n type=\"button\"\n tabindex=\"-1\"\n data-gr-sortable-handle\n :class=\"[handleClass, disabled ? handleDisabledClass : '']\"\n :aria-label=\"handleLabel\"\n :disabled=\"disabled\"\n @pointerdown=\"startPointer(keys[index], $event)\"\n @click.stop\n >\n <slot\n name=\"handle\"\n :item=\"item\"\n :index=\"index\"\n :disabled=\"disabled\"\n >\n <IconGripVertical class=\"block\" aria-hidden=\"true\" />\n </slot>\n </button>\n\n <div :class=\"contentClass\">\n <slot\n name=\"item\"\n :item=\"item\"\n :index=\"index\"\n :dragging=\"sort.mode.value === 'pointer' && sort.source.value === keys[index]\"\n :grabbed=\"sort.mode.value === 'keyboard' && sort.source.value === keys[index]\"\n >\n {{ item }}\n </slot>\n </div>\n </div>\n </div>\n </GrCard>\n</template>\n","<script setup lang=\"ts\" generic=\"T\">\nimport { computed, nextTick, ref, watchEffect } from 'vue'\n\nimport GrCard, { type GrCardVariant } from '../GrCard'\nimport { useAnnouncer } from '../../composables/useAnnouncer'\nimport { useDragSort } from '../../composables/useDragSort'\nimport { useRovingFocus } from '../../composables/useRovingFocus'\nimport { insertionIndex, moveItem } from '../../composables/internal/dragSortGeometry'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n\nimport type { GrSortableOrientation } from './grSortableListStyles'\nimport {\n contentClass,\n dividedClass,\n emptyClass,\n handleClass,\n handleDisabledClass,\n indicatorAfterClass,\n indicatorBeforeClass,\n listClass,\n rowDisabledClass,\n rowDraggingClass,\n rowFocusClass,\n rowGrabbedClass,\n rowLayoutClass,\n} from './grSortableListStyles'\n\nimport IconGripVertical from '~icons/lucide/grip-vertical'\n\nexport interface GrSortableListProps<TItem = unknown> {\n /** Набор в текущем порядке. `v-model`: наружу уходит новый массив, вход не мутируется. */\n modelValue: TItem[]\n /** Ключ элемента: имя поля или функция. Без него — индекс. */\n itemKey?: string | ((item: TItem, index: number) => string | number)\n /** Список только для чтения: ни указателем, ни с клавиатуры. */\n disabled?: boolean\n /** Ось переноса. `horizontal` — ряд с переносом по ширине. */\n orientation?: GrSortableOrientation\n /** Поверхность под списком — вариант карточки. */\n variant?: GrCardVariant\n /** Разделители между строками. */\n divided?: boolean\n /** Тянуть можно только за ручку. Выключено — тянется вся строка. */\n handleOnly?: boolean\n /** Высота видимой части: список становится скроллером с автопрокруткой при переносе. */\n maxHeight?: number | string\n /** Текст пустого состояния. Слот `#empty` сильнее. */\n emptyText?: string\n /** Имя списка для скринридера. Не задано — берётся из локали. */\n ariaLabel?: string\n}\n\nexport interface GrSortableListEmits<TItem = unknown> {\n (e: 'update:modelValue', value: TItem[]): void\n (e: 'move', from: number, to: number): void\n (e: 'change', value: TItem[]): void\n}\n\nconst props = withDefaults(defineProps<GrSortableListProps<T>>(), {\n itemKey: undefined,\n disabled: false,\n orientation: 'vertical',\n variant: undefined,\n divided: true,\n handleOnly: true,\n maxHeight: undefined,\n emptyText: undefined,\n ariaLabel: undefined,\n})\n\nconst emit = defineEmits<GrSortableListEmits<T>>()\n\ndefineSlots<{\n item?: (props: { item: T, index: number, dragging: boolean, grabbed: boolean }) => any\n handle?: (props: { item: T, index: number, disabled: boolean }) => any\n empty?: () => any\n}>()\n\nconst { t } = useGranularityTranslations()\nconst { announce } = useAnnouncer()\n\nconst listEl = ref<HTMLElement | null>(null)\nconst rowEls = new Map<string, HTMLElement>()\n\nconst count = computed(() => props.modelValue.length)\nconst isEmpty = computed(() => count.value === 0)\nconst isVertical = computed(() => props.orientation === 'vertical')\n\nfunction keyOf(item: T, index: number): string {\n if (typeof props.itemKey === 'function')\n return String(props.itemKey(item, index))\n if (typeof props.itemKey === 'string')\n return String((item as Record<string, unknown>)[props.itemKey])\n\n return String(index)\n}\n\nconst keys = computed(() => props.modelValue.map((item, index) => keyOf(item, index)))\n\nfunction indexOfKey(key: string): number {\n return keys.value.indexOf(key)\n}\n\nfunction registerRow(key: string, el: unknown): void {\n if (el instanceof HTMLElement) {\n rowEls.set(key, el)\n return\n }\n\n // Перестановка размонтирует старый узел уже после того, как встал новый:\n // снимаем запись, только если она протухла (приём из `GrTree`).\n const previous = rowEls.get(key)\n if (previous && !previous.isConnected)\n rowEls.delete(key)\n}\n\nconst roving = useRovingFocus<string>({\n items: () => keys.value,\n elementFor: key => rowEls.get(key) ?? null,\n orientation: () => (isVertical.value ? 'vertical' : 'horizontal'),\n wrap: () => false,\n})\n\nfunction applyMove(from: number, to: number): void {\n if (from < 0 || to < 0 || from === to)\n return\n\n const next = moveItem(props.modelValue, from, to)\n emit('update:modelValue', next)\n emit('move', from, to)\n emit('change', next)\n}\n\nconst sort = useDragSort<string, number>({\n items: () => keys.value,\n elementFor: key => rowEls.get(key) ?? null,\n orientation: () => props.orientation,\n scroller: () => (props.maxHeight === undefined ? null : listEl.value),\n disabled: () => props.disabled,\n resolveTarget: (hit, source) => insertionIndex(hit, indexOfKey(source), count.value),\n onDrop: (source, to) => applyMove(indexOfKey(source), to),\n onUpdate: (source, target, mode) => {\n // Объявляется только клавиатурный перенос: у мыши всё видно на экране, а\n // поток объявлений на каждом движении указателя делает диктор бесполезным.\n if (mode !== 'keyboard' || source === null)\n return\n\n const position = (target ?? indexOfKey(source)) + 1\n announce(t('gr.sortable.moved', 'Position {position} of {count}', { position, count: count.value }))\n },\n})\n\nconst activeIndex = computed(() => (sort.source.value === null ? -1 : indexOfKey(sort.source.value)))\n\n/** Куда встанет строка: индикатор рисуется на строке-соседе, а не отдельным узлом. */\nfunction indicatorFor(index: number): 'before' | 'after' | null {\n const target = sort.target.value\n if (target === null || !sort.isActive.value || index === activeIndex.value)\n return null\n\n if (target === index)\n return target < activeIndex.value || activeIndex.value === -1 ? 'before' : 'after'\n\n return null\n}\n\nfunction rowClass(index: number, key: string): string[] {\n const grabbed = sort.mode.value === 'keyboard' && sort.source.value === key\n const dragging = sort.mode.value === 'pointer' && sort.source.value === key\n const indicator = indicatorFor(index)\n\n return [\n rowLayoutClass,\n rowFocusClass,\n props.disabled ? rowDisabledClass : '',\n grabbed ? rowGrabbedClass : '',\n dragging ? rowDraggingClass : '',\n indicator === 'before' ? indicatorBeforeClass : '',\n indicator === 'after' ? indicatorAfterClass : '',\n ]\n}\n\nfunction startPointer(key: string, event: PointerEvent): void {\n if (props.disabled)\n return\n\n sort.startFrom(key)(event)\n}\n\nfunction grab(key: string): void {\n sort.grab(key)\n if (sort.mode.value !== 'keyboard')\n return\n\n // Цель ставится до объявления: `setTarget` сам объявляет позицию, и обратный\n // порядок стёр бы более полное сообщение о захвате.\n sort.setTarget(indexOfKey(key))\n announce(t('gr.sortable.grabbed', 'Grabbed. Position {position} of {count}', {\n position: indexOfKey(key) + 1,\n count: count.value,\n }))\n}\n\nasync function drop(key: string): Promise<void> {\n const target = sort.target.value\n sort.drop()\n\n announce(t('gr.sortable.dropped', 'Dropped at position {position} of {count}', {\n position: (target ?? 0) + 1,\n count: count.value,\n }))\n\n // Фокус остаётся на перенесённой строке: иначе после переноса он падает на\n // тело документа и клавиатурный сценарий обрывается на первом же шаге.\n await nextTick()\n await roving.focusKey(key)\n}\n\nfunction cancel(): void {\n if (!sort.isActive.value)\n return\n\n sort.cancel()\n announce(t('gr.sortable.cancelled', 'Move cancelled'))\n}\n\nfunction moveTargetBy(delta: number): void {\n const current = sort.target.value ?? activeIndex.value\n const next = Math.min(count.value - 1, Math.max(0, current + delta))\n\n if (next !== current)\n sort.setTarget(next)\n}\n\nfunction onRowKeydown(event: KeyboardEvent, key: string): void {\n if (props.disabled)\n return\n\n const forward = isVertical.value ? 'ArrowDown' : 'ArrowRight'\n const backward = isVertical.value ? 'ArrowUp' : 'ArrowLeft'\n const grabbed = sort.mode.value === 'keyboard'\n\n if (event.key === 'Escape') {\n if (!grabbed)\n return\n\n event.preventDefault()\n cancel()\n return\n }\n\n if (event.key === ' ' || event.key === 'Enter') {\n event.preventDefault()\n if (grabbed)\n void drop(key)\n else grab(key)\n return\n }\n\n if (grabbed) {\n // Взятая строка забирает стрелки себе: двигается она, а не фокус.\n if (event.key === forward || event.key === backward) {\n event.preventDefault()\n moveTargetBy(event.key === forward ? 1 : -1)\n }\n return\n }\n\n roving.handleNavigationKeys(event)\n}\n\n/** Фокус ушёл из списка — взятая строка не может остаться взятой навсегда. */\nfunction onFocusout(event: FocusEvent): void {\n if (sort.mode.value !== 'keyboard')\n return\n\n const next = event.relatedTarget as Node | null\n if (next && listEl.value?.contains(next))\n return\n\n cancel()\n}\n\nconst resolvedEmptyText = computed(() => props.emptyText ?? t('gr.sortable.empty', 'Nothing to sort yet'))\nconst resolvedLabel = computed(() => props.ariaLabel ?? t('gr.sortable.label', 'Sortable list'))\nconst handleLabel = computed(() => t('gr.sortable.handle', 'Reorder'))\n\nconst listStyle = computed(() => (props.maxHeight === undefined\n ? undefined\n : { maxHeight: typeof props.maxHeight === 'number' ? `${props.maxHeight}px` : props.maxHeight, overflow: 'auto' }))\n\ndefineExpose({\n /** Программная перестановка — та же, что делает перенос. */\n move: (from: number, to: number) => applyMove(from, to),\n focusItem: (index: number) => roving.focusKey(keys.value[index]),\n})\n\nif (__GR_DEV__) {\n watchEffect(() => {\n if (!Array.isArray(props.modelValue)) {\n console.warn(\n `[granularity] GrSortableList: обязательный проп \\`modelValue\\` должен быть массивом — получено ${String(props.modelValue)}.`,\n )\n }\n })\n}\n</script>\n\n<template>\n <GrCard :variant=\"variant\">\n <div\n ref=\"listEl\"\n role=\"list\"\n data-gr-sortable\n :aria-label=\"resolvedLabel\"\n :aria-disabled=\"disabled ? 'true' : undefined\"\n :class=\"[listClass[orientation], divided ? dividedClass : '']\"\n :style=\"listStyle\"\n @focusout=\"onFocusout\"\n >\n <div\n v-if=\"isEmpty\"\n data-gr-sortable-empty\n :class=\"emptyClass\"\n >\n <slot name=\"empty\">\n {{ resolvedEmptyText }}\n </slot>\n </div>\n\n <div\n v-for=\"(item, index) in modelValue\"\n v-else\n :key=\"keys[index]\"\n :ref=\"el => registerRow(keys[index], el)\"\n role=\"listitem\"\n data-gr-sortable-item\n :tabindex=\"disabled ? -1 : roving.tabindexFor(keys[index])\"\n :aria-roledescription=\"handleLabel\"\n :aria-posinset=\"index + 1\"\n :aria-setsize=\"count\"\n :aria-grabbed=\"sort.mode.value === 'keyboard' && sort.source.value === keys[index] ? 'true' : undefined\"\n :class=\"rowClass(index, keys[index])\"\n @keydown=\"onRowKeydown($event, keys[index])\"\n @focus=\"roving.setActive(keys[index])\"\n @pointerdown=\"handleOnly ? undefined : startPointer(keys[index], $event)\"\n >\n <button\n type=\"button\"\n tabindex=\"-1\"\n data-gr-sortable-handle\n :class=\"[handleClass, disabled ? handleDisabledClass : '']\"\n :aria-label=\"handleLabel\"\n :disabled=\"disabled\"\n @pointerdown=\"startPointer(keys[index], $event)\"\n @click.stop\n >\n <slot\n name=\"handle\"\n :item=\"item\"\n :index=\"index\"\n :disabled=\"disabled\"\n >\n <IconGripVertical class=\"block\" aria-hidden=\"true\" />\n </slot>\n </button>\n\n <div :class=\"contentClass\">\n <slot\n name=\"item\"\n :item=\"item\"\n :index=\"index\"\n :dragging=\"sort.mode.value === 'pointer' && sort.source.value === keys[index]\"\n :grabbed=\"sort.mode.value === 'keyboard' && sort.source.value === keys[index]\"\n >\n {{ item }}\n </slot>\n </div>\n </div>\n </div>\n </GrCard>\n</template>\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
1
  import { n as grSortableListSafelist, t as grSortableListConfig } from "../../chunks/config-CbEbLyDE.js";
2
- import { t as GrSortableList_default } from "./chunks/GrSortableList-CQCaWivn.js";
2
+ import { t as GrSortableList_default } from "./chunks/GrSortableList-ce1psLL0.js";
3
3
  export { GrSortableList_default as GrSortableList, GrSortableList_default as default, grSortableListConfig, grSortableListSafelist };
@@ -3,7 +3,7 @@ import { n as useGranularityTranslations } from "../../../chunks/granularityI18n
3
3
  import { t as check_default } from "../../../chunks/check-BZI24Qs4.js";
4
4
  import { t as GrProgressBar_default } from "../../GrProgressBar/chunks/GrProgressBar-C3NRBqIP.js";
5
5
  import { _ as stepsSizeClassBySize, a as stepsCompactBarClass, b as stepsTriggerEnabledClass, c as stepsCompactRowClass, d as stepsConnectorPendingClass, f as stepsDescriptionClass, g as stepsRootClass, h as stepsListClass, i as grStepsMarkerClass, l as stepsConnectorClass, m as stepsItemConnectedClass, o as stepsCompactCounterClass, p as stepsItemClass, r as grStepsLabelClass, s as stepsCompactLabelClass, u as stepsConnectorDoneClass, v as stepsTriggerClass, y as stepsTriggerClassByOrientation } from "../../../chunks/config-CLjjjPZj.js";
6
- import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, normalizeClass, openBlock, renderList, renderSlot, resolveDynamicComponent, toDisplayString, unref, withCtx } from "vue";
6
+ import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, normalizeClass, openBlock, renderList, renderSlot, resolveDynamicComponent, toDisplayString, unref, watchEffect, withCtx } from "vue";
7
7
  //#region src/components/GrSteps/stepsModel.ts
8
8
  function stepIndexOf(steps, value) {
9
9
  return steps.findIndex((step) => step.value === value);
@@ -200,6 +200,10 @@ var GrSteps_default = /* @__PURE__ */ defineComponent({
200
200
  isFirst: computed(() => previousEnterableIndex(props.steps, currentIndex.value) === -1),
201
201
  isLast: computed(() => nextEnterableIndex(props.steps, currentIndex.value) === -1)
202
202
  });
203
+ if (process.env.NODE_ENV !== "production") watchEffect(() => {
204
+ if (props.modelValue === void 0) console.warn(`[granularity] GrSteps: обязательный проп \`modelValue\` не передан — получено ${String(props.modelValue)}.`);
205
+ if (!Array.isArray(props.steps)) console.warn(`[granularity] GrSteps: обязательный проп \`steps\` должен быть массивом — получено ${String(props.steps)}.`);
206
+ });
203
207
  return (_ctx, _cache) => {
204
208
  return openBlock(), createElementBlock("nav", {
205
209
  "data-gr-steps": "",
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GrSteps-Cng-Wrjk.js","names":[],"sources":["../../../../src/components/GrSteps/stepsModel.ts","../../../../src/components/GrSteps/GrSteps.vue","../../../../src/components/GrSteps/GrSteps.vue"],"sourcesContent":["import type { Component } from 'vue'\n\n/**\n * Правила шагового мастера — чистая арифметика без Vue.\n *\n * Вынесено отдельным модулем и тестируется без монтирования: «можно ли уйти на\n * шаг N» и «какого он статуса» — вся содержательная логика компонента, и\n * проверять её через отрендеренный DOM было бы дороже и слабее (эталон —\n * `GrForm/validation.ts`).\n */\n\n/**\n * Состояние шага.\n *\n * `error` не выводится из позиции — его ставит приложение, когда шаг пройден,\n * но не прошёл проверку. Без него мастер не умеет сказать «на втором шаге\n * остались ошибки», ради чего гейт перехода и заводится.\n */\nexport type GrStepStatus = 'complete' | 'current' | 'upcoming' | 'error'\n\nexport type GrStep = {\n value: string\n label: string\n /** Вторая строка под подписью: что от пользователя ждут на этом шаге. */\n description?: string\n /**\n * Иконка вместо номера — декоративна. Vue-компонент либо класс иконки вашей\n * UnoCSS-сборки (`i-lucide-*` требует вашего `presetIcons`).\n */\n icon?: string | Component\n /** Явный статус. Не задан — выводится из позиции относительно текущего шага. */\n status?: GrStepStatus\n disabled?: boolean\n}\n\nexport function stepIndexOf(steps: readonly GrStep[], value: string): number {\n return steps.findIndex(step => step.value === value)\n}\n\n/**\n * Статус шага: явный побеждает позиционный.\n *\n * Позиция даёт три состояния из четырёх, и только `error` приходит снаружи —\n * вывести его неоткуда, ошибка живёт в форме, а не в порядке шагов.\n */\nexport function stepStatusAt(steps: readonly GrStep[], index: number, currentIndex: number): GrStepStatus {\n const explicit = steps[index]?.status\n if (explicit)\n return explicit\n\n if (index < currentIndex)\n return 'complete'\n if (index === currentIndex)\n return 'current'\n return 'upcoming'\n}\n\n/**\n * Можно ли уйти на шаг по его индексу.\n *\n * `linear` ограничивает **только движение вперёд**, и не одним шагом, а первым\n * непройденным: вернуться назад и снова прыгнуть на тот же шаг, до которого уже\n * дошёл, — нормальный сценарий правки заполненного мастера. Запрещать его\n * значило бы заставлять пользователя проходить всё заново ради одной опечатки.\n */\nexport function canEnterStep(options: {\n steps: readonly GrStep[]\n index: number\n currentIndex: number\n linear: boolean\n}): boolean {\n const { steps, index, currentIndex, linear } = options\n const step = steps[index]\n\n if (!step || step.disabled)\n return false\n if (index === currentIndex)\n return false\n if (!linear || index < currentIndex)\n return true\n\n return index <= furthestReachableIndex(steps, currentIndex)\n}\n\n/**\n * Дальний край доступного в линейном режиме: текущий шаг плюс идущие за ним\n * подряд помеченные пройденными. Шаг с ошибкой краем не является — он и есть\n * причина, по которой дальше идти нельзя.\n */\nfunction furthestReachableIndex(steps: readonly GrStep[], currentIndex: number): number {\n let furthest = currentIndex + 1\n\n for (let index = currentIndex + 1; index < steps.length; index += 1) {\n if (steps[index]?.status !== 'complete')\n break\n furthest = index + 1\n }\n\n return Math.min(furthest, steps.length - 1)\n}\n\n/** Индекс следующего доступного шага, или `-1`, если дальше идти некуда. */\nexport function nextEnterableIndex(steps: readonly GrStep[], currentIndex: number): number {\n for (let index = currentIndex + 1; index < steps.length; index += 1) {\n if (!steps[index]?.disabled)\n return index\n }\n\n return -1\n}\n\n/** Индекс предыдущего доступного шага, или `-1`. */\nexport function previousEnterableIndex(steps: readonly GrStep[], currentIndex: number): number {\n for (let index = currentIndex - 1; index >= 0; index -= 1) {\n if (!steps[index]?.disabled)\n return index\n }\n\n return -1\n}\n","<script setup lang=\"ts\">\nimport type { Component } from 'vue'\nimport { computed, watchEffect } from 'vue'\n\nimport IconCheck from '~icons/lucide/check'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport GrProgressBar from '../GrProgressBar/GrProgressBar.vue'\n\nimport type { GrStepsOrientation, GrStepsSize, GrStepsVariant } from './grStepsStyles'\nimport {\n grStepsLabelClass,\n grStepsMarkerClass,\n stepsCompactBarClass,\n stepsCompactCounterClass,\n stepsCompactLabelClass,\n stepsCompactRowClass,\n stepsConnectorClass,\n stepsConnectorDoneClass,\n stepsConnectorPendingClass,\n stepsDescriptionClass,\n stepsItemClass,\n stepsItemConnectedClass,\n stepsListClass,\n stepsRootClass,\n stepsSizeClassBySize,\n stepsTriggerClass,\n stepsTriggerClassByOrientation,\n stepsTriggerEnabledClass,\n} from './grStepsStyles'\nimport type { GrStep, GrStepStatus } from './stepsModel'\nimport {\n canEnterStep,\n nextEnterableIndex,\n previousEnterableIndex,\n stepIndexOf,\n stepStatusAt,\n} from './stepsModel'\n\n/**\n * Индикатор шагов многошагового мастера.\n *\n * По семантике это **навигация**, а не составной виджет выбора: `<nav>` со\n * списком, где пройденный шаг — кнопка со своей остановкой `Tab`, текущий —\n * `aria-current=\"step\"`, будущий — вне таб-порядка. Той же формы держатся\n * `GrBreadcrumbs` и `GrBottomNav`; `role=\"tab\"` здесь неприменим — tablist без\n * `tabpanel` это сломанный паттерн, а панель шага никакой роли не несёт.\n *\n * Контент шага компонент не рисует: его ставит потребитель обычным `v-if`.\n */\nexport interface GrStepsProps {\n modelValue: string\n steps: GrStep[]\n orientation?: GrStepsOrientation\n size?: GrStepsSize\n /** Компактный вид для узкой колонки: подпись текущего шага и полоса прогресса. */\n variant?: GrStepsVariant\n /**\n * Вперёд — не дальше первого непройденного. Назад свободно всегда: вернуться\n * и поправить заполненное — обычный сценарий, а не обход правила.\n */\n linear?: boolean\n /** Переход кликом по шагу. Выключен — лента становится только индикатором. */\n clickable?: boolean\n ariaLabel?: string\n /**\n * Гейт перехода. Вернул `false` — перехода нет.\n *\n * Сюда потребитель кладёт валидацию шага: `GrSteps` про `GrForm` ничего не\n * знает и знать не должен, а форма уже отдаёт `validate()`/`validateField()`.\n */\n beforeLeave?: (from: string, to: string) => boolean | Promise<boolean>\n}\n\nexport interface GrStepsEmits {\n (e: 'update:modelValue', value: string): void\n (e: 'change', value: string): void\n}\n\nconst props = withDefaults(defineProps<GrStepsProps>(), {\n // Дефолты живут в резолверах ниже: Vue подставил бы свой раньше, чем\n // компонент заглянет в `GrConfigProvider`.\n orientation: undefined,\n size: undefined,\n variant: undefined,\n linear: undefined,\n clickable: undefined,\n ariaLabel: undefined,\n beforeLeave: undefined,\n})\n\nconst emit = defineEmits<GrStepsEmits>()\n\ndefineSlots<{\n /**\n * Своя разметка пункта. Корневой тег, `aria-current` и клик остаются за\n * компонентом — иначе доступность шага пришлось бы собирать заново.\n */\n step?: (props: { step: GrStep, index: number, status: GrStepStatus, enterable: boolean }) => unknown\n}>()\n\nconst { t } = useGranularityTranslations()\n\nconst resolvedSize = useGrComponentSize<GrStepsSize>(() => props.size, { component: 'GrSteps' })\nconst resolvedOrientation = useGrComponentProp('GrSteps', 'orientation', () => props.orientation, 'horizontal')\nconst resolvedVariant = useGrComponentProp('GrSteps', 'variant', () => props.variant, 'steps')\nconst resolvedLinear = useGrComponentProp('GrSteps', 'linear', () => props.linear, true)\nconst resolvedClickable = useGrComponentProp('GrSteps', 'clickable', () => props.clickable, true)\n\nconst currentIndex = computed(() => stepIndexOf(props.steps, props.modelValue))\nconst currentStep = computed(() => props.steps[currentIndex.value])\n\nconst resolvedAriaLabel = computed(() => props.ariaLabel ?? t('gr.steps.label', 'Progress'))\n\nfunction statusAt(index: number): GrStepStatus {\n return stepStatusAt(props.steps, index, currentIndex.value)\n}\n\nfunction isEnterable(index: number): boolean {\n if (!resolvedClickable.value)\n return false\n return canEnterStep({\n steps: props.steps,\n index,\n currentIndex: currentIndex.value,\n linear: resolvedLinear.value,\n })\n}\n\n/**\n * Пройденный шаг печатает галку вместо номера, шаг с ошибкой — восклицание.\n * Оба декоративны: смысл несёт скрытая подпись состояния рядом с меткой.\n */\nfunction markerText(index: number): string {\n return statusAt(index) === 'error' ? '!' : String(index + 1)\n}\n\nfunction stateText(status: GrStepStatus): string {\n if (status === 'complete')\n return t('gr.steps.completed', 'completed')\n if (status === 'error')\n return t('gr.steps.error', 'has errors')\n return ''\n}\n\nconst statusText = computed(() => t('gr.steps.status', 'Step {step} of {count}', {\n step: currentIndex.value + 1,\n count: props.steps.length,\n}))\n\n/** Полоса компактного вида: доля пройденного, а не номер шага. */\nconst progressValue = computed(() => {\n if (props.steps.length <= 1)\n return 100\n return Math.round((currentIndex.value / (props.steps.length - 1)) * 100)\n})\n\nfunction tagFor(index: number): 'button' | 'span' {\n return isEnterable(index) ? 'button' : 'span'\n}\n\nfunction triggerClass(index: number): string {\n const base = `${stepsTriggerClass} ${stepsTriggerClassByOrientation[resolvedOrientation.value]}`\n return isEnterable(index) ? `${base} ${stepsTriggerEnabledClass}` : base\n}\n\n/** Место под соединитель нужно только тем пунктам, за которыми ещё есть шаг. */\nfunction itemClass(index: number): string {\n const base = stepsItemClass[resolvedOrientation.value]\n if (index >= props.steps.length - 1)\n return base\n return `${base} ${stepsItemConnectedClass[resolvedOrientation.value]}`.trim()\n}\n\nfunction connectorClass(index: number): string {\n const done = statusAt(index) === 'complete'\n return `${stepsConnectorClass[resolvedOrientation.value]} ${done ? stepsConnectorDoneClass : stepsConnectorPendingClass}`\n}\n\nfunction iconOf(step: GrStep): string | Component | undefined {\n return step.icon\n}\n\nasync function goToIndex(index: number): Promise<boolean> {\n const target = props.steps[index]\n if (!target || target.value === props.modelValue)\n return false\n\n if (props.beforeLeave) {\n const allowed = await props.beforeLeave(props.modelValue, target.value)\n if (!allowed)\n return false\n }\n\n emit('update:modelValue', target.value)\n emit('change', target.value)\n\n return true\n}\n\nasync function onStepClick(index: number): Promise<void> {\n if (!isEnterable(index))\n return\n await goToIndex(index)\n}\n\nasync function next(): Promise<boolean> {\n const index = nextEnterableIndex(props.steps, currentIndex.value)\n return index === -1 ? false : goToIndex(index)\n}\n\nasync function back(): Promise<boolean> {\n const index = previousEnterableIndex(props.steps, currentIndex.value)\n return index === -1 ? false : goToIndex(index)\n}\n\nasync function goTo(value: string): Promise<boolean> {\n return goToIndex(stepIndexOf(props.steps, value))\n}\n\ndefineExpose({\n next,\n back,\n goTo,\n isFirst: computed(() => previousEnterableIndex(props.steps, currentIndex.value) === -1),\n isLast: computed(() => nextEnterableIndex(props.steps, currentIndex.value) === -1),\n})\n\nif (__GR_DEV__) {\n watchEffect(() => {\n if (props.modelValue === undefined) {\n console.warn(\n `[granularity] GrSteps: обязательный проп \\`modelValue\\` не передан — получено ${String(props.modelValue)}.`,\n )\n }\n\n if (!Array.isArray(props.steps)) {\n console.warn(\n `[granularity] GrSteps: обязательный проп \\`steps\\` должен быть массивом — получено ${String(props.steps)}.`,\n )\n }\n })\n}\n</script>\n\n<template>\n <nav\n data-gr-steps\n :data-orientation=\"resolvedOrientation\"\n :class=\"[stepsRootClass, stepsSizeClassBySize[resolvedSize]]\"\n :aria-label=\"resolvedAriaLabel\"\n >\n <div v-if=\"resolvedVariant === 'compact'\" data-gr-steps-compact>\n <div :class=\"stepsCompactRowClass\">\n <span :class=\"stepsCompactLabelClass\">{{ currentStep?.label }}</span>\n <span :class=\"stepsCompactCounterClass\">{{ currentIndex + 1 }} / {{ steps.length }}</span>\n </div>\n <!--\n Полоса декоративна: то же самое уже сказано текстом рядом и скрытым\n живым регионом ниже, а второй `progressbar` в дереве заставил бы\n диктора читать прогресс дважды.\n -->\n <GrProgressBar\n :value=\"progressValue\"\n :size=\"resolvedSize === 'lg' ? 'md' : 'sm'\"\n :class=\"stepsCompactBarClass\"\n aria-hidden=\"true\"\n />\n </div>\n\n <ol v-else data-gr-steps-list :class=\"stepsListClass[resolvedOrientation]\">\n <li\n v-for=\"(step, index) in steps\"\n :key=\"step.value\"\n data-gr-step\n :data-value=\"step.value\"\n :data-status=\"statusAt(index)\"\n :class=\"itemClass(index)\"\n >\n <component\n :is=\"tagFor(index)\"\n data-gr-step-trigger\n :type=\"tagFor(index) === 'button' ? 'button' : undefined\"\n :class=\"triggerClass(index)\"\n :aria-current=\"statusAt(index) === 'current' ? 'step' : undefined\"\n :aria-disabled=\"step.disabled ? 'true' : undefined\"\n @click=\"tagFor(index) === 'button' ? onStepClick(index) : undefined\"\n >\n <slot\n name=\"step\"\n :step=\"step\"\n :index=\"index\"\n :status=\"statusAt(index)\"\n :enterable=\"isEnterable(index)\"\n >\n <span data-gr-step-marker :class=\"grStepsMarkerClass(statusAt(index))\" aria-hidden=\"true\">\n <component :is=\"iconOf(step)\" v-if=\"step.icon\" class=\"h-[60%] w-[60%]\" />\n <IconCheck v-else-if=\"statusAt(index) === 'complete'\" class=\"h-[60%] w-[60%]\" />\n <template v-else>{{ markerText(index) }}</template>\n </span>\n\n <span class=\"block min-w-0\">\n <span data-gr-step-label :class=\"grStepsLabelClass(statusAt(index))\">{{ step.label }}</span>\n <!--\n Состояние шага словом: пройденный и шаг с ошибкой иначе звучат\n одинаково — цвет маркера диктору недоступен.\n -->\n <span v-if=\"stateText(statusAt(index))\" class=\"sr-only\"> — {{ stateText(statusAt(index)) }}</span>\n <span v-if=\"step.description\" data-gr-step-description :class=\"stepsDescriptionClass\">\n {{ step.description }}\n </span>\n </span>\n </slot>\n </component>\n\n <span\n v-if=\"index < steps.length - 1\"\n data-gr-step-connector\n :class=\"connectorClass(index)\"\n aria-hidden=\"true\"\n />\n </li>\n </ol>\n\n <!--\n «Шаг 2 из 5» — состояние виджета, а не событие, поэтому живёт своим\n регионом в разметке, а не уходит в общий объявитель (`docs/announcer.md`).\n Образец — компактный индикатор `GrPagination`.\n -->\n <div data-gr-steps-status role=\"status\" class=\"sr-only\">\n{{ statusText }}\n</div>\n </nav>\n</template>\n","<script setup lang=\"ts\">\nimport type { Component } from 'vue'\nimport { computed, watchEffect } from 'vue'\n\nimport IconCheck from '~icons/lucide/check'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport GrProgressBar from '../GrProgressBar/GrProgressBar.vue'\n\nimport type { GrStepsOrientation, GrStepsSize, GrStepsVariant } from './grStepsStyles'\nimport {\n grStepsLabelClass,\n grStepsMarkerClass,\n stepsCompactBarClass,\n stepsCompactCounterClass,\n stepsCompactLabelClass,\n stepsCompactRowClass,\n stepsConnectorClass,\n stepsConnectorDoneClass,\n stepsConnectorPendingClass,\n stepsDescriptionClass,\n stepsItemClass,\n stepsItemConnectedClass,\n stepsListClass,\n stepsRootClass,\n stepsSizeClassBySize,\n stepsTriggerClass,\n stepsTriggerClassByOrientation,\n stepsTriggerEnabledClass,\n} from './grStepsStyles'\nimport type { GrStep, GrStepStatus } from './stepsModel'\nimport {\n canEnterStep,\n nextEnterableIndex,\n previousEnterableIndex,\n stepIndexOf,\n stepStatusAt,\n} from './stepsModel'\n\n/**\n * Индикатор шагов многошагового мастера.\n *\n * По семантике это **навигация**, а не составной виджет выбора: `<nav>` со\n * списком, где пройденный шаг — кнопка со своей остановкой `Tab`, текущий —\n * `aria-current=\"step\"`, будущий — вне таб-порядка. Той же формы держатся\n * `GrBreadcrumbs` и `GrBottomNav`; `role=\"tab\"` здесь неприменим — tablist без\n * `tabpanel` это сломанный паттерн, а панель шага никакой роли не несёт.\n *\n * Контент шага компонент не рисует: его ставит потребитель обычным `v-if`.\n */\nexport interface GrStepsProps {\n modelValue: string\n steps: GrStep[]\n orientation?: GrStepsOrientation\n size?: GrStepsSize\n /** Компактный вид для узкой колонки: подпись текущего шага и полоса прогресса. */\n variant?: GrStepsVariant\n /**\n * Вперёд — не дальше первого непройденного. Назад свободно всегда: вернуться\n * и поправить заполненное — обычный сценарий, а не обход правила.\n */\n linear?: boolean\n /** Переход кликом по шагу. Выключен — лента становится только индикатором. */\n clickable?: boolean\n ariaLabel?: string\n /**\n * Гейт перехода. Вернул `false` — перехода нет.\n *\n * Сюда потребитель кладёт валидацию шага: `GrSteps` про `GrForm` ничего не\n * знает и знать не должен, а форма уже отдаёт `validate()`/`validateField()`.\n */\n beforeLeave?: (from: string, to: string) => boolean | Promise<boolean>\n}\n\nexport interface GrStepsEmits {\n (e: 'update:modelValue', value: string): void\n (e: 'change', value: string): void\n}\n\nconst props = withDefaults(defineProps<GrStepsProps>(), {\n // Дефолты живут в резолверах ниже: Vue подставил бы свой раньше, чем\n // компонент заглянет в `GrConfigProvider`.\n orientation: undefined,\n size: undefined,\n variant: undefined,\n linear: undefined,\n clickable: undefined,\n ariaLabel: undefined,\n beforeLeave: undefined,\n})\n\nconst emit = defineEmits<GrStepsEmits>()\n\ndefineSlots<{\n /**\n * Своя разметка пункта. Корневой тег, `aria-current` и клик остаются за\n * компонентом — иначе доступность шага пришлось бы собирать заново.\n */\n step?: (props: { step: GrStep, index: number, status: GrStepStatus, enterable: boolean }) => unknown\n}>()\n\nconst { t } = useGranularityTranslations()\n\nconst resolvedSize = useGrComponentSize<GrStepsSize>(() => props.size, { component: 'GrSteps' })\nconst resolvedOrientation = useGrComponentProp('GrSteps', 'orientation', () => props.orientation, 'horizontal')\nconst resolvedVariant = useGrComponentProp('GrSteps', 'variant', () => props.variant, 'steps')\nconst resolvedLinear = useGrComponentProp('GrSteps', 'linear', () => props.linear, true)\nconst resolvedClickable = useGrComponentProp('GrSteps', 'clickable', () => props.clickable, true)\n\nconst currentIndex = computed(() => stepIndexOf(props.steps, props.modelValue))\nconst currentStep = computed(() => props.steps[currentIndex.value])\n\nconst resolvedAriaLabel = computed(() => props.ariaLabel ?? t('gr.steps.label', 'Progress'))\n\nfunction statusAt(index: number): GrStepStatus {\n return stepStatusAt(props.steps, index, currentIndex.value)\n}\n\nfunction isEnterable(index: number): boolean {\n if (!resolvedClickable.value)\n return false\n return canEnterStep({\n steps: props.steps,\n index,\n currentIndex: currentIndex.value,\n linear: resolvedLinear.value,\n })\n}\n\n/**\n * Пройденный шаг печатает галку вместо номера, шаг с ошибкой — восклицание.\n * Оба декоративны: смысл несёт скрытая подпись состояния рядом с меткой.\n */\nfunction markerText(index: number): string {\n return statusAt(index) === 'error' ? '!' : String(index + 1)\n}\n\nfunction stateText(status: GrStepStatus): string {\n if (status === 'complete')\n return t('gr.steps.completed', 'completed')\n if (status === 'error')\n return t('gr.steps.error', 'has errors')\n return ''\n}\n\nconst statusText = computed(() => t('gr.steps.status', 'Step {step} of {count}', {\n step: currentIndex.value + 1,\n count: props.steps.length,\n}))\n\n/** Полоса компактного вида: доля пройденного, а не номер шага. */\nconst progressValue = computed(() => {\n if (props.steps.length <= 1)\n return 100\n return Math.round((currentIndex.value / (props.steps.length - 1)) * 100)\n})\n\nfunction tagFor(index: number): 'button' | 'span' {\n return isEnterable(index) ? 'button' : 'span'\n}\n\nfunction triggerClass(index: number): string {\n const base = `${stepsTriggerClass} ${stepsTriggerClassByOrientation[resolvedOrientation.value]}`\n return isEnterable(index) ? `${base} ${stepsTriggerEnabledClass}` : base\n}\n\n/** Место под соединитель нужно только тем пунктам, за которыми ещё есть шаг. */\nfunction itemClass(index: number): string {\n const base = stepsItemClass[resolvedOrientation.value]\n if (index >= props.steps.length - 1)\n return base\n return `${base} ${stepsItemConnectedClass[resolvedOrientation.value]}`.trim()\n}\n\nfunction connectorClass(index: number): string {\n const done = statusAt(index) === 'complete'\n return `${stepsConnectorClass[resolvedOrientation.value]} ${done ? stepsConnectorDoneClass : stepsConnectorPendingClass}`\n}\n\nfunction iconOf(step: GrStep): string | Component | undefined {\n return step.icon\n}\n\nasync function goToIndex(index: number): Promise<boolean> {\n const target = props.steps[index]\n if (!target || target.value === props.modelValue)\n return false\n\n if (props.beforeLeave) {\n const allowed = await props.beforeLeave(props.modelValue, target.value)\n if (!allowed)\n return false\n }\n\n emit('update:modelValue', target.value)\n emit('change', target.value)\n\n return true\n}\n\nasync function onStepClick(index: number): Promise<void> {\n if (!isEnterable(index))\n return\n await goToIndex(index)\n}\n\nasync function next(): Promise<boolean> {\n const index = nextEnterableIndex(props.steps, currentIndex.value)\n return index === -1 ? false : goToIndex(index)\n}\n\nasync function back(): Promise<boolean> {\n const index = previousEnterableIndex(props.steps, currentIndex.value)\n return index === -1 ? false : goToIndex(index)\n}\n\nasync function goTo(value: string): Promise<boolean> {\n return goToIndex(stepIndexOf(props.steps, value))\n}\n\ndefineExpose({\n next,\n back,\n goTo,\n isFirst: computed(() => previousEnterableIndex(props.steps, currentIndex.value) === -1),\n isLast: computed(() => nextEnterableIndex(props.steps, currentIndex.value) === -1),\n})\n\nif (__GR_DEV__) {\n watchEffect(() => {\n if (props.modelValue === undefined) {\n console.warn(\n `[granularity] GrSteps: обязательный проп \\`modelValue\\` не передан — получено ${String(props.modelValue)}.`,\n )\n }\n\n if (!Array.isArray(props.steps)) {\n console.warn(\n `[granularity] GrSteps: обязательный проп \\`steps\\` должен быть массивом — получено ${String(props.steps)}.`,\n )\n }\n })\n}\n</script>\n\n<template>\n <nav\n data-gr-steps\n :data-orientation=\"resolvedOrientation\"\n :class=\"[stepsRootClass, stepsSizeClassBySize[resolvedSize]]\"\n :aria-label=\"resolvedAriaLabel\"\n >\n <div v-if=\"resolvedVariant === 'compact'\" data-gr-steps-compact>\n <div :class=\"stepsCompactRowClass\">\n <span :class=\"stepsCompactLabelClass\">{{ currentStep?.label }}</span>\n <span :class=\"stepsCompactCounterClass\">{{ currentIndex + 1 }} / {{ steps.length }}</span>\n </div>\n <!--\n Полоса декоративна: то же самое уже сказано текстом рядом и скрытым\n живым регионом ниже, а второй `progressbar` в дереве заставил бы\n диктора читать прогресс дважды.\n -->\n <GrProgressBar\n :value=\"progressValue\"\n :size=\"resolvedSize === 'lg' ? 'md' : 'sm'\"\n :class=\"stepsCompactBarClass\"\n aria-hidden=\"true\"\n />\n </div>\n\n <ol v-else data-gr-steps-list :class=\"stepsListClass[resolvedOrientation]\">\n <li\n v-for=\"(step, index) in steps\"\n :key=\"step.value\"\n data-gr-step\n :data-value=\"step.value\"\n :data-status=\"statusAt(index)\"\n :class=\"itemClass(index)\"\n >\n <component\n :is=\"tagFor(index)\"\n data-gr-step-trigger\n :type=\"tagFor(index) === 'button' ? 'button' : undefined\"\n :class=\"triggerClass(index)\"\n :aria-current=\"statusAt(index) === 'current' ? 'step' : undefined\"\n :aria-disabled=\"step.disabled ? 'true' : undefined\"\n @click=\"tagFor(index) === 'button' ? onStepClick(index) : undefined\"\n >\n <slot\n name=\"step\"\n :step=\"step\"\n :index=\"index\"\n :status=\"statusAt(index)\"\n :enterable=\"isEnterable(index)\"\n >\n <span data-gr-step-marker :class=\"grStepsMarkerClass(statusAt(index))\" aria-hidden=\"true\">\n <component :is=\"iconOf(step)\" v-if=\"step.icon\" class=\"h-[60%] w-[60%]\" />\n <IconCheck v-else-if=\"statusAt(index) === 'complete'\" class=\"h-[60%] w-[60%]\" />\n <template v-else>{{ markerText(index) }}</template>\n </span>\n\n <span class=\"block min-w-0\">\n <span data-gr-step-label :class=\"grStepsLabelClass(statusAt(index))\">{{ step.label }}</span>\n <!--\n Состояние шага словом: пройденный и шаг с ошибкой иначе звучат\n одинаково — цвет маркера диктору недоступен.\n -->\n <span v-if=\"stateText(statusAt(index))\" class=\"sr-only\"> — {{ stateText(statusAt(index)) }}</span>\n <span v-if=\"step.description\" data-gr-step-description :class=\"stepsDescriptionClass\">\n {{ step.description }}\n </span>\n </span>\n </slot>\n </component>\n\n <span\n v-if=\"index < steps.length - 1\"\n data-gr-step-connector\n :class=\"connectorClass(index)\"\n aria-hidden=\"true\"\n />\n </li>\n </ol>\n\n <!--\n «Шаг 2 из 5» — состояние виджета, а не событие, поэтому живёт своим\n регионом в разметке, а не уходит в общий объявитель (`docs/announcer.md`).\n Образец — компактный индикатор `GrPagination`.\n -->\n <div data-gr-steps-status role=\"status\" class=\"sr-only\">\n{{ statusText }}\n</div>\n </nav>\n</template>\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
1
  import { n as grStepsSafelist, t as grStepsConfig } from "../../chunks/config-CLjjjPZj.js";
2
- import { t as GrSteps_default } from "./chunks/GrSteps-DXj7m-WO.js";
2
+ import { t as GrSteps_default } from "./chunks/GrSteps-Cng-Wrjk.js";
3
3
  export { GrSteps_default as GrSteps, GrSteps_default as default, grStepsConfig, grStepsSafelist };