@merittdev/horus-lens 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/browser.cjs CHANGED
@@ -28892,15 +28892,86 @@ var TargetElementSchema = external_exports.object({
28892
28892
  boundingRect: external_exports.object({ x: external_exports.number(), y: external_exports.number(), width: external_exports.number(), height: external_exports.number() }).optional(),
28893
28893
  reactComponents: external_exports.array(external_exports.string()).optional()
28894
28894
  });
28895
- var AnnotationSchema = external_exports.object({
28896
- kind: external_exports.enum(["rect", "ellipse", "arrow"]),
28897
- x: external_exports.number(),
28898
- y: external_exports.number(),
28899
- width: external_exports.number(),
28900
- height: external_exports.number(),
28901
- /** Page coordinates relative to the viewport at capture time. */
28902
- viewport: external_exports.object({ width: external_exports.number(), height: external_exports.number() })
28903
- });
28895
+ var annotationCommon = {
28896
+ id: external_exports.string().optional(),
28897
+ /** Any CSS color string. */
28898
+ color: external_exports.string().optional(),
28899
+ strokeWidth: external_exports.number().optional()
28900
+ };
28901
+ var AnnotationPointSchema = external_exports.object({ x: external_exports.number(), y: external_exports.number() });
28902
+ var AnnotationSchema = external_exports.discriminatedUnion("kind", [
28903
+ external_exports.object({
28904
+ ...annotationCommon,
28905
+ kind: external_exports.literal("rect"),
28906
+ x: external_exports.number(),
28907
+ y: external_exports.number(),
28908
+ width: external_exports.number(),
28909
+ height: external_exports.number(),
28910
+ /**
28911
+ * OPTIONAL, kept for back-compat with 0.1.x clients that emit
28912
+ * viewport-anchored rects. New clients emit document-pixel rects with no
28913
+ * viewport.
28914
+ */
28915
+ viewport: external_exports.object({ width: external_exports.number(), height: external_exports.number() }).optional()
28916
+ }),
28917
+ external_exports.object({
28918
+ ...annotationCommon,
28919
+ kind: external_exports.literal("ellipse"),
28920
+ x: external_exports.number(),
28921
+ y: external_exports.number(),
28922
+ width: external_exports.number(),
28923
+ height: external_exports.number()
28924
+ }),
28925
+ external_exports.object({
28926
+ ...annotationCommon,
28927
+ kind: external_exports.literal("blur"),
28928
+ x: external_exports.number(),
28929
+ y: external_exports.number(),
28930
+ width: external_exports.number(),
28931
+ height: external_exports.number()
28932
+ }),
28933
+ external_exports.object({
28934
+ ...annotationCommon,
28935
+ kind: external_exports.literal("arrow"),
28936
+ x1: external_exports.number(),
28937
+ y1: external_exports.number(),
28938
+ x2: external_exports.number(),
28939
+ y2: external_exports.number()
28940
+ }),
28941
+ external_exports.object({
28942
+ ...annotationCommon,
28943
+ kind: external_exports.literal("line"),
28944
+ x1: external_exports.number(),
28945
+ y1: external_exports.number(),
28946
+ x2: external_exports.number(),
28947
+ y2: external_exports.number()
28948
+ }),
28949
+ external_exports.object({
28950
+ ...annotationCommon,
28951
+ kind: external_exports.literal("pencil"),
28952
+ points: external_exports.array(AnnotationPointSchema).min(2)
28953
+ }),
28954
+ external_exports.object({
28955
+ ...annotationCommon,
28956
+ kind: external_exports.literal("highlighter"),
28957
+ points: external_exports.array(AnnotationPointSchema).min(2)
28958
+ }),
28959
+ external_exports.object({
28960
+ ...annotationCommon,
28961
+ kind: external_exports.literal("text"),
28962
+ x: external_exports.number(),
28963
+ y: external_exports.number(),
28964
+ text: external_exports.string(),
28965
+ fontSize: external_exports.number().optional()
28966
+ }),
28967
+ external_exports.object({
28968
+ ...annotationCommon,
28969
+ kind: external_exports.literal("step"),
28970
+ x: external_exports.number(),
28971
+ y: external_exports.number(),
28972
+ n: external_exports.number()
28973
+ })
28974
+ ]);
28904
28975
  var AssetKindSchema = external_exports.enum(["replay", "console", "network", "dom", "screenshot", "attachment"]);
28905
28976
  var AssetRefSchema = external_exports.object({
28906
28977
  kind: AssetKindSchema,
@@ -29353,6 +29424,11 @@ var ReplayRecorder = class {
29353
29424
  emit: (event, isCheckout) => this.onEmit(event, isCheckout),
29354
29425
  checkoutEveryNms: this.windowMs,
29355
29426
  recordCanvas: false,
29427
+ // Inline same-origin + CORS images as data URIs at snapshot time so the
29428
+ // dashboard replay paints on a different origin (tainted cross-origin
29429
+ // images fall back to their URL, which is fine for public CDNs).
29430
+ inlineImages: true,
29431
+ collectFonts: true,
29356
29432
  // Keep the Lens overlay itself out of every recording.
29357
29433
  blockSelector: `#${LENS_OVERLAY_HOST_ID}`
29358
29434
  });
@@ -29459,7 +29535,20 @@ var ReplayRecorder = class {
29459
29535
  console.warn("[lens] rrweb recording unavailable; replay disabled", err);
29460
29536
  }
29461
29537
  };
