@jupyterlab/application 4.6.0-alpha.5 → 4.6.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/shell.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  // Copyright (c) Jupyter Development Team.
2
2
  // Distributed under the terms of the Modified BSD License.
3
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
4
 
4
5
  import type { DocumentRegistry } from '@jupyterlab/docregistry';
5
6
  import { DocumentWidget } from '@jupyterlab/docregistry';
@@ -8,7 +9,6 @@ import { nullTranslator } from '@jupyterlab/translation';
8
9
  import type { SidePanel } from '@jupyterlab/ui-components';
9
10
  import {
10
11
  classes,
11
- DockPanelSvg,
12
12
  LabIcon,
13
13
  TabBarSvg,
14
14
  tabIcon,
@@ -35,6 +35,7 @@ import {
35
35
  } from '@lumino/widgets';
36
36
  import type { JupyterFrontEnd } from './frontend';
37
37
  import type { LayoutRestorer } from './layoutrestorer';
38
+ import { OptimizedDockPanelSvg } from './dockpanel';
38
39
 
39
40
  /**
40
41
  * The class name added to AppShell instances.
@@ -63,6 +64,11 @@ const DEFAULT_RANK = 900;
63
64
 
64
65
  const ACTIVITY_CLASS = 'jp-Activity';
65
66
 
67
+ /**
68
+ * The default relative size of the down area when it is expanded.
69
+ */
70
+ const DEFAULT_DOWN_AREA_SIZE = 0.25;
71
+
66
72
  /**
67
73
  * The JupyterLab application shell token.
68
74
  */
@@ -135,8 +141,36 @@ export namespace ILabShell {
135
141
  * Set to `false` for a more compact layout.
136
142
  */
137
143
  dockPanelPadding?: boolean;
144
+
145
+ /**
146
+ * Whether to freeze panel dimensions during handle drag to improve resize
147
+ * performance when panels contain heavy DOM content.
148
+ *
149
+ * The default is `true`.
150
+ */
151
+ optimizeResize?: boolean;
152
+
153
+ /**
154
+ * Position of the side activity bars.
155
+ *
156
+ * The default is `'side'`, which keeps each activity bar on the natural
157
+ * side of its area (left for the left area, right for the right area).
158
+ * `'top'` and `'bottom'` move both activity bars to the top or bottom of
159
+ * their respective area, displaying the tabs horizontally.
160
+ */
161
+ activityBarPosition?: ActivityBarPosition;
138
162
  }
139
163
 
164
+ /**
165
+ * Position of a side activity bar within its area.
166
+ *
167
+ * `'side'` keeps the activity bar on the natural side of the area
168
+ * (left for the left area, right for the right area).
169
+ * `'top'` and `'bottom'` move the activity bar to the top or bottom
170
+ * of the side area, displaying the tabs horizontally.
171
+ */
172
+ export type ActivityBarPosition = 'side' | 'top' | 'bottom';
173
+
140
174
  /**
141
175
  * Widget position
142
176
  */
@@ -245,6 +279,11 @@ export namespace ILabShell {
245
279
  }
246
280
 
247
281
  export interface IDownArea {
282
+ /**
283
+ * A flag denoting whether the down area has been collapsed.
284
+ */
285
+ readonly collapsed?: boolean;
286
+
248
287
  /**
249
288
  * The current widget that has down area focus.
250
289
  */
@@ -358,7 +397,7 @@ export class LabShell extends Widget implements JupyterFrontEnd.IShell {
358
397
  const hboxPanel = new BoxPanel();
359
398
  const vsplitPanel = (this._vsplitPanel =
360
399
  new Private.RestorableSplitPanel());
361
- const dockPanel = (this._dockPanel = new DockPanelSvg({
400
+ const dockPanel = (this._dockPanel = new OptimizedDockPanelSvg({
362
401
  hiddenMode: Widget.HiddenMode.Display
363
402
  }));
364
403
  MessageLoop.installMessageHook(dockPanel, this._dockChildHook);
@@ -368,8 +407,14 @@ export class LabShell extends Widget implements JupyterFrontEnd.IShell {
368
407
  const downPanel = (this._downPanel = new TabPanelSvg({
369
408
  tabsMovable: true
370
409
  }));
371
- const leftHandler = (this._leftHandler = new Private.SideBarHandler());
372
- const rightHandler = (this._rightHandler = new Private.SideBarHandler());
410
+ const leftHandler = (this._leftHandler = new Private.SideBarHandler({
411
+ side: 'left',
412
+ host: hboxPanel
413
+ }));
414
+ const rightHandler = (this._rightHandler = new Private.SideBarHandler({
415
+ side: 'right',
416
+ host: hboxPanel
417
+ }));
373
418
  const rootLayout = new BoxLayout();
374
419
 
375
420
  headerPanel.id = 'jp-header-panel';
@@ -386,11 +431,15 @@ export class LabShell extends Widget implements JupyterFrontEnd.IShell {
386
431
  leftHandler.sideBar.addClass('jp-mod-left');
387
432
  leftHandler.sideBar.node.setAttribute('role', 'complementary');
388
433
  leftHandler.stackedPanel.id = 'jp-left-stack';
434
+ leftHandler.area.addClass('jp-SideArea');
435
+ leftHandler.area.node.setAttribute('data-side', 'left');
389
436
 
390
437
  rightHandler.sideBar.addClass(SIDEBAR_CLASS);
391
438
  rightHandler.sideBar.addClass('jp-mod-right');
392
439
  rightHandler.sideBar.node.setAttribute('role', 'complementary');
393
440
  rightHandler.stackedPanel.id = 'jp-right-stack';
441
+ rightHandler.area.addClass('jp-SideArea');
442
+ rightHandler.area.node.setAttribute('data-side', 'right');
394
443
 
395
444
  dockPanel.node.setAttribute('role', 'main');
396
445
 
@@ -405,10 +454,10 @@ export class LabShell extends Widget implements JupyterFrontEnd.IShell {
405
454
  hsplitPanel.orientation = 'horizontal';
406
455
  bottomPanel.direction = 'bottom-to-top';
407
456
 
408
- SplitPanel.setStretch(leftHandler.stackedPanel, 0);
457
+ SplitPanel.setStretch(leftHandler.area, 0);
409
458
  SplitPanel.setStretch(downPanel, 0);
410
459
  SplitPanel.setStretch(dockPanel, 1);
411
- SplitPanel.setStretch(rightHandler.stackedPanel, 0);
460
+ SplitPanel.setStretch(rightHandler.area, 0);
412
461
 
413
462
  BoxPanel.setStretch(leftHandler.sideBar, 0);
414
463
  BoxPanel.setStretch(hsplitPanel, 1);
@@ -416,9 +465,9 @@ export class LabShell extends Widget implements JupyterFrontEnd.IShell {
416
465
 
417
466
  SplitPanel.setStretch(vsplitPanel, 1);
418
467
 
419
- hsplitPanel.addWidget(leftHandler.stackedPanel);
468
+ hsplitPanel.addWidget(leftHandler.area);
420
469
  hsplitPanel.addWidget(dockPanel);
421
- hsplitPanel.addWidget(rightHandler.stackedPanel);
470
+ hsplitPanel.addWidget(rightHandler.area);
422
471
 
423
472
  vsplitPanel.addWidget(hsplitPanel);
424
473
  vsplitPanel.addWidget(downPanel);
@@ -585,6 +634,13 @@ export class LabShell extends Widget implements JupyterFrontEnd.IShell {
585
634
  return this._tracker.currentWidget;
586
635
  }
587
636
 
637
+ /**
638
+ * Whether the down area is collapsed.
639
+ */
640
+ get downCollapsed(): boolean {
641
+ return this._downPanel.isHidden;
642
+ }
643
+
588
644
  /**
589
645
  * A signal emitted when the main area's layout is modified.
590
646
  */
@@ -804,7 +860,13 @@ export class LabShell extends Widget implements JupyterFrontEnd.IShell {
804
860
  title => title.owner.id === id
805
861
  );
806
862
  if (tabIndex >= 0) {
863
+ const wasHidden = this._downPanel.isHidden;
807
864
  this._downPanel.currentIndex = tabIndex;
865
+ if (wasHidden) {
866
+ this._showDownPanel();
867
+ this._onLayoutModified();
868
+ }
869
+ this._downPanel.currentWidget?.activate();
808
870
  return;
809
871
  }
810
872
 
@@ -982,6 +1044,13 @@ export class LabShell extends Widget implements JupyterFrontEnd.IShell {
982
1044
  }
983
1045
  : undefined;
984
1046
 
1047
+ if (options?.rank !== undefined) {
1048
+ this._sideOptionsCache.set(widget, {
1049
+ ...this._sideOptionsCache.get(widget),
1050
+ rank: options.rank
1051
+ });
1052
+ }
1053
+
985
1054
  switch (area || 'main') {
986
1055
  case 'bottom':
987
1056
  return this._addToBottomArea(widget, options);
@@ -1005,13 +1074,19 @@ export class LabShell extends Widget implements JupyterFrontEnd.IShell {
1005
1074
  }
1006
1075
 
1007
1076
  /**
1008
- * Move a widget type to a new area.
1077
+ * Move a widget to a new area and update the shell user layout.
1009
1078
  *
1010
- * The type is determined from the `widget.id` and fallback to `widget.id`.
1079
+ * The widget is reparented to `area` immediately. The type used as the
1080
+ * user-layout key is determined from `widget.id`, falling back to
1081
+ * `widget.id` itself.
1011
1082
  *
1012
1083
  * #### Notes
1013
- * If `mode` is undefined, both mode are updated.
1014
- * The new layout is now persisted.
1084
+ * If `mode` is undefined, both modes are updated in the user layout.
1085
+ * When `mode` is set, only that mode's user layout is updated, but the
1086
+ * live widget is still reparented regardless of mode.
1087
+ *
1088
+ * The new layout is stored in the shell user layout. Callers are
1089
+ * responsible for persisting it when needed.
1015
1090
  *
1016
1091
  * @param widget Widget to move
1017
1092
  * @param area New area
@@ -1027,12 +1102,22 @@ export class LabShell extends Widget implements JupyterFrontEnd.IShell {
1027
1102
  'multiple-document': ILabShell.IUserLayout;
1028
1103
  } {
1029
1104
  const type = this._idTypeMap.get(widget.id) ?? widget.id;
1105
+ const rank = this._sideOptionsCache.get(widget)?.rank;
1030
1106
  for (const m of ['single-document', 'multiple-document'].filter(
1031
1107
  c => !mode || c === mode
1032
1108
  )) {
1109
+ const position = this._userLayout[m as DockPanel.Mode][type];
1033
1110
  this._userLayout[m as DockPanel.Mode][type] = {
1034
- ...this._userLayout[m as DockPanel.Mode][type],
1035
- area
1111
+ ...position,
1112
+ area,
1113
+ ...(rank !== undefined
1114
+ ? {
1115
+ options: {
1116
+ ...position?.options,
1117
+ rank
1118
+ }
1119
+ }
1120
+ : {})
1036
1121
  };
1037
1122
  }
1038
1123
 
@@ -1057,6 +1142,16 @@ export class LabShell extends Widget implements JupyterFrontEnd.IShell {
1057
1142
  this._onLayoutModified();
1058
1143
  }
1059
1144
 
1145
+ /**
1146
+ * Collapse the down area.
1147
+ */
1148
+ collapseDown(): void {
1149
+ if (!this._downPanel.isHidden) {
1150
+ this._hideDownPanel();
1151
+ this._onLayoutModified();
1152
+ }
1153
+ }
1154
+
1060
1155
  /**
1061
1156
  * Dispose the shell.
1062
1157
  */
@@ -1092,6 +1187,19 @@ export class LabShell extends Widget implements JupyterFrontEnd.IShell {
1092
1187
  this._onLayoutModified();
1093
1188
  }
1094
1189
 
1190
+ /**
1191
+ * Expand the down area.
1192
+ */
1193
+ expandDown(): void {
1194
+ if (this._downPanel.stackedPanel.widgets.length === 0) {
1195
+ return;
1196
+ }
1197
+
1198
+ this._showDownPanel();
1199
+ this._downPanel.currentWidget?.activate();
1200
+ this._onLayoutModified();
1201
+ }
1202
+
1095
1203
  /**
1096
1204
  * Close all widgets in the main and down area.
1097
1205
  */
@@ -1215,12 +1323,42 @@ export class LabShell extends Widget implements JupyterFrontEnd.IShell {
1215
1323
  // Rehydrate the down area
1216
1324
  if (downArea) {
1217
1325
  const { currentWidget, widgets, size } = downArea;
1326
+ const collapsed = downArea.collapsed ?? !size;
1218
1327
 
1219
1328
  const widgetIds = widgets?.map(widget => widget.id) ?? [];
1329
+ const otherAreaWidgetIds = new Set<string>();
1330
+ const collectMainWidgetIds = (
1331
+ area?: ILabShell.AreaConfig | null
1332
+ ): void => {
1333
+ if (!area) {
1334
+ return;
1335
+ }
1336
+ if (area.type === 'tab-area') {
1337
+ area.widgets.forEach(widget => {
1338
+ otherAreaWidgetIds.add(widget.id);
1339
+ });
1340
+ return;
1341
+ }
1342
+ area.children.forEach(collectMainWidgetIds);
1343
+ };
1344
+ collectMainWidgetIds(mainArea?.dock?.main);
1345
+ leftArea?.widgets?.forEach(widget => {
1346
+ otherAreaWidgetIds.add(widget.id);
1347
+ });
1348
+ rightArea?.widgets?.forEach(widget => {
1349
+ otherAreaWidgetIds.add(widget.id);
1350
+ });
1351
+
1220
1352
  // Remove absent widgets
1221
1353
  this._downPanel.tabBar.titles
1222
1354
  .filter(title => !widgetIds.includes(title.owner.id))
1223
- .map(title => title.owner.close());
1355
+ .forEach(title => {
1356
+ if (otherAreaWidgetIds.has(title.owner.id)) {
1357
+ title.owner.parent = null;
1358
+ } else {
1359
+ title.owner.close();
1360
+ }
1361
+ });
1224
1362
  // Add new widgets
1225
1363
  const titleIds = this._downPanel.tabBar.titles.map(
1226
1364
  title => title.owner.id
@@ -1247,18 +1385,23 @@ export class LabShell extends Widget implements JupyterFrontEnd.IShell {
1247
1385
  const index = this._downPanel.stackedPanel.widgets.findIndex(
1248
1386
  widget => widget.id === currentWidget.id
1249
1387
  );
1250
- if (index) {
1388
+ if (index >= 0) {
1251
1389
  this._downPanel.currentIndex = index;
1252
- this._downPanel.currentWidget?.activate();
1253
1390
  }
1254
1391
  }
1255
1392
 
1256
- if (size && size > 0.0) {
1257
- this._vsplitPanel.setRelativeSizes([1.0 - size, size]);
1393
+ if (!collapsed && widgets?.length && size && size > 0.0) {
1394
+ this._showDownPanel(size);
1395
+ this._downPanel.currentWidget?.activate();
1258
1396
  } else {
1259
- // Close all tabs and hide the panel
1260
- this._downPanel.stackedPanel.widgets.forEach(widget => widget.close());
1261
- this._downPanel.hide();
1397
+ this._hideDownPanel();
1398
+ if (size && size > 0.0) {
1399
+ // Remember the saved size so a later expand restores the user's
1400
+ // previous height. `_hideDownPanel` seeds `_lastDownAreaSize`
1401
+ // from the current splitter, which at cold startup reflects the
1402
+ // default layout rather than the persisted value.
1403
+ this._lastDownAreaSize = size;
1404
+ }
1262
1405
  }
1263
1406
  }
1264
1407
 
@@ -1309,9 +1452,15 @@ export class LabShell extends Widget implements JupyterFrontEnd.IShell {
1309
1452
  : this._dockPanel.saveLayout()
1310
1453
  },
1311
1454
  downArea: {
1455
+ collapsed: this.downCollapsed,
1312
1456
  currentWidget: this._downPanel.currentWidget,
1313
1457
  widgets: Array.from(this._downPanel.stackedPanel.widgets),
1314
- size: this._vsplitPanel.relativeSizes()[1]
1458
+ size:
1459
+ this._downPanel.stackedPanel.widgets.length === 0
1460
+ ? 0
1461
+ : this.downCollapsed
1462
+ ? this._lastDownAreaSize
1463
+ : this._vsplitPanel.relativeSizes()[1]
1315
1464
  },
1316
1465
  leftArea: this._leftHandler.dehydrate(),
1317
1466
  rightArea: this._rightHandler.dehydrate(),
@@ -1395,13 +1544,39 @@ export class LabShell extends Widget implements JupyterFrontEnd.IShell {
1395
1544
  }
1396
1545
  this._dockPanel.fit();
1397
1546
  }
1547
+
1548
+ if (config.optimizeResize !== undefined) {
1549
+ this._dockPanel.optimizeResize = config.optimizeResize;
1550
+ }
1551
+
1552
+ if (config.activityBarPosition !== undefined) {
1553
+ this._setActivityBarPosition('left', config.activityBarPosition);
1554
+ this._setActivityBarPosition('right', config.activityBarPosition);
1555
+ }
1556
+ }
1557
+
1558
+ /**
1559
+ * Move the activity bar of a side area to a new position.
1560
+ *
1561
+ * @param side The side area to update.
1562
+ * @param position The new position of the activity bar.
1563
+ */
1564
+ private _setActivityBarPosition(
1565
+ side: 'left' | 'right',
1566
+ position: ILabShell.ActivityBarPosition
1567
+ ): void {
1568
+ const handler = side === 'left' ? this._leftHandler : this._rightHandler;
1569
+ if (handler.position === position) {
1570
+ return;
1571
+ }
1572
+ handler.position = position;
1573
+ this._onLayoutModified();
1398
1574
  }
1399
1575
 
1400
1576
  /**
1401
1577
  * Returns the widgets for an application area.
1402
1578
  */
1403
1579
  widgets(area?: ILabShell.Area): IterableIterator<Widget> {
1404
- // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
1405
1580
  switch (area ?? 'main') {
1406
1581
  case 'main':
1407
1582
  return this._dockPanel.widgets();
@@ -1417,6 +1592,8 @@ export class LabShell extends Widget implements JupyterFrontEnd.IShell {
1417
1592
  return this._menuHandler.panel.children();
1418
1593
  case 'bottom':
1419
1594
  return this._bottomPanel.children();
1595
+ case 'down':
1596
+ return this._downPanel.stackedPanel.children();
1420
1597
  default:
1421
1598
  throw new Error(`Invalid area: ${area}`);
1422
1599
  }
@@ -1435,7 +1612,7 @@ export class LabShell extends Widget implements JupyterFrontEnd.IShell {
1435
1612
  private _updateTitlePanelTitle() {
1436
1613
  let current = this.currentWidget;
1437
1614
  const inputElement = this._titleHandler.inputElement;
1438
- inputElement.value = current ? current.title.label : '';
1615
+ inputElement.value = current ? TabBarSvg.titleLabel(current.title) : '';
1439
1616
  inputElement.title = current ? current.title.caption : '';
1440
1617
  }
1441
1618
 
@@ -1675,11 +1852,30 @@ export class LabShell extends Widget implements JupyterFrontEnd.IShell {
1675
1852
  }
1676
1853
 
1677
1854
  this._downPanel.addWidget(widget);
1678
- this._onLayoutModified();
1679
1855
 
1680
1856
  if (this._downPanel.isHidden) {
1681
- this._downPanel.show();
1857
+ this._showDownPanel();
1858
+ }
1859
+
1860
+ this._onLayoutModified();
1861
+ }
1862
+
1863
+ private _showDownPanel(size: number = this._lastDownAreaSize): void {
1864
+ const downSize = size > 0.0 ? size : DEFAULT_DOWN_AREA_SIZE;
1865
+ this._lastDownAreaSize = downSize;
1866
+ this._vsplitPanel.setRelativeSizes([
1867
+ Math.max(1.0 - downSize, 0.0),
1868
+ downSize
1869
+ ]);
1870
+ this._downPanel.show();
1871
+ }
1872
+
1873
+ private _hideDownPanel(): void {
1874
+ const size = this._vsplitPanel.relativeSizes()[1];
1875
+ if (size > 0.0) {
1876
+ this._lastDownAreaSize = size;
1682
1877
  }
1878
+ this._downPanel.hide();
1683
1879
  }
1684
1880
 
1685
1881
  /*
@@ -1765,7 +1961,7 @@ export class LabShell extends Widget implements JupyterFrontEnd.IShell {
1765
1961
  */
1766
1962
  private _onTabPanelChanged(): void {
1767
1963
  if (this._downPanel.stackedPanel.widgets.length === 0) {
1768
- this._downPanel.hide();
1964
+ this._hideDownPanel();
1769
1965
  }
1770
1966
  this._onLayoutModified();
1771
1967
  }
@@ -1840,9 +2036,10 @@ export class LabShell extends Widget implements JupyterFrontEnd.IShell {
1840
2036
  ILabShell.ICurrentPathChangedArgs
1841
2037
  >(this);
1842
2038
  private _modeChanged = new Signal<this, DockPanel.Mode>(this);
1843
- private _dockPanel: DockPanel;
2039
+ private _dockPanel: OptimizedDockPanelSvg;
1844
2040
  private _downPanel: TabPanel;
1845
2041
  private _isRestored = false;
2042
+ private _lastDownAreaSize = DEFAULT_DOWN_AREA_SIZE;
1846
2043
  private _layoutModified = new Signal<this, void>(this);
1847
2044
  private _layoutDebouncer = new Debouncer(() => {
1848
2045
  this._layoutModified.emit(undefined);
@@ -1988,18 +2185,31 @@ namespace Private {
1988
2185
  /**
1989
2186
  * Construct a new side bar handler.
1990
2187
  */
1991
- constructor() {
2188
+ constructor(options: SideBarHandler.IOptions) {
2189
+ this._side = options.side;
2190
+ this._host = options.host;
1992
2191
  this._sideBar = new TabBar<Widget>({
1993
2192
  insertBehavior: 'none',
1994
2193
  removeBehavior: 'none',
1995
2194
  allowDeselect: true,
1996
2195
  orientation: 'vertical'
1997
2196
  });
2197
+ // Mirror the initial position on the bar via `data-side`. The setter
2198
+ // keeps it in sync on subsequent transitions.
2199
+ this._sideBar.node.setAttribute('data-side', this._side);
1998
2200
  this._stackedPanel = new StackedPanel();
2201
+ this._area = new BoxPanel({ direction: 'top-to-bottom', spacing: 0 });
2202
+ // The stacked panel always lives inside the area wrapper. It is the
2203
+ // only stretchable child so the activity bar (when inside the wrapper)
2204
+ // takes only its natural size at the top or bottom.
2205
+ BoxPanel.setStretch(this._stackedPanel, 1);
2206
+ BoxPanel.setStretch(this._sideBar, 0);
2207
+ this._area.addWidget(this._stackedPanel);
1999
2208
  this._sideBar.hide();
2000
2209
  this._stackedPanel.hide();
2001
2210
  this._lastCurrent = null;
2002
2211
  this._sideBar.currentChanged.connect(this._onCurrentChanged, this);
2212
+ this._sideBar.tabCloseRequested.connect(this._onTabCloseRequested, this);
2003
2213
  this._sideBar.tabActivateRequested.connect(
2004
2214
  this._onTabActivateRequested,
2005
2215
  this
@@ -2028,6 +2238,77 @@ namespace Private {
2028
2238
  return this._stackedPanel;
2029
2239
  }
2030
2240
 
2241
+ /**
2242
+ * Get the wrapper panel for this side area.
2243
+ *
2244
+ * The wrapper always contains the stacked panel. When the activity bar is
2245
+ * positioned inside the area (top or bottom), it is also a child of this
2246
+ * wrapper. The wrapper is what gets added to the main horizontal split.
2247
+ */
2248
+ get area(): BoxPanel {
2249
+ return this._area;
2250
+ }
2251
+
2252
+ /**
2253
+ * Get the current position of the activity bar.
2254
+ */
2255
+ get position(): ILabShell.ActivityBarPosition {
2256
+ return this._position;
2257
+ }
2258
+
2259
+ /**
2260
+ * Set the position of the activity bar relative to the area.
2261
+ *
2262
+ * In `'side'` mode the activity bar is reattached to the host panel
2263
+ * (e.g. the shell's hboxPanel) on its natural side. In `'top'` or
2264
+ * `'bottom'` mode the activity bar is reparented inside the area wrapper
2265
+ * above or below the stacked panel and laid out horizontally.
2266
+ */
2267
+ set position(value: ILabShell.ActivityBarPosition) {
2268
+ if (this._position === value) {
2269
+ return;
2270
+ }
2271
+ this._position = value;
2272
+
2273
+ // The user-collapse flag only has meaning in horizontal mode and should
2274
+ // not carry over from a prior interaction in a different mode.
2275
+ this._isCollapsedByUser = false;
2276
+
2277
+ // Detach the activity bar from its current parent before reattaching
2278
+ // it to the new one (host or area wrapper).
2279
+ this._sideBar.parent = null;
2280
+
2281
+ // Update the orientation and the position-related attributes on the
2282
+ // activity bar. The icon/label rotation depends on these.
2283
+ const isLeft = this._side === 'left';
2284
+ this._sideBar.orientation = value === 'side' ? 'vertical' : 'horizontal';
2285
+ // Keep `jp-mod-left`/`jp-mod-right` for backward compatibility with
2286
+ // existing themes that target those classes.
2287
+ this._sideBar.toggleClass('jp-mod-left', isLeft && value === 'side');
2288
+ this._sideBar.toggleClass('jp-mod-right', !isLeft && value === 'side');
2289
+ // `data-side` mirrors the `data-orientation` attribute set by Lumino
2290
+ // and is the canonical hook for new CSS rules.
2291
+ const dataSide = value === 'side' ? (isLeft ? 'left' : 'right') : value;
2292
+ this._sideBar.node.setAttribute('data-side', dataSide);
2293
+
2294
+ // In 'side' mode, clicking the active tab collapses the area (the
2295
+ // activity bar stays as a thin strip). That collapse UX makes no sense
2296
+ // when the activity bar is at the top or bottom — clicking would just
2297
+ // leave an empty horizontal strip — so disable deselection there.
2298
+ this._sideBar.allowDeselect = value === 'side';
2299
+
2300
+ if (value === 'side') {
2301
+ const index = isLeft ? 0 : this._host.widgets.length;
2302
+ this._host.insertWidget(index, this._sideBar);
2303
+ } else if (value === 'top') {
2304
+ this._area.insertWidget(0, this._sideBar);
2305
+ } else {
2306
+ this._area.addWidget(this._sideBar);
2307
+ }
2308
+
2309
+ this._refreshVisibility();
2310
+ }
2311
+
2031
2312
  /**
2032
2313
  * Signal fires when the stack panel or the sidebar changes
2033
2314
  */
@@ -2054,13 +2335,19 @@ namespace Private {
2054
2335
  *
2055
2336
  * #### Notes
2056
2337
  * This will open the most recently used tab, or the first tab
2057
- * if there is no most recently used.
2338
+ * if there is no most recently used. In `'top'` or `'bottom'` mode it
2339
+ * also re-shows the wrapper area if a previous `collapse()` call hid it.
2058
2340
  */
2059
2341
  expand(): void {
2342
+ this._isCollapsedByUser = false;
2060
2343
  const previous =
2061
2344
  this._lastCurrent || (this._items.length > 0 && this._items[0].widget);
2062
2345
  if (previous) {
2063
2346
  this.activate(previous.id);
2347
+ } else {
2348
+ // No tab to activate, but we still need to reflect the cleared
2349
+ // collapse flag (relevant in 'top'/'bottom' mode).
2350
+ this._refreshVisibility();
2064
2351
  }
2065
2352
  }
2066
2353
 
@@ -2072,6 +2359,7 @@ namespace Private {
2072
2359
  activate(id: string): void {
2073
2360
  const widget = this._findWidgetByID(id);
2074
2361
  if (widget) {
2362
+ this._isCollapsedByUser = false;
2075
2363
  this._sideBar.currentTitle = widget.title;
2076
2364
  widget.activate();
2077
2365
  }
@@ -2086,9 +2374,16 @@ namespace Private {
2086
2374
 
2087
2375
  /**
2088
2376
  * Collapse the sidebar so no items are expanded.
2377
+ *
2378
+ * #### Notes
2379
+ * In `'side'` mode this only deselects the active tab, leaving the
2380
+ * activity bar visible. In `'top'` or `'bottom'` mode it also hides the
2381
+ * wrapper area entirely so the column can reclaim its space.
2089
2382
  */
2090
2383
  collapse(): void {
2384
+ this._isCollapsedByUser = true;
2091
2385
  this._sideBar.currentTitle = null;
2386
+ this._refreshVisibility();
2092
2387
  }
2093
2388
 
2094
2389
  /**
@@ -2106,7 +2401,7 @@ namespace Private {
2106
2401
  const title = this._sideBar.insertTab(index, widget.title);
2107
2402
  // Store the parent id in the title dataset
2108
2403
  // in order to dispatch click events to the right widget.
2109
- title.dataset = { id: widget.id };
2404
+ title.dataset = { ...title.dataset, id: widget.id };
2110
2405
  if (title.icon instanceof LabIcon) {
2111
2406
  // bind an appropriate style to the icon
2112
2407
  title.icon = title.icon.bindprops({
@@ -2162,15 +2457,49 @@ namespace Private {
2162
2457
  * Rehydrate the side bar.
2163
2458
  */
2164
2459
  rehydrate(data: ILabShell.ISideArea): void {
2460
+ if (Array.isArray(data.widgets)) {
2461
+ const widgetIds = data.widgets.map(widget => widget.id);
2462
+ const widgetIdSet = new Set(widgetIds);
2463
+
2464
+ // Add widgets that are in the saved layout but not currently
2465
+ // in the sidebar.
2466
+ const currentIds = this._stackedPanel.widgets.map(widget => widget.id);
2467
+ data.widgets
2468
+ .filter(widget => !currentIds.includes(widget.id))
2469
+ .forEach(widget => {
2470
+ this.addWidget(widget, DEFAULT_RANK);
2471
+ });
2472
+
2473
+ // Merge the saved order into the current sidebar slots so widgets
2474
+ // absent from the saved layout keep their rank-relative positions.
2475
+ let savedIndex = 0;
2476
+ const targetIds = this._stackedPanel.widgets.map(widget =>
2477
+ widgetIdSet.has(widget.id) ? widgetIds[savedIndex++] : widget.id
2478
+ );
2479
+
2480
+ targetIds.forEach((id, targetIndex) => {
2481
+ const currentIndex = this._stackedPanel.widgets.findIndex(
2482
+ widget => widget.id === id
2483
+ );
2484
+ if (currentIndex >= 0 && currentIndex !== targetIndex) {
2485
+ const widget = this._stackedPanel.widgets[currentIndex];
2486
+ ArrayExt.move(this._items, currentIndex, targetIndex);
2487
+ this._stackedPanel.insertWidget(targetIndex, widget);
2488
+ this._sideBar.insertTab(targetIndex, widget.title);
2489
+ }
2490
+ });
2491
+ }
2492
+
2493
+ if (data.visible) {
2494
+ this.show();
2495
+ } else {
2496
+ this.hide();
2497
+ }
2165
2498
  if (data.currentWidget) {
2166
2499
  this.activate(data.currentWidget.id);
2167
- }
2168
- if (data.collapsed) {
2500
+ } else if (data.collapsed) {
2169
2501
  this.collapse();
2170
2502
  }
2171
- if (!data.visible) {
2172
- this.hide();
2173
- }
2174
2503
  if (data.widgetStates) {
2175
2504
  this._stackedPanel.widgets.forEach((w: SidePanel) => {
2176
2505
  if (w.id && w.content instanceof SplitPanel) {
@@ -2181,7 +2510,11 @@ namespace Private {
2181
2510
  typeof expansion === 'boolean' &&
2182
2511
  w.content instanceof AccordionPanel
2183
2512
  ) {
2184
- expansion ? w.content.expand(widx) : w.content.collapse(widx);
2513
+ if (expansion) {
2514
+ w.content.expand(widx);
2515
+ } else {
2516
+ w.content.collapse(widx);
2517
+ }
2185
2518
  }
2186
2519
  });
2187
2520
  if (state.sizes) {
@@ -2205,6 +2538,7 @@ namespace Private {
2205
2538
  */
2206
2539
  show(): void {
2207
2540
  this._isHiddenByUser = false;
2541
+ this._isCollapsedByUser = false;
2208
2542
  this._refreshVisibility();
2209
2543
  }
2210
2544
 
@@ -2242,10 +2576,36 @@ namespace Private {
2242
2576
  * Refresh the visibility of the side bar and stacked panel.
2243
2577
  */
2244
2578
  private _refreshVisibility(): void {
2245
- this._stackedPanel.setHidden(this._sideBar.currentTitle === null);
2579
+ if (this._position === 'side') {
2580
+ // In 'side' mode the activity bar lives outside the wrapper and the
2581
+ // wrapper holds only the stack panel. Hiding the stack panel when no
2582
+ // widget is current collapses the area down to the activity bar.
2583
+ this._stackedPanel.setHidden(this._sideBar.currentTitle === null);
2584
+ } else {
2585
+ // In 'top'/'bottom' mode the activity bar lives inside the wrapper
2586
+ // alongside the stack panel. The stack panel stays visible (and
2587
+ // stretches as an empty area when no widget is current) so the
2588
+ // activity bar stays anchored at the top or bottom of the wrapper.
2589
+ this._stackedPanel.setHidden(this._items.length === 0);
2590
+ }
2246
2591
  this._sideBar.setHidden(
2247
2592
  this._isHiddenByUser || this._sideBar.titles.length === 0
2248
2593
  );
2594
+ // Hide the wrapper area so the parent split panel can reclaim its
2595
+ // allocated width when no contents would be visible.
2596
+ if (this._position === 'side') {
2597
+ // In 'side' mode wrapper visibility tracks the stack panel.
2598
+ this._area.setHidden(this._stackedPanel.isHidden);
2599
+ } else {
2600
+ // In 'top'/'bottom' mode the wrapper hides only when the user has
2601
+ // explicitly collapsed the side area or there are no widgets at all.
2602
+ // Toggling the activity bar visibility ('Show Left/Right Activity Bar')
2603
+ // hides only the bar — the stack panel and selected widget stay
2604
+ // visible inside the wrapper, matching the 'side' mode semantic.
2605
+ this._area.setHidden(
2606
+ this._isCollapsedByUser || this._sideBar.titles.length === 0
2607
+ );
2608
+ }
2249
2609
  this._updated.emit();
2250
2610
  }
2251
2611
 
@@ -2282,6 +2642,16 @@ namespace Private {
2282
2642
  args.title.owner.activate();
2283
2643
  }
2284
2644
 
2645
+ /**
2646
+ * Handle a `tabCloseRequested` signal from the sidebar.
2647
+ */
2648
+ private _onTabCloseRequested(
2649
+ sender: TabBar<Widget>,
2650
+ args: TabBar.ITabCloseRequestedArgs<Widget>
2651
+ ): void {
2652
+ args.title.owner.close();
2653
+ }
2654
+
2285
2655
  /*
2286
2656
  * Handle the `widgetRemoved` signal from the stacked panel.
2287
2657
  */
@@ -2295,13 +2665,41 @@ namespace Private {
2295
2665
  }
2296
2666
 
2297
2667
  private _isHiddenByUser = false;
2668
+ private _isCollapsedByUser = false;
2298
2669
  private _items = new Array<Private.IRankItem>();
2299
2670
  private _sideBar: TabBar<Widget>;
2300
2671
  private _stackedPanel: StackedPanel;
2672
+ private _area: BoxPanel;
2673
+ private _host: BoxPanel;
2301
2674
  private _lastCurrent: Widget | null;
2675
+ private _side: 'left' | 'right';
2676
+ private _position: ILabShell.ActivityBarPosition = 'side';
2302
2677
  private _updated: Signal<SideBarHandler, void> = new Signal(this);
2303
2678
  }
2304
2679
 
2680
+ /**
2681
+ * The namespace for the `SideBarHandler` statics.
2682
+ */
2683
+ export namespace SideBarHandler {
2684
+ /**
2685
+ * The options used to create a `SideBarHandler`.
2686
+ */
2687
+ export interface IOptions {
2688
+ /**
2689
+ * The natural side of the area (`'left'` or `'right'`). The activity
2690
+ * bar lives on this side when the position is `'side'`.
2691
+ */
2692
+ side: 'left' | 'right';
2693
+
2694
+ /**
2695
+ * The host panel that owns the activity bar in `'side'` mode. The
2696
+ * handler reattaches the bar to this host on transitions back to
2697
+ * `'side'` so the shell does not need to manage that reparenting.
2698
+ */
2699
+ host: BoxPanel;
2700
+ }
2701
+ }
2702
+
2305
2703
  export class SkipLinkWidget extends Widget {
2306
2704
  /**
2307
2705
  * Construct a new skipLink widget.
@@ -2411,7 +2809,7 @@ namespace Private {
2411
2809
  if (widget == null) {
2412
2810
  return;
2413
2811
  }
2414
- const oldName = widget.title.label;
2812
+ const oldName = TabBarSvg.titleLabel(widget.title);
2415
2813
  const inputElement = this.inputElement;
2416
2814
  const newName = inputElement.value;
2417
2815
  inputElement.blur();