@loupekit/sdk 0.11.0 → 0.11.1

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
@@ -87,6 +87,15 @@ var STYLES = (
87
87
  display: none; flex-direction: column; align-items: flex-end; gap: 10px;
88
88
  }
89
89
  .fab-cluster.show { display: flex; }
90
+ /* The way back on a touch screen once the launcher is hidden (no keyboard for Alt+Shift+L):
91
+ a slim tab on the right edge, shown only while the panel is closed. */
92
+ .fab-handle {
93
+ position: fixed; z-index: 2147483003; right: 0; bottom: 96px; width: 12px; height: 56px;
94
+ display: none; padding: 0; border: 0; border-radius: 8px 0 0 8px;
95
+ background: var(--accent); opacity: .6; cursor: pointer; touch-action: manipulation;
96
+ }
97
+ .fab-handle.show { display: block; }
98
+ .fab-handle:hover, .fab-handle:focus-visible { opacity: 1; }
90
99
  /* A dragged launcher is anchored to its nearest edges (JS sets left/right/top/bottom),
91
100
  and the quick actions grow INTO the page: downward from the upper half, labels to the
92
101
  right from the left half. */
@@ -193,13 +202,15 @@ var STYLES = (
193
202
  .brand .logo { font-size: 16px; line-height: 1; color: var(--accent); flex: none; }
194
203
  .brand .title { font-size: 13px; }
195
204
  .dctl { display: flex; align-items: center; gap: 2px; margin-left: auto; }
196
- .dctl button {
205
+ /* Only the header's own icon buttons. The position and settings popovers live inside
206
+ .dctl too, and a bare .dctl button squeezed their rows and swatches to 26px. */
207
+ .dctl > button, .dctl > .menu-wrap > button {
197
208
  display: inline-flex; align-items: center; justify-content: center;
198
209
  width: 26px; height: 26px; padding: 0; border: 0; border-radius: 6px;
199
210
  background: transparent; color: var(--muted); cursor: pointer;
200
211
  }
201
- .dctl button:hover { background: var(--bg-3); color: var(--ink); }
202
- .dctl button.on { background: var(--bg-3); color: var(--accent); }
212
+ .dctl > button:hover, .dctl > .menu-wrap > button:hover { background: var(--bg-3); color: var(--ink); }
213
+ .dctl > button.on, .dctl > .menu-wrap > button.on { background: var(--bg-3); color: var(--accent); }
203
214
  .dctl svg { display: block; }
204
215
  .dctl .gap { width: 1px; height: 16px; background: var(--line); margin: 0 4px; flex: none; }
205
216
 
@@ -818,8 +829,10 @@ var STYLES = (
818
829
 
819
830
  /* ------------------------------------------------------------- companion chat */
820
831
  /* Same inset as the Activity view \u2014 without it the composer and its Send button sit
821
- flush against the panel's edge, which reads as a clipped button. */
822
- .chat-view { display: flex; flex-direction: column; gap: 8px; height: 100%; min-height: 0; padding: 10px 12px 16px; }
832
+ flush against the panel's edge, which reads as a clipped button. Scoped to .view.on:
833
+ a bare .chat-view display:flex outranked .view display:none by source
834
+ order, so the chat rendered under every other tab and swallowed their clicks. */
835
+ .view.on.chat-view { display: flex; flex-direction: column; gap: 8px; height: 100%; min-height: 0; padding: 10px 12px 16px; }
823
836
  .chat-tray {
824
837
  display: flex; flex-direction: column; gap: 4px; padding: 8px;
825
838
  border: 1px solid var(--line); border-radius: 10px; background: var(--bg-2);
@@ -1239,6 +1252,7 @@ function normText(t) {
1239
1252
  function usable(el2) {
1240
1253
  if (el2.id === "loupe-root" || el2.closest("#loupe-root")) return false;
1241
1254
  if (el2 === document.body || el2 === document.documentElement) return false;
1255
+ if (typeof el2.checkVisibility === "function" && !el2.checkVisibility()) return false;
1242
1256
  return true;
1243
1257
  }
1244
1258
  function byXPath(xp) {
@@ -3890,7 +3904,7 @@ var HttpAdapter = class {
3890
3904
  };
3891
3905
 
3892
3906
  // src/app.ts
3893
- var SDK_VERSION = true ? "0.11.0" : "dev";
3907
+ var SDK_VERSION = true ? "0.11.1" : "dev";
3894
3908
  var FAB_SIZE = 46;
3895
3909
  var FAB_DRAG_THRESHOLD = 6;
3896
3910
  var LAUNCHER_SHORTCUT = "Alt+Shift+L";
@@ -4040,6 +4054,8 @@ var LoupeApp = class {
4040
4054
  this.scope = "page";
4041
4055
  /** Every-page comments, fetched the first time the "All" scope is opened. */
4042
4056
  this.allComments = [];
4057
+ /** True once the project-wide list has been read, so an empty project still shows 0, not ⋯. */
4058
+ this.allLoaded = false;
4043
4059
  /** A clicked stat tile, which narrows the list. */
4044
4060
  this.statFilter = "";
4045
4061
  /** Repo filter, offered once the scope is "all". */
@@ -4219,6 +4235,20 @@ var LoupeApp = class {
4219
4235
  e.preventDefault();
4220
4236
  this.setLauncherHidden(!this.launcherHidden);
4221
4237
  };
4238
+ /**
4239
+ * The header popovers (position, settings) close on a click anywhere. The shadow-root
4240
+ * click listener in buildDom() only sees clicks inside the widget, so a click on the
4241
+ * host page used to leave the menu open over it. Capture phase, so a host that stops
4242
+ * propagation on its own controls still dismisses the menu.
4243
+ */
4244
+ this.onDocumentClick = (e) => {
4245
+ if (e.composedPath().includes(this.root)) return;
4246
+ this.closeMenus();
4247
+ };
4248
+ /** Escape closes an open popover, whether or not a tool is armed (onKey covers tools). */
4249
+ this.onMenuKey = (e) => {
4250
+ if (e.key === "Escape") this.closeMenus();
4251
+ };
4222
4252
  // ---- region ("free-size screenshot") selection ----------------------------
4223
4253
  this.onRegionDown = (e) => {
4224
4254
  if (this.mode !== "region" && this.mode !== "record") return;
@@ -4309,8 +4339,10 @@ var LoupeApp = class {
4309
4339
  const box = this.regionRect(c, elx);
4310
4340
  if (box) {
4311
4341
  const onScreen = box.x + box.w > 0 && box.x < window.innerWidth && box.y + box.h > 0 && box.y < window.innerHeight;
4312
- Object.assign(pin.style, { left: box.x + "px", top: box.y + "px", display: onScreen ? "grid" : "none" });
4313
- pin.classList.toggle("detached", !elx && !c.region?.rel);
4342
+ const px = box.x < 4 ? Math.min(4, box.x + box.w - 30) : box.x;
4343
+ const py = box.y < 4 ? Math.min(4, box.y + box.h - 30) : box.y;
4344
+ Object.assign(pin.style, { left: px + "px", top: py + "px", display: onScreen ? "grid" : "none" });
4345
+ pin.classList.toggle("detached", !(elx && relUsable(c.region?.rel)) && c.anchor.tag !== "region");
4314
4346
  } else {
4315
4347
  pin.style.display = "none";
4316
4348
  pin.classList.add("detached");
@@ -4321,7 +4353,7 @@ var LoupeApp = class {
4321
4353
  const rect = elx.getBoundingClientRect();
4322
4354
  const px = rect.left + c.offset.x * rect.width;
4323
4355
  const py = rect.top + c.offset.y * rect.height;
4324
- const onScreen = rect.bottom > 0 && rect.top < window.innerHeight && rect.width > 0;
4356
+ const onScreen = rect.bottom > 0 && rect.top < window.innerHeight && rect.right > 0 && rect.left < window.innerWidth && rect.width > 0;
4325
4357
  Object.assign(pin.style, { left: px + "px", top: py + "px", display: onScreen ? "grid" : "none" });
4326
4358
  pin.classList.remove("detached");
4327
4359
  } else {
@@ -4421,6 +4453,8 @@ var LoupeApp = class {
4421
4453
  this.loadProject();
4422
4454
  this.buildDom();
4423
4455
  document.addEventListener("keydown", this.onLauncherKey);
4456
+ document.addEventListener("click", this.onDocumentClick, true);
4457
+ document.addEventListener("keydown", this.onMenuKey);
4424
4458
  if (this.cfg.autoOpen) this.open = true;
4425
4459
  this.applyDockLayout();
4426
4460
  this.lastUrl = this.url;
@@ -4432,7 +4466,7 @@ var LoupeApp = class {
4432
4466
  this.startLiveThreads();
4433
4467
  this.startCompanionStream();
4434
4468
  void this.loadNotifications();
4435
- if (this.scope === "all") void this.loadAllComments();
4469
+ void this.loadAllComments();
4436
4470
  this.observe();
4437
4471
  this.watchNavigation();
4438
4472
  if (!this.tourDone && this.open && !this.isMobile()) this.startTour();
@@ -4493,6 +4527,11 @@ var LoupeApp = class {
4493
4527
  this.shadow.appendChild(this.composer);
4494
4528
  this.dock = this.buildDock();
4495
4529
  this.fabCluster = this.buildFabCluster();
4530
+ this.fabHandle = el("button", "fab-handle");
4531
+ this.fabHandle.type = "button";
4532
+ this.fabHandle.title = "Show the launcher";
4533
+ this.fabHandle.setAttribute("aria-label", "Show the launcher");
4534
+ this.fabHandle.onclick = () => this.setLauncherHidden(false);
4496
4535
  this.recBar = this.buildRecBar();
4497
4536
  this.toastEl = el("div", "toast");
4498
4537
  this.toastEl.setAttribute("role", "status");
@@ -4502,7 +4541,7 @@ var LoupeApp = class {
4502
4541
  this.tourSpot = el("div", "tour-spot");
4503
4542
  this.tourCard = el("div", "tour-card");
4504
4543
  this.tourEl.append(this.tourSpot, this.tourCard);
4505
- this.shadow.append(this.dock, this.fabCluster, this.recBar, this.toastEl, this.tourEl);
4544
+ this.shadow.append(this.dock, this.fabCluster, this.fabHandle, this.recBar, this.toastEl, this.tourEl);
4506
4545
  this.shadow.addEventListener("click", (e) => {
4507
4546
  const t = e.target;
4508
4547
  if (t?.closest && t.closest(".menu-wrap")) return;
@@ -5202,6 +5241,7 @@ var LoupeApp = class {
5202
5241
  async loadAllComments() {
5203
5242
  try {
5204
5243
  this.allComments = await this.store.listAll(this.cfg.projectKey);
5244
+ this.allLoaded = true;
5205
5245
  this.renderHome();
5206
5246
  this.renderList();
5207
5247
  } catch {
@@ -5242,7 +5282,7 @@ var LoupeApp = class {
5242
5282
  };
5243
5283
  });
5244
5284
  }
5245
- const allKnown = this.allComments.length > 0 || this.scope === "all";
5285
+ const allKnown = this.allLoaded || this.scope === "all";
5246
5286
  const counts = {
5247
5287
  page: String(this.comments.length),
5248
5288
  all: allKnown ? String(this.allComments.length) : "\u22EF"
@@ -5561,7 +5601,12 @@ var LoupeApp = class {
5561
5601
  };
5562
5602
  const markers = mini("markers", I_EYE, "Markers", "Show or hide the markers on this page");
5563
5603
  markers.onclick = () => this.toggleMarkers();
5564
- const hide = mini("hide", I_EYE_OFF, "Hide launcher", `Hide this launcher \u2014 ${LAUNCHER_SHORTCUT} brings it back`);
5604
+ const hide = mini(
5605
+ "hide",
5606
+ I_EYE_OFF,
5607
+ "Hide launcher",
5608
+ isTouchDevice() ? "Hide this launcher \u2014 the tab at the screen edge brings it back" : `Hide this launcher \u2014 ${LAUNCHER_SHORTCUT} brings it back`
5609
+ );
5565
5610
  hide.onclick = () => this.setLauncherHidden(true);
5566
5611
  minis.append(comment, note, markers, hide);
5567
5612
  if (this.tabList.some((t) => t.id === "connect")) {
@@ -5664,7 +5709,8 @@ var LoupeApp = class {
5664
5709
  this.applyDockLayout();
5665
5710
  this.renderSettings();
5666
5711
  const label = this.cfg.label ?? "Loupe";
5667
- this.toast(hidden ? `${escapeHtml(label)} launcher hidden \u2014 press <kbd>${LAUNCHER_SHORTCUT}</kbd> to bring it back` : `${escapeHtml(label)} launcher is back`);
5712
+ const back = isTouchDevice() ? "tap the tab at the right edge of the screen" : `press <kbd>${LAUNCHER_SHORTCUT}</kbd>`;
5713
+ this.toast(hidden ? `${escapeHtml(label)} launcher hidden \u2014 ${back} to bring it back` : `${escapeHtml(label)} launcher is back`);
5668
5714
  }
5669
5715
  /** Public seam for the host (`Loupe.showLauncher()`). */
5670
5716
  showLauncher() {
@@ -5728,26 +5774,28 @@ var LoupeApp = class {
5728
5774
  document.removeEventListener("pointercancel", this.onRegionCancel, true);
5729
5775
  }
5730
5776
  /**
5731
- * Anchor a dragged viewport rect to the element under its center so it survives
5732
- * reflow. `rel` (element-relative fractions) is preferred; document coords are the
5733
- * fallback. Shared by both the Region (screenshot) and Record (video) tools.
5777
+ * Anchor a dragged viewport rect to the smallest element that covers most of it, so it
5778
+ * survives reflow. The rule used to take the element under the region's CENTER, which
5779
+ * for a large region is some small control inside it: the stored fractions then ran to
5780
+ * −7 and +15, and on any other viewport the pin landed far from the area. `rel`
5781
+ * (element-relative fractions) is preferred when an anchor is found; document coords
5782
+ * are the fallback. Shared by both the Region (screenshot) and Record (video) tools.
5734
5783
  */
5735
5784
  regionFromViewport(vp) {
5736
5785
  const centerEl = this.pick(vp.x + vp.w / 2, vp.y + vp.h / 2);
5786
+ const anchorEl = regionContainer2(centerEl, vp);
5737
5787
  let rel;
5738
- if (centerEl) {
5739
- const er = centerEl.getBoundingClientRect();
5740
- if (er.width > 0 && er.height > 0) {
5741
- rel = {
5742
- fx: (vp.x - er.left) / er.width,
5743
- fy: (vp.y - er.top) / er.height,
5744
- fw: vp.w / er.width,
5745
- fh: vp.h / er.height
5746
- };
5747
- }
5788
+ if (anchorEl) {
5789
+ const er = anchorEl.getBoundingClientRect();
5790
+ rel = {
5791
+ fx: (vp.x - er.left) / er.width,
5792
+ fy: (vp.y - er.top) / er.height,
5793
+ fw: vp.w / er.width,
5794
+ fh: vp.h / er.height
5795
+ };
5748
5796
  }
5749
5797
  const region = { x: vp.x + window.scrollX, y: vp.y + window.scrollY, w: vp.w, h: vp.h, rel };
5750
- return { region, element: centerEl };
5798
+ return { region, element: anchorEl };
5751
5799
  }
5752
5800
  /**
5753
5801
  * Touch path for the Region tool: capture what is on screen right now and open the
@@ -6168,7 +6216,7 @@ var LoupeApp = class {
6168
6216
  */
6169
6217
  regionRect(c, elx) {
6170
6218
  const rel = c.region?.rel;
6171
- if (elx && rel) {
6219
+ if (elx && relUsable(rel)) {
6172
6220
  const r = elx.getBoundingClientRect();
6173
6221
  return { x: r.left + rel.fx * r.width, y: r.top + rel.fy * r.height, w: rel.fw * r.width, h: rel.fh * r.height };
6174
6222
  }
@@ -6498,6 +6546,7 @@ var LoupeApp = class {
6498
6546
  }
6499
6547
  d.querySelectorAll(".dctl [data-dock]").forEach((b) => b.classList.toggle("on", b.dataset.dock === this.dockMode));
6500
6548
  this.fabCluster.classList.toggle("show", !this.open && !this.launcherHidden);
6549
+ this.fabHandle.classList.toggle("show", !this.open && this.launcherHidden && isTouchDevice());
6501
6550
  this.applyFabPosition();
6502
6551
  this.applyFab();
6503
6552
  this.pushPage();
@@ -7770,6 +7819,8 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
7770
7819
  window.removeEventListener("pointercancel", this.onFabPointerUp);
7771
7820
  window.clearTimeout(this.toastTimer);
7772
7821
  document.removeEventListener("keydown", this.onLauncherKey);
7822
+ document.removeEventListener("click", this.onDocumentClick, true);
7823
+ document.removeEventListener("keydown", this.onMenuKey);
7773
7824
  document.removeEventListener("keydown", this.onKey, true);
7774
7825
  const de = document.documentElement;
7775
7826
  de.style.marginLeft = de.style.marginRight = de.style.marginBottom = "";
@@ -7950,7 +8001,40 @@ function describe(elx) {
7950
8001
  }
7951
8002
  function describeAnchor(c) {
7952
8003
  if (c.kind === "free") return "Free note \xB7 page-level";
7953
- return c.anchor.testid ? `[data-testid="${c.anchor.testid}"]` : c.anchor.cssPath;
8004
+ if (c.kind === "region" && c.region) {
8005
+ const size = `${Math.round(c.region.w)}\xD7${Math.round(c.region.h)}`;
8006
+ if (c.anchor.tag === "region") return `Area ${size} \xB7 page-level`;
8007
+ return `Area ${size} \xB7 ${relUsable(c.region.rel) ? "in" : "near"} ${anchorLabel(c.anchor)}`;
8008
+ }
8009
+ return anchorLabel(c.anchor);
8010
+ }
8011
+ function anchorLabel(a) {
8012
+ const tag = a.tag || "element";
8013
+ if (a.testid) return `${tag}[data-testid="${a.testid}"]`;
8014
+ const text = (a.text || "").replace(/\s+/g, " ").trim();
8015
+ if (text) return `${tag} \xB7 \u201C${text.length > 32 ? text.slice(0, 32).trimEnd() + "\u2026" : text}\u201D`;
8016
+ const attrs = a.attrs ?? {};
8017
+ const named = attrs["aria-label"] || attrs.title || attrs.placeholder || attrs.alt || attrs.name;
8018
+ if (named) return `${tag} \xB7 \u201C${named}\u201D`;
8019
+ return a.cssPath || tag;
8020
+ }
8021
+ var REGION_COVER_MIN = 0.6;
8022
+ function regionContainer2(from, vp) {
8023
+ const area = vp.w * vp.h;
8024
+ if (area <= 0) return null;
8025
+ for (let n = from; n && n !== document.body && n !== document.documentElement; n = n.parentElement) {
8026
+ const r = n.getBoundingClientRect();
8027
+ if (r.width <= 0 || r.height <= 0) continue;
8028
+ const ix = Math.max(0, Math.min(r.right, vp.x + vp.w) - Math.max(r.left, vp.x));
8029
+ const iy = Math.max(0, Math.min(r.bottom, vp.y + vp.h) - Math.max(r.top, vp.y));
8030
+ if (ix * iy / area >= REGION_COVER_MIN) return n;
8031
+ }
8032
+ return null;
8033
+ }
8034
+ function relUsable(rel) {
8035
+ if (!rel) return false;
8036
+ const lo = -1.5, hi = 2.5;
8037
+ return rel.fw > 0 && rel.fh > 0 && rel.fx > lo && rel.fy > lo && rel.fx + rel.fw < hi && rel.fy + rel.fh < hi;
7954
8038
  }
7955
8039
  function isResolved(c) {
7956
8040
  return normalizeStatus(c.status) === "resolved";