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,183 @@
1
+ /**
2
+ * SidebarSection — itemize sub-component for sidebar section items
3
+ *
4
+ * Renders a master panel entry with icon, label.
5
+ * Edit mode: shows visibility toggle + drag handle.
6
+ * Shows sub-panel list with close buttons for non-master panels.
7
+ *
8
+ * @module symbiote-node/layout/LayoutSidebar/SidebarSection
9
+ */
10
+ import Symbiote, { html } from '@symbiotejs/symbiote';
11
+ import { navigate } from '../LayoutRouter/LayoutRouter.js';
12
+
13
+ export class SidebarSection extends Symbiote {
14
+ isoMode = true;
15
+
16
+ init$ = {
17
+ sectionId: '',
18
+ icon: 'dashboard',
19
+ label: '',
20
+ isActive: false,
21
+ isVisible: true,
22
+ isDisabled: false,
23
+ eyeIcon: 'visibility',
24
+ hasSubPanels: false,
25
+ isExpanded: false,
26
+ subPanels: [],
27
+
28
+ onSectionClick: () => {
29
+ if (!this.$.isVisible || this.$.isDisabled) return;
30
+ navigate(this.$.sectionId);
31
+ },
32
+
33
+ onExpandToggle: (e) => {
34
+ e.stopPropagation();
35
+ if (!this.$.hasSubPanels) return;
36
+ this.$.isExpanded = !this.$.isExpanded;
37
+ },
38
+
39
+ onToggleVisibility: (e) => {
40
+ e.stopPropagation();
41
+ const sidebar = this.closest('layout-sidebar');
42
+ if (sidebar) sidebar.toggleVisibility(this.$.sectionId);
43
+ },
44
+ };
45
+
46
+ renderCallback() {
47
+ this.sub('isActive', (val) => {
48
+ this.toggleAttribute('data-active', val);
49
+ });
50
+
51
+ this.sub('isExpanded', (val) => {
52
+ this.toggleAttribute('data-expanded', val);
53
+ });
54
+
55
+ this.sub('isVisible', (val) => {
56
+ this.toggleAttribute('data-hidden', !val);
57
+ this.$.eyeIcon = val ? 'visibility' : 'visibility_off';
58
+ });
59
+
60
+ this.sub('isDisabled', (val) => {
61
+ this.toggleAttribute('data-disabled', val);
62
+ });
63
+
64
+ this.sub('subPanels', (panels) => {
65
+ const has = panels && panels.length > 0;
66
+ this.$.hasSubPanels = has;
67
+ this.toggleAttribute('data-has-sub', has);
68
+ // Collapse if no sub-panels
69
+ if (!has) this.$.isExpanded = false;
70
+ });
71
+
72
+ // Drag support
73
+ this.setAttribute('draggable', 'true');
74
+
75
+ this.addEventListener('dragstart', (e) => {
76
+ const sidebar = this.closest('layout-sidebar');
77
+ if (!sidebar?.hasAttribute('edit-mode')) {
78
+ e.preventDefault();
79
+ return;
80
+ }
81
+ const sections = Array.from(this.parentElement.children);
82
+ const idx = sections.indexOf(this);
83
+ e.dataTransfer.setData('text/plain', String(idx));
84
+ e.dataTransfer.effectAllowed = 'move';
85
+ this.setAttribute('data-dragging', '');
86
+ });
87
+
88
+ this.addEventListener('dragend', () => {
89
+ this.removeAttribute('data-dragging');
90
+ });
91
+
92
+ this.addEventListener('dragover', (e) => {
93
+ const sidebar = this.closest('layout-sidebar');
94
+ if (!sidebar?.hasAttribute('edit-mode')) return;
95
+ e.preventDefault();
96
+ e.dataTransfer.dropEffect = 'move';
97
+ this.setAttribute('data-dragover', '');
98
+ });
99
+
100
+ this.addEventListener('dragleave', () => {
101
+ this.removeAttribute('data-dragover');
102
+ });
103
+
104
+ this.addEventListener('drop', (e) => {
105
+ e.preventDefault();
106
+ this.removeAttribute('data-dragover');
107
+ const sidebar = this.closest('layout-sidebar');
108
+ if (!sidebar?.hasAttribute('edit-mode')) return;
109
+
110
+ const fromIdx = parseInt(e.dataTransfer.getData('text/plain'), 10);
111
+ const sections = Array.from(this.parentElement.children);
112
+ const toIdx = sections.indexOf(this);
113
+ if (fromIdx !== toIdx) {
114
+ sidebar.moveSection(fromIdx, toIdx);
115
+ }
116
+ });
117
+ }
118
+ }
119
+
120
+ SidebarSection.template = html`
121
+ <div class="sec-drag-handle">
122
+ <span class="material-symbols-outlined">drag_indicator</span>
123
+ </div>
124
+ <div class="sec-item" ${{ onclick: 'onSectionClick' }}>
125
+ <span class="material-symbols-outlined sec-icon" ${{ textContent: 'icon' }}></span>
126
+ <span class="sec-label" ${{ textContent: 'label' }}></span>
127
+ <span class="material-symbols-outlined sec-expand" ${{ onclick: 'onExpandToggle' }}>chevron_right</span>
128
+ </div>
129
+ <button class="sec-eye" ${{ onclick: 'onToggleVisibility' }}>
130
+ <span class="material-symbols-outlined" ${{ textContent: 'eyeIcon' }}></span>
131
+ </button>
132
+ <div class="sec-sub-panels" itemize="subPanels" item-tag="sidebar-sub-item"></div>
133
+ `;
134
+
135
+ SidebarSection.reg('sidebar-section');
136
+
137
+ /**
138
+ * SidebarSubItem — sub-panel entry inside a section
139
+ * Shows close button for non-master panels (isMaster=false)
140
+ */
141
+ export class SidebarSubItem extends Symbiote {
142
+ isoMode = true;
143
+
144
+ init$ = {
145
+ title: '',
146
+ icon: 'web_asset',
147
+ panelId: '',
148
+ isMaster: false,
149
+
150
+ onClose: (e) => {
151
+ e.stopPropagation();
152
+ const panelId = this.$.panelId;
153
+ if (!panelId) return;
154
+
155
+ // Find the panel-layout and call joinPanels
156
+ const sidebar = this.closest('layout-sidebar');
157
+ if (sidebar) {
158
+ sidebar.dispatchEvent(new CustomEvent('panel-close', {
159
+ bubbles: true,
160
+ detail: { panelId },
161
+ }));
162
+ }
163
+ },
164
+ };
165
+
166
+ renderCallback() {
167
+ this.sub('isMaster', (val) => {
168
+ this.toggleAttribute('data-master', val);
169
+ });
170
+ }
171
+ }
172
+
173
+ SidebarSubItem.template = html`
174
+ <div class="sub-panel-item">
175
+ <span class="material-symbols-outlined" ${{ textContent: 'icon' }}></span>
176
+ <span ${{ textContent: 'title' }}></span>
177
+ <button class="sub-panel-close" ${{ onclick: 'onClose' }}>
178
+ <span class="material-symbols-outlined">close</span>
179
+ </button>
180
+ </div>
181
+ `;
182
+
183
+ SidebarSubItem.reg('sidebar-sub-item');
@@ -0,0 +1,246 @@
1
+ /**
2
+ * @fileoverview BSP (Binary Space Partitioning) Layout Tree
3
+ * Implements Blender-style area splitting/joining mechanics.
4
+ */
5
+
6
+ /**
7
+ * @typedef {'horizontal' | 'vertical'} SplitDirection
8
+ */
9
+
10
+ /**
11
+ * @typedef {Object} PanelNode
12
+ * @property {string} id - Unique node ID
13
+ * @property {'panel'} type - Node type
14
+ * @property {string} panelType - Panel content type (e.g., 'viewport', 'timeline')
15
+ * @property {boolean} [collapsed] - Whether panel is collapsed
16
+ * @property {Object} [panelState] - Panel-specific state
17
+ */
18
+
19
+ /**
20
+ * @typedef {Object} SplitNode
21
+ * @property {string} id - Unique node ID
22
+ * @property {'split'} type - Node type
23
+ * @property {SplitDirection} direction - Split direction
24
+ * @property {number} ratio - Split ratio (0-1), size of first child
25
+ * @property {LayoutNode} first - First child node
26
+ * @property {LayoutNode} second - Second child node
27
+ */
28
+
29
+ /**
30
+ * @typedef {PanelNode | SplitNode} LayoutNode
31
+ */
32
+
33
+ let idCounter = 0;
34
+
35
+ /**
36
+ * Generate unique node ID
37
+ * @returns {string}
38
+ */
39
+ export function generateId() {
40
+ return `node_${++idCounter}_${Date.now().toString(36)}`;
41
+ }
42
+
43
+ /**
44
+ * Create a panel node
45
+ * @param {string} panelType - Panel content type
46
+ * @param {Object} [panelState] - Initial panel state
47
+ * @returns {PanelNode}
48
+ */
49
+ export function createPanel(panelType, panelState = {}) {
50
+ return {
51
+ id: generateId(),
52
+ type: 'panel',
53
+ panelType,
54
+ panelState,
55
+ collapsed: false
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Create a split node
61
+ * @param {SplitDirection} direction - Split direction
62
+ * @param {LayoutNode} first - First child
63
+ * @param {LayoutNode} second - Second child
64
+ * @param {number} [ratio=0.5] - Split ratio
65
+ * @returns {SplitNode}
66
+ */
67
+ export function createSplit(direction, first, second, ratio = 0.5) {
68
+ return {
69
+ id: generateId(),
70
+ type: 'split',
71
+ direction,
72
+ ratio,
73
+ first,
74
+ second
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Find a node by ID in the tree
80
+ * @param {LayoutNode} root - Root node
81
+ * @param {string} id - Node ID to find
82
+ * @returns {LayoutNode | null}
83
+ */
84
+ export function findNode(root, id) {
85
+ if (root.id === id) return root;
86
+ if (root.type === 'split') {
87
+ return findNode(root.first, id) || findNode(root.second, id);
88
+ }
89
+ return null;
90
+ }
91
+
92
+ /**
93
+ * Find parent of a node
94
+ * @param {LayoutNode} root - Root node
95
+ * @param {string} id - Child node ID
96
+ * @returns {{ parent: SplitNode, which: 'first' | 'second' } | null}
97
+ */
98
+ export function findParent(root, id) {
99
+ if (root.type !== 'split') return null;
100
+
101
+ if (root.first.id === id) return { parent: root, which: 'first' };
102
+ if (root.second.id === id) return { parent: root, which: 'second' };
103
+
104
+ return findParent(root.first, id) || findParent(root.second, id);
105
+ }
106
+
107
+ /**
108
+ * Split a panel into two
109
+ * @param {LayoutNode} root - Root node
110
+ * @param {string} panelId - Panel ID to split
111
+ * @param {SplitDirection} direction - Split direction
112
+ * @param {number} [ratio=0.5] - Split ratio
113
+ * @param {string} [newPanelType] - Type for new panel (defaults to same as original)
114
+ * @returns {LayoutNode} - New root node
115
+ */
116
+ export function splitPanel(root, panelId, direction, ratio = 0.5, newPanelType) {
117
+ const node = findNode(root, panelId);
118
+ if (!node || node.type !== 'panel') {
119
+ console.warn(`Cannot split: panel ${panelId} not found`);
120
+ return root;
121
+ }
122
+
123
+ const newPanel = createPanel(newPanelType || node.panelType);
124
+ const splitNode = createSplit(direction, node, newPanel, ratio);
125
+
126
+ // If splitting the root
127
+ if (root.id === panelId) {
128
+ return splitNode;
129
+ }
130
+
131
+ // Find parent and replace
132
+ const parentInfo = findParent(root, panelId);
133
+ if (parentInfo) {
134
+ parentInfo.parent[parentInfo.which] = splitNode;
135
+ }
136
+
137
+ return root;
138
+ }
139
+
140
+ /**
141
+ * Join two panels (remove one panel and its parent split)
142
+ * @param {LayoutNode} root - Root node
143
+ * @param {string} panelToRemove - Panel ID to remove
144
+ * @returns {LayoutNode} - New root node
145
+ */
146
+ export function joinPanels(root, panelToRemove) {
147
+ const parentInfo = findParent(root, panelToRemove);
148
+ if (!parentInfo) {
149
+ // Trying to remove root - not allowed
150
+ console.warn('Cannot join: panel is root');
151
+ return root;
152
+ }
153
+
154
+ const { parent, which } = parentInfo;
155
+ const survivor = which === 'first' ? parent.second : parent.first;
156
+
157
+ // If parent is root, survivor becomes new root
158
+ const grandparentInfo = findParent(root, parent.id);
159
+ if (!grandparentInfo) {
160
+ return survivor;
161
+ }
162
+
163
+ // Replace parent with survivor in grandparent
164
+ grandparentInfo.parent[grandparentInfo.which] = survivor;
165
+ return root;
166
+ }
167
+
168
+ /**
169
+ * Update split ratio
170
+ * @param {LayoutNode} root - Root node
171
+ * @param {string} splitId - Split node ID
172
+ * @param {number} ratio - New ratio (0-1)
173
+ * @returns {LayoutNode} - Same root (mutated)
174
+ */
175
+ export function resizeSplit(root, splitId, ratio) {
176
+ const node = findNode(root, splitId);
177
+ if (!node || node.type !== 'split') {
178
+ console.warn(`Cannot resize: split ${splitId} not found`);
179
+ return root;
180
+ }
181
+
182
+ node.ratio = Math.max(0.1, Math.min(0.9, ratio));
183
+ return root;
184
+ }
185
+
186
+ /**
187
+ * Serialize layout to JSON string
188
+ * @param {LayoutNode} root - Root node
189
+ * @returns {string}
190
+ */
191
+ export function serialize(root) {
192
+ return JSON.stringify(root);
193
+ }
194
+
195
+ /**
196
+ * Deserialize layout from JSON string
197
+ * @param {string} json - JSON string
198
+ * @returns {LayoutNode}
199
+ */
200
+ export function deserialize(json) {
201
+ return JSON.parse(json);
202
+ }
203
+
204
+ /**
205
+ * Clone a layout tree (deep copy)
206
+ * @param {LayoutNode} root - Root node
207
+ * @returns {LayoutNode}
208
+ */
209
+ export function clone(root) {
210
+ return deserialize(serialize(root));
211
+ }
212
+
213
+ /**
214
+ * Get all panel nodes in tree
215
+ * @param {LayoutNode} root - Root node
216
+ * @returns {PanelNode[]}
217
+ */
218
+ export function getAllPanels(root) {
219
+ if (root.type === 'panel') return [root];
220
+ return [...getAllPanels(root.first), ...getAllPanels(root.second)];
221
+ }
222
+
223
+ /**
224
+ * Update a node's properties by ID
225
+ * @param {LayoutNode} root - Root node
226
+ * @param {string} nodeId - Node ID to update
227
+ * @param {Object} updates - Properties to update
228
+ * @returns {boolean} - True if node was found and updated
229
+ */
230
+ export function updateNode(root, nodeId, updates) {
231
+ const node = findNode(root, nodeId);
232
+ if (!node) return false;
233
+ Object.assign(node, updates);
234
+ return true;
235
+ }
236
+
237
+ /**
238
+ * Get neighbor panel IDs for a panel
239
+ * @param {LayoutNode} root - Root node
240
+ * @param {string} panelId - Panel ID
241
+ * @returns {{ left?: string, right?: string, top?: string, bottom?: string }}
242
+ */
243
+ export function getNeighbors(root, panelId) {
244
+ // TODO: Implement neighbor detection for join preview
245
+ return {};
246
+ }
@@ -0,0 +1,43 @@
1
+ import { css } from '@symbiotejs/symbiote';
2
+
3
+ export const styles = css`
4
+ panel-menu {
5
+ position: fixed;
6
+ z-index: 1000;
7
+ pointer-events: none;
8
+
9
+ .menu-container {
10
+ pointer-events: auto;
11
+ background: var(--bg-popup, #2a2a2a);
12
+ border: 1px solid var(--border-popup, #444);
13
+ border-radius: 6px;
14
+ box-shadow: 0 4px 12px var(--sn-shadow-color, rgba(0, 0, 0, 0.4));
15
+ min-width: 160px;
16
+ padding: 4px 0;
17
+ }
18
+
19
+ .menu-item {
20
+ display: flex;
21
+ align-items: center;
22
+ gap: 8px;
23
+ padding: 8px 12px;
24
+ cursor: pointer;
25
+ color: var(--text-main, #e0e0e0);
26
+ font-size: 0.85rem;
27
+ transition: background 0.1s;
28
+
29
+ &:hover {
30
+ background: var(--bg-hover, #3a3a3a);
31
+ }
32
+
33
+ &[active] {
34
+ color: var(--accent, #4a9eff);
35
+ }
36
+
37
+ .material-symbols-outlined {
38
+ font-size: 18px;
39
+ opacity: 0.8;
40
+ }
41
+ }
42
+ }
43
+ `;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @fileoverview PanelMenu - Dropdown menu for panel type selection
3
+ */
4
+
5
+ import Symbiote from '@symbiotejs/symbiote';
6
+ import { template } from './PanelMenu.tpl.js';
7
+ import { styles } from './PanelMenu.css.js';
8
+
9
+ export class PanelMenu extends Symbiote {
10
+ static isoMode = true;
11
+
12
+ init$ = {
13
+ visible: false,
14
+ panelId: '',
15
+ items: [],
16
+ currentType: '',
17
+
18
+ onItemClick: (e) => {
19
+ const type = e.target.closest('[data-type]')?.dataset.type;
20
+ if (type) {
21
+ this.dispatchEvent(new CustomEvent('panel-type-select', {
22
+ bubbles: true,
23
+ composed: true,
24
+ detail: { panelId: this.$.panelId, type }
25
+ }));
26
+ this.hide();
27
+ }
28
+ },
29
+ };
30
+
31
+ /**
32
+ * Show menu at position
33
+ * @param {number} x
34
+ * @param {number} y
35
+ * @param {string} panelId
36
+ * @param {string} currentType
37
+ * @param {Array<{type: string, title: string, icon: string}>} items
38
+ */
39
+ show(x, y, panelId, currentType, items) {
40
+ this.$.panelId = panelId;
41
+ this.$.currentType = currentType;
42
+
43
+ // Transform items to include isActive flag for template binding
44
+ this.$.items = items.map(item => ({
45
+ ...item,
46
+ icon: item.icon || 'dashboard',
47
+ title: item.title || item.type,
48
+ isActive: item.type === currentType
49
+ }));
50
+
51
+ this.style.left = `${x}px`;
52
+ this.style.top = `${y}px`;
53
+ this.$.visible = true;
54
+
55
+ // Close on outside click
56
+ if (typeof setTimeout !== 'undefined') {
57
+ setTimeout(() => {
58
+ if (typeof document !== 'undefined') {
59
+ document.addEventListener('pointerdown', this._onOutsideClick);
60
+ }
61
+ }, 0);
62
+ }
63
+ }
64
+
65
+ hide() {
66
+ this.$.visible = false;
67
+ if (typeof document !== 'undefined') {
68
+ document.removeEventListener('pointerdown', this._onOutsideClick);
69
+ }
70
+ }
71
+
72
+ _onOutsideClick = (e) => {
73
+ if (!this.contains(e.target)) {
74
+ this.hide();
75
+ }
76
+ };
77
+
78
+ disconnectedCallback() {
79
+ if (typeof document !== 'undefined') {
80
+ document.removeEventListener('pointerdown', this._onOutsideClick);
81
+ }
82
+ }
83
+ }
84
+
85
+ PanelMenu.template = template;
86
+ PanelMenu.rootStyles = styles;
87
+
88
+ PanelMenu.reg('panel-menu');
89
+
@@ -0,0 +1,14 @@
1
+ import { html } from '@symbiotejs/symbiote';
2
+
3
+ export const template = html`
4
+ <div class="menu-container" ${{ '@hidden': '!visible' }}>
5
+ <div class="menu-items" itemize="items">
6
+ <template>
7
+ <div class="menu-item" ${{ onclick: '^onItemClick', '@data-type': 'type', '@active': 'isActive' }}>
8
+ <span class="material-symbols-outlined">{{icon}}</span>
9
+ <span>{{title}}</span>
10
+ </div>
11
+ </template>
12
+ </div>
13
+ </div>
14
+ `;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @fileoverview Layout module exports
3
+ * Blender-style panel layout system for Symbiote.js
4
+ */
5
+
6
+ export { Layout } from './Layout/Layout.js';
7
+ export { LayoutNode } from './LayoutNode/LayoutNode.js';
8
+ export { ActionZone } from './ActionZone/ActionZone.js';
9
+ export { LayoutPreview } from './LayoutPreview/LayoutPreview.js';
10
+ export { LayoutSidebar } from './LayoutSidebar/LayoutSidebar.js';
11
+ export * as LayoutTree from './LayoutTree.js';
12
+ export {
13
+ navigate, updateParams, parseQuery, buildHash, buildQuery,
14
+ getRoute, setDefaultPanel,
15
+ } from './LayoutRouter/LayoutRouter.js';
16
+ export { syncWithRouter } from './LayoutRouter/routerSync.js';
@@ -0,0 +1,61 @@
1
+ /**
2
+ * ContextMenu styles
3
+ * @module symbiote-node/menu/ContextMenu.css
4
+ */
5
+ import { css } from '@symbiotejs/symbiote';
6
+
7
+ export const styles = css`
8
+ context-menu {
9
+ position: absolute;
10
+ inset: 0;
11
+ z-index: 200;
12
+ pointer-events: none;
13
+
14
+ &[hidden] {
15
+ display: none;
16
+ }
17
+
18
+ & .sn-ctx-backdrop {
19
+ position: absolute;
20
+ inset: 0;
21
+ pointer-events: all;
22
+ }
23
+
24
+ & .sn-ctx-menu {
25
+ position: absolute;
26
+ pointer-events: all;
27
+ min-width: 160px;
28
+ background: var(--sn-ctx-bg, #1e1e3a);
29
+ border: 1px solid var(--sn-ctx-border, #3a3a6a);
30
+ border-radius: 8px;
31
+ box-shadow: 0 8px 24px var(--sn-shadow-color, rgba(0, 0, 0, 0.5));
32
+ padding: 4px;
33
+ overflow: hidden;
34
+ }
35
+ }
36
+
37
+ .sn-ctx-btn {
38
+ display: flex;
39
+ align-items: center;
40
+ gap: 8px;
41
+ width: 100%;
42
+ padding: 8px 12px;
43
+ border: none;
44
+ background: transparent;
45
+ color: var(--sn-ctx-color, #e0e0e0);
46
+ font-family: var(--sn-font, 'Inter', sans-serif);
47
+ font-size: 13px;
48
+ cursor: pointer;
49
+ border-radius: 4px;
50
+ transition: background 0.1s;
51
+
52
+ &:hover {
53
+ background: var(--sn-ctx-hover, rgba(74, 158, 255, 0.15));
54
+ }
55
+ }
56
+
57
+ .sn-ctx-icon {
58
+ font-size: 18px;
59
+ opacity: 0.7;
60
+ }
61
+ `;