@loupekit/sdk 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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.js
CHANGED
|
@@ -171,6 +171,16 @@ var STYLES = (
|
|
|
171
171
|
.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); }
|
|
172
172
|
.item .actions button:hover { border-color: var(--accent); }
|
|
173
173
|
.item img.shot { width: 100%; border-radius: 6px; margin-top: 8px; border: 1px solid var(--line); }
|
|
174
|
+
.item video.shot { width: 100%; border-radius: 6px; margin-top: 8px; border: 1px solid var(--line); }
|
|
175
|
+
.item .caret { margin-left: auto; color: var(--muted); font-size: 11px; }
|
|
176
|
+
.item .summary { font-size: 13px; font-weight: 600; line-height: 1.35; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
177
|
+
.item .detail { margin-top: 6px; }
|
|
178
|
+
.item.collapsed .detail { display: none; }
|
|
179
|
+
.listhead .search {
|
|
180
|
+
margin-left: auto; flex: 1; max-width: 170px; font-size: 12px; text-transform: none; letter-spacing: 0;
|
|
181
|
+
padding: 4px 8px; border: 1px solid var(--line); border-radius: 7px; background: var(--bg-2); color: var(--ink); outline: none;
|
|
182
|
+
}
|
|
183
|
+
.listhead .search:focus { border-color: var(--accent); }
|
|
174
184
|
|
|
175
185
|
/* float resize grip (bottom-right corner) */
|
|
176
186
|
.resize { display: none; position: absolute; right: 0; bottom: 0; width: 16px; height: 16px; cursor: nwse-resize; z-index: 1; }
|
|
@@ -182,7 +192,7 @@ var STYLES = (
|
|
|
182
192
|
|
|
183
193
|
/* composer popover */
|
|
184
194
|
.composer {
|
|
185
|
-
position: fixed; z-index: 2147483004; pointer-events: auto; width:
|
|
195
|
+
position: fixed; z-index: 2147483004; pointer-events: auto; width: 320px;
|
|
186
196
|
background: var(--bg); color: var(--ink); border: 1px solid var(--line);
|
|
187
197
|
border-radius: 12px; box-shadow: var(--shadow); padding: 12px; display: none;
|
|
188
198
|
}
|
|
@@ -203,6 +213,25 @@ var STYLES = (
|
|
|
203
213
|
.composer .primary { background: var(--accent); color: #fff; }
|
|
204
214
|
.composer .primary:disabled { opacity: .5; cursor: default; }
|
|
205
215
|
.composer .ghost { background: var(--bg-3); color: var(--ink); }
|
|
216
|
+
.composer input.title {
|
|
217
|
+
width: 100%; border: 1px solid var(--line); border-radius: 8px; padding: 8px; margin-bottom: 8px;
|
|
218
|
+
font-size: 13px; font-weight: 600; color: var(--ink); background: var(--bg-2); outline: none;
|
|
219
|
+
}
|
|
220
|
+
.composer input.title:focus { border-color: var(--accent); }
|
|
221
|
+
.composer .attach { margin-top: 8px; }
|
|
222
|
+
.composer .pick {
|
|
223
|
+
width: 100%; border: 1px dashed var(--line); background: transparent; color: var(--muted);
|
|
224
|
+
border-radius: 8px; padding: 7px; font-size: 12px; font-weight: 600; cursor: pointer;
|
|
225
|
+
}
|
|
226
|
+
.composer .pick:hover { border-color: var(--accent); color: var(--accent); }
|
|
227
|
+
.composer .chips { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
|
|
228
|
+
.composer .chip {
|
|
229
|
+
display: inline-flex; align-items: center; gap: 4px; max-width: 100%;
|
|
230
|
+
background: var(--bg-3); border-radius: 6px; padding: 2px 4px 2px 7px; font-size: 11px; color: var(--ink);
|
|
231
|
+
}
|
|
232
|
+
.composer .chip .x { border: 0; background: transparent; color: var(--muted); cursor: pointer; font-size: 13px; line-height: 1; padding: 0 2px; }
|
|
233
|
+
.composer .err { color: var(--pin); font-size: 11px; margin-top: 4px; }
|
|
234
|
+
.composer .err:empty { display: none; }
|
|
206
235
|
|
|
207
236
|
/* ------------------------------------------------------------ sidebar tabs */
|
|
208
237
|
.tabs { display: flex; gap: 4px; flex: none; padding: 8px 10px 0; border-bottom: 1px solid var(--line); }
|
|
@@ -2145,6 +2174,12 @@ function blobToDataUrl2(blob) {
|
|
|
2145
2174
|
r.readAsDataURL(blob);
|
|
2146
2175
|
});
|
|
2147
2176
|
}
|
|
2177
|
+
function fileToDataUrl(file) {
|
|
2178
|
+
return blobToDataUrl2(file);
|
|
2179
|
+
}
|
|
2180
|
+
function attachmentKind(mime) {
|
|
2181
|
+
return (mime ?? "").startsWith("video/") ? "video" : "image";
|
|
2182
|
+
}
|
|
2148
2183
|
async function captureRegionRecording(rect, opts) {
|
|
2149
2184
|
const md = navigator.mediaDevices;
|
|
2150
2185
|
if (!md?.getDisplayMedia || !window.MediaRecorder) return void 0;
|
|
@@ -2261,6 +2296,20 @@ var LocalStorageAdapter = class {
|
|
|
2261
2296
|
this.writeAll(comment.projectKey, comment.url, all);
|
|
2262
2297
|
return comment;
|
|
2263
2298
|
}
|
|
2299
|
+
/**
|
|
2300
|
+
* Offline mode: keep the file inline. localStorage is only a few MB, so refuse
|
|
2301
|
+
* anything that would blow the quota rather than silently dropping it.
|
|
2302
|
+
*/
|
|
2303
|
+
async upload(_projectKey, file) {
|
|
2304
|
+
if (file.size > 3e6) throw new Error("attachment too large for offline mode");
|
|
2305
|
+
return {
|
|
2306
|
+
url: await fileToDataUrl(file),
|
|
2307
|
+
name: file.name,
|
|
2308
|
+
mime: file.type || void 0,
|
|
2309
|
+
kind: attachmentKind(file.type),
|
|
2310
|
+
size: file.size
|
|
2311
|
+
};
|
|
2312
|
+
}
|
|
2264
2313
|
async update(id, patch) {
|
|
2265
2314
|
for (let i = 0; i < localStorage.length; i++) {
|
|
2266
2315
|
const k = localStorage.key(i);
|
|
@@ -2342,6 +2391,17 @@ var HttpAdapter = class {
|
|
|
2342
2391
|
if (!res.ok) throw new Error(`save failed: ${res.status}`);
|
|
2343
2392
|
return await res.json();
|
|
2344
2393
|
}
|
|
2394
|
+
/** Persist one reporter-attached file to object storage and describe it. */
|
|
2395
|
+
async upload(projectKey, file) {
|
|
2396
|
+
const data = await fileToDataUrl(file);
|
|
2397
|
+
return {
|
|
2398
|
+
url: await this.uploadBlob(projectKey, data),
|
|
2399
|
+
name: file.name,
|
|
2400
|
+
mime: file.type || void 0,
|
|
2401
|
+
kind: attachmentKind(file.type),
|
|
2402
|
+
size: file.size
|
|
2403
|
+
};
|
|
2404
|
+
}
|
|
2345
2405
|
async update(id, patch) {
|
|
2346
2406
|
const res = await fetch(`${this.base}/v1/comments/${encodeURIComponent(id)}`, this.opts({
|
|
2347
2407
|
method: "PATCH",
|
|
@@ -2362,6 +2422,9 @@ var HttpAdapter = class {
|
|
|
2362
2422
|
// src/app.ts
|
|
2363
2423
|
var DOCK_MODES = ["left", "right", "bottom", "float"];
|
|
2364
2424
|
var RECORD_MAX_MS = 2e4;
|
|
2425
|
+
var MAX_FILES = 10;
|
|
2426
|
+
var MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
2427
|
+
var MAX_VIDEO_BYTES = 25 * 1024 * 1024;
|
|
2365
2428
|
var uid2 = () => crypto.randomUUID ? crypto.randomUUID() : "c_" + Math.abs(hash(String(performance.now()))).toString(36);
|
|
2366
2429
|
function hash(s) {
|
|
2367
2430
|
let h = 0;
|
|
@@ -2371,6 +2434,10 @@ function hash(s) {
|
|
|
2371
2434
|
var LoupeApp = class {
|
|
2372
2435
|
constructor(cfg) {
|
|
2373
2436
|
this.comments = [];
|
|
2437
|
+
/** Free-text filter over the list (title / body / author). */
|
|
2438
|
+
this.search = "";
|
|
2439
|
+
/** Ids of list items the user expanded — items are collapsed by default. */
|
|
2440
|
+
this.expanded = /* @__PURE__ */ new Set();
|
|
2374
2441
|
/** comment.id → currently resolved element (or null when detached). */
|
|
2375
2442
|
this.resolved = /* @__PURE__ */ new Map();
|
|
2376
2443
|
/** comment.id → pin element. */
|
|
@@ -2727,6 +2794,15 @@ var LoupeApp = class {
|
|
|
2727
2794
|
listHead.append(document.createTextNode("Comments"));
|
|
2728
2795
|
this.countEl = el("span", "count", "0");
|
|
2729
2796
|
listHead.appendChild(this.countEl);
|
|
2797
|
+
const search = el("input", "search");
|
|
2798
|
+
search.type = "search";
|
|
2799
|
+
search.placeholder = "Search\u2026";
|
|
2800
|
+
search.value = this.search;
|
|
2801
|
+
search.oninput = () => {
|
|
2802
|
+
this.search = search.value;
|
|
2803
|
+
this.renderList();
|
|
2804
|
+
};
|
|
2805
|
+
listHead.appendChild(search);
|
|
2730
2806
|
this.listEl = el("div", "list");
|
|
2731
2807
|
const commentsView = el("div", "view comments-view");
|
|
2732
2808
|
commentsView.append(tools, listHead, this.listEl, this.buildIntegrations());
|
|
@@ -2964,8 +3040,53 @@ var LoupeApp = class {
|
|
|
2964
3040
|
"target",
|
|
2965
3041
|
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"
|
|
2966
3042
|
);
|
|
3043
|
+
const title = el("input", "title");
|
|
3044
|
+
title.type = "text";
|
|
3045
|
+
title.placeholder = "Title \u2014 one line: what's wrong, or what you need";
|
|
2967
3046
|
const ta = el("textarea");
|
|
2968
|
-
ta.placeholder = isRecording ? "
|
|
3047
|
+
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";
|
|
3048
|
+
const files = [];
|
|
3049
|
+
const attach = el("div", "attach");
|
|
3050
|
+
const pick = el("button", "pick", "\uFF0B Attach images / videos");
|
|
3051
|
+
const input = document.createElement("input");
|
|
3052
|
+
input.type = "file";
|
|
3053
|
+
input.accept = "image/*,video/*";
|
|
3054
|
+
input.multiple = true;
|
|
3055
|
+
input.style.display = "none";
|
|
3056
|
+
const chips = el("div", "chips");
|
|
3057
|
+
const err = el("div", "err");
|
|
3058
|
+
const drawChips = () => {
|
|
3059
|
+
chips.innerHTML = "";
|
|
3060
|
+
files.forEach((f, idx) => {
|
|
3061
|
+
const chip = el("span", "chip", `${attachmentKind(f.type) === "video" ? "\u{1F3AC}" : "\u{1F5BC}"} ${f.name}`);
|
|
3062
|
+
const x2 = el("button", "x", "\xD7");
|
|
3063
|
+
x2.onclick = () => {
|
|
3064
|
+
files.splice(idx, 1);
|
|
3065
|
+
drawChips();
|
|
3066
|
+
};
|
|
3067
|
+
chip.appendChild(x2);
|
|
3068
|
+
chips.appendChild(chip);
|
|
3069
|
+
});
|
|
3070
|
+
};
|
|
3071
|
+
input.onchange = () => {
|
|
3072
|
+
err.textContent = "";
|
|
3073
|
+
for (const f of Array.from(input.files ?? [])) {
|
|
3074
|
+
if (files.length >= MAX_FILES) {
|
|
3075
|
+
err.textContent = `Up to ${MAX_FILES} files.`;
|
|
3076
|
+
break;
|
|
3077
|
+
}
|
|
3078
|
+
const cap = attachmentKind(f.type) === "video" ? MAX_VIDEO_BYTES : MAX_IMAGE_BYTES;
|
|
3079
|
+
if (f.size > cap) {
|
|
3080
|
+
err.textContent = `${f.name} is too large.`;
|
|
3081
|
+
continue;
|
|
3082
|
+
}
|
|
3083
|
+
files.push(f);
|
|
3084
|
+
}
|
|
3085
|
+
input.value = "";
|
|
3086
|
+
drawChips();
|
|
3087
|
+
};
|
|
3088
|
+
pick.onclick = () => input.click();
|
|
3089
|
+
attach.append(pick, input, chips, err);
|
|
2969
3090
|
const row = el("div", "row");
|
|
2970
3091
|
let box = null;
|
|
2971
3092
|
if (target.kind !== "free" && !isRecording) {
|
|
@@ -2983,14 +3104,17 @@ var LoupeApp = class {
|
|
|
2983
3104
|
cancel.onclick = () => this.closeComposer();
|
|
2984
3105
|
const save = el("button", "primary", "Comment");
|
|
2985
3106
|
save.disabled = true;
|
|
2986
|
-
|
|
2987
|
-
save.disabled = !ta.value.trim();
|
|
3107
|
+
const sync = () => {
|
|
3108
|
+
save.disabled = !title.value.trim() || !ta.value.trim();
|
|
2988
3109
|
};
|
|
2989
|
-
|
|
3110
|
+
title.oninput = sync;
|
|
3111
|
+
ta.oninput = sync;
|
|
3112
|
+
sync();
|
|
3113
|
+
save.onclick = () => this.submit(target, title.value.trim(), ta.value.trim(), box ? box.checked : false, files.slice());
|
|
2990
3114
|
btns.append(cancel, save);
|
|
2991
3115
|
row.append(btns);
|
|
2992
|
-
c.append(label, ta, row);
|
|
2993
|
-
const w =
|
|
3116
|
+
c.append(label, title, ta, attach, row);
|
|
3117
|
+
const w = 320, h = 340;
|
|
2994
3118
|
const left = Math.min(Math.max(8, x + 12), window.innerWidth - w - 8);
|
|
2995
3119
|
const top = Math.min(Math.max(8, y + 12), window.innerHeight - h - 8);
|
|
2996
3120
|
Object.assign(c.style, { display: "block", left: left + "px", top: top + "px" });
|
|
@@ -3001,8 +3125,8 @@ var LoupeApp = class {
|
|
|
3001
3125
|
this.pending = null;
|
|
3002
3126
|
this.pendingShot = void 0;
|
|
3003
3127
|
}
|
|
3004
|
-
async submit(target, body, withShot) {
|
|
3005
|
-
if (!body) return;
|
|
3128
|
+
async submit(target, title, body, withShot, files) {
|
|
3129
|
+
if (!title || !body) return;
|
|
3006
3130
|
const saveBtn = this.composer.querySelector(".primary");
|
|
3007
3131
|
if (saveBtn) {
|
|
3008
3132
|
saveBtn.disabled = true;
|
|
@@ -3044,6 +3168,7 @@ var LoupeApp = class {
|
|
|
3044
3168
|
projectKey: this.cfg.projectKey,
|
|
3045
3169
|
url: this.url,
|
|
3046
3170
|
author: this.cfg.user,
|
|
3171
|
+
title,
|
|
3047
3172
|
body,
|
|
3048
3173
|
status: "open",
|
|
3049
3174
|
kind: target.kind,
|
|
@@ -3053,6 +3178,7 @@ var LoupeApp = class {
|
|
|
3053
3178
|
region,
|
|
3054
3179
|
screenshot,
|
|
3055
3180
|
recording,
|
|
3181
|
+
attachments: await this.uploadAttachments(files),
|
|
3056
3182
|
// Record the screen the feedback was captured on (desktop / tablet / mobile).
|
|
3057
3183
|
viewport: { w: window.innerWidth, h: window.innerHeight },
|
|
3058
3184
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -3065,6 +3191,19 @@ var LoupeApp = class {
|
|
|
3065
3191
|
this.renderList();
|
|
3066
3192
|
this.flash(comment.id);
|
|
3067
3193
|
}
|
|
3194
|
+
/** Upload the reporter's picked files. A file that fails is skipped, not fatal. */
|
|
3195
|
+
async uploadAttachments(files) {
|
|
3196
|
+
if (!files.length) return void 0;
|
|
3197
|
+
const out = [];
|
|
3198
|
+
for (const f of files) {
|
|
3199
|
+
try {
|
|
3200
|
+
out.push(await this.store.upload(this.cfg.projectKey, f));
|
|
3201
|
+
} catch (e) {
|
|
3202
|
+
console.warn("[loupe] attachment upload failed", f.name, e);
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
return out.length ? out : void 0;
|
|
3206
|
+
}
|
|
3068
3207
|
// ---- pins + re-anchoring --------------------------------------------------
|
|
3069
3208
|
renderPins() {
|
|
3070
3209
|
for (const [id, pin] of this.pins) {
|
|
@@ -3247,19 +3386,22 @@ var LoupeApp = class {
|
|
|
3247
3386
|
// ---- comment list ---------------------------------------------------------
|
|
3248
3387
|
renderList() {
|
|
3249
3388
|
this.listEl.innerHTML = "";
|
|
3250
|
-
|
|
3389
|
+
const q = this.search.trim().toLowerCase();
|
|
3390
|
+
const items = q ? this.comments.filter((c) => `${c.title ?? ""} ${c.body} ${c.author?.name ?? ""}`.toLowerCase().includes(q)) : this.comments;
|
|
3391
|
+
if (!items.length) {
|
|
3251
3392
|
this.listEl.appendChild(el(
|
|
3252
3393
|
"div",
|
|
3253
3394
|
"empty",
|
|
3254
|
-
"No comments yet. Use Inspect to pick an element, or Note to drop a comment anywhere on the page."
|
|
3395
|
+
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."
|
|
3255
3396
|
));
|
|
3256
3397
|
}
|
|
3257
|
-
|
|
3398
|
+
items.forEach((c, i) => this.listEl.appendChild(this.itemView(c, i)));
|
|
3258
3399
|
this.updateCount();
|
|
3259
3400
|
}
|
|
3260
3401
|
itemView(c, i) {
|
|
3261
3402
|
const detached = this.pins.get(c.id)?.classList.contains("detached") && c.status !== "done";
|
|
3262
|
-
const
|
|
3403
|
+
const open = this.expanded.has(c.id);
|
|
3404
|
+
const item = el("div", "item" + (open ? "" : " collapsed"));
|
|
3263
3405
|
const top = el("div", "top");
|
|
3264
3406
|
const num = el("span", "num" + (c.status === "done" ? " done" : detached ? " detached" : ""), String(i + 1));
|
|
3265
3407
|
top.append(num);
|
|
@@ -3272,20 +3414,38 @@ var LoupeApp = class {
|
|
|
3272
3414
|
}
|
|
3273
3415
|
if (c.status === "done") top.appendChild(el("span", "badge done", "done"));
|
|
3274
3416
|
else if (detached) top.appendChild(el("span", "badge detached", "element moved/removed"));
|
|
3417
|
+
top.appendChild(el("span", "caret", open ? "\u25BE" : "\u25B8"));
|
|
3275
3418
|
item.appendChild(top);
|
|
3276
|
-
|
|
3277
|
-
item.appendChild(el("div", "
|
|
3419
|
+
const summary = c.title || (c.body.split("\n")[0] ?? "").slice(0, 140) || "(no description)";
|
|
3420
|
+
item.appendChild(el("div", "summary", summary));
|
|
3421
|
+
const detail = el("div", "detail");
|
|
3422
|
+
if (c.title || c.body.includes("\n")) detail.appendChild(el("div", "body", c.body));
|
|
3423
|
+
detail.appendChild(el("div", "meta", describeAnchor(c)));
|
|
3278
3424
|
if (c.recording) {
|
|
3279
3425
|
const v = el("video", "shot");
|
|
3280
3426
|
v.src = c.recording;
|
|
3281
3427
|
v.controls = true;
|
|
3282
3428
|
v.playsInline = true;
|
|
3283
3429
|
if (c.screenshot) v.poster = c.screenshot;
|
|
3284
|
-
|
|
3430
|
+
detail.appendChild(v);
|
|
3285
3431
|
} else if (c.screenshot) {
|
|
3286
3432
|
const img = el("img", "shot");
|
|
3287
3433
|
img.src = c.screenshot;
|
|
3288
|
-
|
|
3434
|
+
detail.appendChild(img);
|
|
3435
|
+
}
|
|
3436
|
+
for (const a of c.attachments ?? []) {
|
|
3437
|
+
if (a.kind === "video") {
|
|
3438
|
+
const v = el("video", "shot");
|
|
3439
|
+
v.src = a.url;
|
|
3440
|
+
v.controls = true;
|
|
3441
|
+
v.playsInline = true;
|
|
3442
|
+
detail.appendChild(v);
|
|
3443
|
+
} else {
|
|
3444
|
+
const img = el("img", "shot");
|
|
3445
|
+
img.src = a.url;
|
|
3446
|
+
img.alt = a.name ?? "attachment";
|
|
3447
|
+
detail.appendChild(img);
|
|
3448
|
+
}
|
|
3289
3449
|
}
|
|
3290
3450
|
const actions = el("div", "actions");
|
|
3291
3451
|
const doneBtn = el("button", "", c.status === "done" ? "Reopen" : "Mark done");
|
|
@@ -3297,12 +3457,6 @@ var LoupeApp = class {
|
|
|
3297
3457
|
this.renderPins();
|
|
3298
3458
|
this.renderList();
|
|
3299
3459
|
};
|
|
3300
|
-
const claudeBtn = el("button", "", "Copy for Claude");
|
|
3301
|
-
claudeBtn.onclick = async (e) => {
|
|
3302
|
-
e.stopPropagation();
|
|
3303
|
-
await this.copyForClaude(c);
|
|
3304
|
-
claudeBtn.textContent = "Copied \u2713";
|
|
3305
|
-
};
|
|
3306
3460
|
const del = el("button", "", "Delete");
|
|
3307
3461
|
del.onclick = async (e) => {
|
|
3308
3462
|
e.stopPropagation();
|
|
@@ -3312,44 +3466,16 @@ var LoupeApp = class {
|
|
|
3312
3466
|
this.renderPins();
|
|
3313
3467
|
this.renderList();
|
|
3314
3468
|
};
|
|
3315
|
-
actions.append(doneBtn,
|
|
3316
|
-
|
|
3317
|
-
item.
|
|
3469
|
+
actions.append(doneBtn, del);
|
|
3470
|
+
detail.appendChild(actions);
|
|
3471
|
+
item.appendChild(detail);
|
|
3472
|
+
item.onclick = () => {
|
|
3473
|
+
if (open) this.expanded.delete(c.id);
|
|
3474
|
+
else this.expanded.add(c.id);
|
|
3475
|
+
this.renderList();
|
|
3476
|
+
};
|
|
3318
3477
|
return item;
|
|
3319
3478
|
}
|
|
3320
|
-
async copyForClaude(c) {
|
|
3321
|
-
const lines = c.kind === "free" ? [
|
|
3322
|
-
`# Product feedback from ${c.author.name}`,
|
|
3323
|
-
``,
|
|
3324
|
-
`**Note:** ${c.body}`,
|
|
3325
|
-
`**Page:** ${c.url}`,
|
|
3326
|
-
`**Type:** Free note \u2014 a page-level comment not tied to a specific element.`
|
|
3327
|
-
] : [
|
|
3328
|
-
`# Product feedback from ${c.author.name}`,
|
|
3329
|
-
``,
|
|
3330
|
-
`**Comment:** ${c.body}`,
|
|
3331
|
-
`**Page:** ${c.url}`,
|
|
3332
|
-
`**Element:** \`${c.anchor.cssPath}\``,
|
|
3333
|
-
c.anchor.testid ? `**Stable id:** \`${c.anchor.testid}\`` : ``,
|
|
3334
|
-
c.recording ? `**Screen recording (webm):** ${c.recording}` : ``,
|
|
3335
|
-
``,
|
|
3336
|
-
`## Target element`,
|
|
3337
|
-
"```html",
|
|
3338
|
-
c.context.html,
|
|
3339
|
-
"```",
|
|
3340
|
-
``,
|
|
3341
|
-
`## Computed styles`,
|
|
3342
|
-
"```json",
|
|
3343
|
-
JSON.stringify(c.context.styles, null, 2),
|
|
3344
|
-
"```"
|
|
3345
|
-
];
|
|
3346
|
-
const prompt = lines.filter(Boolean).join("\n");
|
|
3347
|
-
try {
|
|
3348
|
-
await navigator.clipboard.writeText(prompt);
|
|
3349
|
-
} catch {
|
|
3350
|
-
console.log("[loupe] copy failed; prompt:\n" + prompt);
|
|
3351
|
-
}
|
|
3352
|
-
}
|
|
3353
3479
|
flash(id) {
|
|
3354
3480
|
const c = this.comments.find((x) => x.id === id);
|
|
3355
3481
|
const pin = this.pins.get(id);
|