project-graph-mcp 2.2.6 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/ARCHITECTURE.md +81 -0
  2. package/CHANGELOG.md +57 -0
  3. package/README.md +9 -4
  4. package/package.json +4 -13
  5. package/project-graph-mcp-2.3.0.tgz +0 -0
  6. package/src/compact/expand.js +1 -1
  7. package/src/core/graph-builder.js +2 -2
  8. package/src/core/parser.js +2 -2
  9. package/src/network/server.js +1 -2
  10. package/src/network/web-server.js +1 -1
  11. package/vendor/symbiote-node/CHANGELOG.md +31 -0
  12. package/vendor/symbiote-node/LICENSE +21 -0
  13. package/vendor/symbiote-node/README.md +206 -0
  14. package/vendor/symbiote-node/canvas/AutoLayout.js +725 -0
  15. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.css.js +73 -0
  16. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.js +93 -0
  17. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.tpl.js +9 -0
  18. package/vendor/symbiote-node/canvas/CanvasConnectionRenderer.js +962 -0
  19. package/vendor/symbiote-node/canvas/ConnectionRenderer.js +1468 -0
  20. package/vendor/symbiote-node/canvas/FlowSimulator.js +323 -0
  21. package/vendor/symbiote-node/canvas/ForceLayout.js +189 -0
  22. package/vendor/symbiote-node/canvas/ForceWorker.js +1325 -0
  23. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.css.js +97 -0
  24. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.js +176 -0
  25. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.tpl.js +12 -0
  26. package/vendor/symbiote-node/canvas/LODManager.js +88 -0
  27. package/vendor/symbiote-node/canvas/Minimap/Minimap.css.js +71 -0
  28. package/vendor/symbiote-node/canvas/Minimap/Minimap.js +207 -0
  29. package/vendor/symbiote-node/canvas/Minimap/Minimap.tpl.js +9 -0
  30. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.css.js +261 -0
  31. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.js +1840 -0
  32. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.tpl.js +22 -0
  33. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.css.js +97 -0
  34. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.js +132 -0
  35. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.tpl.js +21 -0
  36. package/vendor/symbiote-node/canvas/NodeViewManager.js +584 -0
  37. package/vendor/symbiote-node/canvas/PinExpansion.js +131 -0
  38. package/vendor/symbiote-node/canvas/PseudoConnection.js +80 -0
  39. package/vendor/symbiote-node/canvas/SubgraphManager.js +201 -0
  40. package/vendor/symbiote-node/canvas/SubgraphRouter.js +443 -0
  41. package/vendor/symbiote-node/canvas/ViewportActions.js +446 -0
  42. package/vendor/symbiote-node/core/Connection.js +45 -0
  43. package/vendor/symbiote-node/core/Editor.js +451 -0
  44. package/vendor/symbiote-node/core/Frame.js +31 -0
  45. package/vendor/symbiote-node/core/GraphMermaid.js +348 -0
  46. package/vendor/symbiote-node/core/GraphText.js +210 -0
  47. package/vendor/symbiote-node/core/Node.js +143 -0
  48. package/vendor/symbiote-node/core/Portal.js +104 -0
  49. package/vendor/symbiote-node/core/Socket.js +185 -0
  50. package/vendor/symbiote-node/core/SubgraphNode.js +125 -0
  51. package/vendor/symbiote-node/index.js +103 -0
  52. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.css.js +361 -0
  53. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.js +332 -0
  54. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.tpl.js +96 -0
  55. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.css.js +104 -0
  56. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.js +133 -0
  57. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.tpl.js +33 -0
  58. package/vendor/symbiote-node/interactions/ConnectFlow.js +307 -0
  59. package/vendor/symbiote-node/interactions/Drag.js +102 -0
  60. package/vendor/symbiote-node/interactions/Selector.js +132 -0
  61. package/vendor/symbiote-node/interactions/SnapGrid.js +65 -0
  62. package/vendor/symbiote-node/interactions/Zoom.js +140 -0
  63. package/vendor/symbiote-node/layout/ActionZone/ActionZone.css.js +88 -0
  64. package/vendor/symbiote-node/layout/ActionZone/ActionZone.js +254 -0
  65. package/vendor/symbiote-node/layout/ActionZone/ActionZone.tpl.js +11 -0
  66. package/vendor/symbiote-node/layout/Layout/Layout.css.js +88 -0
  67. package/vendor/symbiote-node/layout/Layout/Layout.js +622 -0
  68. package/vendor/symbiote-node/layout/Layout/Layout.tpl.js +25 -0
  69. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.css.js +293 -0
  70. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.js +467 -0
  71. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.tpl.js +33 -0
  72. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.css.js +46 -0
  73. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.js +102 -0
  74. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.tpl.js +6 -0
  75. package/vendor/symbiote-node/layout/LayoutRouter/LayoutRouter.js +156 -0
  76. package/vendor/symbiote-node/layout/LayoutRouter/routerSync.js +250 -0
  77. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.css.js +379 -0
  78. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.js +263 -0
  79. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.tpl.js +20 -0
  80. package/vendor/symbiote-node/layout/LayoutSidebar/SidebarSection.js +183 -0
  81. package/vendor/symbiote-node/layout/LayoutTree.js +246 -0
  82. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.css.js +43 -0
  83. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.js +89 -0
  84. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.tpl.js +14 -0
  85. package/vendor/symbiote-node/layout/index.js +16 -0
  86. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.css.js +61 -0
  87. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.js +79 -0
  88. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.tpl.js +19 -0
  89. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.css.js +41 -0
  90. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.js +24 -0
  91. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.tpl.js +16 -0
  92. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.css.js +65 -0
  93. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.js +29 -0
  94. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.tpl.js +13 -0
  95. package/vendor/symbiote-node/node/GraphNode/GraphNode.css.js +683 -0
  96. package/vendor/symbiote-node/node/GraphNode/GraphNode.js +92 -0
  97. package/vendor/symbiote-node/node/GraphNode/GraphNode.tpl.js +17 -0
  98. package/vendor/symbiote-node/node/NodeSocket/NodeSocket.js +25 -0
  99. package/vendor/symbiote-node/node/NodeSocket/NodeSocket.tpl.js +7 -0
  100. package/vendor/symbiote-node/node/PortItem/PortItem.css.js +90 -0
  101. package/vendor/symbiote-node/node/PortItem/PortItem.js +87 -0
  102. package/vendor/symbiote-node/node/PortItem/PortItem.tpl.js +10 -0
  103. package/vendor/symbiote-node/package.json +59 -0
  104. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.css.js +143 -0
  105. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.js +131 -0
  106. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.tpl.js +16 -0
  107. package/vendor/symbiote-node/plugins/History.js +384 -0
  108. package/vendor/symbiote-node/plugins/Readonly.js +59 -0
  109. package/vendor/symbiote-node/shapes/CircleShape.js +80 -0
  110. package/vendor/symbiote-node/shapes/CommentShape.js +35 -0
  111. package/vendor/symbiote-node/shapes/DiamondShape.js +115 -0
  112. package/vendor/symbiote-node/shapes/NodeShape.js +80 -0
  113. package/vendor/symbiote-node/shapes/PillShape.js +91 -0
  114. package/vendor/symbiote-node/shapes/RectShape.js +72 -0
  115. package/vendor/symbiote-node/shapes/SVGShape.js +494 -0
  116. package/vendor/symbiote-node/shapes/index.js +53 -0
  117. package/vendor/symbiote-node/themes/Palette.js +32 -0
  118. package/vendor/symbiote-node/themes/Skin.js +113 -0
  119. package/vendor/symbiote-node/themes/Theme.js +84 -0
  120. package/vendor/symbiote-node/themes/carbon.js +137 -0
  121. package/vendor/symbiote-node/themes/dark.js +137 -0
  122. package/vendor/symbiote-node/themes/ebook.js +138 -0
  123. package/vendor/symbiote-node/themes/grey.js +137 -0
  124. package/vendor/symbiote-node/themes/light.js +137 -0
  125. package/vendor/symbiote-node/themes/neon.js +138 -0
  126. package/vendor/symbiote-node/themes/pcb.js +273 -0
  127. package/vendor/symbiote-node/themes/synthwave.js +137 -0
  128. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.css.js +86 -0
  129. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.js +128 -0
  130. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.tpl.js +29 -0
  131. package/web/app.js +9 -5
  132. package/web/components/canvas-graph.js +1705 -0
  133. package/web/components/code-block.js +1 -1
  134. package/web/components/event-feed/CodeWidget.js +32 -0
  135. package/web/components/event-feed/EventWidget.js +97 -0
  136. package/web/components/event-feed/ListWidget.js +57 -0
  137. package/web/components/event-feed/MiniGraphWidget.js +159 -0
  138. package/web/components/follow-ribbon.js +134 -0
  139. package/web/dashboard.js +1 -1
  140. package/web/follow-controller.js +241 -0
  141. package/web/index.html +4 -0
  142. package/web/panels/ActionBoard/ActionBoard.js +1 -1
  143. package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -1
  144. package/web/panels/code-viewer.js +50 -15
  145. package/web/panels/dep-graph.js +2691 -7
  146. package/web/panels/file-tree.js +5 -2
  147. package/web/panels/live-monitor.js +75 -3
  148. package/web/style.css +39 -0
  149. package/docs/img/explorer-compact.jpg +0 -0
  150. package/docs/img/explorer-expanded.jpg +0 -0
  151. package/src/.contextignore +0 -22
  152. package/src/.project-graph-cache.json +0 -1
  153. package/src/compact/.project-graph-cache.json +0 -1
  154. package/web/.project-graph-cache.json +0 -1
  155. package/web/panels/SettingsPanel/.project-graph-cache.json +0 -1
