@feugene/granularity 0.20.0 → 0.22.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 (210) hide show
  1. package/dist/chunks/{config-BJ7Bv_G2.js → config-BC8d_smz.js} +10 -2
  2. package/dist/chunks/config-BC8d_smz.js.map +1 -0
  3. package/dist/chunks/config-BDfTXHo1.js.map +1 -0
  4. package/dist/chunks/config-BEVBsvni2.js.map +1 -0
  5. package/dist/chunks/config-BTCOJO5z2.js.map +1 -0
  6. package/dist/chunks/config-B_qi4Fma.js +92 -0
  7. package/dist/chunks/config-B_qi4Fma.js.map +1 -0
  8. package/dist/chunks/{config-gwro_7yn.js → config-Bm_Sovla.js} +6 -2
  9. package/dist/chunks/config-Bm_Sovla.js.map +1 -0
  10. package/dist/chunks/{config-BKvX0q10.js → config-C5mO5DYZ.js} +10 -1
  11. package/dist/chunks/config-C5mO5DYZ.js.map +1 -0
  12. package/dist/chunks/config-CIcynHGr.js +96 -0
  13. package/dist/chunks/config-CIcynHGr.js.map +1 -0
  14. package/dist/chunks/{config-BsuDM3Hb.js → config-CKULiuvc.js} +15 -1
  15. package/dist/chunks/config-CKULiuvc.js.map +1 -0
  16. package/dist/chunks/config-COpuWGr_.js.map +1 -0
  17. package/dist/chunks/config-DBQQT-WP.js +66 -0
  18. package/dist/chunks/config-DBQQT-WP.js.map +1 -0
  19. package/dist/chunks/config-DU2DNSRx.js.map +1 -0
  20. package/dist/chunks/config-QJ54_fcM.js +61 -0
  21. package/dist/chunks/config-QJ54_fcM.js.map +1 -0
  22. package/dist/chunks/config-ScuvHw6k.js.map +1 -0
  23. package/dist/chunks/config-eXB87DOi.js.map +1 -0
  24. package/dist/chunks/config-jyZq8XKL.js +58 -0
  25. package/dist/chunks/config-jyZq8XKL.js.map +1 -0
  26. package/dist/chunks/copy-DChChcq-.js +29 -0
  27. package/dist/chunks/copy-DChChcq-.js.map +1 -0
  28. package/dist/chunks/{en-COdzxgAX.js → en-DCCFP4qu.js} +18 -2
  29. package/dist/chunks/{en-COdzxgAX.js.map → en-DCCFP4qu.js.map} +1 -1
  30. package/dist/chunks/{es-ClqJ-4hr.js → es-CT34HBVM.js} +18 -2
  31. package/dist/chunks/{es-ClqJ-4hr.js.map → es-CT34HBVM.js.map} +1 -1
  32. package/dist/chunks/{es-DGMHwknJ.js → es-DdJsxlUr.js} +3 -3
  33. package/dist/chunks/{es-DGMHwknJ.js.map → es-DdJsxlUr.js.map} +1 -1
  34. package/dist/chunks/{generated-BRkbhyxk.js → generated-CiCrGDQO.js} +147 -0
  35. package/dist/chunks/{generated-BRkbhyxk.js.map → generated-CiCrGDQO.js.map} +1 -1
  36. package/dist/chunks/{granular-provider-CZYm70Pm.js → granular-provider-CYroWMjO.js} +21 -11
  37. package/dist/chunks/granular-provider-CYroWMjO.js.map +1 -0
  38. package/dist/chunks/{ru-9z8ix45c.js → ru-CLiAMwrx.js} +18 -2
  39. package/dist/chunks/{ru-9z8ix45c.js.map → ru-CLiAMwrx.js.map} +1 -1
  40. package/dist/chunks/trending-up-Bf9AQmoq.js +42 -0
  41. package/dist/chunks/trending-up-Bf9AQmoq.js.map +1 -0
  42. package/dist/components/GrCard/chunks/GrCard-Bc7nRfbe.js +144 -0
  43. package/dist/components/GrCard/chunks/GrCard-Bc7nRfbe.js.map +1 -0
  44. package/dist/components/GrCard/index.js +2 -2
  45. package/dist/components/GrCodeBlock/chunks/GrCodeBlock-BmXCLgLS.js +315 -0
  46. package/dist/components/GrCodeBlock/chunks/GrCodeBlock-BmXCLgLS.js.map +1 -0
  47. package/dist/components/GrCodeBlock/index.js +3 -0
  48. package/dist/components/GrCodeBlock/styles.css +1 -0
  49. package/dist/components/GrCollapse/chunks/{GrCollapse-KFcrji1L.js → GrCollapse-C1rCfbc0.js} +2 -2
  50. package/dist/components/GrCollapse/chunks/{GrCollapse-KFcrji1L.js.map → GrCollapse-C1rCfbc0.js.map} +1 -1
  51. package/dist/components/GrCollapse/index.js +2 -2
  52. package/dist/components/GrDataTable/chunks/{GrDataTable-CNvZg2HZ.js → GrDataTable-B5MmZFUg.js} +67 -5
  53. package/dist/components/GrDataTable/chunks/GrDataTable-B5MmZFUg.js.map +1 -0
  54. package/dist/components/GrDataTable/index.js +2 -2
  55. package/dist/components/GrDelta/chunks/GrDelta-BMmpYz8K.js +0 -0
  56. package/dist/components/GrDelta/chunks/GrDelta-BMmpYz8K.js.map +1 -0
  57. package/dist/components/GrDelta/index.js +3 -0
  58. package/dist/components/GrDescriptionList/chunks/GrDescriptionList-pXh1LZEe.js +103 -0
  59. package/dist/components/GrDescriptionList/chunks/GrDescriptionList-pXh1LZEe.js.map +1 -0
  60. package/dist/components/GrDescriptionList/index.js +3 -0
  61. package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenu-LOaFSCyd.js → GrDropdownMenu-DyS6dWDp.js} +2 -2
  62. package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenu-LOaFSCyd.js.map → GrDropdownMenu-DyS6dWDp.js.map} +1 -1
  63. package/dist/components/GrDropdownMenu/index.js +2 -2
  64. package/dist/components/GrFilePreview/chunks/GrFilePreview-rbE3zL9w.js +315 -0
  65. package/dist/components/GrFilePreview/chunks/GrFilePreview-rbE3zL9w.js.map +1 -0
  66. package/dist/components/GrFilePreview/index.js +3 -0
  67. package/dist/components/GrForm/chunks/{GrForm-DdJw23Pz.js → GrForm-B_Qq_13k.js} +14 -7
  68. package/dist/components/GrForm/chunks/GrForm-B_Qq_13k.js.map +1 -0
  69. package/dist/components/GrForm/index.js +1 -1
  70. package/dist/components/GrImageViewer/chunks/{GrImageViewer-CMO10TS7.js.map → GrImageViewer-ng7el4wn.js.map} +1 -1
  71. package/dist/components/GrImageViewer/index.js +1 -1
  72. package/dist/components/GrInputTag/chunks/{GrInputTag-Dghhg03-.js.map → GrInputTag-C7QVPijj.js.map} +1 -1
  73. package/dist/components/GrInputTag/index.js +1 -1
  74. package/dist/components/GrJsonViewer/chunks/GrJsonViewer-DFUppRNb.js +389 -0
  75. package/dist/components/GrJsonViewer/chunks/GrJsonViewer-DFUppRNb.js.map +1 -0
  76. package/dist/components/GrJsonViewer/index.js +3 -0
  77. package/dist/components/GrList/chunks/{GrList-CHibO0xA.js → GrList-BRYhpqIe.js} +4 -4
  78. package/dist/components/GrList/chunks/GrList-BRYhpqIe.js.map +1 -0
  79. package/dist/components/GrList/index.js +2 -2
  80. package/dist/components/GrList/styles.css +1 -1
  81. package/dist/components/GrRadio/index.js +1 -1
  82. package/dist/components/GrSegmented/chunks/{GrSegmented-DP9WMAbm.js → GrSegmented-D1mjxF_O.js} +1 -1
  83. package/dist/components/GrSegmented/chunks/GrSegmented-D1mjxF_O.js.map +1 -0
  84. package/dist/components/GrSegmented/index.js +2 -2
  85. package/dist/components/GrSidebar/chunks/{GrSidebar-DG_t4aPE.js → GrSidebar-D7WxRtIu.js} +7 -1
  86. package/dist/components/GrSidebar/chunks/{GrSidebar-DG_t4aPE.js.map → GrSidebar-D7WxRtIu.js.map} +1 -1
  87. package/dist/components/GrSidebar/index.js +1 -1
  88. package/dist/components/GrSortableList/chunks/{GrSortableList-DJFPdUVv.js → GrSortableList-DPjxilmN.js} +1 -1
  89. package/dist/components/GrSortableList/chunks/{GrSortableList-DJFPdUVv.js.map → GrSortableList-DPjxilmN.js.map} +1 -1
  90. package/dist/components/GrSortableList/index.js +1 -1
  91. package/dist/components/GrStatistic/chunks/{GrStatistic-C3VpykHo.js → GrStatistic-DkzWUAw1.js} +8 -41
  92. package/dist/components/GrStatistic/chunks/GrStatistic-DkzWUAw1.js.map +1 -0
  93. package/dist/components/GrStatistic/index.js +1 -1
  94. package/dist/components/GrTabPanels/index.js +1 -1
  95. package/dist/components/GrTextarea/index.js +1 -1
  96. package/dist/components/GrTimeline/chunks/{GrTimeline-DlTraRn5.js → GrTimeline-Ch9sHzwL.js} +1 -1
  97. package/dist/components/GrTimeline/chunks/GrTimeline-Ch9sHzwL.js.map +1 -0
  98. package/dist/components/GrTimeline/index.js +2 -2
  99. package/dist/components/GrTimeline/styles.css +1 -1
  100. package/dist/components/GrToaster/chunks/{GrToaster-Db2NgrXu.js → GrToaster-DJUzx3sR.js} +1 -1
  101. package/dist/components/GrToaster/chunks/{GrToaster-Db2NgrXu.js.map → GrToaster-DJUzx3sR.js.map} +1 -1
  102. package/dist/components/GrToaster/index.js +2 -2
  103. package/dist/composables/useToast.js +14 -2
  104. package/dist/composables/useToast.js.map +1 -1
  105. package/dist/granular-provider-node.js +1 -1
  106. package/dist/granular-provider.js +1 -1
  107. package/dist/i18n/all.js +1 -1
  108. package/dist/i18n/index.js +1 -1
  109. package/dist/index.js +38 -28
  110. package/dist/theme.js +1 -1
  111. package/dist/tokens.js +1 -1
  112. package/dist/types/components/GrButton/GrButton.vue.d.ts +1 -1
  113. package/dist/types/components/GrCard/GrCard.vue.d.ts +25 -6
  114. package/dist/types/components/GrCard/defaults.d.ts +2 -1
  115. package/dist/types/components/GrCard/grCardStyles.d.ts +8 -0
  116. package/dist/types/components/GrCard/index.d.ts +1 -1
  117. package/dist/types/components/GrCodeBlock/GrCodeBlock.vue.d.ts +79 -0
  118. package/dist/types/components/GrCodeBlock/config.d.ts +1 -0
  119. package/dist/types/components/GrCodeBlock/defaults.d.ts +21 -0
  120. package/dist/types/components/GrCodeBlock/grCodeBlockStyles.d.ts +53 -0
  121. package/dist/types/components/GrCodeBlock/index.d.ts +6 -0
  122. package/dist/types/components/GrCodeBlock/safelist.d.ts +1 -0
  123. package/dist/types/components/GrCodeBlock/serializeCode.d.ts +19 -0
  124. package/dist/types/components/GrCodeBlock/tokenizeJson.d.ts +24 -0
  125. package/dist/types/components/GrCollapse/GrCollapse.vue.d.ts +2 -2
  126. package/dist/types/components/GrCommandPalette/GrCommandPalette.vue.d.ts +2 -2
  127. package/dist/types/components/GrDataTable/GrDataTable.vue.d.ts +38 -1
  128. package/dist/types/components/GrDataTable/grDataTableStyles.d.ts +7 -0
  129. package/dist/types/components/GrDataTable/index.d.ts +1 -1
  130. package/dist/types/components/GrDelta/GrDelta.vue.d.ts +84 -0
  131. package/dist/types/components/GrDelta/config.d.ts +1 -0
  132. package/dist/types/components/GrDelta/defaults.d.ts +22 -0
  133. package/dist/types/components/GrDelta/deltaTone.d.ts +24 -0
  134. package/dist/types/components/GrDelta/grDeltaStyles.d.ts +38 -0
  135. package/dist/types/components/GrDelta/index.d.ts +6 -0
  136. package/dist/types/components/GrDelta/safelist.d.ts +1 -0
  137. package/dist/types/components/GrDescriptionList/GrDescriptionList.vue.d.ts +107 -0
  138. package/dist/types/components/GrDescriptionList/config.d.ts +1 -0
  139. package/dist/types/components/GrDescriptionList/defaults.d.ts +21 -0
  140. package/dist/types/components/GrDescriptionList/grDescriptionListStyles.d.ts +25 -0
  141. package/dist/types/components/GrDescriptionList/index.d.ts +6 -0
  142. package/dist/types/components/GrDescriptionList/safelist.d.ts +1 -0
  143. package/dist/types/components/GrDropdownMenu/GrDropdownMenu.vue.d.ts +1 -1
  144. package/dist/types/components/GrDropdownMenu/grDropdownMenuStyles.d.ts +10 -1
  145. package/dist/types/components/GrFilePreview/GrFilePreview.vue.d.ts +100 -0
  146. package/dist/types/components/GrFilePreview/config.d.ts +1 -0
  147. package/dist/types/components/GrFilePreview/defaults.d.ts +20 -0
  148. package/dist/types/components/GrFilePreview/fileKindOf.d.ts +14 -0
  149. package/dist/types/components/GrFilePreview/grFilePreviewStyles.d.ts +37 -0
  150. package/dist/types/components/GrFilePreview/index.d.ts +6 -0
  151. package/dist/types/components/GrFilePreview/safelist.d.ts +1 -0
  152. package/dist/types/components/GrForm/GrForm.vue.d.ts +47 -86
  153. package/dist/types/components/GrFormField/GrFormField.vue.d.ts +2 -2
  154. package/dist/types/components/GrImageViewer/GrImageViewer.vue.d.ts +1 -1
  155. package/dist/types/components/GrInput/GrInput.vue.d.ts +1 -1
  156. package/dist/types/components/GrInputTag/GrInputTag.vue.d.ts +1 -1
  157. package/dist/types/components/GrJsonViewer/GrJsonViewer.vue.d.ts +129 -0
  158. package/dist/types/components/GrJsonViewer/config.d.ts +1 -0
  159. package/dist/types/components/GrJsonViewer/defaults.d.ts +23 -0
  160. package/dist/types/components/GrJsonViewer/grJsonViewerStyles.d.ts +57 -0
  161. package/dist/types/components/GrJsonViewer/index.d.ts +9 -0
  162. package/dist/types/components/GrJsonViewer/jsonToNodes.d.ts +47 -0
  163. package/dist/types/components/GrJsonViewer/safelist.d.ts +1 -0
  164. package/dist/types/components/GrList/grListStyles.d.ts +13 -0
  165. package/dist/types/components/GrNumberInput/GrNumberInput.vue.d.ts +1 -1
  166. package/dist/types/components/GrRating/GrRating.vue.d.ts +1 -1
  167. package/dist/types/components/GrSegmented/GrSegmented.vue.d.ts +1 -1
  168. package/dist/types/components/GrStatistic/GrStatistic.vue.d.ts +2 -2
  169. package/dist/types/components/GrSwitch/GrSwitch.vue.d.ts +1 -1
  170. package/dist/types/components/GrTable/GrTable.vue.d.ts +2 -2
  171. package/dist/types/components/GrTabs/GrTabs.vue.d.ts +1 -1
  172. package/dist/types/components/GrTimeline/GrTimelineItem.vue.d.ts +3 -3
  173. package/dist/types/components/shared/sizes.d.ts +5 -1
  174. package/dist/types/composables/useToast.d.ts +11 -0
  175. package/dist/types/granular-provider/shared.d.ts +5 -0
  176. package/dist/types/i18n/messages/en.d.ts +28 -0
  177. package/dist/types/i18n/messages/es.d.ts +28 -0
  178. package/dist/types/i18n/messages/ru.d.ts +28 -0
  179. package/dist/types/index.d.ts +5 -0
  180. package/dist/web-types.json +661 -124
  181. package/package.json +22 -1
  182. package/dist/chunks/config-BDfTXHo12.js.map +0 -1
  183. package/dist/chunks/config-BEVBsvni.js.map +0 -1
  184. package/dist/chunks/config-BJ7Bv_G2.js.map +0 -1
  185. package/dist/chunks/config-BKvX0q10.js.map +0 -1
  186. package/dist/chunks/config-BTCOJO5z.js.map +0 -1
  187. package/dist/chunks/config-BsuDM3Hb.js.map +0 -1
  188. package/dist/chunks/config-COpuWGr_2.js.map +0 -1
  189. package/dist/chunks/config-DU2DNSRx2.js.map +0 -1
  190. package/dist/chunks/config-ScuvHw6k2.js.map +0 -1
  191. package/dist/chunks/config-eXB87DOi2.js.map +0 -1
  192. package/dist/chunks/config-gwro_7yn.js.map +0 -1
  193. package/dist/chunks/granular-provider-CZYm70Pm.js.map +0 -1
  194. package/dist/components/GrCard/chunks/GrCard-DfIZOP5N.js +0 -89
  195. package/dist/components/GrCard/chunks/GrCard-DfIZOP5N.js.map +0 -1
  196. package/dist/components/GrDataTable/chunks/GrDataTable-CNvZg2HZ.js.map +0 -1
  197. package/dist/components/GrForm/chunks/GrForm-DdJw23Pz.js.map +0 -1
  198. package/dist/components/GrList/chunks/GrList-CHibO0xA.js.map +0 -1
  199. package/dist/components/GrSegmented/chunks/GrSegmented-DP9WMAbm.js.map +0 -1
  200. package/dist/components/GrStatistic/chunks/GrStatistic-C3VpykHo.js.map +0 -1
  201. package/dist/components/GrTimeline/chunks/GrTimeline-DlTraRn5.js.map +0 -1
  202. package/dist/chunks/{config-BDfTXHo12.js → config-BDfTXHo1.js} +0 -0
  203. package/dist/chunks/{config-BEVBsvni.js → config-BEVBsvni2.js} +0 -0
  204. package/dist/chunks/{config-BTCOJO5z.js → config-BTCOJO5z2.js} +0 -0
  205. package/dist/chunks/{config-COpuWGr_2.js → config-COpuWGr_.js} +0 -0
  206. package/dist/chunks/{config-DU2DNSRx2.js → config-DU2DNSRx.js} +0 -0
  207. package/dist/chunks/{config-ScuvHw6k2.js → config-ScuvHw6k.js} +0 -0
  208. package/dist/chunks/{config-eXB87DOi2.js → config-eXB87DOi.js} +0 -0
  209. package/dist/components/GrImageViewer/chunks/{GrImageViewer-CMO10TS7.js → GrImageViewer-ng7el4wn.js} +1 -1
  210. package/dist/components/GrInputTag/chunks/{GrInputTag-Dghhg03-.js → GrInputTag-C7QVPijj.js} +1 -1
