flexdesk 0.17.15 → 0.17.17

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/css/base.css +236 -5
  3. package/css/flexdesk.css +326 -13
  4. package/css/tokens.css +3 -0
  5. package/dist/charts.js +104 -34
  6. package/dist/charts.js.map +3 -3
  7. package/dist/{chunk-2PM6ZMNS.js → chunk-2Z2ENHR3.js} +2 -2
  8. package/dist/{chunk-AD7FIA3Z.js → chunk-7JI7HHSY.js} +2 -2
  9. package/dist/{chunk-TLIV6QSU.js → chunk-7OL6F2AM.js} +3 -3
  10. package/dist/{chunk-MADMGRX2.js → chunk-PGZ4EHSE.js} +156 -9
  11. package/dist/chunk-PGZ4EHSE.js.map +7 -0
  12. package/dist/{chunk-OEG6NPAN.js → chunk-XJNADRGR.js} +2 -2
  13. package/dist/flexdesk.css +326 -13
  14. package/dist/flow.js +3 -3
  15. package/dist/tiles.js +84 -9
  16. package/dist/tiles.js.map +2 -2
  17. package/dist/tokens.css +3 -0
  18. package/dist/widgets.js +4559 -4091
  19. package/dist/widgets.js.map +4 -4
  20. package/dist/wm.js +212 -40
  21. package/dist/wm.js.map +2 -2
  22. package/docs/README.md +1 -1
  23. package/docs/components/charts.md +70 -3
  24. package/docs/components/forms-and-dialogs.md +61 -0
  25. package/docs/components/menus.md +85 -1
  26. package/docs/components/notifications.md +23 -1
  27. package/docs/components/panels-and-desktops.md +22 -0
  28. package/docs/components/shell.md +91 -0
  29. package/docs/components/tiles.md +37 -0
  30. package/package.json +1 -1
  31. package/src/charts/plot_config_editor.js +125 -29
  32. package/src/tiles/tile_base.js +48 -2
  33. package/src/tiles/tile_grid.js +42 -1
  34. package/src/tiling/kind_taxonomy.js +66 -13
  35. package/src/tiling/shell.js +4 -1
  36. package/src/tiling/wm.js +205 -35
  37. package/src/ui/components/category_picker.js +556 -0
  38. package/src/ui/components/computing_status_window.js +3 -0
  39. package/src/ui/components/confirm_dialog.js +3 -0
  40. package/src/ui/components/managed_window.js +204 -9
  41. package/src/ui/components/notification_history.js +11 -1
  42. package/src/ui/components/wizard.js +4 -1
  43. package/src/ui/notification_center.js +47 -1
  44. package/widgets.js +2 -1
  45. package/dist/chunk-MADMGRX2.js.map +0 -7
  46. /package/dist/{chunk-2PM6ZMNS.js.map → chunk-2Z2ENHR3.js.map} +0 -0
  47. /package/dist/{chunk-AD7FIA3Z.js.map → chunk-7JI7HHSY.js.map} +0 -0
  48. /package/dist/{chunk-TLIV6QSU.js.map → chunk-7OL6F2AM.js.map} +0 -0
  49. /package/dist/{chunk-OEG6NPAN.js.map → chunk-XJNADRGR.js.map} +0 -0
package/dist/wm.js CHANGED
@@ -8,14 +8,14 @@ import {
8
8
  } from "./chunk-REJ5HLIM.js";
9
9
  import {
10
10
  showContextMenu
11
- } from "./chunk-AD7FIA3Z.js";
11
+ } from "./chunk-7JI7HHSY.js";
12
12
  import {
13
13
  openForm
14
- } from "./chunk-OEG6NPAN.js";
14
+ } from "./chunk-XJNADRGR.js";
15
15
  import {
16
16
  ManagedWindow,
17
17
  isRowActivation
18
- } from "./chunk-MADMGRX2.js";
18
+ } from "./chunk-PGZ4EHSE.js";
19
19
  import "./chunk-HNU6MM2B.js";
20
20
  import {
21
21
  NotebookTabBar
@@ -2642,6 +2642,28 @@ function createTaxonomy({ kinds, root } = {}) {
2642
2642
  `createTaxonomy: '${kind}'.single must be true or a prop name`
2643
2643
  );
2644
2644
  }
