flexdesk 0.16.1 → 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 +2217 -42
- package/THIRD_PARTY_LICENSES.md +0 -0
- package/css/base.css +1834 -75
- package/css/flexdesk.css +3146 -164
- 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-B5Q53LAJ.js → chunk-UWO43BZU.js} +2 -2
- 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 +3146 -164
- 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 +657 -234
- package/dist/tiles.js.map +4 -4
- package/dist/tokens.css +0 -0
- package/dist/widgets.js +3252 -2124
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +1227 -347
- 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 +7 -7
- package/src/tiles/scope_picker.js +0 -0
- package/src/tiles/tile_base.js +1 -1
- package/src/tiles/tile_config_form.js +442 -0
- package/src/tiles/tile_grid.js +2 -0
- 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 +0 -0
- 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-B5Q53LAJ.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/dist/chunk-W6FBJLPW.js +0 -367
- package/dist/chunk-W6FBJLPW.js.map +0 -7
- package/src/ui/controllers/panel_state_machine.js +0 -634
package/dist/wm.js
CHANGED
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
import {
|
|
2
|
-
HelpModal
|
|
3
|
-
|
|
2
|
+
HelpModal,
|
|
3
|
+
createMenuBar
|
|
4
|
+
} from "./chunk-JKZLNKG6.js";
|
|
5
|
+
import {
|
|
6
|
+
TAB_LEVELS,
|
|
7
|
+
createTabs
|
|
8
|
+
} from "./chunk-HYJYYEPK.js";
|
|
4
9
|
import {
|
|
5
10
|
showContextMenu
|
|
6
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-UWO43BZU.js";
|
|
7
12
|
import {
|
|
8
13
|
openForm
|
|
9
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-D3COLZZ2.js";
|
|
10
15
|
import {
|
|
11
16
|
ManagedWindow,
|
|
12
17
|
isRowActivation
|
|
13
|
-
} from "./chunk-
|
|
14
|
-
import "./chunk-
|
|
18
|
+
} from "./chunk-CVMCZZX2.js";
|
|
19
|
+
import "./chunk-HNU6MM2B.js";
|
|
15
20
|
import {
|
|
16
21
|
NotebookTabBar
|
|
17
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-UURZ37SD.js";
|
|
23
|
+
import "./chunk-N7HC3ADV.js";
|
|
18
24
|
import "./chunk-WVFGV5FT.js";
|
|
19
25
|
import "./chunk-JYWURG5T.js";
|
|
20
26
|
|
|
21
27
|
// src/tiling/bottom_panel_tabs.js
|
|
22
|
-
var BADGE_LEVELS =
|
|
28
|
+
var BADGE_LEVELS = TAB_LEVELS;
|
|
23
29
|
function createBottomPanelTabs(container, {
|
|
24
30
|
panes = [],
|
|
25
31
|
remember = null,
|
|
@@ -28,103 +34,38 @@ function createBottomPanelTabs(container, {
|
|
|
28
34
|
onSelect = null,
|
|
29
35
|
initial = null
|
|
30
36
|
} = {}) {
|
|
31
|
-
const doc = container.ownerDocument;
|
|
32
|
-
const root = doc.createElement("div");
|
|
33
|
-
root.className = "twm-bp";
|
|
34
|
-
const strip = doc.createElement("div");
|
|
35
|
-
strip.className = "twm-bp__tabs";
|
|
36
|
-
strip.setAttribute("role", "tablist");
|
|
37
|
-
const body = doc.createElement("div");
|
|
38
|
-
body.className = "twm-bp__body";
|
|
39
|
-
root.append(strip, body);
|
|
40
|
-
container.appendChild(root);
|
|
41
37
|
const byId = new Map(panes.map((pane) => [pane.id, pane]));
|
|
42
|
-
|
|
43
|
-
const mounted = /* @__PURE__ */ new Map();
|
|
44
|
-
let active = null;
|
|
38
|
+
let restoring = false;
|
|
45
39
|
let chosen = false;
|
|
46
40
|
let destroyed = false;
|
|
47
|
-
|
|
48
|
-
const tab = doc.createElement("button");
|
|
49
|
-
tab.type = "button";
|
|
50
|
-
tab.className = "twm-bp__tab";
|
|
51
|
-
tab.dataset.pane = pane.id;
|
|
52
|
-
tab.setAttribute("role", "tab");
|
|
53
|
-
tab.setAttribute("aria-selected", "false");
|
|
54
|
-
const label = doc.createElement("span");
|
|
55
|
-
label.className = "twm-bp__label";
|
|
56
|
-
label.textContent = pane.label;
|
|
57
|
-
const badge = doc.createElement("span");
|
|
58
|
-
badge.className = "twm-bp__badge";
|
|
59
|
-
badge.hidden = true;
|
|
60
|
-
tab.append(label, badge);
|
|
61
|
-
tab.addEventListener("click", () => api.select(pane.id));
|
|
62
|
-
strip.appendChild(tab);
|
|
63
|
-
tabs.set(pane.id, tab);
|
|
64
|
-
}
|
|
65
|
-
function mount(id) {
|
|
66
|
-
if (mounted.has(id)) return mounted.get(id);
|
|
67
|
-
const pane = byId.get(id);
|
|
68
|
-
const el = doc.createElement("div");
|
|
69
|
-
el.className = "twm-bp-pane";
|
|
70
|
-
el.dataset.pane = id;
|
|
71
|
-
el.setAttribute("role", "tabpanel");
|
|
72
|
-
el.hidden = id !== active;
|
|
73
|
-
body.appendChild(el);
|
|
74
|
-
const entry = { el, instance: null };
|
|
75
|
-
mounted.set(id, entry);
|
|
76
|
-
try {
|
|
77
|
-
entry.instance = pane.mount?.(el, store ? store(pane) : null, api) ?? null;
|
|
78
|
-
} catch (err) {
|
|
79
|
-
console.error(`[bottom-panel] pane "${id}" failed to mount`, err);
|
|
80
|
-
}
|
|
81
|
-
return entry;
|
|
82
|
-
}
|
|
83
|
-
function show(id) {
|
|
84
|
-
if (destroyed || !byId.has(id)) return false;
|
|
85
|
-
active = id;
|
|
86
|
-
for (const [key, tab] of tabs) {
|
|
87
|
-
const on = key === id;
|
|
88
|
-
tab.classList.toggle("twm-bp__tab--on", on);
|
|
89
|
-
tab.setAttribute("aria-selected", String(on));
|
|
90
|
-
}
|
|
91
|
-
for (const [key, entry] of mounted) entry.el.hidden = key !== id;
|
|
92
|
-
mount(id).instance?.onShow?.();
|
|
93
|
-
onSelect?.(id, byId.get(id));
|
|
94
|
-
return true;
|
|
95
|
-
}
|
|
41
|
+
let strip = null;
|
|
96
42
|
const api = {
|
|
97
|
-
root,
|
|
43
|
+
root: null,
|
|
98
44
|
/** Bring a tab to the front, and remember it. */
|
|
99
45
|
select(id) {
|
|
100
|
-
if (!
|
|
46
|
+
if (destroyed || !byId.has(id)) return;
|
|
101
47
|
chosen = true;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
48
|
+
if (strip.active() !== id) {
|
|
49
|
+
strip.select(id);
|
|
50
|
+
return;
|
|
105
51
|
}
|
|
52
|
+
strip.instance(id)?.onShow?.();
|
|
53
|
+
remembered(id);
|
|
106
54
|
},
|
|
107
55
|
/** The tab in front, or null. */
|
|
108
|
-
active: () => active,
|
|
56
|
+
active: () => strip.active(),
|
|
109
57
|
/** `count` on a tab, coloured by `level`; nothing at zero. */
|
|
110
|
-
setBadge(id, count, level = null)
|
|
111
|
-
const badge = tabs.get(id)?.querySelector(".twm-bp__badge");
|
|
112
|
-
if (!badge) return;
|
|
113
|
-
const n = Number(count) || 0;
|
|
114
|
-
badge.hidden = n <= 0;
|
|
115
|
-
badge.textContent = n > 99 ? "99+" : String(Math.max(n, 0));
|
|
116
|
-
badge.className = "twm-bp__badge" + (BADGE_LEVELS.includes(level) ? ` twm-bp__badge--${level}` : "");
|
|
117
|
-
},
|
|
58
|
+
setBadge: (id, count, level = null) => strip.setBadge(id, count, level),
|
|
118
59
|
/** A mounted pane's live instance, or null — how one tab hands work to
|
|
119
60
|
* another. `select` it first, which mounts it. */
|
|
120
|
-
instance: (id) =>
|
|
61
|
+
instance: (id) => strip.instance(id),
|
|
121
62
|
/** The panel is on screen again (its tile re-mounted): tell the pane in front. */
|
|
122
63
|
reshow() {
|
|
123
|
-
|
|
64
|
+
strip.instance(strip.active())?.onShow?.();
|
|
124
65
|
},
|
|
125
66
|
/** Before the panel is torn down: every mounted pane saves. */
|
|
126
67
|
async persist() {
|
|
127
|
-
for (const
|
|
68
|
+
for (const instance of strip.instances()) await instance.persist?.();
|
|
128
69
|
},
|
|
129
70
|
/** Gone for good: panes save, then are destroyed; the bus is let go. */
|
|
130
71
|
async destroy() {
|
|
@@ -135,47 +76,410 @@ function createBottomPanelTabs(container, {
|
|
|
135
76
|
subscription?.dispose?.();
|
|
136
77
|
} catch {
|
|
137
78
|
}
|
|
138
|
-
|
|
139
|
-
try {
|
|
140
|
-
entry.instance?.destroy?.();
|
|
141
|
-
} catch (err) {
|
|
142
|
-
console.warn("[bottom-panel] pane destroy failed", err);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
mounted.clear();
|
|
146
|
-
root.remove();
|
|
79
|
+
strip.destroy();
|
|
147
80
|
}
|
|
148
81
|
};
|
|
149
|
-
|
|
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()));
|
|
150
114
|
const subscription = eventBus?.on?.(
|
|
151
115
|
"bottompanel:show",
|
|
152
116
|
(payload) => api.select(payload?.tab ?? panes[0]?.id)
|
|
153
117
|
) ?? null;
|
|
154
|
-
show(byId.has(initial) ? initial : panes[0]?.id);
|
|
155
118
|
if (remember?.read) {
|
|
156
119
|
Promise.resolve().then(() => remember.read()).then((saved) => {
|
|
157
|
-
if (chosen || destroyed) return;
|
|
158
|
-
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
|
+
}
|
|
159
132
|
}).catch(() => {
|
|
160
133
|
});
|
|
161
134
|
}
|
|
162
135
|
return api;
|
|
163
136
|
}
|
|
164
137
|
|
|
165
|
-
// src/tiling/
|
|
166
|
-
var
|
|
167
|
-
|
|
168
|
-
{
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
+
};
|
|
177
256
|
}
|
|
257
|
+
if (m[1] === "desktop") {
|
|
258
|
+
return { label: `Switch to desktop ${n + 1}`, run: ({ wm }) => {
|
|
259
|
+
wm.switchDesktop(n);
|
|
260
|
+
} };
|
|
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]
|
|
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"]
|
|
178
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
|
+
}
|
|
179
483
|
var DEFAULT_GROUP_CAP = 4;
|
|
180
484
|
var DEFAULT_COMMAND_MIN_CHARS = 2;
|
|
181
485
|
var REMOTE_DEBOUNCE_MS = 150;
|
|
@@ -197,7 +501,8 @@ function createCommandPalette({
|
|
|
197
501
|
drill = null,
|
|
198
502
|
cache = false,
|
|
199
503
|
tips = null,
|
|
200
|
-
footerNote = ""
|
|
504
|
+
footerNote = "",
|
|
505
|
+
shortcuts = KEYMAP_SHORTCUTS
|
|
201
506
|
}) {
|
|
202
507
|
if (!taxonomy) throw new Error("createCommandPalette: a taxonomy is required");
|
|
203
508
|
if (!catalog) throw new Error("createCommandPalette: an entity catalog is required");
|
|
@@ -206,6 +511,7 @@ function createCommandPalette({
|
|
|
206
511
|
let entities = [];
|
|
207
512
|
let cached = null;
|
|
208
513
|
let onClose = null;
|
|
514
|
+
const isSingle = (row) => !!row && taxonomy.isSingle(row.kind);
|
|
209
515
|
const isOpen = () => !!overlay;
|
|
210
516
|
const close = () => {
|
|
211
517
|
if (!overlay) return;
|
|
@@ -223,6 +529,7 @@ function createCommandPalette({
|
|
|
223
529
|
const invalidate = () => {
|
|
224
530
|
cached = null;
|
|
225
531
|
};
|
|
532
|
+
const staticCommands = _staticCommands(shortcuts);
|
|
226
533
|
const open = async () => {
|
|
227
534
|
if (overlay) return;
|
|
228
535
|
overlay = document.createElement("div");
|
|
@@ -240,19 +547,28 @@ function createCommandPalette({
|
|
|
240
547
|
const chipSlot = overlay.querySelector('[data-role="type-chip"]');
|
|
241
548
|
const chips = overlay.querySelectorAll("[data-shortcut]");
|
|
242
549
|
const toggles = overlay.querySelectorAll("[data-toggle]");
|
|
243
|
-
|
|
550
|
+
const syncToggle = (btn) => {
|
|
244
551
|
const which = btn.dataset.toggle;
|
|
552
|
+
const { available, reason } = wm.getPanelAvailability(which);
|
|
245
553
|
btn.classList.toggle("twm-chip--on", wm.isPanelOpen(which));
|
|
246
|
-
|
|
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);
|
|
247
564
|
chips.forEach((btn) => btn.addEventListener("click", () => {
|
|
248
565
|
const kind = btn.dataset.shortcut;
|
|
249
566
|
wm.openInPrimary(kind);
|
|
250
567
|
close();
|
|
251
568
|
}));
|
|
252
569
|
toggles.forEach((btn) => btn.addEventListener("click", () => {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
btn.classList.toggle("twm-chip--on", wm.isPanelOpen(which));
|
|
570
|
+
wm.togglePanel(btn.dataset.toggle);
|
|
571
|
+
syncToggle(btn);
|
|
256
572
|
}));
|
|
257
573
|
let filtered = [];
|
|
258
574
|
let active = 0;
|
|
@@ -273,12 +589,12 @@ function createCommandPalette({
|
|
|
273
589
|
const renderChip = () => {
|
|
274
590
|
if (scope) {
|
|
275
591
|
chipSlot.innerHTML = `
|
|
276
|
-
<span class="twm-cmdpal__type-chip" title="${
|
|
277
|
-
<span class="material-symbols-outlined twm-cmdpal__type-chip-icon">${
|
|
278
|
-
<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>
|
|
279
595
|
<button type="button" class="twm-cmdpal__type-chip-x"
|
|
280
596
|
data-action="clear-type"
|
|
281
|
-
aria-label="Leave ${
|
|
597
|
+
aria-label="Leave ${_esc2(scope.label)}">\xD7</button>
|
|
282
598
|
</span>`;
|
|
283
599
|
chipSlot.querySelector('[data-action="clear-type"]')?.addEventListener("click", (ev) => {
|
|
284
600
|
ev.preventDefault();
|
|
@@ -295,9 +611,9 @@ function createCommandPalette({
|
|
|
295
611
|
const meta = taxonomy.meta(head);
|
|
296
612
|
const label = typeFilterLabel || meta?.label || head;
|
|
297
613
|
chipSlot.innerHTML = `
|
|
298
|
-
<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"})">
|
|
299
615
|
<span class="material-symbols-outlined twm-cmdpal__type-chip-icon">${meta?.icon || "arrow_right"}</span>
|
|
300
|
-
<span class="twm-cmdpal__type-chip-label">${
|
|
616
|
+
<span class="twm-cmdpal__type-chip-label">${_esc2(label)}</span>
|
|
301
617
|
<button type="button" class="twm-cmdpal__type-chip-x"
|
|
302
618
|
data-action="clear-type"
|
|
303
619
|
aria-label="Clear type filter">\xD7</button>
|
|
@@ -321,9 +637,9 @@ function createCommandPalette({
|
|
|
321
637
|
<div class="twm-cmdpal__item${i === active ? " twm-cmdpal__item--active" : ""}"
|
|
322
638
|
data-idx="${i}">
|
|
323
639
|
<span class="material-symbols-outlined twm-cmdpal__icon">${e.icon || "arrow_right"}</span>
|
|
324
|
-
<span class="twm-cmdpal__label">${
|
|
640
|
+
<span class="twm-cmdpal__label">${_esc2(e.label || e.id)}</span>
|
|
325
641
|
<span class="twm-cmdpal__kind">${e.kind}</span>
|
|
326
|
-
<span class="twm-cmdpal__hint">${
|
|
642
|
+
<span class="twm-cmdpal__hint">${_esc2(e.hint || "")}</span>
|
|
327
643
|
</div>
|
|
328
644
|
`).join("") || "";
|
|
329
645
|
list.querySelectorAll("[data-idx]").forEach((el) => {
|
|
@@ -348,21 +664,22 @@ function createCommandPalette({
|
|
|
348
664
|
if (r.more) {
|
|
349
665
|
return `<div class="twm-cmdpal__item twm-cmdpal__item--more${on}" data-idx="${i}" role="option">
|
|
350
666
|
<span class="material-symbols-outlined twm-cmdpal__icon">expand_more</span>
|
|
351
|
-
<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>
|
|
352
668
|
<span class="twm-cmdpal__side"><span class="twm-cmdpal__acts"><span><kbd>Enter</kbd> show all</span></span></span>
|
|
353
669
|
</div>`;
|
|
354
670
|
}
|
|
355
671
|
const drawn = _drawn(r);
|
|
356
|
-
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>` : "";
|
|
357
673
|
const verb = r.verb || (r.action || r.kind === "command" ? "run" : "open");
|
|
358
674
|
const modes = Array.isArray(r.modes) ? r.modes : [];
|
|
359
|
-
const acts = [`<span><kbd>Enter</kbd> ${
|
|
360
|
-
if (r.drillLabel && drill) acts.push(`<span><kbd>Tab</kbd> ${
|
|
361
|
-
|
|
362
|
-
if (modes.includes("
|
|
363
|
-
|
|
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>`;
|
|
364
681
|
return `<div class="twm-cmdpal__item${on}" data-idx="${i}" role="option">
|
|
365
|
-
<span class="material-symbols-outlined twm-cmdpal__icon">${
|
|
682
|
+
<span class="material-symbols-outlined twm-cmdpal__icon">${_esc2(r.icon || "arrow_right")}</span>
|
|
366
683
|
<span class="twm-cmdpal__main"><span class="twm-cmdpal__label">${_mark(r.label || r.id, r._hits)}</span>${ctx}</span>
|
|
367
684
|
<span class="twm-cmdpal__side">${side}<span class="twm-cmdpal__acts">${acts.join("")}</span></span>
|
|
368
685
|
</div>`;
|
|
@@ -371,22 +688,22 @@ function createCommandPalette({
|
|
|
371
688
|
let html = "";
|
|
372
689
|
for (const s of sections) {
|
|
373
690
|
if (!s.count) continue;
|
|
374
|
-
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>`;
|
|
375
692
|
for (let i = s.start; i < s.start + s.count; i++) html += rowHtml(filtered[i], i);
|
|
376
693
|
}
|
|
377
694
|
const q = input.value.trim();
|
|
378
695
|
if (!filtered.length) {
|
|
379
696
|
if (scope) {
|
|
380
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}.`;
|
|
381
|
-
html += `<div class="twm-cmdpal__empty">${
|
|
698
|
+
html += `<div class="twm-cmdpal__empty">${_esc2(msg)}</div>`;
|
|
382
699
|
} else if (q && q !== ">") {
|
|
383
|
-
html += `<div class="twm-cmdpal__empty">Nothing matches \u201C${
|
|
700
|
+
html += `<div class="twm-cmdpal__empty">Nothing matches \u201C${_esc2(q)}\u201D.</div>`;
|
|
384
701
|
}
|
|
385
702
|
} else if (scope && remote.pending) {
|
|
386
703
|
html += '<div class="twm-cmdpal__empty twm-cmdpal__empty--quiet">Searching\u2026</div>';
|
|
387
704
|
}
|
|
388
705
|
if (!q && !scope && !typeFilter && Array.isArray(tips) && tips.length) {
|
|
389
|
-
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>`;
|
|
390
707
|
}
|
|
391
708
|
list.innerHTML = html;
|
|
392
709
|
renderFooter();
|
|
@@ -405,7 +722,7 @@ function createCommandPalette({
|
|
|
405
722
|
if (scope || typeFilter) parts.push("<span><kbd>Backspace</kbd> leave</span>");
|
|
406
723
|
parts.push("<span><kbd>Esc</kbd> close</span>");
|
|
407
724
|
const note = scope?.note || footerNote;
|
|
408
|
-
foot.innerHTML = parts.join("") + (note ? `<span class="twm-cmdpal__footnote">${
|
|
725
|
+
foot.innerHTML = parts.join("") + (note ? `<span class="twm-cmdpal__footnote">${_esc2(note)}</span>` : "");
|
|
409
726
|
};
|
|
410
727
|
if (grouped) {
|
|
411
728
|
list.addEventListener("mousemove", (e) => {
|
|
@@ -430,6 +747,7 @@ function createCommandPalette({
|
|
|
430
747
|
showAll(picked.more);
|
|
431
748
|
return;
|
|
432
749
|
}
|
|
750
|
+
if (mode !== "open" && isSingle(picked)) return;
|
|
433
751
|
const pick = _public(picked);
|
|
434
752
|
const how = { mode, scope: scope?.id ?? null };
|
|
435
753
|
close();
|
|
@@ -675,7 +993,7 @@ function createCommandPalette({
|
|
|
675
993
|
console.warn("[cmdpal] commands() threw", err);
|
|
676
994
|
}
|
|
677
995
|
}
|
|
678
|
-
return [...
|
|
996
|
+
return [...staticCommands, ...extra];
|
|
679
997
|
};
|
|
680
998
|
const withCommands = (rows) => grouped ? [...rows, ...commandRows()] : [...commandRows(), ...rows];
|
|
681
999
|
entities = _decorateIcons(withCommands(cache && cached ? cached : []), taxonomy);
|
|
@@ -730,7 +1048,7 @@ function _markup(taxonomy, placeholder, wm, { chrome = {}, grouped = false } = {
|
|
|
730
1048
|
<span class="material-symbols-outlined twm-cmdpal__search-icon">search</span>
|
|
731
1049
|
<span class="twm-cmdpal__type-chip-slot" data-role="type-chip"></span>
|
|
732
1050
|
<input type="text" class="twm-cmdpal__input"
|
|
733
|
-
placeholder="${
|
|
1051
|
+
placeholder="${_esc2(placeholder)}"
|
|
734
1052
|
autocomplete="off" spellcheck="false" role="combobox"
|
|
735
1053
|
aria-expanded="true" aria-controls="twm-cmdpal-list" />
|
|
736
1054
|
<kbd class="twm-cmdpal__esc">Esc</kbd>
|
|
@@ -746,7 +1064,7 @@ function _markup(taxonomy, placeholder, wm, { chrome = {}, grouped = false } = {
|
|
|
746
1064
|
<div class="twm-cmdpal__inputrow">
|
|
747
1065
|
<span class="twm-cmdpal__type-chip-slot" data-role="type-chip"></span>
|
|
748
1066
|
<input type="text" class="twm-cmdpal__input"
|
|
749
|
-
placeholder="${
|
|
1067
|
+
placeholder="${_esc2(placeholder)}"
|
|
750
1068
|
autocomplete="off" />
|
|
751
1069
|
</div>
|
|
752
1070
|
<div class="twm-cmdpal__chips">${chips}</div>
|
|
@@ -864,7 +1182,7 @@ function _match(text, query) {
|
|
|
864
1182
|
}
|
|
865
1183
|
function _mark(text, hits) {
|
|
866
1184
|
const s = String(text ?? "");
|
|
867
|
-
if (!hits?.length) return
|
|
1185
|
+
if (!hits?.length) return _esc2(s);
|
|
868
1186
|
const set = new Set(hits);
|
|
869
1187
|
let out = "";
|
|
870
1188
|
let open = false;
|
|
@@ -878,12 +1196,12 @@ function _mark(text, hits) {
|
|
|
878
1196
|
out += "</mark>";
|
|
879
1197
|
open = false;
|
|
880
1198
|
}
|
|
881
|
-
out +=
|
|
1199
|
+
out += _esc2(s[i]);
|
|
882
1200
|
}
|
|
883
1201
|
if (open) out += "</mark>";
|
|
884
1202
|
return out;
|
|
885
1203
|
}
|
|
886
|
-
function
|
|
1204
|
+
function _esc2(s) {
|
|
887
1205
|
return String(s ?? "").replace(/[&<>"']/g, (c) => ({
|
|
888
1206
|
"&": "&",
|
|
889
1207
|
"<": "<",
|
|
@@ -904,7 +1222,7 @@ function windowPlaceholderFactory(hostEl, props, ctx) {
|
|
|
904
1222
|
hostEl.innerHTML = `
|
|
905
1223
|
<div class="twm-window-placeholder">
|
|
906
1224
|
<span class="material-symbols-outlined twm-window-placeholder__icon">open_in_new</span>
|
|
907
|
-
<div class="twm-window-placeholder__title">${
|
|
1225
|
+
<div class="twm-window-placeholder__title">${_esc3(props.originalTitle || props.originalKind || "content")}</div>
|
|
908
1226
|
<div class="twm-window-placeholder__hint">is open in a managed window</div>
|
|
909
1227
|
<button class="twm-window-placeholder__btn" data-action="bring-back">
|
|
910
1228
|
Bring back to this tile
|
|
@@ -956,7 +1274,7 @@ function _placeholder(kind) {
|
|
|
956
1274
|
return { title: kind };
|
|
957
1275
|
};
|
|
958
1276
|
}
|
|
959
|
-
function
|
|
1277
|
+
function _esc3(s) {
|
|
960
1278
|
return String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
961
1279
|
}
|
|
962
1280
|
|
|
@@ -1806,6 +2124,11 @@ function _makeDesktop(label, seed, panelDefaults = DEFAULT_PANEL_STATE) {
|
|
|
1806
2124
|
// when it asked for nothing. Names are assigned by wm._canonicalize via
|
|
1807
2125
|
// PANEL_TITLES.
|
|
1808
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: {},
|
|
1809
2132
|
// C40. Both unset is a desktop exactly as it always was.
|
|
1810
2133
|
group: null,
|
|
1811
2134
|
fixed: false
|
|
@@ -1929,6 +2252,9 @@ var DesktopManager = class _DesktopManager {
|
|
|
1929
2252
|
label: d.label,
|
|
1930
2253
|
tree: d.tree.serialize(),
|
|
1931
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 } } : {},
|
|
1932
2258
|
// C40. Written only when set, so a blob without them is
|
|
1933
2259
|
// byte-for-byte the blob it always was.
|
|
1934
2260
|
...d.group ? { group: d.group } : {},
|
|
@@ -1951,6 +2277,7 @@ var DesktopManager = class _DesktopManager {
|
|
|
1951
2277
|
// this merge order avoids. The default only fills a key the stored
|
|
1952
2278
|
// blob predates.
|
|
1953
2279
|
panels: { ...m.panelDefaults, ...raw.panels || {} },
|
|
2280
|
+
panelShares: { ...raw.panelShares || {} },
|
|
1954
2281
|
group: raw.group || null,
|
|
1955
2282
|
fixed: !!raw.fixed
|
|
1956
2283
|
}));
|
|
@@ -2093,120 +2420,25 @@ function createEntityCatalog({ sources, aliases = {}, aggregate = null } = {}) {
|
|
|
2093
2420
|
});
|
|
2094
2421
|
}
|
|
2095
2422
|
|
|
2096
|
-
// src/tiling/
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
}
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
return;
|
|
2116
|
-
}
|
|
2117
|
-
}
|
|
2118
|
-
if (e.key === "Backspace" && !inField && !e.metaKey && !e.ctrlKey) {
|
|
2119
|
-
e.preventDefault();
|
|
2120
|
-
wm.navigateBack?.();
|
|
2121
|
-
return;
|
|
2122
|
-
}
|
|
2123
|
-
const fMatch = /^F([1-9]|1[0-2])$/.exec(e.key);
|
|
2124
|
-
if (fMatch && !inField && !e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
|
|
2125
|
-
const btns = document.querySelectorAll(
|
|
2126
|
-
opts.navSelector || ".twm-global-top-bar .twm-bar-center.twm-top-nav .twm-top-nav__btn"
|
|
2127
|
-
);
|
|
2128
|
-
const idx = Number(fMatch[1]) - 1;
|
|
2129
|
-
if (idx < btns.length) {
|
|
2130
|
-
e.preventDefault();
|
|
2131
|
-
btns[idx].click();
|
|
2132
|
-
return;
|
|
2133
|
-
}
|
|
2134
|
-
}
|
|
2135
|
-
if (e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey && !inField && (e.key === "ArrowLeft" || e.key === "ArrowRight")) {
|
|
2136
|
-
const tabs = [...document.querySelectorAll(".twm-bp__tabs .twm-bp__tab")];
|
|
2137
|
-
if (tabs.length) {
|
|
2138
|
-
const cur = tabs.findIndex((t) => t.classList.contains("twm-bp__tab--on"));
|
|
2139
|
-
const dir = e.key === "ArrowRight" ? 1 : -1;
|
|
2140
|
-
const next = ((cur < 0 ? 0 : cur) + dir + tabs.length) % tabs.length;
|
|
2141
|
-
e.preventDefault();
|
|
2142
|
-
tabs[next].click();
|
|
2143
|
-
return;
|
|
2144
|
-
}
|
|
2145
|
-
}
|
|
2146
|
-
if (e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey && !inField && (e.key === "ArrowLeft" || e.key === "ArrowRight")) {
|
|
2147
|
-
e.preventDefault();
|
|
2148
|
-
wm.cycleDesktop(e.key === "ArrowRight" ? 1 : -1);
|
|
2149
|
-
return;
|
|
2150
|
-
}
|
|
2151
|
-
if (!e.altKey) return;
|
|
2152
|
-
const key = e.key.toLowerCase();
|
|
2153
|
-
if (/^[1-9]$/.test(e.key)) {
|
|
2154
|
-
e.preventDefault();
|
|
2155
|
-
const idx = Number(e.key) - 1;
|
|
2156
|
-
if (e.shiftKey) wm.moveFocusedToDesktop(idx);
|
|
2157
|
-
else wm.switchDesktop(idx);
|
|
2158
|
-
return;
|
|
2159
|
-
}
|
|
2160
|
-
switch (key) {
|
|
2161
|
-
// Plain Alt+H/V split into an empty pane; with Shift they open
|
|
2162
|
-
// the focused tile's own content in the new split.
|
|
2163
|
-
case "h":
|
|
2164
|
-
e.preventDefault();
|
|
2165
|
-
e.shiftKey ? wm.splitFocusedWith("h") : wm.split("h");
|
|
2166
|
-
return;
|
|
2167
|
-
case "v":
|
|
2168
|
-
e.preventDefault();
|
|
2169
|
-
e.shiftKey ? wm.splitFocusedWith("v") : wm.split("v");
|
|
2170
|
-
return;
|
|
2171
|
-
case "t":
|
|
2172
|
-
e.preventDefault();
|
|
2173
|
-
wm.openFocusedInTab();
|
|
2174
|
-
return;
|
|
2175
|
-
case "n":
|
|
2176
|
-
e.preventDefault();
|
|
2177
|
-
wm.openFocusedInWindow();
|
|
2178
|
-
return;
|
|
2179
|
-
case "w":
|
|
2180
|
-
e.preventDefault();
|
|
2181
|
-
wm.closeFocused();
|
|
2182
|
-
return;
|
|
2183
|
-
case "f":
|
|
2184
|
-
e.preventDefault();
|
|
2185
|
-
wm.toggleManagedFocused();
|
|
2186
|
-
return;
|
|
2187
|
-
case "arrowleft":
|
|
2188
|
-
e.preventDefault();
|
|
2189
|
-
e.shiftKey ? wm.moveFocused("left") : wm.focusDir("left");
|
|
2190
|
-
return;
|
|
2191
|
-
case "arrowright":
|
|
2192
|
-
e.preventDefault();
|
|
2193
|
-
e.shiftKey ? wm.moveFocused("right") : wm.focusDir("right");
|
|
2194
|
-
return;
|
|
2195
|
-
case "arrowup":
|
|
2196
|
-
e.preventDefault();
|
|
2197
|
-
e.shiftKey ? wm.moveFocused("up") : wm.focusDir("up");
|
|
2198
|
-
return;
|
|
2199
|
-
case "arrowdown":
|
|
2200
|
-
e.preventDefault();
|
|
2201
|
-
e.shiftKey ? wm.moveFocused("down") : wm.focusDir("down");
|
|
2202
|
-
return;
|
|
2203
|
-
}
|
|
2204
|
-
if (key.length === 1 && /[a-z]/.test(key) && !inField) {
|
|
2205
|
-
e.preventDefault();
|
|
2206
|
-
}
|
|
2207
|
-
};
|
|
2208
|
-
document.addEventListener("keydown", onKeyDown);
|
|
2209
|
-
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
|
+
}
|
|
2210
2442
|
}
|
|
2211
2443
|
|
|
2212
2444
|
// src/tiling/kind_taxonomy.js
|
|
@@ -2231,6 +2463,27 @@ function createTaxonomy({ kinds, root } = {}) {
|
|
|
2231
2463
|
`createTaxonomy: '${kind}' is both a top-nav kind and owned by one`
|
|
2232
2464
|
);
|
|
2233
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
|
+
);
|
|
2234
2487
|
}
|
|
2235
2488
|
const _kinds = Object.freeze({ ...kinds });
|
|
2236
2489
|
const meta = (kind) => _kinds[kind];
|
|
@@ -2280,12 +2533,31 @@ function createTaxonomy({ kinds, root } = {}) {
|
|
|
2280
2533
|
const s = _kinds[kind]?.sources ?? _kinds[root]?.sources;
|
|
2281
2534
|
return Array.isArray(s) ? s : [];
|
|
2282
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
|
+
};
|
|
2283
2543
|
const topNavEntries = () => Object.entries(_kinds).filter(([, m]) => m.isTopNav).sort((a, b) => (a[1].order ?? 1e3) - (b[1].order ?? 1e3)).map(([kind, m]) => ({
|
|
2284
2544
|
kind,
|
|
2285
2545
|
label: m.shortLabel || m.label,
|
|
2286
2546
|
longLabel: m.label,
|
|
2287
2547
|
icon: m.icon
|
|
2288
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`;
|
|
2289
2561
|
return Object.freeze({
|
|
2290
2562
|
root,
|
|
2291
2563
|
kinds: _kinds,
|
|
@@ -2296,7 +2568,11 @@ function createTaxonomy({ kinds, root } = {}) {
|
|
|
2296
2568
|
parentOf,
|
|
2297
2569
|
labelOf,
|
|
2298
2570
|
sourcesFor,
|
|
2299
|
-
|
|
2571
|
+
panelOffer,
|
|
2572
|
+
topNavEntries,
|
|
2573
|
+
isSingle,
|
|
2574
|
+
instanceKey,
|
|
2575
|
+
singleReason
|
|
2300
2576
|
});
|
|
2301
2577
|
}
|
|
2302
2578
|
|
|
@@ -2762,7 +3038,7 @@ function mountLandingShell(hostEl, opts) {
|
|
|
2762
3038
|
pane = { key: "rows", title: "", hint: "" }
|
|
2763
3039
|
} = opts || {};
|
|
2764
3040
|
const paneKey = String(pane.key || "rows");
|
|
2765
|
-
const
|
|
3041
|
+
const _esc9 = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({
|
|
2766
3042
|
"&": "&",
|
|
2767
3043
|
"<": "<",
|
|
2768
3044
|
">": ">",
|
|
@@ -2771,24 +3047,24 @@ function mountLandingShell(hostEl, opts) {
|
|
|
2771
3047
|
})[c]);
|
|
2772
3048
|
const statsHtml = (stats || []).map((s) => `
|
|
2773
3049
|
<span class="twm-landing__stat twm-entity-header__field">
|
|
2774
|
-
<span>${
|
|
2775
|
-
<strong data-role="${
|
|
3050
|
+
<span>${_esc9(s.label || "")}</span>
|
|
3051
|
+
<strong data-role="${_esc9(s.key)}">${_esc9(s.initial != null ? s.initial : 0)}</strong>
|
|
2776
3052
|
</span>
|
|
2777
3053
|
`).join("");
|
|
2778
3054
|
hostEl.classList.add("twm-landing");
|
|
2779
3055
|
hostEl.innerHTML = `
|
|
2780
3056
|
<header class="twm-entity-header">
|
|
2781
|
-
<h2>${
|
|
3057
|
+
<h2>${_esc9(title)}</h2>
|
|
2782
3058
|
${statsHtml}
|
|
2783
3059
|
<span class="twm-entity-header__spacer"></span>
|
|
2784
3060
|
</header>
|
|
2785
3061
|
<div class="twm-landing__split twm-landing__split--single">
|
|
2786
|
-
<section class="twm-landing__pane" data-pane="${
|
|
3062
|
+
<section class="twm-landing__pane" data-pane="${_esc9(paneKey)}">
|
|
2787
3063
|
<div class="twm-landing__pane-head">
|
|
2788
|
-
<span class="twm-landing__pane-title">${
|
|
2789
|
-
<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>
|
|
2790
3066
|
</div>
|
|
2791
|
-
<div class="twm-landing__table" data-role="${
|
|
3067
|
+
<div class="twm-landing__table" data-role="${_esc9(paneKey)}-host"></div>
|
|
2792
3068
|
</section>
|
|
2793
3069
|
</div>
|
|
2794
3070
|
<footer class="twm-landing__actions" data-role="actions"></footer>
|
|
@@ -2817,9 +3093,9 @@ function mountLandingActions(footerEl, actions, hostEl) {
|
|
|
2817
3093
|
btn.className = "twm-action-btn";
|
|
2818
3094
|
if (a.id) btn.dataset.action = a.id;
|
|
2819
3095
|
if (a.title) btn.title = a.title;
|
|
2820
|
-
const icon = a.icon ? `<span class="material-symbols-outlined">${
|
|
2821
|
-
const sc = a.shortcut ? ` <span class="twm-action-btn__kbd">[${
|
|
2822
|
-
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}`;
|
|
2823
3099
|
btn.addEventListener("click", (ev) => {
|
|
2824
3100
|
ev.preventDefault();
|
|
2825
3101
|
try {
|
|
@@ -2880,7 +3156,7 @@ function actionsCellRenderer(actionsColIdx, options = { edit: true, delete: true
|
|
|
2880
3156
|
return true;
|
|
2881
3157
|
};
|
|
2882
3158
|
}
|
|
2883
|
-
function
|
|
3159
|
+
function _esc4(s) {
|
|
2884
3160
|
return String(s ?? "").replace(/[&<>"']/g, (c) => ({
|
|
2885
3161
|
"&": "&",
|
|
2886
3162
|
"<": "<",
|
|
@@ -2910,37 +3186,46 @@ function mountNavPanel(hostEl, {
|
|
|
2910
3186
|
modes = [],
|
|
2911
3187
|
wm = null,
|
|
2912
3188
|
eventBus = null,
|
|
2913
|
-
filterPlaceholder = "Filter\u2026"
|
|
3189
|
+
filterPlaceholder = "Filter\u2026",
|
|
3190
|
+
showModeStrip = true,
|
|
3191
|
+
showFilter = true,
|
|
3192
|
+
initialMode = null
|
|
2914
3193
|
} = {}) {
|
|
2915
3194
|
if (!Array.isArray(modes) || modes.length === 0) {
|
|
2916
3195
|
throw new Error("mountNavPanel: at least one mode is required");
|
|
2917
3196
|
}
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
3197
|
+
const stripHtml = showModeStrip ? `
|
|
3198
|
+
<div class="twm-nav__modes" data-role="modes"></div>` : "";
|
|
3199
|
+
const filterHtml = showFilter ? `
|
|
2921
3200
|
<div class="twm-sidebar-controls twm-nav__filter">
|
|
2922
3201
|
<span class="material-symbols-outlined twm-sidebar-search-icon">search</span>
|
|
2923
3202
|
<input class="twm-data-page__search twm-nav__filter-input"
|
|
2924
|
-
type="search" placeholder="${
|
|
3203
|
+
type="search" placeholder="${_esc5(filterPlaceholder)}"
|
|
2925
3204
|
autocomplete="off" />
|
|
2926
|
-
</div
|
|
3205
|
+
</div>` : "";
|
|
3206
|
+
hostEl.classList.add("twm-nav");
|
|
3207
|
+
hostEl.innerHTML = `${stripHtml}${filterHtml}
|
|
2927
3208
|
<div class="twm-nav__body" data-role="body"></div>
|
|
2928
3209
|
`;
|
|
2929
3210
|
const modesEl = hostEl.querySelector('[data-role="modes"]');
|
|
2930
3211
|
const bodyEl = hostEl.querySelector('[data-role="body"]');
|
|
3212
|
+
const filterBoxEl = hostEl.querySelector(".twm-nav__filter");
|
|
2931
3213
|
const filterEl = hostEl.querySelector(".twm-nav__filter-input");
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
<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>
|
|
2937
3220
|
</button>
|
|
2938
3221
|
`).join("");
|
|
3222
|
+
}
|
|
2939
3223
|
const byId = new Map(modes.map((m) => [m.id, m]));
|
|
2940
3224
|
const hosts = {};
|
|
2941
3225
|
const panes = {};
|
|
2942
3226
|
const filterByMode = Object.fromEntries(modes.map((m) => [m.id, ""]));
|
|
2943
|
-
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;
|
|
2944
3229
|
const ensureHost = (id) => {
|
|
2945
3230
|
if (hosts[id]) return hosts[id];
|
|
2946
3231
|
const el = document.createElement("div");
|
|
@@ -2957,8 +3242,12 @@ function mountNavPanel(hostEl, {
|
|
|
2957
3242
|
for (const m of modes) {
|
|
2958
3243
|
ensureHost(m.id).style.display = m.id === next ? "flex" : "none";
|
|
2959
3244
|
}
|
|
2960
|
-
modesEl
|
|
2961
|
-
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
|
+
}
|
|
2962
3251
|
const paneEl = ensureHost(next);
|
|
2963
3252
|
if (!panes[next]) {
|
|
2964
3253
|
try {
|
|
@@ -2976,6 +3265,7 @@ function mountNavPanel(hostEl, {
|
|
|
2976
3265
|
_applyFilter();
|
|
2977
3266
|
};
|
|
2978
3267
|
const _applyFilter = () => {
|
|
3268
|
+
if (!hasFilter(mode)) return;
|
|
2979
3269
|
const q = filterByMode[mode] || "";
|
|
2980
3270
|
try {
|
|
2981
3271
|
panes[mode]?.filter?.(q);
|
|
@@ -2983,16 +3273,19 @@ function mountNavPanel(hostEl, {
|
|
|
2983
3273
|
console.warn("[nav] filter failed", mode, err);
|
|
2984
3274
|
}
|
|
2985
3275
|
};
|
|
2986
|
-
filterEl
|
|
3276
|
+
filterEl?.addEventListener("input", () => {
|
|
3277
|
+
if (!hasFilter(mode)) return;
|
|
2987
3278
|
filterByMode[mode] = filterEl.value;
|
|
2988
3279
|
_applyFilter();
|
|
2989
3280
|
});
|
|
2990
|
-
modesEl
|
|
3281
|
+
modesEl?.addEventListener("click", (e) => {
|
|
2991
3282
|
const btn = e.target.closest("[data-mode]");
|
|
2992
3283
|
if (btn) activate(btn.dataset.mode);
|
|
2993
3284
|
});
|
|
2994
3285
|
activate(mode);
|
|
2995
3286
|
return {
|
|
3287
|
+
select: (modeId) => activate(modeId),
|
|
3288
|
+
active: () => mode,
|
|
2996
3289
|
refresh: () => activate(mode),
|
|
2997
3290
|
destroy: () => {
|
|
2998
3291
|
for (const p of Object.values(panes)) {
|
|
@@ -3004,7 +3297,7 @@ function mountNavPanel(hostEl, {
|
|
|
3004
3297
|
}
|
|
3005
3298
|
};
|
|
3006
3299
|
}
|
|
3007
|
-
function
|
|
3300
|
+
function _esc5(s) {
|
|
3008
3301
|
return String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
3009
3302
|
}
|
|
3010
3303
|
|
|
@@ -3158,8 +3451,8 @@ function _renderInto(container, segments) {
|
|
|
3158
3451
|
li.classList.add("twm-topbar-breadcrumb__item--current");
|
|
3159
3452
|
li.setAttribute("aria-current", "page");
|
|
3160
3453
|
li.innerHTML = `
|
|
3161
|
-
<span class="material-symbols-outlined twm-topbar-breadcrumb__icon">${
|
|
3162
|
-
<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>
|
|
3163
3456
|
`;
|
|
3164
3457
|
} else {
|
|
3165
3458
|
const btn = document.createElement("button");
|
|
@@ -3167,8 +3460,8 @@ function _renderInto(container, segments) {
|
|
|
3167
3460
|
btn.type = "button";
|
|
3168
3461
|
btn.title = String(seg.label ?? "");
|
|
3169
3462
|
btn.innerHTML = `
|
|
3170
|
-
<span class="material-symbols-outlined twm-topbar-breadcrumb__icon">${
|
|
3171
|
-
<span>${
|
|
3463
|
+
<span class="material-symbols-outlined twm-topbar-breadcrumb__icon">${_esc6(seg.icon)}</span>
|
|
3464
|
+
<span>${_esc6(seg.label)}</span>
|
|
3172
3465
|
`;
|
|
3173
3466
|
btn.addEventListener("click", () => {
|
|
3174
3467
|
btn.classList.add("twm-tile-breadcrumb__link--flash");
|
|
@@ -3191,7 +3484,7 @@ function _renderInto(container, segments) {
|
|
|
3191
3484
|
});
|
|
3192
3485
|
container.appendChild(ol);
|
|
3193
3486
|
}
|
|
3194
|
-
function
|
|
3487
|
+
function _esc6(s) {
|
|
3195
3488
|
return String(s ?? "").replace(/[&<>"']/g, (c) => ({
|
|
3196
3489
|
"&": "&",
|
|
3197
3490
|
"<": "<",
|
|
@@ -3276,7 +3569,7 @@ function createPageFactories({ eventBus = null, events = {}, refreshOn = {} } =
|
|
|
3276
3569
|
console.error("[page] mount failed", err);
|
|
3277
3570
|
hideLoading();
|
|
3278
3571
|
}
|
|
3279
|
-
const
|
|
3572
|
+
const subscriptions = [];
|
|
3280
3573
|
if (typeof tab?.refresh === "function" && eventBus?.on) {
|
|
3281
3574
|
for (const [busEvent, reason] of Object.entries(refreshOn || {})) {
|
|
3282
3575
|
const fn = () => {
|
|
@@ -3286,14 +3579,13 @@ function createPageFactories({ eventBus = null, events = {}, refreshOn = {} } =
|
|
|
3286
3579
|
console.warn("[page] refresh failed", busEvent, err);
|
|
3287
3580
|
}
|
|
3288
3581
|
};
|
|
3289
|
-
|
|
3290
|
-
eventBus.on(busEvent, fn);
|
|
3582
|
+
subscriptions.push(eventBus.on(busEvent, fn));
|
|
3291
3583
|
}
|
|
3292
3584
|
}
|
|
3293
3585
|
return () => {
|
|
3294
|
-
for (const
|
|
3586
|
+
for (const handle of subscriptions) {
|
|
3295
3587
|
try {
|
|
3296
|
-
|
|
3588
|
+
handle?.dispose?.();
|
|
3297
3589
|
} catch {
|
|
3298
3590
|
}
|
|
3299
3591
|
}
|
|
@@ -3380,6 +3672,7 @@ function createPageFactories({ eventBus = null, events = {}, refreshOn = {} } =
|
|
|
3380
3672
|
|
|
3381
3673
|
// src/tiling/tile_renderer.js
|
|
3382
3674
|
var SPLITTER_PX = 4;
|
|
3675
|
+
var MIN_SLOT_FRACTION = 0.05;
|
|
3383
3676
|
var TAB_LAYOUTS = ["bottom", "top", "header"];
|
|
3384
3677
|
var DEFAULT_TAB_LAYOUT = "bottom";
|
|
3385
3678
|
function _normalizeTabLayout(value) {
|
|
@@ -3412,6 +3705,12 @@ var TileRenderer = class {
|
|
|
3412
3705
|
static get TAB_MIME() {
|
|
3413
3706
|
return TILE_TAB_MIME;
|
|
3414
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
|
+
}
|
|
3415
3714
|
constructor({
|
|
3416
3715
|
root,
|
|
3417
3716
|
tree,
|
|
@@ -3419,6 +3718,7 @@ var TileRenderer = class {
|
|
|
3419
3718
|
ctx,
|
|
3420
3719
|
onFocusChange,
|
|
3421
3720
|
onAfterRender,
|
|
3721
|
+
onSplitResized,
|
|
3422
3722
|
tabLayout = null,
|
|
3423
3723
|
newTabButton = true
|
|
3424
3724
|
}) {
|
|
@@ -3433,6 +3733,8 @@ var TileRenderer = class {
|
|
|
3433
3733
|
});
|
|
3434
3734
|
this.onAfterRender = onAfterRender || (() => {
|
|
3435
3735
|
});
|
|
3736
|
+
this.onSplitResized = onSplitResized || (() => {
|
|
3737
|
+
});
|
|
3436
3738
|
this.newTabButton = newTabButton !== false;
|
|
3437
3739
|
this._leafCache = /* @__PURE__ */ new Map();
|
|
3438
3740
|
this._drag = null;
|
|
@@ -3695,6 +3997,7 @@ var TileRenderer = class {
|
|
|
3695
3997
|
let content = {};
|
|
3696
3998
|
let liveTitle = null;
|
|
3697
3999
|
let liveEntry = null;
|
|
4000
|
+
let liveActionsSet = false;
|
|
3698
4001
|
if (leaf.content) {
|
|
3699
4002
|
const setTitle = (text) => {
|
|
3700
4003
|
const next = text == null || text === "" ? null : String(text);
|
|
@@ -3708,13 +4011,22 @@ var TileRenderer = class {
|
|
|
3708
4011
|
const now = this.tree?.get?.(leaf.id) || leaf;
|
|
3709
4012
|
liveEntry.titleEl.textContent = next || now.title || (now.content ? now.content.kind : "empty");
|
|
3710
4013
|
};
|
|
3711
|
-
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 };
|
|
3712
4024
|
const activeProps = activeTab?.props ?? leaf.content.props;
|
|
3713
4025
|
content = this.content.mount(leaf.content.kind, body, activeProps, leafCtx);
|
|
3714
4026
|
if (content.title) title.textContent = content.title;
|
|
3715
4027
|
if (liveTitle) title.textContent = liveTitle;
|
|
3716
4028
|
_paintLeafIcon(icon, leaf, content, this.ctx);
|
|
3717
|
-
_paintContentActions(contentActions, content.chromeActions);
|
|
4029
|
+
if (!liveActionsSet) _paintContentActions(contentActions, content.chromeActions);
|
|
3718
4030
|
_vetoStructuralActions(actions, content.chrome);
|
|
3719
4031
|
} else {
|
|
3720
4032
|
body.innerHTML = `<div class="tile-placeholder"><div class="tile-placeholder__hint">empty tile</div></div>`;
|
|
@@ -3726,6 +4038,10 @@ var TileRenderer = class {
|
|
|
3726
4038
|
titleEl: title,
|
|
3727
4039
|
iconEl: icon,
|
|
3728
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,
|
|
3729
4045
|
// DERIVED HERE, NOT REUSED FROM ABOVE. `kindKey` was computed
|
|
3730
4046
|
// before the mount; content that records its sub-tab or its
|
|
3731
4047
|
// scroll offset WHILE mounting has already written to the
|
|
@@ -4128,7 +4444,7 @@ var TileRenderer = class {
|
|
|
4128
4444
|
_topTabCallbacks(leafId) {
|
|
4129
4445
|
return {
|
|
4130
4446
|
// C33. `DragReorder` calls this through `NotebookTabBar`, which
|
|
4131
|
-
// sets its own
|
|
4447
|
+
// sets its own tab MIME (getTabDragMime()) first and unchanged — so
|
|
4132
4448
|
// an editor pane sharing the page cannot notice that tiles are
|
|
4133
4449
|
// dragging tabs too.
|
|
4134
4450
|
onDragStart: (ev, key, item) => {
|
|
@@ -4199,9 +4515,9 @@ var TileRenderer = class {
|
|
|
4199
4515
|
${tabs.map((t, i) => `
|
|
4200
4516
|
<li class="twm-leaf__tab${i === activeIdx ? " twm-leaf__tab--on" : ""}"
|
|
4201
4517
|
role="tab" data-tab-idx="${i}"
|
|
4202
|
-
title="${
|
|
4518
|
+
title="${_esc7(t.title || t.kind)}"
|
|
4203
4519
|
draggable="true">
|
|
4204
|
-
<span class="twm-leaf__tab-label">${
|
|
4520
|
+
<span class="twm-leaf__tab-label">${_esc7(t.title || t.kind)}</span>
|
|
4205
4521
|
<button type="button" class="twm-leaf__tab-close"
|
|
4206
4522
|
data-action="tab-close" data-tab-idx="${i}"
|
|
4207
4523
|
title="Close this tab"
|
|
@@ -4552,7 +4868,7 @@ var TileRenderer = class {
|
|
|
4552
4868
|
const px = totalPx - SPLITTER_PX * (split.children.length - 1);
|
|
4553
4869
|
if (px <= 0) return;
|
|
4554
4870
|
const deltaFrac = delta / px * totalSize;
|
|
4555
|
-
const minFrac = totalSize *
|
|
4871
|
+
const minFrac = totalSize * MIN_SLOT_FRACTION;
|
|
4556
4872
|
const newSizes = startSizes.slice();
|
|
4557
4873
|
newSizes[slotIdx] = Math.max(minFrac, startSizes[slotIdx] + deltaFrac);
|
|
4558
4874
|
newSizes[slotIdx + 1] = Math.max(minFrac, startSizes[slotIdx + 1] - deltaFrac);
|
|
@@ -4569,11 +4885,20 @@ var TileRenderer = class {
|
|
|
4569
4885
|
});
|
|
4570
4886
|
}
|
|
4571
4887
|
_onMouseUp() {
|
|
4888
|
+
const drag = this._drag;
|
|
4572
4889
|
this._drag = null;
|
|
4573
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
|
+
}
|
|
4574
4899
|
}
|
|
4575
4900
|
};
|
|
4576
|
-
function
|
|
4901
|
+
function _esc7(s) {
|
|
4577
4902
|
return String(s ?? "").replace(/[&<>"']/g, (c) => ({
|
|
4578
4903
|
"&": "&",
|
|
4579
4904
|
"<": "<",
|
|
@@ -4597,15 +4922,41 @@ function _paintContentActions(hostEl, specs) {
|
|
|
4597
4922
|
}
|
|
4598
4923
|
hostEl.hidden = false;
|
|
4599
4924
|
for (const spec of specs) {
|
|
4600
|
-
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;
|
|
4601
4935
|
const btn = document.createElement("button");
|
|
4602
4936
|
btn.type = "button";
|
|
4603
4937
|
btn.className = "twm-leaf__btn";
|
|
4604
4938
|
btn.title = spec.title || "";
|
|
4605
|
-
btn.setAttribute("aria-label", spec.title ||
|
|
4606
|
-
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
|
+
}
|
|
4607
4957
|
btn.addEventListener("click", (e) => {
|
|
4608
4958
|
e.stopPropagation();
|
|
4959
|
+
if (disabled) return;
|
|
4609
4960
|
try {
|
|
4610
4961
|
spec.onClick?.();
|
|
4611
4962
|
} catch (err) {
|
|
@@ -4779,6 +5130,11 @@ var WindowManager = class _WindowManager {
|
|
|
4779
5130
|
this.eventBus = eventBus || null;
|
|
4780
5131
|
this.onChange = onChange || (() => {
|
|
4781
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;
|
|
4782
5138
|
this.floatActiveTab = !!floatActiveTab;
|
|
4783
5139
|
this._rootLeaf = () => {
|
|
4784
5140
|
const kind = this.taxonomy.root;
|
|
@@ -4823,7 +5179,8 @@ var WindowManager = class _WindowManager {
|
|
|
4823
5179
|
onLeafTabAction: (leafId, action, data) => this._leafTabAction(leafId, action, data)
|
|
4824
5180
|
},
|
|
4825
5181
|
onFocusChange: () => this._notifyChange(),
|
|
4826
|
-
onAfterRender: () => this._rehomeContainedWindows()
|
|
5182
|
+
onAfterRender: () => this._rehomeContainedWindows(),
|
|
5183
|
+
onSplitResized: (splitId) => this._onSplitResized(splitId)
|
|
4827
5184
|
});
|
|
4828
5185
|
this._persistTimer = null;
|
|
4829
5186
|
this._windowToLeaf = /* @__PURE__ */ new Map();
|
|
@@ -4876,6 +5233,8 @@ var WindowManager = class _WindowManager {
|
|
|
4876
5233
|
* so other surfaces (palette, top-bar toggles, page shortcuts) can
|
|
4877
5234
|
* subscribe through the existing event system. */
|
|
4878
5235
|
_notifyChange(reason = null) {
|
|
5236
|
+
if (this._quietDepth > 0) return;
|
|
5237
|
+
this._reconcilePanels();
|
|
4879
5238
|
try {
|
|
4880
5239
|
this.onChange(reason);
|
|
4881
5240
|
} catch (err) {
|
|
@@ -4925,10 +5284,12 @@ var WindowManager = class _WindowManager {
|
|
|
4925
5284
|
}
|
|
4926
5285
|
}
|
|
4927
5286
|
}
|
|
4928
|
-
this._canonicalize(d.tree, d);
|
|
4929
5287
|
}
|
|
5288
|
+
const deduped = this._dropDuplicateSingles();
|
|
5289
|
+
for (const d of this.desktops.desktops) this._canonicalize(d.tree, d);
|
|
4930
5290
|
this.renderer.tree = this.desktops.active().tree;
|
|
4931
5291
|
this.renderer.render();
|
|
5292
|
+
if (deduped) this._persist();
|
|
4932
5293
|
this._notifyChange();
|
|
4933
5294
|
}
|
|
4934
5295
|
_persist() {
|
|
@@ -4938,6 +5299,276 @@ var WindowManager = class _WindowManager {
|
|
|
4938
5299
|
await saveDesktops(this.host?.state, this.desktops.serialize());
|
|
4939
5300
|
}, 500);
|
|
4940
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
|
+
}
|
|
4941
5572
|
// ── Content loading ─────────────────────────────────────────────
|
|
4942
5573
|
/** Load content into the primary tile (last-active non-panel leaf).
|
|
4943
5574
|
/** Walk back one step. Bound to Backspace.
|
|
@@ -5131,11 +5762,13 @@ var WindowManager = class _WindowManager {
|
|
|
5131
5762
|
const tree = this._tree();
|
|
5132
5763
|
const prior = tree.popActiveTabHistory?.(id);
|
|
5133
5764
|
if (!prior || !prior.kind) return false;
|
|
5765
|
+
if (this.revealInstance(prior.kind, prior.props || {})) return true;
|
|
5134
5766
|
const curKind = tree.get(id)?.content?.kind;
|
|
5135
5767
|
const curTopNav = curKind ? this.taxonomy.topNavFor(curKind) : null;
|
|
5136
5768
|
const title = prior.title || _tabTitle(prior.kind, prior.props);
|
|
5137
5769
|
if (prior.topNav && curTopNav && prior.topNav !== curTopNav) {
|
|
5138
|
-
|
|
5770
|
+
this._swapToPage(
|
|
5771
|
+
tree,
|
|
5139
5772
|
id,
|
|
5140
5773
|
{ kind: prior.kind, props: prior.props || {}, title },
|
|
5141
5774
|
curTopNav,
|
|
@@ -5245,6 +5878,7 @@ var WindowManager = class _WindowManager {
|
|
|
5245
5878
|
* the leaf. Used by Backspace + breadcrumb segments + anything
|
|
5246
5879
|
* else that should walk WITHIN the tile rather than reset it. */
|
|
5247
5880
|
navigateActiveTab(leafId, kind, props = {}) {
|
|
5881
|
+
if (this.revealInstance(kind, props)) return;
|
|
5248
5882
|
const tree = this._tree();
|
|
5249
5883
|
const leaf = tree.get(leafId);
|
|
5250
5884
|
if (!leaf || leaf.kind !== "leaf") {
|
|
@@ -5298,6 +5932,7 @@ var WindowManager = class _WindowManager {
|
|
|
5298
5932
|
* tab feature shipped), the behavior is identical to the old
|
|
5299
5933
|
* setLeafContent: one tab in, one tab out. */
|
|
5300
5934
|
openInLeaf(leafId, kind, props = {}) {
|
|
5935
|
+
if (this.revealInstance(kind, props)) return;
|
|
5301
5936
|
const tree = this._tree();
|
|
5302
5937
|
const leaf = tree.get(leafId);
|
|
5303
5938
|
if (!leaf || leaf.kind !== "leaf") {
|
|
@@ -5314,6 +5949,7 @@ var WindowManager = class _WindowManager {
|
|
|
5314
5949
|
* previous mount, mounts the new kind into the same body element,
|
|
5315
5950
|
* and updates the window's title. */
|
|
5316
5951
|
openInWindow(winId, kind, props = {}) {
|
|
5952
|
+
if (this.revealInstance(kind, props)) return;
|
|
5317
5953
|
const rec = this._windowToLeaf.get(winId);
|
|
5318
5954
|
if (!rec) {
|
|
5319
5955
|
this.openInPrimary(kind, props);
|
|
@@ -5352,6 +5988,7 @@ var WindowManager = class _WindowManager {
|
|
|
5352
5988
|
* If only panel tiles exist (or the tree is empty), spawn a new
|
|
5353
5989
|
* content leaf and canonicalize so the panels wrap it. */
|
|
5354
5990
|
openInPrimary(kind, props = {}) {
|
|
5991
|
+
if (this.revealInstance(kind, props)) return;
|
|
5355
5992
|
const tree = this._tree();
|
|
5356
5993
|
let leafId = tree.primaryLeafId();
|
|
5357
5994
|
let spawned = false;
|
|
@@ -5364,7 +6001,8 @@ var WindowManager = class _WindowManager {
|
|
|
5364
6001
|
const currentTopNav = leaf?.content?.kind ? this.taxonomy.topNavFor(leaf.content.kind) || null : null;
|
|
5365
6002
|
const targetTopNav = this.taxonomy.topNavFor(kind) || kind;
|
|
5366
6003
|
const title = _tabTitle(kind, props);
|
|
5367
|
-
|
|
6004
|
+
this._swapToPage(
|
|
6005
|
+
tree,
|
|
5368
6006
|
leafId,
|
|
5369
6007
|
{ kind, props, title },
|
|
5370
6008
|
currentTopNav,
|
|
@@ -5410,6 +6048,7 @@ var WindowManager = class _WindowManager {
|
|
|
5410
6048
|
* pane in a split never dismounts the original). Returns the new
|
|
5411
6049
|
* leaf id, or null if the leaf can't be split. */
|
|
5412
6050
|
splitLeafWith(leafId, dir, kind, props = {}, title = "") {
|
|
6051
|
+
if (this.revealInstance(kind, props)) return null;
|
|
5413
6052
|
const tree = this._tree();
|
|
5414
6053
|
leafId = this._contentLeafId(leafId);
|
|
5415
6054
|
const src = leafId ? tree.get(leafId) : null;
|
|
@@ -5445,17 +6084,18 @@ var WindowManager = class _WindowManager {
|
|
|
5445
6084
|
if (!kind || kind === PLACEHOLDER_KIND || kind.startsWith("panel:")) return null;
|
|
5446
6085
|
return { leafId: id, kind, props: leaf.content.props || {}, title: leaf.title };
|
|
5447
6086
|
}
|
|
5448
|
-
/** 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). */
|
|
5449
6089
|
openFocusedInTab() {
|
|
5450
6090
|
const c = this._focusedContent();
|
|
5451
|
-
if (!c) return;
|
|
6091
|
+
if (!c || this.isSingleKind(c.kind)) return;
|
|
5452
6092
|
this.openInTabFromContext({ leafId: c.leafId }, c.kind, c.props);
|
|
5453
6093
|
}
|
|
5454
6094
|
/** Alt+N — open the focused tile's content in a fresh managed window.
|
|
5455
6095
|
* Unlike Alt+F (promote) this DUPLICATES: the source tile stays put. */
|
|
5456
6096
|
openFocusedInWindow() {
|
|
5457
6097
|
const c = this._focusedContent();
|
|
5458
|
-
if (!c) return;
|
|
6098
|
+
if (!c || this.isSingleKind(c.kind)) return;
|
|
5459
6099
|
this._navigateWindow(c.kind, c.props);
|
|
5460
6100
|
}
|
|
5461
6101
|
/** Alt+Shift+H / Alt+Shift+V — split the focused tile and open a fresh
|
|
@@ -5467,6 +6107,7 @@ var WindowManager = class _WindowManager {
|
|
|
5467
6107
|
this.split(dir);
|
|
5468
6108
|
return;
|
|
5469
6109
|
}
|
|
6110
|
+
if (this.isSingleKind(c.kind)) return;
|
|
5470
6111
|
this.splitLeafWith(c.leafId, dir, c.kind, c.props, c.title);
|
|
5471
6112
|
}
|
|
5472
6113
|
closeFocused() {
|
|
@@ -7227,15 +7868,94 @@ var WindowManager = class _WindowManager {
|
|
|
7227
7868
|
* edge (a recursive split) or closes that leaf. */
|
|
7228
7869
|
isPanelOpen(side) {
|
|
7229
7870
|
const d = this.desktops.active();
|
|
7230
|
-
return !!d.panels[side];
|
|
7871
|
+
return !!d.panels[side] && this.isPanelAvailable(side);
|
|
7231
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. */
|
|
7232
7876
|
togglePanel(side) {
|
|
7877
|
+
if (!this.isPanelAvailable(side)) return false;
|
|
7233
7878
|
const d = this.desktops.active();
|
|
7234
7879
|
d.panels[side] = !d.panels[side];
|
|
7235
7880
|
this._canonicalize(this._tree(), d);
|
|
7236
7881
|
this.renderer.render();
|
|
7237
7882
|
this._persist();
|
|
7238
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
|
+
}
|
|
7239
7959
|
}
|
|
7240
7960
|
/** Rebuild the tree so panel:* leaves sit in the canonical positions:
|
|
7241
7961
|
*
|
|
@@ -7249,13 +7969,13 @@ var WindowManager = class _WindowManager {
|
|
|
7249
7969
|
* Existing panel leaves are detached + re-attached (same id), so the
|
|
7250
7970
|
* renderer cache keeps the mounted content (nav filter state, etc.). */
|
|
7251
7971
|
_canonicalize(tree, desktop) {
|
|
7252
|
-
const
|
|
7253
|
-
for (const
|
|
7254
|
-
|
|
7255
|
-
if (kind === "panel:left") panel.left = leaf;
|
|
7256
|
-
else if (kind === "panel:right") panel.right = leaf;
|
|
7257
|
-
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;
|
|
7258
7975
|
}
|
|
7976
|
+
const panel = _panelLeaves(tree);
|
|
7977
|
+
this._recordPanelShares(tree, desktop);
|
|
7978
|
+
const share = desktop.panelShares;
|
|
7259
7979
|
const detach = (leaf) => {
|
|
7260
7980
|
if (!leaf) return;
|
|
7261
7981
|
const pid = leaf.parentId;
|
|
@@ -7279,7 +7999,7 @@ var WindowManager = class _WindowManager {
|
|
|
7279
7999
|
detach(panel.bottom);
|
|
7280
8000
|
let seededSide = null;
|
|
7281
8001
|
if (!tree.rootId) {
|
|
7282
|
-
seededSide =
|
|
8002
|
+
seededSide = open.left ? "left" : open.bottom ? "bottom" : open.right ? "right" : null;
|
|
7283
8003
|
if (seededSide) {
|
|
7284
8004
|
const seed = panel[seededSide] || makeLeaf({
|
|
7285
8005
|
content: { kind: `panel:${seededSide}`, props: {} },
|
|
@@ -7294,6 +8014,15 @@ var WindowManager = class _WindowManager {
|
|
|
7294
8014
|
tree.rootId = seed.id;
|
|
7295
8015
|
}
|
|
7296
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
|
+
};
|
|
7297
8026
|
const wrap = (panelLeaf, side) => {
|
|
7298
8027
|
if (panelLeaf && !tree.nodes.has(panelLeaf.id)) {
|
|
7299
8028
|
tree.nodes.set(panelLeaf.id, panelLeaf);
|
|
@@ -7309,7 +8038,8 @@ var WindowManager = class _WindowManager {
|
|
|
7309
8038
|
const before = side === "left";
|
|
7310
8039
|
const dir = side === "bottom" ? "v" : "h";
|
|
7311
8040
|
const children = before ? [leaf.id, tree.rootId] : [tree.rootId, leaf.id];
|
|
7312
|
-
const
|
|
8041
|
+
const kept = slotShare(side);
|
|
8042
|
+
const sizes = kept != null ? before ? [kept, 1 - kept] : [1 - kept, kept] : before ? [1, 4] : [4, 1];
|
|
7313
8043
|
const split = {
|
|
7314
8044
|
id: `split-panel-${side}-${Math.random().toString(36).slice(2, 7)}`,
|
|
7315
8045
|
kind: "split",
|
|
@@ -7324,9 +8054,9 @@ var WindowManager = class _WindowManager {
|
|
|
7324
8054
|
leaf.parentId = split.id;
|
|
7325
8055
|
tree.rootId = split.id;
|
|
7326
8056
|
};
|
|
7327
|
-
if (
|
|
7328
|
-
if (
|
|
7329
|
-
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");
|
|
7330
8060
|
const reachable = /* @__PURE__ */ new Set();
|
|
7331
8061
|
const walk = (id) => {
|
|
7332
8062
|
if (!id || reachable.has(id)) return;
|
|
@@ -7339,6 +8069,68 @@ var WindowManager = class _WindowManager {
|
|
|
7339
8069
|
if (!reachable.has(id)) tree.nodes.delete(id);
|
|
7340
8070
|
}
|
|
7341
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
|
+
}
|
|
7342
8134
|
_findPanelLeaf(side) {
|
|
7343
8135
|
const kind = `panel:${side}`;
|
|
7344
8136
|
for (const leaf of this._tree().leaves()) {
|
|
@@ -7684,6 +8476,7 @@ var WindowManager = class _WindowManager {
|
|
|
7684
8476
|
/** Spawn a fresh ManagedWindow with the requested content. No
|
|
7685
8477
|
* source leaf — closing the window just disposes the content. */
|
|
7686
8478
|
_navigateWindow(kind, props) {
|
|
8479
|
+
if (this.revealInstance(kind, props)) return;
|
|
7687
8480
|
const winId = `twm-mw-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 5)}`;
|
|
7688
8481
|
const contentEl = document.createElement("div");
|
|
7689
8482
|
contentEl.className = "twm-window-content";
|
|
@@ -7801,6 +8594,7 @@ var WindowManager = class _WindowManager {
|
|
|
7801
8594
|
* routing) but uses `appendLeafTab` so the existing content
|
|
7802
8595
|
* stays in place as a tab. */
|
|
7803
8596
|
openInTabFromContext(ctx, kind, props = {}, transient = false, background = false) {
|
|
8597
|
+
if (this.revealInstance(kind, props)) return;
|
|
7804
8598
|
if (ctx?.windowId && this._windowToLeaf.has(ctx.windowId)) {
|
|
7805
8599
|
this.openInWindow(ctx.windowId, kind, props);
|
|
7806
8600
|
return;
|
|
@@ -7833,6 +8627,7 @@ var WindowManager = class _WindowManager {
|
|
|
7833
8627
|
* "Add row" button, which passes no ctx) reliably lands as a sibling
|
|
7834
8628
|
* tab in the main tile rather than swapping its content. */
|
|
7835
8629
|
openInTabInPrimary(kind, props = {}, transient = false, background = false) {
|
|
8630
|
+
if (this.revealInstance(kind, props)) return;
|
|
7836
8631
|
const tree = this._tree();
|
|
7837
8632
|
const leafId = tree.primaryLeafId();
|
|
7838
8633
|
if (!leafId) {
|
|
@@ -7851,11 +8646,38 @@ var WindowManager = class _WindowManager {
|
|
|
7851
8646
|
this._notifyChange("tab-open");
|
|
7852
8647
|
}
|
|
7853
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
|
+
}
|
|
7854
8672
|
function _tabTitle(kind, props) {
|
|
7855
8673
|
if (props && typeof props.label === "string" && props.label) return props.label;
|
|
7856
8674
|
if (props && (typeof props.id === "string" || typeof props.id === "number") && String(props.id)) return String(props.id);
|
|
7857
8675
|
return kind || "";
|
|
7858
8676
|
}
|
|
8677
|
+
function _windowTabSpecs(rec) {
|
|
8678
|
+
if (Array.isArray(rec?.tabs) && rec.tabs.length) return rec.tabs;
|
|
8679
|
+
return rec?.original ? [rec.original] : [];
|
|
8680
|
+
}
|
|
7859
8681
|
function _leafTabSpecs(leaf) {
|
|
7860
8682
|
const tabs = Array.isArray(leaf.tabs) ? leaf.tabs : [];
|
|
7861
8683
|
if (tabs.length) {
|
|
@@ -8116,17 +8938,17 @@ function openTileTabMenu({
|
|
|
8116
8938
|
return `
|
|
8117
8939
|
<li class="twm-tile-tabmenu__item${idx === _cursor ? " twm-tile-tabmenu__item--cursor" : ""}"
|
|
8118
8940
|
data-flat-idx="${idx}">
|
|
8119
|
-
<span class="twm-tile-tabmenu__item-label">${
|
|
8120
|
-
${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>` : ""}
|
|
8121
8943
|
</li>
|
|
8122
8944
|
`;
|
|
8123
8945
|
}).join("");
|
|
8124
8946
|
return `
|
|
8125
8947
|
<section class="twm-tile-tabmenu__section"
|
|
8126
|
-
data-section="${
|
|
8948
|
+
data-section="${_esc8(s.src.navKind)}">
|
|
8127
8949
|
<header class="twm-tile-tabmenu__section-head">
|
|
8128
|
-
<span class="material-symbols-outlined">${
|
|
8129
|
-
<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>
|
|
8130
8952
|
<span class="twm-tile-tabmenu__section-count">${s.total}</span>
|
|
8131
8953
|
</header>
|
|
8132
8954
|
${s.view.length === 0 ? `
|
|
@@ -8135,11 +8957,11 @@ function openTileTabMenu({
|
|
|
8135
8957
|
${!_query && s.pages > 1 ? `
|
|
8136
8958
|
<div class="twm-tile-tabmenu__pager">
|
|
8137
8959
|
<button type="button" data-action="prev"
|
|
8138
|
-
data-section="${
|
|
8960
|
+
data-section="${_esc8(s.src.navKind)}"
|
|
8139
8961
|
${s.page === 0 ? "disabled" : ""}><</button>
|
|
8140
8962
|
<span>${s.page + 1} / ${s.pages}</span>
|
|
8141
8963
|
<button type="button" data-action="next"
|
|
8142
|
-
data-section="${
|
|
8964
|
+
data-section="${_esc8(s.src.navKind)}"
|
|
8143
8965
|
${s.page >= s.pages - 1 ? "disabled" : ""}>></button>
|
|
8144
8966
|
</div>
|
|
8145
8967
|
` : ""}
|
|
@@ -8203,7 +9025,7 @@ function openTileTabSwitcher({ x, y, tabs, activeIdx = 0, onPick }) {
|
|
|
8203
9025
|
<li class="twm-tile-tabswitch__item${i === activeIdx ? " twm-tile-tabswitch__item--on" : ""}"
|
|
8204
9026
|
data-idx="${i}" role="option" aria-selected="${i === activeIdx}">
|
|
8205
9027
|
<span class="material-symbols-outlined twm-tile-tabswitch__check">${i === activeIdx ? "check" : ""}</span>
|
|
8206
|
-
<span class="twm-tile-tabswitch__label">${
|
|
9028
|
+
<span class="twm-tile-tabswitch__label">${_esc8(t.title || t.kind || "Tab")}</span>
|
|
8207
9029
|
</li>
|
|
8208
9030
|
`).join("");
|
|
8209
9031
|
overlay.innerHTML = `
|
|
@@ -8304,7 +9126,7 @@ function _matchesShaped(shaped, query) {
|
|
|
8304
9126
|
const hint = String(shaped.hint ?? "").toLowerCase();
|
|
8305
9127
|
return id.includes(query) || label.includes(query) || hint.includes(query);
|
|
8306
9128
|
}
|
|
8307
|
-
function
|
|
9129
|
+
function _esc8(s) {
|
|
8308
9130
|
return String(s ?? "").replace(/[&<>"']/g, (c) => ({
|
|
8309
9131
|
"&": "&",
|
|
8310
9132
|
"<": "<",
|
|
@@ -8442,17 +9264,34 @@ async function createShell({
|
|
|
8442
9264
|
tabLayout = null,
|
|
8443
9265
|
newTabButton = true,
|
|
8444
9266
|
desktopBar = {},
|
|
9267
|
+
menuBar: menuBarCfg = null,
|
|
8445
9268
|
chrome = {},
|
|
8446
9269
|
// An embedder that moved its sections out of the top bar — into an icon
|
|
8447
9270
|
// rail, say — passes the selector its own buttons match, and F1..F8 keep
|
|
8448
9271
|
// working. Omitted, the default top-bar selector applies.
|
|
8449
|
-
navSelector = null
|
|
9272
|
+
navSelector = null,
|
|
9273
|
+
keymap = null
|
|
8450
9274
|
} = {}) {
|
|
8451
9275
|
if (!root || typeof root.appendChild !== "function") {
|
|
8452
9276
|
throw new TypeError("createShell: `root` must be an element");
|
|
8453
9277
|
}
|
|
8454
9278
|
if (!taxonomy) throw new Error("createShell: a taxonomy is required");
|
|
8455
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
|
+
}
|
|
8456
9295
|
const log = logger ?? { info() {
|
|
8457
9296
|
}, warn() {
|
|
8458
9297
|
}, error() {
|
|
@@ -8503,21 +9342,28 @@ async function createShell({
|
|
|
8503
9342
|
},
|
|
8504
9343
|
onChange: () => syncChrome()
|
|
8505
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;
|
|
8506
9349
|
const palette = createCommandPalette({
|
|
8507
9350
|
wm,
|
|
8508
9351
|
api,
|
|
8509
9352
|
taxonomy,
|
|
8510
9353
|
catalog: entities,
|
|
8511
|
-
|
|
9354
|
+
// What *Keyboard shortcuts* lists is what the keys do in THIS shell.
|
|
9355
|
+
shortcuts: keymapShortcuts(bindings),
|
|
9356
|
+
...paletteCfg,
|
|
9357
|
+
commands
|
|
8512
9358
|
});
|
|
8513
|
-
const disposeKeymap = installKeymap({ wm, palette, navSelector });
|
|
9359
|
+
const disposeKeymap = installKeymap({ wm, palette, navSelector, bindings, logger });
|
|
8514
9360
|
const paletteBtn = mountPaletteButton(chrome.paletteButton, palette);
|
|
8515
9361
|
topNavEl = mountTopNav(chrome.topNav, taxonomy, wm);
|
|
8516
9362
|
desktopsEl = mountDesktopBar(chrome.desktops, wm, desktopBar || {});
|
|
8517
9363
|
const zoom = mountZoomControl(chrome.zoom, { root, host });
|
|
8518
9364
|
if (zoom) await zoom.ready;
|
|
8519
9365
|
toggles = bindPanelToggles(chrome.panelToggles, wm);
|
|
8520
|
-
eventBus?.on?.("wm:changed", syncChrome);
|
|
9366
|
+
const chromeSubscription = eventBus?.on?.("wm:changed", syncChrome);
|
|
8521
9367
|
await wm.load();
|
|
8522
9368
|
syncChrome();
|
|
8523
9369
|
log.info?.("shell mounted");
|
|
@@ -8535,7 +9381,9 @@ async function createShell({
|
|
|
8535
9381
|
panelToggles: toggles,
|
|
8536
9382
|
// `get`/`set` so an embedder can drive the zoom from its own settings
|
|
8537
9383
|
// pane, or read it, without reaching into the control's DOM.
|
|
8538
|
-
zoom
|
|
9384
|
+
zoom,
|
|
9385
|
+
// STORY-0030. `setEnabled` / `setChecked` / `open` / `commands`.
|
|
9386
|
+
menuBar
|
|
8539
9387
|
}),
|
|
8540
9388
|
// A shell that can be built can be built TWICE — an embedder that
|
|
8541
9389
|
// rebuilds on a context change (a different project, a different
|
|
@@ -8544,7 +9392,7 @@ async function createShell({
|
|
|
8544
9392
|
// with the first one's keyboard.
|
|
8545
9393
|
dispose: () => {
|
|
8546
9394
|
try {
|
|
8547
|
-
|
|
9395
|
+
chromeSubscription?.dispose?.();
|
|
8548
9396
|
} catch {
|
|
8549
9397
|
}
|
|
8550
9398
|
try {
|
|
@@ -8568,6 +9416,10 @@ async function createShell({
|
|
|
8568
9416
|
desktopsEl?.remove();
|
|
8569
9417
|
} catch {
|
|
8570
9418
|
}
|
|
9419
|
+
try {
|
|
9420
|
+
menuBar?.dispose();
|
|
9421
|
+
} catch {
|
|
9422
|
+
}
|
|
8571
9423
|
}
|
|
8572
9424
|
});
|
|
8573
9425
|
}
|
|
@@ -8619,6 +9471,7 @@ function mountPaletteButton(hostEl, palette) {
|
|
|
8619
9471
|
hostEl.insertBefore(btn, hostEl.firstChild);
|
|
8620
9472
|
return btn;
|
|
8621
9473
|
}
|
|
9474
|
+
var OWN_TOOLTIPS = /* @__PURE__ */ new WeakMap();
|
|
8622
9475
|
function bindPanelToggles(map, wm) {
|
|
8623
9476
|
const fresh = {};
|
|
8624
9477
|
for (const side of ["left", "right", "bottom"]) {
|
|
@@ -8631,6 +9484,10 @@ function bindPanelToggles(map, wm) {
|
|
|
8631
9484
|
el.removeAttribute("aria-pressed");
|
|
8632
9485
|
btn.replaceWith(el);
|
|
8633
9486
|
el.addEventListener("click", () => wm.togglePanel(side));
|
|
9487
|
+
OWN_TOOLTIPS.set(el, {
|
|
9488
|
+
tooltip: el.getAttribute("data-tooltip"),
|
|
9489
|
+
title: el.getAttribute("title")
|
|
9490
|
+
});
|
|
8634
9491
|
fresh[side] = el;
|
|
8635
9492
|
}
|
|
8636
9493
|
return fresh;
|
|
@@ -8640,6 +9497,21 @@ function syncPanelToggles(map, wm) {
|
|
|
8640
9497
|
const btn = map?.[side];
|
|
8641
9498
|
if (!btn) continue;
|
|
8642
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
|
+
}
|
|
8643
9515
|
}
|
|
8644
9516
|
}
|
|
8645
9517
|
function mountDesktopBar(hostEl, wm, { switchOnAdd = false } = {}) {
|
|
@@ -8795,6 +9667,8 @@ function _tileContextMenu(wm, leafId, x, y) {
|
|
|
8795
9667
|
const isPanel = String(leaf.content?.kind || "").startsWith("panel:");
|
|
8796
9668
|
tree.focus(leafId);
|
|
8797
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;
|
|
8798
9672
|
const items = [
|
|
8799
9673
|
{ label: "Split horizontally", icon: "splitscreen_vertical_add", action: "split-h" },
|
|
8800
9674
|
{ label: "Split vertically", icon: "splitscreen_add", action: "split-v" },
|
|
@@ -8803,7 +9677,8 @@ function _tileContextMenu(wm, leafId, x, y) {
|
|
|
8803
9677
|
label: "Open in new tab",
|
|
8804
9678
|
icon: "tab",
|
|
8805
9679
|
action: "open-tab",
|
|
8806
|
-
disabled: isPanel || !leaf.content
|
|
9680
|
+
disabled: isPanel || !leaf.content || single,
|
|
9681
|
+
title: singleTitle
|
|
8807
9682
|
},
|
|
8808
9683
|
// TWO DIFFERENT GLYPHS FOR TWO DIFFERENT DESTINATIONS. `web_asset` is a
|
|
8809
9684
|
// window INSIDE the application — the same glyph `ManagedWindow` uses
|
|
@@ -8816,7 +9691,8 @@ function _tileContextMenu(wm, leafId, x, y) {
|
|
|
8816
9691
|
label: "Open a copy in a window",
|
|
8817
9692
|
icon: "web_asset",
|
|
8818
9693
|
action: "open-window",
|
|
8819
|
-
disabled: isPanel || !leaf.content
|
|
9694
|
+
disabled: isPanel || !leaf.content || single,
|
|
9695
|
+
title: singleTitle
|
|
8820
9696
|
},
|
|
8821
9697
|
// C20, THE OTHER HALF — and it was missing while the `close` half
|
|
8822
9698
|
// below carried a paragraph explaining why it could not be.
|
|
@@ -8882,6 +9758,8 @@ function _tileContextMenu(wm, leafId, x, y) {
|
|
|
8882
9758
|
export {
|
|
8883
9759
|
BADGE_LEVELS,
|
|
8884
9760
|
DesktopManager,
|
|
9761
|
+
KEYBOARD_SHORTCUTS_TOPIC_ID,
|
|
9762
|
+
KEYMAP_SHORTCUTS,
|
|
8885
9763
|
PLACEHOLDER_KIND,
|
|
8886
9764
|
PanelKeyRouter,
|
|
8887
9765
|
TILE_TAB_MIME,
|
|
@@ -8910,6 +9788,8 @@ export {
|
|
|
8910
9788
|
getPanelKeyRouter,
|
|
8911
9789
|
installKeymap,
|
|
8912
9790
|
installPanelKeyRouter,
|
|
9791
|
+
keyboardShortcutsTopic,
|
|
9792
|
+
keymapShortcuts,
|
|
8913
9793
|
loadDesktops,
|
|
8914
9794
|
makeLeaf,
|
|
8915
9795
|
makeLoadingOverlay,
|