@loupekit/sdk 0.11.0-next.76 → 0.11.1-next.77
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/README.md
CHANGED
|
@@ -108,7 +108,8 @@ launcher. The launcher carries the comment count; one tap reopens the panel, and
|
|
|
108
108
|
beside it expands the quick actions (pin a comment, drop a note, hide the markers, hide the
|
|
109
109
|
launcher). Drag the launcher to move it anywhere — the spot is remembered, and **Reset launcher
|
|
110
110
|
position** in the Settings menu puts it back. Hide it from a quick action, the Settings menu,
|
|
111
|
-
`Alt+Shift+L`, or `hideLauncher()` / `showLauncher()` when it covers something on your page.
|
|
111
|
+
`Alt+Shift+L`, or `hideLauncher()` / `showLauncher()` when it covers something on your page. On a
|
|
112
|
+
touch screen a slim tab at the right edge of the screen brings a hidden launcher back.
|
|
112
113
|
Call `destroy()` to tear it down. `init()` is idempotent — safe to call more
|
|
113
114
|
than once. Pass `label` to change the brand name shown in the header.
|
|
114
115
|
|
package/dist/index.global.js
CHANGED
|
@@ -124,6 +124,15 @@ var Loupe = (() => {
|
|
|
124
124
|
display: none; flex-direction: column; align-items: flex-end; gap: 10px;
|
|
125
125
|
}
|
|
126
126
|
.fab-cluster.show { display: flex; }
|
|
127
|
+
/* The way back on a touch screen once the launcher is hidden (no keyboard for Alt+Shift+L):
|
|
128
|
+
a slim tab on the right edge, shown only while the panel is closed. */
|
|
129
|
+
.fab-handle {
|
|
130
|
+
position: fixed; z-index: 2147483003; right: 0; bottom: 96px; width: 12px; height: 56px;
|
|
131
|
+
display: none; padding: 0; border: 0; border-radius: 8px 0 0 8px;
|
|
132
|
+
background: var(--accent); opacity: .6; cursor: pointer; touch-action: manipulation;
|
|
133
|
+
}
|
|
134
|
+
.fab-handle.show { display: block; }
|
|
135
|
+
.fab-handle:hover, .fab-handle:focus-visible { opacity: 1; }
|
|
127
136
|
/* A dragged launcher is anchored to its nearest edges (JS sets left/right/top/bottom),
|
|
128
137
|
and the quick actions grow INTO the page: downward from the upper half, labels to the
|
|
129
138
|
right from the left half. */
|
|
@@ -230,13 +239,15 @@ var Loupe = (() => {
|
|
|
230
239
|
.brand .logo { font-size: 16px; line-height: 1; color: var(--accent); flex: none; }
|
|
231
240
|
.brand .title { font-size: 13px; }
|
|
232
241
|
.dctl { display: flex; align-items: center; gap: 2px; margin-left: auto; }
|
|
233
|
-
.
|
|
242
|
+
/* Only the header's own icon buttons. The position and settings popovers live inside
|
|
243
|
+
.dctl too, and a bare .dctl button squeezed their rows and swatches to 26px. */
|
|
244
|
+
.dctl > button, .dctl > .menu-wrap > button {
|
|
234
245
|
display: inline-flex; align-items: center; justify-content: center;
|
|
235
246
|
width: 26px; height: 26px; padding: 0; border: 0; border-radius: 6px;
|
|
236
247
|
background: transparent; color: var(--muted); cursor: pointer;
|
|
237
248
|
}
|
|
238
|
-
.dctl button:hover { background: var(--bg-3); color: var(--ink); }
|
|
239
|
-
.dctl button.on { background: var(--bg-3); color: var(--accent); }
|
|
249
|
+
.dctl > button:hover, .dctl > .menu-wrap > button:hover { background: var(--bg-3); color: var(--ink); }
|
|
250
|
+
.dctl > button.on, .dctl > .menu-wrap > button.on { background: var(--bg-3); color: var(--accent); }
|
|
240
251
|
.dctl svg { display: block; }
|
|
241
252
|
.dctl .gap { width: 1px; height: 16px; background: var(--line); margin: 0 4px; flex: none; }
|
|
242
253
|
|
|
@@ -855,8 +866,10 @@ var Loupe = (() => {
|
|
|
855
866
|
|
|
856
867
|
/* ------------------------------------------------------------- companion chat */
|
|
857
868
|
/* Same inset as the Activity view \u2014 without it the composer and its Send button sit
|
|
858
|
-
flush against the panel's edge, which reads as a clipped button.
|
|
859
|
-
.chat-view
|
|
869
|
+
flush against the panel's edge, which reads as a clipped button. Scoped to .view.on:
|
|
870
|
+
a bare .chat-view display:flex outranked .view display:none by source
|
|
871
|
+
order, so the chat rendered under every other tab and swallowed their clicks. */
|
|
872
|
+
.view.on.chat-view { display: flex; flex-direction: column; gap: 8px; height: 100%; min-height: 0; padding: 10px 12px 16px; }
|
|
860
873
|
.chat-tray {
|
|
861
874
|
display: flex; flex-direction: column; gap: 4px; padding: 8px;
|
|
862
875
|
border: 1px solid var(--line); border-radius: 10px; background: var(--bg-2);
|
|
@@ -1276,6 +1289,7 @@ var Loupe = (() => {
|
|
|
1276
1289
|
function usable(el2) {
|
|
1277
1290
|
if (el2.id === "loupe-root" || el2.closest("#loupe-root")) return false;
|
|
1278
1291
|
if (el2 === document.body || el2 === document.documentElement) return false;
|
|
1292
|
+
if (typeof el2.checkVisibility === "function" && !el2.checkVisibility()) return false;
|
|
1279
1293
|
return true;
|
|
1280
1294
|
}
|
|
1281
1295
|
function byXPath(xp) {
|
|
@@ -3927,7 +3941,7 @@ var Loupe = (() => {
|
|
|
3927
3941
|
};
|
|
3928
3942
|
|
|
3929
3943
|
// src/app.ts
|
|
3930
|
-
var SDK_VERSION = true ? "0.11.
|
|
3944
|
+
var SDK_VERSION = true ? "0.11.1" : "dev";
|
|
3931
3945
|
var FAB_SIZE = 46;
|
|
3932
3946
|
var FAB_DRAG_THRESHOLD = 6;
|
|
3933
3947
|
var LAUNCHER_SHORTCUT = "Alt+Shift+L";
|
|
@@ -4077,6 +4091,8 @@ var Loupe = (() => {
|
|
|
4077
4091
|
this.scope = "page";
|
|
4078
4092
|
/** Every-page comments, fetched the first time the "All" scope is opened. */
|
|
4079
4093
|
this.allComments = [];
|
|
4094
|
+
/** True once the project-wide list has been read, so an empty project still shows 0, not ⋯. */
|
|
4095
|
+
this.allLoaded = false;
|
|
4080
4096
|
/** A clicked stat tile, which narrows the list. */
|
|
4081
4097
|
this.statFilter = "";
|
|
4082
4098
|
/** Repo filter, offered once the scope is "all". */
|
|
@@ -4256,6 +4272,20 @@ var Loupe = (() => {
|
|
|
4256
4272
|
e.preventDefault();
|
|
4257
4273
|
this.setLauncherHidden(!this.launcherHidden);
|
|
4258
4274
|
};
|
|
4275
|
+
/**
|
|
4276
|
+
* The header popovers (position, settings) close on a click anywhere. The shadow-root
|
|
4277
|
+
* click listener in buildDom() only sees clicks inside the widget, so a click on the
|
|
4278
|
+
* host page used to leave the menu open over it. Capture phase, so a host that stops
|
|
4279
|
+
* propagation on its own controls still dismisses the menu.
|
|
4280
|
+
*/
|
|
4281
|
+
this.onDocumentClick = (e) => {
|
|
4282
|
+
if (e.composedPath().includes(this.root)) return;
|
|
4283
|
+
this.closeMenus();
|
|
4284
|
+
};
|
|
4285
|
+
/** Escape closes an open popover, whether or not a tool is armed (onKey covers tools). */
|
|
4286
|
+
this.onMenuKey = (e) => {
|
|
4287
|
+
if (e.key === "Escape") this.closeMenus();
|
|
4288
|
+
};
|
|
4259
4289
|
// ---- region ("free-size screenshot") selection ----------------------------
|
|
4260
4290
|
this.onRegionDown = (e) => {
|
|
4261
4291
|
if (this.mode !== "region" && this.mode !== "record") return;
|
|
@@ -4346,8 +4376,10 @@ var Loupe = (() => {
|
|
|
4346
4376
|
const box = this.regionRect(c, elx);
|
|
4347
4377
|
if (box) {
|
|
4348
4378
|
const onScreen = box.x + box.w > 0 && box.x < window.innerWidth && box.y + box.h > 0 && box.y < window.innerHeight;
|
|
4349
|
-
|
|
4350
|
-
|
|
4379
|
+
const px = box.x < 4 ? Math.min(4, box.x + box.w - 30) : box.x;
|
|
4380
|
+
const py = box.y < 4 ? Math.min(4, box.y + box.h - 30) : box.y;
|
|
4381
|
+
Object.assign(pin.style, { left: px + "px", top: py + "px", display: onScreen ? "grid" : "none" });
|
|
4382
|
+
pin.classList.toggle("detached", !(elx && relUsable(c.region?.rel)) && c.anchor.tag !== "region");
|
|
4351
4383
|
} else {
|
|
4352
4384
|
pin.style.display = "none";
|
|
4353
4385
|
pin.classList.add("detached");
|
|
@@ -4358,7 +4390,7 @@ var Loupe = (() => {
|
|
|
4358
4390
|
const rect = elx.getBoundingClientRect();
|
|
4359
4391
|
const px = rect.left + c.offset.x * rect.width;
|
|
4360
4392
|
const py = rect.top + c.offset.y * rect.height;
|
|
4361
|
-
const onScreen = rect.bottom > 0 && rect.top < window.innerHeight && rect.width > 0;
|
|
4393
|
+
const onScreen = rect.bottom > 0 && rect.top < window.innerHeight && rect.right > 0 && rect.left < window.innerWidth && rect.width > 0;
|
|
4362
4394
|
Object.assign(pin.style, { left: px + "px", top: py + "px", display: onScreen ? "grid" : "none" });
|
|
4363
4395
|
pin.classList.remove("detached");
|
|
4364
4396
|
} else {
|
|
@@ -4458,6 +4490,8 @@ var Loupe = (() => {
|
|
|
4458
4490
|
this.loadProject();
|
|
4459
4491
|
this.buildDom();
|
|
4460
4492
|
document.addEventListener("keydown", this.onLauncherKey);
|
|
4493
|
+
document.addEventListener("click", this.onDocumentClick, true);
|
|
4494
|
+
document.addEventListener("keydown", this.onMenuKey);
|
|
4461
4495
|
if (this.cfg.autoOpen) this.open = true;
|
|
4462
4496
|
this.applyDockLayout();
|
|
4463
4497
|
this.lastUrl = this.url;
|
|
@@ -4469,7 +4503,7 @@ var Loupe = (() => {
|
|
|
4469
4503
|
this.startLiveThreads();
|
|
4470
4504
|
this.startCompanionStream();
|
|
4471
4505
|
void this.loadNotifications();
|
|
4472
|
-
|
|
4506
|
+
void this.loadAllComments();
|
|
4473
4507
|
this.observe();
|
|
4474
4508
|
this.watchNavigation();
|
|
4475
4509
|
if (!this.tourDone && this.open && !this.isMobile()) this.startTour();
|
|
@@ -4530,6 +4564,11 @@ var Loupe = (() => {
|
|
|
4530
4564
|
this.shadow.appendChild(this.composer);
|
|
4531
4565
|
this.dock = this.buildDock();
|
|
4532
4566
|
this.fabCluster = this.buildFabCluster();
|
|
4567
|
+
this.fabHandle = el("button", "fab-handle");
|
|
4568
|
+
this.fabHandle.type = "button";
|
|
4569
|
+
this.fabHandle.title = "Show the launcher";
|
|
4570
|
+
this.fabHandle.setAttribute("aria-label", "Show the launcher");
|
|
4571
|
+
this.fabHandle.onclick = () => this.setLauncherHidden(false);
|
|
4533
4572
|
this.recBar = this.buildRecBar();
|
|
4534
4573
|
this.toastEl = el("div", "toast");
|
|
4535
4574
|
this.toastEl.setAttribute("role", "status");
|
|
@@ -4539,7 +4578,7 @@ var Loupe = (() => {
|
|
|
4539
4578
|
this.tourSpot = el("div", "tour-spot");
|
|
4540
4579
|
this.tourCard = el("div", "tour-card");
|
|
4541
4580
|
this.tourEl.append(this.tourSpot, this.tourCard);
|
|
4542
|
-
this.shadow.append(this.dock, this.fabCluster, this.recBar, this.toastEl, this.tourEl);
|
|
4581
|
+
this.shadow.append(this.dock, this.fabCluster, this.fabHandle, this.recBar, this.toastEl, this.tourEl);
|
|
4543
4582
|
this.shadow.addEventListener("click", (e) => {
|
|
4544
4583
|
const t = e.target;
|
|
4545
4584
|
if (t?.closest && t.closest(".menu-wrap")) return;
|
|
@@ -5239,6 +5278,7 @@ var Loupe = (() => {
|
|
|
5239
5278
|
async loadAllComments() {
|
|
5240
5279
|
try {
|
|
5241
5280
|
this.allComments = await this.store.listAll(this.cfg.projectKey);
|
|
5281
|
+
this.allLoaded = true;
|
|
5242
5282
|
this.renderHome();
|
|
5243
5283
|
this.renderList();
|
|
5244
5284
|
} catch {
|
|
@@ -5279,7 +5319,7 @@ var Loupe = (() => {
|
|
|
5279
5319
|
};
|
|
5280
5320
|
});
|
|
5281
5321
|
}
|
|
5282
|
-
const allKnown = this.
|
|
5322
|
+
const allKnown = this.allLoaded || this.scope === "all";
|
|
5283
5323
|
const counts = {
|
|
5284
5324
|
page: String(this.comments.length),
|
|
5285
5325
|
all: allKnown ? String(this.allComments.length) : "\u22EF"
|
|
@@ -5598,7 +5638,12 @@ var Loupe = (() => {
|
|
|
5598
5638
|
};
|
|
5599
5639
|
const markers = mini("markers", I_EYE, "Markers", "Show or hide the markers on this page");
|
|
5600
5640
|
markers.onclick = () => this.toggleMarkers();
|
|
5601
|
-
const hide = mini(
|
|
5641
|
+
const hide = mini(
|
|
5642
|
+
"hide",
|
|
5643
|
+
I_EYE_OFF,
|
|
5644
|
+
"Hide launcher",
|
|
5645
|
+
isTouchDevice() ? "Hide this launcher \u2014 the tab at the screen edge brings it back" : `Hide this launcher \u2014 ${LAUNCHER_SHORTCUT} brings it back`
|
|
5646
|
+
);
|
|
5602
5647
|
hide.onclick = () => this.setLauncherHidden(true);
|
|
5603
5648
|
minis.append(comment, note, markers, hide);
|
|
5604
5649
|
if (this.tabList.some((t) => t.id === "connect")) {
|
|
@@ -5701,7 +5746,8 @@ var Loupe = (() => {
|
|
|
5701
5746
|
this.applyDockLayout();
|
|
5702
5747
|
this.renderSettings();
|
|
5703
5748
|
const label = this.cfg.label ?? "Loupe";
|
|
5704
|
-
|
|
5749
|
+
const back = isTouchDevice() ? "tap the tab at the right edge of the screen" : `press <kbd>${LAUNCHER_SHORTCUT}</kbd>`;
|
|
5750
|
+
this.toast(hidden ? `${escapeHtml(label)} launcher hidden \u2014 ${back} to bring it back` : `${escapeHtml(label)} launcher is back`);
|
|
5705
5751
|
}
|
|
5706
5752
|
/** Public seam for the host (`Loupe.showLauncher()`). */
|
|
5707
5753
|
showLauncher() {
|
|
@@ -5765,26 +5811,28 @@ var Loupe = (() => {
|
|
|
5765
5811
|
document.removeEventListener("pointercancel", this.onRegionCancel, true);
|
|
5766
5812
|
}
|
|
5767
5813
|
/**
|
|
5768
|
-
* Anchor a dragged viewport rect to the element
|
|
5769
|
-
* reflow.
|
|
5770
|
-
*
|
|
5814
|
+
* Anchor a dragged viewport rect to the smallest element that covers most of it, so it
|
|
5815
|
+
* survives reflow. The rule used to take the element under the region's CENTER, which
|
|
5816
|
+
* for a large region is some small control inside it: the stored fractions then ran to
|
|
5817
|
+
* −7 and +15, and on any other viewport the pin landed far from the area. `rel`
|
|
5818
|
+
* (element-relative fractions) is preferred when an anchor is found; document coords
|
|
5819
|
+
* are the fallback. Shared by both the Region (screenshot) and Record (video) tools.
|
|
5771
5820
|
*/
|
|
5772
5821
|
regionFromViewport(vp) {
|
|
5773
5822
|
const centerEl = this.pick(vp.x + vp.w / 2, vp.y + vp.h / 2);
|
|
5823
|
+
const anchorEl = regionContainer2(centerEl, vp);
|
|
5774
5824
|
let rel;
|
|
5775
|
-
if (
|
|
5776
|
-
const er =
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
};
|
|
5784
|
-
}
|
|
5825
|
+
if (anchorEl) {
|
|
5826
|
+
const er = anchorEl.getBoundingClientRect();
|
|
5827
|
+
rel = {
|
|
5828
|
+
fx: (vp.x - er.left) / er.width,
|
|
5829
|
+
fy: (vp.y - er.top) / er.height,
|
|
5830
|
+
fw: vp.w / er.width,
|
|
5831
|
+
fh: vp.h / er.height
|
|
5832
|
+
};
|
|
5785
5833
|
}
|
|
5786
5834
|
const region = { x: vp.x + window.scrollX, y: vp.y + window.scrollY, w: vp.w, h: vp.h, rel };
|
|
5787
|
-
return { region, element:
|
|
5835
|
+
return { region, element: anchorEl };
|
|
5788
5836
|
}
|
|
5789
5837
|
/**
|
|
5790
5838
|
* Touch path for the Region tool: capture what is on screen right now and open the
|
|
@@ -6205,7 +6253,7 @@ var Loupe = (() => {
|
|
|
6205
6253
|
*/
|
|
6206
6254
|
regionRect(c, elx) {
|
|
6207
6255
|
const rel = c.region?.rel;
|
|
6208
|
-
if (elx && rel) {
|
|
6256
|
+
if (elx && relUsable(rel)) {
|
|
6209
6257
|
const r = elx.getBoundingClientRect();
|
|
6210
6258
|
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 };
|
|
6211
6259
|
}
|
|
@@ -6535,6 +6583,7 @@ var Loupe = (() => {
|
|
|
6535
6583
|
}
|
|
6536
6584
|
d.querySelectorAll(".dctl [data-dock]").forEach((b) => b.classList.toggle("on", b.dataset.dock === this.dockMode));
|
|
6537
6585
|
this.fabCluster.classList.toggle("show", !this.open && !this.launcherHidden);
|
|
6586
|
+
this.fabHandle.classList.toggle("show", !this.open && this.launcherHidden && isTouchDevice());
|
|
6538
6587
|
this.applyFabPosition();
|
|
6539
6588
|
this.applyFab();
|
|
6540
6589
|
this.pushPage();
|
|
@@ -7807,6 +7856,8 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
7807
7856
|
window.removeEventListener("pointercancel", this.onFabPointerUp);
|
|
7808
7857
|
window.clearTimeout(this.toastTimer);
|
|
7809
7858
|
document.removeEventListener("keydown", this.onLauncherKey);
|
|
7859
|
+
document.removeEventListener("click", this.onDocumentClick, true);
|
|
7860
|
+
document.removeEventListener("keydown", this.onMenuKey);
|
|
7810
7861
|
document.removeEventListener("keydown", this.onKey, true);
|
|
7811
7862
|
const de = document.documentElement;
|
|
7812
7863
|
de.style.marginLeft = de.style.marginRight = de.style.marginBottom = "";
|
|
@@ -7987,7 +8038,40 @@ ${c.body}` : c.body)}</div>` + (c.context?.html ? `<pre class="or-code">${escape
|
|
|
7987
8038
|
}
|
|
7988
8039
|
function describeAnchor(c) {
|
|
7989
8040
|
if (c.kind === "free") return "Free note \xB7 page-level";
|
|
7990
|
-
|
|
8041
|
+
if (c.kind === "region" && c.region) {
|
|
8042
|
+
const size = `${Math.round(c.region.w)}\xD7${Math.round(c.region.h)}`;
|
|
8043
|
+
if (c.anchor.tag === "region") return `Area ${size} \xB7 page-level`;
|
|
8044
|
+
return `Area ${size} \xB7 ${relUsable(c.region.rel) ? "in" : "near"} ${anchorLabel(c.anchor)}`;
|
|
8045
|
+
}
|
|
8046
|
+
return anchorLabel(c.anchor);
|
|
8047
|
+
}
|
|
8048
|
+
function anchorLabel(a) {
|
|
8049
|
+
const tag = a.tag || "element";
|
|
8050
|
+
if (a.testid) return `${tag}[data-testid="${a.testid}"]`;
|
|
8051
|
+
const text = (a.text || "").replace(/\s+/g, " ").trim();
|
|
8052
|
+
if (text) return `${tag} \xB7 \u201C${text.length > 32 ? text.slice(0, 32).trimEnd() + "\u2026" : text}\u201D`;
|
|
8053
|
+
const attrs = a.attrs ?? {};
|
|
8054
|
+
const named = attrs["aria-label"] || attrs.title || attrs.placeholder || attrs.alt || attrs.name;
|
|
8055
|
+
if (named) return `${tag} \xB7 \u201C${named}\u201D`;
|
|
8056
|
+
return a.cssPath || tag;
|
|
8057
|
+
}
|
|
8058
|
+
var REGION_COVER_MIN = 0.6;
|
|
8059
|
+
function regionContainer2(from, vp) {
|
|
8060
|
+
const area = vp.w * vp.h;
|
|
8061
|
+
if (area <= 0) return null;
|
|
8062
|
+
for (let n = from; n && n !== document.body && n !== document.documentElement; n = n.parentElement) {
|
|
8063
|
+
const r = n.getBoundingClientRect();
|
|
8064
|
+
if (r.width <= 0 || r.height <= 0) continue;
|
|
8065
|
+
const ix = Math.max(0, Math.min(r.right, vp.x + vp.w) - Math.max(r.left, vp.x));
|
|
8066
|
+
const iy = Math.max(0, Math.min(r.bottom, vp.y + vp.h) - Math.max(r.top, vp.y));
|
|
8067
|
+
if (ix * iy / area >= REGION_COVER_MIN) return n;
|
|
8068
|
+
}
|
|
8069
|
+
return null;
|
|
8070
|
+
}
|
|
8071
|
+
function relUsable(rel) {
|
|
8072
|
+
if (!rel) return false;
|
|
8073
|
+
const lo = -1.5, hi = 2.5;
|
|
8074
|
+
return rel.fw > 0 && rel.fh > 0 && rel.fx > lo && rel.fy > lo && rel.fx + rel.fw < hi && rel.fy + rel.fh < hi;
|
|
7991
8075
|
}
|
|
7992
8076
|
function isResolved(c) {
|
|
7993
8077
|
return normalizeStatus(c.status) === "resolved";
|