flexdesk 0.2.0 → 0.4.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.
- package/README.md +26 -0
- package/css/base.css +2243 -872
- package/css/flexdesk.css +1375 -14
- package/css/overrides.css +44 -0
- package/css/tokens.css +45 -0
- package/dist/charts.js +5 -3
- package/dist/charts.js.map +1 -1
- package/dist/{chunk-DVU44T77.js → chunk-ELXVW542.js} +196 -75
- package/dist/chunk-ELXVW542.js.map +7 -0
- package/dist/chunk-LH5TSOZW.js +1237 -0
- package/dist/chunk-LH5TSOZW.js.map +7 -0
- package/dist/{chunk-TLZUUFOE.js → chunk-O5OHMWBB.js} +10 -2
- package/dist/chunk-O5OHMWBB.js.map +7 -0
- package/dist/{chunk-CT4YXXLP.js → chunk-QIU5S2RU.js} +371 -73
- package/dist/chunk-QIU5S2RU.js.map +7 -0
- package/dist/chunk-QNQHQ24V.js +408 -0
- package/dist/chunk-QNQHQ24V.js.map +7 -0
- package/dist/{chunk-DRYCDMEG.js → chunk-XKDTIT4Q.js} +168 -12
- package/dist/chunk-XKDTIT4Q.js.map +7 -0
- package/dist/editor.js +3 -380
- package/dist/editor.js.map +3 -3
- package/dist/flexdesk.css +1375 -14
- package/dist/tiles.js +168 -41
- package/dist/tiles.js.map +2 -2
- package/dist/tokens.css +45 -0
- package/dist/widgets.js +44 -14
- package/dist/widgets.js.map +2 -2
- package/dist/wm.js +3140 -157
- package/dist/wm.js.map +4 -4
- package/package.json +3 -2
- package/src/charts/chart_types.js +167 -0
- package/src/charts/plotly_wrapper.js +178 -10
- package/src/editor/notebook_tab_bar.js +39 -3
- package/src/tiles/tile_base.js +143 -35
- package/src/tiles/tile_grid.js +52 -1
- package/src/tiling/command_palette.js +71 -18
- package/src/tiling/desktops.js +36 -12
- package/src/tiling/keymap.js +24 -4
- package/src/tiling/shell.js +156 -25
- package/src/tiling/tab_strip.js +184 -0
- package/src/tiling/tile_breadcrumb.js +34 -2
- package/src/tiling/tile_renderer.js +1386 -21
- package/src/tiling/tile_tab_menu.js +101 -0
- package/src/tiling/tile_tree.js +115 -11
- package/src/tiling/wm.js +2375 -84
- package/src/tiling/zoom.js +248 -0
- package/src/ui/components/action_dropdown.js +34 -3
- package/src/ui/components/autocomplete_field.js +65 -13
- package/src/ui/components/context_menu.js +79 -8
- package/src/ui/components/data_table.js +508 -84
- package/src/ui/components/managed_window.js +928 -36
- package/src/ui/components/modal.js +214 -8
- package/dist/chunk-CT4YXXLP.js.map +0 -7
- package/dist/chunk-DRYCDMEG.js.map +0 -7
- package/dist/chunk-DVU44T77.js.map +0 -7
- package/dist/chunk-TLZUUFOE.js.map +0 -7
- package/dist/chunk-UCJ2WD4D.js +0 -625
- 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
|
package/src/tiling/tile_tree.js
CHANGED
|
@@ -298,15 +298,27 @@ export class TileTree {
|
|
|
298
298
|
}));
|
|
299
299
|
n.activeTabIdx = Math.max(0,
|
|
300
300
|
Math.min(n.tabs.length - 1, saved.activeTabIdx || 0));
|
|
301
|
-
// The caller asked for a specific entity (props.id)
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
301
|
+
// The caller asked for a specific entity (props.id), for a page
|
|
302
|
+
// kind that ISN'T the nav category itself (e.g. `settings` lives
|
|
303
|
+
// under the `home` topNav), or for this page WITH PARTICULAR PROPS
|
|
304
|
+
// (a filter, an ad-hoc expression). In all three we must surface the
|
|
305
|
+
// requested target rather than silently showing whatever the
|
|
306
|
+
// restored page happened to hold.
|
|
307
|
+
//
|
|
308
|
+
// THE THIRD CASE WAS MISSING, and it is not a corner: "show me
|
|
309
|
+
// everything open on bo" is `kind: 'backlog'` with an `expr` and no
|
|
310
|
+
// id — no entity, and `backlog` IS its own nav category — so it fell
|
|
311
|
+
// through to the bare-restore branch and put back whatever the page
|
|
312
|
+
// last held. If that was a record you had been reading, the tile did
|
|
313
|
+
// not visibly change at all, and the click looked broken. (Found and
|
|
314
|
+
// fixed in BugDesk's fork first; this is that fix, upstream.)
|
|
315
|
+
//
|
|
316
|
+
// A BARE page switch still restores as-is, which is the whole point
|
|
317
|
+
// of archiving tabs: clicking a top-nav chip carries no props and
|
|
318
|
+
// must put back the tab you left open.
|
|
308
319
|
const wantsTarget = target.props?.id != null
|
|
309
|
-
|| target.kind !== targetTopNav
|
|
320
|
+
|| target.kind !== targetTopNav
|
|
321
|
+
|| Object.keys(target.props || {}).length > 0;
|
|
310
322
|
if (wantsTarget) {
|
|
311
323
|
const wantId = target.props?.id != null;
|
|
312
324
|
const matchIdx = n.tabs.findIndex((t) =>
|
|
@@ -489,9 +501,19 @@ export class TileTree {
|
|
|
489
501
|
};
|
|
490
502
|
n.tabs = Array.isArray(n.tabs) ? n.tabs : [];
|
|
491
503
|
n.tabs.push(tab);
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
504
|
+
// `background` appends WITHOUT switching to it — the caller asked for a
|
|
505
|
+
// tab to come back to, not for the page to change under them. Without
|
|
506
|
+
// this, "open in a background tab" was indistinguishable from an
|
|
507
|
+
// ordinary click: the tab arrived and took the screen with it.
|
|
508
|
+
//
|
|
509
|
+
// Returns the new tab's index either way, NOT `activeTabIdx`: for a
|
|
510
|
+
// background tab those differ, and a caller that wants to address the
|
|
511
|
+
// tab it just made needs the index of that tab.
|
|
512
|
+
if (!opts.background) {
|
|
513
|
+
n.activeTabIdx = n.tabs.length - 1;
|
|
514
|
+
_syncActiveTab(n);
|
|
515
|
+
}
|
|
516
|
+
return n.tabs.length - 1;
|
|
495
517
|
}
|
|
496
518
|
|
|
497
519
|
/** Switch the active tab on a leaf. No-op if `idx` is out of range. */
|
|
@@ -578,6 +600,88 @@ export class TileTree {
|
|
|
578
600
|
return true;
|
|
579
601
|
}
|
|
580
602
|
|
|
603
|
+
/**
|
|
604
|
+
* C33. MOVE ONE TAB FROM ONE LEAF TO ANOTHER, AS ONE MUTATION.
|
|
605
|
+
*
|
|
606
|
+
* Every other tab operation on this class takes ONE `leafId`, and that was
|
|
607
|
+
* a complete description of the model until a tab could be dragged into a
|
|
608
|
+
* different tile: `appendLeafTab`, `removeLeafTab`, `moveLeafTab` and the
|
|
609
|
+
* three bulk closes all begin and end inside a single leaf. The renderer's
|
|
610
|
+
* refusal to wire `onDropFromOtherPane` named this absence as one of its
|
|
611
|
+
* two reasons (`tile_renderer.js`, `_topTabCallbacks`); this is that half.
|
|
612
|
+
*
|
|
613
|
+
* ONE FUNCTION RATHER THAN A COMPOSE, and that is why it lives here rather
|
|
614
|
+
* than in the WM. `removeLeafTab` then `appendLeafTab` is two mutations
|
|
615
|
+
* with a moment between them in which the tab exists nowhere — and the
|
|
616
|
+
* second can FAIL (a panel destination refuses tabs), which leaves the tree
|
|
617
|
+
* short one tab and nothing on screen saying where it went. Every guard is
|
|
618
|
+
* therefore taken before the first splice.
|
|
619
|
+
*
|
|
620
|
+
* TWO NODES ARE RE-SYNCED, WHICH IS WHAT MAKES THIS DIFFERENT. `tabs` is
|
|
621
|
+
* the source of truth and `content`/`title` mirror `tabs[active]`
|
|
622
|
+
* (`_syncActiveTab`). Every existing mutation touches one leaf, so one
|
|
623
|
+
* re-sync is right; this one touches two, and skipping the SOURCE's leaves
|
|
624
|
+
* a pane whose chrome still names — and whose body still mounts — a tab it
|
|
625
|
+
* no longer holds. That is the mistake this operation invites, and it is
|
|
626
|
+
* what `web/js/shell/tab_drop.test.mjs` asserts against in the consumer.
|
|
627
|
+
*
|
|
628
|
+
* A same-leaf call is a REORDER and delegates, so there is one
|
|
629
|
+
* implementation of "a tab changed position within its strip" rather than
|
|
630
|
+
* two that will disagree about the active-index clamp.
|
|
631
|
+
*
|
|
632
|
+
* @param {string} fromLeafId
|
|
633
|
+
* @param {number} fromIdx
|
|
634
|
+
* @param {string} toLeafId
|
|
635
|
+
* @param {number} [toIdx=-1] where to insert; -1 (or past the end) appends
|
|
636
|
+
* @returns {{ok: boolean, toIdx: number, emptied: boolean}|null} null when
|
|
637
|
+
* the move was refused. `emptied` tells the caller the source pane now
|
|
638
|
+
* holds nothing, which is its cue to re-seed rather than leave a blank
|
|
639
|
+
* tile — the never-empty-tile invariant is the WM's to keep, not this
|
|
640
|
+
* class's.
|
|
641
|
+
*/
|
|
642
|
+
moveTabToLeaf(fromLeafId, fromIdx, toLeafId, toIdx = -1) {
|
|
643
|
+
const from = this.get(fromLeafId);
|
|
644
|
+
const to = this.get(toLeafId);
|
|
645
|
+
if (!from || from.kind !== 'leaf') return null;
|
|
646
|
+
if (!to || to.kind !== 'leaf') return null;
|
|
647
|
+
const tabs = Array.isArray(from.tabs) ? from.tabs : [];
|
|
648
|
+
if (!Number.isInteger(fromIdx) || fromIdx < 0 || fromIdx >= tabs.length) return null;
|
|
649
|
+
// Panel tiles are chrome, not content, and never grow tabs — the same
|
|
650
|
+
// refusal `appendLeafTab` makes, taken here BEFORE anything is spliced
|
|
651
|
+
// so a refused destination cannot cost the source its tab.
|
|
652
|
+
if (_isPanel(to)) return null;
|
|
653
|
+
if (fromLeafId === toLeafId) {
|
|
654
|
+
const dest = (!Number.isInteger(toIdx) || toIdx < 0 || toIdx >= tabs.length)
|
|
655
|
+
? tabs.length - 1 : toIdx;
|
|
656
|
+
if (!this.moveLeafTab(fromLeafId, fromIdx, dest)) return null;
|
|
657
|
+
return { ok: true, toIdx: dest, emptied: false };
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const [moved] = from.tabs.splice(fromIdx, 1);
|
|
661
|
+
// THE SOURCE'S ACTIVE INDEX, WITH `removeLeafTab`'S OWN THREE BRANCHES.
|
|
662
|
+
// Spelled out rather than delegated because `removeLeafTab` re-syncs
|
|
663
|
+
// and returns, and the splice above has already happened; the
|
|
664
|
+
// arithmetic is the contract, so it is written where it can be compared
|
|
665
|
+
// against the original line for line.
|
|
666
|
+
if (from.tabs.length === 0) from.activeTabIdx = 0;
|
|
667
|
+
else if (fromIdx < from.activeTabIdx) from.activeTabIdx -= 1;
|
|
668
|
+
else if (fromIdx === from.activeTabIdx) from.activeTabIdx = Math.max(0, fromIdx - 1);
|
|
669
|
+
|
|
670
|
+
to.tabs = Array.isArray(to.tabs) ? to.tabs : [];
|
|
671
|
+
const at = (!Number.isInteger(toIdx) || toIdx < 0 || toIdx > to.tabs.length)
|
|
672
|
+
? to.tabs.length : toIdx;
|
|
673
|
+
to.tabs.splice(at, 0, moved);
|
|
674
|
+
// THE ARRIVING TAB IS THE ONE YOU WANTED TO SEE. A drop is a deliberate
|
|
675
|
+
// act naming one tab and one destination; leaving the destination on
|
|
676
|
+
// whatever it was already showing would make the gesture look like it
|
|
677
|
+
// did nothing, which is the failure mode of every silent move.
|
|
678
|
+
to.activeTabIdx = at;
|
|
679
|
+
|
|
680
|
+
_syncActiveTab(from);
|
|
681
|
+
_syncActiveTab(to);
|
|
682
|
+
return { ok: true, toIdx: at, emptied: from.tabs.length === 0 };
|
|
683
|
+
}
|
|
684
|
+
|
|
581
685
|
/**
|
|
582
686
|
* Split a leaf in the given direction; existing content stays in the
|
|
583
687
|
* original leaf, a new empty leaf is added next to it. Returns the
|