flexdesk 0.17.14 → 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.
Files changed (47) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/css/base.css +237 -8
  3. package/css/flexdesk.css +344 -17
  4. package/css/overrides.css +6 -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-IG74S3ZH.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-ZMH3OC7H.js → chunk-XJNADRGR.js} +4 -2
  13. package/dist/chunk-XJNADRGR.js.map +7 -0
  14. package/dist/flexdesk.css +344 -17
  15. package/dist/flow.js +3 -3
  16. package/dist/tiles.js +84 -9
  17. package/dist/tiles.js.map +2 -2
  18. package/dist/widgets.js +448 -5
  19. package/dist/widgets.js.map +4 -4
  20. package/dist/wm.js +110 -19
  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 +80 -2
  25. package/docs/components/menus.md +85 -1
  26. package/docs/components/shell.md +52 -0
  27. package/docs/components/slide-out-panel.md +15 -0
  28. package/docs/components/tiles.md +37 -0
  29. package/package.json +1 -1
  30. package/src/charts/plot_config_editor.js +125 -29
  31. package/src/tiles/tile_base.js +48 -2
  32. package/src/tiles/tile_grid.js +42 -1
  33. package/src/tiling/kind_taxonomy.js +45 -0
  34. package/src/tiling/tile_breadcrumb.js +12 -4
  35. package/src/tiling/wm.js +86 -11
  36. package/src/ui/components/category_picker.js +556 -0
  37. package/src/ui/components/computing_status_window.js +3 -0
  38. package/src/ui/components/confirm_dialog.js +3 -0
  39. package/src/ui/components/managed_window.js +204 -9
  40. package/src/ui/components/modal.js +11 -2
  41. package/src/ui/components/wizard.js +4 -1
  42. package/widgets.js +2 -1
  43. package/dist/chunk-MADMGRX2.js.map +0 -7
  44. package/dist/chunk-ZMH3OC7H.js.map +0 -7
  45. /package/dist/{chunk-2PM6ZMNS.js.map → chunk-2Z2ENHR3.js.map} +0 -0
  46. /package/dist/{chunk-IG74S3ZH.js.map → chunk-7JI7HHSY.js.map} +0 -0
  47. /package/dist/{chunk-TLIV6QSU.js.map → chunk-7OL6F2AM.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-IG74S3ZH.js";
11
+ } from "./chunk-7JI7HHSY.js";
12
12
  import {
13
13
  openForm
14
- } from "./chunk-ZMH3OC7H.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(
@@ -2720,6 +2742,11 @@ function createTaxonomy({ kinds, root } = {}) {
2720
2742
  const v = props?.[by];
2721
2743
  return `${kind}\0${v == null ? "" : String(v)}`;
2722
2744
  };
2745
+ const isWindowOnly = (kind) => _kinds[kind]?.windowOnly === true;
2746
+ const windowSize = (kind) => {
2747
+ const w = _kinds[kind]?.window;
2748
+ return w ? { width: w.width ?? null, height: w.height ?? null } : null;
2749
+ };
2723
2750
  const singleReason = (kind) => `${_kinds[kind]?.label || kind} is open once: use the one that is open`;
2724
2751
  return Object.freeze({
2725
2752
  root,
@@ -2735,7 +2762,9 @@ function createTaxonomy({ kinds, root } = {}) {
2735
2762
  topNavEntries,
2736
2763
  isSingle,
2737
2764
  instanceKey,
2738
- singleReason
2765
+ singleReason,
2766
+ isWindowOnly,
2767
+ windowSize
2739
2768
  });
2740
2769
  }
2741
2770
 
@@ -3471,7 +3500,11 @@ function mountTileBreadcrumb(kind, props, ctx) {
3471
3500
  const rootCrumb = ctx?.rootCrumb || null;
3472
3501
  if (!taxonomy) {
3473
3502
  console.error("[breadcrumb] no taxonomy in ctx \u2014 cannot render");
3474
- return { el: document.createElement("nav"), destroy: () => {
3503
+ const el = document.createElement("nav");
3504
+ el.className = "twm-topbar-breadcrumb twm-tile-breadcrumb";
3505
+ el.hidden = true;
3506
+ return { el, refresh: () => {
3507
+ }, destroy: () => {
3475
3508
  } };
3476
3509
  }
3477
3510
  const root = document.createElement("nav");
@@ -3505,10 +3538,9 @@ function mountTileBreadcrumb(kind, props, ctx) {
3505
3538
  }
3506
3539
  };
3507
3540
  const render = () => {
3508
- _renderInto(
3509
- root,
3510
- _segments(kind, props, taxonomy, rootCrumb, rootLabel, navigate, trailOf())
3511
- );
3541
+ const segments = _segments(kind, props, taxonomy, rootCrumb, rootLabel, navigate, trailOf());
3542
+ _renderInto(root, segments);
3543
+ root.hidden = segments.length < 2;
3512
3544
  };
3513
3545
  render();
3514
3546
  let unsubscribe = null;
@@ -5458,7 +5490,11 @@ var WindowManager = class _WindowManager {
5458
5490
  }
5459
5491
  }
5460
5492
  }
