flexdesk 0.16.1 → 0.17.1
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 +112 -0
- package/LICENSE +0 -0
- package/README.md +2435 -42
- package/THIRD_PARTY_LICENSES.md +0 -0
- package/canvas.js +18 -0
- package/charts.js +21 -0
- package/core.js +14 -0
- package/css/base.css +1834 -75
- package/css/flexdesk.css +3165 -159
- 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 +707 -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-FBGP5KN5.js → chunk-LWV23YVE.js} +5073 -532
- package/dist/chunk-LWV23YVE.js.map +7 -0
- package/dist/chunk-N7HC3ADV.js +30 -0
- package/dist/chunk-N7HC3ADV.js.map +7 -0
- package/dist/{chunk-RQM5Z2MU.js → chunk-RMTRWWEO.js} +633 -311
- package/dist/chunk-RMTRWWEO.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 +3165 -159
- 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 +3290 -2124
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +1227 -347
- package/dist/wm.js.map +4 -4
- package/editor.js +20 -0
- package/flow.js +21 -0
- package/host.js +14 -0
- package/package.json +12 -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 +293 -0
- package/src/ui/components/cell_editors.js +480 -0
- package/src/ui/components/cell_grid.js +761 -0
- package/src/ui/components/cell_paste.js +243 -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 +696 -71
- 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 +361 -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/tsv.js +74 -0
- 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/tiles.js +27 -0
- package/widgets.js +69 -0
- package/wm.js +28 -0
- 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
|
@@ -44,8 +44,28 @@
|
|
|
44
44
|
* embedder's business, not the shell's.
|
|
45
45
|
* labelOf? `(props) => string` — display label for ONE instance.
|
|
46
46
|
* Defaults to `props.label`, then `props.id`.
|
|
47
|
+
* panels? `{ left?, right?, bottom? }` — which panels this kind
|
|
48
|
+
* offers while it is the primary tile. Each side is `true`,
|
|
49
|
+
* `false` or `{ reason }` (see panel_availability.js); a side
|
|
50
|
+
* not named is offered. A kind that names no side inherits
|
|
51
|
+
* that side from its top-nav kind, so declaring it on a page
|
|
52
|
+
* covers every entity opened under it. Withheld panels are
|
|
53
|
+
* not inserted and their toggles are disabled, showing
|
|
54
|
+
* `reason`.
|
|
55
|
+
*
|
|
56
|
+
* single? `true`, or the name of a prop. The kind exists ONCE: content
|
|
57
|
+
* that holds heavy app-wide state and must not be mounted
|
|
58
|
+
* twice. With `true` there is one instance of the kind; with a
|
|
59
|
+
* prop name there is one per value of that prop (`'id'`: one
|
|
60
|
+
* tile per record). The window manager makes every open path
|
|
61
|
+
* reveal the instance that exists, refuses the copy verbs with
|
|
62
|
+
* a reason, and keeps the first of any duplicates a restored
|
|
63
|
+
* layout holds. The root kind cannot be single: every new
|
|
64
|
+
* pane, tab and desktop is seeded with it.
|
|
47
65
|
*/
|
|
48
66
|
|
|
67
|
+
import { PANEL_OFFERED, PANEL_SIDES, readPanelOffer } from './panel_availability.js';
|
|
68
|
+
|
|
49
69
|
/** Build a taxonomy from an embedder's kind map. Validated eagerly and
|
|
50
70
|
* frozen — a typo in a `topNav` pointer is a boot error, not a chrome
|
|
51
71
|
* surface that silently renders nothing six clicks later. */
|
|
@@ -68,6 +88,27 @@ export function createTaxonomy({ kinds, root } = {}) {
|
|
|
68
88
|
throw new Error(
|
|
69
89
|
`createTaxonomy: '${kind}' is both a top-nav kind and owned by one`);
|
|
70
90
|
}
|
|
91
|
+
if (def.panels !== undefined) {
|
|
92
|
+
if (!def.panels || typeof def.panels !== 'object' || Array.isArray(def.panels)) {
|
|
93
|
+
throw new TypeError(`createTaxonomy: '${kind}'.panels must be an object`);
|
|
94
|
+
}
|
|
95
|
+
for (const [side, offer] of Object.entries(def.panels)) {
|
|
96
|
+
if (!PANEL_SIDES.includes(side)) {
|
|
97
|
+
throw new Error(`createTaxonomy: '${kind}'.panels names unknown side '${side}'`);
|
|
98
|
+
}
|
|
99
|
+
readPanelOffer(offer, `createTaxonomy: '${kind}'.panels.${side}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (def.single != null && def.single !== false && def.single !== true
|
|
103
|
+
&& !(typeof def.single === 'string' && def.single)) {
|
|
104
|
+
throw new TypeError(
|
|
105
|
+
`createTaxonomy: '${kind}'.single must be true or a prop name`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (kinds[root].single) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`createTaxonomy: the root kind '${root}' cannot be single — `
|
|
111
|
+
+ 'every new pane, tab and desktop is seeded with it');
|
|
71
112
|
}
|
|
72
113
|
const _kinds = Object.freeze({ ...kinds });
|
|
73
114
|
|
|
@@ -143,6 +184,17 @@ export function createTaxonomy({ kinds, root } = {}) {
|
|
|
143
184
|
return Array.isArray(s) ? s : [];
|
|
144
185
|
};
|
|
145
186
|
|
|
187
|
+
/** Whether `kind` offers the panel on `side`: `{ available, reason }`.
|
|
188
|
+
* The kind's own declaration for that side, else its top-nav kind's,
|
|
189
|
+
* else offered. Unknown and absent kinds offer every panel. */
|
|
190
|
+
const panelOffer = (kind, side) => {
|
|
191
|
+
for (const k of [kind, _kinds[kind]?.topNav]) {
|
|
192
|
+
const offer = k ? _kinds[k]?.panels?.[side] : undefined;
|
|
193
|
+
if (offer !== undefined) return readPanelOffer(offer, `taxonomy '${k}'.panels.${side}`);
|
|
194
|
+
}
|
|
195
|
+
return PANEL_OFFERED;
|
|
196
|
+
};
|
|
197
|
+
|
|
146
198
|
/** Top-nav entries in display order:
|
|
147
199
|
* { kind, label (short), longLabel, icon }
|
|
148
200
|
* `label` is the chip text; `longLabel` is what the palette shows. */
|
|
@@ -157,6 +209,28 @@ export function createTaxonomy({ kinds, root } = {}) {
|
|
|
157
209
|
icon: m.icon,
|
|
158
210
|
}));
|
|
159
211
|
|
|
212
|
+
/** Whether `kind` exists once (KindDef `single`). */
|
|
213
|
+
const isSingle = (kind) => {
|
|
214
|
+
const s = _kinds[kind]?.single;
|
|
215
|
+
return s === true || (typeof s === 'string' && s !== '');
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/** The identity of one instance of a single kind: two tab specs with the
|
|
219
|
+
* same key are the same instance. `null` for a kind that is not single,
|
|
220
|
+
* which is how callers ask both questions at once. Keyed by the kind and,
|
|
221
|
+
* for `single: '<prop>'`, that prop's value as a string. */
|
|
222
|
+
const instanceKey = (kind, props) => {
|
|
223
|
+
if (!isSingle(kind)) return null;
|
|
224
|
+
const by = _kinds[kind].single;
|
|
225
|
+
if (by === true) return String(kind);
|
|
226
|
+
const v = props?.[by];
|
|
227
|
+
return `${kind}\u0000${v == null ? '' : String(v)}`;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/** The sentence a refused copy verb shows for a single kind. */
|
|
231
|
+
const singleReason = (kind) =>
|
|
232
|
+
`${_kinds[kind]?.label || kind} is open once: use the one that is open`;
|
|
233
|
+
|
|
160
234
|
return Object.freeze({
|
|
161
235
|
root,
|
|
162
236
|
kinds: _kinds,
|
|
@@ -167,6 +241,10 @@ export function createTaxonomy({ kinds, root } = {}) {
|
|
|
167
241
|
parentOf,
|
|
168
242
|
labelOf,
|
|
169
243
|
sourcesFor,
|
|
244
|
+
panelOffer,
|
|
170
245
|
topNavEntries,
|
|
246
|
+
isSingle,
|
|
247
|
+
instanceKey,
|
|
248
|
+
singleReason,
|
|
171
249
|
});
|
|
172
250
|
}
|
|
File without changes
|
|
File without changes
|
package/src/tiling/nav_panel.js
CHANGED
|
@@ -30,6 +30,30 @@
|
|
|
30
30
|
* `activate()` runs on every selection, INCLUDING the first — a pane that
|
|
31
31
|
* has to rebuild on each visit (a git status list, say) does its work there.
|
|
32
32
|
* The filter query is remembered per mode.
|
|
33
|
+
*
|
|
34
|
+
* A mode may also carry `filter: false`: a pane with controls of its own.
|
|
35
|
+
* While it is the active mode the shared filter box is hidden and its pane's
|
|
36
|
+
* `filter()` is not called; the next mode with a filter shows its own saved
|
|
37
|
+
* text again.
|
|
38
|
+
*
|
|
39
|
+
* Options beyond `modes`:
|
|
40
|
+
*
|
|
41
|
+
* showModeStrip (true) false renders no strip: the embedder switches
|
|
42
|
+
* modes through `select()` instead.
|
|
43
|
+
* showFilter (true) false renders no filter box at all.
|
|
44
|
+
* initialMode (first) the mode shown at mount; an unknown id falls back
|
|
45
|
+
* to the first mode.
|
|
46
|
+
*
|
|
47
|
+
* The handle:
|
|
48
|
+
*
|
|
49
|
+
* select(modeId) -> Promise the strip click's own path: mounts the mode
|
|
50
|
+
* on first use, runs its `activate()` and
|
|
51
|
+
* restores its filter; resolves once
|
|
52
|
+
* `activate()` has run. An unknown id is
|
|
53
|
+
* ignored (a resolved promise).
|
|
54
|
+
* active() -> modeId the mode currently shown.
|
|
55
|
+
* refresh() -> Promise re-runs the active mode's `activate()`.
|
|
56
|
+
* destroy()
|
|
33
57
|
*/
|
|
34
58
|
|
|
35
59
|
export function mountNavPanel(hostEl, {
|
|
@@ -37,39 +61,54 @@ export function mountNavPanel(hostEl, {
|
|
|
37
61
|
wm = null,
|
|
38
62
|
eventBus = null,
|
|
39
63
|
filterPlaceholder = 'Filter…',
|
|
64
|
+
showModeStrip = true,
|
|
65
|
+
showFilter = true,
|
|
66
|
+
initialMode = null,
|
|
40
67
|
} = {}) {
|
|
41
68
|
if (!Array.isArray(modes) || modes.length === 0) {
|
|
42
69
|
throw new Error('mountNavPanel: at least one mode is required');
|
|
43
70
|
}
|
|
44
71
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
<div class="twm-nav__modes" data-role="modes"></div
|
|
72
|
+
const stripHtml = showModeStrip
|
|
73
|
+
? `
|
|
74
|
+
<div class="twm-nav__modes" data-role="modes"></div>`
|
|
75
|
+
: '';
|
|
76
|
+
const filterHtml = showFilter
|
|
77
|
+
? `
|
|
48
78
|
<div class="twm-sidebar-controls twm-nav__filter">
|
|
49
79
|
<span class="material-symbols-outlined twm-sidebar-search-icon">search</span>
|
|
50
80
|
<input class="twm-data-page__search twm-nav__filter-input"
|
|
51
81
|
type="search" placeholder="${_esc(filterPlaceholder)}"
|
|
52
82
|
autocomplete="off" />
|
|
53
|
-
</div
|
|
83
|
+
</div>`
|
|
84
|
+
: '';
|
|
85
|
+
hostEl.classList.add('twm-nav');
|
|
86
|
+
hostEl.innerHTML = `${stripHtml}${filterHtml}
|
|
54
87
|
<div class="twm-nav__body" data-role="body"></div>
|
|
55
88
|
`;
|
|
56
89
|
const modesEl = hostEl.querySelector('[data-role="modes"]');
|
|
57
90
|
const bodyEl = hostEl.querySelector('[data-role="body"]');
|
|
91
|
+
const filterBoxEl = hostEl.querySelector('.twm-nav__filter');
|
|
58
92
|
const filterEl = hostEl.querySelector('.twm-nav__filter-input');
|
|
59
93
|
|
|
60
|
-
|
|
94
|
+
if (modesEl) {
|
|
95
|
+
modesEl.innerHTML = modes.map((m) => `
|
|
61
96
|
<button class="twm-nav__mode twm-has-tooltip" data-mode="${_esc(m.id)}"
|
|
62
97
|
data-tooltip="${_esc(m.label)}">
|
|
63
98
|
<span class="material-symbols-outlined">${_esc(m.icon)}</span>
|
|
64
99
|
<span>${_esc(m.label)}</span>
|
|
65
100
|
</button>
|
|
66
101
|
`).join('');
|
|
102
|
+
}
|
|
67
103
|
|
|
68
104
|
const byId = new Map(modes.map((m) => [m.id, m]));
|
|
69
105
|
const hosts = {}; // mode id -> pane host element
|
|
70
106
|
const panes = {}; // mode id -> the object the mode's mount() returned
|
|
71
107
|
const filterByMode = Object.fromEntries(modes.map((m) => [m.id, '']));
|
|
72
|
-
let mode = modes[0].id;
|
|
108
|
+
let mode = byId.has(initialMode) ? initialMode : modes[0].id;
|
|
109
|
+
|
|
110
|
+
/** Whether the shared filter box serves this mode. */
|
|
111
|
+
const hasFilter = (id) => !!filterEl && byId.get(id)?.filter !== false;
|
|
73
112
|
|
|
74
113
|
const ensureHost = (id) => {
|
|
75
114
|
if (hosts[id]) return hosts[id];
|
|
@@ -88,9 +127,15 @@ export function mountNavPanel(hostEl, {
|
|
|
88
127
|
for (const m of modes) {
|
|
89
128
|
ensureHost(m.id).style.display = (m.id === next) ? 'flex' : 'none';
|
|
90
129
|
}
|
|
91
|
-
modesEl
|
|
130
|
+
modesEl?.querySelectorAll('[data-mode]').forEach((b) =>
|
|
92
131
|
b.classList.toggle('twm-nav__mode--on', b.dataset.mode === next));
|
|
93
|
-
filterEl
|
|
132
|
+
if (filterEl) {
|
|
133
|
+
// Touch the box's style only when a mode without a filter is
|
|
134
|
+
// involved, so a panel whose modes all filter renders as it always has.
|
|
135
|
+
if (!hasFilter(next)) filterBoxEl.style.display = 'none';
|
|
136
|
+
else if (filterBoxEl.style.display) filterBoxEl.style.removeProperty('display');
|
|
137
|
+
filterEl.value = hasFilter(next) ? (filterByMode[next] || '') : '';
|
|
138
|
+
}
|
|
94
139
|
|
|
95
140
|
const paneEl = ensureHost(next);
|
|
96
141
|
if (!panes[next]) {
|
|
@@ -108,17 +153,19 @@ export function mountNavPanel(hostEl, {
|
|
|
108
153
|
};
|
|
109
154
|
|
|
110
155
|
const _applyFilter = () => {
|
|
156
|
+
if (!hasFilter(mode)) return;
|
|
111
157
|
const q = filterByMode[mode] || '';
|
|
112
158
|
try { panes[mode]?.filter?.(q); }
|
|
113
159
|
catch (err) { console.warn('[nav] filter failed', mode, err); }
|
|
114
160
|
};
|
|
115
161
|
|
|
116
|
-
filterEl
|
|
162
|
+
filterEl?.addEventListener('input', () => {
|
|
163
|
+
if (!hasFilter(mode)) return;
|
|
117
164
|
filterByMode[mode] = filterEl.value;
|
|
118
165
|
_applyFilter();
|
|
119
166
|
});
|
|
120
167
|
|
|
121
|
-
modesEl
|
|
168
|
+
modesEl?.addEventListener('click', (e) => {
|
|
122
169
|
const btn = e.target.closest('[data-mode]');
|
|
123
170
|
if (btn) activate(btn.dataset.mode);
|
|
124
171
|
});
|
|
@@ -126,6 +173,8 @@ export function mountNavPanel(hostEl, {
|
|
|
126
173
|
activate(mode);
|
|
127
174
|
|
|
128
175
|
return {
|
|
176
|
+
select: (modeId) => activate(modeId),
|
|
177
|
+
active: () => mode,
|
|
129
178
|
refresh: () => activate(mode),
|
|
130
179
|
destroy: () => {
|
|
131
180
|
for (const p of Object.values(panes)) {
|
|
@@ -111,20 +111,20 @@ export function createPageFactories({ eventBus = null, events = {}, refreshOn =
|
|
|
111
111
|
console.error('[page] mount failed', err);
|
|
112
112
|
hideLoading();
|
|
113
113
|
}
|
|
114
|
-
|
|
114
|
+
// Keep the handles on() returned.
|
|
115
|
+
const subscriptions = [];
|
|
115
116
|
if (typeof tab?.refresh === 'function' && eventBus?.on) {
|
|
116
117
|
for (const [busEvent, reason] of Object.entries(refreshOn || {})) {
|
|
117
118
|
const fn = () => {
|
|
118
119
|
try { tab.refresh(reason); }
|
|
119
120
|
catch (err) { console.warn('[page] refresh failed', busEvent, err); }
|
|
120
121
|
};
|
|
121
|
-
|
|
122
|
-
eventBus.on(busEvent, fn);
|
|
122
|
+
subscriptions.push(eventBus.on(busEvent, fn));
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
return () => {
|
|
126
|
-
for (const
|
|
127
|
-
try {
|
|
126
|
+
for (const handle of subscriptions) {
|
|
127
|
+
try { handle?.dispose?.(); } catch {}
|
|
128
128
|
}
|
|
129
129
|
try { tab?.dispose?.(); tab?.unmount?.(); } catch {}
|
|
130
130
|
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* panel_availability.js — what "this panel is offered here" means, said once.
|
|
3
|
+
*
|
|
4
|
+
* A panel side (`left`, `right`, `bottom`) is either offered or withheld. Two
|
|
5
|
+
* places may say so, and both speak the same small language:
|
|
6
|
+
*
|
|
7
|
+
* - a KindDef's `panels` (kind_taxonomy.js) — what a kind of content offers
|
|
8
|
+
* - `wm.setPanelAvailability(side, offer)` — content overriding at runtime
|
|
9
|
+
*
|
|
10
|
+
* An OFFER is one of:
|
|
11
|
+
*
|
|
12
|
+
* true offered (the same as saying nothing)
|
|
13
|
+
* false withheld, with no reason to show
|
|
14
|
+
* { reason } withheld, and `reason` is what the toggle's tooltip says
|
|
15
|
+
*
|
|
16
|
+
* Read into `{ available, reason }`. Anything else is a boot error rather than
|
|
17
|
+
* a panel that silently stays put, for the same reason a bad `topNav` pointer
|
|
18
|
+
* is one (createTaxonomy).
|
|
19
|
+
*
|
|
20
|
+
* Internal: neither export is part of the package surface.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export const PANEL_SIDES = Object.freeze(['left', 'right', 'bottom']);
|
|
24
|
+
|
|
25
|
+
/** What every side is when nobody declared anything: offered, as panels always
|
|
26
|
+
* were before availability existed. */
|
|
27
|
+
export const PANEL_OFFERED = Object.freeze({ available: true, reason: null });
|
|
28
|
+
|
|
29
|
+
/** Read one offer. `where` names the caller in the error. */
|
|
30
|
+
export function readPanelOffer(offer, where) {
|
|
31
|
+
if (offer === true) return PANEL_OFFERED;
|
|
32
|
+
if (offer === false) return Object.freeze({ available: false, reason: null });
|
|
33
|
+
if (offer && typeof offer === 'object' && !Array.isArray(offer)) {
|
|
34
|
+
const { reason = null } = offer;
|
|
35
|
+
if (reason !== null && typeof reason !== 'string') {
|
|
36
|
+
throw new TypeError(`${where}: a panel's \`reason\` must be a string`);
|
|
37
|
+
}
|
|
38
|
+
return Object.freeze({ available: false, reason: reason || null });
|
|
39
|
+
}
|
|
40
|
+
throw new TypeError(`${where}: a panel offer is true, false or { reason }`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Throw unless `side` is one of PANEL_SIDES. */
|
|
44
|
+
export function assertPanelSide(side, where) {
|
|
45
|
+
if (!PANEL_SIDES.includes(side)) {
|
|
46
|
+
throw new TypeError(`${where}: unknown panel side '${side}' `
|
|
47
|
+
+ `(expected ${PANEL_SIDES.join(', ')})`);
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/tiling/panel_keys.js
CHANGED
|
File without changes
|
package/src/tiling/shell.js
CHANGED
|
@@ -42,10 +42,11 @@
|
|
|
42
42
|
import { WindowManager } from './wm.js';
|
|
43
43
|
import { createContentRegistry } from './content_registry.js';
|
|
44
44
|
import { createCommandPalette } from './command_palette.js';
|
|
45
|
-
import { installKeymap } from './keymap.js';
|
|
45
|
+
import { installKeymap, keymapShortcuts } from './keymap.js';
|
|
46
46
|
import { openTileTabSwitcher } from './tile_tab_menu.js';
|
|
47
47
|
import { mountZoomControl } from './zoom.js';
|
|
48
48
|
import { showContextMenu } from '../ui/components/context_menu.js';
|
|
49
|
+
import { createMenuBar } from '../ui/components/menu_bar.js';
|
|
49
50
|
import { openForm } from '../ui/components/modal.js';
|
|
50
51
|
|
|
51
52
|
/**
|
|
@@ -125,13 +126,36 @@ import { openForm } from '../ui/components/modal.js';
|
|
|
125
126
|
* desktops is said on the desktops
|
|
126
127
|
* themselves (`group`, `fixed` — see
|
|
127
128
|
* `DesktopManager.chips`).
|
|
129
|
+
* @param {object} [cfg.menuBar] STORY-0030. `{ menus, onAction?, label?,
|
|
130
|
+
* emptyLabel?, palette? }` — the model of
|
|
131
|
+
* the menu bar painted into
|
|
132
|
+
* `chrome.menuBar` (see menu_bar.js). Its
|
|
133
|
+
* enabled items are also the palette's
|
|
134
|
+
* commands, after `palette.commands`'
|
|
135
|
+
* own, unless `palette: false`. Each needs
|
|
136
|
+
* the other: either alone is a boot error.
|
|
128
137
|
* @param {object} [cfg.chrome] { topNav?, paletteButton?, desktops?,
|
|
129
138
|
* panelToggles?: { left?, right?, bottom? },
|
|
130
|
-
* zoom? }
|
|
139
|
+
* zoom?, menuBar? }
|
|
131
140
|
* `zoom` is C31: the element the content
|
|
132
141
|
* zoom control is painted into — see
|
|
133
142
|
* zoom.js. Absent, there is no control and
|
|
134
|
-
* nothing is ever scaled.
|
|
143
|
+
* nothing is ever scaled. `menuBar` is the
|
|
144
|
+
* element that becomes the menu bar; it
|
|
145
|
+
* needs `cfg.menuBar`.
|
|
146
|
+
* @param {object} [cfg.keymap] `{ bindings?: { [chord]: action | false | fn } }`
|
|
147
|
+
* — the window manager's chords, changed:
|
|
148
|
+
* `false` hands a chord back to the page
|
|
149
|
+
* (unprevented), an action name such as
|
|
150
|
+
* `'nav:0'` rebinds it, a function runs
|
|
151
|
+
* instead. Chord ids and action names are
|
|
152
|
+
* listed in keymap.js; an unknown one is
|
|
153
|
+
* warned about once and ignored. The
|
|
154
|
+
* palette's *Keyboard shortcuts* lists the
|
|
155
|
+
* bindings in effect. Omitted, the default
|
|
156
|
+
* keymap, `?` for help included. Anything
|
|
157
|
+
* but an object whose only key is
|
|
158
|
+
* `bindings` is a TypeError.
|
|
135
159
|
* @returns {Promise<object>} the frozen shell
|
|
136
160
|
*/
|
|
137
161
|
export async function createShell({
|
|
@@ -155,11 +179,13 @@ export async function createShell({
|
|
|
155
179
|
tabLayout = null,
|
|
156
180
|
newTabButton = true,
|
|
157
181
|
desktopBar = {},
|
|
182
|
+
menuBar: menuBarCfg = null,
|
|
158
183
|
chrome = {},
|
|
159
184
|
// An embedder that moved its sections out of the top bar — into an icon
|
|
160
185
|
// rail, say — passes the selector its own buttons match, and F1..F8 keep
|
|
161
186
|
// working. Omitted, the default top-bar selector applies.
|
|
162
187
|
navSelector = null,
|
|
188
|
+
keymap = null,
|
|
163
189
|
} = {}) {
|
|
164
190
|
// A bad shell is a BOOT error, not a runtime surprise. Same doctrine as
|
|
165
191
|
// createHost / createTaxonomy.
|
|
@@ -168,6 +194,25 @@ export async function createShell({
|
|
|
168
194
|
}
|
|
169
195
|
if (!taxonomy) throw new Error('createShell: a taxonomy is required');
|
|
170
196
|
if (!entities) throw new Error('createShell: an entity catalog is required');
|
|
197
|
+
if (chrome.menuBar && !menuBarCfg?.menus) {
|
|
198
|
+
throw new TypeError('createShell: `chrome.menuBar` needs `menuBar: { menus }`');
|
|
199
|
+
}
|
|
200
|
+
if (menuBarCfg && !chrome.menuBar) {
|
|
201
|
+
throw new TypeError('createShell: `menuBar` needs `chrome.menuBar`, the element it is painted into');
|
|
202
|
+
}
|
|
203
|
+
// `keymap` is checked here, not left to the keymap: a typo such as
|
|
204
|
+
// `{ binding: … }` would otherwise be dropped without a word, and the chord
|
|
205
|
+
// the embedder meant to take back would stay bound.
|
|
206
|
+
if (keymap != null) {
|
|
207
|
+
if (typeof keymap !== 'object' || Array.isArray(keymap)) {
|
|
208
|
+
throw new TypeError('createShell: `keymap` must be an object of { bindings }');
|
|
209
|
+
}
|
|
210
|
+
const extra = Object.keys(keymap).filter((k) => k !== 'bindings');
|
|
211
|
+
if (extra.length) {
|
|
212
|
+
throw new TypeError(`createShell: unknown \`keymap\` key(s) ${extra.map((k) => `"${k}"`).join(', ')}`
|
|
213
|
+
+ ' — the only key is `bindings`');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
171
216
|
|
|
172
217
|
const log = logger ?? { info(){}, warn(){}, error(){}, debug(){} };
|
|
173
218
|
const registry = createContentRegistry(content); // throws on a non-function
|
|
@@ -213,13 +258,25 @@ export async function createShell({
|
|
|
213
258
|
onChange: () => syncChrome(),
|
|
214
259
|
});
|
|
215
260
|
|
|
261
|
+
// STORY-0030. The menu bar is built before the palette so that its model
|
|
262
|
+
// can be the palette's commands too: one list of verbs, two doors.
|
|
263
|
+
const menuBar = chrome.menuBar ? createMenuBar(chrome.menuBar, menuBarCfg) : null;
|
|
264
|
+
const ownCommands = paletteCfg.commands;
|
|
265
|
+
const commands = menuBar && menuBarCfg.palette !== false
|
|
266
|
+
? () => [...(typeof ownCommands === 'function' ? ownCommands() || [] : []), ...menuBar.commands()]
|
|
267
|
+
: ownCommands;
|
|
268
|
+
const bindings = keymap?.bindings ?? null;
|
|
216
269
|
const palette = createCommandPalette({
|
|
217
|
-
wm, api, taxonomy, catalog: entities,
|
|
270
|
+
wm, api, taxonomy, catalog: entities,
|
|
271
|
+
// What *Keyboard shortcuts* lists is what the keys do in THIS shell.
|
|
272
|
+
shortcuts: keymapShortcuts(bindings),
|
|
273
|
+
...paletteCfg, commands,
|
|
218
274
|
});
|
|
219
275
|
// `navSelector` lets an embedder that moved its sections out of the top
|
|
220
276
|
// bar keep F1..F8 working. Omitted, the default top-bar selector applies —
|
|
221
|
-
// the behaviour every existing embedder has today.
|
|
222
|
-
|
|
277
|
+
// the behaviour every existing embedder has today. The keymap also binds
|
|
278
|
+
// `?` to help, so `dispose()` takes the help shortcut off with it.
|
|
279
|
+
const disposeKeymap = installKeymap({ wm, palette, navSelector, bindings, logger });
|
|
223
280
|
|
|
224
281
|
// Chrome — painted into the ELEMENTS the embedder handed over. An absent
|
|
225
282
|
// key means absent chrome, not a crash.
|
|
@@ -240,7 +297,8 @@ export async function createShell({
|
|
|
240
297
|
toggles = bindPanelToggles(chrome.panelToggles, wm);
|
|
241
298
|
|
|
242
299
|
// Belt-and-braces: anything else that mutates the WM fans out to chrome.
|
|
243
|
-
|
|
300
|
+
// Keep the handle on() returned.
|
|
301
|
+
const chromeSubscription = eventBus?.on?.('wm:changed', syncChrome);
|
|
244
302
|
|
|
245
303
|
await wm.load(); // ← FIRST MOUNT
|
|
246
304
|
syncChrome();
|
|
@@ -262,6 +320,8 @@ export async function createShell({
|
|
|
262
320
|
// `get`/`set` so an embedder can drive the zoom from its own settings
|
|
263
321
|
// pane, or read it, without reaching into the control's DOM.
|
|
264
322
|
zoom,
|
|
323
|
+
// STORY-0030. `setEnabled` / `setChecked` / `open` / `commands`.
|
|
324
|
+
menuBar,
|
|
265
325
|
}),
|
|
266
326
|
// A shell that can be built can be built TWICE — an embedder that
|
|
267
327
|
// rebuilds on a context change (a different project, a different
|
|
@@ -269,7 +329,7 @@ export async function createShell({
|
|
|
269
329
|
// outside `root` has to come off, or the second shell shares the page
|
|
270
330
|
// with the first one's keyboard.
|
|
271
331
|
dispose: () => {
|
|
272
|
-
try {
|
|
332
|
+
try { chromeSubscription?.dispose?.(); } catch { /* ignore */ }
|
|
273
333
|
try { disposeKeymap?.(); } catch { /* ignore */ }
|
|
274
334
|
try { palette?.close?.(); } catch { /* ignore */ }
|
|
275
335
|
// Every content factory gets its `destroy()`. Clearing the root
|
|
@@ -290,6 +350,8 @@ export async function createShell({
|
|
|
290
350
|
// persist it under the host the old shell was given. The element
|
|
291
351
|
// goes; the next shell paints its own.
|
|
292
352
|
try { desktopsEl?.remove(); } catch { /* ignore */ }
|
|
353
|
+
// Its listeners are on the document; a rebuilt shell paints its own.
|
|
354
|
+
try { menuBar?.dispose(); } catch { /* ignore */ }
|
|
293
355
|
},
|
|
294
356
|
});
|
|
295
357
|
}
|
|
@@ -368,13 +430,19 @@ function mountPaletteButton(hostEl, palette) {
|
|
|
368
430
|
return btn;
|
|
369
431
|
}
|
|
370
432
|
|
|
433
|
+
/** What each toggle said before a withheld panel made it say why, so the
|
|
434
|
+
* embedder's own tooltip comes back when the panel is offered again. Held
|
|
435
|
+
* here, keyed by the fresh node, never written back to the DOM. */
|
|
436
|
+
const OWN_TOOLTIPS = new WeakMap();
|
|
437
|
+
|
|
371
438
|
/**
|
|
372
439
|
* Take ownership of the embedder's three panel-toggle buttons.
|
|
373
440
|
*
|
|
374
441
|
* cloneNode copies any state the embedder's own machinery left on a button
|
|
375
442
|
* (.active/.pinned/.disabled, the disabled attr, aria-pressed). We strip it so
|
|
376
|
-
* the WM is the sole, clean source of truth — `syncPanelToggles` manages
|
|
377
|
-
* `.panel-toggle-btn--on`
|
|
443
|
+
* the WM is the sole, clean source of truth — `syncPanelToggles` manages
|
|
444
|
+
* `.twm-panel-toggle-btn--on` and a withheld panel's `aria-disabled` and
|
|
445
|
+
* tooltip from here on. The clone keeps the id, so an embedder
|
|
378
446
|
* that drives a toggle by `getElementById(...).click()` still works.
|
|
379
447
|
*
|
|
380
448
|
* @returns {{left?:Element, right?:Element, bottom?:Element}} the FRESH nodes
|
|
@@ -391,16 +459,44 @@ function bindPanelToggles(map, wm) {
|
|
|
391
459
|
el.removeAttribute('aria-pressed');
|
|
392
460
|
btn.replaceWith(el);
|
|
393
461
|
el.addEventListener('click', () => wm.togglePanel(side));
|
|
462
|
+
OWN_TOOLTIPS.set(el, {
|
|
463
|
+
tooltip: el.getAttribute('data-tooltip'),
|
|
464
|
+
title: el.getAttribute('title'),
|
|
465
|
+
});
|
|
394
466
|
fresh[side] = el;
|
|
395
467
|
}
|
|
396
468
|
return fresh;
|
|
397
469
|
}
|
|
398
470
|
|
|
471
|
+
/** Mirror the WM onto the toggles: `--on` while a panel shows, and, while the
|
|
472
|
+
* primary content withholds one, `aria-disabled` and the reason as the
|
|
473
|
+
* tooltip. `aria-disabled` rather than `disabled`: a disabled button gets no
|
|
474
|
+
* pointer events in every engine, and the reason is only worth giving if the
|
|
475
|
+
* pointer can find it. A click on it is answered by `togglePanel`, which
|
|
476
|
+
* refuses a withheld panel. */
|
|
399
477
|
function syncPanelToggles(map, wm) {
|
|
400
478
|
for (const side of ['left', 'right', 'bottom']) {
|
|
401
479
|
const btn = map?.[side];
|
|
402
480
|
if (!btn) continue;
|
|
403
481
|
btn.classList.toggle('twm-panel-toggle-btn--on', wm.isPanelOpen(side));
|
|
482
|
+
const { available, reason } = wm.getPanelAvailability(side);
|
|
483
|
+
const own = OWN_TOOLTIPS.get(btn) || {};
|
|
484
|
+
const restore = (attr, value) => (value == null
|
|
485
|
+
? btn.removeAttribute(attr) : btn.setAttribute(attr, value));
|
|
486
|
+
if (available) {
|
|
487
|
+
btn.removeAttribute('aria-disabled');
|
|
488
|
+
restore('data-tooltip', own.tooltip);
|
|
489
|
+
restore('title', own.title);
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
btn.setAttribute('aria-disabled', 'true');
|
|
493
|
+
// The framework's tooltip reads `data-tooltip` on a `twm-has-tooltip`
|
|
494
|
+
// button and strips `title` from it; any other button shows `title`.
|
|
495
|
+
if (btn.classList.contains('twm-has-tooltip')) {
|
|
496
|
+
restore('data-tooltip', reason ?? own.tooltip);
|
|
497
|
+
} else {
|
|
498
|
+
restore('title', reason ?? own.title);
|
|
499
|
+
}
|
|
404
500
|
}
|
|
405
501
|
}
|
|
406
502
|
|
|
@@ -560,13 +656,18 @@ function _tileContextMenu(wm, leafId, x, y) {
|
|
|
560
656
|
const isPanel = String(leaf.content?.kind || '').startsWith('panel:');
|
|
561
657
|
tree.focus(leafId);
|
|
562
658
|
wm.renderer._updateFocusClasses();
|
|
659
|
+
// A SINGLE KIND HAS NO COPIES. The two copy verbs are greyed, and say why;
|
|
660
|
+
// the move verbs below (float, move to desktop) carry the one instance.
|
|
661
|
+
const single = !!leaf.content && wm.isSingleKind(leaf.content.kind);
|
|
662
|
+
const singleTitle = single ? wm.taxonomy.singleReason(leaf.content.kind) : undefined;
|
|
563
663
|
|
|
564
664
|
const items = [
|
|
565
665
|
{ label: 'Split horizontally', icon: 'splitscreen_vertical_add', action: 'split-h' },
|
|
566
666
|
{ label: 'Split vertically', icon: 'splitscreen_add', action: 'split-v' },
|
|
567
667
|
{ separator: true },
|
|
568
668
|
{ label: 'Open in new tab', icon: 'tab',
|
|
569
|
-
action: 'open-tab', disabled: isPanel || !leaf.content
|
|
669
|
+
action: 'open-tab', disabled: isPanel || !leaf.content || single,
|
|
670
|
+
title: singleTitle },
|
|
570
671
|
// TWO DIFFERENT GLYPHS FOR TWO DIFFERENT DESTINATIONS. `web_asset` is a
|
|
571
672
|
// window INSIDE the application — the same glyph `ManagedWindow` uses
|
|
572
673
|
// for itself — and an embedder that can also send content to a real
|
|
@@ -575,7 +676,8 @@ function _tileContextMenu(wm, leafId, x, y) {
|
|
|
575
676
|
// two commands are the same command, and then loses a window looking
|
|
576
677
|
// for it on the other screen.
|
|
577
678
|
{ label: 'Open a copy in a window', icon: 'web_asset',
|
|
578
|
-
action: 'open-window', disabled: isPanel || !leaf.content
|
|
679
|
+
action: 'open-window', disabled: isPanel || !leaf.content || single,
|
|
680
|
+
title: singleTitle },
|
|
579
681
|
// C20, THE OTHER HALF — and it was missing while the `close` half
|
|
580
682
|
// below carried a paragraph explaining why it could not be.
|
|
581
683
|
//
|
package/src/tiling/tab_strip.js
CHANGED
|
File without changes
|
|
File without changes
|