@leavepulse/ui 0.77.3 → 0.78.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/README.md +68 -0
- package/dist/assets/{LpCommandPalette-DOIZHw-g.css → LpCommandPalette-DDJKICKh.css} +19 -19
- package/dist/assets/{LpDock-Cbh0wQUv.css → LpDock-Dw82tWVD.css} +3 -3
- package/dist/assets/{LpDropzone-B_qRq1-R.css → LpDropzone-DNbh_NFJ.css} +10 -10
- package/dist/assets/{LpImageEditor-XM5bAziW.css → LpImageEditor-Bgu20XLs.css} +6 -6
- package/dist/assets/{LpTable-Bv-EcQs7.css → LpTable-CCk0ZHo7.css} +19 -19
- package/dist/assets/LpTabs-Bh7a-Bdl.css +18 -0
- package/dist/chunks/AppShellContextNav-C6Ho2mof.js +49 -0
- package/dist/chunks/ChartRangeSelector-HO0sMMz6.js +129 -0
- package/dist/chunks/{ContextMenuItems-BXumicbh.js → ContextMenuItems-DmaKSJVv.js} +1 -1
- package/dist/chunks/{DropzoneLinkField-C-9BCGJn.js → DropzoneLinkField-DXIr0eFz.js} +2 -2
- package/dist/chunks/{FieldWrapper-CORPzo9s.js → FieldWrapper-DnuMTrqW.js} +1 -1
- package/dist/chunks/IconCloseButton-COZEsa9u.js +29 -0
- package/dist/chunks/{LayoutCanvas-CNOAUk7D.js → LayoutCanvas-CZKOltfA.js} +1 -1
- package/dist/chunks/{LayoutNode-DF5BMN0J.js → LayoutNode-D2f7v683.js} +45 -45
- package/dist/chunks/{LpActionBar-BP1ld7-S.js → LpActionBar-BGMk5KoG.js} +3 -3
- package/dist/chunks/{LpActivityLine-CJvzrNpj.js → LpActivityLine-Ea-t5Vza.js} +4 -2
- package/dist/chunks/{LpAddressInput-E8KtwU7-.js → LpAddressInput-ZqoZ-N2Y.js} +12 -11
- package/dist/chunks/{LpAlert-n5XPAZfv.js → LpAlert-CjvTZIKF.js} +3 -3
- package/dist/chunks/LpAppShell-BNAVTQsY.js +582 -0
- package/dist/chunks/{LpAutocomplete-C-SpPfBG.js → LpAutocomplete-DkY6CSyD.js} +24 -42
- package/dist/chunks/{LpAvatar-Cg6Az3wz.js → LpAvatar-cQVsNk6F.js} +2 -2
- package/dist/chunks/{LpBadge-wUYLN4U9.js → LpBadge-D5QQdDQ_.js} +2 -2
- package/dist/chunks/{LpBottomNav-CbNQQPuL.js → LpBottomNav-zxLGGbjm.js} +9 -2
- package/dist/chunks/{LpBreadcrumbs-DPdbth-c.js → LpBreadcrumbs-CIIBsygI.js} +4 -3
- package/dist/chunks/{LpButton-CFb44IjC.js → LpButton-DSHioIfD.js} +5 -4
- package/dist/chunks/{LpCalendar-CuoWks-0.js → LpCalendar-D4-LVjfF.js} +1 -1
- package/dist/chunks/{LpCard-r13WRqox.js → LpCard-BPtYXwnT.js} +1 -1
- package/dist/chunks/{LpCheckbox-Cs4SHaHX.js → LpCheckbox-D6RuDW4X.js} +38 -16
- package/dist/chunks/{LpCodeBlock-DRC8VbXW.js → LpCodeBlock-DuzmnIHl.js} +19 -41
- package/dist/chunks/{LpCodeDisplay-CE9TxCA4.js → LpCodeDisplay-CO8fngxa.js} +5 -5
- package/dist/chunks/{LpCollapse-CWS9M_lm.js → LpCollapse-DrAw5DBZ.js} +5 -4
- package/dist/chunks/{LpColorPicker-1y1QJHZR.js → LpColorPicker-BxAHEnR5.js} +11 -12
- package/dist/chunks/{LpCommandScope-CtUhJe8T.js → LpCommandScope-DRUvHiX7.js} +1 -1
- package/dist/chunks/{LpConfirmDialog-DlD45Bi_.js → LpConfirmDialog-6oCIJTDa.js} +3 -3
- package/dist/chunks/{LpConfirmHost-BIWjBLmI.js → LpConfirmHost-DgHvfT6s.js} +1 -1
- package/dist/chunks/{LpContextMenu-rx2tCjwk.js → LpContextMenu--eMGIql6.js} +13 -15
- package/dist/chunks/{LpContextSwitcher-Dwh6dyID.js → LpContextSwitcher-1C1tExdl.js} +2 -2
- package/dist/chunks/{LpCopyButton-xF7wFVoU.js → LpCopyButton-7EOSIuwa.js} +3 -3
- package/dist/chunks/{LpCountdown-DgeY3cLG.js → LpCountdown-DVl7vXI1.js} +4 -3
- package/dist/chunks/{LpDataList-Jxyl9H5w.js → LpDataList-DgBeM01y.js} +13 -4
- package/dist/chunks/{LpDatePicker-CA0QTfBC.js → LpDatePicker-DwSDY0mt.js} +12 -18
- package/dist/chunks/{LpDateRangePicker-u3J51xXk.js → LpDateRangePicker-CNQXdTVv.js} +10 -16
- package/dist/chunks/{LpDisclosure-Cpa7Hc5T.js → LpDisclosure-DG7JCo-d.js} +2 -2
- package/dist/chunks/{LpDrawer-CrG8t_KP.js → LpDrawer-Bt9UUqt_.js} +61 -61
- package/dist/chunks/{LpDropdownMenu-Dl7Z4ZhS.js → LpDropdownMenu-CLzxIOgM.js} +9 -17
- package/dist/chunks/{LpEditable-Cy6GLbFx.js → LpEditable-DVT0Rh8F.js} +9 -6
- package/dist/chunks/{LpEmptyState-EQHiRYhG.js → LpEmptyState-DurPF001.js} +1 -1
- package/dist/chunks/{LpFieldNotice-CzQ2_5rQ.js → LpFieldNotice-DJ10cYIo.js} +1 -1
- package/dist/chunks/LpFileTree-BSx1kBoV.js +735 -0
- package/dist/chunks/{LpFileTreeNode-cT6BIWvz.js → LpFileTreeNode-4dVmFHb3.js} +116 -34
- package/dist/chunks/{LpFormField-Ch69Jd-X.js → LpFormField-BKYn9dyP.js} +3 -3
- package/dist/chunks/{LpFrame-BVQCQLDI.js → LpFrame-Bv5Fphte.js} +25 -11
- package/dist/chunks/{LpHintExample-9Py0wc-L.js → LpHintExample-YzCXiSUp.js} +1 -1
- package/dist/chunks/{LpHintLayer-CgObdUsM.js → LpHintLayer-BeKRsRon.js} +2 -2
- package/dist/chunks/{LpHints-Bz-BlaSl.js → LpHints-qf3xYsza.js} +3 -3
- package/dist/chunks/{LpHoverCard-spGxyw7T.js → LpHoverCard-DanH3Nz1.js} +23 -5
- package/dist/chunks/{LpIcon--5WSb3jJ.js → LpIcon-C6JpQ5td.js} +6 -3
- package/dist/chunks/{LpIdenticon-BzdC_qz9.js → LpIdenticon-Dbbr4fvt.js} +2 -1
- package/dist/chunks/{LpInfraNode-46XOCkfq.js → LpInfraNode-BAU40Km2.js} +2 -2
- package/dist/chunks/{LpInput-Tw-yxT4I.js → LpInput-DL6HEJdi.js} +16 -28
- package/dist/chunks/LpInspector-CuoMAhiw.js +209 -0
- package/dist/chunks/{LpLightbox-B95yu22B.js → LpLightbox-xvpQ64MM.js} +57 -32
- package/dist/chunks/{LpLoader-DVwkOmt9.js → LpLoader-Bf1yZwaU.js} +1 -1
- package/dist/chunks/LpLogInspector-6cNnXqp4.js +119 -0
- package/dist/chunks/{LpLogViewer-ngqkns4m.js → LpLogViewer-Dvb1E9_G.js} +234 -53
- package/dist/chunks/{LpMapPicker-DRGgh89B.js → LpMapPicker-D5_YLLBC.js} +4 -3
- package/dist/chunks/{LpMasonry--k0571qG.js → LpMasonry-DPYekFxs.js} +8 -5
- package/dist/chunks/{LpMediaGallery-Bkram-MO.js → LpMediaGallery-Df-tiWeq.js} +3 -3
- package/dist/chunks/{LpModal-CoGhLTyH.js → LpModal-DrtQ_1A2.js} +11 -13
- package/dist/chunks/{LpModalNav-BGvs2y5Q.js → LpModalNav-DkmGCnZE.js} +11 -37
- package/dist/chunks/{LpNotificationBell-B0034Lk-.js → LpNotificationBell-D_wklO1m.js} +9 -9
- package/dist/chunks/{LpNumberField-e0IBkWU1.js → LpNumberField-CNHsmSDZ.js} +7 -16
- package/dist/chunks/{LpOtpInput-CeBPsM7D.js → LpOtpInput-BRpLBbOD.js} +2 -2
- package/dist/chunks/{LpPagination-BIIiYHXR.js → LpPagination-snQ020xQ.js} +6 -4
- package/dist/chunks/{LpPaletteTheme-DiLYKQiZ.js → LpPaletteTheme-dVtxHx-F.js} +4 -4
- package/dist/chunks/{LpPasswordInput-BkHKQDhM.js → LpPasswordInput-B-zD6tUS.js} +2 -2
- package/dist/chunks/{LpPhoneInput-CETEp5W7.js → LpPhoneInput-BSL7NX4z.js} +15 -26
- package/dist/chunks/{LpPie-DrZepi0p.js → LpPie-9t_guO49.js} +22 -21
- package/dist/chunks/{LpPipeline-B3aOVOkV.js → LpPipeline-rjXvEDRJ.js} +16 -7
- package/dist/chunks/{LpPopover-DmZ3wUtP.js → LpPopover-BQSuxtai.js} +10 -1
- package/dist/chunks/{LpProgress-Yr6ONG5t.js → LpProgress-D_MSjowb.js} +2 -1
- package/dist/chunks/{LpQuota-Dk2MyI2Z.js → LpQuota-CgWPgdVt.js} +1 -1
- package/dist/chunks/{LpRadioGroup-ggaPL92T.js → LpRadioGroup-BrazjLY_.js} +4 -4
- package/dist/chunks/{LpRangeCalendar-CYYUYqJ7.js → LpRangeCalendar-BrqlGZzE.js} +1 -1
- package/dist/chunks/{LpRangeSlider-CM-0ZITZ.js → LpRangeSlider-fz61tzc-.js} +5 -9
- package/dist/chunks/{LpRelativeTime-DwgCb5-8.js → LpRelativeTime-BovUk8MP.js} +1 -1
- package/dist/chunks/{LpRepeater-D7ZmnIYd.js → LpRepeater-BkJHSPI1.js} +3 -3
- package/dist/chunks/{LpRevealOnScroll-DcQjexLy.js → LpRevealOnScroll-CPwyiQAr.js} +1 -1
- package/dist/chunks/{LpScramble-Bn9OoOtC.js → LpScramble-DsE77iVp.js} +1 -1
- package/dist/chunks/{LpScrollArea-MyEnVB_5.js → LpScrollArea-ay4Xo_hg.js} +22 -10
- package/dist/chunks/{LpSecretField-BbmqM9ye.js → LpSecretField-DBvU6iAw.js} +10 -9
- package/dist/chunks/{LpSection-DFK_uoJE.js → LpSection-BfteipxU.js} +1 -1
- package/dist/chunks/{LpSegmented-CeACsMMA.js → LpSegmented-DbEJhbv-.js} +8 -10
- package/dist/chunks/{LpSelect-Cxq4yBGd.js → LpSelect-Y-iuSymW.js} +47 -30
- package/dist/chunks/{LpShaderBackdrop-Bote0iuy.js → LpShaderBackdrop-wQygYb5h.js} +13 -10
- package/dist/chunks/{LpShift-CCq7dofl.js → LpShift-DAgS_d2x.js} +18 -15
- package/dist/chunks/{LpSidebar-DzE1783C.js → LpSidebar-kSkpPh3-.js} +74 -87
- package/dist/chunks/{LpSidebarNav-DE3PhXVP.js → LpSidebarNav-C2mvqrY2.js} +8 -6
- package/dist/chunks/{LpSidebarRow-BRYhNDSo.js → LpSidebarRow-eRP4Q31q.js} +6 -5
- package/dist/chunks/{LpSlider-DxYoCJJO.js → LpSlider-A4ixovgS.js} +5 -9
- package/dist/chunks/{LpSpotlight-4F3NeUMY.js → LpSpotlight-DwxkhFgR.js} +2 -1
- package/dist/chunks/{LpStat-f2LfDCJ4.js → LpStat-pMNZgFcq.js} +2 -2
- package/dist/chunks/{LpStatus-DglxttQ7.js → LpStatus-DGJFwk5F.js} +2 -2
- package/dist/chunks/{LpStepMark-DpzKVyh8.js → LpStepMark-Dw2MRTrt.js} +2 -2
- package/dist/chunks/{LpStepper-DpsocBNJ.js → LpStepper-DNnh7lCu.js} +1 -1
- package/dist/chunks/{LpSwap-C96c0LWh.js → LpSwap-M7nFeW3C.js} +1 -1
- package/dist/chunks/{LpSwitch-BnXd1llc.js → LpSwitch-DnC2-0aV.js} +3 -3
- package/dist/chunks/{LpTabFace-DPCwKkH2.js → LpTabFace-DQoV8mQG.js} +2 -2
- package/dist/chunks/{LpTableFilter-CiVBpaq0.js → LpTableFilter-DgARTsA1.js} +4 -4
- package/dist/chunks/{LpTableOfContents-BN5zvpnx.js → LpTableOfContents-CKkWEK6R.js} +14 -9
- package/dist/chunks/{LpTagsInput-C-mDG648.js → LpTagsInput-Dt0mteSj.js} +19 -27
- package/dist/chunks/{LpTextarea-TE-yZv2U.js → LpTextarea-Co0b9PVx.js} +8 -16
- package/dist/chunks/LpThemeProvider-XK6ZeWAE.js +81 -0
- package/dist/chunks/{LpThemeStudio-BpORDvH6.js → LpThemeStudio-DzYfE-5k.js} +12 -195
- package/dist/chunks/{LpThemeSwitcher-BnEn_mvf.js → LpThemeSwitcher-CFLkdbBL.js} +8 -6
- package/dist/chunks/{LpTimeField-BawSVsmt.js → LpTimeField-DeOcKWJq.js} +6 -9
- package/dist/chunks/{LpToaster-IwTXjbbU.js → LpToaster-QFj0P-5m.js} +10 -8
- package/dist/chunks/{LpTooltip-BcBvIx4z.js → LpTooltip-BJf3lZMq.js} +51 -19
- package/dist/chunks/LpTopologyCanvas-BOHHHZ_6.js +673 -0
- package/dist/chunks/{LpTour-BZq-mUMv.js → LpTour-ClKYhx4h.js} +7 -5
- package/dist/chunks/{LpTruncate-CflAIwiJ.js → LpTruncate-C6u3gjc6.js} +6 -5
- package/dist/chunks/{LpTypewriter-BCL_a6wE.js → LpTypewriter-CZIzpdbe.js} +2 -1
- package/dist/chunks/{LpUptimeBar-DqNvvMBK.js → LpUptimeBar-BnYmR2rj.js} +3 -3
- package/dist/chunks/{LpVideoBackdrop-DS1wGv-X.js → LpVideoBackdrop-Ct4St4ZL.js} +10 -13
- package/dist/chunks/LpWindow-B8YSRVTH.js +297 -0
- package/dist/chunks/LpWorkspaceSwitcher-D0e9upAy.js +139 -0
- package/dist/chunks/{MenuTitle-BmCNNTKH.js → MenuTitle-Bh5dAdV1.js} +1 -1
- package/dist/chunks/{OptionContent-22UT8zLn.js → OptionContent-CGb8QlHA.js} +1 -1
- package/dist/chunks/{PickerField-Bfh8id3d.js → PickerField-BM1dyafh.js} +4 -4
- package/dist/chunks/{RevealToggle-BEit1n6Z.js → RevealToggle-CCPDE8Mv.js} +3 -3
- package/dist/chunks/SliderRail-DhPREa_R.js +18 -0
- package/dist/chunks/{TourCard-DNclH3u_.js → TourCard-DAzTYfav.js} +4 -4
- package/dist/chunks/{icons-DMITlfw5.js → icons-CYfcpSDS.js} +26 -10
- package/dist/component-names.d.ts +1 -1
- package/dist/components/AppShellContextNav.vue.d.ts +13 -0
- package/dist/components/AppShellContextNav.vue.js +4 -0
- package/dist/components/ChartRangeSelector.vue.d.ts +21 -0
- package/dist/components/ChartRangeSelector.vue.js +4 -0
- package/dist/components/ContextMenuItems.vue.js +1 -1
- package/dist/components/DropzoneLinkField.vue.d.ts +2 -2
- package/dist/components/DropzoneLinkField.vue.js +1 -1
- package/dist/components/FieldWrapper.vue.js +1 -1
- package/dist/components/IconCloseButton.vue.d.ts +7 -0
- package/dist/components/IconCloseButton.vue.js +4 -0
- 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/LpActivityLine.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 +110 -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 +5 -0
- package/dist/components/LpBottomNav.vue.js +1 -1
- package/dist/components/LpBreadcrumbs.vue.js +1 -1
- package/dist/components/LpButton.vue.d.ts +14 -2
- 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.d.ts +74 -2
- package/dist/components/LpChart.vue.js +326 -85
- package/dist/components/LpCheckbox.vue.d.ts +6 -2
- 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/LpCollapse.vue.js +1 -1
- package/dist/components/LpColorPicker.vue.js +1 -1
- package/dist/components/LpCommandPalette.vue.d.ts +8 -0
- package/dist/components/LpCommandPalette.vue.js +107 -56
- package/dist/components/LpCommandScope.vue.js +1 -1
- 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/LpContextSwitcher.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.d.ts +1 -1
- package/dist/components/LpDataList.vue.js +1 -1
- package/dist/components/LpDatePicker.vue.d.ts +3 -15
- package/dist/components/LpDatePicker.vue.js +1 -1
- package/dist/components/LpDateRangePicker.vue.d.ts +3 -13
- package/dist/components/LpDateRangePicker.vue.js +1 -1
- package/dist/components/LpDisclosure.vue.js +1 -1
- package/dist/components/LpDock.vue.js +21 -18
- package/dist/components/LpDrawer.vue.d.ts +1 -1
- package/dist/components/LpDrawer.vue.js +1 -1
- package/dist/components/LpDropdownMenu.vue.js +1 -1
- package/dist/components/LpDropzone.vue.js +14 -11
- package/dist/components/LpEditable.vue.js +1 -1
- package/dist/components/LpEmptyState.vue.js +1 -1
- package/dist/components/LpFieldNotice.vue.js +1 -1
- package/dist/components/LpFileTree.vue.d.ts +96 -6
- package/dist/components/LpFileTree.vue.js +1 -1
- package/dist/components/LpFileTreeNode.vue.d.ts +48 -0
- package/dist/components/LpFileTreeNode.vue.js +1 -1
- package/dist/components/LpFormField.vue.js +1 -1
- package/dist/components/LpFrame.vue.js +1 -1
- package/dist/components/LpGauge.vue.d.ts +1 -1
- package/dist/components/LpHintExample.vue.js +1 -1
- package/dist/components/LpHintLayer.vue.js +1 -1
- package/dist/components/LpHints.vue.js +1 -1
- package/dist/components/LpHoverCard.vue.js +1 -1
- package/dist/components/LpIcon.vue.d.ts +2 -0
- package/dist/components/LpIcon.vue.js +1 -1
- package/dist/components/LpIdenticon.vue.js +1 -1
- package/dist/components/LpImageEditor.vue.js +7 -6
- package/dist/components/LpInfraNode.vue.js +1 -1
- package/dist/components/LpInput.vue.d.ts +1 -1
- package/dist/components/LpInput.vue.js +1 -1
- package/dist/components/LpInspector.vue.d.ts +87 -0
- package/dist/components/LpInspector.vue.js +4 -0
- package/dist/components/LpLightbox.vue.d.ts +8 -0
- package/dist/components/LpLightbox.vue.js +1 -1
- package/dist/components/LpLoader.vue.d.ts +1 -1
- package/dist/components/LpLoader.vue.js +1 -1
- package/dist/components/LpLogInspector.vue.d.ts +18 -0
- package/dist/components/LpLogInspector.vue.js +4 -0
- package/dist/components/LpLogViewer.vue.d.ts +93 -0
- package/dist/components/LpLogViewer.vue.js +1 -1
- package/dist/components/LpMapPicker.vue.js +1 -1
- package/dist/components/LpMasonry.vue.js +1 -1
- package/dist/components/LpMediaGallery.vue.js +1 -1
- package/dist/components/LpModal.vue.d.ts +4 -2
- package/dist/components/LpModal.vue.js +1 -1
- package/dist/components/LpModalNav.vue.d.ts +5 -5
- package/dist/components/LpModalNav.vue.js +1 -1
- package/dist/components/LpNotificationBell.vue.d.ts +2 -2
- package/dist/components/LpNotificationBell.vue.js +1 -1
- package/dist/components/LpNumberField.vue.js +1 -1
- package/dist/components/LpOtpInput.vue.js +1 -1
- package/dist/components/LpPagination.vue.js +1 -1
- package/dist/components/LpPaletteFacts.vue.js +1 -1
- package/dist/components/LpPaletteHeader.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.d.ts +2 -2
- package/dist/components/LpPie.vue.js +1 -1
- package/dist/components/LpPipeline.vue.d.ts +4 -0
- package/dist/components/LpPipeline.vue.js +1 -1
- package/dist/components/LpPopover.vue.d.ts +7 -3
- package/dist/components/LpPopover.vue.js +1 -1
- package/dist/components/LpProgress.vue.d.ts +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/LpRangeSlider.vue.js +1 -1
- package/dist/components/LpRelativeTime.vue.js +1 -1
- package/dist/components/LpRepeater.vue.js +1 -1
- package/dist/components/LpRevealOnScroll.vue.js +1 -1
- package/dist/components/LpScramble.vue.js +1 -1
- package/dist/components/LpScrollArea.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/LpShaderBackdrop.vue.js +1 -1
- package/dist/components/LpShift.vue.js +1 -1
- package/dist/components/LpShortcutsDialog.vue.js +1 -1
- package/dist/components/LpSidebar.vue.d.ts +1 -1
- package/dist/components/LpSidebar.vue.js +1 -1
- package/dist/components/LpSidebarNav.vue.js +1 -1
- package/dist/components/LpSidebarRow.vue.js +1 -1
- package/dist/components/LpSlider.vue.js +1 -1
- package/dist/components/LpSparkline.vue.d.ts +1 -1
- package/dist/components/LpSpotlight.vue.js +1 -1
- 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.js +1 -1
- package/dist/components/LpTable.vue.d.ts +132 -0
- package/dist/components/LpTable.vue.js +547 -135
- package/dist/components/LpTableFilter.vue.js +1 -1
- package/dist/components/LpTableOfContents.vue.js +1 -1
- package/dist/components/LpTabs.vue.js +6 -5
- package/dist/components/LpTagsInput.vue.js +1 -1
- package/dist/components/LpTextarea.vue.js +1 -1
- package/dist/components/LpThemeProvider.vue.d.ts +2 -0
- package/dist/components/LpThemeProvider.vue.js +1 -1
- package/dist/components/LpThemeStudio.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/LpTooltip.vue.js +1 -1
- package/dist/components/LpTopologyCanvas.vue.d.ts +131 -5
- 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/LpTypewriter.vue.js +1 -1
- package/dist/components/LpUptimeBar.vue.js +1 -1
- package/dist/components/LpVideoBackdrop.vue.js +1 -1
- package/dist/components/LpWindow.vue.d.ts +99 -0
- package/dist/components/LpWindow.vue.js +4 -0
- package/dist/components/LpWorkspaceSwitcher.vue.d.ts +18 -0
- package/dist/components/LpWorkspaceSwitcher.vue.js +4 -0
- package/dist/components/MenuTitle.vue.js +1 -1
- package/dist/components/OptionContent.vue.js +1 -1
- package/dist/components/PickerField.vue.d.ts +2 -2
- package/dist/components/PickerField.vue.js +1 -1
- package/dist/components/RevealToggle.vue.js +1 -1
- package/dist/components/SliderRail.vue.d.ts +3 -0
- package/dist/components/SliderRail.vue.js +4 -0
- package/dist/components/TourCard.vue.js +1 -1
- package/dist/components/address.js +2 -1
- package/dist/components/appShell.d.ts +36 -0
- package/dist/components/appShell.js +30 -0
- package/dist/components/calendar.d.ts +12 -3
- package/dist/components/calendar.js +2 -5
- package/dist/components/chart.d.ts +21 -0
- package/dist/components/chart.js +4 -3
- package/dist/components/chartWindow.d.ts +57 -0
- package/dist/components/chartWindow.js +136 -0
- package/dist/components/color.js +1 -1
- package/dist/components/commandPalette.d.ts +16 -2
- package/dist/components/commandPalette.js +39 -4
- package/dist/components/controlSize.d.ts +5 -0
- package/dist/components/controlSize.js +8 -0
- package/dist/components/dropdown.d.ts +5 -0
- package/dist/components/dropdown.js +2 -0
- package/dist/components/exportData.d.ts +6 -1
- package/dist/components/exportData.js +25 -18
- package/dist/components/fileTree.d.ts +67 -0
- package/dist/components/fileTree.js +141 -1
- package/dist/components/identicon.js +3 -2
- package/dist/components/logTools.d.ts +93 -0
- package/dist/components/logTools.js +210 -0
- package/dist/components/menuWhen.js +1 -1
- package/dist/components/meter.js +2 -1
- package/dist/components/pipeline.d.ts +2 -0
- package/dist/components/pipeline.js +5 -0
- package/dist/components/relativeTime.js +2 -1
- package/dist/components/reveal.js +2 -1
- package/dist/components/select.d.ts +5 -0
- package/dist/components/select.js +10 -0
- package/dist/components/sidebar.d.ts +1 -0
- package/dist/components/tableFeatures.d.ts +58 -0
- package/dist/components/tableFeatures.js +87 -0
- package/dist/components/topologyGraph.d.ts +164 -0
- package/dist/components/topologyGraph.js +416 -0
- package/dist/components/tour.js +5 -3
- package/dist/components/videoBackdrop.js +7 -6
- package/dist/composables/clamp.d.ts +2 -0
- package/dist/composables/clamp.js +4 -0
- package/dist/composables/devWarn.d.ts +3 -0
- package/dist/composables/devWarn.js +14 -0
- package/dist/composables/easing.d.ts +13 -4
- package/dist/composables/easing.js +16 -10
- package/dist/composables/overlayHistory.js +14 -1
- package/dist/composables/pointerDrag.d.ts +15 -0
- package/dist/composables/pointerDrag.js +40 -0
- package/dist/composables/propCheck.d.ts +10 -0
- package/dist/composables/propCheck.js +78 -0
- package/dist/composables/realm.d.ts +6 -0
- package/dist/composables/realm.js +23 -0
- package/dist/composables/scroll.js +4 -2
- package/dist/composables/scrollMemory.d.ts +12 -1
- package/dist/composables/scrollMemory.js +36 -12
- package/dist/composables/spatialNav.js +2 -1
- package/dist/composables/textEntry.d.ts +17 -0
- package/dist/composables/textEntry.js +19 -0
- package/dist/composables/useAsyncTask.d.ts +6 -0
- package/dist/composables/useChartSync.d.ts +5 -0
- package/dist/composables/useChartSync.js +33 -0
- package/dist/composables/useCloseGuard.d.ts +3 -1
- package/dist/composables/useCloseGuard.js +6 -3
- package/dist/composables/useCommands.d.ts +50 -0
- package/dist/composables/useCommands.js +66 -0
- package/dist/composables/useField.d.ts +37 -0
- package/dist/composables/useField.js +27 -2
- package/dist/composables/useFitScale.js +4 -1
- package/dist/composables/useFormDraft.d.ts +13 -2
- package/dist/composables/useFormDraft.js +17 -9
- package/dist/composables/useHintSystem.js +2 -2
- package/dist/composables/useHotkeys.js +11 -32
- package/dist/composables/useInView.js +2 -1
- package/dist/composables/useInputFilter.js +7 -5
- package/dist/composables/useInputMask.js +5 -3
- package/dist/composables/useLinkedView.d.ts +97 -0
- package/dist/composables/useLinkedView.js +139 -0
- package/dist/composables/useOptimisticToggle.d.ts +3 -1
- package/dist/composables/useOptimisticToggle.js +2 -1
- package/dist/composables/useOverlayHistory.d.ts +7 -0
- package/dist/composables/useOverlayHistory.js +16 -2
- package/dist/composables/usePaintSelect.js +18 -14
- package/dist/composables/usePaletteCommands.d.ts +7 -0
- package/dist/composables/usePaletteCommands.js +12 -1
- package/dist/composables/usePersisted.d.ts +10 -0
- package/dist/composables/usePersisted.js +1 -1
- package/dist/composables/usePillTransition.d.ts +2 -2
- package/dist/composables/usePillTransition.js +5 -5
- package/dist/composables/useResizeObserver.js +2 -1
- package/dist/composables/useReveal.js +2 -1
- package/dist/composables/useRovingFocus.d.ts +2 -2
- package/dist/composables/useRovingFocus.js +9 -11
- package/dist/composables/useScramble.js +7 -1
- package/dist/composables/useShift.js +8 -6
- package/dist/composables/useTilt.js +2 -1
- package/dist/composables/useTween.js +9 -5
- package/dist/composables/useView.d.ts +1 -0
- package/dist/composables/useView.js +14 -2
- package/dist/composables/useZoomPan.js +2 -4
- package/dist/composables/windowing.d.ts +40 -0
- package/dist/composables/windowing.js +118 -0
- package/dist/icons-all.js +1 -1
- package/dist/icons.d.ts +4 -0
- package/dist/index.d.ts +30 -5
- package/dist/index.js +310 -264
- package/dist/layout/tree.js +19 -15
- package/dist/layout/useLayout.d.ts +2 -0
- package/dist/layout/useLayout.js +5 -1
- package/dist/layout/useWorkspaces.d.ts +52 -0
- package/dist/layout/useWorkspaces.js +140 -0
- package/dist/{chunks/backdrop-Dkf2vM81.js → theme/backdrop.js} +5 -5
- package/dist/theme/config.js +25 -0
- package/dist/theme/define.js +61 -0
- package/dist/{chunks/palette-Co8tSASo.js → theme/palette.js} +2 -2
- package/dist/theme/presets.js +239 -0
- package/dist/theme/studio.js +134 -0
- package/dist/theme/tokens.js +63 -0
- package/dist/theme/useTheme.d.ts +3 -2
- package/dist/theme/useTheme.js +325 -0
- package/package.json +8 -1
- package/src/component-names.ts +5 -1
- package/src/components/AppShellContextNav.vue +44 -0
- package/src/components/ChartRangeSelector.vue +120 -0
- package/src/components/IconCloseButton.vue +21 -0
- package/src/components/LayoutCanvas.vue +3 -0
- package/src/components/LayoutNode.vue +37 -38
- package/src/components/LpActivityLine.vue +4 -2
- package/src/components/LpAddressInput.vue +3 -3
- package/src/components/LpAppShell.vue +362 -24
- package/src/components/LpAutocomplete.vue +13 -30
- package/src/components/LpBottomNav.vue +9 -1
- package/src/components/LpBreadcrumbs.vue +2 -1
- package/src/components/LpButton.vue +11 -4
- package/src/components/LpChart.vue +336 -37
- package/src/components/LpCheckbox.vue +43 -9
- package/src/components/LpCodeBlock.vue +17 -23
- package/src/components/LpCollapse.vue +5 -4
- package/src/components/LpColorPicker.vue +8 -9
- package/src/components/LpCommandPalette.vue +133 -50
- package/src/components/LpContextMenu.vue +15 -13
- package/src/components/LpCountdown.vue +2 -1
- package/src/components/LpDataList.vue +13 -1
- package/src/components/LpDatePicker.vue +5 -17
- package/src/components/LpDateRangePicker.vue +5 -16
- package/src/components/LpDock.vue +25 -16
- package/src/components/LpDrawer.vue +70 -61
- package/src/components/LpDropdownMenu.vue +6 -10
- package/src/components/LpDropzone.vue +12 -7
- package/src/components/LpEditable.vue +6 -2
- package/src/components/LpFileTree.vue +507 -66
- package/src/components/LpFileTreeNode.vue +106 -5
- package/src/components/LpFormField.vue +1 -1
- package/src/components/LpFrame.vue +33 -13
- package/src/components/LpHoverCard.vue +30 -5
- package/src/components/LpIcon.vue +6 -2
- package/src/components/LpIdenticon.vue +2 -1
- package/src/components/LpImageEditor.vue +5 -2
- package/src/components/LpInput.vue +15 -23
- package/src/components/LpInspector.vue +197 -0
- package/src/components/LpLightbox.vue +74 -29
- package/src/components/LpLogInspector.vue +85 -0
- package/src/components/LpLogViewer.vue +239 -29
- package/src/components/LpMapPicker.vue +2 -1
- package/src/components/LpMasonry.vue +8 -5
- package/src/components/LpModal.vue +12 -12
- package/src/components/LpModalNav.vue +2 -21
- package/src/components/LpNumberField.vue +6 -15
- package/src/components/LpOtpInput.vue +2 -2
- package/src/components/LpPagination.vue +5 -3
- package/src/components/LpPhoneInput.vue +8 -19
- package/src/components/LpPie.vue +25 -23
- package/src/components/LpPipeline.vue +14 -1
- package/src/components/LpPopover.vue +23 -2
- package/src/components/LpProgress.vue +2 -1
- package/src/components/LpRadioGroup.vue +2 -2
- package/src/components/LpRangeSlider.vue +5 -6
- package/src/components/LpRevealOnScroll.vue +1 -1
- package/src/components/LpScrollArea.vue +28 -11
- package/src/components/LpSecretField.vue +2 -1
- package/src/components/LpSegmented.vue +6 -8
- package/src/components/LpSelect.vue +54 -24
- package/src/components/LpShaderBackdrop.vue +12 -9
- package/src/components/LpShift.vue +18 -15
- package/src/components/LpSidebar.vue +77 -74
- package/src/components/LpSidebarNav.vue +3 -2
- package/src/components/LpSidebarRow.vue +2 -1
- package/src/components/LpSlider.vue +5 -6
- package/src/components/LpSpotlight.vue +2 -1
- package/src/components/LpSwitch.vue +5 -3
- package/src/components/LpTable.vue +609 -93
- package/src/components/LpTableFilter.vue +1 -1
- package/src/components/LpTableOfContents.vue +16 -9
- package/src/components/LpTabs.vue +2 -1
- package/src/components/LpTagsInput.vue +12 -19
- package/src/components/LpTextarea.vue +5 -11
- package/src/components/LpThemeProvider.vue +29 -13
- package/src/components/LpTimeField.vue +4 -7
- package/src/components/LpToaster.vue +7 -6
- package/src/components/LpTooltip.vue +62 -18
- package/src/components/LpTopologyCanvas.vue +567 -42
- package/src/components/LpTour.vue +7 -5
- package/src/components/LpTruncate.vue +4 -3
- package/src/components/LpTypewriter.vue +2 -1
- package/src/components/LpVideoBackdrop.vue +11 -13
- package/src/components/LpWindow.vue +373 -0
- package/src/components/LpWorkspaceSwitcher.vue +116 -0
- package/src/components/PickerField.vue +2 -2
- package/src/components/SliderRail.vue +10 -0
- package/src/components/address.ts +3 -1
- package/src/components/appShell.ts +74 -0
- package/src/components/calendar.ts +12 -5
- package/src/components/chart.ts +27 -3
- package/src/components/chartWindow.ts +198 -0
- package/src/components/color.ts +2 -1
- package/src/components/commandPalette.ts +66 -5
- package/src/components/controlSize.ts +6 -0
- package/src/components/dropdown.ts +3 -0
- package/src/components/exportData.ts +33 -18
- package/src/components/fileTree.ts +215 -0
- package/src/components/identicon.ts +4 -2
- package/src/components/logTools.ts +298 -0
- package/src/components/meter.ts +2 -1
- package/src/components/pipeline.ts +6 -0
- package/src/components/relativeTime.ts +2 -1
- package/src/components/reveal.ts +3 -1
- package/src/components/select.ts +14 -0
- package/src/components/sidebar.ts +1 -0
- package/src/components/tableFeatures.ts +149 -0
- package/src/components/topologyGraph.ts +655 -0
- package/src/components/tour.ts +5 -3
- package/src/components/videoBackdrop.ts +7 -6
- package/src/composables/clamp.ts +2 -0
- package/src/composables/devWarn.ts +19 -0
- package/src/composables/easing.ts +37 -12
- package/src/composables/overlayHistory.ts +24 -0
- package/src/composables/pointerDrag.ts +54 -0
- package/src/composables/propCheck.ts +90 -0
- package/src/composables/realm.ts +25 -0
- package/src/composables/scroll.ts +4 -2
- package/src/composables/scrollMemory.ts +56 -14
- package/src/composables/spatialNav.ts +2 -1
- package/src/composables/textEntry.ts +33 -0
- package/src/composables/useAsyncTask.ts +6 -0
- package/src/composables/useChartSync.ts +52 -0
- package/src/composables/useCloseGuard.ts +22 -10
- package/src/composables/useCommands.ts +123 -0
- package/src/composables/useField.ts +41 -2
- package/src/composables/useFitScale.ts +4 -1
- package/src/composables/useFormDraft.ts +38 -14
- package/src/composables/useHintSystem.ts +2 -2
- package/src/composables/useHotkeys.ts +19 -48
- package/src/composables/useInView.ts +2 -1
- package/src/composables/useInputFilter.ts +8 -5
- package/src/composables/useInputMask.ts +8 -3
- package/src/composables/useLinkedView.ts +222 -0
- package/src/composables/useOptimisticToggle.ts +7 -1
- package/src/composables/useOverlayHistory.ts +20 -0
- package/src/composables/usePaintSelect.ts +20 -14
- package/src/composables/usePaletteCommands.ts +13 -0
- package/src/composables/usePersisted.ts +10 -0
- package/src/composables/usePillTransition.ts +9 -6
- package/src/composables/useResizeObserver.ts +2 -1
- package/src/composables/useReveal.ts +2 -1
- package/src/composables/useRovingFocus.ts +11 -14
- package/src/composables/useScramble.ts +12 -1
- package/src/composables/useShift.ts +8 -6
- package/src/composables/useTilt.ts +2 -1
- package/src/composables/useTween.ts +10 -5
- package/src/composables/useView.ts +17 -3
- package/src/composables/useZoomPan.ts +2 -5
- package/src/composables/windowing.ts +190 -0
- package/src/icons.ts +27 -6
- package/src/index.ts +41 -4
- package/src/kit-icons.ts +1 -1
- package/src/layout/tree.ts +31 -16
- package/src/layout/useLayout.ts +6 -0
- package/src/layout/useWorkspaces.ts +234 -0
- package/src/nuxt.ts +9 -1
- package/src/runtime/propCheck.plugin.ts +10 -0
- package/src/theme/studio.ts +7 -5
- package/src/theme/useTheme.ts +89 -15
- package/src/tokens/tokens.css +8 -1
- package/dist/assets/LpTabs-CJ0rYQbx.css +0 -18
- package/dist/chunks/LpAppShell-Dgejipzz.js +0 -319
- package/dist/chunks/LpFileTree-CooHinsC.js +0 -425
- package/dist/chunks/LpThemeProvider-8MG6yqU9.js +0 -640
- package/dist/chunks/LpTopologyCanvas-B1gIbS0n.js +0 -298
|
@@ -23,11 +23,41 @@ import {
|
|
|
23
23
|
type Padding,
|
|
24
24
|
} from "@vue-flow/core"
|
|
25
25
|
import { MiniMap } from "@vue-flow/minimap"
|
|
26
|
-
import { computed, defineComponent, h, markRaw, ref, watch, type Component } from "vue"
|
|
26
|
+
import { computed, defineComponent, h, markRaw, nextTick, ref, shallowRef, watch, type Component } from "vue"
|
|
27
|
+
import { prefersReducedMotion } from "../composables/easing"
|
|
28
|
+
import { useLabels } from "../composables/useLpConfig"
|
|
29
|
+
import { useResizeObserver } from "../composables/useResizeObserver"
|
|
30
|
+
import { isHTMLElement, nodeWindow } from "../composables/useView"
|
|
31
|
+
import { backgroundOf, resolveCssColor, svgToPng } from "./exportData"
|
|
32
|
+
import LpButton from "./LpButton.vue"
|
|
27
33
|
import LpInfraNode, { type InfraNodeData } from "./LpInfraNode.vue"
|
|
34
|
+
import LpInput from "./LpInput.vue"
|
|
28
35
|
import LpLaneNode from "./LpLaneNode.vue"
|
|
29
36
|
import LpServiceNode, { type ServiceNodeData } from "./LpServiceNode.vue"
|
|
37
|
+
import LpSlider from "./LpSlider.vue"
|
|
30
38
|
import LpWaypointEdge from "./LpWaypointEdge.vue"
|
|
39
|
+
import {
|
|
40
|
+
absolutePositions,
|
|
41
|
+
clusterGraph,
|
|
42
|
+
collapseChildren,
|
|
43
|
+
detailAtZoom,
|
|
44
|
+
diffTopology,
|
|
45
|
+
edgeLabelText,
|
|
46
|
+
findNodes,
|
|
47
|
+
findPath,
|
|
48
|
+
frameSize,
|
|
49
|
+
groupBoxes,
|
|
50
|
+
layeredLayout,
|
|
51
|
+
nearestInDirection,
|
|
52
|
+
neighborhood,
|
|
53
|
+
nodeName,
|
|
54
|
+
topologyToSvg,
|
|
55
|
+
type DetailLevel,
|
|
56
|
+
type GraphDirection,
|
|
57
|
+
type GraphPath,
|
|
58
|
+
type LayoutOptions,
|
|
59
|
+
type TopologyDiff,
|
|
60
|
+
} from "./topologyGraph"
|
|
31
61
|
|
|
32
62
|
// Vue Flow base CSS + canvas chrome theming is shipped via the kit's
|
|
33
63
|
// "@leavepulse/ui/canvas.css" export (a plain CSS file). Component-local CSS
|
|
@@ -53,6 +83,11 @@ export interface TopologyNode {
|
|
|
53
83
|
width?: number
|
|
54
84
|
height?: number
|
|
55
85
|
label?: string
|
|
86
|
+
/**
|
|
87
|
+
* Free-form facts about the node, for `groupBy` ("region", "environment"):
|
|
88
|
+
* what the card does not show but a graph can be sorted by.
|
|
89
|
+
*/
|
|
90
|
+
meta?: Record<string, string | undefined>
|
|
56
91
|
}
|
|
57
92
|
|
|
58
93
|
/**
|
|
@@ -84,8 +119,32 @@ export interface TopologyEdge {
|
|
|
84
119
|
* the gaps are. Requires `edgeType: "waypoints"`; ignored otherwise.
|
|
85
120
|
*/
|
|
86
121
|
waypoints?: { x: number; y: number }[]
|
|
122
|
+
/** Measured round-trip time in ms, printed on the edge after its `kind`. */
|
|
123
|
+
rtt?: number
|
|
124
|
+
/** Measured packet loss in percent, printed on the edge after `rtt`. */
|
|
125
|
+
loss?: number
|
|
87
126
|
}
|
|
88
127
|
|
|
128
|
+
/** One version of the graph, for `timeline` and `compareTo`. */
|
|
129
|
+
export interface TopologySnapshot {
|
|
130
|
+
nodes: TopologyNode[]
|
|
131
|
+
edges: TopologyEdge[]
|
|
132
|
+
/** What the timeline shows for this version: a date, a deploy. */
|
|
133
|
+
label?: string
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** A button over a multi-selection; pressing it emits `selection-action`. */
|
|
137
|
+
export interface TopologySelectionAction {
|
|
138
|
+
id: string
|
|
139
|
+
label: string
|
|
140
|
+
icon?: string
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The node facts a graph can be grouped by, or a function that picks one. */
|
|
144
|
+
export type TopologyGroupBy = string | ((node: TopologyNode) => string | undefined)
|
|
145
|
+
|
|
146
|
+
export type TopologyLayoutOptions = Omit<LayoutOptions, "groupOf">
|
|
147
|
+
|
|
89
148
|
/**
|
|
90
149
|
* A background swimlane: a sized zone (one per project / overlay network) drawn
|
|
91
150
|
* beneath the nodes. The consumer computes the geometry; the canvas renders it
|
|
@@ -103,6 +162,21 @@ export interface TopologyLane {
|
|
|
103
162
|
accent?: string
|
|
104
163
|
}
|
|
105
164
|
|
|
165
|
+
/** Wording of the canvas's own controls. `{n}` is a count. */
|
|
166
|
+
export type TopologyLabels = {
|
|
167
|
+
search: string
|
|
168
|
+
searchResults: string
|
|
169
|
+
selected: string
|
|
170
|
+
timeline: string
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const TOPOLOGY_LABELS: TopologyLabels = {
|
|
174
|
+
search: "Find a node",
|
|
175
|
+
searchResults: "{n} found",
|
|
176
|
+
selected: "{n} selected",
|
|
177
|
+
timeline: "Version",
|
|
178
|
+
}
|
|
179
|
+
|
|
106
180
|
/** Canvas viewport transform — for restoring a saved pan/zoom. */
|
|
107
181
|
export interface CanvasViewport {
|
|
108
182
|
x: number
|
|
@@ -155,6 +229,50 @@ const props = withDefaults(
|
|
|
155
229
|
*/
|
|
156
230
|
minimap?: boolean
|
|
157
231
|
controls?: boolean
|
|
232
|
+
/** Hide edges this returns false for, by kind, state or anything else. */
|
|
233
|
+
edgeFilter?: (edge: TopologyEdge) => boolean
|
|
234
|
+
/** Picking a node fades everything not directly connected to it. */
|
|
235
|
+
highlightNeighbors?: boolean
|
|
236
|
+
/**
|
|
237
|
+
* Two node ids: the shortest path between them stands out, the rest fades.
|
|
238
|
+
* The result arrives in `path-change`. Goes by `directedPath` and by what
|
|
239
|
+
* `edgeFilter` leaves visible.
|
|
240
|
+
*/
|
|
241
|
+
path?: [string, string]
|
|
242
|
+
/** Walk the path along edge direction only. Default: a wire goes both ways. */
|
|
243
|
+
directedPath?: boolean
|
|
244
|
+
/**
|
|
245
|
+
* Draw a lane around each group of nodes, keyed by a `meta` field, a `data`
|
|
246
|
+
* field or a function. `autoLayout` then keeps the groups apart.
|
|
247
|
+
*/
|
|
248
|
+
groupBy?: TopologyGroupBy
|
|
249
|
+
/** A search box that finds a node by name or id and brings it into view. */
|
|
250
|
+
search?: boolean
|
|
251
|
+
/** Arrow keys walk between nodes instead of nudging the focused one. */
|
|
252
|
+
keyboardNav?: boolean
|
|
253
|
+
/** Buttons shown while two or more nodes are selected. */
|
|
254
|
+
selectionActions?: TopologySelectionAction[]
|
|
255
|
+
/** An earlier version: what differs is marked on the graph drawn now. */
|
|
256
|
+
compareTo?: TopologySnapshot
|
|
257
|
+
/**
|
|
258
|
+
* Versions of the graph, oldest first: a slider steps through them and
|
|
259
|
+
* each is drawn with what changed since the one before. `nodes` and `edges`
|
|
260
|
+
* are not drawn meanwhile.
|
|
261
|
+
*/
|
|
262
|
+
timeline?: TopologySnapshot[]
|
|
263
|
+
/** Fit the graph again whenever the canvas changes size. Default "never": the view is the user's. */
|
|
264
|
+
fitOnResize?: "never" | "always"
|
|
265
|
+
/** Change it when a new dataset arrives, to fit the graph to it once. */
|
|
266
|
+
dataRevision?: string | number
|
|
267
|
+
/** Grow each group frame to hold its services, however the data sized it. */
|
|
268
|
+
autoSizeGroups?: boolean
|
|
269
|
+
/**
|
|
270
|
+
* How much to draw. "simple" leaves services out, with their edges moved to
|
|
271
|
+
* their hosts; "overview" folds each `groupBy` group into one node; "auto"
|
|
272
|
+
* picks by zoom. Default "full".
|
|
273
|
+
*/
|
|
274
|
+
detail?: DetailLevel | "auto"
|
|
275
|
+
labels?: Partial<TopologyLabels>
|
|
158
276
|
}>(),
|
|
159
277
|
{
|
|
160
278
|
connectable: false,
|
|
@@ -164,9 +282,23 @@ const props = withDefaults(
|
|
|
164
282
|
lanes: () => [],
|
|
165
283
|
minimap: true,
|
|
166
284
|
controls: true,
|
|
285
|
+
edgeFilter: undefined,
|
|
286
|
+
path: undefined,
|
|
287
|
+
groupBy: undefined,
|
|
288
|
+
selectionActions: undefined,
|
|
289
|
+
compareTo: undefined,
|
|
290
|
+
timeline: undefined,
|
|
291
|
+
fitOnResize: "never",
|
|
292
|
+
dataRevision: undefined,
|
|
293
|
+
detail: "full",
|
|
294
|
+
labels: undefined,
|
|
167
295
|
},
|
|
168
296
|
)
|
|
169
297
|
|
|
298
|
+
const text = useLabels("topology", TOPOLOGY_LABELS, () => props.labels)
|
|
299
|
+
/** Index into `timeline`; unset follows the newest version. */
|
|
300
|
+
const timelineIndex = defineModel<number | undefined>("timelineIndex", { default: undefined })
|
|
301
|
+
|
|
170
302
|
const emit = defineEmits<{
|
|
171
303
|
(e: "connect", value: Connection): void
|
|
172
304
|
(e: "node-select", id: string): void
|
|
@@ -183,6 +315,10 @@ const emit = defineEmits<{
|
|
|
183
315
|
e: "node-drag-stop",
|
|
184
316
|
value: { id: string; position: { x: number; y: number } },
|
|
185
317
|
): void
|
|
318
|
+
/** The path asked for by `path`: null when the two are not connected. */
|
|
319
|
+
(e: "path-change", value: GraphPath | null): void
|
|
320
|
+
/** A `selectionActions` button was pressed for these nodes. */
|
|
321
|
+
(e: "selection-action", value: { action: string; ids: string[] }): void
|
|
186
322
|
/** Pan/zoom settled — the new viewport (for per-user persistence). */
|
|
187
323
|
(
|
|
188
324
|
e: "viewport-change",
|
|
@@ -214,11 +350,159 @@ const nodeTypes: NodeTypesObject = {
|
|
|
214
350
|
// where the gaps are; with none it falls back to the plain bezier.
|
|
215
351
|
const edgeTypes = { waypoints: markRaw(LpWaypointEdge) as Component }
|
|
216
352
|
|
|
353
|
+
const {
|
|
354
|
+
onNodeClick,
|
|
355
|
+
onNodeDoubleClick,
|
|
356
|
+
onNodeContextMenu,
|
|
357
|
+
onEdgeContextMenu,
|
|
358
|
+
onPaneClick,
|
|
359
|
+
onConnect,
|
|
360
|
+
onNodeDragStop,
|
|
361
|
+
onMoveEnd,
|
|
362
|
+
getSelectedNodes,
|
|
363
|
+
fitView,
|
|
364
|
+
setViewport,
|
|
365
|
+
onNodesInitialized,
|
|
366
|
+
vueFlowRef,
|
|
367
|
+
viewport,
|
|
368
|
+
findNode,
|
|
369
|
+
getNodes,
|
|
370
|
+
addSelectedNodes,
|
|
371
|
+
removeSelectedNodes,
|
|
372
|
+
} = useVueFlow()
|
|
373
|
+
|
|
374
|
+
// What is drawn: the chosen version from the timeline, or the graph as given.
|
|
375
|
+
const versionIndex = computed(() => {
|
|
376
|
+
const last = (props.timeline?.length ?? 0) - 1
|
|
377
|
+
return Math.max(0, Math.min(timelineIndex.value ?? last, last))
|
|
378
|
+
})
|
|
379
|
+
const current = computed<TopologySnapshot>(
|
|
380
|
+
() => props.timeline?.[versionIndex.value] ?? { nodes: props.nodes, edges: props.edges },
|
|
381
|
+
)
|
|
382
|
+
const baseline = computed(() => (props.timeline ? props.timeline[versionIndex.value - 1] : props.compareTo))
|
|
383
|
+
const diff = computed<TopologyDiff | null>(() => (baseline.value ? diffTopology(baseline.value, current.value) : null))
|
|
384
|
+
|
|
385
|
+
/*
|
|
386
|
+
* A redraw hands Vue Flow every node again, position included, and highlighting
|
|
387
|
+
* redraws on a click. Where the user dragged a node stands until the host
|
|
388
|
+
* sends new data, so a host that does not persist positions keeps them too.
|
|
389
|
+
*/
|
|
390
|
+
const moved = shallowRef(new Map<string, { x: number; y: number }>())
|
|
391
|
+
watch(() => current.value.nodes, () => (moved.value = new Map()))
|
|
392
|
+
const placed = computed(() =>
|
|
393
|
+
moved.value.size
|
|
394
|
+
? current.value.nodes.map((n) => (moved.value.has(n.id) ? { ...n, position: moved.value.get(n.id)! } : n))
|
|
395
|
+
: current.value.nodes,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
function groupOf(node: TopologyNode): string | undefined {
|
|
399
|
+
const by = props.groupBy
|
|
400
|
+
if (typeof by === "function") return by(node)
|
|
401
|
+
if (!by) return undefined
|
|
402
|
+
const value = node.meta?.[by] ?? (node.data as unknown as Record<string, unknown>)[by]
|
|
403
|
+
return typeof value === "string" && value ? value : undefined
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// With the removed parts of `diff` put back, so what went is seen going.
|
|
407
|
+
const versioned = computed(() => {
|
|
408
|
+
const edges = props.edgeFilter ? current.value.edges.filter(props.edgeFilter) : current.value.edges
|
|
409
|
+
const was = baseline.value
|
|
410
|
+
if (!diff.value || !was) return { nodes: placed.value, edges }
|
|
411
|
+
const goneNodes = new Set(diff.value.removedNodes)
|
|
412
|
+
const goneEdges = new Set(diff.value.removedEdges)
|
|
413
|
+
return {
|
|
414
|
+
nodes: [...placed.value, ...was.nodes.filter((n) => goneNodes.has(n.id))],
|
|
415
|
+
edges: [...edges, ...was.edges.filter((e) => goneEdges.has(e.id) && (props.edgeFilter?.(e) ?? true))],
|
|
416
|
+
}
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
const detailLevel = computed<DetailLevel>(() => {
|
|
420
|
+
if (props.detail === "auto") return detailAtZoom(viewport.value.zoom, !!props.groupBy)
|
|
421
|
+
return props.detail === "overview" && !props.groupBy ? "simple" : props.detail
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
// Merged edges stand in for the ones they fold: the first lends its look.
|
|
425
|
+
function foldedEdges(merged: ReturnType<typeof collapseChildren>["edges"], all: TopologyEdge[]): TopologyEdge[] {
|
|
426
|
+
const byId = new Map(all.map((e) => [e.id, e]))
|
|
427
|
+
return merged.map((m) => {
|
|
428
|
+
const first = byId.get(m.members[0]!)!
|
|
429
|
+
const kind = m.members.length > 1 ? `${first.kind ?? ""} ×${m.members.length}`.trim() : first.kind
|
|
430
|
+
return { ...first, id: m.id, source: m.source, target: m.target, kind, waypoints: undefined }
|
|
431
|
+
})
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const shown = computed<{ nodes: TopologyNode[]; edges: TopologyEdge[] }>(() => {
|
|
435
|
+
const { nodes, edges } = versioned.value
|
|
436
|
+
if (detailLevel.value === "full") return { nodes, edges }
|
|
437
|
+
if (detailLevel.value === "simple") {
|
|
438
|
+
const folded = collapseChildren(nodes, edges)
|
|
439
|
+
return { nodes: folded.nodes, edges: foldedEdges(folded.edges, edges) }
|
|
440
|
+
}
|
|
441
|
+
const graph = clusterGraph(nodes, edges, groupOf)
|
|
442
|
+
const clusters = graph.clusters.map((c): TopologyNode => ({
|
|
443
|
+
id: c.id,
|
|
444
|
+
position: c.position,
|
|
445
|
+
data: { name: `${c.key} (${c.members.length})`, role: "cluster" },
|
|
446
|
+
}))
|
|
447
|
+
return { nodes: [...graph.nodes, ...clusters], edges: foldedEdges(graph.edges, edges) }
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
const query = ref("")
|
|
451
|
+
const matches = computed(() => findNodes(shown.value.nodes, query.value))
|
|
452
|
+
const focused = ref<string | null>(null)
|
|
453
|
+
|
|
454
|
+
const pathFound = computed(() => {
|
|
455
|
+
if (!props.path) return null
|
|
456
|
+
return findPath(shown.value.edges, props.path[0], props.path[1], { directed: props.directedPath })
|
|
457
|
+
})
|
|
458
|
+
// By route, not by object: `:path="[a, b]"` in a template is a new array on each
|
|
459
|
+
// render, and an answer per render would loop if the host renders what it hears.
|
|
460
|
+
watch(() => JSON.stringify(pathFound.value), () => emit("path-change", pathFound.value), { immediate: !!props.path })
|
|
461
|
+
|
|
462
|
+
// What stands out, when something does. Nothing here means the graph as it is.
|
|
463
|
+
const emphasis = computed(() => {
|
|
464
|
+
const picked: GraphPath | null =
|
|
465
|
+
pathFound.value ??
|
|
466
|
+
(matches.value.length ? { nodes: matches.value.map((n) => n.id), edges: [] } : null) ??
|
|
467
|
+
(props.highlightNeighbors && focused.value ? neighborhood(shown.value.edges, focused.value) : null)
|
|
468
|
+
if (!picked) return null
|
|
469
|
+
const lit = new Set(picked.nodes)
|
|
470
|
+
// A frame stays lit around a service that is, or the service fades with it.
|
|
471
|
+
for (const n of shown.value.nodes) if (n.parent && lit.has(n.id)) lit.add(n.parent)
|
|
472
|
+
return { nodes: lit, edges: new Set(picked.edges), path: !!pathFound.value }
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
function diffState(id: string, added: string[] | undefined, changed: string[] | undefined, removed: string[] | undefined) {
|
|
476
|
+
if (added?.includes(id)) return "added"
|
|
477
|
+
if (removed?.includes(id)) return "removed"
|
|
478
|
+
return changed?.includes(id) ? "changed" : null
|
|
479
|
+
}
|
|
480
|
+
const GROUP_WIDTH = 260
|
|
481
|
+
const GROUP_HEIGHT = 160
|
|
482
|
+
const PATH_EXTRA_WIDTH = 1.5
|
|
483
|
+
const REMOVED_DASH = "3 5"
|
|
484
|
+
const DIFF_COLOR = {
|
|
485
|
+
added: "var(--color-action)",
|
|
486
|
+
changed: "var(--color-accent)",
|
|
487
|
+
removed: "var(--color-danger)",
|
|
488
|
+
} as const
|
|
489
|
+
|
|
217
490
|
// Lanes render as non-interactive Vue Flow nodes BENEATH the graph: prepended
|
|
218
491
|
// so they paint first, with selecting/dragging/connecting off and a low z so
|
|
219
492
|
// real nodes always sit on top. They pan/zoom with the viewport for free.
|
|
493
|
+
// Measured where it has been drawn: a card's size is the card's own business.
|
|
494
|
+
function sized(node: TopologyNode): TopologyNode {
|
|
495
|
+
const measured = findNode(node.id)?.dimensions
|
|
496
|
+
return { ...node, width: node.width ?? (measured?.width || undefined), height: node.height ?? (measured?.height || undefined) }
|
|
497
|
+
}
|
|
498
|
+
const allLanes = computed<TopologyLane[]>(() => {
|
|
499
|
+
if (!props.groupBy || detailLevel.value === "overview") return props.lanes
|
|
500
|
+
const groups = groupBoxes(shown.value.nodes.map(sized), (n) => groupOf(n as TopologyNode))
|
|
501
|
+
return [...props.lanes, ...groups.map(({ key, box }) => ({ id: `group-${key}`, label: key, ...box }))]
|
|
502
|
+
})
|
|
503
|
+
|
|
220
504
|
const laneNodes = computed<Node[]>(() =>
|
|
221
|
-
|
|
505
|
+
allLanes.value.map(
|
|
222
506
|
(l): Node => ({
|
|
223
507
|
id: `lane-${l.id}`,
|
|
224
508
|
type: "lane",
|
|
@@ -234,25 +518,37 @@ const laneNodes = computed<Node[]>(() =>
|
|
|
234
518
|
)
|
|
235
519
|
|
|
236
520
|
const graphNodes = computed<Node[]>(() =>
|
|
237
|
-
|
|
521
|
+
shown.value.nodes.map((n): Node => {
|
|
238
522
|
const type = n.type ?? "infra"
|
|
523
|
+
const faded = !!emphasis.value && !emphasis.value.nodes.has(n.id)
|
|
524
|
+
const state = diffState(n.id, diff.value?.addedNodes, diff.value?.changedNodes, diff.value?.removedNodes)
|
|
239
525
|
const node: Node = {
|
|
240
526
|
id: n.id,
|
|
241
527
|
type,
|
|
242
528
|
position: n.position,
|
|
243
|
-
data: n.data,
|
|
529
|
+
data: faded ? { ...n.data, dimmed: true } : n.data,
|
|
244
530
|
zIndex: 1,
|
|
531
|
+
ariaLabel: props.keyboardNav ? nodeName(n) : undefined,
|
|
245
532
|
...(n.parent ? { parentNode: n.parent } : {}),
|
|
533
|
+
...(n.parent && props.autoSizeGroups ? { expandParent: true } : {}),
|
|
534
|
+
}
|
|
535
|
+
if (state) {
|
|
536
|
+
node.style = { outline: `2px solid ${DIFF_COLOR[state]}`, outlineOffset: "2px", borderRadius: "var(--radius-card)" }
|
|
246
537
|
}
|
|
247
538
|
if (type === "group") {
|
|
248
539
|
// A host frame: a Vue Flow group node sized to hold its services.
|
|
540
|
+
const given = { width: n.width ?? GROUP_WIDTH, height: n.height ?? GROUP_HEIGHT }
|
|
541
|
+
const frame = props.autoSizeGroups
|
|
542
|
+
? frameSize(shown.value.nodes.filter((child) => child.parent === n.id).map(sized), { minWidth: given.width, minHeight: given.height })
|
|
543
|
+
: given
|
|
249
544
|
node.style = {
|
|
250
|
-
|
|
251
|
-
|
|
545
|
+
...node.style,
|
|
546
|
+
width: `${frame.width}px`,
|
|
547
|
+
height: `${frame.height}px`,
|
|
252
548
|
background: "color-mix(in srgb, var(--color-surface-soft) 60%, transparent)",
|
|
253
549
|
border: "1px solid var(--color-line)",
|
|
254
550
|
borderRadius: "var(--radius-card)",
|
|
255
|
-
opacity: (n.data as { dimmed?: boolean }).dimmed ? "0.35" : "1",
|
|
551
|
+
opacity: faded || (n.data as { dimmed?: boolean }).dimmed ? "0.35" : "1",
|
|
256
552
|
transition: "opacity var(--duration-fast)",
|
|
257
553
|
}
|
|
258
554
|
node.label = n.label ?? n.data.name
|
|
@@ -288,54 +584,49 @@ function edgeDash(cat: EdgeCategory, o: EdgeObserved): string {
|
|
|
288
584
|
}
|
|
289
585
|
|
|
290
586
|
const flowEdges = computed(() =>
|
|
291
|
-
|
|
587
|
+
shown.value.edges.map((e) => {
|
|
292
588
|
const obs = e.observed ?? "applied"
|
|
293
589
|
const cat = edgeCategory(e)
|
|
294
|
-
const
|
|
590
|
+
const state = diffState(e.id, diff.value?.addedEdges, diff.value?.changedEdges, diff.value?.removedEdges)
|
|
591
|
+
const color = state ? DIFF_COLOR[state] : edgeColor(cat, obs)
|
|
592
|
+
const lit = emphasis.value?.edges.has(e.id) ?? false
|
|
593
|
+
const faded = e.dimmed || (!!emphasis.value && !lit)
|
|
295
594
|
return {
|
|
296
595
|
id: e.id,
|
|
297
596
|
source: e.source,
|
|
298
597
|
target: e.target,
|
|
299
598
|
type: props.edgeType ?? "default",
|
|
300
599
|
data: { waypoints: e.waypoints },
|
|
301
|
-
animated: obs !== "applied" && !
|
|
302
|
-
label: e
|
|
600
|
+
animated: obs !== "applied" && !faded,
|
|
601
|
+
label: edgeLabelText(e),
|
|
303
602
|
labelBgStyle: { fill: "var(--color-surface)" },
|
|
304
603
|
labelStyle: { fill: "var(--color-muted-strong)", fontSize: "10px" },
|
|
305
604
|
style: {
|
|
306
605
|
stroke: color,
|
|
307
|
-
strokeWidth: cat === "structural" ? 1.5 : 2,
|
|
308
|
-
strokeDasharray: edgeDash(cat, obs),
|
|
606
|
+
strokeWidth: (cat === "structural" ? 1.5 : 2) + (lit && emphasis.value?.path ? PATH_EXTRA_WIDTH : 0),
|
|
607
|
+
strokeDasharray: state === "removed" ? REMOVED_DASH : edgeDash(cat, obs),
|
|
309
608
|
strokeLinecap: "round" as const,
|
|
310
|
-
opacity:
|
|
609
|
+
opacity: faded ? 0.12 : 1,
|
|
311
610
|
},
|
|
312
611
|
markerEnd: { type: MarkerType.ArrowClosed, color },
|
|
313
612
|
}
|
|
314
613
|
}),
|
|
315
614
|
)
|
|
316
615
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
onEdgeContextMenu,
|
|
322
|
-
onPaneClick,
|
|
323
|
-
onConnect,
|
|
324
|
-
onNodeDragStop,
|
|
325
|
-
onMoveEnd,
|
|
326
|
-
getSelectedNodes,
|
|
327
|
-
fitView,
|
|
328
|
-
setViewport,
|
|
329
|
-
onNodesInitialized,
|
|
330
|
-
} = useVueFlow()
|
|
331
|
-
onNodeClick(({ node }) => emit("node-select", node.id))
|
|
616
|
+
onNodeClick(({ node }) => {
|
|
617
|
+
focused.value = node.id
|
|
618
|
+
emit("node-select", node.id)
|
|
619
|
+
})
|
|
332
620
|
// A node usually has two levels of interest — pick it out, or open it — and a
|
|
333
621
|
// single click cannot carry both. Hosts that only need one can ignore this.
|
|
334
622
|
onNodeDoubleClick(({ node }) => emit("node-activate", node.id))
|
|
335
623
|
// Drag settled → surface the node's final position so the host can persist it.
|
|
336
|
-
onNodeDragStop(({ node }) =>
|
|
337
|
-
|
|
338
|
-
)
|
|
624
|
+
onNodeDragStop(({ node, nodes }) => {
|
|
625
|
+
const next = new Map(moved.value)
|
|
626
|
+
for (const dragged of nodes.length ? nodes : [node]) next.set(dragged.id, { ...dragged.position })
|
|
627
|
+
moved.value = next
|
|
628
|
+
emit("node-drag-stop", { id: node.id, position: { ...node.position } })
|
|
629
|
+
})
|
|
339
630
|
// Pan/zoom settled → surface the viewport so the host can persist it per user.
|
|
340
631
|
onMoveEnd(({ flowTransform }) => {
|
|
341
632
|
if (flowTransform)
|
|
@@ -388,21 +679,27 @@ onEdgeContextMenu(({ edge, event }) => {
|
|
|
388
679
|
e.preventDefault?.()
|
|
389
680
|
emit("edge-contextmenu", { id: edge.id, x: e.clientX, y: e.clientY })
|
|
390
681
|
})
|
|
391
|
-
onPaneClick(() =>
|
|
682
|
+
onPaneClick(() => {
|
|
683
|
+
focused.value = null
|
|
684
|
+
emit("pane-click")
|
|
685
|
+
})
|
|
392
686
|
onConnect((conn) => emit("connect", conn))
|
|
393
687
|
|
|
394
688
|
// The legend and the minimap float over the canvas, so a plain fit puts the
|
|
395
689
|
// outermost nodes under them. Fitting reserves their corner instead: the side
|
|
396
690
|
// they sit on gets their height on top of the usual margin. On a phone both
|
|
397
|
-
// are hidden and only the margin is left.
|
|
691
|
+
// are hidden and only the margin is left. What is reserved is what is on
|
|
692
|
+
// screen, and they go by the canvas's own width: a canvas in a narrow panel of
|
|
693
|
+
// a wide window is a phone-sized canvas too.
|
|
398
694
|
const legendBox = ref<HTMLElement | null>(null)
|
|
399
695
|
const MINIMAP_HEIGHT = 165
|
|
400
696
|
const FRAME = 24
|
|
401
697
|
function framePadding(): Padding {
|
|
402
|
-
const
|
|
403
|
-
const legendHeight =
|
|
698
|
+
const legendShown = legendBox.value?.offsetHeight ?? 0
|
|
699
|
+
const legendHeight = legendShown ? legendShown + 15 : 0
|
|
700
|
+
const minimap = vueFlowRef.value?.querySelector<HTMLElement>(".vue-flow__minimap")
|
|
404
701
|
const legendTop = props.legendPosition.startsWith("top")
|
|
405
|
-
const bottom = Math.max(legendTop ? 0 : legendHeight,
|
|
702
|
+
const bottom = Math.max(legendTop ? 0 : legendHeight, minimap?.offsetHeight ? MINIMAP_HEIGHT : 0)
|
|
406
703
|
return {
|
|
407
704
|
top: `${FRAME + (legendTop ? legendHeight : 0)}px`,
|
|
408
705
|
bottom: `${FRAME + bottom}px`,
|
|
@@ -413,6 +710,15 @@ function framePadding(): Padding {
|
|
|
413
710
|
function fitFramed(params: Parameters<typeof fitView>[0] = {}) {
|
|
414
711
|
return fitView({ padding: framePadding(), ...params })
|
|
415
712
|
}
|
|
713
|
+
// Read when the button is pressed: the panels do not exist yet at first render.
|
|
714
|
+
const controlsFit = {
|
|
715
|
+
get padding() {
|
|
716
|
+
return framePadding()
|
|
717
|
+
},
|
|
718
|
+
}
|
|
719
|
+
// The canvas fits itself once, on the first nodes: after that the view is the
|
|
720
|
+
// user's. A host whose data arrives in stages, or whose box changed size, calls
|
|
721
|
+
// the exposed `fitView` when it wants the whole graph back in frame.
|
|
416
722
|
let fitted = false
|
|
417
723
|
onNodesInitialized(() => {
|
|
418
724
|
if (fitted) return
|
|
@@ -426,7 +732,178 @@ async function applyViewport(v: CanvasViewport): Promise<void> {
|
|
|
426
732
|
await setViewport({ x: v.x, y: v.y, zoom: v.zoom })
|
|
427
733
|
}
|
|
428
734
|
|
|
429
|
-
|
|
735
|
+
function refit() {
|
|
736
|
+
void nextTick(() => {
|
|
737
|
+
const root = vueFlowRef.value
|
|
738
|
+
if (root) nodeWindow(root).requestAnimationFrame(() => void fitFramed())
|
|
739
|
+
})
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// A new dataset is the one time the canvas takes the view back. The flag also
|
|
743
|
+
// covers data that lands a moment later and has to be measured first.
|
|
744
|
+
watch(() => props.dataRevision, () => {
|
|
745
|
+
fitted = false
|
|
746
|
+
refit()
|
|
747
|
+
})
|
|
748
|
+
|
|
749
|
+
let resizeFrame = 0
|
|
750
|
+
useResizeObserver(vueFlowRef, (_, el) => {
|
|
751
|
+
if (props.fitOnResize !== "always" || !fitted || resizeFrame) return
|
|
752
|
+
resizeFrame = nodeWindow(el).requestAnimationFrame(() => {
|
|
753
|
+
resizeFrame = 0
|
|
754
|
+
void fitFramed()
|
|
755
|
+
})
|
|
756
|
+
})
|
|
757
|
+
|
|
758
|
+
const FOCUS_MAX_ZOOM = 1.2
|
|
759
|
+
const FOCUS_DURATION_MS = 300
|
|
760
|
+
|
|
761
|
+
function nodeElement(id: string): HTMLElement | null {
|
|
762
|
+
const root = vueFlowRef.value
|
|
763
|
+
if (!root) return null
|
|
764
|
+
return root.querySelector<HTMLElement>(`.vue-flow__node[data-id="${nodeWindow(root).CSS.escape(id)}"]`)
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/** Bring a node into view and pick it. False when it is not on the canvas. */
|
|
768
|
+
async function focusNode(id: string, options: { keepFocus?: boolean } = {}): Promise<boolean> {
|
|
769
|
+
if (!findNode(id)) return false
|
|
770
|
+
focused.value = id
|
|
771
|
+
emit("node-select", id)
|
|
772
|
+
await fitView({
|
|
773
|
+
nodes: [id],
|
|
774
|
+
maxZoom: FOCUS_MAX_ZOOM,
|
|
775
|
+
padding: framePadding(),
|
|
776
|
+
duration: prefersReducedMotion() ? 0 : FOCUS_DURATION_MS,
|
|
777
|
+
})
|
|
778
|
+
if (!options.keepFocus) nodeElement(id)?.focus()
|
|
779
|
+
return true
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
let matchCursor = 0
|
|
783
|
+
watch(query, () => (matchCursor = 0))
|
|
784
|
+
function nextMatch() {
|
|
785
|
+
const hits = matches.value
|
|
786
|
+
if (!hits.length) return
|
|
787
|
+
void focusNode(hits[matchCursor++ % hits.length]!.id, { keepFocus: true })
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const selectedIds = computed(() => getSelectedNodes.value.map((n) => n.id))
|
|
791
|
+
|
|
792
|
+
const ARROWS: Record<string, GraphDirection> = {
|
|
793
|
+
ArrowUp: "up",
|
|
794
|
+
ArrowDown: "down",
|
|
795
|
+
ArrowLeft: "left",
|
|
796
|
+
ArrowRight: "right",
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// In the capture phase: Vue Flow's own arrow keys nudge the focused node.
|
|
800
|
+
function onKeydown(event: KeyboardEvent) {
|
|
801
|
+
if (!props.keyboardNav || !isHTMLElement(event.target)) return
|
|
802
|
+
const id = event.target.closest(".vue-flow__node")?.getAttribute("data-id")
|
|
803
|
+
if (!id) return
|
|
804
|
+
const direction = ARROWS[event.key]
|
|
805
|
+
if (direction) {
|
|
806
|
+
const next = nearestInDirection(absolutePositions(shown.value.nodes), id, direction)
|
|
807
|
+
if (next) void focusNode(next)
|
|
808
|
+
} else if (event.key === "Enter") emit("node-activate", id)
|
|
809
|
+
else if (event.key === " ") {
|
|
810
|
+
focused.value = id
|
|
811
|
+
emit("node-select", id)
|
|
812
|
+
} else return
|
|
813
|
+
event.preventDefault()
|
|
814
|
+
event.stopPropagation()
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/* Wider than a lane's padding and label together, so group boxes stay apart. */
|
|
818
|
+
const LANE_BAND_GAP = 104
|
|
819
|
+
const OFFSCREEN = "position:absolute;left:-99999px;top:0;visibility:hidden"
|
|
820
|
+
const EXPORT_NODE_WIDTH = 220
|
|
821
|
+
const EXPORT_NODE_HEIGHT = 96
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Lay the graph out in layers, group by group under `groupBy`. The canvas moves
|
|
825
|
+
* at once; `update:nodes` carries the new positions for a host to keep.
|
|
826
|
+
*/
|
|
827
|
+
function autoLayout(options: TopologyLayoutOptions = {}) {
|
|
828
|
+
const positions = layeredLayout(placed.value.map(sized), current.value.edges, {
|
|
829
|
+
groupGap: LANE_BAND_GAP,
|
|
830
|
+
...options,
|
|
831
|
+
groupOf: props.groupBy ? (n) => groupOf(n as TopologyNode) : undefined,
|
|
832
|
+
})
|
|
833
|
+
moved.value = new Map([...moved.value, ...positions])
|
|
834
|
+
emit("update:nodes", current.value.nodes.map((n) => {
|
|
835
|
+
const position = positions.get(n.id)
|
|
836
|
+
return position ? { ...n, position } : n
|
|
837
|
+
}))
|
|
838
|
+
refit()
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/** The graph as a clean SVG: boxes, wires and names, not a capture of the cards. */
|
|
842
|
+
function exportSvg(): string | null {
|
|
843
|
+
const root = vueFlowRef.value
|
|
844
|
+
if (!root) return null
|
|
845
|
+
const places = absolutePositions(shown.value.nodes)
|
|
846
|
+
const color = (token: string) => resolveCssColor(`var(${token})`, root)
|
|
847
|
+
return topologyToSvg(
|
|
848
|
+
shown.value.nodes.map((n) => {
|
|
849
|
+
const size = sized(n)
|
|
850
|
+
return {
|
|
851
|
+
...places.get(n.id)!,
|
|
852
|
+
id: n.id,
|
|
853
|
+
label: nodeName(n),
|
|
854
|
+
frame: n.type === "group",
|
|
855
|
+
width: size.width ?? EXPORT_NODE_WIDTH,
|
|
856
|
+
height: size.height ?? EXPORT_NODE_HEIGHT,
|
|
857
|
+
}
|
|
858
|
+
}),
|
|
859
|
+
shown.value.edges.map((e) => {
|
|
860
|
+
const obs = e.observed ?? "applied"
|
|
861
|
+
const cat = edgeCategory(e)
|
|
862
|
+
return { ...e, label: edgeLabelText(e), dash: edgeDash(cat, obs), color: resolveCssColor(edgeColor(cat, obs), root) }
|
|
863
|
+
}),
|
|
864
|
+
{
|
|
865
|
+
background: backgroundOf(root) ?? color("--color-surface"),
|
|
866
|
+
surface: color("--color-surface-raised"),
|
|
867
|
+
line: color("--color-line-strong"),
|
|
868
|
+
text: color("--color-ink"),
|
|
869
|
+
edge: color("--color-brand"),
|
|
870
|
+
},
|
|
871
|
+
)
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/** The same drawing as a PNG. */
|
|
875
|
+
async function exportPng(): Promise<Blob | null> {
|
|
876
|
+
const svg = exportSvg()
|
|
877
|
+
const root = vueFlowRef.value
|
|
878
|
+
if (!svg || !root) return null
|
|
879
|
+
const parsed = new (nodeWindow(root).DOMParser)().parseFromString(svg, "image/svg+xml")
|
|
880
|
+
const drawing = root.ownerDocument.importNode(parsed.documentElement, true) as unknown as SVGSVGElement
|
|
881
|
+
// In the page for as long as it takes: the rasteriser measures what it draws.
|
|
882
|
+
drawing.style.cssText = OFFSCREEN
|
|
883
|
+
root.append(drawing)
|
|
884
|
+
try {
|
|
885
|
+
return await svgToPng(drawing)
|
|
886
|
+
} finally {
|
|
887
|
+
drawing.remove()
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/** Replace the marquee selection, for a host driving it from a list. */
|
|
892
|
+
function selectNodes(ids: string[]) {
|
|
893
|
+
removeSelectedNodes(getSelectedNodes.value)
|
|
894
|
+
addSelectedNodes(getNodes.value.filter((n) => ids.includes(n.id)))
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
defineExpose({
|
|
898
|
+
fitView: fitFramed,
|
|
899
|
+
setViewport: applyViewport,
|
|
900
|
+
focusNode,
|
|
901
|
+
searchNodes: (query: string) => findNodes(shown.value.nodes, query).map((n) => n.id),
|
|
902
|
+
autoLayout,
|
|
903
|
+
exportSvg,
|
|
904
|
+
exportPng,
|
|
905
|
+
selectNodes,
|
|
906
|
+
})
|
|
430
907
|
</script>
|
|
431
908
|
|
|
432
909
|
<template>
|
|
@@ -441,10 +918,11 @@ defineExpose({ fitView: fitFramed, setViewport: applyViewport })
|
|
|
441
918
|
:selection-key-code="selectable ? true : null"
|
|
442
919
|
multi-selection-key-code="Shift"
|
|
443
920
|
:selection-mode="SelectionMode.Partial"
|
|
921
|
+
@keydown.capture="onKeydown"
|
|
444
922
|
:default-viewport="{ zoom: 0.9 }"
|
|
445
923
|
:min-zoom="0.3"
|
|
446
924
|
:max-zoom="2"
|
|
447
|
-
class="lp-topology"
|
|
925
|
+
class="lp-topology @container"
|
|
448
926
|
>
|
|
449
927
|
<Background :gap="22" :size="1.4" pattern-color="var(--color-line-strong)" />
|
|
450
928
|
<!-- On a phone the minimap, the zoom controls and the legend would all
|
|
@@ -452,18 +930,65 @@ defineExpose({ fitView: fitFramed, setViewport: applyViewport })
|
|
|
452
930
|
other and the graph; pinch-zoom and panning do the minimap's job there. -->
|
|
453
931
|
<MiniMap
|
|
454
932
|
v-if="minimap"
|
|
455
|
-
class="max-
|
|
933
|
+
class="@max-[40rem]:!hidden"
|
|
456
934
|
pannable
|
|
457
935
|
zoomable
|
|
458
936
|
node-color="var(--color-surface-soft)"
|
|
459
937
|
mask-color="rgba(0,0,0,0.55)"
|
|
460
938
|
/>
|
|
461
|
-
<Controls v-if="controls" :fit-view-params="
|
|
939
|
+
<Controls v-if="controls" :fit-view-params="controlsFit" />
|
|
940
|
+
|
|
941
|
+
<Panel v-if="search" position="top-left" class="nodrag nopan w-56">
|
|
942
|
+
<LpInput
|
|
943
|
+
v-model="query"
|
|
944
|
+
type="search"
|
|
945
|
+
:aria-label="text.search"
|
|
946
|
+
:placeholder="text.search"
|
|
947
|
+
@keydown.enter.prevent="nextMatch"
|
|
948
|
+
/>
|
|
949
|
+
<p v-if="query.trim()" class="mt-1 text-xs text-muted" role="status">
|
|
950
|
+
{{ text.searchResults.replace("{n}", String(matches.length)) }}
|
|
951
|
+
</p>
|
|
952
|
+
</Panel>
|
|
953
|
+
|
|
954
|
+
<Panel
|
|
955
|
+
v-if="selectionActions?.length && selectedIds.length > 1"
|
|
956
|
+
position="top-right"
|
|
957
|
+
class="nodrag nopan flex items-center gap-2 rounded-card border border-line bg-surface-overlay/90 px-3 py-2 text-xs text-muted backdrop-blur"
|
|
958
|
+
>
|
|
959
|
+
<span>{{ text.selected.replace("{n}", String(selectedIds.length)) }}</span>
|
|
960
|
+
<LpButton
|
|
961
|
+
v-for="action in selectionActions"
|
|
962
|
+
:key="action.id"
|
|
963
|
+
size="sm"
|
|
964
|
+
variant="outline"
|
|
965
|
+
:icon="action.icon"
|
|
966
|
+
@click="emit('selection-action', { action: action.id, ids: selectedIds })"
|
|
967
|
+
>
|
|
968
|
+
{{ action.label }}
|
|
969
|
+
</LpButton>
|
|
970
|
+
</Panel>
|
|
971
|
+
|
|
972
|
+
<Panel
|
|
973
|
+
v-if="(timeline?.length ?? 0) > 1"
|
|
974
|
+
position="bottom-center"
|
|
975
|
+
class="nodrag nopan flex w-72 max-w-[60%] flex-col gap-1 rounded-card border border-line bg-surface-overlay/90 px-3 py-2 text-xs text-muted backdrop-blur"
|
|
976
|
+
>
|
|
977
|
+
<span>{{ timeline?.[versionIndex]?.label ?? `${text.timeline} ${versionIndex + 1}` }}</span>
|
|
978
|
+
<LpSlider
|
|
979
|
+
:model-value="versionIndex"
|
|
980
|
+
:min="0"
|
|
981
|
+
:max="(timeline?.length ?? 1) - 1"
|
|
982
|
+
:step="1"
|
|
983
|
+
:aria-label="text.timeline"
|
|
984
|
+
@update:model-value="timelineIndex = $event"
|
|
985
|
+
/>
|
|
986
|
+
</Panel>
|
|
462
987
|
|
|
463
988
|
<!-- Edge-type legend: makes the colour/dash encoding self-explanatory. -->
|
|
464
989
|
<!-- On a phone the legend would sit on the graph itself; the edge styles
|
|
465
990
|
stay readable without it there. -->
|
|
466
|
-
<Panel v-if="legend" :position="legendPosition" :class="legendOffset" class="max-
|
|
991
|
+
<Panel v-if="legend" :position="legendPosition" :class="legendOffset" class="@max-[40rem]:hidden">
|
|
467
992
|
<div
|
|
468
993
|
ref="legendBox"
|
|
469
994
|
class="flex flex-col gap-1.5 rounded-card border border-line bg-surface-overlay/90 px-3 py-2 text-[11px] backdrop-blur"
|