@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
@@ -18,6 +18,7 @@ export default { inheritAttrs: false }
18
18
  */
19
19
  import { Motion } from "motion-v"
20
20
  import { computed, ref, shallowRef, watch, onMounted, onBeforeUnmount } from "vue"
21
+ import { useLabels } from "../composables/useLpConfig"
21
22
  import { easeTravel, prefersReducedMotion } from "../composables/easing"
22
23
  import { usePillTransition } from "../composables/usePillTransition"
23
24
  import { useReveal } from "../composables/useReveal"
@@ -25,6 +26,8 @@ import {
25
26
  areaPath,
26
27
  axisTicks,
27
28
  barPath,
29
+ chartSummary,
30
+ markerIndex,
28
31
  extent,
29
32
  formatTick,
30
33
  linePath,
@@ -38,13 +41,23 @@ import {
38
41
  xAt,
39
42
  yAt,
40
43
  type Box,
44
+ type ChartMarker,
41
45
  type ChartSeries,
46
+ type ChartThreshold,
42
47
  } from "./chart"
43
48
  import LpContextMenu, { type ContextMenuItemDef } from "./LpContextMenu.vue"
44
49
  import LpIcon from "./LpIcon.vue"
45
50
  import LpShift from "./LpShift.vue"
46
51
  import { useMergedAttrs } from "../composables/useMergedClass"
47
52
 
53
+ export interface ChartText {
54
+ [key: string]: string
55
+ empty: string
56
+ rightAxis: string
57
+ /** Prefix of the keyboard readout, before the point's label. */
58
+ point: string
59
+ }
60
+
48
61
  /** What was under the pointer when a point was clicked or right-clicked. */
49
62
  export interface ChartPointEvent {
50
63
  index: number
@@ -94,6 +107,16 @@ const props = withDefaults(
94
107
  animate?: boolean
95
108
  /** Shown when every series is empty. */
96
109
  emptyLabel?: string
110
+ /** Reference lines and bands (SLOs, budgets, alert levels). */
111
+ thresholds?: ChartThreshold[]
112
+ /** Vertical markers at x positions (deploys, incidents). */
113
+ markers?: ChartMarker[]
114
+ /** Accessible name for the plot. Defaults to a generated summary. */
115
+ ariaLabel?: string
116
+ /** Render the data as a visually hidden table for screen readers. */
117
+ dataTable?: boolean
118
+ /** Text overrides (`labels` is taken by the x axis). */
119
+ text?: Partial<ChartText>
97
120
  /**
98
121
  * Right-click menu for a point, built from whatever was hit. Return [] to
99
122
  * leave the browser's own menu alone for that point. Which actions a point
@@ -113,10 +136,18 @@ const props = withDefaults(
113
136
  showLegend: true,
114
137
  yTicks: 4,
115
138
  animate: true,
116
- emptyLabel: "No data",
139
+ thresholds: () => [],
140
+ markers: () => [],
141
+ dataTable: false,
117
142
  },
118
143
  )
119
144
 
145
+ const strings = useLabels<ChartText>(
146
+ "chart",
147
+ { empty: "No data", rightAxis: "Measured on the right axis", point: "Point" },
148
+ () => props.text,
149
+ )
150
+
120
151
  const emit = defineEmits<{
121
152
  /** Click on a point/bar. */
122
153
  (e: "pointClick", payload: ChartPointEvent): void
@@ -139,12 +170,16 @@ let ro: ResizeObserver | null = null
139
170
  * two combine into one question: should this draw itself right now?
140
171
  */
141
172
  const { el: revealAnchor, revealed } = useReveal({ immediate: () => !props.animate })
142
- const drawing = computed(() => props.animate && revealed.value)
173
+ // SMIL ignores the reduced-motion media query, so the wipe is skipped here.
174
+ const reduced = ref(false)
175
+ const still = computed(() => !props.animate || reduced.value)
176
+ const drawing = computed(() => !still.value && revealed.value)
143
177
 
144
178
  onMounted(() => {
145
179
  // One element, two observers: hand the composable the same node rather than
146
180
  // wrapping the chart in a spare div just to carry a second ref.
147
181
  revealAnchor.value = root.value
182
+ reduced.value = prefersReducedMotion()
148
183
  if (!root.value || typeof ResizeObserver === "undefined") return
149
184
  ro = new ResizeObserver(([entry]) => {
150
185
  width.value = entry.contentRect.width
@@ -165,16 +200,19 @@ onBeforeUnmount(() => {
165
200
  // ── data ───────────────────────────────────────
166
201
  const hidden = ref(new Set<string>())
167
202
 
168
- const toggleSeries = (name: string) => {
203
+ const toggleSeries = (id: string) => {
169
204
  const next = new Set(hidden.value)
170
- if (next.has(name)) next.delete(name)
171
- else next.add(name)
205
+ if (next.has(id)) next.delete(id)
206
+ else next.add(id)
172
207
  // Never hide the last visible series — an empty plot reads as broken.
173
208
  if (next.size < props.series.length) hidden.value = next
174
209
  }
175
210
 
176
- const allSeries = computed(() => normalise(props.series))
177
- const visible = computed(() => allSeries.value.filter((s) => !hidden.value.has(s.name)))
211
+ // Identity is position plus name, so two series sharing a name stay separate.
212
+ const allSeries = computed(() =>
213
+ normalise(props.series).map((s, i) => ({ ...s, id: `${i}:${s.name}` })),
214
+ )
215
+ const visible = computed(() => allSeries.value.filter((s) => !hidden.value.has(s.id)))
178
216
 
179
217
  /*
180
218
  * How present each series is, 1 to 0, driven per frame rather than by CSS.
@@ -206,7 +244,7 @@ let presenceFrame: number | undefined
206
244
  * reported the end state — so a series being hidden jumped to the floor while
207
245
  * one being shown eased up from it.
208
246
  */
209
- const presenceOf = (name: string) => presence.value.get(name) ?? 1
247
+ const presenceOf = (id: string) => presence.value.get(id) ?? 1
210
248
 
211
249
  watch(
212
250
  // A string, not the Set: `hidden` is replaced wholesale on every toggle, and a
@@ -214,15 +252,15 @@ watch(
214
252
  // and sees a change it cannot describe — or, with a stale copy in the array,
215
253
  // no change at all. The names that are currently off say it exactly.
216
254
  () =>
217
- allSeries.value.map((s) => `${s.name}:${hidden.value.has(s.name) ? 0 : 1}`).join(","),
255
+ allSeries.value.map((s) => `${s.id}:${hidden.value.has(s.id) ? 0 : 1}`).join(","),
218
256
  () => {
219
- const targets = new Map(allSeries.value.map((s) => [s.name, hidden.value.has(s.name) ? 0 : 1]))
257
+ const targets = new Map(allSeries.value.map((s) => [s.id, hidden.value.has(s.id) ? 0 : 1]))
220
258
  if (!props.animate || prefersReducedMotion()) {
221
259
  presence.value = targets
222
260
  return
223
261
  }
224
262
 
225
- const from = new Map(allSeries.value.map((s) => [s.name, presenceOf(s.name)]))
263
+ const from = new Map(allSeries.value.map((s) => [s.id, presenceOf(s.id)]))
226
264
  if ([...targets].every(([name, to]) => from.get(name) === to)) return
227
265
 
228
266
  const start = performance.now()
@@ -299,12 +337,27 @@ const hasRightAxis = computed(
299
337
  () => declaredRight.value.length > 0 && declaredLeft.value.length > 0,
300
338
  )
301
339
 
340
+ /** A reference level must be on the plot, or it says nothing. */
341
+ const withThresholds = (raw: { min: number; max: number }, axis: "left" | "right") => {
342
+ let { min, max } = raw
343
+ for (const t of props.thresholds) {
344
+ if ((t.axis ?? "left") !== axis) continue
345
+ for (const v of [t.value, t.to]) {
346
+ if (v == null || !Number.isFinite(v)) continue
347
+ min = Math.min(min, v)
348
+ max = Math.max(max, v)
349
+ }
350
+ }
351
+ return { min, max }
352
+ }
353
+
302
354
  const scaleFor = (members: typeof visible.value) => {
303
355
  const totals = stackTotals.value
304
356
  const raw = totals
305
357
  ? { min: Math.min(0, ...totals.neg), max: Math.max(0, ...totals.pos) }
306
358
  : extent(members, zeroBased.value)
307
- return niceScale(raw.min, raw.max, props.yTicks)
359
+ const wide = hasData.value ? withThresholds(raw, "left") : raw
360
+ return niceScale(wide.min, wide.max, props.yTicks)
308
361
  }
309
362
 
310
363
  const targetScale = computed(() =>
@@ -367,7 +420,7 @@ const targetRightScale = computed(() => {
367
420
  axis holds the range it had — an axis that keeps its place while its curve
368
421
  leaves, rather than collapsing to a degenerate 0..1 behind it. */
369
422
  const members = onRight.value.length ? onRight.value : declaredRight.value
370
- const raw = extent(members, zeroBased.value)
423
+ const raw = withThresholds(extent(members, zeroBased.value), "right")
371
424
  /* Steps come from the TARGET left scale. Reading the animated one rebuilt this
372
425
  axis from a fractional range every frame, so a right-hand series jittered
373
426
  against a scale being redrawn underneath it. */
@@ -588,7 +641,7 @@ const baseY = computed(() =>
588
641
  const lines = computed(() =>
589
642
  allSeries.value.map((s) => {
590
643
  const g = geoOf(s)
591
- const p = presenceOf(s.name)
644
+ const p = presenceOf(s.id)
592
645
  // Values pulled toward the axis floor, so the curve flattens into it.
593
646
  const base = Math.min(Math.max(0, g.min), g.max)
594
647
  const values = p === 1 ? s.values : s.values.map((v) => (v == null ? null : base + (v - base) * p))
@@ -660,7 +713,7 @@ const bars = computed(() => {
660
713
  h = Math.abs(top - base)
661
714
  }
662
715
  out.push({
663
- key: `${s.name}-${i}`,
716
+ key: `${s.id}-${i}`,
664
717
  x,
665
718
  y,
666
719
  w: Math.max(1, slot - (props.stacked ? 0 : 2)),
@@ -711,6 +764,7 @@ const active = ref(-1)
711
764
  const pointer = ref({ x: 0, y: 0 })
712
765
 
713
766
  const onMove = (e: PointerEvent) => {
767
+ keyboard.value = false
714
768
  const el = e.currentTarget as SVGElement
715
769
  const rect = el.getBoundingClientRect()
716
770
  const x = e.clientX - rect.left
@@ -729,7 +783,11 @@ const pointEvent = (originalEvent: MouseEvent): ChartPointEvent => ({
729
783
  originalEvent,
730
784
  })
731
785
 
786
+ /* A tap fires pointerleave before click, which has already cleared the hover,
787
+ so the index comes from the click itself. */
732
788
  const onClick = (e: MouseEvent) => {
789
+ const rect = (e.currentTarget as SVGElement).getBoundingClientRect()
790
+ active.value = nearestIndex(e.clientX - rect.left, geo.value)
733
791
  if (active.value < 0) return
734
792
  emit("pointClick", pointEvent(e))
735
793
  }
@@ -772,6 +830,7 @@ const activePoints = computed(() => {
772
830
  const sc = scaleOf(s)
773
831
  return {
774
832
  color: s.color,
833
+ id: s.id,
775
834
  name: s.name,
776
835
  value: v,
777
836
  y: yAt(v, sc.min, sc.max, box.value),
@@ -783,6 +842,7 @@ const tooltipRows = computed(() =>
783
842
  active.value < 0
784
843
  ? []
785
844
  : visible.value.map((s) => ({
845
+ id: s.id,
786
846
  name: s.name,
787
847
  color: s.color,
788
848
  text:
@@ -825,12 +885,13 @@ const floatTransition = usePillTransition("float")
825
885
  // One clip rect wipes across the plot; re-keyed on data change so an update
826
886
  // replays the reveal instead of snapping.
827
887
  const revealKey = shallowRef(0)
888
+ /* Keyed on the chart's SHAPE, not its values: a polled dashboard refreshing
889
+ its numbers every few seconds must not re-wipe on every tick. */
828
890
  watch(
829
- () => [props.series, props.type] as const,
891
+ () => `${props.type}|${count.value}|${allSeries.value.map((s) => s.id).join(",")}`,
830
892
  () => {
831
893
  revealKey.value++
832
894
  },
833
- { deep: true },
834
895
  )
835
896
 
836
897
  const clipId = `lp-chart-${Math.random().toString(36).slice(2, 9)}`
@@ -854,6 +915,103 @@ const clipId = `lp-chart-${Math.random().toString(36).slice(2, 9)}`
854
915
  const gradientId = (name: string, index: number) =>
855
916
  `${clipId}-g-${index}-${name.replace(/[^a-zA-Z0-9_-]/g, "-")}`
856
917
 
918
+ // ── overlays ───────────────────────────────────
919
+ const thresholdMarks = computed(() =>
920
+ props.thresholds.flatMap((t, i) => {
921
+ if (!Number.isFinite(t.value) || width.value <= 0) return []
922
+ const sc = t.axis === "right" && rightScale.value ? rightScale.value : scale.value
923
+ const y = yAt(t.value, sc.min, sc.max, box.value)
924
+ const to = t.to != null && Number.isFinite(t.to) ? yAt(t.to, sc.min, sc.max, box.value) : null
925
+ return [
926
+ {
927
+ key: i,
928
+ y,
929
+ top: to == null ? y : Math.min(y, to),
930
+ height: to == null ? 0 : Math.abs(to - y),
931
+ label: t.label,
932
+ color: t.color ?? "var(--color-line-strong)",
933
+ solid: !!t.solid,
934
+ },
935
+ ]
936
+ }),
937
+ )
938
+
939
+ const markerMarks = computed(() =>
940
+ props.markers.flatMap((m, i) => {
941
+ const index = markerIndex(m.at, labels.value, count.value)
942
+ if (index < 0 || width.value <= 0) return []
943
+ return [
944
+ {
945
+ key: i,
946
+ x: xAt(index, count.value, box.value, banded.value),
947
+ label: m.label,
948
+ color: m.color ?? "var(--color-line-strong)",
949
+ },
950
+ ]
951
+ }),
952
+ )
953
+
954
+ // ── keyboard & assistive text ──────────────────
955
+ const summary = computed(
956
+ () => props.ariaLabel ?? chartSummary(visible.value, labels.value, fmtSeries, props.title),
957
+ )
958
+
959
+ /* Arrow keys walk the points the way the pointer does; the readout below is
960
+ announced politely so each step is heard without stealing focus. */
961
+ const keyboard = ref(false)
962
+
963
+ const onKeydown = (e: KeyboardEvent) => {
964
+ if (!count.value || !hasData.value) return
965
+ const last = count.value - 1
966
+ let next: number
967
+ switch (e.key) {
968
+ case "ArrowRight":
969
+ next = active.value < 0 ? 0 : Math.min(last, active.value + 1)
970
+ break
971
+ case "ArrowLeft":
972
+ next = active.value < 0 ? last : Math.max(0, active.value - 1)
973
+ break
974
+ case "Home":
975
+ next = 0
976
+ break
977
+ case "End":
978
+ next = last
979
+ break
980
+ case "Enter":
981
+ case " ":
982
+ if (active.value < 0) return
983
+ e.preventDefault()
984
+ emit("pointClick", pointEvent(new MouseEvent("click")))
985
+ return
986
+ case "Escape":
987
+ active.value = -1
988
+ return
989
+ default:
990
+ return
991
+ }
992
+ e.preventDefault()
993
+ keyboard.value = true
994
+ active.value = next
995
+ const first = activePoints.value[0]
996
+ pointer.value = {
997
+ x: activeX.value,
998
+ y: first ? first.y : box.value.top + plotHeight(box.value) / 2,
999
+ }
1000
+ }
1001
+
1002
+ const onBlur = () => {
1003
+ if (keyboard.value) active.value = -1
1004
+ keyboard.value = false
1005
+ }
1006
+
1007
+ const readout = computed(() =>
1008
+ keyboard.value && active.value >= 0
1009
+ ? `${labels.value[active.value] ?? `${strings.value.point} ${active.value + 1}`}: ${tooltipRows.value
1010
+ .map((r) => `${r.name} ${r.text}`)
1011
+ .join(", ")}`
1012
+ : "",
1013
+ )
1014
+
857
1015
  // The chart fills its container's width and sets its own gap between plot and legend — both are layout decisions the page makes, not the chart.
858
1016
  const { class: rootClass, attrs: rest } = useMergedAttrs(
859
1017
  "flex w-full flex-col gap-3",
@@ -878,11 +1036,12 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
878
1036
  >
879
1037
  <button
880
1038
  v-for="s in allSeries"
881
- :key="s.name"
1039
+ :key="s.id"
882
1040
  type="button"
883
1041
  class="group flex cursor-pointer items-center gap-1.5 rounded-md text-xs outline-none transition-opacity duration-fast focus-visible:ring-2 focus-visible:ring-ring"
884
- :class="hidden.has(s.name) ? 'opacity-40' : ''"
885
- @click="toggleSeries(s.name)"
1042
+ :class="hidden.has(s.id) ? 'opacity-40' : ''"
1043
+ :aria-pressed="!hidden.has(s.id)"
1044
+ @click="toggleSeries(s.id)"
886
1045
  >
887
1046
  <span
888
1047
  class="size-2 shrink-0 rounded-full transition-transform duration-fast group-hover:scale-125"
@@ -892,7 +1051,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
892
1051
  <span
893
1052
  v-if="hasRightAxis && s.axis === 'right'"
894
1053
  class="text-[10px] text-muted"
895
- title="Measured on the right axis"
1054
+ :title="strings.rightAxis"
896
1055
  >
897
1056
  ↗
898
1057
  </span>
@@ -904,13 +1063,22 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
904
1063
  <LpContextMenu :items="menuEntries" :always="!!menuItems">
905
1064
  <!-- `group/plot` + data-state lets the tooltip hide itself while the
906
1065
  context menu is up: the two would otherwise stack on the same point. -->
907
- <div class="group/plot relative" :style="{ height: `${height}px` }">
1066
+ <div
1067
+ class="group/plot relative rounded-control outline-none focus-visible:ring-2 focus-visible:ring-ring"
1068
+ :style="{ height: `${height}px` }"
1069
+ :tabindex="hasData ? 0 : undefined"
1070
+ :role="hasData ? 'group' : undefined"
1071
+ aria-roledescription="chart"
1072
+ :aria-label="hasData ? summary : undefined"
1073
+ @keydown="onKeydown"
1074
+ @blur="onBlur"
1075
+ >
908
1076
  <div
909
1077
  v-if="!hasData"
910
1078
  class="flex size-full flex-col items-center justify-center gap-2 rounded-control border border-dashed border-line text-muted"
911
1079
  >
912
1080
  <LpIcon name="lucide:chart-no-axes-column" :size="18" />
913
- <span class="text-xs">{{ emptyLabel }}</span>
1081
+ <span class="text-xs">{{ emptyLabel ?? strings.empty }}</span>
914
1082
  </div>
915
1083
 
916
1084
  <svg
@@ -918,6 +1086,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
918
1086
  :width="width"
919
1087
  :height="height"
920
1088
  class="overflow-visible touch-none select-none"
1089
+ aria-hidden="true"
921
1090
  @pointermove="onMove"
922
1091
  @pointerleave="onLeave"
923
1092
  @click="onClick"
@@ -927,7 +1096,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
927
1096
  <linearGradient
928
1097
  v-for="(s, i) in lines"
929
1098
  :id="gradientId(s.name, i)"
930
- :key="s.name"
1099
+ :key="s.id"
931
1100
  x1="0"
932
1101
  y1="0"
933
1102
  x2="0"
@@ -945,7 +1114,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
945
1114
  :x="box.left"
946
1115
  :y="0"
947
1116
  :height="height"
948
- :width="animate ? 0 : plotWidth(box)"
1117
+ :width="still ? plotWidth(box) : 0"
949
1118
  >
950
1119
  <!-- keySplines is --ease-travel written out: SMIL cannot read a
951
1120
  CSS custom property, so the curve is duplicated here. Change
@@ -1022,6 +1191,18 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
1022
1191
  opacity="0.5"
1023
1192
  />
1024
1193
 
1194
+ <!-- threshold bands sit under the data -->
1195
+ <rect
1196
+ v-for="t in thresholdMarks.filter((m) => m.height > 0)"
1197
+ :key="`band-${t.key}`"
1198
+ :x="box.left"
1199
+ :y="t.top"
1200
+ :width="plotWidth(box)"
1201
+ :height="t.height"
1202
+ :fill="t.color"
1203
+ opacity="0.08"
1204
+ />
1205
+
1025
1206
  <g :clip-path="`url(#${clipId}-reveal)`">
1026
1207
  <!-- bars: paths, so a stack rounds as one shape (see barPath) -->
1027
1208
  <path
@@ -1034,7 +1215,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
1034
1215
  />
1035
1216
 
1036
1217
  <!-- one group per series, so hiding one drops only that curve -->
1037
- <g v-for="(s, i) in lines" :key="`s-${s.name}`" :style="{ opacity: s.alpha }">
1218
+ <g v-for="(s, i) in lines" :key="`s-${s.id}`" :style="{ opacity: s.alpha }">
1038
1219
  <path
1039
1220
  v-show="type === 'area'"
1040
1221
  :d="s.fill"
@@ -1064,6 +1245,49 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
1064
1245
  </g>
1065
1246
  </g>
1066
1247
 
1248
+ <!-- threshold lines and markers sit over the data, under the crosshair -->
1249
+ <g v-for="t in thresholdMarks" :key="`t-${t.key}`">
1250
+ <line
1251
+ :x1="box.left"
1252
+ :x2="box.width - box.right"
1253
+ :y1="t.y"
1254
+ :y2="t.y"
1255
+ :stroke="t.color"
1256
+ stroke-width="1"
1257
+ :stroke-dasharray="t.solid ? undefined : '4 3'"
1258
+ />
1259
+ <text
1260
+ v-if="t.label"
1261
+ :x="box.width - box.right - 4"
1262
+ :y="t.top - 4"
1263
+ text-anchor="end"
1264
+ class="text-[10px]"
1265
+ :fill="t.color"
1266
+ >
1267
+ {{ t.label }}
1268
+ </text>
1269
+ </g>
1270
+ <g v-for="m in markerMarks" :key="`m-${m.key}`">
1271
+ <line
1272
+ :x1="m.x"
1273
+ :x2="m.x"
1274
+ :y1="box.top"
1275
+ :y2="box.top + plotHeight(box)"
1276
+ :stroke="m.color"
1277
+ stroke-width="1"
1278
+ stroke-dasharray="2 3"
1279
+ />
1280
+ <text
1281
+ v-if="m.label"
1282
+ :x="m.x + 4"
1283
+ :y="box.top + 9"
1284
+ class="text-[10px]"
1285
+ :fill="m.color"
1286
+ >
1287
+ {{ m.label }}
1288
+ </text>
1289
+ </g>
1290
+
1067
1291
  <!-- crosshair -->
1068
1292
  <g v-if="active >= 0 && type !== 'bar'">
1069
1293
  <line
@@ -1077,7 +1301,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
1077
1301
  />
1078
1302
  <circle
1079
1303
  v-for="p in activePoints"
1080
- :key="p.name"
1304
+ :key="p.id"
1081
1305
  :cx="activeX"
1082
1306
  :cy="p.y"
1083
1307
  r="4"
@@ -1125,7 +1349,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
1125
1349
  <div class="flex flex-col gap-1">
1126
1350
  <div
1127
1351
  v-for="r in tooltipRows"
1128
- :key="r.name"
1352
+ :key="r.id"
1129
1353
  class="flex items-center justify-between gap-3 text-xs"
1130
1354
  >
1131
1355
  <span class="flex items-center gap-1.5 text-muted">
@@ -1137,8 +1361,28 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
1137
1361
  </div>
1138
1362
  </LpShift>
1139
1363
  </Motion>
1364
+
1365
+ <span class="sr-only" aria-live="polite">{{ readout }}</span>
1140
1366
  </div>
1141
1367
  </LpContextMenu>
1368
+
1369
+ <table v-if="dataTable && hasData" class="sr-only">
1370
+ <caption v-if="title">{{ title }}</caption>
1371
+ <thead>
1372
+ <tr>
1373
+ <th scope="col"></th>
1374
+ <th v-for="s in allSeries" :key="s.id" scope="col">{{ s.name }}</th>
1375
+ </tr>
1376
+ </thead>
1377
+ <tbody>
1378
+ <tr v-for="(label, i) in labels" :key="i">
1379
+ <th scope="row">{{ label }}</th>
1380
+ <td v-for="s in allSeries" :key="s.id">
1381
+ {{ s.values[i] == null ? "—" : fmtSeries(s, s.values[i] as number) }}
1382
+ </td>
1383
+ </tr>
1384
+ </tbody>
1385
+ </table>
1142
1386
  </div>
1143
1387
  </template>
1144
1388
 
@@ -8,9 +8,10 @@ export default { inheritAttrs: false }
8
8
  <script setup lang="ts">
9
9
  import { CheckboxIndicator, CheckboxRoot } from "reka-ui"
10
10
  import LpIcon from "./LpIcon.vue"
11
+ import { useFieldControl } from "../composables/useField"
11
12
  import { useMergedAttrs } from "../composables/useMergedClass"
12
13
 
13
- defineProps<{
14
+ const props = defineProps<{
14
15
  modelValue?: boolean
15
16
  /** Mixed state (e.g. a "select all" with a partial selection). Shows a dash. */
16
17
  indeterminate?: boolean
@@ -30,6 +31,8 @@ function onUpdate(v: boolean | "indeterminate") {
30
31
  emit("change", checked)
31
32
  }
32
33
 
34
+ const field = useFieldControl(() => ({ wrapped: false, id: props.id, disabled: props.disabled }))
35
+
33
36
  // The label sits a fixed distance from the box; a dense list of options needs that closer.
34
37
  const { class: rootClass, attrs: rest } = useMergedAttrs(
35
38
  "group inline-flex cursor-pointer items-center gap-2 text-sm text-ink",
@@ -41,10 +44,11 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
41
44
  :class="rootClass"
42
45
  v-bind="rest">
43
46
  <CheckboxRoot
44
- :id="id"
47
+ :id="field.id"
45
48
  :name="name"
46
49
  :model-value="indeterminate ? 'indeterminate' : modelValue"
47
- :disabled="disabled"
50
+ :disabled="field.disabled"
51
+ :aria-describedby="field.describedBy"
48
52
  class="flex size-[18px] items-center justify-center rounded-[5px] border border-line-strong bg-surface-soft outline-none transition-[color,background-color,border-color,box-shadow,scale] duration-[var(--duration-fast)] ease-[var(--ease-emphasized)] not-data-[state=checked]:not-data-[state=indeterminate]:group-hover:border-brand active:scale-90 motion-reduce:active:scale-100 focus-visible:ring-2 focus-visible:ring-ring data-[state=checked]:border-transparent data-[state=checked]:bg-brand data-[state=indeterminate]:border-transparent data-[state=indeterminate]:bg-brand disabled:cursor-not-allowed disabled:opacity-55"
49
53
  @update:model-value="onUpdate"
50
54
  >