@leavepulse/ui 0.43.4 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/README.md +75 -7
  2. package/dist/assets/{LpCommandPalette-B-0Est4Z.css → LpCommandPalette-DCP5g2ik.css} +21 -21
  3. package/dist/assets/{LpDropzone-BBoWvi-J.css → LpDropzone-i4BdMi1H.css} +12 -12
  4. package/dist/assets/{LpImageEditor-Ct7Y3aNu.css → LpImageEditor-CbMA82g8.css} +6 -6
  5. package/dist/assets/{LpShortcutsDialog-C6YoQ8_M.css → LpShortcutsDialog-ChUyU7oV.css} +7 -7
  6. package/dist/assets/{LpSkeleton-D7xU8IPV.css → LpSkeleton-H3Z5dEIk.css} +4 -1
  7. package/dist/assets/{LpTable-BgPLJOXa.css → LpTable-DDQNdf86.css} +9 -9
  8. package/dist/chunks/ContextMenuItems-CrfeNPWT.js +123 -0
  9. package/dist/chunks/{LayoutCanvas-Dd20boUG.js → LayoutCanvas-D-jPOAvg.js} +26 -9
  10. package/dist/chunks/{LayoutNode-DGpZA9LX.js → LayoutNode-D8Xy9N-o.js} +20 -8
  11. package/dist/chunks/{LpActionBar-BlvtGB2P.js → LpActionBar-BbwCF6l5.js} +24 -10
  12. package/dist/chunks/{LpAddressInput-B-Zvxsn7.js → LpAddressInput-BrOqkZqg.js} +53 -48
  13. package/dist/chunks/{LpAlert-BAYHDSS1.js → LpAlert-DkOIjsbq.js} +15 -8
  14. package/dist/chunks/{LpAppShell-BGQVIY-3.js → LpAppShell-BUKPcHco.js} +51 -24
  15. package/dist/chunks/{LpAutocomplete-DExlIZe_.js → LpAutocomplete-DxV3Y9h4.js} +50 -23
  16. package/dist/chunks/{LpAvatar-B3-xAOH3.js → LpAvatar-ClBbEYZN.js} +17 -8
  17. package/dist/chunks/LpBreadcrumbs-B8IJgFIc.js +95 -0
  18. package/dist/chunks/{LpButton-5VXXCjwF.js → LpButton-kqJBHIIb.js} +44 -14
  19. package/dist/chunks/{LpCard-BhI1N_vL.js → LpCard-CpRByR0q.js} +1 -1
  20. package/dist/chunks/{LpCheckbox-CVccKYaH.js → LpCheckbox-DpqchzH4.js} +7 -3
  21. package/dist/chunks/{LpCodeBlock-Bo0B4TFR.js → LpCodeBlock-DeHJPq7p.js} +48 -16
  22. package/dist/chunks/{LpColorPicker-BlXea7LM.js → LpColorPicker-Bhf1qxhB.js} +32 -9
  23. package/dist/chunks/{LpConfirmDialog-Q-bxff8z.js → LpConfirmDialog-CIUOSnWo.js} +58 -16
  24. package/dist/chunks/LpConfirmHost-CDm0Wtpv.js +49 -0
  25. package/dist/chunks/LpContextMenu-DQw5dGfG.js +68 -0
  26. package/dist/chunks/LpDatePicker-Drirx1AK.js +191 -0
  27. package/dist/chunks/{LpDrawer-BZ6k3hvo.js → LpDrawer-bScx-KMO.js} +63 -26
  28. package/dist/chunks/{LpFileTree-_5vFBgaK.js → LpFileTree-CSI_5SK2.js} +23 -8
  29. package/dist/chunks/{LpFileTreeNode-BdcIYKIE.js → LpFileTreeNode-B4MiSQX2.js} +2 -2
  30. package/dist/chunks/LpFormField-CIk5qosh.js +87 -0
  31. package/dist/chunks/{LpInput-9EXWUM9B.js → LpInput-C0e8y8_d.js} +26 -11
  32. package/dist/chunks/{LpLightbox-2sYVMY-S.js → LpLightbox-Mr0W_6HK.js} +147 -60
  33. package/dist/chunks/{LpLink-DCND-zyu.js → LpLink-Krkjhe_y.js} +15 -6
  34. package/dist/chunks/{LpLogViewer-CaRy5MeS.js → LpLogViewer-YBOaFnc_.js} +68 -34
  35. package/dist/chunks/{LpMapPicker-ul4oYno6.js → LpMapPicker-NUzS9e37.js} +38 -13
  36. package/dist/chunks/{LpMediaGallery-FF-NquIo.js → LpMediaGallery-DfJmMW0I.js} +6 -3
  37. package/dist/chunks/{LpModal-Bt0pQCI_.js → LpModal-DPwGKBPv.js} +49 -26
  38. package/dist/chunks/{LpModalNav-YaifHrXC.js → LpModalNav-DPg5H9f3.js} +9 -6
  39. package/dist/chunks/{LpNotificationBell-DyGGsMAM.js → LpNotificationBell-t5yn-siX.js} +58 -24
  40. package/dist/chunks/{LpNumberField-CWuX08iX.js → LpNumberField-DWnQKktS.js} +20 -7
  41. package/dist/chunks/{LpNumberFlow-amWYaeuA.js → LpNumberFlow-z4WPZcZu.js} +22 -12
  42. package/dist/chunks/{LpOtpInput-y2v-pmiN.js → LpOtpInput-CziPF3Oq.js} +37 -8
  43. package/dist/chunks/{LpPagination-5FxjmmB5.js → LpPagination-DFngX7Xo.js} +30 -19
  44. package/dist/chunks/{LpPasswordInput--sjYCkLS.js → LpPasswordInput-CWVDuBJ5.js} +10 -7
  45. package/dist/chunks/{LpPhoneInput-BDSKVl_z.js → LpPhoneInput-CD-ZTeSd.js} +57 -33
  46. package/dist/chunks/{LpPie-D6OnzwlJ.js → LpPie-BLco31Nx.js} +78 -21
  47. package/dist/chunks/{LpPopover-8268UmlC.js → LpPopover-p_e_shN3.js} +4 -2
  48. package/dist/chunks/{LpProgress-Btq5W4Ut.js → LpProgress-CBU1iI69.js} +5 -4
  49. package/dist/chunks/{LpRadioGroup-xPIuVDO_.js → LpRadioGroup-DmEW0l78.js} +9 -2
  50. package/dist/chunks/{LpSegmented-fV0Sk92p.js → LpSegmented-Dejd7cIx.js} +7 -3
  51. package/dist/chunks/{LpSelect-ANIVtrpM.js → LpSelect-DrRD9I7L.js} +40 -18
  52. package/dist/chunks/{LpSidebar-ClBUwoEz.js → LpSidebar-BgwUNWpJ.js} +30 -22
  53. package/dist/chunks/{LpSidebarNav-llOcs_Fn.js → LpSidebarNav-Bqxz8aWT.js} +14 -9
  54. package/dist/chunks/{LpSkeleton-CTI-vBgW.js → LpSkeleton-BB6XIV-w.js} +1 -1
  55. package/dist/chunks/{LpSlider-BwNIBkAK.js → LpSlider-Cuzmrd7p.js} +7 -2
  56. package/dist/chunks/{LpSparkline-C6xQ2dwv.js → LpSparkline-CB80N8SJ.js} +11 -4
  57. package/dist/chunks/{LpStat-CQaW8R--.js → LpStat-dXtyrdX9.js} +1 -1
  58. package/dist/chunks/{LpSwitch-Blizvqyt.js → LpSwitch-B1aLKulj.js} +40 -26
  59. package/dist/chunks/{LpTableOfContents-CjvxPMpU.js → LpTableOfContents-BUcAfGPQ.js} +36 -41
  60. package/dist/chunks/{LpTabs-Cefe_lys.js → LpTabs-_qMVaZJ6.js} +19 -5
  61. package/dist/chunks/LpTextarea-yP8hGedR.js +77 -0
  62. package/dist/chunks/LpThemeProvider-Wjm7Tuit.js +35 -0
  63. package/dist/chunks/LpThemeSwitcher-CKLc10KZ.js +169 -0
  64. package/dist/chunks/{LpTilt-DuyKbpkA.js → LpTilt-B7KHX1zH.js} +2 -2
  65. package/dist/chunks/{LpTimeField-K6nAGboX.js → LpTimeField-C5HRgS5H.js} +13 -7
  66. package/dist/chunks/{LpToaster-gDB1EaU3.js → LpToaster--oT6lfhL.js} +57 -28
  67. package/dist/chunks/{LpTooltip-hCLPAqMG.js → LpTooltip-CBdzeKNf.js} +13 -6
  68. package/dist/chunks/LpTooltipProvider-CftjV7Xc.js +25 -0
  69. package/dist/chunks/{LpTopologyCanvas-Cm1L39uL.js → LpTopologyCanvas-DocsLal3.js} +6 -1
  70. package/dist/chunks/{LpUptimeBar-DvAMno0S.js → LpUptimeBar-DLtxOMRy.js} +43 -14
  71. package/dist/chunks/{LpThemeSwitcher-Cue-myST.js → useTheme-C8JHUlPM.js} +173 -215
  72. package/dist/component-names.d.ts +1 -1
  73. package/dist/components/ContextMenuItems.vue.d.ts +8 -0
  74. package/dist/components/ContextMenuItems.vue.js +4 -0
  75. package/dist/components/LayoutCanvas.vue.js +1 -1
  76. package/dist/components/LayoutNode.vue.d.ts +4 -0
  77. package/dist/components/LayoutNode.vue.js +1 -1
  78. package/dist/components/LpActionBar.vue.d.ts +3 -0
  79. package/dist/components/LpActionBar.vue.js +1 -1
  80. package/dist/components/LpAddressInput.vue.d.ts +1 -1
  81. package/dist/components/LpAddressInput.vue.js +1 -1
  82. package/dist/components/LpAlert.vue.d.ts +9 -0
  83. package/dist/components/LpAlert.vue.js +1 -1
  84. package/dist/components/LpAppShell.vue.d.ts +10 -0
  85. package/dist/components/LpAppShell.vue.js +1 -1
  86. package/dist/components/LpAutocomplete.vue.d.ts +31 -6
  87. package/dist/components/LpAutocomplete.vue.js +1 -1
  88. package/dist/components/LpAvatar.vue.d.ts +3 -3
  89. package/dist/components/LpAvatar.vue.js +1 -1
  90. package/dist/components/LpBadge.vue.d.ts +2 -2
  91. package/dist/components/LpBreadcrumbs.vue.d.ts +13 -9
  92. package/dist/components/LpBreadcrumbs.vue.js +1 -1
  93. package/dist/components/LpButton.vue.d.ts +35 -98
  94. package/dist/components/LpButton.vue.js +1 -1
  95. package/dist/components/LpCard.vue.js +1 -1
  96. package/dist/components/LpChart.vue.d.ts +31 -2
  97. package/dist/components/LpChart.vue.js +317 -95
  98. package/dist/components/LpCheckbox.vue.js +1 -1
  99. package/dist/components/LpCodeBlock.vue.d.ts +2 -0
  100. package/dist/components/LpCodeBlock.vue.js +1 -1
  101. package/dist/components/LpColorPicker.vue.js +1 -1
  102. package/dist/components/LpCommandPalette.vue.d.ts +8 -3
  103. package/dist/components/LpCommandPalette.vue.js +86 -72
  104. package/dist/components/LpConfirmDialog.vue.d.ts +15 -3
  105. package/dist/components/LpConfirmDialog.vue.js +1 -1
  106. package/dist/components/LpConfirmHost.vue.d.ts +9 -0
  107. package/dist/components/LpConfirmHost.vue.js +4 -0
  108. package/dist/components/LpContextMenu.vue.d.ts +3 -5
  109. package/dist/components/LpContextMenu.vue.js +1 -1
  110. package/dist/components/LpDatePicker.vue.d.ts +24 -6
  111. package/dist/components/LpDatePicker.vue.js +1 -1
  112. package/dist/components/LpDrawer.vue.d.ts +10 -0
  113. package/dist/components/LpDrawer.vue.js +1 -1
  114. package/dist/components/LpDropzone.vue.js +22 -6
  115. package/dist/components/LpFileTree.vue.d.ts +11 -2
  116. package/dist/components/LpFileTree.vue.js +1 -1
  117. package/dist/components/LpFileTreeNode.vue.js +1 -1
  118. package/dist/components/LpFormField.vue.d.ts +9 -0
  119. package/dist/components/LpFormField.vue.js +1 -1
  120. package/dist/components/LpImageEditor.vue.js +92 -57
  121. package/dist/components/LpInput.vue.d.ts +3 -3
  122. package/dist/components/LpInput.vue.js +1 -1
  123. package/dist/components/LpLightbox.vue.d.ts +17 -0
  124. package/dist/components/LpLightbox.vue.js +1 -1
  125. package/dist/components/LpLink.vue.d.ts +11 -11
  126. package/dist/components/LpLink.vue.js +1 -1
  127. package/dist/components/LpLogViewer.vue.d.ts +20 -2
  128. package/dist/components/LpLogViewer.vue.js +1 -1
  129. package/dist/components/LpMapPicker.vue.d.ts +4 -4
  130. package/dist/components/LpMapPicker.vue.js +1 -1
  131. package/dist/components/LpMediaGallery.vue.js +1 -1
  132. package/dist/components/LpModal.vue.d.ts +16 -6
  133. package/dist/components/LpModal.vue.js +1 -1
  134. package/dist/components/LpModalNav.vue.d.ts +0 -1
  135. package/dist/components/LpModalNav.vue.js +1 -1
  136. package/dist/components/LpNotificationBell.vue.d.ts +11 -2
  137. package/dist/components/LpNotificationBell.vue.js +1 -1
  138. package/dist/components/LpNumberField.vue.d.ts +2 -0
  139. package/dist/components/LpNumberField.vue.js +1 -1
  140. package/dist/components/LpNumberFlow.vue.js +1 -1
  141. package/dist/components/LpOtpInput.vue.js +1 -1
  142. package/dist/components/LpPagination.vue.d.ts +2 -0
  143. package/dist/components/LpPagination.vue.js +1 -1
  144. package/dist/components/LpPasswordInput.vue.js +1 -1
  145. package/dist/components/LpPhoneInput.vue.js +1 -1
  146. package/dist/components/LpPie.vue.d.ts +21 -2
  147. package/dist/components/LpPie.vue.js +1 -1
  148. package/dist/components/LpPopover.vue.d.ts +2 -0
  149. package/dist/components/LpPopover.vue.js +1 -1
  150. package/dist/components/LpProgress.vue.js +1 -1
  151. package/dist/components/LpRadioGroup.vue.js +1 -1
  152. package/dist/components/LpSection.vue.d.ts +1 -1
  153. package/dist/components/LpSegmented.vue.d.ts +5 -0
  154. package/dist/components/LpSegmented.vue.js +1 -1
  155. package/dist/components/LpSelect.vue.d.ts +1 -0
  156. package/dist/components/LpSelect.vue.js +1 -1
  157. package/dist/components/LpShortcutsDialog.vue.d.ts +1 -1
  158. package/dist/components/LpShortcutsDialog.vue.js +11 -8
  159. package/dist/components/LpSidebar.vue.d.ts +6 -2
  160. package/dist/components/LpSidebar.vue.js +1 -1
  161. package/dist/components/LpSidebarNav.vue.js +1 -1
  162. package/dist/components/LpSkeleton.vue.js +1 -1
  163. package/dist/components/LpSlider.vue.js +1 -1
  164. package/dist/components/LpSparkline.vue.d.ts +1 -0
  165. package/dist/components/LpSparkline.vue.js +1 -1
  166. package/dist/components/LpStat.vue.js +1 -1
  167. package/dist/components/LpSwitch.vue.js +1 -1
  168. package/dist/components/LpTable.vue.d.ts +23 -1
  169. package/dist/components/LpTable.vue.js +74 -36
  170. package/dist/components/LpTableOfContents.vue.d.ts +4 -2
  171. package/dist/components/LpTableOfContents.vue.js +1 -1
  172. package/dist/components/LpTabs.vue.d.ts +2 -0
  173. package/dist/components/LpTabs.vue.js +1 -1
  174. package/dist/components/LpTextarea.vue.d.ts +19 -6
  175. package/dist/components/LpTextarea.vue.js +1 -1
  176. package/dist/components/LpThemeProvider.vue.d.ts +22 -0
  177. package/dist/components/LpThemeProvider.vue.js +4 -0
  178. package/dist/components/LpThemeSwitcher.vue.d.ts +13 -0
  179. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  180. package/dist/components/LpTilt.vue.js +1 -1
  181. package/dist/components/LpTimeField.vue.js +1 -1
  182. package/dist/components/LpToaster.vue.d.ts +6 -1
  183. package/dist/components/LpToaster.vue.js +1 -1
  184. package/dist/components/LpTooltip.vue.d.ts +1 -2
  185. package/dist/components/LpTooltip.vue.js +1 -1
  186. package/dist/components/LpTooltipProvider.vue.d.ts +22 -0
  187. package/dist/components/LpTooltipProvider.vue.js +4 -0
  188. package/dist/components/LpTopologyCanvas.vue.js +1 -1
  189. package/dist/components/LpUptimeBar.vue.d.ts +4 -0
  190. package/dist/components/LpUptimeBar.vue.js +1 -1
  191. package/dist/components/chart.d.ts +32 -1
  192. package/dist/components/chart.js +39 -4
  193. package/dist/components/codeHighlight.d.ts +6 -0
  194. package/dist/components/codeHighlight.js +92 -20
  195. package/dist/components/color.d.ts +1 -1
  196. package/dist/components/color.js +20 -9
  197. package/dist/components/confirm.d.ts +25 -0
  198. package/dist/components/confirm.js +26 -0
  199. package/dist/components/countries.d.ts +6 -0
  200. package/dist/components/countries.js +7 -0
  201. package/dist/components/fileTree.js +16 -7
  202. package/dist/components/imageCrop.d.ts +42 -0
  203. package/dist/components/imageCrop.js +47 -0
  204. package/dist/components/link.d.ts +12 -0
  205. package/dist/components/link.js +19 -0
  206. package/dist/components/modalDepth.d.ts +18 -2
  207. package/dist/components/modalDepth.js +17 -5
  208. package/dist/components/otp.d.ts +5 -0
  209. package/dist/components/otp.js +10 -0
  210. package/dist/components/sidebar.d.ts +7 -1
  211. package/dist/components/tableSort.d.ts +8 -0
  212. package/dist/components/tableSort.js +31 -0
  213. package/dist/composables/useField.d.ts +34 -0
  214. package/dist/composables/useField.js +45 -0
  215. package/dist/composables/useFormDraft.d.ts +10 -3
  216. package/dist/composables/useFormDraft.js +9 -14
  217. package/dist/composables/useHotkeys.d.ts +17 -1
  218. package/dist/composables/useHotkeys.js +55 -18
  219. package/dist/composables/useInputFilter.d.ts +17 -0
  220. package/dist/composables/useInputFilter.js +47 -10
  221. package/dist/composables/useLpConfig.d.ts +24 -0
  222. package/dist/composables/useLpConfig.js +44 -0
  223. package/dist/composables/useRovingFocus.js +7 -0
  224. package/dist/composables/useTilt.d.ts +2 -2
  225. package/dist/composables/useTilt.js +5 -6
  226. package/dist/composables/useToast.d.ts +20 -0
  227. package/dist/composables/useToast.js +28 -4
  228. package/dist/composables/useZoomPan.d.ts +3 -3
  229. package/dist/composables/useZoomPan.js +4 -4
  230. package/dist/index.d.ts +32 -2
  231. package/dist/index.js +184 -137
  232. package/dist/layout/tree.d.ts +2 -0
  233. package/dist/layout/tree.js +16 -1
  234. package/dist/layout/useLayout.d.ts +15 -0
  235. package/dist/layout/useLayout.js +41 -14
  236. package/dist/theme/useTheme.d.ts +32 -9
  237. package/package.json +8 -7
  238. package/src/component-names.ts +5 -7
  239. package/src/components/ContextMenuItems.vue +88 -0
  240. package/src/components/LayoutCanvas.vue +26 -6
  241. package/src/components/LayoutNode.vue +19 -2
  242. package/src/components/LpActionBar.vue +30 -7
  243. package/src/components/LpAddressInput.vue +57 -41
  244. package/src/components/LpAlert.vue +16 -3
  245. package/src/components/LpAppShell.vue +45 -12
  246. package/src/components/LpAutocomplete.vue +46 -10
  247. package/src/components/LpAvatar.vue +19 -5
  248. package/src/components/LpBreadcrumbs.vue +65 -41
  249. package/src/components/LpButton.vue +59 -13
  250. package/src/components/LpChart.vue +272 -28
  251. package/src/components/LpCheckbox.vue +7 -3
  252. package/src/components/LpCodeBlock.vue +50 -14
  253. package/src/components/LpColorPicker.vue +39 -9
  254. package/src/components/LpCommandPalette.vue +34 -13
  255. package/src/components/LpConfirmDialog.vue +68 -17
  256. package/src/components/LpConfirmHost.vue +59 -0
  257. package/src/components/LpContextMenu.vue +14 -115
  258. package/src/components/LpDatePicker.vue +99 -68
  259. package/src/components/LpDrawer.vue +56 -19
  260. package/src/components/LpDropzone.vue +21 -4
  261. package/src/components/LpFileTree.vue +33 -7
  262. package/src/components/LpFormField.vue +30 -7
  263. package/src/components/LpImageEditor.vue +96 -57
  264. package/src/components/LpInput.vue +18 -7
  265. package/src/components/LpLightbox.vue +140 -29
  266. package/src/components/LpLink.vue +24 -13
  267. package/src/components/LpLogViewer.vue +86 -17
  268. package/src/components/LpMapPicker.vue +36 -13
  269. package/src/components/LpMediaGallery.vue +8 -2
  270. package/src/components/LpModal.vue +53 -27
  271. package/src/components/LpModalNav.vue +5 -2
  272. package/src/components/LpNotificationBell.vue +63 -17
  273. package/src/components/LpNumberField.vue +21 -6
  274. package/src/components/LpNumberFlow.vue +26 -10
  275. package/src/components/LpOtpInput.vue +43 -8
  276. package/src/components/LpPagination.vue +23 -7
  277. package/src/components/LpPasswordInput.vue +4 -1
  278. package/src/components/LpPhoneInput.vue +53 -27
  279. package/src/components/LpPie.vue +94 -15
  280. package/src/components/LpPopover.vue +3 -0
  281. package/src/components/LpProgress.vue +5 -3
  282. package/src/components/LpRadioGroup.vue +9 -2
  283. package/src/components/LpSegmented.vue +12 -2
  284. package/src/components/LpSelect.vue +0 -0
  285. package/src/components/LpShortcutsDialog.vue +8 -3
  286. package/src/components/LpSidebar.vue +14 -9
  287. package/src/components/LpSidebarNav.vue +17 -5
  288. package/src/components/LpSkeleton.vue +4 -1
  289. package/src/components/LpSlider.vue +7 -1
  290. package/src/components/LpSparkline.vue +11 -3
  291. package/src/components/LpSwitch.vue +30 -7
  292. package/src/components/LpTable.vue +91 -32
  293. package/src/components/LpTableOfContents.vue +50 -32
  294. package/src/components/LpTabs.vue +23 -6
  295. package/src/components/LpTextarea.vue +44 -6
  296. package/src/components/LpThemeProvider.vue +41 -0
  297. package/src/components/LpThemeSwitcher.vue +64 -17
  298. package/src/components/LpTilt.vue +2 -2
  299. package/src/components/LpTimeField.vue +14 -3
  300. package/src/components/LpToaster.vue +66 -26
  301. package/src/components/LpTooltip.vue +22 -5
  302. package/src/components/LpTooltipProvider.vue +24 -0
  303. package/src/components/LpTopologyCanvas.vue +12 -1
  304. package/src/components/LpUptimeBar.vue +46 -8
  305. package/src/components/chart.ts +77 -5
  306. package/src/components/codeHighlight.ts +111 -15
  307. package/src/components/color.ts +30 -9
  308. package/src/components/confirm.ts +58 -0
  309. package/src/components/countries.ts +12 -0
  310. package/src/components/fileTree.ts +19 -7
  311. package/src/components/imageCrop.ts +107 -0
  312. package/src/components/link.ts +34 -0
  313. package/src/components/modalDepth.ts +48 -6
  314. package/src/components/otp.ts +16 -0
  315. package/src/components/sidebar.ts +7 -1
  316. package/src/components/tableSort.ts +37 -0
  317. package/src/composables/useField.ts +79 -0
  318. package/src/composables/useFormDraft.ts +25 -20
  319. package/src/composables/useHotkeys.ts +89 -18
  320. package/src/composables/useInputFilter.ts +77 -14
  321. package/src/composables/useLpConfig.ts +87 -0
  322. package/src/composables/useRovingFocus.ts +11 -0
  323. package/src/composables/useTilt.ts +5 -6
  324. package/src/composables/useToast.ts +56 -4
  325. package/src/composables/useZoomPan.ts +6 -6
  326. package/src/index.ts +37 -2
  327. package/src/layout/tree.ts +30 -0
  328. package/src/layout/useLayout.ts +68 -18
  329. package/src/nuxt.ts +59 -17
  330. package/src/runtime/config.plugin.ts +14 -0
  331. package/src/runtime/theme.plugin.ts +42 -22
  332. package/src/theme/useTheme.ts +206 -100
  333. package/dist/chunks/LpBreadcrumbs-p4jHe_V-.js +0 -62
  334. package/dist/chunks/LpContextMenu-CwW1QXe_.js +0 -217
  335. package/dist/chunks/LpDatePicker-CwOToxGq.js +0 -164
  336. package/dist/chunks/LpFormField-CazBc2-a.js +0 -62
  337. package/dist/chunks/LpTextarea-t7F6gv8E.js +0 -47
