project-graph-mcp 2.2.6 → 2.3.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 (150) hide show
  1. package/ARCHITECTURE.md +81 -0
  2. package/CHANGELOG.md +57 -0
  3. package/README.md +9 -4
  4. package/package.json +6 -13
  5. package/src/compact/expand.js +1 -1
  6. package/src/core/graph-builder.js +2 -2
  7. package/src/core/parser.js +2 -2
  8. package/src/network/server.js +1 -2
  9. package/vendor/symbiote-node/CHANGELOG.md +31 -0
  10. package/vendor/symbiote-node/LICENSE +21 -0
  11. package/vendor/symbiote-node/README.md +206 -0
  12. package/vendor/symbiote-node/canvas/AutoLayout.js +725 -0
  13. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.css.js +73 -0
  14. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.js +93 -0
  15. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.tpl.js +9 -0
  16. package/vendor/symbiote-node/canvas/CanvasConnectionRenderer.js +962 -0
  17. package/vendor/symbiote-node/canvas/ConnectionRenderer.js +1468 -0
  18. package/vendor/symbiote-node/canvas/FlowSimulator.js +323 -0
  19. package/vendor/symbiote-node/canvas/ForceLayout.js +189 -0
  20. package/vendor/symbiote-node/canvas/ForceWorker.js +1325 -0
  21. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.css.js +97 -0
  22. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.js +176 -0
  23. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.tpl.js +12 -0
  24. package/vendor/symbiote-node/canvas/LODManager.js +88 -0
  25. package/vendor/symbiote-node/canvas/Minimap/Minimap.css.js +71 -0
  26. package/vendor/symbiote-node/canvas/Minimap/Minimap.js +207 -0
  27. package/vendor/symbiote-node/canvas/Minimap/Minimap.tpl.js +9 -0
  28. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.css.js +261 -0
  29. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.js +1840 -0
  30. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.tpl.js +22 -0
  31. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.css.js +97 -0
  32. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.js +132 -0
  33. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.tpl.js +21 -0
  34. package/vendor/symbiote-node/canvas/NodeViewManager.js +584 -0
  35. package/vendor/symbiote-node/canvas/PinExpansion.js +131 -0
  36. package/vendor/symbiote-node/canvas/PseudoConnection.js +80 -0
  37. package/vendor/symbiote-node/canvas/SubgraphManager.js +201 -0
  38. package/vendor/symbiote-node/canvas/SubgraphRouter.js +443 -0
  39. package/vendor/symbiote-node/canvas/ViewportActions.js +446 -0
  40. package/vendor/symbiote-node/core/Connection.js +45 -0
  41. package/vendor/symbiote-node/core/Editor.js +451 -0
  42. package/vendor/symbiote-node/core/Frame.js +31 -0
  43. package/vendor/symbiote-node/core/GraphMermaid.js +348 -0
  44. package/vendor/symbiote-node/core/GraphText.js +210 -0
  45. package/vendor/symbiote-node/core/Node.js +143 -0
  46. package/vendor/symbiote-node/core/Portal.js +104 -0
  47. package/vendor/symbiote-node/core/Socket.js +185 -0
  48. package/vendor/symbiote-node/core/SubgraphNode.js +125 -0
  49. package/vendor/symbiote-node/index.js +103 -0
  50. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.css.js +361 -0
  51. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.js +332 -0
  52. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.tpl.js +96 -0
  53. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.css.js +104 -0
  54. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.js +133 -0
  55. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.tpl.js +33 -0
  56. package/vendor/symbiote-node/interactions/ConnectFlow.js +307 -0
  57. package/vendor/symbiote-node/interactions/Drag.js +102 -0
  58. package/vendor/symbiote-node/interactions/Selector.js +132 -0
  59. package/vendor/symbiote-node/interactions/SnapGrid.js +65 -0
  60. package/vendor/symbiote-node/interactions/Zoom.js +140 -0
  61. package/vendor/symbiote-node/layout/ActionZone/ActionZone.css.js +88 -0
  62. package/vendor/symbiote-node/layout/ActionZone/ActionZone.js +254 -0
  63. package/vendor/symbiote-node/layout/ActionZone/ActionZone.tpl.js +11 -0
  64. package/vendor/symbiote-node/layout/Layout/Layout.css.js +88 -0
  65. package/vendor/symbiote-node/layout/Layout/Layout.js +622 -0
  66. package/vendor/symbiote-node/layout/Layout/Layout.tpl.js +25 -0
  67. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.css.js +293 -0
  68. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.js +467 -0
  69. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.tpl.js +33 -0
  70. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.css.js +46 -0
  71. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.js +102 -0
  72. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.tpl.js +6 -0
  73. package/vendor/symbiote-node/layout/LayoutRouter/LayoutRouter.js +156 -0
  74. package/vendor/symbiote-node/layout/LayoutRouter/routerSync.js +250 -0
  75. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.css.js +379 -0
  76. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.js +263 -0
  77. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.tpl.js +20 -0
  78. package/vendor/symbiote-node/layout/LayoutSidebar/SidebarSection.js +183 -0
  79. package/vendor/symbiote-node/layout/LayoutTree.js +246 -0
  80. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.css.js +43 -0
  81. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.js +89 -0
  82. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.tpl.js +14 -0
  83. package/vendor/symbiote-node/layout/index.js +16 -0
  84. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.css.js +61 -0
  85. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.js +79 -0
  86. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.tpl.js +19 -0
  87. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.css.js +41 -0
  88. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.js +24 -0
  89. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.tpl.js +16 -0
  90. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.css.js +65 -0
  91. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.js +29 -0
  92. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.tpl.js +13 -0
  93. package/vendor/symbiote-node/node/GraphNode/GraphNode.css.js +683 -0
  94. package/vendor/symbiote-node/node/GraphNode/GraphNode.js +92 -0
  95. package/vendor/symbiote-node/node/GraphNode/GraphNode.tpl.js +17 -0
  96. package/vendor/symbiote-node/node/NodeSocket/NodeSocket.js +25 -0
  97. package/vendor/symbiote-node/node/NodeSocket/NodeSocket.tpl.js +7 -0
  98. package/vendor/symbiote-node/node/PortItem/PortItem.css.js +90 -0
  99. package/vendor/symbiote-node/node/PortItem/PortItem.js +87 -0
  100. package/vendor/symbiote-node/node/PortItem/PortItem.tpl.js +10 -0
  101. package/vendor/symbiote-node/package.json +59 -0
  102. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.css.js +143 -0
  103. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.js +131 -0
  104. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.tpl.js +16 -0
  105. package/vendor/symbiote-node/plugins/History.js +384 -0
  106. package/vendor/symbiote-node/plugins/Readonly.js +59 -0
  107. package/vendor/symbiote-node/shapes/CircleShape.js +80 -0
  108. package/vendor/symbiote-node/shapes/CommentShape.js +35 -0
  109. package/vendor/symbiote-node/shapes/DiamondShape.js +115 -0
  110. package/vendor/symbiote-node/shapes/NodeShape.js +80 -0
  111. package/vendor/symbiote-node/shapes/PillShape.js +91 -0
  112. package/vendor/symbiote-node/shapes/RectShape.js +72 -0
  113. package/vendor/symbiote-node/shapes/SVGShape.js +494 -0
  114. package/vendor/symbiote-node/shapes/index.js +53 -0
  115. package/vendor/symbiote-node/themes/Palette.js +32 -0
  116. package/vendor/symbiote-node/themes/Skin.js +113 -0
  117. package/vendor/symbiote-node/themes/Theme.js +84 -0
  118. package/vendor/symbiote-node/themes/carbon.js +137 -0
  119. package/vendor/symbiote-node/themes/dark.js +137 -0
  120. package/vendor/symbiote-node/themes/ebook.js +138 -0
  121. package/vendor/symbiote-node/themes/grey.js +137 -0
  122. package/vendor/symbiote-node/themes/light.js +137 -0
  123. package/vendor/symbiote-node/themes/neon.js +138 -0
  124. package/vendor/symbiote-node/themes/pcb.js +273 -0
  125. package/vendor/symbiote-node/themes/synthwave.js +137 -0
  126. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.css.js +86 -0
  127. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.js +128 -0
  128. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.tpl.js +29 -0
  129. package/web/app.js +6 -5
  130. package/web/components/canvas-graph.js +1666 -0
  131. package/web/components/event-feed/CodeWidget.js +32 -0
  132. package/web/components/event-feed/EventWidget.js +97 -0
  133. package/web/components/event-feed/ListWidget.js +57 -0
  134. package/web/components/event-feed/MiniGraphWidget.js +69 -0
  135. package/web/dashboard.js +1 -1
  136. package/web/index.html +4 -0
  137. package/web/panels/ActionBoard/ActionBoard.js +1 -1
  138. package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -1
  139. package/web/panels/code-viewer.js +50 -15
  140. package/web/panels/dep-graph.js +2712 -7
  141. package/web/panels/file-tree.js +5 -2
  142. package/web/panels/live-monitor.js +75 -3
  143. package/web/style.css +33 -0
  144. package/docs/img/explorer-compact.jpg +0 -0
  145. package/docs/img/explorer-expanded.jpg +0 -0
  146. package/src/.contextignore +0 -22
  147. package/src/.project-graph-cache.json +0 -1
  148. package/src/compact/.project-graph-cache.json +0 -1
  149. package/web/.project-graph-cache.json +0 -1
  150. package/web/panels/SettingsPanel/.project-graph-cache.json +0 -1
