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 CHANGED
@@ -121,6 +121,8 @@ 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)
125
+ newTabButton: false, // …and without the + (0.8.1)
124
126
  chrome: {
125
127
  topNav: document.getElementById('nav'),
126
128
  zoom: document.getElementById('status-bar'), // content zoom, 50–200%
@@ -151,6 +153,19 @@ views of one pane. The float button, Alt+F and the tile menu take only the tab
151
153
  on screen and leave its siblings in the tile, so the window holds one tab and
152
154
  draws no strip. Without it the whole pane floats, strip and all.
153
155
 
156
+ **`tabLayout`** says where a tile draws its tabs. `'bottom'` (the default) is
157
+ the spreadsheet strip under the body, shown from two tabs. `'top'` is the
158
+ editor tab bar between the header and the body, also from two tabs.
159
+ `'header'` (0.8.0) puts that tab bar inside the header in place of the title,
160
+ on every tile, one tab included, with a `+` after the last tab that opens the
161
+ taxonomy root in a new tab (`wm.newTab(leafId)`, the same seed a split uses).
162
+ Pass `newTabButton: false` (0.8.1) to leave the `+` out.
163
+ A panel tile has no tabs and keeps its title. Under `'header'` the tab, not
164
+ the header, is what you drag to move a single-tab pane; the empty part of the
165
+ header still floats the pane on a pull-down or a double-click. Set
166
+ `data-twm-tabs` on the shell's root, or call `wm.setTabLayout()`, to switch
167
+ live; nothing mounted in a tile is unmounted.
168
+
154
169
  **Back and the breadcrumb are one rule.** `wm.navigateBack()` (Backspace) and
155
170
  `wm.navigateUp(kind, props, { ctx })` (every breadcrumb crumb) share it. With
156
171
  `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
  }
@@ -2728,7 +2728,8 @@ var TileRenderer = class {
2728
2728
  ctx,
2729
2729
  onFocusChange,
2730
2730
  onAfterRender,
2731
- tabLayout = null
2731
+ tabLayout = null,
2732
+ newTabButton = true
2732
2733
  }) {
2733
2734
  if (!content || typeof content.mount !== "function") {
2734
2735
  throw new Error("TileRenderer: a content registry is required");
@@ -2741,6 +2742,7 @@ var TileRenderer = class {
2741
2742
  });
2742
2743
  this.onAfterRender = onAfterRender || (() => {
2743
2744
  });
2745
+ this.newTabButton = newTabButton !== false;
2744
2746
  this._leafCache = /* @__PURE__ */ new Map();
2745
2747
  this._drag = null;
2746
2748
  this._tabDrag = null;
@@ -2877,6 +2879,7 @@ var TileRenderer = class {
2877
2879
  if (entry && entry.kindKey === kindKey) {
2878
2880
  entry.titleEl.textContent = leaf.title || (leaf.content ? leaf.content.kind : "empty");
2879
2881
  _paintLeafIcon(entry.iconEl, leaf, entry.content, this.ctx);
2882
+ if (this.tabLayout === "header") this._refreshHeaderLabels(leaf, entry);
2880
2883
  return entry.wrapEl;
2881
2884
  }
2882
2885
  if (entry) {
@@ -2928,7 +2931,14 @@ var TileRenderer = class {
2928
2931
  if (!Array.isArray(leaf.tabs) || leaf.tabs.length <= 1) {
2929
2932
  tabBar.classList.add("twm-leaf__tabbar--hidden");
2930
2933
  }
2931
- wrap.append(chrome, ...this.tabLayout === "top" ? [tabBar, body] : [body, tabBar]);
2934
+ wrap.append(chrome, body, tabBar);
2935
+ this._placeTabBar({
2936
+ wrapEl: wrap,
2937
+ bodyEl: body,
2938
+ chromeEl: chrome,
2939
+ titleEl: title,
2940
+ tabBarEl: tabBar
2941
+ });
2932
2942
  wrap.addEventListener("mousedown", (e) => {
2933
2943
  if (e.target.closest(".twm-splitter")) return;
2934
2944
  this.tree.focus(leaf.id);
@@ -3148,9 +3158,10 @@ var TileRenderer = class {
3148
3158
  * attached element is a re-parent, not a rebuild — the body keeps its
3149
3159
  * DOM, its listeners and its scroll. */
3150
3160
  _placeTabBar(entry) {
3151
- const { wrapEl, bodyEl, tabBarEl } = entry;
3161
+ const { wrapEl, bodyEl, tabBarEl, chromeEl, titleEl } = entry;
3152
3162
  if (!wrapEl || !tabBarEl || !bodyEl) return;
3153
- if (this.tabLayout === "top") wrapEl.insertBefore(tabBarEl, bodyEl);
3163
+ if (this.tabLayout === "header" && chromeEl && titleEl) titleEl.after(tabBarEl);
3164
+ else if (this.tabLayout === "top") wrapEl.insertBefore(tabBarEl, bodyEl);
3154
3165
  else wrapEl.appendChild(tabBarEl);
3155
3166
  }
3156
3167
  /** Tear down the `top` layout's component, if this leaf has one. Safe to
@@ -3173,16 +3184,42 @@ var TileRenderer = class {
3173
3184
  const bar = entry.tabBarEl;
3174
3185
  if (!bar) return;
3175
3186
  const tabs = Array.isArray(leaf.tabs) ? leaf.tabs : [];
3187
+ const header = this.tabLayout === "header";
3176
3188
  bar.classList.toggle("twm-leaf__tabbar--top", this.tabLayout === "top");
3177
- if (tabs.length <= 1) {
3189
+ bar.classList.toggle("twm-leaf__tabbar--header", header);
3190
+ const isPanel = String(leaf.content?.kind || "").startsWith("panel:");
3191
+ const inHeader = header && !isPanel && !!leaf.content && tabs.length >= 1;
3192
+ entry.wrapEl?.classList.toggle("twm-leaf--tabs-in-header", inHeader);
3193
+ if (!inHeader && tabs.length <= 1) {
3178
3194
  bar.classList.add("twm-leaf__tabbar--hidden");
3179
3195
  this._disposeTabStrip(entry);
3180
3196
  bar.innerHTML = "";
3181
3197
  return;
3182
3198
  }
3183
3199
  bar.classList.remove("twm-leaf__tabbar--hidden");
3184
- if (this.tabLayout === "top") this._renderTopTabBar(leaf, entry, tabs);
3185
- else this._renderBottomTabBar(leaf, entry, tabs);
3200
+ if (this.tabLayout === "bottom") this._renderBottomTabBar(leaf, entry, tabs);
3201
+ else this._renderTopTabBar(leaf, entry, tabs);
3202
+ }
3203
+ /** C38. The labels the `header` strip shows for `leaf`, in one string, so
3204
+ * a repaint can tell whether anything it draws has changed. */
3205
+ _headerLabels(leaf, entry) {
3206
+ const tabs = Array.isArray(leaf.tabs) ? leaf.tabs : [];
3207
+ const activeIdx = Math.max(0, Math.min(tabs.length - 1, leaf.activeTabIdx || 0));
3208
+ return tabs.map((t, i) => this._tabLabel(t, i === activeIdx, entry)).join("\n");
3209
+ }
3210
+ /** C38. A cached leaf is not re-mounted, so a tab renamed in the tree
3211
+ * (`TileTree.renameMatchingTabs`) reaches the strip only through here. */
3212
+ _refreshHeaderLabels(leaf, entry) {
3213
+ const labels = this._headerLabels(leaf, entry);
3214
+ if (entry.headerLabels === labels) return;
3215
+ this._renderTabBar(leaf, entry);
3216
+ }
3217
+ /** What a tab in the `top` or `header` strip is called. A tab with no
3218
+ * title of its own falls back, when it is the active one, to the title
3219
+ * its content gave at mount — the string the chrome title would have
3220
+ * shown, and under `header` the chrome title is hidden (C38). */
3221
+ _tabLabel(t, active, entry) {
3222
+ return t.title || active && entry?.content?.title || t.kind || "";
3186
3223
  }
3187
3224
  /**
3188
3225
  * C33. THE CHROME IS THE DRAG SOURCE FOR A LEAF THAT HAS ONE TAB.
@@ -3218,12 +3255,13 @@ var TileRenderer = class {
3218
3255
  const tabs = Array.isArray(leaf.tabs) ? leaf.tabs : [];
3219
3256
  const isPanel = String(leaf.content?.kind || "").startsWith("panel:");
3220
3257
  const vetoed = this.leafChrome(leaf.id)?.promote === false;
3221
- const on = tabs.length === 1 && !isPanel && !vetoed;
3258
+ const on = tabs.length === 1 && !isPanel && !vetoed && this.tabLayout !== "header";
3222
3259
  if (on) chrome.setAttribute("draggable", "true");
3223
3260
  else chrome.removeAttribute("draggable");
3224
3261
  if (chrome.__twmTabDragBound) return;
3225
3262
  chrome.__twmTabDragBound = true;
3226
3263
  chrome.addEventListener("dragstart", (ev) => {
3264
+ if (entry.tabBarEl?.contains(ev.target)) return;
3227
3265
  if (ev.target?.closest?.(CHROME_NO_FLOAT)) {
3228
3266
  ev.preventDefault();
3229
3267
  return;
@@ -3282,11 +3320,27 @@ var TileRenderer = class {
3282
3320
  );
3283
3321
  }, true);
3284
3322
  }
3323
+ if (this.tabLayout === "header" && this.newTabButton && !bar.querySelector(".twm-leaf__tab-new")) {
3324
+ const add = document.createElement("button");
3325
+ add.type = "button";
3326
+ add.className = "twm-leaf__tab-new";
3327
+ add.dataset.action = "tab-new";
3328
+ add.title = "New tab";
3329
+ add.setAttribute("aria-label", "New tab");
3330
+ add.innerHTML = '<span class="material-symbols-outlined">add</span>';
3331
+ add.addEventListener("click", (ev) => {
3332
+ ev.preventDefault();
3333
+ ev.stopPropagation();
3334
+ this.ctx.onLeafTabAction?.(leaf.id, "new", {});
3335
+ });
3336
+ bar.appendChild(add);
3337
+ }
3285
3338
  const activeIdx = Math.max(0, Math.min(tabs.length - 1, leaf.activeTabIdx || 0));
3339
+ entry.headerLabels = this._headerLabels(leaf, entry);
3286
3340
  entry.tabStrip.update(
3287
3341
  tabs.map((t, i) => ({
3288
3342
  filePath: _tabKey(i),
3289
- label: t.title || t.kind || "",
3343
+ label: this._tabLabel(t, i === activeIdx, entry),
3290
3344
  // The component's `fileType` picks a glyph out of a static map
3291
3345
  // of FILE kinds. A tile's kinds are the embedder's, and the
3292
3346
  // taxonomy already answers for them — see `_paintTopTabs`.
@@ -3299,7 +3353,7 @@ var TileRenderer = class {
3299
3353
  })),
3300
3354
  _tabKey(activeIdx)
3301
3355
  );
3302
- this._paintTopTabs(entry.tabStripHostEl, tabs);
3356
+ this._paintTopTabs(entry.tabStripHostEl, tabs, activeIdx, entry);
3303
3357
  }
3304
3358
  /**
3305
3359
  * The two things `NotebookTabBar` derives from a vocabulary a tile does not
@@ -3318,13 +3372,13 @@ var TileRenderer = class {
3318
3372
  * rendered last. The class names used here are the component's published
3319
3373
  * contract, stated in its header comment.
3320
3374
  */
3321
- _paintTopTabs(hostEl, tabs) {
3375
+ _paintTopTabs(hostEl, tabs, activeIdx = -1, entry = null) {
3322
3376
  if (!hostEl) return;
3323
3377
  const els = hostEl.querySelectorAll(".tab");
3324
3378
  els.forEach((el, i) => {
3325
3379
  const spec = tabs[i];
3326
3380
  if (!spec) return;
3327
- el.title = spec.title || spec.kind || "";
3381
+ el.title = this._tabLabel(spec, i === activeIdx, entry);
3328
3382
  const icon = spec.kind ? this.ctx?.taxonomy?.meta?.(spec.kind)?.icon : null;
3329
3383
  const glyph = el.querySelector(".tab-icon");
3330
3384
  if (glyph && icon) glyph.textContent = icon;
@@ -3999,7 +4053,8 @@ var WindowManager = class _WindowManager {
3999
4053
  promoteInPlace = false,
4000
4054
  tabLayout = null,
4001
4055
  backToOpenList = false,
4002
- floatActiveTab = false
4056
+ floatActiveTab = false,
4057
+ newTabButton = true
4003
4058
  }) {
4004
4059
  if (!taxonomy) throw new Error("WindowManager: a taxonomy is required");
4005
4060
  if (!content || typeof content.mount !== "function") {
@@ -4050,6 +4105,8 @@ var WindowManager = class _WindowManager {
4050
4105
  * `data-twm-tabs` and watches it, so an embedder can change it
4051
4106
  * live without holding a renderer reference. */
4052
4107
  tabLayout,
4108
+ /** C38. Whether the `header` strip draws its `+`. */
4109
+ newTabButton,
4053
4110
  ctx: {
4054
4111
  ...this.ctx,
4055
4112
  wm: this,
@@ -4089,6 +4146,23 @@ var WindowManager = class _WindowManager {
4089
4146
  setTabLayout(layout) {
4090
4147
  this.renderer.setTabLayout(layout);
4091
4148
  }
4149
+ /** C38. Open a new tab in a tile and show it. It holds the taxonomy root —
4150
+ * what a split seeds its new pane with, so "a new tab" and "a new pane"
4151
+ * start in the same place. Defaults to the focused tile. A panel tile
4152
+ * grows no tabs (`TileTree.appendLeafTab`); then this does nothing and
4153
+ * answers -1, else the new tab's index. */
4154
+ newTab(leafId = null) {
4155
+ const tree = this._tree();
4156
+ const id = leafId ?? tree.focusedLeafId;
4157
+ const seed = this._rootLeaf();
4158
+ const idx = tree.appendLeafTab(id, seed.content, seed.title);
4159
+ if (idx < 0) return -1;
4160
+ tree.focus(id);
4161
+ this.renderer.render();
4162
+ this._persist();
4163
+ this._notifyChange("tab-new");
4164
+ return idx;
4165
+ }
4092
4166
  /** Emit on bus + call onChange. Use this instead of the bare callback
4093
4167
  * so other surfaces (palette, top-bar toggles, page shortcuts) can
4094
4168
  * subscribe through the existing event system. */
@@ -6706,6 +6780,10 @@ var WindowManager = class _WindowManager {
6706
6780
  this._notifyChange("twm-tab-close");
6707
6781
  return;
6708
6782
  }
6783
+ if (action === "new") {
6784
+ this.newTab(leafId);
6785
+ return;
6786
+ }
6709
6787
  if (action === "move") {
6710
6788
  tree.moveLeafTab(leafId, data.from, data.to);
6711
6789
  this.renderer.render();
@@ -7618,6 +7696,7 @@ async function createShell({
7618
7696
  backToOpenList = false,
7619
7697
  floatActiveTab = false,
7620
7698
  tabLayout = null,
7699
+ newTabButton = true,
7621
7700
  chrome = {},
7622
7701
  // An embedder that moved its sections out of the top bar — into an icon
7623
7702
  // rail, say — passes the selector its own buttons match, and F1..F8 keep
@@ -7659,6 +7738,7 @@ async function createShell({
7659
7738
  backToOpenList,
7660
7739
  floatActiveTab,
7661
7740
  tabLayout,
7741
+ newTabButton,
7662
7742
  // `ctx` is the delivery vehicle for leaf-mounted chrome: tile_renderer
7663
7743
  // spreads it into every content factory, which is how the breadcrumb
7664
7744
  // gets `taxonomy` + `rootCrumb` without a content factory knowing they