@gemx-dev/heatmap-react 3.5.11 → 3.5.13

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 (174) hide show
  1. package/dist/base.css +622 -0
  2. package/dist/esm/components/GraphView.d.ts +9 -0
  3. package/dist/esm/components/GraphView.d.ts.map +1 -0
  4. package/dist/esm/components/Layout/ContentHeader.d.ts +4 -0
  5. package/dist/esm/components/Layout/ContentHeader.d.ts.map +1 -0
  6. package/dist/esm/components/Layout/HeatmapLayout.d.ts +7 -0
  7. package/dist/esm/components/Layout/HeatmapLayout.d.ts.map +1 -0
  8. package/dist/esm/components/Layout/LeftSidebar.d.ts +4 -0
  9. package/dist/esm/components/Layout/LeftSidebar.d.ts.map +1 -0
  10. package/dist/esm/components/Layout/WrapperLayout.d.ts +7 -0
  11. package/dist/esm/components/Layout/WrapperLayout.d.ts.map +1 -0
  12. package/dist/esm/components/Layout/WrapperPreview.d.ts +4 -0
  13. package/dist/esm/components/Layout/WrapperPreview.d.ts.map +1 -0
  14. package/dist/esm/components/Layout/index.d.ts +2 -0
  15. package/dist/esm/components/Layout/index.d.ts.map +1 -0
  16. package/dist/esm/components/Test.d.ts +121 -0
  17. package/dist/esm/components/Test.d.ts.map +1 -0
  18. package/dist/esm/components/VizDom/ReplayControls.d.ts +2 -0
  19. package/dist/esm/components/VizDom/ReplayControls.d.ts.map +1 -0
  20. package/dist/esm/components/VizDom/VizDomContainer.d.ts +2 -0
  21. package/dist/esm/components/VizDom/VizDomContainer.d.ts.map +1 -0
  22. package/dist/esm/components/VizDom/VizDomRenderer.d.ts +6 -0
  23. package/dist/esm/components/VizDom/VizDomRenderer.d.ts.map +1 -0
  24. package/dist/esm/components/VizDom/index.d.ts +2 -0
  25. package/dist/esm/components/VizDom/index.d.ts.map +1 -0
  26. package/dist/esm/components/VizElement/VizElementRank.d.ts +75 -0
  27. package/dist/esm/components/VizElement/VizElementRank.d.ts.map +1 -0
  28. package/dist/esm/components/VizElement/VizElements.d.ts +7 -0
  29. package/dist/esm/components/VizElement/VizElements.d.ts.map +1 -0
  30. package/dist/esm/components/VizElement/index.d.ts +2 -0
  31. package/dist/esm/components/VizElement/index.d.ts.map +1 -0
  32. package/dist/esm/components/index.d.ts +4 -0
  33. package/dist/esm/components/index.d.ts.map +1 -0
  34. package/dist/esm/configs/iframe.d.ts +10 -0
  35. package/dist/esm/configs/iframe.d.ts.map +1 -0
  36. package/dist/esm/configs/index.d.ts +3 -0
  37. package/dist/esm/configs/index.d.ts.map +1 -0
  38. package/dist/esm/configs/style.d.ts +9 -0
  39. package/dist/esm/configs/style.d.ts.map +1 -0
  40. package/dist/esm/helpers/iframe.d.ts +3 -0
  41. package/dist/esm/helpers/iframe.d.ts.map +1 -0
  42. package/dist/esm/helpers/index.d.ts +2 -0
  43. package/dist/esm/helpers/index.d.ts.map +1 -0
  44. package/dist/esm/hooks/index.d.ts +3 -0
  45. package/dist/esm/hooks/index.d.ts.map +1 -0
  46. package/dist/esm/hooks/viz-render/index.d.ts +2 -0
  47. package/dist/esm/hooks/viz-render/index.d.ts.map +1 -0
  48. package/dist/esm/hooks/viz-render/useHeatmapRender.d.ts +6 -0
  49. package/dist/esm/hooks/viz-render/useHeatmapRender.d.ts.map +1 -0
  50. package/dist/esm/hooks/viz-render/useHeatmapVizRender.d.ts +6 -0
  51. package/dist/esm/hooks/viz-render/useHeatmapVizRender.d.ts.map +1 -0
  52. package/dist/esm/hooks/viz-render/useReplayRender.d.ts +9 -0
  53. package/dist/esm/hooks/viz-render/useReplayRender.d.ts.map +1 -0
  54. package/dist/esm/hooks/viz-scale/index.d.ts +2 -0
  55. package/dist/esm/hooks/viz-scale/index.d.ts.map +1 -0
  56. package/dist/esm/hooks/viz-scale/useContainerDimensions.d.ts +10 -0
  57. package/dist/esm/hooks/viz-scale/useContainerDimensions.d.ts.map +1 -0
  58. package/dist/esm/hooks/viz-scale/useContentDimensions.d.ts +11 -0
  59. package/dist/esm/hooks/viz-scale/useContentDimensions.d.ts.map +1 -0
  60. package/dist/esm/hooks/viz-scale/useHeatmapScale.d.ts +19 -0
  61. package/dist/esm/hooks/viz-scale/useHeatmapScale.d.ts.map +1 -0
  62. package/dist/esm/hooks/viz-scale/useIframeHeight.d.ts +10 -0
  63. package/dist/esm/hooks/viz-scale/useIframeHeight.d.ts.map +1 -0
  64. package/dist/esm/hooks/viz-scale/useScaleCalculation.d.ts +10 -0
  65. package/dist/esm/hooks/viz-scale/useScaleCalculation.d.ts.map +1 -0
  66. package/dist/esm/hooks/viz-scale/useScrollSync.d.ts +10 -0
  67. package/dist/esm/hooks/viz-scale/useScrollSync.d.ts.map +1 -0
  68. package/dist/esm/index.d.ts +4 -0
  69. package/dist/esm/index.d.ts.map +1 -0
  70. package/dist/esm/index.js +668 -0
  71. package/dist/esm/index.mjs +668 -0
  72. package/dist/esm/stores/data.d.ts +16 -0
  73. package/dist/esm/stores/data.d.ts.map +1 -0
  74. package/dist/esm/stores/index.d.ts +2 -0
  75. package/dist/esm/stores/index.d.ts.map +1 -0
  76. package/dist/esm/ui/BoxStack/BoxStack.d.ts +56 -0
  77. package/dist/esm/ui/BoxStack/BoxStack.d.ts.map +1 -0
  78. package/dist/esm/ui/BoxStack/index.d.ts +2 -0
  79. package/dist/esm/ui/BoxStack/index.d.ts.map +1 -0
  80. package/dist/esm/ui/index.d.ts +2 -0
  81. package/dist/esm/ui/index.d.ts.map +1 -0
  82. package/dist/esm/utils/device.d.ts +2 -0
  83. package/dist/esm/utils/device.d.ts.map +1 -0
  84. package/dist/esm/utils/retry.d.ts +2 -0
  85. package/dist/esm/utils/retry.d.ts.map +1 -0
  86. package/dist/esm/utils/sort.d.ts +3 -0
  87. package/dist/esm/utils/sort.d.ts.map +1 -0
  88. package/dist/style.css +10 -0
  89. package/dist/umd/components/GraphView.d.ts +9 -0
  90. package/dist/umd/components/GraphView.d.ts.map +1 -0
  91. package/dist/umd/components/Layout/ContentHeader.d.ts +4 -0
  92. package/dist/umd/components/Layout/ContentHeader.d.ts.map +1 -0
  93. package/dist/umd/components/Layout/HeatmapLayout.d.ts +7 -0
  94. package/dist/umd/components/Layout/HeatmapLayout.d.ts.map +1 -0
  95. package/dist/umd/components/Layout/LeftSidebar.d.ts +4 -0
  96. package/dist/umd/components/Layout/LeftSidebar.d.ts.map +1 -0
  97. package/dist/umd/components/Layout/WrapperLayout.d.ts +7 -0
  98. package/dist/umd/components/Layout/WrapperLayout.d.ts.map +1 -0
  99. package/dist/umd/components/Layout/WrapperPreview.d.ts +4 -0
  100. package/dist/umd/components/Layout/WrapperPreview.d.ts.map +1 -0
  101. package/dist/umd/components/Layout/index.d.ts +2 -0
  102. package/dist/umd/components/Layout/index.d.ts.map +1 -0
  103. package/dist/umd/components/Test.d.ts +121 -0
  104. package/dist/umd/components/Test.d.ts.map +1 -0
  105. package/dist/umd/components/VizDom/ReplayControls.d.ts +2 -0
  106. package/dist/umd/components/VizDom/ReplayControls.d.ts.map +1 -0
  107. package/dist/umd/components/VizDom/VizDomContainer.d.ts +2 -0
  108. package/dist/umd/components/VizDom/VizDomContainer.d.ts.map +1 -0
  109. package/dist/umd/components/VizDom/VizDomRenderer.d.ts +6 -0
  110. package/dist/umd/components/VizDom/VizDomRenderer.d.ts.map +1 -0
  111. package/dist/umd/components/VizDom/index.d.ts +2 -0
  112. package/dist/umd/components/VizDom/index.d.ts.map +1 -0
  113. package/dist/umd/components/VizElement/VizElementRank.d.ts +75 -0
  114. package/dist/umd/components/VizElement/VizElementRank.d.ts.map +1 -0
  115. package/dist/umd/components/VizElement/VizElements.d.ts +7 -0
  116. package/dist/umd/components/VizElement/VizElements.d.ts.map +1 -0
  117. package/dist/umd/components/VizElement/index.d.ts +2 -0
  118. package/dist/umd/components/VizElement/index.d.ts.map +1 -0
  119. package/dist/umd/components/index.d.ts +4 -0
  120. package/dist/umd/components/index.d.ts.map +1 -0
  121. package/dist/umd/configs/iframe.d.ts +10 -0
  122. package/dist/umd/configs/iframe.d.ts.map +1 -0
  123. package/dist/umd/configs/index.d.ts +3 -0
  124. package/dist/umd/configs/index.d.ts.map +1 -0
  125. package/dist/umd/configs/style.d.ts +9 -0
  126. package/dist/umd/configs/style.d.ts.map +1 -0
  127. package/dist/umd/helpers/iframe.d.ts +3 -0
  128. package/dist/umd/helpers/iframe.d.ts.map +1 -0
  129. package/dist/umd/helpers/index.d.ts +2 -0
  130. package/dist/umd/helpers/index.d.ts.map +1 -0
  131. package/dist/umd/hooks/index.d.ts +3 -0
  132. package/dist/umd/hooks/index.d.ts.map +1 -0
  133. package/dist/umd/hooks/viz-render/index.d.ts +2 -0
  134. package/dist/umd/hooks/viz-render/index.d.ts.map +1 -0
  135. package/dist/umd/hooks/viz-render/useHeatmapRender.d.ts +6 -0
  136. package/dist/umd/hooks/viz-render/useHeatmapRender.d.ts.map +1 -0
  137. package/dist/umd/hooks/viz-render/useHeatmapVizRender.d.ts +6 -0
  138. package/dist/umd/hooks/viz-render/useHeatmapVizRender.d.ts.map +1 -0
  139. package/dist/umd/hooks/viz-render/useReplayRender.d.ts +9 -0
  140. package/dist/umd/hooks/viz-render/useReplayRender.d.ts.map +1 -0
  141. package/dist/umd/hooks/viz-scale/index.d.ts +2 -0
  142. package/dist/umd/hooks/viz-scale/index.d.ts.map +1 -0
  143. package/dist/umd/hooks/viz-scale/useContainerDimensions.d.ts +10 -0
  144. package/dist/umd/hooks/viz-scale/useContainerDimensions.d.ts.map +1 -0
  145. package/dist/umd/hooks/viz-scale/useContentDimensions.d.ts +11 -0
  146. package/dist/umd/hooks/viz-scale/useContentDimensions.d.ts.map +1 -0
  147. package/dist/umd/hooks/viz-scale/useHeatmapScale.d.ts +19 -0
  148. package/dist/umd/hooks/viz-scale/useHeatmapScale.d.ts.map +1 -0
  149. package/dist/umd/hooks/viz-scale/useIframeHeight.d.ts +10 -0
  150. package/dist/umd/hooks/viz-scale/useIframeHeight.d.ts.map +1 -0
  151. package/dist/umd/hooks/viz-scale/useScaleCalculation.d.ts +10 -0
  152. package/dist/umd/hooks/viz-scale/useScaleCalculation.d.ts.map +1 -0
  153. package/dist/umd/hooks/viz-scale/useScrollSync.d.ts +10 -0
  154. package/dist/umd/hooks/viz-scale/useScrollSync.d.ts.map +1 -0
  155. package/dist/umd/index.d.ts +4 -0
  156. package/dist/umd/index.d.ts.map +1 -0
  157. package/dist/umd/index.js +10 -0
  158. package/dist/umd/stores/data.d.ts +16 -0
  159. package/dist/umd/stores/data.d.ts.map +1 -0
  160. package/dist/umd/stores/index.d.ts +2 -0
  161. package/dist/umd/stores/index.d.ts.map +1 -0
  162. package/dist/umd/ui/BoxStack/BoxStack.d.ts +56 -0
  163. package/dist/umd/ui/BoxStack/BoxStack.d.ts.map +1 -0
  164. package/dist/umd/ui/BoxStack/index.d.ts +2 -0
  165. package/dist/umd/ui/BoxStack/index.d.ts.map +1 -0
  166. package/dist/umd/ui/index.d.ts +2 -0
  167. package/dist/umd/ui/index.d.ts.map +1 -0
  168. package/dist/umd/utils/device.d.ts +2 -0
  169. package/dist/umd/utils/device.d.ts.map +1 -0
  170. package/dist/umd/utils/retry.d.ts +2 -0
  171. package/dist/umd/utils/retry.d.ts.map +1 -0
  172. package/dist/umd/utils/sort.d.ts +3 -0
  173. package/dist/umd/utils/sort.d.ts.map +1 -0
  174. package/package.json +1 -1
