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,576 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notification Center (js_new)
|
|
3
|
+
*
|
|
4
|
+
* Purpose
|
|
5
|
+
* -------
|
|
6
|
+
* Unified notifications/toasts/status-line controller consuming events from the rest of the app.
|
|
7
|
+
*
|
|
8
|
+
* Responsibilities
|
|
9
|
+
* - Render queued notifications with namespace context and severity.
|
|
10
|
+
* - Ingest `log:*` and domain-specific events to present user-facing feedback.
|
|
11
|
+
* - Replace scattered `window.notify` / `status_view.js` hooks.
|
|
12
|
+
*
|
|
13
|
+
* Source Material
|
|
14
|
+
* - html/js/status_view.js and ui_manager.js toast helpers.
|
|
15
|
+
* - html/js/ui_utilities.js notification utilities.
|
|
16
|
+
*/
|
|
17
|
+
import { ComponentBase } from './base/component_base.js';
|
|
18
|
+
import { getSetting } from '../core/settings.js';
|
|
19
|
+
|
|
20
|
+
const SEVERITIES = ['info', 'success', 'warn', 'error', 'fatal'];
|
|
21
|
+
const LOG_SEVERITY_MAP = {
|
|
22
|
+
trace: 'info',
|
|
23
|
+
debug: 'info',
|
|
24
|
+
info: 'info',
|
|
25
|
+
warn: 'warn',
|
|
26
|
+
error: 'error',
|
|
27
|
+
fatal: 'fatal',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const DEFAULT_LOG_LEVELS = new Set(['warn', 'error', 'fatal']);
|
|
31
|
+
|
|
32
|
+
// Internal namespace identifiers that should not be shown as toast titles.
|
|
33
|
+
// Messages from these namespaces are self-descriptive and need no category header.
|
|
34
|
+
const SUPPRESSED_NAMESPACE_TITLES = new Set(['bootstrap', 'app']);
|
|
35
|
+
|
|
36
|
+
export class NotificationCenter extends ComponentBase {
|
|
37
|
+
constructor({
|
|
38
|
+
eventBus,
|
|
39
|
+
logger,
|
|
40
|
+
maxVisible = 3,
|
|
41
|
+
historyLimit = 50,
|
|
42
|
+
defaultDurationMs = 6000,
|
|
43
|
+
logLevels = DEFAULT_LOG_LEVELS,
|
|
44
|
+
} = {}) {
|
|
45
|
+
super({ eventBus, logger });
|
|
46
|
+
|
|
47
|
+
this.maxVisible = maxVisible;
|
|
48
|
+
this.historyLimit = historyLimit;
|
|
49
|
+
this.defaultDurationMs = defaultDurationMs;
|
|
50
|
+
this.logLevels = new Set(logLevels ?? DEFAULT_LOG_LEVELS);
|
|
51
|
+
|
|
52
|
+
this.notifications = [];
|
|
53
|
+
this.history = [];
|
|
54
|
+
this.sequence = 0;
|
|
55
|
+
this.activeTimers = new Map();
|
|
56
|
+
this.eventDisposers = [];
|
|
57
|
+
|
|
58
|
+
this.container = null;
|
|
59
|
+
this.rootEl = null;
|
|
60
|
+
this.listEl = null;
|
|
61
|
+
this.liveRegionEl = null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
mount(container, props = {}) {
|
|
65
|
+
// Always mount to document.body for reliable visibility
|
|
66
|
+
// The passed container is ignored - we create our own fixed-position container
|
|
67
|
+
const existingContainer = document.getElementById('notification-center-root');
|
|
68
|
+
if (existingContainer) {
|
|
69
|
+
this.container = existingContainer;
|
|
70
|
+
this.rootEl = existingContainer.querySelector('.twm-notification-center');
|
|
71
|
+
this.listEl = existingContainer.querySelector('.twm-notification-center__list');
|
|
72
|
+
this.liveRegionEl = existingContainer.querySelector('.notification-center__live-region');
|
|
73
|
+
this._mounted = true;
|
|
74
|
+
this.#subscribeEvents();
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Create a new fixed-position container directly on body
|
|
79
|
+
this.container = document.createElement('div');
|
|
80
|
+
this.container.id = 'notification-center-root';
|
|
81
|
+
this.container.style.cssText = 'position: fixed; right: 16px; bottom: 24px; z-index: 10000; pointer-events: none;';
|
|
82
|
+
|
|
83
|
+
this.rootEl = document.createElement('div');
|
|
84
|
+
this.rootEl.className = 'twm-notification-center';
|
|
85
|
+
|
|
86
|
+
this.listEl = document.createElement('div');
|
|
87
|
+
this.listEl.className = 'twm-notification-center__list';
|
|
88
|
+
|
|
89
|
+
this.liveRegionEl = document.createElement('div');
|
|
90
|
+
this.liveRegionEl.className = 'notification-center__live-region';
|
|
91
|
+
this.liveRegionEl.setAttribute('aria-live', 'polite');
|
|
92
|
+
this.liveRegionEl.setAttribute('aria-atomic', 'true');
|
|
93
|
+
this.liveRegionEl.style.position = 'absolute';
|
|
94
|
+
this.liveRegionEl.style.width = '1px';
|
|
95
|
+
this.liveRegionEl.style.height = '1px';
|
|
96
|
+
this.liveRegionEl.style.overflow = 'hidden';
|
|
97
|
+
this.liveRegionEl.style.clip = 'rect(1px, 1px, 1px, 1px)';
|
|
98
|
+
|
|
99
|
+
this.rootEl.appendChild(this.listEl);
|
|
100
|
+
this.rootEl.appendChild(this.liveRegionEl);
|
|
101
|
+
this.container.appendChild(this.rootEl);
|
|
102
|
+
document.body.appendChild(this.container);
|
|
103
|
+
|
|
104
|
+
this._mounted = true;
|
|
105
|
+
this.#subscribeEvents();
|
|
106
|
+
|
|
107
|
+
if (Array.isArray(props.initialNotifications)) {
|
|
108
|
+
props.initialNotifications.forEach((notification) => this.show(notification));
|
|
109
|
+
}
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
dispose() {
|
|
114
|
+
this.#clearTimers();
|
|
115
|
+
this.#unsubscribeEvents();
|
|
116
|
+
if (this.rootEl?.parentNode) {
|
|
117
|
+
this.rootEl.parentNode.removeChild(this.rootEl);
|
|
118
|
+
}
|
|
119
|
+
this.container = null;
|
|
120
|
+
this.rootEl = null;
|
|
121
|
+
this.listEl = null;
|
|
122
|
+
this.liveRegionEl = null;
|
|
123
|
+
super.dispose();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
show(notification) {
|
|
127
|
+
const normalized = this.#normalizeNotification(notification);
|
|
128
|
+
this.#consoleLog(normalized);
|
|
129
|
+
this.notifications.push(normalized);
|
|
130
|
+
if (this.notifications.length > this.maxVisible) {
|
|
131
|
+
const removed = this.notifications.shift();
|
|
132
|
+
this.#stopTimer(removed.id);
|
|
133
|
+
this.#recordHistory({ ...removed, dismissed: true, reason: 'overflow' });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this.#recordHistory(normalized);
|
|
137
|
+
this.#scheduleAutoDismiss(normalized);
|
|
138
|
+
this.#render();
|
|
139
|
+
this.#announce(normalized);
|
|
140
|
+
return normalized.id;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
dismiss(notificationId, reason = 'dismissed') {
|
|
144
|
+
const idx = this.notifications.findIndex((item) => item.id === notificationId);
|
|
145
|
+
if (idx === -1) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
const [removed] = this.notifications.splice(idx, 1);
|
|
149
|
+
this.#stopTimer(notificationId);
|
|
150
|
+
this.#recordHistory({ ...removed, dismissed: true, reason });
|
|
151
|
+
this.#render();
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Update an existing notification's message in place without re-rendering.
|
|
157
|
+
* This prevents the flicker caused by dismiss+show.
|
|
158
|
+
* @param {string} notificationId - The ID of the notification to update
|
|
159
|
+
* @param {Object} updates - Object with fields to update (message, title, etc.)
|
|
160
|
+
* @returns {boolean} True if the notification was found and updated
|
|
161
|
+
*/
|
|
162
|
+
update(notificationId, updates = {}) {
|
|
163
|
+
const notification = this.notifications.find((item) => item.id === notificationId);
|
|
164
|
+
if (!notification) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Update allowed fields
|
|
169
|
+
if (updates.message !== undefined) notification.message = updates.message;
|
|
170
|
+
if (updates.title !== undefined) notification.title = updates.title;
|
|
171
|
+
if (updates.severity !== undefined) notification.severity = this.#normalizeSeverity(updates.severity);
|
|
172
|
+
if (updates.meta !== undefined) notification.meta = updates.meta;
|
|
173
|
+
|
|
174
|
+
// Update the DOM directly without full re-render
|
|
175
|
+
const notificationEl = this.listEl?.querySelector(`[data-notification-id="${notificationId}"]`);
|
|
176
|
+
if (notificationEl) {
|
|
177
|
+
const bodyEl = notificationEl.querySelector('.twm-notification__body');
|
|
178
|
+
if (bodyEl && updates.message !== undefined) {
|
|
179
|
+
bodyEl.textContent = updates.message;
|
|
180
|
+
}
|
|
181
|
+
const titleEl = notificationEl.querySelector('.twm-notification__title');
|
|
182
|
+
if (titleEl && updates.title !== undefined) {
|
|
183
|
+
titleEl.textContent = updates.title;
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
// Fallback to full re-render if DOM element not found
|
|
187
|
+
this.#render();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
clearAll(reason = 'cleared') {
|
|
194
|
+
[...this.notifications].forEach((notification) => this.dismiss(notification.id, reason));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
ingestLogEntry(entry) {
|
|
198
|
+
if (!entry || !this.logLevels.has(entry.level)) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Suppress state-guard warnings - these are internal and would cause a cascade
|
|
203
|
+
// (NotificationCenter renders -> triggers state-guard -> logs warning -> creates notification -> repeat)
|
|
204
|
+
if (entry.namespace === 'state-guard') {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const severity = LOG_SEVERITY_MAP[entry.level] ?? 'info';
|
|
209
|
+
const suppressTitle = SUPPRESSED_NAMESPACE_TITLES.has(entry.namespace);
|
|
210
|
+
const title = suppressTitle
|
|
211
|
+
? null
|
|
212
|
+
: NotificationCenter.#formatNamespaceTitle(entry.namespace);
|
|
213
|
+
// Info-level log entries from internal namespaces are noise in history
|
|
214
|
+
const skipHistory = severity === 'info' && suppressTitle;
|
|
215
|
+
this.show({
|
|
216
|
+
title,
|
|
217
|
+
message: entry.message,
|
|
218
|
+
severity,
|
|
219
|
+
namespace: entry.namespace,
|
|
220
|
+
meta: entry.data,
|
|
221
|
+
skipHistory,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
#consoleLog(notification) {
|
|
226
|
+
if (notification.severity !== 'warn' && notification.severity !== 'error' && notification.severity !== 'fatal') {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const method = notification.severity === 'warn' ? console.warn : console.error; // eslint-disable-line no-console
|
|
230
|
+
const prefix = notification.title ? `[${notification.title}]` : '[Notification]';
|
|
231
|
+
const details = notification.meta ?? notification.description ?? '';
|
|
232
|
+
method(`${prefix} ${notification.message}`, details); // eslint-disable-line no-console
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
#normalizeNotification(notification) {
|
|
236
|
+
if (!notification) {
|
|
237
|
+
throw new TypeError('Notification payload is required');
|
|
238
|
+
}
|
|
239
|
+
const id = notification.id ?? `notification-${++this.sequence}`;
|
|
240
|
+
const severity = this.#normalizeSeverity(notification.severity ?? 'info');
|
|
241
|
+
return {
|
|
242
|
+
id,
|
|
243
|
+
title: notification.title ?? null,
|
|
244
|
+
message: notification.message ?? '',
|
|
245
|
+
description: notification.description ?? null,
|
|
246
|
+
timestamp: notification.timestamp ?? Date.now(),
|
|
247
|
+
severity,
|
|
248
|
+
namespace: notification.namespace ?? null,
|
|
249
|
+
durationMs: this.#coerceDuration(notification.durationMs),
|
|
250
|
+
persistent: Boolean(notification.persistent),
|
|
251
|
+
skipHistory: Boolean(notification.skipHistory),
|
|
252
|
+
actions: this.#normalizeActions(notification.actions),
|
|
253
|
+
meta: notification.meta ?? null,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
#normalizeSeverity(severity) {
|
|
258
|
+
const value = typeof severity === 'string' ? severity.toLowerCase() : 'info';
|
|
259
|
+
if (!SEVERITIES.includes(value)) {
|
|
260
|
+
return 'info';
|
|
261
|
+
}
|
|
262
|
+
return value;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
#coerceDuration(duration) {
|
|
266
|
+
if (duration === null || duration === undefined) {
|
|
267
|
+
return this.defaultDurationMs;
|
|
268
|
+
}
|
|
269
|
+
const value = Number(duration);
|
|
270
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
271
|
+
return this.defaultDurationMs;
|
|
272
|
+
}
|
|
273
|
+
return value;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
toast(title, message, type = 'info') {
|
|
277
|
+
// Legacy compatibility method
|
|
278
|
+
this.show({
|
|
279
|
+
title,
|
|
280
|
+
message,
|
|
281
|
+
severity: type,
|
|
282
|
+
persistent: type === 'error' || type === 'fatal',
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
error(titleOrMessage, message) {
|
|
287
|
+
const hasTitle = message !== undefined;
|
|
288
|
+
return this.show({
|
|
289
|
+
title: hasTitle ? titleOrMessage : null,
|
|
290
|
+
message: hasTitle ? message : titleOrMessage,
|
|
291
|
+
severity: 'error',
|
|
292
|
+
persistent: true,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
warn(titleOrMessage, message) {
|
|
297
|
+
const hasTitle = message !== undefined;
|
|
298
|
+
return this.show({
|
|
299
|
+
title: hasTitle ? titleOrMessage : null,
|
|
300
|
+
message: hasTitle ? message : titleOrMessage,
|
|
301
|
+
severity: 'warn',
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
info(titleOrMessage, message) {
|
|
306
|
+
const hasTitle = message !== undefined;
|
|
307
|
+
return this.show({
|
|
308
|
+
title: hasTitle ? titleOrMessage : null,
|
|
309
|
+
message: hasTitle ? message : titleOrMessage,
|
|
310
|
+
severity: 'info',
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
success(titleOrMessage, message) {
|
|
315
|
+
const hasTitle = message !== undefined;
|
|
316
|
+
return this.show({
|
|
317
|
+
title: hasTitle ? titleOrMessage : null,
|
|
318
|
+
message: hasTitle ? message : titleOrMessage,
|
|
319
|
+
severity: 'success',
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
#normalizeActions(actions) {
|
|
324
|
+
if (!Array.isArray(actions) || actions.length === 0) {
|
|
325
|
+
return [];
|
|
326
|
+
}
|
|
327
|
+
return actions.map((action, idx) => ({
|
|
328
|
+
id: action.id ?? `action-${idx}`,
|
|
329
|
+
label: action.label ?? 'Action',
|
|
330
|
+
handler: typeof action.handler === 'function' ? action.handler : null,
|
|
331
|
+
eventName: action.eventName ?? null,
|
|
332
|
+
payload: action.payload ?? null,
|
|
333
|
+
}));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
#scheduleAutoDismiss(notification) {
|
|
337
|
+
if (notification.persistent || notification.durationMs === 0) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const timer = setTimeout(() => {
|
|
341
|
+
this.dismiss(notification.id, 'timeout');
|
|
342
|
+
}, notification.durationMs);
|
|
343
|
+
this.activeTimers.set(notification.id, timer);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
#stopTimer(notificationId) {
|
|
347
|
+
const timer = this.activeTimers.get(notificationId);
|
|
348
|
+
if (timer) {
|
|
349
|
+
clearTimeout(timer);
|
|
350
|
+
this.activeTimers.delete(notificationId);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
#clearTimers() {
|
|
355
|
+
this.activeTimers.forEach((timer) => clearTimeout(timer));
|
|
356
|
+
this.activeTimers.clear();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
#recordHistory(entry) {
|
|
360
|
+
if (entry.skipHistory) return;
|
|
361
|
+
this.history.push(entry);
|
|
362
|
+
if (this.history.length > this.historyLimit) {
|
|
363
|
+
this.history.shift();
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
#render() {
|
|
368
|
+
if (!this._mounted || !this.listEl) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
this.listEl.innerHTML = '';
|
|
372
|
+
const fragment = document.createDocumentFragment();
|
|
373
|
+
|
|
374
|
+
this.notifications.forEach((notification) => {
|
|
375
|
+
const item = document.createElement('div');
|
|
376
|
+
item.className = `notification notification--${notification.severity}`;
|
|
377
|
+
// Use setAttribute to avoid StateGuard blocking dataset mutations
|
|
378
|
+
item.setAttribute('data-notification-id', notification.id);
|
|
379
|
+
|
|
380
|
+
const header = document.createElement('div');
|
|
381
|
+
header.className = 'twm-notification__header';
|
|
382
|
+
if (notification.title) {
|
|
383
|
+
const titleEl = document.createElement('span');
|
|
384
|
+
titleEl.className = 'twm-notification__title';
|
|
385
|
+
titleEl.textContent = notification.title;
|
|
386
|
+
header.appendChild(titleEl);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const timeEl = document.createElement('span');
|
|
390
|
+
timeEl.className = 'twm-notification__timestamp';
|
|
391
|
+
timeEl.textContent = this.#formatTimestamp(notification.timestamp);
|
|
392
|
+
header.appendChild(timeEl);
|
|
393
|
+
|
|
394
|
+
const closeBtn = document.createElement('button');
|
|
395
|
+
closeBtn.className = 'twm-notification__close';
|
|
396
|
+
closeBtn.type = 'button';
|
|
397
|
+
closeBtn.setAttribute('aria-label', 'Dismiss notification');
|
|
398
|
+
closeBtn.innerHTML = '<span class="material-symbols-outlined">close</span>';
|
|
399
|
+
closeBtn.addEventListener('click', () => this.dismiss(notification.id, 'manual'));
|
|
400
|
+
header.appendChild(closeBtn);
|
|
401
|
+
item.appendChild(header);
|
|
402
|
+
|
|
403
|
+
const body = document.createElement('div');
|
|
404
|
+
body.className = 'twm-notification__body';
|
|
405
|
+
body.textContent = notification.message;
|
|
406
|
+
item.appendChild(body);
|
|
407
|
+
|
|
408
|
+
if (notification.description) {
|
|
409
|
+
const descriptionEl = document.createElement('div');
|
|
410
|
+
descriptionEl.className = 'twm-notification__description';
|
|
411
|
+
descriptionEl.textContent = notification.description;
|
|
412
|
+
item.appendChild(descriptionEl);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (notification.actions.length > 0) {
|
|
416
|
+
const actionsEl = document.createElement('div');
|
|
417
|
+
actionsEl.className = 'twm-notification__actions';
|
|
418
|
+
notification.actions.forEach((action) => {
|
|
419
|
+
const button = document.createElement('button');
|
|
420
|
+
button.type = 'button';
|
|
421
|
+
button.className = 'twm-notification__action-btn';
|
|
422
|
+
button.textContent = action.label;
|
|
423
|
+
button.addEventListener('click', () => {
|
|
424
|
+
try {
|
|
425
|
+
if (action.handler) {
|
|
426
|
+
action.handler(notification, action);
|
|
427
|
+
} else if (action.eventName && this.eventBus) {
|
|
428
|
+
this.eventBus.emit(action.eventName, action.payload ?? { notificationId: notification.id });
|
|
429
|
+
}
|
|
430
|
+
} catch (err) {
|
|
431
|
+
this.logger?.error?.('twm-notification-center', 'Notification action failed', { err });
|
|
432
|
+
} finally {
|
|
433
|
+
// Don't auto-dismiss error/fatal notifications after action clicks
|
|
434
|
+
if (notification.severity !== 'error' && notification.severity !== 'fatal') {
|
|
435
|
+
this.dismiss(notification.id, 'action');
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
actionsEl.appendChild(button);
|
|
440
|
+
});
|
|
441
|
+
item.appendChild(actionsEl);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
item.addEventListener('click', (event) => {
|
|
445
|
+
if (event.target.closest('button')) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
this.dismiss(notification.id, 'click');
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
fragment.appendChild(item);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
this.listEl.appendChild(fragment);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
#formatTimestamp(timestamp) {
|
|
458
|
+
const date = new Date(timestamp);
|
|
459
|
+
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
#announce(notification) {
|
|
463
|
+
if (!this.liveRegionEl) {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const parts = [notification.title, notification.message].filter(Boolean);
|
|
467
|
+
this.liveRegionEl.textContent = parts.join(': ');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
#handleToast(payload = {}) {
|
|
471
|
+
const message = payload.text ?? payload.message ?? (typeof payload === 'string' ? payload : '');
|
|
472
|
+
if (!message) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const persistent = payload.persistent === true || payload.autoHide === false;
|
|
477
|
+
const durationMs = persistent ? 0 : (payload.durationMs ?? payload.duration ?? payload.timeout);
|
|
478
|
+
this.show({
|
|
479
|
+
title: payload.title ?? null,
|
|
480
|
+
message,
|
|
481
|
+
severity: payload.type ?? payload.severity ?? 'info',
|
|
482
|
+
durationMs,
|
|
483
|
+
persistent,
|
|
484
|
+
namespace: payload.namespace ?? null,
|
|
485
|
+
actions: payload.actions,
|
|
486
|
+
meta: payload.meta ?? null,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
#subscribeEvents() {
|
|
491
|
+
if (!this.eventBus) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
this.eventDisposers.push(
|
|
495
|
+
this.eventBus.on('toast:show', (payload) => this.#handleToast(payload)),
|
|
496
|
+
this.eventBus.on('notification:show', (payload) => this.show(payload)),
|
|
497
|
+
this.eventBus.on('notification:update', (payload) => {
|
|
498
|
+
if (payload?.id) {
|
|
499
|
+
this.update(payload.id, payload);
|
|
500
|
+
}
|
|
501
|
+
}),
|
|
502
|
+
this.eventBus.on('log:entry', (entry) => this.ingestLogEntry(entry)),
|
|
503
|
+
this.eventBus.on('workspace:save:failed', (payload) => {
|
|
504
|
+
// Error toasts are shown by default unless explicitly disabled
|
|
505
|
+
if (getSetting('workspace.save.showErrorToast', true)) {
|
|
506
|
+
this.show({
|
|
507
|
+
title: 'Workspace Save Failed',
|
|
508
|
+
message: payload?.reason ? `Save failed: ${payload.reason}` : 'Workspace save failed',
|
|
509
|
+
severity: 'error',
|
|
510
|
+
meta: payload,
|
|
511
|
+
persistent: true,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}),
|
|
515
|
+
this.eventBus.on('workspace:save:completed', (payload) => {
|
|
516
|
+
// Only show toast if enabled in settings
|
|
517
|
+
if (getSetting('workspace.save.showToast', false)) {
|
|
518
|
+
this.show({
|
|
519
|
+
title: 'Workspace Saved',
|
|
520
|
+
message: 'Changes saved successfully',
|
|
521
|
+
severity: 'success',
|
|
522
|
+
durationMs: 3000,
|
|
523
|
+
meta: payload,
|
|
524
|
+
skipHistory: true,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
}),
|
|
528
|
+
this.eventBus.on('workspace:import:failed', (payload) => {
|
|
529
|
+
// Error toasts are shown by default unless explicitly disabled
|
|
530
|
+
if (getSetting('workspace.import.showErrorToast', true)) {
|
|
531
|
+
this.show({
|
|
532
|
+
title: 'Workspace Import Failed',
|
|
533
|
+
message: payload?.error?.message || 'Import failed',
|
|
534
|
+
severity: 'error',
|
|
535
|
+
meta: payload,
|
|
536
|
+
persistent: true,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
}),
|
|
540
|
+
this.eventBus.on('workspace:import:completed', (payload) => {
|
|
541
|
+
// Show import completion toast if enabled in settings
|
|
542
|
+
if (getSetting('workspace.import.showToast', true)) {
|
|
543
|
+
this.show({
|
|
544
|
+
title: 'Workspace Ready',
|
|
545
|
+
message: 'Workspace import completed',
|
|
546
|
+
severity: 'success',
|
|
547
|
+
durationMs: 3000,
|
|
548
|
+
meta: payload,
|
|
549
|
+
skipHistory: true,
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
}),
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
#unsubscribeEvents() {
|
|
557
|
+
this.eventDisposers.forEach((disposer) => {
|
|
558
|
+
try {
|
|
559
|
+
disposer?.dispose?.();
|
|
560
|
+
} catch (err) {
|
|
561
|
+
this.logger?.warn?.('twm-notification-center', 'Failed to remove event listener', { err });
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
this.eventDisposers = [];
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
static #formatNamespaceTitle(namespace) {
|
|
568
|
+
if (!namespace) return null;
|
|
569
|
+
return namespace
|
|
570
|
+
.split(/[-_]/)
|
|
571
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
572
|
+
.join(' ');
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
NotificationCenter.SEVERITIES = SEVERITIES;
|