flexdesk 0.7.0 → 0.8.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/README.md +15 -0
- package/css/base.css +75 -0
- package/css/flexdesk.css +75 -0
- package/dist/flexdesk.css +75 -0
- package/dist/wm.js +95 -15
- package/dist/wm.js.map +2 -2
- package/package.json +1 -1
- package/src/tiling/shell.js +13 -2
- package/src/tiling/tile_renderer.js +123 -19
- package/src/tiling/wm.js +27 -1
package/package.json
CHANGED
package/src/tiling/shell.js
CHANGED
|
@@ -95,17 +95,26 @@ import { openForm } from '../ui/components/modal.js';
|
|
|
95
95
|
* only the tab on screen, leaving its
|
|
96
96
|
* siblings in the tile. Default off: the
|
|
97
97
|
* whole pane floats, strip and all (R8).
|
|
98
|
-
* @param {'top'|'bottom'} [cfg.tabLayout] C22. Where a multi-tab leaf
|
|
98
|
+
* @param {'top'|'bottom'|'header'} [cfg.tabLayout] C22. Where a multi-tab leaf
|
|
99
|
+
* draws its
|
|
99
100
|
* tabs. `'bottom'` is the framework's own
|
|
100
101
|
* spreadsheet strip under the tile body
|
|
101
102
|
* and is the DEFAULT, so no existing
|
|
102
103
|
* embedder's panes rearrange on upgrade;
|
|
103
104
|
* `'top'` mounts the editor tab bar
|
|
104
|
-
* between the chrome and the body
|
|
105
|
+
* between the chrome and the body;
|
|
106
|
+
* `'header'` (C38) mounts it inside the
|
|
107
|
+
* chrome in place of the title, on every
|
|
108
|
+
* tile, with a `+` for a new tab. The
|
|
105
109
|
* renderer mirrors this onto `root` as
|
|
106
110
|
* `data-twm-tabs` and watches it, so an
|
|
107
111
|
* embedder whose settings pane holds only
|
|
108
112
|
* the root element can change it live.
|
|
113
|
+
* @param {boolean} [cfg.newTabButton] C38. Whether the `'header'` strip
|
|
114
|
+
* draws its `+`. Default on; an embedder
|
|
115
|
+
* whose new tab has nowhere useful to go
|
|
116
|
+
* passes `false`. `wm.newTab()` is there
|
|
117
|
+
* either way.
|
|
109
118
|
* @param {object} [cfg.chrome] { topNav?, paletteButton?, desktops?,
|
|
110
119
|
* panelToggles?: { left?, right?, bottom? },
|
|
111
120
|
* zoom? }
|
|
@@ -134,6 +143,7 @@ export async function createShell({
|
|
|
134
143
|
backToOpenList = false,
|
|
135
144
|
floatActiveTab = false,
|
|
136
145
|
tabLayout = null,
|
|
146
|
+
newTabButton = true,
|
|
137
147
|
chrome = {},
|
|
138
148
|
// An embedder that moved its sections out of the top bar — into an icon
|
|
139
149
|
// rail, say — passes the selector its own buttons match, and F1..F8 keep
|
|
@@ -177,6 +187,7 @@ export async function createShell({
|
|
|
177
187
|
backToOpenList,
|
|
178
188
|
floatActiveTab,
|
|
179
189
|
tabLayout,
|
|
190
|
+
newTabButton,
|
|
180
191
|
// `ctx` is the delivery vehicle for leaf-mounted chrome: tile_renderer
|
|
181
192
|
// spreads it into every content factory, which is how the breadcrumb
|
|
182
193
|
// gets `taxonomy` + `rootCrumb` without a content factory knowing they
|
|
@@ -22,6 +22,36 @@
|
|
|
22
22
|
* tab strip, mounted BETWEEN the chrome and the body. The chrome
|
|
23
23
|
* is the pane's identity — its glyph, its title, its verbs — and
|
|
24
24
|
* the tabs are its contents, so the tabs go under it, not over.
|
|
25
|
+
* `header` — C38. The same `NotebookTabBar`, mounted INSIDE the chrome in
|
|
26
|
+
* place of the title, with a `+` after the last tab. One strip
|
|
27
|
+
* instead of two, so a pane spends one row on chrome, not two.
|
|
28
|
+
*
|
|
29
|
+
* ── C38. THE TABS ARE THE TITLE ─────────────────────────────────────────
|
|
30
|
+
*
|
|
31
|
+
* *"Replace the title with one element with the tab — the title is always in
|
|
32
|
+
* the tabs and we always can create more tabs. This is a more compact layout."*
|
|
33
|
+
*
|
|
34
|
+
* Under `header` the strip is drawn for EVERY content leaf, one tab included,
|
|
35
|
+
* because it is now the only place the pane's name appears. The chrome's own
|
|
36
|
+
* title and glyph are hidden by a wrap class (`twm-leaf--tabs-in-header`), not
|
|
37
|
+
* removed: a panel leaf has no tabs to draw, keeps its title, and a switch
|
|
38
|
+
* back to another layout finds the title element where it left it.
|
|
39
|
+
*
|
|
40
|
+
* The active tab's label is the CONTENT's title when the factory returned one
|
|
41
|
+
* — the string the chrome title used to show — so nothing the title said is
|
|
42
|
+
* lost by moving it into the tab.
|
|
43
|
+
*
|
|
44
|
+
* `+` appends a tab holding the taxonomy root, which is what a split seeds the
|
|
45
|
+
* new pane with (`wm.newTab`). It is a button, so `CHROME_NO_FLOAT` already
|
|
46
|
+
* makes it inert to the pull and the double-click. `newTabButton: false`
|
|
47
|
+
* leaves it out, for an embedder whose root is not somewhere a new tab should
|
|
48
|
+
* start; the tabs still replace the title.
|
|
49
|
+
*
|
|
50
|
+
* The chrome stops being a drag source here (C33): a single-tab leaf now has a
|
|
51
|
+
* tab to grab, and a strip that names the tab is the reason C33 limited the
|
|
52
|
+
* chrome to the case where there was none. A drag that begins on a tab
|
|
53
|
+
* belongs to the strip, so the chrome's `dragstart` lets it pass untouched.
|
|
54
|
+
* The empty chrome around the tabs still takes the pull and the double-click.
|
|
25
55
|
*
|
|
26
56
|
* The tab bar is reused rather than reimplemented. It already has the
|
|
27
57
|
* behaviours a top strip is expected to have — drag-to-reorder, a scrolling
|
|
@@ -153,9 +183,9 @@
|
|
|
153
183
|
import { NotebookTabBar } from '../editor/notebook_tab_bar.js';
|
|
154
184
|
|
|
155
185
|
const SPLITTER_PX = 4;
|
|
156
|
-
/** The
|
|
186
|
+
/** The three tab layouts, and the framework's default. `bottom` is the default
|
|
157
187
|
* because it is what every existing embedder already renders. */
|
|
158
|
-
const TAB_LAYOUTS = ['bottom', 'top'];
|
|
188
|
+
const TAB_LAYOUTS = ['bottom', 'top', 'header'];
|
|
159
189
|
const DEFAULT_TAB_LAYOUT = 'bottom';
|
|
160
190
|
|
|
161
191
|
/** Anything that is not one of the two layouts is the default rather than an
|
|
@@ -195,8 +225,9 @@ const TILE_LIFT_PX = 24;
|
|
|
195
225
|
*
|
|
196
226
|
* A tile's chrome is a grip with controls sitting on it — the split, float
|
|
197
227
|
* and close buttons on the right, whatever the content contributed on the
|
|
198
|
-
* left, and
|
|
199
|
-
*
|
|
228
|
+
* left, and a tab — the strip is a CHILD of the chrome under the `header`
|
|
229
|
+
* layout (C38), and a sibling of it otherwise. Neither the downward pull nor
|
|
230
|
+
* the double-click may fire when
|
|
200
231
|
* the press landed on one of those.
|
|
201
232
|
*
|
|
202
233
|
* ONE CONSTANT, TWO LISTENERS, and that is the point of writing it down
|
|
@@ -204,7 +235,7 @@ const TILE_LIFT_PX = 24;
|
|
|
204
235
|
* about where the door is are worse than one door. A button added to this
|
|
205
236
|
* strip later must become inert under BOTH gestures at once, and it cannot
|
|
206
237
|
* do that if each of them carries its own copy of the rule. */
|
|
207
|
-
const CHROME_NO_FLOAT = 'button, .twm-leaf__tab';
|
|
238
|
+
const CHROME_NO_FLOAT = 'button, .twm-leaf__tab, .notebook-tabs .tab';
|
|
208
239
|
|
|
209
240
|
/**
|
|
210
241
|
* IS THIS GESTURE THE DOWNWARD PULL? Asked in TWO places that must not be
|
|
@@ -256,7 +287,7 @@ export class TileRenderer {
|
|
|
256
287
|
static get TAB_MIME() { return TILE_TAB_MIME; }
|
|
257
288
|
|
|
258
289
|
constructor({ root, tree, content, ctx, onFocusChange, onAfterRender,
|
|
259
|
-
tabLayout = null }) {
|
|
290
|
+
tabLayout = null, newTabButton = true }) {
|
|
260
291
|
if (!content || typeof content.mount !== 'function') {
|
|
261
292
|
throw new Error('TileRenderer: a content registry is required');
|
|
262
293
|
}
|
|
@@ -279,6 +310,9 @@ export class TileRenderer {
|
|
|
279
310
|
* The renderer cannot know about windows, so it says "I have
|
|
280
311
|
* repainted" and the WM re-homes what it owns. */
|
|
281
312
|
this.onAfterRender = onAfterRender || (() => {});
|
|
313
|
+
/** C38. Whether the `header` strip draws its `+`. An embedder whose
|
|
314
|
+
* new tab would open nothing useful turns it off; `wm.newTab` stays. */
|
|
315
|
+
this.newTabButton = newTabButton !== false;
|
|
282
316
|
// leafId -> { wrapEl, bodyEl, chromeEl, content, kindKey, tabStrip }
|
|
283
317
|
this._leafCache = new Map();
|
|
284
318
|
this._drag = null;
|
|
@@ -498,6 +532,11 @@ export class TileRenderer {
|
|
|
498
532
|
// that can change while the content stays put has to be refreshed
|
|
499
533
|
// here or it is frozen at whatever it was when the tile was built.
|
|
500
534
|
_paintLeafIcon(entry.iconEl, leaf, entry.content, this.ctx);
|
|
535
|
+
// C38. …and under `header` the tabs ARE the title, so a renamed
|
|
536
|
+
// tab is a renamed title and has to be refreshed for the same
|
|
537
|
+
// reason. Only when a label changed: rebuilding the strip's tabs
|
|
538
|
+
// on every repaint would detach a tab mid-drag.
|
|
539
|
+
if (this.tabLayout === 'header') this._refreshHeaderLabels(leaf, entry);
|
|
501
540
|
return entry.wrapEl;
|
|
502
541
|
}
|
|
503
542
|
// Build fresh.
|
|
@@ -589,9 +628,11 @@ export class TileRenderer {
|
|
|
589
628
|
}
|
|
590
629
|
// C22. CHROME FIRST IN BOTH LAYOUTS. The chrome is the pane's identity
|
|
591
630
|
// and the tabs are what is inside it; a strip above the title would say
|
|
592
|
-
// the tabs own the pane rather than the other way round.
|
|
593
|
-
|
|
594
|
-
|
|
631
|
+
// the tabs own the pane rather than the other way round. C38's
|
|
632
|
+
// `header` puts the strip inside the chrome — `_placeTabBar` decides.
|
|
633
|
+
wrap.append(chrome, body, tabBar);
|
|
634
|
+
this._placeTabBar({ wrapEl: wrap, bodyEl: body, chromeEl: chrome,
|
|
635
|
+
titleEl: title, tabBarEl: tabBar });
|
|
595
636
|
|
|
596
637
|
wrap.addEventListener('mousedown', (e) => {
|
|
597
638
|
if (e.target.closest('.twm-splitter')) return;
|
|
@@ -947,9 +988,12 @@ export class TileRenderer {
|
|
|
947
988
|
* attached element is a re-parent, not a rebuild — the body keeps its
|
|
948
989
|
* DOM, its listeners and its scroll. */
|
|
949
990
|
_placeTabBar(entry) {
|
|
950
|
-
const { wrapEl, bodyEl, tabBarEl } = entry;
|
|
991
|
+
const { wrapEl, bodyEl, tabBarEl, chromeEl, titleEl } = entry;
|
|
951
992
|
if (!wrapEl || !tabBarEl || !bodyEl) return;
|
|
952
|
-
|
|
993
|
+
// C38. Right after the title, so it takes the title's place and the
|
|
994
|
+
// content's actions and the structural buttons stay on the right.
|
|
995
|
+
if (this.tabLayout === 'header' && chromeEl && titleEl) titleEl.after(tabBarEl);
|
|
996
|
+
else if (this.tabLayout === 'top') wrapEl.insertBefore(tabBarEl, bodyEl);
|
|
953
997
|
else wrapEl.appendChild(tabBarEl);
|
|
954
998
|
}
|
|
955
999
|
|
|
@@ -975,16 +1019,48 @@ export class TileRenderer {
|
|
|
975
1019
|
const bar = entry.tabBarEl;
|
|
976
1020
|
if (!bar) return;
|
|
977
1021
|
const tabs = Array.isArray(leaf.tabs) ? leaf.tabs : [];
|
|
1022
|
+
const header = this.tabLayout === 'header';
|
|
978
1023
|
bar.classList.toggle('twm-leaf__tabbar--top', this.tabLayout === 'top');
|
|
979
|
-
|
|
1024
|
+
bar.classList.toggle('twm-leaf__tabbar--header', header);
|
|
1025
|
+
// C38. One tab is enough under `header`: the strip is the title. A
|
|
1026
|
+
// panel, or a leaf with no content, has no tab to name and keeps the
|
|
1027
|
+
// chrome's title.
|
|
1028
|
+
const isPanel = String(leaf.content?.kind || '').startsWith('panel:');
|
|
1029
|
+
const inHeader = header && !isPanel && !!leaf.content && tabs.length >= 1;
|
|
1030
|
+
entry.wrapEl?.classList.toggle('twm-leaf--tabs-in-header', inHeader);
|
|
1031
|
+
if (!inHeader && tabs.length <= 1) {
|
|
980
1032
|
bar.classList.add('twm-leaf__tabbar--hidden');
|
|
981
1033
|
this._disposeTabStrip(entry);
|
|
982
1034
|
bar.innerHTML = '';
|
|
983
1035
|
return;
|
|
984
1036
|
}
|
|
985
1037
|
bar.classList.remove('twm-leaf__tabbar--hidden');
|
|
986
|
-
if (this.tabLayout === '
|
|
987
|
-
else this.
|
|
1038
|
+
if (this.tabLayout === 'bottom') this._renderBottomTabBar(leaf, entry, tabs);
|
|
1039
|
+
else this._renderTopTabBar(leaf, entry, tabs);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/** C38. The labels the `header` strip shows for `leaf`, in one string, so
|
|
1043
|
+
* a repaint can tell whether anything it draws has changed. */
|
|
1044
|
+
_headerLabels(leaf, entry) {
|
|
1045
|
+
const tabs = Array.isArray(leaf.tabs) ? leaf.tabs : [];
|
|
1046
|
+
const activeIdx = Math.max(0, Math.min(tabs.length - 1, leaf.activeTabIdx || 0));
|
|
1047
|
+
return tabs.map((t, i) => this._tabLabel(t, i === activeIdx, entry)).join('\n');
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/** C38. A cached leaf is not re-mounted, so a tab renamed in the tree
|
|
1051
|
+
* (`TileTree.renameMatchingTabs`) reaches the strip only through here. */
|
|
1052
|
+
_refreshHeaderLabels(leaf, entry) {
|
|
1053
|
+
const labels = this._headerLabels(leaf, entry);
|
|
1054
|
+
if (entry.headerLabels === labels) return;
|
|
1055
|
+
this._renderTabBar(leaf, entry);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/** What a tab in the `top` or `header` strip is called. A tab with no
|
|
1059
|
+
* title of its own falls back, when it is the active one, to the title
|
|
1060
|
+
* its content gave at mount — the string the chrome title would have
|
|
1061
|
+
* shown, and under `header` the chrome title is hidden (C38). */
|
|
1062
|
+
_tabLabel(t, active, entry) {
|
|
1063
|
+
return t.title || (active && entry?.content?.title) || t.kind || '';
|
|
988
1064
|
}
|
|
989
1065
|
|
|
990
1066
|
/**
|
|
@@ -1021,12 +1097,19 @@ export class TileRenderer {
|
|
|
1021
1097
|
const tabs = Array.isArray(leaf.tabs) ? leaf.tabs : [];
|
|
1022
1098
|
const isPanel = String(leaf.content?.kind || '').startsWith('panel:');
|
|
1023
1099
|
const vetoed = this.leafChrome(leaf.id)?.promote === false;
|
|
1024
|
-
|
|
1100
|
+
// C38. Not under `header`: the one tab is drawn in the strip, and the
|
|
1101
|
+
// strip is its drag source.
|
|
1102
|
+
const on = tabs.length === 1 && !isPanel && !vetoed && this.tabLayout !== 'header';
|
|
1025
1103
|
if (on) chrome.setAttribute('draggable', 'true');
|
|
1026
1104
|
else chrome.removeAttribute('draggable');
|
|
1027
1105
|
if (chrome.__twmTabDragBound) return;
|
|
1028
1106
|
chrome.__twmTabDragBound = true;
|
|
1029
1107
|
chrome.addEventListener('dragstart', (ev) => {
|
|
1108
|
+
// C38. A drag that began in the strip — which the `header` layout
|
|
1109
|
+
// puts INSIDE the chrome — is the strip's, and has bubbled here.
|
|
1110
|
+
// Not refused (that would cancel the tab's own drag) and not
|
|
1111
|
+
// re-begun as tab 0: left alone.
|
|
1112
|
+
if (entry.tabBarEl?.contains(ev.target)) return;
|
|
1030
1113
|
// A drag that began on a BUTTON is not a drag of the pane. Buttons
|
|
1031
1114
|
// are not draggable on their own, but a draggable ancestor makes
|
|
1032
1115
|
// them so — hence the same guard the two float gestures apply, from
|
|
@@ -1143,11 +1226,32 @@ export class TileRenderer {
|
|
|
1143
1226
|
{ idx, x: ev.clientX, y: ev.clientY });
|
|
1144
1227
|
}, true);
|
|
1145
1228
|
}
|
|
1229
|
+
// C38. The `+` after the last tab, `header` only. Outside the
|
|
1230
|
+
// component's container so its `update()` never rewrites it, and
|
|
1231
|
+
// built with the strip so `setTabLayout` (which empties the bar and
|
|
1232
|
+
// disposes the strip) takes it away with the strip.
|
|
1233
|
+
if (this.tabLayout === 'header' && this.newTabButton
|
|
1234
|
+
&& !bar.querySelector('.twm-leaf__tab-new')) {
|
|
1235
|
+
const add = document.createElement('button');
|
|
1236
|
+
add.type = 'button';
|
|
1237
|
+
add.className = 'twm-leaf__tab-new';
|
|
1238
|
+
add.dataset.action = 'tab-new';
|
|
1239
|
+
add.title = 'New tab';
|
|
1240
|
+
add.setAttribute('aria-label', 'New tab');
|
|
1241
|
+
add.innerHTML = '<span class="material-symbols-outlined">add</span>';
|
|
1242
|
+
add.addEventListener('click', (ev) => {
|
|
1243
|
+
ev.preventDefault();
|
|
1244
|
+
ev.stopPropagation();
|
|
1245
|
+
this.ctx.onLeafTabAction?.(leaf.id, 'new', {});
|
|
1246
|
+
});
|
|
1247
|
+
bar.appendChild(add);
|
|
1248
|
+
}
|
|
1146
1249
|
const activeIdx = Math.max(0, Math.min(tabs.length - 1, leaf.activeTabIdx || 0));
|
|
1250
|
+
entry.headerLabels = this._headerLabels(leaf, entry);
|
|
1147
1251
|
entry.tabStrip.update(
|
|
1148
1252
|
tabs.map((t, i) => ({
|
|
1149
1253
|
filePath: _tabKey(i),
|
|
1150
|
-
label: t
|
|
1254
|
+
label: this._tabLabel(t, i === activeIdx, entry),
|
|
1151
1255
|
// The component's `fileType` picks a glyph out of a static map
|
|
1152
1256
|
// of FILE kinds. A tile's kinds are the embedder's, and the
|
|
1153
1257
|
// taxonomy already answers for them — see `_paintTopTabs`.
|
|
@@ -1159,7 +1263,7 @@ export class TileRenderer {
|
|
|
1159
1263
|
isDirty: !!t.dirty,
|
|
1160
1264
|
})),
|
|
1161
1265
|
_tabKey(activeIdx));
|
|
1162
|
-
this._paintTopTabs(entry.tabStripHostEl, tabs);
|
|
1266
|
+
this._paintTopTabs(entry.tabStripHostEl, tabs, activeIdx, entry);
|
|
1163
1267
|
}
|
|
1164
1268
|
|
|
1165
1269
|
/**
|
|
@@ -1179,13 +1283,13 @@ export class TileRenderer {
|
|
|
1179
1283
|
* rendered last. The class names used here are the component's published
|
|
1180
1284
|
* contract, stated in its header comment.
|
|
1181
1285
|
*/
|
|
1182
|
-
_paintTopTabs(hostEl, tabs) {
|
|
1286
|
+
_paintTopTabs(hostEl, tabs, activeIdx = -1, entry = null) {
|
|
1183
1287
|
if (!hostEl) return;
|
|
1184
1288
|
const els = hostEl.querySelectorAll('.tab');
|
|
1185
1289
|
els.forEach((el, i) => {
|
|
1186
1290
|
const spec = tabs[i];
|
|
1187
1291
|
if (!spec) return;
|
|
1188
|
-
el.title = spec
|
|
1292
|
+
el.title = this._tabLabel(spec, i === activeIdx, entry);
|
|
1189
1293
|
const icon = spec.kind ? this.ctx?.taxonomy?.meta?.(spec.kind)?.icon : null;
|
|
1190
1294
|
const glyph = el.querySelector('.tab-icon');
|
|
1191
1295
|
if (glyph && icon) glyph.textContent = icon;
|
package/src/tiling/wm.js
CHANGED
|
@@ -35,7 +35,7 @@ export class WindowManager {
|
|
|
35
35
|
constructor({ rootEl, api, ctx, onChange, eventBus, host, taxonomy, events, content,
|
|
36
36
|
panelDefaults = null, snapPromotion = false,
|
|
37
37
|
promoteInPlace = false, tabLayout = null, backToOpenList = false,
|
|
38
|
-
floatActiveTab = false }) {
|
|
38
|
+
floatActiveTab = false, newTabButton = true }) {
|
|
39
39
|
if (!taxonomy) throw new Error('WindowManager: a taxonomy is required');
|
|
40
40
|
if (!content || typeof content.mount !== 'function') {
|
|
41
41
|
throw new Error('WindowManager: a content registry is required '
|
|
@@ -121,6 +121,8 @@ export class WindowManager {
|
|
|
121
121
|
* `data-twm-tabs` and watches it, so an embedder can change it
|
|
122
122
|
* live without holding a renderer reference. */
|
|
123
123
|
tabLayout,
|
|
124
|
+
/** C38. Whether the `header` strip draws its `+`. */
|
|
125
|
+
newTabButton,
|
|
124
126
|
ctx: {
|
|
125
127
|
...this.ctx,
|
|
126
128
|
wm: this,
|
|
@@ -177,6 +179,24 @@ export class WindowManager {
|
|
|
177
179
|
this.renderer.setTabLayout(layout);
|
|
178
180
|
}
|
|
179
181
|
|
|
182
|
+
/** C38. Open a new tab in a tile and show it. It holds the taxonomy root —
|
|
183
|
+
* what a split seeds its new pane with, so "a new tab" and "a new pane"
|
|
184
|
+
* start in the same place. Defaults to the focused tile. A panel tile
|
|
185
|
+
* grows no tabs (`TileTree.appendLeafTab`); then this does nothing and
|
|
186
|
+
* answers -1, else the new tab's index. */
|
|
187
|
+
newTab(leafId = null) {
|
|
188
|
+
const tree = this._tree();
|
|
189
|
+
const id = leafId ?? tree.focusedLeafId;
|
|
190
|
+
const seed = this._rootLeaf();
|
|
191
|
+
const idx = tree.appendLeafTab(id, seed.content, seed.title);
|
|
192
|
+
if (idx < 0) return -1;
|
|
193
|
+
tree.focus(id);
|
|
194
|
+
this.renderer.render();
|
|
195
|
+
this._persist();
|
|
196
|
+
this._notifyChange('tab-new');
|
|
197
|
+
return idx;
|
|
198
|
+
}
|
|
199
|
+
|
|
180
200
|
/** Emit on bus + call onChange. Use this instead of the bare callback
|
|
181
201
|
* so other surfaces (palette, top-bar toggles, page shortcuts) can
|
|
182
202
|
* subscribe through the existing event system. */
|
|
@@ -3449,6 +3469,12 @@ export class WindowManager {
|
|
|
3449
3469
|
this._notifyChange('twm-tab-close');
|
|
3450
3470
|
return;
|
|
3451
3471
|
}
|
|
3472
|
+
if (action === 'new') {
|
|
3473
|
+
// C38. The `header` strip's `+`. `newTab` renders, persists and
|
|
3474
|
+
// notifies on its own.
|
|
3475
|
+
this.newTab(leafId);
|
|
3476
|
+
return;
|
|
3477
|
+
}
|
|
3452
3478
|
if (action === 'move') {
|
|
3453
3479
|
tree.moveLeafTab(leafId, data.from, data.to);
|
|
3454
3480
|
this.renderer.render();
|