@leavepulse/ui 0.77.2 → 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-sYMvYzcY.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-DOu7FN5P.js → LpAutocomplete-DkY6CSyD.js} +26 -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 +18 -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
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The graph logic behind LpTopologyCanvas: paths, neighbours, version diffs,
|
|
3
|
+
* layout and clustering. Plain functions over plain data, with no Vue Flow and
|
|
4
|
+
* no DOM, so each can be tested alone and an app can run them on its own data.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface GraphPoint {
|
|
8
|
+
x: number
|
|
9
|
+
y: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface GraphNode {
|
|
13
|
+
id: string
|
|
14
|
+
position: GraphPoint
|
|
15
|
+
parent?: string
|
|
16
|
+
width?: number
|
|
17
|
+
height?: number
|
|
18
|
+
type?: string
|
|
19
|
+
label?: string
|
|
20
|
+
data?: unknown
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface GraphEdge {
|
|
24
|
+
id: string
|
|
25
|
+
source: string
|
|
26
|
+
target: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface GraphPath {
|
|
30
|
+
nodes: string[]
|
|
31
|
+
edges: string[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PathOptions {
|
|
35
|
+
/** Follow edges only from source to target. Default false: a wire goes both ways. */
|
|
36
|
+
directed?: boolean
|
|
37
|
+
/** Leave out edges that may not be walked (a drifted link, a hidden kind). */
|
|
38
|
+
allow?: (edge: GraphEdge) => boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** What touches a node: itself, the nodes one edge away, and those edges. */
|
|
42
|
+
export function neighborhood(edges: readonly GraphEdge[], id: string): GraphPath {
|
|
43
|
+
const nodes = new Set([id])
|
|
44
|
+
const touching: string[] = []
|
|
45
|
+
for (const edge of edges) {
|
|
46
|
+
if (edge.source !== id && edge.target !== id) continue
|
|
47
|
+
nodes.add(edge.source)
|
|
48
|
+
nodes.add(edge.target)
|
|
49
|
+
touching.push(edge.id)
|
|
50
|
+
}
|
|
51
|
+
return { nodes: [...nodes], edges: touching }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The shortest path by hop count, or null when the two are not connected. */
|
|
55
|
+
export function findPath(
|
|
56
|
+
edges: readonly GraphEdge[],
|
|
57
|
+
from: string,
|
|
58
|
+
to: string,
|
|
59
|
+
options: PathOptions = {},
|
|
60
|
+
): GraphPath | null {
|
|
61
|
+
const links = new Map<string, { next: string; edge: string }[]>()
|
|
62
|
+
const link = (a: string, b: string, edge: string) => {
|
|
63
|
+
const list = links.get(a)
|
|
64
|
+
if (list) list.push({ next: b, edge })
|
|
65
|
+
else links.set(a, [{ next: b, edge }])
|
|
66
|
+
}
|
|
67
|
+
for (const edge of edges) {
|
|
68
|
+
if (options.allow && !options.allow(edge)) continue
|
|
69
|
+
link(edge.source, edge.target, edge.id)
|
|
70
|
+
if (!options.directed) link(edge.target, edge.source, edge.id)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const cameBy = new Map<string, { from: string; edge: string } | null>([[from, null]])
|
|
74
|
+
const queue = [from]
|
|
75
|
+
for (let head = 0; head < queue.length; head++) {
|
|
76
|
+
const at = queue[head]!
|
|
77
|
+
if (at === to) break
|
|
78
|
+
for (const { next, edge } of links.get(at) ?? []) {
|
|
79
|
+
if (cameBy.has(next)) continue
|
|
80
|
+
cameBy.set(next, { from: at, edge })
|
|
81
|
+
queue.push(next)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (!cameBy.has(to)) return null
|
|
85
|
+
|
|
86
|
+
const path: GraphPath = { nodes: [to], edges: [] }
|
|
87
|
+
for (let step = cameBy.get(to); step; step = cameBy.get(step.from)) {
|
|
88
|
+
path.nodes.unshift(step.from)
|
|
89
|
+
path.edges.unshift(step.edge)
|
|
90
|
+
}
|
|
91
|
+
return path
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface TopologyDiff {
|
|
95
|
+
addedNodes: string[]
|
|
96
|
+
removedNodes: string[]
|
|
97
|
+
changedNodes: string[]
|
|
98
|
+
addedEdges: string[]
|
|
99
|
+
removedEdges: string[]
|
|
100
|
+
changedEdges: string[]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/*
|
|
104
|
+
* Position is left out of a node's comparison: dragging a card changes nothing
|
|
105
|
+
* about the infrastructure. `dimmed` is display state and goes the same way.
|
|
106
|
+
*/
|
|
107
|
+
function fingerprint(item: object): string {
|
|
108
|
+
const { position: _position, dimmed: _dimmed, data, ...rest } = item as Record<string, unknown>
|
|
109
|
+
const { dimmed: _dataDimmed, ...own } = (data ?? {}) as Record<string, unknown>
|
|
110
|
+
return JSON.stringify([rest, own], (_key, value) =>
|
|
111
|
+
value && typeof value === "object" && !Array.isArray(value)
|
|
112
|
+
? Object.fromEntries(Object.entries(value).sort(([a], [b]) => (a < b ? -1 : 1)))
|
|
113
|
+
: value,
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function compare<T extends { id: string }>(before: readonly T[], after: readonly T[]) {
|
|
118
|
+
const old = new Map(before.map((item) => [item.id, item]))
|
|
119
|
+
const fresh = new Set(after.map((item) => item.id))
|
|
120
|
+
return {
|
|
121
|
+
added: after.filter((item) => !old.has(item.id)).map((item) => item.id),
|
|
122
|
+
removed: before.filter((item) => !fresh.has(item.id)).map((item) => item.id),
|
|
123
|
+
changed: after
|
|
124
|
+
.filter((item) => old.has(item.id) && fingerprint(old.get(item.id)!) !== fingerprint(item))
|
|
125
|
+
.map((item) => item.id),
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** What changed between two versions of a graph, by id. */
|
|
130
|
+
export function diffTopology(
|
|
131
|
+
before: { nodes: readonly GraphNode[]; edges: readonly GraphEdge[] },
|
|
132
|
+
after: { nodes: readonly GraphNode[]; edges: readonly GraphEdge[] },
|
|
133
|
+
): TopologyDiff {
|
|
134
|
+
const nodes = compare(before.nodes, after.nodes)
|
|
135
|
+
const edges = compare(before.edges, after.edges)
|
|
136
|
+
return {
|
|
137
|
+
addedNodes: nodes.added,
|
|
138
|
+
removedNodes: nodes.removed,
|
|
139
|
+
changedNodes: nodes.changed,
|
|
140
|
+
addedEdges: edges.added,
|
|
141
|
+
removedEdges: edges.removed,
|
|
142
|
+
changedEdges: edges.changed,
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface LayoutOptions {
|
|
147
|
+
/** Which way the layers run. Default "LR": columns, left to right. */
|
|
148
|
+
direction?: "LR" | "TB"
|
|
149
|
+
/** Space between layers. */
|
|
150
|
+
layerGap?: number
|
|
151
|
+
/** Space between nodes of one layer. */
|
|
152
|
+
nodeGap?: number
|
|
153
|
+
/** Size of a node that carries none. */
|
|
154
|
+
nodeWidth?: number
|
|
155
|
+
nodeHeight?: number
|
|
156
|
+
/** Lay each group out on its own, one band after another. */
|
|
157
|
+
groupOf?: (node: GraphNode) => string | undefined
|
|
158
|
+
/** Space between group bands. */
|
|
159
|
+
groupGap?: number
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const DEFAULT_NODE_WIDTH = 220
|
|
163
|
+
const DEFAULT_NODE_HEIGHT = 96
|
|
164
|
+
const DEFAULT_LAYER_GAP = 96
|
|
165
|
+
const DEFAULT_NODE_GAP = 32
|
|
166
|
+
const DEFAULT_GROUP_GAP = 72
|
|
167
|
+
/* Down and up again twice settles a small graph's crossings; more buys little. */
|
|
168
|
+
const ORDER_SWEEPS = 4
|
|
169
|
+
|
|
170
|
+
/** The outermost node each node sits in: edges between children count for it. */
|
|
171
|
+
function topLevelOf(nodes: readonly GraphNode[]): Map<string, string> {
|
|
172
|
+
const parents = new Map(nodes.map((node) => [node.id, node.parent]))
|
|
173
|
+
const top = new Map<string, string>()
|
|
174
|
+
for (const node of nodes) {
|
|
175
|
+
let at = node.id
|
|
176
|
+
const seen = new Set([at])
|
|
177
|
+
for (let up = parents.get(at); up && parents.has(up) && !seen.has(up); up = parents.get(at)) {
|
|
178
|
+
at = up
|
|
179
|
+
seen.add(at)
|
|
180
|
+
}
|
|
181
|
+
top.set(node.id, at)
|
|
182
|
+
}
|
|
183
|
+
return top
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Links between distinct top-level nodes, each pair once, with cycles broken. */
|
|
187
|
+
function acyclicLinks(ids: readonly string[], links: readonly [string, string][]): [string, string][] {
|
|
188
|
+
const out = new Map(ids.map((id): [string, string[]] => [id, []]))
|
|
189
|
+
for (const [a, b] of links) {
|
|
190
|
+
const list = out.get(a)
|
|
191
|
+
if (list && out.has(b) && a !== b && !list.includes(b)) list.push(b)
|
|
192
|
+
}
|
|
193
|
+
const state = new Map<string, "open" | "done">()
|
|
194
|
+
const kept: [string, string][] = []
|
|
195
|
+
// Walked with its own stack: a long chain must not exhaust the call stack.
|
|
196
|
+
for (const root of ids) {
|
|
197
|
+
if (state.has(root)) continue
|
|
198
|
+
state.set(root, "open")
|
|
199
|
+
const stack: { id: string; next: number }[] = [{ id: root, next: 0 }]
|
|
200
|
+
while (stack.length) {
|
|
201
|
+
const top = stack[stack.length - 1]!
|
|
202
|
+
const target = out.get(top.id)![top.next++]
|
|
203
|
+
if (target === undefined) {
|
|
204
|
+
state.set(top.id, "done")
|
|
205
|
+
stack.pop()
|
|
206
|
+
} else if (state.get(target) !== "open") {
|
|
207
|
+
kept.push([top.id, target])
|
|
208
|
+
if (!state.has(target)) {
|
|
209
|
+
state.set(target, "open")
|
|
210
|
+
stack.push({ id: target, next: 0 })
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return kept
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Each id's layer: one past the deepest node leading to it. */
|
|
219
|
+
function rankOf(ids: readonly string[], links: readonly [string, string][]): Map<string, number> {
|
|
220
|
+
const rank = new Map(ids.map((id) => [id, 0]))
|
|
221
|
+
const waiting = new Map(ids.map((id) => [id, 0]))
|
|
222
|
+
const out = new Map(ids.map((id): [string, string[]] => [id, []]))
|
|
223
|
+
for (const [a, b] of links) {
|
|
224
|
+
out.get(a)!.push(b)
|
|
225
|
+
waiting.set(b, waiting.get(b)! + 1)
|
|
226
|
+
}
|
|
227
|
+
const queue = ids.filter((id) => waiting.get(id) === 0)
|
|
228
|
+
for (let head = 0; head < queue.length; head++) {
|
|
229
|
+
const at = queue[head]!
|
|
230
|
+
for (const next of out.get(at)!) {
|
|
231
|
+
rank.set(next, Math.max(rank.get(next)!, rank.get(at)! + 1))
|
|
232
|
+
waiting.set(next, waiting.get(next)! - 1)
|
|
233
|
+
if (waiting.get(next) === 0) queue.push(next)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return rank
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Orders each layer by the mean place of its neighbours, to cut crossings. */
|
|
240
|
+
function orderLayers(layers: string[][], links: readonly [string, string][]): void {
|
|
241
|
+
const around = new Map<string, string[]>()
|
|
242
|
+
const add = (a: string, b: string) => around.set(a, [...(around.get(a) ?? []), b])
|
|
243
|
+
for (const [a, b] of links) {
|
|
244
|
+
add(a, b)
|
|
245
|
+
add(b, a)
|
|
246
|
+
}
|
|
247
|
+
for (let sweep = 0; sweep < ORDER_SWEEPS; sweep++) {
|
|
248
|
+
const down = sweep % 2 === 0
|
|
249
|
+
const place = new Map(layers.flatMap((layer) => layer.map((id, i): [string, number] => [id, i])))
|
|
250
|
+
for (let i = 0; i < layers.length; i++) {
|
|
251
|
+
const at = down ? i : layers.length - 1 - i
|
|
252
|
+
const weight = (id: string, fallback: number) => {
|
|
253
|
+
const known = (around.get(id) ?? []).map((other) => place.get(other)!).filter((n) => n !== undefined)
|
|
254
|
+
return known.length ? known.reduce((sum, n) => sum + n, 0) / known.length : fallback
|
|
255
|
+
}
|
|
256
|
+
const ranked = layers[at]!.map((id, index) => ({ id, index, weight: weight(id, index) }))
|
|
257
|
+
ranked.sort((a, b) => a.weight - b.weight || a.index - b.index)
|
|
258
|
+
layers[at] = ranked.map((entry) => entry.id)
|
|
259
|
+
layers[at]!.forEach((id, index) => place.set(id, index))
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/*
|
|
265
|
+
* A simple layered drawing: edges run one way across the layers, cycles broken
|
|
266
|
+
* at their back edge. A node inside another keeps its place in it, so only
|
|
267
|
+
* top-level nodes get a position; their children's edges count for them.
|
|
268
|
+
*/
|
|
269
|
+
export function layeredLayout(
|
|
270
|
+
nodes: readonly GraphNode[],
|
|
271
|
+
edges: readonly GraphEdge[],
|
|
272
|
+
options: LayoutOptions = {},
|
|
273
|
+
): Map<string, GraphPoint> {
|
|
274
|
+
const vertical = options.direction === "TB"
|
|
275
|
+
const layerGap = options.layerGap ?? DEFAULT_LAYER_GAP
|
|
276
|
+
const nodeGap = options.nodeGap ?? DEFAULT_NODE_GAP
|
|
277
|
+
const groupGap = options.groupGap ?? DEFAULT_GROUP_GAP
|
|
278
|
+
const width = (node: GraphNode) => node.width ?? options.nodeWidth ?? DEFAULT_NODE_WIDTH
|
|
279
|
+
const height = (node: GraphNode) => node.height ?? options.nodeHeight ?? DEFAULT_NODE_HEIGHT
|
|
280
|
+
const along = (node: GraphNode) => (vertical ? height(node) : width(node))
|
|
281
|
+
const across = (node: GraphNode) => (vertical ? width(node) : height(node))
|
|
282
|
+
|
|
283
|
+
const top = topLevelOf(nodes)
|
|
284
|
+
const byId = new Map(nodes.map((node) => [node.id, node]))
|
|
285
|
+
const roots = nodes.filter((node) => top.get(node.id) === node.id)
|
|
286
|
+
const links = edges.flatMap((edge): [string, string][] => {
|
|
287
|
+
const a = top.get(edge.source)
|
|
288
|
+
const b = top.get(edge.target)
|
|
289
|
+
return a && b ? [[a, b]] : []
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
const bands = new Map<string, GraphNode[]>()
|
|
293
|
+
for (const node of roots) {
|
|
294
|
+
const key = options.groupOf?.(node) ?? ""
|
|
295
|
+
const band = bands.get(key)
|
|
296
|
+
if (band) band.push(node)
|
|
297
|
+
else bands.set(key, [node])
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const positions = new Map<string, GraphPoint>()
|
|
301
|
+
let bandStart = 0
|
|
302
|
+
for (const band of bands.values()) {
|
|
303
|
+
const ids = band.map((node) => node.id)
|
|
304
|
+
const acyclic = acyclicLinks(ids, links)
|
|
305
|
+
const rank = rankOf(ids, acyclic)
|
|
306
|
+
const layers: string[][] = []
|
|
307
|
+
for (const id of ids) (layers[rank.get(id)!] ??= []).push(id)
|
|
308
|
+
orderLayers(layers, acyclic)
|
|
309
|
+
|
|
310
|
+
const extent = (layer: string[]) =>
|
|
311
|
+
layer.reduce((sum, id) => sum + across(byId.get(id)!) + nodeGap, -nodeGap)
|
|
312
|
+
const bandSize = Math.max(...layers.map(extent))
|
|
313
|
+
let main = 0
|
|
314
|
+
for (const layer of layers) {
|
|
315
|
+
let cross = bandStart + (bandSize - extent(layer)) / 2
|
|
316
|
+
for (const id of layer) {
|
|
317
|
+
positions.set(id, vertical ? { x: cross, y: main } : { x: main, y: cross })
|
|
318
|
+
cross += across(byId.get(id)!) + nodeGap
|
|
319
|
+
}
|
|
320
|
+
main += Math.max(...layer.map((id) => along(byId.get(id)!))) + layerGap
|
|
321
|
+
}
|
|
322
|
+
bandStart += bandSize + groupGap
|
|
323
|
+
}
|
|
324
|
+
return positions
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export interface GraphBox extends GraphPoint {
|
|
328
|
+
width: number
|
|
329
|
+
height: number
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export interface LaneOptions {
|
|
333
|
+
padding?: number
|
|
334
|
+
/** Room over the nodes for the lane's label. */
|
|
335
|
+
header?: number
|
|
336
|
+
nodeWidth?: number
|
|
337
|
+
nodeHeight?: number
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const LANE_PADDING = 24
|
|
341
|
+
const LANE_HEADER = 28
|
|
342
|
+
|
|
343
|
+
/** One box per group, around its top-level nodes where they stand now. */
|
|
344
|
+
export function groupBoxes(
|
|
345
|
+
nodes: readonly GraphNode[],
|
|
346
|
+
keyOf: (node: GraphNode) => string | undefined,
|
|
347
|
+
options: LaneOptions = {},
|
|
348
|
+
): { key: string; box: GraphBox }[] {
|
|
349
|
+
const padding = options.padding ?? LANE_PADDING
|
|
350
|
+
const header = options.header ?? LANE_HEADER
|
|
351
|
+
const edges = new Map<string, { left: number; top: number; right: number; bottom: number }>()
|
|
352
|
+
for (const node of nodes) {
|
|
353
|
+
const key = node.parent ? undefined : keyOf(node)
|
|
354
|
+
if (key === undefined) continue
|
|
355
|
+
const { x, y } = node.position
|
|
356
|
+
const right = x + (node.width ?? options.nodeWidth ?? DEFAULT_NODE_WIDTH)
|
|
357
|
+
const bottom = y + (node.height ?? options.nodeHeight ?? DEFAULT_NODE_HEIGHT)
|
|
358
|
+
const seen = edges.get(key)
|
|
359
|
+
edges.set(key, seen
|
|
360
|
+
? { left: Math.min(seen.left, x), top: Math.min(seen.top, y), right: Math.max(seen.right, right), bottom: Math.max(seen.bottom, bottom) }
|
|
361
|
+
: { left: x, top: y, right, bottom })
|
|
362
|
+
}
|
|
363
|
+
return [...edges].map(([key, e]) => ({
|
|
364
|
+
key,
|
|
365
|
+
box: {
|
|
366
|
+
x: e.left - padding,
|
|
367
|
+
y: e.top - padding - header,
|
|
368
|
+
width: e.right - e.left + padding * 2,
|
|
369
|
+
height: e.bottom - e.top + padding * 2 + header,
|
|
370
|
+
},
|
|
371
|
+
}))
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export interface FrameOptions {
|
|
375
|
+
padding?: number
|
|
376
|
+
minWidth?: number
|
|
377
|
+
minHeight?: number
|
|
378
|
+
childWidth?: number
|
|
379
|
+
childHeight?: number
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const CHILD_WIDTH = 170
|
|
383
|
+
const CHILD_HEIGHT = 56
|
|
384
|
+
const FRAME_PADDING = 16
|
|
385
|
+
|
|
386
|
+
/** The frame that holds these children, whose positions are relative to it. */
|
|
387
|
+
export function frameSize(children: readonly GraphNode[], options: FrameOptions = {}): { width: number; height: number } {
|
|
388
|
+
const padding = options.padding ?? FRAME_PADDING
|
|
389
|
+
let width = options.minWidth ?? 0
|
|
390
|
+
let height = options.minHeight ?? 0
|
|
391
|
+
for (const child of children) {
|
|
392
|
+
width = Math.max(width, child.position.x + (child.width ?? options.childWidth ?? CHILD_WIDTH) + padding)
|
|
393
|
+
height = Math.max(height, child.position.y + (child.height ?? options.childHeight ?? CHILD_HEIGHT) + padding)
|
|
394
|
+
}
|
|
395
|
+
return { width, height }
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export interface MergedEdge extends GraphEdge {
|
|
399
|
+
/** The original edges this one stands for. */
|
|
400
|
+
members: string[]
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Edges redrawn between what their ends stand for now: inner ones go, and
|
|
405
|
+
* parallel ones become one, keeping which they were.
|
|
406
|
+
*/
|
|
407
|
+
export function mergeEdges(edges: readonly GraphEdge[], mapId: (id: string) => string | undefined): MergedEdge[] {
|
|
408
|
+
const merged = new Map<string, MergedEdge>()
|
|
409
|
+
for (const edge of edges) {
|
|
410
|
+
const source = mapId(edge.source)
|
|
411
|
+
const target = mapId(edge.target)
|
|
412
|
+
if (source === undefined || target === undefined || source === target) continue
|
|
413
|
+
const id = `${source}\u0000${target}`
|
|
414
|
+
const seen = merged.get(id)
|
|
415
|
+
if (seen) seen.members.push(edge.id)
|
|
416
|
+
else merged.set(id, { id, source, target, members: [edge.id] })
|
|
417
|
+
}
|
|
418
|
+
return [...merged.values()]
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export interface GraphCluster {
|
|
422
|
+
id: string
|
|
423
|
+
key: string
|
|
424
|
+
position: GraphPoint
|
|
425
|
+
members: string[]
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export interface ClusteredGraph<N extends GraphNode> {
|
|
429
|
+
nodes: N[]
|
|
430
|
+
clusters: GraphCluster[]
|
|
431
|
+
edges: MergedEdge[]
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const CLUSTER_PREFIX = "lp-cluster:"
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Collapses each group into one cluster at the middle of its top-level nodes.
|
|
438
|
+
* A node with no group stays; a child goes with whatever holds it.
|
|
439
|
+
*/
|
|
440
|
+
export function clusterGraph<N extends GraphNode>(
|
|
441
|
+
nodes: readonly N[],
|
|
442
|
+
edges: readonly GraphEdge[],
|
|
443
|
+
keyOf: (node: N) => string | undefined,
|
|
444
|
+
): ClusteredGraph<N> {
|
|
445
|
+
const top = topLevelOf(nodes)
|
|
446
|
+
const byId = new Map(nodes.map((node) => [node.id, node]))
|
|
447
|
+
const groups = new Map<string, N[]>()
|
|
448
|
+
const standsFor = new Map<string, string>()
|
|
449
|
+
for (const node of nodes) {
|
|
450
|
+
const root = byId.get(top.get(node.id)!)!
|
|
451
|
+
const key = keyOf(root)
|
|
452
|
+
if (key === undefined) {
|
|
453
|
+
standsFor.set(node.id, node.id)
|
|
454
|
+
continue
|
|
455
|
+
}
|
|
456
|
+
standsFor.set(node.id, CLUSTER_PREFIX + key)
|
|
457
|
+
if (node !== root) continue
|
|
458
|
+
const group = groups.get(key)
|
|
459
|
+
if (group) group.push(node)
|
|
460
|
+
else groups.set(key, [node])
|
|
461
|
+
}
|
|
462
|
+
const clusters = [...groups].map(([key, members]): GraphCluster => ({
|
|
463
|
+
id: CLUSTER_PREFIX + key,
|
|
464
|
+
key,
|
|
465
|
+
position: {
|
|
466
|
+
x: members.reduce((sum, node) => sum + node.position.x, 0) / members.length,
|
|
467
|
+
y: members.reduce((sum, node) => sum + node.position.y, 0) / members.length,
|
|
468
|
+
},
|
|
469
|
+
members: nodes.filter((node) => standsFor.get(node.id) === CLUSTER_PREFIX + key).map((node) => node.id),
|
|
470
|
+
}))
|
|
471
|
+
return {
|
|
472
|
+
nodes: nodes.filter((node) => standsFor.get(node.id) === node.id),
|
|
473
|
+
clusters,
|
|
474
|
+
edges: mergeEdges(edges, (id) => standsFor.get(id)),
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** Drops children, with their edges moved up to whatever holds them. */
|
|
479
|
+
export function collapseChildren<N extends GraphNode>(
|
|
480
|
+
nodes: readonly N[],
|
|
481
|
+
edges: readonly GraphEdge[],
|
|
482
|
+
): { nodes: N[]; edges: MergedEdge[] } {
|
|
483
|
+
const top = topLevelOf(nodes)
|
|
484
|
+
return { nodes: nodes.filter((node) => top.get(node.id) === node.id), edges: mergeEdges(edges, (id) => top.get(id)) }
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export type DetailLevel = "full" | "simple" | "overview"
|
|
488
|
+
|
|
489
|
+
export const SIMPLE_BELOW_ZOOM = 0.7
|
|
490
|
+
export const OVERVIEW_BELOW_ZOOM = 0.45
|
|
491
|
+
|
|
492
|
+
/** How much of the graph to draw at a zoom. Clusters need groups to exist. */
|
|
493
|
+
export function detailAtZoom(zoom: number, grouped: boolean): DetailLevel {
|
|
494
|
+
if (zoom < OVERVIEW_BELOW_ZOOM && grouped) return "overview"
|
|
495
|
+
return zoom < SIMPLE_BELOW_ZOOM ? "simple" : "full"
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** Where every node really is: a child's position is relative to its parent. */
|
|
499
|
+
export function absolutePositions(nodes: readonly GraphNode[]): Map<string, GraphPoint> {
|
|
500
|
+
const byId = new Map(nodes.map((node) => [node.id, node]))
|
|
501
|
+
const resolved = new Map<string, GraphPoint>()
|
|
502
|
+
const resolve = (node: GraphNode, seen: Set<string>): GraphPoint => {
|
|
503
|
+
const known = resolved.get(node.id)
|
|
504
|
+
if (known) return known
|
|
505
|
+
const parent = node.parent ? byId.get(node.parent) : undefined
|
|
506
|
+
const base = parent && !seen.has(parent.id) ? resolve(parent, seen.add(node.id)) : { x: 0, y: 0 }
|
|
507
|
+
const point = { x: base.x + node.position.x, y: base.y + node.position.y }
|
|
508
|
+
resolved.set(node.id, point)
|
|
509
|
+
return point
|
|
510
|
+
}
|
|
511
|
+
for (const node of nodes) resolve(node, new Set())
|
|
512
|
+
return resolved
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export function nodeName(node: GraphNode): string {
|
|
516
|
+
const name = (node.data as { name?: unknown } | undefined)?.name
|
|
517
|
+
return node.label ?? (typeof name === "string" ? name : node.id)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** Nodes matching a query, by name or id: names first, then earlier matches. */
|
|
521
|
+
export function findNodes<N extends GraphNode>(nodes: readonly N[], query: string): N[] {
|
|
522
|
+
const needle = query.trim().toLowerCase()
|
|
523
|
+
if (!needle) return []
|
|
524
|
+
const score = (node: N) => {
|
|
525
|
+
const at = nodeName(node).toLowerCase().indexOf(needle)
|
|
526
|
+
if (at === 0) return 0
|
|
527
|
+
if (at > 0) return 1
|
|
528
|
+
return node.id.toLowerCase().includes(needle) ? 2 : -1
|
|
529
|
+
}
|
|
530
|
+
return nodes
|
|
531
|
+
.map((node) => ({ node, rank: score(node) }))
|
|
532
|
+
.filter((entry) => entry.rank >= 0)
|
|
533
|
+
.sort((a, b) => a.rank - b.rank)
|
|
534
|
+
.map((entry) => entry.node)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export type GraphDirection = "up" | "down" | "left" | "right"
|
|
538
|
+
|
|
539
|
+
/* A step sideways costs this much more than a step the way the arrow points. */
|
|
540
|
+
const OFF_AXIS_PENALTY = 2
|
|
541
|
+
|
|
542
|
+
/** The closest node the way an arrow key points, or null at the edge of the graph. */
|
|
543
|
+
export function nearestInDirection(
|
|
544
|
+
positions: ReadonlyMap<string, GraphPoint>,
|
|
545
|
+
from: string,
|
|
546
|
+
direction: GraphDirection,
|
|
547
|
+
): string | null {
|
|
548
|
+
const origin = positions.get(from)
|
|
549
|
+
if (!origin) return null
|
|
550
|
+
let best: string | null = null
|
|
551
|
+
let bestCost = Infinity
|
|
552
|
+
for (const [id, point] of positions) {
|
|
553
|
+
if (id === from) continue
|
|
554
|
+
const dx = point.x - origin.x
|
|
555
|
+
const dy = point.y - origin.y
|
|
556
|
+
const main = { up: -dy, down: dy, left: -dx, right: dx }[direction]
|
|
557
|
+
if (main <= 0) continue
|
|
558
|
+
const side = direction === "up" || direction === "down" ? dx : dy
|
|
559
|
+
const cost = main + Math.abs(side) * OFF_AXIS_PENALTY
|
|
560
|
+
if (cost < bestCost) {
|
|
561
|
+
bestCost = cost
|
|
562
|
+
best = id
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return best
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export interface EdgeReading {
|
|
569
|
+
kind?: string
|
|
570
|
+
/** Round-trip time, in milliseconds. */
|
|
571
|
+
rtt?: number
|
|
572
|
+
/** Packet loss, in percent. */
|
|
573
|
+
loss?: number
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** The edge's label: its kind, then whatever was measured on it. */
|
|
577
|
+
export function edgeLabelText(edge: EdgeReading): string | undefined {
|
|
578
|
+
const parts = [
|
|
579
|
+
edge.kind,
|
|
580
|
+
edge.rtt === undefined ? undefined : `${Math.round(edge.rtt)} ms`,
|
|
581
|
+
edge.loss === undefined ? undefined : `${edge.loss}% loss`,
|
|
582
|
+
].filter((part) => part !== undefined)
|
|
583
|
+
return parts.length ? parts.join(" · ") : undefined
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export interface SvgTheme {
|
|
587
|
+
background: string
|
|
588
|
+
surface: string
|
|
589
|
+
line: string
|
|
590
|
+
text: string
|
|
591
|
+
edge: string
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
export interface SvgNode extends GraphBox {
|
|
595
|
+
id: string
|
|
596
|
+
label: string
|
|
597
|
+
/** A frame holding other nodes: drawn behind them, label at the top. */
|
|
598
|
+
frame?: boolean
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
export interface SvgEdge extends GraphEdge {
|
|
602
|
+
label?: string
|
|
603
|
+
color?: string
|
|
604
|
+
dash?: string
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const SVG_MARGIN = 24
|
|
608
|
+
const SVG_RADIUS = 10
|
|
609
|
+
const SVG_FONT_SIZE = 12
|
|
610
|
+
const SVG_FRAME_LABEL_Y = 18
|
|
611
|
+
|
|
612
|
+
const escapeXml = (text: string) =>
|
|
613
|
+
text.replace(/[&<>"']/g, (char) => `&#${char.charCodeAt(0)};`)
|
|
614
|
+
|
|
615
|
+
/*
|
|
616
|
+
* A clean drawing of the graph rather than a capture of the screen: the cards
|
|
617
|
+
* are HTML, which only goes into an SVG as foreignObject, and that taints the
|
|
618
|
+
* canvas a PNG is drawn on. Boxes, curves and names survive; card detail does not.
|
|
619
|
+
*/
|
|
620
|
+
export function topologyToSvg(nodes: readonly SvgNode[], edges: readonly SvgEdge[], theme: SvgTheme): string {
|
|
621
|
+
const left = Math.min(0, ...nodes.map((n) => n.x)) - SVG_MARGIN
|
|
622
|
+
const top = Math.min(0, ...nodes.map((n) => n.y)) - SVG_MARGIN
|
|
623
|
+
const width = Math.max(0, ...nodes.map((n) => n.x + n.width)) + SVG_MARGIN - left
|
|
624
|
+
const height = Math.max(0, ...nodes.map((n) => n.y + n.height)) + SVG_MARGIN - top
|
|
625
|
+
const byId = new Map(nodes.map((node) => [node.id, node]))
|
|
626
|
+
const middle = (n: SvgNode) => ({ x: n.x + n.width / 2, y: n.y + n.height / 2 })
|
|
627
|
+
|
|
628
|
+
const wires = edges.flatMap((edge) => {
|
|
629
|
+
const from = byId.get(edge.source)
|
|
630
|
+
const to = byId.get(edge.target)
|
|
631
|
+
if (!from || !to) return []
|
|
632
|
+
const a = middle(from)
|
|
633
|
+
const b = middle(to)
|
|
634
|
+
const dash = edge.dash && edge.dash !== "0" ? ` stroke-dasharray="${escapeXml(edge.dash)}"` : ""
|
|
635
|
+
const line = `<line x1="${a.x}" y1="${a.y}" x2="${b.x}" y2="${b.y}" stroke="${escapeXml(edge.color ?? theme.edge)}" stroke-width="2"${dash}/>`
|
|
636
|
+
const label = edge.label
|
|
637
|
+
? `<text x="${(a.x + b.x) / 2}" y="${(a.y + b.y) / 2}" text-anchor="middle" fill="${escapeXml(theme.text)}">${escapeXml(edge.label)}</text>`
|
|
638
|
+
: ""
|
|
639
|
+
return [line + label]
|
|
640
|
+
})
|
|
641
|
+
|
|
642
|
+
const box = (n: SvgNode) => {
|
|
643
|
+
const textX = n.x + n.width / 2
|
|
644
|
+
const textY = n.frame ? n.y + SVG_FRAME_LABEL_Y : n.y + n.height / 2
|
|
645
|
+
return `<rect x="${n.x}" y="${n.y}" width="${n.width}" height="${n.height}" rx="${SVG_RADIUS}" fill="${escapeXml(theme.surface)}" stroke="${escapeXml(theme.line)}"/>`
|
|
646
|
+
+ `<text x="${textX}" y="${textY}" text-anchor="middle" dominant-baseline="middle" fill="${escapeXml(theme.text)}">${escapeXml(n.label)}</text>`
|
|
647
|
+
}
|
|
648
|
+
// Frames go down first, or they would cover the wires and nodes inside them.
|
|
649
|
+
const frames = nodes.filter((n) => n.frame).map(box)
|
|
650
|
+
const cards = nodes.filter((n) => !n.frame).map(box)
|
|
651
|
+
|
|
652
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="${left} ${top} ${width} ${height}" font-family="sans-serif" font-size="${SVG_FONT_SIZE}">`
|
|
653
|
+
+ `<rect x="${left}" y="${top}" width="${width}" height="${height}" fill="${escapeXml(theme.background)}"/>`
|
|
654
|
+
+ frames.join("") + wires.join("") + cards.join("") + "</svg>"
|
|
655
|
+
}
|
package/src/components/tour.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { nodeWindow } from "../composables/realm"
|
|
1
2
|
/*
|
|
2
3
|
* Geometry for LpTour, kept out of the component so it can be tested without a
|
|
3
4
|
* DOM: where the spotlight hole goes and what the dim panels around it cover.
|
|
@@ -71,8 +72,9 @@ export function holeClip(hole: Box | null, viewport: Size, radius: number): stri
|
|
|
71
72
|
*/
|
|
72
73
|
export function openContextMenu(el: Element): () => void {
|
|
73
74
|
const box = el.getBoundingClientRect()
|
|
75
|
+
const win = nodeWindow(el)
|
|
74
76
|
el.dispatchEvent(
|
|
75
|
-
new MouseEvent("contextmenu", {
|
|
77
|
+
new win.MouseEvent("contextmenu", {
|
|
76
78
|
bubbles: true,
|
|
77
79
|
cancelable: true,
|
|
78
80
|
button: 2,
|
|
@@ -83,7 +85,7 @@ export function openContextMenu(el: Element): () => void {
|
|
|
83
85
|
return () => {
|
|
84
86
|
const outside = { bubbles: true, cancelable: true, clientX: 0, clientY: 0 }
|
|
85
87
|
const body = el.ownerDocument.body
|
|
86
|
-
body.dispatchEvent(new PointerEvent("pointerdown", outside))
|
|
87
|
-
body.dispatchEvent(new PointerEvent("pointerup", outside))
|
|
88
|
+
body.dispatchEvent(new win.PointerEvent("pointerdown", outside))
|
|
89
|
+
body.dispatchEvent(new win.PointerEvent("pointerup", outside))
|
|
88
90
|
}
|
|
89
91
|
}
|