2645
+ if (def.window != null) {
2646
+ const w = def.window;
2647
+ const bad = (v) => v != null && !(typeof v === "number" && Number.isFinite(v) && v > 0);
2648
+ if (typeof w !== "object" || Array.isArray(w) || bad(w.width) || bad(w.height)) {
2649
+ throw new TypeError(
2650
+ `createTaxonomy: '${kind}'.window must be { width?, height? } in positive px`
2651
+ );
2652
+ }
2653
+ }
2654
+ if (def.windowOnly != null && typeof def.windowOnly !== "boolean") {
2655
+ throw new TypeError(`createTaxonomy: '${kind}'.windowOnly must be true or false`);
2656
+ }
2657
+ if (def.windowOnly && def.isTopNav) {
2658
+ throw new Error(
2659
+ `createTaxonomy: '${kind}' is a top-nav page and cannot be windowOnly \u2014 a page lives in tiles`
2660
+ );
2661
+ }
2662
+ }
2663
+ if (kinds[root].windowOnly) {
2664
+ throw new Error(
2665
+ `createTaxonomy: the root kind '${root}' cannot be windowOnly \u2014 every new pane, tab and desktop is seeded with it`
2666
+ );
2645
2667
  }
2646
2668
  if (kinds[root].single) {
2647
2669
  throw new Error(
@@ -2713,13 +2735,19 @@ function createTaxonomy({ kinds, root } = {}) {
2713
2735
  const s = _kinds[kind]?.single;
2714
2736
  return s === true || typeof s === "string" && s !== "";
2715
2737
  };
2738
+ const isSinglePerDesktop = (kind) => _kinds[kind]?.single === "desktop";
2716
2739
  const instanceKey = (kind, props) => {
2717
2740
  if (!isSingle(kind)) return null;
2718
2741
  const by = _kinds[kind].single;
2719
- if (by === true) return String(kind);
2742
+ if (by === true || by === "desktop") return String(kind);
2720
2743
  const v = props?.[by];
2721
2744
  return `${kind}\0${v == null ? "" : String(v)}`;
2722
2745
  };
2746
+ const isWindowOnly = (kind) => _kinds[kind]?.windowOnly === true;
2747
+ const windowSize = (kind) => {
2748
+ const w = _kinds[kind]?.window;
2749
+ return w ? { width: w.width ?? null, height: w.height ?? null } : null;
2750
+ };
2723
2751
  const singleReason = (kind) => `${_kinds[kind]?.label || kind} is open once: use the one that is open`;
2724
2752
  return Object.freeze({
2725
2753
  root,
@@ -2734,8 +2762,11 @@ function createTaxonomy({ kinds, root } = {}) {
2734
2762
  panelOffer,
2735
2763
  topNavEntries,
2736
2764
  isSingle,
2765
+ isSinglePerDesktop,
2737
2766
  instanceKey,
2738
- singleReason
2767
+ singleReason,
2768
+ isWindowOnly,
2769
+ windowSize
2739
2770
  });
2740
2771
  }
2741
2772
 
@@ -5461,7 +5492,11 @@ var WindowManager = class _WindowManager {
5461
5492
  }
5462
5493
  }
5463
5494
  }
5464
- const deduped = this._dropDuplicateSingles();
5495
+ const windowOnlyDropped = this._dropWindowOnlyTabs();
5496
+ if (windowOnlyDropped.length) {
5497
+ (this.logger ?? console).warn?.(`[wm] the saved layout held ${windowOnlyDropped.length} tile tab(s) of window-only kind(s) ${[...new Set(windowOnlyDropped)].join(", ")}; they open in a window when asked for`);
5498
+ }
5499
+ const deduped = this._dropDuplicateSingles() || windowOnlyDropped.length > 0;
5465
5500
  for (const d of this.desktops.desktops) this._canonicalize(d.tree, d);
5466
5501
  this.renderer.tree = this.desktops.active().tree;
5467
5502
  this.renderer.render();
