flexdesk 0.16.1 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +93 -0
- package/LICENSE +0 -0
- package/README.md +2217 -42
- package/THIRD_PARTY_LICENSES.md +0 -0
- package/css/base.css +1834 -75
- package/css/flexdesk.css +3146 -164
- package/css/overrides.css +16 -0
- package/css/reset.css +0 -0
- package/css/tokens.css +0 -0
- package/dist/canvas.js +6 -239
- package/dist/canvas.js.map +3 -3
- package/dist/charts.js +709 -18
- package/dist/charts.js.map +4 -4
- package/dist/chunk-3PHCPZHT.js +0 -0
- package/dist/chunk-3PHCPZHT.js.map +0 -0
- package/dist/chunk-4A7SJC3T.js +503 -0
- package/dist/chunk-4A7SJC3T.js.map +7 -0
- package/dist/{chunk-6JSOVNID.js → chunk-CVMCZZX2.js} +635 -10
- package/dist/chunk-CVMCZZX2.js.map +7 -0
- package/dist/chunk-D3COLZZ2.js +1311 -0
- package/dist/chunk-D3COLZZ2.js.map +7 -0
- package/dist/chunk-FOOS3T5L.js +0 -0
- package/dist/chunk-FOOS3T5L.js.map +0 -0
- package/dist/{chunk-SN4LSOAR.js → chunk-GOQLR5WR.js} +452 -2
- package/dist/chunk-GOQLR5WR.js.map +7 -0
- package/dist/{chunk-FL5KFNQH.js → chunk-HNU6MM2B.js} +52 -17
- package/dist/chunk-HNU6MM2B.js.map +7 -0
- package/dist/chunk-HYJYYEPK.js +375 -0
- package/dist/chunk-HYJYYEPK.js.map +7 -0
- package/dist/chunk-JKZLNKG6.js +2041 -0
- package/dist/chunk-JKZLNKG6.js.map +7 -0
- package/dist/chunk-JYWURG5T.js +0 -0
- package/dist/chunk-JYWURG5T.js.map +0 -0
- package/dist/{chunk-RQM5Z2MU.js → chunk-MCIXFNVF.js} +633 -311
- package/dist/chunk-MCIXFNVF.js.map +7 -0
- package/dist/chunk-N7HC3ADV.js +30 -0
- package/dist/chunk-N7HC3ADV.js.map +7 -0
- package/dist/{chunk-FBGP5KN5.js → chunk-Q36EPT2R.js} +3268 -203
- package/dist/chunk-Q36EPT2R.js.map +7 -0
- package/dist/chunk-QWQJUDPD.js +610 -0
- package/dist/chunk-QWQJUDPD.js.map +7 -0
- package/dist/{chunk-NLTOY57M.js → chunk-UURZ37SD.js} +28 -48
- package/dist/chunk-UURZ37SD.js.map +7 -0
- package/dist/{chunk-B5Q53LAJ.js → chunk-UWO43BZU.js} +2 -2
- package/dist/chunk-UWO43BZU.js.map +7 -0
- package/dist/chunk-WVFGV5FT.js +0 -0
- package/dist/chunk-WVFGV5FT.js.map +0 -0
- package/dist/core.js +170 -118
- package/dist/core.js.map +2 -2
- package/dist/editor.js +156 -70
- package/dist/editor.js.map +2 -2
- package/dist/flexdesk.css +3146 -164
- package/dist/flow.js +2789 -306
- package/dist/flow.js.map +4 -4
- package/dist/host.js +109 -3
- package/dist/host.js.map +3 -3
- package/dist/reset.css +0 -0
- package/dist/tile_registry-6WZPTUZV.js +0 -0
- package/dist/tile_registry-6WZPTUZV.js.map +0 -0
- package/dist/tiles.js +657 -234
- package/dist/tiles.js.map +4 -4
- package/dist/tokens.css +0 -0
- package/dist/widgets.js +3252 -2124
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +1227 -347
- package/dist/wm.js.map +4 -4
- package/package.json +3 -1
- package/src/canvas/canvas_adapter.js +5 -0
- package/src/canvas/edge_router.js +0 -0
- package/src/canvas/graph/connector_router.js +0 -0
- package/src/canvas/graph/history_service.js +0 -0
- package/src/canvas/graph/node_platform.js +0 -0
- package/src/canvas/index.js +1 -0
- package/src/canvas/layered_layout.js +244 -0
- package/src/charts/chart_types.js +0 -0
- package/src/charts/data_series_plot_window.js +0 -0
- package/src/charts/downsample.js +2 -2
- package/src/charts/plot_config.js +53 -29
- package/src/charts/plot_config_editor.js +627 -0
- package/src/charts/plot_grid.js +449 -0
- package/src/charts/plot_popout_window.js +103 -317
- package/src/charts/plotly_wrapper.js +22 -2
- package/src/charts/series_transforms.js +206 -0
- package/src/core/event_bus.js +0 -0
- package/src/core/logging.js +1 -1
- package/src/core/settings.js +105 -19
- package/src/core/state_guard.js +93 -51
- package/src/core/state_guard_config.js +85 -74
- package/src/core/state_machine.js +0 -0
- package/src/editor/editor_pane.js +3 -3
- package/src/editor/monaco_editor_factory.js +124 -60
- package/src/editor/monaco_loader.js +103 -28
- package/src/editor/notebook_search.js +0 -0
- package/src/editor/notebook_tab_bar.js +38 -52
- package/src/editor/notebook_undo_manager.js +0 -0
- package/src/editor/split_pane_container.js +6 -3
- package/src/flow/canvas_editor/connections.js +90 -0
- package/src/flow/canvas_editor/editor.js +2351 -153
- package/src/flow/canvas_editor/index.js +2 -1
- package/src/flow/canvas_editor/layout.js +119 -12
- package/src/flow/canvas_editor/strings.js +80 -0
- package/src/flow/kit/always_before.js +0 -0
- package/src/flow/kit/catalogue.js +97 -3
- package/src/flow/kit/chip_input.js +0 -0
- package/src/flow/kit/dom.js +0 -0
- package/src/flow/kit/findings.js +0 -0
- package/src/flow/kit/history.js +0 -0
- package/src/flow/kit/index.js +3 -3
- package/src/flow/kit/keys.js +7 -1
- package/src/flow/kit/logic_graph.js +97 -15
- package/src/flow/kit/references.js +0 -0
- package/src/flow/kit/settings_panel.js +0 -0
- package/src/flow/kit/settings_schema.js +2 -1
- package/src/flow/kit/step_picker.js +0 -0
- package/src/flow/kit/strings.js +3 -1
- package/src/flow/kit/value_picker.js +0 -0
- package/src/flow/kit/widgets.js +86 -109
- package/src/flow/lanes/editor.js +136 -14
- package/src/flow/lanes/index.js +3 -1
- package/src/flow/lanes/layout.js +0 -0
- package/src/flow/lanes/pipeline.js +44 -2
- package/src/flow/lanes/preview.js +0 -0
- package/src/flow/lanes/strings.js +1 -0
- package/src/flow/outline/editor.js +0 -0
- package/src/flow/outline/index.js +0 -0
- package/src/flow/outline/mapping.js +0 -0
- package/src/flow/outline/menu.js +0 -0
- package/src/flow/outline/operations.js +0 -0
- package/src/flow/outline/recognise.js +0 -0
- package/src/flow/outline/strings.js +0 -0
- package/src/help/help_modal.js +168 -42
- package/src/help/help_registry.js +12 -1
- package/src/host/external_links.js +0 -0
- package/src/host/file_picker.js +117 -0
- package/src/host/host.js +10 -2
- package/src/host/photino_host.js +9 -0
- package/src/host/photino_host.md +21 -3
- package/src/host/pywebview_host.js +10 -0
- package/src/tiles/chart_tiles.js +53 -245
- package/src/tiles/config_schema.js +8 -0
- package/src/tiles/figure_tile.js +322 -0
- package/src/tiles/layout_persistence.js +3 -3
- package/src/tiles/list_tiles.js +7 -7
- package/src/tiles/scope_picker.js +0 -0
- package/src/tiles/tile_base.js +1 -1
- package/src/tiles/tile_config_form.js +442 -0
- package/src/tiles/tile_grid.js +2 -0
- package/src/tiles/tile_registry.js +0 -0
- package/src/tiling/bottom_panel_tabs.js +67 -99
- package/src/tiling/command_palette.js +64 -19
- package/src/tiling/content_registry.js +4 -1
- package/src/tiling/desktops.js +11 -1
- package/src/tiling/entity_sources.js +0 -0
- package/src/tiling/keymap.js +403 -128
- package/src/tiling/kind_taxonomy.js +78 -0
- package/src/tiling/landing_table.js +0 -0
- package/src/tiling/loading_overlay.js +0 -0
- package/src/tiling/nav_panel.js +59 -10
- package/src/tiling/page_factory.js +5 -5
- package/src/tiling/panel_availability.js +49 -0
- package/src/tiling/panel_keys.js +0 -0
- package/src/tiling/shell.js +114 -12
- package/src/tiling/tab_strip.js +0 -0
- package/src/tiling/tile_breadcrumb.js +0 -0
- package/src/tiling/tile_renderer.js +105 -9
- package/src/tiling/tile_tab_menu.js +0 -0
- package/src/tiling/tile_tree.js +0 -0
- package/src/tiling/wm.js +562 -30
- package/src/tiling/zoom.js +0 -0
- package/src/ui/base/component_base.js +0 -0
- package/src/ui/base/controller_base.js +0 -0
- package/src/ui/base/page_base.js +0 -0
- package/src/ui/components/about_dialog.js +0 -0
- package/src/ui/components/action_dropdown.js +30 -2
- package/src/ui/components/alert_log.js +0 -0
- package/src/ui/components/alerts_pane.js +0 -0
- package/src/ui/components/attribute_list_editor.js +0 -0
- package/src/ui/components/autocomplete_field.js +0 -0
- package/src/ui/components/cell_editor_host.js +211 -0
- package/src/ui/components/cell_grid.js +648 -0
- package/src/ui/components/cell_selection.js +220 -0
- package/src/ui/components/choice_controls.js +351 -0
- package/src/ui/components/color_field.js +211 -0
- package/src/ui/components/color_list_field.js +210 -0
- package/src/ui/components/column_fit.js +0 -0
- package/src/ui/components/column_groups.js +314 -0
- package/src/ui/components/column_order.js +142 -0
- package/src/ui/components/computing_status_window.js +98 -25
- package/src/ui/components/confirm_dialog.js +81 -38
- package/src/ui/components/context_menu.js +2 -2
- package/src/ui/components/conversation_view.js +440 -0
- package/src/ui/components/crosstab.js +289 -0
- package/src/ui/components/data_table.js +601 -62
- package/src/ui/components/detail_header.js +22 -11
- package/src/ui/components/document_outline.js +397 -0
- package/src/ui/components/drag_reorder.js +0 -0
- package/src/ui/components/fact_table.js +0 -0
- package/src/ui/components/fill_handle.js +322 -0
- package/src/ui/components/gallery_picker.js +19 -8
- package/src/ui/components/header_actions.js +55 -0
- package/src/ui/components/inline_renamer.js +0 -0
- package/src/ui/components/job_progress.js +51 -0
- package/src/ui/components/managed_window.js +27 -4
- package/src/ui/components/menu_bar.js +745 -0
- package/src/ui/components/modal.js +449 -75
- package/src/ui/components/notification_history.js +3 -3
- package/src/ui/components/progress.js +483 -0
- package/src/ui/components/record_list.js +0 -0
- package/src/ui/components/row_activation.js +0 -0
- package/src/ui/components/section_stack.js +298 -0
- package/src/ui/components/series.js +173 -0
- package/src/ui/components/settings_view.js +634 -0
- package/src/ui/components/slide_out_panel.js +43 -6
- package/src/ui/components/slider_field.js +275 -18
- package/src/ui/components/sortable_list.js +0 -0
- package/src/ui/components/staged_edits.js +343 -0
- package/src/ui/components/staged_history.js +172 -0
- package/src/ui/components/table_filter_chips.js +0 -0
- package/src/ui/components/table_state_store.js +0 -0
- package/src/ui/components/table_structure.js +182 -0
- package/src/ui/components/table_values.js +0 -0
- package/src/ui/components/tabs.js +497 -0
- package/src/ui/components/tag_input.js +215 -0
- package/src/ui/components/toast.js +25 -8
- package/src/ui/components/toggle_field.js +175 -0
- package/src/ui/components/tree_view.js +55 -34
- package/src/ui/components/window_taskbar.js +0 -0
- package/src/ui/components/wizard.js +563 -0
- package/src/ui/controllers/window_chrome_controller.js +2 -2
- package/src/ui/notification_center.js +375 -119
- package/src/ui/utils/auto_scrollbars.js +0 -0
- package/src/ui/utils/markdown.js +709 -0
- package/src/ui/utils/native_context_menu.js +0 -0
- package/src/ui/utils/overlay_scrollbar.js +16 -12
- package/src/ui/utils/raf_resize_observer.js +0 -0
- package/src/ui/utils/scroll_lock.js +0 -0
- package/src/ui/utils/strip_scroll.js +55 -0
- package/src/ui/utils/tooltip_service.js +0 -8
- package/dist/chunk-6JSOVNID.js.map +0 -7
- package/dist/chunk-B5Q53LAJ.js.map +0 -7
- package/dist/chunk-FBGP5KN5.js.map +0 -7
- package/dist/chunk-FL5KFNQH.js.map +0 -7
- package/dist/chunk-NLTOY57M.js.map +0 -7
- package/dist/chunk-NNFTETK3.js +0 -633
- package/dist/chunk-NNFTETK3.js.map +0 -7
- package/dist/chunk-O5OHMWBB.js +0 -284
- package/dist/chunk-O5OHMWBB.js.map +0 -7
- package/dist/chunk-RQM5Z2MU.js.map +0 -7
- package/dist/chunk-SN4LSOAR.js.map +0 -7
- package/dist/chunk-W6FBJLPW.js +0 -367
- package/dist/chunk-W6FBJLPW.js.map +0 -7
- package/src/ui/controllers/panel_state_machine.js +0 -634
|
@@ -13,11 +13,30 @@
|
|
|
13
13
|
* Source Material
|
|
14
14
|
* - html/js/status_view.js and ui_manager.js toast helpers.
|
|
15
15
|
* - html/js/ui_utilities.js notification utilities.
|
|
16
|
+
*
|
|
17
|
+
* Progress toasts (0.17.0)
|
|
18
|
+
* ------------------------
|
|
19
|
+
* `show({ progress })` makes a toast that tracks a job: `progress` is a 0..1
|
|
20
|
+
* fraction, `{ value, total }`, or `null` for indeterminate; `status` is a line
|
|
21
|
+
* under the message and `counts` a label beside the bar. Such a toast is
|
|
22
|
+
* persistent and collapsible, and it RUNS until `update()` gives it an outcome
|
|
23
|
+
* severity (success, error or fatal), which ends its bar (done or error) for
|
|
24
|
+
* good and lets a `durationMs` in the same patch dismiss it. Every toast is
|
|
25
|
+
* drawn once and then patched: `update()` touches only its own nodes, and a
|
|
26
|
+
* toast arriving or leaving moves no other toast's element, so a bar, a
|
|
27
|
+
* collapsed state and a hovered button survive. A running progress toast is
|
|
28
|
+
* never held behind "+N more" (and never dropped by 'drop'); it counts toward
|
|
29
|
+
* `maxVisible`, so it takes the place of the newest other toast.
|
|
16
30
|
*/
|
|
17
31
|
import { ComponentBase } from './base/component_base.js';
|
|
18
32
|
import { getSetting } from '../core/settings.js';
|
|
33
|
+
import { setToastBus, getToastBus } from './components/toast.js';
|
|
34
|
+
import { ProgressBar } from './components/progress.js';
|
|
19
35
|
|
|
20
36
|
const SEVERITIES = ['info', 'success', 'warn', 'error', 'fatal'];
|
|
37
|
+
|
|
38
|
+
/** The severity a progress toast finishes with, and the state its bar ends in. */
|
|
39
|
+
const OUTCOME_BAR_STATE = Object.freeze({ success: 'done', error: 'error', fatal: 'error' });
|
|
21
40
|
const LOG_SEVERITY_MAP = {
|
|
22
41
|
trace: 'info',
|
|
23
42
|
debug: 'info',
|
|
@@ -34,6 +53,9 @@ const DEFAULT_LOG_LEVELS = new Set(['warn', 'error', 'fatal']);
|
|
|
34
53
|
const SUPPRESSED_NAMESPACE_TITLES = new Set(['bootstrap', 'app']);
|
|
35
54
|
|
|
36
55
|
export class NotificationCenter extends ComponentBase {
|
|
56
|
+
#items;
|
|
57
|
+
#summaryEl;
|
|
58
|
+
|
|
37
59
|
constructor({
|
|
38
60
|
eventBus,
|
|
39
61
|
logger,
|
|
@@ -65,6 +87,9 @@ export class NotificationCenter extends ComponentBase {
|
|
|
65
87
|
this.sequence = 0;
|
|
66
88
|
this.activeTimers = new Map();
|
|
67
89
|
this.eventDisposers = [];
|
|
90
|
+
/** id -> the drawn toast's nodes, for the toasts on screen. */
|
|
91
|
+
this.#items = new Map();
|
|
92
|
+
this.#summaryEl = null;
|
|
68
93
|
|
|
69
94
|
this.container = null;
|
|
70
95
|
this.rootEl = null;
|
|
@@ -83,6 +108,7 @@ export class NotificationCenter extends ComponentBase {
|
|
|
83
108
|
this.liveRegionEl = existingContainer.querySelector('.notification-center__live-region');
|
|
84
109
|
this._mounted = true;
|
|
85
110
|
this.#subscribeEvents();
|
|
111
|
+
this.#claimToastBus();
|
|
86
112
|
return this;
|
|
87
113
|
}
|
|
88
114
|
|
|
@@ -114,6 +140,7 @@ export class NotificationCenter extends ComponentBase {
|
|
|
114
140
|
|
|
115
141
|
this._mounted = true;
|
|
116
142
|
this.#subscribeEvents();
|
|
143
|
+
this.#claimToastBus();
|
|
117
144
|
|
|
118
145
|
if (Array.isArray(props.initialNotifications)) {
|
|
119
146
|
props.initialNotifications.forEach((notification) => this.show(notification));
|
|
@@ -124,6 +151,12 @@ export class NotificationCenter extends ComponentBase {
|
|
|
124
151
|
dispose() {
|
|
125
152
|
this.#clearTimers();
|
|
126
153
|
this.#unsubscribeEvents();
|
|
154
|
+
// Give the toast helpers back to the console — unless someone has
|
|
155
|
+
// pointed them at another bus since this centre mounted.
|
|
156
|
+
if (this.eventBus && getToastBus() === this.eventBus) setToastBus(null);
|
|
157
|
+
this.#items.forEach((entry) => entry.bar?.dispose());
|
|
158
|
+
this.#items.clear();
|
|
159
|
+
this.#summaryEl = null;
|
|
127
160
|
if (this.rootEl?.parentNode) {
|
|
128
161
|
this.rootEl.parentNode.removeChild(this.rootEl);
|
|
129
162
|
}
|
|
@@ -137,7 +170,8 @@ export class NotificationCenter extends ComponentBase {
|
|
|
137
170
|
show(notification) {
|
|
138
171
|
const normalized = this.#normalizeNotification(notification);
|
|
139
172
|
this.#consoleLog(normalized);
|
|
140
|
-
|
|
173
|
+
const running = NotificationCenter.#isRunning(normalized);
|
|
174
|
+
if (this.overflow === 'collapse' && !running && this.notifications.length >= this.maxVisible) {
|
|
141
175
|
// Held, not shown: its timer starts when it comes on screen.
|
|
142
176
|
this.held.push(normalized);
|
|
143
177
|
this.#recordHistory(normalized);
|
|
@@ -146,10 +180,18 @@ export class NotificationCenter extends ComponentBase {
|
|
|
146
180
|
return normalized.id;
|
|
147
181
|
}
|
|
148
182
|
this.notifications.push(normalized);
|
|
149
|
-
if (this.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
this.#
|
|
183
|
+
if (this.overflow === 'collapse') {
|
|
184
|
+
// Only a running progress toast gets here past the limit: the
|
|
185
|
+
// newest other toast steps back behind "+N more" to make room.
|
|
186
|
+
this.#rebalance();
|
|
187
|
+
} else if (this.notifications.length > this.maxVisible) {
|
|
188
|
+
// The oldest leaves — but never a running progress toast.
|
|
189
|
+
const idx = this.notifications.findIndex((item) => !NotificationCenter.#isRunning(item));
|
|
190
|
+
if (idx !== -1) {
|
|
191
|
+
const [removed] = this.notifications.splice(idx, 1);
|
|
192
|
+
this.#stopTimer(removed.id);
|
|
193
|
+
this.#recordHistory({ ...removed, dismissed: true, reason: 'overflow' });
|
|
194
|
+
}
|
|
153
195
|
}
|
|
154
196
|
|
|
155
197
|
this.#recordHistory(normalized);
|
|
@@ -173,50 +215,93 @@ export class NotificationCenter extends ComponentBase {
|
|
|
173
215
|
this.#stopTimer(notificationId);
|
|
174
216
|
this.#recordHistory({ ...removed, dismissed: true, reason });
|
|
175
217
|
// A place on screen came free: the oldest held-back toast takes it.
|
|
176
|
-
|
|
177
|
-
const next = this.held.shift();
|
|
178
|
-
this.notifications.push(next);
|
|
179
|
-
this.#scheduleAutoDismiss(next);
|
|
180
|
-
}
|
|
218
|
+
this.#rebalance();
|
|
181
219
|
this.#render();
|
|
182
220
|
return true;
|
|
183
221
|
}
|
|
184
222
|
|
|
185
223
|
/**
|
|
186
|
-
*
|
|
187
|
-
*
|
|
224
|
+
* Change a notification in place: only its own nodes are touched, and no
|
|
225
|
+
* other toast is redrawn. A held-back toast is changed too and shows the
|
|
226
|
+
* change when it comes on screen.
|
|
227
|
+
*
|
|
228
|
+
* Fields: `message`, `title`, `description`, `severity`, `meta`, and for
|
|
229
|
+
* progress toasts `progress` (fraction, `{ value, total }`, or `null` for
|
|
230
|
+
* indeterminate), `status`, `counts` and `collapsed`; `progress` on a
|
|
231
|
+
* plain toast turns it into a running progress toast (on screen, as with
|
|
232
|
+
* show()). Giving a running progress toast an outcome severity (success,
|
|
233
|
+
* error, fatal) finishes it, and its bar keeps the state it ended in: a
|
|
234
|
+
* later severity only restyles the toast. A `durationMs` on a toast that
|
|
235
|
+
* is not running (re)starts its auto-dismiss (0 keeps it until closed).
|
|
236
|
+
*
|
|
188
237
|
* @param {string} notificationId - The ID of the notification to update
|
|
189
|
-
* @param {Object} updates -
|
|
238
|
+
* @param {Object} updates - The fields to change
|
|
190
239
|
* @returns {boolean} True if the notification was found and updated
|
|
191
240
|
*/
|
|
192
241
|
update(notificationId, updates = {}) {
|
|
193
|
-
const notification = this.notifications.find((item) => item.id === notificationId)
|
|
242
|
+
const notification = this.notifications.find((item) => item.id === notificationId)
|
|
243
|
+
?? this.held.find((item) => item.id === notificationId);
|
|
194
244
|
if (!notification) {
|
|
195
245
|
return false;
|
|
196
246
|
}
|
|
247
|
+
const hadTitle = Boolean(notification.title);
|
|
248
|
+
const hadDescription = Boolean(notification.description);
|
|
249
|
+
const wasProgress = notification.progress !== undefined;
|
|
250
|
+
const wasRunning = NotificationCenter.#isRunning(notification);
|
|
197
251
|
|
|
198
|
-
// Update allowed fields
|
|
199
252
|
if (updates.message !== undefined) notification.message = updates.message;
|
|
200
253
|
if (updates.title !== undefined) notification.title = updates.title;
|
|
201
|
-
if (updates.
|
|
254
|
+
if (updates.description !== undefined) notification.description = updates.description;
|
|
202
255
|
if (updates.meta !== undefined) notification.meta = updates.meta;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if (
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
256
|
+
if (updates.status !== undefined) notification.status = updates.status ?? '';
|
|
257
|
+
if (updates.counts !== undefined) notification.counts = updates.counts ?? '';
|
|
258
|
+
if (updates.collapsed !== undefined) notification.collapsed = Boolean(updates.collapsed);
|
|
259
|
+
if (updates.progress !== undefined) {
|
|
260
|
+
if (wasProgress) {
|
|
261
|
+
notification.progress = NotificationCenter.#normalizeProgress(updates.progress);
|
|
262
|
+
} else {
|
|
263
|
+
// A plain toast becomes a running progress toast, set up as show() would.
|
|
264
|
+
Object.assign(notification, NotificationCenter.#progressFields({ ...notification, ...updates }));
|
|
265
|
+
notification.persistent = true;
|
|
266
|
+
this.#stopTimer(notification.id);
|
|
210
267
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
268
|
+
}
|
|
269
|
+
if (updates.severity !== undefined) {
|
|
270
|
+
notification.severity = this.#normalizeSeverity(updates.severity);
|
|
271
|
+
const outcome = OUTCOME_BAR_STATE[notification.severity];
|
|
272
|
+
if (NotificationCenter.#isRunning(notification) && outcome) notification.outcome = outcome;
|
|
273
|
+
}
|
|
274
|
+
const running = NotificationCenter.#isRunning(notification);
|
|
275
|
+
const finished = wasRunning && !running;
|
|
276
|
+
if (updates.durationMs !== undefined && !NotificationCenter.#isRunning(notification)) {
|
|
277
|
+
notification.durationMs = this.#coerceDuration(updates.durationMs);
|
|
278
|
+
notification.persistent = notification.durationMs === 0;
|
|
279
|
+
if (this.notifications.includes(notification)) {
|
|
280
|
+
this.#stopTimer(notification.id);
|
|
281
|
+
this.#scheduleAutoDismiss(notification);
|
|
214
282
|
}
|
|
215
|
-
} else {
|
|
216
|
-
// Fallback to full re-render if DOM element not found
|
|
217
|
-
this.#render();
|
|
218
283
|
}
|
|
219
284
|
|
|
285
|
+
const entry = this.#items.get(notification.id);
|
|
286
|
+
if (entry) {
|
|
287
|
+
const structural = hadTitle !== Boolean(notification.title)
|
|
288
|
+
|| hadDescription !== Boolean(notification.description)
|
|
289
|
+
|| wasProgress !== (notification.progress !== undefined);
|
|
290
|
+
if (structural) {
|
|
291
|
+
// A part appears or goes: rebuild this one toast, in its place.
|
|
292
|
+
const next = this.#buildItem(notification);
|
|
293
|
+
entry.el.replaceWith(next.el);
|
|
294
|
+
entry.bar?.dispose();
|
|
295
|
+
this.#items.set(notification.id, next);
|
|
296
|
+
} else {
|
|
297
|
+
this.#patchItem(entry, notification);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (finished) this.#announce(notification);
|
|
301
|
+
// Started, it comes on screen whatever is held; finished, it folds
|
|
302
|
+
// like any other toast.
|
|
303
|
+
if (running !== wasRunning) this.#rebalance();
|
|
304
|
+
this.#render();
|
|
220
305
|
return true;
|
|
221
306
|
}
|
|
222
307
|
|
|
@@ -268,7 +353,9 @@ export class NotificationCenter extends ComponentBase {
|
|
|
268
353
|
}
|
|
269
354
|
const id = notification.id ?? `notification-${++this.sequence}`;
|
|
270
355
|
const severity = this.#normalizeSeverity(notification.severity ?? 'info');
|
|
356
|
+
const isProgress = notification.progress !== undefined;
|
|
271
357
|
return {
|
|
358
|
+
...(isProgress ? NotificationCenter.#progressFields(notification) : {}),
|
|
272
359
|
id,
|
|
273
360
|
title: notification.title ?? null,
|
|
274
361
|
message: notification.message ?? '',
|
|
@@ -277,7 +364,9 @@ export class NotificationCenter extends ComponentBase {
|
|
|
277
364
|
severity,
|
|
278
365
|
namespace: notification.namespace ?? null,
|
|
279
366
|
durationMs: this.#coerceDuration(notification.durationMs),
|
|
280
|
-
|
|
367
|
+
// A progress toast stays until it finishes (and then until its
|
|
368
|
+
// finishing durationMs, or its ×).
|
|
369
|
+
persistent: isProgress || Boolean(notification.persistent),
|
|
281
370
|
skipHistory: Boolean(notification.skipHistory),
|
|
282
371
|
actions: this.#normalizeActions(notification.actions),
|
|
283
372
|
meta: notification.meta ?? null,
|
|
@@ -394,111 +483,273 @@ export class NotificationCenter extends ComponentBase {
|
|
|
394
483
|
}
|
|
395
484
|
}
|
|
396
485
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
486
|
+
/** A progress toast that has not been given an outcome yet. */
|
|
487
|
+
static #isRunning(notification) {
|
|
488
|
+
return notification.progress !== undefined && !notification.outcome;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* The fields a progress toast carries, from a show() payload or an
|
|
493
|
+
* update() that turns a plain toast into one. `outcome` is null while it
|
|
494
|
+
* runs, then the state its bar ended in (done or error).
|
|
495
|
+
*/
|
|
496
|
+
static #progressFields(source) {
|
|
497
|
+
return {
|
|
498
|
+
progress: NotificationCenter.#normalizeProgress(source.progress),
|
|
499
|
+
status: source.status ?? '',
|
|
500
|
+
counts: source.counts ?? '',
|
|
501
|
+
collapsed: Boolean(source.collapsed),
|
|
502
|
+
outcome: null,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** `null` (indeterminate), a 0..1 fraction, or `{ value, total }` -> ProgressBar input. */
|
|
507
|
+
static #normalizeProgress(progress) {
|
|
508
|
+
if (progress === null) return { indeterminate: true };
|
|
509
|
+
if (typeof progress === 'number') {
|
|
510
|
+
return { value: Number.isFinite(progress) ? progress : 0, max: null, indeterminate: false };
|
|
400
511
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
if (notification.title) {
|
|
413
|
-
const titleEl = document.createElement('span');
|
|
414
|
-
titleEl.className = 'twm-notification__title';
|
|
415
|
-
titleEl.textContent = notification.title;
|
|
416
|
-
header.appendChild(titleEl);
|
|
417
|
-
}
|
|
512
|
+
if (typeof progress === 'object') {
|
|
513
|
+
const value = Number(progress.value);
|
|
514
|
+
const total = Number(progress.total);
|
|
515
|
+
return {
|
|
516
|
+
value: Number.isFinite(value) ? value : 0,
|
|
517
|
+
max: Number.isFinite(total) && total > 0 ? total : null,
|
|
518
|
+
indeterminate: false,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
return { indeterminate: true };
|
|
522
|
+
}
|
|
418
523
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
item.appendChild(body);
|
|
437
|
-
|
|
438
|
-
if (notification.description) {
|
|
439
|
-
const descriptionEl = document.createElement('div');
|
|
440
|
-
descriptionEl.className = 'twm-notification__description';
|
|
441
|
-
descriptionEl.textContent = notification.description;
|
|
442
|
-
item.appendChild(descriptionEl);
|
|
524
|
+
/**
|
|
525
|
+
* 'collapse' only: keep at most `maxVisible` on screen, counting running
|
|
526
|
+
* progress toasts, which never leave and are never held (one that started
|
|
527
|
+
* running while held comes on screen first). Past the limit the newest
|
|
528
|
+
* other toast steps back to the front of the held queue (its timer
|
|
529
|
+
* restarts when it returns); under it the oldest held toast comes in.
|
|
530
|
+
*/
|
|
531
|
+
#rebalance() {
|
|
532
|
+
if (this.overflow !== 'collapse') return;
|
|
533
|
+
for (let i = 0; i < this.held.length;) {
|
|
534
|
+
if (NotificationCenter.#isRunning(this.held[i])) this.notifications.push(...this.held.splice(i, 1));
|
|
535
|
+
else i++;
|
|
536
|
+
}
|
|
537
|
+
while (this.notifications.length > this.maxVisible) {
|
|
538
|
+
let idx = -1;
|
|
539
|
+
for (let i = this.notifications.length - 1; i >= 0; i--) {
|
|
540
|
+
if (!NotificationCenter.#isRunning(this.notifications[i])) { idx = i; break; }
|
|
443
541
|
}
|
|
542
|
+
if (idx === -1) break;
|
|
543
|
+
const [back] = this.notifications.splice(idx, 1);
|
|
544
|
+
this.#stopTimer(back.id);
|
|
545
|
+
this.held.unshift(back);
|
|
546
|
+
}
|
|
547
|
+
while (this.held.length && this.notifications.length < this.maxVisible) {
|
|
548
|
+
const next = this.held.shift();
|
|
549
|
+
this.notifications.push(next);
|
|
550
|
+
this.#scheduleAutoDismiss(next);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
444
553
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
});
|
|
469
|
-
actionsEl.appendChild(button);
|
|
470
|
-
});
|
|
471
|
-
item.appendChild(actionsEl);
|
|
554
|
+
/**
|
|
555
|
+
* Draw the toasts on screen. Each toast's element is built once and kept:
|
|
556
|
+
* a toast arriving or leaving only inserts or removes its own element, so
|
|
557
|
+
* every other toast keeps its nodes (and its bar, collapsed state, hover).
|
|
558
|
+
*/
|
|
559
|
+
#render() {
|
|
560
|
+
if (!this._mounted || !this.listEl) {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const visible = new Set(this.notifications.map((n) => n.id));
|
|
564
|
+
for (const [id, entry] of this.#items) {
|
|
565
|
+
if (visible.has(id)) continue;
|
|
566
|
+
entry.bar?.dispose();
|
|
567
|
+
entry.el.remove();
|
|
568
|
+
this.#items.delete(id);
|
|
569
|
+
}
|
|
570
|
+
const wanted = this.notifications.map((notification) => {
|
|
571
|
+
let entry = this.#items.get(notification.id);
|
|
572
|
+
if (!entry) {
|
|
573
|
+
entry = this.#buildItem(notification);
|
|
574
|
+
this.#items.set(notification.id, entry);
|
|
472
575
|
}
|
|
473
|
-
|
|
474
|
-
item.addEventListener('click', (event) => {
|
|
475
|
-
if (event.target.closest('button')) {
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
this.dismiss(notification.id, 'click');
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
fragment.appendChild(item);
|
|
576
|
+
return entry.el;
|
|
482
577
|
});
|
|
578
|
+
const summary = this.#syncSummary();
|
|
579
|
+
if (summary) wanted.push(summary);
|
|
580
|
+
wanted.forEach((el, i) => {
|
|
581
|
+
const at = this.listEl.children[i];
|
|
582
|
+
if (at !== el) this.listEl.insertBefore(el, at ?? null);
|
|
583
|
+
});
|
|
584
|
+
// Anything after them is stale (another instance's toasts on a reused root).
|
|
585
|
+
while (this.listEl.children.length > wanted.length) {
|
|
586
|
+
this.listEl.lastElementChild.remove();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
483
589
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
590
|
+
// The held-back toasts, as one: "+N more". It is a button so the count is
|
|
591
|
+
// reachable by keyboard; what it opens is the consumer's (summaryAction),
|
|
592
|
+
// and it is also announced on the bus for a history panel to answer.
|
|
593
|
+
#syncSummary() {
|
|
594
|
+
if (!this.held.length) {
|
|
595
|
+
this.#summaryEl?.remove();
|
|
596
|
+
this.#summaryEl = null;
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
if (!this.#summaryEl) {
|
|
488
600
|
const more = document.createElement('button');
|
|
489
601
|
more.type = 'button';
|
|
490
602
|
more.className = 'notification twm-notification--summary';
|
|
491
|
-
more.setAttribute('data-notification-summary', String(this.held.length));
|
|
492
|
-
more.textContent = `+${this.held.length} more`;
|
|
493
|
-
more.title = this.held.map((h) => h.title || h.message).join('\n');
|
|
494
603
|
more.addEventListener('click', () => {
|
|
495
604
|
const held = [...this.held];
|
|
496
605
|
try { this.summaryAction?.(held); } catch (err) { this.logger?.warn?.('summaryAction failed', err); }
|
|
497
606
|
this.eventBus?.emit?.('notification:summary', { held });
|
|
498
607
|
});
|
|
499
|
-
|
|
608
|
+
this.#summaryEl = more;
|
|
609
|
+
}
|
|
610
|
+
const more = this.#summaryEl;
|
|
611
|
+
const count = String(this.held.length);
|
|
612
|
+
if (more.getAttribute('data-notification-summary') !== count) more.setAttribute('data-notification-summary', count);
|
|
613
|
+
NotificationCenter.#setText(more, `+${count} more`);
|
|
614
|
+
const tip = this.held.map((h) => h.title || h.message).join('\n');
|
|
615
|
+
if (more.title !== tip) more.title = tip;
|
|
616
|
+
return more;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
static #setText(node, text) {
|
|
620
|
+
const t = text == null ? '' : String(text);
|
|
621
|
+
if (node.textContent !== t) node.textContent = t;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/** Build one toast's element; `#patchItem` then fills what can change. */
|
|
625
|
+
#buildItem(notification) {
|
|
626
|
+
const isProgress = notification.progress !== undefined;
|
|
627
|
+
const item = document.createElement('div');
|
|
628
|
+
// Use setAttribute to avoid StateGuard blocking dataset mutations
|
|
629
|
+
item.setAttribute('data-notification-id', notification.id);
|
|
630
|
+
const entry = { el: item, severity: null, titleEl: null, bodyEl: null, descriptionEl: null,
|
|
631
|
+
statusEl: null, bar: null, collapseBtn: null };
|
|
632
|
+
|
|
633
|
+
const header = document.createElement('div');
|
|
634
|
+
header.className = 'twm-notification__header';
|
|
635
|
+
if (notification.title) {
|
|
636
|
+
entry.titleEl = document.createElement('span');
|
|
637
|
+
entry.titleEl.className = 'twm-notification__title';
|
|
638
|
+
header.appendChild(entry.titleEl);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const timeEl = document.createElement('span');
|
|
642
|
+
timeEl.className = 'twm-notification__timestamp';
|
|
643
|
+
timeEl.textContent = this.#formatTimestamp(notification.timestamp);
|
|
644
|
+
header.appendChild(timeEl);
|
|
645
|
+
|
|
646
|
+
if (isProgress) {
|
|
647
|
+
const toggle = document.createElement('button');
|
|
648
|
+
toggle.className = 'twm-notification__collapse';
|
|
649
|
+
toggle.type = 'button';
|
|
650
|
+
toggle.innerHTML = '<span class="material-symbols-outlined">expand_more</span>';
|
|
651
|
+
toggle.addEventListener('click', () => {
|
|
652
|
+
this.update(notification.id, { collapsed: !notification.collapsed });
|
|
653
|
+
});
|
|
654
|
+
entry.collapseBtn = toggle;
|
|
655
|
+
header.appendChild(toggle);
|
|
500
656
|
}
|
|
501
|
-
|
|
657
|
+
|
|
658
|
+
const closeBtn = document.createElement('button');
|
|
659
|
+
closeBtn.className = 'twm-notification__close';
|
|
660
|
+
closeBtn.type = 'button';
|
|
661
|
+
closeBtn.setAttribute('aria-label', 'Dismiss notification');
|
|
662
|
+
closeBtn.innerHTML = '<span class="material-symbols-outlined">close</span>';
|
|
663
|
+
closeBtn.addEventListener('click', () => this.dismiss(notification.id, 'manual'));
|
|
664
|
+
header.appendChild(closeBtn);
|
|
665
|
+
item.appendChild(header);
|
|
666
|
+
|
|
667
|
+
entry.bodyEl = document.createElement('div');
|
|
668
|
+
entry.bodyEl.className = 'twm-notification__body';
|
|
669
|
+
item.appendChild(entry.bodyEl);
|
|
670
|
+
|
|
671
|
+
if (isProgress) {
|
|
672
|
+
entry.bar = new ProgressBar(item, {
|
|
673
|
+
...notification.progress, showCount: false, className: 'twm-notification__progress',
|
|
674
|
+
});
|
|
675
|
+
entry.statusEl = document.createElement('div');
|
|
676
|
+
entry.statusEl.className = 'twm-notification__status';
|
|
677
|
+
item.appendChild(entry.statusEl);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (notification.description) {
|
|
681
|
+
entry.descriptionEl = document.createElement('div');
|
|
682
|
+
entry.descriptionEl.className = 'twm-notification__description';
|
|
683
|
+
item.appendChild(entry.descriptionEl);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (notification.actions.length > 0) {
|
|
687
|
+
const actionsEl = document.createElement('div');
|
|
688
|
+
actionsEl.className = 'twm-notification__actions';
|
|
689
|
+
notification.actions.forEach((action) => {
|
|
690
|
+
const button = document.createElement('button');
|
|
691
|
+
button.type = 'button';
|
|
692
|
+
button.className = 'twm-notification__action-btn';
|
|
693
|
+
button.textContent = action.label;
|
|
694
|
+
button.addEventListener('click', () => {
|
|
695
|
+
try {
|
|
696
|
+
if (action.handler) {
|
|
697
|
+
action.handler(notification, action);
|
|
698
|
+
} else if (action.eventName && this.eventBus) {
|
|
699
|
+
this.eventBus.emit(action.eventName, action.payload ?? { notificationId: notification.id });
|
|
700
|
+
}
|
|
701
|
+
} catch (err) {
|
|
702
|
+
this.logger?.error?.('twm-notification-center', 'Notification action failed', { err });
|
|
703
|
+
} finally {
|
|
704
|
+
// Don't auto-dismiss error/fatal notifications, or a job
|
|
705
|
+
// still running, after action clicks
|
|
706
|
+
if (notification.severity !== 'error' && notification.severity !== 'fatal'
|
|
707
|
+
&& !NotificationCenter.#isRunning(notification)) {
|
|
708
|
+
this.dismiss(notification.id, 'action');
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
actionsEl.appendChild(button);
|
|
713
|
+
});
|
|
714
|
+
item.appendChild(actionsEl);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
item.addEventListener('click', (event) => {
|
|
718
|
+
if (event.target.closest('button') || NotificationCenter.#isRunning(notification)) {
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
this.dismiss(notification.id, 'click');
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
this.#patchItem(entry, notification);
|
|
725
|
+
return entry;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/** Bring a drawn toast's nodes in line with its data, touching only what changed. */
|
|
729
|
+
#patchItem(entry, notification) {
|
|
730
|
+
const item = entry.el;
|
|
731
|
+
if (entry.severity !== notification.severity) {
|
|
732
|
+
if (entry.severity) item.classList.remove(`notification--${entry.severity}`);
|
|
733
|
+
item.classList.add('notification', `notification--${notification.severity}`);
|
|
734
|
+
entry.severity = notification.severity;
|
|
735
|
+
}
|
|
736
|
+
if (entry.titleEl) NotificationCenter.#setText(entry.titleEl, notification.title);
|
|
737
|
+
NotificationCenter.#setText(entry.bodyEl, notification.message);
|
|
738
|
+
if (entry.descriptionEl) NotificationCenter.#setText(entry.descriptionEl, notification.description);
|
|
739
|
+
if (!entry.bar) return;
|
|
740
|
+
|
|
741
|
+
item.classList.add('twm-notification--progress');
|
|
742
|
+
item.classList.toggle('twm-notification--collapsed', Boolean(notification.collapsed));
|
|
743
|
+
const expanded = String(!notification.collapsed);
|
|
744
|
+
if (entry.collapseBtn.getAttribute('aria-expanded') !== expanded) {
|
|
745
|
+
entry.collapseBtn.setAttribute('aria-expanded', expanded);
|
|
746
|
+
entry.collapseBtn.setAttribute('aria-label', notification.collapsed ? 'Expand notification' : 'Collapse notification');
|
|
747
|
+
}
|
|
748
|
+
NotificationCenter.#setText(entry.statusEl, notification.status);
|
|
749
|
+
entry.statusEl.hidden = !notification.status;
|
|
750
|
+
entry.bar.update({ ...notification.progress, label: notification.counts });
|
|
751
|
+
const end = notification.outcome ?? 'running';
|
|
752
|
+
if (entry.bar.state !== end) entry.bar.setState(end);
|
|
502
753
|
}
|
|
503
754
|
|
|
504
755
|
#formatTimestamp(timestamp) {
|
|
@@ -600,6 +851,11 @@ export class NotificationCenter extends ComponentBase {
|
|
|
600
851
|
);
|
|
601
852
|
}
|
|
602
853
|
|
|
854
|
+
/** The toast helpers (toast.js) emit on the bus of the centre mounted last. */
|
|
855
|
+
#claimToastBus() {
|
|
856
|
+
if (this.eventBus) setToastBus(this.eventBus);
|
|
857
|
+
}
|
|
858
|
+
|
|
603
859
|
#unsubscribeEvents() {
|
|
604
860
|
this.eventDisposers.forEach((disposer) => {
|
|
605
861
|
try {
|
|
File without changes
|