@feugene/granularity 0.18.0 → 0.19.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 (199) hide show
  1. package/dist/chunks/chevron-left-D8AUBAM7.js +23 -0
  2. package/dist/chunks/chevron-left-D8AUBAM7.js.map +1 -0
  3. package/dist/chunks/{chevron-right-BfqMVpbB.js → chevron-right-C6Z01h0X.js} +1 -22
  4. package/dist/chunks/chevron-right-C6Z01h0X.js.map +1 -0
  5. package/dist/chunks/config-8Pcutu1j2.js.map +1 -0
  6. package/dist/chunks/{config-K0C2tcLB.js → config-9PYRZlKE.js} +1 -1
  7. package/dist/chunks/{config-K0C2tcLB.js.map → config-9PYRZlKE.js.map} +1 -1
  8. package/dist/chunks/{config-DEvsMTQE.js → config-BJ7Bv_G2.js} +1 -3
  9. package/dist/chunks/config-BJ7Bv_G2.js.map +1 -0
  10. package/dist/chunks/config-BKvX0q102.js.map +1 -0
  11. package/dist/chunks/{config-gvq_hWNM.js → config-Be9k9bOf.js} +1 -7
  12. package/dist/chunks/config-Be9k9bOf.js.map +1 -0
  13. package/dist/chunks/{config-DpqOnJiz2.js → config-CDat1wXf2.js} +1 -1
  14. package/dist/chunks/config-CDat1wXf2.js.map +1 -0
  15. package/dist/chunks/config-CY0_6cMi2.js.map +1 -0
  16. package/dist/chunks/config-D94Z9po52.js.map +1 -0
  17. package/dist/chunks/{config-D2aNP7Es.js → config-DAX8Deh1.js} +1 -3
  18. package/dist/chunks/config-DAX8Deh1.js.map +1 -0
  19. package/dist/chunks/config-DCFvmHEv.js.map +1 -0
  20. package/dist/chunks/config-DKdlqXiY.js.map +1 -1
  21. package/dist/chunks/config-Rw9OyW702.js.map +1 -0
  22. package/dist/chunks/generated-BRkbhyxk.js +3270 -0
  23. package/dist/chunks/generated-BRkbhyxk.js.map +1 -0
  24. package/dist/chunks/{granular-provider-B9nXQCMu.js → granular-provider-C6uAujdJ.js} +12 -12
  25. package/dist/chunks/{granular-provider-B9nXQCMu.js.map → granular-provider-C6uAujdJ.js.map} +1 -1
  26. package/dist/chunks/grip-vertical-lRha2S7q.js +16 -0
  27. package/dist/chunks/grip-vertical-lRha2S7q.js.map +1 -0
  28. package/dist/chunks/icon-Dag2tIMs.js +21 -0
  29. package/dist/chunks/icon-Dag2tIMs.js.map +1 -0
  30. package/dist/chunks/{portalRoot-CWD4zHBm.js → portalRoot-CVhCgdG6.js} +10 -1
  31. package/dist/chunks/{portalRoot-CWD4zHBm.js.map → portalRoot-CVhCgdG6.js.map} +1 -1
  32. package/dist/chunks/{useModalOverlay-BTq-0ImG.js → useModalOverlay-DrQVYwwA.js} +1 -1
  33. package/dist/chunks/{useModalOverlay-BTq-0ImG.js.map → useModalOverlay-DrQVYwwA.js.map} +1 -1
  34. package/dist/chunks/{usePortalTarget-RyzEgNTb.js → usePortalTarget-Cn-Wiz5d.js} +1 -1
  35. package/dist/chunks/{usePortalTarget-RyzEgNTb.js.map → usePortalTarget-Cn-Wiz5d.js.map} +1 -1
  36. package/dist/components/GrAutocomplete/chunks/{GrAutocomplete-CPEQxmLZ.js → GrAutocomplete-gY4Xopza.js} +32 -24
  37. package/dist/components/GrAutocomplete/chunks/GrAutocomplete-gY4Xopza.js.map +1 -0
  38. package/dist/components/GrAutocomplete/index.js +1 -1
  39. package/dist/components/GrAutocomplete/styles.css +1 -1
  40. package/dist/components/GrBreadcrumbs/chunks/{GrBreadcrumbs-BCNGDS6k.js → GrBreadcrumbs-DS_-PBVt.js} +8 -7
  41. package/dist/components/GrBreadcrumbs/chunks/GrBreadcrumbs-DS_-PBVt.js.map +1 -0
  42. package/dist/components/GrBreadcrumbs/index.js +1 -1
  43. package/dist/components/GrCollapse/chunks/{GrCollapse-B2Fc6A9r.js.map → GrCollapse-Kk0DE8tb.js.map} +1 -1
  44. package/dist/components/GrCollapse/index.js +1 -1
  45. package/dist/components/GrColorPicker/chunks/{GrColorPicker-DAdQ-Gc3.js → GrColorPicker-CkBTzkqn.js} +1 -1
  46. package/dist/components/GrColorPicker/chunks/{GrColorPicker-DAdQ-Gc3.js.map → GrColorPicker-CkBTzkqn.js.map} +1 -1
  47. package/dist/components/GrColorPicker/index.js +1 -1
  48. package/dist/components/GrCommandPalette/chunks/{GrCommandPalette-DMeSqkEv.js → GrCommandPalette-BugTlGdj.js} +37 -11
  49. package/dist/components/GrCommandPalette/chunks/GrCommandPalette-BugTlGdj.js.map +1 -0
  50. package/dist/components/GrCommandPalette/index.js +2 -2
  51. package/dist/components/GrCommandPalette/styles.css +1 -1
  52. package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-hUTi-GdJ.js → GrConfirmDialog-Ds5T0GiR.js} +1 -1
  53. package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-hUTi-GdJ.js.map → GrConfirmDialog-Ds5T0GiR.js.map} +1 -1
  54. package/dist/components/GrConfirmDialog/index.js +1 -1
  55. package/dist/components/GrDataTable/chunks/{GrDataTable-DzaGLG3A.js → GrDataTable-CYe2A2WA.js} +9 -7
  56. package/dist/components/GrDataTable/chunks/GrDataTable-CYe2A2WA.js.map +1 -0
  57. package/dist/components/GrDataTable/index.js +2 -2
  58. package/dist/components/GrDialog/chunks/{GrDialog-MD9Fzy9C.js → GrDialog-C3tYaA8v.js} +1 -1
  59. package/dist/components/GrDialog/chunks/{GrDialog-MD9Fzy9C.js.map → GrDialog-C3tYaA8v.js.map} +1 -1
  60. package/dist/components/GrDialog/index.js +1 -1
  61. package/dist/components/GrDialogService/chunks/{GrDialogService-DtgUijMj.js → GrDialogService-CTBiiJDI.js} +3 -3
  62. package/dist/components/GrDialogService/chunks/{GrDialogService-DtgUijMj.js.map → GrDialogService-CTBiiJDI.js.map} +1 -1
  63. package/dist/components/GrDialogService/index.js +1 -1
  64. package/dist/components/GrDivider/chunks/{GrDivider-B2Xwc_QO.js → GrDivider-xxq1_fIl.js} +1 -1
  65. package/dist/components/GrDivider/chunks/{GrDivider-B2Xwc_QO.js.map → GrDivider-xxq1_fIl.js.map} +1 -1
  66. package/dist/components/GrDivider/index.js +2 -2
  67. package/dist/components/GrDrawer/chunks/{GrDrawer-DWHdZZzW.js → GrDrawer-BSSuiNHU.js} +2 -2
  68. package/dist/components/GrDrawer/chunks/{GrDrawer-DWHdZZzW.js.map → GrDrawer-BSSuiNHU.js.map} +1 -1
  69. package/dist/components/GrDrawer/index.js +2 -2
  70. package/dist/components/GrDropdown/chunks/{GrDropdown-0tJK01Kq.js → GrDropdown-Bh6UK2M5.js} +1 -1
  71. package/dist/components/GrDropdown/chunks/{GrDropdown-0tJK01Kq.js.map → GrDropdown-Bh6UK2M5.js.map} +1 -1
  72. package/dist/components/GrDropdown/index.js +1 -1
  73. package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenu-VPPrpCoA.js → GrDropdownMenu-LyfEFUbg.js} +2 -2
  74. package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenu-VPPrpCoA.js.map → GrDropdownMenu-LyfEFUbg.js.map} +1 -1
  75. package/dist/components/GrDropdownMenu/index.js +2 -2
  76. package/dist/components/GrImageViewer/chunks/{GrImageViewer-DE7MBbF8.js → GrImageViewer-DfD4euH5.js} +3 -2
  77. package/dist/components/GrImageViewer/chunks/{GrImageViewer-DE7MBbF8.js.map → GrImageViewer-DfD4euH5.js.map} +1 -1
  78. package/dist/components/GrImageViewer/index.js +1 -1
  79. package/dist/components/GrModal/chunks/{GrModal-BbhcsDQ8.js → GrModal-CbMMr6W1.js} +1 -1
  80. package/dist/components/GrModal/chunks/{GrModal-BbhcsDQ8.js.map → GrModal-CbMMr6W1.js.map} +1 -1
  81. package/dist/components/GrModal/index.js +1 -1
  82. package/dist/components/GrNumberInput/chunks/{GrNumberInput-CZOyPz4J.js → GrNumberInput-B5CHZDBj.js} +2 -1
  83. package/dist/components/GrNumberInput/chunks/{GrNumberInput-CZOyPz4J.js.map → GrNumberInput-B5CHZDBj.js.map} +1 -1
  84. package/dist/components/GrNumberInput/index.js +1 -1
  85. package/dist/components/GrPagination/chunks/{GrPagination-waKhxBP7.js → GrPagination-BM4FrD6Q.js} +1 -1
  86. package/dist/components/GrPagination/chunks/{GrPagination-waKhxBP7.js.map → GrPagination-BM4FrD6Q.js.map} +1 -1
  87. package/dist/components/GrPagination/index.js +1 -1
  88. package/dist/components/GrPopover/chunks/{GrPopover-DuqeoFaI.js → GrPopover-DMH4Jjst.js} +1 -1
  89. package/dist/components/GrPopover/chunks/{GrPopover-DuqeoFaI.js.map → GrPopover-DMH4Jjst.js.map} +1 -1
  90. package/dist/components/GrPopover/index.js +1 -1
  91. package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-JVVqBsPY.js → GrPromptDialog-Pwr9NPxK.js} +1 -1
  92. package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-JVVqBsPY.js.map → GrPromptDialog-Pwr9NPxK.js.map} +1 -1
  93. package/dist/components/GrPromptDialog/index.js +1 -1
  94. package/dist/components/GrRating/chunks/{GrRating-LarIiwJL.js → GrRating-DVFH3lYu.js} +8 -7
  95. package/dist/components/GrRating/chunks/GrRating-DVFH3lYu.js.map +1 -0
  96. package/dist/components/GrRating/index.js +1 -1
  97. package/dist/components/GrSelect/chunks/{GrSelect-DAdu044K.js → GrSelect-B-Voni7D.js} +65 -52
  98. package/dist/components/GrSelect/chunks/GrSelect-B-Voni7D.js.map +1 -0
  99. package/dist/components/GrSelect/index.js +1 -1
  100. package/dist/components/GrSelect/styles.css +1 -1
  101. package/dist/components/GrSidebar/chunks/{GrSidebar-DRGnnViw.js → GrSidebar-i7AxjhY_.js} +3 -2
  102. package/dist/components/GrSidebar/chunks/{GrSidebar-DRGnnViw.js.map → GrSidebar-i7AxjhY_.js.map} +1 -1
  103. package/dist/components/GrSidebar/index.js +2 -2
  104. package/dist/components/GrSkeleton/index.js +1 -1
  105. package/dist/components/GrSortableList/chunks/{GrSortableList-BefVfWKh.js → GrSortableList-XB7mXM1W.js} +6 -5
  106. package/dist/components/GrSortableList/chunks/GrSortableList-XB7mXM1W.js.map +1 -0
  107. package/dist/components/GrSortableList/index.js +2 -2
  108. package/dist/components/GrStatistic/chunks/{GrStatistic-J4qLs3TH.js → GrStatistic-DOsM6s5V.js} +54 -7
  109. package/dist/components/GrStatistic/chunks/GrStatistic-DOsM6s5V.js.map +1 -0
  110. package/dist/components/GrStatistic/index.js +2 -2
  111. package/dist/components/GrTabs/chunks/{GrTabs-TeXaU5Uh.js → GrTabs-f3-oQenu.js} +5 -4
  112. package/dist/components/GrTabs/chunks/GrTabs-f3-oQenu.js.map +1 -0
  113. package/dist/components/GrTabs/index.js +1 -1
  114. package/dist/components/GrToaster/chunks/{GrToaster-Ct-39hga.js → GrToaster-Dqja0Lf9.js} +1 -1
  115. package/dist/components/GrToaster/chunks/{GrToaster-Ct-39hga.js.map → GrToaster-Dqja0Lf9.js.map} +1 -1
  116. package/dist/components/GrToaster/index.js +1 -1
  117. package/dist/components/GrTooltip/chunks/{GrTooltip-xAcqs0bP.js → GrTooltip-Cn08heWz.js} +1 -1
  118. package/dist/components/GrTooltip/chunks/{GrTooltip-xAcqs0bP.js.map → GrTooltip-Cn08heWz.js.map} +1 -1
  119. package/dist/components/GrTooltip/index.js +1 -1
  120. package/dist/components/GrTree/chunks/{GrTree-CaYK8dGZ.js → GrTree-BR9a2pay.js} +25 -20
  121. package/dist/components/GrTree/chunks/GrTree-BR9a2pay.js.map +1 -0
  122. package/dist/components/GrTree/index.js +2 -2
  123. package/dist/components/GrTree/styles.css +1 -1
  124. package/dist/components/GrTreeSelect/chunks/{GrTreeSelect-DZp7CqqW.js → GrTreeSelect--muUeB0v.js} +27 -13
  125. package/dist/components/GrTreeSelect/chunks/GrTreeSelect--muUeB0v.js.map +1 -0
  126. package/dist/components/GrTreeSelect/index.js +2 -2
  127. package/dist/composables/useAnnouncer.js +1 -1
  128. package/dist/composables/usePortalTarget.js +1 -1
  129. package/dist/directives/loading.js +1 -1
  130. package/dist/granular-provider-node.js +1 -1
  131. package/dist/granular-provider.js +1 -1
  132. package/dist/index.js +41 -41
  133. package/dist/styles/themes/dark.css +6 -1
  134. package/dist/testing.js +199 -0
  135. package/dist/testing.js.map +1 -0
  136. package/dist/theme-apply.js +33 -0
  137. package/dist/theme-apply.js.map +1 -0
  138. package/dist/theme.js +501 -0
  139. package/dist/theme.js.map +1 -0
  140. package/dist/tokens.js +1 -3215
  141. package/dist/types/src/components/GrBreadcrumbs/grBreadcrumbsStyles.d.ts +5 -2
  142. package/dist/types/src/components/GrCommandPalette/GrCommandPalette.vue.d.ts +6 -6
  143. package/dist/types/src/components/GrCommandPalette/filtering.d.ts +6 -2
  144. package/dist/types/src/components/GrDataTable/grDataTableStyles.d.ts +0 -1
  145. package/dist/types/src/components/GrRating/GrRating.vue.d.ts +9 -4
  146. package/dist/types/src/components/GrSortableList/grSortableListStyles.d.ts +0 -1
  147. package/dist/types/src/components/GrStatistic/GrStatistic.vue.d.ts +12 -9
  148. package/dist/types/src/components/GrStatistic/grStatisticStyles.d.ts +0 -1
  149. package/dist/types/src/components/GrTabs/GrTabs.vue.d.ts +6 -2
  150. package/dist/types/src/components/GrTree/grTreeProps.d.ts +11 -4
  151. package/dist/types/src/components/GrTree/safelist.d.ts +1 -1
  152. package/dist/types/src/components/shared/icon.d.ts +13 -0
  153. package/dist/types/src/testing/announcer.d.ts +10 -0
  154. package/dist/types/src/testing/context.d.ts +36 -0
  155. package/dist/types/src/testing/env.d.ts +25 -0
  156. package/dist/types/src/testing/geometry.d.ts +23 -0
  157. package/dist/types/src/testing/index.d.ts +12 -0
  158. package/dist/types/src/testing/pointer.d.ts +31 -0
  159. package/dist/types/src/theme/apply.d.ts +27 -0
  160. package/dist/types/src/theme/color.d.ts +35 -0
  161. package/dist/types/src/theme/extendTheme.d.ts +63 -0
  162. package/dist/types/src/theme/index.d.ts +22 -0
  163. package/dist/types/src/theme/roles.d.ts +16 -0
  164. package/dist/types/src/theme/tone.d.ts +32 -0
  165. package/dist/types/src/theme/validate.d.ts +16 -0
  166. package/dist/types/src/tokens/types.d.ts +8 -0
  167. package/dist/web-types.json +14 -11
  168. package/package.json +17 -2
  169. package/tokens/themes/dark.json +2 -1
  170. package/dist/chunks/chevron-right-BfqMVpbB.js.map +0 -1
  171. package/dist/chunks/config-8Pcutu1j.js.map +0 -1
  172. package/dist/chunks/config-BKvX0q10.js.map +0 -1
  173. package/dist/chunks/config-CY0_6cMi.js.map +0 -1
  174. package/dist/chunks/config-D2aNP7Es.js.map +0 -1
  175. package/dist/chunks/config-D94Z9po5.js.map +0 -1
  176. package/dist/chunks/config-DCFvmHEv2.js.map +0 -1
  177. package/dist/chunks/config-DEvsMTQE.js.map +0 -1
  178. package/dist/chunks/config-DpqOnJiz2.js.map +0 -1
  179. package/dist/chunks/config-Rw9OyW70.js.map +0 -1
  180. package/dist/chunks/config-gvq_hWNM.js.map +0 -1
  181. package/dist/components/GrAutocomplete/chunks/GrAutocomplete-CPEQxmLZ.js.map +0 -1
  182. package/dist/components/GrBreadcrumbs/chunks/GrBreadcrumbs-BCNGDS6k.js.map +0 -1
  183. package/dist/components/GrCommandPalette/chunks/GrCommandPalette-DMeSqkEv.js.map +0 -1
  184. package/dist/components/GrDataTable/chunks/GrDataTable-DzaGLG3A.js.map +0 -1
  185. package/dist/components/GrRating/chunks/GrRating-LarIiwJL.js.map +0 -1
  186. package/dist/components/GrSelect/chunks/GrSelect-DAdu044K.js.map +0 -1
  187. package/dist/components/GrSortableList/chunks/GrSortableList-BefVfWKh.js.map +0 -1
  188. package/dist/components/GrStatistic/chunks/GrStatistic-J4qLs3TH.js.map +0 -1
  189. package/dist/components/GrTabs/chunks/GrTabs-TeXaU5Uh.js.map +0 -1
  190. package/dist/components/GrTree/chunks/GrTree-CaYK8dGZ.js.map +0 -1
  191. package/dist/components/GrTreeSelect/chunks/GrTreeSelect-DZp7CqqW.js.map +0 -1
  192. package/dist/tokens.js.map +0 -1
  193. package/dist/chunks/{config-8Pcutu1j.js → config-8Pcutu1j2.js} +0 -0
  194. package/dist/chunks/{config-BKvX0q10.js → config-BKvX0q102.js} +0 -0
  195. package/dist/chunks/{config-CY0_6cMi.js → config-CY0_6cMi2.js} +0 -0
  196. package/dist/chunks/{config-D94Z9po5.js → config-D94Z9po52.js} +0 -0
  197. package/dist/chunks/{config-DCFvmHEv2.js → config-DCFvmHEv.js} +0 -0
  198. package/dist/chunks/{config-Rw9OyW70.js → config-Rw9OyW702.js} +0 -0
  199. package/dist/components/GrCollapse/chunks/{GrCollapse-B2Fc6A9r.js → GrCollapse-Kk0DE8tb.js} +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GrTabs-f3-oQenu.js","names":[],"sources":["../../../../src/components/GrTabs/GrTabs.vue","../../../../src/components/GrTabs/GrTabs.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, ref, watch, type Component } from 'vue'\n\nimport IconX from '~icons/lucide/x'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { useRovingFocus } from '../../composables/useRovingFocus'\nimport { iconClass, iconTag } from '../shared/icon'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport {\n grTabsBadgeClass,\n grTabsCloseClass,\n grTabsEmptyClass,\n grTabsListClass,\n grTabsTabClass,\n tabCloseIconSizes,\n tabContentClass,\n tabIconClass,\n type GrTabsOrientation,\n type GrTabsSize,\n type GrTabsVariant,\n} from './grTabsStyles'\n\nexport interface GrTab {\n value: string\n label: string\n badge?: string\n /**\n * Иконка слева от подписи: Vue-компонент либо класс иконки вашей UnoCSS-сборки\n * (`'i-lucide-user'` — тогда нужен ваш `presetIcons`, см. `docs/installation.md`).\n */\n icon?: string | Component\n disabled?: boolean\n /** Перекрывает `closable` ряда в обе стороны: закрепить вкладку или закрыть одну её. */\n closable?: boolean\n}\n\nexport interface GrTabsProps {\n modelValue: string\n tabs: GrTab[]\n /**\n * База id для ARIA-связки с `GrTabPanels`. Если задана, каждая вкладка\n * получает `id=\"<idBase>-tab-<value>\"` и `aria-controls=\"<idBase>-panel-<value>\"`.\n * Передайте тот же `idBase` в `GrTabPanels`, чтобы связать `tab`↔`tabpanel`.\n */\n idBase?: string\n size?: GrTabsSize\n /** Вид ряда: обойма с таблетками или ряд с подчёркиванием. */\n variant?: GrTabsVariant\n /**\n * `automatic` (по умолчанию) — стрелка сразу переключает вкладку;\n * `manual` — стрелка двигает только фокус, выбор подтверждается\n * `Enter`/`Space`. Второй режим для вкладок с тяжёлой загрузкой: перебор\n * стрелками иначе тянет каждую панель.\n */\n activationMode?: GrTabsActivationMode\n /** Горизонтальный (по умолчанию) или вертикальный список вкладок. */\n orientation?: GrTabsOrientation\n /**\n * Крестик на вкладках и закрытие по `Delete`/`Backspace`. Точечно снимается\n * `closable: false` у самой вкладки.\n */\n closable?: boolean\n /** Текст, когда список вкладок пуст. Слот `#empty` сильнее. */\n emptyText?: string\n}\n\nexport interface GrTabsEmits {\n (e: 'update:modelValue', value: string): void\n /**\n * Просьба закрыть вкладку. Компонент список не трогает: `tabs` — проп, и\n * закрытие может не состояться («сохранить изменения?»).\n */\n (e: 'close', value: string): void\n}\n\nexport type GrTabsActivationMode = 'automatic' | 'manual'\n\nexport type { GrTabsOrientation, GrTabsVariant } from './grTabsStyles'\n\n/**\n * GrTabs — горизонтальная группа вкладок с паттерном WAI-ARIA `tablist`.\n *\n * A11y:\n * - `role=\"tablist\"` на корне, `role=\"tab\"` на каждой вкладке.\n * - Roving `tabindex`: активная вкладка `0`, остальные `-1`.\n * - Клавиатура: `ArrowLeft`/`ArrowRight` — цикличный переход, `Home`/`End` — к первой/последней.\n * - При переключении стрелками DOM-фокус программно переносится на новую вкладку.\n *\n * Сам компонент не рендерит `tabpanel` — это ответственность консьюмера (по `aria-controls`/внешней разметке).\n */\nconst props = withDefaults(defineProps<GrTabsProps>(), {\n idBase: undefined,\n size: undefined,\n // Дефолт живёт в резолвере: Vue подставил бы свой раньше, чем компонент\n // заглянет в `GrConfigProvider`.\n variant: undefined,\n activationMode: 'automatic',\n orientation: 'horizontal',\n closable: false,\n emptyText: undefined,\n})\n\ndefineSlots<{\n /** Содержимое вкладки целиком — вместо подписи, иконки и счётчика. Крестик остаётся. */\n tab?: (props: { tab: GrTab, active: boolean, disabled: boolean }) => unknown\n /** Пустой ряд — вместо текста из локали. */\n empty?: () => unknown\n}>()\n\nconst emit = defineEmits<GrTabsEmits>()\n\nconst { t } = useGranularityTranslations()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrTabs' })\nconst resolvedVariant = useGrComponentProp('GrTabs', 'variant', () => props.variant, 'pills')\n\nconst tablistClass = computed(() => grTabsListClass({\n variant: resolvedVariant.value,\n size: resolvedSize.value,\n orientation: props.orientation,\n}))\n\nfunction tabClass(tab: GrTab): string {\n return grTabsTabClass({\n variant: resolvedVariant.value,\n size: resolvedSize.value,\n active: tab.value === props.modelValue,\n disabled: Boolean(tab.disabled),\n })\n}\n\nconst badgeClass = computed(() => grTabsBadgeClass(resolvedSize.value))\nconst closeClass = computed(() => grTabsCloseClass(resolvedSize.value))\nconst closeIconClass = computed(() => tabCloseIconSizes[resolvedSize.value])\nconst emptyClass = computed(() => grTabsEmptyClass(resolvedSize.value))\n\nconst isEmpty = computed(() => props.tabs.length === 0)\nconst resolvedEmptyText = computed(() => props.emptyText ?? t('gr.tabs.empty', 'Nothing here yet'))\nconst closeTitle = computed(() => t('gr.tabs.close', 'Close tab'))\n\n/** Отключённая вкладка не принимает взаимодействия, а закрытие — тоже взаимодействие. */\nfunction isClosable(tab: GrTab): boolean {\n return !tab.disabled && (tab.closable ?? props.closable)\n}\n\nconst buttonRefs = ref<HTMLButtonElement[]>([])\n\n/**\n * Vue зовёт ref-функцию с `null`, когда узел исчез. Игнорировать такой вызов\n * нельзя: в массиве оставались отсоединённые от DOM кнопки, и `focus()` по\n * сократившемуся списку молча проваливался в `<body>`.\n */\nfunction setButtonRef(el: unknown, index: number): void {\n if (el instanceof HTMLButtonElement)\n buttonRefs.value[index] = el\n else\n delete buttonRefs.value[index]\n\n if (buttonRefs.value.length > props.tabs.length)\n buttonRefs.value.length = props.tabs.length\n}\n\nconst activeIndex = computed(() => props.tabs.findIndex(t => t.value === props.modelValue))\n\nfunction isEnabled(tab: GrTab): boolean {\n return !tab.disabled\n}\n\nconst tabIndexes = computed(() => props.tabs.map((_, index) => index))\n\n/**\n * Кольцо roving-фокуса. При `activationMode: 'manual'` фокус уезжает вперёд\n * выбора — это и есть разница между `activeKey` примитива и `initialKey`:\n * первый ведёт фокус, второй подставляет выбранную вкладку, пока фокус не\n * трогали.\n *\n * Выбор стрелка **не** переносит: этим заведует `onKeydown` по\n * `activationMode`. Через `onMove` это выразить нельзя — клик тоже зовёт\n * `focusKey`, и выбор эмитился бы дважды.\n */\nconst roving = useRovingFocus<number>({\n items: () => tabIndexes.value,\n elementFor: index => buttonRefs.value[index],\n isDisabled: index => Boolean(props.tabs[index]?.disabled),\n orientation: () => (props.orientation === 'vertical' ? 'vertical' : 'horizontal'),\n skipDisabled: () => true,\n /**\n * Пока фокус не трогали, остановку `Tab` держит выбранная вкладка. Если\n * `modelValue` не совпал ни с одной (пустое начальное значение, асинхронный\n * список, удалённая активная вкладка), примитив ставит её на первую\n * доступную — иначе tablist выпал бы из обхода целиком и молча.\n *\n * Выключенность здесь не проверяется намеренно: на кнопке `aria-disabled`, а\n * не нативный `disabled`, поэтому выключенная вкладка фокусируется и держать\n * остановку вправе.\n */\n initialKey: () => (activeIndex.value >= 0 ? activeIndex.value : undefined),\n /**\n * Прокрутка до фокуса, а не после: иначе браузер доскроллит сам по-своему, и\n * ряд дёрнется дважды. Синхронно — фокус обязан остаться в том же такте, что\n * и раньше: тик, где он нужен, добавляет `focusIndex`.\n */\n beforeFocus: index => scrollIntoView(index),\n})\n\n/**\n * Фокус после перерисовки — для путей, где список только что изменился\n * (закрытие вкладки). Клавиатура идёт через примитив напрямую: там ряд уже\n * отрисован, и лишний тик только отложил бы фокус.\n */\nasync function focusIndex(index: number): Promise<void> {\n await nextTick()\n await roving.focusKey(index)\n}\n\n/**\n * Ряд прокручивается, а не переносится, поэтому активная вкладка может уехать\n * за край — например, когда её выбрали снаружи. `scrollIntoView` нет в jsdom и\n * в старых движках, поэтому вызов необязательный.\n */\nfunction scrollIntoView(index: number): void {\n buttonRefs.value[index]?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' })\n}\n\nwatch(() => props.modelValue, async () => {\n await nextTick()\n if (activeIndex.value >= 0)\n scrollIntoView(activeIndex.value)\n})\n\nasync function selectByIndex(index: number, focus = false): Promise<void> {\n const tab = props.tabs[index]\n if (!tab || !isEnabled(tab))\n return\n if (tab.value !== props.modelValue)\n emit('update:modelValue', tab.value)\n if (focus)\n await focusIndex(index)\n}\n\nfunction onKeydown(event: KeyboardEvent): void {\n if (props.tabs.length === 0)\n return\n\n const currentIndex = roving.rovingKey.value ?? 0\n\n switch (event.key) {\n case 'Enter':\n case ' ':\n // В ручном режиме выбор подтверждается явно — на той вкладке, где фокус.\n if (props.activationMode === 'manual') {\n event.preventDefault()\n void selectByIndex(currentIndex, false)\n }\n return\n case 'Delete':\n case 'Backspace': {\n // Единственный клавиатурный путь к закрытию: крестик в таб-порядке не стоит.\n const tab = props.tabs[currentIndex]\n if (tab && isClosable(tab)) {\n event.preventDefault()\n requestClose(currentIndex)\n }\n return\n }\n }\n\n // Стрелки, `Home`, `End` — фокус ведёт примитив по объявленной ориентации.\n if (!roving.handleNavigationKeys(event))\n return\n\n // `automatic`: выбор едет за фокусом. `manual`: ждёт `Enter`/`Space`.\n if (props.activationMode !== 'manual')\n void selectByIndex(roving.rovingKey.value ?? currentIndex, false)\n}\n\n/**\n * Закрытая вкладка, которая держала фокус. Компонент список не укорачивает —\n * это делает потребитель, — поэтому фокус возвращается только после того, как\n * `tabs` действительно стал короче.\n */\nlet pendingClose: { index: number, value: string } | null = null\n\nfunction requestClose(index: number): void {\n const tab = props.tabs[index]\n if (!tab || !isClosable(tab))\n return\n\n const held = buttonRefs.value[index] === document.activeElement\n pendingClose = held ? { index, value: tab.value } : null\n\n emit('close', tab.value)\n}\n\n// Без этого клавиатурное закрытие роняет фокус в `<body>`: кнопка, на которой\n// он стоял, исчезает из DOM вместе со вкладкой.\nwatch(() => props.tabs.length, async (next, prev) => {\n const pending = pendingClose\n pendingClose = null\n\n if (!pending || next >= prev)\n return\n // Ушла не та вкладка — потребитель поменял список по своей причине.\n if (props.tabs.some(tab => tab.value === pending.value))\n return\n\n await nextTick()\n\n const target = Math.min(pending.index, next - 1)\n if (target >= 0)\n void focusIndex(target)\n else\n roving.reset()\n})\n\nfunction onClick(event: MouseEvent, tab: GrTab, index: number): void {\n if (!isEnabled(tab))\n return\n\n // Крестик — не кнопка, поэтому его клик приходит сюда же и разбирается по цели.\n if (event.target instanceof Element && event.target.closest('[data-gr-tab-close]')) {\n requestClose(index)\n return\n }\n\n roving.setActive(index)\n\n if (tab.value !== props.modelValue)\n emit('update:modelValue', tab.value)\n}\n</script>\n\n<template>\n <!--\n Пустой ряд — не пустой `tablist`: роль обязана владеть потомками `tab`,\n и текст внутри неё был бы нарушением, а не пустым состоянием.\n -->\n <div v-if=\"isEmpty\" data-gr-tabs-empty :class=\"emptyClass\">\n <slot name=\"empty\">\n {{ resolvedEmptyText }}\n </slot>\n </div>\n\n <div\n v-else\n role=\"tablist\"\n data-gr-tabs\n :aria-orientation=\"orientation\"\n :class=\"tablistClass\"\n @keydown=\"onKeydown\"\n >\n <button\n v-for=\"(tab, index) in tabs\"\n :id=\"idBase ? `${idBase}-tab-${tab.value}` : undefined\"\n :key=\"tab.value\"\n :ref=\"el => setButtonRef(el, index)\"\n type=\"button\"\n role=\"tab\"\n data-gr-tab\n :aria-controls=\"idBase ? `${idBase}-panel-${tab.value}` : undefined\"\n :aria-selected=\"tab.value === modelValue ? 'true' : 'false'\"\n :aria-disabled=\"tab.disabled ? 'true' : undefined\"\n :aria-keyshortcuts=\"isClosable(tab) ? 'Delete' : undefined\"\n :tabindex=\"roving.tabindexFor(index)\"\n :class=\"tabClass(tab)\"\n @click=\"onClick($event, tab, index)\"\n >\n <span :class=\"tabContentClass\">\n <slot\n name=\"tab\"\n :tab=\"tab\"\n :active=\"tab.value === modelValue\"\n :disabled=\"Boolean(tab.disabled)\"\n >\n <component\n :is=\"iconTag(tab.icon)\"\n v-if=\"tab.icon\"\n :class=\"[tabIconClass, iconClass(tab.icon)]\"\n aria-hidden=\"true\"\n />\n <span>{{ tab.label }}</span>\n <span v-if=\"tab.badge\" :class=\"badgeClass\">\n {{ tab.badge }}\n </span>\n </slot>\n\n <!-- Снаружи слота: своя разметка вкладки не должна отнимать закрытие. -->\n <span\n v-if=\"isClosable(tab)\"\n data-gr-tab-close\n :class=\"closeClass\"\n :title=\"closeTitle\"\n aria-hidden=\"true\"\n >\n <IconX :class=\"closeIconClass\" />\n </span>\n </span>\n </button>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, nextTick, ref, watch, type Component } from 'vue'\n\nimport IconX from '~icons/lucide/x'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { useRovingFocus } from '../../composables/useRovingFocus'\nimport { iconClass, iconTag } from '../shared/icon'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport {\n grTabsBadgeClass,\n grTabsCloseClass,\n grTabsEmptyClass,\n grTabsListClass,\n grTabsTabClass,\n tabCloseIconSizes,\n tabContentClass,\n tabIconClass,\n type GrTabsOrientation,\n type GrTabsSize,\n type GrTabsVariant,\n} from './grTabsStyles'\n\nexport interface GrTab {\n value: string\n label: string\n badge?: string\n /**\n * Иконка слева от подписи: Vue-компонент либо класс иконки вашей UnoCSS-сборки\n * (`'i-lucide-user'` — тогда нужен ваш `presetIcons`, см. `docs/installation.md`).\n */\n icon?: string | Component\n disabled?: boolean\n /** Перекрывает `closable` ряда в обе стороны: закрепить вкладку или закрыть одну её. */\n closable?: boolean\n}\n\nexport interface GrTabsProps {\n modelValue: string\n tabs: GrTab[]\n /**\n * База id для ARIA-связки с `GrTabPanels`. Если задана, каждая вкладка\n * получает `id=\"<idBase>-tab-<value>\"` и `aria-controls=\"<idBase>-panel-<value>\"`.\n * Передайте тот же `idBase` в `GrTabPanels`, чтобы связать `tab`↔`tabpanel`.\n */\n idBase?: string\n size?: GrTabsSize\n /** Вид ряда: обойма с таблетками или ряд с подчёркиванием. */\n variant?: GrTabsVariant\n /**\n * `automatic` (по умолчанию) — стрелка сразу переключает вкладку;\n * `manual` — стрелка двигает только фокус, выбор подтверждается\n * `Enter`/`Space`. Второй режим для вкладок с тяжёлой загрузкой: перебор\n * стрелками иначе тянет каждую панель.\n */\n activationMode?: GrTabsActivationMode\n /** Горизонтальный (по умолчанию) или вертикальный список вкладок. */\n orientation?: GrTabsOrientation\n /**\n * Крестик на вкладках и закрытие по `Delete`/`Backspace`. Точечно снимается\n * `closable: false` у самой вкладки.\n */\n closable?: boolean\n /** Текст, когда список вкладок пуст. Слот `#empty` сильнее. */\n emptyText?: string\n}\n\nexport interface GrTabsEmits {\n (e: 'update:modelValue', value: string): void\n /**\n * Просьба закрыть вкладку. Компонент список не трогает: `tabs` — проп, и\n * закрытие может не состояться («сохранить изменения?»).\n */\n (e: 'close', value: string): void\n}\n\nexport type GrTabsActivationMode = 'automatic' | 'manual'\n\nexport type { GrTabsOrientation, GrTabsVariant } from './grTabsStyles'\n\n/**\n * GrTabs — горизонтальная группа вкладок с паттерном WAI-ARIA `tablist`.\n *\n * A11y:\n * - `role=\"tablist\"` на корне, `role=\"tab\"` на каждой вкладке.\n * - Roving `tabindex`: активная вкладка `0`, остальные `-1`.\n * - Клавиатура: `ArrowLeft`/`ArrowRight` — цикличный переход, `Home`/`End` — к первой/последней.\n * - При переключении стрелками DOM-фокус программно переносится на новую вкладку.\n *\n * Сам компонент не рендерит `tabpanel` — это ответственность консьюмера (по `aria-controls`/внешней разметке).\n */\nconst props = withDefaults(defineProps<GrTabsProps>(), {\n idBase: undefined,\n size: undefined,\n // Дефолт живёт в резолвере: Vue подставил бы свой раньше, чем компонент\n // заглянет в `GrConfigProvider`.\n variant: undefined,\n activationMode: 'automatic',\n orientation: 'horizontal',\n closable: false,\n emptyText: undefined,\n})\n\ndefineSlots<{\n /** Содержимое вкладки целиком — вместо подписи, иконки и счётчика. Крестик остаётся. */\n tab?: (props: { tab: GrTab, active: boolean, disabled: boolean }) => unknown\n /** Пустой ряд — вместо текста из локали. */\n empty?: () => unknown\n}>()\n\nconst emit = defineEmits<GrTabsEmits>()\n\nconst { t } = useGranularityTranslations()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrTabs' })\nconst resolvedVariant = useGrComponentProp('GrTabs', 'variant', () => props.variant, 'pills')\n\nconst tablistClass = computed(() => grTabsListClass({\n variant: resolvedVariant.value,\n size: resolvedSize.value,\n orientation: props.orientation,\n}))\n\nfunction tabClass(tab: GrTab): string {\n return grTabsTabClass({\n variant: resolvedVariant.value,\n size: resolvedSize.value,\n active: tab.value === props.modelValue,\n disabled: Boolean(tab.disabled),\n })\n}\n\nconst badgeClass = computed(() => grTabsBadgeClass(resolvedSize.value))\nconst closeClass = computed(() => grTabsCloseClass(resolvedSize.value))\nconst closeIconClass = computed(() => tabCloseIconSizes[resolvedSize.value])\nconst emptyClass = computed(() => grTabsEmptyClass(resolvedSize.value))\n\nconst isEmpty = computed(() => props.tabs.length === 0)\nconst resolvedEmptyText = computed(() => props.emptyText ?? t('gr.tabs.empty', 'Nothing here yet'))\nconst closeTitle = computed(() => t('gr.tabs.close', 'Close tab'))\n\n/** Отключённая вкладка не принимает взаимодействия, а закрытие — тоже взаимодействие. */\nfunction isClosable(tab: GrTab): boolean {\n return !tab.disabled && (tab.closable ?? props.closable)\n}\n\nconst buttonRefs = ref<HTMLButtonElement[]>([])\n\n/**\n * Vue зовёт ref-функцию с `null`, когда узел исчез. Игнорировать такой вызов\n * нельзя: в массиве оставались отсоединённые от DOM кнопки, и `focus()` по\n * сократившемуся списку молча проваливался в `<body>`.\n */\nfunction setButtonRef(el: unknown, index: number): void {\n if (el instanceof HTMLButtonElement)\n buttonRefs.value[index] = el\n else\n delete buttonRefs.value[index]\n\n if (buttonRefs.value.length > props.tabs.length)\n buttonRefs.value.length = props.tabs.length\n}\n\nconst activeIndex = computed(() => props.tabs.findIndex(t => t.value === props.modelValue))\n\nfunction isEnabled(tab: GrTab): boolean {\n return !tab.disabled\n}\n\nconst tabIndexes = computed(() => props.tabs.map((_, index) => index))\n\n/**\n * Кольцо roving-фокуса. При `activationMode: 'manual'` фокус уезжает вперёд\n * выбора — это и есть разница между `activeKey` примитива и `initialKey`:\n * первый ведёт фокус, второй подставляет выбранную вкладку, пока фокус не\n * трогали.\n *\n * Выбор стрелка **не** переносит: этим заведует `onKeydown` по\n * `activationMode`. Через `onMove` это выразить нельзя — клик тоже зовёт\n * `focusKey`, и выбор эмитился бы дважды.\n */\nconst roving = useRovingFocus<number>({\n items: () => tabIndexes.value,\n elementFor: index => buttonRefs.value[index],\n isDisabled: index => Boolean(props.tabs[index]?.disabled),\n orientation: () => (props.orientation === 'vertical' ? 'vertical' : 'horizontal'),\n skipDisabled: () => true,\n /**\n * Пока фокус не трогали, остановку `Tab` держит выбранная вкладка. Если\n * `modelValue` не совпал ни с одной (пустое начальное значение, асинхронный\n * список, удалённая активная вкладка), примитив ставит её на первую\n * доступную — иначе tablist выпал бы из обхода целиком и молча.\n *\n * Выключенность здесь не проверяется намеренно: на кнопке `aria-disabled`, а\n * не нативный `disabled`, поэтому выключенная вкладка фокусируется и держать\n * остановку вправе.\n */\n initialKey: () => (activeIndex.value >= 0 ? activeIndex.value : undefined),\n /**\n * Прокрутка до фокуса, а не после: иначе браузер доскроллит сам по-своему, и\n * ряд дёрнется дважды. Синхронно — фокус обязан остаться в том же такте, что\n * и раньше: тик, где он нужен, добавляет `focusIndex`.\n */\n beforeFocus: index => scrollIntoView(index),\n})\n\n/**\n * Фокус после перерисовки — для путей, где список только что изменился\n * (закрытие вкладки). Клавиатура идёт через примитив напрямую: там ряд уже\n * отрисован, и лишний тик только отложил бы фокус.\n */\nasync function focusIndex(index: number): Promise<void> {\n await nextTick()\n await roving.focusKey(index)\n}\n\n/**\n * Ряд прокручивается, а не переносится, поэтому активная вкладка может уехать\n * за край — например, когда её выбрали снаружи. `scrollIntoView` нет в jsdom и\n * в старых движках, поэтому вызов необязательный.\n */\nfunction scrollIntoView(index: number): void {\n buttonRefs.value[index]?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' })\n}\n\nwatch(() => props.modelValue, async () => {\n await nextTick()\n if (activeIndex.value >= 0)\n scrollIntoView(activeIndex.value)\n})\n\nasync function selectByIndex(index: number, focus = false): Promise<void> {\n const tab = props.tabs[index]\n if (!tab || !isEnabled(tab))\n return\n if (tab.value !== props.modelValue)\n emit('update:modelValue', tab.value)\n if (focus)\n await focusIndex(index)\n}\n\nfunction onKeydown(event: KeyboardEvent): void {\n if (props.tabs.length === 0)\n return\n\n const currentIndex = roving.rovingKey.value ?? 0\n\n switch (event.key) {\n case 'Enter':\n case ' ':\n // В ручном режиме выбор подтверждается явно — на той вкладке, где фокус.\n if (props.activationMode === 'manual') {\n event.preventDefault()\n void selectByIndex(currentIndex, false)\n }\n return\n case 'Delete':\n case 'Backspace': {\n // Единственный клавиатурный путь к закрытию: крестик в таб-порядке не стоит.\n const tab = props.tabs[currentIndex]\n if (tab && isClosable(tab)) {\n event.preventDefault()\n requestClose(currentIndex)\n }\n return\n }\n }\n\n // Стрелки, `Home`, `End` — фокус ведёт примитив по объявленной ориентации.\n if (!roving.handleNavigationKeys(event))\n return\n\n // `automatic`: выбор едет за фокусом. `manual`: ждёт `Enter`/`Space`.\n if (props.activationMode !== 'manual')\n void selectByIndex(roving.rovingKey.value ?? currentIndex, false)\n}\n\n/**\n * Закрытая вкладка, которая держала фокус. Компонент список не укорачивает —\n * это делает потребитель, — поэтому фокус возвращается только после того, как\n * `tabs` действительно стал короче.\n */\nlet pendingClose: { index: number, value: string } | null = null\n\nfunction requestClose(index: number): void {\n const tab = props.tabs[index]\n if (!tab || !isClosable(tab))\n return\n\n const held = buttonRefs.value[index] === document.activeElement\n pendingClose = held ? { index, value: tab.value } : null\n\n emit('close', tab.value)\n}\n\n// Без этого клавиатурное закрытие роняет фокус в `<body>`: кнопка, на которой\n// он стоял, исчезает из DOM вместе со вкладкой.\nwatch(() => props.tabs.length, async (next, prev) => {\n const pending = pendingClose\n pendingClose = null\n\n if (!pending || next >= prev)\n return\n // Ушла не та вкладка — потребитель поменял список по своей причине.\n if (props.tabs.some(tab => tab.value === pending.value))\n return\n\n await nextTick()\n\n const target = Math.min(pending.index, next - 1)\n if (target >= 0)\n void focusIndex(target)\n else\n roving.reset()\n})\n\nfunction onClick(event: MouseEvent, tab: GrTab, index: number): void {\n if (!isEnabled(tab))\n return\n\n // Крестик — не кнопка, поэтому его клик приходит сюда же и разбирается по цели.\n if (event.target instanceof Element && event.target.closest('[data-gr-tab-close]')) {\n requestClose(index)\n return\n }\n\n roving.setActive(index)\n\n if (tab.value !== props.modelValue)\n emit('update:modelValue', tab.value)\n}\n</script>\n\n<template>\n <!--\n Пустой ряд — не пустой `tablist`: роль обязана владеть потомками `tab`,\n и текст внутри неё был бы нарушением, а не пустым состоянием.\n -->\n <div v-if=\"isEmpty\" data-gr-tabs-empty :class=\"emptyClass\">\n <slot name=\"empty\">\n {{ resolvedEmptyText }}\n </slot>\n </div>\n\n <div\n v-else\n role=\"tablist\"\n data-gr-tabs\n :aria-orientation=\"orientation\"\n :class=\"tablistClass\"\n @keydown=\"onKeydown\"\n >\n <button\n v-for=\"(tab, index) in tabs\"\n :id=\"idBase ? `${idBase}-tab-${tab.value}` : undefined\"\n :key=\"tab.value\"\n :ref=\"el => setButtonRef(el, index)\"\n type=\"button\"\n role=\"tab\"\n data-gr-tab\n :aria-controls=\"idBase ? `${idBase}-panel-${tab.value}` : undefined\"\n :aria-selected=\"tab.value === modelValue ? 'true' : 'false'\"\n :aria-disabled=\"tab.disabled ? 'true' : undefined\"\n :aria-keyshortcuts=\"isClosable(tab) ? 'Delete' : undefined\"\n :tabindex=\"roving.tabindexFor(index)\"\n :class=\"tabClass(tab)\"\n @click=\"onClick($event, tab, index)\"\n >\n <span :class=\"tabContentClass\">\n <slot\n name=\"tab\"\n :tab=\"tab\"\n :active=\"tab.value === modelValue\"\n :disabled=\"Boolean(tab.disabled)\"\n >\n <component\n :is=\"iconTag(tab.icon)\"\n v-if=\"tab.icon\"\n :class=\"[tabIconClass, iconClass(tab.icon)]\"\n aria-hidden=\"true\"\n />\n <span>{{ tab.label }}</span>\n <span v-if=\"tab.badge\" :class=\"badgeClass\">\n {{ tab.badge }}\n </span>\n </slot>\n\n <!-- Снаружи слота: своя разметка вкладки не должна отнимать закрытие. -->\n <span\n v-if=\"isClosable(tab)\"\n data-gr-tab-close\n :class=\"closeClass\"\n :title=\"closeTitle\"\n aria-hidden=\"true\"\n >\n <IconX :class=\"closeIconClass\" />\n </span>\n </span>\n </button>\n </div>\n</template>\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
1
  import { n as grTabsSafelist, r as GR_TABS_VARIANTS, t as grTabsConfig } from "../../chunks/config-_qPUfb6-2.js";
2
- import { t as GrTabs_default } from "./chunks/GrTabs-TeXaU5Uh.js";
2
+ import { t as GrTabs_default } from "./chunks/GrTabs-f3-oQenu.js";
3
3
  export { GR_TABS_VARIANTS, GrTabs_default as GrTabs, GrTabs_default as default, grTabsConfig, grTabsSafelist };
@@ -3,7 +3,7 @@ import { t as info_default } from "../../../chunks/info-Be2OccTu.js";
3
3
  import { t as x_default } from "../../../chunks/x-2KXnqxiD.js";
4
4
  import { c as useGrThemeAttrs } from "../../../chunks/context-BSYR7RZs.js";
5
5
  import { n as useGranularityTranslations } from "../../../chunks/granularityI18n-BC5zsj_d.js";
6
- import { t as usePortalTarget } from "../../../chunks/usePortalTarget-RyzEgNTb.js";
6
+ import { t as usePortalTarget } from "../../../chunks/usePortalTarget-Cn-Wiz5d.js";
7
7
  import { vHotkey } from "../../../directives/hotkey.js";
8
8
  import { t as GrIcon_default } from "../../GrIcon/chunks/GrIcon-BvA2ZGXP.js";
9
9
  import { t as _plugin_vue_export_helper_default } from "../../../chunks/_plugin-vue_export-helper-BOaGB7Aw.js";
@@ -1 +1 @@
1
- {"version":3,"file":"GrToaster-Ct-39hga.js","names":[],"sources":["../../../../src/components/GrToaster/GrToaster.vue","../../../../src/components/GrToaster/GrToaster.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * GrToaster — GR-примитив области уведомлений (toast).\n *\n * Модель состояния берётся из `useToast`, поэтому в приложении предполагается\n * **один активный `GrToaster`** на корень (или app-scoped через плагин).\n *\n * A11y:\n * - контейнер — `role=\"region\"` с `aria-label` (i18n-проп `regionLabel`);\n * - объявляет каждый тост сам собой: `role=\"status\"`, а критичные\n * (`warning`/`danger`) — ассертивный `role=\"alert\"`. Обёртки-live-region над\n * ними нет намеренно: вложение регионов с разной ассертивностью\n * спецификацией не определено, браузеры и SR расходятся вплоть до потери\n * объявления;\n * - стек достижим с клавиатуры по `focusHotkey` (по умолчанию `F6`) — тосты\n * телепортированы в конец `body`, и без хоткея кнопка «Отменить» лежала бы\n * за пределами разумного числа нажатий `Tab`.\n *\n * UX:\n * - `maxVisible` ограничивает стек; лишние ждут в очереди (их таймеры на паузе);\n * - автозакрытие **останавливается под курсором/фокусом** (WCAG 2.2.1) и\n * визуализируется прогресс-баром до закрытия;\n * - `placement` настраивает угол экрана; слой — `--gr-z-toast`.\n */\nimport { computed, nextTick, ref, useSlots, watchEffect } from 'vue'\n\nimport { usePortalTarget } from '../../composables/usePortalTarget'\nimport type { Component } from 'vue'\n\nimport { useDragGesture } from '../../composables/useDragGesture'\nimport { useToast } from '../../composables/useToast'\nimport { vHotkey } from '../../directives'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport type { GrToastTone, Toast, ToastAction } from '../../composables/useToast'\nimport GrButton from '../GrButton'\nimport GrIcon from '../GrIcon'\nimport {\n PLACEMENT_CLASS,\n SWIPE_DIRECTION,\n SWIPE_FLY_OUT_PX,\n SWIPE_RESISTANCE,\n SWIPE_THRESHOLD_MIN_PX,\n SWIPE_THRESHOLD_RATIO,\n swipeOpacity,\n type GrToasterPlacement,\n} from './grToasterStyles'\n\nimport IconCheck from '~icons/lucide/check-circle'\nimport IconWarning from '~icons/lucide/alert-triangle'\nimport IconError from '~icons/lucide/x-circle'\nimport IconInfo from '~icons/lucide/info'\nimport IconClose from '~icons/lucide/x'\n\nexport type { GrToasterPlacement }\n\ninterface ToneMeta {\n icon: Component\n color: string\n role: 'status' | 'alert'\n}\n\n// Единый маппинг по tone'у: иконка, цвет, a11y-роль. На уровне модуля —\n// чтобы не пересоздавать объект на каждый рендер.\nconst TONE_META: Record<GrToastTone, ToneMeta> = {\n primary: { icon: IconInfo, color: 'var(--gr-primary)', role: 'status' },\n neutral: { icon: IconInfo, color: 'var(--gr-muted-fg)', role: 'status' },\n info: { icon: IconInfo, color: 'var(--gr-info)', role: 'status' },\n success: { icon: IconCheck, color: 'var(--gr-success)', role: 'status' },\n warning: { icon: IconWarning, color: 'var(--gr-warning)', role: 'alert' },\n danger: { icon: IconError, color: 'var(--gr-danger)', role: 'alert' },\n slate: { icon: IconInfo, color: 'var(--gr-slate)', role: 'status' },\n azure: { icon: IconInfo, color: 'var(--gr-azure)', role: 'status' },\n}\n\nexport interface GrToasterProps {\n /** Угол экрана для стека уведомлений. */\n placement?: GrToasterPlacement\n /** Максимум одновременно видимых тостов; остальные ждут в очереди. По умолчанию `4`. */\n maxVisible?: number\n /** A11y-лейбл кнопки закрытия (i18n). */\n dismissLabel?: string\n /** A11y-лейбл контейнера-региона (i18n). */\n regionLabel?: string\n /**\n * Ширина стека. Число — пиксели, строка — любая CSS-длина. Уезжает в\n * `--gr-toaster-width`, поэтому то же самое можно задать и темой.\n */\n width?: string | number\n /**\n * Клавиша перевода фокуса в стек уведомлений; `false` отключает хоткей.\n * `F6` — рекомендация APG для перехода между «регионами» страницы.\n */\n focusHotkey?: string | false\n /**\n * Смахивание тоста к своему краю экрана. Клавиатурный эквивалент — `Delete`\n * и `Backspace` на сфокусированном тосте — остаётся и при выключенном жесте.\n */\n swipeDismiss?: boolean\n}\n\nimport { useGrThemeAttrs } from '../GrConfigProvider/context'\n\nconst props = withDefaults(defineProps<GrToasterProps>(), {\n placement: 'top-right',\n maxVisible: 4,\n dismissLabel: undefined,\n regionLabel: undefined,\n width: undefined,\n focusHotkey: 'F6',\n swipeDismiss: true,\n})\n\n/**\n * Слот `actions` полностью заменяет дефолтные action-кнопки тоста. Получает сам\n * `toast` и `dismiss` — функцию, закрывающую именно этот тост.\n */\ndefineSlots<{\n actions?: (props: { toast: Toast, dismiss: () => void }) => unknown\n}>()\n\nconst { t } = useGranularityTranslations()\nconst slots = useSlots()\nconst resolvedDismissLabel = computed(() => props.dismissLabel ?? t('gr.toaster.dismiss', 'Dismiss'))\nconst resolvedRegionLabel = computed(() => props.regionLabel ?? t('gr.toaster.region', 'Notifications'))\n\nconst { list, dismiss, pause, resume } = useToast()\n\n// Единый список кнопок тоста: `action` (шорткат) + массив `actions`.\nfunction toastActions(toast: Toast): ToastAction[] {\n return [...(toast.action ? [toast.action] : []), ...(toast.actions ?? [])]\n}\n\n// Показываем блок действий, если есть кнопки или задан слот `actions`.\nfunction hasActions(toast: Toast): boolean {\n return Boolean(slots.actions) || toastActions(toast).length > 0\n}\n\n// Клик по action-кнопке тоста: вызываем обработчик и (по умолчанию) закрываем тост.\nfunction onAction(toast: Toast, action: ToastAction): void {\n action.onClick()\n if (action.dismissOnClick !== false)\n dismiss(toast.id)\n}\n\n// SSR-guard: на сервере `document.body` недоступен — отключаем `teleport`.\n// Телепорт включается только ПОСЛЕ монтирования: иначе первый клиентский\n// рендер не совпадает с серверным и ломается гидрация (см. композабл).\nconst { target: portalTarget, enabled: teleportEnabled } = usePortalTarget()\n\n// Тема поддерева на телепортированную панель: в DOM она уезжает в `body`, то\n// есть вне обёртки провайдера, и `data-theme` с неё не наследуется. В дереве\n// компонентов панель остаётся внутри — `inject` доходит, и тему она ставит себе\n// сама.\nconst themeAttrs = useGrThemeAttrs()\n\n// Видимые тосты (не больше `maxVisible`); остальные ждут в очереди. Список\n// хранится новейшими вперёд, поэтому «видимые дожинают, новые ждут» — это\n// хвост: старейшие N. Взяв голову, стек показывал бы новейшие, и каждый пуш\n// вытеснял бы с экрана тост, который пользователь ещё читает.\nconst visibleToasts = computed(() => list.value.slice(-Math.max(1, props.maxVisible)))\nconst visibleIds = computed(() => new Set(visibleToasts.value.map(toast => toast.id)))\n\n// Пауза под курсором/фокусом (WCAG 2.2.1). Флага два и они независимы: один\n// `paused` терялся на «фокус внутри + мышь прошла насквозь» — `mouseleave`\n// возобновлял отсчёт под клавиатурным фокусом.\nconst hovered = ref(false)\nconst focusWithin = ref(false)\n\n// Контейнер стека: по нему ищутся тосты — и для фокуса, и для жеста.\nconst containerEl = ref<HTMLElement | null>(null)\n\n// ————— Смахивание.\n\nconst swipingId = ref<string | null>(null)\nconst swipeOffset = ref(0)\nlet swipeStartX = 0\nlet swipeThreshold = SWIPE_THRESHOLD_MIN_PX\n\n// Тост, который тянут, тоже держит таймеры: иначе уведомление сгорало бы прямо\n// под пальцем, посреди жеста.\nconst paused = computed(() => hovered.value || focusWithin.value || swipingId.value !== null)\n\nfunction toastElement(id: string): HTMLElement | null {\n return containerEl.value?.querySelector<HTMLElement>(`[data-gr-toast][data-toast-id=\"${id}\"]`) ?? null\n}\n\n/** Жест отпущен и тост закрывается: переход снова разрешён (см. `releaseSwiped`). */\nconst swipeReleased = ref(false)\n\nfunction resetSwipe(): void {\n swipingId.value = null\n swipeOffset.value = 0\n swipeReleased.value = false\n}\n\n/**\n * Отпущенный тост уезжает за свой край и только потом закрывается.\n *\n * Два кадра здесь обязательны, и оба по одной причине: уходящий элемент Vue\n * больше не перерисовывает — он уезжает с тем стилем, что был на последнем\n * рендере. Значит и разрешение перехода, и конечную точку нужно успеть отдать\n * до закрытия: первый кадр снимает инлайновый `transition: none` (во время\n * жеста он нужен, иначе переходы группы борются с пальцем), второй задаёт точку\n * отлёта — с этого момента анимация уже идёт, и `leave` подхватывает её на ходу.\n *\n * Закрыв тост сразу, получили бы не отлёт, а мгновенное исчезновение: элемент\n * ушёл бы в `leave` с запрещённым переходом.\n */\nasync function releaseSwiped(id: string): Promise<void> {\n swipeReleased.value = true\n await nextTick()\n\n swipeOffset.value = SWIPE_DIRECTION[props.placement] * SWIPE_FLY_OUT_PX\n await nextTick()\n\n dismiss(id)\n resetSwipe()\n}\n\n/**\n * Движение к своему краю идёт один к одному, в обратную сторону — с\n * сопротивлением: тост едет за пальцем, но закрыть его «наоборот» нельзя.\n */\nfunction resistOffset(delta: number): number {\n const toEdge = SWIPE_DIRECTION[props.placement]\n\n return Math.sign(delta) === toEdge ? delta : delta / SWIPE_RESISTANCE\n}\n\nconst swipe = useDragGesture({\n disabled: () => !props.swipeDismiss,\n onStart: (event) => {\n const target = event.target as HTMLElement | null\n // Нажатие на кнопке — это нажатие на кнопке: закрытие и действия тоста не\n // должны превращаться в протяжку.\n if (target?.closest('button, a[href]')) return false\n\n const host = target?.closest<HTMLElement>('[data-gr-toast]')\n const id = host?.dataset.toastId\n if (!host || !id) return false\n\n swipingId.value = id\n swipeOffset.value = 0\n swipeStartX = event.clientX\n swipeThreshold = Math.max(SWIPE_THRESHOLD_MIN_PX, host.getBoundingClientRect().width * SWIPE_THRESHOLD_RATIO)\n },\n onMove: (event) => {\n // Против выделения текста тоста во время протяжки.\n event.preventDefault()\n swipeOffset.value = resistOffset(event.clientX - swipeStartX)\n },\n onEnd: () => {\n const id = swipingId.value\n const passed = id !== null\n && Math.sign(swipeOffset.value) === SWIPE_DIRECTION[props.placement]\n && Math.abs(swipeOffset.value) >= swipeThreshold\n\n if (passed) void releaseSwiped(id)\n else resetSwipe()\n },\n // Обрыв возвращает тост на место, а не дожимает смахивание: указатель забрал\n // браузер, а не пользователь довёл жест.\n onCancel: resetSwipe,\n})\n\nfunction swipeStyle(toast: Toast): Record<string, string> | undefined {\n if (swipingId.value !== toast.id) return undefined\n\n return {\n transform: `translateX(${swipeOffset.value}px)`,\n opacity: String(swipeOpacity(swipeOffset.value, swipeThreshold)),\n // Переходы группы во время жеста борются с пальцем: тост едет с задержкой\n // и «догоняет» курсор после отпускания. После отпускания запрет снимается —\n // уходящему тосту переход нужен, иначе он не уедет вовсе.\n ...(swipeReleased.value ? {} : { transition: 'none' }),\n }\n}\n\n/**\n * Клавиатурный эквивалент смахивания. `Escape` намеренно не берём: тостер не\n * модальный слой, и перехват закрывал бы уведомление вместо диалога под ним.\n */\nfunction onToastKeydown(event: KeyboardEvent, toast: Toast): void {\n if (event.key !== 'Delete' && event.key !== 'Backspace') return\n\n event.preventDefault()\n\n const index = visibleToasts.value.findIndex(item => item.id === toast.id)\n const next = visibleToasts.value[index + 1] ?? visibleToasts.value[index - 1]\n\n dismiss(toast.id)\n\n // Фокус не должен упасть на `body`: дальше по стеку есть что читать.\n if (next) void nextTick(() => toastElement(next.id)?.focus())\n}\n\n// Единый источник правды по таймерам: тост тикает, только если он видим И стек не на\n// паузе. Очередь и hover — на паузе. Реагирует на изменения списка/hover.\n//\n// `timeoutMs` читается ради зависимости, а не ради значения: `toast.promise` и\n// `update` перезапускают таймер уже существующего тоста, не трогая ни список, ни\n// паузу. Без этой зависимости эффект не просыпался бы, и заново заведённый\n// таймер тикал бы под курсором или у тоста, который ещё ждёт в очереди, — тот\n// сгорал бы, ни разу не показавшись.\nwatchEffect(() => {\n for (const toast of list.value) {\n const shouldTick = toast.timeoutMs > 0 && visibleIds.value.has(toast.id) && !paused.value\n\n if (shouldTick)\n resume(toast.id)\n else\n pause(toast.id)\n }\n})\n\nfunction metaFor(tone: GrToastTone): ToneMeta {\n return TONE_META[tone] ?? TONE_META.info\n}\n\nconst containerClass = computed(() => PLACEMENT_CLASS[props.placement])\n\nconst containerStyle = computed(() => {\n if (props.width === undefined) return undefined\n\n return { '--gr-toaster-width': typeof props.width === 'number' ? `${props.width}px` : props.width }\n})\n\n\n/**\n * Переводит фокус на верхний тост. Тост фокусируем только программно\n * (`tabindex=\"-1\"`): собственной остановкой `Tab` стек быть не должен, иначе\n * всплывающее уведомление перехватывало бы обход страницы.\n */\nfunction focus(): boolean {\n const target = containerEl.value?.querySelector<HTMLElement>('[data-gr-toast]')\n if (!target) return false\n\n target.focus()\n return true\n}\n\nconst hotkeyBinding = computed(() => ({\n enabled: props.focusHotkey !== false && visibleToasts.value.length > 0,\n handlers: { [props.focusHotkey || 'F6']: { handler: onFocusHotkey, allowInEditable: true } },\n}))\n\nfunction onFocusHotkey(event: KeyboardEvent): void {\n if (focus()) event.preventDefault()\n}\n\ndefineExpose({ focus })\n</script>\n\n<template>\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <div\n ref=\"containerEl\"\n v-hotkey=\"hotkeyBinding\"\n v-bind=\"themeAttrs\"\n data-gr-toaster\n data-gr-overlay-root\n role=\"region\"\n :aria-label=\"resolvedRegionLabel\"\n class=\"fixed z-[var(--gr-z-toast)] w-[var(--gr-toaster-width,360px)] max-w-[calc(100vw-2rem)]\"\n :class=\"containerClass\"\n :style=\"containerStyle\"\n @mouseenter=\"hovered = true\"\n @mouseleave=\"hovered = false\"\n @focusin=\"focusWithin = true\"\n @focusout=\"focusWithin = false\"\n >\n <TransitionGroup\n tag=\"div\"\n class=\"grid gap-3\"\n enter-active-class=\"transition duration-[var(--gr-duration-base)] ease-[var(--gr-ease-out)]\"\n enter-from-class=\"opacity-0 translate-y-2\"\n enter-to-class=\"opacity-100 translate-y-0\"\n leave-active-class=\"transition duration-[var(--gr-duration-fast)] ease-[var(--gr-ease-in)] absolute\"\n leave-from-class=\"opacity-100 translate-y-0\"\n leave-to-class=\"opacity-0 translate-y-2\"\n move-class=\"transition-transform duration-[var(--gr-duration-base)] ease-[var(--gr-ease-out)]\"\n >\n <div\n v-for=\"toast in visibleToasts\"\n :key=\"toast.id\"\n data-gr-toast\n :data-tone=\"toast.tone\"\n :data-toast-id=\"toast.id\"\n :role=\"metaFor(toast.tone).role\"\n aria-atomic=\"true\"\n tabindex=\"-1\"\n class=\"relative overflow-hidden rounded-[var(--gr-radius-lg)] border border-[var(--gr-brd)] bg-[var(--gr-card)] px-4 py-3 shadow-[var(--gr-shadow-2)] focus-visible:outline-none focus-visible:shadow-[var(--gr-shadow-2),0_0_0_2px_var(--gr-ring)]\"\n :class=\"swipeDismiss ? '[touch-action:pan-y]' : ''\"\n :style=\"swipeStyle(toast)\"\n @pointerdown=\"swipe.start\"\n @keydown=\"onToastKeydown($event, toast)\"\n >\n <div class=\"flex items-start gap-3\">\n <GrIcon size=\"md\" class=\"mt-0.5\" :style=\"{ color: metaFor(toast.tone).color }\">\n <component :is=\"metaFor(toast.tone).icon\" />\n </GrIcon>\n <div class=\"min-w-0 flex-1\">\n <div class=\"text-[length:var(--gr-text-sm)] font-700\">\n {{ toast.title }}\n </div>\n <div v-if=\"toast.message\" class=\"mt-0.5 text-[length:var(--gr-text-sm)] text-[var(--gr-muted-fg)]\">\n {{ toast.message }}\n </div>\n\n <!-- Action-кнопки тоста: например «Отменить»/«Повторить». Можно\n задать массивом (`actions`) или переопределить слотом. -->\n <div v-if=\"hasActions(toast)\" class=\"mt-2 flex flex-wrap gap-2\">\n <slot name=\"actions\" :toast=\"toast\" :dismiss=\"() => dismiss(toast.id)\">\n <GrButton\n v-for=\"(action, index) in toastActions(toast)\"\n :key=\"index\"\n :variant=\"action.variant ?? 'outline'\"\n :size=\"action.size ?? 'sm'\"\n data-gr-toast-action\n @click=\"onAction(toast, action)\"\n >\n {{ action.label }}\n </GrButton>\n </slot>\n </div>\n </div>\n <GrButton\n variant=\"ghost\"\n size=\"sm\"\n square\n :aria-label=\"resolvedDismissLabel\"\n @click=\"dismiss(toast.id)\"\n >\n <GrIcon size=\"sm\">\n <IconClose />\n </GrIcon>\n </GrButton>\n </div>\n\n <!-- Прогресс до автозакрытия; замирает вместе с таймером на паузе. -->\n <div\n v-if=\"toast.timeoutMs > 0\"\n data-gr-toast-progress\n aria-hidden=\"true\"\n class=\"gr-toast-progress absolute bottom-0 left-0 h-[2px] w-full origin-left\"\n :style=\"{\n backgroundColor: metaFor(toast.tone).color,\n animationDuration: `${toast.timeoutMs}ms`,\n animationPlayState: paused ? 'paused' : 'running',\n }\"\n />\n </div>\n </TransitionGroup>\n </div>\n </teleport>\n</template>\n\n<style scoped>\n.gr-toast-progress {\n animation-name: gr-toast-progress;\n animation-timing-function: linear;\n animation-fill-mode: forwards;\n}\n\n@keyframes gr-toast-progress {\n from {\n transform: scaleX(1);\n }\n to {\n transform: scaleX(0);\n }\n}\n\n/*\n * Единственная анимация пакета, которую глобальный кламп чинит неверно.\n *\n * У полосы `animation-fill-mode: forwards`, поэтому сжатая до `0.01ms` она не\n * возвращается в исходное состояние, а фиксируется на `scaleX(0)`: таймер\n * выглядит истёкшим, пока тост ещё висит на экране. Полоса — единственный\n * носитель этого смысла, и статичной её оставить нельзя, поэтому под reduce она\n * убирается совсем. Автозакрытие ведёт JS-таймер, оно не страдает, а сам элемент\n * и так `aria-hidden`.\n */\n@media (prefers-reduced-motion: reduce) {\n .gr-toast-progress {\n display: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * GrToaster — GR-примитив области уведомлений (toast).\n *\n * Модель состояния берётся из `useToast`, поэтому в приложении предполагается\n * **один активный `GrToaster`** на корень (или app-scoped через плагин).\n *\n * A11y:\n * - контейнер — `role=\"region\"` с `aria-label` (i18n-проп `regionLabel`);\n * - объявляет каждый тост сам собой: `role=\"status\"`, а критичные\n * (`warning`/`danger`) — ассертивный `role=\"alert\"`. Обёртки-live-region над\n * ними нет намеренно: вложение регионов с разной ассертивностью\n * спецификацией не определено, браузеры и SR расходятся вплоть до потери\n * объявления;\n * - стек достижим с клавиатуры по `focusHotkey` (по умолчанию `F6`) — тосты\n * телепортированы в конец `body`, и без хоткея кнопка «Отменить» лежала бы\n * за пределами разумного числа нажатий `Tab`.\n *\n * UX:\n * - `maxVisible` ограничивает стек; лишние ждут в очереди (их таймеры на паузе);\n * - автозакрытие **останавливается под курсором/фокусом** (WCAG 2.2.1) и\n * визуализируется прогресс-баром до закрытия;\n * - `placement` настраивает угол экрана; слой — `--gr-z-toast`.\n */\nimport { computed, nextTick, ref, useSlots, watchEffect } from 'vue'\n\nimport { usePortalTarget } from '../../composables/usePortalTarget'\nimport type { Component } from 'vue'\n\nimport { useDragGesture } from '../../composables/useDragGesture'\nimport { useToast } from '../../composables/useToast'\nimport { vHotkey } from '../../directives'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport type { GrToastTone, Toast, ToastAction } from '../../composables/useToast'\nimport GrButton from '../GrButton'\nimport GrIcon from '../GrIcon'\nimport {\n PLACEMENT_CLASS,\n SWIPE_DIRECTION,\n SWIPE_FLY_OUT_PX,\n SWIPE_RESISTANCE,\n SWIPE_THRESHOLD_MIN_PX,\n SWIPE_THRESHOLD_RATIO,\n swipeOpacity,\n type GrToasterPlacement,\n} from './grToasterStyles'\n\nimport IconCheck from '~icons/lucide/check-circle'\nimport IconWarning from '~icons/lucide/alert-triangle'\nimport IconError from '~icons/lucide/x-circle'\nimport IconInfo from '~icons/lucide/info'\nimport IconClose from '~icons/lucide/x'\n\nexport type { GrToasterPlacement }\n\ninterface ToneMeta {\n icon: Component\n color: string\n role: 'status' | 'alert'\n}\n\n// Единый маппинг по tone'у: иконка, цвет, a11y-роль. На уровне модуля —\n// чтобы не пересоздавать объект на каждый рендер.\nconst TONE_META: Record<GrToastTone, ToneMeta> = {\n primary: { icon: IconInfo, color: 'var(--gr-primary)', role: 'status' },\n neutral: { icon: IconInfo, color: 'var(--gr-muted-fg)', role: 'status' },\n info: { icon: IconInfo, color: 'var(--gr-info)', role: 'status' },\n success: { icon: IconCheck, color: 'var(--gr-success)', role: 'status' },\n warning: { icon: IconWarning, color: 'var(--gr-warning)', role: 'alert' },\n danger: { icon: IconError, color: 'var(--gr-danger)', role: 'alert' },\n slate: { icon: IconInfo, color: 'var(--gr-slate)', role: 'status' },\n azure: { icon: IconInfo, color: 'var(--gr-azure)', role: 'status' },\n}\n\nexport interface GrToasterProps {\n /** Угол экрана для стека уведомлений. */\n placement?: GrToasterPlacement\n /** Максимум одновременно видимых тостов; остальные ждут в очереди. По умолчанию `4`. */\n maxVisible?: number\n /** A11y-лейбл кнопки закрытия (i18n). */\n dismissLabel?: string\n /** A11y-лейбл контейнера-региона (i18n). */\n regionLabel?: string\n /**\n * Ширина стека. Число — пиксели, строка — любая CSS-длина. Уезжает в\n * `--gr-toaster-width`, поэтому то же самое можно задать и темой.\n */\n width?: string | number\n /**\n * Клавиша перевода фокуса в стек уведомлений; `false` отключает хоткей.\n * `F6` — рекомендация APG для перехода между «регионами» страницы.\n */\n focusHotkey?: string | false\n /**\n * Смахивание тоста к своему краю экрана. Клавиатурный эквивалент — `Delete`\n * и `Backspace` на сфокусированном тосте — остаётся и при выключенном жесте.\n */\n swipeDismiss?: boolean\n}\n\nimport { useGrThemeAttrs } from '../GrConfigProvider/context'\n\nconst props = withDefaults(defineProps<GrToasterProps>(), {\n placement: 'top-right',\n maxVisible: 4,\n dismissLabel: undefined,\n regionLabel: undefined,\n width: undefined,\n focusHotkey: 'F6',\n swipeDismiss: true,\n})\n\n/**\n * Слот `actions` полностью заменяет дефолтные action-кнопки тоста. Получает сам\n * `toast` и `dismiss` — функцию, закрывающую именно этот тост.\n */\ndefineSlots<{\n actions?: (props: { toast: Toast, dismiss: () => void }) => unknown\n}>()\n\nconst { t } = useGranularityTranslations()\nconst slots = useSlots()\nconst resolvedDismissLabel = computed(() => props.dismissLabel ?? t('gr.toaster.dismiss', 'Dismiss'))\nconst resolvedRegionLabel = computed(() => props.regionLabel ?? t('gr.toaster.region', 'Notifications'))\n\nconst { list, dismiss, pause, resume } = useToast()\n\n// Единый список кнопок тоста: `action` (шорткат) + массив `actions`.\nfunction toastActions(toast: Toast): ToastAction[] {\n return [...(toast.action ? [toast.action] : []), ...(toast.actions ?? [])]\n}\n\n// Показываем блок действий, если есть кнопки или задан слот `actions`.\nfunction hasActions(toast: Toast): boolean {\n return Boolean(slots.actions) || toastActions(toast).length > 0\n}\n\n// Клик по action-кнопке тоста: вызываем обработчик и (по умолчанию) закрываем тост.\nfunction onAction(toast: Toast, action: ToastAction): void {\n action.onClick()\n if (action.dismissOnClick !== false)\n dismiss(toast.id)\n}\n\n// SSR-guard: на сервере `document.body` недоступен — отключаем `teleport`.\n// Телепорт включается только ПОСЛЕ монтирования: иначе первый клиентский\n// рендер не совпадает с серверным и ломается гидрация (см. композабл).\nconst { target: portalTarget, enabled: teleportEnabled } = usePortalTarget()\n\n// Тема поддерева на телепортированную панель: в DOM она уезжает в `body`, то\n// есть вне обёртки провайдера, и `data-theme` с неё не наследуется. В дереве\n// компонентов панель остаётся внутри — `inject` доходит, и тему она ставит себе\n// сама.\nconst themeAttrs = useGrThemeAttrs()\n\n// Видимые тосты (не больше `maxVisible`); остальные ждут в очереди. Список\n// хранится новейшими вперёд, поэтому «видимые дожинают, новые ждут» — это\n// хвост: старейшие N. Взяв голову, стек показывал бы новейшие, и каждый пуш\n// вытеснял бы с экрана тост, который пользователь ещё читает.\nconst visibleToasts = computed(() => list.value.slice(-Math.max(1, props.maxVisible)))\nconst visibleIds = computed(() => new Set(visibleToasts.value.map(toast => toast.id)))\n\n// Пауза под курсором/фокусом (WCAG 2.2.1). Флага два и они независимы: один\n// `paused` терялся на «фокус внутри + мышь прошла насквозь» — `mouseleave`\n// возобновлял отсчёт под клавиатурным фокусом.\nconst hovered = ref(false)\nconst focusWithin = ref(false)\n\n// Контейнер стека: по нему ищутся тосты — и для фокуса, и для жеста.\nconst containerEl = ref<HTMLElement | null>(null)\n\n// ————— Смахивание.\n\nconst swipingId = ref<string | null>(null)\nconst swipeOffset = ref(0)\nlet swipeStartX = 0\nlet swipeThreshold = SWIPE_THRESHOLD_MIN_PX\n\n// Тост, который тянут, тоже держит таймеры: иначе уведомление сгорало бы прямо\n// под пальцем, посреди жеста.\nconst paused = computed(() => hovered.value || focusWithin.value || swipingId.value !== null)\n\nfunction toastElement(id: string): HTMLElement | null {\n return containerEl.value?.querySelector<HTMLElement>(`[data-gr-toast][data-toast-id=\"${id}\"]`) ?? null\n}\n\n/** Жест отпущен и тост закрывается: переход снова разрешён (см. `releaseSwiped`). */\nconst swipeReleased = ref(false)\n\nfunction resetSwipe(): void {\n swipingId.value = null\n swipeOffset.value = 0\n swipeReleased.value = false\n}\n\n/**\n * Отпущенный тост уезжает за свой край и только потом закрывается.\n *\n * Два кадра здесь обязательны, и оба по одной причине: уходящий элемент Vue\n * больше не перерисовывает — он уезжает с тем стилем, что был на последнем\n * рендере. Значит и разрешение перехода, и конечную точку нужно успеть отдать\n * до закрытия: первый кадр снимает инлайновый `transition: none` (во время\n * жеста он нужен, иначе переходы группы борются с пальцем), второй задаёт точку\n * отлёта — с этого момента анимация уже идёт, и `leave` подхватывает её на ходу.\n *\n * Закрыв тост сразу, получили бы не отлёт, а мгновенное исчезновение: элемент\n * ушёл бы в `leave` с запрещённым переходом.\n */\nasync function releaseSwiped(id: string): Promise<void> {\n swipeReleased.value = true\n await nextTick()\n\n swipeOffset.value = SWIPE_DIRECTION[props.placement] * SWIPE_FLY_OUT_PX\n await nextTick()\n\n dismiss(id)\n resetSwipe()\n}\n\n/**\n * Движение к своему краю идёт один к одному, в обратную сторону — с\n * сопротивлением: тост едет за пальцем, но закрыть его «наоборот» нельзя.\n */\nfunction resistOffset(delta: number): number {\n const toEdge = SWIPE_DIRECTION[props.placement]\n\n return Math.sign(delta) === toEdge ? delta : delta / SWIPE_RESISTANCE\n}\n\nconst swipe = useDragGesture({\n disabled: () => !props.swipeDismiss,\n onStart: (event) => {\n const target = event.target as HTMLElement | null\n // Нажатие на кнопке — это нажатие на кнопке: закрытие и действия тоста не\n // должны превращаться в протяжку.\n if (target?.closest('button, a[href]')) return false\n\n const host = target?.closest<HTMLElement>('[data-gr-toast]')\n const id = host?.dataset.toastId\n if (!host || !id) return false\n\n swipingId.value = id\n swipeOffset.value = 0\n swipeStartX = event.clientX\n swipeThreshold = Math.max(SWIPE_THRESHOLD_MIN_PX, host.getBoundingClientRect().width * SWIPE_THRESHOLD_RATIO)\n },\n onMove: (event) => {\n // Против выделения текста тоста во время протяжки.\n event.preventDefault()\n swipeOffset.value = resistOffset(event.clientX - swipeStartX)\n },\n onEnd: () => {\n const id = swipingId.value\n const passed = id !== null\n && Math.sign(swipeOffset.value) === SWIPE_DIRECTION[props.placement]\n && Math.abs(swipeOffset.value) >= swipeThreshold\n\n if (passed) void releaseSwiped(id)\n else resetSwipe()\n },\n // Обрыв возвращает тост на место, а не дожимает смахивание: указатель забрал\n // браузер, а не пользователь довёл жест.\n onCancel: resetSwipe,\n})\n\nfunction swipeStyle(toast: Toast): Record<string, string> | undefined {\n if (swipingId.value !== toast.id) return undefined\n\n return {\n transform: `translateX(${swipeOffset.value}px)`,\n opacity: String(swipeOpacity(swipeOffset.value, swipeThreshold)),\n // Переходы группы во время жеста борются с пальцем: тост едет с задержкой\n // и «догоняет» курсор после отпускания. После отпускания запрет снимается —\n // уходящему тосту переход нужен, иначе он не уедет вовсе.\n ...(swipeReleased.value ? {} : { transition: 'none' }),\n }\n}\n\n/**\n * Клавиатурный эквивалент смахивания. `Escape` намеренно не берём: тостер не\n * модальный слой, и перехват закрывал бы уведомление вместо диалога под ним.\n */\nfunction onToastKeydown(event: KeyboardEvent, toast: Toast): void {\n if (event.key !== 'Delete' && event.key !== 'Backspace') return\n\n event.preventDefault()\n\n const index = visibleToasts.value.findIndex(item => item.id === toast.id)\n const next = visibleToasts.value[index + 1] ?? visibleToasts.value[index - 1]\n\n dismiss(toast.id)\n\n // Фокус не должен упасть на `body`: дальше по стеку есть что читать.\n if (next) void nextTick(() => toastElement(next.id)?.focus())\n}\n\n// Единый источник правды по таймерам: тост тикает, только если он видим И стек не на\n// паузе. Очередь и hover — на паузе. Реагирует на изменения списка/hover.\n//\n// `timeoutMs` читается ради зависимости, а не ради значения: `toast.promise` и\n// `update` перезапускают таймер уже существующего тоста, не трогая ни список, ни\n// паузу. Без этой зависимости эффект не просыпался бы, и заново заведённый\n// таймер тикал бы под курсором или у тоста, который ещё ждёт в очереди, — тот\n// сгорал бы, ни разу не показавшись.\nwatchEffect(() => {\n for (const toast of list.value) {\n const shouldTick = toast.timeoutMs > 0 && visibleIds.value.has(toast.id) && !paused.value\n\n if (shouldTick)\n resume(toast.id)\n else\n pause(toast.id)\n }\n})\n\nfunction metaFor(tone: GrToastTone): ToneMeta {\n return TONE_META[tone] ?? TONE_META.info\n}\n\nconst containerClass = computed(() => PLACEMENT_CLASS[props.placement])\n\nconst containerStyle = computed(() => {\n if (props.width === undefined) return undefined\n\n return { '--gr-toaster-width': typeof props.width === 'number' ? `${props.width}px` : props.width }\n})\n\n\n/**\n * Переводит фокус на верхний тост. Тост фокусируем только программно\n * (`tabindex=\"-1\"`): собственной остановкой `Tab` стек быть не должен, иначе\n * всплывающее уведомление перехватывало бы обход страницы.\n */\nfunction focus(): boolean {\n const target = containerEl.value?.querySelector<HTMLElement>('[data-gr-toast]')\n if (!target) return false\n\n target.focus()\n return true\n}\n\nconst hotkeyBinding = computed(() => ({\n enabled: props.focusHotkey !== false && visibleToasts.value.length > 0,\n handlers: { [props.focusHotkey || 'F6']: { handler: onFocusHotkey, allowInEditable: true } },\n}))\n\nfunction onFocusHotkey(event: KeyboardEvent): void {\n if (focus()) event.preventDefault()\n}\n\ndefineExpose({ focus })\n</script>\n\n<template>\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <div\n ref=\"containerEl\"\n v-hotkey=\"hotkeyBinding\"\n v-bind=\"themeAttrs\"\n data-gr-toaster\n data-gr-overlay-root\n role=\"region\"\n :aria-label=\"resolvedRegionLabel\"\n class=\"fixed z-[var(--gr-z-toast)] w-[var(--gr-toaster-width,360px)] max-w-[calc(100vw-2rem)]\"\n :class=\"containerClass\"\n :style=\"containerStyle\"\n @mouseenter=\"hovered = true\"\n @mouseleave=\"hovered = false\"\n @focusin=\"focusWithin = true\"\n @focusout=\"focusWithin = false\"\n >\n <TransitionGroup\n tag=\"div\"\n class=\"grid gap-3\"\n enter-active-class=\"transition duration-[var(--gr-duration-base)] ease-[var(--gr-ease-out)]\"\n enter-from-class=\"opacity-0 translate-y-2\"\n enter-to-class=\"opacity-100 translate-y-0\"\n leave-active-class=\"transition duration-[var(--gr-duration-fast)] ease-[var(--gr-ease-in)] absolute\"\n leave-from-class=\"opacity-100 translate-y-0\"\n leave-to-class=\"opacity-0 translate-y-2\"\n move-class=\"transition-transform duration-[var(--gr-duration-base)] ease-[var(--gr-ease-out)]\"\n >\n <div\n v-for=\"toast in visibleToasts\"\n :key=\"toast.id\"\n data-gr-toast\n :data-tone=\"toast.tone\"\n :data-toast-id=\"toast.id\"\n :role=\"metaFor(toast.tone).role\"\n aria-atomic=\"true\"\n tabindex=\"-1\"\n class=\"relative overflow-hidden rounded-[var(--gr-radius-lg)] border border-[var(--gr-brd)] bg-[var(--gr-card)] px-4 py-3 shadow-[var(--gr-shadow-2)] focus-visible:outline-none focus-visible:shadow-[var(--gr-shadow-2),0_0_0_2px_var(--gr-ring)]\"\n :class=\"swipeDismiss ? '[touch-action:pan-y]' : ''\"\n :style=\"swipeStyle(toast)\"\n @pointerdown=\"swipe.start\"\n @keydown=\"onToastKeydown($event, toast)\"\n >\n <div class=\"flex items-start gap-3\">\n <GrIcon size=\"md\" class=\"mt-0.5\" :style=\"{ color: metaFor(toast.tone).color }\">\n <component :is=\"metaFor(toast.tone).icon\" />\n </GrIcon>\n <div class=\"min-w-0 flex-1\">\n <div class=\"text-[length:var(--gr-text-sm)] font-700\">\n {{ toast.title }}\n </div>\n <div v-if=\"toast.message\" class=\"mt-0.5 text-[length:var(--gr-text-sm)] text-[var(--gr-muted-fg)]\">\n {{ toast.message }}\n </div>\n\n <!-- Action-кнопки тоста: например «Отменить»/«Повторить». Можно\n задать массивом (`actions`) или переопределить слотом. -->\n <div v-if=\"hasActions(toast)\" class=\"mt-2 flex flex-wrap gap-2\">\n <slot name=\"actions\" :toast=\"toast\" :dismiss=\"() => dismiss(toast.id)\">\n <GrButton\n v-for=\"(action, index) in toastActions(toast)\"\n :key=\"index\"\n :variant=\"action.variant ?? 'outline'\"\n :size=\"action.size ?? 'sm'\"\n data-gr-toast-action\n @click=\"onAction(toast, action)\"\n >\n {{ action.label }}\n </GrButton>\n </slot>\n </div>\n </div>\n <GrButton\n variant=\"ghost\"\n size=\"sm\"\n square\n :aria-label=\"resolvedDismissLabel\"\n @click=\"dismiss(toast.id)\"\n >\n <GrIcon size=\"sm\">\n <IconClose />\n </GrIcon>\n </GrButton>\n </div>\n\n <!-- Прогресс до автозакрытия; замирает вместе с таймером на паузе. -->\n <div\n v-if=\"toast.timeoutMs > 0\"\n data-gr-toast-progress\n aria-hidden=\"true\"\n class=\"gr-toast-progress absolute bottom-0 left-0 h-[2px] w-full origin-left\"\n :style=\"{\n backgroundColor: metaFor(toast.tone).color,\n animationDuration: `${toast.timeoutMs}ms`,\n animationPlayState: paused ? 'paused' : 'running',\n }\"\n />\n </div>\n </TransitionGroup>\n </div>\n </teleport>\n</template>\n\n<style scoped>\n.gr-toast-progress {\n animation-name: gr-toast-progress;\n animation-timing-function: linear;\n animation-fill-mode: forwards;\n}\n\n@keyframes gr-toast-progress {\n from {\n transform: scaleX(1);\n }\n to {\n transform: scaleX(0);\n }\n}\n\n/*\n * Единственная анимация пакета, которую глобальный кламп чинит неверно.\n *\n * У полосы `animation-fill-mode: forwards`, поэтому сжатая до `0.01ms` она не\n * возвращается в исходное состояние, а фиксируется на `scaleX(0)`: таймер\n * выглядит истёкшим, пока тост ещё висит на экране. Полоса — единственный\n * носитель этого смысла, и статичной её оставить нельзя, поэтому под reduce она\n * убирается совсем. Автозакрытие ведёт JS-таймер, оно не страдает, а сам элемент\n * и так `aria-hidden`.\n */\n@media (prefers-reduced-motion: reduce) {\n .gr-toast-progress {\n display: none;\n }\n}\n</style>\n"],"mappings":"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"}
1
+ {"version":3,"file":"GrToaster-Dqja0Lf9.js","names":[],"sources":["../../../../src/components/GrToaster/GrToaster.vue","../../../../src/components/GrToaster/GrToaster.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * GrToaster — GR-примитив области уведомлений (toast).\n *\n * Модель состояния берётся из `useToast`, поэтому в приложении предполагается\n * **один активный `GrToaster`** на корень (или app-scoped через плагин).\n *\n * A11y:\n * - контейнер — `role=\"region\"` с `aria-label` (i18n-проп `regionLabel`);\n * - объявляет каждый тост сам собой: `role=\"status\"`, а критичные\n * (`warning`/`danger`) — ассертивный `role=\"alert\"`. Обёртки-live-region над\n * ними нет намеренно: вложение регионов с разной ассертивностью\n * спецификацией не определено, браузеры и SR расходятся вплоть до потери\n * объявления;\n * - стек достижим с клавиатуры по `focusHotkey` (по умолчанию `F6`) — тосты\n * телепортированы в конец `body`, и без хоткея кнопка «Отменить» лежала бы\n * за пределами разумного числа нажатий `Tab`.\n *\n * UX:\n * - `maxVisible` ограничивает стек; лишние ждут в очереди (их таймеры на паузе);\n * - автозакрытие **останавливается под курсором/фокусом** (WCAG 2.2.1) и\n * визуализируется прогресс-баром до закрытия;\n * - `placement` настраивает угол экрана; слой — `--gr-z-toast`.\n */\nimport { computed, nextTick, ref, useSlots, watchEffect } from 'vue'\n\nimport { usePortalTarget } from '../../composables/usePortalTarget'\nimport type { Component } from 'vue'\n\nimport { useDragGesture } from '../../composables/useDragGesture'\nimport { useToast } from '../../composables/useToast'\nimport { vHotkey } from '../../directives'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport type { GrToastTone, Toast, ToastAction } from '../../composables/useToast'\nimport GrButton from '../GrButton'\nimport GrIcon from '../GrIcon'\nimport {\n PLACEMENT_CLASS,\n SWIPE_DIRECTION,\n SWIPE_FLY_OUT_PX,\n SWIPE_RESISTANCE,\n SWIPE_THRESHOLD_MIN_PX,\n SWIPE_THRESHOLD_RATIO,\n swipeOpacity,\n type GrToasterPlacement,\n} from './grToasterStyles'\n\nimport IconCheck from '~icons/lucide/check-circle'\nimport IconWarning from '~icons/lucide/alert-triangle'\nimport IconError from '~icons/lucide/x-circle'\nimport IconInfo from '~icons/lucide/info'\nimport IconClose from '~icons/lucide/x'\n\nexport type { GrToasterPlacement }\n\ninterface ToneMeta {\n icon: Component\n color: string\n role: 'status' | 'alert'\n}\n\n// Единый маппинг по tone'у: иконка, цвет, a11y-роль. На уровне модуля —\n// чтобы не пересоздавать объект на каждый рендер.\nconst TONE_META: Record<GrToastTone, ToneMeta> = {\n primary: { icon: IconInfo, color: 'var(--gr-primary)', role: 'status' },\n neutral: { icon: IconInfo, color: 'var(--gr-muted-fg)', role: 'status' },\n info: { icon: IconInfo, color: 'var(--gr-info)', role: 'status' },\n success: { icon: IconCheck, color: 'var(--gr-success)', role: 'status' },\n warning: { icon: IconWarning, color: 'var(--gr-warning)', role: 'alert' },\n danger: { icon: IconError, color: 'var(--gr-danger)', role: 'alert' },\n slate: { icon: IconInfo, color: 'var(--gr-slate)', role: 'status' },\n azure: { icon: IconInfo, color: 'var(--gr-azure)', role: 'status' },\n}\n\nexport interface GrToasterProps {\n /** Угол экрана для стека уведомлений. */\n placement?: GrToasterPlacement\n /** Максимум одновременно видимых тостов; остальные ждут в очереди. По умолчанию `4`. */\n maxVisible?: number\n /** A11y-лейбл кнопки закрытия (i18n). */\n dismissLabel?: string\n /** A11y-лейбл контейнера-региона (i18n). */\n regionLabel?: string\n /**\n * Ширина стека. Число — пиксели, строка — любая CSS-длина. Уезжает в\n * `--gr-toaster-width`, поэтому то же самое можно задать и темой.\n */\n width?: string | number\n /**\n * Клавиша перевода фокуса в стек уведомлений; `false` отключает хоткей.\n * `F6` — рекомендация APG для перехода между «регионами» страницы.\n */\n focusHotkey?: string | false\n /**\n * Смахивание тоста к своему краю экрана. Клавиатурный эквивалент — `Delete`\n * и `Backspace` на сфокусированном тосте — остаётся и при выключенном жесте.\n */\n swipeDismiss?: boolean\n}\n\nimport { useGrThemeAttrs } from '../GrConfigProvider/context'\n\nconst props = withDefaults(defineProps<GrToasterProps>(), {\n placement: 'top-right',\n maxVisible: 4,\n dismissLabel: undefined,\n regionLabel: undefined,\n width: undefined,\n focusHotkey: 'F6',\n swipeDismiss: true,\n})\n\n/**\n * Слот `actions` полностью заменяет дефолтные action-кнопки тоста. Получает сам\n * `toast` и `dismiss` — функцию, закрывающую именно этот тост.\n */\ndefineSlots<{\n actions?: (props: { toast: Toast, dismiss: () => void }) => unknown\n}>()\n\nconst { t } = useGranularityTranslations()\nconst slots = useSlots()\nconst resolvedDismissLabel = computed(() => props.dismissLabel ?? t('gr.toaster.dismiss', 'Dismiss'))\nconst resolvedRegionLabel = computed(() => props.regionLabel ?? t('gr.toaster.region', 'Notifications'))\n\nconst { list, dismiss, pause, resume } = useToast()\n\n// Единый список кнопок тоста: `action` (шорткат) + массив `actions`.\nfunction toastActions(toast: Toast): ToastAction[] {\n return [...(toast.action ? [toast.action] : []), ...(toast.actions ?? [])]\n}\n\n// Показываем блок действий, если есть кнопки или задан слот `actions`.\nfunction hasActions(toast: Toast): boolean {\n return Boolean(slots.actions) || toastActions(toast).length > 0\n}\n\n// Клик по action-кнопке тоста: вызываем обработчик и (по умолчанию) закрываем тост.\nfunction onAction(toast: Toast, action: ToastAction): void {\n action.onClick()\n if (action.dismissOnClick !== false)\n dismiss(toast.id)\n}\n\n// SSR-guard: на сервере `document.body` недоступен — отключаем `teleport`.\n// Телепорт включается только ПОСЛЕ монтирования: иначе первый клиентский\n// рендер не совпадает с серверным и ломается гидрация (см. композабл).\nconst { target: portalTarget, enabled: teleportEnabled } = usePortalTarget()\n\n// Тема поддерева на телепортированную панель: в DOM она уезжает в `body`, то\n// есть вне обёртки провайдера, и `data-theme` с неё не наследуется. В дереве\n// компонентов панель остаётся внутри — `inject` доходит, и тему она ставит себе\n// сама.\nconst themeAttrs = useGrThemeAttrs()\n\n// Видимые тосты (не больше `maxVisible`); остальные ждут в очереди. Список\n// хранится новейшими вперёд, поэтому «видимые дожинают, новые ждут» — это\n// хвост: старейшие N. Взяв голову, стек показывал бы новейшие, и каждый пуш\n// вытеснял бы с экрана тост, который пользователь ещё читает.\nconst visibleToasts = computed(() => list.value.slice(-Math.max(1, props.maxVisible)))\nconst visibleIds = computed(() => new Set(visibleToasts.value.map(toast => toast.id)))\n\n// Пауза под курсором/фокусом (WCAG 2.2.1). Флага два и они независимы: один\n// `paused` терялся на «фокус внутри + мышь прошла насквозь» — `mouseleave`\n// возобновлял отсчёт под клавиатурным фокусом.\nconst hovered = ref(false)\nconst focusWithin = ref(false)\n\n// Контейнер стека: по нему ищутся тосты — и для фокуса, и для жеста.\nconst containerEl = ref<HTMLElement | null>(null)\n\n// ————— Смахивание.\n\nconst swipingId = ref<string | null>(null)\nconst swipeOffset = ref(0)\nlet swipeStartX = 0\nlet swipeThreshold = SWIPE_THRESHOLD_MIN_PX\n\n// Тост, который тянут, тоже держит таймеры: иначе уведомление сгорало бы прямо\n// под пальцем, посреди жеста.\nconst paused = computed(() => hovered.value || focusWithin.value || swipingId.value !== null)\n\nfunction toastElement(id: string): HTMLElement | null {\n return containerEl.value?.querySelector<HTMLElement>(`[data-gr-toast][data-toast-id=\"${id}\"]`) ?? null\n}\n\n/** Жест отпущен и тост закрывается: переход снова разрешён (см. `releaseSwiped`). */\nconst swipeReleased = ref(false)\n\nfunction resetSwipe(): void {\n swipingId.value = null\n swipeOffset.value = 0\n swipeReleased.value = false\n}\n\n/**\n * Отпущенный тост уезжает за свой край и только потом закрывается.\n *\n * Два кадра здесь обязательны, и оба по одной причине: уходящий элемент Vue\n * больше не перерисовывает — он уезжает с тем стилем, что был на последнем\n * рендере. Значит и разрешение перехода, и конечную точку нужно успеть отдать\n * до закрытия: первый кадр снимает инлайновый `transition: none` (во время\n * жеста он нужен, иначе переходы группы борются с пальцем), второй задаёт точку\n * отлёта — с этого момента анимация уже идёт, и `leave` подхватывает её на ходу.\n *\n * Закрыв тост сразу, получили бы не отлёт, а мгновенное исчезновение: элемент\n * ушёл бы в `leave` с запрещённым переходом.\n */\nasync function releaseSwiped(id: string): Promise<void> {\n swipeReleased.value = true\n await nextTick()\n\n swipeOffset.value = SWIPE_DIRECTION[props.placement] * SWIPE_FLY_OUT_PX\n await nextTick()\n\n dismiss(id)\n resetSwipe()\n}\n\n/**\n * Движение к своему краю идёт один к одному, в обратную сторону — с\n * сопротивлением: тост едет за пальцем, но закрыть его «наоборот» нельзя.\n */\nfunction resistOffset(delta: number): number {\n const toEdge = SWIPE_DIRECTION[props.placement]\n\n return Math.sign(delta) === toEdge ? delta : delta / SWIPE_RESISTANCE\n}\n\nconst swipe = useDragGesture({\n disabled: () => !props.swipeDismiss,\n onStart: (event) => {\n const target = event.target as HTMLElement | null\n // Нажатие на кнопке — это нажатие на кнопке: закрытие и действия тоста не\n // должны превращаться в протяжку.\n if (target?.closest('button, a[href]')) return false\n\n const host = target?.closest<HTMLElement>('[data-gr-toast]')\n const id = host?.dataset.toastId\n if (!host || !id) return false\n\n swipingId.value = id\n swipeOffset.value = 0\n swipeStartX = event.clientX\n swipeThreshold = Math.max(SWIPE_THRESHOLD_MIN_PX, host.getBoundingClientRect().width * SWIPE_THRESHOLD_RATIO)\n },\n onMove: (event) => {\n // Против выделения текста тоста во время протяжки.\n event.preventDefault()\n swipeOffset.value = resistOffset(event.clientX - swipeStartX)\n },\n onEnd: () => {\n const id = swipingId.value\n const passed = id !== null\n && Math.sign(swipeOffset.value) === SWIPE_DIRECTION[props.placement]\n && Math.abs(swipeOffset.value) >= swipeThreshold\n\n if (passed) void releaseSwiped(id)\n else resetSwipe()\n },\n // Обрыв возвращает тост на место, а не дожимает смахивание: указатель забрал\n // браузер, а не пользователь довёл жест.\n onCancel: resetSwipe,\n})\n\nfunction swipeStyle(toast: Toast): Record<string, string> | undefined {\n if (swipingId.value !== toast.id) return undefined\n\n return {\n transform: `translateX(${swipeOffset.value}px)`,\n opacity: String(swipeOpacity(swipeOffset.value, swipeThreshold)),\n // Переходы группы во время жеста борются с пальцем: тост едет с задержкой\n // и «догоняет» курсор после отпускания. После отпускания запрет снимается —\n // уходящему тосту переход нужен, иначе он не уедет вовсе.\n ...(swipeReleased.value ? {} : { transition: 'none' }),\n }\n}\n\n/**\n * Клавиатурный эквивалент смахивания. `Escape` намеренно не берём: тостер не\n * модальный слой, и перехват закрывал бы уведомление вместо диалога под ним.\n */\nfunction onToastKeydown(event: KeyboardEvent, toast: Toast): void {\n if (event.key !== 'Delete' && event.key !== 'Backspace') return\n\n event.preventDefault()\n\n const index = visibleToasts.value.findIndex(item => item.id === toast.id)\n const next = visibleToasts.value[index + 1] ?? visibleToasts.value[index - 1]\n\n dismiss(toast.id)\n\n // Фокус не должен упасть на `body`: дальше по стеку есть что читать.\n if (next) void nextTick(() => toastElement(next.id)?.focus())\n}\n\n// Единый источник правды по таймерам: тост тикает, только если он видим И стек не на\n// паузе. Очередь и hover — на паузе. Реагирует на изменения списка/hover.\n//\n// `timeoutMs` читается ради зависимости, а не ради значения: `toast.promise` и\n// `update` перезапускают таймер уже существующего тоста, не трогая ни список, ни\n// паузу. Без этой зависимости эффект не просыпался бы, и заново заведённый\n// таймер тикал бы под курсором или у тоста, который ещё ждёт в очереди, — тот\n// сгорал бы, ни разу не показавшись.\nwatchEffect(() => {\n for (const toast of list.value) {\n const shouldTick = toast.timeoutMs > 0 && visibleIds.value.has(toast.id) && !paused.value\n\n if (shouldTick)\n resume(toast.id)\n else\n pause(toast.id)\n }\n})\n\nfunction metaFor(tone: GrToastTone): ToneMeta {\n return TONE_META[tone] ?? TONE_META.info\n}\n\nconst containerClass = computed(() => PLACEMENT_CLASS[props.placement])\n\nconst containerStyle = computed(() => {\n if (props.width === undefined) return undefined\n\n return { '--gr-toaster-width': typeof props.width === 'number' ? `${props.width}px` : props.width }\n})\n\n\n/**\n * Переводит фокус на верхний тост. Тост фокусируем только программно\n * (`tabindex=\"-1\"`): собственной остановкой `Tab` стек быть не должен, иначе\n * всплывающее уведомление перехватывало бы обход страницы.\n */\nfunction focus(): boolean {\n const target = containerEl.value?.querySelector<HTMLElement>('[data-gr-toast]')\n if (!target) return false\n\n target.focus()\n return true\n}\n\nconst hotkeyBinding = computed(() => ({\n enabled: props.focusHotkey !== false && visibleToasts.value.length > 0,\n handlers: { [props.focusHotkey || 'F6']: { handler: onFocusHotkey, allowInEditable: true } },\n}))\n\nfunction onFocusHotkey(event: KeyboardEvent): void {\n if (focus()) event.preventDefault()\n}\n\ndefineExpose({ focus })\n</script>\n\n<template>\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <div\n ref=\"containerEl\"\n v-hotkey=\"hotkeyBinding\"\n v-bind=\"themeAttrs\"\n data-gr-toaster\n data-gr-overlay-root\n role=\"region\"\n :aria-label=\"resolvedRegionLabel\"\n class=\"fixed z-[var(--gr-z-toast)] w-[var(--gr-toaster-width,360px)] max-w-[calc(100vw-2rem)]\"\n :class=\"containerClass\"\n :style=\"containerStyle\"\n @mouseenter=\"hovered = true\"\n @mouseleave=\"hovered = false\"\n @focusin=\"focusWithin = true\"\n @focusout=\"focusWithin = false\"\n >\n <TransitionGroup\n tag=\"div\"\n class=\"grid gap-3\"\n enter-active-class=\"transition duration-[var(--gr-duration-base)] ease-[var(--gr-ease-out)]\"\n enter-from-class=\"opacity-0 translate-y-2\"\n enter-to-class=\"opacity-100 translate-y-0\"\n leave-active-class=\"transition duration-[var(--gr-duration-fast)] ease-[var(--gr-ease-in)] absolute\"\n leave-from-class=\"opacity-100 translate-y-0\"\n leave-to-class=\"opacity-0 translate-y-2\"\n move-class=\"transition-transform duration-[var(--gr-duration-base)] ease-[var(--gr-ease-out)]\"\n >\n <div\n v-for=\"toast in visibleToasts\"\n :key=\"toast.id\"\n data-gr-toast\n :data-tone=\"toast.tone\"\n :data-toast-id=\"toast.id\"\n :role=\"metaFor(toast.tone).role\"\n aria-atomic=\"true\"\n tabindex=\"-1\"\n class=\"relative overflow-hidden rounded-[var(--gr-radius-lg)] border border-[var(--gr-brd)] bg-[var(--gr-card)] px-4 py-3 shadow-[var(--gr-shadow-2)] focus-visible:outline-none focus-visible:shadow-[var(--gr-shadow-2),0_0_0_2px_var(--gr-ring)]\"\n :class=\"swipeDismiss ? '[touch-action:pan-y]' : ''\"\n :style=\"swipeStyle(toast)\"\n @pointerdown=\"swipe.start\"\n @keydown=\"onToastKeydown($event, toast)\"\n >\n <div class=\"flex items-start gap-3\">\n <GrIcon size=\"md\" class=\"mt-0.5\" :style=\"{ color: metaFor(toast.tone).color }\">\n <component :is=\"metaFor(toast.tone).icon\" />\n </GrIcon>\n <div class=\"min-w-0 flex-1\">\n <div class=\"text-[length:var(--gr-text-sm)] font-700\">\n {{ toast.title }}\n </div>\n <div v-if=\"toast.message\" class=\"mt-0.5 text-[length:var(--gr-text-sm)] text-[var(--gr-muted-fg)]\">\n {{ toast.message }}\n </div>\n\n <!-- Action-кнопки тоста: например «Отменить»/«Повторить». Можно\n задать массивом (`actions`) или переопределить слотом. -->\n <div v-if=\"hasActions(toast)\" class=\"mt-2 flex flex-wrap gap-2\">\n <slot name=\"actions\" :toast=\"toast\" :dismiss=\"() => dismiss(toast.id)\">\n <GrButton\n v-for=\"(action, index) in toastActions(toast)\"\n :key=\"index\"\n :variant=\"action.variant ?? 'outline'\"\n :size=\"action.size ?? 'sm'\"\n data-gr-toast-action\n @click=\"onAction(toast, action)\"\n >\n {{ action.label }}\n </GrButton>\n </slot>\n </div>\n </div>\n <GrButton\n variant=\"ghost\"\n size=\"sm\"\n square\n :aria-label=\"resolvedDismissLabel\"\n @click=\"dismiss(toast.id)\"\n >\n <GrIcon size=\"sm\">\n <IconClose />\n </GrIcon>\n </GrButton>\n </div>\n\n <!-- Прогресс до автозакрытия; замирает вместе с таймером на паузе. -->\n <div\n v-if=\"toast.timeoutMs > 0\"\n data-gr-toast-progress\n aria-hidden=\"true\"\n class=\"gr-toast-progress absolute bottom-0 left-0 h-[2px] w-full origin-left\"\n :style=\"{\n backgroundColor: metaFor(toast.tone).color,\n animationDuration: `${toast.timeoutMs}ms`,\n animationPlayState: paused ? 'paused' : 'running',\n }\"\n />\n </div>\n </TransitionGroup>\n </div>\n </teleport>\n</template>\n\n<style scoped>\n.gr-toast-progress {\n animation-name: gr-toast-progress;\n animation-timing-function: linear;\n animation-fill-mode: forwards;\n}\n\n@keyframes gr-toast-progress {\n from {\n transform: scaleX(1);\n }\n to {\n transform: scaleX(0);\n }\n}\n\n/*\n * Единственная анимация пакета, которую глобальный кламп чинит неверно.\n *\n * У полосы `animation-fill-mode: forwards`, поэтому сжатая до `0.01ms` она не\n * возвращается в исходное состояние, а фиксируется на `scaleX(0)`: таймер\n * выглядит истёкшим, пока тост ещё висит на экране. Полоса — единственный\n * носитель этого смысла, и статичной её оставить нельзя, поэтому под reduce она\n * убирается совсем. Автозакрытие ведёт JS-таймер, оно не страдает, а сам элемент\n * и так `aria-hidden`.\n */\n@media (prefers-reduced-motion: reduce) {\n .gr-toast-progress {\n display: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * GrToaster — GR-примитив области уведомлений (toast).\n *\n * Модель состояния берётся из `useToast`, поэтому в приложении предполагается\n * **один активный `GrToaster`** на корень (или app-scoped через плагин).\n *\n * A11y:\n * - контейнер — `role=\"region\"` с `aria-label` (i18n-проп `regionLabel`);\n * - объявляет каждый тост сам собой: `role=\"status\"`, а критичные\n * (`warning`/`danger`) — ассертивный `role=\"alert\"`. Обёртки-live-region над\n * ними нет намеренно: вложение регионов с разной ассертивностью\n * спецификацией не определено, браузеры и SR расходятся вплоть до потери\n * объявления;\n * - стек достижим с клавиатуры по `focusHotkey` (по умолчанию `F6`) — тосты\n * телепортированы в конец `body`, и без хоткея кнопка «Отменить» лежала бы\n * за пределами разумного числа нажатий `Tab`.\n *\n * UX:\n * - `maxVisible` ограничивает стек; лишние ждут в очереди (их таймеры на паузе);\n * - автозакрытие **останавливается под курсором/фокусом** (WCAG 2.2.1) и\n * визуализируется прогресс-баром до закрытия;\n * - `placement` настраивает угол экрана; слой — `--gr-z-toast`.\n */\nimport { computed, nextTick, ref, useSlots, watchEffect } from 'vue'\n\nimport { usePortalTarget } from '../../composables/usePortalTarget'\nimport type { Component } from 'vue'\n\nimport { useDragGesture } from '../../composables/useDragGesture'\nimport { useToast } from '../../composables/useToast'\nimport { vHotkey } from '../../directives'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport type { GrToastTone, Toast, ToastAction } from '../../composables/useToast'\nimport GrButton from '../GrButton'\nimport GrIcon from '../GrIcon'\nimport {\n PLACEMENT_CLASS,\n SWIPE_DIRECTION,\n SWIPE_FLY_OUT_PX,\n SWIPE_RESISTANCE,\n SWIPE_THRESHOLD_MIN_PX,\n SWIPE_THRESHOLD_RATIO,\n swipeOpacity,\n type GrToasterPlacement,\n} from './grToasterStyles'\n\nimport IconCheck from '~icons/lucide/check-circle'\nimport IconWarning from '~icons/lucide/alert-triangle'\nimport IconError from '~icons/lucide/x-circle'\nimport IconInfo from '~icons/lucide/info'\nimport IconClose from '~icons/lucide/x'\n\nexport type { GrToasterPlacement }\n\ninterface ToneMeta {\n icon: Component\n color: string\n role: 'status' | 'alert'\n}\n\n// Единый маппинг по tone'у: иконка, цвет, a11y-роль. На уровне модуля —\n// чтобы не пересоздавать объект на каждый рендер.\nconst TONE_META: Record<GrToastTone, ToneMeta> = {\n primary: { icon: IconInfo, color: 'var(--gr-primary)', role: 'status' },\n neutral: { icon: IconInfo, color: 'var(--gr-muted-fg)', role: 'status' },\n info: { icon: IconInfo, color: 'var(--gr-info)', role: 'status' },\n success: { icon: IconCheck, color: 'var(--gr-success)', role: 'status' },\n warning: { icon: IconWarning, color: 'var(--gr-warning)', role: 'alert' },\n danger: { icon: IconError, color: 'var(--gr-danger)', role: 'alert' },\n slate: { icon: IconInfo, color: 'var(--gr-slate)', role: 'status' },\n azure: { icon: IconInfo, color: 'var(--gr-azure)', role: 'status' },\n}\n\nexport interface GrToasterProps {\n /** Угол экрана для стека уведомлений. */\n placement?: GrToasterPlacement\n /** Максимум одновременно видимых тостов; остальные ждут в очереди. По умолчанию `4`. */\n maxVisible?: number\n /** A11y-лейбл кнопки закрытия (i18n). */\n dismissLabel?: string\n /** A11y-лейбл контейнера-региона (i18n). */\n regionLabel?: string\n /**\n * Ширина стека. Число — пиксели, строка — любая CSS-длина. Уезжает в\n * `--gr-toaster-width`, поэтому то же самое можно задать и темой.\n */\n width?: string | number\n /**\n * Клавиша перевода фокуса в стек уведомлений; `false` отключает хоткей.\n * `F6` — рекомендация APG для перехода между «регионами» страницы.\n */\n focusHotkey?: string | false\n /**\n * Смахивание тоста к своему краю экрана. Клавиатурный эквивалент — `Delete`\n * и `Backspace` на сфокусированном тосте — остаётся и при выключенном жесте.\n */\n swipeDismiss?: boolean\n}\n\nimport { useGrThemeAttrs } from '../GrConfigProvider/context'\n\nconst props = withDefaults(defineProps<GrToasterProps>(), {\n placement: 'top-right',\n maxVisible: 4,\n dismissLabel: undefined,\n regionLabel: undefined,\n width: undefined,\n focusHotkey: 'F6',\n swipeDismiss: true,\n})\n\n/**\n * Слот `actions` полностью заменяет дефолтные action-кнопки тоста. Получает сам\n * `toast` и `dismiss` — функцию, закрывающую именно этот тост.\n */\ndefineSlots<{\n actions?: (props: { toast: Toast, dismiss: () => void }) => unknown\n}>()\n\nconst { t } = useGranularityTranslations()\nconst slots = useSlots()\nconst resolvedDismissLabel = computed(() => props.dismissLabel ?? t('gr.toaster.dismiss', 'Dismiss'))\nconst resolvedRegionLabel = computed(() => props.regionLabel ?? t('gr.toaster.region', 'Notifications'))\n\nconst { list, dismiss, pause, resume } = useToast()\n\n// Единый список кнопок тоста: `action` (шорткат) + массив `actions`.\nfunction toastActions(toast: Toast): ToastAction[] {\n return [...(toast.action ? [toast.action] : []), ...(toast.actions ?? [])]\n}\n\n// Показываем блок действий, если есть кнопки или задан слот `actions`.\nfunction hasActions(toast: Toast): boolean {\n return Boolean(slots.actions) || toastActions(toast).length > 0\n}\n\n// Клик по action-кнопке тоста: вызываем обработчик и (по умолчанию) закрываем тост.\nfunction onAction(toast: Toast, action: ToastAction): void {\n action.onClick()\n if (action.dismissOnClick !== false)\n dismiss(toast.id)\n}\n\n// SSR-guard: на сервере `document.body` недоступен — отключаем `teleport`.\n// Телепорт включается только ПОСЛЕ монтирования: иначе первый клиентский\n// рендер не совпадает с серверным и ломается гидрация (см. композабл).\nconst { target: portalTarget, enabled: teleportEnabled } = usePortalTarget()\n\n// Тема поддерева на телепортированную панель: в DOM она уезжает в `body`, то\n// есть вне обёртки провайдера, и `data-theme` с неё не наследуется. В дереве\n// компонентов панель остаётся внутри — `inject` доходит, и тему она ставит себе\n// сама.\nconst themeAttrs = useGrThemeAttrs()\n\n// Видимые тосты (не больше `maxVisible`); остальные ждут в очереди. Список\n// хранится новейшими вперёд, поэтому «видимые дожинают, новые ждут» — это\n// хвост: старейшие N. Взяв голову, стек показывал бы новейшие, и каждый пуш\n// вытеснял бы с экрана тост, который пользователь ещё читает.\nconst visibleToasts = computed(() => list.value.slice(-Math.max(1, props.maxVisible)))\nconst visibleIds = computed(() => new Set(visibleToasts.value.map(toast => toast.id)))\n\n// Пауза под курсором/фокусом (WCAG 2.2.1). Флага два и они независимы: один\n// `paused` терялся на «фокус внутри + мышь прошла насквозь» — `mouseleave`\n// возобновлял отсчёт под клавиатурным фокусом.\nconst hovered = ref(false)\nconst focusWithin = ref(false)\n\n// Контейнер стека: по нему ищутся тосты — и для фокуса, и для жеста.\nconst containerEl = ref<HTMLElement | null>(null)\n\n// ————— Смахивание.\n\nconst swipingId = ref<string | null>(null)\nconst swipeOffset = ref(0)\nlet swipeStartX = 0\nlet swipeThreshold = SWIPE_THRESHOLD_MIN_PX\n\n// Тост, который тянут, тоже держит таймеры: иначе уведомление сгорало бы прямо\n// под пальцем, посреди жеста.\nconst paused = computed(() => hovered.value || focusWithin.value || swipingId.value !== null)\n\nfunction toastElement(id: string): HTMLElement | null {\n return containerEl.value?.querySelector<HTMLElement>(`[data-gr-toast][data-toast-id=\"${id}\"]`) ?? null\n}\n\n/** Жест отпущен и тост закрывается: переход снова разрешён (см. `releaseSwiped`). */\nconst swipeReleased = ref(false)\n\nfunction resetSwipe(): void {\n swipingId.value = null\n swipeOffset.value = 0\n swipeReleased.value = false\n}\n\n/**\n * Отпущенный тост уезжает за свой край и только потом закрывается.\n *\n * Два кадра здесь обязательны, и оба по одной причине: уходящий элемент Vue\n * больше не перерисовывает — он уезжает с тем стилем, что был на последнем\n * рендере. Значит и разрешение перехода, и конечную точку нужно успеть отдать\n * до закрытия: первый кадр снимает инлайновый `transition: none` (во время\n * жеста он нужен, иначе переходы группы борются с пальцем), второй задаёт точку\n * отлёта — с этого момента анимация уже идёт, и `leave` подхватывает её на ходу.\n *\n * Закрыв тост сразу, получили бы не отлёт, а мгновенное исчезновение: элемент\n * ушёл бы в `leave` с запрещённым переходом.\n */\nasync function releaseSwiped(id: string): Promise<void> {\n swipeReleased.value = true\n await nextTick()\n\n swipeOffset.value = SWIPE_DIRECTION[props.placement] * SWIPE_FLY_OUT_PX\n await nextTick()\n\n dismiss(id)\n resetSwipe()\n}\n\n/**\n * Движение к своему краю идёт один к одному, в обратную сторону — с\n * сопротивлением: тост едет за пальцем, но закрыть его «наоборот» нельзя.\n */\nfunction resistOffset(delta: number): number {\n const toEdge = SWIPE_DIRECTION[props.placement]\n\n return Math.sign(delta) === toEdge ? delta : delta / SWIPE_RESISTANCE\n}\n\nconst swipe = useDragGesture({\n disabled: () => !props.swipeDismiss,\n onStart: (event) => {\n const target = event.target as HTMLElement | null\n // Нажатие на кнопке — это нажатие на кнопке: закрытие и действия тоста не\n // должны превращаться в протяжку.\n if (target?.closest('button, a[href]')) return false\n\n const host = target?.closest<HTMLElement>('[data-gr-toast]')\n const id = host?.dataset.toastId\n if (!host || !id) return false\n\n swipingId.value = id\n swipeOffset.value = 0\n swipeStartX = event.clientX\n swipeThreshold = Math.max(SWIPE_THRESHOLD_MIN_PX, host.getBoundingClientRect().width * SWIPE_THRESHOLD_RATIO)\n },\n onMove: (event) => {\n // Против выделения текста тоста во время протяжки.\n event.preventDefault()\n swipeOffset.value = resistOffset(event.clientX - swipeStartX)\n },\n onEnd: () => {\n const id = swipingId.value\n const passed = id !== null\n && Math.sign(swipeOffset.value) === SWIPE_DIRECTION[props.placement]\n && Math.abs(swipeOffset.value) >= swipeThreshold\n\n if (passed) void releaseSwiped(id)\n else resetSwipe()\n },\n // Обрыв возвращает тост на место, а не дожимает смахивание: указатель забрал\n // браузер, а не пользователь довёл жест.\n onCancel: resetSwipe,\n})\n\nfunction swipeStyle(toast: Toast): Record<string, string> | undefined {\n if (swipingId.value !== toast.id) return undefined\n\n return {\n transform: `translateX(${swipeOffset.value}px)`,\n opacity: String(swipeOpacity(swipeOffset.value, swipeThreshold)),\n // Переходы группы во время жеста борются с пальцем: тост едет с задержкой\n // и «догоняет» курсор после отпускания. После отпускания запрет снимается —\n // уходящему тосту переход нужен, иначе он не уедет вовсе.\n ...(swipeReleased.value ? {} : { transition: 'none' }),\n }\n}\n\n/**\n * Клавиатурный эквивалент смахивания. `Escape` намеренно не берём: тостер не\n * модальный слой, и перехват закрывал бы уведомление вместо диалога под ним.\n */\nfunction onToastKeydown(event: KeyboardEvent, toast: Toast): void {\n if (event.key !== 'Delete' && event.key !== 'Backspace') return\n\n event.preventDefault()\n\n const index = visibleToasts.value.findIndex(item => item.id === toast.id)\n const next = visibleToasts.value[index + 1] ?? visibleToasts.value[index - 1]\n\n dismiss(toast.id)\n\n // Фокус не должен упасть на `body`: дальше по стеку есть что читать.\n if (next) void nextTick(() => toastElement(next.id)?.focus())\n}\n\n// Единый источник правды по таймерам: тост тикает, только если он видим И стек не на\n// паузе. Очередь и hover — на паузе. Реагирует на изменения списка/hover.\n//\n// `timeoutMs` читается ради зависимости, а не ради значения: `toast.promise` и\n// `update` перезапускают таймер уже существующего тоста, не трогая ни список, ни\n// паузу. Без этой зависимости эффект не просыпался бы, и заново заведённый\n// таймер тикал бы под курсором или у тоста, который ещё ждёт в очереди, — тот\n// сгорал бы, ни разу не показавшись.\nwatchEffect(() => {\n for (const toast of list.value) {\n const shouldTick = toast.timeoutMs > 0 && visibleIds.value.has(toast.id) && !paused.value\n\n if (shouldTick)\n resume(toast.id)\n else\n pause(toast.id)\n }\n})\n\nfunction metaFor(tone: GrToastTone): ToneMeta {\n return TONE_META[tone] ?? TONE_META.info\n}\n\nconst containerClass = computed(() => PLACEMENT_CLASS[props.placement])\n\nconst containerStyle = computed(() => {\n if (props.width === undefined) return undefined\n\n return { '--gr-toaster-width': typeof props.width === 'number' ? `${props.width}px` : props.width }\n})\n\n\n/**\n * Переводит фокус на верхний тост. Тост фокусируем только программно\n * (`tabindex=\"-1\"`): собственной остановкой `Tab` стек быть не должен, иначе\n * всплывающее уведомление перехватывало бы обход страницы.\n */\nfunction focus(): boolean {\n const target = containerEl.value?.querySelector<HTMLElement>('[data-gr-toast]')\n if (!target) return false\n\n target.focus()\n return true\n}\n\nconst hotkeyBinding = computed(() => ({\n enabled: props.focusHotkey !== false && visibleToasts.value.length > 0,\n handlers: { [props.focusHotkey || 'F6']: { handler: onFocusHotkey, allowInEditable: true } },\n}))\n\nfunction onFocusHotkey(event: KeyboardEvent): void {\n if (focus()) event.preventDefault()\n}\n\ndefineExpose({ focus })\n</script>\n\n<template>\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <div\n ref=\"containerEl\"\n v-hotkey=\"hotkeyBinding\"\n v-bind=\"themeAttrs\"\n data-gr-toaster\n data-gr-overlay-root\n role=\"region\"\n :aria-label=\"resolvedRegionLabel\"\n class=\"fixed z-[var(--gr-z-toast)] w-[var(--gr-toaster-width,360px)] max-w-[calc(100vw-2rem)]\"\n :class=\"containerClass\"\n :style=\"containerStyle\"\n @mouseenter=\"hovered = true\"\n @mouseleave=\"hovered = false\"\n @focusin=\"focusWithin = true\"\n @focusout=\"focusWithin = false\"\n >\n <TransitionGroup\n tag=\"div\"\n class=\"grid gap-3\"\n enter-active-class=\"transition duration-[var(--gr-duration-base)] ease-[var(--gr-ease-out)]\"\n enter-from-class=\"opacity-0 translate-y-2\"\n enter-to-class=\"opacity-100 translate-y-0\"\n leave-active-class=\"transition duration-[var(--gr-duration-fast)] ease-[var(--gr-ease-in)] absolute\"\n leave-from-class=\"opacity-100 translate-y-0\"\n leave-to-class=\"opacity-0 translate-y-2\"\n move-class=\"transition-transform duration-[var(--gr-duration-base)] ease-[var(--gr-ease-out)]\"\n >\n <div\n v-for=\"toast in visibleToasts\"\n :key=\"toast.id\"\n data-gr-toast\n :data-tone=\"toast.tone\"\n :data-toast-id=\"toast.id\"\n :role=\"metaFor(toast.tone).role\"\n aria-atomic=\"true\"\n tabindex=\"-1\"\n class=\"relative overflow-hidden rounded-[var(--gr-radius-lg)] border border-[var(--gr-brd)] bg-[var(--gr-card)] px-4 py-3 shadow-[var(--gr-shadow-2)] focus-visible:outline-none focus-visible:shadow-[var(--gr-shadow-2),0_0_0_2px_var(--gr-ring)]\"\n :class=\"swipeDismiss ? '[touch-action:pan-y]' : ''\"\n :style=\"swipeStyle(toast)\"\n @pointerdown=\"swipe.start\"\n @keydown=\"onToastKeydown($event, toast)\"\n >\n <div class=\"flex items-start gap-3\">\n <GrIcon size=\"md\" class=\"mt-0.5\" :style=\"{ color: metaFor(toast.tone).color }\">\n <component :is=\"metaFor(toast.tone).icon\" />\n </GrIcon>\n <div class=\"min-w-0 flex-1\">\n <div class=\"text-[length:var(--gr-text-sm)] font-700\">\n {{ toast.title }}\n </div>\n <div v-if=\"toast.message\" class=\"mt-0.5 text-[length:var(--gr-text-sm)] text-[var(--gr-muted-fg)]\">\n {{ toast.message }}\n </div>\n\n <!-- Action-кнопки тоста: например «Отменить»/«Повторить». Можно\n задать массивом (`actions`) или переопределить слотом. -->\n <div v-if=\"hasActions(toast)\" class=\"mt-2 flex flex-wrap gap-2\">\n <slot name=\"actions\" :toast=\"toast\" :dismiss=\"() => dismiss(toast.id)\">\n <GrButton\n v-for=\"(action, index) in toastActions(toast)\"\n :key=\"index\"\n :variant=\"action.variant ?? 'outline'\"\n :size=\"action.size ?? 'sm'\"\n data-gr-toast-action\n @click=\"onAction(toast, action)\"\n >\n {{ action.label }}\n </GrButton>\n </slot>\n </div>\n </div>\n <GrButton\n variant=\"ghost\"\n size=\"sm\"\n square\n :aria-label=\"resolvedDismissLabel\"\n @click=\"dismiss(toast.id)\"\n >\n <GrIcon size=\"sm\">\n <IconClose />\n </GrIcon>\n </GrButton>\n </div>\n\n <!-- Прогресс до автозакрытия; замирает вместе с таймером на паузе. -->\n <div\n v-if=\"toast.timeoutMs > 0\"\n data-gr-toast-progress\n aria-hidden=\"true\"\n class=\"gr-toast-progress absolute bottom-0 left-0 h-[2px] w-full origin-left\"\n :style=\"{\n backgroundColor: metaFor(toast.tone).color,\n animationDuration: `${toast.timeoutMs}ms`,\n animationPlayState: paused ? 'paused' : 'running',\n }\"\n />\n </div>\n </TransitionGroup>\n </div>\n </teleport>\n</template>\n\n<style scoped>\n.gr-toast-progress {\n animation-name: gr-toast-progress;\n animation-timing-function: linear;\n animation-fill-mode: forwards;\n}\n\n@keyframes gr-toast-progress {\n from {\n transform: scaleX(1);\n }\n to {\n transform: scaleX(0);\n }\n}\n\n/*\n * Единственная анимация пакета, которую глобальный кламп чинит неверно.\n *\n * У полосы `animation-fill-mode: forwards`, поэтому сжатая до `0.01ms` она не\n * возвращается в исходное состояние, а фиксируется на `scaleX(0)`: таймер\n * выглядит истёкшим, пока тост ещё висит на экране. Полоса — единственный\n * носитель этого смысла, и статичной её оставить нельзя, поэтому под reduce она\n * убирается совсем. Автозакрытие ведёт JS-таймер, оно не страдает, а сам элемент\n * и так `aria-hidden`.\n */\n@media (prefers-reduced-motion: reduce) {\n .gr-toast-progress {\n display: none;\n }\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+DA,MAAM,YAA2C;GAC/C,SAAS;IAAE,MAAM;IAAU,OAAO;IAAqB,MAAM;GAAS;GACtE,SAAS;IAAE,MAAM;IAAU,OAAO;IAAsB,MAAM;GAAS;GACvE,MAAM;IAAE,MAAM;IAAU,OAAO;IAAkB,MAAM;GAAS;GAChE,SAAS;IAAE,MAAM;IAAW,OAAO;IAAqB,MAAM;GAAS;GACvE,SAAS;IAAE,MAAM;IAAa,OAAO;IAAqB,MAAM;GAAQ;GACxE,QAAQ;IAAE,MAAM;IAAW,OAAO;IAAoB,MAAM;GAAQ;GACpE,OAAO;IAAE,MAAM;IAAU,OAAO;IAAmB,MAAM;GAAS;GAClE,OAAO;IAAE,MAAM;IAAU,OAAO;IAAmB,MAAM;GAAS;EACpE;EA8BA,MAAM,QAAQ;;;;;EAkBd,MAAM,EAAE,MAAM,2BAA2B;EACzC,MAAM,QAAQ,SAAS;EACvB,MAAM,uBAAuB,eAAe,MAAM,gBAAgB,EAAE,sBAAsB,SAAS,CAAC;EACpG,MAAM,sBAAsB,eAAe,MAAM,eAAe,EAAE,qBAAqB,eAAe,CAAC;EAEvG,MAAM,EAAE,MAAM,SAAS,OAAO,WAAW,SAAS;EAGlD,SAAS,aAAa,OAA6B;GACjD,OAAO,CAAC,GAAI,MAAM,SAAS,CAAC,MAAM,MAAM,IAAI,CAAC,GAAI,GAAI,MAAM,WAAW,CAAC,CAAE;EAC3E;EAGA,SAAS,WAAW,OAAuB;GACzC,OAAO,QAAQ,MAAM,OAAO,KAAK,aAAa,KAAK,CAAC,CAAC,SAAS;EAChE;EAGA,SAAS,SAAS,OAAc,QAA2B;GACzD,OAAO,QAAQ;GACf,IAAI,OAAO,mBAAmB,OAC5B,QAAQ,MAAM,EAAE;EACpB;EAKA,MAAM,EAAE,QAAQ,cAAc,SAAS,oBAAoB,gBAAgB;EAM3E,MAAM,aAAa,gBAAgB;EAMnC,MAAM,gBAAgB,eAAe,KAAK,MAAM,MAAM,CAAC,KAAK,IAAI,GAAG,MAAM,UAAU,CAAC,CAAC;EACrF,MAAM,aAAa,eAAe,IAAI,IAAI,cAAc,MAAM,KAAI,UAAS,MAAM,EAAE,CAAC,CAAC;EAKrF,MAAM,UAAU,IAAI,KAAK;EACzB,MAAM,cAAc,IAAI,KAAK;EAG7B,MAAM,cAAc,IAAwB,IAAI;EAIhD,MAAM,YAAY,IAAmB,IAAI;EACzC,MAAM,cAAc,IAAI,CAAC;EACzB,IAAI,cAAc;EAClB,IAAI,iBAAA;EAIJ,MAAM,SAAS,eAAe,QAAQ,SAAS,YAAY,SAAS,UAAU,UAAU,IAAI;EAE5F,SAAS,aAAa,IAAgC;GACpD,OAAO,YAAY,OAAO,cAA2B,kCAAkC,GAAG,GAAG,KAAK;EACpG;;EAGA,MAAM,gBAAgB,IAAI,KAAK;EAE/B,SAAS,aAAmB;GAC1B,UAAU,QAAQ;GAClB,YAAY,QAAQ;GACpB,cAAc,QAAQ;EACxB;;;;;;;;;;;;;;EAeA,eAAe,cAAc,IAA2B;GACtD,cAAc,QAAQ;GACtB,MAAM,SAAS;GAEf,YAAY,QAAQ,gBAAgB,MAAM,aAAA;GAC1C,MAAM,SAAS;GAEf,QAAQ,EAAE;GACV,WAAW;EACb;;;;;EAMA,SAAS,aAAa,OAAuB;GAC3C,MAAM,SAAS,gBAAgB,MAAM;GAErC,OAAO,KAAK,KAAK,KAAK,MAAM,SAAS,QAAQ,QAAA;EAC/C;EAEA,MAAM,QAAQ,eAAe;GAC3B,gBAAgB,CAAC,MAAM;GACvB,UAAU,UAAU;IAClB,MAAM,SAAS,MAAM;IAGrB,IAAI,QAAQ,QAAQ,iBAAiB,GAAG,OAAO;IAE/C,MAAM,OAAO,QAAQ,QAAqB,iBAAiB;IAC3D,MAAM,KAAK,MAAM,QAAQ;IACzB,IAAI,CAAC,QAAQ,CAAC,IAAI,OAAO;IAEzB,UAAU,QAAQ;IAClB,YAAY,QAAQ;IACpB,cAAc,MAAM;IACpB,iBAAiB,KAAK,IAAA,IAA4B,KAAK,sBAAsB,CAAC,CAAC,QAAQ,qBAAqB;GAC9G;GACA,SAAS,UAAU;IAEjB,MAAM,eAAe;IACrB,YAAY,QAAQ,aAAa,MAAM,UAAU,WAAW;GAC9D;GACA,aAAa;IACX,MAAM,KAAK,UAAU;IAKrB,IAJe,OAAO,QACjB,KAAK,KAAK,YAAY,KAAK,MAAM,gBAAgB,MAAM,cACvD,KAAK,IAAI,YAAY,KAAK,KAAK,gBAExB,cAAmB,EAAE;SAC5B,WAAW;GAClB;GAGA,UAAU;EACZ,CAAC;EAED,SAAS,WAAW,OAAkD;GACpE,IAAI,UAAU,UAAU,MAAM,IAAI,OAAO,KAAA;GAEzC,OAAO;IACL,WAAW,cAAc,YAAY,MAAM;IAC3C,SAAS,OAAO,aAAa,YAAY,OAAO,cAAc,CAAC;IAI/D,GAAI,cAAc,QAAQ,CAAC,IAAI,EAAE,YAAY,OAAO;GACtD;EACF;;;;;EAMA,SAAS,eAAe,OAAsB,OAAoB;GAChE,IAAI,MAAM,QAAQ,YAAY,MAAM,QAAQ,aAAa;GAEzD,MAAM,eAAe;GAErB,MAAM,QAAQ,cAAc,MAAM,WAAU,SAAQ,KAAK,OAAO,MAAM,EAAE;GACxE,MAAM,OAAO,cAAc,MAAM,QAAQ,MAAM,cAAc,MAAM,QAAQ;GAE3E,QAAQ,MAAM,EAAE;GAGhB,IAAI,MAAM,eAAoB,aAAa,KAAK,EAAE,CAAC,EAAE,MAAM,CAAC;EAC9D;EAUA,kBAAkB;GAChB,KAAK,MAAM,SAAS,KAAK,OAGvB,IAFmB,MAAM,YAAY,KAAK,WAAW,MAAM,IAAI,MAAM,EAAE,KAAK,CAAC,OAAO,OAGlF,OAAO,MAAM,EAAE;QAEf,MAAM,MAAM,EAAE;EAEpB,CAAC;EAED,SAAS,QAAQ,MAA6B;GAC5C,OAAO,UAAU,SAAS,UAAU;EACtC;EAEA,MAAM,iBAAiB,eAAe,gBAAgB,MAAM,UAAU;EAEtE,MAAM,iBAAiB,eAAe;GACpC,IAAI,MAAM,UAAU,KAAA,GAAW,OAAO,KAAA;GAEtC,OAAO,EAAE,sBAAsB,OAAO,MAAM,UAAU,WAAW,GAAG,MAAM,MAAM,MAAM,MAAM,MAAM;EACpG,CAAC;;;;;;EAQD,SAAS,QAAiB;GACxB,MAAM,SAAS,YAAY,OAAO,cAA2B,iBAAiB;GAC9E,IAAI,CAAC,QAAQ,OAAO;GAEpB,OAAO,MAAM;GACb,OAAO;EACT;EAEA,MAAM,gBAAgB,gBAAgB;GACpC,SAAS,MAAM,gBAAgB,SAAS,cAAc,MAAM,SAAS;GACrE,UAAU,GAAG,MAAM,eAAe,OAAO;IAAE,SAAS;IAAe,iBAAiB;GAAK,EAAE;EAC7F,EAAE;EAEF,SAAS,cAAc,OAA4B;GACjD,IAAI,MAAM,GAAG,MAAM,eAAe;EACpC;EAEA,SAAa,EAAE,MAAM,CAAC;;uBAIpB,YAoGW,UAAA;IApGA,IAAI,MAAA,YAAA;IAAe,UAAQ,CAAG,MAAA,eAAA;oCACvC,mBAkGM,OAlGN,WAkGM;aAjGE;IAAJ,KAAI;MAEI,MAAA,UAAA,GAAU;IAClB,mBAAA;IACA,wBAAA;IACA,MAAK;IACJ,cAAY,oBAAA;IACb,OAAK,CAAC,0FACE,eAAA,KAAc;IACrB,OAAO,eAAA;IACP,cAAU,OAAA,OAAA,OAAA,MAAA,WAAE,QAAA,QAAO;IACnB,cAAU,OAAA,OAAA,OAAA,MAAA,WAAE,QAAA,QAAO;IACnB,WAAO,OAAA,OAAA,OAAA,MAAA,WAAE,YAAA,QAAW;IACpB,YAAQ,OAAA,OAAA,OAAA,MAAA,WAAE,YAAA,QAAW;QAExB,YAiFkB,iBAAA;IAhFd,KAAI;IACJ,OAAM;IACN,sBAAmB;IACnB,oBAAiB;IACjB,kBAAe;IACf,sBAAmB;IACnB,oBAAiB;IACjB,kBAAe;IACf,cAAW;;2BAGqB,EAAA,UAAA,IAAA,GADlC,mBAqEM,UAAA,MAAA,WApEc,cAAA,QAAT,UAAK;yBADhB,mBAqEM,OAAA;MAnED,KAAK,MAAM;MACZ,iBAAA;MACC,aAAW,MAAM;MACjB,iBAAe,MAAM;MACrB,MAAM,QAAQ,MAAM,IAAI,CAAA,CAAE;MAC3B,eAAY;MACZ,UAAS;MACT,OAAK,eAAA,CAAC,gPACE,QAAA,eAAY,yBAAA,EAAA,CAAA;MACnB,OAAK,eAAE,WAAW,KAAK,CAAA;MACvB,eAAW,OAAA,OAAA,OAAA,MAAA,GAAA,SAAE,MAAA,KAAA,CAAK,CAAC,SAAN,MAAA,KAAA,CAAK,CAAC,MAAK,GAAA,IAAA;MACxB,YAAO,WAAE,eAAe,QAAQ,KAAK;SAExC,mBAwCM,OAxCN,YAwCM;MAvCJ,YAES,MAAA,cAAA,GAAA;OAFD,MAAK;OAAK,OAAM;OAAU,OAAK,eAAA,EAAA,OAAW,QAAQ,MAAM,IAAI,CAAA,CAAE,MAAK,CAAA;;8BAC7B,EAAA,UAAA,GAA5C,YAA4C,wBAA5B,QAAQ,MAAM,IAAI,CAAA,CAAE,IAAI,CAAA,EAAA,CAAA;;;MAE1C,mBAwBM,OAxBN,YAwBM;OAvBJ,mBAEM,OAFN,YAEM,gBADD,MAAM,KAAK,GAAA,CAAA;OAEL,MAAM,WAAA,UAAA,GAAjB,mBAEM,OAFN,YAEM,gBADD,MAAM,OAAO,GAAA,CAAA,KAAA,mBAAA,IAAA,IAAA;OAKP,WAAW,KAAK,KAAA,UAAA,GAA3B,mBAaM,OAbN,YAaM,CAZJ,WAWO,KAAA,QAAA,WAAA;QAXsB;QAAQ,eAAe,MAAA,OAAA,CAAO,CAAC,MAAM,EAAE;gBAW7D,EAAA,UAAA,IAAA,GAVL,mBASW,UAAA,MAAA,WARmB,aAAa,KAAK,IAApC,QAAQ,UAAK;4BADzB,YASW,MAAA,gBAAA,GAAA;SAPN,KAAK;SACL,SAAS,OAAO,WAAO;SACvB,MAAM,OAAO,QAAI;SAClB,wBAAA;SACC,UAAK,WAAE,SAAS,OAAO,MAAM;;gCAEd,CAAA,gBAAA,gBAAf,OAAO,KAAK,GAAA,CAAA,CAAA,CAAA;;;;;;;;;MAKvB,YAUW,MAAA,gBAAA,GAAA;OATP,SAAQ;OACR,MAAK;OACL,QAAA;OACC,cAAY,qBAAA;OACZ,UAAK,WAAE,MAAA,OAAA,CAAO,CAAC,MAAM,EAAE;;8BAIjB,CAFT,YAES,MAAA,cAAA,GAAA,EAFD,MAAK,KAAI,GAAA;+BACF,CAAb,YAAa,MAAA,SAAA,CAAA,CAAA,CAAA;;;;;SAOT,MAAM,YAAS,KAAA,UAAA,GADzB,mBAUE,OAAA;;MARE,0BAAA;MACA,eAAY;MACZ,OAAM;MACL,OAAK,eAAA;wBAAqC,QAAQ,MAAM,IAAI,CAAA,CAAE;6BAA6C,MAAM,UAAS;2BAA0C,OAAA,QAAM,WAAA;;;;;4CAxFvK,cAAA,KAAa,CAAA,CAAA,CAAA,GAAA,GAAA,CAAA,MAAA,UAAA,CAAA"}
@@ -1,3 +1,3 @@
1
1
  import { n as grToasterSafelist, t as grToasterConfig } from "../../chunks/config-ScuvHw6k2.js";
2
- import { t as GrToaster_default } from "./chunks/GrToaster-Ct-39hga.js";
2
+ import { t as GrToaster_default } from "./chunks/GrToaster-Dqja0Lf9.js";
3
3
  export { GrToaster_default as GrToaster, GrToaster_default as default, grToasterConfig, grToasterSafelist };
@@ -1,6 +1,6 @@
1
1
  import { t as info_default } from "../../../chunks/info-Be2OccTu.js";
2
2
  import { c as useGrThemeAttrs, o as useGrComponentSize } from "../../../chunks/context-BSYR7RZs.js";
3
- import { t as usePortalTarget } from "../../../chunks/usePortalTarget-RyzEgNTb.js";
3
+ import { t as usePortalTarget } from "../../../chunks/usePortalTarget-Cn-Wiz5d.js";
4
4
  import { vClickOutside } from "../../../directives/clickOutside.js";
5
5
  import { t as GrIcon_default } from "../../GrIcon/chunks/GrIcon-BvA2ZGXP.js";
6
6
  import { useFloating } from "../../../composables/useFloating.js";
@@ -1 +1 @@
1
- {"version":3,"file":"GrTooltip-xAcqs0bP.js","names":[],"sources":["../../../../src/components/GrTooltip/GrTooltip.vue","../../../../src/components/GrTooltip/GrTooltip.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * GrTooltip — GR-примитив инлайн-подсказки на hover/focus.\n *\n * По умолчанию триггер — иконка info; можно заменить через слот.\n * A11y:\n * - `role=\"tooltip\"` связывается с триггером через уникальный `aria-describedby`;\n * - подсказка закрывается по `Escape` (WCAG 1.4.13 «Content on Hover or Focus» —\n * dismissible без необходимости двигать курсор/фокус);\n * - панель не интерактивна (`pointer-events-none`), поэтому «hoverable»-требование\n * того же критерия неприменимо — навести курсор на саму подсказку невозможно.\n *\n * Позиционирование — через общий `useFloating` (см. `composables/internal`):\n * `placement` задаёт предпочтительную сторону, `flip` переворачивает подсказку,\n * если места не хватает, а `shift` не даёт ей вылезти за края viewport.\n */\nimport { computed, onMounted, onUnmounted, onUpdated, ref, useId, useSlots, watchEffect } from 'vue'\n\nimport { usePortalTarget } from '../../composables/usePortalTarget'\n\nimport { vClickOutside } from '../../directives'\nimport GrIcon from '../GrIcon'\nimport { useFloating, type UseFloatingPlacement } from '../../composables/useFloating'\nimport { useDismissible } from '../../composables/useDismissible'\nimport { useGrComponentSize, useGrThemeAttrs } from '../GrConfigProvider/context'\nimport { type GrTooltipSize, panelSizes, panelWidths, triggerIconSizes } from './grTooltipStyles'\n\nimport IconInfo from '~icons/lucide/info'\n\nexport type GrTooltipPlacement = UseFloatingPlacement\n\nexport interface GrTooltipProps {\n /** Текст подсказки. Разметка вместо текста — слот `#content`. */\n text?: string\n /** Цвет триггер-иконки (CSS color). По умолчанию — `var(--gr-muted-fg)`. */\n iconColor?: string\n /** Размер панели и дефолтной триггер-иконки. */\n size?: GrTooltipSize\n /** Предпочтительная сторона панели; `flip` может её изменить. */\n placement?: GrTooltipPlacement\n /** Зазор между триггером и панелью, px. */\n offsetPx?: number\n /** Задержка перед показом, мс. Спасает от мигания при проведении курсором по панели кнопок. */\n openDelay?: number\n /** Задержка перед скрытием, мс. */\n closeDelay?: number\n /** Подсказка не показывается ничем — ни курсором, ни фокусом, ни `v-model:open`. */\n disabled?: boolean\n /** Управляемая видимость. Без неё компонент ведёт видимость сам. */\n open?: boolean\n}\n\nexport interface GrTooltipEmits {\n (e: 'update:open', value: boolean): void\n}\n\nconst props = withDefaults(defineProps<GrTooltipProps>(), {\n text: undefined,\n iconColor: 'var(--gr-muted-fg)',\n size: undefined,\n placement: 'top',\n offsetPx: undefined,\n openDelay: 0,\n closeDelay: 0,\n disabled: false,\n open: undefined,\n})\n\nconst emit = defineEmits<GrTooltipEmits>()\n\ndefineSlots<{\n /** Триггер. По умолчанию — иконка info. */\n default?: () => unknown\n /** Содержимое подсказки; заменяет проп `text`. */\n content?: () => unknown\n}>()\n\nconst slots = useSlots()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrTooltip' })\n\nconst panelClass = computed(() => [\n panelSizes[resolvedSize.value],\n panelWidths[resolvedSize.value],\n].join(' '))\nconst triggerIconSize = computed(() => triggerIconSizes[resolvedSize.value])\n\nconst tooltipId = `gr-tooltip-${useId()}`\n\nconst triggerStyle = computed(() => ({ color: props.iconColor }))\n\n// Телепорт включается только ПОСЛЕ монтирования: иначе первый клиентский\n// рендер не совпадает с серверным и ломается гидрация (см. композабл).\nconst { target: portalTarget, enabled: teleportEnabled } = usePortalTarget()\n\n// Тема поддерева на телепортированную панель: в DOM она уезжает в `body`, то\n// есть вне обёртки провайдера, и `data-theme` с неё не наследуется. В дереве\n// компонентов панель остаётся внутри — `inject` доходит, и тему она ставит себе\n// сама.\nconst themeAttrs = useGrThemeAttrs()\n\nconst uncontrolledOpen = ref(false)\nconst triggerEl = ref<HTMLElement | null>(null)\nconst panelEl = ref<HTMLElement | null>(null)\n\n/** Показывать нечего — пустая панель хуже отсутствующей. */\nconst hasContent = computed(() => Boolean(slots.content) || Boolean(props.text))\n\nconst open = computed<boolean>({\n get: () => {\n if (props.disabled || !hasContent.value) return false\n return props.open ?? uncontrolledOpen.value\n },\n set: (next) => {\n uncontrolledOpen.value = next\n emit('update:open', next)\n },\n})\n\nconst { floatingStyle } = useFloating(triggerEl, panelEl, open, {\n placement: () => props.placement,\n // Геттер, а не значение: `useFloating` читает опцию на каждом пересчёте, и\n // так проп остаётся живым, а не замирает на моменте setup.\n get offsetPx() { return props.offsetPx },\n zIndexVar: '--gr-z-tooltip',\n})\n\nlet delayTimer: ReturnType<typeof setTimeout> | undefined\n\nfunction schedule(next: boolean, delayMs: number): void {\n clearTimeout(delayTimer)\n\n if (delayMs <= 0) {\n open.value = next\n return\n }\n\n delayTimer = setTimeout(() => { open.value = next }, delayMs)\n}\n\nfunction show(): void {\n if (props.disabled) return\n schedule(true, props.openDelay)\n}\n\nfunction hide(): void {\n schedule(false, props.closeDelay)\n}\n\n/** Escape и клик вне закрывают мгновенно: задержка тут читалась бы как залипание. */\nfunction dismiss(): void {\n clearTimeout(delayTimer)\n if (open.value) open.value = false\n}\n\n// Нужен только тач-режиму: курсор и фокус закрывают подсказку сами.\nconst dismissOnOutside = computed(() => ({ handler: dismiss, enabled: open.value }))\n\n/**\n * На тач-устройствах hover не существует, а фокус по тапу не гарантирован —\n * без этого подсказка там недоступна вовсе. Тап по триггеру переключает\n * панель, тап вне — закрывает (`vClickOutside`).\n */\nfunction onTouch(): void {\n if (open.value) dismiss()\n else show()\n}\n\nonUnmounted(() => clearTimeout(delayTimer))\n\nuseDismissible(open, dismiss)\n\nconst FOCUSABLE_SELECTOR = 'a[href],button,input,select,textarea,[tabindex]:not([tabindex=\"-1\"])'\n\n/**\n * Типовое употребление — подсказка у кнопки. Фокусируемая обёртка вокруг\n * фокусируемого контрола дала бы два таб-стопа на один визуальный элемент,\n * поэтому обёртка забирает `tabindex` только когда в слоте фокусироваться\n * нечему (текст, иконка). Разметку слота пишет потребитель, привязать к ней\n * `aria-describedby` из шаблона нельзя — отсюда единственная точка, где атрибут\n * ставится вручную.\n */\nconst slotFocusableEl = ref<HTMLElement | null>(null)\n\nfunction syncSlotFocusable(): void {\n slotFocusableEl.value = slots.default\n ? triggerEl.value?.querySelector<HTMLElement>(FOCUSABLE_SELECTOR) ?? null\n : null\n}\n\nonMounted(syncSlotFocusable)\nonUpdated(syncSlotFocusable)\n\nwatchEffect((onCleanup) => {\n const el = slotFocusableEl.value\n if (!el) return\n\n el.setAttribute('aria-describedby', tooltipId)\n onCleanup(() => el.removeAttribute('aria-describedby'))\n})\n\nconst wrapperTabindex = computed(() => (slotFocusableEl.value ? undefined : 0))\nconst wrapperDescribedBy = computed(() => (slotFocusableEl.value ? undefined : tooltipId))\n</script>\n\n<template>\n <span\n v-click-outside=\"dismissOnOutside\"\n data-gr-tooltip\n class=\"relative inline-flex\"\n >\n <span\n ref=\"triggerEl\"\n data-gr-tooltip-trigger\n data-testid=\"gr-tooltip-trigger\"\n :tabindex=\"wrapperTabindex\"\n :aria-describedby=\"wrapperDescribedBy\"\n class=\"inline-flex items-center justify-center focus:outline-none\"\n :style=\"triggerStyle\"\n @mouseenter=\"show\"\n @mouseleave=\"hide\"\n @focusin=\"show\"\n @focusout=\"hide\"\n @touchstart.passive=\"onTouch\"\n >\n <slot>\n <GrIcon :size=\"triggerIconSize\">\n <IconInfo />\n </GrIcon>\n </slot>\n </span>\n\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <transition\n enter-active-class=\"transition-opacity duration-[var(--gr-duration-fast)]\"\n enter-from-class=\"opacity-0\"\n enter-to-class=\"opacity-100\"\n leave-active-class=\"transition-opacity duration-[var(--gr-duration-fast)]\"\n leave-from-class=\"opacity-100\"\n leave-to-class=\"opacity-0\"\n >\n <span\n v-show=\"open\"\n :id=\"tooltipId\"\n ref=\"panelEl\"\n role=\"tooltip\"\n v-bind=\"themeAttrs\"\n data-gr-tooltip-panel\n data-gr-overlay-root\n class=\"pointer-events-none rounded-[var(--gr-radius-control)] border border-[var(--gr-brd)] bg-[var(--gr-popover)] text-[var(--gr-popover-fg)] shadow-[var(--gr-shadow-1)]\"\n :class=\"panelClass\"\n :style=\"floatingStyle\"\n >\n <slot name=\"content\">{{ text }}</slot>\n </span>\n </transition>\n </teleport>\n </span>\n</template>\n","<script setup lang=\"ts\">\n/**\n * GrTooltip — GR-примитив инлайн-подсказки на hover/focus.\n *\n * По умолчанию триггер — иконка info; можно заменить через слот.\n * A11y:\n * - `role=\"tooltip\"` связывается с триггером через уникальный `aria-describedby`;\n * - подсказка закрывается по `Escape` (WCAG 1.4.13 «Content on Hover or Focus» —\n * dismissible без необходимости двигать курсор/фокус);\n * - панель не интерактивна (`pointer-events-none`), поэтому «hoverable»-требование\n * того же критерия неприменимо — навести курсор на саму подсказку невозможно.\n *\n * Позиционирование — через общий `useFloating` (см. `composables/internal`):\n * `placement` задаёт предпочтительную сторону, `flip` переворачивает подсказку,\n * если места не хватает, а `shift` не даёт ей вылезти за края viewport.\n */\nimport { computed, onMounted, onUnmounted, onUpdated, ref, useId, useSlots, watchEffect } from 'vue'\n\nimport { usePortalTarget } from '../../composables/usePortalTarget'\n\nimport { vClickOutside } from '../../directives'\nimport GrIcon from '../GrIcon'\nimport { useFloating, type UseFloatingPlacement } from '../../composables/useFloating'\nimport { useDismissible } from '../../composables/useDismissible'\nimport { useGrComponentSize, useGrThemeAttrs } from '../GrConfigProvider/context'\nimport { type GrTooltipSize, panelSizes, panelWidths, triggerIconSizes } from './grTooltipStyles'\n\nimport IconInfo from '~icons/lucide/info'\n\nexport type GrTooltipPlacement = UseFloatingPlacement\n\nexport interface GrTooltipProps {\n /** Текст подсказки. Разметка вместо текста — слот `#content`. */\n text?: string\n /** Цвет триггер-иконки (CSS color). По умолчанию — `var(--gr-muted-fg)`. */\n iconColor?: string\n /** Размер панели и дефолтной триггер-иконки. */\n size?: GrTooltipSize\n /** Предпочтительная сторона панели; `flip` может её изменить. */\n placement?: GrTooltipPlacement\n /** Зазор между триггером и панелью, px. */\n offsetPx?: number\n /** Задержка перед показом, мс. Спасает от мигания при проведении курсором по панели кнопок. */\n openDelay?: number\n /** Задержка перед скрытием, мс. */\n closeDelay?: number\n /** Подсказка не показывается ничем — ни курсором, ни фокусом, ни `v-model:open`. */\n disabled?: boolean\n /** Управляемая видимость. Без неё компонент ведёт видимость сам. */\n open?: boolean\n}\n\nexport interface GrTooltipEmits {\n (e: 'update:open', value: boolean): void\n}\n\nconst props = withDefaults(defineProps<GrTooltipProps>(), {\n text: undefined,\n iconColor: 'var(--gr-muted-fg)',\n size: undefined,\n placement: 'top',\n offsetPx: undefined,\n openDelay: 0,\n closeDelay: 0,\n disabled: false,\n open: undefined,\n})\n\nconst emit = defineEmits<GrTooltipEmits>()\n\ndefineSlots<{\n /** Триггер. По умолчанию — иконка info. */\n default?: () => unknown\n /** Содержимое подсказки; заменяет проп `text`. */\n content?: () => unknown\n}>()\n\nconst slots = useSlots()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrTooltip' })\n\nconst panelClass = computed(() => [\n panelSizes[resolvedSize.value],\n panelWidths[resolvedSize.value],\n].join(' '))\nconst triggerIconSize = computed(() => triggerIconSizes[resolvedSize.value])\n\nconst tooltipId = `gr-tooltip-${useId()}`\n\nconst triggerStyle = computed(() => ({ color: props.iconColor }))\n\n// Телепорт включается только ПОСЛЕ монтирования: иначе первый клиентский\n// рендер не совпадает с серверным и ломается гидрация (см. композабл).\nconst { target: portalTarget, enabled: teleportEnabled } = usePortalTarget()\n\n// Тема поддерева на телепортированную панель: в DOM она уезжает в `body`, то\n// есть вне обёртки провайдера, и `data-theme` с неё не наследуется. В дереве\n// компонентов панель остаётся внутри — `inject` доходит, и тему она ставит себе\n// сама.\nconst themeAttrs = useGrThemeAttrs()\n\nconst uncontrolledOpen = ref(false)\nconst triggerEl = ref<HTMLElement | null>(null)\nconst panelEl = ref<HTMLElement | null>(null)\n\n/** Показывать нечего — пустая панель хуже отсутствующей. */\nconst hasContent = computed(() => Boolean(slots.content) || Boolean(props.text))\n\nconst open = computed<boolean>({\n get: () => {\n if (props.disabled || !hasContent.value) return false\n return props.open ?? uncontrolledOpen.value\n },\n set: (next) => {\n uncontrolledOpen.value = next\n emit('update:open', next)\n },\n})\n\nconst { floatingStyle } = useFloating(triggerEl, panelEl, open, {\n placement: () => props.placement,\n // Геттер, а не значение: `useFloating` читает опцию на каждом пересчёте, и\n // так проп остаётся живым, а не замирает на моменте setup.\n get offsetPx() { return props.offsetPx },\n zIndexVar: '--gr-z-tooltip',\n})\n\nlet delayTimer: ReturnType<typeof setTimeout> | undefined\n\nfunction schedule(next: boolean, delayMs: number): void {\n clearTimeout(delayTimer)\n\n if (delayMs <= 0) {\n open.value = next\n return\n }\n\n delayTimer = setTimeout(() => { open.value = next }, delayMs)\n}\n\nfunction show(): void {\n if (props.disabled) return\n schedule(true, props.openDelay)\n}\n\nfunction hide(): void {\n schedule(false, props.closeDelay)\n}\n\n/** Escape и клик вне закрывают мгновенно: задержка тут читалась бы как залипание. */\nfunction dismiss(): void {\n clearTimeout(delayTimer)\n if (open.value) open.value = false\n}\n\n// Нужен только тач-режиму: курсор и фокус закрывают подсказку сами.\nconst dismissOnOutside = computed(() => ({ handler: dismiss, enabled: open.value }))\n\n/**\n * На тач-устройствах hover не существует, а фокус по тапу не гарантирован —\n * без этого подсказка там недоступна вовсе. Тап по триггеру переключает\n * панель, тап вне — закрывает (`vClickOutside`).\n */\nfunction onTouch(): void {\n if (open.value) dismiss()\n else show()\n}\n\nonUnmounted(() => clearTimeout(delayTimer))\n\nuseDismissible(open, dismiss)\n\nconst FOCUSABLE_SELECTOR = 'a[href],button,input,select,textarea,[tabindex]:not([tabindex=\"-1\"])'\n\n/**\n * Типовое употребление — подсказка у кнопки. Фокусируемая обёртка вокруг\n * фокусируемого контрола дала бы два таб-стопа на один визуальный элемент,\n * поэтому обёртка забирает `tabindex` только когда в слоте фокусироваться\n * нечему (текст, иконка). Разметку слота пишет потребитель, привязать к ней\n * `aria-describedby` из шаблона нельзя — отсюда единственная точка, где атрибут\n * ставится вручную.\n */\nconst slotFocusableEl = ref<HTMLElement | null>(null)\n\nfunction syncSlotFocusable(): void {\n slotFocusableEl.value = slots.default\n ? triggerEl.value?.querySelector<HTMLElement>(FOCUSABLE_SELECTOR) ?? null\n : null\n}\n\nonMounted(syncSlotFocusable)\nonUpdated(syncSlotFocusable)\n\nwatchEffect((onCleanup) => {\n const el = slotFocusableEl.value\n if (!el) return\n\n el.setAttribute('aria-describedby', tooltipId)\n onCleanup(() => el.removeAttribute('aria-describedby'))\n})\n\nconst wrapperTabindex = computed(() => (slotFocusableEl.value ? undefined : 0))\nconst wrapperDescribedBy = computed(() => (slotFocusableEl.value ? undefined : tooltipId))\n</script>\n\n<template>\n <span\n v-click-outside=\"dismissOnOutside\"\n data-gr-tooltip\n class=\"relative inline-flex\"\n >\n <span\n ref=\"triggerEl\"\n data-gr-tooltip-trigger\n data-testid=\"gr-tooltip-trigger\"\n :tabindex=\"wrapperTabindex\"\n :aria-describedby=\"wrapperDescribedBy\"\n class=\"inline-flex items-center justify-center focus:outline-none\"\n :style=\"triggerStyle\"\n @mouseenter=\"show\"\n @mouseleave=\"hide\"\n @focusin=\"show\"\n @focusout=\"hide\"\n @touchstart.passive=\"onTouch\"\n >\n <slot>\n <GrIcon :size=\"triggerIconSize\">\n <IconInfo />\n </GrIcon>\n </slot>\n </span>\n\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <transition\n enter-active-class=\"transition-opacity duration-[var(--gr-duration-fast)]\"\n enter-from-class=\"opacity-0\"\n enter-to-class=\"opacity-100\"\n leave-active-class=\"transition-opacity duration-[var(--gr-duration-fast)]\"\n leave-from-class=\"opacity-100\"\n leave-to-class=\"opacity-0\"\n >\n <span\n v-show=\"open\"\n :id=\"tooltipId\"\n ref=\"panelEl\"\n role=\"tooltip\"\n v-bind=\"themeAttrs\"\n data-gr-tooltip-panel\n data-gr-overlay-root\n class=\"pointer-events-none rounded-[var(--gr-radius-control)] border border-[var(--gr-brd)] bg-[var(--gr-popover)] text-[var(--gr-popover-fg)] shadow-[var(--gr-shadow-1)]\"\n :class=\"panelClass\"\n :style=\"floatingStyle\"\n >\n <slot name=\"content\">{{ text }}</slot>\n </span>\n </transition>\n </teleport>\n </span>\n</template>\n"],"mappings":"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"}
1
+ {"version":3,"file":"GrTooltip-Cn08heWz.js","names":[],"sources":["../../../../src/components/GrTooltip/GrTooltip.vue","../../../../src/components/GrTooltip/GrTooltip.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * GrTooltip — GR-примитив инлайн-подсказки на hover/focus.\n *\n * По умолчанию триггер — иконка info; можно заменить через слот.\n * A11y:\n * - `role=\"tooltip\"` связывается с триггером через уникальный `aria-describedby`;\n * - подсказка закрывается по `Escape` (WCAG 1.4.13 «Content on Hover or Focus» —\n * dismissible без необходимости двигать курсор/фокус);\n * - панель не интерактивна (`pointer-events-none`), поэтому «hoverable»-требование\n * того же критерия неприменимо — навести курсор на саму подсказку невозможно.\n *\n * Позиционирование — через общий `useFloating` (см. `composables/internal`):\n * `placement` задаёт предпочтительную сторону, `flip` переворачивает подсказку,\n * если места не хватает, а `shift` не даёт ей вылезти за края viewport.\n */\nimport { computed, onMounted, onUnmounted, onUpdated, ref, useId, useSlots, watchEffect } from 'vue'\n\nimport { usePortalTarget } from '../../composables/usePortalTarget'\n\nimport { vClickOutside } from '../../directives'\nimport GrIcon from '../GrIcon'\nimport { useFloating, type UseFloatingPlacement } from '../../composables/useFloating'\nimport { useDismissible } from '../../composables/useDismissible'\nimport { useGrComponentSize, useGrThemeAttrs } from '../GrConfigProvider/context'\nimport { type GrTooltipSize, panelSizes, panelWidths, triggerIconSizes } from './grTooltipStyles'\n\nimport IconInfo from '~icons/lucide/info'\n\nexport type GrTooltipPlacement = UseFloatingPlacement\n\nexport interface GrTooltipProps {\n /** Текст подсказки. Разметка вместо текста — слот `#content`. */\n text?: string\n /** Цвет триггер-иконки (CSS color). По умолчанию — `var(--gr-muted-fg)`. */\n iconColor?: string\n /** Размер панели и дефолтной триггер-иконки. */\n size?: GrTooltipSize\n /** Предпочтительная сторона панели; `flip` может её изменить. */\n placement?: GrTooltipPlacement\n /** Зазор между триггером и панелью, px. */\n offsetPx?: number\n /** Задержка перед показом, мс. Спасает от мигания при проведении курсором по панели кнопок. */\n openDelay?: number\n /** Задержка перед скрытием, мс. */\n closeDelay?: number\n /** Подсказка не показывается ничем — ни курсором, ни фокусом, ни `v-model:open`. */\n disabled?: boolean\n /** Управляемая видимость. Без неё компонент ведёт видимость сам. */\n open?: boolean\n}\n\nexport interface GrTooltipEmits {\n (e: 'update:open', value: boolean): void\n}\n\nconst props = withDefaults(defineProps<GrTooltipProps>(), {\n text: undefined,\n iconColor: 'var(--gr-muted-fg)',\n size: undefined,\n placement: 'top',\n offsetPx: undefined,\n openDelay: 0,\n closeDelay: 0,\n disabled: false,\n open: undefined,\n})\n\nconst emit = defineEmits<GrTooltipEmits>()\n\ndefineSlots<{\n /** Триггер. По умолчанию — иконка info. */\n default?: () => unknown\n /** Содержимое подсказки; заменяет проп `text`. */\n content?: () => unknown\n}>()\n\nconst slots = useSlots()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrTooltip' })\n\nconst panelClass = computed(() => [\n panelSizes[resolvedSize.value],\n panelWidths[resolvedSize.value],\n].join(' '))\nconst triggerIconSize = computed(() => triggerIconSizes[resolvedSize.value])\n\nconst tooltipId = `gr-tooltip-${useId()}`\n\nconst triggerStyle = computed(() => ({ color: props.iconColor }))\n\n// Телепорт включается только ПОСЛЕ монтирования: иначе первый клиентский\n// рендер не совпадает с серверным и ломается гидрация (см. композабл).\nconst { target: portalTarget, enabled: teleportEnabled } = usePortalTarget()\n\n// Тема поддерева на телепортированную панель: в DOM она уезжает в `body`, то\n// есть вне обёртки провайдера, и `data-theme` с неё не наследуется. В дереве\n// компонентов панель остаётся внутри — `inject` доходит, и тему она ставит себе\n// сама.\nconst themeAttrs = useGrThemeAttrs()\n\nconst uncontrolledOpen = ref(false)\nconst triggerEl = ref<HTMLElement | null>(null)\nconst panelEl = ref<HTMLElement | null>(null)\n\n/** Показывать нечего — пустая панель хуже отсутствующей. */\nconst hasContent = computed(() => Boolean(slots.content) || Boolean(props.text))\n\nconst open = computed<boolean>({\n get: () => {\n if (props.disabled || !hasContent.value) return false\n return props.open ?? uncontrolledOpen.value\n },\n set: (next) => {\n uncontrolledOpen.value = next\n emit('update:open', next)\n },\n})\n\nconst { floatingStyle } = useFloating(triggerEl, panelEl, open, {\n placement: () => props.placement,\n // Геттер, а не значение: `useFloating` читает опцию на каждом пересчёте, и\n // так проп остаётся живым, а не замирает на моменте setup.\n get offsetPx() { return props.offsetPx },\n zIndexVar: '--gr-z-tooltip',\n})\n\nlet delayTimer: ReturnType<typeof setTimeout> | undefined\n\nfunction schedule(next: boolean, delayMs: number): void {\n clearTimeout(delayTimer)\n\n if (delayMs <= 0) {\n open.value = next\n return\n }\n\n delayTimer = setTimeout(() => { open.value = next }, delayMs)\n}\n\nfunction show(): void {\n if (props.disabled) return\n schedule(true, props.openDelay)\n}\n\nfunction hide(): void {\n schedule(false, props.closeDelay)\n}\n\n/** Escape и клик вне закрывают мгновенно: задержка тут читалась бы как залипание. */\nfunction dismiss(): void {\n clearTimeout(delayTimer)\n if (open.value) open.value = false\n}\n\n// Нужен только тач-режиму: курсор и фокус закрывают подсказку сами.\nconst dismissOnOutside = computed(() => ({ handler: dismiss, enabled: open.value }))\n\n/**\n * На тач-устройствах hover не существует, а фокус по тапу не гарантирован —\n * без этого подсказка там недоступна вовсе. Тап по триггеру переключает\n * панель, тап вне — закрывает (`vClickOutside`).\n */\nfunction onTouch(): void {\n if (open.value) dismiss()\n else show()\n}\n\nonUnmounted(() => clearTimeout(delayTimer))\n\nuseDismissible(open, dismiss)\n\nconst FOCUSABLE_SELECTOR = 'a[href],button,input,select,textarea,[tabindex]:not([tabindex=\"-1\"])'\n\n/**\n * Типовое употребление — подсказка у кнопки. Фокусируемая обёртка вокруг\n * фокусируемого контрола дала бы два таб-стопа на один визуальный элемент,\n * поэтому обёртка забирает `tabindex` только когда в слоте фокусироваться\n * нечему (текст, иконка). Разметку слота пишет потребитель, привязать к ней\n * `aria-describedby` из шаблона нельзя — отсюда единственная точка, где атрибут\n * ставится вручную.\n */\nconst slotFocusableEl = ref<HTMLElement | null>(null)\n\nfunction syncSlotFocusable(): void {\n slotFocusableEl.value = slots.default\n ? triggerEl.value?.querySelector<HTMLElement>(FOCUSABLE_SELECTOR) ?? null\n : null\n}\n\nonMounted(syncSlotFocusable)\nonUpdated(syncSlotFocusable)\n\nwatchEffect((onCleanup) => {\n const el = slotFocusableEl.value\n if (!el) return\n\n el.setAttribute('aria-describedby', tooltipId)\n onCleanup(() => el.removeAttribute('aria-describedby'))\n})\n\nconst wrapperTabindex = computed(() => (slotFocusableEl.value ? undefined : 0))\nconst wrapperDescribedBy = computed(() => (slotFocusableEl.value ? undefined : tooltipId))\n</script>\n\n<template>\n <span\n v-click-outside=\"dismissOnOutside\"\n data-gr-tooltip\n class=\"relative inline-flex\"\n >\n <span\n ref=\"triggerEl\"\n data-gr-tooltip-trigger\n data-testid=\"gr-tooltip-trigger\"\n :tabindex=\"wrapperTabindex\"\n :aria-describedby=\"wrapperDescribedBy\"\n class=\"inline-flex items-center justify-center focus:outline-none\"\n :style=\"triggerStyle\"\n @mouseenter=\"show\"\n @mouseleave=\"hide\"\n @focusin=\"show\"\n @focusout=\"hide\"\n @touchstart.passive=\"onTouch\"\n >\n <slot>\n <GrIcon :size=\"triggerIconSize\">\n <IconInfo />\n </GrIcon>\n </slot>\n </span>\n\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <transition\n enter-active-class=\"transition-opacity duration-[var(--gr-duration-fast)]\"\n enter-from-class=\"opacity-0\"\n enter-to-class=\"opacity-100\"\n leave-active-class=\"transition-opacity duration-[var(--gr-duration-fast)]\"\n leave-from-class=\"opacity-100\"\n leave-to-class=\"opacity-0\"\n >\n <span\n v-show=\"open\"\n :id=\"tooltipId\"\n ref=\"panelEl\"\n role=\"tooltip\"\n v-bind=\"themeAttrs\"\n data-gr-tooltip-panel\n data-gr-overlay-root\n class=\"pointer-events-none rounded-[var(--gr-radius-control)] border border-[var(--gr-brd)] bg-[var(--gr-popover)] text-[var(--gr-popover-fg)] shadow-[var(--gr-shadow-1)]\"\n :class=\"panelClass\"\n :style=\"floatingStyle\"\n >\n <slot name=\"content\">{{ text }}</slot>\n </span>\n </transition>\n </teleport>\n </span>\n</template>\n","<script setup lang=\"ts\">\n/**\n * GrTooltip — GR-примитив инлайн-подсказки на hover/focus.\n *\n * По умолчанию триггер — иконка info; можно заменить через слот.\n * A11y:\n * - `role=\"tooltip\"` связывается с триггером через уникальный `aria-describedby`;\n * - подсказка закрывается по `Escape` (WCAG 1.4.13 «Content on Hover or Focus» —\n * dismissible без необходимости двигать курсор/фокус);\n * - панель не интерактивна (`pointer-events-none`), поэтому «hoverable»-требование\n * того же критерия неприменимо — навести курсор на саму подсказку невозможно.\n *\n * Позиционирование — через общий `useFloating` (см. `composables/internal`):\n * `placement` задаёт предпочтительную сторону, `flip` переворачивает подсказку,\n * если места не хватает, а `shift` не даёт ей вылезти за края viewport.\n */\nimport { computed, onMounted, onUnmounted, onUpdated, ref, useId, useSlots, watchEffect } from 'vue'\n\nimport { usePortalTarget } from '../../composables/usePortalTarget'\n\nimport { vClickOutside } from '../../directives'\nimport GrIcon from '../GrIcon'\nimport { useFloating, type UseFloatingPlacement } from '../../composables/useFloating'\nimport { useDismissible } from '../../composables/useDismissible'\nimport { useGrComponentSize, useGrThemeAttrs } from '../GrConfigProvider/context'\nimport { type GrTooltipSize, panelSizes, panelWidths, triggerIconSizes } from './grTooltipStyles'\n\nimport IconInfo from '~icons/lucide/info'\n\nexport type GrTooltipPlacement = UseFloatingPlacement\n\nexport interface GrTooltipProps {\n /** Текст подсказки. Разметка вместо текста — слот `#content`. */\n text?: string\n /** Цвет триггер-иконки (CSS color). По умолчанию — `var(--gr-muted-fg)`. */\n iconColor?: string\n /** Размер панели и дефолтной триггер-иконки. */\n size?: GrTooltipSize\n /** Предпочтительная сторона панели; `flip` может её изменить. */\n placement?: GrTooltipPlacement\n /** Зазор между триггером и панелью, px. */\n offsetPx?: number\n /** Задержка перед показом, мс. Спасает от мигания при проведении курсором по панели кнопок. */\n openDelay?: number\n /** Задержка перед скрытием, мс. */\n closeDelay?: number\n /** Подсказка не показывается ничем — ни курсором, ни фокусом, ни `v-model:open`. */\n disabled?: boolean\n /** Управляемая видимость. Без неё компонент ведёт видимость сам. */\n open?: boolean\n}\n\nexport interface GrTooltipEmits {\n (e: 'update:open', value: boolean): void\n}\n\nconst props = withDefaults(defineProps<GrTooltipProps>(), {\n text: undefined,\n iconColor: 'var(--gr-muted-fg)',\n size: undefined,\n placement: 'top',\n offsetPx: undefined,\n openDelay: 0,\n closeDelay: 0,\n disabled: false,\n open: undefined,\n})\n\nconst emit = defineEmits<GrTooltipEmits>()\n\ndefineSlots<{\n /** Триггер. По умолчанию — иконка info. */\n default?: () => unknown\n /** Содержимое подсказки; заменяет проп `text`. */\n content?: () => unknown\n}>()\n\nconst slots = useSlots()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrTooltip' })\n\nconst panelClass = computed(() => [\n panelSizes[resolvedSize.value],\n panelWidths[resolvedSize.value],\n].join(' '))\nconst triggerIconSize = computed(() => triggerIconSizes[resolvedSize.value])\n\nconst tooltipId = `gr-tooltip-${useId()}`\n\nconst triggerStyle = computed(() => ({ color: props.iconColor }))\n\n// Телепорт включается только ПОСЛЕ монтирования: иначе первый клиентский\n// рендер не совпадает с серверным и ломается гидрация (см. композабл).\nconst { target: portalTarget, enabled: teleportEnabled } = usePortalTarget()\n\n// Тема поддерева на телепортированную панель: в DOM она уезжает в `body`, то\n// есть вне обёртки провайдера, и `data-theme` с неё не наследуется. В дереве\n// компонентов панель остаётся внутри — `inject` доходит, и тему она ставит себе\n// сама.\nconst themeAttrs = useGrThemeAttrs()\n\nconst uncontrolledOpen = ref(false)\nconst triggerEl = ref<HTMLElement | null>(null)\nconst panelEl = ref<HTMLElement | null>(null)\n\n/** Показывать нечего — пустая панель хуже отсутствующей. */\nconst hasContent = computed(() => Boolean(slots.content) || Boolean(props.text))\n\nconst open = computed<boolean>({\n get: () => {\n if (props.disabled || !hasContent.value) return false\n return props.open ?? uncontrolledOpen.value\n },\n set: (next) => {\n uncontrolledOpen.value = next\n emit('update:open', next)\n },\n})\n\nconst { floatingStyle } = useFloating(triggerEl, panelEl, open, {\n placement: () => props.placement,\n // Геттер, а не значение: `useFloating` читает опцию на каждом пересчёте, и\n // так проп остаётся живым, а не замирает на моменте setup.\n get offsetPx() { return props.offsetPx },\n zIndexVar: '--gr-z-tooltip',\n})\n\nlet delayTimer: ReturnType<typeof setTimeout> | undefined\n\nfunction schedule(next: boolean, delayMs: number): void {\n clearTimeout(delayTimer)\n\n if (delayMs <= 0) {\n open.value = next\n return\n }\n\n delayTimer = setTimeout(() => { open.value = next }, delayMs)\n}\n\nfunction show(): void {\n if (props.disabled) return\n schedule(true, props.openDelay)\n}\n\nfunction hide(): void {\n schedule(false, props.closeDelay)\n}\n\n/** Escape и клик вне закрывают мгновенно: задержка тут читалась бы как залипание. */\nfunction dismiss(): void {\n clearTimeout(delayTimer)\n if (open.value) open.value = false\n}\n\n// Нужен только тач-режиму: курсор и фокус закрывают подсказку сами.\nconst dismissOnOutside = computed(() => ({ handler: dismiss, enabled: open.value }))\n\n/**\n * На тач-устройствах hover не существует, а фокус по тапу не гарантирован —\n * без этого подсказка там недоступна вовсе. Тап по триггеру переключает\n * панель, тап вне — закрывает (`vClickOutside`).\n */\nfunction onTouch(): void {\n if (open.value) dismiss()\n else show()\n}\n\nonUnmounted(() => clearTimeout(delayTimer))\n\nuseDismissible(open, dismiss)\n\nconst FOCUSABLE_SELECTOR = 'a[href],button,input,select,textarea,[tabindex]:not([tabindex=\"-1\"])'\n\n/**\n * Типовое употребление — подсказка у кнопки. Фокусируемая обёртка вокруг\n * фокусируемого контрола дала бы два таб-стопа на один визуальный элемент,\n * поэтому обёртка забирает `tabindex` только когда в слоте фокусироваться\n * нечему (текст, иконка). Разметку слота пишет потребитель, привязать к ней\n * `aria-describedby` из шаблона нельзя — отсюда единственная точка, где атрибут\n * ставится вручную.\n */\nconst slotFocusableEl = ref<HTMLElement | null>(null)\n\nfunction syncSlotFocusable(): void {\n slotFocusableEl.value = slots.default\n ? triggerEl.value?.querySelector<HTMLElement>(FOCUSABLE_SELECTOR) ?? null\n : null\n}\n\nonMounted(syncSlotFocusable)\nonUpdated(syncSlotFocusable)\n\nwatchEffect((onCleanup) => {\n const el = slotFocusableEl.value\n if (!el) return\n\n el.setAttribute('aria-describedby', tooltipId)\n onCleanup(() => el.removeAttribute('aria-describedby'))\n})\n\nconst wrapperTabindex = computed(() => (slotFocusableEl.value ? undefined : 0))\nconst wrapperDescribedBy = computed(() => (slotFocusableEl.value ? undefined : tooltipId))\n</script>\n\n<template>\n <span\n v-click-outside=\"dismissOnOutside\"\n data-gr-tooltip\n class=\"relative inline-flex\"\n >\n <span\n ref=\"triggerEl\"\n data-gr-tooltip-trigger\n data-testid=\"gr-tooltip-trigger\"\n :tabindex=\"wrapperTabindex\"\n :aria-describedby=\"wrapperDescribedBy\"\n class=\"inline-flex items-center justify-center focus:outline-none\"\n :style=\"triggerStyle\"\n @mouseenter=\"show\"\n @mouseleave=\"hide\"\n @focusin=\"show\"\n @focusout=\"hide\"\n @touchstart.passive=\"onTouch\"\n >\n <slot>\n <GrIcon :size=\"triggerIconSize\">\n <IconInfo />\n </GrIcon>\n </slot>\n </span>\n\n <teleport :to=\"portalTarget\" :disabled=\"!teleportEnabled\">\n <transition\n enter-active-class=\"transition-opacity duration-[var(--gr-duration-fast)]\"\n enter-from-class=\"opacity-0\"\n enter-to-class=\"opacity-100\"\n leave-active-class=\"transition-opacity duration-[var(--gr-duration-fast)]\"\n leave-from-class=\"opacity-100\"\n leave-to-class=\"opacity-0\"\n >\n <span\n v-show=\"open\"\n :id=\"tooltipId\"\n ref=\"panelEl\"\n role=\"tooltip\"\n v-bind=\"themeAttrs\"\n data-gr-tooltip-panel\n data-gr-overlay-root\n class=\"pointer-events-none rounded-[var(--gr-radius-control)] border border-[var(--gr-brd)] bg-[var(--gr-popover)] text-[var(--gr-popover-fg)] shadow-[var(--gr-shadow-1)]\"\n :class=\"panelClass\"\n :style=\"floatingStyle\"\n >\n <slot name=\"content\">{{ text }}</slot>\n </span>\n </transition>\n </teleport>\n </span>\n</template>\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
1
  import { n as grTooltipSafelist, t as grTooltipConfig } from "../../chunks/config-CN2ULSQL.js";
2
- import { t as GrTooltip_default } from "./chunks/GrTooltip-xAcqs0bP.js";
2
+ import { t as GrTooltip_default } from "./chunks/GrTooltip-Cn08heWz.js";
3
3
  export { GrTooltip_default as GrTooltip, GrTooltip_default as default, grTooltipConfig, grTooltipSafelist };