@@ -5493,6 +5528,28 @@ var WindowManager = class _WindowManager {
5493
5528
  //
5494
5529
  // MOVE verbs (float, dock, drag a tab, move to desktop) carry the one
5495
5530
  // instance somewhere else and are untouched.
5531
+ //
5532
+ // `single: 'desktop'` keeps the same promises per virtual desktop: a
5533
+ // desktop is the tiles and panels the user arranged on it, so opening the
5534
+ // kind looks only at the ACTIVE desktop — its tiles, their archived pages
5535
+ // and the windows on it — and opens it there when that desktop has none.
5536
+ // A window is on the desktop it was opened on or moved to (`desktopIdx`):
5537
+ // a free-floating one is drawn over every desktop, but it is that one
5538
+ // desktop's instance, so it never stands in for another desktop's. The
5539
+ // move verbs refuse to carry the kind onto a desktop that already holds
5540
+ // it (`desktopMoveRefusal`). Navigating inside a window looks on the
5541
+ // window's own desktop (`openInWindow`). Removing a desktop brings its
5542
+ // windows across, a window never being destroyed by a tile operation, so
5543
+ // the active desktop can then hold the kind twice until the next reload
5544
+ // keeps the first.
5545
+ /** The key a tab's instance is unique under on desktop `desktopIdx`: its
5546
+ * instance key, scoped to that desktop for a `single: 'desktop'` kind;
5547
+ * null for a tab that is not single. */
5548
+ _scopedKey(t, desktopIdx) {
5549
+ const k = t ? this._instanceKey(t.kind, t.props) : null;
5550
+ if (k == null || !this.taxonomy.isSinglePerDesktop(t.kind)) return k;
5551
+ return `${desktopIdx}\0${k}`;
5552
+ }
5496
5553
  /** The instance key of a tab spec, or null when its kind is not single. */
5497
5554
  _instanceKey(kind, props) {
5498
5555
  return this.taxonomy.instanceKey(kind, props);
@@ -5502,27 +5559,36 @@ var WindowManager = class _WindowManager {
5502
5559
  return this.taxonomy.isSingle(kind);
5503
5560
  }
5504
5561
  /** Where the one instance of a single kind is open, or null — also null
5505
- * for a kind that is not single. One of:
5562
+ * for a kind that is not single. A `single: 'desktop'` kind is looked for
5563
+ * on the active desktop only. One of:
5506
5564
  * `{ where: 'window', windowId, idx, desktopIdx }`
5507
5565
  * `{ where: 'tab', desktopIdx, leafId, idx }`
5508
5566
  * `{ where: 'archive', desktopIdx, leafId, page, idx }` — a tab of a
5509
5567
  * top-nav page the tile has switched away from (`pageTabs`). */
5510
5568
  findInstance(kind, props = {}) {
5569
+ const on = this.taxonomy.isSinglePerDesktop(kind) ? this.desktops.activeIdx : null;
5570
+ return this._findInstance(kind, props, on);
5571
+ }
5572
+ /** `findInstance`, looking only at desktop `on` when it is a number, and
5573
+ * past the window `exceptWindowId`. */
5574
+ _findInstance(kind, props, on, exceptWindowId = null) {
5511
5575
  const key = this._instanceKey(kind, props);
5512
5576
  if (key == null) return null;
5513
5577
  const same = (t) => !!t && this._instanceKey(t.kind, t.props) === key;
5578
+ const desktops = [...this.desktops.desktops.entries()].filter(([desktopIdx]) => on == null || desktopIdx === on);
5514
5579
  for (const [windowId, rec] of this._windowToLeaf) {
5580
+ if (windowId === exceptWindowId) continue;
5581
+ if (on != null && rec.desktopIdx !== on) continue;
5515
5582
  const idx = _windowTabSpecs(rec).findIndex(same);
5516
5583
  if (idx >= 0) return { where: "window", windowId, idx, desktopIdx: rec.desktopIdx };
5517
5584
  }
5518
- const desktops = this.desktops.desktops;
5519
- for (const [desktopIdx, d] of desktops.entries()) {
5585
+ for (const [desktopIdx, d] of desktops) {
5520
5586
  for (const leaf of d.tree.leaves()) {
5521
5587
  const idx = (leaf.tabs || []).findIndex(same);
5522
5588
  if (idx >= 0) return { where: "tab", desktopIdx, leafId: leaf.id, idx };
5523
5589
  }
5524
5590
  }
5525
- for (const [desktopIdx, d] of desktops.entries()) {
5591
+ for (const [desktopIdx, d] of desktops) {
5526
5592
  for (const leaf of d.tree.leaves()) {
5527
5593
  for (const [page, arc] of this._archivedPages(leaf)) {
5528
5594
  const idx = arc.tabs.findIndex(same);
@@ -5532,6 +5598,30 @@ var WindowManager = class _WindowManager {
5532
5598
  }
5533
5599
  return null;
5534
5600
  }
5601
+ /** Why moving a pane's or a window's tabs onto desktop `idx` is refused,
5602
+ * or null: that desktop already holds one of their `single: 'desktop'`
5603
+ * kinds (in a tile, an archived page or a window), and a desktop holds
5604
+ * such a kind once. `source` is `{ leafId }` (a tile of the active
5605
+ * desktop) or `{ windowId }`. The move verbs ask it, and their menu rows
5606
+ * are greyed with its sentence. */
5607
+ desktopMoveRefusal(source, idx) {
5608
+ let specs = [];
5609
+ if (source?.windowId) {
5610
+ const rec = this._windowToLeaf.get(source.windowId);
5611
+ specs = rec ? _windowTabSpecs(rec) : [];
5612
+ } else {
5613
+ const leaf = this._tree().get(source?.leafId);
5614
+ specs = leaf?.tabs?.length ? leaf.tabs : leaf?.content ? [leaf.content] : [];
5615
+ }
5616
+ const d = this.desktops.desktops[idx];
5617
+ if (!d) return null;
5618
+ for (const t of specs) {
5619
+ if (!t || !this.taxonomy.isSinglePerDesktop(t.kind)) continue;
5620
+ if (!this._findInstance(t.kind, t.props, idx, source.windowId ?? null)) continue;
5621
+ return `${this.taxonomy.kinds[t.kind]?.label || t.kind} is open on desktop ${d.label} already`;
5622
+ }
5623
+ return null;
5624
+ }
5535
5625
  /** Reveal the instance of a single kind that is open: answers whether
5536
5626
  * there was one. Every open path asks this first.
5537
5627
  *
@@ -5545,7 +5635,11 @@ var WindowManager = class _WindowManager {
5545
5635
  * open names no props at all — a top-nav button, `openInPrimary(kind)` —
5546
5636
  * which asks to SEE the instance, not to empty it. */
5547
5637
  revealInstance(kind, props = {}) {
5548
- const loc = this.findInstance(kind, props);
5638
+ return this._revealFound(this.findInstance(kind, props), kind, props);
5639
+ }
5640
+ /** Reveal the instance at `loc` (a `findInstance` result, or null) and
5641
+ * hand it `props`. Answers whether there was one to reveal. */
5642
+ _revealFound(loc, kind, props) {
5549
5643
  if (!loc || !this._revealAt(loc)) return false;
5550
5644
  if (props && Object.keys(props).length) this._deliverProps(kind, props);
5551
5645
  return true;
@@ -5658,7 +5752,8 @@ var WindowManager = class _WindowManager {
5658
5752
  * soon as the tile leaves the page by a path other than `swapToPage`
5659
5753
  * (`navigateActiveTab`). So a restored tab whose instance is live
5660
5754
  * anywhere else is dropped from the tile and from the restored archive;
5661
- * the instance stays where it is. */
5755
+ * the instance stays where it is. For a `single: 'desktop'` kind,
5756
+ * anywhere else on the tile's desktop. */
5662
5757
  _swapToPage(tree, leafId, target, currentTopNav, targetTopNav, opts) {
5663
5758
  const done = tree.swapToPage(leafId, target, currentTopNav, targetTopNav, opts);
5664
5759
  if (done) this._dropRestoredCopies(tree, leafId, targetTopNav, target);
@@ -5668,11 +5763,12 @@ var WindowManager = class _WindowManager {
5668
5763
  const leaf = tree.get(leafId);
5669
5764
  const tabs = Array.isArray(leaf?.tabs) ? leaf.tabs : null;
5670
5765
  if (!tabs) return;
5766
+ const desktopIdx = this.desktops.desktops.findIndex((d) => d.tree === tree);
5671
5767
  const elsewhere = this._liveInstanceKeys(tree, leafId);
5672
5768
  const keeper = () => {
5673
5769
  const seen = /* @__PURE__ */ new Set();
5674
5770
  return (t) => {
5675
- const k = t ? this._instanceKey(t.kind, t.props) : null;
5771
+ const k = this._scopedKey(t, desktopIdx);
5676
5772
  if (k == null) return true;
5677
5773
  if (elsewhere.has(k) || seen.has(k)) return false;
5678
5774
  seen.add(k);
@@ -5711,16 +5807,23 @@ var WindowManager = class _WindowManager {
5711
5807
  }
5712
5808
  tree.setActiveLeafTab(leafId, Math.max(0, Math.min(tabs.length - 1, active)));
5713
5809
  }
5714
- /** The instance keys of every LIVE tab — windows, and every leaf of every
5715
- * desktop but `leafId` of `tree`. Archived pages are not live. */
5810
+ /** The scoped keys (`_scopedKey`, for `tree`'s desktop) of every LIVE
5811
+ * tab — windows, and every leaf of every desktop but `leafId` of `tree`;
5812
+ * a `single: 'desktop'` tab counts only when it is on `tree`'s desktop.
5813
+ * Archived pages are not live. */
5716
5814
  _liveInstanceKeys(tree, leafId) {
5815
+ const here = this.desktops.desktops.findIndex((d) => d.tree === tree);
5717
5816
  const keys = /* @__PURE__ */ new Set();
5718
- const add = (t) => {
5719
- const k = t ? this._instanceKey(t.kind, t.props) : null;
5817
+ const adder = (onHere) => (t) => {
5818
+ if (!onHere && t && this.taxonomy.isSinglePerDesktop(t.kind)) return;
5819
+ const k = this._scopedKey(t, here);
5720
5820
  if (k != null) keys.add(k);
5721
5821
  };
5722
- for (const [, rec] of this._windowToLeaf) _windowTabSpecs(rec).forEach(add);
5822
+ for (const [, rec] of this._windowToLeaf) {
5823
+ _windowTabSpecs(rec).forEach(adder(rec.desktopIdx === here));
5824
+ }
5723
5825
  for (const d of this.desktops.desktops) {
5826
+ const add = adder(d.tree === tree);
5724
5827
  for (const leaf of d.tree.leaves()) {
5725
5828
  if (d.tree === tree && leaf.id === leafId) continue;
5726
5829
  (leaf.tabs || []).forEach(add);
@@ -5728,28 +5831,62 @@ var WindowManager = class _WindowManager {
5728
5831
  }
5729
5832
  return keys;
5730
5833
  }
5731
- /** A restored layout keeps the FIRST instance of each single kind: later
5732
- * live tabs are dropped (the tile shows the tab that took the dropped
5733
- * one's place, or closes when it held nothing else), and so are later
5734
- * archived ones. Answers whether anything was dropped. */
5834
+ /** Open a window-only kind (KindDef `windowOnly`) in its window — the one
5835
+ * that is open, or a new one — and say so; any other kind: false, and the
5836
+ * caller opens it where it asked (STORY-0127). */
5837
+ _openWindowOnly(kind, props) {
5838
+ if (!this.taxonomy.isWindowOnly?.(kind)) return false;
5839
+ this._navigateWindow(kind, props);
5840
+ return true;
5841
+ }
5842
+ /** Whether a window record holds a window-only kind: such a window never
5843
+ * docks into a tile (Back to tile, maximize, drag-to-dock). */
5844
+ _holdsWindowOnly(rec) {
5845
+ return !!this.taxonomy.isWindowOnly && _windowTabSpecs(rec).some((t) => this.taxonomy.isWindowOnly(t?.kind));
5846
+ }
5847
+ /** A restored layout never holds a window-only kind in a tile: its tabs,
5848
+ * and its tabs archived under a page, go. Windows are not restored, so
5849
+ * the content simply opens in a window the next time it is asked for.
5850
+ * Returns the kinds of the tabs that went, one entry per tab. */
5851
+ _dropWindowOnlyTabs() {
5852
+ if (!this.taxonomy.isWindowOnly) return [];
5853
+ const dropped = [];
5854
+ this._pruneRestoredTabs((t) => {
5855
+ if (!t || !this.taxonomy.isWindowOnly(t.kind)) return true;
5856
+ dropped.push(t.kind);
5857
+ return false;
5858
+ });
5859
+ return dropped;
5860
+ }
5861
+ /** A restored layout keeps the FIRST instance of each single kind (of a
5862
+ * `single: 'desktop'` kind, on each desktop): later live tabs are
5863
+ * dropped (the tile shows the tab that took the dropped one's place, or
5864
+ * closes when it held nothing else), and so are later archived ones.
5865
+ * Answers whether anything was dropped. */
5735
5866
  _dropDuplicateSingles() {
5736
5867
  const seen = /* @__PURE__ */ new Set();
5737
- let dropped = false;
5738
- const keep = (t) => {
5739
- const k = t ? this._instanceKey(t.kind, t.props) : null;
5868
+ return this._pruneRestoredTabs((t, desktopIdx) => {
5869
+ const k = this._scopedKey(t, desktopIdx);
5740
5870
  if (k == null) return true;
5741
5871
  if (seen.has(k)) return false;
5742
5872
  seen.add(k);
5743
5873
  return true;
5744
- };
5745
- for (const d of this.desktops.desktops) {
5874
+ });
5875
+ }
5876
+ /** Drop every tab of the restored layout `keep(tab, desktopIdx)` refuses,
5877
+ * in the tiles and in their archived pages; a tile left empty closes
5878
+ * (seeding a content tile if none is left). Returns whether anything
5879
+ * went. */
5880
+ _pruneRestoredTabs(keep) {
5881
+ let dropped = false;
5882
+ for (const [desktopIdx, d] of this.desktops.desktops.entries()) {
5746
5883
  const tree = d.tree;
5747
5884
  for (const leaf of tree.leaves()) {
5748
5885
  const tabs = Array.isArray(leaf.tabs) ? leaf.tabs : [];
5749
5886
  let active = leaf.activeTabIdx || 0;
5750
5887
  let changed = false;
5751
5888
  for (let i = 0; i < tabs.length; ) {
5752
- if (keep(tabs[i])) {
5889
+ if (keep(tabs[i], desktopIdx)) {
5753
5890
  i++;
5754
5891
  continue;
5755
5892
  }
@@ -5771,10 +5908,10 @@ var WindowManager = class _WindowManager {
5771
5908
  }
5772
5909
  }
5773
5910
  }
5774
- for (const d of this.desktops.desktops) {
5911
+ for (const [desktopIdx, d] of this.desktops.desktops.entries()) {
5775
5912
  for (const leaf of d.tree.leaves()) {
5776
5913
  for (const [page, arc] of this._archivedPages(leaf)) {
5777
- const kept = arc.tabs.filter(keep);
5914
+ const kept = arc.tabs.filter((t) => keep(t, desktopIdx));
5778
5915
  if (kept.length === arc.tabs.length) continue;
5779
5916
  dropped = true;
5780
5917
  if (!kept.length) {
@@ -5981,6 +6118,7 @@ var WindowManager = class _WindowManager {
5981
6118
  const tree = this._tree();
5982
6119
  const prior = tree.popActiveTabHistory?.(id);
5983
6120
  if (!prior || !prior.kind) return false;
6121
+ if (this._openWindowOnly(prior.kind, prior.props || {})) return true;
5984
6122
  if (this.revealInstance(prior.kind, prior.props || {})) return true;
5985
6123
  const curKind = tree.get(id)?.content?.kind;
5986
6124
  const curTopNav = curKind ? this.taxonomy.topNavFor(curKind) : null;
@@ -6097,6 +6235,7 @@ var WindowManager = class _WindowManager {
6097
6235
  * the leaf. Used by Backspace + breadcrumb segments + anything
6098
6236
  * else that should walk WITHIN the tile rather than reset it. */
6099
6237
  navigateActiveTab(leafId, kind, props = {}) {
6238
+ if (this._openWindowOnly(kind, props)) return;
6100
6239
  if (this.revealInstance(kind, props)) return;
6101
6240
  const tree = this._tree();
6102
6241
  const leaf = tree.get(leafId);
@@ -6151,6 +6290,7 @@ var WindowManager = class _WindowManager {
6151
6290
  * tab feature shipped), the behavior is identical to the old
6152
6291
  * setLeafContent: one tab in, one tab out. */
6153
6292
  openInLeaf(leafId, kind, props = {}) {
6293
+ if (this._openWindowOnly(kind, props)) return;
6154
6294
  if (this.revealInstance(kind, props)) return;
6155
6295
  const tree = this._tree();
6156
6296
  const leaf = tree.get(leafId);
@@ -6168,12 +6308,14 @@ var WindowManager = class _WindowManager {
6168
6308
  * previous mount, mounts the new kind into the same body element,
6169
6309
  * and updates the window's title. */
6170
6310
  openInWindow(winId, kind, props = {}) {
6171
- if (this.revealInstance(kind, props)) return;
6311
+ if (this._openWindowOnly(kind, props)) return;
6172
6312
  const rec = this._windowToLeaf.get(winId);
6173
6313
  if (!rec) {
6174
6314
  this.openInPrimary(kind, props);
6175
6315
  return;
6176
6316
  }
6317
+ const loc = this.taxonomy.isSinglePerDesktop(kind) ? this._findInstance(kind, props, rec.desktopIdx) : this.findInstance(kind, props);
6318
+ if (this._revealFound(loc, kind, props)) return;
6177
6319
  try {
6178
6320
  rec.mountInfo?.destroy?.();
6179
6321
  } catch {
@@ -6207,6 +6349,7 @@ var WindowManager = class _WindowManager {
6207
6349
  * If only panel tiles exist (or the tree is empty), spawn a new
6208
6350
  * content leaf and canonicalize so the panels wrap it. */
6209
6351
  openInPrimary(kind, props = {}) {
6352
+ if (this._openWindowOnly(kind, props)) return;
6210
6353
  if (this.revealInstance(kind, props)) return;
6211
6354
  const tree = this._tree();
6212
6355
  let leafId = tree.primaryLeafId();
@@ -6267,6 +6410,7 @@ var WindowManager = class _WindowManager {
6267
6410
  * pane in a split never dismounts the original). Returns the new
6268
6411
  * leaf id, or null if the leaf can't be split. */
6269
6412
  splitLeafWith(leafId, dir, kind, props = {}, title = "") {
6413
+ if (this._openWindowOnly(kind, props)) return null;
6270
6414
  if (this.revealInstance(kind, props)) return null;
6271
6415
  const tree = this._tree();
6272
6416
  leafId = this._contentLeafId(leafId);
@@ -6676,6 +6820,9 @@ var WindowManager = class _WindowManager {
6676
6820
  canMaximize: true,
6677
6821
  canResize: true,
6678
6822
  modal: false,
6823
+ // BUG-0053. The id is minted per window and never opens again, so
6824
+ // geometry saved under it could never be restored.
6825
+ persistGeometry: false,
6679
6826
  // C15. Dropping a promoted window on a tile PUTS IT BACK — as that
6680
6827
  // tile's content, as a split of it, or as one of its tabs. Off
6681
6828
  // unless the embedder asked, because it changes what a drag to an
@@ -7130,6 +7277,7 @@ var WindowManager = class _WindowManager {
7130
7277
  bringBackWindow(windowId) {
7131
7278
  const rec = this._windowToLeaf.get(windowId);
7132
7279
  if (!rec) return false;
7280
+ if (this._holdsWindowOnly(rec)) return false;
7133
7281
  const tree = this.desktops.desktops[rec.desktopIdx]?.tree;
7134
7282
  const home = rec.homeLeafId ? tree?.get(rec.homeLeafId) : null;
7135
7283
  if (home && home.kind === "leaf") {
@@ -7718,6 +7866,7 @@ var WindowManager = class _WindowManager {
7718
7866
  win.clearSnapPreview();
7719
7867
  return true;
7720
7868
  }
7869
+ if (typeof desktopIdx === "number" && desktopIdx !== record.desktopIdx && this.desktopMoveRefusal({ windowId: winId }, desktopIdx)) return false;
7721
7870
  if (typeof desktopIdx === "number") record.desktopIdx = desktopIdx;
7722
7871
  const docked = this.dockWindowInto(winId, {
7723
7872
  leafId,
@@ -7748,6 +7897,7 @@ var WindowManager = class _WindowManager {
7748
7897
  dockWindowInto(windowId, target) {
7749
7898
  const rec = this._windowToLeaf.get(windowId);
7750
7899
  if (!rec || !target?.leafId) return false;
7900
+ if (this._holdsWindowOnly(rec)) return false;
7751
7901
  rec._dock = { ...target };
7752
7902
  rec._demoting = true;
7753
7903
  try {
@@ -7790,6 +7940,7 @@ var WindowManager = class _WindowManager {
7790
7940
  if (!rec) return;
7791
7941
  if (rec.desktopIdx === targetIdx) return;
7792
7942
  if (this.desktops.desktops[targetIdx]?.fixed) return;
7943
+ if (this.desktopMoveRefusal({ windowId }, targetIdx)) return;
7793
7944
  this.desktops.ensureCount(targetIdx + 1);
7794
7945
  rec.desktopIdx = targetIdx;
7795
7946
  if (!rec.homeContainer && rec.homeLeafId) {
@@ -7917,17 +8068,20 @@ var WindowManager = class _WindowManager {
7917
8068
  e.preventDefault();
7918
8069
  const rec = this._windowToLeaf.get(winId);
7919
8070
  if (!rec) return;
7920
- const items = [
8071
+ const items = this._holdsWindowOnly(rec) ? [] : [
7921
8072
  { label: "Back to tile", icon: "close_fullscreen", action: "back" }
7922
8073
  ];
7923
8074
  const targets = [...this.desktops.desktops.entries()].filter(([i, d]) => i !== rec.desktopIdx && !d.fixed);
7924
8075
  if (targets.length) {
7925
8076
  items.push({ separator: true });
7926
8077
  for (const [i, d] of targets) {
8078
+ const refusal = this.desktopMoveRefusal({ windowId: winId }, i);
7927
8079
  items.push({
7928
8080
  label: `Move to desktop ${d.label}`,
7929
8081
  icon: "sweep",
7930
- action: `move:${i}`
8082
+ action: `move:${i}`,
8083
+ disabled: !!refusal,
8084
+ title: refusal || void 0
7931
8085
  });
7932
8086
  }
7933
8087
  }
@@ -8426,7 +8580,9 @@ var WindowManager = class _WindowManager {
8426
8580
  * ruling is that a tile operation may move a window and never destroy it,
8427
8581
  * and removing a desktop is the largest tile operation there is: they come
8428
8582
  * across to the desktop that ends up active, re-homed onto its ground by
8429
- * `_rehomeContainedWindows` on the render below.
8583
+ * `_rehomeContainedWindows` on the render below. One holding a
8584
+ * `single: 'desktop'` kind the active desktop already has makes it hold
8585
+ * the kind twice, until a reload keeps the first.
8430
8586
  */
8431
8587
  removeDesktop(idx) {
8432
8588
  const m = this.desktops;
@@ -8474,6 +8630,7 @@ var WindowManager = class _WindowManager {
8474
8630
  if (closeChrome === false || closeChrome?.disabled === true) return;
8475
8631
  if (this.desktops.desktops[idx]?.fixed) return;
8476
8632
  if (idx === this.desktops.activeIdx) return;
8633
+ if (this.desktopMoveRefusal({ leafId: focused.id }, idx)) return;
8477
8634
  this.desktops.ensureCount(idx + 1);
8478
8635
  const target = this.desktops.desktops[idx];
8479
8636
  const tabs = Array.isArray(focused.tabs) && focused.tabs.length ? focused.tabs : [{ kind: focused.content.kind, props: focused.content.props, title: focused.title }];
@@ -8657,6 +8814,7 @@ var WindowManager = class _WindowManager {
8657
8814
  }
8658
8815
  /** The routing behind `navigate`, for a kind known to be registered. */
8659
8816
  _route(kind, props, opts) {
8817
+ if (this._openWindowOnly(kind, props)) return;
8660
8818
  const { ctx = null, transient = false } = opts;
8661
8819
  const background = !!opts.background;
8662
8820
  let { dest = "main", newTab = false } = opts;
@@ -8725,6 +8883,8 @@ var WindowManager = class _WindowManager {
8725
8883
  * source leaf — closing the window just disposes the content. */
8726
8884
  _navigateWindow(kind, props) {
8727
8885
  if (this.revealInstance(kind, props)) return;
8886
+ const size = this.taxonomy.windowSize?.(kind);
8887
+ const dockable = !this.taxonomy.isWindowOnly?.(kind);
8728
8888
  const winId = `twm-mw-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 5)}`;
8729
8889
  const contentEl = document.createElement("div");
8730
8890
  contentEl.className = "twm-window-content";
@@ -8741,17 +8901,22 @@ var WindowManager = class _WindowManager {
8741
8901
  title: mountInfo?.title || title,
8742
8902
  icon: "web_asset",
8743
8903
  content: contentEl,
8904
+ ...size?.width ? { defaultWidth: size.width } : {},
8905
+ ...size?.height ? { defaultHeight: size.height } : {},
8744
8906
  canMinimize: true,
8745
8907
  canMaximize: true,
8746
8908
  canResize: true,
8747
8909
  modal: false,
8910
+ // BUG-0053. The id is minted per window and never opens again, so
8911
+ // geometry saved under it could never be restored.
8912
+ persistGeometry: false,
8748
8913
  // C15, and this is the SECOND of the two places the WM builds a
8749
8914
  // window. Alt+N and "Open in new window" produce a window that is
8750
8915
  // every bit as dockable as a promoted one, and a window that can be
8751
8916
  // dragged onto a tile in one case and not the other is a rule
8752
8917
  // nobody can learn.
8753
- snap: this.snapPromotion,
8754
- snapController: this.snapPromotion ? this._snapController() : null,
8918
+ snap: dockable && this.snapPromotion,
8919
+ snapController: dockable && this.snapPromotion ? this._snapController() : null,
8755
8920
  // R1. THE PANE IS A BOX WITH `overflow: hidden`. A window contained
8756
8921
  // to one (C21) cannot be dragged a single pixel outside it, so
8757
8922
  // "drag a window from one tile to another" — the gesture all three
@@ -8769,9 +8934,11 @@ var WindowManager = class _WindowManager {
8769
8934
  // harder. So the maximize button docks — and the separate demote
8770
8935
  // button the WM used to inject beside it is gone, because two
8771
8936
  // buttons for one verb is how you get a chrome nobody reads.
8772
- onMaximize: () => this.bringBackWindow(winId),
8773
- maximizeIcon: "close_fullscreen",
8774
- maximizeTitle: "Back to tile",
8937
+ ...dockable ? {
8938
+ onMaximize: () => this.bringBackWindow(winId),
8939
+ maximizeIcon: "close_fullscreen",
8940
+ maximizeTitle: "Back to tile"
8941
+ } : {},
8775
8942
  onClose: () => this._onManagedWindowClosed(winId, mountInfo)
8776
8943
  });
8777
8944
  this._windowToLeaf.set(winId, {
@@ -8842,6 +9009,7 @@ var WindowManager = class _WindowManager {
8842
9009
  * routing) but uses `appendLeafTab` so the existing content
8843
9010
  * stays in place as a tab. */
8844
9011
  openInTabFromContext(ctx, kind, props = {}, transient = false, background = false) {
9012
+ if (this._openWindowOnly(kind, props)) return;
8845
9013
  if (this.revealInstance(kind, props)) return;
8846
9014
  if (ctx?.windowId && this._windowToLeaf.has(ctx.windowId)) {
8847
9015
  this.openInWindow(ctx.windowId, kind, props);
@@ -8875,6 +9043,7 @@ var WindowManager = class _WindowManager {
8875
9043
  * "Add row" button, which passes no ctx) reliably lands as a sibling
8876
9044
  * tab in the main tile rather than swapping its content. */
8877
9045
  openInTabInPrimary(kind, props = {}, transient = false, background = false) {
9046
+ if (this._openWindowOnly(kind, props)) return;
8878
9047
  if (this.revealInstance(kind, props)) return;
8879
9048
  const tree = this._tree();
8880
9049
  const leafId = tree.primaryLeafId();
@@ -9970,10 +10139,13 @@ function _tileContextMenu(wm, leafId, x, y) {
9970
10139
  if (wm.desktops.desktops.length > 1 && !isPanel) {
9971
10140
  for (const [i, d] of wm.desktops.desktops.entries()) {
9972
10141
  if (i === wm.desktops.activeIdx || d.fixed) continue;
10142
+ const refusal = wm.desktopMoveRefusal({ leafId }, i);
9973
10143
  items.push({
9974
10144
  label: `Move to desktop ${d.label}`,
9975
10145
  icon: "sweep",
9976
- action: `move:${i}`
10146
+ action: `move:${i}`,
10147
+ disabled: !!refusal,
10148
+ title: refusal || void 0
9977
10149
  });
9978
10150
  }
9979
10151
  }