@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.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 button .ico { flex: none; display: block; }
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; padding: 8px; border-radius: 8px; letter-spacing: -.01em;
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 full = await domToPng(document.body, {
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, bodyRect.left, bodyRect.top, scale, redact);
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", "\u25CE Loupe");
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 = el("button", "", "\u271B Inspect & comment");
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 = el("button");
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 = el("button", "", "\u2630 Comments");
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);