@feugene/granularity 0.16.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 (425) 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-Zc_bUvZY2.js → config-BDfTXHo12.js} +1 -1
  9. package/dist/chunks/{config-Zc_bUvZY2.js.map → config-BDfTXHo12.js.map} +1 -1
  10. package/dist/chunks/config-BE6n20np2.js.map +1 -0
  11. package/dist/chunks/config-BJ7Bv_G2.js +148 -0
  12. package/dist/chunks/config-BJ7Bv_G2.js.map +1 -0
  13. package/dist/chunks/config-BKvX0q102.js.map +1 -0
  14. package/dist/chunks/{config-CpwwkFBO.js → config-Baj7Wvx-.js} +1 -1
  15. package/dist/chunks/{config-CpwwkFBO.js.map → config-Baj7Wvx-.js.map} +1 -1
  16. package/dist/chunks/{config-D8tSR2uS.js → config-Be9k9bOf.js} +2 -8
  17. package/dist/chunks/config-Be9k9bOf.js.map +1 -0
  18. package/dist/chunks/config-BoNR7_PD2.js.map +1 -0
  19. package/dist/chunks/{config-DxCTVBfx.js → config-BsPDQ0dS.js} +2 -2
  20. package/dist/chunks/config-BsPDQ0dS.js.map +1 -0
  21. package/dist/chunks/{config-DpqOnJiz2.js → config-CDat1wXf2.js} +1 -1
  22. package/dist/chunks/config-CDat1wXf2.js.map +1 -0
  23. package/dist/chunks/config-CN2ULSQL.js.map +1 -0
  24. package/dist/chunks/{config-Cw6TDzWv2.js → config-COpuWGr_.js} +1 -1
  25. package/dist/chunks/config-COpuWGr_.js.map +1 -0
  26. package/dist/chunks/config-CY0_6cMi2.js.map +1 -0
  27. package/dist/chunks/{config-B-g6lT19.js → config-DA6SnEoy.js} +5 -2
  28. package/dist/chunks/config-DA6SnEoy.js.map +1 -0
  29. package/dist/chunks/config-DAX8Deh1.js +68 -0
  30. package/dist/chunks/config-DAX8Deh1.js.map +1 -0
  31. package/dist/chunks/config-DKdlqXiY.js.map +1 -1
  32. package/dist/chunks/{config-CvC0sX8p.js → config-MRvhUl-O.js} +1 -1
  33. package/dist/chunks/{config-CvC0sX8p.js.map → config-MRvhUl-O.js.map} +1 -1
  34. package/dist/chunks/config-Rw9OyW702.js.map +1 -0
  35. package/dist/chunks/config-ScuvHw6k2.js +42 -0
  36. package/dist/chunks/config-ScuvHw6k2.js.map +1 -0
  37. package/dist/chunks/config-T3qdQBgU2.js.map +1 -0
  38. package/dist/chunks/config-eXB87DOi.js.map +1 -0
  39. package/dist/chunks/context-BSYR7RZs.js.map +1 -1
  40. package/dist/chunks/{en-B3xMeswz.js → en-COdzxgAX.js} +30 -5
  41. package/dist/chunks/en-COdzxgAX.js.map +1 -0
  42. package/dist/chunks/{es-CA-mkU--.js → es-ClqJ-4hr.js} +30 -5
  43. package/dist/chunks/es-ClqJ-4hr.js.map +1 -0
  44. package/dist/chunks/{es-Bv8vvqdT.js → es-DGMHwknJ.js} +3 -3
  45. package/dist/chunks/{es-Bv8vvqdT.js.map → es-DGMHwknJ.js.map} +1 -1
  46. package/dist/chunks/generated-BRkbhyxk.js +3270 -0
  47. package/dist/chunks/generated-BRkbhyxk.js.map +1 -0
  48. package/dist/chunks/grKbdStyles-CeZhGQHr.js +43 -0
  49. package/dist/chunks/grKbdStyles-CeZhGQHr.js.map +1 -0
  50. package/dist/chunks/{grTextareaStyles-CWI2SqC8.js → grTextareaStyles-iOcHuVMM.js} +2 -2
  51. package/dist/chunks/{grTextareaStyles-CWI2SqC8.js.map → grTextareaStyles-iOcHuVMM.js.map} +1 -1
  52. package/dist/chunks/{granular-provider-CtrXGa7c.js → granular-provider-C6uAujdJ.js} +23 -21
  53. package/dist/chunks/granular-provider-C6uAujdJ.js.map +1 -0
  54. package/dist/chunks/grip-vertical-lRha2S7q.js +16 -0
  55. package/dist/chunks/grip-vertical-lRha2S7q.js.map +1 -0
  56. package/dist/chunks/icon-Dag2tIMs.js +21 -0
  57. package/dist/chunks/icon-Dag2tIMs.js.map +1 -0
  58. package/dist/chunks/{keyboard-zYvvyHd4.js → keyboard-DE2yhz_b.js} +10 -1
  59. package/dist/chunks/keyboard-DE2yhz_b.js.map +1 -0
  60. package/dist/chunks/{optionFilter-BVY988pC.js → optionFilter-DYw9XPPx.js} +1 -71
  61. package/dist/chunks/optionFilter-DYw9XPPx.js.map +1 -0
  62. package/dist/chunks/{useOverlayLayer-BupuP_hs.js → overlayStack-C8cfsqXa.js} +11 -61
  63. package/dist/chunks/overlayStack-C8cfsqXa.js.map +1 -0
  64. package/dist/chunks/{portalRoot-CWD4zHBm.js → portalRoot-CVhCgdG6.js} +10 -1
  65. package/dist/chunks/{portalRoot-CWD4zHBm.js.map → portalRoot-CVhCgdG6.js.map} +1 -1
  66. package/dist/chunks/{ru-COWxgh2A.js → ru-9z8ix45c.js} +30 -5
  67. package/dist/chunks/ru-9z8ix45c.js.map +1 -0
  68. package/dist/chunks/useDragSort-0l47h4yb.js +265 -0
  69. package/dist/chunks/useDragSort-0l47h4yb.js.map +1 -0
  70. package/dist/chunks/{useModalOverlay-DCM4d3yA.js → useModalOverlay-DrQVYwwA.js} +2 -2
  71. package/dist/chunks/{useModalOverlay-DCM4d3yA.js.map → useModalOverlay-DrQVYwwA.js.map} +1 -1
  72. package/dist/chunks/{usePortalTarget-RyzEgNTb.js → usePortalTarget-Cn-Wiz5d.js} +1 -1
  73. package/dist/chunks/{usePortalTarget-RyzEgNTb.js.map → usePortalTarget-Cn-Wiz5d.js.map} +1 -1
  74. package/dist/components/GrAutocomplete/chunks/{GrAutocomplete-CQ9T7Lly.js → GrAutocomplete-gY4Xopza.js} +35 -26
  75. package/dist/components/GrAutocomplete/chunks/GrAutocomplete-gY4Xopza.js.map +1 -0
  76. package/dist/components/GrAutocomplete/index.js +1 -1
  77. package/dist/components/GrAutocomplete/styles.css +1 -1
  78. package/dist/components/GrBreadcrumbs/chunks/{GrBreadcrumbs-BCNGDS6k.js → GrBreadcrumbs-DS_-PBVt.js} +8 -7
  79. package/dist/components/GrBreadcrumbs/chunks/GrBreadcrumbs-DS_-PBVt.js.map +1 -0
  80. package/dist/components/GrBreadcrumbs/index.js +1 -1
  81. package/dist/components/GrCollapse/chunks/{GrCollapse-B2Fc6A9r.js.map → GrCollapse-Kk0DE8tb.js.map} +1 -1
  82. package/dist/components/GrCollapse/index.js +1 -1
  83. package/dist/components/GrColorPicker/chunks/{GrColorPicker-bnYBRGk8.js → GrColorPicker-CkBTzkqn.js} +4 -4
  84. package/dist/components/GrColorPicker/chunks/{GrColorPicker-bnYBRGk8.js.map → GrColorPicker-CkBTzkqn.js.map} +1 -1
  85. package/dist/components/GrColorPicker/index.js +2 -2
  86. package/dist/components/GrCommandPalette/chunks/{GrCommandPalette-MWDYDPNC.js → GrCommandPalette-BugTlGdj.js} +41 -16
  87. package/dist/components/GrCommandPalette/chunks/GrCommandPalette-BugTlGdj.js.map +1 -0
  88. package/dist/components/GrCommandPalette/index.js +2 -2
  89. package/dist/components/GrCommandPalette/styles.css +1 -1
  90. package/dist/components/GrConfigProvider/index.js +2 -2
  91. package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-DaQmlp1_.js → GrConfirmDialog-Ds5T0GiR.js} +1 -1
  92. package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-DaQmlp1_.js.map → GrConfirmDialog-Ds5T0GiR.js.map} +1 -1
  93. package/dist/components/GrConfirmDialog/index.js +1 -1
  94. package/dist/components/GrDataTable/chunks/{GrDataTable-DVFho9Cj.js → GrDataTable-CYe2A2WA.js} +498 -48
  95. package/dist/components/GrDataTable/chunks/GrDataTable-CYe2A2WA.js.map +1 -0
  96. package/dist/components/GrDataTable/index.js +2 -2
  97. package/dist/components/GrDialog/chunks/{GrDialog-AE1gIZuz.js → GrDialog-C3tYaA8v.js} +1 -1
  98. package/dist/components/GrDialog/chunks/{GrDialog-AE1gIZuz.js.map → GrDialog-C3tYaA8v.js.map} +1 -1
  99. package/dist/components/GrDialog/index.js +1 -1
  100. package/dist/components/GrDialogService/chunks/{GrDialogService-B4DYWDkA.js → GrDialogService-CTBiiJDI.js} +3 -3
  101. package/dist/components/GrDialogService/chunks/{GrDialogService-B4DYWDkA.js.map → GrDialogService-CTBiiJDI.js.map} +1 -1
  102. package/dist/components/GrDialogService/index.js +1 -1
  103. package/dist/components/GrDivider/chunks/{GrDivider-B2Xwc_QO.js → GrDivider-xxq1_fIl.js} +1 -1
  104. package/dist/components/GrDivider/chunks/{GrDivider-B2Xwc_QO.js.map → GrDivider-xxq1_fIl.js.map} +1 -1
  105. package/dist/components/GrDivider/index.js +2 -2
  106. package/dist/components/GrDrawer/chunks/{GrDrawer-DQwi6aRw.js → GrDrawer-BSSuiNHU.js} +1 -1
  107. package/dist/components/GrDrawer/chunks/{GrDrawer-DQwi6aRw.js.map → GrDrawer-BSSuiNHU.js.map} +1 -1
  108. package/dist/components/GrDrawer/index.js +1 -1
  109. package/dist/components/GrDropdown/chunks/{GrDropdown-BLng6V1m.js → GrDropdown-Bh6UK2M5.js} +3 -3
  110. package/dist/components/GrDropdown/chunks/{GrDropdown-BLng6V1m.js.map → GrDropdown-Bh6UK2M5.js.map} +1 -1
  111. package/dist/components/GrDropdown/index.js +1 -1
  112. package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenu-BImk2Vs7.js → GrDropdownMenu-LyfEFUbg.js} +2 -2
  113. package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenu-BImk2Vs7.js.map → GrDropdownMenu-LyfEFUbg.js.map} +1 -1
  114. package/dist/components/GrDropdownMenu/index.js +2 -2
  115. package/dist/components/GrFileUpload/chunks/{GrFileUpload-D0B-BEsd.js → GrFileUpload-DuDzC43S.js} +2 -2
  116. package/dist/components/GrFileUpload/chunks/{GrFileUpload-D0B-BEsd.js.map → GrFileUpload-DuDzC43S.js.map} +1 -1
  117. package/dist/components/GrFileUpload/index.js +1 -1
  118. package/dist/components/GrImageViewer/chunks/{GrImageViewer-BeCnE8XQ.js → GrImageViewer-DfD4euH5.js} +48 -23
  119. package/dist/components/GrImageViewer/chunks/GrImageViewer-DfD4euH5.js.map +1 -0
  120. package/dist/components/GrImageViewer/index.js +1 -1
  121. package/dist/components/GrInput/chunks/{GrInput-DD2gYdqS.js → GrInput-2wd7Crs0.js} +1 -1
  122. package/dist/components/GrInput/chunks/GrInput-2wd7Crs0.js.map +1 -0
  123. package/dist/components/GrInput/index.js +1 -1
  124. package/dist/components/GrInputTag/chunks/{GrInputTag-DmNunixz.js → GrInputTag-CyV2eY7Z.js} +20 -33
  125. package/dist/components/GrInputTag/chunks/GrInputTag-CyV2eY7Z.js.map +1 -0
  126. package/dist/components/GrInputTag/index.js +1 -1
  127. package/dist/components/GrKbd/chunks/{GrKbd-DRFPvW3G.js → GrKbd-DU2lBkRV.js} +218 -52
  128. package/dist/components/GrKbd/chunks/GrKbd-DU2lBkRV.js.map +1 -0
  129. package/dist/components/GrKbd/index.js +3 -3
  130. package/dist/components/GrModal/chunks/{GrModal-B8I78T0N.js → GrModal-CbMMr6W1.js} +1 -1
  131. package/dist/components/GrModal/chunks/{GrModal-B8I78T0N.js.map → GrModal-CbMMr6W1.js.map} +1 -1
  132. package/dist/components/GrModal/index.js +1 -1
  133. package/dist/components/GrNumberInput/chunks/{GrNumberInput-CZOyPz4J.js → GrNumberInput-B5CHZDBj.js} +2 -1
  134. package/dist/components/GrNumberInput/chunks/{GrNumberInput-CZOyPz4J.js.map → GrNumberInput-B5CHZDBj.js.map} +1 -1
  135. package/dist/components/GrNumberInput/index.js +1 -1
  136. package/dist/components/GrPagination/chunks/{GrPagination-B6N0QZBf.js → GrPagination-BM4FrD6Q.js} +4 -4
  137. package/dist/components/GrPagination/chunks/GrPagination-BM4FrD6Q.js.map +1 -0
  138. package/dist/components/GrPagination/index.js +1 -1
  139. package/dist/components/GrPopover/chunks/{GrPopover-B1eRCOoO.js → GrPopover-DMH4Jjst.js} +1 -1
  140. package/dist/components/GrPopover/chunks/{GrPopover-B1eRCOoO.js.map → GrPopover-DMH4Jjst.js.map} +1 -1
  141. package/dist/components/GrPopover/index.js +1 -1
  142. package/dist/components/GrProgressBar/chunks/{GrProgressBar-BiHnaitu.js → GrProgressBar-xwbNM9QB.js} +1 -1
  143. package/dist/components/GrProgressBar/chunks/GrProgressBar-xwbNM9QB.js.map +1 -0
  144. package/dist/components/GrProgressBar/index.js +1 -1
  145. package/dist/components/GrProgressCircle/chunks/{GrProgressCircle-CTogjcHz.js → GrProgressCircle-B8olXRo5.js} +1 -1
  146. package/dist/components/GrProgressCircle/chunks/{GrProgressCircle-CTogjcHz.js.map → GrProgressCircle-B8olXRo5.js.map} +1 -1
  147. package/dist/components/GrProgressCircle/index.js +2 -2
  148. package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-Ct174NVD.js → GrPromptDialog-Pwr9NPxK.js} +3 -3
  149. package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-Ct174NVD.js.map → GrPromptDialog-Pwr9NPxK.js.map} +1 -1
  150. package/dist/components/GrPromptDialog/index.js +1 -1
  151. package/dist/components/GrRadio/chunks/{GrRadio-DksDro_U.js → GrRadio-DWiRaMzt.js} +8 -32
  152. package/dist/components/GrRadio/chunks/GrRadio-DWiRaMzt.js.map +1 -0
  153. package/dist/components/GrRadio/index.js +1 -1
  154. package/dist/components/GrRadioGroup/chunks/{GrRadioGroup-BOIhejE6.js → GrRadioGroup-DIuBw0wk.js} +34 -27
  155. package/dist/components/GrRadioGroup/chunks/GrRadioGroup-DIuBw0wk.js.map +1 -0
  156. package/dist/components/GrRadioGroup/index.js +1 -1
  157. package/dist/components/GrRating/chunks/{GrRating-JrHXz0Rv.js → GrRating-DVFH3lYu.js} +10 -9
  158. package/dist/components/GrRating/chunks/GrRating-DVFH3lYu.js.map +1 -0
  159. package/dist/components/GrRating/index.js +2 -2
  160. package/dist/components/GrSegmented/chunks/{GrSegmented-BQIlH5KI.js → GrSegmented-CQw3ijqa.js} +47 -65
  161. package/dist/components/GrSegmented/chunks/GrSegmented-CQw3ijqa.js.map +1 -0
  162. package/dist/components/GrSegmented/index.js +1 -1
  163. package/dist/components/GrSelect/chunks/{GrSelect-aqkPCqjd.js → GrSelect-B-Voni7D.js} +69 -55
  164. package/dist/components/GrSelect/chunks/GrSelect-B-Voni7D.js.map +1 -0
  165. package/dist/components/GrSelect/index.js +1 -1
  166. package/dist/components/GrSelect/styles.css +1 -1
  167. package/dist/components/GrSidebar/chunks/{GrSidebar-DRGnnViw.js → GrSidebar-i7AxjhY_.js} +3 -2
  168. package/dist/components/GrSidebar/chunks/{GrSidebar-DRGnnViw.js.map → GrSidebar-i7AxjhY_.js.map} +1 -1
  169. package/dist/components/GrSidebar/index.js +2 -2
  170. package/dist/components/GrSkeleton/index.js +1 -1
  171. package/dist/components/GrSlider/chunks/{GrSlider-DkU9IQSX.js → GrSlider-CyhVsNKn.js} +21 -34
  172. package/dist/components/GrSlider/chunks/GrSlider-CyhVsNKn.js.map +1 -0
  173. package/dist/components/GrSlider/index.js +2 -2
  174. package/dist/components/GrSortableList/chunks/GrSortableList-XB7mXM1W.js +277 -0
  175. package/dist/components/GrSortableList/chunks/GrSortableList-XB7mXM1W.js.map +1 -0
  176. package/dist/components/GrSortableList/index.js +3 -0
  177. package/dist/components/GrSplitter/chunks/{GrSplitter-7To8jbJ8.js → GrSplitter-BzxVQwbY.js} +17 -37
  178. package/dist/components/GrSplitter/chunks/GrSplitter-BzxVQwbY.js.map +1 -0
  179. package/dist/components/GrSplitter/index.js +2 -2
  180. package/dist/components/GrStatistic/chunks/{GrStatistic-CttyoYPT.js → GrStatistic-DOsM6s5V.js} +54 -7
  181. package/dist/components/GrStatistic/chunks/GrStatistic-DOsM6s5V.js.map +1 -0
  182. package/dist/components/GrStatistic/index.js +2 -2
  183. package/dist/components/GrSwitch/chunks/{GrSwitch-B9TYbiih.js → GrSwitch-DpyvVrZG.js} +1 -1
  184. package/dist/components/GrSwitch/chunks/{GrSwitch-B9TYbiih.js.map → GrSwitch-DpyvVrZG.js.map} +1 -1
  185. package/dist/components/GrSwitch/index.js +2 -2
  186. package/dist/components/GrTabPanels/index.js +1 -1
  187. package/dist/components/GrTable/chunks/{GrTable-BB-mI_NM.js → GrTable-BfqgwETQ.js} +7 -1
  188. package/dist/components/GrTable/chunks/GrTable-BfqgwETQ.js.map +1 -0
  189. package/dist/components/GrTable/index.js +1 -1
  190. package/dist/components/GrTabs/chunks/{GrTabs-B8CDq2D9.js → GrTabs-f3-oQenu.js} +55 -65
  191. package/dist/components/GrTabs/chunks/GrTabs-f3-oQenu.js.map +1 -0
  192. package/dist/components/GrTabs/index.js +1 -1
  193. package/dist/components/GrTextarea/chunks/{GrTextarea-DUh5Tlo4.js → GrTextarea-DSvd1nSy.js} +1 -1
  194. package/dist/components/GrTextarea/chunks/{GrTextarea-DUh5Tlo4.js.map → GrTextarea-DSvd1nSy.js.map} +1 -1
  195. package/dist/components/GrTextarea/index.js +2 -2
  196. package/dist/components/GrTimeline/chunks/{GrTimeline-CE9b3s18.js → GrTimeline-B-h2Yanw.js} +1 -1
  197. package/dist/components/GrTimeline/chunks/{GrTimeline-CE9b3s18.js.map → GrTimeline-B-h2Yanw.js.map} +1 -1
  198. package/dist/components/GrTimeline/index.js +2 -2
  199. package/dist/components/GrToaster/chunks/{GrToaster-BKQy3Srm.js → GrToaster-Dqja0Lf9.js} +114 -13
  200. package/dist/components/GrToaster/chunks/GrToaster-Dqja0Lf9.js.map +1 -0
  201. package/dist/components/GrToaster/index.js +2 -2
  202. package/dist/components/GrToaster/styles.css +1 -1
  203. package/dist/components/GrTooltip/chunks/{GrTooltip-DEbejfhA.js → GrTooltip-Cn08heWz.js} +2 -2
  204. package/dist/components/GrTooltip/chunks/{GrTooltip-DEbejfhA.js.map → GrTooltip-Cn08heWz.js.map} +1 -1
  205. package/dist/components/GrTooltip/index.js +2 -2
  206. package/dist/components/GrTree/chunks/{GrTree-wfdbvygw.js → GrTree-BR9a2pay.js} +148 -111
  207. package/dist/components/GrTree/chunks/GrTree-BR9a2pay.js.map +1 -0
  208. package/dist/components/GrTree/index.js +2 -2
  209. package/dist/components/GrTree/styles.css +1 -1
  210. package/dist/components/GrTreeSelect/chunks/{GrTreeSelect-lLWmAMtI.js → GrTreeSelect--muUeB0v.js} +28 -14
  211. package/dist/components/GrTreeSelect/chunks/GrTreeSelect--muUeB0v.js.map +1 -0
  212. package/dist/components/GrTreeSelect/index.js +1 -1
  213. package/dist/composables/useAnnouncer.js +1 -1
  214. package/dist/composables/useComboboxNavigation.js +71 -0
  215. package/dist/composables/useComboboxNavigation.js.map +1 -0
  216. package/dist/composables/useDismissible.js +1 -1
  217. package/dist/composables/useDragGesture.js +55 -0
  218. package/dist/composables/useDragGesture.js.map +1 -0
  219. package/dist/composables/useDragSort.js +2 -0
  220. package/dist/composables/useFloating.js +22 -1
  221. package/dist/composables/useFloating.js.map +1 -1
  222. package/dist/composables/useGrComponentConfig.js +2 -0
  223. package/dist/composables/useGranularityTranslations.js +2 -0
  224. package/dist/composables/useOverlayLayer.js +64 -1
  225. package/dist/composables/useOverlayLayer.js.map +1 -0
  226. package/dist/composables/usePortalTarget.js +1 -1
  227. package/dist/composables/useRovingFocus.js +150 -0
  228. package/dist/composables/useRovingFocus.js.map +1 -0
  229. package/dist/directives/hotkey.js +13 -7
  230. package/dist/directives/hotkey.js.map +1 -1
  231. package/dist/directives/loading.js +1 -1
  232. package/dist/granular-provider-node.js +1 -1
  233. package/dist/granular-provider.js +1 -1
  234. package/dist/i18n/all.js +1 -1
  235. package/dist/i18n/index.js +1 -1
  236. package/dist/index.js +77 -68
  237. package/dist/styles/themes/dark.css +6 -1
  238. package/dist/testing.js +199 -0
  239. package/dist/testing.js.map +1 -0
  240. package/dist/theme-apply.js +33 -0
  241. package/dist/theme-apply.js.map +1 -0
  242. package/dist/theme.js +501 -0
  243. package/dist/theme.js.map +1 -0
  244. package/dist/tokens.js +1 -3131
  245. package/dist/types/src/components/GrAlert/defaults.d.ts +1 -1
  246. package/dist/types/src/components/GrAutocomplete/defaults.d.ts +1 -1
  247. package/dist/types/src/components/GrAvatar/defaults.d.ts +1 -1
  248. package/dist/types/src/components/GrBadge/defaults.d.ts +1 -1
  249. package/dist/types/src/components/GrBottomNav/defaults.d.ts +1 -1
  250. package/dist/types/src/components/GrBreadcrumbs/defaults.d.ts +1 -1
  251. package/dist/types/src/components/GrBreadcrumbs/grBreadcrumbsStyles.d.ts +5 -2
  252. package/dist/types/src/components/GrButton/defaults.d.ts +1 -1
  253. package/dist/types/src/components/GrCard/defaults.d.ts +1 -1
  254. package/dist/types/src/components/GrCheckbox/defaults.d.ts +1 -1
  255. package/dist/types/src/components/GrCheckboxGroup/defaults.d.ts +1 -1
  256. package/dist/types/src/components/GrCollapse/defaults.d.ts +1 -1
  257. package/dist/types/src/components/GrColorPicker/defaults.d.ts +1 -1
  258. package/dist/types/src/components/GrColorPicker/grColorPickerStyles.d.ts +2 -2
  259. package/dist/types/src/components/GrCommandPalette/GrCommandPalette.vue.d.ts +6 -6
  260. package/dist/types/src/components/GrCommandPalette/defaults.d.ts +1 -1
  261. package/dist/types/src/components/GrCommandPalette/filtering.d.ts +6 -2
  262. package/dist/types/src/components/GrConfigProvider/context.d.ts +6 -22
  263. package/dist/types/src/components/GrConfigProvider/index.d.ts +1 -1
  264. package/dist/types/src/components/GrConfirmDialog/defaults.d.ts +1 -1
  265. package/dist/types/src/components/GrDataTable/GrDataTable.vue.d.ts +53 -0
  266. package/dist/types/src/components/GrDataTable/defaults.d.ts +1 -1
  267. package/dist/types/src/components/GrDataTable/grDataTableStyles.d.ts +38 -0
  268. package/dist/types/src/components/GrDialog/defaults.d.ts +1 -1
  269. package/dist/types/src/components/GrDivider/defaults.d.ts +1 -1
  270. package/dist/types/src/components/GrDrawer/defaults.d.ts +1 -1
  271. package/dist/types/src/components/GrEmptyState/defaults.d.ts +1 -1
  272. package/dist/types/src/components/GrFileUpload/defaults.d.ts +1 -1
  273. package/dist/types/src/components/GrFormField/defaults.d.ts +1 -1
  274. package/dist/types/src/components/GrFormFile/defaults.d.ts +1 -1
  275. package/dist/types/src/components/GrFormSection/defaults.d.ts +1 -1
  276. package/dist/types/src/components/GrIcon/defaults.d.ts +1 -1
  277. package/dist/types/src/components/GrImageViewer/composables/usePointerGestures.d.ts +1 -0
  278. package/dist/types/src/components/GrImageViewer/composables/useZoomPan.d.ts +1 -2
  279. package/dist/types/src/components/GrInput/defaults.d.ts +1 -1
  280. package/dist/types/src/components/GrInputTag/defaults.d.ts +1 -1
  281. package/dist/types/src/components/GrKbd/GrKbd.vue.d.ts +18 -3
  282. package/dist/types/src/components/GrKbd/defaults.d.ts +1 -1
  283. package/dist/types/src/components/GrKbd/grKbdStyles.d.ts +20 -3
  284. package/dist/types/src/components/GrKbd/index.d.ts +6 -0
  285. package/dist/types/src/components/GrLink/defaults.d.ts +1 -1
  286. package/dist/types/src/components/GrModal/defaults.d.ts +1 -1
  287. package/dist/types/src/components/GrNumberInput/defaults.d.ts +1 -1
  288. package/dist/types/src/components/GrPagination/defaults.d.ts +1 -1
  289. package/dist/types/src/components/GrPopover/GrPopover.vue.d.ts +1 -1
  290. package/dist/types/src/components/GrPopover/defaults.d.ts +1 -1
  291. package/dist/types/src/components/GrProgressBar/defaults.d.ts +1 -1
  292. package/dist/types/src/components/GrProgressCircle/defaults.d.ts +1 -1
  293. package/dist/types/src/components/GrProgressCircle/grProgressCircleStyles.d.ts +1 -1
  294. package/dist/types/src/components/GrPromptDialog/defaults.d.ts +1 -1
  295. package/dist/types/src/components/GrRadio/defaults.d.ts +1 -1
  296. package/dist/types/src/components/GrRadio/grRadioGroupContext.d.ts +12 -4
  297. package/dist/types/src/components/GrRadioGroup/defaults.d.ts +1 -1
  298. package/dist/types/src/components/GrRating/GrRating.vue.d.ts +9 -4
  299. package/dist/types/src/components/GrRating/defaults.d.ts +1 -1
  300. package/dist/types/src/components/GrSegmented/defaults.d.ts +1 -1
  301. package/dist/types/src/components/GrSelect/defaults.d.ts +1 -1
  302. package/dist/types/src/components/GrSlider/defaults.d.ts +1 -1
  303. package/dist/types/src/components/GrSortableList/GrSortableList.vue.d.ts +68 -0
  304. package/dist/types/src/components/GrSortableList/config.d.ts +1 -0
  305. package/dist/types/src/components/GrSortableList/grSortableListStyles.d.ts +37 -0
  306. package/dist/types/src/components/GrSortableList/index.d.ts +9 -0
  307. package/dist/types/src/components/GrSortableList/safelist.d.ts +1 -0
  308. package/dist/types/src/components/GrSplitter/defaults.d.ts +1 -1
  309. package/dist/types/src/components/GrStatistic/GrStatistic.vue.d.ts +12 -9
  310. package/dist/types/src/components/GrStatistic/defaults.d.ts +1 -1
  311. package/dist/types/src/components/GrStatistic/grStatisticStyles.d.ts +1 -2
  312. package/dist/types/src/components/GrSwitch/defaults.d.ts +1 -1
  313. package/dist/types/src/components/GrTable/GrTable.vue.d.ts +13 -0
  314. package/dist/types/src/components/GrTable/defaults.d.ts +1 -1
  315. package/dist/types/src/components/GrTabs/GrTabs.vue.d.ts +6 -2
  316. package/dist/types/src/components/GrTabs/defaults.d.ts +1 -1
  317. package/dist/types/src/components/GrTextarea/defaults.d.ts +1 -1
  318. package/dist/types/src/components/GrTextarea/grTextareaStyles.d.ts +2 -2
  319. package/dist/types/src/components/GrTimeline/defaults.d.ts +1 -1
  320. package/dist/types/src/components/GrTimeline/grTimelineStyles.d.ts +1 -1
  321. package/dist/types/src/components/GrToaster/GrToaster.vue.d.ts +8 -0
  322. package/dist/types/src/components/GrToaster/grToasterStyles.d.ts +27 -0
  323. package/dist/types/src/components/GrTooltip/defaults.d.ts +1 -1
  324. package/dist/types/src/components/GrTree/defaults.d.ts +1 -1
  325. package/dist/types/src/components/GrTree/grTreeInteractionContext.d.ts +0 -1
  326. package/dist/types/src/components/GrTree/grTreeProps.d.ts +11 -4
  327. package/dist/types/src/components/GrTree/safelist.d.ts +1 -1
  328. package/dist/types/src/components/GrTreeSelect/defaults.d.ts +1 -1
  329. package/dist/types/src/components/shared/hotkey.d.ts +7 -15
  330. package/dist/types/src/components/shared/hotkeyTokens.d.ts +37 -0
  331. package/dist/types/src/components/shared/icon.d.ts +13 -0
  332. package/dist/types/src/composables/internal/dragSortGeometry.d.ts +59 -0
  333. package/dist/types/src/composables/internal/overlayStack.d.ts +11 -0
  334. package/dist/types/src/composables/useDragGesture.d.ts +63 -0
  335. package/dist/types/src/composables/useDragSort.d.ts +80 -0
  336. package/dist/types/src/composables/useFloating.d.ts +5 -1
  337. package/dist/types/src/composables/useGrComponentConfig.d.ts +41 -0
  338. package/dist/types/src/composables/useGranularityTranslations.d.ts +13 -0
  339. package/dist/types/src/composables/useRovingFocus.d.ts +108 -0
  340. package/dist/types/src/granular-provider/shared.d.ts +1 -0
  341. package/dist/types/src/i18n/messages/en.d.ts +48 -0
  342. package/dist/types/src/i18n/messages/es.d.ts +48 -0
  343. package/dist/types/src/i18n/messages/ru.d.ts +48 -0
  344. package/dist/types/src/index.d.ts +12 -0
  345. package/dist/types/src/internal/keyboard.d.ts +6 -0
  346. package/dist/types/src/testing/announcer.d.ts +10 -0
  347. package/dist/types/src/testing/context.d.ts +36 -0
  348. package/dist/types/src/testing/env.d.ts +25 -0
  349. package/dist/types/src/testing/geometry.d.ts +23 -0
  350. package/dist/types/src/testing/index.d.ts +12 -0
  351. package/dist/types/src/testing/pointer.d.ts +31 -0
  352. package/dist/types/src/theme/apply.d.ts +27 -0
  353. package/dist/types/src/theme/color.d.ts +35 -0
  354. package/dist/types/src/theme/extendTheme.d.ts +63 -0
  355. package/dist/types/src/theme/index.d.ts +22 -0
  356. package/dist/types/src/theme/roles.d.ts +16 -0
  357. package/dist/types/src/theme/tone.d.ts +32 -0
  358. package/dist/types/src/theme/validate.d.ts +16 -0
  359. package/dist/types/src/tokens/types.d.ts +8 -0
  360. package/dist/web-types.json +214 -13
  361. package/package.json +47 -4
  362. package/tokens/themes/dark.json +2 -1
  363. package/dist/chunks/chevron-right-BfqMVpbB.js.map +0 -1
  364. package/dist/chunks/config-8Pcutu1j.js.map +0 -1
  365. package/dist/chunks/config-B-g6lT19.js.map +0 -1
  366. package/dist/chunks/config-BE6n20np.js.map +0 -1
  367. package/dist/chunks/config-BKvX0q10.js.map +0 -1
  368. package/dist/chunks/config-BNfQwyL4.js +0 -97
  369. package/dist/chunks/config-BNfQwyL4.js.map +0 -1
  370. package/dist/chunks/config-BoNR7_PD.js.map +0 -1
  371. package/dist/chunks/config-CN2ULSQL2.js.map +0 -1
  372. package/dist/chunks/config-CY0_6cMi.js.map +0 -1
  373. package/dist/chunks/config-Cw6TDzWv2.js.map +0 -1
  374. package/dist/chunks/config-D8tSR2uS.js.map +0 -1
  375. package/dist/chunks/config-DpqOnJiz2.js.map +0 -1
  376. package/dist/chunks/config-DxCTVBfx.js.map +0 -1
  377. package/dist/chunks/config-LGrZIdaZ.js +0 -21
  378. package/dist/chunks/config-LGrZIdaZ.js.map +0 -1
  379. package/dist/chunks/config-Rw9OyW70.js.map +0 -1
  380. package/dist/chunks/config-T3qdQBgU.js.map +0 -1
  381. package/dist/chunks/config-eXB87DOi2.js.map +0 -1
  382. package/dist/chunks/en-B3xMeswz.js.map +0 -1
  383. package/dist/chunks/es-CA-mkU--.js.map +0 -1
  384. package/dist/chunks/grKbdStyles-DAoxiytu.js +0 -31
  385. package/dist/chunks/grKbdStyles-DAoxiytu.js.map +0 -1
  386. package/dist/chunks/granular-provider-CtrXGa7c.js.map +0 -1
  387. package/dist/chunks/keyboard-zYvvyHd4.js.map +0 -1
  388. package/dist/chunks/optionFilter-BVY988pC.js.map +0 -1
  389. package/dist/chunks/ru-COWxgh2A.js.map +0 -1
  390. package/dist/chunks/useOverlayLayer-BupuP_hs.js.map +0 -1
  391. package/dist/components/GrAutocomplete/chunks/GrAutocomplete-CQ9T7Lly.js.map +0 -1
  392. package/dist/components/GrBreadcrumbs/chunks/GrBreadcrumbs-BCNGDS6k.js.map +0 -1
  393. package/dist/components/GrCommandPalette/chunks/GrCommandPalette-MWDYDPNC.js.map +0 -1
  394. package/dist/components/GrDataTable/chunks/GrDataTable-DVFho9Cj.js.map +0 -1
  395. package/dist/components/GrImageViewer/chunks/GrImageViewer-BeCnE8XQ.js.map +0 -1
  396. package/dist/components/GrInput/chunks/GrInput-DD2gYdqS.js.map +0 -1
  397. package/dist/components/GrInputTag/chunks/GrInputTag-DmNunixz.js.map +0 -1
  398. package/dist/components/GrKbd/chunks/GrKbd-DRFPvW3G.js.map +0 -1
  399. package/dist/components/GrPagination/chunks/GrPagination-B6N0QZBf.js.map +0 -1
  400. package/dist/components/GrProgressBar/chunks/GrProgressBar-BiHnaitu.js.map +0 -1
  401. package/dist/components/GrRadio/chunks/GrRadio-DksDro_U.js.map +0 -1
  402. package/dist/components/GrRadioGroup/chunks/GrRadioGroup-BOIhejE6.js.map +0 -1
  403. package/dist/components/GrRating/chunks/GrRating-JrHXz0Rv.js.map +0 -1
  404. package/dist/components/GrSegmented/chunks/GrSegmented-BQIlH5KI.js.map +0 -1
  405. package/dist/components/GrSelect/chunks/GrSelect-aqkPCqjd.js.map +0 -1
  406. package/dist/components/GrSlider/chunks/GrSlider-DkU9IQSX.js.map +0 -1
  407. package/dist/components/GrSplitter/chunks/GrSplitter-7To8jbJ8.js.map +0 -1
  408. package/dist/components/GrStatistic/chunks/GrStatistic-CttyoYPT.js.map +0 -1
  409. package/dist/components/GrTable/chunks/GrTable-BB-mI_NM.js.map +0 -1
  410. package/dist/components/GrTabs/chunks/GrTabs-B8CDq2D9.js.map +0 -1
  411. package/dist/components/GrToaster/chunks/GrToaster-BKQy3Srm.js.map +0 -1
  412. package/dist/components/GrTree/chunks/GrTree-wfdbvygw.js.map +0 -1
  413. package/dist/components/GrTreeSelect/chunks/GrTreeSelect-lLWmAMtI.js.map +0 -1
  414. package/dist/tokens.js.map +0 -1
  415. package/dist/chunks/{config-8Pcutu1j.js → config-8Pcutu1j2.js} +0 -0
  416. package/dist/chunks/{config-BE6n20np.js → config-BE6n20np2.js} +0 -0
  417. package/dist/chunks/{config-BKvX0q10.js → config-BKvX0q102.js} +0 -0
  418. package/dist/chunks/{config-BoNR7_PD.js → config-BoNR7_PD2.js} +0 -0
  419. package/dist/chunks/{config-CN2ULSQL2.js → config-CN2ULSQL.js} +0 -0
  420. package/dist/chunks/{config-CY0_6cMi.js → config-CY0_6cMi2.js} +0 -0
  421. package/dist/chunks/{config-Rw9OyW70.js → config-Rw9OyW702.js} +0 -0
  422. package/dist/chunks/{config-T3qdQBgU.js → config-T3qdQBgU2.js} +0 -0
  423. package/dist/chunks/{config-eXB87DOi2.js → config-eXB87DOi.js} +0 -0
  424. package/dist/components/GrCollapse/chunks/{GrCollapse-B2Fc6A9r.js → GrCollapse-Kk0DE8tb.js} +1 -1
  425. /package/dist/types/src/composables/{internal/useComboboxNavigation.d.ts → useComboboxNavigation.d.ts} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"file":"GrSplitter-7To8jbJ8.js","names":[],"sources":["../../../../src/components/GrSplitter/splitterGeometry.ts","../../../../src/components/GrSplitter/GrSplitter.vue","../../../../src/components/GrSplitter/GrSplitter.vue"],"sourcesContent":["/**\n * Арифметика раздвижных панелей. Без Vue — проверяется без монтирования.\n *\n * Всё считается в процентах ширины (или высоты) контейнера: клавиатура,\n * границы и сворачивание работают без единого замера DOM, а серверный рендер\n * совпадает с клиентским. Замер нужен ровно в одном месте — перевод координаты\n * указателя в проценты.\n */\n\nexport const GR_SPLITTER_ORIENTATIONS = ['horizontal', 'vertical'] as const\n\nexport type GrSplitterOrientation = typeof GR_SPLITTER_ORIENTATIONS[number]\n\nexport interface GrSplitterBounds {\n /** Минимум первой панели. */\n min: number\n /** Максимум первой панели. */\n max: number\n /** Минимум второй панели: без него её можно задавить в ноль. */\n minEnd: number\n}\n\nfunction toFinite(value: number, fallback: number): number {\n return Number.isFinite(value) ? value : fallback\n}\n\n/**\n * Приводит долю первой панели к допустимой.\n *\n * При конфликте `min` и `minEnd` побеждает `min`: первая панель не должна\n * уезжать под свой минимум, даже если вторая при этом окажется уже своего.\n * Обратный порядок дал бы панель, которую пользователь не может вернуть.\n */\nexport function clampSize(value: number, bounds: GrSplitterBounds): number {\n const min = Math.min(100, Math.max(0, toFinite(bounds.min, 0)))\n const max = Math.min(100, Math.max(min, toFinite(bounds.max, 100)))\n const minEnd = Math.min(100, Math.max(0, toFinite(bounds.minEnd, 0)))\n\n const upper = Math.max(min, Math.min(max, 100 - minEnd))\n\n return Math.min(upper, Math.max(min, toFinite(value, min)))\n}\n\n/** Доля, соответствующая позиции указателя. Вне контейнера — его края. */\nexport function sizeFromPointer(\n rect: { left: number, top: number, width: number, height: number },\n clientX: number,\n clientY: number,\n orientation: GrSplitterOrientation,\n): number {\n const total = orientation === 'horizontal' ? rect.width : rect.height\n // Схлопнутый контейнер (ещё не в раскладке, `display: none`) — делить не на что.\n if (!(total > 0)) return 0\n\n const offset = orientation === 'horizontal' ? clientX - rect.left : clientY - rect.top\n\n return Math.min(100, Math.max(0, (offset / total) * 100))\n}\n\n/**\n * Тянуть панель ниже половины её минимума пользователь может только с одним\n * намерением — убрать её совсем. Это единственный способ свернуть панель\n * мышью: двойной клик занят сбросом к дефолту, `Enter` — клавиатурой.\n */\nexport function shouldCollapse(value: number, bounds: GrSplitterBounds): boolean {\n const min = Math.min(100, Math.max(0, toFinite(bounds.min, 0)))\n if (min <= 0) return false\n\n return toFinite(value, min) < min / 2\n}\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, useId, watch } from 'vue'\n\nimport { useGrComponentProp } from '../GrConfigProvider/context'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n\nimport { paneClass, rootClass, separatorClass } from './grSplitterStyles'\nimport {\n clampSize,\n shouldCollapse,\n sizeFromPointer,\n type GrSplitterBounds,\n type GrSplitterOrientation,\n} from './splitterGeometry'\n\nexport type { GrSplitterOrientation } from './splitterGeometry'\n\n/**\n * Пропы {@link GrSplitter}.\n *\n * Две панели и разделитель между ними. Размер первой панели — доля контейнера\n * в процентах: она переживает смену ширины окна и рендерится на сервере без\n * замеров.\n */\nexport interface GrSplitterProps {\n /** Доля первой панели в процентах. Поддерживает `v-model`; без него компонент помнит размер сам. */\n modelValue?: number\n /** Стартовая доля и точка возврата по двойному клику. */\n defaultSize?: number\n /** `horizontal` — панели рядом, `vertical` — одна над другой. */\n orientation?: GrSplitterOrientation\n /** Минимум первой панели в процентах. */\n min?: number\n /** Максимум первой панели в процентах. */\n max?: number\n /** Минимум второй панели: без него её можно задавить в ноль. */\n minEnd?: number\n /** Шаг стрелок. */\n step?: number\n /** Шаг стрелок с `Shift`. */\n bigStep?: number\n /** Разрешить сворачивание первой панели. */\n collapsible?: boolean\n /** Первая панель свёрнута. Поддерживает `v-model:collapsed`. */\n collapsed?: boolean\n /** Разделитель не тянется, не фокусируется и не отвечает на клавиши. */\n disabled?: boolean\n /** Имя разделителя для скринридера. Не задано — из локали. */\n ariaLabel?: string\n}\n\nexport interface GrSplitterEmits {\n (e: 'update:modelValue', value: number): void\n (e: 'update:collapsed', value: boolean): void\n /** Конец жеста и каждый шаг с клавиатуры: сюда вешают сохранение раскладки. */\n (e: 'change', value: number): void\n}\n\nconst props = withDefaults(defineProps<GrSplitterProps>(), {\n modelValue: undefined,\n defaultSize: 50,\n min: 10,\n max: 90,\n minEnd: 10,\n step: 1,\n bigStep: 10,\n collapsed: false,\n disabled: false,\n ariaLabel: undefined,\n // `undefined`, а не готовые значения: иначе `componentDefaults` до них не дошли бы.\n orientation: undefined,\n collapsible: undefined,\n})\n\nconst emit = defineEmits<GrSplitterEmits>()\n\ndefineSlots<{\n /** Первая панель — та, чей размер задаёт модель. */\n start?: () => any\n /** Вторая панель: занимает остаток. */\n end?: () => any\n}>()\n\nconst { t } = useGranularityTranslations()\n\nconst orientation = useGrComponentProp('GrSplitter', 'orientation', () => props.orientation, 'horizontal')\nconst collapsible = useGrComponentProp('GrSplitter', 'collapsible', () => props.collapsible, false)\n\nconst startId = useId()\nconst rootEl = ref<HTMLElement | null>(null)\n\nconst bounds = computed<GrSplitterBounds>(() => ({\n min: props.min,\n max: props.max,\n minEnd: props.minEnd,\n}))\n\n/**\n * Локальное состояние с синхронизацией из пропа — поддерживает и controlled\n * (`v-model`), и uncontrolled (сплиттер помнит размер сам), как `collapsed`\n * у `GrSidebar`.\n */\nconst sizeState = ref(clampSize(props.modelValue ?? props.defaultSize, bounds.value))\nwatch(() => props.modelValue, (value) => {\n if (value !== undefined) sizeState.value = clampSize(value, bounds.value)\n})\n\nconst collapsedState = ref(props.collapsed)\nwatch(() => props.collapsed, value => (collapsedState.value = value))\n\n/** Размер, к которому возвращает `Enter`: свёрнутость не должна его терять. */\nconst restoreSize = ref(sizeState.value)\n\n/** Значение до начала жеста: `pointercancel` откатывает к нему. */\nlet sizeBeforeDrag = sizeState.value\nlet collapsedBeforeDrag = false\n\nconst isDragging = ref(false)\n\nconst isCollapsed = computed(() => collapsible.value && collapsedState.value)\n\n/** Что видно на экране: свёрнутая панель — это ноль в треке, но не ноль в модели. */\nconst renderedSize = computed(() => (isCollapsed.value ? 0 : clampSize(sizeState.value, bounds.value)))\n\nconst gridStyle = computed(() => {\n const track = `${renderedSize.value}% var(--gr-splitter-size, 6px) 1fr`\n\n return orientation.value === 'horizontal'\n ? { gridTemplateColumns: track }\n : { gridTemplateRows: track }\n})\n\n/**\n * `aria-orientation` описывает **сам разделитель**, а не раскладку: панели,\n * стоящие рядом, разделяет вертикальная полоса.\n */\nconst separatorOrientation = computed(() => (orientation.value === 'horizontal' ? 'vertical' : 'horizontal'))\n\nconst label = computed(() => props.ariaLabel ?? t('gr.splitter.label', 'Resize panels'))\n\nfunction setSize(next: number, commit: boolean): void {\n const value = clampSize(next, bounds.value)\n const changed = value !== sizeState.value\n\n sizeState.value = value\n if (changed) emit('update:modelValue', value)\n if (commit) emit('change', value)\n}\n\nfunction setCollapsed(value: boolean): void {\n if (collapsedState.value === value) return\n\n collapsedState.value = value\n emit('update:collapsed', value)\n}\n\n/**\n * Шаг размера. Свёрнутая панель на первое нажатие возвращается — двигать\n * то, чего не видно, значит менять раскладку вслепую.\n */\nfunction moveBy(delta: number): void {\n if (isCollapsed.value) {\n setCollapsed(false)\n setSize(restoreSize.value, true)\n return\n }\n\n restoreSize.value = clampSize(sizeState.value + delta, bounds.value)\n setSize(sizeState.value + delta, true)\n}\n\nfunction toggleCollapsed(): void {\n if (!collapsible.value) return\n\n if (isCollapsed.value) {\n setCollapsed(false)\n setSize(restoreSize.value, true)\n return\n }\n\n restoreSize.value = sizeState.value\n setCollapsed(true)\n emit('change', 0)\n}\n\nfunction reset(): void {\n setCollapsed(false)\n restoreSize.value = clampSize(props.defaultSize, bounds.value)\n setSize(props.defaultSize, true)\n}\n\n// ————— Указатель.\nfunction onPointerMove(event: PointerEvent): void {\n const rect = rootEl.value?.getBoundingClientRect()\n if (!rect) return\n\n // Против выделения текста под курсором во время жеста.\n event.preventDefault()\n\n const raw = sizeFromPointer(rect, event.clientX, event.clientY, orientation.value)\n\n if (collapsible.value && shouldCollapse(raw, bounds.value)) {\n if (!collapsedState.value) restoreSize.value = sizeState.value\n setCollapsed(true)\n return\n }\n\n setCollapsed(false)\n restoreSize.value = clampSize(raw, bounds.value)\n setSize(raw, false)\n}\n\nfunction stopListening(): void {\n window.removeEventListener('pointermove', onPointerMove)\n window.removeEventListener('pointerup', onPointerUp)\n window.removeEventListener('pointercancel', onPointerCancel)\n}\n\nfunction endDrag(commit: boolean): void {\n if (!isDragging.value) return\n\n isDragging.value = false\n stopListening()\n\n if (commit) {\n emit('change', renderedSize.value)\n return\n }\n\n // Оборванный жест (браузер забрал указатель) не коммитит ничего: раскладка\n // возвращается к той, что была до нажатия.\n setCollapsed(collapsedBeforeDrag)\n setSize(sizeBeforeDrag, false)\n}\n\nfunction onPointerUp(): void {\n endDrag(true)\n}\n\nfunction onPointerCancel(): void {\n endDrag(false)\n}\n\nfunction onPointerDown(event: PointerEvent): void {\n if (props.disabled) return\n // Только основная кнопка: правый и средний клик перетаскиванием не считаются.\n if (event.button !== 0) return\n\n sizeBeforeDrag = sizeState.value\n collapsedBeforeDrag = collapsedState.value\n isDragging.value = true\n\n window.addEventListener('pointermove', onPointerMove)\n window.addEventListener('pointerup', onPointerUp)\n window.addEventListener('pointercancel', onPointerCancel)\n}\n\nonBeforeUnmount(stopListening)\n\n// ————— Клавиатура.\nconst decreaseKey = computed(() => (orientation.value === 'horizontal' ? 'ArrowLeft' : 'ArrowUp'))\nconst increaseKey = computed(() => (orientation.value === 'horizontal' ? 'ArrowRight' : 'ArrowDown'))\n\nfunction onKeydown(event: KeyboardEvent): void {\n if (props.disabled) return\n\n const step = event.shiftKey ? props.bigStep : props.step\n\n if (event.key === decreaseKey.value) moveBy(-step)\n else if (event.key === increaseKey.value) moveBy(step)\n else if (event.key === 'Home') moveBy(-100)\n else if (event.key === 'End') moveBy(100)\n else if (event.key === 'Enter') toggleCollapsed()\n else return\n\n // Дошли сюда — клавиша наша: страница не должна ещё и прокрутиться.\n event.preventDefault()\n}\n\nfunction onDoubleClick(): void {\n if (props.disabled) return\n\n reset()\n}\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n data-gr-splitter\n :data-orientation=\"orientation\"\n :data-dragging=\"isDragging ? '' : undefined\"\n :data-collapsed=\"isCollapsed ? '' : undefined\"\n :class=\"rootClass\"\n :style=\"gridStyle\"\n >\n <div\n :id=\"startId\"\n data-gr-splitter-pane=\"start\"\n :class=\"paneClass\"\n >\n <slot name=\"start\" />\n </div>\n\n <div\n data-gr-splitter-separator\n role=\"separator\"\n :tabindex=\"disabled ? undefined : 0\"\n :aria-orientation=\"separatorOrientation\"\n :aria-label=\"label\"\n :aria-controls=\"startId\"\n :aria-valuenow=\"Math.round(renderedSize)\"\n :aria-valuemin=\"Math.round(min)\"\n :aria-valuemax=\"Math.round(max)\"\n :aria-disabled=\"disabled ? 'true' : undefined\"\n :class=\"separatorClass\"\n @pointerdown=\"onPointerDown\"\n @keydown=\"onKeydown\"\n @dblclick=\"onDoubleClick\"\n />\n\n <div\n data-gr-splitter-pane=\"end\"\n :class=\"paneClass\"\n >\n <slot name=\"end\" />\n </div>\n </div>\n</template>\n\n<style>\n[data-gr-splitter-separator] {\n background-color: var(--gr-splitter-color, var(--gr-brd));\n}\n\n/*\n * Зона захвата шире видимой полосы: шесть пикселей мышью не берутся, а делать\n * ради этого полосу толще — значит красить раскладку под удобство мыши.\n */\n[data-gr-splitter-separator]::before {\n content: '';\n position: absolute;\n inset: calc(-1 * var(--gr-splitter-hit, 4px));\n}\n\n/* Насечка: короткая полоса посередине — единственный видимый намёк, что это ручка. */\n[data-gr-splitter-separator]::after {\n content: '';\n position: absolute;\n top: 50%;\n left: 50%;\n border-radius: var(--gr-radius-full);\n background-color: var(--gr-splitter-hover-color, var(--gr-muted-fg));\n opacity: 0;\n transform: translate(-50%, -50%);\n transition: opacity var(--gr-duration-fast) var(--gr-ease-out);\n}\n\n[data-gr-splitter][data-orientation='horizontal'] [data-gr-splitter-separator] {\n cursor: col-resize;\n}\n\n[data-gr-splitter][data-orientation='horizontal'] [data-gr-splitter-separator]::after {\n width: 2px;\n height: var(--gr-splitter-grip, 24px);\n}\n\n[data-gr-splitter][data-orientation='vertical'] [data-gr-splitter-separator] {\n cursor: row-resize;\n}\n\n[data-gr-splitter][data-orientation='vertical'] [data-gr-splitter-separator]::after {\n width: var(--gr-splitter-grip, 24px);\n height: 2px;\n}\n\n[data-gr-splitter-separator]:hover {\n background-color: var(--gr-splitter-hover-color, var(--gr-muted-fg));\n}\n\n[data-gr-splitter-separator]:hover::after,\n[data-gr-splitter-separator]:focus-visible::after {\n opacity: 1;\n}\n\n/* На залитой полосе насечка читается только вырезом — цветом страницы. */\n[data-gr-splitter-separator]:hover::after,\n[data-gr-splitter][data-dragging] [data-gr-splitter-separator]::after {\n background-color: var(--gr-bg);\n}\n\n/* Во время жеста полоса подсвечена, даже если курсор ушёл с неё. */\n[data-gr-splitter][data-dragging] [data-gr-splitter-separator] {\n background-color: var(--gr-splitter-active-color, var(--gr-primary));\n}\n\n[data-gr-splitter][data-dragging] [data-gr-splitter-separator]::after {\n opacity: 1;\n}\n\n/* Жест не должен попутно выделять текст в панелях. */\n[data-gr-splitter][data-dragging] {\n user-select: none;\n}\n\n[data-gr-splitter-separator][aria-disabled='true'] {\n cursor: default;\n}\n\n[data-gr-splitter-separator][aria-disabled='true']::after {\n display: none;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, useId, watch } from 'vue'\n\nimport { useGrComponentProp } from '../GrConfigProvider/context'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n\nimport { paneClass, rootClass, separatorClass } from './grSplitterStyles'\nimport {\n clampSize,\n shouldCollapse,\n sizeFromPointer,\n type GrSplitterBounds,\n type GrSplitterOrientation,\n} from './splitterGeometry'\n\nexport type { GrSplitterOrientation } from './splitterGeometry'\n\n/**\n * Пропы {@link GrSplitter}.\n *\n * Две панели и разделитель между ними. Размер первой панели — доля контейнера\n * в процентах: она переживает смену ширины окна и рендерится на сервере без\n * замеров.\n */\nexport interface GrSplitterProps {\n /** Доля первой панели в процентах. Поддерживает `v-model`; без него компонент помнит размер сам. */\n modelValue?: number\n /** Стартовая доля и точка возврата по двойному клику. */\n defaultSize?: number\n /** `horizontal` — панели рядом, `vertical` — одна над другой. */\n orientation?: GrSplitterOrientation\n /** Минимум первой панели в процентах. */\n min?: number\n /** Максимум первой панели в процентах. */\n max?: number\n /** Минимум второй панели: без него её можно задавить в ноль. */\n minEnd?: number\n /** Шаг стрелок. */\n step?: number\n /** Шаг стрелок с `Shift`. */\n bigStep?: number\n /** Разрешить сворачивание первой панели. */\n collapsible?: boolean\n /** Первая панель свёрнута. Поддерживает `v-model:collapsed`. */\n collapsed?: boolean\n /** Разделитель не тянется, не фокусируется и не отвечает на клавиши. */\n disabled?: boolean\n /** Имя разделителя для скринридера. Не задано — из локали. */\n ariaLabel?: string\n}\n\nexport interface GrSplitterEmits {\n (e: 'update:modelValue', value: number): void\n (e: 'update:collapsed', value: boolean): void\n /** Конец жеста и каждый шаг с клавиатуры: сюда вешают сохранение раскладки. */\n (e: 'change', value: number): void\n}\n\nconst props = withDefaults(defineProps<GrSplitterProps>(), {\n modelValue: undefined,\n defaultSize: 50,\n min: 10,\n max: 90,\n minEnd: 10,\n step: 1,\n bigStep: 10,\n collapsed: false,\n disabled: false,\n ariaLabel: undefined,\n // `undefined`, а не готовые значения: иначе `componentDefaults` до них не дошли бы.\n orientation: undefined,\n collapsible: undefined,\n})\n\nconst emit = defineEmits<GrSplitterEmits>()\n\ndefineSlots<{\n /** Первая панель — та, чей размер задаёт модель. */\n start?: () => any\n /** Вторая панель: занимает остаток. */\n end?: () => any\n}>()\n\nconst { t } = useGranularityTranslations()\n\nconst orientation = useGrComponentProp('GrSplitter', 'orientation', () => props.orientation, 'horizontal')\nconst collapsible = useGrComponentProp('GrSplitter', 'collapsible', () => props.collapsible, false)\n\nconst startId = useId()\nconst rootEl = ref<HTMLElement | null>(null)\n\nconst bounds = computed<GrSplitterBounds>(() => ({\n min: props.min,\n max: props.max,\n minEnd: props.minEnd,\n}))\n\n/**\n * Локальное состояние с синхронизацией из пропа — поддерживает и controlled\n * (`v-model`), и uncontrolled (сплиттер помнит размер сам), как `collapsed`\n * у `GrSidebar`.\n */\nconst sizeState = ref(clampSize(props.modelValue ?? props.defaultSize, bounds.value))\nwatch(() => props.modelValue, (value) => {\n if (value !== undefined) sizeState.value = clampSize(value, bounds.value)\n})\n\nconst collapsedState = ref(props.collapsed)\nwatch(() => props.collapsed, value => (collapsedState.value = value))\n\n/** Размер, к которому возвращает `Enter`: свёрнутость не должна его терять. */\nconst restoreSize = ref(sizeState.value)\n\n/** Значение до начала жеста: `pointercancel` откатывает к нему. */\nlet sizeBeforeDrag = sizeState.value\nlet collapsedBeforeDrag = false\n\nconst isDragging = ref(false)\n\nconst isCollapsed = computed(() => collapsible.value && collapsedState.value)\n\n/** Что видно на экране: свёрнутая панель — это ноль в треке, но не ноль в модели. */\nconst renderedSize = computed(() => (isCollapsed.value ? 0 : clampSize(sizeState.value, bounds.value)))\n\nconst gridStyle = computed(() => {\n const track = `${renderedSize.value}% var(--gr-splitter-size, 6px) 1fr`\n\n return orientation.value === 'horizontal'\n ? { gridTemplateColumns: track }\n : { gridTemplateRows: track }\n})\n\n/**\n * `aria-orientation` описывает **сам разделитель**, а не раскладку: панели,\n * стоящие рядом, разделяет вертикальная полоса.\n */\nconst separatorOrientation = computed(() => (orientation.value === 'horizontal' ? 'vertical' : 'horizontal'))\n\nconst label = computed(() => props.ariaLabel ?? t('gr.splitter.label', 'Resize panels'))\n\nfunction setSize(next: number, commit: boolean): void {\n const value = clampSize(next, bounds.value)\n const changed = value !== sizeState.value\n\n sizeState.value = value\n if (changed) emit('update:modelValue', value)\n if (commit) emit('change', value)\n}\n\nfunction setCollapsed(value: boolean): void {\n if (collapsedState.value === value) return\n\n collapsedState.value = value\n emit('update:collapsed', value)\n}\n\n/**\n * Шаг размера. Свёрнутая панель на первое нажатие возвращается — двигать\n * то, чего не видно, значит менять раскладку вслепую.\n */\nfunction moveBy(delta: number): void {\n if (isCollapsed.value) {\n setCollapsed(false)\n setSize(restoreSize.value, true)\n return\n }\n\n restoreSize.value = clampSize(sizeState.value + delta, bounds.value)\n setSize(sizeState.value + delta, true)\n}\n\nfunction toggleCollapsed(): void {\n if (!collapsible.value) return\n\n if (isCollapsed.value) {\n setCollapsed(false)\n setSize(restoreSize.value, true)\n return\n }\n\n restoreSize.value = sizeState.value\n setCollapsed(true)\n emit('change', 0)\n}\n\nfunction reset(): void {\n setCollapsed(false)\n restoreSize.value = clampSize(props.defaultSize, bounds.value)\n setSize(props.defaultSize, true)\n}\n\n// ————— Указатель.\nfunction onPointerMove(event: PointerEvent): void {\n const rect = rootEl.value?.getBoundingClientRect()\n if (!rect) return\n\n // Против выделения текста под курсором во время жеста.\n event.preventDefault()\n\n const raw = sizeFromPointer(rect, event.clientX, event.clientY, orientation.value)\n\n if (collapsible.value && shouldCollapse(raw, bounds.value)) {\n if (!collapsedState.value) restoreSize.value = sizeState.value\n setCollapsed(true)\n return\n }\n\n setCollapsed(false)\n restoreSize.value = clampSize(raw, bounds.value)\n setSize(raw, false)\n}\n\nfunction stopListening(): void {\n window.removeEventListener('pointermove', onPointerMove)\n window.removeEventListener('pointerup', onPointerUp)\n window.removeEventListener('pointercancel', onPointerCancel)\n}\n\nfunction endDrag(commit: boolean): void {\n if (!isDragging.value) return\n\n isDragging.value = false\n stopListening()\n\n if (commit) {\n emit('change', renderedSize.value)\n return\n }\n\n // Оборванный жест (браузер забрал указатель) не коммитит ничего: раскладка\n // возвращается к той, что была до нажатия.\n setCollapsed(collapsedBeforeDrag)\n setSize(sizeBeforeDrag, false)\n}\n\nfunction onPointerUp(): void {\n endDrag(true)\n}\n\nfunction onPointerCancel(): void {\n endDrag(false)\n}\n\nfunction onPointerDown(event: PointerEvent): void {\n if (props.disabled) return\n // Только основная кнопка: правый и средний клик перетаскиванием не считаются.\n if (event.button !== 0) return\n\n sizeBeforeDrag = sizeState.value\n collapsedBeforeDrag = collapsedState.value\n isDragging.value = true\n\n window.addEventListener('pointermove', onPointerMove)\n window.addEventListener('pointerup', onPointerUp)\n window.addEventListener('pointercancel', onPointerCancel)\n}\n\nonBeforeUnmount(stopListening)\n\n// ————— Клавиатура.\nconst decreaseKey = computed(() => (orientation.value === 'horizontal' ? 'ArrowLeft' : 'ArrowUp'))\nconst increaseKey = computed(() => (orientation.value === 'horizontal' ? 'ArrowRight' : 'ArrowDown'))\n\nfunction onKeydown(event: KeyboardEvent): void {\n if (props.disabled) return\n\n const step = event.shiftKey ? props.bigStep : props.step\n\n if (event.key === decreaseKey.value) moveBy(-step)\n else if (event.key === increaseKey.value) moveBy(step)\n else if (event.key === 'Home') moveBy(-100)\n else if (event.key === 'End') moveBy(100)\n else if (event.key === 'Enter') toggleCollapsed()\n else return\n\n // Дошли сюда — клавиша наша: страница не должна ещё и прокрутиться.\n event.preventDefault()\n}\n\nfunction onDoubleClick(): void {\n if (props.disabled) return\n\n reset()\n}\n</script>\n\n<template>\n <div\n ref=\"rootEl\"\n data-gr-splitter\n :data-orientation=\"orientation\"\n :data-dragging=\"isDragging ? '' : undefined\"\n :data-collapsed=\"isCollapsed ? '' : undefined\"\n :class=\"rootClass\"\n :style=\"gridStyle\"\n >\n <div\n :id=\"startId\"\n data-gr-splitter-pane=\"start\"\n :class=\"paneClass\"\n >\n <slot name=\"start\" />\n </div>\n\n <div\n data-gr-splitter-separator\n role=\"separator\"\n :tabindex=\"disabled ? undefined : 0\"\n :aria-orientation=\"separatorOrientation\"\n :aria-label=\"label\"\n :aria-controls=\"startId\"\n :aria-valuenow=\"Math.round(renderedSize)\"\n :aria-valuemin=\"Math.round(min)\"\n :aria-valuemax=\"Math.round(max)\"\n :aria-disabled=\"disabled ? 'true' : undefined\"\n :class=\"separatorClass\"\n @pointerdown=\"onPointerDown\"\n @keydown=\"onKeydown\"\n @dblclick=\"onDoubleClick\"\n />\n\n <div\n data-gr-splitter-pane=\"end\"\n :class=\"paneClass\"\n >\n <slot name=\"end\" />\n </div>\n </div>\n</template>\n\n<style>\n[data-gr-splitter-separator] {\n background-color: var(--gr-splitter-color, var(--gr-brd));\n}\n\n/*\n * Зона захвата шире видимой полосы: шесть пикселей мышью не берутся, а делать\n * ради этого полосу толще — значит красить раскладку под удобство мыши.\n */\n[data-gr-splitter-separator]::before {\n content: '';\n position: absolute;\n inset: calc(-1 * var(--gr-splitter-hit, 4px));\n}\n\n/* Насечка: короткая полоса посередине — единственный видимый намёк, что это ручка. */\n[data-gr-splitter-separator]::after {\n content: '';\n position: absolute;\n top: 50%;\n left: 50%;\n border-radius: var(--gr-radius-full);\n background-color: var(--gr-splitter-hover-color, var(--gr-muted-fg));\n opacity: 0;\n transform: translate(-50%, -50%);\n transition: opacity var(--gr-duration-fast) var(--gr-ease-out);\n}\n\n[data-gr-splitter][data-orientation='horizontal'] [data-gr-splitter-separator] {\n cursor: col-resize;\n}\n\n[data-gr-splitter][data-orientation='horizontal'] [data-gr-splitter-separator]::after {\n width: 2px;\n height: var(--gr-splitter-grip, 24px);\n}\n\n[data-gr-splitter][data-orientation='vertical'] [data-gr-splitter-separator] {\n cursor: row-resize;\n}\n\n[data-gr-splitter][data-orientation='vertical'] [data-gr-splitter-separator]::after {\n width: var(--gr-splitter-grip, 24px);\n height: 2px;\n}\n\n[data-gr-splitter-separator]:hover {\n background-color: var(--gr-splitter-hover-color, var(--gr-muted-fg));\n}\n\n[data-gr-splitter-separator]:hover::after,\n[data-gr-splitter-separator]:focus-visible::after {\n opacity: 1;\n}\n\n/* На залитой полосе насечка читается только вырезом — цветом страницы. */\n[data-gr-splitter-separator]:hover::after,\n[data-gr-splitter][data-dragging] [data-gr-splitter-separator]::after {\n background-color: var(--gr-bg);\n}\n\n/* Во время жеста полоса подсвечена, даже если курсор ушёл с неё. */\n[data-gr-splitter][data-dragging] [data-gr-splitter-separator] {\n background-color: var(--gr-splitter-active-color, var(--gr-primary));\n}\n\n[data-gr-splitter][data-dragging] [data-gr-splitter-separator]::after {\n opacity: 1;\n}\n\n/* Жест не должен попутно выделять текст в панелях. */\n[data-gr-splitter][data-dragging] {\n user-select: none;\n}\n\n[data-gr-splitter-separator][aria-disabled='true'] {\n cursor: default;\n}\n\n[data-gr-splitter-separator][aria-disabled='true']::after {\n display: none;\n}\n</style>\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"GrStatistic-CttyoYPT.js","names":["$slots"],"sources":["../../../../src/components/GrStatistic/countUp.ts","../../../../src/components/GrStatistic/formatStatisticValue.ts","../../../../src/components/GrStatistic/GrStatistic.vue","../../../../src/components/GrStatistic/GrStatistic.vue"],"sourcesContent":["/**\n * Перебор чисел для `GrStatistic`: чистая арифметика кадра без Vue и таймеров.\n *\n * Вынесено отдельным модулем по той же причине, что и `formatStatisticValue`:\n * кривая и округление проверяются напрямую, без монтирования и подмены `rAF`.\n */\n\n/** Быстрый старт, мягкая остановка — та же форма, что у `--gr-ease-out`. */\nexport function easeOutCubic(t: number): number {\n const clamped = Math.min(Math.max(t, 0), 1)\n return 1 - (1 - clamped) ** 3\n}\n\n/**\n * Значение кадра на `elapsed` миллисекунде перехода `from → to`.\n *\n * Округление до `precision` обязательно: без него кадры показывали бы\n * `1283.6666666666667` там, где показатель целый, и ширина строки прыгала бы\n * даже при `tabular-nums`. Последний кадр возвращает ровно `to` — приближение\n * не должно оставлять показатель в 1283.9999.\n */\nexport function countUpFrame(\n from: number,\n to: number,\n elapsed: number,\n duration: number,\n precision?: number,\n): number {\n if (!(duration > 0) || elapsed >= duration) return to\n\n const digits = Math.min(Math.max(precision ?? 0, 0), 20)\n const value = from + (to - from) * easeOutCubic(elapsed / duration)\n\n return Number(value.toFixed(digits))\n}\n","export type GrStatisticFormatOptions = {\n /** Число знаков после запятой. `undefined` — оставить как есть. */\n precision?: number\n /** Разделитель групп разрядов. Сильнее локали. */\n groupSeparator?: string\n /** Десятичный разделитель. Сильнее локали. */\n decimalSeparator?: string\n /** BCP-47 локаль: разделители берутся из `Intl.NumberFormat`. */\n locale?: string\n}\n\nconst DEFAULT_GROUP_SEPARATOR = ' '\nconst DEFAULT_DECIMAL_SEPARATOR = '.'\n\n/**\n * Форматирует значение показателя: фиксирует точность и расставляет разделители.\n *\n * Нечисловые значения возвращаются без изменений — это позволяет показывать в\n * `GrStatistic` уже готовые строки («2 ч 15 мин», «—») без обходных пропов.\n * Вынесено из SFC отдельным модулем, чтобы тестировалось без монтирования.\n *\n * Порядок разрешения разделителей: явный проп → локаль → встроенный дефолт.\n */\nexport function formatStatisticValue(\n value: number | string,\n options: GrStatisticFormatOptions = {},\n): string {\n const { precision, groupSeparator, decimalSeparator, locale } = options\n\n const numeric = typeof value === 'number' ? value : Number(value)\n if (typeof value === 'string' && (value.trim() === '' || !Number.isFinite(numeric))) {\n return value\n }\n if (!Number.isFinite(numeric)) return String(value)\n\n if (locale) {\n return formatByLocale(numeric, locale, { precision, groupSeparator, decimalSeparator })\n }\n\n const fixed = precision === undefined ? String(numeric) : numeric.toFixed(precision)\n // `toFixed` не даёт экспоненциальной записи, а `String(число)` — может (1e21+).\n // В таком случае группировать нечего: возвращаем как есть.\n if (fixed.includes('e') || fixed.includes('E')) return fixed\n\n const negative = fixed.startsWith('-')\n const [intPart, fracPart] = (negative ? fixed.slice(1) : fixed).split('.')\n\n const separator = groupSeparator ?? DEFAULT_GROUP_SEPARATOR\n const grouped = separator\n ? intPart.replace(/\\B(?=(\\d{3})+(?!\\d))/g, separator)\n : intPart\n\n return [\n negative ? '-' : '',\n grouped,\n fracPart ? `${decimalSeparator ?? DEFAULT_DECIMAL_SEPARATOR}${fracPart}` : '',\n ].join('')\n}\n\n/**\n * `formatToParts` вместо `format`: даёт заменить отдельные разделители, оставив\n * остальные правила локали (индийская группировка, знак минуса, порядок частей).\n */\nfunction formatByLocale(\n value: number,\n locale: string,\n options: Pick<GrStatisticFormatOptions, 'precision' | 'groupSeparator' | 'decimalSeparator'>,\n): string {\n const { precision, groupSeparator, decimalSeparator } = options\n // `Intl` принимает не больше 20 знаков и падает `RangeError` на большем,\n // тогда как ручная ветка через `toFixed` пережила бы до 100.\n const digits = precision === undefined ? undefined : Math.min(Math.max(precision, 0), 20)\n\n return new Intl.NumberFormat(locale, {\n useGrouping: true,\n minimumFractionDigits: digits,\n maximumFractionDigits: digits ?? 20,\n })\n .formatToParts(value)\n .map((part) => {\n if (part.type === 'group' && groupSeparator !== undefined) return groupSeparator\n if (part.type === 'decimal' && decimalSeparator !== undefined) return decimalSeparator\n return part.value\n })\n .join('')\n}\n","<script setup lang=\"ts\">\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport { computed, markRaw, onBeforeUnmount, onMounted, ref, useSlots, watch, type Component } from 'vue'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport GrSkeleton from '../GrSkeleton/GrSkeleton.vue'\n\nimport { countUpFrame } from './countUp'\nimport { formatStatisticValue } from './formatStatisticValue'\n\nimport {\n statisticAffixSizeBySize,\n statisticPlaceholderHeightBySize,\n statisticRootClass,\n statisticTitleClass,\n statisticTitleSizeBySize,\n statisticTrendClassByTrend,\n statisticTrendIconByTrend,\n statisticTrendSizeBySize,\n statisticValueClass,\n type GrStatisticSize,\n type GrStatisticTone,\n type GrStatisticTrend,\n} from './grStatisticStyles'\n\nexport type {\n GrStatisticSize,\n GrStatisticTone,\n GrStatisticTrend,\n} from './grStatisticStyles'\nexport type { GrStatisticFormatOptions } from './formatStatisticValue'\n\n/**\n * Публичный GR-примитив «Statistic» — крупный числовой показатель с подписью,\n * префиксом/суффиксом и строкой динамики.\n *\n * Компонент презентационный: не грузит данные и не считает дельту, только\n * форматирует и подаёт. Форматирование — чистая функция `formatStatisticValue`.\n */\nexport interface GrStatisticProps {\n /** Значение показателя. Нечисловая строка выводится как есть. */\n value: number | string\n /** Подпись над значением. */\n title?: string\n /** Число знаков после запятой. */\n precision?: number\n /** Разделитель разрядов. Сильнее локали; без него и без локали — узкий пробел. */\n groupSeparator?: string\n /** Десятичный разделитель. Сильнее локали; без него и без локали — точка. */\n decimalSeparator?: string\n /**\n * BCP-47 локаль форматирования. Не задана — берётся из i18n-адаптера; нет и\n * адаптера — работают ручные разделители.\n */\n locale?: string\n /** Приписка перед значением (валюта, знак). */\n prefix?: string\n /** Приписка после значения (единица измерения, `%`). */\n suffix?: string\n /** UnoCSS-класс иконки слева от блока (например `i-lucide-users`). */\n icon?: string\n size?: GrStatisticSize\n /** Тон значения; точечно перекрывается `--gr-statistic-value-color`. */\n tone?: GrStatisticTone\n /** Направление динамики — задаёт цвет и иконку строки под значением. */\n trend?: GrStatisticTrend\n /** Текст динамики (например `+12,5% к прошлой неделе`). */\n trendText?: string\n /** Состояние загрузки: вместо значения — плейсхолдер. */\n loading?: boolean\n /**\n * Перебирать числа при появлении и при смене значения. Нечисловое значение\n * ставится сразу, под `prefers-reduced-motion: reduce` — тоже.\n */\n animate?: boolean\n /**\n * Длительность перебора в миллисекундах. Не токеном: шкала `--gr-duration-*`\n * заканчивается на 300 мс и описывает смену состояния, а перебор чисел —\n * другой жанр, и его число живёт там же, где твин.\n */\n animateDuration?: number\n /** Свой корневой тег (`RouterLink`, `Link` от Inertia). Сильнее `href`. */\n as?: string | Component\n /** Показатель-ссылка: переход к деталям. */\n href?: string\n /** Показатель-кнопка: интерактивна вся плитка. */\n clickable?: boolean\n}\n\nexport interface GrStatisticEmits {\n (e: 'click', event: MouseEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrStatisticProps>(),\n {\n title: undefined,\n precision: undefined,\n // Дефолты живут в `formatStatisticValue`: иначе «пользователь задал пробел»\n // было бы неотличимо от «сработал дефолт», и локаль не смогла бы победить.\n groupSeparator: undefined,\n decimalSeparator: undefined,\n locale: undefined,\n prefix: undefined,\n suffix: undefined,\n icon: undefined,\n size: undefined,\n tone: 'neutral',\n trend: undefined,\n trendText: undefined,\n loading: false,\n animate: false,\n animateDuration: 600,\n as: undefined,\n href: undefined,\n clickable: false,\n },\n)\n\nconst emit = defineEmits<GrStatisticEmits>()\n\nconst slots = useSlots()\n\nconst { t, locale } = useGranularityTranslations()\n\nconst resolvedLocale = computed(() => props.locale ?? locale.value)\n\n/**\n * Число текущего кадра перебора. `null` — перебора нет, показываем сам проп:\n * так значение остаётся конечным и на сервере, и до первого твина.\n */\nconst frameValue = ref<number | null>(null)\n\nconst formatted = computed(() => formatStatisticValue(frameValue.value ?? props.value, {\n precision: props.precision,\n groupSeparator: props.groupSeparator,\n decimalSeparator: props.decimalSeparator,\n locale: resolvedLocale.value,\n}))\n\n/** Конечное значение — то, что читает диктор, пока на экране идёт перебор. */\nconst finalFormatted = computed(() => formatStatisticValue(props.value, {\n precision: props.precision,\n groupSeparator: props.groupSeparator,\n decimalSeparator: props.decimalSeparator,\n locale: resolvedLocale.value,\n}))\n\nconst isCounting = computed(() => frameValue.value !== null)\n\nconst hasTitle = computed(() => Boolean(props.title || slots.title))\n\nconst isInteractive = computed(() => Boolean(props.as || props.href || props.clickable))\n\nconst rootTag = computed<string | Component>(() => {\n if (props.as)\n return typeof props.as === 'string' ? props.as : markRaw(props.as)\n\n if (props.href)\n return 'a'\n\n return props.clickable ? 'button' : 'div'\n})\n\nconst rootClass = computed(() => statisticRootClass({ interactive: isInteractive.value }))\n\nfunction toNumber(value: number | string): number | null {\n const numeric = typeof value === 'number' ? value : Number(value)\n return Number.isFinite(numeric) ? numeric : null\n}\n\nlet frameHandle: number | null = null\n\nfunction stopCounting(): void {\n if (frameHandle !== null) cancelAnimationFrame(frameHandle)\n frameHandle = null\n}\n\n/**\n * Перебор чисел ведёт JS, а глобальный кламп в `base.css` гасит только CSS —\n * значит уважать «уменьшить движение» обязан сам компонент (`docs/motion.md`).\n * Читается в момент старта, а не в теле `setup`: `matchMedia` на сервере нет, и\n * первый клиентский рендер разошёлся бы с серверным.\n */\nfunction prefersReducedMotion(): boolean {\n return typeof window !== 'undefined'\n && window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches === true\n}\n\nfunction runCountUp(from: number, to: number): void {\n stopCounting()\n\n const duration = props.animateDuration\n if (!(duration > 0) || from === to || prefersReducedMotion()) {\n frameValue.value = null\n return\n }\n\n const started = performance.now()\n frameValue.value = from\n\n const step = (now: number): void => {\n const elapsed = now - started\n const value = countUpFrame(from, to, elapsed, duration, props.precision)\n\n if (elapsed >= duration) {\n // Конец перебора снимает и подмену значения, и подмену для диктора.\n frameValue.value = null\n frameHandle = null\n return\n }\n\n frameValue.value = value\n frameHandle = requestAnimationFrame(step)\n }\n\n frameHandle = requestAnimationFrame(step)\n}\n\n/** Перебирать можно только числа: «2 ч 15 мин» и «—» ставятся сразу. */\nfunction startIfNumeric(from: number | string, to: number | string): void {\n if (!props.animate || props.loading) {\n stopCounting()\n frameValue.value = null\n return\n }\n\n const target = toNumber(to)\n const source = toNumber(from)\n\n if (target === null || source === null) {\n stopCounting()\n frameValue.value = null\n return\n }\n\n runCountUp(source, target)\n}\n\n// Появление плитки считается переходом от нуля: ради этого эффекта проп и нужен.\nonMounted(() => startIfNumeric(0, props.value))\n\nwatch(() => props.value, (next, prev) => startIfNumeric(prev, next))\n\nonBeforeUnmount(stopCounting)\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrStatistic' })\n\n// Иконка направления `aria-hidden`, а «+12,5 %» само по себе рост от падения не\n// отличает: направление доносит скрытая подпись.\nconst trendLabel = computed(() => {\n if (!props.trend) return undefined\n\n const labels = {\n up: () => t('gr.statistic.trendUp', 'Increase'),\n down: () => t('gr.statistic.trendDown', 'Decrease'),\n flat: () => t('gr.statistic.trendFlat', 'No change'),\n }\n\n return labels[props.trend]()\n})\n</script>\n\n<template>\n <component\n :is=\"rootTag\"\n data-gr-statistic\n :type=\"rootTag === 'button' ? 'button' : undefined\"\n :href=\"rootTag === 'a' ? href : undefined\"\n :class=\"rootClass\"\n @click=\"emit('click', $event)\"\n >\n <span\n v-if=\"icon || $slots.icon\"\n data-gr-statistic-icon\n class=\"mt-0.5 inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-[var(--gr-radius-lg)] bg-[var(--gr-muted)] text-[var(--gr-muted-fg)]\"\n >\n <slot name=\"icon\">\n <span class=\"block h-4 w-4\" :class=\"icon\" aria-hidden=\"true\" />\n </slot>\n </span>\n\n <div class=\"min-w-0\">\n <!--\n Подпись и значение — пара «термин — значение», а не два соседних блока.\n `<dl>` появляется только вместе с подписью: список определений без `<dt>`\n — та же бессвязность, только с претензией на семантику. Поля обнуляются\n руками: preflight пакета сбрасывает их лишь у `body`.\n -->\n <component :is=\"hasTitle ? 'dl' : 'div'\" class=\"m-0\">\n <dt\n v-if=\"hasTitle\"\n data-gr-statistic-title\n :class=\"[statisticTitleClass, statisticTitleSizeBySize[resolvedSize]]\"\n >\n <slot name=\"title\">\n {{ title }}\n </slot>\n </dt>\n\n <!-- Плейсхолдер загрузки повторяет высоту строки значения, чтобы блок не прыгал. -->\n <component\n :is=\"hasTitle ? 'dd' : 'div'\"\n v-if=\"loading\"\n data-gr-statistic-placeholder\n data-testid=\"gr-statistic-placeholder\"\n class=\"m-0 mt-1\"\n role=\"status\"\n aria-busy=\"true\"\n >\n <GrSkeleton\n width=\"6rem\"\n :height=\"statisticPlaceholderHeightBySize[resolvedSize]\"\n rounded=\"var(--gr-radius-md)\"\n />\n <span class=\"sr-only\">{{ t('gr.loading.defaultText', 'Loading...') }}</span>\n </component>\n\n <component\n :is=\"hasTitle ? 'dd' : 'div'\"\n v-else\n data-gr-statistic-value\n data-testid=\"gr-statistic-value\"\n class=\"m-0 mt-0.5 flex items-baseline gap-1\"\n >\n <span\n v-if=\"prefix || $slots.prefix\"\n data-gr-statistic-prefix\n class=\"text-[var(--gr-muted-fg)]\"\n :class=\"statisticAffixSizeBySize[resolvedSize]\"\n >\n <slot name=\"prefix\">{{ prefix }}</slot>\n </span>\n\n <span\n data-gr-statistic-number\n :class=\"statisticValueClass({ size: resolvedSize, tone })\"\n :aria-hidden=\"isCounting ? 'true' : undefined\"\n >\n <slot>{{ formatted }}</slot>\n </span>\n\n <!--\n Пока идёт перебор, на экране промежуточное число. Диктору отдаётся\n конечное: «1 284» глазами и «743» в ушах — не шум, а неверные данные.\n -->\n <span v-if=\"isCounting\" data-gr-statistic-final class=\"sr-only\">{{ finalFormatted }}</span>\n\n <span\n v-if=\"suffix || $slots.suffix\"\n data-gr-statistic-suffix\n class=\"text-[var(--gr-muted-fg)]\"\n :class=\"statisticAffixSizeBySize[resolvedSize]\"\n >\n <slot name=\"suffix\">{{ suffix }}</slot>\n </span>\n </component>\n </component>\n\n <div\n v-if=\"trend || trendText || $slots.trend\"\n data-gr-statistic-trend\n data-testid=\"gr-statistic-trend\"\n class=\"mt-1 inline-flex items-center gap-1\"\n :class=\"[statisticTrendSizeBySize[resolvedSize], statisticTrendClassByTrend[trend ?? 'flat']]\"\n >\n <span v-if=\"trendLabel\" data-gr-statistic-trend-label class=\"sr-only\">{{ trendLabel }}</span>\n\n <slot name=\"trend\">\n <span\n v-if=\"trend\"\n class=\"block h-3.5 w-3.5\"\n :class=\"statisticTrendIconByTrend[trend]\"\n aria-hidden=\"true\"\n />\n <span>{{ trendText }}</span>\n </slot>\n </div>\n </div>\n </component>\n</template>\n","<script setup lang=\"ts\">\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport { computed, markRaw, onBeforeUnmount, onMounted, ref, useSlots, watch, type Component } from 'vue'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport GrSkeleton from '../GrSkeleton/GrSkeleton.vue'\n\nimport { countUpFrame } from './countUp'\nimport { formatStatisticValue } from './formatStatisticValue'\n\nimport {\n statisticAffixSizeBySize,\n statisticPlaceholderHeightBySize,\n statisticRootClass,\n statisticTitleClass,\n statisticTitleSizeBySize,\n statisticTrendClassByTrend,\n statisticTrendIconByTrend,\n statisticTrendSizeBySize,\n statisticValueClass,\n type GrStatisticSize,\n type GrStatisticTone,\n type GrStatisticTrend,\n} from './grStatisticStyles'\n\nexport type {\n GrStatisticSize,\n GrStatisticTone,\n GrStatisticTrend,\n} from './grStatisticStyles'\nexport type { GrStatisticFormatOptions } from './formatStatisticValue'\n\n/**\n * Публичный GR-примитив «Statistic» — крупный числовой показатель с подписью,\n * префиксом/суффиксом и строкой динамики.\n *\n * Компонент презентационный: не грузит данные и не считает дельту, только\n * форматирует и подаёт. Форматирование — чистая функция `formatStatisticValue`.\n */\nexport interface GrStatisticProps {\n /** Значение показателя. Нечисловая строка выводится как есть. */\n value: number | string\n /** Подпись над значением. */\n title?: string\n /** Число знаков после запятой. */\n precision?: number\n /** Разделитель разрядов. Сильнее локали; без него и без локали — узкий пробел. */\n groupSeparator?: string\n /** Десятичный разделитель. Сильнее локали; без него и без локали — точка. */\n decimalSeparator?: string\n /**\n * BCP-47 локаль форматирования. Не задана — берётся из i18n-адаптера; нет и\n * адаптера — работают ручные разделители.\n */\n locale?: string\n /** Приписка перед значением (валюта, знак). */\n prefix?: string\n /** Приписка после значения (единица измерения, `%`). */\n suffix?: string\n /** UnoCSS-класс иконки слева от блока (например `i-lucide-users`). */\n icon?: string\n size?: GrStatisticSize\n /** Тон значения; точечно перекрывается `--gr-statistic-value-color`. */\n tone?: GrStatisticTone\n /** Направление динамики — задаёт цвет и иконку строки под значением. */\n trend?: GrStatisticTrend\n /** Текст динамики (например `+12,5% к прошлой неделе`). */\n trendText?: string\n /** Состояние загрузки: вместо значения — плейсхолдер. */\n loading?: boolean\n /**\n * Перебирать числа при появлении и при смене значения. Нечисловое значение\n * ставится сразу, под `prefers-reduced-motion: reduce` — тоже.\n */\n animate?: boolean\n /**\n * Длительность перебора в миллисекундах. Не токеном: шкала `--gr-duration-*`\n * заканчивается на 300 мс и описывает смену состояния, а перебор чисел —\n * другой жанр, и его число живёт там же, где твин.\n */\n animateDuration?: number\n /** Свой корневой тег (`RouterLink`, `Link` от Inertia). Сильнее `href`. */\n as?: string | Component\n /** Показатель-ссылка: переход к деталям. */\n href?: string\n /** Показатель-кнопка: интерактивна вся плитка. */\n clickable?: boolean\n}\n\nexport interface GrStatisticEmits {\n (e: 'click', event: MouseEvent): void\n}\n\nconst props = withDefaults(\n defineProps<GrStatisticProps>(),\n {\n title: undefined,\n precision: undefined,\n // Дефолты живут в `formatStatisticValue`: иначе «пользователь задал пробел»\n // было бы неотличимо от «сработал дефолт», и локаль не смогла бы победить.\n groupSeparator: undefined,\n decimalSeparator: undefined,\n locale: undefined,\n prefix: undefined,\n suffix: undefined,\n icon: undefined,\n size: undefined,\n tone: 'neutral',\n trend: undefined,\n trendText: undefined,\n loading: false,\n animate: false,\n animateDuration: 600,\n as: undefined,\n href: undefined,\n clickable: false,\n },\n)\n\nconst emit = defineEmits<GrStatisticEmits>()\n\nconst slots = useSlots()\n\nconst { t, locale } = useGranularityTranslations()\n\nconst resolvedLocale = computed(() => props.locale ?? locale.value)\n\n/**\n * Число текущего кадра перебора. `null` — перебора нет, показываем сам проп:\n * так значение остаётся конечным и на сервере, и до первого твина.\n */\nconst frameValue = ref<number | null>(null)\n\nconst formatted = computed(() => formatStatisticValue(frameValue.value ?? props.value, {\n precision: props.precision,\n groupSeparator: props.groupSeparator,\n decimalSeparator: props.decimalSeparator,\n locale: resolvedLocale.value,\n}))\n\n/** Конечное значение — то, что читает диктор, пока на экране идёт перебор. */\nconst finalFormatted = computed(() => formatStatisticValue(props.value, {\n precision: props.precision,\n groupSeparator: props.groupSeparator,\n decimalSeparator: props.decimalSeparator,\n locale: resolvedLocale.value,\n}))\n\nconst isCounting = computed(() => frameValue.value !== null)\n\nconst hasTitle = computed(() => Boolean(props.title || slots.title))\n\nconst isInteractive = computed(() => Boolean(props.as || props.href || props.clickable))\n\nconst rootTag = computed<string | Component>(() => {\n if (props.as)\n return typeof props.as === 'string' ? props.as : markRaw(props.as)\n\n if (props.href)\n return 'a'\n\n return props.clickable ? 'button' : 'div'\n})\n\nconst rootClass = computed(() => statisticRootClass({ interactive: isInteractive.value }))\n\nfunction toNumber(value: number | string): number | null {\n const numeric = typeof value === 'number' ? value : Number(value)\n return Number.isFinite(numeric) ? numeric : null\n}\n\nlet frameHandle: number | null = null\n\nfunction stopCounting(): void {\n if (frameHandle !== null) cancelAnimationFrame(frameHandle)\n frameHandle = null\n}\n\n/**\n * Перебор чисел ведёт JS, а глобальный кламп в `base.css` гасит только CSS —\n * значит уважать «уменьшить движение» обязан сам компонент (`docs/motion.md`).\n * Читается в момент старта, а не в теле `setup`: `matchMedia` на сервере нет, и\n * первый клиентский рендер разошёлся бы с серверным.\n */\nfunction prefersReducedMotion(): boolean {\n return typeof window !== 'undefined'\n && window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches === true\n}\n\nfunction runCountUp(from: number, to: number): void {\n stopCounting()\n\n const duration = props.animateDuration\n if (!(duration > 0) || from === to || prefersReducedMotion()) {\n frameValue.value = null\n return\n }\n\n const started = performance.now()\n frameValue.value = from\n\n const step = (now: number): void => {\n const elapsed = now - started\n const value = countUpFrame(from, to, elapsed, duration, props.precision)\n\n if (elapsed >= duration) {\n // Конец перебора снимает и подмену значения, и подмену для диктора.\n frameValue.value = null\n frameHandle = null\n return\n }\n\n frameValue.value = value\n frameHandle = requestAnimationFrame(step)\n }\n\n frameHandle = requestAnimationFrame(step)\n}\n\n/** Перебирать можно только числа: «2 ч 15 мин» и «—» ставятся сразу. */\nfunction startIfNumeric(from: number | string, to: number | string): void {\n if (!props.animate || props.loading) {\n stopCounting()\n frameValue.value = null\n return\n }\n\n const target = toNumber(to)\n const source = toNumber(from)\n\n if (target === null || source === null) {\n stopCounting()\n frameValue.value = null\n return\n }\n\n runCountUp(source, target)\n}\n\n// Появление плитки считается переходом от нуля: ради этого эффекта проп и нужен.\nonMounted(() => startIfNumeric(0, props.value))\n\nwatch(() => props.value, (next, prev) => startIfNumeric(prev, next))\n\nonBeforeUnmount(stopCounting)\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrStatistic' })\n\n// Иконка направления `aria-hidden`, а «+12,5 %» само по себе рост от падения не\n// отличает: направление доносит скрытая подпись.\nconst trendLabel = computed(() => {\n if (!props.trend) return undefined\n\n const labels = {\n up: () => t('gr.statistic.trendUp', 'Increase'),\n down: () => t('gr.statistic.trendDown', 'Decrease'),\n flat: () => t('gr.statistic.trendFlat', 'No change'),\n }\n\n return labels[props.trend]()\n})\n</script>\n\n<template>\n <component\n :is=\"rootTag\"\n data-gr-statistic\n :type=\"rootTag === 'button' ? 'button' : undefined\"\n :href=\"rootTag === 'a' ? href : undefined\"\n :class=\"rootClass\"\n @click=\"emit('click', $event)\"\n >\n <span\n v-if=\"icon || $slots.icon\"\n data-gr-statistic-icon\n class=\"mt-0.5 inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-[var(--gr-radius-lg)] bg-[var(--gr-muted)] text-[var(--gr-muted-fg)]\"\n >\n <slot name=\"icon\">\n <span class=\"block h-4 w-4\" :class=\"icon\" aria-hidden=\"true\" />\n </slot>\n </span>\n\n <div class=\"min-w-0\">\n <!--\n Подпись и значение — пара «термин — значение», а не два соседних блока.\n `<dl>` появляется только вместе с подписью: список определений без `<dt>`\n — та же бессвязность, только с претензией на семантику. Поля обнуляются\n руками: preflight пакета сбрасывает их лишь у `body`.\n -->\n <component :is=\"hasTitle ? 'dl' : 'div'\" class=\"m-0\">\n <dt\n v-if=\"hasTitle\"\n data-gr-statistic-title\n :class=\"[statisticTitleClass, statisticTitleSizeBySize[resolvedSize]]\"\n >\n <slot name=\"title\">\n {{ title }}\n </slot>\n </dt>\n\n <!-- Плейсхолдер загрузки повторяет высоту строки значения, чтобы блок не прыгал. -->\n <component\n :is=\"hasTitle ? 'dd' : 'div'\"\n v-if=\"loading\"\n data-gr-statistic-placeholder\n data-testid=\"gr-statistic-placeholder\"\n class=\"m-0 mt-1\"\n role=\"status\"\n aria-busy=\"true\"\n >\n <GrSkeleton\n width=\"6rem\"\n :height=\"statisticPlaceholderHeightBySize[resolvedSize]\"\n rounded=\"var(--gr-radius-md)\"\n />\n <span class=\"sr-only\">{{ t('gr.loading.defaultText', 'Loading...') }}</span>\n </component>\n\n <component\n :is=\"hasTitle ? 'dd' : 'div'\"\n v-else\n data-gr-statistic-value\n data-testid=\"gr-statistic-value\"\n class=\"m-0 mt-0.5 flex items-baseline gap-1\"\n >\n <span\n v-if=\"prefix || $slots.prefix\"\n data-gr-statistic-prefix\n class=\"text-[var(--gr-muted-fg)]\"\n :class=\"statisticAffixSizeBySize[resolvedSize]\"\n >\n <slot name=\"prefix\">{{ prefix }}</slot>\n </span>\n\n <span\n data-gr-statistic-number\n :class=\"statisticValueClass({ size: resolvedSize, tone })\"\n :aria-hidden=\"isCounting ? 'true' : undefined\"\n >\n <slot>{{ formatted }}</slot>\n </span>\n\n <!--\n Пока идёт перебор, на экране промежуточное число. Диктору отдаётся\n конечное: «1 284» глазами и «743» в ушах — не шум, а неверные данные.\n -->\n <span v-if=\"isCounting\" data-gr-statistic-final class=\"sr-only\">{{ finalFormatted }}</span>\n\n <span\n v-if=\"suffix || $slots.suffix\"\n data-gr-statistic-suffix\n class=\"text-[var(--gr-muted-fg)]\"\n :class=\"statisticAffixSizeBySize[resolvedSize]\"\n >\n <slot name=\"suffix\">{{ suffix }}</slot>\n </span>\n </component>\n </component>\n\n <div\n v-if=\"trend || trendText || $slots.trend\"\n data-gr-statistic-trend\n data-testid=\"gr-statistic-trend\"\n class=\"mt-1 inline-flex items-center gap-1\"\n :class=\"[statisticTrendSizeBySize[resolvedSize], statisticTrendClassByTrend[trend ?? 'flat']]\"\n >\n <span v-if=\"trendLabel\" data-gr-statistic-trend-label class=\"sr-only\">{{ trendLabel }}</span>\n\n <slot name=\"trend\">\n <span\n v-if=\"trend\"\n class=\"block h-3.5 w-3.5\"\n :class=\"statisticTrendIconByTrend[trend]\"\n aria-hidden=\"true\"\n />\n <span>{{ trendText }}</span>\n </slot>\n </div>\n </div>\n </component>\n</template>\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"GrTable-BB-mI_NM.js","names":[],"sources":["../../../../src/components/GrTable/GrTable.vue","../../../../src/components/GrTable/GrTable.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref, useSlots } from 'vue'\n\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport GrSkeleton from '../GrSkeleton/GrSkeleton.vue'\nimport { hasMeaningfulSlotContent } from '../shared/slotNodes'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport {\n emptyCellClass,\n hoverableClass,\n loadingRowCellClass,\n stripedClass,\n tableSizes,\n type GrTableSize,\n} from './grTableStyles'\n\nexport interface GrTableProps {\n /**\n * Размер таблицы — базовый кегль текста. Паддинги ячеек оставлены за\n * консьюмером (GrTable — «тонкий» контейнер).\n */\n size?: GrTableSize\n /**\n * Текст caption для screen reader'ов. Рендерится как `<caption class=\"sr-only\">`,\n * если не передан слот `#caption`.\n */\n caption?: string\n /** Прямой ARIA-label для `<table>`. Игнорируется, если задан `ariaLabelledby`. */\n ariaLabel?: string\n /** ID элемента-заголовка, связанного с `<table>` через `aria-labelledby`. */\n ariaLabelledby?: string\n /**\n * ARIA-label для скролл-контейнера. Включает `role=\"region\"`; сам скролл\n * достижим с клавиатуры всегда, независимо от метки.\n */\n regionLabel?: string\n /** Идёт загрузка: вместо строк — скелетоны, контейнер помечен `aria-busy`. */\n loading?: boolean\n /** Сколько строк-заглушек показать при `loading`. */\n loadingRows?: number\n /** Сколько колонок занимает служебная строка (пустое состояние и скелетоны). */\n columnCount?: number\n /** Текст пустого состояния. Слот `#empty` сильнее. */\n emptyText?: string\n /** Таблица пуста. По умолчанию определяется по содержимому слота. */\n empty?: boolean\n /** Чередование строк. */\n striped?: boolean\n /** Подсветка строки под курсором. */\n hoverable?: boolean\n /**\n * Прилипающий заголовок: `<thead>` остаётся видимым при вертикальном скролле.\n * Осмысленно вместе с `maxHeight` (иначе таблица не скроллится вертикально).\n */\n stickyHeader?: boolean\n /**\n * Полное число строк набора, включая строки заголовка (`aria-rowcount`).\n *\n * Нужен, когда в DOM не весь набор — например, при виртуализации: диктор\n * считает строки по разметке и объявил бы «5 из 20» на таблице в десять\n * тысяч. Строки при этом обязаны нести `aria-rowindex`.\n */\n rowCount?: number\n /**\n * Фиксированная раскладка (`table-layout: fixed`): ширины колонок берутся из\n * первой строки, а не из содержимого всех.\n *\n * Обязателен, если в DOM не весь набор: иначе ширины считаются по\n * отрисованному окну и прыгают на каждой прокрутке.\n *\n * Класс — arbitrary-значение, а не `table-fixed`: такого правила в\n * `presetMini` нет, и класс молча не превратился бы в CSS.\n */\n fixedLayout?: boolean\n /**\n * Максимальная высота скролл-контейнера (включает вертикальный скролл).\n * Число трактуется как пиксели. Нужен для работы `stickyHeader`.\n */\n maxHeight?: string | number\n}\n\n/**\n * `GrTable` — «тонкий» GR-контейнер для табличных данных.\n *\n * Рендерит `<table>` внутри скролл-обёртки и даёт слоты `#caption`, `#header`,\n * default (tbody) и `#footer`. Стили ячеек (`<th>`/`<td>` паддинги, выравнивание)\n * оставлены на консьюмере.\n *\n * ВАЖНО: `min-w-full` сам по себе не даст горизонтального скролла. Для него\n * консьюмеру нужны `white-space: nowrap` на ячейках или явные ширины колонок.\n */\nconst props = withDefaults(defineProps<GrTableProps>(), {\n size: undefined,\n caption: undefined,\n ariaLabel: undefined,\n ariaLabelledby: undefined,\n regionLabel: undefined,\n loading: false,\n loadingRows: 3,\n columnCount: 1,\n emptyText: undefined,\n empty: undefined,\n striped: false,\n hoverable: false,\n stickyHeader: false,\n maxHeight: undefined,\n rowCount: undefined,\n fixedLayout: false,\n})\n\nconst slots = useSlots()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrTable' })\nconst tableTextClass = computed(() => tableSizes[resolvedSize.value])\nconst hasCaption = computed(() => Boolean(slots.caption) || Boolean(props.caption))\n\nconst { t } = useGranularityTranslations()\nconst resolvedEmptyText = computed(() => props.emptyText ?? t('gr.table.empty', 'Nothing here yet'))\n\n// Пустоту видно по слоту: `v-for` по пустому массиву оставляет фрагмент без\n// узлов, `v-if` — комментарий, и ни то ни другое строкой не является.\nconst isEmpty = computed(() => {\n if (props.empty !== undefined)\n return props.empty\n\n return !hasMeaningfulSlotContent(slots.default?.() ?? [])\n})\n\nconst showRows = computed(() => !props.loading && !isEmpty.value)\nconst loadingRowCount = computed(() => Math.max(1, Math.trunc(props.loadingRows)))\nconst serviceColSpan = computed(() => Math.max(1, Math.trunc(props.columnCount)))\n\nconst tbodyClass = computed(() => [\n 'text-[var(--gr-fg)]',\n props.striped && showRows.value ? stripedClass : '',\n props.hoverable && showRows.value ? hoverableClass : '',\n].filter(Boolean).join(' '))\n\nconst scrollStyle = computed(() => {\n // Неполный набор в DOM (`rowCount`) означает виртуализацию, а с ней обязан\n // быть выключен браузерный scroll anchoring: он подправляет `scrollTop`,\n // когда меняется высота содержимого выше видимого узла, — а окно меняет её\n // на каждом кадре прокрутки, и список уезжает.\n const anchor = props.rowCount === undefined ? undefined : { overflowAnchor: 'none' as const }\n\n if (props.maxHeight === undefined)\n return anchor\n\n const value = typeof props.maxHeight === 'number' ? `${props.maxHeight}px` : props.maxHeight\n return { ...anchor, maxHeight: value, overflowY: 'auto' as const }\n})\n\n// Прилипающий заголовок: `<thead>` на `position: sticky`; фон обязателен, иначе\n// сквозь него будут просвечивать строки при скролле.\nconst theadClass = computed(() => [\n 'bg-[var(--gr-muted)] text-[var(--gr-muted-fg)]',\n props.stickyHeader ? 'sticky top-0 z-[1]' : '',\n].filter(Boolean).join(' '))\n\nconst scrollEl = ref<HTMLElement | null>(null)\n\nfunction scrollTo(options: ScrollToOptions): void {\n scrollEl.value?.scrollTo(options)\n}\n\n// Только верхний уровень собственного tbody: вложенная таблица в ячейке не\n// должна сдвигать индексацию строк.\nfunction contentRows(): HTMLElement[] {\n if (!showRows.value) return []\n const el = scrollEl.value\n if (!el) return []\n return Array.from(el.querySelectorAll<HTMLElement>(':scope > table > tbody > tr'))\n}\n\nfunction scrollToRow(index: number, options?: ScrollIntoViewOptions): boolean {\n const row = contentRows()[index]\n if (!row) return false\n row.scrollIntoView(options ?? { block: 'nearest' })\n return true\n}\n\ndefineExpose({\n /** Прокрутить скролл-контейнер таблицы — тот же контракт, что у `GrDataTable`. */\n scrollTo,\n /** Прокрутить к строке содержимого по её индексу в разметке. `false` — строки нет в DOM. */\n scrollToRow,\n})\n</script>\n\n<template>\n <div\n ref=\"scrollEl\"\n data-gr-table-scroll\n :role=\"regionLabel ? 'region' : undefined\"\n :aria-label=\"regionLabel\"\n :aria-busy=\"loading ? 'true' : undefined\"\n tabindex=\"0\"\n class=\"overflow-x-auto rounded-[var(--gr-radius-lg)] border border-[var(--gr-brd)] bg-[var(--gr-card)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)]\"\n :style=\"scrollStyle\"\n >\n <table\n data-gr-table\n class=\"min-w-full\" :class=\"[tableTextClass, fixedLayout ? '[table-layout:fixed]' : '']\"\n :aria-label=\"ariaLabelledby ? undefined : ariaLabel\"\n :aria-labelledby=\"ariaLabelledby\"\n :aria-rowcount=\"rowCount\"\n >\n <caption v-if=\"hasCaption\" class=\"sr-only\">\n <slot name=\"caption\">\n{{ caption }}\n</slot>\n </caption>\n <thead :class=\"theadClass\">\n <slot name=\"header\" />\n </thead>\n <tbody :class=\"tbodyClass\">\n <template v-if=\"loading\">\n <slot name=\"loading\">\n <tr v-for=\"row in loadingRowCount\" :key=\"row\" data-gr-table-loading-row>\n <td :colspan=\"serviceColSpan\" :class=\"loadingRowCellClass\">\n <GrSkeleton />\n </td>\n </tr>\n </slot>\n </template>\n\n <tr v-else-if=\"isEmpty\" data-gr-table-empty>\n <td :colspan=\"serviceColSpan\" :class=\"emptyCellClass\">\n <slot name=\"empty\">\n {{ resolvedEmptyText }}\n </slot>\n </td>\n </tr>\n\n <slot v-else />\n </tbody>\n <tfoot v-if=\"slots.footer\" class=\"text-[var(--gr-fg)]\">\n <slot name=\"footer\" />\n </tfoot>\n </table>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, ref, useSlots } from 'vue'\n\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport GrSkeleton from '../GrSkeleton/GrSkeleton.vue'\nimport { hasMeaningfulSlotContent } from '../shared/slotNodes'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport {\n emptyCellClass,\n hoverableClass,\n loadingRowCellClass,\n stripedClass,\n tableSizes,\n type GrTableSize,\n} from './grTableStyles'\n\nexport interface GrTableProps {\n /**\n * Размер таблицы — базовый кегль текста. Паддинги ячеек оставлены за\n * консьюмером (GrTable — «тонкий» контейнер).\n */\n size?: GrTableSize\n /**\n * Текст caption для screen reader'ов. Рендерится как `<caption class=\"sr-only\">`,\n * если не передан слот `#caption`.\n */\n caption?: string\n /** Прямой ARIA-label для `<table>`. Игнорируется, если задан `ariaLabelledby`. */\n ariaLabel?: string\n /** ID элемента-заголовка, связанного с `<table>` через `aria-labelledby`. */\n ariaLabelledby?: string\n /**\n * ARIA-label для скролл-контейнера. Включает `role=\"region\"`; сам скролл\n * достижим с клавиатуры всегда, независимо от метки.\n */\n regionLabel?: string\n /** Идёт загрузка: вместо строк — скелетоны, контейнер помечен `aria-busy`. */\n loading?: boolean\n /** Сколько строк-заглушек показать при `loading`. */\n loadingRows?: number\n /** Сколько колонок занимает служебная строка (пустое состояние и скелетоны). */\n columnCount?: number\n /** Текст пустого состояния. Слот `#empty` сильнее. */\n emptyText?: string\n /** Таблица пуста. По умолчанию определяется по содержимому слота. */\n empty?: boolean\n /** Чередование строк. */\n striped?: boolean\n /** Подсветка строки под курсором. */\n hoverable?: boolean\n /**\n * Прилипающий заголовок: `<thead>` остаётся видимым при вертикальном скролле.\n * Осмысленно вместе с `maxHeight` (иначе таблица не скроллится вертикально).\n */\n stickyHeader?: boolean\n /**\n * Полное число строк набора, включая строки заголовка (`aria-rowcount`).\n *\n * Нужен, когда в DOM не весь набор — например, при виртуализации: диктор\n * считает строки по разметке и объявил бы «5 из 20» на таблице в десять\n * тысяч. Строки при этом обязаны нести `aria-rowindex`.\n */\n rowCount?: number\n /**\n * Фиксированная раскладка (`table-layout: fixed`): ширины колонок берутся из\n * первой строки, а не из содержимого всех.\n *\n * Обязателен, если в DOM не весь набор: иначе ширины считаются по\n * отрисованному окну и прыгают на каждой прокрутке.\n *\n * Класс — arbitrary-значение, а не `table-fixed`: такого правила в\n * `presetMini` нет, и класс молча не превратился бы в CSS.\n */\n fixedLayout?: boolean\n /**\n * Максимальная высота скролл-контейнера (включает вертикальный скролл).\n * Число трактуется как пиксели. Нужен для работы `stickyHeader`.\n */\n maxHeight?: string | number\n}\n\n/**\n * `GrTable` — «тонкий» GR-контейнер для табличных данных.\n *\n * Рендерит `<table>` внутри скролл-обёртки и даёт слоты `#caption`, `#header`,\n * default (tbody) и `#footer`. Стили ячеек (`<th>`/`<td>` паддинги, выравнивание)\n * оставлены на консьюмере.\n *\n * ВАЖНО: `min-w-full` сам по себе не даст горизонтального скролла. Для него\n * консьюмеру нужны `white-space: nowrap` на ячейках или явные ширины колонок.\n */\nconst props = withDefaults(defineProps<GrTableProps>(), {\n size: undefined,\n caption: undefined,\n ariaLabel: undefined,\n ariaLabelledby: undefined,\n regionLabel: undefined,\n loading: false,\n loadingRows: 3,\n columnCount: 1,\n emptyText: undefined,\n empty: undefined,\n striped: false,\n hoverable: false,\n stickyHeader: false,\n maxHeight: undefined,\n rowCount: undefined,\n fixedLayout: false,\n})\n\nconst slots = useSlots()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrTable' })\nconst tableTextClass = computed(() => tableSizes[resolvedSize.value])\nconst hasCaption = computed(() => Boolean(slots.caption) || Boolean(props.caption))\n\nconst { t } = useGranularityTranslations()\nconst resolvedEmptyText = computed(() => props.emptyText ?? t('gr.table.empty', 'Nothing here yet'))\n\n// Пустоту видно по слоту: `v-for` по пустому массиву оставляет фрагмент без\n// узлов, `v-if` — комментарий, и ни то ни другое строкой не является.\nconst isEmpty = computed(() => {\n if (props.empty !== undefined)\n return props.empty\n\n return !hasMeaningfulSlotContent(slots.default?.() ?? [])\n})\n\nconst showRows = computed(() => !props.loading && !isEmpty.value)\nconst loadingRowCount = computed(() => Math.max(1, Math.trunc(props.loadingRows)))\nconst serviceColSpan = computed(() => Math.max(1, Math.trunc(props.columnCount)))\n\nconst tbodyClass = computed(() => [\n 'text-[var(--gr-fg)]',\n props.striped && showRows.value ? stripedClass : '',\n props.hoverable && showRows.value ? hoverableClass : '',\n].filter(Boolean).join(' '))\n\nconst scrollStyle = computed(() => {\n // Неполный набор в DOM (`rowCount`) означает виртуализацию, а с ней обязан\n // быть выключен браузерный scroll anchoring: он подправляет `scrollTop`,\n // когда меняется высота содержимого выше видимого узла, — а окно меняет её\n // на каждом кадре прокрутки, и список уезжает.\n const anchor = props.rowCount === undefined ? undefined : { overflowAnchor: 'none' as const }\n\n if (props.maxHeight === undefined)\n return anchor\n\n const value = typeof props.maxHeight === 'number' ? `${props.maxHeight}px` : props.maxHeight\n return { ...anchor, maxHeight: value, overflowY: 'auto' as const }\n})\n\n// Прилипающий заголовок: `<thead>` на `position: sticky`; фон обязателен, иначе\n// сквозь него будут просвечивать строки при скролле.\nconst theadClass = computed(() => [\n 'bg-[var(--gr-muted)] text-[var(--gr-muted-fg)]',\n props.stickyHeader ? 'sticky top-0 z-[1]' : '',\n].filter(Boolean).join(' '))\n\nconst scrollEl = ref<HTMLElement | null>(null)\n\nfunction scrollTo(options: ScrollToOptions): void {\n scrollEl.value?.scrollTo(options)\n}\n\n// Только верхний уровень собственного tbody: вложенная таблица в ячейке не\n// должна сдвигать индексацию строк.\nfunction contentRows(): HTMLElement[] {\n if (!showRows.value) return []\n const el = scrollEl.value\n if (!el) return []\n return Array.from(el.querySelectorAll<HTMLElement>(':scope > table > tbody > tr'))\n}\n\nfunction scrollToRow(index: number, options?: ScrollIntoViewOptions): boolean {\n const row = contentRows()[index]\n if (!row) return false\n row.scrollIntoView(options ?? { block: 'nearest' })\n return true\n}\n\ndefineExpose({\n /** Прокрутить скролл-контейнер таблицы — тот же контракт, что у `GrDataTable`. */\n scrollTo,\n /** Прокрутить к строке содержимого по её индексу в разметке. `false` — строки нет в DOM. */\n scrollToRow,\n})\n</script>\n\n<template>\n <div\n ref=\"scrollEl\"\n data-gr-table-scroll\n :role=\"regionLabel ? 'region' : undefined\"\n :aria-label=\"regionLabel\"\n :aria-busy=\"loading ? 'true' : undefined\"\n tabindex=\"0\"\n class=\"overflow-x-auto rounded-[var(--gr-radius-lg)] border border-[var(--gr-brd)] bg-[var(--gr-card)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)]\"\n :style=\"scrollStyle\"\n >\n <table\n data-gr-table\n class=\"min-w-full\" :class=\"[tableTextClass, fixedLayout ? '[table-layout:fixed]' : '']\"\n :aria-label=\"ariaLabelledby ? undefined : ariaLabel\"\n :aria-labelledby=\"ariaLabelledby\"\n :aria-rowcount=\"rowCount\"\n >\n <caption v-if=\"hasCaption\" class=\"sr-only\">\n <slot name=\"caption\">\n{{ caption }}\n</slot>\n </caption>\n <thead :class=\"theadClass\">\n <slot name=\"header\" />\n </thead>\n <tbody :class=\"tbodyClass\">\n <template v-if=\"loading\">\n <slot name=\"loading\">\n <tr v-for=\"row in loadingRowCount\" :key=\"row\" data-gr-table-loading-row>\n <td :colspan=\"serviceColSpan\" :class=\"loadingRowCellClass\">\n <GrSkeleton />\n </td>\n </tr>\n </slot>\n </template>\n\n <tr v-else-if=\"isEmpty\" data-gr-table-empty>\n <td :colspan=\"serviceColSpan\" :class=\"emptyCellClass\">\n <slot name=\"empty\">\n {{ resolvedEmptyText }}\n </slot>\n </td>\n </tr>\n\n <slot v-else />\n </tbody>\n <tfoot v-if=\"slots.footer\" class=\"text-[var(--gr-fg)]\">\n <slot name=\"footer\" />\n </tfoot>\n </table>\n </div>\n</template>\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"GrTabs-B8CDq2D9.js","names":[],"sources":["../../../../src/components/GrTabs/GrTabs.vue","../../../../src/components/GrTabs/GrTabs.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, ref, watch } from 'vue'\n\nimport IconX from '~icons/lucide/x'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\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 /** UnoCSS-класс иконки слева от подписи (например `i-lucide-user`). */\n icon?: string\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\n// Roving tabindex обязан всегда держать ровно один элемент с `0`. Если\n// `modelValue` не совпал ни с одной вкладкой (пустое начальное значение,\n// асинхронный список, удалённая активная вкладка), в таб-порядке остаётся\n// первая доступная — иначе tablist выпадает из него целиком и молча.\nconst rovingIndex = computed(() => {\n if (activeIndex.value >= 0) return activeIndex.value\n\n const firstEnabled = props.tabs.findIndex(tab => !tab.disabled)\n return firstEnabled >= 0 ? firstEnabled : 0\n})\n\nfunction isEnabled(tab: GrTab): boolean {\n return !tab.disabled\n}\n\nconst focusedIndex = ref(-1)\n\n/** Роверная вкладка: при `manual` фокус может уехать вперёд выбора. */\nconst rovingFocusIndex = computed(() => (focusedIndex.value >= 0 ? focusedIndex.value : rovingIndex.value))\n\nasync function focusIndex(index: number): Promise<void> {\n focusedIndex.value = index\n await nextTick()\n buttonRefs.value[index]?.focus()\n scrollIntoView(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 findNextEnabled(from: number, direction: 1 | -1): number {\n const length = props.tabs.length\n if (length === 0)\n return -1\n let index = from\n for (let i = 0; i < length; i++) {\n index = (index + direction + length) % length\n const tab = props.tabs[index]\n if (tab && isEnabled(tab))\n return index\n }\n return -1\n}\n\nfunction firstEnabled(): number {\n return props.tabs.findIndex(isEnabled)\n}\n\nfunction lastEnabled(): number {\n for (let i = props.tabs.length - 1; i >= 0; i--) {\n const tab = props.tabs[i]\n if (tab && isEnabled(tab))\n return i\n }\n return -1\n}\n\nfunction onKeydown(event: KeyboardEvent): void {\n if (props.tabs.length === 0)\n return\n\n const currentIndex = focusedIndex.value >= 0 ? focusedIndex.value : (activeIndex.value < 0 ? 0 : activeIndex.value)\n let nextIndex = -1\n\n const forward = props.orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight'\n const backward = props.orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft'\n\n switch (event.key) {\n case forward:\n nextIndex = findNextEnabled(currentIndex, 1)\n break\n case backward:\n nextIndex = findNextEnabled(currentIndex, -1)\n break\n case 'Home':\n nextIndex = firstEnabled()\n break\n case 'End':\n nextIndex = lastEnabled()\n break\n case 'Enter':\n case ' ':\n // В ручном режиме выбор подтверждается явно.\n if (props.activationMode === 'manual' && focusedIndex.value >= 0) {\n event.preventDefault()\n void selectByIndex(focusedIndex.value, true)\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 default:\n return\n }\n\n if (nextIndex < 0)\n return\n\n event.preventDefault()\n\n if (props.activationMode === 'manual') {\n void focusIndex(nextIndex)\n return\n }\n\n void selectByIndex(nextIndex, true)\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 focusedIndex.value = -1\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 focusedIndex.value = 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=\"index === rovingFocusIndex ? 0 : -1\"\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 <span v-if=\"tab.icon\" :class=\"[tabIconClass, tab.icon]\" aria-hidden=\"true\" />\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 } from 'vue'\n\nimport IconX from '~icons/lucide/x'\n\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\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 /** UnoCSS-класс иконки слева от подписи (например `i-lucide-user`). */\n icon?: string\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\n// Roving tabindex обязан всегда держать ровно один элемент с `0`. Если\n// `modelValue` не совпал ни с одной вкладкой (пустое начальное значение,\n// асинхронный список, удалённая активная вкладка), в таб-порядке остаётся\n// первая доступная — иначе tablist выпадает из него целиком и молча.\nconst rovingIndex = computed(() => {\n if (activeIndex.value >= 0) return activeIndex.value\n\n const firstEnabled = props.tabs.findIndex(tab => !tab.disabled)\n return firstEnabled >= 0 ? firstEnabled : 0\n})\n\nfunction isEnabled(tab: GrTab): boolean {\n return !tab.disabled\n}\n\nconst focusedIndex = ref(-1)\n\n/** Роверная вкладка: при `manual` фокус может уехать вперёд выбора. */\nconst rovingFocusIndex = computed(() => (focusedIndex.value >= 0 ? focusedIndex.value : rovingIndex.value))\n\nasync function focusIndex(index: number): Promise<void> {\n focusedIndex.value = index\n await nextTick()\n buttonRefs.value[index]?.focus()\n scrollIntoView(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 findNextEnabled(from: number, direction: 1 | -1): number {\n const length = props.tabs.length\n if (length === 0)\n return -1\n let index = from\n for (let i = 0; i < length; i++) {\n index = (index + direction + length) % length\n const tab = props.tabs[index]\n if (tab && isEnabled(tab))\n return index\n }\n return -1\n}\n\nfunction firstEnabled(): number {\n return props.tabs.findIndex(isEnabled)\n}\n\nfunction lastEnabled(): number {\n for (let i = props.tabs.length - 1; i >= 0; i--) {\n const tab = props.tabs[i]\n if (tab && isEnabled(tab))\n return i\n }\n return -1\n}\n\nfunction onKeydown(event: KeyboardEvent): void {\n if (props.tabs.length === 0)\n return\n\n const currentIndex = focusedIndex.value >= 0 ? focusedIndex.value : (activeIndex.value < 0 ? 0 : activeIndex.value)\n let nextIndex = -1\n\n const forward = props.orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight'\n const backward = props.orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft'\n\n switch (event.key) {\n case forward:\n nextIndex = findNextEnabled(currentIndex, 1)\n break\n case backward:\n nextIndex = findNextEnabled(currentIndex, -1)\n break\n case 'Home':\n nextIndex = firstEnabled()\n break\n case 'End':\n nextIndex = lastEnabled()\n break\n case 'Enter':\n case ' ':\n // В ручном режиме выбор подтверждается явно.\n if (props.activationMode === 'manual' && focusedIndex.value >= 0) {\n event.preventDefault()\n void selectByIndex(focusedIndex.value, true)\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 default:\n return\n }\n\n if (nextIndex < 0)\n return\n\n event.preventDefault()\n\n if (props.activationMode === 'manual') {\n void focusIndex(nextIndex)\n return\n }\n\n void selectByIndex(nextIndex, true)\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 focusedIndex.value = -1\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 focusedIndex.value = 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=\"index === rovingFocusIndex ? 0 : -1\"\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 <span v-if=\"tab.icon\" :class=\"[tabIconClass, tab.icon]\" aria-hidden=\"true\" />\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 +0,0 @@
1
- {"version":3,"file":"GrToaster-BKQy3Srm.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, ref, useSlots, watchEffect } from 'vue'\n\nimport { usePortalTarget } from '../../composables/usePortalTarget'\nimport type { Component } from 'vue'\n\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 { PLACEMENT_CLASS, type GrToasterPlacement } 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\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})\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)\nconst paused = computed(() => hovered.value || focusWithin.value)\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\nconst containerEl = ref<HTMLElement | null>(null)\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 :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 >\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, ref, useSlots, watchEffect } from 'vue'\n\nimport { usePortalTarget } from '../../composables/usePortalTarget'\nimport type { Component } from 'vue'\n\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 { PLACEMENT_CLASS, type GrToasterPlacement } 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\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})\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)\nconst paused = computed(() => hovered.value || focusWithin.value)\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\nconst containerEl = ref<HTMLElement | null>(null)\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 :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 >\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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"}