@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/index.cjs CHANGED
@@ -14633,15 +14633,86 @@ var TargetElementSchema = external_exports.object({
14633
14633
  boundingRect: external_exports.object({ x: external_exports.number(), y: external_exports.number(), width: external_exports.number(), height: external_exports.number() }).optional(),
14634
14634
  reactComponents: external_exports.array(external_exports.string()).optional()
14635
14635
  });
14636
- var AnnotationSchema = external_exports.object({
14637
- kind: external_exports.enum(["rect", "ellipse", "arrow"]),
14638
- x: external_exports.number(),
14639
- y: external_exports.number(),
14640
- width: external_exports.number(),
14641
- height: external_exports.number(),
14642
- /** Page coordinates relative to the viewport at capture time. */
14643
- viewport: external_exports.object({ width: external_exports.number(), height: external_exports.number() })
14644
- });
14636
+ var annotationCommon = {
14637
+ id: external_exports.string().optional(),
14638
+ /** Any CSS color string. */
14639
+ color: external_exports.string().optional(),
14640
+ strokeWidth: external_exports.number().optional()
14641
+ };
14642
+ var AnnotationPointSchema = external_exports.object({ x: external_exports.number(), y: external_exports.number() });
14643
+ var AnnotationSchema = external_exports.discriminatedUnion("kind", [
14644
+ external_exports.object({
14645
+ ...annotationCommon,
14646
+ kind: external_exports.literal("rect"),
14647
+ x: external_exports.number(),
14648
+ y: external_exports.number(),
14649
+ width: external_exports.number(),
14650
+ height: external_exports.number(),
14651
+ /**
14652
+ * OPTIONAL, kept for back-compat with 0.1.x clients that emit
14653
+ * viewport-anchored rects. New clients emit document-pixel rects with no
14654
+ * viewport.
14655
+ */
14656
+ viewport: external_exports.object({ width: external_exports.number(), height: external_exports.number() }).optional()
14657
+ }),
14658
+ external_exports.object({
14659
+ ...annotationCommon,
14660
+ kind: external_exports.literal("ellipse"),
14661
+ x: external_exports.number(),
14662
+ y: external_exports.number(),
14663
+ width: external_exports.number(),
14664
+ height: external_exports.number()
14665
+ }),
14666
+ external_exports.object({
14667
+ ...annotationCommon,
14668
+ kind: external_exports.literal("blur"),
14669
+ x: external_exports.number(),
14670
+ y: external_exports.number(),
14671
+ width: external_exports.number(),
14672
+ height: external_exports.number()
14673
+ }),
14674
+ external_exports.object({
14675
+ ...annotationCommon,
14676
+ kind: external_exports.literal("arrow"),
14677
+ x1: external_exports.number(),
14678
+ y1: external_exports.number(),
14679
+ x2: external_exports.number(),
14680
+ y2: external_exports.number()
14681
+ }),
14682
+ external_exports.object({
14683
+ ...annotationCommon,
14684
+ kind: external_exports.literal("line"),
14685
+ x1: external_exports.number(),
14686
+ y1: external_exports.number(),
14687
+ x2: external_exports.number(),
14688
+ y2: external_exports.number()
14689
+ }),
14690
+ external_exports.object({
14691
+ ...annotationCommon,
14692
+ kind: external_exports.literal("pencil"),
14693
+ points: external_exports.array(AnnotationPointSchema).min(2)
14694
+ }),
14695
+ external_exports.object({
14696
+ ...annotationCommon,
14697
+ kind: external_exports.literal("highlighter"),
14698
+ points: external_exports.array(AnnotationPointSchema).min(2)
14699
+ }),
14700
+ external_exports.object({
14701
+ ...annotationCommon,
14702
+ kind: external_exports.literal("text"),
14703
+ x: external_exports.number(),
14704
+ y: external_exports.number(),
14705
+ text: external_exports.string(),
14706
+ fontSize: external_exports.number().optional()
14707
+ }),
14708
+ external_exports.object({
14709
+ ...annotationCommon,
14710
+ kind: external_exports.literal("step"),
14711
+ x: external_exports.number(),
14712
+ y: external_exports.number(),
14713
+ n: external_exports.number()
14714
+ })
14715
+ ]);
14645
14716
  var AssetKindSchema = external_exports.enum(["replay", "console", "network", "dom", "screenshot", "attachment"]);
