flexdesk 0.2.0 → 0.3.0

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 (56) hide show
  1. package/css/base.css +1484 -181
  2. package/css/flexdesk.css +1311 -18
  3. package/css/overrides.css +44 -0
  4. package/css/tokens.css +45 -0
  5. package/dist/charts.js +5 -3
  6. package/dist/charts.js.map +1 -1
  7. package/dist/{chunk-DVU44T77.js → chunk-ELXVW542.js} +196 -75
  8. package/dist/chunk-ELXVW542.js.map +7 -0
  9. package/dist/chunk-LH5TSOZW.js +1237 -0
  10. package/dist/chunk-LH5TSOZW.js.map +7 -0
  11. package/dist/{chunk-TLZUUFOE.js → chunk-O5OHMWBB.js} +10 -2
  12. package/dist/chunk-O5OHMWBB.js.map +7 -0
  13. package/dist/{chunk-CT4YXXLP.js → chunk-QIU5S2RU.js} +371 -73
  14. package/dist/chunk-QIU5S2RU.js.map +7 -0
  15. package/dist/chunk-QNQHQ24V.js +408 -0
  16. package/dist/chunk-QNQHQ24V.js.map +7 -0
  17. package/dist/{chunk-DRYCDMEG.js → chunk-XKDTIT4Q.js} +168 -12
  18. package/dist/chunk-XKDTIT4Q.js.map +7 -0
  19. package/dist/editor.js +3 -380
  20. package/dist/editor.js.map +3 -3
  21. package/dist/flexdesk.css +1311 -18
  22. package/dist/tiles.js +168 -41
  23. package/dist/tiles.js.map +2 -2
  24. package/dist/tokens.css +45 -0
  25. package/dist/widgets.js +44 -14
  26. package/dist/widgets.js.map +2 -2
  27. package/dist/wm.js +2983 -142
  28. package/dist/wm.js.map +4 -4
  29. package/package.json +3 -2
  30. package/src/charts/chart_types.js +167 -0
  31. package/src/charts/plotly_wrapper.js +178 -10
  32. package/src/editor/notebook_tab_bar.js +39 -3
  33. package/src/tiles/tile_base.js +143 -35
  34. package/src/tiles/tile_grid.js +52 -1
  35. package/src/tiling/command_palette.js +71 -18
  36. package/src/tiling/desktops.js +36 -12
  37. package/src/tiling/keymap.js +24 -4
  38. package/src/tiling/shell.js +135 -24
  39. package/src/tiling/tab_strip.js +184 -0
  40. package/src/tiling/tile_breadcrumb.js +34 -2
  41. package/src/tiling/tile_renderer.js +1386 -21
  42. package/src/tiling/tile_tab_menu.js +101 -0
  43. package/src/tiling/tile_tree.js +82 -0
  44. package/src/tiling/wm.js +2352 -74
  45. package/src/ui/components/action_dropdown.js +34 -3
  46. package/src/ui/components/autocomplete_field.js +65 -13
  47. package/src/ui/components/context_menu.js +79 -8
  48. package/src/ui/components/data_table.js +508 -84
  49. package/src/ui/components/managed_window.js +928 -36
  50. package/src/ui/components/modal.js +214 -8
  51. package/dist/chunk-CT4YXXLP.js.map +0 -7
  52. package/dist/chunk-DRYCDMEG.js.map +0 -7
  53. package/dist/chunk-DVU44T77.js.map +0 -7
  54. package/dist/chunk-TLZUUFOE.js.map +0 -7
  55. package/dist/chunk-UCJ2WD4D.js +0 -625
  56. package/dist/chunk-UCJ2WD4D.js.map +0 -7
