@loupekit/sdk 0.3.4 → 0.4.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 +96 -17
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +96 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.global.js
CHANGED
|
@@ -96,20 +96,24 @@ var Loupe = (() => {
|
|
|
96
96
|
background: #1b1e27; color: #fff; padding: 6px; border-radius: 12px;
|
|
97
97
|
box-shadow: 0 8px 30px rgba(0,0,0,.4); border: 1px solid #2b2f3b;
|
|
98
98
|
}
|
|
99
|
+
/* every toolbar item (brand + buttons) shares one icon+label layout so they align */
|
|
100
|
+
.toolbar button, .toolbar .brand {
|
|
101
|
+
display: flex; align-items: center; gap: 6px;
|
|
102
|
+
padding: 8px 12px; border-radius: 8px;
|
|
103
|
+
font-size: 13px; font-weight: 500; line-height: 1;
|
|
104
|
+
}
|
|
99
105
|
.toolbar button {
|
|
100
106
|
background: transparent; color: #cfd3de; border: 0; cursor: pointer;
|
|
101
|
-
font-size: 13px; font-weight: 500; padding: 8px 12px; border-radius: 8px;
|
|
102
|
-
display: flex; align-items: center; gap: 6px;
|
|
103
107
|
}
|
|
104
|
-
.toolbar button:hover { background: #2b2f3b; color: #fff; }
|
|
105
|
-
.toolbar
|
|
108
|
+
.toolbar button:hover, .toolbar .brand:hover { background: #2b2f3b; color: #fff; }
|
|
109
|
+
.toolbar .ico { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; }
|
|
110
|
+
.toolbar .ico svg { width: 16px; height: 16px; display: block; }
|
|
111
|
+
.toolbar .label { white-space: nowrap; }
|
|
106
112
|
.toolbar button.on { background: var(--accent); color: #fff; }
|
|
107
113
|
.toolbar .sep { width: 1px; height: 20px; background: #333846; margin: 0 2px; }
|
|
108
114
|
.toolbar .brand {
|
|
109
|
-
font-weight: 700;
|
|
110
|
-
cursor: pointer; user-select: none;
|
|
115
|
+
font-weight: 700; letter-spacing: -.01em; cursor: pointer; user-select: none;
|
|
111
116
|
}
|
|
112
|
-
.toolbar .brand:hover { background: #2b2f3b; }
|
|
113
117
|
/* collapsed \u2192 show only the brand/logo; click it again to expand */
|
|
114
118
|
.toolbar.collapsed { gap: 0; }
|
|
115
119
|
.toolbar.collapsed > *:not(.brand) { display: none; }
|
|
@@ -118,6 +122,16 @@ var Loupe = (() => {
|
|
|
118
122
|
border-radius: 999px; padding: 1px 7px; margin-left: 2px;
|
|
119
123
|
}
|
|
120
124
|
|
|
125
|
+
/* mobile \u2192 compact, icon-only, pinned to the left */
|
|
126
|
+
@media (max-width: 640px) {
|
|
127
|
+
.toolbar { left: 12px; right: auto; transform: none; bottom: 12px; padding: 4px; gap: 2px; }
|
|
128
|
+
.toolbar .label { display: none; }
|
|
129
|
+
.toolbar button, .toolbar .brand { padding: 10px; }
|
|
130
|
+
.toolbar .sep { display: none; }
|
|
131
|
+
.toolbar .count { position: absolute; top: -4px; right: -4px; margin: 0; }
|
|
132
|
+
.toolbar [data-role="comments"] { position: relative; }
|
|
133
|
+
}
|
|
134
|
+
|
|
121
135
|
/* composer popover */
|
|
122
136
|
.composer {
|
|
123
137
|
position: fixed; z-index: 2147483004; pointer-events: auto; width: 300px;
|
|
@@ -165,6 +179,7 @@ var Loupe = (() => {
|
|
|
165
179
|
.item .num.detached { background:#9aa0af; }
|
|
166
180
|
.item .num.done { background:#10935a; }
|
|
167
181
|
.item .who { font-size: 12px; font-weight: 600; }
|
|
182
|
+
.item .device { font-size: 10px; color: var(--muted); background: var(--panel-2); border-radius: 999px; padding: 1px 7px; white-space: nowrap; }
|
|
168
183
|
.item .body { font-size: 13px; line-height: 1.4; }
|
|
169
184
|
.item .meta { font-size: 11px; color: var(--muted); margin-top: 6px; font-family: ui-monospace, Menlo, monospace; }
|
|
170
185
|
.item .badge { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; padding: 1px 6px; border-radius: 5px; margin-left: auto; }
|
|
@@ -1980,22 +1995,35 @@ var Loupe = (() => {
|
|
|
1980
1995
|
try {
|
|
1981
1996
|
await fontsReady();
|
|
1982
1997
|
const scale = Math.min(window.devicePixelRatio || 1, 2);
|
|
1983
|
-
const
|
|
1998
|
+
const container = regionContainer(rect);
|
|
1999
|
+
const origin = container.getBoundingClientRect();
|
|
2000
|
+
const full = await domToPng(container, {
|
|
1984
2001
|
scale,
|
|
1985
2002
|
backgroundColor: getComputedStyle(document.body).backgroundColor || "#ffffff",
|
|
1986
2003
|
timeout: 3e4,
|
|
1987
2004
|
filter: captureFilter
|
|
1988
2005
|
});
|
|
1989
|
-
const bodyRect = document.body.getBoundingClientRect();
|
|
1990
2006
|
const redact = Array.from(document.querySelectorAll("[data-loupe-redact]")).map(
|
|
1991
2007
|
(n) => n.getBoundingClientRect()
|
|
1992
2008
|
);
|
|
1993
|
-
return await cropRegion(full, rect,
|
|
2009
|
+
return await cropRegion(full, rect, origin.left, origin.top, scale, redact);
|
|
1994
2010
|
} catch (err) {
|
|
1995
2011
|
console.warn("[loupe] region capture failed", err);
|
|
1996
2012
|
return void 0;
|
|
1997
2013
|
}
|
|
1998
2014
|
}
|
|
2015
|
+
function regionContainer(rect) {
|
|
2016
|
+
const cx = rect.x + rect.w / 2;
|
|
2017
|
+
const cy = rect.y + rect.h / 2;
|
|
2018
|
+
let node = document.elementFromPoint(cx, cy);
|
|
2019
|
+
if (node && node.closest("#loupe-root")) node = null;
|
|
2020
|
+
const covers = (r) => r.left <= rect.x && r.top <= rect.y && r.right >= rect.x + rect.w && r.bottom >= rect.y + rect.h;
|
|
2021
|
+
while (node && node !== document.body && node !== document.documentElement) {
|
|
2022
|
+
if (covers(node.getBoundingClientRect())) return node;
|
|
2023
|
+
node = node.parentElement;
|
|
2024
|
+
}
|
|
2025
|
+
return document.body;
|
|
2026
|
+
}
|
|
1999
2027
|
function cropRegion(dataUrl, rect, ox, oy, scale, redact) {
|
|
2000
2028
|
return new Promise((resolve, reject) => {
|
|
2001
2029
|
const img = new Image();
|
|
@@ -2149,6 +2177,7 @@ var Loupe = (() => {
|
|
|
2149
2177
|
this.pins = /* @__PURE__ */ new Map();
|
|
2150
2178
|
this.mode = "off";
|
|
2151
2179
|
this.collapsed = false;
|
|
2180
|
+
this.lastUrl = "";
|
|
2152
2181
|
this.targetOffset = { x: 0.5, y: 0.5 };
|
|
2153
2182
|
this.pending = null;
|
|
2154
2183
|
this.dragStart = null;
|
|
@@ -2285,12 +2314,43 @@ var Loupe = (() => {
|
|
|
2285
2314
|
}
|
|
2286
2315
|
async start() {
|
|
2287
2316
|
this.buildDom();
|
|
2317
|
+
this.lastUrl = this.url;
|
|
2288
2318
|
this.comments = await this.store.list(this.cfg.projectKey, this.url);
|
|
2289
2319
|
this.renderPins();
|
|
2290
2320
|
this.renderPanel();
|
|
2291
2321
|
this.observe();
|
|
2322
|
+
this.watchNavigation();
|
|
2292
2323
|
if (this.cfg.autoOpen) this.setMode("inspect");
|
|
2293
2324
|
}
|
|
2325
|
+
/**
|
|
2326
|
+
* Reload comments when the page URL changes without a full reload (SPA
|
|
2327
|
+
* navigation), so each page only ever shows its own comments.
|
|
2328
|
+
*/
|
|
2329
|
+
watchNavigation() {
|
|
2330
|
+
const onChange = () => {
|
|
2331
|
+
if (this.url === this.lastUrl) return;
|
|
2332
|
+
this.lastUrl = this.url;
|
|
2333
|
+
void this.reloadComments();
|
|
2334
|
+
};
|
|
2335
|
+
addEventListener("popstate", onChange);
|
|
2336
|
+
for (const key of ["pushState", "replaceState"]) {
|
|
2337
|
+
const original = history[key];
|
|
2338
|
+
history[key] = function(...args) {
|
|
2339
|
+
const result = original.apply(this, args);
|
|
2340
|
+
dispatchEvent(new Event("loupe:locationchange"));
|
|
2341
|
+
return result;
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
addEventListener("loupe:locationchange", onChange);
|
|
2345
|
+
}
|
|
2346
|
+
async reloadComments() {
|
|
2347
|
+
try {
|
|
2348
|
+
this.comments = await this.store.list(this.cfg.projectKey, this.url);
|
|
2349
|
+
this.renderPins();
|
|
2350
|
+
this.renderPanel();
|
|
2351
|
+
} catch {
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2294
2354
|
// ---- DOM construction -----------------------------------------------------
|
|
2295
2355
|
buildDom() {
|
|
2296
2356
|
this.root = document.createElement("div");
|
|
@@ -2316,25 +2376,32 @@ var Loupe = (() => {
|
|
|
2316
2376
|
}
|
|
2317
2377
|
buildToolbar() {
|
|
2318
2378
|
const bar = el("div", "toolbar");
|
|
2319
|
-
const brand = el("span", "brand"
|
|
2379
|
+
const brand = el("span", "brand");
|
|
2380
|
+
brand.innerHTML = `<span class="ico">\u25CE</span><span class="label">Loupe</span>`;
|
|
2320
2381
|
brand.title = "Collapse / expand the Loupe bar";
|
|
2321
2382
|
brand.setAttribute("role", "button");
|
|
2322
2383
|
brand.onclick = () => this.toggleCollapsed();
|
|
2323
|
-
const inspectBtn =
|
|
2324
|
-
inspectBtn.dataset.role = "inspect";
|
|
2384
|
+
const inspectBtn = this.toolBtn("\u271B", "Inspect & comment", "inspect");
|
|
2325
2385
|
inspectBtn.onclick = () => this.setMode(this.mode === "inspect" ? "off" : "inspect");
|
|
2326
|
-
const regionBtn =
|
|
2327
|
-
regionBtn.innerHTML = `${REGION_ICON}<span>Region shot</span>`;
|
|
2328
|
-
regionBtn.dataset.role = "region";
|
|
2386
|
+
const regionBtn = this.toolBtn(REGION_ICON, "Region shot", "region");
|
|
2329
2387
|
regionBtn.title = "Drag a free-size box, screenshot it, and comment";
|
|
2330
2388
|
regionBtn.onclick = () => this.setMode(this.mode === "region" ? "off" : "region");
|
|
2331
|
-
const listBtn =
|
|
2389
|
+
const listBtn = this.toolBtn("\u2630", "Comments", "comments");
|
|
2332
2390
|
this.countEl = el("span", "count", "0");
|
|
2333
2391
|
listBtn.appendChild(this.countEl);
|
|
2334
2392
|
listBtn.onclick = () => this.togglePanel();
|
|
2335
2393
|
bar.append(brand, sep(), inspectBtn, regionBtn, listBtn);
|
|
2336
2394
|
return bar;
|
|
2337
2395
|
}
|
|
2396
|
+
/** A toolbar button with a uniform icon + label layout. `icon` may be an SVG string. */
|
|
2397
|
+
toolBtn(icon, label, role) {
|
|
2398
|
+
const b = el("button");
|
|
2399
|
+
b.dataset.role = role;
|
|
2400
|
+
b.title = label;
|
|
2401
|
+
b.setAttribute("aria-label", label);
|
|
2402
|
+
b.innerHTML = `<span class="ico">${icon}</span><span class="label">${label}</span>`;
|
|
2403
|
+
return b;
|
|
2404
|
+
}
|
|
2338
2405
|
drawSelection(curX, curY) {
|
|
2339
2406
|
const s = this.dragStart;
|
|
2340
2407
|
Object.assign(this.selbox.style, {
|
|
@@ -2487,6 +2554,8 @@ var Loupe = (() => {
|
|
|
2487
2554
|
offset,
|
|
2488
2555
|
region,
|
|
2489
2556
|
screenshot,
|
|
2557
|
+
// Record the screen the feedback was captured on (desktop / tablet / mobile).
|
|
2558
|
+
viewport: { w: window.innerWidth, h: window.innerHeight },
|
|
2490
2559
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2491
2560
|
};
|
|
2492
2561
|
await this.store.save(comment);
|
|
@@ -2573,6 +2642,10 @@ var Loupe = (() => {
|
|
|
2573
2642
|
this.setMode("off");
|
|
2574
2643
|
this.panel.classList.remove("open");
|
|
2575
2644
|
this.closeComposer();
|
|
2645
|
+
this.overlay.style.display = "none";
|
|
2646
|
+
} else {
|
|
2647
|
+
this.overlay.style.display = "";
|
|
2648
|
+
this.renderPins();
|
|
2576
2649
|
}
|
|
2577
2650
|
}
|
|
2578
2651
|
renderPanel() {
|
|
@@ -2598,6 +2671,12 @@ var Loupe = (() => {
|
|
|
2598
2671
|
const num = el("span", "num" + (c.status === "done" ? " done" : detached ? " detached" : ""), String(i + 1));
|
|
2599
2672
|
const who = el("span", "who", c.author.name);
|
|
2600
2673
|
top.append(num, who);
|
|
2674
|
+
const vw = c.viewport?.w;
|
|
2675
|
+
if (vw) {
|
|
2676
|
+
const kind = vw < 768 ? "mobile" : vw < 1024 ? "tablet" : "desktop";
|
|
2677
|
+
const icon = vw < 768 ? "\u{1F4F1}" : vw < 1024 ? "\u25A6" : "\u{1F5A5}";
|
|
2678
|
+
top.appendChild(el("span", "device", `${icon} ${kind}`));
|
|
2679
|
+
}
|
|
2601
2680
|
if (c.status === "done") top.appendChild(el("span", "badge done", "done"));
|
|
2602
2681
|
else if (detached) top.appendChild(el("span", "badge detached", "element moved/removed"));
|
|
2603
2682
|
item.appendChild(top);
|