@leavepulse/ui 0.71.2 → 0.72.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 (272) hide show
  1. package/dist/assets/LpTabs-CDchzoRL.css +18 -0
  2. package/dist/chunks/{ContextMenuItems-eS7qx9gD.js → ContextMenuItems-BXumicbh.js} +1 -1
  3. package/dist/chunks/{DropzoneLinkField-DkFhdfEJ.js → DropzoneLinkField-D2Rnkcys.js} +2 -2
  4. package/dist/chunks/{FieldWrapper-YzbvvC3M.js → FieldWrapper-DnA1rxZ8.js} +1 -1
  5. package/dist/chunks/{LayoutCanvas-Ba4A9QC5.js → LayoutCanvas-CNOAUk7D.js} +1 -1
  6. package/dist/chunks/{LayoutNode-CTTTLMDM.js → LayoutNode-DF5BMN0J.js} +1 -1
  7. package/dist/chunks/{LpActionBar-B7anGlkI.js → LpActionBar-DfiBL1dE.js} +3 -3
  8. package/dist/chunks/{LpAddressInput-Bu6ws3Fp.js → LpAddressInput-B1bB3F6M.js} +7 -7
  9. package/dist/chunks/{LpAlert-BHi7ARqu.js → LpAlert-CpRF9eYr.js} +3 -3
  10. package/dist/chunks/{LpAppShell-C5YW5pI7.js → LpAppShell-D8Q0Jv9d.js} +68 -18
  11. package/dist/chunks/{LpAutocomplete-DDqe2M5g.js → LpAutocomplete-DOu7FN5P.js} +2 -2
  12. package/dist/chunks/{LpAvatar-DqfKCIdr.js → LpAvatar-Cg6Az3wz.js} +1 -1
  13. package/dist/chunks/{LpBadge-C8fKEmTF.js → LpBadge-vtCpsDmv.js} +3 -3
  14. package/dist/chunks/LpBottomNav-VXJf0cKc.js +89 -0
  15. package/dist/chunks/{LpBreadcrumbs-DhcoStFa.js → LpBreadcrumbs-BdzjQcql.js} +3 -3
  16. package/dist/chunks/{LpButton-BQDAFXBd.js → LpButton-Dk7eT4ro.js} +1 -1
  17. package/dist/chunks/{LpCalendar-NmrA47J9.js → LpCalendar-CuoWks-0.js} +1 -1
  18. package/dist/chunks/{LpCard-CqKLnEhl.js → LpCard-Ce3rw8zm.js} +1 -1
  19. package/dist/chunks/{LpCheckbox-BoOuLnGB.js → LpCheckbox-DLX4amba.js} +2 -2
  20. package/dist/chunks/{LpCodeBlock-Czu-fq1M.js → LpCodeBlock-j0W5bpmF.js} +2 -2
  21. package/dist/chunks/{LpCodeDisplay-C9pIMF2R.js → LpCodeDisplay-COdiv4xu.js} +6 -6
  22. package/dist/chunks/{LpColorPicker-Ee1DZiRd.js → LpColorPicker-1y1QJHZR.js} +56 -28
  23. package/dist/chunks/{LpConfirmDialog-i15pulMT.js → LpConfirmDialog-w1ni5nxh.js} +3 -3
  24. package/dist/chunks/{LpConfirmHost-D0oGavtd.js → LpConfirmHost-Br_-Gn-6.js} +1 -1
  25. package/dist/chunks/{LpContextMenu-DXpFJjlO.js → LpContextMenu-rx2tCjwk.js} +2 -2
  26. package/dist/chunks/{LpCopyButton-Dmq7DmFw.js → LpCopyButton-DaD1KCn6.js} +2 -2
  27. package/dist/chunks/{LpCountdown-CXrEK0ev.js → LpCountdown-Cgy0mVEq.js} +3 -3
  28. package/dist/chunks/{LpDataList-CFPtSsBB.js → LpDataList-CkcGFJgv.js} +3 -3
  29. package/dist/chunks/{LpDatePicker-cC9K-Pk8.js → LpDatePicker-CA0QTfBC.js} +4 -4
  30. package/dist/chunks/{LpDateRangePicker-BVQKPbvo.js → LpDateRangePicker-u3J51xXk.js} +2 -2
  31. package/dist/chunks/{LpDisclosure-hRnX3FWL.js → LpDisclosure-Cpa7Hc5T.js} +1 -1
  32. package/dist/chunks/{LpDrawer-u4rxnuR-.js → LpDrawer-RRamxbLV.js} +1 -1
  33. package/dist/chunks/{LpDropdownMenu-BgZ0Z_Im.js → LpDropdownMenu-Dl7Z4ZhS.js} +2 -2
  34. package/dist/chunks/{LpEditable-CjxuOVh_.js → LpEditable-D3PvD6Zm.js} +5 -5
  35. package/dist/chunks/{LpEmptyState-CKFTtQgJ.js → LpEmptyState-EQHiRYhG.js} +7 -3
  36. package/dist/chunks/{LpFieldNotice-CPI8EwtU.js → LpFieldNotice-CzQ2_5rQ.js} +1 -1
  37. package/dist/chunks/{LpFileTree-BH0gNlSJ.js → LpFileTree-D7KTaJiM.js} +2 -2
  38. package/dist/chunks/{LpFileTreeNode-gPVB1oXM.js → LpFileTreeNode-14GVqQTd.js} +5 -5
  39. package/dist/chunks/{LpFormField-gRASfyj2.js → LpFormField-MSQhRCTz.js} +2 -2
  40. package/dist/chunks/{LpGauge-DFdt33x8.js → LpGauge-BO1PYYqD.js} +2 -2
  41. package/dist/chunks/{LpHintLayer-C2asBIfs.js → LpHintLayer-CmYm37z7.js} +2 -2
  42. package/dist/chunks/{LpHints-CNZQ76JV.js → LpHints-iLIRUkCj.js} +3 -3
  43. package/dist/chunks/{LpIcon-DkP-9mzK.js → LpIcon--5WSb3jJ.js} +1 -1
  44. package/dist/chunks/{LpInfraNode-yTh4-5qa.js → LpInfraNode-BT0VQrOa.js} +2 -2
  45. package/dist/chunks/{LpInput-2H-5ILOB.js → LpInput-1eNvtzXs.js} +2 -2
  46. package/dist/chunks/{LpLightbox-lAt5uIfx.js → LpLightbox-DWw6OSA4.js} +3 -3
  47. package/dist/chunks/{LpLoader-DjJEX8hL.js → LpLoader-B82_CcV0.js} +1 -1
  48. package/dist/chunks/{LpLogViewer-x78YfhJf.js → LpLogViewer-pzanZASC.js} +6 -6
  49. package/dist/chunks/{LpMapPicker-Dp5RSCi9.js → LpMapPicker-DNIUvF6V.js} +2 -2
  50. package/dist/chunks/{LpMediaGallery-D92oJfY_.js → LpMediaGallery-Dx8xzg5N.js} +3 -3
  51. package/dist/chunks/{LpModal-CpH0bNo7.js → LpModal-BnjZ2iem.js} +1 -1
  52. package/dist/chunks/{LpModalNav-Cuna8c_3.js → LpModalNav-Cu04W1pV.js} +4 -4
  53. package/dist/chunks/{LpNotificationBell-BdUQqZhs.js → LpNotificationBell-PSQqFqEv.js} +8 -8
  54. package/dist/chunks/{LpNumberField-DZY3Z670.js → LpNumberField-e0IBkWU1.js} +1 -1
  55. package/dist/chunks/{LpNumberFlow-Dwb9MV1A.js → LpNumberFlow-CcxEGUy0.js} +4 -2
  56. package/dist/chunks/{LpPagination-DbeACavM.js → LpPagination-BIIiYHXR.js} +1 -1
  57. package/dist/chunks/{LpPaletteTheme-DkikZ362.js → LpPaletteTheme-D05ywM1W.js} +4 -4
  58. package/dist/chunks/{LpPasswordInput-BzAZsf5V.js → LpPasswordInput-vdcw-koG.js} +2 -2
  59. package/dist/chunks/{LpPhoneInput-BMpd-0QR.js → LpPhoneInput-CETEp5W7.js} +2 -2
  60. package/dist/chunks/{LpPie-C1uIUFxu.js → LpPie-BlWDxCtG.js} +6 -6
  61. package/dist/chunks/{LpPipeline-zjEM1Lwc.js → LpPipeline-BXjoxkq_.js} +4 -4
  62. package/dist/chunks/{LpProgress-WNaH4emv.js → LpProgress-rOlyafqp.js} +1 -1
  63. package/dist/chunks/{LpQuota-Cz9PvklM.js → LpQuota-B12yijdB.js} +3 -3
  64. package/dist/chunks/{LpRadioGroup-CsXeV9Ct.js → LpRadioGroup-ggaPL92T.js} +1 -1
  65. package/dist/chunks/{LpRangeCalendar-l-0nSFmY.js → LpRangeCalendar-CYYUYqJ7.js} +1 -1
  66. package/dist/chunks/{LpRelativeTime-UM8EkfrN.js → LpRelativeTime-DwgCb5-8.js} +1 -1
  67. package/dist/chunks/{LpRepeater-OvzByE5L.js → LpRepeater-fliVBcN7.js} +3 -3
  68. package/dist/chunks/LpRollingText-B54Pje7P.js +86 -0
  69. package/dist/chunks/{LpScramble-TDO5Kdou.js → LpScramble-Cw-af0Yh.js} +1 -1
  70. package/dist/chunks/{LpSecretField-DwQiPOko.js → LpSecretField-D_IMxEGW.js} +7 -7
  71. package/dist/chunks/{LpSection-DcQsieHG.js → LpSection-DFK_uoJE.js} +1 -1
  72. package/dist/chunks/{LpSegmented-D2VST0o3.js → LpSegmented-CeACsMMA.js} +4 -2
  73. package/dist/chunks/{LpSelect-D3pRJfRY.js → LpSelect-Cxq4yBGd.js} +3 -3
  74. package/dist/chunks/LpShift-BAp8lGoa.js +160 -0
  75. package/dist/chunks/LpSidebar-BftYsk5d.js +494 -0
  76. package/dist/chunks/LpSidebarNav-BzNWMZfc.js +357 -0
  77. package/dist/chunks/LpSidebarRow-BIReMT1f.js +302 -0
  78. package/dist/chunks/{LpStat-Dzn6WFzi.js → LpStat-BFCM6fUn.js} +6 -4
  79. package/dist/chunks/{LpStatus-BQU1TUsv.js → LpStatus-BWTLVPVh.js} +3 -3
  80. package/dist/chunks/{LpStepMark-CXa7DJy_.js → LpStepMark-DkdkebLC.js} +3 -3
  81. package/dist/chunks/{LpStepper-C9RM_yTG.js → LpStepper-BgqjKrqg.js} +1 -1
  82. package/dist/chunks/{LpSwap-DzE6M3Mg.js → LpSwap-B5YOXgtx.js} +1 -1
  83. package/dist/chunks/{LpSwitch-bGZxUDSx.js → LpSwitch-DyrRes7s.js} +1 -1
  84. package/dist/chunks/LpTabFace-D6BMCbAP.js +77 -0
  85. package/dist/chunks/{LpTableFilter-CTZMqFDU.js → LpTableFilter-Dkzamzkl.js} +3 -3
  86. package/dist/chunks/{LpTagsInput-DrjmrOge.js → LpTagsInput-DAZNNehI.js} +5 -5
  87. package/dist/chunks/{LpTextarea-BbYqmG1S.js → LpTextarea-DbmVPj2X.js} +32 -7
  88. package/dist/chunks/{useTheme-BOV_-AiY.js → LpThemeProvider-8MG6yqU9.js} +66 -1
  89. package/dist/chunks/{LpThemeStudio-IupHqP0x.js → LpThemeStudio-DWtG6OZZ.js} +138 -104
  90. package/dist/chunks/LpThemeStudioSection-B7bt8Iyg.js +32 -0
  91. package/dist/chunks/{LpThemeSwitcher-DarPHuX8.js → LpThemeSwitcher-Dcx-VkSk.js} +6 -6
  92. package/dist/chunks/{LpTimeField-DQ8WvPr2.js → LpTimeField-BawSVsmt.js} +1 -1
  93. package/dist/chunks/{LpToaster-BTTl_wuy.js → LpToaster-50hp_B5O.js} +2 -2
  94. package/dist/chunks/{LpTopologyCanvas-Cq-MiJwo.js → LpTopologyCanvas-CnUh8uzS.js} +1 -1
  95. package/dist/chunks/{LpTour-BgCfNlaW.js → LpTour-DC3LTlxg.js} +1 -1
  96. package/dist/chunks/{LpTruncate-TcqfkyJM.js → LpTruncate-Ced4gquB.js} +1 -1
  97. package/dist/chunks/{LpUptimeBar-BbhVTPnL.js → LpUptimeBar-CuB0LX8K.js} +4 -4
  98. package/dist/chunks/{MenuTitle-Dab3EGOp.js → MenuTitle-BmCNNTKH.js} +1 -1
  99. package/dist/chunks/{OptionContent-DPzNsybd.js → OptionContent-22UT8zLn.js} +1 -1
  100. package/dist/chunks/{PickerField-DwLw7j0L.js → PickerField-Bfh8id3d.js} +2 -2
  101. package/dist/chunks/{RevealToggle-BVFM_FAU.js → RevealToggle-B0ITuIX-.js} +2 -2
  102. package/dist/chunks/{TourCard--2IfgnUU.js → TourCard-CBkj1GYi.js} +5 -5
  103. package/dist/chunks/{icons-CLu1iR1j.js → icons-DMITlfw5.js} +1 -1
  104. package/dist/component-names.d.ts +1 -1
  105. package/dist/components/ContextMenuItems.vue.js +1 -1
  106. package/dist/components/DropzoneLinkField.vue.js +1 -1
  107. package/dist/components/FieldWrapper.vue.js +1 -1
  108. package/dist/components/LayoutCanvas.vue.js +1 -1
  109. package/dist/components/LayoutNode.vue.js +1 -1
  110. package/dist/components/LpActionBar.vue.js +1 -1
  111. package/dist/components/LpAddressInput.vue.js +1 -1
  112. package/dist/components/LpAlert.vue.js +1 -1
  113. package/dist/components/LpAppShell.vue.d.ts +67 -2
  114. package/dist/components/LpAppShell.vue.js +1 -1
  115. package/dist/components/LpAutocomplete.vue.js +1 -1
  116. package/dist/components/LpAvatar.vue.js +1 -1
  117. package/dist/components/LpBadge.vue.js +1 -1
  118. package/dist/components/LpBottomNav.vue.d.ts +28 -0
  119. package/dist/components/LpBottomNav.vue.js +4 -0
  120. package/dist/components/LpBreadcrumbs.vue.js +1 -1
  121. package/dist/components/LpButton.vue.js +1 -1
  122. package/dist/components/LpCalendar.vue.js +1 -1
  123. package/dist/components/LpCard.vue.js +1 -1
  124. package/dist/components/LpChart.vue.js +4 -4
  125. package/dist/components/LpCheckbox.vue.js +1 -1
  126. package/dist/components/LpCodeBlock.vue.js +1 -1
  127. package/dist/components/LpCodeDisplay.vue.js +1 -1
  128. package/dist/components/LpColorPicker.vue.d.ts +15 -2
  129. package/dist/components/LpColorPicker.vue.js +1 -1
  130. package/dist/components/LpCommandPalette.vue.js +4 -4
  131. package/dist/components/LpConfirmDialog.vue.js +1 -1
  132. package/dist/components/LpConfirmHost.vue.js +1 -1
  133. package/dist/components/LpContextMenu.vue.js +1 -1
  134. package/dist/components/LpCopyButton.vue.js +1 -1
  135. package/dist/components/LpCountdown.vue.js +1 -1
  136. package/dist/components/LpDataList.vue.js +1 -1
  137. package/dist/components/LpDatePicker.vue.js +1 -1
  138. package/dist/components/LpDateRangePicker.vue.js +1 -1
  139. package/dist/components/LpDisclosure.vue.js +1 -1
  140. package/dist/components/LpDock.vue.js +1 -1
  141. package/dist/components/LpDrawer.vue.js +1 -1
  142. package/dist/components/LpDropdownMenu.vue.js +1 -1
  143. package/dist/components/LpDropzone.vue.js +2 -2
  144. package/dist/components/LpEditable.vue.js +1 -1
  145. package/dist/components/LpEmptyState.vue.d.ts +2 -0
  146. package/dist/components/LpEmptyState.vue.js +1 -1
  147. package/dist/components/LpFieldNotice.vue.js +1 -1
  148. package/dist/components/LpFileTree.vue.js +1 -1
  149. package/dist/components/LpFileTreeNode.vue.js +1 -1
  150. package/dist/components/LpFormField.vue.js +1 -1
  151. package/dist/components/LpGauge.vue.js +1 -1
  152. package/dist/components/LpHintLayer.vue.js +1 -1
  153. package/dist/components/LpHints.vue.js +1 -1
  154. package/dist/components/LpIcon.vue.js +1 -1
  155. package/dist/components/LpImageEditor.vue.js +2 -2
  156. package/dist/components/LpInfraNode.vue.js +1 -1
  157. package/dist/components/LpInput.vue.js +1 -1
  158. package/dist/components/LpLightbox.vue.js +1 -1
  159. package/dist/components/LpLoader.vue.js +1 -1
  160. package/dist/components/LpLogViewer.vue.d.ts +1 -1
  161. package/dist/components/LpLogViewer.vue.js +1 -1
  162. package/dist/components/LpMapPicker.vue.js +1 -1
  163. package/dist/components/LpMediaGallery.vue.js +1 -1
  164. package/dist/components/LpModal.vue.js +1 -1
  165. package/dist/components/LpModalNav.vue.js +1 -1
  166. package/dist/components/LpNotificationBell.vue.js +1 -1
  167. package/dist/components/LpNumberField.vue.js +1 -1
  168. package/dist/components/LpNumberFlow.vue.js +1 -1
  169. package/dist/components/LpPagination.vue.js +1 -1
  170. package/dist/components/LpPaletteFacts.vue.js +2 -2
  171. package/dist/components/LpPaletteHeader.vue.js +2 -2
  172. package/dist/components/LpPaletteStats.vue.js +2 -2
  173. package/dist/components/LpPaletteTheme.vue.js +1 -1
  174. package/dist/components/LpPasswordInput.vue.js +1 -1
  175. package/dist/components/LpPhoneInput.vue.js +1 -1
  176. package/dist/components/LpPie.vue.js +1 -1
  177. package/dist/components/LpPipeline.vue.js +1 -1
  178. package/dist/components/LpProgress.vue.js +1 -1
  179. package/dist/components/LpQuota.vue.js +1 -1
  180. package/dist/components/LpRadioGroup.vue.js +1 -1
  181. package/dist/components/LpRangeCalendar.vue.js +1 -1
  182. package/dist/components/LpRelativeTime.vue.js +1 -1
  183. package/dist/components/LpRepeater.vue.js +1 -1
  184. package/dist/components/LpRollingText.vue.d.ts +2 -2
  185. package/dist/components/LpRollingText.vue.js +1 -1
  186. package/dist/components/LpScramble.vue.js +1 -1
  187. package/dist/components/LpSecretField.vue.js +1 -1
  188. package/dist/components/LpSection.vue.js +1 -1
  189. package/dist/components/LpSegmented.vue.js +1 -1
  190. package/dist/components/LpSelect.vue.js +1 -1
  191. package/dist/components/LpShift.vue.d.ts +20 -0
  192. package/dist/components/LpShift.vue.js +1 -1
  193. package/dist/components/LpShortcutsDialog.vue.js +1 -1
  194. package/dist/components/LpSidebar.vue.d.ts +74 -23
  195. package/dist/components/LpSidebar.vue.js +1 -1
  196. package/dist/components/LpSidebarNav.vue.d.ts +30 -2
  197. package/dist/components/LpSidebarNav.vue.js +1 -1
  198. package/dist/components/LpSidebarRow.vue.d.ts +49 -0
  199. package/dist/components/LpSidebarRow.vue.js +4 -0
  200. package/dist/components/LpStat.vue.d.ts +11 -0
  201. package/dist/components/LpStat.vue.js +1 -1
  202. package/dist/components/LpStatus.vue.js +1 -1
  203. package/dist/components/LpStepMark.vue.js +1 -1
  204. package/dist/components/LpStepper.vue.js +1 -1
  205. package/dist/components/LpSwap.vue.js +1 -1
  206. package/dist/components/LpSwitch.vue.js +1 -1
  207. package/dist/components/LpTabFace.vue.d.ts +14 -0
  208. package/dist/components/LpTabFace.vue.js +4 -0
  209. package/dist/components/LpTable.vue.js +11 -11
  210. package/dist/components/LpTableFilter.vue.js +1 -1
  211. package/dist/components/LpTabs.vue.d.ts +4 -4
  212. package/dist/components/LpTabs.vue.js +18 -72
  213. package/dist/components/LpTagsInput.vue.js +1 -1
  214. package/dist/components/LpTextarea.vue.d.ts +19 -1
  215. package/dist/components/LpTextarea.vue.js +1 -1
  216. package/dist/components/LpThemeProvider.vue.js +1 -1
  217. package/dist/components/LpThemeStudio.vue.d.ts +2 -2
  218. package/dist/components/LpThemeStudio.vue.js +1 -1
  219. package/dist/components/LpThemeStudioSection.vue.d.ts +4 -1
  220. package/dist/components/LpThemeStudioSection.vue.js +1 -1
  221. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  222. package/dist/components/LpTimeField.vue.js +1 -1
  223. package/dist/components/LpToaster.vue.js +1 -1
  224. package/dist/components/LpTopologyCanvas.vue.js +1 -1
  225. package/dist/components/LpTour.vue.js +1 -1
  226. package/dist/components/LpTruncate.vue.js +1 -1
  227. package/dist/components/LpUptimeBar.vue.js +1 -1
  228. package/dist/components/MenuTitle.vue.js +1 -1
  229. package/dist/components/OptionContent.vue.js +1 -1
  230. package/dist/components/PickerField.vue.js +1 -1
  231. package/dist/components/RevealToggle.vue.js +1 -1
  232. package/dist/components/TourCard.vue.js +1 -1
  233. package/dist/components/menuWhen.js +1 -1
  234. package/dist/components/rolling.d.ts +5 -0
  235. package/dist/components/rolling.js +23 -0
  236. package/dist/components/sidebar.d.ts +33 -0
  237. package/dist/components/tabs.d.ts +3 -0
  238. package/dist/components/tabs.js +6 -0
  239. package/dist/icons-all.js +1 -1
  240. package/dist/index.d.ts +1 -0
  241. package/dist/index.js +154 -153
  242. package/package.json +1 -1
  243. package/src/component-names.ts +1 -0
  244. package/src/components/LpAppShell.vue +75 -7
  245. package/src/components/LpBottomNav.vue +84 -0
  246. package/src/components/LpColorPicker.vue +32 -1
  247. package/src/components/LpEmptyState.vue +4 -1
  248. package/src/components/LpNumberFlow.vue +4 -2
  249. package/src/components/LpRollingText.vue +27 -2
  250. package/src/components/LpSegmented.vue +3 -1
  251. package/src/components/LpShift.vue +174 -2
  252. package/src/components/LpSidebar.vue +304 -18
  253. package/src/components/LpSidebarNav.vue +200 -77
  254. package/src/components/LpSidebarRow.vue +238 -0
  255. package/src/components/LpStat.vue +7 -1
  256. package/src/components/LpTabFace.vue +52 -0
  257. package/src/components/LpTabs.vue +6 -41
  258. package/src/components/LpTextarea.vue +34 -4
  259. package/src/components/LpThemeStudio.vue +91 -55
  260. package/src/components/LpThemeStudioSection.vue +15 -7
  261. package/src/components/rolling.ts +28 -0
  262. package/src/components/sidebar.ts +33 -0
  263. package/src/components/tabs.ts +9 -0
  264. package/src/index.ts +1 -0
  265. package/src/kit-icons.ts +1 -1
  266. package/dist/assets/LpTabs-DT0XPTc2.css +0 -18
  267. package/dist/chunks/LpRollingText-DiL717LK.js +0 -51
  268. package/dist/chunks/LpShift-DXNsauQZ.js +0 -40
  269. package/dist/chunks/LpSidebar-Bk7b-K4l.js +0 -256
  270. package/dist/chunks/LpSidebarNav-BR2AW3KT.js +0 -216
  271. package/dist/chunks/LpThemeProvider-DcbfOxsU.js +0 -69
  272. package/dist/chunks/LpThemeStudioSection-BwhqYDLf.js +0 -20
