@leavepulse/ui 0.43.4 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/README.md +75 -7
  2. package/dist/assets/{LpCommandPalette-B-0Est4Z.css → LpCommandPalette-DCP5g2ik.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/{LpShortcutsDialog-C6YoQ8_M.css → LpShortcutsDialog-ChUyU7oV.css} +7 -7
  6. package/dist/assets/{LpSkeleton-D7xU8IPV.css → LpSkeleton-H3Z5dEIk.css} +4 -1
  7. package/dist/assets/{LpTable-BgPLJOXa.css → LpTable-DDQNdf86.css} +9 -9
  8. package/dist/chunks/ContextMenuItems-CrfeNPWT.js +123 -0
  9. package/dist/chunks/{LayoutCanvas-Dd20boUG.js → LayoutCanvas-D-jPOAvg.js} +26 -9
  10. package/dist/chunks/{LayoutNode-DGpZA9LX.js → LayoutNode-D8Xy9N-o.js} +20 -8
  11. package/dist/chunks/{LpActionBar-BlvtGB2P.js → LpActionBar-BbwCF6l5.js} +24 -10
  12. package/dist/chunks/{LpAddressInput-B-Zvxsn7.js → LpAddressInput-BrOqkZqg.js} +53 -48
  13. package/dist/chunks/{LpAlert-BAYHDSS1.js → LpAlert-DkOIjsbq.js} +15 -8
  14. package/dist/chunks/{LpAppShell-BGQVIY-3.js → LpAppShell-BUKPcHco.js} +51 -24
  15. package/dist/chunks/{LpAutocomplete-DExlIZe_.js → LpAutocomplete-DxV3Y9h4.js} +50 -23
  16. package/dist/chunks/{LpAvatar-B3-xAOH3.js → LpAvatar-ClBbEYZN.js} +17 -8
  17. package/dist/chunks/LpBreadcrumbs-B8IJgFIc.js +95 -0
  18. package/dist/chunks/{LpButton-5VXXCjwF.js → LpButton-kqJBHIIb.js} +44 -14
  19. package/dist/chunks/{LpCard-BhI1N_vL.js → LpCard-CpRByR0q.js} +1 -1
  20. package/dist/chunks/{LpCheckbox-CVccKYaH.js → LpCheckbox-DpqchzH4.js} +7 -3
  21. package/dist/chunks/{LpCodeBlock-Bo0B4TFR.js → LpCodeBlock-DeHJPq7p.js} +48 -16
  22. package/dist/chunks/{LpColorPicker-BlXea7LM.js → LpColorPicker-Bhf1qxhB.js} +32 -9
  23. package/dist/chunks/{LpConfirmDialog-Q-bxff8z.js → LpConfirmDialog-CIUOSnWo.js} +58 -16
  24. package/dist/chunks/LpConfirmHost-CDm0Wtpv.js +49 -0
  25. package/dist/chunks/LpContextMenu-DQw5dGfG.js +68 -0
  26. package/dist/chunks/LpDatePicker-Drirx1AK.js +191 -0
  27. package/dist/chunks/{LpDrawer-BZ6k3hvo.js → LpDrawer-bScx-KMO.js} +63 -26
  28. package/dist/chunks/{LpFileTree-_5vFBgaK.js → LpFileTree-CSI_5SK2.js} +23 -8
  29. package/dist/chunks/{LpFileTreeNode-BdcIYKIE.js → LpFileTreeNode-B4MiSQX2.js} +2 -2
  30. package/dist/chunks/LpFormField-CIk5qosh.js +87 -0
  31. package/dist/chunks/{LpInput-9EXWUM9B.js → LpInput-C0e8y8_d.js} +26 -11
  32. package/dist/chunks/{LpLightbox-2sYVMY-S.js → LpLightbox-Mr0W_6HK.js} +147 -60
  33. package/dist/chunks/{LpLink-DCND-zyu.js → LpLink-Krkjhe_y.js} +15 -6
  34. package/dist/chunks/{LpLogViewer-CaRy5MeS.js → LpLogViewer-YBOaFnc_.js} +68 -34
  35. package/dist/chunks/{LpMapPicker-ul4oYno6.js → LpMapPicker-NUzS9e37.js} +38 -13
  36. package/dist/chunks/{LpMediaGallery-FF-NquIo.js → LpMediaGallery-DfJmMW0I.js} +6 -3
  37. package/dist/chunks/{LpModal-Bt0pQCI_.js → LpModal-DPwGKBPv.js} +49 -26
  38. package/dist/chunks/{LpModalNav-YaifHrXC.js → LpModalNav-DPg5H9f3.js} +9 -6
  39. package/dist/chunks/{LpNotificationBell-DyGGsMAM.js → LpNotificationBell-t5yn-siX.js} +58 -24
  40. package/dist/chunks/{LpNumberField-CWuX08iX.js → LpNumberField-DWnQKktS.js} +20 -7
  41. package/dist/chunks/{LpNumberFlow-amWYaeuA.js → LpNumberFlow-z4WPZcZu.js} +22 -12
  42. package/dist/chunks/{LpOtpInput-y2v-pmiN.js → LpOtpInput-CziPF3Oq.js} +37 -8
  43. package/dist/chunks/{LpPagination-5FxjmmB5.js → LpPagination-DFngX7Xo.js} +30 -19
  44. package/dist/chunks/{LpPasswordInput--sjYCkLS.js → LpPasswordInput-CWVDuBJ5.js} +10 -7
  45. package/dist/chunks/{LpPhoneInput-BDSKVl_z.js → LpPhoneInput-CD-ZTeSd.js} +57 -33
  46. package/dist/chunks/{LpPie-D6OnzwlJ.js → LpPie-BLco31Nx.js} +78 -21
  47. package/dist/chunks/{LpPopover-8268UmlC.js → LpPopover-p_e_shN3.js} +4 -2
  48. package/dist/chunks/{LpProgress-Btq5W4Ut.js → LpProgress-CBU1iI69.js} +5 -4
  49. package/dist/chunks/{LpRadioGroup-xPIuVDO_.js → LpRadioGroup-DmEW0l78.js} +9 -2
  50. package/dist/chunks/{LpSegmented-fV0Sk92p.js → LpSegmented-Dejd7cIx.js} +7 -3
  51. package/dist/chunks/{LpSelect-ANIVtrpM.js → LpSelect-DrRD9I7L.js} +40 -18
  52. package/dist/chunks/{LpSidebar-ClBUwoEz.js → LpSidebar-BgwUNWpJ.js} +30 -22
  53. package/dist/chunks/{LpSidebarNav-llOcs_Fn.js → LpSidebarNav-Bqxz8aWT.js} +14 -9
  54. package/dist/chunks/{LpSkeleton-CTI-vBgW.js → LpSkeleton-BB6XIV-w.js} +1 -1
  55. package/dist/chunks/{LpSlider-BwNIBkAK.js → LpSlider-Cuzmrd7p.js} +7 -2
  56. package/dist/chunks/{LpSparkline-C6xQ2dwv.js → LpSparkline-CB80N8SJ.js} +11 -4
  57. package/dist/chunks/{LpStat-CQaW8R--.js → LpStat-dXtyrdX9.js} +1 -1
  58. package/dist/chunks/{LpSwitch-Blizvqyt.js → LpSwitch-B1aLKulj.js} +40 -26
  59. package/dist/chunks/{LpTableOfContents-CjvxPMpU.js → LpTableOfContents-BUcAfGPQ.js} +36 -41
  60. package/dist/chunks/{LpTabs-Cefe_lys.js → LpTabs-_qMVaZJ6.js} +19 -5
  61. package/dist/chunks/LpTextarea-yP8hGedR.js +77 -0
  62. package/dist/chunks/LpThemeProvider-Wjm7Tuit.js +35 -0
  63. package/dist/chunks/LpThemeSwitcher-CKLc10KZ.js +169 -0
  64. package/dist/chunks/{LpTilt-DuyKbpkA.js → LpTilt-B7KHX1zH.js} +2 -2
  65. package/dist/chunks/{LpTimeField-K6nAGboX.js → LpTimeField-C5HRgS5H.js} +13 -7
  66. package/dist/chunks/{LpToaster-gDB1EaU3.js → LpToaster--oT6lfhL.js} +57 -28
  67. package/dist/chunks/{LpTooltip-hCLPAqMG.js → LpTooltip-CBdzeKNf.js} +13 -6
  68. package/dist/chunks/LpTooltipProvider-CftjV7Xc.js +25 -0
  69. package/dist/chunks/{LpTopologyCanvas-Cm1L39uL.js → LpTopologyCanvas-DocsLal3.js} +6 -1
  70. package/dist/chunks/{LpUptimeBar-DvAMno0S.js → LpUptimeBar-DLtxOMRy.js} +43 -14
  71. package/dist/chunks/{LpThemeSwitcher-Cue-myST.js → useTheme-C8JHUlPM.js} +173 -215
  72. package/dist/component-names.d.ts +1 -1
  73. package/dist/components/ContextMenuItems.vue.d.ts +8 -0
  74. package/dist/components/ContextMenuItems.vue.js +4 -0
  75. package/dist/components/LayoutCanvas.vue.js +1 -1
  76. package/dist/components/LayoutNode.vue.d.ts +4 -0
  77. package/dist/components/LayoutNode.vue.js +1 -1
  78. package/dist/components/LpActionBar.vue.d.ts +3 -0
  79. package/dist/components/LpActionBar.vue.js +1 -1
  80. package/dist/components/LpAddressInput.vue.d.ts +1 -1
  81. package/dist/components/LpAddressInput.vue.js +1 -1
  82. package/dist/components/LpAlert.vue.d.ts +9 -0
  83. package/dist/components/LpAlert.vue.js +1 -1
  84. package/dist/components/LpAppShell.vue.d.ts +10 -0
  85. package/dist/components/LpAppShell.vue.js +1 -1
  86. package/dist/components/LpAutocomplete.vue.d.ts +31 -6
  87. package/dist/components/LpAutocomplete.vue.js +1 -1
  88. package/dist/components/LpAvatar.vue.d.ts +3 -3
  89. package/dist/components/LpAvatar.vue.js +1 -1
  90. package/dist/components/LpBadge.vue.d.ts +2 -2
  91. package/dist/components/LpBreadcrumbs.vue.d.ts +13 -9
  92. package/dist/components/LpBreadcrumbs.vue.js +1 -1
  93. package/dist/components/LpButton.vue.d.ts +35 -98
  94. package/dist/components/LpButton.vue.js +1 -1
  95. package/dist/components/LpCard.vue.js +1 -1
  96. package/dist/components/LpChart.vue.d.ts +31 -2
  97. package/dist/components/LpChart.vue.js +317 -95
  98. package/dist/components/LpCheckbox.vue.js +1 -1
  99. package/dist/components/LpCodeBlock.vue.d.ts +2 -0
  100. package/dist/components/LpCodeBlock.vue.js +1 -1
  101. package/dist/components/LpColorPicker.vue.js +1 -1
  102. package/dist/components/LpCommandPalette.vue.d.ts +8 -3
  103. package/dist/components/LpCommandPalette.vue.js +86 -72
  104. package/dist/components/LpConfirmDialog.vue.d.ts +15 -3
  105. package/dist/components/LpConfirmDialog.vue.js +1 -1
  106. package/dist/components/LpConfirmHost.vue.d.ts +9 -0
  107. package/dist/components/LpConfirmHost.vue.js +4 -0
  108. package/dist/components/LpContextMenu.vue.d.ts +3 -5
  109. package/dist/components/LpContextMenu.vue.js +1 -1
  110. package/dist/components/LpDatePicker.vue.d.ts +24 -6
  111. package/dist/components/LpDatePicker.vue.js +1 -1
  112. package/dist/components/LpDrawer.vue.d.ts +10 -0
  113. package/dist/components/LpDrawer.vue.js +1 -1
  114. package/dist/components/LpDropzone.vue.js +22 -6
  115. package/dist/components/LpFileTree.vue.d.ts +11 -2
  116. package/dist/components/LpFileTree.vue.js +1 -1
  117. package/dist/components/LpFileTreeNode.vue.js +1 -1
  118. package/dist/components/LpFormField.vue.d.ts +9 -0
  119. package/dist/components/LpFormField.vue.js +1 -1
  120. package/dist/components/LpImageEditor.vue.js +92 -57
  121. package/dist/components/LpInput.vue.d.ts +3 -3
  122. package/dist/components/LpInput.vue.js +1 -1
  123. package/dist/components/LpLightbox.vue.d.ts +17 -0
  124. package/dist/components/LpLightbox.vue.js +1 -1
  125. package/dist/components/LpLink.vue.d.ts +11 -11
  126. package/dist/components/LpLink.vue.js +1 -1
  127. package/dist/components/LpLogViewer.vue.d.ts +20 -2
  128. package/dist/components/LpLogViewer.vue.js +1 -1
  129. package/dist/components/LpMapPicker.vue.d.ts +4 -4
  130. package/dist/components/LpMapPicker.vue.js +1 -1
  131. package/dist/components/LpMediaGallery.vue.js +1 -1
  132. package/dist/components/LpModal.vue.d.ts +16 -6
  133. package/dist/components/LpModal.vue.js +1 -1
  134. package/dist/components/LpModalNav.vue.d.ts +0 -1
  135. package/dist/components/LpModalNav.vue.js +1 -1
  136. package/dist/components/LpNotificationBell.vue.d.ts +11 -2
  137. package/dist/components/LpNotificationBell.vue.js +1 -1
  138. package/dist/components/LpNumberField.vue.d.ts +2 -0
  139. package/dist/components/LpNumberField.vue.js +1 -1
  140. package/dist/components/LpNumberFlow.vue.js +1 -1
  141. package/dist/components/LpOtpInput.vue.js +1 -1
  142. package/dist/components/LpPagination.vue.d.ts +2 -0
  143. package/dist/components/LpPagination.vue.js +1 -1
  144. package/dist/components/LpPasswordInput.vue.js +1 -1
  145. package/dist/components/LpPhoneInput.vue.js +1 -1
  146. package/dist/components/LpPie.vue.d.ts +21 -2
  147. package/dist/components/LpPie.vue.js +1 -1
  148. package/dist/components/LpPopover.vue.d.ts +2 -0
  149. package/dist/components/LpPopover.vue.js +1 -1
  150. package/dist/components/LpProgress.vue.js +1 -1
  151. package/dist/components/LpRadioGroup.vue.js +1 -1
  152. package/dist/components/LpSection.vue.d.ts +1 -1
  153. package/dist/components/LpSegmented.vue.d.ts +5 -0
  154. package/dist/components/LpSegmented.vue.js +1 -1
  155. package/dist/components/LpSelect.vue.d.ts +1 -0
  156. package/dist/components/LpSelect.vue.js +1 -1
  157. package/dist/components/LpShortcutsDialog.vue.d.ts +1 -1
  158. package/dist/components/LpShortcutsDialog.vue.js +11 -8
  159. package/dist/components/LpSidebar.vue.d.ts +6 -2
  160. package/dist/components/LpSidebar.vue.js +1 -1
  161. package/dist/components/LpSidebarNav.vue.js +1 -1
  162. package/dist/components/LpSkeleton.vue.js +1 -1
  163. package/dist/components/LpSlider.vue.js +1 -1
  164. package/dist/components/LpSparkline.vue.d.ts +1 -0
  165. package/dist/components/LpSparkline.vue.js +1 -1
  166. package/dist/components/LpStat.vue.js +1 -1
  167. package/dist/components/LpSwitch.vue.js +1 -1
  168. package/dist/components/LpTable.vue.d.ts +23 -1
  169. package/dist/components/LpTable.vue.js +74 -36
  170. package/dist/components/LpTableOfContents.vue.d.ts +4 -2
  171. package/dist/components/LpTableOfContents.vue.js +1 -1
  172. package/dist/components/LpTabs.vue.d.ts +2 -0
  173. package/dist/components/LpTabs.vue.js +1 -1
  174. package/dist/components/LpTextarea.vue.d.ts +19 -6
  175. package/dist/components/LpTextarea.vue.js +1 -1
  176. package/dist/components/LpThemeProvider.vue.d.ts +22 -0
  177. package/dist/components/LpThemeProvider.vue.js +4 -0
  178. package/dist/components/LpThemeSwitcher.vue.d.ts +13 -0
  179. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  180. package/dist/components/LpTilt.vue.js +1 -1
  181. package/dist/components/LpTimeField.vue.js +1 -1
  182. package/dist/components/LpToaster.vue.d.ts +6 -1
  183. package/dist/components/LpToaster.vue.js +1 -1
  184. package/dist/components/LpTooltip.vue.d.ts +1 -2
  185. package/dist/components/LpTooltip.vue.js +1 -1
  186. package/dist/components/LpTooltipProvider.vue.d.ts +22 -0
  187. package/dist/components/LpTooltipProvider.vue.js +4 -0
  188. package/dist/components/LpTopologyCanvas.vue.js +1 -1
  189. package/dist/components/LpUptimeBar.vue.d.ts +4 -0
  190. package/dist/components/LpUptimeBar.vue.js +1 -1
  191. package/dist/components/chart.d.ts +32 -1
  192. package/dist/components/chart.js +39 -4
  193. package/dist/components/codeHighlight.d.ts +6 -0
  194. package/dist/components/codeHighlight.js +92 -20
  195. package/dist/components/color.d.ts +1 -1
  196. package/dist/components/color.js +20 -9
  197. package/dist/components/confirm.d.ts +25 -0
  198. package/dist/components/confirm.js +26 -0
  199. package/dist/components/countries.d.ts +6 -0
  200. package/dist/components/countries.js +7 -0
  201. package/dist/components/fileTree.js +16 -7
  202. package/dist/components/imageCrop.d.ts +42 -0
  203. package/dist/components/imageCrop.js +47 -0
  204. package/dist/components/link.d.ts +12 -0
  205. package/dist/components/link.js +19 -0
  206. package/dist/components/modalDepth.d.ts +18 -2
  207. package/dist/components/modalDepth.js +17 -5
  208. package/dist/components/otp.d.ts +5 -0
  209. package/dist/components/otp.js +10 -0
  210. package/dist/components/sidebar.d.ts +7 -1
  211. package/dist/components/tableSort.d.ts +8 -0
  212. package/dist/components/tableSort.js +31 -0
  213. package/dist/composables/useField.d.ts +34 -0
  214. package/dist/composables/useField.js +45 -0
  215. package/dist/composables/useFormDraft.d.ts +10 -3
  216. package/dist/composables/useFormDraft.js +9 -14
  217. package/dist/composables/useHotkeys.d.ts +17 -1
  218. package/dist/composables/useHotkeys.js +55 -18
  219. package/dist/composables/useInputFilter.d.ts +17 -0
  220. package/dist/composables/useInputFilter.js +47 -10
  221. package/dist/composables/useLpConfig.d.ts +24 -0
  222. package/dist/composables/useLpConfig.js +44 -0
  223. package/dist/composables/useRovingFocus.js +7 -0
  224. package/dist/composables/useTilt.d.ts +2 -2
  225. package/dist/composables/useTilt.js +5 -6
  226. package/dist/composables/useToast.d.ts +20 -0
  227. package/dist/composables/useToast.js +28 -4
  228. package/dist/composables/useZoomPan.d.ts +3 -3
  229. package/dist/composables/useZoomPan.js +4 -4
  230. package/dist/index.d.ts +32 -2
  231. package/dist/index.js +184 -137
  232. package/dist/layout/tree.d.ts +2 -0
  233. package/dist/layout/tree.js +16 -1
  234. package/dist/layout/useLayout.d.ts +15 -0
  235. package/dist/layout/useLayout.js +41 -14
  236. package/dist/theme/useTheme.d.ts +32 -9
  237. package/package.json +8 -7
  238. package/src/component-names.ts +5 -7
  239. package/src/components/ContextMenuItems.vue +88 -0
  240. package/src/components/LayoutCanvas.vue +26 -6
  241. package/src/components/LayoutNode.vue +19 -2
  242. package/src/components/LpActionBar.vue +30 -7
  243. package/src/components/LpAddressInput.vue +57 -41
  244. package/src/components/LpAlert.vue +16 -3
  245. package/src/components/LpAppShell.vue +45 -12
  246. package/src/components/LpAutocomplete.vue +46 -10
  247. package/src/components/LpAvatar.vue +19 -5
  248. package/src/components/LpBreadcrumbs.vue +65 -41
  249. package/src/components/LpButton.vue +59 -13
  250. package/src/components/LpChart.vue +272 -28
  251. package/src/components/LpCheckbox.vue +7 -3
  252. package/src/components/LpCodeBlock.vue +50 -14
  253. package/src/components/LpColorPicker.vue +39 -9
  254. package/src/components/LpCommandPalette.vue +34 -13
  255. package/src/components/LpConfirmDialog.vue +68 -17
  256. package/src/components/LpConfirmHost.vue +59 -0
  257. package/src/components/LpContextMenu.vue +14 -115
  258. package/src/components/LpDatePicker.vue +99 -68
  259. package/src/components/LpDrawer.vue +56 -19
  260. package/src/components/LpDropzone.vue +21 -4
  261. package/src/components/LpFileTree.vue +33 -7
  262. package/src/components/LpFormField.vue +30 -7
  263. package/src/components/LpImageEditor.vue +96 -57
  264. package/src/components/LpInput.vue +18 -7
  265. package/src/components/LpLightbox.vue +140 -29
  266. package/src/components/LpLink.vue +24 -13
  267. package/src/components/LpLogViewer.vue +86 -17
  268. package/src/components/LpMapPicker.vue +36 -13
  269. package/src/components/LpMediaGallery.vue +8 -2
  270. package/src/components/LpModal.vue +53 -27
  271. package/src/components/LpModalNav.vue +5 -2
  272. package/src/components/LpNotificationBell.vue +63 -17
  273. package/src/components/LpNumberField.vue +21 -6
  274. package/src/components/LpNumberFlow.vue +26 -10
  275. package/src/components/LpOtpInput.vue +43 -8
  276. package/src/components/LpPagination.vue +23 -7
  277. package/src/components/LpPasswordInput.vue +4 -1
  278. package/src/components/LpPhoneInput.vue +53 -27
  279. package/src/components/LpPie.vue +94 -15
  280. package/src/components/LpPopover.vue +3 -0
  281. package/src/components/LpProgress.vue +5 -3
  282. package/src/components/LpRadioGroup.vue +9 -2
  283. package/src/components/LpSegmented.vue +12 -2
  284. package/src/components/LpSelect.vue +0 -0
  285. package/src/components/LpShortcutsDialog.vue +8 -3
  286. package/src/components/LpSidebar.vue +14 -9
  287. package/src/components/LpSidebarNav.vue +17 -5
  288. package/src/components/LpSkeleton.vue +4 -1
  289. package/src/components/LpSlider.vue +7 -1
  290. package/src/components/LpSparkline.vue +11 -3
  291. package/src/components/LpSwitch.vue +30 -7
  292. package/src/components/LpTable.vue +91 -32
  293. package/src/components/LpTableOfContents.vue +50 -32
  294. package/src/components/LpTabs.vue +23 -6
  295. package/src/components/LpTextarea.vue +44 -6
  296. package/src/components/LpThemeProvider.vue +41 -0
  297. package/src/components/LpThemeSwitcher.vue +64 -17
  298. package/src/components/LpTilt.vue +2 -2
  299. package/src/components/LpTimeField.vue +14 -3
  300. package/src/components/LpToaster.vue +66 -26
  301. package/src/components/LpTooltip.vue +22 -5
  302. package/src/components/LpTooltipProvider.vue +24 -0
  303. package/src/components/LpTopologyCanvas.vue +12 -1
  304. package/src/components/LpUptimeBar.vue +46 -8
  305. package/src/components/chart.ts +77 -5
  306. package/src/components/codeHighlight.ts +111 -15
  307. package/src/components/color.ts +30 -9
  308. package/src/components/confirm.ts +58 -0
  309. package/src/components/countries.ts +12 -0
  310. package/src/components/fileTree.ts +19 -7
  311. package/src/components/imageCrop.ts +107 -0
  312. package/src/components/link.ts +34 -0
  313. package/src/components/modalDepth.ts +48 -6
  314. package/src/components/otp.ts +16 -0
  315. package/src/components/sidebar.ts +7 -1
  316. package/src/components/tableSort.ts +37 -0
  317. package/src/composables/useField.ts +79 -0
  318. package/src/composables/useFormDraft.ts +25 -20
  319. package/src/composables/useHotkeys.ts +89 -18
  320. package/src/composables/useInputFilter.ts +77 -14
  321. package/src/composables/useLpConfig.ts +87 -0
  322. package/src/composables/useRovingFocus.ts +11 -0
  323. package/src/composables/useTilt.ts +5 -6
  324. package/src/composables/useToast.ts +56 -4
  325. package/src/composables/useZoomPan.ts +6 -6
  326. package/src/index.ts +37 -2
  327. package/src/layout/tree.ts +30 -0
  328. package/src/layout/useLayout.ts +68 -18
  329. package/src/nuxt.ts +59 -17
  330. package/src/runtime/config.plugin.ts +14 -0
  331. package/src/runtime/theme.plugin.ts +42 -22
  332. package/src/theme/useTheme.ts +206 -100
  333. package/dist/chunks/LpBreadcrumbs-p4jHe_V-.js +0 -62
  334. package/dist/chunks/LpContextMenu-CwW1QXe_.js +0 -217
  335. package/dist/chunks/LpDatePicker-CwOToxGq.js +0 -164
  336. package/dist/chunks/LpFormField-CazBc2-a.js +0 -62
  337. package/dist/chunks/LpTextarea-t7F6gv8E.js +0 -47
@@ -69,9 +69,16 @@ export interface UseFormDraftOptions<T> {
69
69
  }
70
70
 
71
71
  export interface UseFormDraft<T> {
72
- /** A draft was found for this key, and has not been restored or discarded. */
72
+ /**
73
+ * A draft was found for this key, and has not been restored or discarded.
74
+ * Only a draft found on reading counts: the writes this composable makes as
75
+ * the user types never raise it. While it is up, automatic writes are held
76
+ * back, so the draft on offer cannot be overwritten by the form's own state
77
+ * (an async load of the record, or typing past the banner) before the user
78
+ * has answered.
79
+ */
73
80
  hasDraft: Ref<boolean>
74
- /** When that draft was written, ISO-8601, or `null` when there is none. */
81
+ /** When the found draft was written, ISO-8601, or `null` when there is none. */
75
82
  savedAt: Ref<string | null>
76
83
  /**
77
84
  * The stored value, or `null`. Reading does not apply it — the caller decides
@@ -80,7 +87,7 @@ export interface UseFormDraft<T> {
80
87
  read: () => T | null
81
88
  /** Forget the draft. Call after applying it, and after a successful save. */
82
89
  discard: () => void
83
- /** Write now, ignoring `dirty`. For "save and come back to this later". */
90
+ /** Write now, ignoring `dirty` and a pending draft. For "save and come back to this later". */
84
91
  save: () => void
85
92
  }
86
93
 
@@ -132,6 +139,17 @@ export function useFormDraft<T>(options: UseFormDraftOptions<T>): UseFormDraft<T
132
139
  return String(toValue(options.key) || "").trim()
133
140
  }
