flexdesk 0.13.0 → 0.14.1

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
@@ -36,7 +36,7 @@ import {
36
36
  parseDateValue,
37
37
  parseDuration,
38
38
  resolvePaging
39
- } from "./chunk-M2L4XW2U.js";
39
+ } from "./chunk-FBGP5KN5.js";
40
40
  import {
41
41
  ManagedWindow,
42
42
  ROW_CONTROL_SELECTOR,
@@ -326,6 +326,503 @@ function showAboutDialog(product = {}) {
326
326
  });
327
327
  }
328
328
 
329
+ // src/ui/components/alert_log.js
330
+ var ALERT_LEVELS = Object.freeze({
331
+ info: Object.freeze({ label: "Info", icon: "info" }),
332
+ warning: Object.freeze({ label: "Warning", icon: "warning" }),
333
+ alert: Object.freeze({ label: "Alert", icon: "notifications_active" })
334
+ });
335
+ var LEVEL_ALIASES = { warn: "warning", error: "alert", danger: "alert", critical: "alert", success: "info" };
336
+ function normalizeLevel(level) {
337
+ const l = String(level ?? "").toLowerCase();
338
+ if (ALERT_LEVELS[l]) return l;
339
+ return LEVEL_ALIASES[l] ?? "info";
340
+ }
341
+ function worstLevel(entries) {
342
+ let worst = null;
343
+ for (const e of entries || []) {
344
+ const l = normalizeLevel(e?.level);
345
+ if (l === "alert") return "alert";
346
+ if (l === "warning") worst = "warning";
347
+ else if (!worst) worst = "info";
348
+ }
349
+ return worst;
350
+ }
351
+ function ago(at, now = /* @__PURE__ */ new Date()) {
352
+ const s = Math.max(0, (now - at) / 1e3);
353
+ if (s < 60) return "just now";
354
+ if (s < 3600) return `${Math.floor(s / 60)} min ago`;
355
+ if (s < 86400) return `${Math.floor(s / 3600)} h ago`;
356
+ return at.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
357
+ }
358
+ function dayLabel(at, now = /* @__PURE__ */ new Date()) {
359
+ const d = new Date(at.getFullYear(), at.getMonth(), at.getDate());
360
+ const t = new Date(now.getFullYear(), now.getMonth(), now.getDate());
361
+ const days = Math.round((t - d) / 864e5);
362
+ if (days === 0) return "TODAY";
363
+ if (days === 1) return "YESTERDAY";
364
+ return at.toLocaleDateString([], { weekday: "short", day: "numeric", month: "short", year: "numeric" }).toUpperCase();
365
+ }
366
+ var _seq = 0;
367
+ function normalizeEntry(raw) {
368
+ const at = raw.at instanceof Date ? raw.at : new Date(raw.at ?? Date.now());
369
+ return {
370
+ ...raw,
371
+ id: String(raw.id ?? `${Date.now().toString(36)}-${(_seq++).toString(36)}`),
372
+ at: Number.isNaN(at.getTime()) ? /* @__PURE__ */ new Date() : at,
373
+ level: normalizeLevel(raw.level),
374
+ title: String(raw.title ?? ""),
375
+ read: !!raw.read
376
+ };
377
+ }
378
+ function localSource({ read, write, limit = 300 } = {}) {
379
+ let all = null;
380
+ const save = () => {
381
+ try {
382
+ write?.(all.map((e) => ({ ...e, at: e.at.toISOString() })));
383
+ } catch (err) {
384
+ console.warn("[alert-log] the log could not be saved", err);
385
+ }
386
+ };
387
+ const ensure = async () => {
388
+ if (all) return all;
389
+ let saved = [];
390
+ try {
391
+ saved = await read?.() ?? [];
392
+ } catch (err) {
393
+ console.warn("[alert-log] the log could not be read", err);
394
+ }
395
+ all = (Array.isArray(saved) ? saved : []).filter((e) => e && typeof e === "object").map(normalizeEntry).sort((a, b) => b.at - a.at).slice(0, limit);
396
+ return all;
397
+ };
398
+ return {
399
+ local: true,
400
+ async load({ offset = 0, limit: n = limit } = {}) {
401
+ const list = await ensure();
402
+ return { entries: list.slice(offset, offset + n), more: offset + n < list.length };
403
+ },
404
+ async add(entry) {
405
+ const list = await ensure();
406
+ list.unshift(entry);
407
+ if (list.length > limit) list.length = limit;
408
+ save();
409
+ },
410
+ async markRead(ids) {
411
+ const want = ids ? new Set(ids) : null;
412
+ for (const e of await ensure()) if (!want || want.has(e.id)) e.read = true;
413
+ save();
414
+ },
415
+ async dismiss(ids) {
416
+ const want = ids ? new Set(ids) : null;
417
+ all = (await ensure()).filter((e) => want && !want.has(e.id));
418
+ save();
419
+ }
420
+ };
421
+ }
422
+ function createAlertLog({ source = null, limit = 300 } = {}) {
423
+ let entries = [];
424
+ let more = false;
425
+ let remoteUnread = null;
426
+ let loaded = false;
427
+ let loading = null;
428
+ const listeners = /* @__PURE__ */ new Set();
429
+ const changed = () => {
430
+ for (const fn of [...listeners]) {
431
+ try {
432
+ fn(log);
433
+ } catch (err) {
434
+ console.warn("[alert-log] listener failed", err);
435
+ }
436
+ }
437
+ };
438
+ const tell = (verb, ...args) => {
439
+ const fn = source?.[verb];
440
+ if (typeof fn !== "function") return;
441
+ Promise.resolve().then(() => fn.apply(source, args)).catch((err) => {
442
+ console.warn(`[alert-log] the source refused ${verb}`, err);
443
+ log.refresh();
444
+ });
445
+ };
446
+ const page = async (offset) => {
447
+ const res = await source?.load?.({ offset, limit }) ?? {};
448
+ const got = (res.entries ?? []).map(normalizeEntry);
449
+ return { got, more: !!res.more, unread: Number.isFinite(res.unread) ? res.unread : null };
450
+ };
451
+ const log = {
452
+ /** Loaded entries, newest first. */
453
+ entries: () => entries,
454
+ loaded: () => loaded,
455
+ /** Whether the source has older entries than those loaded. */
456
+ hasMore: () => more,
457
+ /** Whether entries can be dismissed: a log with no source, or a source that can. */
458
+ canDismiss: () => !source || typeof source.dismiss === "function",
459
+ /** Unread entries: the source's count when it knows one, else the loaded ones. */
460
+ unreadCount() {
461
+ const here = entries.filter((e) => !e.read).length;
462
+ return remoteUnread === null ? here : Math.max(remoteUnread, here);
463
+ },
464
+ /** The worst level among unread entries, or null. */
465
+ worstUnread: () => worstLevel(entries.filter((e) => !e.read)),
466
+ get: (id) => entries.find((e) => e.id === String(id)) ?? null,
467
+ /** `fn(log)` after every change; answers the unsubscribe. */
468
+ subscribe(fn) {
469
+ listeners.add(fn);
470
+ return () => listeners.delete(fn);
471
+ },
472
+ /** The first page, again. */
473
+ refresh() {
474
+ if (!source?.load) {
475
+ loaded = true;
476
+ changed();
477
+ return Promise.resolve(log);
478
+ }
479
+ loading = page(0).then(({ got, more: m, unread }) => {
480
+ entries = got;
481
+ more = m;
482
+ remoteUnread = unread;
483
+ loaded = true;
484
+ changed();
485
+ return log;
486
+ }).catch((err) => {
487
+ console.warn("[alert-log] the log could not be loaded", err);
488
+ loaded = true;
489
+ changed();
490
+ return log;
491
+ }).finally(() => {
492
+ loading = null;
493
+ });
494
+ return loading;
495
+ },
496
+ /** Load once; later calls answer the same promise or nothing. */
497
+ load() {
498
+ return loading ?? (loaded ? Promise.resolve(log) : log.refresh());
499
+ },
500
+ /** The next page, after the loaded ones. */
501
+ async loadMore() {
502
+ if (!more || !source?.load) return log;
503
+ const { got, more: m, unread } = await page(entries.length);
504
+ const seen = new Set(entries.map((e) => e.id));
505
+ entries = [...entries, ...got.filter((e) => !seen.has(e.id))];
506
+ more = m;
507
+ if (unread !== null) remoteUnread = unread;
508
+ changed();
509
+ return log;
510
+ },
511
+ /** A new entry raised HERE (bugdesk's rules): kept, and handed to the source to save. */
512
+ add(raw) {
513
+ const entry = normalizeEntry(raw);
514
+ entries = [entry, ...entries.filter((e) => e.id !== entry.id)];
515
+ if (entries.length > limit) entries.length = limit;
516
+ if (remoteUnread !== null && !entry.read) remoteUnread += 1;
517
+ tell("add", entry);
518
+ changed();
519
+ return entry;
520
+ },
521
+ /** Entries that arrived from the source's side (a push): shown, not saved back. */
522
+ receive(raws) {
523
+ const got = (Array.isArray(raws) ? raws : [raws]).map(normalizeEntry);
524
+ const ids = new Set(got.map((e) => e.id));
525
+ const fresh = got.filter((e) => !entries.some((x) => x.id === e.id));
526
+ entries = [...got, ...entries.filter((e) => !ids.has(e.id))].sort((a, b) => b.at - a.at).slice(0, limit);
527
+ if (remoteUnread !== null) remoteUnread += fresh.filter((e) => !e.read).length;
528
+ changed();
529
+ },
530
+ /** Mark these read — all of them when `ids` is omitted. */
531
+ markRead(ids = null) {
532
+ const want = ids ? new Set(ids.map(String)) : null;
533
+ let n = 0;
534
+ for (const e of entries) if (!e.read && (!want || want.has(e.id))) {
535
+ e.read = true;
536
+ n++;
537
+ }
538
+ if (remoteUnread !== null) remoteUnread = want ? Math.max(0, remoteUnread - n) : 0;
539
+ if (!n && want) return;
540
+ tell("markRead", want ? [...want] : null);
541
+ changed();
542
+ },
543
+ /** Remove these — all of them when `ids` is omitted. Nothing, when the source cannot. */
544
+ dismiss(ids = null) {
545
+ if (!log.canDismiss()) return;
546
+ const want = ids ? new Set(ids.map(String)) : null;
547
+ const gone = entries.filter((e) => !want || want.has(e.id));
548
+ if (!gone.length) return;
549
+ entries = want ? entries.filter((e) => !want.has(e.id)) : [];
550
+ if (remoteUnread !== null) {
551
+ remoteUnread = want ? Math.max(0, remoteUnread - gone.filter((e) => !e.read).length) : 0;
552
+ }
553
+ tell("dismiss", want ? [...want] : null);
554
+ changed();
555
+ }
556
+ };
557
+ if (!source?.load) loaded = true;
558
+ return log;
559
+ }
560
+
561
+ // src/ui/components/alerts_pane.js
562
+ var CHIPS = [["all", "All"], ["alert", "Alert"], ["warning", "Warning"], ["info", "Info"]];
563
+ function mountAlertsPane(host, {
564
+ log,
565
+ onOpen = null,
566
+ onBadge = null,
567
+ rules = null,
568
+ stillMatches = null,
569
+ describe = null,
570
+ status = "",
571
+ empty = {}
572
+ } = {}) {
573
+ const doc = host.ownerDocument;
574
+ const el = (tag, cls, text) => {
575
+ const n = doc.createElement(tag);
576
+ if (cls) n.className = cls;
577
+ if (text !== void 0) n.textContent = text;
578
+ return n;
579
+ };
580
+ const icon = (name) => el("span", "material-symbols-outlined", name);
581
+ let level = "all";
582
+ let rule = null;
583
+ let q = "";
584
+ let pointerDown = false;
585
+ let pending = false;
586
+ let destroyed = false;
587
+ const root = el("div", "twm-alerts");
588
+ let ruleList = null;
589
+ if (rules) {
590
+ const side = el("div", "twm-alerts__rules");
591
+ const head = el("div", "twm-alerts__head");
592
+ head.append(el("span", "twm-alerts__caps", "RULES"), el("span", "twm-alerts__spacer"));
593
+ if (rules.onEdit) {
594
+ const edit = el("button", "twm-alerts__link", "Edit rules");
595
+ edit.type = "button";
596
+ edit.dataset.act = "edit-rules";
597
+ head.append(edit);
598
+ }
599
+ ruleList = el("div", "twm-alerts__rulelist");
600
+ side.append(head, ruleList);
601
+ if (rules.foot) side.append(el("div", "twm-alerts__foot", rules.foot));
602
+ root.append(side);
603
+ }
604
+ const main = el("div", "twm-alerts__main");
605
+ const bar = el("div", "twm-alerts__bar");
606
+ const chips = el("div", "twm-alerts__chips");
607
+ chips.setAttribute("role", "group");
608
+ chips.setAttribute("aria-label", "Level");
609
+ const chipEls = /* @__PURE__ */ new Map();
610
+ for (const [key, label] of CHIPS) {
611
+ const chip = el("button", "twm-alerts__chip");
612
+ chip.type = "button";
613
+ chip.dataset.level = key;
614
+ chip.append(doc.createTextNode(`${label} `), el("span", "twm-alerts__count", "0"));
615
+ chips.append(chip);
616
+ chipEls.set(key, chip);
617
+ }
618
+ const input = el("input", "twm-alerts__q");
619
+ input.type = "text";
620
+ input.autocomplete = "off";
621
+ input.spellcheck = false;
622
+ input.placeholder = "Filter";
623
+ input.setAttribute("aria-label", "Filter alerts");
624
+ const readAll = el("button", "twm-btn twm-alerts__btn", "Mark all read");
625
+ readAll.type = "button";
626
+ readAll.dataset.act = "read-all";
627
+ const clear = el("button", "twm-btn twm-alerts__btn", "Clear");
628
+ clear.type = "button";
629
+ clear.dataset.act = "clear";
630
+ bar.append(chips, input, el("span", "twm-alerts__spacer"), readAll, clear);
631
+ const list = el("div", "twm-alerts__log");
632
+ const foot = el("div", "twm-alerts__status", status);
633
+ foot.hidden = !status;
634
+ main.append(bar, list, foot);
635
+ root.append(main);
636
+ host.append(root);
637
+ const byRule = () => rule ? log.entries().filter((e) => String(e.ruleId) === rule) : log.entries();
638
+ const shown = () => {
639
+ const needle = q.trim().toLowerCase();
640
+ return byRule().filter((e) => level === "all" || e.level === level).filter((e) => !needle || `${e.ref ?? ""} ${e.title} ${e.body ?? ""} ${e.rule ?? ""}`.toLowerCase().includes(needle));
641
+ };
642
+ function row(entry, now) {
643
+ const lv = ALERT_LEVELS[normalizeLevel(entry.level)];
644
+ const r = el("div", `twm-alerts__row twm-alerts__row--${entry.level}${entry.read ? "" : " twm-alerts__row--unread"}`);
645
+ r.dataset.id = entry.id;
646
+ const mark = el("span", "twm-alerts__unread");
647
+ mark.setAttribute("aria-label", entry.read ? "Read" : "Unread");
648
+ const ic = icon(lv.icon);
649
+ ic.classList.add("twm-alerts__icon");
650
+ ic.title = lv.label;
651
+ const time = el("span", "twm-alerts__time", ago(entry.at, now));
652
+ time.title = entry.at.toLocaleString();
653
+ const text = el("span", "twm-alerts__text");
654
+ text.append(el("span", "twm-alerts__title", entry.title));
655
+ if (entry.body) text.append(el("span", "twm-alerts__body", entry.body));
656
+ const cells = [mark, ic, time];
657
+ if (rules) cells.push(el("span", "twm-alerts__rule", entry.rule ?? ""));
658
+ const tag = describe?.(entry);
659
+ cells.push(el("span", tag ? "twm-alerts__tag" : "twm-alerts__tag twm-alerts__tag--none", tag ?? ""));
660
+ const ref = el(entry.ref && onOpen ? "button" : "span", "twm-alerts__ref", entry.ref ?? "");
661
+ if (ref.tagName === "BUTTON") {
662
+ ref.type = "button";
663
+ ref.dataset.open = entry.id;
664
+ }
665
+ cells.push(ref, text);
666
+ if (stillMatches) {
667
+ const still = stillMatches(entry);
668
+ cells.push(el(
669
+ "span",
670
+ `twm-alerts__state${still === false ? " twm-alerts__state--gone" : ""}`,
671
+ still === true ? "still matches" : still === false ? "no longer matches" : ""
672
+ ));
673
+ }
674
+ const acts = el("span", "twm-alerts__acts");
675
+ if (onOpen) {
676
+ const open = el("button", "twm-alerts__icon-btn");
677
+ open.type = "button";
678
+ open.dataset.open = entry.id;
679
+ open.title = "Open";
680
+ open.setAttribute("aria-label", "Open");
681
+ open.append(icon("open_in_new"));
682
+ acts.append(open);
683
+ }
684
+ if (log.canDismiss()) {
685
+ const dis = el("button", "twm-alerts__icon-btn");
686
+ dis.type = "button";
687
+ dis.dataset.dismiss = entry.id;
688
+ dis.title = "Dismiss";
689
+ dis.setAttribute("aria-label", "Dismiss");
690
+ dis.append(icon("close"));
691
+ acts.append(dis);
692
+ }
693
+ cells.push(acts);
694
+ r.append(...cells);
695
+ return r;
696
+ }
697
+ function paint() {
698
+ if (destroyed) return;
699
+ if (pointerDown) {
700
+ pending = true;
701
+ return;
702
+ }
703
+ pending = false;
704
+ const now = /* @__PURE__ */ new Date();
705
+ const all = log.entries();
706
+ const scoped = byRule();
707
+ for (const [key, chip] of chipEls) {
708
+ const on = level === key;
709
+ chip.classList.toggle("twm-alerts__chip--on", on);
710
+ chip.setAttribute("aria-pressed", String(on));
711
+ chip.querySelector(".twm-alerts__count").textContent = String(scoped.filter((e) => key === "all" || e.level === key).length);
712
+ }
713
+ readAll.disabled = !all.some((e) => !e.read) && log.unreadCount() === 0;
714
+ clear.hidden = !log.canDismiss();
715
+ clear.disabled = !all.length;
716
+ if (ruleList) {
717
+ const items = [{ id: null, name: "All alerts", meta: String(all.length) }, ...rules.list?.() ?? []];
718
+ ruleList.replaceChildren(...items.map((r) => {
719
+ const key = r.id === null ? null : String(r.id);
720
+ const b = el("button", `twm-alerts__rulebtn${rule === key ? " twm-alerts__rulebtn--on" : ""}${r.off ? " twm-alerts__rulebtn--off" : ""}`);
721
+ b.type = "button";
722
+ b.dataset.rule = key ?? "";
723
+ b.setAttribute("aria-pressed", String(rule === key));
724
+ b.append(
725
+ el("span", `twm-alerts__dot${r.level ? ` twm-alerts__dot--${normalizeLevel(r.level)}` : ""}`),
726
+ el("span", "twm-alerts__rulename", r.name || "Unnamed rule"),
727
+ el("span", "twm-alerts__meta", r.meta ?? "")
728
+ );
729
+ return b;
730
+ }));
731
+ }
732
+ const rows = [];
733
+ let lastDay = "";
734
+ for (const entry of shown()) {
735
+ const day = dayLabel(entry.at, now);
736
+ if (day !== lastDay) rows.push(el("div", "twm-alerts__day", day));
737
+ lastDay = day;
738
+ rows.push(row(entry, now));
739
+ }
740
+ if (!rows.length) {
741
+ rows.push(el("div", "twm-alerts__none", !log.loaded() ? "Loading\u2026" : all.length ? empty.filtered ?? "No alerts match." : empty.none ?? "Nothing yet."));
742
+ }
743
+ if (log.hasMore()) {
744
+ const older = el("button", "twm-btn twm-alerts__more", "Load older");
745
+ older.type = "button";
746
+ older.dataset.act = "more";
747
+ rows.push(older);
748
+ }
749
+ const scroll = list.scrollTop;
750
+ list.replaceChildren(...rows);
751
+ list.scrollTop = scroll;
752
+ onBadge?.({ unread: log.unreadCount(), level: log.worstUnread() });
753
+ }
754
+ const onDown = () => {
755
+ pointerDown = true;
756
+ };
757
+ const onUp = () => {
758
+ if (!pointerDown) return;
759
+ setTimeout(() => {
760
+ pointerDown = false;
761
+ if (pending) paint();
762
+ }, 0);
763
+ };
764
+ root.addEventListener("mousedown", onDown, true);
765
+ root.addEventListener("pointerdown", onDown, true);
766
+ doc.addEventListener("mouseup", onUp, true);
767
+ doc.addEventListener("pointerup", onUp, true);
768
+ doc.addEventListener("pointercancel", onUp, true);
769
+ root.addEventListener("click", (event) => {
770
+ const t = event.target;
771
+ const chip = t.closest("[data-level]");
772
+ if (chip) {
773
+ level = chip.dataset.level;
774
+ paint();
775
+ return;
776
+ }
777
+ const r = t.closest("[data-rule]");
778
+ if (r) {
779
+ rule = r.dataset.rule || null;
780
+ paint();
781
+ return;
782
+ }
783
+ const open = t.closest("[data-open]");
784
+ if (open) {
785
+ const entry = log.get(open.dataset.open);
786
+ if (!entry) return;
787
+ log.markRead([entry.id]);
788
+ onOpen?.(entry);
789
+ return;
790
+ }
791
+ const dis = t.closest("[data-dismiss]");
792
+ if (dis) {
793
+ log.dismiss([dis.dataset.dismiss]);
794
+ return;
795
+ }
796
+ const act = t.closest("[data-act]")?.dataset.act;
797
+ if (act === "read-all") log.markRead();
798
+ else if (act === "clear") log.dismiss();
799
+ else if (act === "more") log.loadMore();
800
+ else if (act === "edit-rules") rules?.onEdit?.();
801
+ });
802
+ input.addEventListener("input", () => {
803
+ q = input.value;
804
+ paint();
805
+ });
806
+ const unsubscribe = log.subscribe(() => paint());
807
+ const timer = setInterval(paint, 6e4);
808
+ paint();
809
+ log.load?.();
810
+ return {
811
+ root,
812
+ refresh: paint,
813
+ destroy() {
814
+ if (destroyed) return;
815
+ destroyed = true;
816
+ clearInterval(timer);
817
+ unsubscribe();
818
+ doc.removeEventListener("mouseup", onUp, true);
819
+ doc.removeEventListener("pointerup", onUp, true);
820
+ doc.removeEventListener("pointercancel", onUp, true);
821
+ root.remove();
822
+ }
823
+ };
824
+ }
825
+
329
826
  // src/ui/components/sortable_list.js
