@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
@@ -1,5 +1,14 @@
1
1
  import { type ComputedRef, type DeepReadonly, type Ref } from "vue";
2
2
  import { type TokenSet } from "./tokens";
3
+ /** A light/dark pair followed by the operating system's colour scheme. */
4
+ export interface SystemThemes {
5
+ light: TokenSet;
6
+ dark: TokenSet;
7
+ }
8
+ /** Whether the OS asks for dark. True where it cannot be asked (server, old engines). */
9
+ export declare function prefersDarkScheme(): boolean;
10
+ /** Every CSS variable a theme sets, in application order. */
11
+ export declare function themeEntries(input: TokenSet): [string, string][];
3
12
  export declare function applyTheme(theme: TokenSet, root?: HTMLElement): void;
4
13
  /** Point a circular theme reveal expands from (viewport px). */
5
14
  export interface RevealOrigin {
@@ -11,31 +20,45 @@ export declare function serializeTheme(theme: TokenSet): string;
11
20
  export declare function themeToCssVars(theme: TokenSet): string;
12
21
  /** Render a theme as a complete CSS rule (default selector `:root`). */
13
22
  export declare function themeToCssRule(theme: TokenSet, selector?: string): string;
14
- /** Parse + shallow-validate a theme JSON; throws on a malformed shape. */
23
+ export declare function normalizeTheme(raw: Partial<TokenSet> & {
24
+ name?: string;
25
+ }): TokenSet;
26
+ /** Parse + validate a theme JSON; throws on a malformed shape. */
15
27
  export declare function parseTheme(json: string): TokenSet;
16
28
  export declare function bootstrapTheme(fallback: TokenSet): TokenSet;
17
29
  export declare function cacheTheme(theme: TokenSet): void;
30
+ /** Persist "follow the OS" with the pair it switches between. */
31
+ export declare function cacheSystemThemes(system: SystemThemes): void;
18
32
  export declare const THEME_COOKIE = "lp-theme";
19
33
  /**
20
34
  * Resolve a theme name (as read from the `lp-theme` cookie) against a set of
21
35
  * known themes. Returns `null` when the name is absent or unknown, so the caller
22
36
  * can fall back to its own default.
23
37
  *
24
- * Server-side counterpart to `cacheTheme`: pair it with `themeToCssRule` to
25
- * inline the right variables into `<head>` during SSR.
38
+ * Accepts both a raw and an already-decoded value: frameworks differ on whether
39
+ * their cookie helpers decode, and a name containing `%` must not throw.
40
+ * A "follow the OS" cookie resolves to its dark member unless `prefersDark` is
41
+ * false (pass it from the `Sec-CH-Prefers-Color-Scheme` request header).
26
42
  */
27
- export declare function themeFromCookie(cookieValue: string | null | undefined, themes: readonly TokenSet[]): TokenSet | null;
43
+ export declare function themeFromCookie(cookieValue: string | null | undefined, themes: readonly TokenSet[], options?: {
44
+ prefersDark?: boolean;
45
+ }): TokenSet | null;
28
46
  export interface UseTheme {
29
47
  /** The active theme, kept in sync by the kit. Null until hydrated (SSR/first
30
- * paint) — reactive, so a switcher binds straight to it. */
48
+ * paint), reactive, so a switcher binds straight to it. */
31
49
  current: DeepReadonly<Ref<TokenSet | null>>;
32
50
  /** The active theme's name (""="not yet hydrated"). */
33
51
  currentName: ComputedRef<string>;
34
- /** Set + persist a theme (with the circular reveal if `origin`/transition).
35
- * This is the one call a switcher needs — it updates `current` too. */
52
+ /** True while the theme follows the OS colour scheme (see setSystem). */
53
+ system: ComputedRef<boolean>;
54
+ /** Set + persist a theme (with the circular reveal). Stops following the OS. */
36
55
  setTheme: (theme: TokenSet, origin?: RevealOrigin) => void;
37
- /** Seed the fallback (used when nothing is cached) and hydrate now. Call once
38
- * at app start (client) — optional; the first useTheme() hydrates anyway. */
56
+ /**
57
+ * Follow the OS colour scheme, switching between `themes.light` and
58
+ * `themes.dark` as it changes. Persisted like any other choice.
59
+ */
60
+ setSystem: (themes: SystemThemes, origin?: RevealOrigin) => void;
61
+ /** Seed the fallback (used when nothing is cached) and hydrate now. */
39
62
  init: (fallback: TokenSet) => TokenSet;
40
63
  apply: (theme: TokenSet) => void;
41
64
  applyWithTransition: (theme: TokenSet, origin?: RevealOrigin) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leavepulse/ui",
3
- "version": "0.43.3",
3
+ "version": "0.44.0",
4
4
  "type": "module",
5
5
  "description": "LeavePulse token-driven Vue component kit.",
6
6
  "repository": {
@@ -28,10 +28,11 @@
28
28
  "./scrollbar.css": "./src/tokens/scrollbar.css",
29
29
  "./canvas.css": "./src/canvas.css",
30
30
  "./package.json": "./package.json",
31
- "./dist/*": {
31
+ "./dist/*.js": {
32
32
  "types": "./dist/*.d.ts",
33
- "default": "./dist/*"
34
- }
33
+ "default": "./dist/*.js"
34
+ },
35
+ "./dist/*": "./dist/*"
35
36
  },
36
37
  "publishConfig": {
37
38
  "registry": "https://registry.npmjs.org",
@@ -40,7 +41,7 @@
40
41
  "sideEffects": [
41
42
  "**/*.css",
42
43
  "**/icons.ts",
43
- "**/icons.js"
44
+ "**/chunks/LpIcon-*.js"
44
45
  ],
45
46
  "scripts": {
46
47
  "dev": "vite",
@@ -52,14 +53,14 @@
52
53
  "test": "bun test",
53
54
  "release": "npm version patch -m \"chore: release v%s\" && git push --follow-tags",
54
55
  "release:minor": "npm version minor -m \"chore: release v%s\" && git push --follow-tags",
55
- "release:major": "npm version major -m \"chore: release v%s\" && git push --follow-tags"
56
+ "release:major": "npm version major -m \"chore: release v%s\" && git push --follow-tags",
57
+ "check:pack": "node scripts/check-pack.mjs"
56
58
  },
57
59
  "peerDependencies": {
58
60
  "@iconify-json/lucide": "^1.2.0",
59
61
  "@iconify/vue": "^5.0.0",
60
62
  "@internationalized/date": "^3.5.0",
61
63
  "@nuxt/kit": "^3.0.0 || ^4.0.0",
62
- "@vueuse/core": "^12.0.0 || ^13.0.0 || ^14.0.0",
63
64
  "motion-v": "^2.2.0",
64
65
  "reka-ui": "^2.9.0",
65
66
  "tailwind-variants": "^3.0.0",
@@ -1,9 +1,8 @@
1
1
  /* AUTO-GENERATED by scripts/gen-component-names.mjs — do not edit.
2
2
  * The full list of kit component names, kept in sync with the component files
3
- * so the Nuxt module (./nuxt) auto-registers every component including new ones. */
3
+ * so the Nuxt module (./nuxt) auto-registers every exported component including new ones. */
4
4
  export const COMPONENT_NAMES = [
5
5
  "LayoutCanvas",
6
- "LayoutNode",
7
6
  "LpActionBar",
8
7
  "LpAddressInput",
9
8
  "LpAlert",
@@ -21,6 +20,7 @@ export const COMPONENT_NAMES = [
21
20
  "LpColorPicker",
22
21
  "LpCommandPalette",
23
22
  "LpConfirmDialog",
23
+ "LpConfirmHost",
24
24
  "LpContextMenu",
25
25
  "LpDatePicker",
26
26
  "LpDisclosure",
@@ -30,13 +30,11 @@ export const COMPONENT_NAMES = [
30
30
  "LpDropzone",
31
31
  "LpEmptyState",
32
32
  "LpFileTree",
33
- "LpFileTreeNode",
34
33
  "LpFormField",
35
34
  "LpIcon",
36
35
  "LpImageEditor",
37
36
  "LpInfraNode",
38
37
  "LpInput",
39
- "LpLaneNode",
40
38
  "LpLightbox",
41
39
  "LpLink",
42
40
  "LpLogViewer",
@@ -65,7 +63,6 @@ export const COMPONENT_NAMES = [
65
63
  "LpShift",
66
64
  "LpShortcutsDialog",
67
65
  "LpSidebar",
68
- "LpSidebarNav",
69
66
  "LpSkeleton",
70
67
  "LpSlider",
71
68
  "LpSparkline",
@@ -76,12 +73,13 @@ export const COMPONENT_NAMES = [
76
73
  "LpTableOfContents",
77
74
  "LpTabs",
78
75
  "LpTextarea",
76
+ "LpThemeProvider",
79
77
  "LpThemeSwitcher",
80
78
  "LpTilt",
81
79
  "LpTimeField",
82
80
  "LpToaster",
83
81
  "LpTooltip",
82
+ "LpTooltipProvider",
84
83
  "LpTopologyCanvas",
85
- "LpUptimeBar",
86
- "LpWaypointEdge"
84
+ "LpUptimeBar"
87
85
  ] as const
@@ -0,0 +1,88 @@
1
+ <script setup lang="ts">
2
+ /*
3
+ * The rows of an LpContextMenu panel, rendering itself for every submenu so
4
+ * `children` nests to any depth. Internal: not exported.
5
+ */
6
+ import {
7
+ ContextMenuCheckboxItem,
8
+ ContextMenuItem,
9
+ ContextMenuPortal,
10
+ ContextMenuSeparator,
11
+ ContextMenuSub,
12
+ ContextMenuSubContent,
13
+ ContextMenuSubTrigger,
14
+ } from "reka-ui"
15
+ import type { ContextMenuItemDef } from "./LpContextMenu.vue"
16
+ import LpIcon from "./LpIcon.vue"
17
+
18
+ defineOptions({ name: "ContextMenuItems" })
19
+
20
+ defineProps<{
21
+ items: ContextMenuItemDef[]
22
+ panel: string
23
+ }>()
24
+
25
+ const ITEM =
26
+ "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"
27
+ const ICON =
28
+ "transition-transform duration-[var(--duration-fast)] ease-[var(--ease-emphasized)] group-data-[highlighted]/item:translate-x-0.5"
29
+ </script>
30
+
31
+ <template>
32
+ <template v-for="(item, i) in items" :key="i">
33
+ <ContextMenuSeparator v-if="item.separatorBefore" class="my-1 h-px bg-line" />
34
+
35
+ <ContextMenuSub v-if="item.children?.length">
36
+ <ContextMenuSubTrigger
37
+ :class="[ITEM, 'justify-between text-ink data-[highlighted]:text-brand data-[state=open]:bg-brand-soft data-[state=open]:text-brand']"
38
+ :disabled="item.disabled"
39
+ >
40
+ <span class="flex items-center gap-2">
41
+ <LpIcon v-if="item.icon" :name="item.icon" :size="15" :class="ICON" />
42
+ {{ item.label }}
43
+ </span>
44
+ <LpIcon name="lucide:chevron-right" :size="14" class="text-muted" />
45
+ </ContextMenuSubTrigger>
46
+ <ContextMenuPortal>
47
+ <ContextMenuSubContent :class="panel" :side-offset="2" :align-offset="-4">
48
+ <ContextMenuItems :items="item.children" :panel="panel" />
49
+ </ContextMenuSubContent>
50
+ </ContextMenuPortal>
51
+ </ContextMenuSub>
52
+
53
+ <!-- Tick-box item: a real checkbox item, so assistive tech hears its state.
54
+ `@select.prevent` keeps the menu open, since these come in lists where
55
+ several are set in one visit. The tick occupies a fixed slot so
56
+ unticked rows stay aligned with ticked ones. -->
57
+ <ContextMenuCheckboxItem
58
+ v-else-if="item.checked !== undefined"
59
+ :model-value="item.checked"
60
+ :class="[ITEM, 'justify-between text-ink data-[highlighted]:text-brand']"
61
+ :disabled="item.disabled"
62
+ @select.prevent="item.onSelect?.()"
63
+ >
64
+ <span class="flex items-center gap-2">
65
+ <LpIcon
66
+ :name="item.checked ? 'lucide:check' : 'lucide:minus'"
67
+ :size="15"
68
+ :class="[ICON, item.checked ? '' : 'opacity-25']"
69
+ />
70
+ {{ item.label }}
71
+ </span>
72
+ <kbd v-if="item.shortcut" class="text-xs text-muted">{{ item.shortcut }}</kbd>
73
+ </ContextMenuCheckboxItem>
74
+
75
+ <ContextMenuItem
76
+ v-else
77
+ :class="[ITEM, 'justify-between', item.danger ? 'text-danger data-[highlighted]:bg-danger-soft data-[highlighted]:text-danger' : 'text-ink data-[highlighted]:text-brand']"
78
+ :disabled="item.disabled"
79
+ @select="item.onSelect?.()"
80
+ >
81
+ <span class="flex items-center gap-2">
82
+ <LpIcon v-if="item.icon" :name="item.icon" :size="15" :class="ICON" />
83
+ {{ item.label }}
84
+ </span>
85
+ <kbd v-if="item.shortcut" class="text-xs text-muted">{{ item.shortcut }}</kbd>
86
+ </ContextMenuItem>
87
+ </template>
88
+ </template>
@@ -22,7 +22,8 @@ import {
22
22
  type Split,
23
23
  } from "../layout/tree"
24
24
  import type { BlockRegistry } from "../layout/registry"
25
- import { ref } from "vue"
25
+ import { layoutController, type LayoutController } from "../layout/useLayout"
26
+ import { computed, ref } from "vue"
26
27
 
27
28
  const props = defineProps<{
28
29
  modelValue: Split
@@ -42,13 +43,30 @@ defineSlots<{
42
43
 
43
44
  const dragId = ref<string | null>(null)
44
45
 
46
+ // A tree owned by useLayout() is edited through its controller, so every edit
47
+ // lands in undo history; a bare tree is mutated directly as before.
48
+ const edits = computed<LayoutController>(() => {
49
+ const tree = props.modelValue
50
+ return (
51
+ layoutController(tree) ?? {
52
+ move: (movingId, targetId, side) => moveLeaf(tree, movingId, targetId, side),
53
+ removeBlock: (leafId, index) => removeBlock(tree, leafId, index),
54
+ reorderTab: (leafId, from, to) => reorderTab(tree, leafId, from, to),
55
+ setActive: (leafId, index) => setActiveTab(tree, leafId, index),
56
+ resize: (parent, index, delta) => resizeAt(parent, index, delta),
57
+ beginResize: () => {},
58
+ endResize: () => {},
59
+ }
60
+ )
61
+ })
62
+
45
63
  function onDrop(targetId: string, side: Side) {
46
- if (dragId.value) moveLeaf(props.modelValue, dragId.value, targetId, side)
64
+ if (dragId.value) edits.value.move(dragId.value, targetId, side)
47
65
  dragId.value = null
48
66
  }
49
67
 
50
68
  function onResize(parent: Split, index: number, delta: number) {
51
- resizeAt(parent, index, delta)
69
+ edits.value.resize(parent, index, delta)
52
70
  }
53
71
  </script>
54
72
 
@@ -63,10 +81,12 @@ function onResize(parent: Split, index: number, delta: number) {
63
81
  @dragstart="(id) => (dragId = id)"
64
82
  @dragend="dragId = null"
65
83
  @drop="onDrop"
66
- @set-active="(leafId, i) => setActiveTab(modelValue, leafId, i)"
67
- @remove-block="(leafId, i) => removeBlock(modelValue, leafId, i)"
68
- @reorder-tab="(leafId, from, to) => reorderTab(modelValue, leafId, from, to)"
84
+ @set-active="(leafId, i) => edits.setActive(leafId, i)"
85
+ @remove-block="(leafId, i) => edits.removeBlock(leafId, i)"
86
+ @reorder-tab="(leafId, from, to) => edits.reorderTab(leafId, from, to)"
69
87
  @resize="onResize"
88
+ @resize-start="edits.beginResize()"
89
+ @resize-end="edits.endResize()"
70
90
  >
71
91
  <template #block="slotProps">
72
92
  <slot name="block" v-bind="slotProps" />
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { Motion } from "motion-v"
3
- import { computed, ref } from "vue"
3
+ import { computed, onBeforeUnmount, ref } from "vue"
4
4
  import type { LayoutNode, Leaf, Side, Split } from "../layout/tree"
5
5
  import type { BlockRegistry } from "../layout/registry"
6
6
  import { blockTitle } from "../layout/registry"
@@ -21,6 +21,8 @@ const emit = defineEmits<{
21
21
  (e: "remove-block", leafId: string, index: number): void
22
22
  (e: "reorder-tab", leafId: string, from: number, to: number): void
23
23
  (e: "resize", parent: Split, index: number, deltaFraction: number): void
24
+ (e: "resize-start"): void
25
+ (e: "resize-end"): void
24
26
  }>()
25
27
 
26
28
  // Explicit slot shape so the recursive #block pass-through types cleanly.
@@ -97,10 +99,18 @@ function onDragOver(ev: DragEvent) {
97
99
  else hoverSide.value = "center"
98
100
  }
99
101
 
102
+ let stopSplitter: (() => void) | null = null
103
+ onBeforeUnmount(() => stopSplitter?.())
104
+
100
105
  function startSplitter(ev: PointerEvent, parent: Split, index: number) {
101
106
  ev.preventDefault()
102
- const host = (ev.currentTarget as HTMLElement).parentElement
107
+ const handle = ev.currentTarget as HTMLElement
108
+ const host = handle.parentElement
103
109
  if (!host) return
110
+ // Capture keeps the moves coming when the pointer crosses an iframe or leaves
111
+ // the window; pointercancel (a touch the browser took over) ends it too.
112
+ handle.setPointerCapture?.(ev.pointerId)
113
+ emit("resize-start")
104
114
  const rect = host.getBoundingClientRect()
105
115
  const horizontal = parent.dir === "row"
106
116
  const span = horizontal ? rect.width : rect.height
@@ -114,9 +124,14 @@ function startSplitter(ev: PointerEvent, parent: Split, index: number) {
114
124
  const up = () => {
115
125
  window.removeEventListener("pointermove", move)
116
126
  window.removeEventListener("pointerup", up)
127
+ window.removeEventListener("pointercancel", up)
128
+ stopSplitter = null
129
+ emit("resize-end")
117
130
  }
131
+ stopSplitter = up
118
132
  window.addEventListener("pointermove", move)
119
133
  window.addEventListener("pointerup", up)
134
+ window.addEventListener("pointercancel", up)
120
135
  }
121
136
  </script>
122
137
 
@@ -141,6 +156,8 @@ function startSplitter(ev: PointerEvent, parent: Split, index: number) {
141
156
  @remove-block="(l, idx) => emit('remove-block', l, idx)"
142
157
  @reorder-tab="(l, from, to) => emit('reorder-tab', l, from, to)"
143
158
  @resize="(p, idx, d) => emit('resize', p, idx, d)"
159
+ @resize-start="emit('resize-start')"
160
+ @resize-end="emit('resize-end')"
144
161
  >
145
162
  <template #block="slotProps">
146
163
  <slot name="block" v-bind="slotProps" />
@@ -20,7 +20,8 @@
20
20
  * name rather than `alertdialog` — it is a landmark you can Tab into, not a
21
21
  * thing demanding an answer before anything else can happen.
22
22
  */
23
- import { computed, nextTick, ref, useSlots, watch } from "vue"
23
+ import { computed, onBeforeUnmount, ref, useSlots, watch } from "vue"
24
+ import { useLabels } from "../composables/useLpConfig"
24
25
  import { useBarStack } from "./actionBar"
25
26
  import { CLOSE_ICON } from "./dropdown"
26
27
  import LpButton from "./LpButton.vue"
@@ -85,6 +86,7 @@ const props = withDefaults(
85
86
  maxWidth?: string
86
87
  /** Accessible name for the landmark. Falls back to `title`. */
87
88
  ariaLabel?: string
89
+ labels?: Partial<{ dismiss: string }>
88
90
  }>(),
89
91
  {
90
92
  open: true,
@@ -100,6 +102,7 @@ const emit = defineEmits<{
100
102
  }>()
101
103
 
102
104
  const slots = useSlots()
105
+ const text = useLabels("actionBar", { dismiss: "Dismiss" }, () => props.labels)
103
106
 
104
107
  // The last action is the primary one unless the caller has said otherwise; the
105
108
  // rest default to `ghost` so a bar with three buttons still has one obvious
@@ -154,19 +157,39 @@ const stackOffset = computed(() => {
154
157
  return offset()
155
158
  })
156
159
 
160
+ /*
161
+ * The height is watched, not measured once: a description that wraps after a
162
+ * rotation, or content swapped while the bar is up, changes it, and the bars
163
+ * above would overlap this one.
164
+ */
165
+ let observer: ResizeObserver | null = null
166
+
157
167
  watch(
158
168
  () => props.open,
159
- async (isOpen) => {
169
+ (isOpen) => {
170
+ // Nothing on the server ever unregisters, so registering there would leak
171
+ // one slot per rendered request into every later page.
172
+ if (typeof window === "undefined") return
160
173
  if (!isOpen) {
161
174
  unregister()
162
175
  return
163
176
  }
164
177
  register()
165
- await nextTick()
166
- if (root.value) setHeight(root.value.getBoundingClientRect().height)
167
178
  },
168
179
  { immediate: true },
169
180
  )
181
+
182
+ watch(root, (el) => {
183
+ observer?.disconnect()
184
+ observer = null
185
+ if (!el) return
186
+ setHeight(el.getBoundingClientRect().height)
187
+ if (typeof ResizeObserver === "undefined") return
188
+ observer = new ResizeObserver(() => setHeight(el.getBoundingClientRect().height))
189
+ observer.observe(el)
190
+ })
191
+
192
+ onBeforeUnmount(() => observer?.disconnect())
170
193
  </script>
171
194
 
172
195
  <template>
@@ -194,9 +217,9 @@ watch(
194
217
  ref="root"
195
218
  role="region"
196
219
  :aria-label="ariaLabel ?? title"
197
- class="pointer-events-none fixed inset-x-0 bottom-0 z-(--z-modal) flex justify-center transition-[translate] duration-[var(--duration-medium)] ease-[var(--ease-emphasized)] motion-reduce:transition-none"
220
+ class="pointer-events-none fixed inset-x-0 bottom-0 flex justify-center transition-[translate] duration-[var(--duration-medium)] ease-[var(--ease-emphasized)] motion-reduce:transition-none"
198
221
  :class="width === 'full' ? '' : 'p-3 sm:p-4'"
199
- :style="{ translate: `0 -${stackOffset}px` }"
222
+ :style="{ translate: `0 -${stackOffset}px`, zIndex: 'calc(var(--z-overlay) - 1)' }"
200
223
  >
201
224
  <div
202
225
  class="lp-skin-panel pointer-events-auto flex items-center gap-3 border-solid bg-surface-overlay"
@@ -261,7 +284,7 @@ watch(
261
284
  <button
262
285
  v-if="dismissible"
263
286
  type="button"
264
- aria-label="Dismiss"
287
+ :aria-label="text.dismiss"
265
288
  class="group -mr-1 flex size-8 shrink-0 items-center justify-center rounded-control text-muted outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
266
289
  @click="dismiss"
267
290
  >
@@ -19,6 +19,7 @@ export default { inheritAttrs: false }
19
19
  * of every form that never shows a map.
20
20
  */
21
21
  import { computed, defineAsyncComponent, onMounted, ref, watch } from "vue"
22
+ import { useAsyncTask } from "../composables/useAsyncTask"
22
23
  import {
23
24
  type AddressField,
24
25
  type AddressLabels,
@@ -113,11 +114,32 @@ const geocoder = computed(() => props.provider ?? fallbackProvider)
113
114
 
114
115
  const value = computed<AddressValue>(() => props.modelValue ?? emptyAddress())
115
116
 
117
+ // Patches build on the last value emitted, not on the prop: the prop only
118
+ // catches up on the parent's next render, and two patches in one tick (the
119
+ // text a suggestion fills in, then the parts it resolved) would otherwise each
120
+ // start from the stale value and the second would undo the first.
121
+ let latest: AddressValue | undefined
122
+ watch(
123
+ () => props.modelValue,
124
+ () => (latest = undefined),
125
+ )
116
126
  function patch(changes: Partial<AddressValue>) {
117
- emit("update:modelValue", { ...value.value, ...changes })
127
+ latest = { ...(latest ?? value.value), ...changes }
128
+ emit("update:modelValue", latest)
129
+ }
130
+
131
+ // A part typed by hand describes a different place than the point a suggestion
132
+ // or the map resolved, so the coordinates go with it: kept, they would pin the
133
+ // stored address to somewhere else.
134
+ function edit(changes: Partial<AddressValue>) {
135
+ patch({ ...changes, lat: undefined, lon: undefined })
118
136
  }
119
137
 
120
- const countries = ref<Country[]>([])
138
+ const allCountries = ref<Country[]>([])
139
+ const countries = computed(() => {
140
+ const only = props.only?.map((code) => code.toUpperCase())
141
+ return only?.length ? allCountries.value.filter((c) => only.includes(c.iso2)) : allCountries.value
142
+ })
121
143
  const countryOptions = computed(() =>
122
144
  countries.value.map((country) => ({
123
145
  value: country.iso2,
@@ -126,9 +148,7 @@ const countryOptions = computed(() =>
126
148
  )
127
149
 
128
150
  onMounted(async () => {
129
- const list = await loadCountries()
130
- const only = props.only?.map((code) => code.toUpperCase())
131
- countries.value = only?.length ? list.filter((c) => only.includes(c.iso2)) : list
151
+ allCountries.value = await loadCountries()
132
152
  })
133
153
 
134
154
  /*
@@ -138,37 +158,31 @@ onMounted(async () => {
138
158
  */
139
159
  function useSuggestions(field: AddressField) {
140
160
  const options = ref<AddressSuggestion[]>([])
141
- const loading = ref(false)
142
- let timer: ReturnType<typeof setTimeout> | undefined
143
- let controller: AbortController | undefined
161
+ const task = useAsyncTask<AddressSuggestion[]>()
144
162
 
145
163
  function query(term: string) {
146
- if (timer) clearTimeout(timer)
147
- controller?.abort()
148
164
  if (term.trim().length < props.minChars) {
165
+ task.cancel()
149
166
  options.value = []
150
- loading.value = false
151
167
  return
152
168
  }
153
- loading.value = true
154
- timer = setTimeout(async () => {
155
- controller = new AbortController()
156
- try {
157
- options.value = await geocoder.value.search(term.trim(), field, {
169
+ task.run(
170
+ (signal) =>
171
+ geocoder.value.search(term.trim(), field, {
158
172
  country: value.value.country || undefined,
159
173
  language: props.language,
160
- signal: controller.signal,
161
- })
162
- } catch {
163
- // Abort or network failure: leave the field usable as free text.
164
- options.value = []
165
- } finally {
166
- loading.value = false
167
- }
168
- }, props.debounce)
174
+ signal,
175
+ }),
176
+ {
177
+ delay: props.debounce,
178
+ apply: (found) => (options.value = found),
179
+ // Network failure: leave the field usable as free text.
180
+ fail: () => (options.value = []),
181
+ },
182
+ )
169
183
  }
170
184
 
171
- return { options, loading, query }
185
+ return { options, loading: task.pending, query }
172
186
  }
173
187
 
174
188
  const lineSuggestions = useSuggestions("line")
@@ -230,19 +244,21 @@ watch(mapOpen, (open) => {
230
244
  resolved.value = undefined
231
245
  })
232
246
 
233
- async function onPick(next: LatLon) {
247
+ // Pins dropped in quick succession each start a lookup; only the latest one's
248
+ // answer may describe the pin on screen.
249
+ const reverse = useAsyncTask<AddressSuggestion | undefined>()
250
+ watch(reverse.pending, (busy) => (resolving.value = busy))
251
+
252
+ function onPick(next: LatLon) {
234
253
  point.value = next
235
- resolving.value = true
236
254
  resolved.value = undefined
237
- try {
238
- resolved.value = await geocoder.value.reverse(next.lat, next.lon, {
239
- language: props.language,
240
- })
241
- } catch {
242
- resolved.value = undefined
243
- } finally {
244
- resolving.value = false
245
- }
255
+ reverse.run(
256
+ () => geocoder.value.reverse(next.lat, next.lon, { language: props.language }),
257
+ {
258
+ apply: (found) => (resolved.value = found),
259
+ fail: () => (resolved.value = undefined),
260
+ },
261
+ )
246
262
  }
247
263
 
248
264
  /** Confirm the pin: fill in whatever came back and always keep the point. */
@@ -288,7 +304,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
288
304
  :empty-text="text.noSuggestions"
289
305
  :loading-text="text.searching"
290
306
  icon="lucide:map-pin"
291
- @update:model-value="(next: string) => { patch({ line: next }); lineSuggestions.query(next) }"
307
+ @update:model-value="(next: string) => { edit({ line: next }); lineSuggestions.query(next) }"
292
308
  @select="onLineSelect"
293
309
  />
294
310
  <LpButton
@@ -330,7 +346,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
330
346
  :placeholder="text.cityPlaceholder"
331
347
  :empty-text="text.noSuggestions"
332
348
  :loading-text="text.searching"
333
- @update:model-value="(next: string) => { patch({ city: next }); citySuggestions.query(next) }"
349
+ @update:model-value="(next: string) => { edit({ city: next }); citySuggestions.query(next) }"
334
350
  @select="onCitySelect"
335
351
  />
336
352
  </LpFormField>
@@ -342,7 +358,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
342
358
  :disabled="disabled"
343
359
  :size="size"
344
360
  autocomplete="postal-code"
345
- @update:model-value="(next: string) => patch({ postalCode: next })"
361
+ @update:model-value="(next: string) => edit({ postalCode: next })"
346
362
  />
347
363
  </div>
348
364
 
@@ -355,7 +371,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
355
371
  :disabled="disabled"
356
372
  :size="size"
357
373
  searchable
358
- @update:model-value="(next) => patch({ country: String(next ?? '') })"
374
+ @update:model-value="(next) => edit({ country: String(next ?? '') })"
359
375
  />
360
376
  </LpFormField>
361
377