@gemx-dev/heatmap-react 3.5.33 → 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 (141) hide show
  1. package/dist/esm/components/Layout/ContentMetricBar.d.ts.map +1 -1
  2. package/dist/esm/components/Layout/ContentToolbar.d.ts.map +1 -1
  3. package/dist/esm/components/Layout/ContentTopBar.d.ts.map +1 -1
  4. package/dist/esm/components/Layout/HeatmapLayout.d.ts.map +1 -1
  5. package/dist/esm/components/VizDom/VizDomRenderer.d.ts.map +1 -1
  6. package/dist/esm/components/VizElement/DefaultRankBadges.d.ts +0 -3
  7. package/dist/esm/components/VizElement/DefaultRankBadges.d.ts.map +1 -1
  8. package/dist/esm/components/VizElement/ElementCallout.d.ts +5 -12
  9. package/dist/esm/components/VizElement/ElementCallout.d.ts.map +1 -1
  10. package/dist/esm/components/VizElement/ElementMissing.d.ts +6 -0
  11. package/dist/esm/components/VizElement/ElementMissing.d.ts.map +1 -0
  12. package/dist/esm/components/VizElement/ElementOverlay.d.ts +10 -0
  13. package/dist/esm/components/VizElement/ElementOverlay.d.ts.map +1 -0
  14. package/dist/esm/components/VizElement/HeatmapElements.d.ts +4 -8
  15. package/dist/esm/components/VizElement/HeatmapElements.d.ts.map +1 -1
  16. package/dist/esm/components/VizElement/VizElements.d.ts +1 -2
  17. package/dist/esm/components/VizElement/VizElements.d.ts.map +1 -1
  18. package/dist/esm/configs/style.d.ts +2 -0
  19. package/dist/esm/configs/style.d.ts.map +1 -1
  20. package/dist/esm/constants/index.d.ts +4 -4
  21. package/dist/esm/constants/index.d.ts.map +1 -1
  22. package/dist/esm/helpers/elm-callout.d.ts +9 -0
  23. package/dist/esm/helpers/elm-callout.d.ts.map +1 -0
  24. package/dist/esm/helpers/elm-getter.d.ts +6 -0
  25. package/dist/esm/helpers/elm-getter.d.ts.map +1 -0
  26. package/dist/esm/helpers/index.d.ts +3 -0
  27. package/dist/esm/helpers/index.d.ts.map +1 -1
  28. package/dist/esm/helpers/viz-elements.d.ts +1 -2
  29. package/dist/esm/helpers/viz-elements.d.ts.map +1 -1
  30. package/dist/esm/hooks/register/useRegisterControl.d.ts.map +1 -1
  31. package/dist/esm/hooks/vix-elements/index.d.ts +1 -0
  32. package/dist/esm/hooks/vix-elements/index.d.ts.map +1 -1
  33. package/dist/esm/hooks/vix-elements/useClickedElement.d.ts +4 -4
  34. package/dist/esm/hooks/vix-elements/useClickedElement.d.ts.map +1 -1
  35. package/dist/esm/hooks/vix-elements/useElementCalloutVisible.d.ts +7 -0
  36. package/dist/esm/hooks/vix-elements/useElementCalloutVisible.d.ts.map +1 -0
  37. package/dist/esm/hooks/vix-elements/useHeatmapElementPosition.d.ts +2 -5
  38. package/dist/esm/hooks/vix-elements/useHeatmapElementPosition.d.ts.map +1 -1
  39. package/dist/esm/hooks/vix-elements/useHoveredElement.d.ts +1 -5
  40. package/dist/esm/hooks/vix-elements/useHoveredElement.d.ts.map +1 -1
  41. package/dist/esm/hooks/viz-render/useHeatmapRender.d.ts.map +1 -1
  42. package/dist/esm/hooks/viz-scale/useContentDimensions.d.ts +0 -2
  43. package/dist/esm/hooks/viz-scale/useContentDimensions.d.ts.map +1 -1
  44. package/dist/esm/hooks/viz-scale/useHeatmapScale.d.ts +1 -4
  45. package/dist/esm/hooks/viz-scale/useHeatmapScale.d.ts.map +1 -1
  46. package/dist/esm/hooks/viz-scale/useScrollSync.d.ts +1 -2
  47. package/dist/esm/hooks/viz-scale/useScrollSync.d.ts.map +1 -1
  48. package/dist/esm/index.js +552 -410
  49. package/dist/esm/index.mjs +552 -410
  50. package/dist/esm/stores/comp.d.ts.map +1 -1
  51. package/dist/esm/stores/interaction.d.ts +2 -0
  52. package/dist/esm/stores/interaction.d.ts.map +1 -1
  53. package/dist/esm/types/control.d.ts +3 -0
  54. package/dist/esm/types/control.d.ts.map +1 -1
  55. package/dist/esm/types/elm-callout.d.ts +9 -0
  56. package/dist/esm/types/elm-callout.d.ts.map +1 -0
  57. package/dist/esm/types/index.d.ts +1 -0
  58. package/dist/esm/types/index.d.ts.map +1 -1
  59. package/dist/esm/types/viz-element.d.ts +7 -5
  60. package/dist/esm/types/viz-element.d.ts.map +1 -1
  61. package/dist/esm/ui/BoxStack/BoxStack.d.ts +11 -1
  62. package/dist/esm/ui/BoxStack/BoxStack.d.ts.map +1 -1
  63. package/dist/esm/utils/debounce.d.ts +2 -0
  64. package/dist/esm/utils/debounce.d.ts.map +1 -0
  65. package/dist/style.css +42 -8
  66. package/dist/umd/components/Layout/ContentMetricBar.d.ts.map +1 -1
  67. package/dist/umd/components/Layout/ContentToolbar.d.ts.map +1 -1
  68. package/dist/umd/components/Layout/ContentTopBar.d.ts.map +1 -1
  69. package/dist/umd/components/Layout/HeatmapLayout.d.ts.map +1 -1
  70. package/dist/umd/components/VizDom/VizDomRenderer.d.ts.map +1 -1
  71. package/dist/umd/components/VizElement/DefaultRankBadges.d.ts +0 -3
  72. package/dist/umd/components/VizElement/DefaultRankBadges.d.ts.map +1 -1
  73. package/dist/umd/components/VizElement/ElementCallout.d.ts +5 -12
  74. package/dist/umd/components/VizElement/ElementCallout.d.ts.map +1 -1
  75. package/dist/umd/components/VizElement/ElementMissing.d.ts +6 -0
  76. package/dist/umd/components/VizElement/ElementMissing.d.ts.map +1 -0
  77. package/dist/umd/components/VizElement/ElementOverlay.d.ts +10 -0
  78. package/dist/umd/components/VizElement/ElementOverlay.d.ts.map +1 -0
  79. package/dist/umd/components/VizElement/HeatmapElements.d.ts +4 -8
  80. package/dist/umd/components/VizElement/HeatmapElements.d.ts.map +1 -1
  81. package/dist/umd/components/VizElement/VizElements.d.ts +1 -2
  82. package/dist/umd/components/VizElement/VizElements.d.ts.map +1 -1
  83. package/dist/umd/configs/style.d.ts +2 -0
  84. package/dist/umd/configs/style.d.ts.map +1 -1
  85. package/dist/umd/constants/index.d.ts +4 -4
  86. package/dist/umd/constants/index.d.ts.map +1 -1
  87. package/dist/umd/helpers/elm-callout.d.ts +9 -0
  88. package/dist/umd/helpers/elm-callout.d.ts.map +1 -0
  89. package/dist/umd/helpers/elm-getter.d.ts +6 -0
  90. package/dist/umd/helpers/elm-getter.d.ts.map +1 -0
  91. package/dist/umd/helpers/index.d.ts +3 -0
  92. package/dist/umd/helpers/index.d.ts.map +1 -1
  93. package/dist/umd/helpers/viz-elements.d.ts +1 -2
  94. package/dist/umd/helpers/viz-elements.d.ts.map +1 -1
  95. package/dist/umd/hooks/register/useRegisterControl.d.ts.map +1 -1
  96. package/dist/umd/hooks/vix-elements/index.d.ts +1 -0
  97. package/dist/umd/hooks/vix-elements/index.d.ts.map +1 -1
  98. package/dist/umd/hooks/vix-elements/useClickedElement.d.ts +4 -4
  99. package/dist/umd/hooks/vix-elements/useClickedElement.d.ts.map +1 -1
  100. package/dist/umd/hooks/vix-elements/useElementCalloutVisible.d.ts +7 -0
  101. package/dist/umd/hooks/vix-elements/useElementCalloutVisible.d.ts.map +1 -0
  102. package/dist/umd/hooks/vix-elements/useHeatmapElementPosition.d.ts +2 -5
  103. package/dist/umd/hooks/vix-elements/useHeatmapElementPosition.d.ts.map +1 -1
  104. package/dist/umd/hooks/vix-elements/useHoveredElement.d.ts +1 -5
  105. package/dist/umd/hooks/vix-elements/useHoveredElement.d.ts.map +1 -1
  106. package/dist/umd/hooks/viz-render/useHeatmapRender.d.ts.map +1 -1
  107. package/dist/umd/hooks/viz-scale/useContentDimensions.d.ts +0 -2
  108. package/dist/umd/hooks/viz-scale/useContentDimensions.d.ts.map +1 -1
  109. package/dist/umd/hooks/viz-scale/useHeatmapScale.d.ts +1 -4
  110. package/dist/umd/hooks/viz-scale/useHeatmapScale.d.ts.map +1 -1
  111. package/dist/umd/hooks/viz-scale/useScrollSync.d.ts +1 -2
  112. package/dist/umd/hooks/viz-scale/useScrollSync.d.ts.map +1 -1
  113. package/dist/umd/index.js +2 -2
  114. package/dist/umd/stores/comp.d.ts.map +1 -1
  115. package/dist/umd/stores/interaction.d.ts +2 -0
  116. package/dist/umd/stores/interaction.d.ts.map +1 -1
  117. package/dist/umd/types/control.d.ts +3 -0
  118. package/dist/umd/types/control.d.ts.map +1 -1
  119. package/dist/umd/types/elm-callout.d.ts +9 -0
  120. package/dist/umd/types/elm-callout.d.ts.map +1 -0
  121. package/dist/umd/types/index.d.ts +1 -0
  122. package/dist/umd/types/index.d.ts.map +1 -1
  123. package/dist/umd/types/viz-element.d.ts +7 -5
  124. package/dist/umd/types/viz-element.d.ts.map +1 -1
  125. package/dist/umd/ui/BoxStack/BoxStack.d.ts +11 -1
  126. package/dist/umd/ui/BoxStack/BoxStack.d.ts.map +1 -1
  127. package/dist/umd/utils/debounce.d.ts +2 -0
  128. package/dist/umd/utils/debounce.d.ts.map +1 -0
  129. package/package.json +1 -1
  130. package/dist/esm/components/VizElement/ClickedElementOverlay.d.ts +0 -17
  131. package/dist/esm/components/VizElement/ClickedElementOverlay.d.ts.map +0 -1
  132. package/dist/esm/components/VizElement/HoveredElementOverlay.d.ts +0 -12
  133. package/dist/esm/components/VizElement/HoveredElementOverlay.d.ts.map +0 -1
  134. package/dist/esm/components/VizElement/MissingElementMessage.d.ts +0 -7
  135. package/dist/esm/components/VizElement/MissingElementMessage.d.ts.map +0 -1
  136. package/dist/umd/components/VizElement/ClickedElementOverlay.d.ts +0 -17
  137. package/dist/umd/components/VizElement/ClickedElementOverlay.d.ts.map +0 -1
  138. package/dist/umd/components/VizElement/HoveredElementOverlay.d.ts +0 -12
  139. package/dist/umd/components/VizElement/HoveredElementOverlay.d.ts.map +0 -1
  140. package/dist/umd/components/VizElement/MissingElementMessage.d.ts +0 -7
  141. package/dist/umd/components/VizElement/MissingElementMessage.d.ts.map +0 -1