330
827
  var SortableList = class {
331
828
  /**
@@ -2573,10 +3070,22 @@ async function mountGalleryPicker(hostEl, opts = {}) {
2573
3070
 
2574
3071
  // src/ui/components/notification_history.js
2575
3072
  var NotificationHistory = class {
2576
- constructor({ notificationCenter, eventBus, logger } = {}) {
3073
+ constructor({
3074
+ notificationCenter,
3075
+ eventBus,
3076
+ logger,
3077
+ mountPoint = null,
3078
+ alertLog = null,
3079
+ alertsTab = "alerts",
3080
+ onOpen = null
3081
+ } = {}) {
2577
3082
  this.notificationCenter = notificationCenter;
2578
3083
  this.eventBus = eventBus || null;
2579
3084
  this.logger = logger || console;
3085
+ this.mountPoint = mountPoint;
3086
+ this.alertLog = alertLog;
3087
+ this.alertsTab = alertsTab;
3088
+ this.onOpen = typeof onOpen === "function" ? onOpen : null;
2580
3089
  this.triggerEl = null;
2581
3090
  this.badgeEl = null;
2582
3091
  this.lastSeenCount = 0;
@@ -2584,7 +3093,7 @@ var NotificationHistory = class {
2584
3093
  this._subscriptions = [];
2585
3094
  }
2586
3095
  mount() {
2587
- const barRight = document.querySelector(".twm-global-bottom-bar .twm-bar-right");
3096
+ const barRight = this.mountPoint || document.querySelector(".twm-global-bottom-bar .twm-bar-right");
2588
3097
  if (!barRight) {
2589
3098
  this.logger.warn?.("[NotificationHistory] .bar-right not found");
2590
3099
  return;
@@ -2598,8 +3107,18 @@ var NotificationHistory = class {
2598
3107
  barRight.appendChild(this.triggerEl);
2599
3108
  this.triggerEl.addEventListener("click", (e) => {
2600
3109
  e.stopPropagation();
2601
- this.#togglePanel();
3110
+ if (this.alertLog) this.#openAlerts();
3111
+ else this.#togglePanel();
2602
3112
  });
3113
+ if (this.alertLog) {
3114
+ this.triggerEl.setAttribute("data-tooltip", "Alerts");
3115
+ this.triggerEl.setAttribute("aria-label", "Alerts");
3116
+ this._subscriptions.push(this.alertLog.subscribe(() => this.#updateBadge()));
3117
+ const summary = this.eventBus?.on?.("notification:summary", () => this.#openAlerts());
3118
+ if (summary) this._subscriptions.push(() => summary.dispose?.());
3119
+ this.#updateBadge();
3120
+ return;
3121
+ }
2603
3122
  this.lastSeenCount = this.notificationCenter?.history?.length ?? 0;
2604
3123
  this.#subscribeEvents();
2605
3124
  }
@@ -2630,10 +3149,27 @@ var NotificationHistory = class {
2630
3149
  this._subscriptions.push(() => this.eventBus.off(name, badgeUpdate));
2631
3150
  });
2632
3151
  }
3152
+ /** The Alerts tab, asked for on the bus — or the embedder's own verb. */
3153
+ #openAlerts() {
3154
+ if (this.onOpen) {
3155
+ try {
3156
+ this.onOpen();
3157
+ } catch (err) {
3158
+ this.logger.warn?.("[NotificationHistory] onOpen failed", err);
3159
+ }
3160
+ return;
3161
+ }
3162
+ this.eventBus?.emit?.("bottompanel:show", { tab: this.alertsTab });
3163
+ }
2633
3164
  #updateBadge() {
