@loupekit/sdk 0.4.3-next.15 → 0.5.0-next.16

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
@@ -59,6 +59,8 @@ var STYLES = (
59
59
  .pin:hover { transform: translate(-4px, -4px) scale(1.12); }
60
60
  .pin.detached { background: #9aa0af; }
61
61
  .pin.done { background: #10935a; }
62
+ .pin.free { background: var(--accent); border-radius: 50% 50% 2px 50%; }
63
+ .pin.free.done { background: #10935a; }
62
64
  .pin.active { outline: 3px solid rgba(74,85,214,.4); }
63
65
 
64
66
  /* toolbar */
@@ -85,11 +87,24 @@ var STYLES = (
85
87
  .toolbar button.on { background: var(--accent); color: #fff; }
86
88
  .toolbar .sep { width: 1px; height: 20px; background: #333846; margin: 0 2px; }
87
89
  .toolbar .brand {
88
- font-weight: 700; letter-spacing: -.01em; cursor: pointer; user-select: none;
90
+ font-weight: 700; letter-spacing: -.01em; cursor: grab; user-select: none; touch-action: none;
89
91
  }
92
+ .toolbar.dragging { transition: none; box-shadow: 0 12px 40px rgba(0,0,0,.5); }
93
+ .toolbar.dragging .brand { cursor: grabbing; }
90
94
  /* collapsed \u2192 show only the brand/logo; click it again to expand */
91
95
  .toolbar.collapsed { gap: 0; }
92
96
  .toolbar.collapsed > *:not(.brand) { display: none; }
97
+
98
+ /* free-move: the bar is fixed-positioned by JS and expands *inward* from its
99
+ docked edge \u2014 vertical (a column) against a left/right edge, horizontal
100
+ otherwise \u2014 so it's always fully on-screen. */
101
+ .toolbar.floating { transform: none; }
102
+ .toolbar.orient-h.flow-rev { flex-direction: row-reverse; }
103
+ .toolbar.orient-v { flex-direction: column; align-items: stretch; }
104
+ .toolbar.orient-v.flow-rev { flex-direction: column-reverse; }
105
+ .toolbar.orient-v button, .toolbar.orient-v .brand { justify-content: flex-start; }
106
+ .toolbar.orient-v .sep { width: 22px; height: 1px; margin: 2px auto; }
107
+ .toolbar.orient-v .count { margin-left: auto; }
93
108
  .toolbar .count {
94
109
  background: var(--pin); color: #fff; font-size: 11px; font-weight: 700;
95
110
  border-radius: 999px; padding: 1px 7px; margin-left: 2px;
@@ -2169,6 +2184,12 @@ var LoupeApp = class {
2169
2184
  this.dragStart = null;
2170
2185
  /** region comment whose outline is currently highlighted (tracks scroll). */
2171
2186
  this.activeRegionId = null;
2187
+ /** Free-move toolbar position as viewport fractions, or null → default (bottom-center). */
2188
+ this.barPos = null;
2189
+ /** In-flight drag of the toolbar by its ◎ handle. */
2190
+ this.barDrag = null;
2191
+ /** True right after a drag so the trailing click doesn't also toggle collapse. */
2192
+ this.justDragged = false;
2172
2193
  this.raf = 0;
2173
2194
  // ---- inspector ------------------------------------------------------------
2174
2195
  this.onMove = (e) => {
@@ -2243,11 +2264,36 @@ var LoupeApp = class {
2243
2264
  this.setMode("off");
2244
2265
  this.finishRegion(vp);
2245
2266
  };
2267
+ // ---- free note (drop a comment anywhere, no element / no screenshot) -------
2268
+ this.onFreeClick = (e) => {
2269
+ if (this.mode !== "free") return;
2270
+ const t = e.target;
2271
+ if (t && (t.id === "loupe-root" || t.closest?.("#loupe-root"))) return;
2272
+ e.preventDefault();
2273
+ e.stopPropagation();
2274
+ const docX = e.clientX + window.scrollX;
2275
+ const docY = e.clientY + window.scrollY;
2276
+ const docW = Math.max(1, document.documentElement.scrollWidth);
2277
+ const docH = Math.max(1, document.documentElement.scrollHeight);
2278
+ const offset = { x: clamp(docX / docW), y: clamp(docY / docH) };
2279
+ this.setMode("off");
2280
+ this.openComposer({ kind: "free", offset, point: { x: docX, y: docY } }, e.clientX, e.clientY);
2281
+ };
2246
2282
  /** Reposition every pin, re-resolving anchors whose element has gone. */
2247
2283
  this.position = () => {
2248
2284
  for (const c of this.comments) {
2249
2285
  const pin = this.pins.get(c.id);
2250
2286
  if (!pin) continue;
2287
+ if (c.kind === "free") {
2288
+ const docW = Math.max(1, document.documentElement.scrollWidth);
2289
+ const docH = Math.max(1, document.documentElement.scrollHeight);
2290
+ const px = c.offset.x * docW - window.scrollX;
2291
+ const py = c.offset.y * docH - window.scrollY;
2292
+ const onScreen = px > -24 && px < window.innerWidth + 24 && py > -24 && py < window.innerHeight + 24;
2293
+ Object.assign(pin.style, { left: px + "px", top: py + "px", display: onScreen ? "grid" : "none" });
2294
+ pin.classList.remove("detached");
2295
+ continue;
2296
+ }
2251
2297
  let elx = this.resolved.get(c.id) ?? null;
2252
2298
  if (!elx || !elx.isConnected) {
2253
2299
  const r = resolveAnchor(c.anchor);
@@ -2292,6 +2338,58 @@ var LoupeApp = class {
2292
2338
  }
2293
2339
  }
2294
2340
  };
2341
+ // ---- draggable, edge-aware floating toolbar --------------------------------
2342
+ this.onResizeBar = () => this.layoutBar();
2343
+ this.onBarPointerDown = (e) => {
2344
+ if (typeof e.button === "number" && e.button !== 0) return;
2345
+ const brand = this.brandEl();
2346
+ const r = brand.getBoundingClientRect();
2347
+ this.barDrag = { px: e.clientX, py: e.clientY, ix: r.left, iy: r.top };
2348
+ this.justDragged = false;
2349
+ try {
2350
+ brand.setPointerCapture(e.pointerId);
2351
+ } catch {
2352
+ }
2353
+ brand.addEventListener("pointermove", this.onBarPointerMove);
2354
+ brand.addEventListener("pointerup", this.onBarPointerUp);
2355
+ brand.addEventListener("pointercancel", this.onBarPointerUp);
2356
+ };
2357
+ this.onBarPointerMove = (e) => {
2358
+ if (!this.barDrag) return;
2359
+ const dx = e.clientX - this.barDrag.px;
2360
+ const dy = e.clientY - this.barDrag.py;
2361
+ if (!this.justDragged && Math.hypot(dx, dy) < 5) return;
2362
+ this.justDragged = true;
2363
+ e.preventDefault();
2364
+ const vw = window.innerWidth, vh = window.innerHeight;
2365
+ const bar = this.toolbar;
2366
+ const brand = this.brandEl();
2367
+ const iw = brand.offsetWidth || 44, ih = brand.offsetHeight || 44;
2368
+ const ix = clampPx(this.barDrag.ix + dx, 6, vw - iw - 6);
2369
+ const iy = clampPx(this.barDrag.iy + dy, 6, vh - ih - 6);
2370
+ bar.classList.add("floating", "dragging");
2371
+ bar.classList.remove("orient-v", "flow-rev");
2372
+ bar.classList.add("orient-h");
2373
+ Object.assign(bar.style, { transform: "none", left: ix + "px", top: iy + "px", right: "auto", bottom: "auto" });
2374
+ this.barPos = { fx: ix / vw, fy: iy / vh };
2375
+ };
2376
+ this.onBarPointerUp = (e) => {
2377
+ const brand = this.brandEl();
2378
+ brand.removeEventListener("pointermove", this.onBarPointerMove);
2379
+ brand.removeEventListener("pointerup", this.onBarPointerUp);
2380
+ brand.removeEventListener("pointercancel", this.onBarPointerUp);
2381
+ try {
2382
+ brand.releasePointerCapture(e.pointerId);
2383
+ } catch {
2384
+ }
2385
+ const dragged = this.justDragged;
2386
+ this.barDrag = null;
2387
+ this.toolbar.classList.remove("dragging");
2388
+ if (dragged) {
2389
+ this.saveBarPos();
2390
+ this.layoutBar();
2391
+ }
2392
+ };
2295
2393
  this.cfg = cfg;
2296
2394
  this.store = cfg.apiBase ? new HttpAdapter(cfg.apiBase, cfg.user, cfg.userHmac, cfg.headers, cfg.credentials) : new LocalStorageAdapter();
2297
2395
  }
@@ -2299,7 +2397,9 @@ var LoupeApp = class {
2299
2397
  return location.pathname + location.search;
2300
2398
  }
2301
2399
  async start() {
2400
+ this.loadBarPos();
2302
2401
  this.buildDom();
2402
+ this.layoutBar();
2303
2403
  this.lastUrl = this.url;
2304
2404
  this.comments = await this.store.list(this.cfg.projectKey, this.url);
2305
2405
  this.renderPins();
@@ -2364,11 +2464,21 @@ var LoupeApp = class {
2364
2464
  const bar = el("div", "toolbar");
2365
2465
  const brand = el("span", "brand");
2366
2466
  brand.innerHTML = `<span class="ico">\u25CE</span><span class="label">Loupe</span>`;
2367
- brand.title = "Collapse / expand the Loupe bar";
2467
+ brand.title = "Drag to move \xB7 click to collapse / expand";
2368
2468
  brand.setAttribute("role", "button");
2369
- brand.onclick = () => this.toggleCollapsed();
2469
+ brand.onclick = () => {
2470
+ if (this.justDragged) {
2471
+ this.justDragged = false;
2472
+ return;
2473
+ }
2474
+ this.toggleCollapsed();
2475
+ };
2476
+ brand.addEventListener("pointerdown", this.onBarPointerDown);
2370
2477
  const inspectBtn = this.toolBtn("\u271B", "Inspect & comment", "inspect");
2371
2478
  inspectBtn.onclick = () => this.setMode(this.mode === "inspect" ? "off" : "inspect");
2479
+ const freeBtn = this.toolBtn(NOTE_ICON, "Note", "free");
2480
+ freeBtn.title = "Drop a note anywhere on the page \u2014 no element, no screenshot";
2481
+ freeBtn.onclick = () => this.setMode(this.mode === "free" ? "off" : "free");
2372
2482
  const regionBtn = this.toolBtn(REGION_ICON, "Region shot", "region");
2373
2483
  regionBtn.title = "Drag a free-size box, screenshot it, and comment";
2374
2484
  regionBtn.onclick = () => this.setMode(this.mode === "region" ? "off" : "region");
@@ -2376,7 +2486,7 @@ var LoupeApp = class {
2376
2486
  this.countEl = el("span", "count", "0");
2377
2487
  listBtn.appendChild(this.countEl);
2378
2488
  listBtn.onclick = () => this.togglePanel();
2379
- bar.append(brand, sep(), inspectBtn, regionBtn, listBtn);
2489
+ bar.append(brand, sep(), inspectBtn, freeBtn, regionBtn, listBtn);
2380
2490
  return bar;
2381
2491
  }
2382
2492
  /** A toolbar button with a uniform icon + label layout. `icon` may be an SVG string. */
@@ -2445,9 +2555,11 @@ var LoupeApp = class {
2445
2555
  this.cancelDrag();
2446
2556
  document.body.style.cursor = mode === "off" ? "" : "crosshair";
2447
2557
  this.toolbar.querySelector('[data-role="inspect"]')?.classList.toggle("on", mode === "inspect");
2558
+ this.toolbar.querySelector('[data-role="free"]')?.classList.toggle("on", mode === "free");
2448
2559
  this.toolbar.querySelector('[data-role="region"]')?.classList.toggle("on", mode === "region");
2449
2560
  document.removeEventListener("mousemove", this.onMove, true);
2450
2561
  document.removeEventListener("click", this.onClick, true);
2562
+ document.removeEventListener("click", this.onFreeClick, true);
2451
2563
  document.removeEventListener("mousedown", this.onRegionDown, true);
2452
2564
  if (mode === "off") return;
2453
2565
  document.addEventListener("keydown", this.onKey, true);
@@ -2455,6 +2567,8 @@ var LoupeApp = class {
2455
2567
  if (mode === "inspect") {
2456
2568
  document.addEventListener("mousemove", this.onMove, true);
2457
2569
  document.addEventListener("click", this.onClick, true);
2570
+ } else if (mode === "free") {
2571
+ document.addEventListener("click", this.onFreeClick, true);
2458
2572
  } else if (mode === "region") {
2459
2573
  document.addEventListener("mousedown", this.onRegionDown, true);
2460
2574
  }
@@ -2467,16 +2581,22 @@ var LoupeApp = class {
2467
2581
  const label = el(
2468
2582
  "div",
2469
2583
  "target",
2470
- target.kind === "element" ? describe(target.element) : `Region \xB7 ${Math.round(target.region.w)}\xD7${Math.round(target.region.h)} px`
2584
+ target.kind === "element" ? describe(target.element) : target.kind === "region" ? `Region \xB7 ${Math.round(target.region.w)}\xD7${Math.round(target.region.h)} px` : "Free note \xB7 anywhere on the page"
2471
2585
  );
2472
2586
  const ta = el("textarea");
2473
- ta.placeholder = target.kind === "region" ? "What's the issue in this area?" : "What should change here?";
2587
+ ta.placeholder = target.kind === "region" ? "What's the issue in this area?" : target.kind === "free" ? "Leave a note about this page\u2026" : "What should change here?";
2474
2588
  const row = el("div", "row");
2475
- const chk = el("label", "chk");
2476
- const box = document.createElement("input");
2477
- box.type = "checkbox";
2478
- box.checked = true;
2479
- chk.append(box, document.createTextNode("Attach screenshot"));
2589
+ let box = null;
2590
+ if (target.kind !== "free") {
2591
+ const chk = el("label", "chk");
2592
+ box = document.createElement("input");
2593
+ box.type = "checkbox";
2594
+ box.checked = true;
2595
+ chk.append(box, document.createTextNode("Attach screenshot"));
2596
+ row.append(chk);
2597
+ } else {
2598
+ row.style.justifyContent = "flex-end";
2599
+ }
2480
2600
  const btns = el("div", "btns");
2481
2601
  const cancel = el("button", "ghost", "Cancel");
2482
2602
  cancel.onclick = () => this.closeComposer();
@@ -2485,9 +2605,9 @@ var LoupeApp = class {
2485
2605
  ta.oninput = () => {
2486
2606
  save.disabled = !ta.value.trim();
2487
2607
  };
2488
- save.onclick = () => this.submit(target, ta.value.trim(), box.checked);
2608
+ save.onclick = () => this.submit(target, ta.value.trim(), box ? box.checked : false);
2489
2609
  btns.append(cancel, save);
2490
- row.append(chk, btns);
2610
+ row.append(btns);
2491
2611
  c.append(label, ta, row);
2492
2612
  const w = 300, h = 190;
2493
2613
  const left = Math.min(Math.max(8, x + 12), window.innerWidth - w - 8);
@@ -2518,6 +2638,11 @@ var LoupeApp = class {
2518
2638
  context = captureElementContext(target.element);
2519
2639
  offset = this.targetOffset;
2520
2640
  anchoredEl = target.element;
2641
+ } else if (target.kind === "free") {
2642
+ screenshot = void 0;
2643
+ anchor = pageAnchor(target.point);
2644
+ context = { html: "", styles: {} };
2645
+ offset = target.offset;
2521
2646
  } else {
2522
2647
  screenshot = withShot ? target.screenshot ?? await this.pendingShot : void 0;
2523
2648
  region = target.region;
@@ -2577,6 +2702,7 @@ var LoupeApp = class {
2577
2702
  }
2578
2703
  pin.textContent = String(i + 1);
2579
2704
  pin.classList.toggle("done", c.status === "done");
2705
+ pin.classList.toggle("free", c.kind === "free");
2580
2706
  });
2581
2707
  this.countEl.textContent = String(this.comments.length);
2582
2708
  this.position();
@@ -2604,6 +2730,7 @@ var LoupeApp = class {
2604
2730
  };
2605
2731
  window.addEventListener("scroll", reposition, true);
2606
2732
  window.addEventListener("resize", reposition);
2733
+ window.addEventListener("resize", this.onResizeBar);
2607
2734
  let debounce = 0;
2608
2735
  this.mo = new MutationObserver(() => {
2609
2736
  clearTimeout(debounce);
@@ -2637,6 +2764,86 @@ var LoupeApp = class {
2637
2764
  this.overlay.style.display = "";
2638
2765
  this.renderPins();
2639
2766
  }
2767
+ this.layoutBar();
2768
+ }
2769
+ loadBarPos() {
2770
+ try {
2771
+ const s = localStorage.getItem("loupe:bar");
2772
+ if (s) {
2773
+ const p = JSON.parse(s);
2774
+ if (typeof p?.fx === "number" && typeof p?.fy === "number") this.barPos = p;
2775
+ }
2776
+ } catch {
2777
+ }
2778
+ }
2779
+ saveBarPos() {
2780
+ try {
2781
+ if (this.barPos) localStorage.setItem("loupe:bar", JSON.stringify(this.barPos));
2782
+ } catch {
2783
+ }
2784
+ }
2785
+ brandEl() {
2786
+ return this.toolbar.querySelector(".brand");
2787
+ }
2788
+ /**
2789
+ * Position the toolbar for its stored spot and make it expand *inward* so it's
2790
+ * always fully on-screen: docked to a left/right edge → vertical; to a
2791
+ * top/bottom edge or a corner → horizontal. No stored spot → default
2792
+ * (bottom-center, governed by CSS).
2793
+ */
2794
+ layoutBar() {
2795
+ const bar = this.toolbar;
2796
+ if (!this.barPos) {
2797
+ bar.classList.remove("floating", "orient-h", "orient-v", "flow-rev");
2798
+ for (const p of ["left", "top", "right", "bottom", "transform"]) bar.style[p] = "";
2799
+ return;
2800
+ }
2801
+ const vw = window.innerWidth, vh = window.innerHeight;
2802
+ const brand = this.brandEl();
2803
+ const iw = brand.offsetWidth || 44, ih = brand.offsetHeight || 44;
2804
+ const ix = clampPx(this.barPos.fx * vw, 6, Math.max(6, vw - iw - 6));
2805
+ const iy = clampPx(this.barPos.fy * vh, 6, Math.max(6, vh - ih - 6));
2806
+ const cx = ix + iw / 2, cy = iy + ih / 2;
2807
+ const horizontalSide = cx < vw / 2 ? "left" : "right";
2808
+ const verticalSide = cy < vh / 2 ? "top" : "bottom";
2809
+ const vertical = Math.min(cx, vw - cx) < Math.min(cy, vh - cy);
2810
+ bar.classList.add("floating");
2811
+ bar.classList.remove("dragging");
2812
+ bar.classList.toggle("orient-v", vertical);
2813
+ bar.classList.toggle("orient-h", !vertical);
2814
+ bar.classList.toggle("flow-rev", vertical ? verticalSide === "bottom" : horizontalSide === "right");
2815
+ bar.style.transform = "none";
2816
+ if (horizontalSide === "left") {
2817
+ bar.style.left = ix + "px";
2818
+ bar.style.right = "auto";
2819
+ } else {
2820
+ bar.style.right = vw - (ix + iw) + "px";
2821
+ bar.style.left = "auto";
2822
+ }
2823
+ if (verticalSide === "top") {
2824
+ bar.style.top = iy + "px";
2825
+ bar.style.bottom = "auto";
2826
+ } else {
2827
+ bar.style.bottom = vh - (iy + ih) + "px";
2828
+ bar.style.top = "auto";
2829
+ }
2830
+ const r = bar.getBoundingClientRect();
2831
+ if (r.right > vw - 6 && bar.style.left !== "auto") {
2832
+ bar.style.left = "auto";
2833
+ bar.style.right = "6px";
2834
+ }
2835
+ if (r.left < 6 && bar.style.right !== "auto") {
2836
+ bar.style.right = "auto";
2837
+ bar.style.left = "6px";
2838
+ }
2839
+ if (r.bottom > vh - 6 && bar.style.top !== "auto") {
2840
+ bar.style.top = "auto";
2841
+ bar.style.bottom = "6px";
2842
+ }
2843
+ if (r.top < 6 && bar.style.bottom !== "auto") {
2844
+ bar.style.bottom = "auto";
2845
+ bar.style.top = "6px";
2846
+ }
2640
2847
  }
2641
2848
  renderPanel() {
2642
2849
  const p = this.panel;
@@ -2649,7 +2856,7 @@ var LoupeApp = class {
2649
2856
  p.appendChild(head);
2650
2857
  const list = el("div", "list");
2651
2858
  if (!this.comments.length) {
2652
- list.appendChild(el("div", "empty", "No comments yet. Click \u201CInspect & comment\u201D, then click any element."));
2859
+ list.appendChild(el("div", "empty", "No comments yet. Click \u201CInspect & comment\u201D and pick an element, or \u201CNote\u201D to drop a comment anywhere."));
2653
2860
  }
2654
2861
  this.comments.forEach((c, i) => list.appendChild(this.itemView(c, i)));
2655
2862
  p.appendChild(list);
@@ -2708,7 +2915,13 @@ var LoupeApp = class {
2708
2915
  return item;
2709
2916
  }
2710
2917
  async copyForClaude(c) {
2711
- const prompt = [
2918
+ const lines = c.kind === "free" ? [
2919
+ `# Product feedback from ${c.author.name}`,
2920
+ ``,
2921
+ `**Note:** ${c.body}`,
2922
+ `**Page:** ${c.url}`,
2923
+ `**Type:** Free note \u2014 a page-level comment not tied to a specific element.`
2924
+ ] : [
2712
2925
  `# Product feedback from ${c.author.name}`,
2713
2926
  ``,
2714
2927
  `**Comment:** ${c.body}`,
@@ -2725,7 +2938,8 @@ var LoupeApp = class {
2725
2938
  "```json",
2726
2939
  JSON.stringify(c.context.styles, null, 2),
2727
2940
  "```"
2728
- ].filter(Boolean).join("\n");
2941
+ ];
2942
+ const prompt = lines.filter(Boolean).join("\n");
2729
2943
  try {
2730
2944
  await navigator.clipboard.writeText(prompt);
2731
2945
  } catch {
@@ -2735,6 +2949,19 @@ var LoupeApp = class {
2735
2949
  flash(id) {
2736
2950
  const c = this.comments.find((x) => x.id === id);
2737
2951
  const pin = this.pins.get(id);
2952
+ if (c?.kind === "free") {
2953
+ for (const p of this.pins.values()) p.classList.remove("active");
2954
+ pin?.classList.add("active");
2955
+ const docW = Math.max(1, document.documentElement.scrollWidth);
2956
+ const docH = Math.max(1, document.documentElement.scrollHeight);
2957
+ window.scrollTo({
2958
+ top: Math.max(0, c.offset.y * docH - window.innerHeight / 2),
2959
+ left: Math.max(0, c.offset.x * docW - window.innerWidth / 2),
2960
+ behavior: "smooth"
2961
+ });
2962
+ this.position();
2963
+ return;
2964
+ }
2738
2965
  if (c?.kind === "region" && c.region) {
2739
2966
  for (const p of this.pins.values()) p.classList.remove("active");
2740
2967
  pin?.classList.add("active");
@@ -2761,6 +2988,7 @@ var LoupeApp = class {
2761
2988
  this.mo?.disconnect();
2762
2989
  if (this.tick) clearInterval(this.tick);
2763
2990
  window.clearTimeout(this.regionTimer);
2991
+ window.removeEventListener("resize", this.onResizeBar);
2764
2992
  document.removeEventListener("keydown", this.onKey, true);
2765
2993
  this.root?.remove();
2766
2994
  }
@@ -2777,7 +3005,24 @@ function sep() {
2777
3005
  function clamp(n) {
2778
3006
  return Math.max(0, Math.min(1, n));
2779
3007
  }
3008
+ function clampPx(n, min, max) {
3009
+ return Math.max(min, Math.min(max, n));
3010
+ }
2780
3011
  var REGION_ICON = `<svg class="ico" width="15" height="15" viewBox="0 0 15 15" fill="none" aria-hidden="true"><rect x="1.5" y="2.5" width="12" height="10" rx="1.5" stroke="currentColor" stroke-width="1.4" stroke-dasharray="2.4 1.8"/></svg>`;
3012
+ var NOTE_ICON = `<svg class="ico" width="15" height="15" viewBox="0 0 15 15" fill="none" aria-hidden="true"><path d="M2 2.5h11v7.5H6l-3 2.5v-2.5H2z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>`;
3013
+ function pageAnchor(point) {
3014
+ return {
3015
+ tag: "page",
3016
+ cssPath: "page",
3017
+ xpath: "",
3018
+ testid: null,
3019
+ text: "",
3020
+ attrs: {},
3021
+ nthOfType: 1,
3022
+ rect: { x: Math.round(point.x), y: Math.round(point.y), w: 0, h: 0 },
3023
+ viewport: { w: window.innerWidth, h: window.innerHeight }
3024
+ };
3025
+ }
2781
3026
  function regionAnchor(region) {
2782
3027
  return {
2783
3028
  tag: "region",
@@ -2803,6 +3048,7 @@ function describe(elx) {
2803
3048
  return txt ? `${tag} \xB7 \u201C${txt}\u201D` : tag;
2804
3049
  }
2805
3050
  function describeAnchor(c) {
3051
+ if (c.kind === "free") return "Free note \xB7 page-level";
2806
3052
  return c.anchor.testid ? `[data-testid="${c.anchor.testid}"]` : c.anchor.cssPath;
2807
3053
  }
2808
3054