@leavepulse/ui 0.43.4 → 0.45.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 (437) hide show
  1. package/README.md +121 -7
  2. package/dist/assets/{LpCommandPalette-B-0Est4Z.css → LpCommandPalette-nGNnzXqQ.css} +21 -21
  3. package/dist/assets/{LpDropzone-BBoWvi-J.css → LpDropzone-i4BdMi1H.css} +12 -12
  4. package/dist/assets/{LpImageEditor-Ct7Y3aNu.css → LpImageEditor-CbMA82g8.css} +6 -6
  5. package/dist/assets/{LpRollingText-CR1mb0Vt.css → LpRollingText-BGnE6q9j.css} +3 -0
  6. package/dist/assets/{LpShortcutsDialog-C6YoQ8_M.css → LpShortcutsDialog-CqYX3yy6.css} +28 -7
  7. package/dist/assets/{LpSkeleton-D7xU8IPV.css → LpSkeleton-H3Z5dEIk.css} +4 -1
  8. package/dist/assets/{LpTable-BgPLJOXa.css → LpTable-CPwWxTJS.css} +49 -9
  9. package/dist/chunks/ContextMenuItems-BtKU7074.js +124 -0
  10. package/dist/chunks/{LayoutCanvas-Dd20boUG.js → LayoutCanvas-BkaGXTjx.js} +26 -9
  11. package/dist/chunks/{LayoutNode-DGpZA9LX.js → LayoutNode-B3_9E1Xa.js} +28 -12
  12. package/dist/chunks/{LpActionBar-BlvtGB2P.js → LpActionBar-CnfFVIwC.js} +26 -12
  13. package/dist/chunks/{LpAddressInput-B-Zvxsn7.js → LpAddressInput-BwX8uo4_.js} +54 -49
  14. package/dist/chunks/{LpAlert-BAYHDSS1.js → LpAlert-B6ilrM7T.js} +16 -9
  15. package/dist/chunks/{LpAppShell-BGQVIY-3.js → LpAppShell-C53_RaGF.js} +53 -26
  16. package/dist/chunks/{LpAutocomplete-DExlIZe_.js → LpAutocomplete-C8IgSMsi.js} +52 -24
  17. package/dist/chunks/{LpAvatar-B3-xAOH3.js → LpAvatar-C1XStdOo.js} +19 -10
  18. package/dist/chunks/LpBreadcrumbs-CQJd73Gm.js +95 -0
  19. package/dist/chunks/{LpButton-5VXXCjwF.js → LpButton-CKSwC6Mu.js} +45 -15
  20. package/dist/chunks/{LpCalendar-B7Ko5RdR.js → LpCalendar-DrA2McTU.js} +1 -1
  21. package/dist/chunks/{LpCard-BhI1N_vL.js → LpCard-C2IvucmX.js} +1 -1
  22. package/dist/chunks/{LpCheckbox-CVccKYaH.js → LpCheckbox-Bkj13CbW.js} +8 -4
  23. package/dist/chunks/{LpCodeBlock-Bo0B4TFR.js → LpCodeBlock-ca7ANFyW.js} +49 -17
  24. package/dist/chunks/{LpColorPicker-BlXea7LM.js → LpColorPicker-zz4VGkG4.js} +75 -26
  25. package/dist/chunks/{LpConfirmDialog-Q-bxff8z.js → LpConfirmDialog-DCw5jR1t.js} +59 -17
  26. package/dist/chunks/LpConfirmHost-CbywMpMP.js +49 -0
  27. package/dist/chunks/LpContextMenu-BqkAYa9X.js +69 -0
  28. package/dist/chunks/LpDatePicker-DuusgT69.js +195 -0
  29. package/dist/chunks/LpDateRangePicker-DHeuznfC.js +208 -0
  30. package/dist/chunks/{LpDisclosure-ChQYmjlR.js → LpDisclosure-Cc23-JOT.js} +1 -1
  31. package/dist/chunks/{LpDrawer-BZ6k3hvo.js → LpDrawer-Dt4aB4Fr.js} +65 -28
  32. package/dist/chunks/{LpDropdownMenu-DJpoqRCb.js → LpDropdownMenu-DdJGQzPn.js} +2 -1
  33. package/dist/chunks/{LpEmptyState-Dsdqz1Wj.js → LpEmptyState-BJbZXC8D.js} +1 -1
  34. package/dist/chunks/LpFieldNotice-wtieLUTj.js +64 -0
  35. package/dist/chunks/{LpFileTree-_5vFBgaK.js → LpFileTree-CNurOvkV.js} +25 -10
  36. package/dist/chunks/{LpFileTreeNode-BdcIYKIE.js → LpFileTreeNode-D_qRRdsH.js} +3 -3
  37. package/dist/chunks/LpFormField-CtVYj6tO.js +101 -0
  38. package/dist/chunks/{LpIcon-CCnX5_2j.js → LpIcon-DYtaid0K.js} +20 -6
  39. package/dist/chunks/{LpInput-9EXWUM9B.js → LpInput-D-YGQbEv.js} +58 -20
  40. package/dist/chunks/{LpLightbox-2sYVMY-S.js → LpLightbox-DSDahF23.js} +148 -61
  41. package/dist/chunks/{LpLink-DCND-zyu.js → LpLink-Krkjhe_y.js} +15 -6
  42. package/dist/chunks/{LpLogViewer-CaRy5MeS.js → LpLogViewer-KypbgOyo.js} +300 -72
  43. package/dist/chunks/{LpMapPicker-ul4oYno6.js → LpMapPicker-Cc84LaM3.js} +39 -14
  44. package/dist/chunks/{LpMediaGallery-FF-NquIo.js → LpMediaGallery-YKr-cQPN.js} +7 -4
  45. package/dist/chunks/{LpModal-Bt0pQCI_.js → LpModal-DMMDQHAu.js} +52 -29
  46. package/dist/chunks/{LpModalNav-YaifHrXC.js → LpModalNav-BVPDyIup.js} +10 -7
  47. package/dist/chunks/{LpNotificationBell-DyGGsMAM.js → LpNotificationBell-CCwyJP5C.js} +79 -33
  48. package/dist/chunks/{LpNumberField-CWuX08iX.js → LpNumberField-B77O-z28.js} +21 -8
  49. package/dist/chunks/{LpNumberFlow-amWYaeuA.js → LpNumberFlow-z4WPZcZu.js} +22 -12
  50. package/dist/chunks/{LpOtpInput-y2v-pmiN.js → LpOtpInput-CziPF3Oq.js} +37 -8
  51. package/dist/chunks/{LpPagination-5FxjmmB5.js → LpPagination-BcDlo3r_.js} +31 -20
  52. package/dist/chunks/{LpPasswordInput--sjYCkLS.js → LpPasswordInput-eKzQhbQP.js} +11 -8
  53. package/dist/chunks/LpPhoneInput-BCbrrhgE.js +336 -0
  54. package/dist/chunks/{LpPie-D6OnzwlJ.js → LpPie-BEJ2SfI6.js} +84 -28
  55. package/dist/chunks/{LpPopover-8268UmlC.js → LpPopover-DCKeqiVn.js} +5 -2
  56. package/dist/chunks/{LpProgress-Btq5W4Ut.js → LpProgress-CBU1iI69.js} +5 -4
  57. package/dist/chunks/{LpRadioGroup-xPIuVDO_.js → LpRadioGroup-DmEW0l78.js} +9 -2
  58. package/dist/chunks/LpRangeCalendar-DZuaRV0W.js +331 -0
  59. package/dist/chunks/{LpRollingText-CM5gkSeF.js → LpRollingText-DB7upf-5.js} +1 -1
  60. package/dist/chunks/{LpScrollArea-D8xyKXcF.js → LpScrollArea-Bry1fdOM.js} +14 -2
  61. package/dist/chunks/{LpSection-aM6J_u51.js → LpSection-tKEH0jYf.js} +1 -1
  62. package/dist/chunks/{LpSegmented-fV0Sk92p.js → LpSegmented-C09EEam2.js} +8 -4
  63. package/dist/chunks/{LpSelect-ANIVtrpM.js → LpSelect-DNIL6MB9.js} +50 -31
  64. package/dist/chunks/{LpSidebar-ClBUwoEz.js → LpSidebar-CQNB0-yU.js} +30 -22
  65. package/dist/chunks/{LpSidebarNav-llOcs_Fn.js → LpSidebarNav-kx-LdqNU.js} +16 -11
  66. package/dist/chunks/{LpSkeleton-CTI-vBgW.js → LpSkeleton-BB6XIV-w.js} +1 -1
  67. package/dist/chunks/{LpSlider-BwNIBkAK.js → LpSlider-Cuzmrd7p.js} +7 -2
  68. package/dist/chunks/{LpSparkline-C6xQ2dwv.js → LpSparkline-CB80N8SJ.js} +11 -4
  69. package/dist/chunks/{LpStat-CQaW8R--.js → LpStat-B-7udBRP.js} +2 -2
  70. package/dist/chunks/{LpStepper-Bk-XaQYU.js → LpStepper-C7XiVKZj.js} +34 -19
  71. package/dist/chunks/{LpSwitch-Blizvqyt.js → LpSwitch-OrcOJX9m.js} +41 -27
  72. package/dist/chunks/LpTableFilter-BV8PpLM9.js +158 -0
  73. package/dist/chunks/{LpTableOfContents-CjvxPMpU.js → LpTableOfContents-BUcAfGPQ.js} +36 -41
  74. package/dist/chunks/{LpTabs-Cefe_lys.js → LpTabs-29iW1XOx.js} +20 -6
  75. package/dist/chunks/LpTagsInput-D1SuqbhB.js +300 -0
  76. package/dist/chunks/LpTextarea-uuScWo6-.js +77 -0
  77. package/dist/chunks/LpThemeProvider-Be4r2TQX.js +35 -0
  78. package/dist/chunks/LpThemeSwitcher-B4aVPbNC.js +166 -0
  79. package/dist/chunks/{LpTilt-DuyKbpkA.js → LpTilt-B7KHX1zH.js} +2 -2
  80. package/dist/chunks/{LpTimeField-K6nAGboX.js → LpTimeField-Cqe7fXnh.js} +14 -8
  81. package/dist/chunks/{LpToaster-gDB1EaU3.js → LpToaster-DpJvoCOh.js} +94 -31
  82. package/dist/chunks/{LpTooltip-hCLPAqMG.js → LpTooltip-CXVKcOXT.js} +14 -6
  83. package/dist/chunks/LpTooltipProvider-CftjV7Xc.js +25 -0
  84. package/dist/chunks/{LpTopologyCanvas-Cm1L39uL.js → LpTopologyCanvas-6ozNrQKh.js} +8 -2
  85. package/dist/chunks/{LpUptimeBar-DvAMno0S.js → LpUptimeBar-ugcv25xj.js} +44 -15
  86. package/dist/chunks/icons-CkA0njzm.js +43 -0
  87. package/dist/chunks/{LpThemeSwitcher-Cue-myST.js → useTheme-DJm1JSsY.js} +215 -260
  88. package/dist/component-names.d.ts +1 -1
  89. package/dist/components/ContextMenuItems.vue.d.ts +8 -0
  90. package/dist/components/ContextMenuItems.vue.js +4 -0
  91. package/dist/components/LayoutCanvas.vue.js +1 -1
  92. package/dist/components/LayoutNode.vue.d.ts +4 -0
  93. package/dist/components/LayoutNode.vue.js +1 -1
  94. package/dist/components/LpActionBar.vue.d.ts +3 -0
  95. package/dist/components/LpActionBar.vue.js +1 -1
  96. package/dist/components/LpAddressInput.vue.d.ts +1 -1
  97. package/dist/components/LpAddressInput.vue.js +1 -1
  98. package/dist/components/LpAlert.vue.d.ts +9 -0
  99. package/dist/components/LpAlert.vue.js +1 -1
  100. package/dist/components/LpAppShell.vue.d.ts +10 -0
  101. package/dist/components/LpAppShell.vue.js +1 -1
  102. package/dist/components/LpAutocomplete.vue.d.ts +31 -6
  103. package/dist/components/LpAutocomplete.vue.js +1 -1
  104. package/dist/components/LpAvatar.vue.d.ts +3 -3
  105. package/dist/components/LpAvatar.vue.js +1 -1
  106. package/dist/components/LpBadge.vue.d.ts +2 -2
  107. package/dist/components/LpBreadcrumbs.vue.d.ts +13 -9
  108. package/dist/components/LpBreadcrumbs.vue.js +1 -1
  109. package/dist/components/LpButton.vue.d.ts +35 -98
  110. package/dist/components/LpButton.vue.js +1 -1
  111. package/dist/components/LpCalendar.vue.js +1 -1
  112. package/dist/components/LpCard.vue.js +1 -1
  113. package/dist/components/LpChart.vue.d.ts +47 -3
  114. package/dist/components/LpChart.vue.js +377 -97
  115. package/dist/components/LpCheckbox.vue.js +1 -1
  116. package/dist/components/LpCodeBlock.vue.d.ts +2 -0
  117. package/dist/components/LpCodeBlock.vue.js +1 -1
  118. package/dist/components/LpColorPicker.vue.d.ts +2 -2
  119. package/dist/components/LpColorPicker.vue.js +1 -1
  120. package/dist/components/LpCommandPalette.vue.d.ts +14 -3
  121. package/dist/components/LpCommandPalette.vue.js +142 -77
  122. package/dist/components/LpConfirmDialog.vue.d.ts +15 -3
  123. package/dist/components/LpConfirmDialog.vue.js +1 -1
  124. package/dist/components/LpConfirmHost.vue.d.ts +9 -0
  125. package/dist/components/LpConfirmHost.vue.js +4 -0
  126. package/dist/components/LpContextMenu.vue.d.ts +3 -5
  127. package/dist/components/LpContextMenu.vue.js +1 -1
  128. package/dist/components/LpDatePicker.vue.d.ts +24 -6
  129. package/dist/components/LpDatePicker.vue.js +1 -1
  130. package/dist/components/LpDateRangePicker.vue.d.ts +60 -0
  131. package/dist/components/LpDateRangePicker.vue.js +4 -0
  132. package/dist/components/LpDisclosure.vue.js +1 -1
  133. package/dist/components/LpDrawer.vue.d.ts +10 -0
  134. package/dist/components/LpDrawer.vue.js +1 -1
  135. package/dist/components/LpDropdownMenu.vue.js +1 -1
  136. package/dist/components/LpDropzone.vue.js +23 -7
  137. package/dist/components/LpEmptyState.vue.js +1 -1
  138. package/dist/components/LpFieldNotice.vue.d.ts +8 -0
  139. package/dist/components/LpFieldNotice.vue.js +4 -0
  140. package/dist/components/LpFileTree.vue.d.ts +11 -2
  141. package/dist/components/LpFileTree.vue.js +1 -1
  142. package/dist/components/LpFileTreeNode.vue.js +1 -1
  143. package/dist/components/LpFormField.vue.d.ts +13 -1
  144. package/dist/components/LpFormField.vue.js +1 -1
  145. package/dist/components/LpIcon.vue.d.ts +0 -1
  146. package/dist/components/LpIcon.vue.js +1 -1
  147. package/dist/components/LpImageEditor.vue.js +93 -58
  148. package/dist/components/LpInput.vue.d.ts +22 -3
  149. package/dist/components/LpInput.vue.js +1 -1
  150. package/dist/components/LpLightbox.vue.d.ts +17 -0
  151. package/dist/components/LpLightbox.vue.js +1 -1
  152. package/dist/components/LpLink.vue.d.ts +11 -11
  153. package/dist/components/LpLink.vue.js +1 -1
  154. package/dist/components/LpLogViewer.vue.d.ts +72 -3
  155. package/dist/components/LpLogViewer.vue.js +1 -1
  156. package/dist/components/LpMapPicker.vue.d.ts +4 -4
  157. package/dist/components/LpMapPicker.vue.js +1 -1
  158. package/dist/components/LpMediaGallery.vue.js +1 -1
  159. package/dist/components/LpModal.vue.d.ts +16 -6
  160. package/dist/components/LpModal.vue.js +1 -1
  161. package/dist/components/LpModalNav.vue.d.ts +0 -1
  162. package/dist/components/LpModalNav.vue.js +1 -1
  163. package/dist/components/LpNotificationBell.vue.d.ts +11 -2
  164. package/dist/components/LpNotificationBell.vue.js +1 -1
  165. package/dist/components/LpNumberField.vue.d.ts +2 -0
  166. package/dist/components/LpNumberField.vue.js +1 -1
  167. package/dist/components/LpNumberFlow.vue.js +1 -1
  168. package/dist/components/LpOtpInput.vue.js +1 -1
  169. package/dist/components/LpPagination.vue.d.ts +2 -0
  170. package/dist/components/LpPagination.vue.js +1 -1
  171. package/dist/components/LpPasswordInput.vue.js +1 -1
  172. package/dist/components/LpPhoneInput.vue.d.ts +13 -0
  173. package/dist/components/LpPhoneInput.vue.js +1 -1
  174. package/dist/components/LpPie.vue.d.ts +21 -2
  175. package/dist/components/LpPie.vue.js +1 -1
  176. package/dist/components/LpPopover.vue.d.ts +2 -0
  177. package/dist/components/LpPopover.vue.js +1 -1
  178. package/dist/components/LpProgress.vue.d.ts +1 -1
  179. package/dist/components/LpProgress.vue.js +1 -1
  180. package/dist/components/LpRadioGroup.vue.js +1 -1
  181. package/dist/components/LpRangeCalendar.vue.d.ts +51 -0
  182. package/dist/components/LpRangeCalendar.vue.js +4 -0
  183. package/dist/components/LpRollingText.vue.d.ts +1 -1
  184. package/dist/components/LpRollingText.vue.js +1 -1
  185. package/dist/components/LpScrollArea.vue.d.ts +2 -2
  186. package/dist/components/LpScrollArea.vue.js +1 -1
  187. package/dist/components/LpSection.vue.d.ts +1 -1
  188. package/dist/components/LpSection.vue.js +1 -1
  189. package/dist/components/LpSegmented.vue.d.ts +5 -0
  190. package/dist/components/LpSegmented.vue.js +1 -1
  191. package/dist/components/LpSelect.vue.d.ts +1 -0
  192. package/dist/components/LpSelect.vue.js +1 -1
  193. package/dist/components/LpShortcutsDialog.vue.d.ts +7 -1
  194. package/dist/components/LpShortcutsDialog.vue.js +60 -15
  195. package/dist/components/LpSidebar.vue.d.ts +6 -2
  196. package/dist/components/LpSidebar.vue.js +1 -1
  197. package/dist/components/LpSidebarNav.vue.js +1 -1
  198. package/dist/components/LpSkeleton.vue.js +1 -1
  199. package/dist/components/LpSlider.vue.js +1 -1
  200. package/dist/components/LpSparkline.vue.d.ts +1 -0
  201. package/dist/components/LpSparkline.vue.js +1 -1
  202. package/dist/components/LpStat.vue.js +1 -1
  203. package/dist/components/LpStepper.vue.js +1 -1
  204. package/dist/components/LpSwitch.vue.js +1 -1
  205. package/dist/components/LpTable.vue.d.ts +107 -3
  206. package/dist/components/LpTable.vue.js +540 -128
  207. package/dist/components/LpTableFilter.vue.d.ts +31 -0
  208. package/dist/components/LpTableFilter.vue.js +4 -0
  209. package/dist/components/LpTableOfContents.vue.d.ts +4 -2
  210. package/dist/components/LpTableOfContents.vue.js +1 -1
  211. package/dist/components/LpTabs.vue.d.ts +2 -0
  212. package/dist/components/LpTabs.vue.js +1 -1
  213. package/dist/components/LpTagsInput.vue.d.ts +77 -0
  214. package/dist/components/LpTagsInput.vue.js +4 -0
  215. package/dist/components/LpTextarea.vue.d.ts +19 -6
  216. package/dist/components/LpTextarea.vue.js +1 -1
  217. package/dist/components/LpThemeProvider.vue.d.ts +22 -0
  218. package/dist/components/LpThemeProvider.vue.js +4 -0
  219. package/dist/components/LpThemeSwitcher.vue.d.ts +15 -2
  220. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  221. package/dist/components/LpTilt.vue.js +1 -1
  222. package/dist/components/LpTimeField.vue.js +1 -1
  223. package/dist/components/LpToaster.vue.d.ts +6 -1
  224. package/dist/components/LpToaster.vue.js +1 -1
  225. package/dist/components/LpTooltip.vue.d.ts +1 -2
  226. package/dist/components/LpTooltip.vue.js +1 -1
  227. package/dist/components/LpTooltipProvider.vue.d.ts +22 -0
  228. package/dist/components/LpTooltipProvider.vue.js +4 -0
  229. package/dist/components/LpTopologyCanvas.vue.js +1 -1
  230. package/dist/components/LpUptimeBar.vue.d.ts +4 -0
  231. package/dist/components/LpUptimeBar.vue.js +1 -1
  232. package/dist/components/chart.d.ts +32 -1
  233. package/dist/components/chart.js +42 -6
  234. package/dist/components/codeHighlight.d.ts +6 -0
  235. package/dist/components/codeHighlight.js +92 -20
  236. package/dist/components/color.d.ts +1 -1
  237. package/dist/components/color.js +20 -9
  238. package/dist/components/commandSearch.js +3 -0
  239. package/dist/components/confirm.d.ts +25 -0
  240. package/dist/components/confirm.js +26 -0
  241. package/dist/components/countries.d.ts +6 -0
  242. package/dist/components/countries.js +7 -0
  243. package/dist/components/dateRange.d.ts +43 -0
  244. package/dist/components/dateRange.js +76 -0
  245. package/dist/components/exportData.d.ts +58 -0
  246. package/dist/components/exportData.js +165 -0
  247. package/dist/components/fileTree.js +16 -7
  248. package/dist/components/imageCrop.d.ts +42 -0
  249. package/dist/components/imageCrop.js +47 -0
  250. package/dist/components/link.d.ts +12 -0
  251. package/dist/components/link.js +19 -0
  252. package/dist/components/logSearch.d.ts +18 -0
  253. package/dist/components/logSearch.js +36 -0
  254. package/dist/components/modalDepth.d.ts +18 -2
  255. package/dist/components/modalDepth.js +17 -5
  256. package/dist/components/otp.d.ts +5 -0
  257. package/dist/components/otp.js +10 -0
  258. package/dist/components/phone.d.ts +41 -0
  259. package/dist/components/phone.js +80 -0
  260. package/dist/components/sidebar.d.ts +7 -1
  261. package/dist/components/tableFilter.d.ts +36 -0
  262. package/dist/components/tableFilter.js +84 -0
  263. package/dist/components/tableSort.d.ts +8 -0
  264. package/dist/components/tableSort.js +31 -0
  265. package/dist/components/tags.d.ts +20 -0
  266. package/dist/components/tags.js +37 -0
  267. package/dist/composables/useField.d.ts +38 -0
  268. package/dist/composables/useField.js +45 -0
  269. package/dist/composables/useFieldNotice.d.ts +17 -0
  270. package/dist/composables/useFieldNotice.js +28 -0
  271. package/dist/composables/useForm.d.ts +53 -0
  272. package/dist/composables/useForm.js +207 -0
  273. package/dist/composables/useFormDraft.d.ts +10 -3
  274. package/dist/composables/useFormDraft.js +9 -14
  275. package/dist/composables/useHotkeys.d.ts +81 -5
  276. package/dist/composables/useHotkeys.js +223 -25
  277. package/dist/composables/useInputFilter.d.ts +17 -0
  278. package/dist/composables/useInputFilter.js +47 -10
  279. package/dist/composables/useInputMask.d.ts +59 -0
  280. package/dist/composables/useInputMask.js +170 -0
  281. package/dist/composables/useLpConfig.d.ts +24 -0
  282. package/dist/composables/useLpConfig.js +44 -0
  283. package/dist/composables/usePaintSelect.d.ts +22 -0
  284. package/dist/composables/usePaintSelect.js +169 -0
  285. package/dist/composables/useRovingFocus.js +7 -0
  286. package/dist/composables/useShift.js +7 -2
  287. package/dist/composables/useTilt.d.ts +2 -2
  288. package/dist/composables/useTilt.js +5 -6
  289. package/dist/composables/useToast.d.ts +20 -0
  290. package/dist/composables/useToast.js +28 -4
  291. package/dist/composables/useZoomPan.d.ts +3 -3
  292. package/dist/composables/useZoomPan.js +4 -4
  293. package/dist/icons-all.d.ts +1 -0
  294. package/dist/icons-all.js +3 -0
  295. package/dist/icons.d.ts +14 -1
  296. package/dist/index.d.ts +60 -4
  297. package/dist/index.js +245 -141
  298. package/dist/kit-icons.d.ts +2 -0
  299. package/dist/layout/tree.d.ts +2 -0
  300. package/dist/layout/tree.js +16 -1
  301. package/dist/layout/useLayout.d.ts +15 -0
  302. package/dist/layout/useLayout.js +41 -14
  303. package/dist/theme/backdrop.d.ts +12 -0
  304. package/dist/theme/presets.d.ts +0 -2
  305. package/dist/theme/useTheme.d.ts +32 -9
  306. package/package.json +18 -8
  307. package/src/component-names.ts +9 -7
  308. package/src/components/ContextMenuItems.vue +88 -0
  309. package/src/components/LayoutCanvas.vue +26 -6
  310. package/src/components/LayoutNode.vue +30 -4
  311. package/src/components/LpActionBar.vue +31 -8
  312. package/src/components/LpAddressInput.vue +57 -41
  313. package/src/components/LpAlert.vue +16 -3
  314. package/src/components/LpAppShell.vue +45 -12
  315. package/src/components/LpAutocomplete.vue +47 -10
  316. package/src/components/LpAvatar.vue +20 -6
  317. package/src/components/LpBreadcrumbs.vue +65 -41
  318. package/src/components/LpButton.vue +60 -14
  319. package/src/components/LpChart.vue +351 -28
  320. package/src/components/LpCheckbox.vue +7 -3
  321. package/src/components/LpCodeBlock.vue +50 -14
  322. package/src/components/LpColorPicker.vue +73 -10
  323. package/src/components/LpCommandPalette.vue +115 -20
  324. package/src/components/LpConfirmDialog.vue +68 -17
  325. package/src/components/LpConfirmHost.vue +59 -0
  326. package/src/components/LpContextMenu.vue +15 -116
  327. package/src/components/LpDatePicker.vue +104 -68
  328. package/src/components/LpDateRangePicker.vue +224 -0
  329. package/src/components/LpDrawer.vue +56 -19
  330. package/src/components/LpDropdownMenu.vue +1 -0
  331. package/src/components/LpDropzone.vue +21 -4
  332. package/src/components/LpFieldNotice.vue +65 -0
  333. package/src/components/LpFileTree.vue +33 -7
  334. package/src/components/LpFormField.vue +59 -7
  335. package/src/components/LpIcon.vue +22 -4
  336. package/src/components/LpImageEditor.vue +96 -57
  337. package/src/components/LpInput.vue +62 -11
  338. package/src/components/LpLightbox.vue +140 -29
  339. package/src/components/LpLink.vue +24 -13
  340. package/src/components/LpLogViewer.vue +311 -39
  341. package/src/components/LpMapPicker.vue +36 -13
  342. package/src/components/LpMediaGallery.vue +8 -2
  343. package/src/components/LpModal.vue +54 -28
  344. package/src/components/LpModalNav.vue +5 -2
  345. package/src/components/LpNotificationBell.vue +77 -23
  346. package/src/components/LpNumberField.vue +21 -6
  347. package/src/components/LpNumberFlow.vue +26 -10
  348. package/src/components/LpOtpInput.vue +43 -8
  349. package/src/components/LpPagination.vue +23 -7
  350. package/src/components/LpPasswordInput.vue +4 -1
  351. package/src/components/LpPhoneInput.vue +224 -39
  352. package/src/components/LpPie.vue +102 -20
  353. package/src/components/LpPopover.vue +4 -0
  354. package/src/components/LpProgress.vue +5 -3
  355. package/src/components/LpRadioGroup.vue +9 -2
  356. package/src/components/LpRangeCalendar.vue +365 -0
  357. package/src/components/LpRollingText.vue +3 -0
  358. package/src/components/LpScrollArea.vue +22 -2
  359. package/src/components/LpSegmented.vue +12 -2
  360. package/src/components/LpSelect.vue +0 -0
  361. package/src/components/LpShortcutsDialog.vue +83 -9
  362. package/src/components/LpSidebar.vue +14 -9
  363. package/src/components/LpSidebarNav.vue +17 -5
  364. package/src/components/LpSkeleton.vue +4 -1
  365. package/src/components/LpSlider.vue +7 -1
  366. package/src/components/LpSparkline.vue +11 -3
  367. package/src/components/LpStepper.vue +23 -10
  368. package/src/components/LpSwitch.vue +31 -8
  369. package/src/components/LpTable.vue +668 -90
  370. package/src/components/LpTableFilter.vue +175 -0
  371. package/src/components/LpTableOfContents.vue +50 -32
  372. package/src/components/LpTabs.vue +23 -6
  373. package/src/components/LpTagsInput.vue +364 -0
  374. package/src/components/LpTextarea.vue +44 -6
  375. package/src/components/LpThemeProvider.vue +41 -0
  376. package/src/components/LpThemeSwitcher.vue +76 -33
  377. package/src/components/LpTilt.vue +2 -2
  378. package/src/components/LpTimeField.vue +14 -3
  379. package/src/components/LpToaster.vue +115 -27
  380. package/src/components/LpTooltip.vue +29 -5
  381. package/src/components/LpTooltipProvider.vue +24 -0
  382. package/src/components/LpTopologyCanvas.vue +19 -2
  383. package/src/components/LpUptimeBar.vue +46 -8
  384. package/src/components/chart.ts +82 -8
  385. package/src/components/codeHighlight.ts +111 -15
  386. package/src/components/color.ts +30 -9
  387. package/src/components/commandSearch.ts +6 -0
  388. package/src/components/confirm.ts +58 -0
  389. package/src/components/countries.ts +12 -0
  390. package/src/components/dateRange.ts +134 -0
  391. package/src/components/exportData.ts +228 -0
  392. package/src/components/fileTree.ts +19 -7
  393. package/src/components/imageCrop.ts +107 -0
  394. package/src/components/link.ts +34 -0
  395. package/src/components/logSearch.ts +46 -0
  396. package/src/components/modalDepth.ts +48 -6
  397. package/src/components/otp.ts +16 -0
  398. package/src/components/phone.ts +140 -0
  399. package/src/components/sidebar.ts +7 -1
  400. package/src/components/tableFilter.ts +122 -0
  401. package/src/components/tableSort.ts +37 -0
  402. package/src/components/tags.ts +62 -0
  403. package/src/composables/useField.ts +83 -0
  404. package/src/composables/useFieldNotice.ts +59 -0
  405. package/src/composables/useForm.ts +314 -0
  406. package/src/composables/useFormDraft.ts +25 -20
  407. package/src/composables/useHotkeys.ts +355 -28
  408. package/src/composables/useInputFilter.ts +77 -14
  409. package/src/composables/useInputMask.ts +254 -0
  410. package/src/composables/useLpConfig.ts +87 -0
  411. package/src/composables/usePaintSelect.ts +243 -0
  412. package/src/composables/useRovingFocus.ts +11 -0
  413. package/src/composables/useShift.ts +13 -2
  414. package/src/composables/useTilt.ts +5 -6
  415. package/src/composables/useToast.ts +56 -4
  416. package/src/composables/useZoomPan.ts +6 -6
  417. package/src/iconSubset.d.mts +10 -0
  418. package/src/iconSubset.mjs +51 -0
  419. package/src/icons-all.ts +10 -0
  420. package/src/icons.ts +59 -8
  421. package/src/index.ts +73 -4
  422. package/src/kit-icons.ts +5 -0
  423. package/src/layout/tree.ts +30 -0
  424. package/src/layout/useLayout.ts +68 -18
  425. package/src/nuxt.ts +87 -17
  426. package/src/runtime/config.plugin.ts +14 -0
  427. package/src/runtime/theme.plugin.ts +43 -23
  428. package/src/theme/backdrop.ts +69 -0
  429. package/src/theme/presets.ts +6 -64
  430. package/src/theme/useTheme.ts +206 -100
  431. package/src/tokens/tokens.css +46 -0
  432. package/dist/chunks/LpBreadcrumbs-p4jHe_V-.js +0 -62
  433. package/dist/chunks/LpContextMenu-CwW1QXe_.js +0 -217
  434. package/dist/chunks/LpDatePicker-CwOToxGq.js +0 -164
  435. package/dist/chunks/LpFormField-CazBc2-a.js +0 -62
  436. package/dist/chunks/LpPhoneInput-BDSKVl_z.js +0 -209
  437. package/dist/chunks/LpTextarea-t7F6gv8E.js +0 -47
