@gemx-dev/heatmap-react 3.5.32 → 3.5.34

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 (77) hide show
  1. package/dist/esm/components/Layout/ContentToolbar.d.ts.map +1 -1
  2. package/dist/esm/components/Layout/HeatmapLayout.d.ts.map +1 -1
  3. package/dist/esm/components/VizDom/VizDomRenderer.d.ts.map +1 -1
  4. package/dist/esm/components/VizElement/ClickedElementOverlay.d.ts +0 -7
  5. package/dist/esm/components/VizElement/ClickedElementOverlay.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 +1 -7
  9. package/dist/esm/components/VizElement/ElementCallout.d.ts.map +1 -1
  10. package/dist/esm/components/VizElement/HeatmapElements.d.ts +2 -7
  11. package/dist/esm/components/VizElement/HeatmapElements.d.ts.map +1 -1
  12. package/dist/esm/components/VizElement/HoveredElementOverlay.d.ts +0 -2
  13. package/dist/esm/components/VizElement/HoveredElementOverlay.d.ts.map +1 -1
  14. package/dist/esm/components/VizElement/MissingElementMessage.d.ts +0 -1
  15. package/dist/esm/components/VizElement/MissingElementMessage.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/hooks/vix-elements/useClickedElement.d.ts +5 -3
  21. package/dist/esm/hooks/vix-elements/useClickedElement.d.ts.map +1 -1
  22. package/dist/esm/hooks/vix-elements/useHeatmapElementPosition.d.ts +2 -5
  23. package/dist/esm/hooks/vix-elements/useHeatmapElementPosition.d.ts.map +1 -1
  24. package/dist/esm/hooks/vix-elements/useHoveredElement.d.ts +2 -5
  25. package/dist/esm/hooks/vix-elements/useHoveredElement.d.ts.map +1 -1
  26. package/dist/esm/hooks/viz-render/useHeatmapRender.d.ts.map +1 -1
  27. package/dist/esm/hooks/viz-scale/useContentDimensions.d.ts +0 -2
  28. package/dist/esm/hooks/viz-scale/useContentDimensions.d.ts.map +1 -1
  29. package/dist/esm/hooks/viz-scale/useHeatmapScale.d.ts +1 -4
  30. package/dist/esm/hooks/viz-scale/useHeatmapScale.d.ts.map +1 -1
  31. package/dist/esm/hooks/viz-scale/useScrollSync.d.ts +1 -2
  32. package/dist/esm/hooks/viz-scale/useScrollSync.d.ts.map +1 -1
  33. package/dist/esm/index.d.ts +1 -1
  34. package/dist/esm/index.d.ts.map +1 -1
  35. package/dist/esm/index.js +171 -225
  36. package/dist/esm/index.mjs +171 -225
  37. package/dist/esm/types/viz-element.d.ts +1 -0
  38. package/dist/esm/types/viz-element.d.ts.map +1 -1
  39. package/dist/style.css +149 -4
  40. package/dist/umd/components/Layout/ContentToolbar.d.ts.map +1 -1
  41. package/dist/umd/components/Layout/HeatmapLayout.d.ts.map +1 -1
  42. package/dist/umd/components/VizDom/VizDomRenderer.d.ts.map +1 -1
  43. package/dist/umd/components/VizElement/ClickedElementOverlay.d.ts +0 -7
  44. package/dist/umd/components/VizElement/ClickedElementOverlay.d.ts.map +1 -1
  45. package/dist/umd/components/VizElement/DefaultRankBadges.d.ts +0 -3
  46. package/dist/umd/components/VizElement/DefaultRankBadges.d.ts.map +1 -1
  47. package/dist/umd/components/VizElement/ElementCallout.d.ts +1 -7
  48. package/dist/umd/components/VizElement/ElementCallout.d.ts.map +1 -1
  49. package/dist/umd/components/VizElement/HeatmapElements.d.ts +2 -7
  50. package/dist/umd/components/VizElement/HeatmapElements.d.ts.map +1 -1
  51. package/dist/umd/components/VizElement/HoveredElementOverlay.d.ts +0 -2
  52. package/dist/umd/components/VizElement/HoveredElementOverlay.d.ts.map +1 -1
  53. package/dist/umd/components/VizElement/MissingElementMessage.d.ts +0 -1
  54. package/dist/umd/components/VizElement/MissingElementMessage.d.ts.map +1 -1
  55. package/dist/umd/components/VizElement/VizElements.d.ts +1 -2
  56. package/dist/umd/components/VizElement/VizElements.d.ts.map +1 -1
  57. package/dist/umd/configs/style.d.ts +2 -0
  58. package/dist/umd/configs/style.d.ts.map +1 -1
  59. package/dist/umd/hooks/vix-elements/useClickedElement.d.ts +5 -3
  60. package/dist/umd/hooks/vix-elements/useClickedElement.d.ts.map +1 -1
  61. package/dist/umd/hooks/vix-elements/useHeatmapElementPosition.d.ts +2 -5
  62. package/dist/umd/hooks/vix-elements/useHeatmapElementPosition.d.ts.map +1 -1
  63. package/dist/umd/hooks/vix-elements/useHoveredElement.d.ts +2 -5
  64. package/dist/umd/hooks/vix-elements/useHoveredElement.d.ts.map +1 -1
  65. package/dist/umd/hooks/viz-render/useHeatmapRender.d.ts.map +1 -1
  66. package/dist/umd/hooks/viz-scale/useContentDimensions.d.ts +0 -2
  67. package/dist/umd/hooks/viz-scale/useContentDimensions.d.ts.map +1 -1
  68. package/dist/umd/hooks/viz-scale/useHeatmapScale.d.ts +1 -4
  69. package/dist/umd/hooks/viz-scale/useHeatmapScale.d.ts.map +1 -1
  70. package/dist/umd/hooks/viz-scale/useScrollSync.d.ts +1 -2
  71. package/dist/umd/hooks/viz-scale/useScrollSync.d.ts.map +1 -1
  72. package/dist/umd/index.d.ts +1 -1
  73. package/dist/umd/index.d.ts.map +1 -1
  74. package/dist/umd/index.js +2 -2
  75. package/dist/umd/types/viz-element.d.ts +1 -0
  76. package/dist/umd/types/viz-element.d.ts.map +1 -1
  77. package/package.json +1 -1