@@ -1,6 +1,15 @@
1
+ <script lang="ts">
2
+ // The consumer's `class` is merged into the root's own rather than appended,
3
+ // so `class="h-12"` or `class="px-6"` beats the size's defaults.
4
+ export default { inheritAttrs: false }
5
+ </script>
6
+
1
7
  <script setup lang="ts">
2
8
  import { tv, type VariantProps } from "tailwind-variants"
3
9
  import { computed, type Component } from "vue"
10
+ import { useLinkComponent } from "../composables/useLpConfig"
11
+ import { useMergedAttrs } from "../composables/useMergedClass"
12
+ import { resolveLink, type LinkTarget } from "./link"
4
13
 
5
14
  const button = tv({
6
15
  base: [
@@ -16,13 +25,15 @@ const button = tv({
16
25
  solid: "bg-brand text-ink-inverse hover:bg-brand-hover",
17
26
  action:
18
27
  "bg-action text-ink-inverse shadow-[0_14px_28px_color-mix(in_srgb,var(--color-action)_24%,transparent)] hover:bg-action-hover",
19
- outline: "border border-line-strong text-ink hover:border-brand hover:bg-white/[0.04]",
28
+ outline: "border border-line-strong text-ink hover:border-brand hover:bg-ink/[0.04]",
20
29
  // raised surface tile with a border — a visible, neutral filled button
21
30
  soft: "border border-line bg-surface-raised/80 text-ink hover:border-line-strong hover:bg-surface-soft",
22
- ghost: "text-ink hover:bg-white/[0.06]",
23
- muted: "text-muted hover:bg-white/[0.04] hover:text-ink",
24
- // high-contrast light fill (e.g. a "download" CTA on a dark hero)
25
- light: "border border-white/20 bg-white text-ink-inverse hover:bg-white/90",
31
+ ghost: "text-ink hover:bg-ink/[0.06]",
32
+ muted: "text-muted hover:bg-ink/[0.04] hover:text-ink",
33
+ // high-contrast light fill (e.g. a "download" CTA on a dark hero). The
34
+ // text is a fixed dark tone: `ink-inverse` is light in light themes,
35
+ // which made this white on white there.
36
+ light: "border border-black/10 bg-white text-[#0b1210] hover:bg-white/90",
26
37
  danger: "bg-danger text-ink-inverse hover:bg-danger-hover",
27
38
  },
28
39
  size: {
@@ -58,7 +69,7 @@ const button = tv({
58
69
  glow: true,
59
70
  variant: "light",
60
71
  class:
61
- "shadow-[0_8px_18px_rgba(255,255,255,0.2)] hover:shadow-[0_12px_24px_rgba(255,255,255,0.3)]",
72
+ "shadow-[0_8px_18px_color-mix(in_srgb,var(--color-ink)_20%,transparent)] hover:shadow-[0_12px_24px_color-mix(in_srgb,var(--color-ink)_30%,transparent)]",
62
73
  },
63
74
  ],
64
75
  defaultVariants: { variant: "solid", size: "md" },
@@ -74,20 +85,52 @@ const props = withDefaults(
74
85
  square?: boolean
75
86
  glow?: boolean
76
87
  /**
77
- * Root element/component. Defaults to a native <button>. Pass a router link
78
- * component (e.g. NuxtLink) or "a" to render a link-styled button; the kit
79
- * stays DOM-agnostic, the consumer supplies the link component.
88
+ * Root element/component. Defaults to a native <button>, or a link when
89
+ * `to`/`href` is set (the link component from provideLpConfig for internal
90
+ * targets). Pass a component or "a" to override.
80
91
  */
81
92
  as?: string | Component
93
+ /** Router destination: renders the configured link component. */
94
+ to?: LinkTarget
95
+ /** Link URL: renders an anchor (or the link component for internal paths). */
96
+ href?: string
82
97
  type?: "button" | "submit" | "reset"
83
98
  disabled?: boolean
84
99
  }>(),
85
- { as: "button", type: "button" },
100
+ { type: "button" },
86
101
  )
87
102
 
88
- const isNativeButton = computed(() => props.as === "button")
103
+ const linkComponent = useLinkComponent()
104
+
105
+ const resolved = computed<{ is: string | Component; props: Record<string, unknown> }>(() => {
106
+ if (props.as && props.as !== "a") {
107
+ const target = props.to != null ? { to: props.to } : props.href != null ? { href: props.href } : {}
108
+ return { is: props.as, props: props.as === "button" ? {} : target }
109
+ }
110
+ if (props.as === "a") return { is: "a", props: { href: props.href } }
111
+ return resolveLink(linkComponent.value, { to: props.to, href: props.href }) ?? { is: "button", props: {} }
112
+ })
113
+
114
+ const isNativeButton = computed(() => resolved.value.is === "button")
115
+
116
+ /*
117
+ * A disabled link has no `disabled` attribute to lean on: it must leave the tab
118
+ * order and swallow the click. The capture phase runs before a router link's
119
+ * own handler, which skips navigation once the event is default-prevented.
120
+ */
121
+ function blockDisabled(event: MouseEvent) {
122
+ if (!props.disabled || isNativeButton.value) return
123
+ event.preventDefault()
124
+ event.stopImmediatePropagation()
125
+ }
126
+
127
+ const linkProps = computed(() => {
128
+ if (isNativeButton.value) return {}
129
+ if (!props.disabled) return resolved.value.props
130
+ return resolved.value.is === "a" ? {} : resolved.value.props
131
+ })
89
132
 
90
- const classes = computed(() =>
133
+ const { class: classes, attrs: rest } = useMergedAttrs(() =>
91
134
  button({
92
135
  variant: props.variant,
93
136
  size: props.size,
@@ -100,11 +143,14 @@ const classes = computed(() =>
100
143
 
101
144
  <template>
102
145
  <component
103
- :is="as"
146
+ :is="resolved.is"
147
+ v-bind="{ ...rest, ...linkProps }"
104
148
  :type="isNativeButton ? type : undefined"
105
149
  :disabled="isNativeButton ? disabled : undefined"
106
150
  :aria-disabled="!isNativeButton && disabled ? 'true' : undefined"
151
+ :tabindex="!isNativeButton && disabled ? -1 : undefined"
107
152
  :class="classes"
153
+ @click.capture="blockDisabled"
108
154
  >
109
155
  <slot />
110
156
  </component>
@@ -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