14646
14717
  var AssetRefSchema = external_exports.object({
14647
14718
  kind: AssetKindSchema,
@@ -29381,6 +29452,11 @@ var ReplayRecorder = class {
29381
29452
  emit: (event, isCheckout) => this.onEmit(event, isCheckout),
29382
29453
  checkoutEveryNms: this.windowMs,
29383
29454
  recordCanvas: false,
29455
+ // Inline same-origin + CORS images as data URIs at snapshot time so the
29456
+ // dashboard replay paints on a different origin (tainted cross-origin
29457
+ // images fall back to their URL, which is fine for public CDNs).
29458
+ inlineImages: true,
29459
+ collectFonts: true,
29384
29460
  // Keep the Lens overlay itself out of every recording.
29385
29461
  blockSelector: `#${LENS_OVERLAY_HOST_ID}`
29386
29462
  });
@@ -29487,7 +29563,20 @@ var ReplayRecorder = class {
29487
29563
  console.warn("[lens] rrweb recording unavailable; replay disabled", err);
29488
29564
  }
29489
29565
  };
29490
- async function captureScreenshot(excludeHost) {
29566
+ var SVG_RASTERIZE_TIMEOUT_MS = 3e3;
29567
+ function computeCropRect(crop, scale, rasterWidth, rasterHeight) {
29568
+ let sx = Math.round(crop.x * scale);
29569
+ let sy = Math.round(crop.y * scale);
29570
+ let sw = Math.round(crop.width * scale);
29571
+ let sh = Math.round(crop.height * scale);
29572
+ sx = Math.max(0, Math.min(sx, rasterWidth));
29573
+ sy = Math.max(0, Math.min(sy, rasterHeight));
29574
+ sw = Math.min(sw, rasterWidth - sx);
29575
+ sh = Math.min(sh, rasterHeight - sy);
29576
+ if (sw <= 0 || sh <= 0) return null;
29577
+ return { sx, sy, sw, sh };
29578
+ }
29579
+ async function captureScreenshot(excludeHost, crop, composite) {
29491
29580
  if (typeof document === "undefined" || typeof window === "undefined") return null;
29492
29581
  try {
29493
29582
  const viewportPixels = window.innerWidth * window.innerHeight || 1;
@@ -29497,6 +29586,15 @@ async function captureScreenshot(excludeHost) {
29497
29586
  scale,
29498
29587
  type: "image/jpeg",
29499
29588
  quality: 0.82,
29589
+ // JPEG has no alpha — transparent page regions (and any capture box
29590
+ // extending past short content) would otherwise encode as BLACK.
29591
+ backgroundColor: "#ffffff",
29592
+ // When compositing annotations, force the capture box to the editor's
29593
+ // document measurement (scrollHeight-based). modern-screenshot otherwise
29594
+ // sizes from documentElement's bounding box, which on pages SHORTER than
29595
+ // the viewport is the content height only — marks drawn in the lower
29596
+ // viewport band would fall off the raster and the crop would clamp short.
29597
+ ...composite ? { width: composite.docWidth, height: composite.docHeight } : {},
29500
29598
  // Exclude the overlay from the capture CLONE — never hide the live node.
29501
29599
  filter: (node2) => node2 !== excludeHost,
29502
29600
  // Per-resource fetch budget: a single slow cross-origin image must not
@@ -29521,12 +29619,171 @@ async function captureScreenshot(excludeHost) {
29521
29619
  );
29522
29620
  return null;
29523
29621
  }
29622
+ if (composite) {
29623
+ const baked = await compositeScreenshot(blob2, composite, scale, crop);
29624
+ if (baked) return baked;
29625
+ if (composite.blurRegions.length > 0) {
29626
+ console.warn(
29627
+ "[lens] screenshot compositing failed with active redactions; dropping screenshot to avoid leaking un-blurred content"
29628
+ );
29629
+ return null;
29630
+ }
29631
+ }
29632
+ if (crop) {
29633
+ const cropped = await cropBlob(blob2, crop, scale);
29634
+ return cropped ?? blob2;
29635
+ }
29524
29636
  return blob2;
29525
29637
  } catch (err) {
29526
29638
  console.warn("[lens] screenshot capture failed:", err);
29527
29639
  return null;
29528
29640
  }
29529
29641
  }
29642
+ async function compositeScreenshot(blob2, composite, scale, crop) {
29643
+ try {
29644
+ if (typeof createImageBitmap !== "function" || typeof document === "undefined") return null;
29645
+ const bitmap = await createImageBitmap(blob2);
29646
+ const canvas = document.createElement("canvas");
29647
+ canvas.width = bitmap.width;
29648
+ canvas.height = bitmap.height;
29649
+ const ctx = canvas.getContext("2d");
29650
+ if (!ctx) {
29651
+ bitmap.close?.();
29652
+ return null;
29653
+ }
29654
+ ctx.fillStyle = "#ffffff";
29655
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
29656
+ ctx.drawImage(bitmap, 0, 0);
29657
+ bitmap.close?.();
29658
+ applyBlurRegions(ctx, canvas, composite.blurRegions, scale);
29659
+ await drawSvgOverlay(ctx, composite, scale);
29660
+ if (crop) {
29661
+ const rect = computeCropRect(crop, scale, canvas.width, canvas.height);
29662
+ if (rect) {
29663
+ const out = document.createElement("canvas");
29664
+ out.width = rect.sw;
29665
+ out.height = rect.sh;
29666
+ const octx = out.getContext("2d");
29667
+ if (octx) {
29668
+ octx.drawImage(canvas, rect.sx, rect.sy, rect.sw, rect.sh, 0, 0, rect.sw, rect.sh);
29669
+ return await canvasToJpegBlob(out);
29670
+ }
29671
+ }
29672
+ }
29673
+ return await canvasToJpegBlob(canvas);
29674
+ } catch (err) {
29675
+ console.warn("[lens] screenshot compositing failed:", err);
29676
+ return null;
29677
+ }
29678
+ }
29679
+ function applyBlurRegions(ctx, canvas, regions, scale) {
29680
+ if (regions.length === 0) return;
29681
+ const filterSupported = typeof ctx.filter === "string";
29682
+ const blurPx = Math.max(8, 12 * scale);
29683
+ const pad = Math.ceil(blurPx);
29684
+ for (const region of regions) {
29685
+ const x = Math.max(0, Math.min(Math.round(region.x * scale), canvas.width));
29686
+ const y = Math.max(0, Math.min(Math.round(region.y * scale), canvas.height));
29687
+ const w = Math.min(Math.round(region.width * scale), canvas.width - x);
29688
+ const h = Math.min(Math.round(region.height * scale), canvas.height - y);
29689
+ if (w <= 0 || h <= 0) continue;
29690
+ if (!filterSupported) {
29691
+ ctx.fillStyle = "rgba(127,127,127,1)";
29692
+ ctx.fillRect(x, y, w, h);
29693
+ continue;
29694
+ }
29695
+ const px = Math.max(0, x - pad);
29696
+ const py = Math.max(0, y - pad);
29697
+ const pw = Math.min(canvas.width, x + w + pad) - px;
29698
+ const ph = Math.min(canvas.height, y + h + pad) - py;
29699
+ if (pw <= 0 || ph <= 0) continue;
29700
+ const temp = document.createElement("canvas");
29701
+ temp.width = pw;
29702
+ temp.height = ph;
29703
+ const tctx = temp.getContext("2d");
29704
+ if (!tctx) {
29705
+ ctx.fillStyle = "rgba(127,127,127,1)";
29706
+ ctx.fillRect(x, y, w, h);
29707
+ continue;
29708
+ }
29709
+ tctx.filter = `blur(${blurPx}px)`;
29710
+ tctx.drawImage(canvas, px, py, pw, ph, 0, 0, pw, ph);
29711
+ tctx.filter = "none";
29712
+ ctx.save();
29713
+ ctx.beginPath();
29714
+ ctx.rect(x, y, w, h);
29715
+ ctx.clip();
29716
+ ctx.drawImage(temp, px, py);
29717
+ ctx.restore();
29718
+ }
29719
+ }
29720
+ async function drawSvgOverlay(ctx, composite, scale) {
29721
+ const { svgMarkup, docWidth, docHeight } = composite;
29722
+ if (!svgMarkup) return;
29723
+ const img = await loadSvgImage(svgMarkup);
29724
+ if (!img) return;
29725
+ try {
29726
+ ctx.drawImage(img, 0, 0, docWidth * scale, docHeight * scale);
29727
+ } catch (err) {
29728
+ console.warn("[lens] annotation overlay draw failed:", err);
29729
+ }
29730
+ }
29731
+ function loadSvgImage(svg) {
29732
+ return new Promise((resolve) => {
29733
+ if (typeof Image !== "function") {
29734
+ resolve(null);
29735
+ return;
29736
+ }
29737
+ let settled = false;
29738
+ const finish = (result2) => {
29739
+ if (settled) return;
29740
+ settled = true;
29741
+ clearTimeout(timer);
29742
+ resolve(result2);
29743
+ };
29744
+ const timer = setTimeout(() => {
29745
+ console.warn("[lens] annotation overlay rasterization timed out");
29746
+ finish(null);
29747
+ }, SVG_RASTERIZE_TIMEOUT_MS);
29748
+ const img = new Image();
29749
+ img.onload = () => finish(img);
29750
+ img.onerror = () => {
29751
+ console.warn("[lens] annotation overlay failed to rasterize");
29752
+ finish(null);
29753
+ };
29754
+ img.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg);
29755
+ });
29756
+ }
29757
+ function canvasToJpegBlob(canvas) {
29758
+ return new Promise((resolve) => {
29759
+ canvas.toBlob((out) => resolve(out), "image/jpeg", 0.82);
29760
+ });
29761
+ }
29762
+ async function cropBlob(blob2, crop, scale) {
29763
+ try {
29764
+ if (typeof createImageBitmap !== "function" || typeof document === "undefined") return null;
29765
+ const bitmap = await createImageBitmap(blob2);
29766
+ const rect = computeCropRect(crop, scale, bitmap.width, bitmap.height);
29767
+ if (!rect) {
29768
+ bitmap.close?.();
29769
+ return null;
29770
+ }
29771
+ const canvas = document.createElement("canvas");
29772
+ canvas.width = rect.sw;
29773
+ canvas.height = rect.sh;
29774
+ const ctx = canvas.getContext("2d");
29775
+ if (!ctx) {
29776
+ bitmap.close?.();
29777
+ return null;
29778
+ }
29779
+ ctx.drawImage(bitmap, rect.sx, rect.sy, rect.sw, rect.sh, 0, 0, rect.sw, rect.sh);
29780
+ bitmap.close?.();
29781
+ return await canvasToJpegBlob(canvas);
29782
+ } catch (err) {
29783
+ console.warn("[lens] screenshot crop failed:", err);
29784
+ return null;
29785
+ }
29786
+ }
29530
29787
  function parseBrowser(ua) {
29531
29788
  if (!ua) return void 0;
29532
29789
  const checks = [
@@ -29791,7 +30048,11 @@ var LensClient = class {
29791
30048
  const wantErrors = include.errors !== false;
29792
30049
  const wantScreenshot = include.screenshot !== false;
29793
30050
  const wantDom = include.dom !== false;
29794
- const screenshotPromise = this.screenshotEnabled && wantScreenshot ? captureScreenshot(input2.excludeFromScreenshot) : Promise.resolve(null);
30051
+ const screenshotPromise = this.screenshotEnabled && wantScreenshot ? captureScreenshot(
30052
+ input2.excludeFromScreenshot,
30053
+ input2.screenshotCrop,
30054
+ input2.annotationComposite
30055
+ ) : Promise.resolve(null);
29795
30056
  const replayEvents = wantReplay ? this.replay?.snapshot() ?? [] : [];
29796
30057
  const errorEntries = this.errorBuffer.drain();
29797
30058
  const consoleTail = this.consoleBuffer.tail(CONSOLE_TAIL_SIZE);