@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
@@ -37,18 +37,31 @@ const props = withDefaults(
37
37
  variant?: AlertVariants["variant"]
38
38
  title?: string
39
39
  icon?: string
40
+ /**
41
+ * Announce the alert to screen readers when it appears. "assertive"
42
+ * (role=alert) interrupts and is for something that needs action now,
43
+ * "polite" (role=status) waits its turn, false reads it in place. By
44
+ * default only `danger` interrupts: an info or success banner that is part
45
+ * of the page from the start should not be shouted on load.
46
+ */
47
+ live?: "assertive" | "polite" | false
40
48
  }>(),
41
- { variant: "info" },
49
+ { variant: "info", live: undefined },
42
50
  )
43
51
 
52
+ const role = computed(() => {
53
+ const live = props.live ?? (props.variant === "danger" ? "assertive" : false)
54
+ return live === "assertive" ? "alert" : live === "polite" ? "status" : undefined
55
+ })
56
+
44
57
  const classes = computed(() => alert({ variant: props.variant }))
45
58
  const resolvedIcon = computed(() => props.icon ?? ICONS[props.variant ?? "info"])
46
59
  const iconTone = computed(() => ICON_TONE[props.variant ?? "info"])
47
60
  </script>
48
61
 
49
62
  <template>
50
- <div :class="classes" role="alert">
51
- <LpIcon :name="resolvedIcon" :size="18" :class="['mt-0.5 shrink-0', iconTone]" />
63
+ <div :class="classes" :role="role">
64
+ <LpIcon :name="resolvedIcon" :size="18" :class="['mt-0.5 shrink-0', iconTone]" aria-hidden="true" />
52
65
  <div class="min-w-0">
53
66
  <p v-if="title" class="font-semibold text-ink">{{ title }}</p>
54
67
  <div :class="['text-muted-strong', title ? 'mt-0.5' : '']">
@@ -31,7 +31,8 @@ export default { inheritAttrs: false }
31
31
  * Overlays that aren't part of the frame (command palette, toaster) live in the
32
32
  * host next to <LpAppShell>.
33
33
  */
34
- import { computed, watch } from "vue"
34
+ import { computed, ref, useId, watch } from "vue"
35
+ import { useLabels } from "../composables/useLpConfig"
35
36
  import LpButton from "./LpButton.vue"
36
37
  import LpIcon from "./LpIcon.vue"
37
38
  import LpScrollArea from "./LpScrollArea.vue"
@@ -81,6 +82,8 @@ const props = withDefaults(
81
82
  fullBleed?: boolean
82
83
  /** Loading skeleton for the nav. */
83
84
  loading?: boolean
85
+ /** Text overrides: the burger's name and the skip link. */
86
+ labels?: Partial<{ openMenu: string; skipToContent: string }>
84
87
  }>(),
