@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/lib/shell.js CHANGED
@@ -1,14 +1,16 @@
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
  import { DocumentWidget } from '@jupyterlab/docregistry';
4
5
  import { nullTranslator } from '@jupyterlab/translation';
5
- import { classes, DockPanelSvg, LabIcon, TabBarSvg, tabIcon, TabPanelSvg } from '@jupyterlab/ui-components';
6
+ import { classes, LabIcon, TabBarSvg, tabIcon, TabPanelSvg } from '@jupyterlab/ui-components';
6
7
  import { ArrayExt, find, map } from '@lumino/algorithm';
7
8
  import { JSONExt, PromiseDelegate, Token } from '@lumino/coreutils';
8
9
  import { MessageLoop } from '@lumino/messaging';
9
10
  import { Debouncer } from '@lumino/polling';
10
11
  import { Signal } from '@lumino/signaling';
11
12
  import { AccordionPanel, BoxLayout, BoxPanel, FocusTracker, Panel, SplitPanel, StackedPanel, TabBar, Widget } from '@lumino/widgets';
13
+ import { OptimizedDockPanelSvg } from './dockpanel';
12
14
  /**
13
15
  * The class name added to AppShell instances.
14
16
  */
@@ -30,6 +32,10 @@ const ACTIVE_CLASS = 'jp-mod-active';
30
32
  */
31
33
  const DEFAULT_RANK = 900;
32
34
  const ACTIVITY_CLASS = 'jp-Activity';
35
+ /**
36
+ * The default relative size of the down area when it is expanded.
37
+ */
38
+ const DEFAULT_DOWN_AREA_SIZE = 0.25;
33
39
  /**
34
40
  * The JupyterLab application shell token.
35
41
  */
