flexdesk 0.13.0 → 0.14.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/README.md +53 -0
- package/css/flexdesk.css +99 -0
- package/dist/flexdesk.css +99 -0
- package/dist/flow.js +3 -0
- package/dist/flow.js.map +2 -2
- package/dist/host.js +138 -0
- package/dist/host.js.map +3 -3
- package/dist/widgets.js +563 -5
- package/dist/widgets.js.map +3 -3
- package/dist/wm.js +146 -0
- package/dist/wm.js.map +3 -3
- package/package.json +1 -1
- package/src/flow/kit/dom.js +6 -0
- package/src/host/photino_host.js +176 -0
- package/src/host/photino_host.md +98 -0
- package/src/tiling/bottom_panel_tabs.js +201 -0
- package/src/ui/components/alert_log.js +274 -0
- package/src/ui/components/alerts_pane.js +294 -0
- package/src/ui/components/notification_history.js +54 -4
- package/src/ui/utils/auto_scrollbars.js +30 -0
package/dist/widgets.js
CHANGED
|
@@ -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({
|
|
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.#
|
|
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
|