85
88
  {
86
89
  mobileBreakpoint: "lg",
@@ -140,10 +143,30 @@ const drawerOpen = computed({
140
143
  get: () => props.open ?? false,
141
144
  set: (v) => emit("update:open", v),
142
145
  })
146
+ const text = useLabels(
147
+ "appShell",
148
+ { openMenu: "Open menu", skipToContent: "Skip to content" },
149
+ () => props.labels,
150
+ )
151
+
152
+ /*
153
+ * The page scrolls inside the shell's own region, not the window, so the
154
+ * router's scroll behaviour never reaches it: without this a route change from
155
+ * a scrolled page landed mid-way down the next one.
156
+ */
157
+ const scrollArea = ref<{ viewportEl: HTMLElement | null } | null>(null)
158
+ const mainId = `lp-main-${useId()}`
159
+
160
+ // Focus instead of following the hash, which a router would treat as a navigation.
161
+ function skipToMain() {
162
+ document.getElementById(mainId)?.focus()
163
+ }
164
+
143
165
  watch(
144
166
  () => props.path,
145
167
  () => {
146
168
  if (drawerOpen.value) emit("update:open", false)
169
+ scrollArea.value?.viewportEl?.scrollTo({ top: 0, left: 0, behavior: "instant" })
147
170
  },
148
171
  )
149
172
 
@@ -176,6 +199,13 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
176
199
  <div
177
200
  :class="rootClass"
178
201
  v-bind="rest">
202
+ <a
203
+ :href="`#${mainId}`"
204
+ @click.prevent="skipToMain"
205
+ class="sr-only z-(--z-toast) rounded-control bg-surface-raised px-3 py-2 text-sm font-medium text-ink focus:not-sr-only focus:fixed focus:left-3 focus:top-3 focus:outline-none focus:ring-2 focus:ring-ring"
206
+ >
207
+ {{ text.skipToContent }}
208
+ </a>
179
209
  <LpSidebar
180
210
  :model-value="modelValue"
181
211
  :items="items"
@@ -225,7 +255,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
225
255
  variant="ghost"
226
256
  size="sm"
227
257
  square
228
- aria-label="Open menu"
258
+ :aria-label="text.openMenu"
229
259
  @click="emit('update:open', true)"
230
260
  >
231
261
  <LpIcon name="lucide:menu" :size="18" />
@@ -265,16 +295,19 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
265
295
 
266
296
  <!-- full-bleed: page owns the whole region (canvas/map); otherwise the
267
297
  single scroll region with padding. -->
268
- <div v-if="fullBleed" class="min-h-0 flex-1 overflow-hidden">
269
- <slot />
270
- </div>
271
- <LpScrollArea
272
- v-else
273
- class="min-h-0 flex-1"
274
- :content-class="`px-4 py-6 md:px-6 ${contentClass}`"
275
- >
276
- <slot />
277
- </LpScrollArea>
298
+ <main :id="mainId" tabindex="-1" class="flex min-h-0 flex-1 flex-col outline-none">
299
+ <div v-if="fullBleed" class="min-h-0 flex-1 overflow-hidden">
300
+ <slot />
301
+ </div>
302
+ <LpScrollArea
303
+ v-else
304
+ ref="scrollArea"
305
+ class="min-h-0 flex-1"
306
+ :content-class="`px-4 py-6 md:px-6 ${contentClass}`"
307
+ >
308
+ <slot />
309
+ </LpScrollArea>
310
+ </main>
278
311
  </div>
279
312
 
280
313
  <slot name="overlays" />
@@ -1,3 +1,7 @@
1
+ <script lang="ts">
2
+ export default { inheritAttrs: false }
3
+ </script>
4
+
1
5
  <script setup lang="ts">
2
6
  /*
3
7
  * Free-text autocomplete — a text field that suggests as you type but never
@@ -21,8 +25,10 @@ import {
21
25
  ComboboxRoot,
22
26
  ComboboxViewport,
23
27
  } from "reka-ui"
24
- import { computed, ref, watch } from "vue"
28
+ import { computed, ref, useAttrs, watch } from "vue"
29
+ import { useFieldControl } from "../composables/useField"
25
30
  import { useInputFilter } from "../composables/useInputFilter"
31
+ import { useLabels } from "../composables/useLpConfig"
26
32
  import { CLOSE_ICON, OPTION_ITEM, POPOVER_PANEL } from "./dropdown"
27
33
  import LpIcon from "./LpIcon.vue"
28
34
 
@@ -78,7 +84,7 @@ const emit = defineEmits<{
78
84
  (e: "select", value: string): void
79
85
  }>()
80
86
 
81
- const { isInvalid, onBeforeInput, onPaste } = useInputFilter({
87
+ const { isInvalid, onBeforeInput, onInput: filterInput, onCompositionEnd, onPaste } = useInputFilter({
82
88
  pattern: () => props.pattern,
83
89
  restrict: () => props.restrict,
84
90
  })
@@ -88,7 +94,22 @@ const text = computed<string>({
88
94
  set: (v) => emit("update:modelValue", v),
89
95
  })
90
96
 
91
- const isInvalidState = computed(() => props.invalid || isInvalid(props.modelValue))
97
+ // class/style lay out the whole control; every other attr (name, autocomplete,
98
+ // @blur, @keydown…) belongs to the text <input>.
99
+ const attrs = useAttrs()
100
+ const inputAttrs = computed(() => {
101
+ const { class: _c, style: _s, ...rest } = attrs
102
+ return rest
103
+ })
104
+
105
+ const field = useFieldControl(() => ({
106
+ wrapped: false,
107
+ id: typeof attrs.id === "string" ? attrs.id : undefined,
108
+ invalid: props.invalid || isInvalid(props.modelValue),
109
+ disabled: props.disabled,
110
+ }))
111
+ const isInvalidState = computed(() => field.value.invalid)
112
+ const labels = useLabels("autocomplete", { clear: "Clear" })
92
113
 
93
114
  // Normalise string|option to a stable shape.
94
115
  const items = computed<AutocompleteOption[]>(() =>
@@ -135,6 +156,13 @@ watch(canOpen, (can) => {
135
156
  if (focused.value && can) open.value = true
136
157
  })
137
158
 
159
+ // reka opens the list on every keystroke; below `minChars` (or with nothing to
160
+ // show) that only produced an empty "No suggestions" panel, so opening is
161
+ // refused here. Closing is always allowed.
162
+ function onOpenChange(next: boolean) {
163
+ open.value = next && canOpen.value
164
+ }
165
+
138
166
  const hasValue = computed(() => (props.modelValue ?? "").length > 0)
139
167
 
140
168
  const anchorSize = {
@@ -144,6 +172,7 @@ const anchorSize = {
144
172
  }
145
173
 
146
174
  function onInput(event: Event) {
175
+ filterInput(event)
147
176
  typing.value = true
148
177
  text.value = (event.target as HTMLInputElement).value
149
178
  }
@@ -172,12 +201,14 @@ function clear() {
172
201
 
173
202
  <template>
174
203
  <ComboboxRoot
175
- v-model:open="open"
204
+ :open="open"
176
205
  :ignore-filter="true"
177
206
  :reset-search-term-on-blur="false"
178
207
  :reset-search-term-on-select="false"
179
- :disabled="disabled"
180
- class="relative"
208
+ :disabled="field.disabled"
209
+ :class="['relative', attrs.class]"
210
+ :style="attrs.style as never"
211
+ @update:open="onOpenChange"
181
212
  >
182
213
  <ComboboxAnchor
183
214
  data-lp-ring-owner
@@ -198,14 +229,18 @@ function clear() {
198
229
  so `text` (and the reactive `open`) track the field exactly.
199
230
  -->
200
231
  <ComboboxInput
232
+ v-bind="inputAttrs"
233
+ :id="field.id"
201
234
  :model-value="text"
202
235
  :placeholder="placeholder"
203
- :disabled="disabled"
236
+ :disabled="field.disabled"
237
+ :required="field.required || undefined"
204
238
  :aria-invalid="isInvalidState || undefined"
205
- auto-focus
239
+ :aria-describedby="field.describedBy"
206
240
  class="lp-autofill min-w-0 flex-1 self-stretch bg-transparent outline-none placeholder:text-muted"
207
241
  @beforeinput="onBeforeInput"
208
242
  @paste="onPaste"
243
+ @compositionend="onCompositionEnd"
209
244
  @input="onInput"
210
245
  @focus="onFocus"
211
246
  @blur="focused = false"
@@ -214,8 +249,9 @@ function clear() {
214
249
  v-if="clearable && hasValue"
215
250
  type="button"
216
251
  class="group flex shrink-0 items-center text-muted outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink"
217
- aria-label="Clear"
218
- @pointerdown.prevent="clear"
252
+ :aria-label="labels.clear"
253
+ @pointerdown.prevent
254
+ @click="clear"
219
255
  >
220
256
  <LpIcon
221
257
  name="lucide:x"
@@ -10,6 +10,7 @@ import { AvatarFallback, AvatarImage, AvatarRoot } from "reka-ui"
10
10
  import { tv, type VariantProps } from "tailwind-variants"
11
11
  import { computed, onMounted, ref } from "vue"
12
12
  import LpContextMenu, { type ContextMenuItemDef } from "./LpContextMenu.vue"
13
+ import { useMergedAttrs } from "../composables/useMergedClass"
13
14
 
14
15
  const avatar = tv({
15
16
  base: "inline-flex shrink-0 select-none items-center justify-center overflow-hidden bg-surface-soft font-medium text-muted-strong",
@@ -65,8 +66,21 @@ const sizeStyle = computed(() =>
65
66
  : undefined,
66
67
  )
67
68
 
68
- const initials = computed(
69
- () => props.fallback ?? props.alt?.slice(0, 2).toUpperCase() ?? "?",
69
+ /* "John Doe" → "JD", "Ada" → "AD". Split by code point, so a name starting
70
+ with an emoji or an astral letter is not cut in half. */
71
+ const initials = computed(() => {
72
+ if (props.fallback) return props.fallback
73
+ const words = (props.alt ?? "").trim().split(/\s+/).filter(Boolean)
74
+ if (!words.length) return "?"
75
+ const first = Array.from(words[0])
76
+ const letters =
77
+ words.length > 1 ? [first[0], Array.from(words[words.length - 1])[0]] : first.slice(0, 2)
78
+ return letters.join("").toUpperCase()
79
+ })
80
+
81
+ // A consumer's `size-20` must beat the preset's `size-9`, not race it.
82
+ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
83
+ avatar({ size: props.pixelSize ? undefined : props.size, shape: props.shape, ring: props.ring }),
70
84
  )
