@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.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  LENS_OVERLAY_HOST_ID,
3
3
  LensClient
4
- } from "./chunk-MTQ3IO4U.js";
4
+ } from "./chunk-JXSK4JG4.js";
5
5
 
6
6
  // ../browser/dist/index.js
7
7
  var DEFAULT_PARAM_NAME = "lens";
@@ -124,170 +124,933 @@ function el(tag, props = {}, children = []) {
124
124
  function clear(node) {
125
125
  while (node.firstChild) node.removeChild(node.firstChild);
126
126
  }
127
- var Annotator = class {
127
+ var THIN_MIN_DIST = 3;
128
+ var MAX_POINTS = 2e3;
129
+ var HIGHLIGHTER_WIDTH = 14;
130
+ var STEP_RADIUS = 12;
131
+ function distance(a, b) {
132
+ return Math.hypot(a.x - b.x, a.y - b.y);
133
+ }
134
+ function shouldKeepPoint(last, candidate, minDist = THIN_MIN_DIST) {
135
+ return distance(last, candidate) >= minDist;
136
+ }
137
+ function perpendicularDistance(p, a, b) {
138
+ const dx = b.x - a.x;
139
+ const dy = b.y - a.y;
140
+ const len2 = dx * dx + dy * dy;
141
+ if (len2 === 0) return distance(p, a);
142
+ let t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2;
143
+ t = Math.max(0, Math.min(1, t));
144
+ return distance(p, { x: a.x + t * dx, y: a.y + t * dy });
145
+ }
146
+ function simplifyPoints(points, tolerance = 1) {
147
+ if (points.length <= 2 || tolerance <= 0) return points.map((p) => ({ ...p }));
148
+ const first = points[0];
149
+ const last = points[points.length - 1];
150
+ let maxDist = 0;
151
+ let index = 0;
152
+ for (let i = 1; i < points.length - 1; i++) {
153
+ const d = perpendicularDistance(points[i], first, last);
154
+ if (d > maxDist) {
155
+ maxDist = d;
156
+ index = i;
157
+ }
158
+ }
159
+ if (maxDist > tolerance) {
160
+ const left = simplifyPoints(points.slice(0, index + 1), tolerance);
161
+ const right = simplifyPoints(points.slice(index), tolerance);
162
+ return left.slice(0, -1).concat(right);
163
+ }
164
+ return [{ ...first }, { ...last }];
165
+ }
166
+ function arrowheadPolygon(x1, y1, x2, y2, size = 14) {
167
+ const angle = Math.atan2(y2 - y1, x2 - x1);
168
+ const spread = Math.PI / 7;
169
+ return [
170
+ { x: x2, y: y2 },
171
+ { x: x2 - size * Math.cos(angle - spread), y: y2 - size * Math.sin(angle - spread) },
172
+ { x: x2 - size * Math.cos(angle + spread), y: y2 - size * Math.sin(angle + spread) }
173
+ ];
174
+ }
175
+ function boundsOfAnnotation(a) {
176
+ switch (a.kind) {
177
+ case "rect":
178
+ case "ellipse":
179
+ case "blur": {
180
+ const x = Math.min(a.x, a.x + a.width);
181
+ const y = Math.min(a.y, a.y + a.height);
182
+ return { x, y, width: Math.abs(a.width), height: Math.abs(a.height) };
183
+ }
184
+ case "arrow":
185
+ case "line": {
186
+ const x = Math.min(a.x1, a.x2);
187
+ const y = Math.min(a.y1, a.y2);
188
+ return { x, y, width: Math.abs(a.x2 - a.x1), height: Math.abs(a.y2 - a.y1) };
189
+ }
190
+ case "pencil":
191
+ case "highlighter": {
192
+ if (a.points.length === 0) return null;
193
+ let minX = Infinity;
194
+ let minY = Infinity;
195
+ let maxX = -Infinity;
196
+ let maxY = -Infinity;
197
+ for (const p of a.points) {
198
+ minX = Math.min(minX, p.x);
199
+ minY = Math.min(minY, p.y);
200
+ maxX = Math.max(maxX, p.x);
201
+ maxY = Math.max(maxY, p.y);
202
+ }
203
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
204
+ }
205
+ case "text": {
206
+ const fontSize = a.fontSize ?? 16;
207
+ const width = Math.max(1, a.text.length * fontSize * 0.6);
208
+ return { x: a.x, y: a.y, width, height: fontSize * 1.2 };
209
+ }
210
+ case "step": {
211
+ return {
212
+ x: a.x - STEP_RADIUS,
213
+ y: a.y - STEP_RADIUS,
214
+ width: STEP_RADIUS * 2,
215
+ height: STEP_RADIUS * 2
216
+ };
217
+ }
218
+ }
219
+ }
220
+ function unionBounds(list) {
221
+ if (list.length === 0) return null;
222
+ let minX = Infinity;
223
+ let minY = Infinity;
224
+ let maxX = -Infinity;
225
+ let maxY = -Infinity;
226
+ for (const b of list) {
227
+ minX = Math.min(minX, b.x);
228
+ minY = Math.min(minY, b.y);
229
+ maxX = Math.max(maxX, b.x + b.width);
230
+ maxY = Math.max(maxY, b.y + b.height);
231
+ }
232
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
233
+ }
234
+ function padAndClampBounds(b, padding, maxWidth, maxHeight) {
235
+ const x = Math.max(0, b.x - padding);
236
+ const y = Math.max(0, b.y - padding);
237
+ const right = Math.min(maxWidth, b.x + b.width + padding);
238
+ const bottom = Math.min(maxHeight, b.y + b.height + padding);
239
+ return { x, y, width: Math.max(0, right - x), height: Math.max(0, bottom - y) };
240
+ }
241
+ 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>`;
242
+ var iconEye = svg(
243
+ '<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"/>'
244
+ );
245
+ var iconCrosshair = svg(
246
+ '<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"/>'
247
+ );
248
+ var iconClose = svg('<line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/>');
249
+ var iconMark = svg(
250
+ '<rect x="3" y="3" width="18" height="18" rx="2" stroke-dasharray="4 3"/>'
251
+ );
252
+ var iconCopy = svg(
253
+ '<rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/>'
254
+ );
255
+ var iconCheck = svg('<polyline points="20 6 9 17 4 12"/>');
256
+ var iconSend = svg('<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>');
257
+ 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>`;
258
+ 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>`;
259
+ var iconRectTool = svg('<rect x="4" y="6" width="16" height="12" rx="1"/>');
260
+ var iconEllipseTool = svg('<ellipse cx="12" cy="12" rx="8" ry="6"/>');
261
+ var iconArrowTool = svg('<line x1="5" y1="19" x2="18" y2="6"/><polyline points="18 12 18 6 12 6"/>');
262
+ var iconLineTool = svg('<line x1="5" y1="19" x2="19" y2="5"/>');
263
+ 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"/>');
264
+ var iconHighlighter = svg(
265
+ '<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"/>'
266
+ );
267
+ var iconText = svg(
268
+ '<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"/>'
269
+ );
270
+ var iconStepTool = svg('<circle cx="12" cy="12" r="9"/><path d="M11.2 9 13 8v8"/>');
271
+ var iconBlur = svg('<path d="M12 3s6 6.4 6 10a6 6 0 0 1-12 0c0-3.6 6-10 6-10Z"/>');
272
+ var iconUndo = svg('<path d="M9 14 4 9l5-5"/><path d="M4 9h11a5 5 0 0 1 0 10h-1"/>');
273
+ var iconTrash = svg(
274
+ '<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"/>'
275
+ );
276
+ var iconAnnotate = iconPencil;
277
+ var SVG_NS = "http://www.w3.org/2000/svg";
278
+ var FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
279
+ var MIN_DRAG = 6;
280
+ var TOOLS = [
281
+ { tool: "rect", icon: iconRectTool, label: "Rectangle" },
282
+ { tool: "ellipse", icon: iconEllipseTool, label: "Ellipse" },
283
+ { tool: "arrow", icon: iconArrowTool, label: "Arrow" },
284
+ { tool: "line", icon: iconLineTool, label: "Line" },
285
+ { tool: "pencil", icon: iconPencil, label: "Pencil" },
286
+ { tool: "highlighter", icon: iconHighlighter, label: "Highlighter" },
287
+ { tool: "text", icon: iconText, label: "Text" },
288
+ { tool: "step", icon: iconStepTool, label: "Step number" },
289
+ { tool: "blur", icon: iconBlur, label: "Blur / redact" }
290
+ ];
291
+ var COLORS = ["#ef4444", "#f59e0b", "#22c55e", "#3b82f6", "#8b5cf6", "#111827"];
292
+ var WIDTHS = [
293
+ { label: "S", value: 2 },
294
+ { label: "M", value: 3.5 },
295
+ { label: "L", value: 6 }
296
+ ];
297
+ var AnnotationEditor = class {
128
298
  constructor(host) {
129
299
  this.host = host;
130
300
  }
131
301
  host;
132
- layer = null;
133
- chips = null;
134
- draft = null;
135
- startX = 0;
136
- startY = 0;
137
- active = false;
138
- dragging = false;
302
+ svg = null;
303
+ bg = null;
304
+ toolbar = null;
305
+ textInputEl = null;
306
+ textAnchor = null;
307
+ textColor = COLORS[0];
139
308
  annotations = [];
140
- renderedRects = [];
309
+ markNodes = [];
310
+ drag = null;
311
+ active = false;
141
312
  sessionBase = 0;
313
+ stepCounter = 0;
314
+ idSeq = 0;
142
315
  onExit = null;
316
+ tool = "rect";
317
+ color = COLORS[0];
318
+ width = 3.5;
319
+ docW = 0;
320
+ docH = 0;
321
+ resizeObserver = null;
322
+ toolBtns = /* @__PURE__ */ new Map();
323
+ colorBtns = /* @__PURE__ */ new Map();
324
+ widthBtns = /* @__PURE__ */ new Map();
325
+ undoBtn = null;
326
+ clearBtn = null;
143
327
  get isActive() {
144
328
  return this.active;
145
329
  }
146
330
  get count() {
147
331
  return this.annotations.length;
148
332
  }
333
+ /** Deep-copied document-coord annotations per the schema contract. */
149
334
  list() {
150
- return this.annotations.map((a) => ({ ...a, viewport: { ...a.viewport } }));
335
+ return this.annotations.map((a) => {
336
+ const copy2 = { ...a };
337
+ if ("points" in copy2 && Array.isArray(copy2.points)) {
338
+ copy2.points = copy2.points.map((p) => ({ ...p }));
339
+ }
340
+ if (copy2.kind === "rect" && copy2.viewport) copy2.viewport = { ...copy2.viewport };
341
+ return copy2;
342
+ });
151
343
  }
344
+ /** Remove every mark. Tears the layer down when not actively drawing. */
152
345
  clear() {
346
+ for (const node of this.markNodes) node.remove();
347
+ this.markNodes = [];
153
348
  this.annotations = [];
154
- for (const rect of this.renderedRects) rect.remove();
155
- this.renderedRects = [];
349
+ this.stepCounter = 0;
350
+ if (!this.active) this.teardownLayer();
351
+ this.syncActionButtons();
156
352
  }
157
353
  start(onExit) {
158
354
  if (this.active) return;
355
+ this.ensureLayer();
356
+ const svg2 = this.svg;
159
357
  this.active = true;
160
358
  this.onExit = onExit;
161
359
  this.sessionBase = this.annotations.length;
162
- this.layer = el("div", { class: "lens-annotate" });
163
- this.layer.addEventListener("pointerdown", this.onDown);
164
- this.chips = el("div", { class: "lens-annotate-chips" }, [
165
- el("span", { class: "lens-annotate-chips__label", text: "Drag to mark an area" }),
360
+ this.stepCounter = 0;
361
+ this.sizeLayer();
362
+ this.updateTransform();
363
+ svg2.style.pointerEvents = "auto";
364
+ svg2.style.touchAction = "none";
365
+ svg2.style.cursor = "crosshair";
366
+ svg2.addEventListener("pointerdown", this.onPointerDown);
367
+ this.buildToolbar();
368
+ window.addEventListener("keydown", this.onKey, true);
369
+ }
370
+ /** Done — keep this session's marks, exit draw mode. */
371
+ exit() {
372
+ if (!this.active) return;
373
+ this.active = false;
374
+ this.commitTextInput();
375
+ if (this.drag) {
376
+ this.drag.node.remove();
377
+ this.detachDragListeners();
378
+ this.drag = null;
379
+ }
380
+ const svg2 = this.svg;
381
+ if (svg2) {
382
+ svg2.removeEventListener("pointerdown", this.onPointerDown);
383
+ svg2.style.pointerEvents = "none";
384
+ svg2.style.touchAction = "";
385
+ svg2.style.cursor = "";
386
+ }
387
+ window.removeEventListener("keydown", this.onKey, true);
388
+ this.removeToolbar();
389
+ const cb = this.onExit;
390
+ this.onExit = null;
391
+ cb?.();
392
+ }
393
+ /** Cancel — discard marks drawn during this session, then exit. */
394
+ cancel() {
395
+ this.cancelTextInput();
396
+ const removed = this.markNodes.splice(this.sessionBase);
397
+ for (const node of removed) node.remove();
398
+ this.annotations.splice(this.sessionBase);
399
+ this.exit();
400
+ }
401
+ undo() {
402
+ const node = this.markNodes.pop();
403
+ if (!node) return;
404
+ node.remove();
405
+ this.annotations.pop();
406
+ this.syncActionButtons();
407
+ }
408
+ // --------------------------------------------------------------------------
409
+ // Composite / crop
410
+ // --------------------------------------------------------------------------
411
+ /**
412
+ * Complete standalone SVG of every NON-blur mark, plus blur regions returned
413
+ * separately. `null` when there are no marks.
414
+ */
415
+ getComposite() {
416
+ if (this.annotations.length === 0 || !this.svg) return null;
417
+ this.sizeLayer();
418
+ const docWidth = this.docW;
419
+ const docHeight = this.docH;
420
+ const clone = this.svg.cloneNode(true);
421
+ clone.removeAttribute("style");
422
+ clone.removeAttribute("class");
423
+ clone.querySelectorAll("[data-lens-chrome],[data-lens-draft],[data-lens-blur]").forEach((n) => n.remove());
424
+ clone.querySelector("defs")?.remove();
425
+ clone.setAttribute("xmlns", SVG_NS);
426
+ clone.setAttribute("viewBox", `0 0 ${docWidth} ${docHeight}`);
427
+ clone.setAttribute("width", String(docWidth));
428
+ clone.setAttribute("height", String(docHeight));
429
+ const svgMarkup = new XMLSerializer().serializeToString(clone);
430
+ const blurRegions = this.annotations.filter((a) => a.kind === "blur").map((a) => ({ x: a.x, y: a.y, width: a.width, height: a.height }));
431
+ return { svgMarkup, docWidth, docHeight, blurRegions };
432
+ }
433
+ /** Union bbox of all marks + 24px padding, clamped to the document. */
434
+ getCropBounds() {
435
+ if (this.annotations.length === 0) return void 0;
436
+ const boxes = [];
437
+ for (let i = 0; i < this.annotations.length; i++) {
438
+ const a = this.annotations[i];
439
+ const b = a.kind === "text" ? this.textBounds(i) : boundsOfAnnotation(a);
440
+ if (b) boxes.push(b);
441
+ }
442
+ const u = unionBounds(boxes);
443
+ if (!u) return void 0;
444
+ this.sizeLayer();
445
+ return padAndClampBounds(u, 24, this.docW, this.docH);
446
+ }
447
+ /** Prefer a live getBBox() for text; fall back to the geometric estimate. */
448
+ textBounds(index) {
449
+ const node = this.markNodes[index];
450
+ const annotation = this.annotations[index];
451
+ if (node && typeof node.getBBox === "function") {
452
+ try {
453
+ const box = node.getBBox();
454
+ if (box.width > 0 && box.height > 0) {
455
+ return { x: box.x, y: box.y, width: box.width, height: box.height };
456
+ }
457
+ } catch {
458
+ }
459
+ }
460
+ return boundsOfAnnotation(annotation);
461
+ }
462
+ // --------------------------------------------------------------------------
463
+ // Layer lifecycle
464
+ // --------------------------------------------------------------------------
465
+ ensureLayer() {
466
+ if (this.svg) return;
467
+ const svg2 = document.createElementNS(SVG_NS, "svg");
468
+ svg2.setAttribute("class", "lens-annotate-svg");
469
+ svg2.setAttribute("xmlns", SVG_NS);
470
+ const defs = document.createElementNS(SVG_NS, "defs");
471
+ defs.setAttribute("data-lens-chrome", "1");
472
+ const pattern = document.createElementNS(SVG_NS, "pattern");
473
+ pattern.setAttribute("id", "lens-blur-hatch");
474
+ pattern.setAttribute("width", "8");
475
+ pattern.setAttribute("height", "8");
476
+ pattern.setAttribute("patternUnits", "userSpaceOnUse");
477
+ pattern.setAttribute("patternTransform", "rotate(45)");
478
+ const patternBase = document.createElementNS(SVG_NS, "rect");
479
+ patternBase.setAttribute("width", "8");
480
+ patternBase.setAttribute("height", "8");
481
+ patternBase.setAttribute("fill", "rgba(17,24,39,0.12)");
482
+ const patternLine = document.createElementNS(SVG_NS, "line");
483
+ patternLine.setAttribute("x1", "0");
484
+ patternLine.setAttribute("y1", "0");
485
+ patternLine.setAttribute("x2", "0");
486
+ patternLine.setAttribute("y2", "8");
487
+ patternLine.setAttribute("stroke", "rgba(17,24,39,0.55)");
488
+ patternLine.setAttribute("stroke-width", "4");
489
+ pattern.append(patternBase, patternLine);
490
+ defs.append(pattern);
491
+ svg2.append(defs);
492
+ const bg = document.createElementNS(SVG_NS, "rect");
493
+ bg.setAttribute("data-lens-chrome", "1");
494
+ bg.setAttribute("x", "0");
495
+ bg.setAttribute("y", "0");
496
+ bg.setAttribute("fill", "transparent");
497
+ bg.style.pointerEvents = "all";
498
+ svg2.append(bg);
499
+ this.svg = svg2;
500
+ this.bg = bg;
501
+ this.host.append(svg2);
502
+ window.addEventListener("scroll", this.onScroll, { passive: true, capture: true });
503
+ window.addEventListener("resize", this.onResize);
504
+ if (typeof ResizeObserver !== "undefined") {
505
+ this.resizeObserver = new ResizeObserver(this.onResize);
506
+ this.resizeObserver.observe(document.documentElement);
507
+ }
508
+ this.sizeLayer();
509
+ this.updateTransform();
510
+ }
511
+ teardownLayer() {
512
+ window.removeEventListener("scroll", this.onScroll, true);
513
+ window.removeEventListener("resize", this.onResize);
514
+ this.resizeObserver?.disconnect();
515
+ this.resizeObserver = null;
516
+ this.svg?.remove();
517
+ this.svg = null;
518
+ this.bg = null;
519
+ }
520
+ docSize() {
521
+ const d = document.documentElement;
522
+ const b = document.body;
523
+ const width = Math.max(d.scrollWidth, b?.scrollWidth ?? 0, d.clientWidth);
524
+ const height = Math.max(d.scrollHeight, b?.scrollHeight ?? 0, d.clientHeight);
525
+ return { width, height };
526
+ }
527
+ sizeLayer() {
528
+ if (!this.svg) return;
529
+ const { width, height } = this.docSize();
530
+ this.docW = width;
531
+ this.docH = height;
532
+ this.svg.setAttribute("width", String(width));
533
+ this.svg.setAttribute("height", String(height));
534
+ this.svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
535
+ this.svg.style.width = `${width}px`;
536
+ this.svg.style.height = `${height}px`;
537
+ if (this.bg) {
538
+ this.bg.setAttribute("width", String(width));
539
+ this.bg.setAttribute("height", String(height));
540
+ }
541
+ }
542
+ updateTransform() {
543
+ if (!this.svg) return;
544
+ const scrollX = window.scrollX || window.pageXOffset || 0;
545
+ const scrollY = window.scrollY || window.pageYOffset || 0;
546
+ this.svg.style.left = `${-scrollX}px`;
547
+ this.svg.style.top = `${-scrollY}px`;
548
+ }
549
+ onScroll = () => this.updateTransform();
550
+ onResize = () => {
551
+ this.sizeLayer();
552
+ this.updateTransform();
553
+ };
554
+ // --------------------------------------------------------------------------
555
+ // Toolbar
556
+ // --------------------------------------------------------------------------
557
+ buildToolbar() {
558
+ this.toolBtns.clear();
559
+ this.colorBtns.clear();
560
+ this.widthBtns.clear();
561
+ const toolGroup = el("div", { class: "lens-tool-group" });
562
+ for (const { tool, icon, label } of TOOLS) {
563
+ const btn = el("button", {
564
+ class: "lens-tool-btn",
565
+ ariaLabel: label,
566
+ title: label,
567
+ html: icon,
568
+ attrs: { type: "button", "aria-pressed": String(tool === this.tool) },
569
+ on: { click: () => this.selectTool(tool) }
570
+ });
571
+ this.toolBtns.set(tool, btn);
572
+ toolGroup.append(btn);
573
+ }
574
+ const colorGroup = el("div", { class: "lens-tool-group lens-swatches" });
575
+ for (const c of COLORS) {
576
+ const btn = el("button", {
577
+ class: "lens-swatch",
578
+ ariaLabel: `Color ${c}`,
579
+ title: c,
580
+ attrs: { type: "button", "aria-pressed": String(c === this.color) },
581
+ style: { background: c },
582
+ on: { click: () => this.selectColor(c) }
583
+ });
584
+ this.colorBtns.set(c, btn);
585
+ colorGroup.append(btn);
586
+ }
587
+ const widthGroup = el("div", { class: "lens-tool-group" });
588
+ for (const { label, value } of WIDTHS) {
589
+ const btn = el("button", {
590
+ class: "lens-width-btn",
591
+ text: label,
592
+ ariaLabel: `Stroke width ${label}`,
593
+ title: `Stroke ${label} (${value}px)`,
594
+ attrs: { type: "button", "aria-pressed": String(value === this.width) },
595
+ on: { click: () => this.selectWidth(value) }
596
+ });
597
+ this.widthBtns.set(value, btn);
598
+ widthGroup.append(btn);
599
+ }
600
+ this.undoBtn = el("button", {
601
+ class: "lens-tool-btn",
602
+ ariaLabel: "Undo",
603
+ title: "Undo",
604
+ html: iconUndo,
605
+ attrs: { type: "button" },
606
+ on: { click: () => this.undo() }
607
+ });
608
+ this.clearBtn = el("button", {
609
+ class: "lens-tool-btn",
610
+ ariaLabel: "Clear all",
611
+ title: "Clear all",
612
+ html: iconTrash,
613
+ attrs: { type: "button" },
614
+ on: { click: () => this.clear() }
615
+ });
616
+ const editGroup = el("div", { class: "lens-tool-group" }, [this.undoBtn, this.clearBtn]);
617
+ const doneGroup = el("div", { class: "lens-tool-group" }, [
166
618
  el("button", {
167
619
  class: "lens-pick-chip__btn",
168
620
  text: "Cancel",
621
+ attrs: { type: "button" },
169
622
  on: { click: () => this.cancel() }
170
623
  }),
171
624
  el("button", {
172
625
  class: "lens-pick-chip__btn lens-pick-chip__btn--primary",
173
626
  text: "Done",
627
+ attrs: { type: "button" },
174
628
  on: { click: () => this.exit() }
175
629
  })
176
630
  ]);
177
- this.host.append(this.layer, this.chips);
178
- window.addEventListener("keydown", this.onKey, true);
631
+ this.toolbar = el(
632
+ "div",
633
+ { class: "lens-annotate-toolbar", attrs: { role: "toolbar", "aria-label": "Annotation tools" } },
634
+ [
635
+ toolGroup,
636
+ el("span", { class: "lens-tool-sep" }),
637
+ colorGroup,
638
+ el("span", { class: "lens-tool-sep" }),
639
+ widthGroup,
640
+ el("span", { class: "lens-tool-sep" }),
641
+ editGroup,
642
+ el("span", { class: "lens-tool-sep" }),
643
+ doneGroup
644
+ ]
645
+ );
646
+ this.host.append(this.toolbar);
647
+ this.syncActionButtons();
648
+ }
649
+ removeToolbar() {
650
+ this.toolbar?.remove();
651
+ this.toolbar = null;
652
+ this.toolBtns.clear();
653
+ this.colorBtns.clear();
654
+ this.widthBtns.clear();
655
+ this.undoBtn = null;
656
+ this.clearBtn = null;
657
+ }
658
+ selectTool(tool) {
659
+ this.commitTextInput();
660
+ this.tool = tool;
661
+ for (const [t, btn] of this.toolBtns) {
662
+ const on = t === tool;
663
+ btn.classList.toggle("is-active", on);
664
+ btn.setAttribute("aria-pressed", String(on));
665
+ }
666
+ if (this.svg) this.svg.style.cursor = tool === "text" ? "text" : "crosshair";
667
+ }
668
+ selectColor(color) {
669
+ this.color = color;
670
+ for (const [c, btn] of this.colorBtns) {
671
+ const on = c === color;
672
+ btn.classList.toggle("is-active", on);
673
+ btn.setAttribute("aria-pressed", String(on));
674
+ }
179
675
  }
180
- /** Discard rectangles drawn during this session, then exit. */
181
- cancel() {
182
- const removed = this.renderedRects.splice(this.sessionBase);
183
- for (const rect of removed) rect.remove();
184
- this.annotations.splice(this.sessionBase);
185
- this.exit();
676
+ selectWidth(value) {
677
+ this.width = value;
678
+ for (const [v, btn] of this.widthBtns) {
679
+ const on = v === value;
680
+ btn.classList.toggle("is-active", on);
681
+ btn.setAttribute("aria-pressed", String(on));
682
+ }
186
683
  }
187
- exit() {
684
+ syncActionButtons() {
685
+ const empty = this.markNodes.length === 0;
686
+ if (this.undoBtn) this.undoBtn.disabled = empty;
687
+ if (this.clearBtn) this.clearBtn.disabled = empty;
688
+ }
689
+ // --------------------------------------------------------------------------
690
+ // Pointer / drawing
691
+ // --------------------------------------------------------------------------
692
+ toDoc(e) {
693
+ return {
694
+ x: e.clientX + (window.scrollX || window.pageXOffset || 0),
695
+ y: e.clientY + (window.scrollY || window.pageYOffset || 0)
696
+ };
697
+ }
698
+ onPointerDown = (e) => {
188
699
  if (!this.active) return;
189
- this.active = false;
190
- this.dragging = false;
191
- window.removeEventListener("keydown", this.onKey, true);
192
- window.removeEventListener("pointermove", this.onMove, true);
193
- window.removeEventListener("pointerup", this.onUp, true);
194
- if (this.draft) {
195
- this.draft.remove();
196
- this.draft = null;
700
+ if (e.button !== void 0 && e.button !== 0) return;
701
+ e.preventDefault();
702
+ const p = this.toDoc(e);
703
+ const tool = this.tool;
704
+ if (tool === "text") {
705
+ this.openTextInput(e.clientX, e.clientY, p);
706
+ return;
197
707
  }
198
- if (this.layer) {
199
- this.layer.removeEventListener("pointerdown", this.onDown);
200
- this.layer.remove();
201
- this.layer = null;
708
+ if (tool === "step") {
709
+ this.placeStep(p);
710
+ return;
202
711
  }
203
- if (this.chips) {
204
- this.chips.remove();
205
- this.chips = null;
712
+ const color = this.color;
713
+ const width = this.width;
714
+ let drag;
715
+ if (tool === "pencil" || tool === "highlighter") {
716
+ const poly = this.svgEl("polyline", {
717
+ "data-lens-draft": "1",
718
+ fill: "none",
719
+ stroke: color,
720
+ "stroke-width": tool === "highlighter" ? HIGHLIGHTER_WIDTH : width,
721
+ "stroke-linecap": "round",
722
+ "stroke-linejoin": "round",
723
+ ...tool === "highlighter" ? { "stroke-opacity": "0.35" } : {},
724
+ points: `${p.x},${p.y}`
725
+ });
726
+ this.svg.append(poly);
727
+ drag = { tool, color, width, start: p, node: poly, points: [p], last: p };
728
+ } else if (tool === "arrow") {
729
+ const g = this.svgEl("g", { "data-lens-draft": "1" });
730
+ const line = this.svgEl("line", {
731
+ stroke: color,
732
+ "stroke-width": width,
733
+ "stroke-linecap": "round"
734
+ });
735
+ const head = this.svgEl("polygon", { fill: color });
736
+ g.append(line, head);
737
+ this.svg.append(g);
738
+ drag = { tool, color, width, start: p, node: g, line, head };
739
+ this.updateArrow(drag, p);
740
+ } else {
741
+ const node = this.createShapeNode(tool, color, width);
742
+ this.svg.append(node);
743
+ drag = { tool, color, width, start: p, node };
744
+ this.updateShape(drag, p);
206
745
  }
207
- const exit = this.onExit;
208
- this.onExit = null;
209
- exit?.();
210
- }
211
- onDown = (e) => {
212
- e.preventDefault();
213
- this.dragging = true;
214
- this.startX = e.clientX;
215
- this.startY = e.clientY;
216
- this.draft = el("div", { class: "lens-rect" });
217
- this.host.append(this.draft);
218
- this.position(this.draft, e.clientX, e.clientY);
219
- window.addEventListener("pointermove", this.onMove, true);
220
- window.addEventListener("pointerup", this.onUp, true);
746
+ this.drag = drag;
747
+ window.addEventListener("pointermove", this.onPointerMove, true);
748
+ window.addEventListener("pointerup", this.onPointerUp, true);
221
749
  };
222
- onMove = (e) => {
223
- if (this.dragging && this.draft) this.position(this.draft, e.clientX, e.clientY);
750
+ onPointerMove = (e) => {
751
+ const d = this.drag;
752
+ if (!d) return;
753
+ const p = this.toDoc(e);
754
+ if (d.tool === "pencil" || d.tool === "highlighter") {
755
+ const pts = d.points;
756
+ if (d.last && shouldKeepPoint(d.last, p) && pts.length < MAX_POINTS) {
757
+ pts.push(p);
758
+ d.last = p;
759
+ d.node.setAttribute("points", this.pointsAttr(pts));
760
+ }
761
+ } else if (d.tool === "arrow") {
762
+ this.updateArrow(d, p);
763
+ } else {
764
+ this.updateShape(d, p);
765
+ }
224
766
  };
225
- onUp = (e) => {
226
- window.removeEventListener("pointermove", this.onMove, true);
227
- window.removeEventListener("pointerup", this.onUp, true);
228
- this.dragging = false;
229
- const x = Math.min(this.startX, e.clientX);
230
- const y = Math.min(this.startY, e.clientY);
231
- const width = Math.abs(e.clientX - this.startX);
232
- const height = Math.abs(e.clientY - this.startY);
233
- if (this.draft) {
234
- this.draft.remove();
235
- this.draft = null;
767
+ onPointerUp = (e) => {
768
+ const d = this.drag;
769
+ this.detachDragListeners();
770
+ this.drag = null;
771
+ if (!d) return;
772
+ const p = this.toDoc(e);
773
+ let annotation = null;
774
+ if (d.tool === "pencil" || d.tool === "highlighter") {
775
+ let pts = d.points;
776
+ if (pts.length >= 2) pts = simplifyPoints(pts, 1);
777
+ if (pts.length < 2) {
778
+ d.node.remove();
779
+ return;
780
+ }
781
+ d.node.setAttribute("points", this.pointsAttr(pts));
782
+ annotation = d.tool === "pencil" ? { kind: "pencil", points: pts, color: d.color, strokeWidth: d.width, id: this.nextId() } : {
783
+ kind: "highlighter",
784
+ points: pts,
785
+ color: d.color,
786
+ strokeWidth: HIGHLIGHTER_WIDTH,
787
+ id: this.nextId()
788
+ };
789
+ } else if (d.tool === "arrow" || d.tool === "line") {
790
+ if (distance(d.start, p) < MIN_DRAG) {
791
+ d.node.remove();
792
+ return;
793
+ }
794
+ if (d.tool === "arrow") {
795
+ this.updateArrow(d, p);
796
+ annotation = {
797
+ kind: "arrow",
798
+ x1: d.start.x,
799
+ y1: d.start.y,
800
+ x2: p.x,
801
+ y2: p.y,
802
+ color: d.color,
803
+ strokeWidth: d.width,
804
+ id: this.nextId()
805
+ };
806
+ } else {
807
+ this.updateShape(d, p);
808
+ annotation = {
809
+ kind: "line",
810
+ x1: d.start.x,
811
+ y1: d.start.y,
812
+ x2: p.x,
813
+ y2: p.y,
814
+ color: d.color,
815
+ strokeWidth: d.width,
816
+ id: this.nextId()
817
+ };
818
+ }
819
+ } else {
820
+ const x = Math.min(d.start.x, p.x);
821
+ const y = Math.min(d.start.y, p.y);
822
+ const width = Math.abs(p.x - d.start.x);
823
+ const height = Math.abs(p.y - d.start.y);
824
+ if (width < MIN_DRAG || height < MIN_DRAG) {
825
+ d.node.remove();
826
+ return;
827
+ }
828
+ this.updateShape(d, p);
829
+ if (d.tool === "rect") {
830
+ annotation = {
831
+ kind: "rect",
832
+ x,
833
+ y,
834
+ width,
835
+ height,
836
+ color: d.color,
837
+ strokeWidth: d.width,
838
+ id: this.nextId()
839
+ };
840
+ } else if (d.tool === "ellipse") {
841
+ annotation = {
842
+ kind: "ellipse",
843
+ x,
844
+ y,
845
+ width,
846
+ height,
847
+ color: d.color,
848
+ strokeWidth: d.width,
849
+ id: this.nextId()
850
+ };
851
+ } else {
852
+ annotation = { kind: "blur", x, y, width, height, id: this.nextId() };
853
+ }
854
+ }
855
+ d.node.removeAttribute("data-lens-draft");
856
+ this.commitMark(annotation, d.node);
857
+ };
858
+ detachDragListeners() {
859
+ window.removeEventListener("pointermove", this.onPointerMove, true);
860
+ window.removeEventListener("pointerup", this.onPointerUp, true);
861
+ }
862
+ commitMark(annotation, node) {
863
+ this.annotations.push(annotation);
864
+ this.markNodes.push(node);
865
+ this.syncActionButtons();
866
+ }
867
+ createShapeNode(tool, color, width) {
868
+ if (tool === "ellipse") {
869
+ return this.svgEl("ellipse", {
870
+ "data-lens-draft": "1",
871
+ fill: "none",
872
+ stroke: color,
873
+ "stroke-width": width
874
+ });
875
+ }
876
+ if (tool === "line") {
877
+ return this.svgEl("line", {
878
+ "data-lens-draft": "1",
879
+ stroke: color,
880
+ "stroke-width": width,
881
+ "stroke-linecap": "round"
882
+ });
236
883
  }
237
- if (width < 6 || height < 6) return;
238
- const rect = el("div", { class: "lens-rect" });
239
- Object.assign(rect.style, {
240
- left: `${x}px`,
241
- top: `${y}px`,
242
- width: `${width}px`,
243
- height: `${height}px`
884
+ if (tool === "blur") {
885
+ return this.svgEl("rect", {
886
+ "data-lens-draft": "1",
887
+ "data-lens-blur": "1",
888
+ fill: "url(#lens-blur-hatch)",
889
+ stroke: "#111827",
890
+ "stroke-width": 1,
891
+ "stroke-dasharray": "4 3"
892
+ });
893
+ }
894
+ return this.svgEl("rect", {
895
+ "data-lens-draft": "1",
896
+ fill: "none",
897
+ stroke: color,
898
+ "stroke-width": width,
899
+ rx: 2
244
900
  });
245
- this.host.append(rect);
246
- this.renderedRects.push(rect);
247
- this.annotations.push({
248
- kind: "rect",
249
- x,
250
- y,
251
- width,
252
- height,
253
- viewport: { width: window.innerWidth, height: window.innerHeight }
901
+ }
902
+ updateShape(d, cur) {
903
+ const x = Math.min(d.start.x, cur.x);
904
+ const y = Math.min(d.start.y, cur.y);
905
+ const w = Math.abs(cur.x - d.start.x);
906
+ const h = Math.abs(cur.y - d.start.y);
907
+ const node = d.node;
908
+ if (d.tool === "ellipse") {
909
+ node.setAttribute("cx", String(x + w / 2));
910
+ node.setAttribute("cy", String(y + h / 2));
911
+ node.setAttribute("rx", String(w / 2));
912
+ node.setAttribute("ry", String(h / 2));
913
+ } else if (d.tool === "line") {
914
+ node.setAttribute("x1", String(d.start.x));
915
+ node.setAttribute("y1", String(d.start.y));
916
+ node.setAttribute("x2", String(cur.x));
917
+ node.setAttribute("y2", String(cur.y));
918
+ } else {
919
+ node.setAttribute("x", String(x));
920
+ node.setAttribute("y", String(y));
921
+ node.setAttribute("width", String(w));
922
+ node.setAttribute("height", String(h));
923
+ }
924
+ }
925
+ updateArrow(d, cur) {
926
+ if (!d.line || !d.head) return;
927
+ d.line.setAttribute("x1", String(d.start.x));
928
+ d.line.setAttribute("y1", String(d.start.y));
929
+ d.line.setAttribute("x2", String(cur.x));
930
+ d.line.setAttribute("y2", String(cur.y));
931
+ const size = Math.max(12, d.width * 3);
932
+ const poly = arrowheadPolygon(d.start.x, d.start.y, cur.x, cur.y, size);
933
+ d.head.setAttribute("points", this.pointsAttr(poly));
934
+ }
935
+ // --------------------------------------------------------------------------
936
+ // Text tool
937
+ // --------------------------------------------------------------------------
938
+ openTextInput(clientX, clientY, docPoint) {
939
+ this.commitTextInput();
940
+ const input = el("input", {
941
+ class: "lens-text-input",
942
+ attrs: { type: "text", "aria-label": "Annotation text" },
943
+ style: { left: `${clientX}px`, top: `${clientY}px`, color: this.color }
254
944
  });
255
- };
256
- position(node, curX, curY) {
257
- const x = Math.min(this.startX, curX);
258
- const y = Math.min(this.startY, curY);
259
- Object.assign(node.style, {
260
- left: `${x}px`,
261
- top: `${y}px`,
262
- width: `${Math.abs(curX - this.startX)}px`,
263
- height: `${Math.abs(curY - this.startY)}px`
945
+ input.addEventListener("blur", () => this.commitTextInput());
946
+ this.host.append(input);
947
+ this.textInputEl = input;
948
+ this.textAnchor = docPoint;
949
+ this.textColor = this.color;
950
+ input.focus();
951
+ }
952
+ commitTextInput() {
953
+ const input = this.textInputEl;
954
+ const anchor = this.textAnchor;
955
+ if (!input || !anchor) return;
956
+ const value = input.value.trim();
957
+ this.textInputEl = null;
958
+ this.textAnchor = null;
959
+ input.remove();
960
+ if (!value) return;
961
+ const node = this.svgEl("text", {
962
+ x: anchor.x,
963
+ y: anchor.y,
964
+ fill: this.textColor,
965
+ "font-size": 16,
966
+ "font-family": FONT,
967
+ "dominant-baseline": "hanging"
264
968
  });
969
+ node.textContent = value;
970
+ this.svg.append(node);
971
+ this.commitMark(
972
+ {
973
+ kind: "text",
974
+ x: anchor.x,
975
+ y: anchor.y,
976
+ text: value,
977
+ fontSize: 16,
978
+ color: this.textColor,
979
+ id: this.nextId()
980
+ },
981
+ node
982
+ );
265
983
  }
984
+ cancelTextInput() {
985
+ if (!this.textInputEl) return;
986
+ this.textInputEl.remove();
987
+ this.textInputEl = null;
988
+ this.textAnchor = null;
989
+ }
990
+ // --------------------------------------------------------------------------
991
+ // Step tool
992
+ // --------------------------------------------------------------------------
993
+ placeStep(p) {
994
+ const n = ++this.stepCounter;
995
+ const g = this.svgEl("g", {});
996
+ const circle = this.svgEl("circle", {
997
+ cx: p.x,
998
+ cy: p.y,
999
+ r: STEP_RADIUS,
1000
+ fill: this.color
1001
+ });
1002
+ const text = this.svgEl("text", {
1003
+ x: p.x,
1004
+ y: p.y,
1005
+ fill: "#ffffff",
1006
+ "font-size": 13,
1007
+ "font-weight": 700,
1008
+ "font-family": FONT,
1009
+ "text-anchor": "middle",
1010
+ "dominant-baseline": "central"
1011
+ });
1012
+ text.textContent = String(n);
1013
+ g.append(circle, text);
1014
+ this.svg.append(g);
1015
+ this.commitMark({ kind: "step", x: p.x, y: p.y, n, color: this.color, id: this.nextId() }, g);
1016
+ }
1017
+ // --------------------------------------------------------------------------
1018
+ // Keyboard
1019
+ // --------------------------------------------------------------------------
266
1020
  onKey = (e) => {
267
- if (e.key !== "Escape") return;
268
- e.preventDefault();
269
- e.stopPropagation();
270
- this.exit();
1021
+ if (this.textInputEl) {
1022
+ if (e.key === "Escape") {
1023
+ e.preventDefault();
1024
+ e.stopPropagation();
1025
+ this.cancelTextInput();
1026
+ } else if (e.key === "Enter") {
1027
+ e.preventDefault();
1028
+ e.stopPropagation();
1029
+ this.commitTextInput();
1030
+ }
1031
+ return;
1032
+ }
1033
+ if (e.key === "Escape") {
1034
+ e.preventDefault();
1035
+ e.stopPropagation();
1036
+ this.exit();
1037
+ }
271
1038
  };
1039
+ // --------------------------------------------------------------------------
1040
+ // Small helpers
1041
+ // --------------------------------------------------------------------------
1042
+ svgEl(tag, attrs) {
1043
+ const node = document.createElementNS(SVG_NS, tag);
1044
+ for (const [k, v] of Object.entries(attrs)) node.setAttribute(k, String(v));
1045
+ return node;
1046
+ }
1047
+ pointsAttr(points) {
1048
+ return points.map((p) => `${p.x},${p.y}`).join(" ");
1049
+ }
1050
+ nextId() {
1051
+ return `lens-${++this.idSeq}`;
1052
+ }
272
1053
  };
273
- 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>`;
274
- var iconEye = svg(
275
- '<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"/>'
276
- );
277
- var iconCrosshair = svg(
278
- '<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"/>'
279
- );
280
- var iconClose = svg('<line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/>');
281
- var iconMark = svg(
282
- '<rect x="3" y="3" width="18" height="18" rx="2" stroke-dasharray="4 3"/>'
283
- );
284
- var iconCopy = svg(
285
- '<rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/>'
286
- );
287
- var iconCheck = svg('<polyline points="20 6 9 17 4 12"/>');
288
- var iconSend = svg('<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>');
289
- 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>`;
290
- 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>`;
291
1054
  var INCLUDE_ITEMS = [
292
1055
  { key: "replay", label: "Replay" },
293
1056
  { key: "console", label: "Console" },
@@ -444,7 +1207,7 @@ var Panel = class {
444
1207
  buildActions() {
445
1208
  const mark = el("button", {
446
1209
  class: "lens-abtn lens-abtn--ghost lens-abtn--compact",
447
- html: iconMark + "<span>Mark area</span>",
1210
+ html: iconAnnotate + "<span>Annotate</span>",
448
1211
  on: { click: () => this.cb.onMarkArea() }
449
1212
  });
450
1213
  const record = el("button", {
@@ -828,21 +1591,33 @@ var OVERLAY_CSS = (
828
1591
  pointer-events: none;
829
1592
  }
830
1593
 
831
- /* ---------------- Annotation layer ---------------- */
832
- .lens-annotate {
833
- position: fixed;
834
- inset: 0;
1594
+ /* ---------------- Annotation SVG layer ---------------- */
1595
+ /* One full-DOCUMENT SVG anchored to page content (translated by -scroll so
1596
+ marks survive scrolling). pointer-events flip to auto only while drawing. */
1597
+ .lens-annotate-svg {
1598
+ position: absolute;
1599
+ left: 0;
1600
+ top: 0;
835
1601
  z-index: calc(var(--lens-z) - 1);
836
- cursor: crosshair;
837
- background: rgba(15, 15, 20, 0.04);
838
- touch-action: none;
1602
+ pointer-events: none;
1603
+ overflow: visible;
839
1604
  }
840
- .lens-rect {
1605
+
1606
+ /* Floating text-entry input for the Text tool (viewport-positioned). */
1607
+ .lens-text-input {
841
1608
  position: fixed;
842
- border: 2px solid var(--lens-accent);
843
- background: rgba(99, 102, 241, 0.1);
844
- border-radius: 3px;
845
- pointer-events: none;
1609
+ z-index: var(--lens-z);
1610
+ min-width: 120px;
1611
+ padding: 2px 6px;
1612
+ border: 1px solid var(--lens-accent);
1613
+ border-radius: 4px;
1614
+ background: rgba(255, 255, 255, 0.96);
1615
+ color: #111827;
1616
+ font-family: var(--lens-font);
1617
+ font-size: 15px;
1618
+ line-height: 1.3;
1619
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
1620
+ outline: none;
846
1621
  }
847
1622
 
848
1623
  /* ---------------- Panel ---------------- */
@@ -1133,6 +1908,114 @@ var OVERLAY_CSS = (
1133
1908
  }
1134
1909
  .lens-annotate-chips { z-index: var(--lens-z); }
1135
1910
 
1911
+ /* ---------------- Annotation toolbar ---------------- */
1912
+ /* Same floating top-center anchor as the chips row; wraps on narrow screens. */
1913
+ .lens-annotate-toolbar {
1914
+ position: fixed;
1915
+ top: calc(16px + env(safe-area-inset-top));
1916
+ left: 50%;
1917
+ transform: translateX(-50%);
1918
+ z-index: var(--lens-z);
1919
+ display: inline-flex;
1920
+ flex-wrap: wrap;
1921
+ align-items: center;
1922
+ justify-content: center;
1923
+ gap: 6px;
1924
+ max-width: calc(100vw - 24px);
1925
+ padding: 6px 8px;
1926
+ border-radius: 14px;
1927
+ background: rgba(20, 20, 23, 0.92);
1928
+ -webkit-backdrop-filter: blur(12px) saturate(180%);
1929
+ backdrop-filter: blur(12px) saturate(180%);
1930
+ border: 1px solid rgba(255, 255, 255, 0.12);
1931
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.35);
1932
+ color: #f4f4f5;
1933
+ }
1934
+ .lens-tool-group {
1935
+ display: inline-flex;
1936
+ align-items: center;
1937
+ gap: 4px;
1938
+ flex-wrap: wrap;
1939
+ }
1940
+ .lens-tool-sep {
1941
+ width: 1px;
1942
+ align-self: stretch;
1943
+ min-height: 22px;
1944
+ background: rgba(255, 255, 255, 0.16);
1945
+ margin: 0 2px;
1946
+ }
1947
+ .lens-tool-btn {
1948
+ display: inline-flex;
1949
+ align-items: center;
1950
+ justify-content: center;
1951
+ width: 30px;
1952
+ height: 30px;
1953
+ padding: 0;
1954
+ border: 1px solid transparent;
1955
+ border-radius: 8px;
1956
+ background: transparent;
1957
+ color: #d4d4d8;
1958
+ cursor: pointer;
1959
+ transition: background 150ms ease, color 150ms ease, border-color 150ms ease;
1960
+ }
1961
+ .lens-tool-btn:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
1962
+ .lens-tool-btn:focus-visible { outline: 2px solid var(--lens-accent); outline-offset: 1px; }
1963
+ .lens-tool-btn.is-active {
1964
+ background: var(--lens-accent);
1965
+ border-color: var(--lens-accent);
1966
+ color: #fff;
1967
+ }
1968
+ .lens-tool-btn:disabled { opacity: 0.4; cursor: default; }
1969
+ .lens-tool-btn:disabled:hover { background: transparent; color: #d4d4d8; }
1970
+
1971
+ .lens-swatch {
1972
+ width: 22px;
1973
+ height: 22px;
1974
+ padding: 0;
1975
+ border-radius: 999px;
1976
+ border: 2px solid transparent;
1977
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.25) inset;
1978
+ cursor: pointer;
1979
+ transition: transform 120ms ease, border-color 120ms ease;
1980
+ }
1981
+ .lens-swatch:hover { transform: scale(1.1); }
1982
+ .lens-swatch:focus-visible { outline: 2px solid #fff; outline-offset: 1px; }
1983
+ .lens-swatch.is-active {
1984
+ border-color: #fff;
1985
+ box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.4);
1986
+ }
1987
+
1988
+ .lens-width-btn {
1989
+ display: inline-flex;
1990
+ align-items: center;
1991
+ justify-content: center;
1992
+ min-width: 26px;
1993
+ height: 30px;
1994
+ padding: 0 6px;
1995
+ border: 1px solid transparent;
1996
+ border-radius: 8px;
1997
+ background: transparent;
1998
+ color: #d4d4d8;
1999
+ font-family: var(--lens-font);
2000
+ font-size: 12px;
2001
+ font-weight: 700;
2002
+ cursor: pointer;
2003
+ transition: background 150ms ease, color 150ms ease, border-color 150ms ease;
2004
+ }
2005
+ .lens-width-btn:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
2006
+ .lens-width-btn:focus-visible { outline: 2px solid var(--lens-accent); outline-offset: 1px; }
2007
+ .lens-width-btn.is-active {
2008
+ background: var(--lens-accent);
2009
+ border-color: var(--lens-accent);
2010
+ color: #fff;
2011
+ }
2012
+
2013
+ /* Coarse pointers: bump the tool targets toward 40px. */
2014
+ @media (pointer: coarse) {
2015
+ .lens-tool-btn, .lens-width-btn { width: 38px; height: 38px; }
2016
+ .lens-swatch { width: 30px; height: 30px; }
2017
+ }
2018
+
1136
2019
  /* Stacking inside the shadow layer: the panel, pill and floating chips always
1137
2020
  sit above the annotation layer/rects and picker highlight, so marked areas
1138
2021
  never paint over interactive UI. */
@@ -1258,7 +2141,7 @@ var Overlay = class {
1258
2141
  this.layer = el("div", { class: "lens-layer" });
1259
2142
  this.shadow.append(this.layer);
1260
2143
  this.picker = new ElementPicker(this.layer, this.host);
1261
- this.annotator = new Annotator(this.layer);
2144
+ this.annotator = new AnnotationEditor(this.layer);
1262
2145
  this.layer.append(this.buildPill());
1263
2146
  document.documentElement.append(this.host);
1264
2147
  window.addEventListener("keydown", this.onGlobalKey, true);
@@ -1426,11 +2309,15 @@ var Overlay = class {
1426
2309
  }
1427
2310
  async submit(comment, include) {
1428
2311
  const annotations = this.annotator.list();
2312
+ const composite = this.annotator.getComposite() ?? void 0;
2313
+ const screenshotCrop = this.annotator.getCropBounds();
1429
2314
  const result = await this.deps.client.submitReport({
1430
2315
  comment: comment || void 0,
1431
2316
  annotations,
1432
2317
  target: this.target ?? void 0,
1433
2318
  excludeFromScreenshot: this.host,
2319
+ screenshotCrop,
2320
+ annotationComposite: composite,
1434
2321
  include
1435
2322
  });
1436
2323
  this.target = null;
@@ -1641,4 +2528,4 @@ export {
1641
2528
  boot,
1642
2529
  autoBoot
1643
2530
  };
1644
- //# sourceMappingURL=chunk-A6SNBIRS.js.map
2531
+ //# sourceMappingURL=chunk-UT7E5LCG.js.map