@@ -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';
@@ -49,7 +49,9 @@ const HEATMAP_IFRAME = {
49
49
  const HEATMAP_CONFIG = {
50
50
  padding: 8,
51
51
  borderWidth: 1,
52
+ borderWidthIframe: 2,
52
53
  borderColor: '#E3E3E3',
54
+ heightToolbar: 60, // height of the toolbar
53
55
  };
54
56
  const HEATMAP_STYLE = {
55
57
  viz: {
@@ -70,6 +72,7 @@ const useHeatmapControlStore = create()((set, get) => {
70
72
  Toolbar: null,
71
73
  MetricBar: null,
72
74
  VizLoading: null,
75
+ ElementCallout: null,
73
76
  },
74
77
  registerControl: (key, control) => {
75
78
  set({
@@ -118,6 +121,8 @@ const useHeatmapInteractionStore = create()((set, get) => {
118
121
  setSelectedElement: (selectedElement) => set({ selectedElement }),
119
122
  hoveredElement: null,
120
123
  setHoveredElement: (hoveredElement) => set({ hoveredElement }),
124
+ shouldShowCallout: false,
125
+ setShouldShowCallout: (shouldShowCallout) => set({ shouldShowCallout }),
121
126
  };
122
127
  });
123
128
 
@@ -150,6 +155,7 @@ const useRegisterControl = (control) => {
150
155
  registerControl('Toolbar', control.Toolbar);
151
156
  registerControl('MetricBar', control.MetricBar);
152
157
  registerControl('VizLoading', control.VizLoading);
158
+ registerControl('ElementCallout', control.ElementCallout);
153
159
  };
154
160
 
155
161
  const useRegisterData = (data, dataInfo) => {
@@ -187,17 +193,282 @@ const useRegisterHeatmap = (clickmap) => {
187
193
  }, [clickmap]);
188
194
  };
189
195
 
190
- const useClickedElement = ({ heatmapInfo, getRect }) => {
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;
378
+ };
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)
386
+ return 0;
387
+ return elements.findIndex((e) => e.hash === hash) + 1;
388
+ };
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 ?? '';
398
+ const baseInfo = {
399
+ hash,
400
+ clicks,
401
+ rank,
402
+ selector,
403
+ };
404
+ return {
405
+ ...baseInfo,
406
+ ...rect,
407
+ };
408
+ };
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)
441
+ return;
442
+ const visualRect = visualRef.current.getBoundingClientRect();
443
+ if (isElementInViewport(rect, visualRef, scale)) {
444
+ return;
445
+ }
446
+ const topRaw = rect.top; // - visualRect.top
447
+ const topScaled = topRaw * scale;
448
+ const viewportHeight = visualRect.height;
449
+ const elementHeightScaled = rect.height * scale;
450
+ const scrollTop = topScaled - (viewportHeight - elementHeightScaled) / 2;
451
+ visualRef.current.scrollTo({
452
+ top: Math.max(0, scrollTop),
453
+ behavior: 'smooth',
454
+ });
455
+ };
456
+ const useClickedElement = ({ visualRef, getRect }) => {
457
+ const heatmapInfo = useHeatmapDataStore((state) => state.dataInfo);
191
458
  const selectedElement = useHeatmapInteractionStore((state) => state.selectedElement);
459
+ const shouldShowCallout = useHeatmapInteractionStore((state) => state.shouldShowCallout);
460
+ const setShouldShowCallout = useHeatmapInteractionStore((state) => state.setShouldShowCallout);
461
+ const scale = useHeatmapVizStore((state) => state.scale);
192
462
  const [clickedElement, setClickedElement] = useState(null);
193
463
  const [showMissingElement, setShowMissingElement] = useState(false);
194
- const [shouldShowCallout, setShouldShowCallout] = useState(false);
464
+ const reset = () => {
465
+ setClickedElement(null);
466
+ setShowMissingElement(false);
467
+ setShouldShowCallout(false);
468
+ };
195
469
  useEffect(() => {
196
- console.log(`🚀 🐥 ~ useClickedElement ~ heatmapInfo?.elementMapInfo:`, heatmapInfo?.elementMapInfo);
197
470
  if (!selectedElement || !heatmapInfo?.elementMapInfo) {
198
- setClickedElement(null);
199
- setShowMissingElement(false);
200
- setShouldShowCallout(false);
471
+ reset();
201
472
  return;
202
473
  }
203
474
  const info = heatmapInfo.elementMapInfo[selectedElement];
@@ -205,38 +476,58 @@ const useClickedElement = ({ heatmapInfo, getRect }) => {
205
476
  setClickedElement(null);
206
477
  return;
207
478
  }
208
- const rect = getRect({ hash: selectedElement, selector: info.selector });
209
- if (rect && heatmapInfo.sortedElements) {
210
- const rank = heatmapInfo.sortedElements.findIndex((e) => e.hash === selectedElement) + 1;
211
- setClickedElement({
212
- ...rect,
213
- hash: selectedElement,
214
- clicks: info.totalclicks ?? 0,
215
- rank,
216
- selector: info.selector ?? '',
217
- });
218
- setShowMissingElement(false);
219
- setShouldShowCallout(true);
220
- }
221
- else {
222
- const rank = (heatmapInfo.sortedElements?.findIndex((e) => e.hash === selectedElement) ?? -1) + 1;
223
- setClickedElement({
224
- hash: selectedElement,
225
- clicks: info.totalclicks ?? 0,
226
- rank,
227
- selector: info.selector ?? '',
228
- left: 0,
229
- top: 0,
230
- width: 0,
231
- height: 0,
232
- });
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) {
484
+ setClickedElement(elementInfo);
233
485
  setShowMissingElement(true);
234
486
  setShouldShowCallout(false);
487
+ return;
235
488
  }
236
- }, [selectedElement, heatmapInfo, getRect]);
489
+ setShowMissingElement(false);
490
+ scrollToElementIfNeeded(visualRef, rect, scale);
491
+ setShouldShowCallout(true);
492
+ requestAnimationFrame(() => {
493
+ setClickedElement(elementInfo);
494
+ });
495
+ }, [selectedElement, heatmapInfo, getRect, visualRef, scale]);
237
496
  return { clickedElement, showMissingElement, shouldShowCallout, setShouldShowCallout };
