@leavepulse/ui 0.43.4 → 0.45.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 (437) hide show
  1. package/README.md +121 -7
  2. package/dist/assets/{LpCommandPalette-B-0Est4Z.css → LpCommandPalette-nGNnzXqQ.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/{LpRollingText-CR1mb0Vt.css → LpRollingText-BGnE6q9j.css} +3 -0
  6. package/dist/assets/{LpShortcutsDialog-C6YoQ8_M.css → LpShortcutsDialog-CqYX3yy6.css} +28 -7
  7. package/dist/assets/{LpSkeleton-D7xU8IPV.css → LpSkeleton-H3Z5dEIk.css} +4 -1
  8. package/dist/assets/{LpTable-BgPLJOXa.css → LpTable-CPwWxTJS.css} +49 -9
  9. package/dist/chunks/ContextMenuItems-BtKU7074.js +124 -0
  10. package/dist/chunks/{LayoutCanvas-Dd20boUG.js → LayoutCanvas-BkaGXTjx.js} +26 -9
  11. package/dist/chunks/{LayoutNode-DGpZA9LX.js → LayoutNode-B3_9E1Xa.js} +28 -12
  12. package/dist/chunks/{LpActionBar-BlvtGB2P.js → LpActionBar-CnfFVIwC.js} +26 -12
  13. package/dist/chunks/{LpAddressInput-B-Zvxsn7.js → LpAddressInput-BwX8uo4_.js} +54 -49
  14. package/dist/chunks/{LpAlert-BAYHDSS1.js → LpAlert-B6ilrM7T.js} +16 -9
  15. package/dist/chunks/{LpAppShell-BGQVIY-3.js → LpAppShell-C53_RaGF.js} +53 -26
  16. package/dist/chunks/{LpAutocomplete-DExlIZe_.js → LpAutocomplete-C8IgSMsi.js} +52 -24
  17. package/dist/chunks/{LpAvatar-B3-xAOH3.js → LpAvatar-C1XStdOo.js} +19 -10
  18. package/dist/chunks/LpBreadcrumbs-CQJd73Gm.js +95 -0
  19. package/dist/chunks/{LpButton-5VXXCjwF.js → LpButton-CKSwC6Mu.js} +45 -15
  20. package/dist/chunks/{LpCalendar-B7Ko5RdR.js → LpCalendar-DrA2McTU.js} +1 -1
  21. package/dist/chunks/{LpCard-BhI1N_vL.js → LpCard-C2IvucmX.js} +1 -1
  22. package/dist/chunks/{LpCheckbox-CVccKYaH.js → LpCheckbox-Bkj13CbW.js} +8 -4
  23. package/dist/chunks/{LpCodeBlock-Bo0B4TFR.js → LpCodeBlock-ca7ANFyW.js} +49 -17
  24. package/dist/chunks/{LpColorPicker-BlXea7LM.js → LpColorPicker-zz4VGkG4.js} +75 -26
  25. package/dist/chunks/{LpConfirmDialog-Q-bxff8z.js → LpConfirmDialog-DCw5jR1t.js} +59 -17
  26. package/dist/chunks/LpConfirmHost-CbywMpMP.js +49 -0
  27. package/dist/chunks/LpContextMenu-BqkAYa9X.js +69 -0
  28. package/dist/chunks/LpDatePicker-DuusgT69.js +195 -0
  29. package/dist/chunks/LpDateRangePicker-DHeuznfC.js +208 -0
  30. package/dist/chunks/{LpDisclosure-ChQYmjlR.js → LpDisclosure-Cc23-JOT.js} +1 -1
  31. package/dist/chunks/{LpDrawer-BZ6k3hvo.js → LpDrawer-Dt4aB4Fr.js} +65 -28
  32. package/dist/chunks/{LpDropdownMenu-DJpoqRCb.js → LpDropdownMenu-DdJGQzPn.js} +2 -1
  33. package/dist/chunks/{LpEmptyState-Dsdqz1Wj.js → LpEmptyState-BJbZXC8D.js} +1 -1
  34. package/dist/chunks/LpFieldNotice-wtieLUTj.js +64 -0
  35. package/dist/chunks/{LpFileTree-_5vFBgaK.js → LpFileTree-CNurOvkV.js} +25 -10
  36. package/dist/chunks/{LpFileTreeNode-BdcIYKIE.js → LpFileTreeNode-D_qRRdsH.js} +3 -3
  37. package/dist/chunks/LpFormField-CtVYj6tO.js +101 -0
  38. package/dist/chunks/{LpIcon-CCnX5_2j.js → LpIcon-DYtaid0K.js} +20 -6
  39. package/dist/chunks/{LpInput-9EXWUM9B.js → LpInput-D-YGQbEv.js} +58 -20
  40. package/dist/chunks/{LpLightbox-2sYVMY-S.js → LpLightbox-DSDahF23.js} +148 -61
  41. package/dist/chunks/{LpLink-DCND-zyu.js → LpLink-Krkjhe_y.js} +15 -6
  42. package/dist/chunks/{LpLogViewer-CaRy5MeS.js → LpLogViewer-KypbgOyo.js} +300 -72
  43. package/dist/chunks/{LpMapPicker-ul4oYno6.js → LpMapPicker-Cc84LaM3.js} +39 -14
  44. package/dist/chunks/{LpMediaGallery-FF-NquIo.js → LpMediaGallery-YKr-cQPN.js} +7 -4
  45. package/dist/chunks/{LpModal-Bt0pQCI_.js → LpModal-DMMDQHAu.js} +52 -29
  46. package/dist/chunks/{LpModalNav-YaifHrXC.js → LpModalNav-BVPDyIup.js} +10 -7
  47. package/dist/chunks/{LpNotificationBell-DyGGsMAM.js → LpNotificationBell-CCwyJP5C.js} +79 -33
  48. package/dist/chunks/{LpNumberField-CWuX08iX.js → LpNumberField-B77O-z28.js} +21 -8
  49. package/dist/chunks/{LpNumberFlow-amWYaeuA.js → LpNumberFlow-z4WPZcZu.js} +22 -12
  50. package/dist/chunks/{LpOtpInput-y2v-pmiN.js → LpOtpInput-CziPF3Oq.js} +37 -8
  51. package/dist/chunks/{LpPagination-5FxjmmB5.js → LpPagination-BcDlo3r_.js} +31 -20
  52. package/dist/chunks/{LpPasswordInput--sjYCkLS.js → LpPasswordInput-eKzQhbQP.js} +11 -8
  53. package/dist/chunks/LpPhoneInput-BCbrrhgE.js +336 -0
  54. package/dist/chunks/{LpPie-D6OnzwlJ.js → LpPie-BEJ2SfI6.js} +84 -28
  55. package/dist/chunks/{LpPopover-8268UmlC.js → LpPopover-DCKeqiVn.js} +5 -2
  56. package/dist/chunks/{LpProgress-Btq5W4Ut.js → LpProgress-CBU1iI69.js} +5 -4
  57. package/dist/chunks/{LpRadioGroup-xPIuVDO_.js → LpRadioGroup-DmEW0l78.js} +9 -2
  58. package/dist/chunks/LpRangeCalendar-DZuaRV0W.js +331 -0
  59. package/dist/chunks/{LpRollingText-CM5gkSeF.js → LpRollingText-DB7upf-5.js} +1 -1
  60. package/dist/chunks/{LpScrollArea-D8xyKXcF.js → LpScrollArea-Bry1fdOM.js} +14 -2
  61. package/dist/chunks/{LpSection-aM6J_u51.js → LpSection-tKEH0jYf.js} +1 -1
  62. package/dist/chunks/{LpSegmented-fV0Sk92p.js → LpSegmented-C09EEam2.js} +8 -4
  63. package/dist/chunks/{LpSelect-ANIVtrpM.js → LpSelect-DNIL6MB9.js} +50 -31
  64. package/dist/chunks/{LpSidebar-ClBUwoEz.js → LpSidebar-CQNB0-yU.js} +30 -22
  65. package/dist/chunks/{LpSidebarNav-llOcs_Fn.js → LpSidebarNav-kx-LdqNU.js} +16 -11
  66. package/dist/chunks/{LpSkeleton-CTI-vBgW.js → LpSkeleton-BB6XIV-w.js} +1 -1
  67. package/dist/chunks/{LpSlider-BwNIBkAK.js → LpSlider-Cuzmrd7p.js} +7 -2
  68. package/dist/chunks/{LpSparkline-C6xQ2dwv.js → LpSparkline-CB80N8SJ.js} +11 -4
  69. package/dist/chunks/{LpStat-CQaW8R--.js → LpStat-B-7udBRP.js} +2 -2
  70. package/dist/chunks/{LpStepper-Bk-XaQYU.js → LpStepper-C7XiVKZj.js} +34 -19
  71. package/dist/chunks/{LpSwitch-Blizvqyt.js → LpSwitch-OrcOJX9m.js} +41 -27
  72. package/dist/chunks/LpTableFilter-BV8PpLM9.js +158 -0
  73. package/dist/chunks/{LpTableOfContents-CjvxPMpU.js → LpTableOfContents-BUcAfGPQ.js} +36 -41
  74. package/dist/chunks/{LpTabs-Cefe_lys.js → LpTabs-29iW1XOx.js} +20 -6
  75. package/dist/chunks/LpTagsInput-D1SuqbhB.js +300 -0
  76. package/dist/chunks/LpTextarea-uuScWo6-.js +77 -0
  77. package/dist/chunks/LpThemeProvider-Be4r2TQX.js +35 -0
  78. package/dist/chunks/LpThemeSwitcher-B4aVPbNC.js +166 -0
  79. package/dist/chunks/{LpTilt-DuyKbpkA.js → LpTilt-B7KHX1zH.js} +2 -2
  80. package/dist/chunks/{LpTimeField-K6nAGboX.js → LpTimeField-Cqe7fXnh.js} +14 -8
  81. package/dist/chunks/{LpToaster-gDB1EaU3.js → LpToaster-DpJvoCOh.js} +94 -31
  82. package/dist/chunks/{LpTooltip-hCLPAqMG.js → LpTooltip-CXVKcOXT.js} +14 -6
  83. package/dist/chunks/LpTooltipProvider-CftjV7Xc.js +25 -0
  84. package/dist/chunks/{LpTopologyCanvas-Cm1L39uL.js → LpTopologyCanvas-6ozNrQKh.js} +8 -2
  85. package/dist/chunks/{LpUptimeBar-DvAMno0S.js → LpUptimeBar-ugcv25xj.js} +44 -15
  86. package/dist/chunks/icons-CkA0njzm.js +43 -0
  87. package/dist/chunks/{LpThemeSwitcher-Cue-myST.js → useTheme-DJm1JSsY.js} +215 -260
  88. package/dist/component-names.d.ts +1 -1
  89. package/dist/components/ContextMenuItems.vue.d.ts +8 -0
  90. package/dist/components/ContextMenuItems.vue.js +4 -0
  91. package/dist/components/LayoutCanvas.vue.js +1 -1
  92. package/dist/components/LayoutNode.vue.d.ts +4 -0
  93. package/dist/components/LayoutNode.vue.js +1 -1
  94. package/dist/components/LpActionBar.vue.d.ts +3 -0
  95. package/dist/components/LpActionBar.vue.js +1 -1
  96. package/dist/components/LpAddressInput.vue.d.ts +1 -1
  97. package/dist/components/LpAddressInput.vue.js +1 -1
  98. package/dist/components/LpAlert.vue.d.ts +9 -0
  99. package/dist/components/LpAlert.vue.js +1 -1
  100. package/dist/components/LpAppShell.vue.d.ts +10 -0
  101. package/dist/components/LpAppShell.vue.js +1 -1
  102. package/dist/components/LpAutocomplete.vue.d.ts +31 -6
  103. package/dist/components/LpAutocomplete.vue.js +1 -1
  104. package/dist/components/LpAvatar.vue.d.ts +3 -3
  105. package/dist/components/LpAvatar.vue.js +1 -1
  106. package/dist/components/LpBadge.vue.d.ts +2 -2
  107. package/dist/components/LpBreadcrumbs.vue.d.ts +13 -9
  108. package/dist/components/LpBreadcrumbs.vue.js +1 -1
  109. package/dist/components/LpButton.vue.d.ts +35 -98
  110. package/dist/components/LpButton.vue.js +1 -1
  111. package/dist/components/LpCalendar.vue.js +1 -1
  112. package/dist/components/LpCard.vue.js +1 -1
  113. package/dist/components/LpChart.vue.d.ts +47 -3
  114. package/dist/components/LpChart.vue.js +377 -97
  115. package/dist/components/LpCheckbox.vue.js +1 -1
  116. package/dist/components/LpCodeBlock.vue.d.ts +2 -0
  117. package/dist/components/LpCodeBlock.vue.js +1 -1
  118. package/dist/components/LpColorPicker.vue.d.ts +2 -2
  119. package/dist/components/LpColorPicker.vue.js +1 -1
  120. package/dist/components/LpCommandPalette.vue.d.ts +14 -3
  121. package/dist/components/LpCommandPalette.vue.js +142 -77
  122. package/dist/components/LpConfirmDialog.vue.d.ts +15 -3
  123. package/dist/components/LpConfirmDialog.vue.js +1 -1
  124. package/dist/components/LpConfirmHost.vue.d.ts +9 -0
  125. package/dist/components/LpConfirmHost.vue.js +4 -0
  126. package/dist/components/LpContextMenu.vue.d.ts +3 -5
  127. package/dist/components/LpContextMenu.vue.js +1 -1
  128. package/dist/components/LpDatePicker.vue.d.ts +24 -6
  129. package/dist/components/LpDatePicker.vue.js +1 -1
  130. package/dist/components/LpDateRangePicker.vue.d.ts +60 -0
  131. package/dist/components/LpDateRangePicker.vue.js +4 -0
  132. package/dist/components/LpDisclosure.vue.js +1 -1
  133. package/dist/components/LpDrawer.vue.d.ts +10 -0
  134. package/dist/components/LpDrawer.vue.js +1 -1
  135. package/dist/components/LpDropdownMenu.vue.js +1 -1
  136. package/dist/components/LpDropzone.vue.js +23 -7
  137. package/dist/components/LpEmptyState.vue.js +1 -1
  138. package/dist/components/LpFieldNotice.vue.d.ts +8 -0
  139. package/dist/components/LpFieldNotice.vue.js +4 -0
  140. package/dist/components/LpFileTree.vue.d.ts +11 -2
  141. package/dist/components/LpFileTree.vue.js +1 -1
  142. package/dist/components/LpFileTreeNode.vue.js +1 -1
  143. package/dist/components/LpFormField.vue.d.ts +13 -1
  144. package/dist/components/LpFormField.vue.js +1 -1
  145. package/dist/components/LpIcon.vue.d.ts +0 -1
  146. package/dist/components/LpIcon.vue.js +1 -1
  147. package/dist/components/LpImageEditor.vue.js +93 -58
  148. package/dist/components/LpInput.vue.d.ts +22 -3
  149. package/dist/components/LpInput.vue.js +1 -1
  150. package/dist/components/LpLightbox.vue.d.ts +17 -0
  151. package/dist/components/LpLightbox.vue.js +1 -1
  152. package/dist/components/LpLink.vue.d.ts +11 -11
  153. package/dist/components/LpLink.vue.js +1 -1
  154. package/dist/components/LpLogViewer.vue.d.ts +72 -3
  155. package/dist/components/LpLogViewer.vue.js +1 -1
  156. package/dist/components/LpMapPicker.vue.d.ts +4 -4
  157. package/dist/components/LpMapPicker.vue.js +1 -1
  158. package/dist/components/LpMediaGallery.vue.js +1 -1
  159. package/dist/components/LpModal.vue.d.ts +16 -6
  160. package/dist/components/LpModal.vue.js +1 -1
  161. package/dist/components/LpModalNav.vue.d.ts +0 -1
  162. package/dist/components/LpModalNav.vue.js +1 -1
  163. package/dist/components/LpNotificationBell.vue.d.ts +11 -2
  164. package/dist/components/LpNotificationBell.vue.js +1 -1
  165. package/dist/components/LpNumberField.vue.d.ts +2 -0
  166. package/dist/components/LpNumberField.vue.js +1 -1
  167. package/dist/components/LpNumberFlow.vue.js +1 -1
  168. package/dist/components/LpOtpInput.vue.js +1 -1
  169. package/dist/components/LpPagination.vue.d.ts +2 -0
  170. package/dist/components/LpPagination.vue.js +1 -1
  171. package/dist/components/LpPasswordInput.vue.js +1 -1
  172. package/dist/components/LpPhoneInput.vue.d.ts +13 -0
  173. package/dist/components/LpPhoneInput.vue.js +1 -1
  174. package/dist/components/LpPie.vue.d.ts +21 -2
  175. package/dist/components/LpPie.vue.js +1 -1
  176. package/dist/components/LpPopover.vue.d.ts +2 -0
  177. package/dist/components/LpPopover.vue.js +1 -1
  178. package/dist/components/LpProgress.vue.d.ts +1 -1
  179. package/dist/components/LpProgress.vue.js +1 -1
  180. package/dist/components/LpRadioGroup.vue.js +1 -1
  181. package/dist/components/LpRangeCalendar.vue.d.ts +51 -0
  182. package/dist/components/LpRangeCalendar.vue.js +4 -0
  183. package/dist/components/LpRollingText.vue.d.ts +1 -1
  184. package/dist/components/LpRollingText.vue.js +1 -1
  185. package/dist/components/LpScrollArea.vue.d.ts +2 -2
  186. package/dist/components/LpScrollArea.vue.js +1 -1
  187. package/dist/components/LpSection.vue.d.ts +1 -1
  188. package/dist/components/LpSection.vue.js +1 -1
  189. package/dist/components/LpSegmented.vue.d.ts +5 -0
  190. package/dist/components/LpSegmented.vue.js +1 -1
  191. package/dist/components/LpSelect.vue.d.ts +1 -0
  192. package/dist/components/LpSelect.vue.js +1 -1
  193. package/dist/components/LpShortcutsDialog.vue.d.ts +7 -1
  194. package/dist/components/LpShortcutsDialog.vue.js +60 -15
  195. package/dist/components/LpSidebar.vue.d.ts +6 -2
  196. package/dist/components/LpSidebar.vue.js +1 -1
  197. package/dist/components/LpSidebarNav.vue.js +1 -1
  198. package/dist/components/LpSkeleton.vue.js +1 -1
  199. package/dist/components/LpSlider.vue.js +1 -1
  200. package/dist/components/LpSparkline.vue.d.ts +1 -0
  201. package/dist/components/LpSparkline.vue.js +1 -1
  202. package/dist/components/LpStat.vue.js +1 -1
  203. package/dist/components/LpStepper.vue.js +1 -1
  204. package/dist/components/LpSwitch.vue.js +1 -1
  205. package/dist/components/LpTable.vue.d.ts +107 -3
  206. package/dist/components/LpTable.vue.js +540 -128
  207. package/dist/components/LpTableFilter.vue.d.ts +31 -0
  208. package/dist/components/LpTableFilter.vue.js +4 -0
  209. package/dist/components/LpTableOfContents.vue.d.ts +4 -2
  210. package/dist/components/LpTableOfContents.vue.js +1 -1
  211. package/dist/components/LpTabs.vue.d.ts +2 -0
  212. package/dist/components/LpTabs.vue.js +1 -1
  213. package/dist/components/LpTagsInput.vue.d.ts +77 -0
  214. package/dist/components/LpTagsInput.vue.js +4 -0
  215. package/dist/components/LpTextarea.vue.d.ts +19 -6
  216. package/dist/components/LpTextarea.vue.js +1 -1
  217. package/dist/components/LpThemeProvider.vue.d.ts +22 -0
  218. package/dist/components/LpThemeProvider.vue.js +4 -0
  219. package/dist/components/LpThemeSwitcher.vue.d.ts +15 -2
  220. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  221. package/dist/components/LpTilt.vue.js +1 -1
  222. package/dist/components/LpTimeField.vue.js +1 -1
  223. package/dist/components/LpToaster.vue.d.ts +6 -1
  224. package/dist/components/LpToaster.vue.js +1 -1
  225. package/dist/components/LpTooltip.vue.d.ts +1 -2
  226. package/dist/components/LpTooltip.vue.js +1 -1
  227. package/dist/components/LpTooltipProvider.vue.d.ts +22 -0
  228. package/dist/components/LpTooltipProvider.vue.js +4 -0
  229. package/dist/components/LpTopologyCanvas.vue.js +1 -1
  230. package/dist/components/LpUptimeBar.vue.d.ts +4 -0
  231. package/dist/components/LpUptimeBar.vue.js +1 -1
  232. package/dist/components/chart.d.ts +32 -1
  233. package/dist/components/chart.js +42 -6
  234. package/dist/components/codeHighlight.d.ts +6 -0
  235. package/dist/components/codeHighlight.js +92 -20
  236. package/dist/components/color.d.ts +1 -1
  237. package/dist/components/color.js +20 -9
  238. package/dist/components/commandSearch.js +3 -0
  239. package/dist/components/confirm.d.ts +25 -0
  240. package/dist/components/confirm.js +26 -0
  241. package/dist/components/countries.d.ts +6 -0
  242. package/dist/components/countries.js +7 -0
  243. package/dist/components/dateRange.d.ts +43 -0
  244. package/dist/components/dateRange.js +76 -0
  245. package/dist/components/exportData.d.ts +58 -0
  246. package/dist/components/exportData.js +165 -0
  247. package/dist/components/fileTree.js +16 -7
  248. package/dist/components/imageCrop.d.ts +42 -0
  249. package/dist/components/imageCrop.js +47 -0
  250. package/dist/components/link.d.ts +12 -0
  251. package/dist/components/link.js +19 -0
  252. package/dist/components/logSearch.d.ts +18 -0
  253. package/dist/components/logSearch.js +36 -0
  254. package/dist/components/modalDepth.d.ts +18 -2
  255. package/dist/components/modalDepth.js +17 -5
  256. package/dist/components/otp.d.ts +5 -0
  257. package/dist/components/otp.js +10 -0
  258. package/dist/components/phone.d.ts +41 -0
  259. package/dist/components/phone.js +80 -0
  260. package/dist/components/sidebar.d.ts +7 -1
  261. package/dist/components/tableFilter.d.ts +36 -0
  262. package/dist/components/tableFilter.js +84 -0
  263. package/dist/components/tableSort.d.ts +8 -0
  264. package/dist/components/tableSort.js +31 -0
  265. package/dist/components/tags.d.ts +20 -0
  266. package/dist/components/tags.js +37 -0
  267. package/dist/composables/useField.d.ts +38 -0
  268. package/dist/composables/useField.js +45 -0
  269. package/dist/composables/useFieldNotice.d.ts +17 -0
  270. package/dist/composables/useFieldNotice.js +28 -0
  271. package/dist/composables/useForm.d.ts +53 -0
  272. package/dist/composables/useForm.js +207 -0
  273. package/dist/composables/useFormDraft.d.ts +10 -3
  274. package/dist/composables/useFormDraft.js +9 -14
  275. package/dist/composables/useHotkeys.d.ts +81 -5
  276. package/dist/composables/useHotkeys.js +223 -25
  277. package/dist/composables/useInputFilter.d.ts +17 -0
  278. package/dist/composables/useInputFilter.js +47 -10
  279. package/dist/composables/useInputMask.d.ts +59 -0
  280. package/dist/composables/useInputMask.js +170 -0
  281. package/dist/composables/useLpConfig.d.ts +24 -0
  282. package/dist/composables/useLpConfig.js +44 -0
  283. package/dist/composables/usePaintSelect.d.ts +22 -0
  284. package/dist/composables/usePaintSelect.js +169 -0
  285. package/dist/composables/useRovingFocus.js +7 -0
  286. package/dist/composables/useShift.js +7 -2
  287. package/dist/composables/useTilt.d.ts +2 -2
  288. package/dist/composables/useTilt.js +5 -6
  289. package/dist/composables/useToast.d.ts +20 -0
  290. package/dist/composables/useToast.js +28 -4
  291. package/dist/composables/useZoomPan.d.ts +3 -3
  292. package/dist/composables/useZoomPan.js +4 -4
  293. package/dist/icons-all.d.ts +1 -0
  294. package/dist/icons-all.js +3 -0
  295. package/dist/icons.d.ts +14 -1
  296. package/dist/index.d.ts +60 -4
  297. package/dist/index.js +245 -141
  298. package/dist/kit-icons.d.ts +2 -0
  299. package/dist/layout/tree.d.ts +2 -0
  300. package/dist/layout/tree.js +16 -1
  301. package/dist/layout/useLayout.d.ts +15 -0
  302. package/dist/layout/useLayout.js +41 -14
  303. package/dist/theme/backdrop.d.ts +12 -0
  304. package/dist/theme/presets.d.ts +0 -2
  305. package/dist/theme/useTheme.d.ts +32 -9
  306. package/package.json +18 -8
  307. package/src/component-names.ts +9 -7
  308. package/src/components/ContextMenuItems.vue +88 -0
  309. package/src/components/LayoutCanvas.vue +26 -6
  310. package/src/components/LayoutNode.vue +30 -4
  311. package/src/components/LpActionBar.vue +31 -8
  312. package/src/components/LpAddressInput.vue +57 -41
  313. package/src/components/LpAlert.vue +16 -3
  314. package/src/components/LpAppShell.vue +45 -12
  315. package/src/components/LpAutocomplete.vue +47 -10
  316. package/src/components/LpAvatar.vue +20 -6
  317. package/src/components/LpBreadcrumbs.vue +65 -41
  318. package/src/components/LpButton.vue +60 -14
  319. package/src/components/LpChart.vue +351 -28
  320. package/src/components/LpCheckbox.vue +7 -3
  321. package/src/components/LpCodeBlock.vue +50 -14
  322. package/src/components/LpColorPicker.vue +73 -10
  323. package/src/components/LpCommandPalette.vue +115 -20
  324. package/src/components/LpConfirmDialog.vue +68 -17
  325. package/src/components/LpConfirmHost.vue +59 -0
  326. package/src/components/LpContextMenu.vue +15 -116
  327. package/src/components/LpDatePicker.vue +104 -68
  328. package/src/components/LpDateRangePicker.vue +224 -0
  329. package/src/components/LpDrawer.vue +56 -19
  330. package/src/components/LpDropdownMenu.vue +1 -0
  331. package/src/components/LpDropzone.vue +21 -4
  332. package/src/components/LpFieldNotice.vue +65 -0
  333. package/src/components/LpFileTree.vue +33 -7
  334. package/src/components/LpFormField.vue +59 -7
  335. package/src/components/LpIcon.vue +22 -4
  336. package/src/components/LpImageEditor.vue +96 -57
  337. package/src/components/LpInput.vue +62 -11
  338. package/src/components/LpLightbox.vue +140 -29
  339. package/src/components/LpLink.vue +24 -13
  340. package/src/components/LpLogViewer.vue +311 -39
  341. package/src/components/LpMapPicker.vue +36 -13
  342. package/src/components/LpMediaGallery.vue +8 -2
  343. package/src/components/LpModal.vue +54 -28
  344. package/src/components/LpModalNav.vue +5 -2
  345. package/src/components/LpNotificationBell.vue +77 -23
  346. package/src/components/LpNumberField.vue +21 -6
  347. package/src/components/LpNumberFlow.vue +26 -10
  348. package/src/components/LpOtpInput.vue +43 -8
  349. package/src/components/LpPagination.vue +23 -7
  350. package/src/components/LpPasswordInput.vue +4 -1
  351. package/src/components/LpPhoneInput.vue +224 -39
  352. package/src/components/LpPie.vue +102 -20
  353. package/src/components/LpPopover.vue +4 -0
  354. package/src/components/LpProgress.vue +5 -3
  355. package/src/components/LpRadioGroup.vue +9 -2
  356. package/src/components/LpRangeCalendar.vue +365 -0
  357. package/src/components/LpRollingText.vue +3 -0
  358. package/src/components/LpScrollArea.vue +22 -2
  359. package/src/components/LpSegmented.vue +12 -2
  360. package/src/components/LpSelect.vue +0 -0
  361. package/src/components/LpShortcutsDialog.vue +83 -9
  362. package/src/components/LpSidebar.vue +14 -9
  363. package/src/components/LpSidebarNav.vue +17 -5
  364. package/src/components/LpSkeleton.vue +4 -1
  365. package/src/components/LpSlider.vue +7 -1
  366. package/src/components/LpSparkline.vue +11 -3
  367. package/src/components/LpStepper.vue +23 -10
  368. package/src/components/LpSwitch.vue +31 -8
  369. package/src/components/LpTable.vue +668 -90
  370. package/src/components/LpTableFilter.vue +175 -0
  371. package/src/components/LpTableOfContents.vue +50 -32
  372. package/src/components/LpTabs.vue +23 -6
  373. package/src/components/LpTagsInput.vue +364 -0
  374. package/src/components/LpTextarea.vue +44 -6
  375. package/src/components/LpThemeProvider.vue +41 -0
  376. package/src/components/LpThemeSwitcher.vue +76 -33
  377. package/src/components/LpTilt.vue +2 -2
  378. package/src/components/LpTimeField.vue +14 -3
  379. package/src/components/LpToaster.vue +115 -27
  380. package/src/components/LpTooltip.vue +29 -5
  381. package/src/components/LpTooltipProvider.vue +24 -0
  382. package/src/components/LpTopologyCanvas.vue +19 -2
  383. package/src/components/LpUptimeBar.vue +46 -8
  384. package/src/components/chart.ts +82 -8
  385. package/src/components/codeHighlight.ts +111 -15
  386. package/src/components/color.ts +30 -9
  387. package/src/components/commandSearch.ts +6 -0
  388. package/src/components/confirm.ts +58 -0
  389. package/src/components/countries.ts +12 -0
  390. package/src/components/dateRange.ts +134 -0
  391. package/src/components/exportData.ts +228 -0
  392. package/src/components/fileTree.ts +19 -7
  393. package/src/components/imageCrop.ts +107 -0
  394. package/src/components/link.ts +34 -0
  395. package/src/components/logSearch.ts +46 -0
  396. package/src/components/modalDepth.ts +48 -6
  397. package/src/components/otp.ts +16 -0
  398. package/src/components/phone.ts +140 -0
  399. package/src/components/sidebar.ts +7 -1
  400. package/src/components/tableFilter.ts +122 -0
  401. package/src/components/tableSort.ts +37 -0
  402. package/src/components/tags.ts +62 -0
  403. package/src/composables/useField.ts +83 -0
  404. package/src/composables/useFieldNotice.ts +59 -0
  405. package/src/composables/useForm.ts +314 -0
  406. package/src/composables/useFormDraft.ts +25 -20
  407. package/src/composables/useHotkeys.ts +355 -28
  408. package/src/composables/useInputFilter.ts +77 -14
  409. package/src/composables/useInputMask.ts +254 -0
  410. package/src/composables/useLpConfig.ts +87 -0
  411. package/src/composables/usePaintSelect.ts +243 -0
  412. package/src/composables/useRovingFocus.ts +11 -0
  413. package/src/composables/useShift.ts +13 -2
  414. package/src/composables/useTilt.ts +5 -6
  415. package/src/composables/useToast.ts +56 -4
  416. package/src/composables/useZoomPan.ts +6 -6
  417. package/src/iconSubset.d.mts +10 -0
  418. package/src/iconSubset.mjs +51 -0
  419. package/src/icons-all.ts +10 -0
  420. package/src/icons.ts +59 -8
  421. package/src/index.ts +73 -4
  422. package/src/kit-icons.ts +5 -0
  423. package/src/layout/tree.ts +30 -0
  424. package/src/layout/useLayout.ts +68 -18
  425. package/src/nuxt.ts +87 -17
  426. package/src/runtime/config.plugin.ts +14 -0
  427. package/src/runtime/theme.plugin.ts +43 -23
  428. package/src/theme/backdrop.ts +69 -0
  429. package/src/theme/presets.ts +6 -64
  430. package/src/theme/useTheme.ts +206 -100
  431. package/src/tokens/tokens.css +46 -0
  432. package/dist/chunks/LpBreadcrumbs-p4jHe_V-.js +0 -62
  433. package/dist/chunks/LpContextMenu-CwW1QXe_.js +0 -217
  434. package/dist/chunks/LpDatePicker-CwOToxGq.js +0 -164
  435. package/dist/chunks/LpFormField-CazBc2-a.js +0 -62
  436. package/dist/chunks/LpPhoneInput-BDSKVl_z.js +0 -209
  437. 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,12 +41,32 @@ 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"
52
+ import { backgroundOf, downloadFile, fileStem, resolveCssColor, svgToPng, toCsv, type CsvOptions } from "./exportData"
53
+
54
+ export interface ChartText {
55
+ [key: string]: string
56
+ empty: string
57
+ rightAxis: string
58
+ /** Prefix of the keyboard readout, before the point's label. */
59
+ point: string
60
+ /** Header of the label column in a CSV export. */
61
+ xColumn: string
62
+ }
63
+
64
+ export interface ChartExportOptions {
65
+ /** File name without extension. Defaults to the title. */
66
+ filename?: string
67
+ /** Return the data without offering a download. */
68
+ download?: boolean
69
+ }
47
70
 
48
71
  /** What was under the pointer when a point was clicked or right-clicked. */
49
72
  export interface ChartPointEvent {
@@ -94,6 +117,16 @@ const props = withDefaults(
94
117
  animate?: boolean
95
118
  /** Shown when every series is empty. */
96
119
  emptyLabel?: string
120
+ /** Reference lines and bands (SLOs, budgets, alert levels). */
121
+ thresholds?: ChartThreshold[]
122
+ /** Vertical markers at x positions (deploys, incidents). */
123
+ markers?: ChartMarker[]
124
+ /** Accessible name for the plot. Defaults to a generated summary. */
125
+ ariaLabel?: string
126
+ /** Render the data as a visually hidden table for screen readers. */
127
+ dataTable?: boolean
128
+ /** Text overrides (`labels` is taken by the x axis). */
129
+ text?: Partial<ChartText>
97
130
  /**
98
131
  * Right-click menu for a point, built from whatever was hit. Return [] to
99
132
  * leave the browser's own menu alone for that point. Which actions a point
@@ -113,10 +146,18 @@ const props = withDefaults(
113
146
  showLegend: true,
114
147
  yTicks: 4,
115
148
  animate: true,
116
- emptyLabel: "No data",
149
+ thresholds: () => [],
150
+ markers: () => [],
151
+ dataTable: false,
117
152
  },
118
153
  )
119
154
 
155
+ const strings = useLabels<ChartText>(
156
+ "chart",
157
+ { empty: "No data", rightAxis: "Measured on the right axis", point: "Point", xColumn: "Label" },
158
+ () => props.text,
159
+ )
160
+
120
161
  const emit = defineEmits<{
121
162
  /** Click on a point/bar. */
122
163
  (e: "pointClick", payload: ChartPointEvent): void
@@ -139,12 +180,16 @@ let ro: ResizeObserver | null = null
139
180
  * two combine into one question: should this draw itself right now?
140
181
  */
141
182
  const { el: revealAnchor, revealed } = useReveal({ immediate: () => !props.animate })
142
- const drawing = computed(() => props.animate && revealed.value)
183
+ // SMIL ignores the reduced-motion media query, so the wipe is skipped here.
184
+ const reduced = ref(false)
185
+ const still = computed(() => !props.animate || reduced.value)
186
+ const drawing = computed(() => !still.value && revealed.value)
143
187
 
144
188
  onMounted(() => {
145
189
  // One element, two observers: hand the composable the same node rather than
146
190
  // wrapping the chart in a spare div just to carry a second ref.
147
191
  revealAnchor.value = root.value
192
+ reduced.value = prefersReducedMotion()
148
193
  if (!root.value || typeof ResizeObserver === "undefined") return
149
194
  ro = new ResizeObserver(([entry]) => {
150
195
  width.value = entry.contentRect.width
@@ -165,16 +210,19 @@ onBeforeUnmount(() => {
165
210
  // ── data ───────────────────────────────────────
166
211
  const hidden = ref(new Set<string>())
167
212
 
168
- const toggleSeries = (name: string) => {
213
+ const toggleSeries = (id: string) => {
169
214
  const next = new Set(hidden.value)
170
- if (next.has(name)) next.delete(name)
171
- else next.add(name)
215
+ if (next.has(id)) next.delete(id)
216
+ else next.add(id)
172
217
  // Never hide the last visible series — an empty plot reads as broken.
173
218
  if (next.size < props.series.length) hidden.value = next
174
219
  }
175
220
 
176
- const allSeries = computed(() => normalise(props.series))
177
- const visible = computed(() => allSeries.value.filter((s) => !hidden.value.has(s.name)))
221
+ // Identity is position plus name, so two series sharing a name stay separate.
222
+ const allSeries = computed(() =>
223
+ normalise(props.series).map((s, i) => ({ ...s, id: `${i}:${s.name}` })),
224
+ )
225
+ const visible = computed(() => allSeries.value.filter((s) => !hidden.value.has(s.id)))
178
226
 
179
227
  /*
180
228
  * How present each series is, 1 to 0, driven per frame rather than by CSS.
@@ -206,7 +254,7 @@ let presenceFrame: number | undefined
206
254
  * reported the end state — so a series being hidden jumped to the floor while
207
255
  * one being shown eased up from it.
208
256
  */
209
- const presenceOf = (name: string) => presence.value.get(name) ?? 1
257
+ const presenceOf = (id: string) => presence.value.get(id) ?? 1
210
258
 
211
259
  watch(
212
260
  // A string, not the Set: `hidden` is replaced wholesale on every toggle, and a
@@ -214,15 +262,15 @@ watch(
214
262
  // and sees a change it cannot describe — or, with a stale copy in the array,
215
263
  // no change at all. The names that are currently off say it exactly.
216
264
  () =>
217
- allSeries.value.map((s) => `${s.name}:${hidden.value.has(s.name) ? 0 : 1}`).join(","),
265
+ allSeries.value.map((s) => `${s.id}:${hidden.value.has(s.id) ? 0 : 1}`).join(","),
218
266
  () => {
219
- const targets = new Map(allSeries.value.map((s) => [s.name, hidden.value.has(s.name) ? 0 : 1]))
267
+ const targets = new Map(allSeries.value.map((s) => [s.id, hidden.value.has(s.id) ? 0 : 1]))
220
268
  if (!props.animate || prefersReducedMotion()) {
221
269
  presence.value = targets
222
270
  return
223
271
  }
224
272
 
225
- const from = new Map(allSeries.value.map((s) => [s.name, presenceOf(s.name)]))
273
+ const from = new Map(allSeries.value.map((s) => [s.id, presenceOf(s.id)]))
226
274
  if ([...targets].every(([name, to]) => from.get(name) === to)) return
227
275
 
228
276
  const start = performance.now()
@@ -299,12 +347,27 @@ const hasRightAxis = computed(
299
347
  () => declaredRight.value.length > 0 && declaredLeft.value.length > 0,
300
348
  )
301
349
 
350
+ /** A reference level must be on the plot, or it says nothing. */
351
+ const withThresholds = (raw: { min: number; max: number }, axis: "left" | "right") => {
352
+ let { min, max } = raw
353
+ for (const t of props.thresholds) {
354
+ if ((t.axis ?? "left") !== axis) continue
355
+ for (const v of [t.value, t.to]) {
356
+ if (v == null || !Number.isFinite(v)) continue
357
+ min = Math.min(min, v)
358
+ max = Math.max(max, v)
359
+ }
360
+ }
361
+ return { min, max }
362
+ }
363
+
302
364
  const scaleFor = (members: typeof visible.value) => {
303
365
  const totals = stackTotals.value
304
366
  const raw = totals
305
367
  ? { min: Math.min(0, ...totals.neg), max: Math.max(0, ...totals.pos) }
306
368
  : extent(members, zeroBased.value)
307
- return niceScale(raw.min, raw.max, props.yTicks)
369
+ const wide = hasData.value ? withThresholds(raw, "left") : raw
370
+ return niceScale(wide.min, wide.max, props.yTicks)
308
371
  }
309
372
 
310
373
  const targetScale = computed(() =>
@@ -367,7 +430,7 @@ const targetRightScale = computed(() => {
367
430
  axis holds the range it had — an axis that keeps its place while its curve
368
431
  leaves, rather than collapsing to a degenerate 0..1 behind it. */
369
432
  const members = onRight.value.length ? onRight.value : declaredRight.value
370
- const raw = extent(members, zeroBased.value)
433
+ const raw = withThresholds(extent(members, zeroBased.value), "right")
371
434
  /* Steps come from the TARGET left scale. Reading the animated one rebuilt this
372
435
  axis from a fractional range every frame, so a right-hand series jittered
373
436
  against a scale being redrawn underneath it. */
@@ -588,7 +651,7 @@ const baseY = computed(() =>
588
651
  const lines = computed(() =>
589
652
  allSeries.value.map((s) => {
590
653
  const g = geoOf(s)
591
- const p = presenceOf(s.name)
654
+ const p = presenceOf(s.id)
592
655
  // Values pulled toward the axis floor, so the curve flattens into it.
593
656
  const base = Math.min(Math.max(0, g.min), g.max)
594
657
  const values = p === 1 ? s.values : s.values.map((v) => (v == null ? null : base + (v - base) * p))
@@ -660,7 +723,7 @@ const bars = computed(() => {
660
723
  h = Math.abs(top - base)
661
724
  }
662
725
  out.push({
663
- key: `${s.name}-${i}`,
726
+ key: `${s.id}-${i}`,
664
727
  x,
665
728
  y,
666
729
  w: Math.max(1, slot - (props.stacked ? 0 : 2)),
@@ -711,6 +774,7 @@ const active = ref(-1)
711
774
  const pointer = ref({ x: 0, y: 0 })
712
775
 
713
776
  const onMove = (e: PointerEvent) => {
777
+ keyboard.value = false
714
778
  const el = e.currentTarget as SVGElement
715
779
  const rect = el.getBoundingClientRect()
716
780
  const x = e.clientX - rect.left
@@ -729,7 +793,11 @@ const pointEvent = (originalEvent: MouseEvent): ChartPointEvent => ({
729
793
  originalEvent,
730
794
  })
731
795
 
796
+ /* A tap fires pointerleave before click, which has already cleared the hover,
797
+ so the index comes from the click itself. */
732
798
  const onClick = (e: MouseEvent) => {
799
+ const rect = (e.currentTarget as SVGElement).getBoundingClientRect()
800
+ active.value = nearestIndex(e.clientX - rect.left, geo.value)
733
801
  if (active.value < 0) return
734
802
  emit("pointClick", pointEvent(e))
735
803
  }
@@ -772,6 +840,7 @@ const activePoints = computed(() => {
772
840
  const sc = scaleOf(s)
773
841
  return {
774
842
  color: s.color,
843
+ id: s.id,
775
844
  name: s.name,
776
845
  value: v,
777
846
  y: yAt(v, sc.min, sc.max, box.value),
@@ -783,6 +852,7 @@ const tooltipRows = computed(() =>
783
852
  active.value < 0
784
853
  ? []
785
854
  : visible.value.map((s) => ({
855
+ id: s.id,
786
856
  name: s.name,
787
857
  color: s.color,
788
858
  text:
@@ -825,12 +895,13 @@ const floatTransition = usePillTransition("float")
825
895
  // One clip rect wipes across the plot; re-keyed on data change so an update
826
896
  // replays the reveal instead of snapping.
827
897
  const revealKey = shallowRef(0)
898
+ /* Keyed on the chart's SHAPE, not its values: a polled dashboard refreshing
899
+ its numbers every few seconds must not re-wipe on every tick. */
828
900
  watch(
829
- () => [props.series, props.type] as const,
901
+ () => `${props.type}|${count.value}|${allSeries.value.map((s) => s.id).join(",")}`,
830
902
  () => {
831
903
  revealKey.value++
832
904
  },
833
- { deep: true },
834
905
  )
835
906
 
836
907
  const clipId = `lp-chart-${Math.random().toString(36).slice(2, 9)}`
@@ -854,6 +925,171 @@ const clipId = `lp-chart-${Math.random().toString(36).slice(2, 9)}`
854
925
  const gradientId = (name: string, index: number) =>
855
926
  `${clipId}-g-${index}-${name.replace(/[^a-zA-Z0-9_-]/g, "-")}`
856
927
 
928
+ // ── overlays ───────────────────────────────────
929
+ const thresholdMarks = computed(() =>
930
+ props.thresholds.flatMap((t, i) => {
931
+ if (!Number.isFinite(t.value) || width.value <= 0) return []
932
+ const sc = t.axis === "right" && rightScale.value ? rightScale.value : scale.value
933
+ const y = yAt(t.value, sc.min, sc.max, box.value)
934
+ const to = t.to != null && Number.isFinite(t.to) ? yAt(t.to, sc.min, sc.max, box.value) : null
935
+ return [
936
+ {
937
+ key: i,
938
+ y,
939
+ top: to == null ? y : Math.min(y, to),
940
+ height: to == null ? 0 : Math.abs(to - y),
941
+ label: t.label,
942
+ color: t.color ?? "var(--color-line-strong)",
943
+ solid: !!t.solid,
944
+ },
945
+ ]
946
+ }),
947
+ )
948
+
949
+ const markerMarks = computed(() =>
950
+ props.markers.flatMap((m, i) => {
951
+ const index = markerIndex(m.at, labels.value, count.value)
952
+ if (index < 0 || width.value <= 0) return []
953
+ return [
954
+ {
955
+ key: i,
956
+ x: xAt(index, count.value, box.value, banded.value),
957
+ label: m.label,
958
+ color: m.color ?? "var(--color-line-strong)",
959
+ },
960
+ ]
961
+ }),
962
+ )
963
+
964
+ // ── keyboard & assistive text ──────────────────
965
+ const summary = computed(
966
+ () => props.ariaLabel ?? chartSummary(visible.value, labels.value, fmtSeries, props.title),
967
+ )
968
+
969
+ /* Arrow keys walk the points the way the pointer does; the readout below is
970
+ announced politely so each step is heard without stealing focus. */
971
+ const keyboard = ref(false)
972
+
973
+ const onKeydown = (e: KeyboardEvent) => {
974
+ if (!count.value || !hasData.value) return
975
+ const last = count.value - 1
976
+ let next: number
977
+ switch (e.key) {
978
+ case "ArrowRight":
979
+ next = active.value < 0 ? 0 : Math.min(last, active.value + 1)
980
+ break
981
+ case "ArrowLeft":
982
+ next = active.value < 0 ? last : Math.max(0, active.value - 1)
983
+ break
984
+ case "Home":
985
+ next = 0
986
+ break
987
+ case "End":
988
+ next = last
989
+ break
990
+ case "Enter":
991
+ case " ":
992
+ if (active.value < 0) return
993
+ e.preventDefault()
994
+ emit("pointClick", pointEvent(new MouseEvent("click")))
995
+ return
996
+ case "Escape":
997
+ active.value = -1
998
+ return
999
+ default:
1000
+ return
1001
+ }
1002
+ e.preventDefault()
1003
+ keyboard.value = true
1004
+ active.value = next
1005
+ const first = activePoints.value[0]
1006
+ pointer.value = {
1007
+ x: activeX.value,
1008
+ y: first ? first.y : box.value.top + plotHeight(box.value) / 2,
1009
+ }
1010
+ }
1011
+
1012
+ const onBlur = () => {
1013
+ if (keyboard.value) active.value = -1
1014
+ keyboard.value = false
1015
+ }
1016
+
1017
+ const readout = computed(() =>
1018
+ keyboard.value && active.value >= 0
1019
+ ? `${labels.value[active.value] ?? `${strings.value.point} ${active.value + 1}`}: ${tooltipRows.value
1020
+ .map((r) => `${r.name} ${r.text}`)
1021
+ .join(", ")}`
1022
+ : "",
1023
+ )
1024
+
1025
+ // ── export ─────────────────────────────────────
1026
+ // Every series, hidden ones included: what a reader toggled off for a closer
1027
+ // look is still part of the data they asked to take away.
1028
+ const svgEl = ref<SVGSVGElement | null>(null)
1029
+
1030
+ function exportCsv(options: ChartExportOptions & CsvOptions = {}): string {
1031
+ const rows = labels.value.map((label, i) => ({ label, i }))
1032
+ const csv = toCsv(rows, [
1033
+ { key: "label", label: strings.value.xColumn },
1034
+ ...allSeries.value.map((s) => ({ key: s.id, label: s.name, value: (r: { i: number }) => s.values[r.i] })),
1035
+ ], options)
1036
+ if (options.download !== false) {
1037
+ downloadFile(`${fileStem(options.filename ?? props.title, "chart")}.csv`, csv, "text/csv;charset=utf-8")
1038
+ }
1039
+ return csv
1040
+ }
1041
+
1042
+ /*
1043
+ * The plot as a picture, in the theme it is shown in. The legend and title are
1044
+ * HTML, so they are painted onto the canvas separately, in the page's font; the
1045
+ * SVG travels with its computed colours inlined, because an SVG drawn as an
1046
+ * image cannot see the page's custom properties.
1047
+ */
1048
+ async function exportPng(options: ChartExportOptions & { scale?: number; background?: string } = {}): Promise<Blob | null> {
1049
+ const svg = svgEl.value
1050
+ if (!svg || typeof document === "undefined") return null
1051
+ const style = getComputedStyle(root.value ?? svg)
1052
+ const font = style.fontFamily
1053
+ const ink = style.getPropertyValue("--color-ink").trim() || style.color
1054
+ const muted = style.getPropertyValue("--color-muted-strong").trim() || style.color
1055
+ const legend = props.showLegend && allSeries.value.length > 1 ? visible.value : []
1056
+ const blob = await svgToPng(svg, {
1057
+ scale: options.scale,
1058
+ padding: 16,
1059
+ background: options.background ?? backgroundOf(root.value),
1060
+ header: (ctx) => {
1061
+ let y = 0
1062
+ if (props.title) {
1063
+ ctx.font = `500 14px ${font}`
1064
+ ctx.fillStyle = ink
1065
+ ctx.textBaseline = "top"
1066
+ ctx.fillText(props.title, 0, y)
1067
+ y += 22
1068
+ }
1069
+ if (legend.length) {
1070
+ ctx.font = `12px ${font}`
1071
+ ctx.textBaseline = "middle"
1072
+ let x = 0
1073
+ for (const s of legend) {
1074
+ ctx.fillStyle = resolveCssColor(s.color, root.value ?? svg)
1075
+ ctx.beginPath()
1076
+ ctx.arc(x + 4, y + 8, 4, 0, Math.PI * 2)
1077
+ ctx.fill()
1078
+ ctx.fillStyle = muted
1079
+ ctx.fillText(s.name, x + 14, y + 8)
1080
+ x += 14 + ctx.measureText(s.name).width + 16
1081
+ }
1082
+ y += 24
1083
+ }
1084
+ return y ? y + 8 : 0
1085
+ },
1086
+ })
1087
+ if (options.download !== false) downloadFile(`${fileStem(options.filename ?? props.title, "chart")}.png`, blob)
1088
+ return blob
1089
+ }
1090
+
1091
+ defineExpose({ exportCsv, exportPng })
1092
+
857
1093
  // 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
1094
  const { class: rootClass, attrs: rest } = useMergedAttrs(
859
1095
  "flex w-full flex-col gap-3",
@@ -878,11 +1114,12 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
878
1114
  >
879
1115
  <button
880
1116
  v-for="s in allSeries"
881
- :key="s.name"
1117
+ :key="s.id"
882
1118
  type="button"
883
1119
  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)"
1120
+ :class="hidden.has(s.id) ? 'opacity-40' : ''"
1121
+ :aria-pressed="!hidden.has(s.id)"
1122
+ @click="toggleSeries(s.id)"
886
1123
  >
887
1124
  <span
888
1125
  class="size-2 shrink-0 rounded-full transition-transform duration-fast group-hover:scale-125"
@@ -892,7 +1129,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
892
1129
  <span
893
1130
  v-if="hasRightAxis && s.axis === 'right'"
894
1131
  class="text-[10px] text-muted"
895
- title="Measured on the right axis"
1132
+ :title="strings.rightAxis"
896
1133
  >
897
1134
  ↗
898
1135
  </span>
@@ -904,20 +1141,31 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
904
1141
  <LpContextMenu :items="menuEntries" :always="!!menuItems">
905
1142
  <!-- `group/plot` + data-state lets the tooltip hide itself while the
906
1143
  context menu is up: the two would otherwise stack on the same point. -->
907
- <div class="group/plot relative" :style="{ height: `${height}px` }">
1144
+ <div
1145
+ class="group/plot relative rounded-control outline-none focus-visible:ring-2 focus-visible:ring-ring"
1146
+ :style="{ height: `${height}px` }"
1147
+ :tabindex="hasData ? 0 : undefined"
1148
+ :role="hasData ? 'group' : undefined"
1149
+ aria-roledescription="chart"
1150
+ :aria-label="hasData ? summary : undefined"
1151
+ @keydown="onKeydown"
1152
+ @blur="onBlur"
1153
+ >
908
1154
  <div
909
1155
  v-if="!hasData"
910
1156
  class="flex size-full flex-col items-center justify-center gap-2 rounded-control border border-dashed border-line text-muted"
911
1157
  >
912
1158
  <LpIcon name="lucide:chart-no-axes-column" :size="18" />
913
- <span class="text-xs">{{ emptyLabel }}</span>
1159
+ <span class="text-xs">{{ emptyLabel ?? strings.empty }}</span>
914
1160
  </div>
915
1161
 
916
1162
  <svg
917
1163
  v-else-if="width > 0"
1164
+ ref="svgEl"
918
1165
  :width="width"
919
1166
  :height="height"
920
1167
  class="overflow-visible touch-none select-none"
1168
+ aria-hidden="true"
921
1169
  @pointermove="onMove"
922
1170
  @pointerleave="onLeave"
923
1171
  @click="onClick"
@@ -927,7 +1175,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
927
1175
  <linearGradient
928
1176
  v-for="(s, i) in lines"
929
1177
  :id="gradientId(s.name, i)"
930
- :key="s.name"
1178
+ :key="s.id"
931
1179
  x1="0"
932
1180
  y1="0"
933
1181
  x2="0"
@@ -945,7 +1193,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
945
1193
  :x="box.left"
946
1194
  :y="0"
947
1195
  :height="height"
948
- :width="animate ? 0 : plotWidth(box)"
1196
+ :width="still ? plotWidth(box) : 0"
949
1197
  >
950
1198
  <!-- keySplines is --ease-travel written out: SMIL cannot read a
951
1199
  CSS custom property, so the curve is duplicated here. Change
@@ -1022,6 +1270,18 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
1022
1270
  opacity="0.5"
1023
1271
  />
1024
1272
 
1273
+ <!-- threshold bands sit under the data -->
1274
+ <rect
1275
+ v-for="t in thresholdMarks.filter((m) => m.height > 0)"
1276
+ :key="`band-${t.key}`"
1277
+ :x="box.left"
1278
+ :y="t.top"
1279
+ :width="plotWidth(box)"
1280
+ :height="t.height"
1281
+ :fill="t.color"
1282
+ opacity="0.08"
1283
+ />
1284
+
1025
1285
  <g :clip-path="`url(#${clipId}-reveal)`">
1026
1286
  <!-- bars: paths, so a stack rounds as one shape (see barPath) -->
1027
1287
  <path
@@ -1034,7 +1294,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
1034
1294
  />
1035
1295
 
1036
1296
  <!-- 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 }">
1297
+ <g v-for="(s, i) in lines" :key="`s-${s.id}`" :style="{ opacity: s.alpha }">
1038
1298
  <path
1039
1299
  v-show="type === 'area'"
1040
1300
  :d="s.fill"
@@ -1064,6 +1324,49 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
1064
1324
  </g>
1065
1325
  </g>
1066
1326
 
1327
+ <!-- threshold lines and markers sit over the data, under the crosshair -->
1328
+ <g v-for="t in thresholdMarks" :key="`t-${t.key}`">
1329
+ <line
1330
+ :x1="box.left"
1331
+ :x2="box.width - box.right"
1332
+ :y1="t.y"
1333
+ :y2="t.y"
1334
+ :stroke="t.color"
1335
+ stroke-width="1"
1336
+ :stroke-dasharray="t.solid ? undefined : '4 3'"
1337
+ />
1338
+ <text
1339
+ v-if="t.label"
1340
+ :x="box.width - box.right - 4"
1341
+ :y="t.top - 4"
1342
+ text-anchor="end"
1343
+ class="text-[10px]"
1344
+ :fill="t.color"
1345
+ >
1346
+ {{ t.label }}
1347
+ </text>
1348
+ </g>
1349
+ <g v-for="m in markerMarks" :key="`m-${m.key}`">
1350
+ <line
1351
+ :x1="m.x"
1352
+ :x2="m.x"
1353
+ :y1="box.top"
1354
+ :y2="box.top + plotHeight(box)"
1355
+ :stroke="m.color"
1356
+ stroke-width="1"
1357
+ stroke-dasharray="2 3"
1358
+ />
1359
+ <text
1360
+ v-if="m.label"
1361
+ :x="m.x + 4"
1362
+ :y="box.top + 9"
1363
+ class="text-[10px]"
1364
+ :fill="m.color"
1365
+ >
1366
+ {{ m.label }}
1367
+ </text>
1368
+ </g>
1369
+
1067
1370
  <!-- crosshair -->
1068
1371
  <g v-if="active >= 0 && type !== 'bar'">
1069
1372
  <line
@@ -1077,7 +1380,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
1077
1380
  />
1078
1381
  <circle
1079
1382
  v-for="p in activePoints"
1080
- :key="p.name"
1383
+ :key="p.id"
1081
1384
  :cx="activeX"
1082
1385
  :cy="p.y"
1083
1386
  r="4"
@@ -1125,7 +1428,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
1125
1428
  <div class="flex flex-col gap-1">
1126
1429
  <div
1127
1430
  v-for="r in tooltipRows"
1128
- :key="r.name"
1431
+ :key="r.id"
1129
1432
  class="flex items-center justify-between gap-3 text-xs"
1130
1433
  >
1131
1434
  <span class="flex items-center gap-1.5 text-muted">
@@ -1137,8 +1440,28 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
1137
1440
  </div>
1138
1441
  </LpShift>
1139
1442
  </Motion>
1443
+
1444
+ <span class="sr-only" aria-live="polite">{{ readout }}</span>
1140
1445
  </div>
1141
1446
  </LpContextMenu>
1447
+
1448
+ <table v-if="dataTable && hasData" class="sr-only">
1449
+ <caption v-if="title">{{ title }}</caption>
1450
+ <thead>
1451
+ <tr>
1452
+ <th scope="col"></th>
1453
+ <th v-for="s in allSeries" :key="s.id" scope="col">{{ s.name }}</th>
1454
+ </tr>
1455
+ </thead>
1456
+ <tbody>
1457
+ <tr v-for="(label, i) in labels" :key="i">
1458
+ <th scope="row">{{ label }}</th>
1459
+ <td v-for="s in allSeries" :key="s.id">
1460
+ {{ s.values[i] == null ? "—" : fmtSeries(s, s.values[i] as number) }}
1461
+ </td>
1462
+ </tr>
1463
+ </tbody>
1464
+ </table>
1142
1465
  </div>
1143
1466
  </template>
1144
1467
 
@@ -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
  >