71
85
 
72
86
  // reka's AvatarImage resolves the image load on the client, so SSR renders the
@@ -83,13 +97,13 @@ onMounted(() => {
83
97
 
84
98
  <template>
85
99
  <LpContextMenu v-if="menuItems?.length" :items="menuItems">
86
- <AvatarRoot :class="avatar({ size: pixelSize ? undefined : size, shape, ring })" :style="sizeStyle" v-bind="$attrs">
100
+ <AvatarRoot :class="rootClass" :style="sizeStyle" v-bind="rest">
87
101
  <AvatarImage v-if="src && mounted" :src="src" :alt="alt" class="size-full object-cover" />
88
102
  <AvatarFallback>{{ initials }}</AvatarFallback>
89
103
  </AvatarRoot>
90
104
  </LpContextMenu>
91
- <AvatarRoot v-else :class="avatar({ size: pixelSize ? undefined : size, shape, ring })" :style="sizeStyle" v-bind="$attrs">
92
- <AvatarImage v-if="src" :src="src" :alt="alt" class="size-full object-cover" />
105
+ <AvatarRoot v-else :class="rootClass" :style="sizeStyle" v-bind="rest">
106
+ <AvatarImage v-if="src && mounted" :src="src" :alt="alt" class="size-full object-cover" />
93
107
  <AvatarFallback>{{ initials }}</AvatarFallback>
94
108
  </AvatarRoot>
95
109
  </template>
@@ -6,32 +6,51 @@ export default { inheritAttrs: false }
6
6
  </script>
7
7
 
8
8
  <script setup lang="ts">
9
- import type { Component } from "vue"
9
+ import { computed, type Component } from "vue"
10
10
  import LpIcon from "./LpIcon.vue"
11
+ import { useLabels, useLinkComponent } from "../composables/useLpConfig"
11
12
  import { useMergedAttrs } from "../composables/useMergedClass"
13
+ import { resolveLink } from "./link"
12
14
 
13
15
  export interface Crumb {
14
16
  label: string
15
17
  href?: string
16
- /** Destination for router links; passed straight to the `as` component. */
18
+ /** Destination for router links; rendered through the link component. */
17
19
  to?: string | Record<string, unknown>
18
20
  /** Leading icon, for crumbs that carry a glyph (a home root, a section). */
19
21
  icon?: string
20
22
  }
21
23
 
22
- withDefaults(
23
- defineProps<{
24
- items: Crumb[]
25
- /**
26
- * Root element/component for linked crumbs. Defaults to a native <a>;
27
- * pass a router link component to keep client-side navigation.
28
- */
29
- as?: string | Component
30
- }>(),
31
- { as: "a" },
32
- )
24
+ const props = defineProps<{
25
+ items: Crumb[]
26
+ /**
27
+ * Component for linked crumbs. By default `to` and internal `href` use the
28
+ * link component from provideLpConfig and other hrefs a plain <a>.
29
+ */
30
+ as?: string | Component
31
+ labels?: Partial<{ nav: string }>
32
+ }>()
33
33
  defineEmits<{ (e: "navigate", item: Crumb, index: number): void }>()
34
34
 
35
+ const text = useLabels("breadcrumbs", { nav: "Breadcrumb" }, () => props.labels)
36
+ const linkComponent = useLinkComponent()
37
+
38
+ function linkOf(item: Crumb) {
39
+ if (props.as && props.as !== "a") {
40
+ if (item.to != null) return { is: props.as, props: { to: item.to } }
41
+ return item.href != null ? { is: "a", props: { href: item.href } } : null
42
+ }
43
+ return resolveLink(props.as === "a" ? "a" : linkComponent.value, { to: item.to, href: item.href })
44
+ }
45
+
46
+ const crumbs = computed(() =>
47
+ props.items.map((item, i) => ({
48
+ item,
49
+ last: i === props.items.length - 1,
50
+ link: i === props.items.length - 1 ? null : linkOf(item),
51
+ })),
52
+ )
53
+
35
54
  // The trail sets its own gap, which a dense header bar needs to reduce.
36
55
  const { class: rootClass, attrs: rest } = useMergedAttrs(
37
56
  "flex items-center gap-1.5 text-sm",
@@ -39,35 +58,40 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
39
58
  </script>
40
59
 
41
60
  <template>
42
- <nav
43
- :class="rootClass"
44
- v-bind="rest" aria-label="Breadcrumb">
45
- <template v-for="(item, i) in items" :key="i">
46
- <LpIcon
47
- v-if="i > 0"
48
- name="lucide:chevron-right"
49
- :size="14"
50
- class="text-muted"
51
- />
52
- <component
53
- :is="as"
54
- v-if="(item.href || item.to) && i < items.length - 1"
55
- :href="as === 'a' ? item.href : undefined"
56
- :to="as === 'a' ? undefined : item.to"
57
- class="inline-flex items-center gap-1.5 rounded-xs text-muted outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
58
- @click="$emit('navigate', item, i)"
59
- >
60
- <LpIcon v-if="item.icon" :name="item.icon" :size="14" />
61
- {{ item.label }}
62
- </component>
63
- <span
64
- v-else
61
+ <nav :aria-label="text.nav" v-bind="rest">
62
+ <ol :class="rootClass">
63
+ <li
64
+ v-for="({ item, last, link }, i) in crumbs"
65
+ :key="i"
65
66
  class="inline-flex items-center gap-1.5"
66
- :class="i === items.length - 1 ? 'text-ink' : 'text-muted'"
67
67
  >
68
- <LpIcon v-if="item.icon" :name="item.icon" :size="14" />
69
- {{ item.label }}
70
- </span>
71
- </template>
68
+ <LpIcon
69
+ v-if="i > 0"
70
+ name="lucide:chevron-right"
71
+ :size="14"
72
+ class="text-muted"
73
+ aria-hidden="true"
74
+ />
75
+ <component
76
+ :is="link.is"
77
+ v-if="link"
78
+ v-bind="link.props"
79
+ class="inline-flex items-center gap-1.5 rounded-xs text-muted outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
80
+ @click="$emit('navigate', item, i)"
81
+ >
82
+ <LpIcon v-if="item.icon" :name="item.icon" :size="14" />
83
+ {{ item.label }}
84
+ </component>
85
+ <span
86
+ v-else
87
+ class="inline-flex items-center gap-1.5"
88
+ :class="last ? 'text-ink' : 'text-muted'"
89
+ :aria-current="last ? 'page' : undefined"
90
+ >
91
+ <LpIcon v-if="item.icon" :name="item.icon" :size="14" />
92
+ {{ item.label }}
93
+ </span>
94
+ </li>
95
+ </ol>
72
96
  </nav>
73
97
  </template>
@@ -1,6 +1,15 @@
1
+ <script lang="ts">
2
+ // The consumer's `class` is merged into the root's own rather than appended,
3
+ // so `class="h-12"` or `class="px-6"` beats the size's defaults.
4
+ export default { inheritAttrs: false }
5
+ </script>
6
+
1
7
  <script setup lang="ts">
2
8
  import { tv, type VariantProps } from "tailwind-variants"
3
9
  import { computed, type Component } from "vue"
10
+ import { useLinkComponent } from "../composables/useLpConfig"
11
+ import { useMergedAttrs } from "../composables/useMergedClass"
12
+ import { resolveLink, type LinkTarget } from "./link"
4
13
 
5
14
  const button = tv({
6
15
  base: [
@@ -16,13 +25,15 @@ const button = tv({
16
25
  solid: "bg-brand text-ink-inverse hover:bg-brand-hover",
17
26
  action:
18
27
  "bg-action text-ink-inverse shadow-[0_14px_28px_color-mix(in_srgb,var(--color-action)_24%,transparent)] hover:bg-action-hover",
19
- outline: "border border-line-strong text-ink hover:border-brand hover:bg-white/[0.04]",
28
+ outline: "border border-line-strong text-ink hover:border-brand hover:bg-ink/[0.04]",
20
29
  // raised surface tile with a border — a visible, neutral filled button
21
30
  soft: "border border-line bg-surface-raised/80 text-ink hover:border-line-strong hover:bg-surface-soft",
22
- ghost: "text-ink hover:bg-white/[0.06]",
23
- muted: "text-muted hover:bg-white/[0.04] hover:text-ink",
24
- // high-contrast light fill (e.g. a "download" CTA on a dark hero)
25
- light: "border border-white/20 bg-white text-ink-inverse hover:bg-white/90",
31
+ ghost: "text-ink hover:bg-ink/[0.06]",
32
+ muted: "text-muted hover:bg-ink/[0.04] hover:text-ink",
33
+ // high-contrast light fill (e.g. a "download" CTA on a dark hero). The
34
+ // text is a fixed dark tone: `ink-inverse` is light in light themes,
35
+ // which made this white on white there.
36
+ light: "border border-black/10 bg-white text-[#0b1210] hover:bg-white/90",
26
37
  danger: "bg-danger text-ink-inverse hover:bg-danger-hover",
27
38
  },
28
39
  size: {
@@ -58,7 +69,7 @@ const button = tv({
58
69
  glow: true,
59
70
  variant: "light",
60
71
  class:
61
- "shadow-[0_8px_18px_rgba(255,255,255,0.2)] hover:shadow-[0_12px_24px_rgba(255,255,255,0.3)]",
72
+ "shadow-[0_8px_18px_color-mix(in_srgb,var(--color-ink)_20%,transparent)] hover:shadow-[0_12px_24px_color-mix(in_srgb,var(--color-ink)_30%,transparent)]",
62
73
  },
63
74
  ],
64
75
  defaultVariants: { variant: "solid", size: "md" },
@@ -74,20 +85,52 @@ const props = withDefaults(
74
85
  square?: boolean
75
86
  glow?: boolean
76
87
  /**
77
- * Root element/component. Defaults to a native <button>. Pass a router link
78
- * component (e.g. NuxtLink) or "a" to render a link-styled button; the kit
79
- * stays DOM-agnostic, the consumer supplies the link component.
88
+ * Root element/component. Defaults to a native <button>, or a link when
89
+ * `to`/`href` is set (the link component from provideLpConfig for internal
90
+ * targets). Pass a component or "a" to override.
80
91
  */
81
92
  as?: string | Component
93
+ /** Router destination: renders the configured link component. */
94
+ to?: LinkTarget
95
+ /** Link URL: renders an anchor (or the link component for internal paths). */
96
+ href?: string
82
97
  type?: "button" | "submit" | "reset"
83
98
  disabled?: boolean
84
99
  }>(),
85
- { as: "button", type: "button" },
100
+ { type: "button" },
86
101
  )
87
102
 
88
- const isNativeButton = computed(() => props.as === "button")
103
+ const linkComponent = useLinkComponent()
104
+
105
+ const resolved = computed<{ is: string | Component; props: Record<string, unknown> }>(() => {
106
+ if (props.as && props.as !== "a") {
107
+ const target = props.to != null ? { to: props.to } : props.href != null ? { href: props.href } : {}
108
+ return { is: props.as, props: props.as === "button" ? {} : target }
109
+ }
110
+ if (props.as === "a") return { is: "a", props: { href: props.href } }
111
+ return resolveLink(linkComponent.value, { to: props.to, href: props.href }) ?? { is: "button", props: {} }
112
+ })
113
+
114
+ const isNativeButton = computed(() => resolved.value.is === "button")
115
+
116
+ /*
117
+ * A disabled link has no `disabled` attribute to lean on: it must leave the tab
118
+ * order and swallow the click. The capture phase runs before a router link's
119
+ * own handler, which skips navigation once the event is default-prevented.
120
+ */
121
+ function blockDisabled(event: MouseEvent) {
122
+ if (!props.disabled || isNativeButton.value) return
123
+ event.preventDefault()
124
+ event.stopImmediatePropagation()
125
+ }
126
+
127
+ const linkProps = computed(() => {
128
+ if (isNativeButton.value) return {}
129
+ if (!props.disabled) return resolved.value.props
130
+ return resolved.value.is === "a" ? {} : resolved.value.props
131
+ })
89
132
 
90
- const classes = computed(() =>
133
+ const { class: classes, attrs: rest } = useMergedAttrs(() =>
91
134
  button({
92
135
  variant: props.variant,
93
136
  size: props.size,
@@ -100,11 +143,14 @@ const classes = computed(() =>
100
143
 
101
144
  <template>
102
145
  <component
103
- :is="as"
146
+ :is="resolved.is"
147
+ v-bind="{ ...rest, ...linkProps }"
104
148
  :type="isNativeButton ? type : undefined"
105
149
  :disabled="isNativeButton ? disabled : undefined"
106
150
  :aria-disabled="!isNativeButton && disabled ? 'true' : undefined"
151
+ :tabindex="!isNativeButton && disabled ? -1 : undefined"
107
152
  :class="classes"
153
+ @click.capture="blockDisabled"
108
154
  >
109
155
  <slot />
110
156
  </component>