@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
@@ -24,7 +24,7 @@
24
24
  * cover is per image and the reveal is remembered per image, because a set is
25
25
  * usually mixed — one marked screenshot among plain ones.
26
26
  */
27
- import { computed, ref } from "vue"
27
+ import { computed, ref, watch } from "vue"
28
28
  import type { LightboxItem } from "./lightbox"
29
29
  import { useMergedAttrs } from "../composables/useMergedClass"
30
30
  import LpIcon from "./LpIcon.vue"
@@ -88,6 +88,12 @@ defineSlots<{
88
88
  const open = ref(false)
89
89
  const index = ref(0)
90
90
 
91
+ // Closing happens inside the lightbox (Escape, the scrim, the X), so the gallery
92
+ // reports both edges of v-model:open, not only the one it starts.
93
+ watch(open, (value, was) => {
94
+ if (!value && was) emit("update:open", false)
95
+ })
96
+
91
97
  // `max` counts what is SHOWN, so the badge reads "+N more" rather than "N
92
98
  // total". A set of 7 capped at 4 shows 4 cells, the last one badged +3.
93
99
  const visible = computed(() =>
@@ -188,8 +194,8 @@ function activate(i: number) {
188
194
  const at = openIndexOf(i)
189
195
  if (at === -1) return
190
196
  index.value = at
197
+ if (!open.value) emit("update:open", true)
191
198
  open.value = true
192
- emit("update:open", true)
193
199
  }
194
200
 
195
201
  /** The badge is about the FIRST hidden image, so that is where it opens. */
@@ -10,6 +10,7 @@ import {
10
10
  } from "reka-ui"
11
11
  import { computed, onBeforeUnmount, ref, useSlots, watch } from "vue"
12
12
  import { useGlass } from "../composables/useGlass"
13
+ import { useLabels } from "../composables/useLpConfig"
13
14
  import { useShift } from "../composables/useShift"
14
15
  import { useModalLayer } from "./modalDepth"
15
16
  import { CLOSE_ICON } from "./dropdown"
@@ -33,7 +34,8 @@ import LpScrollArea from "./LpScrollArea.vue"
33
34
  * same fix, as the lightbox's own rungs in tokens.css — only the depth here is
34
35
  * not known until a dialog finds itself inside another.
35
36
  */
36
- const { claim, release, layer } = useModalLayer()
37
+ const panelRef = ref<HTMLElement | null>(null)
38
+ const { claim, release, layer } = useModalLayer({ element: () => panelRef.value?.parentElement })
37
39
 
38
40
  const props = withDefaults(
39
41
  defineProps<{
@@ -70,10 +72,20 @@ const props = withDefaults(
70
72
  * `provideGlass`, and without one it is off.
71
73
  */
72
74
  glass?: boolean
75
+ /**
76
+ * Accessible name for a dialog without a visible `title`. Rendered as a
77
+ * visually hidden title, so assistive tech still announces what opened.
78
+ */
79
+ ariaLabel?: string
80
+ /** `alertdialog` for a prompt that interrupts the user (a confirmation). */
81
+ role?: "dialog" | "alertdialog"
82
+ labels?: Partial<{ close: string }>
73
83
  }>(),
74
84
  { size: "md", autoFocus: "auto", glass: undefined },
75
85
  )
76
86
 
87
+ const text = useLabels("modal", { close: "Close" }, () => props.labels)
88
+
77
89
  const frosted = useGlass(() => props.glass)
78
90
 
79
91
  defineEmits<{ (e: "update:open", value: boolean): void }>()
@@ -95,9 +107,10 @@ const onInteractOutside = (event: Event) => {
95
107
  }
96
108
  }
97
109
 
98
- const describedByAttrs = computed(() =>
99
- props.description ? {} : { "aria-describedby": undefined },
100
- )
110
+ const describedByAttrs = computed(() => ({
111
+ ...(props.description ? {} : { "aria-describedby": undefined }),
112
+ ...(props.role ? { role: props.role } : {}),
113
+ }))
101
114
 
102
115
  const widthClass = computed(() => {
103
116
  if (props.width) return ""
@@ -152,15 +165,13 @@ const bodyPad = computed(() =>
152
165
  * scroll area exactly as before. A panel whose size never moves is pinned once
153
166
  * and never transitions.
154
167
  */
155
- const panelRef = ref<HTMLElement | null>(null)
156
-
157
168
  /**
158
169
  * Controls that are worth landing on when the dialog opens, in order of
159
170
  * preference. A field the user is going to type in wins over a button, since
160
171
  * typing is the reason most dialogs exist.
161
172
  */
162
173
  const AUTO_FOCUS_ORDER = [
163
- "input:not([type=hidden]):not([disabled]):not([readonly])",
174
+ "input:not([type=hidden]):not([disabled]):not([readonly]):not([tabindex='-1'])",
164
175
  "textarea:not([disabled]):not([readonly])",
165
176
  "select:not([disabled])",
166
177
  "[data-lp-autofocus]",
@@ -178,6 +189,18 @@ const AUTO_FOCUS_ORDER = [
178
189
  *
179
190
  * Both are fixed here: pick a meaningful target, and mark it so the ring shows.
180
191
  */
192
+ /**
193
+ * Whether a candidate can actually take focus: reka renders hidden form inputs
194
+ * for its switches and checkboxes, and a control hidden at this breakpoint
195
+ * (`lg:hidden`) is in the DOM but not on screen.
196
+ */
197
+ function focusable(el: HTMLElement): boolean {
198
+ if (el.hasAttribute("disabled") || el.getAttribute("tabindex") === "-1") return false
199
+ if (el.closest("[aria-hidden='true'], [data-hidden], [inert]")) return false
200
+ if (typeof el.checkVisibility === "function") return el.checkVisibility({ visibilityProperty: true })
201
+ return el.getClientRects().length > 0
202
+ }
203
+
181
204
  function onOpenAutoFocus(event: Event) {
182
205
  if (props.autoFocus === "none") return
183
206
  const panel = panelRef.value
@@ -186,26 +209,26 @@ function onOpenAutoFocus(event: Event) {
186
209
  const selectors =
187
210
  props.autoFocus === "auto" ? AUTO_FOCUS_ORDER : [props.autoFocus]
188
211
  for (const selector of selectors) {
189
- const target = panel.querySelector<HTMLElement>(selector)
190
- if (!target || target.hasAttribute("disabled")) continue
191
- event.preventDefault()
192
- // `focus-visible` is the browser's own heuristic and cannot be forced, so
193
- // the ring is driven off an attribute the kit owns. Removed as soon as the
194
- // element loses focus, leaving normal focus-visible behaviour in charge.
195
- //
196
- // Skipped for a control whose own shell already draws a ring on
197
- // focus-within: our fields put the border and ring on the wrapper, so
198
- // adding one to the bare <input> stacks a second ring inside the first.
199
- if (!target.closest("[data-lp-ring-owner]")) {
200
- target.setAttribute("data-lp-focus-ring", "")
201
- target.addEventListener(
202
- "blur",
203
- () => target.removeAttribute("data-lp-focus-ring"),
204
- { once: true },
205
- )
212
+ for (const target of panel.querySelectorAll<HTMLElement>(selector)) {
213
+ if (!focusable(target)) continue
214
+ target.focus()
215
+ // Only once focus really landed: a prevented auto-focus that failed would
216
+ // leave the keyboard on the page behind the dialog.
217
+ if (document.activeElement !== target) continue
218
+ event.preventDefault()
219
+ // `focus-visible` is the browser's own heuristic and cannot be forced, so
220
+ // the ring is driven off an attribute the kit owns. Skipped for a control
221
+ // whose own shell already draws a ring on focus-within.
222
+ if (!target.closest("[data-lp-ring-owner]")) {
223
+ target.setAttribute("data-lp-focus-ring", "")
224
+ target.addEventListener(
225
+ "blur",
226
+ () => target.removeAttribute("data-lp-focus-ring"),
227
+ { once: true },
228
+ )
229
+ }
230
+ return
206
231
  }
207
- target.focus()
208
- return
209
232
  }
210
233
  }
211
234
  // The panel's height eases between content-derived sizes — see
@@ -301,10 +324,13 @@ watch(
301
324
  </aside>
302
325
  <div
303
326
  ref="panelRef"
304
- class="flex max-h-[min(90vh,calc(100dvh-2rem))] min-h-0 flex-col overflow-hidden rounded-card border border-line shadow-panel"
327
+ class="relative flex max-h-[min(90vh,calc(100dvh-2rem))] min-h-0 flex-col overflow-hidden rounded-card border border-line shadow-panel"
305
328
  :class="[frosted ? (layer.nested ? 'lp-glass-overlay--nested' : 'lp-glass-overlay') : 'bg-surface-raised', widthClass, tweening ? 'transition-[height] duration-fast ease-[var(--ease-emphasized)] motion-reduce:transition-none' : '']"
306
329
  :style="{ ...(width ? { width } : {}), ...(hasAside ? { alignSelf: 'stretch' } : {}) }"
307
330
  >
331
+ <DialogTitle v-if="!(title || $slots.title) && ariaLabel" class="sr-only">
332
+ {{ ariaLabel }}
333
+ </DialogTitle>
308
334
  <header v-if="title || $slots.title" class="flex shrink-0 items-start justify-between gap-4 p-5 pb-3">
309
335
  <div class="flex flex-col gap-1">
310
336
  <DialogTitle class="text-base font-semibold text-ink">
@@ -319,7 +345,7 @@ watch(
319
345
  <DialogClose
320
346
  data-lp-dialog-close
321
347
  class="group flex shrink-0 items-center rounded-md p-1 text-muted outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
322
- aria-label="Close"
348
+ :aria-label="text.close"
323
349
  >
324
350
  <LpIcon
325
351
  name="lucide:x"
@@ -14,6 +14,7 @@
14
14
  * one the sections are simply unreachable on a phone.
15
15
  */
16
16
  import { computed, ref } from "vue"
17
+ import { useLabels } from "../composables/useLpConfig"
17
18
  import LpButton from "./LpButton.vue"
18
19
  import LpIcon from "./LpIcon.vue"
19
20
  import LpModal from "./LpModal.vue"
@@ -62,7 +63,6 @@ const props = withDefaults(
62
63
  size: "2xl",
63
64
  navWidth: "13rem",
64
65
  mobileBreakpoint: "lg",
65
- navLabel: "Sections",
66
66
  navSide: "start",
67
67
  glass: undefined,
68
68
  },
@@ -73,6 +73,9 @@ const emit = defineEmits<{
73
73
  (e: "update:modelValue", value: string): void
74
74
  }>()
75
75
 
76
+ const text = useLabels("modalNav", { nav: "Sections" })
77
+ const navName = computed(() => props.navLabel ?? text.value.nav)
78
+
76
79
  const navOpen = ref(false)
77
80
 
78
81
  const active = computed({
@@ -187,7 +190,7 @@ const railStyle = { maxHeight: "min(90vh, calc(100dvh - 2rem))" }
187
190
  variant="ghost"
188
191
  size="sm"
189
192
  square
190
- :aria-label="navLabel"
193
+ :aria-label="navName"
191
194
  :class="triggerClass"
192
195
  @click="navOpen = true"
193
196
  >
@@ -8,13 +8,15 @@
8
8
  * `select`. Mirrors the kit convention (cf. LpSidebar): data in, events out,
9
9
  * no data fetching inside.
10
10
  */
11
- import { computed, getCurrentInstance } from "vue"
11
+ import { computed, getCurrentInstance, onBeforeUnmount, onMounted, ref } from "vue"
12
+ import { useLabels } from "../composables/useLpConfig"
12
13
  import LpBadge from "./LpBadge.vue"
13
14
  import LpButton from "./LpButton.vue"
14
15
  import LpContextMenu, { type ContextMenuItemDef } from "./LpContextMenu.vue"
15
16
  import LpEmptyState from "./LpEmptyState.vue"
16
17
  import LpIcon from "./LpIcon.vue"
17
18
  import LpPopover from "./LpPopover.vue"
19
+ import LpRollingText from "./LpRollingText.vue"
18
20
  import LpScrollArea from "./LpScrollArea.vue"
19
21
  import LpShift from "./LpShift.vue"
20
22
 
@@ -56,6 +58,15 @@ export interface NotificationLabels {
56
58
  dismiss?: string
57
59
  empty?: string
58
60
  ariaLabel?: string
61
+ /** Appended to the button's name while there is unread news; `{count}` is filled in. */
62
+ unread?: string
63
+ /** Screen-reader marker on an unread row. */
64
+ unreadItem?: string
65
+ /** Relative times; `{n}` is filled in. */
66
+ justNow?: string
67
+ minutes?: string
68
+ hours?: string
69
+ days?: string
59
70
  }
60
71
 
61
72
  const props = withDefaults(
@@ -83,16 +94,32 @@ const props = withDefaults(
83
94
 
84
95
  // Single lookup for every label, so the template never repeats the fallbacks.
85
96
  // `title` stays a top-level prop for compatibility and wins over labels.title.
97
+ const text = useLabels(
98
+ "notifications",
99
+ {
100
+ title: "Notifications",
101
+ markAllRead: "Mark all read",
102
+ clear: "Clear",
103
+ markAsRead: "Mark as read",
104
+ markAsUnread: "Mark as unread",
105
+ open: "Open",
106
+ dismiss: "Dismiss",
107
+ empty: "No notifications",
108
+ ariaLabel: "",
109
+ unread: "{count} unread",
110
+ unreadItem: "Unread",
111
+ justNow: "just now",
112
+ minutes: "{n}m",
113
+ hours: "{n}h",
114
+ days: "{n}d",
115
+ },
116
+ () => props.labels,
117
+ )
86
118
  const l = computed(() => ({
87
- title: props.title ?? props.labels?.title ?? "Notifications",
88
- markAllRead: props.labels?.markAllRead ?? "Mark all read",
89
- clear: props.labels?.clear ?? "Clear",
90
- markAsRead: props.labels?.markAsRead ?? "Mark as read",
91
- markAsUnread: props.labels?.markAsUnread ?? "Mark as unread",
92
- open: props.labels?.open ?? "Open",
93
- dismiss: props.labels?.dismiss ?? "Dismiss",
94
- empty: props.emptyLabel ?? props.labels?.empty ?? "No notifications",
95
- ariaLabel: props.labels?.ariaLabel ?? props.labels?.title ?? "Notifications",
119
+ ...text.value,
120
+ title: props.title ?? text.value.title,
121
+ empty: props.emptyLabel ?? text.value.empty,
122
+ ariaLabel: text.value.ariaLabel || text.value.title,
96
123
  }))
97
124
 
98
125
  // Popover open state. A defineModel (not a plain `open` prop) gives a local
@@ -119,6 +146,11 @@ const badgeLabel = computed(() =>
119
146
  )
120
147
  const hasUnread = computed(() => unread.value > 0)
121
148
 
149
+ // The badge is drawn, not read: its count goes into the button's own name.
150
+ const buttonLabel = computed(() =>
151
+ hasUnread.value ? `${l.value.ariaLabel}, ${l.value.unread.replace("{count}", String(unread.value))}` : l.value.ariaLabel,
152
+ )
153
+
122
154
  /** Icon tints for an unread row, keyed by the item's category tone. */
123
155
  const TONE_CLASS: Record<string, string> = {
124
156
  brand: "bg-brand-soft text-brand",
@@ -208,19 +240,32 @@ function rowMenu(item: NotificationItem): ContextMenuItemDef[] {
208
240
 
209
241
  // Compact relative time ("just now", "5m", "3h", "2d"); falls back to a date.
210
242
  // A `formatTime` prop takes over entirely — that's the localisation seam.
243
+ //
244
+ // The clock is only read after mount and then ticks, so a server render and the
245
+ // hydrating client agree (both print nothing) and "just now" does not stay
246
+ // "just now" for the life of the page.
247
+ const now = ref<number | null>(null)
248
+ let clock: ReturnType<typeof setInterval> | undefined
249
+ onMounted(() => {
250
+ now.value = Date.now()
251
+ clock = setInterval(() => (now.value = Date.now()), 30_000)
252
+ })
253
+ onBeforeUnmount(() => clearInterval(clock))
254
+
211
255
  function timeAgo(iso?: string): string {
212
- if (!iso) return ""
256
+ if (!iso || now.value === null) return ""
213
257
  if (props.formatTime) return props.formatTime(iso)
214
258
  const then = new Date(iso).getTime()
215
259
  if (Number.isNaN(then)) return ""
216
- const secs = Math.max(0, Math.round((Date.now() - then) / 1000))
217
- if (secs < 45) return "just now"
260
+ const secs = Math.max(0, Math.round((now.value - then) / 1000))
261
+ const n = (template: string, value: number) => template.replace("{n}", String(value))
262
+ if (secs < 45) return l.value.justNow
218
263
  const mins = Math.round(secs / 60)
219
- if (mins < 60) return `${mins}m`
264
+ if (mins < 60) return n(l.value.minutes, mins)
220
265
  const hrs = Math.round(mins / 60)
221
- if (hrs < 24) return `${hrs}h`
266
+ if (hrs < 24) return n(l.value.hours, hrs)
222
267
  const days = Math.round(hrs / 24)
223
- if (days < 7) return `${days}d`
268
+ if (days < 7) return n(l.value.days, days)
224
269
  return new Date(then).toLocaleDateString()
225
270
  }
226
271
  </script>
@@ -243,17 +288,25 @@ function timeAgo(iso?: string): string {
243
288
  variant="ghost"
244
289
  size="sm"
245
290
  class="relative"
246
- :aria-label="l.ariaLabel"
291
+ :aria-label="buttonLabel"
247
292
  @click="open = !open"
248
293
  >
249
294
  <LpIcon name="lucide:bell" :size="18" />
250
- <LpBadge
251
- v-if="hasUnread"
252
- tone="danger"
253
- class="absolute -right-1 -top-1 min-w-4 justify-center px-1 py-0 text-[10px] leading-4"
295
+ <!-- The badge pops in and out, and its count rolls rather than
296
+ swapping, so new mail reads as arriving. -->
297
+ <Transition
298
+ enter-active-class="animate-[chip-in_var(--duration-fast)_var(--ease-emphasized)] motion-reduce:animate-none"
299
+ leave-active-class="animate-[chip-out_var(--duration-fast)_ease] motion-reduce:animate-none"
254
300
  >
255
- {{ badgeLabel }}
256
- </LpBadge>
301
+ <LpBadge
302
+ v-if="hasUnread"
303
+ aria-hidden="true"
304
+ tone="danger"
305
+ class="absolute -right-1 -top-1 min-w-4 justify-center px-1 py-0 text-[10px] leading-4"
306
+ >
307
+ <LpRollingText :value="badgeLabel" :duration="220" />
308
+ </LpBadge>
309
+ </Transition>
257
310
  </LpButton>
258
311
  </LpContextMenu>
259
312
  </template>
@@ -327,6 +380,7 @@ function timeAgo(iso?: string): string {
327
380
  {{ item.title }}
328
381
  </span>
329
382
  <span v-if="!item.read" class="size-1.5 shrink-0 rounded-full bg-brand" />
383
+ <span v-if="!item.read" class="sr-only">{{ l.unreadItem }}</span>
330
384
  </span>
331
385
  <span v-if="item.body" class="mt-0.5 line-clamp-2 block text-xs text-muted">
332
386
  {{ item.body }}
@@ -13,11 +13,12 @@ import {
13
13
  NumberFieldInput,
14
14
  NumberFieldRoot,
15
15
  } from "reka-ui"
16
- import { useId } from "vue"
16
+ import { computed, useId } from "vue"
17
17
  import LpIcon from "./LpIcon.vue"
18
+ import { useFieldControl } from "../composables/useField"
18
19
  import { useMergedAttrs } from "../composables/useMergedClass"
19
20
 
20
- defineProps<{
21
+ const props = defineProps<{
21
22
  // null = empty field (reka's native state); consumers that need a number
22
23
  // should default it themselves, the field doesn't invent a value.
23
24
  modelValue?: number | null
@@ -30,6 +31,7 @@ defineProps<{
30
31
  // "Depth", "Weight" tells the reader nothing about which one wants grams.
31
32
  // Not part of the value: it is never typed, selected, or submitted.
32
33
  unit?: string
34
+ invalid?: boolean
33
35
  }>()
34
36
 
35
37
  defineEmits<{ (e: "update:modelValue", value: number | null): void }>()
@@ -39,9 +41,19 @@ defineEmits<{ (e: "update:modelValue", value: number | null): void }>()
39
41
  // and leaves out the milliamps that make it mean anything.
40
42
  const unitId = useId()
41
43
 
44
+ const field = useFieldControl(() => ({ wrapped: false, invalid: props.invalid, disabled: props.disabled }))
45
+ const describedBy = computed(
46
+ () => [field.value.describedBy, props.unit ? unitId : undefined].filter(Boolean).join(" ") || undefined,
47
+ )
48
+
42
49
  // The field is full-width with its own control frame — the two things a consumer laying out a compact numeric column needs to change.
43
- const { class: rootClass, attrs: rest } = useMergedAttrs(
44
- "flex h-(--size-control-md) w-full items-center rounded-control border border-line bg-surface-soft transition-colors duration-[var(--duration-fast)] focus-within:border-brand focus-within:ring-2 focus-within:ring-ring data-[disabled]:cursor-not-allowed data-[disabled]:opacity-55",
50
+ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
51
+ [
52
+ "flex h-(--size-control-md) w-full items-center rounded-control border bg-surface-soft transition-colors duration-[var(--duration-fast)] focus-within:ring-2 data-[disabled]:cursor-not-allowed data-[disabled]:opacity-55",
53
+ field.value.invalid
54
+ ? "border-danger focus-within:border-danger focus-within:ring-danger-soft"
55
+ : "border-line focus-within:border-brand focus-within:ring-ring",
56
+ ].join(" "),
45
57
  )
46
58
  </script>
47
59
 
@@ -51,7 +63,8 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
51
63
  :min="min"
52
64
  :max="max"
53
65
  :step="step"
54
- :disabled="disabled"
66
+ :disabled="field.disabled"
67
+ :required="field.required"
55
68
  data-lp-ring-owner
56
69
  :class="rootClass"
57
70
  v-bind="rest"
@@ -67,7 +80,9 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
67
80
  />
68
81
  </NumberFieldDecrement>
69
82
  <NumberFieldInput
70
- :aria-describedby="unit ? unitId : undefined"
83
+ :id="field.id"
84
+ :aria-describedby="describedBy"
85
+ :aria-invalid="field.invalid || undefined"
71
86
  class="min-w-0 flex-1 bg-transparent text-center text-sm text-ink outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none"
72
87
  />
73
88
  <!-- The unit sits between the value and the increment button, so a long one
@@ -126,6 +126,15 @@ const travel = (to: number) => {
126
126
  const from = displayed.value
127
127
  if (from === to) return
128
128
 
129
+ // Infinity and NaN (a rate divided by zero) have no digits to roll through.
130
+ if (!Number.isFinite(to) || !Number.isFinite(from)) {
131
+ displayed.value = to
132
+ lastTarget = to
133
+ settled.value = true
134
+ padTo.value = 0
135
+ return
136
+ }
137
+
129
138
  if (prefersReducedMotion() || props.duration <= 0) {
130
139
  displayed.value = to
131
140
  return
@@ -221,7 +230,11 @@ const UNITS = [
221
230
 
222
231
  const scaleFor = (v: number) => {
223
232
  const abs = Math.abs(v)
224
- const unit = UNITS.find((u) => abs >= u.limit)
233
+ let at = UNITS.findIndex((u) => abs >= u.limit)
234
+ // 999 950 at one place reads "1000.0k"; that is the next unit up.
235
+ if (at > 0 && Math.abs(Number((v / UNITS[at].limit).toFixed(1))) >= 1000) at--
236
+ if (at === -1 && abs >= 999.5) at = UNITS.length - 1
237
+ const unit = UNITS[at]
225
238
  if (!unit) return { divisor: 1, suffix: "", decimals: 0 }
226
239
  // One decimal is what makes 1.2k readable; a value that lands exactly on the
227
240
  // unit (5M) keeps none, matching how these are written by hand.
@@ -234,11 +247,13 @@ const scaleFor = (v: number) => {
234
247
  }
235
248
 
236
249
 
237
- const formatted = computed(() => {
238
- // Rounded first: every branch below should render the shape the endpoints
239
- // imply, not whatever precision an intermediate value happens to carry.
240
- const v = round(displayed.value)
250
+ const formatted = computed(() => formatAt(round(displayed.value), !settled.value))
251
+
252
+ // Rounded first by the caller: every branch should render the shape the
253
+ // endpoints imply, not whatever precision an intermediate value carries.
254
+ function formatAt(v: number, padded: boolean): string {
241
255
  if (props.format) return props.format(v)
256
+ if (!Number.isFinite(v)) return String(v)
242
257
  if (props.compact) {
243
258
  /*
244
259
  * The unit tracks the CURRENT value, not the destination — the way a real
@@ -254,7 +269,7 @@ const formatted = computed(() => {
254
269
  const places = divisor === 1 ? 0 : 1
255
270
  return `${(v / divisor).toFixed(places)}${suffix}`
256
271
  }
257
- const pad = settled.value ? 1 : Math.max(1, padTo.value)
272
+ const pad = padded ? Math.max(1, padTo.value) : 1
258
273
  if (props.grouped)
259
274
  return v.toLocaleString(props.locale, {
260
275
  minimumFractionDigits: props.decimals,
@@ -271,7 +286,7 @@ const formatted = computed(() => {
271
286
  const int = dot === -1 ? body : body.slice(0, dot)
272
287
  const rest = dot === -1 ? "" : body.slice(dot)
273
288
  return `${neg ? "-" : ""}${int.padStart(pad, "0")}${rest}`
274
- })
289
+ }
275
290
 
276
291
  interface Cell {
277
292
  /** Stable across renders so a column keeps its identity as digits shift. */
@@ -357,8 +372,10 @@ const offsetFor = (digit: number) => {
357
372
  return digit
358
373
  }
359
374
 
375
+ /* What a screen reader hears: the settled value, formatted the way it will
376
+ land, never a frame of the roll. */
360
377
  const label = computed(
361
- () => `${props.prefix ?? ""}${props.value}${props.suffix ?? ""}`,
378
+ () => `${props.prefix ?? ""}${formatAt(props.value, false)}${props.suffix ?? ""}`,
362
379
  )
363
380
  </script>
364
381
 
@@ -368,9 +385,8 @@ const label = computed(
368
385
  <span
369
386
  ref="revealAnchor"
370
387
  class="lp-nf inline-flex items-baseline tabular-nums"
371
- :aria-label="label"
372
- role="text"
373
388
  >
389
+ <span class="sr-only">{{ label }}</span>
374
390
  <span v-if="prefix" aria-hidden="true">{{ prefix }}</span>
375
391
 
376
392
  <span class="lp-nf__track inline-flex items-baseline" aria-hidden="true">
@@ -16,8 +16,10 @@ export default { inheritAttrs: false }
16
16
  * is filled. Digits-only by default (TOTP); flip `alphanumeric` for backup codes.
17
17
  */
18
18
  import { PinInputInput, PinInputRoot } from "reka-ui"
19
- import { computed } from "vue"
19
+ import { computed, ref, watch } from "vue"
20
+ import { useFieldControl } from "../composables/useField"
20
21
  import { useMergedAttrs } from "../composables/useMergedClass"
22
+ import { joinCells, splitCode, type OtpCells } from "./otp"
21
23
 
22
24
  const props = withDefaults(
23
25
  defineProps<{
@@ -45,13 +47,44 @@ const emit = defineEmits<{
45
47
 
46
48
  // reka models the value as an array of per-cell strings; we bridge to a single
47
49
  // string at the boundary so callers never touch the array shape.
50
+ //
51
+ // The cells are kept here, positions and all, rather than re-split from the
52
+ // string on every render: clearing a middle cell leaves a gap the string cannot
53
+ // express, and re-splitting it shifted every later digit one cell left. The
54
+ // cells are only rebuilt when the model changes from outside.
55
+ const local = ref<string[]>(splitCode(props.modelValue, props.length))
56
+ let emitted = props.modelValue ?? ""
57
+ watch(
58
+ () => props.modelValue ?? "",
59
+ (value) => {
60
+ if (value === emitted) return
61
+ emitted = value
62
+ local.value = splitCode(value, props.length)
63
+ },
64
+ )
65
+ watch(
66
+ () => props.length,
67
+ (length) => {
68
+ local.value = local.value.slice(0, length)
69
+ },
70
+ )
48
71
  const cells = computed<string[]>({
49
- get: () => (props.modelValue ?? "").split("").slice(0, props.length),
50
- set: (arr) => emit("update:modelValue", arr.join("")),
72
+ get: () => local.value,
73
+ set: (arr) => {
74
+ local.value = arr
75
+ emitted = joinCells(arr)
76
+ emit("update:modelValue", emitted)
77
+ },
51
78
  })
52
79
 
53
- function onComplete(arr: string[]) {
54
- emit("complete", arr.join(""))
80
+ const field = useFieldControl(() => ({
81
+ wrapped: false,
82
+ invalid: props.invalid,
83
+ disabled: props.disabled,
84
+ }))
85
+
86
+ function onComplete(arr: OtpCells) {
87
+ emit("complete", joinCells(arr))
55
88
  }
56
89
 
57
90
  const cellSize = {
@@ -71,7 +104,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
71
104
  v-model="cells"
72
105
  :type="alphanumeric ? 'text' : 'number'"
73
106
  :mask="mask"
74
- :disabled="disabled"
107
+ :disabled="field.disabled"
75
108
  otp
76
109
  :class="rootClass"
77
110
  v-bind="rest"
@@ -81,14 +114,16 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
81
114
  v-for="i in length"
82
115
  :key="i"
83
116
  :index="i - 1"
84
- :aria-invalid="invalid || undefined"
117
+ :id="i === 1 ? field.id : undefined"
118
+ :aria-invalid="field.invalid || undefined"
119
+ :aria-describedby="field.describedBy"
85
120
  :class="[
86
121
  cellSize[size],
87
122
  'rounded-control border bg-surface-soft text-center font-semibold text-ink caret-brand',
88
123
  'outline-none transition-colors duration-[var(--duration-fast)]',
89
124
  'focus:border-brand focus:ring-2 focus:ring-ring',
90
125
  'disabled:cursor-not-allowed disabled:opacity-55',
91
- invalid ? 'border-danger focus:border-danger focus:ring-danger-soft' : 'border-line',
126
+ field.invalid ? 'border-danger focus:border-danger focus:ring-danger-soft' : 'border-line',
92
127
  ]"
93
128
  />
94
129
  </PinInputRoot>