238
497
  };
239
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
+
240
531
  const useHeatmapEffects = ({ isVisible, isElementSidebarOpen, setShouldShowCallout, resetAll, }) => {
241
532
  const selectedElement = useHeatmapInteractionStore((state) => state.selectedElement);
242
533
  // Reset khi ẩn
@@ -255,34 +546,10 @@ const useHeatmapEffects = ({ isVisible, isElementSidebarOpen, setShouldShowCallo
255
546
  }, [isElementSidebarOpen, selectedElement, setShouldShowCallout]);
256
547
  };
257
548
 
258
- function getElementLayout(element) {
259
- if (!element?.getBoundingClientRect)
260
- return null;
261
- const rect = element.getBoundingClientRect();
262
- if (rect.width === 0 && rect.height === 0)
263
- return null;
264
- return {
265
- top: rect.top,
266
- left: rect.left,
267
- width: rect.width,
268
- height: rect.height,
269
- };
270
- }
271
- function formatPercentage(value, decimals = 2) {
272
- return value.toFixed(decimals);
273
- }
274
- function calculateRankPosition(rect, widthScale) {
275
- const top = rect.top <= 18 ? rect.top + 3 : rect.top - 18;
276
- const left = rect.left <= 18 ? rect.left + 3 : rect.left - 18;
277
- return {
278
- transform: `scale(${1.2 * widthScale})`,
279
- top: Number.isNaN(top) ? undefined : top,
280
- left: Number.isNaN(left) ? undefined : left,
281
- };
282
- }
283
-
284
- const useHeatmapElementPosition = ({ iframeRef, parentRef, visualizer, heatmapWidth, widthScale, projectId, }) => {
549
+ const useHeatmapElementPosition = ({ iframeRef, wrapperRef, visualizer }) => {
550
+ const widthScale = useHeatmapVizStore((state) => state.scale);
285
551
  const iframeHeight = useHeatmapVizStore((state) => state.iframeHeight);
552
+ const heatmapWidth = useHeatmapDataStore((state) => state.config?.width);
286
553
  return useCallback((element) => {
287
554
  const hash = element?.hash;
288
555
  if (!iframeRef.current?.contentDocument || !hash || !visualizer)
@@ -292,7 +559,7 @@ const useHeatmapElementPosition = ({ iframeRef, parentRef, visualizer, heatmapWi
292
559
  domElement = visualizer.get(hash);
293
560
  }
294
561
  catch (error) {
295
- console.error('Visualizer error:', { projectId, hash, error });
562
+ console.error('Visualizer error:', { hash, error });
296
563
  return null;
297
564
  }
298
565
  if (!domElement)
@@ -300,7 +567,7 @@ const useHeatmapElementPosition = ({ iframeRef, parentRef, visualizer, heatmapWi
300
567
  const layout = getElementLayout(domElement);
301
568
  if (!layout)
302
569
  return null;
303
- const parentEl = parentRef.current;
570
+ const parentEl = wrapperRef.current;
304
571
  if (!parentEl)
305
572
  return null;
306
573
  const scrollOffset = parentEl.scrollTop / widthScale;
@@ -316,8 +583,9 @@ const useHeatmapElementPosition = ({ iframeRef, parentRef, visualizer, heatmapWi
316
583
  top: adjustedTop,
317
584
  width: Math.min(layout.width, heatmapWidth || layout.width),
318
585
  height: layout.height,
586
+ outOfBounds,
319
587
  };
320
- }, [iframeRef, parentRef, visualizer, heatmapWidth, iframeHeight, widthScale, projectId]);
588
+ }, [iframeRef, wrapperRef, visualizer, heatmapWidth, iframeHeight, widthScale]);
321
589
  };
322
590
 
323
591
  const debounce = (fn, delay) => {
@@ -327,68 +595,63 @@ const debounce = (fn, delay) => {
327
595
  timeout = setTimeout(() => fn(...args), delay);
328
596
  };
329
597
  };
330
- const useHoveredElement = ({ iframeRef, heatmapInfo, widthScale, getRect }) => {
598
+
599
+ const useHoveredElement = ({ iframeRef, getRect }) => {
331
600
  const hoveredElement = useHeatmapInteractionStore((state) => state.hoveredElement);
332
601
  const setHoveredElement = useHeatmapInteractionStore((state) => state.setHoveredElement);
333
602
  const onSelect = useHeatmapInteractionStore((state) => state.setSelectedElement);
334
- const handleMouseMove = useCallback(debounce((event) => {
335
- if (!iframeRef.current?.contentDocument || !heatmapInfo?.elementMapInfo) {
336
- setHoveredElement(null);
603
+ const widthScale = useHeatmapVizStore((state) => state.scale);
604
+ const heatmapInfo = useHeatmapDataStore((state) => state.dataInfo);
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();
337
614
  return;
338
615
  }
339
616
  const iframe = iframeRef.current;
340
- const iframeRect = iframe.getBoundingClientRect();
341
- let x = event.clientX - iframeRect.left;
342
- let y = event.clientY - iframeRect.top;
343
- if (widthScale !== 1) {
344
- x /= widthScale;
345
- y /= widthScale;
346
- }
347
617
  const doc = iframe.contentDocument;
348
- if (!doc) {
349
- 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();
350
623
  return;
351
624
  }
352
- let targetElement = getElementAtPoint(doc, x, y) || null;
353
- if (!targetElement) {
354
- targetElement = doc.elementFromPoint(x, y);
355
- }
356
- if (!targetElement) {
357
- 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();
358
634
  return;
359
635
  }
360
- const hash = targetElement.getAttribute('data-clarity-hash') ||
361
- targetElement.getAttribute('data-clarity-hashalpha') ||
362
- targetElement.getAttribute('data-clarity-hashbeta');
363
- if (!hash || !heatmapInfo.elementMapInfo[hash]) {
364
- setHoveredElement(null);
636
+ const hash = getHashFromEvent(event);
637
+ if (!hash) {
638
+ reset();
365
639
  return;
366
640
  }
367
- const info = heatmapInfo.elementMapInfo[hash];
368
- const position = getRect({ hash, selector: info.selector });
369
- if (position && heatmapInfo.sortedElements) {
370
- const rank = heatmapInfo.sortedElements.findIndex((e) => e.hash === hash) + 1;
371
- setHoveredElement({
372
- ...position,
373
- hash,
374
- clicks: info.totalclicks ?? 0,
375
- rank,
376
- selector: info.selector ?? '',
377
- });
378
- }
379
- else {
380
- 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;
381
647
  }
382
- }, 16), // ~60fps
383
- [iframeRef, heatmapInfo, getRect]);
384
- const handleMouseLeave = useCallback(() => {
385
- setHoveredElement(null);
386
- }, []);
648
+ setHoveredElement(elementInfo);
649
+ }, 16), [heatmapInfo, getRect, reset, getHashFromEvent]);
387
650
  const handleClick = useCallback(() => {
388
- if (hoveredElement?.hash && onSelect) {
389
- onSelect(hoveredElement.hash);
390
- }
391
- }, [hoveredElement, onSelect]);
651
+ if (!hoveredElement?.hash)
652
+ return;
653
+ onSelect(hoveredElement.hash);
654
+ }, [hoveredElement?.hash]);
392
655
  return {
393
656
  hoveredElement,
394
657
  handleMouseMove,
@@ -396,17 +659,32 @@ const useHoveredElement = ({ iframeRef, heatmapInfo, widthScale, getRect }) => {
396
659
  handleClick,
397
660
  };
398
661
  };
399
- const getElementAtPoint = (doc, x, y) => {
400
- let el = null;
401
- if ('caretPositionFromPoint' in doc) {
402
- 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;
403
668
  }
404
- el = el ?? doc.elementFromPoint(x, y);
405
- let element = el;
406
- while (element && element.nodeType === Node.TEXT_NODE) {
407
- 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);
408
675
  }
409
- 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];
410
688
  };
