@gemx-dev/heatmap-react 3.5.34 → 3.5.35

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 (103) hide show
  1. package/dist/esm/components/Layout/ContentMetricBar.d.ts.map +1 -1
  2. package/dist/esm/components/Layout/ContentTopBar.d.ts.map +1 -1
  3. package/dist/esm/components/VizElement/ElementCallout.d.ts +5 -6
  4. package/dist/esm/components/VizElement/ElementCallout.d.ts.map +1 -1
  5. package/dist/esm/components/VizElement/ElementMissing.d.ts +6 -0
  6. package/dist/esm/components/VizElement/ElementMissing.d.ts.map +1 -0
  7. package/dist/{umd/components/VizElement/HoveredElementOverlay.d.ts → esm/components/VizElement/ElementOverlay.d.ts} +3 -3
  8. package/dist/esm/components/VizElement/ElementOverlay.d.ts.map +1 -0
  9. package/dist/esm/components/VizElement/HeatmapElements.d.ts +2 -1
  10. package/dist/esm/components/VizElement/HeatmapElements.d.ts.map +1 -1
  11. package/dist/esm/constants/index.d.ts +4 -4
  12. package/dist/esm/constants/index.d.ts.map +1 -1
  13. package/dist/esm/helpers/elm-callout.d.ts +9 -0
  14. package/dist/esm/helpers/elm-callout.d.ts.map +1 -0
  15. package/dist/esm/helpers/elm-getter.d.ts +6 -0
  16. package/dist/esm/helpers/elm-getter.d.ts.map +1 -0
  17. package/dist/esm/helpers/index.d.ts +3 -0
  18. package/dist/esm/helpers/index.d.ts.map +1 -1
  19. package/dist/esm/helpers/viz-elements.d.ts +1 -2
  20. package/dist/esm/helpers/viz-elements.d.ts.map +1 -1
  21. package/dist/esm/hooks/register/useRegisterControl.d.ts.map +1 -1
  22. package/dist/esm/hooks/vix-elements/index.d.ts +1 -0
  23. package/dist/esm/hooks/vix-elements/index.d.ts.map +1 -1
  24. package/dist/esm/hooks/vix-elements/useClickedElement.d.ts +2 -4
  25. package/dist/esm/hooks/vix-elements/useClickedElement.d.ts.map +1 -1
  26. package/dist/esm/hooks/vix-elements/useElementCalloutVisible.d.ts +7 -0
  27. package/dist/esm/hooks/vix-elements/useElementCalloutVisible.d.ts.map +1 -0
  28. package/dist/esm/hooks/vix-elements/useHoveredElement.d.ts +1 -2
  29. package/dist/esm/hooks/vix-elements/useHoveredElement.d.ts.map +1 -1
  30. package/dist/esm/index.js +450 -254
  31. package/dist/esm/index.mjs +450 -254
  32. package/dist/esm/stores/comp.d.ts.map +1 -1
  33. package/dist/esm/stores/interaction.d.ts +2 -0
  34. package/dist/esm/stores/interaction.d.ts.map +1 -1
  35. package/dist/esm/types/control.d.ts +3 -0
  36. package/dist/esm/types/control.d.ts.map +1 -1
  37. package/dist/esm/types/elm-callout.d.ts +9 -0
  38. package/dist/esm/types/elm-callout.d.ts.map +1 -0
  39. package/dist/esm/types/index.d.ts +1 -0
  40. package/dist/esm/types/index.d.ts.map +1 -1
  41. package/dist/esm/types/viz-element.d.ts +6 -5
  42. package/dist/esm/types/viz-element.d.ts.map +1 -1
  43. package/dist/esm/ui/BoxStack/BoxStack.d.ts +11 -1
  44. package/dist/esm/ui/BoxStack/BoxStack.d.ts.map +1 -1
  45. package/dist/esm/utils/debounce.d.ts +2 -0
  46. package/dist/esm/utils/debounce.d.ts.map +1 -0
  47. package/dist/style.css +13 -124
  48. package/dist/umd/components/Layout/ContentMetricBar.d.ts.map +1 -1
  49. package/dist/umd/components/Layout/ContentTopBar.d.ts.map +1 -1
  50. package/dist/umd/components/VizElement/ElementCallout.d.ts +5 -6
  51. package/dist/umd/components/VizElement/ElementCallout.d.ts.map +1 -1
  52. package/dist/umd/components/VizElement/ElementMissing.d.ts +6 -0
  53. package/dist/umd/components/VizElement/ElementMissing.d.ts.map +1 -0
  54. package/dist/{esm/components/VizElement/HoveredElementOverlay.d.ts → umd/components/VizElement/ElementOverlay.d.ts} +3 -3
  55. package/dist/umd/components/VizElement/ElementOverlay.d.ts.map +1 -0
  56. package/dist/umd/components/VizElement/HeatmapElements.d.ts +2 -1
  57. package/dist/umd/components/VizElement/HeatmapElements.d.ts.map +1 -1
  58. package/dist/umd/constants/index.d.ts +4 -4
  59. package/dist/umd/constants/index.d.ts.map +1 -1
  60. package/dist/umd/helpers/elm-callout.d.ts +9 -0
  61. package/dist/umd/helpers/elm-callout.d.ts.map +1 -0
  62. package/dist/umd/helpers/elm-getter.d.ts +6 -0
  63. package/dist/umd/helpers/elm-getter.d.ts.map +1 -0
  64. package/dist/umd/helpers/index.d.ts +3 -0
  65. package/dist/umd/helpers/index.d.ts.map +1 -1
  66. package/dist/umd/helpers/viz-elements.d.ts +1 -2
  67. package/dist/umd/helpers/viz-elements.d.ts.map +1 -1
  68. package/dist/umd/hooks/register/useRegisterControl.d.ts.map +1 -1
  69. package/dist/umd/hooks/vix-elements/index.d.ts +1 -0
  70. package/dist/umd/hooks/vix-elements/index.d.ts.map +1 -1
  71. package/dist/umd/hooks/vix-elements/useClickedElement.d.ts +2 -4
  72. package/dist/umd/hooks/vix-elements/useClickedElement.d.ts.map +1 -1
  73. package/dist/umd/hooks/vix-elements/useElementCalloutVisible.d.ts +7 -0
  74. package/dist/umd/hooks/vix-elements/useElementCalloutVisible.d.ts.map +1 -0
  75. package/dist/umd/hooks/vix-elements/useHoveredElement.d.ts +1 -2
  76. package/dist/umd/hooks/vix-elements/useHoveredElement.d.ts.map +1 -1
  77. package/dist/umd/index.js +2 -2
  78. package/dist/umd/stores/comp.d.ts.map +1 -1
  79. package/dist/umd/stores/interaction.d.ts +2 -0
  80. package/dist/umd/stores/interaction.d.ts.map +1 -1
  81. package/dist/umd/types/control.d.ts +3 -0
  82. package/dist/umd/types/control.d.ts.map +1 -1
  83. package/dist/umd/types/elm-callout.d.ts +9 -0
  84. package/dist/umd/types/elm-callout.d.ts.map +1 -0
  85. package/dist/umd/types/index.d.ts +1 -0
  86. package/dist/umd/types/index.d.ts.map +1 -1
  87. package/dist/umd/types/viz-element.d.ts +6 -5
  88. package/dist/umd/types/viz-element.d.ts.map +1 -1
  89. package/dist/umd/ui/BoxStack/BoxStack.d.ts +11 -1
  90. package/dist/umd/ui/BoxStack/BoxStack.d.ts.map +1 -1
  91. package/dist/umd/utils/debounce.d.ts +2 -0
  92. package/dist/umd/utils/debounce.d.ts.map +1 -0
  93. package/package.json +1 -1
  94. package/dist/esm/components/VizElement/ClickedElementOverlay.d.ts +0 -10
  95. package/dist/esm/components/VizElement/ClickedElementOverlay.d.ts.map +0 -1
  96. package/dist/esm/components/VizElement/HoveredElementOverlay.d.ts.map +0 -1
  97. package/dist/esm/components/VizElement/MissingElementMessage.d.ts +0 -6
  98. package/dist/esm/components/VizElement/MissingElementMessage.d.ts.map +0 -1
  99. package/dist/umd/components/VizElement/ClickedElementOverlay.d.ts +0 -10
  100. package/dist/umd/components/VizElement/ClickedElementOverlay.d.ts.map +0 -1
  101. package/dist/umd/components/VizElement/HoveredElementOverlay.d.ts.map +0 -1
  102. package/dist/umd/components/VizElement/MissingElementMessage.d.ts +0 -6
  103. package/dist/umd/components/VizElement/MissingElementMessage.d.ts.map +0 -1
