@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
@@ -31,6 +31,35 @@ import LpContextMenu, { type ContextMenuItemDef } from "./LpContextMenu.vue"
31
31
  import LpIcon from "./LpIcon.vue"
32
32
  import LpScrollArea from "./LpScrollArea.vue"
33
33
  import { useMergedAttrs } from "../composables/useMergedClass"
34
+ import { useLabels } from "../composables/useLpConfig"
35
+ import { countLevels, matchPositions, ringWindow, stepMatch } from "./logSearch"
36
+
37
+ export interface LogViewerLabels {
38
+ [key: string]: string
39
+ empty: string
40
+ frozen: string
41
+ noMatch: string
42
+ loadingOlder: string
43
+ copyMessage: string
44
+ copyLine: string
45
+ copyTimestamp: string
46
+ copied: string
47
+ /** `{source}` is replaced by the line's source. */
48
+ filterSource: string
49
+ /** `{level}` is replaced by the line's level. */
50
+ filterLevel: string
51
+ /** `{n}` is replaced by the count. */
52
+ identical: string
53
+ jump: string
54
+ search: string
55
+ clearSearch: string
56
+ previousMatch: string
57
+ nextMatch: string
58
+ /** `{current}` and `{total}` are replaced. */
59
+ matchCount: string
60
+ noMatches: string
61
+ levels: string
62
+ }
34
63
 
35
64
  export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal" | "success"
36
65
 
@@ -103,6 +132,19 @@ const props = withDefaults(
103
132
  * so a duplicate level word / timestamp doesn't repeat in the message body.
104
133
  */
105
134
  parse?: boolean
135
+ /**
136
+ * Keep only the newest N lines on screen. The consumer can keep pushing
137
+ * into the same array (or hand over a longer one) and the viewer shows the
138
+ * tail; trimming the source itself is still the cheaper way to cap memory.
139
+ */
140
+ maxLines?: number
141
+ /**
142
+ * A bar above the stream with a search box (Enter / Shift+Enter or F3 step
143
+ * through matches) and level chips. The term binds via `v-model:search`,
144
+ * the chosen levels via `v-model:levels`.
145
+ */
146
+ toolbar?: boolean
147
+ labels?: Partial<LogViewerLabels>
106
148
  }>(),