411
689
 
412
690
  const useHeatmapRender = () => {
@@ -422,6 +700,15 @@ const useHeatmapRender = () => {
422
700
  if (!iframe?.contentWindow)
423
701
  return;
424
702
  await visualizer.html(payloads, iframe.contentWindow);
703
+ // iframe.contentDocument?.body.querySelectorAll('body>*').forEach((element) => {
704
+ // console.log(`🚀 🐥 ~ useHeatmapRender ~ element:`, element);
705
+ // const isClosedEcomsendWidget = element.closest('#ecomsend-widget');
706
+ // const isEcomsendWidget = element.querySelector('#ecomsend-widget');
707
+ // const isRemove = !(isClosedEcomsendWidget || isEcomsendWidget);
708
+ // if (isRemove) {
709
+ // element.remove();
710
+ // }
711
+ // });
425
712
  setVizRef(visualizer);
426
713
  }, []);
427
714
  useEffect(() => {
@@ -608,15 +895,16 @@ const useContainerDimensions = (props) => {
608
895
  };
609
896
 
610
897
  const useContentDimensions = (props) => {
611
- const { iframeRef, config } = props;
898
+ const contentWidth = useHeatmapDataStore((state) => state.config?.width ?? 0);
899
+ const { iframeRef } = props;
612
900
  useEffect(() => {
613
- if (!config?.width)
901
+ if (!contentWidth)
614
902
  return;
615
903
  if (!iframeRef.current)
616
904
  return;
617
- iframeRef.current.width = `${config.width}px`;
618
- }, [config?.width, iframeRef]);
619
- return { contentWidth: config?.width ?? 0 };
905
+ iframeRef.current.width = `${contentWidth}px`;
906
+ }, [contentWidth, iframeRef]);
907
+ return { contentWidth };
620
908
  };
621
909
 
622
910
  // Hook 3: Iframe Height Observer
@@ -725,52 +1013,51 @@ const useScaleCalculation = (props) => {
725
1013
  return { scale };
726
1014
  };
727
1015
 