@@ -1 +0,0 @@
1
- {"version":3,"file":"GrSegmented-DP9WMAbm.js","names":[],"sources":["../../../../src/components/GrSegmented/GrSegmented.vue","../../../../src/components/GrSegmented/GrSegmented.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, useId, watch } 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'\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 /** Имя скрытых radio-inputs для интеграции с нативной формой. */\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\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, required: isRequired, readonly: isReadonly } = 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) emitValue(option)\n },\n})\n\nfunction onItemClick(option: GrSegmentedOption): void {\n emitValue(option)\n // Клик тоже переносит остановку `Tab`: иначе она осталась бы там, куда её\n // увела последняя стрелка.\n if (!isOptionBlocked(option)) 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</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 <span v-if=\"option.label\" :class=\"grSegmentedItemLabelClass\">\n {{ option.label }}\n </span>\n </slot>\n </button>\n </div>\n</template>","<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, useId, watch } 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'\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 /** Имя скрытых radio-inputs для интеграции с нативной формой. */\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\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, required: isRequired, readonly: isReadonly } = 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) emitValue(option)\n },\n})\n\nfunction onItemClick(option: GrSegmentedOption): void {\n emitValue(option)\n // Клик тоже переносит остановку `Tab`: иначе она осталась бы там, куда её\n // увела последняя стрелка.\n if (!isOptionBlocked(option)) 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</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 <span v-if=\"option.label\" :class=\"grSegmentedItemLabelClass\">\n {{ option.label }}\n </span>\n </slot>\n </button>\n </div>\n</template>"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"GrStatistic-C3VpykHo.js","names":["$slots"],"sources":["../../../../src/components/GrStatistic/countUp.ts","../../../../src/components/GrStatistic/formatStatisticValue.ts","../../../../src/components/GrStatistic/GrStatistic.vue","../../../../src/components/GrStatistic/GrStatistic.vue"],"sourcesContent":["/**\n * Перебор чисел для `GrStatistic`: чистая арифметика кадра без Vue и таймеров.\n *\n * Вынесено отдельным модулем по той же причине, что и `formatStatisticValue`:\n * кривая и округление проверяются напрямую, без монтирования и подмены `rAF`.\n */\n\n/** Быстрый старт, мягкая остановка — та же форма, что у `--gr-ease-out`. */\nexport function easeOutCubic(t: number): number {\n const clamped = Math.min(Math.max(t, 0), 1)\n return 1 - (1 - clamped) ** 3\n}\n\n/**\n * Значение кадра на `elapsed` миллисекунде перехода `from → to`.\n *\n * Округление до `precision` обязательно: без него кадры показывали бы\n * `1283.6666666666667` там, где показатель целый, и ширина строки прыгала бы\n * даже при `tabular-nums`. Последний кадр возвращает ровно `to` — приближение\n * не должно оставлять показатель в 1283.9999.\n */\nexport function countUpFrame(\n from: number,\n to: number,\n elapsed: number,\n duration: number,\n precision?: number,\n): number {\n if (!(duration > 0) || elapsed >= duration) return to\n\n const digits = Math.min(Math.max(precision ?? 0, 0), 20)\n const value = from + (to - from) * easeOutCubic(elapsed / duration)\n\n return Number(value.toFixed(digits))\n}\n","export type GrStatisticFormatOptions = {\n /** Число знаков после запятой. `undefined` — оставить как есть. */\n precision?: number\n /** Разделитель групп разрядов. Сильнее локали. */\n groupSeparator?: string\n /** Десятичный разделитель. Сильнее локали. */\n decimalSeparator?: string\n /** BCP-47 локаль: разделители берутся из `Intl.NumberFormat`. */\n locale?: string\n}\n\nconst DEFAULT_GROUP_SEPARATOR = ' '\nconst DEFAULT_DECIMAL_SEPARATOR = '.'\n\n/**\n * Форматирует значение показателя: фиксирует точность и расставляет разделители.\n *\n * Нечисловые значения возвращаются без изменений — это позволяет показывать в\n * `GrStatistic` уже готовые строки («2 ч 15 мин», «—») без обходных пропов.\n * Вынесено из SFC отдельным модулем, чтобы тестировалось без монтирования.\n *\n * Порядок разрешения разделителей: явный проп → локаль → встроенный дефолт.\n */\nexport function formatStatisticValue(\n value: number | string,\n options: GrStatisticFormatOptions = {},\n): string {\n const { precision, groupSeparator, decimalSeparator, locale } = options\n\n const numeric = typeof value === 'number' ? value : Number(value)\n if (typeof value === 'string' && (value.trim() === '' || !Number.isFinite(numeric))) {\n return value\n }\n if (!Number.isFinite(numeric)) return String(value)\n\n if (locale) {\n return formatByLocale(numeric, locale, { precision, groupSeparator, decimalSeparator })\n }\n\n const fixed = precision === undefined ? String(numeric) : numeric.toFixed(precision)\n // `toFixed` не даёт экспоненциальной записи, а `String(число)` — может (1e21+).\n // В таком случае группировать нечего: возвращаем как есть.\n if (fixed.includes('e') || fixed.includes('E')) return fixed\n\n const negative = fixed.startsWith('-')\n const [intPart, fracPart] = (negative ? fixed.slice(1) : fixed).split('.')\n\n const separator = groupSeparator ?? DEFAULT_GROUP_SEPARATOR\n const grouped = separator\n ? intPart.replace(/\\B(?=(\\d{3})+(?!\\d))/g, separator)\n : intPart\n\n return [\n negative ? '-' : '',\n grouped,\n fracPart ? `${decimalSeparator ?? DEFAULT_DECIMAL_SEPARATOR}${fracPart}` : '',\n ].join('')\n}\n\n/**\n * `formatToParts` вместо `format`: даёт заменить отдельные разделители, оставив\n * остальные правила локали (индийская группировка, знак минуса, порядок частей).\n */\nfunction formatByLocale(\n value: number,\n locale: string,\n options: Pick<GrStatisticFormatOptions, 'precision' | 'groupSeparator' | 'decimalSeparator'>,\n): string {\n const { precision, groupSeparator, decimalSeparator } = options\n // `Intl` принимает не больше 20 знаков и падает `RangeError` на большем,\n // тогда как ручная ветка через `toFixed` пережила бы до 100.\n const digits = precision === undefined ? undefined : Math.min(Math.max(precision, 0), 20)\n\n return new Intl.NumberFormat(locale, {\n useGrouping: true,\n minimumFractionDigits: digits,\n maximumFractionDigits: digits ?? 20,\n })\n .formatToParts(value)\n .map((part) => {\n if (part.type === 'group' && groupSeparator !== undefined) return groupSeparator\n if (part.type === 'decimal' && decimalSeparator !== undefined) return decimalSeparator\n return part.value\n })\n .join('')\n}\n","<script setup lang=\"ts\">\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport { computed, markRaw, onBeforeUnmount, onMounted, ref, useSlots, watch, type Component } from 'vue'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { iconClass, iconTag } from '../shared/icon'\nimport GrSkeleton from '../GrSkeleton/GrSkeleton.vue'\n\nimport IconMinus from '~icons/lucide/minus'\nimport IconTrendingDown from '~icons/lucide/trending-down'\nimport IconTrendingUp from '~icons/lucide/trending-up'\n\nimport { countUpFrame } from './countUp'\nimport { formatStatisticValue } from './formatStatisticValue'\n\nimport {\n statisticAffixSizeBySize,\n statisticPlaceholderHeightBySize,\n statisticRootClass,\n statisticTitleClass,\n statisticTitleSizeBySize,\n statisticTrendClassByTrend,\n statisticTrendSizeBySize,\n statisticValueClass,\n type GrStatisticSize,\n type GrStatisticTone,\n type GrStatisticTrend,\n} from './grStatisticStyles'\n\nexport type {\n GrStatisticSize,\n GrStatisticTone,\n GrStatisticTrend,\n} from './grStatisticStyles'\nexport type { GrStatisticFormatOptions } from './formatStatisticValue'\n\n/**\n * Публичный GR-примитив «Statistic» — крупный числовой показатель с подписью,\n * префиксом/суффиксом и строкой динамики.\n *\n * Компонент презентационный: не грузит данные и не считает дельту, только\n * форматирует и подаёт. Форматирование — чистая функция `formatStatisticValue`.\n */\nexport interface GrStatisticProps {\n /** Значение показателя. Нечисловая строка выводится как есть. */\n value: number | string\n /** Подпись над значением. */\n title?: string\n /** Число знаков после запятой. */\n precision?: number\n /** Разделитель разрядов. Сильнее локали; без него и без локали — узкий пробел. */\n groupSeparator?: string\n /** Десятичный разделитель. Сильнее локали; без него и без локали — точка. */\n decimalSeparator?: string\n /**\n * BCP-47 локаль форматирования. Не задана — берётся из i18n-адаптера; нет и\n * адаптера — работают ручные разделители.\n */\n locale?: string\n /** Приписка перед значением (валюта, знак). */\n prefix?: string\n /** Приписка после значения (единица измерения, `%`). */\n suffix?: string\n /**\n * Иконка слева от блока: Vue-компонент либо класс иконки вашей UnoCSS-сборки\n * (`'i-lucide-users'` — тогда нужен ваш `presetIcons`, см. `docs/installation.md`).\n */\n icon?: string | Component\n size?: GrStatisticSize\n /** Тон значения; точечно перекрывается `--gr-statistic-value-color`. */\n tone?: GrStatisticTone\n /** Направление динамики — задаёт цвет и иконку строки под значением. */\n trend?: GrStatisticTrend\n /** Текст динамики (например `+12,5% к прошлой неделе`). */\n trendText?: string\n /** Состояние загрузки: вместо значения — плейсхолдер. */\n loading?: boolean\n /**\n * Перебирать числа при появлении и при смене значения. Нечисловое значение\n * ставится сразу, под `prefers-reduced-motion: reduce` — тоже.\n */\n animate?: boolean\n /**\n * Длительность перебора в миллисекундах. Не токеном: шкала `--gr-duration-*`\n * заканчивается на 300 мс и описывает смену состояния, а перебор чисел —\n * другой жанр, и его число живёт там же, где твин.\n */\n animateDuration?: number\n /** Свой корневой тег (`RouterLink`, `Link` от Inertia). Сильнее `href`. */\n as?: string | Component\n /** Показатель-ссылка: переход к деталям. */\n href?: string\n /** Показатель-кнопка: интерактивна вся плитка. */\n clickable?: boolean\n}\n\nexport interface GrStatisticEmits {\n (e: 'click', event: MouseEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrStatisticProps>(),\n {\n title: undefined,\n precision: undefined,\n // Дефолты живут в `formatStatisticValue`: иначе «пользователь задал пробел»\n // было бы неотличимо от «сработал дефолт», и локаль не смогла бы победить.\n groupSeparator: undefined,\n decimalSeparator: undefined,\n locale: undefined,\n prefix: undefined,\n suffix: undefined,\n icon: undefined,\n size: undefined,\n tone: 'neutral',\n trend: undefined,\n trendText: undefined,\n loading: false,\n animate: false,\n animateDuration: 600,\n as: undefined,\n href: undefined,\n clickable: false,\n },\n)\n\nconst emit = defineEmits<GrStatisticEmits>()\n\nconst slots = useSlots()\n\nconst { t, locale } = useGranularityTranslations()\n\nconst resolvedLocale = computed(() => props.locale ?? locale.value)\n\n/**\n * Число текущего кадра перебора. `null` — перебора нет, показываем сам проп:\n * так значение остаётся конечным и на сервере, и до первого твина.\n */\nconst frameValue = ref<number | null>(null)\n\nconst formatted = computed(() => formatStatisticValue(frameValue.value ?? props.value, {\n precision: props.precision,\n groupSeparator: props.groupSeparator,\n decimalSeparator: props.decimalSeparator,\n locale: resolvedLocale.value,\n}))\n\n/** Конечное значение — то, что читает диктор, пока на экране идёт перебор. */\nconst finalFormatted = computed(() => formatStatisticValue(props.value, {\n precision: props.precision,\n groupSeparator: props.groupSeparator,\n decimalSeparator: props.decimalSeparator,\n locale: resolvedLocale.value,\n}))\n\nconst isCounting = computed(() => frameValue.value !== null)\n\nconst hasTitle = computed(() => Boolean(props.title || slots.title))\n\nconst isInteractive = computed(() => Boolean(props.as || props.href || props.clickable))\n\nconst rootTag = computed<string | Component>(() => {\n if (props.as)\n return typeof props.as === 'string' ? props.as : markRaw(props.as)\n\n if (props.href)\n return 'a'\n\n return props.clickable ? 'button' : 'div'\n})\n\nconst rootClass = computed(() => statisticRootClass({ interactive: isInteractive.value }))\n\nfunction toNumber(value: number | string): number | null {\n const numeric = typeof value === 'number' ? value : Number(value)\n return Number.isFinite(numeric) ? numeric : null\n}\n\nlet frameHandle: number | null = null\n\nfunction stopCounting(): void {\n if (frameHandle !== null) cancelAnimationFrame(frameHandle)\n frameHandle = null\n}\n\n/**\n * Перебор чисел ведёт JS, а глобальный кламп в `base.css` гасит только CSS —\n * значит уважать «уменьшить движение» обязан сам компонент (`docs/motion.md`).\n * Читается в момент старта, а не в теле `setup`: `matchMedia` на сервере нет, и\n * первый клиентский рендер разошёлся бы с серверным.\n */\nfunction prefersReducedMotion(): boolean {\n return typeof window !== 'undefined'\n && window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches === true\n}\n\nfunction runCountUp(from: number, to: number): void {\n stopCounting()\n\n const duration = props.animateDuration\n if (!(duration > 0) || from === to || prefersReducedMotion()) {\n frameValue.value = null\n return\n }\n\n const started = performance.now()\n frameValue.value = from\n\n const step = (now: number): void => {\n const elapsed = now - started\n const value = countUpFrame(from, to, elapsed, duration, props.precision)\n\n if (elapsed >= duration) {\n // Конец перебора снимает и подмену значения, и подмену для диктора.\n frameValue.value = null\n frameHandle = null\n return\n }\n\n frameValue.value = value\n frameHandle = requestAnimationFrame(step)\n }\n\n frameHandle = requestAnimationFrame(step)\n}\n\n/** Перебирать можно только числа: «2 ч 15 мин» и «—» ставятся сразу. */\nfunction startIfNumeric(from: number | string, to: number | string): void {\n if (!props.animate || props.loading) {\n stopCounting()\n frameValue.value = null\n return\n }\n\n const target = toNumber(to)\n const source = toNumber(from)\n\n if (target === null || source === null) {\n stopCounting()\n frameValue.value = null\n return\n }\n\n runCountUp(source, target)\n}\n\n// Появление плитки считается переходом от нуля: ради этого эффекта проп и нужен.\nonMounted(() => startIfNumeric(0, props.value))\n\nwatch(() => props.value, (next, prev) => startIfNumeric(prev, next))\n\nonBeforeUnmount(stopCounting)\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrStatistic' })\n\nconst trendIconByTrend: Record<GrStatisticTrend, Component> = {\n up: IconTrendingUp,\n down: IconTrendingDown,\n flat: IconMinus,\n}\n\n// Иконка направления `aria-hidden`, а «+12,5 %» само по себе рост от падения не\n// отличает: направление доносит скрытая подпись.\nconst trendLabel = computed(() => {\n if (!props.trend) return undefined\n\n const labels = {\n up: () => t('gr.statistic.trendUp', 'Increase'),\n down: () => t('gr.statistic.trendDown', 'Decrease'),\n flat: () => t('gr.statistic.trendFlat', 'No change'),\n }\n\n return labels[props.trend]()\n})\n</script>\n\n<template>\n <component\n :is=\"rootTag\"\n data-gr-statistic\n :type=\"rootTag === 'button' ? 'button' : undefined\"\n :href=\"rootTag === 'a' ? href : undefined\"\n :class=\"rootClass\"\n @click=\"emit('click', $event)\"\n >\n <span\n v-if=\"icon || $slots.icon\"\n data-gr-statistic-icon\n class=\"mt-0.5 inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-[var(--gr-radius-lg)] bg-[var(--gr-muted)] text-[var(--gr-muted-fg)]\"\n >\n <slot name=\"icon\">\n <component :is=\"iconTag(icon)\" class=\"block h-4 w-4\" :class=\"iconClass(icon)\" aria-hidden=\"true\" />\n </slot>\n </span>\n\n <div class=\"min-w-0\">\n <!--\n Подпись и значение — пара «термин — значение», а не два соседних блока.\n `<dl>` появляется только вместе с подписью: список определений без `<dt>`\n — та же бессвязность, только с претензией на семантику. Поля обнуляются\n руками: preflight пакета сбрасывает их лишь у `body`.\n -->\n <component :is=\"hasTitle ? 'dl' : 'div'\" class=\"m-0\">\n <dt\n v-if=\"hasTitle\"\n data-gr-statistic-title\n :class=\"[statisticTitleClass, statisticTitleSizeBySize[resolvedSize]]\"\n >\n <slot name=\"title\">\n {{ title }}\n </slot>\n </dt>\n\n <!-- Плейсхолдер загрузки повторяет высоту строки значения, чтобы блок не прыгал. -->\n <component\n :is=\"hasTitle ? 'dd' : 'div'\"\n v-if=\"loading\"\n data-gr-statistic-placeholder\n data-testid=\"gr-statistic-placeholder\"\n class=\"m-0 mt-1\"\n role=\"status\"\n aria-busy=\"true\"\n >\n <GrSkeleton\n width=\"6rem\"\n :height=\"statisticPlaceholderHeightBySize[resolvedSize]\"\n rounded=\"var(--gr-radius-md)\"\n />\n <span class=\"sr-only\">{{ t('gr.loading.defaultText', 'Loading...') }}</span>\n </component>\n\n <component\n :is=\"hasTitle ? 'dd' : 'div'\"\n v-else\n data-gr-statistic-value\n data-testid=\"gr-statistic-value\"\n class=\"m-0 mt-0.5 flex items-baseline gap-1\"\n >\n <span\n v-if=\"prefix || $slots.prefix\"\n data-gr-statistic-prefix\n class=\"text-[var(--gr-muted-fg)]\"\n :class=\"statisticAffixSizeBySize[resolvedSize]\"\n >\n <slot name=\"prefix\">{{ prefix }}</slot>\n </span>\n\n <span\n data-gr-statistic-number\n :class=\"statisticValueClass({ size: resolvedSize, tone })\"\n :aria-hidden=\"isCounting ? 'true' : undefined\"\n >\n <slot>{{ formatted }}</slot>\n </span>\n\n <!--\n Пока идёт перебор, на экране промежуточное число. Диктору отдаётся\n конечное: «1 284» глазами и «743» в ушах — не шум, а неверные данные.\n -->\n <span v-if=\"isCounting\" data-gr-statistic-final class=\"sr-only\">{{ finalFormatted }}</span>\n\n <span\n v-if=\"suffix || $slots.suffix\"\n data-gr-statistic-suffix\n class=\"text-[var(--gr-muted-fg)]\"\n :class=\"statisticAffixSizeBySize[resolvedSize]\"\n >\n <slot name=\"suffix\">{{ suffix }}</slot>\n </span>\n </component>\n </component>\n\n <div\n v-if=\"trend || trendText || $slots.trend\"\n data-gr-statistic-trend\n data-testid=\"gr-statistic-trend\"\n class=\"mt-1 inline-flex items-center gap-1\"\n :class=\"[statisticTrendSizeBySize[resolvedSize], statisticTrendClassByTrend[trend ?? 'flat']]\"\n >\n <span v-if=\"trendLabel\" data-gr-statistic-trend-label class=\"sr-only\">{{ trendLabel }}</span>\n\n <slot name=\"trend\">\n <component\n :is=\"trendIconByTrend[trend]\"\n v-if=\"trend\"\n class=\"block h-3.5 w-3.5\"\n aria-hidden=\"true\"\n />\n <span>{{ trendText }}</span>\n </slot>\n </div>\n </div>\n </component>\n</template>\n","<script setup lang=\"ts\">\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport { computed, markRaw, onBeforeUnmount, onMounted, ref, useSlots, watch, type Component } from 'vue'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { iconClass, iconTag } from '../shared/icon'\nimport GrSkeleton from '../GrSkeleton/GrSkeleton.vue'\n\nimport IconMinus from '~icons/lucide/minus'\nimport IconTrendingDown from '~icons/lucide/trending-down'\nimport IconTrendingUp from '~icons/lucide/trending-up'\n\nimport { countUpFrame } from './countUp'\nimport { formatStatisticValue } from './formatStatisticValue'\n\nimport {\n statisticAffixSizeBySize,\n statisticPlaceholderHeightBySize,\n statisticRootClass,\n statisticTitleClass,\n statisticTitleSizeBySize,\n statisticTrendClassByTrend,\n statisticTrendSizeBySize,\n statisticValueClass,\n type GrStatisticSize,\n type GrStatisticTone,\n type GrStatisticTrend,\n} from './grStatisticStyles'\n\nexport type {\n GrStatisticSize,\n GrStatisticTone,\n GrStatisticTrend,\n} from './grStatisticStyles'\nexport type { GrStatisticFormatOptions } from './formatStatisticValue'\n\n/**\n * Публичный GR-примитив «Statistic» — крупный числовой показатель с подписью,\n * префиксом/суффиксом и строкой динамики.\n *\n * Компонент презентационный: не грузит данные и не считает дельту, только\n * форматирует и подаёт. Форматирование — чистая функция `formatStatisticValue`.\n */\nexport interface GrStatisticProps {\n /** Значение показателя. Нечисловая строка выводится как есть. */\n value: number | string\n /** Подпись над значением. */\n title?: string\n /** Число знаков после запятой. */\n precision?: number\n /** Разделитель разрядов. Сильнее локали; без него и без локали — узкий пробел. */\n groupSeparator?: string\n /** Десятичный разделитель. Сильнее локали; без него и без локали — точка. */\n decimalSeparator?: string\n /**\n * BCP-47 локаль форматирования. Не задана — берётся из i18n-адаптера; нет и\n * адаптера — работают ручные разделители.\n */\n locale?: string\n /** Приписка перед значением (валюта, знак). */\n prefix?: string\n /** Приписка после значения (единица измерения, `%`). */\n suffix?: string\n /**\n * Иконка слева от блока: Vue-компонент либо класс иконки вашей UnoCSS-сборки\n * (`'i-lucide-users'` — тогда нужен ваш `presetIcons`, см. `docs/installation.md`).\n */\n icon?: string | Component\n size?: GrStatisticSize\n /** Тон значения; точечно перекрывается `--gr-statistic-value-color`. */\n tone?: GrStatisticTone\n /** Направление динамики — задаёт цвет и иконку строки под значением. */\n trend?: GrStatisticTrend\n /** Текст динамики (например `+12,5% к прошлой неделе`). */\n trendText?: string\n /** Состояние загрузки: вместо значения — плейсхолдер. */\n loading?: boolean\n /**\n * Перебирать числа при появлении и при смене значения. Нечисловое значение\n * ставится сразу, под `prefers-reduced-motion: reduce` — тоже.\n */\n animate?: boolean\n /**\n * Длительность перебора в миллисекундах. Не токеном: шкала `--gr-duration-*`\n * заканчивается на 300 мс и описывает смену состояния, а перебор чисел —\n * другой жанр, и его число живёт там же, где твин.\n */\n animateDuration?: number\n /** Свой корневой тег (`RouterLink`, `Link` от Inertia). Сильнее `href`. */\n as?: string | Component\n /** Показатель-ссылка: переход к деталям. */\n href?: string\n /** Показатель-кнопка: интерактивна вся плитка. */\n clickable?: boolean\n}\n\nexport interface GrStatisticEmits {\n (e: 'click', event: MouseEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrStatisticProps>(),\n {\n title: undefined,\n precision: undefined,\n // Дефолты живут в `formatStatisticValue`: иначе «пользователь задал пробел»\n // было бы неотличимо от «сработал дефолт», и локаль не смогла бы победить.\n groupSeparator: undefined,\n decimalSeparator: undefined,\n locale: undefined,\n prefix: undefined,\n suffix: undefined,\n icon: undefined,\n size: undefined,\n tone: 'neutral',\n trend: undefined,\n trendText: undefined,\n loading: false,\n animate: false,\n animateDuration: 600,\n as: undefined,\n href: undefined,\n clickable: false,\n },\n)\n\nconst emit = defineEmits<GrStatisticEmits>()\n\nconst slots = useSlots()\n\nconst { t, locale } = useGranularityTranslations()\n\nconst resolvedLocale = computed(() => props.locale ?? locale.value)\n\n/**\n * Число текущего кадра перебора. `null` — перебора нет, показываем сам проп:\n * так значение остаётся конечным и на сервере, и до первого твина.\n */\nconst frameValue = ref<number | null>(null)\n\nconst formatted = computed(() => formatStatisticValue(frameValue.value ?? props.value, {\n precision: props.precision,\n groupSeparator: props.groupSeparator,\n decimalSeparator: props.decimalSeparator,\n locale: resolvedLocale.value,\n}))\n\n/** Конечное значение — то, что читает диктор, пока на экране идёт перебор. */\nconst finalFormatted = computed(() => formatStatisticValue(props.value, {\n precision: props.precision,\n groupSeparator: props.groupSeparator,\n decimalSeparator: props.decimalSeparator,\n locale: resolvedLocale.value,\n}))\n\nconst isCounting = computed(() => frameValue.value !== null)\n\nconst hasTitle = computed(() => Boolean(props.title || slots.title))\n\nconst isInteractive = computed(() => Boolean(props.as || props.href || props.clickable))\n\nconst rootTag = computed<string | Component>(() => {\n if (props.as)\n return typeof props.as === 'string' ? props.as : markRaw(props.as)\n\n if (props.href)\n return 'a'\n\n return props.clickable ? 'button' : 'div'\n})\n\nconst rootClass = computed(() => statisticRootClass({ interactive: isInteractive.value }))\n\nfunction toNumber(value: number | string): number | null {\n const numeric = typeof value === 'number' ? value : Number(value)\n return Number.isFinite(numeric) ? numeric : null\n}\n\nlet frameHandle: number | null = null\n\nfunction stopCounting(): void {\n if (frameHandle !== null) cancelAnimationFrame(frameHandle)\n frameHandle = null\n}\n\n/**\n * Перебор чисел ведёт JS, а глобальный кламп в `base.css` гасит только CSS —\n * значит уважать «уменьшить движение» обязан сам компонент (`docs/motion.md`).\n * Читается в момент старта, а не в теле `setup`: `matchMedia` на сервере нет, и\n * первый клиентский рендер разошёлся бы с серверным.\n */\nfunction prefersReducedMotion(): boolean {\n return typeof window !== 'undefined'\n && window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches === true\n}\n\nfunction runCountUp(from: number, to: number): void {\n stopCounting()\n\n const duration = props.animateDuration\n if (!(duration > 0) || from === to || prefersReducedMotion()) {\n frameValue.value = null\n return\n }\n\n const started = performance.now()\n frameValue.value = from\n\n const step = (now: number): void => {\n const elapsed = now - started\n const value = countUpFrame(from, to, elapsed, duration, props.precision)\n\n if (elapsed >= duration) {\n // Конец перебора снимает и подмену значения, и подмену для диктора.\n frameValue.value = null\n frameHandle = null\n return\n }\n\n frameValue.value = value\n frameHandle = requestAnimationFrame(step)\n }\n\n frameHandle = requestAnimationFrame(step)\n}\n\n/** Перебирать можно только числа: «2 ч 15 мин» и «—» ставятся сразу. */\nfunction startIfNumeric(from: number | string, to: number | string): void {\n if (!props.animate || props.loading) {\n stopCounting()\n frameValue.value = null\n return\n }\n\n const target = toNumber(to)\n const source = toNumber(from)\n\n if (target === null || source === null) {\n stopCounting()\n frameValue.value = null\n return\n }\n\n runCountUp(source, target)\n}\n\n// Появление плитки считается переходом от нуля: ради этого эффекта проп и нужен.\nonMounted(() => startIfNumeric(0, props.value))\n\nwatch(() => props.value, (next, prev) => startIfNumeric(prev, next))\n\nonBeforeUnmount(stopCounting)\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrStatistic' })\n\nconst trendIconByTrend: Record<GrStatisticTrend, Component> = {\n up: IconTrendingUp,\n down: IconTrendingDown,\n flat: IconMinus,\n}\n\n// Иконка направления `aria-hidden`, а «+12,5 %» само по себе рост от падения не\n// отличает: направление доносит скрытая подпись.\nconst trendLabel = computed(() => {\n if (!props.trend) return undefined\n\n const labels = {\n up: () => t('gr.statistic.trendUp', 'Increase'),\n down: () => t('gr.statistic.trendDown', 'Decrease'),\n flat: () => t('gr.statistic.trendFlat', 'No change'),\n }\n\n return labels[props.trend]()\n})\n</script>\n\n<template>\n <component\n :is=\"rootTag\"\n data-gr-statistic\n :type=\"rootTag === 'button' ? 'button' : undefined\"\n :href=\"rootTag === 'a' ? href : undefined\"\n :class=\"rootClass\"\n @click=\"emit('click', $event)\"\n >\n <span\n v-if=\"icon || $slots.icon\"\n data-gr-statistic-icon\n class=\"mt-0.5 inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-[var(--gr-radius-lg)] bg-[var(--gr-muted)] text-[var(--gr-muted-fg)]\"\n >\n <slot name=\"icon\">\n <component :is=\"iconTag(icon)\" class=\"block h-4 w-4\" :class=\"iconClass(icon)\" aria-hidden=\"true\" />\n </slot>\n </span>\n\n <div class=\"min-w-0\">\n <!--\n Подпись и значение — пара «термин — значение», а не два соседних блока.\n `<dl>` появляется только вместе с подписью: список определений без `<dt>`\n — та же бессвязность, только с претензией на семантику. Поля обнуляются\n руками: preflight пакета сбрасывает их лишь у `body`.\n -->\n <component :is=\"hasTitle ? 'dl' : 'div'\" class=\"m-0\">\n <dt\n v-if=\"hasTitle\"\n data-gr-statistic-title\n :class=\"[statisticTitleClass, statisticTitleSizeBySize[resolvedSize]]\"\n >\n <slot name=\"title\">\n {{ title }}\n </slot>\n </dt>\n\n <!-- Плейсхолдер загрузки повторяет высоту строки значения, чтобы блок не прыгал. -->\n <component\n :is=\"hasTitle ? 'dd' : 'div'\"\n v-if=\"loading\"\n data-gr-statistic-placeholder\n data-testid=\"gr-statistic-placeholder\"\n class=\"m-0 mt-1\"\n role=\"status\"\n aria-busy=\"true\"\n >\n <GrSkeleton\n width=\"6rem\"\n :height=\"statisticPlaceholderHeightBySize[resolvedSize]\"\n rounded=\"var(--gr-radius-md)\"\n />\n <span class=\"sr-only\">{{ t('gr.loading.defaultText', 'Loading...') }}</span>\n </component>\n\n <component\n :is=\"hasTitle ? 'dd' : 'div'\"\n v-else\n data-gr-statistic-value\n data-testid=\"gr-statistic-value\"\n class=\"m-0 mt-0.5 flex items-baseline gap-1\"\n >\n <span\n v-if=\"prefix || $slots.prefix\"\n data-gr-statistic-prefix\n class=\"text-[var(--gr-muted-fg)]\"\n :class=\"statisticAffixSizeBySize[resolvedSize]\"\n >\n <slot name=\"prefix\">{{ prefix }}</slot>\n </span>\n\n <span\n data-gr-statistic-number\n :class=\"statisticValueClass({ size: resolvedSize, tone })\"\n :aria-hidden=\"isCounting ? 'true' : undefined\"\n >\n <slot>{{ formatted }}</slot>\n </span>\n\n <!--\n Пока идёт перебор, на экране промежуточное число. Диктору отдаётся\n конечное: «1 284» глазами и «743» в ушах — не шум, а неверные данные.\n -->\n <span v-if=\"isCounting\" data-gr-statistic-final class=\"sr-only\">{{ finalFormatted }}</span>\n\n <span\n v-if=\"suffix || $slots.suffix\"\n data-gr-statistic-suffix\n class=\"text-[var(--gr-muted-fg)]\"\n :class=\"statisticAffixSizeBySize[resolvedSize]\"\n >\n <slot name=\"suffix\">{{ suffix }}</slot>\n </span>\n </component>\n </component>\n\n <div\n v-if=\"trend || trendText || $slots.trend\"\n data-gr-statistic-trend\n data-testid=\"gr-statistic-trend\"\n class=\"mt-1 inline-flex items-center gap-1\"\n :class=\"[statisticTrendSizeBySize[resolvedSize], statisticTrendClassByTrend[trend ?? 'flat']]\"\n >\n <span v-if=\"trendLabel\" data-gr-statistic-trend-label class=\"sr-only\">{{ trendLabel }}</span>\n\n <slot name=\"trend\">\n <component\n :is=\"trendIconByTrend[trend]\"\n v-if=\"trend\"\n class=\"block h-3.5 w-3.5\"\n aria-hidden=\"true\"\n />\n <span>{{ trendText }}</span>\n </slot>\n </div>\n </div>\n </component>\n</template>\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"GrTimeline-DlTraRn5.js","names":["$slots"],"sources":["../../../../src/components/GrTimeline/grTimelineContext.ts","../../../../src/components/GrTimeline/timelineGroups.ts","../../../../src/components/GrTimeline/GrTimeline.vue","../../../../src/components/GrTimeline/GrTimeline.vue","../../../../src/components/GrTimeline/GrTimelineItem.vue","../../../../src/components/GrTimeline/GrTimelineItem.vue"],"sourcesContent":["import type { ComputedRef, InjectionKey } from 'vue'\n\nimport type { GrTimelineDensity, GrTimelineLayout, GrTimelineOrientation } from './grTimelineStyles'\n\n/**\n * Уровень заголовка группы. Тот же довод, что у `GrCollapse`: лента в разделе\n * `<h4>` с захардкоженным `<h3>` рвёт навигацию по заголовкам.\n */\nexport const GR_TIMELINE_HEADING_LEVELS = [2, 3, 4, 5, 6] as const\n\nexport type GrTimelineHeadingLevel = typeof GR_TIMELINE_HEADING_LEVELS[number]\n\n/**\n * Пункт читает раскладку из контейнера, а не получает её пропом: иначе\n * `layout` пришлось бы дублировать на каждом `GrTimelineItem`.\n */\nexport interface GrTimelineContext {\n layout: ComputedRef<GrTimelineLayout>\n orientation: ComputedRef<GrTimelineOrientation>\n density: ComputedRef<GrTimelineDensity>\n}\n\nexport const GR_TIMELINE_CONTEXT: InjectionKey<GrTimelineContext> = Symbol('GR_TIMELINE_CONTEXT')\n","/**\n * Группировка ленты событий. Без Vue — чтобы проверялась без монтирования.\n */\n\nexport type GrTimelineGroupBy<T> = string | ((item: T, index: number) => string)\n\n/** Пункт вместе с его позицией в исходном наборе: ключи `v-for` считаются от неё. */\nexport interface GrTimelineEntry<T> {\n item: T\n index: number\n}\n\nexport interface GrTimelineGroup<T> {\n /** Ключ группы. Пустая строка — заголовка у группы нет. */\n key: string\n entries: GrTimelineEntry<T>[]\n}\n\nfunction keyOf<T>(item: T, index: number, groupBy: GrTimelineGroupBy<T>): string {\n if (typeof groupBy === 'function')\n return String(groupBy(item, index) ?? '')\n\n const value = (item as Record<string, unknown> | null | undefined)?.[groupBy]\n\n // `undefined` в поле — это «группы нет», а не группа с надписью «undefined».\n return value == null ? '' : String(value)\n}\n\n/**\n * Режет набор на группы, **не меняя порядок**: лента хронологична, и\n * пересортировать её компонент не вправе. Порядок групп — порядок первого\n * вхождения ключа; разорванная группа (`A B A`) собирается в одну.\n *\n * Без `groupBy` — одна безымянная группа: плоская лента остаётся частным\n * случаем, и второй ветки рендера у компонента не появляется.\n */\nexport function groupTimelineItems<T>(\n items: readonly T[],\n groupBy?: GrTimelineGroupBy<T>,\n): GrTimelineGroup<T>[] {\n const entries = items.map((item, index) => ({ item, index }))\n\n if (groupBy === undefined)\n return entries.length === 0 ? [] : [{ key: '', entries }]\n\n const byKey = new Map<string, GrTimelineGroup<T>>()\n\n for (const entry of entries) {\n const key = keyOf(entry.item, entry.index, groupBy)\n const group = byKey.get(key)\n\n if (group)\n group.entries.push(entry)\n else\n byKey.set(key, { key, entries: [entry] })\n }\n\n return [...byKey.values()]\n}\n","<script setup lang=\"ts\" generic=\"T = unknown\">\nimport { computed, provide, useSlots, watch } from 'vue'\n\nimport GrSkeleton from '../GrSkeleton/GrSkeleton.vue'\nimport { useGrComponentProp } from '../GrConfigProvider/context'\nimport { hasMeaningfulSlotContent } from '../shared/slotNodes'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n\nimport { GR_TIMELINE_CONTEXT, type GrTimelineHeadingLevel } from './grTimelineContext'\nimport {\n emptyClass,\n groupTitleClass,\n loadingRowClass,\n type GrTimelineDensity,\n type GrTimelineLayout,\n type GrTimelineOrientation,\n} from './grTimelineStyles'\nimport { groupTimelineItems, type GrTimelineGroupBy } from './timelineGroups'\n\nexport type {\n GrTimelineDensity,\n GrTimelineLayout,\n GrTimelineOrientation,\n} from './grTimelineStyles'\nexport type { GrTimelineGroupBy } from './timelineGroups'\n\nexport interface GrTimelineProps<TItem = unknown> {\n /**\n * Данные ленты. С ними пункты рисует слот `#item` — и только так лента знает\n * состав набора, то есть может его группировать.\n */\n items?: TItem[]\n /** Ключ пункта для `v-for`: имя поля или функция. Без него — индекс. */\n itemKey?: string | ((item: TItem, index: number) => string | number)\n /** Группировка: имя поля или функция от пункта. Заголовок группы — слот `#group`. */\n groupBy?: GrTimelineGroupBy<TItem>\n /** Уровень заголовка группы: лента обязана вписаться в структуру заголовков страницы. */\n groupHeadingLevel?: GrTimelineHeadingLevel\n /** Раскладка: одна ось, колонка времени слева или чередование сторон. */\n layout?: GrTimelineLayout\n /** Направление оси. Горизонтальная лента — скроллер. */\n orientation?: GrTimelineOrientation\n /** Плотность вертикальных отступов пункта. */\n density?: GrTimelineDensity\n /** Идёт загрузка: вместо пунктов — заглушки, контейнер помечен `aria-busy`. */\n loading?: boolean\n /** Сколько строк-заглушек показать при `loading`. */\n loadingRows?: number\n /**\n * Лента пуста. По умолчанию определяется сама — по данным или по слоту; проп\n * нужен там, где потребитель знает лучше.\n */\n empty?: boolean\n /** Текст пустого состояния. Слот `#empty` сильнее. */\n emptyText?: string\n}\n\nconst props = withDefaults(defineProps<GrTimelineProps<T>>(), {\n items: undefined,\n itemKey: undefined,\n groupBy: undefined,\n // Настоящие дефолты живут в резолверах `useGrComponentProp`: объявленный здесь\n // дефолт неотличим от значения, переданного потребителем, и перебивал бы конфиг.\n groupHeadingLevel: undefined,\n layout: undefined,\n orientation: undefined,\n density: undefined,\n loading: false,\n loadingRows: 3,\n empty: undefined,\n emptyText: undefined,\n})\n\ndefineSlots<{\n /** Пункты ленты (`GrTimelineItem`), когда данные не переданы пропом. */\n default?: () => any\n /** Пункт в data-режиме. */\n item?: (props: { item: T, index: number }) => any\n /** Заголовок группы. По умолчанию — сам ключ. */\n group?: (props: { group: string, items: T[] }) => any\n /** Содержимое пустого состояния вместо текста по умолчанию. */\n empty?: () => any\n /** Заглушки загрузки вместо строк по умолчанию. */\n loading?: () => any\n}>()\n\nconst slots = useSlots()\nconst { t } = useGranularityTranslations()\n\nconst density = useGrComponentProp('GrTimeline', 'density', () => props.density, 'regular')\nconst orientation = useGrComponentProp('GrTimeline', 'orientation', () => props.orientation, 'vertical')\nconst headingLevel = useGrComponentProp('GrTimeline', 'groupHeadingLevel', () => props.groupHeadingLevel, 3)\nconst requestedLayout = useGrComponentProp('GrTimeline', 'layout', () => props.layout, 'stacked')\n\n/**\n * Горизонтальная лента знает одну раскладку: колонка времени и чередование\n * сторон — это вертикальные приёмы, у горизонтальной оси сторон нет.\n */\nconst layout = computed<GrTimelineLayout>(() => (\n orientation.value === 'horizontal' ? 'stacked' : requestedLayout.value\n))\n\nconst headingTag = computed(() => `h${headingLevel.value}`)\n\nconst dataMode = computed(() => props.items !== undefined)\n\nconst grouped = computed(() => dataMode.value && props.groupBy !== undefined)\n\nconst groups = computed(() => groupTimelineItems(props.items ?? [], props.groupBy))\n\n/** Единственная группа плоской ленты: тот же набор, что и в data-режиме без группировки. */\nconst entries = computed(() => groups.value[0]?.entries ?? [])\n\nconst resolvedEmptyText = computed(() => props.emptyText ?? t('gr.timeline.empty', 'Nothing here yet'))\n\nconst isEmpty = computed(() => {\n if (props.empty !== undefined) return props.empty\n\n if (dataMode.value) return props.items!.length === 0\n\n // `v-for` по пустому массиву оставляет фрагмент без узлов, `v-if` — комментарий,\n // и ни то ни другое пунктом ленты не является.\n return !hasMeaningfulSlotContent(slots.default?.() ?? [])\n})\n\nconst loadingRowCount = computed(() => Math.max(1, Math.trunc(props.loadingRows)))\n\nfunction keyOf(item: T, index: number): string | number {\n const key = props.itemKey\n if (typeof key === 'function') return key(item, index)\n if (typeof key === 'string') return (item as Record<string, unknown>)[key] as string | number\n\n return index\n}\n\nfunction itemsOf(group: { entries: { item: T }[] }): T[] {\n return group.entries.map(entry => entry.item)\n}\n\nprovide(GR_TIMELINE_CONTEXT, { layout, orientation, density })\n\nif (import.meta.env?.DEV) {\n watch(\n () => [dataMode.value, Boolean(slots.item)] as const,\n ([hasItems, hasSlot]) => {\n if (hasItems && !hasSlot) {\n console.warn(\n '[granularity] GrTimeline: с `items` пункты рисует слот `#item` — без него лента пуста.',\n )\n }\n },\n { immediate: true },\n )\n\n watch(\n () => [dataMode.value, props.groupBy !== undefined] as const,\n ([hasItems, hasGroupBy]) => {\n if (hasGroupBy && !hasItems) {\n console.warn(\n '[granularity] GrTimeline: `groupBy` работает только с `items` — пункты из слота по '\n + 'умолчанию лента не разбирает и сгруппировать их ей не из чего.',\n )\n }\n },\n { immediate: true },\n )\n\n watch(\n () => [orientation.value, requestedLayout.value] as const,\n ([axis, requested]) => {\n if (axis === 'horizontal' && requested !== 'stacked') {\n console.warn(\n `[granularity] GrTimeline: layout=\"${requested}\" — вертикальная раскладка, `\n + 'в горизонтальной ленте она игнорируется.',\n )\n }\n },\n { immediate: true },\n )\n}\n</script>\n\n<template>\n <div\n data-gr-timeline\n :data-layout=\"layout\"\n :data-orientation=\"orientation\"\n :aria-busy=\"loading ? 'true' : undefined\"\n >\n <template v-if=\"loading\">\n <slot name=\"loading\">\n <div\n v-for=\"row in loadingRowCount\"\n :key=\"row\"\n data-gr-timeline-loading-row\n :class=\"loadingRowClass\"\n >\n <GrSkeleton variant=\"circle\" width=\"var(--gr-timeline-marker-size, 10px)\" />\n <GrSkeleton />\n </div>\n </slot>\n </template>\n\n <div\n v-else-if=\"isEmpty\"\n data-gr-timeline-empty\n :class=\"emptyClass\"\n >\n <slot name=\"empty\">\n {{ resolvedEmptyText }}\n </slot>\n </div>\n\n <template v-else-if=\"grouped\">\n <!--\n Группа — секция с заголовком, а не пункт-заголовок внутри списка:\n `<li>` с датой попал бы в набор событий и был бы объявлен как одно из них.\n -->\n <section\n v-for=\"group in groups\"\n :key=\"group.key\"\n data-gr-timeline-group\n >\n <div\n v-if=\"group.key\"\n data-gr-timeline-group-header\n >\n <div data-gr-timeline-rail aria-hidden=\"true\">\n <span data-gr-timeline-line />\n </div>\n\n <component\n :is=\"headingTag\"\n data-gr-timeline-group-title\n :class=\"groupTitleClass\"\n >\n <slot name=\"group\" :group=\"group.key\" :items=\"itemsOf(group)\">\n {{ group.key }}\n </slot>\n </component>\n </div>\n\n <ol\n data-gr-timeline-list\n :tabindex=\"orientation === 'horizontal' ? 0 : undefined\"\n >\n <template\n v-for=\"entry in group.entries\"\n :key=\"keyOf(entry.item, entry.index)\"\n >\n <slot name=\"item\" :item=\"entry.item\" :index=\"entry.index\" />\n </template>\n </ol>\n </section>\n </template>\n\n <ol\n v-else\n data-gr-timeline-list\n :tabindex=\"orientation === 'horizontal' ? 0 : undefined\"\n >\n <template v-if=\"dataMode\">\n <template\n v-for=\"entry in entries\"\n :key=\"keyOf(entry.item, entry.index)\"\n >\n <slot name=\"item\" :item=\"entry.item\" :index=\"entry.index\" />\n </template>\n </template>\n\n <slot v-else />\n </ol>\n </div>\n</template>\n\n<style>\n/*\n * Геометрия ленты живёт здесь, а не в утилитах, по одной причине: сторона\n * пункта в `alternate` — это `nth-child`, а обрыв оси — `last-child`, и оба\n * вопроса утилитами не выражаются. Альтернатива — регистрация пунктов в\n * контексте на `onMounted` — дала бы порядок, зависящий от порядка монтирования,\n * и разъезжалась бы на `v-if` посреди ленты.\n */\n\n[data-gr-timeline] {\n --gr-timeline-columns: auto 1fr;\n}\n\n[data-gr-timeline][data-layout='time'] {\n --gr-timeline-columns: var(--gr-timeline-time-width, 5.5rem) auto 1fr;\n}\n\n[data-gr-timeline][data-layout='alternate'] {\n --gr-timeline-columns: 1fr auto 1fr;\n}\n\n[data-gr-timeline-list] {\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n[data-gr-timeline-item],\n[data-gr-timeline-group-header] {\n display: grid;\n grid-template-columns: var(--gr-timeline-columns);\n column-gap: var(--gr-timeline-gap, 0.75rem);\n align-items: start;\n}\n\n[data-gr-timeline-aside] {\n text-align: end;\n}\n\n/*\n * Ось — отрезок у каждого пункта, а не одна длинная линия: соседние отрезки\n * смыкаются сами, и мерить высоту списка не приходится.\n */\n[data-gr-timeline-rail] {\n display: flex;\n flex-direction: column;\n align-items: center;\n align-self: stretch;\n padding-top: var(--gr-timeline-marker-top, 0.35rem);\n}\n\n[data-gr-timeline-rail] > :first-child {\n flex: none;\n}\n\n[data-gr-timeline-marker] {\n box-sizing: border-box;\n width: var(--gr-timeline-marker-size, 10px);\n height: var(--gr-timeline-marker-size, 10px);\n border-width: var(--gr-timeline-marker-ring, 2px);\n border-style: solid;\n border-color: transparent;\n}\n\n[data-gr-timeline-line] {\n flex: 1 1 auto;\n width: var(--gr-timeline-axis-width, 2px);\n min-height: var(--gr-timeline-line-min, 0.75rem);\n margin-top: 0.25rem;\n /* Ровно на столько следующая точка отступает от верха своей строки: без этого\n отрезок не доходит до неё, и ось выглядит пунктиром из длинных штрихов. */\n margin-bottom: calc(-1 * var(--gr-timeline-marker-top, 0.35rem));\n background-color: var(--gr-timeline-axis-color, var(--gr-brd));\n}\n\n/*\n * Пунктиром идёт участок, ведущий **к** незавершённому событию, и всё, что за\n * ним: непройденный путь. Отрезок после точки для этого не годится — обычно\n * незавершённое событие последнее, и его отрезка не видно вовсе.\n */\n[data-gr-timeline-item]:has(+ [data-gr-timeline-item][data-pending]) [data-gr-timeline-line],\n[data-gr-timeline-item][data-pending] [data-gr-timeline-line] {\n background-color: transparent;\n background-image: repeating-linear-gradient(\n to bottom,\n var(--gr-timeline-axis-color, var(--gr-brd)) 0 var(--gr-timeline-dash, 4px),\n transparent var(--gr-timeline-dash, 4px) calc(var(--gr-timeline-dash, 4px) * 2)\n );\n}\n\n/* Хвост после последнего события — линия в никуда. */\n[data-gr-timeline-item]:last-child [data-gr-timeline-line] {\n visibility: hidden;\n}\n\n/* Между группами ось не рвётся: разрыв читался бы как две разные ленты. */\n[data-gr-timeline-group]:not(:last-of-type) [data-gr-timeline-item]:last-child [data-gr-timeline-line] {\n visibility: visible;\n}\n\n[data-gr-timeline-group-header] [data-gr-timeline-rail] {\n padding-top: 0;\n}\n\n/* Воздух перед заголовком — отступом строки, а не полем: поле разорвало бы ось. */\n[data-gr-timeline-group] + [data-gr-timeline-group] [data-gr-timeline-group-header] {\n padding-top: 0.5rem;\n}\n\n[data-gr-timeline-group-header] [data-gr-timeline-line] {\n margin-top: 0;\n}\n\n/* Над первым заголовком оси ещё нет — линия висела бы в воздухе. */\n[data-gr-timeline-group]:first-of-type [data-gr-timeline-group-header] [data-gr-timeline-line] {\n visibility: hidden;\n}\n\n[data-gr-timeline][data-layout='time'] [data-gr-timeline-group-header] [data-gr-timeline-rail],\n[data-gr-timeline][data-layout='alternate'] [data-gr-timeline-group-header] [data-gr-timeline-rail] {\n grid-column: 2;\n}\n\n[data-gr-timeline][data-layout='time'] [data-gr-timeline-group-title],\n[data-gr-timeline][data-layout='alternate'] [data-gr-timeline-group-title] {\n grid-column: 3;\n}\n\n[data-gr-timeline][data-layout='alternate'] [data-gr-timeline-rail] {\n grid-column: 2;\n}\n\n[data-gr-timeline][data-layout='alternate'] [data-gr-timeline-content] {\n grid-column: 3;\n}\n\n[data-gr-timeline][data-layout='alternate'] [data-gr-timeline-item]:nth-child(even) [data-gr-timeline-content] {\n grid-column: 1;\n grid-row: 1;\n text-align: end;\n}\n\n/* Двухсторонняя лента в узкой колонке нечитаема — там она односторонняя. */\n@media (max-width: 640px) {\n [data-gr-timeline][data-layout='alternate'] {\n --gr-timeline-columns: auto 1fr;\n }\n\n [data-gr-timeline][data-layout='alternate'] [data-gr-timeline-rail],\n [data-gr-timeline][data-layout='alternate'] [data-gr-timeline-group-header] [data-gr-timeline-rail] {\n grid-column: 1;\n }\n\n [data-gr-timeline][data-layout='alternate'] [data-gr-timeline-content],\n [data-gr-timeline][data-layout='alternate'] [data-gr-timeline-item]:nth-child(even) [data-gr-timeline-content],\n [data-gr-timeline][data-layout='alternate'] [data-gr-timeline-group-title] {\n grid-column: 2;\n text-align: start;\n }\n}\n\n[data-gr-timeline][data-orientation='horizontal'] [data-gr-timeline-list] {\n display: flex;\n flex-direction: row;\n overflow-x: auto;\n}\n\n[data-gr-timeline][data-orientation='horizontal'] [data-gr-timeline-item] {\n grid-template-columns: 1fr;\n min-width: var(--gr-timeline-item-min-width, 12rem);\n padding-inline-end: var(--gr-timeline-gap, 0.75rem);\n}\n\n[data-gr-timeline][data-orientation='horizontal'] [data-gr-timeline-content] {\n padding-bottom: 0;\n}\n\n[data-gr-timeline][data-orientation='horizontal'] [data-gr-timeline-rail] {\n flex-direction: row;\n align-items: center;\n align-self: auto;\n padding-top: 0;\n padding-bottom: 0.25rem;\n}\n\n[data-gr-timeline][data-orientation='horizontal'] [data-gr-timeline-line] {\n width: auto;\n height: var(--gr-timeline-axis-width, 2px);\n min-width: var(--gr-timeline-line-min, 0.75rem);\n min-height: 0;\n margin-top: 0;\n margin-inline-start: 0.25rem;\n}\n\n[data-gr-timeline][data-orientation='horizontal'] [data-gr-timeline-item]:has(+ [data-gr-timeline-item][data-pending]) [data-gr-timeline-line],\n[data-gr-timeline][data-orientation='horizontal'] [data-gr-timeline-item][data-pending] [data-gr-timeline-line] {\n background-image: repeating-linear-gradient(\n to right,\n var(--gr-timeline-axis-color, var(--gr-brd)) 0 var(--gr-timeline-dash, 4px),\n transparent var(--gr-timeline-dash, 4px) calc(var(--gr-timeline-dash, 4px) * 2)\n );\n}\n</style>\n","<script setup lang=\"ts\" generic=\"T = unknown\">\nimport { computed, provide, useSlots, watch } from 'vue'\n\nimport GrSkeleton from '../GrSkeleton/GrSkeleton.vue'\nimport { useGrComponentProp } from '../GrConfigProvider/context'\nimport { hasMeaningfulSlotContent } from '../shared/slotNodes'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n\nimport { GR_TIMELINE_CONTEXT, type GrTimelineHeadingLevel } from './grTimelineContext'\nimport {\n emptyClass,\n groupTitleClass,\n loadingRowClass,\n type GrTimelineDensity,\n type GrTimelineLayout,\n type GrTimelineOrientation,\n} from './grTimelineStyles'\nimport { groupTimelineItems, type GrTimelineGroupBy } from './timelineGroups'\n\nexport type {\n GrTimelineDensity,\n GrTimelineLayout,\n GrTimelineOrientation,\n} from './grTimelineStyles'\nexport type { GrTimelineGroupBy } from './timelineGroups'\n\nexport interface GrTimelineProps<TItem = unknown> {\n /**\n * Данные ленты. С ними пункты рисует слот `#item` — и только так лента знает\n * состав набора, то есть может его группировать.\n */\n items?: TItem[]\n /** Ключ пункта для `v-for`: имя поля или функция. Без него — индекс. */\n itemKey?: string | ((item: TItem, index: number) => string | number)\n /** Группировка: имя поля или функция от пункта. Заголовок группы — слот `#group`. */\n groupBy?: GrTimelineGroupBy<TItem>\n /** Уровень заголовка группы: лента обязана вписаться в структуру заголовков страницы. */\n groupHeadingLevel?: GrTimelineHeadingLevel\n /** Раскладка: одна ось, колонка времени слева или чередование сторон. */\n layout?: GrTimelineLayout\n /** Направление оси. Горизонтальная лента — скроллер. */\n orientation?: GrTimelineOrientation\n /** Плотность вертикальных отступов пункта. */\n density?: GrTimelineDensity\n /** Идёт загрузка: вместо пунктов — заглушки, контейнер помечен `aria-busy`. */\n loading?: boolean\n /** Сколько строк-заглушек показать при `loading`. */\n loadingRows?: number\n /**\n * Лента пуста. По умолчанию определяется сама — по данным или по слоту; проп\n * нужен там, где потребитель знает лучше.\n */\n empty?: boolean\n /** Текст пустого состояния. Слот `#empty` сильнее. */\n emptyText?: string\n}\n\nconst props = withDefaults(defineProps<GrTimelineProps<T>>(), {\n items: undefined,\n itemKey: undefined,\n groupBy: undefined,\n // Настоящие дефолты живут в резолверах `useGrComponentProp`: объявленный здесь\n // дефолт неотличим от значения, переданного потребителем, и перебивал бы конфиг.\n groupHeadingLevel: undefined,\n layout: undefined,\n orientation: undefined,\n density: undefined,\n loading: false,\n loadingRows: 3,\n empty: undefined,\n emptyText: undefined,\n})\n\ndefineSlots<{\n /** Пункты ленты (`GrTimelineItem`), когда данные не переданы пропом. */\n default?: () => any\n /** Пункт в data-режиме. */\n item?: (props: { item: T, index: number }) => any\n /** Заголовок группы. По умолчанию — сам ключ. */\n group?: (props: { group: string, items: T[] }) => any\n /** Содержимое пустого состояния вместо текста по умолчанию. */\n empty?: () => any\n /** Заглушки загрузки вместо строк по умолчанию. */\n loading?: () => any\n}>()\n\nconst slots = useSlots()\nconst { t } = useGranularityTranslations()\n\nconst density = useGrComponentProp('GrTimeline', 'density', () => props.density, 'regular')\nconst orientation = useGrComponentProp('GrTimeline', 'orientation', () => props.orientation, 'vertical')\nconst headingLevel = useGrComponentProp('GrTimeline', 'groupHeadingLevel', () => props.groupHeadingLevel, 3)\nconst requestedLayout = useGrComponentProp('GrTimeline', 'layout', () => props.layout, 'stacked')\n\n/**\n * Горизонтальная лента знает одну раскладку: колонка времени и чередование\n * сторон — это вертикальные приёмы, у горизонтальной оси сторон нет.\n */\nconst layout = computed<GrTimelineLayout>(() => (\n orientation.value === 'horizontal' ? 'stacked' : requestedLayout.value\n))\n\nconst headingTag = computed(() => `h${headingLevel.value}`)\n\nconst dataMode = computed(() => props.items !== undefined)\n\nconst grouped = computed(() => dataMode.value && props.groupBy !== undefined)\n\nconst groups = computed(() => groupTimelineItems(props.items ?? [], props.groupBy))\n\n/** Единственная группа плоской ленты: тот же набор, что и в data-режиме без группировки. */\nconst entries = computed(() => groups.value[0]?.entries ?? [])\n\nconst resolvedEmptyText = computed(() => props.emptyText ?? t('gr.timeline.empty', 'Nothing here yet'))\n\nconst isEmpty = computed(() => {\n if (props.empty !== undefined) return props.empty\n\n if (dataMode.value) return props.items!.length === 0\n\n // `v-for` по пустому массиву оставляет фрагмент без узлов, `v-if` — комментарий,\n // и ни то ни другое пунктом ленты не является.\n return !hasMeaningfulSlotContent(slots.default?.() ?? [])\n})\n\nconst loadingRowCount = computed(() => Math.max(1, Math.trunc(props.loadingRows)))\n\nfunction keyOf(item: T, index: number): string | number {\n const key = props.itemKey\n if (typeof key === 'function') return key(item, index)\n if (typeof key === 'string') return (item as Record<string, unknown>)[key] as string | number\n\n return index\n}\n\nfunction itemsOf(group: { entries: { item: T }[] }): T[] {\n return group.entries.map(entry => entry.item)\n}\n\nprovide(GR_TIMELINE_CONTEXT, { layout, orientation, density })\n\nif (import.meta.env?.DEV) {\n watch(\n () => [dataMode.value, Boolean(slots.item)] as const,\n ([hasItems, hasSlot]) => {\n if (hasItems && !hasSlot) {\n console.warn(\n '[granularity] GrTimeline: с `items` пункты рисует слот `#item` — без него лента пуста.',\n )\n }\n },\n { immediate: true },\n )\n\n watch(\n () => [dataMode.value, props.groupBy !== undefined] as const,\n ([hasItems, hasGroupBy]) => {\n if (hasGroupBy && !hasItems) {\n console.warn(\n '[granularity] GrTimeline: `groupBy` работает только с `items` — пункты из слота по '\n + 'умолчанию лента не разбирает и сгруппировать их ей не из чего.',\n )\n }\n },\n { immediate: true },\n )\n\n watch(\n () => [orientation.value, requestedLayout.value] as const,\n ([axis, requested]) => {\n if (axis === 'horizontal' && requested !== 'stacked') {\n console.warn(\n `[granularity] GrTimeline: layout=\"${requested}\" — вертикальная раскладка, `\n + 'в горизонтальной ленте она игнорируется.',\n )\n }\n },\n { immediate: true },\n )\n}\n</script>\n\n<template>\n <div\n data-gr-timeline\n :data-layout=\"layout\"\n :data-orientation=\"orientation\"\n :aria-busy=\"loading ? 'true' : undefined\"\n >\n <template v-if=\"loading\">\n <slot name=\"loading\">\n <div\n v-for=\"row in loadingRowCount\"\n :key=\"row\"\n data-gr-timeline-loading-row\n :class=\"loadingRowClass\"\n >\n <GrSkeleton variant=\"circle\" width=\"var(--gr-timeline-marker-size, 10px)\" />\n <GrSkeleton />\n </div>\n </slot>\n </template>\n\n <div\n v-else-if=\"isEmpty\"\n data-gr-timeline-empty\n :class=\"emptyClass\"\n >\n <slot name=\"empty\">\n {{ resolvedEmptyText }}\n </slot>\n </div>\n\n <template v-else-if=\"grouped\">\n <!--\n Группа — секция с заголовком, а не пункт-заголовок внутри списка:\n `<li>` с датой попал бы в набор событий и был бы объявлен как одно из них.\n -->\n <section\n v-for=\"group in groups\"\n :key=\"group.key\"\n data-gr-timeline-group\n >\n <div\n v-if=\"group.key\"\n data-gr-timeline-group-header\n >\n <div data-gr-timeline-rail aria-hidden=\"true\">\n <span data-gr-timeline-line />\n </div>\n\n <component\n :is=\"headingTag\"\n data-gr-timeline-group-title\n :class=\"groupTitleClass\"\n >\n <slot name=\"group\" :group=\"group.key\" :items=\"itemsOf(group)\">\n {{ group.key }}\n </slot>\n </component>\n </div>\n\n <ol\n data-gr-timeline-list\n :tabindex=\"orientation === 'horizontal' ? 0 : undefined\"\n >\n <template\n v-for=\"entry in group.entries\"\n :key=\"keyOf(entry.item, entry.index)\"\n >\n <slot name=\"item\" :item=\"entry.item\" :index=\"entry.index\" />\n </template>\n </ol>\n </section>\n </template>\n\n <ol\n v-else\n data-gr-timeline-list\n :tabindex=\"orientation === 'horizontal' ? 0 : undefined\"\n >\n <template v-if=\"dataMode\">\n <template\n v-for=\"entry in entries\"\n :key=\"keyOf(entry.item, entry.index)\"\n >\n <slot name=\"item\" :item=\"entry.item\" :index=\"entry.index\" />\n </template>\n </template>\n\n <slot v-else />\n </ol>\n </div>\n</template>\n\n<style>\n/*\n * Геометрия ленты живёт здесь, а не в утилитах, по одной причине: сторона\n * пункта в `alternate` — это `nth-child`, а обрыв оси — `last-child`, и оба\n * вопроса утилитами не выражаются. Альтернатива — регистрация пунктов в\n * контексте на `onMounted` — дала бы порядок, зависящий от порядка монтирования,\n * и разъезжалась бы на `v-if` посреди ленты.\n */\n\n[data-gr-timeline] {\n --gr-timeline-columns: auto 1fr;\n}\n\n[data-gr-timeline][data-layout='time'] {\n --gr-timeline-columns: var(--gr-timeline-time-width, 5.5rem) auto 1fr;\n}\n\n[data-gr-timeline][data-layout='alternate'] {\n --gr-timeline-columns: 1fr auto 1fr;\n}\n\n[data-gr-timeline-list] {\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n[data-gr-timeline-item],\n[data-gr-timeline-group-header] {\n display: grid;\n grid-template-columns: var(--gr-timeline-columns);\n column-gap: var(--gr-timeline-gap, 0.75rem);\n align-items: start;\n}\n\n[data-gr-timeline-aside] {\n text-align: end;\n}\n\n/*\n * Ось — отрезок у каждого пункта, а не одна длинная линия: соседние отрезки\n * смыкаются сами, и мерить высоту списка не приходится.\n */\n[data-gr-timeline-rail] {\n display: flex;\n flex-direction: column;\n align-items: center;\n align-self: stretch;\n padding-top: var(--gr-timeline-marker-top, 0.35rem);\n}\n\n[data-gr-timeline-rail] > :first-child {\n flex: none;\n}\n\n[data-gr-timeline-marker] {\n box-sizing: border-box;\n width: var(--gr-timeline-marker-size, 10px);\n height: var(--gr-timeline-marker-size, 10px);\n border-width: var(--gr-timeline-marker-ring, 2px);\n border-style: solid;\n border-color: transparent;\n}\n\n[data-gr-timeline-line] {\n flex: 1 1 auto;\n width: var(--gr-timeline-axis-width, 2px);\n min-height: var(--gr-timeline-line-min, 0.75rem);\n margin-top: 0.25rem;\n /* Ровно на столько следующая точка отступает от верха своей строки: без этого\n отрезок не доходит до неё, и ось выглядит пунктиром из длинных штрихов. */\n margin-bottom: calc(-1 * var(--gr-timeline-marker-top, 0.35rem));\n background-color: var(--gr-timeline-axis-color, var(--gr-brd));\n}\n\n/*\n * Пунктиром идёт участок, ведущий **к** незавершённому событию, и всё, что за\n * ним: непройденный путь. Отрезок после точки для этого не годится — обычно\n * незавершённое событие последнее, и его отрезка не видно вовсе.\n */\n[data-gr-timeline-item]:has(+ [data-gr-timeline-item][data-pending]) [data-gr-timeline-line],\n[data-gr-timeline-item][data-pending] [data-gr-timeline-line] {\n background-color: transparent;\n background-image: repeating-linear-gradient(\n to bottom,\n var(--gr-timeline-axis-color, var(--gr-brd)) 0 var(--gr-timeline-dash, 4px),\n transparent var(--gr-timeline-dash, 4px) calc(var(--gr-timeline-dash, 4px) * 2)\n );\n}\n\n/* Хвост после последнего события — линия в никуда. */\n[data-gr-timeline-item]:last-child [data-gr-timeline-line] {\n visibility: hidden;\n}\n\n/* Между группами ось не рвётся: разрыв читался бы как две разные ленты. */\n[data-gr-timeline-group]:not(:last-of-type) [data-gr-timeline-item]:last-child [data-gr-timeline-line] {\n visibility: visible;\n}\n\n[data-gr-timeline-group-header] [data-gr-timeline-rail] {\n padding-top: 0;\n}\n\n/* Воздух перед заголовком — отступом строки, а не полем: поле разорвало бы ось. */\n[data-gr-timeline-group] + [data-gr-timeline-group] [data-gr-timeline-group-header] {\n padding-top: 0.5rem;\n}\n\n[data-gr-timeline-group-header] [data-gr-timeline-line] {\n margin-top: 0;\n}\n\n/* Над первым заголовком оси ещё нет — линия висела бы в воздухе. */\n[data-gr-timeline-group]:first-of-type [data-gr-timeline-group-header] [data-gr-timeline-line] {\n visibility: hidden;\n}\n\n[data-gr-timeline][data-layout='time'] [data-gr-timeline-group-header] [data-gr-timeline-rail],\n[data-gr-timeline][data-layout='alternate'] [data-gr-timeline-group-header] [data-gr-timeline-rail] {\n grid-column: 2;\n}\n\n[data-gr-timeline][data-layout='time'] [data-gr-timeline-group-title],\n[data-gr-timeline][data-layout='alternate'] [data-gr-timeline-group-title] {\n grid-column: 3;\n}\n\n[data-gr-timeline][data-layout='alternate'] [data-gr-timeline-rail] {\n grid-column: 2;\n}\n\n[data-gr-timeline][data-layout='alternate'] [data-gr-timeline-content] {\n grid-column: 3;\n}\n\n[data-gr-timeline][data-layout='alternate'] [data-gr-timeline-item]:nth-child(even) [data-gr-timeline-content] {\n grid-column: 1;\n grid-row: 1;\n text-align: end;\n}\n\n/* Двухсторонняя лента в узкой колонке нечитаема — там она односторонняя. */\n@media (max-width: 640px) {\n [data-gr-timeline][data-layout='alternate'] {\n --gr-timeline-columns: auto 1fr;\n }\n\n [data-gr-timeline][data-layout='alternate'] [data-gr-timeline-rail],\n [data-gr-timeline][data-layout='alternate'] [data-gr-timeline-group-header] [data-gr-timeline-rail] {\n grid-column: 1;\n }\n\n [data-gr-timeline][data-layout='alternate'] [data-gr-timeline-content],\n [data-gr-timeline][data-layout='alternate'] [data-gr-timeline-item]:nth-child(even) [data-gr-timeline-content],\n [data-gr-timeline][data-layout='alternate'] [data-gr-timeline-group-title] {\n grid-column: 2;\n text-align: start;\n }\n}\n\n[data-gr-timeline][data-orientation='horizontal'] [data-gr-timeline-list] {\n display: flex;\n flex-direction: row;\n overflow-x: auto;\n}\n\n[data-gr-timeline][data-orientation='horizontal'] [data-gr-timeline-item] {\n grid-template-columns: 1fr;\n min-width: var(--gr-timeline-item-min-width, 12rem);\n padding-inline-end: var(--gr-timeline-gap, 0.75rem);\n}\n\n[data-gr-timeline][data-orientation='horizontal'] [data-gr-timeline-content] {\n padding-bottom: 0;\n}\n\n[data-gr-timeline][data-orientation='horizontal'] [data-gr-timeline-rail] {\n flex-direction: row;\n align-items: center;\n align-self: auto;\n padding-top: 0;\n padding-bottom: 0.25rem;\n}\n\n[data-gr-timeline][data-orientation='horizontal'] [data-gr-timeline-line] {\n width: auto;\n height: var(--gr-timeline-axis-width, 2px);\n min-width: var(--gr-timeline-line-min, 0.75rem);\n min-height: 0;\n margin-top: 0;\n margin-inline-start: 0.25rem;\n}\n\n[data-gr-timeline][data-orientation='horizontal'] [data-gr-timeline-item]:has(+ [data-gr-timeline-item][data-pending]) [data-gr-timeline-line],\n[data-gr-timeline][data-orientation='horizontal'] [data-gr-timeline-item][data-pending] [data-gr-timeline-line] {\n background-image: repeating-linear-gradient(\n to right,\n var(--gr-timeline-axis-color, var(--gr-brd)) 0 var(--gr-timeline-dash, 4px),\n transparent var(--gr-timeline-dash, 4px) calc(var(--gr-timeline-dash, 4px) * 2)\n );\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, inject, useSlots } from 'vue'\n\nimport { GR_TIMELINE_CONTEXT } from './grTimelineContext'\nimport {\n descriptionClass,\n grTimelineDensityClass,\n grTimelineMarkerClass,\n timeClass,\n titleClass,\n type GrTimelineMarkerVariant,\n type GrTimelineTone,\n} from './grTimelineStyles'\n\nexport type { GrTimelineMarkerVariant, GrTimelineTone } from './grTimelineStyles'\n\nexport interface GrTimelineItemProps {\n /** Метка времени. Слот `#time` сильнее. */\n time?: string\n /** Машиночитаемое значение метки: попадает в `datetime` тега `<time>`. */\n datetime?: string\n /** Заголовок события. Слот `#title` сильнее. */\n title?: string\n /** Пояснение под заголовком. Слот `#description` сильнее. */\n description?: string\n /** Цвет маркера из палитры пакета. */\n tone?: GrTimelineTone\n /** Заполненная точка — событие случилось, полая — запланировано. */\n variant?: GrTimelineMarkerVariant\n /**\n * Событие ещё не произошло: точка полая, а отрезок оси **после** неё —\n * пунктирный. Перебивает `variant`.\n */\n pending?: boolean\n}\n\nconst props = withDefaults(defineProps<GrTimelineItemProps>(), {\n time: undefined,\n datetime: undefined,\n title: undefined,\n description: undefined,\n tone: 'neutral',\n variant: 'filled',\n pending: false,\n})\n\ndefineSlots<{\n /** Содержимое события под заголовком и описанием. */\n default?: () => any\n /** Маркер вместо точки: иконка статуса, аватар автора, номер шага. */\n marker?: () => any\n /** Метка времени. */\n time?: () => any\n /** Заголовок события. */\n title?: () => any\n /** Пояснение под заголовком. */\n description?: () => any\n}>()\n\nconst slots = useSlots()\n\n/**\n * Раскладку пункт читает из контейнера. Вне `GrTimeline` — дефолты: пункт\n * остаётся рабочей строкой, а не падает.\n */\nconst timeline = inject(GR_TIMELINE_CONTEXT, null)\n\nconst layout = computed(() => timeline?.layout.value ?? 'stacked')\nconst density = computed(() => timeline?.density.value ?? 'regular')\n\nconst markerClass = computed(() => grTimelineMarkerClass(\n props.tone,\n props.pending ? 'outlined' : props.variant,\n))\n\nconst hasTime = computed(() => !!slots.time || !!props.time)\nconst hasTitle = computed(() => !!slots.title || !!props.title)\nconst hasDescription = computed(() => !!slots.description || !!props.description)\n\n/** В `layout=\"time\"` метка уезжает в свою колонку, во всех остальных — в содержимое. */\nconst timeInAside = computed(() => layout.value === 'time')\n</script>\n\n<template>\n <li\n data-gr-timeline-item\n :data-tone=\"tone\"\n :data-pending=\"pending ? '' : undefined\"\n >\n <div\n v-if=\"timeInAside\"\n data-gr-timeline-aside\n >\n <time\n v-if=\"hasTime\"\n :datetime=\"datetime\"\n :class=\"timeClass\"\n >\n <slot name=\"time\">{{ time }}</slot>\n </time>\n </div>\n\n <!-- Ось и точка декоративны: смысл события несёт текст рядом. -->\n <div\n data-gr-timeline-rail\n aria-hidden=\"true\"\n >\n <slot name=\"marker\">\n <span data-gr-timeline-marker :class=\"markerClass\" />\n </slot>\n <span data-gr-timeline-line />\n </div>\n\n <!--\n Вертикальный отступ живёт на содержимом, а не на строке: ось растянута по\n строке грид-ячейкой, и её поля в высоту ячейки не входят — линия обрывалась\n бы, не доходя до следующей точки.\n -->\n <div data-gr-timeline-content :class=\"grTimelineDensityClass(density)\">\n <time\n v-if=\"hasTime && !timeInAside\"\n data-gr-timeline-time\n :datetime=\"datetime\"\n :class=\"timeClass\"\n >\n <slot name=\"time\">{{ time }}</slot>\n </time>\n\n <div v-if=\"hasTitle\" data-gr-timeline-title :class=\"titleClass\">\n <slot name=\"title\">\n{{ title }}\n</slot>\n </div>\n\n <div v-if=\"hasDescription\" :class=\"descriptionClass\">\n <slot name=\"description\">\n{{ description }}\n</slot>\n </div>\n\n <div v-if=\"$slots.default\" data-gr-timeline-body>\n <slot />\n </div>\n </div>\n </li>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, inject, useSlots } from 'vue'\n\nimport { GR_TIMELINE_CONTEXT } from './grTimelineContext'\nimport {\n descriptionClass,\n grTimelineDensityClass,\n grTimelineMarkerClass,\n timeClass,\n titleClass,\n type GrTimelineMarkerVariant,\n type GrTimelineTone,\n} from './grTimelineStyles'\n\nexport type { GrTimelineMarkerVariant, GrTimelineTone } from './grTimelineStyles'\n\nexport interface GrTimelineItemProps {\n /** Метка времени. Слот `#time` сильнее. */\n time?: string\n /** Машиночитаемое значение метки: попадает в `datetime` тега `<time>`. */\n datetime?: string\n /** Заголовок события. Слот `#title` сильнее. */\n title?: string\n /** Пояснение под заголовком. Слот `#description` сильнее. */\n description?: string\n /** Цвет маркера из палитры пакета. */\n tone?: GrTimelineTone\n /** Заполненная точка — событие случилось, полая — запланировано. */\n variant?: GrTimelineMarkerVariant\n /**\n * Событие ещё не произошло: точка полая, а отрезок оси **после** неё —\n * пунктирный. Перебивает `variant`.\n */\n pending?: boolean\n}\n\nconst props = withDefaults(defineProps<GrTimelineItemProps>(), {\n time: undefined,\n datetime: undefined,\n title: undefined,\n description: undefined,\n tone: 'neutral',\n variant: 'filled',\n pending: false,\n})\n\ndefineSlots<{\n /** Содержимое события под заголовком и описанием. */\n default?: () => any\n /** Маркер вместо точки: иконка статуса, аватар автора, номер шага. */\n marker?: () => any\n /** Метка времени. */\n time?: () => any\n /** Заголовок события. */\n title?: () => any\n /** Пояснение под заголовком. */\n description?: () => any\n}>()\n\nconst slots = useSlots()\n\n/**\n * Раскладку пункт читает из контейнера. Вне `GrTimeline` — дефолты: пункт\n * остаётся рабочей строкой, а не падает.\n */\nconst timeline = inject(GR_TIMELINE_CONTEXT, null)\n\nconst layout = computed(() => timeline?.layout.value ?? 'stacked')\nconst density = computed(() => timeline?.density.value ?? 'regular')\n\nconst markerClass = computed(() => grTimelineMarkerClass(\n props.tone,\n props.pending ? 'outlined' : props.variant,\n))\n\nconst hasTime = computed(() => !!slots.time || !!props.time)\nconst hasTitle = computed(() => !!slots.title || !!props.title)\nconst hasDescription = computed(() => !!slots.description || !!props.description)\n\n/** В `layout=\"time\"` метка уезжает в свою колонку, во всех остальных — в содержимое. */\nconst timeInAside = computed(() => layout.value === 'time')\n</script>\n\n<template>\n <li\n data-gr-timeline-item\n :data-tone=\"tone\"\n :data-pending=\"pending ? '' : undefined\"\n >\n <div\n v-if=\"timeInAside\"\n data-gr-timeline-aside\n >\n <time\n v-if=\"hasTime\"\n :datetime=\"datetime\"\n :class=\"timeClass\"\n >\n <slot name=\"time\">{{ time }}</slot>\n </time>\n </div>\n\n <!-- Ось и точка декоративны: смысл события несёт текст рядом. -->\n <div\n data-gr-timeline-rail\n aria-hidden=\"true\"\n >\n <slot name=\"marker\">\n <span data-gr-timeline-marker :class=\"markerClass\" />\n </slot>\n <span data-gr-timeline-line />\n </div>\n\n <!--\n Вертикальный отступ живёт на содержимом, а не на строке: ось растянута по\n строке грид-ячейкой, и её поля в высоту ячейки не входят — линия обрывалась\n бы, не доходя до следующей точки.\n -->\n <div data-gr-timeline-content :class=\"grTimelineDensityClass(density)\">\n <time\n v-if=\"hasTime && !timeInAside\"\n data-gr-timeline-time\n :datetime=\"datetime\"\n :class=\"timeClass\"\n >\n <slot name=\"time\">{{ time }}</slot>\n </time>\n\n <div v-if=\"hasTitle\" data-gr-timeline-title :class=\"titleClass\">\n <slot name=\"title\">\n{{ title }}\n</slot>\n </div>\n\n <div v-if=\"hasDescription\" :class=\"descriptionClass\">\n <slot name=\"description\">\n{{ description }}\n</slot>\n </div>\n\n <div v-if=\"$slots.default\" data-gr-timeline-body>\n <slot />\n </div>\n </div>\n </li>\n</template>\n"],"mappings":"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"}
@@ -2,9 +2,9 @@ import { t as x_default } from "../../../chunks/x-2KXnqxiD.js";
2
2
  import { n as useGranularityTranslations } from "../../../chunks/granularityI18n-BC5zsj_d.js";
3
3
  import { t as GrIcon_default } from "../../GrIcon/chunks/GrIcon-bQwtZtYT.js";
4
4
  import { t as minus_default } from "../../../chunks/minus-oxdbHF33.js";
5
+ import { useAnnouncer } from "../../../composables/useAnnouncer.js";
5
6
  import { t as useModalOverlay } from "../../../chunks/useModalOverlay-Cv5hF63W.js";
6
7
  import { useDragGesture } from "../../../composables/useDragGesture.js";
7
- import { useAnnouncer } from "../../../composables/useAnnouncer.js";
8
8
  import { t as chevron_left_default } from "../../../chunks/chevron-left-D8AUBAM7.js";
9
9
  import { t as chevron_right_default } from "../../../chunks/chevron-right-C6Z01h0X.js";
10
10
  import { a as emptyStateClass, c as toolbarSeparatorClass, i as chromeButtonClass, l as toolbarShellClass, o as scrimClass, r as badgeClass, s as toolbarButtonClass } from "../../../chunks/config-BeZRuonp.js";
@@ -8,8 +8,8 @@ import { useGrFormControl } from "../../../composables/useGrFormControl.js";
8
8
  import { t as useFocusWithin } from "../../../chunks/useFocusWithin-BwHE0rzm.js";
9
9
  import { t as useControlAddons } from "../../../chunks/useControlAddons-asUr10Pf.js";
10
10
  import { t as GrBadge_default } from "../../GrBadge/chunks/GrBadge-u2ZdS7CN.js";
11
- import { t as loader_2_default } from "../../../chunks/loader-2-B1fdsdWW.js";
12
11
  import { useAnnouncer } from "../../../composables/useAnnouncer.js";
12
+ import { t as loader_2_default } from "../../../chunks/loader-2-B1fdsdWW.js";
13
13
  import { useRovingFocus } from "../../../composables/useRovingFocus.js";
14
14
  import { a as grInputTagWrapperClass, i as grInputTagInputClass, o as removeButtonClass, r as clearButtonClass, s as spinnerClass } from "../../../chunks/config-BN4sXYXn.js";
15
15
  import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, nextTick, normalizeClass, normalizeStyle, openBlock, ref, renderList, renderSlot, toDisplayString, unref, withCtx, withModifiers } from "vue";