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/src/tiling/wm.js CHANGED
@@ -303,8 +303,14 @@ export class WindowManager {
303
303
  }
304
304
  }
305
305
  }
306
- // A single kind is open once, whatever the saved layout says.
307
- const deduped = this._dropDuplicateSingles();
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
- let dropped = false;
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
- const items = [
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
- onMaximize: () => this.bringBackWindow(winId),
4341
- maximizeIcon: 'close_fullscreen',
4342
- maximizeTitle: 'Back to tile',
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();