@leavepulse/ui 0.43.3 → 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 (340) hide show
  1. package/README.md +75 -7
  2. package/dist/assets/{LpCommandPalette-DH7sPcKP.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 -10
  103. package/dist/components/LpCommandPalette.vue.js +88 -78
  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/commandSearch.d.ts +0 -1
  198. package/dist/components/commandSearch.js +1 -17
  199. package/dist/components/confirm.d.ts +25 -0
  200. package/dist/components/confirm.js +26 -0
  201. package/dist/components/countries.d.ts +6 -0
  202. package/dist/components/countries.js +7 -0
  203. package/dist/components/fileTree.js +16 -7
  204. package/dist/components/imageCrop.d.ts +42 -0
  205. package/dist/components/imageCrop.js +47 -0
  206. package/dist/components/link.d.ts +12 -0
  207. package/dist/components/link.js +19 -0
  208. package/dist/components/modalDepth.d.ts +18 -2
  209. package/dist/components/modalDepth.js +17 -5
  210. package/dist/components/otp.d.ts +5 -0
  211. package/dist/components/otp.js +10 -0
  212. package/dist/components/sidebar.d.ts +7 -1
  213. package/dist/components/tableSort.d.ts +8 -0
  214. package/dist/components/tableSort.js +31 -0
  215. package/dist/composables/useField.d.ts +34 -0
  216. package/dist/composables/useField.js +45 -0
  217. package/dist/composables/useFormDraft.d.ts +10 -3
  218. package/dist/composables/useFormDraft.js +9 -14
  219. package/dist/composables/useHotkeys.d.ts +17 -1
  220. package/dist/composables/useHotkeys.js +55 -18
  221. package/dist/composables/useInputFilter.d.ts +17 -0
  222. package/dist/composables/useInputFilter.js +47 -10
  223. package/dist/composables/useLpConfig.d.ts +24 -0
  224. package/dist/composables/useLpConfig.js +44 -0
  225. package/dist/composables/useRovingFocus.js +7 -0
  226. package/dist/composables/useTilt.d.ts +2 -2
  227. package/dist/composables/useTilt.js +5 -6
  228. package/dist/composables/useToast.d.ts +20 -0
  229. package/dist/composables/useToast.js +28 -4
  230. package/dist/composables/useZoomPan.d.ts +3 -3
  231. package/dist/composables/useZoomPan.js +4 -4
  232. package/dist/index.d.ts +33 -3
  233. package/dist/index.js +185 -139
  234. package/dist/layout/tree.d.ts +2 -0
  235. package/dist/layout/tree.js +16 -1
  236. package/dist/layout/useLayout.d.ts +15 -0
  237. package/dist/layout/useLayout.js +41 -14
  238. package/dist/theme/useTheme.d.ts +32 -9
  239. package/package.json +8 -7
  240. package/src/component-names.ts +5 -7
  241. package/src/components/ContextMenuItems.vue +88 -0
  242. package/src/components/LayoutCanvas.vue +26 -6
  243. package/src/components/LayoutNode.vue +19 -2
  244. package/src/components/LpActionBar.vue +30 -7
  245. package/src/components/LpAddressInput.vue +57 -41
  246. package/src/components/LpAlert.vue +16 -3
  247. package/src/components/LpAppShell.vue +45 -12
  248. package/src/components/LpAutocomplete.vue +46 -10
  249. package/src/components/LpAvatar.vue +19 -5
  250. package/src/components/LpBreadcrumbs.vue +65 -41
  251. package/src/components/LpButton.vue +59 -13
  252. package/src/components/LpChart.vue +272 -28
  253. package/src/components/LpCheckbox.vue +7 -3
  254. package/src/components/LpCodeBlock.vue +50 -14
  255. package/src/components/LpColorPicker.vue +39 -9
  256. package/src/components/LpCommandPalette.vue +36 -25
  257. package/src/components/LpConfirmDialog.vue +68 -17
  258. package/src/components/LpConfirmHost.vue +59 -0
  259. package/src/components/LpContextMenu.vue +14 -115
  260. package/src/components/LpDatePicker.vue +99 -68
  261. package/src/components/LpDrawer.vue +56 -19
  262. package/src/components/LpDropzone.vue +21 -4
  263. package/src/components/LpFileTree.vue +33 -7
  264. package/src/components/LpFormField.vue +30 -7
  265. package/src/components/LpImageEditor.vue +96 -57
  266. package/src/components/LpInput.vue +18 -7
  267. package/src/components/LpLightbox.vue +140 -29
  268. package/src/components/LpLink.vue +24 -13
  269. package/src/components/LpLogViewer.vue +86 -17
  270. package/src/components/LpMapPicker.vue +36 -13
  271. package/src/components/LpMediaGallery.vue +8 -2
  272. package/src/components/LpModal.vue +53 -27
  273. package/src/components/LpModalNav.vue +5 -2
  274. package/src/components/LpNotificationBell.vue +63 -17
  275. package/src/components/LpNumberField.vue +21 -6
  276. package/src/components/LpNumberFlow.vue +26 -10
  277. package/src/components/LpOtpInput.vue +43 -8
  278. package/src/components/LpPagination.vue +23 -7
  279. package/src/components/LpPasswordInput.vue +4 -1
  280. package/src/components/LpPhoneInput.vue +53 -27
  281. package/src/components/LpPie.vue +94 -15
  282. package/src/components/LpPopover.vue +3 -0
  283. package/src/components/LpProgress.vue +5 -3
  284. package/src/components/LpRadioGroup.vue +9 -2
  285. package/src/components/LpSegmented.vue +12 -2
  286. package/src/components/LpSelect.vue +0 -0
  287. package/src/components/LpShortcutsDialog.vue +8 -3
  288. package/src/components/LpSidebar.vue +14 -9
  289. package/src/components/LpSidebarNav.vue +17 -5
  290. package/src/components/LpSkeleton.vue +4 -1
  291. package/src/components/LpSlider.vue +7 -1
  292. package/src/components/LpSparkline.vue +11 -3
  293. package/src/components/LpSwitch.vue +30 -7
  294. package/src/components/LpTable.vue +91 -32
  295. package/src/components/LpTableOfContents.vue +50 -32
  296. package/src/components/LpTabs.vue +23 -6
  297. package/src/components/LpTextarea.vue +44 -6
  298. package/src/components/LpThemeProvider.vue +41 -0
  299. package/src/components/LpThemeSwitcher.vue +64 -17
  300. package/src/components/LpTilt.vue +2 -2
  301. package/src/components/LpTimeField.vue +14 -3
  302. package/src/components/LpToaster.vue +66 -26
  303. package/src/components/LpTooltip.vue +22 -5
  304. package/src/components/LpTooltipProvider.vue +24 -0
  305. package/src/components/LpTopologyCanvas.vue +12 -1
  306. package/src/components/LpUptimeBar.vue +46 -8
  307. package/src/components/chart.ts +77 -5
  308. package/src/components/codeHighlight.ts +111 -15
  309. package/src/components/color.ts +30 -9
  310. package/src/components/commandSearch.ts +0 -17
  311. package/src/components/confirm.ts +58 -0
  312. package/src/components/countries.ts +12 -0
  313. package/src/components/fileTree.ts +19 -7
  314. package/src/components/imageCrop.ts +107 -0
  315. package/src/components/link.ts +34 -0
  316. package/src/components/modalDepth.ts +48 -6
  317. package/src/components/otp.ts +16 -0
  318. package/src/components/sidebar.ts +7 -1
  319. package/src/components/tableSort.ts +37 -0
  320. package/src/composables/useField.ts +79 -0
  321. package/src/composables/useFormDraft.ts +25 -20
  322. package/src/composables/useHotkeys.ts +89 -18
  323. package/src/composables/useInputFilter.ts +77 -14
  324. package/src/composables/useLpConfig.ts +87 -0
  325. package/src/composables/useRovingFocus.ts +11 -0
  326. package/src/composables/useTilt.ts +5 -6
  327. package/src/composables/useToast.ts +56 -4
  328. package/src/composables/useZoomPan.ts +6 -6
  329. package/src/index.ts +38 -3
  330. package/src/layout/tree.ts +30 -0
  331. package/src/layout/useLayout.ts +68 -18
  332. package/src/nuxt.ts +59 -17
  333. package/src/runtime/config.plugin.ts +14 -0
  334. package/src/runtime/theme.plugin.ts +42 -22
  335. package/src/theme/useTheme.ts +206 -100
  336. package/dist/chunks/LpBreadcrumbs-p4jHe_V-.js +0 -62
  337. package/dist/chunks/LpContextMenu-CwW1QXe_.js +0 -217
  338. package/dist/chunks/LpDatePicker-CwOToxGq.js +0 -164
  339. package/dist/chunks/LpFormField-CazBc2-a.js +0 -62
  340. package/dist/chunks/LpTextarea-t7F6gv8E.js +0 -47
@@ -6,20 +6,10 @@
6
6
  * long-press on touch) opens the menu at the pointer. Shares the floating-panel
