@ourroadmaps/web-sdk 0.3.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.
@@ -0,0 +1,1251 @@
1
+ import { __publicField } from './chunk-V6TY7KAL.js';
2
+
3
+ // src/overlay/targeting.ts
4
+ function isSelectorTarget(target) {
5
+ return typeof target === "object" && "selector" in target;
6
+ }
7
+ function isCursorTarget(target) {
8
+ return typeof target === "object" && "cursor" in target && target.cursor === true;
9
+ }
10
+ function isCoordinates(target) {
11
+ return typeof target === "object" && "x" in target && "y" in target && !("selector" in target);
12
+ }
13
+ function resolveElement(target) {
14
+ if (isSelectorTarget(target)) {
15
+ return document.querySelector(target.selector);
16
+ }
17
+ return null;
18
+ }
19
+ function getAnchorPosition(rect, anchor = "center") {
20
+ switch (anchor) {
21
+ case "top":
22
+ return { x: rect.left + rect.width / 2, y: rect.top };
23
+ case "bottom":
24
+ return { x: rect.left + rect.width / 2, y: rect.bottom };
25
+ case "left":
26
+ return { x: rect.left, y: rect.top + rect.height / 2 };
27
+ case "right":
28
+ return { x: rect.right, y: rect.top + rect.height / 2 };
29
+ case "top-left":
30
+ return { x: rect.left, y: rect.top };
31
+ case "top-right":
32
+ return { x: rect.right, y: rect.top };
33
+ case "bottom-left":
34
+ return { x: rect.left, y: rect.bottom };
35
+ case "bottom-right":
36
+ return { x: rect.right, y: rect.bottom };
37
+ case "center":
38
+ return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
39
+ }
40
+ }
41
+ function resolveCoordinates(target, cursorPosition) {
42
+ if (isCoordinates(target)) {
43
+ return target;
44
+ }
45
+ if (isCursorTarget(target)) {
46
+ return cursorPosition;
47
+ }
48
+ if (isSelectorTarget(target)) {
49
+ const element = document.querySelector(target.selector);
50
+ if (!element) {
51
+ return target.fallback ?? null;
52
+ }
53
+ const rect = element.getBoundingClientRect();
54
+ const position = getAnchorPosition(rect, target.anchor);
55
+ if (target.offset) {
56
+ position.x += target.offset.x;
57
+ position.y += target.offset.y;
58
+ }
59
+ return position;
60
+ }
61
+ return null;
62
+ }
63
+ function isInViewport(element) {
64
+ const rect = element.getBoundingClientRect();
65
+ return rect.top >= 0 && rect.left >= 0 && rect.bottom <= window.innerHeight && rect.right <= window.innerWidth;
66
+ }
67
+
68
+ // src/overlay/animations.ts
69
+ var DURATIONS = {
70
+ cursorMove: 600,
71
+ cursorClick: 150,
72
+ clickRipple: 400,
73
+ annotationIn: 200,
74
+ annotationOut: 150,
75
+ scrollPause: 300
76
+ };
77
+ var EASINGS = {
78
+ easeOut: "cubic-bezier(0, 0, 0.2, 1)",
79
+ easeInOut: "cubic-bezier(0.4, 0, 0.2, 1)",
80
+ linear: "linear"
81
+ };
82
+ function delay(ms) {
83
+ return new Promise((resolve) => setTimeout(resolve, ms));
84
+ }
85
+ function fadeInKeyframes() {
86
+ return [
87
+ { opacity: 0, transform: "scale(0.9)" },
88
+ { opacity: 1, transform: "scale(1)" }
89
+ ];
90
+ }
91
+ function fadeOutKeyframes() {
92
+ return [{ opacity: 1 }, { opacity: 0 }];
93
+ }
94
+
95
+ // src/overlay/annotations/Base.ts
96
+ var BaseAnnotation = class {
97
+ constructor(id, config, container, reducedMotion) {
98
+ __publicField(this, "id");
99
+ __publicField(this, "config");
100
+ __publicField(this, "element", null);
101
+ __publicField(this, "container");
102
+ __publicField(this, "reducedMotion");
103
+ __publicField(this, "hideTimeout", null);
104
+ __publicField(this, "currentAnimation", null);
105
+ this.id = id;
106
+ this.config = config;
107
+ this.container = container;
108
+ this.reducedMotion = reducedMotion;
109
+ }
110
+ /**
111
+ * Render the annotation with animation
112
+ */
113
+ async show() {
114
+ if (this.config.delay && this.config.delay > 0) {
115
+ await new Promise((r) => setTimeout(r, this.config.delay));
116
+ }
117
+ this.element = this.createElement();
118
+ if (this.config.color) {
119
+ this.element.style.setProperty("--annotation-color", this.config.color);
120
+ }
121
+ this.container.appendChild(this.element);
122
+ const duration = this.reducedMotion ? 1 : DURATIONS.annotationIn;
123
+ this.currentAnimation = this.element.animate(fadeInKeyframes(), {
124
+ duration,
125
+ easing: EASINGS.easeOut,
126
+ fill: "forwards"
127
+ });
128
+ await this.currentAnimation.finished;
129
+ this.currentAnimation = null;
130
+ if (this.config.duration && this.config.duration > 0) {
131
+ this.hideTimeout = setTimeout(() => this.hide(), this.config.duration);
132
+ }
133
+ }
134
+ /**
135
+ * Hide and remove the annotation
136
+ */
137
+ async hide() {
138
+ if (this.hideTimeout) {
139
+ clearTimeout(this.hideTimeout);
140
+ this.hideTimeout = null;
141
+ }
142
+ if (!this.element) return;
143
+ const duration = this.reducedMotion ? 1 : DURATIONS.annotationOut;
144
+ this.currentAnimation = this.element.animate(fadeOutKeyframes(), {
145
+ duration,
146
+ easing: EASINGS.linear,
147
+ fill: "forwards"
148
+ });
149
+ await this.currentAnimation.finished;
150
+ this.currentAnimation = null;
151
+ this.element.remove();
152
+ this.element = null;
153
+ }
154
+ /**
155
+ * Cancel any running animations
156
+ */
157
+ cancelAnimations() {
158
+ this.currentAnimation?.cancel();
159
+ this.currentAnimation = null;
160
+ }
161
+ /**
162
+ * Clean up resources
163
+ */
164
+ destroy() {
165
+ if (this.hideTimeout) {
166
+ clearTimeout(this.hideTimeout);
167
+ }
168
+ this.cancelAnimations();
169
+ this.element?.remove();
170
+ }
171
+ };
172
+
173
+ // src/overlay/annotations/Arrow.ts
174
+ var ARROWHEAD_LENGTH = 12;
175
+ var ARROWHEAD_WIDTH = 8;
176
+ var Arrow = class extends BaseAnnotation {
177
+ constructor(id, config, container, reducedMotion, cursorPosition) {
178
+ super(id, config, container, reducedMotion);
179
+ __publicField(this, "cursorPosition");
180
+ __publicField(this, "pathElement", null);
181
+ __publicField(this, "markerDef", null);
182
+ this.cursorPosition = cursorPosition;
183
+ }
184
+ createElement() {
185
+ const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
186
+ g.classList.add("arrow");
187
+ const markerId = `arrowhead-${this.id}`;
188
+ this.markerDef = document.createElementNS("http://www.w3.org/2000/svg", "defs");
189
+ const marker = document.createElementNS("http://www.w3.org/2000/svg", "marker");
190
+ marker.setAttribute("id", markerId);
191
+ marker.setAttribute("markerWidth", String(ARROWHEAD_LENGTH));
192
+ marker.setAttribute("markerHeight", String(ARROWHEAD_WIDTH));
193
+ marker.setAttribute("refX", String(ARROWHEAD_LENGTH));
194
+ marker.setAttribute("refY", String(ARROWHEAD_WIDTH / 2));
195
+ marker.setAttribute("orient", "auto");
196
+ marker.setAttribute("markerUnits", "userSpaceOnUse");
197
+ const polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
198
+ polygon.setAttribute(
199
+ "points",
200
+ `0,0 ${ARROWHEAD_LENGTH},${ARROWHEAD_WIDTH / 2} 0,${ARROWHEAD_WIDTH}`
201
+ );
202
+ polygon.setAttribute("fill", "currentColor");
203
+ marker.appendChild(polygon);
204
+ this.markerDef.appendChild(marker);
205
+ g.appendChild(this.markerDef);
206
+ this.pathElement = document.createElementNS("http://www.w3.org/2000/svg", "path");
207
+ this.pathElement.setAttribute("marker-end", `url(#${markerId})`);
208
+ this.pathElement.setAttribute("fill", "none");
209
+ this.pathElement.setAttribute("stroke", "currentColor");
210
+ g.appendChild(this.pathElement);
211
+ return g;
212
+ }
213
+ /**
214
+ * Update the cursor position for relative positioning
215
+ */
216
+ setCursorPosition(coords) {
217
+ this.cursorPosition = coords;
218
+ if (this.element) {
219
+ this.updatePosition();
220
+ }
221
+ }
222
+ resolveTargetPoint(target) {
223
+ if (isCoordinates(target)) {
224
+ return resolveCoordinates(target, this.cursorPosition);
225
+ }
226
+ if (isCursorTarget(target)) {
227
+ return this.cursorPosition;
228
+ }
229
+ if (isSelectorTarget(target)) {
230
+ const element = document.querySelector(target.selector);
231
+ if (!element) {
232
+ return target.fallback ?? null;
233
+ }
234
+ const rect = element.getBoundingClientRect();
235
+ return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
236
+ }
237
+ return null;
238
+ }
239
+ updatePosition() {
240
+ if (!this.pathElement) return;
241
+ const from = this.config.from;
242
+ const to = this.config.to;
243
+ const startPoint = this.resolveTargetPoint(from);
244
+ const endPoint = this.resolveTargetPoint(to);
245
+ if (!startPoint || !endPoint) return;
246
+ const d = `M ${startPoint.x} ${startPoint.y} L ${endPoint.x} ${endPoint.y}`;
247
+ this.pathElement.setAttribute("d", d);
248
+ }
249
+ async show() {
250
+ await super.show();
251
+ this.updatePosition();
252
+ }
253
+ destroy() {
254
+ this.pathElement = null;
255
+ this.markerDef = null;
256
+ super.destroy();
257
+ }
258
+ };
259
+
260
+ // src/overlay/annotations/Badge.ts
261
+ var Badge = class extends BaseAnnotation {
262
+ constructor(id, config, container, reducedMotion, cursorPosition) {
263
+ super(id, config, container, reducedMotion);
264
+ __publicField(this, "cursorPosition");
265
+ this.cursorPosition = cursorPosition;
266
+ }
267
+ createElement() {
268
+ const badge = document.createElement("div");
269
+ badge.className = "badge";
270
+ badge.textContent = String(this.config.number);
271
+ const coords = resolveCoordinates(this.config.target, this.cursorPosition);
272
+ if (coords) {
273
+ badge.style.left = `${coords.x}px`;
274
+ badge.style.top = `${coords.y}px`;
275
+ }
276
+ return badge;
277
+ }
278
+ };
279
+
280
+ // src/overlay/annotations/Box.ts
281
+ var DEFAULT_PADDING = 4;
282
+ var DEFAULT_RADIUS = 4;
283
+ var Box = class extends BaseAnnotation {
284
+ constructor(id, config, container, reducedMotion, cursorPosition) {
285
+ super(id, config, container, reducedMotion);
286
+ __publicField(this, "cursorPosition");
287
+ this.cursorPosition = cursorPosition;
288
+ }
289
+ createElement() {
290
+ const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
291
+ rect.classList.add("box");
292
+ rect.setAttribute("rx", String(DEFAULT_RADIUS));
293
+ rect.setAttribute("ry", String(DEFAULT_RADIUS));
294
+ if (this.config.dashed) {
295
+ rect.setAttribute("stroke-dasharray", "8 4");
296
+ }
297
+ return rect;
298
+ }
299
+ /**
300
+ * Update the cursor position for relative positioning
301
+ */
302
+ setCursorPosition(coords) {
303
+ this.cursorPosition = coords;
304
+ if (this.element) {
305
+ this.updatePosition();
306
+ }
307
+ }
308
+ updatePosition() {
309
+ if (!this.element) return;
310
+ const rect = this.element;
311
+ const target = this.config.target;
312
+ const padding = this.config.padding ?? DEFAULT_PADDING;
313
+ let bounds;
314
+ if (isSelectorTarget(target)) {
315
+ const el = document.querySelector(target.selector);
316
+ if (!el) return;
317
+ bounds = el.getBoundingClientRect();
318
+ } else if (isCoordinates(target)) {
319
+ const resolved = resolveCoordinates(target, this.cursorPosition);
320
+ if (!resolved) return;
321
+ const width = this.config.size?.width ?? 100;
322
+ const height = this.config.size?.height ?? 100;
323
+ bounds = new DOMRect(resolved.x, resolved.y, width, height);
324
+ } else {
325
+ return;
326
+ }
327
+ rect.setAttribute("x", String(bounds.left - padding));
328
+ rect.setAttribute("y", String(bounds.top - padding));
329
+ rect.setAttribute("width", String(bounds.width + padding * 2));
330
+ rect.setAttribute("height", String(bounds.height + padding * 2));
331
+ }
332
+ async show() {
333
+ await super.show();
334
+ this.updatePosition();
335
+ }
336
+ };
337
+
338
+ // src/overlay/annotations/Circle.ts
339
+ var DEFAULT_PADDING2 = 4;
340
+ var Circle = class extends BaseAnnotation {
341
+ constructor(id, config, container, reducedMotion, cursorPosition) {
342
+ super(id, config, container, reducedMotion);
343
+ __publicField(this, "cursorPosition");
344
+ this.cursorPosition = cursorPosition;
345
+ }
346
+ createElement() {
347
+ const ellipse = document.createElementNS("http://www.w3.org/2000/svg", "ellipse");
348
+ ellipse.classList.add("circle");
349
+ return ellipse;
350
+ }
351
+ /**
352
+ * Update the cursor position for relative positioning
353
+ */
354
+ setCursorPosition(coords) {
355
+ this.cursorPosition = coords;
356
+ if (this.element) {
357
+ this.updatePosition();
358
+ }
359
+ }
360
+ updatePosition() {
361
+ if (!this.element) return;
362
+ const ellipse = this.element;
363
+ const target = this.config.target;
364
+ const padding = this.config.padding ?? DEFAULT_PADDING2;
365
+ let bounds;
366
+ if (isSelectorTarget(target)) {
367
+ const el = document.querySelector(target.selector);
368
+ if (!el) return;
369
+ bounds = el.getBoundingClientRect();
370
+ } else if (isCoordinates(target)) {
371
+ const resolved = resolveCoordinates(target, this.cursorPosition);
372
+ if (!resolved) return;
373
+ const width = this.config.size?.width ?? 100;
374
+ const height = this.config.size?.height ?? 100;
375
+ bounds = new DOMRect(resolved.x, resolved.y, width, height);
376
+ } else {
377
+ return;
378
+ }
379
+ const cx = bounds.left + bounds.width / 2;
380
+ const cy = bounds.top + bounds.height / 2;
381
+ const rx = bounds.width / 2 + padding;
382
+ const ry = bounds.height / 2 + padding;
383
+ ellipse.setAttribute("cx", String(cx));
384
+ ellipse.setAttribute("cy", String(cy));
385
+ ellipse.setAttribute("rx", String(rx));
386
+ ellipse.setAttribute("ry", String(ry));
387
+ }
388
+ async show() {
389
+ await super.show();
390
+ this.updatePosition();
391
+ }
392
+ };
393
+
394
+ // src/overlay/annotations/Label.ts
395
+ var DEFAULT_GAP = 8;
396
+ var Label = class extends BaseAnnotation {
397
+ constructor(id, config, container, reducedMotion, cursorPosition) {
398
+ super(id, config, container, reducedMotion);
399
+ __publicField(this, "cursorPosition");
400
+ this.cursorPosition = cursorPosition;
401
+ }
402
+ createElement() {
403
+ const el = document.createElement("div");
404
+ el.className = "label";
405
+ el.textContent = this.config.text;
406
+ return el;
407
+ }
408
+ /**
409
+ * Update the cursor position for relative positioning
410
+ */
411
+ setCursorPosition(coords) {
412
+ this.cursorPosition = coords;
413
+ if (this.element) {
414
+ this.updatePosition();
415
+ }
416
+ }
417
+ updatePosition() {
418
+ if (!this.element) return;
419
+ const target = this.config.target;
420
+ const position = this.config.position || "top";
421
+ const gap = this.config.gap ?? DEFAULT_GAP;
422
+ let anchorRect;
423
+ if (isCursorTarget(target)) {
424
+ if (!this.cursorPosition) return;
425
+ anchorRect = new DOMRect(this.cursorPosition.x, this.cursorPosition.y, 0, 0);
426
+ } else if (isCoordinates(target)) {
427
+ const resolved = resolveCoordinates(target, this.cursorPosition);
428
+ if (!resolved) return;
429
+ anchorRect = new DOMRect(resolved.x, resolved.y, 0, 0);
430
+ } else if (isSelectorTarget(target)) {
431
+ const el = document.querySelector(target.selector);
432
+ if (!el) return;
433
+ anchorRect = el.getBoundingClientRect();
434
+ } else {
435
+ return;
436
+ }
437
+ const coords = this.calculatePosition(anchorRect, position, gap);
438
+ this.element.style.left = `${coords.x}px`;
439
+ this.element.style.top = `${coords.y}px`;
440
+ this.element.style.transform = this.getTransform(position);
441
+ }
442
+ calculatePosition(rect, position, gap) {
443
+ const centerX = rect.left + rect.width / 2;
444
+ const centerY = rect.top + rect.height / 2;
445
+ switch (position) {
446
+ case "top":
447
+ return { x: centerX, y: rect.top - gap };
448
+ case "top-left":
449
+ return { x: rect.left, y: rect.top - gap };
450
+ case "top-right":
451
+ return { x: rect.right, y: rect.top - gap };
452
+ case "bottom":
453
+ return { x: centerX, y: rect.bottom + gap };
454
+ case "bottom-left":
455
+ return { x: rect.left, y: rect.bottom + gap };
456
+ case "bottom-right":
457
+ return { x: rect.right, y: rect.bottom + gap };
458
+ case "left":
459
+ return { x: rect.left - gap, y: centerY };
460
+ case "right":
461
+ return { x: rect.right + gap, y: centerY };
462
+ default:
463
+ return { x: centerX, y: rect.top - gap };
464
+ }
465
+ }
466
+ getTransform(position) {
467
+ switch (position) {
468
+ case "top":
469
+ case "bottom":
470
+ return "translate(-50%, -100%)";
471
+ case "top-left":
472
+ case "bottom-left":
473
+ return "translate(0, -100%)";
474
+ case "top-right":
475
+ case "bottom-right":
476
+ return "translate(-100%, -100%)";
477
+ case "left":
478
+ return "translate(-100%, -50%)";
479
+ case "right":
480
+ return "translate(0, -50%)";
481
+ default:
482
+ return "translate(-50%, -100%)";
483
+ }
484
+ }
485
+ async show() {
486
+ await super.show();
487
+ this.updatePosition();
488
+ }
489
+ };
490
+
491
+ // src/overlay/AnnotationManager.ts
492
+ var idCounter = 0;
493
+ function generateId() {
494
+ return `ann-${++idCounter}`;
495
+ }
496
+ var AnnotationManager = class {
497
+ constructor(config, domContainer, svgContainer, reducedMotion, getCursorPosition) {
498
+ __publicField(this, "config");
499
+ __publicField(this, "domContainer");
500
+ __publicField(this, "svgContainer");
501
+ __publicField(this, "annotations", /* @__PURE__ */ new Map());
502
+ __publicField(this, "reducedMotion");
503
+ __publicField(this, "getCursorPosition");
504
+ this.config = config;
505
+ this.domContainer = domContainer;
506
+ this.svgContainer = svgContainer;
507
+ this.reducedMotion = reducedMotion;
508
+ this.getCursorPosition = getCursorPosition;
509
+ }
510
+ get activeIds() {
511
+ return Array.from(this.annotations.keys());
512
+ }
513
+ show(input) {
514
+ if (Array.isArray(input)) {
515
+ return input.map((a) => this.showOne(a));
516
+ }
517
+ return this.showOne(input);
518
+ }
519
+ showOne(annotation) {
520
+ const id = annotation.id ?? generateId();
521
+ const cursorPos = this.getCursorPosition();
522
+ const annotationWithDefaults = {
523
+ ...annotation,
524
+ color: annotation.color ?? this.config.color
525
+ };
526
+ let instance;
527
+ switch (annotation.type) {
528
+ case "arrow":
529
+ instance = new Arrow(
530
+ id,
531
+ annotationWithDefaults,
532
+ this.svgContainer,
533
+ // SVG for arrows
534
+ this.reducedMotion,
535
+ cursorPos
536
+ );
537
+ break;
538
+ case "badge":
539
+ instance = new Badge(
540
+ id,
541
+ annotationWithDefaults,
542
+ this.domContainer,
543
+ this.reducedMotion,
544
+ cursorPos
545
+ );
546
+ break;
547
+ case "box":
548
+ instance = new Box(
549
+ id,
550
+ annotationWithDefaults,
551
+ this.svgContainer,
552
+ // SVG for boxes
553
+ this.reducedMotion,
554
+ cursorPos
555
+ );
556
+ break;
557
+ case "circle":
558
+ instance = new Circle(
559
+ id,
560
+ annotationWithDefaults,
561
+ this.svgContainer,
562
+ // SVG for circles
563
+ this.reducedMotion,
564
+ cursorPos
565
+ );
566
+ break;
567
+ case "label":
568
+ instance = new Label(
569
+ id,
570
+ annotationWithDefaults,
571
+ this.domContainer,
572
+ this.reducedMotion,
573
+ cursorPos
574
+ );
575
+ break;
576
+ default:
577
+ throw new Error(`Unknown annotation type: ${annotation.type}`);
578
+ }
579
+ this.annotations.set(id, instance);
580
+ instance.show();
581
+ return id;
582
+ }
583
+ hide(input) {
584
+ const ids = Array.isArray(input) ? input : [input];
585
+ for (const id of ids) {
586
+ const annotation = this.annotations.get(id);
587
+ if (annotation) {
588
+ annotation.hide();
589
+ this.annotations.delete(id);
590
+ }
591
+ }
592
+ }
593
+ hideAll() {
594
+ for (const annotation of this.annotations.values()) {
595
+ annotation.hide();
596
+ }
597
+ this.annotations.clear();
598
+ }
599
+ // Convenience methods
600
+ arrow(from, to, options) {
601
+ return this.show({ type: "arrow", from, to, ...options });
602
+ }
603
+ badge(target, number, options) {
604
+ return this.show({ type: "badge", target, number, ...options });
605
+ }
606
+ box(target, options) {
607
+ return this.show({ type: "box", target, ...options });
608
+ }
609
+ circle(target, options) {
610
+ return this.show({ type: "circle", target, ...options });
611
+ }
612
+ label(target, text, options) {
613
+ return this.show({ type: "label", target, text, ...options });
614
+ }
615
+ cancelAllAnimations() {
616
+ for (const annotation of this.annotations.values()) {
617
+ annotation.cancelAnimations();
618
+ }
619
+ }
620
+ destroy() {
621
+ for (const annotation of this.annotations.values()) {
622
+ annotation.destroy();
623
+ }
624
+ this.annotations.clear();
625
+ }
626
+ };
627
+
628
+ // src/shared/colors.ts
629
+ function hexToRgb(hex) {
630
+ const cleaned = hex.replace("#", "");
631
+ const bigint = parseInt(cleaned, 16);
632
+ return [bigint >> 16 & 255, bigint >> 8 & 255, bigint & 255];
633
+ }
634
+ function getLuminance(hex) {
635
+ const [r, g, b] = hexToRgb(hex).map((c) => {
636
+ const sRGB = c / 255;
637
+ return sRGB <= 0.03928 ? sRGB / 12.92 : ((sRGB + 0.055) / 1.055) ** 2.4;
638
+ });
639
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
640
+ }
641
+ function getContrastColor(hex) {
642
+ return getLuminance(hex) > 0.5 ? "black" : "white";
643
+ }
644
+
645
+ // src/overlay/scroller.ts
646
+ async function scrollIntoViewIfNeeded(element) {
647
+ const rect = element.getBoundingClientRect();
648
+ const isFullyVisible = rect.top >= 0 && rect.left >= 0 && rect.bottom <= window.innerHeight && rect.right <= window.innerWidth;
649
+ if (isFullyVisible) {
650
+ return;
651
+ }
652
+ return new Promise((resolve) => {
653
+ element.scrollIntoView({
654
+ behavior: "smooth",
655
+ block: "center",
656
+ inline: "center"
657
+ });
658
+ const observer = new IntersectionObserver(
659
+ (entries) => {
660
+ const entry = entries[0];
661
+ if (entry?.isIntersecting) {
662
+ observer.disconnect();
663
+ resolve();
664
+ }
665
+ },
666
+ { threshold: 0.5 }
667
+ );
668
+ observer.observe(element);
669
+ setTimeout(() => {
670
+ observer.disconnect();
671
+ resolve();
672
+ }, 1e3);
673
+ });
674
+ }
675
+
676
+ // src/overlay/Cursor.ts
677
+ var CURSOR_ARROW_SVG = `<svg class="cursor-arrow" viewBox="0 0 24 24">
678
+ <path d="M5.5 3.21V20.8c0 .45.54.67.85.35l4.86-4.86a.5.5 0 0 1 .35-.15h6.87a.5.5 0 0 0 .35-.85L6.35 2.85a.5.5 0 0 0-.85.36Z"/>
679
+ </svg>`;
680
+ var Cursor = class {
681
+ constructor(container, reducedMotion) {
682
+ __publicField(this, "container");
683
+ __publicField(this, "element");
684
+ __publicField(this, "labelElement");
685
+ __publicField(this, "rippleElement");
686
+ __publicField(this, "currentAnimation", null);
687
+ __publicField(this, "activeRipples", /* @__PURE__ */ new Set());
688
+ __publicField(this, "_position", null);
689
+ __publicField(this, "_isVisible", true);
690
+ __publicField(this, "reducedMotion");
691
+ this.container = container;
692
+ this.reducedMotion = reducedMotion;
693
+ this.element = document.createElement("div");
694
+ this.element.className = "cursor";
695
+ this.element.innerHTML = `${CURSOR_ARROW_SVG}<div class="cursor-label">Guide</div>`;
696
+ this.labelElement = this.element.querySelector(".cursor-label");
697
+ this.rippleElement = document.createElement("div");
698
+ this.rippleElement.className = "click-ripple";
699
+ container.appendChild(this.element);
700
+ container.appendChild(this.rippleElement);
701
+ }
702
+ get position() {
703
+ return this._position;
704
+ }
705
+ get isVisible() {
706
+ return this._isVisible;
707
+ }
708
+ show() {
709
+ this._isVisible = true;
710
+ this.element.classList.remove("hidden");
711
+ }
712
+ hide() {
713
+ this._isVisible = false;
714
+ this.element.classList.add("hidden");
715
+ }
716
+ setName(name) {
717
+ this.labelElement.textContent = name;
718
+ }
719
+ setColor(color) {
720
+ this.element.style.setProperty("--cursor-color", color);
721
+ this.element.style.setProperty("--cursor-text-color", getContrastColor(color));
722
+ }
723
+ async moveTo(target, options) {
724
+ const el = resolveElement(target);
725
+ if (el && !isInViewport(el)) {
726
+ await scrollIntoViewIfNeeded(el);
727
+ await delay(DURATIONS.scrollPause);
728
+ }
729
+ const coords = resolveCoordinates(target, this._position);
730
+ if (!coords) {
731
+ console.warn("[Overlay] Target not found:", target);
732
+ return;
733
+ }
734
+ this.currentAnimation?.cancel();
735
+ const duration = this.reducedMotion ? 1 : options?.duration ?? DURATIONS.cursorMove;
736
+ const from = this._position ?? coords;
737
+ this.currentAnimation = this.element.animate(
738
+ [
739
+ { transform: `translate(${from.x}px, ${from.y}px)` },
740
+ { transform: `translate(${coords.x}px, ${coords.y}px)` }
741
+ ],
742
+ { duration, easing: EASINGS.easeInOut, fill: "forwards" }
743
+ );
744
+ try {
745
+ await this.currentAnimation.finished;
746
+ this._position = coords;
747
+ } catch {
748
+ } finally {
749
+ this.currentAnimation = null;
750
+ }
751
+ }
752
+ async click(target) {
753
+ if (target) {
754
+ await this.moveTo(target);
755
+ }
756
+ if (!this._position) return;
757
+ const duration = this.reducedMotion ? 1 : DURATIONS.cursorClick;
758
+ const pressAnimation = this.element.animate(
759
+ [
760
+ { transform: `translate(${this._position.x}px, ${this._position.y}px) scale(1)` },
761
+ { transform: `translate(${this._position.x}px, ${this._position.y}px) scale(0.85)` },
762
+ { transform: `translate(${this._position.x}px, ${this._position.y}px) scale(1)` }
763
+ ],
764
+ { duration, easing: EASINGS.easeOut }
765
+ );
766
+ this.showRipple(this._position);
767
+ await pressAnimation.finished;
768
+ }
769
+ showRipple(position) {
770
+ const ripple = this.rippleElement.cloneNode();
771
+ ripple.style.left = `${position.x}px`;
772
+ ripple.style.top = `${position.y}px`;
773
+ this.container.appendChild(ripple);
774
+ this.activeRipples.add(ripple);
775
+ const duration = this.reducedMotion ? 1 : DURATIONS.clickRipple;
776
+ const animation = ripple.animate(
777
+ [
778
+ { opacity: 0.4, transform: "translate(-50%, -50%) scale(0)" },
779
+ { opacity: 0, transform: "translate(-50%, -50%) scale(2)" }
780
+ ],
781
+ { duration, easing: EASINGS.easeOut }
782
+ );
783
+ animation.onfinish = () => {
784
+ ripple.remove();
785
+ this.activeRipples.delete(ripple);
786
+ };
787
+ }
788
+ cancelAnimations() {
789
+ this.currentAnimation?.cancel();
790
+ this.currentAnimation = null;
791
+ for (const r of this.activeRipples) {
792
+ r.remove();
793
+ }
794
+ this.activeRipples.clear();
795
+ }
796
+ destroy() {
797
+ this.cancelAnimations();
798
+ this.element.remove();
799
+ this.rippleElement.remove();
800
+ }
801
+ };
802
+
803
+ // src/overlay/styles.ts
804
+ var OVERLAY_STYLES = `
805
+ :host {
806
+ all: initial;
807
+ }
808
+
809
+ .overlay-container {
810
+ position: fixed;
811
+ inset: 0;
812
+ pointer-events: none;
813
+ z-index: var(--overlay-z-index, 9999);
814
+ overflow: hidden;
815
+ }
816
+
817
+ /* Cursor */
818
+ .cursor {
819
+ position: absolute;
820
+ display: flex;
821
+ align-items: flex-start;
822
+ gap: 2px;
823
+ filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));
824
+ will-change: transform;
825
+ transition: opacity 0.15s ease-out;
826
+ }
827
+
828
+ .cursor.hidden {
829
+ opacity: 0;
830
+ pointer-events: none;
831
+ }
832
+
833
+ .cursor-arrow {
834
+ width: 20px;
835
+ height: 20px;
836
+ fill: var(--cursor-color, #3B82F6);
837
+ }
838
+
839
+ .cursor-label {
840
+ background: var(--cursor-color, #3B82F6);
841
+ color: var(--cursor-text-color, white);
842
+ font-family: system-ui, -apple-system, sans-serif;
843
+ font-size: 12px;
844
+ font-weight: 500;
845
+ padding: 4px 8px;
846
+ border-radius: 4px;
847
+ white-space: nowrap;
848
+ margin-top: 12px;
849
+ }
850
+
851
+ /* Click Ripple */
852
+ .click-ripple {
853
+ position: absolute;
854
+ width: 24px;
855
+ height: 24px;
856
+ border-radius: 50%;
857
+ background: var(--cursor-color, #3B82F6);
858
+ opacity: 0;
859
+ transform: translate(-50%, -50%) scale(0);
860
+ pointer-events: none;
861
+ }
862
+
863
+ /* Annotations SVG Layer */
864
+ .annotations-svg {
865
+ position: absolute;
866
+ inset: 0;
867
+ width: 100%;
868
+ height: 100%;
869
+ overflow: visible;
870
+ }
871
+
872
+ /* Annotations DOM Layer */
873
+ .annotations-dom {
874
+ position: absolute;
875
+ inset: 0;
876
+ }
877
+
878
+ /* Badge */
879
+ .badge {
880
+ position: absolute;
881
+ width: 28px;
882
+ height: 28px;
883
+ border-radius: 50%;
884
+ background: var(--annotation-color, #EF4444);
885
+ color: white;
886
+ font-family: system-ui, -apple-system, sans-serif;
887
+ font-size: 14px;
888
+ font-weight: 600;
889
+ display: flex;
890
+ align-items: center;
891
+ justify-content: center;
892
+ transform: translate(-50%, -50%);
893
+ }
894
+
895
+ /* Label */
896
+ .label {
897
+ position: absolute;
898
+ background: var(--annotation-color, #EF4444);
899
+ color: white;
900
+ font-family: system-ui, -apple-system, sans-serif;
901
+ font-size: 14px;
902
+ font-weight: 500;
903
+ padding: 6px 12px;
904
+ border-radius: 6px;
905
+ max-width: 280px;
906
+ overflow: hidden;
907
+ text-overflow: ellipsis;
908
+ display: -webkit-box;
909
+ -webkit-line-clamp: 3;
910
+ -webkit-box-orient: vertical;
911
+ }
912
+
913
+ /* Note: Box, Circle, and Arrow annotations use SVG elements
914
+ styled via attributes, not CSS classes */
915
+
916
+ /* Reduced Motion */
917
+ @media (prefers-reduced-motion: reduce) {
918
+ *, *::before, *::after {
919
+ animation-duration: 0.01ms !important;
920
+ transition-duration: 0.01ms !important;
921
+ }
922
+ }
923
+ `;
924
+
925
+ // src/overlay/Timeline.ts
926
+ var Timeline = class {
927
+ constructor(cursor, annotations, callbacks) {
928
+ __publicField(this, "cursor");
929
+ __publicField(this, "annotations");
930
+ __publicField(this, "callbacks");
931
+ __publicField(this, "isRunning", false);
932
+ __publicField(this, "currentIndex", 0);
933
+ __publicField(this, "abortController", null);
934
+ this.cursor = cursor;
935
+ this.annotations = annotations;
936
+ this.callbacks = callbacks;
937
+ }
938
+ get running() {
939
+ return this.isRunning;
940
+ }
941
+ get actionIndex() {
942
+ return this.isRunning ? this.currentIndex : null;
943
+ }
944
+ async play(script) {
945
+ if (this.isRunning) {
946
+ console.warn("[Overlay] Stopping current playback to start new script");
947
+ this.stop();
948
+ }
949
+ this.isRunning = true;
950
+ this.currentIndex = 0;
951
+ this.abortController = new AbortController();
952
+ if (script.cursor) {
953
+ if (script.cursor.name) this.cursor.setName(script.cursor.name);
954
+ if (script.cursor.color) this.cursor.setColor(script.cursor.color);
955
+ if (script.cursor.visible === false) this.cursor.hide();
956
+ else this.cursor.show();
957
+ }
958
+ this.callbacks.onStart();
959
+ try {
960
+ for (let i = 0; i < script.actions.length; i++) {
961
+ if (!this.isRunning) break;
962
+ this.currentIndex = i;
963
+ const action = script.actions[i];
964
+ this.callbacks.onAction(action, i, "start");
965
+ try {
966
+ await this.executeAction(action);
967
+ } catch (err) {
968
+ if (err instanceof Error && err.name === "AbortError") {
969
+ break;
970
+ }
971
+ this.callbacks.onError(err, action);
972
+ }
973
+ this.callbacks.onAction(action, i, "complete");
974
+ }
975
+ if (this.isRunning) {
976
+ this.callbacks.onComplete();
977
+ }
978
+ } finally {
979
+ this.isRunning = false;
980
+ this.abortController = null;
981
+ }
982
+ }
983
+ stop() {
984
+ if (!this.isRunning) return;
985
+ this.isRunning = false;
986
+ this.abortController?.abort();
987
+ this.cursor.cancelAnimations();
988
+ this.annotations.cancelAllAnimations();
989
+ this.callbacks.onStop();
990
+ }
991
+ async executeAction(action) {
992
+ switch (action.type) {
993
+ case "move":
994
+ await this.executeMove(action);
995
+ break;
996
+ case "click":
997
+ await this.executeClick(action);
998
+ break;
999
+ case "wait":
1000
+ await this.executeWait(action);
1001
+ break;
1002
+ case "setCursor":
1003
+ this.executeSetCursor(action);
1004
+ break;
1005
+ case "showAnnotations":
1006
+ this.executeShowAnnotations(action);
1007
+ break;
1008
+ case "hideAnnotations":
1009
+ this.executeHideAnnotations(action);
1010
+ break;
1011
+ default:
1012
+ console.warn("[Overlay] Unknown action type:", action.type);
1013
+ }
1014
+ }
1015
+ async executeMove(action) {
1016
+ await this.cursor.moveTo(action.target, { duration: action.duration });
1017
+ if (action.showAnnotations) {
1018
+ this.annotations.show(action.showAnnotations);
1019
+ }
1020
+ }
1021
+ async executeClick(action) {
1022
+ await this.cursor.click(action.target);
1023
+ }
1024
+ async executeWait(action) {
1025
+ await delay(action.duration);
1026
+ }
1027
+ executeSetCursor(action) {
1028
+ if (action.visible !== void 0) {
1029
+ if (action.visible) this.cursor.show();
1030
+ else this.cursor.hide();
1031
+ }
1032
+ if (action.name) this.cursor.setName(action.name);
1033
+ if (action.color) this.cursor.setColor(action.color);
1034
+ }
1035
+ executeShowAnnotations(action) {
1036
+ this.annotations.show(action.annotations);
1037
+ }
1038
+ executeHideAnnotations(action) {
1039
+ if (action.ids) {
1040
+ this.annotations.hide(action.ids);
1041
+ } else {
1042
+ this.annotations.hideAll();
1043
+ }
1044
+ }
1045
+ destroy() {
1046
+ this.stop();
1047
+ }
1048
+ };
1049
+
1050
+ // src/overlay/Overlay.ts
1051
+ var DEFAULT_CURSOR_COLOR = "#3B82F6";
1052
+ var DEFAULT_ANNOTATION_COLOR = "#EF4444";
1053
+ var DEFAULT_Z_INDEX = 9999;
1054
+ var _Overlay = class _Overlay {
1055
+ constructor(options = {}) {
1056
+ __publicField(this, "root");
1057
+ __publicField(this, "shadow");
1058
+ __publicField(this, "container");
1059
+ __publicField(this, "svgContainer");
1060
+ __publicField(this, "domContainer");
1061
+ __publicField(this, "_cursor");
1062
+ __publicField(this, "_annotations");
1063
+ __publicField(this, "timeline");
1064
+ __publicField(this, "reducedMotion");
1065
+ __publicField(this, "motionQuery");
1066
+ __publicField(this, "eventHandlers", /* @__PURE__ */ new Map());
1067
+ __publicField(this, "_isDestroyed", false);
1068
+ // Note: Reduced motion preference is checked at construction time for cursor/annotations.
1069
+ // This handler updates our flag but doesn't propagate to already-constructed components.
1070
+ // This is acceptable since reduced motion is typically set before page interaction.
1071
+ __publicField(this, "handleMotionChange", (e) => {
1072
+ this.reducedMotion = e.matches;
1073
+ });
1074
+ const parentContainer = options.container ?? document.body;
1075
+ const zIndex = options.zIndex ?? DEFAULT_Z_INDEX;
1076
+ this.motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
1077
+ this.reducedMotion = this.motionQuery.matches;
1078
+ this.motionQuery.addEventListener("change", this.handleMotionChange);
1079
+ this.root = document.createElement("div");
1080
+ this.root.id = "ourroadmaps-overlay";
1081
+ this.shadow = this.root.attachShadow({ mode: "open" });
1082
+ const styleEl = document.createElement("style");
1083
+ styleEl.textContent = OVERLAY_STYLES;
1084
+ this.shadow.appendChild(styleEl);
1085
+ this.container = document.createElement("div");
1086
+ this.container.className = "overlay-container";
1087
+ this.container.style.setProperty("--overlay-z-index", String(zIndex));
1088
+ this.shadow.appendChild(this.container);
1089
+ this.svgContainer = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1090
+ this.svgContainer.setAttribute("class", "annotations-svg");
1091
+ this.container.appendChild(this.svgContainer);
1092
+ this.domContainer = document.createElement("div");
1093
+ this.domContainer.className = "annotations-dom";
1094
+ this.container.appendChild(this.domContainer);
1095
+ const cursorConfig = {
1096
+ name: options.cursor?.name ?? "Guide",
1097
+ color: options.cursor?.color ?? DEFAULT_CURSOR_COLOR,
1098
+ visible: options.cursor?.visible ?? true
1099
+ };
1100
+ this._cursor = new Cursor(this.container, this.reducedMotion);
1101
+ this._cursor.setName(cursorConfig.name);
1102
+ this._cursor.setColor(cursorConfig.color);
1103
+ if (!cursorConfig.visible) this._cursor.hide();
1104
+ const annotationsConfig = {
1105
+ color: options.annotations?.color ?? DEFAULT_ANNOTATION_COLOR,
1106
+ strokeWidth: options.annotations?.strokeWidth ?? 3
1107
+ };
1108
+ this._annotations = new AnnotationManager(
1109
+ annotationsConfig,
1110
+ this.domContainer,
1111
+ this.svgContainer,
1112
+ this.reducedMotion,
1113
+ () => this._cursor.position
1114
+ );
1115
+ this.timeline = new Timeline(this._cursor, this._annotations, {
1116
+ onStart: () => this.emit("start"),
1117
+ onComplete: () => this.emit("complete"),
1118
+ onStop: () => this.emit("stop"),
1119
+ onError: (error, action) => {
1120
+ this.emit("error", {
1121
+ code: "ANIMATION_FAILED",
1122
+ message: error.message,
1123
+ action
1124
+ });
1125
+ },
1126
+ onAction: (action, index, phase) => this.emit("action", action, index, phase)
1127
+ });
1128
+ parentContainer.appendChild(this.root);
1129
+ }
1130
+ // Controller interface
1131
+ get cursor() {
1132
+ return this._cursor;
1133
+ }
1134
+ get annotations() {
1135
+ return this._annotations;
1136
+ }
1137
+ get isPlaying() {
1138
+ return this.timeline.running;
1139
+ }
1140
+ get currentActionIndex() {
1141
+ return this.timeline.actionIndex;
1142
+ }
1143
+ get isDestroyed() {
1144
+ return this._isDestroyed;
1145
+ }
1146
+ validateAction(action, index) {
1147
+ const errors = [];
1148
+ if (!action || typeof action !== "object") {
1149
+ errors.push(`Action ${index} must be an object`);
1150
+ return errors;
1151
+ }
1152
+ const a = action;
1153
+ if (!_Overlay.VALID_ACTION_TYPES.includes(a.type)) {
1154
+ errors.push(`Action ${index} has invalid type: ${a.type}`);
1155
+ }
1156
+ if (a.type === "wait" && typeof a.duration !== "number") {
1157
+ errors.push(`Wait action ${index} must have a numeric duration`);
1158
+ }
1159
+ if (a.type === "move" && !a.target) {
1160
+ errors.push(`Move action ${index} must have a target`);
1161
+ }
1162
+ return errors;
1163
+ }
1164
+ validate(script) {
1165
+ const errors = [];
1166
+ const warnings = [];
1167
+ if (!script || typeof script !== "object") {
1168
+ errors.push("Script must be an object");
1169
+ return { valid: false, errors, warnings };
1170
+ }
1171
+ const s = script;
1172
+ if (s.version !== 1) {
1173
+ errors.push("Script version must be 1");
1174
+ }
1175
+ if (!Array.isArray(s.actions)) {
1176
+ errors.push("Script must have an actions array");
1177
+ } else {
1178
+ for (let i = 0; i < s.actions.length; i++) {
1179
+ errors.push(...this.validateAction(s.actions[i], i));
1180
+ }
1181
+ }
1182
+ return {
1183
+ valid: errors.length === 0,
1184
+ errors,
1185
+ warnings
1186
+ };
1187
+ }
1188
+ async play(script) {
1189
+ if (this._isDestroyed) {
1190
+ throw new Error("Overlay has been destroyed");
1191
+ }
1192
+ const validation = this.validate(script);
1193
+ if (!validation.valid) {
1194
+ throw new Error(`Invalid script: ${validation.errors.join(", ")}`);
1195
+ }
1196
+ await this.timeline.play(script);
1197
+ }
1198
+ stop() {
1199
+ this.timeline.stop();
1200
+ }
1201
+ on(event, handler) {
1202
+ if (!this.eventHandlers.has(event)) {
1203
+ this.eventHandlers.set(event, /* @__PURE__ */ new Set());
1204
+ }
1205
+ this.eventHandlers.get(event).add(handler);
1206
+ return () => {
1207
+ this.eventHandlers.get(event)?.delete(handler);
1208
+ };
1209
+ }
1210
+ emit(event, ...args) {
1211
+ const handlers = this.eventHandlers.get(event);
1212
+ if (handlers) {
1213
+ for (const handler of handlers) {
1214
+ try {
1215
+ ;
1216
+ handler(...args);
1217
+ } catch (err) {
1218
+ console.error(`[Overlay] Error in ${event} handler:`, err);
1219
+ }
1220
+ }
1221
+ }
1222
+ }
1223
+ destroy() {
1224
+ if (this._isDestroyed) return;
1225
+ this._isDestroyed = true;
1226
+ this.timeline.destroy();
1227
+ this._cursor.destroy();
1228
+ this._annotations.destroy();
1229
+ this.motionQuery.removeEventListener("change", this.handleMotionChange);
1230
+ this.eventHandlers.clear();
1231
+ this.root.remove();
1232
+ }
1233
+ };
1234
+ __publicField(_Overlay, "VALID_ACTION_TYPES", [
1235
+ "move",
1236
+ "click",
1237
+ "wait",
1238
+ "setCursor",
1239
+ "showAnnotations",
1240
+ "hideAnnotations"
1241
+ ]);
1242
+ var Overlay = _Overlay;
1243
+
1244
+ // src/overlay/index.ts
1245
+ function createOverlay(options) {
1246
+ return new Overlay(options);
1247
+ }
1248
+
1249
+ export { Overlay, createOverlay };
1250
+ //# sourceMappingURL=chunk-XFAAEDJK.js.map
1251
+ //# sourceMappingURL=chunk-XFAAEDJK.js.map