dockview-core 5.2.0 → 6.0.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 (117) hide show
  1. package/README.md +3 -1
  2. package/dist/cjs/api/component.api.d.ts +93 -1
  3. package/dist/cjs/api/component.api.js +146 -0
  4. package/dist/cjs/api/dockviewGroupPanelApi.d.ts +26 -0
  5. package/dist/cjs/api/dockviewGroupPanelApi.js +21 -1
  6. package/dist/cjs/api/entryPoints.js +4 -5
  7. package/dist/cjs/array.js +7 -8
  8. package/dist/cjs/dnd/dataTransfer.d.ts +2 -1
  9. package/dist/cjs/dnd/dataTransfer.js +5 -4
  10. package/dist/cjs/dnd/droptarget.d.ts +12 -0
  11. package/dist/cjs/dnd/droptarget.js +38 -10
  12. package/dist/cjs/dnd/ghost.js +1 -2
  13. package/dist/cjs/dockview/components/panel/content.js +5 -1
  14. package/dist/cjs/dockview/components/popupService.d.ts +9 -2
  15. package/dist/cjs/dockview/components/popupService.js +24 -9
  16. package/dist/cjs/dockview/components/tab/tab.d.ts +6 -1
  17. package/dist/cjs/dockview/components/tab/tab.js +81 -9
  18. package/dist/cjs/dockview/components/titlebar/tabGroupChip.d.ts +30 -0
  19. package/dist/cjs/dockview/components/titlebar/tabGroupChip.js +95 -0
  20. package/dist/cjs/dockview/components/titlebar/tabGroupIndicator.d.ts +71 -0
  21. package/dist/cjs/dockview/components/titlebar/tabGroupIndicator.js +471 -0
  22. package/dist/cjs/dockview/components/titlebar/tabGroups.d.ts +57 -0
  23. package/dist/cjs/dockview/components/titlebar/tabGroups.js +612 -0
  24. package/dist/cjs/dockview/components/titlebar/tabOverflowControl.js +1 -2
  25. package/dist/cjs/dockview/components/titlebar/tabs.d.ts +59 -0
  26. package/dist/cjs/dockview/components/titlebar/tabs.js +1227 -144
  27. package/dist/cjs/dockview/components/titlebar/tabsContainer.d.ts +6 -0
  28. package/dist/cjs/dockview/components/titlebar/tabsContainer.js +132 -14
  29. package/dist/cjs/dockview/contextMenu.d.ts +10 -0
  30. package/dist/cjs/dockview/contextMenu.js +298 -0
  31. package/dist/cjs/dockview/dockviewComponent.d.ts +60 -3
  32. package/dist/cjs/dockview/dockviewComponent.js +712 -126
  33. package/dist/cjs/dockview/dockviewGroupPanelModel.d.ts +83 -0
  34. package/dist/cjs/dockview/dockviewGroupPanelModel.js +619 -27
  35. package/dist/cjs/dockview/dockviewShell.d.ts +128 -0
  36. package/dist/cjs/dockview/dockviewShell.js +681 -0
  37. package/dist/cjs/dockview/events.d.ts +9 -0
  38. package/dist/cjs/dockview/framework.d.ts +14 -0
  39. package/dist/cjs/dockview/options.d.ts +92 -10
  40. package/dist/cjs/dockview/options.js +10 -7
  41. package/dist/cjs/dockview/tabGroup.d.ts +99 -0
  42. package/dist/cjs/dockview/tabGroup.js +219 -0
  43. package/dist/cjs/dockview/tabGroupAccent.d.ts +65 -0
  44. package/dist/cjs/dockview/tabGroupAccent.js +128 -0
  45. package/dist/cjs/dockview/theme.d.ts +56 -1
  46. package/dist/cjs/dockview/theme.js +103 -6
  47. package/dist/cjs/dockview/types.d.ts +2 -0
  48. package/dist/cjs/dom.js +19 -19
  49. package/dist/cjs/events.js +2 -2
  50. package/dist/cjs/gridview/baseComponentGridview.d.ts +1 -0
  51. package/dist/cjs/gridview/baseComponentGridview.js +6 -3
  52. package/dist/cjs/gridview/gridview.js +7 -7
  53. package/dist/cjs/index.d.ts +8 -5
  54. package/dist/cjs/index.js +6 -1
  55. package/dist/cjs/popoutWindow.js +3 -3
  56. package/dist/cjs/splitview/splitviewPanel.d.ts +1 -1
  57. package/dist/dockview-core.js +6942 -2777
  58. package/dist/dockview-core.min.js +2 -2
  59. package/dist/dockview-core.min.js.map +1 -1
  60. package/dist/dockview-core.min.noStyle.js +2 -2
  61. package/dist/dockview-core.min.noStyle.js.map +1 -1
  62. package/dist/dockview-core.noStyle.js +6940 -2775
  63. package/dist/esm/api/component.api.d.ts +93 -1
  64. package/dist/esm/api/component.api.js +118 -0
  65. package/dist/esm/api/dockviewGroupPanelApi.d.ts +26 -0
  66. package/dist/esm/api/dockviewGroupPanelApi.js +21 -1
  67. package/dist/esm/dnd/dataTransfer.d.ts +2 -1
  68. package/dist/esm/dnd/dataTransfer.js +2 -1
  69. package/dist/esm/dnd/droptarget.d.ts +12 -0
  70. package/dist/esm/dnd/droptarget.js +33 -5
  71. package/dist/esm/dockview/components/panel/content.js +5 -1
  72. package/dist/esm/dockview/components/popupService.d.ts +9 -2
  73. package/dist/esm/dockview/components/popupService.js +23 -9
  74. package/dist/esm/dockview/components/tab/tab.d.ts +6 -1
  75. package/dist/esm/dockview/components/tab/tab.js +83 -9
  76. package/dist/esm/dockview/components/titlebar/tabGroupChip.d.ts +30 -0
  77. package/dist/esm/dockview/components/titlebar/tabGroupChip.js +68 -0
  78. package/dist/esm/dockview/components/titlebar/tabGroupIndicator.d.ts +71 -0
  79. package/dist/esm/dockview/components/titlebar/tabGroupIndicator.js +354 -0
  80. package/dist/esm/dockview/components/titlebar/tabGroups.d.ts +57 -0
  81. package/dist/esm/dockview/components/titlebar/tabGroups.js +406 -0
  82. package/dist/esm/dockview/components/titlebar/tabs.d.ts +59 -0
  83. package/dist/esm/dockview/components/titlebar/tabs.js +1011 -99
  84. package/dist/esm/dockview/components/titlebar/tabsContainer.d.ts +6 -0
  85. package/dist/esm/dockview/components/titlebar/tabsContainer.js +105 -7
  86. package/dist/esm/dockview/contextMenu.d.ts +10 -0
  87. package/dist/esm/dockview/contextMenu.js +213 -0
  88. package/dist/esm/dockview/dockviewComponent.d.ts +60 -3
  89. package/dist/esm/dockview/dockviewComponent.js +460 -35
  90. package/dist/esm/dockview/dockviewGroupPanelModel.d.ts +83 -0
  91. package/dist/esm/dockview/dockviewGroupPanelModel.js +460 -4
  92. package/dist/esm/dockview/dockviewShell.d.ts +128 -0
  93. package/dist/esm/dockview/dockviewShell.js +621 -0
  94. package/dist/esm/dockview/events.d.ts +9 -0
  95. package/dist/esm/dockview/framework.d.ts +14 -0
  96. package/dist/esm/dockview/options.d.ts +92 -10
  97. package/dist/esm/dockview/options.js +5 -2
  98. package/dist/esm/dockview/tabGroup.d.ts +99 -0
  99. package/dist/esm/dockview/tabGroup.js +144 -0
  100. package/dist/esm/dockview/tabGroupAccent.d.ts +65 -0
  101. package/dist/esm/dockview/tabGroupAccent.js +116 -0
  102. package/dist/esm/dockview/theme.d.ts +56 -1
  103. package/dist/esm/dockview/theme.js +102 -5
  104. package/dist/esm/dockview/types.d.ts +2 -0
  105. package/dist/esm/dom.js +1 -1
  106. package/dist/esm/gridview/baseComponentGridview.d.ts +1 -0
  107. package/dist/esm/gridview/baseComponentGridview.js +4 -1
  108. package/dist/esm/index.d.ts +8 -5
  109. package/dist/esm/index.js +2 -1
  110. package/dist/esm/popoutWindow.js +1 -1
  111. package/dist/esm/splitview/splitviewPanel.d.ts +1 -1
  112. package/dist/package/main.cjs.js +6936 -2801
  113. package/dist/package/main.cjs.min.js +2 -2
  114. package/dist/package/main.esm.min.mjs +2 -2
  115. package/dist/package/main.esm.mjs +6922 -2800
  116. package/dist/styles/dockview.css +1945 -196
  117. package/package.json +5 -1