@@ -12,19 +12,15 @@ export default { inheritAttrs: false }
12
12
  * in two shells — the static desktop rail and the swipeable mobile drawer —
13
13
  * without duplicating markup. Not exported on its own; an LpSidebar detail.
14
14
  */
15
- import { Motion } from "motion-v"
16
15
  import { CollapsibleContent, CollapsibleRoot, CollapsibleTrigger } from "reka-ui"
17
16
  import { computed, ref } from "vue"
18
- import { useLocale } from "../composables/useLocale"
19
- import { usePillTransition } from "../composables/usePillTransition"
20
17
  import LpBadge from "./LpBadge.vue"
21
18
  import LpContextMenu from "./LpContextMenu.vue"
22
19
  import LpIcon from "./LpIcon.vue"
23
20
  import LpScrollArea from "./LpScrollArea.vue"
24
21
  import LpSkeleton from "./LpSkeleton.vue"
22
+ import LpSidebarRow from "./LpSidebarRow.vue"
25
23
  import type { SidebarItem, SidebarSection } from "./sidebar"
26
- import { resolveLink, type ResolvedLink } from "./link"
27
- import { useLinkComponent } from "../composables/useLpConfig"
28
24
  import { useMergedAttrs } from "../composables/useMergedClass"
29
25
 