134
141
 
142
+ // An envelope still worth offering, or null. No side effects: the key watcher
143
+ // only looks, and `read` decides what to clear.
144
+ function fresh(parsed: DraftEnvelope<T> | null): boolean {
145
+ if (!parsed || parsed.version !== ENVELOPE_VERSION) return false
146
+ if (maxAge <= 0) return true
147
+ // NaN when the stamp is unreadable, and NaN > maxAge is false — an
148
+ // unreadable stamp is treated as "no idea how old", which errs towards
149
+ // offering the draft rather than throwing away somebody's typing.
150
+ return !(Date.now() - Date.parse(parsed.savedAt) > maxAge)
151
+ }
152
+
135
153
  function read(): T | null {
136
154
  const store = storage()
137
155
  const key = currentKey()
@@ -145,20 +163,10 @@ export function useFormDraft<T>(options: UseFormDraftOptions<T>): UseFormDraft<T
145
163
  if (!raw) return null
146
164
  try {
147
165
  const parsed = JSON.parse(raw) as DraftEnvelope<T>
148
- if (!parsed || parsed.version !== ENVELOPE_VERSION) {
166
+ if (!fresh(parsed)) {
149
167
  discard()
150
168
  return null
151
169
  }
152
- if (maxAge > 0) {
153
- const age = Date.now() - Date.parse(parsed.savedAt)
154
- // NaN when the stamp is unreadable, and NaN > maxAge is false — an
155
- // unreadable stamp is treated as "no idea how old", which errs towards
156
- // offering the draft rather than throwing away somebody's typing.
157
- if (age > maxAge) {
158
- discard()
159
- return null
160
- }
161
- }
162
170
  return parsed.value
163
171
  } catch {
164
172
  // Corrupt or hand-edited. Nothing to rescue, and leaving it means asking
@@ -172,13 +180,12 @@ export function useFormDraft<T>(options: UseFormDraftOptions<T>): UseFormDraft<T
172
180
  const store = storage()
173
181
  const key = currentKey()
174
182
  if (!store || !key) return
175
- const stamp = new Date().toISOString()
176
183
  try {
177
184
  store.setItem(
178
185
  key,
179
186
  JSON.stringify({
180
187
  version: ENVELOPE_VERSION,
181
- savedAt: stamp,
188
+ savedAt: new Date().toISOString(),
182
189
  value,
183
190
  } satisfies DraftEnvelope<T>),
184
191
  )
@@ -187,8 +194,6 @@ export function useFormDraft<T>(options: UseFormDraftOptions<T>): UseFormDraft<T
187
194
  // unharmed; it simply has no safety net.
188
195
  return
189
196
  }
190
- hasDraft.value = true
191
- savedAt.value = stamp
192
197
  }
193
198
 
194
199
  function discard() {
@@ -226,7 +231,7 @@ export function useFormDraft<T>(options: UseFormDraftOptions<T>): UseFormDraft<T
226
231
  if (!raw) return
227
232
  try {
228
233
  const parsed = JSON.parse(raw) as DraftEnvelope<T>
229
- if (parsed?.version !== ENVELOPE_VERSION) return
234
+ if (!fresh(parsed)) return
230
235
  hasDraft.value = true
231
236
  savedAt.value = parsed.savedAt
232
237
  } catch {
@@ -240,7 +245,7 @@ export function useFormDraft<T>(options: UseFormDraftOptions<T>): UseFormDraft<T
240
245
  watch(
241
246
  () => [toValue(options.value), toValue(options.dirty), toValue(options.disabled)],
242
247
  ([value, dirty, disabled]) => {
243
- if (disabled) return
248
+ if (disabled || hasDraft.value) return
244
249
  // `dirty` unset means "every change is worth keeping"; the option exists
245
250
  // so a form that knows it is untouched leaves no trace.
246
251
  if (options.dirty !== undefined && !dirty) return
@@ -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
@@ -39,6 +49,16 @@ export interface HotkeyBinding {
39
49
  * something there — Escape, and the palette the user reaches from anywhere.
40
50
  */
41
51
  allowInInput?: boolean
52
+ /**
53
+ * Runs even while an overlay owns the keyboard. By default a binding declared
54
+ * outside the topmost modal stays quiet while that modal is open, and an
55
+ * Escape binding declared outside the topmost dismissable layer (dialog,
56
+ * popover, menu) leaves Escape to that layer, so a page-level shortcut can
57
+ * neither act behind a dialog nor stop the dialog from closing.
58
+ */
59
+ overLayers?: boolean
60
+ /** Set `false` to ignore auto-repeat while the key is held. */
61
+ repeat?: boolean
42
62
  /**
43
63
  * How the key reference lists this binding while it is mounted. Bindings
44
64
  * without it still work, they just stay out of `activeShortcuts`.
@@ -87,12 +107,22 @@ export const isAppleKeyboard =
87
107
  typeof navigator !== "undefined"
88
108
  && /Mac|iPhone|iPad|iPod/.test(navigator.platform || "")
89
109
 
110
+ /**
111
+ * `isAppleKeyboard` for render paths: false until mounted, so the server and
112
+ * the hydrating client print the same label and the client switches after.
113
+ */
114
+ export function useAppleKeyboard() {
115
+ const apple = ref(false)
116
+ onMounted(() => (apple.value = isAppleKeyboard))
117
+ return apple
118
+ }
119
+
90
120
  /**
91
121
  * The symbol to print for a modifier on this platform. Bindings treat Ctrl and
92
122
  * Meta as one modifier, so only the label may differ — never the binding.
93
123
  */
94
- export function appleModifierLabel(key: string): string {
95
- if (!isAppleKeyboard) return key
124
+ export function appleModifierLabel(key: string, apple = isAppleKeyboard): string {
125
+ if (!apple) return key
96
126
  if (key === "Ctrl") return "⌘"
97
127
  if (key === "Alt") return "⌥"
98
128
  return key
@@ -123,13 +153,22 @@ function isTextEntry(target: EventTarget | null): boolean {
123
153
  return false
124
154
  }
125
155
 
126
- function matches(event: KeyboardEvent, binding: HotkeyBinding): boolean {
156
+ const MODIFIER_KEYS = new Set(["control", "meta", "shift", "alt"])
157
+
158
+ export function matchesHotkey(event: KeyboardEvent, binding: HotkeyBinding): boolean {
159
+ const key = binding.key.toLowerCase()
127
160
  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
161
+ const wantsCtrl = Boolean(binding.ctrl) || key === "control" || key === "meta"
162
+ if (ctrlHeld !== wantsCtrl) return false
163
+ // Shift is part of typing a symbol on most layouts ("?", "+"), so a binding
164
+ // on a symbol that does not mention Shift accepts it either way.
165
+ const symbol = key.length === 1 && key.toLowerCase() === key.toUpperCase()
166
+ const shiftFree = key === "shift" || (binding.shift === undefined && symbol)
167
+ if (!shiftFree && event.shiftKey !== Boolean(binding.shift)) return false
168
+ if (key !== "alt" && event.altKey !== Boolean(binding.alt)) return false
169
+ if (MODIFIER_KEYS.has(key)) return event.key.toLowerCase() === key
131
170
  return (
132
- event.key.toLowerCase() === binding.key.toLowerCase()
171
+ event.key.toLowerCase() === key
133
172
  || (binding.code !== undefined && event.code === binding.code)
134
173
  )
135
174
  }
@@ -165,11 +204,23 @@ interface MountedHotkeys {
165
204
  enabled: MaybeRefOrGetter<boolean> | undefined
166
205
  priority: MaybeRefOrGetter<number> | undefined
167
206
  order: number
207
+ /** Where the declaring component sits in the DOM, for the overlay check. */
208
+ host: () => Node | null
168
209
  }
169
210
 
170
- const mounted = shallowReactive(new Set<MountedHotkeys>())
171
- let mountOrder = 0
172
- let listening = false
211
+ // Pinned to a global symbol for the same reason as the toast queue: a barrel
212
+ // and a deep import must not end up with two registries and two listeners.
213
+ const STATE_KEY = Symbol.for("@leavepulse/ui:hotkeys")
214
+ type GlobalWithHotkeys = typeof globalThis & {
215
+ [STATE_KEY]?: { mounted: Set<MountedHotkeys>; order: number; listening: boolean }
216
+ }
217
+ const g = globalThis as GlobalWithHotkeys
218
+ const state = (g[STATE_KEY] ??= {
219
+ mounted: shallowReactive(new Set<MountedHotkeys>()),
220
+ order: 0,
221
+ listening: false,
222
+ })
223
+ const mounted = state.mounted
173
224
 
174
225
  function inScope(target: EventTarget | null, scope: MountedHotkeys["scope"]): boolean {
175
226
  const el = toValue(scope)
@@ -178,17 +229,32 @@ function inScope(target: EventTarget | null, scope: MountedHotkeys["scope"]): bo
178
229
  return !!node && (el === node || el.contains(node))
179
230
  }
180
231
 
232
+ function topLayer(): Element | null {
233
+ const layers = document.querySelectorAll("[data-dismissable-layer]")
234
+ return layers[layers.length - 1] ?? null
235
+ }
236
+
237
+ function insideOf(entry: MountedHotkeys, layer: Element): boolean {
238
+ const scope = toValue(entry.scope)
239
+ const node = scope ?? entry.host()
240
+ return !!node && layer.contains(node)
241
+ }
242
+
181
243
  function onKeydown(event: KeyboardEvent) {
182
- if (event.isComposing) return
244
+ if (event.isComposing || event.defaultPrevented) return
183
245
  const inTextEntry = isTextEntry(event.target)
246
+ const blocker = event.key === "Escape" ? topLayer() : topModalElement()
184
247
  const entries = [...mounted].sort(
185
248
  (a, b) => Number(toValue(b.priority) || 0) - Number(toValue(a.priority) || 0) || b.order - a.order,
186
249
  )
187
250
  for (const entry of entries) {
188
251
  if (toValue(entry.enabled) === false || !inScope(event.target, entry.scope)) continue
252
+ const covered = !!blocker && !insideOf(entry, blocker)
189
253
  for (const binding of entry.read()) {
190
254
  if (inTextEntry && !binding.allowInInput) continue
191
- if (!matches(event, binding)) continue
255
+ if (event.repeat && binding.repeat === false) continue
256
+ if (covered && !binding.overLayers) continue
257
+ if (!matchesHotkey(event, binding)) continue
192
258
  if (binding.handler(event) === false) continue
193
259
  event.preventDefault()
194
260
  return
@@ -200,25 +266,30 @@ export function useHotkeys(
200
266
  bindings: MaybeRefOrGetter<HotkeyBinding[]>,
201
267
  options: UseHotkeysOptions = {},
202
268
  ) {
269
+ const instance = getCurrentInstance()
203
270
  const entry: MountedHotkeys = {
204
271
  read: () => toValue(bindings),
205
272
  scope: options.scope,
206
273
  enabled: options.enabled,
207
274
  priority: options.priority,
208
- order: ++mountOrder,
275
+ order: ++state.order,
276
+ host: () => {
277
+ const el = instance?.vnode.el
278
+ return el instanceof Node ? el : null
279
+ },
209
280
  }
210
281
  onMounted(() => {
211
282
  mounted.add(entry)
212
- if (!listening) {
283
+ if (!state.listening) {
213
284
  window.addEventListener("keydown", onKeydown)
214
- listening = true
285
+ state.listening = true
215
286
  }
216
287
  })
217
288
  onBeforeUnmount(() => {
218
289
  mounted.delete(entry)
219
- if (!mounted.size && listening) {
290
+ if (!mounted.size && state.listening) {
220
291
  window.removeEventListener("keydown", onKeydown)
221
- listening = false
292
+ state.listening = false
222
293
  }
223
294
  })
224
295
  }
@@ -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
  }
@@ -0,0 +1,87 @@
1
+ import {
2
+ computed,
3
+ inject,
4
+ provide,
5
+ toValue,
6
+ type App,
7
+ type Component,
8
+ type ComputedRef,
9
+ type InjectionKey,
10
+ type MaybeRefOrGetter,
11
+ } from "vue"
12
+
13
+ /*
14
+ * App-wide kit configuration, said once at the root instead of on every call
15
+ * site: the router link component that navigation pieces render, and the text
16
+ * the kit would otherwise hard-code in English.
17
+ *
18
+ * Labels are grouped by scope (one scope per component, e.g. "lightbox",
19
+ * "toast") so each component owns its own keys and defaults. The precedence is
20
+ * always: the component's own `labels` prop, then the nearest config, then the
21
+ * component's English defaults.
22
+ */
23
+ export type LabelScope = Record<string, string>
24
+
25
+ export interface LpConfig {
26
+ /** Component rendered for internal links (RouterLink, NuxtLink). Plain `<a>` when unset. */
27
+ linkComponent?: Component | string
28
+ /** Text overrides by scope: `{ lightbox: { previous: "Back" } }`. */
29
+ labels?: Partial<Record<string, Partial<LabelScope>>>
30
+ }
31
+
32
+ export const LP_CONFIG: InjectionKey<MaybeRefOrGetter<LpConfig>> = Symbol.for("lp-config")
33
+
34
+ /** Provide kit configuration to every component below the caller. */
35
+ export function provideLpConfig(config: MaybeRefOrGetter<LpConfig>): void {
36
+ provide(LP_CONFIG, layered(inject(LP_CONFIG, {}), config))
37
+ }
38
+
39
+ /** Install the configuration app-wide: `app.use(lpConfig({ ... }))`. */
40
+ export function lpConfig(config: MaybeRefOrGetter<LpConfig>) {
41
+ return {
42
+ install: (app: App) => {
43
+ const outer = app.runWithContext(() => inject(LP_CONFIG, {}))
44
+ app.provide(LP_CONFIG, layered(outer, config))
45
+ },
46
+ }
47
+ }
48
+
49
+ /*
50
+ * A nested config refines the one above it rather than replacing it: an app's
51
+ * own labels must not drop the link component a framework plugin provided.
52
+ */
53
+ function layered(outer: MaybeRefOrGetter<LpConfig>, inner: MaybeRefOrGetter<LpConfig>): () => LpConfig {
54
+ return () => mergeConfig(toValue(outer), toValue(inner))
55
+ }
56
+
57
+ export function mergeConfig(outer: LpConfig, inner: LpConfig): LpConfig {
58
+ const labels: Partial<Record<string, Partial<LabelScope>>> = { ...outer.labels }
59
+ for (const [scope, values] of Object.entries(inner.labels ?? {})) {
60
+ labels[scope] = { ...labels[scope], ...values }
61
+ }
62
+ return { ...outer, ...inner, linkComponent: inner.linkComponent ?? outer.linkComponent, labels }
63
+ }
64
+
65
+ export function useLpConfig(): ComputedRef<LpConfig> {
66
+ const config = inject(LP_CONFIG, {})
67
+ return computed(() => toValue(config))
68
+ }
69
+
70
+ /**
71
+ * The labels a component renders: its own `labels` prop over the configured
72
+ * scope over its defaults.
73
+ */
74
+ export function useLabels<T extends LabelScope>(
75
+ scope: string,
76
+ defaults: T,
77
+ own?: () => Partial<T> | undefined,
78
+ ): ComputedRef<T> {
79
+ const config = useLpConfig()
80
+ return computed(() => ({ ...defaults, ...config.value.labels?.[scope], ...own?.() }) as T)
81
+ }
82
+
83
+ /** The link component to render for an internal destination. */
84
+ export function useLinkComponent(): ComputedRef<Component | string> {
85
+ const config = useLpConfig()
86
+ return computed(() => config.value.linkComponent ?? "a")
87
+ }
@@ -1,5 +1,7 @@
1
1
  import { computed, ref, watch, type MaybeRefOrGetter, toValue } from "vue"
2
2
 
3
+ const NON_TEXT_INPUTS = new Set(["checkbox", "radio", "button", "submit", "reset", "range", "color", "file", "image"])
4
+
3
5
  /*
4
6
  * Arrow-key navigation over a list, with a roving tabindex.
5
7
  *
@@ -152,7 +154,16 @@ export function useRovingFocus(
152
154
  }
153
155
  }
154
156
 
157
+ function isTextEntry(target: EventTarget | null): boolean {
158
+ if (!(target instanceof HTMLElement)) return false
159
+ if (target.isContentEditable || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement) return true
160
+ return target instanceof HTMLInputElement && !NON_TEXT_INPUTS.has(target.type)
161
+ }
162
+
155
163
  function onKeydown(event: KeyboardEvent) {
164
+ // A field inside a row (an inline rename, a filter) owns its keys: the list
165
+ // taking letters for typeahead or Space for activation made it untypeable.
166
+ if (event.isComposing || isTextEntry(event.target)) return
156
167
  const vertical = orientation === "vertical" || orientation === "both"
157
168
  const horizontal = orientation === "horizontal" || orientation === "both"
158
169
 
@@ -1,5 +1,5 @@
1
1
  import { useReducedMotion } from "motion-v"
2
- import { computed, ref, type ComputedRef, type CSSProperties, type Ref } from "vue"
2
+ import { computed, ref, toValue, type ComputedRef, type CSSProperties, type MaybeRefOrGetter, type Ref } from "vue"
3
3
 
4
4
  /*
5
5
  * useTilt — pointer-reactive 3D tilt for a "tactile" surface (a card, a tile, a
@@ -35,10 +35,8 @@ export interface UseTilt {
35
35
  active: Ref<boolean>
36
36
  }
37
37
 
38
- export function useTilt(options: UseTiltOptions = {}): UseTilt {
39
- const max = options.max ?? 10
40
- const lift = options.lift ?? 8
41
- const scale = options.scale ?? 1.03
38
+ export function useTilt(source: MaybeRefOrGetter<UseTiltOptions> = {}): UseTilt {
39
+ const options = computed(() => toValue(source))
42
40
  const reduceMotion = useReducedMotion()
43
41
 
44
42
  const active = ref(false)
@@ -72,6 +70,7 @@ export function useTilt(options: UseTiltOptions = {}): UseTilt {
72
70
  // rotateX tips away from the pointer's vertical position; rotateY toward its
73
71
  // horizontal one. Only tilt while active so it rests flat when the pointer
74
72
  // leaves (nx/ny are already zeroed on leave).
73
+ const { max = 10, lift = 8, scale = 1.03 } = options.value
75
74
  const rx = active.value ? -ny.value * max : 0
76
75
  const ry = active.value ? nx.value * max : 0
77
76
  const z = active.value ? lift : 0
@@ -86,7 +85,7 @@ export function useTilt(options: UseTiltOptions = {}): UseTilt {
86
85
  })
87
86
 
88
87
  const glareStyle = computed<CSSProperties>(() => {
89
- if (!options.glare || reduceMotion.value || !active.value) {
88
+ if (!options.value.glare || reduceMotion.value || !active.value) {
90
89
  return { opacity: 0 }
91
90
  }
92
91
  // Bright spot tracks the pointer; map [-1,1] → [0%,100%].
@@ -11,6 +11,9 @@ export interface ToastAction {
11
11
 
12
12
  export interface ToastItem {
13
13
  id: number
14
+ /** Bumped when a toast with the same `key` replaces this one in place. */
15
+ rev: number
16
+ key?: string
14
17
  title?: string
15
18
  message: string
16
19
  variant: ToastVariant
@@ -23,6 +26,12 @@ export interface ToastItem {
23
26
  }
24
27
 
25
28
  export interface ToastOptions {
29
+ /**
30
+ * A toast with the same key replaces the one on screen instead of stacking
31
+ * beside it: "Saved" ten times is one toast, and a progress toast can turn
32
+ * into its result.
33
+ */
34
+ key?: string
26
35
  title?: string
27
36
  duration?: number
28
37
  actions?: ToastAction[]
@@ -48,10 +57,12 @@ function push(
48
57
  message: string,
49
58
  options: ToastOptions = {},
50
59
  ): number {
51
- const id = state.nextId++
60
+ // A server render has no one to show it to, and the queue would outlive the
61
+ // request and surface in the next visitor's page.
62
+ if (typeof window === "undefined") return -1
52
63
  const duration = options.duration ?? DEFAULT_DURATION
53
- const item: ToastItem = {
54
- id,
64
+ const fields = {
65
+ key: options.key,
55
66
  title: options.title,
56
67
  message,
57
68
  variant,
@@ -61,14 +72,54 @@ function push(
61
72
  onClick: options.onClick,
62
73
  }
63
74
 
75
+ const same = options.key === undefined ? -1 : queue.findIndex((t) => t.key === options.key)
76
+ const existing = queue[same]
77
+ if (existing) {
78
+ queue.splice(same, 1, { ...fields, id: existing.id, rev: existing.rev + 1 })
79
+ return existing.id
80
+ }
81
+
82
+ const id = state.nextId++
64
83
  // Cap the active count: drop the oldest beyond the limit.
65
84
  const overflow = queue.length - (MAX_ACTIVE - 1)
66
85
  if (overflow > 0) queue.splice(0, overflow)
67
- queue.push(item)
86
+ queue.push({ ...fields, id, rev: 0 })
68
87
 
69
88
  return id
70
89
  }
71
90
 
91
+ export interface ToastPromiseMessages<T> {
92
+ loading: string
93
+ success: string | ((value: T) => string)
94
+ error: string | ((error: unknown) => string)
95
+ }
96
+
97
+ let promiseKey = 0
98
+
99
+ /**
100
+ * One toast that follows a promise: a persistent "loading" toast that turns
101
+ * into the success or error toast when it settles. Returns the promise itself.
102
+ */
103
+ function promise<T>(
104
+ task: Promise<T>,
105
+ messages: ToastPromiseMessages<T>,
106
+ options: ToastOptions = {},
107
+ ): Promise<T> {
108
+ const key = options.key ?? `@leavepulse/ui:promise-${promiseKey++}`
109
+ push("info", messages.loading, { ...options, key, duration: 0, actions: undefined })
110
+ task.then(
111
+ (value) => {
112
+ const text = typeof messages.success === "function" ? messages.success(value) : messages.success
113
+ push("success", text, { ...options, key })
114
+ },
115
+ (error: unknown) => {
116
+ const text = typeof messages.error === "function" ? messages.error(error) : messages.error
117
+ push("error", text, { ...options, key })
118
+ },
119
+ )
120
+ return task
121
+ }
122
+
72
123
  function dismiss(id: number): void {
73
124
  const i = queue.findIndex((t) => t.id === id)
74
125
  if (i !== -1) queue.splice(i, 1)
@@ -86,6 +137,7 @@ export function useToast() {
86
137
  success: (message: string, o?: ToastOptions) => push("success", message, o),
87
138
  warning: (message: string, o?: ToastOptions) => push("warning", message, o),
88
139
  error: (message: string, o?: ToastOptions) => push("error", message, o),
140
+ promise,
89
141
  /** Low-level push for bridges (e.g. @leavepulse/ui-sdk apiError). */
90
142
  push,
91
143
  dismiss,