@loupekit/sdk 0.10.12-next.57 → 0.10.13-next.58
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 +6 -1
- package/dist/index.global.js +439 -32
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +439 -32
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.global.js
CHANGED
|
@@ -36,6 +36,7 @@ var Loupe = (() => {
|
|
|
36
36
|
Dark is the default; the host element gets .theme-light to flip to light. */
|
|
37
37
|
:host {
|
|
38
38
|
--accent: #6b73e6;
|
|
39
|
+
--accent-soft: rgba(107, 115, 230, 0.12);
|
|
39
40
|
--pin: #ff5842;
|
|
40
41
|
--bg: #14161d;
|
|
41
42
|
--bg-2: #1b1e27;
|
|
@@ -47,6 +48,7 @@ var Loupe = (() => {
|
|
|
47
48
|
}
|
|
48
49
|
:host(.theme-light) {
|
|
49
50
|
--accent: #4a55d6;
|
|
51
|
+
--accent-soft: rgba(74, 85, 214, 0.12);
|
|
50
52
|
--pin: #ff5842;
|
|
51
53
|
--bg: #ffffff;
|
|
52
54
|
--bg-2: #f6f7fb;
|
|
@@ -63,7 +65,7 @@ var Loupe = (() => {
|
|
|
63
65
|
.hl {
|
|
64
66
|
position: fixed; pointer-events: none; z-index: 2147483001;
|
|
65
67
|
border: 2px solid var(--accent);
|
|
66
|
-
background:
|
|
68
|
+
background: var(--accent-soft);
|
|
67
69
|
border-radius: 4px; display: none;
|
|
68
70
|
transition: all 60ms linear;
|
|
69
71
|
}
|
|
@@ -76,7 +78,7 @@ var Loupe = (() => {
|
|
|
76
78
|
/* region selection (during drag) + active-comment outline */
|
|
77
79
|
.selbox {
|
|
78
80
|
position: fixed; pointer-events: none; z-index: 2147483001; display: none;
|
|
79
|
-
border: 2px dashed var(--accent); background:
|
|
81
|
+
border: 2px dashed var(--accent); background: var(--accent-soft); border-radius: 4px;
|
|
80
82
|
}
|
|
81
83
|
.region-box {
|
|
82
84
|
position: fixed; pointer-events: none; z-index: 2147483001; display: none;
|
|
@@ -378,6 +380,12 @@ var Loupe = (() => {
|
|
|
378
380
|
|
|
379
381
|
/* Timeline grouping (project scope) + the repo filter. */
|
|
380
382
|
.daylabel { margin: 8px 2px 0; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); font-weight: 700; }
|
|
383
|
+
/* The page a comment belongs to, shown in the project scope (Settings \u2192 Page paths). */
|
|
384
|
+
.pathtag {
|
|
385
|
+
padding: 1px 6px; border-radius: 999px; background: var(--bg-3); color: var(--muted);
|
|
386
|
+
font-size: 10.5px; font-family: ui-monospace, Menlo, monospace;
|
|
387
|
+
max-width: 130px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
388
|
+
}
|
|
381
389
|
.listhead .reposel {
|
|
382
390
|
margin-left: 6px; font-size: 11px; padding: 3px 6px; border: 1px solid var(--line);
|
|
383
391
|
border-radius: 7px; background: var(--bg-2); color: var(--ink);
|
|
@@ -425,6 +433,97 @@ var Loupe = (() => {
|
|
|
425
433
|
@keyframes loupe-recpulse { 0%,100% { opacity: 1; } 50% { opacity: .25; } }
|
|
426
434
|
@media (prefers-reduced-motion: reduce) { .recbar .recdot { animation: none; } }
|
|
427
435
|
|
|
436
|
+
/* ---------------------------------------------- header popovers (pos + settings) */
|
|
437
|
+
.menu-wrap { position: relative; }
|
|
438
|
+
.menu {
|
|
439
|
+
position: absolute; top: calc(100% + 6px); right: 0; z-index: 30;
|
|
440
|
+
min-width: 214px; padding: 8px; display: none;
|
|
441
|
+
background: var(--bg-2); border: 1px solid var(--line); border-radius: 12px; box-shadow: var(--shadow);
|
|
442
|
+
}
|
|
443
|
+
.menu.open { display: block; }
|
|
444
|
+
.menu-label {
|
|
445
|
+
margin: 2px 5px 6px; font-size: 10px; font-weight: 700; letter-spacing: .06em;
|
|
446
|
+
text-transform: uppercase; color: var(--muted);
|
|
447
|
+
}
|
|
448
|
+
/* dock-position grid: the four layouts as a 2x2 of buttons */
|
|
449
|
+
.pos-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
|
|
450
|
+
.pos-grid button {
|
|
451
|
+
display: flex; align-items: center; gap: 6px; padding: 7px 8px; cursor: pointer;
|
|
452
|
+
border: 1px solid var(--line); border-radius: 8px; background: var(--bg); color: var(--muted);
|
|
453
|
+
font-size: 11.5px; font-weight: 600;
|
|
454
|
+
}
|
|
455
|
+
.pos-grid button:hover { border-color: var(--accent); color: var(--ink); }
|
|
456
|
+
.pos-grid button.on { border-color: var(--accent); color: var(--accent); }
|
|
457
|
+
.pos-grid button svg { width: 14px; height: 14px; flex: none; }
|
|
458
|
+
/* settings rows */
|
|
459
|
+
.menu-row {
|
|
460
|
+
display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%;
|
|
461
|
+
padding: 7px 6px; border: 0; border-radius: 8px; background: transparent; color: var(--ink);
|
|
462
|
+
font-size: 12.5px; text-align: left; cursor: pointer;
|
|
463
|
+
}
|
|
464
|
+
.menu-row:hover { background: var(--bg-3); }
|
|
465
|
+
.menu-sep { height: 1px; margin: 6px 4px; background: var(--line); }
|
|
466
|
+
/* on/off switch inside a settings row */
|
|
467
|
+
.sw { flex: none; position: relative; width: 30px; height: 17px; border-radius: 999px; background: var(--line); transition: background .12s; }
|
|
468
|
+
.sw::after { content: ""; position: absolute; top: 2px; left: 2px; width: 13px; height: 13px; border-radius: 50%; background: #fff; transition: transform .12s; }
|
|
469
|
+
.menu-row[aria-pressed="true"] .sw { background: var(--accent); }
|
|
470
|
+
.menu-row[aria-pressed="true"] .sw::after { transform: translateX(13px); }
|
|
471
|
+
/* accent swatches */
|
|
472
|
+
.acc-dots { display: flex; gap: 8px; padding: 4px 6px 2px; }
|
|
473
|
+
.acc-dot { width: 20px; height: 20px; border-radius: 50%; border: 2px solid transparent; padding: 0; cursor: pointer; }
|
|
474
|
+
.acc-dot.on { border-color: var(--ink); }
|
|
475
|
+
|
|
476
|
+
/* ---------------------------------------------------------------- minimize bar */
|
|
477
|
+
.minbar { display: none; align-items: center; gap: 8px; padding: 9px 10px; cursor: pointer; background: var(--bg-2); }
|
|
478
|
+
.dock.minimized .minbar { display: flex; }
|
|
479
|
+
.dock.minimized .tabs, .dock.minimized .view, .dock.minimized .resize,
|
|
480
|
+
.dock.minimized .dctl [data-role="min"], .dock.minimized .dctl .menu-wrap { display: none !important; }
|
|
481
|
+
.minbar .logo { color: var(--accent); font-size: 14px; }
|
|
482
|
+
.minbar .mtext { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; color: var(--muted); }
|
|
483
|
+
.minbar .mtext b { color: var(--ink); }
|
|
484
|
+
.minbar .mrestore { flex: none; padding: 0 2px; border: 0; background: transparent; color: var(--accent); font-size: 14px; cursor: pointer; }
|
|
485
|
+
|
|
486
|
+
/* ------------------------------------------------------- hint card (once per view) */
|
|
487
|
+
.hint {
|
|
488
|
+
position: relative; margin: 8px 12px 0; padding: 10px 28px 10px 11px;
|
|
489
|
+
border: 1px solid var(--accent); border-radius: 10px; background: var(--bg-2);
|
|
490
|
+
}
|
|
491
|
+
.hint-t { margin-bottom: 3px; font-size: 12.5px; font-weight: 700; color: var(--ink); }
|
|
492
|
+
.hint-b { font-size: 11.5px; line-height: 1.45; color: var(--muted); }
|
|
493
|
+
.hint-off { display: inline-block; margin-top: 6px; padding: 0; border: 0; background: transparent; color: var(--accent); font-size: 11px; text-decoration: underline; cursor: pointer; }
|
|
494
|
+
.hint-x {
|
|
495
|
+
position: absolute; top: 6px; right: 6px; width: 18px; height: 18px; padding: 0;
|
|
496
|
+
border: 0; border-radius: 5px; background: transparent; color: var(--muted); font-size: 13px; line-height: 1; cursor: pointer;
|
|
497
|
+
}
|
|
498
|
+
.hint-x:hover { background: var(--bg-3); color: var(--ink); }
|
|
499
|
+
|
|
500
|
+
/* ---------------------------------------------------------------- guided tour */
|
|
501
|
+
/* Non-modal on purpose: the dimmer and the spotlight are click-through, so a tour
|
|
502
|
+
can never trap someone mid-task. Only the card itself takes pointer events. */
|
|
503
|
+
.tour { position: fixed; inset: 0; z-index: 2147483200; display: none; pointer-events: none; }
|
|
504
|
+
.tour.open { display: block; }
|
|
505
|
+
.tour-spot {
|
|
506
|
+
position: fixed; border: 2px solid var(--accent); border-radius: 10px; pointer-events: none;
|
|
507
|
+
box-shadow: 0 0 0 9999px rgba(8, 10, 16, .62);
|
|
508
|
+
transition: all .18s ease;
|
|
509
|
+
}
|
|
510
|
+
.tour-card {
|
|
511
|
+
position: fixed; width: 252px; padding: 12px; pointer-events: auto;
|
|
512
|
+
background: var(--bg-2); border: 1px solid var(--line); border-radius: 12px; box-shadow: var(--shadow); color: var(--ink);
|
|
513
|
+
}
|
|
514
|
+
.tour-title { margin-bottom: 4px; font-size: 13px; font-weight: 700; }
|
|
515
|
+
.tour-body { font-size: 11.5px; line-height: 1.5; color: var(--muted); }
|
|
516
|
+
.tour-foot { display: flex; align-items: center; gap: 6px; margin-top: 10px; }
|
|
517
|
+
.tour-dots { display: flex; flex: 1; gap: 4px; }
|
|
518
|
+
.tour-dots i { width: 5px; height: 5px; border-radius: 50%; background: var(--line); }
|
|
519
|
+
.tour-dots i.on { background: var(--accent); }
|
|
520
|
+
.tour-foot button {
|
|
521
|
+
padding: 5px 9px; border: 1px solid var(--line); border-radius: 7px;
|
|
522
|
+
background: var(--bg); color: var(--ink); font-size: 11.5px; font-weight: 600; cursor: pointer;
|
|
523
|
+
}
|
|
524
|
+
.tour-foot .t-next { border-color: var(--accent); background: var(--accent); color: #fff; }
|
|
525
|
+
.tour-foot .t-skip { border: 0; background: transparent; color: var(--muted); }
|
|
526
|
+
|
|
428
527
|
/* Mobile: left/right/float docking is a desktop affordance. On small screens the
|
|
429
528
|
panel collapses to a bottom sheet that OVERLAYS the page (pushing a side dock
|
|
430
529
|
here would squeeze the page to a useless sliver), regardless of the chosen dock
|
|
@@ -435,7 +534,8 @@ var Loupe = (() => {
|
|
|
435
534
|
width: auto !important; height: 76vh !important;
|
|
436
535
|
border-width: 1px 0 0 0 !important; border-radius: 16px 16px 0 0 !important;
|
|
437
536
|
}
|
|
438
|
-
.dctl [data-
|
|
537
|
+
.dctl [data-role="pos"], .dctl .gap { display: none; } /* dock positions don't apply on mobile */
|
|
538
|
+
.menu { min-width: 190px; }
|
|
439
539
|
.resize { display: none !important; }
|
|
440
540
|
.tools button { flex: 1; justify-content: center; } /* full-width tap targets */
|
|
441
541
|
.dock.mode-bottom .list { grid-template-columns: 1fr; }
|
|
@@ -525,10 +625,10 @@ var Loupe = (() => {
|
|
|
525
625
|
const t = normText(el2.textContent);
|
|
526
626
|
add(W.text, t === a.text ? 1 : t && (t.includes(a.text) || a.text.includes(t)) ? 0.5 : 0);
|
|
527
627
|
}
|
|
528
|
-
const attrKeys = Object.keys(a.attrs);
|
|
628
|
+
const attrKeys = Object.keys(a.attrs ?? {});
|
|
529
629
|
if (attrKeys.length) {
|
|
530
630
|
let matched = 0;
|
|
531
|
-
for (const k of attrKeys) if (el2.getAttribute(k) === a.attrs[k]) matched++;
|
|
631
|
+
for (const k of attrKeys) if (el2.getAttribute(k) === (a.attrs ?? {})[k]) matched++;
|
|
532
632
|
add(W.attrs, matched / attrKeys.length);
|
|
533
633
|
}
|
|
534
634
|
if (a.testid) add(W.testid, stableId(el2) === a.testid ? 1 : 0);
|
|
@@ -536,9 +636,11 @@ var Loupe = (() => {
|
|
|
536
636
|
const r = el2.getBoundingClientRect();
|
|
537
637
|
const cx = r.left + window.scrollX + r.width / 2;
|
|
538
638
|
const cy = r.top + window.scrollY + r.height / 2;
|
|
539
|
-
const
|
|
540
|
-
const
|
|
541
|
-
const
|
|
639
|
+
const ar = a.rect ?? { x: 0, y: 0, w: 0, h: 0 };
|
|
640
|
+
const av = a.viewport ?? { w: 0, h: 0 };
|
|
641
|
+
const ax = ar.x + ar.w / 2;
|
|
642
|
+
const ay = ar.y + ar.h / 2;
|
|
643
|
+
const diag = Math.hypot(av.w, av.h) || 1;
|
|
542
644
|
const dist = Math.hypot(cx - ax, cy - ay);
|
|
543
645
|
add(W.position, Math.max(0, 1 - dist / diag));
|
|
544
646
|
return total ? sum / total : 0;
|
|
@@ -2648,7 +2750,48 @@ var Loupe = (() => {
|
|
|
2648
2750
|
}
|
|
2649
2751
|
|
|
2650
2752
|
// src/app.ts
|
|
2651
|
-
var SDK_VERSION = true ? "0.10.
|
|
2753
|
+
var SDK_VERSION = true ? "0.10.13" : "dev";
|
|
2754
|
+
var ACCENTS = [
|
|
2755
|
+
{ id: "indigo", dark: "#6b73e6", light: "#4a55d6", soft: "rgba(107,115,230,0.12)" },
|
|
2756
|
+
{ id: "violet", dark: "#a06be6", light: "#7c3fd4", soft: "rgba(160,107,230,0.14)" },
|
|
2757
|
+
{ id: "teal", dark: "#2fb6a8", light: "#0f8f83", soft: "rgba(47,182,168,0.14)" },
|
|
2758
|
+
{ id: "amber", dark: "#d99a2b", light: "#a9700f", soft: "rgba(217,154,43,0.14)" },
|
|
2759
|
+
{ id: "rose", dark: "#e05c86", light: "#c2295a", soft: "rgba(224,92,134,0.14)" }
|
|
2760
|
+
];
|
|
2761
|
+
var ACCENT_IDS = ACCENTS.map((a) => a.id);
|
|
2762
|
+
var TOUR = [
|
|
2763
|
+
// Home first: the panel already opens there, so the first step never moves the
|
|
2764
|
+
// user — the tour starts where they are.
|
|
2765
|
+
{
|
|
2766
|
+
sel: ".hstat",
|
|
2767
|
+
tab: "home",
|
|
2768
|
+
title: "Home shows what needs you",
|
|
2769
|
+
body: "Four tiles count open, needs-you, resolved and stale feedback. Click one to narrow the list to that bucket."
|
|
2770
|
+
},
|
|
2771
|
+
{
|
|
2772
|
+
sel: ".hscope",
|
|
2773
|
+
tab: "home",
|
|
2774
|
+
title: "This page, or the whole project",
|
|
2775
|
+
body: "Switch to All to see every page's feedback as a day-grouped timeline, with a repo filter."
|
|
2776
|
+
},
|
|
2777
|
+
{
|
|
2778
|
+
sel: ".tools",
|
|
2779
|
+
tab: "comments",
|
|
2780
|
+
title: "Pin feedback anywhere",
|
|
2781
|
+
body: "Inspect picks an element, Note drops a page-level comment, Region captures a rectangle, and Record films one."
|
|
2782
|
+
},
|
|
2783
|
+
{
|
|
2784
|
+
sel: '.tabs [data-tab="connect"]',
|
|
2785
|
+
tab: "connect",
|
|
2786
|
+
title: "Hand it to Claude",
|
|
2787
|
+
body: "Connect Claude wires up the MCP server so an agent reads this feedback with its element context."
|
|
2788
|
+
}
|
|
2789
|
+
];
|
|
2790
|
+
var HINTS = {
|
|
2791
|
+
home: { title: "Your triage at a glance", body: "The tiles count this page by default. Switch to All for the whole project, or click a tile to jump straight to that bucket." },
|
|
2792
|
+
comments: { title: "Pin, note or record", body: "Inspect selects an element, Note comments anywhere on the page, Region screenshots a rectangle, and Record captures video of one." },
|
|
2793
|
+
connect: { title: "Claude reads these", body: "Add the MCP server to your client and it can list, read and answer this feedback \u2014 screenshot included." }
|
|
2794
|
+
};
|
|
2652
2795
|
var DOCK_MODES = ["left", "right", "bottom", "float"];
|
|
2653
2796
|
var RECORD_MAX_MS = 2e4;
|
|
2654
2797
|
var MAX_FILES = 10;
|
|
@@ -2727,6 +2870,20 @@ var Loupe = (() => {
|
|
|
2727
2870
|
this.statFilter = "";
|
|
2728
2871
|
/** Repo filter, offered once the scope is "all". */
|
|
2729
2872
|
this.repoFilter = "";
|
|
2873
|
+
/** Accent preset id (see ACCENTS), applied as inline --accent / --accent-soft. */
|
|
2874
|
+
this.accent = "indigo";
|
|
2875
|
+
/** Collapsed to the one-line minimize bar. */
|
|
2876
|
+
this.minimized = false;
|
|
2877
|
+
/** The "help layer": FAB tooltips and the contextual hint cards. */
|
|
2878
|
+
this.hoverHints = true;
|
|
2879
|
+
/** Show each comment's page path (useful in the project scope). */
|
|
2880
|
+
this.showPaths = false;
|
|
2881
|
+
/** Views whose hint card has already been shown. */
|
|
2882
|
+
this.hintsSeen = /* @__PURE__ */ new Set();
|
|
2883
|
+
/** Current guided-tour step, or -1 when the tour is closed. */
|
|
2884
|
+
this.tourStep = -1;
|
|
2885
|
+
/** The first-run tour has been finished or skipped. */
|
|
2886
|
+
this.tourDone = false;
|
|
2730
2887
|
/** Float-mode window geometry; (x<=0 && y<=0) → placed on first layout. */
|
|
2731
2888
|
this.floatRect = { x: 0, y: 0, w: 380, h: 540 };
|
|
2732
2889
|
this.floatDrag = null;
|
|
@@ -2851,7 +3008,12 @@ var Loupe = (() => {
|
|
|
2851
3008
|
}
|
|
2852
3009
|
let elx = this.resolved.get(c.id) ?? null;
|
|
2853
3010
|
if (!elx || !elx.isConnected) {
|
|
2854
|
-
|
|
3011
|
+
let r = null;
|
|
3012
|
+
try {
|
|
3013
|
+
r = resolveAnchor(c.anchor);
|
|
3014
|
+
} catch {
|
|
3015
|
+
r = null;
|
|
3016
|
+
}
|
|
2855
3017
|
elx = r?.element ?? null;
|
|
2856
3018
|
this.resolved.set(c.id, elx);
|
|
2857
3019
|
}
|
|
@@ -2894,6 +3056,8 @@ var Loupe = (() => {
|
|
|
2894
3056
|
}
|
|
2895
3057
|
};
|
|
2896
3058
|
this.onWinResize = () => this.applyDockLayout();
|
|
3059
|
+
/** Which view's hint has been handled this session (so it is not re-painted). */
|
|
3060
|
+
this.hintFor = null;
|
|
2897
3061
|
// ---- float-mode drag + resize ---------------------------------------------
|
|
2898
3062
|
this.onHeadPointerDown = (e) => {
|
|
2899
3063
|
if (this.dockMode !== "float" || e.button !== 0 || this.isMobile()) return;
|
|
@@ -2959,6 +3123,7 @@ var Loupe = (() => {
|
|
|
2959
3123
|
if (this.scope === "all") void this.loadAllComments();
|
|
2960
3124
|
this.observe();
|
|
2961
3125
|
this.watchNavigation();
|
|
3126
|
+
if (!this.tourDone && this.open && !this.isMobile()) this.startTour();
|
|
2962
3127
|
if (this.cfg.autoOpen) this.setMode("inspect");
|
|
2963
3128
|
}
|
|
2964
3129
|
/**
|
|
@@ -3011,7 +3176,16 @@ var Loupe = (() => {
|
|
|
3011
3176
|
this.dock = this.buildDock();
|
|
3012
3177
|
this.fabCluster = this.buildFabCluster();
|
|
3013
3178
|
this.recBar = this.buildRecBar();
|
|
3014
|
-
this.
|
|
3179
|
+
this.tourEl = el("div", "tour");
|
|
3180
|
+
this.tourSpot = el("div", "tour-spot");
|
|
3181
|
+
this.tourCard = el("div", "tour-card");
|
|
3182
|
+
this.tourEl.append(this.tourSpot, this.tourCard);
|
|
3183
|
+
this.shadow.append(this.dock, this.fabCluster, this.recBar, this.tourEl);
|
|
3184
|
+
this.shadow.addEventListener("click", (e) => {
|
|
3185
|
+
const t = e.target;
|
|
3186
|
+
if (t?.closest && t.closest(".menu-wrap")) return;
|
|
3187
|
+
this.closeMenus();
|
|
3188
|
+
});
|
|
3015
3189
|
}
|
|
3016
3190
|
/**
|
|
3017
3191
|
* The dockable control panel:
|
|
@@ -3026,33 +3200,71 @@ var Loupe = (() => {
|
|
|
3026
3200
|
brand.querySelector(".title").textContent = this.cfg.label ?? "Loupe";
|
|
3027
3201
|
head.addEventListener("pointerdown", this.onHeadPointerDown);
|
|
3028
3202
|
const ctl = el("div", "dctl");
|
|
3029
|
-
const
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3203
|
+
const posWrap = el("div", "menu-wrap");
|
|
3204
|
+
const posBtn = el("button");
|
|
3205
|
+
posBtn.dataset.role = "pos";
|
|
3206
|
+
posBtn.title = "Panel position";
|
|
3207
|
+
posBtn.setAttribute("aria-label", "Panel position");
|
|
3208
|
+
posBtn.innerHTML = I_DOCK_RIGHT;
|
|
3209
|
+
this.posMenu = el("div", "menu");
|
|
3210
|
+
const posMeta = [
|
|
3211
|
+
{ mode: "left", icon: I_DOCK_LEFT, label: "Left" },
|
|
3212
|
+
{ mode: "bottom", icon: I_DOCK_BOTTOM, label: "Bottom" },
|
|
3213
|
+
{ mode: "right", icon: I_DOCK_RIGHT, label: "Right" },
|
|
3214
|
+
{ mode: "float", icon: I_FLOAT, label: "Float" }
|
|
3215
|
+
];
|
|
3216
|
+
this.posMenu.innerHTML = `<div class="menu-label">Position</div><div class="pos-grid">` + posMeta.map((m) => `<button data-pos="${m.mode}">${m.icon}<span>${m.label}</span></button>`).join("") + `</div>`;
|
|
3217
|
+
this.posMenu.querySelectorAll("[data-pos]").forEach((b) => {
|
|
3218
|
+
b.onclick = () => {
|
|
3219
|
+
this.setDock(b.dataset.pos);
|
|
3220
|
+
this.closeMenus();
|
|
3221
|
+
};
|
|
3222
|
+
});
|
|
3223
|
+
posBtn.onclick = () => this.toggleMenu(this.posMenu);
|
|
3224
|
+
posWrap.append(posBtn, this.posMenu);
|
|
3045
3225
|
this.themeBtn = el("button");
|
|
3046
3226
|
this.themeBtn.dataset.role = "theme";
|
|
3047
3227
|
this.themeBtn.onclick = () => this.toggleTheme();
|
|
3228
|
+
const setWrap = el("div", "menu-wrap");
|
|
3229
|
+
const setBtn = el("button");
|
|
3230
|
+
setBtn.dataset.role = "settings";
|
|
3231
|
+
setBtn.title = "Settings";
|
|
3232
|
+
setBtn.setAttribute("aria-label", "Settings");
|
|
3233
|
+
setBtn.innerHTML = I_GEAR;
|
|
3234
|
+
setBtn.onclick = () => this.toggleMenu(this.settingsMenu);
|
|
3235
|
+
this.settingsMenu = el("div", "menu");
|
|
3236
|
+
this.settingsMenu.innerHTML = `<div class="menu-label">Appearance</div><div class="acc-dots">` + ACCENTS.map((a) => `<button class="acc-dot" data-accent="${a.id}" style="background:${a.dark}" title="${a.id}" aria-label="${a.id} accent"></button>`).join("") + `</div><div class="menu-sep"></div><div class="menu-label">Show</div><button class="menu-row" data-set="hoverHints" aria-pressed="true"><span>Hover hints</span><span class="sw"></span></button><button class="menu-row" data-set="markersHidden" aria-pressed="true"><span>Markers</span><span class="sw"></span></button><button class="menu-row" data-set="showPaths" aria-pressed="false"><span>Page paths</span><span class="sw"></span></button><div class="menu-sep"></div><button class="menu-row" data-set="tour"><span>Restart tour</span></button>`;
|
|
3237
|
+
this.settingsMenu.querySelectorAll("[data-accent]").forEach((b) => {
|
|
3238
|
+
b.onclick = () => this.setAccent(b.dataset.accent);
|
|
3239
|
+
});
|
|
3240
|
+
this.settingsMenu.querySelectorAll("[data-set]").forEach((b) => {
|
|
3241
|
+
b.onclick = () => {
|
|
3242
|
+
const key = b.dataset.set;
|
|
3243
|
+
if (key === "tour") {
|
|
3244
|
+
this.closeMenus();
|
|
3245
|
+
this.startTour();
|
|
3246
|
+
return;
|
|
3247
|
+
}
|
|
3248
|
+
this.toggleSetting(key);
|
|
3249
|
+
};
|
|
3250
|
+
});
|
|
3251
|
+
setWrap.append(setBtn, this.settingsMenu);
|
|
3252
|
+
const minBtn = el("button");
|
|
3253
|
+
minBtn.dataset.role = "min";
|
|
3254
|
+
minBtn.title = "Minimize";
|
|
3255
|
+
minBtn.setAttribute("aria-label", "Minimize");
|
|
3256
|
+
minBtn.innerHTML = I_MINIMIZE;
|
|
3257
|
+
minBtn.onclick = () => this.setMinimized(true);
|
|
3048
3258
|
const closeBtn = el("button");
|
|
3049
3259
|
closeBtn.dataset.role = "close";
|
|
3050
3260
|
closeBtn.title = "Close";
|
|
3051
3261
|
closeBtn.setAttribute("aria-label", "Close");
|
|
3052
3262
|
closeBtn.innerHTML = I_CLOSE;
|
|
3053
3263
|
closeBtn.onclick = () => this.closeDock();
|
|
3054
|
-
ctl.append(this.themeBtn, closeBtn);
|
|
3264
|
+
ctl.append(posWrap, this.themeBtn, setWrap, minBtn, closeBtn);
|
|
3055
3265
|
head.append(brand, ctl);
|
|
3266
|
+
this.minBar = el("div", "minbar");
|
|
3267
|
+
this.minBar.onclick = () => this.setMinimized(false);
|
|
3056
3268
|
const tabs = el("div", "tabs");
|
|
3057
3269
|
const tabBtn = (key, label) => {
|
|
3058
3270
|
const b = el("button", "tab", label);
|
|
@@ -3110,11 +3322,14 @@ var Loupe = (() => {
|
|
|
3110
3322
|
const homeView = el("div", "view home-view");
|
|
3111
3323
|
this.homeEl = homeView;
|
|
3112
3324
|
const commentsView = el("div", "view comments-view");
|
|
3113
|
-
|
|
3325
|
+
this.commentsHint = el("div", "hint-slot");
|
|
3326
|
+
commentsView.append(this.commentsHint, tools, listHead, this.listEl, this.buildIntegrations());
|
|
3114
3327
|
const connectView = this.buildConnectPanel();
|
|
3328
|
+
this.connectHint = el("div", "hint-slot");
|
|
3329
|
+
connectView.prepend(this.connectHint);
|
|
3115
3330
|
const resize = el("div", "resize");
|
|
3116
3331
|
resize.addEventListener("pointerdown", this.onResizeDown);
|
|
3117
|
-
dock.append(head, tabs, homeView, commentsView, connectView, resize);
|
|
3332
|
+
dock.append(head, this.minBar, tabs, homeView, commentsView, connectView, resize);
|
|
3118
3333
|
this.buildHomePanel();
|
|
3119
3334
|
return dock;
|
|
3120
3335
|
}
|
|
@@ -3125,7 +3340,7 @@ var Loupe = (() => {
|
|
|
3125
3340
|
*/
|
|
3126
3341
|
buildHomePanel() {
|
|
3127
3342
|
const title = this.cfg.label ?? "Loupe";
|
|
3128
|
-
this.homeEl.innerHTML = `<div class="hstat" id="loupe-hstats"></div><div class="hscope"><button class="hscope-b" data-scope="page">This page</button><button class="hscope-b" data-scope="all">All</button><button class="hrefresh" title="Refresh" aria-label="Refresh">\u27F3</button></div><button class="hpin" data-role="home-pin">\u271B Pin feedback on this page</button><div class="hlabel">Recent</div><div class="hfeed" id="loupe-hfeed"></div><div class="hfoot">${escapeHtml(title)} \xB7 ${escapeHtml(this.cfg.repo ?? "no repo linked")}</div>`;
|
|
3343
|
+
this.homeEl.innerHTML = `<div class="hint-slot" id="loupe-hhint"></div><div class="hstat" id="loupe-hstats"></div><div class="hscope"><button class="hscope-b" data-scope="page">This page</button><button class="hscope-b" data-scope="all">All</button><button class="hrefresh" title="Refresh" aria-label="Refresh">\u27F3</button></div><button class="hpin" data-role="home-pin">\u271B Pin feedback on this page</button><div class="hlabel">Recent</div><div class="hfeed" id="loupe-hfeed"></div><div class="hfoot">${escapeHtml(title)} \xB7 ${escapeHtml(this.cfg.repo ?? "no repo linked")}</div>`;
|
|
3129
3344
|
this.homeEl.querySelectorAll(".hscope-b").forEach((b) => {
|
|
3130
3345
|
b.onclick = () => this.setScope(b.dataset.scope === "all" ? "all" : "page");
|
|
3131
3346
|
});
|
|
@@ -3868,6 +4083,7 @@ var Loupe = (() => {
|
|
|
3868
4083
|
if (this.tab === tab) return;
|
|
3869
4084
|
if (tab !== "comments") this.setMode("off");
|
|
3870
4085
|
this.tab = tab;
|
|
4086
|
+
this.hintFor = null;
|
|
3871
4087
|
this.saveState();
|
|
3872
4088
|
if (tab === "home") {
|
|
3873
4089
|
this.renderHome();
|
|
@@ -3881,6 +4097,160 @@ var Loupe = (() => {
|
|
|
3881
4097
|
this.saveState();
|
|
3882
4098
|
this.applyDockLayout();
|
|
3883
4099
|
}
|
|
4100
|
+
// ---- panel shell: popovers, minimize, accent ------------------------------
|
|
4101
|
+
toggleMenu(menu) {
|
|
4102
|
+
const wasOpen = menu.classList.contains("open");
|
|
4103
|
+
this.closeMenus();
|
|
4104
|
+
if (!wasOpen) menu.classList.add("open");
|
|
4105
|
+
}
|
|
4106
|
+
closeMenus() {
|
|
4107
|
+
this.posMenu?.classList.remove("open");
|
|
4108
|
+
this.settingsMenu?.classList.remove("open");
|
|
4109
|
+
}
|
|
4110
|
+
setAccent(id) {
|
|
4111
|
+
if (!ACCENT_IDS.includes(id)) return;
|
|
4112
|
+
this.accent = id;
|
|
4113
|
+
this.saveState();
|
|
4114
|
+
this.applyDockLayout();
|
|
4115
|
+
}
|
|
4116
|
+
setMinimized(v) {
|
|
4117
|
+
this.minimized = v;
|
|
4118
|
+
this.closeMenus();
|
|
4119
|
+
this.saveState();
|
|
4120
|
+
this.applyDockLayout();
|
|
4121
|
+
}
|
|
4122
|
+
/** Flip one of the "Show …" settings and repaint whatever it governs. */
|
|
4123
|
+
toggleSetting(key) {
|
|
4124
|
+
if (key === "markersHidden") this.markersHidden = !this.markersHidden;
|
|
4125
|
+
else if (key === "hoverHints") this.hoverHints = !this.hoverHints;
|
|
4126
|
+
else this.showPaths = !this.showPaths;
|
|
4127
|
+
this.saveState();
|
|
4128
|
+
if (key === "markersHidden") this.renderPins();
|
|
4129
|
+
if (key === "showPaths") {
|
|
4130
|
+
this.renderList();
|
|
4131
|
+
this.renderHome();
|
|
4132
|
+
}
|
|
4133
|
+
if (key === "hoverHints") this.hintFor = null;
|
|
4134
|
+
this.applyDockLayout();
|
|
4135
|
+
}
|
|
4136
|
+
/** Reflect the current state into the settings menu (never rebuild it). */
|
|
4137
|
+
renderSettings() {
|
|
4138
|
+
if (!this.settingsMenu) return;
|
|
4139
|
+
const on = {
|
|
4140
|
+
hoverHints: this.hoverHints,
|
|
4141
|
+
markersHidden: !this.markersHidden,
|
|
4142
|
+
showPaths: this.showPaths
|
|
4143
|
+
};
|
|
4144
|
+
this.settingsMenu.querySelectorAll("[data-set]").forEach((b) => {
|
|
4145
|
+
const key = b.dataset.set;
|
|
4146
|
+
if (key in on) b.setAttribute("aria-pressed", String(on[key]));
|
|
4147
|
+
});
|
|
4148
|
+
this.settingsMenu.querySelectorAll("[data-accent]").forEach((b) => b.classList.toggle("on", b.dataset.accent === this.accent));
|
|
4149
|
+
}
|
|
4150
|
+
/**
|
|
4151
|
+
* The contextual hint card for the active view. Shown at most once per view and
|
|
4152
|
+
* never again after that — dismissed, or switched off wholesale with "Turn off
|
|
4153
|
+
* hints" (the same switch as Settings → Hover hints).
|
|
4154
|
+
*/
|
|
4155
|
+
renderHints() {
|
|
4156
|
+
if (!this.homeEl) return;
|
|
4157
|
+
const slots = {
|
|
4158
|
+
home: this.homeEl.querySelector("#loupe-hhint"),
|
|
4159
|
+
comments: this.commentsHint ?? null,
|
|
4160
|
+
connect: this.connectHint ?? null
|
|
4161
|
+
};
|
|
4162
|
+
const slot = slots[this.tab];
|
|
4163
|
+
if (!slot || this.hintFor === this.tab) return;
|
|
4164
|
+
this.hintFor = this.tab;
|
|
4165
|
+
slot.innerHTML = "";
|
|
4166
|
+
if (!this.hoverHints || this.minimized || this.hintsSeen.has(this.tab)) return;
|
|
4167
|
+
this.hintsSeen.add(this.tab);
|
|
4168
|
+
this.saveState();
|
|
4169
|
+
const hint = HINTS[this.tab];
|
|
4170
|
+
slot.innerHTML = `<div class="hint"><div class="hint-t">${escapeHtml(hint.title)}</div><div class="hint-b">${escapeHtml(hint.body)}</div><button class="hint-off" data-role="hint-off">Turn off hints</button><button class="hint-x" aria-label="Dismiss hint">\u2715</button></div>`;
|
|
4171
|
+
slot.querySelector('[data-role="hint-off"]').onclick = () => {
|
|
4172
|
+
this.hoverHints = false;
|
|
4173
|
+
this.hintFor = null;
|
|
4174
|
+
this.saveState();
|
|
4175
|
+
this.applyDockLayout();
|
|
4176
|
+
};
|
|
4177
|
+
slot.querySelector(".hint-x").onclick = () => {
|
|
4178
|
+
slot.innerHTML = "";
|
|
4179
|
+
};
|
|
4180
|
+
}
|
|
4181
|
+
// ---- guided tour ----------------------------------------------------------
|
|
4182
|
+
startTour() {
|
|
4183
|
+
this.tourStep = 0;
|
|
4184
|
+
this.open = true;
|
|
4185
|
+
this.minimized = false;
|
|
4186
|
+
this.applyDockLayout();
|
|
4187
|
+
this.renderTour();
|
|
4188
|
+
}
|
|
4189
|
+
stopTour() {
|
|
4190
|
+
this.tourStep = -1;
|
|
4191
|
+
this.tourDone = true;
|
|
4192
|
+
this.saveState();
|
|
4193
|
+
this.tourEl.classList.remove("open");
|
|
4194
|
+
this.applyDockLayout();
|
|
4195
|
+
}
|
|
4196
|
+
gotoTour(step) {
|
|
4197
|
+
if (step < 0) {
|
|
4198
|
+
this.stopTour();
|
|
4199
|
+
return;
|
|
4200
|
+
}
|
|
4201
|
+
this.tourStep = step;
|
|
4202
|
+
this.renderTour();
|
|
4203
|
+
}
|
|
4204
|
+
/** Position the spotlight over the current step's target and lay out the card. */
|
|
4205
|
+
renderTour() {
|
|
4206
|
+
if (this.tourStep < 0 || this.tourStep >= TOUR.length) {
|
|
4207
|
+
this.tourEl.classList.remove("open");
|
|
4208
|
+
return;
|
|
4209
|
+
}
|
|
4210
|
+
const step = TOUR[this.tourStep];
|
|
4211
|
+
if (this.tab !== step.tab) this.setTab(step.tab);
|
|
4212
|
+
this.tourEl.classList.add("open");
|
|
4213
|
+
const pad = 4;
|
|
4214
|
+
const target = this.shadow.querySelector(step.sel);
|
|
4215
|
+
const box = target?.getBoundingClientRect();
|
|
4216
|
+
const r = box && box.width ? { left: box.left - pad, top: box.top - pad, width: box.width + pad * 2, height: box.height + pad * 2 } : { left: window.innerWidth / 2, top: window.innerHeight / 2, width: 0, height: 0 };
|
|
4217
|
+
this.tourSpot.style.left = `${r.left}px`;
|
|
4218
|
+
this.tourSpot.style.top = `${r.top}px`;
|
|
4219
|
+
this.tourSpot.style.width = `${r.width}px`;
|
|
4220
|
+
this.tourSpot.style.height = `${r.height}px`;
|
|
4221
|
+
const last = this.tourStep === TOUR.length - 1;
|
|
4222
|
+
this.tourCard.innerHTML = `<div class="tour-title">${escapeHtml(step.title)}</div><div class="tour-body">${escapeHtml(step.body)}</div><div class="tour-foot"><span class="tour-dots">${TOUR.map((_, i) => `<i class="${i === this.tourStep ? "on" : ""}"></i>`).join("")}</span>` + (this.tourStep > 0 ? `<button class="t-back">Back</button>` : "") + `<button class="t-skip">Skip</button><button class="t-next">${last ? "Done" : "Next"}</button></div>`;
|
|
4223
|
+
const W2 = 252;
|
|
4224
|
+
const H = this.tourCard.offsetHeight || 130;
|
|
4225
|
+
const overlapsTarget = (x, y) => x < r.left + r.width + 8 && x + W2 > r.left - 8 && y < r.top + r.height + 8 && y + H > r.top - 8;
|
|
4226
|
+
const candidates = [
|
|
4227
|
+
[r.left + r.width / 2 - W2 / 2, r.top + r.height + 12],
|
|
4228
|
+
// below
|
|
4229
|
+
[r.left + r.width / 2 - W2 / 2, r.top - H - 12],
|
|
4230
|
+
// above
|
|
4231
|
+
[r.left - W2 - 12, r.top + r.height / 2 - H / 2],
|
|
4232
|
+
// beside (left)
|
|
4233
|
+
[r.left + r.width + 12, r.top + r.height / 2 - H / 2]
|
|
4234
|
+
// beside (right)
|
|
4235
|
+
];
|
|
4236
|
+
let cx = clampPx(candidates[0][0], 8, Math.max(8, window.innerWidth - W2 - 8));
|
|
4237
|
+
let cy = clampPx(candidates[0][1], 8, Math.max(8, window.innerHeight - H - 8));
|
|
4238
|
+
for (const [x, y] of candidates) {
|
|
4239
|
+
const px = clampPx(x, 8, Math.max(8, window.innerWidth - W2 - 8));
|
|
4240
|
+
const py = clampPx(y, 8, Math.max(8, window.innerHeight - H - 8));
|
|
4241
|
+
cx = px;
|
|
4242
|
+
cy = py;
|
|
4243
|
+
if (!overlapsTarget(px, py)) break;
|
|
4244
|
+
}
|
|
4245
|
+
this.tourCard.style.left = `${cx}px`;
|
|
4246
|
+
this.tourCard.style.top = `${cy}px`;
|
|
4247
|
+
this.tourCard.querySelector(".t-back")?.addEventListener("click", () => this.gotoTour(this.tourStep - 1));
|
|
4248
|
+
this.tourCard.querySelector(".t-skip").addEventListener("click", () => this.stopTour());
|
|
4249
|
+
this.tourCard.querySelector(".t-next").addEventListener("click", () => {
|
|
4250
|
+
if (last) this.stopTour();
|
|
4251
|
+
else this.gotoTour(this.tourStep + 1);
|
|
4252
|
+
});
|
|
4253
|
+
}
|
|
3884
4254
|
/** Narrow viewports render the panel as a bottom sheet, not a side/float dock. */
|
|
3885
4255
|
isMobile() {
|
|
3886
4256
|
return window.innerWidth <= 640;
|
|
@@ -3899,6 +4269,12 @@ var Loupe = (() => {
|
|
|
3899
4269
|
if (typeof p?.repoFilter === "string") this.repoFilter = p.repoFilter;
|
|
3900
4270
|
if (p?.float && typeof p.float.w === "number") this.floatRect = { ...this.floatRect, ...p.float };
|
|
3901
4271
|
if (typeof p?.markersHidden === "boolean") this.markersHidden = p.markersHidden;
|
|
4272
|
+
if (p?.accent && ACCENT_IDS.includes(p.accent)) this.accent = p.accent;
|
|
4273
|
+
if (typeof p?.minimized === "boolean") this.minimized = p.minimized;
|
|
4274
|
+
if (typeof p?.hoverHints === "boolean") this.hoverHints = p.hoverHints;
|
|
4275
|
+
if (typeof p?.showPaths === "boolean") this.showPaths = p.showPaths;
|
|
4276
|
+
if (typeof p?.tourDone === "boolean") this.tourDone = p.tourDone;
|
|
4277
|
+
if (Array.isArray(p?.hintsSeen)) this.hintsSeen = new Set(p.hintsSeen.filter((x) => typeof x === "string"));
|
|
3902
4278
|
} catch {
|
|
3903
4279
|
}
|
|
3904
4280
|
}
|
|
@@ -3913,7 +4289,13 @@ var Loupe = (() => {
|
|
|
3913
4289
|
markersHidden: this.markersHidden,
|
|
3914
4290
|
scope: this.scope,
|
|
3915
4291
|
statFilter: this.statFilter,
|
|
3916
|
-
repoFilter: this.repoFilter
|
|
4292
|
+
repoFilter: this.repoFilter,
|
|
4293
|
+
accent: this.accent,
|
|
4294
|
+
minimized: this.minimized,
|
|
4295
|
+
hoverHints: this.hoverHints,
|
|
4296
|
+
showPaths: this.showPaths,
|
|
4297
|
+
tourDone: this.tourDone,
|
|
4298
|
+
hintsSeen: [...this.hintsSeen]
|
|
3917
4299
|
}));
|
|
3918
4300
|
} catch {
|
|
3919
4301
|
}
|
|
@@ -3923,14 +4305,23 @@ var Loupe = (() => {
|
|
|
3923
4305
|
this.root.classList.toggle("theme-light", this.theme === "light");
|
|
3924
4306
|
this.themeBtn.title = this.theme === "dark" ? "Switch to light theme" : "Switch to dark theme";
|
|
3925
4307
|
this.themeBtn.innerHTML = this.theme === "dark" ? I_SUN : I_MOON;
|
|
4308
|
+
const accent = ACCENTS.find((a) => a.id === this.accent) ?? ACCENTS[0];
|
|
4309
|
+
this.root.style.setProperty("--accent", this.theme === "light" ? accent.light : accent.dark);
|
|
4310
|
+
this.root.style.setProperty("--accent-soft", accent.soft);
|
|
3926
4311
|
const d = this.dock;
|
|
3927
4312
|
d.classList.toggle("open", this.open);
|
|
3928
4313
|
for (const m of DOCK_MODES) d.classList.toggle("mode-" + m, this.dockMode === m);
|
|
4314
|
+
d.classList.toggle("minimized", this.minimized);
|
|
4315
|
+
const openCount = this.comments.filter((c) => !isResolved(c)).length;
|
|
4316
|
+
this.minBar.innerHTML = `<span class="logo">\u25CE</span><span class="mtext"><b>${openCount}</b> open ${this.scope === "all" ? "in this project" : "on this page"}</span><span class="mrestore" aria-hidden="true">\u25B8</span>`;
|
|
3929
4317
|
d.classList.toggle("tab-home", this.tab === "home");
|
|
3930
4318
|
d.classList.toggle("tab-comments", this.tab === "comments");
|
|
3931
4319
|
d.classList.toggle("tab-connect", this.tab === "connect");
|
|
3932
4320
|
this.renderHome();
|
|
3933
4321
|
d.querySelectorAll(".tabs .tab").forEach((b) => b.classList.toggle("on", b.dataset.tab === this.tab));
|
|
4322
|
+
this.posMenu.querySelectorAll("[data-pos]").forEach((b) => b.classList.toggle("on", b.dataset.pos === this.dockMode));
|
|
4323
|
+
this.renderSettings();
|
|
4324
|
+
this.renderHints();
|
|
3934
4325
|
if (this.dockMode === "float") {
|
|
3935
4326
|
const vw = window.innerWidth, vh = window.innerHeight;
|
|
3936
4327
|
let { x, y, w, h } = this.floatRect;
|
|
@@ -4032,6 +4423,9 @@ var Loupe = (() => {
|
|
|
4032
4423
|
}
|
|
4033
4424
|
if (isResolved(c)) top.appendChild(el("span", "badge done", "resolved"));
|
|
4034
4425
|
else if (detached) top.appendChild(el("span", "badge detached", "element moved/removed"));
|
|
4426
|
+
if (this.showPaths && this.scope === "all" && c.url) {
|
|
4427
|
+
top.appendChild(el("span", "pathtag", shortPath(c.url)));
|
|
4428
|
+
}
|
|
4035
4429
|
top.appendChild(el("span", "caret", open ? "\u25BE" : "\u25B8"));
|
|
4036
4430
|
item.appendChild(top);
|
|
4037
4431
|
const summary = c.title || (c.body.split("\n")[0] ?? "").slice(0, 140) || "(no description)";
|
|
@@ -4168,6 +4562,13 @@ var Loupe = (() => {
|
|
|
4168
4562
|
if (h < 24) return `${h}h ago`;
|
|
4169
4563
|
return `${Math.round(h / 24)}d ago`;
|
|
4170
4564
|
}
|
|
4565
|
+
function shortPath(url) {
|
|
4566
|
+
try {
|
|
4567
|
+
return new URL(url, location.origin).pathname || "/";
|
|
4568
|
+
} catch {
|
|
4569
|
+
return url;
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4171
4572
|
function dayLabel(iso) {
|
|
4172
4573
|
const d = new Date(iso);
|
|
4173
4574
|
if (Number.isNaN(d.getTime())) return "Earlier";
|
|
@@ -4212,6 +4613,12 @@ var Loupe = (() => {
|
|
|
4212
4613
|
var I_CLOSE = svg(
|
|
4213
4614
|
`<path d="M4.2 4.2l7.6 7.6M11.8 4.2l-7.6 7.6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>`
|
|
4214
4615
|
);
|
|
4616
|
+
var I_GEAR = svg(
|
|
4617
|
+
`<circle cx="8" cy="8" r="2.2" stroke="currentColor" stroke-width="1.4"/><path d="M8 1.6v1.5M8 12.9v1.5M1.6 8h1.5M12.9 8h1.5M3.5 3.5l1.1 1.1M11.4 11.4l1.1 1.1M12.5 3.5l-1.1 1.1M4.6 11.4l-1.1 1.1" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>`
|
|
4618
|
+
);
|
|
4619
|
+
var I_MINIMIZE = svg(
|
|
4620
|
+
`<path d="M3.5 8h9" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>`
|
|
4621
|
+
);
|
|
4215
4622
|
var I_COMMENT = svg(
|
|
4216
4623
|
`<path d="M2 3.1h12v7.4H6.5l-3.3 2.6v-2.6H2z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/><path d="M8 5.1v3.4M6.3 6.8h3.4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>`
|
|
4217
4624
|
);
|