@loupekit/sdk 0.4.3 → 0.5.0-next.16
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 -2
- package/dist/index.global.js +262 -16
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +262 -16
- package/dist/index.js.map +1 -1
- package/package.json +26 -7
package/dist/index.js
CHANGED
|
@@ -59,6 +59,8 @@ var STYLES = (
|
|
|
59
59
|
.pin:hover { transform: translate(-4px, -4px) scale(1.12); }
|
|
60
60
|
.pin.detached { background: #9aa0af; }
|
|
61
61
|
.pin.done { background: #10935a; }
|
|
62
|
+
.pin.free { background: var(--accent); border-radius: 50% 50% 2px 50%; }
|
|
63
|
+
.pin.free.done { background: #10935a; }
|
|
62
64
|
.pin.active { outline: 3px solid rgba(74,85,214,.4); }
|
|
63
65
|
|
|
64
66
|
/* toolbar */
|
|
@@ -85,11 +87,24 @@ var STYLES = (
|
|
|
85
87
|
.toolbar button.on { background: var(--accent); color: #fff; }
|
|
86
88
|
.toolbar .sep { width: 1px; height: 20px; background: #333846; margin: 0 2px; }
|
|
87
89
|
.toolbar .brand {
|
|
88
|
-
font-weight: 700; letter-spacing: -.01em; cursor:
|
|
90
|
+
font-weight: 700; letter-spacing: -.01em; cursor: grab; user-select: none; touch-action: none;
|
|
89
91
|
}
|
|
92
|
+
.toolbar.dragging { transition: none; box-shadow: 0 12px 40px rgba(0,0,0,.5); }
|
|
93
|
+
.toolbar.dragging .brand { cursor: grabbing; }
|
|
90
94
|
/* collapsed \u2192 show only the brand/logo; click it again to expand */
|
|
91
95
|
.toolbar.collapsed { gap: 0; }
|
|
92
96
|
.toolbar.collapsed > *:not(.brand) { display: none; }
|
|
97
|
+
|
|
98
|
+
/* free-move: the bar is fixed-positioned by JS and expands *inward* from its
|
|
99
|
+
docked edge \u2014 vertical (a column) against a left/right edge, horizontal
|
|
100
|
+
otherwise \u2014 so it's always fully on-screen. */
|
|
101
|
+
.toolbar.floating { transform: none; }
|
|
102
|
+
.toolbar.orient-h.flow-rev { flex-direction: row-reverse; }
|
|
103
|
+
.toolbar.orient-v { flex-direction: column; align-items: stretch; }
|
|
104
|
+
.toolbar.orient-v.flow-rev { flex-direction: column-reverse; }
|
|
105
|
+
.toolbar.orient-v button, .toolbar.orient-v .brand { justify-content: flex-start; }
|
|
106
|
+
.toolbar.orient-v .sep { width: 22px; height: 1px; margin: 2px auto; }
|
|
107
|
+
.toolbar.orient-v .count { margin-left: auto; }
|
|
93
108
|
.toolbar .count {
|
|
94
109
|
background: var(--pin); color: #fff; font-size: 11px; font-weight: 700;
|
|
95
110
|
border-radius: 999px; padding: 1px 7px; margin-left: 2px;
|
|
@@ -2169,6 +2184,12 @@ var LoupeApp = class {
|
|
|
2169
2184
|
this.dragStart = null;
|
|
2170
2185
|
/** region comment whose outline is currently highlighted (tracks scroll). */
|
|
2171
2186
|
this.activeRegionId = null;
|
|
2187
|
+
/** Free-move toolbar position as viewport fractions, or null → default (bottom-center). */
|
|
2188
|
+
this.barPos = null;
|
|
2189
|
+
/** In-flight drag of the toolbar by its ◎ handle. */
|
|
2190
|
+
this.barDrag = null;
|
|
2191
|
+
/** True right after a drag so the trailing click doesn't also toggle collapse. */
|
|
2192
|
+
this.justDragged = false;
|
|
2172
2193
|
this.raf = 0;
|
|
2173
2194
|
// ---- inspector ------------------------------------------------------------
|
|
2174
2195
|
this.onMove = (e) => {
|
|
@@ -2243,11 +2264,36 @@ var LoupeApp = class {
|
|
|
2243
2264
|
this.setMode("off");
|
|
2244
2265
|
this.finishRegion(vp);
|
|
2245
2266
|
};
|
|
2267
|
+
// ---- free note (drop a comment anywhere, no element / no screenshot) -------
|
|
2268
|
+
this.onFreeClick = (e) => {
|
|
2269
|
+
if (this.mode !== "free") return;
|
|
2270
|
+
const t = e.target;
|
|
2271
|
+
if (t && (t.id === "loupe-root" || t.closest?.("#loupe-root"))) return;
|
|
2272
|
+
e.preventDefault();
|
|
2273
|
+
e.stopPropagation();
|
|
2274
|
+
const docX = e.clientX + window.scrollX;
|
|
2275
|
+
const docY = e.clientY + window.scrollY;
|
|
2276
|
+
const docW = Math.max(1, document.documentElement.scrollWidth);
|
|
2277
|
+
const docH = Math.max(1, document.documentElement.scrollHeight);
|
|
2278
|
+
const offset = { x: clamp(docX / docW), y: clamp(docY / docH) };
|
|
2279
|
+
this.setMode("off");
|
|
2280
|
+
this.openComposer({ kind: "free", offset, point: { x: docX, y: docY } }, e.clientX, e.clientY);
|
|
2281
|
+
};
|
|
2246
2282
|
/** Reposition every pin, re-resolving anchors whose element has gone. */
|
|
2247
2283
|
this.position = () => {
|
|
2248
2284
|
for (const c of this.comments) {
|
|
2249
2285
|
const pin = this.pins.get(c.id);
|
|
2250
2286
|
if (!pin) continue;
|
|
2287
|
+
if (c.kind === "free") {
|
|
2288
|
+
const docW = Math.max(1, document.documentElement.scrollWidth);
|
|
2289
|
+
const docH = Math.max(1, document.documentElement.scrollHeight);
|
|
2290
|
+
const px = c.offset.x * docW - window.scrollX;
|
|
2291
|
+
const py = c.offset.y * docH - window.scrollY;
|
|
2292
|
+
const onScreen = px > -24 && px < window.innerWidth + 24 && py > -24 && py < window.innerHeight + 24;
|
|
2293
|
+
Object.assign(pin.style, { left: px + "px", top: py + "px", display: onScreen ? "grid" : "none" });
|
|
2294
|
+
pin.classList.remove("detached");
|
|
2295
|
+
continue;
|
|
2296
|
+
}
|
|
2251
2297
|
let elx = this.resolved.get(c.id) ?? null;
|
|
2252
2298
|
if (!elx || !elx.isConnected) {
|
|
2253
2299
|
const r = resolveAnchor(c.anchor);
|
|
@@ -2292,6 +2338,58 @@ var LoupeApp = class {
|
|
|
2292
2338
|
}
|
|
2293
2339
|
}
|
|
2294
2340
|
};
|
|
2341
|
+
// ---- draggable, edge-aware floating toolbar --------------------------------
|
|
2342
|
+
this.onResizeBar = () => this.layoutBar();
|
|
2343
|
+
this.onBarPointerDown = (e) => {
|
|
2344
|
+
if (typeof e.button === "number" && e.button !== 0) return;
|
|
2345
|
+
const brand = this.brandEl();
|
|
2346
|
+
const r = brand.getBoundingClientRect();
|
|
2347
|
+
this.barDrag = { px: e.clientX, py: e.clientY, ix: r.left, iy: r.top };
|
|
2348
|
+
this.justDragged = false;
|
|
2349
|
+
try {
|
|
2350
|
+
brand.setPointerCapture(e.pointerId);
|
|
2351
|
+
} catch {
|
|
2352
|
+
}
|
|
2353
|
+
brand.addEventListener("pointermove", this.onBarPointerMove);
|
|
2354
|
+
brand.addEventListener("pointerup", this.onBarPointerUp);
|
|
2355
|
+
brand.addEventListener("pointercancel", this.onBarPointerUp);
|
|
2356
|
+
};
|
|
2357
|
+
this.onBarPointerMove = (e) => {
|
|
2358
|
+
if (!this.barDrag) return;
|
|
2359
|
+
const dx = e.clientX - this.barDrag.px;
|
|
2360
|
+
const dy = e.clientY - this.barDrag.py;
|
|
2361
|
+
if (!this.justDragged && Math.hypot(dx, dy) < 5) return;
|
|
2362
|
+
this.justDragged = true;
|
|
2363
|
+
e.preventDefault();
|
|
2364
|
+
const vw = window.innerWidth, vh = window.innerHeight;
|
|
2365
|
+
const bar = this.toolbar;
|
|
2366
|
+
const brand = this.brandEl();
|
|
2367
|
+
const iw = brand.offsetWidth || 44, ih = brand.offsetHeight || 44;
|
|
2368
|
+
const ix = clampPx(this.barDrag.ix + dx, 6, vw - iw - 6);
|
|
2369
|
+
const iy = clampPx(this.barDrag.iy + dy, 6, vh - ih - 6);
|
|
2370
|
+
bar.classList.add("floating", "dragging");
|
|
2371
|
+
bar.classList.remove("orient-v", "flow-rev");
|
|
2372
|
+
bar.classList.add("orient-h");
|
|
2373
|
+
Object.assign(bar.style, { transform: "none", left: ix + "px", top: iy + "px", right: "auto", bottom: "auto" });
|
|
2374
|
+
this.barPos = { fx: ix / vw, fy: iy / vh };
|
|
2375
|
+
};
|
|
2376
|
+
this.onBarPointerUp = (e) => {
|
|
2377
|
+
const brand = this.brandEl();
|
|
2378
|
+
brand.removeEventListener("pointermove", this.onBarPointerMove);
|
|
2379
|
+
brand.removeEventListener("pointerup", this.onBarPointerUp);
|
|
2380
|
+
brand.removeEventListener("pointercancel", this.onBarPointerUp);
|
|
2381
|
+
try {
|
|
2382
|
+
brand.releasePointerCapture(e.pointerId);
|
|
2383
|
+
} catch {
|
|
2384
|
+
}
|
|
2385
|
+
const dragged = this.justDragged;
|
|
2386
|
+
this.barDrag = null;
|
|
2387
|
+
this.toolbar.classList.remove("dragging");
|
|
2388
|
+
if (dragged) {
|
|
2389
|
+
this.saveBarPos();
|
|
2390
|
+
this.layoutBar();
|
|
2391
|
+
}
|
|
2392
|
+
};
|
|
2295
2393
|
this.cfg = cfg;
|
|
2296
2394
|
this.store = cfg.apiBase ? new HttpAdapter(cfg.apiBase, cfg.user, cfg.userHmac, cfg.headers, cfg.credentials) : new LocalStorageAdapter();
|
|
2297
2395
|
}
|
|
@@ -2299,7 +2397,9 @@ var LoupeApp = class {
|
|
|
2299
2397
|
return location.pathname + location.search;
|
|
2300
2398
|
}
|
|
2301
2399
|
async start() {
|
|
2400
|
+
this.loadBarPos();
|
|
2302
2401
|
this.buildDom();
|
|
2402
|
+
this.layoutBar();
|
|
2303
2403
|
this.lastUrl = this.url;
|
|
2304
2404
|
this.comments = await this.store.list(this.cfg.projectKey, this.url);
|
|
2305
2405
|
this.renderPins();
|
|
@@ -2364,11 +2464,21 @@ var LoupeApp = class {
|
|
|
2364
2464
|
const bar = el("div", "toolbar");
|
|
2365
2465
|
const brand = el("span", "brand");
|
|
2366
2466
|
brand.innerHTML = `<span class="ico">\u25CE</span><span class="label">Loupe</span>`;
|
|
2367
|
-
brand.title = "
|
|
2467
|
+
brand.title = "Drag to move \xB7 click to collapse / expand";
|
|
2368
2468
|
brand.setAttribute("role", "button");
|
|
2369
|
-
brand.onclick = () =>
|
|
2469
|
+
brand.onclick = () => {
|
|
2470
|
+
if (this.justDragged) {
|
|
2471
|
+
this.justDragged = false;
|
|
2472
|
+
return;
|
|
2473
|
+
}
|
|
2474
|
+
this.toggleCollapsed();
|
|
2475
|
+
};
|
|
2476
|
+
brand.addEventListener("pointerdown", this.onBarPointerDown);
|
|
2370
2477
|
const inspectBtn = this.toolBtn("\u271B", "Inspect & comment", "inspect");
|
|
2371
2478
|
inspectBtn.onclick = () => this.setMode(this.mode === "inspect" ? "off" : "inspect");
|
|
2479
|
+
const freeBtn = this.toolBtn(NOTE_ICON, "Note", "free");
|
|
2480
|
+
freeBtn.title = "Drop a note anywhere on the page \u2014 no element, no screenshot";
|
|
2481
|
+
freeBtn.onclick = () => this.setMode(this.mode === "free" ? "off" : "free");
|
|
2372
2482
|
const regionBtn = this.toolBtn(REGION_ICON, "Region shot", "region");
|
|
2373
2483
|
regionBtn.title = "Drag a free-size box, screenshot it, and comment";
|
|
2374
2484
|
regionBtn.onclick = () => this.setMode(this.mode === "region" ? "off" : "region");
|
|
@@ -2376,7 +2486,7 @@ var LoupeApp = class {
|
|
|
2376
2486
|
this.countEl = el("span", "count", "0");
|
|
2377
2487
|
listBtn.appendChild(this.countEl);
|
|
2378
2488
|
listBtn.onclick = () => this.togglePanel();
|
|
2379
|
-
bar.append(brand, sep(), inspectBtn, regionBtn, listBtn);
|
|
2489
|
+
bar.append(brand, sep(), inspectBtn, freeBtn, regionBtn, listBtn);
|
|
2380
2490
|
return bar;
|
|
2381
2491
|
}
|
|
2382
2492
|
/** A toolbar button with a uniform icon + label layout. `icon` may be an SVG string. */
|
|
@@ -2445,9 +2555,11 @@ var LoupeApp = class {
|
|
|
2445
2555
|
this.cancelDrag();
|
|
2446
2556
|
document.body.style.cursor = mode === "off" ? "" : "crosshair";
|
|
2447
2557
|
this.toolbar.querySelector('[data-role="inspect"]')?.classList.toggle("on", mode === "inspect");
|
|
2558
|
+
this.toolbar.querySelector('[data-role="free"]')?.classList.toggle("on", mode === "free");
|
|
2448
2559
|
this.toolbar.querySelector('[data-role="region"]')?.classList.toggle("on", mode === "region");
|
|
2449
2560
|
document.removeEventListener("mousemove", this.onMove, true);
|
|
2450
2561
|
document.removeEventListener("click", this.onClick, true);
|
|
2562
|
+
document.removeEventListener("click", this.onFreeClick, true);
|
|
2451
2563
|
document.removeEventListener("mousedown", this.onRegionDown, true);
|
|
2452
2564
|
if (mode === "off") return;
|
|
2453
2565
|
document.addEventListener("keydown", this.onKey, true);
|
|
@@ -2455,6 +2567,8 @@ var LoupeApp = class {
|
|
|
2455
2567
|
if (mode === "inspect") {
|
|
2456
2568
|
document.addEventListener("mousemove", this.onMove, true);
|
|
2457
2569
|
document.addEventListener("click", this.onClick, true);
|
|
2570
|
+
} else if (mode === "free") {
|
|
2571
|
+
document.addEventListener("click", this.onFreeClick, true);
|
|
2458
2572
|
} else if (mode === "region") {
|
|
2459
2573
|
document.addEventListener("mousedown", this.onRegionDown, true);
|
|
2460
2574
|
}
|
|
@@ -2467,16 +2581,22 @@ var LoupeApp = class {
|
|
|
2467
2581
|
const label = el(
|
|
2468
2582
|
"div",
|
|
2469
2583
|
"target",
|
|
2470
|
-
target.kind === "element" ? describe(target.element) : `Region \xB7 ${Math.round(target.region.w)}\xD7${Math.round(target.region.h)} px`
|
|
2584
|
+
target.kind === "element" ? describe(target.element) : target.kind === "region" ? `Region \xB7 ${Math.round(target.region.w)}\xD7${Math.round(target.region.h)} px` : "Free note \xB7 anywhere on the page"
|
|
2471
2585
|
);
|
|
2472
2586
|
const ta = el("textarea");
|
|
2473
|
-
ta.placeholder = target.kind === "region" ? "What's the issue in this area?" : "What should change here?";
|
|
2587
|
+
ta.placeholder = target.kind === "region" ? "What's the issue in this area?" : target.kind === "free" ? "Leave a note about this page\u2026" : "What should change here?";
|
|
2474
2588
|
const row = el("div", "row");
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2589
|
+
let box = null;
|
|
2590
|
+
if (target.kind !== "free") {
|
|
2591
|
+
const chk = el("label", "chk");
|
|
2592
|
+
box = document.createElement("input");
|
|
2593
|
+
box.type = "checkbox";
|
|
2594
|
+
box.checked = true;
|
|
2595
|
+
chk.append(box, document.createTextNode("Attach screenshot"));
|
|
2596
|
+
row.append(chk);
|
|
2597
|
+
} else {
|
|
2598
|
+
row.style.justifyContent = "flex-end";
|
|
2599
|
+
}
|
|
2480
2600
|
const btns = el("div", "btns");
|
|
2481
2601
|
const cancel = el("button", "ghost", "Cancel");
|
|
2482
2602
|
cancel.onclick = () => this.closeComposer();
|
|
@@ -2485,9 +2605,9 @@ var LoupeApp = class {
|
|
|
2485
2605
|
ta.oninput = () => {
|
|
2486
2606
|
save.disabled = !ta.value.trim();
|
|
2487
2607
|
};
|
|
2488
|
-
save.onclick = () => this.submit(target, ta.value.trim(), box.checked);
|
|
2608
|
+
save.onclick = () => this.submit(target, ta.value.trim(), box ? box.checked : false);
|
|
2489
2609
|
btns.append(cancel, save);
|
|
2490
|
-
row.append(
|
|
2610
|
+
row.append(btns);
|
|
2491
2611
|
c.append(label, ta, row);
|
|
2492
2612
|
const w = 300, h = 190;
|
|
2493
2613
|
const left = Math.min(Math.max(8, x + 12), window.innerWidth - w - 8);
|
|
@@ -2518,6 +2638,11 @@ var LoupeApp = class {
|
|
|
2518
2638
|
context = captureElementContext(target.element);
|
|
2519
2639
|
offset = this.targetOffset;
|
|
2520
2640
|
anchoredEl = target.element;
|
|
2641
|
+
} else if (target.kind === "free") {
|
|
2642
|
+
screenshot = void 0;
|
|
2643
|
+
anchor = pageAnchor(target.point);
|
|
2644
|
+
context = { html: "", styles: {} };
|
|
2645
|
+
offset = target.offset;
|
|
2521
2646
|
} else {
|
|
2522
2647
|
screenshot = withShot ? target.screenshot ?? await this.pendingShot : void 0;
|
|
2523
2648
|
region = target.region;
|
|
@@ -2577,6 +2702,7 @@ var LoupeApp = class {
|
|
|
2577
2702
|
}
|
|
2578
2703
|
pin.textContent = String(i + 1);
|
|
2579
2704
|
pin.classList.toggle("done", c.status === "done");
|
|
2705
|
+
pin.classList.toggle("free", c.kind === "free");
|
|
2580
2706
|
});
|
|
2581
2707
|
this.countEl.textContent = String(this.comments.length);
|
|
2582
2708
|
this.position();
|
|
@@ -2604,6 +2730,7 @@ var LoupeApp = class {
|
|
|
2604
2730
|
};
|
|
2605
2731
|
window.addEventListener("scroll", reposition, true);
|
|
2606
2732
|
window.addEventListener("resize", reposition);
|
|
2733
|
+
window.addEventListener("resize", this.onResizeBar);
|
|
2607
2734
|
let debounce = 0;
|
|
2608
2735
|
this.mo = new MutationObserver(() => {
|
|
2609
2736
|
clearTimeout(debounce);
|
|
@@ -2637,6 +2764,86 @@ var LoupeApp = class {
|
|
|
2637
2764
|
this.overlay.style.display = "";
|
|
2638
2765
|
this.renderPins();
|
|
2639
2766
|
}
|
|
2767
|
+
this.layoutBar();
|
|
2768
|
+
}
|
|
2769
|
+
loadBarPos() {
|
|
2770
|
+
try {
|
|
2771
|
+
const s = localStorage.getItem("loupe:bar");
|
|
2772
|
+
if (s) {
|
|
2773
|
+
const p = JSON.parse(s);
|
|
2774
|
+
if (typeof p?.fx === "number" && typeof p?.fy === "number") this.barPos = p;
|
|
2775
|
+
}
|
|
2776
|
+
} catch {
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
saveBarPos() {
|
|
2780
|
+
try {
|
|
2781
|
+
if (this.barPos) localStorage.setItem("loupe:bar", JSON.stringify(this.barPos));
|
|
2782
|
+
} catch {
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
brandEl() {
|
|
2786
|
+
return this.toolbar.querySelector(".brand");
|
|
2787
|
+
}
|
|
2788
|
+
/**
|
|
2789
|
+
* Position the toolbar for its stored spot and make it expand *inward* so it's
|
|
2790
|
+
* always fully on-screen: docked to a left/right edge → vertical; to a
|
|
2791
|
+
* top/bottom edge or a corner → horizontal. No stored spot → default
|
|
2792
|
+
* (bottom-center, governed by CSS).
|
|
2793
|
+
*/
|
|
2794
|
+
layoutBar() {
|
|
2795
|
+
const bar = this.toolbar;
|
|
2796
|
+
if (!this.barPos) {
|
|
2797
|
+
bar.classList.remove("floating", "orient-h", "orient-v", "flow-rev");
|
|
2798
|
+
for (const p of ["left", "top", "right", "bottom", "transform"]) bar.style[p] = "";
|
|
2799
|
+
return;
|
|
2800
|
+
}
|
|
2801
|
+
const vw = window.innerWidth, vh = window.innerHeight;
|
|
2802
|
+
const brand = this.brandEl();
|
|
2803
|
+
const iw = brand.offsetWidth || 44, ih = brand.offsetHeight || 44;
|
|
2804
|
+
const ix = clampPx(this.barPos.fx * vw, 6, Math.max(6, vw - iw - 6));
|
|
2805
|
+
const iy = clampPx(this.barPos.fy * vh, 6, Math.max(6, vh - ih - 6));
|
|
2806
|
+
const cx = ix + iw / 2, cy = iy + ih / 2;
|
|
2807
|
+
const horizontalSide = cx < vw / 2 ? "left" : "right";
|
|
2808
|
+
const verticalSide = cy < vh / 2 ? "top" : "bottom";
|
|
2809
|
+
const vertical = Math.min(cx, vw - cx) < Math.min(cy, vh - cy);
|
|
2810
|
+
bar.classList.add("floating");
|
|
2811
|
+
bar.classList.remove("dragging");
|
|
2812
|
+
bar.classList.toggle("orient-v", vertical);
|
|
2813
|
+
bar.classList.toggle("orient-h", !vertical);
|
|
2814
|
+
bar.classList.toggle("flow-rev", vertical ? verticalSide === "bottom" : horizontalSide === "right");
|
|
2815
|
+
bar.style.transform = "none";
|
|
2816
|
+
if (horizontalSide === "left") {
|
|
2817
|
+
bar.style.left = ix + "px";
|
|
2818
|
+
bar.style.right = "auto";
|
|
2819
|
+
} else {
|
|
2820
|
+
bar.style.right = vw - (ix + iw) + "px";
|
|
2821
|
+
bar.style.left = "auto";
|
|
2822
|
+
}
|
|
2823
|
+
if (verticalSide === "top") {
|
|
2824
|
+
bar.style.top = iy + "px";
|
|
2825
|
+
bar.style.bottom = "auto";
|
|
2826
|
+
} else {
|
|
2827
|
+
bar.style.bottom = vh - (iy + ih) + "px";
|
|
2828
|
+
bar.style.top = "auto";
|
|
2829
|
+
}
|
|
2830
|
+
const r = bar.getBoundingClientRect();
|
|
2831
|
+
if (r.right > vw - 6 && bar.style.left !== "auto") {
|
|
2832
|
+
bar.style.left = "auto";
|
|
2833
|
+
bar.style.right = "6px";
|
|
2834
|
+
}
|
|
2835
|
+
if (r.left < 6 && bar.style.right !== "auto") {
|
|
2836
|
+
bar.style.right = "auto";
|
|
2837
|
+
bar.style.left = "6px";
|
|
2838
|
+
}
|
|
2839
|
+
if (r.bottom > vh - 6 && bar.style.top !== "auto") {
|
|
2840
|
+
bar.style.top = "auto";
|
|
2841
|
+
bar.style.bottom = "6px";
|
|
2842
|
+
}
|
|
2843
|
+
if (r.top < 6 && bar.style.bottom !== "auto") {
|
|
2844
|
+
bar.style.bottom = "auto";
|
|
2845
|
+
bar.style.top = "6px";
|
|
2846
|
+
}
|
|
2640
2847
|
}
|
|
2641
2848
|
renderPanel() {
|
|
2642
2849
|
const p = this.panel;
|
|
@@ -2649,7 +2856,7 @@ var LoupeApp = class {
|
|
|
2649
2856
|
p.appendChild(head);
|
|
2650
2857
|
const list = el("div", "list");
|
|
2651
2858
|
if (!this.comments.length) {
|
|
2652
|
-
list.appendChild(el("div", "empty", "No comments yet. Click \u201CInspect & comment\u201D,
|
|
2859
|
+
list.appendChild(el("div", "empty", "No comments yet. Click \u201CInspect & comment\u201D and pick an element, or \u201CNote\u201D to drop a comment anywhere."));
|
|
2653
2860
|
}
|
|
2654
2861
|
this.comments.forEach((c, i) => list.appendChild(this.itemView(c, i)));
|
|
2655
2862
|
p.appendChild(list);
|
|
@@ -2708,7 +2915,13 @@ var LoupeApp = class {
|
|
|
2708
2915
|
return item;
|
|
2709
2916
|
}
|
|
2710
2917
|
async copyForClaude(c) {
|
|
2711
|
-
const
|
|
2918
|
+
const lines = c.kind === "free" ? [
|
|
2919
|
+
`# Product feedback from ${c.author.name}`,
|
|
2920
|
+
``,
|
|
2921
|
+
`**Note:** ${c.body}`,
|
|
2922
|
+
`**Page:** ${c.url}`,
|
|
2923
|
+
`**Type:** Free note \u2014 a page-level comment not tied to a specific element.`
|
|
2924
|
+
] : [
|
|
2712
2925
|
`# Product feedback from ${c.author.name}`,
|
|
2713
2926
|
``,
|
|
2714
2927
|
`**Comment:** ${c.body}`,
|
|
@@ -2725,7 +2938,8 @@ var LoupeApp = class {
|
|
|
2725
2938
|
"```json",
|
|
2726
2939
|
JSON.stringify(c.context.styles, null, 2),
|
|
2727
2940
|
"```"
|
|
2728
|
-
]
|
|
2941
|
+
];
|
|
2942
|
+
const prompt = lines.filter(Boolean).join("\n");
|
|
2729
2943
|
try {
|
|
2730
2944
|
await navigator.clipboard.writeText(prompt);
|
|
2731
2945
|
} catch {
|
|
@@ -2735,6 +2949,19 @@ var LoupeApp = class {
|
|
|
2735
2949
|
flash(id) {
|
|
2736
2950
|
const c = this.comments.find((x) => x.id === id);
|
|
2737
2951
|
const pin = this.pins.get(id);
|
|
2952
|
+
if (c?.kind === "free") {
|
|
2953
|
+
for (const p of this.pins.values()) p.classList.remove("active");
|
|
2954
|
+
pin?.classList.add("active");
|
|
2955
|
+
const docW = Math.max(1, document.documentElement.scrollWidth);
|
|
2956
|
+
const docH = Math.max(1, document.documentElement.scrollHeight);
|
|
2957
|
+
window.scrollTo({
|
|
2958
|
+
top: Math.max(0, c.offset.y * docH - window.innerHeight / 2),
|
|
2959
|
+
left: Math.max(0, c.offset.x * docW - window.innerWidth / 2),
|
|
2960
|
+
behavior: "smooth"
|
|
2961
|
+
});
|
|
2962
|
+
this.position();
|
|
2963
|
+
return;
|
|
2964
|
+
}
|
|
2738
2965
|
if (c?.kind === "region" && c.region) {
|
|
2739
2966
|
for (const p of this.pins.values()) p.classList.remove("active");
|
|
2740
2967
|
pin?.classList.add("active");
|
|
@@ -2761,6 +2988,7 @@ var LoupeApp = class {
|
|
|
2761
2988
|
this.mo?.disconnect();
|
|
2762
2989
|
if (this.tick) clearInterval(this.tick);
|
|
2763
2990
|
window.clearTimeout(this.regionTimer);
|
|
2991
|
+
window.removeEventListener("resize", this.onResizeBar);
|
|
2764
2992
|
document.removeEventListener("keydown", this.onKey, true);
|
|
2765
2993
|
this.root?.remove();
|
|
2766
2994
|
}
|
|
@@ -2777,7 +3005,24 @@ function sep() {
|
|
|
2777
3005
|
function clamp(n) {
|
|
2778
3006
|
return Math.max(0, Math.min(1, n));
|
|
2779
3007
|
}
|
|
3008
|
+
function clampPx(n, min, max) {
|
|
3009
|
+
return Math.max(min, Math.min(max, n));
|
|
3010
|
+
}
|
|
2780
3011
|
var REGION_ICON = `<svg class="ico" width="15" height="15" viewBox="0 0 15 15" fill="none" aria-hidden="true"><rect x="1.5" y="2.5" width="12" height="10" rx="1.5" stroke="currentColor" stroke-width="1.4" stroke-dasharray="2.4 1.8"/></svg>`;
|
|
3012
|
+
var NOTE_ICON = `<svg class="ico" width="15" height="15" viewBox="0 0 15 15" fill="none" aria-hidden="true"><path d="M2 2.5h11v7.5H6l-3 2.5v-2.5H2z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>`;
|
|
3013
|
+
function pageAnchor(point) {
|
|
3014
|
+
return {
|
|
3015
|
+
tag: "page",
|
|
3016
|
+
cssPath: "page",
|
|
3017
|
+
xpath: "",
|
|
3018
|
+
testid: null,
|
|
3019
|
+
text: "",
|
|
3020
|
+
attrs: {},
|
|
3021
|
+
nthOfType: 1,
|
|
3022
|
+
rect: { x: Math.round(point.x), y: Math.round(point.y), w: 0, h: 0 },
|
|
3023
|
+
viewport: { w: window.innerWidth, h: window.innerHeight }
|
|
3024
|
+
};
|
|
3025
|
+
}
|
|
2781
3026
|
function regionAnchor(region) {
|
|
2782
3027
|
return {
|
|
2783
3028
|
tag: "region",
|
|
@@ -2803,6 +3048,7 @@ function describe(elx) {
|
|
|
2803
3048
|
return txt ? `${tag} \xB7 \u201C${txt}\u201D` : tag;
|
|
2804
3049
|
}
|
|
2805
3050
|
function describeAnchor(c) {
|
|
3051
|
+
if (c.kind === "free") return "Free note \xB7 page-level";
|
|
2806
3052
|
return c.anchor.testid ? `[data-testid="${c.anchor.testid}"]` : c.anchor.cssPath;
|
|
2807
3053
|
}
|
|
2808
3054
|
|