@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
@@ -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
 
@@ -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>