@merittdev/horus-lens 0.4.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/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.0" : "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) {
@@ -29686,8 +29714,14 @@ async function captureScreenshot(excludeHost, crop, composite) {
29686
29714
  return null;
29687
29715
  }
29688
29716
  }
29689
- if (crop) {
29690
- const cropped = await cropBlob(blob2, crop, scale);
29717
+ const effectiveCrop = crop ?? (composite ? void 0 : {
29718
+ x: window.scrollX,
29719
+ y: window.scrollY,
29720
+ width: window.innerWidth,
29721
+ height: window.innerHeight
29722
+ });
29723
+ if (effectiveCrop) {
29724
+ const cropped = await cropBlob(blob2, effectiveCrop, scale);
29691
29725
  return cropped ?? blob2;
29692
29726
  }
29693
29727
  return blob2;
@@ -30121,12 +30155,22 @@ var LensClient = class {
30121
30155
  input2.screenshotCrop,
30122
30156
  input2.annotationComposite
30123
30157
  ) : Promise.resolve(null);
30158
+ const consumingManualRecording = wantReplay && (this.replay?.hasManualRecording() ?? false) && !(this.replay?.isManualRecording() ?? false);
30124
30159
  const replayEvents = wantReplay ? this.replay?.snapshot() ?? [] : [];
30125
30160
  const errorEntries = this.errorBuffer.drain();
30126
- const consoleTail = this.consoleBuffer.tail(CONSOLE_TAIL_SIZE);
30127
- const networkTail = this.networkBuffer.tail(NETWORK_TAIL_SIZE);
30128
30161
  const consoleEntries = this.consoleBuffer.drain();
30129
30162
  const networkEntries = this.networkBuffer.drain();
30163
+ this.errorBuffer.clear();
30164
+ this.consoleBuffer.clear();
30165
+ this.networkBuffer.clear();
30166
+ const consoleTail = consoleEntries.slice(-CONSOLE_TAIL_SIZE);
30167
+ const isNetFailure = (e) => e.ok !== true || Boolean(e.error);
30168
+ const recentFailures = networkEntries.filter(isNetFailure).slice(-NETWORK_TAIL_SIZE);
30169
+ const keep = /* @__PURE__ */ new Set([
30170
+ ...recentFailures,
30171
+ ...networkEntries.slice(-NETWORK_TAIL_SIZE)
30172
+ ]);
30173
+ const networkTail = networkEntries.filter((e) => keep.has(e)).slice(-NETWORK_TAIL_SIZE);
30130
30174
  let metadata = {
30131
30175
  annotations: input2.annotations ?? [],
30132
30176
  env: collectEnv(),
@@ -30180,13 +30224,14 @@ var LensClient = class {
30180
30224
  }
30181
30225
  if (!asset.blob) continue;
30182
30226
  try {
30183
- await this.transport.uploadAsset(upload, asset.blob);
30227
+ await this.uploadAssetWithTimeout(upload, asset.blob);
30184
30228
  uploadedAssets.push({ kind: upload.kind, key: upload.key, byteSize: asset.blob.size });
30185
30229
  } catch (err) {
30186
30230
  this.warn(`asset upload failed (${upload.kind})`, err);
30187
30231
  }
30188
30232
  }
30189
30233
  const finalized = await this.transport.finalize(initiated.reportId, metadata, uploadedAssets);
30234
+ if (consumingManualRecording) this.replay?.discardManualRecording();
30190
30235
  if (screenshotUpload) {
30191
30236
  const upload = screenshotUpload;
30192
30237
  void screenshotPromise.then(async (blob2) => {
@@ -30204,6 +30249,21 @@ var LensClient = class {
30204
30249
  }
30205
30250
  return { reportId: finalized.reportId };
30206
30251
  }
30252
+ /**
30253
+ * Upload a foreground asset with a bounded timeout. Without this a stuck S3
30254
+ * PUT (dropped connection, half-open socket) would hang `submitReport`
30255
+ * forever; the timeout aborts the individual PUT so it fails as one asset
30256
+ * (caught by the caller) instead of wedging the whole submit.
30257
+ */
30258
+ async uploadAssetWithTimeout(upload, blob2) {
30259
+ const controller = new AbortController();
30260
+ const timer = setTimeout(() => controller.abort(), SCREENSHOT_TIMEOUT_MS);
30261
+ try {
30262
+ await this.transport.uploadAsset(upload, blob2, controller.signal);
30263
+ } finally {
30264
+ clearTimeout(timer);
30265
+ }
30266
+ }
30207
30267
  warn(message, err) {
30208
30268
  if (!this.debug) return;
30209
30269
  if (err !== void 0) console.warn(`[lens] ${message}`, err);
@@ -30332,6 +30392,113 @@ function el(tag, props = {}, children = []) {
30332
30392
  function clear(node2) {
30333
30393
  while (node2.firstChild) node2.removeChild(node2.firstChild);
30334
30394
  }
30395
+ var MARGIN = 6;
30396
+ var DRAG_THRESHOLD = 3;
30397
+ var savedPositions = /* @__PURE__ */ new Map();
30398
+ function isInteractiveTarget(target) {
30399
+ return target instanceof Element && target.closest(
30400
+ 'button, a, input, textarea, select, [role="switch"], [role="button"], [contenteditable="true"]'
30401
+ ) !== null;
30402
+ }
30403
+ function clampToViewport(element, left, top) {
30404
+ const rect = element.getBoundingClientRect();
30405
+ const vw = window.innerWidth;
30406
+ const vh = window.innerHeight;
30407
+ const maxLeft = Math.max(MARGIN, vw - rect.width - MARGIN);
30408
+ const maxTop = Math.max(MARGIN, vh - rect.height - MARGIN);
30409
+ return {
30410
+ left: Math.min(Math.max(left, MARGIN), maxLeft),
30411
+ top: Math.min(Math.max(top, MARGIN), maxTop)
30412
+ };
30413
+ }
30414
+ function makeDraggable(element, opts) {
30415
+ const { handle, key } = opts;
30416
+ const ignore = opts.ignore ?? isInteractiveTarget;
30417
+ let startX = 0;
30418
+ let startY = 0;
30419
+ let originLeft = 0;
30420
+ let originTop = 0;
30421
+ let dragging = false;
30422
+ let armed = false;
30423
+ let pointerId = null;
30424
+ const apply = (left, top) => {
30425
+ const p = clampToViewport(element, left, top);
30426
+ element.style.left = `${p.left}px`;
30427
+ element.style.top = `${p.top}px`;
30428
+ element.style.right = "auto";
30429
+ element.style.bottom = "auto";
30430
+ element.style.transform = "none";
30431
+ savedPositions.set(key, p);
30432
+ opts.onMove?.();
30433
+ };
30434
+ const onPointerDown = (e) => {
30435
+ if (e.button !== 0 && e.pointerType === "mouse") return;
30436
+ if (ignore(e.target)) return;
30437
+ armed = true;
30438
+ dragging = false;
30439
+ pointerId = e.pointerId;
30440
+ startX = e.clientX;
30441
+ startY = e.clientY;
30442
+ const rect = element.getBoundingClientRect();
30443
+ originLeft = rect.left;
30444
+ originTop = rect.top;
30445
+ window.addEventListener("pointermove", onPointerMove, true);
30446
+ window.addEventListener("pointerup", onPointerUp, true);
30447
+ window.addEventListener("pointercancel", onPointerUp, true);
30448
+ };
30449
+ const onPointerMove = (e) => {
30450
+ if (!armed || e.pointerId !== pointerId) return;
30451
+ const dx = e.clientX - startX;
30452
+ const dy = e.clientY - startY;
30453
+ if (!dragging) {
30454
+ if (Math.abs(dx) + Math.abs(dy) < DRAG_THRESHOLD) return;
30455
+ dragging = true;
30456
+ element.classList.add("is-dragging");
30457
+ }
30458
+ e.preventDefault();
30459
+ apply(originLeft + dx, originTop + dy);
30460
+ };
30461
+ const onPointerUp = (e) => {
30462
+ if (e.pointerId !== pointerId) return;
30463
+ armed = false;
30464
+ if (dragging) {
30465
+ dragging = false;
30466
+ element.classList.remove("is-dragging");
30467
+ }
30468
+ pointerId = null;
30469
+ window.removeEventListener("pointermove", onPointerMove, true);
30470
+ window.removeEventListener("pointerup", onPointerUp, true);
30471
+ window.removeEventListener("pointercancel", onPointerUp, true);
30472
+ };
30473
+ const onResize = () => {
30474
+ const p = savedPositions.get(key);
30475
+ if (p) apply(p.left, p.top);
30476
+ };
30477
+ handle.classList.add("lens-drag-handle");
30478
+ handle.addEventListener("pointerdown", onPointerDown);
30479
+ window.addEventListener("resize", onResize);
30480
+ const stored = savedPositions.get(key);
30481
+ if (stored) requestAnimationFrame(() => apply(stored.left, stored.top));
30482
+ return {
30483
+ reclamp: onResize,
30484
+ reset() {
30485
+ savedPositions.delete(key);
30486
+ element.style.left = "";
30487
+ element.style.top = "";
30488
+ element.style.right = "";
30489
+ element.style.bottom = "";
30490
+ element.style.transform = "";
30491
+ element.classList.remove("is-dragging");
30492
+ },
30493
+ destroy() {
30494
+ handle.removeEventListener("pointerdown", onPointerDown);
30495
+ window.removeEventListener("pointermove", onPointerMove, true);
30496
+ window.removeEventListener("pointerup", onPointerUp, true);
30497
+ window.removeEventListener("pointercancel", onPointerUp, true);
30498
+ window.removeEventListener("resize", onResize);
30499
+ }
30500
+ };
30501
+ }
30335
30502
  var THIN_MIN_DIST = 3;
30336
30503
  var MAX_POINTS = 2e3;
30337
30504
  var HIGHLIGHTER_WIDTH = 14;
@@ -30482,6 +30649,8 @@ var iconTrash = svg(
30482
30649
  '<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
30650
  );
30484
30651
  var iconAnnotate = iconPencil;
30652
+ 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>`;
30653
+ 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
30654
  var SVG_NS = "http://www.w3.org/2000/svg";
30486
30655
  var FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
30487
30656
  var MIN_DRAG = 6;
@@ -30510,6 +30679,7 @@ var AnnotationEditor = class {
30510
30679
  svg = null;
30511
30680
  bg = null;
30512
30681
  toolbar = null;
30682
+ toolbarDrag = null;
30513
30683
  textInputEl = null;
30514
30684
  textAnchor = null;
30515
30685
  textColor = COLORS[0];
@@ -30555,6 +30725,7 @@ var AnnotationEditor = class {
30555
30725
  this.markNodes = [];
30556
30726
  this.annotations = [];
30557
30727
  this.stepCounter = 0;
30728
+ this.sessionBase = Math.min(this.sessionBase, this.annotations.length);
30558
30729
  if (!this.active) this.teardownLayer();
30559
30730
  this.syncActionButtons();
30560
30731
  }
@@ -30569,6 +30740,7 @@ var AnnotationEditor = class {
30569
30740
  this.sizeLayer();
30570
30741
  this.updateTransform();
30571
30742
  svg2.style.pointerEvents = "auto";
30743
+ if (this.bg) this.bg.style.pointerEvents = "all";
30572
30744
  svg2.style.touchAction = "none";
30573
30745
  svg2.style.cursor = "crosshair";
30574
30746
  svg2.addEventListener("pointerdown", this.onPointerDown);
@@ -30589,6 +30761,7 @@ var AnnotationEditor = class {
30589
30761
  if (svg2) {
30590
30762
  svg2.removeEventListener("pointerdown", this.onPointerDown);
30591
30763
  svg2.style.pointerEvents = "none";
30764
+ if (this.bg) this.bg.style.pointerEvents = "none";
30592
30765
  svg2.style.touchAction = "";
30593
30766
  svg2.style.cursor = "";
30594
30767
  }
@@ -30598,15 +30771,8 @@ var AnnotationEditor = class {
30598
30771
  this.onExit = null;
30599
30772
  cb?.();
30600
30773
  }
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
30774
  undo() {
30775
+ if (this.markNodes.length <= this.sessionBase) return;
30610
30776
  const node2 = this.markNodes.pop();
30611
30777
  if (!node2) return;
30612
30778
  node2.remove();
@@ -30822,39 +30988,45 @@ var AnnotationEditor = class {
30822
30988
  on: { click: () => this.clear() }
30823
30989
  });
30824
30990
  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
- })
30991
+ const closeBtn = el("button", {
30992
+ class: "lens-tool-close",
30993
+ ariaLabel: "Done",
30994
+ title: "Done",
30995
+ html: iconClose,
30996
+ attrs: { type: "button" },
30997
+ on: { click: () => this.exit() }
30998
+ });
30999
+ const grip = el("span", {
31000
+ class: "lens-tool-grip",
31001
+ html: iconGrip,
31002
+ title: "Drag to move toolbar",
31003
+ ariaLabel: "Drag to move toolbar"
31004
+ });
31005
+ const track = el("div", { class: "lens-tool-track" }, [
31006
+ toolGroup,
31007
+ el("span", { class: "lens-tool-sep" }),
31008
+ colorGroup,
31009
+ el("span", { class: "lens-tool-sep" }),
31010
+ widthGroup,
31011
+ el("span", { class: "lens-tool-sep" }),
31012
+ editGroup
30838
31013
  ]);
30839
31014
  this.toolbar = el(
30840
31015
  "div",
30841
31016
  { 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
- ]
31017
+ [grip, track, closeBtn]
30853
31018
  );
30854
31019
  this.host.append(this.toolbar);
31020
+ this.toolbarDrag = makeDraggable(this.toolbar, {
31021
+ handle: grip,
31022
+ key: "lens-annotate-toolbar",
31023
+ ignore: isInteractiveTarget
31024
+ });
30855
31025
  this.syncActionButtons();
30856
31026
  }
30857
31027
  removeToolbar() {
31028
+ this.toolbarDrag?.destroy();
31029
+ this.toolbarDrag = null;
30858
31030
  this.toolbar?.remove();
30859
31031
  this.toolbar = null;
30860
31032
  this.toolBtns.clear();
@@ -30891,7 +31063,7 @@ var AnnotationEditor = class {
30891
31063
  }
30892
31064
  syncActionButtons() {
30893
31065
  const empty = this.markNodes.length === 0;
30894
- if (this.undoBtn) this.undoBtn.disabled = empty;
31066
+ if (this.undoBtn) this.undoBtn.disabled = this.markNodes.length <= this.sessionBase;
30895
31067
  if (this.clearBtn) this.clearBtn.disabled = empty;
30896
31068
  }
30897
31069
  // --------------------------------------------------------------------------
@@ -30905,6 +31077,7 @@ var AnnotationEditor = class {
30905
31077
  }
30906
31078
  onPointerDown = (e) => {
30907
31079
  if (!this.active) return;
31080
+ if (this.drag) return;
30908
31081
  if (e.button !== void 0 && e.button !== 0) return;
30909
31082
  e.preventDefault();
30910
31083
  const p = this.toDoc(e);
@@ -30954,6 +31127,7 @@ var AnnotationEditor = class {
30954
31127
  this.drag = drag;
30955
31128
  window.addEventListener("pointermove", this.onPointerMove, true);
30956
31129
  window.addEventListener("pointerup", this.onPointerUp, true);
31130
+ window.addEventListener("pointercancel", this.onPointerCancel, true);
30957
31131
  };
30958
31132
  onPointerMove = (e) => {
30959
31133
  const d = this.drag;
@@ -31063,9 +31237,18 @@ var AnnotationEditor = class {
31063
31237
  d.node.removeAttribute("data-lens-draft");
31064
31238
  this.commitMark(annotation, d.node);
31065
31239
  };
31240
+ /** Pointer capture was cancelled (OS gesture, page hidden, etc.): drop the
31241
+ * in-progress draft node and detach listeners so nothing leaks. */
31242
+ onPointerCancel = () => {
31243
+ const d = this.drag;
31244
+ this.detachDragListeners();
31245
+ this.drag = null;
31246
+ d?.node.remove();
31247
+ };
31066
31248
  detachDragListeners() {
31067
31249
  window.removeEventListener("pointermove", this.onPointerMove, true);
31068
31250
  window.removeEventListener("pointerup", this.onPointerUp, true);
31251
+ window.removeEventListener("pointercancel", this.onPointerCancel, true);
31069
31252
  }
31070
31253
  commitMark(annotation, node2) {
31071
31254
  this.annotations.push(annotation);
@@ -31259,29 +31442,31 @@ var AnnotationEditor = class {
31259
31442
  return `lens-${++this.idSeq}`;
31260
31443
  }
31261
31444
  };
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
31445
  var Panel = class {
31270
31446
  constructor(cb) {
31271
31447
  this.cb = cb;
31272
31448
  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
31449
+ const head = el("div", { class: "lens-panel__head" }, [
31450
+ el("span", {
31451
+ class: "lens-panel__grip",
31452
+ html: iconGrip,
31453
+ title: "Drag to move",
31454
+ ariaLabel: "Drag to move panel"
31455
+ }),
31456
+ el("div", { class: "lens-panel__title", text: "Report an issue" }),
31457
+ el("button", {
31458
+ class: "lens-panel__x",
31459
+ ariaLabel: "Close",
31460
+ html: iconClose,
31461
+ on: { click: () => this.cb.onClose() }
31462
+ })
31284
31463
  ]);
31464
+ this.root = el(
31465
+ "div",
31466
+ { class: "lens-panel", attrs: { role: "dialog", "aria-label": "Report an issue" } },
31467
+ [head, this.body]
31468
+ );
31469
+ this.drag = makeDraggable(this.root, { handle: head, key: "lens-panel" });
31285
31470
  this.renderForm();
31286
31471
  }
31287
31472
  cb;
@@ -31289,6 +31474,7 @@ var Panel = class {
31289
31474
  body;
31290
31475
  textarea;
31291
31476
  submitBtn;
31477
+ drag = null;
31292
31478
  autoCloseTimer = null;
31293
31479
  submitting = false;
31294
31480
  /** Selected capture toggles; every flag starts ON. Persists across refresh. */
@@ -31308,6 +31494,8 @@ var Panel = class {
31308
31494
  }
31309
31495
  destroy() {
31310
31496
  if (this.autoCloseTimer) clearTimeout(this.autoCloseTimer);
31497
+ this.drag?.destroy();
31498
+ this.drag = null;
31311
31499
  this.root.remove();
31312
31500
  }
31313
31501
  renderForm() {
@@ -31322,7 +31510,6 @@ var Panel = class {
31322
31510
  this.body.append(
31323
31511
  this.textarea,
31324
31512
  this.buildContext(),
31325
- this.buildIncludes(),
31326
31513
  this.buildChips(),
31327
31514
  this.buildActions()
31328
31515
  );
@@ -31342,39 +31529,6 @@ var Panel = class {
31342
31529
  });
31343
31530
  return el("div", { class: "lens-context", title: s.url }, nodes);
31344
31531
  }
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
31532
  /** Removable context chips: annotations, retained recording, picked element. */
31379
31533
  buildChips() {
31380
31534
  const row = el("div", { class: "lens-chips" });
@@ -31679,14 +31833,28 @@ var OVERLAY_CSS = (
31679
31833
  `
31680
31834
  :host {
31681
31835
  all: initial;
31682
- --lens-accent: #6366f1;
31683
- --lens-accent-strong: #4f46e5;
31836
+ /* Black & white, mirroring the Horus Cloud dashboard: the accent is near-black
31837
+ in light and near-white in dark; the always-dark pill/toolbar use explicit
31838
+ light highlights (defined at their rules). Annotation swatch colors and
31839
+ status red/green are content, not brand, and keep their hues. */
31840
+ --lens-accent: #18181b;
31841
+ --lens-accent-strong: #000000;
31842
+ --lens-on-accent: #ffffff;
31843
+ --lens-focus: rgba(0, 0, 0, 0.16);
31684
31844
  --lens-radius: 8px;
31685
31845
  --lens-font: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
31686
31846
  --lens-z: 2147483000;
31687
31847
  font-family: var(--lens-font);
31688
31848
  line-height: 1.4;
31689
31849
  }
31850
+ @media (prefers-color-scheme: dark) {
31851
+ :host {
31852
+ --lens-accent: #fafafa;
31853
+ --lens-accent-strong: #ffffff;
31854
+ --lens-on-accent: #18181b;
31855
+ --lens-focus: rgba(255, 255, 255, 0.22);
31856
+ }
31857
+ }
31690
31858
 
31691
31859
  *, *::before, *::after { box-sizing: border-box; }
31692
31860
 
@@ -31706,7 +31874,7 @@ var OVERLAY_CSS = (
31706
31874
  left: 50%;
31707
31875
  transform: translateX(-50%);
31708
31876
  max-width: calc(100vw - 16px);
31709
- flex-wrap: wrap;
31877
+ flex-wrap: nowrap;
31710
31878
  justify-content: center;
31711
31879
  display: inline-flex;
31712
31880
  align-items: center;
@@ -31730,7 +31898,7 @@ var OVERLAY_CSS = (
31730
31898
  height: 26px;
31731
31899
  margin-left: 4px;
31732
31900
  border-radius: 999px;
31733
- color: #a5b4fc;
31901
+ color: #f4f4f5;
31734
31902
  }
31735
31903
  .lens-btn {
31736
31904
  display: inline-flex;
@@ -31749,11 +31917,14 @@ var OVERLAY_CSS = (
31749
31917
  transition: background 150ms ease, color 150ms ease;
31750
31918
  }
31751
31919
  .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); }
31920
+ .lens-btn:focus-visible { outline: 2px solid rgba(255, 255, 255, 0.7); outline-offset: 1px; }
31921
+ .lens-btn--primary { background: #fff; color: #18181b; }
31922
+ .lens-btn--primary:hover { background: #e4e4e7; }
31923
+ .lens-btn--report svg { flex: none; }
31924
+ .lens-btn__label { white-space: nowrap; }
31755
31925
  .lens-btn--icon { width: 30px; padding: 0; justify-content: center; }
31756
- .lens-btn--icon.is-active { background: var(--lens-accent); color: #fff; }
31926
+ /* Always-dark pill: an active control reads as a light (white) highlight. */
31927
+ .lens-btn--icon.is-active { background: #fff; color: #18181b; }
31757
31928
  .lens-pill__sep {
31758
31929
  width: 1px;
31759
31930
  height: 18px;
@@ -31767,7 +31938,10 @@ var OVERLAY_CSS = (
31767
31938
  z-index: calc(var(--lens-z) - 2);
31768
31939
  pointer-events: none;
31769
31940
  border: 2px solid var(--lens-accent);
31770
- background: rgba(99, 102, 241, 0.14);
31941
+ /* A light outer ring so the (black-in-light / white-in-dark) border stays
31942
+ visible over any page background. */
31943
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.6), 0 0 0 3px rgba(0, 0, 0, 0.15);
31944
+ background: rgba(127, 127, 127, 0.14);
31771
31945
  border-radius: 3px;
31772
31946
  transition: all 60ms linear;
31773
31947
  display: none;
@@ -31779,7 +31953,7 @@ var OVERLAY_CSS = (
31779
31953
  padding: 1px 6px;
31780
31954
  border-radius: 4px;
31781
31955
  background: var(--lens-accent);
31782
- color: #fff;
31956
+ color: var(--lens-on-accent);
31783
31957
  font: 500 11px/1.5 var(--lens-font);
31784
31958
  white-space: nowrap;
31785
31959
  }
@@ -31858,7 +32032,7 @@ var OVERLAY_CSS = (
31858
32032
  padding: 14px 16px;
31859
32033
  border-bottom: 1px solid rgba(0, 0, 0, 0.07);
31860
32034
  }
31861
- .lens-panel__title { font-size: 14px; font-weight: 600; }
32035
+ .lens-panel__title { font-size: 14px; font-weight: 600; margin-right: auto; }
31862
32036
  .lens-panel__x {
31863
32037
  display: inline-flex;
31864
32038
  align-items: center;
@@ -31896,7 +32070,7 @@ var OVERLAY_CSS = (
31896
32070
  .lens-textarea:focus {
31897
32071
  outline: none;
31898
32072
  border-color: var(--lens-accent);
31899
- box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.18);
32073
+ box-shadow: 0 0 0 3px var(--lens-focus);
31900
32074
  }
31901
32075
  /* ---- Context row: static, non-interactive page facts (muted, dot-joined) ---- */
31902
32076
  .lens-context {
@@ -31911,49 +32085,6 @@ var OVERLAY_CSS = (
31911
32085
  .lens-context__item { white-space: nowrap; }
31912
32086
  .lens-context__dot { opacity: 0.55; }
31913
32087
 
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
32088
  .lens-chips { display: flex; flex-wrap: wrap; gap: 6px; }
31958
32089
  .lens-chip {
31959
32090
  display: inline-flex;
@@ -31974,9 +32105,9 @@ var OVERLAY_CSS = (
31974
32105
  .lens-chip--ok { color: #15803d; background: #f0fdf4; border-color: rgba(21, 128, 61, 0.18); }
31975
32106
  .lens-chip--target {
31976
32107
  max-width: 100%;
31977
- background: #eef2ff;
31978
- border-color: rgba(99, 102, 241, 0.25);
31979
- color: var(--lens-accent-strong);
32108
+ background: rgba(0, 0, 0, 0.05);
32109
+ border-color: rgba(0, 0, 0, 0.16);
32110
+ color: #18181b;
31980
32111
  }
31981
32112
  .lens-chip__code {
31982
32113
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
@@ -32024,7 +32155,7 @@ var OVERLAY_CSS = (
32024
32155
  .lens-abtn svg { flex: none; }
32025
32156
  .lens-abtn--ghost { background: transparent; border-color: rgba(0, 0, 0, 0.14); color: #3f3f46; }
32026
32157
  .lens-abtn--ghost:hover { background: rgba(0, 0, 0, 0.04); }
32027
- .lens-abtn--primary { background: var(--lens-accent); color: #fff; }
32158
+ .lens-abtn--primary { background: var(--lens-accent); color: var(--lens-on-accent); }
32028
32159
  .lens-abtn--primary:hover { background: var(--lens-accent-strong); }
32029
32160
  .lens-abtn:disabled { opacity: 0.55; cursor: default; }
32030
32161
  .lens-abtn:focus-visible { outline: 2px solid var(--lens-accent); outline-offset: 1px; }
@@ -32078,12 +32209,9 @@ var OVERLAY_CSS = (
32078
32209
  .lens-panel__x:hover { background: rgba(255, 255, 255, 0.08); }
32079
32210
  .lens-textarea { background: #26262a; border-color: rgba(255, 255, 255, 0.14); color: #f4f4f5; }
32080
32211
  .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
32212
  .lens-chip { background: #26262a; border-color: rgba(255, 255, 255, 0.08); color: #d4d4d8; }
32085
32213
  .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; }
32214
+ .lens-chip--target { background: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.18); color: #f4f4f5; }
32087
32215
  .lens-abtn--ghost { border-color: rgba(255, 255, 255, 0.16); color: #d4d4d8; }
32088
32216
  .lens-abtn--ghost:hover { background: rgba(255, 255, 255, 0.06); }
32089
32217
  .lens-state__msg { color: #a1a1aa; }
@@ -32117,7 +32245,9 @@ var OVERLAY_CSS = (
32117
32245
  .lens-annotate-chips { z-index: var(--lens-z); }
32118
32246
 
32119
32247
  /* ---------------- Annotation toolbar ---------------- */
32120
- /* Same floating top-center anchor as the chips row; wraps on narrow screens. */
32248
+ /* Floating top-center anchor. A fixed drag grip sits left of a tools "track":
32249
+ the track wraps on desktop and becomes a single horizontal-scroll row on
32250
+ narrow screens, so the bar stays one slim height instead of a bulky block. */
32121
32251
  .lens-annotate-toolbar {
32122
32252
  position: fixed;
32123
32253
  top: calc(16px + env(safe-area-inset-top));
@@ -32125,11 +32255,10 @@ var OVERLAY_CSS = (
32125
32255
  transform: translateX(-50%);
32126
32256
  z-index: var(--lens-z);
32127
32257
  display: inline-flex;
32128
- flex-wrap: wrap;
32258
+ flex-wrap: nowrap;
32129
32259
  align-items: center;
32130
- justify-content: center;
32131
32260
  gap: 6px;
32132
- max-width: calc(100vw - 24px);
32261
+ max-width: calc(100vw - 16px);
32133
32262
  padding: 6px 8px;
32134
32263
  border-radius: 14px;
32135
32264
  background: rgba(20, 20, 23, 0.92);
@@ -32139,12 +32268,50 @@ var OVERLAY_CSS = (
32139
32268
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.35);
32140
32269
  color: #f4f4f5;
32141
32270
  }
32271
+ /* Tools lay out as ONE horizontal row that scrolls if it can't fit, so the bar
32272
+ is always a single slim height \u2014 never a wrapping (or, worse, min-content
32273
+ collapsed vertical) block. The grip sits outside this track. */
32274
+ .lens-tool-track {
32275
+ display: flex;
32276
+ align-items: center;
32277
+ flex-wrap: nowrap;
32278
+ justify-content: flex-start;
32279
+ gap: 6px;
32280
+ flex: 1 1 auto;
32281
+ min-width: 0;
32282
+ overflow-x: auto;
32283
+ overflow-y: hidden;
32284
+ scrollbar-width: none;
32285
+ -webkit-overflow-scrolling: touch;
32286
+ }
32287
+ .lens-tool-track::-webkit-scrollbar { display: none; }
32142
32288
  .lens-tool-group {
32143
32289
  display: inline-flex;
32144
32290
  align-items: center;
32145
32291
  gap: 4px;
32146
- flex-wrap: wrap;
32292
+ flex-wrap: nowrap;
32293
+ flex: none;
32294
+ }
32295
+ /* Single close (=Done, keeps marks) pinned at the right, always reachable
32296
+ while the tools scroll. Reads as a light control on the dark bar. */
32297
+ .lens-tool-close {
32298
+ display: inline-flex;
32299
+ align-items: center;
32300
+ justify-content: center;
32301
+ flex: none;
32302
+ width: 32px;
32303
+ height: 30px;
32304
+ padding: 0;
32305
+ border: 0;
32306
+ border-radius: 8px;
32307
+ background: rgba(255, 255, 255, 0.14);
32308
+ color: #f4f4f5;
32309
+ cursor: pointer;
32310
+ transition: background 150ms ease;
32147
32311
  }
32312
+ .lens-tool-close:hover { background: rgba(255, 255, 255, 0.26); }
32313
+ .lens-tool-close:focus-visible { outline: 2px solid rgba(255, 255, 255, 0.7); outline-offset: 1px; }
32314
+ .lens-tool-close svg { width: 18px; height: 18px; }
32148
32315
  .lens-tool-sep {
32149
32316
  width: 1px;
32150
32317
  align-self: stretch;
@@ -32152,6 +32319,51 @@ var OVERLAY_CSS = (
32152
32319
  background: rgba(255, 255, 255, 0.16);
32153
32320
  margin: 0 2px;
32154
32321
  }
32322
+
32323
+ /* ---------------- Draggable surfaces ---------------- */
32324
+ /* Every drag handle: no page pan/scroll gesture starts here, and the cursor
32325
+ signals grabbability. Applied to the pill, panel header, toolbar grip, chips. */
32326
+ .lens-drag-handle { touch-action: none; cursor: grab; }
32327
+ .lens-drag-handle:active { cursor: grabbing; }
32328
+ .is-dragging, .is-dragging * { cursor: grabbing !important; }
32329
+ .is-dragging { user-select: none; }
32330
+ /* While dragging, drop the entry animation/transition so it tracks 1:1. */
32331
+ .lens-pill.is-dragging, .lens-panel.is-dragging,
32332
+ .lens-annotate-toolbar.is-dragging, .lens-rec-chip.is-dragging {
32333
+ animation: none;
32334
+ transition: none;
32335
+ }
32336
+
32337
+ /* Toolbar drag grip \u2014 the only grab region, kept out of the scroll track. */
32338
+ .lens-tool-grip {
32339
+ display: inline-flex;
32340
+ align-items: center;
32341
+ justify-content: center;
32342
+ flex: none;
32343
+ width: 22px;
32344
+ height: 30px;
32345
+ margin-right: 2px;
32346
+ border-radius: 6px;
32347
+ color: #a1a1aa;
32348
+ }
32349
+ .lens-tool-grip:hover { color: #f4f4f5; background: rgba(255, 255, 255, 0.08); }
32350
+ .lens-tool-grip svg { width: 16px; height: 16px; }
32351
+
32352
+ /* Pill grip (the eye dot) + panel header grip. */
32353
+ .lens-pill__grip { cursor: grab; }
32354
+ .lens-panel__grip {
32355
+ display: inline-flex;
32356
+ align-items: center;
32357
+ justify-content: center;
32358
+ flex: none;
32359
+ width: 22px;
32360
+ height: 22px;
32361
+ margin-right: 6px;
32362
+ color: #a1a1aa;
32363
+ }
32364
+ .lens-panel__grip svg { width: 16px; height: 16px; }
32365
+ .lens-panel__head { cursor: grab; }
32366
+ .lens-panel__head:active { cursor: grabbing; }
32155
32367
  .lens-tool-btn {
32156
32368
  display: inline-flex;
32157
32369
  align-items: center;
@@ -32167,11 +32379,11 @@ var OVERLAY_CSS = (
32167
32379
  transition: background 150ms ease, color 150ms ease, border-color 150ms ease;
32168
32380
  }
32169
32381
  .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; }
32382
+ .lens-tool-btn:focus-visible { outline: 2px solid rgba(255, 255, 255, 0.7); outline-offset: 1px; }
32171
32383
  .lens-tool-btn.is-active {
32172
- background: var(--lens-accent);
32173
- border-color: var(--lens-accent);
32174
- color: #fff;
32384
+ background: #fff;
32385
+ border-color: #fff;
32386
+ color: #18181b;
32175
32387
  }
32176
32388
  .lens-tool-btn:disabled { opacity: 0.4; cursor: default; }
32177
32389
  .lens-tool-btn:disabled:hover { background: transparent; color: #d4d4d8; }
@@ -32211,17 +32423,18 @@ var OVERLAY_CSS = (
32211
32423
  transition: background 150ms ease, color 150ms ease, border-color 150ms ease;
32212
32424
  }
32213
32425
  .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; }
32426
+ .lens-width-btn:focus-visible { outline: 2px solid rgba(255, 255, 255, 0.7); outline-offset: 1px; }
32215
32427
  .lens-width-btn.is-active {
32216
- background: var(--lens-accent);
32217
- border-color: var(--lens-accent);
32218
- color: #fff;
32428
+ background: #fff;
32429
+ border-color: #fff;
32430
+ color: #18181b;
32219
32431
  }
32220
32432
 
32221
32433
  /* Coarse pointers: bump the tool targets toward 40px. */
32222
32434
  @media (pointer: coarse) {
32223
32435
  .lens-tool-btn, .lens-width-btn { width: 38px; height: 38px; }
32224
32436
  .lens-swatch { width: 30px; height: 30px; }
32437
+ .lens-tool-grip { width: 30px; height: 38px; }
32225
32438
  }
32226
32439
 
32227
32440
  /* Stacking inside the shadow layer: the panel, pill and floating chips always
@@ -32246,8 +32459,8 @@ var OVERLAY_CSS = (
32246
32459
  transition: background 150ms ease;
32247
32460
  }
32248
32461
  .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); }
32462
+ .lens-pick-chip__btn--primary { background: #fff; color: #18181b; }
32463
+ .lens-pick-chip__btn--primary:hover { background: #e4e4e7; }
32251
32464
 
32252
32465
  /* ---------------- Recording chip ---------------- */
32253
32466
  .lens-rec-chip {
@@ -32302,21 +32515,29 @@ var OVERLAY_CSS = (
32302
32515
  .lens-rec-chip__stop:hover { background: #dc2626; }
32303
32516
  .lens-chip--rec { gap: 6px; }
32304
32517
 
32518
+ /* Very narrow screens: collapse the pill's "Report issue" label to its icon so
32519
+ the pill stays a single slim row (never wraps into a 2-row blob). */
32520
+ @media (max-width: 400px) {
32521
+ .lens-btn__label { display: none; }
32522
+ .lens-btn--report { width: 44px; padding: 0; justify-content: center; }
32523
+ }
32524
+
32305
32525
  /* ---------------- Coarse pointers (touch): \u226544px targets ---------------- */
32306
32526
  @media (pointer: coarse) {
32307
32527
  .lens-btn { height: 44px; padding: 0 16px; }
32308
32528
  .lens-btn--icon { width: 44px; }
32529
+ .lens-tool-close { width: 44px; height: 38px; }
32309
32530
  .lens-pill__dot { width: 40px; height: 40px; }
32310
32531
  .lens-abtn { height: 44px; }
32311
- .lens-tgl { min-height: 40px; padding: 4px 14px; }
32312
32532
  .lens-panel__x { width: 44px; height: 44px; }
32313
32533
  .lens-chip__x { min-width: 32px; min-height: 32px; align-items: center; justify-content: center; }
32314
32534
  .lens-pick-chip__btn, .lens-rec-chip__stop { height: 40px; padding: 0 18px; }
32315
32535
  }
32316
32536
 
32317
- /* ---------------- Bottom sheet under 640px ---------------- */
32537
+ /* ---------------- Narrow screens (\u2264640px) ---------------- */
32318
32538
  @media (max-width: 640px) {
32319
- .lens-panel {
32539
+ /* Report panel as a bottom sheet (unless the user has dragged it loose). */
32540
+ .lens-panel:not(.is-dragging) {
32320
32541
  left: 0;
32321
32542
  right: 0;
32322
32543
  bottom: 0;
@@ -32333,6 +32554,14 @@ var OVERLAY_CSS = (
32333
32554
  to { opacity: 1; transform: none; }
32334
32555
  }
32335
32556
  .lens-textarea { font-size: 16px; }
32557
+
32558
+ /* On narrow screens the single-row tool track usually overflows: fade its
32559
+ trailing edge so it reads as "more tools this way" (the grip sits outside
32560
+ the track and stays fully opaque). */
32561
+ .lens-tool-track {
32562
+ -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 20px), transparent 100%);
32563
+ mask-image: linear-gradient(90deg, #000 calc(100% - 20px), transparent 100%);
32564
+ }
32336
32565
  }
32337
32566
  `
32338
32567
  );
@@ -32350,8 +32579,14 @@ var Overlay = class {
32350
32579
  this.shadow.append(this.layer);
32351
32580
  this.picker = new ElementPicker(this.layer, this.host);
32352
32581
  this.annotator = new AnnotationEditor(this.layer);
32353
- this.layer.append(this.buildPill());
32582
+ const pill = this.buildPill();
32583
+ this.layer.append(pill);
32354
32584
  document.documentElement.append(this.host);
32585
+ this.pillDrag = makeDraggable(pill, {
32586
+ handle: pill,
32587
+ key: "lens-pill",
32588
+ ignore: isInteractiveTarget
32589
+ });
32355
32590
  window.addEventListener("keydown", this.onGlobalKey, true);
32356
32591
  }
32357
32592
  deps;
@@ -32364,7 +32599,9 @@ var Overlay = class {
32364
32599
  panel = null;
32365
32600
  target = null;
32366
32601
  destroyed = false;
32602
+ pillDrag = null;
32367
32603
  recordingChip = null;
32604
+ recordingDrag = null;
32368
32605
  recordingTimerEl = null;
32369
32606
  recordingInterval = null;
32370
32607
  recordingStartTs = 0;
@@ -32379,10 +32616,16 @@ var Overlay = class {
32379
32616
  on: { click: () => this.togglePicker() }
32380
32617
  });
32381
32618
  return el("div", { class: "lens-pill", attrs: { role: "toolbar", "aria-label": "Lens" } }, [
32382
- el("span", { class: "lens-pill__dot", html: iconEye }),
32619
+ el("span", {
32620
+ class: "lens-pill__dot lens-pill__grip",
32621
+ html: iconEye,
32622
+ title: "Drag to move",
32623
+ ariaLabel: "Drag to move Lens"
32624
+ }),
32383
32625
  el("button", {
32384
- class: "lens-btn",
32385
- text: "Report issue",
32626
+ class: "lens-btn lens-btn--report",
32627
+ html: iconReport + '<span class="lens-btn__label">Report issue</span>',
32628
+ ariaLabel: "Report issue",
32386
32629
  on: { click: () => this.open() }
32387
32630
  }),
32388
32631
  this.pickBtn,
@@ -32436,6 +32679,7 @@ var Overlay = class {
32436
32679
  this.stopPicker();
32437
32680
  return;
32438
32681
  }
32682
+ if (this.annotator.isActive) this.annotator.exit();
32439
32683
  this.close();
32440
32684
  this.pickBtn.classList.add("is-active");
32441
32685
  this.picker.start(
@@ -32497,6 +32741,11 @@ var Overlay = class {
32497
32741
  })
32498
32742
  ]);
32499
32743
  this.layer.append(this.recordingChip);
32744
+ this.recordingDrag = makeDraggable(this.recordingChip, {
32745
+ handle: this.recordingChip,
32746
+ key: "lens-rec-chip",
32747
+ ignore: isInteractiveTarget
32748
+ });
32500
32749
  this.recordingInterval = setInterval(() => this.tickRecording(), 250);
32501
32750
  }
32502
32751
  tickRecording() {
@@ -32509,6 +32758,10 @@ var Overlay = class {
32509
32758
  clearInterval(this.recordingInterval);
32510
32759
  this.recordingInterval = null;
32511
32760
  }
32761
+ if (this.recordingDrag) {
32762
+ this.recordingDrag.destroy();
32763
+ this.recordingDrag = null;
32764
+ }
32512
32765
  if (this.recordingChip) {
32513
32766
  this.recordingChip.remove();
32514
32767
  this.recordingChip = null;
@@ -32549,6 +32802,8 @@ var Overlay = class {
32549
32802
  if (this.destroyed) return;
32550
32803
  this.destroyed = true;
32551
32804
  window.removeEventListener("keydown", this.onGlobalKey, true);
32805
+ this.pillDrag?.destroy();
32806
+ this.pillDrag = null;
32552
32807
  this.stopPicker();
32553
32808
  if (this.deps.client.isRecording()) this.deps.client.stopRecording();
32554
32809
  this.hideRecordingChip();