728
- const useScrollSync = (props) => {
729
- const { iframeRef, scale } = props;
1016
+ const useScrollSync = ({ iframeRef }) => {
1017
+ const widthScale = useHeatmapVizStore((state) => state.scale);
730
1018
  const handleScroll = useCallback((scrollTop) => {
731
1019
  const iframe = iframeRef.current;
732
- if (!iframe || scale <= 0)
1020
+ if (!iframe || widthScale <= 0)
733
1021
  return;
734
1022
  try {
735
1023
  const iframeWindow = iframe.contentWindow;
736
1024
  const iframeDocument = iframe.contentDocument;
737
1025
  if (iframeWindow && iframeDocument) {
738
- const iframeScrollTop = scrollTop / scale;
1026
+ const iframeScrollTop = scrollTop / widthScale;
739
1027
  iframe.style.top = `${iframeScrollTop}px`;
1028
+ // iframeWindow.scrollTo({ top: iframeScrollTop, behavior: 'smooth' });
740
1029
  }
741
1030
  }
742
1031
  catch (error) {
743
1032
  console.warn('Cannot sync scroll to iframe:', error);
744
1033
  }
745
- }, [iframeRef, scale]);
1034
+ }, [iframeRef, widthScale]);
746
1035
  return { handleScroll };
747
1036
  };
748
1037
 
749
1038
  const useHeatmapScale = (props) => {
750
- const { wrapperRef, iframeRef, config } = props;
1039
+ const { wrapperRef, iframeRef, visualRef } = props;
751
1040
  // 1. Observe container dimensions
752
1041
  const { containerWidth, containerHeight } = useContainerDimensions({ wrapperRef });
753
1042
  // 2. Get content dimensions from config
754
- const { contentWidth } = useContentDimensions({ iframeRef, config });
1043
+ const { contentWidth } = useContentDimensions({ iframeRef });
755
1044
  // 3. Observe iframe height (now reacts to width changes)
756
1045
  const { iframeHeight } = useIframeHeight({ iframeRef, contentWidth });
757
1046
  // 4. Calculate scale
758
1047
  const { scale } = useScaleCalculation({ containerWidth, contentWidth });
759
1048
  // 5. Setup scroll sync
760
- const { handleScroll } = useScrollSync({ iframeRef, scale });
1049
+ const { handleScroll } = useScrollSync({ iframeRef });
761
1050
  return {
762
1051
  containerWidth,
763
1052
  containerHeight,
764
- contentWidth,
765
1053
  iframeHeight,
766
- scale,
767
1054
  scaledWidth: contentWidth * scale,
768
1055
  scaledHeight: iframeHeight * scale,
769
1056
  handleScroll,
770
1057
  };
771
1058
  };
772
1059
 
