@leavepulse/ui 0.43.4 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/README.md +75 -7
  2. package/dist/assets/{LpCommandPalette-B-0Est4Z.css → LpCommandPalette-DCP5g2ik.css} +21 -21
  3. package/dist/assets/{LpDropzone-BBoWvi-J.css → LpDropzone-i4BdMi1H.css} +12 -12
  4. package/dist/assets/{LpImageEditor-Ct7Y3aNu.css → LpImageEditor-CbMA82g8.css} +6 -6
  5. package/dist/assets/{LpShortcutsDialog-C6YoQ8_M.css → LpShortcutsDialog-ChUyU7oV.css} +7 -7
  6. package/dist/assets/{LpSkeleton-D7xU8IPV.css → LpSkeleton-H3Z5dEIk.css} +4 -1
  7. package/dist/assets/{LpTable-BgPLJOXa.css → LpTable-DDQNdf86.css} +9 -9
  8. package/dist/chunks/ContextMenuItems-CrfeNPWT.js +123 -0
  9. package/dist/chunks/{LayoutCanvas-Dd20boUG.js → LayoutCanvas-D-jPOAvg.js} +26 -9
  10. package/dist/chunks/{LayoutNode-DGpZA9LX.js → LayoutNode-D8Xy9N-o.js} +20 -8
  11. package/dist/chunks/{LpActionBar-BlvtGB2P.js → LpActionBar-BbwCF6l5.js} +24 -10
  12. package/dist/chunks/{LpAddressInput-B-Zvxsn7.js → LpAddressInput-BrOqkZqg.js} +53 -48
  13. package/dist/chunks/{LpAlert-BAYHDSS1.js → LpAlert-DkOIjsbq.js} +15 -8
  14. package/dist/chunks/{LpAppShell-BGQVIY-3.js → LpAppShell-BUKPcHco.js} +51 -24
  15. package/dist/chunks/{LpAutocomplete-DExlIZe_.js → LpAutocomplete-DxV3Y9h4.js} +50 -23
  16. package/dist/chunks/{LpAvatar-B3-xAOH3.js → LpAvatar-ClBbEYZN.js} +17 -8
  17. package/dist/chunks/LpBreadcrumbs-B8IJgFIc.js +95 -0
  18. package/dist/chunks/{LpButton-5VXXCjwF.js → LpButton-kqJBHIIb.js} +44 -14
  19. package/dist/chunks/{LpCard-BhI1N_vL.js → LpCard-CpRByR0q.js} +1 -1
  20. package/dist/chunks/{LpCheckbox-CVccKYaH.js → LpCheckbox-DpqchzH4.js} +7 -3
  21. package/dist/chunks/{LpCodeBlock-Bo0B4TFR.js → LpCodeBlock-DeHJPq7p.js} +48 -16
  22. package/dist/chunks/{LpColorPicker-BlXea7LM.js → LpColorPicker-Bhf1qxhB.js} +32 -9
  23. package/dist/chunks/{LpConfirmDialog-Q-bxff8z.js → LpConfirmDialog-CIUOSnWo.js} +58 -16
  24. package/dist/chunks/LpConfirmHost-CDm0Wtpv.js +49 -0
  25. package/dist/chunks/LpContextMenu-DQw5dGfG.js +68 -0
  26. package/dist/chunks/LpDatePicker-Drirx1AK.js +191 -0
  27. package/dist/chunks/{LpDrawer-BZ6k3hvo.js → LpDrawer-bScx-KMO.js} +63 -26
  28. package/dist/chunks/{LpFileTree-_5vFBgaK.js → LpFileTree-CSI_5SK2.js} +23 -8
  29. package/dist/chunks/{LpFileTreeNode-BdcIYKIE.js → LpFileTreeNode-B4MiSQX2.js} +2 -2
  30. package/dist/chunks/LpFormField-CIk5qosh.js +87 -0
  31. package/dist/chunks/{LpInput-9EXWUM9B.js → LpInput-C0e8y8_d.js} +26 -11
  32. package/dist/chunks/{LpLightbox-2sYVMY-S.js → LpLightbox-Mr0W_6HK.js} +147 -60
  33. package/dist/chunks/{LpLink-DCND-zyu.js → LpLink-Krkjhe_y.js} +15 -6
  34. package/dist/chunks/{LpLogViewer-CaRy5MeS.js → LpLogViewer-YBOaFnc_.js} +68 -34
  35. package/dist/chunks/{LpMapPicker-ul4oYno6.js → LpMapPicker-NUzS9e37.js} +38 -13
  36. package/dist/chunks/{LpMediaGallery-FF-NquIo.js → LpMediaGallery-DfJmMW0I.js} +6 -3
  37. package/dist/chunks/{LpModal-Bt0pQCI_.js → LpModal-DPwGKBPv.js} +49 -26
  38. package/dist/chunks/{LpModalNav-YaifHrXC.js → LpModalNav-DPg5H9f3.js} +9 -6
  39. package/dist/chunks/{LpNotificationBell-DyGGsMAM.js → LpNotificationBell-t5yn-siX.js} +58 -24
  40. package/dist/chunks/{LpNumberField-CWuX08iX.js → LpNumberField-DWnQKktS.js} +20 -7
  41. package/dist/chunks/{LpNumberFlow-amWYaeuA.js → LpNumberFlow-z4WPZcZu.js} +22 -12
  42. package/dist/chunks/{LpOtpInput-y2v-pmiN.js → LpOtpInput-CziPF3Oq.js} +37 -8
  43. package/dist/chunks/{LpPagination-5FxjmmB5.js → LpPagination-DFngX7Xo.js} +30 -19
  44. package/dist/chunks/{LpPasswordInput--sjYCkLS.js → LpPasswordInput-CWVDuBJ5.js} +10 -7
  45. package/dist/chunks/{LpPhoneInput-BDSKVl_z.js → LpPhoneInput-CD-ZTeSd.js} +57 -33
  46. package/dist/chunks/{LpPie-D6OnzwlJ.js → LpPie-BLco31Nx.js} +78 -21
  47. package/dist/chunks/{LpPopover-8268UmlC.js → LpPopover-p_e_shN3.js} +4 -2
  48. package/dist/chunks/{LpProgress-Btq5W4Ut.js → LpProgress-CBU1iI69.js} +5 -4
  49. package/dist/chunks/{LpRadioGroup-xPIuVDO_.js → LpRadioGroup-DmEW0l78.js} +9 -2
  50. package/dist/chunks/{LpSegmented-fV0Sk92p.js → LpSegmented-Dejd7cIx.js} +7 -3
  51. package/dist/chunks/{LpSelect-ANIVtrpM.js → LpSelect-DrRD9I7L.js} +40 -18
  52. package/dist/chunks/{LpSidebar-ClBUwoEz.js → LpSidebar-BgwUNWpJ.js} +30 -22
  53. package/dist/chunks/{LpSidebarNav-llOcs_Fn.js → LpSidebarNav-Bqxz8aWT.js} +14 -9
  54. package/dist/chunks/{LpSkeleton-CTI-vBgW.js → LpSkeleton-BB6XIV-w.js} +1 -1
  55. package/dist/chunks/{LpSlider-BwNIBkAK.js → LpSlider-Cuzmrd7p.js} +7 -2
  56. package/dist/chunks/{LpSparkline-C6xQ2dwv.js → LpSparkline-CB80N8SJ.js} +11 -4
  57. package/dist/chunks/{LpStat-CQaW8R--.js → LpStat-dXtyrdX9.js} +1 -1
  58. package/dist/chunks/{LpSwitch-Blizvqyt.js → LpSwitch-B1aLKulj.js} +40 -26
  59. package/dist/chunks/{LpTableOfContents-CjvxPMpU.js → LpTableOfContents-BUcAfGPQ.js} +36 -41
  60. package/dist/chunks/{LpTabs-Cefe_lys.js → LpTabs-_qMVaZJ6.js} +19 -5
  61. package/dist/chunks/LpTextarea-yP8hGedR.js +77 -0
  62. package/dist/chunks/LpThemeProvider-Wjm7Tuit.js +35 -0
  63. package/dist/chunks/LpThemeSwitcher-CKLc10KZ.js +169 -0
  64. package/dist/chunks/{LpTilt-DuyKbpkA.js → LpTilt-B7KHX1zH.js} +2 -2
  65. package/dist/chunks/{LpTimeField-K6nAGboX.js → LpTimeField-C5HRgS5H.js} +13 -7
  66. package/dist/chunks/{LpToaster-gDB1EaU3.js → LpToaster--oT6lfhL.js} +57 -28
  67. package/dist/chunks/{LpTooltip-hCLPAqMG.js → LpTooltip-CBdzeKNf.js} +13 -6
  68. package/dist/chunks/LpTooltipProvider-CftjV7Xc.js +25 -0
  69. package/dist/chunks/{LpTopologyCanvas-Cm1L39uL.js → LpTopologyCanvas-DocsLal3.js} +6 -1
  70. package/dist/chunks/{LpUptimeBar-DvAMno0S.js → LpUptimeBar-DLtxOMRy.js} +43 -14
  71. package/dist/chunks/{LpThemeSwitcher-Cue-myST.js → useTheme-C8JHUlPM.js} +173 -215
  72. package/dist/component-names.d.ts +1 -1
  73. package/dist/components/ContextMenuItems.vue.d.ts +8 -0
  74. package/dist/components/ContextMenuItems.vue.js +4 -0
  75. package/dist/components/LayoutCanvas.vue.js +1 -1
  76. package/dist/components/LayoutNode.vue.d.ts +4 -0
  77. package/dist/components/LayoutNode.vue.js +1 -1
  78. package/dist/components/LpActionBar.vue.d.ts +3 -0
  79. package/dist/components/LpActionBar.vue.js +1 -1
  80. package/dist/components/LpAddressInput.vue.d.ts +1 -1
  81. package/dist/components/LpAddressInput.vue.js +1 -1
  82. package/dist/components/LpAlert.vue.d.ts +9 -0
  83. package/dist/components/LpAlert.vue.js +1 -1
  84. package/dist/components/LpAppShell.vue.d.ts +10 -0
  85. package/dist/components/LpAppShell.vue.js +1 -1
  86. package/dist/components/LpAutocomplete.vue.d.ts +31 -6
  87. package/dist/components/LpAutocomplete.vue.js +1 -1
  88. package/dist/components/LpAvatar.vue.d.ts +3 -3
  89. package/dist/components/LpAvatar.vue.js +1 -1
  90. package/dist/components/LpBadge.vue.d.ts +2 -2
  91. package/dist/components/LpBreadcrumbs.vue.d.ts +13 -9
  92. package/dist/components/LpBreadcrumbs.vue.js +1 -1
  93. package/dist/components/LpButton.vue.d.ts +35 -98
  94. package/dist/components/LpButton.vue.js +1 -1
  95. package/dist/components/LpCard.vue.js +1 -1
  96. package/dist/components/LpChart.vue.d.ts +31 -2
  97. package/dist/components/LpChart.vue.js +317 -95
  98. package/dist/components/LpCheckbox.vue.js +1 -1
  99. package/dist/components/LpCodeBlock.vue.d.ts +2 -0
  100. package/dist/components/LpCodeBlock.vue.js +1 -1
  101. package/dist/components/LpColorPicker.vue.js +1 -1
  102. package/dist/components/LpCommandPalette.vue.d.ts +8 -3
  103. package/dist/components/LpCommandPalette.vue.js +86 -72
  104. package/dist/components/LpConfirmDialog.vue.d.ts +15 -3
  105. package/dist/components/LpConfirmDialog.vue.js +1 -1
  106. package/dist/components/LpConfirmHost.vue.d.ts +9 -0
  107. package/dist/components/LpConfirmHost.vue.js +4 -0
  108. package/dist/components/LpContextMenu.vue.d.ts +3 -5
  109. package/dist/components/LpContextMenu.vue.js +1 -1
  110. package/dist/components/LpDatePicker.vue.d.ts +24 -6
  111. package/dist/components/LpDatePicker.vue.js +1 -1
  112. package/dist/components/LpDrawer.vue.d.ts +10 -0
  113. package/dist/components/LpDrawer.vue.js +1 -1
  114. package/dist/components/LpDropzone.vue.js +22 -6
  115. package/dist/components/LpFileTree.vue.d.ts +11 -2
  116. package/dist/components/LpFileTree.vue.js +1 -1
  117. package/dist/components/LpFileTreeNode.vue.js +1 -1
  118. package/dist/components/LpFormField.vue.d.ts +9 -0
  119. package/dist/components/LpFormField.vue.js +1 -1
  120. package/dist/components/LpImageEditor.vue.js +92 -57
  121. package/dist/components/LpInput.vue.d.ts +3 -3
  122. package/dist/components/LpInput.vue.js +1 -1
  123. package/dist/components/LpLightbox.vue.d.ts +17 -0
  124. package/dist/components/LpLightbox.vue.js +1 -1
  125. package/dist/components/LpLink.vue.d.ts +11 -11
  126. package/dist/components/LpLink.vue.js +1 -1
  127. package/dist/components/LpLogViewer.vue.d.ts +20 -2
  128. package/dist/components/LpLogViewer.vue.js +1 -1
  129. package/dist/components/LpMapPicker.vue.d.ts +4 -4
  130. package/dist/components/LpMapPicker.vue.js +1 -1
  131. package/dist/components/LpMediaGallery.vue.js +1 -1
  132. package/dist/components/LpModal.vue.d.ts +16 -6
  133. package/dist/components/LpModal.vue.js +1 -1
  134. package/dist/components/LpModalNav.vue.d.ts +0 -1
  135. package/dist/components/LpModalNav.vue.js +1 -1
  136. package/dist/components/LpNotificationBell.vue.d.ts +11 -2
  137. package/dist/components/LpNotificationBell.vue.js +1 -1
  138. package/dist/components/LpNumberField.vue.d.ts +2 -0
  139. package/dist/components/LpNumberField.vue.js +1 -1
  140. package/dist/components/LpNumberFlow.vue.js +1 -1
  141. package/dist/components/LpOtpInput.vue.js +1 -1
  142. package/dist/components/LpPagination.vue.d.ts +2 -0
  143. package/dist/components/LpPagination.vue.js +1 -1
  144. package/dist/components/LpPasswordInput.vue.js +1 -1
  145. package/dist/components/LpPhoneInput.vue.js +1 -1
  146. package/dist/components/LpPie.vue.d.ts +21 -2
  147. package/dist/components/LpPie.vue.js +1 -1
  148. package/dist/components/LpPopover.vue.d.ts +2 -0
  149. package/dist/components/LpPopover.vue.js +1 -1
  150. package/dist/components/LpProgress.vue.js +1 -1
  151. package/dist/components/LpRadioGroup.vue.js +1 -1
  152. package/dist/components/LpSection.vue.d.ts +1 -1
  153. package/dist/components/LpSegmented.vue.d.ts +5 -0
  154. package/dist/components/LpSegmented.vue.js +1 -1
  155. package/dist/components/LpSelect.vue.d.ts +1 -0
  156. package/dist/components/LpSelect.vue.js +1 -1
  157. package/dist/components/LpShortcutsDialog.vue.d.ts +1 -1
  158. package/dist/components/LpShortcutsDialog.vue.js +11 -8
  159. package/dist/components/LpSidebar.vue.d.ts +6 -2
  160. package/dist/components/LpSidebar.vue.js +1 -1
  161. package/dist/components/LpSidebarNav.vue.js +1 -1
  162. package/dist/components/LpSkeleton.vue.js +1 -1
  163. package/dist/components/LpSlider.vue.js +1 -1
  164. package/dist/components/LpSparkline.vue.d.ts +1 -0
  165. package/dist/components/LpSparkline.vue.js +1 -1
  166. package/dist/components/LpStat.vue.js +1 -1
  167. package/dist/components/LpSwitch.vue.js +1 -1
  168. package/dist/components/LpTable.vue.d.ts +23 -1
  169. package/dist/components/LpTable.vue.js +74 -36
  170. package/dist/components/LpTableOfContents.vue.d.ts +4 -2
  171. package/dist/components/LpTableOfContents.vue.js +1 -1
  172. package/dist/components/LpTabs.vue.d.ts +2 -0
  173. package/dist/components/LpTabs.vue.js +1 -1
  174. package/dist/components/LpTextarea.vue.d.ts +19 -6
  175. package/dist/components/LpTextarea.vue.js +1 -1
  176. package/dist/components/LpThemeProvider.vue.d.ts +22 -0
  177. package/dist/components/LpThemeProvider.vue.js +4 -0
  178. package/dist/components/LpThemeSwitcher.vue.d.ts +13 -0
  179. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  180. package/dist/components/LpTilt.vue.js +1 -1
  181. package/dist/components/LpTimeField.vue.js +1 -1
  182. package/dist/components/LpToaster.vue.d.ts +6 -1
  183. package/dist/components/LpToaster.vue.js +1 -1
  184. package/dist/components/LpTooltip.vue.d.ts +1 -2
  185. package/dist/components/LpTooltip.vue.js +1 -1
  186. package/dist/components/LpTooltipProvider.vue.d.ts +22 -0
  187. package/dist/components/LpTooltipProvider.vue.js +4 -0
  188. package/dist/components/LpTopologyCanvas.vue.js +1 -1
  189. package/dist/components/LpUptimeBar.vue.d.ts +4 -0
  190. package/dist/components/LpUptimeBar.vue.js +1 -1
  191. package/dist/components/chart.d.ts +32 -1
  192. package/dist/components/chart.js +39 -4
  193. package/dist/components/codeHighlight.d.ts +6 -0
  194. package/dist/components/codeHighlight.js +92 -20
  195. package/dist/components/color.d.ts +1 -1
  196. package/dist/components/color.js +20 -9
  197. package/dist/components/confirm.d.ts +25 -0
  198. package/dist/components/confirm.js +26 -0
  199. package/dist/components/countries.d.ts +6 -0
  200. package/dist/components/countries.js +7 -0
  201. package/dist/components/fileTree.js +16 -7
  202. package/dist/components/imageCrop.d.ts +42 -0
  203. package/dist/components/imageCrop.js +47 -0
  204. package/dist/components/link.d.ts +12 -0
  205. package/dist/components/link.js +19 -0
  206. package/dist/components/modalDepth.d.ts +18 -2
  207. package/dist/components/modalDepth.js +17 -5
  208. package/dist/components/otp.d.ts +5 -0
  209. package/dist/components/otp.js +10 -0
  210. package/dist/components/sidebar.d.ts +7 -1
  211. package/dist/components/tableSort.d.ts +8 -0
  212. package/dist/components/tableSort.js +31 -0
  213. package/dist/composables/useField.d.ts +34 -0
  214. package/dist/composables/useField.js +45 -0
  215. package/dist/composables/useFormDraft.d.ts +10 -3
  216. package/dist/composables/useFormDraft.js +9 -14
  217. package/dist/composables/useHotkeys.d.ts +17 -1
  218. package/dist/composables/useHotkeys.js +55 -18
  219. package/dist/composables/useInputFilter.d.ts +17 -0
  220. package/dist/composables/useInputFilter.js +47 -10
  221. package/dist/composables/useLpConfig.d.ts +24 -0
  222. package/dist/composables/useLpConfig.js +44 -0
  223. package/dist/composables/useRovingFocus.js +7 -0
  224. package/dist/composables/useTilt.d.ts +2 -2
  225. package/dist/composables/useTilt.js +5 -6
  226. package/dist/composables/useToast.d.ts +20 -0
  227. package/dist/composables/useToast.js +28 -4
  228. package/dist/composables/useZoomPan.d.ts +3 -3
  229. package/dist/composables/useZoomPan.js +4 -4
  230. package/dist/index.d.ts +32 -2
  231. package/dist/index.js +184 -137
  232. package/dist/layout/tree.d.ts +2 -0
  233. package/dist/layout/tree.js +16 -1
  234. package/dist/layout/useLayout.d.ts +15 -0
  235. package/dist/layout/useLayout.js +41 -14
  236. package/dist/theme/useTheme.d.ts +32 -9
  237. package/package.json +8 -7
  238. package/src/component-names.ts +5 -7
  239. package/src/components/ContextMenuItems.vue +88 -0
  240. package/src/components/LayoutCanvas.vue +26 -6
  241. package/src/components/LayoutNode.vue +19 -2
  242. package/src/components/LpActionBar.vue +30 -7
  243. package/src/components/LpAddressInput.vue +57 -41
  244. package/src/components/LpAlert.vue +16 -3
  245. package/src/components/LpAppShell.vue +45 -12
  246. package/src/components/LpAutocomplete.vue +46 -10
  247. package/src/components/LpAvatar.vue +19 -5
  248. package/src/components/LpBreadcrumbs.vue +65 -41
  249. package/src/components/LpButton.vue +59 -13
  250. package/src/components/LpChart.vue +272 -28
  251. package/src/components/LpCheckbox.vue +7 -3
  252. package/src/components/LpCodeBlock.vue +50 -14
  253. package/src/components/LpColorPicker.vue +39 -9
  254. package/src/components/LpCommandPalette.vue +34 -13
  255. package/src/components/LpConfirmDialog.vue +68 -17
  256. package/src/components/LpConfirmHost.vue +59 -0
  257. package/src/components/LpContextMenu.vue +14 -115
  258. package/src/components/LpDatePicker.vue +99 -68
  259. package/src/components/LpDrawer.vue +56 -19
  260. package/src/components/LpDropzone.vue +21 -4
  261. package/src/components/LpFileTree.vue +33 -7
  262. package/src/components/LpFormField.vue +30 -7
  263. package/src/components/LpImageEditor.vue +96 -57
  264. package/src/components/LpInput.vue +18 -7
  265. package/src/components/LpLightbox.vue +140 -29
  266. package/src/components/LpLink.vue +24 -13
  267. package/src/components/LpLogViewer.vue +86 -17
  268. package/src/components/LpMapPicker.vue +36 -13
  269. package/src/components/LpMediaGallery.vue +8 -2
  270. package/src/components/LpModal.vue +53 -27
  271. package/src/components/LpModalNav.vue +5 -2
  272. package/src/components/LpNotificationBell.vue +63 -17
  273. package/src/components/LpNumberField.vue +21 -6
  274. package/src/components/LpNumberFlow.vue +26 -10
  275. package/src/components/LpOtpInput.vue +43 -8
  276. package/src/components/LpPagination.vue +23 -7
  277. package/src/components/LpPasswordInput.vue +4 -1
  278. package/src/components/LpPhoneInput.vue +53 -27
  279. package/src/components/LpPie.vue +94 -15
  280. package/src/components/LpPopover.vue +3 -0
  281. package/src/components/LpProgress.vue +5 -3
  282. package/src/components/LpRadioGroup.vue +9 -2
  283. package/src/components/LpSegmented.vue +12 -2
  284. package/src/components/LpSelect.vue +0 -0
  285. package/src/components/LpShortcutsDialog.vue +8 -3
  286. package/src/components/LpSidebar.vue +14 -9
  287. package/src/components/LpSidebarNav.vue +17 -5
  288. package/src/components/LpSkeleton.vue +4 -1
  289. package/src/components/LpSlider.vue +7 -1
  290. package/src/components/LpSparkline.vue +11 -3
  291. package/src/components/LpSwitch.vue +30 -7
  292. package/src/components/LpTable.vue +91 -32
  293. package/src/components/LpTableOfContents.vue +50 -32
  294. package/src/components/LpTabs.vue +23 -6
  295. package/src/components/LpTextarea.vue +44 -6
  296. package/src/components/LpThemeProvider.vue +41 -0
  297. package/src/components/LpThemeSwitcher.vue +64 -17
  298. package/src/components/LpTilt.vue +2 -2
  299. package/src/components/LpTimeField.vue +14 -3
  300. package/src/components/LpToaster.vue +66 -26
  301. package/src/components/LpTooltip.vue +22 -5
  302. package/src/components/LpTooltipProvider.vue +24 -0
  303. package/src/components/LpTopologyCanvas.vue +12 -1
  304. package/src/components/LpUptimeBar.vue +46 -8
  305. package/src/components/chart.ts +77 -5
  306. package/src/components/codeHighlight.ts +111 -15
  307. package/src/components/color.ts +30 -9
  308. package/src/components/confirm.ts +58 -0
  309. package/src/components/countries.ts +12 -0
  310. package/src/components/fileTree.ts +19 -7
  311. package/src/components/imageCrop.ts +107 -0
  312. package/src/components/link.ts +34 -0
  313. package/src/components/modalDepth.ts +48 -6
  314. package/src/components/otp.ts +16 -0
  315. package/src/components/sidebar.ts +7 -1
  316. package/src/components/tableSort.ts +37 -0
  317. package/src/composables/useField.ts +79 -0
  318. package/src/composables/useFormDraft.ts +25 -20
  319. package/src/composables/useHotkeys.ts +89 -18
  320. package/src/composables/useInputFilter.ts +77 -14
  321. package/src/composables/useLpConfig.ts +87 -0
  322. package/src/composables/useRovingFocus.ts +11 -0
  323. package/src/composables/useTilt.ts +5 -6
  324. package/src/composables/useToast.ts +56 -4
  325. package/src/composables/useZoomPan.ts +6 -6
  326. package/src/index.ts +37 -2
  327. package/src/layout/tree.ts +30 -0
  328. package/src/layout/useLayout.ts +68 -18
  329. package/src/nuxt.ts +59 -17
  330. package/src/runtime/config.plugin.ts +14 -0
  331. package/src/runtime/theme.plugin.ts +42 -22
  332. package/src/theme/useTheme.ts +206 -100
  333. package/dist/chunks/LpBreadcrumbs-p4jHe_V-.js +0 -62
  334. package/dist/chunks/LpContextMenu-CwW1QXe_.js +0 -217
  335. package/dist/chunks/LpDatePicker-CwOToxGq.js +0 -164
  336. package/dist/chunks/LpFormField-CazBc2-a.js +0 -62
  337. package/dist/chunks/LpTextarea-t7F6gv8E.js +0 -47