107
149
  {
108
150
  showTime: true,
@@ -111,8 +153,6 @@ const props = withDefaults(
111
153
  wrap: false,
112
154
  tail: true,
113
155
  height: "20rem",
114
- emptyLabel: "No logs yet",
115
- frozenLabel: "Held — release Ctrl to resume",
116
156
  rowMenu: true,
117
157
  parse: false,
118
158
  },
@@ -129,6 +169,38 @@ const emit = defineEmits<{
129
169
  (e: "reach-top"): void
130
170
  }>()
131
171
 
172
+ /** The toolbar's search term. Without the toolbar, `highlight` is the term. */
173
+ const query = defineModel<string>("search", { default: "" })
174
+ /** Levels to show. Empty shows every level. */
175
+ const levels = defineModel<LogLevel[]>("levels", { default: () => [] })
176
+ const term = computed(() => (props.toolbar && query.value ? query.value : (props.highlight ?? "")).trim())
177
+
178
+ const text = useLabels<LogViewerLabels>(
179
+ "logViewer",
180
+ {
181
+ empty: "No logs yet",
182
+ frozen: "Held — release Ctrl to resume",
183
+ noMatch: "No lines match",
184
+ loadingOlder: "loading older…",
185
+ copyMessage: "Copy message",
186
+ copyLine: "Copy line",
187
+ copyTimestamp: "Copy timestamp",
188
+ copied: "Copied to clipboard",
189
+ filterSource: "Filter by [{source}]",
190
+ filterLevel: "Filter by {level}",
191
+ identical: "{n} identical lines",
192
+ jump: "Jump to latest",
193
+ search: "Search logs",
194
+ clearSearch: "Clear search",
195
+ previousMatch: "Previous match",
196
+ nextMatch: "Next match",
197
+ matchCount: "{current} / {total}",
198
+ noMatches: "No matches",
199
+ levels: "Levels",
200
+ },
201
+ () => props.labels,
202
+ )
203
+
132
204
  // Level → gutter rail + text colour. Reads only semantic tokens so re-skinning
133
205
  // just works. trace/debug stay muted; success is green; warn amber; error/fatal
134
206
  // red (fatal gets a filled chip to stand apart from plain error).
@@ -242,7 +314,10 @@ const root = ref<HTMLElement | null>(null)
242
314
  const frozen = ref(false)
243
315
  const frozenLines = shallowRef<LogLine[] | null>(null)
244
316
 
245
- const sourceLines = computed<LogLine[]>(() => frozenLines.value ?? props.lines)
317
+ const windowed = computed(() => ringWindow(frozenLines.value ?? props.lines, props.maxLines))
318
+ const sourceLines = computed<LogLine[]>(() => windowed.value.lines as LogLine[])
319
+ // Line numbers keep counting from the start of the stream after the cap trims it.
320
+ const lineOffset = computed(() => windowed.value.offset)
246
321
 
247
322
  function freeze(): boolean {
248
323
  // Declining unless the stream is being looked at: `false` hands Ctrl back
@@ -253,7 +328,9 @@ function freeze(): boolean {
253
328
  // Snapshotted by reference, not copied: the array the consumer hands over is
254
329
  // replaced wholesale on every update, so holding the old one is enough — and
255
330
  // a copy of ten thousand lines on every keypress is not.
256
- frozenLines.value = props.lines
331
+ // A capped stream is usually one array pushed into in place, which a
332
+ // reference would not hold still; its window is small enough to copy.
333
+ frozenLines.value = props.maxLines ? props.lines.slice(-props.maxLines) : props.lines
257
334
  frozen.value = true
258
335
  return true
259
336
  }
@@ -279,9 +356,13 @@ function focusWithin() {
279
356
  return !!el && el.contains(document.activeElement)
280
357
  }
281
358
 
359
+ // `when` rather than a bare decline: several viewers on one page all bind these
360
+ // keys, and only the one being looked at should claim them.
361
+ const lookedAt = () => hovered.value || focusWithin()
362
+
282
363
  useHotkeys(() => [
283
- { key: "control", allowInInput: true, handler: freeze },
284
- { key: "meta", allowInInput: true, handler: freeze },
364
+ { key: "control", allowInInput: true, when: lookedAt, handler: freeze },
365
+ { key: "meta", allowInInput: true, when: lookedAt, handler: freeze },
285
366
  ])
286
367
 
287
368
  // Released on keyup anywhere, and on blur: a window that loses focus mid-hold
@@ -309,10 +390,12 @@ const parsedLines = computed<LogLine[]>(() =>
309
390
  // With filterMatches + a search term, keep only lines whose message contains it
310
391
  // (case-insensitive); otherwise pass everything.
311
392
  const visibleLines = computed<{ line: LogLine; n: number; count: number }[]>(() => {
312
- const term = props.filterMatches ? props.highlight?.trim().toLowerCase() : ""
393
+ const needle = props.filterMatches ? term.value.toLowerCase() : ""
394
+ const shown = levels.value.length ? new Set<LogLevel>(levels.value) : null
313
395
  const filtered = parsedLines.value
314
396
  .map((line, n) => ({ line, n }))
315
- .filter(({ line }) => !term || line.message.toLowerCase().includes(term))
397
+ .filter(({ line }) => !shown || shown.has(line.level ?? "info"))
398
+ .filter(({ line }) => !needle || matchPositions([line], needle).length > 0)
316
399
 
317
400
  if (!props.compact) return filtered.map((row) => ({ ...row, count: 1 }))
318
401
 
@@ -336,6 +419,106 @@ const visibleLines = computed<{ line: LogLine; n: number; count: number }[]>(()
336
419
  return folded
337
420
  })
338
421
 
422
+ /*
423
+ * Rows are keyed by the identity of the line object the consumer passed, not by
424
+ * position: prepending older history shifts every index, and index keys made
425
+ * the bottom rows play the enter animation instead of the new top ones.
426
+ */
427
+ const lineIds = new WeakMap<LogLine, number>()
428
+ let nextLineId = 0
429
+ function lineKey(n: number): number {
430
+ const line = sourceLines.value[n]
431
+ if (!line) return -1 - n
432
+ let id = lineIds.get(line)
433
+ if (id === undefined) {
434
+ id = nextLineId++
435
+ lineIds.set(line, id)
436
+ }
437
+ return id
438
+ }
439
+
440
+ // ── toolbar: level chips and match navigation ───────────────────────
441
+ const LEVEL_ORDER: LogLevel[] = ["trace", "debug", "info", "success", "warn", "error", "fatal"]
442
+ const levelCounts = computed(() => countLevels<LogLevel>(parsedLines.value, "info"))
443
+ const presentLevels = computed(() => LEVEL_ORDER.filter((l) => levelCounts.value.has(l)))
444
+
445
+ function toggleLevel(level: LogLevel) {
446
+ levels.value = levels.value.includes(level)
447
+ ? levels.value.filter((l) => l !== level)
448
+ : [...levels.value, level]
449
+ }
450
+
451
+ const matches = computed(() => matchPositions(visibleLines.value.map((r) => r.line), term.value))
452
+ const current = ref(-1)
453
+ const currentRow = computed(() => {
454
+ const pos = matches.value[current.value]
455
+ return pos === undefined ? -1 : (visibleLines.value[pos]?.n ?? -1)
456
+ })
457
+ watch(term, () => {
458
+ current.value = -1
459
+ })
460
+ watch(matches, (list) => {
461
+ if (current.value >= list.length) current.value = list.length - 1
462
+ })
463
+
464
+ const matchLabel = computed(() => {
465
+ if (!term.value) return ""
466
+ const total = matches.value.length
467
+ if (!total) return text.value.noMatches
468
+ return text.value.matchCount
469
+ .replace("{current}", current.value >= 0 ? String(current.value + 1) : "–")
470
+ .replace("{total}", String(total))
471
+ })
472
+
473
+ function step(direction: 1 | -1) {
474
+ const next = stepMatch(current.value, matches.value.length, direction)
475
+ if (next < 0) return
476
+ current.value = next
477
+ pinned.value = false
478
+ nextTick(() => revealMatch(matches.value[next]))
479
+ }
480
+
481
+ // Brings the match to the middle of the viewport. A virtualised row may not be
482
+ // in the DOM yet, so the offset is computed from the fixed row height instead.
483
+ function revealMatch(position: number | undefined) {
484
+ const el = viewport()
485
+ if (!el || position === undefined) return
486
+ const behavior: ScrollBehavior = prefersReducedMotion() ? "auto" : "smooth"
487
+ if (virtualise.value) {
488
+ el.scrollTo({ top: Math.max(0, position * ROW_HEIGHT - el.clientHeight / 2), behavior })
489
+ return
490
+ }
491
+ const n = visibleLines.value[position]?.n
492
+ el.querySelector<HTMLElement>(`[data-log-row="${n}"]`)?.scrollIntoView({ block: "center", behavior })
493
+ }
494
+
495
+ const searchEl = ref<HTMLInputElement | null>(null)
496
+
497
+ function onSearchKeydown(event: KeyboardEvent) {
498
+ if (event.isComposing) return
499
+ if (event.key === "Enter") {
500
+ event.preventDefault()
501
+ step(event.shiftKey ? -1 : 1)
502
+ } else if (event.key === "Escape" && query.value) {
503
+ event.preventDefault()
504
+ event.stopPropagation()
505
+ query.value = ""
506
+ }
507
+ }
508
+
509
+ const searching = () => !!term.value && lookedAt()
510
+
511
+ useHotkeys(() => [
512
+ { key: "f3", allowInInput: true, when: searching, handler: () => step(1) },
513
+ { key: "f3", shift: true, allowInInput: true, when: searching, handler: () => step(-1) },
514
+ ])
515
+
516
+ defineExpose({
517
+ next: () => step(1),
518
+ previous: () => step(-1),
519
+ focusSearch: () => searchEl.value?.focus(),
520
+ })
521
+
339
522
  // True when filtering has emptied an otherwise non-empty stream.
340
523
  const filteredEmpty = computed(
341
524
  () => props.lines.length > 0 && visibleLines.value.length === 0,
@@ -356,7 +539,7 @@ function fmtTime(time?: number | string): string {
356
539
  // A menu item closes the menu as it runs, leaving nothing on screen to say the
357
540
  // copy happened — so unlike LpCodeBlock's button, this one announces itself.
358
541
  const { copy: writeClipboard } = useClipboard()
359
- const copy = (text: string) => writeClipboard(text, { toast: "Copied to clipboard" })
542
+ const copy = (value: string) => writeClipboard(value, { toast: text.value.copied })
360
543
 
361
544
  // The whole line as one copyable string: "time LEVEL [source] message".
362
545
  function lineText(line: LogLine): string {
@@ -398,15 +581,15 @@ function onRowPointerDown(e: PointerEvent) {
398
581
  function menuFor(line: LogLine, index: number): ContextMenuItemDef[] {
399
582
  if (!props.rowMenu) return []
400
583
  const items: ContextMenuItemDef[] = [
401
- { label: "Copy message", icon: "lucide:copy", onSelect: () => copy(line.message) },
402
- { label: "Copy line", icon: "lucide:clipboard-list", onSelect: () => copy(lineText(line)) },
584
+ { label: text.value.copyMessage, icon: "lucide:copy", onSelect: () => copy(line.message) },
585
+ { label: text.value.copyLine, icon: "lucide:clipboard-list", onSelect: () => copy(lineText(line)) },
403
586
  ]
404
587
  if (line.time != null) {
405
- items.push({ label: "Copy timestamp", icon: "lucide:clock", onSelect: () => copy(fmtTime(line.time)) })
588
+ items.push({ label: text.value.copyTimestamp, icon: "lucide:clock", onSelect: () => copy(fmtTime(line.time)) })
406
589
  }
407
590
  if (line.source) {
408
591
  items.push({
409
- label: `Filter by [${line.source}]`,
592
+ label: text.value.filterSource.replace("{source}", line.source),
410
593
  icon: "lucide:filter",
411
594
  separatorBefore: true,
412
595
  onSelect: () => emit("filter", { source: line.source }),
@@ -414,10 +597,13 @@ function menuFor(line: LogLine, index: number): ContextMenuItemDef[] {
414
597
  }
415
598
  if (line.level) {
416
599
  items.push({
417
- label: `Filter by ${line.level}`,
600
+ label: text.value.filterLevel.replace("{level}", line.level),
418
601
  icon: "lucide:filter",
419
602
  separatorBefore: !line.source,
420
- onSelect: () => emit("filter", { level: line.level }),
603
+ onSelect: () => {
604
+ if (props.toolbar && line.level) levels.value = [line.level]
605
+ emit("filter", { level: line.level })
606
+ },
421
607
  })
422
608
  }
423
609
  const extra = props.extraRowItems?.(line, index) ?? []
@@ -432,11 +618,10 @@ interface Part {
432
618
  hit: boolean
433
619
  }
434
620
  function splitHighlight(message: string): Part[] {
435
- const term = props.highlight?.trim()
436
- if (!term) return [{ text: message, hit: false }]
621
+ if (!term.value) return [{ text: message, hit: false }]
437
622
  const parts: Part[] = []
438
623
  const lower = message.toLowerCase()
439
- const needle = term.toLowerCase()
624
+ const needle = term.value.toLowerCase()
440
625
  let i = 0
441
626
  while (i < message.length) {
442
627
  const at = lower.indexOf(needle, i)
@@ -569,6 +754,7 @@ function rideToBottom(smooth = true, force = false) {
569
754
  }
570
755
 
571
756
  function scrollToBottom() {
757
+ current.value = -1
572
758
  rideToBottom(true, true)
573
759
  pinned.value = true
574
760
  }
@@ -576,8 +762,15 @@ function scrollToBottom() {
576
762
  // Snapshot the scroll anchor BEFORE the DOM updates for a line change (flush:pre)
577
763
  // so a prepend can be compensated post-update. Only matters when NOT pinned to
578
764
  // the bottom (i.e. the user is reading history up the stream).
765
+ /*
766
+ * A capped buffer (shift + push, or slice(-N)) keeps the length constant once
767
+ * full, so the length alone stops seeing new lines. The array, its length and
768
+ * its last line together change on every append, prepend or replacement.
769
+ */
770
+ const linesSignal = () => [props.lines, props.lines.length, props.lines[props.lines.length - 1]]
771
+
579
772
  watch(
580
- () => props.lines.length,
773
+ linesSignal,
581
774
  () => {
582
775
  if (!pinned.value) captureScrollAnchor()
583
776
  },
@@ -587,10 +780,10 @@ watch(
587
780
  // After the DOM updates: tail to the bottom if pinned, else keep the view glued
588
781
  // to where it was so a top prepend (older history) doesn't yank it.
589
782
  watch(
590
- () => props.lines.length,
783
+ linesSignal,
591
784
  async () => {
592
785
  await nextTick()
593
- if (props.tail && pinned.value) {
786
+ if (props.tail && pinned.value && current.value < 0) {
594
787
  requestAnimationFrame(() => rideToBottom(true))
595
788
  } else {
596
789
  restoreScrollAnchor()
@@ -632,13 +825,13 @@ onBeforeUnmount(() => {
632
825
  // (a component :is can't carry TransitionGroup-only props inertly otherwise).
633
826
  const transitionProps = {
634
827
  tag: "ol",
635
- "enter-active-class": "transition duration-200 ease-[var(--ease-emphasized)]",
828
+ "enter-active-class": "transition duration-[var(--duration-medium)] ease-[var(--ease-emphasized)]",
636
829
  "enter-from-class": "-translate-x-1 opacity-0",
637
830
  "leave-active-class":
638
- "overflow-hidden transition-all duration-200 ease-[var(--ease-emphasized)]",
831
+ "overflow-hidden transition-all duration-[var(--duration-medium)] ease-[var(--ease-emphasized)]",
639
832
  "leave-from-class": "max-h-8 opacity-100",
640
833
  "leave-to-class": "max-h-0 -translate-y-1 opacity-0",
641
- "move-class": "transition-transform duration-200 ease-[var(--ease-emphasized)]",
834
+ "move-class": "transition-transform duration-[var(--duration-medium)] ease-[var(--ease-emphasized)]",
642
835
  }
643
836
 
644
837
  // The viewer draws its own card frame and hides its overflow; a consumer
@@ -673,10 +866,82 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
673
866
  v-if="frozen"
674
867
  class="pointer-events-none absolute top-2 left-1/2 z-10 -translate-x-1/2 rounded-pill bg-brand px-2.5 py-1 text-[11px] font-medium text-ink-inverse"
675
868
  >
676
- {{ frozenLabel }}
869
+ {{ frozenLabel ?? text.frozen }}
677
870
  </div>
678
871
  </Transition>
679
872
 
873
+ <div
874
+ v-if="toolbar"
875
+ class="flex flex-wrap items-center gap-2 border-b border-line px-2 py-1.5 font-sans"
876
+ >
877
+ <div class="flex min-w-48 flex-1 items-center gap-1.5 rounded-control bg-surface-soft px-2 focus-within:ring-2 focus-within:ring-ring">
878
+ <LpIcon name="lucide:search" :size="14" class="shrink-0 text-muted" />
879
+ <input
880
+ ref="searchEl"
881
+ v-model="query"
882
+ type="search"
883
+ :aria-label="text.search"
884
+ :placeholder="text.search"
885
+ class="h-7 min-w-0 flex-1 bg-transparent text-xs text-ink outline-none placeholder:text-muted [&::-webkit-search-cancel-button]:hidden"
886
+ @keydown="onSearchKeydown"
887
+ />
888
+ <span
889
+ v-if="term"
890
+ class="shrink-0 text-[11px] tabular-nums text-muted"
891
+ aria-live="polite"
892
+ >{{ matchLabel }}</span>
893
+ <button
894
+ v-if="query"
895
+ type="button"
896
+ class="rounded-xs p-0.5 text-muted outline-none hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
897
+ :aria-label="text.clearSearch"
898
+ @click="query = ''; searchEl?.focus()"
899
+ >
900
+ <LpIcon name="lucide:x" :size="12" />
901
+ </button>
902
+ </div>
903
+ <div class="flex items-center">
904
+ <button
905
+ type="button"
906
+ class="rounded-xs p-1 text-muted outline-none hover:text-ink focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-40"
907
+ :aria-label="text.previousMatch"
908
+ :disabled="!matches.length"
909
+ @click="step(-1)"
910
+ >
911
+ <LpIcon name="lucide:chevron-up" :size="14" />
912
+ </button>
913
+ <button
914
+ type="button"
915
+ class="rounded-xs p-1 text-muted outline-none hover:text-ink focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-40"
916
+ :aria-label="text.nextMatch"
917
+ :disabled="!matches.length"
918
+ @click="step(1)"
919
+ >
920
+ <LpIcon name="lucide:chevron-down" :size="14" />
921
+ </button>
922
+ </div>
923
+ <div
924
+ v-if="presentLevels.length > 1"
925
+ role="group"
926
+ :aria-label="text.levels"
927
+ class="flex flex-wrap items-center gap-1"
928
+ >
929
+ <button
930
+ v-for="level in presentLevels"
931
+ :key="level"
932
+ type="button"
933
+ class="flex items-center gap-1 rounded-pill border px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.06em] outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring"
934
+ :class="levels.includes(level) ? 'border-line-strong bg-surface-soft text-ink' : 'border-line text-muted hover:text-ink'"
935
+ :aria-pressed="levels.includes(level)"
936
+ @click="toggleLevel(level)"
937
+ >
938
+ <span class="size-1.5 rounded-full" :class="metaFor(level).rail" aria-hidden="true" />
939
+ {{ level }}
940
+ <span class="font-normal tabular-nums text-muted">{{ levelCounts.get(level) }}</span>
941
+ </button>
942
+ </div>
943
+ </div>
944
+
680
945
  <!-- `instant`: this viewer decides for itself when a jump animates (see
681
946
  rideToBottom) and restores scrollTop directly after prepending older
682
947
  lines. A CSS smooth scroll would override both. -->
@@ -691,8 +956,8 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
691
956
  v-if="lines.length === 0 || filteredEmpty"
692
957
  class="flex h-full flex-col items-center justify-center gap-1 px-4 py-10 text-center"
693
958
  >
694
- <span class="text-muted">{{ filteredEmpty ? "No lines match" : emptyLabel }}</span>
695
- <span v-if="filteredEmpty" class="text-muted/70">“{{ highlight }}”</span>
959
+ <span class="text-muted">{{ filteredEmpty ? text.noMatch : (emptyLabel ?? text.empty) }}</span>
960
+ <span v-if="filteredEmpty && term" class="text-muted/70">“{{ term }}”</span>
696
961
  </div>
697
962
 
698
963
  <template v-else>
@@ -704,7 +969,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
704
969
  class="flex items-center justify-center gap-2 py-2 text-muted"
705
970
  >
706
971
  <LpIcon name="lucide:loader-circle" :size="14" class="animate-spin" />
707
- <span class="text-[11px]">loading older…</span>
972
+ <span class="text-[11px]">{{ text.loadingOlder }}</span>
708
973
  </div>
709
974
 
710
975
  <!-- ONE context menu wraps the whole list (not each row): @contextmenu
@@ -738,10 +1003,11 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
738
1003
  />
739
1004
  <li
740
1005
  v-for="{ line, n, count } in renderedLines"
741
- :key="n"
1006
+ :key="lineKey(n)"
742
1007
  :data-log-row="n"
743
1008
  class="group relative flex items-start gap-0 px-0 leading-5 transition-colors hover:bg-surface-soft/60"
744
- :class="metaFor(line.level).sel"
1009
+ :class="[metaFor(line.level).sel, n === currentRow ? 'bg-accent/15' : '']"
1010
+ :aria-current="n === currentRow ? 'true' : undefined"
745
1011
  >
746
1012
  <!-- level rail -->
747
1013
  <!-- Level rail: absolutely positioned so it spans the FULL row height
@@ -759,7 +1025,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
759
1025
  <span
760
1026
  v-if="lineNumbers"
761
1027
  class="mr-3 w-10 shrink-0 select-none text-right tabular-nums text-muted/70"
762
- >{{ n + 1 }}</span>
1028
+ >{{ n + lineOffset + 1 }}</span>
763
1029
 
764
1030
  <!-- timestamp -->
765
1031
  <span
@@ -785,7 +1051,13 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
785
1051
  class="min-w-0 pr-3"
786
1052
  :class="[metaFor(line.level).text, wrap ? 'whitespace-pre-wrap break-words' : 'whitespace-pre']"
787
1053
  >
788
- <template v-for="(part, p) in splitHighlight(line.message)" :key="p">
1054
+ <!-- Virtualised rows must stay one row tall, so a multi-line
1055
+ message (a stack trace) is shown on one line with ↵ marks;
1056
+ copying still yields the original text. -->
1057
+ <template
1058
+ v-for="(part, p) in splitHighlight(virtualise ? line.message.replace(/\r?\n/g, ' ↵ ') : line.message)"
1059
+ :key="p"
1060
+ >
789
1061
  <mark
790
1062
  v-if="part.hit"
791
1063
  class="rounded-xs bg-accent/30 text-ink"
@@ -796,14 +1068,14 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
796
1068
  Keyed on count so each new fold re-mounts the chip and pops it,
797
1069
  cueing where the duplicate just merged. -->
798
1070
  <Transition
799
- enter-active-class="inline-block transition duration-200 ease-[var(--ease-emphasized)]"
1071
+ enter-active-class="transition duration-[var(--duration-medium)] ease-[var(--ease-emphasized)]"
800
1072
  enter-from-class="scale-50 opacity-0"
801
1073
  >
802
1074
  <span
803
1075
  v-if="count > 1"
804
1076
  :key="count"
805
- class="ml-2 select-none rounded-pill bg-surface-soft px-1.5 align-[1px] text-[10px] font-semibold tabular-nums text-muted-strong"
806
- :title="`${count} identical lines`"
1077
+ class="ml-2 inline-block select-none rounded-pill bg-surface-soft px-1.5 align-[1px] text-[10px] font-semibold tabular-nums text-muted-strong"
1078
+ :title="text.identical.replace('{n}', String(count))"
807
1079
  >×{{ count }}</span>
808
1080
  </Transition>
809
1081
  </span>
@@ -823,9 +1095,9 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
823
1095
  Transition rides an outer wrapper so its transform/opacity don't fight
824
1096
  the button's own -translate-x-1/2 centring and hover colour transition. -->
825
1097
  <Transition
826
- enter-active-class="transition duration-200 ease-[var(--ease-emphasized)]"
1098
+ enter-active-class="transition duration-[var(--duration-medium)] ease-[var(--ease-emphasized)]"
827
1099
  enter-from-class="translate-y-3 scale-90 opacity-0"
828
- leave-active-class="transition duration-150 ease-in"
1100
+ leave-active-class="transition duration-[var(--duration-fast)] ease-in"
829
1101
  leave-to-class="translate-y-3 scale-90 opacity-0"
830
1102
  >
831
1103
  <div v-if="showJump" class="absolute inset-x-0 bottom-3 flex justify-center">
@@ -837,9 +1109,9 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
837
1109
  <LpIcon
838
1110
  name="lucide:arrow-down"
839
1111
  :size="14"
840
- class="transition-transform duration-200 ease-[var(--ease-emphasized)] group-hover:translate-y-0.5"
1112
+ class="transition-transform duration-[var(--duration-medium)] ease-[var(--ease-emphasized)] group-hover:translate-y-0.5"
841
1113
  />
842
- Jump to latest
1114
+ {{ text.jump }}
843
1115
  </button>
844
1116
  </div>
845
1117
  </Transition>
@@ -25,6 +25,7 @@ import { prefersReducedMotion } from "../composables/easing"
25
25
  import { type LatLon, OSM_TILES, type TileSource, project, tileUrl, unproject } from "./address"
26
26
  import LpIcon from "./LpIcon.vue"
27
27
  import { useMergedAttrs } from "../composables/useMergedClass"
28
+ import { useLabels } from "../composables/useLpConfig"
28
29
 
29
30
  const props = withDefaults(
30
31
  defineProps<{
@@ -45,6 +46,8 @@ const props = withDefaults(
45
46
  zoomOutLabel?: string
46
47
  locateLabel?: string
47
48
  recentreLabel?: string
49
+ /** Accessible name of the map itself. */
50
+ mapLabel?: string
48
51
  }>(),
49
52
  {
50
53
  tiles: () => OSM_TILES,
@@ -52,15 +55,26 @@ const props = withDefaults(
52
55
  center: () => ({ lat: 50.11, lon: 14.44 }),
53
56
  height: "20rem",
54
57
  locatable: true,
55
- zoomInLabel: "Zoom in",
56
- zoomOutLabel: "Zoom out",
57
- locateLabel: "Use my location",
58
- recentreLabel: "Back to the pin",
59
58
  },
60
59
  )
61
60
 
62
61
  const emit = defineEmits<{ (e: "update:modelValue", value: LatLon): void }>()
63
62
 
63
+ const labelSet = useLabels("mapPicker", {
64
+ zoomIn: "Zoom in",
65
+ zoomOut: "Zoom out",
66
+ locate: "Use my location",
67
+ recentre: "Back to the pin",
68
+ map: "Map — arrows pan, +/− zoom, Enter drops the pin in the centre",
69
+ })
70
+ const text = computed(() => ({
71
+ zoomIn: props.zoomInLabel ?? labelSet.value.zoomIn,
72
+ zoomOut: props.zoomOutLabel ?? labelSet.value.zoomOut,
73
+ locate: props.locateLabel ?? labelSet.value.locate,
74
+ recentre: props.recentreLabel ?? labelSet.value.recentre,
75
+ map: props.mapLabel ?? labelSet.value.map,
76
+ }))
77
+
64
78
  const PAN_STEP = 80
65
79
  const ZOOM_MS = 220
66
80
 
@@ -452,6 +466,15 @@ function onKeydown(e: KeyboardEvent) {
452
466
  e.preventDefault()
453
467
  recentre()
454
468
  break
469
+ // Without this a keyboard could pan and zoom but never actually pick.
470
+ case "Enter":
471
+ case " ": {
472
+ const el = viewport.value
473
+ if (!el) return
474
+ e.preventDefault()
475
+ emit("update:modelValue", toLatLon({ x: el.clientWidth / 2, y: el.clientHeight / 2 }))
476
+ break
477
+ }
455
478
  }
456
479
  }
457
480
 
@@ -563,7 +586,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
563
586
  :class="disabled ? 'cursor-not-allowed opacity-55' : panning ? 'cursor-grabbing' : 'cursor-grab'"
564
587
  :tabindex="disabled ? -1 : 0"
565
588
  role="application"
566
- :aria-label="recentreLabel"
589
+ :aria-label="text.map"
567
590
  @pointerdown="onPointerDown"
568
591
  @pointermove="onPointerMove"
569
592
  @pointerup="onPointerUp"
@@ -616,8 +639,8 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
616
639
  :class="CONTROL"
617
640
  type="button"
618
641
  :disabled="disabled || level >= maxZoom"
619
- :title="zoomInLabel"
620
- :aria-label="zoomInLabel"
642
+ :title="text.zoomIn"
643
+ :aria-label="text.zoomIn"
621
644
  @click="zoomBy(1)"
622
645
  >
623
646
  <LpIcon name="lucide:plus" :size="16" />
@@ -626,8 +649,8 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
626
649
  :class="CONTROL"
627
650
  type="button"
628
651
  :disabled="disabled || level <= minZoom"
629
- :title="zoomOutLabel"
630
- :aria-label="zoomOutLabel"
652
+ :title="text.zoomOut"
653
+ :aria-label="text.zoomOut"
631
654
  @click="zoomBy(-1)"
632
655
  >
633
656
  <LpIcon name="lucide:minus" :size="16" />
@@ -636,8 +659,8 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
636
659
  v-if="canRecentre"
637
660
  :class="CONTROL"
638
661
  type="button"
639
- :title="recentreLabel"
640
- :aria-label="recentreLabel"
662
+ :title="text.recentre"
663
+ :aria-label="text.recentre"
641
664
  @click="recentre"
642
665
  >
643
666
  <LpIcon name="lucide:locate-fixed" :size="16" />
@@ -647,8 +670,8 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
647
670
  :class="CONTROL"
648
671
  type="button"
649
672
  :disabled="disabled || locating"
650
- :title="locateLabel"
651
- :aria-label="locateLabel"
673
+ :title="text.locate"
674
+ :aria-label="text.locate"
652
675
  @click="locate"
653
676
  >
654
677
  <LpIcon