flexdesk 0.17.1 → 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/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
  });
@@ -3939,6 +3929,51 @@ function fitNumber(n, { min, max, step }) {
3939
3929
  if (lo != null && at(k) < lo) k = Math.ceil((lo - base) / s - EPS);
3940
3930
  return at(k);
3941
3931
  }
3932
+ function filterBox(onInput) {
3933
+ const label = el2("label", "twm-settings__search");
3934
+ const input = el2("input", "twm-settings__search-input");
3935
+ input.type = "search";
3936
+ input.placeholder = "Filter settings\u2026";
3937
+ input.autocomplete = "off";
3938
+ input.spellcheck = false;
3939
+ input.setAttribute("aria-label", "Filter settings");
3940
+ input.addEventListener("input", () => onInput(input.value));
3941
+ label.append(icon("search"), input);
3942
+ return { label, input };
3943
+ }
3944
+ function categoryNav(onSelect) {
3945
+ const nav = el2("nav", "twm-settings__nav");
3946
+ nav.setAttribute("aria-label", "Settings categories");
3947
+ nav.addEventListener("click", (e) => {
3948
+ const btn = e.target.closest?.("[data-category]");
3949
+ if (btn && nav.contains(btn)) onSelect(btn.dataset.category);
3950
+ });
3951
+ return nav;
3952
+ }
3953
+ function paintCategories(nav, categories, active) {
3954
+ const buttons = categories.map((cat) => {
3955
+ const b = el2("button", "twm-settings__cat");
3956
+ b.type = "button";
3957
+ b.dataset.category = cat.id;
3958
+ if (cat.id === active) {
3959
+ b.classList.add("twm-settings__cat--active");
3960
+ b.setAttribute("aria-current", "page");
3961
+ }
3962
+ b.append(icon(cat.icon || "tune"), el2("span", "twm-settings__cat-label", cat.label ?? cat.id));
3963
+ if (cat.description) b.title = String(cat.description);
3964
+ return b;
3965
+ });
3966
+ nav.replaceChildren(...buttons);
3967
+ }
3968
+ function resetAllButton(onClick) {
3969
+ const b = el2("button", "twm-btn twm-settings__reset-all");
3970
+ b.type = "button";
3971
+ b.append(icon("restart_alt"), el2("span", "", "Reset all"));
3972
+ b.addEventListener("click", () => {
3973
+ onClick();
3974
+ });
3975
+ return b;
3976
+ }
3942
3977
  var SettingsView = class extends ComponentBase {
3943
3978
  /**
3944
3979
  * @param {object} o
@@ -3948,6 +3983,8 @@ var SettingsView = class extends ComponentBase {
3948
3983
  * @param {string} [o.category] the category shown first (default: the first)
3949
3984
  * @param {number} [o.textDebounceMs]
3950
3985
  * @param {string} [o.title='Settings'] the sidebar heading
3986
+ * @param {boolean} [o.sidebar=true] false leaves the sidebar (filter,
3987
+ * categories, *Reset all*) out, for a `SettingsCategories` elsewhere
3951
3988
  */
3952
3989
  constructor({
3953
3990
  store,
@@ -3956,6 +3993,7 @@ var SettingsView = class extends ComponentBase {
3956
3993
  category = null,
3957
3994
  textDebounceMs = SETTINGS_TEXT_DEBOUNCE_MS,
3958
3995
  title = "Settings",
3996
+ sidebar = true,
3959
3997
  logger
3960
3998
  } = {}) {
3961
3999
  super({ eventBus, logger });
@@ -3971,6 +4009,8 @@ var SettingsView = class extends ComponentBase {
3971
4009
  this._query = "";
3972
4010
  this._debounceMs = textDebounceMs;
3973
4011
  this._title = title;
4012
+ this._sidebar = sidebar !== false;
4013
+ this._listeners = /* @__PURE__ */ new Set();
3974
4014
  this.root = null;
3975
4015
  this._nav = null;
3976
4016
  this._content = null;
@@ -3984,36 +4024,19 @@ var SettingsView = class extends ComponentBase {
3984
4024
  mount(container) {
3985
4025
  if (this.root) return this.root;
3986
4026
  const root = el2("div", "twm-settings");
3987
- const side = el2("aside", "twm-settings__sidebar");
3988
- const head = el2("div", "twm-settings__sidebar-title");
3989
- head.append(icon("settings"), el2("span", "", this._title));
3990
- const search = el2("label", "twm-settings__search");
3991
- const input = el2("input", "twm-settings__search-input");
3992
- input.type = "search";
3993
- input.placeholder = "Filter settings\u2026";
3994
- input.autocomplete = "off";
3995
- input.spellcheck = false;
3996
- input.setAttribute("aria-label", "Filter settings");
3997
- input.addEventListener("input", () => this.setQuery(input.value));
3998
- search.append(icon("search"), input);
3999
- this._search = input;
4000
- const nav = el2("nav", "twm-settings__nav");
4001
- nav.setAttribute("aria-label", "Settings categories");
4002
- nav.addEventListener("click", (e) => {
4003
- const btn = e.target.closest?.("[data-category]");
4004
- if (btn && nav.contains(btn)) this.selectCategory(btn.dataset.category);
4005
- });
4006
- this._nav = nav;
4007
- const resetAll = el2("button", "twm-btn twm-settings__reset-all");
4008
- resetAll.type = "button";
4009
- resetAll.append(icon("restart_alt"), el2("span", "", "Reset all"));
4010
- resetAll.addEventListener("click", () => {
4011
- this.resetAll();
4012
- });
4013
- side.append(head, search, nav, resetAll);
4027
+ if (this._sidebar) {
4028
+ const side = el2("aside", "twm-settings__sidebar");
4029
+ const head = el2("div", "twm-settings__sidebar-title");
4030
+ head.append(icon("settings"), el2("span", "", this._title));
4031
+ const search = filterBox((text) => this.setQuery(text));
4032
+ this._search = search.input;
4033
+ this._nav = categoryNav((id) => this.selectCategory(id));
4034
+ side.append(head, search.label, this._nav, resetAllButton(() => this.resetAll()));
4035
+ root.append(side);
4036
+ }
4014
4037
  const content = el2("div", "twm-settings__content");
4015
4038
  this._content = content;
4016
- root.append(side, content);
4039
+ root.append(content);
4017
4040
  this.root = root;
4018
4041
  container.appendChild(root);
4019
4042
  this._mounted = true;
@@ -4024,6 +4047,7 @@ var SettingsView = class extends ComponentBase {
4024
4047
  this._subs.push(this.eventBus.on("settings:registered", (p) => this.#onRegistered(p)));
4025
4048
  this.#paintNav();
4026
4049
  this.#paintContent();
4050
+ this.#notify();
4027
4051
  return root;
4028
4052
  }
4029
4053
  /** The category on screen (its id), or null. */
@@ -4034,6 +4058,19 @@ var SettingsView = class extends ComponentBase {
4034
4058
  getQuery() {
4035
4059
  return this._query;
4036
4060
  }
4061
+ /** True while the filter is in effect (its trimmed text is not empty). */
4062
+ isFiltering() {
4063
+ return this.#searching();
4064
+ }
4065
+ /**
4066
+ * `fn({ category, query })` after every change of the category, the filter
4067
+ * or the registered categories, until the view is disposed. Answers the
4068
+ * unsubscribe.
4069
+ */
4070
+ subscribe(fn) {
4071
+ this._listeners.add(fn);
4072
+ return () => this._listeners.delete(fn);
4073
+ }
4037
4074
  /** Show one category; clears the filter. */
4038
4075
  selectCategory(id) {
4039
4076
  if (!this.root) return;
@@ -4043,6 +4080,7 @@ var SettingsView = class extends ComponentBase {
4043
4080
  if (this._search) this._search.value = "";
4044
4081
  this.#paintNav();
4045
4082
  this.#paintContent();
4083
+ this.#notify();
4046
4084
  }
4047
4085
  /** Search every category; an empty query goes back to the category. */
4048
4086
  setQuery(query) {
@@ -4052,6 +4090,7 @@ var SettingsView = class extends ComponentBase {
4052
4090
  if (this._search && this._search.value !== q) this._search.value = q;
4053
4091
  this.#paintNav();
4054
4092
  this.#paintContent();
4093
+ this.#notify();
4055
4094
  }
4056
4095
  /** Reset one category after `confirm`; resolves true when it was reset. */
4057
4096
  async resetCategory(id = this.getCategory()) {
@@ -4097,6 +4136,7 @@ var SettingsView = class extends ComponentBase {
4097
4136
  this._nav = null;
4098
4137
  this._content = null;
4099
4138
  this._search = null;
4139
+ this._listeners.clear();
4100
4140
  super.dispose();
4101
4141
  }
4102
4142
  // ── store events ─────────────────────────────────────────────────────
@@ -4115,10 +4155,11 @@ var SettingsView = class extends ComponentBase {
4115
4155
  const touches = this.#searching() || !this._rows.size || paths.some((p) => schema[p]?.category === active);
4116
4156
  if (touches) {
4117
4157
  this.#paintContent();
4118
- return;
4158
+ } else {
4159
+ this.#flushAll();
4160
+ this.#syncAll();
4119
4161
  }
4120
- this.#flushAll();
4121
- this.#syncAll();
4162
+ this.#notify();
4122
4163
  }
4123
4164
  /** True while a filter is in effect: the trimmed query is not empty. */
4124
4165
  #searching() {
@@ -4140,20 +4181,13 @@ var SettingsView = class extends ComponentBase {
4140
4181
  return cats.find((c) => c.id === this._category) ?? cats[0] ?? null;
4141
4182
  }
4142
4183
  #paintNav() {
4143
- const active = this.#searching() ? null : this.getCategory();
4144
- const buttons = this.store.getCategories().map((cat) => {
4145
- const b = el2("button", "twm-settings__cat");
4146
- b.type = "button";
4147
- b.dataset.category = cat.id;
4148
- if (cat.id === active) {
4149
- b.classList.add("twm-settings__cat--active");
4150
- b.setAttribute("aria-current", "page");
4151
- }
4152
- b.append(icon(cat.icon || "tune"), el2("span", "twm-settings__cat-label", cat.label ?? cat.id));
4153
- if (cat.description) b.title = String(cat.description);
4154
- return b;
4155
- });
4156
- this._nav.replaceChildren(...buttons);
4184
+ if (!this._nav) return;
4185
+ paintCategories(this._nav, this.store.getCategories(), this.#searching() ? null : this.getCategory());
4186
+ }
4187
+ /** Tell the listeners, after the content is drawn (it settles the category). */
4188
+ #notify() {
4189
+ const state = { category: this.getCategory(), query: this._query };
4190
+ for (const fn of [...this._listeners]) fn(state);
4157
4191
  }
4158
4192
  #paintContent() {
4159
4193
  this.#flushAll();
@@ -4418,6 +4452,54 @@ var SettingsView = class extends ComponentBase {
4418
4452
  }
4419
4453
  }
4420
4454
  };
4455
+ var SettingsCategories = class extends ComponentBase {
4456
+ /**
4457
+ * @param {object} o
4458
+ * @param {SettingsView} o.view the view it lists the categories of
4459
+ */
4460
+ constructor({ view, logger } = {}) {
4461
+ if (!(view instanceof SettingsView)) {
4462
+ throw new TypeError("SettingsCategories: `view` must be a SettingsView");
4463
+ }
4464
+ super({ eventBus: view.eventBus, logger });
4465
+ this.view = view;
4466
+ this.root = null;
4467
+ this._search = null;
4468
+ this._nav = null;
4469
+ this._unsubscribe = null;
4470
+ }
4471
+ /** Build the list into `container` (once). */
4472
+ mount(container) {
4473
+ if (this.root) return this.root;
4474
+ const view = this.view;
4475
+ const root = el2("div", "twm-settings-categories");
4476
+ const search = filterBox((text) => view.setQuery(text));
4477
+ this._search = search.input;
4478
+ this._nav = categoryNav((id) => view.selectCategory(id));
4479
+ root.append(search.label, this._nav, resetAllButton(() => view.resetAll()));
4480
+ this.root = root;
4481
+ container.appendChild(root);
4482
+ this._mounted = true;
4483
+ this._unsubscribe = view.subscribe(() => this.#paint());
4484
+ this.#paint();
4485
+ return root;
4486
+ }
4487
+ dispose() {
4488
+ this._unsubscribe?.();
4489
+ this._unsubscribe = null;
4490
+ this.root?.remove();
4491
+ this.root = null;
4492
+ this._search = null;
4493
+ this._nav = null;
4494
+ super.dispose();
4495
+ }
4496
+ #paint() {
4497
+ const view = this.view;
4498
+ const q = view.getQuery();
4499
+ if (this._search.value !== q) this._search.value = q;
4500
+ paintCategories(this._nav, view.store.getCategories(), view.isFiltering() ? null : view.getCategory());
4501
+ }
4502
+ };
4421
4503
  function settingsViewContent(opts = {}) {
4422
4504
  return (hostEl, props = {}) => {
4423
4505
  const view = new SettingsView({ ...opts, category: props?.category ?? opts.category ?? null });
@@ -6607,6 +6689,7 @@ var SUPPRESSED_NAMESPACE_TITLES = /* @__PURE__ */ new Set(["bootstrap", "app"]);
6607
6689
  var NotificationCenter = class _NotificationCenter extends ComponentBase {
6608
6690
  #items;
6609
6691
  #summaryEl;
6692
+ #historyListeners;
6610
6693
  constructor({
6611
6694
  eventBus,
6612
6695
  logger,
@@ -6637,6 +6720,7 @@ var NotificationCenter = class _NotificationCenter extends ComponentBase {
6637
6720
  this.eventDisposers = [];
6638
6721
  this.#items = /* @__PURE__ */ new Map();
6639
6722
  this.#summaryEl = null;
6723
+ this.#historyListeners = /* @__PURE__ */ new Set();
6640
6724
  this.container = null;
6641
6725
  this.rootEl = null;
6642
6726
  this.listEl = null;
@@ -6648,7 +6732,7 @@ var NotificationCenter = class _NotificationCenter extends ComponentBase {
6648
6732
  this.container = existingContainer;
6649
6733
  this.rootEl = existingContainer.querySelector(".twm-notification-center");
6650
6734
  this.listEl = existingContainer.querySelector(".twm-notification-center__list");
6651
- this.liveRegionEl = existingContainer.querySelector(".notification-center__live-region");
6735
+ this.liveRegionEl = existingContainer.querySelector(".twm-notification-center__live-region");
6652
6736
  this._mounted = true;
6653
6737
  this.#subscribeEvents();
6654
6738
  this.#claimToastBus();
@@ -6662,7 +6746,7 @@ var NotificationCenter = class _NotificationCenter extends ComponentBase {
6662
6746
  this.listEl = document.createElement("div");
6663
6747
  this.listEl.className = "twm-notification-center__list";
6664
6748
  this.liveRegionEl = document.createElement("div");
6665
- this.liveRegionEl.className = "notification-center__live-region";
6749
+ this.liveRegionEl.className = "twm-notification-center__live-region";
6666
6750
  this.liveRegionEl.setAttribute("aria-live", "polite");
6667
6751
  this.liveRegionEl.setAttribute("aria-atomic", "true");
6668
6752
  this.liveRegionEl.style.position = "absolute";
@@ -6685,6 +6769,7 @@ var NotificationCenter = class _NotificationCenter extends ComponentBase {
6685
6769
  dispose() {
6686
6770
  this.#clearTimers();
6687
6771
  this.#unsubscribeEvents();
6772
+ this.#historyListeners.clear();
6688
6773
  if (this.eventBus && getToastBus() === this.eventBus) setToastBus(null);
6689
6774
  this.#items.forEach((entry) => entry.bar?.dispose());
6690
6775
  this.#items.clear();
@@ -6717,7 +6802,7 @@ var NotificationCenter = class _NotificationCenter extends ComponentBase {
6717
6802
  if (idx !== -1) {
6718
6803
  const [removed] = this.notifications.splice(idx, 1);
6719
6804
  this.#stopTimer(removed.id);
6720
- this.#recordHistory({ ...removed, dismissed: true, reason: "overflow" });
6805
+ this.#recordDismissal(removed, "overflow");
6721
6806
  }
6722
6807
  }
6723
6808
  this.#recordHistory(normalized);
@@ -6732,13 +6817,13 @@ var NotificationCenter = class _NotificationCenter extends ComponentBase {
6732
6817
  const h = this.held.findIndex((item) => item.id === notificationId);
6733
6818
  if (h === -1) return false;
6734
6819
  const [gone] = this.held.splice(h, 1);
6735
- this.#recordHistory({ ...gone, dismissed: true, reason });
6820
+ this.#recordDismissal(gone, reason);
6736
6821
  this.#render();
6737
6822
  return true;
6738
6823
  }
6739
6824
  const [removed] = this.notifications.splice(idx, 1);
6740
6825
  this.#stopTimer(notificationId);
6741
- this.#recordHistory({ ...removed, dismissed: true, reason });
6826
+ this.#recordDismissal(removed, reason);
6742
6827
  this.#rebalance();
6743
6828
  this.#render();
6744
6829
  return true;
@@ -6965,12 +7050,40 @@ var NotificationCenter = class _NotificationCenter extends ComponentBase {
6965
7050
  this.activeTimers.forEach((timer) => clearTimeout(timer));
6966
7051
  this.activeTimers.clear();
6967
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
+ }
6968
7067
  #recordHistory(entry) {
6969
7068
  if (entry.skipHistory) return;
6970
7069
  this.history.push(entry);
6971
7070
  if (this.history.length > this.historyLimit) {
6972
7071
  this.history.shift();
6973
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 };
6974
7087
  }
6975
7088
  /** A progress toast that has not been given an outcome yet. */
6976
7089
  static #isRunning(notification) {
@@ -7085,7 +7198,7 @@ var NotificationCenter = class _NotificationCenter extends ComponentBase {
7085
7198
  if (!this.#summaryEl) {
7086
7199
  const more2 = document.createElement("button");
7087
7200
  more2.type = "button";
7088
- more2.className = "notification twm-notification--summary";
7201
+ more2.className = "twm-notification twm-notification--summary";
7089
7202
  more2.addEventListener("click", () => {
7090
7203
  const held = [...this.held];
7091
7204
  try {
@@ -7212,8 +7325,8 @@ var NotificationCenter = class _NotificationCenter extends ComponentBase {
7212
7325
  #patchItem(entry, notification) {
7213
7326
  const item = entry.el;
7214
7327
  if (entry.severity !== notification.severity) {
7215
- if (entry.severity) item.classList.remove(`notification--${entry.severity}`);
7216
- item.classList.add("notification", `notification--${notification.severity}`);
7328
+ if (entry.severity) item.classList.remove(`twm-notification--${entry.severity}`);
7329
+ item.classList.add("twm-notification", `twm-notification--${notification.severity}`);
7217
7330
  entry.severity = notification.severity;
7218
7331
  }
7219
7332
  if (entry.titleEl) _NotificationCenter.#setText(entry.titleEl, notification.title);
@@ -8393,6 +8506,7 @@ export {
8393
8506
  SCALAR_TYPES,
8394
8507
  SETTINGS_TEXT_DEBOUNCE_MS,
8395
8508
  STEP_STATES,
8509
+ SettingsCategories,
8396
8510
  SettingsView,
8397
8511
  SlideOutPanel,
8398
8512
  SliderField,