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