@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.
Files changed (597) hide show
  1. package/README.md +68 -0
  2. package/dist/assets/{LpCommandPalette-DOIZHw-g.css → LpCommandPalette-DDJKICKh.css} +19 -19
  3. package/dist/assets/{LpDock-Cbh0wQUv.css → LpDock-Dw82tWVD.css} +3 -3
  4. package/dist/assets/{LpDropzone-B_qRq1-R.css → LpDropzone-DNbh_NFJ.css} +10 -10
  5. package/dist/assets/{LpImageEditor-XM5bAziW.css → LpImageEditor-Bgu20XLs.css} +6 -6
  6. package/dist/assets/{LpTable-Bv-EcQs7.css → LpTable-CCk0ZHo7.css} +19 -19
  7. package/dist/assets/LpTabs-Bh7a-Bdl.css +18 -0
  8. package/dist/chunks/AppShellContextNav-C6Ho2mof.js +49 -0
  9. package/dist/chunks/ChartRangeSelector-HO0sMMz6.js +129 -0
  10. package/dist/chunks/{ContextMenuItems-BXumicbh.js → ContextMenuItems-DmaKSJVv.js} +1 -1
  11. package/dist/chunks/{DropzoneLinkField-C-9BCGJn.js → DropzoneLinkField-DXIr0eFz.js} +2 -2
  12. package/dist/chunks/{FieldWrapper-CORPzo9s.js → FieldWrapper-DnuMTrqW.js} +1 -1
  13. package/dist/chunks/IconCloseButton-COZEsa9u.js +29 -0
  14. package/dist/chunks/{LayoutCanvas-CNOAUk7D.js → LayoutCanvas-CZKOltfA.js} +1 -1
  15. package/dist/chunks/{LayoutNode-DF5BMN0J.js → LayoutNode-D2f7v683.js} +45 -45
  16. package/dist/chunks/{LpActionBar-BP1ld7-S.js → LpActionBar-BGMk5KoG.js} +3 -3
  17. package/dist/chunks/{LpActivityLine-CJvzrNpj.js → LpActivityLine-Ea-t5Vza.js} +4 -2
  18. package/dist/chunks/{LpAddressInput-sYMvYzcY.js → LpAddressInput-ZqoZ-N2Y.js} +12 -11
  19. package/dist/chunks/{LpAlert-n5XPAZfv.js → LpAlert-CjvTZIKF.js} +3 -3
  20. package/dist/chunks/LpAppShell-BNAVTQsY.js +582 -0
  21. package/dist/chunks/{LpAutocomplete-DOu7FN5P.js → LpAutocomplete-DkY6CSyD.js} +26 -42
  22. package/dist/chunks/{LpAvatar-Cg6Az3wz.js → LpAvatar-cQVsNk6F.js} +2 -2
  23. package/dist/chunks/{LpBadge-wUYLN4U9.js → LpBadge-D5QQdDQ_.js} +2 -2
  24. package/dist/chunks/{LpBottomNav-CbNQQPuL.js → LpBottomNav-zxLGGbjm.js} +9 -2
  25. package/dist/chunks/{LpBreadcrumbs-DPdbth-c.js → LpBreadcrumbs-CIIBsygI.js} +4 -3
  26. package/dist/chunks/{LpButton-CFb44IjC.js → LpButton-DSHioIfD.js} +5 -4
  27. package/dist/chunks/{LpCalendar-CuoWks-0.js → LpCalendar-D4-LVjfF.js} +1 -1
  28. package/dist/chunks/{LpCard-r13WRqox.js → LpCard-BPtYXwnT.js} +1 -1
  29. package/dist/chunks/{LpCheckbox-Cs4SHaHX.js → LpCheckbox-D6RuDW4X.js} +38 -16
  30. package/dist/chunks/{LpCodeBlock-DRC8VbXW.js → LpCodeBlock-DuzmnIHl.js} +19 -41
  31. package/dist/chunks/{LpCodeDisplay-CE9TxCA4.js → LpCodeDisplay-CO8fngxa.js} +5 -5
  32. package/dist/chunks/{LpCollapse-CWS9M_lm.js → LpCollapse-DrAw5DBZ.js} +5 -4
  33. package/dist/chunks/{LpColorPicker-1y1QJHZR.js → LpColorPicker-BxAHEnR5.js} +11 -12
  34. package/dist/chunks/{LpCommandScope-CtUhJe8T.js → LpCommandScope-DRUvHiX7.js} +1 -1
  35. package/dist/chunks/{LpConfirmDialog-DlD45Bi_.js → LpConfirmDialog-6oCIJTDa.js} +3 -3
  36. package/dist/chunks/{LpConfirmHost-BIWjBLmI.js → LpConfirmHost-DgHvfT6s.js} +1 -1
  37. package/dist/chunks/{LpContextMenu-rx2tCjwk.js → LpContextMenu--eMGIql6.js} +13 -15
  38. package/dist/chunks/{LpContextSwitcher-Dwh6dyID.js → LpContextSwitcher-1C1tExdl.js} +2 -2
  39. package/dist/chunks/{LpCopyButton-xF7wFVoU.js → LpCopyButton-7EOSIuwa.js} +3 -3
  40. package/dist/chunks/{LpCountdown-DgeY3cLG.js → LpCountdown-DVl7vXI1.js} +4 -3
  41. package/dist/chunks/{LpDataList-Jxyl9H5w.js → LpDataList-DgBeM01y.js} +13 -4
  42. package/dist/chunks/{LpDatePicker-CA0QTfBC.js → LpDatePicker-DwSDY0mt.js} +12 -18
  43. package/dist/chunks/{LpDateRangePicker-u3J51xXk.js → LpDateRangePicker-CNQXdTVv.js} +10 -16
  44. package/dist/chunks/{LpDisclosure-Cpa7Hc5T.js → LpDisclosure-DG7JCo-d.js} +2 -2
  45. package/dist/chunks/{LpDrawer-CrG8t_KP.js → LpDrawer-Bt9UUqt_.js} +61 -61
  46. package/dist/chunks/{LpDropdownMenu-Dl7Z4ZhS.js → LpDropdownMenu-CLzxIOgM.js} +9 -17
  47. package/dist/chunks/{LpEditable-Cy6GLbFx.js → LpEditable-DVT0Rh8F.js} +9 -6
  48. package/dist/chunks/{LpEmptyState-EQHiRYhG.js → LpEmptyState-DurPF001.js} +1 -1
  49. package/dist/chunks/{LpFieldNotice-CzQ2_5rQ.js → LpFieldNotice-DJ10cYIo.js} +1 -1
  50. package/dist/chunks/LpFileTree-BSx1kBoV.js +735 -0
  51. package/dist/chunks/{LpFileTreeNode-cT6BIWvz.js → LpFileTreeNode-4dVmFHb3.js} +116 -34
  52. package/dist/chunks/{LpFormField-Ch69Jd-X.js → LpFormField-BKYn9dyP.js} +3 -3
  53. package/dist/chunks/{LpFrame-BVQCQLDI.js → LpFrame-Bv5Fphte.js} +25 -11
  54. package/dist/chunks/{LpHintExample-9Py0wc-L.js → LpHintExample-YzCXiSUp.js} +1 -1
  55. package/dist/chunks/{LpHintLayer-CgObdUsM.js → LpHintLayer-BeKRsRon.js} +2 -2
  56. package/dist/chunks/{LpHints-Bz-BlaSl.js → LpHints-qf3xYsza.js} +3 -3
  57. package/dist/chunks/{LpHoverCard-spGxyw7T.js → LpHoverCard-DanH3Nz1.js} +23 -5
  58. package/dist/chunks/{LpIcon--5WSb3jJ.js → LpIcon-C6JpQ5td.js} +6 -3
  59. package/dist/chunks/{LpIdenticon-BzdC_qz9.js → LpIdenticon-Dbbr4fvt.js} +2 -1
  60. package/dist/chunks/{LpInfraNode-46XOCkfq.js → LpInfraNode-BAU40Km2.js} +2 -2
  61. package/dist/chunks/{LpInput-Tw-yxT4I.js → LpInput-DL6HEJdi.js} +16 -28
  62. package/dist/chunks/LpInspector-CuoMAhiw.js +209 -0
  63. package/dist/chunks/{LpLightbox-B95yu22B.js → LpLightbox-xvpQ64MM.js} +57 -32
  64. package/dist/chunks/{LpLoader-DVwkOmt9.js → LpLoader-Bf1yZwaU.js} +1 -1
  65. package/dist/chunks/LpLogInspector-6cNnXqp4.js +119 -0
  66. package/dist/chunks/{LpLogViewer-ngqkns4m.js → LpLogViewer-Dvb1E9_G.js} +234 -53
  67. package/dist/chunks/{LpMapPicker-DRGgh89B.js → LpMapPicker-D5_YLLBC.js} +4 -3
  68. package/dist/chunks/{LpMasonry--k0571qG.js → LpMasonry-DPYekFxs.js} +8 -5
  69. package/dist/chunks/{LpMediaGallery-Bkram-MO.js → LpMediaGallery-Df-tiWeq.js} +3 -3
  70. package/dist/chunks/{LpModal-CoGhLTyH.js → LpModal-DrtQ_1A2.js} +11 -13
  71. package/dist/chunks/{LpModalNav-BGvs2y5Q.js → LpModalNav-DkmGCnZE.js} +11 -37
  72. package/dist/chunks/{LpNotificationBell-B0034Lk-.js → LpNotificationBell-D_wklO1m.js} +9 -9
  73. package/dist/chunks/{LpNumberField-e0IBkWU1.js → LpNumberField-CNHsmSDZ.js} +7 -16
  74. package/dist/chunks/{LpOtpInput-CeBPsM7D.js → LpOtpInput-BRpLBbOD.js} +2 -2
  75. package/dist/chunks/{LpPagination-BIIiYHXR.js → LpPagination-snQ020xQ.js} +6 -4
  76. package/dist/chunks/{LpPaletteTheme-DiLYKQiZ.js → LpPaletteTheme-dVtxHx-F.js} +4 -4
  77. package/dist/chunks/{LpPasswordInput-BkHKQDhM.js → LpPasswordInput-B-zD6tUS.js} +2 -2
  78. package/dist/chunks/{LpPhoneInput-CETEp5W7.js → LpPhoneInput-BSL7NX4z.js} +15 -26
  79. package/dist/chunks/{LpPie-DrZepi0p.js → LpPie-9t_guO49.js} +22 -21
  80. package/dist/chunks/{LpPipeline-B3aOVOkV.js → LpPipeline-rjXvEDRJ.js} +16 -7
  81. package/dist/chunks/{LpPopover-DmZ3wUtP.js → LpPopover-BQSuxtai.js} +10 -1
  82. package/dist/chunks/{LpProgress-Yr6ONG5t.js → LpProgress-D_MSjowb.js} +2 -1
  83. package/dist/chunks/{LpQuota-Dk2MyI2Z.js → LpQuota-CgWPgdVt.js} +1 -1
  84. package/dist/chunks/{LpRadioGroup-ggaPL92T.js → LpRadioGroup-BrazjLY_.js} +4 -4
  85. package/dist/chunks/{LpRangeCalendar-CYYUYqJ7.js → LpRangeCalendar-BrqlGZzE.js} +1 -1
  86. package/dist/chunks/{LpRangeSlider-CM-0ZITZ.js → LpRangeSlider-fz61tzc-.js} +5 -9
  87. package/dist/chunks/{LpRelativeTime-DwgCb5-8.js → LpRelativeTime-BovUk8MP.js} +1 -1
  88. package/dist/chunks/{LpRepeater-D7ZmnIYd.js → LpRepeater-BkJHSPI1.js} +3 -3
  89. package/dist/chunks/{LpRevealOnScroll-DcQjexLy.js → LpRevealOnScroll-CPwyiQAr.js} +1 -1
  90. package/dist/chunks/{LpScramble-Bn9OoOtC.js → LpScramble-DsE77iVp.js} +1 -1
  91. package/dist/chunks/{LpScrollArea-MyEnVB_5.js → LpScrollArea-ay4Xo_hg.js} +22 -10
  92. package/dist/chunks/{LpSecretField-BbmqM9ye.js → LpSecretField-DBvU6iAw.js} +10 -9
  93. package/dist/chunks/{LpSection-DFK_uoJE.js → LpSection-BfteipxU.js} +1 -1
  94. package/dist/chunks/{LpSegmented-CeACsMMA.js → LpSegmented-DbEJhbv-.js} +8 -10
  95. package/dist/chunks/{LpSelect-Cxq4yBGd.js → LpSelect-Y-iuSymW.js} +47 -30
  96. package/dist/chunks/{LpShaderBackdrop-Bote0iuy.js → LpShaderBackdrop-wQygYb5h.js} +13 -10
  97. package/dist/chunks/{LpShift-CCq7dofl.js → LpShift-DAgS_d2x.js} +18 -15
  98. package/dist/chunks/{LpSidebar-DzE1783C.js → LpSidebar-kSkpPh3-.js} +74 -87
  99. package/dist/chunks/{LpSidebarNav-DE3PhXVP.js → LpSidebarNav-C2mvqrY2.js} +8 -6
  100. package/dist/chunks/{LpSidebarRow-BRYhNDSo.js → LpSidebarRow-eRP4Q31q.js} +6 -5
  101. package/dist/chunks/{LpSlider-DxYoCJJO.js → LpSlider-A4ixovgS.js} +5 -9
  102. package/dist/chunks/{LpSpotlight-4F3NeUMY.js → LpSpotlight-DwxkhFgR.js} +2 -1
  103. package/dist/chunks/{LpStat-f2LfDCJ4.js → LpStat-pMNZgFcq.js} +2 -2
  104. package/dist/chunks/{LpStatus-DglxttQ7.js → LpStatus-DGJFwk5F.js} +2 -2
  105. package/dist/chunks/{LpStepMark-DpzKVyh8.js → LpStepMark-Dw2MRTrt.js} +2 -2
  106. package/dist/chunks/{LpStepper-DpsocBNJ.js → LpStepper-DNnh7lCu.js} +1 -1
  107. package/dist/chunks/{LpSwap-C96c0LWh.js → LpSwap-M7nFeW3C.js} +1 -1
  108. package/dist/chunks/{LpSwitch-BnXd1llc.js → LpSwitch-DnC2-0aV.js} +3 -3
  109. package/dist/chunks/{LpTabFace-DPCwKkH2.js → LpTabFace-DQoV8mQG.js} +2 -2
  110. package/dist/chunks/{LpTableFilter-CiVBpaq0.js → LpTableFilter-DgARTsA1.js} +4 -4
  111. package/dist/chunks/{LpTableOfContents-BN5zvpnx.js → LpTableOfContents-CKkWEK6R.js} +14 -9
  112. package/dist/chunks/{LpTagsInput-C-mDG648.js → LpTagsInput-Dt0mteSj.js} +19 -27
  113. package/dist/chunks/{LpTextarea-TE-yZv2U.js → LpTextarea-Co0b9PVx.js} +8 -16
  114. package/dist/chunks/LpThemeProvider-XK6ZeWAE.js +81 -0
  115. package/dist/chunks/{LpThemeStudio-BpORDvH6.js → LpThemeStudio-DzYfE-5k.js} +12 -195
  116. package/dist/chunks/{LpThemeSwitcher-BnEn_mvf.js → LpThemeSwitcher-CFLkdbBL.js} +8 -6
  117. package/dist/chunks/{LpTimeField-BawSVsmt.js → LpTimeField-DeOcKWJq.js} +6 -9
  118. package/dist/chunks/{LpToaster-IwTXjbbU.js → LpToaster-QFj0P-5m.js} +10 -8
  119. package/dist/chunks/{LpTooltip-BcBvIx4z.js → LpTooltip-BJf3lZMq.js} +51 -19
  120. package/dist/chunks/LpTopologyCanvas-BOHHHZ_6.js +673 -0
  121. package/dist/chunks/{LpTour-BZq-mUMv.js → LpTour-ClKYhx4h.js} +7 -5
  122. package/dist/chunks/{LpTruncate-CflAIwiJ.js → LpTruncate-C6u3gjc6.js} +6 -5
  123. package/dist/chunks/{LpTypewriter-BCL_a6wE.js → LpTypewriter-CZIzpdbe.js} +2 -1
  124. package/dist/chunks/{LpUptimeBar-DqNvvMBK.js → LpUptimeBar-BnYmR2rj.js} +3 -3
  125. package/dist/chunks/{LpVideoBackdrop-DS1wGv-X.js → LpVideoBackdrop-Ct4St4ZL.js} +10 -13
  126. package/dist/chunks/LpWindow-B8YSRVTH.js +297 -0
  127. package/dist/chunks/LpWorkspaceSwitcher-D0e9upAy.js +139 -0
  128. package/dist/chunks/{MenuTitle-BmCNNTKH.js → MenuTitle-Bh5dAdV1.js} +1 -1
  129. package/dist/chunks/{OptionContent-22UT8zLn.js → OptionContent-CGb8QlHA.js} +1 -1
  130. package/dist/chunks/{PickerField-Bfh8id3d.js → PickerField-BM1dyafh.js} +4 -4
  131. package/dist/chunks/{RevealToggle-BEit1n6Z.js → RevealToggle-CCPDE8Mv.js} +3 -3
  132. package/dist/chunks/SliderRail-DhPREa_R.js +18 -0
  133. package/dist/chunks/{TourCard-DNclH3u_.js → TourCard-DAzTYfav.js} +4 -4
  134. package/dist/chunks/{icons-DMITlfw5.js → icons-CYfcpSDS.js} +26 -10
  135. package/dist/component-names.d.ts +1 -1
  136. package/dist/components/AppShellContextNav.vue.d.ts +13 -0
  137. package/dist/components/AppShellContextNav.vue.js +4 -0
  138. package/dist/components/ChartRangeSelector.vue.d.ts +21 -0
  139. package/dist/components/ChartRangeSelector.vue.js +4 -0
  140. package/dist/components/ContextMenuItems.vue.js +1 -1
  141. package/dist/components/DropzoneLinkField.vue.d.ts +2 -2
  142. package/dist/components/DropzoneLinkField.vue.js +1 -1
  143. package/dist/components/FieldWrapper.vue.js +1 -1
  144. package/dist/components/IconCloseButton.vue.d.ts +7 -0
  145. package/dist/components/IconCloseButton.vue.js +4 -0
  146. package/dist/components/LayoutCanvas.vue.js +1 -1
  147. package/dist/components/LayoutNode.vue.js +1 -1
  148. package/dist/components/LpActionBar.vue.js +1 -1
  149. package/dist/components/LpActivityLine.vue.js +1 -1
  150. package/dist/components/LpAddressInput.vue.js +1 -1
  151. package/dist/components/LpAlert.vue.js +1 -1
  152. package/dist/components/LpAppShell.vue.d.ts +110 -2
  153. package/dist/components/LpAppShell.vue.js +1 -1
  154. package/dist/components/LpAutocomplete.vue.js +1 -1
  155. package/dist/components/LpAvatar.vue.js +1 -1
  156. package/dist/components/LpBadge.vue.js +1 -1
  157. package/dist/components/LpBottomNav.vue.d.ts +5 -0
  158. package/dist/components/LpBottomNav.vue.js +1 -1
  159. package/dist/components/LpBreadcrumbs.vue.js +1 -1
  160. package/dist/components/LpButton.vue.d.ts +14 -2
  161. package/dist/components/LpButton.vue.js +1 -1
  162. package/dist/components/LpCalendar.vue.js +1 -1
  163. package/dist/components/LpCard.vue.js +1 -1
  164. package/dist/components/LpChart.vue.d.ts +74 -2
  165. package/dist/components/LpChart.vue.js +326 -85
  166. package/dist/components/LpCheckbox.vue.d.ts +6 -2
  167. package/dist/components/LpCheckbox.vue.js +1 -1
  168. package/dist/components/LpCodeBlock.vue.js +1 -1
  169. package/dist/components/LpCodeDisplay.vue.js +1 -1
  170. package/dist/components/LpCollapse.vue.js +1 -1
  171. package/dist/components/LpColorPicker.vue.js +1 -1
  172. package/dist/components/LpCommandPalette.vue.d.ts +8 -0
  173. package/dist/components/LpCommandPalette.vue.js +107 -56
  174. package/dist/components/LpCommandScope.vue.js +1 -1
  175. package/dist/components/LpConfirmDialog.vue.js +1 -1
  176. package/dist/components/LpConfirmHost.vue.js +1 -1
  177. package/dist/components/LpContextMenu.vue.js +1 -1
  178. package/dist/components/LpContextSwitcher.vue.js +1 -1
  179. package/dist/components/LpCopyButton.vue.js +1 -1
  180. package/dist/components/LpCountdown.vue.js +1 -1
  181. package/dist/components/LpDataList.vue.d.ts +1 -1
  182. package/dist/components/LpDataList.vue.js +1 -1
  183. package/dist/components/LpDatePicker.vue.d.ts +3 -15
  184. package/dist/components/LpDatePicker.vue.js +1 -1
  185. package/dist/components/LpDateRangePicker.vue.d.ts +3 -13
  186. package/dist/components/LpDateRangePicker.vue.js +1 -1
  187. package/dist/components/LpDisclosure.vue.js +1 -1
  188. package/dist/components/LpDock.vue.js +21 -18
  189. package/dist/components/LpDrawer.vue.d.ts +1 -1
  190. package/dist/components/LpDrawer.vue.js +1 -1
  191. package/dist/components/LpDropdownMenu.vue.js +1 -1
  192. package/dist/components/LpDropzone.vue.js +14 -11
  193. package/dist/components/LpEditable.vue.js +1 -1
  194. package/dist/components/LpEmptyState.vue.js +1 -1
  195. package/dist/components/LpFieldNotice.vue.js +1 -1
  196. package/dist/components/LpFileTree.vue.d.ts +96 -6
  197. package/dist/components/LpFileTree.vue.js +1 -1
  198. package/dist/components/LpFileTreeNode.vue.d.ts +48 -0
  199. package/dist/components/LpFileTreeNode.vue.js +1 -1
  200. package/dist/components/LpFormField.vue.js +1 -1
  201. package/dist/components/LpFrame.vue.js +1 -1
  202. package/dist/components/LpGauge.vue.d.ts +1 -1
  203. package/dist/components/LpHintExample.vue.js +1 -1
  204. package/dist/components/LpHintLayer.vue.js +1 -1
  205. package/dist/components/LpHints.vue.js +1 -1
  206. package/dist/components/LpHoverCard.vue.js +1 -1
  207. package/dist/components/LpIcon.vue.d.ts +2 -0
  208. package/dist/components/LpIcon.vue.js +1 -1
  209. package/dist/components/LpIdenticon.vue.js +1 -1
  210. package/dist/components/LpImageEditor.vue.js +7 -6
  211. package/dist/components/LpInfraNode.vue.js +1 -1
  212. package/dist/components/LpInput.vue.d.ts +1 -1
  213. package/dist/components/LpInput.vue.js +1 -1
  214. package/dist/components/LpInspector.vue.d.ts +87 -0
  215. package/dist/components/LpInspector.vue.js +4 -0
  216. package/dist/components/LpLightbox.vue.d.ts +8 -0
  217. package/dist/components/LpLightbox.vue.js +1 -1
  218. package/dist/components/LpLoader.vue.d.ts +1 -1
  219. package/dist/components/LpLoader.vue.js +1 -1
  220. package/dist/components/LpLogInspector.vue.d.ts +18 -0
  221. package/dist/components/LpLogInspector.vue.js +4 -0
  222. package/dist/components/LpLogViewer.vue.d.ts +93 -0
  223. package/dist/components/LpLogViewer.vue.js +1 -1
  224. package/dist/components/LpMapPicker.vue.js +1 -1
  225. package/dist/components/LpMasonry.vue.js +1 -1
  226. package/dist/components/LpMediaGallery.vue.js +1 -1
  227. package/dist/components/LpModal.vue.d.ts +4 -2
  228. package/dist/components/LpModal.vue.js +1 -1
  229. package/dist/components/LpModalNav.vue.d.ts +5 -5
  230. package/dist/components/LpModalNav.vue.js +1 -1
  231. package/dist/components/LpNotificationBell.vue.d.ts +2 -2
  232. package/dist/components/LpNotificationBell.vue.js +1 -1
  233. package/dist/components/LpNumberField.vue.js +1 -1
  234. package/dist/components/LpOtpInput.vue.js +1 -1
  235. package/dist/components/LpPagination.vue.js +1 -1
  236. package/dist/components/LpPaletteFacts.vue.js +1 -1
  237. package/dist/components/LpPaletteHeader.vue.js +2 -2
  238. package/dist/components/LpPaletteTheme.vue.js +1 -1
  239. package/dist/components/LpPasswordInput.vue.js +1 -1
  240. package/dist/components/LpPhoneInput.vue.js +1 -1
  241. package/dist/components/LpPie.vue.d.ts +2 -2
  242. package/dist/components/LpPie.vue.js +1 -1
  243. package/dist/components/LpPipeline.vue.d.ts +4 -0
  244. package/dist/components/LpPipeline.vue.js +1 -1
  245. package/dist/components/LpPopover.vue.d.ts +7 -3
  246. package/dist/components/LpPopover.vue.js +1 -1
  247. package/dist/components/LpProgress.vue.d.ts +1 -1
  248. package/dist/components/LpProgress.vue.js +1 -1
  249. package/dist/components/LpQuota.vue.js +1 -1
  250. package/dist/components/LpRadioGroup.vue.js +1 -1
  251. package/dist/components/LpRangeCalendar.vue.js +1 -1
  252. package/dist/components/LpRangeSlider.vue.js +1 -1
  253. package/dist/components/LpRelativeTime.vue.js +1 -1
  254. package/dist/components/LpRepeater.vue.js +1 -1
  255. package/dist/components/LpRevealOnScroll.vue.js +1 -1
  256. package/dist/components/LpScramble.vue.js +1 -1
  257. package/dist/components/LpScrollArea.vue.js +1 -1
  258. package/dist/components/LpSecretField.vue.js +1 -1
  259. package/dist/components/LpSection.vue.js +1 -1
  260. package/dist/components/LpSegmented.vue.js +1 -1
  261. package/dist/components/LpSelect.vue.js +1 -1
  262. package/dist/components/LpShaderBackdrop.vue.js +1 -1
  263. package/dist/components/LpShift.vue.js +1 -1
  264. package/dist/components/LpShortcutsDialog.vue.js +1 -1
  265. package/dist/components/LpSidebar.vue.d.ts +1 -1
  266. package/dist/components/LpSidebar.vue.js +1 -1
  267. package/dist/components/LpSidebarNav.vue.js +1 -1
  268. package/dist/components/LpSidebarRow.vue.js +1 -1
  269. package/dist/components/LpSlider.vue.js +1 -1
  270. package/dist/components/LpSparkline.vue.d.ts +1 -1
  271. package/dist/components/LpSpotlight.vue.js +1 -1
  272. package/dist/components/LpStat.vue.js +1 -1
  273. package/dist/components/LpStatus.vue.js +1 -1
  274. package/dist/components/LpStepMark.vue.js +1 -1
  275. package/dist/components/LpStepper.vue.js +1 -1
  276. package/dist/components/LpSwap.vue.js +1 -1
  277. package/dist/components/LpSwitch.vue.js +1 -1
  278. package/dist/components/LpTabFace.vue.js +1 -1
  279. package/dist/components/LpTable.vue.d.ts +132 -0
  280. package/dist/components/LpTable.vue.js +547 -135
  281. package/dist/components/LpTableFilter.vue.js +1 -1
  282. package/dist/components/LpTableOfContents.vue.js +1 -1
  283. package/dist/components/LpTabs.vue.js +6 -5
  284. package/dist/components/LpTagsInput.vue.js +1 -1
  285. package/dist/components/LpTextarea.vue.js +1 -1
  286. package/dist/components/LpThemeProvider.vue.d.ts +2 -0
  287. package/dist/components/LpThemeProvider.vue.js +1 -1
  288. package/dist/components/LpThemeStudio.vue.js +1 -1
  289. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  290. package/dist/components/LpTimeField.vue.js +1 -1
  291. package/dist/components/LpToaster.vue.js +1 -1
  292. package/dist/components/LpTooltip.vue.js +1 -1
  293. package/dist/components/LpTopologyCanvas.vue.d.ts +131 -5
  294. package/dist/components/LpTopologyCanvas.vue.js +1 -1
  295. package/dist/components/LpTour.vue.js +1 -1
  296. package/dist/components/LpTruncate.vue.js +1 -1
  297. package/dist/components/LpTypewriter.vue.js +1 -1
  298. package/dist/components/LpUptimeBar.vue.js +1 -1
  299. package/dist/components/LpVideoBackdrop.vue.js +1 -1
  300. package/dist/components/LpWindow.vue.d.ts +99 -0
  301. package/dist/components/LpWindow.vue.js +4 -0
  302. package/dist/components/LpWorkspaceSwitcher.vue.d.ts +18 -0
  303. package/dist/components/LpWorkspaceSwitcher.vue.js +4 -0
  304. package/dist/components/MenuTitle.vue.js +1 -1
  305. package/dist/components/OptionContent.vue.js +1 -1
  306. package/dist/components/PickerField.vue.d.ts +2 -2
  307. package/dist/components/PickerField.vue.js +1 -1
  308. package/dist/components/RevealToggle.vue.js +1 -1
  309. package/dist/components/SliderRail.vue.d.ts +3 -0
  310. package/dist/components/SliderRail.vue.js +4 -0
  311. package/dist/components/TourCard.vue.js +1 -1
  312. package/dist/components/address.js +2 -1
  313. package/dist/components/appShell.d.ts +36 -0
  314. package/dist/components/appShell.js +30 -0
  315. package/dist/components/calendar.d.ts +12 -3
  316. package/dist/components/calendar.js +2 -5
  317. package/dist/components/chart.d.ts +21 -0
  318. package/dist/components/chart.js +4 -3
  319. package/dist/components/chartWindow.d.ts +57 -0
  320. package/dist/components/chartWindow.js +136 -0
  321. package/dist/components/color.js +1 -1
  322. package/dist/components/commandPalette.d.ts +16 -2
  323. package/dist/components/commandPalette.js +39 -4
  324. package/dist/components/controlSize.d.ts +5 -0
  325. package/dist/components/controlSize.js +8 -0
  326. package/dist/components/dropdown.d.ts +5 -0
  327. package/dist/components/dropdown.js +2 -0
  328. package/dist/components/exportData.d.ts +6 -1
  329. package/dist/components/exportData.js +25 -18
  330. package/dist/components/fileTree.d.ts +67 -0
  331. package/dist/components/fileTree.js +141 -1
  332. package/dist/components/identicon.js +3 -2
  333. package/dist/components/logTools.d.ts +93 -0
  334. package/dist/components/logTools.js +210 -0
  335. package/dist/components/menuWhen.js +1 -1
  336. package/dist/components/meter.js +2 -1
  337. package/dist/components/pipeline.d.ts +2 -0
  338. package/dist/components/pipeline.js +5 -0
  339. package/dist/components/relativeTime.js +2 -1
  340. package/dist/components/reveal.js +2 -1
  341. package/dist/components/select.d.ts +5 -0
  342. package/dist/components/select.js +10 -0
  343. package/dist/components/sidebar.d.ts +1 -0
  344. package/dist/components/tableFeatures.d.ts +58 -0
  345. package/dist/components/tableFeatures.js +87 -0
  346. package/dist/components/topologyGraph.d.ts +164 -0
  347. package/dist/components/topologyGraph.js +416 -0
  348. package/dist/components/tour.js +5 -3
  349. package/dist/components/videoBackdrop.js +7 -6
  350. package/dist/composables/clamp.d.ts +2 -0
  351. package/dist/composables/clamp.js +4 -0
  352. package/dist/composables/devWarn.d.ts +3 -0
  353. package/dist/composables/devWarn.js +14 -0
  354. package/dist/composables/easing.d.ts +13 -4
  355. package/dist/composables/easing.js +16 -10
  356. package/dist/composables/overlayHistory.js +14 -1
  357. package/dist/composables/pointerDrag.d.ts +15 -0
  358. package/dist/composables/pointerDrag.js +40 -0
  359. package/dist/composables/propCheck.d.ts +10 -0
  360. package/dist/composables/propCheck.js +78 -0
  361. package/dist/composables/realm.d.ts +6 -0
  362. package/dist/composables/realm.js +23 -0
  363. package/dist/composables/scroll.js +4 -2
  364. package/dist/composables/scrollMemory.d.ts +12 -1
  365. package/dist/composables/scrollMemory.js +36 -12
  366. package/dist/composables/spatialNav.js +2 -1
  367. package/dist/composables/textEntry.d.ts +17 -0
  368. package/dist/composables/textEntry.js +19 -0
  369. package/dist/composables/useAsyncTask.d.ts +6 -0
  370. package/dist/composables/useChartSync.d.ts +5 -0
  371. package/dist/composables/useChartSync.js +33 -0
  372. package/dist/composables/useCloseGuard.d.ts +3 -1
  373. package/dist/composables/useCloseGuard.js +6 -3
  374. package/dist/composables/useCommands.d.ts +50 -0
  375. package/dist/composables/useCommands.js +66 -0
  376. package/dist/composables/useField.d.ts +37 -0
  377. package/dist/composables/useField.js +27 -2
  378. package/dist/composables/useFitScale.js +4 -1
  379. package/dist/composables/useFormDraft.d.ts +13 -2
  380. package/dist/composables/useFormDraft.js +17 -9
  381. package/dist/composables/useHintSystem.js +2 -2
  382. package/dist/composables/useHotkeys.js +11 -32
  383. package/dist/composables/useInView.js +2 -1
  384. package/dist/composables/useInputFilter.js +7 -5
  385. package/dist/composables/useInputMask.js +5 -3
  386. package/dist/composables/useLinkedView.d.ts +97 -0
  387. package/dist/composables/useLinkedView.js +139 -0
  388. package/dist/composables/useOptimisticToggle.d.ts +3 -1
  389. package/dist/composables/useOptimisticToggle.js +2 -1
  390. package/dist/composables/useOverlayHistory.d.ts +7 -0
  391. package/dist/composables/useOverlayHistory.js +16 -2
  392. package/dist/composables/usePaintSelect.js +18 -14
  393. package/dist/composables/usePaletteCommands.d.ts +7 -0
  394. package/dist/composables/usePaletteCommands.js +12 -1
  395. package/dist/composables/usePersisted.d.ts +10 -0
  396. package/dist/composables/usePersisted.js +1 -1
  397. package/dist/composables/usePillTransition.d.ts +2 -2
  398. package/dist/composables/usePillTransition.js +5 -5
  399. package/dist/composables/useResizeObserver.js +2 -1
  400. package/dist/composables/useReveal.js +2 -1
  401. package/dist/composables/useRovingFocus.d.ts +2 -2
  402. package/dist/composables/useRovingFocus.js +9 -11
  403. package/dist/composables/useScramble.js +7 -1
  404. package/dist/composables/useShift.js +8 -6
  405. package/dist/composables/useTilt.js +2 -1
  406. package/dist/composables/useTween.js +9 -5
  407. package/dist/composables/useView.d.ts +1 -0
  408. package/dist/composables/useView.js +14 -2
  409. package/dist/composables/useZoomPan.js +2 -4
  410. package/dist/composables/windowing.d.ts +40 -0
  411. package/dist/composables/windowing.js +118 -0
  412. package/dist/icons-all.js +1 -1
  413. package/dist/icons.d.ts +4 -0
  414. package/dist/index.d.ts +30 -5
  415. package/dist/index.js +310 -264
  416. package/dist/layout/tree.js +19 -15
  417. package/dist/layout/useLayout.d.ts +2 -0
  418. package/dist/layout/useLayout.js +5 -1
  419. package/dist/layout/useWorkspaces.d.ts +52 -0
  420. package/dist/layout/useWorkspaces.js +140 -0
  421. package/dist/{chunks/backdrop-Dkf2vM81.js → theme/backdrop.js} +5 -5
  422. package/dist/theme/config.js +25 -0
  423. package/dist/theme/define.js +61 -0
  424. package/dist/{chunks/palette-Co8tSASo.js → theme/palette.js} +2 -2
  425. package/dist/theme/presets.js +239 -0
  426. package/dist/theme/studio.js +134 -0
  427. package/dist/theme/tokens.js +63 -0
  428. package/dist/theme/useTheme.d.ts +3 -2
  429. package/dist/theme/useTheme.js +325 -0
  430. package/package.json +8 -1
  431. package/src/component-names.ts +5 -1
  432. package/src/components/AppShellContextNav.vue +44 -0
  433. package/src/components/ChartRangeSelector.vue +120 -0
  434. package/src/components/IconCloseButton.vue +21 -0
  435. package/src/components/LayoutCanvas.vue +3 -0
  436. package/src/components/LayoutNode.vue +37 -38
  437. package/src/components/LpActivityLine.vue +4 -2
  438. package/src/components/LpAddressInput.vue +3 -3
  439. package/src/components/LpAppShell.vue +362 -24
  440. package/src/components/LpAutocomplete.vue +18 -30
  441. package/src/components/LpBottomNav.vue +9 -1
  442. package/src/components/LpBreadcrumbs.vue +2 -1
  443. package/src/components/LpButton.vue +11 -4
  444. package/src/components/LpChart.vue +336 -37
  445. package/src/components/LpCheckbox.vue +43 -9
  446. package/src/components/LpCodeBlock.vue +17 -23
  447. package/src/components/LpCollapse.vue +5 -4
  448. package/src/components/LpColorPicker.vue +8 -9
  449. package/src/components/LpCommandPalette.vue +133 -50
  450. package/src/components/LpContextMenu.vue +15 -13
  451. package/src/components/LpCountdown.vue +2 -1
  452. package/src/components/LpDataList.vue +13 -1
  453. package/src/components/LpDatePicker.vue +5 -17
  454. package/src/components/LpDateRangePicker.vue +5 -16
  455. package/src/components/LpDock.vue +25 -16
  456. package/src/components/LpDrawer.vue +70 -61
  457. package/src/components/LpDropdownMenu.vue +6 -10
  458. package/src/components/LpDropzone.vue +12 -7
  459. package/src/components/LpEditable.vue +6 -2
  460. package/src/components/LpFileTree.vue +507 -66
  461. package/src/components/LpFileTreeNode.vue +106 -5
  462. package/src/components/LpFormField.vue +1 -1
  463. package/src/components/LpFrame.vue +33 -13
  464. package/src/components/LpHoverCard.vue +30 -5
  465. package/src/components/LpIcon.vue +6 -2
  466. package/src/components/LpIdenticon.vue +2 -1
  467. package/src/components/LpImageEditor.vue +5 -2
  468. package/src/components/LpInput.vue +15 -23
  469. package/src/components/LpInspector.vue +197 -0
  470. package/src/components/LpLightbox.vue +74 -29
  471. package/src/components/LpLogInspector.vue +85 -0
  472. package/src/components/LpLogViewer.vue +239 -29
  473. package/src/components/LpMapPicker.vue +2 -1
  474. package/src/components/LpMasonry.vue +8 -5
  475. package/src/components/LpModal.vue +12 -12
  476. package/src/components/LpModalNav.vue +2 -21
  477. package/src/components/LpNumberField.vue +6 -15
  478. package/src/components/LpOtpInput.vue +2 -2
  479. package/src/components/LpPagination.vue +5 -3
  480. package/src/components/LpPhoneInput.vue +8 -19
  481. package/src/components/LpPie.vue +25 -23
  482. package/src/components/LpPipeline.vue +14 -1
  483. package/src/components/LpPopover.vue +23 -2
  484. package/src/components/LpProgress.vue +2 -1
  485. package/src/components/LpRadioGroup.vue +2 -2
  486. package/src/components/LpRangeSlider.vue +5 -6
  487. package/src/components/LpRevealOnScroll.vue +1 -1
  488. package/src/components/LpScrollArea.vue +28 -11
  489. package/src/components/LpSecretField.vue +2 -1
  490. package/src/components/LpSegmented.vue +6 -8
  491. package/src/components/LpSelect.vue +54 -24
  492. package/src/components/LpShaderBackdrop.vue +12 -9
  493. package/src/components/LpShift.vue +18 -15
  494. package/src/components/LpSidebar.vue +77 -74
  495. package/src/components/LpSidebarNav.vue +3 -2
  496. package/src/components/LpSidebarRow.vue +2 -1
  497. package/src/components/LpSlider.vue +5 -6
  498. package/src/components/LpSpotlight.vue +2 -1
  499. package/src/components/LpSwitch.vue +5 -3
  500. package/src/components/LpTable.vue +609 -93
  501. package/src/components/LpTableFilter.vue +1 -1
  502. package/src/components/LpTableOfContents.vue +16 -9
  503. package/src/components/LpTabs.vue +2 -1
  504. package/src/components/LpTagsInput.vue +12 -19
  505. package/src/components/LpTextarea.vue +5 -11
  506. package/src/components/LpThemeProvider.vue +29 -13
  507. package/src/components/LpTimeField.vue +4 -7
  508. package/src/components/LpToaster.vue +7 -6
  509. package/src/components/LpTooltip.vue +62 -18
  510. package/src/components/LpTopologyCanvas.vue +567 -42
  511. package/src/components/LpTour.vue +7 -5
  512. package/src/components/LpTruncate.vue +4 -3
  513. package/src/components/LpTypewriter.vue +2 -1
  514. package/src/components/LpVideoBackdrop.vue +11 -13
  515. package/src/components/LpWindow.vue +373 -0
  516. package/src/components/LpWorkspaceSwitcher.vue +116 -0
  517. package/src/components/PickerField.vue +2 -2
  518. package/src/components/SliderRail.vue +10 -0
  519. package/src/components/address.ts +3 -1
  520. package/src/components/appShell.ts +74 -0
  521. package/src/components/calendar.ts +12 -5
  522. package/src/components/chart.ts +27 -3
  523. package/src/components/chartWindow.ts +198 -0
  524. package/src/components/color.ts +2 -1
  525. package/src/components/commandPalette.ts +66 -5
  526. package/src/components/controlSize.ts +6 -0
  527. package/src/components/dropdown.ts +3 -0
  528. package/src/components/exportData.ts +33 -18
  529. package/src/components/fileTree.ts +215 -0
  530. package/src/components/identicon.ts +4 -2
  531. package/src/components/logTools.ts +298 -0
  532. package/src/components/meter.ts +2 -1
  533. package/src/components/pipeline.ts +6 -0
  534. package/src/components/relativeTime.ts +2 -1
  535. package/src/components/reveal.ts +3 -1
  536. package/src/components/select.ts +14 -0
  537. package/src/components/sidebar.ts +1 -0
  538. package/src/components/tableFeatures.ts +149 -0
  539. package/src/components/topologyGraph.ts +655 -0
  540. package/src/components/tour.ts +5 -3
  541. package/src/components/videoBackdrop.ts +7 -6
  542. package/src/composables/clamp.ts +2 -0
  543. package/src/composables/devWarn.ts +19 -0
  544. package/src/composables/easing.ts +37 -12
  545. package/src/composables/overlayHistory.ts +24 -0
  546. package/src/composables/pointerDrag.ts +54 -0
  547. package/src/composables/propCheck.ts +90 -0
  548. package/src/composables/realm.ts +25 -0
  549. package/src/composables/scroll.ts +4 -2
  550. package/src/composables/scrollMemory.ts +56 -14
  551. package/src/composables/spatialNav.ts +2 -1
  552. package/src/composables/textEntry.ts +33 -0
  553. package/src/composables/useAsyncTask.ts +6 -0
  554. package/src/composables/useChartSync.ts +52 -0
  555. package/src/composables/useCloseGuard.ts +22 -10
  556. package/src/composables/useCommands.ts +123 -0
  557. package/src/composables/useField.ts +41 -2
  558. package/src/composables/useFitScale.ts +4 -1
  559. package/src/composables/useFormDraft.ts +38 -14
  560. package/src/composables/useHintSystem.ts +2 -2
  561. package/src/composables/useHotkeys.ts +19 -48
  562. package/src/composables/useInView.ts +2 -1
  563. package/src/composables/useInputFilter.ts +8 -5
  564. package/src/composables/useInputMask.ts +8 -3
  565. package/src/composables/useLinkedView.ts +222 -0
  566. package/src/composables/useOptimisticToggle.ts +7 -1
  567. package/src/composables/useOverlayHistory.ts +20 -0
  568. package/src/composables/usePaintSelect.ts +20 -14
  569. package/src/composables/usePaletteCommands.ts +13 -0
  570. package/src/composables/usePersisted.ts +10 -0
  571. package/src/composables/usePillTransition.ts +9 -6
  572. package/src/composables/useResizeObserver.ts +2 -1
  573. package/src/composables/useReveal.ts +2 -1
  574. package/src/composables/useRovingFocus.ts +11 -14
  575. package/src/composables/useScramble.ts +12 -1
  576. package/src/composables/useShift.ts +8 -6
  577. package/src/composables/useTilt.ts +2 -1
  578. package/src/composables/useTween.ts +10 -5
  579. package/src/composables/useView.ts +17 -3
  580. package/src/composables/useZoomPan.ts +2 -5
  581. package/src/composables/windowing.ts +190 -0
  582. package/src/icons.ts +27 -6
  583. package/src/index.ts +41 -4
  584. package/src/kit-icons.ts +1 -1
  585. package/src/layout/tree.ts +31 -16
  586. package/src/layout/useLayout.ts +6 -0
  587. package/src/layout/useWorkspaces.ts +234 -0
  588. package/src/nuxt.ts +9 -1
  589. package/src/runtime/propCheck.plugin.ts +10 -0
  590. package/src/theme/studio.ts +7 -5
  591. package/src/theme/useTheme.ts +89 -15
  592. package/src/tokens/tokens.css +8 -1
  593. package/dist/assets/LpTabs-CJ0rYQbx.css +0 -18
  594. package/dist/chunks/LpAppShell-Dgejipzz.js +0 -319
  595. package/dist/chunks/LpFileTree-CooHinsC.js +0 -425
  596. package/dist/chunks/LpThemeProvider-8MG6yqU9.js +0 -640
  597. 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