@@ -320,6 +320,107 @@ export function openTileTabMenu({
320
320
  }
321
321
 
322
322
 
323
+ /** Open a compact menu anchored to the leaf's hamburger button that
324
+ * lists the leaf's OPEN TABS for quick switching (a browser-style tab
325
+ * overflow list). Clicking a row switches to that tab. Closes on
326
+ * Escape, click-outside, or selection.
327
+ *
328
+ * Anchored to the button: the tab strip sits at the bottom of the tile,
329
+ * so the menu opens UPWARD, its bottom edge pinned just above the
330
+ * hamburger at (x, y) = the button's top-left corner.
331
+ *
332
+ * This is deliberately NOT the same surface as `openTileTabMenu` above
333
+ * (which browses a page's content sources to open NEW tabs). The
334
+ * hamburger's job is "show me the tabs I already have open here".
335
+ *
336
+ * @param {{x:number, y:number,
337
+ * tabs: Array<{kind:string, title?:string}>,
338
+ * activeIdx?:number,
339
+ * onPick:(idx:number)=>void}} opts
340
+ * @returns {() => void} teardown closure (the menu also self-disposes).
341
+ */
342
+ export function openTileTabSwitcher({ x, y, tabs, activeIdx = 0, onPick }) {
343
+ const list = Array.isArray(tabs) ? tabs : [];
344
+ if (list.length === 0) return () => {};
345
+
346
+ const overlay = document.createElement('div');
347
+ overlay.className = 'twm-tile-tabswitch-overlay';
348
+ const rows = list.map((t, i) => `
349
+ <li class="twm-tile-tabswitch__item${i === activeIdx ? ' twm-tile-tabswitch__item--on' : ''}"
350
+ data-idx="${i}" role="option" aria-selected="${i === activeIdx}">
351
+ <span class="material-symbols-outlined twm-tile-tabswitch__check">${i === activeIdx ? 'check' : ''}</span>
352
+ <span class="twm-tile-tabswitch__label">${_esc(t.title || t.kind || 'Tab')}</span>
353
+ </li>
354
+ `).join('');
355
+ overlay.innerHTML = `
356
+ <div class="twm-tile-tabswitch" role="dialog" aria-label="Open tabs">
357
+ <header class="twm-tile-tabswitch__head">
358
+ <span class="material-symbols-outlined">tab</span>
359
+ <span class="twm-tile-tabswitch__head-label">Open tabs</span>
360
+ </header>
361
+ <ul class="twm-tile-tabswitch__list" role="listbox">${rows}</ul>
362
+ </div>
363
+ `;
364
+ document.body.appendChild(overlay);
365
+
366
+ const panel = overlay.querySelector('.twm-tile-tabswitch');
367
+ const W = 260;
368
+ panel.style.position = 'fixed';
369
+ panel.style.left = `${Math.max(8, Math.min(window.innerWidth - W - 8, x))}px`;
370
+ // Open upward: pin the panel's bottom just above the hamburger.
371
+ panel.style.bottom = `${Math.max(8, window.innerHeight - y + 4)}px`;
372
+ panel.style.maxHeight = `${Math.max(120, y - 16)}px`;
373
+
374
+ let alive = true;
375
+ let cursor = Math.max(0, Math.min(list.length - 1, activeIdx));
376
+
377
+ const close = () => {
378
+ if (!alive) return;
379
+ alive = false;
380
+ document.removeEventListener('mousedown', onOutside, true);
381
+ document.removeEventListener('keydown', onKey, true);
382
+ overlay.remove();
383
+ };
384
+ const onOutside = (ev) => { if (!overlay.contains(ev.target)) close(); };
385
+ const pick = (idx) => {
386
+ close();
387
+ try { onPick?.(idx); }
388
+ catch (err) { console.warn('[tile-tab-switch] pick failed', err); }
389
+ };
390
+ const paint = () => {
391
+ overlay.querySelectorAll('.twm-tile-tabswitch__item').forEach((li) => {
392
+ const on = Number(li.dataset.idx) === cursor;
393
+ li.classList.toggle('twm-tile-tabswitch__item--cursor', on);
394
+ if (on) li.scrollIntoView({ block: 'nearest' });
395
+ });
396
+ };
397
+ const onKey = (ev) => {
398
+ if (!alive || ev.isComposing) return;
399
+ switch (ev.key) {
400
+ case 'Escape': ev.preventDefault(); close(); return;
401
+ case 'ArrowDown': ev.preventDefault();
402
+ cursor = Math.min(list.length - 1, cursor + 1); paint(); return;
403
+ case 'ArrowUp': ev.preventDefault();
404
+ cursor = Math.max(0, cursor - 1); paint(); return;
405
+ case 'Home': ev.preventDefault(); cursor = 0; paint(); return;
406
+ case 'End': ev.preventDefault(); cursor = list.length - 1; paint(); return;
407
+ case 'Enter': ev.preventDefault(); pick(cursor); return;
408
+ }
409
+ };
410
+ overlay.querySelectorAll('.twm-tile-tabswitch__item').forEach((li) => {
411
+ const idx = Number(li.dataset.idx);
412
+ li.addEventListener('mousemove', () => {
413
+ if (cursor !== idx) { cursor = idx; paint(); }
414
+ });
415
+ li.addEventListener('click', () => pick(idx));
416
+ });
417
+ document.addEventListener('mousedown', onOutside, true);
418
+ document.addEventListener('keydown', onKey, true);
419
+ paint();
420
+ return close;
421
+ }
422
+
423
+
323
424
  /** Match a shaped record (`{id, label, hint}`) against `query`.
324
425
  * Case-insensitive substring against any field. Mirrors the command
325
426
  * palette's matching so search semantics are consistent across both
@@ -578,6 +578,88 @@ export class TileTree {
578
578
  return true;
579
579
  }
580
580
 
581
+ /**
582
+ * C33. MOVE ONE TAB FROM ONE LEAF TO ANOTHER, AS ONE MUTATION.
583
+ *
584
+ * Every other tab operation on this class takes ONE `leafId`, and that was
585
+ * a complete description of the model until a tab could be dragged into a
586
+ * different tile: `appendLeafTab`, `removeLeafTab`, `moveLeafTab` and the
587
+ * three bulk closes all begin and end inside a single leaf. The renderer's
588
+ * refusal to wire `onDropFromOtherPane` named this absence as one of its
589
+ * two reasons (`tile_renderer.js`, `_topTabCallbacks`); this is that half.
590
+ *
591
+ * ONE FUNCTION RATHER THAN A COMPOSE, and that is why it lives here rather
592
+ * than in the WM. `removeLeafTab` then `appendLeafTab` is two mutations
593
+ * with a moment between them in which the tab exists nowhere — and the
594
+ * second can FAIL (a panel destination refuses tabs), which leaves the tree
595
+ * short one tab and nothing on screen saying where it went. Every guard is
596
+ * therefore taken before the first splice.
597
+ *
598
+ * TWO NODES ARE RE-SYNCED, WHICH IS WHAT MAKES THIS DIFFERENT. `tabs` is
599
+ * the source of truth and `content`/`title` mirror `tabs[active]`
600
+ * (`_syncActiveTab`). Every existing mutation touches one leaf, so one
601
+ * re-sync is right; this one touches two, and skipping the SOURCE's leaves
602
+ * a pane whose chrome still names — and whose body still mounts — a tab it
603
+ * no longer holds. That is the mistake this operation invites, and it is
604
+ * what `web/js/shell/tab_drop.test.mjs` asserts against in the consumer.
605
+ *
606
+ * A same-leaf call is a REORDER and delegates, so there is one
607
+ * implementation of "a tab changed position within its strip" rather than
608
+ * two that will disagree about the active-index clamp.
609
+ *
610
+ * @param {string} fromLeafId
611
+ * @param {number} fromIdx
612
+ * @param {string} toLeafId
613
+ * @param {number} [toIdx=-1] where to insert; -1 (or past the end) appends
614
+ * @returns {{ok: boolean, toIdx: number, emptied: boolean}|null} null when
615
+ * the move was refused. `emptied` tells the caller the source pane now
616
+ * holds nothing, which is its cue to re-seed rather than leave a blank
617
+ * tile — the never-empty-tile invariant is the WM's to keep, not this
618
+ * class's.
619
+ */
620
+ moveTabToLeaf(fromLeafId, fromIdx, toLeafId, toIdx = -1) {
621
+ const from = this.get(fromLeafId);
622
+ const to = this.get(toLeafId);
623
+ if (!from || from.kind !== 'leaf') return null;
624
+ if (!to || to.kind !== 'leaf') return null;
625
+ const tabs = Array.isArray(from.tabs) ? from.tabs : [];
626
+ if (!Number.isInteger(fromIdx) || fromIdx < 0 || fromIdx >= tabs.length) return null;
627
+ // Panel tiles are chrome, not content, and never grow tabs — the same
628
+ // refusal `appendLeafTab` makes, taken here BEFORE anything is spliced
629
+ // so a refused destination cannot cost the source its tab.
630
+ if (_isPanel(to)) return null;
631
+ if (fromLeafId === toLeafId) {
632
+ const dest = (!Number.isInteger(toIdx) || toIdx < 0 || toIdx >= tabs.length)
633
+ ? tabs.length - 1 : toIdx;
634
+ if (!this.moveLeafTab(fromLeafId, fromIdx, dest)) return null;
635
+ return { ok: true, toIdx: dest, emptied: false };
636
+ }
637
+
638
+ const [moved] = from.tabs.splice(fromIdx, 1);
639
+ // THE SOURCE'S ACTIVE INDEX, WITH `removeLeafTab`'S OWN THREE BRANCHES.
640
+ // Spelled out rather than delegated because `removeLeafTab` re-syncs
641
+ // and returns, and the splice above has already happened; the
642
+ // arithmetic is the contract, so it is written where it can be compared
643
+ // against the original line for line.
644
+ if (from.tabs.length === 0) from.activeTabIdx = 0;
645
+ else if (fromIdx < from.activeTabIdx) from.activeTabIdx -= 1;
646
+ else if (fromIdx === from.activeTabIdx) from.activeTabIdx = Math.max(0, fromIdx - 1);
647
+
648
+ to.tabs = Array.isArray(to.tabs) ? to.tabs : [];
649
+ const at = (!Number.isInteger(toIdx) || toIdx < 0 || toIdx > to.tabs.length)
650
+ ? to.tabs.length : toIdx;
651
+ to.tabs.splice(at, 0, moved);
652
+ // THE ARRIVING TAB IS THE ONE YOU WANTED TO SEE. A drop is a deliberate
653
+ // act naming one tab and one destination; leaving the destination on
654
+ // whatever it was already showing would make the gesture look like it
655
+ // did nothing, which is the failure mode of every silent move.
656
+ to.activeTabIdx = at;
657
+
658
+ _syncActiveTab(from);
659
+ _syncActiveTab(to);
660
+ return { ok: true, toIdx: at, emptied: from.tabs.length === 0 };
661
+ }
662
+
581
663
  /**
582
664
  * Split a leaf in the given direction; existing content stays in the
583
665
  * original leaf, a new empty leaf is added next to it. Returns the