flexdesk 0.6.3 → 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 +339 -0
- package/css/base.css +408 -0
- package/css/flexdesk.css +408 -0
- package/dist/charts.js +2 -2
- package/dist/{chunk-WLASPRGP.js → chunk-JLCJWCRS.js} +1577 -56
- package/dist/chunk-JLCJWCRS.js.map +7 -0
- package/dist/{chunk-WAVLVXYV.js → chunk-WKTYKXWF.js} +2 -2
- package/dist/flexdesk.css +408 -0
- package/dist/flow.js +24 -8
- package/dist/flow.js.map +2 -2
- package/dist/tiles.js +2 -2
- package/dist/widgets.js +672 -1
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +86 -13
- package/dist/wm.js.map +2 -2
- package/package.json +1 -1
- package/src/flow/outline/editor.js +46 -8
- package/src/tiling/shell.js +6 -2
- package/src/tiling/tile_renderer.js +116 -18
- package/src/tiling/wm.js +24 -0
- package/src/ui/components/data_table.js +1650 -66
- package/src/ui/components/fact_table.js +342 -0
- package/src/ui/components/record_list.js +594 -0
- package/src/ui/components/table_filter_chips.js +153 -0
- package/dist/chunk-WLASPRGP.js.map +0 -7
- /package/dist/{chunk-WAVLVXYV.js.map → chunk-WKTYKXWF.js.map} +0 -0
package/package.json
CHANGED
|
@@ -27,7 +27,9 @@
|
|
|
27
27
|
* ══ GESTURES ════════════════════════════════════════════════════════════
|
|
28
28
|
*
|
|
29
29
|
* A press SELECTS — it toggles classes and fills the settings panel, and
|
|
30
|
-
* rebuilds no row, so the click lands on the node the press went down on.
|
|
30
|
+
* rebuilds no row, so the click lands on the node the press went down on. In
|
|
31
|
+
* a narrow editor, where the panel is stacked under the list, a press (like
|
|
32
|
+
* Enter, Go to it and `select()`) scrolls the panel on screen when it is off it.
|
|
31
33
|
* "+" between rows (on hover AND on focus), the add rows and a row's menu
|
|
32
34
|
* open the step picker and the step menu, both the kit's popovers. A step is
|
|
33
35
|
* dragged by its grip with POINTER EVENTS and a ghost on `document.body`
|
|
@@ -528,7 +530,7 @@ export function createOutlineEditor(host, options = {}) {
|
|
|
528
530
|
if (fs.length) row.appendChild(badge(fs));
|
|
529
531
|
item.appendChild(row);
|
|
530
532
|
item.setAttribute('aria-label', [map.startLabel(S), sum].filter(Boolean).join(', '));
|
|
531
|
-
row.addEventListener('click', () => select('flow', { focus: true }));
|
|
533
|
+
row.addEventListener('click', () => select('flow', { focus: true, picked: true }));
|
|
532
534
|
if (filter) item.classList.add('twm-flow-outline__item--context');
|
|
533
535
|
itemEls.set('flow', item);
|
|
534
536
|
return item;
|
|
@@ -645,7 +647,7 @@ export function createOutlineEditor(host, options = {}) {
|
|
|
645
647
|
row.appendChild(menu);
|
|
646
648
|
row.addEventListener('contextmenu', (ev) => { ev.preventDefault(); select(item.id); openMenu(item.id, menu); });
|
|
647
649
|
}
|
|
648
|
-
row.addEventListener('click', () => select(item.id, { focus: true }));
|
|
650
|
+
row.addEventListener('click', () => select(item.id, { focus: true, picked: true }));
|
|
649
651
|
return { row, label: [name, type?.label !== name ? type?.label : null, summary].filter(Boolean).join(', ') };
|
|
650
652
|
}
|
|
651
653
|
|
|
@@ -836,7 +838,13 @@ export function createOutlineEditor(host, options = {}) {
|
|
|
836
838
|
}
|
|
837
839
|
}
|
|
838
840
|
|
|
839
|
-
|
|
841
|
+
/**
|
|
842
|
+
* `picked`: the step was PICKED — a press on its row, Go to it, `select()`
|
|
843
|
+
* — so its settings are what the person looks at next, and they are
|
|
844
|
+
* brought on screen (`revealSettings`). ↑ ↓ ← → only move the selection
|
|
845
|
+
* through the list and leave the list where it is.
|
|
846
|
+
*/
|
|
847
|
+
function select(id, { focus = false, silent = false, picked = false } = {}) {
|
|
840
848
|
const next = id === null || id === undefined ? 'flow' : id;
|
|
841
849
|
const changed = next !== selected;
|
|
842
850
|
selected = next;
|
|
@@ -844,9 +852,33 @@ export function createOutlineEditor(host, options = {}) {
|
|
|
844
852
|
if (changed) { showPanel(); drawAside(); }
|
|
845
853
|
void silent;
|
|
846
854
|
if (focus) itemEls.get(selected)?.focus({ preventScroll: false });
|
|
855
|
+
if (picked) revealSettings();
|
|
847
856
|
if (changed) onSelect?.({ kind: selected === 'flow' ? 'flow' : 'step', id: selected === 'flow' ? null : selected });
|
|
848
857
|
}
|
|
849
858
|
|
|
859
|
+
/**
|
|
860
|
+
* BRING THE SETTINGS ON SCREEN when the layout has put them off it — the
|
|
861
|
+
* one rule for Enter, a press on a row, Go to it and `select()`. Under
|
|
862
|
+
* 1000px the columns wrap and the body scrolls as one (the container query
|
|
863
|
+
* in the sheet), so the panel sits under the whole list: a step picked
|
|
864
|
+
* there filled a panel ~860px below with nothing scrolled, and the press
|
|
865
|
+
* looked like it did nothing. A panel wholly inside the body's visible box
|
|
866
|
+
* — always, side by side, where the panel IS the body's height — or one
|
|
867
|
+
* that covers it is left alone, so nothing moves, the page included;
|
|
868
|
+
* otherwise it is scrolled into view, 'nearest' (all of it when it fits,
|
|
869
|
+
* its top when it does not).
|
|
870
|
+
*/
|
|
871
|
+
function revealSettings() {
|
|
872
|
+
if (destroyed) return;
|
|
873
|
+
const box = body.getBoundingClientRect();
|
|
874
|
+
if (!box.height) return; // not laid out: hidden, detached
|
|
875
|
+
const r = settingsHost.getBoundingClientRect();
|
|
876
|
+
const inside = r.top >= box.top - 1 && r.bottom <= box.bottom + 1;
|
|
877
|
+
const covers = r.top <= box.top + 1 && r.bottom >= box.bottom - 1;
|
|
878
|
+
if (inside || covers) return;
|
|
879
|
+
settingsHost.scrollIntoView?.({ block: 'nearest' });
|
|
880
|
+
}
|
|
881
|
+
|
|
850
882
|
function focusSelected() {
|
|
851
883
|
const li = itemEls.get(selected) ?? itemEls.get('flow');
|
|
852
884
|
if (li) li.focus({ preventScroll: true });
|
|
@@ -1152,7 +1184,8 @@ export function createOutlineEditor(host, options = {}) {
|
|
|
1152
1184
|
}
|
|
1153
1185
|
|
|
1154
1186
|
function rename(id) {
|
|
1155
|
-
|
|
1187
|
+
// The title is focused without scrolling, so it is brought on screen first.
|
|
1188
|
+
select(id, { picked: true });
|
|
1156
1189
|
const input = settingsHost.querySelector('[data-twm-flow-title]');
|
|
1157
1190
|
if (input) { input.focus({ preventScroll: true }); input.select?.(); }
|
|
1158
1191
|
}
|
|
@@ -1168,7 +1201,7 @@ export function createOutlineEditor(host, options = {}) {
|
|
|
1168
1201
|
function goTo(f) {
|
|
1169
1202
|
const id = f?.node_id && nodeOf(f.node_id) ? f.node_id : 'flow';
|
|
1170
1203
|
if (reveal(id)) render();
|
|
1171
|
-
select(id, { focus: true });
|
|
1204
|
+
select(id, { focus: true, picked: true });
|
|
1172
1205
|
if (f?.field) panel.focusField(f.field);
|
|
1173
1206
|
}
|
|
1174
1207
|
|
|
@@ -1208,6 +1241,7 @@ export function createOutlineEditor(host, options = {}) {
|
|
|
1208
1241
|
const f = settingsHost.querySelector('.twm-flow-panel__fields input, .twm-flow-panel__fields select, '
|
|
1209
1242
|
+ '.twm-flow-panel__fields textarea, .twm-flow-panel__fields [contenteditable="true"], .twm-flow-panel__fields button')
|
|
1210
1243
|
|| settingsHost.querySelector('[data-twm-flow-title]');
|
|
1244
|
+
if (f) revealSettings();
|
|
1211
1245
|
f?.focus({ preventScroll: false });
|
|
1212
1246
|
return Boolean(f);
|
|
1213
1247
|
},
|
|
@@ -1477,11 +1511,15 @@ export function createOutlineEditor(host, options = {}) {
|
|
|
1477
1511
|
updateTools();
|
|
1478
1512
|
},
|
|
1479
1513
|
setStatus(text) { statusEl.textContent = text ?? ''; },
|
|
1480
|
-
/**
|
|
1514
|
+
/**
|
|
1515
|
+
* Select a step (unfolding the blocks around it, so it is on the
|
|
1516
|
+
* screen), the flow, or nothing — and, in a narrow editor whose panel
|
|
1517
|
+
* is scrolled off, bring its settings on screen as a press does.
|
|
1518
|
+
*/
|
|
1481
1519
|
select(id) {
|
|
1482
1520
|
const want = id === null || id === undefined ? 'flow' : id;
|
|
1483
1521
|
if (want !== 'flow' && reveal(want)) render();
|
|
1484
|
-
select(want);
|
|
1522
|
+
select(want, { picked: true });
|
|
1485
1523
|
},
|
|
1486
1524
|
focus() { focusSelected(); },
|
|
1487
1525
|
undo() { if (editable() && history.undo()) emit('undo'); },
|
package/src/tiling/shell.js
CHANGED
|
@@ -95,13 +95,17 @@ 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
|
|
@@ -22,6 +22,34 @@
|
|
|
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.
|
|
47
|
+
*
|
|
48
|
+
* The chrome stops being a drag source here (C33): a single-tab leaf now has a
|
|
49
|
+
* tab to grab, and a strip that names the tab is the reason C33 limited the
|
|
50
|
+
* chrome to the case where there was none. A drag that begins on a tab
|
|
51
|
+
* belongs to the strip, so the chrome's `dragstart` lets it pass untouched.
|
|
52
|
+
* The empty chrome around the tabs still takes the pull and the double-click.
|
|
25
53
|
*
|
|
26
54
|
* The tab bar is reused rather than reimplemented. It already has the
|
|
27
55
|
* behaviours a top strip is expected to have — drag-to-reorder, a scrolling
|
|
@@ -153,9 +181,9 @@
|
|
|
153
181
|
import { NotebookTabBar } from '../editor/notebook_tab_bar.js';
|
|
154
182
|
|
|
155
183
|
const SPLITTER_PX = 4;
|
|
156
|
-
/** The
|
|
184
|
+
/** The three tab layouts, and the framework's default. `bottom` is the default
|
|
157
185
|
* because it is what every existing embedder already renders. */
|
|
158
|
-
const TAB_LAYOUTS = ['bottom', 'top'];
|
|
186
|
+
const TAB_LAYOUTS = ['bottom', 'top', 'header'];
|
|
159
187
|
const DEFAULT_TAB_LAYOUT = 'bottom';
|
|
160
188
|
|
|
161
189
|
/** Anything that is not one of the two layouts is the default rather than an
|
|
@@ -195,8 +223,9 @@ const TILE_LIFT_PX = 24;
|
|
|
195
223
|
*
|
|
196
224
|
* A tile's chrome is a grip with controls sitting on it — the split, float
|
|
197
225
|
* and close buttons on the right, whatever the content contributed on the
|
|
198
|
-
* left, and
|
|
199
|
-
*
|
|
226
|
+
* left, and a tab — the strip is a CHILD of the chrome under the `header`
|
|
227
|
+
* layout (C38), and a sibling of it otherwise. Neither the downward pull nor
|
|
228
|
+
* the double-click may fire when
|
|
200
229
|
* the press landed on one of those.
|
|
201
230
|
*
|
|
202
231
|
* ONE CONSTANT, TWO LISTENERS, and that is the point of writing it down
|
|
@@ -204,7 +233,7 @@ const TILE_LIFT_PX = 24;
|
|
|
204
233
|
* about where the door is are worse than one door. A button added to this
|
|
205
234
|
* strip later must become inert under BOTH gestures at once, and it cannot
|
|
206
235
|
* do that if each of them carries its own copy of the rule. */
|
|
207
|
-
const CHROME_NO_FLOAT = 'button, .twm-leaf__tab';
|
|
236
|
+
const CHROME_NO_FLOAT = 'button, .twm-leaf__tab, .notebook-tabs .tab';
|
|
208
237
|
|
|
209
238
|
/**
|
|
210
239
|
* IS THIS GESTURE THE DOWNWARD PULL? Asked in TWO places that must not be
|
|
@@ -498,6 +527,11 @@ export class TileRenderer {
|
|
|
498
527
|
// that can change while the content stays put has to be refreshed
|
|
499
528
|
// here or it is frozen at whatever it was when the tile was built.
|
|
500
529
|
_paintLeafIcon(entry.iconEl, leaf, entry.content, this.ctx);
|
|
530
|
+
// C38. …and under `header` the tabs ARE the title, so a renamed
|
|
531
|
+
// tab is a renamed title and has to be refreshed for the same
|
|
532
|
+
// reason. Only when a label changed: rebuilding the strip's tabs
|
|
533
|
+
// on every repaint would detach a tab mid-drag.
|
|
534
|
+
if (this.tabLayout === 'header') this._refreshHeaderLabels(leaf, entry);
|
|
501
535
|
return entry.wrapEl;
|
|
502
536
|
}
|
|
503
537
|
// Build fresh.
|
|
@@ -589,9 +623,11 @@ export class TileRenderer {
|
|
|
589
623
|
}
|
|
590
624
|
// C22. CHROME FIRST IN BOTH LAYOUTS. The chrome is the pane's identity
|
|
591
625
|
// 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
|
-
|
|
626
|
+
// the tabs own the pane rather than the other way round. C38's
|
|
627
|
+
// `header` puts the strip inside the chrome — `_placeTabBar` decides.
|
|
628
|
+
wrap.append(chrome, body, tabBar);
|
|
629
|
+
this._placeTabBar({ wrapEl: wrap, bodyEl: body, chromeEl: chrome,
|
|
630
|
+
titleEl: title, tabBarEl: tabBar });
|
|
595
631
|
|
|
596
632
|
wrap.addEventListener('mousedown', (e) => {
|
|
597
633
|
if (e.target.closest('.twm-splitter')) return;
|
|
@@ -947,9 +983,12 @@ export class TileRenderer {
|
|
|
947
983
|
* attached element is a re-parent, not a rebuild — the body keeps its
|
|
948
984
|
* DOM, its listeners and its scroll. */
|
|
949
985
|
_placeTabBar(entry) {
|
|
950
|
-
const { wrapEl, bodyEl, tabBarEl } = entry;
|
|
986
|
+
const { wrapEl, bodyEl, tabBarEl, chromeEl, titleEl } = entry;
|
|
951
987
|
if (!wrapEl || !tabBarEl || !bodyEl) return;
|
|
952
|
-
|
|
988
|
+
// C38. Right after the title, so it takes the title's place and the
|
|
989
|
+
// content's actions and the structural buttons stay on the right.
|
|
990
|
+
if (this.tabLayout === 'header' && chromeEl && titleEl) titleEl.after(tabBarEl);
|
|
991
|
+
else if (this.tabLayout === 'top') wrapEl.insertBefore(tabBarEl, bodyEl);
|
|
953
992
|
else wrapEl.appendChild(tabBarEl);
|
|
954
993
|
}
|
|
955
994
|
|
|
@@ -975,16 +1014,48 @@ export class TileRenderer {
|
|
|
975
1014
|
const bar = entry.tabBarEl;
|
|
976
1015
|
if (!bar) return;
|
|
977
1016
|
const tabs = Array.isArray(leaf.tabs) ? leaf.tabs : [];
|
|
1017
|
+
const header = this.tabLayout === 'header';
|
|
978
1018
|
bar.classList.toggle('twm-leaf__tabbar--top', this.tabLayout === 'top');
|
|
979
|
-
|
|
1019
|
+
bar.classList.toggle('twm-leaf__tabbar--header', header);
|
|
1020
|
+
// C38. One tab is enough under `header`: the strip is the title. A
|
|
1021
|
+
// panel, or a leaf with no content, has no tab to name and keeps the
|
|
1022
|
+
// chrome's title.
|
|
1023
|
+
const isPanel = String(leaf.content?.kind || '').startsWith('panel:');
|
|
1024
|
+
const inHeader = header && !isPanel && !!leaf.content && tabs.length >= 1;
|
|
1025
|
+
entry.wrapEl?.classList.toggle('twm-leaf--tabs-in-header', inHeader);
|
|
1026
|
+
if (!inHeader && tabs.length <= 1) {
|
|
980
1027
|
bar.classList.add('twm-leaf__tabbar--hidden');
|
|
981
1028
|
this._disposeTabStrip(entry);
|
|
982
1029
|
bar.innerHTML = '';
|
|
983
1030
|
return;
|
|
984
1031
|
}
|
|
985
1032
|
bar.classList.remove('twm-leaf__tabbar--hidden');
|
|
986
|
-
if (this.tabLayout === '
|
|
987
|
-
else this.
|
|
1033
|
+
if (this.tabLayout === 'bottom') this._renderBottomTabBar(leaf, entry, tabs);
|
|
1034
|
+
else this._renderTopTabBar(leaf, entry, tabs);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/** C38. The labels the `header` strip shows for `leaf`, in one string, so
|
|
1038
|
+
* a repaint can tell whether anything it draws has changed. */
|
|
1039
|
+
_headerLabels(leaf, entry) {
|
|
1040
|
+
const tabs = Array.isArray(leaf.tabs) ? leaf.tabs : [];
|
|
1041
|
+
const activeIdx = Math.max(0, Math.min(tabs.length - 1, leaf.activeTabIdx || 0));
|
|
1042
|
+
return tabs.map((t, i) => this._tabLabel(t, i === activeIdx, entry)).join('\n');
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/** C38. A cached leaf is not re-mounted, so a tab renamed in the tree
|
|
1046
|
+
* (`TileTree.renameMatchingTabs`) reaches the strip only through here. */
|
|
1047
|
+
_refreshHeaderLabels(leaf, entry) {
|
|
1048
|
+
const labels = this._headerLabels(leaf, entry);
|
|
1049
|
+
if (entry.headerLabels === labels) return;
|
|
1050
|
+
this._renderTabBar(leaf, entry);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/** What a tab in the `top` or `header` strip is called. A tab with no
|
|
1054
|
+
* title of its own falls back, when it is the active one, to the title
|
|
1055
|
+
* its content gave at mount — the string the chrome title would have
|
|
1056
|
+
* shown, and under `header` the chrome title is hidden (C38). */
|
|
1057
|
+
_tabLabel(t, active, entry) {
|
|
1058
|
+
return t.title || (active && entry?.content?.title) || t.kind || '';
|
|
988
1059
|
}
|
|
989
1060
|
|
|
990
1061
|
/**
|
|
@@ -1021,12 +1092,19 @@ export class TileRenderer {
|
|
|
1021
1092
|
const tabs = Array.isArray(leaf.tabs) ? leaf.tabs : [];
|
|
1022
1093
|
const isPanel = String(leaf.content?.kind || '').startsWith('panel:');
|
|
1023
1094
|
const vetoed = this.leafChrome(leaf.id)?.promote === false;
|
|
1024
|
-
|
|
1095
|
+
// C38. Not under `header`: the one tab is drawn in the strip, and the
|
|
1096
|
+
// strip is its drag source.
|
|
1097
|
+
const on = tabs.length === 1 && !isPanel && !vetoed && this.tabLayout !== 'header';
|
|
1025
1098
|
if (on) chrome.setAttribute('draggable', 'true');
|
|
1026
1099
|
else chrome.removeAttribute('draggable');
|
|
1027
1100
|
if (chrome.__twmTabDragBound) return;
|
|
1028
1101
|
chrome.__twmTabDragBound = true;
|
|
1029
1102
|
chrome.addEventListener('dragstart', (ev) => {
|
|
1103
|
+
// C38. A drag that began in the strip — which the `header` layout
|
|
1104
|
+
// puts INSIDE the chrome — is the strip's, and has bubbled here.
|
|
1105
|
+
// Not refused (that would cancel the tab's own drag) and not
|
|
1106
|
+
// re-begun as tab 0: left alone.
|
|
1107
|
+
if (entry.tabBarEl?.contains(ev.target)) return;
|
|
1030
1108
|
// A drag that began on a BUTTON is not a drag of the pane. Buttons
|
|
1031
1109
|
// are not draggable on their own, but a draggable ancestor makes
|
|
1032
1110
|
// them so — hence the same guard the two float gestures apply, from
|
|
@@ -1143,11 +1221,31 @@ export class TileRenderer {
|
|
|
1143
1221
|
{ idx, x: ev.clientX, y: ev.clientY });
|
|
1144
1222
|
}, true);
|
|
1145
1223
|
}
|
|
1224
|
+
// C38. The `+` after the last tab, `header` only. Outside the
|
|
1225
|
+
// component's container so its `update()` never rewrites it, and
|
|
1226
|
+
// built with the strip so `setTabLayout` (which empties the bar and
|
|
1227
|
+
// disposes the strip) takes it away with the strip.
|
|
1228
|
+
if (this.tabLayout === 'header' && !bar.querySelector('.twm-leaf__tab-new')) {
|
|
1229
|
+
const add = document.createElement('button');
|
|
1230
|
+
add.type = 'button';
|
|
1231
|
+
add.className = 'twm-leaf__tab-new';
|
|
1232
|
+
add.dataset.action = 'tab-new';
|
|
1233
|
+
add.title = 'New tab';
|
|
1234
|
+
add.setAttribute('aria-label', 'New tab');
|
|
1235
|
+
add.innerHTML = '<span class="material-symbols-outlined">add</span>';
|
|
1236
|
+
add.addEventListener('click', (ev) => {
|
|
1237
|
+
ev.preventDefault();
|
|
1238
|
+
ev.stopPropagation();
|
|
1239
|
+
this.ctx.onLeafTabAction?.(leaf.id, 'new', {});
|
|
1240
|
+
});
|
|
1241
|
+
bar.appendChild(add);
|
|
1242
|
+
}
|
|
1146
1243
|
const activeIdx = Math.max(0, Math.min(tabs.length - 1, leaf.activeTabIdx || 0));
|
|
1244
|
+
entry.headerLabels = this._headerLabels(leaf, entry);
|
|
1147
1245
|
entry.tabStrip.update(
|
|
1148
1246
|
tabs.map((t, i) => ({
|
|
1149
1247
|
filePath: _tabKey(i),
|
|
1150
|
-
label: t
|
|
1248
|
+
label: this._tabLabel(t, i === activeIdx, entry),
|
|
1151
1249
|
// The component's `fileType` picks a glyph out of a static map
|
|
1152
1250
|
// of FILE kinds. A tile's kinds are the embedder's, and the
|
|
1153
1251
|
// taxonomy already answers for them — see `_paintTopTabs`.
|
|
@@ -1159,7 +1257,7 @@ export class TileRenderer {
|
|
|
1159
1257
|
isDirty: !!t.dirty,
|
|
1160
1258
|
})),
|
|
1161
1259
|
_tabKey(activeIdx));
|
|
1162
|
-
this._paintTopTabs(entry.tabStripHostEl, tabs);
|
|
1260
|
+
this._paintTopTabs(entry.tabStripHostEl, tabs, activeIdx, entry);
|
|
1163
1261
|
}
|
|
1164
1262
|
|
|
1165
1263
|
/**
|
|
@@ -1179,13 +1277,13 @@ export class TileRenderer {
|
|
|
1179
1277
|
* rendered last. The class names used here are the component's published
|
|
1180
1278
|
* contract, stated in its header comment.
|
|
1181
1279
|
*/
|
|
1182
|
-
_paintTopTabs(hostEl, tabs) {
|
|
1280
|
+
_paintTopTabs(hostEl, tabs, activeIdx = -1, entry = null) {
|
|
1183
1281
|
if (!hostEl) return;
|
|
1184
1282
|
const els = hostEl.querySelectorAll('.tab');
|
|
1185
1283
|
els.forEach((el, i) => {
|
|
1186
1284
|
const spec = tabs[i];
|
|
1187
1285
|
if (!spec) return;
|
|
1188
|
-
el.title = spec
|
|
1286
|
+
el.title = this._tabLabel(spec, i === activeIdx, entry);
|
|
1189
1287
|
const icon = spec.kind ? this.ctx?.taxonomy?.meta?.(spec.kind)?.icon : null;
|
|
1190
1288
|
const glyph = el.querySelector('.tab-icon');
|
|
1191
1289
|
if (glyph && icon) glyph.textContent = icon;
|
package/src/tiling/wm.js
CHANGED
|
@@ -177,6 +177,24 @@ export class WindowManager {
|
|
|
177
177
|
this.renderer.setTabLayout(layout);
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
/** C38. Open a new tab in a tile and show it. It holds the taxonomy root —
|
|
181
|
+
* what a split seeds its new pane with, so "a new tab" and "a new pane"
|
|
182
|
+
* start in the same place. Defaults to the focused tile. A panel tile
|
|
183
|
+
* grows no tabs (`TileTree.appendLeafTab`); then this does nothing and
|
|
184
|
+
* answers -1, else the new tab's index. */
|
|
185
|
+
newTab(leafId = null) {
|
|
186
|
+
const tree = this._tree();
|
|
187
|
+
const id = leafId ?? tree.focusedLeafId;
|
|
188
|
+
const seed = this._rootLeaf();
|
|
189
|
+
const idx = tree.appendLeafTab(id, seed.content, seed.title);
|
|
190
|
+
if (idx < 0) return -1;
|
|
191
|
+
tree.focus(id);
|
|
192
|
+
this.renderer.render();
|
|
193
|
+
this._persist();
|
|
194
|
+
this._notifyChange('tab-new');
|
|
195
|
+
return idx;
|
|
196
|
+
}
|
|
197
|
+
|
|
180
198
|
/** Emit on bus + call onChange. Use this instead of the bare callback
|
|
181
199
|
* so other surfaces (palette, top-bar toggles, page shortcuts) can
|
|
182
200
|
* subscribe through the existing event system. */
|
|
@@ -3449,6 +3467,12 @@ export class WindowManager {
|
|
|
3449
3467
|
this._notifyChange('twm-tab-close');
|
|
3450
3468
|
return;
|
|
3451
3469
|
}
|
|
3470
|
+
if (action === 'new') {
|
|
3471
|
+
// C38. The `header` strip's `+`. `newTab` renders, persists and
|
|
3472
|
+
// notifies on its own.
|
|
3473
|
+
this.newTab(leafId);
|
|
3474
|
+
return;
|
|
3475
|
+
}
|
|
3452
3476
|
if (action === 'move') {
|
|
3453
3477
|
tree.moveLeafTab(leafId, data.from, data.to);
|
|
3454
3478
|
this.renderer.render();
|