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,622 @@
1
+ /**
2
+ * @fileoverview Layout - Root container for Blender-style panel layout
3
+ * Uses LayoutNode for recursive BSP tree rendering.
4
+ * Handles action zone events for split/join operations.
5
+ */
6
+
7
+ import Symbiote from '@symbiotejs/symbiote';
8
+ import * as LayoutTree from './../LayoutTree.js';
9
+ import { template } from './Layout.tpl.js';
10
+ import { styles } from './Layout.css.js';
11
+ import './../LayoutNode/LayoutNode.js';
12
+ import './../LayoutPreview/LayoutPreview.js';
13
+ import './../PanelMenu/PanelMenu.js';
14
+
15
+ export class Layout extends Symbiote {
16
+ static isoMode = true;
17
+
18
+ init$ = {
19
+ // Attributes
20
+ '@storage-key': '',
21
+ '@min-panel-size': 50,
22
+
23
+ // Layout tree data
24
+ layoutTree: null,
25
+
26
+ // Panel type registry
27
+ panelTypes: {},
28
+
29
+ // Current gesture state
30
+ activeGesture: null,
31
+
32
+ // Fullscreen panel ID (null = no fullscreen)
33
+ fullscreenPanelId: null,
34
+
35
+ // Tab bar state for Itemize API
36
+ hasFullscreenTabs: false,
37
+ tabItems: [],
38
+
39
+ // Tab click handler for Itemize
40
+ onTabClick: (e) => {
41
+ const panelId = e.target.closest('[data-panel-id]')?.dataset.panelId;
42
+ if (panelId && panelId !== this.$.fullscreenPanelId) {
43
+ this._switchFullscreenPanel(panelId);
44
+ }
45
+ },
46
+
47
+ // Methods for LayoutNode to inherit
48
+ onLayoutChange: () => this._saveLayout(),
49
+ };
50
+
51
+ /**
52
+ * Register panel type
53
+ * @param {string} name - Panel type name
54
+ * @param {Object} config - Panel configuration
55
+ * @param {string} [config.title] - Default title
56
+ * @param {string} [config.icon] - Material Symbols icon name
57
+ * @param {string} [config.component] - Custom element tag name
58
+ */
59
+ registerPanelType(name, config) {
60
+ this.$.panelTypes = {
61
+ ...this.$.panelTypes,
62
+ [name]: config
63
+ };
64
+ }
65
+
66
+ initCallback() {
67
+ this._loadLayout();
68
+
69
+ // Listen for layout changes from children
70
+ this.addEventListener('layout-change', () => this._saveLayout());
71
+
72
+ // Listen for action zone events
73
+ this.addEventListener('action-zone-start', (e) => this._onActionZoneStart(e));
74
+ this.addEventListener('action-zone-gesture', (e) => this._onActionZoneGesture(e));
75
+ this.addEventListener('action-zone-execute', (e) => this._onActionZoneExecute(e));
76
+ this.addEventListener('action-zone-end', (e) => this._onActionZoneEnd(e));
77
+
78
+ // Listen for panel UX events
79
+ this.addEventListener('panel-type-menu', (e) => this._onPanelTypeMenu(e));
80
+ this.addEventListener('panel-type-select', (e) => this._onPanelTypeSelect(e));
81
+ this.addEventListener('panel-fullscreen', (e) => this._onPanelFullscreen(e));
82
+ this.addEventListener('panel-collapse-toggle', (e) => this._onPanelCollapseToggle(e));
83
+
84
+ // Global fallback: hide preview when pointer is released anywhere
85
+ // This covers touchpad edge cases when pointer events don't bubble correctly
86
+ this._globalPointerFallback = () => {
87
+ if (this.$.activeGesture) {
88
+ this.$.activeGesture = null;
89
+ if (this.ref.preview) {
90
+ this.ref.preview.hide();
91
+ }
92
+ }
93
+ };
94
+ if (typeof document !== 'undefined') {
95
+ document.addEventListener('pointerup', this._globalPointerFallback);
96
+ document.addEventListener('pointercancel', this._globalPointerFallback);
97
+ }
98
+ }
99
+
100
+ disconnectedCallback() {
101
+ if (this._globalPointerFallback && typeof document !== 'undefined') {
102
+ document.removeEventListener('pointerup', this._globalPointerFallback);
103
+ document.removeEventListener('pointercancel', this._globalPointerFallback);
104
+ }
105
+ }
106
+
107
+ renderCallback() {
108
+ this._renderRoot();
109
+ this.sub('layoutTree', () => {
110
+ this._renderRoot();
111
+ // Recalculate tabs if in fullscreen mode
112
+ if (this.$.fullscreenPanelId) {
113
+ // Wait for DOM update, then recalculate tabs
114
+ if (typeof requestAnimationFrame !== 'undefined') {
115
+ requestAnimationFrame(() => {
116
+ const allPanels = this.querySelectorAll('layout-node[node-type="panel"]');
117
+ // Check if current fullscreen panel still exists
118
+ const panelExists = Array.from(allPanels).some(p => p.$.nodeId === this.$.fullscreenPanelId);
119
+ if (panelExists) {
120
+ this._updateTabItems(allPanels, this.$.fullscreenPanelId);
121
+ } else {
122
+ // Fullscreen panel was removed, exit fullscreen
123
+ this.$.fullscreenPanelId = null;
124
+ this.$.hasFullscreenTabs = false;
125
+ this.$.tabItems = [];
126
+ allPanels.forEach(p => {
127
+ p.removeAttribute('fullscreen');
128
+ p.$.isFullscreen = false;
129
+ p.style.display = '';
130
+ });
131
+ }
132
+ });
133
+ }
134
+ }
135
+ });
136
+ }
137
+
138
+ _loadLayout() {
139
+ const storageKey = this.$['@storage-key'];
140
+
141
+ // Try localStorage
142
+ if (storageKey && typeof localStorage !== 'undefined') {
143
+ const stored = localStorage.getItem(storageKey);
144
+ if (stored) {
145
+ try {
146
+ this.$.layoutTree = LayoutTree.deserialize(stored);
147
+ return;
148
+ } catch (e) {
149
+ console.warn('Failed to load layout:', e);
150
+ }
151
+ }
152
+ }
153
+
154
+ // Try layout attribute
155
+ const layoutAttr = this.getAttribute('layout');
156
+ if (layoutAttr) {
157
+ try {
158
+ this.$.layoutTree = JSON.parse(layoutAttr);
159
+ return;
160
+ } catch (e) {
161
+ console.warn('Failed to parse layout:', e);
162
+ }
163
+ }
164
+
165
+ // Default single panel
166
+ this.$.layoutTree = LayoutTree.createPanel('default');
167
+ }
168
+
169
+ _saveLayout() {
170
+ const storageKey = this.$['@storage-key'];
171
+ if (storageKey && this.$.layoutTree && typeof localStorage !== 'undefined') {
172
+ localStorage.setItem(storageKey, LayoutTree.serialize(this.$.layoutTree));
173
+ }
174
+ }
175
+
176
+ _renderRoot() {
177
+ if (!this.$.layoutTree || !this.ref.root) return;
178
+
179
+ // Ensure root node exists
180
+ let rootNode = this.ref.root.querySelector('layout-node');
181
+ if (!rootNode) {
182
+ rootNode = document.createElement('layout-node');
183
+ this.ref.root.appendChild(rootNode);
184
+ }
185
+
186
+ // Pass data to root node
187
+ rootNode.$.nodeData = this.$.layoutTree;
188
+ }
189
+
190
+ // Action Zone Event Handlers
191
+
192
+ /**
193
+ * Called when action zone drag starts
194
+ * @param {CustomEvent} e
195
+ */
196
+ _onActionZoneStart(e) {
197
+ const { panelId, corner } = e.detail;
198
+ this.$.activeGesture = { panelId, corner };
199
+ }
200
+
201
+ /**
202
+ * Called during action zone drag with gesture type
203
+ * @param {CustomEvent} e
204
+ */
205
+ _onActionZoneGesture(e) {
206
+ const { panelId, gesture, dx, dy } = e.detail;
207
+
208
+ // Find the panel element
209
+ const panelNode = this._findPanelNode(panelId);
210
+ if (!panelNode) return;
211
+
212
+ const panelRect = panelNode.getBoundingClientRect();
213
+
214
+ // Show preview
215
+ const preview = this.ref.preview;
216
+ if (!preview) return;
217
+
218
+ if (gesture === 'split-h' || gesture === 'split-v') {
219
+ preview.showSplit(gesture, panelRect, 0.5);
220
+ } else if (gesture === 'join') {
221
+ // For join, find the neighbor panel that would be removed
222
+ const neighborInfo = this._findJoinTarget(panelId, dx, dy);
223
+ if (neighborInfo) {
224
+ const neighborNode = this._findPanelNode(neighborInfo.id);
225
+ if (neighborNode) {
226
+ preview.showJoin(neighborNode.getBoundingClientRect());
227
+ }
228
+ }
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Called when action zone gesture is completed
234
+ * @param {CustomEvent} e
235
+ */
236
+ _onActionZoneExecute(e) {
237
+ const { panelId, corner, gesture } = e.detail;
238
+
239
+ if (gesture === 'split-h') {
240
+ this.splitPanel(panelId, 'horizontal', 0.5);
241
+ } else if (gesture === 'split-v') {
242
+ this.splitPanel(panelId, 'vertical', 0.5);
243
+ } else if (gesture === 'join') {
244
+ // Join removes the current panel, expanding neighbor
245
+ this.joinPanels(panelId);
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Called when action zone drag ends
251
+ * @param {CustomEvent} e
252
+ */
253
+ _onActionZoneEnd(e) {
254
+ this.$.activeGesture = null;
255
+
256
+ // Hide preview
257
+ const preview = this.ref.preview;
258
+ if (preview) {
259
+ preview.hide();
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Show panel type selection menu
265
+ * @param {CustomEvent} e
266
+ */
267
+ _onPanelTypeMenu(e) {
268
+ const { panelId, currentType, x, y } = e.detail;
269
+ const menu = this.ref.menu;
270
+ if (!menu) return;
271
+
272
+ // Convert panelTypes to array for menu
273
+ const items = Object.entries(this.$.panelTypes).map(([type, config]) => ({
274
+ type,
275
+ title: config.title || type,
276
+ icon: config.icon || 'dashboard'
277
+ }));
278
+
279
+ menu.show(x, y, panelId, currentType, items);
280
+ }
281
+
282
+ /**
283
+ * Handle panel type change
284
+ * @param {CustomEvent} e
285
+ */
286
+ _onPanelTypeSelect(e) {
287
+ const { panelId, type } = e.detail;
288
+
289
+ // Update tree
290
+ const tree = this.$.layoutTree;
291
+ if (!tree) return;
292
+
293
+ const updateNode = (node) => {
294
+ if (!node) return;
295
+ if (node.id === panelId) {
296
+ node.panelType = type;
297
+ return;
298
+ }
299
+ if (node.first) updateNode(node.first);
300
+ if (node.second) updateNode(node.second);
301
+ };
302
+
303
+ updateNode(tree);
304
+ this.$.layoutTree = { ...tree };
305
+ this._saveLayout();
306
+ }
307
+
308
+ /**
309
+ * Toggle panel collapse state
310
+ * @param {CustomEvent} e
311
+ */
312
+ _onPanelCollapseToggle(e) {
313
+ const { panelId, collapsed } = e.detail;
314
+ const tree = this.$.layoutTree;
315
+ if (!tree) return;
316
+
317
+ // Update the node's collapsed state in tree
318
+ LayoutTree.updateNode(tree, panelId, { collapsed });
319
+
320
+ // Trigger full re-render to propagate changes to all split nodes
321
+ this.$.layoutTree = { ...tree };
322
+ this._renderRoot();
323
+ this._saveLayout();
324
+
325
+ // Update both panels' canCollapse state
326
+ // When one panel collapses/expands, both need to recalculate
327
+ if (typeof requestAnimationFrame !== 'undefined') {
328
+ requestAnimationFrame(() => {
329
+ const panelNode = this._findPanelNode(panelId);
330
+ if (panelNode) {
331
+ // Find parent split container
332
+ const container = panelNode.parentElement;
333
+ if (container?.classList.contains('split-first') || container?.classList.contains('split-second')) {
334
+ const siblingContainer = container.classList.contains('split-first')
335
+ ? container.parentElement?.querySelector('.split-second')
336
+ : container.parentElement?.querySelector('.split-first');
337
+
338
+ // Update sibling panel
339
+ if (siblingContainer) {
340
+ const siblingPanel = siblingContainer.querySelector('layout-node[node-type="panel"]');
341
+ if (siblingPanel?._updatePanelInfo) {
342
+ siblingPanel._updatePanelInfo();
343
+ }
344
+ }
345
+
346
+ // Also update the collapsed panel itself (for when it expands)
347
+ if (panelNode._updatePanelInfo) {
348
+ panelNode._updatePanelInfo();
349
+ }
350
+ }
351
+ }
352
+ });
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Toggle panel fullscreen
358
+ * @param {CustomEvent} e
359
+ */
360
+ _onPanelFullscreen(e) {
361
+ const { panelId } = e.detail;
362
+ const panelNode = this._findPanelNode(panelId);
363
+ if (!panelNode) return;
364
+
365
+ const allPanels = this.querySelectorAll('layout-node[node-type="panel"]');
366
+
367
+ if (this.$.fullscreenPanelId === panelId) {
368
+ // Exit fullscreen
369
+ this.$.fullscreenPanelId = null;
370
+ this.$.hasFullscreenTabs = false;
371
+ this.$.tabItems = [];
372
+
373
+ panelNode.removeAttribute('fullscreen');
374
+ panelNode.$.isFullscreen = false;
375
+ panelNode.$.fullscreenIcon = 'fullscreen';
376
+
377
+ // Remove fullscreen styles from all panels
378
+ allPanels.forEach((p) => {
379
+ p.removeAttribute('fullscreen');
380
+ p.$.isFullscreen = false;
381
+ p.$.fullscreenIcon = 'fullscreen';
382
+ p.style.display = '';
383
+ });
384
+
385
+ // Force layout recalculation
386
+ this._renderRoot();
387
+ this.dispatchEvent(new CustomEvent('layout-change', { bubbles: true }));
388
+ } else {
389
+ // Enter fullscreen
390
+ this.$.fullscreenPanelId = panelId;
391
+
392
+ // Hide all panels except fullscreen one
393
+ allPanels.forEach((p) => {
394
+ if (p === panelNode) {
395
+ p.setAttribute('fullscreen', '');
396
+ p.$.isFullscreen = true;
397
+ p.$.fullscreenIcon = 'fullscreen_exit';
398
+ p.style.display = '';
399
+ } else {
400
+ p.style.display = 'none';
401
+ }
402
+ });
403
+
404
+ // Update tab bar via Itemize API
405
+ this._updateTabItems(allPanels, panelId);
406
+ this.$.hasFullscreenTabs = true;
407
+ }
408
+ }
409
+
410
+ /**
411
+ * Update tabItems array for Itemize-based tab bar
412
+ * @param {NodeListOf<Element>} [allPanels] - Optional, will query DOM if not provided
413
+ * @param {string} [activePanelId] - Optional, defaults to fullscreenPanelId
414
+ */
415
+ _updateTabItems(allPanels, activePanelId) {
416
+ const panels = allPanels || this.querySelectorAll('layout-node[node-type="panel"]');
417
+ const activeId = activePanelId || this.$.fullscreenPanelId;
418
+
419
+ this.$.tabItems = Array.from(panels).map((p) => {
420
+ const nodeData = p.$.nodeData;
421
+ const panelType = nodeData?.panelType || 'panel';
422
+ const typeConfig = this.$.panelTypes[panelType] || {};
423
+
424
+ return {
425
+ panelId: p.$.nodeId,
426
+ icon: typeConfig.icon || 'dashboard',
427
+ title: typeConfig.title || panelType,
428
+ isActive: p.$.nodeId === activeId
429
+ };
430
+ });
431
+ }
432
+
433
+ /**
434
+ * Switch fullscreen to another panel
435
+ * @param {string} panelId - Panel ID to switch to
436
+ */
437
+ _switchFullscreenPanel(panelId) {
438
+ const allPanels = this.querySelectorAll('layout-node[node-type="panel"]');
439
+ const newPanel = this._findPanelNode(panelId);
440
+ if (!newPanel) return;
441
+
442
+ // Update panel states
443
+ allPanels.forEach((p) => {
444
+ if (p.$.nodeId === panelId) {
445
+ p.setAttribute('fullscreen', '');
446
+ p.$.isFullscreen = true;
447
+ p.$.fullscreenIcon = 'fullscreen_exit';
448
+ p.style.display = '';
449
+ } else {
450
+ p.removeAttribute('fullscreen');
451
+ p.$.isFullscreen = false;
452
+ p.$.fullscreenIcon = 'fullscreen';
453
+ p.style.display = 'none';
454
+ }
455
+ });
456
+
457
+ this.$.fullscreenPanelId = panelId;
458
+
459
+ // Update tab bar
460
+ this._updateTabItems(allPanels, panelId);
461
+ }
462
+
463
+ /**
464
+ * Find a panel node by ID
465
+ * @param {string} panelId
466
+ * @returns {HTMLElement|null}
467
+ */
468
+ _findPanelNode(panelId) {
469
+ const nodes = this.querySelectorAll('layout-node[node-type="panel"]');
470
+ for (const node of nodes) {
471
+ if (node.$.nodeId === panelId) {
472
+ return node;
473
+ }
474
+ }
475
+ return null;
476
+ }
477
+
478
+ /**
479
+ * Find the neighbor panel for join operation
480
+ * @param {string} panelId
481
+ * @param {number} dx
482
+ * @param {number} dy
483
+ * @returns {{id: string, direction: string}|null}
484
+ */
485
+ _findJoinTarget(panelId, dx, dy) {
486
+ // Find parent split of this panel
487
+ const parentInfo = LayoutTree.findParent(this.$.layoutTree, panelId);
488
+ if (!parentInfo) return null;
489
+
490
+ const { parent, which } = parentInfo;
491
+
492
+ // The sibling is the join target (the panel that will expand)
493
+ const sibling = which === 'first' ? parent.second : parent.first;
494
+ if (!sibling) return null;
495
+
496
+ // For nested splits, get the leaf panel ID
497
+ const siblingId = this._getFirstPanelId(sibling);
498
+
499
+ return { id: siblingId, direction: parent.direction };
500
+ }
501
+
502
+ /**
503
+ * Get the first panel ID from a node (handles nested splits)
504
+ * @param {Object} node
505
+ * @returns {string}
506
+ */
507
+ _getFirstPanelId(node) {
508
+ if (node.type === 'panel') return node.id;
509
+ // For split nodes, recursively get first panel
510
+ return this._getFirstPanelId(node.first);
511
+ }
512
+
513
+
514
+ // Public API
515
+
516
+ /**
517
+ * Split a panel
518
+ * @param {string} panelId - Panel ID to split
519
+ * @param {'horizontal' | 'vertical'} direction - Split direction
520
+ * @param {number} [ratio=0.5] - Split ratio
521
+ * @param {string} [newPanelType] - Type for new panel
522
+ */
523
+ splitPanel(panelId, direction, ratio = 0.5, newPanelType) {
524
+ const newTree = LayoutTree.splitPanel(
525
+ LayoutTree.clone(this.$.layoutTree),
526
+ panelId,
527
+ direction,
528
+ ratio,
529
+ newPanelType
530
+ );
531
+
532
+ if (newTree) {
533
+ this.$.layoutTree = newTree;
534
+ this._saveLayout();
535
+ }
536
+ }
537
+
538
+ /**
539
+ * Join panels (remove one)
540
+ * @param {string} panelToRemove - Panel ID to remove
541
+ */
542
+ joinPanels(panelToRemove) {
543
+ const newTree = LayoutTree.joinPanels(
544
+ LayoutTree.clone(this.$.layoutTree),
545
+ panelToRemove
546
+ );
547
+
548
+ if (newTree) {
549
+ this.$.layoutTree = newTree;
550
+ this._saveLayout();
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Get current layout
556
+ * @returns {import('./../LayoutTree.js').LayoutNode}
557
+ */
558
+ getLayout() {
559
+ return LayoutTree.clone(this.$.layoutTree);
560
+ }
561
+
562
+ /**
563
+ * Set layout
564
+ * @param {import('./../LayoutTree.js').LayoutNode} layout
565
+ */
566
+ setLayout(layout) {
567
+ // Clear fullscreen state
568
+ if (this.$.fullscreenPanelId) {
569
+ const panelNode = this._findPanelNode(this.$.fullscreenPanelId);
570
+ if (panelNode) {
571
+ panelNode.removeAttribute('fullscreen');
572
+ panelNode.$.isFullscreen = false;
573
+ panelNode.$.fullscreenIcon = 'fullscreen';
574
+ panelNode.style.left = '';
575
+ panelNode.style.width = '';
576
+ }
577
+ this.$.fullscreenPanelId = null;
578
+ this.$.hasFullscreenTabs = false;
579
+ this.$.tabItems = [];
580
+ }
581
+
582
+ // Clear stripe mode from all panels
583
+ this.querySelectorAll('layout-node[stripe]').forEach((node) => {
584
+ node.removeAttribute('stripe');
585
+ node.style.left = '';
586
+ node.style.top = '';
587
+ node.style.width = '';
588
+ node.style.height = '';
589
+ });
590
+
591
+ // Clear all collapsed states from DOM
592
+ this.querySelectorAll('layout-node[collapsed]').forEach((node) => {
593
+ node.removeAttribute('collapsed');
594
+ node.removeAttribute('collapse-dir');
595
+ node.$.isCollapsed = false;
596
+ // Reset collapse icon based on direction
597
+ if (node.$.collapseDirection === 'horizontal') {
598
+ node.$.collapseIcon = 'chevron_left';
599
+ } else {
600
+ node.$.collapseIcon = 'expand_less';
601
+ }
602
+ });
603
+
604
+ // Clear container collapsed-child attributes
605
+ this.querySelectorAll('[collapsed-child]').forEach((el) => {
606
+ el.removeAttribute('collapsed-child');
607
+ el.removeAttribute('saved-ratio');
608
+ el.style.width = '';
609
+ el.style.height = '';
610
+ el.style.flex = '';
611
+ });
612
+
613
+ this.$.layoutTree = layout;
614
+ this._saveLayout();
615
+ }
616
+ }
617
+
618
+ Layout.template = template;
619
+ Layout.rootStyles = styles;
620
+
621
+ Layout.reg('panel-layout');
622
+
@@ -0,0 +1,25 @@
1
+ import { html } from '@symbiotejs/symbiote';
2
+
3
+ export const template = html`
4
+ <div class="layout-root" ref="root"></div>
5
+ <layout-preview ref="preview"></layout-preview>
6
+ <panel-menu ref="menu"></panel-menu>
7
+
8
+ <!-- Fullscreen tab bar (hidden by default) -->
9
+ <div class="fullscreen-tab-bar" ${{ '@hidden': '!hasFullscreenTabs' }}>
10
+ <div class="tab-list" itemize="tabItems">
11
+ <template>
12
+ <button class="fullscreen-tab"
13
+ ${{
14
+ onclick: '^onTabClick',
15
+ '@data-panel-id': 'panelId',
16
+ '@active': 'isActive'
17
+ }}>
18
+ <span class="material-symbols-outlined">{{icon}}</span>
19
+ <span>{{title}}</span>
20
+ </button>
21
+ </template>
22
+ </div>
23
+ <div class="tab-filler"></div>
24
+ </div>
25
+ `;