flexdesk 0.1.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/LICENSE +21 -0
- package/README.md +195 -0
- package/THIRD_PARTY_LICENSES.md +53 -0
- package/css/base.css +3439 -0
- package/css/flexdesk.css +5797 -0
- package/css/overrides.css +2349 -0
- package/css/reset.css +46 -0
- package/css/tokens.css +137 -0
- package/dist/charts.js +780 -0
- package/dist/charts.js.map +7 -0
- package/dist/chunk-3PHCPZHT.js +222 -0
- package/dist/chunk-3PHCPZHT.js.map +7 -0
- package/dist/chunk-CT4YXXLP.js +1770 -0
- package/dist/chunk-CT4YXXLP.js.map +7 -0
- package/dist/chunk-DRYCDMEG.js +2217 -0
- package/dist/chunk-DRYCDMEG.js.map +7 -0
- package/dist/chunk-DVU44T77.js +855 -0
- package/dist/chunk-DVU44T77.js.map +7 -0
- package/dist/chunk-FL5KFNQH.js +640 -0
- package/dist/chunk-FL5KFNQH.js.map +7 -0
- package/dist/chunk-FOOS3T5L.js +117 -0
- package/dist/chunk-FOOS3T5L.js.map +7 -0
- package/dist/chunk-JYWURG5T.js +32 -0
- package/dist/chunk-JYWURG5T.js.map +7 -0
- package/dist/chunk-TLZUUFOE.js +276 -0
- package/dist/chunk-TLZUUFOE.js.map +7 -0
- package/dist/chunk-UCJ2WD4D.js +625 -0
- package/dist/chunk-UCJ2WD4D.js.map +7 -0
- package/dist/chunk-WVFGV5FT.js +205 -0
- package/dist/chunk-WVFGV5FT.js.map +7 -0
- package/dist/core.js +1098 -0
- package/dist/core.js.map +7 -0
- package/dist/editor.js +1661 -0
- package/dist/editor.js.map +7 -0
- package/dist/flexdesk.css +5797 -0
- package/dist/host.js +132 -0
- package/dist/host.js.map +7 -0
- package/dist/reset.css +46 -0
- package/dist/tile_registry-6WZPTUZV.js +28 -0
- package/dist/tile_registry-6WZPTUZV.js.map +7 -0
- package/dist/tiles.js +2068 -0
- package/dist/tiles.js.map +7 -0
- package/dist/tokens.css +137 -0
- package/dist/widgets.js +5438 -0
- package/dist/widgets.js.map +7 -0
- package/dist/wm.js +4830 -0
- package/dist/wm.js.map +7 -0
- package/package.json +80 -0
- package/src/charts/chart_types.js +699 -0
- package/src/charts/data_series_plot_window.js +761 -0
- package/src/charts/downsample.js +119 -0
- package/src/charts/plot_config.js +176 -0
- package/src/charts/plot_popout_window.js +1560 -0
- package/src/charts/plotly_wrapper.js +432 -0
- package/src/core/event_bus.js +230 -0
- package/src/core/logging.js +300 -0
- package/src/core/settings.js +813 -0
- package/src/core/state_guard.js +382 -0
- package/src/core/state_guard_config.js +309 -0
- package/src/core/state_machine.js +258 -0
- package/src/editor/editor_pane.js +264 -0
- package/src/editor/monaco_editor_factory.js +449 -0
- package/src/editor/monaco_loader.js +74 -0
- package/src/editor/notebook_search.js +460 -0
- package/src/editor/notebook_tab_bar.js +456 -0
- package/src/editor/notebook_undo_manager.js +108 -0
- package/src/editor/split_pane_container.js +197 -0
- package/src/help/help_modal.js +409 -0
- package/src/help/help_registry.js +65 -0
- package/src/host/host.js +69 -0
- package/src/host/pywebview_host.js +133 -0
- package/src/tiles/config_schema.js +256 -0
- package/src/tiles/layout_persistence.js +662 -0
- package/src/tiles/tile_base.js +785 -0
- package/src/tiles/tile_grid.js +905 -0
- package/src/tiles/tile_registry.js +136 -0
- package/src/tiling/command_palette.js +328 -0
- package/src/tiling/content_registry.js +110 -0
- package/src/tiling/desktops.js +133 -0
- package/src/tiling/entity_sources.js +179 -0
- package/src/tiling/keymap.js +156 -0
- package/src/tiling/kind_taxonomy.js +172 -0
- package/src/tiling/landing_table.js +717 -0
- package/src/tiling/loading_overlay.js +33 -0
- package/src/tiling/nav_panel.js +142 -0
- package/src/tiling/page_factory.js +215 -0
- package/src/tiling/panel_keys.js +145 -0
- package/src/tiling/shell.js +437 -0
- package/src/tiling/tile_breadcrumb.js +222 -0
- package/src/tiling/tile_renderer.js +459 -0
- package/src/tiling/tile_tab_menu.js +340 -0
- package/src/tiling/tile_tree.js +880 -0
- package/src/tiling/wm.js +1352 -0
- package/src/ui/base/component_base.js +30 -0
- package/src/ui/base/controller_base.js +48 -0
- package/src/ui/base/page_base.js +102 -0
- package/src/ui/components/about_dialog.js +224 -0
- package/src/ui/components/action_dropdown.js +389 -0
- package/src/ui/components/attribute_list_editor.js +652 -0
- package/src/ui/components/autocomplete_field.js +732 -0
- package/src/ui/components/computing_status_window.js +81 -0
- package/src/ui/components/confirm_dialog.js +307 -0
- package/src/ui/components/context_menu.js +87 -0
- package/src/ui/components/data_table.js +2228 -0
- package/src/ui/components/detail_header.js +177 -0
- package/src/ui/components/drag_reorder.js +0 -0
- package/src/ui/components/gallery_picker.js +147 -0
- package/src/ui/components/inline_renamer.js +129 -0
- package/src/ui/components/managed_window.js +804 -0
- package/src/ui/components/modal.js +512 -0
- package/src/ui/components/notification_history.js +264 -0
- package/src/ui/components/slide_out_panel.js +185 -0
- package/src/ui/components/slider_field.js +122 -0
- package/src/ui/components/sortable_list.js +309 -0
- package/src/ui/components/table_state_store.js +81 -0
- package/src/ui/components/toast.js +59 -0
- package/src/ui/components/tree_view.js +460 -0
- package/src/ui/components/window_taskbar.js +143 -0
- package/src/ui/controllers/panel_state_machine.js +634 -0
- package/src/ui/controllers/window_chrome_controller.js +659 -0
- package/src/ui/notification_center.js +576 -0
- package/src/ui/utils/overlay_scrollbar.js +492 -0
- package/src/ui/utils/raf_resize_observer.js +31 -0
- package/src/ui/utils/tooltip_service.js +406 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Computing Status Window
|
|
3
|
+
*
|
|
4
|
+
* Lightweight ManagedWindow that shows a spinner and status message
|
|
5
|
+
* while a long-running computation is in progress. Returns a handle
|
|
6
|
+
* with update() and close() methods.
|
|
7
|
+
*
|
|
8
|
+
* Optionally shows a Cancel button when an onCancel callback is provided.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { ManagedWindow } from './managed_window.js';
|
|
12
|
+
|
|
13
|
+
let _activeWindow = null;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Show a computing status window.
|
|
17
|
+
* @param {Object} options
|
|
18
|
+
* @param {string} [options.title='Computing...'] - Window title
|
|
19
|
+
* @param {string} [options.message=''] - Status message below spinner
|
|
20
|
+
* @param {string} [options.icon='hourglass_top'] - Material icon name
|
|
21
|
+
* @param {Function} [options.onCancel] - If provided, shows a Cancel button. Called once when clicked.
|
|
22
|
+
* @returns {{ update(msg: string): void, close(): void }}
|
|
23
|
+
*/
|
|
24
|
+
export function showComputingWindow({ title = 'Computing...', message = '', icon = 'hourglass_top', onCancel } = {}) {
|
|
25
|
+
if (_activeWindow) {
|
|
26
|
+
_activeWindow.close();
|
|
27
|
+
_activeWindow = null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const content = document.createElement('div');
|
|
31
|
+
content.className = 'twm-computing-status';
|
|
32
|
+
content.innerHTML = `
|
|
33
|
+
<div class="twm-computing-status__spinner"></div>
|
|
34
|
+
<div class="twm-computing-status__message">${message}</div>
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
if (typeof onCancel === 'function') {
|
|
38
|
+
const btn = document.createElement('button');
|
|
39
|
+
btn.className = 'twm-computing-status__cancel-btn';
|
|
40
|
+
btn.textContent = 'Cancel';
|
|
41
|
+
btn.addEventListener('click', () => {
|
|
42
|
+
btn.disabled = true;
|
|
43
|
+
const msgEl = content.querySelector('.twm-computing-status__message');
|
|
44
|
+
if (msgEl) msgEl.textContent = 'Cancelling...';
|
|
45
|
+
onCancel();
|
|
46
|
+
}, { once: true });
|
|
47
|
+
content.appendChild(btn);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_activeWindow = new ManagedWindow({
|
|
51
|
+
id: 'computing-status-window',
|
|
52
|
+
title,
|
|
53
|
+
icon,
|
|
54
|
+
content,
|
|
55
|
+
minWidth: 320,
|
|
56
|
+
minHeight: 140,
|
|
57
|
+
defaultWidth: 380,
|
|
58
|
+
defaultHeight: onCancel ? 190 : 160,
|
|
59
|
+
canMinimize: true,
|
|
60
|
+
canMaximize: false,
|
|
61
|
+
canResize: false,
|
|
62
|
+
canDrag: true,
|
|
63
|
+
modal: false,
|
|
64
|
+
onClose: () => { _activeWindow = null; },
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
_activeWindow.show();
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
update(msg) {
|
|
71
|
+
const el = content.querySelector('.twm-computing-status__message');
|
|
72
|
+
if (el) el.textContent = msg;
|
|
73
|
+
},
|
|
74
|
+
close() {
|
|
75
|
+
if (_activeWindow) {
|
|
76
|
+
_activeWindow.close();
|
|
77
|
+
_activeWindow = null;
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Confirm Dialog Component
|
|
3
|
+
* Location: ui/components/confirm_dialog.js
|
|
4
|
+
*
|
|
5
|
+
* Thin delegator over the canonical modal helpers in
|
|
6
|
+
* `ecoagent/ui/modal.js`. Every dialog signature here is preserved so
|
|
7
|
+
* the 10+ call sites across notebook/data/results/ai pages keep
|
|
8
|
+
* working unchanged; under the hood, all four standard dialogs now
|
|
9
|
+
* flow through `openConfirm` / `openForm` / `openModal` so they
|
|
10
|
+
* inherit the TUI styling (`▸ TITLE` strip, accent-blue primary
|
|
11
|
+
* action, square frame, Tab focus trap).
|
|
12
|
+
*
|
|
13
|
+
* The one custom case is `showRunProgressDialog`, which exposes an
|
|
14
|
+
* imperative `update / complete / cancelled / close` API and so
|
|
15
|
+
* stays as a direct `ManagedWindow` build — but it still benefits
|
|
16
|
+
* from the focus trap + chrome by virtue of `modal: true`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { ManagedWindow } from './managed_window.js';
|
|
20
|
+
import {
|
|
21
|
+
openConfirm, openForm, openModal,
|
|
22
|
+
} from './modal.js';
|
|
23
|
+
|
|
24
|
+
// Counter — used only by showRunProgressDialog which still builds its
|
|
25
|
+
// own ManagedWindow. The migrated helpers don't need it.
|
|
26
|
+
let dialogIdCounter = 0;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Show a confirmation dialog. Delegates to `openConfirm`.
|
|
30
|
+
* @param {Object} options
|
|
31
|
+
* @param {string} options.title
|
|
32
|
+
* @param {string} options.message
|
|
33
|
+
* @param {string} [options.icon='warning']
|
|
34
|
+
* @param {string} [options.okLabel='OK']
|
|
35
|
+
* @param {string} [options.cancelLabel='Cancel']
|
|
36
|
+
* @param {string} [options.okVariant='primary'] - 'primary' or 'danger'
|
|
37
|
+
* @returns {Promise<boolean>}
|
|
38
|
+
*/
|
|
39
|
+
export async function showConfirmDialog(options = {}) {
|
|
40
|
+
const {
|
|
41
|
+
title = 'Confirm',
|
|
42
|
+
message = 'Are you sure?',
|
|
43
|
+
icon = 'warning',
|
|
44
|
+
okLabel = 'OK',
|
|
45
|
+
cancelLabel = 'Cancel',
|
|
46
|
+
okVariant = 'primary',
|
|
47
|
+
} = options;
|
|
48
|
+
return openConfirm({
|
|
49
|
+
title,
|
|
50
|
+
message,
|
|
51
|
+
icon,
|
|
52
|
+
confirmLabel: okLabel,
|
|
53
|
+
cancelLabel,
|
|
54
|
+
danger: okVariant === 'danger',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Show a delete-confirmation dialog. Always danger-styled.
|
|
60
|
+
* @returns {Promise<boolean>}
|
|
61
|
+
*/
|
|
62
|
+
export async function showDeleteConfirmDialog(options = {}) {
|
|
63
|
+
const {
|
|
64
|
+
itemName = '',
|
|
65
|
+
itemType = 'item',
|
|
66
|
+
additionalMessage = '',
|
|
67
|
+
} = options;
|
|
68
|
+
let message = `Delete ${itemType}`;
|
|
69
|
+
if (itemName) message += ` "${itemName}"`;
|
|
70
|
+
message += '?';
|
|
71
|
+
if (additionalMessage) message += `<br><br>${escapeHtml(additionalMessage)}`;
|
|
72
|
+
return openConfirm({
|
|
73
|
+
title: `Delete ${capitalize(itemType)}`,
|
|
74
|
+
message,
|
|
75
|
+
icon: 'delete',
|
|
76
|
+
confirmLabel: 'Delete',
|
|
77
|
+
cancelLabel: 'Cancel',
|
|
78
|
+
danger: true,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Show a close-confirmation dialog.
|
|
84
|
+
* @returns {Promise<boolean>}
|
|
85
|
+
*/
|
|
86
|
+
export async function showCloseConfirmDialog(options = {}) {
|
|
87
|
+
const {
|
|
88
|
+
itemName = '',
|
|
89
|
+
itemType = 'tab',
|
|
90
|
+
} = options;
|
|
91
|
+
let message = `Close ${itemType}`;
|
|
92
|
+
if (itemName) message += ` "${itemName}"`;
|
|
93
|
+
message += '?';
|
|
94
|
+
return openConfirm({
|
|
95
|
+
title: `Close ${capitalize(itemType)}`,
|
|
96
|
+
message,
|
|
97
|
+
icon: 'close',
|
|
98
|
+
confirmLabel: 'Close',
|
|
99
|
+
cancelLabel: 'Cancel',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Show a dialog with multiple action buttons. Delegates to `openModal`
|
|
105
|
+
* so each action maps to a button in the canonical footer.
|
|
106
|
+
* @returns {Promise<string|null>}
|
|
107
|
+
*/
|
|
108
|
+
export async function showChoiceDialog(options = {}) {
|
|
109
|
+
const {
|
|
110
|
+
title = 'Choose',
|
|
111
|
+
message = '',
|
|
112
|
+
icon = 'warning',
|
|
113
|
+
actions = [],
|
|
114
|
+
} = options;
|
|
115
|
+
const content = document.createElement('div');
|
|
116
|
+
content.className = 'twm-confirm-dialog__body';
|
|
117
|
+
content.innerHTML = `
|
|
118
|
+
<span class="twm-confirm-dialog__icon material-symbols-outlined">${escapeHtml(icon)}</span>
|
|
119
|
+
<div class="twm-confirm-dialog__message">${escapeHtml(message)}</div>
|
|
120
|
+
`;
|
|
121
|
+
return openModal({
|
|
122
|
+
title,
|
|
123
|
+
icon,
|
|
124
|
+
content,
|
|
125
|
+
width: 480,
|
|
126
|
+
height: 220,
|
|
127
|
+
actions: actions.map((a) => ({
|
|
128
|
+
label: a.label,
|
|
129
|
+
value: a.value,
|
|
130
|
+
primary: a.variant === 'primary',
|
|
131
|
+
danger: a.variant === 'danger',
|
|
132
|
+
})),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Show a dialog with a dropdown select + OK/Cancel. Delegates to
|
|
138
|
+
* `openForm` with a single select field.
|
|
139
|
+
* @returns {Promise<string|null>}
|
|
140
|
+
*/
|
|
141
|
+
export async function showSelectDialog(options = {}) {
|
|
142
|
+
const {
|
|
143
|
+
title = 'Select',
|
|
144
|
+
message = '',
|
|
145
|
+
items = [],
|
|
146
|
+
okLabel = 'OK',
|
|
147
|
+
cancelLabel = 'Cancel',
|
|
148
|
+
} = options;
|
|
149
|
+
const fields = [];
|
|
150
|
+
if (message) {
|
|
151
|
+
fields.push({ section: message });
|
|
152
|
+
}
|
|
153
|
+
fields.push({
|
|
154
|
+
name: '_choice',
|
|
155
|
+
label: 'Choice',
|
|
156
|
+
type: 'select',
|
|
157
|
+
default: items[0]?.value || '',
|
|
158
|
+
options: items.map((i) => ({ value: i.value, label: i.label })),
|
|
159
|
+
});
|
|
160
|
+
// NB: cancelLabel is currently hardcoded as "Cancel" on openForm.
|
|
161
|
+
// openConfirm has cancelLabel; if a caller really needs custom
|
|
162
|
+
// cancel text on openForm, we'd extend openForm there. The
|
|
163
|
+
// explicit no-op suppresses the unused-arg lint.
|
|
164
|
+
void cancelLabel;
|
|
165
|
+
const data = await openForm({
|
|
166
|
+
title,
|
|
167
|
+
submitLabel: okLabel,
|
|
168
|
+
fields,
|
|
169
|
+
});
|
|
170
|
+
if (!data) return null;
|
|
171
|
+
return data._choice;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
// ─── helpers ────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
function escapeHtml(str) {
|
|
178
|
+
if (!str) return '';
|
|
179
|
+
return String(str)
|
|
180
|
+
.replace(/&/g, '&')
|
|
181
|
+
.replace(/</g, '<')
|
|
182
|
+
.replace(/>/g, '>')
|
|
183
|
+
.replace(/"/g, '"');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function capitalize(str) {
|
|
187
|
+
if (!str) return '';
|
|
188
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
// ─── showRunProgressDialog (unchanged custom build) ─────────────────
|
|
193
|
+
//
|
|
194
|
+
// This one exposes an imperative API (`update`, `complete`, `cancelled`,
|
|
195
|
+
// `close`) that the openModal/openForm/openConfirm helpers don't model
|
|
196
|
+
// — they're one-shot promise-returning. Keep the direct ManagedWindow
|
|
197
|
+
// build, but it still gets the TUI chrome + focus trap for free via
|
|
198
|
+
// `modal: true`.
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Show a run progress dialog with spinner, live message updates, and
|
|
202
|
+
* final status. Returns a control object — keep this build instead of
|
|
203
|
+
* delegating so callers retain the imperative update / complete API.
|
|
204
|
+
* @returns {{ update(msg: string): void, complete(result: {ok: boolean, message: string}): void, cancelled(msg?: string): void, close(): void }}
|
|
205
|
+
*/
|
|
206
|
+
export function showRunProgressDialog(options = {}) {
|
|
207
|
+
const {
|
|
208
|
+
title = 'Running Pipeline',
|
|
209
|
+
message = 'Starting…',
|
|
210
|
+
onCancel,
|
|
211
|
+
} = options;
|
|
212
|
+
|
|
213
|
+
const dialogId = `run-progress-${++dialogIdCounter}`;
|
|
214
|
+
let finished = false;
|
|
215
|
+
|
|
216
|
+
const contentEl = document.createElement('div');
|
|
217
|
+
contentEl.className = 'twm-confirm-dialog__content run-progress';
|
|
218
|
+
|
|
219
|
+
const bodyEl = document.createElement('div');
|
|
220
|
+
bodyEl.className = 'twm-run-progress__body';
|
|
221
|
+
bodyEl.innerHTML = `
|
|
222
|
+
<div class="twm-run-progress__spinner"></div>
|
|
223
|
+
<div class="twm-run-progress__message">${escapeHtml(message)}</div>
|
|
224
|
+
`;
|
|
225
|
+
contentEl.appendChild(bodyEl);
|
|
226
|
+
|
|
227
|
+
let stopBtn = null;
|
|
228
|
+
if (typeof onCancel === 'function') {
|
|
229
|
+
const stopActionsEl = document.createElement('div');
|
|
230
|
+
stopActionsEl.className = 'twm-confirm-dialog__actions twm-run-progress__stop-actions';
|
|
231
|
+
stopBtn = document.createElement('button');
|
|
232
|
+
stopBtn.type = 'button';
|
|
233
|
+
stopBtn.className = 'twm-confirm-dialog__btn twm-run-progress__stop-btn';
|
|
234
|
+
stopBtn.innerHTML = '<span class="material-symbols-outlined">stop</span> Stop';
|
|
235
|
+
stopBtn.addEventListener('click', () => {
|
|
236
|
+
stopBtn.disabled = true;
|
|
237
|
+
stopBtn.innerHTML = '<span class="material-symbols-outlined">hourglass_top</span> Stopping…';
|
|
238
|
+
onCancel();
|
|
239
|
+
});
|
|
240
|
+
stopActionsEl.appendChild(stopBtn);
|
|
241
|
+
contentEl.appendChild(stopActionsEl);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const actionsEl = document.createElement('div');
|
|
245
|
+
actionsEl.className = 'twm-confirm-dialog__actions';
|
|
246
|
+
actionsEl.style.display = 'none';
|
|
247
|
+
|
|
248
|
+
const closeBtn = document.createElement('button');
|
|
249
|
+
closeBtn.type = 'button';
|
|
250
|
+
closeBtn.className = 'twm-confirm-dialog__btn twm-confirm-dialog__btn--primary';
|
|
251
|
+
closeBtn.textContent = 'Close';
|
|
252
|
+
closeBtn.addEventListener('click', () => dialogWindow.close());
|
|
253
|
+
actionsEl.appendChild(closeBtn);
|
|
254
|
+
contentEl.appendChild(actionsEl);
|
|
255
|
+
|
|
256
|
+
const dialogWindow = new ManagedWindow({
|
|
257
|
+
id: dialogId,
|
|
258
|
+
title,
|
|
259
|
+
icon: 'play_arrow',
|
|
260
|
+
content: contentEl,
|
|
261
|
+
minWidth: 320,
|
|
262
|
+
minHeight: 120,
|
|
263
|
+
defaultWidth: 420,
|
|
264
|
+
defaultHeight: 200,
|
|
265
|
+
canMinimize: false,
|
|
266
|
+
canMaximize: false,
|
|
267
|
+
canResize: false,
|
|
268
|
+
canDrag: false,
|
|
269
|
+
modal: true,
|
|
270
|
+
});
|
|
271
|
+
dialogWindow.show();
|
|
272
|
+
|
|
273
|
+
const messageEl = bodyEl.querySelector('.twm-run-progress__message');
|
|
274
|
+
const spinnerEl = bodyEl.querySelector('.twm-run-progress__spinner');
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
update(msg) {
|
|
278
|
+
if (finished || !messageEl) return;
|
|
279
|
+
messageEl.textContent = msg;
|
|
280
|
+
},
|
|
281
|
+
complete(result) {
|
|
282
|
+
if (finished) return;
|
|
283
|
+
finished = true;
|
|
284
|
+
if (spinnerEl) spinnerEl.style.display = 'none';
|
|
285
|
+
if (stopBtn) stopBtn.parentElement.style.display = 'none';
|
|
286
|
+
if (messageEl) {
|
|
287
|
+
messageEl.textContent = result?.message || '';
|
|
288
|
+
messageEl.className = `twm-run-progress__message ${result?.ok ? 'is-ok' : 'is-err'}`;
|
|
289
|
+
}
|
|
290
|
+
actionsEl.style.display = '';
|
|
291
|
+
},
|
|
292
|
+
cancelled(msg) {
|
|
293
|
+
if (finished) return;
|
|
294
|
+
finished = true;
|
|
295
|
+
if (spinnerEl) spinnerEl.style.display = 'none';
|
|
296
|
+
if (stopBtn) stopBtn.parentElement.style.display = 'none';
|
|
297
|
+
if (messageEl) {
|
|
298
|
+
messageEl.textContent = msg || 'Cancelled.';
|
|
299
|
+
messageEl.className = 'twm-run-progress__message is-cancelled';
|
|
300
|
+
}
|
|
301
|
+
actionsEl.style.display = '';
|
|
302
|
+
},
|
|
303
|
+
close() {
|
|
304
|
+
try { dialogWindow.close(); } catch { /* already closed */ }
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context_menu.js — small popup menu that matches Ecosim's `.context-menu`
|
|
3
|
+
* idiom (see main_new.css line 9713 and ui/js/ui/components/data_table.js).
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* showContextMenu(x, y, [
|
|
7
|
+
* { label: 'Open', icon: 'open_in_new', action: 'open' },
|
|
8
|
+
* { separator: true },
|
|
9
|
+
* { label: 'Delete', icon: 'delete', action: 'delete', danger: true },
|
|
10
|
+
* ], (action) => { ... });
|
|
11
|
+
*
|
|
12
|
+
* The menu auto-closes on outside click / scroll / Escape.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
let _activeMenu = null;
|
|
16
|
+
|
|
17
|
+
export function showContextMenu(x, y, items, onAction) {
|
|
18
|
+
hideContextMenu();
|
|
19
|
+
|
|
20
|
+
const menu = document.createElement('div');
|
|
21
|
+
menu.className = 'twm-context-menu ea-context-menu';
|
|
22
|
+
|
|
23
|
+
for (const it of items) {
|
|
24
|
+
if (it.separator) {
|
|
25
|
+
const sep = document.createElement('div');
|
|
26
|
+
sep.className = 'twm-context-menu__separator';
|
|
27
|
+
menu.appendChild(sep);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const row = document.createElement('div');
|
|
31
|
+
let cls = 'twm-context-menu-item';
|
|
32
|
+
if (it.danger) cls += ' twm-delete-node';
|
|
33
|
+
if (it.disabled) cls += ' disabled';
|
|
34
|
+
row.className = cls;
|
|
35
|
+
row.innerHTML = `
|
|
36
|
+
<span class="material-symbols-outlined">${it.icon || ''}</span>
|
|
37
|
+
<span>${escapeHtml(it.label)}</span>
|
|
38
|
+
`;
|
|
39
|
+
if (!it.disabled) {
|
|
40
|
+
row.addEventListener('click', (e) => {
|
|
41
|
+
e.stopPropagation();
|
|
42
|
+
hideContextMenu();
|
|
43
|
+
onAction?.(it.action);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
menu.appendChild(row);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
document.body.appendChild(menu);
|
|
50
|
+
menu.style.display = 'block';
|
|
51
|
+
_activeMenu = menu;
|
|
52
|
+
|
|
53
|
+
// Position with viewport clamping.
|
|
54
|
+
const rect = menu.getBoundingClientRect();
|
|
55
|
+
const left = Math.min(x, window.innerWidth - rect.width - 8);
|
|
56
|
+
const top = Math.min(y, window.innerHeight - rect.height - 8);
|
|
57
|
+
menu.style.left = `${Math.max(0, left)}px`;
|
|
58
|
+
menu.style.top = `${Math.max(0, top)}px`;
|
|
59
|
+
|
|
60
|
+
// Close on outside click / scroll / Escape.
|
|
61
|
+
setTimeout(() => {
|
|
62
|
+
document.addEventListener('mousedown', _outsideHandler, { once: true, capture: true });
|
|
63
|
+
}, 0);
|
|
64
|
+
document.addEventListener('keydown', _escHandler);
|
|
65
|
+
window.addEventListener('scroll', hideContextMenu, { once: true, capture: true });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function hideContextMenu() {
|
|
69
|
+
if (!_activeMenu) return;
|
|
70
|
+
_activeMenu.remove();
|
|
71
|
+
_activeMenu = null;
|
|
72
|
+
document.removeEventListener('keydown', _escHandler);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function _outsideHandler(e) {
|
|
76
|
+
if (_activeMenu && !_activeMenu.contains(e.target)) hideContextMenu();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function _escHandler(e) {
|
|
80
|
+
if (e.key === 'Escape') hideContextMenu();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function escapeHtml(s) {
|
|
84
|
+
return String(s).replace(/[&<>"']/g, (c) => ({
|
|
85
|
+
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
|
86
|
+
}[c]));
|
|
87
|
+
}
|