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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.17.3 (2026-10-22)
4
+
5
+ A patch release: everything below is additive. Every entry is documented in a
6
+ README section headed "(0.17.3)"; each entry links to it.
7
+
8
+ ### Added and changed
9
+
10
+ - [One toast, one history entry, one unread](README.md#one-toast-one-history-entry-one-unread-0173): `NotificationCenter.history` holds one entry per toast however it was raised (`show()`, `warn()` / `error()` / `info()` / `success()`, `toast:show`, `notification:show`, `log:entry`), and a toast that leaves marks its own entry `dismissed: true` with the `reason` instead of adding a second one. `nc.subscribe(fn)` calls `fn(entry, center)` once per entry added and answers the unsubscribe. `NotificationHistory` follows the centre through `subscribe()`, so every toast counts once on the bell, also one raised through the API (BUG-0034). It no longer watches the bus for toasts: its `notificationCenter` is a `NotificationCenter` (or anything with `history` and `subscribe`).
11
+
12
+ ### Removed or renamed exports
13
+
14
+ None. Compared with 0.17.2, across every entry point (`core`, `widgets`, `wm`,
15
+ `host`, `canvas`, `flow`, `charts`, `tiles`, `editor`) and the package
16
+ `exports` map, no export was added, removed or renamed. Not an export but worth
17
+ knowing: `NotificationCenter` gained the `subscribe(fn)` method (see above).
18
+
3
19
  ## 0.17.2 (2026-10-22)
4
20
 
5
21
  A patch release: everything below is additive. Every entry is documented in a
package/README.md CHANGED
@@ -3527,7 +3527,8 @@ again: its pane hears `onShow` and `onSelect` fires, and nothing is written back
3527
3527
  Toasts, from `toast:show` on the bus (or `toastInfo` / `toastWarn` /
3528
3528
  `toastError` / `toastSuccess`) or `show()`. A payload with `persistent: true`
3529
3529
  stays until its × is clicked. `history` keeps every notification shown, and
3530
- `NotificationHistory` puts it behind a bell in the bottom bar.
3530
+ `NotificationHistory` puts it behind a bell in the bottom bar (one entry per toast;
3531
+ `subscribe(fn)` hears each one, 0.17.3).
3531
3532
 
3532
3533
  *New in 0.11.0:* **`overflow: 'collapse'`.** By default, a toast past
3533
3534
  `maxVisible` (3) pushes the oldest off the screen. With `'collapse'`, the toasts
@@ -3607,6 +3608,32 @@ clicked. A consumer stylesheet that targeted `.notification` should target
3607
3608
  `tests/notification_toast_styles.test.mjs` holds the classes, the cascade and
3608
3609
  the sheets.
3609
3610
 
3611
+ ### One toast, one history entry, one unread (0.17.3)
3612
+
3613
+ `history` holds **one entry per toast**, however it was raised: `show()`, the
3614
+ `warn()` / `error()` / `info()` / `success()` helpers, `toast:show`,
3615
+ `notification:show` or `log:entry`. A toast that leaves (timeout, ×, overflow,
3616
+ `clearAll`) marks its own entry `dismissed: true` with the `reason`, and adds
3617
+ no entry. **`nc.subscribe(fn)`** calls `fn(entry, center)` once for each entry
3618
+ added, after it is in `history` (not for a `skipHistory` toast, and not for a
3619
+ dismissal), and answers the unsubscribe; `dispose()` drops every listener.
3620
+
3621
+ `NotificationHistory` follows the centre through `subscribe()`: every entry
3622
+ recorded is one unread on the bell until the popover is opened, also past
3623
+ `historyLimit`. It no longer watches the bus for toasts, so its
3624
+ `notificationCenter` is a `NotificationCenter` (or anything with `history` and
3625
+ `subscribe`).
3626
+
3627
+ ```js
3628
+ const off = nc.subscribe((entry) => console.log('recorded', entry.severity, entry.message));
3629
+ nc.warn('Disk almost full'); // the bell says 1
3630
+ ```
3631
+
3632
+ Until 0.17.3 the bell counted only on bus events, so a toast raised through
3633
+ `show()` / `warn()` / `error()` / `info()` never raised its badge, and a dismissal
3634
+ was pushed as a second entry: a toast that timed out was in `history` twice
3635
+ and counted twice. `tests/notification_history_follows_center.test.mjs` holds it.
3636
+
3610
3637
  ### Alerts with history: a log, a pane, a tab host and a bell (0.14.0)
3611
3638
 
3612
3639
  A toast is the moment. The **alert log** keeps what came in until it is
package/dist/widgets.js CHANGED
@@ -3033,7 +3033,7 @@ var NotificationHistory = class {
3033
3033
  this.onOpen = typeof onOpen === "function" ? onOpen : null;
3034
3034
  this.triggerEl = null;
3035
3035
  this.badgeEl = null;
3036
- this.lastSeenCount = 0;
3036
+ this.unreadCount = 0;
3037
3037
  this._popoverCleanup = null;
3038
3038
  this._subscriptions = [];
3039
3039
  }
@@ -3064,8 +3064,13 @@ var NotificationHistory = class {
3064
3064
  this.#updateBadge();
3065
3065
  return;
3066
3066
  }
3067
- this.lastSeenCount = this.notificationCenter?.history?.length ?? 0;
3068
- this.#subscribeEvents();
3067
+ this.unreadCount = 0;
3068
+ const unsubscribe = this.notificationCenter?.subscribe?.(() => {
3069
+ this.unreadCount += 1;
3070
+ this.#updateBadge();
3071
+ });
3072
+ if (typeof unsubscribe === "function") this._subscriptions.push(unsubscribe);
3073
+ this.#updateBadge();
3069
3074
  }
