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,309 @@
1
+ /**
2
+ * Sortable List Component
3
+ * Location: ui/components/sortable_list.js
4
+ *
5
+ * Purpose: Provide a reusable list component with add, remove, reorder, and rename
6
+ * capabilities. Used for named inputs in function nodes, rules in switch nodes, etc.
7
+ *
8
+ * Features:
9
+ * - Add/remove items
10
+ * - Drag-and-drop reordering
11
+ * - Inline rename
12
+ * - Custom item renderer
13
+ * - Persistence via onChange callback
14
+ *
15
+ * Reordering is delegated to the shared DragReorder module (the same
16
+ * ghost + drop-placeholder technique used by the tab bars and the schema
17
+ * editor) so the reorder behaviour is implemented once.
18
+ */
19
+
20
+ import { DragReorder } from './drag_reorder.js';
21
+
22
+ export class SortableList {
23
+ /**
24
+ * Create a sortable list.
25
+ * @param {Object} options - Configuration options
26
+ * @param {string} options.containerId - Unique ID for the container
27
+ * @param {Array} options.items - Initial items array
28
+ * @param {Function} options.onChange - Called with updated items array when list changes
29
+ * @param {Function} options.renderItem - Custom renderer for item content (receives item, index, callbacks)
30
+ * @param {Function} options.createItem - Factory function to create new items
31
+ * @param {string} options.emptyMessage - Message to show when list is empty
32
+ * @param {string} options.addButtonText - Text for add button
33
+ * @param {boolean} options.allowReorder - Whether to allow drag-and-drop reordering
34
+ * @param {boolean} options.allowRemove - Whether to allow removing items
35
+ * @param {number} options.minItems - Minimum number of items (blocks removal if at min)
36
+ */
37
+ constructor({
38
+ containerId,
39
+ items = [],
40
+ onChange = null,
41
+ renderItem = null,
42
+ createItem = null,
43
+ emptyMessage = 'No items',
44
+ addButtonText = 'Add Item',
45
+ allowReorder = true,
46
+ allowRemove = true,
47
+ minItems = 0,
48
+ } = {}) {
49
+ this.containerId = containerId || `sortable-list-${Date.now()}`;
50
+ this.items = Array.isArray(items) ? [...items] : [];
51
+ this.onChange = typeof onChange === 'function' ? onChange : null;
52
+ this.renderItem = typeof renderItem === 'function' ? renderItem : this.#defaultRenderItem.bind(this);
53
+ this.createItem = typeof createItem === 'function' ? createItem : this.#defaultCreateItem.bind(this);
54
+ this.emptyMessage = emptyMessage;
55
+ this.addButtonText = addButtonText;
56
+ this.allowReorder = allowReorder;
57
+ this.allowRemove = allowRemove;
58
+ this.minItems = minItems;
59
+
60
+ this.root = null;
61
+ this.listEl = null;
62
+ this._dragReorder = null;
63
+ }
64
+
65
+ /**
66
+ * Render the sortable list.
67
+ * @returns {HTMLElement} The root container element
68
+ */
69
+ render() {
70
+ if (this.root) {
71
+ this.#refresh();
72
+ return this.root;
73
+ }
74
+
75
+ this.root = document.createElement('div');
76
+ this.root.className = 'twm-sortable-list';
77
+ this.root.id = this.containerId;
78
+
79
+ this.listEl = document.createElement('div');
80
+ this.listEl.className = 'twm-sortable-list__items';
81
+
82
+ this.root.appendChild(this.listEl);
83
+
84
+ // Only show add button if addButtonText is provided
85
+ if (this.addButtonText) {
86
+ const addBtn = document.createElement('button');
87
+ addBtn.type = 'button';
88
+ addBtn.className = 'twm-sortable-list__add';
89
+ addBtn.innerHTML = `<span class="material-symbols-outlined">add</span> ${this.#escapeHtml(this.addButtonText)}`;
90
+ addBtn.addEventListener('click', () => this.addItem());
91
+ this.root.appendChild(addBtn);
92
+ }
93
+
94
+ this.#refresh();
95
+ return this.root;
96
+ }
97
+
98
+ /**
99
+ * Get current items.
100
+ * @returns {Array} Copy of items array
101
+ */
102
+ getItems() {
103
+ return [...this.items];
104
+ }
105
+
106
+ /**
107
+ * Set items and re-render.
108
+ * @param {Array} items - New items array
109
+ */
110
+ setItems(items) {
111
+ this.items = Array.isArray(items) ? [...items] : [];
112
+ this.#refresh();
113
+ }
114
+
115
+ /**
116
+ * Add a new item.
117
+ */
118
+ addItem() {
119
+ const newItem = this.createItem(this.items.length);
120
+ this.items.push(newItem);
121
+ this.#notifyChange();
122
+ this.#refresh();
123
+ }
124
+
125
+ /**
126
+ * Remove an item by index.
127
+ * @param {number} index - Index of item to remove
128
+ */
129
+ removeItem(index) {
130
+ if (index < 0 || index >= this.items.length) return;
131
+ if (this.items.length <= this.minItems) return;
132
+
133
+ this.items.splice(index, 1);
134
+ this.#notifyChange();
135
+ this.#refresh();
136
+ }
137
+
138
+ /**
139
+ * Update an item at index.
140
+ * @param {number} index - Index of item to update
141
+ * @param {Object} updates - Properties to merge into item
142
+ */
143
+ updateItem(index, updates) {
144
+ if (index < 0 || index >= this.items.length) return;
145
+
146
+ this.items[index] = { ...this.items[index], ...updates };
147
+ this.#notifyChange();
148
+ }
149
+
150
+ /**
151
+ * Move an item from one index to another.
152
+ * @param {number} fromIndex - Source index
153
+ * @param {number} toIndex - Destination index
154
+ */
155
+ moveItem(fromIndex, toIndex) {
156
+ if (fromIndex < 0 || fromIndex >= this.items.length) return;
157
+ if (toIndex < 0 || toIndex >= this.items.length) return;
158
+ if (fromIndex === toIndex) return;
159
+
160
+ const [item] = this.items.splice(fromIndex, 1);
161
+ this.items.splice(toIndex, 0, item);
162
+ this.#notifyChange();
163
+ this.#refresh();
164
+ }
165
+
166
+ /**
167
+ * Dispose the component.
168
+ */
169
+ dispose() {
170
+ this._dragReorder?.destroy();
171
+ this._dragReorder = null;
172
+ this.root = null;
173
+ this.listEl = null;
174
+ this.items = [];
175
+ }
176
+
177
+ // ─── Private Methods ───────────────────────────────────────────────────
178
+
179
+ #refresh() {
180
+ if (!this.listEl) return;
181
+
182
+ this.listEl.innerHTML = '';
183
+
184
+ if (this.items.length === 0) {
185
+ const empty = document.createElement('div');
186
+ empty.className = 'twm-sortable-list__empty';
187
+ empty.textContent = this.emptyMessage;
188
+ this.listEl.appendChild(empty);
189
+ return;
190
+ }
191
+
192
+ this.items.forEach((item, index) => {
193
+ const row = this.#createRow(item, index);
194
+ this.listEl.appendChild(row);
195
+ });
196
+
197
+ // Drag-to-reorder via the shared DragReorder module (ghost +
198
+ // placeholder). Created once, bound to the persistent list
199
+ // element; re-attached after each refresh to wire the rebuilt
200
+ // rows. Drag is restricted to the grip handle.
201
+ if (this.allowReorder) {
202
+ if (!this._dragReorder) {
203
+ this._dragReorder = new DragReorder({
204
+ container: this.listEl,
205
+ itemSelector: '.sortable-list__row',
206
+ keyAttr: 'index',
207
+ axis: 'y',
208
+ indicatorClass: 'twm-sortable-list__drop',
209
+ draggableGuard: (e) => !!e.target.closest('.twm-sortable-list__handle'),
210
+ onReorder: (order) => this.#applyReorder(order),
211
+ });
212
+ }
213
+ this._dragReorder.attach();
214
+ }
215
+ }
216
+
217
+ /** Reorder `items` to match the new DOM order produced by a drag,
218
+ * then notify + re-render. `order` holds the rows' pre-drag
219
+ * `data-index` values in their new arrangement. */
220
+ #applyReorder(order) {
221
+ const next = order
222
+ .map((i) => this.items[Number(i)])
223
+ .filter((it) => it !== undefined);
224
+ if (next.length !== this.items.length) return; // guard against drift
225
+ this.items = next;
226
+ this.#notifyChange();
227
+ this.#refresh();
228
+ }
229
+
230
+ #createRow(item, index) {
231
+ const row = document.createElement('div');
232
+ row.className = 'twm-sortable-list__row';
233
+ row.dataset.index = index;
234
+
235
+ // Drag handle (if reordering enabled). The drag mechanics are
236
+ // owned by the shared DragReorder instance (see #refresh); the
237
+ // handle is just the grab affordance the drag is restricted to.
238
+ if (this.allowReorder) {
239
+ const handle = document.createElement('div');
240
+ handle.className = 'twm-sortable-list__handle';
241
+ handle.innerHTML = '<span class="material-symbols-outlined">drag_indicator</span>';
242
+ row.appendChild(handle);
243
+ }
244
+
245
+ // Item content (custom renderer)
246
+ const content = document.createElement('div');
247
+ content.className = 'twm-sortable-list__content';
248
+
249
+ const callbacks = {
250
+ update: (updates) => this.updateItem(index, updates),
251
+ remove: () => this.removeItem(index),
252
+ moveUp: () => this.moveItem(index, index - 1),
253
+ moveDown: () => this.moveItem(index, index + 1),
254
+ };
255
+
256
+ this.renderItem(content, item, index, callbacks);
257
+ row.appendChild(content);
258
+
259
+ // Remove button (if removal enabled)
260
+ if (this.allowRemove) {
261
+ const canRemove = this.items.length > this.minItems;
262
+ const removeBtn = document.createElement('button');
263
+ removeBtn.type = 'button';
264
+ removeBtn.className = 'twm-sortable-list__remove';
265
+ removeBtn.disabled = !canRemove;
266
+ removeBtn.title = canRemove ? 'Remove' : `Minimum ${this.minItems} items required`;
267
+ removeBtn.innerHTML = '<span class="material-symbols-outlined">close</span>';
268
+ removeBtn.addEventListener('click', () => {
269
+ if (canRemove) this.removeItem(index);
270
+ });
271
+ row.appendChild(removeBtn);
272
+ }
273
+
274
+ return row;
275
+ }
276
+
277
+ #defaultRenderItem(container, item, index, callbacks) {
278
+ const input = document.createElement('input');
279
+ input.type = 'text';
280
+ input.className = 'twm-sortable-list__input';
281
+ input.value = item.name ?? item.label ?? '';
282
+ input.placeholder = `Item ${index + 1}`;
283
+ input.addEventListener('change', () => {
284
+ callbacks.update({ name: input.value });
285
+ });
286
+ container.appendChild(input);
287
+ }
288
+
289
+ #defaultCreateItem(index) {
290
+ return {
291
+ id: `item_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`,
292
+ name: `item${index + 1}`,
293
+ };
294
+ }
295
+
296
+ #notifyChange() {
297
+ if (this.onChange) {
298
+ this.onChange([...this.items]);
299
+ }
300
+ }
301
+
302
+
303
+ #escapeHtml(str) {
304
+ return String(str ?? '')
305
+ .replace(/&/g, '&amp;')
306
+ .replace(/</g, '&lt;')
307
+ .replace(/>/g, '&gt;');
308
+ }
309
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * table_state_store.js — persist DataTable view-state (sort, filters,
3
+ * column widths), keyed by a caller-supplied `persistKey`.
4
+ *
5
+ * A FACTORY, not a singleton: the store reads and writes through a Host's
6
+ * `state` capability (see ui/js/host/host.js) under one logical state key.
7
+ * One blob holds a map of `{ [persistKey]: state }` for every persisted table.
8
+ * Where that key lands on disk is the embedder's business, not ours — it is
9
+ * decided by the host's `resolvePath`.
10
+ *
11
+ * A host without a `state` capability is legal: tables simply stay ephemeral.
12
+ *
13
+ * The in-memory cache must be dropped when the project changes (the app swaps
14
+ * projects in-place without a page reload) — `reset()` is wired to the
15
+ * `project:opened` event in app_bootstrap.
16
+ */
17
+
18
+ /**
19
+ * @param {object} opts
20
+ * @param {object} opts.host a Host. May lack `state` -> tables become ephemeral.
21
+ * @param {string} opts.key logical state key, e.g. 'datatable_state'
22
+ * @param {object} [opts.logger]
23
+ * @param {number} [opts.debounceMs=500]
24
+ */
25
+ export function createTableStateStore({ host, key, logger = console, debounceMs = 500 }) {
26
+ if (!key) throw new Error('createTableStateStore requires { key }');
27
+ const state = host?.state || null;
28
+
29
+ let cache = null; // { [persistKey]: state } — null until first load
30
+ let loadPromise = null;
31
+ let saveTimer = null;
32
+
33
+ const scheduleSave = () => {
34
+ if (saveTimer) clearTimeout(saveTimer);
35
+ saveTimer = setTimeout(async () => {
36
+ saveTimer = null;
37
+ if (!state || !cache) return;
38
+ try { await state.write(key, cache); }
39
+ catch (err) { logger.warn?.('[table-state] save failed', err); }
40
+ }, debounceMs);
41
+ };
42
+
43
+ return {
44
+ /** Resolve once the persisted blob has been read into the cache. Safe
45
+ * to call repeatedly — the read happens at most once per project. */
46
+ ready() {
47
+ if (loadPromise) return loadPromise;
48
+ loadPromise = (async () => {
49
+ if (!state) { cache = {}; return cache; }
50
+ try { cache = (await state.read(key)) || {}; }
51
+ catch (err) { logger.warn?.('[table-state] load failed', err); cache = {}; }
52
+ return cache;
53
+ })();
54
+ return loadPromise;
55
+ },
56
+
57
+ /** Synchronous read of a table's persisted state. Returns null until
58
+ * the cache is loaded (await `ready()` first to be sure). */
59
+ get(k) {
60
+ if (!k || !cache) return null;
61
+ return cache[k] || null;
62
+ },
63
+
64
+ /** Merge-and-persist a table's state (debounced). Pass null to forget
65
+ * a key. Writes the whole map back atomically through the host. */
66
+ set(k, s) {
67
+ if (!k) return;
68
+ if (!cache) cache = {};
69
+ if (s == null) delete cache[k]; else cache[k] = s;
70
+ scheduleSave();
71
+ },
72
+
73
+ /** Drop the cache + pending read so the next access reloads from the
74
+ * newly-opened project. Cancels any in-flight save for the old one. */
75
+ reset() {
76
+ cache = null;
77
+ loadPromise = null;
78
+ if (saveTimer) { clearTimeout(saveTimer); saveTimer = null; }
79
+ },
80
+ };
81
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * toast.js — thin wrapper around the global NotificationCenter for
3
+ * callers that don't already hold an eventBus reference.
4
+ *
5
+ * The NotificationCenter (../notification_center.js) subscribes to
6
+ * `toast:show` on the global event bus; we re-export tiny helpers
7
+ * that emit the same payload shape.
8
+ *
9
+ * Usage:
10
+ * import { toastError, toastSuccess, toastWarn, toastInfo } from './toast.js';
11
+ * toastError('Save failed', 'sector_update refused: …');
12
+ *
13
+ * The 1-arg form treats the string as the message (no title); the
14
+ * 2-arg form is title + message.
15
+ */
16
+
17
+ function _bus() {
18
+ return window.__ecoagent?.eventBus ?? null;
19
+ }
20
+
21
+ function _emit(severity, persistent, titleOrMsg, msg) {
22
+ const bus = _bus();
23
+ const title = (msg !== undefined) ? titleOrMsg : null;
24
+ const message = (msg !== undefined) ? msg : titleOrMsg;
25
+ if (!bus) {
26
+ // Fall back to console so the message isn't lost if the bus is
27
+ // unavailable (early bootstrap, isolated test pages).
28
+ // eslint-disable-next-line no-console
29
+ (severity === 'error' || severity === 'warn' ? console.warn : console.info)(
30
+ `[toast/${severity}]`, title ? `${title}:` : '', message);
31
+ return;
32
+ }
33
+ bus.emit('toast:show', {
34
+ title, message, severity,
35
+ persistent,
36
+ durationMs: persistent ? 0 : 5000,
37
+ });
38
+ }
39
+
40
+ export function toastError(titleOrMsg, msg) { _emit('error', true, titleOrMsg, msg); }
41
+ export function toastWarn(titleOrMsg, msg) { _emit('warn', false, titleOrMsg, msg); }
42
+ export function toastInfo(titleOrMsg, msg) { _emit('info', false, titleOrMsg, msg); }
43
+ export function toastSuccess(titleOrMsg, msg) { _emit('success', false, titleOrMsg, msg); }
44
+
45
+ /** Coerce a bridge-call result/exception into a user-facing error toast.
46
+ * Drops the toast only if `res?.ok === false` *or* an exception was caught.
47
+ * Returns true if the call succeeded (caller can continue), false otherwise. */
48
+ export function reportBridgeError(opName, resOrErr) {
49
+ if (!resOrErr) return true;
50
+ if (resOrErr instanceof Error) {
51
+ toastError(opName, resOrErr.message || String(resOrErr));
52
+ return false;
53
+ }
54
+ if (resOrErr && resOrErr.ok === false) {
55
+ toastError(opName, resOrErr.error || 'operation refused');
56
+ return false;
57
+ }
58
+ return true;
59
+ }