@@ -68,6 +74,7 @@ export class LabShell extends Widget {
68
74
  this._currentPathChanged = new Signal(this);
69
75
  this._modeChanged = new Signal(this);
70
76
  this._isRestored = false;
77
+ this._lastDownAreaSize = DEFAULT_DOWN_AREA_SIZE;
71
78
  this._layoutModified = new Signal(this);
72
79
  this._layoutDebouncer = new Debouncer(() => {
73
80
  this._layoutModified.emit(undefined);
@@ -101,7 +108,7 @@ export class LabShell extends Widget {
101
108
  const hboxPanel = new BoxPanel();
102
109
  const vsplitPanel = (this._vsplitPanel =
103
110
  new Private.RestorableSplitPanel());
104
- const dockPanel = (this._dockPanel = new DockPanelSvg({
111
+ const dockPanel = (this._dockPanel = new OptimizedDockPanelSvg({
105
112
  hiddenMode: Widget.HiddenMode.Display
106
113
  }));
107
114
  MessageLoop.installMessageHook(dockPanel, this._dockChildHook);
@@ -110,8 +117,14 @@ export class LabShell extends Widget {
110
117
  const downPanel = (this._downPanel = new TabPanelSvg({
111
118
  tabsMovable: true
112
119
  }));
113
- const leftHandler = (this._leftHandler = new Private.SideBarHandler());
114
- const rightHandler = (this._rightHandler = new Private.SideBarHandler());
120
+ const leftHandler = (this._leftHandler = new Private.SideBarHandler({
121
+ side: 'left',
122
+ host: hboxPanel
123
+ }));
124
+ const rightHandler = (this._rightHandler = new Private.SideBarHandler({
125
+ side: 'right',
126
+ host: hboxPanel
127
+ }));
115
128
  const rootLayout = new BoxLayout();
116
129
  headerPanel.id = 'jp-header-panel';
117
130
  menuHandler.panel.id = 'jp-menu-panel';
@@ -126,10 +139,14 @@ export class LabShell extends Widget {
126
139
  leftHandler.sideBar.addClass('jp-mod-left');
127
140
  leftHandler.sideBar.node.setAttribute('role', 'complementary');
128
141
  leftHandler.stackedPanel.id = 'jp-left-stack';
142
+ leftHandler.area.addClass('jp-SideArea');
143
+ leftHandler.area.node.setAttribute('data-side', 'left');
129
144
  rightHandler.sideBar.addClass(SIDEBAR_CLASS);
130
145
  rightHandler.sideBar.addClass('jp-mod-right');
131
146
  rightHandler.sideBar.node.setAttribute('role', 'complementary');
132
147
  rightHandler.stackedPanel.id = 'jp-right-stack';
148
+ rightHandler.area.addClass('jp-SideArea');
149
+ rightHandler.area.node.setAttribute('data-side', 'right');
133
150
  dockPanel.node.setAttribute('role', 'main');
134
151
  hboxPanel.spacing = 0;
135
152
  vsplitPanel.spacing = 1;
@@ -140,17 +157,17 @@ export class LabShell extends Widget {
140
157
  hboxPanel.direction = 'left-to-right';
141
158
  hsplitPanel.orientation = 'horizontal';
142
159
  bottomPanel.direction = 'bottom-to-top';
143
- SplitPanel.setStretch(leftHandler.stackedPanel, 0);
160
+ SplitPanel.setStretch(leftHandler.area, 0);
144
161
  SplitPanel.setStretch(downPanel, 0);
145
162
  SplitPanel.setStretch(dockPanel, 1);
146
- SplitPanel.setStretch(rightHandler.stackedPanel, 0);
163
+ SplitPanel.setStretch(rightHandler.area, 0);
147
164
  BoxPanel.setStretch(leftHandler.sideBar, 0);
148
165
  BoxPanel.setStretch(hsplitPanel, 1);
149
166
  BoxPanel.setStretch(rightHandler.sideBar, 0);
150
167
  SplitPanel.setStretch(vsplitPanel, 1);
151
- hsplitPanel.addWidget(leftHandler.stackedPanel);
168
+ hsplitPanel.addWidget(leftHandler.area);
152
169
  hsplitPanel.addWidget(dockPanel);
153
- hsplitPanel.addWidget(rightHandler.stackedPanel);
170
+ hsplitPanel.addWidget(rightHandler.area);
154
171
  vsplitPanel.addWidget(hsplitPanel);
155
172
  vsplitPanel.addWidget(downPanel);
156
173
  hboxPanel.addWidget(leftHandler.sideBar);
@@ -282,6 +299,12 @@ export class LabShell extends Widget {
282
299
  get currentWidget() {
283
300
  return this._tracker.currentWidget;
284
301
  }
302
+ /**
303
+ * Whether the down area is collapsed.
304
+ */
305
+ get downCollapsed() {
306
+ return this._downPanel.isHidden;
307
+ }
285
308
  /**
286
309
  * A signal emitted when the main area's layout is modified.
287
310
  */
@@ -443,6 +466,7 @@ export class LabShell extends Widget {
443
466
  * Activate a widget in its area.
444
467
  */
445
468
  activateById(id) {
469
+ var _a;
446
470
  if (this._leftHandler.has(id)) {
447
471
  this._leftHandler.activate(id);
448
472
  return;
@@ -453,7 +477,13 @@ export class LabShell extends Widget {
453
477
  }
454
478
  const tabIndex = this._downPanel.tabBar.titles.findIndex(title => title.owner.id === id);
455
479
  if (tabIndex >= 0) {
480
+ const wasHidden = this._downPanel.isHidden;
456
481
  this._downPanel.currentIndex = tabIndex;
482
+ if (wasHidden) {
483
+ this._showDownPanel();
484
+ this._onLayoutModified();
485
+ }
486
+ (_a = this._downPanel.currentWidget) === null || _a === void 0 ? void 0 : _a.activate();
457
487
  return;
458
488
  }
459
489
  const dock = this._dockPanel;
@@ -613,6 +643,12 @@ export class LabShell extends Widget {
613
643
  ...userPosition === null || userPosition === void 0 ? void 0 : userPosition.options
614
644
  }
615
645
  : undefined;
646
+ if ((options === null || options === void 0 ? void 0 : options.rank) !== undefined) {
647
+ this._sideOptionsCache.set(widget, {
648
+ ...this._sideOptionsCache.get(widget),
649
+ rank: options.rank
650
+ });
651
+ }
616
652
  switch (area || 'main') {
617
653
  case 'bottom':
618
654
  return this._addToBottomArea(widget, options);
@@ -635,13 +671,19 @@ export class LabShell extends Widget {
635
671
  }
636
672
  }
637
673
  /**
638
- * Move a widget type to a new area.
674
+ * Move a widget to a new area and update the shell user layout.
639
675
  *
640
- * The type is determined from the `widget.id` and fallback to `widget.id`.
676
+ * The widget is reparented to `area` immediately. The type used as the
677
+ * user-layout key is determined from `widget.id`, falling back to
678
+ * `widget.id` itself.
641
679
  *
642
680
  * #### Notes
643
- * If `mode` is undefined, both mode are updated.
644
- * The new layout is now persisted.
681
+ * If `mode` is undefined, both modes are updated in the user layout.
682
+ * When `mode` is set, only that mode's user layout is updated, but the
683
+ * live widget is still reparented regardless of mode.
684
+ *
685
+ * The new layout is stored in the shell user layout. Callers are
686
+ * responsible for persisting it when needed.
645
687
  *
646
688
  * @param widget Widget to move
647
689
  * @param area New area
@@ -649,12 +691,22 @@ export class LabShell extends Widget {
649
691
  * @returns The new user layout
650
692
  */
651
693
  move(widget, area, mode) {
652
- var _a;
694
+ var _a, _b;
653
695
  const type = (_a = this._idTypeMap.get(widget.id)) !== null && _a !== void 0 ? _a : widget.id;
696
+ const rank = (_b = this._sideOptionsCache.get(widget)) === null || _b === void 0 ? void 0 : _b.rank;
654
697
  for (const m of ['single-document', 'multiple-document'].filter(c => !mode || c === mode)) {
698
+ const position = this._userLayout[m][type];
655
699
  this._userLayout[m][type] = {
656
- ...this._userLayout[m][type],
657
- area
700
+ ...position,
701
+ area,
702
+ ...(rank !== undefined
703
+ ? {
704
+ options: {
705
+ ...position === null || position === void 0 ? void 0 : position.options,
706
+ rank
707
+ }
708
+ }
709
+ : {})
658
710
  };
659
711
  }
660
712
  this.add(widget, area);
@@ -674,6 +726,15 @@ export class LabShell extends Widget {
674
726
  this._rightHandler.collapse();
675
727
  this._onLayoutModified();
676
728
  }
729
+ /**
730
+ * Collapse the down area.
731
+ */
732
+ collapseDown() {
733
+ if (!this._downPanel.isHidden) {
734
+ this._hideDownPanel();
735
+ this._onLayoutModified();
736
+ }
737
+ }
677
738
  /**
678
739
  * Dispose the shell.
679
740
  */
@@ -706,6 +767,18 @@ export class LabShell extends Widget {
706
767
  this._rightHandler.expand();
707
768
  this._onLayoutModified();
708
769
  }
770
+ /**
771
+ * Expand the down area.
772
+ */
773
+ expandDown() {
774
+ var _a;
775
+ if (this._downPanel.stackedPanel.widgets.length === 0) {
776
+ return;
777
+ }
778
+ this._showDownPanel();
779
+ (_a = this._downPanel.currentWidget) === null || _a === void 0 ? void 0 : _a.activate();
780
+ this._onLayoutModified();
781
+ }
709
782
  /**
710
783
  * Close all widgets in the main and down area.
711
784
  */
@@ -770,7 +843,7 @@ export class LabShell extends Widget {
770
843
  * This should only be called once.
771
844
  */
772
845
  async restoreLayout(mode, layoutRestorer, configuration = {}) {
773
- var _a, _b, _c, _d;
846
+ var _a, _b, _c, _d, _e, _f, _g, _h;
774
847
  // Set the configuration and add widgets added before the shell was ready.
775
848
  this._userLayout = {
776
849
  'single-document': (_a = configuration['single-document']) !== null && _a !== void 0 ? _a : {},
@@ -813,11 +886,39 @@ export class LabShell extends Widget {
813
886
  // Rehydrate the down area
814
887
  if (downArea) {
815
888
  const { currentWidget, widgets, size } = downArea;
816
- const widgetIds = (_c = widgets === null || widgets === void 0 ? void 0 : widgets.map(widget => widget.id)) !== null && _c !== void 0 ? _c : [];
889
+ const collapsed = (_c = downArea.collapsed) !== null && _c !== void 0 ? _c : !size;
890
+ const widgetIds = (_d = widgets === null || widgets === void 0 ? void 0 : widgets.map(widget => widget.id)) !== null && _d !== void 0 ? _d : [];
891
+ const otherAreaWidgetIds = new Set();
892
+ const collectMainWidgetIds = (area) => {
893
+ if (!area) {
894
+ return;
895
+ }
896
+ if (area.type === 'tab-area') {
897
+ area.widgets.forEach(widget => {
898
+ otherAreaWidgetIds.add(widget.id);
899
+ });
900
+ return;
901
+ }
902
+ area.children.forEach(collectMainWidgetIds);
903
+ };
904
+ collectMainWidgetIds((_e = mainArea === null || mainArea === void 0 ? void 0 : mainArea.dock) === null || _e === void 0 ? void 0 : _e.main);
905
+ (_f = leftArea === null || leftArea === void 0 ? void 0 : leftArea.widgets) === null || _f === void 0 ? void 0 : _f.forEach(widget => {
906
+ otherAreaWidgetIds.add(widget.id);
907
+ });
908
+ (_g = rightArea === null || rightArea === void 0 ? void 0 : rightArea.widgets) === null || _g === void 0 ? void 0 : _g.forEach(widget => {
909
+ otherAreaWidgetIds.add(widget.id);
910
+ });
817
911
  // Remove absent widgets
818
912
  this._downPanel.tabBar.titles
819
913
  .filter(title => !widgetIds.includes(title.owner.id))
820
- .map(title => title.owner.close());
914
+ .forEach(title => {
915
+ if (otherAreaWidgetIds.has(title.owner.id)) {
916
+ title.owner.parent = null;
917
+ }
918
+ else {
919
+ title.owner.close();
920
+ }
921
+ });
821
922
  // Add new widgets
822
923
  const titleIds = this._downPanel.tabBar.titles.map(title => title.owner.id);
823
924
  widgets === null || widgets === void 0 ? void 0 : widgets.filter(widget => !titleIds.includes(widget.id)).map(widget => this._downPanel.addWidget(widget));
@@ -832,18 +933,23 @@ export class LabShell extends Widget {
832
933
  }
833
934
  if (currentWidget) {
834
935
  const index = this._downPanel.stackedPanel.widgets.findIndex(widget => widget.id === currentWidget.id);
835
- if (index) {
936
+ if (index >= 0) {
836
937
  this._downPanel.currentIndex = index;
837
- (_d = this._downPanel.currentWidget) === null || _d === void 0 ? void 0 : _d.activate();
838
938
  }
839
939
  }
840
- if (size && size > 0.0) {
841
- this._vsplitPanel.setRelativeSizes([1.0 - size, size]);
940
+ if (!collapsed && (widgets === null || widgets === void 0 ? void 0 : widgets.length) && size && size > 0.0) {
941
+ this._showDownPanel(size);
942
+ (_h = this._downPanel.currentWidget) === null || _h === void 0 ? void 0 : _h.activate();
842
943
  }
843
944
  else {
844
- // Close all tabs and hide the panel
845
- this._downPanel.stackedPanel.widgets.forEach(widget => widget.close());
846
- this._downPanel.hide();
945
+ this._hideDownPanel();
946
+ if (size && size > 0.0) {
947
+ // Remember the saved size so a later expand restores the user's
948
+ // previous height. `_hideDownPanel` seeds `_lastDownAreaSize`
949
+ // from the current splitter, which at cold startup reflects the
950
+ // default layout rather than the persisted value.
951
+ this._lastDownAreaSize = size;
952
+ }
847
953
  }
848
954
  }
849
955
  // Rehydrate the left area.
@@ -890,9 +996,14 @@ export class LabShell extends Widget {
890
996
  : this._dockPanel.saveLayout()
891
997
  },
892
998
  downArea: {
999
+ collapsed: this.downCollapsed,
893
1000
  currentWidget: this._downPanel.currentWidget,
894
1001
  widgets: Array.from(this._downPanel.stackedPanel.widgets),
895
- size: this._vsplitPanel.relativeSizes()[1]
1002
+ size: this._downPanel.stackedPanel.widgets.length === 0
1003
+ ? 0
1004
+ : this.downCollapsed
1005
+ ? this._lastDownAreaSize
1006
+ : this._vsplitPanel.relativeSizes()[1]
896
1007
  },
897
1008
  leftArea: this._leftHandler.dehydrate(),
898
1009
  rightArea: this._rightHandler.dehydrate(),
@@ -971,12 +1082,32 @@ export class LabShell extends Widget {
971
1082
  }
972
1083
  this._dockPanel.fit();
973
1084
  }
1085
+ if (config.optimizeResize !== undefined) {
1086
+ this._dockPanel.optimizeResize = config.optimizeResize;
1087
+ }
1088
+ if (config.activityBarPosition !== undefined) {
1089
+ this._setActivityBarPosition('left', config.activityBarPosition);
1090
+ this._setActivityBarPosition('right', config.activityBarPosition);
1091
+ }
1092
+ }
1093
+ /**
1094
+ * Move the activity bar of a side area to a new position.
1095
+ *
1096
+ * @param side The side area to update.
1097
+ * @param position The new position of the activity bar.
1098
+ */
1099
+ _setActivityBarPosition(side, position) {
1100
+ const handler = side === 'left' ? this._leftHandler : this._rightHandler;
1101
+ if (handler.position === position) {
1102
+ return;
1103
+ }
1104
+ handler.position = position;
1105
+ this._onLayoutModified();
974
1106
  }
975
1107
  /**
976
1108
  * Returns the widgets for an application area.
977
1109
  */
978
1110
  widgets(area) {
979
- // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
980
1111
  switch (area !== null && area !== void 0 ? area : 'main') {
981
1112
  case 'main':
982
1113
  return this._dockPanel.widgets();
@@ -992,6 +1123,8 @@ export class LabShell extends Widget {
992
1123
  return this._menuHandler.panel.children();
993
1124
  case 'bottom':
994
1125
  return this._bottomPanel.children();
1126
+ case 'down':
1127
+ return this._downPanel.stackedPanel.children();
995
1128
  default:
996
1129
  throw new Error(`Invalid area: ${area}`);
997
1130
  }
@@ -1008,7 +1141,7 @@ export class LabShell extends Widget {
1008
1141
  _updateTitlePanelTitle() {
1009
1142
  let current = this.currentWidget;
1010
1143
  const inputElement = this._titleHandler.inputElement;
1011
- inputElement.value = current ? current.title.label : '';
1144
+ inputElement.value = current ? TabBarSvg.titleLabel(current.title) : '';
1012
1145
  inputElement.title = current ? current.title.caption : '';
1013
1146
  }
1014
1147
  /**
@@ -1205,10 +1338,26 @@ export class LabShell extends Widget {
1205
1338
  title.iconClass = classes(title.iconClass, 'jp-Icon');
1206
1339
  }
1207
1340
  this._downPanel.addWidget(widget);
1208
- this._onLayoutModified();
1209
1341
  if (this._downPanel.isHidden) {
1210
- this._downPanel.show();
1342
+ this._showDownPanel();
1211
1343
  }
1344
+ this._onLayoutModified();
1345
+ }
1346
+ _showDownPanel(size = this._lastDownAreaSize) {
1347
+ const downSize = size > 0.0 ? size : DEFAULT_DOWN_AREA_SIZE;
1348
+ this._lastDownAreaSize = downSize;
1349
+ this._vsplitPanel.setRelativeSizes([
1350
+ Math.max(1.0 - downSize, 0.0),
1351
+ downSize
1352
+ ]);
1353
+ this._downPanel.show();
1354
+ }
1355
+ _hideDownPanel() {
1356
+ const size = this._vsplitPanel.relativeSizes()[1];
1357
+ if (size > 0.0) {
1358
+ this._lastDownAreaSize = size;
1359
+ }
1360
+ this._downPanel.hide();
1212
1361
  }
1213
1362
  /*
1214
1363
  * Return the tab bar adjacent to the current TabBar or `null`.
@@ -1273,7 +1422,7 @@ export class LabShell extends Widget {
1273
1422
  */
1274
1423
  _onTabPanelChanged() {
1275
1424
  if (this._downPanel.stackedPanel.widgets.length === 0) {
1276
- this._downPanel.hide();
1425
+ this._hideDownPanel();
1277
1426
  }
1278
1427
  this._onLayoutModified();
1279
1428
  }
@@ -1401,21 +1550,36 @@ var Private;
1401
1550
  /**
1402
1551
  * Construct a new side bar handler.
1403
1552
  */
1404
- constructor() {
1553
+ constructor(options) {
1405
1554
  this._isHiddenByUser = false;
1555
+ this._isCollapsedByUser = false;
1406
1556
  this._items = new Array();
1557
+ this._position = 'side';
1407
1558
  this._updated = new Signal(this);
1559
+ this._side = options.side;
1560
+ this._host = options.host;
1408
1561
  this._sideBar = new TabBar({
1409
1562
  insertBehavior: 'none',
1410
1563
  removeBehavior: 'none',
1411
1564
  allowDeselect: true,
1412
1565
  orientation: 'vertical'
1413
1566
  });
1567
+ // Mirror the initial position on the bar via `data-side`. The setter
1568
+ // keeps it in sync on subsequent transitions.
1569
+ this._sideBar.node.setAttribute('data-side', this._side);
1414
1570
  this._stackedPanel = new StackedPanel();
1571
+ this._area = new BoxPanel({ direction: 'top-to-bottom', spacing: 0 });
1572
+ // The stacked panel always lives inside the area wrapper. It is the
1573
+ // only stretchable child so the activity bar (when inside the wrapper)
1574
+ // takes only its natural size at the top or bottom.
1575
+ BoxPanel.setStretch(this._stackedPanel, 1);
1576
+ BoxPanel.setStretch(this._sideBar, 0);
1577
+ this._area.addWidget(this._stackedPanel);
1415
1578
  this._sideBar.hide();
1416
1579
  this._stackedPanel.hide();
1417
1580
  this._lastCurrent = null;
1418
1581
  this._sideBar.currentChanged.connect(this._onCurrentChanged, this);
1582
+ this._sideBar.tabCloseRequested.connect(this._onTabCloseRequested, this);
1419
1583
  this._sideBar.tabActivateRequested.connect(this._onTabActivateRequested, this);
1420
1584
  this._stackedPanel.widgetRemoved.connect(this._onWidgetRemoved, this);
1421
1585
  }
@@ -1437,6 +1601,70 @@ var Private;
1437
1601
  get stackedPanel() {
1438
1602
  return this._stackedPanel;
1439
1603
  }
1604
+ /**
1605
+ * Get the wrapper panel for this side area.
1606
+ *
1607
+ * The wrapper always contains the stacked panel. When the activity bar is
1608
+ * positioned inside the area (top or bottom), it is also a child of this
1609
+ * wrapper. The wrapper is what gets added to the main horizontal split.
1610
+ */
1611
+ get area() {
1612
+ return this._area;
1613
+ }
1614
+ /**
1615
+ * Get the current position of the activity bar.
1616
+ */
1617
+ get position() {
1618
+ return this._position;
1619
+ }
1620
+ /**
1621
+ * Set the position of the activity bar relative to the area.
1622
+ *
1623
+ * In `'side'` mode the activity bar is reattached to the host panel
1624
+ * (e.g. the shell's hboxPanel) on its natural side. In `'top'` or
1625
+ * `'bottom'` mode the activity bar is reparented inside the area wrapper
1626
+ * above or below the stacked panel and laid out horizontally.
1627
+ */
1628
+ set position(value) {
1629
+ if (this._position === value) {
1630
+ return;
1631
+ }
1632
+ this._position = value;
1633
+ // The user-collapse flag only has meaning in horizontal mode and should
1634
+ // not carry over from a prior interaction in a different mode.
1635
+ this._isCollapsedByUser = false;
1636
+ // Detach the activity bar from its current parent before reattaching
1637
+ // it to the new one (host or area wrapper).
1638
+ this._sideBar.parent = null;
1639
+ // Update the orientation and the position-related attributes on the
1640
+ // activity bar. The icon/label rotation depends on these.
1641
+ const isLeft = this._side === 'left';
1642
+ this._sideBar.orientation = value === 'side' ? 'vertical' : 'horizontal';
1643
+ // Keep `jp-mod-left`/`jp-mod-right` for backward compatibility with
1644
+ // existing themes that target those classes.
1645
+ this._sideBar.toggleClass('jp-mod-left', isLeft && value === 'side');
1646
+ this._sideBar.toggleClass('jp-mod-right', !isLeft && value === 'side');
1647
+ // `data-side` mirrors the `data-orientation` attribute set by Lumino
1648
+ // and is the canonical hook for new CSS rules.
1649
+ const dataSide = value === 'side' ? (isLeft ? 'left' : 'right') : value;
1650
+ this._sideBar.node.setAttribute('data-side', dataSide);
1651
+ // In 'side' mode, clicking the active tab collapses the area (the
1652
+ // activity bar stays as a thin strip). That collapse UX makes no sense
1653
+ // when the activity bar is at the top or bottom — clicking would just
1654
+ // leave an empty horizontal strip — so disable deselection there.
1655
+ this._sideBar.allowDeselect = value === 'side';
1656
+ if (value === 'side') {
1657
+ const index = isLeft ? 0 : this._host.widgets.length;
1658
+ this._host.insertWidget(index, this._sideBar);
1659
+ }
1660
+ else if (value === 'top') {
1661
+ this._area.insertWidget(0, this._sideBar);
1662
+ }
1663
+ else {
1664
+ this._area.addWidget(this._sideBar);
1665
+ }
1666
+ this._refreshVisibility();
1667
+ }
1440
1668
  /**
1441
1669
  * Signal fires when the stack panel or the sidebar changes
1442
1670
  */
@@ -1460,13 +1688,20 @@ var Private;
1460
1688
  *
1461
1689
  * #### Notes
1462
1690
  * This will open the most recently used tab, or the first tab
1463
- * if there is no most recently used.
1691
+ * if there is no most recently used. In `'top'` or `'bottom'` mode it
1692
+ * also re-shows the wrapper area if a previous `collapse()` call hid it.
1464
1693
  */
1465
1694
  expand() {
1695
+ this._isCollapsedByUser = false;
1466
1696
  const previous = this._lastCurrent || (this._items.length > 0 && this._items[0].widget);
1467
1697
  if (previous) {
1468
1698
  this.activate(previous.id);
1469
1699
  }
1700
+ else {
1701
+ // No tab to activate, but we still need to reflect the cleared
1702
+ // collapse flag (relevant in 'top'/'bottom' mode).
1703
+ this._refreshVisibility();
1704
+ }
1470
1705
  }
1471
1706
  /**
1472
1707
  * Activate a widget residing in the side bar by ID.
@@ -1476,6 +1711,7 @@ var Private;
1476
1711
  activate(id) {
1477
1712
  const widget = this._findWidgetByID(id);
1478
1713
  if (widget) {
1714
+ this._isCollapsedByUser = false;
1479
1715
  this._sideBar.currentTitle = widget.title;
1480
1716
  widget.activate();
1481
1717
  }
@@ -1488,9 +1724,16 @@ var Private;
1488
1724
  }
1489
1725
  /**
1490
1726
  * Collapse the sidebar so no items are expanded.
1727
+ *
1728
+ * #### Notes
1729
+ * In `'side'` mode this only deselects the active tab, leaving the
1730
+ * activity bar visible. In `'top'` or `'bottom'` mode it also hides the
1731
+ * wrapper area entirely so the column can reclaim its space.
1491
1732
  */
1492
1733
  collapse() {
1734
+ this._isCollapsedByUser = true;
1493
1735
  this._sideBar.currentTitle = null;
1736
+ this._refreshVisibility();
1494
1737
  }
1495
1738
  /**
1496
1739
  * Add a widget and its title to the stacked panel and side bar.
@@ -1508,7 +1751,7 @@ var Private;
1508
1751
  const title = this._sideBar.insertTab(index, widget.title);
1509
1752
  // Store the parent id in the title dataset
1510
1753
  // in order to dispatch click events to the right widget.
1511
- title.dataset = { id: widget.id };
1754
+ title.dataset = { ...title.dataset, id: widget.id };
1512
1755
  if (title.icon instanceof LabIcon) {
1513
1756
  // bind an appropriate style to the icon
1514
1757
  title.icon = title.icon.bindprops({
@@ -1559,15 +1802,43 @@ var Private;
1559
1802
  * Rehydrate the side bar.
1560
1803
  */
1561
1804
  rehydrate(data) {
1805
+ if (Array.isArray(data.widgets)) {
1806
+ const widgetIds = data.widgets.map(widget => widget.id);
1807
+ const widgetIdSet = new Set(widgetIds);
1808
+ // Add widgets that are in the saved layout but not currently
1809
+ // in the sidebar.
1810
+ const currentIds = this._stackedPanel.widgets.map(widget => widget.id);
1811
+ data.widgets
1812
+ .filter(widget => !currentIds.includes(widget.id))
1813
+ .forEach(widget => {
1814
+ this.addWidget(widget, DEFAULT_RANK);
1815
+ });
1816
+ // Merge the saved order into the current sidebar slots so widgets
1817
+ // absent from the saved layout keep their rank-relative positions.
1818
+ let savedIndex = 0;
1819
+ const targetIds = this._stackedPanel.widgets.map(widget => widgetIdSet.has(widget.id) ? widgetIds[savedIndex++] : widget.id);
1820
+ targetIds.forEach((id, targetIndex) => {
1821
+ const currentIndex = this._stackedPanel.widgets.findIndex(widget => widget.id === id);
1822
+ if (currentIndex >= 0 && currentIndex !== targetIndex) {
1823
+ const widget = this._stackedPanel.widgets[currentIndex];
1824
+ ArrayExt.move(this._items, currentIndex, targetIndex);
1825
+ this._stackedPanel.insertWidget(targetIndex, widget);
1826
+ this._sideBar.insertTab(targetIndex, widget.title);
1827
+ }
1828
+ });
1829
+ }
1830
+ if (data.visible) {
1831
+ this.show();
1832
+ }
1833
+ else {
1834
+ this.hide();
1835
+ }
1562
1836
  if (data.currentWidget) {
1563
1837
  this.activate(data.currentWidget.id);
1564
1838
  }
1565
- if (data.collapsed) {
1839
+ else if (data.collapsed) {
1566
1840
  this.collapse();
1567
1841
  }
1568
- if (!data.visible) {
1569
- this.hide();
1570
- }
1571
1842
  if (data.widgetStates) {
1572
1843
  this._stackedPanel.widgets.forEach((w) => {
1573
1844
  var _a;
@@ -1578,7 +1849,12 @@ var Private;
1578
1849
  const expansion = ((_a = state.expansionStates) !== null && _a !== void 0 ? _a : [])[widx];
1579
1850
  if (typeof expansion === 'boolean' &&
1580
1851
  w.content instanceof AccordionPanel) {
1581
- expansion ? w.content.expand(widx) : w.content.collapse(widx);
1852
+ if (expansion) {
1853
+ w.content.expand(widx);
1854
+ }
1855
+ else {
1856
+ w.content.collapse(widx);
1857
+ }
1582
1858
  }
1583
1859
  });
1584
1860
  if (state.sizes) {
@@ -1600,6 +1876,7 @@ var Private;
1600
1876
  */
1601
1877
  show() {
1602
1878
  this._isHiddenByUser = false;
1879
+ this._isCollapsedByUser = false;
1603
1880
  this._refreshVisibility();
1604
1881
  }
1605
1882
  /**
@@ -1632,8 +1909,34 @@ var Private;
1632
1909
  * Refresh the visibility of the side bar and stacked panel.
1633
1910
  */
1634
1911
  _refreshVisibility() {
1635
- this._stackedPanel.setHidden(this._sideBar.currentTitle === null);
1912
+ if (this._position === 'side') {
1913
+ // In 'side' mode the activity bar lives outside the wrapper and the
1914
+ // wrapper holds only the stack panel. Hiding the stack panel when no
1915
+ // widget is current collapses the area down to the activity bar.
1916
+ this._stackedPanel.setHidden(this._sideBar.currentTitle === null);
1917
+ }
1918
+ else {
1919
+ // In 'top'/'bottom' mode the activity bar lives inside the wrapper
1920
+ // alongside the stack panel. The stack panel stays visible (and
1921
+ // stretches as an empty area when no widget is current) so the
1922
+ // activity bar stays anchored at the top or bottom of the wrapper.
1923
+ this._stackedPanel.setHidden(this._items.length === 0);
1924
+ }
1636
1925
  this._sideBar.setHidden(this._isHiddenByUser || this._sideBar.titles.length === 0);
1926
+ // Hide the wrapper area so the parent split panel can reclaim its
1927
+ // allocated width when no contents would be visible.
1928
+ if (this._position === 'side') {
1929
+ // In 'side' mode wrapper visibility tracks the stack panel.
1930
+ this._area.setHidden(this._stackedPanel.isHidden);
1931
+ }
1932
+ else {
1933
+ // In 'top'/'bottom' mode the wrapper hides only when the user has
1934
+ // explicitly collapsed the side area or there are no widgets at all.
1935
+ // Toggling the activity bar visibility ('Show Left/Right Activity Bar')
1936
+ // hides only the bar — the stack panel and selected widget stay
1937
+ // visible inside the wrapper, matching the 'side' mode semantic.
1938
+ this._area.setHidden(this._isCollapsedByUser || this._sideBar.titles.length === 0);
1939
+ }
1637
1940
  this._updated.emit();
1638
1941
  }
1639
1942
  /**
@@ -1661,6 +1964,12 @@ var Private;
1661
1964
  _onTabActivateRequested(sender, args) {
1662
1965
  args.title.owner.activate();
1663
1966
  }
1967
+ /**
1968
+ * Handle a `tabCloseRequested` signal from the sidebar.
1969
+ */
1970
+ _onTabCloseRequested(sender, args) {
1971
+ args.title.owner.close();
1972
+ }
1664
1973
  /*
1665
1974
  * Handle the `widgetRemoved` signal from the stacked panel.
1666
1975
  */
@@ -1774,7 +2083,7 @@ var Private;
1774
2083
  if (widget == null) {
1775
2084
  return;
1776
2085
  }
1777
- const oldName = widget.title.label;
2086
+ const oldName = TabBarSvg.titleLabel(widget.title);
1778
2087
  const inputElement = this.inputElement;
1779
2088
  const newName = inputElement.value;
1780
2089
  inputElement.blur();