@@ -1,4 +1,14 @@
1
- import { computed, onBeforeUnmount, onMounted, shallowReactive, type MaybeRefOrGetter, toValue } from "vue"
1
+ import {
2
+ computed,
3
+ getCurrentInstance,
4
+ onBeforeUnmount,
5
+ onMounted,
6
+ ref,
7
+ shallowReactive,
8
+ toValue,
9
+ type MaybeRefOrGetter,
10
+ } from "vue"
11
+ import { topModalElement } from "../components/modalDepth"
2
12
 
3
13
  /*
4
14
  * One keydown listener over a registry of bindings, so a shortcut is declared
@@ -20,11 +30,30 @@ import { computed, onBeforeUnmount, onMounted, shallowReactive, type MaybeRefOrG
20
30
  *
21
31
  * Modifier note: Ctrl and Meta are one modifier here, since macOS binds ⌘ where
22
32
  * the rest bind Ctrl. Label the difference in the UI, not in the binding.
33
+ *
34
+ * Sequences ("g i", as on GitHub and Gmail) are bindings with `sequence`. A key
35
+ * that starts a sequence is held while the next one is awaited: if it
36
+ * continues a sequence, the sequence runs; if not, or after the timeout, the
37
+ * single-key binding on the held key (if any) runs, and the new key is handled
38
+ * as a fresh press. A key that starts no sequence runs at once, so sequences
39
+ * cost nothing to the bindings they do not collide with.
23
40
  */
