@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,8 +16,10 @@ export default { inheritAttrs: false }
16
16
  * is filled. Digits-only by default (TOTP); flip `alphanumeric` for backup codes.
17
17
  */
18
18
  import { PinInputInput, PinInputRoot } from "reka-ui"
19
- import { computed } from "vue"
19
+ import { computed, ref, watch } from "vue"
20
+ import { useFieldControl } from "../composables/useField"
20
21
  import { useMergedAttrs } from "../composables/useMergedClass"
22
+ import { joinCells, splitCode, type OtpCells } from "./otp"
21
23
 
22
24
  const props = withDefaults(
23
25
  defineProps<{
@@ -45,13 +47,44 @@ const emit = defineEmits<{
45
47
 
46
48
  // reka models the value as an array of per-cell strings; we bridge to a single
47
49
  // string at the boundary so callers never touch the array shape.
50
+ //
51
+ // The cells are kept here, positions and all, rather than re-split from the
52
+ // string on every render: clearing a middle cell leaves a gap the string cannot
53
+ // express, and re-splitting it shifted every later digit one cell left. The
54
+ // cells are only rebuilt when the model changes from outside.
55
+ const local = ref<string[]>(splitCode(props.modelValue, props.length))
56
+ let emitted = props.modelValue ?? ""
57
+ watch(
58
+ () => props.modelValue ?? "",
59
+ (value) => {
60
+ if (value === emitted) return
61
+ emitted = value
62
+ local.value = splitCode(value, props.length)
63
+ },
64
+ )
65
+ watch(
66
+ () => props.length,
67
+ (length) => {
68
+ local.value = local.value.slice(0, length)
69
+ },
70
+ )
48
71
  const cells = computed<string[]>({
49
- get: () => (props.modelValue ?? "").split("").slice(0, props.length),
50
- set: (arr) => emit("update:modelValue", arr.join("")),
72
+ get: () => local.value,
73
+ set: (arr) => {
74
+ local.value = arr
75
+ emitted = joinCells(arr)
76
+ emit("update:modelValue", emitted)
77
+ },
51
78
  })
52
79
 
53
- function onComplete(arr: string[]) {
54
- emit("complete", arr.join(""))
80
+ const field = useFieldControl(() => ({
81
+ wrapped: false,
82
+ invalid: props.invalid,
83
+ disabled: props.disabled,
84
+ }))
85
+
86
+ function onComplete(arr: OtpCells) {
87
+ emit("complete", joinCells(arr))
55
88
  }
56
89
 
57
90
  const cellSize = {
@@ -71,7 +104,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
71
104
  v-model="cells"
72
105
  :type="alphanumeric ? 'text' : 'number'"
73
106
  :mask="mask"
74
- :disabled="disabled"
107
+ :disabled="field.disabled"
75
108
  otp
76
109
  :class="rootClass"
77
110
  v-bind="rest"
@@ -81,14 +114,16 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
81
114
  v-for="i in length"
82
115
  :key="i"
83
116
  :index="i - 1"
84
- :aria-invalid="invalid || undefined"
117
+ :id="i === 1 ? field.id : undefined"
118
+ :aria-invalid="field.invalid || undefined"
119
+ :aria-describedby="field.describedBy"
85
120
  :class="[
86
121
  cellSize[size],
87
122
  'rounded-control border bg-surface-soft text-center font-semibold text-ink caret-brand',
88
123
  'outline-none transition-colors duration-[var(--duration-fast)]',
89
124
  'focus:border-brand focus:ring-2 focus:ring-ring',
90
125
  'disabled:cursor-not-allowed disabled:opacity-55',
91
- invalid ? 'border-danger focus:border-danger focus:ring-danger-soft' : 'border-line',
126
+ field.invalid ? 'border-danger focus:border-danger focus:ring-danger-soft' : 'border-line',
92
127
  ]"
93
128
  />
94
129
  </PinInputRoot>
@@ -22,6 +22,7 @@ import { computed, useId } from "vue"
22
22
  import { usePillTransition } from "../composables/usePillTransition"
23
23
  import LpIcon from "./LpIcon.vue"
24
24
  import { useMergedAttrs } from "../composables/useMergedClass"
25
+ import { useLabels } from "../composables/useLpConfig"
25
26
 
26
27
  const props = withDefaults(
27
28
  defineProps<{
@@ -35,22 +36,37 @@ const props = withDefaults(
35
36
  /** Pages shown on each side of the current one in the inner run. */
36
37
  siblings?: number
37
38
  disabled?: boolean
39
+ labels?: Partial<Record<"nav" | "previous" | "next", string>>
38
40
  }>(),
39
41
  { page: 1, pageSize: 10, siblings: 1 },
40
42
  )
41
43
 
44
+ const text = useLabels(
45
+ "pagination",
46
+ { nav: "Pagination", previous: "Previous page", next: "Next page" },
47
+ () => props.labels,
48
+ )
49
+
42
50
  const emit = defineEmits<{
43
51
  (e: "update:page", value: number): void
44
52
  (e: "change", value: number): void
45
53
  }>()
46
54
 
47
55
  const count = computed(() => {
48
- if (props.pageCount != null) return Math.max(1, props.pageCount)
49
- if (props.total != null) return Math.max(1, Math.ceil(props.total / props.pageSize))
50
- return 1
56
+ // Guarded so a zero page size or a total still loading never yields an
57
+ // "Infinity" or "NaN" page.
58
+ const n =
59
+ props.pageCount != null
60
+ ? props.pageCount
61
+ : props.total != null && props.pageSize > 0
62
+ ? Math.ceil(props.total / props.pageSize)
63
+ : 1
64
+ return Number.isFinite(n) ? Math.max(1, Math.floor(n)) : 1
51
65
  })
52
66
 
53
- const current = computed(() => Math.min(Math.max(1, props.page), count.value))
67
+ const current = computed(() =>
68
+ Number.isFinite(props.page) ? Math.min(Math.max(1, Math.floor(props.page)), count.value) : 1,
69
+ )
54
70
 
55
71
  // A fixed run of page slots. Each slot is either a real page or a gap ("…").
56
72
  // The number of slots is constant for a given `count`, so the row width is
@@ -122,12 +138,12 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
122
138
  <template>
123
139
  <nav
124
140
  :class="rootClass"
125
- v-bind="rest" aria-label="Pagination">
141
+ v-bind="rest" :aria-label="text.nav">
126
142
  <button
127
143
  type="button"
128
144
  :class="[navBase, navArrow]"
129
145
  :disabled="disabled || current === 1"
130
- aria-label="Previous page"
146
+ :aria-label="text.previous"
131
147
  @click="go(current - 1)"
132
148
  >
133
149
  <LpIcon name="lucide:chevron-left" :size="16" />
@@ -171,7 +187,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
171
187
  type="button"
172
188
  :class="[navBase, navArrow]"
173
189
  :disabled="disabled || current === count"
174
- aria-label="Next page"
190
+ :aria-label="text.next"
175
191
  @click="go(current + 1)"
176
192
  >
177
193
  <LpIcon name="lucide:chevron-right" :size="16" />
@@ -6,6 +6,7 @@ export default { inheritAttrs: false }
6
6
  // Password field = LpInput + a show/hide toggle in its trailing slot.
7
7
  // No duplicated field styling — the shell lives in LpInput.
8
8
  import { ref } from "vue"
9
+ import { useLabels } from "../composables/useLpConfig"
9
10
  import LpIcon from "./LpIcon.vue"
10
11
  import LpInput from "./LpInput.vue"
11
12
 
@@ -23,6 +24,7 @@ defineProps<{
23
24
  }>()
24
25
  defineEmits<{ (e: "update:modelValue", value: string): void }>()
25
26
 
27
+ const text = useLabels("passwordInput", { show: "Show password", hide: "Hide password" })
26
28
  const visible = ref(false)
27
29
  const inner = ref<InstanceType<typeof LpInput> | null>(null)
28
30
  defineExpose({
@@ -35,6 +37,7 @@ defineExpose({
35
37
  <template>
36
38
  <LpInput
37
39
  ref="inner"
40
+ v-bind="$attrs"
38
41
  :model-value="modelValue"
39
42
  :type="visible ? 'text' : 'password'"
40
43
  :placeholder="placeholder"
@@ -51,7 +54,7 @@ defineExpose({
51
54
  <button
52
55
  type="button"
53
56
  class="group rounded-md p-1 text-muted outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink focus-visible:text-ink"
54
- :aria-label="visible ? 'Hide password' : 'Show password'"
57
+ :aria-label="visible ? text.hide : text.show"
55
58
  @click="visible = !visible"
56
59
  >
57
60
  <LpIcon
@@ -27,9 +27,11 @@ import {
27
27
  ComboboxTrigger,
28
28
  ComboboxViewport,
29
29
  } from "reka-ui"
30
- import { computed, onMounted, ref } from "vue"
30
+ import { computed, onMounted, ref, useAttrs } from "vue"
31
+ import { useFieldControl } from "../composables/useField"
31
32
  import { useInputFilter } from "../composables/useInputFilter"
32
- import { type Country, flagEmoji, loadCountries, matchCountryByValue } from "./countries"
33
+ import { useLabels } from "../composables/useLpConfig"
34
+ import { type Country, filterCountries, flagEmoji, loadCountries, matchCountryByValue } from "./countries"
33
35
  import { OPTION_ITEM, POPOVER_PANEL } from "./dropdown"
34
36
  import LpIcon from "./LpIcon.vue"
35
37
  import { useMergedAttrs } from "../composables/useMergedClass"
@@ -74,7 +76,9 @@ const emit = defineEmits<{
74
76
  // Phone keys only: digits, "+", spaces and the usual separators. Free typing of
75
77
  // "+code" stays possible; letters and junk are blocked at the source.
76
78
  const PHONE_CHARS = /[\d+()\-\s]/
77
- const { onBeforeInput, onPaste } = useInputFilter({ restrict: () => PHONE_CHARS })
79
+ const { onBeforeInput, onInput: filterInput, onCompositionEnd, onPaste } = useInputFilter({
80
+ restrict: () => PHONE_CHARS,
81
+ })
78
82
 
79
83
  // The dial-code table is code-split; it's empty until the lazy chunk resolves.
80
84
  // Everything degrades gracefully meanwhile: digits still parse, the flag shows a
@@ -100,13 +104,7 @@ const activeCountry = computed<Country | undefined>(
100
104
  const open = ref(false)
101
105
  const query = ref("")
102
106
 
103
- const filtered = computed(() => {
104
- const q = query.value.trim().toLowerCase()
105
- if (!q) return countries.value
106
- return countries.value.filter(
107
- (c) => c.name.toLowerCase().includes(q) || c.dial.includes(q.replace(/\D/g, "")),
108
- )
109
- })
107
+ const filtered = computed(() => filterCountries(countries.value, query.value))
110
108
 
111
109
  function parse(value: string): PhoneDetail {
112
110
  const country = matchCountryByValue(value, countries.value)
@@ -123,6 +121,7 @@ function setValue(value: string) {
123
121
  }
124
122
 
125
123
  function onInput(e: Event) {
124
+ filterInput(e)
126
125
  setValue((e.target as HTMLInputElement).value)
127
126
  }
128
127
 
@@ -139,16 +138,22 @@ function chooseCountry(c: Country) {
139
138
  query.value = ""
140
139
  }
141
140
 
142
- // Load the dial-code table on mount (lazy chunk), then seed the default
143
- // country's "+code" into an empty field so the user starts with the prefix in
144
- // place — but never clobber a value the consumer already provided.
145
141
  onMounted(async () => {
146
142
  allCountries.value = await loadCountries()
147
- if (!props.modelValue && fallback.value) {
148
- emit("update:modelValue", `+${fallback.value.dial} `)
149
- }
150
143
  })
151
144
 
145
+ // The default country's "+code" is put in place when the user enters an empty
146
+ // field, and taken back out if they leave without typing a number. Seeding it
147
+ // on mount instead made every untouched optional field submit "+380 ", pass
148
+ // `required`, and read as edited.
149
+ const seeded = computed(() => (fallback.value ? `+${fallback.value.dial} ` : ""))
150
+ function onFocus() {
151
+ if (!props.modelValue && seeded.value) setValue(seeded.value)
152
+ }
153
+ function onBlur() {
154
+ if (seeded.value && props.modelValue?.trim() === seeded.value.trim()) setValue("")
155
+ }
156
+
152
157
  const sizeClass = {
153
158
  sm: "h-(--size-control-sm) text-xs",
154
159
  md: "h-(--size-control-md) text-sm",
@@ -157,23 +162,37 @@ const sizeClass = {
157
162
 
158
163
  // The control is full-width with its own frame — the two things a consumer
159
164
  // placing it in a narrow column has to change, so `class="w-48"` must win.
165
+ // style lays out the frame; every other attr (name, autocomplete, @blur…)
166
+ // belongs to the number <input>.
167
+ const attrs = useAttrs()
168
+ const field = useFieldControl(() => ({
169
+ wrapped: false,
170
+ id: typeof attrs.id === "string" ? attrs.id : undefined,
171
+ invalid: props.invalid,
172
+ disabled: props.disabled,
173
+ }))
174
+ const text = useLabels("phoneInput", { selectCountry: "Select country" })
160
175
  const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
161
176
  [
162
177
  "flex w-full items-stretch rounded-control border bg-surface-soft text-ink transition-colors duration-[var(--duration-fast)] focus-within:ring-2 focus-within:ring-ring",
163
178
  sizeClass[props.size],
164
- props.invalid
179
+ field.value.invalid
165
180
  ? "border-danger focus-within:border-danger focus-within:ring-danger-soft"
166
181
  : "border-line focus-within:border-brand",
167
- props.disabled ? "cursor-not-allowed opacity-55" : "",
182
+ field.value.disabled ? "cursor-not-allowed opacity-55" : "",
168
183
  ].join(" "),
169
184
  )
185
+ const inputAttrs = computed(() => {
186
+ const { style: _s, ...others } = rest.value
187
+ return others
188
+ })
170
189
  </script>
171
190
 
172
191
  <template>
173
192
  <div
174
193
  data-lp-ring-owner
175
194
  :class="rootClass"
176
- v-bind="rest"
195
+ :style="attrs.style as never"
177
196
  >
178
197
  <!-- reset-search-term-on-{select,blur}: as in LpSelect, the trigger shows
179
198
  the selection and the box only filters, so reka seeding the chosen value
@@ -184,15 +203,15 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
184
203
  :ignore-filter="true"
185
204
  :reset-search-term-on-select="false"
186
205
  :reset-search-term-on-blur="false"
187
- :disabled="disabled"
206
+ :disabled="field.disabled"
188
207
  class="flex"
189
208
  >
190
209
  <ComboboxAnchor as-child>
191
210
  <ComboboxTrigger
192
211
  type="button"
193
- :disabled="disabled"
194
- class="flex shrink-0 items-center gap-1 rounded-l-control border-r border-line px-2.5 outline-none hover:bg-white/[0.04] data-[state=open]:bg-white/[0.04]"
195
- aria-label="Select country"
212
+ :disabled="field.disabled"
213
+ class="flex shrink-0 items-center gap-1 rounded-l-control border-r border-line px-2.5 outline-none hover:bg-ink/5 data-[state=open]:bg-ink/5"
214
+ :aria-label="text.selectCountry"
196
215
  >
197
216
  <span class="text-base leading-none">{{ activeCountry ? flagEmoji(activeCountry.iso2) : "🌐" }}</span>
198
217
  <LpIcon
@@ -247,17 +266,24 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
247
266
  </ComboboxRoot>
248
267
 
249
268
  <input
250
- :value="modelValue"
251
- :placeholder="placeholder"
252
- :disabled="disabled"
253
- :aria-invalid="invalid || undefined"
254
269
  type="tel"
255
270
  inputmode="tel"
256
271
  autocomplete="tel"
272
+ v-bind="inputAttrs"
273
+ :id="field.id"
274
+ :value="modelValue"
275
+ :placeholder="placeholder"
276
+ :disabled="field.disabled"
277
+ :required="field.required || undefined"
278
+ :aria-invalid="field.invalid || undefined"
279
+ :aria-describedby="field.describedBy"
257
280
  class="lp-autofill min-w-0 flex-1 self-stretch bg-transparent px-3 outline-none placeholder:text-muted"
258
281
  @beforeinput="onBeforeInput"
259
282
  @paste="onPaste"
283
+ @compositionend="onCompositionEnd"
260
284
  @input="onInput"
285
+ @focus="onFocus"
286
+ @blur="onBlur"
261
287
  />
262
288
  </div>
263
289
  </template>
@@ -31,6 +31,16 @@ import LpRollingText from "./LpRollingText.vue"
31
31
  import LpShift from "./LpShift.vue"
32
32
  import LpTooltip from "./LpTooltip.vue"
33
33
  import { useMergedAttrs } from "../composables/useMergedClass"
34
+ import { useLabels } from "../composables/useLpConfig"
35
+
36
+ export interface PieText {
37
+ [key: string]: string
38
+ empty: string
39
+ other: string
40
+ total: string
41
+ /** "{n} selected"; `{n}` is replaced by the count. */
42
+ selected: string
43
+ }
34
44
 
35
45
  /** A slice, as reported by every interaction event. */
36
46
  export interface SliceEvent {
@@ -107,21 +117,26 @@ const props = withDefaults(
107
117
  */
108
118
  menuItems?: (slice: SliceEvent, selection: SliceEvent[]) => ContextMenuItemDef[]
109
119
  emptyLabel?: string
120
+ /** Accessible name for the chart. Defaults to a generated summary. */
121
+ ariaLabel?: string
122
+ /** Render the slices as a visually hidden table for screen readers. */
123
+ dataTable?: boolean
124
+ /** Text overrides (`labels` is taken by the callout switch). */
125
+ text?: Partial<PieText>
110
126
  }>(),
111
127
  {
112
128
  type: "donut",
113
129
  size: 180,
114
130
  innerRatio: 0.62,
115
131
  maxSlices: 6,
116
- otherLabel: "Other",
117
132
  showPercent: true,
118
133
  showTotal: true,
119
134
  labelWidth: 140,
120
135
  padAngle: 1.5,
121
136
  cornerRadius: 4,
122
137
  hoverOffset: 8,
123
- emptyLabel: "No data",
124
138
  animate: true,
139
+ dataTable: false,
125
140
  duration: 900,
126
141
  },
127
142
  )
@@ -134,11 +149,17 @@ const emit = defineEmits<{
134
149
  (e: "sliceContextMenu", payload: SliceEvent): void
135
150
  }>()
136
151
 
152
+ const strings = useLabels<PieText>(
153
+ "pie",
154
+ { empty: "No data", other: "Other", total: "total", selected: "{n} selected" },
155
+ () => props.text,
156
+ )
157
+
137
158
  const resolved = computed(() =>
138
159
  resolveSlices(
139
160
  props.slices,
140
161
  props.maxSlices || undefined,
141
- props.otherLabel,
162
+ props.otherLabel ?? strings.value.other,
142
163
  !props.sortBySize,
143
164
  ),
144
165
  )
@@ -336,6 +357,23 @@ const dragging = ref(false)
336
357
 
337
358
  const isSelected = (i: number) => selected.value.has(i)
338
359
 
360
+ /*
361
+ * Indices only mean something against the slices they were taken from. When
362
+ * the data changes shape, a selection or hover pointing past the end would read
363
+ * a slice that no longer exists, so anything out of range is dropped.
364
+ */
365
+ watch(
366
+ () => arcs.value.length,
367
+ (n) => {
368
+ if (active.value >= n) active.value = -1
369
+ if (anchor.value >= n) anchor.value = -1
370
+ if ([...selected.value].some((i) => i >= n)) {
371
+ selected.value = new Set([...selected.value].filter((i) => i < n))
372
+ emitSelection()
373
+ }
374
+ },
375
+ )
376
+
339
377
  const payloadFor = (i: number) => {
340
378
  const a = arcs.value[i]
341
379
  return { index: i, label: a.label, value: a.value, isOther: a.isOther }
@@ -354,7 +392,9 @@ const rangeTo = (i: number) => {
354
392
  return next
355
393
  }
356
394
 
357
- const onSlicePointerDown = (i: number, e: PointerEvent) => {
395
+ type Modifiers = Pick<MouseEvent, "button" | "shiftKey" | "ctrlKey" | "metaKey">
396
+
397
+ const onSlicePointerDown = (i: number, e: Modifiers, drag = true) => {
358
398
  // Right-click opens a menu; it must not disturb a selection the menu acts on.
359
399
  if (e.button === 2) return
360
400
 
@@ -372,12 +412,18 @@ const onSlicePointerDown = (i: number, e: PointerEvent) => {
372
412
  const only = selected.value.size === 1 && selected.value.has(i)
373
413
  selected.value = only ? new Set() : new Set([i])
374
414
  anchor.value = only ? -1 : i
375
- if (!only) beginDrag()
415
+ if (!only && drag) beginDrag()
376
416
  }
377
417
  emitSelection()
378
418
  emit("sliceClick", payloadFor(i))
379
419
  }
380
420
 
421
+ /* Enter and Space on a legend entry arrive as a click with no pointer behind
422
+ it (detail 0); pointer clicks were already handled on pointerdown. */
423
+ const onLegendClick = (i: number, e: MouseEvent) => {
424
+ if (e.detail === 0) onSlicePointerDown(i, e, false)
425
+ }
426
+
381
427
  /** Drag across slices to gather them — same gesture as a rubber-band in a list. */
382
428
  const onSlicePointerEnter = (i: number) => {
383
429
  clearTimeout(leaveTimer)
@@ -516,8 +562,10 @@ const onSliceContextMenu = (i: number) => {
516
562
 
517
563
  /** The slice the centre reads out: the hovered one, else a lone selection. */
518
564
  const focused = computed(() => {
519
- if (active.value >= 0) return active.value
520
- return selected.value.size === 1 ? [...selected.value][0] : -1
565
+ const n = arcs.value.length
566
+ if (active.value >= 0 && active.value < n) return active.value
567
+ const only = selected.value.size === 1 ? [...selected.value][0] : -1
568
+ return only < n ? only : -1
521
569
  })
522
570
 
523
571
  /** Unselected slices only recede once something is actually selected. */
@@ -526,11 +574,14 @@ const dimmed = (i: number) => selected.value.size > 0 && !selected.value.has(i)
526
574
  const total = computed(() => resolved.value.total)
527
575
 
528
576
  const selectedArcs = computed(() =>
529
- [...selected.value].sort((x, y) => x - y).map((i) => arcs.value[i]),
577
+ [...selected.value]
578
+ .sort((x, y) => x - y)
579
+ .filter((i) => i < arcs.value.length)
580
+ .map((i) => arcs.value[i]),
530
581
  )
531
582
 
532
583
  /** The slice under the pointer; null over the gaps, which disables the tip. */
533
- const hovered = computed(() => (active.value >= 0 ? arcs.value[active.value] : null))
584
+ const hovered = computed(() => arcs.value[active.value] ?? null)
534
585
 
535
586
  /* Sticky copy for the tooltip's own contents: `hovered` drops to null the
536
587
  instant the pointer leaves a slice, and reading it directly would blank the
@@ -550,10 +601,22 @@ const centreValue = computed(() => {
550
601
 
551
602
  const centreLabel = computed(() => {
552
603
  if (focused.value >= 0) return arcs.value[focused.value].label
553
- if (selected.value.size > 1) return `${selected.value.size} selected`
554
- return "total"
604
+ if (selected.value.size > 1)
605
+ return strings.value.selected.replace("{n}", String(selected.value.size))
606
+ return strings.value.total
555
607
  })
556
608
 
609
+ const summary = computed(
610
+ () =>
611
+ props.ariaLabel ??
612
+ [
613
+ props.title,
614
+ ...arcs.value.map((a) => `${a.label}: ${fmt(a.value)} (${pct(a.fraction)})`),
615
+ ]
616
+ .filter(Boolean)
617
+ .join(", "),
618
+ )
619
+
557
620
  // The pie sets its own gap between chart and legend, which a narrow card has to close up.
558
621
  const { class: rootClass, attrs: rest } = useMergedAttrs(
559
622
  "flex flex-col gap-3",
@@ -575,7 +638,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
575
638
  :style="{ width: `${size}px`, height: `${size}px` }"
576
639
  >
577
640
  <LpIcon name="lucide:chart-pie" :size="18" />
578
- <span class="text-xs">{{ emptyLabel }}</span>
641
+ <span class="text-xs">{{ emptyLabel ?? strings.empty }}</span>
579
642
  </div>
580
643
 
581
644
  <!-- items-start, not items-center: an unfolded legend can be taller than
@@ -644,6 +707,8 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
644
707
  :width="canvasWidth"
645
708
  :height="canvasHeight"
646
709
  class="touch-none select-none"
710
+ role="img"
711
+ :aria-label="summary"
647
712
  @pointerleave="leaveChart"
648
713
  @pointerup="endDrag"
649
714
  @click.self="clearSelection"
@@ -672,7 +737,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
672
737
  <g :clip-path="animate ? `url(#${sweepId}-sweep)` : undefined">
673
738
  <path
674
739
  v-for="(a, i) in arcs"
675
- :key="a.label"
740
+ :key="`${i}-${a.label}`"
676
741
  :d="a.d"
677
742
  :fill="a.color"
678
743
  class="pointer-events-none transition-[transform,opacity] duration-medium ease-[var(--ease-emphasized)] motion-reduce:transition-none"
@@ -684,7 +749,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
684
749
  <!-- Invisible, unmoving, and on top: the actual hit area. -->
685
750
  <path
686
751
  v-for="(a, i) in arcs"
687
- :key="`hit-${a.label}`"
752
+ :key="`hit-${i}-${a.label}`"
688
753
  :d="a.d"
689
754
  fill="transparent"
690
755
  class="cursor-pointer"
@@ -765,7 +830,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
765
830
  <div v-if="legendShown" class="flex min-w-0 flex-col gap-1.5" @pointerup="endDrag">
766
831
  <button
767
832
  v-for="(a, i) in arcs"
768
- :key="a.label"
833
+ :key="`${i}-${a.label}`"
769
834
  type="button"
770
835
  class="group flex cursor-pointer items-center gap-2 rounded-md px-1 py-0.5 text-xs outline-none transition-[opacity,background-color] duration-fast focus-visible:ring-2 focus-visible:ring-ring"
771
836
  :class="[dimmed(i) ? 'opacity-45' : '', isSelected(i) ? 'bg-surface-soft' : '']"
@@ -773,6 +838,9 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
773
838
  @pointerenter="onSlicePointerEnter(i)"
774
839
  @pointerleave="onSlicePointerLeave(i)"
775
840
  @pointerdown="onSlicePointerDown(i, $event)"
841
+ @click="onLegendClick(i, $event)"
842
+ @focus="active = i"
843
+ @blur="active === i && (active = -1)"
776
844
  @contextmenu="onSliceContextMenu(i)"
777
845
  >
778
846
  <span
@@ -789,5 +857,16 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
789
857
  </div>
790
858
  </div>
791
859
  </LpContextMenu>
860
+
861
+ <table v-if="dataTable && hasData" class="sr-only">
862
+ <caption v-if="title">{{ title }}</caption>
863
+ <tbody>
864
+ <tr v-for="(a, i) in arcs" :key="`${i}-${a.label}`">
865
+ <th scope="row">{{ a.label }}</th>
866
+ <td>{{ fmt(a.value) }}</td>
867
+ <td>{{ pct(a.fraction) }}</td>
868
+ </tr>
869
+ </tbody>
870
+ </table>
792
871
  </div>
793
872
  </template>
@@ -14,6 +14,8 @@ withDefaults(
14
14
  sideOffset?: number
15
15
  /** Extra classes for the floating panel (width, padding, overflow). */
16
16
  panelClass?: string
17
+ /** Accessible name of the panel, which reka exposes as a dialog. */
18
+ ariaLabel?: string
17
19
  }>(),
18
20
  { side: "bottom", align: "center", sideOffset: 6 },
19
21
  )
@@ -33,6 +35,7 @@ const open = defineModel<boolean>("open")
33
35
  :side="side"
34
36
  :align="align"
35
37
  :side-offset="sideOffset"
38
+ :aria-label="ariaLabel"
36
39
  :class="[POPOVER_PANEL, 'z-(--z-popover) w-64 rounded-card p-3 text-sm text-ink outline-none', panelClass]"
37
40
  >
38
41
  <slot />
@@ -166,9 +166,11 @@ watch(
166
166
  () => runSweep(props.value, props.duration),
167
167
  )
168
168
 
169
- const pct = computed(() =>
170
- Math.max(0, Math.min(100, (shown.value / props.max) * 100)),
171
- )
169
+ // A zero or missing max (no quota known yet) reads as empty, not as NaN%.
170
+ const pct = computed(() => {
171
+ const ratio = props.max > 0 ? (shown.value / props.max) * 100 : 0
172
+ return Number.isFinite(ratio) ? Math.max(0, Math.min(100, ratio)) : 0
173
+ })
172
174
 
173
175
  /*
174
176
  * The tone actually painted. `auto` reads the fill and crosses two thresholds;
@@ -7,6 +7,7 @@ export default { inheritAttrs: false }
7
7
 
8
8
  <script setup lang="ts">
9
9
  import { RadioGroupIndicator, RadioGroupItem, RadioGroupRoot } from "reka-ui"
10
+ import { useFieldControl } from "../composables/useField"
10
11
  import { useMergedAttrs } from "../composables/useMergedClass"
11
12
 
12
13
  export interface RadioOption {
@@ -17,9 +18,11 @@ export interface RadioOption {
17
18
  // `options` renders simple {value,label} rows. For rich items (icons, badges,
18
19
  // multi-line) pass <LpRadio> children in the default slot instead — they share
19
20
  // the same RadioGroupRoot, so selection still flows through v-model.
20
- defineProps<{ modelValue?: string; options?: RadioOption[]; disabled?: boolean }>()
21
+ const props = defineProps<{ modelValue?: string; options?: RadioOption[]; disabled?: boolean }>()
21
22
  defineEmits<{ (e: "update:modelValue", value: string): void }>()
22
23
 
24
+ const field = useFieldControl(() => ({ wrapped: false, disabled: props.disabled }))
25
+
23
26
  // A radio group stacks with a fixed gap; a dense settings list needs it smaller, and a horizontal layout needs it gone.
24
27
  const { class: rootClass, attrs: rest } = useMergedAttrs(
25
28
  "flex flex-col gap-2",
@@ -29,7 +32,11 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
29
32
  <template>
30
33
  <RadioGroupRoot
31
34
  :model-value="modelValue"
32
- :disabled="disabled"
35
+ :disabled="field.disabled"
36
+ :required="field.required"
37
+ :aria-labelledby="field.labelId"
38
+ :aria-describedby="field.describedBy"
39
+ :aria-invalid="field.invalid || undefined"
33
40
  :class="rootClass"
34
41
  v-bind="rest"
35
42
  @update:model-value="(v) => $emit('update:modelValue', v as string)"