7
7
  * surface + option-row look with the rest of the kit via dropdown.ts.
8
8
  */
9
- import {
10
- ContextMenuContent,
11
- ContextMenuItem,
12
- ContextMenuPortal,
13
- ContextMenuRoot,
14
- ContextMenuSeparator,
15
- ContextMenuSub,
16
- ContextMenuSubContent,
17
- ContextMenuSubTrigger,
18
- ContextMenuTrigger,
19
- } from "reka-ui"
9
+ import { ContextMenuContent, ContextMenuPortal, ContextMenuRoot, ContextMenuTrigger } from "reka-ui"
20
10
  import { computed } from "vue"
11
+ import ContextMenuItems from "./ContextMenuItems.vue"
21
12
  import { POPOVER_PANEL } from "./dropdown"
22
- import LpIcon from "./LpIcon.vue"
23
13
 
24
14
  export interface ContextMenuItemDef {
25
15
  label: string
@@ -30,7 +20,7 @@ export interface ContextMenuItemDef {
30
20
  disabled?: boolean
31
21
  separatorBefore?: boolean
32
22
  onSelect?: () => void
33
- /** Nested submenu. When present, onSelect is ignored. */
23
+ /** Nested submenu, to any depth. When present, onSelect is ignored. */
34
24
  children?: ContextMenuItemDef[]
35
25
  /**
36
26
  * Renders as a tick-box rather than a plain item, and the menu stays open on
@@ -52,17 +42,13 @@ const props = defineProps<{
52
42
  always?: boolean
53
43
  }>()
54
44
 
55
- // With no items, render just the trigger slot — no reka machinery — so the
56
- // element keeps the browser's native context menu. Lets callers wrap
57
- // unconditionally and pass [] to opt out per element. `always` overrides this so
58
- // a late-populated menu is ready on the very first click.
45
+ // With no items the trigger is disabled, so the element keeps the browser's
46
+ // native context menu. Lets callers wrap unconditionally and pass [] to opt out
47
+ // per element. `always` overrides this so a late-populated menu is ready on the
48
+ // very first click.
59
49
  const enabled = computed(() => props.always || props.items.length > 0)
60
50
 
61
51
  const PANEL = `${POPOVER_PANEL} z-(--z-popover) min-w-48 p-1 outline-none`
62
- const ITEM =
63
- "group/item flex cursor-pointer select-none items-center gap-2 rounded-md px-2.5 py-1.5 text-sm outline-none transition-[background-color,color] duration-[var(--duration-fast)] data-[disabled]:pointer-events-none data-[disabled]:opacity-45 data-[highlighted]:bg-brand-soft"
64
- const ICON =
65
- "transition-transform duration-[var(--duration-fast)] ease-[var(--ease-emphasized)] group-data-[highlighted]/item:translate-x-0.5"
66
52
 
67
53
  // A menu opened over a text field takes focus, and on close reka hands it to the
68
54
  // trigger, so the caret and the selection the user right-clicked are gone — the
@@ -92,104 +78,17 @@ function returnFocus(event: Event) {
92
78
  </script>
93
79
 
94
80
  <template>
95
- <slot v-if="!enabled" />
96
-
97
- <ContextMenuRoot v-else>
98
- <ContextMenuTrigger as-child @contextmenu.capture="rememberField">
81
+ <!-- One tree whether or not there are items: swapping between a bare slot and
82
+ the reka trigger remounted the wrapped content whenever items arrived
83
+ late, losing its state and focus. A disabled trigger leaves the browser's
84
+ own menu in place. -->
85
+ <ContextMenuRoot>
86
+ <ContextMenuTrigger as-child :disabled="!enabled" @contextmenu.capture="rememberField">
99
87
  <slot />
100
88
  </ContextMenuTrigger>
101
89
  <ContextMenuPortal v-if="items.length">
102
90
  <ContextMenuContent :class="PANEL" @close-auto-focus="returnFocus">
103
- <template v-for="(item, i) in items" :key="i">
104
- <ContextMenuSeparator v-if="item.separatorBefore" class="my-1 h-px bg-line" />
105
-
106
- <!-- Submenu branch -->
107
- <ContextMenuSub v-if="item.children?.length">
108
- <ContextMenuSubTrigger
109
- :class="[ITEM, 'justify-between text-ink data-[highlighted]:text-brand data-[state=open]:bg-brand-soft data-[state=open]:text-brand']"
110
- >
111
- <span class="flex items-center gap-2">
112
- <LpIcon v-if="item.icon" :name="item.icon" :size="15" :class="ICON" />
113
- {{ item.label }}
114
- </span>
115
- <LpIcon name="lucide:chevron-right" :size="14" class="text-muted" />
116
- </ContextMenuSubTrigger>
117
- <ContextMenuPortal>
118
- <ContextMenuSubContent :class="PANEL" :side-offset="2" :align-offset="-4">
119
- <template v-for="(sub, j) in item.children" :key="j">
120
- <ContextMenuSeparator v-if="sub.separatorBefore" class="my-1 h-px bg-line" />
121
- <!-- The tick-box case, as in the top-level menu: this is
122
- where most of them live, since a submenu is what a list
123
- of choices opens into. -->
124
- <ContextMenuItem
125
- v-if="sub.checked !== undefined"
126
- :class="[ITEM, 'justify-between text-ink data-[highlighted]:text-brand']"
127
- :disabled="sub.disabled"
128
- @select.prevent="sub.onSelect?.()"
129
- >
130
- <span class="flex items-center gap-2">
131
- <LpIcon
132
- :name="sub.checked ? 'lucide:check' : 'lucide:minus'"
133
- :size="15"
134
- :class="[ICON, sub.checked ? '' : 'opacity-25']"
135
- />
136
- {{ sub.label }}
137
- </span>
138
- <kbd v-if="sub.shortcut" class="text-xs text-muted">{{ sub.shortcut }}</kbd>
139
- </ContextMenuItem>
140
-
141
- <ContextMenuItem
142
- v-else
143
- :class="[ITEM, 'justify-between', sub.danger ? 'text-danger data-[highlighted]:bg-danger-soft data-[highlighted]:text-danger' : 'text-ink data-[highlighted]:text-brand']"
144
- :disabled="sub.disabled"
145
- @select="sub.onSelect?.()"
146
- >
147
- <span class="flex items-center gap-2">
148
- <LpIcon v-if="sub.icon" :name="sub.icon" :size="15" :class="ICON" />
149
- {{ sub.label }}
150
- </span>
151
- <kbd v-if="sub.shortcut" class="text-xs text-muted">{{ sub.shortcut }}</kbd>
152
- </ContextMenuItem>
153
- </template>
154
- </ContextMenuSubContent>
155
- </ContextMenuPortal>
156
- </ContextMenuSub>
157
-
158
- <!-- Tick-box item. `@select.prevent` keeps the menu open: these come
159
- in lists where several are set in one visit, and closing after
160
- each tick would mean reopening for the next. The tick occupies a
161
- fixed slot so unticked rows stay aligned with ticked ones. -->
162
- <ContextMenuItem
163
- v-else-if="item.checked !== undefined"
164
- :class="[ITEM, 'justify-between text-ink data-[highlighted]:text-brand']"
165
- :disabled="item.disabled"
166
- @select.prevent="item.onSelect?.()"
167
- >
168
- <span class="flex items-center gap-2">
169
- <LpIcon
170
- :name="item.checked ? 'lucide:check' : 'lucide:minus'"
171
- :size="15"
172
- :class="[ICON, item.checked ? '' : 'opacity-25']"
173
- />
174
- {{ item.label }}
175
- </span>
176
- <kbd v-if="item.shortcut" class="text-xs text-muted">{{ item.shortcut }}</kbd>
177
- </ContextMenuItem>
178
-
179
- <!-- Leaf item -->
180
- <ContextMenuItem
181
- v-else
182
- :class="[ITEM, 'justify-between', item.danger ? 'text-danger data-[highlighted]:bg-danger-soft data-[highlighted]:text-danger' : 'text-ink data-[highlighted]:text-brand']"
183
- :disabled="item.disabled"
184
- @select="item.onSelect?.()"
185
- >
186
- <span class="flex items-center gap-2">
187
- <LpIcon v-if="item.icon" :name="item.icon" :size="15" :class="ICON" />
188
- {{ item.label }}
189
- </span>
190
- <kbd v-if="item.shortcut" class="text-xs text-muted">{{ item.shortcut }}</kbd>
191
- </ContextMenuItem>
192
- </template>
91
+ <ContextMenuItems :items="items" :panel="PANEL" />
193
92
  </ContextMenuContent>
194
93
  </ContextMenuPortal>
195
94
  </ContextMenuRoot>
@@ -1,3 +1,7 @@
1
+ <script lang="ts">
2
+ export default { inheritAttrs: false }
3
+ </script>
4
+
1
5
  <script setup lang="ts">
2
6
  /*
3
7
  * Date field + calendar popover. The model is an ISO date string ("YYYY-MM-DD",
@@ -14,7 +18,10 @@
14
18
  * the common case.
15
19
  */
16
20
  import { PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from "reka-ui"
17
- import { computed, ref } from "vue"
21
+ import { computed, ref, useAttrs } from "vue"
22
+ import { useFieldControl } from "../composables/useField"
23
+ import { useLabels } from "../composables/useLpConfig"
24
+ import { mergeClass } from "../composables/useMergedClass"
18
25
  import { POPOVER_PANEL } from "./dropdown"
19
26
  import LpCalendar from "./LpCalendar.vue"
20
27
  import LpIcon from "./LpIcon.vue"
@@ -80,7 +87,11 @@ const shellSize = {
80
87
  lg: "h-(--size-control-lg) text-sm",
81
88
  }
82
89
 
83
- function onPick(v: string | undefined) {
90
+ function onPick(picked: string | undefined) {
91
+ // Clicking the selected day again is the calendar's "deselect". Here it is
92
+ // someone confirming the date they already have, so it keeps the value and
93
+ // closes like any other pick; clearing has its own button.
94
+ const v = picked ?? (day.value || undefined)
84
95
  if (!props.time) {
85
96
  emit("update:modelValue", v)
86
97
  open.value = false
@@ -101,75 +112,95 @@ function onTime(at: string) {
101
112
  function clear() {
102
113
  emit("update:modelValue", undefined)
103
114
  }
115
+
116
+ const attrs = useAttrs()
117
+ const field = useFieldControl(() => ({
118
+ wrapped: false,
119
+ id: typeof attrs.id === "string" ? attrs.id : undefined,
120
+ invalid: props.invalid,
121
+ disabled: props.disabled,
122
+ }))
123
+ const labels = useLabels("datePicker", { clear: "Clear" })
124
+ const showClear = computed(() => props.clearable && !!props.modelValue && !field.value.disabled)
104
125
  </script>
105
126
 
106
127
  <template>
107
- <PopoverRoot v-model:open="open">
108
- <PopoverTrigger as-child>
109
- <button
110
- type="button"
111
- :disabled="disabled"
112
- class="group flex w-full items-center gap-2 rounded-control border bg-surface-soft px-3 text-left text-ink outline-none transition-colors duration-[var(--duration-fast)] focus-visible:ring-2 focus-visible:ring-ring data-[state=open]:border-brand disabled:cursor-not-allowed disabled:opacity-55"
113
- :class="[
114
- shellSize[size],
115
- invalid ? 'border-danger focus-visible:ring-danger-soft' : 'border-line focus-visible:border-brand',
116
- ]"
117
- >
118
- <LpIcon name="lucide:calendar" :size="16" class="shrink-0 text-muted" />
119
- <span class="min-w-0 flex-1 truncate" :class="display ? '' : 'text-muted'">
120
- {{ display || placeholder }}
121
- </span>
122
- <span
123
- v-if="clearable && modelValue && !disabled"
124
- role="button"
125
- tabindex="-1"
126
- aria-label="Clear"
127
- class="shrink-0 text-muted transition-colors hover:text-ink"
128
- @click.stop="clear"
129
- @keydown.enter.stop="clear"
128
+ <!-- The clear control sits beside the trigger, not inside it: a button
129
+ nested in a button is invalid, and the nested one was unreachable by
130
+ keyboard. -->
131
+ <div :class="mergeClass('relative w-full', attrs.class as string)" :style="attrs.style as never">
132
+ <PopoverRoot v-model:open="open">
133
+ <PopoverTrigger as-child>
134
+ <button
135
+ v-bind="{ ...attrs, class: undefined, style: undefined }"
136
+ :id="field.id"
137
+ type="button"
138
+ :disabled="field.disabled"
139
+ :aria-invalid="field.invalid || undefined"
140
+ :aria-describedby="field.describedBy"
141
+ class="group flex w-full items-center gap-2 rounded-control border bg-surface-soft px-3 text-left text-ink outline-none transition-colors duration-[var(--duration-fast)] focus-visible:ring-2 focus-visible:ring-ring data-[state=open]:border-brand disabled:cursor-not-allowed disabled:opacity-55"
142
+ :class="[
143
+ shellSize[size],
144
+ field.invalid ? 'border-danger focus-visible:ring-danger-soft' : 'border-line focus-visible:border-brand',
145
+ ]"
146
+ >
147
+ <LpIcon name="lucide:calendar" :size="16" class="shrink-0 text-muted" />
148
+ <span class="min-w-0 flex-1 truncate" :class="display ? '' : 'text-muted'">
149
+ {{ display || placeholder }}
150
+ </span>
151
+ <span v-if="showClear" class="w-[15px] shrink-0" aria-hidden="true" />
152
+ <LpIcon
153
+ name="lucide:chevron-down"
154
+ :size="15"
155
+ class="shrink-0 text-muted transition-transform duration-[var(--duration-fast)] ease-[var(--ease-emphasized)] group-data-[state=open]:rotate-180"
156
+ />
157
+ </button>
158
+ </PopoverTrigger>
159
+ <PopoverPortal>
160
+ <PopoverContent
161
+ :side-offset="6"
162
+ align="start"
163
+ :class="[POPOVER_PANEL, 'z-(--z-popover) rounded-card p-0 outline-none']"
130
164
  >
131
- <LpIcon name="lucide:x" :size="15" />
132
- </span>
133
- <LpIcon
134
- name="lucide:chevron-down"
135
- :size="15"
136
- class="shrink-0 text-muted transition-transform duration-[var(--duration-fast)] ease-[var(--ease-emphasized)] group-data-[state=open]:rotate-180"
137
- />
138
- </button>
139
- </PopoverTrigger>
140
- <PopoverPortal>
141
- <PopoverContent
142
- :side-offset="6"
143
- align="start"
144
- :class="[POPOVER_PANEL, 'z-(--z-popover) rounded-card p-0 outline-none']"
145
- >
146
- <LpCalendar
147
- :model-value="day"
148
- :min="min"
149
- :max="max"
150
- :is-disabled="isDisabled"
151
- class="border-0 bg-transparent"
152
- @update:model-value="onPick"
153
- />
154
- <div v-if="time" class="flex items-center gap-2 border-t border-line px-3 py-2">
155
- <LpIcon name="lucide:clock" :size="15" class="shrink-0 text-muted" />
156
- <LpTimeField
157
- :model-value="minute || undefined"
158
- :disabled="!day"
159
- :icon="false"
160
- size="sm"
161
- class="flex-1"
162
- @update:model-value="(at) => onTime(at || '')"
165
+ <LpCalendar
166
+ :model-value="day"
167
+ :min="min"
168
+ :max="max"
169
+ :is-disabled="isDisabled"
170
+ class="border-0 bg-transparent"
171
+ @update:model-value="onPick"
163
172
  />
164
- <button
165
- type="button"
166
- class="shrink-0 rounded-control px-2 py-1 text-xs text-muted transition-colors hover:text-ink"
167
- @click="open = false"
168
- >
169
- {{ doneLabel }}
170
- </button>
171
- </div>
172
- </PopoverContent>
173
- </PopoverPortal>
174
- </PopoverRoot>
173
+ <div v-if="time" class="flex items-center gap-2 border-t border-line px-3 py-2">
174
+ <LpIcon name="lucide:clock" :size="15" class="shrink-0 text-muted" />
175
+ <LpTimeField
176
+ :model-value="minute || undefined"
177
+ :disabled="!day"
178
+ :icon="false"
179
+ size="sm"
180
+ class="flex-1"
181
+ @update:model-value="(at) => onTime(at || '')"
182
+ />
183
+ <button
184
+ type="button"
185
+ class="shrink-0 rounded-control px-2 py-1 text-xs text-muted transition-colors hover:text-ink"
186
+ @click="open = false"
187
+ >
188
+ {{ doneLabel }}
189
+ </button>
190
+ </div>
191
+ </PopoverContent>
192
+ </PopoverPortal>
193
+ </PopoverRoot>
194
+ <!-- Positioned over the spacer the trigger leaves for it, just before the
195
+ chevron (gap-2 + 15px chevron + px-3 from the right edge). -->
196
+ <button
197
+ v-if="showClear"
198
+ type="button"
199
+ :aria-label="labels.clear"
200
+ class="absolute top-1/2 right-[calc(0.75rem+15px+0.5rem)] flex -translate-y-1/2 items-center rounded-sm text-muted outline-none transition-colors hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
201
+ @click="clear"
202
+ >
203
+ <LpIcon name="lucide:x" :size="15" />
204
+ </button>
205
+ </div>
175
206
  </template>
@@ -1,8 +1,11 @@
1
1
  <script lang="ts">
2
- import { ref as sharedRef } from "vue"
2
+ import { ref as sharedRef, type Ref } from "vue"
3
3
 
4
- // One pull at a time across every edge-open drawer on the page.
5
- const edgeOwner = sharedRef<symbol | null>(null)
4
+ // One pull at a time across every edge-open drawer on the page, pinned to a
5
+ // global symbol so a barrel and a deep import still share it.
6
+ const EDGE_KEY = Symbol.for("@leavepulse/ui:drawer-edge-owner")
7
+ type GlobalWithEdge = typeof globalThis & { [EDGE_KEY]?: Ref<symbol | null> }
8
+ const edgeOwner = ((globalThis as GlobalWithEdge)[EDGE_KEY] ??= sharedRef<symbol | null>(null))
6
9
  </script>
7
10
 
8
11
  <script setup lang="ts">
@@ -32,6 +35,7 @@ import {
32
35
  import { computed, onBeforeUnmount, ref, useSlots, watch } from "vue"
33
36
  import { CLOSE_ICON } from "./dropdown"
34
37
  import { useGlass } from "../composables/useGlass"
38
+ import { useLabels } from "../composables/useLpConfig"
35
39
  import LpIcon from "./LpIcon.vue"
36
40
  import LpScrollArea from "./LpScrollArea.vue"
37
41
  import { anyModalOpen, useModalLayer } from "./modalDepth"
@@ -109,6 +113,9 @@ const props = withDefaults(
109
113
  * `provideGlass`, and without one it is off.
110
114
  */
111
115
  glass?: boolean
116
+ /** Accessible name for a drawer without a visible `title`. */
117
+ ariaLabel?: string
118
+ labels?: Partial<{ close: string }>
112
119
  }>(),
113
120
  {
114
121
  side: "right",
@@ -124,6 +131,7 @@ const props = withDefaults(
124
131
  )
125
132
 
126
133
  const frosted = useGlass(() => props.glass)
134
+ const text = useLabels("drawer", { close: "Close" }, () => props.labels)
127
135
 
128
136
  const emit = defineEmits<{
129
137
  (e: "update:open", value: boolean): void
@@ -148,7 +156,8 @@ const SIZES = { sm: 22, md: 28, lg: 36, xl: 48 } // rem along the main axis
148
156
  * settings drawer) claimed those same rungs and the tie went to DOM order —
149
157
  * the drawer, portalled later on reopen, covered the dialog it had opened.
150
158
  */
151
- const { claim, release, layer } = useModalLayer()
159
+ const contentRef = ref<{ $el: HTMLElement } | null>(null)
160
+ const { claim, release, layer } = useModalLayer({ element: () => contentRef.value?.$el })
152
161
  watch(() => props.open, (open) => (open ? claim() : release()), { immediate: true })
153
162
  onBeforeUnmount(release)
154
163
 
@@ -179,36 +188,60 @@ const sizeStyle = computed(() => {
179
188
  return isHorizontal.value ? { width: len } : { height: len }
180
189
  })
181
190
 
191
+ // Resolve a CSS length to px against the viewport. Anything it cannot read
192
+ // (calc(), min()) comes back NaN for the caller to fall back on.
193
+ function toPx(css: string): number {
194
+ const n = Number.parseFloat(css)
195
+ if (css.endsWith("rem")) {
196
+ const root = typeof document === "undefined" ? 16 : Number.parseFloat(getComputedStyle(document.documentElement).fontSize) || 16
197
+ return n * root
198
+ }
199
+ if (css.endsWith("vw")) return (n / 100) * window.innerWidth
200
+ if (css.endsWith("vh")) return (n / 100) * window.innerHeight
201
+ if (css.endsWith("px") || /^[\d.]+$/.test(css.trim())) return n
202
+ return Number.NaN
203
+ }
204
+
182
205
  // Drag the inner edge to resize. For a right drawer the panel grows as the
183
- // pointer moves left (width = viewportRight − clientX); mirror for left.
206
+ // pointer moves left (width = viewportRight − clientX); mirror for left. The
207
+ // pointer is captured, so a release over an iframe or outside the window still
208
+ // ends the drag, and a cancelled touch ends it too.
209
+ let endResize: (() => void) | null = null
210
+
184
211
  function startResize(e: PointerEvent) {
185
212
  if (!props.resizable || !isHorizontal.value) return
186
213
  e.preventDefault()
214
+ endResize?.()
215
+ const grip = e.currentTarget instanceof HTMLElement ? e.currentTarget : null
216
+ grip?.setPointerCapture(e.pointerId)
187
217
  const fromRight = dir.value === "right"
188
- const toPx = (css: string) => {
189
- // Resolve rem/vw to px against the viewport for clamping.
190
- if (css.endsWith("rem")) return Number.parseFloat(css) * 16
191
- if (css.endsWith("vw")) return (Number.parseFloat(css) / 100) * window.innerWidth
192
- return Number.parseFloat(css)
193
- }
194
218
  const min = toPx(props.minSize)
195
219
  const max = toPx(props.maxSize)
196
220
  const onMove = (ev: PointerEvent) => {
221
+ if (ev.pointerId !== e.pointerId) return
197
222
  const raw = fromRight ? window.innerWidth - ev.clientX : ev.clientX
198
- resizedPx.value = Math.max(min, Math.min(max, raw))
223
+ resizedPx.value = Math.max(Number.isNaN(min) ? 0 : min, Math.min(Number.isNaN(max) ? Infinity : max, raw))
199
224
  }
200
- const onUp = () => {
225
+ const end = () => {
201
226
  window.removeEventListener("pointermove", onMove)
202
- window.removeEventListener("pointerup", onUp)
227
+ window.removeEventListener("pointerup", end)
228
+ window.removeEventListener("pointercancel", end)
229
+ grip?.removeEventListener("lostpointercapture", end)
203
230
  document.body.style.userSelect = ""
204
231
  document.body.style.cursor = ""
232
+ endResize = null
205
233
  }
234
+ endResize = end
206
235
  document.body.style.userSelect = "none"
207
236
  document.body.style.cursor = "ew-resize"
208
237
  window.addEventListener("pointermove", onMove)
209
- window.addEventListener("pointerup", onUp)
238
+ window.addEventListener("pointerup", end)
239
+ window.addEventListener("pointercancel", end)
240
+ grip?.addEventListener("lostpointercapture", end)
210
241
  }
211
242
 
243
+ onBeforeUnmount(() => endResize?.())
244
+
212
245
  const slots = useSlots()
213
246
  const hasHeader = computed(() => Boolean(props.title || slots.title))
214
247
 
@@ -281,9 +314,9 @@ watch(
281
314
  // Resolve the panel's main-axis size in px to map drag distance → progress.
282
315
  function panelPx(): number {
283
316
  const vw = typeof window === "undefined" ? 0 : window.innerWidth
284
- // mirror sizeStyle's min(94vw, SIZES[size]rem); 1rem≈16px.
285
- const cap = props.width ? Number.parseFloat(props.width) : SIZES[props.size] * 16
286
- return Math.min(vw * 0.94, Number.isFinite(cap) ? cap : SIZES[props.size] * 16)
317
+ const preset = toPx(`${SIZES[props.size]}rem`)
318
+ const cap = props.width ? toPx(props.width) : preset
319
+ return Math.min(vw * 0.94, Number.isFinite(cap) ? cap : preset)
287
320
  }
288
321
 
289
322
  function edgeEnabled(): boolean {
@@ -479,6 +512,8 @@ const previewStyle = computed(() => {
479
512
  :class="{ 'lp-scrim--light': frosted, 'lp-scrim--nested': layer.nested }"
480
513
  />
481
514
  <DrawerContent
515
+ ref="contentRef"
516
+ v-bind="description ? {} : { 'aria-describedby': undefined }"
482
517
  :class="contentClass"
483
518
  :style="[sizeStyle, { zIndex: layer.panel }]"
484
519
  :data-vaul-animate="handingOff || skipOpenSlide ? 'false' : undefined"
@@ -498,6 +533,8 @@ const previewStyle = computed(() => {
498
533
  />
499
534
  </div>
500
535
 
536
+ <DrawerTitle v-if="!hasHeader && ariaLabel" class="sr-only">{{ ariaLabel }}</DrawerTitle>
537
+
501
538
  <!-- Drag handle: a pill the user grabs; vaul wires the drag to it. -->
502
539
  <DrawerHandle
503
540
  v-if="showHandle"
@@ -519,7 +556,7 @@ const previewStyle = computed(() => {
519
556
  </div>
520
557
  <DrawerClose
521
558
  class="group flex shrink-0 items-center rounded-md p-1 text-muted outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
522
- aria-label="Close"
559
+ :aria-label="text.close"
523
560
  >
524
561
  <LpIcon
525
562
  name="lucide:x"
@@ -23,7 +23,7 @@ export default { inheritAttrs: false }
23
23
  * invisible to a keyboard: nothing focusable, nothing that Enter activates, and
24
24
  * the file dialog unreachable without a mouse.
25
25
  */
26
- import { onBeforeUnmount, onMounted, ref } from "vue"
26
+ import { onBeforeUnmount, onMounted, ref, watch } from "vue"
27
27
  import { useMergedAttrs } from "../composables/useMergedClass"
28
28
  import { useReveal } from "../composables/useReveal"
29
29
  import LpIcon from "./LpIcon.vue"
@@ -168,17 +168,34 @@ function onPicked(event: Event) {
168
168
  }
169
169
 
170
170
  function windowPaste(event: ClipboardEvent) {
171
- if (props.disabled) return
171
+ // Already taken by the zone itself, which the paste bubbled up from.
172
+ if (props.disabled || event.defaultPrevented) return
172
173
  const target = event.target as HTMLElement | null
173
174
  // A paste aimed at somewhere you can type belongs to that thing.
174
175
  if (target?.closest("input, textarea, [contenteditable]")) return
175
176
  onPaste(event)
176
177
  }
177
178
 
179
+ let listening = false
180
+ function listen(on: boolean) {
181
+ if (on === listening || typeof window === "undefined") return
182
+ listening = on
183
+ if (on) window.addEventListener("paste", windowPaste)
184
+ else window.removeEventListener("paste", windowPaste)
185
+ }
186
+ let mounted = false
178
187
  onMounted(() => {
179
- if (props.pasteTarget) window.addEventListener("paste", windowPaste)
188
+ mounted = true
189
+ listen(props.pasteTarget)
190
+ })
191
+ watch(
192
+ () => props.pasteTarget,
193
+ (on) => mounted && listen(on),
194
+ )
195
+ onBeforeUnmount(() => {
196
+ mounted = false
197
+ listen(false)
180
198
  })
181
- onBeforeUnmount(() => window.removeEventListener("paste", windowPaste))
182
199
 
183
200
  defineExpose({ browse: () => input.value?.click() })
184
201
 
@@ -38,10 +38,21 @@ import LpEmptyState from "./LpEmptyState.vue"
38
38
  import LpFileTreeNode from "./LpFileTreeNode.vue"
39
39
  import LpScrollArea from "./LpScrollArea.vue"
40
40
  import { useMergedAttrs } from "../composables/useMergedClass"
41
+ import { useLabels } from "../composables/useLpConfig"
41
42
 
42
43
  export type { FileNode } from "./fileTree"
43
44
 
44
45
  /** Rolled-up stats for the ticked entries — what a backup picker summarises. */
46
+ export interface FileTreeLabels {
47
+ [key: string]: string
48
+ empty: string
49
+ emptyDescription: string
50
+ file: string
51
+ files: string
52
+ folder: string
53
+ folders: string
54
+ }
55
+
45
56
  export interface FileTreeSummary {
46
57
  /** Ticked files (directories excluded — they're containers, not payload). */
47
58
  files: number
@@ -84,6 +95,7 @@ const props = withDefaults(
84
95
  /** Empty-state copy when `nodes` is empty. */
85
96
  emptyLabel?: string
86
97
  emptyDescription?: string
98
+ labels?: Partial<FileTreeLabels>
87
99
  }>(),
88
100
  {
89
101
  sort: true,
@@ -93,8 +105,6 @@ const props = withDefaults(
93
105
  showModified: false,
94
106
  loading: false,
95
107
  skeletonRows: 8,
96
- emptyLabel: "No files",
97
- emptyDescription: "This folder is empty.",
98
108
  },
99
109
  )
100
110
 
@@ -131,6 +141,19 @@ watch(
131
141
  },
132
142
  )
133
143
 
144
+ const text = useLabels<FileTreeLabels>(
145
+ "fileTree",
146
+ {
147
+ empty: "No files",
148
+ emptyDescription: "This folder is empty.",
149
+ file: "file",
150
+ files: "files",
151
+ folder: "folder",
152
+ folders: "folders",
153
+ },
154
+ () => props.labels,
155
+ )
156
+
134
157
  const loadingSet = computed(() => new Set(props.loadingIds ?? []))
135
158
 
136
159
  // Same controlled/uncontrolled deal as `expanded`.
@@ -229,7 +252,7 @@ const summary = computed<FileTreeSummary>(() => {
229
252
  }
230
253
  })
231
254
 
232
- watch(summary, (value) => emit("summary", value), { deep: false })
255
+ watch(summary, (value) => emit("summary", value), { immediate: true })
233
256
 
234
257
  function setChecked(next: Set<string>) {
235
258
  checkedIds.value = next
@@ -350,6 +373,9 @@ function expand(node: FileNode) {
350
373
 
351
374
  function collapse(node: FileNode) {
352
375
  if (!openIds.value.has(node.id)) return
376
+ // A fetch that failed left `children` unset; collapsing lets the next open
377
+ // ask again instead of showing an empty folder forever.
378
+ if (node.children === undefined && !loadingSet.value.has(node.id)) requested.delete(node.id)
353
379
  const next = new Set(openIds.value)
354
380
  next.delete(node.id)
355
381
  setExpanded(next)
@@ -506,8 +532,8 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
506
532
  <LpEmptyState
507
533
  v-else-if="!roots.length"
508
534
  icon="lucide:folder-search"
509
- :title="emptyLabel"
510
- :description="emptyDescription"
535
+ :title="emptyLabel ?? text.empty"
536
+ :description="emptyDescription ?? text.emptyDescription"
511
537
  />
512
538
 
513
539
  <!-- h-full so the tree fills whatever box the caller gave it and the scroll
@@ -558,9 +584,9 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
558
584
  class="shrink-0 border-t border-line px-2 py-1.5 text-xs text-muted"
559
585
  >
560
586
  <slot name="summary" v-bind="summary">
561
- {{ summary.files }} {{ summary.files === 1 ? "file" : "files" }}<template
587
+ {{ summary.files }} {{ summary.files === 1 ? text.file : text.files }}<template
562
588
  v-if="summary.dirs"
563
- >, {{ summary.dirs }} {{ summary.dirs === 1 ? "folder" : "folders" }}</template>
589
+ >, {{ summary.dirs }} {{ summary.dirs === 1 ? text.folder : text.folders }}</template>
564
590
  <template v-if="summary.bytes !== undefined"> · {{ summary.sizeLabel }}</template>
565
591
  </slot>
566
592
  </div>