@immense/vue-pom-generator 1.0.48 → 1.0.50

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.
Files changed (37) hide show
  1. package/RELEASE_NOTES.md +47 -36
  2. package/class-generation/{BasePage.ts → base-page.ts} +19 -4
  3. package/class-generation/callout.ts +827 -0
  4. package/class-generation/floating-ui.ts +814 -0
  5. package/class-generation/index.ts +11 -10
  6. package/class-generation/{Pointer.ts → pointer.ts} +81 -42
  7. package/dist/class-generation/{BasePage.d.ts → base-page.d.ts} +6 -2
  8. package/dist/class-generation/base-page.d.ts.map +1 -0
  9. package/dist/class-generation/callout.d.ts +20 -0
  10. package/dist/class-generation/callout.d.ts.map +1 -0
  11. package/dist/class-generation/floating-ui.d.ts +100 -0
  12. package/dist/class-generation/floating-ui.d.ts.map +1 -0
  13. package/dist/class-generation/{Pointer.d.ts → pointer.d.ts} +6 -6
  14. package/dist/class-generation/pointer.d.ts.map +1 -0
  15. package/dist/index.cjs +27 -14
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.mjs +27 -14
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/playwright.config.d.ts +3 -0
  20. package/dist/playwright.config.d.ts.map +1 -0
  21. package/dist/plugin/support/build-plugin.d.ts.map +1 -1
  22. package/dist/plugin/support/dev-plugin.d.ts.map +1 -1
  23. package/dist/plugin/types.d.ts +1 -1
  24. package/dist/tests/fixtures/generated-tsc/{BasePage.full.d.ts → base-page.full.d.ts} +1 -1
  25. package/dist/tests/fixtures/generated-tsc/base-page.full.d.ts.map +1 -0
  26. package/dist/tests/fixtures/generated-tsc/{BasePage.minimal.d.ts → base-page.minimal.d.ts} +1 -1
  27. package/dist/tests/fixtures/generated-tsc/base-page.minimal.d.ts.map +1 -0
  28. package/dist/tests/fixtures/generated-tsc/{Pointer.d.ts → pointer.d.ts} +1 -1
  29. package/dist/tests/fixtures/generated-tsc/pointer.d.ts.map +1 -0
  30. package/dist/tests/playwright/pointer-callout.spec.d.ts +2 -0
  31. package/dist/tests/playwright/pointer-callout.spec.d.ts.map +1 -0
  32. package/package.json +6 -2
  33. package/dist/class-generation/BasePage.d.ts.map +0 -1
  34. package/dist/class-generation/Pointer.d.ts.map +0 -1
  35. package/dist/tests/fixtures/generated-tsc/BasePage.full.d.ts.map +0 -1
  36. package/dist/tests/fixtures/generated-tsc/BasePage.minimal.d.ts.map +0 -1
  37. package/dist/tests/fixtures/generated-tsc/Pointer.d.ts.map +0 -1
