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