@merittdev/horus-lens 0.3.0 → 0.5.0

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/browser.cjs CHANGED
@@ -29082,7 +29082,7 @@ var ApiErrorSchema = external_exports.object({
29082
29082
 
29083
29083
  // ../core/dist/index.js
29084
29084
  var SDK_NAME = "@merittdev/horus-lens";
29085
- var SDK_VERSION = true ? "0.3.0" : "0.0.0-dev";
29085
+ var SDK_VERSION = true ? "0.5.0" : "0.0.0-dev";
29086
29086
  var DEFAULT_ENDPOINT = "https://api.horus.sh";
29087
29087
  var DEFAULT_REPLAY_WINDOW_MS = 15e3;
29088
29088
  var DEFAULT_MAX_CONSOLE_ENTRIES = 200;
@@ -29143,11 +29143,15 @@ function startAutoCapture(client, options = {}) {
29143
29143
  const last = lastSeen.get(signature);
29144
29144
  if (last !== void 0 && now - last < AUTO_CAPTURE_COOLDOWN_MS) return;
29145
29145
  if (sessionCount >= AUTO_CAPTURE_SESSION_CAP) return;
29146
+ const previousSeen = lastSeen.get(signature);
29146
29147
  lastSeen.set(signature, now);
29147
29148
  sessionCount += 1;
29148
29149
  submitting = true;
29149
29150
  options.onCapture?.(signature);
29150
29151
  void Promise.resolve().then(() => client.submitReport({ trigger: "error", include: { screenshot: false } })).catch(() => {
29152
+ if (previousSeen === void 0) lastSeen.delete(signature);
29153
+ else lastSeen.set(signature, previousSeen);
29154
+ sessionCount -= 1;
29151
29155
  }).finally(() => {
29152
29156
  submitting = false;
29153
29157
  });
@@ -29202,6 +29206,14 @@ var RingBuffer = class {
29202
29206
  return this.items.length;
29203
29207
  }
29204
29208
  };
29209
+ var HttpError = class extends Error {
29210
+ constructor(status, message) {
29211
+ super(message);
29212
+ this.status = status;
29213
+ this.name = "HttpError";
29214
+ }
29215
+ status;
29216
+ };
29205
29217
  var Transport = class {
29206
29218
  constructor(config2) {
29207
29219
  this.config = config2;
@@ -29209,18 +29221,23 @@ var Transport = class {
29209
29221
  config;
29210
29222
  async initiate(assets) {
29211
29223
  const body = { accessId: this.config.accessId, assets };
29212
- return this.postJson(`${this.config.endpoint}/v1/reports/initiate`, body);
29224
+ return this.postJson(
29225
+ `${this.config.endpoint}/v1/reports/initiate`,
29226
+ body,
29227
+ { retries: 0 }
29228
+ );
29213
29229
  }
29214
29230
  /** Record a late-uploaded asset (background screenshot) on a finalized report. */
29215
29231
  async attachAsset(reportId, asset) {
29216
29232
  const body = { accessId: this.config.accessId, ...asset };
29217
29233
  await this.postJson(`${this.config.endpoint}/v1/reports/${encodeURIComponent(reportId)}/assets`, body);
29218
29234
  }
29219
- async uploadAsset(upload, blob2) {
29235
+ async uploadAsset(upload, blob2, signal) {
29220
29236
  const response = await fetch(upload.url, {
29221
29237
  method: upload.method,
29222
29238
  headers: upload.headers,
29223
- body: blob2
29239
+ body: blob2,
29240
+ signal
29224
29241
  });
29225
29242
  if (!response.ok) {
29226
29243
  throw new Error(`Asset upload failed (${upload.kind}): ${response.status}`);
@@ -29228,12 +29245,20 @@ var Transport = class {
29228
29245
  }
29229
29246
  async finalize(reportId, metadata, uploadedAssets) {
29230
29247
  const body = { accessId: this.config.accessId, metadata, uploadedAssets };
29231
- return this.postJson(
29232
- `${this.config.endpoint}/v1/reports/${reportId}/finalize`,
29233
- body
29234
- );
29248
+ try {
29249
+ return await this.postJson(
29250
+ `${this.config.endpoint}/v1/reports/${reportId}/finalize`,
29251
+ body
29252
+ );
29253
+ } catch (err) {
29254
+ if (err instanceof HttpError && err.status === 409) {
29255
+ return { reportId, status: "submitted" };
29256
+ }
29257
+ throw err;
29258
+ }
29235
29259
  }
29236
- async postJson(url2, body, retries = 1) {
29260
+ async postJson(url2, body, opts = {}) {
29261
+ const retries = opts.retries ?? 1;
29237
29262
  let lastError;
29238
29263
  for (let attempt = 0; attempt <= retries; attempt++) {
29239
29264
  try {
@@ -29244,7 +29269,10 @@ var Transport = class {
29244
29269
  });
29245
29270
  if (!response.ok) {
29246
29271
  const text = await response.text().catch(() => "");
29247
- throw new Error(`Request to ${url2} failed: ${response.status} ${text}`.trim());
29272
+ throw new HttpError(
29273
+ response.status,
29274
+ `Request to ${url2} failed: ${response.status} ${text}`.trim()
29275
+ );
29248
29276
  }
29249
29277
  return await response.json();
29250
29278
  } catch (err) {
@@ -29685,8 +29713,14 @@ async function captureScreenshot(excludeHost, crop, composite) {
29685
29713
  return null;
29686
29714
  }
29687
29715
  }
29688
- if (crop) {
29689
- const cropped = await cropBlob(blob2, crop, scale);
29716
+ const effectiveCrop = crop ?? (composite ? void 0 : {
29717
+ x: window.scrollX,
29718
+ y: window.scrollY,
29719
+ width: window.innerWidth,
29720
+ height: window.innerHeight
29721
+ });
29722
+ if (effectiveCrop) {
29723
+ const cropped = await cropBlob(blob2, effectiveCrop, scale);
29690
29724
  return cropped ?? blob2;
29691
29725
  }
29692
29726
  return blob2;
@@ -30120,12 +30154,22 @@ var LensClient = class {
30120
30154
  input2.screenshotCrop,
30121
30155
  input2.annotationComposite
30122
30156
  ) : Promise.resolve(null);
30157
+ const consumingManualRecording = wantReplay && (this.replay?.hasManualRecording() ?? false) && !(this.replay?.isManualRecording() ?? false);
30123
30158
  const replayEvents = wantReplay ? this.replay?.snapshot() ?? [] : [];
30124
30159
  const errorEntries = this.errorBuffer.drain();
30125
- const consoleTail = this.consoleBuffer.tail(CONSOLE_TAIL_SIZE);
30126
- const networkTail = this.networkBuffer.tail(NETWORK_TAIL_SIZE);
30127
30160
  const consoleEntries = this.consoleBuffer.drain();
30128
30161
  const networkEntries = this.networkBuffer.drain();
30162
+ this.errorBuffer.clear();
30163
+ this.consoleBuffer.clear();
30164
+ this.networkBuffer.clear();
30165
+ const consoleTail = consoleEntries.slice(-CONSOLE_TAIL_SIZE);
30166
+ const isNetFailure = (e) => e.ok !== true || Boolean(e.error);
30167
+ const recentFailures = networkEntries.filter(isNetFailure).slice(-NETWORK_TAIL_SIZE);
30168
+ const keep = /* @__PURE__ */ new Set([
30169
+ ...recentFailures,
30170
+ ...networkEntries.slice(-NETWORK_TAIL_SIZE)
30171
+ ]);
30172
+ const networkTail = networkEntries.filter((e) => keep.has(e)).slice(-NETWORK_TAIL_SIZE);
30129
30173
  let metadata = {
30130
30174
  annotations: input2.annotations ?? [],
30131
30175
  env: collectEnv(),
@@ -30179,13 +30223,14 @@ var LensClient = class {
30179
30223
  }
30180
30224
  if (!asset.blob) continue;
30181
30225
  try {
30182
- await this.transport.uploadAsset(upload, asset.blob);
30226
+ await this.uploadAssetWithTimeout(upload, asset.blob);
30183
30227
  uploadedAssets.push({ kind: upload.kind, key: upload.key, byteSize: asset.blob.size });
30184
30228
  } catch (err) {
30185
30229
  this.warn(`asset upload failed (${upload.kind})`, err);
30186
30230
  }
30187
30231
  }
30188
30232
  const finalized = await this.transport.finalize(initiated.reportId, metadata, uploadedAssets);
30233
+ if (consumingManualRecording) this.replay?.discardManualRecording();
30189
30234
  if (screenshotUpload) {
30190
30235
  const upload = screenshotUpload;
30191
30236
  void screenshotPromise.then(async (blob2) => {
@@ -30203,6 +30248,21 @@ var LensClient = class {
30203
30248
  }
30204
30249
  return { reportId: finalized.reportId };
30205
30250
  }
30251
+ /**
30252
+ * Upload a foreground asset with a bounded timeout. Without this a stuck S3
30253
+ * PUT (dropped connection, half-open socket) would hang `submitReport`
30254
+ * forever; the timeout aborts the individual PUT so it fails as one asset
30255
+ * (caught by the caller) instead of wedging the whole submit.
30256
+ */
30257
+ async uploadAssetWithTimeout(upload, blob2) {
30258
+ const controller = new AbortController();
30259
+ const timer = setTimeout(() => controller.abort(), SCREENSHOT_TIMEOUT_MS);
30260
+ try {
30261
+ await this.transport.uploadAsset(upload, blob2, controller.signal);
30262
+ } finally {
30263
+ clearTimeout(timer);
30264
+ }
30265
+ }
30206
30266
  warn(message, err) {
30207
30267
  if (!this.debug) return;
30208
30268
  if (err !== void 0) console.warn(`[lens] ${message}`, err);
@@ -30331,6 +30391,113 @@ function el(tag, props = {}, children = []) {
30331
30391
  function clear(node2) {
30332
30392
  while (node2.firstChild) node2.removeChild(node2.firstChild);
30333
30393
  }
30394
+ var MARGIN = 6;
30395
+ var DRAG_THRESHOLD = 3;
30396
+ var savedPositions = /* @__PURE__ */ new Map();
30397
+ function isInteractiveTarget(target) {
30398
+ return target instanceof Element && target.closest(
30399
+ 'button, a, input, textarea, select, [role="switch"], [role="button"], [contenteditable="true"]'
30400
+ ) !== null;
30401
+ }
30402
+ function clampToViewport(element, left, top) {
30403
+ const rect = element.getBoundingClientRect();
30404
+ const vw = window.innerWidth;
30405
+ const vh = window.innerHeight;
30406
+ const maxLeft = Math.max(MARGIN, vw - rect.width - MARGIN);
30407
+ const maxTop = Math.max(MARGIN, vh - rect.height - MARGIN);
30408
+ return {
30409
+ left: Math.min(Math.max(left, MARGIN), maxLeft),
30410
+ top: Math.min(Math.max(top, MARGIN), maxTop)
30411
+ };
30412
+ }
30413
+ function makeDraggable(element, opts) {
30414
+ const { handle, key } = opts;
30415
+ const ignore = opts.ignore ?? isInteractiveTarget;
30416
+ let startX = 0;
30417
+ let startY = 0;
30418
+ let originLeft = 0;
30419
+ let originTop = 0;
30420
+ let dragging = false;
30421
+ let armed = false;
30422
+ let pointerId = null;
30423
+ const apply = (left, top) => {
30424
+ const p = clampToViewport(element, left, top);
30425
+ element.style.left = `${p.left}px`;
30426
+ element.style.top = `${p.top}px`;
30427
+ element.style.right = "auto";
30428
+ element.style.bottom = "auto";
30429
+ element.style.transform = "none";
30430
+ savedPositions.set(key, p);
30431
+ opts.onMove?.();
30432
+ };
30433
+ const onPointerDown = (e) => {
30434
+ if (e.button !== 0 && e.pointerType === "mouse") return;
30435
+ if (ignore(e.target)) return;
30436
+ armed = true;
30437
+ dragging = false;
30438
+ pointerId = e.pointerId;
30439
+ startX = e.clientX;
30440
+ startY = e.clientY;
30441
+ const rect = element.getBoundingClientRect();
30442
+ originLeft = rect.left;
30443
+ originTop = rect.top;
30444
+ window.addEventListener("pointermove", onPointerMove, true);
30445
+ window.addEventListener("pointerup", onPointerUp, true);
30446
+ window.addEventListener("pointercancel", onPointerUp, true);
30447
+ };
30448
+ const onPointerMove = (e) => {
30449
+ if (!armed || e.pointerId !== pointerId) return;
30450
+ const dx = e.clientX - startX;
30451
+ const dy = e.clientY - startY;
30452
+ if (!dragging) {
30453
+ if (Math.abs(dx) + Math.abs(dy) < DRAG_THRESHOLD) return;
30454
+ dragging = true;
30455
+ element.classList.add("is-dragging");
30456
+ }
30457
+ e.preventDefault();
30458
+ apply(originLeft + dx, originTop + dy);
30459
+ };
30460
+ const onPointerUp = (e) => {
30461
+ if (e.pointerId !== pointerId) return;
30462
+ armed = false;
30463
+ if (dragging) {
30464
+ dragging = false;
30465
+ element.classList.remove("is-dragging");
30466
+ }
30467
+ pointerId = null;
30468
+ window.removeEventListener("pointermove", onPointerMove, true);
30469
+ window.removeEventListener("pointerup", onPointerUp, true);
30470
+ window.removeEventListener("pointercancel", onPointerUp, true);
30471
+ };
30472
+ const onResize = () => {
30473
+ const p = savedPositions.get(key);
30474
+ if (p) apply(p.left, p.top);
30475
+ };
30476
+ handle.classList.add("lens-drag-handle");
30477
+ handle.addEventListener("pointerdown", onPointerDown);
30478
+ window.addEventListener("resize", onResize);
30479
+ const stored = savedPositions.get(key);
30480
+ if (stored) requestAnimationFrame(() => apply(stored.left, stored.top));
30481
+ return {
30482
+ reclamp: onResize,
30483
+ reset() {
30484
+ savedPositions.delete(key);
30485
+ element.style.left = "";
30486
+ element.style.top = "";
30487
+ element.style.right = "";
30488
+ element.style.bottom = "";
30489
+ element.style.transform = "";
30490
+ element.classList.remove("is-dragging");
30491
+ },
30492
+ destroy() {
30493
+ handle.removeEventListener("pointerdown", onPointerDown);
30494
+ window.removeEventListener("pointermove", onPointerMove, true);
30495
+ window.removeEventListener("pointerup", onPointerUp, true);
30496
+ window.removeEventListener("pointercancel", onPointerUp, true);
30497
+ window.removeEventListener("resize", onResize);
30498
+ }
30499
+ };
30500
+ }
30334
30501
  var THIN_MIN_DIST = 3;
30335
30502
  var MAX_POINTS = 2e3;
30336
30503
  var HIGHLIGHTER_WIDTH = 14;
@@ -30481,6 +30648,8 @@ var iconTrash = svg(
30481
30648
  '<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"/>'
30482
30649
  );
30483
30650
  var iconAnnotate = iconPencil;
30651
+ 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>`;
30652
+ 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"/>');
30484
30653
  var SVG_NS = "http://www.w3.org/2000/svg";
30485
30654
  var FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
30486
30655
  var MIN_DRAG = 6;
@@ -30509,6 +30678,7 @@ var AnnotationEditor = class {
30509
30678
  svg = null;
30510
30679
  bg = null;
30511
30680
  toolbar = null;
30681
+ toolbarDrag = null;
30512
30682
  textInputEl = null;
30513
30683
  textAnchor = null;
30514
30684
  textColor = COLORS[0];
@@ -30554,6 +30724,7 @@ var AnnotationEditor = class {
30554
30724
  this.markNodes = [];
30555
30725
  this.annotations = [];
30556
30726
  this.stepCounter = 0;
30727
+ this.sessionBase = Math.min(this.sessionBase, this.annotations.length);
30557
30728
  if (!this.active) this.teardownLayer();
30558
30729
  this.syncActionButtons();
30559
30730
  }
@@ -30568,6 +30739,7 @@ var AnnotationEditor = class {
30568
30739
  this.sizeLayer();
30569
30740
  this.updateTransform();
30570
30741
  svg2.style.pointerEvents = "auto";
30742
+ if (this.bg) this.bg.style.pointerEvents = "all";
30571
30743
  svg2.style.touchAction = "none";
30572
30744
  svg2.style.cursor = "crosshair";
30573
30745
  svg2.addEventListener("pointerdown", this.onPointerDown);
@@ -30588,6 +30760,7 @@ var AnnotationEditor = class {
30588
30760
  if (svg2) {
30589
30761
  svg2.removeEventListener("pointerdown", this.onPointerDown);
30590
30762
  svg2.style.pointerEvents = "none";
30763
+ if (this.bg) this.bg.style.pointerEvents = "none";
30591
30764
  svg2.style.touchAction = "";
30592
30765
  svg2.style.cursor = "";
30593
30766
  }
@@ -30597,15 +30770,8 @@ var AnnotationEditor = class {
30597
30770
  this.onExit = null;
30598
30771
  cb?.();
30599
30772
  }
30600
- /** Cancel — discard marks drawn during this session, then exit. */
30601
- cancel() {
30602
- this.cancelTextInput();
30603
- const removed = this.markNodes.splice(this.sessionBase);
30604
- for (const node2 of removed) node2.remove();
30605
- this.annotations.splice(this.sessionBase);
30606
- this.exit();
30607
- }
30608
30773
  undo() {
30774
+ if (this.markNodes.length <= this.sessionBase) return;
30609
30775
  const node2 = this.markNodes.pop();
30610
30776
  if (!node2) return;
30611
30777
  node2.remove();
@@ -30821,39 +30987,45 @@ var AnnotationEditor = class {
30821
30987
  on: { click: () => this.clear() }
30822
30988
  });
30823
30989
  const editGroup = el("div", { class: "lens-tool-group" }, [this.undoBtn, this.clearBtn]);
30824
- const doneGroup = el("div", { class: "lens-tool-group" }, [
30825
- el("button", {
30826
- class: "lens-pick-chip__btn",
30827
- text: "Cancel",
30828
- attrs: { type: "button" },
30829
- on: { click: () => this.cancel() }
30830
- }),
30831
- el("button", {
30832
- class: "lens-pick-chip__btn lens-pick-chip__btn--primary",
30833
- text: "Done",
30834
- attrs: { type: "button" },
30835
- on: { click: () => this.exit() }
30836
- })
30990
+ const closeBtn = el("button", {
30991
+ class: "lens-tool-close",
30992
+ ariaLabel: "Done",
30993
+ title: "Done",
30994
+ html: iconClose,
30995
+ attrs: { type: "button" },
30996
+ on: { click: () => this.exit() }
30997
+ });
30998
+ const grip = el("span", {
30999
+ class: "lens-tool-grip",
31000
+ html: iconGrip,
31001
+ title: "Drag to move toolbar",
31002
+ ariaLabel: "Drag to move toolbar"
31003
+ });
31004
+ const track = el("div", { class: "lens-tool-track" }, [
31005
+ toolGroup,
31006
+ el("span", { class: "lens-tool-sep" }),
31007
+ colorGroup,
31008
+ el("span", { class: "lens-tool-sep" }),
31009
+ widthGroup,
31010
+ el("span", { class: "lens-tool-sep" }),
31011
+ editGroup
30837
31012
  ]);
30838
31013
  this.toolbar = el(
30839
31014
  "div",
30840
31015
  { class: "lens-annotate-toolbar", attrs: { role: "toolbar", "aria-label": "Annotation tools" } },
30841
- [
30842
- toolGroup,
30843
- el("span", { class: "lens-tool-sep" }),
30844
- colorGroup,
30845
- el("span", { class: "lens-tool-sep" }),
30846
- widthGroup,
30847
- el("span", { class: "lens-tool-sep" }),
30848
- editGroup,
30849
- el("span", { class: "lens-tool-sep" }),
30850
- doneGroup
30851
- ]
31016
+ [grip, track, closeBtn]
30852
31017
  );
30853
31018
  this.host.append(this.toolbar);
31019
+ this.toolbarDrag = makeDraggable(this.toolbar, {
31020
+ handle: grip,
31021
+ key: "lens-annotate-toolbar",
31022
+ ignore: isInteractiveTarget
31023
+ });
30854
31024
  this.syncActionButtons();
30855
31025
  }
30856
31026
  removeToolbar() {
31027
+ this.toolbarDrag?.destroy();
31028
+ this.toolbarDrag = null;
30857
31029
  this.toolbar?.remove();
30858
31030
  this.toolbar = null;
30859
31031
  this.toolBtns.clear();
@@ -30890,7 +31062,7 @@ var AnnotationEditor = class {
30890
31062
  }
30891
31063
  syncActionButtons() {
30892
31064
  const empty = this.markNodes.length === 0;
30893
- if (this.undoBtn) this.undoBtn.disabled = empty;
31065
+ if (this.undoBtn) this.undoBtn.disabled = this.markNodes.length <= this.sessionBase;
30894
31066
  if (this.clearBtn) this.clearBtn.disabled = empty;
30895
31067
  }
30896
31068
  // --------------------------------------------------------------------------
@@ -30904,6 +31076,7 @@ var AnnotationEditor = class {
30904
31076
  }
30905
31077
  onPointerDown = (e) => {
30906
31078
  if (!this.active) return;
31079
+ if (this.drag) return;
30907
31080
  if (e.button !== void 0 && e.button !== 0) return;
30908
31081
  e.preventDefault();
30909
31082
  const p = this.toDoc(e);
@@ -30953,6 +31126,7 @@ var AnnotationEditor = class {
30953
31126
  this.drag = drag;
30954
31127
  window.addEventListener("pointermove", this.onPointerMove, true);
30955
31128
  window.addEventListener("pointerup", this.onPointerUp, true);
31129
+ window.addEventListener("pointercancel", this.onPointerCancel, true);
30956
31130
  };
30957
31131
  onPointerMove = (e) => {
30958
31132
  const d = this.drag;
@@ -31062,9 +31236,18 @@ var AnnotationEditor = class {
31062
31236
  d.node.removeAttribute("data-lens-draft");
31063
31237
  this.commitMark(annotation, d.node);
31064
31238
  };
31239
+ /** Pointer capture was cancelled (OS gesture, page hidden, etc.): drop the
31240
+ * in-progress draft node and detach listeners so nothing leaks. */
31241
+ onPointerCancel = () => {
31242
+ const d = this.drag;
31243
+ this.detachDragListeners();
31244
+ this.drag = null;
31245
+ d?.node.remove();
31246
+ };
31065
31247
  detachDragListeners() {
31066
31248
  window.removeEventListener("pointermove", this.onPointerMove, true);
31067
31249
  window.removeEventListener("pointerup", this.onPointerUp, true);
31250
+ window.removeEventListener("pointercancel", this.onPointerCancel, true);
31068
31251
  }
31069
31252
  commitMark(annotation, node2) {
31070
31253
  this.annotations.push(annotation);
@@ -31258,29 +31441,31 @@ var AnnotationEditor = class {
31258
31441
  return `lens-${++this.idSeq}`;
31259
31442
  }
31260
31443
  };
31261
- var INCLUDE_ITEMS = [
31262
- { key: "replay", label: "Replay" },
31263
- { key: "console", label: "Console" },
31264
- { key: "network", label: "Network" },
31265
- { key: "errors", label: "Errors" },
31266
- { key: "screenshot", label: "Screenshot" }
31267
- ];
31268
31444
  var Panel = class {
31269
31445
  constructor(cb) {
31270
31446
  this.cb = cb;
31271
31447
  this.body = el("div", { class: "lens-panel__body" });
31272
- this.root = el("div", { class: "lens-panel", attrs: { role: "dialog", "aria-label": "Report an issue" } }, [
31273
- el("div", { class: "lens-panel__head" }, [
31274
- el("div", { class: "lens-panel__title", text: "Report an issue" }),
31275
- el("button", {
31276
- class: "lens-panel__x",
31277
- ariaLabel: "Close",
31278
- html: iconClose,
31279
- on: { click: () => this.cb.onClose() }
31280
- })
31281
- ]),
31282
- this.body
31448
+ const head = el("div", { class: "lens-panel__head" }, [
31449
+ el("span", {
31450
+ class: "lens-panel__grip",
31451
+ html: iconGrip,
31452
+ title: "Drag to move",
31453
+ ariaLabel: "Drag to move panel"
31454
+ }),
31455
+ el("div", { class: "lens-panel__title", text: "Report an issue" }),
31456
+ el("button", {
31457
+ class: "lens-panel__x",
31458
+ ariaLabel: "Close",
31459
+ html: iconClose,
31460
+ on: { click: () => this.cb.onClose() }
31461
+ })
31283
31462
  ]);
31463
+ this.root = el(
31464
+ "div",
31465
+ { class: "lens-panel", attrs: { role: "dialog", "aria-label": "Report an issue" } },
31466
+ [head, this.body]
31467
+ );
31468
+ this.drag = makeDraggable(this.root, { handle: head, key: "lens-panel" });
31284
31469
  this.renderForm();
31285
31470
  }
31286
31471
  cb;
@@ -31288,6 +31473,7 @@ var Panel = class {
31288
31473
  body;
31289
31474
  textarea;
31290
31475
  submitBtn;
31476
+ drag = null;
31291
31477
  autoCloseTimer = null;
31292
31478
  submitting = false;
31293
31479
  /** Selected capture toggles; every flag starts ON. Persists across refresh. */
@@ -31307,6 +31493,8 @@ var Panel = class {
31307
31493
  }
31308
31494
  destroy() {
31309
31495
  if (this.autoCloseTimer) clearTimeout(this.autoCloseTimer);
31496
+ this.drag?.destroy();
31497
+ this.drag = null;
31310
31498
  this.root.remove();
31311
31499
  }
31312
31500
  renderForm() {
@@ -31321,7 +31509,6 @@ var Panel = class {
31321
31509
  this.body.append(
31322
31510
  this.textarea,
31323
31511
  this.buildContext(),
31324
- this.buildIncludes(),
31325
31512
  this.buildChips(),
31326
31513
  this.buildActions()
31327
31514
  );
@@ -31341,39 +31528,6 @@ var Panel = class {
31341
31528
  });
31342
31529
  return el("div", { class: "lens-context", title: s.url }, nodes);
31343
31530
  }
31344
- /** Real toggles ("switch"): which captured data to attach. All ON by default. */
31345
- buildIncludes() {
31346
- const s = this.cb.getStats();
31347
- const counts = {
31348
- console: s.console,
31349
- network: s.network,
31350
- errors: s.errors
31351
- };
31352
- const toggles = INCLUDE_ITEMS.map((item) => this.buildToggle(item.key, item.label, counts[item.key]));
31353
- return el("div", { class: "lens-includes" }, [
31354
- el("span", { class: "lens-includes__label", text: "Include:" }),
31355
- ...toggles
31356
- ]);
31357
- }
31358
- buildToggle(key, label, count) {
31359
- const on2 = this.include[key];
31360
- const btn = el("button", {
31361
- class: `lens-tgl${on2 ? " is-on" : ""}`,
31362
- attrs: { type: "button", role: "switch", "aria-checked": String(on2) },
31363
- html: `<span class="lens-tgl__check" aria-hidden="true">${iconCheck}</span>`
31364
- });
31365
- btn.append(el("span", { class: "lens-tgl__label", text: label }));
31366
- if (typeof count === "number" && count > 0) {
31367
- btn.append(el("span", { class: "lens-tgl__count", text: String(count) }));
31368
- }
31369
- btn.addEventListener("click", () => {
31370
- const next = !this.include[key];
31371
- this.include[key] = next;
31372
- btn.classList.toggle("is-on", next);
31373
- btn.setAttribute("aria-checked", String(next));
31374
- });
31375
- return btn;
31376
- }
31377
31531
  /** Removable context chips: annotations, retained recording, picked element. */
31378
31532
  buildChips() {
31379
31533
  const row = el("div", { class: "lens-chips" });
@@ -31678,14 +31832,28 @@ var OVERLAY_CSS = (
31678
31832
  `
31679
31833
  :host {
31680
31834
  all: initial;
31681
- --lens-accent: #6366f1;
31682
- --lens-accent-strong: #4f46e5;
31835
+ /* Black & white, mirroring the Horus Cloud dashboard: the accent is near-black
31836
+ in light and near-white in dark; the always-dark pill/toolbar use explicit
31837
+ light highlights (defined at their rules). Annotation swatch colors and
31838
+ status red/green are content, not brand, and keep their hues. */
31839
+ --lens-accent: #18181b;
31840
+ --lens-accent-strong: #000000;
31841
+ --lens-on-accent: #ffffff;
31842
+ --lens-focus: rgba(0, 0, 0, 0.16);
31683
31843
  --lens-radius: 8px;
31684
31844
  --lens-font: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
31685
31845
  --lens-z: 2147483000;
31686
31846
  font-family: var(--lens-font);
31687
31847
  line-height: 1.4;
31688
31848
  }
31849
+ @media (prefers-color-scheme: dark) {
31850
+ :host {
31851
+ --lens-accent: #fafafa;
31852
+ --lens-accent-strong: #ffffff;
31853
+ --lens-on-accent: #18181b;
31854
+ --lens-focus: rgba(255, 255, 255, 0.22);
31855
+ }
31856
+ }
31689
31857
 
31690
31858
  *, *::before, *::after { box-sizing: border-box; }
31691
31859
 
@@ -31705,7 +31873,7 @@ var OVERLAY_CSS = (
31705
31873
  left: 50%;
31706
31874
  transform: translateX(-50%);
31707
31875
  max-width: calc(100vw - 16px);
31708
- flex-wrap: wrap;
31876
+ flex-wrap: nowrap;
31709
31877
  justify-content: center;
31710
31878
  display: inline-flex;
31711
31879
  align-items: center;
@@ -31729,7 +31897,7 @@ var OVERLAY_CSS = (
31729
31897
  height: 26px;
31730
31898
  margin-left: 4px;
31731
31899
  border-radius: 999px;
31732
- color: #a5b4fc;
31900
+ color: #f4f4f5;
31733
31901
  }
31734
31902
  .lens-btn {
31735
31903
  display: inline-flex;
@@ -31748,11 +31916,14 @@ var OVERLAY_CSS = (
31748
31916
  transition: background 150ms ease, color 150ms ease;
31749
31917
  }
31750
31918
  .lens-btn:hover { background: rgba(255, 255, 255, 0.1); }
31751
- .lens-btn:focus-visible { outline: 2px solid var(--lens-accent); outline-offset: 1px; }
31752
- .lens-btn--primary { background: var(--lens-accent); color: #fff; }
31753
- .lens-btn--primary:hover { background: var(--lens-accent-strong); }
31919
+ .lens-btn:focus-visible { outline: 2px solid rgba(255, 255, 255, 0.7); outline-offset: 1px; }
31920
+ .lens-btn--primary { background: #fff; color: #18181b; }
31921
+ .lens-btn--primary:hover { background: #e4e4e7; }
31922
+ .lens-btn--report svg { flex: none; }
31923
+ .lens-btn__label { white-space: nowrap; }
31754
31924
  .lens-btn--icon { width: 30px; padding: 0; justify-content: center; }
31755
- .lens-btn--icon.is-active { background: var(--lens-accent); color: #fff; }
31925
+ /* Always-dark pill: an active control reads as a light (white) highlight. */
31926
+ .lens-btn--icon.is-active { background: #fff; color: #18181b; }
31756
31927
  .lens-pill__sep {
31757
31928
  width: 1px;
31758
31929
  height: 18px;
@@ -31766,7 +31937,10 @@ var OVERLAY_CSS = (
31766
31937
  z-index: calc(var(--lens-z) - 2);
31767
31938
  pointer-events: none;
31768
31939
  border: 2px solid var(--lens-accent);
31769
- background: rgba(99, 102, 241, 0.14);
31940
+ /* A light outer ring so the (black-in-light / white-in-dark) border stays
31941
+ visible over any page background. */
31942
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.6), 0 0 0 3px rgba(0, 0, 0, 0.15);
31943
+ background: rgba(127, 127, 127, 0.14);
31770
31944
  border-radius: 3px;
31771
31945
  transition: all 60ms linear;
31772
31946
  display: none;
@@ -31778,7 +31952,7 @@ var OVERLAY_CSS = (
31778
31952
  padding: 1px 6px;
31779
31953
  border-radius: 4px;
31780
31954
  background: var(--lens-accent);
31781
- color: #fff;
31955
+ color: var(--lens-on-accent);
31782
31956
  font: 500 11px/1.5 var(--lens-font);
31783
31957
  white-space: nowrap;
31784
31958
  }
@@ -31857,7 +32031,7 @@ var OVERLAY_CSS = (
31857
32031
  padding: 14px 16px;
31858
32032
  border-bottom: 1px solid rgba(0, 0, 0, 0.07);
31859
32033
  }
31860
- .lens-panel__title { font-size: 14px; font-weight: 600; }
32034
+ .lens-panel__title { font-size: 14px; font-weight: 600; margin-right: auto; }
31861
32035
  .lens-panel__x {
31862
32036
  display: inline-flex;
31863
32037
  align-items: center;
@@ -31895,7 +32069,7 @@ var OVERLAY_CSS = (
31895
32069
  .lens-textarea:focus {
31896
32070
  outline: none;
31897
32071
  border-color: var(--lens-accent);
31898
- box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.18);
32072
+ box-shadow: 0 0 0 3px var(--lens-focus);
31899
32073
  }
31900
32074
  /* ---- Context row: static, non-interactive page facts (muted, dot-joined) ---- */
31901
32075
  .lens-context {
@@ -31910,49 +32084,6 @@ var OVERLAY_CSS = (
31910
32084
  .lens-context__item { white-space: nowrap; }
31911
32085
  .lens-context__dot { opacity: 0.55; }
31912
32086
 
31913
- /* ---- Include row: real toggles (role=switch) for which data to attach ---- */
31914
- .lens-includes {
31915
- display: flex;
31916
- flex-wrap: wrap;
31917
- align-items: center;
31918
- gap: 6px;
31919
- }
31920
- .lens-includes__label {
31921
- font-size: 11.5px;
31922
- font-weight: 600;
31923
- color: #71717a;
31924
- margin-right: 2px;
31925
- }
31926
- .lens-tgl {
31927
- display: inline-flex;
31928
- align-items: center;
31929
- gap: 5px;
31930
- padding: 4px 10px;
31931
- border-radius: 999px;
31932
- border: 1px solid rgba(0, 0, 0, 0.14);
31933
- background: #fafafa;
31934
- color: #a1a1aa;
31935
- font-family: var(--lens-font);
31936
- font-size: 11.5px;
31937
- font-weight: 600;
31938
- white-space: nowrap;
31939
- cursor: pointer;
31940
- opacity: 0.75;
31941
- transition: background 150ms ease, border-color 150ms ease, color 150ms ease, opacity 150ms ease;
31942
- }
31943
- .lens-tgl:hover { opacity: 1; }
31944
- .lens-tgl:focus-visible { outline: 2px solid var(--lens-accent); outline-offset: 1px; }
31945
- .lens-tgl__check { display: none; align-items: center; }
31946
- .lens-tgl__check svg { width: 13px; height: 13px; }
31947
- .lens-tgl__count { font-weight: 500; opacity: 0.75; font-variant-numeric: tabular-nums; }
31948
- .lens-tgl.is-on {
31949
- opacity: 1;
31950
- color: var(--lens-accent-strong);
31951
- border-color: rgba(99, 102, 241, 0.5);
31952
- background: #eef2ff;
31953
- }
31954
- .lens-tgl.is-on .lens-tgl__check { display: inline-flex; }
31955
-
31956
32087
  .lens-chips { display: flex; flex-wrap: wrap; gap: 6px; }
31957
32088
  .lens-chip {
31958
32089
  display: inline-flex;
@@ -31973,9 +32104,9 @@ var OVERLAY_CSS = (
31973
32104
  .lens-chip--ok { color: #15803d; background: #f0fdf4; border-color: rgba(21, 128, 61, 0.18); }
31974
32105
  .lens-chip--target {
31975
32106
  max-width: 100%;
31976
- background: #eef2ff;
31977
- border-color: rgba(99, 102, 241, 0.25);
31978
- color: var(--lens-accent-strong);
32107
+ background: rgba(0, 0, 0, 0.05);
32108
+ border-color: rgba(0, 0, 0, 0.16);
32109
+ color: #18181b;
31979
32110
  }
31980
32111
  .lens-chip__code {
31981
32112
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
@@ -32023,7 +32154,7 @@ var OVERLAY_CSS = (
32023
32154
  .lens-abtn svg { flex: none; }
32024
32155
  .lens-abtn--ghost { background: transparent; border-color: rgba(0, 0, 0, 0.14); color: #3f3f46; }
32025
32156
  .lens-abtn--ghost:hover { background: rgba(0, 0, 0, 0.04); }
32026
- .lens-abtn--primary { background: var(--lens-accent); color: #fff; }
32157
+ .lens-abtn--primary { background: var(--lens-accent); color: var(--lens-on-accent); }
32027
32158
  .lens-abtn--primary:hover { background: var(--lens-accent-strong); }
32028
32159
  .lens-abtn:disabled { opacity: 0.55; cursor: default; }
32029
32160
  .lens-abtn:focus-visible { outline: 2px solid var(--lens-accent); outline-offset: 1px; }
@@ -32077,12 +32208,9 @@ var OVERLAY_CSS = (
32077
32208
  .lens-panel__x:hover { background: rgba(255, 255, 255, 0.08); }
32078
32209
  .lens-textarea { background: #26262a; border-color: rgba(255, 255, 255, 0.14); color: #f4f4f5; }
32079
32210
  .lens-context { color: #a1a1aa; }
32080
- .lens-includes__label { color: #a1a1aa; }
32081
- .lens-tgl { background: #26262a; border-color: rgba(255, 255, 255, 0.12); color: #a1a1aa; }
32082
- .lens-tgl.is-on { background: rgba(99, 102, 241, 0.18); border-color: rgba(99, 102, 241, 0.55); color: #c7d2fe; }
32083
32211
  .lens-chip { background: #26262a; border-color: rgba(255, 255, 255, 0.08); color: #d4d4d8; }
32084
32212
  .lens-chip--ok { color: #4ade80; background: rgba(34, 197, 94, 0.12); border-color: rgba(74, 222, 128, 0.22); }
32085
- .lens-chip--target { background: rgba(99, 102, 241, 0.16); color: #c7d2fe; }
32213
+ .lens-chip--target { background: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.18); color: #f4f4f5; }
32086
32214
  .lens-abtn--ghost { border-color: rgba(255, 255, 255, 0.16); color: #d4d4d8; }
32087
32215
  .lens-abtn--ghost:hover { background: rgba(255, 255, 255, 0.06); }
32088
32216
  .lens-state__msg { color: #a1a1aa; }
@@ -32116,7 +32244,9 @@ var OVERLAY_CSS = (
32116
32244
  .lens-annotate-chips { z-index: var(--lens-z); }
32117
32245
 
32118
32246
  /* ---------------- Annotation toolbar ---------------- */
32119
- /* Same floating top-center anchor as the chips row; wraps on narrow screens. */
32247
+ /* Floating top-center anchor. A fixed drag grip sits left of a tools "track":
32248
+ the track wraps on desktop and becomes a single horizontal-scroll row on
32249
+ narrow screens, so the bar stays one slim height instead of a bulky block. */
32120
32250
  .lens-annotate-toolbar {
32121
32251
  position: fixed;
32122
32252
  top: calc(16px + env(safe-area-inset-top));
@@ -32124,11 +32254,10 @@ var OVERLAY_CSS = (
32124
32254
  transform: translateX(-50%);
32125
32255
  z-index: var(--lens-z);
32126
32256
  display: inline-flex;
32127
- flex-wrap: wrap;
32257
+ flex-wrap: nowrap;
32128
32258
  align-items: center;
32129
- justify-content: center;
32130
32259
  gap: 6px;
32131
- max-width: calc(100vw - 24px);
32260
+ max-width: calc(100vw - 16px);
32132
32261
  padding: 6px 8px;
32133
32262
  border-radius: 14px;
32134
32263
  background: rgba(20, 20, 23, 0.92);
@@ -32138,12 +32267,50 @@ var OVERLAY_CSS = (
32138
32267
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.35);
32139
32268
  color: #f4f4f5;
32140
32269
  }
32270
+ /* Tools lay out as ONE horizontal row that scrolls if it can't fit, so the bar
32271
+ is always a single slim height \u2014 never a wrapping (or, worse, min-content
32272
+ collapsed vertical) block. The grip sits outside this track. */
32273
+ .lens-tool-track {
32274
+ display: flex;
32275
+ align-items: center;
32276
+ flex-wrap: nowrap;
32277
+ justify-content: flex-start;
32278
+ gap: 6px;
32279
+ flex: 1 1 auto;
32280
+ min-width: 0;
32281
+ overflow-x: auto;
32282
+ overflow-y: hidden;
32283
+ scrollbar-width: none;
32284
+ -webkit-overflow-scrolling: touch;
32285
+ }
32286
+ .lens-tool-track::-webkit-scrollbar { display: none; }
32141
32287
  .lens-tool-group {
32142
32288
  display: inline-flex;
32143
32289
  align-items: center;
32144
32290
  gap: 4px;
32145
- flex-wrap: wrap;
32291
+ flex-wrap: nowrap;
32292
+ flex: none;
32293
+ }
32294
+ /* Single close (=Done, keeps marks) pinned at the right, always reachable
32295
+ while the tools scroll. Reads as a light control on the dark bar. */
32296
+ .lens-tool-close {
32297
+ display: inline-flex;
32298
+ align-items: center;
32299
+ justify-content: center;
32300
+ flex: none;
32301
+ width: 32px;
32302
+ height: 30px;
32303
+ padding: 0;
32304
+ border: 0;
32305
+ border-radius: 8px;
32306
+ background: rgba(255, 255, 255, 0.14);
32307
+ color: #f4f4f5;
32308
+ cursor: pointer;
32309
+ transition: background 150ms ease;
32146
32310
  }
32311
+ .lens-tool-close:hover { background: rgba(255, 255, 255, 0.26); }
32312
+ .lens-tool-close:focus-visible { outline: 2px solid rgba(255, 255, 255, 0.7); outline-offset: 1px; }
32313
+ .lens-tool-close svg { width: 18px; height: 18px; }
32147
32314
  .lens-tool-sep {
32148
32315
  width: 1px;
32149
32316
  align-self: stretch;
@@ -32151,6 +32318,51 @@ var OVERLAY_CSS = (
32151
32318
  background: rgba(255, 255, 255, 0.16);
32152
32319
  margin: 0 2px;
32153
32320
  }
32321
+
32322
+ /* ---------------- Draggable surfaces ---------------- */
32323
+ /* Every drag handle: no page pan/scroll gesture starts here, and the cursor
32324
+ signals grabbability. Applied to the pill, panel header, toolbar grip, chips. */
32325
+ .lens-drag-handle { touch-action: none; cursor: grab; }
32326
+ .lens-drag-handle:active { cursor: grabbing; }
32327
+ .is-dragging, .is-dragging * { cursor: grabbing !important; }
32328
+ .is-dragging { user-select: none; }
32329
+ /* While dragging, drop the entry animation/transition so it tracks 1:1. */
32330
+ .lens-pill.is-dragging, .lens-panel.is-dragging,
32331
+ .lens-annotate-toolbar.is-dragging, .lens-rec-chip.is-dragging {
32332
+ animation: none;
32333
+ transition: none;
32334
+ }
32335
+
32336
+ /* Toolbar drag grip \u2014 the only grab region, kept out of the scroll track. */
32337
+ .lens-tool-grip {
32338
+ display: inline-flex;
32339
+ align-items: center;
32340
+ justify-content: center;
32341
+ flex: none;
32342
+ width: 22px;
32343
+ height: 30px;
32344
+ margin-right: 2px;
32345
+ border-radius: 6px;
32346
+ color: #a1a1aa;
32347
+ }
32348
+ .lens-tool-grip:hover { color: #f4f4f5; background: rgba(255, 255, 255, 0.08); }
32349
+ .lens-tool-grip svg { width: 16px; height: 16px; }
32350
+
32351
+ /* Pill grip (the eye dot) + panel header grip. */
32352
+ .lens-pill__grip { cursor: grab; }
32353
+ .lens-panel__grip {
32354
+ display: inline-flex;
32355
+ align-items: center;
32356
+ justify-content: center;
32357
+ flex: none;
32358
+ width: 22px;
32359
+ height: 22px;
32360
+ margin-right: 6px;
32361
+ color: #a1a1aa;
32362
+ }
32363
+ .lens-panel__grip svg { width: 16px; height: 16px; }
32364
+ .lens-panel__head { cursor: grab; }
32365
+ .lens-panel__head:active { cursor: grabbing; }
32154
32366
  .lens-tool-btn {
32155
32367
  display: inline-flex;
32156
32368
  align-items: center;
@@ -32166,11 +32378,11 @@ var OVERLAY_CSS = (
32166
32378
  transition: background 150ms ease, color 150ms ease, border-color 150ms ease;
32167
32379
  }
32168
32380
  .lens-tool-btn:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
32169
- .lens-tool-btn:focus-visible { outline: 2px solid var(--lens-accent); outline-offset: 1px; }
32381
+ .lens-tool-btn:focus-visible { outline: 2px solid rgba(255, 255, 255, 0.7); outline-offset: 1px; }
32170
32382
  .lens-tool-btn.is-active {
32171
- background: var(--lens-accent);
32172
- border-color: var(--lens-accent);
32173
- color: #fff;
32383
+ background: #fff;
32384
+ border-color: #fff;
32385
+ color: #18181b;
32174
32386
  }
32175
32387
  .lens-tool-btn:disabled { opacity: 0.4; cursor: default; }
32176
32388
  .lens-tool-btn:disabled:hover { background: transparent; color: #d4d4d8; }
@@ -32210,17 +32422,18 @@ var OVERLAY_CSS = (
32210
32422
  transition: background 150ms ease, color 150ms ease, border-color 150ms ease;
32211
32423
  }
32212
32424
  .lens-width-btn:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
32213
- .lens-width-btn:focus-visible { outline: 2px solid var(--lens-accent); outline-offset: 1px; }
32425
+ .lens-width-btn:focus-visible { outline: 2px solid rgba(255, 255, 255, 0.7); outline-offset: 1px; }
32214
32426
  .lens-width-btn.is-active {
32215
- background: var(--lens-accent);
32216
- border-color: var(--lens-accent);
32217
- color: #fff;
32427
+ background: #fff;
32428
+ border-color: #fff;
32429
+ color: #18181b;
32218
32430
  }
32219
32431
 
32220
32432
  /* Coarse pointers: bump the tool targets toward 40px. */
32221
32433
  @media (pointer: coarse) {
32222
32434
  .lens-tool-btn, .lens-width-btn { width: 38px; height: 38px; }
32223
32435
  .lens-swatch { width: 30px; height: 30px; }
32436
+ .lens-tool-grip { width: 30px; height: 38px; }
32224
32437
  }
32225
32438
 
32226
32439
  /* Stacking inside the shadow layer: the panel, pill and floating chips always
@@ -32245,8 +32458,8 @@ var OVERLAY_CSS = (
32245
32458
  transition: background 150ms ease;
32246
32459
  }
32247
32460
  .lens-pick-chip__btn:hover { background: rgba(255, 255, 255, 0.2); }
32248
- .lens-pick-chip__btn--primary { background: var(--lens-accent); }
32249
- .lens-pick-chip__btn--primary:hover { background: var(--lens-accent-strong); }
32461
+ .lens-pick-chip__btn--primary { background: #fff; color: #18181b; }
32462
+ .lens-pick-chip__btn--primary:hover { background: #e4e4e7; }
32250
32463
 
32251
32464
  /* ---------------- Recording chip ---------------- */
32252
32465
  .lens-rec-chip {
@@ -32301,21 +32514,29 @@ var OVERLAY_CSS = (
32301
32514
  .lens-rec-chip__stop:hover { background: #dc2626; }
32302
32515
  .lens-chip--rec { gap: 6px; }
32303
32516
 
32517
+ /* Very narrow screens: collapse the pill's "Report issue" label to its icon so
32518
+ the pill stays a single slim row (never wraps into a 2-row blob). */
32519
+ @media (max-width: 400px) {
32520
+ .lens-btn__label { display: none; }
32521
+ .lens-btn--report { width: 44px; padding: 0; justify-content: center; }
32522
+ }
32523
+
32304
32524
  /* ---------------- Coarse pointers (touch): \u226544px targets ---------------- */
32305
32525
  @media (pointer: coarse) {
32306
32526
  .lens-btn { height: 44px; padding: 0 16px; }
32307
32527
  .lens-btn--icon { width: 44px; }
32528
+ .lens-tool-close { width: 44px; height: 38px; }
32308
32529
  .lens-pill__dot { width: 40px; height: 40px; }
32309
32530
  .lens-abtn { height: 44px; }
32310
- .lens-tgl { min-height: 40px; padding: 4px 14px; }
32311
32531
  .lens-panel__x { width: 44px; height: 44px; }
32312
32532
  .lens-chip__x { min-width: 32px; min-height: 32px; align-items: center; justify-content: center; }
32313
32533
  .lens-pick-chip__btn, .lens-rec-chip__stop { height: 40px; padding: 0 18px; }
32314
32534
  }
32315
32535
 
32316
- /* ---------------- Bottom sheet under 640px ---------------- */
32536
+ /* ---------------- Narrow screens (\u2264640px) ---------------- */
32317
32537
  @media (max-width: 640px) {
32318
- .lens-panel {
32538
+ /* Report panel as a bottom sheet (unless the user has dragged it loose). */
32539
+ .lens-panel:not(.is-dragging) {
32319
32540
  left: 0;
32320
32541
  right: 0;
32321
32542
  bottom: 0;
@@ -32332,6 +32553,14 @@ var OVERLAY_CSS = (
32332
32553
  to { opacity: 1; transform: none; }
32333
32554
  }
32334
32555
  .lens-textarea { font-size: 16px; }
32556
+
32557
+ /* On narrow screens the single-row tool track usually overflows: fade its
32558
+ trailing edge so it reads as "more tools this way" (the grip sits outside
32559
+ the track and stays fully opaque). */
32560
+ .lens-tool-track {
32561
+ -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 20px), transparent 100%);
32562
+ mask-image: linear-gradient(90deg, #000 calc(100% - 20px), transparent 100%);
32563
+ }
32335
32564
  }
32336
32565
  `
32337
32566
  );
@@ -32349,8 +32578,14 @@ var Overlay = class {
32349
32578
  this.shadow.append(this.layer);
32350
32579
  this.picker = new ElementPicker(this.layer, this.host);
32351
32580
  this.annotator = new AnnotationEditor(this.layer);
32352
- this.layer.append(this.buildPill());
32581
+ const pill = this.buildPill();
32582
+ this.layer.append(pill);
32353
32583
  document.documentElement.append(this.host);
32584
+ this.pillDrag = makeDraggable(pill, {
32585
+ handle: pill,
32586
+ key: "lens-pill",
32587
+ ignore: isInteractiveTarget
32588
+ });
32354
32589
  window.addEventListener("keydown", this.onGlobalKey, true);
32355
32590
  }
32356
32591
  deps;
@@ -32363,7 +32598,9 @@ var Overlay = class {
32363
32598
  panel = null;
32364
32599
  target = null;
32365
32600
  destroyed = false;
32601
+ pillDrag = null;
32366
32602
  recordingChip = null;
32603
+ recordingDrag = null;
32367
32604
  recordingTimerEl = null;
32368
32605
  recordingInterval = null;
32369
32606
  recordingStartTs = 0;
@@ -32378,10 +32615,16 @@ var Overlay = class {
32378
32615
  on: { click: () => this.togglePicker() }
32379
32616
  });
32380
32617
  return el("div", { class: "lens-pill", attrs: { role: "toolbar", "aria-label": "Lens" } }, [
32381
- el("span", { class: "lens-pill__dot", html: iconEye }),
32618
+ el("span", {
32619
+ class: "lens-pill__dot lens-pill__grip",
32620
+ html: iconEye,
32621
+ title: "Drag to move",
32622
+ ariaLabel: "Drag to move Lens"
32623
+ }),
32382
32624
  el("button", {
32383
- class: "lens-btn",
32384
- text: "Report issue",
32625
+ class: "lens-btn lens-btn--report",
32626
+ html: iconReport + '<span class="lens-btn__label">Report issue</span>',
32627
+ ariaLabel: "Report issue",
32385
32628
  on: { click: () => this.open() }
32386
32629
  }),
32387
32630
  this.pickBtn,
@@ -32435,6 +32678,7 @@ var Overlay = class {
32435
32678
  this.stopPicker();
32436
32679
  return;
32437
32680
  }
32681
+ if (this.annotator.isActive) this.annotator.exit();
32438
32682
  this.close();
32439
32683
  this.pickBtn.classList.add("is-active");
32440
32684
  this.picker.start(
@@ -32496,6 +32740,11 @@ var Overlay = class {
32496
32740
  })
32497
32741
  ]);
32498
32742
  this.layer.append(this.recordingChip);
32743
+ this.recordingDrag = makeDraggable(this.recordingChip, {
32744
+ handle: this.recordingChip,
32745
+ key: "lens-rec-chip",
32746
+ ignore: isInteractiveTarget
32747
+ });
32499
32748
  this.recordingInterval = setInterval(() => this.tickRecording(), 250);
32500
32749
  }
32501
32750
  tickRecording() {
@@ -32508,6 +32757,10 @@ var Overlay = class {
32508
32757
  clearInterval(this.recordingInterval);
32509
32758
  this.recordingInterval = null;
32510
32759
  }
32760
+ if (this.recordingDrag) {
32761
+ this.recordingDrag.destroy();
32762
+ this.recordingDrag = null;
32763
+ }
32511
32764
  if (this.recordingChip) {
32512
32765
  this.recordingChip.remove();
32513
32766
  this.recordingChip = null;
@@ -32548,6 +32801,8 @@ var Overlay = class {
32548
32801
  if (this.destroyed) return;
32549
32802
  this.destroyed = true;
32550
32803
  window.removeEventListener("keydown", this.onGlobalKey, true);
32804
+ this.pillDrag?.destroy();
32805
+ this.pillDrag = null;
32551
32806
  this.stopPicker();
32552
32807
  if (this.deps.client.isRecording()) this.deps.client.stopRecording();
32553
32808
  this.hideRecordingChip();