flexdesk 0.17.2 → 0.17.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flexdesk",
3
- "version": "0.17.2",
3
+ "version": "0.17.3",
4
4
  "description": "A tiling window manager and widget set for desktop-class web applications. Zero runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -2,8 +2,10 @@
2
2
  * NotificationHistory - Bottom bar notification history panel
3
3
  *
4
4
  * Bell icon in the global-bottom-bar that opens a popover listing past notifications.
5
- * Reads from NotificationCenter.history (no modifications to NotificationCenter needed).
6
- * Tracks unread count via lastSeenCount delta and updates a badge on the bell icon.
5
+ * Reads from NotificationCenter.history and follows it through
6
+ * `notificationCenter.subscribe()` (0.17.3): every entry the centre records — a
7
+ * toast from `show()`/`warn()`/... or from the bus — is one unread, until the
8
+ * popover is opened. The bus is not watched for toasts.
7
9
  *
8
10
  * THREE OPTIONS, all additive (FlexDesk STORY-0020); without them it is the bell
9
11
  * it always was:
@@ -34,7 +36,8 @@ export class NotificationHistory {
34
36
 
35
37
  this.triggerEl = null;
36
38
  this.badgeEl = null;
37
- this.lastSeenCount = 0;
39
+ /** Entries recorded since the popover was last opened. */
40
+ this.unreadCount = 0;
38
41
  this._popoverCleanup = null;
39
42
  this._subscriptions = [];
40
43
  }
@@ -80,10 +83,14 @@ export class NotificationHistory {
80
83
  return;
81
84
  }
82
85
 
83
- // Sync initial count so existing history doesn't show as unread
84
- this.lastSeenCount = this.notificationCenter?.history?.length ?? 0;
85
-
86
- this.#subscribeEvents();
86
+ // What is in the history at mount is not unread.
87
+ this.unreadCount = 0;
88
+ const unsubscribe = this.notificationCenter?.subscribe?.(() => {
89
+ this.unreadCount += 1;
90
+ this.#updateBadge();
91
+ });
92
+ if (typeof unsubscribe === 'function') this._subscriptions.push(unsubscribe);
93
+ this.#updateBadge();
87
94
  }
88
95
 
