@feugene/granularity 0.42.0 → 0.44.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 (193) hide show
  1. package/README.md +2 -2
  2. package/dist/chunks/config-BXRmsn3t.js +172 -0
  3. package/dist/chunks/config-BXRmsn3t.js.map +1 -0
  4. package/dist/chunks/config-BkQ2jvXv.js +49 -0
  5. package/dist/chunks/config-BkQ2jvXv.js.map +1 -0
  6. package/dist/chunks/config-Dg0bSoGw.js +81 -0
  7. package/dist/chunks/config-Dg0bSoGw.js.map +1 -0
  8. package/dist/chunks/config-DmED6aJV.js +237 -0
  9. package/dist/chunks/config-DmED6aJV.js.map +1 -0
  10. package/dist/chunks/{config-DWL81-aM.js → config-hnoZvcIM.js} +2 -20
  11. package/dist/chunks/config-hnoZvcIM.js.map +1 -0
  12. package/dist/chunks/dragSortGeometry-CY-llfrQ.js +87 -0
  13. package/dist/chunks/dragSortGeometry-CY-llfrQ.js.map +1 -0
  14. package/dist/chunks/{en-B9E3jO6m.js → en-DiHzFact.js} +34 -1
  15. package/dist/chunks/en-DiHzFact.js.map +1 -0
  16. package/dist/chunks/{es-QpzdIEut.js → es-1ZL6NtkB.js} +34 -1
  17. package/dist/chunks/es-1ZL6NtkB.js.map +1 -0
  18. package/dist/chunks/{es-DCkqHTlU.js → es-CeKkMxZ_.js} +3 -3
  19. package/dist/chunks/{es-DCkqHTlU.js.map → es-CeKkMxZ_.js.map} +1 -1
  20. package/dist/chunks/{generated-B95IBXs6.js → generated-Hxu_sWPs.js} +238 -0
  21. package/dist/chunks/generated-Hxu_sWPs.js.map +1 -0
  22. package/dist/chunks/{granular-provider-BG_2XqkS.js → granular-provider-CCNbQa5U.js} +9 -1
  23. package/dist/chunks/granular-provider-CCNbQa5U.js.map +1 -0
  24. package/dist/chunks/{ru-D_1Ya489.js → ru-Dr2RYn0P.js} +34 -1
  25. package/dist/chunks/ru-Dr2RYn0P.js.map +1 -0
  26. package/dist/chunks/scrollOverflow-B_kEdcol.js +31 -0
  27. package/dist/chunks/scrollOverflow-B_kEdcol.js.map +1 -0
  28. package/dist/chunks/tones-CRJAaYyg.js +21 -0
  29. package/dist/chunks/tones-CRJAaYyg.js.map +1 -0
  30. package/dist/chunks/useScrollSpy-CIJpw0RI.js +360 -0
  31. package/dist/chunks/useScrollSpy-CIJpw0RI.js.map +1 -0
  32. package/dist/components/GrAffix/chunks/GrAffix-CWtC7yi8.js +272 -0
  33. package/dist/components/GrAffix/chunks/GrAffix-CWtC7yi8.js.map +1 -0
  34. package/dist/components/GrAffix/index.js +3 -0
  35. package/dist/components/GrCarousel/chunks/GrCarousel-ByuughtI.js +761 -0
  36. package/dist/components/GrCarousel/chunks/GrCarousel-ByuughtI.js.map +1 -0
  37. package/dist/components/GrCarousel/index.js +3 -0
  38. package/dist/components/GrCarousel/styles.css +1 -0
  39. package/dist/components/GrColorPicker/chunks/{GrColorPicker-BwzRLs-i.js → GrColorPicker-9mTR-YZ_.js} +1 -1
  40. package/dist/components/GrColorPicker/chunks/{GrColorPicker-BwzRLs-i.js.map → GrColorPicker-9mTR-YZ_.js.map} +1 -1
  41. package/dist/components/GrColorPicker/index.js +1 -1
  42. package/dist/components/GrDataTable/chunks/{GrDataTable-BBe7-uUe.js → GrDataTable-xNSmO_I-.js} +4 -3
  43. package/dist/components/GrDataTable/chunks/{GrDataTable-BBe7-uUe.js.map → GrDataTable-xNSmO_I-.js.map} +1 -1
  44. package/dist/components/GrDataTable/index.js +1 -1
  45. package/dist/components/GrImageViewer/chunks/{GrImageViewer-Dd2gJ-j_.js.map → GrImageViewer-DK-iVHHC.js.map} +1 -1
  46. package/dist/components/GrImageViewer/index.js +1 -1
  47. package/dist/components/GrInputTag/chunks/{GrInputTag-BKzm1PKw.js.map → GrInputTag-Ci8AO1ze.js.map} +1 -1
  48. package/dist/components/GrInputTag/index.js +1 -1
  49. package/dist/components/GrJsonViewer/chunks/{GrJsonViewer-BLCEWlwg.js → GrJsonViewer-CwCBMh4h.js} +2 -2
  50. package/dist/components/GrJsonViewer/chunks/{GrJsonViewer-BLCEWlwg.js.map → GrJsonViewer-CwCBMh4h.js.map} +1 -1
  51. package/dist/components/GrJsonViewer/index.js +1 -1
  52. package/dist/components/GrNumberInput/chunks/{GrNumberInput-Bm-V59TG.js.map → GrNumberInput-CL5QhOmx.js.map} +1 -1
  53. package/dist/components/GrNumberInput/index.js +1 -1
  54. package/dist/components/GrProgressBar/index.js +1 -1
  55. package/dist/components/GrScrollSpy/chunks/GrScrollSpy-CQCH3zLC.js +207 -0
  56. package/dist/components/GrScrollSpy/chunks/GrScrollSpy-CQCH3zLC.js.map +1 -0
  57. package/dist/components/GrScrollSpy/index.js +3 -0
  58. package/dist/components/GrSlider/chunks/{GrSlider-Db-jACXz.js.map → GrSlider-CO8N9SF6.js.map} +1 -1
  59. package/dist/components/GrSlider/index.js +1 -1
  60. package/dist/components/GrSortableList/chunks/{GrSortableList-ce1psLL0.js → GrSortableList-NKriuGm_.js} +2 -1
  61. package/dist/components/GrSortableList/chunks/{GrSortableList-ce1psLL0.js.map → GrSortableList-NKriuGm_.js.map} +1 -1
  62. package/dist/components/GrSortableList/index.js +1 -1
  63. package/dist/components/GrTabs/chunks/{GrTabs-BArefUFv.js → GrTabs-s9RzC1WX.js} +2 -31
  64. package/dist/components/GrTabs/chunks/GrTabs-s9RzC1WX.js.map +1 -0
  65. package/dist/components/GrTabs/index.js +1 -1
  66. package/dist/components/GrTransfer/chunks/GrTransfer-8FA78ny6.js +1368 -0
  67. package/dist/components/GrTransfer/chunks/GrTransfer-8FA78ny6.js.map +1 -0
  68. package/dist/components/GrTransfer/index.js +3 -0
  69. package/dist/components/GrTransfer/styles.css +1 -0
  70. package/dist/components/GrTree/chunks/{GrTree-BFWFc0Bm.js → GrTree-CkHzgYg1.js} +3 -3
  71. package/dist/components/GrTree/chunks/{GrTree-BFWFc0Bm.js.map → GrTree-CkHzgYg1.js.map} +1 -1
  72. package/dist/components/GrTree/index.js +1 -1
  73. package/dist/components/GrTreeSelect/chunks/{GrTreeSelect-DJa2lYGe.js → GrTreeSelect-DUsv3_mF.js} +1 -1
  74. package/dist/components/GrTreeSelect/chunks/{GrTreeSelect-DJa2lYGe.js.map → GrTreeSelect-DUsv3_mF.js.map} +1 -1
  75. package/dist/components/GrTreeSelect/index.js +1 -1
  76. package/dist/composables/useDragSort.js +179 -1
  77. package/dist/composables/useDragSort.js.map +1 -0
  78. package/dist/composables/useScrollSpy.js +2 -0
  79. package/dist/granular-provider-node.js +1 -1
  80. package/dist/granular-provider.js +1 -1
  81. package/dist/i18n/all.js +1 -1
  82. package/dist/i18n/index.js +1 -1
  83. package/dist/index.js +26 -19
  84. package/dist/theme.js +1 -1
  85. package/dist/tokens.js +1 -1
  86. package/dist/types/components/GrAffix/GrAffix.vue.d.ts +91 -0
  87. package/dist/types/components/GrAffix/affixState.d.ts +81 -0
  88. package/dist/types/components/GrAffix/config.d.ts +1 -0
  89. package/dist/types/components/GrAffix/grAffixStyles.d.ts +46 -0
  90. package/dist/types/components/GrAffix/index.d.ts +9 -0
  91. package/dist/types/components/GrAffix/safelist.d.ts +1 -0
  92. package/dist/types/components/GrAlert/GrAlert.vue.d.ts +1 -1
  93. package/dist/types/components/GrBadgeWrap/GrBadgeWrap.vue.d.ts +2 -2
  94. package/dist/types/components/GrButton/GrButton.vue.d.ts +2 -2
  95. package/dist/types/components/GrCard/GrCard.vue.d.ts +1 -1
  96. package/dist/types/components/GrCarousel/GrCarousel.vue.d.ts +151 -0
  97. package/dist/types/components/GrCarousel/GrCarouselSlide.vue.d.ts +53 -0
  98. package/dist/types/components/GrCarousel/carouselNavigation.d.ts +57 -0
  99. package/dist/types/components/GrCarousel/composables/useCarouselAutoplay.d.ts +33 -0
  100. package/dist/types/components/GrCarousel/composables/useCarouselSwipe.d.ts +20 -0
  101. package/dist/types/components/GrCarousel/config.d.ts +7 -0
  102. package/dist/types/components/GrCarousel/defaults.d.ts +13 -0
  103. package/dist/types/components/GrCarousel/grCarouselContext.d.ts +46 -0
  104. package/dist/types/components/GrCarousel/grCarouselStyles.d.ts +80 -0
  105. package/dist/types/components/GrCarousel/index.d.ts +12 -0
  106. package/dist/types/components/GrCarousel/safelist.d.ts +1 -0
  107. package/dist/types/components/GrCheckbox/GrCheckbox.vue.d.ts +3 -3
  108. package/dist/types/components/GrCheckboxGroup/GrCheckboxGroup.vue.d.ts +2 -2
  109. package/dist/types/components/GrChip/GrChip.vue.d.ts +2 -2
  110. package/dist/types/components/GrChipGroup/GrChipGroup.vue.d.ts +2 -2
  111. package/dist/types/components/GrCollapse/GrCollapse.vue.d.ts +1 -1
  112. package/dist/types/components/GrCollapse/GrCollapseItem.vue.d.ts +1 -1
  113. package/dist/types/components/GrColorPicker/GrColorPicker.vue.d.ts +3 -3
  114. package/dist/types/components/GrCommandPalette/GrCommandPalette.vue.d.ts +1 -1
  115. package/dist/types/components/GrConfirmDialog/GrConfirmDialog.vue.d.ts +1 -1
  116. package/dist/types/components/GrContextMenu/GrContextMenu.vue.d.ts +2 -2
  117. package/dist/types/components/GrDialog/GrDialog.vue.d.ts +1 -1
  118. package/dist/types/components/GrDivider/GrDivider.vue.d.ts +2 -2
  119. package/dist/types/components/GrDrawer/GrDrawer.vue.d.ts +1 -1
  120. package/dist/types/components/GrDropdown/GrDropdown.vue.d.ts +3 -3
  121. package/dist/types/components/GrDropdownMenu/GrDropdownMenu.vue.d.ts +3 -3
  122. package/dist/types/components/GrDropdownMenu/GrDropdownMenuItem.vue.d.ts +1 -1
  123. package/dist/types/components/GrEmptyState/GrEmptyState.vue.d.ts +1 -1
  124. package/dist/types/components/GrFileUpload/GrFileUpload.vue.d.ts +1 -1
  125. package/dist/types/components/GrFormField/GrFormField.vue.d.ts +3 -3
  126. package/dist/types/components/GrFormFile/GrFormFile.vue.d.ts +2 -2
  127. package/dist/types/components/GrFormSection/GrFormSection.vue.d.ts +1 -1
  128. package/dist/types/components/GrIcon/GrIcon.vue.d.ts +1 -1
  129. package/dist/types/components/GrImageViewer/GrImageViewer.vue.d.ts +3 -3
  130. package/dist/types/components/GrInput/GrInput.vue.d.ts +4 -4
  131. package/dist/types/components/GrInputTag/GrInputTag.vue.d.ts +3 -3
  132. package/dist/types/components/GrKbd/GrKbd.vue.d.ts +1 -1
  133. package/dist/types/components/GrLink/GrLink.vue.d.ts +1 -1
  134. package/dist/types/components/GrList/GrListItem.vue.d.ts +2 -2
  135. package/dist/types/components/GrNavbar/GrNavbar.vue.d.ts +2 -2
  136. package/dist/types/components/GrNumberInput/GrNumberInput.vue.d.ts +3 -3
  137. package/dist/types/components/GrPagination/GrPagination.vue.d.ts +1 -1
  138. package/dist/types/components/GrPopover/GrPopover.vue.d.ts +3 -3
  139. package/dist/types/components/GrPromptDialog/GrPromptDialog.vue.d.ts +3 -3
  140. package/dist/types/components/GrRadio/GrRadio.vue.d.ts +2 -2
  141. package/dist/types/components/GrRadioGroup/GrRadioGroup.vue.d.ts +2 -2
  142. package/dist/types/components/GrRating/GrRating.vue.d.ts +2 -2
  143. package/dist/types/components/GrScrollSpy/GrScrollSpy.vue.d.ts +105 -0
  144. package/dist/types/components/GrScrollSpy/config.d.ts +1 -0
  145. package/dist/types/components/GrScrollSpy/grScrollSpyStyles.d.ts +26 -0
  146. package/dist/types/components/GrScrollSpy/index.d.ts +9 -0
  147. package/dist/types/components/GrScrollSpy/safelist.d.ts +1 -0
  148. package/dist/types/components/GrScrollSpy/scrollSpyItems.d.ts +44 -0
  149. package/dist/types/components/GrSegmented/GrSegmented.vue.d.ts +2 -2
  150. package/dist/types/components/GrSidebar/GrSidebarItem.vue.d.ts +1 -1
  151. package/dist/types/components/GrSkeleton/GrSkeleton.vue.d.ts +1 -1
  152. package/dist/types/components/GrSlider/GrSlider.vue.d.ts +3 -3
  153. package/dist/types/components/GrSplitter/GrSplitter.vue.d.ts +1 -1
  154. package/dist/types/components/GrStatistic/GrStatistic.vue.d.ts +2 -2
  155. package/dist/types/components/GrSwitch/GrSwitch.vue.d.ts +3 -3
  156. package/dist/types/components/GrTable/GrTable.vue.d.ts +1 -1
  157. package/dist/types/components/GrTabs/GrTabs.vue.d.ts +1 -1
  158. package/dist/types/components/GrTextarea/GrTextarea.vue.d.ts +2 -2
  159. package/dist/types/components/GrTimeline/GrTimelineItem.vue.d.ts +2 -2
  160. package/dist/types/components/GrToaster/GrToaster.vue.d.ts +1 -1
  161. package/dist/types/components/GrTooltip/GrTooltip.vue.d.ts +3 -3
  162. package/dist/types/components/GrTransfer/GrTransfer.vue.d.ts +103 -0
  163. package/dist/types/components/GrTransfer/config.d.ts +1 -0
  164. package/dist/types/components/GrTransfer/defaults.d.ts +15 -0
  165. package/dist/types/components/GrTransfer/grTransferStyles.d.ts +127 -0
  166. package/dist/types/components/GrTransfer/index.d.ts +11 -0
  167. package/dist/types/components/GrTransfer/safelist.d.ts +1 -0
  168. package/dist/types/components/GrTransfer/transferGeometry.d.ts +44 -0
  169. package/dist/types/components/GrTransfer/transferModel.d.ts +68 -0
  170. package/dist/types/components/GrTransfer/transferSelection.d.ts +36 -0
  171. package/dist/types/components/GrTransfer/useTransferDrag.d.ts +48 -0
  172. package/dist/types/composables/internal/scrollSpyGeometry.d.ts +67 -0
  173. package/dist/types/composables/useScrollSpy.d.ts +59 -0
  174. package/dist/types/granular-provider/shared.d.ts +4 -0
  175. package/dist/types/i18n/messages/en.d.ts +64 -0
  176. package/dist/types/i18n/messages/es.d.ts +64 -0
  177. package/dist/types/i18n/messages/ru.d.ts +64 -0
  178. package/dist/types/index.d.ts +6 -0
  179. package/dist/web-types.json +809 -284
  180. package/package.json +25 -1
  181. package/dist/chunks/config-DWL81-aM.js.map +0 -1
  182. package/dist/chunks/en-B9E3jO6m.js.map +0 -1
  183. package/dist/chunks/es-QpzdIEut.js.map +0 -1
  184. package/dist/chunks/generated-B95IBXs6.js.map +0 -1
  185. package/dist/chunks/granular-provider-BG_2XqkS.js.map +0 -1
  186. package/dist/chunks/ru-D_1Ya489.js.map +0 -1
  187. package/dist/chunks/useDragSort-0l47h4yb.js +0 -265
  188. package/dist/chunks/useDragSort-0l47h4yb.js.map +0 -1
  189. package/dist/components/GrTabs/chunks/GrTabs-BArefUFv.js.map +0 -1
  190. package/dist/components/GrImageViewer/chunks/{GrImageViewer-Dd2gJ-j_.js → GrImageViewer-DK-iVHHC.js} +3 -3
  191. package/dist/components/GrInputTag/chunks/{GrInputTag-BKzm1PKw.js → GrInputTag-Ci8AO1ze.js} +1 -1
  192. package/dist/components/GrNumberInput/chunks/{GrNumberInput-Bm-V59TG.js → GrNumberInput-CL5QhOmx.js} +1 -1
  193. package/dist/components/GrSlider/chunks/{GrSlider-Db-jACXz.js → GrSlider-CO8N9SF6.js} +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GrScrollSpy-CQCH3zLC.js","names":[],"sources":["../../../../src/components/GrScrollSpy/scrollSpyItems.ts","../../../../src/components/GrScrollSpy/GrScrollSpy.vue","../../../../src/components/GrScrollSpy/GrScrollSpy.vue"],"sourcesContent":["/**\n * Арифметика пунктов оглавления: всё, что следует из того, что пункт —\n * **ссылка**, а не из положения прокрутки. Положение живёт в\n * `composables/internal/scrollSpyGeometry.ts`.\n */\n\n/** Раздел в оглавлении. */\nexport interface GrScrollSpySection {\n id: string\n label: string\n /** Уровень вложенности, 1 — верхний. Влияет на отступ и цепочку предков. */\n level?: number\n}\n\n/**\n * Значение пропа `offset` в CSS-длину для инлайновой переменной.\n *\n * Строка уходит как есть и намеренно не валидируется: `4rem`,\n * `var(--gr-navbar-height)`, `calc(…)` обязаны работать — иначе проп проигрывает\n * голому `style`. Отрицательное зажимается в ноль: линия активации выше верха\n * скроллпорта смысла не имеет.\n */\nexport function scrollSpyOffsetLength(value: number | string | undefined): string | undefined {\n if (value == null)\n return undefined\n\n if (typeof value === 'number') {\n if (!Number.isFinite(value))\n return undefined\n\n // Число выносится в переменную, чтобы литерал остался одним токеном: гейт\n // safelist разбирает шаблонные строки по пробелам и обломок вида `value)}px`\n // принимает за утилиту UnoCSS — которой он, по совпадению, и оказывается.\n const px = Math.max(0, value)\n\n return `${px}px`\n }\n\n const trimmed = value.trim()\n\n return trimmed === '' ? undefined : trimmed\n}\n\n/**\n * Наш ли это клик.\n *\n * Пункт остаётся `<a href=\"#id\">` ровно ради того, что браузер умеет с ссылкой\n * сам: `Cmd`-клик открывает в новой вкладке, средняя кнопка — тоже. Перехватив\n * их, компонент отнял бы у пользователя работающее поведение и ничего не дал\n * взамен.\n */\nexport function anchorClickHandled(event: {\n button: number\n metaKey: boolean\n ctrlKey: boolean\n shiftKey: boolean\n altKey: boolean\n defaultPrevented: boolean\n}): boolean {\n if (event.defaultPrevented || event.button !== 0)\n return false\n\n return !(event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)\n}\n\n/**\n * Предки активного пункта — те разделы выше по списку, чей уровень мельче.\n *\n * Считается по документному порядку массива, а не по дереву: дерево позволило\n * бы объявить вложенность, противоречащую порядку разделов на странице.\n */\nexport function ancestorSectionIds(\n sections: readonly GrScrollSpySection[],\n activeId: string | null,\n): Set<string> {\n const ancestors = new Set<string>()\n\n if (activeId === null)\n return ancestors\n\n const index = sections.findIndex(section => section.id === activeId)\n\n if (index < 0)\n return ancestors\n\n let level = sections[index].level ?? 1\n\n for (let cursor = index - 1; cursor >= 0; cursor -= 1) {\n const candidate = sections[cursor].level ?? 1\n\n if (candidate < level) {\n ancestors.add(sections[cursor].id)\n level = candidate\n }\n }\n\n return ancestors\n}\n","<script setup lang=\"ts\">\nimport { computed, ref, watchEffect } from 'vue'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { useScrollSpy } from '../../composables/useScrollSpy'\n\nimport type { GrScrollSpySection } from './scrollSpyItems'\nimport { anchorClickHandled, ancestorSectionIds, scrollSpyOffsetLength } from './scrollSpyItems'\nimport { grScrollSpyItemClass, scrollSpyListClass, scrollSpyRootClass } from './grScrollSpyStyles'\n\n/**\n * GrScrollSpy — оглавление длинной страницы, которое знает, до какого раздела\n * дочитали: активный пункт подсвечивается по прокрутке, клик по пункту ведёт к\n * разделу.\n *\n * Логика положения живёт в композабле `useScrollSpy` и доступна отдельно — этот\n * компонент добавляет к ней разметку, `aria-current` и всё, что следует из того,\n * что пункт является ссылкой.\n */\nexport interface GrScrollSpyProps {\n /** Разделы в порядке их следования на странице. */\n sections: GrScrollSpySection[]\n /**\n * Отступ линии активации от верха скроллпорта. Число — пиксели, строка — как\n * есть. Почти всегда равен отступу липкой шапки (`--gr-affix-offset`).\n */\n offset?: number | string\n /** Скроллпорт: элемент или селектор. Не задан — ищется от первого раздела. */\n scroller?: HTMLElement | string | null\n ariaLabel?: string\n /** Обновлять `#hash` адреса при переходе. */\n updateHash?: boolean\n /** Плавность прокрутки. Под `prefers-reduced-motion` всегда мгновенная. */\n behavior?: ScrollBehavior\n /** Переносить фокус на раздел, как это делает переход по якорю. */\n focusTarget?: boolean\n disabled?: boolean\n}\n\nexport interface GrScrollSpyEmits {\n (e: 'activeChange', id: string | null): void\n (e: 'select', id: string, event: MouseEvent): void\n}\n\nconst props = withDefaults(defineProps<GrScrollSpyProps>(), {\n offset: undefined,\n scroller: undefined,\n ariaLabel: undefined,\n updateHash: true,\n behavior: undefined,\n focusTarget: true,\n disabled: false,\n})\n\nconst emit = defineEmits<GrScrollSpyEmits>()\n\ndefineSlots<{\n /** Пункт целиком. `activate` повторяет клик по нему. */\n item?: (props: {\n section: GrScrollSpySection\n active: boolean\n ancestor: boolean\n activate: () => void\n }) => unknown\n}>()\n\nconst { t } = useGranularityTranslations()\n\nconst rootEl = ref<HTMLElement | null>(null)\n\nconst label = computed(() => props.ariaLabel ?? t('gr.scrollSpy.label', 'On this page'))\n\nconst rootStyle = computed(() => {\n const length = scrollSpyOffsetLength(props.offset)\n\n return length === undefined ? undefined : { '--gr-scroll-spy-offset': length }\n})\n\n/**\n * Отступ берётся из вычисленного стиля, а не из пропа: браузер уже разрешил\n * `4rem`, `var(...)` и `calc(...)` в used value, и свой парсер CSS не нужен.\n * Заодно работает отступ, заданный каскадом, — а он и есть общий дефолт группы.\n *\n * Носитель — `scroll-margin-top` на корне: вычисленное значение этого свойства\n * всегда абсолютная длина, а на само оглавление оно влияет ровно там, где нужно,\n * и в ту же сторону — отодвигает его от края при переходе по якорю.\n */\nfunction measureOffsetPx(): number {\n const el = rootEl.value\n\n if (!el || typeof getComputedStyle !== 'function')\n return 0\n\n const parsed = Number.parseFloat(getComputedStyle(el).scrollMarginTop)\n\n return Number.isFinite(parsed) ? Math.max(0, parsed) : 0\n}\n\nfunction resolveScroller(): HTMLElement | null {\n const value = props.scroller\n\n if (typeof value === 'string')\n return typeof document === 'undefined' ? null : document.querySelector<HTMLElement>(value)\n\n return value ?? null\n}\n\nconst spy = useScrollSpy({\n sections: () => (Array.isArray(props.sections) ? props.sections.map(section => section.id) : []),\n scroller: resolveScroller,\n offset: measureOffsetPx,\n disabled: () => props.disabled,\n onChange: id => emit('activeChange', id),\n})\n\nconst ancestors = computed(() => ancestorSectionIds(props.sections ?? [], spy.active.value))\n\nfunction itemState(id: string): 'active' | 'ancestor' | 'idle' {\n if (spy.active.value === id)\n return 'active'\n\n return ancestors.value.has(id) ? 'ancestor' : 'idle'\n}\n\n/**\n * `preventDefault()` отнимает у ссылки то, что браузер делает сам при переходе\n * по якорю, — перенос точки последовательной навигации. Без компенсации\n * клавиатурный пользователь после активации пункта продолжает `Tab` внутри\n * оглавления, а не с начала раздела.\n *\n * `preventScroll` обязателен: иначе фокус телепортирует прокрутку и подерётся с\n * плавной. Временный `tabindex` снимается по `blur` — чужой DOM остаётся чужим.\n */\nfunction focusSection(id: string): void {\n const el = typeof document === 'undefined' ? null : document.getElementById(id)\n\n if (!el)\n return\n\n if (!el.hasAttribute('tabindex')) {\n el.setAttribute('tabindex', '-1')\n el.addEventListener('blur', () => el.removeAttribute('tabindex'), { once: true })\n }\n\n el.focus({ preventScroll: true })\n}\n\nfunction activate(id: string, event?: MouseEvent): void {\n spy.scrollTo(id, props.behavior === undefined ? undefined : { behavior: props.behavior })\n\n if (props.updateHash && typeof history !== 'undefined') {\n // `replaceState`, а не `pushState`: запись истории на каждый пункт\n // превратила бы «Назад» в отмену прокрутки вместо возврата на страницу.\n history.replaceState(null, '', `#${id}`)\n }\n\n if (props.focusTarget)\n focusSection(id)\n\n if (event)\n emit('select', id, event)\n}\n\nfunction onItemClick(id: string, event: MouseEvent): void {\n if (!anchorClickHandled(event))\n return\n\n event.preventDefault()\n activate(id, event)\n}\n\ndefineExpose({\n active: spy.active,\n scrollTo: spy.scrollTo,\n refresh: spy.refresh,\n})\n\nif (__GR_DEV__) {\n watchEffect(() => {\n if (!Array.isArray(props.sections)) {\n console.warn(\n `[granularity] GrScrollSpy: обязательный проп \\`sections\\` должен быть массивом — получено ${String(props.sections)}.`,\n )\n }\n })\n}\n</script>\n\n<template>\n <!--\n Пустой список не рендерит ничего: пустой лендмарк `navigation` шумит в\n списке ориентиров у диктора, не давая взамен ни одной цели.\n -->\n <nav\n v-if=\"Array.isArray(sections) && sections.length > 0\"\n ref=\"rootEl\"\n data-gr-scroll-spy\n :aria-label=\"label\"\n :class=\"scrollSpyRootClass\"\n :style=\"rootStyle\"\n >\n <!--\n `role=\"list\"` обязателен: снятый `list-style` отнимает у списка роль в\n WebKit, и оглавление перестаёт объявляться списком.\n -->\n <ul role=\"list\" :class=\"scrollSpyListClass\">\n <li\n v-for=\"section in sections\"\n :key=\"section.id\"\n :aria-level=\"section.level ?? 1\"\n :data-level=\"section.level ?? 1\"\n :style=\"{ paddingInlineStart: `calc(var(--gr-scroll-spy-indent, 0.75rem) * ${(section.level ?? 1) - 1})` }\"\n >\n <slot\n name=\"item\"\n :section=\"section\"\n :active=\"spy.active.value === section.id\"\n :ancestor=\"ancestors.has(section.id)\"\n :activate=\"() => activate(section.id)\"\n >\n <a\n :href=\"`#${section.id}`\"\n data-gr-scroll-spy-item\n :data-active=\"spy.active.value === section.id ? 'true' : undefined\"\n :data-ancestor=\"ancestors.has(section.id) ? 'true' : undefined\"\n :aria-current=\"spy.active.value === section.id ? 'location' : undefined\"\n :class=\"grScrollSpyItemClass(itemState(section.id))\"\n @click=\"onItemClick(section.id, $event)\"\n >\n {{ section.label }}\n </a>\n </slot>\n </li>\n </ul>\n </nav>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, ref, watchEffect } from 'vue'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { useScrollSpy } from '../../composables/useScrollSpy'\n\nimport type { GrScrollSpySection } from './scrollSpyItems'\nimport { anchorClickHandled, ancestorSectionIds, scrollSpyOffsetLength } from './scrollSpyItems'\nimport { grScrollSpyItemClass, scrollSpyListClass, scrollSpyRootClass } from './grScrollSpyStyles'\n\n/**\n * GrScrollSpy — оглавление длинной страницы, которое знает, до какого раздела\n * дочитали: активный пункт подсвечивается по прокрутке, клик по пункту ведёт к\n * разделу.\n *\n * Логика положения живёт в композабле `useScrollSpy` и доступна отдельно — этот\n * компонент добавляет к ней разметку, `aria-current` и всё, что следует из того,\n * что пункт является ссылкой.\n */\nexport interface GrScrollSpyProps {\n /** Разделы в порядке их следования на странице. */\n sections: GrScrollSpySection[]\n /**\n * Отступ линии активации от верха скроллпорта. Число — пиксели, строка — как\n * есть. Почти всегда равен отступу липкой шапки (`--gr-affix-offset`).\n */\n offset?: number | string\n /** Скроллпорт: элемент или селектор. Не задан — ищется от первого раздела. */\n scroller?: HTMLElement | string | null\n ariaLabel?: string\n /** Обновлять `#hash` адреса при переходе. */\n updateHash?: boolean\n /** Плавность прокрутки. Под `prefers-reduced-motion` всегда мгновенная. */\n behavior?: ScrollBehavior\n /** Переносить фокус на раздел, как это делает переход по якорю. */\n focusTarget?: boolean\n disabled?: boolean\n}\n\nexport interface GrScrollSpyEmits {\n (e: 'activeChange', id: string | null): void\n (e: 'select', id: string, event: MouseEvent): void\n}\n\nconst props = withDefaults(defineProps<GrScrollSpyProps>(), {\n offset: undefined,\n scroller: undefined,\n ariaLabel: undefined,\n updateHash: true,\n behavior: undefined,\n focusTarget: true,\n disabled: false,\n})\n\nconst emit = defineEmits<GrScrollSpyEmits>()\n\ndefineSlots<{\n /** Пункт целиком. `activate` повторяет клик по нему. */\n item?: (props: {\n section: GrScrollSpySection\n active: boolean\n ancestor: boolean\n activate: () => void\n }) => unknown\n}>()\n\nconst { t } = useGranularityTranslations()\n\nconst rootEl = ref<HTMLElement | null>(null)\n\nconst label = computed(() => props.ariaLabel ?? t('gr.scrollSpy.label', 'On this page'))\n\nconst rootStyle = computed(() => {\n const length = scrollSpyOffsetLength(props.offset)\n\n return length === undefined ? undefined : { '--gr-scroll-spy-offset': length }\n})\n\n/**\n * Отступ берётся из вычисленного стиля, а не из пропа: браузер уже разрешил\n * `4rem`, `var(...)` и `calc(...)` в used value, и свой парсер CSS не нужен.\n * Заодно работает отступ, заданный каскадом, — а он и есть общий дефолт группы.\n *\n * Носитель — `scroll-margin-top` на корне: вычисленное значение этого свойства\n * всегда абсолютная длина, а на само оглавление оно влияет ровно там, где нужно,\n * и в ту же сторону — отодвигает его от края при переходе по якорю.\n */\nfunction measureOffsetPx(): number {\n const el = rootEl.value\n\n if (!el || typeof getComputedStyle !== 'function')\n return 0\n\n const parsed = Number.parseFloat(getComputedStyle(el).scrollMarginTop)\n\n return Number.isFinite(parsed) ? Math.max(0, parsed) : 0\n}\n\nfunction resolveScroller(): HTMLElement | null {\n const value = props.scroller\n\n if (typeof value === 'string')\n return typeof document === 'undefined' ? null : document.querySelector<HTMLElement>(value)\n\n return value ?? null\n}\n\nconst spy = useScrollSpy({\n sections: () => (Array.isArray(props.sections) ? props.sections.map(section => section.id) : []),\n scroller: resolveScroller,\n offset: measureOffsetPx,\n disabled: () => props.disabled,\n onChange: id => emit('activeChange', id),\n})\n\nconst ancestors = computed(() => ancestorSectionIds(props.sections ?? [], spy.active.value))\n\nfunction itemState(id: string): 'active' | 'ancestor' | 'idle' {\n if (spy.active.value === id)\n return 'active'\n\n return ancestors.value.has(id) ? 'ancestor' : 'idle'\n}\n\n/**\n * `preventDefault()` отнимает у ссылки то, что браузер делает сам при переходе\n * по якорю, — перенос точки последовательной навигации. Без компенсации\n * клавиатурный пользователь после активации пункта продолжает `Tab` внутри\n * оглавления, а не с начала раздела.\n *\n * `preventScroll` обязателен: иначе фокус телепортирует прокрутку и подерётся с\n * плавной. Временный `tabindex` снимается по `blur` — чужой DOM остаётся чужим.\n */\nfunction focusSection(id: string): void {\n const el = typeof document === 'undefined' ? null : document.getElementById(id)\n\n if (!el)\n return\n\n if (!el.hasAttribute('tabindex')) {\n el.setAttribute('tabindex', '-1')\n el.addEventListener('blur', () => el.removeAttribute('tabindex'), { once: true })\n }\n\n el.focus({ preventScroll: true })\n}\n\nfunction activate(id: string, event?: MouseEvent): void {\n spy.scrollTo(id, props.behavior === undefined ? undefined : { behavior: props.behavior })\n\n if (props.updateHash && typeof history !== 'undefined') {\n // `replaceState`, а не `pushState`: запись истории на каждый пункт\n // превратила бы «Назад» в отмену прокрутки вместо возврата на страницу.\n history.replaceState(null, '', `#${id}`)\n }\n\n if (props.focusTarget)\n focusSection(id)\n\n if (event)\n emit('select', id, event)\n}\n\nfunction onItemClick(id: string, event: MouseEvent): void {\n if (!anchorClickHandled(event))\n return\n\n event.preventDefault()\n activate(id, event)\n}\n\ndefineExpose({\n active: spy.active,\n scrollTo: spy.scrollTo,\n refresh: spy.refresh,\n})\n\nif (__GR_DEV__) {\n watchEffect(() => {\n if (!Array.isArray(props.sections)) {\n console.warn(\n `[granularity] GrScrollSpy: обязательный проп \\`sections\\` должен быть массивом — получено ${String(props.sections)}.`,\n )\n }\n })\n}\n</script>\n\n<template>\n <!--\n Пустой список не рендерит ничего: пустой лендмарк `navigation` шумит в\n списке ориентиров у диктора, не давая взамен ни одной цели.\n -->\n <nav\n v-if=\"Array.isArray(sections) && sections.length > 0\"\n ref=\"rootEl\"\n data-gr-scroll-spy\n :aria-label=\"label\"\n :class=\"scrollSpyRootClass\"\n :style=\"rootStyle\"\n >\n <!--\n `role=\"list\"` обязателен: снятый `list-style` отнимает у списка роль в\n WebKit, и оглавление перестаёт объявляться списком.\n -->\n <ul role=\"list\" :class=\"scrollSpyListClass\">\n <li\n v-for=\"section in sections\"\n :key=\"section.id\"\n :aria-level=\"section.level ?? 1\"\n :data-level=\"section.level ?? 1\"\n :style=\"{ paddingInlineStart: `calc(var(--gr-scroll-spy-indent, 0.75rem) * ${(section.level ?? 1) - 1})` }\"\n >\n <slot\n name=\"item\"\n :section=\"section\"\n :active=\"spy.active.value === section.id\"\n :ancestor=\"ancestors.has(section.id)\"\n :activate=\"() => activate(section.id)\"\n >\n <a\n :href=\"`#${section.id}`\"\n data-gr-scroll-spy-item\n :data-active=\"spy.active.value === section.id ? 'true' : undefined\"\n :data-ancestor=\"ancestors.has(section.id) ? 'true' : undefined\"\n :aria-current=\"spy.active.value === section.id ? 'location' : undefined\"\n :class=\"grScrollSpyItemClass(itemState(section.id))\"\n @click=\"onItemClick(section.id, $event)\"\n >\n {{ section.label }}\n </a>\n </slot>\n </li>\n </ul>\n </nav>\n</template>\n"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { t as GrScrollSpy_default } from "./chunks/GrScrollSpy-CQCH3zLC.js";
2
+ import { n as grScrollSpySafelist, t as grScrollSpyConfig } from "../../chunks/config-BkQ2jvXv.js";
3
+ export { GrScrollSpy_default as GrScrollSpy, GrScrollSpy_default as default, grScrollSpyConfig, grScrollSpySafelist };
@@ -1 +1 @@
1
- {"version":3,"file":"GrSlider-Db-jACXz.js","names":[],"sources":["../../../../src/components/GrSlider/GrSlider.vue","../../../../src/components/GrSlider/GrSlider.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref, watchEffect } from 'vue'\n\nimport { useGrComponentSize } from '../GrConfigProvider/context'\n\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { bigStep } from '../shared/numericStep'\nimport { useDragGesture } from '../../composables/useDragGesture'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useFocusWithin } from '../../composables/internal/useFocusWithin'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n\nimport {\n sliderFillClass,\n sliderMarkLabelClassFor,\n sliderMarkTickClass,\n sliderRailClass,\n sliderRootClass,\n sliderThumbClass,\n sliderTooltipClass,\n sliderTrackHeightBySize,\n sliderTrackVerticalLengthClass,\n sliderTrackWidthBySize,\n sliderFillOrientationClass,\n type GrSliderMarks,\n type GrSliderModelValue,\n type GrSliderOrientation,\n type GrSliderSize,\n} from './grSliderStyles'\n\nexport type {\n GrSliderMarks,\n GrSliderModelValue,\n GrSliderOrientation,\n GrSliderSize,\n} from './grSliderStyles'\n\n/**\n * Публичный GR-примитив «Slider» (WAI-ARIA slider pattern).\n *\n * Поддерживает одиночное значение и диапазон (`range` — два бегунка), шаг, метки\n * делений, всплывающее значение (tooltip), полную клавиатурную навигацию\n * (стрелки / PageUp/Down / Home / End) и `role=\"slider\"` с\n * `aria-valuemin`/`max`/`now` на каждом бегунке.\n */\nexport interface GrSliderProps {\n /** `number` — одиночное значение; `[lo, hi]` — диапазон (при `range=true`). */\n modelValue: GrSliderModelValue\n min?: number\n max?: number\n step?: number\n /** Диапазон с двумя бегунками; модель — кортеж `[lo, hi]`. */\n range?: boolean\n disabled?: boolean\n /** Только для чтения: значение видно, но не меняется. */\n readonly?: boolean\n /** Визуальное и ARIA-состояние ошибки. */\n invalid?: boolean\n /** Обязательное поле (`aria-required`). */\n required?: boolean\n size?: GrSliderSize\n /** Метки делений: `{ [value]: label }` или массив значений. */\n marks?: GrSliderMarks\n /** Всплывающее значение над бегунком: `true`/`'hover'` — при hover/drag/focus, `'always'` — всегда. */\n showTooltip?: boolean | 'always' | 'hover'\n /** Форматирование значения в tooltip. Оно же уходит в `aria-valuetext`. */\n formatTooltip?: (value: number) => string\n /** Ориентация дорожки. В вертикальной минимум внизу. */\n orientation?: GrSliderOrientation\n /**\n * Эмитить `update:modelValue` только по завершении жеста: во время\n * перетаскивания значение ведёт сам слайдер. Клавиатура коммитит сразу —\n * нажатие клавиши дискретно, придерживать его нечего.\n */\n lazy?: boolean\n ariaLabel?: string\n /** Имя для нативной формы: hidden input на значение, при `range` — два с одним именем. */\n name?: string\n}\n\nexport interface GrSliderEmits {\n (e: 'update:modelValue', value: GrSliderModelValue): void\n /** Значение зафиксировано (отпущен бегунок / клавиша). */\n (e: 'change', value: GrSliderModelValue): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrSliderProps>(),\n {\n min: 0,\n max: 100,\n step: 1,\n range: false,\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n size: undefined,\n marks: undefined,\n showTooltip: false,\n formatTooltip: undefined,\n orientation: 'horizontal',\n lazy: false,\n ariaLabel: undefined,\n name: undefined,\n },\n)\n\nconst { t } = useGranularityTranslations()\n\nconst isVertical = computed(() => props.orientation === 'vertical')\n\n// Эффективный размер: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentSize(() => props.size, {\n component: 'GrSlider',\n supported: ['xs', 'sm', 'md', 'lg'],\n})\n\nconst emit = defineEmits<GrSliderEmits>()\n\n// Контекст `GrFormField`: id/aria-describedby/invalid/required как fallback.\nconst field = useGrFormFieldContext()\nconst resolvedId = computed(() => field?.id.value)\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n} = useGrFormControl(() => props)\nconst describedBy = computed(() => field?.describedById.value)\n\nconst trackEl = ref<HTMLElement | null>(null)\nconst rootEl = ref<HTMLElement | null>(null)\n\n// У диапазона два бегунка, и фокус ходит между ними: без границы переход с\n// одного на другой давал бы потребителю пару `blur` + `focus`.\nconst { onFocusIn, onFocusOut } = useFocusWithin(rootEl, {\n enter: event => emit('focus', event),\n leave: event => emit('blur', event),\n})\n\nconst thumbEls = ref<HTMLElement[]>([])\n\nfunction focus(): void {\n thumbEls.value[0]?.focus()\n}\n\nfunction blur(): void {\n thumbEls.value[0]?.blur()\n}\n\ndefineExpose({ focus, blur })\n\n// Нормализованные границы (гард от max<=min).\nconst span = computed(() => (props.max > props.min ? props.max - props.min : 1))\n\nconst stepDecimals = computed(() => {\n const s = String(props.step)\n const dot = s.indexOf('.')\n return dot >= 0 ? s.length - dot - 1 : 0\n})\n\nfunction clamp(value: number): number {\n return Math.min(props.max, Math.max(props.min, value))\n}\n\nfunction snap(value: number): number {\n const stepped = Math.round((value - props.min) / props.step) * props.step + props.min\n return Number(clamp(stepped).toFixed(stepDecimals.value))\n}\n\nfunction percent(value: number): number {\n return ((clamp(value) - props.min) / span.value) * 100\n}\n\n// Черновик жеста: при `lazy` значение до отпускания живёт здесь, а наружу не\n// уходит — иначе смысл режима теряется.\nconst draftValues = ref<number[] | null>(null)\n\n// Не доехавшее значение (промах вызова, асинхронный стор) увело бы в `NaN`\n// и смещение бегунка, и `aria-valuenow`; нижняя граница — честный старт.\nfunction toFiniteValue(value: number | undefined): number {\n return Number.isFinite(value) ? (value as number) : props.min\n}\n\n// ————— Значения бегунков как массив (single = один бегунок).\nconst modelValues = computed<number[]>(() => {\n if (props.range) {\n const v = Array.isArray(props.modelValue) ? props.modelValue : [props.min, props.min]\n return [snap(toFiniteValue(v[0])), snap(toFiniteValue(v[1]))]\n }\n const v = Array.isArray(props.modelValue) ? props.modelValue[0] : props.modelValue\n return [snap(toFiniteValue(v))]\n})\n\nif (__GR_DEV__) {\n watchEffect(() => {\n const missing = props.range\n ? !Array.isArray(props.modelValue)\n : !Number.isFinite(props.modelValue)\n\n if (missing) {\n console.warn(\n `[granularity] GrSlider: обязательный проп \\`modelValue\\` должен быть ${props.range ? 'парой чисел' : 'числом'} — получено ${String(props.modelValue)}.`,\n )\n }\n })\n}\n\nconst values = computed<number[]>(() => draftValues.value ?? modelValues.value)\n\n// `[touch-action:none]` обязателен: без него вертикальный свайп по бегунку\n// уходит в скролл страницы, браузер шлёт `pointercancel`, и слайдер пальцем\n// неуправляем. Утилиты `touch-none` нет ни в `presetMini`, ни в extra-rules —\n// поэтому arbitrary-значение.\nconst trackClass = computed(() => (isVertical.value\n ? `[touch-action:none] ${sliderTrackWidthBySize[resolvedSize.value]} ${sliderTrackVerticalLengthClass}`\n : `[touch-action:none] w-full ${sliderTrackHeightBySize[resolvedSize.value]}`))\n\n/** Смещение вдоль дорожки: по горизонтали слева, по вертикали снизу. */\nfunction offsetStyle(value: number): Record<string, string> {\n return isVertical.value ? { bottom: `${percent(value)}%` } : { left: `${percent(value)}%` }\n}\n\nconst fillStyle = computed(() => {\n const start = props.range\n ? Math.min(percent(values.value[0]), percent(values.value[1]))\n : 0\n const end = props.range\n ? Math.max(percent(values.value[0]), percent(values.value[1]))\n : percent(values.value[0])\n\n return isVertical.value\n ? { bottom: `${start}%`, top: `${100 - end}%` }\n : { left: `${start}%`, right: `${100 - end}%` }\n})\n\n// ————— Метки делений.\ntype SliderMark = { value: number, label: string }\nconst normalizedMarks = computed<SliderMark[]>(() => {\n if (!props.marks)\n return []\n if (Array.isArray(props.marks))\n return props.marks.map(v => ({ value: v, label: String(v) }))\n return Object.entries(props.marks).map(([value, label]) => ({ value: Number(value), label }))\n})\n\nfunction tooltipText(value: number): string {\n return props.formatTooltip ? props.formatTooltip(value) : String(value)\n}\n\n// ————— Обновление значения конкретного бегунка.\nfunction emitValue(next: number[], commit: boolean): void {\n const payload: GrSliderModelValue = props.range ? [next[0], next[1]] : next[0]\n emit('update:modelValue', payload)\n if (commit)\n emit('change', payload)\n}\n\nfunction setThumb(index: number, value: number, commit: boolean): void {\n const next = values.value.slice()\n next[index] = snap(value)\n\n // range: не даём бегункам «перепрыгнуть» друг друга.\n if (props.range) {\n if (index === 0)\n next[0] = Math.min(next[0], next[1])\n else next[1] = Math.max(next[1], next[0])\n }\n\n // `lazy` придерживает только непрерывный жест: `commit` приходит от клавиатуры\n // и от отпускания, и там значение уходит наружу как обычно.\n if (props.lazy && !commit) {\n draftValues.value = next\n return\n }\n\n draftValues.value = null\n emitValue(next, commit)\n}\n\n// ————— Pointer-драг.\nconst activeThumb = ref<number | null>(null)\nconst hoveredThumb = ref<number | null>(null)\n\nfunction valueFromPointer(event: { clientX: number, clientY: number }): number {\n const rect = trackEl.value?.getBoundingClientRect()\n if (!rect)\n return props.min\n\n // В вертикали минимум внизу, поэтому доля отсчитывается от нижнего края.\n const ratio = isVertical.value\n ? (rect.height === 0 ? 0 : 1 - (event.clientY - rect.top) / rect.height)\n : (rect.width === 0 ? 0 : (event.clientX - rect.left) / rect.width)\n\n return props.min + Math.min(1, Math.max(0, ratio)) * span.value\n}\n\n/**\n * Какой бегунок повезёт клик. При равенстве расстояний берём верхний: иначе\n * схлопнувшийся в точку диапазон не развести мышью — нижний бегунок упирается\n * в верхний и оба стоят на месте.\n */\nfunction nearestThumb(value: number): number {\n if (!props.range)\n return 0\n\n const [lo, hi] = values.value\n if (value < lo)\n return 0\n if (value > hi)\n return 1\n\n return Math.abs(value - lo) < Math.abs(value - hi) ? 0 : 1\n}\n\nfunction endDrag(commit: boolean): void {\n if (activeThumb.value !== null && commit) {\n // Финальный commit значения: у `lazy` он же и единственный `update:modelValue`.\n const next = values.value.slice()\n draftValues.value = null\n emitValue(next, true)\n }\n // Оборванный жест (`pointercancel`: браузер забрал указатель) значения не\n // коммитит — черновик просто отбрасывается, бегунок возвращается к модели.\n draftValues.value = null\n activeThumb.value = null\n}\n\nconst drag = useDragGesture({\n disabled: () => isDisabled.value || isReadonly.value,\n onStart: (event) => {\n const value = valueFromPointer(event)\n const index = nearestThumb(value)\n activeThumb.value = index\n // Фокусируем активный бегунок сразу — чтобы клавиатура работала после клика по\n // дорожке (div не получает фокус по клику автоматически, только через `.focus()`).\n thumbEls.value[index]?.focus()\n setThumb(index, value, false)\n },\n onMove: (event) => {\n if (activeThumb.value === null)\n return\n event.preventDefault()\n setThumb(activeThumb.value, valueFromPointer(event), false)\n },\n onEnd: () => endDrag(true),\n onCancel: () => endDrag(false),\n})\n\n// ————— Клавиатура.\nfunction keyboardBigStep(): number {\n // Границы передаются от нормализованного размаха, а не от сырых пропов:\n // `span` уже подставил единицу вместо вырожденного `max <= min`.\n return bigStep(props.step, props.min, props.min + span.value)\n}\n\nfunction onThumbKeydown(event: KeyboardEvent, index: number): void {\n if (isDisabled.value || isReadonly.value)\n return\n const current = values.value[index]\n let next: number | null = null\n\n switch (event.key) {\n case 'ArrowRight':\n case 'ArrowUp':\n next = current + props.step\n break\n case 'ArrowLeft':\n case 'ArrowDown':\n next = current - props.step\n break\n case 'PageUp':\n next = current + keyboardBigStep()\n break\n case 'PageDown':\n next = current - keyboardBigStep()\n break\n case 'Home':\n next = props.min\n break\n case 'End':\n next = props.max\n break\n default:\n return\n }\n\n event.preventDefault()\n setThumb(index, next, true)\n}\n\nfunction showTooltipFor(index: number): boolean {\n if (props.showTooltip === 'always')\n return true\n if (!props.showTooltip)\n return false\n return activeThumb.value === index || hoveredThumb.value === index\n}\n\nfunction thumbAriaLabel(index: number): string | undefined {\n if (!props.ariaLabel)\n return undefined\n if (!props.range)\n return props.ariaLabel\n\n const bound = index === 0 ? t('gr.slider.min', 'min') : t('gr.slider.max', 'max')\n return `${props.ariaLabel} (${bound})`\n}\n\n// `aria-valuetext` нужен, только когда число само по себе не читается: «1200»\n// против «$1 200». Без своего формата атрибут не добавляем.\nfunction thumbValueText(value: number): string | undefined {\n return props.formatTooltip ? props.formatTooltip(value) : undefined\n}\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n data-gr-slider\n :data-orientation=\"orientation\"\n :class=\"sliderRootClass({ size: resolvedSize, disabled: isDisabled, hasMarks: normalizedMarks.length > 0, orientation })\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n >\n <!-- Нативная форма: роль-виджет не labelable и в submit не попадает.\n Сериализуется модель (snapped), а не черновик жеста. -->\n <template v-if=\"name\">\n <input\n v-for=\"(value, index) in modelValues\"\n :key=\"index\"\n type=\"hidden\"\n :name=\"name\"\n :value=\"String(value)\"\n >\n </template>\n <div\n ref=\"trackEl\"\n data-gr-slider-track\n class=\"relative\"\n :class=\"trackClass\"\n @pointerdown=\"drag.start\"\n >\n <div :class=\"sliderRailClass\" />\n <div\n data-gr-slider-fill\n :class=\"[sliderFillClass(isDisabled), sliderFillOrientationClass[orientation]]\"\n :style=\"fillStyle\"\n />\n\n <!-- Метки делений. -->\n <template v-for=\"mark in normalizedMarks\" :key=\"mark.value\">\n <span\n data-gr-slider-mark\n :class=\"sliderMarkTickClass(orientation)\"\n :style=\"offsetStyle(mark.value)\"\n aria-hidden=\"true\"\n />\n <!-- Подпись метки дублирует значение, которое диктор читает с бегунка:\n как отдельный текст внутри слайдера она только мешает. -->\n <span\n data-gr-slider-mark-label\n :class=\"sliderMarkLabelClassFor(percent(mark.value), { orientation, disabled: isDisabled })\"\n :style=\"offsetStyle(mark.value)\"\n aria-hidden=\"true\"\n >\n {{ mark.label }}\n </span>\n </template>\n\n <!-- Бегунки. -->\n <div\n v-for=\"(value, index) in values\"\n :id=\"index === 0 ? resolvedId : undefined\"\n :key=\"index\"\n :ref=\"(el) => { if (el) thumbEls[index] = el as HTMLElement }\"\n data-gr-slider-thumb\n :data-testid=\"`gr-slider-thumb-${index}`\"\n :class=\"sliderThumbClass({ size: resolvedSize, disabled: isDisabled, orientation })\"\n :style=\"offsetStyle(value)\"\n role=\"slider\"\n :tabindex=\"isDisabled ? -1 : 0\"\n :aria-valuemin=\"range && index === 1 ? values[0] : min\"\n :aria-valuemax=\"range && index === 0 ? values[1] : max\"\n :aria-valuenow=\"value\"\n :aria-valuetext=\"thumbValueText(value)\"\n :aria-orientation=\"orientation\"\n :aria-label=\"thumbAriaLabel(index)\"\n :aria-disabled=\"isDisabled ? 'true' : undefined\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-describedby=\"index === 0 ? describedBy : undefined\"\n :aria-required=\"isRequired && index === 0 ? 'true' : undefined\"\n :aria-readonly=\"isReadonly ? 'true' : undefined\"\n @keydown=\"onThumbKeydown($event, index)\"\n @mouseenter=\"hoveredThumb = index\"\n @mouseleave=\"hoveredThumb = null\"\n @focus=\"hoveredThumb = index\"\n @blur=\"hoveredThumb = null\"\n >\n <span\n v-if=\"showTooltipFor(index)\"\n data-gr-slider-tooltip\n :class=\"sliderTooltipClass(orientation)\"\n >\n {{ tooltipText(value) }}\n </span>\n </div>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, ref, watchEffect } from 'vue'\n\nimport { useGrComponentSize } from '../GrConfigProvider/context'\n\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { bigStep } from '../shared/numericStep'\nimport { useDragGesture } from '../../composables/useDragGesture'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useFocusWithin } from '../../composables/internal/useFocusWithin'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n\nimport {\n sliderFillClass,\n sliderMarkLabelClassFor,\n sliderMarkTickClass,\n sliderRailClass,\n sliderRootClass,\n sliderThumbClass,\n sliderTooltipClass,\n sliderTrackHeightBySize,\n sliderTrackVerticalLengthClass,\n sliderTrackWidthBySize,\n sliderFillOrientationClass,\n type GrSliderMarks,\n type GrSliderModelValue,\n type GrSliderOrientation,\n type GrSliderSize,\n} from './grSliderStyles'\n\nexport type {\n GrSliderMarks,\n GrSliderModelValue,\n GrSliderOrientation,\n GrSliderSize,\n} from './grSliderStyles'\n\n/**\n * Публичный GR-примитив «Slider» (WAI-ARIA slider pattern).\n *\n * Поддерживает одиночное значение и диапазон (`range` — два бегунка), шаг, метки\n * делений, всплывающее значение (tooltip), полную клавиатурную навигацию\n * (стрелки / PageUp/Down / Home / End) и `role=\"slider\"` с\n * `aria-valuemin`/`max`/`now` на каждом бегунке.\n */\nexport interface GrSliderProps {\n /** `number` — одиночное значение; `[lo, hi]` — диапазон (при `range=true`). */\n modelValue: GrSliderModelValue\n min?: number\n max?: number\n step?: number\n /** Диапазон с двумя бегунками; модель — кортеж `[lo, hi]`. */\n range?: boolean\n disabled?: boolean\n /** Только для чтения: значение видно, но не меняется. */\n readonly?: boolean\n /** Визуальное и ARIA-состояние ошибки. */\n invalid?: boolean\n /** Обязательное поле (`aria-required`). */\n required?: boolean\n size?: GrSliderSize\n /** Метки делений: `{ [value]: label }` или массив значений. */\n marks?: GrSliderMarks\n /** Всплывающее значение над бегунком: `true`/`'hover'` — при hover/drag/focus, `'always'` — всегда. */\n showTooltip?: boolean | 'always' | 'hover'\n /** Форматирование значения в tooltip. Оно же уходит в `aria-valuetext`. */\n formatTooltip?: (value: number) => string\n /** Ориентация дорожки. В вертикальной минимум внизу. */\n orientation?: GrSliderOrientation\n /**\n * Эмитить `update:modelValue` только по завершении жеста: во время\n * перетаскивания значение ведёт сам слайдер. Клавиатура коммитит сразу —\n * нажатие клавиши дискретно, придерживать его нечего.\n */\n lazy?: boolean\n ariaLabel?: string\n /** Имя для нативной формы: hidden input на значение, при `range` — два с одним именем. */\n name?: string\n}\n\nexport interface GrSliderEmits {\n (e: 'update:modelValue', value: GrSliderModelValue): void\n /** Значение зафиксировано (отпущен бегунок / клавиша). */\n (e: 'change', value: GrSliderModelValue): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrSliderProps>(),\n {\n min: 0,\n max: 100,\n step: 1,\n range: false,\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n size: undefined,\n marks: undefined,\n showTooltip: false,\n formatTooltip: undefined,\n orientation: 'horizontal',\n lazy: false,\n ariaLabel: undefined,\n name: undefined,\n },\n)\n\nconst { t } = useGranularityTranslations()\n\nconst isVertical = computed(() => props.orientation === 'vertical')\n\n// Эффективный размер: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentSize(() => props.size, {\n component: 'GrSlider',\n supported: ['xs', 'sm', 'md', 'lg'],\n})\n\nconst emit = defineEmits<GrSliderEmits>()\n\n// Контекст `GrFormField`: id/aria-describedby/invalid/required как fallback.\nconst field = useGrFormFieldContext()\nconst resolvedId = computed(() => field?.id.value)\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n} = useGrFormControl(() => props)\nconst describedBy = computed(() => field?.describedById.value)\n\nconst trackEl = ref<HTMLElement | null>(null)\nconst rootEl = ref<HTMLElement | null>(null)\n\n// У диапазона два бегунка, и фокус ходит между ними: без границы переход с\n// одного на другой давал бы потребителю пару `blur` + `focus`.\nconst { onFocusIn, onFocusOut } = useFocusWithin(rootEl, {\n enter: event => emit('focus', event),\n leave: event => emit('blur', event),\n})\n\nconst thumbEls = ref<HTMLElement[]>([])\n\nfunction focus(): void {\n thumbEls.value[0]?.focus()\n}\n\nfunction blur(): void {\n thumbEls.value[0]?.blur()\n}\n\ndefineExpose({ focus, blur })\n\n// Нормализованные границы (гард от max<=min).\nconst span = computed(() => (props.max > props.min ? props.max - props.min : 1))\n\nconst stepDecimals = computed(() => {\n const s = String(props.step)\n const dot = s.indexOf('.')\n return dot >= 0 ? s.length - dot - 1 : 0\n})\n\nfunction clamp(value: number): number {\n return Math.min(props.max, Math.max(props.min, value))\n}\n\nfunction snap(value: number): number {\n const stepped = Math.round((value - props.min) / props.step) * props.step + props.min\n return Number(clamp(stepped).toFixed(stepDecimals.value))\n}\n\nfunction percent(value: number): number {\n return ((clamp(value) - props.min) / span.value) * 100\n}\n\n// Черновик жеста: при `lazy` значение до отпускания живёт здесь, а наружу не\n// уходит — иначе смысл режима теряется.\nconst draftValues = ref<number[] | null>(null)\n\n// Не доехавшее значение (промах вызова, асинхронный стор) увело бы в `NaN`\n// и смещение бегунка, и `aria-valuenow`; нижняя граница — честный старт.\nfunction toFiniteValue(value: number | undefined): number {\n return Number.isFinite(value) ? (value as number) : props.min\n}\n\n// ————— Значения бегунков как массив (single = один бегунок).\nconst modelValues = computed<number[]>(() => {\n if (props.range) {\n const v = Array.isArray(props.modelValue) ? props.modelValue : [props.min, props.min]\n return [snap(toFiniteValue(v[0])), snap(toFiniteValue(v[1]))]\n }\n const v = Array.isArray(props.modelValue) ? props.modelValue[0] : props.modelValue\n return [snap(toFiniteValue(v))]\n})\n\nif (__GR_DEV__) {\n watchEffect(() => {\n const missing = props.range\n ? !Array.isArray(props.modelValue)\n : !Number.isFinite(props.modelValue)\n\n if (missing) {\n console.warn(\n `[granularity] GrSlider: обязательный проп \\`modelValue\\` должен быть ${props.range ? 'парой чисел' : 'числом'} — получено ${String(props.modelValue)}.`,\n )\n }\n })\n}\n\nconst values = computed<number[]>(() => draftValues.value ?? modelValues.value)\n\n// `[touch-action:none]` обязателен: без него вертикальный свайп по бегунку\n// уходит в скролл страницы, браузер шлёт `pointercancel`, и слайдер пальцем\n// неуправляем. Утилиты `touch-none` нет ни в `presetMini`, ни в extra-rules —\n// поэтому arbitrary-значение.\nconst trackClass = computed(() => (isVertical.value\n ? `[touch-action:none] ${sliderTrackWidthBySize[resolvedSize.value]} ${sliderTrackVerticalLengthClass}`\n : `[touch-action:none] w-full ${sliderTrackHeightBySize[resolvedSize.value]}`))\n\n/** Смещение вдоль дорожки: по горизонтали слева, по вертикали снизу. */\nfunction offsetStyle(value: number): Record<string, string> {\n return isVertical.value ? { bottom: `${percent(value)}%` } : { left: `${percent(value)}%` }\n}\n\nconst fillStyle = computed(() => {\n const start = props.range\n ? Math.min(percent(values.value[0]), percent(values.value[1]))\n : 0\n const end = props.range\n ? Math.max(percent(values.value[0]), percent(values.value[1]))\n : percent(values.value[0])\n\n return isVertical.value\n ? { bottom: `${start}%`, top: `${100 - end}%` }\n : { left: `${start}%`, right: `${100 - end}%` }\n})\n\n// ————— Метки делений.\ntype SliderMark = { value: number, label: string }\nconst normalizedMarks = computed<SliderMark[]>(() => {\n if (!props.marks)\n return []\n if (Array.isArray(props.marks))\n return props.marks.map(v => ({ value: v, label: String(v) }))\n return Object.entries(props.marks).map(([value, label]) => ({ value: Number(value), label }))\n})\n\nfunction tooltipText(value: number): string {\n return props.formatTooltip ? props.formatTooltip(value) : String(value)\n}\n\n// ————— Обновление значения конкретного бегунка.\nfunction emitValue(next: number[], commit: boolean): void {\n const payload: GrSliderModelValue = props.range ? [next[0], next[1]] : next[0]\n emit('update:modelValue', payload)\n if (commit)\n emit('change', payload)\n}\n\nfunction setThumb(index: number, value: number, commit: boolean): void {\n const next = values.value.slice()\n next[index] = snap(value)\n\n // range: не даём бегункам «перепрыгнуть» друг друга.\n if (props.range) {\n if (index === 0)\n next[0] = Math.min(next[0], next[1])\n else next[1] = Math.max(next[1], next[0])\n }\n\n // `lazy` придерживает только непрерывный жест: `commit` приходит от клавиатуры\n // и от отпускания, и там значение уходит наружу как обычно.\n if (props.lazy && !commit) {\n draftValues.value = next\n return\n }\n\n draftValues.value = null\n emitValue(next, commit)\n}\n\n// ————— Pointer-драг.\nconst activeThumb = ref<number | null>(null)\nconst hoveredThumb = ref<number | null>(null)\n\nfunction valueFromPointer(event: { clientX: number, clientY: number }): number {\n const rect = trackEl.value?.getBoundingClientRect()\n if (!rect)\n return props.min\n\n // В вертикали минимум внизу, поэтому доля отсчитывается от нижнего края.\n const ratio = isVertical.value\n ? (rect.height === 0 ? 0 : 1 - (event.clientY - rect.top) / rect.height)\n : (rect.width === 0 ? 0 : (event.clientX - rect.left) / rect.width)\n\n return props.min + Math.min(1, Math.max(0, ratio)) * span.value\n}\n\n/**\n * Какой бегунок повезёт клик. При равенстве расстояний берём верхний: иначе\n * схлопнувшийся в точку диапазон не развести мышью — нижний бегунок упирается\n * в верхний и оба стоят на месте.\n */\nfunction nearestThumb(value: number): number {\n if (!props.range)\n return 0\n\n const [lo, hi] = values.value\n if (value < lo)\n return 0\n if (value > hi)\n return 1\n\n return Math.abs(value - lo) < Math.abs(value - hi) ? 0 : 1\n}\n\nfunction endDrag(commit: boolean): void {\n if (activeThumb.value !== null && commit) {\n // Финальный commit значения: у `lazy` он же и единственный `update:modelValue`.\n const next = values.value.slice()\n draftValues.value = null\n emitValue(next, true)\n }\n // Оборванный жест (`pointercancel`: браузер забрал указатель) значения не\n // коммитит — черновик просто отбрасывается, бегунок возвращается к модели.\n draftValues.value = null\n activeThumb.value = null\n}\n\nconst drag = useDragGesture({\n disabled: () => isDisabled.value || isReadonly.value,\n onStart: (event) => {\n const value = valueFromPointer(event)\n const index = nearestThumb(value)\n activeThumb.value = index\n // Фокусируем активный бегунок сразу — чтобы клавиатура работала после клика по\n // дорожке (div не получает фокус по клику автоматически, только через `.focus()`).\n thumbEls.value[index]?.focus()\n setThumb(index, value, false)\n },\n onMove: (event) => {\n if (activeThumb.value === null)\n return\n event.preventDefault()\n setThumb(activeThumb.value, valueFromPointer(event), false)\n },\n onEnd: () => endDrag(true),\n onCancel: () => endDrag(false),\n})\n\n// ————— Клавиатура.\nfunction keyboardBigStep(): number {\n // Границы передаются от нормализованного размаха, а не от сырых пропов:\n // `span` уже подставил единицу вместо вырожденного `max <= min`.\n return bigStep(props.step, props.min, props.min + span.value)\n}\n\nfunction onThumbKeydown(event: KeyboardEvent, index: number): void {\n if (isDisabled.value || isReadonly.value)\n return\n const current = values.value[index]\n let next: number | null = null\n\n switch (event.key) {\n case 'ArrowRight':\n case 'ArrowUp':\n next = current + props.step\n break\n case 'ArrowLeft':\n case 'ArrowDown':\n next = current - props.step\n break\n case 'PageUp':\n next = current + keyboardBigStep()\n break\n case 'PageDown':\n next = current - keyboardBigStep()\n break\n case 'Home':\n next = props.min\n break\n case 'End':\n next = props.max\n break\n default:\n return\n }\n\n event.preventDefault()\n setThumb(index, next, true)\n}\n\nfunction showTooltipFor(index: number): boolean {\n if (props.showTooltip === 'always')\n return true\n if (!props.showTooltip)\n return false\n return activeThumb.value === index || hoveredThumb.value === index\n}\n\nfunction thumbAriaLabel(index: number): string | undefined {\n if (!props.ariaLabel)\n return undefined\n if (!props.range)\n return props.ariaLabel\n\n const bound = index === 0 ? t('gr.slider.min', 'min') : t('gr.slider.max', 'max')\n return `${props.ariaLabel} (${bound})`\n}\n\n// `aria-valuetext` нужен, только когда число само по себе не читается: «1200»\n// против «$1 200». Без своего формата атрибут не добавляем.\nfunction thumbValueText(value: number): string | undefined {\n return props.formatTooltip ? props.formatTooltip(value) : undefined\n}\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n data-gr-slider\n :data-orientation=\"orientation\"\n :class=\"sliderRootClass({ size: resolvedSize, disabled: isDisabled, hasMarks: normalizedMarks.length > 0, orientation })\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n >\n <!-- Нативная форма: роль-виджет не labelable и в submit не попадает.\n Сериализуется модель (snapped), а не черновик жеста. -->\n <template v-if=\"name\">\n <input\n v-for=\"(value, index) in modelValues\"\n :key=\"index\"\n type=\"hidden\"\n :name=\"name\"\n :value=\"String(value)\"\n >\n </template>\n <div\n ref=\"trackEl\"\n data-gr-slider-track\n class=\"relative\"\n :class=\"trackClass\"\n @pointerdown=\"drag.start\"\n >\n <div :class=\"sliderRailClass\" />\n <div\n data-gr-slider-fill\n :class=\"[sliderFillClass(isDisabled), sliderFillOrientationClass[orientation]]\"\n :style=\"fillStyle\"\n />\n\n <!-- Метки делений. -->\n <template v-for=\"mark in normalizedMarks\" :key=\"mark.value\">\n <span\n data-gr-slider-mark\n :class=\"sliderMarkTickClass(orientation)\"\n :style=\"offsetStyle(mark.value)\"\n aria-hidden=\"true\"\n />\n <!-- Подпись метки дублирует значение, которое диктор читает с бегунка:\n как отдельный текст внутри слайдера она только мешает. -->\n <span\n data-gr-slider-mark-label\n :class=\"sliderMarkLabelClassFor(percent(mark.value), { orientation, disabled: isDisabled })\"\n :style=\"offsetStyle(mark.value)\"\n aria-hidden=\"true\"\n >\n {{ mark.label }}\n </span>\n </template>\n\n <!-- Бегунки. -->\n <div\n v-for=\"(value, index) in values\"\n :id=\"index === 0 ? resolvedId : undefined\"\n :key=\"index\"\n :ref=\"(el) => { if (el) thumbEls[index] = el as HTMLElement }\"\n data-gr-slider-thumb\n :data-testid=\"`gr-slider-thumb-${index}`\"\n :class=\"sliderThumbClass({ size: resolvedSize, disabled: isDisabled, orientation })\"\n :style=\"offsetStyle(value)\"\n role=\"slider\"\n :tabindex=\"isDisabled ? -1 : 0\"\n :aria-valuemin=\"range && index === 1 ? values[0] : min\"\n :aria-valuemax=\"range && index === 0 ? values[1] : max\"\n :aria-valuenow=\"value\"\n :aria-valuetext=\"thumbValueText(value)\"\n :aria-orientation=\"orientation\"\n :aria-label=\"thumbAriaLabel(index)\"\n :aria-disabled=\"isDisabled ? 'true' : undefined\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-describedby=\"index === 0 ? describedBy : undefined\"\n :aria-required=\"isRequired && index === 0 ? 'true' : undefined\"\n :aria-readonly=\"isReadonly ? 'true' : undefined\"\n @keydown=\"onThumbKeydown($event, index)\"\n @mouseenter=\"hoveredThumb = index\"\n @mouseleave=\"hoveredThumb = null\"\n @focus=\"hoveredThumb = index\"\n @blur=\"hoveredThumb = null\"\n >\n <span\n v-if=\"showTooltipFor(index)\"\n data-gr-slider-tooltip\n :class=\"sliderTooltipClass(orientation)\"\n >\n {{ tooltipText(value) }}\n </span>\n </div>\n </div>\n </div>\n</template>\n"],"mappings":"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"}
1
+ {"version":3,"file":"GrSlider-CO8N9SF6.js","names":[],"sources":["../../../../src/components/GrSlider/GrSlider.vue","../../../../src/components/GrSlider/GrSlider.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref, watchEffect } from 'vue'\n\nimport { useGrComponentSize } from '../GrConfigProvider/context'\n\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { bigStep } from '../shared/numericStep'\nimport { useDragGesture } from '../../composables/useDragGesture'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useFocusWithin } from '../../composables/internal/useFocusWithin'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n\nimport {\n sliderFillClass,\n sliderMarkLabelClassFor,\n sliderMarkTickClass,\n sliderRailClass,\n sliderRootClass,\n sliderThumbClass,\n sliderTooltipClass,\n sliderTrackHeightBySize,\n sliderTrackVerticalLengthClass,\n sliderTrackWidthBySize,\n sliderFillOrientationClass,\n type GrSliderMarks,\n type GrSliderModelValue,\n type GrSliderOrientation,\n type GrSliderSize,\n} from './grSliderStyles'\n\nexport type {\n GrSliderMarks,\n GrSliderModelValue,\n GrSliderOrientation,\n GrSliderSize,\n} from './grSliderStyles'\n\n/**\n * Публичный GR-примитив «Slider» (WAI-ARIA slider pattern).\n *\n * Поддерживает одиночное значение и диапазон (`range` — два бегунка), шаг, метки\n * делений, всплывающее значение (tooltip), полную клавиатурную навигацию\n * (стрелки / PageUp/Down / Home / End) и `role=\"slider\"` с\n * `aria-valuemin`/`max`/`now` на каждом бегунке.\n */\nexport interface GrSliderProps {\n /** `number` — одиночное значение; `[lo, hi]` — диапазон (при `range=true`). */\n modelValue: GrSliderModelValue\n min?: number\n max?: number\n step?: number\n /** Диапазон с двумя бегунками; модель — кортеж `[lo, hi]`. */\n range?: boolean\n disabled?: boolean\n /** Только для чтения: значение видно, но не меняется. */\n readonly?: boolean\n /** Визуальное и ARIA-состояние ошибки. */\n invalid?: boolean\n /** Обязательное поле (`aria-required`). */\n required?: boolean\n size?: GrSliderSize\n /** Метки делений: `{ [value]: label }` или массив значений. */\n marks?: GrSliderMarks\n /** Всплывающее значение над бегунком: `true`/`'hover'` — при hover/drag/focus, `'always'` — всегда. */\n showTooltip?: boolean | 'always' | 'hover'\n /** Форматирование значения в tooltip. Оно же уходит в `aria-valuetext`. */\n formatTooltip?: (value: number) => string\n /** Ориентация дорожки. В вертикальной минимум внизу. */\n orientation?: GrSliderOrientation\n /**\n * Эмитить `update:modelValue` только по завершении жеста: во время\n * перетаскивания значение ведёт сам слайдер. Клавиатура коммитит сразу —\n * нажатие клавиши дискретно, придерживать его нечего.\n */\n lazy?: boolean\n ariaLabel?: string\n /** Имя для нативной формы: hidden input на значение, при `range` — два с одним именем. */\n name?: string\n}\n\nexport interface GrSliderEmits {\n (e: 'update:modelValue', value: GrSliderModelValue): void\n /** Значение зафиксировано (отпущен бегунок / клавиша). */\n (e: 'change', value: GrSliderModelValue): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrSliderProps>(),\n {\n min: 0,\n max: 100,\n step: 1,\n range: false,\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n size: undefined,\n marks: undefined,\n showTooltip: false,\n formatTooltip: undefined,\n orientation: 'horizontal',\n lazy: false,\n ariaLabel: undefined,\n name: undefined,\n },\n)\n\nconst { t } = useGranularityTranslations()\n\nconst isVertical = computed(() => props.orientation === 'vertical')\n\n// Эффективный размер: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentSize(() => props.size, {\n component: 'GrSlider',\n supported: ['xs', 'sm', 'md', 'lg'],\n})\n\nconst emit = defineEmits<GrSliderEmits>()\n\n// Контекст `GrFormField`: id/aria-describedby/invalid/required как fallback.\nconst field = useGrFormFieldContext()\nconst resolvedId = computed(() => field?.id.value)\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n} = useGrFormControl(() => props)\nconst describedBy = computed(() => field?.describedById.value)\n\nconst trackEl = ref<HTMLElement | null>(null)\nconst rootEl = ref<HTMLElement | null>(null)\n\n// У диапазона два бегунка, и фокус ходит между ними: без границы переход с\n// одного на другой давал бы потребителю пару `blur` + `focus`.\nconst { onFocusIn, onFocusOut } = useFocusWithin(rootEl, {\n enter: event => emit('focus', event),\n leave: event => emit('blur', event),\n})\n\nconst thumbEls = ref<HTMLElement[]>([])\n\nfunction focus(): void {\n thumbEls.value[0]?.focus()\n}\n\nfunction blur(): void {\n thumbEls.value[0]?.blur()\n}\n\ndefineExpose({ focus, blur })\n\n// Нормализованные границы (гард от max<=min).\nconst span = computed(() => (props.max > props.min ? props.max - props.min : 1))\n\nconst stepDecimals = computed(() => {\n const s = String(props.step)\n const dot = s.indexOf('.')\n return dot >= 0 ? s.length - dot - 1 : 0\n})\n\nfunction clamp(value: number): number {\n return Math.min(props.max, Math.max(props.min, value))\n}\n\nfunction snap(value: number): number {\n const stepped = Math.round((value - props.min) / props.step) * props.step + props.min\n return Number(clamp(stepped).toFixed(stepDecimals.value))\n}\n\nfunction percent(value: number): number {\n return ((clamp(value) - props.min) / span.value) * 100\n}\n\n// Черновик жеста: при `lazy` значение до отпускания живёт здесь, а наружу не\n// уходит — иначе смысл режима теряется.\nconst draftValues = ref<number[] | null>(null)\n\n// Не доехавшее значение (промах вызова, асинхронный стор) увело бы в `NaN`\n// и смещение бегунка, и `aria-valuenow`; нижняя граница — честный старт.\nfunction toFiniteValue(value: number | undefined): number {\n return Number.isFinite(value) ? (value as number) : props.min\n}\n\n// ————— Значения бегунков как массив (single = один бегунок).\nconst modelValues = computed<number[]>(() => {\n if (props.range) {\n const v = Array.isArray(props.modelValue) ? props.modelValue : [props.min, props.min]\n return [snap(toFiniteValue(v[0])), snap(toFiniteValue(v[1]))]\n }\n const v = Array.isArray(props.modelValue) ? props.modelValue[0] : props.modelValue\n return [snap(toFiniteValue(v))]\n})\n\nif (__GR_DEV__) {\n watchEffect(() => {\n const missing = props.range\n ? !Array.isArray(props.modelValue)\n : !Number.isFinite(props.modelValue)\n\n if (missing) {\n console.warn(\n `[granularity] GrSlider: обязательный проп \\`modelValue\\` должен быть ${props.range ? 'парой чисел' : 'числом'} — получено ${String(props.modelValue)}.`,\n )\n }\n })\n}\n\nconst values = computed<number[]>(() => draftValues.value ?? modelValues.value)\n\n// `[touch-action:none]` обязателен: без него вертикальный свайп по бегунку\n// уходит в скролл страницы, браузер шлёт `pointercancel`, и слайдер пальцем\n// неуправляем. Утилиты `touch-none` нет ни в `presetMini`, ни в extra-rules —\n// поэтому arbitrary-значение.\nconst trackClass = computed(() => (isVertical.value\n ? `[touch-action:none] ${sliderTrackWidthBySize[resolvedSize.value]} ${sliderTrackVerticalLengthClass}`\n : `[touch-action:none] w-full ${sliderTrackHeightBySize[resolvedSize.value]}`))\n\n/** Смещение вдоль дорожки: по горизонтали слева, по вертикали снизу. */\nfunction offsetStyle(value: number): Record<string, string> {\n return isVertical.value ? { bottom: `${percent(value)}%` } : { left: `${percent(value)}%` }\n}\n\nconst fillStyle = computed(() => {\n const start = props.range\n ? Math.min(percent(values.value[0]), percent(values.value[1]))\n : 0\n const end = props.range\n ? Math.max(percent(values.value[0]), percent(values.value[1]))\n : percent(values.value[0])\n\n return isVertical.value\n ? { bottom: `${start}%`, top: `${100 - end}%` }\n : { left: `${start}%`, right: `${100 - end}%` }\n})\n\n// ————— Метки делений.\ntype SliderMark = { value: number, label: string }\nconst normalizedMarks = computed<SliderMark[]>(() => {\n if (!props.marks)\n return []\n if (Array.isArray(props.marks))\n return props.marks.map(v => ({ value: v, label: String(v) }))\n return Object.entries(props.marks).map(([value, label]) => ({ value: Number(value), label }))\n})\n\nfunction tooltipText(value: number): string {\n return props.formatTooltip ? props.formatTooltip(value) : String(value)\n}\n\n// ————— Обновление значения конкретного бегунка.\nfunction emitValue(next: number[], commit: boolean): void {\n const payload: GrSliderModelValue = props.range ? [next[0], next[1]] : next[0]\n emit('update:modelValue', payload)\n if (commit)\n emit('change', payload)\n}\n\nfunction setThumb(index: number, value: number, commit: boolean): void {\n const next = values.value.slice()\n next[index] = snap(value)\n\n // range: не даём бегункам «перепрыгнуть» друг друга.\n if (props.range) {\n if (index === 0)\n next[0] = Math.min(next[0], next[1])\n else next[1] = Math.max(next[1], next[0])\n }\n\n // `lazy` придерживает только непрерывный жест: `commit` приходит от клавиатуры\n // и от отпускания, и там значение уходит наружу как обычно.\n if (props.lazy && !commit) {\n draftValues.value = next\n return\n }\n\n draftValues.value = null\n emitValue(next, commit)\n}\n\n// ————— Pointer-драг.\nconst activeThumb = ref<number | null>(null)\nconst hoveredThumb = ref<number | null>(null)\n\nfunction valueFromPointer(event: { clientX: number, clientY: number }): number {\n const rect = trackEl.value?.getBoundingClientRect()\n if (!rect)\n return props.min\n\n // В вертикали минимум внизу, поэтому доля отсчитывается от нижнего края.\n const ratio = isVertical.value\n ? (rect.height === 0 ? 0 : 1 - (event.clientY - rect.top) / rect.height)\n : (rect.width === 0 ? 0 : (event.clientX - rect.left) / rect.width)\n\n return props.min + Math.min(1, Math.max(0, ratio)) * span.value\n}\n\n/**\n * Какой бегунок повезёт клик. При равенстве расстояний берём верхний: иначе\n * схлопнувшийся в точку диапазон не развести мышью — нижний бегунок упирается\n * в верхний и оба стоят на месте.\n */\nfunction nearestThumb(value: number): number {\n if (!props.range)\n return 0\n\n const [lo, hi] = values.value\n if (value < lo)\n return 0\n if (value > hi)\n return 1\n\n return Math.abs(value - lo) < Math.abs(value - hi) ? 0 : 1\n}\n\nfunction endDrag(commit: boolean): void {\n if (activeThumb.value !== null && commit) {\n // Финальный commit значения: у `lazy` он же и единственный `update:modelValue`.\n const next = values.value.slice()\n draftValues.value = null\n emitValue(next, true)\n }\n // Оборванный жест (`pointercancel`: браузер забрал указатель) значения не\n // коммитит — черновик просто отбрасывается, бегунок возвращается к модели.\n draftValues.value = null\n activeThumb.value = null\n}\n\nconst drag = useDragGesture({\n disabled: () => isDisabled.value || isReadonly.value,\n onStart: (event) => {\n const value = valueFromPointer(event)\n const index = nearestThumb(value)\n activeThumb.value = index\n // Фокусируем активный бегунок сразу — чтобы клавиатура работала после клика по\n // дорожке (div не получает фокус по клику автоматически, только через `.focus()`).\n thumbEls.value[index]?.focus()\n setThumb(index, value, false)\n },\n onMove: (event) => {\n if (activeThumb.value === null)\n return\n event.preventDefault()\n setThumb(activeThumb.value, valueFromPointer(event), false)\n },\n onEnd: () => endDrag(true),\n onCancel: () => endDrag(false),\n})\n\n// ————— Клавиатура.\nfunction keyboardBigStep(): number {\n // Границы передаются от нормализованного размаха, а не от сырых пропов:\n // `span` уже подставил единицу вместо вырожденного `max <= min`.\n return bigStep(props.step, props.min, props.min + span.value)\n}\n\nfunction onThumbKeydown(event: KeyboardEvent, index: number): void {\n if (isDisabled.value || isReadonly.value)\n return\n const current = values.value[index]\n let next: number | null = null\n\n switch (event.key) {\n case 'ArrowRight':\n case 'ArrowUp':\n next = current + props.step\n break\n case 'ArrowLeft':\n case 'ArrowDown':\n next = current - props.step\n break\n case 'PageUp':\n next = current + keyboardBigStep()\n break\n case 'PageDown':\n next = current - keyboardBigStep()\n break\n case 'Home':\n next = props.min\n break\n case 'End':\n next = props.max\n break\n default:\n return\n }\n\n event.preventDefault()\n setThumb(index, next, true)\n}\n\nfunction showTooltipFor(index: number): boolean {\n if (props.showTooltip === 'always')\n return true\n if (!props.showTooltip)\n return false\n return activeThumb.value === index || hoveredThumb.value === index\n}\n\nfunction thumbAriaLabel(index: number): string | undefined {\n if (!props.ariaLabel)\n return undefined\n if (!props.range)\n return props.ariaLabel\n\n const bound = index === 0 ? t('gr.slider.min', 'min') : t('gr.slider.max', 'max')\n return `${props.ariaLabel} (${bound})`\n}\n\n// `aria-valuetext` нужен, только когда число само по себе не читается: «1200»\n// против «$1 200». Без своего формата атрибут не добавляем.\nfunction thumbValueText(value: number): string | undefined {\n return props.formatTooltip ? props.formatTooltip(value) : undefined\n}\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n data-gr-slider\n :data-orientation=\"orientation\"\n :class=\"sliderRootClass({ size: resolvedSize, disabled: isDisabled, hasMarks: normalizedMarks.length > 0, orientation })\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n >\n <!-- Нативная форма: роль-виджет не labelable и в submit не попадает.\n Сериализуется модель (snapped), а не черновик жеста. -->\n <template v-if=\"name\">\n <input\n v-for=\"(value, index) in modelValues\"\n :key=\"index\"\n type=\"hidden\"\n :name=\"name\"\n :value=\"String(value)\"\n >\n </template>\n <div\n ref=\"trackEl\"\n data-gr-slider-track\n class=\"relative\"\n :class=\"trackClass\"\n @pointerdown=\"drag.start\"\n >\n <div :class=\"sliderRailClass\" />\n <div\n data-gr-slider-fill\n :class=\"[sliderFillClass(isDisabled), sliderFillOrientationClass[orientation]]\"\n :style=\"fillStyle\"\n />\n\n <!-- Метки делений. -->\n <template v-for=\"mark in normalizedMarks\" :key=\"mark.value\">\n <span\n data-gr-slider-mark\n :class=\"sliderMarkTickClass(orientation)\"\n :style=\"offsetStyle(mark.value)\"\n aria-hidden=\"true\"\n />\n <!-- Подпись метки дублирует значение, которое диктор читает с бегунка:\n как отдельный текст внутри слайдера она только мешает. -->\n <span\n data-gr-slider-mark-label\n :class=\"sliderMarkLabelClassFor(percent(mark.value), { orientation, disabled: isDisabled })\"\n :style=\"offsetStyle(mark.value)\"\n aria-hidden=\"true\"\n >\n {{ mark.label }}\n </span>\n </template>\n\n <!-- Бегунки. -->\n <div\n v-for=\"(value, index) in values\"\n :id=\"index === 0 ? resolvedId : undefined\"\n :key=\"index\"\n :ref=\"(el) => { if (el) thumbEls[index] = el as HTMLElement }\"\n data-gr-slider-thumb\n :data-testid=\"`gr-slider-thumb-${index}`\"\n :class=\"sliderThumbClass({ size: resolvedSize, disabled: isDisabled, orientation })\"\n :style=\"offsetStyle(value)\"\n role=\"slider\"\n :tabindex=\"isDisabled ? -1 : 0\"\n :aria-valuemin=\"range && index === 1 ? values[0] : min\"\n :aria-valuemax=\"range && index === 0 ? values[1] : max\"\n :aria-valuenow=\"value\"\n :aria-valuetext=\"thumbValueText(value)\"\n :aria-orientation=\"orientation\"\n :aria-label=\"thumbAriaLabel(index)\"\n :aria-disabled=\"isDisabled ? 'true' : undefined\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-describedby=\"index === 0 ? describedBy : undefined\"\n :aria-required=\"isRequired && index === 0 ? 'true' : undefined\"\n :aria-readonly=\"isReadonly ? 'true' : undefined\"\n @keydown=\"onThumbKeydown($event, index)\"\n @mouseenter=\"hoveredThumb = index\"\n @mouseleave=\"hoveredThumb = null\"\n @focus=\"hoveredThumb = index\"\n @blur=\"hoveredThumb = null\"\n >\n <span\n v-if=\"showTooltipFor(index)\"\n data-gr-slider-tooltip\n :class=\"sliderTooltipClass(orientation)\"\n >\n {{ tooltipText(value) }}\n </span>\n </div>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, ref, watchEffect } from 'vue'\n\nimport { useGrComponentSize } from '../GrConfigProvider/context'\n\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { bigStep } from '../shared/numericStep'\nimport { useDragGesture } from '../../composables/useDragGesture'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useFocusWithin } from '../../composables/internal/useFocusWithin'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n\nimport {\n sliderFillClass,\n sliderMarkLabelClassFor,\n sliderMarkTickClass,\n sliderRailClass,\n sliderRootClass,\n sliderThumbClass,\n sliderTooltipClass,\n sliderTrackHeightBySize,\n sliderTrackVerticalLengthClass,\n sliderTrackWidthBySize,\n sliderFillOrientationClass,\n type GrSliderMarks,\n type GrSliderModelValue,\n type GrSliderOrientation,\n type GrSliderSize,\n} from './grSliderStyles'\n\nexport type {\n GrSliderMarks,\n GrSliderModelValue,\n GrSliderOrientation,\n GrSliderSize,\n} from './grSliderStyles'\n\n/**\n * Публичный GR-примитив «Slider» (WAI-ARIA slider pattern).\n *\n * Поддерживает одиночное значение и диапазон (`range` — два бегунка), шаг, метки\n * делений, всплывающее значение (tooltip), полную клавиатурную навигацию\n * (стрелки / PageUp/Down / Home / End) и `role=\"slider\"` с\n * `aria-valuemin`/`max`/`now` на каждом бегунке.\n */\nexport interface GrSliderProps {\n /** `number` — одиночное значение; `[lo, hi]` — диапазон (при `range=true`). */\n modelValue: GrSliderModelValue\n min?: number\n max?: number\n step?: number\n /** Диапазон с двумя бегунками; модель — кортеж `[lo, hi]`. */\n range?: boolean\n disabled?: boolean\n /** Только для чтения: значение видно, но не меняется. */\n readonly?: boolean\n /** Визуальное и ARIA-состояние ошибки. */\n invalid?: boolean\n /** Обязательное поле (`aria-required`). */\n required?: boolean\n size?: GrSliderSize\n /** Метки делений: `{ [value]: label }` или массив значений. */\n marks?: GrSliderMarks\n /** Всплывающее значение над бегунком: `true`/`'hover'` — при hover/drag/focus, `'always'` — всегда. */\n showTooltip?: boolean | 'always' | 'hover'\n /** Форматирование значения в tooltip. Оно же уходит в `aria-valuetext`. */\n formatTooltip?: (value: number) => string\n /** Ориентация дорожки. В вертикальной минимум внизу. */\n orientation?: GrSliderOrientation\n /**\n * Эмитить `update:modelValue` только по завершении жеста: во время\n * перетаскивания значение ведёт сам слайдер. Клавиатура коммитит сразу —\n * нажатие клавиши дискретно, придерживать его нечего.\n */\n lazy?: boolean\n ariaLabel?: string\n /** Имя для нативной формы: hidden input на значение, при `range` — два с одним именем. */\n name?: string\n}\n\nexport interface GrSliderEmits {\n (e: 'update:modelValue', value: GrSliderModelValue): void\n /** Значение зафиксировано (отпущен бегунок / клавиша). */\n (e: 'change', value: GrSliderModelValue): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrSliderProps>(),\n {\n min: 0,\n max: 100,\n step: 1,\n range: false,\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n size: undefined,\n marks: undefined,\n showTooltip: false,\n formatTooltip: undefined,\n orientation: 'horizontal',\n lazy: false,\n ariaLabel: undefined,\n name: undefined,\n },\n)\n\nconst { t } = useGranularityTranslations()\n\nconst isVertical = computed(() => props.orientation === 'vertical')\n\n// Эффективный размер: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentSize(() => props.size, {\n component: 'GrSlider',\n supported: ['xs', 'sm', 'md', 'lg'],\n})\n\nconst emit = defineEmits<GrSliderEmits>()\n\n// Контекст `GrFormField`: id/aria-describedby/invalid/required как fallback.\nconst field = useGrFormFieldContext()\nconst resolvedId = computed(() => field?.id.value)\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n} = useGrFormControl(() => props)\nconst describedBy = computed(() => field?.describedById.value)\n\nconst trackEl = ref<HTMLElement | null>(null)\nconst rootEl = ref<HTMLElement | null>(null)\n\n// У диапазона два бегунка, и фокус ходит между ними: без границы переход с\n// одного на другой давал бы потребителю пару `blur` + `focus`.\nconst { onFocusIn, onFocusOut } = useFocusWithin(rootEl, {\n enter: event => emit('focus', event),\n leave: event => emit('blur', event),\n})\n\nconst thumbEls = ref<HTMLElement[]>([])\n\nfunction focus(): void {\n thumbEls.value[0]?.focus()\n}\n\nfunction blur(): void {\n thumbEls.value[0]?.blur()\n}\n\ndefineExpose({ focus, blur })\n\n// Нормализованные границы (гард от max<=min).\nconst span = computed(() => (props.max > props.min ? props.max - props.min : 1))\n\nconst stepDecimals = computed(() => {\n const s = String(props.step)\n const dot = s.indexOf('.')\n return dot >= 0 ? s.length - dot - 1 : 0\n})\n\nfunction clamp(value: number): number {\n return Math.min(props.max, Math.max(props.min, value))\n}\n\nfunction snap(value: number): number {\n const stepped = Math.round((value - props.min) / props.step) * props.step + props.min\n return Number(clamp(stepped).toFixed(stepDecimals.value))\n}\n\nfunction percent(value: number): number {\n return ((clamp(value) - props.min) / span.value) * 100\n}\n\n// Черновик жеста: при `lazy` значение до отпускания живёт здесь, а наружу не\n// уходит — иначе смысл режима теряется.\nconst draftValues = ref<number[] | null>(null)\n\n// Не доехавшее значение (промах вызова, асинхронный стор) увело бы в `NaN`\n// и смещение бегунка, и `aria-valuenow`; нижняя граница — честный старт.\nfunction toFiniteValue(value: number | undefined): number {\n return Number.isFinite(value) ? (value as number) : props.min\n}\n\n// ————— Значения бегунков как массив (single = один бегунок).\nconst modelValues = computed<number[]>(() => {\n if (props.range) {\n const v = Array.isArray(props.modelValue) ? props.modelValue : [props.min, props.min]\n return [snap(toFiniteValue(v[0])), snap(toFiniteValue(v[1]))]\n }\n const v = Array.isArray(props.modelValue) ? props.modelValue[0] : props.modelValue\n return [snap(toFiniteValue(v))]\n})\n\nif (__GR_DEV__) {\n watchEffect(() => {\n const missing = props.range\n ? !Array.isArray(props.modelValue)\n : !Number.isFinite(props.modelValue)\n\n if (missing) {\n console.warn(\n `[granularity] GrSlider: обязательный проп \\`modelValue\\` должен быть ${props.range ? 'парой чисел' : 'числом'} — получено ${String(props.modelValue)}.`,\n )\n }\n })\n}\n\nconst values = computed<number[]>(() => draftValues.value ?? modelValues.value)\n\n// `[touch-action:none]` обязателен: без него вертикальный свайп по бегунку\n// уходит в скролл страницы, браузер шлёт `pointercancel`, и слайдер пальцем\n// неуправляем. Утилиты `touch-none` нет ни в `presetMini`, ни в extra-rules —\n// поэтому arbitrary-значение.\nconst trackClass = computed(() => (isVertical.value\n ? `[touch-action:none] ${sliderTrackWidthBySize[resolvedSize.value]} ${sliderTrackVerticalLengthClass}`\n : `[touch-action:none] w-full ${sliderTrackHeightBySize[resolvedSize.value]}`))\n\n/** Смещение вдоль дорожки: по горизонтали слева, по вертикали снизу. */\nfunction offsetStyle(value: number): Record<string, string> {\n return isVertical.value ? { bottom: `${percent(value)}%` } : { left: `${percent(value)}%` }\n}\n\nconst fillStyle = computed(() => {\n const start = props.range\n ? Math.min(percent(values.value[0]), percent(values.value[1]))\n : 0\n const end = props.range\n ? Math.max(percent(values.value[0]), percent(values.value[1]))\n : percent(values.value[0])\n\n return isVertical.value\n ? { bottom: `${start}%`, top: `${100 - end}%` }\n : { left: `${start}%`, right: `${100 - end}%` }\n})\n\n// ————— Метки делений.\ntype SliderMark = { value: number, label: string }\nconst normalizedMarks = computed<SliderMark[]>(() => {\n if (!props.marks)\n return []\n if (Array.isArray(props.marks))\n return props.marks.map(v => ({ value: v, label: String(v) }))\n return Object.entries(props.marks).map(([value, label]) => ({ value: Number(value), label }))\n})\n\nfunction tooltipText(value: number): string {\n return props.formatTooltip ? props.formatTooltip(value) : String(value)\n}\n\n// ————— Обновление значения конкретного бегунка.\nfunction emitValue(next: number[], commit: boolean): void {\n const payload: GrSliderModelValue = props.range ? [next[0], next[1]] : next[0]\n emit('update:modelValue', payload)\n if (commit)\n emit('change', payload)\n}\n\nfunction setThumb(index: number, value: number, commit: boolean): void {\n const next = values.value.slice()\n next[index] = snap(value)\n\n // range: не даём бегункам «перепрыгнуть» друг друга.\n if (props.range) {\n if (index === 0)\n next[0] = Math.min(next[0], next[1])\n else next[1] = Math.max(next[1], next[0])\n }\n\n // `lazy` придерживает только непрерывный жест: `commit` приходит от клавиатуры\n // и от отпускания, и там значение уходит наружу как обычно.\n if (props.lazy && !commit) {\n draftValues.value = next\n return\n }\n\n draftValues.value = null\n emitValue(next, commit)\n}\n\n// ————— Pointer-драг.\nconst activeThumb = ref<number | null>(null)\nconst hoveredThumb = ref<number | null>(null)\n\nfunction valueFromPointer(event: { clientX: number, clientY: number }): number {\n const rect = trackEl.value?.getBoundingClientRect()\n if (!rect)\n return props.min\n\n // В вертикали минимум внизу, поэтому доля отсчитывается от нижнего края.\n const ratio = isVertical.value\n ? (rect.height === 0 ? 0 : 1 - (event.clientY - rect.top) / rect.height)\n : (rect.width === 0 ? 0 : (event.clientX - rect.left) / rect.width)\n\n return props.min + Math.min(1, Math.max(0, ratio)) * span.value\n}\n\n/**\n * Какой бегунок повезёт клик. При равенстве расстояний берём верхний: иначе\n * схлопнувшийся в точку диапазон не развести мышью — нижний бегунок упирается\n * в верхний и оба стоят на месте.\n */\nfunction nearestThumb(value: number): number {\n if (!props.range)\n return 0\n\n const [lo, hi] = values.value\n if (value < lo)\n return 0\n if (value > hi)\n return 1\n\n return Math.abs(value - lo) < Math.abs(value - hi) ? 0 : 1\n}\n\nfunction endDrag(commit: boolean): void {\n if (activeThumb.value !== null && commit) {\n // Финальный commit значения: у `lazy` он же и единственный `update:modelValue`.\n const next = values.value.slice()\n draftValues.value = null\n emitValue(next, true)\n }\n // Оборванный жест (`pointercancel`: браузер забрал указатель) значения не\n // коммитит — черновик просто отбрасывается, бегунок возвращается к модели.\n draftValues.value = null\n activeThumb.value = null\n}\n\nconst drag = useDragGesture({\n disabled: () => isDisabled.value || isReadonly.value,\n onStart: (event) => {\n const value = valueFromPointer(event)\n const index = nearestThumb(value)\n activeThumb.value = index\n // Фокусируем активный бегунок сразу — чтобы клавиатура работала после клика по\n // дорожке (div не получает фокус по клику автоматически, только через `.focus()`).\n thumbEls.value[index]?.focus()\n setThumb(index, value, false)\n },\n onMove: (event) => {\n if (activeThumb.value === null)\n return\n event.preventDefault()\n setThumb(activeThumb.value, valueFromPointer(event), false)\n },\n onEnd: () => endDrag(true),\n onCancel: () => endDrag(false),\n})\n\n// ————— Клавиатура.\nfunction keyboardBigStep(): number {\n // Границы передаются от нормализованного размаха, а не от сырых пропов:\n // `span` уже подставил единицу вместо вырожденного `max <= min`.\n return bigStep(props.step, props.min, props.min + span.value)\n}\n\nfunction onThumbKeydown(event: KeyboardEvent, index: number): void {\n if (isDisabled.value || isReadonly.value)\n return\n const current = values.value[index]\n let next: number | null = null\n\n switch (event.key) {\n case 'ArrowRight':\n case 'ArrowUp':\n next = current + props.step\n break\n case 'ArrowLeft':\n case 'ArrowDown':\n next = current - props.step\n break\n case 'PageUp':\n next = current + keyboardBigStep()\n break\n case 'PageDown':\n next = current - keyboardBigStep()\n break\n case 'Home':\n next = props.min\n break\n case 'End':\n next = props.max\n break\n default:\n return\n }\n\n event.preventDefault()\n setThumb(index, next, true)\n}\n\nfunction showTooltipFor(index: number): boolean {\n if (props.showTooltip === 'always')\n return true\n if (!props.showTooltip)\n return false\n return activeThumb.value === index || hoveredThumb.value === index\n}\n\nfunction thumbAriaLabel(index: number): string | undefined {\n if (!props.ariaLabel)\n return undefined\n if (!props.range)\n return props.ariaLabel\n\n const bound = index === 0 ? t('gr.slider.min', 'min') : t('gr.slider.max', 'max')\n return `${props.ariaLabel} (${bound})`\n}\n\n// `aria-valuetext` нужен, только когда число само по себе не читается: «1200»\n// против «$1 200». Без своего формата атрибут не добавляем.\nfunction thumbValueText(value: number): string | undefined {\n return props.formatTooltip ? props.formatTooltip(value) : undefined\n}\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n data-gr-slider\n :data-orientation=\"orientation\"\n :class=\"sliderRootClass({ size: resolvedSize, disabled: isDisabled, hasMarks: normalizedMarks.length > 0, orientation })\"\n @focusin=\"onFocusIn\"\n @focusout=\"onFocusOut\"\n >\n <!-- Нативная форма: роль-виджет не labelable и в submit не попадает.\n Сериализуется модель (snapped), а не черновик жеста. -->\n <template v-if=\"name\">\n <input\n v-for=\"(value, index) in modelValues\"\n :key=\"index\"\n type=\"hidden\"\n :name=\"name\"\n :value=\"String(value)\"\n >\n </template>\n <div\n ref=\"trackEl\"\n data-gr-slider-track\n class=\"relative\"\n :class=\"trackClass\"\n @pointerdown=\"drag.start\"\n >\n <div :class=\"sliderRailClass\" />\n <div\n data-gr-slider-fill\n :class=\"[sliderFillClass(isDisabled), sliderFillOrientationClass[orientation]]\"\n :style=\"fillStyle\"\n />\n\n <!-- Метки делений. -->\n <template v-for=\"mark in normalizedMarks\" :key=\"mark.value\">\n <span\n data-gr-slider-mark\n :class=\"sliderMarkTickClass(orientation)\"\n :style=\"offsetStyle(mark.value)\"\n aria-hidden=\"true\"\n />\n <!-- Подпись метки дублирует значение, которое диктор читает с бегунка:\n как отдельный текст внутри слайдера она только мешает. -->\n <span\n data-gr-slider-mark-label\n :class=\"sliderMarkLabelClassFor(percent(mark.value), { orientation, disabled: isDisabled })\"\n :style=\"offsetStyle(mark.value)\"\n aria-hidden=\"true\"\n >\n {{ mark.label }}\n </span>\n </template>\n\n <!-- Бегунки. -->\n <div\n v-for=\"(value, index) in values\"\n :id=\"index === 0 ? resolvedId : undefined\"\n :key=\"index\"\n :ref=\"(el) => { if (el) thumbEls[index] = el as HTMLElement }\"\n data-gr-slider-thumb\n :data-testid=\"`gr-slider-thumb-${index}`\"\n :class=\"sliderThumbClass({ size: resolvedSize, disabled: isDisabled, orientation })\"\n :style=\"offsetStyle(value)\"\n role=\"slider\"\n :tabindex=\"isDisabled ? -1 : 0\"\n :aria-valuemin=\"range && index === 1 ? values[0] : min\"\n :aria-valuemax=\"range && index === 0 ? values[1] : max\"\n :aria-valuenow=\"value\"\n :aria-valuetext=\"thumbValueText(value)\"\n :aria-orientation=\"orientation\"\n :aria-label=\"thumbAriaLabel(index)\"\n :aria-disabled=\"isDisabled ? 'true' : undefined\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-describedby=\"index === 0 ? describedBy : undefined\"\n :aria-required=\"isRequired && index === 0 ? 'true' : undefined\"\n :aria-readonly=\"isReadonly ? 'true' : undefined\"\n @keydown=\"onThumbKeydown($event, index)\"\n @mouseenter=\"hoveredThumb = index\"\n @mouseleave=\"hoveredThumb = null\"\n @focus=\"hoveredThumb = index\"\n @blur=\"hoveredThumb = null\"\n >\n <span\n v-if=\"showTooltipFor(index)\"\n data-gr-slider-tooltip\n :class=\"sliderTooltipClass(orientation)\"\n >\n {{ tooltipText(value) }}\n </span>\n </div>\n </div>\n </div>\n</template>\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
- import { t as GrSlider_default } from "./chunks/GrSlider-Db-jACXz.js";
1
+ import { t as GrSlider_default } from "./chunks/GrSlider-CO8N9SF6.js";
2
2
  import { n as grSliderSafelist, t as grSliderConfig } from "../../chunks/config-E1cTyRVV.js";
3
3
  export { GrSlider_default as GrSlider, GrSlider_default as default, grSliderConfig, grSliderSafelist };
@@ -2,7 +2,8 @@ import { n as useGranularityTranslations } from "../../../chunks/granularityI18n
2
2
  import { useRovingFocus } from "../../../composables/useRovingFocus.js";
3
3
  import { t as GrCard_default } from "../../GrCard/chunks/GrCard-Ck5--lMF.js";
4
4
  import { useAnnouncer } from "../../../composables/useAnnouncer.js";
5
- import { n as insertionIndex, r as moveItem, t as useDragSort } from "../../../chunks/useDragSort-0l47h4yb.js";
5
+ import { i as moveItem, r as insertionIndex } from "../../../chunks/dragSortGeometry-CY-llfrQ.js";
6
+ import { useDragSort } from "../../../composables/useDragSort.js";
6
7
  import { t as grip_vertical_default } from "../../../chunks/grip-vertical-lRha2S7q.js";
7
8
  import { a as emptyClass, c as indicatorAfterClass, d as rowDisabledClass, f as rowDraggingClass, h as rowLayoutClass, i as dividedClass, l as indicatorBeforeClass, m as rowGrabbedClass, o as handleClass, p as rowFocusClass, r as contentClass, s as handleDisabledClass, u as listClass } from "../../../chunks/config-CbEbLyDE.js";
8
9
  import { Fragment, computed, createBlock, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, nextTick, normalizeClass, normalizeStyle, openBlock, ref, renderList, renderSlot, toDisplayString, unref, watchEffect, withCtx, withModifiers } from "vue";
@@ -1 +1 @@
1
- {"version":3,"file":"GrSortableList-ce1psLL0.js","names":[],"sources":["../../../../src/components/GrSortableList/GrSortableList.vue","../../../../src/components/GrSortableList/GrSortableList.vue"],"sourcesContent":["<script setup lang=\"ts\" generic=\"T\">\nimport { computed, nextTick, ref, watchEffect } from 'vue'\n\nimport GrCard, { type GrCardVariant } from '../GrCard'\nimport { useAnnouncer } from '../../composables/useAnnouncer'\nimport { useDragSort } from '../../composables/useDragSort'\nimport { useRovingFocus } from '../../composables/useRovingFocus'\nimport { insertionIndex, moveItem } from '../../composables/internal/dragSortGeometry'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n\nimport type { GrSortableOrientation } from './grSortableListStyles'\nimport {\n contentClass,\n dividedClass,\n emptyClass,\n handleClass,\n handleDisabledClass,\n indicatorAfterClass,\n indicatorBeforeClass,\n listClass,\n rowDisabledClass,\n rowDraggingClass,\n rowFocusClass,\n rowGrabbedClass,\n rowLayoutClass,\n} from './grSortableListStyles'\n\nimport IconGripVertical from '~icons/lucide/grip-vertical'\n\nexport interface GrSortableListProps<TItem = unknown> {\n /** Набор в текущем порядке. `v-model`: наружу уходит новый массив, вход не мутируется. */\n modelValue: TItem[]\n /** Ключ элемента: имя поля или функция. Без него — индекс. */\n itemKey?: string | ((item: TItem, index: number) => string | number)\n /** Список только для чтения: ни указателем, ни с клавиатуры. */\n disabled?: boolean\n /** Ось переноса. `horizontal` — ряд с переносом по ширине. */\n orientation?: GrSortableOrientation\n /** Поверхность под списком — вариант карточки. */\n variant?: GrCardVariant\n /** Разделители между строками. */\n divided?: boolean\n /** Тянуть можно только за ручку. Выключено — тянется вся строка. */\n handleOnly?: boolean\n /** Высота видимой части: список становится скроллером с автопрокруткой при переносе. */\n maxHeight?: number | string\n /** Текст пустого состояния. Слот `#empty` сильнее. */\n emptyText?: string\n /** Имя списка для скринридера. Не задано — берётся из локали. */\n ariaLabel?: string\n}\n\nexport interface GrSortableListEmits<TItem = unknown> {\n (e: 'update:modelValue', value: TItem[]): void\n (e: 'move', from: number, to: number): void\n (e: 'change', value: TItem[]): void\n}\n\nconst props = withDefaults(defineProps<GrSortableListProps<T>>(), {\n itemKey: undefined,\n disabled: false,\n orientation: 'vertical',\n variant: undefined,\n divided: true,\n handleOnly: true,\n maxHeight: undefined,\n emptyText: undefined,\n ariaLabel: undefined,\n})\n\nconst emit = defineEmits<GrSortableListEmits<T>>()\n\ndefineSlots<{\n item?: (props: { item: T, index: number, dragging: boolean, grabbed: boolean }) => any\n handle?: (props: { item: T, index: number, disabled: boolean }) => any\n empty?: () => any\n}>()\n\nconst { t } = useGranularityTranslations()\nconst { announce } = useAnnouncer()\n\nconst listEl = ref<HTMLElement | null>(null)\nconst rowEls = new Map<string, HTMLElement>()\n\nconst count = computed(() => props.modelValue.length)\nconst isEmpty = computed(() => count.value === 0)\nconst isVertical = computed(() => props.orientation === 'vertical')\n\nfunction keyOf(item: T, index: number): string {\n if (typeof props.itemKey === 'function')\n return String(props.itemKey(item, index))\n if (typeof props.itemKey === 'string')\n return String((item as Record<string, unknown>)[props.itemKey])\n\n return String(index)\n}\n\nconst keys = computed(() => props.modelValue.map((item, index) => keyOf(item, index)))\n\nfunction indexOfKey(key: string): number {\n return keys.value.indexOf(key)\n}\n\nfunction registerRow(key: string, el: unknown): void {\n if (el instanceof HTMLElement) {\n rowEls.set(key, el)\n return\n }\n\n // Перестановка размонтирует старый узел уже после того, как встал новый:\n // снимаем запись, только если она протухла (приём из `GrTree`).\n const previous = rowEls.get(key)\n if (previous && !previous.isConnected)\n rowEls.delete(key)\n}\n\nconst roving = useRovingFocus<string>({\n items: () => keys.value,\n elementFor: key => rowEls.get(key) ?? null,\n orientation: () => (isVertical.value ? 'vertical' : 'horizontal'),\n wrap: () => false,\n})\n\nfunction applyMove(from: number, to: number): void {\n if (from < 0 || to < 0 || from === to)\n return\n\n const next = moveItem(props.modelValue, from, to)\n emit('update:modelValue', next)\n emit('move', from, to)\n emit('change', next)\n}\n\nconst sort = useDragSort<string, number>({\n items: () => keys.value,\n elementFor: key => rowEls.get(key) ?? null,\n orientation: () => props.orientation,\n scroller: () => (props.maxHeight === undefined ? null : listEl.value),\n disabled: () => props.disabled,\n resolveTarget: (hit, source) => insertionIndex(hit, indexOfKey(source), count.value),\n onDrop: (source, to) => applyMove(indexOfKey(source), to),\n onUpdate: (source, target, mode) => {\n // Объявляется только клавиатурный перенос: у мыши всё видно на экране, а\n // поток объявлений на каждом движении указателя делает диктор бесполезным.\n if (mode !== 'keyboard' || source === null)\n return\n\n const position = (target ?? indexOfKey(source)) + 1\n announce(t('gr.sortable.moved', 'Position {position} of {count}', { position, count: count.value }))\n },\n})\n\nconst activeIndex = computed(() => (sort.source.value === null ? -1 : indexOfKey(sort.source.value)))\n\n/** Куда встанет строка: индикатор рисуется на строке-соседе, а не отдельным узлом. */\nfunction indicatorFor(index: number): 'before' | 'after' | null {\n const target = sort.target.value\n if (target === null || !sort.isActive.value || index === activeIndex.value)\n return null\n\n if (target === index)\n return target < activeIndex.value || activeIndex.value === -1 ? 'before' : 'after'\n\n return null\n}\n\nfunction rowClass(index: number, key: string): string[] {\n const grabbed = sort.mode.value === 'keyboard' && sort.source.value === key\n const dragging = sort.mode.value === 'pointer' && sort.source.value === key\n const indicator = indicatorFor(index)\n\n return [\n rowLayoutClass,\n rowFocusClass,\n props.disabled ? rowDisabledClass : '',\n grabbed ? rowGrabbedClass : '',\n dragging ? rowDraggingClass : '',\n indicator === 'before' ? indicatorBeforeClass : '',\n indicator === 'after' ? indicatorAfterClass : '',\n ]\n}\n\nfunction startPointer(key: string, event: PointerEvent): void {\n if (props.disabled)\n return\n\n sort.startFrom(key)(event)\n}\n\nfunction grab(key: string): void {\n sort.grab(key)\n if (sort.mode.value !== 'keyboard')\n return\n\n // Цель ставится до объявления: `setTarget` сам объявляет позицию, и обратный\n // порядок стёр бы более полное сообщение о захвате.\n sort.setTarget(indexOfKey(key))\n announce(t('gr.sortable.grabbed', 'Grabbed. Position {position} of {count}', {\n position: indexOfKey(key) + 1,\n count: count.value,\n }))\n}\n\nasync function drop(key: string): Promise<void> {\n const target = sort.target.value\n sort.drop()\n\n announce(t('gr.sortable.dropped', 'Dropped at position {position} of {count}', {\n position: (target ?? 0) + 1,\n count: count.value,\n }))\n\n // Фокус остаётся на перенесённой строке: иначе после переноса он падает на\n // тело документа и клавиатурный сценарий обрывается на первом же шаге.\n await nextTick()\n await roving.focusKey(key)\n}\n\nfunction cancel(): void {\n if (!sort.isActive.value)\n return\n\n sort.cancel()\n announce(t('gr.sortable.cancelled', 'Move cancelled'))\n}\n\nfunction moveTargetBy(delta: number): void {\n const current = sort.target.value ?? activeIndex.value\n const next = Math.min(count.value - 1, Math.max(0, current + delta))\n\n if (next !== current)\n sort.setTarget(next)\n}\n\nfunction onRowKeydown(event: KeyboardEvent, key: string): void {\n if (props.disabled)\n return\n\n const forward = isVertical.value ? 'ArrowDown' : 'ArrowRight'\n const backward = isVertical.value ? 'ArrowUp' : 'ArrowLeft'\n const grabbed = sort.mode.value === 'keyboard'\n\n if (event.key === 'Escape') {\n if (!grabbed)\n return\n\n event.preventDefault()\n cancel()\n return\n }\n\n if (event.key === ' ' || event.key === 'Enter') {\n event.preventDefault()\n if (grabbed)\n void drop(key)\n else grab(key)\n return\n }\n\n if (grabbed) {\n // Взятая строка забирает стрелки себе: двигается она, а не фокус.\n if (event.key === forward || event.key === backward) {\n event.preventDefault()\n moveTargetBy(event.key === forward ? 1 : -1)\n }\n return\n }\n\n roving.handleNavigationKeys(event)\n}\n\n/** Фокус ушёл из списка — взятая строка не может остаться взятой навсегда. */\nfunction onFocusout(event: FocusEvent): void {\n if (sort.mode.value !== 'keyboard')\n return\n\n const next = event.relatedTarget as Node | null\n if (next && listEl.value?.contains(next))\n return\n\n cancel()\n}\n\nconst resolvedEmptyText = computed(() => props.emptyText ?? t('gr.sortable.empty', 'Nothing to sort yet'))\nconst resolvedLabel = computed(() => props.ariaLabel ?? t('gr.sortable.label', 'Sortable list'))\nconst handleLabel = computed(() => t('gr.sortable.handle', 'Reorder'))\n\nconst listStyle = computed(() => (props.maxHeight === undefined\n ? undefined\n : { maxHeight: typeof props.maxHeight === 'number' ? `${props.maxHeight}px` : props.maxHeight, overflow: 'auto' }))\n\ndefineExpose({\n /** Программная перестановка — та же, что делает перенос. */\n move: (from: number, to: number) => applyMove(from, to),\n focusItem: (index: number) => roving.focusKey(keys.value[index]),\n})\n\nif (__GR_DEV__) {\n watchEffect(() => {\n if (!Array.isArray(props.modelValue)) {\n console.warn(\n `[granularity] GrSortableList: обязательный проп \\`modelValue\\` должен быть массивом — получено ${String(props.modelValue)}.`,\n )\n }\n })\n}\n</script>\n\n<template>\n <GrCard :variant=\"variant\">\n <div\n ref=\"listEl\"\n role=\"list\"\n data-gr-sortable\n :aria-label=\"resolvedLabel\"\n :aria-disabled=\"disabled ? 'true' : undefined\"\n :class=\"[listClass[orientation], divided ? dividedClass : '']\"\n :style=\"listStyle\"\n @focusout=\"onFocusout\"\n >\n <div\n v-if=\"isEmpty\"\n data-gr-sortable-empty\n :class=\"emptyClass\"\n >\n <slot name=\"empty\">\n {{ resolvedEmptyText }}\n </slot>\n </div>\n\n <div\n v-for=\"(item, index) in modelValue\"\n v-else\n :key=\"keys[index]\"\n :ref=\"el => registerRow(keys[index], el)\"\n role=\"listitem\"\n data-gr-sortable-item\n :tabindex=\"disabled ? -1 : roving.tabindexFor(keys[index])\"\n :aria-roledescription=\"handleLabel\"\n :aria-posinset=\"index + 1\"\n :aria-setsize=\"count\"\n :aria-grabbed=\"sort.mode.value === 'keyboard' && sort.source.value === keys[index] ? 'true' : undefined\"\n :class=\"rowClass(index, keys[index])\"\n @keydown=\"onRowKeydown($event, keys[index])\"\n @focus=\"roving.setActive(keys[index])\"\n @pointerdown=\"handleOnly ? undefined : startPointer(keys[index], $event)\"\n >\n <button\n type=\"button\"\n tabindex=\"-1\"\n data-gr-sortable-handle\n :class=\"[handleClass, disabled ? handleDisabledClass : '']\"\n :aria-label=\"handleLabel\"\n :disabled=\"disabled\"\n @pointerdown=\"startPointer(keys[index], $event)\"\n @click.stop\n >\n <slot\n name=\"handle\"\n :item=\"item\"\n :index=\"index\"\n :disabled=\"disabled\"\n >\n <IconGripVertical class=\"block\" aria-hidden=\"true\" />\n </slot>\n </button>\n\n <div :class=\"contentClass\">\n <slot\n name=\"item\"\n :item=\"item\"\n :index=\"index\"\n :dragging=\"sort.mode.value === 'pointer' && sort.source.value === keys[index]\"\n :grabbed=\"sort.mode.value === 'keyboard' && sort.source.value === keys[index]\"\n >\n {{ item }}\n </slot>\n </div>\n </div>\n </div>\n </GrCard>\n</template>\n","<script setup lang=\"ts\" generic=\"T\">\nimport { computed, nextTick, ref, watchEffect } from 'vue'\n\nimport GrCard, { type GrCardVariant } from '../GrCard'\nimport { useAnnouncer } from '../../composables/useAnnouncer'\nimport { useDragSort } from '../../composables/useDragSort'\nimport { useRovingFocus } from '../../composables/useRovingFocus'\nimport { insertionIndex, moveItem } from '../../composables/internal/dragSortGeometry'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n\nimport type { GrSortableOrientation } from './grSortableListStyles'\nimport {\n contentClass,\n dividedClass,\n emptyClass,\n handleClass,\n handleDisabledClass,\n indicatorAfterClass,\n indicatorBeforeClass,\n listClass,\n rowDisabledClass,\n rowDraggingClass,\n rowFocusClass,\n rowGrabbedClass,\n rowLayoutClass,\n} from './grSortableListStyles'\n\nimport IconGripVertical from '~icons/lucide/grip-vertical'\n\nexport interface GrSortableListProps<TItem = unknown> {\n /** Набор в текущем порядке. `v-model`: наружу уходит новый массив, вход не мутируется. */\n modelValue: TItem[]\n /** Ключ элемента: имя поля или функция. Без него — индекс. */\n itemKey?: string | ((item: TItem, index: number) => string | number)\n /** Список только для чтения: ни указателем, ни с клавиатуры. */\n disabled?: boolean\n /** Ось переноса. `horizontal` — ряд с переносом по ширине. */\n orientation?: GrSortableOrientation\n /** Поверхность под списком — вариант карточки. */\n variant?: GrCardVariant\n /** Разделители между строками. */\n divided?: boolean\n /** Тянуть можно только за ручку. Выключено — тянется вся строка. */\n handleOnly?: boolean\n /** Высота видимой части: список становится скроллером с автопрокруткой при переносе. */\n maxHeight?: number | string\n /** Текст пустого состояния. Слот `#empty` сильнее. */\n emptyText?: string\n /** Имя списка для скринридера. Не задано — берётся из локали. */\n ariaLabel?: string\n}\n\nexport interface GrSortableListEmits<TItem = unknown> {\n (e: 'update:modelValue', value: TItem[]): void\n (e: 'move', from: number, to: number): void\n (e: 'change', value: TItem[]): void\n}\n\nconst props = withDefaults(defineProps<GrSortableListProps<T>>(), {\n itemKey: undefined,\n disabled: false,\n orientation: 'vertical',\n variant: undefined,\n divided: true,\n handleOnly: true,\n maxHeight: undefined,\n emptyText: undefined,\n ariaLabel: undefined,\n})\n\nconst emit = defineEmits<GrSortableListEmits<T>>()\n\ndefineSlots<{\n item?: (props: { item: T, index: number, dragging: boolean, grabbed: boolean }) => any\n handle?: (props: { item: T, index: number, disabled: boolean }) => any\n empty?: () => any\n}>()\n\nconst { t } = useGranularityTranslations()\nconst { announce } = useAnnouncer()\n\nconst listEl = ref<HTMLElement | null>(null)\nconst rowEls = new Map<string, HTMLElement>()\n\nconst count = computed(() => props.modelValue.length)\nconst isEmpty = computed(() => count.value === 0)\nconst isVertical = computed(() => props.orientation === 'vertical')\n\nfunction keyOf(item: T, index: number): string {\n if (typeof props.itemKey === 'function')\n return String(props.itemKey(item, index))\n if (typeof props.itemKey === 'string')\n return String((item as Record<string, unknown>)[props.itemKey])\n\n return String(index)\n}\n\nconst keys = computed(() => props.modelValue.map((item, index) => keyOf(item, index)))\n\nfunction indexOfKey(key: string): number {\n return keys.value.indexOf(key)\n}\n\nfunction registerRow(key: string, el: unknown): void {\n if (el instanceof HTMLElement) {\n rowEls.set(key, el)\n return\n }\n\n // Перестановка размонтирует старый узел уже после того, как встал новый:\n // снимаем запись, только если она протухла (приём из `GrTree`).\n const previous = rowEls.get(key)\n if (previous && !previous.isConnected)\n rowEls.delete(key)\n}\n\nconst roving = useRovingFocus<string>({\n items: () => keys.value,\n elementFor: key => rowEls.get(key) ?? null,\n orientation: () => (isVertical.value ? 'vertical' : 'horizontal'),\n wrap: () => false,\n})\n\nfunction applyMove(from: number, to: number): void {\n if (from < 0 || to < 0 || from === to)\n return\n\n const next = moveItem(props.modelValue, from, to)\n emit('update:modelValue', next)\n emit('move', from, to)\n emit('change', next)\n}\n\nconst sort = useDragSort<string, number>({\n items: () => keys.value,\n elementFor: key => rowEls.get(key) ?? null,\n orientation: () => props.orientation,\n scroller: () => (props.maxHeight === undefined ? null : listEl.value),\n disabled: () => props.disabled,\n resolveTarget: (hit, source) => insertionIndex(hit, indexOfKey(source), count.value),\n onDrop: (source, to) => applyMove(indexOfKey(source), to),\n onUpdate: (source, target, mode) => {\n // Объявляется только клавиатурный перенос: у мыши всё видно на экране, а\n // поток объявлений на каждом движении указателя делает диктор бесполезным.\n if (mode !== 'keyboard' || source === null)\n return\n\n const position = (target ?? indexOfKey(source)) + 1\n announce(t('gr.sortable.moved', 'Position {position} of {count}', { position, count: count.value }))\n },\n})\n\nconst activeIndex = computed(() => (sort.source.value === null ? -1 : indexOfKey(sort.source.value)))\n\n/** Куда встанет строка: индикатор рисуется на строке-соседе, а не отдельным узлом. */\nfunction indicatorFor(index: number): 'before' | 'after' | null {\n const target = sort.target.value\n if (target === null || !sort.isActive.value || index === activeIndex.value)\n return null\n\n if (target === index)\n return target < activeIndex.value || activeIndex.value === -1 ? 'before' : 'after'\n\n return null\n}\n\nfunction rowClass(index: number, key: string): string[] {\n const grabbed = sort.mode.value === 'keyboard' && sort.source.value === key\n const dragging = sort.mode.value === 'pointer' && sort.source.value === key\n const indicator = indicatorFor(index)\n\n return [\n rowLayoutClass,\n rowFocusClass,\n props.disabled ? rowDisabledClass : '',\n grabbed ? rowGrabbedClass : '',\n dragging ? rowDraggingClass : '',\n indicator === 'before' ? indicatorBeforeClass : '',\n indicator === 'after' ? indicatorAfterClass : '',\n ]\n}\n\nfunction startPointer(key: string, event: PointerEvent): void {\n if (props.disabled)\n return\n\n sort.startFrom(key)(event)\n}\n\nfunction grab(key: string): void {\n sort.grab(key)\n if (sort.mode.value !== 'keyboard')\n return\n\n // Цель ставится до объявления: `setTarget` сам объявляет позицию, и обратный\n // порядок стёр бы более полное сообщение о захвате.\n sort.setTarget(indexOfKey(key))\n announce(t('gr.sortable.grabbed', 'Grabbed. Position {position} of {count}', {\n position: indexOfKey(key) + 1,\n count: count.value,\n }))\n}\n\nasync function drop(key: string): Promise<void> {\n const target = sort.target.value\n sort.drop()\n\n announce(t('gr.sortable.dropped', 'Dropped at position {position} of {count}', {\n position: (target ?? 0) + 1,\n count: count.value,\n }))\n\n // Фокус остаётся на перенесённой строке: иначе после переноса он падает на\n // тело документа и клавиатурный сценарий обрывается на первом же шаге.\n await nextTick()\n await roving.focusKey(key)\n}\n\nfunction cancel(): void {\n if (!sort.isActive.value)\n return\n\n sort.cancel()\n announce(t('gr.sortable.cancelled', 'Move cancelled'))\n}\n\nfunction moveTargetBy(delta: number): void {\n const current = sort.target.value ?? activeIndex.value\n const next = Math.min(count.value - 1, Math.max(0, current + delta))\n\n if (next !== current)\n sort.setTarget(next)\n}\n\nfunction onRowKeydown(event: KeyboardEvent, key: string): void {\n if (props.disabled)\n return\n\n const forward = isVertical.value ? 'ArrowDown' : 'ArrowRight'\n const backward = isVertical.value ? 'ArrowUp' : 'ArrowLeft'\n const grabbed = sort.mode.value === 'keyboard'\n\n if (event.key === 'Escape') {\n if (!grabbed)\n return\n\n event.preventDefault()\n cancel()\n return\n }\n\n if (event.key === ' ' || event.key === 'Enter') {\n event.preventDefault()\n if (grabbed)\n void drop(key)\n else grab(key)\n return\n }\n\n if (grabbed) {\n // Взятая строка забирает стрелки себе: двигается она, а не фокус.\n if (event.key === forward || event.key === backward) {\n event.preventDefault()\n moveTargetBy(event.key === forward ? 1 : -1)\n }\n return\n }\n\n roving.handleNavigationKeys(event)\n}\n\n/** Фокус ушёл из списка — взятая строка не может остаться взятой навсегда. */\nfunction onFocusout(event: FocusEvent): void {\n if (sort.mode.value !== 'keyboard')\n return\n\n const next = event.relatedTarget as Node | null\n if (next && listEl.value?.contains(next))\n return\n\n cancel()\n}\n\nconst resolvedEmptyText = computed(() => props.emptyText ?? t('gr.sortable.empty', 'Nothing to sort yet'))\nconst resolvedLabel = computed(() => props.ariaLabel ?? t('gr.sortable.label', 'Sortable list'))\nconst handleLabel = computed(() => t('gr.sortable.handle', 'Reorder'))\n\nconst listStyle = computed(() => (props.maxHeight === undefined\n ? undefined\n : { maxHeight: typeof props.maxHeight === 'number' ? `${props.maxHeight}px` : props.maxHeight, overflow: 'auto' }))\n\ndefineExpose({\n /** Программная перестановка — та же, что делает перенос. */\n move: (from: number, to: number) => applyMove(from, to),\n focusItem: (index: number) => roving.focusKey(keys.value[index]),\n})\n\nif (__GR_DEV__) {\n watchEffect(() => {\n if (!Array.isArray(props.modelValue)) {\n console.warn(\n `[granularity] GrSortableList: обязательный проп \\`modelValue\\` должен быть массивом — получено ${String(props.modelValue)}.`,\n )\n }\n })\n}\n</script>\n\n<template>\n <GrCard :variant=\"variant\">\n <div\n ref=\"listEl\"\n role=\"list\"\n data-gr-sortable\n :aria-label=\"resolvedLabel\"\n :aria-disabled=\"disabled ? 'true' : undefined\"\n :class=\"[listClass[orientation], divided ? dividedClass : '']\"\n :style=\"listStyle\"\n @focusout=\"onFocusout\"\n >\n <div\n v-if=\"isEmpty\"\n data-gr-sortable-empty\n :class=\"emptyClass\"\n >\n <slot name=\"empty\">\n {{ resolvedEmptyText }}\n </slot>\n </div>\n\n <div\n v-for=\"(item, index) in modelValue\"\n v-else\n :key=\"keys[index]\"\n :ref=\"el => registerRow(keys[index], el)\"\n role=\"listitem\"\n data-gr-sortable-item\n :tabindex=\"disabled ? -1 : roving.tabindexFor(keys[index])\"\n :aria-roledescription=\"handleLabel\"\n :aria-posinset=\"index + 1\"\n :aria-setsize=\"count\"\n :aria-grabbed=\"sort.mode.value === 'keyboard' && sort.source.value === keys[index] ? 'true' : undefined\"\n :class=\"rowClass(index, keys[index])\"\n @keydown=\"onRowKeydown($event, keys[index])\"\n @focus=\"roving.setActive(keys[index])\"\n @pointerdown=\"handleOnly ? undefined : startPointer(keys[index], $event)\"\n >\n <button\n type=\"button\"\n tabindex=\"-1\"\n data-gr-sortable-handle\n :class=\"[handleClass, disabled ? handleDisabledClass : '']\"\n :aria-label=\"handleLabel\"\n :disabled=\"disabled\"\n @pointerdown=\"startPointer(keys[index], $event)\"\n @click.stop\n >\n <slot\n name=\"handle\"\n :item=\"item\"\n :index=\"index\"\n :disabled=\"disabled\"\n >\n <IconGripVertical class=\"block\" aria-hidden=\"true\" />\n </slot>\n </button>\n\n <div :class=\"contentClass\">\n <slot\n name=\"item\"\n :item=\"item\"\n :index=\"index\"\n :dragging=\"sort.mode.value === 'pointer' && sort.source.value === keys[index]\"\n :grabbed=\"sort.mode.value === 'keyboard' && sort.source.value === keys[index]\"\n >\n {{ item }}\n </slot>\n </div>\n </div>\n </div>\n </GrCard>\n</template>\n"],"mappings":"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"}
1
+ {"version":3,"file":"GrSortableList-NKriuGm_.js","names":[],"sources":["../../../../src/components/GrSortableList/GrSortableList.vue","../../../../src/components/GrSortableList/GrSortableList.vue"],"sourcesContent":["<script setup lang=\"ts\" generic=\"T\">\nimport { computed, nextTick, ref, watchEffect } from 'vue'\n\nimport GrCard, { type GrCardVariant } from '../GrCard'\nimport { useAnnouncer } from '../../composables/useAnnouncer'\nimport { useDragSort } from '../../composables/useDragSort'\nimport { useRovingFocus } from '../../composables/useRovingFocus'\nimport { insertionIndex, moveItem } from '../../composables/internal/dragSortGeometry'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n\nimport type { GrSortableOrientation } from './grSortableListStyles'\nimport {\n contentClass,\n dividedClass,\n emptyClass,\n handleClass,\n handleDisabledClass,\n indicatorAfterClass,\n indicatorBeforeClass,\n listClass,\n rowDisabledClass,\n rowDraggingClass,\n rowFocusClass,\n rowGrabbedClass,\n rowLayoutClass,\n} from './grSortableListStyles'\n\nimport IconGripVertical from '~icons/lucide/grip-vertical'\n\nexport interface GrSortableListProps<TItem = unknown> {\n /** Набор в текущем порядке. `v-model`: наружу уходит новый массив, вход не мутируется. */\n modelValue: TItem[]\n /** Ключ элемента: имя поля или функция. Без него — индекс. */\n itemKey?: string | ((item: TItem, index: number) => string | number)\n /** Список только для чтения: ни указателем, ни с клавиатуры. */\n disabled?: boolean\n /** Ось переноса. `horizontal` — ряд с переносом по ширине. */\n orientation?: GrSortableOrientation\n /** Поверхность под списком — вариант карточки. */\n variant?: GrCardVariant\n /** Разделители между строками. */\n divided?: boolean\n /** Тянуть можно только за ручку. Выключено — тянется вся строка. */\n handleOnly?: boolean\n /** Высота видимой части: список становится скроллером с автопрокруткой при переносе. */\n maxHeight?: number | string\n /** Текст пустого состояния. Слот `#empty` сильнее. */\n emptyText?: string\n /** Имя списка для скринридера. Не задано — берётся из локали. */\n ariaLabel?: string\n}\n\nexport interface GrSortableListEmits<TItem = unknown> {\n (e: 'update:modelValue', value: TItem[]): void\n (e: 'move', from: number, to: number): void\n (e: 'change', value: TItem[]): void\n}\n\nconst props = withDefaults(defineProps<GrSortableListProps<T>>(), {\n itemKey: undefined,\n disabled: false,\n orientation: 'vertical',\n variant: undefined,\n divided: true,\n handleOnly: true,\n maxHeight: undefined,\n emptyText: undefined,\n ariaLabel: undefined,\n})\n\nconst emit = defineEmits<GrSortableListEmits<T>>()\n\ndefineSlots<{\n item?: (props: { item: T, index: number, dragging: boolean, grabbed: boolean }) => any\n handle?: (props: { item: T, index: number, disabled: boolean }) => any\n empty?: () => any\n}>()\n\nconst { t } = useGranularityTranslations()\nconst { announce } = useAnnouncer()\n\nconst listEl = ref<HTMLElement | null>(null)\nconst rowEls = new Map<string, HTMLElement>()\n\nconst count = computed(() => props.modelValue.length)\nconst isEmpty = computed(() => count.value === 0)\nconst isVertical = computed(() => props.orientation === 'vertical')\n\nfunction keyOf(item: T, index: number): string {\n if (typeof props.itemKey === 'function')\n return String(props.itemKey(item, index))\n if (typeof props.itemKey === 'string')\n return String((item as Record<string, unknown>)[props.itemKey])\n\n return String(index)\n}\n\nconst keys = computed(() => props.modelValue.map((item, index) => keyOf(item, index)))\n\nfunction indexOfKey(key: string): number {\n return keys.value.indexOf(key)\n}\n\nfunction registerRow(key: string, el: unknown): void {\n if (el instanceof HTMLElement) {\n rowEls.set(key, el)\n return\n }\n\n // Перестановка размонтирует старый узел уже после того, как встал новый:\n // снимаем запись, только если она протухла (приём из `GrTree`).\n const previous = rowEls.get(key)\n if (previous && !previous.isConnected)\n rowEls.delete(key)\n}\n\nconst roving = useRovingFocus<string>({\n items: () => keys.value,\n elementFor: key => rowEls.get(key) ?? null,\n orientation: () => (isVertical.value ? 'vertical' : 'horizontal'),\n wrap: () => false,\n})\n\nfunction applyMove(from: number, to: number): void {\n if (from < 0 || to < 0 || from === to)\n return\n\n const next = moveItem(props.modelValue, from, to)\n emit('update:modelValue', next)\n emit('move', from, to)\n emit('change', next)\n}\n\nconst sort = useDragSort<string, number>({\n items: () => keys.value,\n elementFor: key => rowEls.get(key) ?? null,\n orientation: () => props.orientation,\n scroller: () => (props.maxHeight === undefined ? null : listEl.value),\n disabled: () => props.disabled,\n resolveTarget: (hit, source) => insertionIndex(hit, indexOfKey(source), count.value),\n onDrop: (source, to) => applyMove(indexOfKey(source), to),\n onUpdate: (source, target, mode) => {\n // Объявляется только клавиатурный перенос: у мыши всё видно на экране, а\n // поток объявлений на каждом движении указателя делает диктор бесполезным.\n if (mode !== 'keyboard' || source === null)\n return\n\n const position = (target ?? indexOfKey(source)) + 1\n announce(t('gr.sortable.moved', 'Position {position} of {count}', { position, count: count.value }))\n },\n})\n\nconst activeIndex = computed(() => (sort.source.value === null ? -1 : indexOfKey(sort.source.value)))\n\n/** Куда встанет строка: индикатор рисуется на строке-соседе, а не отдельным узлом. */\nfunction indicatorFor(index: number): 'before' | 'after' | null {\n const target = sort.target.value\n if (target === null || !sort.isActive.value || index === activeIndex.value)\n return null\n\n if (target === index)\n return target < activeIndex.value || activeIndex.value === -1 ? 'before' : 'after'\n\n return null\n}\n\nfunction rowClass(index: number, key: string): string[] {\n const grabbed = sort.mode.value === 'keyboard' && sort.source.value === key\n const dragging = sort.mode.value === 'pointer' && sort.source.value === key\n const indicator = indicatorFor(index)\n\n return [\n rowLayoutClass,\n rowFocusClass,\n props.disabled ? rowDisabledClass : '',\n grabbed ? rowGrabbedClass : '',\n dragging ? rowDraggingClass : '',\n indicator === 'before' ? indicatorBeforeClass : '',\n indicator === 'after' ? indicatorAfterClass : '',\n ]\n}\n\nfunction startPointer(key: string, event: PointerEvent): void {\n if (props.disabled)\n return\n\n sort.startFrom(key)(event)\n}\n\nfunction grab(key: string): void {\n sort.grab(key)\n if (sort.mode.value !== 'keyboard')\n return\n\n // Цель ставится до объявления: `setTarget` сам объявляет позицию, и обратный\n // порядок стёр бы более полное сообщение о захвате.\n sort.setTarget(indexOfKey(key))\n announce(t('gr.sortable.grabbed', 'Grabbed. Position {position} of {count}', {\n position: indexOfKey(key) + 1,\n count: count.value,\n }))\n}\n\nasync function drop(key: string): Promise<void> {\n const target = sort.target.value\n sort.drop()\n\n announce(t('gr.sortable.dropped', 'Dropped at position {position} of {count}', {\n position: (target ?? 0) + 1,\n count: count.value,\n }))\n\n // Фокус остаётся на перенесённой строке: иначе после переноса он падает на\n // тело документа и клавиатурный сценарий обрывается на первом же шаге.\n await nextTick()\n await roving.focusKey(key)\n}\n\nfunction cancel(): void {\n if (!sort.isActive.value)\n return\n\n sort.cancel()\n announce(t('gr.sortable.cancelled', 'Move cancelled'))\n}\n\nfunction moveTargetBy(delta: number): void {\n const current = sort.target.value ?? activeIndex.value\n const next = Math.min(count.value - 1, Math.max(0, current + delta))\n\n if (next !== current)\n sort.setTarget(next)\n}\n\nfunction onRowKeydown(event: KeyboardEvent, key: string): void {\n if (props.disabled)\n return\n\n const forward = isVertical.value ? 'ArrowDown' : 'ArrowRight'\n const backward = isVertical.value ? 'ArrowUp' : 'ArrowLeft'\n const grabbed = sort.mode.value === 'keyboard'\n\n if (event.key === 'Escape') {\n if (!grabbed)\n return\n\n event.preventDefault()\n cancel()\n return\n }\n\n if (event.key === ' ' || event.key === 'Enter') {\n event.preventDefault()\n if (grabbed)\n void drop(key)\n else grab(key)\n return\n }\n\n if (grabbed) {\n // Взятая строка забирает стрелки себе: двигается она, а не фокус.\n if (event.key === forward || event.key === backward) {\n event.preventDefault()\n moveTargetBy(event.key === forward ? 1 : -1)\n }\n return\n }\n\n roving.handleNavigationKeys(event)\n}\n\n/** Фокус ушёл из списка — взятая строка не может остаться взятой навсегда. */\nfunction onFocusout(event: FocusEvent): void {\n if (sort.mode.value !== 'keyboard')\n return\n\n const next = event.relatedTarget as Node | null\n if (next && listEl.value?.contains(next))\n return\n\n cancel()\n}\n\nconst resolvedEmptyText = computed(() => props.emptyText ?? t('gr.sortable.empty', 'Nothing to sort yet'))\nconst resolvedLabel = computed(() => props.ariaLabel ?? t('gr.sortable.label', 'Sortable list'))\nconst handleLabel = computed(() => t('gr.sortable.handle', 'Reorder'))\n\nconst listStyle = computed(() => (props.maxHeight === undefined\n ? undefined\n : { maxHeight: typeof props.maxHeight === 'number' ? `${props.maxHeight}px` : props.maxHeight, overflow: 'auto' }))\n\ndefineExpose({\n /** Программная перестановка — та же, что делает перенос. */\n move: (from: number, to: number) => applyMove(from, to),\n focusItem: (index: number) => roving.focusKey(keys.value[index]),\n})\n\nif (__GR_DEV__) {\n watchEffect(() => {\n if (!Array.isArray(props.modelValue)) {\n console.warn(\n `[granularity] GrSortableList: обязательный проп \\`modelValue\\` должен быть массивом — получено ${String(props.modelValue)}.`,\n )\n }\n })\n}\n</script>\n\n<template>\n <GrCard :variant=\"variant\">\n <div\n ref=\"listEl\"\n role=\"list\"\n data-gr-sortable\n :aria-label=\"resolvedLabel\"\n :aria-disabled=\"disabled ? 'true' : undefined\"\n :class=\"[listClass[orientation], divided ? dividedClass : '']\"\n :style=\"listStyle\"\n @focusout=\"onFocusout\"\n >\n <div\n v-if=\"isEmpty\"\n data-gr-sortable-empty\n :class=\"emptyClass\"\n >\n <slot name=\"empty\">\n {{ resolvedEmptyText }}\n </slot>\n </div>\n\n <div\n v-for=\"(item, index) in modelValue\"\n v-else\n :key=\"keys[index]\"\n :ref=\"el => registerRow(keys[index], el)\"\n role=\"listitem\"\n data-gr-sortable-item\n :tabindex=\"disabled ? -1 : roving.tabindexFor(keys[index])\"\n :aria-roledescription=\"handleLabel\"\n :aria-posinset=\"index + 1\"\n :aria-setsize=\"count\"\n :aria-grabbed=\"sort.mode.value === 'keyboard' && sort.source.value === keys[index] ? 'true' : undefined\"\n :class=\"rowClass(index, keys[index])\"\n @keydown=\"onRowKeydown($event, keys[index])\"\n @focus=\"roving.setActive(keys[index])\"\n @pointerdown=\"handleOnly ? undefined : startPointer(keys[index], $event)\"\n >\n <button\n type=\"button\"\n tabindex=\"-1\"\n data-gr-sortable-handle\n :class=\"[handleClass, disabled ? handleDisabledClass : '']\"\n :aria-label=\"handleLabel\"\n :disabled=\"disabled\"\n @pointerdown=\"startPointer(keys[index], $event)\"\n @click.stop\n >\n <slot\n name=\"handle\"\n :item=\"item\"\n :index=\"index\"\n :disabled=\"disabled\"\n >\n <IconGripVertical class=\"block\" aria-hidden=\"true\" />\n </slot>\n </button>\n\n <div :class=\"contentClass\">\n <slot\n name=\"item\"\n :item=\"item\"\n :index=\"index\"\n :dragging=\"sort.mode.value === 'pointer' && sort.source.value === keys[index]\"\n :grabbed=\"sort.mode.value === 'keyboard' && sort.source.value === keys[index]\"\n >\n {{ item }}\n </slot>\n </div>\n </div>\n </div>\n </GrCard>\n</template>\n","<script setup lang=\"ts\" generic=\"T\">\nimport { computed, nextTick, ref, watchEffect } from 'vue'\n\nimport GrCard, { type GrCardVariant } from '../GrCard'\nimport { useAnnouncer } from '../../composables/useAnnouncer'\nimport { useDragSort } from '../../composables/useDragSort'\nimport { useRovingFocus } from '../../composables/useRovingFocus'\nimport { insertionIndex, moveItem } from '../../composables/internal/dragSortGeometry'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n\nimport type { GrSortableOrientation } from './grSortableListStyles'\nimport {\n contentClass,\n dividedClass,\n emptyClass,\n handleClass,\n handleDisabledClass,\n indicatorAfterClass,\n indicatorBeforeClass,\n listClass,\n rowDisabledClass,\n rowDraggingClass,\n rowFocusClass,\n rowGrabbedClass,\n rowLayoutClass,\n} from './grSortableListStyles'\n\nimport IconGripVertical from '~icons/lucide/grip-vertical'\n\nexport interface GrSortableListProps<TItem = unknown> {\n /** Набор в текущем порядке. `v-model`: наружу уходит новый массив, вход не мутируется. */\n modelValue: TItem[]\n /** Ключ элемента: имя поля или функция. Без него — индекс. */\n itemKey?: string | ((item: TItem, index: number) => string | number)\n /** Список только для чтения: ни указателем, ни с клавиатуры. */\n disabled?: boolean\n /** Ось переноса. `horizontal` — ряд с переносом по ширине. */\n orientation?: GrSortableOrientation\n /** Поверхность под списком — вариант карточки. */\n variant?: GrCardVariant\n /** Разделители между строками. */\n divided?: boolean\n /** Тянуть можно только за ручку. Выключено — тянется вся строка. */\n handleOnly?: boolean\n /** Высота видимой части: список становится скроллером с автопрокруткой при переносе. */\n maxHeight?: number | string\n /** Текст пустого состояния. Слот `#empty` сильнее. */\n emptyText?: string\n /** Имя списка для скринридера. Не задано — берётся из локали. */\n ariaLabel?: string\n}\n\nexport interface GrSortableListEmits<TItem = unknown> {\n (e: 'update:modelValue', value: TItem[]): void\n (e: 'move', from: number, to: number): void\n (e: 'change', value: TItem[]): void\n}\n\nconst props = withDefaults(defineProps<GrSortableListProps<T>>(), {\n itemKey: undefined,\n disabled: false,\n orientation: 'vertical',\n variant: undefined,\n divided: true,\n handleOnly: true,\n maxHeight: undefined,\n emptyText: undefined,\n ariaLabel: undefined,\n})\n\nconst emit = defineEmits<GrSortableListEmits<T>>()\n\ndefineSlots<{\n item?: (props: { item: T, index: number, dragging: boolean, grabbed: boolean }) => any\n handle?: (props: { item: T, index: number, disabled: boolean }) => any\n empty?: () => any\n}>()\n\nconst { t } = useGranularityTranslations()\nconst { announce } = useAnnouncer()\n\nconst listEl = ref<HTMLElement | null>(null)\nconst rowEls = new Map<string, HTMLElement>()\n\nconst count = computed(() => props.modelValue.length)\nconst isEmpty = computed(() => count.value === 0)\nconst isVertical = computed(() => props.orientation === 'vertical')\n\nfunction keyOf(item: T, index: number): string {\n if (typeof props.itemKey === 'function')\n return String(props.itemKey(item, index))\n if (typeof props.itemKey === 'string')\n return String((item as Record<string, unknown>)[props.itemKey])\n\n return String(index)\n}\n\nconst keys = computed(() => props.modelValue.map((item, index) => keyOf(item, index)))\n\nfunction indexOfKey(key: string): number {\n return keys.value.indexOf(key)\n}\n\nfunction registerRow(key: string, el: unknown): void {\n if (el instanceof HTMLElement) {\n rowEls.set(key, el)\n return\n }\n\n // Перестановка размонтирует старый узел уже после того, как встал новый:\n // снимаем запись, только если она протухла (приём из `GrTree`).\n const previous = rowEls.get(key)\n if (previous && !previous.isConnected)\n rowEls.delete(key)\n}\n\nconst roving = useRovingFocus<string>({\n items: () => keys.value,\n elementFor: key => rowEls.get(key) ?? null,\n orientation: () => (isVertical.value ? 'vertical' : 'horizontal'),\n wrap: () => false,\n})\n\nfunction applyMove(from: number, to: number): void {\n if (from < 0 || to < 0 || from === to)\n return\n\n const next = moveItem(props.modelValue, from, to)\n emit('update:modelValue', next)\n emit('move', from, to)\n emit('change', next)\n}\n\nconst sort = useDragSort<string, number>({\n items: () => keys.value,\n elementFor: key => rowEls.get(key) ?? null,\n orientation: () => props.orientation,\n scroller: () => (props.maxHeight === undefined ? null : listEl.value),\n disabled: () => props.disabled,\n resolveTarget: (hit, source) => insertionIndex(hit, indexOfKey(source), count.value),\n onDrop: (source, to) => applyMove(indexOfKey(source), to),\n onUpdate: (source, target, mode) => {\n // Объявляется только клавиатурный перенос: у мыши всё видно на экране, а\n // поток объявлений на каждом движении указателя делает диктор бесполезным.\n if (mode !== 'keyboard' || source === null)\n return\n\n const position = (target ?? indexOfKey(source)) + 1\n announce(t('gr.sortable.moved', 'Position {position} of {count}', { position, count: count.value }))\n },\n})\n\nconst activeIndex = computed(() => (sort.source.value === null ? -1 : indexOfKey(sort.source.value)))\n\n/** Куда встанет строка: индикатор рисуется на строке-соседе, а не отдельным узлом. */\nfunction indicatorFor(index: number): 'before' | 'after' | null {\n const target = sort.target.value\n if (target === null || !sort.isActive.value || index === activeIndex.value)\n return null\n\n if (target === index)\n return target < activeIndex.value || activeIndex.value === -1 ? 'before' : 'after'\n\n return null\n}\n\nfunction rowClass(index: number, key: string): string[] {\n const grabbed = sort.mode.value === 'keyboard' && sort.source.value === key\n const dragging = sort.mode.value === 'pointer' && sort.source.value === key\n const indicator = indicatorFor(index)\n\n return [\n rowLayoutClass,\n rowFocusClass,\n props.disabled ? rowDisabledClass : '',\n grabbed ? rowGrabbedClass : '',\n dragging ? rowDraggingClass : '',\n indicator === 'before' ? indicatorBeforeClass : '',\n indicator === 'after' ? indicatorAfterClass : '',\n ]\n}\n\nfunction startPointer(key: string, event: PointerEvent): void {\n if (props.disabled)\n return\n\n sort.startFrom(key)(event)\n}\n\nfunction grab(key: string): void {\n sort.grab(key)\n if (sort.mode.value !== 'keyboard')\n return\n\n // Цель ставится до объявления: `setTarget` сам объявляет позицию, и обратный\n // порядок стёр бы более полное сообщение о захвате.\n sort.setTarget(indexOfKey(key))\n announce(t('gr.sortable.grabbed', 'Grabbed. Position {position} of {count}', {\n position: indexOfKey(key) + 1,\n count: count.value,\n }))\n}\n\nasync function drop(key: string): Promise<void> {\n const target = sort.target.value\n sort.drop()\n\n announce(t('gr.sortable.dropped', 'Dropped at position {position} of {count}', {\n position: (target ?? 0) + 1,\n count: count.value,\n }))\n\n // Фокус остаётся на перенесённой строке: иначе после переноса он падает на\n // тело документа и клавиатурный сценарий обрывается на первом же шаге.\n await nextTick()\n await roving.focusKey(key)\n}\n\nfunction cancel(): void {\n if (!sort.isActive.value)\n return\n\n sort.cancel()\n announce(t('gr.sortable.cancelled', 'Move cancelled'))\n}\n\nfunction moveTargetBy(delta: number): void {\n const current = sort.target.value ?? activeIndex.value\n const next = Math.min(count.value - 1, Math.max(0, current + delta))\n\n if (next !== current)\n sort.setTarget(next)\n}\n\nfunction onRowKeydown(event: KeyboardEvent, key: string): void {\n if (props.disabled)\n return\n\n const forward = isVertical.value ? 'ArrowDown' : 'ArrowRight'\n const backward = isVertical.value ? 'ArrowUp' : 'ArrowLeft'\n const grabbed = sort.mode.value === 'keyboard'\n\n if (event.key === 'Escape') {\n if (!grabbed)\n return\n\n event.preventDefault()\n cancel()\n return\n }\n\n if (event.key === ' ' || event.key === 'Enter') {\n event.preventDefault()\n if (grabbed)\n void drop(key)\n else grab(key)\n return\n }\n\n if (grabbed) {\n // Взятая строка забирает стрелки себе: двигается она, а не фокус.\n if (event.key === forward || event.key === backward) {\n event.preventDefault()\n moveTargetBy(event.key === forward ? 1 : -1)\n }\n return\n }\n\n roving.handleNavigationKeys(event)\n}\n\n/** Фокус ушёл из списка — взятая строка не может остаться взятой навсегда. */\nfunction onFocusout(event: FocusEvent): void {\n if (sort.mode.value !== 'keyboard')\n return\n\n const next = event.relatedTarget as Node | null\n if (next && listEl.value?.contains(next))\n return\n\n cancel()\n}\n\nconst resolvedEmptyText = computed(() => props.emptyText ?? t('gr.sortable.empty', 'Nothing to sort yet'))\nconst resolvedLabel = computed(() => props.ariaLabel ?? t('gr.sortable.label', 'Sortable list'))\nconst handleLabel = computed(() => t('gr.sortable.handle', 'Reorder'))\n\nconst listStyle = computed(() => (props.maxHeight === undefined\n ? undefined\n : { maxHeight: typeof props.maxHeight === 'number' ? `${props.maxHeight}px` : props.maxHeight, overflow: 'auto' }))\n\ndefineExpose({\n /** Программная перестановка — та же, что делает перенос. */\n move: (from: number, to: number) => applyMove(from, to),\n focusItem: (index: number) => roving.focusKey(keys.value[index]),\n})\n\nif (__GR_DEV__) {\n watchEffect(() => {\n if (!Array.isArray(props.modelValue)) {\n console.warn(\n `[granularity] GrSortableList: обязательный проп \\`modelValue\\` должен быть массивом — получено ${String(props.modelValue)}.`,\n )\n }\n })\n}\n</script>\n\n<template>\n <GrCard :variant=\"variant\">\n <div\n ref=\"listEl\"\n role=\"list\"\n data-gr-sortable\n :aria-label=\"resolvedLabel\"\n :aria-disabled=\"disabled ? 'true' : undefined\"\n :class=\"[listClass[orientation], divided ? dividedClass : '']\"\n :style=\"listStyle\"\n @focusout=\"onFocusout\"\n >\n <div\n v-if=\"isEmpty\"\n data-gr-sortable-empty\n :class=\"emptyClass\"\n >\n <slot name=\"empty\">\n {{ resolvedEmptyText }}\n </slot>\n </div>\n\n <div\n v-for=\"(item, index) in modelValue\"\n v-else\n :key=\"keys[index]\"\n :ref=\"el => registerRow(keys[index], el)\"\n role=\"listitem\"\n data-gr-sortable-item\n :tabindex=\"disabled ? -1 : roving.tabindexFor(keys[index])\"\n :aria-roledescription=\"handleLabel\"\n :aria-posinset=\"index + 1\"\n :aria-setsize=\"count\"\n :aria-grabbed=\"sort.mode.value === 'keyboard' && sort.source.value === keys[index] ? 'true' : undefined\"\n :class=\"rowClass(index, keys[index])\"\n @keydown=\"onRowKeydown($event, keys[index])\"\n @focus=\"roving.setActive(keys[index])\"\n @pointerdown=\"handleOnly ? undefined : startPointer(keys[index], $event)\"\n >\n <button\n type=\"button\"\n tabindex=\"-1\"\n data-gr-sortable-handle\n :class=\"[handleClass, disabled ? handleDisabledClass : '']\"\n :aria-label=\"handleLabel\"\n :disabled=\"disabled\"\n @pointerdown=\"startPointer(keys[index], $event)\"\n @click.stop\n >\n <slot\n name=\"handle\"\n :item=\"item\"\n :index=\"index\"\n :disabled=\"disabled\"\n >\n <IconGripVertical class=\"block\" aria-hidden=\"true\" />\n </slot>\n </button>\n\n <div :class=\"contentClass\">\n <slot\n name=\"item\"\n :item=\"item\"\n :index=\"index\"\n :dragging=\"sort.mode.value === 'pointer' && sort.source.value === keys[index]\"\n :grabbed=\"sort.mode.value === 'keyboard' && sort.source.value === keys[index]\"\n >\n {{ item }}\n </slot>\n </div>\n </div>\n </div>\n </GrCard>\n</template>\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
1
  import { n as grSortableListSafelist, t as grSortableListConfig } from "../../chunks/config-CbEbLyDE.js";
2
- import { t as GrSortableList_default } from "./chunks/GrSortableList-ce1psLL0.js";
2
+ import { t as GrSortableList_default } from "./chunks/GrSortableList-NKriuGm_.js";
3
3
  export { GrSortableList_default as GrSortableList, GrSortableList_default as default, grSortableListConfig, grSortableListSafelist };
@@ -3,39 +3,10 @@ import { a as useGrComponentSize, i as useGrComponentProp } from "../../../chunk
3
3
  import { n as useGranularityTranslations } from "../../../chunks/granularityI18n-BC5zsj_d.js";
4
4
  import { useRovingFocus } from "../../../composables/useRovingFocus.js";
5
5
  import { n as iconTag, t as iconClass } from "../../../chunks/icon-Dag2tIMs.js";
6
+ import { t as resolveScrollOverflow } from "../../../chunks/scrollOverflow-B_kEdcol.js";
6
7
  import { a as grTabsEmptyClass, c as tabCloseIconSizes, i as grTabsCloseClass, l as tabContentClass, o as grTabsListClass, r as grTabsBadgeClass, s as grTabsTabClass, u as tabIconClass } from "../../../chunks/config-CMNfZO7S.js";
7
8
  import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, nextTick, normalizeClass, onBeforeUnmount, onMounted, openBlock, ref, renderList, renderSlot, resolveDynamicComponent, toDisplayString, unref, watch, watchEffect } from "vue";
8
- import '../styles.css';//#region src/components/shared/scrollOverflow.ts
9
- /**
10
- * Состояние переполнения скроллера — по трём числам, без DOM.
11
- *
12
- * Отдельным модулем, потому что в jsdom раскладки нет: `scrollWidth` и
13
- * `clientWidth` там нули, и через смонтированный компонент ни одна ветка не
14
- * проверяется. Здесь она проверяется вся.
15
- *
16
- * `scrollStart` — расстояние от начала ряда, а не `scrollLeft`. Это не
17
- * педантизм: в RTL современные браузеры отсчитывают `scrollLeft` от правого
18
- * края и уводят его в минус, поэтому физическое «слева» и логическое «в начале»
19
- * там не совпадают. Модуль знает только про начало и конец, а какой это край
20
- * экрана — дело CSS.
21
- *
22
- * Запас в пиксель — как в {@link titleWhenTruncated}: браузеры округляют
23
- * размеры вверх, и у ряда, влезающего целиком, разница бывает единичной.
24
- * Без запаса затухание висело бы на ровном месте.
25
- */
26
- function resolveScrollOverflow(scrollStart, scrollSize, clientSize) {
27
- const hidden = scrollSize - clientSize;
28
- if (hidden <= 1) return "none";
29
- const start = Math.abs(scrollStart);
30
- const atStart = start <= 1;
31
- const atEnd = hidden - start <= 1;
32
- if (atStart && atEnd) return "none";
33
- if (atStart) return "end";
34
- if (atEnd) return "start";
35
- return "both";
36
- }
37
- //#endregion
38
- //#region src/components/GrTabs/GrTabs.vue?vue&type=script&setup=true&lang.ts
9
+ import '../styles.css';//#region src/components/GrTabs/GrTabs.vue?vue&type=script&setup=true&lang.ts
39
10
  var _hoisted_1 = ["aria-orientation", "data-overflow"];
40
11
  var _hoisted_2 = [
41
12
  "id",