@@ -0,0 +1,668 @@
1
+ "use client"
2
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
3
+ import { useNodesState, ReactFlow, Controls, Background } from '@xyflow/react';
4
+ import { useEffect, useMemo, useRef, useCallback, useState } from 'react';
5
+ import { Visualizer } from '@gemx-dev/clarity-visualize';
6
+ import { create } from 'zustand';
7
+
8
+ const initialNodes = { id: '1', position: { x: 0, y: 0 }, data: { label: '1' } };
9
+ const GraphView = ({ children, width, height }) => {
10
+ const [nodes, setNodes, onNodesChange] = useNodesState([
11
+ {
12
+ ...initialNodes,
13
+ width: width,
14
+ height: height,
15
+ },
16
+ ]);
17
+ const CustomNode = () => {
18
+ return jsx(Fragment, { children: children });
19
+ };
20
+ const nodeTypes = {
21
+ default: CustomNode,
22
+ };
23
+ useEffect(() => {
24
+ if (!width)
25
+ return;
26
+ setNodes((prev) => {
27
+ const node = prev.find((node) => node.id === '1');
28
+ const newNode = {
29
+ ...node,
30
+ measured: { height: height, width: width },
31
+ height: height,
32
+ width: width,
33
+ };
34
+ return [newNode];
35
+ });
36
+ }, [width, height, setNodes]);
37
+ return (jsxs(ReactFlow, { nodes: nodes, nodeTypes: nodeTypes, onNodesChange: onNodesChange, debug: true, minZoom: 0.5, maxZoom: 2, fitView: true, children: [jsx(Controls, {}), jsx(Background, {})] }));
38
+ };
39
+
40
+ const BoxStack = ({ children, ...props }) => {
41
+ const id = props.id;
42
+ const flexDirection = props.flexDirection;
43
+ const overflow = props.overflow || 'hidden';
44
+ const position = props.position || 'relative';
45
+ const flex = props.flex || 'none';
46
+ const justifyContent = props.justifyContent;
47
+ const alignItems = props.alignItems;
48
+ const style = props.style || {};
49
+ const gap = props.gap || 0;
50
+ const height = props.height || 'auto';
51
+ const styleGap = useMemo(() => {
52
+ switch (flexDirection) {
53
+ case 'row':
54
+ return {
55
+ columnGap: gap,
56
+ };
57
+ case 'column':
58
+ return {
59
+ rowGap: gap,
60
+ };
61
+ }
62
+ }, [gap, flexDirection]);
63
+ const styleProps = {
64
+ display: 'flex',
65
+ flexDirection,
66
+ overflow,
67
+ position,
68
+ flex,
69
+ justifyContent,
70
+ alignItems,
71
+ height,
72
+ ...styleGap,
73
+ ...style,
74
+ };
75
+ return (jsx("div", { id: id, style: styleProps, children: children }));
76
+ };
77
+
78
+ const ContentHeader = ({ children }) => {
79
+ return (jsx(BoxStack, { id: "gx-hm-content-header", flexDirection: "row", alignItems: "center", overflow: "auto", children: children }));
80
+ };
81
+
82
+ const HEATMAP_IFRAME = {
83
+ id: 'clarity-iframe',
84
+ title: 'Clarity Session Replay',
85
+ sandbox: 'allow-same-origin allow-scripts',
86
+ scrolling: 'no',
87
+ height: '100%',
88
+ };
89
+
90
+ const HEATMAP_CONFIG = {
91
+ paddingBlock: 0,
92
+ };
93
+ const HEATMAP_STYLE = {
94
+ wrapper: {
95
+ padding: `${HEATMAP_CONFIG.paddingBlock}px 0`,
96
+ },
97
+ };
98
+
99
+ const recreateIframe = (iframeRef, config) => {
100
+ const container = iframeRef.current?.parentElement;
101
+ if (!container)
102
+ return;
103
+ const oldIframe = iframeRef.current;
104
+ if (!oldIframe?.contentDocument?.body.innerHTML) {
105
+ return oldIframe;
106
+ }
107
+ if (oldIframe && oldIframe.parentElement) {
108
+ oldIframe.parentElement.removeChild(oldIframe);
109
+ }
110
+ const newIframe = document.createElement('iframe');
111
+ newIframe.id = HEATMAP_IFRAME.id;
112
+ newIframe.title = HEATMAP_IFRAME.title;
113
+ newIframe.sandbox = HEATMAP_IFRAME.sandbox;
114
+ newIframe.scrolling = HEATMAP_IFRAME.scrolling;
115
+ newIframe.width = config?.width ? `${config.width}px` : '100%';
116
+ // Append to container
117
+ container.appendChild(newIframe);
118
+ // Update ref
119
+ iframeRef.current = newIframe;
120
+ return newIframe;
121
+ };
122
+
123
+ const useHeatmapDataStore = create()((set, get) => ({
124
+ data: undefined,
125
+ config: undefined,
126
+ iframeHeight: 0,
127
+ state: {
128
+ hideSidebar: false,
129
+ },
130
+ setData: (data) => set({ data }),
131
+ setState: (state) => set({ state: { ...get().state, ...state } }),
132
+ setConfig: (config) => set({ config: { ...get().config, ...config } }),
133
+ setIframeHeight: (iframeHeight) => set({ iframeHeight }),
134
+ }));
135
+
136
+ function isMobileDevice(userAgent) {
137
+ if (!userAgent)
138
+ return false;
139
+ return /android|webos|iphone|ipad|ipod|blackberry|windows phone|opera mini|iemobile|mobile|silk|fennec|bada|tizen|symbian|nokia|palmsource|meego|sailfish|kindle|playbook|bb10|rim/i.test(userAgent);
140
+ }
141
+
142
+ const useHeatmapRender = () => {
143
+ const data = useHeatmapDataStore((state) => state.data);
144
+ const config = useHeatmapDataStore((state) => state.config);
145
+ const setConfig = useHeatmapDataStore((state) => state.setConfig);
146
+ const visualizerRef = useRef(null);
147
+ const iframeRef = useRef(null);
148
+ const initializeVisualizer = useCallback((envelope, userAgent) => {
149
+ const iframe = iframeRef.current;
150
+ if (!iframe?.contentWindow)
151
+ return null;
152
+ const visualizer = new Visualizer();
153
+ const mobile = isMobileDevice(userAgent);
154
+ visualizer.setup(iframe.contentWindow, {
155
+ version: envelope.version,
156
+ onresize: (width) => {
157
+ setConfig({ width });
158
+ },
159
+ mobile,
160
+ vNext: true,
161
+ locale: 'en-us',
162
+ });
163
+ return visualizer;
164
+ }, [setConfig]);
165
+ // Process and render heatmap HTML
166
+ const renderHeatmap = useCallback(async (payloads) => {
167
+ if (!payloads || payloads.length === 0)
168
+ return;
169
+ let visualizer = visualizerRef.current || new Visualizer();
170
+ const iframe = recreateIframe(iframeRef, config);
171
+ // setIframeHeight(Number(iframeRef.current?.height || 0));
172
+ // for (const decoded of payloads) {
173
+ // // Initialize on first sequence
174
+ // if (decoded.envelope.sequence === 1) {
175
+ // const userAgent = (decoded.dimension?.[0]?.data[0]?.[0] as string) || '';
176
+ // visualizer = initializeVisualizer(decoded.envelope as any, userAgent);
177
+ // if (!visualizer) return;
178
+ // visualizerRef.current = visualizer;
179
+ // }
180
+ // if (!visualizer) continue;
181
+ // // Merge and process DOM
182
+ // const merged = visualizer.merge([decoded]);
183
+ // visualizer.dom(merged.dom);
184
+ // }
185
+ // Render static HTML
186
+ if (visualizer && iframe?.contentWindow) {
187
+ await visualizer.html(payloads, iframe.contentWindow);
188
+ }
189
+ }, [initializeVisualizer]);
190
+ useEffect(() => {
191
+ if (!data || data.length === 0)
192
+ return;
193
+ renderHeatmap(data);
194
+ return () => {
195
+ visualizerRef.current = null;
196
+ };
197
+ }, [config, data, renderHeatmap]);
198
+ return {
199
+ iframeRef,
200
+ };
201
+ };
202
+
203
+ function sortEvents(a, b) {
204
+ return a.time - b.time;
205
+ }
206
+
207
+ const useReplayRender = () => {
208
+ const data = useHeatmapDataStore((state) => state.data);
209
+ const setConfig = useHeatmapDataStore((state) => state.setConfig);
210
+ const visualizerRef = useRef(null);
211
+ const iframeRef = useRef(null);
212
+ const eventsRef = useRef([]);
213
+ const animationFrameRef = useRef(null);
214
+ const isPlayingRef = useRef(false);
215
+ // Initialize visualizer for replay
216
+ const initializeVisualizer = useCallback((envelope, userAgent) => {
217
+ const iframe = iframeRef.current;
218
+ if (!iframe?.contentWindow)
219
+ return null;
220
+ // Clear previous events
221
+ eventsRef.current = [];
222
+ const visualizer = new Visualizer();
223
+ const mobile = isMobileDevice(userAgent);
224
+ visualizer.setup(iframe.contentWindow, {
225
+ version: envelope.version,
226
+ onresize: (width) => {
227
+ setConfig({ width });
228
+ },
229
+ mobile,
230
+ vNext: true,
231
+ locale: 'en-us',
232
+ });
233
+ return visualizer;
234
+ }, [setConfig]);
235
+ // Animation loop for replay
236
+ const replayLoop = useCallback(() => {
237
+ if (!isPlayingRef.current)
238
+ return;
239
+ const events = eventsRef.current;
240
+ const visualizer = visualizerRef.current;
241
+ if (!visualizer || events.length === 0) {
242
+ animationFrameRef.current = requestAnimationFrame(replayLoop);
243
+ return;
244
+ }
245
+ const event = events[0];
246
+ const end = event.time + 16; // 60FPS => 16ms per frame
247
+ let index = 0;
248
+ // Get events within current frame
249
+ while (events[index] && events[index].time < end) {
250
+ index++;
251
+ }
252
+ // Render events for this frame
253
+ if (index > 0) {
254
+ visualizer.render(events.splice(0, index));
255
+ }
256
+ animationFrameRef.current = requestAnimationFrame(replayLoop);
257
+ }, []);
258
+ // Start replay
259
+ const startReplay = useCallback(async (payloads) => {
260
+ if (!payloads || payloads.length === 0)
261
+ return;
262
+ let visualizer = visualizerRef.current;
263
+ for (const decoded of payloads) {
264
+ // Initialize on first sequence
265
+ if (decoded.envelope.sequence === 1) {
266
+ const userAgent = decoded.dimension?.[0]?.data[0]?.[0] || '';
267
+ visualizer = initializeVisualizer(decoded.envelope, userAgent);
268
+ if (!visualizer)
269
+ return;
270
+ visualizerRef.current = visualizer;
271
+ }
272
+ if (!visualizer)
273
+ continue;
274
+ // Merge events and DOM
275
+ const merged = visualizer.merge([decoded]);
276
+ eventsRef.current = eventsRef.current.concat(merged.events).sort(sortEvents);
277
+ visualizer.dom(merged.dom);
278
+ }
279
+ // Render HTML
280
+ if (visualizer && iframeRef.current?.contentWindow) {
281
+ await visualizer.html(payloads, iframeRef.current.contentWindow);
282
+ }
283
+ // Auto-start replay
284
+ isPlayingRef.current = true;
285
+ animationFrameRef.current = requestAnimationFrame(replayLoop);
286
+ }, [initializeVisualizer, replayLoop]);
287
+ // Play control
288
+ const play = useCallback(() => {
289
+ if (!isPlayingRef.current) {
290
+ isPlayingRef.current = true;
291
+ animationFrameRef.current = requestAnimationFrame(replayLoop);
292
+ }
293
+ }, [replayLoop]);
294
+ // Pause control
295
+ const pause = useCallback(() => {
296
+ isPlayingRef.current = false;
297
+ if (animationFrameRef.current) {
298
+ cancelAnimationFrame(animationFrameRef.current);
299
+ animationFrameRef.current = null;
300
+ }
301
+ }, []);
302
+ // Main effect: Start replay when data changes
303
+ useEffect(() => {
304
+ if (!data || data.length === 0)
305
+ return;
306
+ startReplay(data);
307
+ // Cleanup
308
+ return () => {
309
+ isPlayingRef.current = false;
310
+ if (animationFrameRef.current) {
311
+ cancelAnimationFrame(animationFrameRef.current);
312
+ animationFrameRef.current = null;
313
+ }
314
+ eventsRef.current = [];
315
+ visualizerRef.current = null;
316
+ };
317
+ }, [data, startReplay]);
318
+ return {
319
+ iframeRef,
320
+ isPlaying: isPlayingRef.current,
321
+ play,
322
+ pause,
323
+ };
324
+ };
325
+
326
+ const useHeatmapVizRender = (mode) => {
327
+ const heatmapResult = useMemo(() => {
328
+ switch (mode) {
329
+ case 'heatmap':
330
+ return useHeatmapRender;
331
+ case 'replay':
332
+ return useReplayRender;
333
+ }
334
+ }, [mode]);
335
+ return heatmapResult();
336
+ };
337
+
338
+ const useContainerDimensions = (props) => {
339
+ const { wrapperRef } = props;
340
+ const [containerWidth, setContainerWidth] = useState(0);
341
+ const [containerHeight, setContainerHeight] = useState(0);
342
+ const resizeObserverRef = useRef(null);
343
+ const updateDimensions = useCallback(() => {
344
+ const scrollContainer = wrapperRef.current?.parentElement?.parentElement;
345
+ if (scrollContainer) {
346
+ setContainerWidth(scrollContainer.clientWidth);
347
+ setContainerHeight(scrollContainer.clientHeight);
348
+ }
349
+ }, [wrapperRef]);
350
+ useEffect(() => {
351
+ const scrollContainer = wrapperRef.current?.parentElement?.parentElement;
352
+ if (!scrollContainer || typeof window.ResizeObserver === 'undefined') {
353
+ return;
354
+ }
355
+ resizeObserverRef.current = new ResizeObserver(updateDimensions);
356
+ resizeObserverRef.current.observe(scrollContainer);
357
+ updateDimensions();
358
+ return () => {
359
+ if (resizeObserverRef.current && scrollContainer) {
360
+ resizeObserverRef.current.unobserve(scrollContainer);
361
+ }
362
+ };
363
+ }, [wrapperRef, updateDimensions]);
364
+ return { containerWidth, containerHeight };
365
+ };
366
+
367
+ const useContentDimensions = (props) => {
368
+ const { iframeRef, config } = props;
369
+ const [contentWidth, setContentWidth] = useState(0);
370
+ useEffect(() => {
371
+ if (config?.width) {
372
+ if (iframeRef.current) {
373
+ iframeRef.current.width = `${config.width}px`;
374
+ }
375
+ setContentWidth(config.width);
376
+ }
377
+ }, [config?.width, iframeRef]);
378
+ return { contentWidth };
379
+ };
380
+
381
+ // Hook 3: Iframe Height Observer
382
+ const useIframeHeight = (props) => {
383
+ const { iframeRef, contentWidth } = props;
384
+ const iframeHeight = useHeatmapDataStore((state) => state.iframeHeight);
385
+ const setIframeHeight = useHeatmapDataStore((state) => state.setIframeHeight);
386
+ const resizeObserverRef = useRef(null);
387
+ const mutationObserverRef = useRef(null);
388
+ const updateIframeHeight = useCallback(() => {
389
+ const iframe = iframeRef.current;
390
+ if (!iframe)
391
+ return;
392
+ try {
393
+ const iframeDocument = iframe.contentDocument;
394
+ const iframeBody = iframeDocument?.body;
395
+ if (!iframeBody)
396
+ return;
397
+ const bodyHeight = Math.max(iframeBody.scrollHeight, iframeBody.offsetHeight, iframeBody.clientHeight);
398
+ if (bodyHeight > 0) {
399
+ iframe.height = `${bodyHeight}px`;
400
+ setIframeHeight(bodyHeight);
401
+ }
402
+ }
403
+ catch (error) {
404
+ console.warn('Cannot measure iframe content:', error);
405
+ }
406
+ }, [iframeRef, setIframeHeight]);
407
+ // Trigger height update when content width changes
408
+ useEffect(() => {
409
+ if (contentWidth > 0) {
410
+ // Delay to allow iframe content to reflow after width change
411
+ const timeoutId = setTimeout(() => {
412
+ updateIframeHeight();
413
+ }, 100);
414
+ return () => clearTimeout(timeoutId);
415
+ }
416
+ }, [contentWidth, updateIframeHeight]);
417
+ useEffect(() => {
418
+ const iframe = iframeRef.current;
419
+ if (!iframe)
420
+ return;
421
+ const setupObservers = () => {
422
+ try {
423
+ const iframeDocument = iframe.contentDocument;
424
+ const iframeBody = iframeDocument?.body;
425
+ if (!iframeBody)
426
+ return;
427
+ // Cleanup existing observers
428
+ if (resizeObserverRef.current) {
429
+ resizeObserverRef.current.disconnect();
430
+ }
431
+ if (mutationObserverRef.current) {
432
+ mutationObserverRef.current.disconnect();
433
+ }
434
+ // ResizeObserver for size changes
435
+ if (typeof window.ResizeObserver !== 'undefined') {
436
+ resizeObserverRef.current = new ResizeObserver(updateIframeHeight);
437
+ resizeObserverRef.current.observe(iframeBody);
438
+ }
439
+ // MutationObserver for DOM changes
440
+ if (typeof window.MutationObserver !== 'undefined') {
441
+ mutationObserverRef.current = new MutationObserver(updateIframeHeight);
442
+ mutationObserverRef.current.observe(iframeBody, {
443
+ childList: true,
444
+ subtree: true,
445
+ attributes: true,
446
+ characterData: true,
447
+ });
448
+ }
449
+ // Initial measurement
450
+ updateIframeHeight();
451
+ }
452
+ catch (error) {
453
+ console.warn('Cannot access iframe content:', error);
454
+ }
455
+ };
456
+ if (iframe.contentDocument?.readyState === 'complete') {
457
+ setupObservers();
458
+ }
459
+ else {
460
+ iframe.addEventListener('load', setupObservers, { once: true });
461
+ }
462
+ return () => {
463
+ if (resizeObserverRef.current) {
464
+ resizeObserverRef.current.disconnect();
465
+ }
466
+ if (mutationObserverRef.current) {
467
+ mutationObserverRef.current.disconnect();
468
+ }
469
+ iframe.removeEventListener('load', setupObservers);
470
+ };
471
+ }, [iframeRef, updateIframeHeight]);
472
+ return { iframeHeight };
473
+ };
474
+
475
+ const useScaleCalculation = (props) => {
476
+ const { containerWidth, contentWidth } = props;
477
+ const [scale, setScale] = useState(1);
478
+ useEffect(() => {
479
+ if (containerWidth > 0 && contentWidth > 0) {
480
+ const availableWidth = containerWidth - HEATMAP_CONFIG['paddingBlock'] * 2;
481
+ const calculatedScale = Math.min(availableWidth / contentWidth, 1);
482
+ setScale(calculatedScale);
483
+ }
484
+ }, [containerWidth, contentWidth]);
485
+ return { scale };
486
+ };
487
+
488
+ const useScrollSync = (props) => {
489
+ const { iframeRef, scale } = props;
490
+ const handleScroll = useCallback((scrollTop) => {
491
+ const iframe = iframeRef.current;
492
+ if (!iframe || scale <= 0)
493
+ return;
494
+ try {
495
+ const iframeWindow = iframe.contentWindow;
496
+ const iframeDocument = iframe.contentDocument;
497
+ if (iframeWindow && iframeDocument) {
498
+ const iframeScrollTop = scrollTop / scale;
499
+ iframe.style.top = `${iframeScrollTop}px`;
500
+ }
501
+ }
502
+ catch (error) {
503
+ console.warn('Cannot sync scroll to iframe:', error);
504
+ }
505
+ }, [iframeRef, scale]);
506
+ return { handleScroll };
507
+ };
508
+
509
+ const useHeatmapScale = (props) => {
510
+ const { wrapperRef, iframeRef, config } = props;
511
+ // 1. Observe container dimensions
512
+ const { containerWidth, containerHeight } = useContainerDimensions({ wrapperRef });
513
+ // 2. Get content dimensions from config
514
+ const { contentWidth } = useContentDimensions({ iframeRef, config });
515
+ // 3. Observe iframe height (now reacts to width changes)
516
+ const { iframeHeight } = useIframeHeight({ iframeRef, contentWidth });
517
+ // 4. Calculate scale
518
+ const { scale } = useScaleCalculation({ containerWidth, contentWidth });
519
+ // 5. Setup scroll sync
520
+ const { handleScroll } = useScrollSync({ iframeRef, scale });
521
+ return {
522
+ containerWidth,
523
+ containerHeight,
524
+ contentWidth,
525
+ iframeHeight,
526
+ scale,
527
+ scaledWidth: contentWidth * scale,
528
+ scaledHeight: iframeHeight * scale,
529
+ handleScroll,
530
+ };
531
+ };
532
+
533
+ const VizElements = ({ width, height }) => {
534
+ return (jsx("div", { className: "gx-hm-elements", style: { width, height }, children: jsx("div", { className: "gx-hm-element" }) }));
535
+ };
536
+
537
+ const ReplayControls = () => {
538
+ const replayResult = useReplayRender();
539
+ return (jsxs("div", { style: {
540
+ position: 'absolute',
541
+ bottom: 20,
542
+ left: '50%',
543
+ transform: 'translateX(-50%)',
544
+ display: 'flex',
545
+ gap: 10,
546
+ backgroundColor: 'rgba(0, 0, 0, 0.7)',
547
+ padding: '10px 20px',
548
+ borderRadius: 8,
549
+ }, children: [jsx("button", { onClick: replayResult.play, style: {
550
+ padding: '8px 16px',
551
+ backgroundColor: '#4CAF50',
552
+ color: 'white',
553
+ border: 'none',
554
+ borderRadius: 4,
555
+ cursor: 'pointer',
556
+ }, children: "Play" }), jsx("button", { onClick: replayResult.pause, style: {
557
+ padding: '8px 16px',
558
+ backgroundColor: '#f44336',
559
+ color: 'white',
560
+ border: 'none',
561
+ borderRadius: 4,
562
+ cursor: 'pointer',
563
+ }, children: "Pause" })] }));
564
+ };
565
+
566
+ const VizDomRenderer = ({ mode = 'heatmap' }) => {
567
+ const config = useHeatmapDataStore((state) => state.config);
568
+ const wrapperRef = useRef(null);
569
+ const visualRef = useRef(null);
570
+ const { iframeRef } = useHeatmapVizRender(mode);
571
+ const { contentWidth, iframeHeight, scale, scaledHeight, handleScroll } = useHeatmapScale({
572
+ wrapperRef,
573
+ iframeRef,
574
+ config,
575
+ });
576
+ const onScroll = (e) => {
577
+ const scrollTop = e.currentTarget.scrollTop;
578
+ handleScroll(scrollTop);
579
+ };
580
+ return (jsxs("div", { ref: visualRef, className: "gx-hm-visual", onScroll: onScroll, style: {
581
+ overflow: 'hidden auto',
582
+ display: 'flex',
583
+ position: 'relative',
584
+ justifyContent: 'center',
585
+ flex: 1,
586
+ backgroundColor: '#fff',
587
+ // borderRadius: '8px',
588
+ }, children: [jsx("div", { className: "gx-hm-visual-unscaled", style: {
589
+ width: '100%',
590
+ display: 'flex',
591
+ justifyContent: 'center',
592
+ alignItems: 'flex-start',
593
+ height: scaledHeight > 0 ? `${scaledHeight + HEATMAP_CONFIG['paddingBlock']}px` : 'auto',
594
+ padding: HEATMAP_STYLE['wrapper']['padding'],
595
+ }, children: jsxs("div", { className: "gx-hm-wrapper", ref: wrapperRef, style: {
596
+ width: contentWidth,
597
+ height: iframeHeight,
598
+ transform: `scale(${scale})`,
599
+ transformOrigin: 'top center',
600
+ }, children: [jsx(VizElements, { width: contentWidth, height: iframeHeight }), jsx("iframe", {
601
+ // key={iframeKey}
602
+ ref: iframeRef, ...HEATMAP_IFRAME, width: contentWidth, height: iframeHeight, scrolling: "no" })] }) }), mode === 'replay' && jsx(ReplayControls, {})] }));
603
+ };
604
+
605
+ const VizDomContainer = () => {
606
+ return (jsx(BoxStack, { id: "gx-hm-viz-container", flexDirection: "column", flex: "1 1 auto", overflow: "auto", children: jsx(BoxStack, { id: "gx-hm-content", flexDirection: "column", flex: "1 1 auto", overflow: "hidden", style: {
607
+ // margin: '0px 16px 0px 12px',
608
+ // borderRadius: '8px 8px 0 0',
609
+ // borderRadius: '8px',
610
+ minWidth: '394px',
611
+ padding: '12px',
612
+ background: '#f1f1f1',
613
+ }, children: jsx(VizDomRenderer, {}) }) }));
614
+ };
615
+
616
+ const SIDEBAR_WIDTH = 280;
617
+ const LeftSidebar = ({ children }) => {
618
+ const isHideSidebar = useHeatmapDataStore((state) => state.state.hideSidebar);
619
+ if (isHideSidebar) {
620
+ return null;
621
+ }
622
+ return (jsx("div", { className: "gx-hm-sidebar", style: {
623
+ height: '100%',
624
+ display: 'flex',
625
+ ...(isHideSidebar
626
+ ? {
627
+ width: '0',
628
+ transform: 'translateX(-100%)',
629
+ visibility: 'hidden',
630
+ }
631
+ : { width: `${SIDEBAR_WIDTH}px` }),
632
+ }, children: jsx("div", { className: "gx-hm-sidebar-wrapper", style: { height: '100%', width: `${SIDEBAR_WIDTH}px` }, children: children }) }));
633
+ };
634
+
635
+ const WrapperPreview = ({ children }) => {
636
+ return (jsxs("div", { className: "gx-hm-container", style: { display: 'flex', overflowY: 'hidden', flex: '1', position: 'relative' }, children: [jsx(LeftSidebar, { children: children }), jsx(VizDomContainer, {})] }));
637
+ };
638
+
639
+ const WrapperLayout = ({ header, sidebar }) => {
640
+ return (jsxs(BoxStack, { id: "gx-hm-layout", flexDirection: "column", flex: "1", children: [jsx(ContentHeader, { children: header }), jsx(WrapperPreview, { children: sidebar })] }));
641
+ };
642
+
643
+ const HeatmapLayout = ({ header, sidebar }) => {
644
+ return (jsx(BoxStack, { id: "gx-hm-project", flexDirection: "column", flex: "1", height: "100%", children: jsx(BoxStack, { id: "gx-hm-project-content", flexDirection: "column", flex: "1", children: jsx("div", { style: {
645
+ minHeight: '100%',
646
+ display: 'flex',
647
+ }, children: jsx(WrapperLayout, { header: header, sidebar: sidebar }) }) }) }));
648
+ };
649
+
650
+ var PanelContent;
651
+ (function (PanelContent) {
652
+ PanelContent["Sessions"] = "Sessions";
653
+ PanelContent["Timeline"] = "Timeline";
654
+ PanelContent["Area"] = "Area";
655
+ PanelContent["Click"] = "Click";
656
+ PanelContent["Scroll"] = "Scroll";
657
+ PanelContent["Attention"] = "Attention";
658
+ })(PanelContent || (PanelContent = {}));
659
+ var ErrorType;
660
+ (function (ErrorType) {
661
+ ErrorType["NoResults"] = "NoResults";
662
+ ErrorType["NoClicks"] = "NoClicks";
663
+ ErrorType["NoScroll"] = "NoScroll";
664
+ ErrorType["ServerError"] = "ServerError";
665
+ ErrorType["DataError"] = "DataError";
666
+ })(ErrorType || (ErrorType = {}));
667
+
668
+ export { GraphView, HeatmapLayout, useHeatmapDataStore };
@@ -0,0 +1,16 @@
1
+ import { DecodedPayload, IHeatmapConfig } from '../types';
2
+ export interface IHeatmapState {
3
+ hideSidebar: boolean;
4
+ }
5
+ export interface IHeatmapDataStore {
6
+ data?: DecodedPayload[];
7
+ config?: IHeatmapConfig;
8
+ iframeHeight: number;
9
+ state: IHeatmapState;
10
+ setState: (state: IHeatmapState) => void;
11
+ setData: (data: DecodedPayload[]) => void;
12
+ setConfig: (config: IHeatmapConfig) => void;
13
+ setIframeHeight: (iframeHeight: number) => void;
14
+ }
15
+ export declare const useHeatmapDataStore: import("zustand").UseBoundStore<import("zustand").StoreApi<IHeatmapDataStore>>;
16
+ //# sourceMappingURL=data.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data.d.ts","sourceRoot":"","sources":["../../src/stores/data.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE1D,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,EAAE,cAAc,EAAE,CAAC;IACxB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,aAAa,CAAC;IACrB,QAAQ,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IACzC,OAAO,EAAE,CAAC,IAAI,EAAE,cAAc,EAAE,KAAK,IAAI,CAAC;IAC1C,SAAS,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,CAAC;IAC5C,eAAe,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;CACjD;AAED,eAAO,MAAM,mBAAmB,gFAW7B,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from './data';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/stores/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC"}