@loupekit/sdk 0.8.1 → 0.9.0-next.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.global.js +185 -59
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +185 -59
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.global.js
CHANGED
|
@@ -198,6 +198,16 @@ var Loupe = (() => {
|
|
|
198
198
|
.item .actions button { font-size: 11px; border: 1px solid var(--line); background: var(--bg-3); border-radius: 6px; padding: 4px 8px; cursor: pointer; color: var(--ink); }
|
|
199
199
|
.item .actions button:hover { border-color: var(--accent); }
|
|
200
200
|
.item img.shot { width: 100%; border-radius: 6px; margin-top: 8px; border: 1px solid var(--line); }
|
|
201
|
+
.item video.shot { width: 100%; border-radius: 6px; margin-top: 8px; border: 1px solid var(--line); }
|
|
202
|
+
.item .caret { margin-left: auto; color: var(--muted); font-size: 11px; }
|
|
203
|
+
.item .summary { font-size: 13px; font-weight: 600; line-height: 1.35; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
204
|
+
.item .detail { margin-top: 6px; }
|
|
205
|
+
.item.collapsed .detail { display: none; }
|
|
206
|
+
.listhead .search {
|
|
207
|
+
margin-left: auto; flex: 1; max-width: 170px; font-size: 12px; text-transform: none; letter-spacing: 0;
|
|
208
|
+
padding: 4px 8px; border: 1px solid var(--line); border-radius: 7px; background: var(--bg-2); color: var(--ink); outline: none;
|
|
209
|
+
}
|
|
210
|
+
.listhead .search:focus { border-color: var(--accent); }
|
|
201
211
|
|
|
202
212
|
/* float resize grip (bottom-right corner) */
|
|
203
213
|
.resize { display: none; position: absolute; right: 0; bottom: 0; width: 16px; height: 16px; cursor: nwse-resize; z-index: 1; }
|
|
@@ -209,7 +219,7 @@ var Loupe = (() => {
|
|
|
209
219
|
|
|
210
220
|
/* composer popover */
|
|
211
221
|
.composer {
|
|
212
|
-
position: fixed; z-index: 2147483004; pointer-events: auto; width:
|
|
222
|
+
position: fixed; z-index: 2147483004; pointer-events: auto; width: 320px;
|
|
213
223
|
background: var(--bg); color: var(--ink); border: 1px solid var(--line);
|
|
214
224
|
border-radius: 12px; box-shadow: var(--shadow); padding: 12px; display: none;
|
|
215
225
|
}
|
|
@@ -230,6 +240,25 @@ var Loupe = (() => {
|
|
|
230
240
|
.composer .primary { background: var(--accent); color: #fff; }
|
|
231
241
|
.composer .primary:disabled { opacity: .5; cursor: default; }
|
|
232
242
|
.composer .ghost { background: var(--bg-3); color: var(--ink); }
|
|
243
|
+
.composer input.title {
|
|
244
|
+
width: 100%; border: 1px solid var(--line); border-radius: 8px; padding: 8px; margin-bottom: 8px;
|
|
245
|
+
font-size: 13px; font-weight: 600; color: var(--ink); background: var(--bg-2); outline: none;
|
|
246
|
+
}
|
|
247
|
+
.composer input.title:focus { border-color: var(--accent); }
|
|
248
|
+
.composer .attach { margin-top: 8px; }
|
|
249
|
+
.composer .pick {
|
|
250
|
+
width: 100%; border: 1px dashed var(--line); background: transparent; color: var(--muted);
|
|
251
|
+
border-radius: 8px; padding: 7px; font-size: 12px; font-weight: 600; cursor: pointer;
|
|
252
|
+
}
|
|
253
|
+
.composer .pick:hover { border-color: var(--accent); color: var(--accent); }
|
|
254
|
+
.composer .chips { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
|
|
255
|
+
.composer .chip {
|
|
256
|
+
display: inline-flex; align-items: center; gap: 4px; max-width: 100%;
|
|
257
|
+
background: var(--bg-3); border-radius: 6px; padding: 2px 4px 2px 7px; font-size: 11px; color: var(--ink);
|
|
258
|
+
}
|
|
259
|
+
.composer .chip .x { border: 0; background: transparent; color: var(--muted); cursor: pointer; font-size: 13px; line-height: 1; padding: 0 2px; }
|
|
260
|
+
.composer .err { color: var(--pin); font-size: 11px; margin-top: 4px; }
|
|
261
|
+
.composer .err:empty { display: none; }
|
|
233
262
|
|
|
234
263
|
/* ------------------------------------------------------------ sidebar tabs */
|
|
235
264
|
.tabs { display: flex; gap: 4px; flex: none; padding: 8px 10px 0; border-bottom: 1px solid var(--line); }
|
|
@@ -2172,6 +2201,12 @@ var Loupe = (() => {
|
|
|
2172
2201
|
r.readAsDataURL(blob);
|
|
2173
2202
|
});
|
|
2174
2203
|
}
|
|
2204
|
+
function fileToDataUrl(file) {
|
|
2205
|
+
return blobToDataUrl2(file);
|
|
2206
|
+
}
|
|
2207
|
+
function attachmentKind(mime) {
|
|
2208
|
+
return (mime ?? "").startsWith("video/") ? "video" : "image";
|
|
2209
|
+
}
|
|
2175
2210
|
async function captureRegionRecording(rect, opts) {
|
|
2176
2211
|
const md = navigator.mediaDevices;
|
|
2177
2212
|
if (!md?.getDisplayMedia || !window.MediaRecorder) return void 0;
|
|
@@ -2288,6 +2323,20 @@ var Loupe = (() => {
|
|
|
2288
2323
|
this.writeAll(comment.projectKey, comment.url, all);
|
|
2289
2324
|
return comment;
|
|
2290
2325
|
}
|
|
2326
|
+
/**
|
|
2327
|
+
* Offline mode: keep the file inline. localStorage is only a few MB, so refuse
|
|
2328
|
+
* anything that would blow the quota rather than silently dropping it.
|
|
2329
|
+
*/
|
|
2330
|
+
async upload(_projectKey, file) {
|
|
2331
|
+
if (file.size > 3e6) throw new Error("attachment too large for offline mode");
|
|
2332
|
+
return {
|
|
2333
|
+
url: await fileToDataUrl(file),
|
|
2334
|
+
name: file.name,
|
|
2335
|
+
mime: file.type || void 0,
|
|
2336
|
+
kind: attachmentKind(file.type),
|
|
2337
|
+
size: file.size
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2291
2340
|
async update(id, patch) {
|
|
2292
2341
|
for (let i = 0; i < localStorage.length; i++) {
|
|
2293
2342
|
const k = localStorage.key(i);
|
|
@@ -2369,6 +2418,17 @@ var Loupe = (() => {
|
|
|
2369
2418
|
if (!res.ok) throw new Error(`save failed: ${res.status}`);
|
|
2370
2419
|
return await res.json();
|
|
2371
2420
|
}
|
|
2421
|
+
/** Persist one reporter-attached file to object storage and describe it. */
|
|
2422
|
+
async upload(projectKey, file) {
|
|
2423
|
+
const data = await fileToDataUrl(file);
|
|
2424
|
+
return {
|
|
2425
|
+
url: await this.uploadBlob(projectKey, data),
|
|
2426
|
+
name: file.name,
|
|
2427
|
+
mime: file.type || void 0,
|
|
2428
|
+
kind: attachmentKind(file.type),
|
|
2429
|
+
size: file.size
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2372
2432
|
async update(id, patch) {
|
|
2373
2433
|
const res = await fetch(`${this.base}/v1/comments/${encodeURIComponent(id)}`, this.opts({
|
|
2374
2434
|
method: "PATCH",
|
|
@@ -2389,6 +2449,9 @@ var Loupe = (() => {
|
|
|
2389
2449
|
// src/app.ts
|
|
2390
2450
|
var DOCK_MODES = ["left", "right", "bottom", "float"];
|
|
2391
2451
|
var RECORD_MAX_MS = 2e4;
|
|
2452
|
+
var MAX_FILES = 10;
|
|
2453
|
+
var MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
2454
|
+
var MAX_VIDEO_BYTES = 25 * 1024 * 1024;
|
|
2392
2455
|
var uid2 = () => crypto.randomUUID ? crypto.randomUUID() : "c_" + Math.abs(hash(String(performance.now()))).toString(36);
|
|
2393
2456
|
function hash(s) {
|
|
2394
2457
|
let h = 0;
|
|
@@ -2398,6 +2461,10 @@ var Loupe = (() => {
|
|
|
2398
2461
|
var LoupeApp = class {
|
|
2399
2462
|
constructor(cfg) {
|
|
2400
2463
|
this.comments = [];
|
|
2464
|
+
/** Free-text filter over the list (title / body / author). */
|
|
2465
|
+
this.search = "";
|
|
2466
|
+
/** Ids of list items the user expanded — items are collapsed by default. */
|
|
2467
|
+
this.expanded = /* @__PURE__ */ new Set();
|
|
2401
2468
|
/** comment.id → currently resolved element (or null when detached). */
|
|
2402
2469
|
this.resolved = /* @__PURE__ */ new Map();
|
|
2403
2470
|
/** comment.id → pin element. */
|
|
@@ -2754,6 +2821,15 @@ var Loupe = (() => {
|
|
|
2754
2821
|
listHead.append(document.createTextNode("Comments"));
|
|
2755
2822
|
this.countEl = el("span", "count", "0");
|
|
2756
2823
|
listHead.appendChild(this.countEl);
|
|
2824
|
+
const search = el("input", "search");
|
|
2825
|
+
search.type = "search";
|
|
2826
|
+
search.placeholder = "Search\u2026";
|
|
2827
|
+
search.value = this.search;
|
|
2828
|
+
search.oninput = () => {
|
|
2829
|
+
this.search = search.value;
|
|
2830
|
+
this.renderList();
|
|
2831
|
+
};
|
|
2832
|
+
listHead.appendChild(search);
|
|
2757
2833
|
this.listEl = el("div", "list");
|
|
2758
2834
|
const commentsView = el("div", "view comments-view");
|
|
2759
2835
|
commentsView.append(tools, listHead, this.listEl, this.buildIntegrations());
|
|
@@ -2991,8 +3067,53 @@ var Loupe = (() => {
|
|
|
2991
3067
|
"target",
|
|
2992
3068
|
target.kind === "element" ? describe(target.element) : target.kind === "region" ? isRecording ? `\u23FA Recording \xB7 ${Math.round(target.region.w)}\xD7${Math.round(target.region.h)} px` : `Region \xB7 ${Math.round(target.region.w)}\xD7${Math.round(target.region.h)} px` : "Free note \xB7 anywhere on the page"
|
|
2993
3069
|
);
|
|
3070
|
+
const title = el("input", "title");
|
|
3071
|
+
title.type = "text";
|
|
3072
|
+
title.placeholder = "Title \u2014 one line: what's wrong, or what you need";
|
|
2994
3073
|
const ta = el("textarea");
|
|
2995
|
-
ta.placeholder = isRecording ? "
|
|
3074
|
+
ta.placeholder = isRecording ? "Describe the issue in this recording\u2026" : target.kind === "region" ? "Describe the issue in this area\u2026" : target.kind === "free" ? "Describe this note\u2026" : "Describe what should change here\u2026";
|
|
3075
|
+
const files = [];
|
|
3076
|
+
const attach = el("div", "attach");
|
|
3077
|
+
const pick = el("button", "pick", "\uFF0B Attach images / videos");
|
|
3078
|
+
const input = document.createElement("input");
|
|
3079
|
+
input.type = "file";
|
|
3080
|
+
input.accept = "image/*,video/*";
|
|
3081
|
+
input.multiple = true;
|
|
3082
|
+
input.style.display = "none";
|
|
3083
|
+
const chips = el("div", "chips");
|
|
3084
|
+
const err = el("div", "err");
|
|
3085
|
+
const drawChips = () => {
|
|
3086
|
+
chips.innerHTML = "";
|
|
3087
|
+
files.forEach((f, idx) => {
|
|
3088
|
+
const chip = el("span", "chip", `${attachmentKind(f.type) === "video" ? "\u{1F3AC}" : "\u{1F5BC}"} ${f.name}`);
|
|
3089
|
+
const x2 = el("button", "x", "\xD7");
|
|
3090
|
+
x2.onclick = () => {
|
|
3091
|
+
files.splice(idx, 1);
|
|
3092
|
+
drawChips();
|
|
3093
|
+
};
|
|
3094
|
+
chip.appendChild(x2);
|
|
3095
|
+
chips.appendChild(chip);
|
|
3096
|
+
});
|
|
3097
|
+
};
|
|
3098
|
+
input.onchange = () => {
|
|
3099
|
+
err.textContent = "";
|
|
3100
|
+
for (const f of Array.from(input.files ?? [])) {
|
|
3101
|
+
if (files.length >= MAX_FILES) {
|
|
3102
|
+
err.textContent = `Up to ${MAX_FILES} files.`;
|
|
3103
|
+
break;
|
|
3104
|
+
}
|
|
3105
|
+
const cap = attachmentKind(f.type) === "video" ? MAX_VIDEO_BYTES : MAX_IMAGE_BYTES;
|
|
3106
|
+
if (f.size > cap) {
|
|
3107
|
+
err.textContent = `${f.name} is too large.`;
|
|
3108
|
+
continue;
|
|
3109
|
+
}
|
|
3110
|
+
files.push(f);
|
|
3111
|
+
}
|
|
3112
|
+
input.value = "";
|
|
3113
|
+
drawChips();
|
|
3114
|
+
};
|
|
3115
|
+
pick.onclick = () => input.click();
|
|
3116
|
+
attach.append(pick, input, chips, err);
|
|
2996
3117
|
const row = el("div", "row");
|
|
2997
3118
|
let box = null;
|
|
2998
3119
|
if (target.kind !== "free" && !isRecording) {
|
|
@@ -3010,14 +3131,17 @@ var Loupe = (() => {
|
|
|
3010
3131
|
cancel.onclick = () => this.closeComposer();
|
|
3011
3132
|
const save = el("button", "primary", "Comment");
|
|
3012
3133
|
save.disabled = true;
|
|
3013
|
-
|
|
3014
|
-
save.disabled = !ta.value.trim();
|
|
3134
|
+
const sync = () => {
|
|
3135
|
+
save.disabled = !title.value.trim() || !ta.value.trim();
|
|
3015
3136
|
};
|
|
3016
|
-
|
|
3137
|
+
title.oninput = sync;
|
|
3138
|
+
ta.oninput = sync;
|
|
3139
|
+
sync();
|
|
3140
|
+
save.onclick = () => this.submit(target, title.value.trim(), ta.value.trim(), box ? box.checked : false, files.slice());
|
|
3017
3141
|
btns.append(cancel, save);
|
|
3018
3142
|
row.append(btns);
|
|
3019
|
-
c.append(label, ta, row);
|
|
3020
|
-
const w =
|
|
3143
|
+
c.append(label, title, ta, attach, row);
|
|
3144
|
+
const w = 320, h = 340;
|
|
3021
3145
|
const left = Math.min(Math.max(8, x + 12), window.innerWidth - w - 8);
|
|
3022
3146
|
const top = Math.min(Math.max(8, y + 12), window.innerHeight - h - 8);
|
|
3023
3147
|
Object.assign(c.style, { display: "block", left: left + "px", top: top + "px" });
|
|
@@ -3028,8 +3152,8 @@ var Loupe = (() => {
|
|
|
3028
3152
|
this.pending = null;
|
|
3029
3153
|
this.pendingShot = void 0;
|
|
3030
3154
|
}
|
|
3031
|
-
async submit(target, body, withShot) {
|
|
3032
|
-
if (!body) return;
|
|
3155
|
+
async submit(target, title, body, withShot, files) {
|
|
3156
|
+
if (!title || !body) return;
|
|
3033
3157
|
const saveBtn = this.composer.querySelector(".primary");
|
|
3034
3158
|
if (saveBtn) {
|
|
3035
3159
|
saveBtn.disabled = true;
|
|
@@ -3071,6 +3195,7 @@ var Loupe = (() => {
|
|
|
3071
3195
|
projectKey: this.cfg.projectKey,
|
|
3072
3196
|
url: this.url,
|
|
3073
3197
|
author: this.cfg.user,
|
|
3198
|
+
title,
|
|
3074
3199
|
body,
|
|
3075
3200
|
status: "open",
|
|
3076
3201
|
kind: target.kind,
|
|
@@ -3080,6 +3205,7 @@ var Loupe = (() => {
|
|
|
3080
3205
|
region,
|
|
3081
3206
|
screenshot,
|
|
3082
3207
|
recording,
|
|
3208
|
+
attachments: await this.uploadAttachments(files),
|
|
3083
3209
|
// Record the screen the feedback was captured on (desktop / tablet / mobile).
|
|
3084
3210
|
viewport: { w: window.innerWidth, h: window.innerHeight },
|
|
3085
3211
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -3092,6 +3218,19 @@ var Loupe = (() => {
|
|
|
3092
3218
|
this.renderList();
|
|
3093
3219
|
this.flash(comment.id);
|
|
3094
3220
|
}
|
|
3221
|
+
/** Upload the reporter's picked files. A file that fails is skipped, not fatal. */
|
|
3222
|
+
async uploadAttachments(files) {
|
|
3223
|
+
if (!files.length) return void 0;
|
|
3224
|
+
const out = [];
|
|
3225
|
+
for (const f of files) {
|
|
3226
|
+
try {
|
|
3227
|
+
out.push(await this.store.upload(this.cfg.projectKey, f));
|
|
3228
|
+
} catch (e) {
|
|
3229
|
+
console.warn("[loupe] attachment upload failed", f.name, e);
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
return out.length ? out : void 0;
|
|
3233
|
+
}
|
|
3095
3234
|
// ---- pins + re-anchoring --------------------------------------------------
|
|
3096
3235
|
renderPins() {
|
|
3097
3236
|
for (const [id, pin] of this.pins) {
|
|
@@ -3274,19 +3413,22 @@ var Loupe = (() => {
|
|
|
3274
3413
|
// ---- comment list ---------------------------------------------------------
|
|
3275
3414
|
renderList() {
|
|
3276
3415
|
this.listEl.innerHTML = "";
|
|
3277
|
-
|
|
3416
|
+
const q = this.search.trim().toLowerCase();
|
|
3417
|
+
const items = q ? this.comments.filter((c) => `${c.title ?? ""} ${c.body} ${c.author?.name ?? ""}`.toLowerCase().includes(q)) : this.comments;
|
|
3418
|
+
if (!items.length) {
|
|
3278
3419
|
this.listEl.appendChild(el(
|
|
3279
3420
|
"div",
|
|
3280
3421
|
"empty",
|
|
3281
|
-
"No comments yet. Use Inspect to pick an element, or Note to drop a comment anywhere on the page."
|
|
3422
|
+
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."
|
|
3282
3423
|
));
|
|
3283
3424
|
}
|
|
3284
|
-
|
|
3425
|
+
items.forEach((c, i) => this.listEl.appendChild(this.itemView(c, i)));
|
|
3285
3426
|
this.updateCount();
|
|
3286
3427
|
}
|
|
3287
3428
|
itemView(c, i) {
|
|
3288
3429
|
const detached = this.pins.get(c.id)?.classList.contains("detached") && c.status !== "done";
|
|
3289
|
-
const
|
|
3430
|
+
const open = this.expanded.has(c.id);
|
|
3431
|
+
const item = el("div", "item" + (open ? "" : " collapsed"));
|
|
3290
3432
|
const top = el("div", "top");
|
|
3291
3433
|
const num = el("span", "num" + (c.status === "done" ? " done" : detached ? " detached" : ""), String(i + 1));
|
|
3292
3434
|
top.append(num);
|
|
@@ -3299,20 +3441,38 @@ var Loupe = (() => {
|
|
|
3299
3441
|
}
|
|
3300
3442
|
if (c.status === "done") top.appendChild(el("span", "badge done", "done"));
|
|
3301
3443
|
else if (detached) top.appendChild(el("span", "badge detached", "element moved/removed"));
|
|
3444
|
+
top.appendChild(el("span", "caret", open ? "\u25BE" : "\u25B8"));
|
|
3302
3445
|
item.appendChild(top);
|
|
3303
|
-
|
|
3304
|
-
item.appendChild(el("div", "
|
|
3446
|
+
const summary = c.title || (c.body.split("\n")[0] ?? "").slice(0, 140) || "(no description)";
|
|
3447
|
+
item.appendChild(el("div", "summary", summary));
|
|
3448
|
+
const detail = el("div", "detail");
|
|
3449
|
+
if (c.title || c.body.includes("\n")) detail.appendChild(el("div", "body", c.body));
|
|
3450
|
+
detail.appendChild(el("div", "meta", describeAnchor(c)));
|
|
3305
3451
|
if (c.recording) {
|
|
3306
3452
|
const v = el("video", "shot");
|
|
3307
3453
|
v.src = c.recording;
|
|
3308
3454
|
v.controls = true;
|
|
3309
3455
|
v.playsInline = true;
|
|
3310
3456
|
if (c.screenshot) v.poster = c.screenshot;
|
|
3311
|
-
|
|
3457
|
+
detail.appendChild(v);
|
|
3312
3458
|
} else if (c.screenshot) {
|
|
3313
3459
|
const img = el("img", "shot");
|
|
3314
3460
|
img.src = c.screenshot;
|
|
3315
|
-
|
|
3461
|
+
detail.appendChild(img);
|
|
3462
|
+
}
|
|
3463
|
+
for (const a of c.attachments ?? []) {
|
|
3464
|
+
if (a.kind === "video") {
|
|
3465
|
+
const v = el("video", "shot");
|
|
3466
|
+
v.src = a.url;
|
|
3467
|
+
v.controls = true;
|
|
3468
|
+
v.playsInline = true;
|
|
3469
|
+
detail.appendChild(v);
|
|
3470
|
+
} else {
|
|
3471
|
+
const img = el("img", "shot");
|
|
3472
|
+
img.src = a.url;
|
|
3473
|
+
img.alt = a.name ?? "attachment";
|
|
3474
|
+
detail.appendChild(img);
|
|
3475
|
+
}
|
|
3316
3476
|
}
|
|
3317
3477
|
const actions = el("div", "actions");
|
|
3318
3478
|
const doneBtn = el("button", "", c.status === "done" ? "Reopen" : "Mark done");
|
|
@@ -3324,12 +3484,6 @@ var Loupe = (() => {
|
|
|
3324
3484
|
this.renderPins();
|
|
3325
3485
|
this.renderList();
|
|
3326
3486
|
};
|
|
3327
|
-
const claudeBtn = el("button", "", "Copy for Claude");
|
|
3328
|
-
claudeBtn.onclick = async (e) => {
|
|
3329
|
-
e.stopPropagation();
|
|
3330
|
-
await this.copyForClaude(c);
|
|
3331
|
-
claudeBtn.textContent = "Copied \u2713";
|
|
3332
|
-
};
|
|
3333
3487
|
const del = el("button", "", "Delete");
|
|
3334
3488
|
del.onclick = async (e) => {
|
|
3335
3489
|
e.stopPropagation();
|
|
@@ -3339,44 +3493,16 @@ var Loupe = (() => {
|
|
|
3339
3493
|
this.renderPins();
|
|
3340
3494
|
this.renderList();
|
|
3341
3495
|
};
|
|
3342
|
-
actions.append(doneBtn,
|
|
3343
|
-
|
|
3344
|
-
item.
|
|
3496
|
+
actions.append(doneBtn, del);
|
|
3497
|
+
detail.appendChild(actions);
|
|
3498
|
+
item.appendChild(detail);
|
|
3499
|
+
item.onclick = () => {
|
|
3500
|
+
if (open) this.expanded.delete(c.id);
|
|
3501
|
+
else this.expanded.add(c.id);
|
|
3502
|
+
this.renderList();
|
|
3503
|
+
};
|
|
3345
3504
|
return item;
|
|
3346
3505
|
}
|
|
3347
|
-
async copyForClaude(c) {
|
|
3348
|
-
const lines = c.kind === "free" ? [
|
|
3349
|
-
`# Product feedback from ${c.author.name}`,
|
|
3350
|
-
``,
|
|
3351
|
-
`**Note:** ${c.body}`,
|
|
3352
|
-
`**Page:** ${c.url}`,
|
|
3353
|
-
`**Type:** Free note \u2014 a page-level comment not tied to a specific element.`
|
|
3354
|
-
] : [
|
|
3355
|
-
`# Product feedback from ${c.author.name}`,
|
|
3356
|
-
``,
|
|
3357
|
-
`**Comment:** ${c.body}`,
|
|
3358
|
-
`**Page:** ${c.url}`,
|
|
3359
|
-
`**Element:** \`${c.anchor.cssPath}\``,
|
|
3360
|
-
c.anchor.testid ? `**Stable id:** \`${c.anchor.testid}\`` : ``,
|
|
3361
|
-
c.recording ? `**Screen recording (webm):** ${c.recording}` : ``,
|
|
3362
|
-
``,
|
|
3363
|
-
`## Target element`,
|
|
3364
|
-
"```html",
|
|
3365
|
-
c.context.html,
|
|
3366
|
-
"```",
|
|
3367
|
-
``,
|
|
3368
|
-
`## Computed styles`,
|
|
3369
|
-
"```json",
|
|
3370
|
-
JSON.stringify(c.context.styles, null, 2),
|
|
3371
|
-
"```"
|
|
3372
|
-
];
|
|
3373
|
-
const prompt = lines.filter(Boolean).join("\n");
|
|
3374
|
-
try {
|
|
3375
|
-
await navigator.clipboard.writeText(prompt);
|
|
3376
|
-
} catch {
|
|
3377
|
-
console.log("[loupe] copy failed; prompt:\n" + prompt);
|
|
3378
|
-
}
|
|
3379
|
-
}
|
|
3380
3506
|
flash(id) {
|
|
3381
3507
|
const c = this.comments.find((x) => x.id === id);
|
|
3382
3508
|
const pin = this.pins.get(id);
|