@@ -7,6 +7,7 @@ import { CompositeDisposable, MutableDisposable, } from '../lifecycle';
7
7
  import { ContentContainer, } from './components/panel/content';
8
8
  import { TabsContainer, } from './components/titlebar/tabsContainer';
9
9
  import { DockviewUnhandledDragOverEvent, } from './options';
10
+ import { TabGroup, } from './tabGroup';
10
11
  export class DockviewDidDropEvent extends DockviewEvent {
11
12
  get nativeEvent() {
12
13
  return this.options.nativeEvent;
@@ -41,6 +42,9 @@ export class DockviewWillDropEvent extends DockviewDidDropEvent {
41
42
  }
42
43
  }
43
44
  export class DockviewGroupPanelModel extends CompositeDisposable {
45
+ get tabGroups() {
46
+ return this._tabGroups;
47
+ }
44
48
  get element() {
45
49
  throw new Error('dockview: not supported');
46
50
  }
@@ -108,6 +112,7 @@ export class DockviewGroupPanelModel extends CompositeDisposable {
108
112
  this._location = value;
109
113
  toggleClass(this.container, 'dv-groupview-floating', false);
110
114
  toggleClass(this.container, 'dv-groupview-popout', false);
115
+ toggleClass(this.container, 'dv-groupview-edge', false);
111
116
  switch (value.type) {
112
117
  case 'grid':
113
118
  this.contentContainer.dropTarget.setTargetZones([
@@ -129,6 +134,10 @@ export class DockviewGroupPanelModel extends CompositeDisposable {
129
134
  this.contentContainer.dropTarget.setTargetZones(['center']);
130
135
  toggleClass(this.container, 'dv-groupview-popout', true);
131
136
  break;
137
+ case 'edge':
138
+ this.contentContainer.dropTarget.setTargetZones(['center']);
139
+ toggleClass(this.container, 'dv-groupview-edge', true);
140
+ break;
132
141
  }
133
142
  this.groupPanel.api._onDidLocationChange.fire({
134
143
  location: this.location,
@@ -157,6 +166,8 @@ export class DockviewGroupPanelModel extends CompositeDisposable {
157
166
  this._height = 0;
158
167
  this._panels = [];
159
168
  this._panelDisposables = new Map();
169
+ this._tabGroupDisposables = new Map();
170
+ this._pendingMicrotaskDisposables = new Set();
160
171
  this._onMove = new Emitter();
161
172
  this.onMove = this._onMove.event;
162
173
  this._onDidDrop = new Emitter();
@@ -181,6 +192,23 @@ export class DockviewGroupPanelModel extends CompositeDisposable {
181
192
  this.onDidActivePanelChange = this._onDidActivePanelChange.event;
182
193
  this._onUnhandledDragOverEvent = new Emitter();
183
194
  this.onUnhandledDragOverEvent = this._onUnhandledDragOverEvent.event;
195
+ this._tabGroups = [];
196
+ this._tabGroupMap = new Map();
197
+ this._panelToTabGroup = new Map();
198
+ this._tabGroupIdCounter = 0;
199
+ this._pendingTabGroupUpdate = false;
200
+ this._onDidCreateTabGroup = new Emitter();
201
+ this.onDidCreateTabGroup = this._onDidCreateTabGroup.event;
202
+ this._onDidDestroyTabGroup = new Emitter();
203
+ this.onDidDestroyTabGroup = this._onDidDestroyTabGroup.event;
204
+ this._onDidAddPanelToTabGroup = new Emitter();
205
+ this.onDidAddPanelToTabGroup = this._onDidAddPanelToTabGroup.event;
206
+ this._onDidRemovePanelFromTabGroup = new Emitter();
207
+ this.onDidRemovePanelFromTabGroup = this._onDidRemovePanelFromTabGroup.event;
208
+ this._onDidTabGroupChange = new Emitter();
209
+ this.onDidTabGroupChange = this._onDidTabGroupChange.event;
210
+ this._onDidTabGroupCollapsedChange = new Emitter();
211
+ this.onDidTabGroupCollapsedChange = this._onDidTabGroupCollapsedChange.event;
184
212
  toggleClass(this.container, 'dv-groupview', true);
185
213
  this._api = new DockviewApi(this.accessor);
186
214
  this.tabsContainer = new TabsContainer(this.accessor, this.groupPanel);
@@ -195,7 +223,38 @@ export class DockviewGroupPanelModel extends CompositeDisposable {
195
223
  }), this.tabsContainer.onGroupDragStart((event) => {
196
224
  this._onGroupDragStart.fire(event);
197
225
  }), this.tabsContainer.onDrop((event) => {
226
+ var _a;
227
+ // Capture panel data before handleDropEvent (which may trigger moves)
228
+ const dragData = getPanelData();
229
+ const draggedPanelId = (_a = dragData === null || dragData === void 0 ? void 0 : dragData.panelId) !== null && _a !== void 0 ? _a : null;
198
230
  this.handleDropEvent('header', event.event, 'center', event.index);
231
+ // Update tab group membership after the move completes
232
+ if (draggedPanelId && event.targetTabGroupId) {
233
+ // Compute the local index within the target tab group
234
+ // from the global panel index so the panel is inserted
235
+ // at the correct position, not just appended.
236
+ const tabGroup = this._tabGroupMap.get(event.targetTabGroupId);
237
+ let localIndex;
238
+ if (tabGroup) {
239
+ const globalIdx = this._panels.findIndex((p) => p.id === draggedPanelId);
240
+ if (globalIdx !== -1) {
241
+ // Count how many of this group's panels
242
+ // appear before the dragged panel
243
+ localIndex = 0;
244
+ for (const pid of tabGroup.panelIds) {
245
+ const pidIdx = this._panels.findIndex((p) => p.id === pid);
246
+ if (pidIdx < globalIdx) {
247
+ localIndex++;
248
+ }
249
+ }
250
+ }
251
+ }
252
+ this.addPanelToTabGroup(event.targetTabGroupId, draggedPanelId, localIndex);
253
+ }
254
+ else if (draggedPanelId && event.targetTabGroupId === null) {
255
+ // Dropped outside any group — remove from current group
256
+ this.removePanelFromTabGroup(draggedPanelId);
257
+ }
199
258
  }), this.contentContainer.onDidFocus(() => {
200
259
  this.accessor.doSetGroupActive(this.groupPanel);
201
260
  }), this.contentContainer.onDidBlur(() => {
@@ -212,7 +271,378 @@ export class DockviewGroupPanelModel extends CompositeDisposable {
212
271
  group: this.groupPanel,
213
272
  getData: getPanelData,
214
273
  }));
215
- }), this._onMove, this._onDidChange, this._onDidDrop, this._onWillDrop, this._onDidAddPanel, this._onDidRemovePanel, this._onDidActivePanelChange, this._onUnhandledDragOverEvent, this._onDidPanelTitleChange, this._onDidPanelParametersChange);
274
+ }), this._onMove, this._onDidChange, this._onDidDrop, this._onWillDrop, this._onDidAddPanel, this._onDidRemovePanel, this._onDidActivePanelChange, this._onUnhandledDragOverEvent, this._onDidPanelTitleChange, this._onDidPanelParametersChange, this._onDidCreateTabGroup, this._onDidDestroyTabGroup, this._onDidAddPanelToTabGroup, this._onDidRemovePanelFromTabGroup, this._onDidTabGroupChange, this._onDidTabGroupCollapsedChange, this._onDidCreateTabGroup.event(() => {
275
+ this._scheduleTabGroupUpdate();
276
+ }), this._onDidDestroyTabGroup.event(() => {
277
+ this._scheduleTabGroupUpdate();
278
+ }), this._onDidAddPanelToTabGroup.event(() => {
279
+ this._scheduleTabGroupUpdate();
280
+ }), this._onDidRemovePanelFromTabGroup.event(() => {
281
+ this._scheduleTabGroupUpdate();
282
+ }), this._onDidTabGroupChange.event(() => {
283
+ this._scheduleTabGroupUpdate();
284
+ }), this._onDidTabGroupCollapsedChange.event(() => {
285
+ this._scheduleTabGroupUpdate();
286
+ }));
287
+ }
288
+ _scheduleTabGroupUpdate() {
289
+ if (this._pendingTabGroupUpdate) {
290
+ return;
291
+ }
292
+ this._pendingTabGroupUpdate = true;
293
+ queueMicrotask(() => {
294
+ this._pendingTabGroupUpdate = false;
295
+ if (!this.isDisposed) {
296
+ this.tabsContainer.updateTabGroups();
297
+ }
298
+ });
299
+ }
300
+ createTabGroup(options) {
301
+ var _a;
302
+ const id = (_a = options === null || options === void 0 ? void 0 : options.id) !== null && _a !== void 0 ? _a : `tg-${this.id}-${this._tabGroupIdCounter++}`;
303
+ const tabGroup = new TabGroup(id, {
304
+ label: options === null || options === void 0 ? void 0 : options.label,
305
+ color: options === null || options === void 0 ? void 0 : options.color,
306
+ collapsed: options === null || options === void 0 ? void 0 : options.collapsed,
307
+ componentParams: options === null || options === void 0 ? void 0 : options.componentParams,
308
+ });
309
+ this._tabGroups.push(tabGroup);
310
+ this._tabGroupMap.set(id, tabGroup);
311
+ this._tabGroupDisposables.set(id, new CompositeDisposable(tabGroup.onDidChange(() => {
312
+ this._onDidTabGroupChange.fire({ tabGroup });
313
+ }), tabGroup.onDidCollapseChange((isCollapsed) => {
314
+ if (isCollapsed) {
315
+ this._handleGroupCollapse(tabGroup);
316
+ }
317
+ else {
318
+ this._handleGroupExpand(tabGroup);
319
+ }
320
+ this._onDidTabGroupCollapsedChange.fire({
321
+ tabGroup,
322
+ });
323
+ }), tabGroup.onDidDestroy(() => {
324
+ this._removeTabGroupInternal(tabGroup);
325
+ })));
326
+ this._onDidCreateTabGroup.fire({ tabGroup });
327
+ return tabGroup;
328
+ }
329
+ dissolveTabGroup(tabGroupId) {
330
+ const tabGroup = this._tabGroupMap.get(tabGroupId);
331
+ if (!tabGroup) {
332
+ return;
333
+ }
334
+ // Remove all panels from the group (they stay in the flat panel list)
335
+ const panelIds = [...tabGroup.panelIds];
336
+ for (const panelId of panelIds) {
337
+ tabGroup.removePanel(panelId);
338
+ this._panelToTabGroup.delete(panelId);
339
+ this._onDidRemovePanelFromTabGroup.fire({ tabGroup, panelId });
340
+ }
341
+ tabGroup.dispose();
342
+ }
343
+ addPanelToTabGroup(tabGroupId, panelId, index) {
344
+ const tabGroup = this._tabGroupMap.get(tabGroupId);
345
+ if (!tabGroup) {
346
+ return;
347
+ }
348
+ // Ensure the panel actually exists in this group model
349
+ if (!this._panels.some((p) => p.id === panelId)) {
350
+ return;
351
+ }
352
+ // Remove from any existing group first
353
+ const existingGroup = this.getTabGroupForPanel(panelId);
354
+ if (existingGroup) {
355
+ if (existingGroup.id === tabGroupId) {
356
+ return; // already in this group
357
+ }
358
+ this.removePanelFromTabGroup(panelId);
359
+ }
360
+ tabGroup.addPanel(panelId, index);
361
+ this._panelToTabGroup.set(panelId, tabGroup);
362
+ // Enforce contiguity: move the panel in the flat _panels array
363
+ // to the correct global position matching its group-local index
364
+ this._enforceContiguity(tabGroup, panelId);
365
+ this._onDidAddPanelToTabGroup.fire({ tabGroup, panelId });
366
+ }
367
+ /**
368
+ * Move a panel to a new index within its tab group.
369
+ * Updates both the group's panelIds order and the flat _panels array.
370
+ */
371
+ movePanelWithinGroup(tabGroupId, panelId, newIndex) {
372
+ const tabGroup = this._tabGroupMap.get(tabGroupId);
373
+ if (!tabGroup || !tabGroup.containsPanel(panelId)) {
374
+ return;
375
+ }
376
+ // Remove and re-add at new index within the group
377
+ tabGroup.removePanel(panelId);
378
+ tabGroup.addPanel(panelId, newIndex);
379
+ // Re-enforce contiguity in the flat array
380
+ this._enforceContiguity(tabGroup, panelId);
381
+ this.tabsContainer.updateTabGroups();
382
+ }
383
+ /**
384
+ * Move a panel from one tab group to another.
385
+ */
386
+ movePanelBetweenGroups(sourcePanelId, targetTabGroupId, targetIndex) {
387
+ const sourceGroup = this._findTabGroupForPanel(sourcePanelId);
388
+ const targetGroup = this._tabGroupMap.get(targetTabGroupId);
389
+ if (!targetGroup) {
390
+ return;
391
+ }
392
+ if (sourceGroup) {
393
+ sourceGroup.removePanel(sourcePanelId);
394
+ this._panelToTabGroup.delete(sourcePanelId);
395
+ this._onDidRemovePanelFromTabGroup.fire({
396
+ tabGroup: sourceGroup,
397
+ panelId: sourcePanelId,
398
+ });
399
+ // Auto-destroy empty source group
400
+ if (sourceGroup.isEmpty) {
401
+ sourceGroup.dispose();
402
+ }
403
+ }
404
+ targetGroup.addPanel(sourcePanelId, targetIndex);
405
+ this._panelToTabGroup.set(sourcePanelId, targetGroup);
406
+ this._enforceContiguity(targetGroup, sourcePanelId);
407
+ this._onDidAddPanelToTabGroup.fire({
408
+ tabGroup: targetGroup,
409
+ panelId: sourcePanelId,
410
+ });
411
+ }
412
+ /**
413
+ * Move an entire tab group to a new position in the tab bar.
414
+ * The group's internal panel order is preserved.
415
+ */
416
+ moveTabGroup(tabGroupId, targetIndex) {
417
+ const tabGroup = this._tabGroupMap.get(tabGroupId);
418
+ if (!tabGroup || tabGroup.panelIds.length === 0) {
419
+ return;
420
+ }
421
+ // Collect group panels in their current order
422
+ const groupPanelIds = new Set(tabGroup.panelIds);
423
+ const groupPanels = tabGroup.panelIds
424
+ .map((pid) => this._panels.find((p) => p.id === pid))
425
+ .filter((p) => p !== undefined);
426
+ if (groupPanels.length === 0) {
427
+ return;
428
+ }
429
+ // Count how many group panels sit before the target index so
430
+ // we can compensate after removing them from the array.
431
+ let groupPanelsBefore = 0;
432
+ for (let i = 0; i < Math.min(targetIndex, this._panels.length); i++) {
433
+ if (groupPanelIds.has(this._panels[i].id)) {
434
+ groupPanelsBefore++;
435
+ }
436
+ }
437
+ // Remove group panels from the flat array
438
+ for (const panel of groupPanels) {
439
+ const idx = this._panels.indexOf(panel);
440
+ if (idx !== -1) {
441
+ this._panels.splice(idx, 1);
442
+ }
443
+ }
444
+ // Adjust target index to account for removed panels
445
+ const adjustedIndex = targetIndex - groupPanelsBefore;
446
+ // Clamp target index to valid range after removal
447
+ const insertAt = Math.max(0, Math.min(adjustedIndex, this._panels.length));
448
+ // Insert group panels at the target position
449
+ this._panels.splice(insertAt, 0, ...groupPanels);
450
+ // Rebuild the tabs container to match new order
451
+ for (const panel of this._panels) {
452
+ this.tabsContainer.delete(panel.id);
453
+ }
454
+ for (let i = 0; i < this._panels.length; i++) {
455
+ this.tabsContainer.openPanel(this._panels[i], i);
456
+ }
457
+ this.tabsContainer.updateTabGroups();
458
+ }
459
+ /**
460
+ * Ensure a panel is at the correct global index in _panels
461
+ * to maintain contiguity of its tab group members.
462
+ */
463
+ _enforceContiguity(tabGroup, panelId) {
464
+ const panel = this._panels.find((p) => p.id === panelId);
465
+ if (!panel) {
466
+ return;
467
+ }
468
+ const localIndex = tabGroup.indexOfPanel(panelId);
469
+ const globalIndex = this._computeGlobalIndex(tabGroup, localIndex);
470
+ const currentIndex = this._panels.indexOf(panel);
471
+ if (currentIndex === globalIndex) {
472
+ return;
473
+ }
474
+ // Move panel in the flat array
475
+ this._panels.splice(currentIndex, 1);
476
+ const adjustedIndex = globalIndex > currentIndex ? globalIndex - 1 : globalIndex;
477
+ this._panels.splice(adjustedIndex, 0, panel);
478
+ // Reorder in the tabs container to match
479
+ this.tabsContainer.delete(panelId);
480
+ this.tabsContainer.openPanel(panel, adjustedIndex);
481
+ }
482
+ /**
483
+ * Compute the global index in _panels for a group-local index.
484
+ * Finds where the group's panels start in the flat array and offsets.
485
+ */
486
+ _computeGlobalIndex(tabGroup, localIndex) {
487
+ const groupPanelIds = tabGroup.panelIds;
488
+ if (groupPanelIds.length <= 1) {
489
+ // Only one panel (the one being added), keep current position
490
+ const panel = this._panels.find((p) => p.id === groupPanelIds[0]);
491
+ return panel ? this._panels.indexOf(panel) : this._panels.length;
492
+ }
493
+ // Find the first existing group member (other than the one at localIndex)
494
+ // to anchor the group position
495
+ for (let i = 0; i < groupPanelIds.length; i++) {
496
+ if (i === localIndex) {
497
+ continue;
498
+ }
499
+ const existingPanel = this._panels.find((p) => p.id === groupPanelIds[i]);
500
+ if (existingPanel) {
501
+ const existingGlobalIndex = this._panels.indexOf(existingPanel);
502
+ // Offset based on relative position within group
503
+ return Math.max(0, existingGlobalIndex + (localIndex - i));
504
+ }
505
+ }
506
+ return this._panels.length;
507
+ }
508
+ removePanelFromTabGroup(panelId) {
509
+ const tabGroup = this._findTabGroupForPanel(panelId);
510
+ if (!tabGroup) {
511
+ return;
512
+ }
513
+ tabGroup.removePanel(panelId);
514
+ this._panelToTabGroup.delete(panelId);
515
+ this._onDidRemovePanelFromTabGroup.fire({ tabGroup, panelId });
516
+ // Auto-destroy empty groups
517
+ if (tabGroup.isEmpty) {
518
+ tabGroup.dispose();
519
+ }
520
+ }
521
+ getTabGroups() {
522
+ return this._tabGroups;
523
+ }
524
+ updateTabGroups() {
525
+ this.tabsContainer.updateTabGroups();
526
+ }
527
+ refreshTabGroupAccent() {
528
+ this.tabsContainer.refreshTabGroupAccent();
529
+ }
530
+ getTabGroupForPanel(panelId) {
531
+ return this._findTabGroupForPanel(panelId);
532
+ }
533
+ _findTabGroupForPanel(panelId) {
534
+ return this._panelToTabGroup.get(panelId);
535
+ }
536
+ _removeTabGroupInternal(tabGroup) {
537
+ const index = this._tabGroups.indexOf(tabGroup);
538
+ if (index !== -1) {
539
+ this._tabGroups.splice(index, 1);
540
+ this._tabGroupMap.delete(tabGroup.id);
541
+ for (const panelId of tabGroup.panelIds) {
542
+ this._panelToTabGroup.delete(panelId);
543
+ }
544
+ this._onDidDestroyTabGroup.fire({ tabGroup });
545
+ // Dispose the external listeners (onDidChange, onDidCollapseChange)
546
+ // we registered on this group. We cannot dispose synchronously
547
+ // here because this method runs inside the onDidDestroy fire
548
+ // loop — disposing the CompositeDisposable that holds the
549
+ // onDidDestroy subscription would splice listeners mid-iteration.
550
+ // Schedule cleanup on the next microtask instead.
551
+ const tabGroupDisposable = this._tabGroupDisposables.get(tabGroup.id);
552
+ this._tabGroupDisposables.delete(tabGroup.id);
553
+ if (tabGroupDisposable) {
554
+ this._pendingMicrotaskDisposables.add(tabGroupDisposable);
555
+ queueMicrotask(() => {
556
+ this._pendingMicrotaskDisposables.delete(tabGroupDisposable);
557
+ tabGroupDisposable.dispose();
558
+ });
559
+ }
560
+ }
561
+ }
562
+ _handleGroupCollapse(tabGroup) {
563
+ if (!this._activePanel) {
564
+ return;
565
+ }
566
+ // Only act if the active panel belongs to the collapsed group
567
+ if (!tabGroup.containsPanel(this._activePanel.id)) {
568
+ return;
569
+ }
570
+ const activePanelIndex = this._panels.indexOf(this._activePanel);
571
+ // Search right first, then left, for a visible (non-collapsed-group) panel
572
+ for (let i = activePanelIndex + 1; i < this._panels.length; i++) {
573
+ const candidate = this._panels[i];
574
+ const candidateGroup = this._findTabGroupForPanel(candidate.id);
575
+ if (!candidateGroup || !candidateGroup.collapsed) {
576
+ this.doSetActivePanel(candidate);
577
+ this.updateContainer();
578
+ return;
579
+ }
580
+ }
581
+ for (let i = activePanelIndex - 1; i >= 0; i--) {
582
+ const candidate = this._panels[i];
583
+ const candidateGroup = this._findTabGroupForPanel(candidate.id);
584
+ if (!candidateGroup || !candidateGroup.collapsed) {
585
+ this.doSetActivePanel(candidate);
586
+ this.updateContainer();
587
+ return;
588
+ }
589
+ }
590
+ // All tabs are in collapsed groups — show watermark
591
+ this.contentContainer.closePanel();
592
+ this.doSetActivePanel(undefined);
593
+ this.updateContainer();
594
+ }
595
+ _handleGroupExpand(tabGroup) {
596
+ if (this._activePanel) {
597
+ return;
598
+ }
599
+ // Watermark is showing because all groups were collapsed.
600
+ // Activate the first panel in the newly expanded group.
601
+ const firstPanelId = tabGroup.panelIds[0];
602
+ if (firstPanelId) {
603
+ const panel = this._panels.find((p) => p.id === firstPanelId);
604
+ if (panel) {
605
+ this.doSetActivePanel(panel);
606
+ this.updateContainer();
607
+ }
608
+ }
609
+ }
610
+ /** Restore tab groups from serialized data (used by fromJSON) */
611
+ restoreTabGroups(serializedGroups) {
612
+ // Bump counter past any restored numeric suffixes to avoid ID collisions
613
+ for (const data of serializedGroups) {
614
+ const match = data.id.match(/-(\d+)$/);
615
+ if (match) {
616
+ const num = parseInt(match[1], 10) + 1;
617
+ if (num > this._tabGroupIdCounter) {
618
+ this._tabGroupIdCounter = num;
619
+ }
620
+ }
621
+ }
622
+ for (const data of serializedGroups) {
623
+ const tabGroup = this.createTabGroup({
624
+ id: data.id,
625
+ label: data.label,
626
+ color: data.color,
627
+ componentParams: data.componentParams,
628
+ });
629
+ const concreteGroup = this._tabGroupMap.get(tabGroup.id);
630
+ for (const panelId of data.panelIds) {
631
+ // Only add panels that actually exist in this group model
632
+ if (this._panels.some((p) => p.id === panelId)) {
633
+ tabGroup.addPanel(panelId);
634
+ this._panelToTabGroup.set(panelId, concreteGroup);
635
+ this._enforceContiguity(concreteGroup, panelId);
636
+ }
637
+ }
638
+ if (data.collapsed) {
639
+ tabGroup.collapse();
640
+ }
641
+ // Auto-destroy if no valid panels were added
642
+ if (tabGroup.isEmpty) {
643
+ tabGroup.dispose();
644
+ }
645
+ }
216
646
  }
217
647
  focusContent() {
218
648
  this.contentContainer.element.focus();
@@ -325,6 +755,9 @@ export class DockviewGroupPanelModel extends CompositeDisposable {
325
755
  if (this.headerPosition !== 'top') {
326
756
  result.headerPosition = this.headerPosition;
327
757
  }
758
+ if (this._tabGroups.length > 0) {
759
+ result.tabGroups = this._tabGroups.map((tg) => tg.toJSON());
760
+ }
328
761
  return result;
329
762
  }
330
763
  moveToNext(options) {
@@ -464,7 +897,13 @@ export class DockviewGroupPanelModel extends CompositeDisposable {
464
897
  toggleClass(this.container, 'dv-inactive-group', !isGroupActive);
465
898
  this.tabsContainer.setActive(this.isActive);
466
899
  if (!this._activePanel && this.panels.length > 0) {
467
- this.doSetActivePanel(this.panels[0]);
900
+ const candidate = this._panels.find((p) => {
901
+ const tg = this._findTabGroupForPanel(p.id);
902
+ return !tg || !tg.collapsed;
903
+ });
904
+ if (candidate) {
905
+ this.doSetActivePanel(candidate);
906
+ }
468
907
  }
469
908
  this.updateContainer();
470
909
  }
@@ -511,6 +950,8 @@ export class DockviewGroupPanelModel extends CompositeDisposable {
511
950
  disposable.dispose();
512
951
  this._panelDisposables.delete(panel.id);
513
952
  }
953
+ // Remove panel from its tab group (auto-destroys empty groups)
954
+ this.removePanelFromTabGroup(panel.id);
514
955
  this._onDidRemovePanel.fire({ panel });
515
956
  }
516
957
  doAddPanel(panel, index = this.panels.length, options = { skipSetActive: false }) {
@@ -560,7 +1001,8 @@ export class DockviewGroupPanelModel extends CompositeDisposable {
560
1001
  updateContainer() {
561
1002
  var _a, _b;
562
1003
  this.panels.forEach((panel) => panel.runEvents());
563
- if (this.isEmpty && !this.watermark) {
1004
+ const shouldShowWatermark = this.isEmpty || !this._activePanel;
1005
+ if (shouldShowWatermark && !this.watermark) {
564
1006
  const watermark = this.accessor.createWatermarkComponent();
565
1007
  watermark.init({
566
1008
  containerApi: this._api,
@@ -574,7 +1016,7 @@ export class DockviewGroupPanelModel extends CompositeDisposable {
574
1016
  });
575
1017
  this.contentContainer.element.appendChild(this.watermark.element);
576
1018
  }
577
- if (!this.isEmpty && this.watermark) {
1019
+ if (!shouldShowWatermark && this.watermark) {
578
1020
  this.watermark.element.remove();
579
1021
  (_b = (_a = this.watermark).dispose) === null || _b === void 0 ? void 0 : _b.call(_a);
580
1022
  this.watermark = undefined;
@@ -639,6 +1081,7 @@ export class DockviewGroupPanelModel extends CompositeDisposable {
639
1081
  target: position,
640
1082
  groupId: groupId,
641
1083
  index,
1084
+ tabGroupId: data.tabGroupId,
642
1085
  });
643
1086
  return;
644
1087
  }
@@ -681,6 +1124,19 @@ export class DockviewGroupPanelModel extends CompositeDisposable {
681
1124
  (_a = this.watermark) === null || _a === void 0 ? void 0 : _a.element.remove();
682
1125
  (_c = (_b = this.watermark) === null || _b === void 0 ? void 0 : _b.dispose) === null || _c === void 0 ? void 0 : _c.call(_b);
683
1126
  this.watermark = undefined;
1127
+ // Dispose all tab groups
1128
+ for (const tabGroup of [...this._tabGroups]) {
1129
+ tabGroup.dispose();
1130
+ }
1131
+ for (const disposable of this._tabGroupDisposables.values()) {
1132
+ disposable.dispose();
1133
+ }
1134
+ this._tabGroupDisposables.clear();
1135
+ // Dispose any microtask-deferred disposables that haven't run yet
1136
+ for (const disposable of this._pendingMicrotaskDisposables) {
1137
+ disposable.dispose();
1138
+ }
1139
+ this._pendingMicrotaskDisposables.clear();
684
1140
  for (const panel of this.panels) {
685
1141
  panel.dispose();
686
1142
  }
@@ -0,0 +1,128 @@
1
+ import { Event } from '../events';
2
+ import { IDisposable } from '../lifecycle';
3
+ import { IView, LayoutPriority } from '../splitview/splitview';
4
+ export type EdgeGroupPosition = 'top' | 'bottom' | 'left' | 'right';
5
+ export interface EdgeGroupOptions {
6
+ id: string;
7
+ initialSize?: number;
8
+ minimumSize?: number;
9
+ maximumSize?: number;
10
+ collapsedSize?: number;
11
+ collapsed?: boolean;
12
+ }
13
+ export interface SerializedEdgeGroups {
14
+ top?: {
15
+ size: number;
16
+ visible: boolean;
17
+ collapsed?: boolean;
18
+ group?: unknown;
19
+ };
20
+ bottom?: {
21
+ size: number;
22
+ visible: boolean;
23
+ collapsed?: boolean;
24
+ group?: unknown;
25
+ };
26
+ left?: {
27
+ size: number;
28
+ visible: boolean;
29
+ collapsed?: boolean;
30
+ group?: unknown;
31
+ };
32
+ right?: {
33
+ size: number;
34
+ visible: boolean;
35
+ collapsed?: boolean;
36
+ group?: unknown;
37
+ };
38
+ }
39
+ /**
40
+ * Minimal interface for a edge group host.
41
+ * Avoids circular imports by not referencing DockviewGroupPanel directly.
42
+ */
43
+ export interface IEdgeGroupHost {
44
+ readonly element: HTMLElement;
45
+ layout(width: number, height: number): void;
46
+ }
47
+ export declare class EdgeGroupView implements IView {
48
+ private readonly _group;
49
+ private readonly _orientation;
50
+ private readonly _onDidChange;
51
+ readonly onDidChange: Event<{
52
+ size?: number;
53
+ orthogonalSize?: number;
54
+ }>;
55
+ readonly snap = false;
56
+ readonly priority = LayoutPriority.Low;
57
+ private _isCollapsed;
58
+ private _lastExpandedSize;
59
+ private _collapsedSize;
60
+ private _expandedMinimumSize;
61
+ private readonly _expandedMaximumSize;
62
+ get minimumSize(): number;
63
+ get maximumSize(): number;
64
+ get element(): HTMLElement;
65
+ get isCollapsed(): boolean;
66
+ get lastExpandedSize(): number;
67
+ get collapsedSize(): number;
68
+ constructor(options: EdgeGroupOptions, group: IEdgeGroupHost, orientation: 'horizontal' | 'vertical');
69
+ layout(size: number, orthogonalSize: number): void;
70
+ setCollapsed(collapsed: boolean): void;
71
+ setVisible(_visible: boolean): void;
72
+ /**
73
+ * Restore the last-expanded size from serialized state without triggering
74
+ * a layout. Must be called before setCollapsed(true) during fromJSON so
75
+ * that expanding after deserialization restores the correct size.
76
+ */
77
+ restoreExpandedSize(size: number): void;
78
+ /**
79
+ * Apply new effective collapsed and expanded-minimum sizes after a theme
80
+ * or gap change. The caller (ShellManager) is responsible for computing
81
+ * the correct values from the original config and the new gap.
82
+ */
83
+ updateCollapsedSize(newCollapsedSize: number, newExpandedMinimumSize: number): void;
84
+ dispose(): void;
85
+ }
86
+ export declare class ShellManager implements IDisposable {
87
+ private readonly _outerSplitview;
88
+ private readonly _middleColumn;
89
+ private readonly _shellElement;
90
+ private _topView;
91
+ private _bottomView;
92
+ private _leftView;
93
+ private _rightView;
94
+ private _leftIndex;
95
+ private _middleIndex;
96
+ private _rightIndex;
97
+ private readonly _disposables;
98
+ private readonly _viewConfigs;
99
+ private _currentWidth;
100
+ private _currentHeight;
101
+ private _gap;
102
+ private _defaultCollapsedSize;
103
+ constructor(container: HTMLElement, dockviewElement: HTMLElement, layoutGrid: (width: number, height: number) => void, gap?: number, defaultCollapsedSize?: number);
104
+ get element(): HTMLElement;
105
+ /**
106
+ * Add an edge group view at the given position. The view wraps the
107
+ * provided group element inside the shell's splitview layout.
108
+ * Throws if a group at this position is already registered.
109
+ */
110
+ addEdgeView(position: EdgeGroupPosition, options: EdgeGroupOptions, group: IEdgeGroupHost): EdgeGroupView;
111
+ layout(width: number, height: number): void;
112
+ /**
113
+ * Called when the active theme changes. Updates splitview margins and
114
+ * edge-group collapsed sizes so the layout matches the new theme's gap
115
+ * and tab-strip dimensions.
116
+ */
117
+ updateTheme(gap: number, defaultCollapsedSize: number): void;
118
+ removeEdgeView(position: EdgeGroupPosition): void;
119
+ hasEdgeGroup(position: EdgeGroupPosition): boolean;
120
+ setEdgeGroupVisible(position: EdgeGroupPosition, visible: boolean): void;
121
+ isEdgeGroupVisible(position: EdgeGroupPosition): boolean;
122
+ setEdgeGroupCollapsed(position: EdgeGroupPosition, collapsed: boolean): void;
123
+ isEdgeGroupCollapsed(position: EdgeGroupPosition): boolean;
124
+ private _getView;
125
+ toJSON(): SerializedEdgeGroups;
126
+ fromJSON(data: SerializedEdgeGroups): void;
127
+ dispose(): void;
128
+ }