flexdesk 0.7.0 → 0.8.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.
package/README.md CHANGED
@@ -121,6 +121,7 @@ await createShell({
121
121
  promoteInPlace: true, // a window floated out of a tile stays inside that tile's pane
122
122
  backToOpenList: true, // Back in a record opened beside its list closes onto that list
123
123
  floatActiveTab: true, // floating a tile takes the tab on screen, not the whole pane
124
+ tabLayout: 'header', // tabs in the tile header, in place of its title, with a + (0.8.0)
124
125
  chrome: {
125
126
  topNav: document.getElementById('nav'),
126
127
  zoom: document.getElementById('status-bar'), // content zoom, 50–200%
@@ -151,6 +152,18 @@ views of one pane. The float button, Alt+F and the tile menu take only the tab
151
152
  on screen and leave its siblings in the tile, so the window holds one tab and
152
153
  draws no strip. Without it the whole pane floats, strip and all.
153
154
 
155
+ **`tabLayout`** says where a tile draws its tabs. `'bottom'` (the default) is
156
+ the spreadsheet strip under the body, shown from two tabs. `'top'` is the
157
+ editor tab bar between the header and the body, also from two tabs.
158
+ `'header'` (0.8.0) puts that tab bar inside the header in place of the title,
159
+ on every tile, one tab included, with a `+` after the last tab that opens the
160
+ taxonomy root in a new tab (`wm.newTab(leafId)`, the same seed a split uses).
161
+ A panel tile has no tabs and keeps its title. Under `'header'` the tab, not
162
+ the header, is what you drag to move a single-tab pane; the empty part of the
163
+ header still floats the pane on a pull-down or a double-click. Set
164
+ `data-twm-tabs` on the shell's root, or call `wm.setTabLayout()`, to switch
165
+ live; nothing mounted in a tile is unmounted.
166
+
154
167
  **Back and the breadcrumb are one rule.** `wm.navigateBack()` (Backspace) and
155
168
  `wm.navigateUp(kind, props, { ctx })` (every breadcrumb crumb) share it. With
156
169
  `backToOpenList`, a record whose section has a list open closes onto that list;
package/css/base.css CHANGED
@@ -4844,6 +4844,81 @@ button.twm-about-dialog__btn--close:focus {
4844
4844
  .twm-window-body { flex: 1 1 auto; min-height: 0; min-width: 0; }
4845
4845
 
4846
4846
 
4847
+ /* ══ C38. THE TABS ARE THE TITLE ═════════════════════════════════════
4848
+ *
4849
+ * `tabLayout: 'header'` mounts the same strip INSIDE the chrome, in place of
4850
+ * the title, on every content tile (`tile_renderer.js`, C38). The renderer
4851
+ * marks such a leaf `twm-leaf--tabs-in-header`; a panel, which has no tabs,
4852
+ * is not marked and keeps its title.
4853
+ *
4854
+ * Every selector here is two classes or more, so none of it depends on
4855
+ * whether this sheet or the one holding `.twm-leaf__chrome` loads first.
4856
+ *
4857
+ * The chrome grows to the editor strip's 26px rather than the strip shrinking
4858
+ * to 22: one 26px row replaces a 22px row plus a 26px one. */
4859
+ .twm-leaf--tabs-in-header > .twm-leaf__chrome {
4860
+ height: 26px;
4861
+ padding-left: 0;
4862
+ gap: var(--space-sm);
4863
+ }
4864
+ .twm-leaf--tabs-in-header > .twm-leaf__chrome > .twm-leaf__title,
4865
+ .twm-leaf--tabs-in-header > .twm-leaf__chrome > .twm-leaf__icon.material-symbols-outlined {
4866
+ display: none;
4867
+ }
4868
+
4869
+ /* The strip takes the room the title had, and the actions keep the right. */
4870
+ .twm-leaf__tabbar.twm-leaf__tabbar--header {
4871
+ flex: 1 1 auto;
4872
+ align-self: stretch;
4873
+ min-width: 0;
4874
+ height: auto;
4875
+ padding: 0;
4876
+ border: none;
4877
+ background: transparent;
4878
+ overflow: hidden;
4879
+ }
4880
+
4881
+ /* `0 1 auto`, not the top strip's `1 1 auto`: the list is only as wide as
4882
+ * its tabs, so the `+` sits right after the last one rather than at the far
4883
+ * end of the chrome. It still shrinks, and scrolls, when the tabs outgrow it. */
4884
+ .twm-leaf__tabbar--header > .tabs.notebook-tabs {
4885
+ flex: 0 1 auto;
4886
+ min-width: 0;
4887
+ background: transparent;
4888
+ }
4889
+
4890
+ /* An inactive tab is the chrome's colour, focused or not, so the active one —
4891
+ * the body's colour — is the only box in the row. */
4892
+ .twm-leaf__tabbar--header .notebook-tabs > .tab:not(.active) { background: transparent; }
4893
+ .twm-leaf__tabbar--header .notebook-tabs > .tab:not(.active):hover {
4894
+ background: var(--state-hover);
4895
+ color: var(--text-primary);
4896
+ }
4897
+
4898
+ .twm-leaf__tabbar--header > .twm-leaf__tab-new {
4899
+ flex: 0 0 auto;
4900
+ display: inline-flex;
4901
+ align-items: center;
4902
+ justify-content: center;
4903
+ width: 24px;
4904
+ padding: 0;
4905
+ background: transparent;
4906
+ border: none;
4907
+ border-radius: 0;
4908
+ color: var(--text-muted);
4909
+ cursor: pointer;
4910
+ }
4911
+ .twm-leaf__tabbar--header > .twm-leaf__tab-new:hover {
4912
+ background: var(--state-hover);
4913
+ color: var(--text-primary);
4914
+ }
4915
+ /* Two classes, for the Material Symbols tie — see `.twm-leaf__icon`. */
4916
+ .twm-leaf__tab-new .material-symbols-outlined {
4917
+ font-size: 16px;
4918
+ line-height: 1;
4919
+ }
4920
+
4921
+
4847
4922
  /* ══ NotebookTabBar ══════════════════════════════════════════════════
4848
4923
  *
4849
4924
  * A CLASS ONE SIDE KNOWS AND THE OTHER HAD NEVER HEARD OF — the fifth in this
package/css/flexdesk.css CHANGED
@@ -5971,6 +5971,81 @@ body.twm-dragging * { user-select: none; }
5971
5971
  * strip is pushed off the bottom edge. */
5972
5972
  .twm-window-body { flex: 1 1 auto; min-height: 0; min-width: 0; }
5973
5973
 
5974
+
5975
+ /* ══ C38. THE TABS ARE THE TITLE ═════════════════════════════════════
5976
+ *
5977
+ * `tabLayout: 'header'` mounts the same strip INSIDE the chrome, in place of
5978
+ * the title, on every content tile (`tile_renderer.js`, C38). The renderer
5979
+ * marks such a leaf `twm-leaf--tabs-in-header`; a panel, which has no tabs,
5980
+ * is not marked and keeps its title.
5981
+ *
5982
+ * Every selector here is two classes or more, so none of it depends on
5983
+ * whether this sheet or the one holding `.twm-leaf__chrome` loads first.
5984
+ *
5985
+ * The chrome grows to the editor strip's 26px rather than the strip shrinking
5986
+ * to 22: one 26px row replaces a 22px row plus a 26px one. */
5987
+ .twm-leaf--tabs-in-header > .twm-leaf__chrome {
5988
+ height: 26px;
5989
+ padding-left: 0;
5990
+ gap: var(--space-sm);
5991
+ }
5992
+ .twm-leaf--tabs-in-header > .twm-leaf__chrome > .twm-leaf__title,
5993
+ .twm-leaf--tabs-in-header > .twm-leaf__chrome > .twm-leaf__icon.material-symbols-outlined {
5994
+ display: none;
5995
+ }
5996
+
5997
+ /* The strip takes the room the title had, and the actions keep the right. */
5998
+ .twm-leaf__tabbar.twm-leaf__tabbar--header {
5999
+ flex: 1 1 auto;
6000
+ align-self: stretch;
6001
+ min-width: 0;
6002
+ height: auto;
6003
+ padding: 0;
6004
+ border: none;
6005
+ background: transparent;
6006
+ overflow: hidden;
6007
+ }
6008
+
6009
+ /* `0 1 auto`, not the top strip's `1 1 auto`: the list is only as wide as
6010
+ * its tabs, so the `+` sits right after the last one rather than at the far
6011
+ * end of the chrome. It still shrinks, and scrolls, when the tabs outgrow it. */
6012
+ .twm-leaf__tabbar--header > .tabs.notebook-tabs {
6013
+ flex: 0 1 auto;
6014
+ min-width: 0;
6015
+ background: transparent;
6016
+ }
6017
+
6018
+ /* An inactive tab is the chrome's colour, focused or not, so the active one —
6019
+ * the body's colour — is the only box in the row. */
6020
+ .twm-leaf__tabbar--header .notebook-tabs > .tab:not(.active) { background: transparent; }
6021
+ .twm-leaf__tabbar--header .notebook-tabs > .tab:not(.active):hover {
6022
+ background: var(--state-hover);
6023
+ color: var(--text-primary);
6024
+ }
6025
+
6026
+ .twm-leaf__tabbar--header > .twm-leaf__tab-new {
6027
+ flex: 0 0 auto;
6028
+ display: inline-flex;
6029
+ align-items: center;
6030
+ justify-content: center;
6031
+ width: 24px;
6032
+ padding: 0;
6033
+ background: transparent;
6034
+ border: none;
6035
+ border-radius: 0;
6036
+ color: var(--text-muted);
6037
+ cursor: pointer;
6038
+ }
6039
+ .twm-leaf__tabbar--header > .twm-leaf__tab-new:hover {
6040
+ background: var(--state-hover);
6041
+ color: var(--text-primary);
6042
+ }
6043
+ /* Two classes, for the Material Symbols tie — see `.twm-leaf__icon`. */
6044
+ .twm-leaf__tab-new .material-symbols-outlined {
6045
+ font-size: 16px;
6046
+ line-height: 1;
6047
+ }
6048
+
5974
6049
  /* ══ NotebookTabBar ══════════════════════════════════════════════════
5975
6050
  *
5976
6051
  * A CLASS ONE SIDE KNOWS AND THE OTHER HAD NEVER HEARD OF — the fifth in this
package/dist/flexdesk.css CHANGED
@@ -5971,6 +5971,81 @@ body.twm-dragging * { user-select: none; }
5971
5971
  * strip is pushed off the bottom edge. */
5972
5972
  .twm-window-body { flex: 1 1 auto; min-height: 0; min-width: 0; }
5973
5973
 
5974
+
5975
+ /* ══ C38. THE TABS ARE THE TITLE ═════════════════════════════════════
5976
+ *
5977
+ * `tabLayout: 'header'` mounts the same strip INSIDE the chrome, in place of
5978
+ * the title, on every content tile (`tile_renderer.js`, C38). The renderer
5979
+ * marks such a leaf `twm-leaf--tabs-in-header`; a panel, which has no tabs,
5980
+ * is not marked and keeps its title.
5981
+ *
5982
+ * Every selector here is two classes or more, so none of it depends on
5983
+ * whether this sheet or the one holding `.twm-leaf__chrome` loads first.
5984
+ *
5985
+ * The chrome grows to the editor strip's 26px rather than the strip shrinking
5986
+ * to 22: one 26px row replaces a 22px row plus a 26px one. */
5987
+ .twm-leaf--tabs-in-header > .twm-leaf__chrome {
5988
+ height: 26px;
5989
+ padding-left: 0;
5990
+ gap: var(--space-sm);
5991
+ }
5992
+ .twm-leaf--tabs-in-header > .twm-leaf__chrome > .twm-leaf__title,
5993
+ .twm-leaf--tabs-in-header > .twm-leaf__chrome > .twm-leaf__icon.material-symbols-outlined {
5994
+ display: none;
5995
+ }
5996
+
5997
+ /* The strip takes the room the title had, and the actions keep the right. */
5998
+ .twm-leaf__tabbar.twm-leaf__tabbar--header {
5999
+ flex: 1 1 auto;
6000
+ align-self: stretch;
6001
+ min-width: 0;
6002
+ height: auto;
6003
+ padding: 0;
6004
+ border: none;
6005
+ background: transparent;
6006
+ overflow: hidden;
6007
+ }
6008
+
6009
+ /* `0 1 auto`, not the top strip's `1 1 auto`: the list is only as wide as
6010
+ * its tabs, so the `+` sits right after the last one rather than at the far
6011
+ * end of the chrome. It still shrinks, and scrolls, when the tabs outgrow it. */
6012
+ .twm-leaf__tabbar--header > .tabs.notebook-tabs {
6013
+ flex: 0 1 auto;
6014
+ min-width: 0;
6015
+ background: transparent;
6016
+ }
6017
+
6018
+ /* An inactive tab is the chrome's colour, focused or not, so the active one —
6019
+ * the body's colour — is the only box in the row. */
6020
+ .twm-leaf__tabbar--header .notebook-tabs > .tab:not(.active) { background: transparent; }
6021
+ .twm-leaf__tabbar--header .notebook-tabs > .tab:not(.active):hover {
6022
+ background: var(--state-hover);
6023
+ color: var(--text-primary);
6024
+ }
6025
+
6026
+ .twm-leaf__tabbar--header > .twm-leaf__tab-new {
6027
+ flex: 0 0 auto;
6028
+ display: inline-flex;
6029
+ align-items: center;
6030
+ justify-content: center;
6031
+ width: 24px;
6032
+ padding: 0;
6033
+ background: transparent;
6034
+ border: none;
6035
+ border-radius: 0;
6036
+ color: var(--text-muted);
6037
+ cursor: pointer;
6038
+ }
6039
+ .twm-leaf__tabbar--header > .twm-leaf__tab-new:hover {
6040
+ background: var(--state-hover);
6041
+ color: var(--text-primary);
6042
+ }
6043
+ /* Two classes, for the Material Symbols tie — see `.twm-leaf__icon`. */
6044
+ .twm-leaf__tab-new .material-symbols-outlined {
6045
+ font-size: 16px;
6046
+ line-height: 1;
6047
+ }
6048
+
5974
6049
  /* ══ NotebookTabBar ══════════════════════════════════════════════════
5975
6050
  *
5976
6051
  * A CLASS ONE SIDE KNOWS AND THE OTHER HAD NEVER HEARD OF — the fifth in this
package/dist/wm.js CHANGED
@@ -2689,7 +2689,7 @@ function createPageFactories({ eventBus = null, events = {}, refreshOn = {} } =
2689
2689
 
2690
2690
  // src/tiling/tile_renderer.js
2691
2691
  var SPLITTER_PX = 4;
2692
- var TAB_LAYOUTS = ["bottom", "top"];
2692
+ var TAB_LAYOUTS = ["bottom", "top", "header"];
2693
2693
  var DEFAULT_TAB_LAYOUT = "bottom";
2694
2694
  function _normalizeTabLayout(value) {
2695
2695
  return TAB_LAYOUTS.includes(value) ? value : DEFAULT_TAB_LAYOUT;
@@ -2702,7 +2702,7 @@ function _tabKeyIndex(key) {
2702
2702
  return Number.isInteger(n) && n >= 0 ? n : -1;
2703
2703
  }
2704
2704
  var TILE_LIFT_PX = 24;
2705
- var CHROME_NO_FLOAT = "button, .twm-leaf__tab";
2705
+ var CHROME_NO_FLOAT = "button, .twm-leaf__tab, .notebook-tabs .tab";
2706
2706
  function _isDownwardPull(down, sideways) {
2707
2707
  return down > 0 && sideways <= down;
2708
2708
  }
@@ -2877,6 +2877,7 @@ var TileRenderer = class {
2877
2877
  if (entry && entry.kindKey === kindKey) {
2878
2878
  entry.titleEl.textContent = leaf.title || (leaf.content ? leaf.content.kind : "empty");
2879
2879
  _paintLeafIcon(entry.iconEl, leaf, entry.content, this.ctx);
2880
+ if (this.tabLayout === "header") this._refreshHeaderLabels(leaf, entry);
2880
2881
  return entry.wrapEl;
2881
2882
  }
2882
2883
  if (entry) {
@@ -2928,7 +2929,14 @@ var TileRenderer = class {
2928
2929
  if (!Array.isArray(leaf.tabs) || leaf.tabs.length <= 1) {
2929
2930
  tabBar.classList.add("twm-leaf__tabbar--hidden");
2930
2931
  }
2931
- wrap.append(chrome, ...this.tabLayout === "top" ? [tabBar, body] : [body, tabBar]);
2932
+ wrap.append(chrome, body, tabBar);
2933
+ this._placeTabBar({
2934
+ wrapEl: wrap,
2935
+ bodyEl: body,
2936
+ chromeEl: chrome,
2937
+ titleEl: title,
2938
+ tabBarEl: tabBar
2939
+ });
2932
2940
  wrap.addEventListener("mousedown", (e) => {
2933
2941
  if (e.target.closest(".twm-splitter")) return;
2934
2942
  this.tree.focus(leaf.id);
@@ -3148,9 +3156,10 @@ var TileRenderer = class {
3148
3156
  * attached element is a re-parent, not a rebuild — the body keeps its
3149
3157
  * DOM, its listeners and its scroll. */
3150
3158
  _placeTabBar(entry) {
3151
- const { wrapEl, bodyEl, tabBarEl } = entry;
3159
+ const { wrapEl, bodyEl, tabBarEl, chromeEl, titleEl } = entry;
3152
3160
  if (!wrapEl || !tabBarEl || !bodyEl) return;
3153
- if (this.tabLayout === "top") wrapEl.insertBefore(tabBarEl, bodyEl);
3161
+ if (this.tabLayout === "header" && chromeEl && titleEl) titleEl.after(tabBarEl);
3162
+ else if (this.tabLayout === "top") wrapEl.insertBefore(tabBarEl, bodyEl);
3154
3163
  else wrapEl.appendChild(tabBarEl);
3155
3164
  }
3156
3165
  /** Tear down the `top` layout's component, if this leaf has one. Safe to
@@ -3173,16 +3182,42 @@ var TileRenderer = class {
3173
3182
  const bar = entry.tabBarEl;
3174
3183
  if (!bar) return;
3175
3184
  const tabs = Array.isArray(leaf.tabs) ? leaf.tabs : [];
3185
+ const header = this.tabLayout === "header";
3176
3186
  bar.classList.toggle("twm-leaf__tabbar--top", this.tabLayout === "top");
3177
- if (tabs.length <= 1) {
3187
+ bar.classList.toggle("twm-leaf__tabbar--header", header);
3188
+ const isPanel = String(leaf.content?.kind || "").startsWith("panel:");
3189
+ const inHeader = header && !isPanel && !!leaf.content && tabs.length >= 1;
3190
+ entry.wrapEl?.classList.toggle("twm-leaf--tabs-in-header", inHeader);
3191
+ if (!inHeader && tabs.length <= 1) {
3178
3192
  bar.classList.add("twm-leaf__tabbar--hidden");
3179
3193
  this._disposeTabStrip(entry);
3180
3194
  bar.innerHTML = "";
3181
3195
  return;
3182
3196
  }
3183
3197
  bar.classList.remove("twm-leaf__tabbar--hidden");
3184
- if (this.tabLayout === "top") this._renderTopTabBar(leaf, entry, tabs);
3185
- else this._renderBottomTabBar(leaf, entry, tabs);
3198
+ if (this.tabLayout === "bottom") this._renderBottomTabBar(leaf, entry, tabs);
3199
+ else this._renderTopTabBar(leaf, entry, tabs);
3200
+ }
3201
+ /** C38. The labels the `header` strip shows for `leaf`, in one string, so
3202
+ * a repaint can tell whether anything it draws has changed. */
3203
+ _headerLabels(leaf, entry) {
3204
+ const tabs = Array.isArray(leaf.tabs) ? leaf.tabs : [];
3205
+ const activeIdx = Math.max(0, Math.min(tabs.length - 1, leaf.activeTabIdx || 0));
3206
+ return tabs.map((t, i) => this._tabLabel(t, i === activeIdx, entry)).join("\n");
3207
+ }
3208
+ /** C38. A cached leaf is not re-mounted, so a tab renamed in the tree
3209
+ * (`TileTree.renameMatchingTabs`) reaches the strip only through here. */
3210
+ _refreshHeaderLabels(leaf, entry) {
3211
+ const labels = this._headerLabels(leaf, entry);
3212
+ if (entry.headerLabels === labels) return;
3213
+ this._renderTabBar(leaf, entry);
3214
+ }
3215
+ /** What a tab in the `top` or `header` strip is called. A tab with no
3216
+ * title of its own falls back, when it is the active one, to the title
3217
+ * its content gave at mount — the string the chrome title would have
3218
+ * shown, and under `header` the chrome title is hidden (C38). */
3219
+ _tabLabel(t, active, entry) {
3220
+ return t.title || active && entry?.content?.title || t.kind || "";
3186
3221
  }
3187
3222
  /**
3188
3223
  * C33. THE CHROME IS THE DRAG SOURCE FOR A LEAF THAT HAS ONE TAB.
@@ -3218,12 +3253,13 @@ var TileRenderer = class {
3218
3253
  const tabs = Array.isArray(leaf.tabs) ? leaf.tabs : [];
3219
3254
  const isPanel = String(leaf.content?.kind || "").startsWith("panel:");
3220
3255
  const vetoed = this.leafChrome(leaf.id)?.promote === false;
3221
- const on = tabs.length === 1 && !isPanel && !vetoed;
3256
+ const on = tabs.length === 1 && !isPanel && !vetoed && this.tabLayout !== "header";
3222
3257
  if (on) chrome.setAttribute("draggable", "true");
3223
3258
  else chrome.removeAttribute("draggable");
3224
3259
  if (chrome.__twmTabDragBound) return;
3225
3260
  chrome.__twmTabDragBound = true;
3226
3261
  chrome.addEventListener("dragstart", (ev) => {
3262
+ if (entry.tabBarEl?.contains(ev.target)) return;
3227
3263
  if (ev.target?.closest?.(CHROME_NO_FLOAT)) {
3228
3264
  ev.preventDefault();
3229
3265
  return;
@@ -3282,11 +3318,27 @@ var TileRenderer = class {
3282
3318
  );
3283
3319
  }, true);
3284
3320
  }
3321
+ if (this.tabLayout === "header" && !bar.querySelector(".twm-leaf__tab-new")) {
3322
+ const add = document.createElement("button");
3323
+ add.type = "button";
3324
+ add.className = "twm-leaf__tab-new";
3325
+ add.dataset.action = "tab-new";
3326
+ add.title = "New tab";
3327
+ add.setAttribute("aria-label", "New tab");
3328
+ add.innerHTML = '<span class="material-symbols-outlined">add</span>';
3329
+ add.addEventListener("click", (ev) => {
3330
+ ev.preventDefault();
3331
+ ev.stopPropagation();
3332
+ this.ctx.onLeafTabAction?.(leaf.id, "new", {});
3333
+ });
3334
+ bar.appendChild(add);
3335
+ }
3285
3336
  const activeIdx = Math.max(0, Math.min(tabs.length - 1, leaf.activeTabIdx || 0));
3337
+ entry.headerLabels = this._headerLabels(leaf, entry);
3286
3338
  entry.tabStrip.update(
3287
3339
  tabs.map((t, i) => ({
3288
3340
  filePath: _tabKey(i),
3289
- label: t.title || t.kind || "",
3341
+ label: this._tabLabel(t, i === activeIdx, entry),
3290
3342
  // The component's `fileType` picks a glyph out of a static map
3291
3343
  // of FILE kinds. A tile's kinds are the embedder's, and the
3292
3344
  // taxonomy already answers for them — see `_paintTopTabs`.
@@ -3299,7 +3351,7 @@ var TileRenderer = class {
3299
3351
  })),
3300
3352
  _tabKey(activeIdx)
3301
3353
  );
3302
- this._paintTopTabs(entry.tabStripHostEl, tabs);
3354
+ this._paintTopTabs(entry.tabStripHostEl, tabs, activeIdx, entry);
3303
3355
  }
3304
3356
  /**
3305
3357
  * The two things `NotebookTabBar` derives from a vocabulary a tile does not
@@ -3318,13 +3370,13 @@ var TileRenderer = class {
3318
3370
  * rendered last. The class names used here are the component's published
3319
3371
  * contract, stated in its header comment.
3320
3372
  */
3321
- _paintTopTabs(hostEl, tabs) {
3373
+ _paintTopTabs(hostEl, tabs, activeIdx = -1, entry = null) {
3322
3374
  if (!hostEl) return;
3323
3375
  const els = hostEl.querySelectorAll(".tab");
3324
3376
  els.forEach((el, i) => {
3325
3377
  const spec = tabs[i];
3326
3378
  if (!spec) return;
3327
- el.title = spec.title || spec.kind || "";
3379
+ el.title = this._tabLabel(spec, i === activeIdx, entry);
3328
3380
  const icon = spec.kind ? this.ctx?.taxonomy?.meta?.(spec.kind)?.icon : null;
3329
3381
  const glyph = el.querySelector(".tab-icon");
3330
3382
  if (glyph && icon) glyph.textContent = icon;
@@ -4089,6 +4141,23 @@ var WindowManager = class _WindowManager {
4089
4141
  setTabLayout(layout) {
4090
4142
  this.renderer.setTabLayout(layout);
4091
4143
  }
4144
+ /** C38. Open a new tab in a tile and show it. It holds the taxonomy root —
4145
+ * what a split seeds its new pane with, so "a new tab" and "a new pane"
4146
+ * start in the same place. Defaults to the focused tile. A panel tile
4147
+ * grows no tabs (`TileTree.appendLeafTab`); then this does nothing and
4148
+ * answers -1, else the new tab's index. */
4149
+ newTab(leafId = null) {
4150
+ const tree = this._tree();
4151
+ const id = leafId ?? tree.focusedLeafId;
4152
+ const seed = this._rootLeaf();
4153
+ const idx = tree.appendLeafTab(id, seed.content, seed.title);
4154
+ if (idx < 0) return -1;
4155
+ tree.focus(id);
4156
+ this.renderer.render();
4157
+ this._persist();
4158
+ this._notifyChange("tab-new");
4159
+ return idx;
4160
+ }
4092
4161
  /** Emit on bus + call onChange. Use this instead of the bare callback
4093
4162
  * so other surfaces (palette, top-bar toggles, page shortcuts) can
4094
4163
  * subscribe through the existing event system. */
@@ -6706,6 +6775,10 @@ var WindowManager = class _WindowManager {
6706
6775
  this._notifyChange("twm-tab-close");
6707
6776
  return;
6708
6777
  }
6778
+ if (action === "new") {
6779
+ this.newTab(leafId);
6780
+ return;
6781
+ }
6709
6782
  if (action === "move") {
6710
6783
  tree.moveLeafTab(leafId, data.from, data.to);
6711
6784
  this.renderer.render();