@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/README.md +2 -1
- package/dist/index.global.js +114 -30
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +114 -30
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
-
.
|
|
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
|
|
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.
|
|
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
|
-
|
|
4313
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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
|
|
5732
|
-
* reflow.
|
|
5733
|
-
*
|
|
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 (
|
|
5739
|
-
const er =
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
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:
|
|
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
|
-
|
|
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";
|