flexdesk 0.4.1 → 0.4.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flexdesk",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "A tiling window manager and widget set for desktop-class web applications. Zero runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -91,6 +91,10 @@ import { openForm } from '../ui/components/modal.js';
91
91
  * its section that is already open, rather
92
92
  * than rewriting it into a second copy of
93
93
  * that list. Default off.
94
+ * @param {boolean} [cfg.floatActiveTab] C34. Whether floating a tile takes
95
+ * only the tab on screen, leaving its
96
+ * siblings in the tile. Default off: the
97
+ * whole pane floats, strip and all (R8).
94
98
  * @param {'top'|'bottom'} [cfg.tabLayout] C22. Where a multi-tab leaf draws its
95
99
  * tabs. `'bottom'` is the framework's own
96
100
  * spreadsheet strip under the tile body
@@ -128,6 +132,7 @@ export async function createShell({
128
132
  snapPromotion = false,
129
133
  promoteInPlace = false,
130
134
  backToOpenList = false,
135
+ floatActiveTab = false,
131
136
  tabLayout = null,
132
137
  chrome = {},
133
138
  // An embedder that moved its sections out of the top bar — into an icon
@@ -170,6 +175,7 @@ export async function createShell({
170
175
  snapPromotion,
171
176
  promoteInPlace,
172
177
  backToOpenList,
178
+ floatActiveTab,
173
179
  tabLayout,
174
180
  // `ctx` is the delivery vehicle for leaf-mounted chrome: tile_renderer
175
181
  // spreads it into every content factory, which is how the breadcrumb
@@ -528,7 +534,8 @@ function _tileContextMenu(wm, leafId, x, y) {
528
534
  // most of them — a menu disagreeing with its verb in the generous
529
535
  // direction is a dead control; in the mean direction it is a missing
530
536
  // feature, and this file has shipped one of each.
531
- { label: 'Float this pane as a window', icon: 'web_asset',
537
+ { label: wm.floatActiveTab ? 'Float this tab as a window' : 'Float this pane as a window',
538
+ icon: 'web_asset',
532
539
  action: 'promote',
533
540
  disabled: isPanel || !leaf.content
534
541
  || wm.renderer?.leafChrome?.(leafId)?.promote === false },
@@ -561,7 +561,7 @@ export class TileRenderer {
561
561
  <button class="twm-leaf__btn" data-action="split-v" title="Split vertically (Alt+V)">
562
562
  <span class="material-symbols-outlined">splitscreen_add</span>
563
563
  </button>
564
- <button class="twm-leaf__btn" data-action="promote" title="Float this pane as a window (Alt+F)">
564
+ <button class="twm-leaf__btn" data-action="promote" title="Float this ${this.ctx.wm?.floatActiveTab ? 'tab' : 'pane'} as a window (Alt+F)">
565
565
  <span class="material-symbols-outlined">web_asset</span>
566
566
  </button>`}
567
567
  <button class="twm-leaf__btn" data-action="close" title="Close (Alt+W)">
package/src/tiling/wm.js CHANGED
@@ -34,7 +34,8 @@ const PANEL_TITLES = {
34
34
  export class WindowManager {
35
35
  constructor({ rootEl, api, ctx, onChange, eventBus, host, taxonomy, events, content,
36
36
  panelDefaults = null, snapPromotion = false,
37
- promoteInPlace = false, tabLayout = null, backToOpenList = false }) {
37
+ promoteInPlace = false, tabLayout = null, backToOpenList = false,
38
+ floatActiveTab = false }) {
38
39
  if (!taxonomy) throw new Error('WindowManager: a taxonomy is required');
39
40
  if (!content || typeof content.mount !== 'function') {
40
41
  throw new Error('WindowManager: a content registry is required '
@@ -57,6 +58,8 @@ export class WindowManager {
57
58
  this.ctx = ctx || {};
58
59
  this.eventBus = eventBus || null;
59
60
  this.onChange = onChange || (() => {});
61
+ // C34. Whether the tile's float gesture takes only the tab on screen.
62
+ this.floatActiveTab = !!floatActiveTab;
60
63
 
61
64
  // The leaf a fresh or emptied desktop starts with. Comes from the
62
65
  // taxonomy's root kind — NOT a hardcoded 'home'. An embedder with a
@@ -89,6 +92,20 @@ export class WindowManager {
89
92
  * does, and an existing embedder upgrading must not find tabs closing
90
93
  * where they used to navigate. */
91
94
  this.backToOpenList = !!backToOpenList;
95
+
96
+ // C34. BACK BELONGS TO WHAT YOU ARE LOOKING AT. A floating window is not a
97
+ // tile, so the tree's focus never moves to it, and Backspace pressed while
98
+ // reading a window used to walk the tile BEHIND it. The last pointerdown
99
+ // says which one the user is in: inside a window it names that window,
100
+ // anywhere else it clears. Capture phase, so content that stops the event
101
+ // cannot hide it.
102
+ this._backWindowId = null;
103
+ const doc = rootEl?.ownerDocument || globalThis.document;
104
+ doc?.addEventListener?.('pointerdown', (e) => {
105
+ const el = e.target?.closest?.('[data-window-id]');
106
+ this._backWindowId = el && this._windowToLeaf.has(el.getAttribute('data-window-id'))
107
+ ? el.getAttribute('data-window-id') : null;
108
+ }, true);
92
109
  this._snapCtl = null;
93
110
  this.desktops = new DesktopManager({
94
111
  seed: this._rootLeaf, panelDefaults: this.panelDefaults });
@@ -244,6 +261,8 @@ export class WindowManager {
244
261
  * methods (openFromContext / openInLeaf / navigateActiveTab) —
245
262
  * they all record history by default. */
246
263
  navigateBack() {
264
+ // C34. A window the user is reading gets the Back, not the tile under it.
265
+ if (this._navigateBackInWindow()) return;
247
266
  const tree = this._tree();
248
267
  // Prefer the FOCUSED leaf — that's where the user was just
249
268
  // interacting (Open click, breadcrumb step, etc.). Falls back
@@ -418,6 +437,72 @@ export class WindowManager {
418
437
  return true;
419
438
  }
420
439
 
440
+ /**
441
+ * C34. Back while a floating window is the thing being read. Returns true
442
+ * when it handled the press, false to let the tile path run.
443
+ *
444
+ * Only the window's ACTIVE tab is considered, in this order:
445
+ * (1) with `backToOpenList`, a record closes onto an open list of its
446
+ * section: first among the window's own tabs, then any tile of the
447
+ * window's desktop, primary tile first. Closing the last tab closes
448
+ * the window, which discards it (a plain close never docks back).
449
+ * (2) otherwise it walks up IN the window, exactly as a tile does.
450
+ * A window whose content has nowhere to go still counts as handled: the
451
+ * press was aimed at it, and moving the tile behind it would be the bug.
452
+ */
453
+ _navigateBackInWindow() {
454
+ const winId = this._backWindowId;
455
+ const rec = winId ? this._windowToLeaf.get(winId) : null;
456
+ if (!rec) { this._backWindowId = null; return false; }
457
+ const tabs = Array.isArray(rec.tabs) && rec.tabs.length ? rec.tabs : [rec.original].filter(Boolean);
458
+ const activeIdx = Math.max(0, Math.min(tabs.length - 1, rec.activeTabIdx || 0));
459
+ const active = tabs[activeIdx];
460
+ if (!active) return true;
461
+ const section = this.backToOpenList && _isEntityTab(active)
462
+ ? this.taxonomy.topNavFor(active.kind) : null;
463
+ const isList = (t) => !!t && !_isEntityTab(t) && this.taxonomy.topNavFor(t.kind) === section;
464
+
465
+ if (section) {
466
+ // (1a) a list among the window's own tabs, nearest to the left first.
467
+ let own = -1;
468
+ for (let i = activeIdx - 1; i >= 0 && own < 0; i--) if (isList(tabs[i])) own = i;
469
+ for (let i = activeIdx + 1; i < tabs.length && own < 0; i++) if (isList(tabs[i])) own = i;
470
+ if (own >= 0 && Array.isArray(rec.tabs) && rec.tabs.length > 1) {
471
+ this._windowTabAction(winId, 'close', { idx: activeIdx });
472
+ this.showWindowTab(winId, own > activeIdx ? own - 1 : own);
473
+ return true;
474
+ }
475
+ // (1b) a list in a tile of the window's desktop.
476
+ const desk = this.desktops.desktops[rec.desktopIdx] || this.desktops.active();
477
+ const tree = desk.tree;
478
+ const leaves = tree.leaves?.() || [];
479
+ const primaryId = tree.primaryLeafId();
480
+ const ordered = [...leaves.filter((l) => l.id === primaryId), ...leaves.filter((l) => l.id !== primaryId)];
481
+ for (const leaf of ordered) {
482
+ const ltabs = Array.isArray(leaf.tabs) ? leaf.tabs : [];
483
+ const cur = Math.max(0, Math.min(ltabs.length - 1, leaf.activeTabIdx || 0));
484
+ const hit = isList(ltabs[cur]) ? cur : ltabs.findIndex(isList);
485
+ if (hit < 0) continue;
486
+ this._backWindowId = null;
487
+ if (Array.isArray(rec.tabs) && rec.tabs.length > 1) this._windowTabAction(winId, 'close', { idx: activeIdx });
488
+ else { try { rec.window.close({ force: true }); } catch { /* already gone */ } }
489
+ if (this.desktops.active().tree === tree) {
490
+ tree.setActiveLeafTab(leaf.id, hit);
491
+ tree.focus(leaf.id);
492
+ this.renderer.render();
493
+ }
494
+ this._persist();
495
+ this._notifyChange('navigate-back-close');
496
+ return true;
497
+ }
498
+ }
499
+ // (2) walk up inside the window.
500
+ const up = this.taxonomy.parentOf?.(active.kind, active.props || {});
501
+ const parentKind = up ? up.kind : this.taxonomy.parentKindFor(active.kind);
502
+ if (parentKind) this.openInWindow(winId, parentKind, up ? (up.props || {}) : {});
503
+ return true;
504
+ }
505
+
421
506
  navigateActiveTab(leafId, kind, props = {}) {
422
507
  const tree = this._tree();
423
508
  const leaf = tree.get(leafId);
@@ -828,6 +913,14 @@ export class WindowManager {
828
913
  const tree = this._tree();
829
914
  const focused = tree.focused();
830
915
  if (!focused) return null;
916
+ // C34. An embedder whose tabs are separate records, not one pane's
917
+ // views, floats the record on screen and leaves its siblings in place.
918
+ // A window then holds one tab, so it draws no strip at all.
919
+ const tabs = Array.isArray(focused.tabs) ? focused.tabs : [];
920
+ if (this.floatActiveTab && tabs.length > 1) {
921
+ const idx = Math.max(0, Math.min(tabs.length - 1, focused.activeTabIdx || 0));
922
+ return this.floatTabAsWindow(focused.id, idx);
923
+ }
831
924
  return this.floatPane(focused.id);
832
925
  }
833
926