@merittdev/horus-lens 0.4.0 → 0.5.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/react.cjs CHANGED
@@ -29083,7 +29083,7 @@ var ApiErrorSchema = external_exports.object({
29083
29083
 
29084
29084
  // ../core/dist/index.js
29085
29085
  var SDK_NAME = "@merittdev/horus-lens";
29086
- var SDK_VERSION = true ? "0.4.0" : "0.0.0-dev";
29086
+ var SDK_VERSION = true ? "0.5.1" : "0.0.0-dev";
29087
29087
  var DEFAULT_ENDPOINT = "https://api.horus.sh";
29088
29088
  var DEFAULT_REPLAY_WINDOW_MS = 15e3;
29089
29089
  var DEFAULT_MAX_CONSOLE_ENTRIES = 200;
@@ -29144,11 +29144,15 @@ function startAutoCapture(client, options = {}) {
29144
29144
  const last = lastSeen.get(signature);
29145
29145
  if (last !== void 0 && now - last < AUTO_CAPTURE_COOLDOWN_MS) return;
29146
29146
  if (sessionCount >= AUTO_CAPTURE_SESSION_CAP) return;
29147
+ const previousSeen = lastSeen.get(signature);
29147
29148
  lastSeen.set(signature, now);
29148
29149
  sessionCount += 1;
29149
29150
  submitting = true;
29150
29151
  options.onCapture?.(signature);
29151
29152
  void Promise.resolve().then(() => client.submitReport({ trigger: "error", include: { screenshot: false } })).catch(() => {
29153
+ if (previousSeen === void 0) lastSeen.delete(signature);
29154
+ else lastSeen.set(signature, previousSeen);
29155
+ sessionCount -= 1;
29152
29156
  }).finally(() => {
29153
29157
  submitting = false;
29154
29158
  });
@@ -29203,6 +29207,14 @@ var RingBuffer = class {
29203
29207
  return this.items.length;
29204
29208
  }
29205
29209
  };
29210
+ var HttpError = class extends Error {
29211
+ constructor(status, message) {
29212
+ super(message);
29213
+ this.status = status;
29214
+ this.name = "HttpError";
29215
+ }
29216
+ status;
29217
+ };
29206
29218
  var Transport = class {
29207
29219
  constructor(config2) {
29208
29220
  this.config = config2;
@@ -29210,18 +29222,23 @@ var Transport = class {
29210
29222
  config;
29211
29223
  async initiate(assets) {
29212
29224
  const body = { accessId: this.config.accessId, assets };
29213
- return this.postJson(`${this.config.endpoint}/v1/reports/initiate`, body);
29225
+ return this.postJson(
29226
+ `${this.config.endpoint}/v1/reports/initiate`,
29227
+ body,
29228
+ { retries: 0 }
29229
+ );
29214
29230
  }
29215
29231
  /** Record a late-uploaded asset (background screenshot) on a finalized report. */
29216
29232
  async attachAsset(reportId, asset) {
29217
29233
  const body = { accessId: this.config.accessId, ...asset };
29218
29234
  await this.postJson(`${this.config.endpoint}/v1/reports/${encodeURIComponent(reportId)}/assets`, body);
29219
29235
  }
29220
- async uploadAsset(upload, blob2) {
29236
+ async uploadAsset(upload, blob2, signal) {
29221
29237
  const response = await fetch(upload.url, {
29222
29238
  method: upload.method,
29223
29239
  headers: upload.headers,
29224
- body: blob2
29240
+ body: blob2,
29241
+ signal
29225
29242
  });
29226
29243
  if (!response.ok) {
29227
29244
  throw new Error(`Asset upload failed (${upload.kind}): ${response.status}`);
@@ -29229,12 +29246,20 @@ var Transport = class {
29229
29246
  }
29230
29247
  async finalize(reportId, metadata, uploadedAssets) {
29231
29248
  const body = { accessId: this.config.accessId, metadata, uploadedAssets };
29232
- return this.postJson(
29233
- `${this.config.endpoint}/v1/reports/${reportId}/finalize`,
29234
- body
29235
- );
29249
+ try {
29250
+ return await this.postJson(
29251
+ `${this.config.endpoint}/v1/reports/${reportId}/finalize`,
29252
+ body
29253
+ );
29254
+ } catch (err) {
29255
+ if (err instanceof HttpError && err.status === 409) {
29256
+ return { reportId, status: "submitted" };
29257
+ }
29258
+ throw err;
29259
+ }
29236
29260
  }
29237
- async postJson(url2, body, retries = 1) {
29261
+ async postJson(url2, body, opts = {}) {
29262
+ const retries = opts.retries ?? 1;
29238
29263
  let lastError;
29239
29264
  for (let attempt = 0; attempt <= retries; attempt++) {
29240
29265
  try {
@@ -29245,7 +29270,10 @@ var Transport = class {
29245
29270
  });
29246
29271
  if (!response.ok) {
29247
29272
  const text = await response.text().catch(() => "");
29248
- throw new Error(`Request to ${url2} failed: ${response.status} ${text}`.trim());
29273
+ throw new HttpError(
29274
+ response.status,
29275
+ `Request to ${url2} failed: ${response.status} ${text}`.trim()
29276
+ );
29249
29277
  }
29250
29278
  return await response.json();
29251
29279
  } catch (err) {
@@ -29636,8 +29664,11 @@ function computeCropRect(crop, scale, rasterWidth, rasterHeight) {
29636
29664
  async function captureScreenshot(excludeHost, crop, composite) {
29637
29665
  if (typeof document === "undefined" || typeof window === "undefined") return null;
29638
29666
  try {
29639
- const viewportPixels = window.innerWidth * window.innerHeight || 1;
29640
- const pixelCap = Math.sqrt(SCREENSHOT_MAX_PIXELS / viewportPixels);
29667
+ const viewportOnly = !composite && !crop;
29668
+ const captureW = composite ? composite.docWidth : window.innerWidth;
29669
+ const captureH = composite ? composite.docHeight : window.innerHeight;
29670
+ const capturePixels = captureW * captureH || 1;
29671
+ const pixelCap = Math.sqrt(SCREENSHOT_MAX_PIXELS / capturePixels);
29641
29672
  const scale = Math.max(1, Math.min(window.devicePixelRatio || 1, SCREENSHOT_MAX_SCALE, pixelCap));
29642
29673
  const capture = domToBlob(document.documentElement, {
29643
29674
  scale,
@@ -29646,12 +29677,19 @@ async function captureScreenshot(excludeHost, crop, composite) {
29646
29677
  // JPEG has no alpha — transparent page regions (and any capture box
29647
29678
  // extending past short content) would otherwise encode as BLACK.
29648
29679
  backgroundColor: "#ffffff",
29649
- // When compositing annotations, force the capture box to the editor's
29650
- // document measurement (scrollHeight-based). modern-screenshot otherwise
29651
- // sizes from documentElement's bounding box, which on pages SHORTER than
29652
- // the viewport is the content height only — marks drawn in the lower
29653
- // viewport band would fall off the raster and the crop would clamp short.
29680
+ // Composite (annotations): force the capture box to the editor's document
29681
+ // measurement so marks in the lower viewport band aren't clipped.
29654
29682
  ...composite ? { width: composite.docWidth, height: composite.docHeight } : {},
29683
+ // Plain report: clip the render to the viewport and shift the scrolled
29684
+ // content into it, so only the visible region is rasterized/fetched.
29685
+ ...viewportOnly ? {
29686
+ width: window.innerWidth,
29687
+ height: window.innerHeight,
29688
+ style: {
29689
+ transform: `translate(${-window.scrollX}px, ${-window.scrollY}px)`,
29690
+ transformOrigin: "top left"
29691
+ }
29692
+ } : {},
29655
29693
  // Exclude the overlay from the capture CLONE — never hide the live node.
29656
29694
  filter: (node2) => node2 !== excludeHost,
29657
29695
  // Per-resource fetch budget: a single slow cross-origin image must not
@@ -30121,12 +30159,22 @@ var LensClient = class {
30121
30159
  input2.screenshotCrop,
30122
30160
  input2.annotationComposite
30123
30161
  ) : Promise.resolve(null);
30162
+ const consumingManualRecording = wantReplay && (this.replay?.hasManualRecording() ?? false) && !(this.replay?.isManualRecording() ?? false);
30124
30163
  const replayEvents = wantReplay ? this.replay?.snapshot() ?? [] : [];
30125
30164
  const errorEntries = this.errorBuffer.drain();
30126
- const consoleTail = this.consoleBuffer.tail(CONSOLE_TAIL_SIZE);
30127
- const networkTail = this.networkBuffer.tail(NETWORK_TAIL_SIZE);
30128
30165
  const consoleEntries = this.consoleBuffer.drain();
30129
30166
  const networkEntries = this.networkBuffer.drain();
30167
+ this.errorBuffer.clear();
30168
+ this.consoleBuffer.clear();
30169
+ this.networkBuffer.clear();
30170
+ const consoleTail = consoleEntries.slice(-CONSOLE_TAIL_SIZE);
30171
+ const isNetFailure = (e) => e.ok !== true || Boolean(e.error);
30172
+ const recentFailures = networkEntries.filter(isNetFailure).slice(-NETWORK_TAIL_SIZE);
30173
+ const keep = /* @__PURE__ */ new Set([
30174
+ ...recentFailures,
30175
+ ...networkEntries.slice(-NETWORK_TAIL_SIZE)
30176
+ ]);
30177
+ const networkTail = networkEntries.filter((e) => keep.has(e)).slice(-NETWORK_TAIL_SIZE);
30130
30178
  let metadata = {
30131
30179
  annotations: input2.annotations ?? [],
30132
30180
  env: collectEnv(),
@@ -30180,13 +30228,14 @@ var LensClient = class {
30180
30228
  }
30181
30229
  if (!asset.blob) continue;
30182
30230
  try {
30183
- await this.transport.uploadAsset(upload, asset.blob);
30231
+ await this.uploadAssetWithTimeout(upload, asset.blob);
30184
30232
  uploadedAssets.push({ kind: upload.kind, key: upload.key, byteSize: asset.blob.size });
30185
30233
  } catch (err) {
30186
30234
  this.warn(`asset upload failed (${upload.kind})`, err);
30187
30235
  }
30188
30236
  }
30189
30237
  const finalized = await this.transport.finalize(initiated.reportId, metadata, uploadedAssets);
30238
+ if (consumingManualRecording) this.replay?.discardManualRecording();
30190
30239
  if (screenshotUpload) {
30191
30240
  const upload = screenshotUpload;
30192
30241
  void screenshotPromise.then(async (blob2) => {
@@ -30204,6 +30253,21 @@ var LensClient = class {
30204
30253
  }
30205
30254
  return { reportId: finalized.reportId };
30206
30255
  }
30256
+ /**
30257
+ * Upload a foreground asset with a bounded timeout. Without this a stuck S3
30258
+ * PUT (dropped connection, half-open socket) would hang `submitReport`
30259
+ * forever; the timeout aborts the individual PUT so it fails as one asset
30260
+ * (caught by the caller) instead of wedging the whole submit.
30261
+ */
30262
+ async uploadAssetWithTimeout(upload, blob2) {
30263
+ const controller = new AbortController();
30264
+ const timer = setTimeout(() => controller.abort(), SCREENSHOT_TIMEOUT_MS);
30265
+ try {
30266
+ await this.transport.uploadAsset(upload, blob2, controller.signal);
30267
+ } finally {
30268
+ clearTimeout(timer);
30269
+ }
30270
+ }
30207
30271
  warn(message, err) {
30208
30272
  if (!this.debug) return;
30209
30273
  if (err !== void 0) console.warn(`[lens] ${message}`, err);
@@ -30332,6 +30396,113 @@ function el(tag, props = {}, children = []) {
30332
30396
  function clear(node2) {
30333
30397
  while (node2.firstChild) node2.removeChild(node2.firstChild);
30334
30398
  }
30399
+ var MARGIN = 6;
30400
+ var DRAG_THRESHOLD = 3;
30401
+ var savedPositions = /* @__PURE__ */ new Map();
30402
+ function isInteractiveTarget(target) {
30403
+ return target instanceof Element && target.closest(
30404
+ 'button, a, input, textarea, select, [role="switch"], [role="button"], [contenteditable="true"]'
30405
+ ) !== null;
30406
+ }
30407
+ function clampToViewport(element, left, top) {
30408
+ const rect = element.getBoundingClientRect();
30409
+ const vw = window.innerWidth;
30410
+ const vh = window.innerHeight;
30411
+ const maxLeft = Math.max(MARGIN, vw - rect.width - MARGIN);
30412
+ const maxTop = Math.max(MARGIN, vh - rect.height - MARGIN);
30413
+ return {
30414
+ left: Math.min(Math.max(left, MARGIN), maxLeft),
30415
+ top: Math.min(Math.max(top, MARGIN), maxTop)
30416
+ };
30417
+ }
30418
+ function makeDraggable(element, opts) {
30419
+ const { handle, key } = opts;
30420
+ const ignore = opts.ignore ?? isInteractiveTarget;
30421
+ let startX = 0;
30422
+ let startY = 0;
30423
+ let originLeft = 0;
30424
+ let originTop = 0;
30425
+ let dragging = false;
30426
+ let armed = false;
30427
+ let pointerId = null;
30428
+ const apply = (left, top) => {
30429
+ const p = clampToViewport(element, left, top);
30430
+ element.style.left = `${p.left}px`;
30431
+ element.style.top = `${p.top}px`;
30432
+ element.style.right = "auto";
30433
+ element.style.bottom = "auto";
30434
+ element.style.transform = "none";
30435
+ savedPositions.set(key, p);
30436
+ opts.onMove?.();
30437
+ };
30438
+ const onPointerDown = (e) => {
30439
+ if (e.button !== 0 && e.pointerType === "mouse") return;
30440
+ if (ignore(e.target)) return;
30441
+ armed = true;
30442
+ dragging = false;
30443
+ pointerId = e.pointerId;
30444
+ startX = e.clientX;
30445
+ startY = e.clientY;
30446
+ const rect = element.getBoundingClientRect();
30447
+ originLeft = rect.left;
30448
+ originTop = rect.top;
30449
+ window.addEventListener("pointermove", onPointerMove, true);
30450
+ window.addEventListener("pointerup", onPointerUp, true);
30451
+ window.addEventListener("pointercancel", onPointerUp, true);
30452
+ };
30453
+ const onPointerMove = (e) => {
30454
+ if (!armed || e.pointerId !== pointerId) return;
30455
+ const dx = e.clientX - startX;
30456
+ const dy = e.clientY - startY;
30457
+ if (!dragging) {
30458
+ if (Math.abs(dx) + Math.abs(dy) < DRAG_THRESHOLD) return;
30459
+ dragging = true;
30460
+ element.classList.add("is-dragging");
30461
+ }
30462
+ e.preventDefault();
30463
+ apply(originLeft + dx, originTop + dy);
30464
+ };
30465
+ const onPointerUp = (e) => {
30466
+ if (e.pointerId !== pointerId) return;
30467
+ armed = false;
30468
+ if (dragging) {
30469
+ dragging = false;
30470
+ element.classList.remove("is-dragging");
30471
+ }
30472
+ pointerId = null;
30473
+ window.removeEventListener("pointermove", onPointerMove, true);
30474
+ window.removeEventListener("pointerup", onPointerUp, true);
30475
+ window.removeEventListener("pointercancel", onPointerUp, true);
30476
+ };
30477
+ const onResize = () => {
30478
+ const p = savedPositions.get(key);
30479
+ if (p) apply(p.left, p.top);
30480
+ };
30481
+ handle.classList.add("lens-drag-handle");
30482
+ handle.addEventListener("pointerdown", onPointerDown);
30483
+ window.addEventListener("resize", onResize);
30484
+ const stored = savedPositions.get(key);
30485
+ if (stored) requestAnimationFrame(() => apply(stored.left, stored.top));
30486
+ return {
30487
+ reclamp: onResize,
30488
+ reset() {
30489
+ savedPositions.delete(key);
30490
+ element.style.left = "";
30491
+ element.style.top = "";
30492
+ element.style.right = "";
30493
+ element.style.bottom = "";
30494
+ element.style.transform = "";
30495
+ element.classList.remove("is-dragging");
30496
+ },
30497
+ destroy() {
30498
+ handle.removeEventListener("pointerdown", onPointerDown);
30499
+ window.removeEventListener("pointermove", onPointerMove, true);
30500
+ window.removeEventListener("pointerup", onPointerUp, true);
30501
+ window.removeEventListener("pointercancel", onPointerUp, true);
30502
+ window.removeEventListener("resize", onResize);
30503
+ }
30504
+ };
30505
+ }
30335
30506
  var THIN_MIN_DIST = 3;
30336
30507
  var MAX_POINTS = 2e3;
30337
30508
  var HIGHLIGHTER_WIDTH = 14;
@@ -30482,6 +30653,8 @@ var iconTrash = svg(
30482
30653
  '<polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>'
30483
30654
  );
30484
30655
  var iconAnnotate = iconPencil;
30656
+ var iconGrip = `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="9" cy="6" r="1.6"/><circle cx="15" cy="6" r="1.6"/><circle cx="9" cy="12" r="1.6"/><circle cx="15" cy="12" r="1.6"/><circle cx="9" cy="18" r="1.6"/><circle cx="15" cy="18" r="1.6"/></svg>`;
30657
+ var iconReport = svg('<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2Z"/>');
30485
30658
  var SVG_NS = "http://www.w3.org/2000/svg";
30486
30659
  var FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
30487
30660
  var MIN_DRAG = 6;
@@ -30510,6 +30683,7 @@ var AnnotationEditor = class {
30510
30683
  svg = null;
30511
30684
  bg = null;
30512
30685
  toolbar = null;
30686
+ toolbarDrag = null;
30513
30687
  textInputEl = null;
30514
30688
  textAnchor = null;
30515
30689
  textColor = COLORS[0];
@@ -30555,6 +30729,7 @@ var AnnotationEditor = class {
30555
30729
  this.markNodes = [];
30556
30730
  this.annotations = [];
30557
30731
  this.stepCounter = 0;
30732
+ this.sessionBase = Math.min(this.sessionBase, this.annotations.length);
30558
30733
  if (!this.active) this.teardownLayer();
30559
30734
  this.syncActionButtons();
30560
30735
  }
@@ -30569,6 +30744,7 @@ var AnnotationEditor = class {
30569
30744
  this.sizeLayer();
30570
30745
  this.updateTransform();
30571
30746
  svg2.style.pointerEvents = "auto";
30747
+ if (this.bg) this.bg.style.pointerEvents = "all";
30572
30748
  svg2.style.touchAction = "none";
30573
30749
  svg2.style.cursor = "crosshair";
30574
30750
  svg2.addEventListener("pointerdown", this.onPointerDown);
@@ -30589,6 +30765,7 @@ var AnnotationEditor = class {
30589
30765
  if (svg2) {
30590
30766
  svg2.removeEventListener("pointerdown", this.onPointerDown);
30591
30767
  svg2.style.pointerEvents = "none";
30768
+ if (this.bg) this.bg.style.pointerEvents = "none";
30592
30769
  svg2.style.touchAction = "";
30593
30770
  svg2.style.cursor = "";
30594
30771
  }
@@ -30598,15 +30775,8 @@ var AnnotationEditor = class {
30598
30775
  this.onExit = null;
30599
30776
  cb?.();
30600
30777
  }
30601
- /** Cancel — discard marks drawn during this session, then exit. */
30602
- cancel() {
30603
- this.cancelTextInput();
30604
- const removed = this.markNodes.splice(this.sessionBase);
30605
- for (const node2 of removed) node2.remove();
30606
- this.annotations.splice(this.sessionBase);
30607
- this.exit();
30608
- }
30609
30778
  undo() {
30779
+ if (this.markNodes.length <= this.sessionBase) return;
30610
30780
  const node2 = this.markNodes.pop();
30611
30781
  if (!node2) return;
30612
30782
  node2.remove();
@@ -30822,39 +30992,45 @@ var AnnotationEditor = class {
30822
30992
  on: { click: () => this.clear() }
30823
30993
  });
30824
30994
  const editGroup = el("div", { class: "lens-tool-group" }, [this.undoBtn, this.clearBtn]);
30825
- const doneGroup = el("div", { class: "lens-tool-group" }, [
30826
- el("button", {
30827
- class: "lens-pick-chip__btn",
30828
- text: "Cancel",
30829
- attrs: { type: "button" },
30830
- on: { click: () => this.cancel() }
30831
- }),
30832
- el("button", {
30833
- class: "lens-pick-chip__btn lens-pick-chip__btn--primary",
30834
- text: "Done",
30835
- attrs: { type: "button" },
30836
- on: { click: () => this.exit() }
30837
- })
30995
+ const closeBtn = el("button", {
30996
+ class: "lens-tool-close",
30997
+ ariaLabel: "Done",
30998
+ title: "Done",
30999
+ html: iconClose,
31000
+ attrs: { type: "button" },
31001
+ on: { click: () => this.exit() }
31002
+ });
31003
+ const grip = el("span", {
31004
+ class: "lens-tool-grip",
31005
+ html: iconGrip,
31006
+ title: "Drag to move toolbar",
31007
+ ariaLabel: "Drag to move toolbar"
31008
+ });
31009
+ const track = el("div", { class: "lens-tool-track" }, [
31010
+ toolGroup,
31011
+ el("span", { class: "lens-tool-sep" }),
31012
+ colorGroup,
31013
+ el("span", { class: "lens-tool-sep" }),
31014
+ widthGroup,
31015
+ el("span", { class: "lens-tool-sep" }),
31016
+ editGroup
30838
31017
  ]);
30839
31018
  this.toolbar = el(
30840
31019
  "div",
30841
31020
  { class: "lens-annotate-toolbar", attrs: { role: "toolbar", "aria-label": "Annotation tools" } },
30842
- [
30843
- toolGroup,
30844
- el("span", { class: "lens-tool-sep" }),
30845
- colorGroup,
30846
- el("span", { class: "lens-tool-sep" }),
30847
- widthGroup,
30848
- el("span", { class: "lens-tool-sep" }),
30849
- editGroup,
30850
- el("span", { class: "lens-tool-sep" }),
30851
- doneGroup
30852
- ]
31021
+ [grip, track, closeBtn]
30853
31022
  );
30854
31023
  this.host.append(this.toolbar);
31024
+ this.toolbarDrag = makeDraggable(this.toolbar, {
31025
+ handle: grip,
31026
+ key: "lens-annotate-toolbar",
31027
+ ignore: isInteractiveTarget
31028
+ });
30855
31029
  this.syncActionButtons();
30856
31030
  }
30857
31031
  removeToolbar() {
31032
+ this.toolbarDrag?.destroy();
31033
+ this.toolbarDrag = null;
30858
31034
  this.toolbar?.remove();
30859
31035
  this.toolbar = null;
30860
31036
  this.toolBtns.clear();
@@ -30891,7 +31067,7 @@ var AnnotationEditor = class {
30891
31067
  }
30892
31068
  syncActionButtons() {
30893
31069
  const empty = this.markNodes.length === 0;
30894
- if (this.undoBtn) this.undoBtn.disabled = empty;
31070
+ if (this.undoBtn) this.undoBtn.disabled = this.markNodes.length <= this.sessionBase;
30895
31071
  if (this.clearBtn) this.clearBtn.disabled = empty;
30896
31072
  }
30897
31073
  // --------------------------------------------------------------------------
@@ -30905,6 +31081,7 @@ var AnnotationEditor = class {
30905
31081
  }
30906
31082
  onPointerDown = (e) => {
30907
31083
  if (!this.active) return;
31084
+ if (this.drag) return;
30908
31085
  if (e.button !== void 0 && e.button !== 0) return;
30909
31086
  e.preventDefault();
30910
31087
  const p = this.toDoc(e);
@@ -30954,6 +31131,7 @@ var AnnotationEditor = class {
30954
31131
  this.drag = drag;
30955
31132
  window.addEventListener("pointermove", this.onPointerMove, true);
30956
31133
  window.addEventListener("pointerup", this.onPointerUp, true);
31134
+ window.addEventListener("pointercancel", this.onPointerCancel, true);
30957
31135
  };
30958
31136
  onPointerMove = (e) => {
30959
31137
  const d = this.drag;
@@ -31063,9 +31241,18 @@ var AnnotationEditor = class {
31063
31241
  d.node.removeAttribute("data-lens-draft");
31064
31242
  this.commitMark(annotation, d.node);
31065
31243
  };
31244
+ /** Pointer capture was cancelled (OS gesture, page hidden, etc.): drop the
31245
+ * in-progress draft node and detach listeners so nothing leaks. */
31246
+ onPointerCancel = () => {
31247
+ const d = this.drag;
31248
+ this.detachDragListeners();
31249
+ this.drag = null;
31250
+ d?.node.remove();
31251
+ };
31066
31252
  detachDragListeners() {
31067
31253
  window.removeEventListener("pointermove", this.onPointerMove, true);
31068
31254
  window.removeEventListener("pointerup", this.onPointerUp, true);
31255
+ window.removeEventListener("pointercancel", this.onPointerCancel, true);
31069
31256
  }
31070
31257
  commitMark(annotation, node2) {
31071
31258
  this.annotations.push(annotation);
@@ -31259,29 +31446,31 @@ var AnnotationEditor = class {
31259
31446
  return `lens-${++this.idSeq}`;
31260
31447
  }
31261
31448
  };
31262
- var INCLUDE_ITEMS = [
31263
- { key: "replay", label: "Replay" },
31264
- { key: "console", label: "Console" },
31265
- { key: "network", label: "Network" },
31266
- { key: "errors", label: "Errors" },
31267
- { key: "screenshot", label: "Screenshot" }
31268
- ];
31269
31449
  var Panel = class {
31270
31450
  constructor(cb) {
31271
31451
  this.cb = cb;
31272
31452
  this.body = el("div", { class: "lens-panel__body" });
31273
- this.root = el("div", { class: "lens-panel", attrs: { role: "dialog", "aria-label": "Report an issue" } }, [
31274
- el("div", { class: "lens-panel__head" }, [
31275
- el("div", { class: "lens-panel__title", text: "Report an issue" }),
31276
- el("button", {
31277
- class: "lens-panel__x",
31278
- ariaLabel: "Close",
31279
- html: iconClose,
31280
- on: { click: () => this.cb.onClose() }
31281
- })
31282
- ]),
31283
- this.body
31453
+ const head = el("div", { class: "lens-panel__head" }, [
31454
+ el("span", {
31455
+ class: "lens-panel__grip",
31456
+ html: iconGrip,
31457
+ title: "Drag to move",
31458
+ ariaLabel: "Drag to move panel"
31459
+ }),
31460
+ el("div", { class: "lens-panel__title", text: "Report an issue" }),
31461
+ el("button", {
31462
+ class: "lens-panel__x",
31463
+ ariaLabel: "Close",
31464
+ html: iconClose,
31465
+ on: { click: () => this.cb.onClose() }
31466
+ })
31284
31467
  ]);
31468
+ this.root = el(
31469
+ "div",
31470
+ { class: "lens-panel", attrs: { role: "dialog", "aria-label": "Report an issue" } },
31471
+ [head, this.body]
31472
+ );
31473
+ this.drag = makeDraggable(this.root, { handle: head, key: "lens-panel" });
31285
31474
  this.renderForm();
31286
31475
  }
31287
31476
  cb;
@@ -31289,6 +31478,7 @@ var Panel = class {
31289
31478
  body;
31290
31479
  textarea;
31291
31480
  submitBtn;
31481
+ drag = null;
31292
31482
  autoCloseTimer = null;
31293
31483
  submitting = false;
31294
31484
  /** Selected capture toggles; every flag starts ON. Persists across refresh. */
@@ -31308,6 +31498,8 @@ var Panel = class {
31308
31498
  }
31309
31499
  destroy() {
31310
31500
  if (this.autoCloseTimer) clearTimeout(this.autoCloseTimer);
31501
+ this.drag?.destroy();
31502
+ this.drag = null;
31311
31503
  this.root.remove();
31312
31504
  }
31313
31505
  renderForm() {
@@ -31322,7 +31514,6 @@ var Panel = class {
31322
31514
  this.body.append(
31323
31515
  this.textarea,
31324
31516
  this.buildContext(),
31325
- this.buildIncludes(),
31326
31517
  this.buildChips(),
31327
31518
  this.buildActions()
31328
31519
  );
@@ -31342,39 +31533,6 @@ var Panel = class {
31342
31533
  });
31343
31534
  return el("div", { class: "lens-context", title: s.url }, nodes);
31344
31535
  }
31345
- /** Real toggles ("switch"): which captured data to attach. All ON by default. */
31346
- buildIncludes() {
31347
- const s = this.cb.getStats();
31348
- const counts = {
31349
- console: s.console,
31350
- network: s.network,
31351
- errors: s.errors
31352
- };
31353
- const toggles = INCLUDE_ITEMS.map((item) => this.buildToggle(item.key, item.label, counts[item.key]));
31354
- return el("div", { class: "lens-includes" }, [
31355
- el("span", { class: "lens-includes__label", text: "Include:" }),
31356
- ...toggles
31357
- ]);
31358
- }
31359
- buildToggle(key, label, count) {
31360
- const on2 = this.include[key];
31361
- const btn = el("button", {
31362
- class: `lens-tgl${on2 ? " is-on" : ""}`,
31363
- attrs: { type: "button", role: "switch", "aria-checked": String(on2) },
31364
- html: `<span class="lens-tgl__check" aria-hidden="true">${iconCheck}</span>`
31365
- });
31366
- btn.append(el("span", { class: "lens-tgl__label", text: label }));
31367
- if (typeof count === "number" && count > 0) {
31368
- btn.append(el("span", { class: "lens-tgl__count", text: String(count) }));
31369
- }
31370
- btn.addEventListener("click", () => {
31371
- const next = !this.include[key];
31372
- this.include[key] = next;
31373
- btn.classList.toggle("is-on", next);
31374
- btn.setAttribute("aria-checked", String(next));
31375
- });
31376
- return btn;
31377
- }
31378
31536
  /** Removable context chips: annotations, retained recording, picked element. */
31379
31537
  buildChips() {
31380
31538
  const row = el("div", { class: "lens-chips" });
@@ -31679,14 +31837,28 @@ var OVERLAY_CSS = (
31679
31837
  `
31680
31838
  :host {
31681
31839
  all: initial;
31682
- --lens-accent: #6366f1;
31683
- --lens-accent-strong: #4f46e5;
31840
+ /* Black & white, mirroring the Horus Cloud dashboard: the accent is near-black
31841
+ in light and near-white in dark; the always-dark pill/toolbar use explicit
31842
+ light highlights (defined at their rules). Annotation swatch colors and
31843
+ status red/green are content, not brand, and keep their hues. */
31844
+ --lens-accent: #18181b;
31845
+ --lens-accent-strong: #000000;
31846
+ --lens-on-accent: #ffffff;
31847
+ --lens-focus: rgba(0, 0, 0, 0.16);
31684
31848
  --lens-radius: 8px;
31685
31849
  --lens-font: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
31686
31850
  --lens-z: 2147483000;
31687
31851
  font-family: var(--lens-font);
31688
31852
  line-height: 1.4;
31689
31853
  }
31854
+ @media (prefers-color-scheme: dark) {
31855
+ :host {
31856
+ --lens-accent: #fafafa;
31857
+ --lens-accent-strong: #ffffff;
31858
+ --lens-on-accent: #18181b;
31859
+ --lens-focus: rgba(255, 255, 255, 0.22);
31860
+ }
31861
+ }
31690
31862
 
31691
31863
  *, *::before, *::after { box-sizing: border-box; }
31692
31864
 
@@ -31706,7 +31878,7 @@ var OVERLAY_CSS = (
31706
31878
  left: 50%;
31707
31879
  transform: translateX(-50%);
31708
31880
  max-width: calc(100vw - 16px);
31709
- flex-wrap: wrap;
31881
+ flex-wrap: nowrap;
31710
31882
  justify-content: center;
31711
31883
  display: inline-flex;
31712
31884
  align-items: center;
@@ -31730,7 +31902,7 @@ var OVERLAY_CSS = (
31730
31902
  height: 26px;
31731
31903
  margin-left: 4px;
31732
31904
  border-radius: 999px;
31733
- color: #a5b4fc;
31905
+ color: #f4f4f5;
31734
31906
  }
31735
31907
  .lens-btn {
31736
31908
  display: inline-flex;
@@ -31749,11 +31921,14 @@ var OVERLAY_CSS = (
31749
31921
  transition: background 150ms ease, color 150ms ease;
31750
31922
  }
31751
31923
  .lens-btn:hover { background: rgba(255, 255, 255, 0.1); }
31752
- .lens-btn:focus-visible { outline: 2px solid var(--lens-accent); outline-offset: 1px; }
31753
- .lens-btn--primary { background: var(--lens-accent); color: #fff; }
31754
- .lens-btn--primary:hover { background: var(--lens-accent-strong); }
31924
+ .lens-btn:focus-visible { outline: 2px solid rgba(255, 255, 255, 0.7); outline-offset: 1px; }
31925
+ .lens-btn--primary { background: #fff; color: #18181b; }
31926
+ .lens-btn--primary:hover { background: #e4e4e7; }
31927
+ .lens-btn--report svg { flex: none; }
31928
+ .lens-btn__label { white-space: nowrap; }
31755
31929
  .lens-btn--icon { width: 30px; padding: 0; justify-content: center; }
31756
- .lens-btn--icon.is-active { background: var(--lens-accent); color: #fff; }
31930
+ /* Always-dark pill: an active control reads as a light (white) highlight. */
31931
+ .lens-btn--icon.is-active { background: #fff; color: #18181b; }
31757
31932
  .lens-pill__sep {
31758
31933
  width: 1px;
31759
31934
  height: 18px;
@@ -31767,7 +31942,10 @@ var OVERLAY_CSS = (
31767
31942
  z-index: calc(var(--lens-z) - 2);
31768
31943
  pointer-events: none;
31769
31944
  border: 2px solid var(--lens-accent);
31770
- background: rgba(99, 102, 241, 0.14);
31945
+ /* A light outer ring so the (black-in-light / white-in-dark) border stays
31946
+ visible over any page background. */
31947
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.6), 0 0 0 3px rgba(0, 0, 0, 0.15);
31948
+ background: rgba(127, 127, 127, 0.14);
31771
31949
  border-radius: 3px;
31772
31950
  transition: all 60ms linear;
31773
31951
  display: none;
@@ -31779,7 +31957,7 @@ var OVERLAY_CSS = (
31779
31957
  padding: 1px 6px;
31780
31958
  border-radius: 4px;
31781
31959
  background: var(--lens-accent);
31782
- color: #fff;
31960
+ color: var(--lens-on-accent);
31783
31961
  font: 500 11px/1.5 var(--lens-font);
31784
31962
  white-space: nowrap;
31785
31963
  }
@@ -31858,7 +32036,7 @@ var OVERLAY_CSS = (
31858
32036
  padding: 14px 16px;
31859
32037
  border-bottom: 1px solid rgba(0, 0, 0, 0.07);
31860
32038
  }
31861
- .lens-panel__title { font-size: 14px; font-weight: 600; }
32039
+ .lens-panel__title { font-size: 14px; font-weight: 600; margin-right: auto; }
31862
32040
  .lens-panel__x {
31863
32041
  display: inline-flex;
31864
32042
  align-items: center;
@@ -31896,7 +32074,7 @@ var OVERLAY_CSS = (
31896
32074
  .lens-textarea:focus {
31897
32075
  outline: none;
31898
32076
  border-color: var(--lens-accent);
31899
- box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.18);
32077
+ box-shadow: 0 0 0 3px var(--lens-focus);
31900
32078
  }
31901
32079
  /* ---- Context row: static, non-interactive page facts (muted, dot-joined) ---- */
31902
32080
  .lens-context {
@@ -31911,49 +32089,6 @@ var OVERLAY_CSS = (
31911
32089
  .lens-context__item { white-space: nowrap; }
31912
32090
  .lens-context__dot { opacity: 0.55; }
31913
32091
 
31914
- /* ---- Include row: real toggles (role=switch) for which data to attach ---- */
31915
- .lens-includes {
31916
- display: flex;
31917
- flex-wrap: wrap;
31918
- align-items: center;
31919
- gap: 6px;
31920
- }
31921
- .lens-includes__label {
31922
- font-size: 11.5px;
31923
- font-weight: 600;
31924
- color: #71717a;
31925
- margin-right: 2px;
31926
- }
31927
- .lens-tgl {
31928
- display: inline-flex;
31929
- align-items: center;
31930
- gap: 5px;
31931
- padding: 4px 10px;
31932
- border-radius: 999px;
31933
- border: 1px solid rgba(0, 0, 0, 0.14);
31934
- background: #fafafa;
31935
- color: #a1a1aa;
31936
- font-family: var(--lens-font);
31937
- font-size: 11.5px;
31938
- font-weight: 600;
31939
- white-space: nowrap;
31940
- cursor: pointer;
31941
- opacity: 0.75;
31942
- transition: background 150ms ease, border-color 150ms ease, color 150ms ease, opacity 150ms ease;
31943
- }
31944
- .lens-tgl:hover { opacity: 1; }
31945
- .lens-tgl:focus-visible { outline: 2px solid var(--lens-accent); outline-offset: 1px; }
31946
- .lens-tgl__check { display: none; align-items: center; }
31947
- .lens-tgl__check svg { width: 13px; height: 13px; }
31948
- .lens-tgl__count { font-weight: 500; opacity: 0.75; font-variant-numeric: tabular-nums; }
31949
- .lens-tgl.is-on {
31950
- opacity: 1;
31951
- color: var(--lens-accent-strong);
31952
- border-color: rgba(99, 102, 241, 0.5);
31953
- background: #eef2ff;
31954
- }
31955
- .lens-tgl.is-on .lens-tgl__check { display: inline-flex; }
31956
-
31957
32092
  .lens-chips { display: flex; flex-wrap: wrap; gap: 6px; }
31958
32093
  .lens-chip {
31959
32094
  display: inline-flex;
@@ -31974,9 +32109,9 @@ var OVERLAY_CSS = (
31974
32109
  .lens-chip--ok { color: #15803d; background: #f0fdf4; border-color: rgba(21, 128, 61, 0.18); }
31975
32110
  .lens-chip--target {
31976
32111
  max-width: 100%;
31977
- background: #eef2ff;
31978
- border-color: rgba(99, 102, 241, 0.25);
31979
- color: var(--lens-accent-strong);
32112
+ background: rgba(0, 0, 0, 0.05);
32113
+ border-color: rgba(0, 0, 0, 0.16);
32114
+ color: #18181b;
31980
32115
  }
31981
32116
  .lens-chip__code {
31982
32117
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
@@ -32024,7 +32159,7 @@ var OVERLAY_CSS = (
32024
32159
  .lens-abtn svg { flex: none; }
32025
32160
  .lens-abtn--ghost { background: transparent; border-color: rgba(0, 0, 0, 0.14); color: #3f3f46; }
32026
32161
  .lens-abtn--ghost:hover { background: rgba(0, 0, 0, 0.04); }
32027
- .lens-abtn--primary { background: var(--lens-accent); color: #fff; }
32162
+ .lens-abtn--primary { background: var(--lens-accent); color: var(--lens-on-accent); }
32028
32163
  .lens-abtn--primary:hover { background: var(--lens-accent-strong); }
32029
32164
  .lens-abtn:disabled { opacity: 0.55; cursor: default; }
32030
32165
  .lens-abtn:focus-visible { outline: 2px solid var(--lens-accent); outline-offset: 1px; }
@@ -32078,12 +32213,9 @@ var OVERLAY_CSS = (
32078
32213
  .lens-panel__x:hover { background: rgba(255, 255, 255, 0.08); }
32079
32214
  .lens-textarea { background: #26262a; border-color: rgba(255, 255, 255, 0.14); color: #f4f4f5; }
32080
32215
  .lens-context { color: #a1a1aa; }
32081
- .lens-includes__label { color: #a1a1aa; }
32082
- .lens-tgl { background: #26262a; border-color: rgba(255, 255, 255, 0.12); color: #a1a1aa; }
32083
- .lens-tgl.is-on { background: rgba(99, 102, 241, 0.18); border-color: rgba(99, 102, 241, 0.55); color: #c7d2fe; }
32084
32216
  .lens-chip { background: #26262a; border-color: rgba(255, 255, 255, 0.08); color: #d4d4d8; }
32085
32217
  .lens-chip--ok { color: #4ade80; background: rgba(34, 197, 94, 0.12); border-color: rgba(74, 222, 128, 0.22); }
32086
- .lens-chip--target { background: rgba(99, 102, 241, 0.16); color: #c7d2fe; }
32218
+ .lens-chip--target { background: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.18); color: #f4f4f5; }
32087
32219
  .lens-abtn--ghost { border-color: rgba(255, 255, 255, 0.16); color: #d4d4d8; }
32088
32220
  .lens-abtn--ghost:hover { background: rgba(255, 255, 255, 0.06); }
32089
32221
  .lens-state__msg { color: #a1a1aa; }
@@ -32117,7 +32249,9 @@ var OVERLAY_CSS = (
32117
32249
  .lens-annotate-chips { z-index: var(--lens-z); }
32118
32250
 
32119
32251
  /* ---------------- Annotation toolbar ---------------- */
32120
- /* Same floating top-center anchor as the chips row; wraps on narrow screens. */
32252
+ /* Floating top-center anchor. A fixed drag grip sits left of a tools "track":
32253
+ the track wraps on desktop and becomes a single horizontal-scroll row on
32254
+ narrow screens, so the bar stays one slim height instead of a bulky block. */
32121
32255
  .lens-annotate-toolbar {
32122
32256
  position: fixed;
32123
32257
  top: calc(16px + env(safe-area-inset-top));
@@ -32125,11 +32259,10 @@ var OVERLAY_CSS = (
32125
32259
  transform: translateX(-50%);
32126
32260
  z-index: var(--lens-z);
32127
32261
  display: inline-flex;
32128
- flex-wrap: wrap;
32262
+ flex-wrap: nowrap;
32129
32263
  align-items: center;
32130
- justify-content: center;
32131
32264
  gap: 6px;
32132
- max-width: calc(100vw - 24px);
32265
+ max-width: calc(100vw - 16px);
32133
32266
  padding: 6px 8px;
32134
32267
  border-radius: 14px;
32135
32268
  background: rgba(20, 20, 23, 0.92);
@@ -32139,12 +32272,50 @@ var OVERLAY_CSS = (
32139
32272
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.35);
32140
32273
  color: #f4f4f5;
32141
32274
  }
32275
+ /* Tools lay out as ONE horizontal row that scrolls if it can't fit, so the bar
32276
+ is always a single slim height \u2014 never a wrapping (or, worse, min-content
32277
+ collapsed vertical) block. The grip sits outside this track. */
32278
+ .lens-tool-track {
32279
+ display: flex;
32280
+ align-items: center;
32281
+ flex-wrap: nowrap;
32282
+ justify-content: flex-start;
32283
+ gap: 6px;
32284
+ flex: 1 1 auto;
32285
+ min-width: 0;
32286
+ overflow-x: auto;
32287
+ overflow-y: hidden;
32288
+ scrollbar-width: none;
32289
+ -webkit-overflow-scrolling: touch;
32290
+ }
32291
+ .lens-tool-track::-webkit-scrollbar { display: none; }
32142
32292
  .lens-tool-group {
32143
32293
  display: inline-flex;
32144
32294
  align-items: center;
32145
32295
  gap: 4px;
32146
- flex-wrap: wrap;
32296
+ flex-wrap: nowrap;
32297
+ flex: none;
32147
32298
  }
32299
+ /* Single close (=Done, keeps marks) pinned at the right, always reachable
32300
+ while the tools scroll. Reads as a light control on the dark bar. */
32301
+ .lens-tool-close {
32302
+ display: inline-flex;
32303
+ align-items: center;
32304
+ justify-content: center;
32305
+ flex: none;
32306
+ width: 32px;
32307
+ height: 30px;
32308
+ padding: 0;
32309
+ border: 0;
32310
+ border-radius: 8px;
32311
+ background: rgba(255, 255, 255, 0.14);
32312
+ color: #f4f4f5;
32313
+ cursor: pointer;
32314
+ transition: background 150ms ease;
32315
+ }
32316
+ .lens-tool-close:hover { background: rgba(255, 255, 255, 0.26); }
32317
+ .lens-tool-close:focus-visible { outline: 2px solid rgba(255, 255, 255, 0.7); outline-offset: 1px; }
32318
+ .lens-tool-close svg { width: 18px; height: 18px; }
32148
32319
  .lens-tool-sep {
32149
32320
  width: 1px;
32150
32321
  align-self: stretch;
@@ -32152,6 +32323,51 @@ var OVERLAY_CSS = (
32152
32323
  background: rgba(255, 255, 255, 0.16);
32153
32324
  margin: 0 2px;
32154
32325
  }
32326
+
32327
+ /* ---------------- Draggable surfaces ---------------- */
32328
+ /* Every drag handle: no page pan/scroll gesture starts here, and the cursor
32329
+ signals grabbability. Applied to the pill, panel header, toolbar grip, chips. */
32330
+ .lens-drag-handle { touch-action: none; cursor: grab; }
32331
+ .lens-drag-handle:active { cursor: grabbing; }
32332
+ .is-dragging, .is-dragging * { cursor: grabbing !important; }
32333
+ .is-dragging { user-select: none; }
32334
+ /* While dragging, drop the entry animation/transition so it tracks 1:1. */
32335
+ .lens-pill.is-dragging, .lens-panel.is-dragging,
32336
+ .lens-annotate-toolbar.is-dragging, .lens-rec-chip.is-dragging {
32337
+ animation: none;
32338
+ transition: none;
32339
+ }
32340
+
32341
+ /* Toolbar drag grip \u2014 the only grab region, kept out of the scroll track. */
32342
+ .lens-tool-grip {
32343
+ display: inline-flex;
32344
+ align-items: center;
32345
+ justify-content: center;
32346
+ flex: none;
32347
+ width: 22px;
32348
+ height: 30px;
32349
+ margin-right: 2px;
32350
+ border-radius: 6px;
32351
+ color: #a1a1aa;
32352
+ }
32353
+ .lens-tool-grip:hover { color: #f4f4f5; background: rgba(255, 255, 255, 0.08); }
32354
+ .lens-tool-grip svg { width: 16px; height: 16px; }
32355
+
32356
+ /* Pill grip (the eye dot) + panel header grip. */
32357
+ .lens-pill__grip { cursor: grab; }
32358
+ .lens-panel__grip {
32359
+ display: inline-flex;
32360
+ align-items: center;
32361
+ justify-content: center;
32362
+ flex: none;
32363
+ width: 22px;
32364
+ height: 22px;
32365
+ margin-right: 6px;
32366
+ color: #a1a1aa;
32367
+ }
32368
+ .lens-panel__grip svg { width: 16px; height: 16px; }
32369
+ .lens-panel__head { cursor: grab; }
32370
+ .lens-panel__head:active { cursor: grabbing; }
32155
32371
  .lens-tool-btn {
32156
32372
  display: inline-flex;
32157
32373
  align-items: center;
@@ -32167,11 +32383,11 @@ var OVERLAY_CSS = (
32167
32383
  transition: background 150ms ease, color 150ms ease, border-color 150ms ease;
32168
32384
  }
32169
32385
  .lens-tool-btn:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
32170
- .lens-tool-btn:focus-visible { outline: 2px solid var(--lens-accent); outline-offset: 1px; }
32386
+ .lens-tool-btn:focus-visible { outline: 2px solid rgba(255, 255, 255, 0.7); outline-offset: 1px; }
32171
32387
  .lens-tool-btn.is-active {
32172
- background: var(--lens-accent);
32173
- border-color: var(--lens-accent);
32174
- color: #fff;
32388
+ background: #fff;
32389
+ border-color: #fff;
32390
+ color: #18181b;
32175
32391
  }
32176
32392
  .lens-tool-btn:disabled { opacity: 0.4; cursor: default; }
32177
32393
  .lens-tool-btn:disabled:hover { background: transparent; color: #d4d4d8; }
@@ -32211,17 +32427,18 @@ var OVERLAY_CSS = (
32211
32427
  transition: background 150ms ease, color 150ms ease, border-color 150ms ease;
32212
32428
  }
32213
32429
  .lens-width-btn:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
32214
- .lens-width-btn:focus-visible { outline: 2px solid var(--lens-accent); outline-offset: 1px; }
32430
+ .lens-width-btn:focus-visible { outline: 2px solid rgba(255, 255, 255, 0.7); outline-offset: 1px; }
32215
32431
  .lens-width-btn.is-active {
32216
- background: var(--lens-accent);
32217
- border-color: var(--lens-accent);
32218
- color: #fff;
32432
+ background: #fff;
32433
+ border-color: #fff;
32434
+ color: #18181b;
32219
32435
  }
32220
32436
 
32221
32437
  /* Coarse pointers: bump the tool targets toward 40px. */
32222
32438
  @media (pointer: coarse) {
32223
32439
  .lens-tool-btn, .lens-width-btn { width: 38px; height: 38px; }
32224
32440
  .lens-swatch { width: 30px; height: 30px; }
32441
+ .lens-tool-grip { width: 30px; height: 38px; }
32225
32442
  }
32226
32443
 
32227
32444
  /* Stacking inside the shadow layer: the panel, pill and floating chips always
@@ -32246,8 +32463,8 @@ var OVERLAY_CSS = (
32246
32463
  transition: background 150ms ease;
32247
32464
  }
32248
32465
  .lens-pick-chip__btn:hover { background: rgba(255, 255, 255, 0.2); }
32249
- .lens-pick-chip__btn--primary { background: var(--lens-accent); }
32250
- .lens-pick-chip__btn--primary:hover { background: var(--lens-accent-strong); }
32466
+ .lens-pick-chip__btn--primary { background: #fff; color: #18181b; }
32467
+ .lens-pick-chip__btn--primary:hover { background: #e4e4e7; }
32251
32468
 
32252
32469
  /* ---------------- Recording chip ---------------- */
32253
32470
  .lens-rec-chip {
@@ -32302,21 +32519,29 @@ var OVERLAY_CSS = (
32302
32519
  .lens-rec-chip__stop:hover { background: #dc2626; }
32303
32520
  .lens-chip--rec { gap: 6px; }
32304
32521
 
32522
+ /* Very narrow screens: collapse the pill's "Report issue" label to its icon so
32523
+ the pill stays a single slim row (never wraps into a 2-row blob). */
32524
+ @media (max-width: 400px) {
32525
+ .lens-btn__label { display: none; }
32526
+ .lens-btn--report { width: 44px; padding: 0; justify-content: center; }
32527
+ }
32528
+
32305
32529
  /* ---------------- Coarse pointers (touch): \u226544px targets ---------------- */
32306
32530
  @media (pointer: coarse) {
32307
32531
  .lens-btn { height: 44px; padding: 0 16px; }
32308
32532
  .lens-btn--icon { width: 44px; }
32533
+ .lens-tool-close { width: 44px; height: 38px; }
32309
32534
  .lens-pill__dot { width: 40px; height: 40px; }
32310
32535
  .lens-abtn { height: 44px; }
32311
- .lens-tgl { min-height: 40px; padding: 4px 14px; }
32312
32536
  .lens-panel__x { width: 44px; height: 44px; }
32313
32537
  .lens-chip__x { min-width: 32px; min-height: 32px; align-items: center; justify-content: center; }
32314
32538
  .lens-pick-chip__btn, .lens-rec-chip__stop { height: 40px; padding: 0 18px; }
32315
32539
  }
32316
32540
 
32317
- /* ---------------- Bottom sheet under 640px ---------------- */
32541
+ /* ---------------- Narrow screens (\u2264640px) ---------------- */
32318
32542
  @media (max-width: 640px) {
32319
- .lens-panel {
32543
+ /* Report panel as a bottom sheet (unless the user has dragged it loose). */
32544
+ .lens-panel:not(.is-dragging) {
32320
32545
  left: 0;
32321
32546
  right: 0;
32322
32547
  bottom: 0;
@@ -32333,6 +32558,14 @@ var OVERLAY_CSS = (
32333
32558
  to { opacity: 1; transform: none; }
32334
32559
  }
32335
32560
  .lens-textarea { font-size: 16px; }
32561
+
32562
+ /* On narrow screens the single-row tool track usually overflows: fade its
32563
+ trailing edge so it reads as "more tools this way" (the grip sits outside
32564
+ the track and stays fully opaque). */
32565
+ .lens-tool-track {
32566
+ -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 20px), transparent 100%);
32567
+ mask-image: linear-gradient(90deg, #000 calc(100% - 20px), transparent 100%);
32568
+ }
32336
32569
  }
32337
32570
  `
32338
32571
  );
@@ -32350,8 +32583,14 @@ var Overlay = class {
32350
32583
  this.shadow.append(this.layer);
32351
32584
  this.picker = new ElementPicker(this.layer, this.host);
32352
32585
  this.annotator = new AnnotationEditor(this.layer);
32353
- this.layer.append(this.buildPill());
32586
+ const pill = this.buildPill();
32587
+ this.layer.append(pill);
32354
32588
  document.documentElement.append(this.host);
32589
+ this.pillDrag = makeDraggable(pill, {
32590
+ handle: pill,
32591
+ key: "lens-pill",
32592
+ ignore: isInteractiveTarget
32593
+ });
32355
32594
  window.addEventListener("keydown", this.onGlobalKey, true);
32356
32595
  }
32357
32596
  deps;
@@ -32364,7 +32603,9 @@ var Overlay = class {
32364
32603
  panel = null;
32365
32604
  target = null;
32366
32605
  destroyed = false;
32606
+ pillDrag = null;
32367
32607
  recordingChip = null;
32608
+ recordingDrag = null;
32368
32609
  recordingTimerEl = null;
32369
32610
  recordingInterval = null;
32370
32611
  recordingStartTs = 0;
@@ -32379,10 +32620,16 @@ var Overlay = class {
32379
32620
  on: { click: () => this.togglePicker() }
32380
32621
  });
32381
32622
  return el("div", { class: "lens-pill", attrs: { role: "toolbar", "aria-label": "Lens" } }, [
32382
- el("span", { class: "lens-pill__dot", html: iconEye }),
32623
+ el("span", {
32624
+ class: "lens-pill__dot lens-pill__grip",
32625
+ html: iconEye,
32626
+ title: "Drag to move",
32627
+ ariaLabel: "Drag to move Lens"
32628
+ }),
32383
32629
  el("button", {
32384
- class: "lens-btn",
32385
- text: "Report issue",
32630
+ class: "lens-btn lens-btn--report",
32631
+ html: iconReport + '<span class="lens-btn__label">Report issue</span>',
32632
+ ariaLabel: "Report issue",
32386
32633
  on: { click: () => this.open() }
32387
32634
  }),
32388
32635
  this.pickBtn,
@@ -32436,6 +32683,7 @@ var Overlay = class {
32436
32683
  this.stopPicker();
32437
32684
  return;
32438
32685
  }
32686
+ if (this.annotator.isActive) this.annotator.exit();
32439
32687
  this.close();
32440
32688
  this.pickBtn.classList.add("is-active");
32441
32689
  this.picker.start(
@@ -32497,6 +32745,11 @@ var Overlay = class {
32497
32745
  })
32498
32746
  ]);
32499
32747
  this.layer.append(this.recordingChip);
32748
+ this.recordingDrag = makeDraggable(this.recordingChip, {
32749
+ handle: this.recordingChip,
32750
+ key: "lens-rec-chip",
32751
+ ignore: isInteractiveTarget
32752
+ });
32500
32753
  this.recordingInterval = setInterval(() => this.tickRecording(), 250);
32501
32754
  }
32502
32755
  tickRecording() {
@@ -32509,6 +32762,10 @@ var Overlay = class {
32509
32762
  clearInterval(this.recordingInterval);
32510
32763
  this.recordingInterval = null;
32511
32764
  }
32765
+ if (this.recordingDrag) {
32766
+ this.recordingDrag.destroy();
32767
+ this.recordingDrag = null;
32768
+ }
32512
32769
  if (this.recordingChip) {
32513
32770
  this.recordingChip.remove();
32514
32771
  this.recordingChip = null;
@@ -32549,6 +32806,8 @@ var Overlay = class {
32549
32806
  if (this.destroyed) return;
32550
32807
  this.destroyed = true;
32551
32808
  window.removeEventListener("keydown", this.onGlobalKey, true);
32809
+ this.pillDrag?.destroy();
32810
+ this.pillDrag = null;
32552
32811
  this.stopPicker();
32553
32812
  if (this.deps.client.isRecording()) this.deps.client.stopRecording();
32554
32813
  this.hideRecordingChip();