5461
- const deduped = this._dropDuplicateSingles();
5493
+ const windowOnlyDropped = this._dropWindowOnlyTabs();
5494
+ if (windowOnlyDropped.length) {
5495
+ (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`);
5496
+ }
5497
+ const deduped = this._dropDuplicateSingles() || windowOnlyDropped.length > 0;
5462
5498
  for (const d of this.desktops.desktops) this._canonicalize(d.tree, d);
5463
5499
  this.renderer.tree = this.desktops.active().tree;
5464
5500
  this.renderer.render();
@@ -5725,20 +5761,52 @@ var WindowManager = class _WindowManager {
5725
5761
  }
5726
5762
  return keys;
5727
5763
  }
5764
+ /** Open a window-only kind (KindDef `windowOnly`) in its window — the one
5765
+ * that is open, or a new one — and say so; any other kind: false, and the
5766
+ * caller opens it where it asked (STORY-0127). */
5767
+ _openWindowOnly(kind, props) {
5768
+ if (!this.taxonomy.isWindowOnly?.(kind)) return false;
5769
+ this._navigateWindow(kind, props);
5770
+ return true;
5771
+ }
5772
+ /** Whether a window record holds a window-only kind: such a window never
5773
+ * docks into a tile (Back to tile, maximize, drag-to-dock). */
5774
+ _holdsWindowOnly(rec) {
5775
+ return !!this.taxonomy.isWindowOnly && _windowTabSpecs(rec).some((t) => this.taxonomy.isWindowOnly(t?.kind));
5776
+ }
5777
+ /** A restored layout never holds a window-only kind in a tile: its tabs,
5778
+ * and its tabs archived under a page, go. Windows are not restored, so
5779
+ * the content simply opens in a window the next time it is asked for.
5780
+ * Returns the kinds of the tabs that went, one entry per tab. */
5781
+ _dropWindowOnlyTabs() {
5782
+ if (!this.taxonomy.isWindowOnly) return [];
5783
+ const dropped = [];
5784
+ this._pruneRestoredTabs((t) => {
5785
+ if (!t || !this.taxonomy.isWindowOnly(t.kind)) return true;
5786
+ dropped.push(t.kind);
5787
+ return false;
5788
+ });
5789
+ return dropped;
5790
+ }
5728
5791
  /** A restored layout keeps the FIRST instance of each single kind: later
5729
5792
  * live tabs are dropped (the tile shows the tab that took the dropped
5730
5793
  * one's place, or closes when it held nothing else), and so are later
5731
5794
  * archived ones. Answers whether anything was dropped. */
5732
5795
  _dropDuplicateSingles() {
5733
5796
  const seen = /* @__PURE__ */ new Set();
5734
- let dropped = false;
5735
- const keep = (t) => {
5797
+ return this._pruneRestoredTabs((t) => {
5736
5798
  const k = t ? this._instanceKey(t.kind, t.props) : null;
5737
5799
  if (k == null) return true;
5738
5800
  if (seen.has(k)) return false;
5739
5801
  seen.add(k);
5740
5802
  return true;
5741
- };
5803
+ });
5804
+ }
5805
+ /** Drop every tab of the restored layout `keep(tab)` refuses, in the
5806
+ * tiles and in their archived pages; a tile left empty closes (seeding a
5807
+ * content tile if none is left). Returns whether anything went. */
5808
+ _pruneRestoredTabs(keep) {
5809
+ let dropped = false;
5742
5810
  for (const d of this.desktops.desktops) {
5743
5811
  const tree = d.tree;
5744
5812
  for (const leaf of tree.leaves()) {
@@ -5978,6 +6046,7 @@ var WindowManager = class _WindowManager {
5978
6046
  const tree = this._tree();
5979
6047
  const prior = tree.popActiveTabHistory?.(id);
5980
6048
  if (!prior || !prior.kind) return false;
6049
+ if (this._openWindowOnly(prior.kind, prior.props || {})) return true;
5981
6050
  if (this.revealInstance(prior.kind, prior.props || {})) return true;
5982
6051
  const curKind = tree.get(id)?.content?.kind;
5983
6052
  const curTopNav = curKind ? this.taxonomy.topNavFor(curKind) : null;
@@ -6094,6 +6163,7 @@ var WindowManager = class _WindowManager {
6094
6163
  * the leaf. Used by Backspace + breadcrumb segments + anything
6095
6164
  * else that should walk WITHIN the tile rather than reset it. */
6096
6165
  navigateActiveTab(leafId, kind, props = {}) {
6166
+ if (this._openWindowOnly(kind, props)) return;
6097
6167
  if (this.revealInstance(kind, props)) return;
6098
6168
  const tree = this._tree();
6099
6169
  const leaf = tree.get(leafId);
@@ -6148,6 +6218,7 @@ var WindowManager = class _WindowManager {
6148
6218
  * tab feature shipped), the behavior is identical to the old
6149
6219
  * setLeafContent: one tab in, one tab out. */
6150
6220
  openInLeaf(leafId, kind, props = {}) {
6221
+ if (this._openWindowOnly(kind, props)) return;
6151
6222
  if (this.revealInstance(kind, props)) return;
6152
6223
  const tree = this._tree();
6153
6224
  const leaf = tree.get(leafId);
@@ -6165,6 +6236,7 @@ var WindowManager = class _WindowManager {
6165
6236
  * previous mount, mounts the new kind into the same body element,
6166
6237
  * and updates the window's title. */
6167
6238
  openInWindow(winId, kind, props = {}) {
6239
+ if (this._openWindowOnly(kind, props)) return;
6168
6240
  if (this.revealInstance(kind, props)) return;
6169
6241
  const rec = this._windowToLeaf.get(winId);
6170
6242
  if (!rec) {
@@ -6204,6 +6276,7 @@ var WindowManager = class _WindowManager {
6204
6276
  * If only panel tiles exist (or the tree is empty), spawn a new
6205
6277
  * content leaf and canonicalize so the panels wrap it. */
6206
6278
  openInPrimary(kind, props = {}) {
6279
+ if (this._openWindowOnly(kind, props)) return;
6207
6280
  if (this.revealInstance(kind, props)) return;
6208
6281
  const tree = this._tree();
6209
6282
  let leafId = tree.primaryLeafId();
@@ -6264,6 +6337,7 @@ var WindowManager = class _WindowManager {
6264
6337
  * pane in a split never dismounts the original). Returns the new
6265
6338
  * leaf id, or null if the leaf can't be split. */
6266
6339
  splitLeafWith(leafId, dir, kind, props = {}, title = "") {
6340
+ if (this._openWindowOnly(kind, props)) return null;
6267
6341
  if (this.revealInstance(kind, props)) return null;
6268
6342
  const tree = this._tree();
6269
6343
  leafId = this._contentLeafId(leafId);
@@ -6673,6 +6747,9 @@ var WindowManager = class _WindowManager {
6673
6747
  canMaximize: true,
6674
6748
  canResize: true,
6675
6749
  modal: false,
6750
+ // BUG-0053. The id is minted per window and never opens again, so
6751
+ // geometry saved under it could never be restored.
6752
+ persistGeometry: false,
6676
6753
  // C15. Dropping a promoted window on a tile PUTS IT BACK — as that
6677
6754
  // tile's content, as a split of it, or as one of its tabs. Off
6678
6755
  // unless the embedder asked, because it changes what a drag to an
@@ -7127,6 +7204,7 @@ var WindowManager = class _WindowManager {
7127
7204
  bringBackWindow(windowId) {
7128
7205
  const rec = this._windowToLeaf.get(windowId);
7129
7206
  if (!rec) return false;
7207
+ if (this._holdsWindowOnly(rec)) return false;
7130
7208
  const tree = this.desktops.desktops[rec.desktopIdx]?.tree;
7131
7209
  const home = rec.homeLeafId ? tree?.get(rec.homeLeafId) : null;
7132
7210
  if (home && home.kind === "leaf") {
@@ -7745,6 +7823,7 @@ var WindowManager = class _WindowManager {
7745
7823
  dockWindowInto(windowId, target) {
7746
7824
  const rec = this._windowToLeaf.get(windowId);
7747
7825
  if (!rec || !target?.leafId) return false;
7826
+ if (this._holdsWindowOnly(rec)) return false;
7748
7827
  rec._dock = { ...target };
7749
7828
  rec._demoting = true;
7750
7829
  try {
@@ -7914,7 +7993,7 @@ var WindowManager = class _WindowManager {
7914
7993
  e.preventDefault();
7915
7994
  const rec = this._windowToLeaf.get(winId);
7916
7995
  if (!rec) return;
7917
- const items = [
7996
+ const items = this._holdsWindowOnly(rec) ? [] : [
7918
7997
  { label: "Back to tile", icon: "close_fullscreen", action: "back" }
7919
7998
  ];
7920
7999
  const targets = [...this.desktops.desktops.entries()].filter(([i, d]) => i !== rec.desktopIdx && !d.fixed);
@@ -8654,6 +8733,7 @@ var WindowManager = class _WindowManager {
8654
8733
  }
8655
8734
  /** The routing behind `navigate`, for a kind known to be registered. */
8656
8735
  _route(kind, props, opts) {
8736
+ if (this._openWindowOnly(kind, props)) return;
8657
8737
  const { ctx = null, transient = false } = opts;
8658
8738
  const background = !!opts.background;
8659
8739
  let { dest = "main", newTab = false } = opts;
@@ -8722,6 +8802,8 @@ var WindowManager = class _WindowManager {
8722
8802
  * source leaf — closing the window just disposes the content. */
8723
8803
  _navigateWindow(kind, props) {
8724
8804
  if (this.revealInstance(kind, props)) return;
8805
+ const size = this.taxonomy.windowSize?.(kind);
8806
+ const dockable = !this.taxonomy.isWindowOnly?.(kind);
8725
8807
  const winId = `twm-mw-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 5)}`;
8726
8808
  const contentEl = document.createElement("div");
8727
8809
  contentEl.className = "twm-window-content";
@@ -8738,17 +8820,22 @@ var WindowManager = class _WindowManager {
8738
8820
  title: mountInfo?.title || title,
8739
8821
  icon: "web_asset",
8740
8822
  content: contentEl,
8823
+ ...size?.width ? { defaultWidth: size.width } : {},
8824
+ ...size?.height ? { defaultHeight: size.height } : {},
8741
8825
  canMinimize: true,
8742
8826
  canMaximize: true,
8743
8827
  canResize: true,
8744
8828
  modal: false,
8829
+ // BUG-0053. The id is minted per window and never opens again, so
8830
+ // geometry saved under it could never be restored.
8831
+ persistGeometry: false,
8745
8832
  // C15, and this is the SECOND of the two places the WM builds a
8746
8833
  // window. Alt+N and "Open in new window" produce a window that is
8747
8834
  // every bit as dockable as a promoted one, and a window that can be
8748
8835
  // dragged onto a tile in one case and not the other is a rule
8749
8836
  // nobody can learn.
8750
- snap: this.snapPromotion,
8751
- snapController: this.snapPromotion ? this._snapController() : null,
8837
+ snap: dockable && this.snapPromotion,
8838
+ snapController: dockable && this.snapPromotion ? this._snapController() : null,
8752
8839
  // R1. THE PANE IS A BOX WITH `overflow: hidden`. A window contained
8753
8840
  // to one (C21) cannot be dragged a single pixel outside it, so
8754
8841
  // "drag a window from one tile to another" — the gesture all three
@@ -8766,9 +8853,11 @@ var WindowManager = class _WindowManager {
8766
8853
  // harder. So the maximize button docks — and the separate demote
8767
8854
  // button the WM used to inject beside it is gone, because two
8768
8855
  // buttons for one verb is how you get a chrome nobody reads.
8769
- onMaximize: () => this.bringBackWindow(winId),
8770
- maximizeIcon: "close_fullscreen",
8771
- maximizeTitle: "Back to tile",
8856
+ ...dockable ? {
8857
+ onMaximize: () => this.bringBackWindow(winId),
8858
+ maximizeIcon: "close_fullscreen",
8859
+ maximizeTitle: "Back to tile"
8860
+ } : {},
8772
8861
  onClose: () => this._onManagedWindowClosed(winId, mountInfo)
8773
8862
  });
8774
8863
  this._windowToLeaf.set(winId, {
@@ -8839,6 +8928,7 @@ var WindowManager = class _WindowManager {
8839
8928
  * routing) but uses `appendLeafTab` so the existing content
8840
8929
  * stays in place as a tab. */
8841
8930
  openInTabFromContext(ctx, kind, props = {}, transient = false, background = false) {
8931
+ if (this._openWindowOnly(kind, props)) return;
8842
8932
  if (this.revealInstance(kind, props)) return;
8843
8933
  if (ctx?.windowId && this._windowToLeaf.has(ctx.windowId)) {
8844
8934
  this.openInWindow(ctx.windowId, kind, props);
@@ -8872,6 +8962,7 @@ var WindowManager = class _WindowManager {
8872
8962
  * "Add row" button, which passes no ctx) reliably lands as a sibling
8873
8963
  * tab in the main tile rather than swapping its content. */
8874
8964
  openInTabInPrimary(kind, props = {}, transient = false, background = false) {
8965
+ if (this._openWindowOnly(kind, props)) return;
8875
8966
  if (this.revealInstance(kind, props)) return;
8876
8967
  const tree = this._tree();
8877
8968
  const leafId = tree.primaryLeafId();