- props.lanes.map(
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
- props.nodes.map((n): Node => {
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
- width: `${n.width ?? 260}px`,
251
- height: `${n.height ?? 160}px`,
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
- props.edges.map((e) => {
587
+ shown.value.edges.map((e) => {
292
588
  const obs = e.observed ?? "applied"
293
589
  const cat = edgeCategory(e)
294
- const color = edgeColor(cat, obs)
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" && !e.dimmed,
302
- label: e.kind,
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: e.dimmed ? 0.12 : 1,
609
+ opacity: faded ? 0.12 : 1,
311
610
  },
312
611
  markerEnd: { type: MarkerType.ArrowClosed, color },
313
612
  }
314
613
  }),
315
614
  )
316
615
 
317
- const {
318
- onNodeClick,
319
- onNodeDoubleClick,
320
- onNodeContextMenu,
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
- emit("node-drag-stop", { id: node.id, position: { ...node.position } }),
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(() => emit("pane-click"))
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 wide = typeof window === "undefined" || window.matchMedia("(min-width: 640px)").matches
403
- const legendHeight = wide && props.legend ? (legendBox.value?.offsetHeight ?? 0) + 15 : 0
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, wide && props.minimap ? MINIMAP_HEIGHT : 0)
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
- defineExpose({ fitView: fitFramed, setViewport: applyViewport })
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-sm:!hidden"
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="{ padding: framePadding() }" />
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-sm:hidden">
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"