@loupekit/sdk 0.10.12 → 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.js
CHANGED
|
@@ -9,6 +9,7 @@ var STYLES = (
|
|
|
9
9
|
Dark is the default; the host element gets .theme-light to flip to light. */
|
|
10
10
|
:host {
|
|
11
11
|
--accent: #6b73e6;
|
|
12
|
+
--accent-soft: rgba(107, 115, 230, 0.12);
|
|
12
13
|
--pin: #ff5842;
|
|
13
14
|
--bg: #14161d;
|
|
14
15
|
--bg-2: #1b1e27;
|
|
@@ -20,6 +21,7 @@ var STYLES = (
|
|
|
20
21
|
}
|
|
21
22
|
:host(.theme-light) {
|
|
22
23
|
--accent: #4a55d6;
|
|
24
|
+
--accent-soft: rgba(74, 85, 214, 0.12);
|
|
23
25
|
--pin: #ff5842;
|
|
24
26
|
--bg: #ffffff;
|
|
25
27
|
--bg-2: #f6f7fb;
|
|
@@ -36,7 +38,7 @@ var STYLES = (
|
|
|
36
38
|
.hl {
|
|
37
39
|
position: fixed; pointer-events: none; z-index: 2147483001;
|
|
38
40
|
border: 2px solid var(--accent);
|
|
39
|
-
background:
|
|
41
|
+
background: var(--accent-soft);
|
|
40
42
|
border-radius: 4px; display: none;
|
|
41
43
|
transition: all 60ms linear;
|
|
42
44
|
}
|
|
@@ -49,7 +51,7 @@ var STYLES = (
|
|
|
49
51
|
/* region selection (during drag) + active-comment outline */
|
|
50
52
|
.selbox {
|
|
51
53
|
position: fixed; pointer-events: none; z-index: 2147483001; display: none;
|
|
52
|
-
border: 2px dashed var(--accent); background:
|
|
54
|
+
border: 2px dashed var(--accent); background: var(--accent-soft); border-radius: 4px;
|
|
53
55
|
}
|
|
54
56
|
.region-box {
|
|
55
57
|
position: fixed; pointer-events: none; z-index: 2147483001; display: none;
|
|
@@ -351,6 +353,12 @@ var STYLES = (
|
|
|
351
353
|
|
|
352
354
|
/* Timeline grouping (project scope) + the repo filter. */
|
|
353
355
|
.daylabel { margin: 8px 2px 0; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); font-weight: 700; }
|
|
356
|
+
/* The page a comment belongs to, shown in the project scope (Settings \u2192 Page paths). */
|
|
357
|
+
.pathtag {
|
|
358
|
+
padding: 1px 6px; border-radius: 999px; background: var(--bg-3); color: var(--muted);
|
|
359
|
+
font-size: 10.5px; font-family: ui-monospace, Menlo, monospace;
|
|
360
|
+
max-width: 130px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
361
|
+
}
|
|
354
362
|
.listhead .reposel {
|
|
355
363
|
margin-left: 6px; font-size: 11px; padding: 3px 6px; border: 1px solid var(--line);
|
|
356
364
|
border-radius: 7px; background: var(--bg-2); color: var(--ink);
|
|
@@ -398,6 +406,97 @@ var STYLES = (
|
|
|
398
406
|
@keyframes loupe-recpulse { 0%,100% { opacity: 1; } 50% { opacity: .25; } }
|
|
399
407
|
@media (prefers-reduced-motion: reduce) { .recbar .recdot { animation: none; } }
|
|
400
408
|
|
|
409
|
+
/* ---------------------------------------------- header popovers (pos + settings) */
|
|
410
|
+
.menu-wrap { position: relative; }
|
|
411
|
+
.menu {
|
|
412
|
+
position: absolute; top: calc(100% + 6px); right: 0; z-index: 30;
|
|
413
|
+
min-width: 214px; padding: 8px; display: none;
|
|
414
|
+
background: var(--bg-2); border: 1px solid var(--line); border-radius: 12px; box-shadow: var(--shadow);
|
|
415
|
+
}
|
|
416
|
+
.menu.open { display: block; }
|
|
417
|
+
.menu-label {
|
|
418
|
+
margin: 2px 5px 6px; font-size: 10px; font-weight: 700; letter-spacing: .06em;
|
|
419
|
+
text-transform: uppercase; color: var(--muted);
|
|
420
|
+
}
|
|
421
|
+
/* dock-position grid: the four layouts as a 2x2 of buttons */
|
|
422
|
+
.pos-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
|
|
423
|
+
.pos-grid button {
|
|
424
|
+
display: flex; align-items: center; gap: 6px; padding: 7px 8px; cursor: pointer;
|
|
425
|
+
border: 1px solid var(--line); border-radius: 8px; background: var(--bg); color: var(--muted);
|
|
426
|
+
font-size: 11.5px; font-weight: 600;
|
|
427
|
+
}
|
|
428
|
+
.pos-grid button:hover { border-color: var(--accent); color: var(--ink); }
|
|
429
|
+
.pos-grid button.on { border-color: var(--accent); color: var(--accent); }
|
|
430
|
+
.pos-grid button svg { width: 14px; height: 14px; flex: none; }
|
|
431
|
+
/* settings rows */
|
|
432
|
+
.menu-row {
|
|
433
|
+
display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%;
|
|
434
|
+
padding: 7px 6px; border: 0; border-radius: 8px; background: transparent; color: var(--ink);
|
|
435
|
+
font-size: 12.5px; text-align: left; cursor: pointer;
|
|
436
|
+
}
|
|
437
|
+
.menu-row:hover { background: var(--bg-3); }
|
|
438
|
+
.menu-sep { height: 1px; margin: 6px 4px; background: var(--line); }
|
|
439
|
+
/* on/off switch inside a settings row */
|
|
440
|
+
.sw { flex: none; position: relative; width: 30px; height: 17px; border-radius: 999px; background: var(--line); transition: background .12s; }
|
|
441
|
+
.sw::after { content: ""; position: absolute; top: 2px; left: 2px; width: 13px; height: 13px; border-radius: 50%; background: #fff; transition: transform .12s; }
|
|
442
|
+
.menu-row[aria-pressed="true"] .sw { background: var(--accent); }
|
|
443
|
+
.menu-row[aria-pressed="true"] .sw::after { transform: translateX(13px); }
|
|
444
|
+
/* accent swatches */
|
|
445
|
+
.acc-dots { display: flex; gap: 8px; padding: 4px 6px 2px; }
|
|
446
|
+
.acc-dot { width: 20px; height: 20px; border-radius: 50%; border: 2px solid transparent; padding: 0; cursor: pointer; }
|
|
447
|
+
.acc-dot.on { border-color: var(--ink); }
|
|
448
|
+
|
|
449
|
+
/* ---------------------------------------------------------------- minimize bar */
|
|
450
|
+
.minbar { display: none; align-items: center; gap: 8px; padding: 9px 10px; cursor: pointer; background: var(--bg-2); }
|
|
451
|
+
.dock.minimized .minbar { display: flex; }
|
|
452
|
+
.dock.minimized .tabs, .dock.minimized .view, .dock.minimized .resize,
|
|
453
|
+
.dock.minimized .dctl [data-role="min"], .dock.minimized .dctl .menu-wrap { display: none !important; }
|
|
454
|
+
.minbar .logo { color: var(--accent); font-size: 14px; }
|
|
455
|
+
.minbar .mtext { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; color: var(--muted); }
|
|
456
|
+
.minbar .mtext b { color: var(--ink); }
|
|
457
|
+
.minbar .mrestore { flex: none; padding: 0 2px; border: 0; background: transparent; color: var(--accent); font-size: 14px; cursor: pointer; }
|
|
458
|
+
|
|
459
|
+
/* ------------------------------------------------------- hint card (once per view) */
|
|
460
|
+
.hint {
|
|
461
|
+
position: relative; margin: 8px 12px 0; padding: 10px 28px 10px 11px;
|
|
462
|
+
border: 1px solid var(--accent); border-radius: 10px; background: var(--bg-2);
|
|
463
|
+
}
|
|
464
|
+
.hint-t { margin-bottom: 3px; font-size: 12.5px; font-weight: 700; color: var(--ink); }
|
|
465
|
+
.hint-b { font-size: 11.5px; line-height: 1.45; color: var(--muted); }
|
|
466
|
+
.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; }
|
|
467
|
+
.hint-x {
|
|
468
|
+
position: absolute; top: 6px; right: 6px; width: 18px; height: 18px; padding: 0;
|
|
469
|
+
border: 0; border-radius: 5px; background: transparent; color: var(--muted); font-size: 13px; line-height: 1; cursor: pointer;
|
|
470
|
+
}
|
|
471
|
+
.hint-x:hover { background: var(--bg-3); color: var(--ink); }
|
|
472
|
+
|
|
473
|
+
/* ---------------------------------------------------------------- guided tour */
|
|
474
|
+
/* Non-modal on purpose: the dimmer and the spotlight are click-through, so a tour
|
|
475
|
+
can never trap someone mid-task. Only the card itself takes pointer events. */
|
|
476
|
+
.tour { position: fixed; inset: 0; z-index: 2147483200; display: none; pointer-events: none; }
|
|
477
|
+
.tour.open { display: block; }
|
|
478
|
+
.tour-spot {
|
|
479
|
+
position: fixed; border: 2px solid var(--accent); border-radius: 10px; pointer-events: none;
|
|
480
|
+
box-shadow: 0 0 0 9999px rgba(8, 10, 16, .62);
|
|
481
|
+
transition: all .18s ease;
|
|
482
|
+
}
|
|
483
|
+
.tour-card {
|
|
484
|
+
position: fixed; width: 252px; padding: 12px; pointer-events: auto;
|
|
485
|
+
background: var(--bg-2); border: 1px solid var(--line); border-radius: 12px; box-shadow: var(--shadow); color: var(--ink);
|
|
486
|
+
}
|
|
487
|
+
.tour-title { margin-bottom: 4px; font-size: 13px; font-weight: 700; }
|
|
488
|
+
.tour-body { font-size: 11.5px; line-height: 1.5; color: var(--muted); }
|
|
489
|
+
.tour-foot { display: flex; align-items: center; gap: 6px; margin-top: 10px; }
|
|
490
|
+
.tour-dots { display: flex; flex: 1; gap: 4px; }
|
|
491
|
+
.tour-dots i { width: 5px; height: 5px; border-radius: 50%; background: var(--line); }
|
|
492
|
+
.tour-dots i.on { background: var(--accent); }
|
|
493
|
+
.tour-foot button {
|
|
494
|
+
padding: 5px 9px; border: 1px solid var(--line); border-radius: 7px;
|
|
495
|
+
background: var(--bg); color: var(--ink); font-size: 11.5px; font-weight: 600; cursor: pointer;
|
|
496
|
+
}
|
|
497
|
+
.tour-foot .t-next { border-color: var(--accent); background: var(--accent); color: #fff; }
|
|
498
|
+
.tour-foot .t-skip { border: 0; background: transparent; color: var(--muted); }
|
|
499
|
+
|
|
401
500
|
/* Mobile: left/right/float docking is a desktop affordance. On small screens the
|
|
402
501
|
panel collapses to a bottom sheet that OVERLAYS the page (pushing a side dock
|
|
403
502
|
here would squeeze the page to a useless sliver), regardless of the chosen dock
|
|
@@ -408,7 +507,8 @@ var STYLES = (
|
|
|
408
507
|
width: auto !important; height: 76vh !important;
|
|
409
508
|
border-width: 1px 0 0 0 !important; border-radius: 16px 16px 0 0 !important;
|
|
410
509
|
}
|
|
411
|
-
.dctl [data-
|
|
510
|
+
.dctl [data-role="pos"], .dctl .gap { display: none; } /* dock positions don't apply on mobile */
|
|
511
|
+
.menu { min-width: 190px; }
|
|
412
512
|
.resize { display: none !important; }
|
|
413
513
|
.tools button { flex: 1; justify-content: center; } /* full-width tap targets */
|
|
414
514
|
.dock.mode-bottom .list { grid-template-columns: 1fr; }
|
|
@@ -498,10 +598,10 @@ function similarity(a, el2, via) {
|
|
|
498
598
|
const t = normText(el2.textContent);
|
|
499
599
|
add(W.text, t === a.text ? 1 : t && (t.includes(a.text) || a.text.includes(t)) ? 0.5 : 0);
|
|
500
600
|
}
|
|
501
|
-
const attrKeys = Object.keys(a.attrs);
|
|
601
|
+
const attrKeys = Object.keys(a.attrs ?? {});
|
|
502
602
|
if (attrKeys.length) {
|
|
503
603
|
let matched = 0;
|
|
504
|
-
for (const k of attrKeys) if (el2.getAttribute(k) === a.attrs[k]) matched++;
|
|
604
|
+
for (const k of attrKeys) if (el2.getAttribute(k) === (a.attrs ?? {})[k]) matched++;
|
|
505
605
|
add(W.attrs, matched / attrKeys.length);
|
|
506
606
|
}
|
|
507
607
|
if (a.testid) add(W.testid, stableId(el2) === a.testid ? 1 : 0);
|
|
@@ -509,9 +609,11 @@ function similarity(a, el2, via) {
|
|
|
509
609
|
const r = el2.getBoundingClientRect();
|
|
510
610
|
const cx = r.left + window.scrollX + r.width / 2;
|
|
511
611
|
const cy = r.top + window.scrollY + r.height / 2;
|
|
512
|
-
const
|
|
513
|
-
const
|
|
514
|
-
const
|
|
612
|
+
const ar = a.rect ?? { x: 0, y: 0, w: 0, h: 0 };
|
|
613
|
+
const av = a.viewport ?? { w: 0, h: 0 };
|
|
614
|
+
const ax = ar.x + ar.w / 2;
|
|
615
|
+
const ay = ar.y + ar.h / 2;
|
|
616
|
+
const diag = Math.hypot(av.w, av.h) || 1;
|
|
515
617
|
const dist = Math.hypot(cx - ax, cy - ay);
|
|
516
618
|
add(W.position, Math.max(0, 1 - dist / diag));
|
|
517
619
|
return total ? sum / total : 0;
|
|
@@ -2621,7 +2723,48 @@ function normalizePriority(value) {
|
|
|
2621
2723
|
}
|
|
2622
2724
|
|
|
2623
2725
|
// src/app.ts
|
|
2624
|
-
var SDK_VERSION = true ? "0.10.
|
|
2726
|
+
var SDK_VERSION = true ? "0.10.13" : "dev";
|
|
2727
|
+
var ACCENTS = [
|
|
2728
|
+
{ id: "indigo", dark: "#6b73e6", light: "#4a55d6", soft: "rgba(107,115,230,0.12)" },
|
|
2729
|
+
{ id: "violet", dark: "#a06be6", light: "#7c3fd4", soft: "rgba(160,107,230,0.14)" },
|
|
2730
|
+
{ id: "teal", dark: "#2fb6a8", light: "#0f8f83", soft: "rgba(47,182,168,0.14)" },
|
|
2731
|
+
{ id: "amber", dark: "#d99a2b", light: "#a9700f", soft: "rgba(217,154,43,0.14)" },
|
|
2732
|
+
{ id: "rose", dark: "#e05c86", light: "#c2295a", soft: "rgba(224,92,134,0.14)" }
|
|
2733
|
+
];
|
|
2734
|
+
var ACCENT_IDS = ACCENTS.map((a) => a.id);
|
|
2735
|
+
var TOUR = [
|
|
2736
|
+
// Home first: the panel already opens there, so the first step never moves the
|
|
2737
|
+
// user — the tour starts where they are.
|
|
2738
|
+
{
|
|
2739
|
+
sel: ".hstat",
|
|
2740
|
+
tab: "home",
|
|
2741
|
+
title: "Home shows what needs you",
|
|
2742
|
+
body: "Four tiles count open, needs-you, resolved and stale feedback. Click one to narrow the list to that bucket."
|
|
2743
|
+
},
|
|
2744
|
+
{
|
|
2745
|
+
sel: ".hscope",
|
|
2746
|
+
tab: "home",
|
|
2747
|
+
title: "This page, or the whole project",
|
|
2748
|
+
body: "Switch to All to see every page's feedback as a day-grouped timeline, with a repo filter."
|
|
2749
|
+
},
|
|
2750
|
+
{
|
|
2751
|
+
sel: ".tools",
|
|
2752
|
+
tab: "comments",
|
|
2753
|
+
title: "Pin feedback anywhere",
|
|
2754
|
+
body: "Inspect picks an element, Note drops a page-level comment, Region captures a rectangle, and Record films one."
|
|
2755
|
+
},
|
|
2756
|
+
{
|
|
2757
|
+
sel: '.tabs [data-tab="connect"]',
|
|
2758
|
+
tab: "connect",
|
|
2759
|
+
title: "Hand it to Claude",
|
|
2760
|
+
body: "Connect Claude wires up the MCP server so an agent reads this feedback with its element context."
|
|
2761
|
+
}
|
|
2762
|
+
];
|
|
2763
|
+
var HINTS = {
|
|
2764
|
+
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." },
|
|
2765
|
+
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." },
|
|
2766
|
+
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." }
|
|
2767
|
+
};
|
|
2625
2768
|
var DOCK_MODES = ["left", "right", "bottom", "float"];
|
|
2626
2769
|
var RECORD_MAX_MS = 2e4;
|
|
2627
2770
|
var MAX_FILES = 10;
|
|
@@ -2700,6 +2843,20 @@ var LoupeApp = class {
|
|
|
2700
2843
|
this.statFilter = "";
|
|
2701
2844
|
/** Repo filter, offered once the scope is "all". */
|
|
2702
2845
|
this.repoFilter = "";
|
|
2846
|
+
/** Accent preset id (see ACCENTS), applied as inline --accent / --accent-soft. */
|
|
2847
|
+
this.accent = "indigo";
|
|
2848
|
+
/** Collapsed to the one-line minimize bar. */
|
|
2849
|
+
this.minimized = false;
|
|
2850
|
+
/** The "help layer": FAB tooltips and the contextual hint cards. */
|
|
2851
|
+
this.hoverHints = true;
|
|
2852
|
+
/** Show each comment's page path (useful in the project scope). */
|
|
2853
|
+
this.showPaths = false;
|
|
2854
|
+
/** Views whose hint card has already been shown. */
|
|
2855
|
+
this.hintsSeen = /* @__PURE__ */ new Set();
|
|
2856
|
+
/** Current guided-tour step, or -1 when the tour is closed. */
|
|
2857
|
+
this.tourStep = -1;
|
|
2858
|
+
/** The first-run tour has been finished or skipped. */
|
|
2859
|
+
this.tourDone = false;
|
|
2703
2860
|
/** Float-mode window geometry; (x<=0 && y<=0) → placed on first layout. */
|
|
2704
2861
|
this.floatRect = { x: 0, y: 0, w: 380, h: 540 };
|
|
2705
2862
|
this.floatDrag = null;
|
|
@@ -2824,7 +2981,12 @@ var LoupeApp = class {
|
|
|
2824
2981
|
}
|
|
2825
2982
|
let elx = this.resolved.get(c.id) ?? null;
|
|
2826
2983
|
if (!elx || !elx.isConnected) {
|
|
2827
|
-
|
|
2984
|
+
let r = null;
|
|
2985
|
+
try {
|
|
2986
|
+
r = resolveAnchor(c.anchor);
|
|
2987
|
+
} catch {
|
|
2988
|
+
r = null;
|
|
2989
|
+
}
|
|
2828
2990
|
elx = r?.element ?? null;
|
|
2829
2991
|
this.resolved.set(c.id, elx);
|
|
2830
2992
|
}
|
|
@@ -2867,6 +3029,8 @@ var LoupeApp = class {
|
|
|
2867
3029
|
}
|
|
2868
3030
|
};
|
|
2869
3031
|
this.onWinResize = () => this.applyDockLayout();
|
|
3032
|
+
/** Which view's hint has been handled this session (so it is not re-painted). */
|
|
3033
|
+
this.hintFor = null;
|
|
2870
3034
|
// ---- float-mode drag + resize ---------------------------------------------
|
|
2871
3035
|
this.onHeadPointerDown = (e) => {
|
|
2872
3036
|
if (this.dockMode !== "float" || e.button !== 0 || this.isMobile()) return;
|
|
@@ -2932,6 +3096,7 @@ var LoupeApp = class {
|
|
|
2932
3096
|
if (this.scope === "all") void this.loadAllComments();
|
|
2933
3097
|
this.observe();
|
|
2934
3098
|
this.watchNavigation();
|
|
3099
|
+
if (!this.tourDone && this.open && !this.isMobile()) this.startTour();
|
|
2935
3100
|
if (this.cfg.autoOpen) this.setMode("inspect");
|
|
2936
3101
|
}
|
|
2937
3102
|
/**
|
|
@@ -2984,7 +3149,16 @@ var LoupeApp = class {
|
|
|
2984
3149
|
this.dock = this.buildDock();
|
|
2985
3150
|
this.fabCluster = this.buildFabCluster();
|
|
2986
3151
|
this.recBar = this.buildRecBar();
|
|
2987
|
-
this.
|
|
3152
|
+
this.tourEl = el("div", "tour");
|
|
3153
|
+
this.tourSpot = el("div", "tour-spot");
|
|
3154
|
+
this.tourCard = el("div", "tour-card");
|
|
3155
|
+
this.tourEl.append(this.tourSpot, this.tourCard);
|
|
3156
|
+
this.shadow.append(this.dock, this.fabCluster, this.recBar, this.tourEl);
|
|
3157
|
+
this.shadow.addEventListener("click", (e) => {
|
|
3158
|
+
const t = e.target;
|
|
3159
|
+
if (t?.closest && t.closest(".menu-wrap")) return;
|
|
3160
|
+
this.closeMenus();
|
|
3161
|
+
});
|
|
2988
3162
|
}
|
|
2989
3163
|
/**
|
|
2990
3164
|
* The dockable control panel:
|
|
@@ -2999,33 +3173,71 @@ var LoupeApp = class {
|
|
|
2999
3173
|
brand.querySelector(".title").textContent = this.cfg.label ?? "Loupe";
|
|
3000
3174
|
head.addEventListener("pointerdown", this.onHeadPointerDown);
|
|
3001
3175
|
const ctl = el("div", "dctl");
|
|
3002
|
-
const
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3176
|
+
const posWrap = el("div", "menu-wrap");
|
|
3177
|
+
const posBtn = el("button");
|
|
3178
|
+
posBtn.dataset.role = "pos";
|
|
3179
|
+
posBtn.title = "Panel position";
|
|
3180
|
+
posBtn.setAttribute("aria-label", "Panel position");
|
|
3181
|
+
posBtn.innerHTML = I_DOCK_RIGHT;
|
|
3182
|
+
this.posMenu = el("div", "menu");
|
|
3183
|
+
const posMeta = [
|
|
3184
|
+
{ mode: "left", icon: I_DOCK_LEFT, label: "Left" },
|
|
3185
|
+
{ mode: "bottom", icon: I_DOCK_BOTTOM, label: "Bottom" },
|
|
3186
|
+
{ mode: "right", icon: I_DOCK_RIGHT, label: "Right" },
|
|
3187
|
+
{ mode: "float", icon: I_FLOAT, label: "Float" }
|
|
3188
|
+
];
|
|
3189
|
+
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>`;
|
|
3190
|
+
this.posMenu.querySelectorAll("[data-pos]").forEach((b) => {
|
|
3191
|
+
b.onclick = () => {
|
|
3192
|
+
this.setDock(b.dataset.pos);
|
|
3193
|
+
this.closeMenus();
|
|
3194
|
+
};
|
|
3195
|
+
});
|
|
3196
|
+
posBtn.onclick = () => this.toggleMenu(this.posMenu);
|
|
3197
|
+
posWrap.append(posBtn, this.posMenu);
|
|
3018
3198
|
this.themeBtn = el("button");
|
|
3019
3199
|
this.themeBtn.dataset.role = "theme";
|
|
3020
3200
|
this.themeBtn.onclick = () => this.toggleTheme();
|
|
3201
|
+
const setWrap = el("div", "menu-wrap");
|
|
3202
|
+
const setBtn = el("button");
|
|
3203
|
+
setBtn.dataset.role = "settings";
|
|
3204
|
+
setBtn.title = "Settings";
|
|
3205
|
+
setBtn.setAttribute("aria-label", "Settings");
|
|
3206
|
+
setBtn.innerHTML = I_GEAR;
|
|
3207
|
+
setBtn.onclick = () => this.toggleMenu(this.settingsMenu);
|
|
3208
|
+
this.settingsMenu = el("div", "menu");
|
|
3209
|
+
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>`;
|
|
3210
|
+
this.settingsMenu.querySelectorAll("[data-accent]").forEach((b) => {
|
|
3211
|
+
b.onclick = () => this.setAccent(b.dataset.accent);
|
|
3212
|
+
});
|
|
3213
|
+
this.settingsMenu.querySelectorAll("[data-set]").forEach((b) => {
|
|
3214
|
+
b.onclick = () => {
|
|
3215
|
+
const key = b.dataset.set;
|
|
3216
|
+
if (key === "tour") {
|
|
3217
|
+
this.closeMenus();
|
|
3218
|
+
this.startTour();
|
|
3219
|
+
return;
|
|
3220
|
+
}
|
|
3221
|
+
this.toggleSetting(key);
|
|
3222
|
+
};
|
|
3223
|
+
});
|
|
3224
|
+
setWrap.append(setBtn, this.settingsMenu);
|
|
3225
|
+
const minBtn = el("button");
|
|
3226
|
+
minBtn.dataset.role = "min";
|
|
3227
|
+
minBtn.title = "Minimize";
|
|
3228
|
+
minBtn.setAttribute("aria-label", "Minimize");
|
|
3229
|
+
minBtn.innerHTML = I_MINIMIZE;
|
|
3230
|
+
minBtn.onclick = () => this.setMinimized(true);
|
|
3021
3231
|
const closeBtn = el("button");
|
|
3022
3232
|
closeBtn.dataset.role = "close";
|
|
3023
3233
|
closeBtn.title = "Close";
|
|
3024
3234
|
closeBtn.setAttribute("aria-label", "Close");
|
|
3025
3235
|
closeBtn.innerHTML = I_CLOSE;
|
|
3026
3236
|
closeBtn.onclick = () => this.closeDock();
|
|
3027
|
-
ctl.append(this.themeBtn, closeBtn);
|
|
3237
|
+
ctl.append(posWrap, this.themeBtn, setWrap, minBtn, closeBtn);
|
|
3028
3238
|
head.append(brand, ctl);
|
|
3239
|
+
this.minBar = el("div", "minbar");
|
|
3240
|
+
this.minBar.onclick = () => this.setMinimized(false);
|
|
3029
3241
|
const tabs = el("div", "tabs");
|
|
3030
3242
|
const tabBtn = (key, label) => {
|
|
3031
3243
|
const b = el("button", "tab", label);
|
|
@@ -3083,11 +3295,14 @@ var LoupeApp = class {
|
|
|
3083
3295
|
const homeView = el("div", "view home-view");
|
|
3084
3296
|
this.homeEl = homeView;
|
|
3085
3297
|
const commentsView = el("div", "view comments-view");
|
|
3086
|
-
|
|
3298
|
+
this.commentsHint = el("div", "hint-slot");
|
|
3299
|
+
commentsView.append(this.commentsHint, tools, listHead, this.listEl, this.buildIntegrations());
|
|
3087
3300
|
const connectView = this.buildConnectPanel();
|
|
3301
|
+
this.connectHint = el("div", "hint-slot");
|
|
3302
|
+
connectView.prepend(this.connectHint);
|
|
3088
3303
|
const resize = el("div", "resize");
|
|
3089
3304
|
resize.addEventListener("pointerdown", this.onResizeDown);
|
|
3090
|
-
dock.append(head, tabs, homeView, commentsView, connectView, resize);
|
|
3305
|
+
dock.append(head, this.minBar, tabs, homeView, commentsView, connectView, resize);
|
|
3091
3306
|
this.buildHomePanel();
|
|
3092
3307
|
return dock;
|
|
3093
3308
|
}
|
|
@@ -3098,7 +3313,7 @@ var LoupeApp = class {
|
|
|
3098
3313
|
*/
|
|
3099
3314
|
buildHomePanel() {
|
|
3100
3315
|
const title = this.cfg.label ?? "Loupe";
|
|
3101
|
-
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>`;
|
|
3316
|
+
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>`;
|
|
3102
3317
|
this.homeEl.querySelectorAll(".hscope-b").forEach((b) => {
|
|
3103
3318
|
b.onclick = () => this.setScope(b.dataset.scope === "all" ? "all" : "page");
|
|
3104
3319
|
});
|
|
@@ -3841,6 +4056,7 @@ var LoupeApp = class {
|
|
|
3841
4056
|
if (this.tab === tab) return;
|
|
3842
4057
|
if (tab !== "comments") this.setMode("off");
|
|
3843
4058
|
this.tab = tab;
|
|
4059
|
+
this.hintFor = null;
|
|
3844
4060
|
this.saveState();
|
|
3845
4061
|
if (tab === "home") {
|
|
3846
4062
|
this.renderHome();
|
|
@@ -3854,6 +4070,160 @@ var LoupeApp = class {
|
|
|
3854
4070
|
this.saveState();
|
|
3855
4071
|
this.applyDockLayout();
|
|
3856
4072
|
}
|
|
4073
|
+
// ---- panel shell: popovers, minimize, accent ------------------------------
|
|
4074
|
+
toggleMenu(menu) {
|
|
4075
|
+
const wasOpen = menu.classList.contains("open");
|
|
4076
|
+
this.closeMenus();
|
|
4077
|
+
if (!wasOpen) menu.classList.add("open");
|
|
4078
|
+
}
|
|
4079
|
+
closeMenus() {
|
|
4080
|
+
this.posMenu?.classList.remove("open");
|
|
4081
|
+
this.settingsMenu?.classList.remove("open");
|
|
4082
|
+
}
|
|
4083
|
+
setAccent(id) {
|
|
4084
|
+
if (!ACCENT_IDS.includes(id)) return;
|
|
4085
|
+
this.accent = id;
|
|
4086
|
+
this.saveState();
|
|
4087
|
+
this.applyDockLayout();
|
|
4088
|
+
}
|
|
4089
|
+
setMinimized(v) {
|
|
4090
|
+
this.minimized = v;
|
|
4091
|
+
this.closeMenus();
|
|
4092
|
+
this.saveState();
|
|
4093
|
+
this.applyDockLayout();
|
|
4094
|
+
}
|
|
4095
|
+
/** Flip one of the "Show …" settings and repaint whatever it governs. */
|
|
4096
|
+
toggleSetting(key) {
|
|
4097
|
+
if (key === "markersHidden") this.markersHidden = !this.markersHidden;
|
|
4098
|
+
else if (key === "hoverHints") this.hoverHints = !this.hoverHints;
|
|
4099
|
+
else this.showPaths = !this.showPaths;
|
|
4100
|
+
this.saveState();
|
|
4101
|
+
if (key === "markersHidden") this.renderPins();
|
|
4102
|
+
if (key === "showPaths") {
|
|
4103
|
+
this.renderList();
|
|
4104
|
+
this.renderHome();
|
|
4105
|
+
}
|
|
4106
|
+
if (key === "hoverHints") this.hintFor = null;
|
|
4107
|
+
this.applyDockLayout();
|
|
4108
|
+
}
|
|
4109
|
+
/** Reflect the current state into the settings menu (never rebuild it). */
|
|
4110
|
+
renderSettings() {
|
|
4111
|
+
if (!this.settingsMenu) return;
|
|
4112
|
+
const on = {
|
|
4113
|
+
hoverHints: this.hoverHints,
|
|
4114
|
+
markersHidden: !this.markersHidden,
|
|
4115
|
+
showPaths: this.showPaths
|
|
4116
|
+
};
|
|
4117
|
+
this.settingsMenu.querySelectorAll("[data-set]").forEach((b) => {
|
|
4118
|
+
const key = b.dataset.set;
|
|
4119
|
+
if (key in on) b.setAttribute("aria-pressed", String(on[key]));
|
|
4120
|
+
});
|
|
4121
|
+
this.settingsMenu.querySelectorAll("[data-accent]").forEach((b) => b.classList.toggle("on", b.dataset.accent === this.accent));
|
|
4122
|
+
}
|
|
4123
|
+
/**
|
|
4124
|
+
* The contextual hint card for the active view. Shown at most once per view and
|
|
4125
|
+
* never again after that — dismissed, or switched off wholesale with "Turn off
|
|
4126
|
+
* hints" (the same switch as Settings → Hover hints).
|
|
4127
|
+
*/
|
|
4128
|
+
renderHints() {
|
|
4129
|
+
if (!this.homeEl) return;
|
|
4130
|
+
const slots = {
|
|
4131
|
+
home: this.homeEl.querySelector("#loupe-hhint"),
|
|
4132
|
+
comments: this.commentsHint ?? null,
|
|
4133
|
+
connect: this.connectHint ?? null
|
|
4134
|
+
};
|
|
4135
|
+
const slot = slots[this.tab];
|
|
4136
|
+
if (!slot || this.hintFor === this.tab) return;
|
|
4137
|
+
this.hintFor = this.tab;
|
|
4138
|
+
slot.innerHTML = "";
|
|
4139
|
+
if (!this.hoverHints || this.minimized || this.hintsSeen.has(this.tab)) return;
|
|
4140
|
+
this.hintsSeen.add(this.tab);
|
|
4141
|
+
this.saveState();
|
|
4142
|
+
const hint = HINTS[this.tab];
|
|
4143
|
+
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>`;
|
|
4144
|
+
slot.querySelector('[data-role="hint-off"]').onclick = () => {
|
|
4145
|
+
this.hoverHints = false;
|
|
4146
|
+
this.hintFor = null;
|
|
4147
|
+
this.saveState();
|
|
4148
|
+
this.applyDockLayout();
|
|
4149
|
+
};
|
|
4150
|
+
slot.querySelector(".hint-x").onclick = () => {
|
|
4151
|
+
slot.innerHTML = "";
|
|
4152
|
+
};
|
|
4153
|
+
}
|
|
4154
|
+
// ---- guided tour ----------------------------------------------------------
|
|
4155
|
+
startTour() {
|
|
4156
|
+
this.tourStep = 0;
|
|
4157
|
+
this.open = true;
|
|
4158
|
+
this.minimized = false;
|
|
4159
|
+
this.applyDockLayout();
|
|
4160
|
+
this.renderTour();
|
|
4161
|
+
}
|
|
4162
|
+
stopTour() {
|
|
4163
|
+
this.tourStep = -1;
|
|
4164
|
+
this.tourDone = true;
|
|
4165
|
+
this.saveState();
|
|
4166
|
+
this.tourEl.classList.remove("open");
|
|
4167
|
+
this.applyDockLayout();
|
|
4168
|
+
}
|
|
4169
|
+
gotoTour(step) {
|
|
4170
|
+
if (step < 0) {
|
|
4171
|
+
this.stopTour();
|
|
4172
|
+
return;
|
|
4173
|
+
}
|
|
4174
|
+
this.tourStep = step;
|
|
4175
|
+
this.renderTour();
|
|
4176
|
+
}
|
|
4177
|
+
/** Position the spotlight over the current step's target and lay out the card. */
|
|
4178
|
+
renderTour() {
|
|
4179
|
+
if (this.tourStep < 0 || this.tourStep >= TOUR.length) {
|
|
4180
|
+
this.tourEl.classList.remove("open");
|
|
4181
|
+
return;
|
|
4182
|
+
}
|
|
4183
|
+
const step = TOUR[this.tourStep];
|
|
4184
|
+
if (this.tab !== step.tab) this.setTab(step.tab);
|
|
4185
|
+
this.tourEl.classList.add("open");
|
|
4186
|
+
const pad = 4;
|
|
4187
|
+
const target = this.shadow.querySelector(step.sel);
|
|
4188
|
+
const box = target?.getBoundingClientRect();
|
|
4189
|
+
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 };
|
|
4190
|
+
this.tourSpot.style.left = `${r.left}px`;
|
|
4191
|
+
this.tourSpot.style.top = `${r.top}px`;
|
|
4192
|
+
this.tourSpot.style.width = `${r.width}px`;
|
|
4193
|
+
this.tourSpot.style.height = `${r.height}px`;
|
|
4194
|
+
const last = this.tourStep === TOUR.length - 1;
|
|
4195
|
+
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>`;
|
|
4196
|
+
const W2 = 252;
|
|
4197
|
+
const H = this.tourCard.offsetHeight || 130;
|
|
4198
|
+
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;
|
|
4199
|
+
const candidates = [
|
|
4200
|
+
[r.left + r.width / 2 - W2 / 2, r.top + r.height + 12],
|
|
4201
|
+
// below
|
|
4202
|
+
[r.left + r.width / 2 - W2 / 2, r.top - H - 12],
|
|
4203
|
+
// above
|
|
4204
|
+
[r.left - W2 - 12, r.top + r.height / 2 - H / 2],
|
|
4205
|
+
// beside (left)
|
|
4206
|
+
[r.left + r.width + 12, r.top + r.height / 2 - H / 2]
|
|
4207
|
+
// beside (right)
|
|
4208
|
+
];
|
|
4209
|
+
let cx = clampPx(candidates[0][0], 8, Math.max(8, window.innerWidth - W2 - 8));
|
|
4210
|
+
let cy = clampPx(candidates[0][1], 8, Math.max(8, window.innerHeight - H - 8));
|
|
4211
|
+
for (const [x, y] of candidates) {
|
|
4212
|
+
const px = clampPx(x, 8, Math.max(8, window.innerWidth - W2 - 8));
|
|
4213
|
+
const py = clampPx(y, 8, Math.max(8, window.innerHeight - H - 8));
|
|
4214
|
+
cx = px;
|
|
4215
|
+
cy = py;
|
|
4216
|
+
if (!overlapsTarget(px, py)) break;
|
|
4217
|
+
}
|
|
4218
|
+
this.tourCard.style.left = `${cx}px`;
|
|
4219
|
+
this.tourCard.style.top = `${cy}px`;
|
|
4220
|
+
this.tourCard.querySelector(".t-back")?.addEventListener("click", () => this.gotoTour(this.tourStep - 1));
|
|
4221
|
+
this.tourCard.querySelector(".t-skip").addEventListener("click", () => this.stopTour());
|
|
4222
|
+
this.tourCard.querySelector(".t-next").addEventListener("click", () => {
|
|
4223
|
+
if (last) this.stopTour();
|
|
4224
|
+
else this.gotoTour(this.tourStep + 1);
|
|
4225
|
+
});
|
|
4226
|
+
}
|
|
3857
4227
|
/** Narrow viewports render the panel as a bottom sheet, not a side/float dock. */
|
|
3858
4228
|
isMobile() {
|
|
3859
4229
|
return window.innerWidth <= 640;
|
|
@@ -3872,6 +4242,12 @@ var LoupeApp = class {
|
|
|
3872
4242
|
if (typeof p?.repoFilter === "string") this.repoFilter = p.repoFilter;
|
|
3873
4243
|
if (p?.float && typeof p.float.w === "number") this.floatRect = { ...this.floatRect, ...p.float };
|
|
3874
4244
|
if (typeof p?.markersHidden === "boolean") this.markersHidden = p.markersHidden;
|
|
4245
|
+
if (p?.accent && ACCENT_IDS.includes(p.accent)) this.accent = p.accent;
|
|
4246
|
+
if (typeof p?.minimized === "boolean") this.minimized = p.minimized;
|
|
4247
|
+
if (typeof p?.hoverHints === "boolean") this.hoverHints = p.hoverHints;
|
|
4248
|
+
if (typeof p?.showPaths === "boolean") this.showPaths = p.showPaths;
|
|
4249
|
+
if (typeof p?.tourDone === "boolean") this.tourDone = p.tourDone;
|
|
4250
|
+
if (Array.isArray(p?.hintsSeen)) this.hintsSeen = new Set(p.hintsSeen.filter((x) => typeof x === "string"));
|
|
3875
4251
|
} catch {
|
|
3876
4252
|
}
|
|
3877
4253
|
}
|
|
@@ -3886,7 +4262,13 @@ var LoupeApp = class {
|
|
|
3886
4262
|
markersHidden: this.markersHidden,
|
|
3887
4263
|
scope: this.scope,
|
|
3888
4264
|
statFilter: this.statFilter,
|
|
3889
|
-
repoFilter: this.repoFilter
|
|
4265
|
+
repoFilter: this.repoFilter,
|
|
4266
|
+
accent: this.accent,
|
|
4267
|
+
minimized: this.minimized,
|
|
4268
|
+
hoverHints: this.hoverHints,
|
|
4269
|
+
showPaths: this.showPaths,
|
|
4270
|
+
tourDone: this.tourDone,
|
|
4271
|
+
hintsSeen: [...this.hintsSeen]
|
|
3890
4272
|
}));
|
|
3891
4273
|
} catch {
|
|
3892
4274
|
}
|
|
@@ -3896,14 +4278,23 @@ var LoupeApp = class {
|
|
|
3896
4278
|
this.root.classList.toggle("theme-light", this.theme === "light");
|
|
3897
4279
|
this.themeBtn.title = this.theme === "dark" ? "Switch to light theme" : "Switch to dark theme";
|
|
3898
4280
|
this.themeBtn.innerHTML = this.theme === "dark" ? I_SUN : I_MOON;
|
|
4281
|
+
const accent = ACCENTS.find((a) => a.id === this.accent) ?? ACCENTS[0];
|
|
4282
|
+
this.root.style.setProperty("--accent", this.theme === "light" ? accent.light : accent.dark);
|
|
4283
|
+
this.root.style.setProperty("--accent-soft", accent.soft);
|
|
3899
4284
|
const d = this.dock;
|
|
3900
4285
|
d.classList.toggle("open", this.open);
|
|
3901
4286
|
for (const m of DOCK_MODES) d.classList.toggle("mode-" + m, this.dockMode === m);
|
|
4287
|
+
d.classList.toggle("minimized", this.minimized);
|
|
4288
|
+
const openCount = this.comments.filter((c) => !isResolved(c)).length;
|
|
4289
|
+
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>`;
|
|
3902
4290
|
d.classList.toggle("tab-home", this.tab === "home");
|
|
3903
4291
|
d.classList.toggle("tab-comments", this.tab === "comments");
|
|
3904
4292
|
d.classList.toggle("tab-connect", this.tab === "connect");
|
|
3905
4293
|
this.renderHome();
|
|
3906
4294
|
d.querySelectorAll(".tabs .tab").forEach((b) => b.classList.toggle("on", b.dataset.tab === this.tab));
|
|
4295
|
+
this.posMenu.querySelectorAll("[data-pos]").forEach((b) => b.classList.toggle("on", b.dataset.pos === this.dockMode));
|
|
4296
|
+
this.renderSettings();
|
|
4297
|
+
this.renderHints();
|
|
3907
4298
|
if (this.dockMode === "float") {
|
|
3908
4299
|
const vw = window.innerWidth, vh = window.innerHeight;
|
|
3909
4300
|
let { x, y, w, h } = this.floatRect;
|
|
@@ -4005,6 +4396,9 @@ var LoupeApp = class {
|
|
|
4005
4396
|
}
|
|
4006
4397
|
if (isResolved(c)) top.appendChild(el("span", "badge done", "resolved"));
|
|
4007
4398
|
else if (detached) top.appendChild(el("span", "badge detached", "element moved/removed"));
|
|
4399
|
+
if (this.showPaths && this.scope === "all" && c.url) {
|
|
4400
|
+
top.appendChild(el("span", "pathtag", shortPath(c.url)));
|
|
4401
|
+
}
|
|
4008
4402
|
top.appendChild(el("span", "caret", open ? "\u25BE" : "\u25B8"));
|
|
4009
4403
|
item.appendChild(top);
|
|
4010
4404
|
const summary = c.title || (c.body.split("\n")[0] ?? "").slice(0, 140) || "(no description)";
|
|
@@ -4141,6 +4535,13 @@ function fmtAgo(iso) {
|
|
|
4141
4535
|
if (h < 24) return `${h}h ago`;
|
|
4142
4536
|
return `${Math.round(h / 24)}d ago`;
|
|
4143
4537
|
}
|
|
4538
|
+
function shortPath(url) {
|
|
4539
|
+
try {
|
|
4540
|
+
return new URL(url, location.origin).pathname || "/";
|
|
4541
|
+
} catch {
|
|
4542
|
+
return url;
|
|
4543
|
+
}
|
|
4544
|
+
}
|
|
4144
4545
|
function dayLabel(iso) {
|
|
4145
4546
|
const d = new Date(iso);
|
|
4146
4547
|
if (Number.isNaN(d.getTime())) return "Earlier";
|
|
@@ -4185,6 +4586,12 @@ var I_MOON = svg(
|
|
|
4185
4586
|
var I_CLOSE = svg(
|
|
4186
4587
|
`<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"/>`
|
|
4187
4588
|
);
|
|
4589
|
+
var I_GEAR = svg(
|
|
4590
|
+
`<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"/>`
|
|
4591
|
+
);
|
|
4592
|
+
var I_MINIMIZE = svg(
|
|
4593
|
+
`<path d="M3.5 8h9" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>`
|
|
4594
|
+
);
|
|
4188
4595
|
var I_COMMENT = svg(
|
|
4189
4596
|
`<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"/>`
|
|
4190
4597
|
);
|