flexdesk 0.17.15 → 0.17.16
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/CHANGELOG.md +23 -0
- package/css/base.css +224 -4
- package/css/flexdesk.css +314 -12
- package/dist/charts.js +104 -34
- package/dist/charts.js.map +3 -3
- package/dist/{chunk-2PM6ZMNS.js → chunk-2Z2ENHR3.js} +2 -2
- package/dist/{chunk-AD7FIA3Z.js → chunk-7JI7HHSY.js} +2 -2
- package/dist/{chunk-TLIV6QSU.js → chunk-7OL6F2AM.js} +3 -3
- package/dist/{chunk-MADMGRX2.js → chunk-PGZ4EHSE.js} +156 -9
- package/dist/chunk-PGZ4EHSE.js.map +7 -0
- package/dist/{chunk-OEG6NPAN.js → chunk-XJNADRGR.js} +2 -2
- package/dist/flexdesk.css +314 -12
- package/dist/flow.js +3 -3
- package/dist/tiles.js +84 -9
- package/dist/tiles.js.map +2 -2
- package/dist/widgets.js +448 -5
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +102 -14
- package/dist/wm.js.map +2 -2
- package/docs/README.md +1 -1
- package/docs/components/charts.md +70 -3
- package/docs/components/forms-and-dialogs.md +61 -0
- package/docs/components/menus.md +85 -1
- package/docs/components/shell.md +37 -0
- package/docs/components/tiles.md +37 -0
- package/package.json +1 -1
- package/src/charts/plot_config_editor.js +125 -29
- package/src/tiles/tile_base.js +48 -2
- package/src/tiles/tile_grid.js +42 -1
- package/src/tiling/kind_taxonomy.js +45 -0
- package/src/tiling/wm.js +86 -11
- package/src/ui/components/category_picker.js +556 -0
- package/src/ui/components/computing_status_window.js +3 -0
- package/src/ui/components/confirm_dialog.js +3 -0
- package/src/ui/components/managed_window.js +204 -9
- package/src/ui/components/wizard.js +4 -1
- package/widgets.js +2 -1
- package/dist/chunk-MADMGRX2.js.map +0 -7
- /package/dist/{chunk-2PM6ZMNS.js.map → chunk-2Z2ENHR3.js.map} +0 -0
- /package/dist/{chunk-AD7FIA3Z.js.map → chunk-7JI7HHSY.js.map} +0 -0
- /package/dist/{chunk-TLIV6QSU.js.map → chunk-7OL6F2AM.js.map} +0 -0
- /package/dist/{chunk-OEG6NPAN.js.map → chunk-XJNADRGR.js.map} +0 -0
package/src/tiles/tile_base.js
CHANGED
|
@@ -47,10 +47,13 @@ export class TileBase {
|
|
|
47
47
|
* @param {{resolve: Function}} [options.dataSource] - Where this tile asks for its
|
|
48
48
|
* own rows. Handed down by `TileGrid`; absent for a grid whose data is
|
|
49
49
|
* broadcast with `setData()`, and then nothing ever calls `loadData()`.
|
|
50
|
+
* @param {boolean} [options.configOnDoubleClick=true] - A double-click on the
|
|
51
|
+
* title strip (not on its buttons or an editable field in it) does what
|
|
52
|
+
* the cogwheel does. False leaves the cogwheel as the only way in.
|
|
50
53
|
*/
|
|
51
54
|
constructor({ id, grid, eventBus = null, config = {}, readonly = false,
|
|
52
55
|
headless = false, host = null, stateGuard = null,
|
|
53
|
-
dataSource = null }) {
|
|
56
|
+
dataSource = null, configOnDoubleClick = true }) {
|
|
54
57
|
this.host = host;
|
|
55
58
|
// THE TILE PULLS; THE GRID DOES NOT PUSH.
|
|
56
59
|
//
|
|
@@ -74,6 +77,9 @@ export class TileBase {
|
|
|
74
77
|
this.config = { ...this.getDefaultConfig(), ...config };
|
|
75
78
|
this.readonly = readonly;
|
|
76
79
|
this.headless = headless;
|
|
80
|
+
this.configOnDoubleClick = configOnDoubleClick;
|
|
81
|
+
/** STORY-0128. See `setConfiguring`. */
|
|
82
|
+
this._configuring = false;
|
|
77
83
|
this.element = null;
|
|
78
84
|
this.contentElement = null;
|
|
79
85
|
this.chartInstance = null;
|
|
@@ -288,7 +294,8 @@ export class TileBase {
|
|
|
288
294
|
if (this._disposed) return;
|
|
289
295
|
|
|
290
296
|
this.element = document.createElement('div');
|
|
291
|
-
this.element.className = `tile${this.readonly ? ' tile--readonly' : ''}
|
|
297
|
+
this.element.className = `tile${this.readonly ? ' tile--readonly' : ''}`
|
|
298
|
+
+ `${this._configuring ? ' tile--configuring' : ''}`;
|
|
292
299
|
|
|
293
300
|
// Use StateGuard bypass if available to set dataset properties
|
|
294
301
|
const stateGuard = this.stateGuard;
|
|
@@ -595,6 +602,17 @@ export class TileBase {
|
|
|
595
602
|
this._onConfigClick();
|
|
596
603
|
});
|
|
597
604
|
|
|
605
|
+
// STORY-0128. The title strip's double-click is a second door to
|
|
606
|
+
// the cogwheel, through the same `_onConfigClick`. Not on a button
|
|
607
|
+
// in the strip (each has its own verb) and not on an editable
|
|
608
|
+
// field, where a double-click selects a word.
|
|
609
|
+
header.addEventListener('dblclick', (e) => {
|
|
610
|
+
if (!this.configOnDoubleClick || !configBtn) return;
|
|
611
|
+
if (e.target.closest?.('button, input, textarea, select, [contenteditable=""], [contenteditable="true"], .tile-menu-dropdown')) return;
|
|
612
|
+
e.stopPropagation();
|
|
613
|
+
this._onConfigClick();
|
|
614
|
+
});
|
|
615
|
+
|
|
598
616
|
// "..." menu button + dropdown
|
|
599
617
|
const menuBtn = header.querySelector('.tile-menu-btn');
|
|
600
618
|
const menuDropdown = header.querySelector('.tile-menu-dropdown');
|
|
@@ -705,6 +723,34 @@ export class TileBase {
|
|
|
705
723
|
if (this._menuHome && menu.parentNode !== this._menuHome) this._menuHome.appendChild(menu);
|
|
706
724
|
}
|
|
707
725
|
|
|
726
|
+
/**
|
|
727
|
+
* Mark this tile as being configured, or clear the mark (STORY-0128).
|
|
728
|
+
* A convenience for `grid.setConfiguring(tile.id, on)`, which is the API:
|
|
729
|
+
* the grid holds the mark by tile id, so it outlives this tile instance
|
|
730
|
+
* (`setLayout` disposes and rebuilds every tile). A tile with no grid
|
|
731
|
+
* holds the mark itself.
|
|
732
|
+
*
|
|
733
|
+
* @param {boolean} [on=true]
|
|
734
|
+
*/
|
|
735
|
+
setConfiguring(on = true) {
|
|
736
|
+
if (typeof this.grid?.setConfiguring === 'function') {
|
|
737
|
+
this.grid.setConfiguring(this.id, on);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
this._applyConfiguring(on);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/** Draw (or clear) the configuring mark. Called by the grid.
|
|
744
|
+
* @param {boolean} on
|
|
745
|
+
* @private */
|
|
746
|
+
_applyConfiguring(on) {
|
|
747
|
+
this._configuring = !!on;
|
|
748
|
+
this.element?.classList.toggle('tile--configuring', this._configuring);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/** True while this tile is marked as being configured. */
|
|
752
|
+
get isConfiguring() { return this._configuring; }
|
|
753
|
+
|
|
708
754
|
/**
|
|
709
755
|
* Handle config button click — highlight tile and emit config request.
|
|
710
756
|
* @private
|
package/src/tiles/tile_grid.js
CHANGED
|
@@ -27,6 +27,8 @@ export class TileGrid {
|
|
|
27
27
|
* @param {*} [options.scope=null] - The board's scope, a filter every tile
|
|
28
28
|
* follows (0.16.1); see `setScope`.
|
|
29
29
|
* @param {Function} [options.onScopeChange] - `(scope)` after `setScope` changed it.
|
|
30
|
+
* @param {boolean} [options.configOnDoubleClick=true] - Whether a double-click
|
|
31
|
+
* on a tile's title strip opens its config, as its cogwheel does (0.17.16).
|
|
30
32
|
*/
|
|
31
33
|
constructor({ container, columns = 12, rowHeight = 80, gap = 16, eventBus = null,
|
|
32
34
|
showToolbar = true, readonly = false, onLayoutChange, onBeforeLayoutChange,
|
|
@@ -38,12 +40,19 @@ export class TileGrid {
|
|
|
38
40
|
host = null, stateGuard = null,
|
|
39
41
|
// Injected, never constructed here: the grid does not know what
|
|
40
42
|
// a binding is, only that a tile may have somewhere to ask.
|
|
41
|
-
dataSource = null, scope = null, onScopeChange = null
|
|
43
|
+
dataSource = null, scope = null, onScopeChange = null,
|
|
44
|
+
// STORY-0128. Handed to every tile: false turns off the
|
|
45
|
+
// title strip's double-click to configure.
|
|
46
|
+
configOnDoubleClick = true }) {
|
|
42
47
|
this.host = host;
|
|
43
48
|
this.scope = scope;
|
|
44
49
|
this._onScopeChangeCallback = onScopeChange;
|
|
45
50
|
this.stateGuard = stateGuard;
|
|
46
51
|
this.dataSource = dataSource;
|
|
52
|
+
this.configOnDoubleClick = configOnDoubleClick;
|
|
53
|
+
/** STORY-0128. Ids of the tiles marked as being configured; see
|
|
54
|
+
* `setConfiguring`. Kept by id so a rebuilt tile is marked again. */
|
|
55
|
+
this._configuringIds = new Set();
|
|
47
56
|
this.container = container;
|
|
48
57
|
this.columns = columns;
|
|
49
58
|
this.rowHeight = rowHeight;
|
|
@@ -194,11 +203,14 @@ export class TileGrid {
|
|
|
194
203
|
host: this.host,
|
|
195
204
|
stateGuard: this.stateGuard,
|
|
196
205
|
dataSource: this.dataSource,
|
|
206
|
+
configOnDoubleClick: this.configOnDoubleClick,
|
|
197
207
|
});
|
|
198
208
|
|
|
199
209
|
if (!tile) return null;
|
|
200
210
|
// Before mount, so a tile's first render and first load already follow it.
|
|
201
211
|
tile.scope = this.scope;
|
|
212
|
+
// STORY-0128. A tile rebuilt under a marked id (setLayout) is marked.
|
|
213
|
+
if (this._configuringIds.has(tileId)) tile._applyConfiguring(true);
|
|
202
214
|
|
|
203
215
|
// Add to layout
|
|
204
216
|
const layoutEntry = {
|
|
@@ -340,6 +352,35 @@ export class TileGrid {
|
|
|
340
352
|
}));
|
|
341
353
|
}
|
|
342
354
|
|
|
355
|
+
/**
|
|
356
|
+
* Mark a tile as being configured, or clear the mark (STORY-0128).
|
|
357
|
+
*
|
|
358
|
+
* A config request (cogwheel or title double-click) highlights the
|
|
359
|
+
* requesting tile with `tile--selected` and takes that highlight off every
|
|
360
|
+
* other tile: one selection per grid. An embedder that keeps a config
|
|
361
|
+
* window open per tile marks each such tile here instead. The mark
|
|
362
|
+
* (`tile--configuring`, drawn like the selection) is the embedder's alone:
|
|
363
|
+
* no config request clears it, and it is held by id, so a tile that
|
|
364
|
+
* `setLayout` rebuilds (or an id added later) is marked again. Clear it
|
|
365
|
+
* when that tile's config window closes.
|
|
366
|
+
*
|
|
367
|
+
* @param {string} tileId
|
|
368
|
+
* @param {boolean} [on=true]
|
|
369
|
+
*/
|
|
370
|
+
setConfiguring(tileId, on = true) {
|
|
371
|
+
if (on) this._configuringIds.add(tileId);
|
|
372
|
+
else this._configuringIds.delete(tileId);
|
|
373
|
+
this.tiles.get(tileId)?._applyConfiguring(!!on);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* @param {string} tileId
|
|
378
|
+
* @returns {boolean} Whether the tile is marked as being configured.
|
|
379
|
+
*/
|
|
380
|
+
isConfiguring(tileId) {
|
|
381
|
+
return this._configuringIds.has(tileId);
|
|
382
|
+
}
|
|
383
|
+
|
|
343
384
|
/**
|
|
344
385
|
* Set layout and rebuild tiles.
|
|
345
386
|
* @param {Array|Object} layout - Layout array or object with tiles property
|
|
@@ -62,6 +62,20 @@
|
|
|
62
62
|
* a reason, and keeps the first of any duplicates a restored
|
|
63
63
|
* layout holds. The root kind cannot be single: every new
|
|
64
64
|
* pane, tab and desktop is seeded with it.
|
|
65
|
+
*
|
|
66
|
+
* window? `{ width?, height? }` — the initial size, in px, of a managed
|
|
67
|
+
* window this kind opens in (`navigate(…, { dest: 'window' })`,
|
|
68
|
+
* or always for `windowOnly`); clamped to the screen and to the
|
|
69
|
+
* window's minimum (400 × 300) as any window is. Absent: the
|
|
70
|
+
* window's default size (STORY-0127).
|
|
71
|
+
* windowOnly? `true`: the kind lives in a managed window, never in a tile
|
|
72
|
+
* — a tool, not a page. Every open path (navigate with any
|
|
73
|
+
* `dest`, openInPrimary, openInLeaf, the tab and split verbs)
|
|
74
|
+
* opens it in a window; its window never docks into a tile
|
|
75
|
+
* (no Back to tile, maximise is geometric, no drag-to-dock); a
|
|
76
|
+
* restored layout drops a tile tab of it, with one warning, and
|
|
77
|
+
* a tab's history entry of it opens a window. Neither the root
|
|
78
|
+
* nor a top-nav kind can be window-only (STORY-0127).
|
|
65
79
|
*/
|
|
66
80
|
|
|
67
81
|
import { PANEL_OFFERED, PANEL_SIDES, readPanelOffer } from './panel_availability.js';
|
|
@@ -104,6 +118,26 @@ export function createTaxonomy({ kinds, root } = {}) {
|
|
|
104
118
|
throw new TypeError(
|
|
105
119
|
`createTaxonomy: '${kind}'.single must be true or a prop name`);
|
|
106
120
|
}
|
|
121
|
+
if (def.window != null) {
|
|
122
|
+
const w = def.window;
|
|
123
|
+
const bad = (v) => v != null && !(typeof v === 'number' && Number.isFinite(v) && v > 0);
|
|
124
|
+
if (typeof w !== 'object' || Array.isArray(w) || bad(w.width) || bad(w.height)) {
|
|
125
|
+
throw new TypeError(
|
|
126
|
+
`createTaxonomy: '${kind}'.window must be { width?, height? } in positive px`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (def.windowOnly != null && typeof def.windowOnly !== 'boolean') {
|
|
130
|
+
throw new TypeError(`createTaxonomy: '${kind}'.windowOnly must be true or false`);
|
|
131
|
+
}
|
|
132
|
+
if (def.windowOnly && def.isTopNav) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`createTaxonomy: '${kind}' is a top-nav page and cannot be windowOnly — a page lives in tiles`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (kinds[root].windowOnly) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`createTaxonomy: the root kind '${root}' cannot be windowOnly — `
|
|
140
|
+
+ 'every new pane, tab and desktop is seeded with it');
|
|
107
141
|
}
|
|
108
142
|
if (kinds[root].single) {
|
|
109
143
|
throw new Error(
|
|
@@ -227,6 +261,15 @@ export function createTaxonomy({ kinds, root } = {}) {
|
|
|
227
261
|
return `${kind}\u0000${v == null ? '' : String(v)}`;
|
|
228
262
|
};
|
|
229
263
|
|
|
264
|
+
/** Whether `kind` lives in a managed window only (KindDef `windowOnly`). */
|
|
265
|
+
const isWindowOnly = (kind) => _kinds[kind]?.windowOnly === true;
|
|
266
|
+
|
|
267
|
+
/** The initial size of a window `kind` opens in (KindDef `window`), or null. */
|
|
268
|
+
const windowSize = (kind) => {
|
|
269
|
+
const w = _kinds[kind]?.window;
|
|
270
|
+
return w ? { width: w.width ?? null, height: w.height ?? null } : null;
|
|
271
|
+
};
|
|
272
|
+
|
|
230
273
|
/** The sentence a refused copy verb shows for a single kind. */
|
|
231
274
|
const singleReason = (kind) =>
|
|
232
275
|
`${_kinds[kind]?.label || kind} is open once: use the one that is open`;
|
|
@@ -246,5 +289,7 @@ export function createTaxonomy({ kinds, root } = {}) {
|
|
|
246
289
|
isSingle,
|
|
247
290
|
instanceKey,
|
|
248
291
|
singleReason,
|
|
292
|
+
isWindowOnly,
|
|
293
|
+
windowSize,
|
|
249
294
|
});
|
|
250
295
|
}
|
package/src/tiling/wm.js
CHANGED
|
@@ -303,8 +303,14 @@ export class WindowManager {
|
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
305
|
}
|
|
306
|
-
// A
|
|
307
|
-
|
|
306
|
+
// A window-only kind is never in a tile, and a single kind is open
|
|
307
|
+
// once, whatever the saved layout says.
|
|
308
|
+
const windowOnlyDropped = this._dropWindowOnlyTabs();
|
|
309
|
+
if (windowOnlyDropped.length) {
|
|
310
|
+
(this.logger ?? console).warn?.(`[wm] the saved layout held ${windowOnlyDropped.length} tile tab(s) `
|
|
311
|
+
+ `of window-only kind(s) ${[...new Set(windowOnlyDropped)].join(', ')}; they open in a window when asked for`);
|
|
312
|
+
}
|
|
313
|
+
const deduped = this._dropDuplicateSingles() || windowOnlyDropped.length > 0;
|
|
308
314
|
for (const d of this.desktops.desktops) this._canonicalize(d.tree, d);
|
|
309
315
|
this.renderer.tree = this.desktops.active().tree;
|
|
310
316
|
this.renderer.render();
|
|
@@ -581,20 +587,57 @@ export class WindowManager {
|
|
|
581
587
|
return keys;
|
|
582
588
|
}
|
|
583
589
|
|
|
590
|
+
/** Open a window-only kind (KindDef `windowOnly`) in its window — the one
|
|
591
|
+
* that is open, or a new one — and say so; any other kind: false, and the
|
|
592
|
+
* caller opens it where it asked (STORY-0127). */
|
|
593
|
+
_openWindowOnly(kind, props) {
|
|
594
|
+
if (!this.taxonomy.isWindowOnly?.(kind)) return false;
|
|
595
|
+
this._navigateWindow(kind, props);
|
|
596
|
+
return true;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** Whether a window record holds a window-only kind: such a window never
|
|
600
|
+
* docks into a tile (Back to tile, maximize, drag-to-dock). */
|
|
601
|
+
_holdsWindowOnly(rec) {
|
|
602
|
+
return !!this.taxonomy.isWindowOnly
|
|
603
|
+
&& _windowTabSpecs(rec).some((t) => this.taxonomy.isWindowOnly(t?.kind));
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/** A restored layout never holds a window-only kind in a tile: its tabs,
|
|
607
|
+
* and its tabs archived under a page, go. Windows are not restored, so
|
|
608
|
+
* the content simply opens in a window the next time it is asked for.
|
|
609
|
+
* Returns the kinds of the tabs that went, one entry per tab. */
|
|
610
|
+
_dropWindowOnlyTabs() {
|
|
611
|
+
if (!this.taxonomy.isWindowOnly) return [];
|
|
612
|
+
const dropped = [];
|
|
613
|
+
this._pruneRestoredTabs((t) => {
|
|
614
|
+
if (!t || !this.taxonomy.isWindowOnly(t.kind)) return true;
|
|
615
|
+
dropped.push(t.kind);
|
|
616
|
+
return false;
|
|
617
|
+
});
|
|
618
|
+
return dropped;
|
|
619
|
+
}
|
|
620
|
+
|
|
584
621
|
/** A restored layout keeps the FIRST instance of each single kind: later
|
|
585
622
|
* live tabs are dropped (the tile shows the tab that took the dropped
|
|
586
623
|
* one's place, or closes when it held nothing else), and so are later
|
|
587
624
|
* archived ones. Answers whether anything was dropped. */
|
|
588
625
|
_dropDuplicateSingles() {
|
|
589
626
|
const seen = new Set();
|
|
590
|
-
|
|
591
|
-
const keep = (t) => {
|
|
627
|
+
return this._pruneRestoredTabs((t) => {
|
|
592
628
|
const k = t ? this._instanceKey(t.kind, t.props) : null;
|
|
593
629
|
if (k == null) return true;
|
|
594
630
|
if (seen.has(k)) return false;
|
|
595
631
|
seen.add(k);
|
|
596
632
|
return true;
|
|
597
|
-
};
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** Drop every tab of the restored layout `keep(tab)` refuses, in the
|
|
637
|
+
* tiles and in their archived pages; a tile left empty closes (seeding a
|
|
638
|
+
* content tile if none is left). Returns whether anything went. */
|
|
639
|
+
_pruneRestoredTabs(keep) {
|
|
640
|
+
let dropped = false;
|
|
598
641
|
for (const d of this.desktops.desktops) {
|
|
599
642
|
const tree = d.tree;
|
|
600
643
|
for (const leaf of tree.leaves()) {
|
|
@@ -862,6 +905,9 @@ export class WindowManager {
|
|
|
862
905
|
const tree = this._tree();
|
|
863
906
|
const prior = tree.popActiveTabHistory?.(id);
|
|
864
907
|
if (!prior || !prior.kind) return false;
|
|
908
|
+
// A window-only kind in a tab's history (a layout saved before the kind
|
|
909
|
+
// was one) goes back to its window, never into this tile.
|
|
910
|
+
if (this._openWindowOnly(prior.kind, prior.props || {})) return true;
|
|
865
911
|
// A single kind opened elsewhere since this entry was pushed is
|
|
866
912
|
// revealed there, not put back here as a second copy.
|
|
867
913
|
if (this.revealInstance(prior.kind, prior.props || {})) return true;
|
|
@@ -973,6 +1019,7 @@ export class WindowManager {
|
|
|
973
1019
|
* the leaf. Used by Backspace + breadcrumb segments + anything
|
|
974
1020
|
* else that should walk WITHIN the tile rather than reset it. */
|
|
975
1021
|
navigateActiveTab(leafId, kind, props = {}) {
|
|
1022
|
+
if (this._openWindowOnly(kind, props)) return;
|
|
976
1023
|
if (this.revealInstance(kind, props)) return;
|
|
977
1024
|
const tree = this._tree();
|
|
978
1025
|
const leaf = tree.get(leafId);
|
|
@@ -1029,6 +1076,7 @@ export class WindowManager {
|
|
|
1029
1076
|
* tab feature shipped), the behavior is identical to the old
|
|
1030
1077
|
* setLeafContent: one tab in, one tab out. */
|
|
1031
1078
|
openInLeaf(leafId, kind, props = {}) {
|
|
1079
|
+
if (this._openWindowOnly(kind, props)) return;
|
|
1032
1080
|
if (this.revealInstance(kind, props)) return;
|
|
1033
1081
|
const tree = this._tree();
|
|
1034
1082
|
const leaf = tree.get(leafId);
|
|
@@ -1047,6 +1095,7 @@ export class WindowManager {
|
|
|
1047
1095
|
* previous mount, mounts the new kind into the same body element,
|
|
1048
1096
|
* and updates the window's title. */
|
|
1049
1097
|
openInWindow(winId, kind, props = {}) {
|
|
1098
|
+
if (this._openWindowOnly(kind, props)) return;
|
|
1050
1099
|
if (this.revealInstance(kind, props)) return;
|
|
1051
1100
|
const rec = this._windowToLeaf.get(winId);
|
|
1052
1101
|
if (!rec) { this.openInPrimary(kind, props); return; }
|
|
@@ -1089,6 +1138,7 @@ export class WindowManager {
|
|
|
1089
1138
|
* If only panel tiles exist (or the tree is empty), spawn a new
|
|
1090
1139
|
* content leaf and canonicalize so the panels wrap it. */
|
|
1091
1140
|
openInPrimary(kind, props = {}) {
|
|
1141
|
+
if (this._openWindowOnly(kind, props)) return;
|
|
1092
1142
|
if (this.revealInstance(kind, props)) return;
|
|
1093
1143
|
const tree = this._tree();
|
|
1094
1144
|
let leafId = tree.primaryLeafId();
|
|
@@ -1172,6 +1222,7 @@ export class WindowManager {
|
|
|
1172
1222
|
* pane in a split never dismounts the original). Returns the new
|
|
1173
1223
|
* leaf id, or null if the leaf can't be split. */
|
|
1174
1224
|
splitLeafWith(leafId, dir, kind, props = {}, title = '') {
|
|
1225
|
+
if (this._openWindowOnly(kind, props)) return null;
|
|
1175
1226
|
if (this.revealInstance(kind, props)) return null;
|
|
1176
1227
|
const tree = this._tree();
|
|
1177
1228
|
leafId = this._contentLeafId(leafId);
|
|
@@ -1661,6 +1712,9 @@ export class WindowManager {
|
|
|
1661
1712
|
canMaximize: true,
|
|
1662
1713
|
canResize: true,
|
|
1663
1714
|
modal: false,
|
|
1715
|
+
// BUG-0053. The id is minted per window and never opens again, so
|
|
1716
|
+
// geometry saved under it could never be restored.
|
|
1717
|
+
persistGeometry: false,
|
|
1664
1718
|
// C15. Dropping a promoted window on a tile PUTS IT BACK — as that
|
|
1665
1719
|
// tile's content, as a split of it, or as one of its tabs. Off
|
|
1666
1720
|
// unless the embedder asked, because it changes what a drag to an
|
|
@@ -2273,6 +2327,9 @@ export class WindowManager {
|
|
|
2273
2327
|
// false lets the geometric maximise happen instead — the answer every
|
|
2274
2328
|
// other window manager gives a window with nowhere to go back to.
|
|
2275
2329
|
if (!rec) return false;
|
|
2330
|
+
// A window-only kind has no tile to go back to (STORY-0127): the
|
|
2331
|
+
// geometric maximise happens instead.
|
|
2332
|
+
if (this._holdsWindowOnly(rec)) return false;
|
|
2276
2333
|
const tree = this.desktops.desktops[rec.desktopIdx]?.tree;
|
|
2277
2334
|
const home = rec.homeLeafId ? tree?.get(rec.homeLeafId) : null;
|
|
2278
2335
|
if (home && home.kind === 'leaf') {
|
|
@@ -3138,6 +3195,7 @@ export class WindowManager {
|
|
|
3138
3195
|
dockWindowInto(windowId, target) {
|
|
3139
3196
|
const rec = this._windowToLeaf.get(windowId);
|
|
3140
3197
|
if (!rec || !target?.leafId) return false;
|
|
3198
|
+
if (this._holdsWindowOnly(rec)) return false; // never into a tile (STORY-0127)
|
|
3141
3199
|
rec._dock = { ...target };
|
|
3142
3200
|
rec._demoting = true;
|
|
3143
3201
|
try { rec.window.close({ force: true }); }
|
|
@@ -3343,7 +3401,8 @@ export class WindowManager {
|
|
|
3343
3401
|
e.preventDefault();
|
|
3344
3402
|
const rec = this._windowToLeaf.get(winId);
|
|
3345
3403
|
if (!rec) return;
|
|
3346
|
-
|
|
3404
|
+
// A window-only kind's window has no tile to go back to
|
|
3405
|
+
const items = this._holdsWindowOnly(rec) ? [] : [
|
|
3347
3406
|
{ label: 'Back to tile', icon: 'close_fullscreen', action: 'back' },
|
|
3348
3407
|
];
|
|
3349
3408
|
// C40. Never onto a fixed desktop — `moveWindowToDesktop` refuses.
|
|
@@ -4223,6 +4282,9 @@ export class WindowManager {
|
|
|
4223
4282
|
|
|
4224
4283
|
/** The routing behind `navigate`, for a kind known to be registered. */
|
|
4225
4284
|
_route(kind, props, opts) {
|
|
4285
|
+
// A window-only kind (KindDef `windowOnly`) opens in a window, whatever
|
|
4286
|
+
// the destination asked for.
|
|
4287
|
+
if (this._openWindowOnly(kind, props)) return;
|
|
4226
4288
|
const { ctx = null, transient = false } = opts;
|
|
4227
4289
|
// Append the tab but stay where you are. Only meaningful with `newTab`.
|
|
4228
4290
|
const background = !!opts.background;
|
|
@@ -4297,6 +4359,10 @@ export class WindowManager {
|
|
|
4297
4359
|
* source leaf — closing the window just disposes the content. */
|
|
4298
4360
|
_navigateWindow(kind, props) {
|
|
4299
4361
|
if (this.revealInstance(kind, props)) return;
|
|
4362
|
+
const size = this.taxonomy.windowSize?.(kind);
|
|
4363
|
+
// A window-only kind's window maximises as any window does and never
|
|
4364
|
+
// snaps into a tile (STORY-0127)
|
|
4365
|
+
const dockable = !this.taxonomy.isWindowOnly?.(kind);
|
|
4300
4366
|
const winId = `twm-mw-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 5)}`;
|
|
4301
4367
|
const contentEl = document.createElement('div');
|
|
4302
4368
|
contentEl.className = 'twm-window-content';
|
|
@@ -4309,17 +4375,22 @@ export class WindowManager {
|
|
|
4309
4375
|
title: mountInfo?.title || title,
|
|
4310
4376
|
icon: 'web_asset',
|
|
4311
4377
|
content: contentEl,
|
|
4378
|
+
...(size?.width ? { defaultWidth: size.width } : {}),
|
|
4379
|
+
...(size?.height ? { defaultHeight: size.height } : {}),
|
|
4312
4380
|
canMinimize: true,
|
|
4313
4381
|
canMaximize: true,
|
|
4314
4382
|
canResize: true,
|
|
4315
4383
|
modal: false,
|
|
4384
|
+
// BUG-0053. The id is minted per window and never opens again, so
|
|
4385
|
+
// geometry saved under it could never be restored.
|
|
4386
|
+
persistGeometry: false,
|
|
4316
4387
|
// C15, and this is the SECOND of the two places the WM builds a
|
|
4317
4388
|
// window. Alt+N and "Open in new window" produce a window that is
|
|
4318
4389
|
// every bit as dockable as a promoted one, and a window that can be
|
|
4319
4390
|
// dragged onto a tile in one case and not the other is a rule
|
|
4320
4391
|
// nobody can learn.
|
|
4321
|
-
snap: this.snapPromotion,
|
|
4322
|
-
snapController: this.snapPromotion ? this._snapController() : null,
|
|
4392
|
+
snap: dockable && this.snapPromotion,
|
|
4393
|
+
snapController: dockable && this.snapPromotion ? this._snapController() : null,
|
|
4323
4394
|
// R1. THE PANE IS A BOX WITH `overflow: hidden`. A window contained
|
|
4324
4395
|
// to one (C21) cannot be dragged a single pixel outside it, so
|
|
4325
4396
|
// "drag a window from one tile to another" — the gesture all three
|
|
@@ -4337,9 +4408,11 @@ export class WindowManager {
|
|
|
4337
4408
|
// harder. So the maximize button docks — and the separate demote
|
|
4338
4409
|
// button the WM used to inject beside it is gone, because two
|
|
4339
4410
|
// buttons for one verb is how you get a chrome nobody reads.
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4411
|
+
...(dockable ? {
|
|
4412
|
+
onMaximize: () => this.bringBackWindow(winId),
|
|
4413
|
+
maximizeIcon: 'close_fullscreen',
|
|
4414
|
+
maximizeTitle: 'Back to tile',
|
|
4415
|
+
} : {}),
|
|
4343
4416
|
onClose: () => this._onManagedWindowClosed(winId, mountInfo),
|
|
4344
4417
|
});
|
|
4345
4418
|
// DECORATED ONCE, below, after the record exists. It was called here as
|
|
@@ -4425,6 +4498,7 @@ export class WindowManager {
|
|
|
4425
4498
|
* routing) but uses `appendLeafTab` so the existing content
|
|
4426
4499
|
* stays in place as a tab. */
|
|
4427
4500
|
openInTabFromContext(ctx, kind, props = {}, transient = false, background = false) {
|
|
4501
|
+
if (this._openWindowOnly(kind, props)) return;
|
|
4428
4502
|
if (this.revealInstance(kind, props)) return;
|
|
4429
4503
|
// Managed-window content: just open in the window — managed
|
|
4430
4504
|
// windows aren't tabbed (one window = one content).
|
|
@@ -4461,6 +4535,7 @@ export class WindowManager {
|
|
|
4461
4535
|
* "Add row" button, which passes no ctx) reliably lands as a sibling
|
|
4462
4536
|
* tab in the main tile rather than swapping its content. */
|
|
4463
4537
|
openInTabInPrimary(kind, props = {}, transient = false, background = false) {
|
|
4538
|
+
if (this._openWindowOnly(kind, props)) return;
|
|
4464
4539
|
if (this.revealInstance(kind, props)) return;
|
|
4465
4540
|
const tree = this._tree();
|
|
4466
4541
|
const leafId = tree.primaryLeafId();
|