@leavepulse/ui 0.71.2 → 0.72.1
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.
- package/dist/assets/{LpKeys-BxRhfQda.css → LpKeys-C5VMcBOW.css} +10 -5
- package/dist/assets/LpTabs-CDchzoRL.css +18 -0
- package/dist/chunks/{ContextMenuItems-eS7qx9gD.js → ContextMenuItems-BXumicbh.js} +1 -1
- package/dist/chunks/{DropzoneLinkField-DkFhdfEJ.js → DropzoneLinkField-DpOB20Lx.js} +2 -2
- package/dist/chunks/{FieldWrapper-YzbvvC3M.js → FieldWrapper-DHhRxO5G.js} +1 -1
- package/dist/chunks/{LayoutCanvas-Ba4A9QC5.js → LayoutCanvas-CNOAUk7D.js} +1 -1
- package/dist/chunks/{LayoutNode-CTTTLMDM.js → LayoutNode-DF5BMN0J.js} +1 -1
- package/dist/chunks/{LpActionBar-B7anGlkI.js → LpActionBar-BqicINE_.js} +3 -3
- package/dist/chunks/{LpAddressInput-Bu6ws3Fp.js → LpAddressInput-CkbKLufb.js} +7 -7
- package/dist/chunks/{LpAlert-BHi7ARqu.js → LpAlert-BigMdAbu.js} +3 -3
- package/dist/chunks/{LpAppShell-C5YW5pI7.js → LpAppShell-DIMy-vjP.js} +68 -18
- package/dist/chunks/{LpAutocomplete-DDqe2M5g.js → LpAutocomplete-DOu7FN5P.js} +2 -2
- package/dist/chunks/{LpAvatar-DqfKCIdr.js → LpAvatar-Cg6Az3wz.js} +1 -1
- package/dist/chunks/{LpBadge-C8fKEmTF.js → LpBadge-Huo8EKew.js} +3 -3
- package/dist/chunks/LpBottomNav-BZIpahzA.js +89 -0
- package/dist/chunks/{LpBreadcrumbs-DhcoStFa.js → LpBreadcrumbs-DGtpBhWt.js} +3 -3
- package/dist/chunks/{LpButton-BQDAFXBd.js → LpButton-CW34A1e4.js} +1 -1
- package/dist/chunks/{LpCalendar-NmrA47J9.js → LpCalendar-CuoWks-0.js} +1 -1
- package/dist/chunks/{LpCard-CqKLnEhl.js → LpCard-B0sFXQzc.js} +1 -1
- package/dist/chunks/{LpCheckbox-BoOuLnGB.js → LpCheckbox-iaD63DEF.js} +2 -2
- package/dist/chunks/{LpCodeBlock-Czu-fq1M.js → LpCodeBlock-CFFa_pJb.js} +2 -2
- package/dist/chunks/{LpCodeDisplay-C9pIMF2R.js → LpCodeDisplay-CFmGMAmP.js} +6 -6
- package/dist/chunks/{LpColorPicker-Ee1DZiRd.js → LpColorPicker-1y1QJHZR.js} +56 -28
- package/dist/chunks/{LpConfirmDialog-i15pulMT.js → LpConfirmDialog-BDystNtm.js} +3 -3
- package/dist/chunks/{LpConfirmHost-D0oGavtd.js → LpConfirmHost-oOsxJHfQ.js} +1 -1
- package/dist/chunks/{LpContextMenu-DXpFJjlO.js → LpContextMenu-rx2tCjwk.js} +2 -2
- package/dist/chunks/{LpCopyButton-Dmq7DmFw.js → LpCopyButton-BNcGbVXF.js} +2 -2
- package/dist/chunks/{LpCountdown-CXrEK0ev.js → LpCountdown-Cak7cq3X.js} +3 -3
- package/dist/chunks/{LpDataList-CFPtSsBB.js → LpDataList-B6Meb4M9.js} +3 -3
- package/dist/chunks/{LpDatePicker-cC9K-Pk8.js → LpDatePicker-CA0QTfBC.js} +4 -4
- package/dist/chunks/{LpDateRangePicker-BVQKPbvo.js → LpDateRangePicker-u3J51xXk.js} +2 -2
- package/dist/chunks/{LpDisclosure-hRnX3FWL.js → LpDisclosure-Cpa7Hc5T.js} +1 -1
- package/dist/chunks/{LpDrawer-u4rxnuR-.js → LpDrawer-RRamxbLV.js} +1 -1
- package/dist/chunks/{LpDropdownMenu-BgZ0Z_Im.js → LpDropdownMenu-Dl7Z4ZhS.js} +2 -2
- package/dist/chunks/{LpEditable-CjxuOVh_.js → LpEditable-BNogo5DL.js} +5 -5
- package/dist/chunks/{LpEmptyState-CKFTtQgJ.js → LpEmptyState-EQHiRYhG.js} +7 -3
- package/dist/chunks/{LpFieldNotice-CPI8EwtU.js → LpFieldNotice-CzQ2_5rQ.js} +1 -1
- package/dist/chunks/{LpFileTree-BH0gNlSJ.js → LpFileTree-CHgtVh14.js} +2 -2
- package/dist/chunks/{LpFileTreeNode-gPVB1oXM.js → LpFileTreeNode-LqHgHd3B.js} +5 -5
- package/dist/chunks/{LpFormField-gRASfyj2.js → LpFormField-CC57DdTi.js} +2 -2
- package/dist/chunks/{LpGauge-DFdt33x8.js → LpGauge-BO1PYYqD.js} +2 -2
- package/dist/chunks/{LpHintLayer-C2asBIfs.js → LpHintLayer-CvAj4aa6.js} +2 -2
- package/dist/chunks/{LpHints-CNZQ76JV.js → LpHints-BtRvHQQ0.js} +3 -3
- package/dist/chunks/{LpIcon-DkP-9mzK.js → LpIcon--5WSb3jJ.js} +1 -1
- package/dist/chunks/{LpInfraNode-yTh4-5qa.js → LpInfraNode-CxngdQAG.js} +2 -2
- package/dist/chunks/{LpInput-2H-5ILOB.js → LpInput-DqmL-438.js} +2 -2
- package/dist/chunks/{LpLightbox-lAt5uIfx.js → LpLightbox-7YP6zSDm.js} +3 -3
- package/dist/chunks/{LpLoader-DjJEX8hL.js → LpLoader-CUo1MX_0.js} +1 -1
- package/dist/chunks/{LpLogViewer-x78YfhJf.js → LpLogViewer-DQOIsnZ1.js} +6 -6
- package/dist/chunks/{LpMapPicker-Dp5RSCi9.js → LpMapPicker-Dm3vJHN2.js} +2 -2
- package/dist/chunks/{LpMediaGallery-D92oJfY_.js → LpMediaGallery-znEbvIEo.js} +3 -3
- package/dist/chunks/{LpModal-CpH0bNo7.js → LpModal-BnjZ2iem.js} +1 -1
- package/dist/chunks/{LpModalNav-Cuna8c_3.js → LpModalNav-CH1UE9Ch.js} +4 -4
- package/dist/chunks/{LpNotificationBell-BdUQqZhs.js → LpNotificationBell-CrywodQ_.js} +8 -8
- package/dist/chunks/{LpNumberField-DZY3Z670.js → LpNumberField-e0IBkWU1.js} +1 -1
- package/dist/chunks/{LpNumberFlow-Dwb9MV1A.js → LpNumberFlow-CcxEGUy0.js} +4 -2
- package/dist/chunks/{LpPagination-DbeACavM.js → LpPagination-BIIiYHXR.js} +1 -1
- package/dist/chunks/{LpPaletteTheme-DkikZ362.js → LpPaletteTheme-BzHBh-mN.js} +4 -4
- package/dist/chunks/{LpPasswordInput-BzAZsf5V.js → LpPasswordInput-DVgRgTVx.js} +2 -2
- package/dist/chunks/{LpPhoneInput-BMpd-0QR.js → LpPhoneInput-CETEp5W7.js} +2 -2
- package/dist/chunks/{LpPie-C1uIUFxu.js → LpPie-BjDbeytx.js} +6 -6
- package/dist/chunks/{LpPipeline-zjEM1Lwc.js → LpPipeline-zZUj7I6K.js} +4 -4
- package/dist/chunks/{LpProgress-WNaH4emv.js → LpProgress-rOlyafqp.js} +1 -1
- package/dist/chunks/{LpQuota-Cz9PvklM.js → LpQuota-B12yijdB.js} +3 -3
- package/dist/chunks/{LpRadioGroup-CsXeV9Ct.js → LpRadioGroup-ggaPL92T.js} +1 -1
- package/dist/chunks/{LpRangeCalendar-l-0nSFmY.js → LpRangeCalendar-CYYUYqJ7.js} +1 -1
- package/dist/chunks/{LpRelativeTime-UM8EkfrN.js → LpRelativeTime-DwgCb5-8.js} +1 -1
- package/dist/chunks/{LpRepeater-OvzByE5L.js → LpRepeater-BQB0twJ5.js} +3 -3
- package/dist/chunks/LpRollingText-B54Pje7P.js +86 -0
- package/dist/chunks/{LpScramble-TDO5Kdou.js → LpScramble-DbQyBjUY.js} +1 -1
- package/dist/chunks/{LpSecretField-DwQiPOko.js → LpSecretField-uoD6Fngf.js} +7 -7
- package/dist/chunks/{LpSection-DcQsieHG.js → LpSection-DFK_uoJE.js} +1 -1
- package/dist/chunks/{LpSegmented-D2VST0o3.js → LpSegmented-CeACsMMA.js} +4 -2
- package/dist/chunks/{LpSelect-D3pRJfRY.js → LpSelect-Cxq4yBGd.js} +3 -3
- package/dist/chunks/LpShift-C-UGuKMN.js +171 -0
- package/dist/chunks/LpSidebar-DXdbvgM6.js +494 -0
- package/dist/chunks/LpSidebarNav-CE6CVCb0.js +367 -0
- package/dist/chunks/LpSidebarRow-CF9VFZ2w.js +304 -0
- package/dist/chunks/{LpStat-Dzn6WFzi.js → LpStat-CjpXrC8R.js} +6 -4
- package/dist/chunks/{LpStatus-BQU1TUsv.js → LpStatus-g-8z9b_u.js} +3 -3
- package/dist/chunks/{LpStepMark-CXa7DJy_.js → LpStepMark-DU7xalwW.js} +3 -3
- package/dist/chunks/{LpStepper-C9RM_yTG.js → LpStepper-CbRXiXNs.js} +1 -1
- package/dist/chunks/{LpSwap-DzE6M3Mg.js → LpSwap-Xi4j9PR_.js} +1 -1
- package/dist/chunks/{LpSwitch-bGZxUDSx.js → LpSwitch-BWcXEahf.js} +1 -1
- package/dist/chunks/LpTabFace-CG0VqD8I.js +77 -0
- package/dist/chunks/{LpTableFilter-CTZMqFDU.js → LpTableFilter-CYtK9GsF.js} +3 -3
- package/dist/chunks/{LpTagsInput-DrjmrOge.js → LpTagsInput-D1RzCwb5.js} +5 -5
- package/dist/chunks/{LpTextarea-BbYqmG1S.js → LpTextarea-MdIzNTjw.js} +32 -7
- package/dist/chunks/{useTheme-BOV_-AiY.js → LpThemeProvider-8MG6yqU9.js} +66 -1
- package/dist/chunks/{LpThemeStudio-IupHqP0x.js → LpThemeStudio-B0Kk1bX0.js} +138 -104
- package/dist/chunks/LpThemeStudioSection-B7bt8Iyg.js +32 -0
- package/dist/chunks/{LpThemeSwitcher-DarPHuX8.js → LpThemeSwitcher-CI4I7ygL.js} +6 -6
- package/dist/chunks/{LpTimeField-DQ8WvPr2.js → LpTimeField-BawSVsmt.js} +1 -1
- package/dist/chunks/{LpToaster-BTTl_wuy.js → LpToaster-C-P1nmMS.js} +2 -2
- package/dist/chunks/{LpTopologyCanvas-Cq-MiJwo.js → LpTopologyCanvas-CbEZhsP1.js} +1 -1
- package/dist/chunks/{LpTour-BgCfNlaW.js → LpTour-DRyzgpjt.js} +1 -1
- package/dist/chunks/{LpTruncate-TcqfkyJM.js → LpTruncate-4WvCWqnW.js} +1 -1
- package/dist/chunks/{LpUptimeBar-BbhVTPnL.js → LpUptimeBar-B2LNSNxs.js} +4 -4
- package/dist/chunks/{MenuTitle-Dab3EGOp.js → MenuTitle-BmCNNTKH.js} +1 -1
- package/dist/chunks/{OptionContent-DPzNsybd.js → OptionContent-22UT8zLn.js} +1 -1
- package/dist/chunks/{PickerField-DwLw7j0L.js → PickerField-Bfh8id3d.js} +2 -2
- package/dist/chunks/{RevealToggle-BVFM_FAU.js → RevealToggle-jBLJwVwO.js} +2 -2
- package/dist/chunks/{TourCard--2IfgnUU.js → TourCard-BOBcGbEo.js} +5 -5
- package/dist/chunks/{icons-CLu1iR1j.js → icons-DMITlfw5.js} +1 -1
- package/dist/component-names.d.ts +1 -1
- package/dist/components/ContextMenuItems.vue.js +1 -1
- package/dist/components/DropzoneLinkField.vue.js +1 -1
- package/dist/components/FieldWrapper.vue.js +1 -1
- package/dist/components/LayoutCanvas.vue.js +1 -1
- package/dist/components/LayoutNode.vue.js +1 -1
- package/dist/components/LpActionBar.vue.js +1 -1
- package/dist/components/LpAddressInput.vue.js +1 -1
- package/dist/components/LpAlert.vue.js +1 -1
- package/dist/components/LpAppShell.vue.d.ts +67 -2
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpAutocomplete.vue.js +1 -1
- package/dist/components/LpAvatar.vue.js +1 -1
- package/dist/components/LpBadge.vue.js +1 -1
- package/dist/components/LpBottomNav.vue.d.ts +28 -0
- package/dist/components/LpBottomNav.vue.js +4 -0
- package/dist/components/LpBreadcrumbs.vue.js +1 -1
- package/dist/components/LpButton.vue.js +1 -1
- package/dist/components/LpCalendar.vue.js +1 -1
- package/dist/components/LpCard.vue.js +1 -1
- package/dist/components/LpChart.vue.js +4 -4
- package/dist/components/LpCheckbox.vue.js +1 -1
- package/dist/components/LpCodeBlock.vue.js +1 -1
- package/dist/components/LpCodeDisplay.vue.js +1 -1
- package/dist/components/LpColorPicker.vue.d.ts +15 -2
- package/dist/components/LpColorPicker.vue.js +1 -1
- package/dist/components/LpCommandPalette.vue.js +4 -4
- package/dist/components/LpConfirmDialog.vue.js +1 -1
- package/dist/components/LpConfirmHost.vue.js +1 -1
- package/dist/components/LpContextMenu.vue.js +1 -1
- package/dist/components/LpCopyButton.vue.js +1 -1
- package/dist/components/LpCountdown.vue.js +1 -1
- package/dist/components/LpDataList.vue.js +1 -1
- package/dist/components/LpDatePicker.vue.js +1 -1
- package/dist/components/LpDateRangePicker.vue.js +1 -1
- package/dist/components/LpDisclosure.vue.js +1 -1
- package/dist/components/LpDock.vue.js +1 -1
- package/dist/components/LpDrawer.vue.js +1 -1
- package/dist/components/LpDropdownMenu.vue.js +1 -1
- package/dist/components/LpDropzone.vue.js +2 -2
- package/dist/components/LpEditable.vue.js +1 -1
- package/dist/components/LpEmptyState.vue.d.ts +2 -0
- package/dist/components/LpEmptyState.vue.js +1 -1
- package/dist/components/LpFieldNotice.vue.js +1 -1
- package/dist/components/LpFileTree.vue.js +1 -1
- package/dist/components/LpFileTreeNode.vue.js +1 -1
- package/dist/components/LpFormField.vue.js +1 -1
- package/dist/components/LpGauge.vue.js +1 -1
- package/dist/components/LpHintLayer.vue.js +1 -1
- package/dist/components/LpHints.vue.js +1 -1
- package/dist/components/LpIcon.vue.js +1 -1
- package/dist/components/LpImageEditor.vue.js +3 -3
- package/dist/components/LpInfraNode.vue.js +1 -1
- package/dist/components/LpInput.vue.js +1 -1
- package/dist/components/LpKeys.vue.js +13 -10
- package/dist/components/LpLightbox.vue.js +1 -1
- package/dist/components/LpLoader.vue.js +1 -1
- package/dist/components/LpLogViewer.vue.d.ts +1 -1
- package/dist/components/LpLogViewer.vue.js +1 -1
- package/dist/components/LpMapPicker.vue.js +1 -1
- package/dist/components/LpMediaGallery.vue.js +1 -1
- package/dist/components/LpModal.vue.js +1 -1
- package/dist/components/LpModalNav.vue.js +1 -1
- package/dist/components/LpNotificationBell.vue.js +1 -1
- package/dist/components/LpNumberField.vue.js +1 -1
- package/dist/components/LpNumberFlow.vue.js +1 -1
- package/dist/components/LpPagination.vue.js +1 -1
- package/dist/components/LpPaletteFacts.vue.js +2 -2
- package/dist/components/LpPaletteHeader.vue.js +2 -2
- package/dist/components/LpPaletteStats.vue.js +2 -2
- package/dist/components/LpPaletteTheme.vue.js +1 -1
- package/dist/components/LpPasswordInput.vue.js +1 -1
- package/dist/components/LpPhoneInput.vue.js +1 -1
- package/dist/components/LpPie.vue.js +1 -1
- package/dist/components/LpPipeline.vue.js +1 -1
- package/dist/components/LpProgress.vue.js +1 -1
- package/dist/components/LpQuota.vue.js +1 -1
- package/dist/components/LpRadioGroup.vue.js +1 -1
- package/dist/components/LpRangeCalendar.vue.js +1 -1
- package/dist/components/LpRelativeTime.vue.js +1 -1
- package/dist/components/LpRepeater.vue.js +1 -1
- package/dist/components/LpRollingText.vue.d.ts +2 -2
- package/dist/components/LpRollingText.vue.js +1 -1
- package/dist/components/LpScramble.vue.js +1 -1
- package/dist/components/LpSecretField.vue.js +1 -1
- package/dist/components/LpSection.vue.js +1 -1
- package/dist/components/LpSegmented.vue.js +1 -1
- package/dist/components/LpSelect.vue.js +1 -1
- package/dist/components/LpShift.vue.d.ts +20 -0
- package/dist/components/LpShift.vue.js +1 -1
- package/dist/components/LpShortcutsDialog.vue.js +1 -1
- package/dist/components/LpSidebar.vue.d.ts +74 -23
- package/dist/components/LpSidebar.vue.js +1 -1
- package/dist/components/LpSidebarNav.vue.d.ts +31 -3
- package/dist/components/LpSidebarNav.vue.js +1 -1
- package/dist/components/LpSidebarRow.vue.d.ts +49 -0
- package/dist/components/LpSidebarRow.vue.js +4 -0
- package/dist/components/LpStat.vue.d.ts +11 -0
- package/dist/components/LpStat.vue.js +1 -1
- package/dist/components/LpStatus.vue.js +1 -1
- package/dist/components/LpStepMark.vue.js +1 -1
- package/dist/components/LpStepper.vue.js +1 -1
- package/dist/components/LpSwap.vue.js +1 -1
- package/dist/components/LpSwitch.vue.js +1 -1
- package/dist/components/LpTabFace.vue.d.ts +14 -0
- package/dist/components/LpTabFace.vue.js +4 -0
- package/dist/components/LpTable.vue.js +11 -11
- package/dist/components/LpTableFilter.vue.js +1 -1
- package/dist/components/LpTabs.vue.d.ts +4 -4
- package/dist/components/LpTabs.vue.js +18 -72
- package/dist/components/LpTagsInput.vue.js +1 -1
- package/dist/components/LpTextarea.vue.d.ts +19 -1
- package/dist/components/LpTextarea.vue.js +1 -1
- package/dist/components/LpThemeProvider.vue.js +1 -1
- package/dist/components/LpThemeStudio.vue.d.ts +2 -2
- package/dist/components/LpThemeStudio.vue.js +1 -1
- package/dist/components/LpThemeStudioSection.vue.d.ts +4 -1
- package/dist/components/LpThemeStudioSection.vue.js +1 -1
- package/dist/components/LpThemeSwitcher.vue.js +1 -1
- package/dist/components/LpTimeField.vue.js +1 -1
- package/dist/components/LpToaster.vue.js +1 -1
- package/dist/components/LpTopologyCanvas.vue.js +1 -1
- package/dist/components/LpTour.vue.js +1 -1
- package/dist/components/LpTruncate.vue.js +1 -1
- package/dist/components/LpUptimeBar.vue.js +1 -1
- package/dist/components/MenuTitle.vue.js +1 -1
- package/dist/components/OptionContent.vue.js +1 -1
- package/dist/components/PickerField.vue.js +1 -1
- package/dist/components/RevealToggle.vue.js +1 -1
- package/dist/components/TourCard.vue.js +1 -1
- package/dist/components/menuWhen.js +1 -1
- package/dist/components/rolling.d.ts +5 -0
- package/dist/components/rolling.js +23 -0
- package/dist/components/sidebar.d.ts +38 -0
- package/dist/components/sidebar.js +6 -1
- package/dist/components/tabs.d.ts +3 -0
- package/dist/components/tabs.js +6 -0
- package/dist/icons-all.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +154 -153
- package/package.json +1 -1
- package/src/component-names.ts +1 -0
- package/src/components/LpAppShell.vue +75 -7
- package/src/components/LpBottomNav.vue +84 -0
- package/src/components/LpColorPicker.vue +32 -1
- package/src/components/LpEmptyState.vue +4 -1
- package/src/components/LpKeys.vue +10 -1
- package/src/components/LpNumberFlow.vue +4 -2
- package/src/components/LpRollingText.vue +27 -2
- package/src/components/LpSegmented.vue +3 -1
- package/src/components/LpShift.vue +189 -2
- package/src/components/LpSidebar.vue +304 -18
- package/src/components/LpSidebarNav.vue +211 -78
- package/src/components/LpSidebarRow.vue +242 -0
- package/src/components/LpStat.vue +7 -1
- package/src/components/LpTabFace.vue +52 -0
- package/src/components/LpTabs.vue +6 -41
- package/src/components/LpTextarea.vue +34 -4
- package/src/components/LpThemeStudio.vue +91 -55
- package/src/components/LpThemeStudioSection.vue +15 -7
- package/src/components/rolling.ts +28 -0
- package/src/components/sidebar.ts +41 -0
- package/src/components/tabs.ts +9 -0
- package/src/index.ts +1 -0
- package/src/kit-icons.ts +1 -1
- package/dist/assets/LpTabs-DT0XPTc2.css +0 -18
- package/dist/chunks/LpRollingText-DiL717LK.js +0 -51
- package/dist/chunks/LpShift-DXNsauQZ.js +0 -40
- package/dist/chunks/LpSidebar-Bk7b-K4l.js +0 -256
- package/dist/chunks/LpSidebarNav-BR2AW3KT.js +0 -216
- package/dist/chunks/LpThemeProvider-DcbfOxsU.js +0 -69
- 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"
|
|
25
|
-
import
|
|
26
|
-
import {
|
|
27
|
-
import { useLinkComponent } from "../composables/useLpConfig"
|
|
22
|
+
import LpSidebarRow from "./LpSidebarRow.vue"
|
|
23
|
+
import { waiting, type SidebarItem, type SidebarSection } from "./sidebar"
|
|
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
|
-
|
|
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,112 @@ function pillUnder(item: SidebarItem): boolean {
|
|
|
105
102
|
return hovered.value ? hovered.value === item.id : props.itemActive(item)
|
|
106
103
|
}
|
|
107
104
|
|
|
108
|
-
|
|
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
|
-
*
|
|
112
|
-
* and
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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 {
|
|
200
|
+
id: `${SECTION}${sectionKey(section)}`,
|
|
201
|
+
label: section.title ?? "",
|
|
202
|
+
icon,
|
|
203
|
+
children: section.items,
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function activate(item: SidebarItem) {
|
|
208
|
+
if (item.id.startsWith(SECTION)) return
|
|
209
|
+
if (item.children?.length && !item.to && !item.href) toggle(item)
|
|
210
|
+
emit("activate", item)
|
|
118
211
|
}
|
|
119
212
|
|
|
120
213
|
// The nav fills the remaining height with a fixed gap between groups; a short sidebar needs both under the consumer's control.
|
|
@@ -151,12 +244,51 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
|
151
244
|
<LpScrollArea
|
|
152
245
|
v-else
|
|
153
246
|
class="min-h-0 flex-1"
|
|
154
|
-
content-class="flex min-h-full flex-col gap-4"
|
|
247
|
+
:content-class="compact ? 'flex min-h-full flex-col gap-1' : 'flex min-h-full flex-col gap-4'"
|
|
155
248
|
@pointerleave="hovered = null"
|
|
156
249
|
>
|
|
250
|
+
<!-- `contents`: no box of its own, only somewhere for the rows' keys to bubble to. -->
|
|
251
|
+
<div class="contents" @keydown="onKey">
|
|
252
|
+
<!-- Pinned pages come first and carry no pill: the same page further down
|
|
253
|
+
already holds it, and one pill shared by two rows would jump between
|
|
254
|
+
them. -->
|
|
255
|
+
<div v-if="pinned.length" class="flex flex-col gap-0.5">
|
|
256
|
+
<p v-if="!compact" class="px-2 pb-1 pt-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted">
|
|
257
|
+
{{ labels.pinned }}
|
|
258
|
+
</p>
|
|
259
|
+
<LpSidebarRow
|
|
260
|
+
v-for="item in pinned"
|
|
261
|
+
:key="`pinned:${item.id}`"
|
|
262
|
+
:item="item"
|
|
263
|
+
:active="itemActive(item)"
|
|
264
|
+
:pill="false"
|
|
265
|
+
:pill-id="pillId"
|
|
266
|
+
:compact="compact"
|
|
267
|
+
:menu="menuOf(item)"
|
|
268
|
+
:preview-delay="previewDelay"
|
|
269
|
+
:item-active="itemActive"
|
|
270
|
+
@activate="activate"
|
|
271
|
+
/>
|
|
272
|
+
</div>
|
|
273
|
+
<template v-for="(section, si) in groups" :key="si">
|
|
274
|
+
<!-- Compact, a group that folds is one icon: its pages open in the card
|
|
275
|
+
beside it, as a page's own pages do. A hundred initials in a column
|
|
276
|
+
would be no nav at all. -->
|
|
277
|
+
<div v-if="compact && section.collapsible && section.title" class="flex flex-col gap-0.5">
|
|
278
|
+
<LpSidebarRow
|
|
279
|
+
:item="sectionItem(section)"
|
|
280
|
+
:active="false"
|
|
281
|
+
:pill="false"
|
|
282
|
+
:pill-id="pillId"
|
|
283
|
+
compact
|
|
284
|
+
:on-path="section.items.some((i) => itemActive(i) || onPath(i))"
|
|
285
|
+
:preview-delay="previewDelay"
|
|
286
|
+
:item-active="itemActive"
|
|
287
|
+
@activate="activate"
|
|
288
|
+
/>
|
|
289
|
+
</div>
|
|
157
290
|
<CollapsibleRoot
|
|
158
|
-
v-
|
|
159
|
-
:key="si"
|
|
291
|
+
v-else
|
|
160
292
|
as="div"
|
|
161
293
|
class="flex flex-col gap-0.5"
|
|
162
294
|
:open="isOpen(section)"
|
|
@@ -165,8 +297,10 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
|
165
297
|
>
|
|
166
298
|
<!-- A plain heading while the group is fixed; a control once it folds,
|
|
167
299
|
because a thing that reacts to a click must look like one. -->
|
|
300
|
+
<div v-if="compact && (si > 0 || pinned.length)" class="mx-3 mb-1 border-t border-line" />
|
|
168
301
|
<CollapsibleTrigger
|
|
169
|
-
v-if="section.title && section.collapsible"
|
|
302
|
+
v-else-if="!compact && section.title && section.collapsible"
|
|
303
|
+
data-lp-nav-row
|
|
170
304
|
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
305
|
>
|
|
172
306
|
<LpIcon
|
|
@@ -182,6 +316,11 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
|
182
316
|
It is always rendered and merely fades, never removed: taking it
|
|
183
317
|
out of the row changes the heading's height, and a heading that
|
|
184
318
|
resizes under the cursor makes its own click feel like a misfire. -->
|
|
319
|
+
<span
|
|
320
|
+
v-if="section.items.some(waiting)"
|
|
321
|
+
class="size-1.5 shrink-0 rounded-full bg-brand transition-opacity duration-[var(--duration-fast)] motion-reduce:transition-none"
|
|
322
|
+
:class="isOpen(section) ? 'opacity-0' : 'opacity-100'"
|
|
323
|
+
/>
|
|
185
324
|
<LpBadge
|
|
186
325
|
tone="neutral"
|
|
187
326
|
class="shrink-0 transition-opacity duration-[var(--duration-fast)] motion-reduce:transition-none"
|
|
@@ -197,7 +336,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
|
197
336
|
/>
|
|
198
337
|
</CollapsibleTrigger>
|
|
199
338
|
<p
|
|
200
|
-
v-else-if="section.title"
|
|
339
|
+
v-else-if="!compact && section.title"
|
|
201
340
|
class="px-2 pb-1 pt-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted"
|
|
202
341
|
>
|
|
203
342
|
{{ section.title }}
|
|
@@ -206,58 +345,52 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
|
206
345
|
<CollapsibleContent
|
|
207
346
|
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
347
|
>
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
:
|
|
221
|
-
|
|
222
|
-
:
|
|
223
|
-
:
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
: itemActive(item) || hovered === item.id
|
|
228
|
-
? 'text-ink'
|
|
229
|
-
: 'text-muted hover:text-ink'"
|
|
230
|
-
@click="emit('activate', item)"
|
|
231
|
-
@pointerenter="hover(item)"
|
|
348
|
+
<template v-for="item in section.items" :key="item.id">
|
|
349
|
+
<LpSidebarRow
|
|
350
|
+
:item="item"
|
|
351
|
+
:active="itemActive(item)"
|
|
352
|
+
:pill="pillUnder(item)"
|
|
353
|
+
:pill-id="pillId"
|
|
354
|
+
:compact="compact"
|
|
355
|
+
:menu="menuOf(item)"
|
|
356
|
+
:expandable="!compact && !!item.children?.length"
|
|
357
|
+
:expanded="expanded(item)"
|
|
358
|
+
:on-path="onPath(item)"
|
|
359
|
+
:preview-delay="previewDelay"
|
|
360
|
+
:hovered="hovered === item.id"
|
|
361
|
+
:expand-label="labels.expand"
|
|
362
|
+
:item-active="itemActive"
|
|
363
|
+
@activate="activate"
|
|
364
|
+
@hover="hover"
|
|
365
|
+
@toggle="toggle(item)"
|
|
232
366
|
>
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
:
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
:
|
|
245
|
-
:
|
|
246
|
-
|
|
247
|
-
:
|
|
367
|
+
<template v-if="$slots.item" #default="slotProps"><slot name="item" v-bind="slotProps" /></template>
|
|
368
|
+
</LpSidebarRow>
|
|
369
|
+
<CollapsibleRoot v-if="!compact && item.children?.length" :open="expanded(item)">
|
|
370
|
+
<CollapsibleContent
|
|
371
|
+
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"
|
|
372
|
+
>
|
|
373
|
+
<LpSidebarRow
|
|
374
|
+
v-for="child in item.children"
|
|
375
|
+
:key="child.id"
|
|
376
|
+
nested
|
|
377
|
+
:item="child"
|
|
378
|
+
:active="itemActive(child)"
|
|
379
|
+
:pill="pillUnder(child)"
|
|
380
|
+
:pill-id="pillId"
|
|
381
|
+
:menu="menuOf(child)"
|
|
382
|
+
:preview-delay="previewDelay"
|
|
383
|
+
:hovered="hovered === child.id"
|
|
384
|
+
:item-active="itemActive"
|
|
385
|
+
@activate="activate"
|
|
386
|
+
@hover="hover"
|
|
248
387
|
/>
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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>
|
|
388
|
+
</CollapsibleContent>
|
|
389
|
+
</CollapsibleRoot>
|
|
390
|
+
</template>
|
|
260
391
|
</CollapsibleContent>
|
|
261
392
|
</CollapsibleRoot>
|
|
393
|
+
</template>
|
|
394
|
+
</div>
|
|
262
395
|
</LpScrollArea>
|
|
263
396
|
</template>
|
|
@@ -0,0 +1,242 @@
|
|
|
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 { waiting, 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
|
+
// Something waiting on a page under this row shows on the row while those pages
|
|
96
|
+
// are out of sight, folded away or behind a compact icon: otherwise closing a
|
|
97
|
+
// group would hide that anything is waiting in it.
|
|
98
|
+
const hidden = computed(() => (props.compact || !props.expanded) && !!props.item.children?.some(waiting))
|
|
99
|
+
const hasBadge = computed(() => props.item.badge != null || !!props.item.dot || hidden.value)
|
|
100
|
+
const card = computed(() => !props.item.disabled && (props.compact || !!props.item.preview))
|
|
101
|
+
const delay = computed(() => (props.compact && !props.item.preview ? 250 : props.previewDelay))
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
<template>
|
|
105
|
+
<LpHoverCard
|
|
106
|
+
:disabled="!card"
|
|
107
|
+
side="right"
|
|
108
|
+
align="start"
|
|
109
|
+
:side-offset="compact ? 12 : 16"
|
|
110
|
+
:open-delay="delay"
|
|
111
|
+
:close-delay="120"
|
|
112
|
+
touch="none"
|
|
113
|
+
:panel-class="item.preview ? 'w-80 p-0 overflow-hidden' : 'w-56 p-2'"
|
|
114
|
+
>
|
|
115
|
+
<!-- The card anchors on a plain box: the context menu inside it renders a
|
|
116
|
+
fragment, which an as-child trigger cannot take. -->
|
|
117
|
+
<template #trigger>
|
|
118
|
+
<div class="relative flex flex-col">
|
|
119
|
+
<LpContextMenu :items="menu" :title="(item.menuTitle ?? item.label) || undefined" :title-icon="item.icon">
|
|
120
|
+
<component
|
|
121
|
+
:is="rowOf(item).is"
|
|
122
|
+
v-bind="rowOf(item).props"
|
|
123
|
+
:aria-current="active ? 'page' : undefined"
|
|
124
|
+
:aria-disabled="item.disabled || undefined"
|
|
125
|
+
:aria-label="compact ? item.label : undefined"
|
|
126
|
+
data-lp-nav-row
|
|
127
|
+
:aria-keyshortcuts="item.hotkey"
|
|
128
|
+
:aria-busy="item.busy ? true : undefined"
|
|
129
|
+
:data-lp-nav-nested="nested || undefined"
|
|
130
|
+
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"
|
|
131
|
+
:class="[
|
|
132
|
+
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',
|
|
133
|
+
item.disabled
|
|
134
|
+
? 'cursor-not-allowed text-muted/50'
|
|
135
|
+
: active || hovered || onPath
|
|
136
|
+
? 'text-ink'
|
|
137
|
+
: 'text-muted hover:text-ink',
|
|
138
|
+
]"
|
|
139
|
+
@click="emit('activate', item)"
|
|
140
|
+
@pointerenter="emit('hover', item)"
|
|
141
|
+
>
|
|
142
|
+
<span
|
|
143
|
+
v-if="item.busy !== undefined && item.busy !== false"
|
|
144
|
+
class="pointer-events-none absolute bottom-0.5 z-10 h-0.5 overflow-hidden rounded-pill bg-brand/15"
|
|
145
|
+
:class="compact ? 'inset-x-2' : 'inset-x-3'"
|
|
146
|
+
>
|
|
147
|
+
<span
|
|
148
|
+
v-if="typeof item.busy === 'number'"
|
|
149
|
+
class="block h-full rounded-pill bg-brand transition-[width] duration-(--duration-medium) ease-[var(--ease-emphasized)]"
|
|
150
|
+
:style="{ width: `${Math.round(Math.min(1, Math.max(0, item.busy)) * 100)}%` }"
|
|
151
|
+
/>
|
|
152
|
+
<span v-else class="lp-sweep absolute inset-y-0 left-0 w-2/5 rounded-pill bg-brand" />
|
|
153
|
+
</span>
|
|
154
|
+
<Motion v-if="pill" :layout-id="pillId" :transition="pillTransition" class="absolute inset-0 z-0 rounded-control bg-brand-soft" />
|
|
155
|
+
<slot :item="item" :active="active">
|
|
156
|
+
<span v-if="item.icon" class="relative z-10 shrink-0">
|
|
157
|
+
<LpIcon :name="item.icon" :size="nested ? 15 : 17" class="transition-colors" :class="active ? 'text-brand' : ''" />
|
|
158
|
+
<!-- Compact, the count has no room: a dot on the icon says there is one. -->
|
|
159
|
+
<span
|
|
160
|
+
v-if="compact && hasBadge"
|
|
161
|
+
class="absolute -right-1 -top-1 size-2 rounded-full bg-brand ring-2 ring-surface-raised"
|
|
162
|
+
:class="bumped ? 'animate-pulse' : ''"
|
|
163
|
+
/>
|
|
164
|
+
</span>
|
|
165
|
+
<span v-else-if="compact" class="relative z-10 text-xs font-semibold uppercase">{{ initials }}</span>
|
|
166
|
+
<span v-else-if="nested" class="relative z-10 size-1 shrink-0 rounded-full bg-current opacity-50" />
|
|
167
|
+
<template v-if="!compact">
|
|
168
|
+
<span class="relative z-10 min-w-0 flex-1 truncate text-left">{{ item.label }}</span>
|
|
169
|
+
<!-- Over the row's end and only where nothing else sits there: in
|
|
170
|
+
the flow, keys that show on hover would take the label's
|
|
171
|
+
width even while hidden. -->
|
|
172
|
+
<LpKeys
|
|
173
|
+
v-if="item.hotkey && !hasBadge && !expandable"
|
|
174
|
+
:combo="item.hotkey"
|
|
175
|
+
size="sm"
|
|
176
|
+
aria-hidden="true"
|
|
177
|
+
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"
|
|
178
|
+
/>
|
|
179
|
+
<span v-if="hasBadge && item.badge == null" class="relative z-10 size-2 shrink-0 rounded-full bg-brand" />
|
|
180
|
+
<LpBadge v-if="item.badge != null" :tone="active ? 'brand' : 'neutral'" class="relative z-10 shrink-0">
|
|
181
|
+
<span v-if="bumped" class="absolute inset-0 animate-ping rounded-pill bg-brand/30" />
|
|
182
|
+
<LpNumberFlow v-if="typeof item.badge === 'number'" :value="item.badge" grouped :locale="locale" :animate-on-mount="false" />
|
|
183
|
+
<template v-else>{{ item.badge }}</template>
|
|
184
|
+
</LpBadge>
|
|
185
|
+
<span v-if="expandable" class="w-5 shrink-0" aria-hidden="true" />
|
|
186
|
+
</template>
|
|
187
|
+
</slot>
|
|
188
|
+
</component>
|
|
189
|
+
</LpContextMenu>
|
|
190
|
+
<!-- Beside the row, not inside it: a control within the link or button
|
|
191
|
+
would be one control to a screen reader, and the pages under a
|
|
192
|
+
link have to open without following it. -->
|
|
193
|
+
<button
|
|
194
|
+
v-if="expandable && !compact"
|
|
195
|
+
type="button"
|
|
196
|
+
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"
|
|
197
|
+
:aria-label="expandLabel"
|
|
198
|
+
:aria-expanded="expanded"
|
|
199
|
+
@click="emit('toggle')"
|
|
200
|
+
>
|
|
201
|
+
<LpIcon
|
|
202
|
+
name="lucide:chevron-right"
|
|
203
|
+
:size="14"
|
|
204
|
+
class="transition-transform duration-(--duration-fast) ease-[var(--ease-emphasized)] motion-reduce:transition-none"
|
|
205
|
+
:class="expanded ? 'rotate-90' : ''"
|
|
206
|
+
/>
|
|
207
|
+
</button>
|
|
208
|
+
</div>
|
|
209
|
+
</template>
|
|
210
|
+
|
|
211
|
+
<div v-if="item.preview" class="flex flex-col">
|
|
212
|
+
<div v-if="compact" class="flex items-center gap-2 border-b border-line px-3 py-2 text-sm font-medium text-ink">
|
|
213
|
+
<span class="min-w-0 flex-1 truncate">{{ item.label }}</span>
|
|
214
|
+
<LpKeys v-if="item.hotkey" :combo="item.hotkey" size="sm" />
|
|
215
|
+
</div>
|
|
216
|
+
<component :is="item.preview" />
|
|
217
|
+
</div>
|
|
218
|
+
<div v-else class="flex flex-col gap-0.5">
|
|
219
|
+
<div class="flex items-center gap-2 px-2 py-1 text-sm font-medium text-ink">
|
|
220
|
+
<span class="min-w-0 flex-1 truncate">{{ item.label }}</span>
|
|
221
|
+
<LpBadge v-if="item.badge != null" tone="neutral">{{ typeof item.badge === "number" ? item.badge.toLocaleString(locale) : item.badge }}</LpBadge>
|
|
222
|
+
<LpKeys v-if="item.hotkey" :combo="item.hotkey" size="sm" />
|
|
223
|
+
</div>
|
|
224
|
+
<div v-if="item.children?.length" class="flex max-h-[min(70vh,30rem)] flex-col gap-0.5 overflow-y-auto">
|
|
225
|
+
<component
|
|
226
|
+
:is="rowOf(child).is"
|
|
227
|
+
v-for="child in item.children"
|
|
228
|
+
:key="child.id"
|
|
229
|
+
v-bind="rowOf(child).props"
|
|
230
|
+
:aria-current="itemActive?.(child) ? 'page' : undefined"
|
|
231
|
+
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"
|
|
232
|
+
:class="child.disabled ? 'opacity-50' : itemActive?.(child) ? 'text-brand' : 'text-muted hover:text-ink'"
|
|
233
|
+
@click="emit('activate', child)"
|
|
234
|
+
>
|
|
235
|
+
<LpIcon v-if="child.icon" :name="child.icon" :size="15" class="shrink-0" />
|
|
236
|
+
<span class="min-w-0 flex-1 truncate">{{ child.label }}</span>
|
|
237
|
+
<LpBadge v-if="child.badge != null" tone="neutral">{{ child.badge }}</LpBadge>
|
|
238
|
+
</component>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
</LpHoverCard>
|
|
242
|
+
</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"
|