2634
3165
  const historyLen = this.notificationCenter?.history?.length ?? 0;
2635
- const unread = Math.max(0, historyLen - this.lastSeenCount);
3166
+ const unread = this.alertLog ? this.alertLog.unreadCount() : Math.max(0, historyLen - this.lastSeenCount);
2636
3167
  if (!this.badgeEl) return;
3168
+ if (this.alertLog) {
3169
+ const level = unread > 0 ? this.alertLog.worstUnread() : null;
3170
+ this.badgeEl.className = "twm-notification-history__badge" + (level ? ` twm-notification-history__badge--${level}` : "");
3171
+ this.triggerEl?.classList.toggle("twm-notification-history__trigger--unread", unread > 0);
3172
+ }
2637
3173
  if (unread > 0) {
2638
3174
  this.badgeEl.textContent = unread > 99 ? "99+" : String(unread);
2639
3175
  this.badgeEl.style.display = "";
@@ -5589,6 +6125,12 @@ function thumbGeometry(scrollSize, clientSize, scrollPos, trackLen) {
5589
6125
  const pos = (trackLen - size) * Math.min(1, Math.max(0, scrollPos / max));
5590
6126
  return { size, pos };
5591
6127
  }
6128
+ function barCovered(el, stacks) {
6129
+ return stacks.some((stack) => {
6130
+ const top = (stack || []).find((h) => !h.closest?.(".twm-autoscroll-layer"));
6131
+ return Boolean(top) && top !== el && !el.contains(top);
6132
+ });
6133
+ }
5592
6134
  function scrollsOn(el, axis) {
5593
6135
  if (!(el instanceof Element)) return false;
5594
6136
  const cs = getComputedStyle(el);
@@ -5677,6 +6219,7 @@ function installAutoScrollbars(root = document.body, { exclude = null } = {}) {
5677
6219
  }
5678
6220
  return rec;
5679
6221
  };
6222
+ const coveredAlong = (el, points) => typeof doc.elementsFromPoint === "function" && barCovered(el, points.map(([x, y]) => doc.elementsFromPoint(x, y)));
5680
6223
  const place = (el) => {
5681
6224
  const rec = managed.get(el);
5682
6225
  if (!rec) return;
@@ -5711,6 +6254,8 @@ function installAutoScrollbars(root = document.body, { exclude = null } = {}) {
5711
6254
  continue;
5712
6255
  }
5713
6256
  Object.assign(thumb.style, { top: `${g.pos}px`, height: `${g.size}px`, left: "0", width: "100%" });
6257
+ const x = right - BAR / 2 - 1;
6258
+ if (coveredAlong(el, [[x, top + 2], [x, top + len / 2], [x, bottom - 2]])) bar.style.display = "none";
5714
6259
  } else {
5715
6260
  const len = right - left;
5716
6261
  Object.assign(bar.style, { left: `${left}px`, top: `${bottom - BAR - 1}px`, width: `${len}px`, height: `${BAR}px` });
@@ -5720,6 +6265,8 @@ function installAutoScrollbars(root = document.body, { exclude = null } = {}) {
5720
6265
  continue;
5721
6266
  }
5722
6267
  Object.assign(thumb.style, { left: `${g.pos}px`, width: `${g.size}px`, top: "0", height: "100%" });
6268
+ const y = bottom - BAR / 2 - 1;
6269
+ if (coveredAlong(el, [[left + 2, y], [left + len / 2, y], [right - 2, y]])) bar.style.display = "none";
5723
6270
  }
5724
6271
  }