package/dist/esm/index.js CHANGED
@@ -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: {
@@ -187,17 +189,77 @@ const useRegisterHeatmap = (clickmap) => {
187
189
  }, [clickmap]);
188
190
  };
189
191
 
190
- const useClickedElement = ({ heatmapInfo, getRect }) => {
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;
207
+ };
208
+ const calculateElementRank = (selectedElement, sortedElements) => {
209
+ if (!sortedElements)
210
+ return 0;
211
+ return sortedElements.findIndex((e) => e.hash === selectedElement) + 1;
212
+ };
213
+ const buildElementInfo = (selectedElement, info, rect, rank) => {
214
+ const baseInfo = {
215
+ hash: selectedElement,
216
+ clicks: info.totalclicks ?? 0,
217
+ rank,
218
+ selector: info.selector ?? '',
219
+ };
220
+ if (rect) {
221
+ return { ...baseInfo, ...rect };
222
+ }
223
+ return {
224
+ ...baseInfo,
225
+ left: 0,
226
+ top: 0,
227
+ width: 0,
228
+ height: 0,
229
+ };
230
+ };
231
+ const scrollToElementIfNeeded = (visualRef, wrapperRef, rect, scale) => {
232
+ if (!visualRef.current || !wrapperRef.current)
233
+ return;
234
+ const visualRect = visualRef.current.getBoundingClientRect();
235
+ if (isElementInViewport(rect, visualRef, scale)) {
236
+ return;
237
+ }
238
+ const topRaw = rect.top; // - visualRect.top
239
+ const topScaled = topRaw * scale;
240
+ const viewportHeight = visualRect.height;
241
+ const elementHeightScaled = rect.height * scale;
242
+ const scrollTop = topScaled - (viewportHeight - elementHeightScaled) / 2;
243
+ visualRef.current.scrollTo({
244
+ top: Math.max(0, scrollTop),
245
+ behavior: 'smooth',
246
+ });
247
+ };
248
+ const useClickedElement = ({ visualRef, wrapperRef, getRect }) => {
249
+ const heatmapInfo = useHeatmapDataStore((state) => state.dataInfo);
191
250
  const selectedElement = useHeatmapInteractionStore((state) => state.selectedElement);
251
+ const scale = useHeatmapVizStore((state) => state.scale);
192
252
  const [clickedElement, setClickedElement] = useState(null);
193
253
  const [showMissingElement, setShowMissingElement] = useState(false);
194
254
  const [shouldShowCallout, setShouldShowCallout] = useState(false);
255
+ const reset = () => {
256
+ setClickedElement(null);
257
+ setShowMissingElement(false);
258
+ setShouldShowCallout(false);
259
+ };
195
260
  useEffect(() => {
196
- console.log(`🚀 🐥 ~ useClickedElement ~ heatmapInfo?.elementMapInfo:`, heatmapInfo?.elementMapInfo);
197
261
  if (!selectedElement || !heatmapInfo?.elementMapInfo) {
198
- setClickedElement(null);
199
- setShowMissingElement(false);
200
- setShouldShowCallout(false);
262
+ reset();
201
263
  return;
202
264
  }
203
265
  const info = heatmapInfo.elementMapInfo[selectedElement];
@@ -206,34 +268,22 @@ const useClickedElement = ({ heatmapInfo, getRect }) => {
206
268
  return;
207
269
  }
208
270
  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
- });
271
+ const rank = calculateElementRank(selectedElement, heatmapInfo.sortedElements);
272
+ const elementInfo = buildElementInfo(selectedElement, info, rect, rank);
273
+ if (rect) {
218
274
  setShowMissingElement(false);
275
+ scrollToElementIfNeeded(visualRef, wrapperRef, rect, scale);
219
276
  setShouldShowCallout(true);
277
+ requestAnimationFrame(() => {
278
+ setClickedElement(elementInfo);
279
+ });
220
280
  }
221
281
  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
- });
282
+ setClickedElement(elementInfo);
233
283
  setShowMissingElement(true);