@@ -0,0 +1,467 @@
1
+ /**
2
+ * @fileoverview LayoutNode - Universal recursive layout node
3
+ * Renders panel or split based on node type.
4
+ * Split nodes recursively create child LayoutNodes.
5
+ * Panels include action zones for split/join gestures.
6
+ */
7
+
8
+ import Symbiote from '@symbiotejs/symbiote';
9
+ import { template } from './LayoutNode.tpl.js';
10
+ import { styles } from './LayoutNode.css.js';
11
+ import './../ActionZone/ActionZone.js';
12
+
13
+ export class LayoutNode extends Symbiote {
14
+ static isoMode = true;
15
+
16
+ init$ = {
17
+ // Node data
18
+ nodeData: null,
19
+
20
+ // Computed values (updated in sub())
21
+ nodeType: 'panel',
22
+ isPanel: true,
23
+ isSplit: false,
24
+ direction: 'horizontal',
25
+ ratio: 0.5,
26
+ panelType: 'default',
27
+ nodeId: '',
28
+
29
+ // Panel display
30
+ panelTitle: 'Panel',
31
+ panelIcon: 'dashboard',
32
+
33
+ // Panel states
34
+ isCollapsed: false,
35
+ canCollapse: true, // Whether collapse is possible (has sibling panel)
36
+ collapseDirection: 'vertical', // 'vertical' or 'horizontal' - based on parent split
37
+ collapseIcon: 'expand_less',
38
+ savedRatio: 0.5, // Saved ratio before collapse for proper restore
39
+ isFullscreen: false,
40
+ fullscreenIcon: 'fullscreen',
41
+
42
+ // Split sizing
43
+ firstStyle: '',
44
+ secondStyle: '',
45
+
46
+ // Inherited from Layout
47
+ '^panelTypes': {},
48
+ '^fullscreenPanelId': null,
49
+
50
+ // Handlers
51
+ onResizerDown: (e) => this._startResize(e),
52
+ onTypeClick: (e) => this._showTypeMenu(e),
53
+ onCollapseClick: () => this._toggleCollapse(),
54
+ onExpandClick: () => this._toggleCollapse(), // Alias for collapsed state
55
+ onFullscreenClick: () => this._toggleFullscreen(),
56
+ };
57
+
58
+ renderCallback() {
59
+ // Subscribe to nodeData changes and update computed values
60
+ this.sub('nodeData', (data) => {
61
+ if (!data) return;
62
+
63
+ this.$.nodeType = data.type || 'panel';
64
+ this.$.isPanel = this.$.nodeType === 'panel';
65
+ this.$.isSplit = this.$.nodeType === 'split';
66
+ this.$.direction = data.direction || 'horizontal';
67
+ this.$.ratio = data.ratio || 0.5;
68
+ this.$.panelType = data.panelType || 'default';
69
+ this.$.nodeId = data.id || '';
70
+
71
+ // Read collapsed state from data (declarative)
72
+ if (data.type === 'panel') {
73
+ this.$.isCollapsed = data.collapsed || false;
74
+ if (this.$.isCollapsed) {
75
+ this.setAttribute('collapsed', '');
76
+ this.setAttribute('collapse-dir', this.$.collapseDirection);
77
+ } else {
78
+ this.removeAttribute('collapsed');
79
+ this.removeAttribute('collapse-dir');
80
+ }
81
+ // Update icon based on direction
82
+ if (this.$.isCollapsed) {
83
+ if (this.$.collapseDirection === 'horizontal') {
84
+ this.$.collapseIcon = 'chevron_right';
85
+ } else {
86
+ this.$.collapseIcon = 'expand_more';
87
+ }
88
+ } else {
89
+ if (this.$.collapseDirection === 'horizontal') {
90
+ this.$.collapseIcon = 'chevron_left';
91
+ } else {
92
+ this.$.collapseIcon = 'expand_less';
93
+ }
94
+ }
95
+ }
96
+
97
+ this._updateStyles();
98
+ this._updatePanelInfo();
99
+ this._renderNode(data);
100
+ });
101
+
102
+ // Subscribe to panelTypes changes to update icons when registered after render
103
+ this.sub('^panelTypes', () => {
104
+ this._updatePanelInfo();
105
+ });
106
+
107
+ // Subscribe to panelType changes to inject component when type changes via menu
108
+ this.sub('panelType', () => {
109
+ this._updatePanelInfo();
110
+ });
111
+
112
+ // Initial render if data already set
113
+ if (this.$.nodeData) {
114
+ this.sub('nodeData', (d) => { }); // Trigger subscription
115
+ }
116
+ }
117
+
118
+ _updateStyles() {
119
+ const ratio = this.$.ratio;
120
+ const dir = this.$.direction;
121
+ const data = this.$.nodeData;
122
+
123
+ // Check if children are collapsed (declarative from nodeData)
124
+ const firstCollapsed = data?.first?.collapsed || false;
125
+ const secondCollapsed = data?.second?.collapsed || false;
126
+
127
+ // Collapsed size constants
128
+ const COLLAPSED_SIZE = dir === 'horizontal' ? '32px' : '28px';
129
+
130
+ if (firstCollapsed) {
131
+ // First child collapsed - fixed size, second expands
132
+ if (dir === 'horizontal') {
133
+ this.$.firstStyle = `width: ${COLLAPSED_SIZE}; height: 100%; flex: 0 0 ${COLLAPSED_SIZE};`;
134
+ this.$.secondStyle = 'flex: 1; height: 100%;';
135
+ } else {
136
+ this.$.firstStyle = `height: ${COLLAPSED_SIZE}; width: 100%; flex: 0 0 ${COLLAPSED_SIZE};`;
137
+ this.$.secondStyle = 'flex: 1; width: 100%;';
138
+ }
139
+ } else if (secondCollapsed) {
140
+ // Second child collapsed - first expands, fixed size
141
+ if (dir === 'horizontal') {
142
+ this.$.firstStyle = 'flex: 1; height: 100%;';
143
+ this.$.secondStyle = `width: ${COLLAPSED_SIZE}; height: 100%; flex: 0 0 ${COLLAPSED_SIZE};`;
144
+ } else {
145
+ this.$.firstStyle = 'flex: 1; width: 100%;';
146
+ this.$.secondStyle = `height: ${COLLAPSED_SIZE}; width: 100%; flex: 0 0 ${COLLAPSED_SIZE};`;
147
+ }
148
+ } else {
149
+ // Normal ratio-based sizing
150
+ if (dir === 'horizontal') {
151
+ this.$.firstStyle = `width: ${ratio * 100}%; height: 100%;`;
152
+ this.$.secondStyle = `width: ${(1 - ratio) * 100}%; height: 100%;`;
153
+ } else {
154
+ this.$.firstStyle = `height: ${ratio * 100}%; width: 100%;`;
155
+ this.$.secondStyle = `height: ${(1 - ratio) * 100}%; width: 100%;`;
156
+ }
157
+ }
158
+ }
159
+
160
+ _updatePanelInfo() {
161
+ const panelTypes = this.$['^panelTypes'] || {};
162
+ const config = panelTypes[this.$.panelType] || {};
163
+ this.$.panelTitle = config.title || this.$.panelType;
164
+ this.$.panelIcon = config.icon || 'dashboard';
165
+
166
+ // Inject component if specified and not already created
167
+ this._injectPanelComponent(config);
168
+
169
+ // Check if panel can collapse (must be child of a split)
170
+ const container = this.parentElement;
171
+ if (!container) return;
172
+ const isSplitChild = container && (container.classList.contains('split-first') || container.classList.contains('split-second'));
173
+
174
+ // Additional safety check: Ensure sibling exists and is not collapsed
175
+ let siblingExists = false;
176
+ let siblingCollapsed = false;
177
+ let isFirst = false;
178
+
179
+ if (isSplitChild) {
180
+ isFirst = container.classList.contains('split-first');
181
+ // Use :scope > to find direct child only, not nested ones
182
+ const siblingContainer = isFirst
183
+ ? container.parentElement.querySelector(':scope > .split-second')
184
+ : container.parentElement.querySelector(':scope > .split-first');
185
+ siblingExists = !!siblingContainer;
186
+
187
+ // Check if sibling panel is collapsed (direct child panel only)
188
+ if (siblingContainer) {
189
+ const siblingNode = siblingContainer.querySelector(':scope > layout-node');
190
+ // Only check collapsed state if sibling is a panel
191
+ if (siblingNode?.getAttribute('node-type') === 'panel') {
192
+ siblingCollapsed = siblingNode.$.isCollapsed || false;
193
+ }
194
+ }
195
+ }
196
+
197
+ // If we are a panel, update canCollapse based on position
198
+ if (this.$.nodeType === 'panel') {
199
+ // Can't collapse if no sibling OR if sibling is already collapsed (would leave empty space)
200
+ this.$.canCollapse = !!isSplitChild && siblingExists && !siblingCollapsed;
201
+
202
+ if (isSplitChild) {
203
+ // Update direction based on parent split
204
+ let parentNode = container.closest('layout-node');
205
+ if (!parentNode && container.getRootNode() instanceof ShadowRoot) {
206
+ parentNode = container.getRootNode().host;
207
+ }
208
+
209
+ if (parentNode) {
210
+ const parentDir = parentNode.getAttribute('direction');
211
+ this.$.collapseDirection = parentDir;
212
+
213
+ // Arrow shows direction panel will collapse TO:
214
+ // First panel collapses left/up, second panel collapses right/down
215
+ if (!this.$.isCollapsed) {
216
+ if (parentDir === 'horizontal') {
217
+ this.$.collapseIcon = isFirst ? 'chevron_left' : 'chevron_right';
218
+ } else {
219
+ this.$.collapseIcon = isFirst ? 'expand_less' : 'expand_more';
220
+ }
221
+ }
222
+ }
223
+ }
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Inject custom component into panel content.
229
+ * Hides existing components instead of destroying them to preserve state.
230
+ * Uses style.display instead of hidden attribute because components may have
231
+ * CSS rules (e.g. display:block) that override the hidden attribute.
232
+ * @param {Object} config - Panel type configuration
233
+ */
234
+ _injectPanelComponent(config) {
235
+ const contentEl = this.ref.panelContent;
236
+ if (!contentEl) return;
237
+
238
+ const componentTag = config.component;
239
+ if (!componentTag) return;
240
+
241
+ // Hide all existing panel components via inline style (overrides CSS)
242
+ for (const child of contentEl.children) {
243
+ child.style.display = 'none';
244
+ }
245
+
246
+ // Check if target component already exists — show it
247
+ const existing = contentEl.querySelector(componentTag);
248
+ if (existing) {
249
+ existing.style.display = '';
250
+ return;
251
+ }
252
+
253
+ // Create new component
254
+ const component = document.createElement(componentTag);
255
+ component.setAttribute('data-panel-id', this.$.nodeData?.id || '');
256
+ contentEl.appendChild(component);
257
+ }
258
+
259
+ _renderNode(data) {
260
+ // Update attributes for CSS selectors
261
+ const prevType = this.getAttribute('node-type');
262
+ this.setAttribute('node-type', data.type);
263
+
264
+ if (data.type === 'split') {
265
+ this.setAttribute('direction', data.direction);
266
+ this._renderSplit(data);
267
+ } else {
268
+ this.removeAttribute('direction');
269
+
270
+ // CRITICAL: Clean up child nodes if we changed from split to panel
271
+ // This prevents orphan layout-node elements staying in DOM
272
+ if (prevType === 'split') {
273
+ if (this.ref.first) this.ref.first.innerHTML = '';
274
+ if (this.ref.second) this.ref.second.innerHTML = '';
275
+ }
276
+
277
+ // For panels, setup action zones
278
+ this._setupActionZones(data.id);
279
+ // Ensure collapse status is updated
280
+ this._updatePanelInfo();
281
+ }
282
+ }
283
+
284
+ _renderSplit(data) {
285
+ // Create child nodes for first and second
286
+ // Pass the current split direction so panels know which way to collapse
287
+ if (data.first && this.ref.first) {
288
+ this._ensureChildNode(this.ref.first, data.first);
289
+ }
290
+ if (data.second && this.ref.second) {
291
+ this._ensureChildNode(this.ref.second, data.second);
292
+ }
293
+ }
294
+
295
+ /**
296
+ * @param {HTMLElement} container
297
+ * @param {Object} nodeData
298
+ */
299
+ _ensureChildNode(container, nodeData) {
300
+ let child = container.querySelector('layout-node');
301
+ if (!child) {
302
+ child = document.createElement('layout-node');
303
+ container.appendChild(child);
304
+ // Wait for child to initialize then update info
305
+ if (typeof setTimeout !== 'undefined') {
306
+ setTimeout(() => child._updatePanelInfo && child._updatePanelInfo());
307
+ }
308
+ }
309
+ // Use shallow copy to ensure subscription triggers even if only nested properties changed
310
+ child.$.nodeData = { ...nodeData };
311
+ }
312
+
313
+ _setupActionZones(panelId) {
314
+ // Action zones are in the template, just set their panel ID
315
+ const zones = this.querySelectorAll('action-zone');
316
+ zones.forEach((zone) => {
317
+ zone.$.panelId = panelId;
318
+ });
319
+ }
320
+
321
+ _startResize(e) {
322
+ e.preventDefault();
323
+ const startPos = this.$.direction === 'horizontal' ? e.clientX : e.clientY;
324
+ const startRatio = this.$.ratio;
325
+
326
+ this.setAttribute('resizing', '');
327
+
328
+ // Collapse thresholds
329
+ const COLLAPSE_THRESHOLD = 0.05;
330
+ const UNCOLLAPSE_THRESHOLD = 0.08;
331
+
332
+ const onMove = (moveEvent) => {
333
+ const rect = this.getBoundingClientRect();
334
+ const currentPos = this.$.direction === 'horizontal' ? moveEvent.clientX : moveEvent.clientY;
335
+ const containerSize = this.$.direction === 'horizontal' ? rect.width : rect.height;
336
+ const startOffset = this.$.direction === 'horizontal' ? rect.left : rect.top;
337
+
338
+ // Calculate new ratio based on mouse position relative to container
339
+ let rawRatio = (currentPos - startOffset) / containerSize;
340
+
341
+ // Get first and second child nodes
342
+ const firstChild = this.ref.first?.querySelector('layout-node');
343
+ const secondChild = this.ref.second?.querySelector('layout-node');
344
+
345
+ // Check for collapse/uncollapse of first panel
346
+ if (rawRatio < COLLAPSE_THRESHOLD && firstChild && !firstChild.$.isCollapsed) {
347
+ // Collapse first panel
348
+ firstChild._setCollapsed(true);
349
+ return; // Don't update styles further when collapsed
350
+ } else if (rawRatio > UNCOLLAPSE_THRESHOLD && firstChild?.$.isCollapsed) {
351
+ // Uncollapse first panel
352
+ firstChild._setCollapsed(false);
353
+ }
354
+
355
+ // Check for collapse/uncollapse of second panel
356
+ if (rawRatio > (1 - COLLAPSE_THRESHOLD) && secondChild && !secondChild.$.isCollapsed) {
357
+ // Collapse second panel
358
+ secondChild._setCollapsed(true);
359
+ return; // Don't update styles further when collapsed
360
+ } else if (rawRatio < (1 - UNCOLLAPSE_THRESHOLD) && secondChild?.$.isCollapsed) {
361
+ // Uncollapse second panel
362
+ secondChild._setCollapsed(false);
363
+ }
364
+
365
+ // Skip style updates if any panel is still collapsed
366
+ if (firstChild?.$.isCollapsed || secondChild?.$.isCollapsed) {
367
+ return;
368
+ }
369
+
370
+ // Clamp ratio
371
+ let newRatio = Math.max(0.1, Math.min(0.9, rawRatio));
372
+
373
+ // Update ratio and styles
374
+ this.$.ratio = newRatio;
375
+ this._updateStyles();
376
+
377
+ // Update nodeData for persistence
378
+ if (this.$.nodeData) {
379
+ this.$.nodeData.ratio = newRatio;
380
+ }
381
+
382
+ // Notify parent
383
+ this._notifyChange();
384
+ };
385
+
386
+ const onUp = () => {
387
+ this.removeAttribute('resizing');
388
+ document.removeEventListener('pointermove', onMove);
389
+ document.removeEventListener('pointerup', onUp);
390
+ };
391
+
392
+ document.addEventListener('pointermove', onMove);
393
+ document.addEventListener('pointerup', onUp);
394
+ }
395
+
396
+ _notifyChange() {
397
+ this.dispatchEvent(new CustomEvent('layout-change', {
398
+ bubbles: true,
399
+ detail: { nodeId: this.$.nodeId }
400
+ }));
401
+ }
402
+
403
+ _toggleCollapse() {
404
+ // Dispatch event to Layout - it will update the tree data
405
+ // which triggers a re-render with declarative collapsed handling
406
+ this.dispatchEvent(new CustomEvent('panel-collapse-toggle', {
407
+ bubbles: true,
408
+ composed: true,
409
+ detail: {
410
+ panelId: this.$.nodeId,
411
+ collapsed: !this.$.isCollapsed
412
+ }
413
+ }));
414
+ }
415
+
416
+ /**
417
+ * Programmatically set collapsed state (used by resize gesture)
418
+ * @param {boolean} collapsed
419
+ */
420
+ _setCollapsed(collapsed) {
421
+ if (this.$.isCollapsed === collapsed) return;
422
+
423
+ // Dispatch event to Layout - it will update the tree data
424
+ this.dispatchEvent(new CustomEvent('panel-collapse-toggle', {
425
+ bubbles: true,
426
+ composed: true,
427
+ detail: {
428
+ panelId: this.$.nodeId,
429
+ collapsed: collapsed
430
+ }
431
+ }));
432
+ }
433
+
434
+ _toggleFullscreen() {
435
+ // Don't allow fullscreen when collapsed
436
+ if (this.$.isCollapsed) return;
437
+
438
+ this.dispatchEvent(new CustomEvent('panel-fullscreen', {
439
+ bubbles: true,
440
+ composed: true,
441
+ detail: { panelId: this.$.nodeId }
442
+ }));
443
+ }
444
+
445
+ _showTypeMenu(e) {
446
+ // Don't show type menu when collapsed
447
+ if (this.$.isCollapsed) return;
448
+
449
+ const rect = e.target.getBoundingClientRect();
450
+ this.dispatchEvent(new CustomEvent('panel-type-menu', {
451
+ bubbles: true,
452
+ composed: true,
453
+ detail: {
454
+ panelId: this.$.nodeId,
455
+ currentType: this.$.panelType,
456
+ x: rect.left,
457
+ y: rect.bottom + 4
458
+ }
459
+ }));
460
+ }
461
+ }
462
+
463
+ LayoutNode.template = template;
464
+ LayoutNode.rootStyles = styles;
465
+
466
+ LayoutNode.reg('layout-node');
467
+
@@ -0,0 +1,33 @@
1
+ import { html } from '@symbiotejs/symbiote';
2
+
3
+ export const template = html`
4
+ <div class="panel-view" ${{ '@hidden': '!isPanel' }}>
5
+ <div class="panel-header">
6
+ <button class="header-btn type-btn" ${{ onclick: 'onTypeClick' }}>
7
+ <span class="material-symbols-outlined panel-icon" ${{ textContent: 'panelIcon' }}></span>
8
+ <span class="panel-title" ${{ textContent: 'panelTitle' }}></span>
9
+ <span class="material-symbols-outlined dropdown-arrow">arrow_drop_down</span>
10
+ </button>
11
+ <div class="header-spacer"></div>
12
+ <button class="header-btn collapse-btn" ${{ onclick: 'onCollapseClick', '@hidden': '!canCollapse' }} title="Collapse">
13
+ <span class="material-symbols-outlined" ${{ textContent: 'collapseIcon' }}></span>
14
+ </button>
15
+ <button class="header-btn fullscreen-btn" ${{ onclick: 'onFullscreenClick' }} title="Fullscreen">
16
+ <span class="material-symbols-outlined" ${{ textContent: 'fullscreenIcon' }}></span>
17
+ </button>
18
+ </div>
19
+ <div class="panel-content" ref="panelContent" ${{ '@hidden': 'isCollapsed' }}></div>
20
+
21
+ <!-- Action zones for split/join -->
22
+ <action-zone corner="tl"></action-zone>
23
+ <action-zone corner="tr"></action-zone>
24
+ <action-zone corner="bl"></action-zone>
25
+ <action-zone corner="br"></action-zone>
26
+ </div>
27
+
28
+ <div class="split-view" ${{ '@hidden': '!isSplit', '@direction': 'direction' }}>
29
+ <div class="split-first" ref="first" ${{ '@style': 'firstStyle' }}></div>
30
+ <div class="split-resizer" ${{ onpointerdown: 'onResizerDown' }}></div>
31
+ <div class="split-second" ref="second" ${{ '@style': 'secondStyle' }}></div>
32
+ </div>
33
+ `;
@@ -0,0 +1,46 @@
1
+ import { css } from '@symbiotejs/symbiote';
2
+
3
+ export const styles = css`
4
+ layout-preview {
5
+ position: fixed;
6
+ top: 0;
7
+ left: 0;
8
+ width: 100%;
9
+ height: 100%;
10
+ pointer-events: none;
11
+ z-index: 9999;
12
+
13
+ &[hidden] {
14
+ display: none;
15
+ }
16
+
17
+ .preview-overlay {
18
+ position: absolute;
19
+ background: color-mix(in srgb, var(--sn-danger-color, #ef4444) 30%, transparent);
20
+ border: 2px solid color-mix(in srgb, var(--sn-danger-color, #ef4444) 60%, transparent);
21
+ display: none;
22
+ }
23
+
24
+ &[type="join"] .preview-overlay {
25
+ display: block;
26
+ }
27
+
28
+ .preview-line {
29
+ position: absolute;
30
+ background: var(--layout-highlight, #888);
31
+ box-shadow: 0 0 8px var(--layout-highlight, #888);
32
+ display: none;
33
+ }
34
+
35
+ &[type="split-h"] .preview-line,
36
+ &[type="split-v"] .preview-line {
37
+ display: block;
38
+ }
39
+
40
+ /* Hidden attribute overrides */
41
+ .preview-overlay[hidden],
42
+ .preview-line[hidden] {
43
+ display: none !important;
44
+ }
45
+ }
46
+ `;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * @fileoverview LayoutPreview - Visual preview for split/join operations
3
+ * Shows where the new panel will be created or which panel will be removed.
4
+ */
5
+
6
+ import Symbiote from '@symbiotejs/symbiote';
7
+ import { template } from './LayoutPreview.tpl.js';
8
+ import { styles } from './LayoutPreview.css.js';
9
+
10
+ export class LayoutPreview extends Symbiote {
11
+ static isoMode = true;
12
+
13
+ init$ = {
14
+ // Preview type: 'split-h' | 'split-v' | 'join' | null
15
+ previewType: null,
16
+
17
+ // Target panel rect for positioning
18
+ targetRect: null,
19
+
20
+ // Preview visibility
21
+ visible: false,
22
+
23
+ // Computed styles
24
+ overlayStyle: '',
25
+ lineStyle: '',
26
+ };
27
+
28
+ renderCallback() {
29
+ this.sub('previewType', (type) => {
30
+ if (type) {
31
+ this.setAttribute('type', type);
32
+ } else {
33
+ this.removeAttribute('type');
34
+ }
35
+ });
36
+ }
37
+
38
+ /**
39
+ * Show split preview
40
+ * @param {'split-h' | 'split-v'} direction
41
+ * @param {DOMRect} panelRect
42
+ * @param {number} [ratio=0.5]
43
+ */
44
+ showSplit(direction, panelRect, ratio = 0.5) {
45
+ this.$.previewType = direction;
46
+ this.$.visible = true;
47
+
48
+ if (direction === 'split-h') {
49
+ // Horizontal split - vertical line
50
+ const x = panelRect.left + panelRect.width * ratio;
51
+ this.$.lineStyle = `
52
+ left: ${x}px;
53
+ top: ${panelRect.top}px;
54
+ width: 4px;
55
+ height: ${panelRect.height}px;
56
+ `;
57
+ this.$.overlayStyle = '';
58
+ } else {
59
+ // Vertical split - horizontal line
60
+ const y = panelRect.top + panelRect.height * ratio;
61
+ this.$.lineStyle = `
62
+ left: ${panelRect.left}px;
63
+ top: ${y}px;
64
+ width: ${panelRect.width}px;
65
+ height: 4px;
66
+ `;
67
+ this.$.overlayStyle = '';
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Show join preview (overlay on target panel)
73
+ * @param {DOMRect} targetRect - Panel that will be removed
74
+ */
75
+ showJoin(targetRect) {
76
+ this.$.previewType = 'join';
77
+ this.$.visible = true;
78
+
79
+ this.$.overlayStyle = `
80
+ left: ${targetRect.left}px;
81
+ top: ${targetRect.top}px;
82
+ width: ${targetRect.width}px;
83
+ height: ${targetRect.height}px;
84
+ `;
85
+ this.$.lineStyle = '';
86
+ }
87
+
88
+ /**
89
+ * Hide preview
90
+ */
91
+ hide() {
92
+ this.$.visible = false;
93
+ this.$.previewType = null;
94
+ this.$.overlayStyle = '';
95
+ this.$.lineStyle = '';
96
+ }
97
+ }
98
+
99
+ LayoutPreview.template = template;
100
+ LayoutPreview.rootStyles = styles;
101
+
102
+ LayoutPreview.reg('layout-preview');
@@ -0,0 +1,6 @@
1
+ import { html } from '@symbiotejs/symbiote';
2
+
3
+ export const template = html`
4
+ <div class="preview-overlay" ${{ '@style': 'overlayStyle', '@hidden': '!visible' }}></div>
5
+ <div class="preview-line" ${{ '@style': 'lineStyle', '@hidden': '!visible' }}></div>
6
+ `;