30
26
  const props = withDefaults(
@@ -52,22 +48,24 @@ const props = withDefaults(
52
48
  * short rail (an app switcher, a settings list) can turn it on.
53
49
  */
54
50
  hoverFollow?: boolean
51
+ /** Icons only, each row's name in a card beside it. */
52
+ compact?: boolean
53
+ /** Rows offer "Pin to top" in their menu; `pinned` are shown first. */
54
+ pinnable?: boolean
55
+ pinned?: SidebarItem[]
56
+ previewDelay?: number
57
+ labels: { pinned: string; pin: string; unpin: string; expand: string }
55
58
  }>(),
56
- { skeletonRows: 6, skeletonHeader: true, collapsedKeys: () => [], hoverFollow: false },
59
+ { skeletonRows: 6, skeletonHeader: true, collapsedKeys: () => [], hoverFollow: false, pinned: () => [], previewDelay: 900 },
57
60
  )
58
61
 
59
62
  const emit = defineEmits<{
60
63
  (e: "activate", item: SidebarItem): void
61
64
  (e: "toggle-section", key: string, collapsed: boolean): void
65
+ (e: "toggle-pin", id: string): void
62
66
  }>()
63
67
 
64
68
  /** `key` when the app set one, else the title — see SidebarSection.key. */