234
284
  setShouldShowCallout(false);
235
285
  }
236
- }, [selectedElement, heatmapInfo, getRect]);
286
+ }, [selectedElement, heatmapInfo, getRect, visualRef, wrapperRef, scale]);
237
287
  return { clickedElement, showMissingElement, shouldShowCallout, setShouldShowCallout };
238
288
  };
239
289
 
@@ -281,8 +331,10 @@ function calculateRankPosition(rect, widthScale) {
281
331
  };
282
332
  }
283
333
 
284
- const useHeatmapElementPosition = ({ iframeRef, parentRef, visualizer, heatmapWidth, widthScale, projectId, }) => {
334
+ const useHeatmapElementPosition = ({ iframeRef, wrapperRef, visualizer }) => {
335
+ const widthScale = useHeatmapVizStore((state) => state.scale);
285
336
  const iframeHeight = useHeatmapVizStore((state) => state.iframeHeight);
337
+ const heatmapWidth = useHeatmapDataStore((state) => state.config?.width);
286
338
  return useCallback((element) => {
287
339
  const hash = element?.hash;
288
340
  if (!iframeRef.current?.contentDocument || !hash || !visualizer)
@@ -292,7 +344,7 @@ const useHeatmapElementPosition = ({ iframeRef, parentRef, visualizer, heatmapWi
292
344
  domElement = visualizer.get(hash);
293
345
  }
294
346
  catch (error) {
295
- console.error('Visualizer error:', { projectId, hash, error });
347
+ console.error('Visualizer error:', { hash, error });
296
348
  return null;
297
349
  }
298
350
  if (!domElement)
@@ -300,7 +352,7 @@ const useHeatmapElementPosition = ({ iframeRef, parentRef, visualizer, heatmapWi
300
352
  const layout = getElementLayout(domElement);
301
353
  if (!layout)
302
354
  return null;
303
- const parentEl = parentRef.current;
355
+ const parentEl = wrapperRef.current;
304
356
  if (!parentEl)
305
357
  return null;
306
358
  const scrollOffset = parentEl.scrollTop / widthScale;
@@ -316,8 +368,9 @@ const useHeatmapElementPosition = ({ iframeRef, parentRef, visualizer, heatmapWi
316
368
  top: adjustedTop,
317
369
  width: Math.min(layout.width, heatmapWidth || layout.width),
318
370
  height: layout.height,
371
+ outOfBounds,
319
372
  };
320
- }, [iframeRef, parentRef, visualizer, heatmapWidth, iframeHeight, widthScale, projectId]);
373
+ }, [iframeRef, wrapperRef, visualizer, heatmapWidth, iframeHeight, widthScale]);
321
374
  };
322
375
 
323
376
  const debounce = (fn, delay) => {
@@ -327,10 +380,12 @@ const debounce = (fn, delay) => {
327
380
  timeout = setTimeout(() => fn(...args), delay);
328
381
  };
329
382
  };
330
- const useHoveredElement = ({ iframeRef, heatmapInfo, widthScale, getRect }) => {
383
+ const useHoveredElement = ({ iframeRef, getRect }) => {
331
384
  const hoveredElement = useHeatmapInteractionStore((state) => state.hoveredElement);
332
385
  const setHoveredElement = useHeatmapInteractionStore((state) => state.setHoveredElement);
333
386
  const onSelect = useHeatmapInteractionStore((state) => state.setSelectedElement);
387
+ const widthScale = useHeatmapVizStore((state) => state.scale);
388
+ const heatmapInfo = useHeatmapDataStore((state) => state.dataInfo);
334
389
  const handleMouseMove = useCallback(debounce((event) => {
335
390
  if (!iframeRef.current?.contentDocument || !heatmapInfo?.elementMapInfo) {
336
391
  setHoveredElement(null);
@@ -422,6 +477,15 @@ const useHeatmapRender = () => {
422
477
  if (!iframe?.contentWindow)
423
478
  return;
424
479
  await visualizer.html(payloads, iframe.contentWindow);
480
+ // iframe.contentDocument?.body.querySelectorAll('body>*').forEach((element) => {
481
+ // console.log(`🚀 🐥 ~ useHeatmapRender ~ element:`, element);
482
+ // const isClosedEcomsendWidget = element.closest('#ecomsend-widget');
483
+ // const isEcomsendWidget = element.querySelector('#ecomsend-widget');
484
+ // const isRemove = !(isClosedEcomsendWidget || isEcomsendWidget);
485
+ // if (isRemove) {
486
+ // element.remove();
487
+ // }
488
+ // });
425
489
  setVizRef(visualizer);
426
490
  }, []);
427
491
  useEffect(() => {
@@ -608,15 +672,16 @@ const useContainerDimensions = (props) => {
608
672
  };
609
673
 
610
674
  const useContentDimensions = (props) => {
611
- const { iframeRef, config } = props;
675
+ const contentWidth = useHeatmapDataStore((state) => state.config?.width ?? 0);
676
+ const { iframeRef } = props;
612
677
  useEffect(() => {
613
- if (!config?.width)
678
+ if (!contentWidth)
614
679
  return;
615
680
  if (!iframeRef.current)
616
681
  return;
617
- iframeRef.current.width = `${config.width}px`;
618
- }, [config?.width, iframeRef]);
619
- return { contentWidth: config?.width ?? 0 };
682
+ iframeRef.current.width = `${contentWidth}px`;
683
+ }, [contentWidth, iframeRef]);
684
+ return { contentWidth };
620
685
  };
621
686
 
622
687
  // Hook 3: Iframe Height Observer
@@ -725,45 +790,44 @@ const useScaleCalculation = (props) => {
725
790
  return { scale };
726
791
  };
727
792
 
728
- const useScrollSync = (props) => {
729
- const { iframeRef, scale } = props;
793
+ const useScrollSync = ({ iframeRef }) => {
794
+ const widthScale = useHeatmapVizStore((state) => state.scale);
730
795
  const handleScroll = useCallback((scrollTop) => {
731
796
  const iframe = iframeRef.current;
732
- if (!iframe || scale <= 0)
797
+ if (!iframe || widthScale <= 0)
733
798
  return;
734
799
  try {
735
800
  const iframeWindow = iframe.contentWindow;
736
801
  const iframeDocument = iframe.contentDocument;
737
802
  if (iframeWindow && iframeDocument) {
738
- const iframeScrollTop = scrollTop / scale;
803
+ const iframeScrollTop = scrollTop / widthScale;
739
804
  iframe.style.top = `${iframeScrollTop}px`;
805
+ // iframeWindow.scrollTo({ top: iframeScrollTop, behavior: 'smooth' });
740
806
  }
741
807
  }
742
808
  catch (error) {
743
809
  console.warn('Cannot sync scroll to iframe:', error);
744
810
  }
745
- }, [iframeRef, scale]);
811
+ }, [iframeRef, widthScale]);
746
812
  return { handleScroll };
747
813
  };
748
814
 
749
815
  const useHeatmapScale = (props) => {
750
- const { wrapperRef, iframeRef, config } = props;
816
+ const { wrapperRef, iframeRef, visualRef } = props;
751
817
  // 1. Observe container dimensions
752
818
  const { containerWidth, containerHeight } = useContainerDimensions({ wrapperRef });
753
819
  // 2. Get content dimensions from config
754
- const { contentWidth } = useContentDimensions({ iframeRef, config });
820
+ const { contentWidth } = useContentDimensions({ iframeRef });
755
821
  // 3. Observe iframe height (now reacts to width changes)
756
822
  const { iframeHeight } = useIframeHeight({ iframeRef, contentWidth });
757
823
  // 4. Calculate scale
758
824
  const { scale } = useScaleCalculation({ containerWidth, contentWidth });
759
825
  // 5. Setup scroll sync
760
- const { handleScroll } = useScrollSync({ iframeRef, scale });
826
+ const { handleScroll } = useScrollSync({ iframeRef });
761
827
  return {
762
828
  containerWidth,
763
829
  containerHeight,
764
- contentWidth,
765
830
  iframeHeight,
766
- scale,
767
831
  scaledWidth: contentWidth * scale,
768
832
  scaledHeight: iframeHeight * scale,
769
833
  handleScroll,
@@ -853,13 +917,16 @@ const SECONDARY_CLICKED_ELEMENT_ID = 'secondaryClickedElementID';
853
917
  const HOVERED_ELEMENT_ID = 'hoveredElement';
854
918
  const SECONDARY_HOVERED_ELEMENT_ID = 'secondaryhoveredElementID';
855
919
 
856
- const ElementCallout = ({ element, target, totalClicks, isSecondary, isRecordingView, isCompareMode, deviceType, heatmapType, language, widthScale, parentRef, }) => {
920
+ const ElementCallout = (props) => {
921
+ const { element, target, isSecondary, language, wrapperRef } = props;
922
+ const heatmapInfo = useHeatmapDataStore((state) => state.dataInfo);
857
923
  const calloutRef = useRef(null);
858
924
  const [position, setPosition] = useState({
859
925
  top: 0,
860
926
  left: 0,
861
927
  placement: 'top',
862
928
  });
929
+ const totalClicks = heatmapInfo?.totalClicks ?? 1;
863
930
  const percentage = formatPercentage(((element.clicks ?? 0) / totalClicks) * 100, 2);
864
931
  useEffect(() => {
865
932
  const targetElement = document.querySelector(target);
@@ -918,163 +985,20 @@ const ElementCallout = ({ element, target, totalClicks, isSecondary, isRecording
918
985
  };
919
986
  window.addEventListener('scroll', handleUpdate, true);
920
987
  window.addEventListener('resize', handleUpdate);
921
- parentRef?.current?.addEventListener('scroll', handleUpdate);
988
+ wrapperRef?.current?.addEventListener('scroll', handleUpdate);
922
989
  return () => {
923
990
  window.removeEventListener('scroll', handleUpdate, true);
924
991
  window.removeEventListener('resize', handleUpdate);
925
- parentRef?.current?.removeEventListener('scroll', handleUpdate);
992
+ wrapperRef?.current?.removeEventListener('scroll', handleUpdate);
926
993
  };
927
- }, [target, parentRef]);
994
+ }, [target, wrapperRef]);
928
995
  const calloutContent = (jsxs("div", { ref: calloutRef, className: `clarity-callout clarity-callout--${position.placement} ${isSecondary ? 'clarity-callout--secondary' : ''}`, style: {
929
996
  position: 'fixed',
930
997
  top: position.top,
931
998
  left: position.left,
932
999
  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);
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'));
1078
1002
  };
1079
1003
 
1080
1004
  const RankBadge = ({ index, elementRect, widthScale, clickOnElement, }) => {
@@ -1082,7 +1006,8 @@ const RankBadge = ({ index, elementRect, widthScale, clickOnElement, }) => {
1082
1006
  return (jsx("div", { className: "rankBadge", style: style, onClick: clickOnElement, "data-testid": "elementRank", children: index }));
1083
1007
  };
1084
1008
 
1085
- const ClickedElementOverlay = ({ element, shouldShowCallout, isSecondary, targetId, totalClicks = 1, isRecordingView, isCompareMode, deviceType, heatmapType, widthScale, }) => {
1009
+ const ClickedElementOverlay = ({ element, shouldShowCallout, isSecondary, targetId, }) => {
1010
+ const widthScale = useHeatmapVizStore((state) => state.scale);
1086
1011
  if (!element || (element.width === 0 && element.height === 0))
1087
1012
  return null;
1088
1013
  return (jsxs(Fragment$1, { children: [jsx("div", { className: "heatmapElement", id: targetId, style: {
@@ -1090,10 +1015,14 @@ const ClickedElementOverlay = ({ element, shouldShowCallout, isSecondary, target
1090
1015
  left: element.left,
1091
1016
  width: element.width,
1092
1017
  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 }))] }));
1018
+ } }), jsx(RankBadge, { index: element.rank, elementRect: element, widthScale: widthScale }), shouldShowCallout && (jsx(ElementCallout, { element: element, target: `#${targetId}`, isSecondary: isSecondary }))] }));
1094
1019
  };
1095
1020
 
1096
- const DefaultRankBadges = ({ elements, getRect, widthScale, hidden }) => {
1021
+ const NUMBER_OF_TOP_ELEMENTS = 10;
1022
+ const DefaultRankBadges = ({ getRect, hidden }) => {
1023
+ const heatmapInfo = useHeatmapDataStore((state) => state.dataInfo);
1024
+ const widthScale = useHeatmapVizStore((state) => state.scale);
1025
+ const elements = heatmapInfo?.sortedElements?.slice(0, NUMBER_OF_TOP_ELEMENTS) ?? [];
1097
1026
  if (hidden || elements.length === 0)
1098
1027
  return null;
1099
1028
  return (jsx(Fragment, { children: elements.map((element, index) => {
@@ -1104,19 +1033,25 @@ const DefaultRankBadges = ({ elements, getRect, widthScale, hidden }) => {
1104
1033
  }) }));
1105
1034
  };
1106
1035
 
1107
- const HoveredElementOverlay = ({ element, onClick, isSecondary, targetId, totalClicks = 1, }) => {
1036
+ const HoveredElementOverlay = ({ element, onClick, isSecondary, targetId, }) => {
1108
1037
  if (!element)
1109
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;
1110
1044
  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,
1045
+ top,
1046
+ left,
1047
+ width,
1048
+ height,
1115
1049
  cursor: 'pointer',
1116
1050
  } }), jsx(RankBadge, { index: element.rank, elementRect: element, widthScale: 1, clickOnElement: onClick })] }));
1117
1051
  };
1118
1052
 
1119
- const MissingElementMessage = ({ widthScale }) => {
1053
+ const MissingElementMessage = () => {
1054
+ const widthScale = useHeatmapVizStore((state) => state.scale);
1120
1055
  return (jsx("div", { className: "missingElement", style: {
1121
1056
  position: 'absolute',
1122
1057
  top: '50%',
@@ -1136,25 +1071,21 @@ const MissingElementMessage = ({ widthScale }) => {
1136
1071
 
1137
1072
  const HeatmapElements = (props) => {
1138
1073
  const height = useHeatmapVizStore((state) => state.iframeHeight);
1139
- const { heatmapInfo, iframeRef, parentRef, visualizer, widthScale, iframeDimensions, isElementSidebarOpen, isVisible = true, areDefaultRanksHidden, isSecondary, ...rest } = props;
1074
+ const { iframeRef, wrapperRef, visualRef, visualizer, iframeDimensions, isElementSidebarOpen, isVisible = true, areDefaultRanksHidden, isSecondary, ...rest } = props;
1140
1075
  const getRect = useHeatmapElementPosition({
1141
1076
  iframeRef,
1142
- parentRef,
1077
+ wrapperRef,
1143
1078
  visualizer,
1144
- heatmapWidth: heatmapInfo?.width,
1145
- widthScale,
1146
- projectId: props.projectId,
1147
1079
  });
1148
1080
  const { clickedElement, showMissingElement, shouldShowCallout, setShouldShowCallout } = useClickedElement({
1149
- heatmapInfo,
1081
+ iframeRef,
1082
+ visualRef,
1083
+ wrapperRef,
1150
1084
  getRect,
1151
1085
  });
1152
- console.log(`🚀 🐥 ~ HeatmapElements ~ clickedElement:`, clickedElement);
1153
1086
  const { hoveredElement, handleMouseMove, handleMouseLeave, handleClick } = useHoveredElement({
1154
1087
  iframeRef,
1155
- heatmapInfo,
1156
1088
  getRect,
1157
- widthScale,
1158
1089
  });
1159
1090
  const resetAll = () => {
1160
1091
  // setShouldShowCallout(false);
@@ -1167,12 +1098,12 @@ const HeatmapElements = (props) => {
1167
1098
  });
1168
1099
  if (!isVisible)
1169
1100
  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 }))] }));
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 }))] }));
1172
1102
  };