89
96
  unmount() {
@@ -99,20 +106,6 @@ export class NotificationHistory {
99
106
  this.badgeEl = null;
100
107
  }
101
108
 
102
- #subscribeEvents() {
103
- if (!this.eventBus) return;
104
-
105
- // Update badge whenever a new notification comes in
106
- const badgeUpdate = () => queueMicrotask(() => this.#updateBadge());
107
- const events = ['toast:show', 'notification:show', 'log:entry',
108
- 'workspace:save:failed', 'workspace:import:failed'];
109
-
110
- events.forEach(name => {
111
- const handle = this.eventBus.on(name, badgeUpdate);
112
- this._subscriptions.push(() => handle?.dispose?.());
113
- });
114
- }
115
-
116
109
  /** The Alerts tab, asked for on the bus — or the embedder's own verb. */
117
110
  #openAlerts() {
118
111
  if (this.onOpen) {
@@ -126,7 +119,7 @@ export class NotificationHistory {
126
119
  const historyLen = this.notificationCenter?.history?.length ?? 0;
127
120
  const unread = this.alertLog
128
121
  ? this.alertLog.unreadCount()
129
- : Math.max(0, historyLen - this.lastSeenCount);
122
+ : Math.min(this.unreadCount, historyLen);
130
123
 
131
124
  if (!this.badgeEl) return;
132
125
 
@@ -164,7 +157,7 @@ export class NotificationHistory {
164
157
  this.#closePanel();
165
158
 
166
159
  // Mark all as seen
167
- this.lastSeenCount = this.notificationCenter?.history?.length ?? 0;
160
+ this.unreadCount = 0;
168
161
  this.#updateBadge();
169
162
 
170
163
  const panel = document.createElement('div');
@@ -197,7 +190,7 @@ export class NotificationHistory {
197
190
  header.querySelector('.twm-notification-history__clear-btn')
198
191
  .addEventListener('click', () => {
199
192
  this.notificationCenter.history.splice(0);
200
- this.lastSeenCount = 0;
193
+ this.unreadCount = 0;
201
194
  this.#updateBadge();
202
195
  this.#renderList(list);
203
196
  });
@@ -27,6 +27,14 @@
27
27
  * collapsed state and a hovered button survive. A running progress toast is
28
28
  * never held behind "+N more" (and never dropped by 'drop'); it counts toward
29
29
  * `maxVisible`, so it takes the place of the newest other toast.
30
+ *
31
+ * History subscribers (0.17.3)
32
+ * ----------------------------
33
+ * `history` holds one entry per toast, whichever way it came (`show()`, the
34
+ * `warn()`/`error()`/... helpers, `toast:show`, `notification:show`,
35
+ * `log:entry`). A toast that leaves marks its own entry `dismissed` with a
36
+ * `reason`; it adds none. `subscribe(fn)` calls `fn(entry, center)` for each
37
+ * entry added, so a history view follows the centre rather than the bus.
30
38
  */
31
39
  import { ComponentBase } from './base/component_base.js';
32
40
  import { getSetting } from '../core/settings.js';
@@ -55,6 +63,7 @@ const SUPPRESSED_NAMESPACE_TITLES = new Set(['bootstrap', 'app']);
55
63
  export class NotificationCenter extends ComponentBase {
56
64
  #items;
57
65
  #summaryEl;
66
+ #historyListeners;
58
67
 
59
68
  constructor({
60
69
  eventBus,
@@ -90,6 +99,7 @@ export class NotificationCenter extends ComponentBase {
90
99
  /** id -> the drawn toast's nodes, for the toasts on screen. */
91
100
  this.#items = new Map();
92
101
  this.#summaryEl = null;
102
+ this.#historyListeners = new Set();
93
103
 
94
104
  this.container = null;
95
105
  this.rootEl = null;
@@ -151,6 +161,7 @@ export class NotificationCenter extends ComponentBase {
151
161
  dispose() {
152
162
  this.#clearTimers();
153
163
  this.#unsubscribeEvents();
164
+ this.#historyListeners.clear();
154
165
  // Give the toast helpers back to the console — unless someone has
155
166
  // pointed them at another bus since this centre mounted.
156
167
  if (this.eventBus && getToastBus() === this.eventBus) setToastBus(null);
@@ -190,7 +201,7 @@ export class NotificationCenter extends ComponentBase {
190
201
  if (idx !== -1) {
191
202
  const [removed] = this.notifications.splice(idx, 1);
192
203
  this.#stopTimer(removed.id);
193
- this.#recordHistory({ ...removed, dismissed: true, reason: 'overflow' });
204
+ this.#recordDismissal(removed, 'overflow');
194
205
  }
195
206
  }
196
207
 
@@ -207,13 +218,13 @@ export class NotificationCenter extends ComponentBase {
207
218
  const h = this.held.findIndex((item) => item.id === notificationId);
208
219
  if (h === -1) return false;
209
220
  const [gone] = this.held.splice(h, 1);
210
- this.#recordHistory({ ...gone, dismissed: true, reason });
221
+ this.#recordDismissal(gone, reason);
211
222
  this.#render();
212
223
  return true;
213
224
  }
214
225
  const [removed] = this.notifications.splice(idx, 1);
215
226
  this.#stopTimer(notificationId);
216
- this.#recordHistory({ ...removed, dismissed: true, reason });
227
+ this.#recordDismissal(removed, reason);
217
228
  // A place on screen came free: the oldest held-back toast takes it.
218
229
  this.#rebalance();
219
230
  this.#render();
@@ -475,12 +486,39 @@ export class NotificationCenter extends ComponentBase {
475
486
  this.activeTimers.clear();
476
487
  }
477
488
 
489
+ /**
490
+ * Hear of every entry added to `history`, by `show()`, `warn()`/`error()`/
491
+ * `info()`/`success()` and the bus alike: `listener(entry, center)`, once
492
+ * per toast, after the entry is in `history`. A dismissal changes the
493
+ * toast's own entry and is not an entry of its own. Answers the unsubscribe.
494
+ */
495
+ subscribe(listener) {
496
+ if (typeof listener !== 'function') return () => {};
497
+ this.#historyListeners.add(listener);
498
+ return () => { this.#historyListeners.delete(listener); };
499
+ }
500
+
478
501
  #recordHistory(entry) {
479
502
  if (entry.skipHistory) return;
480
503
  this.history.push(entry);
481
504
  if (this.history.length > this.historyLimit) {
482
505
  this.history.shift();
483
506
  }
507
+ [...this.#historyListeners].forEach((listener) => {
508
+ try {
509
+ listener(entry, this);
510
+ } catch (err) {
511
+ this.logger?.warn?.('twm-notification-center', 'History listener failed', { err });
512
+ }
513
+ });
514
+ }
515
+
516
+ /** A toast that left marks its own entry: one toast, one entry. */
517
+ #recordDismissal(notification, reason) {
518
+ if (notification.skipHistory) return;
519
+ const at = this.history.findLastIndex((entry) => entry.id === notification.id);
520
+ if (at === -1) return;
521
+ this.history[at] = { ...notification, dismissed: true, reason };
484
522
  }
485
523
 
486
524
  /** A progress toast that has not been given an outcome yet. */