@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
@@ -16,6 +16,7 @@ export default { inheritAttrs: false }
16
16
  import { Motion } from "motion-v"
17
17
  import { ToggleGroupItem, ToggleGroupRoot } from "reka-ui"
18
18
  import { useId } from "vue"
19
+ import { useFieldControl } from "../composables/useField"
19
20
  import { useMergedAttrs } from "../composables/useMergedClass"
20
21
  import { usePillTransition } from "../composables/usePillTransition"
21
22
  import LpIcon from "./LpIcon.vue"
@@ -25,6 +26,11 @@ export interface SegmentedOption {
25
26
  label?: string
26
27
  /** Optional iconify name shown before the label (or alone, icon-only). */
27
28
  icon?: string
29
+ /**
30
+ * Accessible name for an icon-only segment. Without it the raw `value` is
31
+ * announced, which is an identifier rather than words.
32
+ */
33
+ ariaLabel?: string
28
34
  }
29
35
 
30
36
  const props = withDefaults(
@@ -66,6 +72,8 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
66
72
  ].join(" "),
67
73
  )
68
74
 
75
+ const field = useFieldControl(() => ({ wrapped: false, disabled: props.disabled }))
76
+
69
77
  const pillId = `lp-segmented-${useId()}`
70
78
  const pillTransition = usePillTransition()
71
79
  </script>
@@ -74,7 +82,9 @@ const pillTransition = usePillTransition()
74
82
  <ToggleGroupRoot
75
83
  :model-value="modelValue"
76
84
  type="single"
77
- :disabled="disabled"
85
+ :disabled="field.disabled"
86
+ :aria-labelledby="field.labelId"
87
+ :aria-describedby="field.describedBy"
78
88
  :rovingFocus="true"
79
89
  :class="rootClass"
80
90
  v-bind="rest"
@@ -84,7 +94,7 @@ const pillTransition = usePillTransition()
84
94
  v-for="opt in options"
85
95
  :key="opt.value"
86
96
  :value="opt.value"
87
- :aria-label="opt.label ? undefined : opt.value"
97
+ :aria-label="opt.ariaLabel ?? (opt.label ? undefined : opt.value)"
88
98
  class="relative inline-flex h-full items-center justify-center gap-1.5 rounded-md px-3 font-medium outline-none transition-colors duration-[var(--duration-fast)] ease-[var(--ease-emphasized)] focus-visible:ring-2 focus-visible:ring-ring data-[state=on]:text-ink data-[state=off]:text-muted data-[state=off]:hover:text-ink disabled:cursor-not-allowed disabled:opacity-55"
89
99
  :class="block ? 'flex-1' : ''"
90
100
  >
Binary file
@@ -18,8 +18,10 @@ import {
18
18
  activeShortcuts,
19
19
  appleModifierLabel,
20
20
  mergeShortcutGroups,
21
+ useAppleKeyboard,
21
22
  type ShortcutGroup,
22
23
  } from "../composables/useHotkeys"
24
+ import { useLabels } from "../composables/useLpConfig"
23
25
 