24
41
 
25
- export interface HotkeyBinding {
26
- /** Layout-produced key, compared case-insensitively ("l", "f2", "escape"). */
42
+ /** One key of a sequence: a bare key ("g") or a full chord. */
43
+ export interface HotkeyStep {
27
44
  key: string
45
+ code?: string
46
+ ctrl?: boolean
47
+ shift?: boolean
48
+ alt?: boolean
49
+ }
50
+
51
+ export interface HotkeyBinding {
52
+ /**
53
+ * Layout-produced key, compared case-insensitively ("l", "f2", "escape").
54
+ * Required unless the binding is a `sequence`.
55
+ */
56
+ key?: string
28
57
  /**
29
58
  * Physical key position ("KeyL", "F2", "Digit1"). Matches on its own, which
30
59
  * is what makes non-Latin layouts work.
@@ -34,11 +63,31 @@ export interface HotkeyBinding {
34
63
  ctrl?: boolean
35
64
  shift?: boolean
36
65
  alt?: boolean
66
+ /**
67
+ * Keys pressed one after another: `"g i"`, or steps for chords and physical
68
+ * positions (`[{ key: "g", code: "KeyG" }, { key: "i", code: "KeyI" }]`).
69
+ * The string form splits on spaces, so a Space step needs the array form.
70
+ */
71
+ sequence?: string | (string | HotkeyStep)[]
72
+ /** How long a sequence waits for its next key, in ms. Default 1000. */
73
+ timeout?: number
74
+ /** Extra condition checked on every press; `false` leaves the key alone. */
75
+ when?: (event: KeyboardEvent) => boolean
37
76
  /**
38
77
  * Runs even while a text field has focus. Only for shortcuts that mean
39
78
  * something there — Escape, and the palette the user reaches from anywhere.
40
79
  */
41
80
  allowInInput?: boolean
81
+ /**
82
+ * Runs even while an overlay owns the keyboard. By default a binding declared
83
+ * outside the topmost modal stays quiet while that modal is open, and an
84
+ * Escape binding declared outside the topmost dismissable layer (dialog,
85
+ * popover, menu) leaves Escape to that layer, so a page-level shortcut can
86
+ * neither act behind a dialog nor stop the dialog from closing.
87
+ */
88
+ overLayers?: boolean
89
+ /** Set `false` to ignore auto-repeat while the key is held. */
90
+ repeat?: boolean
42
91
  /**
43
92
  * How the key reference lists this binding while it is mounted. Bindings
44
93
  * without it still work, they just stay out of `activeShortcuts`.
@@ -62,15 +111,20 @@ export interface HotkeyHelp {
62
111
  /**
63
112
  * Written the way it reads, "+"-separated. Defaults to one built from the
64
113
  * binding; set it where the binding is not the whole story, such as the
65
- * second key of a chord ("G+D") or a key that needs Shift to type ("?").
114
+ * key that needs Shift to type ("?"). Sequences read with spaces: "G I".
66
115
  */
67
116
  combo?: string
68
117
  }
69
118
 
70
119
  export interface ShortcutEntry {
71
- /** Rendered one <kbd> per "+"-separated key, so write it the way it reads. */
120
+ /**
121
+ * Rendered one <kbd> per "+"-separated key, so write it the way it reads.
122
+ * Spaces separate the steps of a sequence ("G I", "Ctrl+K Ctrl+S").
123
+ */
72
124
  combo: string
73
125
  label: string
126
+ /** Another mounted binding answers the same keys at the same priority. */
127
+ conflict?: boolean
74
128
  }
75
129
 
76
130
  export interface ShortcutGroup {
@@ -87,12 +141,22 @@ export const isAppleKeyboard =
87
141
  typeof navigator !== "undefined"
88
142
  && /Mac|iPhone|iPad|iPod/.test(navigator.platform || "")
89
143
 
144
+ /**
145
+ * `isAppleKeyboard` for render paths: false until mounted, so the server and
146
+ * the hydrating client print the same label and the client switches after.
147
+ */
148
+ export function useAppleKeyboard() {
149
+ const apple = ref(false)
150
+ onMounted(() => (apple.value = isAppleKeyboard))
151
+ return apple
152
+ }
153
+
90
154
  /**
91
155
  * The symbol to print for a modifier on this platform. Bindings treat Ctrl and
92
156
  * Meta as one modifier, so only the label may differ — never the binding.
93
157
  */
94
- export function appleModifierLabel(key: string): string {
95
- if (!isAppleKeyboard) return key
158
+ export function appleModifierLabel(key: string, apple = isAppleKeyboard): string {
159
+ if (!apple) return key
96
160
  if (key === "Ctrl") return "⌘"
97
161
  if (key === "Alt") return "⌥"
98
162
  return key
@@ -123,17 +187,54 @@ function isTextEntry(target: EventTarget | null): boolean {
123
187
  return false
124
188
  }
125
189
 
126
- function matches(event: KeyboardEvent, binding: HotkeyBinding): boolean {
190
+ const MODIFIER_KEYS = new Set(["control", "meta", "shift", "alt"])
191
+
192
+ type KeyLike = Pick<KeyboardEvent, "key" | "code" | "ctrlKey" | "metaKey" | "shiftKey" | "altKey">
193
+
194
+ export function matchesHotkey(event: KeyLike, binding: HotkeyBinding | HotkeyStep): boolean {
195
+ if (binding.key === undefined) return false
196
+ const key = binding.key.toLowerCase()
127
197
  const ctrlHeld = event.ctrlKey || event.metaKey
128
- if (ctrlHeld !== Boolean(binding.ctrl)) return false
129
- if (event.shiftKey !== Boolean(binding.shift)) return false
130
- if (event.altKey !== Boolean(binding.alt)) return false
198
+ const wantsCtrl = Boolean(binding.ctrl) || key === "control" || key === "meta"
199
+ if (ctrlHeld !== wantsCtrl) return false
200
+ // Shift is part of typing a symbol on most layouts ("?", "+"), so a binding
201
+ // on a symbol that does not mention Shift accepts it either way.
202
+ const symbol = key.length === 1 && key.toLowerCase() === key.toUpperCase()
203
+ const shiftFree = key === "shift" || (binding.shift === undefined && symbol)
204
+ if (!shiftFree && event.shiftKey !== Boolean(binding.shift)) return false
205
+ if (key !== "alt" && event.altKey !== Boolean(binding.alt)) return false
206
+ if (MODIFIER_KEYS.has(key)) return event.key.toLowerCase() === key
131
207
  return (
132
- event.key.toLowerCase() === binding.key.toLowerCase()
208
+ event.key.toLowerCase() === key
133
209
  || (binding.code !== undefined && event.code === binding.code)
134
210
  )
135
211
  }
136
212
 
213
+ /** The keys a binding waits for, in order: one step for a plain binding. */
214
+ export function hotkeySteps(binding: HotkeyBinding): HotkeyStep[] {
215
+ const raw = binding.sequence
216
+ if (raw === undefined) {
217
+ if (binding.key === undefined) return []
218
+ const { key, code, ctrl, shift, alt } = binding
219
+ return [{ key, code, ctrl, shift, alt }]
220
+ }
221
+ const list = typeof raw === "string" ? raw.trim().split(/\s+/) : raw
222
+ return list.map((step) => (typeof step === "string" ? { key: step } : step))
223
+ }
224
+
225
+ /**
226
+ * Where a run of presses stands against a binding: "full" when they spell it
227
+ * out, "prefix" when more keys would, otherwise null.
228
+ */
229
+ export function sequenceProgress(presses: KeyLike[], binding: HotkeyBinding): "full" | "prefix" | null {
230
+ const steps = hotkeySteps(binding)
231
+ if (!steps.length || presses.length > steps.length) return null
232
+ for (let i = 0; i < presses.length; i++) {
233
+ if (!matchesHotkey(presses[i]!, steps[i]!)) return null
234
+ }
235
+ return presses.length === steps.length ? "full" : "prefix"
236
+ }
237
+
137
238
  /**
138
239
  * Bind a list of hotkeys for as long as the calling component is mounted.
139
240
  *
@@ -165,11 +266,23 @@ interface MountedHotkeys {
165
266
  enabled: MaybeRefOrGetter<boolean> | undefined
166
267
  priority: MaybeRefOrGetter<number> | undefined
167
268
  order: number
269
+ /** Where the declaring component sits in the DOM, for the overlay check. */
270
+ host: () => Node | null
168
271
  }
169
272
 
170
- const mounted = shallowReactive(new Set<MountedHotkeys>())
171
- let mountOrder = 0
172
- let listening = false
273
+ // Pinned to a global symbol for the same reason as the toast queue: a barrel
274
+ // and a deep import must not end up with two registries and two listeners.
275
+ const STATE_KEY = Symbol.for("@leavepulse/ui:hotkeys")
276
+ type GlobalWithHotkeys = typeof globalThis & {
277
+ [STATE_KEY]?: { mounted: Set<MountedHotkeys>; order: number; listening: boolean }
278
+ }
279
+ const g = globalThis as GlobalWithHotkeys
280
+ const state = (g[STATE_KEY] ??= {
281
+ mounted: shallowReactive(new Set<MountedHotkeys>()),
282
+ order: 0,
283
+ listening: false,
284
+ })
285
+ const mounted = state.mounted
173
286
 
174
287
  function inScope(target: EventTarget | null, scope: MountedHotkeys["scope"]): boolean {
175
288
  const el = toValue(scope)
@@ -178,47 +291,159 @@ function inScope(target: EventTarget | null, scope: MountedHotkeys["scope"]): bo
178
291
  return !!node && (el === node || el.contains(node))
179
292
  }
180
293
 
181
- function onKeydown(event: KeyboardEvent) {
182
- if (event.isComposing) return
294
+ function topLayer(): Element | null {
295
+ const layers = document.querySelectorAll("[data-dismissable-layer]")
296
+ return layers[layers.length - 1] ?? null
297
+ }
298
+
299
+ function insideOf(entry: MountedHotkeys, layer: Element): boolean {
300
+ const scope = toValue(entry.scope)
301
+ const node = scope ?? entry.host()
302
+ return !!node && layer.contains(node)
303
+ }
304
+
305
+ interface Candidate {
306
+ binding: HotkeyBinding
307
+ event: KeyboardEvent
308
+ }
309
+
310
+ const DEFAULT_TIMEOUT = 1000
311
+ const pressed: KeyLike[] = []
312
+ let held: Candidate[] = []
313
+ let holdTimer: ReturnType<typeof setTimeout> | undefined
314
+ const pendingKeys = ref<string[]>([])
315
+
316
+ /**
317
+ * The keys of a sequence typed so far ("G" after the first key of "g i"),
318
+ * empty when nothing is pending. For a "G…" hint while the next key is awaited.
319
+ */
320
+ export const pendingHotkey = computed(() => pendingKeys.value)
321
+
322
+ function snapshot(event: KeyboardEvent): KeyLike {
323
+ const { key, code, ctrlKey, metaKey, shiftKey, altKey } = event
324
+ return { key, code, ctrlKey, metaKey, shiftKey, altKey }
325
+ }
326
+
327
+ function eligible(event: KeyboardEvent): HotkeyBinding[] {
183
328
  const inTextEntry = isTextEntry(event.target)
329
+ const blocker = event.key === "Escape" ? topLayer() : topModalElement()
184
330
  const entries = [...mounted].sort(
185
331
  (a, b) => Number(toValue(b.priority) || 0) - Number(toValue(a.priority) || 0) || b.order - a.order,
186
332
  )
333
+ const out: HotkeyBinding[] = []
187
334
  for (const entry of entries) {
188
335
  if (toValue(entry.enabled) === false || !inScope(event.target, entry.scope)) continue
336
+ const covered = !!blocker && !insideOf(entry, blocker)
189
337
  for (const binding of entry.read()) {
190
338
  if (inTextEntry && !binding.allowInInput) continue
191
- if (!matches(event, binding)) continue
192
- if (binding.handler(event) === false) continue
339
+ if (event.repeat && binding.repeat === false) continue
340
+ if (covered && !binding.overLayers) continue
341
+ if (binding.when && !binding.when(event)) continue
342
+ out.push(binding)
343
+ }
344
+ }
345
+ return out
346
+ }
347
+
348
+ function run(candidates: Candidate[]): boolean {
349
+ for (const { binding, event } of candidates) {
350
+ if (binding.handler(event) !== false) return true
351
+ }
352
+ return false
353
+ }
354
+
355
+ function resetSequence() {
356
+ clearTimeout(holdTimer)
357
+ holdTimer = undefined
358
+ pressed.length = 0
359
+ held = []
360
+ pendingKeys.value = []
361
+ }
362
+
363
+ function flushHeld() {
364
+ const waiting = held
365
+ resetSequence()
366
+ run(waiting)
367
+ }
368
+
369
+ function onKeydown(event: KeyboardEvent) {
370
+ if (event.isComposing || event.defaultPrevented) return
371
+ if (pressed.length && MODIFIER_KEYS.has(event.key.toLowerCase())) return
372
+ if (pressed.length && event.key === "Escape") {
373
+ resetSequence()
374
+ }
375
+ const presses = [...pressed, snapshot(event)]
376
+ const bindings = eligible(event)
377
+ const full: Candidate[] = []
378
+ let waitFor = 0
379
+ for (const binding of bindings) {
380
+ const progress = sequenceProgress(presses, binding)
381
+ if (progress === "full") full.push({ binding, event })
382
+ else if (progress === "prefix") waitFor = Math.max(waitFor, binding.timeout ?? DEFAULT_TIMEOUT)
383
+ }
384
+ if (waitFor) {
385
+ clearTimeout(holdTimer)
386
+ pressed.push(snapshot(event))
387
+ held = full
388
+ pendingKeys.value = pressed.map(pressLabel)
389
+ holdTimer = setTimeout(flushHeld, waitFor)
390
+ event.preventDefault()
391
+ return
392
+ }
393
+ if (full.length) {
394
+ resetSequence()
395
+ if (run(full)) {
193
396
  event.preventDefault()
194
397
  return
195
398
  }
196
399
  }
400
+ if (pressed.length) {
401
+ flushHeld()
402
+ onKeydown(event)
403
+ }
404
+ }
405
+
406
+ function pressLabel(press: KeyLike): string {
407
+ return chordOf({
408
+ key: press.key,
409
+ ctrl: press.ctrlKey || press.metaKey,
410
+ alt: press.altKey,
411
+ shift: press.shiftKey && press.key.length !== 1,
412
+ })
197
413
  }
198
414
 
199
415
  export function useHotkeys(
200
416
  bindings: MaybeRefOrGetter<HotkeyBinding[]>,
201
417
  options: UseHotkeysOptions = {},
202
418
  ) {
419
+ const instance = getCurrentInstance()
203
420
  const entry: MountedHotkeys = {
204
421
  read: () => toValue(bindings),
205
422
  scope: options.scope,
206
423
  enabled: options.enabled,
207
424
  priority: options.priority,
208
- order: ++mountOrder,
425
+ order: ++state.order,
426
+ host: () => {
427
+ const el = instance?.vnode.el
428
+ return el instanceof Node ? el : null
429
+ },
209
430
  }
210
431
  onMounted(() => {
211
432
  mounted.add(entry)
212
- if (!listening) {
433
+ warnConflicts()
434
+ if (!state.listening) {
213
435
  window.addEventListener("keydown", onKeydown)
214
- listening = true
436
+ window.addEventListener("blur", resetSequence)
437
+ state.listening = true
215
438
  }
216
439
  })
217
440
  onBeforeUnmount(() => {
218
441
  mounted.delete(entry)
219
- if (!mounted.size && listening) {
442
+ if (!mounted.size && state.listening) {
220
443
  window.removeEventListener("keydown", onKeydown)
221
- listening = false
444
+ window.removeEventListener("blur", resetSequence)
445
+ state.listening = false
446
+ resetSequence()
222
447
  }
223
448
  })
224
449
  }
@@ -236,10 +461,110 @@ const KEY_NAMES: Record<string, string> = {
236
461
  arrowright: "→",
237
462
  }
238
463
 
464
+ function chordOf(step: HotkeyStep): string {
465
+ const key = step.key.toLowerCase()
466
+ const name = KEY_NAMES[key] ?? (key.length === 1 || /^f\d+$/.test(key) ? key.toUpperCase() : step.key)
467
+ return [step.ctrl && "Ctrl", step.alt && "Alt", step.shift && "Shift", name].filter(Boolean).join("+")
468
+ }
469
+
239
470
  function comboOf(binding: HotkeyBinding): string {
240
- const key = binding.key.toLowerCase()
241
- const name = KEY_NAMES[key] ?? (key.length === 1 || /^f\d+$/.test(key) ? key.toUpperCase() : binding.key)
242
- return [binding.ctrl && "Ctrl", binding.alt && "Alt", binding.shift && "Shift", name].filter(Boolean).join("+")
471
+ return hotkeySteps(binding).map(chordOf).join(" ")
472
+ }
473
+
474
+ /** Identity of the keys a binding answers, for telling two of them apart. */
475
+ function signatureOf(binding: HotkeyBinding): string {
476
+ return hotkeySteps(binding)
477
+ .map((step) => {
478
+ const key = step.key.toLowerCase()
479
+ const symbol = key.length === 1 && key === key.toUpperCase()
480
+ const shift = step.shift === undefined && symbol ? "*" : String(Boolean(step.shift))
481
+ return `${Boolean(step.ctrl)}:${Boolean(step.alt)}:${shift}:${key}`
482
+ })
483
+ .join(" ")
484
+ }
485
+
486
+ export interface HotkeyConflict {
487
+ /** The keys both answer, written as in the shortcut reference. */
488
+ combo: string
489
+ /** `help` labels of the clashing bindings; unlabelled ones are omitted. */
490
+ labels: string[]
491
+ priority: number
492
+ }
493
+
494
+ /**
495
+ * Pure part of `hotkeyConflicts`: bindings from different `useHotkeys` calls
496
+ * answering the same keys at the same priority within the same scope. Within
497
+ * one call a repeat is deliberate (the first may decline), and a binding with
498
+ * `when` is conditional by design, so neither counts.
499
+ */
500
+ export function findHotkeyConflicts(
501
+ groups: { bindings: HotkeyBinding[]; priority: number; scope: unknown }[],
502
+ ): HotkeyConflict[] {
503
+ const scopeIds = new Map<unknown, number>()
504
+ const seen = new Map<string, { combo: string; labels: string[]; owners: Set<number>; priority: number }>()
505
+ groups.forEach((group, owner) => {
506
+ if (!scopeIds.has(group.scope)) scopeIds.set(group.scope, scopeIds.size)
507
+ for (const binding of group.bindings) {
508
+ if (binding.when || !hotkeySteps(binding).length) continue
509
+ const id = `${group.priority}|${scopeIds.get(group.scope)}|${signatureOf(binding)}`
510
+ const slot = seen.get(id) ?? {
511
+ combo: binding.help?.combo ?? comboOf(binding),
512
+ labels: [],
513
+ owners: new Set<number>(),
514
+ priority: group.priority,
515
+ }
516
+ slot.owners.add(owner)
517
+ if (binding.help && !slot.labels.includes(binding.help.label)) slot.labels.push(binding.help.label)
518
+ seen.set(id, slot)
519
+ }
520
+ })
521
+ return [...seen.values()]
522
+ .filter((slot) => slot.owners.size > 1)
523
+ .map(({ combo, labels, priority }) => ({ combo, labels, priority }))
524
+ }
525
+
526
+ /** Bindings among the mounted ones that answer the same keys. */
527
+ export const hotkeyConflicts = computed<HotkeyConflict[]>(() =>
528
+ findHotkeyConflicts(
529
+ [...mounted]
530
+ .filter((entry) => toValue(entry.enabled) !== false)
531
+ .map((entry) => ({
532
+ bindings: entry.read(),
533
+ priority: Number(toValue(entry.priority) || 0),
534
+ scope: toValue(entry.scope) ?? null,
535
+ })),
536
+ ),
537
+ )
538
+
539
+ const warned = new Set<string>()
540
+
541
+ declare const process: { env: { NODE_ENV?: string } }
542
+
543
+ /*
544
+ * Written as the literal `process.env.NODE_ENV` so the consumer's bundler
545
+ * replaces it (Vite and webpack both do for dependencies); where nothing
546
+ * replaces it and no `process` exists, the warning simply stays off.
547
+ */
548
+ function isDev(): boolean {
549
+ try {
550
+ return process.env.NODE_ENV !== "production"
551
+ } catch {
552
+ return false
553
+ }
554
+ }
555
+
556
+ function warnConflicts() {
557
+ if (!isDev()) return
558
+ for (const conflict of hotkeyConflicts.value) {
559
+ const id = `${conflict.priority}|${conflict.combo}`
560
+ if (warned.has(id)) continue
561
+ warned.add(id)
562
+ console.warn(
563
+ `[@leavepulse/ui] ${conflict.combo} is bound more than once at priority ${conflict.priority}`
564
+ + (conflict.labels.length ? ` (${conflict.labels.join(", ")})` : "")
565
+ + "; only the most recently mounted one runs unless it declines.",
566
+ )
567
+ }
243
568
  }
244
569
 
245
570
  /**
@@ -250,13 +575,15 @@ function comboOf(binding: HotkeyBinding): string {
250
575
  */
251
576
  export const activeShortcuts = computed<ShortcutGroup[]>(() => {
252
577
  const groups = new Map<string, ShortcutEntry[]>()
578
+ const clashing = new Set(hotkeyConflicts.value.map((c) => c.combo))
253
579
  for (const entry of mounted) {
254
580
  if (toValue(entry.enabled) === false) continue
255
581
  for (const binding of entry.read()) {
256
582
  if (!binding.help) continue
257
583
  const combo = binding.help.combo ?? comboOf(binding)
258
584
  const entries = groups.get(binding.help.group) ?? []
259
- if (!entries.some((e) => e.combo === combo)) entries.push({ combo, label: binding.help.label })
585
+ const conflict = clashing.has(combo) || undefined
586
+ if (!entries.some((e) => e.combo === combo)) entries.push({ combo, label: binding.help.label, conflict })
260
587
  groups.set(binding.help.group, entries)
261
588
  }
262
589
  }
@@ -18,6 +18,12 @@ import { computed, type MaybeRefOrGetter, toValue } from "vue"
18
18
  *
19
19
  * A bare string pattern is treated as a full-string match (anchored), mirroring
20
20
  * the native HTML `pattern` attribute.
21
+ *
22
+ * Composition cannot be vetoed (its `beforeinput` is not cancelable), and on
23
+ * Android most keyboards compose ordinary typing, so `beforeinput` alone left
24
+ * `restrict` doing nothing there. `onInput` and `onCompositionEnd` clean up
25
+ * whatever got through — composed text, a drop, an autocorrect replacement —
26
+ * keeping the caret where it was. Bind `onInput` BEFORE reading the value.
21
27
  */
22
28
 
23
29
  export interface UseInputFilterOptions {
@@ -27,31 +33,57 @@ export interface UseInputFilterOptions {
27
33
  pattern?: MaybeRefOrGetter<RegExp | string | undefined>
28
34
  }
29
35
 
30
- function toAnchored(p: RegExp | string): RegExp {
31
- if (p instanceof RegExp) {
32
- // Re-anchor so `pattern` validates the whole string, like HTML's attribute.
33
- const src = p.source.replace(/^\^/, "").replace(/\$$/, "")
34
- return new RegExp(`^(?:${src})$`, p.flags.replace(/[gy]/g, ""))
35
- }
36
+ /**
37
+ * Anchor a pattern to the whole string, like HTML's `pattern` attribute. The
38
+ * source is wrapped, never edited: anchors the caller wrote stay valid inside
39
+ * the group, while trimming a trailing `$` broke an escaped `\$` into an
40
+ * unterminated group.
41
+ */
42
+ export function anchorPattern(p: RegExp | string): RegExp {
43
+ if (p instanceof RegExp) return new RegExp(`^(?:${p.source})$`, p.flags.replace(/[gy]/g, ""))
36
44
  return new RegExp(`^(?:${p})$`)
37
45
  }
38
46
 
47
+ // Strip g/y so lastIndex can't leak state across .test() calls.
48
+ function charTester(charRe: RegExp): (ch: string) => boolean {
49
+ const re = new RegExp(charRe.source, charRe.flags.replace(/[gy]/g, ""))
50
+ return (ch) => re.test(ch)
51
+ }
52
+
39
53
  /** Does a string contain only restrict-allowed characters? */
40
54
  function allAllowed(text: string, charRe: RegExp): boolean {
41
- // Strip g/y so lastIndex can't leak state across .test() calls, then reuse
42
- // the one instance per char (tested by code point, so surrogate pairs hold).
43
- const re = new RegExp(charRe.source, charRe.flags.replace(/[gy]/g, ""))
55
+ const ok = charTester(charRe)
56
+ // By code point, so surrogate pairs hold.
44
57
  for (const ch of text) {
45
- if (!re.test(ch)) return false
58
+ if (!ok(ch)) return false
46
59
  }
47
60
  return true
48
61
  }
49
62
 
63
+ /**
64
+ * `text` with every disallowed character removed, and where `caret` lands once
65
+ * the characters before it that were removed are gone.
66
+ */
67
+ export function stripDisallowed(text: string, charRe: RegExp, caret = text.length): { text: string; caret: number } {
68
+ const ok = charTester(charRe)
69
+ let kept = ""
70
+ let at = 0
71
+ let offset = 0
72
+ for (const ch of text) {
73
+ if (ok(ch)) {
74
+ kept += ch
75
+ if (offset < caret) at += ch.length
76
+ }
77
+ offset += ch.length
78
+ }
79
+ return { text: kept, caret: at }
80
+ }
81
+
50
82
  export function useInputFilter(options: UseInputFilterOptions = {}) {
51
83
  const restrict = computed(() => toValue(options.restrict))
52
84
  const patternRe = computed(() => {
53
85
  const p = toValue(options.pattern)
54
- return p === undefined ? undefined : toAnchored(p)
86
+ return p === undefined ? undefined : anchorPattern(p)
55
87
  })
56
88
 
57
89
  /** True when a non-empty value fails `pattern`. Empty is treated as valid. */
@@ -68,12 +100,43 @@ export function useInputFilter(options: UseInputFilterOptions = {}) {
68
100
  function onBeforeInput(event: InputEvent): void {
69
101
  const re = restrict.value
70
102
  if (!re || event.isComposing) return
71
- // Deletions / formatting carry no data to validate; let them through.
72
- const data = event.data
103
+ // A drop or a replacement carries its text in dataTransfer, not data.
104
+ // Deletions / formatting carry neither; let them through.
105
+ const data = event.data ?? event.dataTransfer?.getData("text") ?? null
73
106
  if (data == null) return
74
107
  if (!allAllowed(data, re)) event.preventDefault()
75
108
  }
76
109
 
110
+ // Remove what slipped past `beforeinput`; true when the value changed.
111
+ function sanitize(target: EventTarget | null): boolean {
112
+ const re = restrict.value
113
+ if (!re) return false
114
+ if (!(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)) return false
115
+ const { text, caret } = stripDisallowed(target.value, re, target.selectionStart ?? target.value.length)
116
+ if (text === target.value) return false
117
+ target.value = text
118
+ try {
119
+ target.setSelectionRange(caret, caret)
120
+ } catch {
121
+ // Input types without a selection (number, email) cannot place a caret.
122
+ }
123
+ return true
124
+ }
125
+
126
+ /** Attach to `input`, ahead of whatever reads the value. */
127
+ function onInput(event: Event): void {
128
+ if ((event as InputEvent).isComposing) return
129
+ sanitize(event.target)
130
+ }
131
+
132
+ /**
133
+ * Attach to `compositionend`. Some browsers fire the last `input` before
134
+ * this, so a cleaned value is announced with a fresh `input` event.
135
+ */
136
+ function onCompositionEnd(event: CompositionEvent): void {
137
+ if (sanitize(event.target)) event.target?.dispatchEvent(new Event("input", { bubbles: true }))
138
+ }
139
+
77
140
  /**
78
141
  * Attach to `paste`. Veto the paste if any pasted char is disallowed, rather
79
142
  * than silently dropping part of it — partial pastes confuse more than help.
@@ -85,5 +148,5 @@ export function useInputFilter(options: UseInputFilterOptions = {}) {
85
148
  if (text && !allAllowed(text, re)) event.preventDefault()
86
149
  }
87
150
 
88
- return { isInvalid, onBeforeInput, onPaste, patternRe, restrict }
151
+ return { isInvalid, onBeforeInput, onInput, onCompositionEnd, onPaste, patternRe, restrict }
89
152
  }