@feugene/granularity 0.31.0 → 0.32.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 (287) hide show
  1. package/README.md +0 -1
  2. package/dist/chunks/adapter-CAOxmn2G.js.map +1 -1
  3. package/dist/chunks/{config-BHcS5Cwm.js → config-87fszqnD.js} +1 -1
  4. package/dist/chunks/{config-BHcS5Cwm.js.map → config-87fszqnD.js.map} +1 -1
  5. package/dist/chunks/config-A-bBiotF.js.map +1 -1
  6. package/dist/chunks/config-B3J0alyY.js.map +1 -1
  7. package/dist/chunks/config-BhfNgyWL.js.map +1 -1
  8. package/dist/chunks/config-Bjn30smH.js.map +1 -1
  9. package/dist/chunks/config-BqT6PMzO.js.map +1 -1
  10. package/dist/chunks/config-C0Ea9ePP.js.map +1 -1
  11. package/dist/chunks/config-C0Eu8jpG.js.map +1 -1
  12. package/dist/chunks/config-CDat1wXf.js.map +1 -1
  13. package/dist/chunks/config-CQLNjc9X2.js.map +1 -1
  14. package/dist/chunks/config-CbEbLyDE.js.map +1 -1
  15. package/dist/chunks/{config-jyr6AIZt2.js → config-CuQmUkMw2.js} +2 -2
  16. package/dist/chunks/{config-jyr6AIZt2.js.map → config-CuQmUkMw2.js.map} +1 -1
  17. package/dist/chunks/config-DeyT9dJl.js.map +1 -1
  18. package/dist/chunks/config-DnF472Ox.js.map +1 -1
  19. package/dist/chunks/config-DwABJHEo.js.map +1 -1
  20. package/dist/chunks/{config-tiUtEn4Q.js → config-E1cTyRVV.js} +1 -1
  21. package/dist/chunks/{config-tiUtEn4Q.js.map → config-E1cTyRVV.js.map} +1 -1
  22. package/dist/chunks/config-MmPym-Z72.js.map +1 -1
  23. package/dist/chunks/config-T3-DMXYm.js.map +1 -1
  24. package/dist/chunks/config-Yy67uWHY.js.map +1 -1
  25. package/dist/chunks/config-_jbgsFpw.js.map +1 -1
  26. package/dist/chunks/config-g8-JGZ_Y.js.map +1 -1
  27. package/dist/chunks/config-mlheIMtV.js.map +1 -1
  28. package/dist/chunks/config-rLkc9Zn2.js.map +1 -1
  29. package/dist/chunks/config-uzUuosv7.js.map +1 -1
  30. package/dist/chunks/context-CTbuwcYs.js.map +1 -1
  31. package/dist/chunks/deltaTone-B5OoS1vo.js.map +1 -1
  32. package/dist/chunks/es-Do2CNrC2.js.map +1 -1
  33. package/dist/chunks/fileValidation-D5TFBhbv.js.map +1 -1
  34. package/dist/chunks/focusables-BkzK42SQ.js.map +1 -1
  35. package/dist/chunks/{generated-DJ2dJ__C.js → generated-CSyPsWtM.js} +1 -1
  36. package/dist/chunks/{generated-DJ2dJ__C.js.map → generated-CSyPsWtM.js.map} +1 -1
  37. package/dist/chunks/grBadgeStyles-D8Mz73B5.js.map +1 -1
  38. package/dist/chunks/{grButtonStyles-B_-YRE__.js → grButtonStyles-B3wRWW_i.js} +4 -4
  39. package/dist/chunks/grButtonStyles-B3wRWW_i.js.map +1 -0
  40. package/dist/chunks/grLinkStyles-FME_qMpi.js.map +1 -1
  41. package/dist/chunks/grLoadingStyles-CnQaf5ry.js.map +1 -1
  42. package/dist/chunks/{grRadioStyles-D4gnU65n.js → grRadioStyles-DGL2QLy6.js} +1 -1
  43. package/dist/chunks/{grRadioStyles-D4gnU65n.js.map → grRadioStyles-DGL2QLy6.js.map} +1 -1
  44. package/dist/chunks/grSelectStyles-Cdtd10eY.js.map +1 -1
  45. package/dist/chunks/{grSliderStyles-B5uNxNJq.js → grSliderStyles-kaz6VXZ3.js} +9 -1
  46. package/dist/chunks/grSliderStyles-kaz6VXZ3.js.map +1 -0
  47. package/dist/chunks/{granular-provider-yJNX1g1G.js → granular-provider-Da2CJO6j.js} +3 -3
  48. package/dist/chunks/{granular-provider-yJNX1g1G.js.map → granular-provider-Da2CJO6j.js.map} +1 -1
  49. package/dist/chunks/granularityI18n-BC5zsj_d.js.map +1 -1
  50. package/dist/chunks/icon-Dag2tIMs.js.map +1 -1
  51. package/dist/chunks/inert-DIHCCn_1.js.map +1 -1
  52. package/dist/chunks/keyboard-DE2yhz_b.js.map +1 -1
  53. package/dist/chunks/numericStep-Bq2lj4ew.js.map +1 -1
  54. package/dist/chunks/optionFilter-DYw9XPPx.js.map +1 -1
  55. package/dist/chunks/{overlayStack-C8cfsqXa.js → overlayStack-0CZS99Da.js} +40 -6
  56. package/dist/chunks/overlayStack-0CZS99Da.js.map +1 -0
  57. package/dist/chunks/portalRoot-CVhCgdG6.js.map +1 -1
  58. package/dist/chunks/renamedProp-BwcN-SJg.js.map +1 -1
  59. package/dist/chunks/{safelist-BOq5Li7p.js → safelist-DxkMj5IW.js} +1 -1
  60. package/dist/chunks/{safelist-BOq5Li7p.js.map → safelist-DxkMj5IW.js.map} +1 -1
  61. package/dist/chunks/slotNodes-DxNXpC7v.js.map +1 -1
  62. package/dist/chunks/useControlAddons-asUr10Pf.js.map +1 -1
  63. package/dist/chunks/useControlledOpen-DV3SqLTP.js.map +1 -1
  64. package/dist/chunks/useDragSort-0l47h4yb.js.map +1 -1
  65. package/dist/chunks/useFilePreviews-DnwgdyBs.js.map +1 -1
  66. package/dist/chunks/useFocusWithin-BwHE0rzm.js.map +1 -1
  67. package/dist/chunks/{useModalOverlay-Cv5hF63W.js → useModalOverlay-D__CqLGn.js} +3 -0
  68. package/dist/chunks/useModalOverlay-D__CqLGn.js.map +1 -0
  69. package/dist/chunks/usePortalTarget-C0p-mhaM.js.map +1 -1
  70. package/dist/chunks/validation-5HhpanPP.js.map +1 -1
  71. package/dist/components/GrAlert/chunks/GrAlert-FAVf-ksH.js.map +1 -1
  72. package/dist/components/GrAutocomplete/chunks/{GrAutocomplete-X6RlmiJQ.js → GrAutocomplete-rYEY15UI.js} +1 -1
  73. package/dist/components/GrAutocomplete/chunks/GrAutocomplete-rYEY15UI.js.map +1 -0
  74. package/dist/components/GrAutocomplete/index.js +1 -1
  75. package/dist/components/GrAutocomplete/styles.css +1 -1
  76. package/dist/components/GrAvatar/chunks/GrAvatar-CNmLmgcr.js.map +1 -1
  77. package/dist/components/GrBadge/chunks/{GrBadge-BsoocpF-.js → GrBadge-D2MfThPz.js} +1 -1
  78. package/dist/components/GrBadge/chunks/GrBadge-D2MfThPz.js.map +1 -0
  79. package/dist/components/GrBadge/index.js +1 -1
  80. package/dist/components/GrBadge/styles.css +1 -1
  81. package/dist/components/GrBadgeWrap/chunks/GrBadgeWrap-B-A3PRkE.js.map +1 -1
  82. package/dist/components/GrBottomNav/chunks/GrBottomNav-wtFatLjB.js.map +1 -1
  83. package/dist/components/GrBreadcrumbs/chunks/GrBreadcrumbs-DRDNan_n.js.map +1 -1
  84. package/dist/components/GrButton/chunks/{GrButton-Bw2AH4uX.js → GrButton-7GP0zObj.js} +1 -1
  85. package/dist/components/GrButton/chunks/GrButton-7GP0zObj.js.map +1 -0
  86. package/dist/components/GrButton/index.js +3 -3
  87. package/dist/components/GrButtonGroup/chunks/GrButtonGroup-CKyiAV8K.js.map +1 -1
  88. package/dist/components/GrCard/chunks/GrCard-BttXz1pQ.js.map +1 -1
  89. package/dist/components/GrCheckbox/chunks/GrCheckbox-CEcHIsXM.js.map +1 -1
  90. package/dist/components/GrCheckboxGroup/chunks/GrCheckboxGroup-BOwrVcFr.js.map +1 -1
  91. package/dist/components/GrChip/chunks/GrChip-C0vU69tf.js.map +1 -1
  92. package/dist/components/GrChipGroup/chunks/GrChipGroup-C78Vcwos.js.map +1 -1
  93. package/dist/components/GrCodeBlock/chunks/{GrCodeBlock-Dkvkr91t.js → GrCodeBlock-Dp0eEBTZ.js} +1 -1
  94. package/dist/components/GrCodeBlock/chunks/GrCodeBlock-Dp0eEBTZ.js.map +1 -0
  95. package/dist/components/GrCodeBlock/index.js +1 -1
  96. package/dist/components/GrCollapse/chunks/GrCollapse-BlW6ybdL.js.map +1 -1
  97. package/dist/components/GrColorPicker/chunks/{GrColorPicker-C7ifyQ0R.js → GrColorPicker-p2XH_8cE.js} +2 -2
  98. package/dist/components/GrColorPicker/chunks/GrColorPicker-p2XH_8cE.js.map +1 -0
  99. package/dist/components/GrColorPicker/index.js +1 -1
  100. package/dist/components/GrCommandPalette/chunks/{GrCommandPalette-C5abL2Y7.js → GrCommandPalette-BXbh3q7O.js} +2 -2
  101. package/dist/components/GrCommandPalette/chunks/GrCommandPalette-BXbh3q7O.js.map +1 -0
  102. package/dist/components/GrCommandPalette/index.js +1 -1
  103. package/dist/components/GrCommandPalette/styles.css +1 -1
  104. package/dist/components/GrConfigProvider/chunks/GrConfigProvider-BF7R3hgg.js.map +1 -1
  105. package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-B9mIU78E.js → GrConfirmDialog-CUP43q__.js} +3 -3
  106. package/dist/components/GrConfirmDialog/chunks/GrConfirmDialog-CUP43q__.js.map +1 -0
  107. package/dist/components/GrConfirmDialog/index.js +1 -1
  108. package/dist/components/GrContextMenu/chunks/{GrContextMenu-_FKwQvuQ.js → GrContextMenu-ClkVjdcP.js} +1 -1
  109. package/dist/components/GrContextMenu/chunks/GrContextMenu-ClkVjdcP.js.map +1 -0
  110. package/dist/components/GrContextMenu/index.js +1 -1
  111. package/dist/components/GrDataTable/chunks/{GrDataTable-C8a9_nCg.js → GrDataTable-CAGgRk27.js} +5 -3
  112. package/dist/components/GrDataTable/chunks/GrDataTable-CAGgRk27.js.map +1 -0
  113. package/dist/components/GrDataTable/index.js +1 -1
  114. package/dist/components/GrDelta/chunks/GrDelta-BZwkgQW-.js.map +1 -1
  115. package/dist/components/GrDescriptionList/chunks/GrDescriptionList-BMeCgFV3.js.map +1 -1
  116. package/dist/components/GrDialog/chunks/{GrDialog-bOUDYfmh.js → GrDialog-BiMJ9_T4.js} +2 -2
  117. package/dist/components/GrDialog/chunks/{GrDialog-bOUDYfmh.js.map → GrDialog-BiMJ9_T4.js.map} +1 -1
  118. package/dist/components/GrDialog/index.js +1 -1
  119. package/dist/components/GrDialogService/chunks/{GrDialogService-6s2cQ-_6.js → GrDialogService-BxNK6crD.js} +4 -4
  120. package/dist/components/GrDialogService/chunks/GrDialogService-BxNK6crD.js.map +1 -0
  121. package/dist/components/GrDialogService/index.js +1 -1
  122. package/dist/components/GrDivider/chunks/GrDivider-BXavGcMX.js.map +1 -1
  123. package/dist/components/GrDrawer/chunks/{GrDrawer-B7PiAcbU.js → GrDrawer-Ce2l3zaG.js} +6 -5
  124. package/dist/components/GrDrawer/chunks/GrDrawer-Ce2l3zaG.js.map +1 -0
  125. package/dist/components/GrDrawer/index.js +1 -1
  126. package/dist/components/GrDropdown/chunks/GrDropdown-13Wt9eB_.js.map +1 -1
  127. package/dist/components/GrDropdownMenu/chunks/GrDropdownMenu-GOzuAXsi.js.map +1 -1
  128. package/dist/components/GrDropdownMenu/chunks/GrDropdownMenuList-B33ZzwfB.js.map +1 -1
  129. package/dist/components/GrFilePreview/chunks/GrFilePreview-C9Z7hMK8.js.map +1 -1
  130. package/dist/components/GrFileUpload/chunks/GrFileUpload-NsHkRRIa.js.map +1 -1
  131. package/dist/components/GrForm/chunks/GrForm-CrnfCsgj.js.map +1 -1
  132. package/dist/components/GrFormField/chunks/GrFormField-DSylU7IU.js.map +1 -1
  133. package/dist/components/GrFormFile/chunks/{GrFormFile-CYtXU9HV.js → GrFormFile-B1jGZyIj.js} +2 -2
  134. package/dist/components/GrFormFile/chunks/GrFormFile-B1jGZyIj.js.map +1 -0
  135. package/dist/components/GrFormFile/index.js +1 -1
  136. package/dist/components/GrFormFile/styles.css +1 -1
  137. package/dist/components/GrIcon/chunks/GrIcon-Dmf0c9ba.js.map +1 -1
  138. package/dist/components/GrImageViewer/chunks/{GrImageViewer-CyVN6Zpc.js → GrImageViewer-CG4vUBg_.js} +4 -3
  139. package/dist/components/GrImageViewer/chunks/GrImageViewer-CG4vUBg_.js.map +1 -0
  140. package/dist/components/GrImageViewer/index.js +1 -1
  141. package/dist/components/GrInput/chunks/GrInput-BLywRZUN.js.map +1 -1
  142. package/dist/components/GrInputTag/chunks/GrInputTag-Cj4A9Xgs.js.map +1 -1
  143. package/dist/components/GrJsonViewer/chunks/{GrJsonViewer-Cy2bknHO.js → GrJsonViewer-BCZ7lE2k.js} +2 -2
  144. package/dist/components/GrJsonViewer/chunks/GrJsonViewer-BCZ7lE2k.js.map +1 -0
  145. package/dist/components/GrJsonViewer/index.js +1 -1
  146. package/dist/components/GrKbd/chunks/GrKbd-D2ga1H9i.js.map +1 -1
  147. package/dist/components/GrLink/chunks/GrLink-DYXw3W9w.js.map +1 -1
  148. package/dist/components/GrList/chunks/{GrList-DmT4qzLO.js → GrList-B_CRJB4c.js} +1 -1
  149. package/dist/components/GrList/chunks/GrList-B_CRJB4c.js.map +1 -0
  150. package/dist/components/GrList/index.js +1 -1
  151. package/dist/components/GrList/styles.css +1 -1
  152. package/dist/components/GrModal/chunks/{GrModal-CCoWZuIl.js → GrModal-D0mf1SAx.js} +3 -2
  153. package/dist/components/GrModal/chunks/GrModal-D0mf1SAx.js.map +1 -0
  154. package/dist/components/GrModal/index.js +1 -1
  155. package/dist/components/GrNavbar/chunks/{GrNavbar-C5EjMKaC.js → GrNavbar-_rtHKwTf.js} +1 -1
  156. package/dist/components/GrNavbar/chunks/{GrNavbar-C5EjMKaC.js.map → GrNavbar-_rtHKwTf.js.map} +1 -1
  157. package/dist/components/GrNavbar/index.js +1 -1
  158. package/dist/components/GrNumberInput/chunks/GrNumberInput-DQ59YICu.js.map +1 -1
  159. package/dist/components/GrPagination/chunks/{GrPagination-Cw3R21dp.js → GrPagination-B_kAhoAV.js} +2 -2
  160. package/dist/components/GrPagination/chunks/{GrPagination-Cw3R21dp.js.map → GrPagination-B_kAhoAV.js.map} +1 -1
  161. package/dist/components/GrPagination/index.js +1 -1
  162. package/dist/components/GrPopover/chunks/{GrPopover-CofG-Tfe.js → GrPopover-faJanmGs.js} +1 -1
  163. package/dist/components/GrPopover/chunks/GrPopover-faJanmGs.js.map +1 -0
  164. package/dist/components/GrPopover/index.js +1 -1
  165. package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-DLcl1xiq.js → GrPromptDialog-C6QGC7QJ.js} +4 -4
  166. package/dist/components/GrPromptDialog/chunks/GrPromptDialog-C6QGC7QJ.js.map +1 -0
  167. package/dist/components/GrPromptDialog/index.js +1 -1
  168. package/dist/components/GrRadio/chunks/{GrRadio-C_rESc5g.js → GrRadio-Aq3v8XgG.js} +1 -1
  169. package/dist/components/GrRadio/chunks/GrRadio-Aq3v8XgG.js.map +1 -0
  170. package/dist/components/GrRadio/index.js +2 -2
  171. package/dist/components/GrRadioGroup/chunks/{GrRadioGroup-CdQO4G-U.js → GrRadioGroup-y9LVdTWh.js} +1 -1
  172. package/dist/components/GrRadioGroup/chunks/GrRadioGroup-y9LVdTWh.js.map +1 -0
  173. package/dist/components/GrRadioGroup/index.js +1 -1
  174. package/dist/components/GrRating/chunks/GrRating-IEiTOdnF.js.map +1 -1
  175. package/dist/components/GrResponseErrorBanner/chunks/{GrResponseErrorBanner-C_skObtv.js → GrResponseErrorBanner-CBVRpgIG.js} +1 -1
  176. package/dist/components/GrResponseErrorBanner/chunks/{GrResponseErrorBanner-C_skObtv.js.map → GrResponseErrorBanner-CBVRpgIG.js.map} +1 -1
  177. package/dist/components/GrResponseErrorBanner/chunks/{GrResponseErrorBanner-DudGc9LO.js → GrResponseErrorBanner-DMeOWlzj.js} +1 -1
  178. package/dist/components/GrResponseErrorBanner/chunks/GrResponseErrorBanner-DMeOWlzj.js.map +1 -0
  179. package/dist/components/GrResponseErrorBanner/index.js +2 -2
  180. package/dist/components/GrSegmented/chunks/GrSegmented-CB-VR-yL.js.map +1 -1
  181. package/dist/components/GrSelect/chunks/{GrSelect-BiUrSSxc.js → GrSelect-gCeRFgQo.js} +1 -1
  182. package/dist/components/GrSelect/chunks/GrSelect-gCeRFgQo.js.map +1 -0
  183. package/dist/components/GrSelect/index.js +1 -1
  184. package/dist/components/GrSelect/styles.css +1 -1
  185. package/dist/components/GrSidebar/chunks/{GrSidebar-B8EQIlfW.js → GrSidebar-alkwBUzL.js} +1 -1
  186. package/dist/components/GrSidebar/chunks/GrSidebar-alkwBUzL.js.map +1 -0
  187. package/dist/components/GrSidebar/index.js +1 -1
  188. package/dist/components/GrSkeleton/chunks/GrSkeleton-CgfXxPQ2.js.map +1 -1
  189. package/dist/components/GrSlider/chunks/{GrSlider-DJfeEl4h.js → GrSlider-DTTrZTHK.js} +3 -3
  190. package/dist/components/GrSlider/chunks/GrSlider-DTTrZTHK.js.map +1 -0
  191. package/dist/components/GrSlider/index.js +2 -2
  192. package/dist/components/GrSortableList/chunks/GrSortableList-HyKX--rO.js.map +1 -1
  193. package/dist/components/GrSplitter/chunks/GrSplitter-MltjaADn.js.map +1 -1
  194. package/dist/components/GrStatistic/chunks/GrStatistic-qHVasK37.js.map +1 -1
  195. package/dist/components/GrSteps/chunks/GrSteps-C2jxpZoN.js.map +1 -1
  196. package/dist/components/GrSwitch/chunks/{GrSwitch-g_OOSkh7.js → GrSwitch-7Oe5QfQZ.js} +1 -1
  197. package/dist/components/GrSwitch/chunks/GrSwitch-7Oe5QfQZ.js.map +1 -0
  198. package/dist/components/GrSwitch/index.js +1 -1
  199. package/dist/components/GrTabPanels/chunks/GrTabPanels-BZQS8-JM.js.map +1 -1
  200. package/dist/components/GrTable/chunks/GrTable-Zcdz2St_.js.map +1 -1
  201. package/dist/components/GrTextarea/chunks/{GrTextarea-KREK75Bu.js → GrTextarea-DinXG12-.js} +10 -10
  202. package/dist/components/GrTextarea/chunks/GrTextarea-DinXG12-.js.map +1 -0
  203. package/dist/components/GrTextarea/index.js +1 -1
  204. package/dist/components/GrTimeline/chunks/GrTimeline-BiHpArsg.js.map +1 -1
  205. package/dist/components/GrToaster/chunks/{GrToaster-Dp7-4B8k.js → GrToaster-COXIBKzq.js} +2 -2
  206. package/dist/components/GrToaster/chunks/GrToaster-COXIBKzq.js.map +1 -0
  207. package/dist/components/GrToaster/index.js +1 -1
  208. package/dist/components/GrToaster/styles.css +1 -1
  209. package/dist/components/GrTooltip/chunks/GrTooltip-uYETdbCD.js.map +1 -1
  210. package/dist/components/GrTree/chunks/{GrTree-DC89I4AP.js → GrTree-Ci6CZfKR.js} +5 -5
  211. package/dist/components/GrTree/chunks/GrTree-Ci6CZfKR.js.map +1 -0
  212. package/dist/components/GrTree/index.js +1 -1
  213. package/dist/components/GrTree/styles.css +1 -1
  214. package/dist/components/GrTreeSelect/chunks/{GrTreeSelect-iJnMl2t9.js → GrTreeSelect-BHwgGJcT.js} +1 -1
  215. package/dist/components/GrTreeSelect/chunks/GrTreeSelect-BHwgGJcT.js.map +1 -0
  216. package/dist/components/GrTreeSelect/index.js +1 -1
  217. package/dist/composables/useAnnouncer.js.map +1 -1
  218. package/dist/composables/useComboboxNavigation.js.map +1 -1
  219. package/dist/composables/useDragGesture.js.map +1 -1
  220. package/dist/composables/useFloating.js +2 -22
  221. package/dist/composables/useFloating.js.map +1 -1
  222. package/dist/composables/useFocusTrap.js.map +1 -1
  223. package/dist/composables/useOverlayLayer.js +10 -3
  224. package/dist/composables/useOverlayLayer.js.map +1 -1
  225. package/dist/composables/useRovingFocus.js.map +1 -1
  226. package/dist/composables/useTheme.js.map +1 -1
  227. package/dist/composables/useToast.js.map +1 -1
  228. package/dist/composables/useVirtualList.js.map +1 -1
  229. package/dist/directives/autofocus.js.map +1 -1
  230. package/dist/directives/autosize.js.map +1 -1
  231. package/dist/directives/clickOutside.js.map +1 -1
  232. package/dist/directives/dropzone.js.map +1 -1
  233. package/dist/directives/hotkey.js.map +1 -1
  234. package/dist/directives/loading.js.map +1 -1
  235. package/dist/granular-provider-node.js +1 -1
  236. package/dist/granular-provider.js +1 -1
  237. package/dist/index.js +37 -37
  238. package/dist/testing.js +2 -2
  239. package/dist/testing.js.map +1 -1
  240. package/dist/theme-apply.js.map +1 -1
  241. package/dist/theme.js +1 -1
  242. package/dist/theme.js.map +1 -1
  243. package/dist/tokens.js +1 -1
  244. package/dist/types/components/GrCodeBlock/tokenizeJson.d.ts +1 -3
  245. package/dist/types/components/GrImageViewer/GrImageViewer.vue.d.ts +1 -1
  246. package/dist/types/components/GrInput/GrInput.vue.d.ts +24 -24
  247. package/dist/types/components/GrSlider/grSliderStyles.d.ts +9 -1
  248. package/dist/types/components/GrTabPanels/GrTabPanel.vue.d.ts +3 -3
  249. package/dist/types/composables/internal/overlayStack.d.ts +30 -0
  250. package/dist/types/composables/internal/useModalOverlay.d.ts +14 -0
  251. package/dist/types/composables/useOverlayLayer.d.ts +8 -0
  252. package/dist/vue/index.js.map +1 -1
  253. package/dist/web-types.json +1 -1
  254. package/package.json +3 -3
  255. package/dist/chunks/grButtonStyles-B_-YRE__.js.map +0 -1
  256. package/dist/chunks/grSliderStyles-B5uNxNJq.js.map +0 -1
  257. package/dist/chunks/overlayStack-C8cfsqXa.js.map +0 -1
  258. package/dist/chunks/useModalOverlay-Cv5hF63W.js.map +0 -1
  259. package/dist/components/GrAutocomplete/chunks/GrAutocomplete-X6RlmiJQ.js.map +0 -1
  260. package/dist/components/GrBadge/chunks/GrBadge-BsoocpF-.js.map +0 -1
  261. package/dist/components/GrButton/chunks/GrButton-Bw2AH4uX.js.map +0 -1
  262. package/dist/components/GrCodeBlock/chunks/GrCodeBlock-Dkvkr91t.js.map +0 -1
  263. package/dist/components/GrColorPicker/chunks/GrColorPicker-C7ifyQ0R.js.map +0 -1
  264. package/dist/components/GrCommandPalette/chunks/GrCommandPalette-C5abL2Y7.js.map +0 -1
  265. package/dist/components/GrConfirmDialog/chunks/GrConfirmDialog-B9mIU78E.js.map +0 -1
  266. package/dist/components/GrContextMenu/chunks/GrContextMenu-_FKwQvuQ.js.map +0 -1
  267. package/dist/components/GrDataTable/chunks/GrDataTable-C8a9_nCg.js.map +0 -1
  268. package/dist/components/GrDialogService/chunks/GrDialogService-6s2cQ-_6.js.map +0 -1
  269. package/dist/components/GrDrawer/chunks/GrDrawer-B7PiAcbU.js.map +0 -1
  270. package/dist/components/GrFormFile/chunks/GrFormFile-CYtXU9HV.js.map +0 -1
  271. package/dist/components/GrImageViewer/chunks/GrImageViewer-CyVN6Zpc.js.map +0 -1
  272. package/dist/components/GrJsonViewer/chunks/GrJsonViewer-Cy2bknHO.js.map +0 -1
  273. package/dist/components/GrList/chunks/GrList-DmT4qzLO.js.map +0 -1
  274. package/dist/components/GrModal/chunks/GrModal-CCoWZuIl.js.map +0 -1
  275. package/dist/components/GrPopover/chunks/GrPopover-CofG-Tfe.js.map +0 -1
  276. package/dist/components/GrPromptDialog/chunks/GrPromptDialog-DLcl1xiq.js.map +0 -1
  277. package/dist/components/GrRadio/chunks/GrRadio-C_rESc5g.js.map +0 -1
  278. package/dist/components/GrRadioGroup/chunks/GrRadioGroup-CdQO4G-U.js.map +0 -1
  279. package/dist/components/GrResponseErrorBanner/chunks/GrResponseErrorBanner-DudGc9LO.js.map +0 -1
  280. package/dist/components/GrSelect/chunks/GrSelect-BiUrSSxc.js.map +0 -1
  281. package/dist/components/GrSidebar/chunks/GrSidebar-B8EQIlfW.js.map +0 -1
  282. package/dist/components/GrSlider/chunks/GrSlider-DJfeEl4h.js.map +0 -1
  283. package/dist/components/GrSwitch/chunks/GrSwitch-g_OOSkh7.js.map +0 -1
  284. package/dist/components/GrTextarea/chunks/GrTextarea-KREK75Bu.js.map +0 -1
  285. package/dist/components/GrToaster/chunks/GrToaster-Dp7-4B8k.js.map +0 -1
  286. package/dist/components/GrTree/chunks/GrTree-DC89I4AP.js.map +0 -1
  287. package/dist/components/GrTreeSelect/chunks/GrTreeSelect-iJnMl2t9.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"useRovingFocus.js","names":[],"sources":["../../src/composables/useRovingFocus.ts"],"sourcesContent":["import type { ComputedRef, Ref } from 'vue'\nimport { computed, ref } from 'vue'\n\n/**\n * Roving tabindex: группа — одна остановка `Tab`, внутрь заходят стрелками.\n *\n * Механика написана в пакете шесть раз (`GrDropdownMenu`, `GrTree`, `GrTabs`,\n * `GrSegmented`, `GrRadio`, `GrInputTag`) и разошлась не стилистически, а по\n * существу: у одних кольцо зациклено, у других упирается в края; одни\n * пропускают выключенные элементы, другие намеренно нет; адресуются то\n * индексом, то ключом, то значением. Каждое расхождение здесь стало опцией —\n * поэтому опций много, и это дешевле, чем седьмая копия.\n *\n * Осознанно **не** обобщается активация: `Enter`/`Space` и «двигает ли стрелка\n * выбор» — доменная логика компонента. У `GrSegmented` и `GrRadio` стрелка\n * меняет значение, у `GrTree` и `GrInputTag` — нет, у `GrTabs` это проп.\n * Примитив ведёт фокус и отдаёт `onMove`; что делать с переездом, решает\n * компонент.\n *\n * Инвариант «ровно один элемент с `tabindex=\"0\"`» держит `rovingKey`: это\n * `computed`, а не `ref`, поэтому при смене набора он самокорректируется в\n * момент чтения. Отдельная нормализация с `watch(..., { flush: 'sync' })`,\n * как в `GrTree`, больше не нужна.\n */\n\n/** Какие стрелки ведут по набору. */\nexport type GrRovingOrientation = 'horizontal' | 'vertical' | 'both' | 'grid'\n\n/** Край набора, в который упёрлось движение. */\nexport type GrRovingEdge = 'start' | 'end'\n\nexport interface UseRovingFocusOptions<TKey> {\n /** Ключи элементов в порядке отображения. Индекс, значение, opaque-ключ — что угодно сравнимое по `===`. */\n items: () => readonly TKey[]\n /**\n * DOM-узел элемента. Массив ref-ов, `Map`, `querySelector` — примитиву\n * безразлично, знать раскладку обязан компонент.\n */\n elementFor: (key: TKey) => HTMLElement | null | undefined\n /** Выключен ли элемент. Спрашивается только при `skipDisabled`. */\n isDisabled?: (key: TKey) => boolean\n /** По умолчанию `vertical`. */\n orientation?: () => GrRovingOrientation\n /** Ширина строки для `orientation: 'grid'`. Игнорируется в остальных режимах. */\n columns?: () => number\n /** Замкнуто ли кольцо. По умолчанию `true`. */\n wrap?: () => boolean\n /**\n * Пропускать выключенные при движении. По умолчанию `false`: в паттерне\n * `menu` выключенный пункт остаётся в обходе намеренно — `aria-disabled`\n * оставляет его фокусируемым, чтобы скринридер прочёл, что он есть.\n */\n skipDisabled?: () => boolean\n /**\n * Стартовый элемент, когда фокус в группе ещё не был — обычно выбранное\n * значение.\n *\n * **Выключенность здесь не проверяется, и это не упущение.** Проверить её\n * автоматически по `isDisabled` нельзя: у `GrSegmented` загружающийся сегмент\n * остановку `Tab` сохраняет, хотя для движения заблокирован, а у `GrRadio`\n * выключенный переключатель терять её обязан. Условие знает только вызывающий,\n * поэтому геттер обязан вернуть `undefined` для значения, которое не может\n * принять фокус. Забыть об этом — значит оставить группу без остановки `Tab`\n * вовсе: элемент с `tabindex=\"0\"` будет назначен, но отрисует `-1`.\n */\n initialKey?: () => TKey | undefined\n /**\n * Позиция переехала — компонент решает, менять ли заодно выбор. Вызывается\n * **до** переноса фокуса: в `radiogroup` стрелка переносит выбор, и модель\n * обязана примениться раньше, чем сфокусируется целевой элемент.\n */\n onMove?: (key: TKey, previous: TKey | undefined) => void\n /**\n * Движение упёрлось в край при `wrap: false`. Вернул `true` — считаем клавишу\n * обработанной (кольцо `GrInputTag` так отдаёт фокус полю ввода, календарь —\n * листает месяц).\n */\n onOverflow?: (edge: GrRovingEdge, delta: number) => boolean\n /**\n * Донести элемент до экрана перед фокусом: виртуализация, `scrollIntoView`,\n * ожидание перерисовки после `onMove`. Вернул промис — фокус ждёт ровно его,\n * без добавленных примитивом тиков; вернул `undefined` — фокус остаётся\n * синхронным, чтобы не менять тайминг у компонентов, которые на него\n * полагаются.\n */\n beforeFocus?: (key: TKey) => void | Promise<void>\n}\n\nexport interface UseRovingFocusReturn<TKey> {\n /** Элемент, который держит `tabindex=\"0\"`. Никогда не «никто» при непустом наборе. */\n rovingKey: ComputedRef<TKey | undefined>\n /** `0` для роверного элемента, `-1` для всех остальных. */\n tabindexFor: (key: TKey) => 0 | -1\n /** Сделать активным без переноса фокуса — для синхронизации с мышью (`@focus`, `@click`). */\n setActive: (key: TKey) => void\n /**\n * Забыть явно наведённый фокус: остановка `Tab` возвращается к производной\n * (`initialKey` → первый доступный). Нужно, когда набор опустел, — иначе\n * устаревший ключ всплывёт, как только элементы появятся снова.\n */\n reset: () => void\n /** Сделать активным и перенести фокус. */\n focusKey: (key: TKey) => Promise<void>\n /** Сдвиг на `delta` позиций от текущего. Вернёт `false`, если движение не состоялось. */\n moveBy: (delta: number) => boolean\n /** На край набора (в `grid` — на край текущей строки). */\n moveToEdge: (edge: GrRovingEdge) => boolean\n /**\n * Стрелки, `Home`, `End` по объявленной ориентации. Возвращает `true`, если\n * клавиша обработана (с `preventDefault`) — остальное компонент разбирает сам.\n */\n handleNavigationKeys: (event: KeyboardEvent) => boolean\n}\n\nexport function useRovingFocus<TKey>(\n options: UseRovingFocusOptions<TKey>,\n): UseRovingFocusReturn<TKey> {\n const activeKey: Ref<TKey | undefined> = ref()\n\n const orientation = (): GrRovingOrientation => options.orientation?.() ?? 'vertical'\n const wraps = (): boolean => options.wrap?.() ?? true\n const skips = (): boolean => options.skipDisabled?.() ?? false\n const disabled = (key: TKey): boolean => options.isDisabled?.(key) ?? false\n\n function firstFocusable(items: readonly TKey[]): TKey | undefined {\n if (!skips()) return items[0]\n return items.find(key => !disabled(key)) ?? items[0]\n }\n\n /**\n * Роверный элемент. Порядок: явно активный (если ещё в наборе) → стартовый →\n * первый доступный. Именно здесь живёт инвариант «ровно один `tabindex=0`»:\n * пока набор непуст, ответ не бывает пустым.\n */\n const rovingKey = computed<TKey | undefined>(() => {\n const items = options.items()\n if (items.length === 0) return undefined\n\n const active = activeKey.value\n if (active !== undefined && items.includes(active)) return active\n\n const initial = options.initialKey?.()\n if (initial !== undefined && items.includes(initial)) return initial\n\n return firstFocusable(items)\n })\n\n function tabindexFor(key: TKey): 0 | -1 {\n return rovingKey.value === key ? 0 : -1\n }\n\n function setActive(key: TKey): void {\n activeKey.value = key\n }\n\n function reset(): void {\n activeKey.value = undefined\n }\n\n function focusKey(key: TKey): Promise<void> {\n const previous = rovingKey.value\n activeKey.value = key\n\n // Состояние — до фокуса: `onMove` часто меняет модель (в `radiogroup`\n // стрелка переносит выбор), и компонент вправе рассчитывать, что к моменту\n // фокуса оно уже применено.\n if (key !== previous) options.onMove?.(key, previous)\n\n const focusNow = (): void => {\n options.elementFor(key)?.focus()\n }\n\n const pending = options.beforeFocus?.(key)\n if (pending && typeof pending.then === 'function') {\n return pending.then(focusNow)\n }\n\n focusNow()\n return Promise.resolve()\n }\n\n /**\n * Индекс цели после сдвига. `undefined` — движение невозможно: упёрлись в\n * край без зацикливания либо весь набор выключен.\n */\n function targetIndex(from: number, delta: number): number | undefined {\n const items = options.items()\n const len = items.length\n if (len === 0 || delta === 0) return undefined\n\n let index = from\n\n // Шагов не больше размера набора: полный оборот без единого доступного\n // элемента означает, что двигаться некуда. Шаг на каждой итерации тот же —\n // выключенный элемент в сетке перешагивается по вертикали, сохраняя\n // колонку; прыжок «вбок» был бы неожиданным.\n for (let attempt = 0; attempt < len; attempt += 1) {\n const next = index + delta\n\n if (next < 0 || next >= len) {\n if (!wraps()) return undefined\n // Зацикливание по модулю набора. Колонку это сохраняет, когда длина\n // кратна ширине строки — у календарной сетки 42 = 6 × 7 так и есть.\n index = ((next % len) + len) % len\n }\n else {\n index = next\n }\n\n if (!skips() || !disabled(items[index])) return index\n }\n\n return undefined\n }\n\n function currentIndex(): number {\n const items = options.items()\n const key = rovingKey.value\n return key === undefined ? -1 : items.indexOf(key)\n }\n\n function moveBy(delta: number): boolean {\n const items = options.items()\n // `rovingKey` пуст только при пустом наборе — двигаться нечем.\n const from = currentIndex()\n if (from < 0) return false\n\n const target = targetIndex(from, delta)\n if (target === undefined) {\n const edge: GrRovingEdge = delta > 0 ? 'end' : 'start'\n return options.onOverflow?.(edge, delta) ?? false\n }\n\n void focusKey(items[target])\n return true\n }\n\n function rowBounds(index: number): [number, number] {\n const len = options.items().length\n const width = Math.max(1, options.columns?.() ?? len)\n const row = Math.floor(index / width)\n return [row * width, Math.min(row * width + width - 1, len - 1)]\n }\n\n function moveToEdge(edge: GrRovingEdge): boolean {\n const items = options.items()\n if (items.length === 0) return false\n\n const from = currentIndex()\n // В сетке `Home`/`End` — края строки (в календаре это начало и конец\n // недели); в линейных режимах — края всего набора.\n const [start, end] = orientation() === 'grid' && from >= 0\n ? rowBounds(from)\n : [0, items.length - 1]\n\n const target = edge === 'start' ? start : end\n if (!skips()) {\n void focusKey(items[target])\n return true\n }\n\n const direction = edge === 'start' ? 1 : -1\n for (let index = target; index >= start && index <= end; index += direction) {\n const key = items[index]\n if (!disabled(key)) {\n void focusKey(key)\n return true\n }\n }\n return false\n }\n\n /** Шаг вперёд/назад по объявленной ориентации; `0` — клавиша этой оси не касается. */\n function deltaForKey(key: string): number {\n const mode = orientation()\n const columns = Math.max(1, options.columns?.() ?? 1)\n\n switch (key) {\n case 'ArrowRight':\n return mode === 'vertical' ? 0 : 1\n case 'ArrowLeft':\n return mode === 'vertical' ? 0 : -1\n case 'ArrowDown':\n if (mode === 'horizontal') return 0\n return mode === 'grid' ? columns : 1\n case 'ArrowUp':\n if (mode === 'horizontal') return 0\n return mode === 'grid' ? -columns : -1\n default:\n return 0\n }\n }\n\n function handleNavigationKeys(event: KeyboardEvent): boolean {\n if (event.altKey || event.ctrlKey || event.metaKey) return false\n\n if (event.key === 'Home' || event.key === 'End') {\n const moved = moveToEdge(event.key === 'Home' ? 'start' : 'end')\n if (moved) event.preventDefault()\n return moved\n }\n\n const delta = deltaForKey(event.key)\n if (delta === 0) return false\n\n const moved = moveBy(delta)\n if (moved) event.preventDefault()\n return moved\n }\n\n return { rovingKey, tabindexFor, setActive, reset, focusKey, moveBy, moveToEdge, handleNavigationKeys }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"useRovingFocus.js","names":[],"sources":["../../src/composables/useRovingFocus.ts"],"sourcesContent":["import type { ComputedRef, Ref } from 'vue'\nimport { computed, ref } from 'vue'\n\n/**\n * Roving tabindex: группа — одна остановка `Tab`, внутрь заходят стрелками.\n *\n * Механика написана в пакете шесть раз (`GrDropdownMenu`, `GrTree`, `GrTabs`,\n * `GrSegmented`, `GrRadio`, `GrInputTag`) и разошлась не стилистически, а по\n * существу: у одних кольцо зациклено, у других упирается в края; одни\n * пропускают выключенные элементы, другие намеренно нет; адресуются то\n * индексом, то ключом, то значением. Каждое расхождение здесь стало опцией —\n * поэтому опций много, и это дешевле, чем седьмая копия.\n *\n * Осознанно **не** обобщается активация: `Enter`/`Space` и «двигает ли стрелка\n * выбор» — доменная логика компонента. У `GrSegmented` и `GrRadio` стрелка\n * меняет значение, у `GrTree` и `GrInputTag` — нет, у `GrTabs` это проп.\n * Примитив ведёт фокус и отдаёт `onMove`; что делать с переездом, решает\n * компонент.\n *\n * Инвариант «ровно один элемент с `tabindex=\"0\"`» держит `rovingKey`: это\n * `computed`, а не `ref`, поэтому при смене набора он самокорректируется в\n * момент чтения. Отдельная нормализация с `watch(..., { flush: 'sync' })`,\n * как в `GrTree`, больше не нужна.\n */\n\n/** Какие стрелки ведут по набору. */\nexport type GrRovingOrientation = 'horizontal' | 'vertical' | 'both' | 'grid'\n\n/** Край набора, в который упёрлось движение. */\nexport type GrRovingEdge = 'start' | 'end'\n\nexport interface UseRovingFocusOptions<TKey> {\n /** Ключи элементов в порядке отображения. Индекс, значение, opaque-ключ — что угодно сравнимое по `===`. */\n items: () => readonly TKey[]\n /**\n * DOM-узел элемента. Массив ref-ов, `Map`, `querySelector` — примитиву\n * безразлично, знать раскладку обязан компонент.\n */\n elementFor: (key: TKey) => HTMLElement | null | undefined\n /** Выключен ли элемент. Спрашивается только при `skipDisabled`. */\n isDisabled?: (key: TKey) => boolean\n /** По умолчанию `vertical`. */\n orientation?: () => GrRovingOrientation\n /** Ширина строки для `orientation: 'grid'`. Игнорируется в остальных режимах. */\n columns?: () => number\n /** Замкнуто ли кольцо. По умолчанию `true`. */\n wrap?: () => boolean\n /**\n * Пропускать выключенные при движении. По умолчанию `false`: в паттерне\n * `menu` выключенный пункт остаётся в обходе намеренно — `aria-disabled`\n * оставляет его фокусируемым, чтобы скринридер прочёл, что он есть.\n */\n skipDisabled?: () => boolean\n /**\n * Стартовый элемент, когда фокус в группе ещё не был — обычно выбранное\n * значение.\n *\n * **Выключенность здесь не проверяется, и это не упущение.** Проверить её\n * автоматически по `isDisabled` нельзя: у `GrSegmented` загружающийся сегмент\n * остановку `Tab` сохраняет, хотя для движения заблокирован, а у `GrRadio`\n * выключенный переключатель терять её обязан. Условие знает только вызывающий,\n * поэтому геттер обязан вернуть `undefined` для значения, которое не может\n * принять фокус. Забыть об этом — значит оставить группу без остановки `Tab`\n * вовсе: элемент с `tabindex=\"0\"` будет назначен, но отрисует `-1`.\n */\n initialKey?: () => TKey | undefined\n /**\n * Позиция переехала — компонент решает, менять ли заодно выбор. Вызывается\n * **до** переноса фокуса: в `radiogroup` стрелка переносит выбор, и модель\n * обязана примениться раньше, чем сфокусируется целевой элемент.\n */\n onMove?: (key: TKey, previous: TKey | undefined) => void\n /**\n * Движение упёрлось в край при `wrap: false`. Вернул `true` — считаем клавишу\n * обработанной (кольцо `GrInputTag` так отдаёт фокус полю ввода, календарь —\n * листает месяц).\n */\n onOverflow?: (edge: GrRovingEdge, delta: number) => boolean\n /**\n * Донести элемент до экрана перед фокусом: виртуализация, `scrollIntoView`,\n * ожидание перерисовки после `onMove`. Вернул промис — фокус ждёт ровно его,\n * без добавленных примитивом тиков; вернул `undefined` — фокус остаётся\n * синхронным, чтобы не менять тайминг у компонентов, которые на него\n * полагаются.\n */\n beforeFocus?: (key: TKey) => void | Promise<void>\n}\n\nexport interface UseRovingFocusReturn<TKey> {\n /** Элемент, который держит `tabindex=\"0\"`. Никогда не «никто» при непустом наборе. */\n rovingKey: ComputedRef<TKey | undefined>\n /** `0` для роверного элемента, `-1` для всех остальных. */\n tabindexFor: (key: TKey) => 0 | -1\n /** Сделать активным без переноса фокуса — для синхронизации с мышью (`@focus`, `@click`). */\n setActive: (key: TKey) => void\n /**\n * Забыть явно наведённый фокус: остановка `Tab` возвращается к производной\n * (`initialKey` → первый доступный). Нужно, когда набор опустел, — иначе\n * устаревший ключ всплывёт, как только элементы появятся снова.\n */\n reset: () => void\n /** Сделать активным и перенести фокус. */\n focusKey: (key: TKey) => Promise<void>\n /** Сдвиг на `delta` позиций от текущего. Вернёт `false`, если движение не состоялось. */\n moveBy: (delta: number) => boolean\n /** На край набора (в `grid` — на край текущей строки). */\n moveToEdge: (edge: GrRovingEdge) => boolean\n /**\n * Стрелки, `Home`, `End` по объявленной ориентации. Возвращает `true`, если\n * клавиша обработана (с `preventDefault`) — остальное компонент разбирает сам.\n */\n handleNavigationKeys: (event: KeyboardEvent) => boolean\n}\n\nexport function useRovingFocus<TKey>(\n options: UseRovingFocusOptions<TKey>,\n): UseRovingFocusReturn<TKey> {\n const activeKey: Ref<TKey | undefined> = ref()\n\n const orientation = (): GrRovingOrientation => options.orientation?.() ?? 'vertical'\n const wraps = (): boolean => options.wrap?.() ?? true\n const skips = (): boolean => options.skipDisabled?.() ?? false\n const disabled = (key: TKey): boolean => options.isDisabled?.(key) ?? false\n\n function firstFocusable(items: readonly TKey[]): TKey | undefined {\n if (!skips())\n return items[0]\n return items.find(key => !disabled(key)) ?? items[0]\n }\n\n /**\n * Роверный элемент. Порядок: явно активный (если ещё в наборе) → стартовый →\n * первый доступный. Именно здесь живёт инвариант «ровно один `tabindex=0`»:\n * пока набор непуст, ответ не бывает пустым.\n */\n const rovingKey = computed<TKey | undefined>(() => {\n const items = options.items()\n if (items.length === 0)\n return undefined\n\n const active = activeKey.value\n if (active !== undefined && items.includes(active))\n return active\n\n const initial = options.initialKey?.()\n if (initial !== undefined && items.includes(initial))\n return initial\n\n return firstFocusable(items)\n })\n\n function tabindexFor(key: TKey): 0 | -1 {\n return rovingKey.value === key ? 0 : -1\n }\n\n function setActive(key: TKey): void {\n activeKey.value = key\n }\n\n function reset(): void {\n activeKey.value = undefined\n }\n\n function focusKey(key: TKey): Promise<void> {\n const previous = rovingKey.value\n activeKey.value = key\n\n // Состояние — до фокуса: `onMove` часто меняет модель (в `radiogroup`\n // стрелка переносит выбор), и компонент вправе рассчитывать, что к моменту\n // фокуса оно уже применено.\n if (key !== previous)\n options.onMove?.(key, previous)\n\n const focusNow = (): void => {\n options.elementFor(key)?.focus()\n }\n\n const pending = options.beforeFocus?.(key)\n if (pending && typeof pending.then === 'function') {\n return pending.then(focusNow)\n }\n\n focusNow()\n return Promise.resolve()\n }\n\n /**\n * Индекс цели после сдвига. `undefined` — движение невозможно: упёрлись в\n * край без зацикливания либо весь набор выключен.\n */\n function targetIndex(from: number, delta: number): number | undefined {\n const items = options.items()\n const len = items.length\n if (len === 0 || delta === 0)\n return undefined\n\n let index = from\n\n // Шагов не больше размера набора: полный оборот без единого доступного\n // элемента означает, что двигаться некуда. Шаг на каждой итерации тот же —\n // выключенный элемент в сетке перешагивается по вертикали, сохраняя\n // колонку; прыжок «вбок» был бы неожиданным.\n for (let attempt = 0; attempt < len; attempt += 1) {\n const next = index + delta\n\n if (next < 0 || next >= len) {\n if (!wraps())\n return undefined\n // Зацикливание по модулю набора. Колонку это сохраняет, когда длина\n // кратна ширине строки — у календарной сетки 42 = 6 × 7 так и есть.\n index = ((next % len) + len) % len\n }\n else {\n index = next\n }\n\n if (!skips() || !disabled(items[index]))\n return index\n }\n\n return undefined\n }\n\n function currentIndex(): number {\n const items = options.items()\n const key = rovingKey.value\n return key === undefined ? -1 : items.indexOf(key)\n }\n\n function moveBy(delta: number): boolean {\n const items = options.items()\n // `rovingKey` пуст только при пустом наборе — двигаться нечем.\n const from = currentIndex()\n if (from < 0)\n return false\n\n const target = targetIndex(from, delta)\n if (target === undefined) {\n const edge: GrRovingEdge = delta > 0 ? 'end' : 'start'\n return options.onOverflow?.(edge, delta) ?? false\n }\n\n void focusKey(items[target])\n return true\n }\n\n function rowBounds(index: number): [number, number] {\n const len = options.items().length\n const width = Math.max(1, options.columns?.() ?? len)\n const row = Math.floor(index / width)\n return [row * width, Math.min(row * width + width - 1, len - 1)]\n }\n\n function moveToEdge(edge: GrRovingEdge): boolean {\n const items = options.items()\n if (items.length === 0)\n return false\n\n const from = currentIndex()\n // В сетке `Home`/`End` — края строки (в календаре это начало и конец\n // недели); в линейных режимах — края всего набора.\n const [start, end] = orientation() === 'grid' && from >= 0\n ? rowBounds(from)\n : [0, items.length - 1]\n\n const target = edge === 'start' ? start : end\n if (!skips()) {\n void focusKey(items[target])\n return true\n }\n\n const direction = edge === 'start' ? 1 : -1\n for (let index = target; index >= start && index <= end; index += direction) {\n const key = items[index]\n if (!disabled(key)) {\n void focusKey(key)\n return true\n }\n }\n return false\n }\n\n /** Шаг вперёд/назад по объявленной ориентации; `0` — клавиша этой оси не касается. */\n function deltaForKey(key: string): number {\n const mode = orientation()\n const columns = Math.max(1, options.columns?.() ?? 1)\n\n switch (key) {\n case 'ArrowRight':\n return mode === 'vertical' ? 0 : 1\n case 'ArrowLeft':\n return mode === 'vertical' ? 0 : -1\n case 'ArrowDown':\n if (mode === 'horizontal')\n return 0\n return mode === 'grid' ? columns : 1\n case 'ArrowUp':\n if (mode === 'horizontal')\n return 0\n return mode === 'grid' ? -columns : -1\n default:\n return 0\n }\n }\n\n function handleNavigationKeys(event: KeyboardEvent): boolean {\n if (event.altKey || event.ctrlKey || event.metaKey)\n return false\n\n if (event.key === 'Home' || event.key === 'End') {\n const moved = moveToEdge(event.key === 'Home' ? 'start' : 'end')\n if (moved)\n event.preventDefault()\n return moved\n }\n\n const delta = deltaForKey(event.key)\n if (delta === 0)\n return false\n\n const moved = moveBy(delta)\n if (moved)\n event.preventDefault()\n return moved\n }\n\n return { rovingKey, tabindexFor, setActive, reset, focusKey, moveBy, moveToEdge, handleNavigationKeys }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"useTheme.js","names":[],"sources":["../../src/composables/useTheme.ts"],"sourcesContent":["import { computed, hasInjectionContext, inject, ref } from 'vue'\nimport type { App, InjectionKey, Ref } from 'vue'\n\nexport type ThemeName = 'light' | 'dark'\n\nexport type UseThemeOptions = {\n /**\n * Ключ для хранения выбранной темы в localStorage.\n * По умолчанию: `gr-theme`.\n */\n storageKey?: string\n\n /**\n * Если `false` — не читать/не писать localStorage (полезно для SSR/embedded сценариев).\n * По умолчанию: `true`.\n */\n persist?: boolean\n\n /**\n * Куда писать `data-theme`. По умолчанию — `<html>` (канон одного приложения).\n *\n * Геттер, а не элемент: на момент `app.use(...)` корень приложения ещё не\n * смонтирован. Обязателен нескольким приложениям с независимыми темами:\n * селектор тем атрибутный (`[data-theme='dark']`), поэтому тема работает от\n * любого контейнера, а без `target` все приложения писали бы в один `<html>`\n * и побеждал бы последний.\n */\n target?: () => HTMLElement | null\n}\n\nconst DEFAULT_STORAGE_KEY = 'gr-theme'\nconst IS_SERVER = typeof window === 'undefined'\n\nfunction readStoredTheme(storageKey: string, persist: boolean): ThemeName | null {\n if (typeof window === 'undefined' || !persist) return null\n\n // Доступ к `localStorage` может бросать `SecurityError` в Safari private mode\n // и при отключённых cookies/storage — поэтому оборачиваем в try/catch.\n try {\n const storage = window.localStorage\n if (typeof storage?.getItem !== 'function') return null\n\n const stored = storage.getItem(storageKey)\n if (stored === 'light' || stored === 'dark') return stored\n }\n catch {\n // ignore\n }\n\n return null\n}\n\nfunction getSystemTheme(): ThemeName {\n if (typeof window === 'undefined') return 'light'\n return window.matchMedia?.('(prefers-color-scheme: dark)')?.matches ? 'dark' : 'light'\n}\n\nfunction getPreferredTheme(storageKey = DEFAULT_STORAGE_KEY, persist = true): ThemeName {\n return readStoredTheme(storageKey, persist) ?? getSystemTheme()\n}\n\nfunction applyTheme(theme: ThemeName, target?: () => HTMLElement | null) {\n if (typeof document === 'undefined') return\n\n // Канон GR — атрибут `[data-theme]`: на `<html>` (см. docs/styling.md →\n // «Темизация») либо на корне приложения, когда состояние задало `target`.\n // `useTheme`/`initThemeEarly` — единственный рантайм-API переключения темы.\n const root = target?.() ?? document.documentElement\n root.dataset.theme = theme\n}\n\n/**\n * Состояние темы одного приложения.\n *\n * `theme` общий для всех вызовов `useTheme()` внутри приложения: свой `ref` на\n * вызов означал бы, что переключение темы в одном компоненте не трогает\n * `theme`/`isDark` в другом — менялся бы только DOM-атрибут, а реактивные\n * потребители разъехались бы.\n */\ntype ThemeState = {\n theme: Ref<ThemeName>\n storageKey: string\n persist: boolean\n /** Корень, получающий `data-theme`; не задан — `<html>`. */\n target?: () => HTMLElement | null\n listenersBound: boolean\n /**\n * Снимает window-слушатели состояния. Появляется после `bindListeners`;\n * зовёт `app.onUnmount` у app-scoped состояний — модульный синглтон живёт со\n * страницей, и снимать его незачем.\n */\n teardownListeners?: () => void\n /** Разрешена ли мутация — см. `resolveThemeState`. */\n writable: boolean\n}\n\nfunction createThemeState(options: UseThemeOptions = {}, writable = true): ThemeState {\n const storageKey = options.storageKey ?? DEFAULT_STORAGE_KEY\n const persist = options.persist ?? true\n\n return {\n theme: ref<ThemeName>(getPreferredTheme(storageKey, persist)),\n storageKey,\n persist,\n target: options.target,\n listenersBound: false,\n writable,\n }\n}\n\n/** Ключ provide/inject для app-scoped состояния темы (устанавливает плагин ниже). */\nexport const GRANULARITY_THEME_STATE: InjectionKey<ThemeState> = Symbol.for('@feugene/granularity/theme-state')\n\n/**\n * Vue-плагин: даёт каждому приложению собственное состояние темы через\n * `app.provide`. Обязателен для (а) SSR, если приложение переключает тему на\n * сервере — иначе тема одного запроса уедет в ответ следующему, — и (б)\n * нескольких Vue-приложений на одной странице, которым нужны независимые темы.\n *\n * Для (б) обязателен и `target` — корень, получающий `data-theme`: без него\n * изолировано только состояние, а атрибут все приложения пишут в один `<html>`,\n * и побеждает последний.\n *\n * ```ts\n * app.use(granularityThemePlugin)\n * app.use(granularityThemePlugin, {\n * storageKey: 'admin-theme',\n * persist: false,\n * target: () => document.getElementById('admin-app'),\n * })\n * ```\n */\n// Где-то на странице установлен плагин: фолбэк в синглтон для такого приложения —\n// почти наверняка ошибка вызова, а не осознанный выбор.\nlet appScopedThemeProvided = false\nlet contextFallbackWarned = false\n\nexport const granularityThemePlugin = {\n install(app: App, options: UseThemeOptions = {}) {\n appScopedThemeProvided = true\n const state = createThemeState(options)\n app.provide(GRANULARITY_THEME_STATE, state)\n // Слушатели `storage`/`prefers-color-scheme` держат состояние; без снятия\n // каждый цикл mount/unmount микрофронтенда копил бы их на window навсегда.\n app.onUnmount(() => state.teardownListeners?.())\n },\n}\n\n// Ленивый модульный синглтон — канонический фолбэк для простых SPA без плагина.\nlet moduleThemeState: ThemeState | null = null\n\n/** Сброс dev-предупреждения о фолбэке. Внутреннее API — нужно тестам. */\nexport function resetThemeContextWarning(): void {\n contextFallbackWarned = false\n}\n\nfunction resolveThemeState(options: UseThemeOptions): ThemeState {\n // App-scoped состояние (provide через плагин): setup-контекст ИЛИ\n // `app.runWithContext(() => useTheme())` — `hasInjectionContext` покрывает оба.\n if (hasInjectionContext()) {\n const provided = inject(GRANULARITY_THEME_STATE, null)\n if (provided) return provided\n }\n\n // На сервере без плагина модульное состояние текло бы между запросами. Но\n // читать тему на сервере — нормальный сценарий (шаблон смотрит `isDark`), и\n // ронять на нём весь рендер нельзя: отдаём состояние на один вызов и запрещаем\n // мутацию — течёт именно она.\n if (IS_SERVER) return createThemeState(options, false)\n\n // Дедуп и текст — прямо под гардом: `__GR_DEV__` разворачивается в условие\n // на сборке, и у потребителя вся ветка выкидывается вместе с сообщением.\n if (\n appScopedThemeProvided && !contextFallbackWarned\n && __GR_DEV__\n ) {\n contextFallbackWarned = true\n console.warn(\n '[granularity] useTheme() вызван вне setup/inject-контекста: app-scoped состояние '\n + 'granularityThemePlugin недоступно, использован модульный синглтон. '\n + 'Оберните вызов: app.runWithContext(() => useTheme()).',\n )\n }\n\n if (!moduleThemeState)\n moduleThemeState = createThemeState(options)\n\n return moduleThemeState\n}\n\n// Подписки на системную тему и синхронизацию между вкладками. Привязаны к\n// состоянию, а не к модулю: у каждого приложения своё. Одна подписка на\n// состояние, а не на потребителя, — `onScopeDispose` здесь был бы вреден, он\n// порвал бы общую подписку при размонтировании одного из них.\nfunction bindListeners(state: ThemeState) {\n if (state.listenersBound || typeof window === 'undefined') return\n state.listenersBound = true\n\n // Cross-tab: другая вкладка записала тему в localStorage.\n const onStorage = (event: StorageEvent): void => {\n if (!state.persist || event.key !== state.storageKey) return\n if (event.newValue === 'light' || event.newValue === 'dark') {\n state.theme.value = event.newValue\n applyTheme(event.newValue, state.target)\n }\n }\n window.addEventListener?.('storage', onStorage)\n\n // Системная смена `prefers-color-scheme` — следуем ей только если пользователь\n // НЕ выбрал тему явно (в storage ничего не сохранено).\n const mql = window.matchMedia?.('(prefers-color-scheme: dark)')\n const onSchemeChange = (event: MediaQueryListEvent): void => {\n if (readStoredTheme(state.storageKey, state.persist) !== null) return\n const next: ThemeName = event.matches ? 'dark' : 'light'\n state.theme.value = next\n applyTheme(next, state.target)\n }\n mql?.addEventListener?.('change', onSchemeChange)\n\n state.teardownListeners = () => {\n window.removeEventListener?.('storage', onStorage)\n mql?.removeEventListener?.('change', onSchemeChange)\n state.listenersBound = false\n state.teardownListeners = undefined\n }\n}\n\n/**\n * Для применения темы максимально рано (до монтирования Vue), чтобы избежать \"мигания\".\n * Также сидирует модульное состояние, чтобы значение в `useTheme()` совпадало с уже\n * применённым к документу.\n *\n * Функция клиентская: её зовут из браузерной точки входа до создания приложения,\n * то есть app-scoped состояние инжектить там ещё неоткуда. Расхождения с плагином\n * это не даёт — оба сидируются из одного `localStorage`.\n */\nexport function initThemeEarly(options: UseThemeOptions = {}) {\n const storageKey = options.storageKey ?? DEFAULT_STORAGE_KEY\n const persist = options.persist ?? true\n const preferred = getPreferredTheme(storageKey, persist)\n\n moduleThemeState ??= createThemeState(options)\n moduleThemeState.storageKey = storageKey\n moduleThemeState.persist = persist\n moduleThemeState.theme.value = preferred\n\n applyTheme(preferred)\n}\n\nexport function useTheme(options: UseThemeOptions = {}) {\n const state = resolveThemeState(options)\n\n // Опции вызова уточняют состояние — тот же приём, что был у модульного\n // синглтона: слушателям нужно знать, какой ключ отслеживать.\n if (options.storageKey !== undefined) state.storageKey = options.storageKey\n if (options.persist !== undefined) state.persist = options.persist\n if (options.target !== undefined) state.target = options.target\n\n bindListeners(state)\n\n const theme = state.theme\n const isDark = computed(() => theme.value === 'dark')\n\n function setTheme(next: ThemeName) {\n if (!state.writable) {\n throw new Error(\n '[granularity] useTheme: setTheme/toggleTheme require `app.use(granularityThemePlugin)` during SSR — '\n + 'the module-singleton fallback is disabled server-side, so a theme set by one request would leak into '\n + 'the next response. Reading `theme`/`isDark` on the server works without the plugin.',\n )\n }\n\n theme.value = next\n applyTheme(next, state.target)\n\n if (state.persist && typeof window !== 'undefined') {\n // См. комментарий в `readStoredTheme`: запись тоже может бросать.\n try {\n const storage = window.localStorage\n if (typeof storage?.setItem === 'function')\n storage.setItem(state.storageKey, next)\n }\n catch {\n // ignore\n }\n }\n }\n\n function toggleTheme() {\n setTheme(theme.value === 'dark' ? 'light' : 'dark')\n }\n\n function initTheme() {\n applyTheme(theme.value, state.target)\n }\n\n return {\n theme,\n isDark,\n setTheme,\n toggleTheme,\n initTheme,\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"useTheme.js","names":[],"sources":["../../src/composables/useTheme.ts"],"sourcesContent":["import { computed, hasInjectionContext, inject, ref } from 'vue'\nimport type { App, InjectionKey, Ref } from 'vue'\n\nexport type ThemeName = 'light' | 'dark'\n\nexport type UseThemeOptions = {\n /**\n * Ключ для хранения выбранной темы в localStorage.\n * По умолчанию: `gr-theme`.\n */\n storageKey?: string\n\n /**\n * Если `false` — не читать/не писать localStorage (полезно для SSR/embedded сценариев).\n * По умолчанию: `true`.\n */\n persist?: boolean\n\n /**\n * Куда писать `data-theme`. По умолчанию — `<html>` (канон одного приложения).\n *\n * Геттер, а не элемент: на момент `app.use(...)` корень приложения ещё не\n * смонтирован. Обязателен нескольким приложениям с независимыми темами:\n * селектор тем атрибутный (`[data-theme='dark']`), поэтому тема работает от\n * любого контейнера, а без `target` все приложения писали бы в один `<html>`\n * и побеждал бы последний.\n */\n target?: () => HTMLElement | null\n}\n\nconst DEFAULT_STORAGE_KEY = 'gr-theme'\nconst IS_SERVER = typeof window === 'undefined'\n\nfunction readStoredTheme(storageKey: string, persist: boolean): ThemeName | null {\n if (typeof window === 'undefined' || !persist)\n return null\n\n // Доступ к `localStorage` может бросать `SecurityError` в Safari private mode\n // и при отключённых cookies/storage — поэтому оборачиваем в try/catch.\n try {\n const storage = window.localStorage\n if (typeof storage?.getItem !== 'function')\n return null\n\n const stored = storage.getItem(storageKey)\n if (stored === 'light' || stored === 'dark')\n return stored\n }\n catch {\n // ignore\n }\n\n return null\n}\n\nfunction getSystemTheme(): ThemeName {\n if (typeof window === 'undefined')\n return 'light'\n return window.matchMedia?.('(prefers-color-scheme: dark)')?.matches ? 'dark' : 'light'\n}\n\nfunction getPreferredTheme(storageKey = DEFAULT_STORAGE_KEY, persist = true): ThemeName {\n return readStoredTheme(storageKey, persist) ?? getSystemTheme()\n}\n\nfunction applyTheme(theme: ThemeName, target?: () => HTMLElement | null) {\n if (typeof document === 'undefined')\n return\n\n // Канон GR — атрибут `[data-theme]`: на `<html>` (см. docs/styling.md →\n // «Темизация») либо на корне приложения, когда состояние задало `target`.\n // `useTheme`/`initThemeEarly` — единственный рантайм-API переключения темы.\n const root = target?.() ?? document.documentElement\n root.dataset.theme = theme\n}\n\n/**\n * Состояние темы одного приложения.\n *\n * `theme` общий для всех вызовов `useTheme()` внутри приложения: свой `ref` на\n * вызов означал бы, что переключение темы в одном компоненте не трогает\n * `theme`/`isDark` в другом — менялся бы только DOM-атрибут, а реактивные\n * потребители разъехались бы.\n */\ntype ThemeState = {\n theme: Ref<ThemeName>\n storageKey: string\n persist: boolean\n /** Корень, получающий `data-theme`; не задан — `<html>`. */\n target?: () => HTMLElement | null\n listenersBound: boolean\n /**\n * Снимает window-слушатели состояния. Появляется после `bindListeners`;\n * зовёт `app.onUnmount` у app-scoped состояний — модульный синглтон живёт со\n * страницей, и снимать его незачем.\n */\n teardownListeners?: () => void\n /** Разрешена ли мутация — см. `resolveThemeState`. */\n writable: boolean\n}\n\nfunction createThemeState(options: UseThemeOptions = {}, writable = true): ThemeState {\n const storageKey = options.storageKey ?? DEFAULT_STORAGE_KEY\n const persist = options.persist ?? true\n\n return {\n theme: ref<ThemeName>(getPreferredTheme(storageKey, persist)),\n storageKey,\n persist,\n target: options.target,\n listenersBound: false,\n writable,\n }\n}\n\n/** Ключ provide/inject для app-scoped состояния темы (устанавливает плагин ниже). */\nexport const GRANULARITY_THEME_STATE: InjectionKey<ThemeState> = Symbol.for('@feugene/granularity/theme-state')\n\n/**\n * Vue-плагин: даёт каждому приложению собственное состояние темы через\n * `app.provide`. Обязателен для (а) SSR, если приложение переключает тему на\n * сервере — иначе тема одного запроса уедет в ответ следующему, — и (б)\n * нескольких Vue-приложений на одной странице, которым нужны независимые темы.\n *\n * Для (б) обязателен и `target` — корень, получающий `data-theme`: без него\n * изолировано только состояние, а атрибут все приложения пишут в один `<html>`,\n * и побеждает последний.\n *\n * ```ts\n * app.use(granularityThemePlugin)\n * app.use(granularityThemePlugin, {\n * storageKey: 'admin-theme',\n * persist: false,\n * target: () => document.getElementById('admin-app'),\n * })\n * ```\n */\n// Где-то на странице установлен плагин: фолбэк в синглтон для такого приложения —\n// почти наверняка ошибка вызова, а не осознанный выбор.\nlet appScopedThemeProvided = false\nlet contextFallbackWarned = false\n\nexport const granularityThemePlugin = {\n install(app: App, options: UseThemeOptions = {}) {\n appScopedThemeProvided = true\n const state = createThemeState(options)\n app.provide(GRANULARITY_THEME_STATE, state)\n // Слушатели `storage`/`prefers-color-scheme` держат состояние; без снятия\n // каждый цикл mount/unmount микрофронтенда копил бы их на window навсегда.\n app.onUnmount(() => state.teardownListeners?.())\n },\n}\n\n// Ленивый модульный синглтон — канонический фолбэк для простых SPA без плагина.\nlet moduleThemeState: ThemeState | null = null\n\n/** Сброс dev-предупреждения о фолбэке. Внутреннее API — нужно тестам. */\nexport function resetThemeContextWarning(): void {\n contextFallbackWarned = false\n}\n\nfunction resolveThemeState(options: UseThemeOptions): ThemeState {\n // App-scoped состояние (provide через плагин): setup-контекст ИЛИ\n // `app.runWithContext(() => useTheme())` — `hasInjectionContext` покрывает оба.\n if (hasInjectionContext()) {\n const provided = inject(GRANULARITY_THEME_STATE, null)\n if (provided)\n return provided\n }\n\n // На сервере без плагина модульное состояние текло бы между запросами. Но\n // читать тему на сервере — нормальный сценарий (шаблон смотрит `isDark`), и\n // ронять на нём весь рендер нельзя: отдаём состояние на один вызов и запрещаем\n // мутацию — течёт именно она.\n if (IS_SERVER)\n return createThemeState(options, false)\n\n // Дедуп и текст — прямо под гардом: `__GR_DEV__` разворачивается в условие\n // на сборке, и у потребителя вся ветка выкидывается вместе с сообщением.\n if (\n appScopedThemeProvided && !contextFallbackWarned\n && __GR_DEV__\n ) {\n contextFallbackWarned = true\n console.warn(\n '[granularity] useTheme() вызван вне setup/inject-контекста: app-scoped состояние '\n + 'granularityThemePlugin недоступно, использован модульный синглтон. '\n + 'Оберните вызов: app.runWithContext(() => useTheme()).',\n )\n }\n\n if (!moduleThemeState)\n moduleThemeState = createThemeState(options)\n\n return moduleThemeState\n}\n\n// Подписки на системную тему и синхронизацию между вкладками. Привязаны к\n// состоянию, а не к модулю: у каждого приложения своё. Одна подписка на\n// состояние, а не на потребителя, — `onScopeDispose` здесь был бы вреден, он\n// порвал бы общую подписку при размонтировании одного из них.\nfunction bindListeners(state: ThemeState) {\n if (state.listenersBound || typeof window === 'undefined')\n return\n state.listenersBound = true\n\n // Cross-tab: другая вкладка записала тему в localStorage.\n const onStorage = (event: StorageEvent): void => {\n if (!state.persist || event.key !== state.storageKey)\n return\n if (event.newValue === 'light' || event.newValue === 'dark') {\n state.theme.value = event.newValue\n applyTheme(event.newValue, state.target)\n }\n }\n window.addEventListener?.('storage', onStorage)\n\n // Системная смена `prefers-color-scheme` — следуем ей только если пользователь\n // НЕ выбрал тему явно (в storage ничего не сохранено).\n const mql = window.matchMedia?.('(prefers-color-scheme: dark)')\n const onSchemeChange = (event: MediaQueryListEvent): void => {\n if (readStoredTheme(state.storageKey, state.persist) !== null)\n return\n const next: ThemeName = event.matches ? 'dark' : 'light'\n state.theme.value = next\n applyTheme(next, state.target)\n }\n mql?.addEventListener?.('change', onSchemeChange)\n\n state.teardownListeners = () => {\n window.removeEventListener?.('storage', onStorage)\n mql?.removeEventListener?.('change', onSchemeChange)\n state.listenersBound = false\n state.teardownListeners = undefined\n }\n}\n\n/**\n * Для применения темы максимально рано (до монтирования Vue), чтобы избежать \"мигания\".\n * Также сидирует модульное состояние, чтобы значение в `useTheme()` совпадало с уже\n * применённым к документу.\n *\n * Функция клиентская: её зовут из браузерной точки входа до создания приложения,\n * то есть app-scoped состояние инжектить там ещё неоткуда. Расхождения с плагином\n * это не даёт — оба сидируются из одного `localStorage`.\n */\nexport function initThemeEarly(options: UseThemeOptions = {}) {\n const storageKey = options.storageKey ?? DEFAULT_STORAGE_KEY\n const persist = options.persist ?? true\n const preferred = getPreferredTheme(storageKey, persist)\n\n moduleThemeState ??= createThemeState(options)\n moduleThemeState.storageKey = storageKey\n moduleThemeState.persist = persist\n moduleThemeState.theme.value = preferred\n\n applyTheme(preferred)\n}\n\nexport function useTheme(options: UseThemeOptions = {}) {\n const state = resolveThemeState(options)\n\n // Опции вызова уточняют состояние — тот же приём, что был у модульного\n // синглтона: слушателям нужно знать, какой ключ отслеживать.\n if (options.storageKey !== undefined)\n state.storageKey = options.storageKey\n if (options.persist !== undefined)\n state.persist = options.persist\n if (options.target !== undefined)\n state.target = options.target\n\n bindListeners(state)\n\n const theme = state.theme\n const isDark = computed(() => theme.value === 'dark')\n\n function setTheme(next: ThemeName) {\n if (!state.writable) {\n throw new Error(\n '[granularity] useTheme: setTheme/toggleTheme require `app.use(granularityThemePlugin)` during SSR — '\n + 'the module-singleton fallback is disabled server-side, so a theme set by one request would leak into '\n + 'the next response. Reading `theme`/`isDark` on the server works without the plugin.',\n )\n }\n\n theme.value = next\n applyTheme(next, state.target)\n\n if (state.persist && typeof window !== 'undefined') {\n // См. комментарий в `readStoredTheme`: запись тоже может бросать.\n try {\n const storage = window.localStorage\n if (typeof storage?.setItem === 'function')\n storage.setItem(state.storageKey, next)\n }\n catch {\n // ignore\n }\n }\n }\n\n function toggleTheme() {\n setTheme(theme.value === 'dark' ? 'light' : 'dark')\n }\n\n function initTheme() {\n applyTheme(theme.value, state.target)\n }\n\n return {\n theme,\n isDark,\n setTheme,\n toggleTheme,\n initTheme,\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"useToast.js","names":[],"sources":["../../src/composables/useToast.ts"],"sourcesContent":["import { computed, hasInjectionContext, inject, reactive } from 'vue'\nimport type { App, InjectionKey } from 'vue'\n\nimport type { GrButtonSize, GrButtonVariant } from '../components/GrButton/grButtonStyles'\nimport type { GrTone } from '../components/shared/tones'\n\nexport type GrToastTone = GrTone\n\n/**\n * Опциональная action-кнопка тоста: `label` — текст, `onClick` — обработчик.\n * По клику вызывается `onClick`, затем тост закрывается (если `dismissOnClick`\n * не установлен в `false`). `size`/`variant` настраивают вид кнопки.\n */\nexport type ToastAction = {\n label: string\n onClick: () => void\n /** Закрывать ли тост после клика по действию. По умолчанию `true`. */\n dismissOnClick?: boolean\n /** Размер кнопки действия. По умолчанию `sm`. */\n size?: GrButtonSize\n /** Вариант кнопки действия. По умолчанию `outline`. */\n variant?: GrButtonVariant\n}\n\nexport type ToastInput = {\n title: string\n message?: string\n tone?: GrToastTone\n /**\n * Автозакрытие через N мс. `0` или отрицательное значение — «навсегда»\n * (до явного вызова `dismiss(id)` / `clear()`). По умолчанию: `3500`.\n */\n timeoutMs?: number\n /** Одна action-кнопка в теле тоста (шорткат для `actions: [action]`). */\n action?: ToastAction\n /** Несколько action-кнопок в теле тоста. Рендерятся после `action`. */\n actions?: ToastAction[]\n /**\n * Схлопывать повтор: `push` с уже занятым ключом обновляет **живой** тост\n * и перезапускает его автозакрытие вместо того, чтобы завести второй.\n *\n * Ключ действует только пока тост на экране. Это не то же самое, что помнить\n * последнее показанное: повтор действия с тем же текстом обязан показаться\n * снова, иначе второе «Сохранено» пропадёт молча.\n */\n dedupeKey?: string\n}\n\n/**\n * Сообщения для `promise`. Строка — шорткат для `{ title }`; функция получает\n * результат промиса (или причину отказа), чтобы подставить его в текст.\n */\nexport type ToastPromiseMessages<T> = {\n loading: string | ToastInput\n success: string | ToastInput | ((value: T) => string | ToastInput)\n error: string | ToastInput | ((reason: unknown) => string | ToastInput)\n}\n\nexport type Toast = {\n id: string\n title: string\n message?: string\n tone: GrToastTone\n /** Исходный таймаут автозакрытия, мс (`0` — без автозакрытия). Нужен для progress-бара. */\n timeoutMs: number\n /** Одна action-кнопка в теле тоста. */\n action?: ToastAction\n /** Несколько action-кнопок в теле тоста. */\n actions?: ToastAction[]\n /** Ключ схлопывания повторов, если тост создан с ним. */\n dedupeKey?: string\n}\n\nconst DEFAULT_TIMEOUT_MS = 3500\nconst IS_SERVER = typeof window === 'undefined'\n\n// Метаданные таймера автозакрытия с поддержкой pause/resume (WCAG 2.2.1: под\n// курсором/фокусом отсчёт должен останавливаться).\ntype ToastTimer = {\n handle: number | null\n /** Сколько мс осталось до автозакрытия на момент последней паузы/старта. */\n remaining: number\n /** `performance.now()` момента запуска текущего отрезка (для вычисления остатка). */\n startedAt: number\n}\n\nexport type ToastState = {\n toasts: Toast[]\n timers: Map<string, ToastTimer>\n /** Потолок очереди: сверх него самые старые тосты вытесняются. */\n maxToasts: number\n}\n\n/**\n * Потолок очереди по умолчанию. `maxVisible` у `GrToaster` режет только видимые,\n * поэтому без него поток событий (переподключение сокета, цикл ошибок) копил бы\n * очередь и вываливал её на пользователя, когда стек освободится.\n */\nconst DEFAULT_MAX_TOASTS = 20\n\nexport type GranularityToastPluginOptions = {\n /** Потолок очереди. По умолчанию `20`. */\n maxToasts?: number\n}\n\nfunction createToastState(maxToasts = DEFAULT_MAX_TOASTS): ToastState {\n return {\n toasts: reactive<Toast[]>([]),\n timers: new Map<string, ToastTimer>(),\n maxToasts: Math.max(1, Math.trunc(maxToasts)),\n }\n}\n\n/** Ключ provide/inject для app-scoped состояния тостов (устанавливает плагин ниже). */\nexport const GRANULARITY_TOAST_STATE: InjectionKey<ToastState> = Symbol.for('@feugene/granularity/toast-state')\n\n/**\n * Vue-плагин: даёт каждому приложению собственное изолированное состояние тостов\n * через `app.provide`. Обязателен для (а) нескольких Vue-приложений на одной\n * странице (микрофронтенды — иначе делят один стек тостов) и (б) SSR (иначе\n * модульное состояние течёт между запросами).\n *\n * ```ts\n * app.use(granularityToastPlugin)\n * app.use(granularityToastPlugin, { maxToasts: 50 })\n * ```\n */\n// Где-то на странице установлен плагин: фолбэк в синглтон для такого приложения —\n// почти наверняка ошибка вызова, а не осознанный выбор.\nlet appScopedToastProvided = false\nlet contextFallbackWarned = false\n\nexport const granularityToastPlugin = {\n install(app: App, options: GranularityToastPluginOptions = {}) {\n appScopedToastProvided = true\n app.provide(GRANULARITY_TOAST_STATE, createToastState(options.maxToasts))\n },\n}\n\n// Ленивый модульный синглтон — канонический фолбэк для простых SPA без плагина.\nlet moduleToastState: ToastState | null = null\n\n/** Сброс dev-предупреждения о фолбэке. Внутреннее API — нужно тестам. */\nexport function resetToastContextWarning(): void {\n contextFallbackWarned = false\n}\n\nfunction resolveToastState(): ToastState {\n // App-scoped состояние (provide через плагин): setup-контекст ИЛИ\n // `app.runWithContext(() => useToast())` — `hasInjectionContext` покрывает оба,\n // поэтому вызов из router guard / интерцептора достаёт состояние приложения.\n if (hasInjectionContext()) {\n const provided = inject(GRANULARITY_TOAST_STATE, null)\n if (provided) return provided\n }\n\n // В SSR модульный фолбэк запрещён: одно mutable-состояние на модуль утекало бы\n // между запросами. Требуем установленный плагин (app.provide).\n if (IS_SERVER) {\n throw new Error(\n '[granularity] useToast requires `app.use(granularityToastPlugin)` during SSR — '\n + 'the module-singleton fallback is disabled server-side to avoid state leaking between requests.',\n )\n }\n\n // Дедуп и текст — прямо под гардом: `__GR_DEV__` разворачивается в условие\n // на сборке, и у потребителя вся ветка выкидывается вместе с сообщением.\n if (\n appScopedToastProvided && !contextFallbackWarned\n && __GR_DEV__\n ) {\n contextFallbackWarned = true\n console.warn(\n '[granularity] useToast() вызван вне setup/inject-контекста: app-scoped состояние '\n + 'granularityToastPlugin недоступно, использован модульный синглтон — эти тосты не попадут '\n + 'в GrToaster приложения. Оберните вызов: app.runWithContext(() => useToast()).',\n )\n }\n\n if (!moduleToastState)\n moduleToastState = createToastState()\n\n return moduleToastState\n}\n\nfunction now(): number {\n return typeof performance !== 'undefined' ? performance.now() : Date.now()\n}\n\nfunction makeId(): string {\n return `${Date.now()}-${Math.random().toString(16).slice(2)}`\n}\n\nexport function useToast() {\n const state = resolveToastState()\n const list = computed(() => state.toasts)\n\n function armTimer(id: string, timer: ToastTimer): void {\n if (timer.remaining <= 0 || typeof window === 'undefined') return\n timer.startedAt = now()\n timer.handle = window.setTimeout(() => {\n state.timers.delete(id)\n dismiss(id)\n }, timer.remaining)\n }\n\n function clearTimer(id: string): void {\n const timer = state.timers.get(id)\n if (timer?.handle != null) {\n clearTimeout(timer.handle)\n timer.handle = null\n }\n state.timers.delete(id)\n }\n\n function push(input: ToastInput): string {\n const timeout = input.timeoutMs ?? DEFAULT_TIMEOUT_MS\n\n // Схлопывание ищет тост только среди живых. Помнить последний показанный\n // ключ нельзя: тогда повтор того же действия («Сохранено» второй раз)\n // не показался бы вообще — тост уже ушёл, а ключ ещё занят.\n if (input.dedupeKey !== undefined) {\n const existing = state.toasts.find(item => item.dedupeKey === input.dedupeKey)\n if (existing) {\n // Замена целиком, а не патч: повторный `push` — это законченное\n // высказывание, и `message` от прошлого раза донашивать он не должен.\n applyStage(existing.id, { ...input, tone: input.tone ?? 'info', timeoutMs: timeout })\n return existing.id\n }\n }\n\n const id = makeId()\n const toast: Toast = {\n id,\n title: input.title,\n message: input.message,\n tone: input.tone ?? 'info',\n timeoutMs: timeout > 0 ? timeout : 0,\n action: input.action,\n actions: input.actions,\n dedupeKey: input.dedupeKey,\n }\n\n state.toasts.unshift(toast)\n trimQueue()\n\n if (toast.timeoutMs > 0) {\n const timer: ToastTimer = { handle: null, remaining: toast.timeoutMs, startedAt: now() }\n state.timers.set(id, timer)\n armTimer(id, timer)\n }\n\n return id\n }\n\n // Вытесняем с хвоста — там самые старые: при потоке событий устаревшее\n // уведомление менее ценно, чем свежее. Таймеры снимаем, иначе остались бы\n // висящие `setTimeout` на тосты, которых больше нет.\n //\n // Это флуд-защита уровня состояния, а не очередь показа (`GrToaster` держит\n // видимыми старейших): при переполнении потолка вытеснен может быть и тост,\n // который сейчас на экране, — осознанный компромисс.\n function trimQueue(): void {\n while (state.toasts.length > state.maxToasts) {\n const dropped = state.toasts.pop()\n if (dropped)\n clearTimer(dropped.id)\n }\n }\n\n /**\n * Меняет показанный тост на месте. `timeoutMs` в патче перезапускает таймер\n * (`0` — снова «навсегда»), поэтому «вечный» loading может стать\n * самозакрывающимся success.\n *\n * Возвращает `false`, если тоста уже нет: пользователь мог закрыть его руками,\n * и воскрешать его нельзя.\n */\n function update(id: string, patch: Partial<ToastInput>): boolean {\n const toast = state.toasts.find(item => item.id === id)\n if (!toast) return false\n\n if (patch.title !== undefined) toast.title = patch.title\n if (patch.message !== undefined) toast.message = patch.message\n if (patch.tone !== undefined) toast.tone = patch.tone\n if (patch.action !== undefined) toast.action = patch.action\n if (patch.actions !== undefined) toast.actions = patch.actions\n\n if (patch.timeoutMs !== undefined)\n toast.timeoutMs = rearmTimer(id, patch.timeoutMs)\n\n return true\n }\n\n /** Перезапускает автозакрытие с нового таймаута; `0` и меньше — «навсегда». */\n function rearmTimer(id: string, timeoutMs: number): number {\n clearTimer(id)\n const next = timeoutMs > 0 ? timeoutMs : 0\n\n if (next > 0) {\n const timer: ToastTimer = { handle: null, remaining: next, startedAt: now() }\n state.timers.set(id, timer)\n armTimer(id, timer)\n }\n\n return next\n }\n\n /**\n * Полная замена стадии для `promise()`. В отличие от `update`, поле без\n * значения очищается: успех не должен донашивать `message` и action-кнопки\n * загрузки — патч-семантика оставляла бы их в тосте навсегда.\n */\n function applyStage(id: string, stage: ToastInput & { tone: GrToastTone, timeoutMs: number }): void {\n const toast = state.toasts.find(item => item.id === id)\n if (!toast) return\n\n toast.title = stage.title\n toast.message = stage.message\n toast.tone = stage.tone\n toast.action = stage.action\n toast.actions = stage.actions\n toast.timeoutMs = rearmTimer(id, stage.timeoutMs)\n }\n\n /**\n * Один тост на весь жизненный цикл промиса: «загружаем» переписывается в\n * успех или ошибку, а не закрывается ради нового — стек не дёргается.\n *\n * Каждая стадия **заменяет** предыдущую целиком: поле, не заданное в\n * success/error, очищается, а не наследуется от loading.\n *\n * Возвращает исходный промис и **не глотает отказ**: тост не заменяет\n * обработку ошибки, вызывающий по-прежнему обязан её обработать.\n */\n function promise<T>(input: Promise<T>, messages: ToastPromiseMessages<T>): Promise<T> {\n const id = push({ tone: 'info', ...toToastInput(messages.loading), timeoutMs: 0 })\n\n input.then(\n (value) => {\n const stage = resolveMessage(messages.success, value)\n applyStage(id, { ...stage, tone: stage.tone ?? 'success', timeoutMs: stage.timeoutMs ?? DEFAULT_TIMEOUT_MS })\n },\n (reason: unknown) => {\n const stage = resolveMessage(messages.error, reason)\n applyStage(id, { ...stage, tone: stage.tone ?? 'danger', timeoutMs: stage.timeoutMs ?? DEFAULT_TIMEOUT_MS })\n },\n )\n\n return input\n }\n\n /** Останавливает отсчёт автозакрытия, сохраняя остаток (идемпотентно). */\n function pause(id: string): void {\n const timer = state.timers.get(id)\n if (!timer || timer.handle == null) return\n\n clearTimeout(timer.handle)\n timer.handle = null\n timer.remaining = Math.max(0, timer.remaining - (now() - timer.startedAt))\n }\n\n /** Возобновляет отсчёт с сохранённого остатка (идемпотентно). */\n function resume(id: string): void {\n const timer = state.timers.get(id)\n if (!timer || timer.handle != null) return\n\n armTimer(id, timer)\n }\n\n function dismiss(id: string): void {\n clearTimer(id)\n const index = state.toasts.findIndex(toast => toast.id === id)\n if (index >= 0)\n state.toasts.splice(index, 1)\n }\n\n function clear(): void {\n for (const id of [...state.timers.keys()]) clearTimer(id)\n state.toasts.splice(0, state.toasts.length)\n }\n\n return {\n list,\n push,\n update,\n promise,\n dismiss,\n clear,\n pause,\n resume,\n }\n}\n\nfunction toToastInput(message: string | ToastInput): ToastInput {\n return typeof message === 'string' ? { title: message } : message\n}\n\nfunction resolveMessage<T>(\n message: string | ToastInput | ((value: T) => string | ToastInput),\n value: T,\n): ToastInput {\n return toToastInput(typeof message === 'function' ? message(value) : message)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"useToast.js","names":[],"sources":["../../src/composables/useToast.ts"],"sourcesContent":["import { computed, hasInjectionContext, inject, reactive } from 'vue'\nimport type { App, InjectionKey } from 'vue'\n\nimport type { GrButtonSize, GrButtonVariant } from '../components/GrButton/grButtonStyles'\nimport type { GrTone } from '../components/shared/tones'\n\nexport type GrToastTone = GrTone\n\n/**\n * Опциональная action-кнопка тоста: `label` — текст, `onClick` — обработчик.\n * По клику вызывается `onClick`, затем тост закрывается (если `dismissOnClick`\n * не установлен в `false`). `size`/`variant` настраивают вид кнопки.\n */\nexport type ToastAction = {\n label: string\n onClick: () => void\n /** Закрывать ли тост после клика по действию. По умолчанию `true`. */\n dismissOnClick?: boolean\n /** Размер кнопки действия. По умолчанию `sm`. */\n size?: GrButtonSize\n /** Вариант кнопки действия. По умолчанию `outline`. */\n variant?: GrButtonVariant\n}\n\nexport type ToastInput = {\n title: string\n message?: string\n tone?: GrToastTone\n /**\n * Автозакрытие через N мс. `0` или отрицательное значение — «навсегда»\n * (до явного вызова `dismiss(id)` / `clear()`). По умолчанию: `3500`.\n */\n timeoutMs?: number\n /** Одна action-кнопка в теле тоста (шорткат для `actions: [action]`). */\n action?: ToastAction\n /** Несколько action-кнопок в теле тоста. Рендерятся после `action`. */\n actions?: ToastAction[]\n /**\n * Схлопывать повтор: `push` с уже занятым ключом обновляет **живой** тост\n * и перезапускает его автозакрытие вместо того, чтобы завести второй.\n *\n * Ключ действует только пока тост на экране. Это не то же самое, что помнить\n * последнее показанное: повтор действия с тем же текстом обязан показаться\n * снова, иначе второе «Сохранено» пропадёт молча.\n */\n dedupeKey?: string\n}\n\n/**\n * Сообщения для `promise`. Строка — шорткат для `{ title }`; функция получает\n * результат промиса (или причину отказа), чтобы подставить его в текст.\n */\nexport type ToastPromiseMessages<T> = {\n loading: string | ToastInput\n success: string | ToastInput | ((value: T) => string | ToastInput)\n error: string | ToastInput | ((reason: unknown) => string | ToastInput)\n}\n\nexport type Toast = {\n id: string\n title: string\n message?: string\n tone: GrToastTone\n /** Исходный таймаут автозакрытия, мс (`0` — без автозакрытия). Нужен для progress-бара. */\n timeoutMs: number\n /** Одна action-кнопка в теле тоста. */\n action?: ToastAction\n /** Несколько action-кнопок в теле тоста. */\n actions?: ToastAction[]\n /** Ключ схлопывания повторов, если тост создан с ним. */\n dedupeKey?: string\n}\n\nconst DEFAULT_TIMEOUT_MS = 3500\nconst IS_SERVER = typeof window === 'undefined'\n\n// Метаданные таймера автозакрытия с поддержкой pause/resume (WCAG 2.2.1: под\n// курсором/фокусом отсчёт должен останавливаться).\ntype ToastTimer = {\n handle: number | null\n /** Сколько мс осталось до автозакрытия на момент последней паузы/старта. */\n remaining: number\n /** `performance.now()` момента запуска текущего отрезка (для вычисления остатка). */\n startedAt: number\n}\n\nexport type ToastState = {\n toasts: Toast[]\n timers: Map<string, ToastTimer>\n /** Потолок очереди: сверх него самые старые тосты вытесняются. */\n maxToasts: number\n}\n\n/**\n * Потолок очереди по умолчанию. `maxVisible` у `GrToaster` режет только видимые,\n * поэтому без него поток событий (переподключение сокета, цикл ошибок) копил бы\n * очередь и вываливал её на пользователя, когда стек освободится.\n */\nconst DEFAULT_MAX_TOASTS = 20\n\nexport type GranularityToastPluginOptions = {\n /** Потолок очереди. По умолчанию `20`. */\n maxToasts?: number\n}\n\nfunction createToastState(maxToasts = DEFAULT_MAX_TOASTS): ToastState {\n return {\n toasts: reactive<Toast[]>([]),\n timers: new Map<string, ToastTimer>(),\n maxToasts: Math.max(1, Math.trunc(maxToasts)),\n }\n}\n\n/** Ключ provide/inject для app-scoped состояния тостов (устанавливает плагин ниже). */\nexport const GRANULARITY_TOAST_STATE: InjectionKey<ToastState> = Symbol.for('@feugene/granularity/toast-state')\n\n/**\n * Vue-плагин: даёт каждому приложению собственное изолированное состояние тостов\n * через `app.provide`. Обязателен для (а) нескольких Vue-приложений на одной\n * странице (микрофронтенды — иначе делят один стек тостов) и (б) SSR (иначе\n * модульное состояние течёт между запросами).\n *\n * ```ts\n * app.use(granularityToastPlugin)\n * app.use(granularityToastPlugin, { maxToasts: 50 })\n * ```\n */\n// Где-то на странице установлен плагин: фолбэк в синглтон для такого приложения —\n// почти наверняка ошибка вызова, а не осознанный выбор.\nlet appScopedToastProvided = false\nlet contextFallbackWarned = false\n\nexport const granularityToastPlugin = {\n install(app: App, options: GranularityToastPluginOptions = {}) {\n appScopedToastProvided = true\n app.provide(GRANULARITY_TOAST_STATE, createToastState(options.maxToasts))\n },\n}\n\n// Ленивый модульный синглтон — канонический фолбэк для простых SPA без плагина.\nlet moduleToastState: ToastState | null = null\n\n/** Сброс dev-предупреждения о фолбэке. Внутреннее API — нужно тестам. */\nexport function resetToastContextWarning(): void {\n contextFallbackWarned = false\n}\n\nfunction resolveToastState(): ToastState {\n // App-scoped состояние (provide через плагин): setup-контекст ИЛИ\n // `app.runWithContext(() => useToast())` — `hasInjectionContext` покрывает оба,\n // поэтому вызов из router guard / интерцептора достаёт состояние приложения.\n if (hasInjectionContext()) {\n const provided = inject(GRANULARITY_TOAST_STATE, null)\n if (provided)\n return provided\n }\n\n // В SSR модульный фолбэк запрещён: одно mutable-состояние на модуль утекало бы\n // между запросами. Требуем установленный плагин (app.provide).\n if (IS_SERVER) {\n throw new Error(\n '[granularity] useToast requires `app.use(granularityToastPlugin)` during SSR — '\n + 'the module-singleton fallback is disabled server-side to avoid state leaking between requests.',\n )\n }\n\n // Дедуп и текст — прямо под гардом: `__GR_DEV__` разворачивается в условие\n // на сборке, и у потребителя вся ветка выкидывается вместе с сообщением.\n if (\n appScopedToastProvided && !contextFallbackWarned\n && __GR_DEV__\n ) {\n contextFallbackWarned = true\n console.warn(\n '[granularity] useToast() вызван вне setup/inject-контекста: app-scoped состояние '\n + 'granularityToastPlugin недоступно, использован модульный синглтон — эти тосты не попадут '\n + 'в GrToaster приложения. Оберните вызов: app.runWithContext(() => useToast()).',\n )\n }\n\n if (!moduleToastState)\n moduleToastState = createToastState()\n\n return moduleToastState\n}\n\nfunction now(): number {\n return typeof performance !== 'undefined' ? performance.now() : Date.now()\n}\n\nfunction makeId(): string {\n return `${Date.now()}-${Math.random().toString(16).slice(2)}`\n}\n\nexport function useToast() {\n const state = resolveToastState()\n const list = computed(() => state.toasts)\n\n function armTimer(id: string, timer: ToastTimer): void {\n if (timer.remaining <= 0 || typeof window === 'undefined')\n return\n timer.startedAt = now()\n timer.handle = window.setTimeout(() => {\n state.timers.delete(id)\n dismiss(id)\n }, timer.remaining)\n }\n\n function clearTimer(id: string): void {\n const timer = state.timers.get(id)\n if (timer?.handle != null) {\n clearTimeout(timer.handle)\n timer.handle = null\n }\n state.timers.delete(id)\n }\n\n function push(input: ToastInput): string {\n const timeout = input.timeoutMs ?? DEFAULT_TIMEOUT_MS\n\n // Схлопывание ищет тост только среди живых. Помнить последний показанный\n // ключ нельзя: тогда повтор того же действия («Сохранено» второй раз)\n // не показался бы вообще — тост уже ушёл, а ключ ещё занят.\n if (input.dedupeKey !== undefined) {\n const existing = state.toasts.find(item => item.dedupeKey === input.dedupeKey)\n if (existing) {\n // Замена целиком, а не патч: повторный `push` — это законченное\n // высказывание, и `message` от прошлого раза донашивать он не должен.\n applyStage(existing.id, { ...input, tone: input.tone ?? 'info', timeoutMs: timeout })\n return existing.id\n }\n }\n\n const id = makeId()\n const toast: Toast = {\n id,\n title: input.title,\n message: input.message,\n tone: input.tone ?? 'info',\n timeoutMs: timeout > 0 ? timeout : 0,\n action: input.action,\n actions: input.actions,\n dedupeKey: input.dedupeKey,\n }\n\n state.toasts.unshift(toast)\n trimQueue()\n\n if (toast.timeoutMs > 0) {\n const timer: ToastTimer = { handle: null, remaining: toast.timeoutMs, startedAt: now() }\n state.timers.set(id, timer)\n armTimer(id, timer)\n }\n\n return id\n }\n\n // Вытесняем с хвоста — там самые старые: при потоке событий устаревшее\n // уведомление менее ценно, чем свежее. Таймеры снимаем, иначе остались бы\n // висящие `setTimeout` на тосты, которых больше нет.\n //\n // Это флуд-защита уровня состояния, а не очередь показа (`GrToaster` держит\n // видимыми старейших): при переполнении потолка вытеснен может быть и тост,\n // который сейчас на экране, — осознанный компромисс.\n function trimQueue(): void {\n while (state.toasts.length > state.maxToasts) {\n const dropped = state.toasts.pop()\n if (dropped)\n clearTimer(dropped.id)\n }\n }\n\n /**\n * Меняет показанный тост на месте. `timeoutMs` в патче перезапускает таймер\n * (`0` — снова «навсегда»), поэтому «вечный» loading может стать\n * самозакрывающимся success.\n *\n * Возвращает `false`, если тоста уже нет: пользователь мог закрыть его руками,\n * и воскрешать его нельзя.\n */\n function update(id: string, patch: Partial<ToastInput>): boolean {\n const toast = state.toasts.find(item => item.id === id)\n if (!toast)\n return false\n\n if (patch.title !== undefined)\n toast.title = patch.title\n if (patch.message !== undefined)\n toast.message = patch.message\n if (patch.tone !== undefined)\n toast.tone = patch.tone\n if (patch.action !== undefined)\n toast.action = patch.action\n if (patch.actions !== undefined)\n toast.actions = patch.actions\n\n if (patch.timeoutMs !== undefined)\n toast.timeoutMs = rearmTimer(id, patch.timeoutMs)\n\n return true\n }\n\n /** Перезапускает автозакрытие с нового таймаута; `0` и меньше — «навсегда». */\n function rearmTimer(id: string, timeoutMs: number): number {\n clearTimer(id)\n const next = timeoutMs > 0 ? timeoutMs : 0\n\n if (next > 0) {\n const timer: ToastTimer = { handle: null, remaining: next, startedAt: now() }\n state.timers.set(id, timer)\n armTimer(id, timer)\n }\n\n return next\n }\n\n /**\n * Полная замена стадии для `promise()`. В отличие от `update`, поле без\n * значения очищается: успех не должен донашивать `message` и action-кнопки\n * загрузки — патч-семантика оставляла бы их в тосте навсегда.\n */\n function applyStage(id: string, stage: ToastInput & { tone: GrToastTone, timeoutMs: number }): void {\n const toast = state.toasts.find(item => item.id === id)\n if (!toast)\n return\n\n toast.title = stage.title\n toast.message = stage.message\n toast.tone = stage.tone\n toast.action = stage.action\n toast.actions = stage.actions\n toast.timeoutMs = rearmTimer(id, stage.timeoutMs)\n }\n\n /**\n * Один тост на весь жизненный цикл промиса: «загружаем» переписывается в\n * успех или ошибку, а не закрывается ради нового — стек не дёргается.\n *\n * Каждая стадия **заменяет** предыдущую целиком: поле, не заданное в\n * success/error, очищается, а не наследуется от loading.\n *\n * Возвращает исходный промис и **не глотает отказ**: тост не заменяет\n * обработку ошибки, вызывающий по-прежнему обязан её обработать.\n */\n function promise<T>(input: Promise<T>, messages: ToastPromiseMessages<T>): Promise<T> {\n const id = push({ tone: 'info', ...toToastInput(messages.loading), timeoutMs: 0 })\n\n input.then(\n (value) => {\n const stage = resolveMessage(messages.success, value)\n applyStage(id, { ...stage, tone: stage.tone ?? 'success', timeoutMs: stage.timeoutMs ?? DEFAULT_TIMEOUT_MS })\n },\n (reason: unknown) => {\n const stage = resolveMessage(messages.error, reason)\n applyStage(id, { ...stage, tone: stage.tone ?? 'danger', timeoutMs: stage.timeoutMs ?? DEFAULT_TIMEOUT_MS })\n },\n )\n\n return input\n }\n\n /** Останавливает отсчёт автозакрытия, сохраняя остаток (идемпотентно). */\n function pause(id: string): void {\n const timer = state.timers.get(id)\n if (!timer || timer.handle == null)\n return\n\n clearTimeout(timer.handle)\n timer.handle = null\n timer.remaining = Math.max(0, timer.remaining - (now() - timer.startedAt))\n }\n\n /** Возобновляет отсчёт с сохранённого остатка (идемпотентно). */\n function resume(id: string): void {\n const timer = state.timers.get(id)\n if (!timer || timer.handle != null)\n return\n\n armTimer(id, timer)\n }\n\n function dismiss(id: string): void {\n clearTimer(id)\n const index = state.toasts.findIndex(toast => toast.id === id)\n if (index >= 0)\n state.toasts.splice(index, 1)\n }\n\n function clear(): void {\n for (const id of [...state.timers.keys()]) clearTimer(id)\n state.toasts.splice(0, state.toasts.length)\n }\n\n return {\n list,\n push,\n update,\n promise,\n dismiss,\n clear,\n pause,\n resume,\n }\n}\n\nfunction toToastInput(message: string | ToastInput): ToastInput {\n return typeof message === 'string' ? { title: message } : message\n}\n\nfunction resolveMessage<T>(\n message: string | ToastInput | ((value: T) => string | ToastInput),\n value: T,\n): ToastInput {\n return toToastInput(typeof message === 'function' ? message(value) : message)\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"useVirtualList.js","names":[],"sources":["../../src/composables/useVirtualList.ts"],"sourcesContent":["import type { ComputedRef, Ref } from 'vue'\nimport { computed, onScopeDispose, ref, watch } from 'vue'\n\n/**\n * Виртуализация длинного списка: в DOM живёт только окно вокруг вьюпорта.\n *\n * Композабл считает геометрию и отдаёт три числа — какой срез рисовать\n * (`range`), какой высоты держать распорку (`totalSize`) и на сколько сдвинуть\n * отрисованное (`offset`). Разметку строит потребитель: примитив ничего не\n * рендерит и ничего не знает про роли и клавиатуру своего списка.\n *\n * Скролл-протяжённость держат псевдоэлементы контейнера: `spacerStyle` отдаёт\n * их высоты переменными, а правило живёт в `<style>` компонента. Так\n * отрисованные строки остаются **прямыми детьми** контейнера, и роль вроде\n * `tree` или `listbox` не теряет обязательных потомков.\n *\n * ```vue\n * const container = useTemplateRef('scroller')\n * const { range, spacerStyle, measure } = useVirtualList({\n * container,\n * count: () => rows.value.length,\n * itemSize: 28,\n * viewportSize: () => props.maxHeight,\n * })\n *\n * <div\n * ref=\"scroller\"\n * role=\"tree\"\n * data-gr-virtual\n * :style=\"{ overflow: 'auto', maxHeight: '280px', ...spacerStyle }\"\n * >\n * <Row v-for=\"i in window\" :key=\"…\" :ref=\"el => measure(i, el)\" />\n * </div>\n *\n * <style scoped>\n * [data-gr-virtual]::before,\n * [data-gr-virtual]::after { content: ''; display: block; flex: none; }\n * [data-gr-virtual]::before { height: var(--gr-virtual-before, 0px); }\n * [data-gr-virtual]::after { height: var(--gr-virtual-after, 0px); }\n * </style>\n * ```\n *\n * Почему правило повторяется в каждом компоненте, а не лежит в одном файле:\n * единственный глобальный стиль пакета (`styles/base.css`) потребитель вправе\n * не подключать, и уехавшее туда правило молча ломало бы прокрутку. Сверяет\n * копии гейт `src/__tests__/virtualSpacer.test.ts`.\n */\n\n/** Куда поставить элемент относительно вьюпорта при прокрутке к нему. */\nexport type GrVirtualAlign = 'auto' | 'start' | 'center' | 'end'\n\nexport interface UseVirtualListOptions {\n /** Скроллящийся контейнер. До монтирования — `null`. */\n container: Ref<HTMLElement | null>\n /** Сколько элементов в списке. */\n count: () => number\n /**\n * Оценка высоты элемента. Фактическая берётся из замера, как только элемент\n * отрисован, — строки пакета заданы через `min-height`, и точного числа тут\n * знать неоткуда.\n */\n itemSize: number | ((index: number) => number)\n /**\n * Высота вьюпорта до первого замера контейнера. Нужна, чтобы **первый рендер\n * был детерминированным**: на сервере контейнера нет вовсе, а на клиенте он\n * ещё не измерен. Без неё сервер и клиент нарисовали бы разные окна и\n * гидрация разъехалась бы; с ней оба считают окно от одного числа.\n */\n viewportSize?: () => number | undefined\n /**\n * Идентити-токен данных списка — обычно сам массив. Смена идентичности\n * сбрасывает все замеры (`invalidate()`): они привязаны к индексам, и после\n * сортировки или замены массива хранили бы высоты чужих строк.\n *\n * Правило потребителя: **мутация массива на месте сохраняет замеры, замена\n * массива сбрасывает**. Append в тот же реактивный массив (инфинити-скролл)\n * идентичность не меняет — префикс замеров остаётся валидным.\n */\n source?: () => unknown\n /**\n * Зазор между элементами в пикселях (`gap` у flex/grid-контейнера).\n *\n * Замер элемента его не видит — `getBoundingClientRect` меряет коробку, а не\n * шаг раскладки. Без этого числа список считался бы короче, чем он есть,\n * и окно уползало бы тем сильнее, чем дальше прокрутка.\n */\n gap?: number\n /** Сколько элементов рисовать за пределами вьюпорта. По умолчанию `4`. */\n overscan?: number\n}\n\nexport interface UseVirtualListReturn {\n /** Полуинтервал индексов к отрисовке: `[start, end)`. */\n range: ComputedRef<{ start: number, end: number }>\n /** Полная высота содержимого: распорка, которая держит скроллбар. */\n totalSize: ComputedRef<number>\n /** Отступ сверху: высота срезанного до окна. */\n offset: ComputedRef<number>\n /** Отступ снизу: высота срезанного после окна. */\n offsetEnd: ComputedRef<number>\n /**\n * Стиль контейнера: высоты распорок и выключенный scroll anchoring.\n *\n * Имена переменных приезжают готовыми, поэтому написать их руками — и\n * разойтись с правилом в `<style>` — нельзя. Опечатка в имени давала бы\n * список без прокрутки, и увидеть это можно было бы только глазами.\n */\n spacerStyle: ComputedRef<Record<string, string>>\n /** Регистрация отрисованного элемента: замер и подписка на изменение высоты. */\n measure: (index: number, el: Element | null) => void\n /** Прокрутка к элементу. Без выравнивания — минимальным движением. */\n scrollToIndex: (index: number, align?: GrVirtualAlign) => void\n /**\n * Сбрасывает замеры с индекса `from` (по умолчанию — все) к оценкам.\n * Видимые строки перемеряются первым же рендером; для смены данных обычно\n * достаточно опции `source` — ручной вызов нужен, когда высоты строк\n * изменились без замены массива (смена плотности, ресайз колонок).\n */\n invalidate: (from?: number) => void\n}\n\nconst DEFAULT_OVERSCAN = 4\n\n/** Порог, ниже которого разницу замеров считаем шумом округления. */\nconst SIZE_EPSILON = 0.5\n\nfunction readSize(el: Element): number {\n const box = el.getBoundingClientRect().height\n if (box > 0) return box\n\n // jsdom не считает layout: `getBoundingClientRect` там всегда нулевой, а\n // `offsetHeight` тест может задать сам.\n return (el as HTMLElement).offsetHeight ?? 0\n}\n\nexport function useVirtualList(options: UseVirtualListOptions): UseVirtualListReturn {\n const overscan = Math.max(0, Math.trunc(options.overscan ?? DEFAULT_OVERSCAN))\n const gap = Math.max(0, options.gap ?? 0)\n const estimateOf = typeof options.itemSize === 'function'\n ? options.itemSize\n : () => options.itemSize as number\n\n const scrollTop = ref(0)\n const measuredViewport = ref(0)\n\n const measured = new Map<number, number>()\n const observedIndex = new WeakMap<Element, number>()\n\n // `ResizeObserver` нет ни на сервере, ни в jsdom. Без него композабл работает\n // на оценках и явных замерах из `measure()` — это рабочий режим, а не отказ.\n const hasResizeObserver = typeof ResizeObserver !== 'undefined'\n const sizeObserver = hasResizeObserver\n ? new ResizeObserver((entries) => {\n for (const entry of entries) {\n const index = observedIndex.get(entry.target)\n if (index !== undefined) applySize(index, readSize(entry.target))\n }\n })\n : null\n const viewportObserver = hasResizeObserver ? new ResizeObserver(() => syncViewport()) : null\n let attachedTo: HTMLElement | null = null\n let offsets: number[] = [0]\n /** До какого индекса префикс-суммы актуальны. */\n let builtTo = 0\n\n /** Шаг раскладки: коробка элемента плюс зазор до следующего. */\n function sizeAt(index: number): number {\n return (measured.get(index) ?? estimateOf(index)) + gap\n }\n\n /**\n * Достраивает префикс-суммы до нужного индекса.\n *\n * Замер инвалидирует хвост от своего индекса, а не весь массив: иначе каждая\n * отрисованная строка стоила бы полного пересчёта по всему списку.\n */\n function buildTo(index: number): void {\n const limit = Math.min(index, options.count())\n while (builtTo < limit) {\n offsets[builtTo + 1] = offsets[builtTo] + sizeAt(builtTo)\n builtTo += 1\n }\n }\n\n function offsetAt(index: number): number {\n buildTo(index)\n return offsets[Math.min(Math.max(index, 0), builtTo)] ?? 0\n }\n\n function invalidateFrom(index: number): void {\n if (index < builtTo) builtTo = Math.max(0, index)\n }\n\n /** Последний индекс, чьё начало не ниже `px`. */\n function indexAt(px: number): number {\n const count = options.count()\n if (count === 0) return 0\n\n buildTo(count)\n\n let low = 0\n let high = builtTo\n while (low < high) {\n const mid = (low + high + 1) >> 1\n if ((offsets[mid] ?? 0) <= px) low = mid\n else high = mid - 1\n }\n\n return Math.min(low, count - 1)\n }\n\n function viewportSize(): number {\n return measuredViewport.value || options.viewportSize?.() || 0\n }\n\n /**\n * Геометрия окна одним снимком.\n *\n * Считается явно, а не `computed`'ом поверх смещений: массив префикс-сумм\n * мутируется на месте (иначе каждый замер пересоздавал бы десять тысяч чисел),\n * и вывести из него реактивную зависимость нечем — трекер увидел бы только те\n * индексы, которые пересчитывались в последний проход.\n */\n const geometry = ref({ start: 0, end: 0, offset: 0, offsetEnd: 0, total: 0 })\n\n function recompute(): void {\n const count = options.count()\n if (count === 0) {\n geometry.value = { start: 0, end: 0, offset: 0, offsetEnd: 0, total: 0 }\n return\n }\n\n const totalSize = offsetAt(count)\n const height = viewportSize()\n\n // Высота неизвестна (контейнера нет и оценки не дали) — рисуем всё: меньше\n // «списка целиком» отрисовать нечего без риска, что до замера так и не\n // дойдёт ни одна строка.\n if (height <= 0) {\n geometry.value = { start: 0, end: count, offset: 0, offsetEnd: 0, total: totalSize }\n return\n }\n\n const start = Math.max(0, indexAt(scrollTop.value) - overscan)\n const end = Math.min(count, indexAt(scrollTop.value + height) + 1 + overscan)\n\n geometry.value = {\n start,\n end,\n offset: offsetAt(start),\n offsetEnd: Math.max(0, totalSize - offsetAt(end)),\n total: totalSize,\n }\n }\n\n const range = computed(() => ({ start: geometry.value.start, end: geometry.value.end }))\n const total = computed(() => geometry.value.total)\n const offset = computed(() => geometry.value.offset)\n const offsetEnd = computed(() => geometry.value.offsetEnd)\n\n const spacerStyle = computed(() => ({\n // Браузерный scroll anchoring обязан быть выключен. Он удерживает видимый\n // узел на месте, подправляя `scrollTop`, когда меняется высота содержимого\n // выше него, — а окно меняет её на каждом кадре прокрутки. Дальше пересчёт\n // окна и якорь начинают двигать список навстречу друг другу, и прокрутка\n // уезжает тем сильнее, чем дальше оценка строки от фактической высоты.\n 'overflow-anchor': 'none',\n '--gr-virtual-before': `${geometry.value.offset}px`,\n '--gr-virtual-after': `${geometry.value.offsetEnd}px`,\n }))\n\n function syncScrollTop(): void {\n const el = options.container.value\n if (!el) return\n\n scrollTop.value = el.scrollTop\n recompute()\n }\n\n function syncViewport(): void {\n const el = options.container.value\n if (!el) return\n\n measuredViewport.value = el.clientHeight\n recompute()\n }\n\n function measure(index: number, el: Element | null): void {\n if (!el) return\n\n observedIndex.set(el, index)\n sizeObserver?.observe(el)\n\n applySize(index, readSize(el))\n }\n\n function applySize(index: number, size: number): void {\n if (size <= 0) return\n\n const previous = measured.get(index)\n if (previous !== undefined && Math.abs(previous - size) < SIZE_EPSILON) return\n\n // Считаем смещение ДО правки: строка ниже вьюпорта позицию не двигает, а\n // строка выше — двигает всё содержимое под курсором.\n const start = offsetAt(index)\n const delta = size - (previous ?? estimateOf(index))\n\n measured.set(index, size)\n invalidateFrom(index)\n\n const el = options.container.value\n // Компенсация: без неё уточнение высоты уже прокрученных строк выглядит как\n // рывок списка вверх или вниз ровно в момент замера.\n if (delta !== 0 && start < scrollTop.value && el) {\n el.scrollTop += delta\n scrollTop.value = el.scrollTop\n }\n\n recompute()\n }\n\n function scrollToIndex(index: number, align: GrVirtualAlign = 'auto'): void {\n const el = options.container.value\n const count = options.count()\n if (!el || count === 0) return\n\n const clamped = Math.min(Math.max(index, 0), count - 1)\n const start = offsetAt(clamped)\n const size = sizeAt(clamped)\n const height = el.clientHeight || viewportSize()\n const end = start + size\n\n let next: number\n switch (align) {\n case 'start':\n next = start\n break\n case 'end':\n next = end - height\n break\n case 'center':\n next = start + size / 2 - height / 2\n break\n case 'auto':\n if (start < el.scrollTop) next = start\n else if (end > el.scrollTop + height) next = end - height\n else return\n break\n }\n\n el.scrollTop = Math.max(0, Math.min(next, offsetAt(count) - height))\n scrollTop.value = el.scrollTop\n recompute()\n }\n\n function detach(): void {\n if (!attachedTo) return\n attachedTo.removeEventListener('scroll', syncScrollTop)\n viewportObserver?.unobserve(attachedTo)\n attachedTo = null\n }\n\n // Первый снимок до всякого монтирования: на сервере и на первом клиентском\n // рендере контейнера ещё нет, а окно уже должно быть посчитано — и одинаково.\n recompute()\n\n watch(options.container, (el) => {\n detach()\n if (!el) return\n\n attachedTo = el\n el.addEventListener('scroll', syncScrollTop, { passive: true })\n viewportObserver?.observe(el)\n syncViewport()\n syncScrollTop()\n }, { immediate: true })\n\n // Длина списка изменилась. Сокращение дополнительно чистит хвост смещений и\n // замеров: он относится к строкам, которых больше нет.\n watch(() => options.count(), (count) => {\n if (count < builtTo) truncate(count)\n recompute()\n })\n\n function invalidate(from = 0): void {\n for (const index of [...measured.keys()]) {\n if (index >= from) measured.delete(index)\n }\n invalidateFrom(from)\n recompute()\n }\n\n if (options.source) {\n // Идентичность данных сменилась — замеры принадлежат прошлому набору.\n watch(options.source, () => invalidate())\n }\n\n function truncate(count: number): void {\n builtTo = count\n offsets = offsets.slice(0, count + 1)\n for (const index of [...measured.keys()]) {\n if (index >= count) measured.delete(index)\n }\n }\n\n onScopeDispose(() => {\n detach()\n sizeObserver?.disconnect()\n viewportObserver?.disconnect()\n })\n\n return { range, totalSize: total, offset, offsetEnd, spacerStyle, measure, scrollToIndex, invalidate }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"useVirtualList.js","names":[],"sources":["../../src/composables/useVirtualList.ts"],"sourcesContent":["import type { ComputedRef, Ref } from 'vue'\nimport { computed, onScopeDispose, ref, watch } from 'vue'\n\n/**\n * Виртуализация длинного списка: в DOM живёт только окно вокруг вьюпорта.\n *\n * Композабл считает геометрию и отдаёт три числа — какой срез рисовать\n * (`range`), какой высоты держать распорку (`totalSize`) и на сколько сдвинуть\n * отрисованное (`offset`). Разметку строит потребитель: примитив ничего не\n * рендерит и ничего не знает про роли и клавиатуру своего списка.\n *\n * Скролл-протяжённость держат псевдоэлементы контейнера: `spacerStyle` отдаёт\n * их высоты переменными, а правило живёт в `<style>` компонента. Так\n * отрисованные строки остаются **прямыми детьми** контейнера, и роль вроде\n * `tree` или `listbox` не теряет обязательных потомков.\n *\n * ```vue\n * const container = useTemplateRef('scroller')\n * const { range, spacerStyle, measure } = useVirtualList({\n * container,\n * count: () => rows.value.length,\n * itemSize: 28,\n * viewportSize: () => props.maxHeight,\n * })\n *\n * <div\n * ref=\"scroller\"\n * role=\"tree\"\n * data-gr-virtual\n * :style=\"{ overflow: 'auto', maxHeight: '280px', ...spacerStyle }\"\n * >\n * <Row v-for=\"i in window\" :key=\"…\" :ref=\"el => measure(i, el)\" />\n * </div>\n *\n * <style scoped>\n * [data-gr-virtual]::before,\n * [data-gr-virtual]::after { content: ''; display: block; flex: none; }\n * [data-gr-virtual]::before { height: var(--gr-virtual-before, 0px); }\n * [data-gr-virtual]::after { height: var(--gr-virtual-after, 0px); }\n * </style>\n * ```\n *\n * Почему правило повторяется в каждом компоненте, а не лежит в одном файле:\n * единственный глобальный стиль пакета (`styles/base.css`) потребитель вправе\n * не подключать, и уехавшее туда правило молча ломало бы прокрутку. Сверяет\n * копии гейт `src/__tests__/virtualSpacer.test.ts`.\n */\n\n/** Куда поставить элемент относительно вьюпорта при прокрутке к нему. */\nexport type GrVirtualAlign = 'auto' | 'start' | 'center' | 'end'\n\nexport interface UseVirtualListOptions {\n /** Скроллящийся контейнер. До монтирования — `null`. */\n container: Ref<HTMLElement | null>\n /** Сколько элементов в списке. */\n count: () => number\n /**\n * Оценка высоты элемента. Фактическая берётся из замера, как только элемент\n * отрисован, — строки пакета заданы через `min-height`, и точного числа тут\n * знать неоткуда.\n */\n itemSize: number | ((index: number) => number)\n /**\n * Высота вьюпорта до первого замера контейнера. Нужна, чтобы **первый рендер\n * был детерминированным**: на сервере контейнера нет вовсе, а на клиенте он\n * ещё не измерен. Без неё сервер и клиент нарисовали бы разные окна и\n * гидрация разъехалась бы; с ней оба считают окно от одного числа.\n */\n viewportSize?: () => number | undefined\n /**\n * Идентити-токен данных списка — обычно сам массив. Смена идентичности\n * сбрасывает все замеры (`invalidate()`): они привязаны к индексам, и после\n * сортировки или замены массива хранили бы высоты чужих строк.\n *\n * Правило потребителя: **мутация массива на месте сохраняет замеры, замена\n * массива сбрасывает**. Append в тот же реактивный массив (инфинити-скролл)\n * идентичность не меняет — префикс замеров остаётся валидным.\n */\n source?: () => unknown\n /**\n * Зазор между элементами в пикселях (`gap` у flex/grid-контейнера).\n *\n * Замер элемента его не видит — `getBoundingClientRect` меряет коробку, а не\n * шаг раскладки. Без этого числа список считался бы короче, чем он есть,\n * и окно уползало бы тем сильнее, чем дальше прокрутка.\n */\n gap?: number\n /** Сколько элементов рисовать за пределами вьюпорта. По умолчанию `4`. */\n overscan?: number\n}\n\nexport interface UseVirtualListReturn {\n /** Полуинтервал индексов к отрисовке: `[start, end)`. */\n range: ComputedRef<{ start: number, end: number }>\n /** Полная высота содержимого: распорка, которая держит скроллбар. */\n totalSize: ComputedRef<number>\n /** Отступ сверху: высота срезанного до окна. */\n offset: ComputedRef<number>\n /** Отступ снизу: высота срезанного после окна. */\n offsetEnd: ComputedRef<number>\n /**\n * Стиль контейнера: высоты распорок и выключенный scroll anchoring.\n *\n * Имена переменных приезжают готовыми, поэтому написать их руками — и\n * разойтись с правилом в `<style>` — нельзя. Опечатка в имени давала бы\n * список без прокрутки, и увидеть это можно было бы только глазами.\n */\n spacerStyle: ComputedRef<Record<string, string>>\n /** Регистрация отрисованного элемента: замер и подписка на изменение высоты. */\n measure: (index: number, el: Element | null) => void\n /** Прокрутка к элементу. Без выравнивания — минимальным движением. */\n scrollToIndex: (index: number, align?: GrVirtualAlign) => void\n /**\n * Сбрасывает замеры с индекса `from` (по умолчанию — все) к оценкам.\n * Видимые строки перемеряются первым же рендером; для смены данных обычно\n * достаточно опции `source` — ручной вызов нужен, когда высоты строк\n * изменились без замены массива (смена плотности, ресайз колонок).\n */\n invalidate: (from?: number) => void\n}\n\nconst DEFAULT_OVERSCAN = 4\n\n/** Порог, ниже которого разницу замеров считаем шумом округления. */\nconst SIZE_EPSILON = 0.5\n\nfunction readSize(el: Element): number {\n const box = el.getBoundingClientRect().height\n if (box > 0)\n return box\n\n // jsdom не считает layout: `getBoundingClientRect` там всегда нулевой, а\n // `offsetHeight` тест может задать сам.\n return (el as HTMLElement).offsetHeight ?? 0\n}\n\nexport function useVirtualList(options: UseVirtualListOptions): UseVirtualListReturn {\n const overscan = Math.max(0, Math.trunc(options.overscan ?? DEFAULT_OVERSCAN))\n const gap = Math.max(0, options.gap ?? 0)\n const estimateOf = typeof options.itemSize === 'function'\n ? options.itemSize\n : () => options.itemSize as number\n\n const scrollTop = ref(0)\n const measuredViewport = ref(0)\n\n const measured = new Map<number, number>()\n const observedIndex = new WeakMap<Element, number>()\n\n // `ResizeObserver` нет ни на сервере, ни в jsdom. Без него композабл работает\n // на оценках и явных замерах из `measure()` — это рабочий режим, а не отказ.\n const hasResizeObserver = typeof ResizeObserver !== 'undefined'\n const sizeObserver = hasResizeObserver\n ? new ResizeObserver((entries) => {\n for (const entry of entries) {\n const index = observedIndex.get(entry.target)\n if (index !== undefined)\n applySize(index, readSize(entry.target))\n }\n })\n : null\n const viewportObserver = hasResizeObserver ? new ResizeObserver(() => syncViewport()) : null\n let attachedTo: HTMLElement | null = null\n let offsets: number[] = [0]\n /** До какого индекса префикс-суммы актуальны. */\n let builtTo = 0\n\n /** Шаг раскладки: коробка элемента плюс зазор до следующего. */\n function sizeAt(index: number): number {\n return (measured.get(index) ?? estimateOf(index)) + gap\n }\n\n /**\n * Достраивает префикс-суммы до нужного индекса.\n *\n * Замер инвалидирует хвост от своего индекса, а не весь массив: иначе каждая\n * отрисованная строка стоила бы полного пересчёта по всему списку.\n */\n function buildTo(index: number): void {\n const limit = Math.min(index, options.count())\n while (builtTo < limit) {\n offsets[builtTo + 1] = offsets[builtTo] + sizeAt(builtTo)\n builtTo += 1\n }\n }\n\n function offsetAt(index: number): number {\n buildTo(index)\n return offsets[Math.min(Math.max(index, 0), builtTo)] ?? 0\n }\n\n function invalidateFrom(index: number): void {\n if (index < builtTo)\n builtTo = Math.max(0, index)\n }\n\n /** Последний индекс, чьё начало не ниже `px`. */\n function indexAt(px: number): number {\n const count = options.count()\n if (count === 0)\n return 0\n\n buildTo(count)\n\n let low = 0\n let high = builtTo\n while (low < high) {\n const mid = (low + high + 1) >> 1\n if ((offsets[mid] ?? 0) <= px)\n low = mid\n else high = mid - 1\n }\n\n return Math.min(low, count - 1)\n }\n\n function viewportSize(): number {\n return measuredViewport.value || options.viewportSize?.() || 0\n }\n\n /**\n * Геометрия окна одним снимком.\n *\n * Считается явно, а не `computed`'ом поверх смещений: массив префикс-сумм\n * мутируется на месте (иначе каждый замер пересоздавал бы десять тысяч чисел),\n * и вывести из него реактивную зависимость нечем — трекер увидел бы только те\n * индексы, которые пересчитывались в последний проход.\n */\n const geometry = ref({ start: 0, end: 0, offset: 0, offsetEnd: 0, total: 0 })\n\n function recompute(): void {\n const count = options.count()\n if (count === 0) {\n geometry.value = { start: 0, end: 0, offset: 0, offsetEnd: 0, total: 0 }\n return\n }\n\n const totalSize = offsetAt(count)\n const height = viewportSize()\n\n // Высота неизвестна (контейнера нет и оценки не дали) — рисуем всё: меньше\n // «списка целиком» отрисовать нечего без риска, что до замера так и не\n // дойдёт ни одна строка.\n if (height <= 0) {\n geometry.value = { start: 0, end: count, offset: 0, offsetEnd: 0, total: totalSize }\n return\n }\n\n const start = Math.max(0, indexAt(scrollTop.value) - overscan)\n const end = Math.min(count, indexAt(scrollTop.value + height) + 1 + overscan)\n\n geometry.value = {\n start,\n end,\n offset: offsetAt(start),\n offsetEnd: Math.max(0, totalSize - offsetAt(end)),\n total: totalSize,\n }\n }\n\n const range = computed(() => ({ start: geometry.value.start, end: geometry.value.end }))\n const total = computed(() => geometry.value.total)\n const offset = computed(() => geometry.value.offset)\n const offsetEnd = computed(() => geometry.value.offsetEnd)\n\n const spacerStyle = computed(() => ({\n // Браузерный scroll anchoring обязан быть выключен. Он удерживает видимый\n // узел на месте, подправляя `scrollTop`, когда меняется высота содержимого\n // выше него, — а окно меняет её на каждом кадре прокрутки. Дальше пересчёт\n // окна и якорь начинают двигать список навстречу друг другу, и прокрутка\n // уезжает тем сильнее, чем дальше оценка строки от фактической высоты.\n 'overflow-anchor': 'none',\n '--gr-virtual-before': `${geometry.value.offset}px`,\n '--gr-virtual-after': `${geometry.value.offsetEnd}px`,\n }))\n\n function syncScrollTop(): void {\n const el = options.container.value\n if (!el)\n return\n\n scrollTop.value = el.scrollTop\n recompute()\n }\n\n function syncViewport(): void {\n const el = options.container.value\n if (!el)\n return\n\n measuredViewport.value = el.clientHeight\n recompute()\n }\n\n function measure(index: number, el: Element | null): void {\n if (!el)\n return\n\n observedIndex.set(el, index)\n sizeObserver?.observe(el)\n\n applySize(index, readSize(el))\n }\n\n function applySize(index: number, size: number): void {\n if (size <= 0)\n return\n\n const previous = measured.get(index)\n if (previous !== undefined && Math.abs(previous - size) < SIZE_EPSILON)\n return\n\n // Считаем смещение ДО правки: строка ниже вьюпорта позицию не двигает, а\n // строка выше — двигает всё содержимое под курсором.\n const start = offsetAt(index)\n const delta = size - (previous ?? estimateOf(index))\n\n measured.set(index, size)\n invalidateFrom(index)\n\n const el = options.container.value\n // Компенсация: без неё уточнение высоты уже прокрученных строк выглядит как\n // рывок списка вверх или вниз ровно в момент замера.\n if (delta !== 0 && start < scrollTop.value && el) {\n el.scrollTop += delta\n scrollTop.value = el.scrollTop\n }\n\n recompute()\n }\n\n function scrollToIndex(index: number, align: GrVirtualAlign = 'auto'): void {\n const el = options.container.value\n const count = options.count()\n if (!el || count === 0)\n return\n\n const clamped = Math.min(Math.max(index, 0), count - 1)\n const start = offsetAt(clamped)\n const size = sizeAt(clamped)\n const height = el.clientHeight || viewportSize()\n const end = start + size\n\n let next: number\n switch (align) {\n case 'start':\n next = start\n break\n case 'end':\n next = end - height\n break\n case 'center':\n next = start + size / 2 - height / 2\n break\n case 'auto':\n if (start < el.scrollTop)\n next = start\n else if (end > el.scrollTop + height)\n next = end - height\n else return\n break\n }\n\n el.scrollTop = Math.max(0, Math.min(next, offsetAt(count) - height))\n scrollTop.value = el.scrollTop\n recompute()\n }\n\n function detach(): void {\n if (!attachedTo)\n return\n attachedTo.removeEventListener('scroll', syncScrollTop)\n viewportObserver?.unobserve(attachedTo)\n attachedTo = null\n }\n\n // Первый снимок до всякого монтирования: на сервере и на первом клиентском\n // рендере контейнера ещё нет, а окно уже должно быть посчитано — и одинаково.\n recompute()\n\n watch(options.container, (el) => {\n detach()\n if (!el)\n return\n\n attachedTo = el\n el.addEventListener('scroll', syncScrollTop, { passive: true })\n viewportObserver?.observe(el)\n syncViewport()\n syncScrollTop()\n }, { immediate: true })\n\n // Длина списка изменилась. Сокращение дополнительно чистит хвост смещений и\n // замеров: он относится к строкам, которых больше нет.\n watch(() => options.count(), (count) => {\n if (count < builtTo)\n truncate(count)\n recompute()\n })\n\n function invalidate(from = 0): void {\n for (const index of [...measured.keys()]) {\n if (index >= from)\n measured.delete(index)\n }\n invalidateFrom(from)\n recompute()\n }\n\n if (options.source) {\n // Идентичность данных сменилась — замеры принадлежат прошлому набору.\n watch(options.source, () => invalidate())\n }\n\n function truncate(count: number): void {\n builtTo = count\n offsets = offsets.slice(0, count + 1)\n for (const index of [...measured.keys()]) {\n if (index >= count)\n measured.delete(index)\n }\n }\n\n onScopeDispose(() => {\n detach()\n sizeObserver?.disconnect()\n viewportObserver?.disconnect()\n })\n\n return { range, totalSize: total, offset, offsetEnd, spacerStyle, measure, scrollToIndex, invalidate }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"autofocus.js","names":[],"sources":["../../src/directives/autofocus.ts"],"sourcesContent":["import type { Directive } from 'vue'\nimport { nextTick } from 'vue'\n\nexport type AutofocusBindingValue =\n | boolean\n | string\n | {\n selector?: string\n preventScroll?: boolean\n disabled?: boolean\n }\n\nfunction resolveOptions(value: AutofocusBindingValue | undefined) {\n if (value === false) {\n return { enabled: false, selector: '', preventScroll: true }\n }\n\n if (typeof value === 'string') {\n return { enabled: true, selector: value, preventScroll: true }\n }\n\n if (value && typeof value === 'object') {\n return {\n enabled: value.disabled !== true,\n selector: value.selector,\n preventScroll: value.preventScroll ?? true,\n }\n }\n\n return { enabled: true, selector: undefined as string | undefined, preventScroll: true }\n}\n\nfunction tryFocus(target: Element | null | undefined, preventScroll: boolean) {\n if (!target) return\n\n const focusable = target as unknown as { focus?: (options?: any) => void }\n if (typeof focusable.focus !== 'function') return\n\n try {\n focusable.focus({ preventScroll })\n } catch {\n // Fallback for browsers that don't support FocusOptions.\n focusable.focus()\n }\n}\n\n/**\n * `v-autofocus` — универсальный автофокус.\n * Работает как на нативных элементах, так и на компонентах-обёртках (фокусирует вложенный `input/textarea/...`).\n */\nexport const vAutofocus: Directive<HTMLElement, AutofocusBindingValue> = {\n mounted(el, binding) {\n const { enabled, selector, preventScroll } = resolveOptions(binding.value)\n if (!enabled) return\n\n void nextTick().then(() => {\n // Небольшая задержка помогает, когда внутренний input появляется после обновления/анимаций.\n requestAnimationFrame(() => {\n const defaultSelector = 'input, textarea, select, button, [tabindex]:not([tabindex=\"-1\"])'\n const finalSelector = selector || defaultSelector\n\n if ((el as any).matches?.(finalSelector)) {\n tryFocus(el, preventScroll)\n return\n }\n\n const inner = el.querySelector?.(finalSelector)\n tryFocus(inner, preventScroll)\n })\n })\n },\n}\n"],"mappings":";;AAYA,SAAS,eAAe,OAA0C;CAChE,IAAI,UAAU,OACZ,OAAO;EAAE,SAAS;EAAO,UAAU;EAAI,eAAe;CAAK;CAG7D,IAAI,OAAO,UAAU,UACnB,OAAO;EAAE,SAAS;EAAM,UAAU;EAAO,eAAe;CAAK;CAG/D,IAAI,SAAS,OAAO,UAAU,UAC5B,OAAO;EACL,SAAS,MAAM,aAAa;EAC5B,UAAU,MAAM;EAChB,eAAe,MAAM,iBAAiB;CACxC;CAGF,OAAO;EAAE,SAAS;EAAM,UAAU,KAAA;EAAiC,eAAe;CAAK;AACzF;AAEA,SAAS,SAAS,QAAoC,eAAwB;CAC5E,IAAI,CAAC,QAAQ;CAEb,MAAM,YAAY;CAClB,IAAI,OAAO,UAAU,UAAU,YAAY;CAE3C,IAAI;EACF,UAAU,MAAM,EAAE,cAAc,CAAC;CACnC,QAAQ;EAEN,UAAU,MAAM;CAClB;AACF;;;;;AAMA,IAAa,aAA4D,EACvE,QAAQ,IAAI,SAAS;CACnB,MAAM,EAAE,SAAS,UAAU,kBAAkB,eAAe,QAAQ,KAAK;CACzE,IAAI,CAAC,SAAS;CAEd,SAAc,CAAC,CAAC,WAAW;EAEzB,4BAA4B;GAE1B,MAAM,gBAAgB,YAAY;GAElC,IAAK,GAAW,UAAU,aAAa,GAAG;IACxC,SAAS,IAAI,aAAa;IAC1B;GACF;GAEA,MAAM,QAAQ,GAAG,gBAAgB,aAAa;GAC9C,SAAS,OAAO,aAAa;EAC/B,CAAC;CACH,CAAC;AACH,EACF"}
1
+ {"version":3,"file":"autofocus.js","names":[],"sources":["../../src/directives/autofocus.ts"],"sourcesContent":["import type { Directive } from 'vue'\nimport { nextTick } from 'vue'\n\nexport type AutofocusBindingValue\n = | boolean\n | string\n | {\n selector?: string\n preventScroll?: boolean\n disabled?: boolean\n }\n\nfunction resolveOptions(value: AutofocusBindingValue | undefined) {\n if (value === false) {\n return { enabled: false, selector: '', preventScroll: true }\n }\n\n if (typeof value === 'string') {\n return { enabled: true, selector: value, preventScroll: true }\n }\n\n if (value && typeof value === 'object') {\n return {\n enabled: value.disabled !== true,\n selector: value.selector,\n preventScroll: value.preventScroll ?? true,\n }\n }\n\n return { enabled: true, selector: undefined as string | undefined, preventScroll: true }\n}\n\nfunction tryFocus(target: Element | null | undefined, preventScroll: boolean) {\n if (!target)\n return\n\n const focusable = target as unknown as { focus?: (options?: any) => void }\n if (typeof focusable.focus !== 'function')\n return\n\n try {\n focusable.focus({ preventScroll })\n }\n catch {\n // Fallback for browsers that don't support FocusOptions.\n focusable.focus()\n }\n}\n\n/**\n * `v-autofocus` — универсальный автофокус.\n * Работает как на нативных элементах, так и на компонентах-обёртках (фокусирует вложенный `input/textarea/...`).\n */\nexport const vAutofocus: Directive<HTMLElement, AutofocusBindingValue> = {\n mounted(el, binding) {\n const { enabled, selector, preventScroll } = resolveOptions(binding.value)\n if (!enabled)\n return\n\n void nextTick().then(() => {\n // Небольшая задержка помогает, когда внутренний input появляется после обновления/анимаций.\n requestAnimationFrame(() => {\n const defaultSelector = 'input, textarea, select, button, [tabindex]:not([tabindex=\"-1\"])'\n const finalSelector = selector || defaultSelector\n\n if ((el as any).matches?.(finalSelector)) {\n tryFocus(el, preventScroll)\n return\n }\n\n const inner = el.querySelector?.(finalSelector)\n tryFocus(inner, preventScroll)\n })\n })\n },\n}\n"],"mappings":";;AAYA,SAAS,eAAe,OAA0C;CAChE,IAAI,UAAU,OACZ,OAAO;EAAE,SAAS;EAAO,UAAU;EAAI,eAAe;CAAK;CAG7D,IAAI,OAAO,UAAU,UACnB,OAAO;EAAE,SAAS;EAAM,UAAU;EAAO,eAAe;CAAK;CAG/D,IAAI,SAAS,OAAO,UAAU,UAC5B,OAAO;EACL,SAAS,MAAM,aAAa;EAC5B,UAAU,MAAM;EAChB,eAAe,MAAM,iBAAiB;CACxC;CAGF,OAAO;EAAE,SAAS;EAAM,UAAU,KAAA;EAAiC,eAAe;CAAK;AACzF;AAEA,SAAS,SAAS,QAAoC,eAAwB;CAC5E,IAAI,CAAC,QACH;CAEF,MAAM,YAAY;CAClB,IAAI,OAAO,UAAU,UAAU,YAC7B;CAEF,IAAI;EACF,UAAU,MAAM,EAAE,cAAc,CAAC;CACnC,QACM;EAEJ,UAAU,MAAM;CAClB;AACF;;;;;AAMA,IAAa,aAA4D,EACvE,QAAQ,IAAI,SAAS;CACnB,MAAM,EAAE,SAAS,UAAU,kBAAkB,eAAe,QAAQ,KAAK;CACzE,IAAI,CAAC,SACH;CAEF,SAAc,CAAC,CAAC,WAAW;EAEzB,4BAA4B;GAE1B,MAAM,gBAAgB,YAAY;GAElC,IAAK,GAAW,UAAU,aAAa,GAAG;IACxC,SAAS,IAAI,aAAa;IAC1B;GACF;GAEA,MAAM,QAAQ,GAAG,gBAAgB,aAAa;GAC9C,SAAS,OAAO,aAAa;EAC/B,CAAC;CACH,CAAC;AACH,EACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"autosize.js","names":[],"sources":["../../src/directives/autosize.ts"],"sourcesContent":["import type { Directive } from 'vue'\nimport { nextTick } from 'vue'\n\nexport type AutosizeBindingValue =\n | boolean\n | {\n enabled?: boolean\n }\n\ntype InternalState = {\n textarea: HTMLTextAreaElement | null\n onInput: (() => void) | null\n prevOverflowY: string | null\n}\n\nconst states = new WeakMap<HTMLElement, InternalState>()\n\nfunction normalize(value: AutosizeBindingValue | undefined) {\n if (value === false) return { enabled: false }\n if (value && typeof value === 'object') return { enabled: value.enabled ?? true }\n return { enabled: true }\n}\n\nfunction resolveTextarea(el: HTMLElement): HTMLTextAreaElement | null {\n if (el instanceof HTMLTextAreaElement) return el\n return el.querySelector('textarea')\n}\n\nfunction toNumber(input: string): number {\n const n = Number.parseFloat(input)\n return Number.isFinite(n) ? n : 0\n}\n\nfunction autosize(textarea: HTMLTextAreaElement) {\n // Важно сбросить, чтобы textarea могла уменьшаться при удалении текста.\n textarea.style.height = 'auto'\n\n const style = getComputedStyle(textarea)\n const border = toNumber(style.borderTopWidth) + toNumber(style.borderBottomWidth)\n const height = style.boxSizing === 'border-box' ? textarea.scrollHeight + border : textarea.scrollHeight\n\n textarea.style.height = `${height}px`\n}\n\nfunction scheduleAutosize(textarea: HTMLTextAreaElement) {\n void nextTick().then(() => {\n requestAnimationFrame(() => autosize(textarea))\n })\n}\n\n/**\n * `v-autosize` — автоподстройка высоты `textarea`.\n *\n * Пример:\n * ```vue\n * <textarea v-autosize />\n * ```\n */\nexport const vAutosize: Directive<HTMLElement, AutosizeBindingValue> = {\n mounted(el, binding) {\n const { enabled } = normalize(binding.value)\n const textarea = resolveTextarea(el)\n\n const state: InternalState = {\n textarea,\n onInput: null,\n prevOverflowY: textarea ? textarea.style.overflowY : null,\n }\n states.set(el, state)\n\n if (!enabled || !textarea) return\n\n textarea.style.overflowY = 'hidden'\n autosize(textarea)\n scheduleAutosize(textarea)\n\n const onInput = () => {\n const current = states.get(el)\n if (!current?.textarea) return\n autosize(current.textarea)\n }\n\n state.onInput = onInput\n textarea.addEventListener('input', onInput)\n },\n updated(el, binding) {\n const { enabled } = normalize(binding.value)\n const state = states.get(el)\n if (!state) return\n\n const nextTextarea = resolveTextarea(el)\n if (state.textarea !== nextTextarea) {\n if (state.textarea && state.onInput) {\n state.textarea.removeEventListener('input', state.onInput)\n }\n\n state.textarea = nextTextarea\n state.prevOverflowY = nextTextarea ? nextTextarea.style.overflowY : null\n state.onInput = null\n\n if (enabled && nextTextarea) {\n nextTextarea.style.overflowY = 'hidden'\n autosize(nextTextarea)\n scheduleAutosize(nextTextarea)\n\n const onInput = () => {\n const current = states.get(el)\n if (!current?.textarea) return\n autosize(current.textarea)\n }\n state.onInput = onInput\n nextTextarea.addEventListener('input', onInput)\n }\n\n return\n }\n\n if (!state.textarea) return\n\n if (!enabled) {\n // Disabled in-flight: detach listener and restore previous overflow.\n if (state.onInput) {\n state.textarea.removeEventListener('input', state.onInput)\n state.onInput = null\n }\n if (state.prevOverflowY !== null) {\n state.textarea.style.overflowY = state.prevOverflowY\n }\n return\n }\n\n // Re-enable after being disabled: reattach listener and re-apply overflow.\n if (!state.onInput) {\n state.prevOverflowY = state.textarea.style.overflowY\n state.textarea.style.overflowY = 'hidden'\n\n const onInput = () => {\n const current = states.get(el)\n if (!current?.textarea) return\n autosize(current.textarea)\n }\n state.onInput = onInput\n state.textarea.addEventListener('input', onInput)\n }\n\n autosize(state.textarea)\n scheduleAutosize(state.textarea)\n },\n unmounted(el) {\n const state = states.get(el)\n if (!state) return\n\n if (state.textarea && state.onInput) {\n state.textarea.removeEventListener('input', state.onInput)\n if (state.prevOverflowY !== null) state.textarea.style.overflowY = state.prevOverflowY\n }\n\n states.delete(el)\n },\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"autosize.js","names":[],"sources":["../../src/directives/autosize.ts"],"sourcesContent":["import type { Directive } from 'vue'\nimport { nextTick } from 'vue'\n\nexport type AutosizeBindingValue\n = | boolean\n | {\n enabled?: boolean\n }\n\ntype InternalState = {\n textarea: HTMLTextAreaElement | null\n onInput: (() => void) | null\n prevOverflowY: string | null\n}\n\nconst states = new WeakMap<HTMLElement, InternalState>()\n\nfunction normalize(value: AutosizeBindingValue | undefined) {\n if (value === false)\n return { enabled: false }\n if (value && typeof value === 'object')\n return { enabled: value.enabled ?? true }\n return { enabled: true }\n}\n\nfunction resolveTextarea(el: HTMLElement): HTMLTextAreaElement | null {\n if (el instanceof HTMLTextAreaElement)\n return el\n return el.querySelector('textarea')\n}\n\nfunction toNumber(input: string): number {\n const n = Number.parseFloat(input)\n return Number.isFinite(n) ? n : 0\n}\n\nfunction autosize(textarea: HTMLTextAreaElement) {\n // Важно сбросить, чтобы textarea могла уменьшаться при удалении текста.\n textarea.style.height = 'auto'\n\n const style = getComputedStyle(textarea)\n const border = toNumber(style.borderTopWidth) + toNumber(style.borderBottomWidth)\n const height = style.boxSizing === 'border-box' ? textarea.scrollHeight + border : textarea.scrollHeight\n\n textarea.style.height = `${height}px`\n}\n\nfunction scheduleAutosize(textarea: HTMLTextAreaElement) {\n void nextTick().then(() => {\n requestAnimationFrame(() => autosize(textarea))\n })\n}\n\n/**\n * `v-autosize` — автоподстройка высоты `textarea`.\n *\n * Пример:\n * ```vue\n * <textarea v-autosize />\n * ```\n */\nexport const vAutosize: Directive<HTMLElement, AutosizeBindingValue> = {\n mounted(el, binding) {\n const { enabled } = normalize(binding.value)\n const textarea = resolveTextarea(el)\n\n const state: InternalState = {\n textarea,\n onInput: null,\n prevOverflowY: textarea ? textarea.style.overflowY : null,\n }\n states.set(el, state)\n\n if (!enabled || !textarea)\n return\n\n textarea.style.overflowY = 'hidden'\n autosize(textarea)\n scheduleAutosize(textarea)\n\n const onInput = () => {\n const current = states.get(el)\n if (!current?.textarea)\n return\n autosize(current.textarea)\n }\n\n state.onInput = onInput\n textarea.addEventListener('input', onInput)\n },\n updated(el, binding) {\n const { enabled } = normalize(binding.value)\n const state = states.get(el)\n if (!state)\n return\n\n const nextTextarea = resolveTextarea(el)\n if (state.textarea !== nextTextarea) {\n if (state.textarea && state.onInput) {\n state.textarea.removeEventListener('input', state.onInput)\n }\n\n state.textarea = nextTextarea\n state.prevOverflowY = nextTextarea ? nextTextarea.style.overflowY : null\n state.onInput = null\n\n if (enabled && nextTextarea) {\n nextTextarea.style.overflowY = 'hidden'\n autosize(nextTextarea)\n scheduleAutosize(nextTextarea)\n\n const onInput = () => {\n const current = states.get(el)\n if (!current?.textarea)\n return\n autosize(current.textarea)\n }\n state.onInput = onInput\n nextTextarea.addEventListener('input', onInput)\n }\n\n return\n }\n\n if (!state.textarea)\n return\n\n if (!enabled) {\n // Disabled in-flight: detach listener and restore previous overflow.\n if (state.onInput) {\n state.textarea.removeEventListener('input', state.onInput)\n state.onInput = null\n }\n if (state.prevOverflowY !== null) {\n state.textarea.style.overflowY = state.prevOverflowY\n }\n return\n }\n\n // Re-enable after being disabled: reattach listener and re-apply overflow.\n if (!state.onInput) {\n state.prevOverflowY = state.textarea.style.overflowY\n state.textarea.style.overflowY = 'hidden'\n\n const onInput = () => {\n const current = states.get(el)\n if (!current?.textarea)\n return\n autosize(current.textarea)\n }\n state.onInput = onInput\n state.textarea.addEventListener('input', onInput)\n }\n\n autosize(state.textarea)\n scheduleAutosize(state.textarea)\n },\n unmounted(el) {\n const state = states.get(el)\n if (!state)\n return\n\n if (state.textarea && state.onInput) {\n state.textarea.removeEventListener('input', state.onInput)\n if (state.prevOverflowY !== null)\n state.textarea.style.overflowY = state.prevOverflowY\n }\n\n states.delete(el)\n },\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"clickOutside.js","names":[],"sources":["../../src/directives/clickOutside.ts"],"sourcesContent":["import type { Directive } from 'vue'\n\nexport type ClickOutsideHandler = (event: MouseEvent | TouchEvent | PointerEvent) => void\n\nexport type ClickOutsideExclude = HTMLElement | string | (() => HTMLElement | null | undefined)\n\nexport type ClickOutsideBindingValue =\n | ClickOutsideHandler\n | {\n handler: ClickOutsideHandler\n /** По умолчанию `true`. */\n enabled?: boolean\n /** По умолчанию `true` (слушаем в capture, чтобы stopPropagation внутри не ломал закрытие). */\n capture?: boolean\n /** Список событий. По умолчанию `['click']`. */\n events?: Array<'click' | 'mousedown' | 'touchstart' | 'pointerdown'>\n /** Элементы/селекторы, клики по которым считаются \"внутри\". */\n exclude?: ClickOutsideExclude[]\n }\n\ntype NormalizedOptions = {\n enabled: boolean\n capture: boolean\n events: Array<'click' | 'mousedown' | 'touchstart' | 'pointerdown'>\n handler: ClickOutsideHandler | undefined\n exclude: ClickOutsideExclude[]\n}\n\nfunction normalize(value: ClickOutsideBindingValue | undefined): NormalizedOptions {\n if (typeof value === 'function') {\n return { enabled: true, capture: true, events: ['click'], handler: value, exclude: [] }\n }\n\n if (value && typeof value === 'object') {\n return {\n enabled: value.enabled ?? true,\n capture: value.capture ?? true,\n events: value.events?.length ? value.events : ['click'],\n handler: value.handler,\n exclude: value.exclude ?? [],\n }\n }\n\n return { enabled: false, capture: true, events: ['click'], handler: undefined, exclude: [] }\n}\n\nfunction isEventInside(el: HTMLElement, event: Event): boolean {\n const target = event.target as Node | null\n if (!target) return false\n\n const composedPath = (event as any).composedPath?.() as EventTarget[] | undefined\n if (Array.isArray(composedPath)) {\n return composedPath.includes(el)\n }\n\n return el.contains(target)\n}\n\nfunction resolveExclude(doc: Document, exclude: ClickOutsideExclude[]): HTMLElement[] {\n const resolved: HTMLElement[] = []\n\n for (const item of exclude) {\n if (typeof item === 'string') {\n const found = doc.querySelector(item)\n if (found instanceof HTMLElement) resolved.push(found)\n continue\n }\n\n if (typeof item === 'function') {\n const found = item()\n if (found instanceof HTMLElement) resolved.push(found)\n continue\n }\n\n if (item instanceof HTMLElement) {\n resolved.push(item)\n }\n }\n\n return resolved\n}\n\ntype ElementState = {\n options: NormalizedOptions\n doc: Document\n /** Ключи подписок, которые этот элемент держит на своём документе. */\n keys: string[]\n}\n\nconst states = new WeakMap<HTMLElement, ElementState>()\n\n/**\n * Слушатель — один на документ и вид события, а не на элемент.\n *\n * Каждый элемент со своим слушателем — это десяток подписок на документе у\n * страницы с десятком оверлеев, и полное переподключение на каждом обновлении\n * хозяина: хук `updated` срабатывает на любой ререндер, а набор событий при\n * этом обычно тот же. Здесь подписка живёт по счётчику потребителей, а элементы\n * обходятся общим обработчиком.\n */\nconst documentElements = new WeakMap<Document, Set<HTMLElement>>()\nconst documentSubscriptions = new WeakMap<Document, Map<string, { listener: (event: Event) => void, count: number }>>()\n\nfunction subscriptionKey(type: string, capture: boolean): string {\n return `${type}|${capture ? 'capture' : 'bubble'}`\n}\n\nfunction keysOf(options: NormalizedOptions): string[] {\n if (!options.enabled || !options.handler) return []\n\n return options.events.map(type => subscriptionKey(type, options.capture))\n}\n\nfunction dispatch(doc: Document, event: Event, capture: boolean): void {\n const elements = documentElements.get(doc)\n if (!elements) return\n\n // Копия: обработчик закрывает оверлей, а тот на размонтировании правит набор.\n for (const el of [...elements]) {\n const state = states.get(el)\n if (!state) continue\n\n const { enabled, handler, exclude, events } = state.options\n if (!enabled || !handler) continue\n if (state.options.capture !== capture) continue\n if (!events.includes(event.type as NormalizedOptions['events'][number])) continue\n\n if ('button' in (event as any) && typeof (event as any).button === 'number' && (event as any).button !== 0) {\n continue\n }\n\n if (!el.isConnected) continue\n if (isEventInside(el, event)) continue\n\n const excluded = resolveExclude(state.doc, exclude)\n if (excluded.some(item => isEventInside(item, event))) continue\n\n handler(event as any)\n }\n}\n\nfunction subscribe(doc: Document, key: string): void {\n let subscriptions = documentSubscriptions.get(doc)\n if (!subscriptions) {\n subscriptions = new Map()\n documentSubscriptions.set(doc, subscriptions)\n }\n\n const existing = subscriptions.get(key)\n if (existing) {\n existing.count += 1\n return\n }\n\n const [type, mode] = key.split('|')\n const capture = mode === 'capture'\n const listener = (event: Event) => dispatch(doc, event, capture)\n\n subscriptions.set(key, { listener, count: 1 })\n doc.addEventListener(type, listener, capture)\n}\n\nfunction unsubscribe(doc: Document, key: string): void {\n const subscriptions = documentSubscriptions.get(doc)\n const existing = subscriptions?.get(key)\n if (!subscriptions || !existing) return\n\n existing.count -= 1\n if (existing.count > 0) return\n\n const [type, mode] = key.split('|')\n doc.removeEventListener(type, existing.listener, mode === 'capture')\n subscriptions.delete(key)\n}\n\nfunction unbind(el: HTMLElement) {\n const state = states.get(el)\n if (!state) return\n\n for (const key of state.keys) unsubscribe(state.doc, key)\n\n documentElements.get(state.doc)?.delete(el)\n states.delete(el)\n}\n\nfunction bind(el: HTMLElement, value: ClickOutsideBindingValue | undefined) {\n const doc = el.ownerDocument\n const previous = states.get(el)\n\n // Элемент, переехавший в другой документ (портал в отдельное окно), обязан\n // отпустить подписки прежнего: там его больше нет.\n if (previous && previous.doc !== doc) unbind(el)\n\n const options = normalize(value)\n const nextKeys = keysOf(options)\n const prevKeys = states.get(el)?.keys ?? []\n\n states.set(el, { options, doc, keys: nextKeys })\n\n let elements = documentElements.get(doc)\n if (!elements) {\n elements = new Set()\n documentElements.set(doc, elements)\n }\n elements.add(el)\n\n for (const key of nextKeys) if (!prevKeys.includes(key)) subscribe(doc, key)\n for (const key of prevKeys) if (!nextKeys.includes(key)) unsubscribe(doc, key)\n}\n\n/**\n * `v-click-outside` — обработчик клика вне элемента (например, закрытие дропдауна/модалки).\n */\nexport const vClickOutside: Directive<HTMLElement, ClickOutsideBindingValue> = {\n mounted(el, binding) {\n bind(el, binding.value)\n },\n updated(el, binding) {\n // Не `unbind` + `bind`: разницу подписок считает сам `bind`, а снятие с\n // последующей постановкой того же слушателя — лишняя работа на каждый\n // ререндер хозяина.\n bind(el, binding.value)\n },\n unmounted(el) {\n unbind(el)\n },\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"clickOutside.js","names":[],"sources":["../../src/directives/clickOutside.ts"],"sourcesContent":["import type { Directive } from 'vue'\n\nexport type ClickOutsideHandler = (event: MouseEvent | TouchEvent | PointerEvent) => void\n\nexport type ClickOutsideExclude = HTMLElement | string | (() => HTMLElement | null | undefined)\n\nexport type ClickOutsideBindingValue\n = | ClickOutsideHandler\n | {\n handler: ClickOutsideHandler\n /** По умолчанию `true`. */\n enabled?: boolean\n /** По умолчанию `true` (слушаем в capture, чтобы stopPropagation внутри не ломал закрытие). */\n capture?: boolean\n /** Список событий. По умолчанию `['click']`. */\n events?: Array<'click' | 'mousedown' | 'touchstart' | 'pointerdown'>\n /** Элементы/селекторы, клики по которым считаются \"внутри\". */\n exclude?: ClickOutsideExclude[]\n }\n\ntype NormalizedOptions = {\n enabled: boolean\n capture: boolean\n events: Array<'click' | 'mousedown' | 'touchstart' | 'pointerdown'>\n handler: ClickOutsideHandler | undefined\n exclude: ClickOutsideExclude[]\n}\n\nfunction normalize(value: ClickOutsideBindingValue | undefined): NormalizedOptions {\n if (typeof value === 'function') {\n return { enabled: true, capture: true, events: ['click'], handler: value, exclude: [] }\n }\n\n if (value && typeof value === 'object') {\n return {\n enabled: value.enabled ?? true,\n capture: value.capture ?? true,\n events: value.events?.length ? value.events : ['click'],\n handler: value.handler,\n exclude: value.exclude ?? [],\n }\n }\n\n return { enabled: false, capture: true, events: ['click'], handler: undefined, exclude: [] }\n}\n\nfunction isEventInside(el: HTMLElement, event: Event): boolean {\n const target = event.target as Node | null\n if (!target)\n return false\n\n const composedPath = (event as any).composedPath?.() as EventTarget[] | undefined\n if (Array.isArray(composedPath)) {\n return composedPath.includes(el)\n }\n\n return el.contains(target)\n}\n\nfunction resolveExclude(doc: Document, exclude: ClickOutsideExclude[]): HTMLElement[] {\n const resolved: HTMLElement[] = []\n\n for (const item of exclude) {\n if (typeof item === 'string') {\n const found = doc.querySelector(item)\n if (found instanceof HTMLElement)\n resolved.push(found)\n continue\n }\n\n if (typeof item === 'function') {\n const found = item()\n if (found instanceof HTMLElement)\n resolved.push(found)\n continue\n }\n\n if (item instanceof HTMLElement) {\n resolved.push(item)\n }\n }\n\n return resolved\n}\n\ntype ElementState = {\n options: NormalizedOptions\n doc: Document\n /** Ключи подписок, которые этот элемент держит на своём документе. */\n keys: string[]\n}\n\nconst states = new WeakMap<HTMLElement, ElementState>()\n\n/**\n * Слушатель — один на документ и вид события, а не на элемент.\n *\n * Каждый элемент со своим слушателем — это десяток подписок на документе у\n * страницы с десятком оверлеев, и полное переподключение на каждом обновлении\n * хозяина: хук `updated` срабатывает на любой ререндер, а набор событий при\n * этом обычно тот же. Здесь подписка живёт по счётчику потребителей, а элементы\n * обходятся общим обработчиком.\n */\nconst documentElements = new WeakMap<Document, Set<HTMLElement>>()\nconst documentSubscriptions = new WeakMap<Document, Map<string, { listener: (event: Event) => void, count: number }>>()\n\nfunction subscriptionKey(type: string, capture: boolean): string {\n return `${type}|${capture ? 'capture' : 'bubble'}`\n}\n\nfunction keysOf(options: NormalizedOptions): string[] {\n if (!options.enabled || !options.handler)\n return []\n\n return options.events.map(type => subscriptionKey(type, options.capture))\n}\n\nfunction dispatch(doc: Document, event: Event, capture: boolean): void {\n const elements = documentElements.get(doc)\n if (!elements)\n return\n\n // Копия: обработчик закрывает оверлей, а тот на размонтировании правит набор.\n for (const el of [...elements]) {\n const state = states.get(el)\n if (!state)\n continue\n\n const { enabled, handler, exclude, events } = state.options\n if (!enabled || !handler)\n continue\n if (state.options.capture !== capture)\n continue\n if (!events.includes(event.type as NormalizedOptions['events'][number]))\n continue\n\n if ('button' in (event as any) && typeof (event as any).button === 'number' && (event as any).button !== 0) {\n continue\n }\n\n if (!el.isConnected)\n continue\n if (isEventInside(el, event))\n continue\n\n const excluded = resolveExclude(state.doc, exclude)\n if (excluded.some(item => isEventInside(item, event)))\n continue\n\n handler(event as any)\n }\n}\n\nfunction subscribe(doc: Document, key: string): void {\n let subscriptions = documentSubscriptions.get(doc)\n if (!subscriptions) {\n subscriptions = new Map()\n documentSubscriptions.set(doc, subscriptions)\n }\n\n const existing = subscriptions.get(key)\n if (existing) {\n existing.count += 1\n return\n }\n\n const [type, mode] = key.split('|')\n const capture = mode === 'capture'\n const listener = (event: Event) => dispatch(doc, event, capture)\n\n subscriptions.set(key, { listener, count: 1 })\n doc.addEventListener(type, listener, capture)\n}\n\nfunction unsubscribe(doc: Document, key: string): void {\n const subscriptions = documentSubscriptions.get(doc)\n const existing = subscriptions?.get(key)\n if (!subscriptions || !existing)\n return\n\n existing.count -= 1\n if (existing.count > 0)\n return\n\n const [type, mode] = key.split('|')\n doc.removeEventListener(type, existing.listener, mode === 'capture')\n subscriptions.delete(key)\n}\n\nfunction unbind(el: HTMLElement) {\n const state = states.get(el)\n if (!state)\n return\n\n for (const key of state.keys) unsubscribe(state.doc, key)\n\n documentElements.get(state.doc)?.delete(el)\n states.delete(el)\n}\n\nfunction bind(el: HTMLElement, value: ClickOutsideBindingValue | undefined) {\n const doc = el.ownerDocument\n const previous = states.get(el)\n\n // Элемент, переехавший в другой документ (портал в отдельное окно), обязан\n // отпустить подписки прежнего: там его больше нет.\n if (previous && previous.doc !== doc)\n unbind(el)\n\n const options = normalize(value)\n const nextKeys = keysOf(options)\n const prevKeys = states.get(el)?.keys ?? []\n\n states.set(el, { options, doc, keys: nextKeys })\n\n let elements = documentElements.get(doc)\n if (!elements) {\n elements = new Set()\n documentElements.set(doc, elements)\n }\n elements.add(el)\n\n for (const key of nextKeys) {\n if (!prevKeys.includes(key))\n subscribe(doc, key)\n }\n for (const key of prevKeys) {\n if (!nextKeys.includes(key))\n unsubscribe(doc, key)\n }\n}\n\n/**\n * `v-click-outside` — обработчик клика вне элемента (например, закрытие дропдауна/модалки).\n */\nexport const vClickOutside: Directive<HTMLElement, ClickOutsideBindingValue> = {\n mounted(el, binding) {\n bind(el, binding.value)\n },\n updated(el, binding) {\n // Не `unbind` + `bind`: разницу подписок считает сам `bind`, а снятие с\n // последующей постановкой того же слушателя — лишняя работа на каждый\n // ререндер хозяина.\n bind(el, binding.value)\n },\n unmounted(el) {\n unbind(el)\n },\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"dropzone.js","names":[],"sources":["../../src/directives/dropzone.ts"],"sourcesContent":["import type { Directive } from 'vue'\n\nimport type { FileValidator } from '../fileValidation'\n\nimport { FileValidationError, runFileValidators } from '../fileValidation'\n\nexport type DropzoneOnFiles = (files: File[], event: DragEvent) => void | Promise<void>\n\nexport type DropzoneBindingValue =\n | DropzoneOnFiles\n | {\n onFiles: DropzoneOnFiles\n /** По умолчанию `true`. */\n enabled?: boolean\n /** По умолчанию `true`. */\n multiple?: boolean\n\n /** Валидаторы, которые будут запущены перед `onFiles`. */\n validators?: FileValidator[]\n /** По умолчанию `true` (предотвращаем дефолтное поведение браузера при drop). */\n preventDefault?: boolean\n /** По умолчанию `false`. */\n stopPropagation?: boolean\n /** Вызывается, если валидация файлов не прошла. */\n onError?: (error: unknown) => void\n /** Вызывается при изменении состояния drag-over. */\n onStateChange?: (state: { isOver: boolean }) => void\n /** CSS-класс, который навешивается на элемент, пока курсор с файлами над зоной. */\n overClass?: string\n }\n\ntype NormalizedOptions = {\n enabled: boolean\n multiple: boolean\n validators: FileValidator[] | undefined\n preventDefault: boolean\n stopPropagation: boolean\n onFiles: DropzoneOnFiles | undefined\n onError: ((error: unknown) => void) | undefined\n onStateChange: ((state: { isOver: boolean }) => void) | undefined\n overClass: string\n}\n\nfunction normalize(value: DropzoneBindingValue | undefined): NormalizedOptions {\n if (typeof value === 'function') {\n return {\n enabled: true,\n multiple: true,\n validators: undefined,\n preventDefault: true,\n stopPropagation: false,\n onFiles: value,\n onError: undefined,\n onStateChange: undefined,\n overClass: 'gr-dropzone--over',\n }\n }\n\n if (value && typeof value === 'object') {\n return {\n enabled: value.enabled ?? true,\n multiple: value.multiple ?? true,\n validators: value.validators,\n preventDefault: value.preventDefault ?? true,\n stopPropagation: value.stopPropagation ?? false,\n onFiles: value.onFiles,\n onError: value.onError,\n onStateChange: value.onStateChange,\n overClass: value.overClass ?? 'gr-dropzone--over',\n }\n }\n\n return {\n enabled: false,\n multiple: true,\n validators: undefined,\n preventDefault: true,\n stopPropagation: false,\n onFiles: undefined,\n onError: undefined,\n onStateChange: undefined,\n overClass: 'gr-dropzone--over',\n }\n}\n\ntype InternalState = {\n options: NormalizedOptions\n overCounter: number\n isOver: boolean\n onDragEnter: (event: DragEvent) => void\n onDragOver: (event: DragEvent) => void\n onDragLeave: (event: DragEvent) => void\n onDrop: (event: DragEvent) => void\n}\n\nconst states = new WeakMap<HTMLElement, InternalState>()\n\n/**\n * `classList.add/remove` не принимают строки с пробелами/пустые токены —\n * иначе DOM кидает `InvalidCharacterError`. Поэтому всегда токенизируем\n * `overClass` (поддерживая Tailwind-подобные multi-class значения).\n */\nfunction tokenizeClass(value: string): string[] {\n return value.split(/\\s+/).filter(token => token.length > 0)\n}\n\nfunction setOver(el: HTMLElement, next: boolean) {\n const state = states.get(el)\n if (!state) return\n\n // `state.isOver` может рассинхронизироваться с DOM (например, при обновлениях\n // компонента/директивы во время drag-over). Поэтому DOM-состояние всегда\n // приводим к `next`, а коллбек вызываем только при фактическом изменении.\n const wasDomOver = el.dataset.grDropzoneOver === 'true'\n const prev = state.isOver\n\n state.isOver = next\n\n const tokens = tokenizeClass(state.options.overClass)\n if (next) {\n if (tokens.length) el.classList.add(...tokens)\n el.dataset.grDropzoneOver = 'true'\n } else {\n if (tokens.length) el.classList.remove(...tokens)\n delete el.dataset.grDropzoneOver\n }\n\n if (prev !== next || wasDomOver !== next) {\n state.options.onStateChange?.({ isOver: next })\n }\n}\n\nfunction unbind(el: HTMLElement) {\n const state = states.get(el)\n if (!state) return\n\n // Гарантированно очищаем визуальное состояние и счётчики.\n // Это важно, если директива обновлялась/перебиндилась во время DnD.\n state.overCounter = 0\n setOver(el, false)\n\n delete el.dataset.grDropzone\n\n el.removeEventListener('dragenter', state.onDragEnter)\n el.removeEventListener('dragover', state.onDragOver)\n el.removeEventListener('dragleave', state.onDragLeave)\n el.removeEventListener('drop', state.onDrop)\n\n states.delete(el)\n}\n\nfunction bind(el: HTMLElement, value: DropzoneBindingValue | undefined) {\n const options = normalize(value)\n\n const state: InternalState = {\n options,\n overCounter: 0,\n isOver: false,\n onDragEnter(event) {\n const current = states.get(el)\n if (!current?.options.enabled) return\n\n if (current.options.preventDefault) event.preventDefault()\n if (current.options.stopPropagation) event.stopPropagation()\n\n current.overCounter += 1\n setOver(el, true)\n },\n onDragOver(event) {\n const current = states.get(el)\n if (!current?.options.enabled) return\n\n if (current.options.preventDefault) event.preventDefault()\n if (current.options.stopPropagation) event.stopPropagation()\n\n if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy'\n },\n onDragLeave(event) {\n const current = states.get(el)\n if (!current?.options.enabled) return\n\n if (current.options.preventDefault) event.preventDefault()\n if (current.options.stopPropagation) event.stopPropagation()\n\n current.overCounter = Math.max(0, current.overCounter - 1)\n if (current.overCounter === 0) setOver(el, false)\n },\n onDrop(event) {\n const current = states.get(el)\n if (!current?.options.enabled) return\n\n if (current.options.preventDefault) event.preventDefault()\n if (current.options.stopPropagation) event.stopPropagation()\n\n current.overCounter = 0\n setOver(el, false)\n\n const dt = event.dataTransfer\n const files = dt?.files ? Array.prototype.slice.call(dt.files) as File[] : []\n if (!files.length) return\n\n void (async () => {\n try {\n const validators = current.options.validators ?? []\n\n const { files: picked, issues } = await runFileValidators(files, validators, {\n source: 'drop',\n multiple: current.options.multiple,\n })\n\n if (issues.length > 0) {\n current.options.onError?.(new FileValidationError(issues, picked))\n return\n }\n\n if (!picked.length) return\n await current.options.onFiles?.(picked, event)\n } catch (error) {\n current.options.onError?.(error)\n }\n })()\n },\n }\n\n states.set(el, state)\n\n // Base marker.\n el.dataset.grDropzone = 'true'\n\n el.addEventListener('dragenter', state.onDragEnter)\n el.addEventListener('dragover', state.onDragOver)\n el.addEventListener('dragleave', state.onDragLeave)\n el.addEventListener('drop', state.onDrop)\n}\n\nfunction update(el: HTMLElement, value: DropzoneBindingValue | undefined) {\n const state = states.get(el)\n if (!state) {\n bind(el, value)\n return\n }\n\n const prevOverClass = state.options.overClass\n const nextOptions = normalize(value)\n\n state.options = nextOptions\n\n // Если поменялся `overClass`, синхронизируем DOM (учитывая multi-token значения).\n if (prevOverClass !== nextOptions.overClass) {\n const prevTokens = tokenizeClass(prevOverClass)\n if (prevTokens.length) el.classList.remove(...prevTokens)\n if (state.isOver) {\n const nextTokens = tokenizeClass(nextOptions.overClass)\n if (nextTokens.length) el.classList.add(...nextTokens)\n }\n }\n\n // Если директива выключена — сбрасываем состояние.\n if (!nextOptions.enabled) {\n state.overCounter = 0\n setOver(el, false)\n }\n}\n\n/**\n * `v-dropzone` — декларативная DnD-зона для файлов (перетаскивание + drop).\n *\n * Директива не делает загрузку сама — только нормализует события и отдаёт `File[]`.\n */\nexport const vDropzone: Directive<HTMLElement, DropzoneBindingValue> = {\n mounted(el, binding) {\n bind(el, binding.value)\n },\n updated(el, binding) {\n update(el, binding.value)\n },\n unmounted(el) {\n unbind(el)\n },\n}\n"],"mappings":";;AA2CA,SAAS,UAAU,OAA4D;CAC7E,IAAI,OAAO,UAAU,YACnB,OAAO;EACL,SAAS;EACT,UAAU;EACV,YAAY,KAAA;EACZ,gBAAgB;EAChB,iBAAiB;EACjB,SAAS;EACT,SAAS,KAAA;EACT,eAAe,KAAA;EACf,WAAW;CACb;CAGF,IAAI,SAAS,OAAO,UAAU,UAC5B,OAAO;EACL,SAAS,MAAM,WAAW;EAC1B,UAAU,MAAM,YAAY;EAC5B,YAAY,MAAM;EAClB,gBAAgB,MAAM,kBAAkB;EACxC,iBAAiB,MAAM,mBAAmB;EAC1C,SAAS,MAAM;EACf,SAAS,MAAM;EACf,eAAe,MAAM;EACrB,WAAW,MAAM,aAAa;CAChC;CAGF,OAAO;EACL,SAAS;EACT,UAAU;EACV,YAAY,KAAA;EACZ,gBAAgB;EAChB,iBAAiB;EACjB,SAAS,KAAA;EACT,SAAS,KAAA;EACT,eAAe,KAAA;EACf,WAAW;CACb;AACF;AAYA,IAAM,yBAAS,IAAI,QAAoC;;;;;;AAOvD,SAAS,cAAc,OAAyB;CAC9C,OAAO,MAAM,MAAM,KAAK,CAAC,CAAC,QAAO,UAAS,MAAM,SAAS,CAAC;AAC5D;AAEA,SAAS,QAAQ,IAAiB,MAAe;CAC/C,MAAM,QAAQ,OAAO,IAAI,EAAE;CAC3B,IAAI,CAAC,OAAO;CAKZ,MAAM,aAAa,GAAG,QAAQ,mBAAmB;CACjD,MAAM,OAAO,MAAM;CAEnB,MAAM,SAAS;CAEf,MAAM,SAAS,cAAc,MAAM,QAAQ,SAAS;CACpD,IAAI,MAAM;EACR,IAAI,OAAO,QAAQ,GAAG,UAAU,IAAI,GAAG,MAAM;EAC7C,GAAG,QAAQ,iBAAiB;CAC9B,OAAO;EACL,IAAI,OAAO,QAAQ,GAAG,UAAU,OAAO,GAAG,MAAM;EAChD,OAAO,GAAG,QAAQ;CACpB;CAEA,IAAI,SAAS,QAAQ,eAAe,MAClC,MAAM,QAAQ,gBAAgB,EAAE,QAAQ,KAAK,CAAC;AAElD;AAEA,SAAS,OAAO,IAAiB;CAC/B,MAAM,QAAQ,OAAO,IAAI,EAAE;CAC3B,IAAI,CAAC,OAAO;CAIZ,MAAM,cAAc;CACpB,QAAQ,IAAI,KAAK;CAEjB,OAAO,GAAG,QAAQ;CAElB,GAAG,oBAAoB,aAAa,MAAM,WAAW;CACrD,GAAG,oBAAoB,YAAY,MAAM,UAAU;CACnD,GAAG,oBAAoB,aAAa,MAAM,WAAW;CACrD,GAAG,oBAAoB,QAAQ,MAAM,MAAM;CAE3C,OAAO,OAAO,EAAE;AAClB;AAEA,SAAS,KAAK,IAAiB,OAAyC;CAGtE,MAAM,QAAuB;EAC3B,SAHc,UAAU,KAGxB;EACA,aAAa;EACb,QAAQ;EACR,YAAY,OAAO;GACjB,MAAM,UAAU,OAAO,IAAI,EAAE;GAC7B,IAAI,CAAC,SAAS,QAAQ,SAAS;GAE/B,IAAI,QAAQ,QAAQ,gBAAgB,MAAM,eAAe;GACzD,IAAI,QAAQ,QAAQ,iBAAiB,MAAM,gBAAgB;GAE3D,QAAQ,eAAe;GACvB,QAAQ,IAAI,IAAI;EAClB;EACA,WAAW,OAAO;GAChB,MAAM,UAAU,OAAO,IAAI,EAAE;GAC7B,IAAI,CAAC,SAAS,QAAQ,SAAS;GAE/B,IAAI,QAAQ,QAAQ,gBAAgB,MAAM,eAAe;GACzD,IAAI,QAAQ,QAAQ,iBAAiB,MAAM,gBAAgB;GAE3D,IAAI,MAAM,cAAc,MAAM,aAAa,aAAa;EAC1D;EACA,YAAY,OAAO;GACjB,MAAM,UAAU,OAAO,IAAI,EAAE;GAC7B,IAAI,CAAC,SAAS,QAAQ,SAAS;GAE/B,IAAI,QAAQ,QAAQ,gBAAgB,MAAM,eAAe;GACzD,IAAI,QAAQ,QAAQ,iBAAiB,MAAM,gBAAgB;GAE3D,QAAQ,cAAc,KAAK,IAAI,GAAG,QAAQ,cAAc,CAAC;GACzD,IAAI,QAAQ,gBAAgB,GAAG,QAAQ,IAAI,KAAK;EAClD;EACA,OAAO,OAAO;GACZ,MAAM,UAAU,OAAO,IAAI,EAAE;GAC7B,IAAI,CAAC,SAAS,QAAQ,SAAS;GAE/B,IAAI,QAAQ,QAAQ,gBAAgB,MAAM,eAAe;GACzD,IAAI,QAAQ,QAAQ,iBAAiB,MAAM,gBAAgB;GAE3D,QAAQ,cAAc;GACtB,QAAQ,IAAI,KAAK;GAEjB,MAAM,KAAK,MAAM;GACjB,MAAM,QAAQ,IAAI,QAAQ,MAAM,UAAU,MAAM,KAAK,GAAG,KAAK,IAAc,CAAC;GAC5E,IAAI,CAAC,MAAM,QAAQ;GAEnB,CAAM,YAAY;IAChB,IAAI;KACF,MAAM,aAAa,QAAQ,QAAQ,cAAc,CAAC;KAElD,MAAM,EAAE,OAAO,QAAQ,WAAW,MAAM,kBAAkB,OAAO,YAAY;MAC3E,QAAQ;MACR,UAAU,QAAQ,QAAQ;KAC5B,CAAC;KAED,IAAI,OAAO,SAAS,GAAG;MACrB,QAAQ,QAAQ,UAAU,IAAI,oBAAoB,QAAQ,MAAM,CAAC;MACjE;KACF;KAEA,IAAI,CAAC,OAAO,QAAQ;KACpB,MAAM,QAAQ,QAAQ,UAAU,QAAQ,KAAK;IAC/C,SAAS,OAAO;KACd,QAAQ,QAAQ,UAAU,KAAK;IACjC;GACF,EAAA,CAAG;EACL;CACF;CAEA,OAAO,IAAI,IAAI,KAAK;CAGpB,GAAG,QAAQ,aAAa;CAExB,GAAG,iBAAiB,aAAa,MAAM,WAAW;CAClD,GAAG,iBAAiB,YAAY,MAAM,UAAU;CAChD,GAAG,iBAAiB,aAAa,MAAM,WAAW;CAClD,GAAG,iBAAiB,QAAQ,MAAM,MAAM;AAC1C;AAEA,SAAS,OAAO,IAAiB,OAAyC;CACxE,MAAM,QAAQ,OAAO,IAAI,EAAE;CAC3B,IAAI,CAAC,OAAO;EACV,KAAK,IAAI,KAAK;EACd;CACF;CAEA,MAAM,gBAAgB,MAAM,QAAQ;CACpC,MAAM,cAAc,UAAU,KAAK;CAEnC,MAAM,UAAU;CAGhB,IAAI,kBAAkB,YAAY,WAAW;EAC3C,MAAM,aAAa,cAAc,aAAa;EAC9C,IAAI,WAAW,QAAQ,GAAG,UAAU,OAAO,GAAG,UAAU;EACxD,IAAI,MAAM,QAAQ;GAChB,MAAM,aAAa,cAAc,YAAY,SAAS;GACtD,IAAI,WAAW,QAAQ,GAAG,UAAU,IAAI,GAAG,UAAU;EACvD;CACF;CAGA,IAAI,CAAC,YAAY,SAAS;EACxB,MAAM,cAAc;EACpB,QAAQ,IAAI,KAAK;CACnB;AACF;;;;;;AAOA,IAAa,YAA0D;CACrE,QAAQ,IAAI,SAAS;EACnB,KAAK,IAAI,QAAQ,KAAK;CACxB;CACA,QAAQ,IAAI,SAAS;EACnB,OAAO,IAAI,QAAQ,KAAK;CAC1B;CACA,UAAU,IAAI;EACZ,OAAO,EAAE;CACX;AACF"}
1
+ {"version":3,"file":"dropzone.js","names":[],"sources":["../../src/directives/dropzone.ts"],"sourcesContent":["import type { Directive } from 'vue'\n\nimport type { FileValidator } from '../fileValidation'\n\nimport { FileValidationError, runFileValidators } from '../fileValidation'\n\nexport type DropzoneOnFiles = (files: File[], event: DragEvent) => void | Promise<void>\n\nexport type DropzoneBindingValue\n = | DropzoneOnFiles\n | {\n onFiles: DropzoneOnFiles\n /** По умолчанию `true`. */\n enabled?: boolean\n /** По умолчанию `true`. */\n multiple?: boolean\n\n /** Валидаторы, которые будут запущены перед `onFiles`. */\n validators?: FileValidator[]\n /** По умолчанию `true` (предотвращаем дефолтное поведение браузера при drop). */\n preventDefault?: boolean\n /** По умолчанию `false`. */\n stopPropagation?: boolean\n /** Вызывается, если валидация файлов не прошла. */\n onError?: (error: unknown) => void\n /** Вызывается при изменении состояния drag-over. */\n onStateChange?: (state: { isOver: boolean }) => void\n /** CSS-класс, который навешивается на элемент, пока курсор с файлами над зоной. */\n overClass?: string\n }\n\ntype NormalizedOptions = {\n enabled: boolean\n multiple: boolean\n validators: FileValidator[] | undefined\n preventDefault: boolean\n stopPropagation: boolean\n onFiles: DropzoneOnFiles | undefined\n onError: ((error: unknown) => void) | undefined\n onStateChange: ((state: { isOver: boolean }) => void) | undefined\n overClass: string\n}\n\nfunction normalize(value: DropzoneBindingValue | undefined): NormalizedOptions {\n if (typeof value === 'function') {\n return {\n enabled: true,\n multiple: true,\n validators: undefined,\n preventDefault: true,\n stopPropagation: false,\n onFiles: value,\n onError: undefined,\n onStateChange: undefined,\n overClass: 'gr-dropzone--over',\n }\n }\n\n if (value && typeof value === 'object') {\n return {\n enabled: value.enabled ?? true,\n multiple: value.multiple ?? true,\n validators: value.validators,\n preventDefault: value.preventDefault ?? true,\n stopPropagation: value.stopPropagation ?? false,\n onFiles: value.onFiles,\n onError: value.onError,\n onStateChange: value.onStateChange,\n overClass: value.overClass ?? 'gr-dropzone--over',\n }\n }\n\n return {\n enabled: false,\n multiple: true,\n validators: undefined,\n preventDefault: true,\n stopPropagation: false,\n onFiles: undefined,\n onError: undefined,\n onStateChange: undefined,\n overClass: 'gr-dropzone--over',\n }\n}\n\ntype InternalState = {\n options: NormalizedOptions\n overCounter: number\n isOver: boolean\n onDragEnter: (event: DragEvent) => void\n onDragOver: (event: DragEvent) => void\n onDragLeave: (event: DragEvent) => void\n onDrop: (event: DragEvent) => void\n}\n\nconst states = new WeakMap<HTMLElement, InternalState>()\n\n/**\n * `classList.add/remove` не принимают строки с пробелами/пустые токены —\n * иначе DOM кидает `InvalidCharacterError`. Поэтому всегда токенизируем\n * `overClass` (поддерживая Tailwind-подобные multi-class значения).\n */\nfunction tokenizeClass(value: string): string[] {\n return value.split(/\\s+/).filter(token => token.length > 0)\n}\n\nfunction setOver(el: HTMLElement, next: boolean) {\n const state = states.get(el)\n if (!state)\n return\n\n // `state.isOver` может рассинхронизироваться с DOM (например, при обновлениях\n // компонента/директивы во время drag-over). Поэтому DOM-состояние всегда\n // приводим к `next`, а коллбек вызываем только при фактическом изменении.\n const wasDomOver = el.dataset.grDropzoneOver === 'true'\n const prev = state.isOver\n\n state.isOver = next\n\n const tokens = tokenizeClass(state.options.overClass)\n if (next) {\n if (tokens.length)\n el.classList.add(...tokens)\n el.dataset.grDropzoneOver = 'true'\n }\n else {\n if (tokens.length)\n el.classList.remove(...tokens)\n delete el.dataset.grDropzoneOver\n }\n\n if (prev !== next || wasDomOver !== next) {\n state.options.onStateChange?.({ isOver: next })\n }\n}\n\nfunction unbind(el: HTMLElement) {\n const state = states.get(el)\n if (!state)\n return\n\n // Гарантированно очищаем визуальное состояние и счётчики.\n // Это важно, если директива обновлялась/перебиндилась во время DnD.\n state.overCounter = 0\n setOver(el, false)\n\n delete el.dataset.grDropzone\n\n el.removeEventListener('dragenter', state.onDragEnter)\n el.removeEventListener('dragover', state.onDragOver)\n el.removeEventListener('dragleave', state.onDragLeave)\n el.removeEventListener('drop', state.onDrop)\n\n states.delete(el)\n}\n\nfunction bind(el: HTMLElement, value: DropzoneBindingValue | undefined) {\n const options = normalize(value)\n\n const state: InternalState = {\n options,\n overCounter: 0,\n isOver: false,\n onDragEnter(event) {\n const current = states.get(el)\n if (!current?.options.enabled)\n return\n\n if (current.options.preventDefault)\n event.preventDefault()\n if (current.options.stopPropagation)\n event.stopPropagation()\n\n current.overCounter += 1\n setOver(el, true)\n },\n onDragOver(event) {\n const current = states.get(el)\n if (!current?.options.enabled)\n return\n\n if (current.options.preventDefault)\n event.preventDefault()\n if (current.options.stopPropagation)\n event.stopPropagation()\n\n if (event.dataTransfer)\n event.dataTransfer.dropEffect = 'copy'\n },\n onDragLeave(event) {\n const current = states.get(el)\n if (!current?.options.enabled)\n return\n\n if (current.options.preventDefault)\n event.preventDefault()\n if (current.options.stopPropagation)\n event.stopPropagation()\n\n current.overCounter = Math.max(0, current.overCounter - 1)\n if (current.overCounter === 0)\n setOver(el, false)\n },\n onDrop(event) {\n const current = states.get(el)\n if (!current?.options.enabled)\n return\n\n if (current.options.preventDefault)\n event.preventDefault()\n if (current.options.stopPropagation)\n event.stopPropagation()\n\n current.overCounter = 0\n setOver(el, false)\n\n const dt = event.dataTransfer\n const files = dt?.files ? Array.prototype.slice.call(dt.files) as File[] : []\n if (!files.length)\n return\n\n void (async () => {\n try {\n const validators = current.options.validators ?? []\n\n const { files: picked, issues } = await runFileValidators(files, validators, {\n source: 'drop',\n multiple: current.options.multiple,\n })\n\n if (issues.length > 0) {\n current.options.onError?.(new FileValidationError(issues, picked))\n return\n }\n\n if (!picked.length)\n return\n await current.options.onFiles?.(picked, event)\n }\n catch (error) {\n current.options.onError?.(error)\n }\n })()\n },\n }\n\n states.set(el, state)\n\n // Base marker.\n el.dataset.grDropzone = 'true'\n\n el.addEventListener('dragenter', state.onDragEnter)\n el.addEventListener('dragover', state.onDragOver)\n el.addEventListener('dragleave', state.onDragLeave)\n el.addEventListener('drop', state.onDrop)\n}\n\nfunction update(el: HTMLElement, value: DropzoneBindingValue | undefined) {\n const state = states.get(el)\n if (!state) {\n bind(el, value)\n return\n }\n\n const prevOverClass = state.options.overClass\n const nextOptions = normalize(value)\n\n state.options = nextOptions\n\n // Если поменялся `overClass`, синхронизируем DOM (учитывая multi-token значения).\n if (prevOverClass !== nextOptions.overClass) {\n const prevTokens = tokenizeClass(prevOverClass)\n if (prevTokens.length)\n el.classList.remove(...prevTokens)\n if (state.isOver) {\n const nextTokens = tokenizeClass(nextOptions.overClass)\n if (nextTokens.length)\n el.classList.add(...nextTokens)\n }\n }\n\n // Если директива выключена — сбрасываем состояние.\n if (!nextOptions.enabled) {\n state.overCounter = 0\n setOver(el, false)\n }\n}\n\n/**\n * `v-dropzone` — декларативная DnD-зона для файлов (перетаскивание + drop).\n *\n * Директива не делает загрузку сама — только нормализует события и отдаёт `File[]`.\n */\nexport const vDropzone: Directive<HTMLElement, DropzoneBindingValue> = {\n mounted(el, binding) {\n bind(el, binding.value)\n },\n updated(el, binding) {\n update(el, binding.value)\n },\n unmounted(el) {\n unbind(el)\n },\n}\n"],"mappings":"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"}