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.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -0
  3. package/THIRD_PARTY_LICENSES.md +53 -0
  4. package/css/base.css +3439 -0
  5. package/css/flexdesk.css +5797 -0
  6. package/css/overrides.css +2349 -0
  7. package/css/reset.css +46 -0
  8. package/css/tokens.css +137 -0
  9. package/dist/charts.js +780 -0
  10. package/dist/charts.js.map +7 -0
  11. package/dist/chunk-3PHCPZHT.js +222 -0
  12. package/dist/chunk-3PHCPZHT.js.map +7 -0
  13. package/dist/chunk-CT4YXXLP.js +1770 -0
  14. package/dist/chunk-CT4YXXLP.js.map +7 -0
  15. package/dist/chunk-DRYCDMEG.js +2217 -0
  16. package/dist/chunk-DRYCDMEG.js.map +7 -0
  17. package/dist/chunk-DVU44T77.js +855 -0
  18. package/dist/chunk-DVU44T77.js.map +7 -0
  19. package/dist/chunk-FL5KFNQH.js +640 -0
  20. package/dist/chunk-FL5KFNQH.js.map +7 -0
  21. package/dist/chunk-FOOS3T5L.js +117 -0
  22. package/dist/chunk-FOOS3T5L.js.map +7 -0
  23. package/dist/chunk-JYWURG5T.js +32 -0
  24. package/dist/chunk-JYWURG5T.js.map +7 -0
  25. package/dist/chunk-TLZUUFOE.js +276 -0
  26. package/dist/chunk-TLZUUFOE.js.map +7 -0
  27. package/dist/chunk-UCJ2WD4D.js +625 -0
  28. package/dist/chunk-UCJ2WD4D.js.map +7 -0
  29. package/dist/chunk-WVFGV5FT.js +205 -0
  30. package/dist/chunk-WVFGV5FT.js.map +7 -0
  31. package/dist/core.js +1098 -0
  32. package/dist/core.js.map +7 -0
  33. package/dist/editor.js +1661 -0
  34. package/dist/editor.js.map +7 -0
  35. package/dist/flexdesk.css +5797 -0
  36. package/dist/host.js +132 -0
  37. package/dist/host.js.map +7 -0
  38. package/dist/reset.css +46 -0
  39. package/dist/tile_registry-6WZPTUZV.js +28 -0
  40. package/dist/tile_registry-6WZPTUZV.js.map +7 -0
  41. package/dist/tiles.js +2068 -0
  42. package/dist/tiles.js.map +7 -0
  43. package/dist/tokens.css +137 -0
  44. package/dist/widgets.js +5438 -0
  45. package/dist/widgets.js.map +7 -0
  46. package/dist/wm.js +4830 -0
  47. package/dist/wm.js.map +7 -0
  48. package/package.json +80 -0
  49. package/src/charts/chart_types.js +699 -0
  50. package/src/charts/data_series_plot_window.js +761 -0
  51. package/src/charts/downsample.js +119 -0
  52. package/src/charts/plot_config.js +176 -0
  53. package/src/charts/plot_popout_window.js +1560 -0
  54. package/src/charts/plotly_wrapper.js +432 -0
  55. package/src/core/event_bus.js +230 -0
  56. package/src/core/logging.js +300 -0
  57. package/src/core/settings.js +813 -0
  58. package/src/core/state_guard.js +382 -0
  59. package/src/core/state_guard_config.js +309 -0
  60. package/src/core/state_machine.js +258 -0
  61. package/src/editor/editor_pane.js +264 -0
  62. package/src/editor/monaco_editor_factory.js +449 -0
  63. package/src/editor/monaco_loader.js +74 -0
  64. package/src/editor/notebook_search.js +460 -0
  65. package/src/editor/notebook_tab_bar.js +456 -0
  66. package/src/editor/notebook_undo_manager.js +108 -0
  67. package/src/editor/split_pane_container.js +197 -0
  68. package/src/help/help_modal.js +409 -0
  69. package/src/help/help_registry.js +65 -0
  70. package/src/host/host.js +69 -0
  71. package/src/host/pywebview_host.js +133 -0
  72. package/src/tiles/config_schema.js +256 -0
  73. package/src/tiles/layout_persistence.js +662 -0
  74. package/src/tiles/tile_base.js +785 -0
  75. package/src/tiles/tile_grid.js +905 -0
  76. package/src/tiles/tile_registry.js +136 -0
  77. package/src/tiling/command_palette.js +328 -0
  78. package/src/tiling/content_registry.js +110 -0
  79. package/src/tiling/desktops.js +133 -0
  80. package/src/tiling/entity_sources.js +179 -0
  81. package/src/tiling/keymap.js +156 -0
  82. package/src/tiling/kind_taxonomy.js +172 -0
  83. package/src/tiling/landing_table.js +717 -0
  84. package/src/tiling/loading_overlay.js +33 -0
  85. package/src/tiling/nav_panel.js +142 -0
  86. package/src/tiling/page_factory.js +215 -0
  87. package/src/tiling/panel_keys.js +145 -0
  88. package/src/tiling/shell.js +437 -0
  89. package/src/tiling/tile_breadcrumb.js +222 -0
  90. package/src/tiling/tile_renderer.js +459 -0
  91. package/src/tiling/tile_tab_menu.js +340 -0
  92. package/src/tiling/tile_tree.js +880 -0
  93. package/src/tiling/wm.js +1352 -0
  94. package/src/ui/base/component_base.js +30 -0
  95. package/src/ui/base/controller_base.js +48 -0
  96. package/src/ui/base/page_base.js +102 -0
  97. package/src/ui/components/about_dialog.js +224 -0
  98. package/src/ui/components/action_dropdown.js +389 -0
  99. package/src/ui/components/attribute_list_editor.js +652 -0
  100. package/src/ui/components/autocomplete_field.js +732 -0
  101. package/src/ui/components/computing_status_window.js +81 -0
  102. package/src/ui/components/confirm_dialog.js +307 -0
  103. package/src/ui/components/context_menu.js +87 -0
  104. package/src/ui/components/data_table.js +2228 -0
  105. package/src/ui/components/detail_header.js +177 -0
  106. package/src/ui/components/drag_reorder.js +0 -0
  107. package/src/ui/components/gallery_picker.js +147 -0
  108. package/src/ui/components/inline_renamer.js +129 -0
  109. package/src/ui/components/managed_window.js +804 -0
  110. package/src/ui/components/modal.js +512 -0
  111. package/src/ui/components/notification_history.js +264 -0
  112. package/src/ui/components/slide_out_panel.js +185 -0
  113. package/src/ui/components/slider_field.js +122 -0
  114. package/src/ui/components/sortable_list.js +309 -0
  115. package/src/ui/components/table_state_store.js +81 -0
  116. package/src/ui/components/toast.js +59 -0
  117. package/src/ui/components/tree_view.js +460 -0
  118. package/src/ui/components/window_taskbar.js +143 -0
  119. package/src/ui/controllers/panel_state_machine.js +634 -0
  120. package/src/ui/controllers/window_chrome_controller.js +659 -0
  121. package/src/ui/notification_center.js +576 -0
  122. package/src/ui/utils/overlay_scrollbar.js +492 -0
  123. package/src/ui/utils/raf_resize_observer.js +31 -0
  124. 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, '&amp;')
181
+ .replace(/</g, '&lt;')
182
+ .replace(/>/g, '&gt;')
183
+ .replace(/"/g, '&quot;');
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
+ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
86
+ }[c]));
87
+ }