1173
1103
 
1174
- const VizElements = ({ width, iframeRef, wrapperRef, widthScale, }) => {
1104
+ const VizElements = ({ iframeRef, visualRef, wrapperRef }) => {
1175
1105
  const heatmapInfo = useHeatmapDataStore((state) => state.dataInfo);
1106
+ const contentWidth = useHeatmapDataStore((state) => state.config?.width ?? 0);
1176
1107
  const visualizer = {
1177
1108
  get: (hash) => {
1178
1109
  const doc = iframeRef.current?.contentDocument;
@@ -1191,8 +1122,8 @@ const VizElements = ({ width, iframeRef, wrapperRef, widthScale, }) => {
1191
1122
  };
1192
1123
  if (!iframeRef.current)
1193
1124
  return null;
1194
- return (jsx(HeatmapElements, { visualizer: visualizer, iframeRef: iframeRef, parentRef: wrapperRef, widthScale: widthScale, heatmapInfo: heatmapInfo, isVisible: true, iframeDimensions: {
1195
- width,
1125
+ return (jsx(HeatmapElements, { visualizer: visualizer, visualRef: visualRef, iframeRef: iframeRef, wrapperRef: wrapperRef, heatmapInfo: heatmapInfo, isVisible: true, iframeDimensions: {
1126
+ width: contentWidth,
1196
1127
  position: 'absolute',
1197
1128
  top: 0,
1198
1129
  left: 0,
@@ -1232,14 +1163,20 @@ const ReplayControls = () => {
1232
1163
  const VizDomRenderer = ({ mode = 'heatmap' }) => {
1233
1164
  const config = useHeatmapDataStore((state) => state.config);
1234
1165
  const setIframeHeight = useHeatmapVizStore((state) => state.setIframeHeight);
1166
+ const widthScale = useHeatmapVizStore((state) => state.scale);
1167
+ const setSelectedElement = useHeatmapInteractionStore((state) => state.setSelectedElement);
1235
1168
  const wrapperRef = useRef(null);
1236
1169
  const visualRef = useRef(null);
1237
1170
  const { iframeRef } = useHeatmapVizRender(mode);
1238
- const { contentWidth, iframeHeight, scale, scaledHeight, handleScroll } = useHeatmapScale({
1171
+ const { iframeHeight, scaledHeight, handleScroll } = useHeatmapScale({
1239
1172
  wrapperRef,
1240
1173
  iframeRef,
1241
- config,
1174
+ visualRef,
1242
1175
  });
1176
+ const contentWidth = config?.width ?? 0;
1177
+ const contentHeight = scaledHeight > 0
1178
+ ? `${scaledHeight + HEATMAP_CONFIG['heightToolbar'] + HEATMAP_CONFIG['padding'] * 2}px`
1179
+ : 'auto';
1243
1180
  const onScroll = (e) => {
1244
1181
  const scrollTop = e.currentTarget.scrollTop;
1245
1182
  handleScroll(scrollTop);
@@ -1247,6 +1184,7 @@ const VizDomRenderer = ({ mode = 'heatmap' }) => {
1247
1184
  useHeatmapVizCanvas({ type: config?.heatmapType });
1248
1185
  const cleanUp = () => {
1249
1186
  setIframeHeight(0);
1187
+ setSelectedElement(null);
1250
1188
  };
1251
1189
  useEffect(() => {
1252
1190
  return cleanUp;
@@ -1264,17 +1202,17 @@ const VizDomRenderer = ({ mode = 'heatmap' }) => {
1264
1202
  display: 'flex',
1265
1203
  justifyContent: 'center',
1266
1204
  alignItems: 'flex-start',
1267
- height: scaledHeight > 0 ? `${scaledHeight + HEATMAP_CONFIG['padding'] * 2}px` : 'auto',
1205
+ height: contentHeight,
1268
1206
  padding: HEATMAP_STYLE['wrapper']['padding'],
1269
1207
  paddingBottom: HEATMAP_STYLE['viz']['paddingBottom'],
1270
1208
  background: HEATMAP_STYLE['viz']['background'],
1271
1209
  }, children: jsxs("div", { className: "gx-hm-wrapper", ref: wrapperRef, style: {
1272
1210
  width: contentWidth,
1273
1211
  height: iframeHeight,
1274
- transform: `scale(${scale})`,
1212
+ transform: `scale(${widthScale})`,
1275
1213
  transformOrigin: 'top center',
1276
1214
  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, {})] }));
1215
+ }, 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
1216
  };
1279
1217
 
1280
1218
  const VizDomContainer = () => {
@@ -1296,7 +1234,14 @@ const ContentMetricBar = () => {
1296
1234
 
1297
1235
  const ContentToolbar = () => {
1298
1236
  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 }));
1237
+ return (jsx("div", { id: "gx-hm-content-toolbar", style: {
1238
+ position: 'absolute',
1239
+ bottom: 0,
1240
+ left: '8px',
1241
+ right: '24px',
1242
+ padding: '8px',
1243
+ paddingBlock: '16px',
1244
+ }, children: controls.Toolbar ?? null }));
1300
1245
  };
1301
1246
 
1302
1247
  const LeftSidebar = () => {
@@ -1344,6 +1289,7 @@ const HeatmapLayout = ({ data, clickmap, controls, dataInfo, }) => {
1344
1289
  function getVariableStyle() {
1345
1290
  return {
1346
1291
  '--gx-hm-border-width': `${HEATMAP_CONFIG.borderWidth}px`,
1292
+ '--gx-hm-border-width-iframe': `${HEATMAP_CONFIG.borderWidthIframe}px`,
1347
1293
  '--gx-hm-border-color': `${HEATMAP_CONFIG.borderColor}`,
1348
1294
  };
1349
1295
  }
@@ -1367,4 +1313,4 @@ var ErrorType;
1367
1313
  ErrorType["DataError"] = "DataError";
1368
1314
  })(ErrorType || (ErrorType = {}));
1369
1315
 
1370
- export { GraphView, HeatmapLayout, IHeatmapType, useHeatmapDataStore };
1316
+ export { GraphView, HeatmapLayout, IHeatmapType, useHeatmapDataStore, useHeatmapInteractionStore };