@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.
- 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-D2Rnkcys.js} +2 -2
- package/dist/chunks/{FieldWrapper-YzbvvC3M.js → FieldWrapper-DnA1rxZ8.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-DfiBL1dE.js} +3 -3
- package/dist/chunks/{LpAddressInput-Bu6ws3Fp.js → LpAddressInput-B1bB3F6M.js} +7 -7
- package/dist/chunks/{LpAlert-BHi7ARqu.js → LpAlert-CpRF9eYr.js} +3 -3
- package/dist/chunks/{LpAppShell-C5YW5pI7.js → LpAppShell-D8Q0Jv9d.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-vtCpsDmv.js} +3 -3
- package/dist/chunks/LpBottomNav-VXJf0cKc.js +89 -0
- package/dist/chunks/{LpBreadcrumbs-DhcoStFa.js → LpBreadcrumbs-BdzjQcql.js} +3 -3
- package/dist/chunks/{LpButton-BQDAFXBd.js → LpButton-Dk7eT4ro.js} +1 -1
- package/dist/chunks/{LpCalendar-NmrA47J9.js → LpCalendar-CuoWks-0.js} +1 -1
- package/dist/chunks/{LpCard-CqKLnEhl.js → LpCard-Ce3rw8zm.js} +1 -1
- package/dist/chunks/{LpCheckbox-BoOuLnGB.js → LpCheckbox-DLX4amba.js} +2 -2
- package/dist/chunks/{LpCodeBlock-Czu-fq1M.js → LpCodeBlock-j0W5bpmF.js} +2 -2
- package/dist/chunks/{LpCodeDisplay-C9pIMF2R.js → LpCodeDisplay-COdiv4xu.js} +6 -6
- package/dist/chunks/{LpColorPicker-Ee1DZiRd.js → LpColorPicker-1y1QJHZR.js} +56 -28
- package/dist/chunks/{LpConfirmDialog-i15pulMT.js → LpConfirmDialog-w1ni5nxh.js} +3 -3
- package/dist/chunks/{LpConfirmHost-D0oGavtd.js → LpConfirmHost-Br_-Gn-6.js} +1 -1
- package/dist/chunks/{LpContextMenu-DXpFJjlO.js → LpContextMenu-rx2tCjwk.js} +2 -2
- package/dist/chunks/{LpCopyButton-Dmq7DmFw.js → LpCopyButton-DaD1KCn6.js} +2 -2
- package/dist/chunks/{LpCountdown-CXrEK0ev.js → LpCountdown-Cgy0mVEq.js} +3 -3
- package/dist/chunks/{LpDataList-CFPtSsBB.js → LpDataList-CkcGFJgv.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-D3PvD6Zm.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-D7KTaJiM.js} +2 -2
- package/dist/chunks/{LpFileTreeNode-gPVB1oXM.js → LpFileTreeNode-14GVqQTd.js} +5 -5
- package/dist/chunks/{LpFormField-gRASfyj2.js → LpFormField-MSQhRCTz.js} +2 -2
- package/dist/chunks/{LpGauge-DFdt33x8.js → LpGauge-BO1PYYqD.js} +2 -2
- package/dist/chunks/{LpHintLayer-C2asBIfs.js → LpHintLayer-CmYm37z7.js} +2 -2
- package/dist/chunks/{LpHints-CNZQ76JV.js → LpHints-iLIRUkCj.js} +3 -3
- package/dist/chunks/{LpIcon-DkP-9mzK.js → LpIcon--5WSb3jJ.js} +1 -1
- package/dist/chunks/{LpInfraNode-yTh4-5qa.js → LpInfraNode-BT0VQrOa.js} +2 -2
- package/dist/chunks/{LpInput-2H-5ILOB.js → LpInput-1eNvtzXs.js} +2 -2
- package/dist/chunks/{LpLightbox-lAt5uIfx.js → LpLightbox-DWw6OSA4.js} +3 -3
- package/dist/chunks/{LpLoader-DjJEX8hL.js → LpLoader-B82_CcV0.js} +1 -1
- package/dist/chunks/{LpLogViewer-x78YfhJf.js → LpLogViewer-pzanZASC.js} +6 -6
- package/dist/chunks/{LpMapPicker-Dp5RSCi9.js → LpMapPicker-DNIUvF6V.js} +2 -2
- package/dist/chunks/{LpMediaGallery-D92oJfY_.js → LpMediaGallery-Dx8xzg5N.js} +3 -3
- package/dist/chunks/{LpModal-CpH0bNo7.js → LpModal-BnjZ2iem.js} +1 -1
- package/dist/chunks/{LpModalNav-Cuna8c_3.js → LpModalNav-Cu04W1pV.js} +4 -4
- package/dist/chunks/{LpNotificationBell-BdUQqZhs.js → LpNotificationBell-PSQqFqEv.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-D05ywM1W.js} +4 -4
- package/dist/chunks/{LpPasswordInput-BzAZsf5V.js → LpPasswordInput-vdcw-koG.js} +2 -2
- package/dist/chunks/{LpPhoneInput-BMpd-0QR.js → LpPhoneInput-CETEp5W7.js} +2 -2
- package/dist/chunks/{LpPie-C1uIUFxu.js → LpPie-BlWDxCtG.js} +6 -6
- package/dist/chunks/{LpPipeline-zjEM1Lwc.js → LpPipeline-BXjoxkq_.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-fliVBcN7.js} +3 -3
- package/dist/chunks/LpRollingText-B54Pje7P.js +86 -0
- package/dist/chunks/{LpScramble-TDO5Kdou.js → LpScramble-Cw-af0Yh.js} +1 -1
- package/dist/chunks/{LpSecretField-DwQiPOko.js → LpSecretField-D_IMxEGW.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-BAp8lGoa.js +160 -0
- package/dist/chunks/LpSidebar-BftYsk5d.js +494 -0
- package/dist/chunks/LpSidebarNav-BzNWMZfc.js +357 -0
- package/dist/chunks/LpSidebarRow-BIReMT1f.js +302 -0
- package/dist/chunks/{LpStat-Dzn6WFzi.js → LpStat-BFCM6fUn.js} +6 -4
- package/dist/chunks/{LpStatus-BQU1TUsv.js → LpStatus-BWTLVPVh.js} +3 -3
- package/dist/chunks/{LpStepMark-CXa7DJy_.js → LpStepMark-DkdkebLC.js} +3 -3
- package/dist/chunks/{LpStepper-C9RM_yTG.js → LpStepper-BgqjKrqg.js} +1 -1
- package/dist/chunks/{LpSwap-DzE6M3Mg.js → LpSwap-B5YOXgtx.js} +1 -1
- package/dist/chunks/{LpSwitch-bGZxUDSx.js → LpSwitch-DyrRes7s.js} +1 -1
- package/dist/chunks/LpTabFace-D6BMCbAP.js +77 -0
- package/dist/chunks/{LpTableFilter-CTZMqFDU.js → LpTableFilter-Dkzamzkl.js} +3 -3
- package/dist/chunks/{LpTagsInput-DrjmrOge.js → LpTagsInput-DAZNNehI.js} +5 -5
- package/dist/chunks/{LpTextarea-BbYqmG1S.js → LpTextarea-DbmVPj2X.js} +32 -7
- package/dist/chunks/{useTheme-BOV_-AiY.js → LpThemeProvider-8MG6yqU9.js} +66 -1
- package/dist/chunks/{LpThemeStudio-IupHqP0x.js → LpThemeStudio-DWtG6OZZ.js} +138 -104
- package/dist/chunks/LpThemeStudioSection-B7bt8Iyg.js +32 -0
- package/dist/chunks/{LpThemeSwitcher-DarPHuX8.js → LpThemeSwitcher-Dcx-VkSk.js} +6 -6
- package/dist/chunks/{LpTimeField-DQ8WvPr2.js → LpTimeField-BawSVsmt.js} +1 -1
- package/dist/chunks/{LpToaster-BTTl_wuy.js → LpToaster-50hp_B5O.js} +2 -2
- package/dist/chunks/{LpTopologyCanvas-Cq-MiJwo.js → LpTopologyCanvas-CnUh8uzS.js} +1 -1
- package/dist/chunks/{LpTour-BgCfNlaW.js → LpTour-DC3LTlxg.js} +1 -1
- package/dist/chunks/{LpTruncate-TcqfkyJM.js → LpTruncate-Ced4gquB.js} +1 -1
- package/dist/chunks/{LpUptimeBar-BbhVTPnL.js → LpUptimeBar-CuB0LX8K.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-B0ITuIX-.js} +2 -2
- package/dist/chunks/{TourCard--2IfgnUU.js → TourCard-CBkj1GYi.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 +2 -2
- package/dist/components/LpInfraNode.vue.js +1 -1
- package/dist/components/LpInput.vue.js +1 -1
- 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 +30 -2
- 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 +33 -0
- 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/LpNumberFlow.vue +4 -2
- package/src/components/LpRollingText.vue +27 -2
- package/src/components/LpSegmented.vue +3 -1
- package/src/components/LpShift.vue +174 -2
- package/src/components/LpSidebar.vue +304 -18
- package/src/components/LpSidebarNav.vue +200 -77
- package/src/components/LpSidebarRow.vue +238 -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 +33 -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
|
@@ -33,6 +33,7 @@ export default { inheritAttrs: false }
|
|
|
33
33
|
*/
|
|
34
34
|
import { computed, ref, useId, watch } from "vue"
|
|
35
35
|
import { useLabels } from "../composables/useLpConfig"
|
|
36
|
+
import LpBottomNav from "./LpBottomNav.vue"
|
|
36
37
|
import LpButton from "./LpButton.vue"
|
|
37
38
|
import LpIcon from "./LpIcon.vue"
|
|
38
39
|
import LpScrollArea from "./LpScrollArea.vue"
|
|
@@ -82,11 +83,37 @@ const props = withDefaults(
|
|
|
82
83
|
loading?: boolean
|
|
83
84
|
/** Text overrides: the burger's name and the skip link. */
|
|
84
85
|
labels?: Partial<{ openMenu: string; skipToContent: string }>
|
|
86
|
+
/** Icon-only rail (v-model:compact). Forwarded to LpSidebar. */
|
|
87
|
+
compact?: boolean
|
|
88
|
+
/** The person sizes the rail and folds it to icons. Forwarded to LpSidebar. */
|
|
89
|
+
resizable?: boolean
|
|
90
|
+
/** Rows can be pinned to the top (v-model:pinned). Forwarded to LpSidebar. */
|
|
91
|
+
pinnable?: boolean
|
|
92
|
+
pinned?: string[]
|
|
93
|
+
/** Remember folded groups, width, compact and pins under this key. Forwarded to LpSidebar. */
|
|
94
|
+
persist?: string
|
|
95
|
+
/** Rest time before a row's `preview` card opens. Forwarded to LpSidebar. */
|
|
96
|
+
previewDelay?: number
|
|
97
|
+
/** The key that folds the rail; false turns it off. Forwarded to LpSidebar. */
|
|
98
|
+
compactHotkey?: string | false
|
|
99
|
+
/** Marks a non-production copy along the top of the rail. Forwarded to LpSidebar. */
|
|
100
|
+
environment?: string | { label: string; tone?: "warning" | "danger" | "action" | "brand" }
|
|
101
|
+
/** The app's hint system; rows with a `hintPage` show unread news as a dot. Forwarded to LpSidebar. */
|
|
102
|
+
hints?: { unreadNews: (page?: string) => readonly unknown[] }
|
|
103
|
+
/**
|
|
104
|
+
* On phones, the main pages along the bottom instead of the burger, with
|
|
105
|
+
* "More" for the rest (it opens the drawer). `true` takes the first four
|
|
106
|
+
* pages, a number that many, a list of ids those pages.
|
|
107
|
+
*/
|
|
108
|
+
bottomNav?: boolean | number | string[]
|
|
85
109
|
}>(),
|
|
86
110
|
{
|
|
87
111
|
mobileBreakpoint: "lg",
|
|
88
112
|
divider: true,
|
|
89
113
|
contentClass: "mx-auto w-full max-w-6xl",
|
|
114
|
+
// Unset, not false: an absent boolean would hand the sidebar "expanded"
|
|
115
|
+
// and override the state it remembers.
|
|
116
|
+
compact: undefined,
|
|
90
117
|
},
|
|
91
118
|
)
|
|
92
119
|
|
|
@@ -94,11 +121,13 @@ const emit = defineEmits<{
|
|
|
94
121
|
(e: "update:modelValue", id: string): void
|
|
95
122
|
(e: "update:collapsed", keys: string[]): void
|
|
96
123
|
(e: "select", item: SidebarItem): void
|
|
124
|
+
(e: "update:compact", value: boolean): void
|
|
125
|
+
(e: "update:pinned", ids: string[]): void
|
|
97
126
|
}>()
|
|
98
127
|
|
|
99
128
|
// Normalise items/sections to a flat list for the title computation.
|
|
100
129
|
const flatItems = computed<SidebarItem[]>(() =>
|
|
101
|
-
props.sections ? props.sections.flatMap((s) => s.items) : (props.items ?? []),
|
|
130
|
+
(props.sections ? props.sections.flatMap((s) => s.items) : (props.items ?? [])).flatMap((i) => [i, ...(i.children ?? [])]),
|
|
102
131
|
)
|
|
103
132
|
|
|
104
133
|
// Default active test: exact match for "/", prefix match (on a segment
|
|
@@ -179,7 +208,23 @@ const burgerHidden: Record<NonNullable<typeof props.mobileBreakpoint>, string> =
|
|
|
179
208
|
lg: "lg:hidden",
|
|
180
209
|
xl: "xl:hidden",
|
|
181
210
|
}
|
|
182
|
-
const burgerClass = computed(() => burgerHidden[props.mobileBreakpoint])
|
|
211
|
+
const burgerClass = computed(() => (props.bottomNav ? "hidden" : burgerHidden[props.mobileBreakpoint]))
|
|
212
|
+
|
|
213
|
+
const topItems = computed<SidebarItem[]>(() => (props.sections ? props.sections.flatMap((s) => s.items) : (props.items ?? [])))
|
|
214
|
+
const bottomItems = computed<SidebarItem[]>(() => {
|
|
215
|
+
const want = props.bottomNav
|
|
216
|
+
if (!want) return []
|
|
217
|
+
if (Array.isArray(want)) return want.map((id) => topItems.value.find((i) => i.id === id)).filter((i): i is SidebarItem => !!i)
|
|
218
|
+
return topItems.value.slice(0, want === true ? 4 : want)
|
|
219
|
+
})
|
|
220
|
+
const moreActive = computed(() => {
|
|
221
|
+
const shown = new Set(bottomItems.value.map((i) => i.id))
|
|
222
|
+
return flatItems.value.some((i) => !shown.has(i.id) && activeTest.value(i)) && !bottomItems.value.some((i) => activeTest.value(i))
|
|
223
|
+
})
|
|
224
|
+
function onBottom(item: SidebarItem) {
|
|
225
|
+
onModel(item.id)
|
|
226
|
+
onSelect(item)
|
|
227
|
+
}
|
|
183
228
|
|
|
184
229
|
// Expose the burger trigger so a host can omit its own header entirely.
|
|
185
230
|
const _openDrawer = () => (drawerOpen.value = true)
|
|
@@ -215,20 +260,31 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
|
215
260
|
:loading="loading"
|
|
216
261
|
:collapsed="collapsed"
|
|
217
262
|
:auto-collapse-after="autoCollapseAfter"
|
|
263
|
+
:compact="compact"
|
|
264
|
+
:resizable="resizable"
|
|
265
|
+
:pinnable="pinnable"
|
|
266
|
+
:pinned="pinned"
|
|
267
|
+
:persist="persist"
|
|
268
|
+
:preview-delay="previewDelay"
|
|
269
|
+
:compact-hotkey="compactHotkey"
|
|
270
|
+
:environment="environment"
|
|
271
|
+
:hints="hints"
|
|
218
272
|
class="shrink-0"
|
|
219
273
|
@update:model-value="onModel"
|
|
274
|
+
@update:compact="(v) => emit('update:compact', v)"
|
|
275
|
+
@update:pinned="(v) => emit('update:pinned', v)"
|
|
220
276
|
@update:open="(v) => (drawerOpen = v)"
|
|
221
277
|
@update:collapsed="(v) => emit('update:collapsed', v)"
|
|
222
278
|
@select="onSelect"
|
|
223
279
|
>
|
|
224
|
-
<template v-if="$slots.logo" #header>
|
|
225
|
-
<slot name="logo" />
|
|
280
|
+
<template v-if="$slots.logo" #header="{ compact: folded }">
|
|
281
|
+
<slot name="logo" :compact="folded" />
|
|
226
282
|
</template>
|
|
227
283
|
<template v-if="$slots['sidebar-actions']" #actions>
|
|
228
284
|
<slot name="sidebar-actions" />
|
|
229
285
|
</template>
|
|
230
|
-
<template v-if="$slots['sidebar-footer']" #footer>
|
|
231
|
-
<slot name="sidebar-footer" />
|
|
286
|
+
<template v-if="$slots['sidebar-footer']" #footer="{ compact: folded }">
|
|
287
|
+
<slot name="sidebar-footer" :compact="folded" />
|
|
232
288
|
</template>
|
|
233
289
|
<template v-if="$slots['sidebar-item']" #item="slotProps">
|
|
234
290
|
<slot name="sidebar-item" v-bind="slotProps" />
|
|
@@ -261,7 +317,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
|
261
317
|
translation that wraps a long title onto a second line, otherwise
|
|
262
318
|
re-lays the whole bar in one frame. Width is left free so the
|
|
263
319
|
truncation still resolves against the space actually available. -->
|
|
264
|
-
<LpShift axis="height" class="flex min-w-0 flex-1 items-center">
|
|
320
|
+
<LpShift axis="height" presence class="flex min-w-0 flex-1 items-center">
|
|
265
321
|
<slot name="header-title">
|
|
266
322
|
<h1 class="truncate text-lg font-semibold">{{ pageTitle }}</h1>
|
|
267
323
|
</slot>
|
|
@@ -283,6 +339,8 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
|
283
339
|
the title keeps a third and every button stays reachable. -->
|
|
284
340
|
<LpShift
|
|
285
341
|
axis="width"
|
|
342
|
+
anchor="end"
|
|
343
|
+
presence
|
|
286
344
|
class="ml-auto flex max-w-[66%] shrink items-center gap-2 overflow-x-auto lp-scrollbar-none md:max-w-none md:shrink-0 md:overflow-visible"
|
|
287
345
|
>
|
|
288
346
|
<slot name="header-actions" />
|
|
@@ -304,6 +362,16 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
|
304
362
|
<slot />
|
|
305
363
|
</LpScrollArea>
|
|
306
364
|
</main>
|
|
365
|
+
<LpBottomNav
|
|
366
|
+
v-if="bottomNav"
|
|
367
|
+
:class="burgerHidden[mobileBreakpoint]"
|
|
368
|
+
:items="bottomItems"
|
|
369
|
+
:is-active="activeTest"
|
|
370
|
+
more
|
|
371
|
+
:more-active="moreActive"
|
|
372
|
+
@select="onBottom"
|
|
373
|
+
@more="drawerOpen = true"
|
|
374
|
+
/>
|
|
307
375
|
</div>
|
|
308
376
|
|
|
309
377
|
<slot name="overlays" />
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/*
|
|
3
|
+
* A phone app's bottom navigation: the main pages along the bottom edge, each
|
|
4
|
+
* a real link (or a button that emits `select`), the current one marked by a
|
|
5
|
+
* bar that slides between them, and "More" last for everything else.
|
|
6
|
+
*
|
|
7
|
+
* It looks exactly like LpTabs' bottom variant, sharing its bar and its tab
|
|
8
|
+
* face, but it is navigation between pages, not tabs within one: links that a
|
|
9
|
+
* screen reader announces as links, that open in a new tab when asked to, and
|
|
10
|
+
* that need no router call from the app. LpAppShell shows one on phones with
|
|
11
|
+
* `bottomNav`; an app with a shell of its own uses it directly.
|
|
12
|
+
*/
|
|
13
|
+
import { Motion } from "motion-v"
|
|
14
|
+
import { useId } from "vue"
|
|
15
|
+
import { usePillTransition } from "../composables/usePillTransition"
|
|
16
|
+
import { useLabels, useLinkComponent } from "../composables/useLpConfig"
|
|
17
|
+
import { resolveLink, type ResolvedLink } from "./link"
|
|
18
|
+
import LpTabFace from "./LpTabFace.vue"
|
|
19
|
+
import type { SidebarItem } from "./sidebar"
|
|
20
|
+
import { BOTTOM_BAR, BOTTOM_ITEM, BOTTOM_MARKER } from "./tabs"
|
|
21
|
+
|
|
22
|
+
const props = withDefaults(
|
|
23
|
+
defineProps<{
|
|
24
|
+
items: SidebarItem[]
|
|
25
|
+
/** Active page id. Ignored when `isActive` is given. */
|
|
26
|
+
modelValue?: string
|
|
27
|
+
isActive?: (item: SidebarItem) => boolean
|
|
28
|
+
/** A last "More" entry that emits `more` (the shell opens its drawer). */
|
|
29
|
+
more?: boolean
|
|
30
|
+
/** Marks "More" current: the page shown is one of those behind it. */
|
|
31
|
+
moreActive?: boolean
|
|
32
|
+
label?: string
|
|
33
|
+
labels?: Partial<{ nav: string; more: string }>
|
|
34
|
+
}>(),
|
|
35
|
+
{ more: false, moreActive: false },
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
const emit = defineEmits<{
|
|
39
|
+
(e: "select", item: SidebarItem): void
|
|
40
|
+
(e: "more"): void
|
|
41
|
+
}>()
|
|
42
|
+
|
|
43
|
+
const text = useLabels("bottomNav", { nav: "Main", more: "More" }, () => props.labels)
|
|
44
|
+
const linkComponent = useLinkComponent()
|
|
45
|
+
const pillTransition = usePillTransition()
|
|
46
|
+
const markerId = `lp-bottom-nav-${useId()}`
|
|
47
|
+
|
|
48
|
+
function active(item: SidebarItem): boolean {
|
|
49
|
+
return props.isActive ? props.isActive(item) : item.id === props.modelValue
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function rowOf(item: SidebarItem): ResolvedLink {
|
|
53
|
+
const link = item.disabled ? null : resolveLink(linkComponent.value, { to: item.to, href: item.href })
|
|
54
|
+
return link ?? { is: "button", props: { type: "button", disabled: item.disabled || undefined } }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const ROW = "relative flex items-center justify-center outline-none transition-[color,opacity] duration-[var(--duration-fast)] focus-visible:ring-2 focus-visible:ring-ring"
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<template>
|
|
61
|
+
<nav :aria-label="label ?? text.nav" :class="BOTTOM_BAR">
|
|
62
|
+
<component
|
|
63
|
+
:is="rowOf(item).is"
|
|
64
|
+
v-for="item in items"
|
|
65
|
+
:key="item.id"
|
|
66
|
+
v-bind="rowOf(item).props"
|
|
67
|
+
:aria-current="active(item) ? 'page' : undefined"
|
|
68
|
+
:class="[ROW, BOTTOM_ITEM, item.disabled ? 'cursor-not-allowed opacity-45' : active(item) ? 'text-brand' : 'text-muted']"
|
|
69
|
+
@click="emit('select', item)"
|
|
70
|
+
>
|
|
71
|
+
<Motion v-if="active(item)" :layout-id="markerId" :transition="pillTransition" :class="[BOTTOM_MARKER, 'bg-brand']" />
|
|
72
|
+
<LpTabFace :item="item" bottom :active="active(item)" />
|
|
73
|
+
</component>
|
|
74
|
+
<button
|
|
75
|
+
v-if="more"
|
|
76
|
+
type="button"
|
|
77
|
+
:class="[ROW, BOTTOM_ITEM, moreActive ? 'text-brand' : 'text-muted']"
|
|
78
|
+
@click="emit('more')"
|
|
79
|
+
>
|
|
80
|
+
<Motion v-if="moreActive" :layout-id="markerId" :transition="pillTransition" :class="[BOTTOM_MARKER, 'bg-brand']" />
|
|
81
|
+
<LpTabFace :item="{ label: text.more, icon: 'lucide:menu' }" bottom :active="moreActive" />
|
|
82
|
+
</button>
|
|
83
|
+
</nav>
|
|
84
|
+
</template>
|
|
@@ -104,6 +104,17 @@ const props = withDefaults(
|
|
|
104
104
|
size?: "sm" | "md" | "lg"
|
|
105
105
|
/** Trigger text beside the swatch. Defaults to the colour's own value. */
|
|
106
106
|
label?: string
|
|
107
|
+
/**
|
|
108
|
+
* `swatch` makes the trigger a round chip of the colour alone, for a row of
|
|
109
|
+
* presets that ends in "any colour" or a toolbar with no room for a field.
|
|
110
|
+
* The panel is the same.
|
|
111
|
+
*/
|
|
112
|
+
variant?: "field" | "swatch"
|
|
113
|
+
/**
|
|
114
|
+
* The swatch is the current choice. False draws it as "any colour" (a
|
|
115
|
+
* colour wheel), for when the value came from presets beside it.
|
|
116
|
+
*/
|
|
117
|
+
selected?: boolean
|
|
107
118
|
/** Accessible name for the trigger when it carries no visible text. */
|
|
108
119
|
ariaLabel?: string
|
|
109
120
|
/** Popover side, as LpPopover. */
|
|
@@ -156,6 +167,8 @@ const props = withDefaults(
|
|
|
156
167
|
recent: false,
|
|
157
168
|
recentKey: "lp.color.recent",
|
|
158
169
|
recentLimit: 8,
|
|
170
|
+
variant: "field",
|
|
171
|
+
selected: true,
|
|
159
172
|
},
|
|
160
173
|
)
|
|
161
174
|
|
|
@@ -372,6 +385,9 @@ function tickClass(value: string) {
|
|
|
372
385
|
return rgb && isLight(rgbToHsv(rgb)) ? "text-black" : "text-white"
|
|
373
386
|
}
|
|
374
387
|
|
|
388
|
+
const SWATCH_SIZE = { sm: "size-6", md: "size-7", lg: "size-8" }
|
|
389
|
+
const ANY_COLOUR = "conic-gradient(from 90deg, #ff5c5c, #ffd23f, #39e58c, #2dd4bf, #00bcff, #7c5cff, #ff5ca8, #ff5c5c)"
|
|
390
|
+
|
|
375
391
|
const triggerSize = {
|
|
376
392
|
sm: "h-(--size-control-sm) text-xs",
|
|
377
393
|
md: "h-(--size-control-md) text-sm",
|
|
@@ -634,10 +650,25 @@ const guardOutside = useOutsideGuard()
|
|
|
634
650
|
menu's trigger and the popover's trigger both. LpContextMenu renders
|
|
635
651
|
just its slot when `items` is empty, so `menu="none"` with no items of
|
|
636
652
|
your own leaves the native browser menu in place. -->
|
|
637
|
-
<PopoverAnchor class="block w-full">
|
|
653
|
+
<PopoverAnchor :class="variant === 'swatch' ? 'inline-block' : 'block w-full'">
|
|
638
654
|
<LpContextMenu :items="menuItems">
|
|
639
655
|
<PopoverTrigger as-child>
|
|
640
656
|
<button
|
|
657
|
+
v-if="variant === 'swatch'"
|
|
658
|
+
type="button"
|
|
659
|
+
:id="field.id"
|
|
660
|
+
:disabled="field.disabled"
|
|
661
|
+
:aria-label="ariaLabel ?? current"
|
|
662
|
+
:title="current"
|
|
663
|
+
class="relative shrink-0 overflow-hidden rounded-full outline-none ring-2 ring-offset-2 ring-offset-surface-raised transition-[box-shadow,scale] duration-(--duration-fast) hover:scale-110 focus-visible:ring-ring data-[state=open]:ring-brand disabled:cursor-not-allowed disabled:opacity-55"
|
|
664
|
+
:class="[SWATCH_SIZE[size], selected ? 'ring-ink' : 'ring-line']"
|
|
665
|
+
:style="{ background: CHECKERBOARD }"
|
|
666
|
+
>
|
|
667
|
+
<span class="block size-full" :style="{ background: selected ? previewCss : ANY_COLOUR }" />
|
|
668
|
+
<LpIcon v-if="!selected" name="lucide:pipette" :size="12" class="absolute inset-0 m-auto text-white drop-shadow" />
|
|
669
|
+
</button>
|
|
670
|
+
<button
|
|
671
|
+
v-else
|
|
641
672
|
type="button"
|
|
642
673
|
:id="field.id"
|
|
643
674
|
:disabled="field.disabled"
|
|
@@ -45,7 +45,10 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
|
|
|
45
45
|
</div>
|
|
46
46
|
<div class="flex flex-col gap-1">
|
|
47
47
|
<p class="font-semibold text-ink" :class="compact ? 'text-sm' : ''">{{ title }}</p>
|
|
48
|
-
|
|
48
|
+
<!-- The slot is for a line a string cannot carry: a command, a link. -->
|
|
49
|
+
<p v-if="description || $slots.description" class="max-w-sm text-sm text-muted">
|
|
50
|
+
<slot name="description">{{ description }}</slot>
|
|
51
|
+
</p>
|
|
49
52
|
</div>
|
|
50
53
|
<div v-if="$slots.default" class="mt-1"><slot /></div>
|
|
51
54
|
</div>
|
|
@@ -375,7 +375,9 @@ const label = computed(
|
|
|
375
375
|
class="lp-nf inline-flex items-baseline tabular-nums"
|
|
376
376
|
>
|
|
377
377
|
<span class="sr-only">{{ label }}</span>
|
|
378
|
-
|
|
378
|
+
<!-- `pre`: a flex item drops its leading and trailing spaces, so " ms"
|
|
379
|
+
as a suffix printed "12.8ms". -->
|
|
380
|
+
<span v-if="prefix" aria-hidden="true" class="whitespace-pre">{{ prefix }}</span>
|
|
379
381
|
|
|
380
382
|
<span class="lp-nf__track inline-flex items-baseline" aria-hidden="true">
|
|
381
383
|
<TransitionGroup name="lp-nf-cell">
|
|
@@ -402,7 +404,7 @@ const label = computed(
|
|
|
402
404
|
</TransitionGroup>
|
|
403
405
|
</span>
|
|
404
406
|
|
|
405
|
-
<span v-if="suffix" aria-hidden="true">{{ suffix }}</span>
|
|
407
|
+
<span v-if="suffix" aria-hidden="true" class="whitespace-pre">{{ suffix }}</span>
|
|
406
408
|
</span>
|
|
407
409
|
</template>
|
|
408
410
|
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
* <LpRollingText :value="label" />
|
|
21
21
|
* <LpRollingText :value="status" mode="flip" />
|
|
22
22
|
*/
|
|
23
|
-
import { computed } from "vue"
|
|
23
|
+
import { computed, ref, useSlots, watch } from "vue"
|
|
24
|
+
import { splitChange } from "./rolling"
|
|
24
25
|
|
|
25
26
|
const props = withDefaults(
|
|
26
27
|
defineProps<{
|
|
@@ -45,6 +46,21 @@ const props = withDefaults(
|
|
|
45
46
|
text patch — without it there is nothing to transition between. */
|
|
46
47
|
const key = computed(() => String(props.value))
|
|
47
48
|
|
|
49
|
+
/*
|
|
50
|
+
* Only what changed rolls. "Valid for 2:54" turning into "2:53" rolled every
|
|
51
|
+
* word of the line once a second (a countdown badge), so the whole badge
|
|
52
|
+
* flickered; now "Valid for 2:5" stays and the "4" rolls to "3". With a slot
|
|
53
|
+
* of its own, or wrapping text, the whole value rolls as before.
|
|
54
|
+
*/
|
|
55
|
+
const slots = useSlots()
|
|
56
|
+
const partial = computed(() => !slots.default && !props.wrap)
|
|
57
|
+
const parts = ref({ head: "", mid: String(props.value), tail: "" })
|
|
58
|
+
watch(
|
|
59
|
+
() => String(props.value),
|
|
60
|
+
(next, was) => (parts.value = splitChange(was, next)),
|
|
61
|
+
)
|
|
62
|
+
const midKey = computed(() => `${parts.value.head.length}:${parts.value.mid}`)
|
|
63
|
+
|
|
48
64
|
const name = computed(() =>
|
|
49
65
|
props.mode === "flip" ? "lp-roll-flip" : `lp-roll-${props.direction}`,
|
|
50
66
|
)
|
|
@@ -68,7 +84,16 @@ const style = computed(() => ({
|
|
|
68
84
|
:class="[justify, wrap && 'lp-roll--wrap']"
|
|
69
85
|
:style="style"
|
|
70
86
|
>
|
|
71
|
-
<
|
|
87
|
+
<template v-if="partial">
|
|
88
|
+
<span v-if="parts.head" class="whitespace-pre">{{ parts.head }}</span>
|
|
89
|
+
<span class="relative inline-flex overflow-hidden">
|
|
90
|
+
<Transition :name="name">
|
|
91
|
+
<span :key="midKey" class="lp-roll__item whitespace-pre">{{ parts.mid }}</span>
|
|
92
|
+
</Transition>
|
|
93
|
+
</span>
|
|
94
|
+
<span v-if="parts.tail" class="whitespace-pre">{{ parts.tail }}</span>
|
|
95
|
+
</template>
|
|
96
|
+
<Transition v-else :name="name">
|
|
72
97
|
<span :key="key" class="lp-roll__item">
|
|
73
98
|
<slot :value="value">{{ value }}</slot>
|
|
74
99
|
</span>
|
|
@@ -79,7 +79,9 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
|
|
|
79
79
|
[
|
|
80
80
|
"inline-flex items-center gap-1 rounded-control border border-line bg-surface-soft p-1",
|
|
81
81
|
sizeCls[props.size],
|
|
82
|
-
|
|
82
|
+
// `w-fit`, not left to the parent: in a column a flex item stretches, and
|
|
83
|
+
// the options sat on the left of an empty bar (the theme studio met it).
|
|
84
|
+
props.block ? "flex w-full" : "w-fit",
|
|
83
85
|
].join(" "),
|
|
84
86
|
)
|
|
85
87
|
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
* The mechanism — measure the natural size, pin it in pixels, tween — lives in
|
|
26
26
|
* `useShift`, and LpModal drives its panel with the same composable.
|
|
27
27
|
*/
|
|
28
|
-
import { computed } from "vue"
|
|
28
|
+
import { computed, onBeforeUnmount, watch } from "vue"
|
|
29
|
+
import { prefersReducedMotion } from "../composables/easing"
|
|
29
30
|
import { useShift, type ShiftAxis } from "../composables/useShift"
|
|
30
31
|
|
|
31
32
|
const props = withDefaults(
|
|
@@ -43,8 +44,26 @@ const props = withDefaults(
|
|
|
43
44
|
* canvas), where a tween would fight the thing already animating it.
|
|
44
45
|
*/
|
|
45
46
|
disabled?: boolean
|
|
47
|
+
/**
|
|
48
|
+
* Which edge the content holds to while the box catches up, for a flex
|
|
49
|
+
* row. A box pinned to the right (`ml-auto`) grows leftwards, and content
|
|
50
|
+
* laid out from its left edge throws every unchanged control to the right
|
|
51
|
+
* for the first frame and slides it back: the controls that did not change
|
|
52
|
+
* are the ones that jump. `"end"` keeps them where they are and lets the
|
|
53
|
+
* new one come in from the free side.
|
|
54
|
+
*/
|
|
55
|
+
anchor?: "start" | "end"
|
|
56
|
+
/**
|
|
57
|
+
* Fade the box's own children in and out as they come and go. Without it
|
|
58
|
+
* the box glides but a control still pops into being as the edge uncovers
|
|
59
|
+
* it, and one that leaves is gone in a frame while its space closes up.
|
|
60
|
+
* Only direct children: what a child does inside itself is its own affair,
|
|
61
|
+
* so a control that changes its label carries the label as its `key` and
|
|
62
|
+
* is replaced, the old one fading as the new one arrives.
|
|
63
|
+
*/
|
|
64
|
+
presence?: boolean
|
|
46
65
|
}>(),
|
|
47
|
-
{ axis: "height", disabled: false },
|
|
66
|
+
{ axis: "height", disabled: false, anchor: "start", presence: false },
|
|
48
67
|
)
|
|
49
68
|
|
|
50
69
|
const { el, tweening, resizing } = useShift({
|
|
@@ -67,6 +86,157 @@ const TRANSITION_CLASS: Record<ShiftAxis, string> = {
|
|
|
67
86
|
}
|
|
68
87
|
const transitionClass = computed(() => TRANSITION_CLASS[props.axis])
|
|
69
88
|
|
|
89
|
+
/*
|
|
90
|
+
* Presence. A leaving element is already out of the DOM when the observer
|
|
91
|
+
* hears of it, so where it stood is read from the last time it was seen: its
|
|
92
|
+
* offset from the anchored edge, which stays true while the box eases towards
|
|
93
|
+
* that edge. A clone stands there, out of the flow, and fades.
|
|
94
|
+
*
|
|
95
|
+
* What stays but is moved by the change (a title centred in a box that just
|
|
96
|
+
* lost its subtitle) glides from where it was instead of jumping (FLIP).
|
|
97
|
+
*/
|
|
98
|
+
const PRESENCE_MS = 200
|
|
99
|
+
const MOVE_MS = 160
|
|
100
|
+
const EASE = "cubic-bezier(0.2, 0, 0, 1)"
|
|
101
|
+
const GHOST = "data-lp-shift-ghost"
|
|
102
|
+
|
|
103
|
+
interface Seen {
|
|
104
|
+
from: number
|
|
105
|
+
top: number
|
|
106
|
+
width: number
|
|
107
|
+
height: number
|
|
108
|
+
x: number
|
|
109
|
+
y: number
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const seen = new WeakMap<Element, Seen>()
|
|
113
|
+
let presenceObserver: MutationObserver | undefined
|
|
114
|
+
let rememberTimer: ReturnType<typeof setTimeout> | undefined
|
|
115
|
+
let presenceSize: ResizeObserver | undefined
|
|
116
|
+
|
|
117
|
+
function rememberSoon(box: HTMLElement, wait = PRESENCE_MS + 80) {
|
|
118
|
+
clearTimeout(rememberTimer)
|
|
119
|
+
rememberTimer = setTimeout(() => remember(box), wait)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function elementsOf(box: HTMLElement): Element[] {
|
|
123
|
+
return [...box.querySelectorAll("*")].filter((node) => !node.closest(`[${GHOST}]`) && !(node instanceof SVGElement && !(node instanceof SVGSVGElement)))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function remember(box: HTMLElement) {
|
|
127
|
+
const outer = box.getBoundingClientRect()
|
|
128
|
+
for (const node of elementsOf(box)) {
|
|
129
|
+
const r = node.getBoundingClientRect()
|
|
130
|
+
seen.set(node, {
|
|
131
|
+
from: props.anchor === "end" ? outer.right - r.right : r.left - outer.left,
|
|
132
|
+
top: r.top - outer.top,
|
|
133
|
+
width: r.width,
|
|
134
|
+
height: r.height,
|
|
135
|
+
x: r.left,
|
|
136
|
+
y: r.top,
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const offset = () => (props.anchor === "end" ? -6 : 6)
|
|
142
|
+
|
|
143
|
+
function ghost(box: HTMLElement, node: Element) {
|
|
144
|
+
const at = seen.get(node)
|
|
145
|
+
if (!at || !(node instanceof HTMLElement) || node.hasAttribute(GHOST) || at.width === 0) return
|
|
146
|
+
const copy = node.cloneNode(true) as HTMLElement
|
|
147
|
+
copy.setAttribute(GHOST, "")
|
|
148
|
+
copy.setAttribute("aria-hidden", "true")
|
|
149
|
+
copy.inert = true
|
|
150
|
+
Object.assign(copy.style, {
|
|
151
|
+
position: "absolute",
|
|
152
|
+
top: `${at.top}px`,
|
|
153
|
+
[props.anchor === "end" ? "right" : "left"]: `${at.from}px`,
|
|
154
|
+
width: `${at.width}px`,
|
|
155
|
+
height: `${at.height}px`,
|
|
156
|
+
margin: "0",
|
|
157
|
+
pointerEvents: "none",
|
|
158
|
+
})
|
|
159
|
+
box.appendChild(copy)
|
|
160
|
+
copy
|
|
161
|
+
.animate(
|
|
162
|
+
[
|
|
163
|
+
{ opacity: 1, transform: "none", filter: "blur(0)" },
|
|
164
|
+
{ opacity: 0, transform: `translateX(${offset()}px) scale(0.96)`, filter: "blur(2px)" },
|
|
165
|
+
],
|
|
166
|
+
{ duration: PRESENCE_MS, easing: EASE },
|
|
167
|
+
)
|
|
168
|
+
.finished.catch(() => undefined)
|
|
169
|
+
.finally(() => copy.remove())
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function arrive(node: Element) {
|
|
173
|
+
if (!(node instanceof HTMLElement) || node.hasAttribute(GHOST)) return
|
|
174
|
+
node.animate(
|
|
175
|
+
[
|
|
176
|
+
{ opacity: 0, transform: `translateX(${offset()}px) scale(0.96)`, filter: "blur(2px)" },
|
|
177
|
+
{ opacity: 1, transform: "none", filter: "blur(0)" },
|
|
178
|
+
],
|
|
179
|
+
{ duration: PRESENCE_MS, delay: 40, easing: EASE, fill: "backwards" },
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function glide(node: Element) {
|
|
184
|
+
const was = seen.get(node)
|
|
185
|
+
if (!was || !(node instanceof HTMLElement)) return
|
|
186
|
+
const now = node.getBoundingClientRect()
|
|
187
|
+
const dx = was.x - now.left
|
|
188
|
+
const dy = was.y - now.top
|
|
189
|
+
if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) return
|
|
190
|
+
node.animate([{ transform: `translate(${dx}px, ${dy}px)` }, { transform: "none" }], { duration: MOVE_MS, easing: EASE })
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
watch(
|
|
194
|
+
[el, () => props.presence && !props.disabled],
|
|
195
|
+
([box, on]) => {
|
|
196
|
+
presenceObserver?.disconnect()
|
|
197
|
+
presenceObserver = undefined
|
|
198
|
+
presenceSize?.disconnect()
|
|
199
|
+
presenceSize = undefined
|
|
200
|
+
if (!box || !on) return
|
|
201
|
+
remember(box)
|
|
202
|
+
presenceObserver = new MutationObserver((records) => {
|
|
203
|
+
const real = records.filter(
|
|
204
|
+
(r) =>
|
|
205
|
+
![...r.addedNodes, ...r.removedNodes].every((n) => n instanceof Element && n.hasAttribute(GHOST)),
|
|
206
|
+
)
|
|
207
|
+
if (real.length && !prefersReducedMotion()) {
|
|
208
|
+
const added = new Set<Node>()
|
|
209
|
+
for (const record of real) {
|
|
210
|
+
record.removedNodes.forEach((node) => node instanceof Element && ghost(box, node))
|
|
211
|
+
record.addedNodes.forEach((node) => {
|
|
212
|
+
added.add(node)
|
|
213
|
+
if (node instanceof Element) arrive(node)
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
// Measured in the next frame, which still comes before the paint: by
|
|
217
|
+
// then the box has taken the classes the change gave it (held to its
|
|
218
|
+
// edge), and the place an element lands is the one it will keep.
|
|
219
|
+
const parents = real.map((r) => r.target).filter((t): t is Element => t instanceof Element && !t.hasAttribute(GHOST))
|
|
220
|
+
requestAnimationFrame(() => {
|
|
221
|
+
for (const parent of parents) for (const child of parent.children) if (!added.has(child)) glide(child)
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
rememberSoon(box)
|
|
225
|
+
})
|
|
226
|
+
presenceObserver.observe(box, { childList: true, subtree: true })
|
|
227
|
+
presenceSize = new ResizeObserver(() => rememberSoon(box, 60))
|
|
228
|
+
presenceSize.observe(box)
|
|
229
|
+
void document.fonts?.ready.then(() => rememberSoon(box, 0))
|
|
230
|
+
},
|
|
231
|
+
{ flush: "post" },
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
onBeforeUnmount(() => {
|
|
235
|
+
presenceObserver?.disconnect()
|
|
236
|
+
presenceSize?.disconnect()
|
|
237
|
+
clearTimeout(rememberTimer)
|
|
238
|
+
})
|
|
239
|
+
|
|
70
240
|
defineExpose({ resizing })
|
|
71
241
|
</script>
|
|
72
242
|
|
|
@@ -83,6 +253,8 @@ defineExpose({ resizing })
|
|
|
83
253
|
? `${transitionClass} duration-fast ease-[var(--ease-emphasized)] motion-reduce:transition-none`
|
|
84
254
|
: '',
|
|
85
255
|
resizing ? 'overflow-hidden' : '',
|
|
256
|
+
resizing && anchor === 'end' ? 'justify-end' : '',
|
|
257
|
+
presence ? 'relative' : '',
|
|
86
258
|
]"
|
|
87
259
|
>
|
|
88
260
|
<slot :resizing="resizing" />
|