@loupekit/sdk 0.10.11 → 0.10.12
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 +2 -1
- package/dist/index.global.js +312 -28
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +312 -28
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -300,11 +300,63 @@ var STYLES = (
|
|
|
300
300
|
.tabs .tab:hover { color: var(--ink); background: var(--bg-2); }
|
|
301
301
|
.tabs .tab.on { color: var(--accent); border-bottom-color: var(--accent); }
|
|
302
302
|
|
|
303
|
-
/*
|
|
303
|
+
/* Three sidebar pages: only the active one shows. */
|
|
304
304
|
.view { display: none; }
|
|
305
|
+
.dock.tab-home .home-view { display: block; flex: 1; min-height: 0; overflow-y: auto; }
|
|
305
306
|
.dock.tab-comments .comments-view { display: flex; flex-direction: column; flex: 1; min-height: 0; }
|
|
306
307
|
.dock.tab-connect .connect-view { display: block; flex: 1; overflow-y: auto; }
|
|
307
308
|
|
|
309
|
+
/* ------------------------------------------------------------- Home overview */
|
|
310
|
+
.home-view { padding: 12px 12px 18px; }
|
|
311
|
+
.hstat { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
|
|
312
|
+
.hstat-b {
|
|
313
|
+
display: flex; flex-direction: column; gap: 2px; text-align: left; padding: 10px;
|
|
314
|
+
border: 1px solid var(--line); border-radius: 10px; background: var(--bg-2); color: var(--ink); cursor: pointer;
|
|
315
|
+
}
|
|
316
|
+
.hstat-b:hover { border-color: var(--accent); }
|
|
317
|
+
.hstat-b.on { border-color: var(--accent); background: var(--bg-3); }
|
|
318
|
+
.hstat-n { font-size: 20px; font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums; }
|
|
319
|
+
.hstat-l { font-size: 11px; color: var(--muted); }
|
|
320
|
+
.hscope { display: flex; align-items: center; gap: 6px; margin-top: 10px; }
|
|
321
|
+
.hscope-b {
|
|
322
|
+
flex: 1; padding: 7px 8px; border: 1px solid var(--line); border-radius: 8px;
|
|
323
|
+
background: var(--bg-2); color: var(--muted); font-size: 12px; font-weight: 600; cursor: pointer;
|
|
324
|
+
}
|
|
325
|
+
.hscope-b.on { background: var(--accent); border-color: var(--accent); color: #fff; }
|
|
326
|
+
.hrefresh {
|
|
327
|
+
flex: none; width: 30px; height: 30px; border: 1px solid var(--line); border-radius: 8px;
|
|
328
|
+
background: var(--bg-2); color: var(--muted); cursor: pointer; font-size: 14px;
|
|
329
|
+
}
|
|
330
|
+
.hrefresh:hover { border-color: var(--accent); color: var(--ink); }
|
|
331
|
+
.hpin {
|
|
332
|
+
margin-top: 10px; width: 100%; padding: 9px; border: 0; border-radius: 9px;
|
|
333
|
+
background: var(--accent); color: #fff; font-size: 13px; font-weight: 600; cursor: pointer;
|
|
334
|
+
}
|
|
335
|
+
.hlabel { margin-top: 14px; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); font-weight: 700; }
|
|
336
|
+
.hfeed { margin-top: 6px; display: flex; flex-direction: column; gap: 4px; }
|
|
337
|
+
.hfeed-i {
|
|
338
|
+
display: flex; flex-direction: column; gap: 3px; text-align: left; padding: 8px;
|
|
339
|
+
border: 1px solid var(--line); border-radius: 8px; background: var(--bg-2); color: var(--ink); cursor: pointer;
|
|
340
|
+
}
|
|
341
|
+
.hfeed-i:hover { border-color: var(--accent); }
|
|
342
|
+
.hfeed-t { font-size: 12.5px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
343
|
+
.hfeed-m { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 11px; color: var(--muted); }
|
|
344
|
+
.hfeed-s { font-weight: 700; }
|
|
345
|
+
.hfeed-s-resolved { color: #34c281; }
|
|
346
|
+
.hfeed-p { font-weight: 700; }
|
|
347
|
+
.hfeed-p-critical { color: var(--pin); }
|
|
348
|
+
.hfeed-p-high { color: #e0a92c; }
|
|
349
|
+
.hempty { padding: 14px; text-align: center; color: var(--muted); font-size: 12px; }
|
|
350
|
+
.hfoot { margin-top: 14px; padding-top: 10px; border-top: 1px solid var(--line); font-size: 11px; color: var(--muted); text-align: center; }
|
|
351
|
+
|
|
352
|
+
/* Timeline grouping (project scope) + the repo filter. */
|
|
353
|
+
.daylabel { margin: 8px 2px 0; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); font-weight: 700; }
|
|
354
|
+
.listhead .reposel {
|
|
355
|
+
margin-left: 6px; font-size: 11px; padding: 3px 6px; border: 1px solid var(--line);
|
|
356
|
+
border-radius: 7px; background: var(--bg-2); color: var(--ink);
|
|
357
|
+
text-transform: none; letter-spacing: 0; max-width: 130px;
|
|
358
|
+
}
|
|
359
|
+
|
|
308
360
|
/* recording marker + video in the list */
|
|
309
361
|
.item .rectag { font-size: 10px; font-weight: 700; color: var(--pin); background: var(--bg-3); border-radius: 999px; padding: 1px 7px; white-space: nowrap; }
|
|
310
362
|
.item video.shot { width: 100%; border-radius: 6px; margin-top: 8px; border: 1px solid var(--line); background: #000; display: block; }
|
|
@@ -2355,6 +2407,20 @@ var LocalStorageAdapter = class {
|
|
|
2355
2407
|
async list(projectKey, url) {
|
|
2356
2408
|
return this.readAll(projectKey, url);
|
|
2357
2409
|
}
|
|
2410
|
+
/** Every page's comments for this project, newest first — the "All" scope. */
|
|
2411
|
+
async listAll(projectKey) {
|
|
2412
|
+
const prefix = `loupe:${projectKey}:`;
|
|
2413
|
+
const out = [];
|
|
2414
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
2415
|
+
const k = localStorage.key(i);
|
|
2416
|
+
if (!k || !k.startsWith(prefix)) continue;
|
|
2417
|
+
try {
|
|
2418
|
+
out.push(...JSON.parse(localStorage.getItem(k) || "[]"));
|
|
2419
|
+
} catch {
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
return out.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
2423
|
+
}
|
|
2358
2424
|
async save(comment) {
|
|
2359
2425
|
const all = this.readAll(comment.projectKey, comment.url);
|
|
2360
2426
|
all.push(comment);
|
|
@@ -2375,11 +2441,30 @@ var LocalStorageAdapter = class {
|
|
|
2375
2441
|
size: file.size
|
|
2376
2442
|
};
|
|
2377
2443
|
}
|
|
2378
|
-
|
|
2444
|
+
/**
|
|
2445
|
+
* Keys that hold a comment list. `loupe:dock` also lives under `loupe:` but
|
|
2446
|
+
* holds an OBJECT (panel state), so treating every `loupe:` key as a list made
|
|
2447
|
+
* update()/remove() throw as soon as the panel persisted anything.
|
|
2448
|
+
*/
|
|
2449
|
+
commentKeys() {
|
|
2450
|
+
const keys = [];
|
|
2379
2451
|
for (let i = 0; i < localStorage.length; i++) {
|
|
2380
2452
|
const k = localStorage.key(i);
|
|
2381
|
-
if (
|
|
2382
|
-
|
|
2453
|
+
if (k && k.startsWith("loupe:") && k !== "loupe:dock") keys.push(k);
|
|
2454
|
+
}
|
|
2455
|
+
return keys;
|
|
2456
|
+
}
|
|
2457
|
+
parseList(key) {
|
|
2458
|
+
try {
|
|
2459
|
+
const value = JSON.parse(localStorage.getItem(key) || "[]");
|
|
2460
|
+
return Array.isArray(value) ? value : [];
|
|
2461
|
+
} catch {
|
|
2462
|
+
return [];
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
async update(id, patch) {
|
|
2466
|
+
for (const k of this.commentKeys()) {
|
|
2467
|
+
const list = this.parseList(k);
|
|
2383
2468
|
const idx = list.findIndex((c) => c.id === id);
|
|
2384
2469
|
if (idx >= 0) {
|
|
2385
2470
|
list[idx] = { ...list[idx], ...patch };
|
|
@@ -2389,10 +2474,8 @@ var LocalStorageAdapter = class {
|
|
|
2389
2474
|
}
|
|
2390
2475
|
}
|
|
2391
2476
|
async remove(id) {
|
|
2392
|
-
for (
|
|
2393
|
-
const
|
|
2394
|
-
if (!k || !k.startsWith("loupe:")) continue;
|
|
2395
|
-
const list = JSON.parse(localStorage.getItem(k) || "[]");
|
|
2477
|
+
for (const k of this.commentKeys()) {
|
|
2478
|
+
const list = this.parseList(k);
|
|
2396
2479
|
const next = list.filter((c) => c.id !== id);
|
|
2397
2480
|
if (next.length !== list.length) {
|
|
2398
2481
|
localStorage.setItem(k, JSON.stringify(next));
|
|
@@ -2428,6 +2511,13 @@ var HttpAdapter = class {
|
|
|
2428
2511
|
if (!res.ok) throw new Error(`list failed: ${res.status}`);
|
|
2429
2512
|
return await res.json();
|
|
2430
2513
|
}
|
|
2514
|
+
/** Every comment in the project (no page filter) — the "All" scope. */
|
|
2515
|
+
async listAll(projectKey) {
|
|
2516
|
+
const q = new URLSearchParams({ projectKey });
|
|
2517
|
+
const res = await fetch(`${this.base}/v1/comments?${q}`, this.opts({ headers: this.headers() }));
|
|
2518
|
+
if (!res.ok) throw new Error(`listAll failed: ${res.status}`);
|
|
2519
|
+
return await res.json();
|
|
2520
|
+
}
|
|
2431
2521
|
/** Upload an inline data-URL asset to object storage; return its URL (or the data URL on failure). */
|
|
2432
2522
|
async uploadBlob(projectKey, data) {
|
|
2433
2523
|
try {
|
|
@@ -2486,6 +2576,13 @@ var HttpAdapter = class {
|
|
|
2486
2576
|
|
|
2487
2577
|
// ../shared/dist/index.js
|
|
2488
2578
|
var COMMENT_STAGES = ["queue", "todo", "in_progress", "in_review", "resolved"];
|
|
2579
|
+
var STAGE_LABELS = {
|
|
2580
|
+
queue: "Queue",
|
|
2581
|
+
todo: "To Do",
|
|
2582
|
+
in_progress: "In Progress",
|
|
2583
|
+
in_review: "In Review",
|
|
2584
|
+
resolved: "Resolved"
|
|
2585
|
+
};
|
|
2489
2586
|
var LEGACY_STATUS = {
|
|
2490
2587
|
open: "queue",
|
|
2491
2588
|
in_progress: "in_progress",
|
|
@@ -2516,9 +2613,15 @@ var CHANGE_TYPE_LABELS = {
|
|
|
2516
2613
|
};
|
|
2517
2614
|
var DEFAULT_PRIORITY = "medium";
|
|
2518
2615
|
var DEFAULT_CHANGE_TYPE = "other";
|
|
2616
|
+
function normalizePriority(value) {
|
|
2617
|
+
if (typeof value === "string" && COMMENT_PRIORITIES.includes(value)) {
|
|
2618
|
+
return value;
|
|
2619
|
+
}
|
|
2620
|
+
return DEFAULT_PRIORITY;
|
|
2621
|
+
}
|
|
2519
2622
|
|
|
2520
2623
|
// src/app.ts
|
|
2521
|
-
var SDK_VERSION = true ? "0.10.
|
|
2624
|
+
var SDK_VERSION = true ? "0.10.12" : "dev";
|
|
2522
2625
|
var DOCK_MODES = ["left", "right", "bottom", "float"];
|
|
2523
2626
|
var RECORD_MAX_MS = 2e4;
|
|
2524
2627
|
var MAX_FILES = 10;
|
|
@@ -2588,7 +2691,15 @@ var LoupeApp = class {
|
|
|
2588
2691
|
this.dockMode = "right";
|
|
2589
2692
|
this.open = true;
|
|
2590
2693
|
this.theme = "dark";
|
|
2591
|
-
this.tab = "
|
|
2694
|
+
this.tab = "home";
|
|
2695
|
+
/** "page" = this URL only; "all" = the whole project (the timeline scope). */
|
|
2696
|
+
this.scope = "page";
|
|
2697
|
+
/** Every-page comments, fetched the first time the "All" scope is opened. */
|
|
2698
|
+
this.allComments = [];
|
|
2699
|
+
/** A clicked stat tile, which narrows the list. */
|
|
2700
|
+
this.statFilter = "";
|
|
2701
|
+
/** Repo filter, offered once the scope is "all". */
|
|
2702
|
+
this.repoFilter = "";
|
|
2592
2703
|
/** Float-mode window geometry; (x<=0 && y<=0) → placed on first layout. */
|
|
2593
2704
|
this.floatRect = { x: 0, y: 0, w: 380, h: 540 };
|
|
2594
2705
|
this.floatDrag = null;
|
|
@@ -2817,6 +2928,8 @@ var LoupeApp = class {
|
|
|
2817
2928
|
this.comments = await this.store.list(this.cfg.projectKey, this.url);
|
|
2818
2929
|
this.renderPins();
|
|
2819
2930
|
this.renderList();
|
|
2931
|
+
this.renderHome();
|
|
2932
|
+
if (this.scope === "all") void this.loadAllComments();
|
|
2820
2933
|
this.observe();
|
|
2821
2934
|
this.watchNavigation();
|
|
2822
2935
|
if (this.cfg.autoOpen) this.setMode("inspect");
|
|
@@ -2914,13 +3027,13 @@ var LoupeApp = class {
|
|
|
2914
3027
|
ctl.append(this.themeBtn, closeBtn);
|
|
2915
3028
|
head.append(brand, ctl);
|
|
2916
3029
|
const tabs = el("div", "tabs");
|
|
2917
|
-
const
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
tabs.append(
|
|
3030
|
+
const tabBtn = (key, label) => {
|
|
3031
|
+
const b = el("button", "tab", label);
|
|
3032
|
+
b.dataset.tab = key;
|
|
3033
|
+
b.onclick = () => this.setTab(key);
|
|
3034
|
+
return b;
|
|
3035
|
+
};
|
|
3036
|
+
tabs.append(tabBtn("home", "Home"), tabBtn("comments", "Comments"), tabBtn("connect", "Connect Claude"));
|
|
2924
3037
|
const tools = el("div", "tools");
|
|
2925
3038
|
const inspectBtn = this.toolBtn("\u271B", "Inspect", "inspect");
|
|
2926
3039
|
inspectBtn.title = "Inspect an element and comment on it";
|
|
@@ -2957,15 +3070,119 @@ var LoupeApp = class {
|
|
|
2957
3070
|
this.renderList();
|
|
2958
3071
|
};
|
|
2959
3072
|
listHead.appendChild(search);
|
|
3073
|
+
this.repoSel = el("select", "reposel");
|
|
3074
|
+
this.repoSel.title = "Filter by repository";
|
|
3075
|
+
this.repoSel.setAttribute("aria-label", "Filter by repository");
|
|
3076
|
+
this.repoSel.onchange = () => {
|
|
3077
|
+
this.repoFilter = this.repoSel.value;
|
|
3078
|
+
this.renderList();
|
|
3079
|
+
};
|
|
3080
|
+
this.repoSel.style.display = "none";
|
|
3081
|
+
listHead.appendChild(this.repoSel);
|
|
2960
3082
|
this.listEl = el("div", "list");
|
|
3083
|
+
const homeView = el("div", "view home-view");
|
|
3084
|
+
this.homeEl = homeView;
|
|
2961
3085
|
const commentsView = el("div", "view comments-view");
|
|
2962
3086
|
commentsView.append(tools, listHead, this.listEl, this.buildIntegrations());
|
|
2963
3087
|
const connectView = this.buildConnectPanel();
|
|
2964
3088
|
const resize = el("div", "resize");
|
|
2965
3089
|
resize.addEventListener("pointerdown", this.onResizeDown);
|
|
2966
|
-
dock.append(head, tabs, commentsView, connectView, resize);
|
|
3090
|
+
dock.append(head, tabs, homeView, commentsView, connectView, resize);
|
|
3091
|
+
this.buildHomePanel();
|
|
2967
3092
|
return dock;
|
|
2968
3093
|
}
|
|
3094
|
+
/**
|
|
3095
|
+
* The Home overview: four stat tiles over the current scope, a scope switch
|
|
3096
|
+
* (this page ↔ the whole project), a one-click way into capture, and the
|
|
3097
|
+
* most recent feedback. Every tile is a button that narrows the list.
|
|
3098
|
+
*/
|
|
3099
|
+
buildHomePanel() {
|
|
3100
|
+
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>`;
|
|
3102
|
+
this.homeEl.querySelectorAll(".hscope-b").forEach((b) => {
|
|
3103
|
+
b.onclick = () => this.setScope(b.dataset.scope === "all" ? "all" : "page");
|
|
3104
|
+
});
|
|
3105
|
+
this.homeEl.querySelector('[data-role="home-pin"]').onclick = () => {
|
|
3106
|
+
this.setTab("comments");
|
|
3107
|
+
this.setMode("inspect");
|
|
3108
|
+
};
|
|
3109
|
+
this.homeEl.querySelector(".hrefresh").onclick = () => {
|
|
3110
|
+
void this.reloadComments();
|
|
3111
|
+
if (this.scope === "all") void this.loadAllComments();
|
|
3112
|
+
};
|
|
3113
|
+
}
|
|
3114
|
+
/** The comments the panel is currently looking at (page scope or project scope). */
|
|
3115
|
+
get visibleComments() {
|
|
3116
|
+
return this.scope === "all" ? this.allComments : this.comments;
|
|
3117
|
+
}
|
|
3118
|
+
/** Switch the panel between "this page" and the whole project. */
|
|
3119
|
+
setScope(scope) {
|
|
3120
|
+
this.scope = scope;
|
|
3121
|
+
this.saveState();
|
|
3122
|
+
if (scope === "all" && !this.allComments.length) void this.loadAllComments();
|
|
3123
|
+
this.renderHome();
|
|
3124
|
+
this.renderList();
|
|
3125
|
+
}
|
|
3126
|
+
async loadAllComments() {
|
|
3127
|
+
try {
|
|
3128
|
+
this.allComments = await this.store.listAll(this.cfg.projectKey);
|
|
3129
|
+
this.renderHome();
|
|
3130
|
+
this.renderList();
|
|
3131
|
+
} catch {
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
/**
|
|
3135
|
+
* The four buckets a triager actually asks about. "Stale" is open work older
|
|
3136
|
+
* than a week — the thing that quietly rots on a board.
|
|
3137
|
+
*/
|
|
3138
|
+
homeStats() {
|
|
3139
|
+
const list = this.visibleComments;
|
|
3140
|
+
const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1e3;
|
|
3141
|
+
return [
|
|
3142
|
+
{ key: "open", label: "Open", n: list.filter((c) => normalizeStatus(c.status) !== "resolved").length },
|
|
3143
|
+
{ key: "needs_you", label: "Needs you", n: list.filter((c) => normalizeStatus(c.status) === "in_review").length },
|
|
3144
|
+
{ key: "resolved", label: "Resolved", n: list.filter((c) => normalizeStatus(c.status) === "resolved").length },
|
|
3145
|
+
{
|
|
3146
|
+
key: "stale",
|
|
3147
|
+
label: "Stale",
|
|
3148
|
+
n: list.filter((c) => normalizeStatus(c.status) !== "resolved" && Date.parse(c.createdAt) < weekAgo).length
|
|
3149
|
+
}
|
|
3150
|
+
];
|
|
3151
|
+
}
|
|
3152
|
+
renderHome() {
|
|
3153
|
+
if (!this.homeEl) return;
|
|
3154
|
+
const stats = this.homeStats();
|
|
3155
|
+
const statsEl = this.homeEl.querySelector("#loupe-hstats");
|
|
3156
|
+
if (statsEl) {
|
|
3157
|
+
statsEl.innerHTML = stats.map((s) => `<button class="hstat-b${this.statFilter === s.key ? " on" : ""}" data-stat="${s.key}"><span class="hstat-n">${s.n}</span><span class="hstat-l">${s.label}</span></button>`).join("");
|
|
3158
|
+
statsEl.querySelectorAll(".hstat-b").forEach((b) => {
|
|
3159
|
+
b.onclick = () => {
|
|
3160
|
+
const key = b.dataset.stat;
|
|
3161
|
+
this.statFilter = this.statFilter === key ? "" : key;
|
|
3162
|
+
this.setTab("comments");
|
|
3163
|
+
};
|
|
3164
|
+
});
|
|
3165
|
+
}
|
|
3166
|
+
this.homeEl.querySelectorAll(".hscope-b").forEach((b) => b.classList.toggle("on", b.dataset.scope === this.scope));
|
|
3167
|
+
const feed = this.homeEl.querySelector("#loupe-hfeed");
|
|
3168
|
+
if (!feed) return;
|
|
3169
|
+
const recent = [...this.visibleComments].sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, 8);
|
|
3170
|
+
feed.innerHTML = recent.length ? recent.map((c) => {
|
|
3171
|
+
const stage = STAGE_LABELS[normalizeStatus(c.status)];
|
|
3172
|
+
const prio = normalizePriority(c.priority);
|
|
3173
|
+
return `<button class="hfeed-i" data-id="${escapeAttr(c.id)}"><span class="hfeed-t">${escapeHtml(c.title || (c.body.split("\n")[0] ?? "").slice(0, 60))}</span><span class="hfeed-m">${escapeHtml(c.author?.name ?? "")} \xB7 ${fmtAgo(c.createdAt)} \xB7 <span class="hfeed-s hfeed-s-${normalizeStatus(c.status)}">${stage}</span><span class="hfeed-p hfeed-p-${prio}">${PRIORITY_LABELS[prio]}</span></span></button>`;
|
|
3174
|
+
}).join("") : `<div class="hempty">Nothing here yet.</div>`;
|
|
3175
|
+
feed.querySelectorAll(".hfeed-i").forEach((b) => {
|
|
3176
|
+
b.onclick = () => {
|
|
3177
|
+
const id = b.dataset.id;
|
|
3178
|
+
this.statFilter = "";
|
|
3179
|
+
this.setTab("comments");
|
|
3180
|
+
this.expanded.add(id);
|
|
3181
|
+
this.renderList();
|
|
3182
|
+
this.flash(id);
|
|
3183
|
+
};
|
|
3184
|
+
});
|
|
3185
|
+
}
|
|
2969
3186
|
/** The "INTEGRATES WITH" footer on the Comments page (visual only for now). */
|
|
2970
3187
|
buildIntegrations() {
|
|
2971
3188
|
const wrap = el("div", "integrations");
|
|
@@ -3558,9 +3775,9 @@ var LoupeApp = class {
|
|
|
3558
3775
|
this.updateCount();
|
|
3559
3776
|
this.position();
|
|
3560
3777
|
}
|
|
3561
|
-
updateCount() {
|
|
3778
|
+
updateCount(shown = this.comments.length) {
|
|
3779
|
+
this.countEl.textContent = String(shown);
|
|
3562
3780
|
const n = this.comments.length;
|
|
3563
|
-
this.countEl.textContent = String(n);
|
|
3564
3781
|
this.fabBadge.textContent = n ? String(n) : "";
|
|
3565
3782
|
}
|
|
3566
3783
|
/**
|
|
@@ -3619,14 +3836,18 @@ var LoupeApp = class {
|
|
|
3619
3836
|
this.saveState();
|
|
3620
3837
|
this.applyDockLayout();
|
|
3621
3838
|
}
|
|
3622
|
-
/** Switch the sidebar page (Comments ↔ Connect Claude). */
|
|
3839
|
+
/** Switch the sidebar page (Home ↔ Comments ↔ Connect Claude). */
|
|
3623
3840
|
setTab(tab) {
|
|
3624
3841
|
if (this.tab === tab) return;
|
|
3625
3842
|
if (tab !== "comments") this.setMode("off");
|
|
3626
3843
|
this.tab = tab;
|
|
3627
3844
|
this.saveState();
|
|
3628
|
-
|
|
3845
|
+
if (tab === "home") {
|
|
3846
|
+
this.renderHome();
|
|
3847
|
+
if (this.scope === "all" && !this.allComments.length) void this.loadAllComments();
|
|
3848
|
+
}
|
|
3629
3849
|
if (tab === "comments") this.renderList();
|
|
3850
|
+
this.applyDockLayout();
|
|
3630
3851
|
}
|
|
3631
3852
|
toggleTheme() {
|
|
3632
3853
|
this.theme = this.theme === "dark" ? "light" : "dark";
|
|
@@ -3645,7 +3866,10 @@ var LoupeApp = class {
|
|
|
3645
3866
|
if (DOCK_MODES.includes(p?.mode)) this.dockMode = p.mode;
|
|
3646
3867
|
if (typeof p?.open === "boolean") this.open = p.open;
|
|
3647
3868
|
if (p?.theme === "light" || p?.theme === "dark") this.theme = p.theme;
|
|
3648
|
-
if (p?.tab === "comments" || p?.tab === "connect") this.tab = p.tab;
|
|
3869
|
+
if (p?.tab === "home" || p?.tab === "comments" || p?.tab === "connect") this.tab = p.tab;
|
|
3870
|
+
if (p?.scope === "page" || p?.scope === "all") this.scope = p.scope;
|
|
3871
|
+
if (typeof p?.statFilter === "string") this.statFilter = p.statFilter;
|
|
3872
|
+
if (typeof p?.repoFilter === "string") this.repoFilter = p.repoFilter;
|
|
3649
3873
|
if (p?.float && typeof p.float.w === "number") this.floatRect = { ...this.floatRect, ...p.float };
|
|
3650
3874
|
if (typeof p?.markersHidden === "boolean") this.markersHidden = p.markersHidden;
|
|
3651
3875
|
} catch {
|
|
@@ -3659,7 +3883,10 @@ var LoupeApp = class {
|
|
|
3659
3883
|
theme: this.theme,
|
|
3660
3884
|
tab: this.tab,
|
|
3661
3885
|
float: this.floatRect,
|
|
3662
|
-
markersHidden: this.markersHidden
|
|
3886
|
+
markersHidden: this.markersHidden,
|
|
3887
|
+
scope: this.scope,
|
|
3888
|
+
statFilter: this.statFilter,
|
|
3889
|
+
repoFilter: this.repoFilter
|
|
3663
3890
|
}));
|
|
3664
3891
|
} catch {
|
|
3665
3892
|
}
|
|
@@ -3672,8 +3899,10 @@ var LoupeApp = class {
|
|
|
3672
3899
|
const d = this.dock;
|
|
3673
3900
|
d.classList.toggle("open", this.open);
|
|
3674
3901
|
for (const m of DOCK_MODES) d.classList.toggle("mode-" + m, this.dockMode === m);
|
|
3902
|
+
d.classList.toggle("tab-home", this.tab === "home");
|
|
3675
3903
|
d.classList.toggle("tab-comments", this.tab === "comments");
|
|
3676
3904
|
d.classList.toggle("tab-connect", this.tab === "connect");
|
|
3905
|
+
this.renderHome();
|
|
3677
3906
|
d.querySelectorAll(".tabs .tab").forEach((b) => b.classList.toggle("on", b.dataset.tab === this.tab));
|
|
3678
3907
|
if (this.dockMode === "float") {
|
|
3679
3908
|
const vw = window.innerWidth, vh = window.innerHeight;
|
|
@@ -3719,16 +3948,46 @@ var LoupeApp = class {
|
|
|
3719
3948
|
renderList() {
|
|
3720
3949
|
this.listEl.innerHTML = "";
|
|
3721
3950
|
const q = this.search.trim().toLowerCase();
|
|
3722
|
-
const
|
|
3951
|
+
const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1e3;
|
|
3952
|
+
let items = this.visibleComments.filter((c) => {
|
|
3953
|
+
const stage = normalizeStatus(c.status);
|
|
3954
|
+
if (this.statFilter === "open" && stage === "resolved") return false;
|
|
3955
|
+
if (this.statFilter === "needs_you" && stage !== "in_review") return false;
|
|
3956
|
+
if (this.statFilter === "resolved" && stage !== "resolved") return false;
|
|
3957
|
+
if (this.statFilter === "stale" && (stage === "resolved" || !(Date.parse(c.createdAt) < weekAgo))) return false;
|
|
3958
|
+
if (this.repoFilter && c.repo !== this.repoFilter) return false;
|
|
3959
|
+
return !q || `${c.title ?? ""} ${c.body} ${c.author?.name ?? ""}`.toLowerCase().includes(q);
|
|
3960
|
+
});
|
|
3961
|
+
if (this.scope === "all") items = [...items].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
3962
|
+
this.renderRepoFilter();
|
|
3723
3963
|
if (!items.length) {
|
|
3724
3964
|
this.listEl.appendChild(el(
|
|
3725
3965
|
"div",
|
|
3726
3966
|
"empty",
|
|
3727
|
-
q ? "No comments match your search." : "No comments yet. Use Inspect to pick an element, or Note to drop a comment anywhere on the page."
|
|
3967
|
+
this.statFilter ? "Nothing in this bucket." : q ? "No comments match your search." : this.scope === "all" ? "No feedback in this project yet." : "No comments yet. Use Inspect to pick an element, or Note to drop a comment anywhere on the page."
|
|
3728
3968
|
));
|
|
3729
3969
|
}
|
|
3730
|
-
|
|
3731
|
-
|
|
3970
|
+
let lastDay = "";
|
|
3971
|
+
items.forEach((c, i) => {
|
|
3972
|
+
if (this.scope === "all") {
|
|
3973
|
+
const day = dayLabel(c.createdAt);
|
|
3974
|
+
if (day !== lastDay) {
|
|
3975
|
+
this.listEl.appendChild(el("div", "daylabel", day));
|
|
3976
|
+
lastDay = day;
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
this.listEl.appendChild(this.itemView(c, i));
|
|
3980
|
+
});
|
|
3981
|
+
this.updateCount(items.length);
|
|
3982
|
+
}
|
|
3983
|
+
/** Repo filter — only offered in the project scope, where it means something. */
|
|
3984
|
+
renderRepoFilter() {
|
|
3985
|
+
if (!this.repoSel) return;
|
|
3986
|
+
const repos = [...new Set(this.visibleComments.map((c) => c.repo).filter((r) => !!r))].sort();
|
|
3987
|
+
this.repoSel.style.display = this.scope === "all" && repos.length > 1 ? "" : "none";
|
|
3988
|
+
const current = this.repoFilter;
|
|
3989
|
+
this.repoSel.innerHTML = `<option value="">All repos</option>` + repos.map((r) => `<option value="${escapeHtml(r)}">${escapeHtml(r)}</option>`).join("");
|
|
3990
|
+
this.repoSel.value = current;
|
|
3732
3991
|
}
|
|
3733
3992
|
itemView(c, i) {
|
|
3734
3993
|
const detached = this.pins.get(c.id)?.classList.contains("detached") && !isResolved(c);
|
|
@@ -3871,6 +4130,28 @@ function el(tag, cls = "", text = "") {
|
|
|
3871
4130
|
function clamp(n) {
|
|
3872
4131
|
return Math.max(0, Math.min(1, n));
|
|
3873
4132
|
}
|
|
4133
|
+
function fmtAgo(iso) {
|
|
4134
|
+
const then = Date.parse(iso);
|
|
4135
|
+
if (Number.isNaN(then)) return "";
|
|
4136
|
+
const s = Math.max(0, Math.round((Date.now() - then) / 1e3));
|
|
4137
|
+
if (s < 60) return "just now";
|
|
4138
|
+
const m = Math.round(s / 60);
|
|
4139
|
+
if (m < 60) return `${m}m ago`;
|
|
4140
|
+
const h = Math.round(m / 60);
|
|
4141
|
+
if (h < 24) return `${h}h ago`;
|
|
4142
|
+
return `${Math.round(h / 24)}d ago`;
|
|
4143
|
+
}
|
|
4144
|
+
function dayLabel(iso) {
|
|
4145
|
+
const d = new Date(iso);
|
|
4146
|
+
if (Number.isNaN(d.getTime())) return "Earlier";
|
|
4147
|
+
const today = /* @__PURE__ */ new Date();
|
|
4148
|
+
const startOf = (x) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();
|
|
4149
|
+
const days = Math.round((startOf(today) - startOf(d)) / 864e5);
|
|
4150
|
+
if (days <= 0) return "Today";
|
|
4151
|
+
if (days === 1) return "Yesterday";
|
|
4152
|
+
if (days < 7) return `${days} days ago`;
|
|
4153
|
+
return d.toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" });
|
|
4154
|
+
}
|
|
3874
4155
|
function clampPx(n, min, max) {
|
|
3875
4156
|
return Math.max(min, Math.min(max, n));
|
|
3876
4157
|
}
|
|
@@ -3884,6 +4165,9 @@ function optionEl(text, value, selected) {
|
|
|
3884
4165
|
function escapeHtml(s) {
|
|
3885
4166
|
return s.replace(/[&<>"']/g, (m) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[m]);
|
|
3886
4167
|
}
|
|
4168
|
+
function escapeAttr(s) {
|
|
4169
|
+
return escapeHtml(s);
|
|
4170
|
+
}
|
|
3887
4171
|
var svg = (inner) => `<svg viewBox="0 0 16 16" width="15" height="15" fill="none" aria-hidden="true">${inner}</svg>`;
|
|
3888
4172
|
var DOCK_FRAME = `<rect x="1.5" y="2.5" width="13" height="11" rx="1.6" stroke="currentColor" stroke-width="1.3"/>`;
|
|
3889
4173
|
var I_DOCK_LEFT = svg(`${DOCK_FRAME}<rect x="2.2" y="3.2" width="4" height="9.6" rx="1" fill="currentColor"/>`);
|