@@ -0,0 +1,827 @@
1
+ import { computePosition, limitShift, offset, shift } from "./floating-ui";
2
+ import type { PwLocator, PwPage } from "./playwright-types";
3
+
4
+ const __PW_CURSOR_ID__ = "__pw_cursor__";
5
+ const __PW_CURSOR_ANNOTATION_ID__ = "__pw_cursor_annotation__";
6
+ const __PW_CURSOR_ANNOTATION_CONTENT_ID__ = "__pw_cursor_annotation_content__";
7
+ const __PW_CURSOR_ANNOTATION_ARROW_ID__ = "__pw_cursor_annotation_arrow__";
8
+ const __PW_CURSOR_ANNOTATION_AVOID_SELECTOR__
9
+ = [
10
+ "[data-callout-avoid]",
11
+ "button",
12
+ "input",
13
+ "textarea",
14
+ "select",
15
+ "summary",
16
+ "a[href]",
17
+ "[role='button']",
18
+ "[role='link']",
19
+ "[role='textbox']",
20
+ "[role='combobox']",
21
+ "[role='option']",
22
+ "[role='tab']",
23
+ "[role='menuitem']",
24
+ "[contenteditable='']",
25
+ "[contenteditable='true']",
26
+ "[contenteditable]:not([contenteditable='false'])",
27
+ ].join(",");
28
+ const __PW_CURSOR_ANNOTATION_MARGIN__ = 18;
29
+ const __PW_CURSOR_ANNOTATION_GAP__ = 18;
30
+ const __PW_CURSOR_ANNOTATION_ARROW_SIZE__ = 14;
31
+ const __PW_CURSOR_ANNOTATION_AVOID_PADDING__ = 12;
32
+ const __PW_CURSOR_ANNOTATION_BACKGROUND__ = "#dc2626";
33
+ const __PW_CURSOR_ANNOTATION_BORDER__ = "0px solid transparent";
34
+ const __PW_CURSOR_ANNOTATION_BOX_SHADOW__ = "0 20px 44px rgba(127, 29, 29, 0.32)";
35
+ const __PW_CURSOR_ANNOTATION_TEXT_COLOR__ = "#f8fafc";
36
+ const __PW_CURSOR_ANNOTATION_RADIUS__ = 0;
37
+
38
+ type Placement =
39
+ | "top"
40
+ | "top-start"
41
+ | "top-end"
42
+ | "right"
43
+ | "right-start"
44
+ | "right-end"
45
+ | "bottom"
46
+ | "bottom-start"
47
+ | "bottom-end"
48
+ | "left"
49
+ | "left-start"
50
+ | "left-end";
51
+
52
+ const __PW_CURSOR_ALLOWED_PLACEMENTS__: Placement[] = [
53
+ "top-start",
54
+ "top",
55
+ "top-end",
56
+ "right-start",
57
+ "right",
58
+ "right-end",
59
+ "bottom-start",
60
+ "bottom",
61
+ "bottom-end",
62
+ "left-start",
63
+ "left",
64
+ "left-end",
65
+ ];
66
+
67
+ export type ElementTarget = string | PwLocator;
68
+
69
+ export interface CalloutTargetBox {
70
+ x: number;
71
+ y: number;
72
+ width: number;
73
+ height: number;
74
+ }
75
+
76
+ interface CalloutContext {
77
+ avoidRects: CalloutTargetBox[];
78
+ protectedTargetRects: CalloutTargetBox[];
79
+ viewportHeight: number;
80
+ viewportWidth: number;
81
+ }
82
+
83
+ interface FloatingVirtualElement extends CalloutTargetBox {
84
+ kind: "arrow" | "floating" | "reference";
85
+ }
86
+
87
+ interface CalloutLayout {
88
+ arrowX: number | null;
89
+ arrowY: number | null;
90
+ placement: Placement;
91
+ staticSide: "bottom" | "left" | "right" | "top";
92
+ x: number;
93
+ y: number;
94
+ }
95
+
96
+ export interface ShowCalloutOptions {
97
+ skipScroll?: boolean;
98
+ targetBox?: CalloutTargetBox;
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
+ async function __pw_compute_shifted_position__(
218
+ referenceRect: CalloutTargetBox,
219
+ floatingRect: CalloutTargetBox,
220
+ placement: Placement,
221
+ protectedRects: CalloutTargetBox[],
222
+ viewportWidth: number,
223
+ viewportHeight: number,
224
+ ): Promise<{
225
+ adjustmentDistance: number;
226
+ arrowX: number | null;
227
+ arrowY: number | null;
228
+ placement: Placement;
229
+ x: number;
230
+ y: number;
231
+ }> {
232
+ const platform = __pw_create_platform__(viewportWidth, viewportHeight);
233
+ const floatingElement = __pw_create_virtual_element__(floatingRect, "floating");
234
+ const referenceElement = __pw_create_virtual_element__(referenceRect, "reference");
235
+ const result = await computePosition(referenceElement, floatingElement, {
236
+ middleware: [
237
+ offset(__PW_CURSOR_ANNOTATION_GAP__),
238
+ shift({
239
+ limiter: limitShift({}),
240
+ padding: __PW_CURSOR_ANNOTATION_MARGIN__,
241
+ }),
242
+ ],
243
+ placement,
244
+ platform,
245
+ strategy: "fixed",
246
+ });
247
+ const resolvedPlacement = result.placement as Placement;
248
+ const { side } = __pw_parse_placement__(resolvedPlacement);
249
+ let x = Math.round(result.x);
250
+ let y = Math.round(result.y);
251
+ const baseX = x;
252
+ const baseY = y;
253
+ let layoutAdjustedForProtectedRect = false;
254
+
255
+ for (const protectedRect of protectedRects) {
256
+ const horizontalOverlap = x < protectedRect.x + protectedRect.width && x + floatingRect.width > protectedRect.x;
257
+ const verticalOverlap = y < protectedRect.y + protectedRect.height && y + floatingRect.height > protectedRect.y;
258
+
259
+ if ((side === "top" || side === "bottom") && horizontalOverlap) {
260
+ if (side === "top" && verticalOverlap) {
261
+ y = Math.min(y, protectedRect.y - floatingRect.height);
262
+ layoutAdjustedForProtectedRect = true;
263
+ }
264
+ if (side === "bottom" && verticalOverlap) {
265
+ y = Math.max(y, protectedRect.y + protectedRect.height);
266
+ layoutAdjustedForProtectedRect = true;
267
+ }
268
+ }
269
+
270
+ if ((side === "left" || side === "right") && verticalOverlap) {
271
+ if (side === "left" && horizontalOverlap) {
272
+ x = Math.min(x, protectedRect.x - floatingRect.width);
273
+ layoutAdjustedForProtectedRect = true;
274
+ }
275
+ if (side === "right" && horizontalOverlap) {
276
+ x = Math.max(x, protectedRect.x + protectedRect.width);
277
+ layoutAdjustedForProtectedRect = true;
278
+ }
279
+ }
280
+ }
281
+
282
+ x = Math.min(
283
+ Math.max(x, __PW_CURSOR_ANNOTATION_MARGIN__),
284
+ Math.max(__PW_CURSOR_ANNOTATION_MARGIN__, viewportWidth - floatingRect.width - __PW_CURSOR_ANNOTATION_MARGIN__),
285
+ );
286
+ y = Math.min(
287
+ Math.max(y, __PW_CURSOR_ANNOTATION_MARGIN__),
288
+ Math.max(__PW_CURSOR_ANNOTATION_MARGIN__, viewportHeight - floatingRect.height - __PW_CURSOR_ANNOTATION_MARGIN__),
289
+ );
290
+ const adjustmentDistance = Math.abs(x - baseX) + Math.abs(y - baseY);
291
+ const referenceCenterX = referenceRect.x + referenceRect.width / 2;
292
+ const referenceCenterY = referenceRect.y + referenceRect.height / 2;
293
+ const arrowPadding = 10;
294
+ const arrowHalf = __PW_CURSOR_ANNOTATION_ARROW_SIZE__ / 2;
295
+ const middlewareData = result.middlewareData as { arrow?: { x?: number; y?: number } };
296
+ const baseArrowData = middlewareData.arrow;
297
+ const arrowX = side === "top" || side === "bottom"
298
+ ? !layoutAdjustedForProtectedRect && typeof baseArrowData?.x === "number"
299
+ ? Math.round(baseArrowData.x)
300
+ : Math.min(
301
+ Math.max(referenceCenterX - x - arrowHalf, arrowPadding),
302
+ Math.max(arrowPadding, floatingRect.width - __PW_CURSOR_ANNOTATION_ARROW_SIZE__ - arrowPadding),
303
+ )
304
+ : null;
305
+ const arrowY = side === "left" || side === "right"
306
+ ? !layoutAdjustedForProtectedRect && typeof baseArrowData?.y === "number"
307
+ ? Math.round(baseArrowData.y)
308
+ : Math.min(
309
+ Math.max(referenceCenterY - y - arrowHalf, arrowPadding),
310
+ Math.max(arrowPadding, floatingRect.height - __PW_CURSOR_ANNOTATION_ARROW_SIZE__ - arrowPadding),
311
+ )
312
+ : null;
313
+
314
+ return {
315
+ adjustmentDistance,
316
+ arrowX,
317
+ arrowY,
318
+ placement: resolvedPlacement,
319
+ x,
320
+ y,
321
+ };
322
+ }
323
+
324
+ async function __pw_compute_callout_layout__(
325
+ targetRect: CalloutTargetBox,
326
+ floatingRect: CalloutTargetBox,
327
+ context: CalloutContext,
328
+ ): Promise<CalloutLayout> {
329
+ const referenceRect = targetRect;
330
+ let bestLayout: (CalloutLayout & { score: number }) | null = null;
331
+
332
+ for (const placement of __PW_CURSOR_ALLOWED_PLACEMENTS__) {
333
+ const result = await __pw_compute_shifted_position__(
334
+ referenceRect,
335
+ floatingRect,
336
+ placement,
337
+ context.protectedTargetRects,
338
+ context.viewportWidth,
339
+ context.viewportHeight,
340
+ );
341
+
342
+ const positionedRect: CalloutTargetBox = {
343
+ x: result.x,
344
+ y: result.y,
345
+ width: floatingRect.width,
346
+ height: floatingRect.height,
347
+ };
348
+ const protectedOverlap = context.protectedTargetRects.reduce(
349
+ (sum, avoidRect) => sum + __pw_overlap_area__(positionedRect, avoidRect),
350
+ 0,
351
+ );
352
+ const avoidOverlap = context.avoidRects.reduce(
353
+ (sum, avoidRect) => sum + __pw_overlap_area__(positionedRect, __pw_expand_rect__(avoidRect, __PW_CURSOR_ANNOTATION_AVOID_PADDING__)),
354
+ 0,
355
+ );
356
+ const targetGap = __pw_rect_gap__(positionedRect, targetRect);
357
+ const score = (protectedOverlap * 10)
358
+ + (avoidOverlap * 8)
359
+ + (result.adjustmentDistance * 60)
360
+ + (targetGap * 40)
361
+ + (__pw_rect_center_distance__(positionedRect, referenceRect) * 0.08);
362
+
363
+ if (!bestLayout || score < bestLayout.score) {
364
+ bestLayout = {
365
+ arrowX: result.arrowX,
366
+ arrowY: result.arrowY,
367
+ placement: result.placement,
368
+ score,
369
+ staticSide: (() => {
370
+ const side = __pw_parse_placement__(result.placement).side;
371
+ if (side === "bottom") return "top";
372
+ if (side === "left") return "right";
373
+ if (side === "right") return "left";
374
+ return "bottom";
375
+ })(),
376
+ x: result.x,
377
+ y: result.y,
378
+ };
379
+ }
380
+ }
381
+
382
+ if (!bestLayout) {
383
+ return {
384
+ arrowX: null,
385
+ arrowY: null,
386
+ placement: "bottom",
387
+ staticSide: "top",
388
+ x: targetRect.x,
389
+ y: targetRect.y,
390
+ };
391
+ }
392
+
393
+ return bestLayout;
394
+ }
395
+
396
+ function __pw_get_callout_dimensions__(annotationText: string): { bubbleHeight: number; bubbleWidth: number } {
397
+ const charsPerLine = 28;
398
+ return {
399
+ bubbleWidth: Math.min(320, Math.max(180, Math.min(annotationText.length, charsPerLine) * 7 + 44)),
400
+ bubbleHeight: Math.max(52, Math.ceil(Math.max(annotationText.length, 1) / charsPerLine) * 20 + 24),
401
+ };
402
+ }
403
+
404
+ async function __pw_ensure_callout__(page: PwPage): Promise<void> {
405
+ const exists = await page.evaluate(
406
+ ({ contentId, annotationId, arrowId }: { contentId: string; annotationId: string; arrowId: string }) =>
407
+ document.getElementById(annotationId) != null
408
+ && document.getElementById(contentId) != null
409
+ && document.getElementById(arrowId) != null,
410
+ {
411
+ arrowId: __PW_CURSOR_ANNOTATION_ARROW_ID__,
412
+ contentId: __PW_CURSOR_ANNOTATION_CONTENT_ID__,
413
+ annotationId: __PW_CURSOR_ANNOTATION_ID__,
414
+ },
415
+ );
416
+ if (exists) return;
417
+
418
+ await page.evaluate(
419
+ ({
420
+ annotationId,
421
+ contentId,
422
+ arrowId,
423
+ arrowSize,
424
+ background,
425
+ border,
426
+ borderRadius,
427
+ boxShadow,
428
+ textColor,
429
+ }: {
430
+ annotationId: string;
431
+ contentId: string;
432
+ arrowId: string;
433
+ arrowSize: number;
434
+ background: string;
435
+ border: string;
436
+ borderRadius: number;
437
+ boxShadow: string;
438
+ textColor: string;
439
+ }) => {
440
+ const annotation = document.createElement("div");
441
+ annotation.setAttribute("id", annotationId);
442
+ annotation.setAttribute(
443
+ "style",
444
+ [
445
+ "position:fixed",
446
+ "z-index:2147483647",
447
+ "pointer-events:none",
448
+ "left:18px",
449
+ "top:18px",
450
+ "width:220px",
451
+ "box-sizing:border-box",
452
+ "padding:12px 16px",
453
+ "border:" + border,
454
+ "border-radius:" + borderRadius + "px",
455
+ "background:" + background,
456
+ "color:" + textColor,
457
+ "font:600 13px/1.45 Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
458
+ "letter-spacing:0.01em",
459
+ "box-shadow:" + boxShadow,
460
+ "opacity:0",
461
+ "white-space:normal",
462
+ "transform:translate3d(0,0,0)",
463
+ "transform-origin:center",
464
+ "isolation:isolate",
465
+ ].join(";"),
466
+ );
467
+
468
+ const contentEl = document.createElement("div");
469
+ contentEl.setAttribute("id", contentId);
470
+ contentEl.setAttribute("style", "position:relative;z-index:1;");
471
+
472
+ const arrowEl = document.createElement("div");
473
+ arrowEl.setAttribute("id", arrowId);
474
+ arrowEl.setAttribute(
475
+ "style",
476
+ [
477
+ "position:absolute",
478
+ "width:" + arrowSize + "px",
479
+ "height:" + arrowSize + "px",
480
+ "background:" + background,
481
+ "transform:rotate(45deg)",
482
+ "pointer-events:none",
483
+ "left:0",
484
+ "top:0",
485
+ "box-shadow:0 12px 24px rgba(15,23,42,0.18)",
486
+ "z-index:-1",
487
+ "opacity:0",
488
+ ].join(";"),
489
+ );
490
+
491
+ annotation.appendChild(contentEl);
492
+ annotation.appendChild(arrowEl);
493
+ document.body.appendChild(annotation);
494
+ },
495
+ {
496
+ annotationId: __PW_CURSOR_ANNOTATION_ID__,
497
+ contentId: __PW_CURSOR_ANNOTATION_CONTENT_ID__,
498
+ arrowId: __PW_CURSOR_ANNOTATION_ARROW_ID__,
499
+ arrowSize: __PW_CURSOR_ANNOTATION_ARROW_SIZE__,
500
+ background: __PW_CURSOR_ANNOTATION_BACKGROUND__,
501
+ border: __PW_CURSOR_ANNOTATION_BORDER__,
502
+ borderRadius: __PW_CURSOR_ANNOTATION_RADIUS__,
503
+ boxShadow: __PW_CURSOR_ANNOTATION_BOX_SHADOW__,
504
+ textColor: __PW_CURSOR_ANNOTATION_TEXT_COLOR__,
505
+ },
506
+ );
507
+ }
508
+
509
+ export class Callout {
510
+ private readonly page: PwPage;
511
+
512
+ public constructor(page: PwPage) {
513
+ this.page = page;
514
+ }
515
+
516
+ private toLocator(target: ElementTarget): PwLocator {
517
+ return typeof target === "string" ? this.page.locator(target) : target;
518
+ }
519
+
520
+ public async hide(): Promise<void> {
521
+ await this.page.evaluate(
522
+ ({ annotationId, contentId, arrowId }: { annotationId: string; contentId: string; arrowId: string }) => {
523
+ const annotation = document.getElementById(annotationId) as HTMLDivElement | null;
524
+ const content = document.getElementById(contentId) as HTMLDivElement | null;
525
+ const arrow = document.getElementById(arrowId) as HTMLDivElement | null;
526
+ if (!annotation) {
527
+ return;
528
+ }
529
+
530
+ if (content) {
531
+ content.textContent = "";
532
+ }
533
+
534
+ annotation.style.transition = "opacity 120ms ease-in-out, transform 160ms ease-in-out";
535
+ annotation.style.opacity = "0";
536
+ annotation.style.transform = "scale(0.96)";
537
+ annotation.setAttribute("data-placement", "hidden");
538
+ if (arrow) {
539
+ arrow.style.opacity = "0";
540
+ arrow.style.left = "";
541
+ arrow.style.top = "";
542
+ arrow.style.right = "";
543
+ arrow.style.bottom = "";
544
+ }
545
+ },
546
+ {
547
+ annotationId: __PW_CURSOR_ANNOTATION_ID__,
548
+ contentId: __PW_CURSOR_ANNOTATION_CONTENT_ID__,
549
+ arrowId: __PW_CURSOR_ANNOTATION_ARROW_ID__,
550
+ },
551
+ );
552
+ }
553
+
554
+ public async showForElement(
555
+ target: ElementTarget,
556
+ annotationText: string,
557
+ options?: ShowCalloutOptions,
558
+ ): Promise<void> {
559
+ const text = annotationText.trim();
560
+ if (!text) {
561
+ await this.hide();
562
+ return;
563
+ }
564
+
565
+ const locator = this.toLocator(target);
566
+ if (!options?.skipScroll) {
567
+ try {
568
+ await locator.first().scrollIntoViewIfNeeded();
569
+ }
570
+ catch {
571
+ // Element may detach during navigation; the bounding-box lookup will surface the failure.
572
+ }
573
+ }
574
+
575
+ const targetBox = options?.targetBox ?? await locator.first().boundingBox();
576
+ if (!targetBox) {
577
+ throw new Error("Callout.showForElement: target has no bounding box");
578
+ }
579
+
580
+ await __pw_ensure_callout__(this.page);
581
+ const { bubbleHeight, bubbleWidth } = __pw_get_callout_dimensions__(text);
582
+ const context = await this.page.evaluate(
583
+ ({
584
+ annotationId,
585
+ arrowId,
586
+ avoidSelector,
587
+ contentId,
588
+ cursorId,
589
+ ex,
590
+ ey,
591
+ }: {
592
+ annotationId: string;
593
+ arrowId: string;
594
+ avoidSelector: string;
595
+ contentId: string;
596
+ cursorId: string;
597
+ ex: number;
598
+ ey: number;
599
+ }) => {
600
+ type BrowserRect = { x: number; y: number; width: number; height: number };
601
+
602
+ const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));
603
+ const viewportWidth = Math.max(window.innerWidth || 0, document.documentElement.clientWidth, 1280);
604
+ const viewportHeight = Math.max(window.innerHeight || 0, document.documentElement.clientHeight, 720);
605
+ const viewportArea = viewportWidth * viewportHeight;
606
+ const overlayIds = new Set([annotationId, arrowId, contentId, cursorId]);
607
+
608
+ const toViewportRect = (candidateRect: { left: number; top: number; right: number; bottom: number } | DOMRect): BrowserRect | null => {
609
+ const left = clamp(candidateRect.left, 0, viewportWidth);
610
+ const top = clamp(candidateRect.top, 0, viewportHeight);
611
+ const right = clamp(candidateRect.right, 0, viewportWidth);
612
+ const bottom = clamp(candidateRect.bottom, 0, viewportHeight);
613
+ const width = right - left;
614
+ const height = bottom - top;
615
+ if (width < 24 || height < 24) {
616
+ return null;
617
+ }
618
+
619
+ return { x: left, y: top, width, height };
620
+ };
621
+
622
+ const pushUniqueRect = (rects: BrowserRect[], rect: BrowserRect | null) => {
623
+ if (!rect) {
624
+ return;
625
+ }
626
+
627
+ const duplicate = rects.some((existing) =>
628
+ Math.abs(existing.x - rect.x) < 1
629
+ && Math.abs(existing.y - rect.y) < 1
630
+ && Math.abs(existing.width - rect.width) < 1
631
+ && Math.abs(existing.height - rect.height) < 1,
632
+ );
633
+ if (!duplicate) {
634
+ rects.push(rect);
635
+ }
636
+ };
637
+
638
+ const collectAvoidRects = (): BrowserRect[] =>
639
+ Array.from(document.querySelectorAll<HTMLElement>(avoidSelector))
640
+ .filter((candidate) => !overlayIds.has(candidate.id))
641
+ .flatMap((candidate) => {
642
+ const computedStyle = window.getComputedStyle(candidate);
643
+ if (
644
+ computedStyle.display === "none"
645
+ || computedStyle.visibility === "hidden"
646
+ || Number.parseFloat(computedStyle.opacity || "1") <= 0.05
647
+ ) {
648
+ return [];
649
+ }
650
+
651
+ const normalizedRect = toViewportRect(candidate.getBoundingClientRect());
652
+ if (!normalizedRect) {
653
+ return [];
654
+ }
655
+
656
+ if (!candidate.hasAttribute("data-callout-avoid") && normalizedRect.width * normalizedRect.height > viewportArea * 0.35) {
657
+ return [];
658
+ }
659
+
660
+ return [normalizedRect];
661
+ });
662
+
663
+ const collectProtectedTargetRects = (): BrowserRect[] => {
664
+ const protectedRects: BrowserRect[] = [];
665
+ if (typeof document.elementFromPoint !== "function") {
666
+ return protectedRects;
667
+ }
668
+
669
+ const targetElement = document.elementFromPoint(ex, ey);
670
+ if (!(targetElement instanceof HTMLElement)) {
671
+ return protectedRects;
672
+ }
673
+
674
+ const targetRect = toViewportRect(targetElement.getBoundingClientRect());
675
+ pushUniqueRect(protectedRects, targetRect);
676
+ if (!targetRect) {
677
+ return protectedRects;
678
+ }
679
+
680
+ const targetArea = targetRect.width * targetRect.height;
681
+ let ancestor = targetElement.parentElement;
682
+ while (ancestor && ancestor !== document.body) {
683
+ const computedStyle = window.getComputedStyle(ancestor);
684
+ if (
685
+ computedStyle.display === "none"
686
+ || computedStyle.visibility === "hidden"
687
+ || Number.parseFloat(computedStyle.opacity || "1") <= 0.05
688
+ ) {
689
+ ancestor = ancestor.parentElement;
690
+ continue;
691
+ }
692
+
693
+ const ancestorRect = toViewportRect(ancestor.getBoundingClientRect());
694
+ if (!ancestorRect) {
695
+ ancestor = ancestor.parentElement;
696
+ continue;
697
+ }
698
+
699
+ const ancestorArea = ancestorRect.width * ancestorRect.height;
700
+ const clearlyBiggerThanTarget = ancestorArea >= Math.max(targetArea * 1.75, 18_000);
701
+ const stillReasonablyLocal = ancestorArea <= viewportArea * 0.28;
702
+ const containsTarget = ancestorRect.x <= targetRect.x
703
+ && ancestorRect.y <= targetRect.y
704
+ && ancestorRect.x + ancestorRect.width >= targetRect.x + targetRect.width
705
+ && ancestorRect.y + ancestorRect.height >= targetRect.y + targetRect.height;
706
+ if (clearlyBiggerThanTarget && stillReasonablyLocal && containsTarget) {
707
+ pushUniqueRect(protectedRects, ancestorRect);
708
+ }
709
+
710
+ if (protectedRects.length >= 3) {
711
+ break;
712
+ }
713
+
714
+ ancestor = ancestor.parentElement;
715
+ }
716
+
717
+ return protectedRects;
718
+ };
719
+
720
+ return {
721
+ avoidRects: collectAvoidRects(),
722
+ protectedTargetRects: collectProtectedTargetRects(),
723
+ viewportHeight,
724
+ viewportWidth,
725
+ };
726
+ },
727
+ {
728
+ annotationId: __PW_CURSOR_ANNOTATION_ID__,
729
+ arrowId: __PW_CURSOR_ANNOTATION_ARROW_ID__,
730
+ avoidSelector: __PW_CURSOR_ANNOTATION_AVOID_SELECTOR__,
731
+ contentId: __PW_CURSOR_ANNOTATION_CONTENT_ID__,
732
+ cursorId: __PW_CURSOR_ID__,
733
+ ex: targetBox.x + targetBox.width / 2,
734
+ ey: targetBox.y + targetBox.height / 2,
735
+ },
736
+ );
737
+ const layout = await __pw_compute_callout_layout__(
738
+ { x: targetBox.x, y: targetBox.y, width: targetBox.width, height: targetBox.height },
739
+ { x: 0, y: 0, width: bubbleWidth, height: bubbleHeight },
740
+ context,
741
+ );
742
+
743
+ await this.page.evaluate(
744
+ ({
745
+ annotationId,
746
+ arrowId,
747
+ arrowSize,
748
+ background,
749
+ border,
750
+ borderRadius,
751
+ bubbleHeight,
752
+ bubbleWidth,
753
+ contentId,
754
+ fallbackX,
755
+ fallbackY,
756
+ layout,
757
+ text,
758
+ }: {
759
+ annotationId: string;
760
+ arrowId: string;
761
+ arrowSize: number;
762
+ background: string;
763
+ border: string;
764
+ borderRadius: number;
765
+ bubbleHeight: number;
766
+ bubbleWidth: number;
767
+ contentId: string;
768
+ fallbackX: number;
769
+ fallbackY: number;
770
+ layout: CalloutLayout | null;
771
+ text: string;
772
+ }) => {
773
+ const annotation = document.getElementById(annotationId) as HTMLDivElement | null;
774
+ const content = document.getElementById(contentId) as HTMLDivElement | null;
775
+ const arrow = document.getElementById(arrowId) as HTMLDivElement | null;
776
+ if (!annotation || !content) {
777
+ return;
778
+ }
779
+
780
+ content.textContent = text;
781
+ annotation.style.width = `${bubbleWidth}px`;
782
+ annotation.style.minHeight = `${bubbleHeight}px`;
783
+ annotation.style.background = background;
784
+ annotation.style.border = border;
785
+ annotation.style.borderRadius = `${borderRadius}px`;
786
+ annotation.style.transition = "opacity 120ms ease-in-out, transform 160ms ease-in-out";
787
+ annotation.style.willChange = "left, top, opacity, transform";
788
+ annotation.style.left = `${layout?.x ?? fallbackX}px`;
789
+ annotation.style.top = `${layout?.y ?? fallbackY}px`;
790
+ annotation.style.opacity = "1";
791
+ annotation.style.transform = "scale(1)";
792
+ annotation.setAttribute("data-placement", layout?.placement ?? "hidden");
793
+
794
+ if (arrow && layout) {
795
+ arrow.style.left = "";
796
+ arrow.style.top = "";
797
+ arrow.style.right = "";
798
+ arrow.style.bottom = "";
799
+ arrow.style.transform = "rotate(45deg)";
800
+ if (layout.arrowX !== null) {
801
+ arrow.style.left = `${layout.arrowX}px`;
802
+ }
803
+ if (layout.arrowY !== null) {
804
+ arrow.style.top = `${layout.arrowY}px`;
805
+ }
806
+ arrow.style.setProperty(layout.staticSide, `${Math.round(arrowSize / -2)}px`);
807
+ arrow.style.opacity = "1";
808
+ }
809
+ },
810
+ {
811
+ annotationId: __PW_CURSOR_ANNOTATION_ID__,
812
+ arrowId: __PW_CURSOR_ANNOTATION_ARROW_ID__,
813
+ arrowSize: __PW_CURSOR_ANNOTATION_ARROW_SIZE__,
814
+ background: __PW_CURSOR_ANNOTATION_BACKGROUND__,
815
+ border: __PW_CURSOR_ANNOTATION_BORDER__,
816
+ borderRadius: __PW_CURSOR_ANNOTATION_RADIUS__,
817
+ bubbleHeight,
818
+ bubbleWidth,
819
+ contentId: __PW_CURSOR_ANNOTATION_CONTENT_ID__,
820
+ fallbackX: targetBox.x + (targetBox.width / 2) + __PW_CURSOR_ANNOTATION_GAP__,
821
+ fallbackY: targetBox.y + (targetBox.height / 2) + __PW_CURSOR_ANNOTATION_GAP__,
822
+ layout,
823
+ text,
824
+ },
825
+ );
826
+ }
827
+ }