773
- const BoxStack = ({ children, ...props }) => {
1060
+ const BoxStack = forwardRef(({ children, ...props }, ref) => {
774
1061
  const id = props.id;
775
1062
  const flexDirection = props.flexDirection;
776
1063
  const overflow = props.overflow || 'hidden';
@@ -781,6 +1068,8 @@ const BoxStack = ({ children, ...props }) => {
781
1068
  const style = props.style || {};
782
1069
  const gap = props.gap || 0;
783
1070
  const height = props.height || 'auto';
1071
+ const zIndex = props.zIndex || 0;
1072
+ const backgroundColor = props.backgroundColor || 'transparent';
784
1073
  const styleGap = useMemo(() => {
785
1074
  switch (flexDirection) {
786
1075
  case 'row':
@@ -802,15 +1091,17 @@ const BoxStack = ({ children, ...props }) => {
802
1091
  justifyContent,
803
1092
  alignItems,
804
1093
  height,
1094
+ zIndex,
1095
+ backgroundColor,
805
1096
  ...styleGap,
806
1097
  ...style,
807
1098
  };
808
- return (jsx("div", { id: id, style: styleProps, children: children }));
809
- };
1099
+ return (jsx("div", { id: id, style: styleProps, ref: ref, children: children }));
1100
+ });
810
1101
 
811
1102
  const ContentTopBar = () => {
812
1103
  const controls = useHeatmapControlStore((state) => state.controls);
813
- 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: {
814
1105
  borderBottom: `${HEATMAP_CONFIG.borderWidth}px solid ${HEATMAP_CONFIG.borderColor}`,
815
1106
  }, children: controls.TopBar ?? null }));
816
1107
  };
@@ -848,277 +1139,82 @@ const useHeatmapVizCanvas = ({ type }) => {
848
1139
  return heatmapRender?.();
849
1140
  };
850
1141
 
851
- const CLICKED_ELEMENT_ID = 'clickedElement';
852
- const SECONDARY_CLICKED_ELEMENT_ID = 'secondaryClickedElementID';
853
- const HOVERED_ELEMENT_ID = 'hoveredElement';
854
- const SECONDARY_HOVERED_ELEMENT_ID = 'secondaryhoveredElementID';
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';
1146
+
1147
+ const RankBadge = ({ index, elementRect, widthScale, clickOnElement, }) => {
1148
+ const style = calculateRankPosition(elementRect, widthScale);
1149
+ return (jsx("div", { className: "gx-hm-rank-badge", style: style, onClick: clickOnElement, children: index }));
1150
+ };
1151
+
1152
+ const NUMBER_OF_TOP_ELEMENTS = 10;
1153
+ const DefaultRankBadges = ({ getRect, hidden }) => {
1154
+ const heatmapInfo = useHeatmapDataStore((state) => state.dataInfo);
1155
+ const widthScale = useHeatmapVizStore((state) => state.scale);
1156
+ const elements = heatmapInfo?.sortedElements?.slice(0, NUMBER_OF_TOP_ELEMENTS) ?? [];
1157
+ if (hidden || elements.length === 0)
1158
+ return null;
1159
+ return (jsx(Fragment, { children: elements.map((element, index) => {
1160
+ const rect = getRect(element);
1161
+ if (!rect)
1162
+ return null;
1163
+ return (jsx(RankBadge, { index: index + 1, elementRect: rect, widthScale: widthScale }, element.hash));
1164
+ }) }));
1165
+ };
855
1166
 
856
- const ElementCallout = ({ element, target, totalClicks, isSecondary, isRecordingView, isCompareMode, deviceType, heatmapType, language, widthScale, parentRef, }) => {
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;
857
1176
  const calloutRef = useRef(null);
858
- const [position, setPosition] = useState({
859
- top: 0,
860
- left: 0,
861
- placement: 'top',
862
- });
863
- const percentage = formatPercentage(((element.clicks ?? 0) / totalClicks) * 100, 2);
1177
+ const [position, setPosition] = useState(DEFAULT_POSITION);
864
1178
  useEffect(() => {
865
- const targetElement = document.querySelector(target);
866
- const calloutElement = calloutRef.current;
867
- if (!targetElement || !calloutElement)
1179
+ const targetElm = document.querySelector(target);
1180
+ const calloutElm = calloutRef.current;
1181
+ if (!targetElm || !calloutElm)
868
1182
  return;
869
- const calculatePosition = () => {
870
- const targetRect = targetElement.getBoundingClientRect();
871
- const calloutRect = calloutElement.getBoundingClientRect();
872
- const viewportWidth = window.innerWidth;
873
- const viewportHeight = window.innerHeight;
874
- const padding = 12; // Space between target and callout
875
- const arrowSize = 8;
876
- let top = 0;
877
- let left = 0;
878
- let placement = 'top';
879
- const positions = [
880
- {
881
- placement: 'top',
882
- top: targetRect.top - calloutRect.height - padding - arrowSize,
883
- left: targetRect.left + targetRect.width / 2 - calloutRect.width / 2,
884
- valid: targetRect.top - calloutRect.height - padding - arrowSize > 0,
885
- },
886
- {
887
- placement: 'bottom',
888
- top: targetRect.bottom + padding + arrowSize,
889
- left: targetRect.left + targetRect.width / 2 - calloutRect.width / 2,
890
- valid: targetRect.bottom + calloutRect.height + padding + arrowSize < viewportHeight,
891
- },
892
- {
893
- placement: 'right',
894
- top: targetRect.top + targetRect.height / 2 - calloutRect.height / 2,
895
- left: targetRect.right + padding + arrowSize,
896
- valid: targetRect.right + calloutRect.width + padding + arrowSize < viewportWidth,
897
- },
898
- {
899
- placement: 'left',
900
- top: targetRect.top + targetRect.height / 2 - calloutRect.height / 2,
901
- left: targetRect.left - calloutRect.width - padding - arrowSize,
902
- valid: targetRect.left - calloutRect.width - padding - arrowSize > 0,
903
- },
904
- ];
905
- // Find first valid position
906
- const validPosition = positions.find((p) => p.valid) || positions[0];
907
- top = validPosition.top;
908
- left = validPosition.left;
909
- placement = validPosition.placement;
910
- // Keep within viewport bounds
911
- left = Math.max(padding, Math.min(left, viewportWidth - calloutRect.width - padding));
912
- top = Math.max(padding, Math.min(top, viewportHeight - calloutRect.height - padding));
913
- setPosition({ top, left, placement });
914
- };
915
- calculatePosition();
1183
+ const positionFn = calcCalloutPosition({
1184
+ targetElm,
1185
+ calloutElm,
1186
+ setPosition,
1187
+ hozOffset,
1188
+ alignment,
1189
+ });
1190
+ positionFn();
916
1191
  const handleUpdate = () => {
917
- requestAnimationFrame(calculatePosition);
1192
+ requestAnimationFrame(positionFn);
918
1193
  };
919
1194
  window.addEventListener('scroll', handleUpdate, true);
920
1195
  window.addEventListener('resize', handleUpdate);
921
- parentRef?.current?.addEventListener('scroll', handleUpdate);
1196
+ visualRef?.current?.addEventListener('scroll', handleUpdate);
922
1197
  return () => {
923
1198
  window.removeEventListener('scroll', handleUpdate, true);
924
1199
  window.removeEventListener('resize', handleUpdate);
925
- parentRef?.current?.removeEventListener('scroll', handleUpdate);
1200
+ visualRef?.current?.removeEventListener('scroll', handleUpdate);
926
1201
  };
927
- }, [target, parentRef]);
928
- const calloutContent = (jsxs("div", { ref: calloutRef, className: `clarity-callout clarity-callout--${position.placement} ${isSecondary ? 'clarity-callout--secondary' : ''}`, style: {
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: {
929
1204
  position: 'fixed',
930
1205
  top: position.top,
931
1206
  left: position.left,
932
1207
  zIndex: 2147483647,
933
- }, "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, "%)"] })] })] }) }), jsx("style", { children: `
934
- .clarity-callout {
935
- background: white;
936
- border-radius: 8px;
937
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
938
- padding: 16px;
939
- min-width: 200px;
940
- max-width: 280px;
941
- animation: clarity-callout-fade-in 0.2s ease-out;
942
- pointer-events: auto;
943
- }
944
-
945
- @keyframes clarity-callout-fade-in {
946
- from {
947
- opacity: 0;
948
- transform: scale(0.95);
949
- }
950
- to {
951
- opacity: 1;
952
- transform: scale(1);
953
- }
954
- }
955
-
956
- .clarity-callout__arrow {
957
- position: absolute;
958
- width: 16px;
959
- height: 16px;
960
- background: white;
961
- transform: rotate(45deg);
962
- }
963
-
964
- /* Arrow positions */
965
- .clarity-callout--top .clarity-callout__arrow {
966
- bottom: -8px;
967
- left: 50%;
968
- margin-left: -8px;
969
- box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
970
- }
971
-
972
- .clarity-callout--bottom .clarity-callout__arrow {
973
- top: -8px;
974
- left: 50%;
975
- margin-left: -8px;
976
- box-shadow: -2px -2px 4px rgba(0, 0, 0, 0.1);
977
- }
978
-
979
- .clarity-callout--left .clarity-callout__arrow {
980
- right: -8px;
981
- top: 50%;
982
- margin-top: -8px;
983
- box-shadow: 2px -2px 4px rgba(0, 0, 0, 0.1);
984
- }
985
-
986
- .clarity-callout--right .clarity-callout__arrow {
987
- left: -8px;
988
- top: 50%;
989
- margin-top: -8px;
990
- box-shadow: -2px 2px 4px rgba(0, 0, 0, 0.1);
991
- }
992
-
993
- .clarity-callout__content {
994
- position: relative;
995
- z-index: 1;
996
- }
997
-
998
- .clarity-callout__rank {
999
- display: inline-flex;
1000
- align-items: center;
1001
- justify-content: center;
1002
- width: 32px;
1003
- height: 32px;
1004
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1005
- color: white;
1006
- border-radius: 8px;
1007
- font-weight: 600;
1008
- font-size: 16px;
1009
- margin-bottom: 12px;
1010
- }
1011
-
1012
- .clarity-callout--secondary .clarity-callout__rank {
1013
- background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
1014
- }
1015
-
1016
- .clarity-callout__stats {
1017
- margin-bottom: 12px;
1018
- }
1019
-
1020
- .clarity-callout__label {
1021
- font-size: 12px;
1022
- color: #6b7280;
1023
- margin-bottom: 4px;
1024
- font-weight: 500;
1025
- }
1026
-
1027
- .clarity-callout__value {
1028
- display: flex;
1029
- align-items: baseline;
1030
- gap: 6px;
1031
- }
1032
-
1033
- .clarity-callout__count {
1034
- font-size: 20px;
1035
- font-weight: 700;
1036
- color: #111827;
1037
- }
1038
-
1039
- .clarity-callout__percentage {
1040
- font-size: 14px;
1041
- color: #6b7280;
1042
- font-weight: 500;
1043
- }
1044
-
1045
- .clarity-callout__button {
1046
- display: flex;
1047
- align-items: center;
1048
- justify-content: center;
1049
- gap: 6px;
1050
- width: 100%;
1051
- padding: 8px 12px;
1052
- background: #667eea;
1053
- color: white;
1054
- border: none;
1055
- border-radius: 6px;
1056
- font-size: 13px;
1057
- font-weight: 600;
1058
- cursor: pointer;
1059
- transition: all 0.2s;
1060
- }
1061
-
1062
- .clarity-callout__button:hover {
1063
- background: #5568d3;
1064
- transform: translateY(-1px);
1065
- box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4);
1066
- }
1067
-
1068
- .clarity-callout__button:active {
1069
- transform: translateY(0);
1070
- }
1071
-
1072
- .clarity-callout__button svg {
1073
- width: 14px;
1074
- height: 14px;
1075
- }
1076
- ` })] }));
1077
- return createPortal(calloutContent, document.body);
1078
- };
1079
-
1080
- const RankBadge = ({ index, elementRect, widthScale, clickOnElement, }) => {
1081
- const style = calculateRankPosition(elementRect, widthScale);
1082
- return (jsx("div", { className: "rankBadge", style: style, onClick: clickOnElement, "data-testid": "elementRank", children: index }));
1208
+ }, "aria-live": "assertive", role: "tooltip", children: CompElementCallout && jsx(CompElementCallout, { elementHash: element.hash }) }));
1209
+ return createPortal(calloutContent, document.getElementById('gx-hm-viz-container'));
1083
1210
  };
1084
1211
 
1085
- const ClickedElementOverlay = ({ element, shouldShowCallout, isSecondary, targetId, totalClicks = 1, isRecordingView, isCompareMode, deviceType, heatmapType, widthScale, }) => {
1086
- if (!element || (element.width === 0 && element.height === 0))
1087
- return null;
1088
- return (jsxs(Fragment$1, { children: [jsx("div", { className: "heatmapElement", id: targetId, style: {
1089
- top: element.top,
1090
- left: element.left,
1091
- width: element.width,
1092
- height: element.height,
1093
- } }), jsx(RankBadge, { index: element.rank, elementRect: element, widthScale: widthScale }), shouldShowCallout && (jsx(ElementCallout, { element: element, target: `#${targetId}`, totalClicks: totalClicks, isSecondary: isSecondary, isRecordingView: isRecordingView, isCompareMode: isCompareMode, deviceType: deviceType, heatmapType: heatmapType, widthScale: widthScale }))] }));
1094
- };
1095
-
1096
- const DefaultRankBadges = ({ elements, getRect, widthScale, hidden }) => {
1097
- if (hidden || elements.length === 0)
1212
+ const ElementMissing = ({ show = true }) => {
1213
+ if (!show)
1098
1214
  return null;
1099
- return (jsx(Fragment, { children: elements.map((element, index) => {
1100
- const rect = getRect(element);
1101
- if (!rect)
1102
- return null;
1103
- return (jsx(RankBadge, { index: index + 1, elementRect: rect, widthScale: widthScale }, element.hash));
1104
- }) }));
1105
- };
1106
-
1107
- const HoveredElementOverlay = ({ element, onClick, isSecondary, targetId, totalClicks = 1, }) => {
1108
- if (!element)
1109
- return null;
1110
- return (jsxs(Fragment$1, { children: [jsx("div", { onClick: onClick, className: "heatmapElement hovered", id: targetId, style: {
1111
- top: element.top,
1112
- left: element.left,
1113
- width: element.width,
1114
- height: element.height,
1115
- cursor: 'pointer',
1116
- } }), jsx(RankBadge, { index: element.rank, elementRect: element, widthScale: 1, clickOnElement: onClick })] }));
1117
- };
1118
-
1119
- const MissingElementMessage = ({ widthScale }) => {
1215
+ const widthScale = useHeatmapVizStore((state) => state.scale);
1120
1216
  return (jsx("div", { className: "missingElement", style: {
1121
- position: 'absolute',
1217
+ position: 'fixed',
1122
1218
  top: '50%',
1123
1219
  left: '50%',
1124
1220
  transform: `translate(-50%, -50%) scale(${1 / widthScale})`,
@@ -1134,27 +1230,58 @@ const MissingElementMessage = ({ widthScale }) => {
1134
1230
  }, "aria-live": "assertive", children: "Element not visible on current screen" }));
1135
1231
  };
1136
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
+ };
1137
1266
  const HeatmapElements = (props) => {
1138
1267
  const height = useHeatmapVizStore((state) => state.iframeHeight);
1139
- const { heatmapInfo, iframeRef, parentRef, visualizer, widthScale, iframeDimensions, isElementSidebarOpen, isVisible = true, areDefaultRanksHidden, isSecondary, ...rest } = props;
1268
+ const { iframeRef, wrapperRef, visualRef, visualizer, iframeDimensions, isElementSidebarOpen, isVisible = true, areDefaultRanksHidden, isSecondary, ...rest } = props;
1140
1269
  const getRect = useHeatmapElementPosition({
1141
1270
  iframeRef,
1142
- parentRef,
1271
+ wrapperRef,
1143
1272
  visualizer,
1144
- heatmapWidth: heatmapInfo?.width,
1145
- widthScale,
1146
- projectId: props.projectId,
1147
1273
  });
1148
1274
  const { clickedElement, showMissingElement, shouldShowCallout, setShouldShowCallout } = useClickedElement({
1149
- heatmapInfo,
1275
+ visualRef,
1150
1276
  getRect,
1151
1277
  });
1152
- console.log(`🚀 🐥 ~ HeatmapElements ~ clickedElement:`, clickedElement);
1153
1278
  const { hoveredElement, handleMouseMove, handleMouseLeave, handleClick } = useHoveredElement({
1154
1279
  iframeRef,
1155
- heatmapInfo,
1156
1280
  getRect,
1157
- widthScale,
1281
+ });
1282
+ useElementCalloutVisible({
1283
+ visualRef,
1284
+ getRect,
1158
1285
  });
1159
1286
  const resetAll = () => {
1160
1287
  // setShouldShowCallout(false);
@@ -1167,12 +1294,12 @@ const HeatmapElements = (props) => {
1167
1294
  });
1168
1295
  if (!isVisible)
1169
1296
  return null;
1170
- const top10 = heatmapInfo?.sortedElements?.slice(0, 10) ?? [];
1171
- return (jsxs("div", { onMouseMove: handleMouseMove, onMouseLeave: handleMouseLeave, className: "heatmapElements gx-hm-elements", style: { ...iframeDimensions, height }, children: [jsx(DefaultRankBadges, { elements: top10, getRect: getRect, widthScale: widthScale, hidden: areDefaultRanksHidden }), jsx(ClickedElementOverlay, { widthScale: widthScale, element: clickedElement, shouldShowCallout: shouldShowCallout, isSecondary: isSecondary, targetId: isSecondary ? SECONDARY_CLICKED_ELEMENT_ID : CLICKED_ELEMENT_ID, ...rest }), showMissingElement && jsx(MissingElementMessage, { widthScale: widthScale }), jsx(HoveredElementOverlay, { element: hoveredElement, onClick: handleClick, isSecondary: isSecondary, targetId: isSecondary ? SECONDARY_HOVERED_ELEMENT_ID : HOVERED_ELEMENT_ID, totalClicks: heatmapInfo?.totalClicks ?? 1 }), hoveredElement?.hash !== clickedElement?.hash && hoveredElement && (jsx(ElementCallout, { element: hoveredElement, target: `#${props.isSecondary ? SECONDARY_HOVERED_ELEMENT_ID : HOVERED_ELEMENT_ID}`, totalClicks: props.heatmapInfo?.totalClicks ?? 1, isSecondary: props.isSecondary, parentRef: props.parentRef }))] }));
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 }))] }));
1172
1298
  };