24
26
  const props = withDefaults(
25
27
  defineProps<{
@@ -32,9 +34,12 @@ const props = withDefaults(
32
34
  /** Frosted glass panel, passed to LpModal. Follows `provideGlass` when unset. */
33
35
  glass?: boolean
34
36
  }>(),
35
- { open: false, groups: () => [], live: false, title: "Keyboard shortcuts", glass: undefined },
37
+ { open: false, groups: () => [], live: false, title: undefined, glass: undefined },
36
38
  )
37
39
 
40
+ const text = useLabels("shortcuts", { title: "Keyboard shortcuts" })
41
+ const apple = useAppleKeyboard()
42
+
38
43
  /*
39
44
  * One key per "+", except a "+" that is the key itself: "Ctrl++" is Ctrl and
40
45
  * plus, not Ctrl and two blanks.
@@ -53,7 +58,7 @@ const emit = defineEmits<{ (e: "update:open", value: boolean): void }>()
53
58
  :glass="props.glass"
54
59
  :open="props.open"
55
60
  size="lg"
56
- :title="props.title"
61
+ :title="props.title ?? text.title"
57
62
  :description="props.description"
58
63
  @update:open="(value: boolean) => emit('update:open', value)"
59
64
  >
@@ -65,7 +70,7 @@ const emit = defineEmits<{ (e: "update:open", value: boolean): void }>()
65
70
  <dt>{{ entry.label }}</dt>
66
71
  <dd>
67
72
  <kbd v-for="(key, index) in keysOf(entry.combo)" :key="`${key}-${index}`">
68
- {{ appleModifierLabel(key.trim()) }}
73
+ {{ appleModifierLabel(key.trim(), apple) }}
69
74
  </kbd>
70
75
  </dd>
71
76
  </div>
@@ -14,6 +14,7 @@
14
14
  * the static <nav> it always has — existing call sites are unaffected.
15
15
  */
16
16
  import { computed, ref, useId } from "vue"
17
+ import { useLabels } from "../composables/useLpConfig"
17
18
  import LpDrawer from "./LpDrawer.vue"
18
19
  import LpInput from "./LpInput.vue"
19
20
  import LpSidebarNav from "./LpSidebarNav.vue"
@@ -45,8 +46,10 @@ const props = withDefaults(
45
46
  * query is active — a hidden match is the same as no match.
46
47
  */
47
48
  searchable?: boolean
48
- /** Placeholder for that field. */
49
+ /** Placeholder for that field (defaults to the `search` label). */
49
50
  searchPlaceholder?: string
51
+ /** Text overrides: `nav` names the landmark, `search` the filter field. */
52
+ labels?: Partial<{ nav: string; search: string }>
50
53
  /** The query, when the app wants to own it (v-model:search). */
51
54
  search?: string
52
55
  /** Show a skeleton placeholder instead of the items. */
@@ -113,7 +116,6 @@ const props = withDefaults(
113
116
  autoCollapseAfter?: number
114
117
  }>(),
115
118
  {
116
- searchPlaceholder: "Search",
117
119
  skeletonRows: 6,
118
120
  skeletonHeader: true,
119
121
  responsive: false,
@@ -132,6 +134,9 @@ const emit = defineEmits<{
132
134
  (e: "update:search", value: string): void
133
135
  }>()
134
136
 
137
+ const text = useLabels("sidebar", { nav: "Sidebar", search: "Search" }, () => props.labels)
138
+ const placeholder = computed(() => props.searchPlaceholder ?? text.value.search)
139
+
135
140
  // Normalise both inputs to a single section list.
136
141
  const allGroups = computed<SidebarSection[]>(() =>
137
142
  props.sections ?? (props.items ? [{ items: props.items }] : []),
@@ -271,7 +276,7 @@ defineOptions({ inheritAttrs: false })
271
276
  class="h-full flex-col gap-1 p-3"
272
277
  :class="[railClass, chromeClass, width ? '' : 'w-60']"
273
278
  :style="width ? { width } : undefined"
274
- aria-label="Sidebar"
279
+ :aria-label="text.nav"
275
280
  v-bind="$attrs"
276
281
  >
277
282
  <!-- Rail: edge-to-edge (negative margins cancel the p-3) and h-16 to match
@@ -298,8 +303,8 @@ defineOptions({ inheritAttrs: false })
298
303
  size="sm"
299
304
  icon="lucide:search"
300
305
  class="mb-2 shrink-0"
301
- :placeholder="searchPlaceholder"
302
- :aria-label="searchPlaceholder"
306
+ :placeholder="placeholder"
307
+ :aria-label="placeholder"
303
308
  />
304
309
  <LpSidebarNav
305
310
  :groups="groups"
@@ -339,7 +344,7 @@ defineOptions({ inheritAttrs: false })
339
344
  :edge-breakpoint="mobileBreakpoint"
340
345
  @update:open="(v) => emit('update:open', v)"
341
346
  >
342
- <div class="flex h-full flex-col gap-1">
347
+ <nav class="flex h-full flex-col gap-1" :aria-label="text.nav">
343
348
  <div
344
349
  v-if="$slots.header"
345
350
  class="shrink-0"
@@ -357,8 +362,8 @@ defineOptions({ inheritAttrs: false })
357
362
  size="sm"
358
363
  icon="lucide:search"
359
364
  class="mb-2 shrink-0"
360
- :placeholder="searchPlaceholder"
361
- :aria-label="searchPlaceholder"
365
+ :placeholder="placeholder"
366
+ :aria-label="placeholder"
362
367
  />
363
368
  <LpSidebarNav
364
369
  :groups="groups"
@@ -383,6 +388,6 @@ defineOptions({ inheritAttrs: false })
383
388
  <div v-if="$slots.footer" class="mt-2 shrink-0 border-t border-line pt-3">
384
389
  <slot name="footer" />
385
390
  </div>
386
- </div>
391
+ </nav>
387
392
  </LpDrawer>
388
393
  </template>
@@ -21,6 +21,8 @@ import LpContextMenu from "./LpContextMenu.vue"
21
21
  import LpIcon from "./LpIcon.vue"
22
22
  import LpScrollArea from "./LpScrollArea.vue"
23
23
  import type { SidebarItem, SidebarSection } from "./sidebar"
24
+ import { resolveLink, type ResolvedLink } from "./link"
25
+ import { useLinkComponent } from "../composables/useLpConfig"
24
26
  import { useMergedAttrs } from "../composables/useMergedClass"
25
27
 
26
28
  const props = withDefaults(
@@ -75,6 +77,18 @@ function isOpen(section: SidebarSection): boolean {
75
77
 
76
78
  const pillTransition = usePillTransition()
77
79
 
80
+ const linkComponent = useLinkComponent()
81
+
82
+ /*
83
+ * A disabled item is always a disabled button: an anchor cannot be disabled,
84
+ * and the old enabled-button fallback for a disabled link stayed focusable.
85
+ */
86
+ function rowOf(item: SidebarItem): ResolvedLink {
87
+ const link = item.disabled ? null : resolveLink(linkComponent.value, { to: item.to, href: item.href })
88
+ if (link) return link
89
+ return { is: "button", props: { type: "button", disabled: item.disabled || undefined } }
90
+ }
91
+
78
92
  // The nav fills the remaining height with a fixed gap between groups; a short sidebar needs both under the consumer's control.
79
93
  const { class: rootClass, attrs: rest } = useMergedAttrs(
80
94
  "flex min-h-0 flex-1 flex-col gap-4",
@@ -118,7 +132,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
118
132
  because a thing that reacts to a click must look like one. -->
119
133
  <CollapsibleTrigger
120
134
  v-if="section.title && section.collapsible"
121
- class="group/section mt-2 flex cursor-pointer items-center gap-2 rounded-control px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted outline-none transition-colors duration-[var(--duration-fast)] hover:bg-white/[0.04] hover:text-ink focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
135
+ class="group/section mt-2 flex cursor-pointer items-center gap-2 rounded-control px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted outline-none transition-colors duration-[var(--duration-fast)] hover:bg-ink/[0.04] hover:text-ink focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
122
136
  >
123
137
  <LpIcon
124
138
  v-if="section.icon"
@@ -165,10 +179,8 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
165
179
  :items="item.menu ?? []"
166
180
  >
167
181
  <component
168
- :is="item.href && !item.disabled ? 'a' : 'button'"
169
- :href="item.href && !item.disabled ? item.href : undefined"
170
- :type="item.href ? undefined : 'button'"
171
- :disabled="item.href ? undefined : item.disabled"
182
+ :is="rowOf(item).is"
183
+ v-bind="rowOf(item).props"
172
184
  :aria-current="itemActive(item) ? 'page' : undefined"
173
185
  :aria-disabled="item.disabled || undefined"
174
186
  class="group/item relative flex items-center gap-3 rounded-control px-3 py-2 text-sm font-medium outline-none transition-colors duration-[var(--duration-fast)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
@@ -88,7 +88,10 @@ const isBlock = computed(() => !slots.default)
88
88
  }
89
89
 
90
90
  @media (prefers-reduced-motion: reduce) {
91
- .lp-skeleton-item {
91
+ /* Repeats the descendant form: it outranks a bare class, and grouped items
92
+ kept pulsing under reduced motion. */
93
+ .lp-skeleton-item,
94
+ .lp-skeleton .lp-skeleton-item {
92
95
  animation: none;
93
96
  opacity: 0.75;
94
97
  }
@@ -9,6 +9,7 @@ export default { inheritAttrs: false }
9
9
  // Single-value slider (reka Slider). reka models value as an array; we expose
10
10
  // a plain number for the common single-thumb case. Themed via tokens.
11
11
  import { SliderRange, SliderRoot, SliderThumb, SliderTrack } from "reka-ui"
12
+ import { useFieldControl } from "../composables/useField"
12
13
  import { useMergedAttrs } from "../composables/useMergedClass"
13
14
 
14
15
  const props = withDefaults(
@@ -38,6 +39,8 @@ function onCommit(v: number[] | undefined) {
38
39
  if (v && v.length) emit("change", v[0])
39
40
  }
40
41
 
42
+ const field = useFieldControl(() => ({ wrapped: false, disabled: props.disabled }))
43
+
41
44
  // A slider is full-width by default; putting one in a toolbar means constraining it, and a fixed h-5 has to yield to a denser row.
42
45
  const { class: rootClass, attrs: rest } = useMergedAttrs(
43
46
  "relative flex h-5 w-full touch-none select-none items-center data-[disabled]:opacity-55",
@@ -50,7 +53,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
50
53
  :min="min"
51
54
  :max="max"
52
55
  :step="step"
53
- :disabled="disabled"
56
+ :disabled="field.disabled"
54
57
  :class="rootClass"
55
58
  v-bind="rest"
56
59
  @update:model-value="onUpdate"
@@ -60,7 +63,10 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
60
63
  <SliderRange class="absolute h-full rounded-pill bg-brand" />
61
64
  </SliderTrack>
62
65
  <SliderThumb
66
+ :id="field.id"
63
67
  :aria-label="ariaLabel"
68
+ :aria-labelledby="ariaLabel ? undefined : field.labelId"
69
+ :aria-describedby="field.describedBy"
64
70
  class="block size-4 rounded-pill border-2 border-brand bg-surface shadow outline-none transition-transform duration-[var(--duration-fast)] ease-[var(--ease-emphasized)] hover:scale-110 active:scale-95 focus-visible:ring-2 focus-visible:ring-ring motion-reduce:hover:scale-100 motion-reduce:active:scale-100"
65
71
  />
66
72
  </SliderRoot>
@@ -16,6 +16,7 @@
16
16
  */
17
17
  import { computed } from "vue"
18
18
  import { useReveal } from "../composables/useReveal"
19
+ import { useLabels } from "../composables/useLpConfig"
19
20
  import { areaPath, extent, linePath, normalise, type Box } from "./chart"
20
21
 
21
22
  const props = withDefaults(
@@ -56,6 +57,7 @@ const props = withDefaults(
56
57
  * watched being drawn — not a figure mostly spent easing out.
57
58
  */
58
59
  duration?: number
60
+ labels?: Partial<Record<"empty" | "up" | "down" | "flat" | "latest", string>>
59
61
  }>(),
60
62
  { width: 84, height: 24, tone: "brand", smooth: 0.9, animate: true, duration: 900 },
61
63
  )
@@ -131,14 +133,20 @@ const lastPoint = computed(() => {
131
133
  const hasData = computed(() => series.value[0].values.some((v) => v != null))
132
134
 
133
135
  /* A bare <svg> is invisible to a screen reader; say the shape out loud. */
136
+ const text = useLabels(
137
+ "sparkline",
138
+ { empty: "No data", up: "trending up", down: "trending down", flat: "flat", latest: "latest" },
139
+ () => props.labels,
140
+ )
141
+
134
142
  const label = computed(() => {
135
143
  if (props.ariaLabel) return props.ariaLabel
136
144
  const values = series.value[0].values.filter((v): v is number => v != null)
137
- if (!values.length) return "No data"
145
+ if (!values.length) return text.value.empty
138
146
  const first = values[0]
139
147
  const last = values[values.length - 1]
140
- const dir = last > first ? "trending up" : last < first ? "trending down" : "flat"
141
- return `${dir}, latest ${last}`
148
+ const dir = last > first ? text.value.up : last < first ? text.value.down : text.value.flat
149
+ return `${dir}, ${text.value.latest} ${last}`
142
150
  })
143
151
 
144
152
  /*
@@ -6,8 +6,9 @@ export default { inheritAttrs: false }
6
6
  </script>
7
7
 
8
8
  <script setup lang="ts">
9
- import { computed, useSlots } from "vue"
9
+ import { computed, useId, useSlots } from "vue"
10
10
  import { SwitchRoot, SwitchThumb } from "reka-ui"
11
+ import { useFieldControl } from "../composables/useField"
11
12
  import { useMergedAttrs } from "../composables/useMergedClass"
12
13
 
13
14
  const props = withDefaults(
@@ -46,31 +47,53 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() => {
46
47
  : "inline-flex cursor-pointer items-center gap-2 text-sm text-ink"
47
48
  })
48
49
 
50
+ // Whatever is not class (name, aria-*, @focus…) belongs to the switch itself,
51
+ // labelled or not: on the label row it would land on the <label>.
52
+ const field = useFieldControl(() => ({
53
+ wrapped: false,
54
+ id: typeof rest.value.id === "string" ? rest.value.id : undefined,
55
+ disabled: props.disabled,
56
+ }))
57
+ const descriptionId = `${useId()}-description`
58
+ const describedBy = computed(
59
+ () => [props.description && labelled.value ? descriptionId : "", field.value.describedBy ?? ""].filter(Boolean).join(" ") || undefined,
60
+ )
61
+
49
62
  function onUpdate(value: boolean) {
50
63
  emit("update:modelValue", value)
51
64
  }
52
65
  </script>
53
66
 
54
67
  <template>
55
- <label v-if="labelled" :class="[rootClass, disabled && 'cursor-not-allowed']" v-bind="rest">
68
+ <label v-if="labelled" :class="[rootClass, field.disabled && 'cursor-not-allowed']">
56
69
  <span v-if="labelPosition === 'start'" class="flex min-w-0 flex-col">
57
70
  <slot>{{ label }}</slot>
58
- <span v-if="description" class="text-xs text-muted">{{ description }}</span>
71
+ <span v-if="description" :id="descriptionId" class="text-xs text-muted">{{ description }}</span>
59
72
  </span>
60
- <SwitchRoot :model-value="modelValue" :disabled="disabled" :class="TRACK" @update:model-value="onUpdate">
73
+ <SwitchRoot
74
+ v-bind="rest"
75
+ :id="field.id"
76
+ :model-value="modelValue"
77
+ :disabled="field.disabled"
78
+ :aria-describedby="describedBy"
79
+ :class="TRACK"
80
+ @update:model-value="onUpdate"
81
+ >
61
82
  <SwitchThumb :class="THUMB" />
62
83
  </SwitchRoot>
63
84
  <span v-if="labelPosition === 'end'" class="flex min-w-0 flex-col">
64
85
  <slot>{{ label }}</slot>
65
- <span v-if="description" class="text-xs text-muted">{{ description }}</span>
86
+ <span v-if="description" :id="descriptionId" class="text-xs text-muted">{{ description }}</span>
66
87
  </span>
67
88
  </label>
68
89
  <SwitchRoot
69
90
  v-else
91
+ v-bind="rest"
92
+ :id="field.id"
70
93
  :model-value="modelValue"
71
- :disabled="disabled"
94
+ :disabled="field.disabled"
95
+ :aria-describedby="describedBy"
72
96
  :class="rootClass"
73
- v-bind="rest"
74
97
  @update:model-value="onUpdate"
75
98
  >
76
99
  <SwitchThumb :class="THUMB" />
@@ -13,6 +13,8 @@ import LpPagination from "./LpPagination.vue"
13
13
  import LpScrollArea from "./LpScrollArea.vue"
14
14
  import { useAsyncTask } from "../composables/useAsyncTask"
15
15
  import { useRovingFocus } from "../composables/useRovingFocus"
16
+ import { useLabels } from "../composables/useLpConfig"
17
+ import { compareValues } from "./tableSort"
16
18
 
17
19
  export interface TableColumn<Row> {
18
20
  key: string
@@ -21,6 +23,10 @@ export interface TableColumn<Row> {
21
23
  width?: string
22
24
  /** Allow clicking the header to sort by this column. */
23
25
  sortable?: boolean
26
+ /** The value this column sorts by, when the cell holds something else. */
27
+ sortValue?: (row: Row) => unknown
28
+ /** Full control over the order; wins over `sortValue`. Ascending order. */
29
+ compare?: (a: Row, b: Row) => number
24
30
  /**
25
31
  * Offer "show only these values" in the header's menu, built from the values
26
32
  * actually present in the rows. For a column that holds a small vocabulary —
@@ -38,6 +44,19 @@ export interface TableColumn<Row> {
38
44
  alwaysVisible?: boolean
39
45
  }
40
46
 
47
+ export interface TableLabels {
48
+ [key: string]: string
49
+ loading: string
50
+ error: string
51
+ empty: string
52
+ sortAsc: string
53
+ sortDesc: string
54
+ sortClear: string
55
+ filter: string
56
+ showAll: string
57
+ columns: string
58
+ }
59
+
41
60
  export interface SortState {
42
61
  key: string
43
62
  dir: "asc" | "desc"
@@ -68,6 +87,7 @@ export interface TablePage<Row> {
68
87
 
69
88
  type RowKey = string | number
70
89
 
90
+
71
91
  const props = withDefaults(
72
92
  defineProps<{
73
93
  columns: TableColumn<T>[]
@@ -86,7 +106,11 @@ const props = withDefaults(
86
106
  debounce?: number
87
107
  loadingLabel?: string
88
108
  errorLabel?: string
89
- /** Stable id per row — required for selection; falls back to index otherwise. */
109
+ /**
110
+ * Stable id per row. Without it a row is keyed by its position in `rows`,
111
+ * which survives sorting and filtering but not a new array; set it whenever
112
+ * rows are replaced (`load`, polling) and selection must carry over.
113
+ */
90
114
  rowKey?: keyof T | ((row: T) => RowKey)
91
115
  emptyLabel?: string
92
116
  emptyIcon?: string
@@ -111,27 +135,50 @@ const props = withDefaults(
111
135
  filterLabel?: string
112
136
  showAllLabel?: string
113
137
  columnsLabel?: string
138
+ /** All of the wording above in one object, and the config-level scope "table". */
139
+ labels?: Partial<TableLabels>
114
140
  }>(),
115
141
  {
116
142
  rows: () => [],
117
143
  pageSize: 25,
118
144
  debounce: 150,
119
- loadingLabel: "Loading…",
120
- errorLabel: "Could not load",
121
- emptyLabel: "Nothing here yet",
122
145
  emptyIcon: "lucide:inbox",
123
146
  selectable: false,
124
147
  selected: () => [],
125
- sort: null,
126
- sortAscLabel: "Sort ascending",
127
- sortDescLabel: "Sort descending",
128
- sortClearLabel: "Clear sorting",
129
- filterLabel: "Show only",
130
- showAllLabel: "Show all",
131
- columnsLabel: "Columns",
148
+ // undefined, not null: null is a parent saying "unsorted", undefined is no
149
+ // parent at all, and only the latter lets the table keep its own sort.
150
+ sort: undefined,
132
151
  },
133
152
  )
134
153
 
154
+ const labelSet = useLabels<TableLabels>(
155
+ "table",
156
+ {
157
+ loading: "Loading…",
158
+ error: "Could not load",
159
+ empty: "Nothing here yet",
160
+ sortAsc: "Sort ascending",
161
+ sortDesc: "Sort descending",
162
+ sortClear: "Clear sorting",
163
+ filter: "Show only",
164
+ showAll: "Show all",
165
+ columns: "Columns",
166
+ },
167
+ () => props.labels,
168
+ )
169
+ const text = computed<TableLabels>(() => ({
170
+ ...labelSet.value,
171
+ loading: props.loadingLabel ?? labelSet.value.loading,
172
+ error: props.errorLabel ?? labelSet.value.error,
173
+ empty: props.emptyLabel ?? labelSet.value.empty,
174
+ sortAsc: props.sortAscLabel ?? labelSet.value.sortAsc,
175
+ sortDesc: props.sortDescLabel ?? labelSet.value.sortDesc,
176
+ sortClear: props.sortClearLabel ?? labelSet.value.sortClear,
177
+ filter: props.filterLabel ?? labelSet.value.filter,
178
+ showAll: props.showAllLabel ?? labelSet.value.showAll,
179
+ columns: props.columnsLabel ?? labelSet.value.columns,
180
+ }))
181
+
135
182
  /*
136
183
  * Layout classes belong on the root, not on the scroll area. Since the pager
137
184
  * wrapper became the root, a caller's `min-h-0 flex-1` landed on the inner
@@ -156,7 +203,9 @@ const emit = defineEmits<{
156
203
  function keyFor(row: T, index: number): RowKey {
157
204
  if (typeof props.rowKey === "function") return props.rowKey(row)
158
205
  if (props.rowKey) return row[props.rowKey] as RowKey
159
- return index
206
+ // Position in the source, not in the sorted view: a sort must not move a
207
+ // tick from one row to another.
208
+ return sourceIndex.value.get(row) ?? index
160
209
  }
161
210
 
162
211
  function alignClass(align?: "left" | "right" | "center"): string {
@@ -207,7 +256,7 @@ const contextMenu = computed<ContextMenuItemDef[]>(() => {
207
256
  // emitted an event nobody was listening for, and left the rows exactly as they
208
257
  // were: a control that answers a click by doing nothing.
209
258
  const ownSort = ref<SortState | null>(null)
210
- const activeSort = computed(() => props.sort ?? ownSort.value)
259
+ const activeSort = computed(() => (props.sort !== undefined ? props.sort : ownSort.value))
211
260
 
212
261
  // ── server-backed rows ───────────────────────────────────────
213
262
  const loaded = shallowRef<T[]>([])
@@ -219,6 +268,7 @@ const task = useAsyncTask<TablePage<T>>()
219
268
  const loading = task.pending
220
269
 
221
270
  const sourceRows = computed<T[]>(() => (props.load ? loaded.value : props.rows))
271
+ const sourceIndex = computed(() => new Map(sourceRows.value.map((row, i) => [row, i])))
222
272
 
223
273
  // The tallest page shown so far, held as a floor: the last page is usually
224
274
  // short, and without it the pager below jumps up the moment you reach it.
@@ -321,17 +371,13 @@ const displayRows = computed<T[]>(() => {
321
371
  const col = props.columns.find((c) => c.key === s.key)
322
372
  if (!col?.sortable) return filteredRows.value
323
373
  const factor = s.dir === "asc" ? 1 : -1
324
- return [...filteredRows.value].sort((a, b) => compare(a[s.key], b[s.key]) * factor)
374
+ const own = col.compare
375
+ const value = col.sortValue ?? ((row: T) => row[s.key])
376
+ return [...filteredRows.value].sort(
377
+ (a, b) => (own ? own(a, b) : compareValues(value(a), value(b))) * factor,
378
+ )
325
379
  })
326
380
 
327
- function compare(a: unknown, b: unknown): number {
328
- if (a == null && b == null) return 0
329
- if (a == null) return -1
330
- if (b == null) return 1
331
- if (typeof a === "number" && typeof b === "number") return a - b
332
- return String(a).localeCompare(String(b), undefined, { numeric: true })
333
- }
334
-
335
381
  function toggleSort(col: TableColumn<T>) {
336
382
  if (!col.sortable) return
337
383
  const s = activeSort.value
@@ -377,19 +423,19 @@ function headerMenu(col: TableColumn<T>): ContextMenuItemDef[] {
377
423
  if (col.sortable) {
378
424
  items.push(
379
425
  {
380
- label: props.sortAscLabel,
426
+ label: text.value.sortAsc,
381
427
  icon: "lucide:arrow-up",
382
428
  disabled: s?.key === col.key && s.dir === "asc",
383
429
  onSelect: set({ key: col.key, dir: "asc" }),
384
430
  },
385
431
  {
386
- label: props.sortDescLabel,
432
+ label: text.value.sortDesc,
387
433
  icon: "lucide:arrow-down",
388
434
  disabled: s?.key === col.key && s.dir === "desc",
389
435
  onSelect: set({ key: col.key, dir: "desc" }),
390
436
  },
391
437
  {
392
- label: props.sortClearLabel,
438
+ label: text.value.sortClear,
393
439
  icon: "lucide:x",
394
440
  disabled: !s,
395
441
  onSelect: set(null),
@@ -402,7 +448,7 @@ function headerMenu(col: TableColumn<T>): ContextMenuItemDef[] {
402
448
  if (col.filterable) {
403
449
  const hidden = hiddenValues.value[col.key]
404
450
  items.push({
405
- label: props.filterLabel,
451
+ label: text.value.filter,
406
452
  icon: "lucide:filter",
407
453
  separatorBefore: items.length > 0,
408
454
  children: [
@@ -412,7 +458,7 @@ function headerMenu(col: TableColumn<T>): ContextMenuItemDef[] {
412
458
  onSelect: () => toggleValue(col.key, value),
413
459
  })),
414
460
  {
415
- label: props.showAllLabel,
461
+ label: text.value.showAll,
416
462
  icon: "lucide:list",
417
463
  separatorBefore: true,
418
464
  disabled: !hidden?.size,
@@ -435,7 +481,7 @@ function tableMenu(): ContextMenuItemDef[] {
435
481
  if (!offered.length) return []
436
482
  return [
437
483
  {
438
- label: props.columnsLabel,
484
+ label: text.value.columns,
439
485
  icon: "lucide:columns-3",
440
486
  children: offered.map((col) => ({
441
487
  label: col.label,
@@ -462,6 +508,13 @@ function fetchPage(delay = 0) {
462
508
  task.run((signal) => load(query, signal), {
463
509
  delay,
464
510
  apply: (result) => {
511
+ // Rows deleted from the last page leave it past the end; step back to
512
+ // the page that now exists rather than strand the user on an empty one.
513
+ const last = Math.max(1, Math.ceil(result.total / props.pageSize))
514
+ if (page.value > last) {
515
+ page.value = last
516
+ return
517
+ }
465
518
  loaded.value = result.rows
466
519
  total.value = result.total
467
520
  facets.value = result.facets ?? {}
@@ -488,7 +541,8 @@ defineExpose({ reload: () => fetchPage() })
488
541
 
489
542
  // ── selection ────────────────────────────────────────────────
490
543
  const selectedSet = computed(() => new Set(props.selected))
491
- const allKeys = computed(() => sourceRows.value.map((r, i) => keyFor(r, i)))
544
+ // "All" is what the user can see: rows a header filter hides are left alone.
545
+ const allKeys = computed(() => filteredRows.value.map((r, i) => keyFor(r, i)))
492
546
  const allChecked = computed(
493
547
  () => allKeys.value.length > 0 && allKeys.value.every((k) => selectedSet.value.has(k)),
494
548
  )
@@ -497,7 +551,12 @@ const someChecked = computed(
497
551
  )
498
552
 
499
553
  function toggleAll(checked: boolean) {
500
- emit("update:selected", checked ? [...allKeys.value] : [])
554
+ const next = new Set(selectedSet.value)
555
+ for (const key of allKeys.value) {
556
+ if (checked) next.add(key)
557
+ else next.delete(key)
558
+ }
559
+ emit("update:selected", [...next])
501
560
  }
502
561
 
503
562
  function toggleRow(key: RowKey, checked: boolean) {
@@ -507,7 +566,7 @@ function toggleRow(key: RowKey, checked: boolean) {
507
566
  emit("update:selected", [...next])
508
567
  }
509
568
 
510
- const colSpan = computed(() => props.columns.length + (props.selectable ? 1 : 0))
569
+ const colSpan = computed(() => visibleColumns.value.length + (props.selectable ? 1 : 0))
511
570
 
512
571
  /*
513
572
  * Keyboard navigation over the rows. A table is one Tab stop, not one per row:
@@ -703,7 +762,7 @@ const barInsetTop = computed(() =>
703
762
  class="mx-auto mb-2 opacity-60"
704
763
  :class="loading ? 'animate-spin' : ''"
705
764
  />
706
- <div>{{ loading ? loadingLabel : failed ? errorLabel : emptyLabel }}</div>
765
+ <div>{{ loading ? text.loading : failed ? text.error : text.empty }}</div>
707
766
  </td>
708
767
  </tr>
709
768
  <!-- A plain <tr>, because TransitionGroup animates element children and