@merittdev/horus-lens 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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.4.0" : "0.0.0-dev";
29085
+ var SDK_VERSION = true ? "0.5.1" : "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) {
@@ -29635,8 +29663,11 @@ function computeCropRect(crop, scale, rasterWidth, rasterHeight) {
29635
29663
  async function captureScreenshot(excludeHost, crop, composite) {
29636
29664
  if (typeof document === "undefined" || typeof window === "undefined") return null;
29637
29665
  try {
29638
- const viewportPixels = window.innerWidth * window.innerHeight || 1;
29639
- const pixelCap = Math.sqrt(SCREENSHOT_MAX_PIXELS / viewportPixels);
29666
+ const viewportOnly = !composite && !crop;
29667
+ const captureW = composite ? composite.docWidth : window.innerWidth;
29668
+ const captureH = composite ? composite.docHeight : window.innerHeight;
29669
+ const capturePixels = captureW * captureH || 1;
29670
+ const pixelCap = Math.sqrt(SCREENSHOT_MAX_PIXELS / capturePixels);
29640
29671
  const scale = Math.max(1, Math.min(window.devicePixelRatio || 1, SCREENSHOT_MAX_SCALE, pixelCap));
29641
29672
  const capture = domToBlob(document.documentElement, {
29642
29673
  scale,
@@ -29645,12 +29676,19 @@ async function captureScreenshot(excludeHost, crop, composite) {
29645
29676
  // JPEG has no alpha — transparent page regions (and any capture box
29646
29677
  // extending past short content) would otherwise encode as BLACK.
29647
29678
  backgroundColor: "#ffffff",
29648
- // When compositing annotations, force the capture box to the editor's
29649
- // document measurement (scrollHeight-based). modern-screenshot otherwise
29650
- // sizes from documentElement's bounding box, which on pages SHORTER than
29651
- // the viewport is the content height only — marks drawn in the lower
29652
- // viewport band would fall off the raster and the crop would clamp short.
29679
+ // Composite (annotations): force the capture box to the editor's document
29680
+ // measurement so marks in the lower viewport band aren't clipped.
29653
29681
  ...composite ? { width: composite.docWidth, height: composite.docHeight } : {},
29682
+ // Plain report: clip the render to the viewport and shift the scrolled
29683
+ // content into it, so only the visible region is rasterized/fetched.
29684
+ ...viewportOnly ? {
29685
+ width: window.innerWidth,
29686
+ height: window.innerHeight,
29687
+ style: {
29688
+ transform: `translate(${-window.scrollX}px, ${-window.scrollY}px)`,
29689
+ transformOrigin: "top left"
29690
+ }
29691
+ } : {},
29654
29692
  // Exclude the overlay from the capture CLONE — never hide the live node.
29655
29693
  filter: (node2) => node2 !== excludeHost,
29656
29694
  // Per-resource fetch budget: a single slow cross-origin image must not
@@ -30120,12 +30158,22 @@ var LensClient = class {
30120
30158
  input2.screenshotCrop,
30121
30159
  input2.annotationComposite
30122
30160
  ) : Promise.resolve(null);
30161
+ const consumingManualRecording = wantReplay && (this.replay?.hasManualRecording() ?? false) && !(this.replay?.isManualRecording() ?? false);
30123
30162
  const replayEvents = wantReplay ? this.replay?.snapshot() ?? [] : [];
30124
30163
  const errorEntries = this.errorBuffer.drain();
30125
- const consoleTail = this.consoleBuffer.tail(CONSOLE_TAIL_SIZE);
30126
- const networkTail = this.networkBuffer.tail(NETWORK_TAIL_SIZE);
30127
30164
  const consoleEntries = this.consoleBuffer.drain();
30128
30165
  const networkEntries = this.networkBuffer.drain();
30166
+ this.errorBuffer.clear();
30167
+ this.consoleBuffer.clear();
30168
+ this.networkBuffer.clear();
30169
+ const consoleTail = consoleEntries.slice(-CONSOLE_TAIL_SIZE);
30170
+ const isNetFailure = (e) => e.ok !== true || Boolean(e.error);
30171
+ const recentFailures = networkEntries.filter(isNetFailure).slice(-NETWORK_TAIL_SIZE);
30172
+ const keep = /* @__PURE__ */ new Set([
30173
+ ...recentFailures,
30174
+ ...networkEntries.slice(-NETWORK_TAIL_SIZE)
30175
+ ]);
30176
+ const networkTail = networkEntries.filter((e) => keep.has(e)).slice(-NETWORK_TAIL_SIZE);
30129
30177
  let metadata = {
30130
30178
  annotations: input2.annotations ?? [],
30131
30179
  env: collectEnv(),
@@ -30179,13 +30227,14 @@ var LensClient = class {
30179
30227
  }
30180
30228
  if (!asset.blob) continue;
30181
30229
  try {
30182
- await this.transport.uploadAsset(upload, asset.blob);
30230
+ await this.uploadAssetWithTimeout(upload, asset.blob);
30183
30231
  uploadedAssets.push({ kind: upload.kind, key: upload.key, byteSize: asset.blob.size });
30184
30232
  } catch (err) {
30185
30233
  this.warn(`asset upload failed (${upload.kind})`, err);
30186
30234
  }
30187
30235
  }
30188
30236
  const finalized = await this.transport.finalize(initiated.reportId, metadata, uploadedAssets);
30237
+ if (consumingManualRecording) this.replay?.discardManualRecording();
30189
30238
  if (screenshotUpload) {
30190
30239
  const upload = screenshotUpload;
30191
30240
  void screenshotPromise.then(async (blob2) => {
@@ -30203,6 +30252,21 @@ var LensClient = class {
30203
30252
  }
30204
30253
  return { reportId: finalized.reportId };
30205
30254
  }
30255
+ /**
30256
+ * Upload a foreground asset with a bounded timeout. Without this a stuck S3
30257
+ * PUT (dropped connection, half-open socket) would hang `submitReport`
30258
+ * forever; the timeout aborts the individual PUT so it fails as one asset
30259
+ * (caught by the caller) instead of wedging the whole submit.
30260
+ */
30261
+ async uploadAssetWithTimeout(upload, blob2) {
30262
+ const controller = new AbortController();
30263
+ const timer = setTimeout(() => controller.abort(), SCREENSHOT_TIMEOUT_MS);
30264
+ try {
30265
+ await this.transport.uploadAsset(upload, blob2, controller.signal);
30266
+ } finally {
30267
+ clearTimeout(timer);
30268
+ }
30269
+ }
30206
30270
  warn(message, err) {
30207
30271
  if (!this.debug) return;
30208
30272
  if (err !== void 0) console.warn(`[lens] ${message}`, err);
@@ -30331,6 +30395,113 @@ function el(tag, props = {}, children = []) {
30331
30395
  function clear(node2) {
30332
30396
  while (node2.firstChild) node2.removeChild(node2.firstChild);
30333
30397
  }
30398
+ var MARGIN = 6;
30399
+ var DRAG_THRESHOLD = 3;
30400
+ var savedPositions = /* @__PURE__ */ new Map();
30401
+ function isInteractiveTarget(target) {
30402
+ return target instanceof Element && target.closest(
30403
+ 'button, a, input, textarea, select, [role="switch"], [role="button"], [contenteditable="true"]'
30404
+ ) !== null;
30405
+ }
30406
+ function clampToViewport(element, left, top) {
30407
+ const rect = element.getBoundingClientRect();
30408
+ const vw = window.innerWidth;
30409
+ const vh = window.innerHeight;
30410
+ const maxLeft = Math.max(MARGIN, vw - rect.width - MARGIN);
30411
+ const maxTop = Math.max(MARGIN, vh - rect.height - MARGIN);
30412
+ return {
30413
+ left: Math.min(Math.max(left, MARGIN), maxLeft),
30414
+ top: Math.min(Math.max(top, MARGIN), maxTop)
30415
+ };
30416
+ }
30417
+ function makeDraggable(element, opts) {
30418
+ const { handle, key } = opts;
30419
+ const ignore = opts.ignore ?? isInteractiveTarget;
30420
+ let startX = 0;
30421
+ let startY = 0;
30422
+ let originLeft = 0;
30423
+ let originTop = 0;
30424
+ let dragging = false;
30425
+ let armed = false;
30426
+ let pointerId = null;
30427
+ const apply = (left, top) => {
30428
+ const p = clampToViewport(element, left, top);
30429
+ element.style.left = `${p.left}px`;
30430
+ element.style.top = `${p.top}px`;
30431
+ element.style.right = "auto";
30432
+ element.style.bottom = "auto";
30433
+ element.style.transform = "none";
30434
+ savedPositions.set(key, p);
30435
+ opts.onMove?.();
30436
+ };
30437
+ const onPointerDown = (e) => {
30438
+ if (e.button !== 0 && e.pointerType === "mouse") return;
30439
+ if (ignore(e.target)) return;
30440
+ armed = true;
30441
+ dragging = false;
30442
+ pointerId = e.pointerId;
30443
+ startX = e.clientX;
30444
+ startY = e.clientY;
30445
+ const rect = element.getBoundingClientRect();
30446
+ originLeft = rect.left;
30447
+ originTop = rect.top;
30448
+ window.addEventListener("pointermove", onPointerMove, true);
30449
+ window.addEventListener("pointerup", onPointerUp, true);
30450
+ window.addEventListener("pointercancel", onPointerUp, true);
30451
+ };
30452
+ const onPointerMove = (e) => {
30453
+ if (!armed || e.pointerId !== pointerId) return;
30454
+ const dx = e.clientX - startX;
30455
+ const dy = e.clientY - startY;
30456
+ if (!dragging) {
30457
+ if (Math.abs(dx) + Math.abs(dy) < DRAG_THRESHOLD) return;
30458
+ dragging = true;
30459
+ element.classList.add("is-dragging");
30460
+ }
30461
+ e.preventDefault();
30462
+ apply(originLeft + dx, originTop + dy);
30463
+ };
30464
+ const onPointerUp = (e) => {
30465
+ if (e.pointerId !== pointerId) return;
30466
+ armed = false;
30467
+ if (dragging) {
30468
+ dragging = false;
30469
+ element.classList.remove("is-dragging");
30470
+ }
30471
+ pointerId = null;
30472
+ window.removeEventListener("pointermove", onPointerMove, true);
30473
+ window.removeEventListener("pointerup", onPointerUp, true);
30474
+ window.removeEventListener("pointercancel", onPointerUp, true);
30475
+ };
30476
+ const onResize = () => {
30477
+ const p = savedPositions.get(key);
30478
+ if (p) apply(p.left, p.top);
30479
+ };
30480
+ handle.classList.add("lens-drag-handle");
30481
+ handle.addEventListener("pointerdown", onPointerDown);
30482
+ window.addEventListener("resize", onResize);
30483
+ const stored = savedPositions.get(key);
30484
+ if (stored) requestAnimationFrame(() => apply(stored.left, stored.top));
30485
+ return {
30486
+ reclamp: onResize,
30487
+ reset() {
30488
+ savedPositions.delete(key);
30489
+ element.style.left = "";
30490
+ element.style.top = "";
30491
+ element.style.right = "";
30492
+ element.style.bottom = "";
30493
+ element.style.transform = "";
30494
+ element.classList.remove("is-dragging");
30495
+ },
30496
+ destroy() {
30497
+ handle.removeEventListener("pointerdown", onPointerDown);
30498
+ window.removeEventListener("pointermove", onPointerMove, true);
30499
+ window.removeEventListener("pointerup", onPointerUp, true);
30500
+ window.removeEventListener("pointercancel", onPointerUp, true);
30501
+ window.removeEventListener("resize", onResize);
30502
+ }
30503
+ };
30504
+ }
30334
30505
  var THIN_MIN_DIST = 3;
30335
30506
  var MAX_POINTS = 2e3;
30336
30507
  var HIGHLIGHTER_WIDTH = 14;
@@ -30481,6 +30652,8 @@ var iconTrash = svg(
30481
30652
  '<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
30653
  );
30483
30654
  var iconAnnotate = iconPencil;
30655
+ 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>`;
30656
+ 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
30657
  var SVG_NS = "http://www.w3.org/2000/svg";
30485
30658
  var FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
30486
30659
  var MIN_DRAG = 6;
@@ -30509,6 +30682,7 @@ var AnnotationEditor = class {
30509
30682
  svg = null;
30510
30683
  bg = null;
30511
30684
  toolbar = null;
30685
+ toolbarDrag = null;
30512
30686
  textInputEl = null;
30513
30687
  textAnchor = null;
30514
30688
  textColor = COLORS[0];
@@ -30554,6 +30728,7 @@ var AnnotationEditor = class {
30554
30728
  this.markNodes = [];
30555
30729
  this.annotations = [];
30556
30730
  this.stepCounter = 0;
30731
+ this.sessionBase = Math.min(this.sessionBase, this.annotations.length);
30557
30732
  if (!this.active) this.teardownLayer();
30558
30733
  this.syncActionButtons();
30559
30734
  }
@@ -30568,6 +30743,7 @@ var AnnotationEditor = class {
30568
30743
  this.sizeLayer();
30569
30744
  this.updateTransform();
30570
30745
  svg2.style.pointerEvents = "auto";
30746
+ if (this.bg) this.bg.style.pointerEvents = "all";
30571
30747
  svg2.style.touchAction = "none";
30572
30748
  svg2.style.cursor = "crosshair";
30573
30749
  svg2.addEventListener("pointerdown", this.onPointerDown);
@@ -30588,6 +30764,7 @@ var AnnotationEditor = class {
30588
30764
  if (svg2) {
30589
30765
  svg2.removeEventListener("pointerdown", this.onPointerDown);
30590
30766
  svg2.style.pointerEvents = "none";
30767
+ if (this.bg) this.bg.style.pointerEvents = "none";
30591
30768
  svg2.style.touchAction = "";
30592
30769
  svg2.style.cursor = "";
30593
30770
  }
@@ -30597,15 +30774,8 @@ var AnnotationEditor = class {
30597
30774
  this.onExit = null;
30598
30775
  cb?.();
30599
30776
  }
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
30777
  undo() {
30778
+ if (this.markNodes.length <= this.sessionBase) return;
30609
30779
  const node2 = this.markNodes.pop();
30610
30780
  if (!node2) return;
30611
30781
  node2.remove();
@@ -30821,39 +30991,45 @@ var AnnotationEditor = class {
30821
30991
  on: { click: () => this.clear() }
30822
30992
  });
30823
30993
  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
- })
30994
+ const closeBtn = el("button", {
30995
+ class: "lens-tool-close",
30996
+ ariaLabel: "Done",
30997
+ title: "Done",
30998
+ html: iconClose,
30999
+ attrs: { type: "button" },
31000
+ on: { click: () => this.exit() }
31001
+ });
31002
+ const grip = el("span", {
31003
+ class: "lens-tool-grip",
31004
+ html: iconGrip,
31005
+ title: "Drag to move toolbar",
31006
+ ariaLabel: "Drag to move toolbar"
31007
+ });
31008
+ const track = el("div", { class: "lens-tool-track" }, [
31009
+ toolGroup,
31010
+ el("span", { class: "lens-tool-sep" }),
31011
+ colorGroup,
31012
+ el("span", { class: "lens-tool-sep" }),
31013
+ widthGroup,
31014
+ el("span", { class: "lens-tool-sep" }),
31015
+ editGroup
30837
31016
  ]);
30838
31017
  this.toolbar = el(
30839
31018
  "div",
30840
31019
  { 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
- ]
31020
+ [grip, track, closeBtn]
30852
31021
  );
30853
31022
  this.host.append(this.toolbar);
31023
+ this.toolbarDrag = makeDraggable(this.toolbar, {
31024
+ handle: grip,
31025
+ key: "lens-annotate-toolbar",
31026
+ ignore: isInteractiveTarget
31027
+ });
30854
31028
  this.syncActionButtons();
30855
31029
  }
30856
31030
  removeToolbar() {
31031
+ this.toolbarDrag?.destroy();
31032
+ this.toolbarDrag = null;
30857
31033
  this.toolbar?.remove();
30858
31034
  this.toolbar = null;
30859
31035
  this.toolBtns.clear();
@@ -30890,7 +31066,7 @@ var AnnotationEditor = class {
30890
31066
  }
30891
31067
  syncActionButtons() {
30892
31068
  const empty = this.markNodes.length === 0;
30893
- if (this.undoBtn) this.undoBtn.disabled = empty;
31069
+ if (this.undoBtn) this.undoBtn.disabled = this.markNodes.length <= this.sessionBase;
30894
31070
  if (this.clearBtn) this.clearBtn.disabled = empty;
30895
31071
  }
30896
31072
  // --------------------------------------------------------------------------
@@ -30904,6 +31080,7 @@ var AnnotationEditor = class {
30904
31080
  }
30905
31081
  onPointerDown = (e) => {
30906
31082
  if (!this.active) return;
31083
+ if (this.drag) return;
30907
31084
  if (e.button !== void 0 && e.button !== 0) return;
30908
31085
  e.preventDefault();
30909
31086
  const p = this.toDoc(e);
@@ -30953,6 +31130,7 @@ var AnnotationEditor = class {
30953
31130
  this.drag = drag;
30954
31131
  window.addEventListener("pointermove", this.onPointerMove, true);
30955
31132
  window.addEventListener("pointerup", this.onPointerUp, true);
31133
+ window.addEventListener("pointercancel", this.onPointerCancel, true);
30956
31134
  };
30957
31135
  onPointerMove = (e) => {
30958
31136
  const d = this.drag;
@@ -31062,9 +31240,18 @@ var AnnotationEditor = class {
31062
31240
  d.node.removeAttribute("data-lens-draft");
31063
31241
  this.commitMark(annotation, d.node);
31064
31242
  };
31243
+ /** Pointer capture was cancelled (OS gesture, page hidden, etc.): drop the
31244
+ * in-progress draft node and detach listeners so nothing leaks. */
31245
+ onPointerCancel = () => {
31246
+ const d = this.drag;
31247
+ this.detachDragListeners();
31248
+ this.drag = null;
31249
+ d?.node.remove();
31250
+ };
31065
31251
  detachDragListeners() {
31066
31252
  window.removeEventListener("pointermove", this.onPointerMove, true);
31067
31253
  window.removeEventListener("pointerup", this.onPointerUp, true);
31254
+ window.removeEventListener("pointercancel", this.onPointerCancel, true);
31068
31255
  }
31069
31256
  commitMark(annotation, node2) {
31070
31257
  this.annotations.push(annotation);
@@ -31258,29 +31445,31 @@ var AnnotationEditor = class {
31258
31445
  return `lens-${++this.idSeq}`;
31259
31446
  }
31260
31447
  };
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
31448
  var Panel = class {
31269
31449
  constructor(cb) {
31270
31450
  this.cb = cb;
31271
31451
  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
31452
+ const head = el("div", { class: "lens-panel__head" }, [
31453
+ el("span", {
31454
+ class: "lens-panel__grip",
31455
+ html: iconGrip,
31456
+ title: "Drag to move",
31457
+ ariaLabel: "Drag to move panel"
31458
+ }),
31459
+ el("div", { class: "lens-panel__title", text: "Report an issue" }),
31460
+ el("button", {
31461
+ class: "lens-panel__x",
31462
+ ariaLabel: "Close",
31463
+ html: iconClose,
31464
+ on: { click: () => this.cb.onClose() }
31465
+ })
31283
31466
  ]);
31467
+ this.root = el(
31468
+ "div",
31469
+ { class: "lens-panel", attrs: { role: "dialog", "aria-label": "Report an issue" } },
31470
+ [head, this.body]
31471
+ );
31472
+ this.drag = makeDraggable(this.root, { handle: head, key: "lens-panel" });
31284
31473
  this.renderForm();
31285
31474
  }
31286
31475
  cb;
@@ -31288,6 +31477,7 @@ var Panel = class {
31288
31477
  body;
31289
31478
  textarea;
31290
31479
  submitBtn;
31480
+ drag = null;
31291
31481
  autoCloseTimer = null;
31292
31482
  submitting = false;
31293
31483
  /** Selected capture toggles; every flag starts ON. Persists across refresh. */
@@ -31307,6 +31497,8 @@ var Panel = class {
31307
31497
  }
31308
31498
  destroy() {
31309
31499
  if (this.autoCloseTimer) clearTimeout(this.autoCloseTimer);
31500
+ this.drag?.destroy();
31501
+ this.drag = null;
31310
31502
  this.root.remove();
31311
31503
  }
31312
31504
  renderForm() {
@@ -31321,7 +31513,6 @@ var Panel = class {
31321
31513
  this.body.append(
31322
31514
  this.textarea,
31323
31515
  this.buildContext(),
31324
- this.buildIncludes(),
31325
31516
  this.buildChips(),
31326
31517
  this.buildActions()
31327
31518
  );
@@ -31341,39 +31532,6 @@ var Panel = class {
31341
31532
  });
31342
31533
  return el("div", { class: "lens-context", title: s.url }, nodes);
31343
31534
  }
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
31535
  /** Removable context chips: annotations, retained recording, picked element. */
31378
31536
  buildChips() {
31379
31537
  const row = el("div", { class: "lens-chips" });
@@ -31678,14 +31836,28 @@ var OVERLAY_CSS = (
31678
31836
  `
31679
31837
  :host {
31680
31838
  all: initial;
31681
- --lens-accent: #6366f1;
31682
- --lens-accent-strong: #4f46e5;
31839
+ /* Black & white, mirroring the Horus Cloud dashboard: the accent is near-black
31840
+ in light and near-white in dark; the always-dark pill/toolbar use explicit
31841
+ light highlights (defined at their rules). Annotation swatch colors and
31842
+ status red/green are content, not brand, and keep their hues. */
31843
+ --lens-accent: #18181b;
31844
+ --lens-accent-strong: #000000;
31845
+ --lens-on-accent: #ffffff;
31846
+ --lens-focus: rgba(0, 0, 0, 0.16);
31683
31847
  --lens-radius: 8px;
31684
31848
  --lens-font: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
31685
31849
  --lens-z: 2147483000;
31686
31850
  font-family: var(--lens-font);
31687
31851
  line-height: 1.4;
31688
31852
  }
31853
+ @media (prefers-color-scheme: dark) {
31854
+ :host {
31855
+ --lens-accent: #fafafa;
31856
+ --lens-accent-strong: #ffffff;
31857
+ --lens-on-accent: #18181b;
31858
+ --lens-focus: rgba(255, 255, 255, 0.22);
31859
+ }
31860
+ }
31689
31861
 
31690
31862
  *, *::before, *::after { box-sizing: border-box; }
31691
31863
 
@@ -31705,7 +31877,7 @@ var OVERLAY_CSS = (
31705
31877
  left: 50%;
31706
31878
  transform: translateX(-50%);
31707
31879
  max-width: calc(100vw - 16px);
31708
- flex-wrap: wrap;
31880
+ flex-wrap: nowrap;
31709
31881
  justify-content: center;
31710
31882
  display: inline-flex;
31711
31883
  align-items: center;
@@ -31729,7 +31901,7 @@ var OVERLAY_CSS = (
31729
31901
  height: 26px;
31730
31902
  margin-left: 4px;
31731
31903
  border-radius: 999px;
31732
- color: #a5b4fc;
31904
+ color: #f4f4f5;
31733
31905
  }
31734
31906
  .lens-btn {
31735
31907
  display: inline-flex;
@@ -31748,11 +31920,14 @@ var OVERLAY_CSS = (
31748
31920
  transition: background 150ms ease, color 150ms ease;
31749
31921
  }
31750
31922
  .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); }
31923
+ .lens-btn:focus-visible { outline: 2px solid rgba(255, 255, 255, 0.7); outline-offset: 1px; }
31924
+ .lens-btn--primary { background: #fff; color: #18181b; }
31925
+ .lens-btn--primary:hover { background: #e4e4e7; }
31926
+ .lens-btn--report svg { flex: none; }
31927
+ .lens-btn__label { white-space: nowrap; }
31754
31928
  .lens-btn--icon { width: 30px; padding: 0; justify-content: center; }
31755
- .lens-btn--icon.is-active { background: var(--lens-accent); color: #fff; }
31929
+ /* Always-dark pill: an active control reads as a light (white) highlight. */
31930
+ .lens-btn--icon.is-active { background: #fff; color: #18181b; }
31756
31931
  .lens-pill__sep {
31757
31932
  width: 1px;
31758
31933
  height: 18px;
@@ -31766,7 +31941,10 @@ var OVERLAY_CSS = (
31766
31941
  z-index: calc(var(--lens-z) - 2);
31767
31942
  pointer-events: none;
31768
31943
  border: 2px solid var(--lens-accent);
31769
- background: rgba(99, 102, 241, 0.14);
31944
+ /* A light outer ring so the (black-in-light / white-in-dark) border stays
31945
+ visible over any page background. */
31946
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.6), 0 0 0 3px rgba(0, 0, 0, 0.15);
31947
+ background: rgba(127, 127, 127, 0.14);
31770
31948
  border-radius: 3px;
31771
31949
  transition: all 60ms linear;
31772
31950
  display: none;
@@ -31778,7 +31956,7 @@ var OVERLAY_CSS = (
31778
31956
  padding: 1px 6px;
31779
31957
  border-radius: 4px;
31780
31958
  background: var(--lens-accent);
31781
- color: #fff;
31959
+ color: var(--lens-on-accent);
31782
31960
  font: 500 11px/1.5 var(--lens-font);
31783
31961
  white-space: nowrap;
31784
31962
  }
@@ -31857,7 +32035,7 @@ var OVERLAY_CSS = (
31857
32035
  padding: 14px 16px;
31858
32036
  border-bottom: 1px solid rgba(0, 0, 0, 0.07);
31859
32037
  }
31860
- .lens-panel__title { font-size: 14px; font-weight: 600; }
32038
+ .lens-panel__title { font-size: 14px; font-weight: 600; margin-right: auto; }
31861
32039
  .lens-panel__x {
31862
32040
  display: inline-flex;
31863
32041
  align-items: center;
@@ -31895,7 +32073,7 @@ var OVERLAY_CSS = (
31895
32073
  .lens-textarea:focus {
31896
32074
  outline: none;
31897
32075
  border-color: var(--lens-accent);
31898
- box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.18);
32076
+ box-shadow: 0 0 0 3px var(--lens-focus);
31899
32077
  }
31900
32078
  /* ---- Context row: static, non-interactive page facts (muted, dot-joined) ---- */
31901
32079
  .lens-context {
@@ -31910,49 +32088,6 @@ var OVERLAY_CSS = (
31910
32088
  .lens-context__item { white-space: nowrap; }
31911
32089
  .lens-context__dot { opacity: 0.55; }
31912
32090
 
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
32091
  .lens-chips { display: flex; flex-wrap: wrap; gap: 6px; }
31957
32092
  .lens-chip {
31958
32093
  display: inline-flex;
@@ -31973,9 +32108,9 @@ var OVERLAY_CSS = (
31973
32108
  .lens-chip--ok { color: #15803d; background: #f0fdf4; border-color: rgba(21, 128, 61, 0.18); }
31974
32109
  .lens-chip--target {
31975
32110
  max-width: 100%;
31976
- background: #eef2ff;
31977
- border-color: rgba(99, 102, 241, 0.25);
31978
- color: var(--lens-accent-strong);
32111
+ background: rgba(0, 0, 0, 0.05);
32112
+ border-color: rgba(0, 0, 0, 0.16);
32113
+ color: #18181b;
31979
32114
  }
31980
32115
  .lens-chip__code {
31981
32116
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
@@ -32023,7 +32158,7 @@ var OVERLAY_CSS = (
32023
32158
  .lens-abtn svg { flex: none; }
32024
32159
  .lens-abtn--ghost { background: transparent; border-color: rgba(0, 0, 0, 0.14); color: #3f3f46; }
32025
32160
  .lens-abtn--ghost:hover { background: rgba(0, 0, 0, 0.04); }
32026
- .lens-abtn--primary { background: var(--lens-accent); color: #fff; }
32161
+ .lens-abtn--primary { background: var(--lens-accent); color: var(--lens-on-accent); }
32027
32162
  .lens-abtn--primary:hover { background: var(--lens-accent-strong); }
32028
32163
  .lens-abtn:disabled { opacity: 0.55; cursor: default; }
32029
32164
  .lens-abtn:focus-visible { outline: 2px solid var(--lens-accent); outline-offset: 1px; }
@@ -32077,12 +32212,9 @@ var OVERLAY_CSS = (
32077
32212
  .lens-panel__x:hover { background: rgba(255, 255, 255, 0.08); }
32078
32213
  .lens-textarea { background: #26262a; border-color: rgba(255, 255, 255, 0.14); color: #f4f4f5; }
32079
32214
  .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
32215
  .lens-chip { background: #26262a; border-color: rgba(255, 255, 255, 0.08); color: #d4d4d8; }
32084
32216
  .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; }
32217
+ .lens-chip--target { background: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.18); color: #f4f4f5; }
32086
32218
  .lens-abtn--ghost { border-color: rgba(255, 255, 255, 0.16); color: #d4d4d8; }
32087
32219
  .lens-abtn--ghost:hover { background: rgba(255, 255, 255, 0.06); }
32088
32220
  .lens-state__msg { color: #a1a1aa; }
@@ -32116,7 +32248,9 @@ var OVERLAY_CSS = (
32116
32248
  .lens-annotate-chips { z-index: var(--lens-z); }
32117
32249
 
32118
32250
  /* ---------------- Annotation toolbar ---------------- */
32119
- /* Same floating top-center anchor as the chips row; wraps on narrow screens. */
32251
+ /* Floating top-center anchor. A fixed drag grip sits left of a tools "track":
32252
+ the track wraps on desktop and becomes a single horizontal-scroll row on
32253
+ narrow screens, so the bar stays one slim height instead of a bulky block. */
32120
32254
  .lens-annotate-toolbar {
32121
32255
  position: fixed;
32122
32256
  top: calc(16px + env(safe-area-inset-top));
@@ -32124,11 +32258,10 @@ var OVERLAY_CSS = (
32124
32258
  transform: translateX(-50%);
32125
32259
  z-index: var(--lens-z);
32126
32260
  display: inline-flex;
32127
- flex-wrap: wrap;
32261
+ flex-wrap: nowrap;
32128
32262
  align-items: center;
32129
- justify-content: center;
32130
32263
  gap: 6px;
32131
- max-width: calc(100vw - 24px);
32264
+ max-width: calc(100vw - 16px);
32132
32265
  padding: 6px 8px;
32133
32266
  border-radius: 14px;
32134
32267
  background: rgba(20, 20, 23, 0.92);
@@ -32138,12 +32271,50 @@ var OVERLAY_CSS = (
32138
32271
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.35);
32139
32272
  color: #f4f4f5;
32140
32273
  }
32274
+ /* Tools lay out as ONE horizontal row that scrolls if it can't fit, so the bar
32275
+ is always a single slim height \u2014 never a wrapping (or, worse, min-content
32276
+ collapsed vertical) block. The grip sits outside this track. */
32277
+ .lens-tool-track {
32278
+ display: flex;
32279
+ align-items: center;
32280
+ flex-wrap: nowrap;
32281
+ justify-content: flex-start;
32282
+ gap: 6px;
32283
+ flex: 1 1 auto;
32284
+ min-width: 0;
32285
+ overflow-x: auto;
32286
+ overflow-y: hidden;
32287
+ scrollbar-width: none;
32288
+ -webkit-overflow-scrolling: touch;
32289
+ }
32290
+ .lens-tool-track::-webkit-scrollbar { display: none; }
32141
32291
  .lens-tool-group {
32142
32292
  display: inline-flex;
32143
32293
  align-items: center;
32144
32294
  gap: 4px;
32145
- flex-wrap: wrap;
32295
+ flex-wrap: nowrap;
32296
+ flex: none;
32146
32297
  }
32298
+ /* Single close (=Done, keeps marks) pinned at the right, always reachable
32299
+ while the tools scroll. Reads as a light control on the dark bar. */
32300
+ .lens-tool-close {
32301
+ display: inline-flex;
32302
+ align-items: center;
32303
+ justify-content: center;
32304
+ flex: none;
32305
+ width: 32px;
32306
+ height: 30px;
32307
+ padding: 0;
32308
+ border: 0;
32309
+ border-radius: 8px;
32310
+ background: rgba(255, 255, 255, 0.14);
32311
+ color: #f4f4f5;
32312
+ cursor: pointer;
32313
+ transition: background 150ms ease;
32314
+ }
32315
+ .lens-tool-close:hover { background: rgba(255, 255, 255, 0.26); }
32316
+ .lens-tool-close:focus-visible { outline: 2px solid rgba(255, 255, 255, 0.7); outline-offset: 1px; }
32317
+ .lens-tool-close svg { width: 18px; height: 18px; }
32147
32318
  .lens-tool-sep {
32148
32319
  width: 1px;
32149
32320
  align-self: stretch;
@@ -32151,6 +32322,51 @@ var OVERLAY_CSS = (
32151
32322
  background: rgba(255, 255, 255, 0.16);
32152
32323
  margin: 0 2px;
32153
32324
  }
32325
+
32326
+ /* ---------------- Draggable surfaces ---------------- */
32327
+ /* Every drag handle: no page pan/scroll gesture starts here, and the cursor
32328
+ signals grabbability. Applied to the pill, panel header, toolbar grip, chips. */
32329
+ .lens-drag-handle { touch-action: none; cursor: grab; }
32330
+ .lens-drag-handle:active { cursor: grabbing; }
32331
+ .is-dragging, .is-dragging * { cursor: grabbing !important; }
32332
+ .is-dragging { user-select: none; }
32333
+ /* While dragging, drop the entry animation/transition so it tracks 1:1. */
32334
+ .lens-pill.is-dragging, .lens-panel.is-dragging,
32335
+ .lens-annotate-toolbar.is-dragging, .lens-rec-chip.is-dragging {
32336
+ animation: none;
32337
+ transition: none;
32338
+ }
32339
+
32340
+ /* Toolbar drag grip \u2014 the only grab region, kept out of the scroll track. */
32341
+ .lens-tool-grip {
32342
+ display: inline-flex;
32343
+ align-items: center;
32344
+ justify-content: center;
32345
+ flex: none;
32346
+ width: 22px;
32347
+ height: 30px;
32348
+ margin-right: 2px;
32349
+ border-radius: 6px;
32350
+ color: #a1a1aa;
32351
+ }
32352
+ .lens-tool-grip:hover { color: #f4f4f5; background: rgba(255, 255, 255, 0.08); }
32353
+ .lens-tool-grip svg { width: 16px; height: 16px; }
32354
+
32355
+ /* Pill grip (the eye dot) + panel header grip. */
32356
+ .lens-pill__grip { cursor: grab; }
32357
+ .lens-panel__grip {
32358
+ display: inline-flex;
32359
+ align-items: center;
32360
+ justify-content: center;
32361
+ flex: none;
32362
+ width: 22px;
32363
+ height: 22px;
32364
+ margin-right: 6px;
32365
+ color: #a1a1aa;
32366
+ }
32367
+ .lens-panel__grip svg { width: 16px; height: 16px; }
32368
+ .lens-panel__head { cursor: grab; }
32369
+ .lens-panel__head:active { cursor: grabbing; }
32154
32370
  .lens-tool-btn {
32155
32371
  display: inline-flex;
32156
32372
  align-items: center;
@@ -32166,11 +32382,11 @@ var OVERLAY_CSS = (
32166
32382
  transition: background 150ms ease, color 150ms ease, border-color 150ms ease;
32167
32383
  }
32168
32384
  .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; }
32385
+ .lens-tool-btn:focus-visible { outline: 2px solid rgba(255, 255, 255, 0.7); outline-offset: 1px; }
32170
32386
  .lens-tool-btn.is-active {
32171
- background: var(--lens-accent);
32172
- border-color: var(--lens-accent);
32173
- color: #fff;
32387
+ background: #fff;
32388
+ border-color: #fff;
32389
+ color: #18181b;
32174
32390
  }
32175
32391
  .lens-tool-btn:disabled { opacity: 0.4; cursor: default; }
32176
32392
  .lens-tool-btn:disabled:hover { background: transparent; color: #d4d4d8; }
@@ -32210,17 +32426,18 @@ var OVERLAY_CSS = (
32210
32426
  transition: background 150ms ease, color 150ms ease, border-color 150ms ease;
32211
32427
  }
32212
32428
  .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; }
32429
+ .lens-width-btn:focus-visible { outline: 2px solid rgba(255, 255, 255, 0.7); outline-offset: 1px; }
32214
32430
  .lens-width-btn.is-active {
32215
- background: var(--lens-accent);
32216
- border-color: var(--lens-accent);
32217
- color: #fff;
32431
+ background: #fff;
32432
+ border-color: #fff;
32433
+ color: #18181b;
32218
32434
  }
32219
32435
 
32220
32436
  /* Coarse pointers: bump the tool targets toward 40px. */
32221
32437
  @media (pointer: coarse) {
32222
32438
  .lens-tool-btn, .lens-width-btn { width: 38px; height: 38px; }
32223
32439
  .lens-swatch { width: 30px; height: 30px; }
32440
+ .lens-tool-grip { width: 30px; height: 38px; }
32224
32441
  }
32225
32442
 
32226
32443
  /* Stacking inside the shadow layer: the panel, pill and floating chips always
@@ -32245,8 +32462,8 @@ var OVERLAY_CSS = (
32245
32462
  transition: background 150ms ease;
32246
32463
  }
32247
32464
  .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); }
32465
+ .lens-pick-chip__btn--primary { background: #fff; color: #18181b; }
32466
+ .lens-pick-chip__btn--primary:hover { background: #e4e4e7; }
32250
32467
 
32251
32468
  /* ---------------- Recording chip ---------------- */
32252
32469
  .lens-rec-chip {
@@ -32301,21 +32518,29 @@ var OVERLAY_CSS = (
32301
32518
  .lens-rec-chip__stop:hover { background: #dc2626; }
32302
32519
  .lens-chip--rec { gap: 6px; }
32303
32520
 
32521
+ /* Very narrow screens: collapse the pill's "Report issue" label to its icon so
32522
+ the pill stays a single slim row (never wraps into a 2-row blob). */
32523
+ @media (max-width: 400px) {
32524
+ .lens-btn__label { display: none; }
32525
+ .lens-btn--report { width: 44px; padding: 0; justify-content: center; }
32526
+ }
32527
+
32304
32528
  /* ---------------- Coarse pointers (touch): \u226544px targets ---------------- */
32305
32529
  @media (pointer: coarse) {
32306
32530
  .lens-btn { height: 44px; padding: 0 16px; }
32307
32531
  .lens-btn--icon { width: 44px; }
32532
+ .lens-tool-close { width: 44px; height: 38px; }
32308
32533
  .lens-pill__dot { width: 40px; height: 40px; }
32309
32534
  .lens-abtn { height: 44px; }
32310
- .lens-tgl { min-height: 40px; padding: 4px 14px; }
32311
32535
  .lens-panel__x { width: 44px; height: 44px; }
32312
32536
  .lens-chip__x { min-width: 32px; min-height: 32px; align-items: center; justify-content: center; }
32313
32537
  .lens-pick-chip__btn, .lens-rec-chip__stop { height: 40px; padding: 0 18px; }
32314
32538
  }
32315
32539
 
32316
- /* ---------------- Bottom sheet under 640px ---------------- */
32540
+ /* ---------------- Narrow screens (\u2264640px) ---------------- */
32317
32541
  @media (max-width: 640px) {
32318
- .lens-panel {
32542
+ /* Report panel as a bottom sheet (unless the user has dragged it loose). */
32543
+ .lens-panel:not(.is-dragging) {
32319
32544
  left: 0;
32320
32545
  right: 0;
32321
32546
  bottom: 0;
@@ -32332,6 +32557,14 @@ var OVERLAY_CSS = (
32332
32557
  to { opacity: 1; transform: none; }
32333
32558
  }
32334
32559
  .lens-textarea { font-size: 16px; }
32560
+
32561
+ /* On narrow screens the single-row tool track usually overflows: fade its
32562
+ trailing edge so it reads as "more tools this way" (the grip sits outside
32563
+ the track and stays fully opaque). */
32564
+ .lens-tool-track {
32565
+ -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 20px), transparent 100%);
32566
+ mask-image: linear-gradient(90deg, #000 calc(100% - 20px), transparent 100%);
32567
+ }
32335
32568
  }
32336
32569
  `
32337
32570
  );
@@ -32349,8 +32582,14 @@ var Overlay = class {
32349
32582
  this.shadow.append(this.layer);
32350
32583
  this.picker = new ElementPicker(this.layer, this.host);
32351
32584
  this.annotator = new AnnotationEditor(this.layer);
32352
- this.layer.append(this.buildPill());
32585
+ const pill = this.buildPill();
32586
+ this.layer.append(pill);
32353
32587
  document.documentElement.append(this.host);
32588
+ this.pillDrag = makeDraggable(pill, {
32589
+ handle: pill,
32590
+ key: "lens-pill",
32591
+ ignore: isInteractiveTarget
32592
+ });
32354
32593
  window.addEventListener("keydown", this.onGlobalKey, true);
32355
32594
  }
32356
32595
  deps;
@@ -32363,7 +32602,9 @@ var Overlay = class {
32363
32602
  panel = null;
32364
32603
  target = null;
32365
32604
  destroyed = false;
32605
+ pillDrag = null;
32366
32606
  recordingChip = null;
32607
+ recordingDrag = null;
32367
32608
  recordingTimerEl = null;
32368
32609
  recordingInterval = null;
32369
32610
  recordingStartTs = 0;
@@ -32378,10 +32619,16 @@ var Overlay = class {
32378
32619
  on: { click: () => this.togglePicker() }
32379
32620
  });
32380
32621
  return el("div", { class: "lens-pill", attrs: { role: "toolbar", "aria-label": "Lens" } }, [
32381
- el("span", { class: "lens-pill__dot", html: iconEye }),
32622
+ el("span", {
32623
+ class: "lens-pill__dot lens-pill__grip",
32624
+ html: iconEye,
32625
+ title: "Drag to move",
32626
+ ariaLabel: "Drag to move Lens"
32627
+ }),
32382
32628
  el("button", {
32383
- class: "lens-btn",
32384
- text: "Report issue",
32629
+ class: "lens-btn lens-btn--report",
32630
+ html: iconReport + '<span class="lens-btn__label">Report issue</span>',
32631
+ ariaLabel: "Report issue",
32385
32632
  on: { click: () => this.open() }
32386
32633
  }),
32387
32634
  this.pickBtn,
@@ -32435,6 +32682,7 @@ var Overlay = class {
32435
32682
  this.stopPicker();
32436
32683
  return;
32437
32684
  }
32685
+ if (this.annotator.isActive) this.annotator.exit();
32438
32686
  this.close();
32439
32687
  this.pickBtn.classList.add("is-active");
32440
32688
  this.picker.start(
@@ -32496,6 +32744,11 @@ var Overlay = class {
32496
32744
  })
32497
32745
  ]);
32498
32746
  this.layer.append(this.recordingChip);
32747
+ this.recordingDrag = makeDraggable(this.recordingChip, {
32748
+ handle: this.recordingChip,
32749
+ key: "lens-rec-chip",
32750
+ ignore: isInteractiveTarget
32751
+ });
32499
32752
  this.recordingInterval = setInterval(() => this.tickRecording(), 250);
32500
32753
  }
32501
32754
  tickRecording() {
@@ -32508,6 +32761,10 @@ var Overlay = class {
32508
32761
  clearInterval(this.recordingInterval);
32509
32762
  this.recordingInterval = null;
32510
32763
  }
32764
+ if (this.recordingDrag) {
32765
+ this.recordingDrag.destroy();
32766
+ this.recordingDrag = null;
32767
+ }
32511
32768
  if (this.recordingChip) {
32512
32769
  this.recordingChip.remove();
32513
32770
  this.recordingChip = null;
@@ -32548,6 +32805,8 @@ var Overlay = class {
32548
32805
  if (this.destroyed) return;
32549
32806
  this.destroyed = true;
32550
32807
  window.removeEventListener("keydown", this.onGlobalKey, true);
32808
+ this.pillDrag?.destroy();
32809
+ this.pillDrag = null;
32551
32810
  this.stopPicker();
32552
32811
  if (this.deps.client.isRecording()) this.deps.client.stopRecording();
32553
32812
  this.hideRecordingChip();