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/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,
|
|
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 === "
|
|
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
|
-
|
|
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 === "
|
|
3185
|
-
else this.
|
|
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
|
|
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
|
|
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
|