3070
3075
  unmount() {
3071
3076
  this.#closePanel();
@@ -3079,21 +3084,6 @@ var NotificationHistory = class {
3079
3084
  this.triggerEl = null;
3080
3085
  this.badgeEl = null;
3081
3086
  }
3082
- #subscribeEvents() {
3083
- if (!this.eventBus) return;
3084
- const badgeUpdate = () => queueMicrotask(() => this.#updateBadge());
3085
- const events = [
3086
- "toast:show",
3087
- "notification:show",
3088
- "log:entry",
3089
- "workspace:save:failed",
3090
- "workspace:import:failed"
3091
- ];
3092
- events.forEach((name) => {
3093
- const handle = this.eventBus.on(name, badgeUpdate);
3094
- this._subscriptions.push(() => handle?.dispose?.());
3095
- });
3096
- }
3097
3087
  /** The Alerts tab, asked for on the bus — or the embedder's own verb. */
3098
3088
  #openAlerts() {
3099
3089
  if (this.onOpen) {
@@ -3108,7 +3098,7 @@ var NotificationHistory = class {
3108
3098
  }
3109
3099
  #updateBadge() {
3110
3100
  const historyLen = this.notificationCenter?.history?.length ?? 0;
3111
- const unread = this.alertLog ? this.alertLog.unreadCount() : Math.max(0, historyLen - this.lastSeenCount);
3101
+ const unread = this.alertLog ? this.alertLog.unreadCount() : Math.min(this.unreadCount, historyLen);
3112
3102
  if (!this.badgeEl) return;
3113
3103
  if (this.alertLog) {
3114
3104
  const level = unread > 0 ? this.alertLog.worstUnread() : null;
@@ -3140,7 +3130,7 @@ var NotificationHistory = class {
3140
3130
  }
3141
3131
  #openPanel() {
3142
3132
  this.#closePanel();
3143
- this.lastSeenCount = this.notificationCenter?.history?.length ?? 0;
3133
+ this.unreadCount = 0;
3144
3134
  this.#updateBadge();
3145
3135
  const panel = document.createElement("div");
3146
3136
  panel.className = "twm-notification-history__panel";
@@ -3155,7 +3145,7 @@ var NotificationHistory = class {
3155
3145
  header.querySelector(".twm-notification-history__close-btn").addEventListener("click", () => this.#closePanel());
3156
3146
  header.querySelector(".twm-notification-history__clear-btn").addEventListener("click", () => {
3157
3147
  this.notificationCenter.history.splice(0);
3158
- this.lastSeenCount = 0;
3148
+ this.unreadCount = 0;
3159
3149
  this.#updateBadge();
3160
3150
  this.#renderList(list);
3161
3151
  });
@@ -6699,6 +6689,7 @@ var SUPPRESSED_NAMESPACE_TITLES = /* @__PURE__ */ new Set(["bootstrap", "app"]);
6699
6689
  var NotificationCenter = class _NotificationCenter extends ComponentBase {
6700
6690
  #items;
6701
6691
  #summaryEl;
6692
+ #historyListeners;
6702
6693
  constructor({
6703
6694
  eventBus,
6704
6695
  logger,
@@ -6729,6 +6720,7 @@ var NotificationCenter = class _NotificationCenter extends ComponentBase {
6729
6720
  this.eventDisposers = [];
6730
6721
  this.#items = /* @__PURE__ */ new Map();
6731
6722
  this.#summaryEl = null;
6723
+ this.#historyListeners = /* @__PURE__ */ new Set();
6732
6724
  this.container = null;
6733
6725
  this.rootEl = null;
6734
6726
  this.listEl = null;
@@ -6777,6 +6769,7 @@ var NotificationCenter = class _NotificationCenter extends ComponentBase {
6777
6769
  dispose() {
6778
6770
  this.#clearTimers();
6779
6771
  this.#unsubscribeEvents();
6772
+ this.#historyListeners.clear();
6780
6773
  if (this.eventBus && getToastBus() === this.eventBus) setToastBus(null);
6781
6774
  this.#items.forEach((entry) => entry.bar?.dispose());
6782
6775
  this.#items.clear();
@@ -6809,7 +6802,7 @@ var NotificationCenter = class _NotificationCenter extends ComponentBase {
6809
6802
  if (idx !== -1) {
6810
6803
  const [removed] = this.notifications.splice(idx, 1);
6811
6804
  this.#stopTimer(removed.id);
6812
- this.#recordHistory({ ...removed, dismissed: true, reason: "overflow" });
6805
+ this.#recordDismissal(removed, "overflow");
6813
6806
  }
6814
6807
  }
6815
6808
  this.#recordHistory(normalized);
@@ -6824,13 +6817,13 @@ var NotificationCenter = class _NotificationCenter extends ComponentBase {
6824
6817
  const h = this.held.findIndex((item) => item.id === notificationId);
6825
6818
  if (h === -1) return false;
6826
6819
  const [gone] = this.held.splice(h, 1);
6827
- this.#recordHistory({ ...gone, dismissed: true, reason });
6820
+ this.#recordDismissal(gone, reason);
6828
6821
  this.#render();
6829
6822
  return true;
6830
6823
  }
6831
6824
  const [removed] = this.notifications.splice(idx, 1);
6832
6825
  this.#stopTimer(notificationId);
6833
- this.#recordHistory({ ...removed, dismissed: true, reason });
6826
+ this.#recordDismissal(removed, reason);
6834
6827
  this.#rebalance();
6835
6828
  this.#render();
6836
6829
  return true;
@@ -7057,12 +7050,40 @@ var NotificationCenter = class _NotificationCenter extends ComponentBase {
7057
7050
  this.activeTimers.forEach((timer) => clearTimeout(timer));
7058
7051
  this.activeTimers.clear();
7059
7052
  }
7053
+ /**
7054
+ * Hear of every entry added to `history`, by `show()`, `warn()`/`error()`/
7055
+ * `info()`/`success()` and the bus alike: `listener(entry, center)`, once
7056
+ * per toast, after the entry is in `history`. A dismissal changes the
7057
+ * toast's own entry and is not an entry of its own. Answers the unsubscribe.
7058
+ */
7059
+ subscribe(listener) {
7060
+ if (typeof listener !== "function") return () => {
7061
+ };
7062
+ this.#historyListeners.add(listener);
7063
+ return () => {
7064
+ this.#historyListeners.delete(listener);
7065
+ };
7066
+ }
7060
7067
  #recordHistory(entry) {
7061
7068
  if (entry.skipHistory) return;
7062
7069
  this.history.push(entry);
7063
7070
  if (this.history.length > this.historyLimit) {
7064
7071
  this.history.shift();
7065
7072
  }
7073
+ [...this.#historyListeners].forEach((listener) => {
7074
+ try {
7075
+ listener(entry, this);
7076
+ } catch (err) {
7077
+ this.logger?.warn?.("twm-notification-center", "History listener failed", { err });
7078
+ }
7079
+ });
7080
+ }
7081
+ /** A toast that left marks its own entry: one toast, one entry. */
7082
+ #recordDismissal(notification, reason) {
7083
+ if (notification.skipHistory) return;
7084
+ const at = this.history.findLastIndex((entry) => entry.id === notification.id);
7085
+ if (at === -1) return;
7086
+ this.history[at] = { ...notification, dismissed: true, reason };
7066
7087
  }
7067
7088
  /** A progress toast that has not been given an outcome yet. */
7068
7089
  static #isRunning(notification) {