package/dist/esm/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use client"
2
2
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
3
3
  import { useNodesState, ReactFlow, Controls, Background } from '@xyflow/react';
4
- import { useEffect, useCallback, useState, useRef, useMemo, Fragment as Fragment$1 } from 'react';
4
+ import { useEffect, useCallback, useState, useRef, useMemo, forwardRef, Fragment as Fragment$1 } from 'react';
5
5
  import { create } from 'zustand';
6
6
  import { Visualizer } from '@gemx-dev/clarity-visualize';
7
7
  import { createPortal } from 'react-dom';
@@ -72,6 +72,7 @@ const useHeatmapControlStore = create()((set, get) => {
72
72
  Toolbar: null,
73
73
  MetricBar: null,
74
74
  VizLoading: null,
75
+ ElementCallout: null,
75
76
  },
76
77
  registerControl: (key, control) => {
77
78
  set({
@@ -120,6 +121,8 @@ const useHeatmapInteractionStore = create()((set, get) => {
120
121
  setSelectedElement: (selectedElement) => set({ selectedElement }),
121
122
  hoveredElement: null,
122
123
  setHoveredElement: (hoveredElement) => set({ hoveredElement }),
124
+ shouldShowCallout: false,
125
+ setShouldShowCallout: (shouldShowCallout) => set({ shouldShowCallout }),
123
126
  };
124
127
  });
125
128
 
@@ -152,6 +155,7 @@ const useRegisterControl = (control) => {
152
155
  registerControl('Toolbar', control.Toolbar);
153
156
  registerControl('MetricBar', control.MetricBar);
154
157
  registerControl('VizLoading', control.VizLoading);
158
+ registerControl('ElementCallout', control.ElementCallout);
155
159
  };
156
160
 
157
161
  const useRegisterData = (data, dataInfo) => {
@@ -189,47 +193,251 @@ const useRegisterHeatmap = (clickmap) => {
189
193
  }, [clickmap]);
190
194
  };
191
195
 
192
- const isElementInViewport = (elementRect, visualRef, scale) => {
193
- const visualRect = visualRef.current?.getBoundingClientRect();
194
- if (!visualRect)
195
- return false;
196
- // Absolute position of element so visual container (not scaled)
197
- const elementTopRaw = elementRect.top - visualRect.top;
198
- const elementBottomRaw = elementTopRaw + elementRect.height;
199
- // Apply scale because wrapper is scaled
200
- const elementTop = elementTopRaw * scale;
201
- const elementBottom = elementBottomRaw * scale;
202
- const scrollTop = visualRef.current?.scrollTop || 0;
203
- const viewportHeight = visualRect.height;
204
- const viewportTop = scrollTop;
205
- const viewportBottom = scrollTop + viewportHeight;
206
- return elementTop >= viewportTop && elementBottom <= viewportBottom;
196
+ const PADDING = 0;
197
+ const ARROW_SIZE = 8;
198
+ const HORIZONTAL_OFFSET = 0;
199
+ // ============================================================================
200
+ // Viewport & Dimensions
201
+ // ============================================================================
202
+ const getViewportDimensions = () => ({
203
+ width: window.innerWidth,
204
+ height: window.innerHeight,
205
+ });
206
+ const getElementDimensions = (targetElm, calloutElm) => ({
207
+ targetRect: targetElm.getBoundingClientRect(),
208
+ calloutRect: calloutElm.getBoundingClientRect(),
209
+ });
210
+ // ============================================================================
211
+ // Alignment Order
212
+ // ============================================================================
213
+ const getAlignmentOrder = (alignment) => {
214
+ switch (alignment) {
215
+ case 'center':
216
+ return ['center', 'left', 'right'];
217
+ case 'left':
218
+ return ['left', 'center', 'right'];
219
+ case 'right':
220
+ return ['right', 'center', 'left'];
221
+ }
222
+ };
223
+ // ============================================================================
224
+ // Position Calculation
225
+ // ============================================================================
226
+ const calculateLeftPosition = ({ targetRect, calloutRect, hozOffset, align, }) => {
227
+ switch (align) {
228
+ case 'left':
229
+ return targetRect.left + hozOffset;
230
+ case 'right':
231
+ return targetRect.right - calloutRect.width - hozOffset;
232
+ case 'center':
233
+ default:
234
+ return targetRect.left + targetRect.width / 2 - calloutRect.width / 2;
235
+ }
236
+ };
237
+ const calculateVerticalPosition = (targetRect, calloutRect, placement, padding, arrowSize) => {
238
+ return placement === 'top'
239
+ ? targetRect.top - calloutRect.height - padding - arrowSize
240
+ : targetRect.bottom + padding + arrowSize;
241
+ };
242
+ const calculateHorizontalPlacementPosition = (targetRect, calloutRect, placement, padding, arrowSize) => {
243
+ const top = targetRect.top + targetRect.height / 2 - calloutRect.height / 2;
244
+ const left = placement === 'right'
245
+ ? targetRect.right + padding + arrowSize
246
+ : targetRect.left - calloutRect.width - padding - arrowSize;
247
+ return { top, left };
248
+ };
249
+ // ============================================================================
250
+ // Validation
251
+ // ============================================================================
252
+ const isLeftPositionValid = (leftPos, calloutWidth, viewportWidth, padding) => {
253
+ return leftPos >= padding && leftPos + calloutWidth <= viewportWidth - padding;
254
+ };
255
+ const isVerticalPositionValid = (targetRect, calloutRect, placement, viewportHeight, padding, arrowSize) => {
256
+ return placement === 'top'
257
+ ? targetRect.top - calloutRect.height - padding - arrowSize > 0
258
+ : targetRect.bottom + calloutRect.height + padding + arrowSize < viewportHeight;
259
+ };
260
+ const isHorizontalPlacementValid = (targetRect, calloutRect, placement, viewportWidth, padding, arrowSize) => {
261
+ return placement === 'right'
262
+ ? targetRect.right + calloutRect.width + padding + arrowSize < viewportWidth
263
+ : targetRect.left - calloutRect.width - padding - arrowSize > 0;
264
+ };
265
+ // ============================================================================
266
+ // Position Candidates Generation
267
+ // ============================================================================
268
+ const generateVerticalPositionCandidates = (targetRect, calloutRect, viewportHeight, viewportWidth, alignment, hozOffset, padding, arrowSize) => {
269
+ const candidates = [];
270
+ const placements = ['top', 'bottom'];
271
+ placements.forEach((placement) => {
272
+ const verticalPos = calculateVerticalPosition(targetRect, calloutRect, placement, padding, arrowSize);
273
+ const verticalValid = isVerticalPositionValid(targetRect, calloutRect, placement, viewportHeight, padding, arrowSize);
274
+ const alignmentOrder = getAlignmentOrder(alignment);
275
+ alignmentOrder.forEach((align) => {
276
+ const horizontalPos = calculateLeftPosition({
277
+ targetRect,
278
+ calloutRect,
279
+ hozOffset,
280
+ align,
281
+ });
282
+ candidates.push({
283
+ placement,
284
+ top: verticalPos,
285
+ left: horizontalPos,
286
+ horizontalAlign: align,
287
+ valid: verticalValid &&
288
+ isLeftPositionValid(horizontalPos, calloutRect.width, viewportWidth, padding),
289
+ });
290
+ });
291
+ });
292
+ return candidates;
293
+ };
294
+ const generateHorizontalPositionCandidates = (targetRect, calloutRect, viewportWidth, padding, arrowSize) => {
295
+ const placements = ['left', 'right'];
296
+ return placements.map((placement) => {
297
+ const { top, left } = calculateHorizontalPlacementPosition(targetRect, calloutRect, placement, padding, arrowSize);
298
+ return {
299
+ placement,
300
+ top,
301
+ left,
302
+ horizontalAlign: 'center',
303
+ valid: isHorizontalPlacementValid(targetRect, calloutRect, placement, viewportWidth, padding, arrowSize),
304
+ };
305
+ });
306
+ };
307
+ const generateAllPositionCandidates = (rectDimensions, viewport, alignment, hozOffset, padding, arrowSize) => {
308
+ const { targetRect, calloutRect } = rectDimensions;
309
+ const verticalCandidates = generateVerticalPositionCandidates(targetRect, calloutRect, viewport.height, viewport.width, alignment, hozOffset, padding, arrowSize);
310
+ const horizontalCandidates = generateHorizontalPositionCandidates(targetRect, calloutRect, viewport.width, padding, arrowSize);
311
+ return [...verticalCandidates, ...horizontalCandidates];
312
+ };
313
+ // ============================================================================
314
+ // Position Selection
315
+ // ============================================================================
316
+ const selectBestPosition = (candidates) => {
317
+ return candidates.find((p) => p.valid) || candidates[0];
318
+ };
319
+ // ============================================================================
320
+ // Viewport Boundary Adjustment
321
+ // ============================================================================
322
+ const constrainToViewport = (position, calloutRect, viewport, padding) => {
323
+ const left = Math.max(padding, Math.min(position.left, viewport.width - calloutRect.width - padding));
324
+ const top = Math.max(padding, Math.min(position.top, viewport.height - calloutRect.height - padding));
325
+ return { top, left };
326
+ };
327
+ // ============================================================================
328
+ // Main Function
329
+ // ============================================================================
330
+ const calcCalloutPosition = ({ targetElm, calloutElm, setPosition, hozOffset = HORIZONTAL_OFFSET, alignment = 'center', }) => {
331
+ return () => {
332
+ // 1. Get dimensions
333
+ const rectDimensions = getElementDimensions(targetElm, calloutElm);
334
+ const viewport = getViewportDimensions();
335
+ const padding = PADDING;
336
+ const arrowSize = ARROW_SIZE;
337
+ // 2. Generate all position candidates
338
+ const candidates = generateAllPositionCandidates(rectDimensions, viewport, alignment, hozOffset, padding, arrowSize);
339
+ // 3. Select best position
340
+ const bestPosition = selectBestPosition(candidates);
341
+ // 4. Constrain to viewport
342
+ const constrainedPosition = constrainToViewport({ top: bestPosition.top, left: bestPosition.left }, rectDimensions.calloutRect, viewport, padding);
343
+ // 5. Create final position object
344
+ const finalPosition = {
345
+ top: constrainedPosition.top,
346
+ left: constrainedPosition.left,
347
+ placement: bestPosition.placement,
348
+ horizontalAlign: bestPosition.horizontalAlign,
349
+ };
350
+ setPosition(finalPosition);
351
+ };
352
+ };
353
+
354
+ function getElementLayout(element) {
355
+ if (!element?.getBoundingClientRect)
356
+ return null;
357
+ const rect = element.getBoundingClientRect();
358
+ if (rect.width === 0 && rect.height === 0)
359
+ return null;
360
+ return {
361
+ top: rect.top,
362
+ left: rect.left,
363
+ width: rect.width,
364
+ height: rect.height,
365
+ };
366
+ }
367
+ const getElementAtPoint = (doc, x, y) => {
368
+ let el = null;
369
+ if ('caretPositionFromPoint' in doc) {
370
+ el = doc.caretPositionFromPoint(x, y)?.offsetNode ?? null;
371
+ }
372
+ el = el ?? doc.elementFromPoint(x, y);
373
+ let element = el;
374
+ while (element && element.nodeType === Node.TEXT_NODE) {
375
+ element = element.parentElement;
376
+ }
377
+ return element;
207
378
  };
208
- const calculateElementRank = (selectedElement, sortedElements) => {
209
- if (!sortedElements)
379
+ function getElementHash(element) {
380
+ return (element.getAttribute('data-clarity-hash') ||
381
+ element.getAttribute('data-clarity-hashalpha') ||
382
+ element.getAttribute('data-clarity-hashbeta'));
383
+ }
384
+ const getElementRank = (hash, elements) => {
385
+ if (!elements)
210
386
  return 0;
211
- return sortedElements.findIndex((e) => e.hash === selectedElement) + 1;
387
+ return elements.findIndex((e) => e.hash === hash) + 1;
212
388
  };
213
- const buildElementInfo = (selectedElement, info, rect, rank) => {
389
+ const buildElementInfo = (hash, rect, heatmapInfo) => {
390
+ if (!rect || !heatmapInfo)
391
+ return null;
392
+ const info = heatmapInfo.elementMapInfo?.[hash];
393
+ if (!info)
394
+ return null;
395
+ const rank = getElementRank(hash, heatmapInfo.sortedElements);
396
+ const clicks = info.totalclicks ?? 0;
397
+ const selector = info.selector ?? '';
214
398
  const baseInfo = {
215
- hash: selectedElement,
216
- clicks: info.totalclicks ?? 0,
399
+ hash,
400
+ clicks,
217
401
  rank,
218
- selector: info.selector ?? '',
402
+ selector,
219
403
  };
220
- if (rect) {
221
- return { ...baseInfo, ...rect };
222
- }
223
404
  return {
224
405
  ...baseInfo,
225
- left: 0,
226
- top: 0,
227
- width: 0,
228
- height: 0,
406
+ ...rect,
229
407
  };
230
408
  };
231
- const scrollToElementIfNeeded = (visualRef, wrapperRef, rect, scale) => {
232
- if (!visualRef.current || !wrapperRef.current)
409
+
410
+ function calculateRankPosition(rect, widthScale) {
411
+ const top = rect.top <= 18 ? rect.top + 3 : rect.top - 18;
412
+ const left = rect.left <= 18 ? rect.left + 3 : rect.left - 18;
413
+ return {
414
+ transform: `scale(${1.2 * widthScale})`,
415
+ top: Number.isNaN(top) ? undefined : top,
416
+ left: Number.isNaN(left) ? undefined : left,
417
+ };
418
+ }
419
+ function isElementInViewport(elementRect, visualRef, scale) {
420
+ if (!elementRect)
421
+ return false;
422
+ const visualRect = visualRef.current?.getBoundingClientRect();
423
+ if (!visualRect)
424
+ return false;
425
+ // Element position relative to the document (or container's content)
426
+ const elementTop = elementRect.top * scale;
427
+ const elementBottom = (elementRect.top + elementRect.height) * scale;
428
+ // Current scroll position
429
+ const scrollTop = visualRef.current?.scrollTop || 0;
430
+ const viewportHeight = visualRect.height;
431
+ // Visible viewport range in the scrollable content
432
+ const viewportTop = scrollTop;
433
+ const viewportBottom = scrollTop + viewportHeight;
434
+ // Check if element is within the visible viewport
435
+ // Element is visible if it overlaps with the viewport
436
+ return elementBottom > viewportTop && elementTop < viewportBottom;
437
+ }
438
+
439
+ const scrollToElementIfNeeded = (visualRef, rect, scale) => {
440
+ if (!visualRef.current)
233
441
  return;
234
442
  const visualRect = visualRef.current.getBoundingClientRect();
235
443
  if (isElementInViewport(rect, visualRef, scale)) {
@@ -245,13 +453,14 @@ const scrollToElementIfNeeded = (visualRef, wrapperRef, rect, scale) => {
245
453
  behavior: 'smooth',
246
454
  });
247
455
  };
248
- const useClickedElement = ({ visualRef, wrapperRef, getRect }) => {
456
+ const useClickedElement = ({ visualRef, getRect }) => {
249
457
  const heatmapInfo = useHeatmapDataStore((state) => state.dataInfo);
250
458
  const selectedElement = useHeatmapInteractionStore((state) => state.selectedElement);
459
+ const shouldShowCallout = useHeatmapInteractionStore((state) => state.shouldShowCallout);
460
+ const setShouldShowCallout = useHeatmapInteractionStore((state) => state.setShouldShowCallout);
251
461
  const scale = useHeatmapVizStore((state) => state.scale);
252
462
  const [clickedElement, setClickedElement] = useState(null);
253
463
  const [showMissingElement, setShowMissingElement] = useState(false);
254
- const [shouldShowCallout, setShouldShowCallout] = useState(false);
255
464
  const reset = () => {
256
465
  setClickedElement(null);
257
466
  setShowMissingElement(false);
@@ -267,26 +476,58 @@ const useClickedElement = ({ visualRef, wrapperRef, getRect }) => {
267
476
  setClickedElement(null);
268
477
  return;
269
478
  }
270
- const rect = getRect({ hash: selectedElement, selector: info.selector });
271
- const rank = calculateElementRank(selectedElement, heatmapInfo.sortedElements);
272
- const elementInfo = buildElementInfo(selectedElement, info, rect, rank);
273
- if (rect) {
274
- setShowMissingElement(false);
275
- scrollToElementIfNeeded(visualRef, wrapperRef, rect, scale);
276
- setShouldShowCallout(true);
277
- requestAnimationFrame(() => {
278
- setClickedElement(elementInfo);
279
- });
280
- }
281
- else {
479
+ const hash = selectedElement;
480
+ const selector = info.selector;
481
+ const rect = getRect({ hash: selectedElement, selector });
482
+ const elementInfo = buildElementInfo(hash, rect, heatmapInfo);
483
+ if (!rect) {
282
484
  setClickedElement(elementInfo);
283
485
  setShowMissingElement(true);
284
486
  setShouldShowCallout(false);
487
+ return;
285
488
  }
286
- }, [selectedElement, heatmapInfo, getRect, visualRef, wrapperRef, scale]);
489
+ setShowMissingElement(false);
490
+ scrollToElementIfNeeded(visualRef, rect, scale);
491
+ setShouldShowCallout(true);
492
+ requestAnimationFrame(() => {
493
+ setClickedElement(elementInfo);
494
+ });
495
+ }, [selectedElement, heatmapInfo, getRect, visualRef, scale]);
287
496
  return { clickedElement, showMissingElement, shouldShowCallout, setShouldShowCallout };
288
497
  };
289
498
 
499
+ const useElementCalloutVisible = ({ visualRef, getRect }) => {
500
+ const scale = useHeatmapVizStore((state) => state.scale);
501
+ const selectedElement = useHeatmapInteractionStore((state) => state.selectedElement);
502
+ const heatmapInfo = useHeatmapDataStore((state) => state.dataInfo);
503
+ const setShouldShowCallout = useHeatmapInteractionStore((state) => state.setShouldShowCallout);
504
+ useEffect(() => {
505
+ if (!selectedElement)
506
+ return;
507
+ const elementIsInViewportFn = () => {
508
+ const elementInfo = heatmapInfo?.elementMapInfo?.[selectedElement];
509
+ if (!elementInfo)
510
+ return;
511
+ const rect = getRect({ hash: selectedElement, selector: elementInfo.selector });
512
+ const isInViewport = isElementInViewport(rect, visualRef, scale);
513
+ setShouldShowCallout(isInViewport);
514
+ };
515
+ elementIsInViewportFn();
516
+ const handleUpdate = () => {
517
+ requestAnimationFrame(elementIsInViewportFn);
518
+ };
519
+ window.addEventListener('scroll', handleUpdate, true);
520
+ window.addEventListener('resize', handleUpdate);
521
+ visualRef?.current?.addEventListener('scroll', handleUpdate);
522
+ return () => {
523
+ window.removeEventListener('scroll', handleUpdate, true);
524
+ window.removeEventListener('resize', handleUpdate);
525
+ visualRef?.current?.removeEventListener('scroll', handleUpdate);
526
+ };
527
+ }, [selectedElement, visualRef, getRect, scale, heatmapInfo, setShouldShowCallout]);
528
+ return {};
529
+ };
530
+
290
531
  const useHeatmapEffects = ({ isVisible, isElementSidebarOpen, setShouldShowCallout, resetAll, }) => {
291
532
  const selectedElement = useHeatmapInteractionStore((state) => state.selectedElement);
292
533
  // Reset khi ẩn
@@ -305,32 +546,6 @@ const useHeatmapEffects = ({ isVisible, isElementSidebarOpen, setShouldShowCallo
305
546
  }, [isElementSidebarOpen, selectedElement, setShouldShowCallout]);
306
547
  };
307
548
 
308
- function getElementLayout(element) {
309
- if (!element?.getBoundingClientRect)
310
- return null;
311
- const rect = element.getBoundingClientRect();
312
- if (rect.width === 0 && rect.height === 0)
313
- return null;
314
- return {
315
- top: rect.top,
316
- left: rect.left,
317
- width: rect.width,
318
- height: rect.height,
319
- };
320
- }
321
- function formatPercentage(value, decimals = 2) {
322
- return value.toFixed(decimals);
323
- }
324
- function calculateRankPosition(rect, widthScale) {
325
- const top = rect.top <= 18 ? rect.top + 3 : rect.top - 18;
326
- const left = rect.left <= 18 ? rect.left + 3 : rect.left - 18;
327
- return {
328
- transform: `scale(${1.2 * widthScale})`,
329
- top: Number.isNaN(top) ? undefined : top,
330
- left: Number.isNaN(left) ? undefined : left,
331
- };
332
- }
333
-
334
549
  const useHeatmapElementPosition = ({ iframeRef, wrapperRef, visualizer }) => {
335
550
  const widthScale = useHeatmapVizStore((state) => state.scale);
336
551
  const iframeHeight = useHeatmapVizStore((state) => state.iframeHeight);
@@ -380,70 +595,63 @@ const debounce = (fn, delay) => {
380
595
  timeout = setTimeout(() => fn(...args), delay);
381
596
  };
382
597
  };
598
+
383
599
  const useHoveredElement = ({ iframeRef, getRect }) => {
384
600
  const hoveredElement = useHeatmapInteractionStore((state) => state.hoveredElement);
385
601
  const setHoveredElement = useHeatmapInteractionStore((state) => state.setHoveredElement);
386
602
  const onSelect = useHeatmapInteractionStore((state) => state.setSelectedElement);
387
603
  const widthScale = useHeatmapVizStore((state) => state.scale);
388
604
  const heatmapInfo = useHeatmapDataStore((state) => state.dataInfo);
389
- const handleMouseMove = useCallback(debounce((event) => {
390
- if (!iframeRef.current?.contentDocument || !heatmapInfo?.elementMapInfo) {
391
- setHoveredElement(null);
605
+ const reset = useCallback(() => {
606
+ setHoveredElement(null);
607
+ }, [setHoveredElement]);
608
+ const handleMouseLeave = useCallback(() => {
609
+ reset();
610
+ }, [reset]);
611
+ const getHashFromEvent = useCallback((event) => {
612
+ if (!heatmapInfo || !isIframeReady(iframeRef, heatmapInfo)) {
613
+ reset();
392
614
  return;
393
615
  }
394
616
  const iframe = iframeRef.current;
395
- const iframeRect = iframe.getBoundingClientRect();
396
- let x = event.clientX - iframeRect.left;
397
- let y = event.clientY - iframeRect.top;
398
- if (widthScale !== 1) {
399
- x /= widthScale;
400
- y /= widthScale;
401
- }
402
617
  const doc = iframe.contentDocument;
403
- if (!doc) {
404
- setHoveredElement(null);
618
+ const iframeRect = iframe.getBoundingClientRect();
619
+ const { x, y } = convertViewportToIframeCoords(event.clientX, event.clientY, iframeRect, widthScale);
620
+ const targetElement = findTargetElement(doc, x, y);
621
+ if (!targetElement || !isValidElement(targetElement, heatmapInfo)) {
622
+ reset();
405
623
  return;
406
624
  }
407
- let targetElement = getElementAtPoint(doc, x, y) || null;
408
- if (!targetElement) {
409
- targetElement = doc.elementFromPoint(x, y);
410
- }
411
- if (!targetElement) {
412
- setHoveredElement(null);
625
+ const hash = getElementHash(targetElement);
626
+ if (!!hash)
627
+ return hash;
628
+ reset();
629
+ return;
630
+ }, [heatmapInfo, iframeRef, getRect, widthScale, reset]);
631
+ const handleMouseMove = useCallback(debounce((event) => {
632
+ if (!heatmapInfo) {
633
+ reset();
413
634
  return;
414
635
  }
415
- const hash = targetElement.getAttribute('data-clarity-hash') ||
416
- targetElement.getAttribute('data-clarity-hashalpha') ||
417
- targetElement.getAttribute('data-clarity-hashbeta');
418
- if (!hash || !heatmapInfo.elementMapInfo[hash]) {
419
- setHoveredElement(null);
636
+ const hash = getHashFromEvent(event);
637
+ if (!hash) {
638
+ reset();
420
639
  return;
421
640
  }
422
- const info = heatmapInfo.elementMapInfo[hash];
423
- const position = getRect({ hash, selector: info.selector });
424
- if (position && heatmapInfo.sortedElements) {
425
- const rank = heatmapInfo.sortedElements.findIndex((e) => e.hash === hash) + 1;
426
- setHoveredElement({
427
- ...position,
428
- hash,
429
- clicks: info.totalclicks ?? 0,
430
- rank,
431
- selector: info.selector ?? '',
432
- });
433
- }
434
- else {
435
- setHoveredElement(null);
641
+ const selector = heatmapInfo?.elementMapInfo?.[hash];
642
+ const rect = getRect({ hash, selector });
643
+ const elementInfo = buildElementInfo(hash, rect, heatmapInfo);
644
+ if (!elementInfo) {
645
+ reset();
646
+ return;
436
647
  }
437
- }, 16), // ~60fps
438
- [iframeRef, heatmapInfo, getRect]);
439
- const handleMouseLeave = useCallback(() => {
440
- setHoveredElement(null);
441
- }, []);
648
+ setHoveredElement(elementInfo);
649
+ }, 16), [heatmapInfo, getRect, reset, getHashFromEvent]);
442
650
  const handleClick = useCallback(() => {
443
- if (hoveredElement?.hash && onSelect) {
444
- onSelect(hoveredElement.hash);
445
- }
446
- }, [hoveredElement, onSelect]);
651
+ if (!hoveredElement?.hash)
652
+ return;
653
+ onSelect(hoveredElement.hash);
654
+ }, [hoveredElement?.hash]);
447
655
  return {
448
656
  hoveredElement,
449
657
  handleMouseMove,
@@ -451,17 +659,32 @@ const useHoveredElement = ({ iframeRef, getRect }) => {
451
659
  handleClick,
452
660
  };
453
661
  };
454
- const getElementAtPoint = (doc, x, y) => {
455
- let el = null;
456
- if ('caretPositionFromPoint' in doc) {
457
- el = doc.caretPositionFromPoint(x, y)?.offsetNode ?? null;
662
+ const convertViewportToIframeCoords = (clientX, clientY, iframeRect, scale) => {
663
+ let x = clientX - iframeRect.left;
664
+ let y = clientY - iframeRect.top;
665
+ if (scale !== 1) {
666
+ x /= scale;
667
+ y /= scale;
458
668
  }
459
- el = el ?? doc.elementFromPoint(x, y);
460
- let element = el;
461
- while (element && element.nodeType === Node.TEXT_NODE) {
462
- element = element.parentElement;
669
+ return { x, y };
670
+ };
671
+ const findTargetElement = (doc, x, y) => {
672
+ let targetElement = getElementAtPoint(doc, x, y);
673
+ if (!targetElement) {
674
+ targetElement = doc.elementFromPoint(x, y);
463
675
  }
464
- return element;
676
+ return targetElement;
677
+ };
678
+ const isIframeReady = (iframeRef, heatmapInfo) => {
679
+ return !!(iframeRef.current?.contentDocument && heatmapInfo?.elementMapInfo);
680
+ };
681
+ const isValidElement = (element, heatmapInfo) => {
682
+ if (!element)
683
+ return false;
684
+ const hash = getElementHash(element);
685
+ if (!hash)
686
+ return false;
687
+ return !!heatmapInfo?.elementMapInfo?.[hash];
465
688
  };
466
689
 
467
690
  const useHeatmapRender = () => {
@@ -834,7 +1057,7 @@ const useHeatmapScale = (props) => {
834
1057
  };
835
1058
  };
836
1059
 
837
- const BoxStack = ({ children, ...props }) => {
1060
+ const BoxStack = forwardRef(({ children, ...props }, ref) => {
838
1061
  const id = props.id;
839
1062
  const flexDirection = props.flexDirection;
840
1063
  const overflow = props.overflow || 'hidden';
@@ -845,6 +1068,8 @@ const BoxStack = ({ children, ...props }) => {
845
1068
  const style = props.style || {};
846
1069
  const gap = props.gap || 0;
847
1070
  const height = props.height || 'auto';
1071
+ const zIndex = props.zIndex || 0;
1072
+ const backgroundColor = props.backgroundColor || 'transparent';
848
1073
  const styleGap = useMemo(() => {
849
1074
  switch (flexDirection) {
850
1075
  case 'row':
@@ -866,15 +1091,17 @@ const BoxStack = ({ children, ...props }) => {
866
1091
  justifyContent,
867
1092
  alignItems,
868
1093
  height,
1094
+ zIndex,
1095
+ backgroundColor,
869
1096
  ...styleGap,
870
1097
  ...style,
871
1098
  };
872
- return (jsx("div", { id: id, style: styleProps, children: children }));
873
- };
1099
+ return (jsx("div", { id: id, style: styleProps, ref: ref, children: children }));
1100
+ });
874
1101
 
875
1102
  const ContentTopBar = () => {
876
1103
  const controls = useHeatmapControlStore((state) => state.controls);
877
- return (jsx(BoxStack, { id: "gx-hm-content-header", flexDirection: "row", alignItems: "center", overflow: "auto", style: {
1104
+ return (jsx(BoxStack, { id: "gx-hm-content-header", flexDirection: "row", alignItems: "center", overflow: "auto", zIndex: 1, backgroundColor: "white", style: {
878
1105
  borderBottom: `${HEATMAP_CONFIG.borderWidth}px solid ${HEATMAP_CONFIG.borderColor}`,
879
1106
  }, children: controls.TopBar ?? null }));
880
1107
  };
@@ -912,110 +1139,14 @@ const useHeatmapVizCanvas = ({ type }) => {
912
1139
  return heatmapRender?.();
913
1140
  };
914
1141
 
915
- const CLICKED_ELEMENT_ID = 'clickedElement';
916
- const SECONDARY_CLICKED_ELEMENT_ID = 'secondaryClickedElementID';
917
- const HOVERED_ELEMENT_ID = 'hoveredElement';
918
- const SECONDARY_HOVERED_ELEMENT_ID = 'secondaryhoveredElementID';
919
-
920
- const ElementCallout = (props) => {
921
- const { element, target, isSecondary, language, wrapperRef } = props;
922
- const heatmapInfo = useHeatmapDataStore((state) => state.dataInfo);
923
- const calloutRef = useRef(null);
924
- const [position, setPosition] = useState({
925
- top: 0,
926
- left: 0,
927
- placement: 'top',
928
- });
929
- const totalClicks = heatmapInfo?.totalClicks ?? 1;
930
- const percentage = formatPercentage(((element.clicks ?? 0) / totalClicks) * 100, 2);
931
- useEffect(() => {
932
- const targetElement = document.querySelector(target);
933
- const calloutElement = calloutRef.current;
934
- if (!targetElement || !calloutElement)
935
- return;
936
- const calculatePosition = () => {
937
- const targetRect = targetElement.getBoundingClientRect();
938
- const calloutRect = calloutElement.getBoundingClientRect();
939
- const viewportWidth = window.innerWidth;
940
- const viewportHeight = window.innerHeight;
941
- const padding = 12; // Space between target and callout
942
- const arrowSize = 8;
943
- let top = 0;
944
- let left = 0;
945
- let placement = 'top';
946
- const positions = [
947
- {
948
- placement: 'top',
949
- top: targetRect.top - calloutRect.height - padding - arrowSize,
950
- left: targetRect.left + targetRect.width / 2 - calloutRect.width / 2,
951
- valid: targetRect.top - calloutRect.height - padding - arrowSize > 0,
952
- },
953
- {
954
- placement: 'bottom',
955
- top: targetRect.bottom + padding + arrowSize,
956
- left: targetRect.left + targetRect.width / 2 - calloutRect.width / 2,
957
- valid: targetRect.bottom + calloutRect.height + padding + arrowSize < viewportHeight,
958
- },
959
- {
960
- placement: 'right',
961
- top: targetRect.top + targetRect.height / 2 - calloutRect.height / 2,
962
- left: targetRect.right + padding + arrowSize,
963
- valid: targetRect.right + calloutRect.width + padding + arrowSize < viewportWidth,
964
- },
965
- {
966
- placement: 'left',
967
- top: targetRect.top + targetRect.height / 2 - calloutRect.height / 2,
968
- left: targetRect.left - calloutRect.width - padding - arrowSize,
969
- valid: targetRect.left - calloutRect.width - padding - arrowSize > 0,
970
- },
971
- ];
972
- // Find first valid position
973
- const validPosition = positions.find((p) => p.valid) || positions[0];
974
- top = validPosition.top;
975
- left = validPosition.left;
976
- placement = validPosition.placement;
977
- // Keep within viewport bounds
978
- left = Math.max(padding, Math.min(left, viewportWidth - calloutRect.width - padding));
979
- top = Math.max(padding, Math.min(top, viewportHeight - calloutRect.height - padding));
980
- setPosition({ top, left, placement });
981
- };
982
- calculatePosition();
983
- const handleUpdate = () => {
984
- requestAnimationFrame(calculatePosition);
985
- };
986
- window.addEventListener('scroll', handleUpdate, true);
987
- window.addEventListener('resize', handleUpdate);
988
- wrapperRef?.current?.addEventListener('scroll', handleUpdate);
989
- return () => {
990
- window.removeEventListener('scroll', handleUpdate, true);
991
- window.removeEventListener('resize', handleUpdate);
992
- wrapperRef?.current?.removeEventListener('scroll', handleUpdate);
993
- };
994
- }, [target, wrapperRef]);
995
- const calloutContent = (jsxs("div", { ref: calloutRef, className: `clarity-callout clarity-callout--${position.placement} ${isSecondary ? 'clarity-callout--secondary' : ''}`, style: {
996
- position: 'fixed',
997
- top: position.top,
998
- left: position.left,
999
- zIndex: 2147483647,
1000
- }, "aria-live": "assertive", role: "tooltip", children: [jsx("div", { className: "clarity-callout__arrow" }), jsx("div", { className: "clarity-callout__content", children: jsxs("div", { className: "clarity-callout__stats", children: [jsx("div", { className: "clarity-callout__label", children: "Clicks" }), jsxs("div", { className: "clarity-callout__value", children: [jsx("span", { className: "clarity-callout__count", children: element.clicks?.toLocaleString(language) }), jsxs("span", { className: "clarity-callout__percentage", children: ["(", percentage, "%)"] })] })] }) })] }));
1001
- return createPortal(calloutContent, document.getElementById('gx-hm-viz-container'));
1002
- };
1142
+ const CLICKED_ELEMENT_ID = 'gx-hm-clicked-element';
1143
+ const SECONDARY_CLICKED_ELEMENT_ID = 'gx-hm-secondary-clicked-element';
1144
+ const HOVERED_ELEMENT_ID = 'gx-hm-hovered-element';
1145
+ const SECONDARY_HOVERED_ELEMENT_ID = 'gx-hm-secondary-hovered-element';
1003
1146
 
1004
1147
  const RankBadge = ({ index, elementRect, widthScale, clickOnElement, }) => {
1005
1148
  const style = calculateRankPosition(elementRect, widthScale);
1006
- return (jsx("div", { className: "rankBadge", style: style, onClick: clickOnElement, "data-testid": "elementRank", children: index }));
1007
- };
1008
-
1009
- const ClickedElementOverlay = ({ element, shouldShowCallout, isSecondary, targetId, }) => {
1010
- const widthScale = useHeatmapVizStore((state) => state.scale);
1011
- if (!element || (element.width === 0 && element.height === 0))
1012
- return null;
1013
- return (jsxs(Fragment$1, { children: [jsx("div", { className: "heatmapElement", id: targetId, style: {
1014
- top: element.top,
1015
- left: element.left,
1016
- width: element.width,
1017
- height: element.height,
1018
- } }), jsx(RankBadge, { index: element.rank, elementRect: element, widthScale: widthScale }), shouldShowCallout && (jsx(ElementCallout, { element: element, target: `#${targetId}`, isSecondary: isSecondary }))] }));
1149
+ return (jsx("div", { className: "gx-hm-rank-badge", style: style, onClick: clickOnElement, children: index }));
1019
1150
  };
1020
1151
 
1021
1152
  const NUMBER_OF_TOP_ELEMENTS = 10;
@@ -1033,27 +1164,57 @@ const DefaultRankBadges = ({ getRect, hidden }) => {
1033
1164
  }) }));
1034
1165
  };
1035
1166
 
1036
- const HoveredElementOverlay = ({ element, onClick, isSecondary, targetId, }) => {
1037
- if (!element)
1038
- return null;
1039
- // Iframe has border, so we need to add it to the top position
1040
- const top = element.top + HEATMAP_CONFIG['borderWidthIframe'];
1041
- const left = element.left + HEATMAP_CONFIG['borderWidthIframe'];
1042
- const width = element.width;
1043
- const height = element.height;
1044
- return (jsxs(Fragment$1, { children: [jsx("div", { onClick: onClick, className: "heatmapElement hovered", id: targetId, style: {
1045
- top,
1046
- left,
1047
- width,
1048
- height,
1049
- cursor: 'pointer',
1050
- } }), jsx(RankBadge, { index: element.rank, elementRect: element, widthScale: 1, clickOnElement: onClick })] }));
1167
+ const DEFAULT_POSITION = {
1168
+ top: 0,
1169
+ left: 0,
1170
+ placement: 'top',
1171
+ horizontalAlign: 'center',
1172
+ };
1173
+ const ElementCallout = (props) => {
1174
+ const CompElementCallout = useHeatmapControlStore((state) => state.controls.ElementCallout);
1175
+ const { element, target, visualRef, hozOffset, alignment = 'left' } = props;
1176
+ const calloutRef = useRef(null);
1177
+ const [position, setPosition] = useState(DEFAULT_POSITION);
1178
+ useEffect(() => {
1179
+ const targetElm = document.querySelector(target);
1180
+ const calloutElm = calloutRef.current;
1181
+ if (!targetElm || !calloutElm)
1182
+ return;
1183
+ const positionFn = calcCalloutPosition({
1184
+ targetElm,
1185
+ calloutElm,
1186
+ setPosition,
1187
+ hozOffset,
1188
+ alignment,
1189
+ });
1190
+ positionFn();
1191
+ const handleUpdate = () => {
1192
+ requestAnimationFrame(positionFn);
1193
+ };
1194
+ window.addEventListener('scroll', handleUpdate, true);
1195
+ window.addEventListener('resize', handleUpdate);
1196
+ visualRef?.current?.addEventListener('scroll', handleUpdate);
1197
+ return () => {
1198
+ window.removeEventListener('scroll', handleUpdate, true);
1199
+ window.removeEventListener('resize', handleUpdate);
1200
+ visualRef?.current?.removeEventListener('scroll', handleUpdate);
1201
+ };
1202
+ }, [target, visualRef, hozOffset, alignment]);
1203
+ const calloutContent = (jsx("div", { ref: calloutRef, className: `clarity-callout clarity-callout--${position.placement} clarity-callout--align-${position.horizontalAlign}`, style: {
1204
+ position: 'fixed',
1205
+ top: position.top,
1206
+ left: position.left,
1207
+ zIndex: 2147483647,
1208
+ }, "aria-live": "assertive", role: "tooltip", children: CompElementCallout && jsx(CompElementCallout, { elementHash: element.hash }) }));
1209
+ return createPortal(calloutContent, document.getElementById('gx-hm-viz-container'));
1051
1210
  };
1052
1211
 
1053
- const MissingElementMessage = () => {
1212
+ const ElementMissing = ({ show = true }) => {
1213
+ if (!show)
1214
+ return null;
1054
1215
  const widthScale = useHeatmapVizStore((state) => state.scale);
1055
1216
  return (jsx("div", { className: "missingElement", style: {
1056
- position: 'absolute',
1217
+ position: 'fixed',
1057
1218
  top: '50%',
1058
1219
  left: '50%',
1059
1220
  transform: `translate(-50%, -50%) scale(${1 / widthScale})`,
@@ -1069,6 +1230,39 @@ const MissingElementMessage = () => {
1069
1230
  }, "aria-live": "assertive", children: "Element not visible on current screen" }));
1070
1231
  };
1071
1232
 
1233
+ const TARGET_ID_BY_TYPE = {
1234
+ hovered: {
1235
+ default: HOVERED_ELEMENT_ID,
1236
+ secondary: SECONDARY_HOVERED_ELEMENT_ID,
1237
+ },
1238
+ clicked: {
1239
+ default: CLICKED_ELEMENT_ID,
1240
+ secondary: SECONDARY_CLICKED_ELEMENT_ID,
1241
+ },
1242
+ };
1243
+ const ElementOverlay = ({ type, element, onClick, isSecondary }) => {
1244
+ const widthScale = useHeatmapVizStore((state) => state.scale);
1245
+ if (!element || (element.width === 0 && element.height === 0))
1246
+ return null;
1247
+ // Iframe has border, so we need to add it to the top position
1248
+ const top = element.top + HEATMAP_CONFIG['borderWidthIframe'];
1249
+ const left = element.left + HEATMAP_CONFIG['borderWidthIframe'];
1250
+ const width = element.width;
1251
+ const height = element.height;
1252
+ const targetId = TARGET_ID_BY_TYPE[type][isSecondary ? 'secondary' : 'default'];
1253
+ return (jsxs(Fragment$1, { children: [jsx("div", { onClick: onClick, className: "heatmapElement", id: targetId, style: {
1254
+ top,
1255
+ left,
1256
+ width,
1257
+ height,
1258
+ cursor: 'pointer',
1259
+ } }), jsx(RankBadge, { index: element.rank, elementRect: element, widthScale: type === 'hovered' ? 1 : widthScale, clickOnElement: onClick })] }));
1260
+ };
1261
+
1262
+ const ELEMENT_CALLOUT = {
1263
+ hozOffset: -8,
1264
+ alignment: 'left',
1265
+ };
1072
1266
  const HeatmapElements = (props) => {
1073
1267
  const height = useHeatmapVizStore((state) => state.iframeHeight);
1074
1268
  const { iframeRef, wrapperRef, visualRef, visualizer, iframeDimensions, isElementSidebarOpen, isVisible = true, areDefaultRanksHidden, isSecondary, ...rest } = props;
@@ -1078,15 +1272,17 @@ const HeatmapElements = (props) => {
1078
1272
  visualizer,
1079
1273
  });
1080
1274
  const { clickedElement, showMissingElement, shouldShowCallout, setShouldShowCallout } = useClickedElement({
1081
- iframeRef,
1082
1275
  visualRef,
1083
- wrapperRef,
1084
1276
  getRect,
1085
1277
  });
1086
1278
  const { hoveredElement, handleMouseMove, handleMouseLeave, handleClick } = useHoveredElement({
1087
1279
  iframeRef,
1088
1280
  getRect,
1089
1281
  });
1282
+ useElementCalloutVisible({
1283
+ visualRef,
1284
+ getRect,
1285
+ });
1090
1286
  const resetAll = () => {
1091
1287
  // setShouldShowCallout(false);
1092
1288
  };
@@ -1098,7 +1294,7 @@ const HeatmapElements = (props) => {
1098
1294
  });
1099
1295
  if (!isVisible)
1100
1296
  return null;
1101
- return (jsxs("div", { onMouseMove: handleMouseMove, onMouseLeave: handleMouseLeave, className: "heatmapElements gx-hm-elements", style: { ...iframeDimensions, height }, children: [jsx(DefaultRankBadges, { getRect: getRect, hidden: areDefaultRanksHidden }), jsx(ClickedElementOverlay, { element: clickedElement, shouldShowCallout: shouldShowCallout, isSecondary: isSecondary, targetId: isSecondary ? SECONDARY_CLICKED_ELEMENT_ID : CLICKED_ELEMENT_ID, ...rest }), showMissingElement && jsx(MissingElementMessage, {}), jsx(HoveredElementOverlay, { element: hoveredElement, onClick: handleClick, isSecondary: isSecondary, targetId: isSecondary ? SECONDARY_HOVERED_ELEMENT_ID : HOVERED_ELEMENT_ID }), hoveredElement?.hash !== clickedElement?.hash && hoveredElement && (jsx(ElementCallout, { element: hoveredElement, target: `#${props.isSecondary ? SECONDARY_HOVERED_ELEMENT_ID : HOVERED_ELEMENT_ID}`, isSecondary: props.isSecondary, wrapperRef: props.wrapperRef }))] }));
1297
+ return (jsxs("div", { onMouseMove: handleMouseMove, onMouseLeave: handleMouseLeave, className: "gx-hm-elements", style: { ...iframeDimensions, height }, children: [jsx(ElementMissing, { show: showMissingElement }), jsx(DefaultRankBadges, { getRect: getRect, hidden: areDefaultRanksHidden }), jsx(ElementOverlay, { type: "clicked", element: clickedElement, isSecondary: isSecondary }), jsx(ElementOverlay, { type: "hovered", element: hoveredElement, isSecondary: isSecondary, onClick: handleClick }), hoveredElement?.hash !== clickedElement?.hash && hoveredElement && (jsx(ElementCallout, { element: hoveredElement, target: `#${HOVERED_ELEMENT_ID}`, visualRef: visualRef, ...ELEMENT_CALLOUT })), shouldShowCallout && clickedElement && (jsx(ElementCallout, { element: clickedElement, target: `#${CLICKED_ELEMENT_ID}`, visualRef: visualRef, ...ELEMENT_CALLOUT }))] }));
1102
1298
  };
1103
1299
 
1104
1300
  const VizElements = ({ iframeRef, visualRef, wrapperRef }) => {
@@ -1227,7 +1423,7 @@ const VizDomContainer = () => {
1227
1423
 
1228
1424
  const ContentMetricBar = () => {
1229
1425
  const controls = useHeatmapControlStore((state) => state.controls);
1230
- return (jsx(BoxStack, { id: "gx-hm-content-header", flexDirection: "row", alignItems: "center", overflow: "auto", style: {
1426
+ return (jsx(BoxStack, { id: "gx-hm-content-metric-bar", flexDirection: "row", alignItems: "center", overflow: "auto", zIndex: 1, backgroundColor: "white", style: {
1231
1427
  borderBottom: `${HEATMAP_CONFIG.borderWidth}px solid ${HEATMAP_CONFIG.borderColor}`,
1232
1428
  }, children: controls.MetricBar ?? null }));
1233
1429
  };