@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.js
CHANGED
|
@@ -69,20 +69,24 @@ var STYLES = (
|
|
|
69
69
|
background: #1b1e27; color: #fff; padding: 6px; border-radius: 12px;
|
|
70
70
|
box-shadow: 0 8px 30px rgba(0,0,0,.4); border: 1px solid #2b2f3b;
|
|
71
71
|
}
|
|
72
|
+
/* every toolbar item (brand + buttons) shares one icon+label layout so they align */
|
|
73
|
+
.toolbar button, .toolbar .brand {
|
|
74
|
+
display: flex; align-items: center; gap: 6px;
|
|
75
|
+
padding: 8px 12px; border-radius: 8px;
|
|
76
|
+
font-size: 13px; font-weight: 500; line-height: 1;
|
|
77
|
+
}
|
|
72
78
|
.toolbar button {
|
|
73
79
|
background: transparent; color: #cfd3de; border: 0; cursor: pointer;
|
|
74
|
-
font-size: 13px; font-weight: 500; padding: 8px 12px; border-radius: 8px;
|
|
75
|
-
display: flex; align-items: center; gap: 6px;
|
|
76
80
|
}
|
|
77
|
-
.toolbar button:hover { background: #2b2f3b; color: #fff; }
|
|
78
|
-
.toolbar
|
|
81
|
+
.toolbar button:hover, .toolbar .brand:hover { background: #2b2f3b; color: #fff; }
|
|
82
|
+
.toolbar .ico { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; }
|
|
83
|
+
.toolbar .ico svg { width: 16px; height: 16px; display: block; }
|
|
84
|
+
.toolbar .label { white-space: nowrap; }
|
|
79
85
|
.toolbar button.on { background: var(--accent); color: #fff; }
|
|
80
86
|
.toolbar .sep { width: 1px; height: 20px; background: #333846; margin: 0 2px; }
|
|
81
87
|
.toolbar .brand {
|
|
82
|
-
font-weight: 700;
|
|
83
|
-
cursor: pointer; user-select: none;
|
|
88
|
+
font-weight: 700; letter-spacing: -.01em; cursor: pointer; user-select: none;
|
|
84
89
|
}
|
|
85
|
-
.toolbar .brand:hover { background: #2b2f3b; }
|
|
86
90
|
/* collapsed \u2192 show only the brand/logo; click it again to expand */
|
|
87
91
|
.toolbar.collapsed { gap: 0; }
|
|
88
92
|
.toolbar.collapsed > *:not(.brand) { display: none; }
|
|
@@ -91,6 +95,16 @@ var STYLES = (
|
|
|
91
95
|
border-radius: 999px; padding: 1px 7px; margin-left: 2px;
|
|
92
96
|
}
|
|
93
97
|
|
|
98
|
+
/* mobile \u2192 compact, icon-only, pinned to the left */
|
|
99
|
+
@media (max-width: 640px) {
|
|
100
|
+
.toolbar { left: 12px; right: auto; transform: none; bottom: 12px; padding: 4px; gap: 2px; }
|
|
101
|
+
.toolbar .label { display: none; }
|
|
102
|
+
.toolbar button, .toolbar .brand { padding: 10px; }
|
|
103
|
+
.toolbar .sep { display: none; }
|
|
104
|
+
.toolbar .count { position: absolute; top: -4px; right: -4px; margin: 0; }
|
|
105
|
+
.toolbar [data-role="comments"] { position: relative; }
|
|
106
|
+
}
|
|
107
|
+
|
|
94
108
|
/* composer popover */
|
|
95
109
|
.composer {
|
|
96
110
|
position: fixed; z-index: 2147483004; pointer-events: auto; width: 300px;
|
|
@@ -138,6 +152,7 @@ var STYLES = (
|
|
|
138
152
|
.item .num.detached { background:#9aa0af; }
|
|
139
153
|
.item .num.done { background:#10935a; }
|
|
140
154
|
.item .who { font-size: 12px; font-weight: 600; }
|
|
155
|
+
.item .device { font-size: 10px; color: var(--muted); background: var(--panel-2); border-radius: 999px; padding: 1px 7px; white-space: nowrap; }
|
|
141
156
|
.item .body { font-size: 13px; line-height: 1.4; }
|
|
142
157
|
.item .meta { font-size: 11px; color: var(--muted); margin-top: 6px; font-family: ui-monospace, Menlo, monospace; }
|
|
143
158
|
.item .badge { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; padding: 1px 6px; border-radius: 5px; margin-left: auto; }
|
|
@@ -1953,22 +1968,35 @@ async function captureRegionScreenshot(rect) {
|
|
|
1953
1968
|
try {
|
|
1954
1969
|
await fontsReady();
|
|
1955
1970
|
const scale = Math.min(window.devicePixelRatio || 1, 2);
|
|
1956
|
-
const
|
|
1971
|
+
const container = regionContainer(rect);
|
|
1972
|
+
const origin = container.getBoundingClientRect();
|
|
1973
|
+
const full = await domToPng(container, {
|
|
1957
1974
|
scale,
|
|
1958
1975
|
backgroundColor: getComputedStyle(document.body).backgroundColor || "#ffffff",
|
|
1959
1976
|
timeout: 3e4,
|
|
1960
1977
|
filter: captureFilter
|
|
1961
1978
|
});
|
|
1962
|
-
const bodyRect = document.body.getBoundingClientRect();
|
|
1963
1979
|
const redact = Array.from(document.querySelectorAll("[data-loupe-redact]")).map(
|
|
1964
1980
|
(n) => n.getBoundingClientRect()
|
|
1965
1981
|
);
|
|
1966
|
-
return await cropRegion(full, rect,
|
|
1982
|
+
return await cropRegion(full, rect, origin.left, origin.top, scale, redact);
|
|
1967
1983
|
} catch (err) {
|
|
1968
1984
|
console.warn("[loupe] region capture failed", err);
|
|
1969
1985
|
return void 0;
|
|
1970
1986
|
}
|
|
1971
1987
|
}
|
|
1988
|
+
function regionContainer(rect) {
|
|
1989
|
+
const cx = rect.x + rect.w / 2;
|
|
1990
|
+
const cy = rect.y + rect.h / 2;
|
|
1991
|
+
let node = document.elementFromPoint(cx, cy);
|
|
1992
|
+
if (node && node.closest("#loupe-root")) node = null;
|
|
1993
|
+
const covers = (r) => r.left <= rect.x && r.top <= rect.y && r.right >= rect.x + rect.w && r.bottom >= rect.y + rect.h;
|
|
1994
|
+
while (node && node !== document.body && node !== document.documentElement) {
|
|
1995
|
+
if (covers(node.getBoundingClientRect())) return node;
|
|
1996
|
+
node = node.parentElement;
|
|
1997
|
+
}
|
|
1998
|
+
return document.body;
|
|
1999
|
+
}
|
|
1972
2000
|
function cropRegion(dataUrl, rect, ox, oy, scale, redact) {
|
|
1973
2001
|
return new Promise((resolve, reject) => {
|
|
1974
2002
|
const img = new Image();
|
|
@@ -2122,6 +2150,7 @@ var LoupeApp = class {
|
|
|
2122
2150
|
this.pins = /* @__PURE__ */ new Map();
|
|
2123
2151
|
this.mode = "off";
|
|
2124
2152
|
this.collapsed = false;
|
|
2153
|
+
this.lastUrl = "";
|
|
2125
2154
|
this.targetOffset = { x: 0.5, y: 0.5 };
|
|
2126
2155
|
this.pending = null;
|
|
2127
2156
|
this.dragStart = null;
|
|
@@ -2258,12 +2287,43 @@ var LoupeApp = class {
|
|
|
2258
2287
|
}
|
|
2259
2288
|
async start() {
|
|
2260
2289
|
this.buildDom();
|
|
2290
|
+
this.lastUrl = this.url;
|
|
2261
2291
|
this.comments = await this.store.list(this.cfg.projectKey, this.url);
|
|
2262
2292
|
this.renderPins();
|
|
2263
2293
|
this.renderPanel();
|
|
2264
2294
|
this.observe();
|
|
2295
|
+
this.watchNavigation();
|
|
2265
2296
|
if (this.cfg.autoOpen) this.setMode("inspect");
|
|
2266
2297
|
}
|
|
2298
|
+
/**
|
|
2299
|
+
* Reload comments when the page URL changes without a full reload (SPA
|
|
2300
|
+
* navigation), so each page only ever shows its own comments.
|
|
2301
|
+
*/
|
|
2302
|
+
watchNavigation() {
|
|
2303
|
+
const onChange = () => {
|
|
2304
|
+
if (this.url === this.lastUrl) return;
|
|
2305
|
+
this.lastUrl = this.url;
|
|
2306
|
+
void this.reloadComments();
|
|
2307
|
+
};
|
|
2308
|
+
addEventListener("popstate", onChange);
|
|
2309
|
+
for (const key of ["pushState", "replaceState"]) {
|
|
2310
|
+
const original = history[key];
|
|
2311
|
+
history[key] = function(...args) {
|
|
2312
|
+
const result = original.apply(this, args);
|
|
2313
|
+
dispatchEvent(new Event("loupe:locationchange"));
|
|
2314
|
+
return result;
|
|
2315
|
+
};
|
|
2316
|
+
}
|
|
2317
|
+
addEventListener("loupe:locationchange", onChange);
|
|
2318
|
+
}
|
|
2319
|
+
async reloadComments() {
|
|
2320
|
+
try {
|
|
2321
|
+
this.comments = await this.store.list(this.cfg.projectKey, this.url);
|
|
2322
|
+
this.renderPins();
|
|
2323
|
+
this.renderPanel();
|
|
2324
|
+
} catch {
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2267
2327
|
// ---- DOM construction -----------------------------------------------------
|
|
2268
2328
|
buildDom() {
|
|
2269
2329
|
this.root = document.createElement("div");
|
|
@@ -2289,25 +2349,32 @@ var LoupeApp = class {
|
|
|
2289
2349
|
}
|
|
2290
2350
|
buildToolbar() {
|
|
2291
2351
|
const bar = el("div", "toolbar");
|
|
2292
|
-
const brand = el("span", "brand"
|
|
2352
|
+
const brand = el("span", "brand");
|
|
2353
|
+
brand.innerHTML = `<span class="ico">\u25CE</span><span class="label">Loupe</span>`;
|
|
2293
2354
|
brand.title = "Collapse / expand the Loupe bar";
|
|
2294
2355
|
brand.setAttribute("role", "button");
|
|
2295
2356
|
brand.onclick = () => this.toggleCollapsed();
|
|
2296
|
-
const inspectBtn =
|
|
2297
|
-
inspectBtn.dataset.role = "inspect";
|
|
2357
|
+
const inspectBtn = this.toolBtn("\u271B", "Inspect & comment", "inspect");
|
|
2298
2358
|
inspectBtn.onclick = () => this.setMode(this.mode === "inspect" ? "off" : "inspect");
|
|
2299
|
-
const regionBtn =
|
|
2300
|
-
regionBtn.innerHTML = `${REGION_ICON}<span>Region shot</span>`;
|
|
2301
|
-
regionBtn.dataset.role = "region";
|
|
2359
|
+
const regionBtn = this.toolBtn(REGION_ICON, "Region shot", "region");
|
|
2302
2360
|
regionBtn.title = "Drag a free-size box, screenshot it, and comment";
|
|
2303
2361
|
regionBtn.onclick = () => this.setMode(this.mode === "region" ? "off" : "region");
|
|
2304
|
-
const listBtn =
|
|
2362
|
+
const listBtn = this.toolBtn("\u2630", "Comments", "comments");
|
|
2305
2363
|
this.countEl = el("span", "count", "0");
|
|
2306
2364
|
listBtn.appendChild(this.countEl);
|
|
2307
2365
|
listBtn.onclick = () => this.togglePanel();
|
|
2308
2366
|
bar.append(brand, sep(), inspectBtn, regionBtn, listBtn);
|
|
2309
2367
|
return bar;
|
|
2310
2368
|
}
|
|
2369
|
+
/** A toolbar button with a uniform icon + label layout. `icon` may be an SVG string. */
|
|
2370
|
+
toolBtn(icon, label, role) {
|
|
2371
|
+
const b = el("button");
|
|
2372
|
+
b.dataset.role = role;
|
|
2373
|
+
b.title = label;
|
|
2374
|
+
b.setAttribute("aria-label", label);
|
|
2375
|
+
b.innerHTML = `<span class="ico">${icon}</span><span class="label">${label}</span>`;
|
|
2376
|
+
return b;
|
|
2377
|
+
}
|
|
2311
2378
|
drawSelection(curX, curY) {
|
|
2312
2379
|
const s = this.dragStart;
|
|
2313
2380
|
Object.assign(this.selbox.style, {
|
|
@@ -2460,6 +2527,8 @@ var LoupeApp = class {
|
|
|
2460
2527
|
offset,
|
|
2461
2528
|
region,
|
|
2462
2529
|
screenshot,
|
|
2530
|
+
// Record the screen the feedback was captured on (desktop / tablet / mobile).
|
|
2531
|
+
viewport: { w: window.innerWidth, h: window.innerHeight },
|
|
2463
2532
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2464
2533
|
};
|
|
2465
2534
|
await this.store.save(comment);
|
|
@@ -2546,6 +2615,10 @@ var LoupeApp = class {
|
|
|
2546
2615
|
this.setMode("off");
|
|
2547
2616
|
this.panel.classList.remove("open");
|
|
2548
2617
|
this.closeComposer();
|
|
2618
|
+
this.overlay.style.display = "none";
|
|
2619
|
+
} else {
|
|
2620
|
+
this.overlay.style.display = "";
|
|
2621
|
+
this.renderPins();
|
|
2549
2622
|
}
|
|
2550
2623
|
}
|
|
2551
2624
|
renderPanel() {
|
|
@@ -2571,6 +2644,12 @@ var LoupeApp = class {
|
|
|
2571
2644
|
const num = el("span", "num" + (c.status === "done" ? " done" : detached ? " detached" : ""), String(i + 1));
|
|
2572
2645
|
const who = el("span", "who", c.author.name);
|
|
2573
2646
|
top.append(num, who);
|
|
2647
|
+
const vw = c.viewport?.w;
|
|
2648
|
+
if (vw) {
|
|
2649
|
+
const kind = vw < 768 ? "mobile" : vw < 1024 ? "tablet" : "desktop";
|
|
2650
|
+
const icon = vw < 768 ? "\u{1F4F1}" : vw < 1024 ? "\u25A6" : "\u{1F5A5}";
|
|
2651
|
+
top.appendChild(el("span", "device", `${icon} ${kind}`));
|
|
2652
|
+
}
|
|
2574
2653
|
if (c.status === "done") top.appendChild(el("span", "badge done", "done"));
|
|
2575
2654
|
else if (detached) top.appendChild(el("span", "badge detached", "element moved/removed"));
|
|
2576
2655
|
item.appendChild(top);
|