@@ -0,0 +1,97 @@
1
+ /**
2
+ * GraphTabs styles
3
+ * @module symbiote-node/canvas/GraphTabs.css
4
+ */
5
+ import { css } from '@symbiotejs/symbiote';
6
+
7
+ export const styles = css`
8
+ graph-tabs {
9
+ display: flex;
10
+ align-items: stretch;
11
+ height: 32px;
12
+ background: var(--sn-ctx-bg, #1e1e2e);
13
+ border-bottom: 1px solid var(--sn-node-border, rgba(255, 255, 255, 0.08));
14
+ font-family: var(--sn-font, 'Inter', sans-serif);
15
+ font-size: 12px;
16
+ color: var(--sn-text-dim, #a0a0a0);
17
+ overflow-x: auto;
18
+ overflow-y: hidden;
19
+ user-select: none;
20
+ scrollbar-width: none;
21
+
22
+ &::-webkit-scrollbar {
23
+ display: none;
24
+ }
25
+ }
26
+
27
+ tab-item {
28
+ display: flex;
29
+ align-items: center;
30
+ gap: 6px;
31
+ padding: 0 14px;
32
+ cursor: pointer;
33
+ white-space: nowrap;
34
+ border-right: 1px solid var(--sn-node-border, rgba(255, 255, 255, 0.06));
35
+ transition: background 0.15s ease-out, color 0.15s ease-out;
36
+ position: relative;
37
+
38
+ &:hover {
39
+ background: color-mix(in srgb, currentColor 4%, transparent);
40
+ color: var(--sn-text, #cdd6f4);
41
+ }
42
+
43
+ &[data-active] {
44
+ background: var(--sn-node-bg, #2d2d3d);
45
+ color: var(--sn-text, #cdd6f4);
46
+
47
+ &::after {
48
+ content: '';
49
+ position: absolute;
50
+ bottom: 0;
51
+ left: 0;
52
+ right: 0;
53
+ height: 2px;
54
+ background: var(--sn-node-selected, #4a9eff);
55
+ }
56
+ }
57
+
58
+ & .material-symbols-outlined {
59
+ font-size: 14px;
60
+ }
61
+
62
+ & .tab-close {
63
+ font-size: 14px;
64
+ opacity: 0;
65
+ transition: opacity 0.15s;
66
+ padding: 2px;
67
+ border-radius: 3px;
68
+
69
+ &:hover {
70
+ background: color-mix(in srgb, currentColor 10%, transparent);
71
+ }
72
+ }
73
+
74
+ &:hover .tab-close {
75
+ opacity: 0.7;
76
+ }
77
+ }
78
+
79
+ .tab-add {
80
+ display: flex;
81
+ align-items: center;
82
+ justify-content: center;
83
+ width: 32px;
84
+ cursor: pointer;
85
+ color: var(--sn-text-dim, #a0a0a0);
86
+ transition: background 0.15s ease-out, color 0.15s ease-out;
87
+
88
+ &:hover {
89
+ background: color-mix(in srgb, currentColor 4%, transparent);
90
+ color: var(--sn-text, #cdd6f4);
91
+ }
92
+
93
+ & .material-symbols-outlined {
94
+ font-size: 16px;
95
+ }
96
+ }
97
+ `;
@@ -0,0 +1,176 @@
1
+ /**
2
+ * GraphTabs — Tab/Page management for multi-canvas workflows
3
+ *
4
+ * Each tab represents an independent graph page with its own
5
+ * set of nodes and connections. Provides tab bar UI and
6
+ * state switching for the NodeCanvas.
7
+ *
8
+ * @module symbiote-node/canvas/GraphTabs
9
+ */
10
+
11
+ import Symbiote, { html } from '@symbiotejs/symbiote';
12
+ import { template } from './GraphTabs.tpl.js';
13
+ import { styles } from './GraphTabs.css.js';
14
+
15
+ /**
16
+ * @typedef {Object} TabPage
17
+ * @property {string} id
18
+ * @property {string} name
19
+ * @property {Object} state - Serialized editor state
20
+ */
21
+
22
+ class TabItem extends Symbiote {
23
+ init$ = {
24
+ id: '',
25
+ name: '',
26
+ isActive: false,
27
+ showClose: false,
28
+ };
29
+
30
+ renderCallback() {
31
+ this.sub('isActive', (val) => {
32
+ this.toggleAttribute('data-active', val);
33
+ });
34
+ }
35
+ }
36
+
37
+ TabItem.template = html`
38
+ <span class="material-symbols-outlined">description</span>
39
+ <span ${{ textContent: 'name' }}></span>
40
+ <span
41
+ class="tab-close material-symbols-outlined"
42
+ ${{ onclick: '^onCloseTab', '@hidden': '!showClose' }}
43
+ >close</span>
44
+ `;
45
+
46
+ TabItem.reg('tab-item');
47
+
48
+ export class GraphTabs extends Symbiote {
49
+
50
+ init$ = {
51
+ tabItems: [],
52
+ };
53
+
54
+ /** @type {TabPage[]} */
55
+ #tabs = [];
56
+
57
+ /** @type {string} */
58
+ #activeTabId = '';
59
+
60
+ /** @type {function|null} */
61
+ #onSwitch = null;
62
+
63
+ /** @type {function|null} */
64
+ #onAdd = null;
65
+
66
+ /** @type {function|null} */
67
+ #onClose = null;
68
+
69
+ /**
70
+ * Set callbacks for tab events
71
+ * @param {object} callbacks
72
+ * @param {function} callbacks.onSwitch - (tabId) => void
73
+ * @param {function} callbacks.onAdd - () => TabPage
74
+ * @param {function} callbacks.onClose - (tabId) => void
75
+ */
76
+ setCallbacks(callbacks) {
77
+ this.#onSwitch = callbacks.onSwitch;
78
+ this.#onAdd = callbacks.onAdd;
79
+ this.#onClose = callbacks.onClose;
80
+ }
81
+
82
+ /**
83
+ * Add a tab
84
+ * @param {string} id
85
+ * @param {string} name
86
+ * @param {Object} [state={}]
87
+ */
88
+ addTab(id, name, state = {}) {
89
+ this.#tabs.push({ id, name, state });
90
+ this.#syncItems();
91
+ this.switchTo(id);
92
+ }
93
+
94
+ /**
95
+ * Switch to a tab
96
+ * @param {string} id
97
+ */
98
+ switchTo(id) {
99
+ this.#activeTabId = id;
100
+ this.#syncItems();
101
+ if (this.#onSwitch) this.#onSwitch(id);
102
+ }
103
+
104
+ /**
105
+ * Remove a tab
106
+ * @param {string} id
107
+ */
108
+ removeTab(id) {
109
+ const idx = this.#tabs.findIndex(t => t.id === id);
110
+ if (idx === -1) return;
111
+ this.#tabs.splice(idx, 1);
112
+ if (this.#activeTabId === id && this.#tabs.length > 0) {
113
+ this.switchTo(this.#tabs[Math.max(0, idx - 1)].id);
114
+ }
115
+ this.#syncItems();
116
+ if (this.#onClose) this.#onClose(id);
117
+ }
118
+
119
+ /** @returns {string} */
120
+ get activeTab() { return this.#activeTabId; }
121
+
122
+ /** @returns {TabPage[]} */
123
+ get tabs() { return [...this.#tabs]; }
124
+
125
+ /**
126
+ * Update tab state (for saving before switch)
127
+ * @param {string} id
128
+ * @param {Object} state
129
+ */
130
+ setTabState(id, state) {
131
+ const tab = this.#tabs.find(t => t.id === id);
132
+ if (tab) tab.state = state;
133
+ }
134
+
135
+ /**
136
+ * Get tab state
137
+ * @param {string} id
138
+ * @returns {Object|undefined}
139
+ */
140
+ getTabState(id) {
141
+ return this.#tabs.find(t => t.id === id)?.state;
142
+ }
143
+
144
+ #syncItems() {
145
+ const showClose = this.#tabs.length > 1;
146
+ this.$.tabItems = this.#tabs.map(t => ({
147
+ id: t.id,
148
+ name: t.name,
149
+ isActive: t.id === this.#activeTabId,
150
+ showClose,
151
+ }));
152
+ }
153
+
154
+ onTabClick(e) {
155
+ const item = e.target.closest('tab-item');
156
+ if (!item) return;
157
+ this.switchTo(item.$.id);
158
+ }
159
+
160
+ onCloseTab(e) {
161
+ e.stopPropagation();
162
+ const item = e.target.closest('tab-item');
163
+ if (item) this.removeTab(item.$.id);
164
+ }
165
+
166
+ onAddTab() {
167
+ if (this.#onAdd) {
168
+ const newTab = this.#onAdd();
169
+ if (newTab) this.addTab(newTab.id, newTab.name, newTab.state);
170
+ }
171
+ }
172
+ }
173
+
174
+ GraphTabs.template = template;
175
+ GraphTabs.rootStyles = styles;
176
+ GraphTabs.reg('graph-tabs');
@@ -0,0 +1,12 @@
1
+ /**
2
+ * GraphTabs template
3
+ * @module symbiote-node/canvas/GraphTabs.tpl
4
+ */
5
+ import { html } from '@symbiotejs/symbiote';
6
+
7
+ export const template = html`
8
+ <div ${{ itemize: 'tabItems', 'item-tag': 'tab-item', onclick: 'onTabClick' }}></div>
9
+ <div class="tab-add" title="New tab" ${{ onclick: 'onAddTab' }}>
10
+ <span class="material-symbols-outlined">add</span>
11
+ </div>
12
+ `;
@@ -0,0 +1,88 @@
1
+ export class LODManager {
2
+ /** @type {import('./NodeCanvas/NodeCanvas.js').NodeCanvas} */
3
+ #canvas;
4
+
5
+ /** @type {number} */
6
+ #threshold;
7
+
8
+ /** @type {string} */
9
+ #currentLod = 'collapsed';
10
+
11
+ /** @type {boolean} */
12
+ #attached = false;
13
+
14
+ #listeners = [];
15
+
16
+ /**
17
+ * @param {import('./NodeCanvas/NodeCanvas.js').NodeCanvas} canvas
18
+ * @param {object} config
19
+ * @param {number} [config.threshold=0.7] - Zoom level at which LOD expands
20
+ */
21
+ constructor(canvas, { threshold = 0.7 } = {}) {
22
+ this.#canvas = canvas;
23
+ this.#threshold = threshold;
24
+ }
25
+
26
+ get currentLod() {
27
+ return this.#currentLod;
28
+ }
29
+
30
+ /**
31
+ * Attach LOD tracking to the canvas.
32
+ * Note: Symbiote's sub() does not return an unsubscribe handle —
33
+ * subscriptions are auto-cleaned when the canvas component is destroyed.
34
+ */
35
+ attach() {
36
+ if (this.#attached || !this.#canvas) return;
37
+ this.#attached = true;
38
+
39
+ const initialZoom = this.#canvas.$.zoom || 1;
40
+ this.#currentLod = initialZoom >= this.#threshold ? 'expanded' : 'collapsed';
41
+
42
+ this.#canvas.sub('zoom', (zoom) => {
43
+ if (!this.#attached) return; // guard after destroy
44
+ const newLod = zoom >= this.#threshold ? 'expanded' : 'collapsed';
45
+ if (newLod === this.#currentLod) return;
46
+
47
+ this.#currentLod = newLod;
48
+ this.#emit(newLod);
49
+ });
50
+ }
51
+
52
+ /**
53
+ * Perform immediate LOD update evaluation (e.g. after manual navigation or fitView)
54
+ */
55
+ update() {
56
+ if (!this.#canvas || !this.#attached) return;
57
+ const zoom = this.#canvas.$.zoom || 1;
58
+ const newLod = zoom >= this.#threshold ? 'expanded' : 'collapsed';
59
+ if (newLod !== this.#currentLod) {
60
+ this.#currentLod = newLod;
61
+ this.#emit(newLod);
62
+ }
63
+ }
64
+
65
+ /**
66
+ * @param {Function} callback
67
+ */
68
+ onLodChange(callback) {
69
+ this.#listeners.push(callback);
70
+ // Immediately notify new listener of current state
71
+ if (this.#attached) {
72
+ callback(this.#currentLod);
73
+ }
74
+ }
75
+
76
+ #emit(lod) {
77
+ for (const fn of this.#listeners) {
78
+ fn(lod);
79
+ }
80
+ }
81
+
82
+ destroy() {
83
+ // Symbiote sub() auto-cleans on component disconnect.
84
+ // We just disable the guard flag so the callback becomes a no-op.
85
+ this.#listeners = [];
86
+ this.#attached = false;
87
+ }
88
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Minimap styles
3
+ * @module symbiote-node/canvas/Minimap.css
4
+ */
5
+ import { css } from '@symbiotejs/symbiote';
6
+
7
+ export const styles = css`
8
+ node-minimap {
9
+ position: absolute;
10
+ bottom: 16px;
11
+ right: 16px;
12
+ width: 200px;
13
+ height: 140px;
14
+ border-radius: 8px;
15
+ overflow: hidden;
16
+ border: 1px solid var(--sn-node-border, rgba(255,255,255,0.08));
17
+ box-shadow: 0 4px 16px var(--sn-shadow-color, rgba(0,0,0,0.3));
18
+ z-index: 90;
19
+ cursor: crosshair;
20
+ opacity: 1;
21
+ transition: opacity 0.4s ease;
22
+
23
+ &[hidden] {
24
+ display: none;
25
+ }
26
+
27
+ &[data-fading] {
28
+ opacity: 0;
29
+ pointer-events: none;
30
+ }
31
+
32
+ & canvas {
33
+ display: block;
34
+ width: 100%;
35
+ height: 100%;
36
+ }
37
+ }
38
+
39
+ .sn-minimap-toggle {
40
+ position: absolute;
41
+ bottom: 16px;
42
+ right: 16px;
43
+ width: 32px;
44
+ height: 32px;
45
+ border-radius: 6px;
46
+ border: 1px solid var(--sn-node-border, rgba(255,255,255,0.08));
47
+ background: var(--sn-node-bg, #2a2a3e);
48
+ color: var(--sn-text-dim, #888);
49
+ cursor: pointer;
50
+ z-index: 89;
51
+ display: flex;
52
+ align-items: center;
53
+ justify-content: center;
54
+ padding: 0;
55
+ transition: color 0.15s, border-color 0.15s;
56
+
57
+ & .material-symbols-outlined {
58
+ font-size: 18px;
59
+ }
60
+
61
+ &:hover {
62
+ color: var(--sn-text, #ddd);
63
+ border-color: var(--sn-node-selected, #4a9eff);
64
+ }
65
+
66
+ &[data-active] {
67
+ color: var(--sn-node-selected, #4a9eff);
68
+ border-color: var(--sn-node-selected, #4a9eff);
69
+ }
70
+ }
71
+ `;
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Minimap — small overview of the entire node graph
3
+ *
4
+ * Shows a scaled-down view of all nodes and connections.
5
+ * The viewport rectangle shows current visible area and can be dragged to pan.
6
+ * Positioned bottom-right of the canvas.
7
+ *
8
+ * @module symbiote-node/canvas/Minimap
9
+ */
10
+
11
+ import Symbiote from '@symbiotejs/symbiote';
12
+ import { template } from './Minimap.tpl.js';
13
+ import { styles } from './Minimap.css.js';
14
+
15
+ export class Minimap extends Symbiote {
16
+
17
+ init$ = {
18
+ visible: true,
19
+ };
20
+
21
+ /** @type {HTMLCanvasElement|null} */
22
+ #canvas2d = null;
23
+
24
+ /** @type {CanvasRenderingContext2D|null} */
25
+ #ctx = null;
26
+
27
+ /** @type {function|null} */
28
+ #getState = null;
29
+
30
+ /** @type {boolean} */
31
+ #isDragging = false;
32
+
33
+ /** @type {number} */
34
+ #rafId = 0;
35
+
36
+ renderCallback() {
37
+ this.#canvas2d = this.querySelector('canvas');
38
+ this.#ctx = this.#canvas2d?.getContext('2d');
39
+
40
+ // Pointer events for viewport drag
41
+ this.#canvas2d?.addEventListener('pointerdown', (e) => this.#onPointerDown(e));
42
+ window.addEventListener('pointermove', (e) => this.#onPointerMove(e));
43
+ window.addEventListener('pointerup', () => this.#onPointerUp());
44
+ }
45
+
46
+ /**
47
+ * Set state getter for reading canvas state
48
+ * @param {function} fn - returns { nodes, transform, containerSize }
49
+ */
50
+ setStateGetter(fn) {
51
+ this.#getState = fn;
52
+ this.#startLoop();
53
+ }
54
+
55
+ #startLoop() {
56
+ const draw = () => {
57
+ this.#draw();
58
+ this.#rafId = requestAnimationFrame(draw);
59
+ };
60
+ this.#rafId = requestAnimationFrame(draw);
61
+ }
62
+
63
+ #draw() {
64
+ const ctx = this.#ctx;
65
+ const canvas = this.#canvas2d;
66
+ if (!ctx || !canvas || !this.#getState) return;
67
+
68
+ const state = this.#getState();
69
+ if (!state) return;
70
+
71
+ const { nodes, transform, containerSize } = state;
72
+ const w = canvas.width;
73
+ const h = canvas.height;
74
+
75
+ // Read theme colors from CSS variables
76
+ const cs = getComputedStyle(this);
77
+ const bgColor = cs.getPropertyValue('--sn-minimap-bg').trim()
78
+ || cs.getPropertyValue('--sn-bg').trim()
79
+ || 'rgba(20, 20, 35, 0.85)';
80
+ const nodeColor = cs.getPropertyValue('--sn-minimap-node').trim()
81
+ || 'rgba(80, 130, 200, 0.6)';
82
+ const nodeStroke = cs.getPropertyValue('--sn-minimap-node-stroke').trim()
83
+ || cs.getPropertyValue('--sn-node-border').trim()
84
+ || 'rgba(120, 170, 255, 0.3)';
85
+ const vpStroke = cs.getPropertyValue('--sn-minimap-viewport').trim()
86
+ || 'rgba(255, 255, 255, 0.6)';
87
+ const vpFill = cs.getPropertyValue('--sn-minimap-viewport-fill').trim()
88
+ || 'rgba(255, 255, 255, 0.04)';
89
+
90
+ // Clear
91
+ ctx.clearRect(0, 0, w, h);
92
+
93
+ // Background
94
+ ctx.fillStyle = bgColor;
95
+ ctx.fillRect(0, 0, w, h);
96
+
97
+ if (nodes.length === 0) return;
98
+
99
+ // Calculate bounds of all nodes
100
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
101
+ for (const n of nodes) {
102
+ minX = Math.min(minX, n.x);
103
+ minY = Math.min(minY, n.y);
104
+ maxX = Math.max(maxX, n.x + (n.width || 200));
105
+ maxY = Math.max(maxY, n.y + (n.height || 100));
106
+ }
107
+
108
+ // Add padding
109
+ const pad = 100;
110
+ minX -= pad; minY -= pad;
111
+ maxX += pad; maxY += pad;
112
+
113
+ const graphW = maxX - minX;
114
+ const graphH = maxY - minY;
115
+
116
+ // Scale to fit
117
+ const scaleX = w / graphW;
118
+ const scaleY = h / graphH;
119
+ const scale = Math.min(scaleX, scaleY);
120
+
121
+ const offsetX = (w - graphW * scale) / 2;
122
+ const offsetY = (h - graphH * scale) / 2;
123
+
124
+ // Store for viewport drag
125
+ this._mapMinX = minX;
126
+ this._mapMinY = minY;
127
+ this._mapScale = scale;
128
+ this._mapOffsetX = offsetX;
129
+ this._mapOffsetY = offsetY;
130
+
131
+ // Draw nodes as rectangles
132
+ for (const n of nodes) {
133
+ const x = (n.x - minX) * scale + offsetX;
134
+ const y = (n.y - minY) * scale + offsetY;
135
+ const nw = (n.width || 200) * scale;
136
+ const nh = (n.height || 80) * scale;
137
+
138
+ ctx.fillStyle = n.bypassed ? 'rgba(100, 100, 100, 0.5)' : nodeColor;
139
+ ctx.fillRect(x, y, nw, nh);
140
+ ctx.strokeStyle = nodeStroke;
141
+ ctx.lineWidth = 0.5;
142
+ ctx.strokeRect(x, y, nw, nh);
143
+ }
144
+
145
+ // Draw viewport rectangle
146
+ if (containerSize && transform) {
147
+ const vx = (-transform.x / transform.zoom - minX) * scale + offsetX;
148
+ const vy = (-transform.y / transform.zoom - minY) * scale + offsetY;
149
+ const vw = (containerSize.width / transform.zoom) * scale;
150
+ const vh = (containerSize.height / transform.zoom) * scale;
151
+
152
+ ctx.strokeStyle = vpStroke;
153
+ ctx.lineWidth = 1.5;
154
+ ctx.strokeRect(vx, vy, vw, vh);
155
+ ctx.fillStyle = vpFill;
156
+ ctx.fillRect(vx, vy, vw, vh);
157
+ }
158
+ }
159
+
160
+ #onPointerDown(e) {
161
+ this.#isDragging = true;
162
+ this.#navigateTo(e);
163
+ e.preventDefault();
164
+ }
165
+
166
+ #onPointerMove(e) {
167
+ if (!this.#isDragging) return;
168
+ this.#navigateTo(e);
169
+ }
170
+
171
+ #onPointerUp() {
172
+ this.#isDragging = false;
173
+ }
174
+
175
+ #navigateTo(e) {
176
+ if (!this.#getState || !this.#canvas2d) return;
177
+ const rect = this.#canvas2d.getBoundingClientRect();
178
+ const mx = e.clientX - rect.left;
179
+ const my = e.clientY - rect.top;
180
+
181
+ const state = this.#getState();
182
+ if (!state?.containerSize || !state?.transform) return;
183
+
184
+ // Convert minimap coords to graph coords
185
+ const graphX = (mx - this._mapOffsetX) / this._mapScale + this._mapMinX;
186
+ const graphY = (my - this._mapOffsetY) / this._mapScale + this._mapMinY;
187
+
188
+ // Center the viewport on that point
189
+ const zoom = state.transform.zoom;
190
+ const newX = -(graphX * zoom - state.containerSize.width / 2);
191
+ const newY = -(graphY * zoom - state.containerSize.height / 2);
192
+
193
+ // Dispatch event so NodeCanvas can update its transform
194
+ this.dispatchEvent(new CustomEvent('minimap-navigate', {
195
+ detail: { x: newX, y: newY },
196
+ bubbles: true,
197
+ }));
198
+ }
199
+
200
+ destroyCallback() {
201
+ cancelAnimationFrame(this.#rafId);
202
+ }
203
+ }
204
+
205
+ Minimap.template = template;
206
+ Minimap.rootStyles = styles;
207
+ Minimap.reg('node-minimap');
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Minimap template
3
+ * @module symbiote-node/canvas/Minimap.tpl
4
+ */
5
+ import { html } from '@symbiotejs/symbiote';
6
+
7
+ export const template = html`
8
+ <canvas width="200" height="140"></canvas>
9
+ `;