flexdesk 0.16.0 → 0.17.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/CHANGELOG.md +93 -0
- package/LICENSE +0 -0
- package/README.md +2261 -42
- package/THIRD_PARTY_LICENSES.md +0 -0
- package/css/base.css +1834 -75
- package/css/flexdesk.css +3373 -238
- package/css/overrides.css +16 -0
- package/css/reset.css +0 -0
- package/css/tokens.css +0 -0
- package/dist/canvas.js +6 -239
- package/dist/canvas.js.map +3 -3
- package/dist/charts.js +709 -18
- package/dist/charts.js.map +4 -4
- package/dist/chunk-3PHCPZHT.js +0 -0
- package/dist/chunk-3PHCPZHT.js.map +0 -0
- package/dist/chunk-4A7SJC3T.js +503 -0
- package/dist/chunk-4A7SJC3T.js.map +7 -0
- package/dist/{chunk-6JSOVNID.js → chunk-CVMCZZX2.js} +635 -10
- package/dist/chunk-CVMCZZX2.js.map +7 -0
- package/dist/chunk-D3COLZZ2.js +1311 -0
- package/dist/chunk-D3COLZZ2.js.map +7 -0
- package/dist/chunk-FOOS3T5L.js +0 -0
- package/dist/chunk-FOOS3T5L.js.map +0 -0
- package/dist/{chunk-SN4LSOAR.js → chunk-GOQLR5WR.js} +452 -2
- package/dist/chunk-GOQLR5WR.js.map +7 -0
- package/dist/{chunk-FL5KFNQH.js → chunk-HNU6MM2B.js} +52 -17
- package/dist/chunk-HNU6MM2B.js.map +7 -0
- package/dist/chunk-HYJYYEPK.js +375 -0
- package/dist/chunk-HYJYYEPK.js.map +7 -0
- package/dist/chunk-JKZLNKG6.js +2041 -0
- package/dist/chunk-JKZLNKG6.js.map +7 -0
- package/dist/chunk-JYWURG5T.js +0 -0
- package/dist/chunk-JYWURG5T.js.map +0 -0
- package/dist/{chunk-RQM5Z2MU.js → chunk-MCIXFNVF.js} +633 -311
- package/dist/chunk-MCIXFNVF.js.map +7 -0
- package/dist/chunk-N7HC3ADV.js +30 -0
- package/dist/chunk-N7HC3ADV.js.map +7 -0
- package/dist/{chunk-FBGP5KN5.js → chunk-Q36EPT2R.js} +3268 -203
- package/dist/chunk-Q36EPT2R.js.map +7 -0
- package/dist/chunk-QWQJUDPD.js +610 -0
- package/dist/chunk-QWQJUDPD.js.map +7 -0
- package/dist/{chunk-NLTOY57M.js → chunk-UURZ37SD.js} +28 -48
- package/dist/chunk-UURZ37SD.js.map +7 -0
- package/dist/chunk-UWO43BZU.js +111 -0
- package/dist/chunk-UWO43BZU.js.map +7 -0
- package/dist/chunk-WVFGV5FT.js +0 -0
- package/dist/chunk-WVFGV5FT.js.map +0 -0
- package/dist/core.js +170 -118
- package/dist/core.js.map +2 -2
- package/dist/editor.js +156 -70
- package/dist/editor.js.map +2 -2
- package/dist/flexdesk.css +3373 -238
- package/dist/flow.js +2789 -306
- package/dist/flow.js.map +4 -4
- package/dist/host.js +109 -3
- package/dist/host.js.map +3 -3
- package/dist/reset.css +0 -0
- package/dist/tile_registry-6WZPTUZV.js +0 -0
- package/dist/tile_registry-6WZPTUZV.js.map +0 -0
- package/dist/tiles.js +1131 -253
- package/dist/tiles.js.map +4 -4
- package/dist/tokens.css +0 -0
- package/dist/widgets.js +3254 -2112
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +1227 -345
- package/dist/wm.js.map +4 -4
- package/package.json +3 -1
- package/src/canvas/canvas_adapter.js +5 -0
- package/src/canvas/edge_router.js +0 -0
- package/src/canvas/graph/connector_router.js +0 -0
- package/src/canvas/graph/history_service.js +0 -0
- package/src/canvas/graph/node_platform.js +0 -0
- package/src/canvas/index.js +1 -0
- package/src/canvas/layered_layout.js +244 -0
- package/src/charts/chart_types.js +0 -0
- package/src/charts/data_series_plot_window.js +0 -0
- package/src/charts/downsample.js +2 -2
- package/src/charts/plot_config.js +53 -29
- package/src/charts/plot_config_editor.js +627 -0
- package/src/charts/plot_grid.js +449 -0
- package/src/charts/plot_popout_window.js +103 -317
- package/src/charts/plotly_wrapper.js +22 -2
- package/src/charts/series_transforms.js +206 -0
- package/src/core/event_bus.js +0 -0
- package/src/core/logging.js +1 -1
- package/src/core/settings.js +105 -19
- package/src/core/state_guard.js +93 -51
- package/src/core/state_guard_config.js +85 -74
- package/src/core/state_machine.js +0 -0
- package/src/editor/editor_pane.js +3 -3
- package/src/editor/monaco_editor_factory.js +124 -60
- package/src/editor/monaco_loader.js +103 -28
- package/src/editor/notebook_search.js +0 -0
- package/src/editor/notebook_tab_bar.js +38 -52
- package/src/editor/notebook_undo_manager.js +0 -0
- package/src/editor/split_pane_container.js +6 -3
- package/src/flow/canvas_editor/connections.js +90 -0
- package/src/flow/canvas_editor/editor.js +2351 -153
- package/src/flow/canvas_editor/index.js +2 -1
- package/src/flow/canvas_editor/layout.js +119 -12
- package/src/flow/canvas_editor/strings.js +80 -0
- package/src/flow/kit/always_before.js +0 -0
- package/src/flow/kit/catalogue.js +97 -3
- package/src/flow/kit/chip_input.js +0 -0
- package/src/flow/kit/dom.js +0 -0
- package/src/flow/kit/findings.js +0 -0
- package/src/flow/kit/history.js +0 -0
- package/src/flow/kit/index.js +3 -3
- package/src/flow/kit/keys.js +7 -1
- package/src/flow/kit/logic_graph.js +97 -15
- package/src/flow/kit/references.js +0 -0
- package/src/flow/kit/settings_panel.js +0 -0
- package/src/flow/kit/settings_schema.js +2 -1
- package/src/flow/kit/step_picker.js +0 -0
- package/src/flow/kit/strings.js +3 -1
- package/src/flow/kit/value_picker.js +0 -0
- package/src/flow/kit/widgets.js +86 -109
- package/src/flow/lanes/editor.js +136 -14
- package/src/flow/lanes/index.js +3 -1
- package/src/flow/lanes/layout.js +0 -0
- package/src/flow/lanes/pipeline.js +44 -2
- package/src/flow/lanes/preview.js +0 -0
- package/src/flow/lanes/strings.js +1 -0
- package/src/flow/outline/editor.js +0 -0
- package/src/flow/outline/index.js +0 -0
- package/src/flow/outline/mapping.js +0 -0
- package/src/flow/outline/menu.js +0 -0
- package/src/flow/outline/operations.js +0 -0
- package/src/flow/outline/recognise.js +0 -0
- package/src/flow/outline/strings.js +0 -0
- package/src/help/help_modal.js +168 -42
- package/src/help/help_registry.js +12 -1
- package/src/host/external_links.js +0 -0
- package/src/host/file_picker.js +117 -0
- package/src/host/host.js +10 -2
- package/src/host/photino_host.js +9 -0
- package/src/host/photino_host.md +21 -3
- package/src/host/pywebview_host.js +10 -0
- package/src/tiles/chart_tiles.js +53 -245
- package/src/tiles/config_schema.js +8 -0
- package/src/tiles/figure_tile.js +322 -0
- package/src/tiles/layout_persistence.js +3 -3
- package/src/tiles/list_tiles.js +422 -0
- package/src/tiles/scope_picker.js +96 -0
- package/src/tiles/tile_base.js +12 -1
- package/src/tiles/tile_config_form.js +442 -0
- package/src/tiles/tile_grid.js +38 -1
- package/src/tiles/tile_registry.js +0 -0
- package/src/tiling/bottom_panel_tabs.js +67 -99
- package/src/tiling/command_palette.js +64 -19
- package/src/tiling/content_registry.js +4 -1
- package/src/tiling/desktops.js +11 -1
- package/src/tiling/entity_sources.js +0 -0
- package/src/tiling/keymap.js +403 -128
- package/src/tiling/kind_taxonomy.js +78 -0
- package/src/tiling/landing_table.js +0 -0
- package/src/tiling/loading_overlay.js +0 -0
- package/src/tiling/nav_panel.js +59 -10
- package/src/tiling/page_factory.js +5 -5
- package/src/tiling/panel_availability.js +49 -0
- package/src/tiling/panel_keys.js +0 -0
- package/src/tiling/shell.js +114 -12
- package/src/tiling/tab_strip.js +0 -0
- package/src/tiling/tile_breadcrumb.js +0 -0
- package/src/tiling/tile_renderer.js +105 -9
- package/src/tiling/tile_tab_menu.js +0 -0
- package/src/tiling/tile_tree.js +0 -0
- package/src/tiling/wm.js +562 -30
- package/src/tiling/zoom.js +0 -0
- package/src/ui/base/component_base.js +0 -0
- package/src/ui/base/controller_base.js +0 -0
- package/src/ui/base/page_base.js +0 -0
- package/src/ui/components/about_dialog.js +0 -0
- package/src/ui/components/action_dropdown.js +30 -2
- package/src/ui/components/alert_log.js +0 -0
- package/src/ui/components/alerts_pane.js +0 -0
- package/src/ui/components/attribute_list_editor.js +0 -0
- package/src/ui/components/autocomplete_field.js +0 -0
- package/src/ui/components/cell_editor_host.js +211 -0
- package/src/ui/components/cell_grid.js +648 -0
- package/src/ui/components/cell_selection.js +220 -0
- package/src/ui/components/choice_controls.js +351 -0
- package/src/ui/components/color_field.js +211 -0
- package/src/ui/components/color_list_field.js +210 -0
- package/src/ui/components/column_fit.js +0 -0
- package/src/ui/components/column_groups.js +314 -0
- package/src/ui/components/column_order.js +142 -0
- package/src/ui/components/computing_status_window.js +98 -25
- package/src/ui/components/confirm_dialog.js +81 -38
- package/src/ui/components/context_menu.js +2 -2
- package/src/ui/components/conversation_view.js +440 -0
- package/src/ui/components/crosstab.js +289 -0
- package/src/ui/components/data_table.js +601 -62
- package/src/ui/components/detail_header.js +22 -11
- package/src/ui/components/document_outline.js +397 -0
- package/src/ui/components/drag_reorder.js +0 -0
- package/src/ui/components/fact_table.js +0 -0
- package/src/ui/components/fill_handle.js +322 -0
- package/src/ui/components/gallery_picker.js +19 -8
- package/src/ui/components/header_actions.js +55 -0
- package/src/ui/components/inline_renamer.js +0 -0
- package/src/ui/components/job_progress.js +51 -0
- package/src/ui/components/managed_window.js +27 -4
- package/src/ui/components/menu_bar.js +745 -0
- package/src/ui/components/modal.js +449 -75
- package/src/ui/components/notification_history.js +3 -3
- package/src/ui/components/progress.js +483 -0
- package/src/ui/components/record_list.js +0 -0
- package/src/ui/components/row_activation.js +0 -0
- package/src/ui/components/section_stack.js +298 -0
- package/src/ui/components/series.js +173 -0
- package/src/ui/components/settings_view.js +634 -0
- package/src/ui/components/slide_out_panel.js +43 -6
- package/src/ui/components/slider_field.js +275 -18
- package/src/ui/components/sortable_list.js +26 -4
- package/src/ui/components/staged_edits.js +343 -0
- package/src/ui/components/staged_history.js +172 -0
- package/src/ui/components/table_filter_chips.js +0 -0
- package/src/ui/components/table_state_store.js +0 -0
- package/src/ui/components/table_structure.js +182 -0
- package/src/ui/components/table_values.js +0 -0
- package/src/ui/components/tabs.js +497 -0
- package/src/ui/components/tag_input.js +215 -0
- package/src/ui/components/toast.js +25 -8
- package/src/ui/components/toggle_field.js +175 -0
- package/src/ui/components/tree_view.js +55 -34
- package/src/ui/components/window_taskbar.js +0 -0
- package/src/ui/components/wizard.js +563 -0
- package/src/ui/controllers/window_chrome_controller.js +2 -2
- package/src/ui/notification_center.js +375 -119
- package/src/ui/utils/auto_scrollbars.js +0 -0
- package/src/ui/utils/markdown.js +709 -0
- package/src/ui/utils/native_context_menu.js +0 -0
- package/src/ui/utils/overlay_scrollbar.js +16 -12
- package/src/ui/utils/raf_resize_observer.js +0 -0
- package/src/ui/utils/scroll_lock.js +0 -0
- package/src/ui/utils/strip_scroll.js +55 -0
- package/src/ui/utils/tooltip_service.js +0 -8
- package/dist/chunk-6JSOVNID.js.map +0 -7
- package/dist/chunk-CWPMLX67.js +0 -475
- package/dist/chunk-CWPMLX67.js.map +0 -7
- package/dist/chunk-FBGP5KN5.js.map +0 -7
- package/dist/chunk-FL5KFNQH.js.map +0 -7
- package/dist/chunk-NLTOY57M.js.map +0 -7
- package/dist/chunk-NNFTETK3.js +0 -633
- package/dist/chunk-NNFTETK3.js.map +0 -7
- package/dist/chunk-O5OHMWBB.js +0 -284
- package/dist/chunk-O5OHMWBB.js.map +0 -7
- package/dist/chunk-RQM5Z2MU.js.map +0 -7
- package/dist/chunk-SN4LSOAR.js.map +0 -7
- package/src/ui/controllers/panel_state_machine.js +0 -634
package/dist/wm.js
CHANGED
|
@@ -1,23 +1,31 @@
|
|
|
1
1
|
import {
|
|
2
2
|
HelpModal,
|
|
3
|
+
createMenuBar
|
|
4
|
+
} from "./chunk-JKZLNKG6.js";
|
|
5
|
+
import {
|
|
6
|
+
TAB_LEVELS,
|
|
7
|
+
createTabs
|
|
8
|
+
} from "./chunk-HYJYYEPK.js";
|
|
9
|
+
import {
|
|
3
10
|
showContextMenu
|
|
4
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-UWO43BZU.js";
|
|
5
12
|
import {
|
|
6
13
|
openForm
|
|
7
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-D3COLZZ2.js";
|
|
8
15
|
import {
|
|
9
16
|
ManagedWindow,
|
|
10
17
|
isRowActivation
|
|
11
|
-
} from "./chunk-
|
|
12
|
-
import "./chunk-
|
|
18
|
+
} from "./chunk-CVMCZZX2.js";
|
|
19
|
+
import "./chunk-HNU6MM2B.js";
|
|
13
20
|
import {
|
|
14
21
|
NotebookTabBar
|
|
15
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-UURZ37SD.js";
|
|
23
|
+
import "./chunk-N7HC3ADV.js";
|
|
16
24
|
import "./chunk-WVFGV5FT.js";
|
|
17
25
|
import "./chunk-JYWURG5T.js";
|
|
18
26
|
|
|
19
27
|
// src/tiling/bottom_panel_tabs.js
|
|
20
|
-
var BADGE_LEVELS =
|
|
28
|
+
var BADGE_LEVELS = TAB_LEVELS;
|
|
21
29
|
function createBottomPanelTabs(container, {
|
|
22
30
|
panes = [],
|
|
23
31
|
remember = null,
|
|
@@ -26,103 +34,38 @@ function createBottomPanelTabs(container, {
|
|
|
26
34
|
onSelect = null,
|
|
27
35
|
initial = null
|
|
28
36
|
} = {}) {
|
|
29
|
-
const doc = container.ownerDocument;
|
|
30
|
-
const root = doc.createElement("div");
|
|
31
|
-
root.className = "twm-bp";
|
|
32
|
-
const strip = doc.createElement("div");
|
|
33
|
-
strip.className = "twm-bp__tabs";
|
|
34
|
-
strip.setAttribute("role", "tablist");
|
|
35
|
-
const body = doc.createElement("div");
|
|
36
|
-
body.className = "twm-bp__body";
|
|
37
|
-
root.append(strip, body);
|
|
38
|
-
container.appendChild(root);
|
|
39
37
|
const byId = new Map(panes.map((pane) => [pane.id, pane]));
|
|
40
|
-
|
|
41
|
-
const mounted = /* @__PURE__ */ new Map();
|
|
42
|
-
let active = null;
|
|
38
|
+
let restoring = false;
|
|
43
39
|
let chosen = false;
|
|
44
40
|
let destroyed = false;
|
|
45
|
-
|
|
46
|
-
const tab = doc.createElement("button");
|
|
47
|
-
tab.type = "button";
|
|
48
|
-
tab.className = "twm-bp__tab";
|
|
49
|
-
tab.dataset.pane = pane.id;
|
|
50
|
-
tab.setAttribute("role", "tab");
|
|
51
|
-
tab.setAttribute("aria-selected", "false");
|
|
52
|
-
const label = doc.createElement("span");
|
|
53
|
-
label.className = "twm-bp__label";
|
|
54
|
-
label.textContent = pane.label;
|
|
55
|
-
const badge = doc.createElement("span");
|
|
56
|
-
badge.className = "twm-bp__badge";
|
|
57
|
-
badge.hidden = true;
|
|
58
|
-
tab.append(label, badge);
|
|
59
|
-
tab.addEventListener("click", () => api.select(pane.id));
|
|
60
|
-
strip.appendChild(tab);
|
|
61
|
-
tabs.set(pane.id, tab);
|
|
62
|
-
}
|
|
63
|
-
function mount(id) {
|
|
64
|
-
if (mounted.has(id)) return mounted.get(id);
|
|
65
|
-
const pane = byId.get(id);
|
|
66
|
-
const el = doc.createElement("div");
|
|
67
|
-
el.className = "twm-bp-pane";
|
|
68
|
-
el.dataset.pane = id;
|
|
69
|
-
el.setAttribute("role", "tabpanel");
|
|
70
|
-
el.hidden = id !== active;
|
|
71
|
-
body.appendChild(el);
|
|
72
|
-
const entry = { el, instance: null };
|
|
73
|
-
mounted.set(id, entry);
|
|
74
|
-
try {
|
|
75
|
-
entry.instance = pane.mount?.(el, store ? store(pane) : null, api) ?? null;
|
|
76
|
-
} catch (err) {
|
|
77
|
-
console.error(`[bottom-panel] pane "${id}" failed to mount`, err);
|
|
78
|
-
}
|
|
79
|
-
return entry;
|
|
80
|
-
}
|
|
81
|
-
function show(id) {
|
|
82
|
-
if (destroyed || !byId.has(id)) return false;
|
|
83
|
-
active = id;
|
|
84
|
-
for (const [key, tab] of tabs) {
|
|
85
|
-
const on = key === id;
|
|
86
|
-
tab.classList.toggle("twm-bp__tab--on", on);
|
|
87
|
-
tab.setAttribute("aria-selected", String(on));
|
|
88
|
-
}
|
|
89
|
-
for (const [key, entry] of mounted) entry.el.hidden = key !== id;
|
|
90
|
-
mount(id).instance?.onShow?.();
|
|
91
|
-
onSelect?.(id, byId.get(id));
|
|
92
|
-
return true;
|
|
93
|
-
}
|
|
41
|
+
let strip = null;
|
|
94
42
|
const api = {
|
|
95
|
-
root,
|
|
43
|
+
root: null,
|
|
96
44
|
/** Bring a tab to the front, and remember it. */
|
|
97
45
|
select(id) {
|
|
98
|
-
if (!
|
|
46
|
+
if (destroyed || !byId.has(id)) return;
|
|
99
47
|
chosen = true;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
48
|
+
if (strip.active() !== id) {
|
|
49
|
+
strip.select(id);
|
|
50
|
+
return;
|
|
103
51
|
}
|
|
52
|
+
strip.instance(id)?.onShow?.();
|
|
53
|
+
remembered(id);
|
|
104
54
|
},
|
|
105
55
|
/** The tab in front, or null. */
|
|
106
|
-
active: () => active,
|
|
56
|
+
active: () => strip.active(),
|
|
107
57
|
/** `count` on a tab, coloured by `level`; nothing at zero. */
|
|
108
|
-
setBadge(id, count, level = null)
|
|
109
|
-
const badge = tabs.get(id)?.querySelector(".twm-bp__badge");
|
|
110
|
-
if (!badge) return;
|
|
111
|
-
const n = Number(count) || 0;
|
|
112
|
-
badge.hidden = n <= 0;
|
|
113
|
-
badge.textContent = n > 99 ? "99+" : String(Math.max(n, 0));
|
|
114
|
-
badge.className = "twm-bp__badge" + (BADGE_LEVELS.includes(level) ? ` twm-bp__badge--${level}` : "");
|
|
115
|
-
},
|
|
58
|
+
setBadge: (id, count, level = null) => strip.setBadge(id, count, level),
|
|
116
59
|
/** A mounted pane's live instance, or null — how one tab hands work to
|
|
117
60
|
* another. `select` it first, which mounts it. */
|
|
118
|
-
instance: (id) =>
|
|
61
|
+
instance: (id) => strip.instance(id),
|
|
119
62
|
/** The panel is on screen again (its tile re-mounted): tell the pane in front. */
|
|
120
63
|
reshow() {
|
|
121
|
-
|
|
64
|
+
strip.instance(strip.active())?.onShow?.();
|
|
122
65
|
},
|
|
123
66
|
/** Before the panel is torn down: every mounted pane saves. */
|
|
124
67
|
async persist() {
|
|
125
|
-
for (const
|
|
68
|
+
for (const instance of strip.instances()) await instance.persist?.();
|
|
126
69
|
},
|
|
127
70
|
/** Gone for good: panes save, then are destroyed; the bus is let go. */
|
|
128
71
|
async destroy() {
|
|
@@ -133,47 +76,410 @@ function createBottomPanelTabs(container, {
|
|
|
133
76
|
subscription?.dispose?.();
|
|
134
77
|
} catch {
|
|
135
78
|
}
|
|
136
|
-
|
|
137
|
-
try {
|
|
138
|
-
entry.instance?.destroy?.();
|
|
139
|
-
} catch (err) {
|
|
140
|
-
console.warn("[bottom-panel] pane destroy failed", err);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
mounted.clear();
|
|
144
|
-
root.remove();
|
|
79
|
+
strip.destroy();
|
|
145
80
|
}
|
|
146
81
|
};
|
|
147
|
-
|
|
82
|
+
function remembered(id) {
|
|
83
|
+
onSelect?.(id, byId.get(id));
|
|
84
|
+
try {
|
|
85
|
+
remember?.write?.(id);
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
strip = createTabs(container, {
|
|
90
|
+
block: "twm-bp",
|
|
91
|
+
dataKey: "pane",
|
|
92
|
+
items: panes.map(({ id, label, eager }) => ({ id, label, eager: !!eager })),
|
|
93
|
+
active: byId.has(initial) ? initial : panes[0]?.id,
|
|
94
|
+
mountPane: (item, el, tabs) => {
|
|
95
|
+
strip ??= tabs;
|
|
96
|
+
api.root ??= tabs.root;
|
|
97
|
+
const pane = byId.get(item.id);
|
|
98
|
+
return pane.mount?.(el, store ? store(pane) : null, api) ?? null;
|
|
99
|
+
},
|
|
100
|
+
onSelect: (id) => {
|
|
101
|
+
if (restoring) {
|
|
102
|
+
onSelect?.(id, byId.get(id));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
chosen = true;
|
|
106
|
+
remembered(id);
|
|
107
|
+
},
|
|
108
|
+
// A click on the tab already in front is a select here, as it always
|
|
109
|
+
// was: the pane hears onShow again and the choice is remembered.
|
|
110
|
+
onReselect: (id) => api.select(id)
|
|
111
|
+
});
|
|
112
|
+
api.root ??= strip.root;
|
|
113
|
+
if (strip.active() != null) onSelect?.(strip.active(), byId.get(strip.active()));
|
|
148
114
|
const subscription = eventBus?.on?.(
|
|
149
115
|
"bottompanel:show",
|
|
150
116
|
(payload) => api.select(payload?.tab ?? panes[0]?.id)
|
|
151
117
|
) ?? null;
|
|
152
|
-
show(byId.has(initial) ? initial : panes[0]?.id);
|
|
153
118
|
if (remember?.read) {
|
|
154
119
|
Promise.resolve().then(() => remember.read()).then((saved) => {
|
|
155
|
-
if (chosen || destroyed) return;
|
|
156
|
-
if (
|
|
120
|
+
if (chosen || destroyed || !byId.has(saved)) return;
|
|
121
|
+
if (strip.active() === saved) {
|
|
122
|
+
strip.instance(saved)?.onShow?.();
|
|
123
|
+
onSelect?.(saved, byId.get(saved));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
restoring = true;
|
|
127
|
+
try {
|
|
128
|
+
strip.select(saved);
|
|
129
|
+
} finally {
|
|
130
|
+
restoring = false;
|
|
131
|
+
}
|
|
157
132
|
}).catch(() => {
|
|
158
133
|
});
|
|
159
134
|
}
|
|
160
135
|
return api;
|
|
161
136
|
}
|
|
162
137
|
|
|
163
|
-
// src/tiling/
|
|
164
|
-
var
|
|
165
|
-
|
|
166
|
-
{
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
138
|
+
// src/tiling/keymap.js
|
|
139
|
+
var KEYMAP_SHORTCUTS = Object.freeze([
|
|
140
|
+
{ keys: "Ctrl+K", action: "Open the command palette" },
|
|
141
|
+
{ keys: "?", action: "Open help" },
|
|
142
|
+
{ keys: "Esc", action: "Close the palette or a modal" },
|
|
143
|
+
{ keys: "F1..F8", action: "Jump to the Nth top-nav page" },
|
|
144
|
+
{ keys: "Backspace", action: "Go back one step up the breadcrumb" },
|
|
145
|
+
{ keys: "Alt+H / Alt+V", action: "Split the focused tile horizontally / vertically (empty pane)" },
|
|
146
|
+
{ keys: "Alt+Shift+H / Alt+Shift+V", action: "Open the focused tile's content in a horizontal / vertical split" },
|
|
147
|
+
{ keys: "Alt+T", action: "Open the focused tile's content in a new tab" },
|
|
148
|
+
{ keys: "Alt+N", action: "Open the focused tile's content in a new window" },
|
|
149
|
+
{ keys: "Alt+W", action: "Close the focused tile" },
|
|
150
|
+
{ keys: "Alt+F", action: "Float the focused tile into a window, or dock it back" },
|
|
151
|
+
{ keys: "Alt+Arrow", action: "Move focus to the neighbouring tile" },
|
|
152
|
+
{ keys: "Alt+Shift+Arrow", action: "Swap the focused tile with its neighbour" },
|
|
153
|
+
{ keys: "Alt+1..9", action: "Switch to desktop N" },
|
|
154
|
+
{ keys: "Alt+Shift+1..9", action: "Move the focused tile to desktop N" },
|
|
155
|
+
{ keys: "Ctrl+Left / Ctrl+Right", action: "Previous / next desktop" },
|
|
156
|
+
{ keys: "Shift+Left / Shift+Right", action: "Previous / next bottom-panel tab" },
|
|
157
|
+
{ keys: "Ctrl+Tab / Ctrl+Shift+Tab", action: "Next / previous tab in the focused tile" }
|
|
158
|
+
].map((r) => Object.freeze(r)));
|
|
159
|
+
var KEYBOARD_SHORTCUTS_TOPIC_ID = "keyboard-shortcuts";
|
|
160
|
+
var _esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
161
|
+
function keyboardShortcutsTopic(shortcuts = KEYMAP_SHORTCUTS) {
|
|
162
|
+
const rows = shortcuts.map((r) => `<tr><td><kbd>${_esc(r.keys)}</kbd></td><td>${_esc(r.action)}</td></tr>`).join("");
|
|
163
|
+
return {
|
|
164
|
+
id: KEYBOARD_SHORTCUTS_TOPIC_ID,
|
|
165
|
+
title: "Keyboard shortcuts",
|
|
166
|
+
html: `<table class="help-shortcuts"><thead><tr><th>Keys</th><th>Action</th></tr></thead><tbody>${rows}</tbody></table>`
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
var DEFAULT_NAV_SELECTOR = ".twm-global-top-bar .twm-bar-center.twm-top-nav .twm-top-nav__btn";
|
|
170
|
+
var DIRS = ["left", "right", "up", "down"];
|
|
171
|
+
var _where = (d) => d === "up" || d === "down" ? d : `on the ${d}`;
|
|
172
|
+
var ACTIONS = {
|
|
173
|
+
"palette": { label: "Open the command palette", run: ({ palette }) => {
|
|
174
|
+
palette.toggle();
|
|
175
|
+
} },
|
|
176
|
+
"palette:close": { label: "Close the palette", run: ({ palette }) => {
|
|
177
|
+
if (palette.isOpen()) palette.close();
|
|
178
|
+
return false;
|
|
179
|
+
} },
|
|
180
|
+
"help": { label: "Open help", run: () => {
|
|
181
|
+
HelpModal.toggle();
|
|
182
|
+
} },
|
|
183
|
+
"back": { label: "Go back one step up the breadcrumb", run: ({ wm }) => {
|
|
184
|
+
wm.navigateBack?.();
|
|
185
|
+
} },
|
|
186
|
+
"tab:next": { label: "Next tab in the focused tile", run: ({ wm }) => {
|
|
187
|
+
wm.cycleFocusedTab(1);
|
|
188
|
+
} },
|
|
189
|
+
"tab:prev": { label: "Previous tab in the focused tile", run: ({ wm }) => {
|
|
190
|
+
wm.cycleFocusedTab(-1);
|
|
191
|
+
} },
|
|
192
|
+
"desktop:next": { label: "Next desktop", run: ({ wm }) => {
|
|
193
|
+
wm.cycleDesktop(1);
|
|
194
|
+
} },
|
|
195
|
+
"desktop:prev": { label: "Previous desktop", run: ({ wm }) => {
|
|
196
|
+
wm.cycleDesktop(-1);
|
|
197
|
+
} },
|
|
198
|
+
"panelTab:next": { label: "Next bottom-panel tab", run: () => _cyclePanelTab(1) },
|
|
199
|
+
"panelTab:prev": { label: "Previous bottom-panel tab", run: () => _cyclePanelTab(-1) },
|
|
200
|
+
"split:h": { label: "Split the focused tile horizontally (empty pane)", run: ({ wm }) => {
|
|
201
|
+
wm.split("h");
|
|
202
|
+
} },
|
|
203
|
+
"split:v": { label: "Split the focused tile vertically (empty pane)", run: ({ wm }) => {
|
|
204
|
+
wm.split("v");
|
|
205
|
+
} },
|
|
206
|
+
"splitWith:h": { label: "Open the focused tile's content in a horizontal split", run: ({ wm }) => {
|
|
207
|
+
wm.splitFocusedWith("h");
|
|
208
|
+
} },
|
|
209
|
+
"splitWith:v": { label: "Open the focused tile's content in a vertical split", run: ({ wm }) => {
|
|
210
|
+
wm.splitFocusedWith("v");
|
|
211
|
+
} },
|
|
212
|
+
"openInTab": { label: "Open the focused tile's content in a new tab", run: ({ wm }) => {
|
|
213
|
+
wm.openFocusedInTab();
|
|
214
|
+
} },
|
|
215
|
+
"openInWindow": { label: "Open the focused tile's content in a new window", run: ({ wm }) => {
|
|
216
|
+
wm.openFocusedInWindow();
|
|
217
|
+
} },
|
|
218
|
+
"close": { label: "Close the focused tile", run: ({ wm }) => {
|
|
219
|
+
wm.closeFocused();
|
|
220
|
+
} },
|
|
221
|
+
"float": { label: "Float the focused tile into a window, or dock it back", run: ({ wm }) => {
|
|
222
|
+
wm.toggleManagedFocused();
|
|
223
|
+
} },
|
|
224
|
+
// Bare Alt+letter: browsers open their menu bar on it, mid-flow.
|
|
225
|
+
"suppress": { label: null, run: () => {
|
|
226
|
+
} },
|
|
227
|
+
...Object.fromEntries(DIRS.flatMap((d) => [
|
|
228
|
+
[`focus:${d}`, { label: `Move focus to the tile ${_where(d)}`, run: ({ wm }) => {
|
|
229
|
+
wm.focusDir(d);
|
|
230
|
+
} }],
|
|
231
|
+
[`move:${d}`, { label: `Swap the focused tile with its neighbour ${_where(d)}`, run: ({ wm }) => {
|
|
232
|
+
wm.moveFocused(d);
|
|
233
|
+
} }]
|
|
234
|
+
]))
|
|
235
|
+
};
|
|
236
|
+
function _action(name) {
|
|
237
|
+
if (typeof name !== "string") return null;
|
|
238
|
+
if (Object.hasOwn(ACTIONS, name)) return ACTIONS[name];
|
|
239
|
+
const m = /^(nav|desktop|toDesktop):(\d+)$/.exec(name);
|
|
240
|
+
if (!m) return null;
|
|
241
|
+
const n = Number(m[2]);
|
|
242
|
+
if (m[1] === "nav") {
|
|
243
|
+
return {
|
|
244
|
+
label: `Jump to top-nav page ${n + 1}`,
|
|
245
|
+
// The selector is a CONFIG option, not a constant. An embedder that
|
|
246
|
+
// moves its sections out of the top bar — into an icon rail, say —
|
|
247
|
+
// would otherwise find F1..F8 silently doing nothing. Passing
|
|
248
|
+
// `navSelector` is how such an embedder keeps the keys.
|
|
249
|
+
run: ({ navSelector }) => {
|
|
250
|
+
const btns = document.querySelectorAll(navSelector || DEFAULT_NAV_SELECTOR);
|
|
251
|
+
if (n >= btns.length) return false;
|
|
252
|
+
btns[n].click();
|
|
253
|
+
return void 0;
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
if (m[1] === "desktop") {
|
|
258
|
+
return { label: `Switch to desktop ${n + 1}`, run: ({ wm }) => {
|
|
259
|
+
wm.switchDesktop(n);
|
|
260
|
+
} };
|
|
175
261
|
}
|
|
262
|
+
return { label: `Move the focused tile to desktop ${n + 1}`, run: ({ wm }) => {
|
|
263
|
+
wm.moveFocusedToDesktop(n);
|
|
264
|
+
} };
|
|
265
|
+
}
|
|
266
|
+
function _cyclePanelTab(dir) {
|
|
267
|
+
const tabs = [...document.querySelectorAll(".twm-bp__tabs .twm-bp__tab")];
|
|
268
|
+
if (!tabs.length) return false;
|
|
269
|
+
const cur = tabs.findIndex((t) => t.classList.contains("twm-bp__tab--on"));
|
|
270
|
+
tabs[((cur < 0 ? 0 : cur) + dir + tabs.length) % tabs.length].click();
|
|
271
|
+
return void 0;
|
|
272
|
+
}
|
|
273
|
+
var ARROWS = ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"];
|
|
274
|
+
var DIGITS = [1, 2, 3, 4, 5, 6, 7, 8, 9];
|
|
275
|
+
var F_KEYS = Array.from({ length: 12 }, (_, i) => `F${i + 1}`);
|
|
276
|
+
var DEFAULT_CHORDS = [
|
|
277
|
+
["Ctrl+K", "palette", true],
|
|
278
|
+
["Meta+K", "palette", true],
|
|
279
|
+
["?", "help", false],
|
|
280
|
+
["Escape", "palette:close", true],
|
|
281
|
+
...F_KEYS.map((f, i) => [f, `nav:${i}`, false]),
|
|
282
|
+
["Backspace", "back", false],
|
|
283
|
+
["Alt+H", "split:h", true],
|
|
284
|
+
["Alt+V", "split:v", true],
|
|
285
|
+
["Alt+Shift+H", "splitWith:h", true],
|
|
286
|
+
["Alt+Shift+V", "splitWith:v", true],
|
|
287
|
+
["Alt+T", "openInTab", true],
|
|
288
|
+
["Alt+N", "openInWindow", true],
|
|
289
|
+
["Alt+W", "close", true],
|
|
290
|
+
["Alt+F", "float", true],
|
|
291
|
+
...ARROWS.map((k, i) => [`Alt+${k}`, `focus:${DIRS[i]}`, true]),
|
|
292
|
+
...ARROWS.map((k, i) => [`Alt+Shift+${k}`, `move:${DIRS[i]}`, true]),
|
|
293
|
+
...DIGITS.map((d) => [`Alt+${d}`, `desktop:${d - 1}`, true]),
|
|
294
|
+
...DIGITS.map((d) => [`Alt+Shift+${d}`, `toDesktop:${d - 1}`, true]),
|
|
295
|
+
["Ctrl+ArrowLeft", "desktop:prev", false],
|
|
296
|
+
["Ctrl+ArrowRight", "desktop:next", false],
|
|
297
|
+
["Shift+ArrowLeft", "panelTab:prev", false],
|
|
298
|
+
["Shift+ArrowRight", "panelTab:next", false],
|
|
299
|
+
["Ctrl+Tab", "tab:next", true],
|
|
300
|
+
["Meta+Tab", "tab:next", true],
|
|
301
|
+
["Ctrl+Shift+Tab", "tab:prev", true],
|
|
302
|
+
["Meta+Shift+Tab", "tab:prev", true],
|
|
303
|
+
["Alt+Letter", "suppress", false]
|
|
176
304
|
];
|
|
305
|
+
var SHORTCUT_CHORDS = [
|
|
306
|
+
["Ctrl+K", "Meta+K"],
|
|
307
|
+
["?"],
|
|
308
|
+
["Escape"],
|
|
309
|
+
F_KEYS,
|
|
310
|
+
["Backspace"],
|
|
311
|
+
["Alt+H", "Alt+V"],
|
|
312
|
+
["Alt+Shift+H", "Alt+Shift+V"],
|
|
313
|
+
["Alt+T"],
|
|
314
|
+
["Alt+N"],
|
|
315
|
+
["Alt+W"],
|
|
316
|
+
["Alt+F"],
|
|
317
|
+
ARROWS.map((k) => `Alt+${k}`),
|
|
318
|
+
ARROWS.map((k) => `Alt+Shift+${k}`),
|
|
319
|
+
DIGITS.map((d) => `Alt+${d}`),
|
|
320
|
+
DIGITS.map((d) => `Alt+Shift+${d}`),
|
|
321
|
+
["Ctrl+ArrowLeft", "Ctrl+ArrowRight"],
|
|
322
|
+
["Shift+ArrowLeft", "Shift+ArrowRight"],
|
|
323
|
+
["Ctrl+Tab", "Meta+Tab", "Ctrl+Shift+Tab", "Meta+Shift+Tab"]
|
|
324
|
+
];
|
|
325
|
+
var DEFAULTS = new Map(DEFAULT_CHORDS.map(([id, action, inField]) => [id, { action, inField }]));
|
|
326
|
+
var BY_LOWER = new Map([...DEFAULTS.keys()].map((id) => [id.toLowerCase(), id]));
|
|
327
|
+
var MODIFIER_ORDER = ["Ctrl", "Meta", "Alt", "Shift"];
|
|
328
|
+
var MODIFIER_NAMES = {
|
|
329
|
+
ctrl: "Ctrl",
|
|
330
|
+
control: "Ctrl",
|
|
331
|
+
meta: "Meta",
|
|
332
|
+
cmd: "Meta",
|
|
333
|
+
command: "Meta",
|
|
334
|
+
alt: "Alt",
|
|
335
|
+
option: "Alt",
|
|
336
|
+
shift: "Shift"
|
|
337
|
+
};
|
|
338
|
+
var KEY_ALIASES = { esc: "Escape", left: "ArrowLeft", right: "ArrowRight", up: "ArrowUp", down: "ArrowDown" };
|
|
339
|
+
var _shiftCounts = (key) => key.length !== 1 || /^[\p{L}\p{N}]$/u.test(key);
|
|
340
|
+
function _compose(mods, key) {
|
|
341
|
+
const k = key.length === 1 ? key.toUpperCase() : key;
|
|
342
|
+
const used = MODIFIER_ORDER.filter((m) => mods.has(m) && (m !== "Shift" || _shiftCounts(key)));
|
|
343
|
+
return [...used, k].join("+");
|
|
344
|
+
}
|
|
345
|
+
function _chordOfEvent(e) {
|
|
346
|
+
const key = e.key;
|
|
347
|
+
if (!key || key === "Control" || key === "Shift" || key === "Alt" || key === "Meta") return null;
|
|
348
|
+
const { ctrlKey: ctrl, metaKey: meta, altKey: alt, shiftKey: shift } = e;
|
|
349
|
+
const lower = key.toLowerCase();
|
|
350
|
+
const cm = ctrl ? "Ctrl" : "Meta";
|
|
351
|
+
if ((ctrl || meta) && lower === "k" && !shift) return `${cm}+K`;
|
|
352
|
+
if ((ctrl || meta) && !alt && key === "Tab") return `${cm}+${shift ? "Shift+" : ""}Tab`;
|
|
353
|
+
if (key === "Escape") return "Escape";
|
|
354
|
+
if (key === "?") return "?";
|
|
355
|
+
if (key === "Backspace") return ctrl || meta ? null : "Backspace";
|
|
356
|
+
if (!alt) {
|
|
357
|
+
const only = (m) => ctrl === (m === "Ctrl") && shift === (m === "Shift") && !meta;
|
|
358
|
+
if (/^F([1-9]|1[0-2])$/.test(key)) return !ctrl && !meta && !shift ? key : null;
|
|
359
|
+
if (key === "ArrowLeft" || key === "ArrowRight") {
|
|
360
|
+
if (only("Shift")) return `Shift+${key}`;
|
|
361
|
+
if (only("Ctrl")) return `Ctrl+${key}`;
|
|
362
|
+
}
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
const sh = shift ? "Shift+" : "";
|
|
366
|
+
if (/^[1-9]$/.test(key)) return `Alt+${sh}${key}`;
|
|
367
|
+
if (lower === "h" || lower === "v") return `Alt+${sh}${lower.toUpperCase()}`;
|
|
368
|
+
if (lower === "t" || lower === "n" || lower === "w" || lower === "f") return `Alt+${lower.toUpperCase()}`;
|
|
369
|
+
if (ARROWS.includes(key)) return `Alt+${sh}${key}`;
|
|
370
|
+
if (/^[a-z]$/.test(lower)) return "Alt+Letter";
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
function _canonicalChord(id) {
|
|
374
|
+
if (typeof id !== "string" || !id) return null;
|
|
375
|
+
const parts = id.split("+").map((p) => p.trim());
|
|
376
|
+
const keyRaw = parts.pop();
|
|
377
|
+
if (!keyRaw) return null;
|
|
378
|
+
const mods = /* @__PURE__ */ new Set();
|
|
379
|
+
for (const p of parts) {
|
|
380
|
+
const m = MODIFIER_NAMES[p.toLowerCase()];
|
|
381
|
+
if (!m) return null;
|
|
382
|
+
mods.add(m);
|
|
383
|
+
}
|
|
384
|
+
const key = KEY_ALIASES[keyRaw.toLowerCase()] ?? keyRaw;
|
|
385
|
+
return BY_LOWER.get(_compose(mods, key).toLowerCase()) ?? null;
|
|
386
|
+
}
|
|
387
|
+
function _resolve(bindings) {
|
|
388
|
+
const table = /* @__PURE__ */ new Map();
|
|
389
|
+
for (const [id, { action, inField }] of DEFAULTS) {
|
|
390
|
+
const a = _action(action);
|
|
391
|
+
table.set(id, { run: a.run, label: a.label, inField, isDefault: true });
|
|
392
|
+
}
|
|
393
|
+
const problems = [];
|
|
394
|
+
if (bindings == null) return { table, problems };
|
|
395
|
+
if (typeof bindings !== "object") {
|
|
396
|
+
problems.push("`bindings` must be an object of { chord: action | false | function } \u2014 ignored");
|
|
397
|
+
return { table, problems };
|
|
398
|
+
}
|
|
399
|
+
for (const [rawId, value] of Object.entries(bindings)) {
|
|
400
|
+
const id = _canonicalChord(rawId);
|
|
401
|
+
if (!id) {
|
|
402
|
+
problems.push(`unknown chord "${rawId}" in bindings \u2014 ignored`);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
const { action: defaultAction, inField } = DEFAULTS.get(id);
|
|
406
|
+
if (value === false) {
|
|
407
|
+
table.set(id, { disabled: true });
|
|
408
|
+
} else if (typeof value === "function") {
|
|
409
|
+
table.set(id, {
|
|
410
|
+
run: (ctx, e) => value(e, { wm: ctx.wm, palette: ctx.palette }),
|
|
411
|
+
label: "Defined by the application",
|
|
412
|
+
inField,
|
|
413
|
+
isDefault: false
|
|
414
|
+
});
|
|
415
|
+
} else {
|
|
416
|
+
const a = _action(value);
|
|
417
|
+
if (!a) {
|
|
418
|
+
problems.push(`unknown action ${JSON.stringify(value)} for chord "${rawId}" \u2014 ignored`);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
table.set(id, { run: a.run, label: a.label, inField, isDefault: value === defaultAction });
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return { table, problems };
|
|
425
|
+
}
|
|
426
|
+
function keymapShortcuts(bindings = null) {
|
|
427
|
+
const { table } = _resolve(bindings);
|
|
428
|
+
const rows = [];
|
|
429
|
+
SHORTCUT_CHORDS.forEach((chords, i) => {
|
|
430
|
+
if (chords.every((id) => table.get(id).isDefault)) {
|
|
431
|
+
rows.push(KEYMAP_SHORTCUTS[i]);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
for (const id of chords) {
|
|
435
|
+
const b = table.get(id);
|
|
436
|
+
if (b.disabled || !b.label) continue;
|
|
437
|
+
rows.push(Object.freeze({ keys: id, action: b.label }));
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
return Object.freeze(rows);
|
|
441
|
+
}
|
|
442
|
+
function installKeymap({ wm, palette, navSelector = null, bindings = null, logger = null } = {}) {
|
|
443
|
+
const { table, problems } = _resolve(bindings);
|
|
444
|
+
const log = logger ?? console;
|
|
445
|
+
for (const p of problems) log.warn?.(`[keymap] ${p}`);
|
|
446
|
+
const ctx = { wm, palette, navSelector };
|
|
447
|
+
const onKeyDown = (e) => {
|
|
448
|
+
if (e.defaultPrevented) return;
|
|
449
|
+
const chord = _chordOfEvent(e);
|
|
450
|
+
if (!chord) return;
|
|
451
|
+
const b = table.get(chord);
|
|
452
|
+
if (!b || b.disabled) return;
|
|
453
|
+
if (!b.inField && e.target?.closest?.('input, textarea, select, [contenteditable="true"]')) return;
|
|
454
|
+
if (b.run(ctx, e) !== false) e.preventDefault();
|
|
455
|
+
};
|
|
456
|
+
document.addEventListener("keydown", onKeyDown);
|
|
457
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/tiling/command_palette.js
|
|
461
|
+
var ROOT_ID = "twm-cmdpal";
|
|
462
|
+
var HELP_ACTION = KEYMAP_SHORTCUTS.find((r) => r.keys === "?").action;
|
|
463
|
+
function _staticCommands(shortcuts) {
|
|
464
|
+
const helpKeys = shortcuts.filter((r) => r.action === HELP_ACTION).map((r) => r.keys).join(" / ");
|
|
465
|
+
return [
|
|
466
|
+
{
|
|
467
|
+
kind: "command",
|
|
468
|
+
id: "shortcuts",
|
|
469
|
+
label: "Keyboard shortcuts",
|
|
470
|
+
hint: helpKeys ? `Show all key bindings (${helpKeys})` : "Show all key bindings",
|
|
471
|
+
icon: "keyboard",
|
|
472
|
+
...helpKeys ? { shortcut: helpKeys } : {},
|
|
473
|
+
verb: "run",
|
|
474
|
+
// The application's help may define the topic; when it does not, the
|
|
475
|
+
// window manager's own bindings are shown rather than an error.
|
|
476
|
+
action: () => HelpModal.open(
|
|
477
|
+
KEYBOARD_SHORTCUTS_TOPIC_ID,
|
|
478
|
+
{ fallback: keyboardShortcutsTopic(shortcuts) }
|
|
479
|
+
)
|
|
480
|
+
}
|
|
481
|
+
];
|
|
482
|
+
}
|
|
177
483
|
var DEFAULT_GROUP_CAP = 4;
|
|
178
484
|
var DEFAULT_COMMAND_MIN_CHARS = 2;
|
|
179
485
|
var REMOTE_DEBOUNCE_MS = 150;
|
|
@@ -195,7 +501,8 @@ function createCommandPalette({
|
|
|
195
501
|
drill = null,
|
|
196
502
|
cache = false,
|
|
197
503
|
tips = null,
|
|
198
|
-
footerNote = ""
|
|
504
|
+
footerNote = "",
|
|
505
|
+
shortcuts = KEYMAP_SHORTCUTS
|
|
199
506
|
}) {
|
|
200
507
|
if (!taxonomy) throw new Error("createCommandPalette: a taxonomy is required");
|
|
201
508
|
if (!catalog) throw new Error("createCommandPalette: an entity catalog is required");
|
|
@@ -204,6 +511,7 @@ function createCommandPalette({
|
|
|
204
511
|
let entities = [];
|
|
205
512
|
let cached = null;
|
|
206
513
|
let onClose = null;
|
|
514
|
+
const isSingle = (row) => !!row && taxonomy.isSingle(row.kind);
|
|
207
515
|
const isOpen = () => !!overlay;
|
|
208
516
|
const close = () => {
|
|
209
517
|
if (!overlay) return;
|
|
@@ -221,6 +529,7 @@ function createCommandPalette({
|
|
|
221
529
|
const invalidate = () => {
|
|
222
530
|
cached = null;
|
|
223
531
|
};
|
|
532
|
+
const staticCommands = _staticCommands(shortcuts);
|
|
224
533
|
const open = async () => {
|
|
225
534
|
if (overlay) return;
|
|
226
535
|
overlay = document.createElement("div");
|
|
@@ -238,19 +547,28 @@ function createCommandPalette({
|
|
|
238
547
|
const chipSlot = overlay.querySelector('[data-role="type-chip"]');
|
|
239
548
|
const chips = overlay.querySelectorAll("[data-shortcut]");
|
|
240
549
|
const toggles = overlay.querySelectorAll("[data-toggle]");
|
|
241
|
-
|
|
550
|
+
const syncToggle = (btn) => {
|
|
242
551
|
const which = btn.dataset.toggle;
|
|
552
|
+
const { available, reason } = wm.getPanelAvailability(which);
|
|
243
553
|
btn.classList.toggle("twm-chip--on", wm.isPanelOpen(which));
|
|
244
|
-
|
|
554
|
+
if (available) {
|
|
555
|
+
btn.removeAttribute("aria-disabled");
|
|
556
|
+
btn.removeAttribute("title");
|
|
557
|
+
} else {
|
|
558
|
+
btn.setAttribute("aria-disabled", "true");
|
|
559
|
+
if (reason) btn.setAttribute("title", reason);
|
|
560
|
+
else btn.removeAttribute("title");
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
toggles.forEach(syncToggle);
|
|
245
564
|
chips.forEach((btn) => btn.addEventListener("click", () => {
|
|
246
565
|
const kind = btn.dataset.shortcut;
|
|
247
566
|
wm.openInPrimary(kind);
|
|
248
567
|
close();
|
|
249
568
|
}));
|
|
250
569
|
toggles.forEach((btn) => btn.addEventListener("click", () => {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
btn.classList.toggle("twm-chip--on", wm.isPanelOpen(which));
|
|
570
|
+
wm.togglePanel(btn.dataset.toggle);
|
|
571
|
+
syncToggle(btn);
|
|
254
572
|
}));
|
|
255
573
|
let filtered = [];
|
|
256
574
|
let active = 0;
|
|
@@ -271,12 +589,12 @@ function createCommandPalette({
|
|
|
271
589
|
const renderChip = () => {
|
|
272
590
|
if (scope) {
|
|
273
591
|
chipSlot.innerHTML = `
|
|
274
|
-
<span class="twm-cmdpal__type-chip" title="${
|
|
275
|
-
<span class="material-symbols-outlined twm-cmdpal__type-chip-icon">${
|
|
276
|
-
<span class="twm-cmdpal__type-chip-label twm-cmdpal__type-chip-label--scope">${
|
|
592
|
+
<span class="twm-cmdpal__type-chip" title="${_esc2(scope.label)}">
|
|
593
|
+
<span class="material-symbols-outlined twm-cmdpal__type-chip-icon">${_esc2(scope.icon || "arrow_right")}</span>
|
|
594
|
+
<span class="twm-cmdpal__type-chip-label twm-cmdpal__type-chip-label--scope">${_esc2(scope.label)}</span>
|
|
277
595
|
<button type="button" class="twm-cmdpal__type-chip-x"
|
|
278
596
|
data-action="clear-type"
|
|
279
|
-
aria-label="Leave ${
|
|
597
|
+
aria-label="Leave ${_esc2(scope.label)}">\xD7</button>
|
|
280
598
|
</span>`;
|
|
281
599
|
chipSlot.querySelector('[data-action="clear-type"]')?.addEventListener("click", (ev) => {
|
|
282
600
|
ev.preventDefault();
|
|
@@ -293,9 +611,9 @@ function createCommandPalette({
|
|
|
293
611
|
const meta = taxonomy.meta(head);
|
|
294
612
|
const label = typeFilterLabel || meta?.label || head;
|
|
295
613
|
chipSlot.innerHTML = `
|
|
296
|
-
<span class="twm-cmdpal__type-chip" title="Filtering by ${
|
|
614
|
+
<span class="twm-cmdpal__type-chip" title="Filtering by ${_esc2(label)} (${typeFilter.length} kind${typeFilter.length === 1 ? "" : "s"})">
|
|
297
615
|
<span class="material-symbols-outlined twm-cmdpal__type-chip-icon">${meta?.icon || "arrow_right"}</span>
|
|
298
|
-
<span class="twm-cmdpal__type-chip-label">${
|
|
616
|
+
<span class="twm-cmdpal__type-chip-label">${_esc2(label)}</span>
|
|
299
617
|
<button type="button" class="twm-cmdpal__type-chip-x"
|
|
300
618
|
data-action="clear-type"
|
|
301
619
|
aria-label="Clear type filter">\xD7</button>
|
|
@@ -319,9 +637,9 @@ function createCommandPalette({
|
|
|
319
637
|
<div class="twm-cmdpal__item${i === active ? " twm-cmdpal__item--active" : ""}"
|
|
320
638
|
data-idx="${i}">
|
|
321
639
|
<span class="material-symbols-outlined twm-cmdpal__icon">${e.icon || "arrow_right"}</span>
|
|
322
|
-
<span class="twm-cmdpal__label">${
|
|
640
|
+
<span class="twm-cmdpal__label">${_esc2(e.label || e.id)}</span>
|
|
323
641
|
<span class="twm-cmdpal__kind">${e.kind}</span>
|
|
324
|
-
<span class="twm-cmdpal__hint">${
|
|
642
|
+
<span class="twm-cmdpal__hint">${_esc2(e.hint || "")}</span>
|
|
325
643
|
</div>
|
|
326
644
|
`).join("") || "";
|
|
327
645
|
list.querySelectorAll("[data-idx]").forEach((el) => {
|
|
@@ -346,21 +664,22 @@ function createCommandPalette({
|
|
|
346
664
|
if (r.more) {
|
|
347
665
|
return `<div class="twm-cmdpal__item twm-cmdpal__item--more${on}" data-idx="${i}" role="option">
|
|
348
666
|
<span class="material-symbols-outlined twm-cmdpal__icon">expand_more</span>
|
|
349
|
-
<span class="twm-cmdpal__main"><span class="twm-cmdpal__label">${
|
|
667
|
+
<span class="twm-cmdpal__main"><span class="twm-cmdpal__label">${_esc2(r.label)}</span></span>
|
|
350
668
|
<span class="twm-cmdpal__side"><span class="twm-cmdpal__acts"><span><kbd>Enter</kbd> show all</span></span></span>
|
|
351
669
|
</div>`;
|
|
352
670
|
}
|
|
353
671
|
const drawn = _drawn(r);
|
|
354
|
-
const ctx = r.cells?.length ? `<span class="twm-cmdpal__cells">${r.cells.map(
|
|
672
|
+
const ctx = r.cells?.length ? `<span class="twm-cmdpal__cells">${r.cells.map(_esc2).join(" \xB7 ")}</span>` : drawn ? `<span class="twm-cmdpal__context">${_esc2(drawn)}</span>` : "";
|
|
355
673
|
const verb = r.verb || (r.action || r.kind === "command" ? "run" : "open");
|
|
356
674
|
const modes = Array.isArray(r.modes) ? r.modes : [];
|
|
357
|
-
const acts = [`<span><kbd>Enter</kbd> ${
|
|
358
|
-
if (r.drillLabel && drill) acts.push(`<span><kbd>Tab</kbd> ${
|
|
359
|
-
|
|
360
|
-
if (modes.includes("
|
|
361
|
-
|
|
675
|
+
const acts = [`<span><kbd>Enter</kbd> ${_esc2(verb)}</span>`];
|
|
676
|
+
if (r.drillLabel && drill) acts.push(`<span><kbd>Tab</kbd> ${_esc2(r.drillLabel)}</span>`);
|
|
677
|
+
const refused = isSingle(r) ? ` class="twm-cmdpal__act--disabled" aria-disabled="true" title="${_esc2(taxonomy.singleReason(r.kind))}"` : "";
|
|
678
|
+
if (modes.includes("split")) acts.push(`<span${refused}><kbd>Ctrl Enter</kbd> split</span>`);
|
|
679
|
+
if (modes.includes("window")) acts.push(`<span${refused}><kbd>Shift Enter</kbd> window</span>`);
|
|
680
|
+
const side = r.shortcut ? `<kbd class="twm-cmdpal__shortcut">${_esc2(r.shortcut)}</kbd>` : `<span class="twm-cmdpal__kind">${_esc2(kindLabel(r))}</span>`;
|
|
362
681
|
return `<div class="twm-cmdpal__item${on}" data-idx="${i}" role="option">
|
|
363
|
-
<span class="material-symbols-outlined twm-cmdpal__icon">${
|
|
682
|
+
<span class="material-symbols-outlined twm-cmdpal__icon">${_esc2(r.icon || "arrow_right")}</span>
|
|
364
683
|
<span class="twm-cmdpal__main"><span class="twm-cmdpal__label">${_mark(r.label || r.id, r._hits)}</span>${ctx}</span>
|
|
365
684
|
<span class="twm-cmdpal__side">${side}<span class="twm-cmdpal__acts">${acts.join("")}</span></span>
|
|
366
685
|
</div>`;
|
|
@@ -369,22 +688,22 @@ function createCommandPalette({
|
|
|
369
688
|
let html = "";
|
|
370
689
|
for (const s of sections) {
|
|
371
690
|
if (!s.count) continue;
|
|
372
|
-
if (s.title) html += `<div class="twm-cmdpal__group" role="presentation">${
|
|
691
|
+
if (s.title) html += `<div class="twm-cmdpal__group" role="presentation">${_esc2(s.title)}</div>`;
|
|
373
692
|
for (let i = s.start; i < s.start + s.count; i++) html += rowHtml(filtered[i], i);
|
|
374
693
|
}
|
|
375
694
|
const q = input.value.trim();
|
|
376
695
|
if (!filtered.length) {
|
|
377
696
|
if (scope) {
|
|
378
697
|
const msg = remote.error ? remote.error : remote.pending ? "Searching\u2026" : q ? `Nothing in ${scope.label} matches \u201C${q}\u201D.` : scope.emptyText || `Type to search ${scope.label}.`;
|
|
379
|
-
html += `<div class="twm-cmdpal__empty">${
|
|
698
|
+
html += `<div class="twm-cmdpal__empty">${_esc2(msg)}</div>`;
|
|
380
699
|
} else if (q && q !== ">") {
|
|
381
|
-
html += `<div class="twm-cmdpal__empty">Nothing matches \u201C${
|
|
700
|
+
html += `<div class="twm-cmdpal__empty">Nothing matches \u201C${_esc2(q)}\u201D.</div>`;
|
|
382
701
|
}
|
|
383
702
|
} else if (scope && remote.pending) {
|
|
384
703
|
html += '<div class="twm-cmdpal__empty twm-cmdpal__empty--quiet">Searching\u2026</div>';
|
|
385
704
|
}
|
|
386
705
|
if (!q && !scope && !typeFilter && Array.isArray(tips) && tips.length) {
|
|
387
|
-
html += `<div class="twm-cmdpal__tips">${tips.map((t) => `<span>${
|
|
706
|
+
html += `<div class="twm-cmdpal__tips">${tips.map((t) => `<span>${_esc2(t)}</span>`).join("")}</div>`;
|
|
388
707
|
}
|
|
389
708
|
list.innerHTML = html;
|
|
390
709
|
renderFooter();
|
|
@@ -403,7 +722,7 @@ function createCommandPalette({
|
|
|
403
722
|
if (scope || typeFilter) parts.push("<span><kbd>Backspace</kbd> leave</span>");
|
|
404
723
|
parts.push("<span><kbd>Esc</kbd> close</span>");
|
|
405
724
|
const note = scope?.note || footerNote;
|
|
406
|
-
foot.innerHTML = parts.join("") + (note ? `<span class="twm-cmdpal__footnote">${
|
|
725
|
+
foot.innerHTML = parts.join("") + (note ? `<span class="twm-cmdpal__footnote">${_esc2(note)}</span>` : "");
|
|
407
726
|
};
|
|
408
727
|
if (grouped) {
|
|
409
728
|
list.addEventListener("mousemove", (e) => {
|
|
@@ -428,6 +747,7 @@ function createCommandPalette({
|
|
|
428
747
|
showAll(picked.more);
|
|
429
748
|
return;
|
|
430
749
|
}
|
|
750
|
+
if (mode !== "open" && isSingle(picked)) return;
|
|
431
751
|
const pick = _public(picked);
|
|
432
752
|
const how = { mode, scope: scope?.id ?? null };
|
|
433
753
|
close();
|
|
@@ -673,7 +993,7 @@ function createCommandPalette({
|
|
|
673
993
|
console.warn("[cmdpal] commands() threw", err);
|
|
674
994
|
}
|
|
675
995
|
}
|
|
676
|
-
return [...
|
|
996
|
+
return [...staticCommands, ...extra];
|
|
677
997
|
};
|
|
678
998
|
const withCommands = (rows) => grouped ? [...rows, ...commandRows()] : [...commandRows(), ...rows];
|
|
679
999
|
entities = _decorateIcons(withCommands(cache && cached ? cached : []), taxonomy);
|
|
@@ -728,7 +1048,7 @@ function _markup(taxonomy, placeholder, wm, { chrome = {}, grouped = false } = {
|
|
|
728
1048
|
<span class="material-symbols-outlined twm-cmdpal__search-icon">search</span>
|
|
729
1049
|
<span class="twm-cmdpal__type-chip-slot" data-role="type-chip"></span>
|
|
730
1050
|
<input type="text" class="twm-cmdpal__input"
|
|
731
|
-
placeholder="${
|
|
1051
|
+
placeholder="${_esc2(placeholder)}"
|
|
732
1052
|
autocomplete="off" spellcheck="false" role="combobox"
|
|
733
1053
|
aria-expanded="true" aria-controls="twm-cmdpal-list" />
|
|
734
1054
|
<kbd class="twm-cmdpal__esc">Esc</kbd>
|
|
@@ -744,7 +1064,7 @@ function _markup(taxonomy, placeholder, wm, { chrome = {}, grouped = false } = {
|
|
|
744
1064
|
<div class="twm-cmdpal__inputrow">
|
|
745
1065
|
<span class="twm-cmdpal__type-chip-slot" data-role="type-chip"></span>
|
|
746
1066
|
<input type="text" class="twm-cmdpal__input"
|
|
747
|
-
placeholder="${
|
|
1067
|
+
placeholder="${_esc2(placeholder)}"
|
|
748
1068
|
autocomplete="off" />
|
|
749
1069
|
</div>
|
|
750
1070
|
<div class="twm-cmdpal__chips">${chips}</div>
|
|
@@ -862,7 +1182,7 @@ function _match(text, query) {
|
|
|
862
1182
|
}
|
|
863
1183
|
function _mark(text, hits) {
|
|
864
1184
|
const s = String(text ?? "");
|
|
865
|
-
if (!hits?.length) return
|
|
1185
|
+
if (!hits?.length) return _esc2(s);
|
|
866
1186
|
const set = new Set(hits);
|
|
867
1187
|
let out = "";
|
|
868
1188
|
let open = false;
|
|
@@ -876,12 +1196,12 @@ function _mark(text, hits) {
|
|
|
876
1196
|
out += "</mark>";
|
|
877
1197
|
open = false;
|
|
878
1198
|
}
|
|
879
|
-
out +=
|
|
1199
|
+
out += _esc2(s[i]);
|
|
880
1200
|
}
|
|
881
1201
|
if (open) out += "</mark>";
|
|
882
1202
|
return out;
|
|
883
1203
|
}
|
|
884
|
-
function
|
|
1204
|
+
function _esc2(s) {
|
|
885
1205
|
return String(s ?? "").replace(/[&<>"']/g, (c) => ({
|
|
886
1206
|
"&": "&",
|
|
887
1207
|
"<": "<",
|
|
@@ -902,7 +1222,7 @@ function windowPlaceholderFactory(hostEl, props, ctx) {
|
|
|
902
1222
|
hostEl.innerHTML = `
|
|
903
1223
|
<div class="twm-window-placeholder">
|
|
904
1224
|
<span class="material-symbols-outlined twm-window-placeholder__icon">open_in_new</span>
|
|
905
|
-
<div class="twm-window-placeholder__title">${
|
|
1225
|
+
<div class="twm-window-placeholder__title">${_esc3(props.originalTitle || props.originalKind || "content")}</div>
|
|
906
1226
|
<div class="twm-window-placeholder__hint">is open in a managed window</div>
|
|
907
1227
|
<button class="twm-window-placeholder__btn" data-action="bring-back">
|
|
908
1228
|
Bring back to this tile
|
|
@@ -954,7 +1274,7 @@ function _placeholder(kind) {
|
|
|
954
1274
|
return { title: kind };
|
|
955
1275
|
};
|
|
956
1276
|
}
|
|
957
|
-
function
|
|
1277
|
+
function _esc3(s) {
|
|
958
1278
|
return String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
959
1279
|
}
|
|
960
1280
|
|
|
@@ -1804,6 +2124,11 @@ function _makeDesktop(label, seed, panelDefaults = DEFAULT_PANEL_STATE) {
|
|
|
1804
2124
|
// when it asked for nothing. Names are assigned by wm._canonicalize via
|
|
1805
2125
|
// PANEL_TITLES.
|
|
1806
2126
|
panels: { ...panelDefaults },
|
|
2127
|
+
// Each panel's last share of the WM area along its axis, recorded by
|
|
2128
|
+
// the WM when a splitter drag ends (and, for a panel not yet
|
|
2129
|
+
// measured, when it rebuilds the panels). A side with none opens at
|
|
2130
|
+
// the default ratio.
|
|
2131
|
+
panelShares: {},
|
|
1807
2132
|
// C40. Both unset is a desktop exactly as it always was.
|
|
1808
2133
|
group: null,
|
|
1809
2134
|
fixed: false
|
|
@@ -1927,6 +2252,9 @@ var DesktopManager = class _DesktopManager {
|
|
|
1927
2252
|
label: d.label,
|
|
1928
2253
|
tree: d.tree.serialize(),
|
|
1929
2254
|
panels: { ...d.panels },
|
|
2255
|
+
// Written only when a panel has been measured, like the C40
|
|
2256
|
+
// keys below.
|
|
2257
|
+
...Object.keys(d.panelShares).length ? { panelShares: { ...d.panelShares } } : {},
|
|
1930
2258
|
// C40. Written only when set, so a blob without them is
|
|
1931
2259
|
// byte-for-byte the blob it always was.
|
|
1932
2260
|
...d.group ? { group: d.group } : {},
|
|
@@ -1949,6 +2277,7 @@ var DesktopManager = class _DesktopManager {
|
|
|
1949
2277
|
// this merge order avoids. The default only fills a key the stored
|
|
1950
2278
|
// blob predates.
|
|
1951
2279
|
panels: { ...m.panelDefaults, ...raw.panels || {} },
|
|
2280
|
+
panelShares: { ...raw.panelShares || {} },
|
|
1952
2281
|
group: raw.group || null,
|
|
1953
2282
|
fixed: !!raw.fixed
|
|
1954
2283
|
}));
|
|
@@ -2091,120 +2420,25 @@ function createEntityCatalog({ sources, aliases = {}, aggregate = null } = {}) {
|
|
|
2091
2420
|
});
|
|
2092
2421
|
}
|
|
2093
2422
|
|
|
2094
|
-
// src/tiling/
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
}
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
return;
|
|
2114
|
-
}
|
|
2115
|
-
}
|
|
2116
|
-
if (e.key === "Backspace" && !inField && !e.metaKey && !e.ctrlKey) {
|
|
2117
|
-
e.preventDefault();
|
|
2118
|
-
wm.navigateBack?.();
|
|
2119
|
-
return;
|
|
2120
|
-
}
|
|
2121
|
-
const fMatch = /^F([1-9]|1[0-2])$/.exec(e.key);
|
|
2122
|
-
if (fMatch && !inField && !e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
|
|
2123
|
-
const btns = document.querySelectorAll(
|
|
2124
|
-
opts.navSelector || ".twm-global-top-bar .twm-bar-center.twm-top-nav .twm-top-nav__btn"
|
|
2125
|
-
);
|
|
2126
|
-
const idx = Number(fMatch[1]) - 1;
|
|
2127
|
-
if (idx < btns.length) {
|
|
2128
|
-
e.preventDefault();
|
|
2129
|
-
btns[idx].click();
|
|
2130
|
-
return;
|
|
2131
|
-
}
|
|
2132
|
-
}
|
|
2133
|
-
if (e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey && !inField && (e.key === "ArrowLeft" || e.key === "ArrowRight")) {
|
|
2134
|
-
const tabs = [...document.querySelectorAll(".twm-bp__tabs .twm-bp__tab")];
|
|
2135
|
-
if (tabs.length) {
|
|
2136
|
-
const cur = tabs.findIndex((t) => t.classList.contains("twm-bp__tab--on"));
|
|
2137
|
-
const dir = e.key === "ArrowRight" ? 1 : -1;
|
|
2138
|
-
const next = ((cur < 0 ? 0 : cur) + dir + tabs.length) % tabs.length;
|
|
2139
|
-
e.preventDefault();
|
|
2140
|
-
tabs[next].click();
|
|
2141
|
-
return;
|
|
2142
|
-
}
|
|
2143
|
-
}
|
|
2144
|
-
if (e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey && !inField && (e.key === "ArrowLeft" || e.key === "ArrowRight")) {
|
|
2145
|
-
e.preventDefault();
|
|
2146
|
-
wm.cycleDesktop(e.key === "ArrowRight" ? 1 : -1);
|
|
2147
|
-
return;
|
|
2148
|
-
}
|
|
2149
|
-
if (!e.altKey) return;
|
|
2150
|
-
const key = e.key.toLowerCase();
|
|
2151
|
-
if (/^[1-9]$/.test(e.key)) {
|
|
2152
|
-
e.preventDefault();
|
|
2153
|
-
const idx = Number(e.key) - 1;
|
|
2154
|
-
if (e.shiftKey) wm.moveFocusedToDesktop(idx);
|
|
2155
|
-
else wm.switchDesktop(idx);
|
|
2156
|
-
return;
|
|
2157
|
-
}
|
|
2158
|
-
switch (key) {
|
|
2159
|
-
// Plain Alt+H/V split into an empty pane; with Shift they open
|
|
2160
|
-
// the focused tile's own content in the new split.
|
|
2161
|
-
case "h":
|
|
2162
|
-
e.preventDefault();
|
|
2163
|
-
e.shiftKey ? wm.splitFocusedWith("h") : wm.split("h");
|
|
2164
|
-
return;
|
|
2165
|
-
case "v":
|
|
2166
|
-
e.preventDefault();
|
|
2167
|
-
e.shiftKey ? wm.splitFocusedWith("v") : wm.split("v");
|
|
2168
|
-
return;
|
|
2169
|
-
case "t":
|
|
2170
|
-
e.preventDefault();
|
|
2171
|
-
wm.openFocusedInTab();
|
|
2172
|
-
return;
|
|
2173
|
-
case "n":
|
|
2174
|
-
e.preventDefault();
|
|
2175
|
-
wm.openFocusedInWindow();
|
|
2176
|
-
return;
|
|
2177
|
-
case "w":
|
|
2178
|
-
e.preventDefault();
|
|
2179
|
-
wm.closeFocused();
|
|
2180
|
-
return;
|
|
2181
|
-
case "f":
|
|
2182
|
-
e.preventDefault();
|
|
2183
|
-
wm.toggleManagedFocused();
|
|
2184
|
-
return;
|
|
2185
|
-
case "arrowleft":
|
|
2186
|
-
e.preventDefault();
|
|
2187
|
-
e.shiftKey ? wm.moveFocused("left") : wm.focusDir("left");
|
|
2188
|
-
return;
|
|
2189
|
-
case "arrowright":
|
|
2190
|
-
e.preventDefault();
|
|
2191
|
-
e.shiftKey ? wm.moveFocused("right") : wm.focusDir("right");
|
|
2192
|
-
return;
|
|
2193
|
-
case "arrowup":
|
|
2194
|
-
e.preventDefault();
|
|
2195
|
-
e.shiftKey ? wm.moveFocused("up") : wm.focusDir("up");
|
|
2196
|
-
return;
|
|
2197
|
-
case "arrowdown":
|
|
2198
|
-
e.preventDefault();
|
|
2199
|
-
e.shiftKey ? wm.moveFocused("down") : wm.focusDir("down");
|
|
2200
|
-
return;
|
|
2201
|
-
}
|
|
2202
|
-
if (key.length === 1 && /[a-z]/.test(key) && !inField) {
|
|
2203
|
-
e.preventDefault();
|
|
2204
|
-
}
|
|
2205
|
-
};
|
|
2206
|
-
document.addEventListener("keydown", onKeyDown);
|
|
2207
|
-
return () => document.removeEventListener("keydown", onKeyDown);
|
|
2423
|
+
// src/tiling/panel_availability.js
|
|
2424
|
+
var PANEL_SIDES = Object.freeze(["left", "right", "bottom"]);
|
|
2425
|
+
var PANEL_OFFERED = Object.freeze({ available: true, reason: null });
|
|
2426
|
+
function readPanelOffer(offer, where) {
|
|
2427
|
+
if (offer === true) return PANEL_OFFERED;
|
|
2428
|
+
if (offer === false) return Object.freeze({ available: false, reason: null });
|
|
2429
|
+
if (offer && typeof offer === "object" && !Array.isArray(offer)) {
|
|
2430
|
+
const { reason = null } = offer;
|
|
2431
|
+
if (reason !== null && typeof reason !== "string") {
|
|
2432
|
+
throw new TypeError(`${where}: a panel's \`reason\` must be a string`);
|
|
2433
|
+
}
|
|
2434
|
+
return Object.freeze({ available: false, reason: reason || null });
|
|
2435
|
+
}
|
|
2436
|
+
throw new TypeError(`${where}: a panel offer is true, false or { reason }`);
|
|
2437
|
+
}
|
|
2438
|
+
function assertPanelSide(side, where) {
|
|
2439
|
+
if (!PANEL_SIDES.includes(side)) {
|
|
2440
|
+
throw new TypeError(`${where}: unknown panel side '${side}' (expected ${PANEL_SIDES.join(", ")})`);
|
|
2441
|
+
}
|
|
2208
2442
|
}
|
|
2209
2443
|
|
|
2210
2444
|
// src/tiling/kind_taxonomy.js
|
|
@@ -2229,6 +2463,27 @@ function createTaxonomy({ kinds, root } = {}) {
|
|
|
2229
2463
|
`createTaxonomy: '${kind}' is both a top-nav kind and owned by one`
|
|
2230
2464
|
);
|
|
2231
2465
|
}
|
|
2466
|
+
if (def.panels !== void 0) {
|
|
2467
|
+
if (!def.panels || typeof def.panels !== "object" || Array.isArray(def.panels)) {
|
|
2468
|
+
throw new TypeError(`createTaxonomy: '${kind}'.panels must be an object`);
|
|
2469
|
+
}
|
|
2470
|
+
for (const [side, offer] of Object.entries(def.panels)) {
|
|
2471
|
+
if (!PANEL_SIDES.includes(side)) {
|
|
2472
|
+
throw new Error(`createTaxonomy: '${kind}'.panels names unknown side '${side}'`);
|
|
2473
|
+
}
|
|
2474
|
+
readPanelOffer(offer, `createTaxonomy: '${kind}'.panels.${side}`);
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
if (def.single != null && def.single !== false && def.single !== true && !(typeof def.single === "string" && def.single)) {
|
|
2478
|
+
throw new TypeError(
|
|
2479
|
+
`createTaxonomy: '${kind}'.single must be true or a prop name`
|
|
2480
|
+
);
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
if (kinds[root].single) {
|
|
2484
|
+
throw new Error(
|
|
2485
|
+
`createTaxonomy: the root kind '${root}' cannot be single \u2014 every new pane, tab and desktop is seeded with it`
|
|
2486
|
+
);
|
|
2232
2487
|
}
|
|
2233
2488
|
const _kinds = Object.freeze({ ...kinds });
|
|
2234
2489
|
const meta = (kind) => _kinds[kind];
|
|
@@ -2278,12 +2533,31 @@ function createTaxonomy({ kinds, root } = {}) {
|
|
|
2278
2533
|
const s = _kinds[kind]?.sources ?? _kinds[root]?.sources;
|
|
2279
2534
|
return Array.isArray(s) ? s : [];
|
|
2280
2535
|
};
|
|
2536
|
+
const panelOffer = (kind, side) => {
|
|
2537
|
+
for (const k of [kind, _kinds[kind]?.topNav]) {
|
|
2538
|
+
const offer = k ? _kinds[k]?.panels?.[side] : void 0;
|
|
2539
|
+
if (offer !== void 0) return readPanelOffer(offer, `taxonomy '${k}'.panels.${side}`);
|
|
2540
|
+
}
|
|
2541
|
+
return PANEL_OFFERED;
|
|
2542
|
+
};
|
|
2281
2543
|
const topNavEntries = () => Object.entries(_kinds).filter(([, m]) => m.isTopNav).sort((a, b) => (a[1].order ?? 1e3) - (b[1].order ?? 1e3)).map(([kind, m]) => ({
|
|
2282
2544
|
kind,
|
|
2283
2545
|
label: m.shortLabel || m.label,
|
|
2284
2546
|
longLabel: m.label,
|
|
2285
2547
|
icon: m.icon
|
|
2286
2548
|
}));
|
|
2549
|
+
const isSingle = (kind) => {
|
|
2550
|
+
const s = _kinds[kind]?.single;
|
|
2551
|
+
return s === true || typeof s === "string" && s !== "";
|
|
2552
|
+
};
|
|
2553
|
+
const instanceKey = (kind, props) => {
|
|
2554
|
+
if (!isSingle(kind)) return null;
|
|
2555
|
+
const by = _kinds[kind].single;
|
|
2556
|
+
if (by === true) return String(kind);
|
|
2557
|
+
const v = props?.[by];
|
|
2558
|
+
return `${kind}\0${v == null ? "" : String(v)}`;
|
|
2559
|
+
};
|
|
2560
|
+
const singleReason = (kind) => `${_kinds[kind]?.label || kind} is open once: use the one that is open`;
|
|
2287
2561
|
return Object.freeze({
|
|
2288
2562
|
root,
|
|
2289
2563
|
kinds: _kinds,
|
|
@@ -2294,7 +2568,11 @@ function createTaxonomy({ kinds, root } = {}) {
|
|
|
2294
2568
|
parentOf,
|
|
2295
2569
|
labelOf,
|
|
2296
2570
|
sourcesFor,
|
|
2297
|
-
|
|
2571
|
+
panelOffer,
|
|
2572
|
+
topNavEntries,
|
|
2573
|
+
isSingle,
|
|
2574
|
+
instanceKey,
|
|
2575
|
+
singleReason
|
|
2298
2576
|
});
|
|
2299
2577
|
}
|
|
2300
2578
|
|
|
@@ -2760,7 +3038,7 @@ function mountLandingShell(hostEl, opts) {
|
|
|
2760
3038
|
pane = { key: "rows", title: "", hint: "" }
|
|
2761
3039
|
} = opts || {};
|
|
2762
3040
|
const paneKey = String(pane.key || "rows");
|
|
2763
|
-
const
|
|
3041
|
+
const _esc9 = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({
|
|
2764
3042
|
"&": "&",
|
|
2765
3043
|
"<": "<",
|
|
2766
3044
|
">": ">",
|
|
@@ -2769,24 +3047,24 @@ function mountLandingShell(hostEl, opts) {
|
|
|
2769
3047
|
})[c]);
|
|
2770
3048
|
const statsHtml = (stats || []).map((s) => `
|
|
2771
3049
|
<span class="twm-landing__stat twm-entity-header__field">
|
|
2772
|
-
<span>${
|
|
2773
|
-
<strong data-role="${
|
|
3050
|
+
<span>${_esc9(s.label || "")}</span>
|
|
3051
|
+
<strong data-role="${_esc9(s.key)}">${_esc9(s.initial != null ? s.initial : 0)}</strong>
|
|
2774
3052
|
</span>
|
|
2775
3053
|
`).join("");
|
|
2776
3054
|
hostEl.classList.add("twm-landing");
|
|
2777
3055
|
hostEl.innerHTML = `
|
|
2778
3056
|
<header class="twm-entity-header">
|
|
2779
|
-
<h2>${
|
|
3057
|
+
<h2>${_esc9(title)}</h2>
|
|
2780
3058
|
${statsHtml}
|
|
2781
3059
|
<span class="twm-entity-header__spacer"></span>
|
|
2782
3060
|
</header>
|
|
2783
3061
|
<div class="twm-landing__split twm-landing__split--single">
|
|
2784
|
-
<section class="twm-landing__pane" data-pane="${
|
|
3062
|
+
<section class="twm-landing__pane" data-pane="${_esc9(paneKey)}">
|
|
2785
3063
|
<div class="twm-landing__pane-head">
|
|
2786
|
-
<span class="twm-landing__pane-title">${
|
|
2787
|
-
<span class="twm-landing__pane-hint">${
|
|
3064
|
+
<span class="twm-landing__pane-title">${_esc9(pane.title || "")}</span>
|
|
3065
|
+
<span class="twm-landing__pane-hint">${_esc9(pane.hint || "")}</span>
|
|
2788
3066
|
</div>
|
|
2789
|
-
<div class="twm-landing__table" data-role="${
|
|
3067
|
+
<div class="twm-landing__table" data-role="${_esc9(paneKey)}-host"></div>
|
|
2790
3068
|
</section>
|
|
2791
3069
|
</div>
|
|
2792
3070
|
<footer class="twm-landing__actions" data-role="actions"></footer>
|
|
@@ -2815,9 +3093,9 @@ function mountLandingActions(footerEl, actions, hostEl) {
|
|
|
2815
3093
|
btn.className = "twm-action-btn";
|
|
2816
3094
|
if (a.id) btn.dataset.action = a.id;
|
|
2817
3095
|
if (a.title) btn.title = a.title;
|
|
2818
|
-
const icon = a.icon ? `<span class="material-symbols-outlined">${
|
|
2819
|
-
const sc = a.shortcut ? ` <span class="twm-action-btn__kbd">[${
|
|
2820
|
-
btn.innerHTML = `${icon}<span>${
|
|
3096
|
+
const icon = a.icon ? `<span class="material-symbols-outlined">${_esc4(a.icon)}</span>` : "";
|
|
3097
|
+
const sc = a.shortcut ? ` <span class="twm-action-btn__kbd">[${_esc4(a.shortcut.toUpperCase())}]</span>` : "";
|
|
3098
|
+
btn.innerHTML = `${icon}<span>${_esc4(a.label || "")}</span>${sc}`;
|
|
2821
3099
|
btn.addEventListener("click", (ev) => {
|
|
2822
3100
|
ev.preventDefault();
|
|
2823
3101
|
try {
|
|
@@ -2878,7 +3156,7 @@ function actionsCellRenderer(actionsColIdx, options = { edit: true, delete: true
|
|
|
2878
3156
|
return true;
|
|
2879
3157
|
};
|
|
2880
3158
|
}
|
|
2881
|
-
function
|
|
3159
|
+
function _esc4(s) {
|
|
2882
3160
|
return String(s ?? "").replace(/[&<>"']/g, (c) => ({
|
|
2883
3161
|
"&": "&",
|
|
2884
3162
|
"<": "<",
|
|
@@ -2908,37 +3186,46 @@ function mountNavPanel(hostEl, {
|
|
|
2908
3186
|
modes = [],
|
|
2909
3187
|
wm = null,
|
|
2910
3188
|
eventBus = null,
|
|
2911
|
-
filterPlaceholder = "Filter\u2026"
|
|
3189
|
+
filterPlaceholder = "Filter\u2026",
|
|
3190
|
+
showModeStrip = true,
|
|
3191
|
+
showFilter = true,
|
|
3192
|
+
initialMode = null
|
|
2912
3193
|
} = {}) {
|
|
2913
3194
|
if (!Array.isArray(modes) || modes.length === 0) {
|
|
2914
3195
|
throw new Error("mountNavPanel: at least one mode is required");
|
|
2915
3196
|
}
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
3197
|
+
const stripHtml = showModeStrip ? `
|
|
3198
|
+
<div class="twm-nav__modes" data-role="modes"></div>` : "";
|
|
3199
|
+
const filterHtml = showFilter ? `
|
|
2919
3200
|
<div class="twm-sidebar-controls twm-nav__filter">
|
|
2920
3201
|
<span class="material-symbols-outlined twm-sidebar-search-icon">search</span>
|
|
2921
3202
|
<input class="twm-data-page__search twm-nav__filter-input"
|
|
2922
|
-
type="search" placeholder="${
|
|
3203
|
+
type="search" placeholder="${_esc5(filterPlaceholder)}"
|
|
2923
3204
|
autocomplete="off" />
|
|
2924
|
-
</div
|
|
3205
|
+
</div>` : "";
|
|
3206
|
+
hostEl.classList.add("twm-nav");
|
|
3207
|
+
hostEl.innerHTML = `${stripHtml}${filterHtml}
|
|
2925
3208
|
<div class="twm-nav__body" data-role="body"></div>
|
|
2926
3209
|
`;
|
|
2927
3210
|
const modesEl = hostEl.querySelector('[data-role="modes"]');
|
|
2928
3211
|
const bodyEl = hostEl.querySelector('[data-role="body"]');
|
|
3212
|
+
const filterBoxEl = hostEl.querySelector(".twm-nav__filter");
|
|
2929
3213
|
const filterEl = hostEl.querySelector(".twm-nav__filter-input");
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
<span>${
|
|
3214
|
+
if (modesEl) {
|
|
3215
|
+
modesEl.innerHTML = modes.map((m) => `
|
|
3216
|
+
<button class="twm-nav__mode twm-has-tooltip" data-mode="${_esc5(m.id)}"
|
|
3217
|
+
data-tooltip="${_esc5(m.label)}">
|
|
3218
|
+
<span class="material-symbols-outlined">${_esc5(m.icon)}</span>
|
|
3219
|
+
<span>${_esc5(m.label)}</span>
|
|
2935
3220
|
</button>
|
|
2936
3221
|
`).join("");
|
|
3222
|
+
}
|
|
2937
3223
|
const byId = new Map(modes.map((m) => [m.id, m]));
|
|
2938
3224
|
const hosts = {};
|
|
2939
3225
|
const panes = {};
|
|
2940
3226
|
const filterByMode = Object.fromEntries(modes.map((m) => [m.id, ""]));
|
|
2941
|
-
let mode = modes[0].id;
|
|
3227
|
+
let mode = byId.has(initialMode) ? initialMode : modes[0].id;
|
|
3228
|
+
const hasFilter = (id) => !!filterEl && byId.get(id)?.filter !== false;
|
|
2942
3229
|
const ensureHost = (id) => {
|
|
2943
3230
|
if (hosts[id]) return hosts[id];
|
|
2944
3231
|
const el = document.createElement("div");
|
|
@@ -2955,8 +3242,12 @@ function mountNavPanel(hostEl, {
|
|
|
2955
3242
|
for (const m of modes) {
|
|
2956
3243
|
ensureHost(m.id).style.display = m.id === next ? "flex" : "none";
|
|
2957
3244
|
}
|
|
2958
|
-
modesEl
|
|
2959
|
-
filterEl
|
|
3245
|
+
modesEl?.querySelectorAll("[data-mode]").forEach((b) => b.classList.toggle("twm-nav__mode--on", b.dataset.mode === next));
|
|
3246
|
+
if (filterEl) {
|
|
3247
|
+
if (!hasFilter(next)) filterBoxEl.style.display = "none";
|
|
3248
|
+
else if (filterBoxEl.style.display) filterBoxEl.style.removeProperty("display");
|
|
3249
|
+
filterEl.value = hasFilter(next) ? filterByMode[next] || "" : "";
|
|
3250
|
+
}
|
|
2960
3251
|
const paneEl = ensureHost(next);
|
|
2961
3252
|
if (!panes[next]) {
|
|
2962
3253
|
try {
|
|
@@ -2974,6 +3265,7 @@ function mountNavPanel(hostEl, {
|
|
|
2974
3265
|
_applyFilter();
|
|
2975
3266
|
};
|
|
2976
3267
|
const _applyFilter = () => {
|
|
3268
|
+
if (!hasFilter(mode)) return;
|
|
2977
3269
|
const q = filterByMode[mode] || "";
|
|
2978
3270
|
try {
|
|
2979
3271
|
panes[mode]?.filter?.(q);
|
|
@@ -2981,16 +3273,19 @@ function mountNavPanel(hostEl, {
|
|
|
2981
3273
|
console.warn("[nav] filter failed", mode, err);
|
|
2982
3274
|
}
|
|
2983
3275
|
};
|
|
2984
|
-
filterEl
|
|
3276
|
+
filterEl?.addEventListener("input", () => {
|
|
3277
|
+
if (!hasFilter(mode)) return;
|
|
2985
3278
|
filterByMode[mode] = filterEl.value;
|
|
2986
3279
|
_applyFilter();
|
|
2987
3280
|
});
|
|
2988
|
-
modesEl
|
|
3281
|
+
modesEl?.addEventListener("click", (e) => {
|
|
2989
3282
|
const btn = e.target.closest("[data-mode]");
|
|
2990
3283
|
if (btn) activate(btn.dataset.mode);
|
|
2991
3284
|
});
|
|
2992
3285
|
activate(mode);
|
|
2993
3286
|
return {
|
|
3287
|
+
select: (modeId) => activate(modeId),
|
|
3288
|
+
active: () => mode,
|
|
2994
3289
|
refresh: () => activate(mode),
|
|
2995
3290
|
destroy: () => {
|
|
2996
3291
|
for (const p of Object.values(panes)) {
|
|
@@ -3002,7 +3297,7 @@ function mountNavPanel(hostEl, {
|
|
|
3002
3297
|
}
|
|
3003
3298
|
};
|
|
3004
3299
|
}
|
|
3005
|
-
function
|
|
3300
|
+
function _esc5(s) {
|
|
3006
3301
|
return String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
3007
3302
|
}
|
|
3008
3303
|
|
|
@@ -3156,8 +3451,8 @@ function _renderInto(container, segments) {
|
|
|
3156
3451
|
li.classList.add("twm-topbar-breadcrumb__item--current");
|
|
3157
3452
|
li.setAttribute("aria-current", "page");
|
|
3158
3453
|
li.innerHTML = `
|
|
3159
|
-
<span class="material-symbols-outlined twm-topbar-breadcrumb__icon">${
|
|
3160
|
-
<span class="twm-topbar-breadcrumb__current" title="${
|
|
3454
|
+
<span class="material-symbols-outlined twm-topbar-breadcrumb__icon">${_esc6(seg.icon)}</span>
|
|
3455
|
+
<span class="twm-topbar-breadcrumb__current" title="${_esc6(seg.label)}">${_esc6(seg.label)}</span>
|
|
3161
3456
|
`;
|
|
3162
3457
|
} else {
|
|
3163
3458
|
const btn = document.createElement("button");
|
|
@@ -3165,8 +3460,8 @@ function _renderInto(container, segments) {
|
|
|
3165
3460
|
btn.type = "button";
|
|
3166
3461
|
btn.title = String(seg.label ?? "");
|
|
3167
3462
|
btn.innerHTML = `
|
|
3168
|
-
<span class="material-symbols-outlined twm-topbar-breadcrumb__icon">${
|
|
3169
|
-
<span>${
|
|
3463
|
+
<span class="material-symbols-outlined twm-topbar-breadcrumb__icon">${_esc6(seg.icon)}</span>
|
|
3464
|
+
<span>${_esc6(seg.label)}</span>
|
|
3170
3465
|
`;
|
|
3171
3466
|
btn.addEventListener("click", () => {
|
|
3172
3467
|
btn.classList.add("twm-tile-breadcrumb__link--flash");
|
|
@@ -3189,7 +3484,7 @@ function _renderInto(container, segments) {
|
|
|
3189
3484
|
});
|
|
3190
3485
|
container.appendChild(ol);
|
|
3191
3486
|
}
|
|
3192
|
-
function
|
|
3487
|
+
function _esc6(s) {
|
|
3193
3488
|
return String(s ?? "").replace(/[&<>"']/g, (c) => ({
|
|
3194
3489
|
"&": "&",
|
|
3195
3490
|
"<": "<",
|
|
@@ -3274,7 +3569,7 @@ function createPageFactories({ eventBus = null, events = {}, refreshOn = {} } =
|
|
|
3274
3569
|
console.error("[page] mount failed", err);
|
|
3275
3570
|
hideLoading();
|
|
3276
3571
|
}
|
|
3277
|
-
const
|
|
3572
|
+
const subscriptions = [];
|
|
3278
3573
|
if (typeof tab?.refresh === "function" && eventBus?.on) {
|
|
3279
3574
|
for (const [busEvent, reason] of Object.entries(refreshOn || {})) {
|
|
3280
3575
|
const fn = () => {
|
|
@@ -3284,14 +3579,13 @@ function createPageFactories({ eventBus = null, events = {}, refreshOn = {} } =
|
|
|
3284
3579
|
console.warn("[page] refresh failed", busEvent, err);
|
|
3285
3580
|
}
|
|
3286
3581
|
};
|
|
3287
|
-
|
|
3288
|
-
eventBus.on(busEvent, fn);
|
|
3582
|
+
subscriptions.push(eventBus.on(busEvent, fn));
|
|
3289
3583
|
}
|
|
3290
3584
|
}
|
|
3291
3585
|
return () => {
|
|
3292
|
-
for (const
|
|
3586
|
+
for (const handle of subscriptions) {
|
|
3293
3587
|
try {
|
|
3294
|
-
|
|
3588
|
+
handle?.dispose?.();
|
|
3295
3589
|
} catch {
|
|
3296
3590
|
}
|
|
3297
3591
|
}
|
|
@@ -3378,6 +3672,7 @@ function createPageFactories({ eventBus = null, events = {}, refreshOn = {} } =
|
|
|
3378
3672
|
|
|
3379
3673
|
// src/tiling/tile_renderer.js
|
|
3380
3674
|
var SPLITTER_PX = 4;
|
|
3675
|
+
var MIN_SLOT_FRACTION = 0.05;
|
|
3381
3676
|
var TAB_LAYOUTS = ["bottom", "top", "header"];
|
|
3382
3677
|
var DEFAULT_TAB_LAYOUT = "bottom";
|
|
3383
3678
|
function _normalizeTabLayout(value) {
|
|
@@ -3410,6 +3705,12 @@ var TileRenderer = class {
|
|
|
3410
3705
|
static get TAB_MIME() {
|
|
3411
3706
|
return TILE_TAB_MIME;
|
|
3412
3707
|
}
|
|
3708
|
+
/** The smallest share of its split a splitter drag leaves a slot. Readable
|
|
3709
|
+
* so a caller that sets split sizes itself (the WM rebuilding its panel
|
|
3710
|
+
* splits) never builds one a drag could not have produced. */
|
|
3711
|
+
static get MIN_SLOT_FRACTION() {
|
|
3712
|
+
return MIN_SLOT_FRACTION;
|
|
3713
|
+
}
|
|
3413
3714
|
constructor({
|
|
3414
3715
|
root,
|
|
3415
3716
|
tree,
|
|
@@ -3417,6 +3718,7 @@ var TileRenderer = class {
|
|
|
3417
3718
|
ctx,
|
|
3418
3719
|
onFocusChange,
|
|
3419
3720
|
onAfterRender,
|
|
3721
|
+
onSplitResized,
|
|
3420
3722
|
tabLayout = null,
|
|
3421
3723
|
newTabButton = true
|
|
3422
3724
|
}) {
|
|
@@ -3431,6 +3733,8 @@ var TileRenderer = class {
|
|
|
3431
3733
|
});
|
|
3432
3734
|
this.onAfterRender = onAfterRender || (() => {
|
|
3433
3735
|
});
|
|
3736
|
+
this.onSplitResized = onSplitResized || (() => {
|
|
3737
|
+
});
|
|
3434
3738
|
this.newTabButton = newTabButton !== false;
|
|
3435
3739
|
this._leafCache = /* @__PURE__ */ new Map();
|
|
3436
3740
|
this._drag = null;
|
|
@@ -3693,6 +3997,7 @@ var TileRenderer = class {
|
|
|
3693
3997
|
let content = {};
|
|
3694
3998
|
let liveTitle = null;
|
|
3695
3999
|
let liveEntry = null;
|
|
4000
|
+
let liveActionsSet = false;
|
|
3696
4001
|
if (leaf.content) {
|
|
3697
4002
|
const setTitle = (text) => {
|
|
3698
4003
|
const next = text == null || text === "" ? null : String(text);
|
|
@@ -3706,13 +4011,22 @@ var TileRenderer = class {
|
|
|
3706
4011
|
const now = this.tree?.get?.(leaf.id) || leaf;
|
|
3707
4012
|
liveEntry.titleEl.textContent = next || now.title || (now.content ? now.content.kind : "empty");
|
|
3708
4013
|
};
|
|
3709
|
-
const
|
|
4014
|
+
const setChromeActions = (specs) => {
|
|
4015
|
+
if (!liveEntry) {
|
|
4016
|
+
liveActionsSet = true;
|
|
4017
|
+
_paintContentActions(contentActions, specs);
|
|
4018
|
+
return;
|
|
4019
|
+
}
|
|
4020
|
+
if (this._leafCache.get(leaf.id) !== liveEntry) return;
|
|
4021
|
+
_paintContentActions(liveEntry.contentActionsEl, specs);
|
|
4022
|
+
};
|
|
4023
|
+
const leafCtx = { ...this.ctx, leafId: leaf.id, setTitle, setChromeActions };
|
|
3710
4024
|
const activeProps = activeTab?.props ?? leaf.content.props;
|
|
3711
4025
|
content = this.content.mount(leaf.content.kind, body, activeProps, leafCtx);
|
|
3712
4026
|
if (content.title) title.textContent = content.title;
|
|
3713
4027
|
if (liveTitle) title.textContent = liveTitle;
|
|
3714
4028
|
_paintLeafIcon(icon, leaf, content, this.ctx);
|
|
3715
|
-
_paintContentActions(contentActions, content.chromeActions);
|
|
4029
|
+
if (!liveActionsSet) _paintContentActions(contentActions, content.chromeActions);
|
|
3716
4030
|
_vetoStructuralActions(actions, content.chrome);
|
|
3717
4031
|
} else {
|
|
3718
4032
|
body.innerHTML = `<div class="tile-placeholder"><div class="tile-placeholder__hint">empty tile</div></div>`;
|
|
@@ -3724,6 +4038,10 @@ var TileRenderer = class {
|
|
|
3724
4038
|
titleEl: title,
|
|
3725
4039
|
iconEl: icon,
|
|
3726
4040
|
content,
|
|
4041
|
+
// C42. The strip `ctx.setChromeActions` repaints after the
|
|
4042
|
+
// mount. A cached repaint never rebuilds it, which is what
|
|
4043
|
+
// makes a list set later survive one.
|
|
4044
|
+
contentActionsEl: contentActions,
|
|
3727
4045
|
// DERIVED HERE, NOT REUSED FROM ABOVE. `kindKey` was computed
|
|
3728
4046
|
// before the mount; content that records its sub-tab or its
|
|
3729
4047
|
// scroll offset WHILE mounting has already written to the
|
|
@@ -4126,7 +4444,7 @@ var TileRenderer = class {
|
|
|
4126
4444
|
_topTabCallbacks(leafId) {
|
|
4127
4445
|
return {
|
|
4128
4446
|
// C33. `DragReorder` calls this through `NotebookTabBar`, which
|
|
4129
|
-
// sets its own
|
|
4447
|
+
// sets its own tab MIME (getTabDragMime()) first and unchanged — so
|
|
4130
4448
|
// an editor pane sharing the page cannot notice that tiles are
|
|
4131
4449
|
// dragging tabs too.
|
|
4132
4450
|
onDragStart: (ev, key, item) => {
|
|
@@ -4197,9 +4515,9 @@ var TileRenderer = class {
|
|
|
4197
4515
|
${tabs.map((t, i) => `
|
|
4198
4516
|
<li class="twm-leaf__tab${i === activeIdx ? " twm-leaf__tab--on" : ""}"
|
|
4199
4517
|
role="tab" data-tab-idx="${i}"
|
|
4200
|
-
title="${
|
|
4518
|
+
title="${_esc7(t.title || t.kind)}"
|
|
4201
4519
|
draggable="true">
|
|
4202
|
-
<span class="twm-leaf__tab-label">${
|
|
4520
|
+
<span class="twm-leaf__tab-label">${_esc7(t.title || t.kind)}</span>
|
|
4203
4521
|
<button type="button" class="twm-leaf__tab-close"
|
|
4204
4522
|
data-action="tab-close" data-tab-idx="${i}"
|
|
4205
4523
|
title="Close this tab"
|
|
@@ -4550,7 +4868,7 @@ var TileRenderer = class {
|
|
|
4550
4868
|
const px = totalPx - SPLITTER_PX * (split.children.length - 1);
|
|
4551
4869
|
if (px <= 0) return;
|
|
4552
4870
|
const deltaFrac = delta / px * totalSize;
|
|
4553
|
-
const minFrac = totalSize *
|
|
4871
|
+
const minFrac = totalSize * MIN_SLOT_FRACTION;
|
|
4554
4872
|
const newSizes = startSizes.slice();
|
|
4555
4873
|
newSizes[slotIdx] = Math.max(minFrac, startSizes[slotIdx] + deltaFrac);
|
|
4556
4874
|
newSizes[slotIdx + 1] = Math.max(minFrac, startSizes[slotIdx + 1] - deltaFrac);
|
|
@@ -4567,11 +4885,20 @@ var TileRenderer = class {
|
|
|
4567
4885
|
});
|
|
4568
4886
|
}
|
|
4569
4887
|
_onMouseUp() {
|
|
4888
|
+
const drag = this._drag;
|
|
4570
4889
|
this._drag = null;
|
|
4571
4890
|
document.body.classList.remove("twm-dragging");
|
|
4891
|
+
if (!drag) return;
|
|
4892
|
+
const sizes = this.tree.get(drag.splitId)?.sizes;
|
|
4893
|
+
if (!sizes || sizes.every((v, i) => v === drag.startSizes[i])) return;
|
|
4894
|
+
try {
|
|
4895
|
+
this.onSplitResized(drag.splitId);
|
|
4896
|
+
} catch (err) {
|
|
4897
|
+
console.error("[tile-renderer] onSplitResized threw", err);
|
|
4898
|
+
}
|
|
4572
4899
|
}
|
|
4573
4900
|
};
|
|
4574
|
-
function
|
|
4901
|
+
function _esc7(s) {
|
|
4575
4902
|
return String(s ?? "").replace(/[&<>"']/g, (c) => ({
|
|
4576
4903
|
"&": "&",
|
|
4577
4904
|
"<": "<",
|
|
@@ -4595,15 +4922,41 @@ function _paintContentActions(hostEl, specs) {
|
|
|
4595
4922
|
}
|
|
4596
4923
|
hostEl.hidden = false;
|
|
4597
4924
|
for (const spec of specs) {
|
|
4598
|
-
if (!spec
|
|
4925
|
+
if (!spec) continue;
|
|
4926
|
+
if (spec.separator) {
|
|
4927
|
+
const sep = document.createElement("span");
|
|
4928
|
+
sep.className = "twm-leaf__sep";
|
|
4929
|
+
sep.setAttribute("role", "separator");
|
|
4930
|
+
hostEl.appendChild(sep);
|
|
4931
|
+
continue;
|
|
4932
|
+
}
|
|
4933
|
+
const label = spec.label == null || spec.label === "" ? "" : String(spec.label);
|
|
4934
|
+
if (!spec.icon && !label) continue;
|
|
4599
4935
|
const btn = document.createElement("button");
|
|
4600
4936
|
btn.type = "button";
|
|
4601
4937
|
btn.className = "twm-leaf__btn";
|
|
4602
4938
|
btn.title = spec.title || "";
|
|
4603
|
-
btn.setAttribute("aria-label", spec.title ||
|
|
4604
|
-
btn.innerHTML = `<span class="material-symbols-outlined">${spec.icon}</span
|
|
4939
|
+
btn.setAttribute("aria-label", spec.title || label);
|
|
4940
|
+
btn.innerHTML = spec.icon ? `<span class="material-symbols-outlined">${spec.icon}</span>` : "";
|
|
4941
|
+
if (label) {
|
|
4942
|
+
const text = document.createElement("span");
|
|
4943
|
+
text.className = "twm-leaf__btn-label";
|
|
4944
|
+
text.textContent = label;
|
|
4945
|
+
btn.appendChild(text);
|
|
4946
|
+
btn.classList.add("twm-leaf__btn--labelled");
|
|
4947
|
+
}
|
|
4948
|
+
if (spec.pressed != null) {
|
|
4949
|
+
btn.setAttribute("aria-pressed", spec.pressed ? "true" : "false");
|
|
4950
|
+
if (spec.pressed) btn.classList.add("twm-leaf__btn--pressed");
|
|
4951
|
+
}
|
|
4952
|
+
const disabled = !!spec.disabled;
|
|
4953
|
+
if (disabled) {
|
|
4954
|
+
btn.setAttribute("aria-disabled", "true");
|
|
4955
|
+
btn.classList.add("twm-leaf__btn--disabled");
|
|
4956
|
+
}
|
|
4605
4957
|
btn.addEventListener("click", (e) => {
|
|
4606
4958
|
e.stopPropagation();
|
|
4959
|
+
if (disabled) return;
|
|
4607
4960
|
try {
|
|
4608
4961
|
spec.onClick?.();
|
|
4609
4962
|
} catch (err) {
|
|
@@ -4777,6 +5130,11 @@ var WindowManager = class _WindowManager {
|
|
|
4777
5130
|
this.eventBus = eventBus || null;
|
|
4778
5131
|
this.onChange = onChange || (() => {
|
|
4779
5132
|
});
|
|
5133
|
+
this._panelOverrides = {};
|
|
5134
|
+
this._panelAvailability = Object.fromEntries(PANEL_SIDES.map((s) => [s, PANEL_OFFERED]));
|
|
5135
|
+
this._panelAvailabilityListeners = /* @__PURE__ */ new Set();
|
|
5136
|
+
this._reconcilingPanels = false;
|
|
5137
|
+
this._quietDepth = 0;
|
|
4780
5138
|
this.floatActiveTab = !!floatActiveTab;
|
|
4781
5139
|
this._rootLeaf = () => {
|
|
4782
5140
|
const kind = this.taxonomy.root;
|
|
@@ -4821,7 +5179,8 @@ var WindowManager = class _WindowManager {
|
|
|
4821
5179
|
onLeafTabAction: (leafId, action, data) => this._leafTabAction(leafId, action, data)
|
|
4822
5180
|
},
|
|
4823
5181
|
onFocusChange: () => this._notifyChange(),
|
|
4824
|
-
onAfterRender: () => this._rehomeContainedWindows()
|
|
5182
|
+
onAfterRender: () => this._rehomeContainedWindows(),
|
|
5183
|
+
onSplitResized: (splitId) => this._onSplitResized(splitId)
|
|
4825
5184
|
});
|
|
4826
5185
|
this._persistTimer = null;
|
|
4827
5186
|
this._windowToLeaf = /* @__PURE__ */ new Map();
|
|
@@ -4874,6 +5233,8 @@ var WindowManager = class _WindowManager {
|
|
|
4874
5233
|
* so other surfaces (palette, top-bar toggles, page shortcuts) can
|
|
4875
5234
|
* subscribe through the existing event system. */
|
|
4876
5235
|
_notifyChange(reason = null) {
|
|
5236
|
+
if (this._quietDepth > 0) return;
|
|
5237
|
+
this._reconcilePanels();
|
|
4877
5238
|
try {
|
|
4878
5239
|
this.onChange(reason);
|
|
4879
5240
|
} catch (err) {
|
|
@@ -4923,10 +5284,12 @@ var WindowManager = class _WindowManager {
|
|
|
4923
5284
|
}
|
|
4924
5285
|
}
|
|
4925
5286
|
}
|
|
4926
|
-
this._canonicalize(d.tree, d);
|
|
4927
5287
|
}
|
|
5288
|
+
const deduped = this._dropDuplicateSingles();
|
|
5289
|
+
for (const d of this.desktops.desktops) this._canonicalize(d.tree, d);
|
|
4928
5290
|
this.renderer.tree = this.desktops.active().tree;
|
|
4929
5291
|
this.renderer.render();
|
|
5292
|
+
if (deduped) this._persist();
|
|
4930
5293
|
this._notifyChange();
|
|
4931
5294
|
}
|
|
4932
5295
|
_persist() {
|
|
@@ -4936,6 +5299,276 @@ var WindowManager = class _WindowManager {
|
|
|
4936
5299
|
await saveDesktops(this.host?.state, this.desktops.serialize());
|
|
4937
5300
|
}, 500);
|
|
4938
5301
|
}
|
|
5302
|
+
// ── Single-instance kinds (KindDef `single`) ────────────────────
|
|
5303
|
+
//
|
|
5304
|
+
// Some content holds heavy app-wide state and must exist once. The
|
|
5305
|
+
// taxonomy says which kinds (`single: true`, or `single: '<prop>'` for one
|
|
5306
|
+
// per value of that prop); the WM keeps three promises about them:
|
|
5307
|
+
//
|
|
5308
|
+
// - every OPEN path reveals the instance that exists — it switches to
|
|
5309
|
+
// its desktop, activates its tab (restoring an archived page if that is
|
|
5310
|
+
// where it is) and focuses its tile, or raises its window — instead of
|
|
5311
|
+
// mounting a second one;
|
|
5312
|
+
// - the COPY verbs (Alt+T / Alt+N / Alt+Shift+H / Alt+Shift+V, the tile
|
|
5313
|
+
// menu's "Open in new tab" and "Open a copy in a window", the palette's
|
|
5314
|
+
// split and window modes) are refused, the menus greyed with a reason;
|
|
5315
|
+
// - a restored layout holding duplicates keeps the FIRST (desktop order,
|
|
5316
|
+
// then tile order; live tabs before archived pages).
|
|
5317
|
+
//
|
|
5318
|
+
// MOVE verbs (float, dock, drag a tab, move to desktop) carry the one
|
|
5319
|
+
// instance somewhere else and are untouched.
|
|
5320
|
+
/** The instance key of a tab spec, or null when its kind is not single. */
|
|
5321
|
+
_instanceKey(kind, props) {
|
|
5322
|
+
return this.taxonomy.instanceKey(kind, props);
|
|
5323
|
+
}
|
|
5324
|
+
/** Whether `kind` exists once, and so refuses the copy verbs. */
|
|
5325
|
+
isSingleKind(kind) {
|
|
5326
|
+
return this.taxonomy.isSingle(kind);
|
|
5327
|
+
}
|
|
5328
|
+
/** Where the one instance of a single kind is open, or null — also null
|
|
5329
|
+
* for a kind that is not single. One of:
|
|
5330
|
+
* `{ where: 'window', windowId, idx, desktopIdx }`
|
|
5331
|
+
* `{ where: 'tab', desktopIdx, leafId, idx }`
|
|
5332
|
+
* `{ where: 'archive', desktopIdx, leafId, page, idx }` — a tab of a
|
|
5333
|
+
* top-nav page the tile has switched away from (`pageTabs`). */
|
|
5334
|
+
findInstance(kind, props = {}) {
|
|
5335
|
+
const key = this._instanceKey(kind, props);
|
|
5336
|
+
if (key == null) return null;
|
|
5337
|
+
const same = (t) => !!t && this._instanceKey(t.kind, t.props) === key;
|
|
5338
|
+
for (const [windowId, rec] of this._windowToLeaf) {
|
|
5339
|
+
const idx = _windowTabSpecs(rec).findIndex(same);
|
|
5340
|
+
if (idx >= 0) return { where: "window", windowId, idx, desktopIdx: rec.desktopIdx };
|
|
5341
|
+
}
|
|
5342
|
+
const desktops = this.desktops.desktops;
|
|
5343
|
+
for (const [desktopIdx, d] of desktops.entries()) {
|
|
5344
|
+
for (const leaf of d.tree.leaves()) {
|
|
5345
|
+
const idx = (leaf.tabs || []).findIndex(same);
|
|
5346
|
+
if (idx >= 0) return { where: "tab", desktopIdx, leafId: leaf.id, idx };
|
|
5347
|
+
}
|
|
5348
|
+
}
|
|
5349
|
+
for (const [desktopIdx, d] of desktops.entries()) {
|
|
5350
|
+
for (const leaf of d.tree.leaves()) {
|
|
5351
|
+
for (const [page, arc] of this._archivedPages(leaf)) {
|
|
5352
|
+
const idx = arc.tabs.findIndex(same);
|
|
5353
|
+
if (idx >= 0) return { where: "archive", desktopIdx, leafId: leaf.id, page, idx };
|
|
5354
|
+
}
|
|
5355
|
+
}
|
|
5356
|
+
}
|
|
5357
|
+
return null;
|
|
5358
|
+
}
|
|
5359
|
+
/** Reveal the instance of a single kind that is open: answers whether
|
|
5360
|
+
* there was one. Every open path asks this first. */
|
|
5361
|
+
revealInstance(kind, props = {}) {
|
|
5362
|
+
const loc = this.findInstance(kind, props);
|
|
5363
|
+
return loc ? this._revealAt(loc) : false;
|
|
5364
|
+
}
|
|
5365
|
+
/** A leaf's archived pages that can come back — every `pageTabs` entry
|
|
5366
|
+
* but the CURRENT page's. `swapToPage` restores an archive without
|
|
5367
|
+
* deleting it, so the current page's entry is a stale copy of the live
|
|
5368
|
+
* tabs, overwritten the next time the tile leaves the page; counting it
|
|
5369
|
+
* would find every live instance twice. */
|
|
5370
|
+
_archivedPages(leaf) {
|
|
5371
|
+
const kind = leaf?.content?.kind;
|
|
5372
|
+
const current = kind ? this.taxonomy.topNavFor(kind) || null : null;
|
|
5373
|
+
return Object.entries(leaf?.pageTabs || {}).filter(([page, arc]) => page !== current && Array.isArray(arc?.tabs));
|
|
5374
|
+
}
|
|
5375
|
+
/** Reveal `loc` with ONE change notification. The verbs a reveal is made
|
|
5376
|
+
* of — `showWindowTab`, `switchDesktop` (directly or via `revealWindow`)
|
|
5377
|
+
* — each notify on their own; inside a reveal they are quiet, and the
|
|
5378
|
+
* reveal notifies once, as every other verb does. */
|
|
5379
|
+
_revealAt(loc) {
|
|
5380
|
+
this._quietDepth += 1;
|
|
5381
|
+
let shown = false;
|
|
5382
|
+
try {
|
|
5383
|
+
shown = this._revealAtQuietly(loc);
|
|
5384
|
+
} finally {
|
|
5385
|
+
this._quietDepth -= 1;
|
|
5386
|
+
}
|
|
5387
|
+
if (shown) this._notifyChange("single-reveal");
|
|
5388
|
+
return shown;
|
|
5389
|
+
}
|
|
5390
|
+
_revealAtQuietly(loc) {
|
|
5391
|
+
if (loc.where === "window") {
|
|
5392
|
+
const rec = this._windowToLeaf.get(loc.windowId);
|
|
5393
|
+
if (!rec) return false;
|
|
5394
|
+
if (Array.isArray(rec.tabs) && rec.tabs.length) this.showWindowTab(loc.windowId, loc.idx);
|
|
5395
|
+
this.revealWindow(rec.window);
|
|
5396
|
+
try {
|
|
5397
|
+
rec.window.show();
|
|
5398
|
+
} catch (err) {
|
|
5399
|
+
console.warn("[wm] reveal: show failed", err);
|
|
5400
|
+
}
|
|
5401
|
+
return true;
|
|
5402
|
+
}
|
|
5403
|
+
const tree = this.desktops.desktops[loc.desktopIdx]?.tree;
|
|
5404
|
+
const leaf = tree?.get(loc.leafId);
|
|
5405
|
+
if (!leaf || leaf.kind !== "leaf") return false;
|
|
5406
|
+
if (loc.where === "archive") {
|
|
5407
|
+
const tab = leaf.pageTabs[loc.page].tabs[loc.idx];
|
|
5408
|
+
const key = this._instanceKey(tab.kind, tab.props);
|
|
5409
|
+
const curKind = leaf.content?.kind;
|
|
5410
|
+
const current = curKind ? this.taxonomy.topNavFor(curKind) || null : null;
|
|
5411
|
+
this._swapToPage(
|
|
5412
|
+
tree,
|
|
5413
|
+
loc.leafId,
|
|
5414
|
+
{ kind: tab.kind, props: tab.props || {}, title: tab.title },
|
|
5415
|
+
current,
|
|
5416
|
+
loc.page,
|
|
5417
|
+
{ recordHistory: false }
|
|
5418
|
+
);
|
|
5419
|
+
const at = (leaf.tabs || []).findIndex((t) => this._instanceKey(t.kind, t.props) === key);
|
|
5420
|
+
if (at >= 0) tree.setActiveLeafTab(loc.leafId, at);
|
|
5421
|
+
} else {
|
|
5422
|
+
tree.setActiveLeafTab(loc.leafId, loc.idx);
|
|
5423
|
+
}
|
|
5424
|
+
tree.focus(loc.leafId);
|
|
5425
|
+
if (loc.desktopIdx !== this.desktops.activeIdx) {
|
|
5426
|
+
this.switchDesktop(loc.desktopIdx);
|
|
5427
|
+
} else {
|
|
5428
|
+
this.renderer.render();
|
|
5429
|
+
this._persist();
|
|
5430
|
+
}
|
|
5431
|
+
return true;
|
|
5432
|
+
}
|
|
5433
|
+
/** `TileTree.swapToPage`, and then no restored tab is a second copy of a
|
|
5434
|
+
* single kind. Every page restore in the WM goes through here.
|
|
5435
|
+
*
|
|
5436
|
+
* A page archive (`pageTabs`) is a snapshot: the instance it lists can
|
|
5437
|
+
* have moved to another tile or a window since — and the current page's
|
|
5438
|
+
* own entry is a stale copy of the live tabs that comes back into play as
|
|
5439
|
+
* soon as the tile leaves the page by a path other than `swapToPage`
|
|
5440
|
+
* (`navigateActiveTab`). So a restored tab whose instance is live
|
|
5441
|
+
* anywhere else is dropped from the tile and from the restored archive;
|
|
5442
|
+
* the instance stays where it is. */
|
|
5443
|
+
_swapToPage(tree, leafId, target, currentTopNav, targetTopNav, opts) {
|
|
5444
|
+
const done = tree.swapToPage(leafId, target, currentTopNav, targetTopNav, opts);
|
|
5445
|
+
if (done) this._dropRestoredCopies(tree, leafId, targetTopNav, target);
|
|
5446
|
+
return done;
|
|
5447
|
+
}
|
|
5448
|
+
_dropRestoredCopies(tree, leafId, page, target) {
|
|
5449
|
+
const leaf = tree.get(leafId);
|
|
5450
|
+
const tabs = Array.isArray(leaf?.tabs) ? leaf.tabs : null;
|
|
5451
|
+
if (!tabs) return;
|
|
5452
|
+
const elsewhere = this._liveInstanceKeys(tree, leafId);
|
|
5453
|
+
const keeper = () => {
|
|
5454
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5455
|
+
return (t) => {
|
|
5456
|
+
const k = t ? this._instanceKey(t.kind, t.props) : null;
|
|
5457
|
+
if (k == null) return true;
|
|
5458
|
+
if (elsewhere.has(k) || seen.has(k)) return false;
|
|
5459
|
+
seen.add(k);
|
|
5460
|
+
return true;
|
|
5461
|
+
};
|
|
5462
|
+
};
|
|
5463
|
+
const arc = leaf.pageTabs?.[page];
|
|
5464
|
+
if (arc && Array.isArray(arc.tabs)) {
|
|
5465
|
+
const kept = arc.tabs.filter(keeper());
|
|
5466
|
+
if (kept.length !== arc.tabs.length) {
|
|
5467
|
+
arc.tabs = kept;
|
|
5468
|
+
arc.activeTabIdx = Math.max(0, Math.min(kept.length - 1, arc.activeTabIdx || 0));
|
|
5469
|
+
}
|
|
5470
|
+
}
|
|
5471
|
+
const keep = keeper();
|
|
5472
|
+
let active = leaf.activeTabIdx || 0;
|
|
5473
|
+
let changed = false;
|
|
5474
|
+
for (let i = 0; i < tabs.length; ) {
|
|
5475
|
+
if (keep(tabs[i])) {
|
|
5476
|
+
i++;
|
|
5477
|
+
continue;
|
|
5478
|
+
}
|
|
5479
|
+
tabs.splice(i, 1);
|
|
5480
|
+
if (i < active) active -= 1;
|
|
5481
|
+
changed = true;
|
|
5482
|
+
}
|
|
5483
|
+
if (!changed) return;
|
|
5484
|
+
if (!tabs.length) {
|
|
5485
|
+
tabs.push({
|
|
5486
|
+
kind: target.kind,
|
|
5487
|
+
props: target.props || {},
|
|
5488
|
+
title: target.title || _tabTitle(target.kind, target.props),
|
|
5489
|
+
history: []
|
|
5490
|
+
});
|
|
5491
|
+
active = 0;
|
|
5492
|
+
}
|
|
5493
|
+
tree.setActiveLeafTab(leafId, Math.max(0, Math.min(tabs.length - 1, active)));
|
|
5494
|
+
}
|
|
5495
|
+
/** The instance keys of every LIVE tab — windows, and every leaf of every
|
|
5496
|
+
* desktop but `leafId` of `tree`. Archived pages are not live. */
|
|
5497
|
+
_liveInstanceKeys(tree, leafId) {
|
|
5498
|
+
const keys = /* @__PURE__ */ new Set();
|
|
5499
|
+
const add = (t) => {
|
|
5500
|
+
const k = t ? this._instanceKey(t.kind, t.props) : null;
|
|
5501
|
+
if (k != null) keys.add(k);
|
|
5502
|
+
};
|
|
5503
|
+
for (const [, rec] of this._windowToLeaf) _windowTabSpecs(rec).forEach(add);
|
|
5504
|
+
for (const d of this.desktops.desktops) {
|
|
5505
|
+
for (const leaf of d.tree.leaves()) {
|
|
5506
|
+
if (d.tree === tree && leaf.id === leafId) continue;
|
|
5507
|
+
(leaf.tabs || []).forEach(add);
|
|
5508
|
+
}
|
|
5509
|
+
}
|
|
5510
|
+
return keys;
|
|
5511
|
+
}
|
|
5512
|
+
/** A restored layout keeps the FIRST instance of each single kind: later
|
|
5513
|
+
* live tabs are dropped (the tile shows the tab that took the dropped
|
|
5514
|
+
* one's place, or closes when it held nothing else), and so are later
|
|
5515
|
+
* archived ones. Answers whether anything was dropped. */
|
|
5516
|
+
_dropDuplicateSingles() {
|
|
5517
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5518
|
+
let dropped = false;
|
|
5519
|
+
const keep = (t) => {
|
|
5520
|
+
const k = t ? this._instanceKey(t.kind, t.props) : null;
|
|
5521
|
+
if (k == null) return true;
|
|
5522
|
+
if (seen.has(k)) return false;
|
|
5523
|
+
seen.add(k);
|
|
5524
|
+
return true;
|
|
5525
|
+
};
|
|
5526
|
+
for (const d of this.desktops.desktops) {
|
|
5527
|
+
const tree = d.tree;
|
|
5528
|
+
for (const leaf of tree.leaves()) {
|
|
5529
|
+
const tabs = Array.isArray(leaf.tabs) ? leaf.tabs : [];
|
|
5530
|
+
let active = leaf.activeTabIdx || 0;
|
|
5531
|
+
let changed = false;
|
|
5532
|
+
for (let i = 0; i < tabs.length; ) {
|
|
5533
|
+
if (keep(tabs[i])) {
|
|
5534
|
+
i++;
|
|
5535
|
+
continue;
|
|
5536
|
+
}
|
|
5537
|
+
tabs.splice(i, 1);
|
|
5538
|
+
if (i < active) active -= 1;
|
|
5539
|
+
changed = true;
|
|
5540
|
+
}
|
|
5541
|
+
if (!changed) continue;
|
|
5542
|
+
dropped = true;
|
|
5543
|
+
if (tabs.length) {
|
|
5544
|
+
tree.setActiveLeafTab(leaf.id, Math.max(0, Math.min(tabs.length - 1, active)));
|
|
5545
|
+
continue;
|
|
5546
|
+
}
|
|
5547
|
+
tree.close(leaf.id);
|
|
5548
|
+
if (!tree.rootId) tree.setRoot(makeLeaf(this._rootLeaf()));
|
|
5549
|
+
if (!tree.leaves().some((l) => !String(l.content?.kind || "").startsWith("panel:"))) {
|
|
5550
|
+
const spawned = this._spawnContentLeaf(tree);
|
|
5551
|
+
if (spawned) this._seedHome(tree, spawned);
|
|
5552
|
+
}
|
|
5553
|
+
}
|
|
5554
|
+
}
|
|
5555
|
+
for (const d of this.desktops.desktops) {
|
|
5556
|
+
for (const leaf of d.tree.leaves()) {
|
|
5557
|
+
for (const [page, arc] of this._archivedPages(leaf)) {
|
|
5558
|
+
const kept = arc.tabs.filter(keep);
|
|
5559
|
+
if (kept.length === arc.tabs.length) continue;
|
|
5560
|
+
dropped = true;
|
|
5561
|
+
if (!kept.length) {
|
|
5562
|
+
delete leaf.pageTabs[page];
|
|
5563
|
+
continue;
|
|
5564
|
+
}
|
|
5565
|
+
arc.tabs = kept;
|
|
5566
|
+
arc.activeTabIdx = Math.max(0, Math.min(kept.length - 1, arc.activeTabIdx || 0));
|
|
5567
|
+
}
|
|
5568
|
+
}
|
|
5569
|
+
}
|
|
5570
|
+
return dropped;
|
|
5571
|
+
}
|
|
4939
5572
|
// ── Content loading ─────────────────────────────────────────────
|
|
4940
5573
|
/** Load content into the primary tile (last-active non-panel leaf).
|
|
4941
5574
|
/** Walk back one step. Bound to Backspace.
|
|
@@ -5129,11 +5762,13 @@ var WindowManager = class _WindowManager {
|
|
|
5129
5762
|
const tree = this._tree();
|
|
5130
5763
|
const prior = tree.popActiveTabHistory?.(id);
|
|
5131
5764
|
if (!prior || !prior.kind) return false;
|
|
5765
|
+
if (this.revealInstance(prior.kind, prior.props || {})) return true;
|
|
5132
5766
|
const curKind = tree.get(id)?.content?.kind;
|
|
5133
5767
|
const curTopNav = curKind ? this.taxonomy.topNavFor(curKind) : null;
|
|
5134
5768
|
const title = prior.title || _tabTitle(prior.kind, prior.props);
|
|
5135
5769
|
if (prior.topNav && curTopNav && prior.topNav !== curTopNav) {
|
|
5136
|
-
|
|
5770
|
+
this._swapToPage(
|
|
5771
|
+
tree,
|
|
5137
5772
|
id,
|
|
5138
5773
|
{ kind: prior.kind, props: prior.props || {}, title },
|
|
5139
5774
|
curTopNav,
|
|
@@ -5243,6 +5878,7 @@ var WindowManager = class _WindowManager {
|
|
|
5243
5878
|
* the leaf. Used by Backspace + breadcrumb segments + anything
|
|
5244
5879
|
* else that should walk WITHIN the tile rather than reset it. */
|
|
5245
5880
|
navigateActiveTab(leafId, kind, props = {}) {
|
|
5881
|
+
if (this.revealInstance(kind, props)) return;
|
|
5246
5882
|
const tree = this._tree();
|
|
5247
5883
|
const leaf = tree.get(leafId);
|
|
5248
5884
|
if (!leaf || leaf.kind !== "leaf") {
|
|
@@ -5296,6 +5932,7 @@ var WindowManager = class _WindowManager {
|
|
|
5296
5932
|
* tab feature shipped), the behavior is identical to the old
|
|
5297
5933
|
* setLeafContent: one tab in, one tab out. */
|
|
5298
5934
|
openInLeaf(leafId, kind, props = {}) {
|
|
5935
|
+
if (this.revealInstance(kind, props)) return;
|
|
5299
5936
|
const tree = this._tree();
|
|
5300
5937
|
const leaf = tree.get(leafId);
|
|
5301
5938
|
if (!leaf || leaf.kind !== "leaf") {
|
|
@@ -5312,6 +5949,7 @@ var WindowManager = class _WindowManager {
|
|
|
5312
5949
|
* previous mount, mounts the new kind into the same body element,
|
|
5313
5950
|
* and updates the window's title. */
|
|
5314
5951
|
openInWindow(winId, kind, props = {}) {
|
|
5952
|
+
if (this.revealInstance(kind, props)) return;
|
|
5315
5953
|
const rec = this._windowToLeaf.get(winId);
|
|
5316
5954
|
if (!rec) {
|
|
5317
5955
|
this.openInPrimary(kind, props);
|
|
@@ -5350,6 +5988,7 @@ var WindowManager = class _WindowManager {
|
|
|
5350
5988
|
* If only panel tiles exist (or the tree is empty), spawn a new
|
|
5351
5989
|
* content leaf and canonicalize so the panels wrap it. */
|
|
5352
5990
|
openInPrimary(kind, props = {}) {
|
|
5991
|
+
if (this.revealInstance(kind, props)) return;
|
|
5353
5992
|
const tree = this._tree();
|
|
5354
5993
|
let leafId = tree.primaryLeafId();
|
|
5355
5994
|
let spawned = false;
|
|
@@ -5362,7 +6001,8 @@ var WindowManager = class _WindowManager {
|
|
|
5362
6001
|
const currentTopNav = leaf?.content?.kind ? this.taxonomy.topNavFor(leaf.content.kind) || null : null;
|
|
5363
6002
|
const targetTopNav = this.taxonomy.topNavFor(kind) || kind;
|
|
5364
6003
|
const title = _tabTitle(kind, props);
|
|
5365
|
-
|
|
6004
|
+
this._swapToPage(
|
|
6005
|
+
tree,
|
|
5366
6006
|
leafId,
|
|
5367
6007
|
{ kind, props, title },
|
|
5368
6008
|
currentTopNav,
|
|
@@ -5408,6 +6048,7 @@ var WindowManager = class _WindowManager {
|
|
|
5408
6048
|
* pane in a split never dismounts the original). Returns the new
|
|
5409
6049
|
* leaf id, or null if the leaf can't be split. */
|
|
5410
6050
|
splitLeafWith(leafId, dir, kind, props = {}, title = "") {
|
|
6051
|
+
if (this.revealInstance(kind, props)) return null;
|
|
5411
6052
|
const tree = this._tree();
|
|
5412
6053
|
leafId = this._contentLeafId(leafId);
|
|
5413
6054
|
const src = leafId ? tree.get(leafId) : null;
|
|
@@ -5443,17 +6084,18 @@ var WindowManager = class _WindowManager {
|
|
|
5443
6084
|
if (!kind || kind === PLACEHOLDER_KIND || kind.startsWith("panel:")) return null;
|
|
5444
6085
|
return { leafId: id, kind, props: leaf.content.props || {}, title: leaf.title };
|
|
5445
6086
|
}
|
|
5446
|
-
/** Alt+T — open the focused tile's content in a new tab on that tile.
|
|
6087
|
+
/** Alt+T — open the focused tile's content in a new tab on that tile.
|
|
6088
|
+
* A copy verb, so a single kind refuses it (and Alt+N, Alt+Shift+H/V). */
|
|
5447
6089
|
openFocusedInTab() {
|
|
5448
6090
|
const c = this._focusedContent();
|
|
5449
|
-
if (!c) return;
|
|
6091
|
+
if (!c || this.isSingleKind(c.kind)) return;
|
|
5450
6092
|
this.openInTabFromContext({ leafId: c.leafId }, c.kind, c.props);
|
|
5451
6093
|
}
|
|
5452
6094
|
/** Alt+N — open the focused tile's content in a fresh managed window.
|
|
5453
6095
|
* Unlike Alt+F (promote) this DUPLICATES: the source tile stays put. */
|
|
5454
6096
|
openFocusedInWindow() {
|
|
5455
6097
|
const c = this._focusedContent();
|
|
5456
|
-
if (!c) return;
|
|
6098
|
+
if (!c || this.isSingleKind(c.kind)) return;
|
|
5457
6099
|
this._navigateWindow(c.kind, c.props);
|
|
5458
6100
|
}
|
|
5459
6101
|
/** Alt+Shift+H / Alt+Shift+V — split the focused tile and open a fresh
|
|
@@ -5465,6 +6107,7 @@ var WindowManager = class _WindowManager {
|
|
|
5465
6107
|
this.split(dir);
|
|
5466
6108
|
return;
|
|
5467
6109
|
}
|
|
6110
|
+
if (this.isSingleKind(c.kind)) return;
|
|
5468
6111
|
this.splitLeafWith(c.leafId, dir, c.kind, c.props, c.title);
|
|
5469
6112
|
}
|
|
5470
6113
|
closeFocused() {
|
|
@@ -7225,15 +7868,94 @@ var WindowManager = class _WindowManager {
|
|
|
7225
7868
|
* edge (a recursive split) or closes that leaf. */
|
|
7226
7869
|
isPanelOpen(side) {
|
|
7227
7870
|
const d = this.desktops.active();
|
|
7228
|
-
return !!d.panels[side];
|
|
7871
|
+
return !!d.panels[side] && this.isPanelAvailable(side);
|
|
7229
7872
|
}
|
|
7873
|
+
/** Flip a panel. A withheld panel is not flipped: answers `false` and
|
|
7874
|
+
* touches nothing — not the remembered open state, not the layout, not
|
|
7875
|
+
* what is persisted. Answers `true` when it flipped. */
|
|
7230
7876
|
togglePanel(side) {
|
|
7877
|
+
if (!this.isPanelAvailable(side)) return false;
|
|
7231
7878
|
const d = this.desktops.active();
|
|
7232
7879
|
d.panels[side] = !d.panels[side];
|
|
7233
7880
|
this._canonicalize(this._tree(), d);
|
|
7234
7881
|
this.renderer.render();
|
|
7235
7882
|
this._persist();
|
|
7236
7883
|
this._notifyChange();
|
|
7884
|
+
return true;
|
|
7885
|
+
}
|
|
7886
|
+
// ── Panel availability ──────────────────────────────────────────
|
|
7887
|
+
/** Whether the panel on `side` is offered on the active desktop right now:
|
|
7888
|
+
* `{ available, reason }`. A runtime override wins; else the primary
|
|
7889
|
+
* tile's kind says (`KindDef.panels`, see kind_taxonomy.js); else it is
|
|
7890
|
+
* offered. `desktops.panels` is untouched by any of this — it remembers
|
|
7891
|
+
* what the user OPENED, so a panel withheld while it was open comes back
|
|
7892
|
+
* open once it is offered again. */
|
|
7893
|
+
getPanelAvailability(side, tree = this._tree()) {
|
|
7894
|
+
assertPanelSide(side, "getPanelAvailability");
|
|
7895
|
+
const override = this._panelOverrides[side];
|
|
7896
|
+
if (override) return override;
|
|
7897
|
+
const id = tree.primaryLeafId();
|
|
7898
|
+
const content = id ? tree.get(id)?.content : null;
|
|
7899
|
+
const kind = content?.kind === PLACEHOLDER_KIND ? content.props?.originalKind : content?.kind;
|
|
7900
|
+
return this.taxonomy.panelOffer(kind, side);
|
|
7901
|
+
}
|
|
7902
|
+
isPanelAvailable(side) {
|
|
7903
|
+
return this.getPanelAvailability(side).available;
|
|
7904
|
+
}
|
|
7905
|
+
/** Content's say, at runtime: `offer` is `true`, `false` or `{ reason }`
|
|
7906
|
+
* and beats the kind's declaration until it is cleared with `null`. The
|
|
7907
|
+
* override belongs to the shell, not to a tile — content that sets one
|
|
7908
|
+
* clears it when it goes (its `destroy`). */
|
|
7909
|
+
setPanelAvailability(side, offer) {
|
|
7910
|
+
assertPanelSide(side, "setPanelAvailability");
|
|
7911
|
+
if (offer === null || offer === void 0) delete this._panelOverrides[side];
|
|
7912
|
+
else this._panelOverrides[side] = readPanelOffer(offer, "setPanelAvailability");
|
|
7913
|
+
this._notifyChange("panel-availability");
|
|
7914
|
+
}
|
|
7915
|
+
/** Subscribe to availability changes. `fn({ panels, changed })` runs ONCE
|
|
7916
|
+
* per change, however many sides changed in it: `panels` is every side's
|
|
7917
|
+
* `{ available, reason }`, `changed` the sides that differ from the last
|
|
7918
|
+
* call. The same payload goes out on the bus as `wm:panels:availability`.
|
|
7919
|
+
* Returns the unsubscribe. */
|
|
7920
|
+
onPanelAvailabilityChange(fn) {
|
|
7921
|
+
this._panelAvailabilityListeners.add(fn);
|
|
7922
|
+
return () => this._panelAvailabilityListeners.delete(fn);
|
|
7923
|
+
}
|
|
7924
|
+
/** Bring the active desktop's panel leaves in line with what is offered,
|
|
7925
|
+
* and announce a change of availability. Called from `_notifyChange`, so
|
|
7926
|
+
* every path that can change the primary kind passes through it. */
|
|
7927
|
+
_reconcilePanels() {
|
|
7928
|
+
if (this._reconcilingPanels || !this.desktops) return;
|
|
7929
|
+
this._reconcilingPanels = true;
|
|
7930
|
+
try {
|
|
7931
|
+
const d = this.desktops.active();
|
|
7932
|
+
const panels = {};
|
|
7933
|
+
for (const side of PANEL_SIDES) panels[side] = this.getPanelAvailability(side, d.tree);
|
|
7934
|
+
const shown = (side) => !!d.panels[side] && panels[side].available;
|
|
7935
|
+
if (d.tree.rootId && PANEL_SIDES.some((s) => shown(s) !== !!this._findPanelLeaf(s))) {
|
|
7936
|
+
this._canonicalize(d.tree, d);
|
|
7937
|
+
this.renderer.render();
|
|
7938
|
+
}
|
|
7939
|
+
const last = this._panelAvailability;
|
|
7940
|
+
const changed = PANEL_SIDES.filter((s) => last[s].available !== panels[s].available || last[s].reason !== panels[s].reason);
|
|
7941
|
+
if (!changed.length) return;
|
|
7942
|
+
this._panelAvailability = panels;
|
|
7943
|
+
const payload = Object.freeze({ panels: Object.freeze(panels), changed, wm: this });
|
|
7944
|
+
for (const fn of [...this._panelAvailabilityListeners]) {
|
|
7945
|
+
try {
|
|
7946
|
+
fn(payload);
|
|
7947
|
+
} catch (err) {
|
|
7948
|
+
console.error("[wm] panel availability listener threw", err);
|
|
7949
|
+
}
|
|
7950
|
+
}
|
|
7951
|
+
try {
|
|
7952
|
+
this.eventBus?.emit?.("wm:panels:availability", payload);
|
|
7953
|
+
} catch (err) {
|
|
7954
|
+
console.warn("[wm] event emit failed", err);
|
|
7955
|
+
}
|
|
7956
|
+
} finally {
|
|
7957
|
+
this._reconcilingPanels = false;
|
|
7958
|
+
}
|
|
7237
7959
|
}
|
|
7238
7960
|
/** Rebuild the tree so panel:* leaves sit in the canonical positions:
|
|
7239
7961
|
*
|
|
@@ -7247,13 +7969,13 @@ var WindowManager = class _WindowManager {
|
|
|
7247
7969
|
* Existing panel leaves are detached + re-attached (same id), so the
|
|
7248
7970
|
* renderer cache keeps the mounted content (nav filter state, etc.). */
|
|
7249
7971
|
_canonicalize(tree, desktop) {
|
|
7250
|
-
const
|
|
7251
|
-
for (const
|
|
7252
|
-
|
|
7253
|
-
if (kind === "panel:left") panel.left = leaf;
|
|
7254
|
-
else if (kind === "panel:right") panel.right = leaf;
|
|
7255
|
-
else if (kind === "panel:bottom") panel.bottom = leaf;
|
|
7972
|
+
const open = {};
|
|
7973
|
+
for (const side of PANEL_SIDES) {
|
|
7974
|
+
open[side] = !!desktop.panels[side] && this.getPanelAvailability(side, tree).available;
|
|
7256
7975
|
}
|
|
7976
|
+
const panel = _panelLeaves(tree);
|
|
7977
|
+
this._recordPanelShares(tree, desktop);
|
|
7978
|
+
const share = desktop.panelShares;
|
|
7257
7979
|
const detach = (leaf) => {
|
|
7258
7980
|
if (!leaf) return;
|
|
7259
7981
|
const pid = leaf.parentId;
|
|
@@ -7277,7 +7999,7 @@ var WindowManager = class _WindowManager {
|
|
|
7277
7999
|
detach(panel.bottom);
|
|
7278
8000
|
let seededSide = null;
|
|
7279
8001
|
if (!tree.rootId) {
|
|
7280
|
-
seededSide =
|
|
8002
|
+
seededSide = open.left ? "left" : open.bottom ? "bottom" : open.right ? "right" : null;
|
|
7281
8003
|
if (seededSide) {
|
|
7282
8004
|
const seed = panel[seededSide] || makeLeaf({
|
|
7283
8005
|
content: { kind: `panel:${seededSide}`, props: {} },
|
|
@@ -7292,6 +8014,15 @@ var WindowManager = class _WindowManager {
|
|
|
7292
8014
|
tree.rootId = seed.id;
|
|
7293
8015
|
}
|
|
7294
8016
|
}
|
|
8017
|
+
const willWrap = (side) => open[side] && seededSide !== side && !!tree.rootId;
|
|
8018
|
+
const DEFAULT_SHARE = 1 / 5;
|
|
8019
|
+
const slotShare = (side) => {
|
|
8020
|
+
const own = share[side];
|
|
8021
|
+
if (own == null) return null;
|
|
8022
|
+
if (side !== "left" || !willWrap("right")) return _clampSlotShare(own);
|
|
8023
|
+
const right = _clampSlotShare(share.right ?? DEFAULT_SHARE);
|
|
8024
|
+
return _clampSlotShare(own / (1 - right));
|
|
8025
|
+
};
|
|
7295
8026
|
const wrap = (panelLeaf, side) => {
|
|
7296
8027
|
if (panelLeaf && !tree.nodes.has(panelLeaf.id)) {
|
|
7297
8028
|
tree.nodes.set(panelLeaf.id, panelLeaf);
|
|
@@ -7307,7 +8038,8 @@ var WindowManager = class _WindowManager {
|
|
|
7307
8038
|
const before = side === "left";
|
|
7308
8039
|
const dir = side === "bottom" ? "v" : "h";
|
|
7309
8040
|
const children = before ? [leaf.id, tree.rootId] : [tree.rootId, leaf.id];
|
|
7310
|
-
const
|
|
8041
|
+
const kept = slotShare(side);
|
|
8042
|
+
const sizes = kept != null ? before ? [kept, 1 - kept] : [1 - kept, kept] : before ? [1, 4] : [4, 1];
|
|
7311
8043
|
const split = {
|
|
7312
8044
|
id: `split-panel-${side}-${Math.random().toString(36).slice(2, 7)}`,
|
|
7313
8045
|
kind: "split",
|
|
@@ -7322,9 +8054,9 @@ var WindowManager = class _WindowManager {
|
|
|
7322
8054
|
leaf.parentId = split.id;
|
|
7323
8055
|
tree.rootId = split.id;
|
|
7324
8056
|
};
|
|
7325
|
-
if (
|
|
7326
|
-
if (
|
|
7327
|
-
if (
|
|
8057
|
+
if (willWrap("left")) wrap(panel.left, "left");
|
|
8058
|
+
if (willWrap("right")) wrap(panel.right, "right");
|
|
8059
|
+
if (willWrap("bottom")) wrap(panel.bottom, "bottom");
|
|
7328
8060
|
const reachable = /* @__PURE__ */ new Set();
|
|
7329
8061
|
const walk = (id) => {
|
|
7330
8062
|
if (!id || reachable.has(id)) return;
|
|
@@ -7337,6 +8069,68 @@ var WindowManager = class _WindowManager {
|
|
|
7337
8069
|
if (!reachable.has(id)) tree.nodes.delete(id);
|
|
7338
8070
|
}
|
|
7339
8071
|
}
|
|
8072
|
+
/** A finished splitter drag of split `splitId` is a layout change like
|
|
8073
|
+
* any other: record the shares of the panels that split sizes (the ones
|
|
8074
|
+
* inside it; the Navigator sits inside the right panel's split) while
|
|
8075
|
+
* the tree is still canonical (a drag never changes its structure), then
|
|
8076
|
+
* save, so the dragged size is what the next load, or the next
|
|
8077
|
+
* structural change, gives back. */
|
|
8078
|
+
_onSplitResized(splitId) {
|
|
8079
|
+
this._recordPanelShares(this._tree(), this.desktops.active(), splitId);
|
|
8080
|
+
this._persist();
|
|
8081
|
+
this._notifyChange("split-resized");
|
|
8082
|
+
}
|
|
8083
|
+
/** Keep `desktop.panelShares` current: each panel's share of the WM area
|
|
8084
|
+
* along its own axis (width for left/right, height for bottom), kept on
|
|
8085
|
+
* the desktop record and serialized with it, so a panel the user closes
|
|
8086
|
+
* comes back at the size it had, even across a reload while it was
|
|
8087
|
+
* closed. With `resizedSplitId`, the panels inside that split are
|
|
8088
|
+
* measured (a drag); without, the panels that have no share yet are.
|
|
8089
|
+
* A panel that measures nothing (not on the tree, the root, or under a
|
|
8090
|
+
* split that is not one of its canonical panel splits) keeps the share
|
|
8091
|
+
* it last had. A share is recorded as measured, never clamped: the drag
|
|
8092
|
+
* range belongs to the slot of the split being built (step 5 of
|
|
8093
|
+
* _canonicalize), not to an area share. A stored value that is not a
|
|
8094
|
+
* share at all (not a finite number strictly between 0 and 1) is
|
|
8095
|
+
* dropped, so that panel opens at the default again. */
|
|
8096
|
+
_recordPanelShares(tree, desktop, resizedSplitId = null) {
|
|
8097
|
+
const panel = _panelLeaves(tree);
|
|
8098
|
+
for (const side of PANEL_SIDES) {
|
|
8099
|
+
const kept = _validShare(desktop.panelShares[side]);
|
|
8100
|
+
const measure = resizedSplitId == null ? kept == null : _isInside(tree, panel[side], resizedSplitId);
|
|
8101
|
+
const v = (measure ? this._panelShare(tree, panel[side], side) : null) ?? kept;
|
|
8102
|
+
if (v == null) delete desktop.panelShares[side];
|
|
8103
|
+
else desktop.panelShares[side] = v;
|
|
8104
|
+
}
|
|
8105
|
+
}
|
|
8106
|
+
/** The share (0..1) of the WM area the `side` panel leaf takes along its
|
|
8107
|
+
* axis: the product of its shares in every ancestor split running that
|
|
8108
|
+
* way (splits across the axis do not change it). Only a CANONICAL
|
|
8109
|
+
* placement measures: the leaf's own parent is its `split-panel-<side>`
|
|
8110
|
+
* split and every ancestor is a panel split. Any other tree is one a
|
|
8111
|
+
* structural change has left in passing (the last content tile closed,
|
|
8112
|
+
* or a spawned content leaf wrapping the whole root), whose sizes are
|
|
8113
|
+
* not the user's; that, a missing or root leaf, or a degenerate split
|
|
8114
|
+
* gives null. */
|
|
8115
|
+
_panelShare(tree, leaf, side) {
|
|
8116
|
+
if (!leaf?.parentId) return null;
|
|
8117
|
+
if (!String(leaf.parentId).startsWith(`split-panel-${side}-`)) return null;
|
|
8118
|
+
const axis = side === "bottom" ? "v" : "h";
|
|
8119
|
+
let frac = 1;
|
|
8120
|
+
let node = leaf;
|
|
8121
|
+
while (node.parentId) {
|
|
8122
|
+
const parent = tree.get(node.parentId);
|
|
8123
|
+
if (!parent || !String(parent.id).startsWith("split-panel-")) return null;
|
|
8124
|
+
if (parent.dir === axis) {
|
|
8125
|
+
const idx = parent.children.indexOf(node.id);
|
|
8126
|
+
const total = parent.sizes.reduce((a, b) => a + b, 0);
|
|
8127
|
+
if (idx < 0 || !(total > 0) || !(parent.sizes[idx] > 0)) return null;
|
|
8128
|
+
frac *= parent.sizes[idx] / total;
|
|
8129
|
+
}
|
|
8130
|
+
node = parent;
|
|
8131
|
+
}
|
|
8132
|
+
return frac > 0 && frac < 1 ? frac : null;
|
|
8133
|
+
}
|
|
7340
8134
|
_findPanelLeaf(side) {
|
|
7341
8135
|
const kind = `panel:${side}`;
|
|
7342
8136
|
for (const leaf of this._tree().leaves()) {
|
|
@@ -7682,6 +8476,7 @@ var WindowManager = class _WindowManager {
|
|
|
7682
8476
|
/** Spawn a fresh ManagedWindow with the requested content. No
|
|
7683
8477
|
* source leaf — closing the window just disposes the content. */
|
|
7684
8478
|
_navigateWindow(kind, props) {
|
|
8479
|
+
if (this.revealInstance(kind, props)) return;
|
|
7685
8480
|
const winId = `twm-mw-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 5)}`;
|
|
7686
8481
|
const contentEl = document.createElement("div");
|
|
7687
8482
|
contentEl.className = "twm-window-content";
|
|
@@ -7799,6 +8594,7 @@ var WindowManager = class _WindowManager {
|
|
|
7799
8594
|
* routing) but uses `appendLeafTab` so the existing content
|
|
7800
8595
|
* stays in place as a tab. */
|
|
7801
8596
|
openInTabFromContext(ctx, kind, props = {}, transient = false, background = false) {
|
|
8597
|
+
if (this.revealInstance(kind, props)) return;
|
|
7802
8598
|
if (ctx?.windowId && this._windowToLeaf.has(ctx.windowId)) {
|
|
7803
8599
|
this.openInWindow(ctx.windowId, kind, props);
|
|
7804
8600
|
return;
|
|
@@ -7831,6 +8627,7 @@ var WindowManager = class _WindowManager {
|
|
|
7831
8627
|
* "Add row" button, which passes no ctx) reliably lands as a sibling
|
|
7832
8628
|
* tab in the main tile rather than swapping its content. */
|
|
7833
8629
|
openInTabInPrimary(kind, props = {}, transient = false, background = false) {
|
|
8630
|
+
if (this.revealInstance(kind, props)) return;
|
|
7834
8631
|
const tree = this._tree();
|
|
7835
8632
|
const leafId = tree.primaryLeafId();
|
|
7836
8633
|
if (!leafId) {
|
|
@@ -7849,11 +8646,38 @@ var WindowManager = class _WindowManager {
|
|
|
7849
8646
|
this._notifyChange("tab-open");
|
|
7850
8647
|
}
|
|
7851
8648
|
};
|
|
8649
|
+
function _panelLeaves(tree) {
|
|
8650
|
+
const panel = {};
|
|
8651
|
+
for (const leaf of tree.leaves()) {
|
|
8652
|
+
const kind = leaf.content?.kind;
|
|
8653
|
+
if (kind === "panel:left") panel.left = leaf;
|
|
8654
|
+
else if (kind === "panel:right") panel.right = leaf;
|
|
8655
|
+
else if (kind === "panel:bottom") panel.bottom = leaf;
|
|
8656
|
+
}
|
|
8657
|
+
return panel;
|
|
8658
|
+
}
|
|
8659
|
+
function _isInside(tree, node, splitId) {
|
|
8660
|
+
for (let id = node?.parentId; id; id = tree.get(id)?.parentId) {
|
|
8661
|
+
if (id === splitId) return true;
|
|
8662
|
+
}
|
|
8663
|
+
return false;
|
|
8664
|
+
}
|
|
8665
|
+
function _validShare(v) {
|
|
8666
|
+
return typeof v === "number" && Number.isFinite(v) && v > 0 && v < 1 ? v : null;
|
|
8667
|
+
}
|
|
8668
|
+
function _clampSlotShare(v) {
|
|
8669
|
+
const min = TileRenderer.MIN_SLOT_FRACTION;
|
|
8670
|
+
return Math.min(Math.max(v, min), 1 - min);
|
|
8671
|
+
}
|
|
7852
8672
|
function _tabTitle(kind, props) {
|
|
7853
8673
|
if (props && typeof props.label === "string" && props.label) return props.label;
|
|
7854
8674
|
if (props && (typeof props.id === "string" || typeof props.id === "number") && String(props.id)) return String(props.id);
|
|
7855
8675
|
return kind || "";
|
|
7856
8676
|
}
|
|
8677
|
+
function _windowTabSpecs(rec) {
|
|
8678
|
+
if (Array.isArray(rec?.tabs) && rec.tabs.length) return rec.tabs;
|
|
8679
|
+
return rec?.original ? [rec.original] : [];
|
|
8680
|
+
}
|
|
7857
8681
|
function _leafTabSpecs(leaf) {
|
|
7858
8682
|
const tabs = Array.isArray(leaf.tabs) ? leaf.tabs : [];
|
|
7859
8683
|
if (tabs.length) {
|
|
@@ -8114,17 +8938,17 @@ function openTileTabMenu({
|
|
|
8114
8938
|
return `
|
|
8115
8939
|
<li class="twm-tile-tabmenu__item${idx === _cursor ? " twm-tile-tabmenu__item--cursor" : ""}"
|
|
8116
8940
|
data-flat-idx="${idx}">
|
|
8117
|
-
<span class="twm-tile-tabmenu__item-label">${
|
|
8118
|
-
${hint ? `<span class="twm-tile-tabmenu__item-hint">${
|
|
8941
|
+
<span class="twm-tile-tabmenu__item-label">${_esc8(label)}</span>
|
|
8942
|
+
${hint ? `<span class="twm-tile-tabmenu__item-hint">${_esc8(hint)}</span>` : ""}
|
|
8119
8943
|
</li>
|
|
8120
8944
|
`;
|
|
8121
8945
|
}).join("");
|
|
8122
8946
|
return `
|
|
8123
8947
|
<section class="twm-tile-tabmenu__section"
|
|
8124
|
-
data-section="${
|
|
8948
|
+
data-section="${_esc8(s.src.navKind)}">
|
|
8125
8949
|
<header class="twm-tile-tabmenu__section-head">
|
|
8126
|
-
<span class="material-symbols-outlined">${
|
|
8127
|
-
<span class="twm-tile-tabmenu__section-label">${
|
|
8950
|
+
<span class="material-symbols-outlined">${_esc8(s.meta?.icon || "arrow_right")}</span>
|
|
8951
|
+
<span class="twm-tile-tabmenu__section-label">${_esc8(s.meta?.label || s.src.navKind)}</span>
|
|
8128
8952
|
<span class="twm-tile-tabmenu__section-count">${s.total}</span>
|
|
8129
8953
|
</header>
|
|
8130
8954
|
${s.view.length === 0 ? `
|
|
@@ -8133,11 +8957,11 @@ function openTileTabMenu({
|
|
|
8133
8957
|
${!_query && s.pages > 1 ? `
|
|
8134
8958
|
<div class="twm-tile-tabmenu__pager">
|
|
8135
8959
|
<button type="button" data-action="prev"
|
|
8136
|
-
data-section="${
|
|
8960
|
+
data-section="${_esc8(s.src.navKind)}"
|
|
8137
8961
|
${s.page === 0 ? "disabled" : ""}><</button>
|
|
8138
8962
|
<span>${s.page + 1} / ${s.pages}</span>
|
|
8139
8963
|
<button type="button" data-action="next"
|
|
8140
|
-
data-section="${
|
|
8964
|
+
data-section="${_esc8(s.src.navKind)}"
|
|
8141
8965
|
${s.page >= s.pages - 1 ? "disabled" : ""}>></button>
|
|
8142
8966
|
</div>
|
|
8143
8967
|
` : ""}
|
|
@@ -8201,7 +9025,7 @@ function openTileTabSwitcher({ x, y, tabs, activeIdx = 0, onPick }) {
|
|
|
8201
9025
|
<li class="twm-tile-tabswitch__item${i === activeIdx ? " twm-tile-tabswitch__item--on" : ""}"
|
|
8202
9026
|
data-idx="${i}" role="option" aria-selected="${i === activeIdx}">
|
|
8203
9027
|
<span class="material-symbols-outlined twm-tile-tabswitch__check">${i === activeIdx ? "check" : ""}</span>
|
|
8204
|
-
<span class="twm-tile-tabswitch__label">${
|
|
9028
|
+
<span class="twm-tile-tabswitch__label">${_esc8(t.title || t.kind || "Tab")}</span>
|
|
8205
9029
|
</li>
|
|
8206
9030
|
`).join("");
|
|
8207
9031
|
overlay.innerHTML = `
|
|
@@ -8302,7 +9126,7 @@ function _matchesShaped(shaped, query) {
|
|
|
8302
9126
|
const hint = String(shaped.hint ?? "").toLowerCase();
|
|
8303
9127
|
return id.includes(query) || label.includes(query) || hint.includes(query);
|
|
8304
9128
|
}
|
|
8305
|
-
function
|
|
9129
|
+
function _esc8(s) {
|
|
8306
9130
|
return String(s ?? "").replace(/[&<>"']/g, (c) => ({
|
|
8307
9131
|
"&": "&",
|
|
8308
9132
|
"<": "<",
|
|
@@ -8440,17 +9264,34 @@ async function createShell({
|
|
|
8440
9264
|
tabLayout = null,
|
|
8441
9265
|
newTabButton = true,
|
|
8442
9266
|
desktopBar = {},
|
|
9267
|
+
menuBar: menuBarCfg = null,
|
|
8443
9268
|
chrome = {},
|
|
8444
9269
|
// An embedder that moved its sections out of the top bar — into an icon
|
|
8445
9270
|
// rail, say — passes the selector its own buttons match, and F1..F8 keep
|
|
8446
9271
|
// working. Omitted, the default top-bar selector applies.
|
|
8447
|
-
navSelector = null
|
|
9272
|
+
navSelector = null,
|
|
9273
|
+
keymap = null
|
|
8448
9274
|
} = {}) {
|
|
8449
9275
|
if (!root || typeof root.appendChild !== "function") {
|
|
8450
9276
|
throw new TypeError("createShell: `root` must be an element");
|
|
8451
9277
|
}
|
|
8452
9278
|
if (!taxonomy) throw new Error("createShell: a taxonomy is required");
|
|
8453
9279
|
if (!entities) throw new Error("createShell: an entity catalog is required");
|
|
9280
|
+
if (chrome.menuBar && !menuBarCfg?.menus) {
|
|
9281
|
+
throw new TypeError("createShell: `chrome.menuBar` needs `menuBar: { menus }`");
|
|
9282
|
+
}
|
|
9283
|
+
if (menuBarCfg && !chrome.menuBar) {
|
|
9284
|
+
throw new TypeError("createShell: `menuBar` needs `chrome.menuBar`, the element it is painted into");
|
|
9285
|
+
}
|
|
9286
|
+
if (keymap != null) {
|
|
9287
|
+
if (typeof keymap !== "object" || Array.isArray(keymap)) {
|
|
9288
|
+
throw new TypeError("createShell: `keymap` must be an object of { bindings }");
|
|
9289
|
+
}
|
|
9290
|
+
const extra = Object.keys(keymap).filter((k) => k !== "bindings");
|
|
9291
|
+
if (extra.length) {
|
|
9292
|
+
throw new TypeError(`createShell: unknown \`keymap\` key(s) ${extra.map((k) => `"${k}"`).join(", ")} \u2014 the only key is \`bindings\``);
|
|
9293
|
+
}
|
|
9294
|
+
}
|
|
8454
9295
|
const log = logger ?? { info() {
|
|
8455
9296
|
}, warn() {
|
|
8456
9297
|
}, error() {
|
|
@@ -8501,21 +9342,28 @@ async function createShell({
|
|
|
8501
9342
|
},
|
|
8502
9343
|
onChange: () => syncChrome()
|
|
8503
9344
|
});
|
|
9345
|
+
const menuBar = chrome.menuBar ? createMenuBar(chrome.menuBar, menuBarCfg) : null;
|
|
9346
|
+
const ownCommands = paletteCfg.commands;
|
|
9347
|
+
const commands = menuBar && menuBarCfg.palette !== false ? () => [...typeof ownCommands === "function" ? ownCommands() || [] : [], ...menuBar.commands()] : ownCommands;
|
|
9348
|
+
const bindings = keymap?.bindings ?? null;
|
|
8504
9349
|
const palette = createCommandPalette({
|
|
8505
9350
|
wm,
|
|
8506
9351
|
api,
|
|
8507
9352
|
taxonomy,
|
|
8508
9353
|
catalog: entities,
|
|
8509
|
-
|
|
9354
|
+
// What *Keyboard shortcuts* lists is what the keys do in THIS shell.
|
|
9355
|
+
shortcuts: keymapShortcuts(bindings),
|
|
9356
|
+
...paletteCfg,
|
|
9357
|
+
commands
|
|
8510
9358
|
});
|
|
8511
|
-
const disposeKeymap = installKeymap({ wm, palette, navSelector });
|
|
9359
|
+
const disposeKeymap = installKeymap({ wm, palette, navSelector, bindings, logger });
|
|
8512
9360
|
const paletteBtn = mountPaletteButton(chrome.paletteButton, palette);
|
|
8513
9361
|
topNavEl = mountTopNav(chrome.topNav, taxonomy, wm);
|
|
8514
9362
|
desktopsEl = mountDesktopBar(chrome.desktops, wm, desktopBar || {});
|
|
8515
9363
|
const zoom = mountZoomControl(chrome.zoom, { root, host });
|
|
8516
9364
|
if (zoom) await zoom.ready;
|
|
8517
9365
|
toggles = bindPanelToggles(chrome.panelToggles, wm);
|
|
8518
|
-
eventBus?.on?.("wm:changed", syncChrome);
|
|
9366
|
+
const chromeSubscription = eventBus?.on?.("wm:changed", syncChrome);
|
|
8519
9367
|
await wm.load();
|
|
8520
9368
|
syncChrome();
|
|
8521
9369
|
log.info?.("shell mounted");
|
|
@@ -8533,7 +9381,9 @@ async function createShell({
|
|
|
8533
9381
|
panelToggles: toggles,
|
|
8534
9382
|
// `get`/`set` so an embedder can drive the zoom from its own settings
|
|
8535
9383
|
// pane, or read it, without reaching into the control's DOM.
|
|
8536
|
-
zoom
|
|
9384
|
+
zoom,
|
|
9385
|
+
// STORY-0030. `setEnabled` / `setChecked` / `open` / `commands`.
|
|
9386
|
+
menuBar
|
|
8537
9387
|
}),
|
|
8538
9388
|
// A shell that can be built can be built TWICE — an embedder that
|
|
8539
9389
|
// rebuilds on a context change (a different project, a different
|
|
@@ -8542,7 +9392,7 @@ async function createShell({
|
|
|
8542
9392
|
// with the first one's keyboard.
|
|
8543
9393
|
dispose: () => {
|
|
8544
9394
|
try {
|
|
8545
|
-
|
|
9395
|
+
chromeSubscription?.dispose?.();
|
|
8546
9396
|
} catch {
|
|
8547
9397
|
}
|
|
8548
9398
|
try {
|
|
@@ -8566,6 +9416,10 @@ async function createShell({
|
|
|
8566
9416
|
desktopsEl?.remove();
|
|
8567
9417
|
} catch {
|
|
8568
9418
|
}
|
|
9419
|
+
try {
|
|
9420
|
+
menuBar?.dispose();
|
|
9421
|
+
} catch {
|
|
9422
|
+
}
|
|
8569
9423
|
}
|
|
8570
9424
|
});
|
|
8571
9425
|
}
|
|
@@ -8617,6 +9471,7 @@ function mountPaletteButton(hostEl, palette) {
|
|
|
8617
9471
|
hostEl.insertBefore(btn, hostEl.firstChild);
|
|
8618
9472
|
return btn;
|
|
8619
9473
|
}
|
|
9474
|
+
var OWN_TOOLTIPS = /* @__PURE__ */ new WeakMap();
|
|
8620
9475
|
function bindPanelToggles(map, wm) {
|
|
8621
9476
|
const fresh = {};
|
|
8622
9477
|
for (const side of ["left", "right", "bottom"]) {
|
|
@@ -8629,6 +9484,10 @@ function bindPanelToggles(map, wm) {
|
|
|
8629
9484
|
el.removeAttribute("aria-pressed");
|
|
8630
9485
|
btn.replaceWith(el);
|
|
8631
9486
|
el.addEventListener("click", () => wm.togglePanel(side));
|
|
9487
|
+
OWN_TOOLTIPS.set(el, {
|
|
9488
|
+
tooltip: el.getAttribute("data-tooltip"),
|
|
9489
|
+
title: el.getAttribute("title")
|
|
9490
|
+
});
|
|
8632
9491
|
fresh[side] = el;
|
|
8633
9492
|
}
|
|
8634
9493
|
return fresh;
|
|
@@ -8638,6 +9497,21 @@ function syncPanelToggles(map, wm) {
|
|
|
8638
9497
|
const btn = map?.[side];
|
|
8639
9498
|
if (!btn) continue;
|
|
8640
9499
|
btn.classList.toggle("twm-panel-toggle-btn--on", wm.isPanelOpen(side));
|
|
9500
|
+
const { available, reason } = wm.getPanelAvailability(side);
|
|
9501
|
+
const own = OWN_TOOLTIPS.get(btn) || {};
|
|
9502
|
+
const restore = (attr, value) => value == null ? btn.removeAttribute(attr) : btn.setAttribute(attr, value);
|
|
9503
|
+
if (available) {
|
|
9504
|
+
btn.removeAttribute("aria-disabled");
|
|
9505
|
+
restore("data-tooltip", own.tooltip);
|
|
9506
|
+
restore("title", own.title);
|
|
9507
|
+
continue;
|
|
9508
|
+
}
|
|
9509
|
+
btn.setAttribute("aria-disabled", "true");
|
|
9510
|
+
if (btn.classList.contains("twm-has-tooltip")) {
|
|
9511
|
+
restore("data-tooltip", reason ?? own.tooltip);
|
|
9512
|
+
} else {
|
|
9513
|
+
restore("title", reason ?? own.title);
|
|
9514
|
+
}
|
|
8641
9515
|
}
|
|
8642
9516
|
}
|
|
8643
9517
|
function mountDesktopBar(hostEl, wm, { switchOnAdd = false } = {}) {
|
|
@@ -8793,6 +9667,8 @@ function _tileContextMenu(wm, leafId, x, y) {
|
|
|
8793
9667
|
const isPanel = String(leaf.content?.kind || "").startsWith("panel:");
|
|
8794
9668
|
tree.focus(leafId);
|
|
8795
9669
|
wm.renderer._updateFocusClasses();
|
|
9670
|
+
const single = !!leaf.content && wm.isSingleKind(leaf.content.kind);
|
|
9671
|
+
const singleTitle = single ? wm.taxonomy.singleReason(leaf.content.kind) : void 0;
|
|
8796
9672
|
const items = [
|
|
8797
9673
|
{ label: "Split horizontally", icon: "splitscreen_vertical_add", action: "split-h" },
|
|
8798
9674
|
{ label: "Split vertically", icon: "splitscreen_add", action: "split-v" },
|
|
@@ -8801,7 +9677,8 @@ function _tileContextMenu(wm, leafId, x, y) {
|
|
|
8801
9677
|
label: "Open in new tab",
|
|
8802
9678
|
icon: "tab",
|
|
8803
9679
|
action: "open-tab",
|
|
8804
|
-
disabled: isPanel || !leaf.content
|
|
9680
|
+
disabled: isPanel || !leaf.content || single,
|
|
9681
|
+
title: singleTitle
|
|
8805
9682
|
},
|
|
8806
9683
|
// TWO DIFFERENT GLYPHS FOR TWO DIFFERENT DESTINATIONS. `web_asset` is a
|
|
8807
9684
|
// window INSIDE the application — the same glyph `ManagedWindow` uses
|
|
@@ -8814,7 +9691,8 @@ function _tileContextMenu(wm, leafId, x, y) {
|
|
|
8814
9691
|
label: "Open a copy in a window",
|
|
8815
9692
|
icon: "web_asset",
|
|
8816
9693
|
action: "open-window",
|
|
8817
|
-
disabled: isPanel || !leaf.content
|
|
9694
|
+
disabled: isPanel || !leaf.content || single,
|
|
9695
|
+
title: singleTitle
|
|
8818
9696
|
},
|
|
8819
9697
|
// C20, THE OTHER HALF — and it was missing while the `close` half
|
|
8820
9698
|
// below carried a paragraph explaining why it could not be.
|
|
@@ -8880,6 +9758,8 @@ function _tileContextMenu(wm, leafId, x, y) {
|
|
|
8880
9758
|
export {
|
|
8881
9759
|
BADGE_LEVELS,
|
|
8882
9760
|
DesktopManager,
|
|
9761
|
+
KEYBOARD_SHORTCUTS_TOPIC_ID,
|
|
9762
|
+
KEYMAP_SHORTCUTS,
|
|
8883
9763
|
PLACEHOLDER_KIND,
|
|
8884
9764
|
PanelKeyRouter,
|
|
8885
9765
|
TILE_TAB_MIME,
|
|
@@ -8908,6 +9788,8 @@ export {
|
|
|
8908
9788
|
getPanelKeyRouter,
|
|
8909
9789
|
installKeymap,
|
|
8910
9790
|
installPanelKeyRouter,
|
|
9791
|
+
keyboardShortcutsTopic,
|
|
9792
|
+
keymapShortcuts,
|
|
8911
9793
|
loadDesktops,
|
|
8912
9794
|
makeLeaf,
|
|
8913
9795
|
makeLoadingOverlay,
|