65
- // A count reads in the app's locale like every other figure: "7 563", not "7563".
66
- const locale = useLocale()
67
- function badgeText(badge: number | string): string {
68
- return typeof badge === "number" ? badge.toLocaleString(locale.value) : badge
69
- }
70
-
71
69
  function sectionKey(section: SidebarSection): string {
72
70
  return section.key ?? section.title ?? ""
73
71
  }
@@ -80,7 +78,7 @@ function sectionKey(section: SidebarSection): string {
80
78
  const foldedKeys = computed(() => {
81
79
  const holdsActive = new Set(
82
80
  props.groups
83
- .filter((g) => g.items.some((i) => props.itemActive(i)))
81
+ .filter((g) => g.items.some((i) => props.itemActive(i) || onPath(i)))
84
82
  .map(sectionKey),
85
83
  )
86
84
  return new Set(
@@ -89,11 +87,10 @@ const foldedKeys = computed(() => {
89
87
  })
90
88
 
91
89
  function isOpen(section: SidebarSection): boolean {
92
- return !foldedKeys.value.has(sectionKey(section))
90
+ // Compact, a group has no heading to unfold it by.
91
+ return props.compact || !foldedKeys.value.has(sectionKey(section))
93
92
  }
94
93
 
95
- const pillTransition = usePillTransition()
96
-
97
94
  const hovered = ref<string | null>(null)
98
95
 
99
96
  function hover(item: SidebarItem) {
@@ -105,16 +102,107 @@ function pillUnder(item: SidebarItem): boolean {
105
102
  return hovered.value ? hovered.value === item.id : props.itemActive(item)
106
103
  }
107
104
 
108
- const linkComponent = useLinkComponent()
105
+ /** One of the item's pages is the current one. */
106
+ function onPath(item: SidebarItem): boolean {
107
+ return !!item.children?.some((c) => props.itemActive(c))
108
+ }
109
+
110
+ // Pages under a row open with the current page among them and otherwise when
111
+ // asked; the asked-for ones are remembered for as long as the nav lives.
112
+ const opened = ref(new Set<string>())
113
+ function expanded(item: SidebarItem): boolean {
114
+ return onPath(item) || opened.value.has(item.id)
115
+ }
116
+ function toggle(item: SidebarItem) {
117
+ const next = new Set(opened.value)
118
+ if (next.has(item.id)) next.delete(item.id)
119
+ else next.add(item.id)
120
+ opened.value = next
121
+ }
122
+
123
+ const pinnedIds = computed(() => new Set(props.pinned.map((i) => i.id)))
124
+ function menuOf(item: SidebarItem) {
125
+ const own = item.menu ?? []
126
+ if (!props.pinnable) return own
127
+ const pinned = pinnedIds.value.has(item.id)
128
+ return [
129
+ ...own,
130
+ {
131
+ label: pinned ? props.labels.unpin : props.labels.pin,
132
+ icon: pinned ? "lucide:pin-off" : "lucide:pin",
133
+ separatorBefore: own.length > 0,
134
+ onSelect: () => emit("toggle-pin", item.id),
135
+ },
136
+ ]
137
+ }
109
138
 
110
139
  /*
111
- * A disabled item is always a disabled button: an anchor cannot be disabled,
112
- * and the old enabled-button fallback for a disabled link stayed focusable.
140
+ * The keyboard, as a tree: arrows walk the rows that are showing, right and
141
+ * left open and close a group or the pages under a row (left from such a page
142
+ * goes back to its row), Home and End jump to the ends, and typing jumps to the
143
+ * next row starting with what was typed. Tab still reaches every row, so this
144
+ * only adds a faster way through a long nav.
113
145
  */
114
- function rowOf(item: SidebarItem): ResolvedLink {
115
- const link = item.disabled ? null : resolveLink(linkComponent.value, { to: item.to, href: item.href })
116
- if (link) return link
117
- return { is: "button", props: { type: "button", disabled: item.disabled || undefined } }
146
+ let typed = ""
147
+ let typedAt = 0
148
+
149
+ function onKey(event: KeyboardEvent) {
150
+ const box = event.currentTarget as HTMLElement
151
+ const list = [...box.querySelectorAll<HTMLElement>("[data-lp-nav-row]")].filter((r) => r.offsetParent !== null)
152
+ const at = list.indexOf(document.activeElement as HTMLElement)
153
+ if (at < 0) return
154
+ const row = list[at]!
155
+ const go = (i: number) => {
156
+ event.preventDefault()
157
+ list[(i + list.length) % list.length]?.focus()
158
+ }
159
+ switch (event.key) {
160
+ case "ArrowDown":
161
+ return go(at + 1)
162
+ case "ArrowUp":
163
+ return go(at - 1)
164
+ case "Home":
165
+ return go(0)
166
+ case "End":
167
+ return go(list.length - 1)
168
+ case "ArrowRight":
169
+ case "ArrowLeft": {
170
+ const open = event.key === "ArrowRight"
171
+ const toggle = row.hasAttribute("aria-expanded")
172
+ ? row
173
+ : row.parentElement?.querySelector<HTMLElement>(":scope > button[aria-expanded]")
174
+ if (toggle && (toggle.getAttribute("aria-expanded") === "true") !== open) {
175
+ event.preventDefault()
176
+ toggle.click()
177
+ return
178
+ }
179
+ if (!open && row.dataset.lpNavNested !== undefined) {
180
+ for (let i = at - 1; i >= 0; i--) if (list[i]!.dataset.lpNavNested === undefined) return go(i)
181
+ }
182
+ return
183
+ }
184
+ }
185
+ if (event.key.length !== 1 || event.ctrlKey || event.metaKey || event.altKey) return
186
+ const now = Date.now()
187
+ typed = now - typedAt > 700 ? event.key : typed + event.key
188
+ typedAt = now
189
+ const needle = typed.toLocaleLowerCase()
190
+ for (let k = 1; k <= list.length; k++) {
191
+ const next = list[(at + k) % list.length]!
192
+ if ((next.textContent ?? "").trim().toLocaleLowerCase().startsWith(needle)) return go(at + k)
193
+ }
194
+ }
195
+
196
+ const SECTION = "lp-section:"
197
+ function sectionItem(section: SidebarSection): SidebarItem {
198
+ const icon = section.icon ?? section.items.find((i) => i.icon)?.icon
199
+ return { id: `${SECTION}${sectionKey(section)}`, label: section.title ?? "", icon, children: section.items }
200
+ }
201
+
202
+ function activate(item: SidebarItem) {
203
+ if (item.id.startsWith(SECTION)) return
204
+ if (item.children?.length && !item.to && !item.href) toggle(item)
205
+ emit("activate", item)
118
206
  }
119
207
 
120
208
  // The nav fills the remaining height with a fixed gap between groups; a short sidebar needs both under the consumer's control.
@@ -151,12 +239,51 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
151
239
  <LpScrollArea
152
240
  v-else
153
241
  class="min-h-0 flex-1"
154
- content-class="flex min-h-full flex-col gap-4"
242
+ :content-class="compact ? 'flex min-h-full flex-col gap-1' : 'flex min-h-full flex-col gap-4'"
155
243
  @pointerleave="hovered = null"
156
244
  >
245
+ <!-- `contents`: no box of its own, only somewhere for the rows' keys to bubble to. -->
246
+ <div class="contents" @keydown="onKey">
247
+ <!-- Pinned pages come first and carry no pill: the same page further down
248
+ already holds it, and one pill shared by two rows would jump between
249
+ them. -->
250
+ <div v-if="pinned.length" class="flex flex-col gap-0.5">
251
+ <p v-if="!compact" class="px-2 pb-1 pt-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted">
252
+ {{ labels.pinned }}
253
+ </p>
254
+ <LpSidebarRow
255
+ v-for="item in pinned"
256
+ :key="`pinned:${item.id}`"
257
+ :item="item"
258
+ :active="itemActive(item)"
259
+ :pill="false"
260
+ :pill-id="pillId"
261
+ :compact="compact"
262
+ :menu="menuOf(item)"
263
+ :preview-delay="previewDelay"
264
+ :item-active="itemActive"
265
+ @activate="activate"
266
+ />
267
+ </div>
268
+ <template v-for="(section, si) in groups" :key="si">
269
+ <!-- Compact, a group that folds is one icon: its pages open in the card
270
+ beside it, as a page's own pages do. A hundred initials in a column
271
+ would be no nav at all. -->
272
+ <div v-if="compact && section.collapsible && section.title" class="flex flex-col gap-0.5">
273
+ <LpSidebarRow
274
+ :item="sectionItem(section)"
275
+ :active="false"
276
+ :pill="false"
277
+ :pill-id="pillId"
278
+ compact
279
+ :on-path="section.items.some((i) => itemActive(i) || onPath(i))"
280
+ :preview-delay="previewDelay"
281
+ :item-active="itemActive"
282
+ @activate="activate"
283
+ />
284
+ </div>
157
285
  <CollapsibleRoot
158
- v-for="(section, si) in groups"
159
- :key="si"
286
+ v-else
160
287
  as="div"
161
288
  class="flex flex-col gap-0.5"
162
289
  :open="isOpen(section)"
@@ -165,8 +292,10 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
165
292
  >
166
293
  <!-- A plain heading while the group is fixed; a control once it folds,
167
294
  because a thing that reacts to a click must look like one. -->
295
+ <div v-if="compact && (si > 0 || pinned.length)" class="mx-3 mb-1 border-t border-line" />
168
296
  <CollapsibleTrigger
169
- v-if="section.title && section.collapsible"
297
+ v-else-if="!compact && section.title && section.collapsible"
298
+ data-lp-nav-row
170
299
  class="group/section mt-2 flex cursor-pointer items-center gap-2 rounded-control px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted outline-none transition-colors duration-[var(--duration-fast)] hover:bg-ink/[0.04] hover:text-ink focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
171
300
  >
172
301
  <LpIcon
@@ -197,7 +326,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
197
326
  />
198
327
  </CollapsibleTrigger>
199
328
  <p
200
- v-else-if="section.title"
329
+ v-else-if="!compact && section.title"
201
330
  class="px-2 pb-1 pt-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted"
202
331
  >
203
332
  {{ section.title }}
@@ -206,58 +335,52 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
206
335
  <CollapsibleContent
207
336
  class="flex flex-col gap-0.5 overflow-hidden data-[state=open]:animate-[collapsible-down_260ms_var(--ease-emphasized)] data-[state=closed]:animate-[collapsible-up_200ms_var(--ease-emphasized)] motion-reduce:animate-none"
208
337
  >
209
- <!-- Optional right-click menu per item; passthrough when item.menu is
210
- absent or empty, so the item keeps the native context menu. Headed
211
- by the item's label and icon unless it names its own. -->
212
- <LpContextMenu
213
- v-for="item in section.items"
214
- :key="item.id"
215
- :items="item.menu ?? []"
216
- :title="(item.menuTitle ?? item.label) || undefined"
217
- :title-icon="item.icon"
218
- >
219
- <component
220
- :is="rowOf(item).is"
221
- v-bind="rowOf(item).props"
222
- :aria-current="itemActive(item) ? 'page' : undefined"
223
- :aria-disabled="item.disabled || undefined"
224
- class="group/item relative flex items-center gap-3 rounded-control px-3 py-2 text-sm font-medium outline-none transition-colors duration-[var(--duration-fast)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
225
- :class="item.disabled
226
- ? 'cursor-not-allowed text-muted/50'
227
- : itemActive(item) || hovered === item.id
228
- ? 'text-ink'
229
- : 'text-muted hover:text-ink'"
230
- @click="emit('activate', item)"
231
- @pointerenter="hover(item)"
338
+ <template v-for="item in section.items" :key="item.id">
339
+ <LpSidebarRow
340
+ :item="item"
341
+ :active="itemActive(item)"
342
+ :pill="pillUnder(item)"
343
+ :pill-id="pillId"
344
+ :compact="compact"
345
+ :menu="menuOf(item)"
346
+ :expandable="!compact && !!item.children?.length"
347
+ :expanded="expanded(item)"
348
+ :on-path="onPath(item)"
349
+ :preview-delay="previewDelay"
350
+ :hovered="hovered === item.id"
351
+ :expand-label="labels.expand"
352
+ :item-active="itemActive"
353
+ @activate="activate"
354
+ @hover="hover"
355
+ @toggle="toggle(item)"
232
356
  >
233
- <!-- Sliding active pill -->
234
- <Motion
235
- v-if="pillUnder(item)"
236
- :layout-id="pillId"
237
- :transition="pillTransition"
238
- class="absolute inset-0 z-0 rounded-control bg-brand-soft"
239
- />
240
- <!-- #item slot for fully custom rows; falls back to icon+label+badge. -->
241
- <slot name="item" :item="item" :active="itemActive(item)">
242
- <LpIcon
243
- v-if="item.icon"
244
- :name="item.icon"
245
- :size="17"
246
- class="relative z-10 shrink-0 transition-colors"
247
- :class="itemActive(item) ? 'text-brand' : ''"
357
+ <template v-if="$slots.item" #default="slotProps"><slot name="item" v-bind="slotProps" /></template>
358
+ </LpSidebarRow>
359
+ <CollapsibleRoot v-if="!compact && item.children?.length" :open="expanded(item)">
360
+ <CollapsibleContent
361
+ class="flex flex-col gap-0.5 overflow-hidden data-[state=open]:animate-[collapsible-down_220ms_var(--ease-emphasized)] data-[state=closed]:animate-[collapsible-up_180ms_var(--ease-emphasized)] motion-reduce:animate-none"
362
+ >
363
+ <LpSidebarRow
364
+ v-for="child in item.children"
365
+ :key="child.id"
366
+ nested
367
+ :item="child"
368
+ :active="itemActive(child)"
369
+ :pill="pillUnder(child)"
370
+ :pill-id="pillId"
371
+ :menu="menuOf(child)"
372
+ :preview-delay="previewDelay"
373
+ :hovered="hovered === child.id"
374
+ :item-active="itemActive"
375
+ @activate="activate"
376
+ @hover="hover"
248
377
  />
249
- <span class="relative z-10 min-w-0 flex-1 truncate text-left">{{ item.label }}</span>
250
- <LpBadge
251
- v-if="item.badge != null"
252
- :tone="itemActive(item) ? 'brand' : 'neutral'"
253
- class="relative z-10 shrink-0"
254
- >
255
- {{ badgeText(item.badge) }}
256
- </LpBadge>
257
- </slot>
258
- </component>
259
- </LpContextMenu>
378
+ </CollapsibleContent>
379
+ </CollapsibleRoot>
380
+ </template>
260
381
  </CollapsibleContent>
261
382
  </CollapsibleRoot>
383
+ </template>
384
+ </div>
262
385
  </LpScrollArea>
263
386
  </template>
@@ -0,0 +1,238 @@
1
+ <script setup lang="ts">
2
+ /*
3
+ * One row of LpSidebarNav: the link or button, its active pill, icon, label,
4
+ * count and keys, its right-click menu and the card beside it. A page, a page
5
+ * under another and a pinned page are drawn by this one component, so the
6
+ * three never disagree on what a row looks like. Not exported on its own.
7
+ *
8
+ * The card beside the row does two jobs that are one mechanism: in a compact
9
+ * sidebar it is the row's name (and the pages under it), since the row shows
10
+ * only an icon; with a `preview` it is a glance at the page before going there.
11
+ */
12
+ import { Motion } from "motion-v"
13
+ import { computed, onBeforeUnmount, ref, watch } from "vue"
14
+ import { useLocale } from "../composables/useLocale"
15
+ import { usePillTransition } from "../composables/usePillTransition"
16
+ import { useLinkComponent } from "../composables/useLpConfig"
17
+ import LpBadge from "./LpBadge.vue"
18
+ import LpContextMenu, { type ContextMenuItemDef } from "./LpContextMenu.vue"
19
+ import LpHoverCard from "./LpHoverCard.vue"
20
+ import LpIcon from "./LpIcon.vue"
21
+ import LpKeys from "./LpKeys.vue"
22
+ import LpNumberFlow from "./LpNumberFlow.vue"
23
+ import type { SidebarItem } from "./sidebar"
24
+ import { resolveLink, type ResolvedLink } from "./link"
25
+
26
+ const props = withDefaults(
27
+ defineProps<{
28
+ item: SidebarItem
29
+ active: boolean
30
+ /** The pill sits under this row (active, or hovered while following hover). */
31
+ pill: boolean
32
+ pillId: string
33
+ compact?: boolean
34
+ /** A page under another: indented, a size smaller. */
35
+ nested?: boolean
36
+ menu?: ContextMenuItemDef[]
37
+ /** Has pages under it; `expanded` says whether they show. */
38
+ expandable?: boolean
39
+ expanded?: boolean
40
+ /** One of its pages is the current one: the row reads as on the path. */
41
+ onPath?: boolean
42
+ previewDelay?: number
43
+ hovered?: boolean
44
+ expandLabel?: string
45
+ itemActive?: (item: SidebarItem) => boolean
46
+ }>(),
47
+ { previewDelay: 900, menu: () => [] },
48
+ )
49
+
50
+ const emit = defineEmits<{
51
+ (e: "activate", item: SidebarItem): void
52
+ (e: "hover", item: SidebarItem): void
53
+ (e: "toggle"): void
54
+ }>()
55
+
56
+ const locale = useLocale()
57
+ const pillTransition = usePillTransition()
58
+ const linkComponent = useLinkComponent()
59
+
60
+ /*
61
+ * A disabled item is always a disabled button: an anchor cannot be disabled,
62
+ * and the old enabled-button fallback for a disabled link stayed focusable.
63
+ */
64
+ function rowOf(item: SidebarItem): ResolvedLink {
65
+ const link = item.disabled ? null : resolveLink(linkComponent.value, { to: item.to, href: item.href })
66
+ if (link) return link
67
+ return { is: "button", props: { type: "button", disabled: item.disabled || undefined } }
68
+ }
69
+
70
+ // A count that grows pulses once: a number that changes in place is easy to
71
+ // miss in the corner of the eye, and the nav is exactly the corner of the eye.
72
+ const bumped = ref(false)
73
+ let bumpTimer: ReturnType<typeof setTimeout> | undefined
74
+ watch(
75
+ () => props.item.badge,
76
+ (now, was) => {
77
+ if (typeof now !== "number" || typeof was !== "number" || now <= was) return
78
+ bumped.value = true
79
+ clearTimeout(bumpTimer)
80
+ bumpTimer = setTimeout(() => (bumped.value = false), 1200)
81
+ },
82
+ )
83
+ onBeforeUnmount(() => clearTimeout(bumpTimer))
84
+
85
+ /** A compact row without an icon shows the first letters of its name. */
86
+ const initials = computed(() =>
87
+ props.item.label
88
+ .split(/\s+/)
89
+ .filter(Boolean)
90
+ .slice(0, 2)
91
+ .map((w) => [...w][0])
92
+ .join(""),
93
+ )
94
+
95
+ const hasBadge = computed(() => props.item.badge != null || props.item.dot)
96
+ const card = computed(() => !props.item.disabled && (props.compact || !!props.item.preview))
97
+ const delay = computed(() => (props.compact && !props.item.preview ? 250 : props.previewDelay))
98
+ </script>
99
+
100
+ <template>
101
+ <LpHoverCard
102
+ :disabled="!card"
103
+ side="right"
104
+ align="start"
105
+ :side-offset="compact ? 12 : 16"
106
+ :open-delay="delay"
107
+ :close-delay="120"
108
+ touch="none"
109
+ :panel-class="item.preview ? 'w-80 p-0 overflow-hidden' : 'w-56 p-2'"
110
+ >
111
+ <!-- The card anchors on a plain box: the context menu inside it renders a
112
+ fragment, which an as-child trigger cannot take. -->
113
+ <template #trigger>
114
+ <div class="relative flex flex-col">
115
+ <LpContextMenu :items="menu" :title="(item.menuTitle ?? item.label) || undefined" :title-icon="item.icon">
116
+ <component
117
+ :is="rowOf(item).is"
118
+ v-bind="rowOf(item).props"
119
+ :aria-current="active ? 'page' : undefined"
120
+ :aria-disabled="item.disabled || undefined"
121
+ :aria-label="compact ? item.label : undefined"
122
+ data-lp-nav-row
123
+ :aria-keyshortcuts="item.hotkey"
124
+ :aria-busy="item.busy ? true : undefined"
125
+ :data-lp-nav-nested="nested || undefined"
126
+ class="group/item relative flex items-center rounded-control font-medium outline-none transition-colors duration-[var(--duration-fast)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
127
+ :class="[
128
+ compact ? ['ml-1 size-10 justify-center', onPath && !active ? 'bg-ink/[0.06]' : ''] : nested ? 'gap-2.5 py-1.5 pl-9 pr-3 text-[13px]' : 'gap-3 px-3 py-2 text-sm',
129
+ item.disabled
130
+ ? 'cursor-not-allowed text-muted/50'
131
+ : active || hovered || onPath
132
+ ? 'text-ink'
133
+ : 'text-muted hover:text-ink',
134
+ ]"
135
+ @click="emit('activate', item)"
136
+ @pointerenter="emit('hover', item)"
137
+ >
138
+ <span
139
+ v-if="item.busy !== undefined && item.busy !== false"
140
+ class="pointer-events-none absolute bottom-0.5 z-10 h-0.5 overflow-hidden rounded-pill bg-brand/15"
141
+ :class="compact ? 'inset-x-2' : 'inset-x-3'"
142
+ >
143
+ <span
144
+ v-if="typeof item.busy === 'number'"
145
+ class="block h-full rounded-pill bg-brand transition-[width] duration-(--duration-medium) ease-[var(--ease-emphasized)]"
146
+ :style="{ width: `${Math.round(Math.min(1, Math.max(0, item.busy)) * 100)}%` }"
147
+ />
148
+ <span v-else class="lp-sweep absolute inset-y-0 left-0 w-2/5 rounded-pill bg-brand" />
149
+ </span>
150
+ <Motion v-if="pill" :layout-id="pillId" :transition="pillTransition" class="absolute inset-0 z-0 rounded-control bg-brand-soft" />
151
+ <slot :item="item" :active="active">
152
+ <span v-if="item.icon" class="relative z-10 shrink-0">
153
+ <LpIcon :name="item.icon" :size="nested ? 15 : 17" class="transition-colors" :class="active ? 'text-brand' : ''" />
154
+ <!-- Compact, the count has no room: a dot on the icon says there is one. -->
155
+ <span
156
+ v-if="compact && hasBadge"
157
+ class="absolute -right-1 -top-1 size-2 rounded-full bg-brand ring-2 ring-surface-raised"
158
+ :class="bumped ? 'animate-pulse' : ''"
159
+ />
160
+ </span>
161
+ <span v-else-if="compact" class="relative z-10 text-xs font-semibold uppercase">{{ initials }}</span>
162
+ <span v-else-if="nested" class="relative z-10 size-1 shrink-0 rounded-full bg-current opacity-50" />
163
+ <template v-if="!compact">
164
+ <span class="relative z-10 min-w-0 flex-1 truncate text-left">{{ item.label }}</span>
165
+ <!-- Over the row's end and only where nothing else sits there: in
166
+ the flow, keys that show on hover would take the label's
167
+ width even while hidden. -->
168
+ <LpKeys
169
+ v-if="item.hotkey && !hasBadge && !expandable"
170
+ :combo="item.hotkey"
171
+ size="sm"
172
+ aria-hidden="true"
173
+ class="pointer-events-none absolute right-2 z-10 rounded-small bg-surface-raised/90 opacity-0 transition-opacity duration-(--duration-fast) group-hover/item:opacity-100 group-focus-visible/item:opacity-100"
174
+ />
175
+ <span v-if="item.dot && item.badge == null" class="relative z-10 size-2 shrink-0 rounded-full bg-brand" />
176
+ <LpBadge v-if="item.badge != null" :tone="active ? 'brand' : 'neutral'" class="relative z-10 shrink-0">
177
+ <span v-if="bumped" class="absolute inset-0 animate-ping rounded-pill bg-brand/30" />
178
+ <LpNumberFlow v-if="typeof item.badge === 'number'" :value="item.badge" grouped :locale="locale" :animate-on-mount="false" />
179
+ <template v-else>{{ item.badge }}</template>
180
+ </LpBadge>
181
+ <span v-if="expandable" class="w-5 shrink-0" aria-hidden="true" />
182
+ </template>
183
+ </slot>
184
+ </component>
185
+ </LpContextMenu>
186
+ <!-- Beside the row, not inside it: a control within the link or button
187
+ would be one control to a screen reader, and the pages under a
188
+ link have to open without following it. -->
189
+ <button
190
+ v-if="expandable && !compact"
191
+ type="button"
192
+ class="absolute right-2 top-1/2 z-20 grid size-6 -translate-y-1/2 place-items-center rounded-small text-muted outline-none transition-colors hover:bg-ink/[0.06] hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
193
+ :aria-label="expandLabel"
194
+ :aria-expanded="expanded"
195
+ @click="emit('toggle')"
196
+ >
197
+ <LpIcon
198
+ name="lucide:chevron-right"
199
+ :size="14"
200
+ class="transition-transform duration-(--duration-fast) ease-[var(--ease-emphasized)] motion-reduce:transition-none"
201
+ :class="expanded ? 'rotate-90' : ''"
202
+ />
203
+ </button>
204
+ </div>
205
+ </template>
206
+
207
+ <div v-if="item.preview" class="flex flex-col">
208
+ <div v-if="compact" class="flex items-center gap-2 border-b border-line px-3 py-2 text-sm font-medium text-ink">
209
+ <span class="min-w-0 flex-1 truncate">{{ item.label }}</span>
210
+ <LpKeys v-if="item.hotkey" :combo="item.hotkey" size="sm" />
211
+ </div>
212
+ <component :is="item.preview" />
213
+ </div>
214
+ <div v-else class="flex flex-col gap-0.5">
215
+ <div class="flex items-center gap-2 px-2 py-1 text-sm font-medium text-ink">
216
+ <span class="min-w-0 flex-1 truncate">{{ item.label }}</span>
217
+ <LpBadge v-if="item.badge != null" tone="neutral">{{ typeof item.badge === "number" ? item.badge.toLocaleString(locale) : item.badge }}</LpBadge>
218
+ <LpKeys v-if="item.hotkey" :combo="item.hotkey" size="sm" />
219
+ </div>
220
+ <div v-if="item.children?.length" class="flex max-h-[min(70vh,30rem)] flex-col gap-0.5 overflow-y-auto">
221
+ <component
222
+ :is="rowOf(child).is"
223
+ v-for="child in item.children"
224
+ :key="child.id"
225
+ v-bind="rowOf(child).props"
226
+ :aria-current="itemActive?.(child) ? 'page' : undefined"
227
+ class="flex shrink-0 items-center gap-2 rounded-control px-2 py-1.5 text-left text-[13px] outline-none transition-colors hover:bg-ink/[0.05] focus-visible:ring-2 focus-visible:ring-ring"
228
+ :class="child.disabled ? 'opacity-50' : itemActive?.(child) ? 'text-brand' : 'text-muted hover:text-ink'"
229
+ @click="emit('activate', child)"
230
+ >
231
+ <LpIcon v-if="child.icon" :name="child.icon" :size="15" class="shrink-0" />
232
+ <span class="min-w-0 flex-1 truncate">{{ child.label }}</span>
233
+ <LpBadge v-if="child.badge != null" tone="neutral">{{ child.badge }}</LpBadge>
234
+ </component>
235
+ </div>
236
+ </div>
237
+ </LpHoverCard>
238
+ </template>
@@ -29,6 +29,11 @@ const props = withDefaults(
29
29
  /** Formatting for a numeric `value` — see LpNumberFlow. */
30
30
  compact?: boolean
31
31
  grouped?: boolean
32
+ /**
33
+ * Count up from zero when the tile first shows (the default), or show the
34
+ * value outright: a report or a screenshot wants the figure, not the run.
35
+ */
36
+ animateOnMount?: boolean
32
37
  decimals?: number
33
38
  prefix?: string
34
39
  valueSuffix?: string
@@ -54,7 +59,7 @@ const props = withDefaults(
54
59
  */
55
60
  loading?: boolean
56
61
  }>(),
57
- { trend: "auto", decimals: 0 },
62
+ { trend: "auto", decimals: 0, animateOnMount: true },
58
63
  )
59
64
 
60
65
  // Resolve the trend direction: explicit wins; "auto" reads a numeric delta.
@@ -159,6 +164,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
159
164
  :value="value"
160
165
  :compact="compact"
161
166
  :grouped="grouped"
167
+ :animate-on-mount="animateOnMount"
162
168
  :decimals="decimals"
163
169
  :prefix="prefix"
164
170
  :suffix="valueSuffix"