29462
- async function captureScreenshot(excludeHost) {
29538
+ var SVG_RASTERIZE_TIMEOUT_MS = 3e3;
29539
+ function computeCropRect(crop, scale, rasterWidth, rasterHeight) {
29540
+ let sx = Math.round(crop.x * scale);
29541
+ let sy = Math.round(crop.y * scale);
29542
+ let sw = Math.round(crop.width * scale);
29543
+ let sh = Math.round(crop.height * scale);
29544
+ sx = Math.max(0, Math.min(sx, rasterWidth));
29545
+ sy = Math.max(0, Math.min(sy, rasterHeight));
29546
+ sw = Math.min(sw, rasterWidth - sx);
29547
+ sh = Math.min(sh, rasterHeight - sy);
29548
+ if (sw <= 0 || sh <= 0) return null;
29549
+ return { sx, sy, sw, sh };
29550
+ }
29551
+ async function captureScreenshot(excludeHost, crop, composite) {
29463
29552
  if (typeof document === "undefined" || typeof window === "undefined") return null;
29464
29553
  try {
29465
29554
  const viewportPixels = window.innerWidth * window.innerHeight || 1;
@@ -29469,6 +29558,15 @@ async function captureScreenshot(excludeHost) {
29469
29558
  scale,
29470
29559
  type: "image/jpeg",
29471
29560
  quality: 0.82,
29561
+ // JPEG has no alpha — transparent page regions (and any capture box
29562
+ // extending past short content) would otherwise encode as BLACK.
29563
+ backgroundColor: "#ffffff",
29564
+ // When compositing annotations, force the capture box to the editor's
29565
+ // document measurement (scrollHeight-based). modern-screenshot otherwise
29566
+ // sizes from documentElement's bounding box, which on pages SHORTER than
29567
+ // the viewport is the content height only — marks drawn in the lower
29568
+ // viewport band would fall off the raster and the crop would clamp short.
29569
+ ...composite ? { width: composite.docWidth, height: composite.docHeight } : {},
29472
29570
  // Exclude the overlay from the capture CLONE — never hide the live node.
29473
29571
  filter: (node2) => node2 !== excludeHost,
29474
29572
  // Per-resource fetch budget: a single slow cross-origin image must not
@@ -29493,12 +29591,171 @@ async function captureScreenshot(excludeHost) {
29493
29591
  );
29494
29592
  return null;
29495
29593
  }
29594
+ if (composite) {
29595
+ const baked = await compositeScreenshot(blob2, composite, scale, crop);
29596
+ if (baked) return baked;
29597
+ if (composite.blurRegions.length > 0) {
29598
+ console.warn(
29599
+ "[lens] screenshot compositing failed with active redactions; dropping screenshot to avoid leaking un-blurred content"
29600
+ );
29601
+ return null;
29602
+ }
29603
+ }
29604
+ if (crop) {
29605
+ const cropped = await cropBlob(blob2, crop, scale);
29606
+ return cropped ?? blob2;
29607
+ }
29496
29608
  return blob2;
29497
29609
  } catch (err) {
29498
29610
  console.warn("[lens] screenshot capture failed:", err);
29499
29611
  return null;
29500
29612
  }
29501
29613
  }
29614
+ async function compositeScreenshot(blob2, composite, scale, crop) {
29615
+ try {
29616
+ if (typeof createImageBitmap !== "function" || typeof document === "undefined") return null;
29617
+ const bitmap = await createImageBitmap(blob2);
29618
+ const canvas = document.createElement("canvas");
29619
+ canvas.width = bitmap.width;
29620
+ canvas.height = bitmap.height;
29621
+ const ctx = canvas.getContext("2d");
29622
+ if (!ctx) {
29623
+ bitmap.close?.();
29624
+ return null;
29625
+ }
29626
+ ctx.fillStyle = "#ffffff";
29627
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
29628
+ ctx.drawImage(bitmap, 0, 0);
29629
+ bitmap.close?.();
29630
+ applyBlurRegions(ctx, canvas, composite.blurRegions, scale);
29631
+ await drawSvgOverlay(ctx, composite, scale);
29632
+ if (crop) {
29633
+ const rect = computeCropRect(crop, scale, canvas.width, canvas.height);
29634
+ if (rect) {
29635
+ const out = document.createElement("canvas");
29636
+ out.width = rect.sw;
29637
+ out.height = rect.sh;
29638
+ const octx = out.getContext("2d");
29639
+ if (octx) {
29640
+ octx.drawImage(canvas, rect.sx, rect.sy, rect.sw, rect.sh, 0, 0, rect.sw, rect.sh);
29641
+ return await canvasToJpegBlob(out);
29642
+ }
29643
+ }
29644
+ }
29645
+ return await canvasToJpegBlob(canvas);
29646
+ } catch (err) {
29647
+ console.warn("[lens] screenshot compositing failed:", err);
29648
+ return null;
29649
+ }
29650
+ }
29651
+ function applyBlurRegions(ctx, canvas, regions, scale) {
29652
+ if (regions.length === 0) return;
29653
+ const filterSupported = typeof ctx.filter === "string";
29654
+ const blurPx = Math.max(8, 12 * scale);
29655
+ const pad = Math.ceil(blurPx);
29656
+ for (const region of regions) {
29657
+ const x = Math.max(0, Math.min(Math.round(region.x * scale), canvas.width));
29658
+ const y = Math.max(0, Math.min(Math.round(region.y * scale), canvas.height));
29659
+ const w = Math.min(Math.round(region.width * scale), canvas.width - x);
29660
+ const h = Math.min(Math.round(region.height * scale), canvas.height - y);
29661
+ if (w <= 0 || h <= 0) continue;
29662
+ if (!filterSupported) {
29663
+ ctx.fillStyle = "rgba(127,127,127,1)";
29664
+ ctx.fillRect(x, y, w, h);
29665
+ continue;
29666
+ }
29667
+ const px = Math.max(0, x - pad);
29668
+ const py = Math.max(0, y - pad);
29669
+ const pw = Math.min(canvas.width, x + w + pad) - px;
29670
+ const ph = Math.min(canvas.height, y + h + pad) - py;
29671
+ if (pw <= 0 || ph <= 0) continue;
29672
+ const temp = document.createElement("canvas");
29673
+ temp.width = pw;
29674
+ temp.height = ph;
29675
+ const tctx = temp.getContext("2d");
29676
+ if (!tctx) {
29677
+ ctx.fillStyle = "rgba(127,127,127,1)";
29678
+ ctx.fillRect(x, y, w, h);
29679
+ continue;
29680
+ }
29681
+ tctx.filter = `blur(${blurPx}px)`;
29682
+ tctx.drawImage(canvas, px, py, pw, ph, 0, 0, pw, ph);
29683
+ tctx.filter = "none";
29684
+ ctx.save();
29685
+ ctx.beginPath();
29686
+ ctx.rect(x, y, w, h);
29687
+ ctx.clip();
29688
+ ctx.drawImage(temp, px, py);
29689
+ ctx.restore();
29690
+ }
29691
+ }
29692
+ async function drawSvgOverlay(ctx, composite, scale) {
29693
+ const { svgMarkup, docWidth, docHeight } = composite;
29694
+ if (!svgMarkup) return;
29695
+ const img = await loadSvgImage(svgMarkup);
29696
+ if (!img) return;
29697
+ try {
29698
+ ctx.drawImage(img, 0, 0, docWidth * scale, docHeight * scale);
29699
+ } catch (err) {
29700
+ console.warn("[lens] annotation overlay draw failed:", err);
29701
+ }
29702
+ }
29703
+ function loadSvgImage(svg2) {
29704
+ return new Promise((resolve) => {
29705
+ if (typeof Image !== "function") {
29706
+ resolve(null);
29707
+ return;
29708
+ }
29709
+ let settled = false;
29710
+ const finish = (result2) => {
29711
+ if (settled) return;
29712
+ settled = true;
29713
+ clearTimeout(timer);
29714
+ resolve(result2);
29715
+ };
29716
+ const timer = setTimeout(() => {
29717
+ console.warn("[lens] annotation overlay rasterization timed out");
29718
+ finish(null);
29719
+ }, SVG_RASTERIZE_TIMEOUT_MS);
29720
+ const img = new Image();
29721
+ img.onload = () => finish(img);
29722
+ img.onerror = () => {
29723
+ console.warn("[lens] annotation overlay failed to rasterize");
29724
+ finish(null);
29725
+ };
29726
+ img.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg2);
29727
+ });
29728
+ }
29729
+ function canvasToJpegBlob(canvas) {
29730
+ return new Promise((resolve) => {
29731
+ canvas.toBlob((out) => resolve(out), "image/jpeg", 0.82);
29732
+ });
29733
+ }
29734
+ async function cropBlob(blob2, crop, scale) {
29735
+ try {
29736
+ if (typeof createImageBitmap !== "function" || typeof document === "undefined") return null;
29737
+ const bitmap = await createImageBitmap(blob2);
29738
+ const rect = computeCropRect(crop, scale, bitmap.width, bitmap.height);
29739
+ if (!rect) {
29740
+ bitmap.close?.();
29741
+ return null;
29742
+ }
29743
+ const canvas = document.createElement("canvas");
29744
+ canvas.width = rect.sw;
29745
+ canvas.height = rect.sh;
29746
+ const ctx = canvas.getContext("2d");
29747
+ if (!ctx) {
29748
+ bitmap.close?.();
29749
+ return null;
29750
+ }
29751
+ ctx.drawImage(bitmap, rect.sx, rect.sy, rect.sw, rect.sh, 0, 0, rect.sw, rect.sh);
29752
+ bitmap.close?.();
29753
+ return await canvasToJpegBlob(canvas);
29754
+ } catch (err) {
29755
+ console.warn("[lens] screenshot crop failed:", err);
29756
+ return null;
29757
+ }
29758
+ }
29502
29759
  function parseBrowser(ua) {
29503
29760
  if (!ua) return void 0;
29504
29761
  const checks = [
@@ -29763,7 +30020,11 @@ var LensClient = class {
29763
30020
  const wantErrors = include.errors !== false;
29764
30021
  const wantScreenshot = include.screenshot !== false;
29765
30022
  const wantDom = include.dom !== false;
29766
- const screenshotPromise = this.screenshotEnabled && wantScreenshot ? captureScreenshot(input2.excludeFromScreenshot) : Promise.resolve(null);
30023
+ const screenshotPromise = this.screenshotEnabled && wantScreenshot ? captureScreenshot(
30024
+ input2.excludeFromScreenshot,
30025
+ input2.screenshotCrop,
30026
+ input2.annotationComposite
30027
+ ) : Promise.resolve(null);
29767
30028
  const replayEvents = wantReplay ? this.replay?.snapshot() ?? [] : [];
29768
30029
  const errorEntries = this.errorBuffer.drain();
29769
30030
  const consoleTail = this.consoleBuffer.tail(CONSOLE_TAIL_SIZE);
@@ -29974,170 +30235,933 @@ function el(tag, props = {}, children = []) {
29974
30235
  function clear(node2) {
29975
30236
  while (node2.firstChild) node2.removeChild(node2.firstChild);
29976
30237
  }
29977
- var Annotator = class {
30238
+ var THIN_MIN_DIST = 3;
30239
+ var MAX_POINTS = 2e3;
30240
+ var HIGHLIGHTER_WIDTH = 14;
30241
+ var STEP_RADIUS = 12;
30242
+ function distance(a, b) {
30243
+ return Math.hypot(a.x - b.x, a.y - b.y);
30244
+ }
30245
+ function shouldKeepPoint(last, candidate, minDist = THIN_MIN_DIST) {
30246
+ return distance(last, candidate) >= minDist;
30247
+ }
30248
+ function perpendicularDistance(p, a, b) {
30249
+ const dx = b.x - a.x;
30250
+ const dy = b.y - a.y;
30251
+ const len2 = dx * dx + dy * dy;
30252
+ if (len2 === 0) return distance(p, a);
30253
+ let t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2;
30254
+ t = Math.max(0, Math.min(1, t));
30255
+ return distance(p, { x: a.x + t * dx, y: a.y + t * dy });
30256
+ }
30257
+ function simplifyPoints(points, tolerance = 1) {
30258
+ if (points.length <= 2 || tolerance <= 0) return points.map((p) => ({ ...p }));
30259
+ const first = points[0];
30260
+ const last = points[points.length - 1];
30261
+ let maxDist = 0;
30262
+ let index2 = 0;
30263
+ for (let i = 1; i < points.length - 1; i++) {
30264
+ const d = perpendicularDistance(points[i], first, last);
30265
+ if (d > maxDist) {
30266
+ maxDist = d;
30267
+ index2 = i;
30268
+ }
30269
+ }
30270
+ if (maxDist > tolerance) {
30271
+ const left = simplifyPoints(points.slice(0, index2 + 1), tolerance);
30272
+ const right = simplifyPoints(points.slice(index2), tolerance);
30273
+ return left.slice(0, -1).concat(right);
30274
+ }
30275
+ return [{ ...first }, { ...last }];
30276
+ }
30277
+ function arrowheadPolygon(x1, y1, x2, y2, size = 14) {
30278
+ const angle = Math.atan2(y2 - y1, x2 - x1);
30279
+ const spread = Math.PI / 7;
30280
+ return [
30281
+ { x: x2, y: y2 },
30282
+ { x: x2 - size * Math.cos(angle - spread), y: y2 - size * Math.sin(angle - spread) },
30283
+ { x: x2 - size * Math.cos(angle + spread), y: y2 - size * Math.sin(angle + spread) }
30284
+ ];
30285
+ }
30286
+ function boundsOfAnnotation(a) {
30287
+ switch (a.kind) {
30288
+ case "rect":
30289
+ case "ellipse":
30290
+ case "blur": {
30291
+ const x = Math.min(a.x, a.x + a.width);
30292
+ const y = Math.min(a.y, a.y + a.height);
30293
+ return { x, y, width: Math.abs(a.width), height: Math.abs(a.height) };
30294
+ }
30295
+ case "arrow":
30296
+ case "line": {
30297
+ const x = Math.min(a.x1, a.x2);
30298
+ const y = Math.min(a.y1, a.y2);
30299
+ return { x, y, width: Math.abs(a.x2 - a.x1), height: Math.abs(a.y2 - a.y1) };
30300
+ }
30301
+ case "pencil":
30302
+ case "highlighter": {
30303
+ if (a.points.length === 0) return null;
30304
+ let minX = Infinity;
30305
+ let minY = Infinity;
30306
+ let maxX = -Infinity;
30307
+ let maxY = -Infinity;
30308
+ for (const p of a.points) {
30309
+ minX = Math.min(minX, p.x);
30310
+ minY = Math.min(minY, p.y);
30311
+ maxX = Math.max(maxX, p.x);
30312
+ maxY = Math.max(maxY, p.y);
30313
+ }
30314
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
30315
+ }
30316
+ case "text": {
30317
+ const fontSize = a.fontSize ?? 16;
30318
+ const width = Math.max(1, a.text.length * fontSize * 0.6);
30319
+ return { x: a.x, y: a.y, width, height: fontSize * 1.2 };
30320
+ }
30321
+ case "step": {
30322
+ return {
30323
+ x: a.x - STEP_RADIUS,
30324
+ y: a.y - STEP_RADIUS,
30325
+ width: STEP_RADIUS * 2,
30326
+ height: STEP_RADIUS * 2
30327
+ };
30328
+ }
30329
+ }
30330
+ }
30331
+ function unionBounds(list) {
30332
+ if (list.length === 0) return null;
30333
+ let minX = Infinity;
30334
+ let minY = Infinity;
30335
+ let maxX = -Infinity;
30336
+ let maxY = -Infinity;
30337
+ for (const b of list) {
30338
+ minX = Math.min(minX, b.x);
30339
+ minY = Math.min(minY, b.y);
30340
+ maxX = Math.max(maxX, b.x + b.width);
30341
+ maxY = Math.max(maxY, b.y + b.height);
30342
+ }
30343
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
30344
+ }
30345
+ function padAndClampBounds(b, padding, maxWidth, maxHeight) {
30346
+ const x = Math.max(0, b.x - padding);
30347
+ const y = Math.max(0, b.y - padding);
30348
+ const right = Math.min(maxWidth, b.x + b.width + padding);
30349
+ const bottom = Math.min(maxHeight, b.y + b.height + padding);
30350
+ return { x, y, width: Math.max(0, right - x), height: Math.max(0, bottom - y) };
30351
+ }
30352
+ var svg = (inner) => `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${inner}</svg>`;
30353
+ var iconEye = svg(
30354
+ '<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>'
30355
+ );
30356
+ var iconCrosshair = svg(
30357
+ '<circle cx="12" cy="12" r="8"/><line x1="12" y1="2" x2="12" y2="6"/><line x1="12" y1="18" x2="12" y2="22"/><line x1="2" y1="12" x2="6" y2="12"/><line x1="18" y1="12" x2="22" y2="12"/>'
30358
+ );
30359
+ var iconClose = svg('<line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/>');
30360
+ var iconMark = svg(
30361
+ '<rect x="3" y="3" width="18" height="18" rx="2" stroke-dasharray="4 3"/>'
30362
+ );
30363
+ var iconCopy = svg(
30364
+ '<rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/>'
30365
+ );
30366
+ var iconCheck = svg('<polyline points="20 6 9 17 4 12"/>');
30367
+ var iconSend = svg('<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>');
30368
+ var iconRecord = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="12" cy="12" r="6" fill="#ef4444"/></svg>`;
30369
+ var iconStop = `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>`;
30370
+ var iconRectTool = svg('<rect x="4" y="6" width="16" height="12" rx="1"/>');
30371
+ var iconEllipseTool = svg('<ellipse cx="12" cy="12" rx="8" ry="6"/>');
30372
+ var iconArrowTool = svg('<line x1="5" y1="19" x2="18" y2="6"/><polyline points="18 12 18 6 12 6"/>');
30373
+ var iconLineTool = svg('<line x1="5" y1="19" x2="19" y2="5"/>');
30374
+ var iconPencil = svg('<path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/>');
30375
+ var iconHighlighter = svg(
30376
+ '<path d="m9 11-6 6v3h9l3-3"/><path d="m22 12-4.6 4.6a1 1 0 0 1-1.4 0l-5.6-5.6a1 1 0 0 1 0-1.4L14 4"/>'
30377
+ );
30378
+ var iconText = svg(
30379
+ '<polyline points="4 7 4 4 20 4 20 7"/><line x1="12" y1="4" x2="12" y2="20"/><line x1="9" y1="20" x2="15" y2="20"/>'
30380
+ );
30381
+ var iconStepTool = svg('<circle cx="12" cy="12" r="9"/><path d="M11.2 9 13 8v8"/>');
30382
+ var iconBlur = svg('<path d="M12 3s6 6.4 6 10a6 6 0 0 1-12 0c0-3.6 6-10 6-10Z"/>');
30383
+ var iconUndo = svg('<path d="M9 14 4 9l5-5"/><path d="M4 9h11a5 5 0 0 1 0 10h-1"/>');
30384
+ var iconTrash = svg(
30385
+ '<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"/>'
30386
+ );
30387
+ var iconAnnotate = iconPencil;
30388
+ var SVG_NS = "http://www.w3.org/2000/svg";
30389
+ var FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
30390
+ var MIN_DRAG = 6;
30391
+ var TOOLS = [
30392
+ { tool: "rect", icon: iconRectTool, label: "Rectangle" },
30393
+ { tool: "ellipse", icon: iconEllipseTool, label: "Ellipse" },
30394
+ { tool: "arrow", icon: iconArrowTool, label: "Arrow" },
30395
+ { tool: "line", icon: iconLineTool, label: "Line" },
30396
+ { tool: "pencil", icon: iconPencil, label: "Pencil" },
30397
+ { tool: "highlighter", icon: iconHighlighter, label: "Highlighter" },
30398
+ { tool: "text", icon: iconText, label: "Text" },
30399
+ { tool: "step", icon: iconStepTool, label: "Step number" },
30400
+ { tool: "blur", icon: iconBlur, label: "Blur / redact" }
30401
+ ];
30402
+ var COLORS = ["#ef4444", "#f59e0b", "#22c55e", "#3b82f6", "#8b5cf6", "#111827"];
30403
+ var WIDTHS = [
30404
+ { label: "S", value: 2 },
30405
+ { label: "M", value: 3.5 },
30406
+ { label: "L", value: 6 }
30407
+ ];
30408
+ var AnnotationEditor = class {
29978
30409
  constructor(host2) {
29979
30410
  this.host = host2;
29980
30411
  }
29981
30412
  host;
29982
- layer = null;
29983
- chips = null;
29984
- draft = null;
29985
- startX = 0;
29986
- startY = 0;
29987
- active = false;
29988
- dragging = false;
30413
+ svg = null;
30414
+ bg = null;
30415
+ toolbar = null;
30416
+ textInputEl = null;
30417
+ textAnchor = null;
30418
+ textColor = COLORS[0];
29989
30419
  annotations = [];
29990
- renderedRects = [];
30420
+ markNodes = [];
30421
+ drag = null;
30422
+ active = false;
29991
30423
  sessionBase = 0;
30424
+ stepCounter = 0;
30425
+ idSeq = 0;
29992
30426
  onExit = null;
30427
+ tool = "rect";
30428
+ color = COLORS[0];
30429
+ width = 3.5;
30430
+ docW = 0;
30431
+ docH = 0;
30432
+ resizeObserver = null;
30433
+ toolBtns = /* @__PURE__ */ new Map();
30434
+ colorBtns = /* @__PURE__ */ new Map();
30435
+ widthBtns = /* @__PURE__ */ new Map();
30436
+ undoBtn = null;
30437
+ clearBtn = null;
29993
30438
  get isActive() {
29994
30439
  return this.active;
29995
30440
  }
29996
30441
  get count() {
29997
30442
  return this.annotations.length;
29998
30443
  }
30444
+ /** Deep-copied document-coord annotations per the schema contract. */
29999
30445
  list() {
30000
- return this.annotations.map((a) => ({ ...a, viewport: { ...a.viewport } }));
30446
+ return this.annotations.map((a) => {
30447
+ const copy2 = { ...a };
30448
+ if ("points" in copy2 && Array.isArray(copy2.points)) {
30449
+ copy2.points = copy2.points.map((p) => ({ ...p }));
30450
+ }
30451
+ if (copy2.kind === "rect" && copy2.viewport) copy2.viewport = { ...copy2.viewport };
30452
+ return copy2;
30453
+ });
30001
30454
  }
30455
+ /** Remove every mark. Tears the layer down when not actively drawing. */
30002
30456
  clear() {
30457
+ for (const node2 of this.markNodes) node2.remove();
30458
+ this.markNodes = [];
30003
30459
  this.annotations = [];
30004
- for (const rect of this.renderedRects) rect.remove();
30005
- this.renderedRects = [];
30460
+ this.stepCounter = 0;
30461
+ if (!this.active) this.teardownLayer();
30462
+ this.syncActionButtons();
30006
30463
  }
30007
30464
  start(onExit) {
30008
30465
  if (this.active) return;
30466
+ this.ensureLayer();
30467
+ const svg2 = this.svg;
30009
30468
  this.active = true;
30010
30469
  this.onExit = onExit;
30011
30470
  this.sessionBase = this.annotations.length;
30012
- this.layer = el("div", { class: "lens-annotate" });
30013
- this.layer.addEventListener("pointerdown", this.onDown);
30014
- this.chips = el("div", { class: "lens-annotate-chips" }, [
30015
- el("span", { class: "lens-annotate-chips__label", text: "Drag to mark an area" }),
30471
+ this.stepCounter = 0;
30472
+ this.sizeLayer();
30473
+ this.updateTransform();
30474
+ svg2.style.pointerEvents = "auto";
30475
+ svg2.style.touchAction = "none";
30476
+ svg2.style.cursor = "crosshair";
30477
+ svg2.addEventListener("pointerdown", this.onPointerDown);
30478
+ this.buildToolbar();
30479
+ window.addEventListener("keydown", this.onKey, true);
30480
+ }
30481
+ /** Done — keep this session's marks, exit draw mode. */
30482
+ exit() {
30483
+ if (!this.active) return;
30484
+ this.active = false;
30485
+ this.commitTextInput();
30486
+ if (this.drag) {
30487
+ this.drag.node.remove();
30488
+ this.detachDragListeners();
30489
+ this.drag = null;
30490
+ }
30491
+ const svg2 = this.svg;
30492
+ if (svg2) {
30493
+ svg2.removeEventListener("pointerdown", this.onPointerDown);
30494
+ svg2.style.pointerEvents = "none";
30495
+ svg2.style.touchAction = "";
30496
+ svg2.style.cursor = "";
30497
+ }
30498
+ window.removeEventListener("keydown", this.onKey, true);
30499
+ this.removeToolbar();
30500
+ const cb = this.onExit;
30501
+ this.onExit = null;
30502
+ cb?.();
30503
+ }
30504
+ /** Cancel — discard marks drawn during this session, then exit. */
30505
+ cancel() {
30506
+ this.cancelTextInput();
30507
+ const removed = this.markNodes.splice(this.sessionBase);
30508
+ for (const node2 of removed) node2.remove();
30509
+ this.annotations.splice(this.sessionBase);
30510
+ this.exit();
30511
+ }
30512
+ undo() {
30513
+ const node2 = this.markNodes.pop();
30514
+ if (!node2) return;
30515
+ node2.remove();
30516
+ this.annotations.pop();
30517
+ this.syncActionButtons();
30518
+ }
30519
+ // --------------------------------------------------------------------------
30520
+ // Composite / crop
30521
+ // --------------------------------------------------------------------------
30522
+ /**
30523
+ * Complete standalone SVG of every NON-blur mark, plus blur regions returned
30524
+ * separately. `null` when there are no marks.
30525
+ */
30526
+ getComposite() {
30527
+ if (this.annotations.length === 0 || !this.svg) return null;
30528
+ this.sizeLayer();
30529
+ const docWidth = this.docW;
30530
+ const docHeight = this.docH;
30531
+ const clone2 = this.svg.cloneNode(true);
30532
+ clone2.removeAttribute("style");
30533
+ clone2.removeAttribute("class");
30534
+ clone2.querySelectorAll("[data-lens-chrome],[data-lens-draft],[data-lens-blur]").forEach((n2) => n2.remove());
30535
+ clone2.querySelector("defs")?.remove();
30536
+ clone2.setAttribute("xmlns", SVG_NS);
30537
+ clone2.setAttribute("viewBox", `0 0 ${docWidth} ${docHeight}`);
30538
+ clone2.setAttribute("width", String(docWidth));
30539
+ clone2.setAttribute("height", String(docHeight));
30540
+ const svgMarkup = new XMLSerializer().serializeToString(clone2);
30541
+ const blurRegions = this.annotations.filter((a) => a.kind === "blur").map((a) => ({ x: a.x, y: a.y, width: a.width, height: a.height }));
30542
+ return { svgMarkup, docWidth, docHeight, blurRegions };
30543
+ }
30544
+ /** Union bbox of all marks + 24px padding, clamped to the document. */
30545
+ getCropBounds() {
30546
+ if (this.annotations.length === 0) return void 0;
30547
+ const boxes = [];
30548
+ for (let i = 0; i < this.annotations.length; i++) {
30549
+ const a = this.annotations[i];
30550
+ const b = a.kind === "text" ? this.textBounds(i) : boundsOfAnnotation(a);
30551
+ if (b) boxes.push(b);
30552
+ }
30553
+ const u = unionBounds(boxes);
30554
+ if (!u) return void 0;
30555
+ this.sizeLayer();
30556
+ return padAndClampBounds(u, 24, this.docW, this.docH);
30557
+ }
30558
+ /** Prefer a live getBBox() for text; fall back to the geometric estimate. */
30559
+ textBounds(index2) {
30560
+ const node2 = this.markNodes[index2];
30561
+ const annotation = this.annotations[index2];
30562
+ if (node2 && typeof node2.getBBox === "function") {
30563
+ try {
30564
+ const box = node2.getBBox();
30565
+ if (box.width > 0 && box.height > 0) {
30566
+ return { x: box.x, y: box.y, width: box.width, height: box.height };
30567
+ }
30568
+ } catch {
30569
+ }
30570
+ }
30571
+ return boundsOfAnnotation(annotation);
30572
+ }
30573
+ // --------------------------------------------------------------------------
30574
+ // Layer lifecycle
30575
+ // --------------------------------------------------------------------------
30576
+ ensureLayer() {
30577
+ if (this.svg) return;
30578
+ const svg2 = document.createElementNS(SVG_NS, "svg");
30579
+ svg2.setAttribute("class", "lens-annotate-svg");
30580
+ svg2.setAttribute("xmlns", SVG_NS);
30581
+ const defs = document.createElementNS(SVG_NS, "defs");
30582
+ defs.setAttribute("data-lens-chrome", "1");
30583
+ const pattern = document.createElementNS(SVG_NS, "pattern");
30584
+ pattern.setAttribute("id", "lens-blur-hatch");
30585
+ pattern.setAttribute("width", "8");
30586
+ pattern.setAttribute("height", "8");
30587
+ pattern.setAttribute("patternUnits", "userSpaceOnUse");
30588
+ pattern.setAttribute("patternTransform", "rotate(45)");
30589
+ const patternBase = document.createElementNS(SVG_NS, "rect");
30590
+ patternBase.setAttribute("width", "8");
30591
+ patternBase.setAttribute("height", "8");
30592
+ patternBase.setAttribute("fill", "rgba(17,24,39,0.12)");
30593
+ const patternLine = document.createElementNS(SVG_NS, "line");
30594
+ patternLine.setAttribute("x1", "0");
30595
+ patternLine.setAttribute("y1", "0");
30596
+ patternLine.setAttribute("x2", "0");
30597
+ patternLine.setAttribute("y2", "8");
30598
+ patternLine.setAttribute("stroke", "rgba(17,24,39,0.55)");
30599
+ patternLine.setAttribute("stroke-width", "4");
30600
+ pattern.append(patternBase, patternLine);
30601
+ defs.append(pattern);
30602
+ svg2.append(defs);
30603
+ const bg = document.createElementNS(SVG_NS, "rect");
30604
+ bg.setAttribute("data-lens-chrome", "1");
30605
+ bg.setAttribute("x", "0");
30606
+ bg.setAttribute("y", "0");
30607
+ bg.setAttribute("fill", "transparent");
30608
+ bg.style.pointerEvents = "all";
30609
+ svg2.append(bg);
30610
+ this.svg = svg2;
30611
+ this.bg = bg;
30612
+ this.host.append(svg2);
30613
+ window.addEventListener("scroll", this.onScroll, { passive: true, capture: true });
30614
+ window.addEventListener("resize", this.onResize);
30615
+ if (typeof ResizeObserver !== "undefined") {
30616
+ this.resizeObserver = new ResizeObserver(this.onResize);
30617
+ this.resizeObserver.observe(document.documentElement);
30618
+ }
30619
+ this.sizeLayer();
30620
+ this.updateTransform();
30621
+ }
30622
+ teardownLayer() {
30623
+ window.removeEventListener("scroll", this.onScroll, true);
30624
+ window.removeEventListener("resize", this.onResize);
30625
+ this.resizeObserver?.disconnect();
30626
+ this.resizeObserver = null;
30627
+ this.svg?.remove();
30628
+ this.svg = null;
30629
+ this.bg = null;
30630
+ }
30631
+ docSize() {
30632
+ const d = document.documentElement;
30633
+ const b = document.body;
30634
+ const width = Math.max(d.scrollWidth, b?.scrollWidth ?? 0, d.clientWidth);
30635
+ const height = Math.max(d.scrollHeight, b?.scrollHeight ?? 0, d.clientHeight);
30636
+ return { width, height };
30637
+ }
30638
+ sizeLayer() {
30639
+ if (!this.svg) return;
30640
+ const { width, height } = this.docSize();
30641
+ this.docW = width;
30642
+ this.docH = height;
30643
+ this.svg.setAttribute("width", String(width));
30644
+ this.svg.setAttribute("height", String(height));
30645
+ this.svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
30646
+ this.svg.style.width = `${width}px`;
30647
+ this.svg.style.height = `${height}px`;
30648
+ if (this.bg) {
30649
+ this.bg.setAttribute("width", String(width));
30650
+ this.bg.setAttribute("height", String(height));
30651
+ }
30652
+ }
30653
+ updateTransform() {
30654
+ if (!this.svg) return;
30655
+ const scrollX = window.scrollX || window.pageXOffset || 0;
30656
+ const scrollY = window.scrollY || window.pageYOffset || 0;
30657
+ this.svg.style.left = `${-scrollX}px`;
30658
+ this.svg.style.top = `${-scrollY}px`;
30659
+ }
30660
+ onScroll = () => this.updateTransform();
30661
+ onResize = () => {
30662
+ this.sizeLayer();
30663
+ this.updateTransform();
30664
+ };
30665
+ // --------------------------------------------------------------------------
30666
+ // Toolbar
30667
+ // --------------------------------------------------------------------------
30668
+ buildToolbar() {
30669
+ this.toolBtns.clear();
30670
+ this.colorBtns.clear();
30671
+ this.widthBtns.clear();
30672
+ const toolGroup = el("div", { class: "lens-tool-group" });
30673
+ for (const { tool, icon, label } of TOOLS) {
30674
+ const btn = el("button", {
30675
+ class: "lens-tool-btn",
30676
+ ariaLabel: label,
30677
+ title: label,
30678
+ html: icon,
30679
+ attrs: { type: "button", "aria-pressed": String(tool === this.tool) },
30680
+ on: { click: () => this.selectTool(tool) }
30681
+ });
30682
+ this.toolBtns.set(tool, btn);
30683
+ toolGroup.append(btn);
30684
+ }
30685
+ const colorGroup = el("div", { class: "lens-tool-group lens-swatches" });
30686
+ for (const c of COLORS) {
30687
+ const btn = el("button", {
30688
+ class: "lens-swatch",
30689
+ ariaLabel: `Color ${c}`,
30690
+ title: c,
30691
+ attrs: { type: "button", "aria-pressed": String(c === this.color) },
30692
+ style: { background: c },
30693
+ on: { click: () => this.selectColor(c) }
30694
+ });
30695
+ this.colorBtns.set(c, btn);
30696
+ colorGroup.append(btn);
30697
+ }
30698
+ const widthGroup = el("div", { class: "lens-tool-group" });
30699
+ for (const { label, value } of WIDTHS) {
30700
+ const btn = el("button", {
30701
+ class: "lens-width-btn",
30702
+ text: label,
30703
+ ariaLabel: `Stroke width ${label}`,
30704
+ title: `Stroke ${label} (${value}px)`,
30705
+ attrs: { type: "button", "aria-pressed": String(value === this.width) },
30706
+ on: { click: () => this.selectWidth(value) }
30707
+ });
30708
+ this.widthBtns.set(value, btn);
30709
+ widthGroup.append(btn);
30710
+ }
30711
+ this.undoBtn = el("button", {
30712
+ class: "lens-tool-btn",
30713
+ ariaLabel: "Undo",
30714
+ title: "Undo",
30715
+ html: iconUndo,
30716
+ attrs: { type: "button" },
30717
+ on: { click: () => this.undo() }
30718
+ });
30719
+ this.clearBtn = el("button", {
30720
+ class: "lens-tool-btn",
30721
+ ariaLabel: "Clear all",
30722
+ title: "Clear all",
30723
+ html: iconTrash,
30724
+ attrs: { type: "button" },
30725
+ on: { click: () => this.clear() }
30726
+ });
30727
+ const editGroup = el("div", { class: "lens-tool-group" }, [this.undoBtn, this.clearBtn]);
30728
+ const doneGroup = el("div", { class: "lens-tool-group" }, [
30016
30729
  el("button", {
30017
30730
  class: "lens-pick-chip__btn",
30018
30731
  text: "Cancel",
30732
+ attrs: { type: "button" },
30019
30733
  on: { click: () => this.cancel() }
30020
30734
  }),
30021
30735
  el("button", {
30022
30736
  class: "lens-pick-chip__btn lens-pick-chip__btn--primary",
30023
30737
  text: "Done",
30738
+ attrs: { type: "button" },
30024
30739
  on: { click: () => this.exit() }
30025
30740
  })
30026
30741
  ]);
30027
- this.host.append(this.layer, this.chips);
30028
- window.addEventListener("keydown", this.onKey, true);
30029
- }
30030
- /** Discard rectangles drawn during this session, then exit. */
30031
- cancel() {
30032
- const removed = this.renderedRects.splice(this.sessionBase);
30033
- for (const rect of removed) rect.remove();
30034
- this.annotations.splice(this.sessionBase);
30035
- this.exit();
30742
+ this.toolbar = el(
30743
+ "div",
30744
+ { class: "lens-annotate-toolbar", attrs: { role: "toolbar", "aria-label": "Annotation tools" } },
30745
+ [
30746
+ toolGroup,
30747
+ el("span", { class: "lens-tool-sep" }),
30748
+ colorGroup,
30749
+ el("span", { class: "lens-tool-sep" }),
30750
+ widthGroup,
30751
+ el("span", { class: "lens-tool-sep" }),
30752
+ editGroup,
30753
+ el("span", { class: "lens-tool-sep" }),
30754
+ doneGroup
30755
+ ]
30756
+ );
30757
+ this.host.append(this.toolbar);
30758
+ this.syncActionButtons();
30759
+ }
30760
+ removeToolbar() {
30761
+ this.toolbar?.remove();
30762
+ this.toolbar = null;
30763
+ this.toolBtns.clear();
30764
+ this.colorBtns.clear();
30765
+ this.widthBtns.clear();
30766
+ this.undoBtn = null;
30767
+ this.clearBtn = null;
30768
+ }
30769
+ selectTool(tool) {
30770
+ this.commitTextInput();
30771
+ this.tool = tool;
30772
+ for (const [t, btn] of this.toolBtns) {
30773
+ const on2 = t === tool;
30774
+ btn.classList.toggle("is-active", on2);
30775
+ btn.setAttribute("aria-pressed", String(on2));
30776
+ }
30777
+ if (this.svg) this.svg.style.cursor = tool === "text" ? "text" : "crosshair";
30778
+ }
30779
+ selectColor(color) {
30780
+ this.color = color;
30781
+ for (const [c, btn] of this.colorBtns) {
30782
+ const on2 = c === color;
30783
+ btn.classList.toggle("is-active", on2);
30784
+ btn.setAttribute("aria-pressed", String(on2));
30785
+ }
30786
+ }
30787
+ selectWidth(value) {
30788
+ this.width = value;
30789
+ for (const [v, btn] of this.widthBtns) {
30790
+ const on2 = v === value;
30791
+ btn.classList.toggle("is-active", on2);
30792
+ btn.setAttribute("aria-pressed", String(on2));
30793
+ }
30794
+ }
30795
+ syncActionButtons() {
30796
+ const empty = this.markNodes.length === 0;
30797
+ if (this.undoBtn) this.undoBtn.disabled = empty;
30798
+ if (this.clearBtn) this.clearBtn.disabled = empty;
30799
+ }
30800
+ // --------------------------------------------------------------------------
30801
+ // Pointer / drawing
30802
+ // --------------------------------------------------------------------------
30803
+ toDoc(e) {
30804
+ return {
30805
+ x: e.clientX + (window.scrollX || window.pageXOffset || 0),
30806
+ y: e.clientY + (window.scrollY || window.pageYOffset || 0)
30807
+ };
30036
30808
  }
30037
- exit() {
30809
+ onPointerDown = (e) => {
30038
30810
  if (!this.active) return;
30039
- this.active = false;
30040
- this.dragging = false;
30041
- window.removeEventListener("keydown", this.onKey, true);
30042
- window.removeEventListener("pointermove", this.onMove, true);
30043
- window.removeEventListener("pointerup", this.onUp, true);
30044
- if (this.draft) {
30045
- this.draft.remove();
30046
- this.draft = null;
30811
+ if (e.button !== void 0 && e.button !== 0) return;
30812
+ e.preventDefault();
30813
+ const p = this.toDoc(e);
30814
+ const tool = this.tool;
30815
+ if (tool === "text") {
30816
+ this.openTextInput(e.clientX, e.clientY, p);
30817
+ return;
30047
30818
  }
30048
- if (this.layer) {
30049
- this.layer.removeEventListener("pointerdown", this.onDown);
30050
- this.layer.remove();
30051
- this.layer = null;
30819
+ if (tool === "step") {
30820
+ this.placeStep(p);
30821
+ return;
30052
30822
  }
30053
- if (this.chips) {
30054
- this.chips.remove();
30055
- this.chips = null;
30823
+ const color = this.color;
30824
+ const width = this.width;
30825
+ let drag;
30826
+ if (tool === "pencil" || tool === "highlighter") {
30827
+ const poly = this.svgEl("polyline", {
30828
+ "data-lens-draft": "1",
30829
+ fill: "none",
30830
+ stroke: color,
30831
+ "stroke-width": tool === "highlighter" ? HIGHLIGHTER_WIDTH : width,
30832
+ "stroke-linecap": "round",
30833
+ "stroke-linejoin": "round",
30834
+ ...tool === "highlighter" ? { "stroke-opacity": "0.35" } : {},
30835
+ points: `${p.x},${p.y}`
30836
+ });
30837
+ this.svg.append(poly);
30838
+ drag = { tool, color, width, start: p, node: poly, points: [p], last: p };
30839
+ } else if (tool === "arrow") {
30840
+ const g = this.svgEl("g", { "data-lens-draft": "1" });
30841
+ const line = this.svgEl("line", {
30842
+ stroke: color,
30843
+ "stroke-width": width,
30844
+ "stroke-linecap": "round"
30845
+ });
30846
+ const head = this.svgEl("polygon", { fill: color });
30847
+ g.append(line, head);
30848
+ this.svg.append(g);
30849
+ drag = { tool, color, width, start: p, node: g, line, head };
30850
+ this.updateArrow(drag, p);
30851
+ } else {
30852
+ const node2 = this.createShapeNode(tool, color, width);
30853
+ this.svg.append(node2);
30854
+ drag = { tool, color, width, start: p, node: node2 };
30855
+ this.updateShape(drag, p);
30856
+ }
30857
+ this.drag = drag;
30858
+ window.addEventListener("pointermove", this.onPointerMove, true);
30859
+ window.addEventListener("pointerup", this.onPointerUp, true);
30860
+ };
30861
+ onPointerMove = (e) => {
30862
+ const d = this.drag;
30863
+ if (!d) return;
30864
+ const p = this.toDoc(e);
30865
+ if (d.tool === "pencil" || d.tool === "highlighter") {
30866
+ const pts = d.points;
30867
+ if (d.last && shouldKeepPoint(d.last, p) && pts.length < MAX_POINTS) {
30868
+ pts.push(p);
30869
+ d.last = p;
30870
+ d.node.setAttribute("points", this.pointsAttr(pts));
30871
+ }
30872
+ } else if (d.tool === "arrow") {
30873
+ this.updateArrow(d, p);
30874
+ } else {
30875
+ this.updateShape(d, p);
30876
+ }
30877
+ };
30878
+ onPointerUp = (e) => {
30879
+ const d = this.drag;
30880
+ this.detachDragListeners();
30881
+ this.drag = null;
30882
+ if (!d) return;
30883
+ const p = this.toDoc(e);
30884
+ let annotation = null;
30885
+ if (d.tool === "pencil" || d.tool === "highlighter") {
30886
+ let pts = d.points;
30887
+ if (pts.length >= 2) pts = simplifyPoints(pts, 1);
30888
+ if (pts.length < 2) {
30889
+ d.node.remove();
30890
+ return;
30891
+ }
30892
+ d.node.setAttribute("points", this.pointsAttr(pts));
30893
+ annotation = d.tool === "pencil" ? { kind: "pencil", points: pts, color: d.color, strokeWidth: d.width, id: this.nextId() } : {
30894
+ kind: "highlighter",
30895
+ points: pts,
30896
+ color: d.color,
30897
+ strokeWidth: HIGHLIGHTER_WIDTH,
30898
+ id: this.nextId()
30899
+ };
30900
+ } else if (d.tool === "arrow" || d.tool === "line") {
30901
+ if (distance(d.start, p) < MIN_DRAG) {
30902
+ d.node.remove();
30903
+ return;
30904
+ }
30905
+ if (d.tool === "arrow") {
30906
+ this.updateArrow(d, p);
30907
+ annotation = {
30908
+ kind: "arrow",
30909
+ x1: d.start.x,
30910
+ y1: d.start.y,
30911
+ x2: p.x,
30912
+ y2: p.y,
30913
+ color: d.color,
30914
+ strokeWidth: d.width,
30915
+ id: this.nextId()
30916
+ };
30917
+ } else {
30918
+ this.updateShape(d, p);
30919
+ annotation = {
30920
+ kind: "line",
30921
+ x1: d.start.x,
30922
+ y1: d.start.y,
30923
+ x2: p.x,
30924
+ y2: p.y,
30925
+ color: d.color,
30926
+ strokeWidth: d.width,
30927
+ id: this.nextId()
30928
+ };
30929
+ }
30930
+ } else {
30931
+ const x = Math.min(d.start.x, p.x);
30932
+ const y = Math.min(d.start.y, p.y);
30933
+ const width = Math.abs(p.x - d.start.x);
30934
+ const height = Math.abs(p.y - d.start.y);
30935
+ if (width < MIN_DRAG || height < MIN_DRAG) {
30936
+ d.node.remove();
30937
+ return;
30938
+ }
30939
+ this.updateShape(d, p);
30940
+ if (d.tool === "rect") {
30941
+ annotation = {
30942
+ kind: "rect",
30943
+ x,
30944
+ y,
30945
+ width,
30946
+ height,
30947
+ color: d.color,
30948
+ strokeWidth: d.width,
30949
+ id: this.nextId()
30950
+ };
30951
+ } else if (d.tool === "ellipse") {
30952
+ annotation = {
30953
+ kind: "ellipse",
30954
+ x,
30955
+ y,
30956
+ width,
30957
+ height,
30958
+ color: d.color,
30959
+ strokeWidth: d.width,
30960
+ id: this.nextId()
30961
+ };
30962
+ } else {
30963
+ annotation = { kind: "blur", x, y, width, height, id: this.nextId() };
30964
+ }
30056
30965
  }
30057
- const exit = this.onExit;
30058
- this.onExit = null;
30059
- exit?.();
30060
- }
30061
- onDown = (e) => {
30062
- e.preventDefault();
30063
- this.dragging = true;
30064
- this.startX = e.clientX;
30065
- this.startY = e.clientY;
30066
- this.draft = el("div", { class: "lens-rect" });
30067
- this.host.append(this.draft);
30068
- this.position(this.draft, e.clientX, e.clientY);
30069
- window.addEventListener("pointermove", this.onMove, true);
30070
- window.addEventListener("pointerup", this.onUp, true);
30966
+ d.node.removeAttribute("data-lens-draft");
30967
+ this.commitMark(annotation, d.node);
30071
30968
  };
30072
- onMove = (e) => {
30073
- if (this.dragging && this.draft) this.position(this.draft, e.clientX, e.clientY);
30074
- };
30075
- onUp = (e) => {
30076
- window.removeEventListener("pointermove", this.onMove, true);
30077
- window.removeEventListener("pointerup", this.onUp, true);
30078
- this.dragging = false;
30079
- const x = Math.min(this.startX, e.clientX);
30080
- const y = Math.min(this.startY, e.clientY);
30081
- const width = Math.abs(e.clientX - this.startX);
30082
- const height = Math.abs(e.clientY - this.startY);
30083
- if (this.draft) {
30084
- this.draft.remove();
30085
- this.draft = null;
30086
- }
30087
- if (width < 6 || height < 6) return;
30088
- const rect = el("div", { class: "lens-rect" });
30089
- Object.assign(rect.style, {
30090
- left: `${x}px`,
30091
- top: `${y}px`,
30092
- width: `${width}px`,
30093
- height: `${height}px`
30969
+ detachDragListeners() {
30970
+ window.removeEventListener("pointermove", this.onPointerMove, true);
30971
+ window.removeEventListener("pointerup", this.onPointerUp, true);
30972
+ }
30973
+ commitMark(annotation, node2) {
30974
+ this.annotations.push(annotation);
30975
+ this.markNodes.push(node2);
30976
+ this.syncActionButtons();
30977
+ }
30978
+ createShapeNode(tool, color, width) {
30979
+ if (tool === "ellipse") {
30980
+ return this.svgEl("ellipse", {
30981
+ "data-lens-draft": "1",
30982
+ fill: "none",
30983
+ stroke: color,
30984
+ "stroke-width": width
30985
+ });
30986
+ }
30987
+ if (tool === "line") {
30988
+ return this.svgEl("line", {
30989
+ "data-lens-draft": "1",
30990
+ stroke: color,
30991
+ "stroke-width": width,
30992
+ "stroke-linecap": "round"
30993
+ });
30994
+ }
30995
+ if (tool === "blur") {
30996
+ return this.svgEl("rect", {
30997
+ "data-lens-draft": "1",
30998
+ "data-lens-blur": "1",
30999
+ fill: "url(#lens-blur-hatch)",
31000
+ stroke: "#111827",
31001
+ "stroke-width": 1,
31002
+ "stroke-dasharray": "4 3"
31003
+ });
31004
+ }
31005
+ return this.svgEl("rect", {
31006
+ "data-lens-draft": "1",
31007
+ fill: "none",
31008
+ stroke: color,
31009
+ "stroke-width": width,
31010
+ rx: 2
30094
31011
  });
30095
- this.host.append(rect);
30096
- this.renderedRects.push(rect);
30097
- this.annotations.push({
30098
- kind: "rect",
30099
- x,
30100
- y,
30101
- width,
30102
- height,
30103
- viewport: { width: window.innerWidth, height: window.innerHeight }
31012
+ }
31013
+ updateShape(d, cur) {
31014
+ const x = Math.min(d.start.x, cur.x);
31015
+ const y = Math.min(d.start.y, cur.y);
31016
+ const w = Math.abs(cur.x - d.start.x);
31017
+ const h = Math.abs(cur.y - d.start.y);
31018
+ const node2 = d.node;
31019
+ if (d.tool === "ellipse") {
31020
+ node2.setAttribute("cx", String(x + w / 2));
31021
+ node2.setAttribute("cy", String(y + h / 2));
31022
+ node2.setAttribute("rx", String(w / 2));
31023
+ node2.setAttribute("ry", String(h / 2));
31024
+ } else if (d.tool === "line") {
31025
+ node2.setAttribute("x1", String(d.start.x));
31026
+ node2.setAttribute("y1", String(d.start.y));
31027
+ node2.setAttribute("x2", String(cur.x));
31028
+ node2.setAttribute("y2", String(cur.y));
31029
+ } else {
31030
+ node2.setAttribute("x", String(x));
31031
+ node2.setAttribute("y", String(y));
31032
+ node2.setAttribute("width", String(w));
31033
+ node2.setAttribute("height", String(h));
31034
+ }
31035
+ }
31036
+ updateArrow(d, cur) {
31037
+ if (!d.line || !d.head) return;
31038
+ d.line.setAttribute("x1", String(d.start.x));
31039
+ d.line.setAttribute("y1", String(d.start.y));
31040
+ d.line.setAttribute("x2", String(cur.x));
31041
+ d.line.setAttribute("y2", String(cur.y));
31042
+ const size = Math.max(12, d.width * 3);
31043
+ const poly = arrowheadPolygon(d.start.x, d.start.y, cur.x, cur.y, size);
31044
+ d.head.setAttribute("points", this.pointsAttr(poly));
31045
+ }
31046
+ // --------------------------------------------------------------------------
31047
+ // Text tool
31048
+ // --------------------------------------------------------------------------
31049
+ openTextInput(clientX, clientY, docPoint) {
31050
+ this.commitTextInput();
31051
+ const input2 = el("input", {
31052
+ class: "lens-text-input",
31053
+ attrs: { type: "text", "aria-label": "Annotation text" },
31054
+ style: { left: `${clientX}px`, top: `${clientY}px`, color: this.color }
30104
31055
  });
30105
- };
30106
- position(node2, curX, curY) {
30107
- const x = Math.min(this.startX, curX);
30108
- const y = Math.min(this.startY, curY);
30109
- Object.assign(node2.style, {
30110
- left: `${x}px`,
30111
- top: `${y}px`,
30112
- width: `${Math.abs(curX - this.startX)}px`,
30113
- height: `${Math.abs(curY - this.startY)}px`
31056
+ input2.addEventListener("blur", () => this.commitTextInput());
31057
+ this.host.append(input2);
31058
+ this.textInputEl = input2;
31059
+ this.textAnchor = docPoint;
31060
+ this.textColor = this.color;
31061
+ input2.focus();
31062
+ }
31063
+ commitTextInput() {
31064
+ const input2 = this.textInputEl;
31065
+ const anchor = this.textAnchor;
31066
+ if (!input2 || !anchor) return;
31067
+ const value = input2.value.trim();
31068
+ this.textInputEl = null;
31069
+ this.textAnchor = null;
31070
+ input2.remove();
31071
+ if (!value) return;
31072
+ const node2 = this.svgEl("text", {
31073
+ x: anchor.x,
31074
+ y: anchor.y,
31075
+ fill: this.textColor,
31076
+ "font-size": 16,
31077
+ "font-family": FONT,
31078
+ "dominant-baseline": "hanging"
30114
31079
  });
31080
+ node2.textContent = value;
31081
+ this.svg.append(node2);
31082
+ this.commitMark(
31083
+ {
31084
+ kind: "text",
31085
+ x: anchor.x,
31086
+ y: anchor.y,
31087
+ text: value,
31088
+ fontSize: 16,
31089
+ color: this.textColor,
31090
+ id: this.nextId()
31091
+ },
31092
+ node2
31093
+ );
30115
31094
  }
31095
+ cancelTextInput() {
31096
+ if (!this.textInputEl) return;
31097
+ this.textInputEl.remove();
31098
+ this.textInputEl = null;
31099
+ this.textAnchor = null;
31100
+ }
31101
+ // --------------------------------------------------------------------------
31102
+ // Step tool
31103
+ // --------------------------------------------------------------------------
31104
+ placeStep(p) {
31105
+ const n2 = ++this.stepCounter;
31106
+ const g = this.svgEl("g", {});
31107
+ const circle = this.svgEl("circle", {
31108
+ cx: p.x,
31109
+ cy: p.y,
31110
+ r: STEP_RADIUS,
31111
+ fill: this.color
31112
+ });
31113
+ const text = this.svgEl("text", {
31114
+ x: p.x,
31115
+ y: p.y,
31116
+ fill: "#ffffff",
31117
+ "font-size": 13,
31118
+ "font-weight": 700,
31119
+ "font-family": FONT,
31120
+ "text-anchor": "middle",
31121
+ "dominant-baseline": "central"
31122
+ });
31123
+ text.textContent = String(n2);
31124
+ g.append(circle, text);
31125
+ this.svg.append(g);
31126
+ this.commitMark({ kind: "step", x: p.x, y: p.y, n: n2, color: this.color, id: this.nextId() }, g);
31127
+ }
31128
+ // --------------------------------------------------------------------------
31129
+ // Keyboard
31130
+ // --------------------------------------------------------------------------
30116
31131
  onKey = (e) => {
30117
- if (e.key !== "Escape") return;
30118
- e.preventDefault();
30119
- e.stopPropagation();
30120
- this.exit();
31132
+ if (this.textInputEl) {
31133
+ if (e.key === "Escape") {
31134
+ e.preventDefault();
31135
+ e.stopPropagation();
31136
+ this.cancelTextInput();
31137
+ } else if (e.key === "Enter") {
31138
+ e.preventDefault();
31139
+ e.stopPropagation();
31140
+ this.commitTextInput();
31141
+ }
31142
+ return;
31143
+ }
31144
+ if (e.key === "Escape") {
31145
+ e.preventDefault();
31146
+ e.stopPropagation();
31147
+ this.exit();
31148
+ }
30121
31149
  };
31150
+ // --------------------------------------------------------------------------
31151
+ // Small helpers
31152
+ // --------------------------------------------------------------------------
31153
+ svgEl(tag, attrs) {
31154
+ const node2 = document.createElementNS(SVG_NS, tag);
31155
+ for (const [k, v] of Object.entries(attrs)) node2.setAttribute(k, String(v));
31156
+ return node2;
31157
+ }
31158
+ pointsAttr(points) {
31159
+ return points.map((p) => `${p.x},${p.y}`).join(" ");
31160
+ }
31161
+ nextId() {
31162
+ return `lens-${++this.idSeq}`;
31163
+ }
30122
31164
  };
30123
- var svg = (inner) => `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${inner}</svg>`;
30124
- var iconEye = svg(
30125
- '<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>'
30126
- );
30127
- var iconCrosshair = svg(
30128
- '<circle cx="12" cy="12" r="8"/><line x1="12" y1="2" x2="12" y2="6"/><line x1="12" y1="18" x2="12" y2="22"/><line x1="2" y1="12" x2="6" y2="12"/><line x1="18" y1="12" x2="22" y2="12"/>'
30129
- );
30130
- var iconClose = svg('<line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/>');
30131
- var iconMark = svg(
30132
- '<rect x="3" y="3" width="18" height="18" rx="2" stroke-dasharray="4 3"/>'
30133
- );
30134
- var iconCopy = svg(
30135
- '<rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/>'
30136
- );
30137
- var iconCheck = svg('<polyline points="20 6 9 17 4 12"/>');
30138
- var iconSend = svg('<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>');
30139
- var iconRecord = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="12" cy="12" r="6" fill="#ef4444"/></svg>`;
30140
- var iconStop = `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>`;
30141
31165
  var INCLUDE_ITEMS = [
30142
31166
  { key: "replay", label: "Replay" },
30143
31167
  { key: "console", label: "Console" },
@@ -30294,7 +31318,7 @@ var Panel = class {
30294
31318
  buildActions() {
30295
31319
  const mark = el("button", {
30296
31320
  class: "lens-abtn lens-abtn--ghost lens-abtn--compact",
30297
- html: iconMark + "<span>Mark area</span>",
31321
+ html: iconAnnotate + "<span>Annotate</span>",
30298
31322
  on: { click: () => this.cb.onMarkArea() }
30299
31323
  });
30300
31324
  const record3 = el("button", {
@@ -30678,21 +31702,33 @@ var OVERLAY_CSS = (
30678
31702
  pointer-events: none;
30679
31703
  }
30680
31704
 
30681
- /* ---------------- Annotation layer ---------------- */
30682
- .lens-annotate {
30683
- position: fixed;
30684
- inset: 0;
31705
+ /* ---------------- Annotation SVG layer ---------------- */
31706
+ /* One full-DOCUMENT SVG anchored to page content (translated by -scroll so
31707
+ marks survive scrolling). pointer-events flip to auto only while drawing. */
31708
+ .lens-annotate-svg {
31709
+ position: absolute;
31710
+ left: 0;
31711
+ top: 0;
30685
31712
  z-index: calc(var(--lens-z) - 1);
30686
- cursor: crosshair;
30687
- background: rgba(15, 15, 20, 0.04);
30688
- touch-action: none;
31713
+ pointer-events: none;
31714
+ overflow: visible;
30689
31715
  }
30690
- .lens-rect {
31716
+
31717
+ /* Floating text-entry input for the Text tool (viewport-positioned). */
31718
+ .lens-text-input {
30691
31719
  position: fixed;
30692
- border: 2px solid var(--lens-accent);
30693
- background: rgba(99, 102, 241, 0.1);
30694
- border-radius: 3px;
30695
- pointer-events: none;
31720
+ z-index: var(--lens-z);
31721
+ min-width: 120px;
31722
+ padding: 2px 6px;
31723
+ border: 1px solid var(--lens-accent);
31724
+ border-radius: 4px;
31725
+ background: rgba(255, 255, 255, 0.96);
31726
+ color: #111827;
31727
+ font-family: var(--lens-font);
31728
+ font-size: 15px;
31729
+ line-height: 1.3;
31730
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
31731
+ outline: none;
30696
31732
  }
30697
31733
 
30698
31734
  /* ---------------- Panel ---------------- */
@@ -30983,6 +32019,114 @@ var OVERLAY_CSS = (
30983
32019
  }
30984
32020
  .lens-annotate-chips { z-index: var(--lens-z); }
30985
32021
 
32022
+ /* ---------------- Annotation toolbar ---------------- */
32023
+ /* Same floating top-center anchor as the chips row; wraps on narrow screens. */
32024
+ .lens-annotate-toolbar {
32025
+ position: fixed;
32026
+ top: calc(16px + env(safe-area-inset-top));
32027
+ left: 50%;
32028
+ transform: translateX(-50%);
32029
+ z-index: var(--lens-z);
32030
+ display: inline-flex;
32031
+ flex-wrap: wrap;
32032
+ align-items: center;
32033
+ justify-content: center;
32034
+ gap: 6px;
32035
+ max-width: calc(100vw - 24px);
32036
+ padding: 6px 8px;
32037
+ border-radius: 14px;
32038
+ background: rgba(20, 20, 23, 0.92);
32039
+ -webkit-backdrop-filter: blur(12px) saturate(180%);
32040
+ backdrop-filter: blur(12px) saturate(180%);
32041
+ border: 1px solid rgba(255, 255, 255, 0.12);
32042
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.35);
32043
+ color: #f4f4f5;
32044
+ }
32045
+ .lens-tool-group {
32046
+ display: inline-flex;
32047
+ align-items: center;
32048
+ gap: 4px;
32049
+ flex-wrap: wrap;
32050
+ }
32051
+ .lens-tool-sep {
32052
+ width: 1px;
32053
+ align-self: stretch;
32054
+ min-height: 22px;
32055
+ background: rgba(255, 255, 255, 0.16);
32056
+ margin: 0 2px;
32057
+ }
32058
+ .lens-tool-btn {
32059
+ display: inline-flex;
32060
+ align-items: center;
32061
+ justify-content: center;
32062
+ width: 30px;
32063
+ height: 30px;
32064
+ padding: 0;
32065
+ border: 1px solid transparent;
32066
+ border-radius: 8px;
32067
+ background: transparent;
32068
+ color: #d4d4d8;
32069
+ cursor: pointer;
32070
+ transition: background 150ms ease, color 150ms ease, border-color 150ms ease;
32071
+ }
32072
+ .lens-tool-btn:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
32073
+ .lens-tool-btn:focus-visible { outline: 2px solid var(--lens-accent); outline-offset: 1px; }
32074
+ .lens-tool-btn.is-active {
32075
+ background: var(--lens-accent);
32076
+ border-color: var(--lens-accent);
32077
+ color: #fff;
32078
+ }
32079
+ .lens-tool-btn:disabled { opacity: 0.4; cursor: default; }
32080
+ .lens-tool-btn:disabled:hover { background: transparent; color: #d4d4d8; }
32081
+
32082
+ .lens-swatch {
32083
+ width: 22px;
32084
+ height: 22px;
32085
+ padding: 0;
32086
+ border-radius: 999px;
32087
+ border: 2px solid transparent;
32088
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.25) inset;
32089
+ cursor: pointer;
32090
+ transition: transform 120ms ease, border-color 120ms ease;
32091
+ }
32092
+ .lens-swatch:hover { transform: scale(1.1); }
32093
+ .lens-swatch:focus-visible { outline: 2px solid #fff; outline-offset: 1px; }
32094
+ .lens-swatch.is-active {
32095
+ border-color: #fff;
32096
+ box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.4);
32097
+ }
32098
+
32099
+ .lens-width-btn {
32100
+ display: inline-flex;
32101
+ align-items: center;
32102
+ justify-content: center;
32103
+ min-width: 26px;
32104
+ height: 30px;
32105
+ padding: 0 6px;
32106
+ border: 1px solid transparent;
32107
+ border-radius: 8px;
32108
+ background: transparent;
32109
+ color: #d4d4d8;
32110
+ font-family: var(--lens-font);
32111
+ font-size: 12px;
32112
+ font-weight: 700;
32113
+ cursor: pointer;
32114
+ transition: background 150ms ease, color 150ms ease, border-color 150ms ease;
32115
+ }
32116
+ .lens-width-btn:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
32117
+ .lens-width-btn:focus-visible { outline: 2px solid var(--lens-accent); outline-offset: 1px; }
32118
+ .lens-width-btn.is-active {
32119
+ background: var(--lens-accent);
32120
+ border-color: var(--lens-accent);
32121
+ color: #fff;
32122
+ }
32123
+
32124
+ /* Coarse pointers: bump the tool targets toward 40px. */
32125
+ @media (pointer: coarse) {
32126
+ .lens-tool-btn, .lens-width-btn { width: 38px; height: 38px; }
32127
+ .lens-swatch { width: 30px; height: 30px; }
32128
+ }
32129
+
30986
32130
  /* Stacking inside the shadow layer: the panel, pill and floating chips always
30987
32131
  sit above the annotation layer/rects and picker highlight, so marked areas
30988
32132
  never paint over interactive UI. */
@@ -31108,7 +32252,7 @@ var Overlay = class {
31108
32252
  this.layer = el("div", { class: "lens-layer" });
31109
32253
  this.shadow.append(this.layer);
31110
32254
  this.picker = new ElementPicker(this.layer, this.host);
31111
- this.annotator = new Annotator(this.layer);
32255
+ this.annotator = new AnnotationEditor(this.layer);
31112
32256
  this.layer.append(this.buildPill());
31113
32257
  document.documentElement.append(this.host);
31114
32258
  window.addEventListener("keydown", this.onGlobalKey, true);
@@ -31276,11 +32420,15 @@ var Overlay = class {
31276
32420
  }
31277
32421
  async submit(comment2, include) {
31278
32422
  const annotations = this.annotator.list();
32423
+ const composite = this.annotator.getComposite() ?? void 0;
32424
+ const screenshotCrop = this.annotator.getCropBounds();
31279
32425
  const result2 = await this.deps.client.submitReport({
31280
32426
  comment: comment2 || void 0,
31281
32427
  annotations,
31282
32428
  target: this.target ?? void 0,
31283
32429
  excludeFromScreenshot: this.host,
32430
+ screenshotCrop,
32431
+ annotationComposite: composite,
31284
32432
  include
31285
32433
  });
31286
32434
  this.target = null;