1173
1299
 
1174
- const VizElements = ({ width, iframeRef, wrapperRef, widthScale, }) => {
1300
+ const VizElements = ({ iframeRef, visualRef, wrapperRef }) => {
1175
1301
  const heatmapInfo = useHeatmapDataStore((state) => state.dataInfo);
1302
+ const contentWidth = useHeatmapDataStore((state) => state.config?.width ?? 0);
1176
1303
  const visualizer = {
1177
1304
  get: (hash) => {
1178
1305
  const doc = iframeRef.current?.contentDocument;
@@ -1191,8 +1318,8 @@ const VizElements = ({ width, iframeRef, wrapperRef, widthScale, }) => {
1191
1318
  };
1192
1319
  if (!iframeRef.current)
1193
1320
  return null;
1194
- return (jsx(HeatmapElements, { visualizer: visualizer, iframeRef: iframeRef, parentRef: wrapperRef, widthScale: widthScale, heatmapInfo: heatmapInfo, isVisible: true, iframeDimensions: {
1195
- width,
1321
+ return (jsx(HeatmapElements, { visualizer: visualizer, visualRef: visualRef, iframeRef: iframeRef, wrapperRef: wrapperRef, heatmapInfo: heatmapInfo, isVisible: true, iframeDimensions: {
1322
+ width: contentWidth,
1196
1323
  position: 'absolute',
1197
1324
  top: 0,
1198
1325
  left: 0,
@@ -1232,14 +1359,20 @@ const ReplayControls = () => {
1232
1359
  const VizDomRenderer = ({ mode = 'heatmap' }) => {
1233
1360
  const config = useHeatmapDataStore((state) => state.config);
1234
1361
  const setIframeHeight = useHeatmapVizStore((state) => state.setIframeHeight);
1362
+ const widthScale = useHeatmapVizStore((state) => state.scale);
1363
+ const setSelectedElement = useHeatmapInteractionStore((state) => state.setSelectedElement);
1235
1364
  const wrapperRef = useRef(null);
1236
1365
  const visualRef = useRef(null);
1237
1366
  const { iframeRef } = useHeatmapVizRender(mode);
1238
- const { contentWidth, iframeHeight, scale, scaledHeight, handleScroll } = useHeatmapScale({
1367
+ const { iframeHeight, scaledHeight, handleScroll } = useHeatmapScale({
1239
1368
  wrapperRef,
1240
1369
  iframeRef,
1241
- config,
1370
+ visualRef,
1242
1371
  });
1372
+ const contentWidth = config?.width ?? 0;
1373
+ const contentHeight = scaledHeight > 0
1374
+ ? `${scaledHeight + HEATMAP_CONFIG['heightToolbar'] + HEATMAP_CONFIG['padding'] * 2}px`
1375
+ : 'auto';
1243
1376
  const onScroll = (e) => {
1244
1377
  const scrollTop = e.currentTarget.scrollTop;
1245
1378
  handleScroll(scrollTop);
@@ -1247,6 +1380,7 @@ const VizDomRenderer = ({ mode = 'heatmap' }) => {
1247
1380
  useHeatmapVizCanvas({ type: config?.heatmapType });
1248
1381
  const cleanUp = () => {
1249
1382
  setIframeHeight(0);
1383
+ setSelectedElement(null);
1250
1384
  };
1251
1385
  useEffect(() => {
1252
1386
  return cleanUp;
@@ -1264,17 +1398,17 @@ const VizDomRenderer = ({ mode = 'heatmap' }) => {
1264
1398
  display: 'flex',
1265
1399
  justifyContent: 'center',
1266
1400
  alignItems: 'flex-start',
1267
- height: scaledHeight > 0 ? `${scaledHeight + HEATMAP_CONFIG['padding'] * 2}px` : 'auto',
1401
+ height: contentHeight,
1268
1402
  padding: HEATMAP_STYLE['wrapper']['padding'],
1269
1403
  paddingBottom: HEATMAP_STYLE['viz']['paddingBottom'],
1270
1404
  background: HEATMAP_STYLE['viz']['background'],
1271
1405
  }, children: jsxs("div", { className: "gx-hm-wrapper", ref: wrapperRef, style: {
1272
1406
  width: contentWidth,
1273
1407
  height: iframeHeight,
1274
- transform: `scale(${scale})`,
1408
+ transform: `scale(${widthScale})`,
1275
1409
  transformOrigin: 'top center',
1276
1410
  paddingBlockEnd: '0',
1277
- }, children: [jsx(VizElements, { width: contentWidth, widthScale: scale, iframeRef: iframeRef, wrapperRef: wrapperRef }), jsx("iframe", { ref: iframeRef, ...HEATMAP_IFRAME, width: contentWidth, height: iframeHeight, scrolling: "no" })] }) }), mode === 'replay' && jsx(ReplayControls, {})] }));
1411
+ }, children: [jsx(VizElements, { iframeRef: iframeRef, visualRef: visualRef, wrapperRef: wrapperRef }), jsx("iframe", { ref: iframeRef, ...HEATMAP_IFRAME, width: contentWidth, height: iframeHeight, scrolling: "no" })] }) }), mode === 'replay' && jsx(ReplayControls, {})] }));
1278
1412
  };
1279
1413
 
1280
1414
  const VizDomContainer = () => {
@@ -1289,14 +1423,21 @@ const VizDomContainer = () => {
1289
1423
 
1290
1424
  const ContentMetricBar = () => {
1291
1425
  const controls = useHeatmapControlStore((state) => state.controls);
1292
- 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: {
1293
1427
  borderBottom: `${HEATMAP_CONFIG.borderWidth}px solid ${HEATMAP_CONFIG.borderColor}`,
1294
1428
  }, children: controls.MetricBar ?? null }));
1295
1429
  };
1296
1430
 
1297
1431
  const ContentToolbar = () => {
1298
1432
  const controls = useHeatmapControlStore((state) => state.controls);
1299
- return (jsx("div", { id: "gx-hm-content-toolbar", style: { position: 'absolute', bottom: 0, left: '8px', right: '24px', padding: '8px' }, children: controls.Toolbar ?? null }));
1433
+ return (jsx("div", { id: "gx-hm-content-toolbar", style: {
1434
+ position: 'absolute',
1435
+ bottom: 0,
1436
+ left: '8px',
1437
+ right: '24px',
1438
+ padding: '8px',
1439
+ paddingBlock: '16px',
1440
+ }, children: controls.Toolbar ?? null }));
1300
1441
  };
1301
1442
 
1302
1443
  const LeftSidebar = () => {
@@ -1344,6 +1485,7 @@ const HeatmapLayout = ({ data, clickmap, controls, dataInfo, }) => {
1344
1485
  function getVariableStyle() {
1345
1486
  return {
1346
1487
  '--gx-hm-border-width': `${HEATMAP_CONFIG.borderWidth}px`,
1488
+ '--gx-hm-border-width-iframe': `${HEATMAP_CONFIG.borderWidthIframe}px`,
1347
1489
  '--gx-hm-border-color': `${HEATMAP_CONFIG.borderColor}`,
1348
1490
  };
1349
1491
  }