@@ -8,9 +8,10 @@ export default { inheritAttrs: false }
8
8
  <script setup lang="ts">
9
9
  import { CheckboxIndicator, CheckboxRoot } from "reka-ui"
10
10
  import LpIcon from "./LpIcon.vue"
11
+ import { useFieldControl } from "../composables/useField"
11
12
  import { useMergedAttrs } from "../composables/useMergedClass"
12
13
 
13
- defineProps<{
14
+ const props = defineProps<{
14
15
  modelValue?: boolean
15
16
  /** Mixed state (e.g. a "select all" with a partial selection). Shows a dash. */
16
17
  indeterminate?: boolean
@@ -30,6 +31,8 @@ function onUpdate(v: boolean | "indeterminate") {
30
31
  emit("change", checked)
31
32
  }
32
33
 
34
+ const field = useFieldControl(() => ({ wrapped: false, id: props.id, disabled: props.disabled }))
35
+
33
36
  // The label sits a fixed distance from the box; a dense list of options needs that closer.
34
37
  const { class: rootClass, attrs: rest } = useMergedAttrs(
35
38
  "group inline-flex cursor-pointer items-center gap-2 text-sm text-ink",
@@ -41,10 +44,11 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
41
44
  :class="rootClass"
42
45
  v-bind="rest">
43
46
  <CheckboxRoot
44
- :id="id"
47
+ :id="field.id"
45
48
  :name="name"
46
49
  :model-value="indeterminate ? 'indeterminate' : modelValue"
47
- :disabled="disabled"
50
+ :disabled="field.disabled"
51
+ :aria-describedby="field.describedBy"
48
52
  class="flex size-[18px] items-center justify-center rounded-[5px] border border-line-strong bg-surface-soft outline-none transition-[color,background-color,border-color,box-shadow,scale] duration-[var(--duration-fast)] ease-[var(--ease-emphasized)] not-data-[state=checked]:not-data-[state=indeterminate]:group-hover:border-brand active:scale-90 motion-reduce:active:scale-100 focus-visible:ring-2 focus-visible:ring-ring data-[state=checked]:border-transparent data-[state=checked]:bg-brand data-[state=indeterminate]:border-transparent data-[state=indeterminate]:bg-brand disabled:cursor-not-allowed disabled:opacity-55"
49
53
  @update:model-value="onUpdate"
50
54
  >
@@ -24,7 +24,8 @@ export default { inheritAttrs: false }
24
24
  */
25
25
  import { computed, reactive, ref, watch } from "vue"
26
26
  import { useClipboard } from "../composables/useClipboard"
27
- import { type CodeLang, tokenizeLine } from "./codeHighlight"
27
+ import { type CodeLang, tokenizeLines } from "./codeHighlight"
28
+ import { useLabels } from "../composables/useLpConfig"
28
29
  import LpIcon from "./LpIcon.vue"
29
30
  import { useMergedAttrs } from "../composables/useMergedClass"
30
31
 
@@ -49,6 +50,7 @@ const props = withDefaults(
49
50
  height?: string
50
51
  /** Auto-collapse a leading import/package block (no markers needed). */
51
52
  foldImports?: boolean
53
+ labels?: Partial<Record<"unlock" | "lock" | "copy" | "copied" | "imports" | "details" | "lines", string>>
52
54
  }>(),
53
55
  {
54
56
  lang: "plain",
@@ -66,6 +68,21 @@ const emit = defineEmits<{
66
68
  (e: "update:locked", value: boolean): void
67
69
  }>()
68
70
 
71
+ const text = useLabels(
72
+ "codeBlock",
73
+ {
74
+ unlock: "Unlock to edit",
75
+ lock: "Lock (read-only)",
76
+ copy: "Copy code",
77
+ copied: "Copied",
78
+ imports: "imports",
79
+ details: "details",
80
+ /** `{n}` is replaced by the count. */
81
+ lines: "({n} lines)",
82
+ },
83
+ () => props.labels,
84
+ )
85
+
69
86
  const code = computed(() => props.modelValue ?? "")
70
87
  const rawLines = computed(() => code.value.replace(/\n$/, "").split("\n"))
71
88
 
@@ -114,7 +131,7 @@ const segments = computed<Segment[]>(() => {
114
131
  flushPlain()
115
132
  const lines: NumberedLine[] = []
116
133
  for (let k = start; k < realEnd; k++) lines.push({ n: k + 1, text: src[k] })
117
- out.push({ kind: "fold", id: foldId++, label: "imports", defaultOpen: false, lines })
134
+ out.push({ kind: "fold", id: foldId++, label: text.value.imports, defaultOpen: false, lines })
118
135
  i = realEnd
119
136
  }
120
137
  }
@@ -126,7 +143,7 @@ const segments = computed<Segment[]>(() => {
126
143
  flushPlain()
127
144
  const rest = startMatch[1] ?? ""
128
145
  const defaultOpen = /\bopen\b/.test(rest)
129
- const label = rest.replace(/\bopen\b/, "").trim() || "details"
146
+ const label = rest.replace(/\bopen\b/, "").trim() || text.value.details
130
147
  const lines: NumberedLine[] = []
131
148
  i++
132
149
  for (; i < src.length; i++) {
@@ -143,6 +160,7 @@ const segments = computed<Segment[]>(() => {
143
160
  })
144
161
 
145
162
  const hasFolds = computed(() => segments.value.some((s) => s.kind === "fold"))
163
+ const editing = computed(() => !props.locked && !hasFolds.value)
146
164
 
147
165
  // Per-region open state, keyed by fold id, defaulting to each region's declared
148
166
  // open flag. Reset when the set of folds changes.
@@ -159,9 +177,21 @@ function toggleFold(id: number) {
159
177
  openState[id] = !openState[id]
160
178
  }
161
179
 
162
- /** Highlight one line's text into tokens. */
163
- function tokens(text: string) {
164
- return tokenizeLine(text, props.lang)
180
+ /* Highlighted once for the whole snippet, so a block comment that spans lines
181
+ is coloured as one; looked up by the line's 1-based number. */
182
+ const highlighted = computed(() => tokenizeLines(rawLines.value, props.lang))
183
+ function tokens(n: number) {
184
+ return highlighted.value[n - 1] ?? []
185
+ }
186
+
187
+ /* The textarea is the scroller while editing; the highlighted copy under it
188
+ follows, or the caret drifts over the wrong text on a long line. */
189
+ const preEl = ref<HTMLElement | null>(null)
190
+ function syncScroll(e: Event) {
191
+ const area = e.target as HTMLTextAreaElement
192
+ if (!preEl.value) return
193
+ preEl.value.scrollLeft = area.scrollLeft
194
+ preEl.value.scrollTop = area.scrollTop
165
195
  }
166
196
 
167
197
  // Copied text is the real code with marker lines removed (so it pastes clean),
@@ -214,7 +244,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
214
244
  v-if="lockToggle"
215
245
  type="button"
216
246
  class="group/lock flex items-center gap-1 rounded px-1.5 py-1 outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
217
- :aria-label="locked ? 'Unlock to edit' : 'Lock (read-only)'"
247
+ :aria-label="locked ? text.unlock : text.lock"
218
248
  @click="toggleLock"
219
249
  >
220
250
  <LpIcon
@@ -227,7 +257,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
227
257
  v-if="copyable"
228
258
  type="button"
229
259
  class="group/copy flex items-center gap-1 rounded px-1.5 py-1 outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
230
- :aria-label="copied ? 'Copied' : 'Copy code'"
260
+ :aria-label="copied ? text.copied : text.copy"
231
261
  @click="copy"
232
262
  >
233
263
  <LpIcon
@@ -245,7 +275,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
245
275
  <div
246
276
  class="lp-scrollbar-none relative"
247
277
  :style="height ? { height } : undefined"
248
- :class="height ? 'overflow-auto' : ''"
278
+ :class="height ? (editing ? 'overflow-hidden' : 'overflow-auto') : ''"
249
279
  >
250
280
  <!-- Editable overlay: only when unlocked and there are no folds. -->
251
281
  <textarea
@@ -253,12 +283,18 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
253
283
  :value="code"
254
284
  spellcheck="false"
255
285
  class="lp-scrollbar-none absolute inset-0 size-full resize-none overflow-auto whitespace-pre bg-transparent p-3 leading-relaxed text-transparent caret-brand outline-none"
256
- :class="[wrap ? 'whitespace-pre-wrap break-words' : 'whitespace-pre', lineNumbers ? 'pl-12' : '']"
286
+ :class="[wrap ? 'whitespace-pre-wrap break-words' : 'whitespace-pre', lineNumbers ? 'pl-13' : '']"
257
287
  @input="onInput"
288
+ @scroll="syncScroll"
258
289
  />
259
290
  <pre
291
+ ref="preEl"
260
292
  class="lp-scrollbar-none m-0 overflow-auto p-3 leading-relaxed"
261
- :class="[wrap ? 'whitespace-pre-wrap break-words' : 'whitespace-pre', !locked && !hasFolds ? 'pointer-events-none' : '']"
293
+ :class="[
294
+ wrap ? 'whitespace-pre-wrap break-words' : 'whitespace-pre',
295
+ editing ? 'pointer-events-none' : '',
296
+ editing && height ? 'h-full overflow-hidden' : '',
297
+ ]"
262
298
  ><code class="block"><template
263
299
  v-for="(seg, s) in segments"
264
300
  :key="s"
@@ -271,7 +307,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
271
307
  class="mr-3 inline-block w-7 select-none text-right text-muted/60"
272
308
  aria-hidden="true"
273
309
  >{{ ln.n }}</span><span
274
- v-for="(tok, j) in tokens(ln.text)"
310
+ v-for="(tok, j) in tokens(ln.n)"
275
311
  :key="j"
276
312
  :class="tok.cls"
277
313
  >{{ tok.text }}</span></span></template><!-- Fold region --><template
@@ -288,7 +324,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
288
324
  /><span class="select-none">{{ openState[seg.id] ? seg.label : seg.label + ' …' }}</span><span
289
325
  v-if="!openState[seg.id]"
290
326
  class="select-none text-muted/50"
291
- >({{ seg.lines.length }} lines)</span></button><template
327
+ >{{ text.lines.replace("{n}", String(seg.lines.length)) }}</span></button><template
292
328
  v-if="openState[seg.id]"
293
329
  ><span
294
330
  v-for="ln in seg.lines"
@@ -299,7 +335,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
299
335
  class="mr-3 inline-block w-7 select-none text-right text-muted/60"
300
336
  aria-hidden="true"
301
337
  >{{ ln.n }}</span><span
302
- v-for="(tok, j) in tokens(ln.text)"
338
+ v-for="(tok, j) in tokens(ln.n)"
303
339
  :key="j"
304
340
  :class="tok.cls"
305
341
  >{{ tok.text }}</span></span></template></template></template></code></pre>
@@ -37,6 +37,7 @@ import {
37
37
  PopoverTrigger,
38
38
  } from "reka-ui"
39
39
  import { computed, ref, watch } from "vue"
40
+ import { useFieldControl } from "../composables/useField"
40
41
  import {
41
42
  clampHsv,
42
43
  DEFAULT_SWATCHES,
@@ -271,7 +272,9 @@ function trackPointer(e: PointerEvent, onMove: (x: number, y: number) => void) {
271
272
 
272
273
  const onFieldPointer = (e: PointerEvent) =>
273
274
  trackPointer(e, (x, y) => set({ s: x, v: 1 - y }))
274
- const onHuePointer = (e: PointerEvent) => trackPointer(e, (x) => set({ h: x * 360 }))
275
+ // 360° is 0° again, so the far edge of the rail would throw the thumb back to
276
+ // the start; it stops just short instead, as End does.
277
+ const onHuePointer = (e: PointerEvent) => trackPointer(e, (x) => set({ h: Math.min(x * 360, 359.9) }))
275
278
  const onAlphaPointer = (e: PointerEvent) => trackPointer(e, (x) => set({ a: x }))
276
279
 
277
280
  /* ── keyboard ─────────────────────────────────────
@@ -303,11 +306,13 @@ function step(e: KeyboardEvent, axis: "sv" | "h" | "a") {
303
306
  }
304
307
 
305
308
  /* ── text field ───────────────────────────────────
306
- Typed text is parsed on every keystroke but only applied when it resolves to
307
- a real colour, so a half-written "#3b8" never snaps the picker somewhere the
308
- user is still on their way past. The field shows what is typed while it has
309
- focus and the committed value once it doesn't. */
309
+ Typed text is parsed on every keystroke and previews as soon as it resolves
310
+ to a real colour, but is only committed (`change`) on Enter or when the field
311
+ loses focus: "#3b8" and "#3b82" are valid colours on the way to "#3b82f6",
312
+ and committing each one handed listeners three colours nobody chose. The
313
+ field shows what is typed while it has focus and the value once it doesn't. */
310
314
  const typed = ref<string | null>(null)
315
+ let typedChange = false
311
316
  const inputValue = computed(() => typed.value ?? current.value)
312
317
 
313
318
  function onInput(e: Event) {
@@ -318,11 +323,19 @@ function onInput(e: Event) {
318
323
  const next = rgbToHsv(rgb)
319
324
  if (next.s === 0) next.h = hsv.value.h
320
325
  if (next.v === 0) next.s = hsv.value.s
321
- set(props.alpha ? next : { ...next, a: 1 }, true)
326
+ set(props.alpha ? next : { ...next, a: 1 })
327
+ typedChange = true
328
+ }
329
+
330
+ function commitTyped() {
331
+ if (!typedChange) return
332
+ typedChange = false
333
+ emit("change", current.value)
322
334
  }
323
335
 
324
336
  function onInputBlur() {
325
337
  typed.value = null
338
+ commitTyped()
326
339
  }
327
340
 
328
341
  function pickSwatch(value: string) {
@@ -463,8 +476,12 @@ function remember(value: string) {
463
476
  // Remember on close rather than on every commit: the colour a person leaves the
464
477
  // popover on is the one they chose, while the ones they passed through on the
465
478
  // way are not history, they are the search for it.
479
+ // Only a colour that changed while the popover was open: opening it to look,
480
+ // or to copy the value out, is not a choice worth recording.
481
+ let openedOn = ""
466
482
  watch(open, (isOpen, was) => {
467
- if (was && !isOpen) remember(current.value)
483
+ if (isOpen) openedOn = current.value
484
+ else if (was && current.value !== openedOn) remember(current.value)
468
485
  })
469
486
 
470
487
  /**
@@ -573,6 +590,12 @@ const menuItems = computed<ContextMenuItemDef[]>(() => {
573
590
  : builtin
574
591
  })
575
592
 
593
+ const field = useFieldControl(() => ({
594
+ wrapped: false,
595
+ invalid: props.invalid,
596
+ disabled: props.disabled,
597
+ }))
598
+
576
599
  defineExpose({ open })
577
600
  </script>
578
601
 
@@ -592,12 +615,15 @@ defineExpose({ open })
592
615
  <PopoverTrigger as-child>
593
616
  <button
594
617
  type="button"
595
- :disabled="disabled"
618
+ :id="field.id"
619
+ :disabled="field.disabled"
596
620
  :aria-label="ariaLabel"
621
+ :aria-invalid="field.invalid || undefined"
622
+ :aria-describedby="field.describedBy"
597
623
  class="group inline-flex w-full items-center gap-2 rounded-control border bg-surface-soft px-2 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"
598
624
  :class="[
599
625
  triggerSize[size],
600
- invalid ? 'border-danger focus-visible:ring-danger-soft' : 'border-line focus-visible:border-brand',
626
+ field.invalid ? 'border-danger focus-visible:ring-danger-soft' : 'border-line focus-visible:border-brand',
601
627
  ]"
602
628
  >
603
629
  <!-- The swatch sits on the checkerboard so a transparent colour looks
@@ -645,6 +671,9 @@ defineExpose({ open })
645
671
  role="slider"
646
672
  :tabindex="disabled ? -1 : 0"
647
673
  :aria-label="l.saturation"
674
+ :aria-valuemin="0"
675
+ :aria-valuemax="100"
676
+ :aria-valuenow="Math.round(hsv.s * 100)"
648
677
  :aria-valuetext="fill(l.saturationValueText, { s: Math.round(hsv.s * 100), v: Math.round(hsv.v * 100) })"
649
678
  class="relative h-36 w-full cursor-crosshair touch-none overflow-hidden rounded-control outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
650
679
  :style="fieldStyle"
@@ -728,6 +757,7 @@ defineExpose({ open })
728
757
  :aria-label="l.value"
729
758
  class="min-w-0 flex-1 select-text bg-transparent font-mono text-xs text-ink outline-none placeholder:text-muted"
730
759
  @input="onInput"
760
+ @keydown.enter="commitTyped"
731
761
  @blur="onInputBlur"
732
762
  />
733
763
  </div>
@@ -44,6 +44,7 @@ import { useGlass } from "../composables/useGlass"
44
44
  import { useClipboard } from "../composables/useClipboard"
45
45
  import { useHotkeys } from "../composables/useHotkeys"
46
46
  import { useModalLayer } from "./modalDepth"
47
+ import { useLabels } from "../composables/useLpConfig"
47
48
  import { parseCommandQuery, rankCommandItems } from "./commandSearch"
48
49
 
49
50
  export interface Command {
@@ -87,7 +88,7 @@ export interface Command {
87
88
  * `onSelect` still runs when the mode opens, so a command can both act and
88
89
  * drill in.
89
90
  */
90
- children?: Command[] | ((query: string) => Command[] | Promise<Command[]>)
91
+ children?: Command[] | ((query: string, signal: AbortSignal) => Command[] | Promise<Command[]>)
91
92
  /** Placeholder while inside this command's mode. */
92
93
  childPlaceholder?: string
93
94
  /**
@@ -121,7 +122,7 @@ export interface Command {
121
122
  }
122
123
 
123
124
  /** Footer hints and the mode chip, spelled out so an app can translate them. */
124
- export interface PaletteLabels {
125
+ export type PaletteLabels = {
125
126
  navigate: string
126
127
  select: string
127
128
  close: string
@@ -144,6 +145,11 @@ export interface PaletteLabels {
144
145
  * text is never displayed. Kept so existing `labels` objects still type-check.
145
146
  */
146
147
  noPreview?: string
148
+ /** Accessible name and description of the dialog, read by screen readers only. */
149
+ title?: string
150
+ description?: string
151
+ /** The key hint printed beside the box and in the footer. */
152
+ esc?: string
147
153
  }
148
154
 
149
155
  const props = withDefaults(
@@ -229,9 +235,10 @@ const props = withDefaults(
229
235
  )
230
236
 
231
237
  const frosted = useGlass(() => props.glass)
232
- const { claim, release, layer } = useModalLayer()
238
+ const contentRef = ref<{ $el: HTMLElement } | null>(null)
239
+ const { claim, release, layer } = useModalLayer({ element: () => contentRef.value?.$el })
233
240
 
234
- const DEFAULT_LABELS: PaletteLabels = {
241
+ const DEFAULT_LABELS: Required<PaletteLabels> = {
235
242
  navigate: "navigate",
236
243
  select: "select",
237
244
  close: "close",
@@ -245,9 +252,12 @@ const DEFAULT_LABELS: PaletteLabels = {
245
252
  copyLink: "Copy link",
246
253
  linkCopied: "Link copied",
247
254
  noPreview: "No preview",
255
+ title: "Command palette",
256
+ description: "Search and run a command.",
257
+ esc: "esc",
248
258
  }
249
259
 
250
- const text = computed<PaletteLabels>(() => ({ ...DEFAULT_LABELS, ...props.labels }))
260
+ const text = useLabels("palette", DEFAULT_LABELS, () => props.labels)
251
261
 
252
262
  const emit = defineEmits<{
253
263
  (e: "update:open", value: boolean): void
@@ -330,12 +340,14 @@ const selectedActions = computed(() => actionsOf(flat.value[selectedIndex.value]
330
340
  * actions it keeps that default.
331
341
  */
332
342
  function onTab(event: KeyboardEvent) {
333
- event.preventDefault()
334
- event.stopPropagation()
343
+ // Without actions Tab stays Tab, so the trail chips and a Retry button are
344
+ // still reachable from the keyboard.
335
345
  if (event.shiftKey) return
336
346
  const c = flat.value[selectedIndex.value]
337
347
  const actions = selectedActions.value
338
348
  if (!c || !actions.length) return
349
+ event.preventDefault()
350
+ event.stopPropagation()
339
351
  enter({ id: `${c.id}:actions`, label: c.label, icon: c.icon, children: actions })
340
352
  }
341
353
 
@@ -392,7 +404,7 @@ function fetcher(): ((q: string, signal: AbortSignal) => Command[] | Promise<Com
392
404
  const current = mode.value
393
405
  if (current) {
394
406
  return typeof current.children === "function"
395
- ? (q: string) => (current.children as (q: string) => Command[] | Promise<Command[]>)(q)
407
+ ? current.children
396
408
  : null
397
409
  }
398
410
  return props.search ?? null
@@ -599,8 +611,11 @@ watch(
599
611
  else viewport.scrollTop = 0
600
612
  },
601
613
  )
614
+ // Rows ask for their index several times per render; a lookup keeps that O(1)
615
+ // instead of a scan of the whole list per row.
616
+ const flatIndex = computed(() => new Map(flat.value.map((c, i) => [c, i])))
602
617
  function flatIndexOf(c: Command): number {
603
- return flat.value.indexOf(c)
618
+ return flatIndex.value.get(c) ?? -1
604
619
  }
605
620
 
606
621
  // ── highlight ────────────────────────────────────────────────
@@ -689,6 +704,9 @@ function movePage(direction: -1 | 1) {
689
704
  function onPaletteKeydown(event: KeyboardEvent) {
690
705
  if (event.isComposing) return
691
706
  if (!(["ArrowDown", "ArrowUp", "Home", "End", "PageDown", "PageUp"] as string[]).includes(event.key)) return
707
+ // Home and End belong to the caret while there is text to move it through.
708
+ const typing = event.target instanceof HTMLInputElement && event.target.value !== ""
709
+ if (typing && (event.key === "Home" || event.key === "End")) return
692
710
  event.preventDefault()
693
711
  event.stopPropagation()
694
712
  if (event.key === "ArrowDown") moveDown()
@@ -828,6 +846,8 @@ useHotkeys(() =>
828
846
  code: "KeyK",
829
847
  ctrl: true,
830
848
  allowInInput: true,
849
+ overLayers: true,
850
+ repeat: false,
831
851
  handler: () => emit("update:open", !props.open),
832
852
  },
833
853
  ]
@@ -844,6 +864,7 @@ useHotkeys(() =>
844
864
  :style="{ zIndex: layer.scrim }"
845
865
  />
846
866
  <DialogContent
867
+ ref="contentRef"
847
868
  class="lp-cmd-dialog fixed left-1/2 top-[12vh] flex max-h-[76vh] -translate-x-1/2 flex-col overflow-hidden rounded-card border border-line shadow-panel outline-none data-[state=open]:animate-[pop-in_160ms_var(--ease-emphasized)] data-[state=closed]:animate-[pop-out_130ms_ease]"
848
869
  :class="[frosted ? (layer.nested ? 'lp-glass-overlay--nested' : 'lp-glass-overlay') : 'bg-surface-raised', previewPane && 'lp-cmd-split']"
849
870
  :style="{ zIndex: layer.panel }"
@@ -851,8 +872,8 @@ useHotkeys(() =>
851
872
  @open-auto-focus.prevent
852
873
  @keydown.capture="onPaletteKeydown"
853
874
  >
854
- <DialogTitle class="sr-only">Command palette</DialogTitle>
855
- <DialogDescription class="sr-only">Search and run a command.</DialogDescription>
875
+ <DialogTitle class="sr-only">{{ text.title }}</DialogTitle>
876
+ <DialogDescription class="sr-only">{{ text.description }}</DialogDescription>
856
877
 
857
878
  <!-- Filter. The trail sits inside the box as chips, the way a scoped
858
879
  search reads everywhere else: what you type is filtered by what is
@@ -895,7 +916,7 @@ useHotkeys(() =>
895
916
  @keydown.esc="onEscape"
896
917
  @keydown.tab="onTab"
897
918
  />
898
- <kbd class="shrink-0 rounded border border-line px-1.5 py-0.5 text-[10px] text-muted">esc</kbd>
919
+ <kbd class="shrink-0 rounded border border-line px-1.5 py-0.5 text-[10px] text-muted">{{ text.esc }}</kbd>
899
920
  </div>
900
921
 
901
922
  <!-- Results -->
@@ -1069,7 +1090,7 @@ useHotkeys(() =>
1069
1090
  {{ text.back }}
1070
1091
  </span>
1071
1092
  <span class="ml-auto flex items-center gap-1">
1072
- <kbd class="rounded border border-line px-1 text-[10px]">esc</kbd>
1093
+ <kbd class="rounded border border-line px-1 text-[10px]">{{ text.esc }}</kbd>
1073
1094
  {{ text.close }}
1074
1095
  </span>
1075
1096
  </div>
@@ -1,10 +1,18 @@
1
1
  <script setup lang="ts">
2
2
  // Confirmation dialog built on LpModal. Resolves an action via confirm/cancel.
3
- import { computed } from "vue"
3
+ import { computed, nextTick, ref, watch } from "vue"
4
+ import { useLabels } from "../composables/useLpConfig"
4
5
  import LpButton from "./LpButton.vue"
5
6
  import LpIcon from "./LpIcon.vue"
6
7
  import LpModal from "./LpModal.vue"
7
8
 
9
+ export type ConfirmLabels = {
10
+ confirm: string
11
+ cancel: string
12
+ loading: string
13
+ failed: string
14
+ }
15
+
8
16
  const props = withDefaults(
9
17
  defineProps<{
10
18
  open?: boolean
@@ -33,13 +41,22 @@ const props = withDefaults(
33
41
  * The confirm action is in flight: the button disables and swaps to
34
42
  * `loadingLabel`, and confirming no longer closes the dialog — the owner
35
43
  * closes it once the work settles, so a failure can stay on screen.
44
+ * Setting it from the `confirm` handler is enough; the dialog waits a tick
45
+ * for it before deciding to close.
36
46
  */
37
47
  loading?: boolean
38
48
  loadingLabel?: string
49
+ /**
50
+ * The work behind the confirm button. When given, the dialog runs it,
51
+ * shows the loading state itself, closes once it resolves and stays open
52
+ * with the error's message when it rejects.
53
+ */
54
+ action?: () => unknown
39
55
  /** Frosted glass panel, passed to LpModal. Follows `provideGlass` when unset. */
40
56
  glass?: boolean
57
+ labels?: Partial<ConfirmLabels>
41
58
  }>(),
42
- { confirmLabel: "Confirm", cancelLabel: "Cancel", loadingLabel: "Working…", glass: undefined },
59
+ { glass: undefined },
43
60
  )
44
61
 
45
62
  const emit = defineEmits<{
@@ -48,29 +65,61 @@ const emit = defineEmits<{
48
65
  (e: "cancel"): void
49
66
  }>()
50
67
 
68
+ const text = useLabels<ConfirmLabels>(
69
+ "confirm",
70
+ { confirm: "Confirm", cancel: "Cancel", loading: "Working…", failed: "Something went wrong." },
71
+ () => props.labels,
72
+ )
73
+
51
74
  /*
52
75
  * Only a destructive prompt is marked by default. A routine confirmation
53
76
  * ("Save these changes?") with a warning badge cries wolf, and the badge stops
54
77
  * meaning anything on the dialog where it matters.
55
- *
56
- * "Not given" cannot be tested with `undefined` here. A prop whose type union
57
- * includes Boolean is boolean-cast by Vue, so an absent `icon` arrives as
58
- * `false` — the same value the caller passes to suppress the marker — and the
59
- * default silently never applied. A string is an override, and everything else
60
- * (absent, or an explicit false) falls through to the danger default.
61
78
  */
62
79
  const marker = computed<string>(() => {
63
80
  if (props.icon !== undefined) return props.icon
64
81
  return props.danger ? "lucide:triangle-alert" : ""
65
82
  })
66
83
 
67
- function confirm() {
84
+ const running = ref(false)
85
+ const error = ref("")
86
+ const busy = computed(() => props.loading || running.value)
87
+
88
+ watch(
89
+ () => props.open,
90
+ (open) => {
91
+ if (open) error.value = ""
92
+ },
93
+ )
94
+
95
+ async function confirm() {
96
+ if (busy.value) return
97
+ error.value = ""
68
98
  emit("confirm")
69
- // An async confirm owns its own dismissal; closing here would tear the
70
- // dialog down before the caller knows whether the action succeeded.
99
+ if (props.action) {
100
+ running.value = true
101
+ try {
102
+ await props.action()
103
+ } catch (e) {
104
+ error.value = e instanceof Error && e.message ? e.message : text.value.failed
105
+ return
106
+ } finally {
107
+ running.value = false
108
+ }
109
+ emit("update:open", false)
110
+ return
111
+ }
112
+ // A handler that sets `loading` does so through the parent, whose new props
113
+ // reach this component only on the next render.
114
+ await nextTick()
71
115
  if (!props.loading) emit("update:open", false)
72
116
  }
73
- function cancel() {
117
+
118
+ // Every way out that is not the confirm button counts as a cancel: Escape, the
119
+ // scrim, the close button. None of them may close it while work is in flight.
120
+ function dismiss(open: boolean) {
121
+ if (open) return emit("update:open", true)
122
+ if (busy.value) return
74
123
  emit("cancel")
75
124
  emit("update:open", false)
76
125
  }
@@ -82,7 +131,8 @@ function cancel() {
82
131
  :open="open"
83
132
  :title="title"
84
133
  :description="description"
85
- @update:open="(v) => $emit('update:open', v)"
134
+ role="alertdialog"
135
+ @update:open="dismiss"
86
136
  >
87
137
  <template v-if="marker" #title>
88
138
  <span class="inline-flex items-center gap-2.5">
@@ -100,16 +150,17 @@ function cancel() {
100
150
  </span>
101
151
  </template>
102
152
  <slot />
153
+ <p v-if="error" role="alert" class="mt-3 text-sm text-danger">{{ error }}</p>
103
154
  <template #footer>
104
- <LpButton variant="ghost" :disabled="loading" @click="cancel">
105
- {{ cancelLabel }}
155
+ <LpButton variant="ghost" :disabled="busy" @click="dismiss(false)">
156
+ {{ cancelLabel ?? text.cancel }}
106
157
  </LpButton>
107
158
  <LpButton
108
159
  :variant="danger ? 'danger' : 'solid'"
109
- :disabled="loading"
160
+ :disabled="busy"
110
161
  @click="confirm"
111
162
  >
112
- {{ loading ? loadingLabel : confirmLabel }}
163
+ {{ busy ? (loadingLabel ?? text.loading) : (confirmLabel ?? text.confirm) }}
113
164
  </LpButton>
114
165
  </template>
115
166
  </LpModal>