5725
6272
  };
@@ -5791,12 +6338,14 @@ function installAutoScrollbars(root = document.body, { exclude = null } = {}) {
5791
6338
  doc.addEventListener("mouseout", onLeaveWindow, true);
5792
6339
  doc.addEventListener("scroll", onScroll, true);
5793
6340
  win.addEventListener("resize", onResize);
6341
+ doc.addEventListener("focusin", onResize, true);
5794
6342
  return {
5795
6343
  dispose() {
5796
6344
  doc.removeEventListener("mouseover", onOver, true);
5797
6345
  doc.removeEventListener("mouseout", onLeaveWindow, true);
5798
6346
  doc.removeEventListener("scroll", onScroll, true);
5799
6347
  win.removeEventListener("resize", onResize);
6348
+ doc.removeEventListener("focusin", onResize, true);
5800
6349
  for (const el of [...managed.keys()]) drop(el);
5801
6350
  root.classList.remove(ROOT_CLASS);
5802
6351
  }
@@ -6530,6 +7079,7 @@ if (typeof window !== "undefined") {
6530
7079
  window.LatexTooltip = TooltipService;
6531
7080
  }
6532
7081
  export {
7082
+ ALERT_LEVELS,
6533
7083
  ActionDropdown,
6534
7084
  AutocompleteField,
6535
7085
  COLUMN_FIT,
@@ -6561,7 +7111,10 @@ export {
6561
7111
  TreeView,
6562
7112
  WindowChromeController,
6563
7113
  WindowTaskbar,
7114
+ ago,
6564
7115
  attachInlineRenamer,
7116
+ barCovered,
7117
+ createAlertLog,
6565
7118
  createChipsFilter,
6566
7119
  createDetailHeader,
6567
7120
  createFactTable,
@@ -6572,6 +7125,7 @@ export {
6572
7125
  createTreeCategory,
6573
7126
  createTreeItem,
6574
7127
  createTreeNode,
7128
+ dayLabel,
6575
7129
  endsTextSelection,
6576
7130
  filterTree,
6577
7131
  fitColumns,
@@ -6588,11 +7142,14 @@ export {
6588
7142
  installWorkspaceScrollbars,
6589
7143
  isRowActivation,
6590
7144
  isRowControl,
7145
+ localSource,
6591
7146
  matchDateFilter,
6592
7147
  matchDurationFilter,
6593
7148
  modalHost,
7149
+ mountAlertsPane,
6594
7150
  mountAttributeListEditor,
6595
7151
  mountGalleryPicker,
7152
+ normalizeLevel,
6596
7153
  openConfirm,
6597
7154
  openForm,
6598
7155
  openModal,
@@ -6621,6 +7178,7 @@ export {
6621
7178
  toastWarn,
6622
7179
  toggleCategory,
6623
7180
  toggleNode,
6624
- updateDetailHeader
7181
+ updateDetailHeader,
7182
+ worstLevel
6625
7183
  };
6626
7184
  //# sourceMappingURL=widgets.js.map