@immense/vue-pom-generator 1.0.53 → 1.0.55

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,857 @@
1
+ import { arrow, autoPlacement, computePosition, limitShift, offset, shift } from "./floating-ui";
2
+ import {
3
+ POINTER_CALLOUT_IDS,
4
+ POINTER_CALLOUT_THEME,
5
+ type CalloutRenderer,
6
+ type CalloutTargetBox,
7
+ measureCalloutBubble,
8
+ } from "./callout";
9
+ import type { PwPage } from "./playwright-types";
10
+
11
+ const __PW_POINTER_CALLOUT_AVOID_SELECTOR__
12
+ = [
13
+ "[data-callout-avoid]",
14
+ "button",
15
+ "input",
16
+ "textarea",
17
+ "select",
18
+ "summary",
19
+ "a[href]",
20
+ "[role='button']",
21
+ "[role='link']",
22
+ "[role='textbox']",
23
+ "[role='combobox']",
24
+ "[role='option']",
25
+ "[role='tab']",
26
+ "[role='menuitem']",
27
+ "[contenteditable='']",
28
+ "[contenteditable='true']",
29
+ "[contenteditable]:not([contenteditable='false'])",
30
+ ].join(",");
31
+
32
+ type Placement =
33
+ | "top"
34
+ | "top-start"
35
+ | "top-end"
36
+ | "right"
37
+ | "right-start"
38
+ | "right-end"
39
+ | "bottom"
40
+ | "bottom-start"
41
+ | "bottom-end"
42
+ | "left"
43
+ | "left-start"
44
+ | "left-end";
45
+
46
+ const __PW_POINTER_ALLOWED_PLACEMENTS__: Placement[] = [
47
+ "top-start",
48
+ "top",
49
+ "top-end",
50
+ "right-start",
51
+ "right",
52
+ "right-end",
53
+ "bottom-start",
54
+ "bottom",
55
+ "bottom-end",
56
+ "left-start",
57
+ "left",
58
+ "left-end",
59
+ ];
60
+
61
+ const __PW_POINTER_ORDERED_PLACEMENTS__ = Object.fromEntries(
62
+ __PW_POINTER_ALLOWED_PLACEMENTS__.map(placement => [
63
+ placement,
64
+ [
65
+ placement,
66
+ ...__PW_POINTER_ALLOWED_PLACEMENTS__.filter(candidate => candidate !== placement),
67
+ ],
68
+ ]),
69
+ ) as Record<Placement, Placement[]>;
70
+
71
+ interface CalloutContext {
72
+ avoidRects: CalloutTargetBox[];
73
+ preferredPosition?: CalloutPositionResult;
74
+ protectedTargetRects: CalloutTargetBox[];
75
+ viewportHeight: number;
76
+ viewportWidth: number;
77
+ }
78
+
79
+ interface FloatingVirtualElement extends CalloutTargetBox {
80
+ kind: "arrow" | "floating" | "reference";
81
+ }
82
+
83
+ interface CalloutLayout {
84
+ arrowX: number | null;
85
+ arrowY: number | null;
86
+ placement: Placement;
87
+ staticSide: "bottom" | "left" | "right" | "top";
88
+ x: number;
89
+ y: number;
90
+ }
91
+
92
+ interface CalloutPositionResult {
93
+ adjustmentDistance: number;
94
+ arrowX: number | null;
95
+ arrowY: number | null;
96
+ placement: Placement;
97
+ x: number;
98
+ y: number;
99
+ }
100
+
101
+ function __pw_overlap_area__(first: CalloutTargetBox, second: CalloutTargetBox): number {
102
+ const horizontal = Math.max(0, Math.min(first.x + first.width, second.x + second.width) - Math.max(first.x, second.x));
103
+ const vertical = Math.max(0, Math.min(first.y + first.height, second.y + second.height) - Math.max(first.y, second.y));
104
+ return horizontal * vertical;
105
+ }
106
+
107
+ function __pw_rect_center_distance__(first: CalloutTargetBox, second: CalloutTargetBox): number {
108
+ const firstCenterX = first.x + first.width / 2;
109
+ const firstCenterY = first.y + first.height / 2;
110
+ const secondCenterX = second.x + second.width / 2;
111
+ const secondCenterY = second.y + second.height / 2;
112
+ return Math.hypot(firstCenterX - secondCenterX, firstCenterY - secondCenterY);
113
+ }
114
+
115
+ function __pw_rect_gap__(first: CalloutTargetBox, second: CalloutTargetBox): number {
116
+ const horizontalGap = Math.max(0, Math.max(second.x - (first.x + first.width), first.x - (second.x + second.width)));
117
+ const verticalGap = Math.max(0, Math.max(second.y - (first.y + first.height), first.y - (second.y + second.height)));
118
+ return Math.max(horizontalGap, verticalGap);
119
+ }
120
+
121
+ function __pw_expand_rect__(rect: CalloutTargetBox, padding: number): CalloutTargetBox {
122
+ return {
123
+ height: rect.height + (padding * 2),
124
+ width: rect.width + (padding * 2),
125
+ x: rect.x - padding,
126
+ y: rect.y - padding,
127
+ };
128
+ }
129
+
130
+ function __pw_parse_placement__(placement: Placement): {
131
+ align: "center" | "end" | "start";
132
+ side: "bottom" | "left" | "right" | "top";
133
+ } {
134
+ const [side, align] = placement.split("-") as [Placement extends `${infer Side}-${string}` ? Side : never, "end" | "start" | undefined];
135
+ return {
136
+ align: align ?? "center",
137
+ side,
138
+ };
139
+ }
140
+
141
+ function __pw_to_client_rect__(rect: CalloutTargetBox) {
142
+ return {
143
+ bottom: rect.y + rect.height,
144
+ height: rect.height,
145
+ left: rect.x,
146
+ right: rect.x + rect.width,
147
+ top: rect.y,
148
+ width: rect.width,
149
+ x: rect.x,
150
+ y: rect.y,
151
+ };
152
+ }
153
+
154
+ function __pw_create_virtual_element__(rect: CalloutTargetBox, kind: FloatingVirtualElement["kind"]): FloatingVirtualElement {
155
+ return {
156
+ height: rect.height,
157
+ kind,
158
+ width: rect.width,
159
+ x: rect.x,
160
+ y: rect.y,
161
+ };
162
+ }
163
+
164
+ function __pw_create_platform__(viewportWidth: number, viewportHeight: number) {
165
+ const offsetParent = {
166
+ clientHeight: viewportHeight,
167
+ clientLeft: 0,
168
+ clientTop: 0,
169
+ clientWidth: viewportWidth,
170
+ };
171
+
172
+ return {
173
+ convertOffsetParentRelativeRectToViewportRelativeRect: ({ rect }: { rect: CalloutTargetBox }) => rect,
174
+ getClientRects: (element: FloatingVirtualElement) => [__pw_to_client_rect__(element)],
175
+ getClippingRect: () => ({
176
+ height: viewportHeight,
177
+ width: viewportWidth,
178
+ x: 0,
179
+ y: 0,
180
+ }),
181
+ getDimensions: (element: FloatingVirtualElement) => ({
182
+ height: element.height,
183
+ width: element.width,
184
+ }),
185
+ getDocumentElement: () => ({
186
+ clientHeight: viewportHeight,
187
+ clientWidth: viewportWidth,
188
+ }),
189
+ getElementRects: ({
190
+ floating,
191
+ reference,
192
+ }: {
193
+ floating: FloatingVirtualElement;
194
+ reference: FloatingVirtualElement;
195
+ strategy: string;
196
+ }) => ({
197
+ floating: {
198
+ height: floating.height,
199
+ width: floating.width,
200
+ x: 0,
201
+ y: 0,
202
+ },
203
+ reference: {
204
+ height: reference.height,
205
+ width: reference.width,
206
+ x: reference.x,
207
+ y: reference.y,
208
+ },
209
+ }),
210
+ getOffsetParent: () => offsetParent,
211
+ getScale: () => ({ x: 1, y: 1 }),
212
+ isElement: () => false,
213
+ isRTL: () => false,
214
+ };
215
+ }
216
+
217
+ function __pw_finalize_callout_position__(
218
+ referenceRect: CalloutTargetBox,
219
+ floatingRect: CalloutTargetBox,
220
+ protectedRects: CalloutTargetBox[],
221
+ viewportWidth: number,
222
+ viewportHeight: number,
223
+ resolvedPlacement: Placement,
224
+ rawX: number,
225
+ rawY: number,
226
+ baseArrowData?: { x?: number; y?: number },
227
+ ): CalloutPositionResult {
228
+ const { side } = __pw_parse_placement__(resolvedPlacement);
229
+ let x = Math.round(rawX);
230
+ let y = Math.round(rawY);
231
+ const baseX = x;
232
+ const baseY = y;
233
+ let layoutAdjustedForProtectedRect = false;
234
+
235
+ for (const protectedRect of protectedRects) {
236
+ const horizontalOverlap = x < protectedRect.x + protectedRect.width && x + floatingRect.width > protectedRect.x;
237
+ const verticalOverlap = y < protectedRect.y + protectedRect.height && y + floatingRect.height > protectedRect.y;
238
+
239
+ if ((side === "top" || side === "bottom") && horizontalOverlap) {
240
+ if (side === "top" && verticalOverlap) {
241
+ y = Math.min(y, protectedRect.y - floatingRect.height);
242
+ layoutAdjustedForProtectedRect = true;
243
+ }
244
+ if (side === "bottom" && verticalOverlap) {
245
+ y = Math.max(y, protectedRect.y + protectedRect.height);
246
+ layoutAdjustedForProtectedRect = true;
247
+ }
248
+ }
249
+
250
+ if ((side === "left" || side === "right") && verticalOverlap) {
251
+ if (side === "left" && horizontalOverlap) {
252
+ x = Math.min(x, protectedRect.x - floatingRect.width);
253
+ layoutAdjustedForProtectedRect = true;
254
+ }
255
+ if (side === "right" && horizontalOverlap) {
256
+ x = Math.max(x, protectedRect.x + protectedRect.width);
257
+ layoutAdjustedForProtectedRect = true;
258
+ }
259
+ }
260
+ }
261
+
262
+ x = Math.min(
263
+ Math.max(x, POINTER_CALLOUT_THEME.margin),
264
+ Math.max(POINTER_CALLOUT_THEME.margin, viewportWidth - floatingRect.width - POINTER_CALLOUT_THEME.margin),
265
+ );
266
+ y = Math.min(
267
+ Math.max(y, POINTER_CALLOUT_THEME.margin),
268
+ Math.max(POINTER_CALLOUT_THEME.margin, viewportHeight - floatingRect.height - POINTER_CALLOUT_THEME.margin),
269
+ );
270
+ const adjustmentDistance = Math.abs(x - baseX) + Math.abs(y - baseY);
271
+ const referenceCenterX = referenceRect.x + referenceRect.width / 2;
272
+ const referenceCenterY = referenceRect.y + referenceRect.height / 2;
273
+ const arrowHalf = POINTER_CALLOUT_THEME.arrowSize / 2;
274
+ const arrowX = side === "top" || side === "bottom"
275
+ ? !layoutAdjustedForProtectedRect && typeof baseArrowData?.x === "number"
276
+ ? Math.round(baseArrowData.x)
277
+ : Math.min(
278
+ Math.max(referenceCenterX - x - arrowHalf, POINTER_CALLOUT_THEME.arrowPadding),
279
+ Math.max(POINTER_CALLOUT_THEME.arrowPadding, floatingRect.width - POINTER_CALLOUT_THEME.arrowSize - POINTER_CALLOUT_THEME.arrowPadding),
280
+ )
281
+ : null;
282
+ const arrowY = side === "left" || side === "right"
283
+ ? !layoutAdjustedForProtectedRect && typeof baseArrowData?.y === "number"
284
+ ? Math.round(baseArrowData.y)
285
+ : Math.min(
286
+ Math.max(referenceCenterY - y - arrowHalf, POINTER_CALLOUT_THEME.arrowPadding),
287
+ Math.max(POINTER_CALLOUT_THEME.arrowPadding, floatingRect.height - POINTER_CALLOUT_THEME.arrowSize - POINTER_CALLOUT_THEME.arrowPadding),
288
+ )
289
+ : null;
290
+
291
+ return {
292
+ adjustmentDistance,
293
+ arrowX,
294
+ arrowY,
295
+ placement: resolvedPlacement,
296
+ x,
297
+ y,
298
+ };
299
+ }
300
+
301
+ async function __pw_compute_shifted_position__(
302
+ referenceRect: CalloutTargetBox,
303
+ floatingRect: CalloutTargetBox,
304
+ placement: Placement,
305
+ protectedRects: CalloutTargetBox[],
306
+ viewportWidth: number,
307
+ viewportHeight: number,
308
+ ): Promise<CalloutPositionResult> {
309
+ const platform = __pw_create_platform__(viewportWidth, viewportHeight);
310
+ const floatingElement = __pw_create_virtual_element__(floatingRect, "floating");
311
+ const referenceElement = __pw_create_virtual_element__(referenceRect, "reference");
312
+ const arrowElement = __pw_create_virtual_element__(
313
+ { x: 0, y: 0, width: POINTER_CALLOUT_THEME.arrowSize, height: POINTER_CALLOUT_THEME.arrowSize },
314
+ "arrow",
315
+ );
316
+ const result = await computePosition(referenceElement, floatingElement, {
317
+ middleware: [
318
+ offset(POINTER_CALLOUT_THEME.gap),
319
+ shift({
320
+ limiter: limitShift({}),
321
+ padding: POINTER_CALLOUT_THEME.margin,
322
+ }),
323
+ arrow({
324
+ element: arrowElement,
325
+ padding: POINTER_CALLOUT_THEME.arrowPadding,
326
+ }),
327
+ ],
328
+ placement,
329
+ platform,
330
+ strategy: "fixed",
331
+ });
332
+ return __pw_finalize_callout_position__(
333
+ referenceRect,
334
+ floatingRect,
335
+ protectedRects,
336
+ viewportWidth,
337
+ viewportHeight,
338
+ result.placement as Placement,
339
+ result.x,
340
+ result.y,
341
+ (result.middlewareData as { arrow?: { x?: number; y?: number } }).arrow,
342
+ );
343
+ }
344
+
345
+ async function __pw_compute_callout_layout__(
346
+ targetRect: CalloutTargetBox,
347
+ floatingRect: CalloutTargetBox,
348
+ context: CalloutContext,
349
+ ): Promise<CalloutLayout> {
350
+ const referenceRect = targetRect;
351
+ let bestLayout: (CalloutLayout & { score: number }) | null = null;
352
+ const orderedPlacements = context.preferredPosition
353
+ ? __PW_POINTER_ORDERED_PLACEMENTS__[context.preferredPosition.placement]
354
+ : __PW_POINTER_ALLOWED_PLACEMENTS__;
355
+
356
+ for (const placement of orderedPlacements) {
357
+ const result = context.preferredPosition && placement === context.preferredPosition.placement
358
+ ? context.preferredPosition
359
+ : await __pw_compute_shifted_position__(
360
+ referenceRect,
361
+ floatingRect,
362
+ placement,
363
+ context.protectedTargetRects,
364
+ context.viewportWidth,
365
+ context.viewportHeight,
366
+ );
367
+
368
+ const positionedRect: CalloutTargetBox = {
369
+ x: result.x,
370
+ y: result.y,
371
+ width: floatingRect.width,
372
+ height: floatingRect.height,
373
+ };
374
+ const protectedOverlap = context.protectedTargetRects.reduce(
375
+ (sum, avoidRect) => sum + __pw_overlap_area__(positionedRect, avoidRect),
376
+ 0,
377
+ );
378
+ const avoidOverlap = context.avoidRects.reduce(
379
+ (sum, avoidRect) => sum + __pw_overlap_area__(positionedRect, __pw_expand_rect__(avoidRect, POINTER_CALLOUT_THEME.avoidPadding)),
380
+ 0,
381
+ );
382
+ const targetGap = __pw_rect_gap__(positionedRect, targetRect);
383
+ const score = (protectedOverlap * 10)
384
+ + (avoidOverlap * 8)
385
+ + (result.adjustmentDistance * 60)
386
+ + (targetGap * 40)
387
+ + (__pw_rect_center_distance__(positionedRect, referenceRect) * 0.08);
388
+
389
+ if (!bestLayout || score < bestLayout.score) {
390
+ bestLayout = {
391
+ arrowX: result.arrowX,
392
+ arrowY: result.arrowY,
393
+ placement: result.placement,
394
+ score,
395
+ staticSide: (() => {
396
+ const side = __pw_parse_placement__(result.placement).side;
397
+ if (side === "bottom") return "top";
398
+ if (side === "left") return "right";
399
+ if (side === "right") return "left";
400
+ return "bottom";
401
+ })(),
402
+ x: result.x,
403
+ y: result.y,
404
+ };
405
+ }
406
+ }
407
+
408
+ if (!bestLayout) {
409
+ return {
410
+ arrowX: null,
411
+ arrowY: null,
412
+ placement: "bottom",
413
+ staticSide: "top",
414
+ x: targetRect.x,
415
+ y: targetRect.y,
416
+ };
417
+ }
418
+
419
+ return bestLayout;
420
+ }
421
+
422
+ async function __pw_get_preferred_callout_position__(
423
+ targetRect: CalloutTargetBox,
424
+ floatingRect: CalloutTargetBox,
425
+ protectedRects: CalloutTargetBox[],
426
+ viewportWidth: number,
427
+ viewportHeight: number,
428
+ ): Promise<CalloutPositionResult> {
429
+ const platform = __pw_create_platform__(viewportWidth, viewportHeight);
430
+ const floatingElement = __pw_create_virtual_element__(floatingRect, "floating");
431
+ const referenceElement = __pw_create_virtual_element__(targetRect, "reference");
432
+ const arrowElement = __pw_create_virtual_element__(
433
+ { x: 0, y: 0, width: POINTER_CALLOUT_THEME.arrowSize, height: POINTER_CALLOUT_THEME.arrowSize },
434
+ "arrow",
435
+ );
436
+ const result = await computePosition(referenceElement, floatingElement, {
437
+ middleware: [
438
+ offset(POINTER_CALLOUT_THEME.gap),
439
+ autoPlacement({
440
+ allowedPlacements: __PW_POINTER_ALLOWED_PLACEMENTS__,
441
+ padding: POINTER_CALLOUT_THEME.margin,
442
+ }),
443
+ shift({
444
+ limiter: limitShift({}),
445
+ padding: POINTER_CALLOUT_THEME.margin,
446
+ }),
447
+ arrow({
448
+ element: arrowElement,
449
+ padding: POINTER_CALLOUT_THEME.arrowPadding,
450
+ }),
451
+ ],
452
+ placement: "top",
453
+ platform,
454
+ strategy: "fixed",
455
+ });
456
+ return __pw_finalize_callout_position__(
457
+ targetRect,
458
+ floatingRect,
459
+ protectedRects,
460
+ viewportWidth,
461
+ viewportHeight,
462
+ result.placement as Placement,
463
+ result.x,
464
+ result.y,
465
+ (result.middlewareData as { arrow?: { x?: number; y?: number } }).arrow,
466
+ );
467
+ }
468
+
469
+ async function __pw_ensure_floating_ui_callout__(page: PwPage): Promise<void> {
470
+ await page.evaluate(
471
+ ({
472
+ annotationId,
473
+ contentId,
474
+ arrowId,
475
+ arrowSize,
476
+ background,
477
+ border,
478
+ borderRadius,
479
+ boxShadow,
480
+ textColor,
481
+ }: {
482
+ annotationId: string;
483
+ contentId: string;
484
+ arrowId: string;
485
+ arrowSize: number;
486
+ background: string;
487
+ border: string;
488
+ borderRadius: number;
489
+ boxShadow: string;
490
+ textColor: string;
491
+ }) => {
492
+ const ensureElement = <T extends HTMLElement>(id: string, tagName: keyof HTMLElementTagNameMap): T => {
493
+ const existing = document.getElementById(id);
494
+ if (existing instanceof HTMLElement) {
495
+ return existing as T;
496
+ }
497
+ const created = document.createElement(tagName);
498
+ created.id = id;
499
+ return created as T;
500
+ };
501
+
502
+ const annotation = ensureElement<HTMLDivElement>(annotationId, "div");
503
+ annotation.setAttribute(
504
+ "style",
505
+ [
506
+ "position:fixed",
507
+ "z-index:2147483647",
508
+ "pointer-events:none",
509
+ "left:18px",
510
+ "top:18px",
511
+ "width:220px",
512
+ "box-sizing:border-box",
513
+ "padding:12px 16px",
514
+ "border:" + border,
515
+ "border-radius:" + borderRadius + "px",
516
+ "background:" + background,
517
+ "color:" + textColor,
518
+ "font:600 13px/1.45 Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
519
+ "letter-spacing:0.01em",
520
+ "box-shadow:" + boxShadow,
521
+ "opacity:0",
522
+ "white-space:normal",
523
+ "transform:translate3d(0,0,0)",
524
+ "transform-origin:center",
525
+ "isolation:isolate",
526
+ ].join(";"),
527
+ );
528
+
529
+ const contentEl = ensureElement<HTMLDivElement>(contentId, "div");
530
+ contentEl.setAttribute("style", "position:relative;z-index:1;");
531
+
532
+ const arrowEl = ensureElement<HTMLDivElement>(arrowId, "div");
533
+ arrowEl.setAttribute(
534
+ "style",
535
+ [
536
+ "position:absolute",
537
+ "width:" + arrowSize + "px",
538
+ "height:" + arrowSize + "px",
539
+ "background:" + background,
540
+ "transform:rotate(45deg)",
541
+ "pointer-events:none",
542
+ "left:0",
543
+ "top:0",
544
+ "box-shadow:0 12px 24px rgba(15,23,42,0.18)",
545
+ "z-index:-1",
546
+ "opacity:0",
547
+ ].join(";"),
548
+ );
549
+
550
+ if (!annotation.isConnected) {
551
+ document.body.appendChild(annotation);
552
+ }
553
+ if (contentEl.parentElement !== annotation) {
554
+ annotation.appendChild(contentEl);
555
+ }
556
+ if (arrowEl.parentElement !== annotation) {
557
+ annotation.appendChild(arrowEl);
558
+ }
559
+ },
560
+ {
561
+ annotationId: POINTER_CALLOUT_IDS.annotation,
562
+ contentId: POINTER_CALLOUT_IDS.content,
563
+ arrowId: POINTER_CALLOUT_IDS.arrow,
564
+ arrowSize: POINTER_CALLOUT_THEME.arrowSize,
565
+ background: POINTER_CALLOUT_THEME.background,
566
+ border: POINTER_CALLOUT_THEME.border,
567
+ borderRadius: POINTER_CALLOUT_THEME.borderRadius,
568
+ boxShadow: POINTER_CALLOUT_THEME.boxShadow,
569
+ textColor: POINTER_CALLOUT_THEME.textColor,
570
+ },
571
+ );
572
+ }
573
+
574
+ export const floatingUiCalloutRenderer: CalloutRenderer = {
575
+ overlayIds: [
576
+ POINTER_CALLOUT_IDS.annotation,
577
+ POINTER_CALLOUT_IDS.content,
578
+ POINTER_CALLOUT_IDS.arrow,
579
+ ],
580
+ async hide(page) {
581
+ await page.evaluate(
582
+ ({ annotationId, contentId, arrowId }: { annotationId: string; contentId: string; arrowId: string }) => {
583
+ const annotation = document.getElementById(annotationId) as HTMLDivElement | null;
584
+ const content = document.getElementById(contentId) as HTMLDivElement | null;
585
+ const arrow = document.getElementById(arrowId) as HTMLDivElement | null;
586
+ if (!annotation) {
587
+ return;
588
+ }
589
+
590
+ if (content) {
591
+ content.textContent = "";
592
+ }
593
+
594
+ annotation.style.transition = "opacity 120ms ease-in-out, transform 160ms ease-in-out";
595
+ annotation.style.opacity = "0";
596
+ annotation.style.transform = "scale(0.96)";
597
+ annotation.setAttribute("data-placement", "hidden");
598
+ if (arrow) {
599
+ arrow.style.opacity = "0";
600
+ arrow.style.left = "";
601
+ arrow.style.top = "";
602
+ arrow.style.right = "";
603
+ arrow.style.bottom = "";
604
+ }
605
+ },
606
+ {
607
+ annotationId: POINTER_CALLOUT_IDS.annotation,
608
+ contentId: POINTER_CALLOUT_IDS.content,
609
+ arrowId: POINTER_CALLOUT_IDS.arrow,
610
+ },
611
+ );
612
+ },
613
+ async show(page, request) {
614
+ await __pw_ensure_floating_ui_callout__(page);
615
+ const { bubbleHeight, bubbleWidth } = measureCalloutBubble(request.text);
616
+ const context = await page.evaluate(
617
+ ({
618
+ avoidSelector,
619
+ ex,
620
+ ey,
621
+ overlayIds,
622
+ }: {
623
+ avoidSelector: string;
624
+ ex: number;
625
+ ey: number;
626
+ overlayIds: string[];
627
+ }) => {
628
+ type BrowserRect = { x: number; y: number; width: number; height: number };
629
+
630
+ const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));
631
+ const viewportWidth = Math.max(window.innerWidth || 0, document.documentElement.clientWidth, 1280);
632
+ const viewportHeight = Math.max(window.innerHeight || 0, document.documentElement.clientHeight, 720);
633
+ const viewportArea = viewportWidth * viewportHeight;
634
+ const overlayIdSet = new Set(overlayIds);
635
+
636
+ const toViewportRect = (candidateRect: { left: number; top: number; right: number; bottom: number } | DOMRect): BrowserRect | null => {
637
+ const left = clamp(candidateRect.left, 0, viewportWidth);
638
+ const top = clamp(candidateRect.top, 0, viewportHeight);
639
+ const right = clamp(candidateRect.right, 0, viewportWidth);
640
+ const bottom = clamp(candidateRect.bottom, 0, viewportHeight);
641
+ const width = right - left;
642
+ const height = bottom - top;
643
+ if (width < 24 || height < 24) {
644
+ return null;
645
+ }
646
+
647
+ return { x: left, y: top, width, height };
648
+ };
649
+
650
+ const pushUniqueRect = (rects: BrowserRect[], rect: BrowserRect | null) => {
651
+ if (!rect) {
652
+ return;
653
+ }
654
+
655
+ const duplicate = rects.some((existing) =>
656
+ Math.abs(existing.x - rect.x) < 1
657
+ && Math.abs(existing.y - rect.y) < 1
658
+ && Math.abs(existing.width - rect.width) < 1
659
+ && Math.abs(existing.height - rect.height) < 1,
660
+ );
661
+ if (!duplicate) {
662
+ rects.push(rect);
663
+ }
664
+ };
665
+
666
+ const collectAvoidRects = (): BrowserRect[] =>
667
+ Array.from(document.querySelectorAll<HTMLElement>(avoidSelector))
668
+ .filter((candidate) => !overlayIdSet.has(candidate.id))
669
+ .flatMap((candidate) => {
670
+ const computedStyle = window.getComputedStyle(candidate);
671
+ if (
672
+ computedStyle.display === "none"
673
+ || computedStyle.visibility === "hidden"
674
+ || Number.parseFloat(computedStyle.opacity || "1") <= 0.05
675
+ ) {
676
+ return [];
677
+ }
678
+
679
+ const normalizedRect = toViewportRect(candidate.getBoundingClientRect());
680
+ if (!normalizedRect) {
681
+ return [];
682
+ }
683
+
684
+ if (!candidate.hasAttribute("data-callout-avoid") && normalizedRect.width * normalizedRect.height > viewportArea * 0.35) {
685
+ return [];
686
+ }
687
+
688
+ return [normalizedRect];
689
+ });
690
+
691
+ const collectProtectedTargetRects = (): BrowserRect[] => {
692
+ const protectedRects: BrowserRect[] = [];
693
+ if (typeof document.elementFromPoint !== "function") {
694
+ return protectedRects;
695
+ }
696
+
697
+ const targetElement = document.elementFromPoint(ex, ey);
698
+ if (!(targetElement instanceof HTMLElement)) {
699
+ return protectedRects;
700
+ }
701
+
702
+ const targetRect = toViewportRect(targetElement.getBoundingClientRect());
703
+ pushUniqueRect(protectedRects, targetRect);
704
+ if (!targetRect) {
705
+ return protectedRects;
706
+ }
707
+
708
+ const targetArea = targetRect.width * targetRect.height;
709
+ let ancestor = targetElement.parentElement;
710
+ while (ancestor && ancestor !== document.body) {
711
+ const computedStyle = window.getComputedStyle(ancestor);
712
+ if (
713
+ computedStyle.display === "none"
714
+ || computedStyle.visibility === "hidden"
715
+ || Number.parseFloat(computedStyle.opacity || "1") <= 0.05
716
+ ) {
717
+ ancestor = ancestor.parentElement;
718
+ continue;
719
+ }
720
+
721
+ const ancestorRect = toViewportRect(ancestor.getBoundingClientRect());
722
+ if (!ancestorRect) {
723
+ ancestor = ancestor.parentElement;
724
+ continue;
725
+ }
726
+
727
+ const ancestorArea = ancestorRect.width * ancestorRect.height;
728
+ const clearlyBiggerThanTarget = ancestorArea >= Math.max(targetArea * 1.75, 18_000);
729
+ const stillReasonablyLocal = ancestorArea <= viewportArea * 0.28;
730
+ const containsTarget = ancestorRect.x <= targetRect.x
731
+ && ancestorRect.y <= targetRect.y
732
+ && ancestorRect.x + ancestorRect.width >= targetRect.x + targetRect.width
733
+ && ancestorRect.y + ancestorRect.height >= targetRect.y + targetRect.height;
734
+ if (clearlyBiggerThanTarget && stillReasonablyLocal && containsTarget) {
735
+ pushUniqueRect(protectedRects, ancestorRect);
736
+ }
737
+
738
+ if (protectedRects.length >= 3) {
739
+ break;
740
+ }
741
+
742
+ ancestor = ancestor.parentElement;
743
+ }
744
+
745
+ return protectedRects;
746
+ };
747
+
748
+ return {
749
+ avoidRects: collectAvoidRects(),
750
+ protectedTargetRects: collectProtectedTargetRects(),
751
+ viewportHeight,
752
+ viewportWidth,
753
+ };
754
+ },
755
+ {
756
+ avoidSelector: __PW_POINTER_CALLOUT_AVOID_SELECTOR__,
757
+ ex: request.targetBox.x + request.targetBox.width / 2,
758
+ ey: request.targetBox.y + request.targetBox.height / 2,
759
+ overlayIds: request.overlayIds,
760
+ },
761
+ );
762
+ const preferredPosition = await __pw_get_preferred_callout_position__(
763
+ { x: request.targetBox.x, y: request.targetBox.y, width: request.targetBox.width, height: request.targetBox.height },
764
+ { x: 0, y: 0, width: bubbleWidth, height: bubbleHeight },
765
+ context.protectedTargetRects,
766
+ context.viewportWidth,
767
+ context.viewportHeight,
768
+ );
769
+ const layout = await __pw_compute_callout_layout__(
770
+ { x: request.targetBox.x, y: request.targetBox.y, width: request.targetBox.width, height: request.targetBox.height },
771
+ { x: 0, y: 0, width: bubbleWidth, height: bubbleHeight },
772
+ {
773
+ ...context,
774
+ preferredPosition,
775
+ },
776
+ );
777
+ await page.evaluate(
778
+ ({
779
+ annotationId,
780
+ arrowId,
781
+ arrowSize,
782
+ background,
783
+ border,
784
+ borderRadius,
785
+ bubbleHeight,
786
+ bubbleWidth,
787
+ contentId,
788
+ layout,
789
+ text,
790
+ }: {
791
+ annotationId: string;
792
+ arrowId: string;
793
+ arrowSize: number;
794
+ background: string;
795
+ border: string;
796
+ borderRadius: number;
797
+ bubbleHeight: number;
798
+ bubbleWidth: number;
799
+ contentId: string;
800
+ layout: CalloutLayout;
801
+ text: string;
802
+ }) => {
803
+ const annotation = document.getElementById(annotationId) as HTMLDivElement | null;
804
+ const content = document.getElementById(contentId) as HTMLDivElement | null;
805
+ const arrow = document.getElementById(arrowId) as HTMLDivElement | null;
806
+ if (!annotation || !content || !arrow) {
807
+ return;
808
+ }
809
+
810
+ content.textContent = text;
811
+ annotation.style.width = `${bubbleWidth}px`;
812
+ annotation.style.minHeight = `${bubbleHeight}px`;
813
+ annotation.style.background = background;
814
+ annotation.style.border = border;
815
+ annotation.style.borderRadius = `${borderRadius}px`;
816
+ annotation.style.transition = "opacity 120ms ease-in-out, transform 160ms ease-in-out";
817
+ annotation.style.willChange = "left, top, opacity, transform";
818
+ annotation.style.left = `${layout.x}px`;
819
+ annotation.style.top = `${layout.y}px`;
820
+ annotation.style.opacity = "1";
821
+ annotation.style.transform = "scale(1)";
822
+ annotation.setAttribute("data-placement", layout.placement);
823
+
824
+ arrow.style.left = "";
825
+ arrow.style.top = "";
826
+ arrow.style.right = "";
827
+ arrow.style.bottom = "";
828
+ arrow.style.transform = "rotate(45deg)";
829
+ if (layout.arrowX !== null) {
830
+ arrow.style.left = `${layout.arrowX}px`;
831
+ }
832
+ if (layout.arrowY !== null) {
833
+ arrow.style.top = `${layout.arrowY}px`;
834
+ }
835
+ arrow.style.setProperty(layout.staticSide, `${Math.round(arrowSize / -2)}px`);
836
+ arrow.style.opacity = "1";
837
+ },
838
+ {
839
+ annotationId: POINTER_CALLOUT_IDS.annotation,
840
+ arrowId: POINTER_CALLOUT_IDS.arrow,
841
+ arrowSize: POINTER_CALLOUT_THEME.arrowSize,
842
+ background: POINTER_CALLOUT_THEME.background,
843
+ border: POINTER_CALLOUT_THEME.border,
844
+ borderRadius: POINTER_CALLOUT_THEME.borderRadius,
845
+ bubbleHeight,
846
+ bubbleWidth,
847
+ contentId: POINTER_CALLOUT_IDS.content,
848
+ layout,
849
+ text: request.text,
850
+ },
851
+ );
852
+ },
853
+ };
854
+
855
+ export function createFloatingUiCalloutRenderer(): CalloutRenderer {
856
+ return floatingUiCalloutRenderer;
857
+ }