@stacksjs/ts-analytics 0.1.6

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 (237) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +361 -0
  4. package/bin/cli.ts +169 -0
  5. package/dist/Analytics.d.ts +558 -0
  6. package/dist/api.d.ts +109 -0
  7. package/dist/batching.d.ts +93 -0
  8. package/dist/chunk-2mx7fq49.js +4 -0
  9. package/dist/chunk-3z29508k.js +204 -0
  10. package/dist/chunk-deephkz6.js +56 -0
  11. package/dist/chunk-j261vgyp.js +305 -0
  12. package/dist/chunk-xga17tz7.js +207 -0
  13. package/dist/config.d.ts +132 -0
  14. package/dist/dashboard/components/index.d.ts +80 -0
  15. package/dist/dashboard/composables/useAnalytics.d.ts +99 -0
  16. package/dist/dashboard/index.d.ts +110 -0
  17. package/dist/dashboard/types/index.d.ts +144 -0
  18. package/dist/dashboard/utils/index.d.ts +81 -0
  19. package/dist/dynamodb.d.ts +86 -0
  20. package/dist/funnels.d.ts +104 -0
  21. package/dist/geolocation.d.ts +96 -0
  22. package/dist/index.d.ts +371 -0
  23. package/dist/index.js +10211 -0
  24. package/dist/infrastructure/cdk.d.ts +60 -0
  25. package/dist/infrastructure/cloudformation.d.ts +44 -0
  26. package/dist/infrastructure/index.d.ts +45 -0
  27. package/dist/infrastructure/setup.d.ts +133 -0
  28. package/dist/integrations/cloudflare.d.ts +76 -0
  29. package/dist/integrations/hono.d.ts +60 -0
  30. package/dist/integrations/index.d.ts +24 -0
  31. package/dist/integrations/nuxt.d.ts +7 -0
  32. package/dist/integrations/nuxt.js +42 -0
  33. package/dist/integrations/runtime/use-ts-analytics.d.ts +18 -0
  34. package/dist/integrations/runtime/use-ts-analytics.js +17 -0
  35. package/dist/integrations/stx.d.ts +88 -0
  36. package/dist/integrations/stx.js +18 -0
  37. package/dist/lib/crypto-random.d.ts +4 -0
  38. package/dist/lib/salt.d.ts +16 -0
  39. package/dist/local.d.ts +56 -0
  40. package/dist/model-connector.d.ts +145 -0
  41. package/dist/models/AggregatedStats.d.ts +9 -0
  42. package/dist/models/CampaignStats.d.ts +9 -0
  43. package/dist/models/Conversion.d.ts +11 -0
  44. package/dist/models/CustomEvent.d.ts +11 -0
  45. package/dist/models/DeviceStats.d.ts +9 -0
  46. package/dist/models/EventStats.d.ts +9 -0
  47. package/dist/models/GeoStats.d.ts +9 -0
  48. package/dist/models/Goal.d.ts +9 -0
  49. package/dist/models/GoalStats.d.ts +9 -0
  50. package/dist/models/PageStats.d.ts +11 -0
  51. package/dist/models/PageView.d.ts +13 -0
  52. package/dist/models/RealtimeStats.d.ts +9 -0
  53. package/dist/models/ReferrerStats.d.ts +9 -0
  54. package/dist/models/Session.d.ts +11 -0
  55. package/dist/models/Site.d.ts +11 -0
  56. package/dist/models/index.d.ts +28 -0
  57. package/dist/models/types.d.ts +60 -0
  58. package/dist/sqs-buffering.d.ts +243 -0
  59. package/dist/stacks-integration.d.ts +159 -0
  60. package/dist/tracking-script.d.ts +71 -0
  61. package/dist/tracking.d.ts +17 -0
  62. package/dist/tracking.js +26 -0
  63. package/dist/types.d.ts +595 -0
  64. package/dist/utils/geolocation.d.ts +111 -0
  65. package/dist/utils/user-agent.d.ts +17 -0
  66. package/dist/version.d.ts +7 -0
  67. package/package.json +119 -0
  68. package/src/Analytics.ts +3349 -0
  69. package/src/api.ts +1286 -0
  70. package/src/assets/crosswind.css +2220 -0
  71. package/src/batching.ts +452 -0
  72. package/src/components/dashboard/index.ts +18 -0
  73. package/src/config.ts +456 -0
  74. package/src/dashboard/Dashboard.stx +1517 -0
  75. package/src/dashboard/components/AlertCard.stx +177 -0
  76. package/src/dashboard/components/AnalyticsDashboard.stx +354 -0
  77. package/src/dashboard/components/AnimatedNumber.stx +86 -0
  78. package/src/dashboard/components/BarChart.stx +220 -0
  79. package/src/dashboard/components/BrowserBreakdown.stx +98 -0
  80. package/src/dashboard/components/BrowsersTable.stx +125 -0
  81. package/src/dashboard/components/CampaignBreakdown.stx +163 -0
  82. package/src/dashboard/components/CampaignTable.stx +238 -0
  83. package/src/dashboard/components/CountryList.stx +101 -0
  84. package/src/dashboard/components/DataTable.stx +226 -0
  85. package/src/dashboard/components/DateRangePicker.stx +77 -0
  86. package/src/dashboard/components/DeviceBreakdown.stx +94 -0
  87. package/src/dashboard/components/DevicesTable.stx +163 -0
  88. package/src/dashboard/components/DonutChart.stories.ts +55 -0
  89. package/src/dashboard/components/DonutChart.stx +157 -0
  90. package/src/dashboard/components/EmptyState.stx +90 -0
  91. package/src/dashboard/components/EngagementMetrics.stx +183 -0
  92. package/src/dashboard/components/EventsSection.stx +192 -0
  93. package/src/dashboard/components/FilterBar.stx +104 -0
  94. package/src/dashboard/components/FullAnalyticsDashboard.stx +455 -0
  95. package/src/dashboard/components/FunnelChart.stx +142 -0
  96. package/src/dashboard/components/GeoTable.stx +337 -0
  97. package/src/dashboard/components/GoalsPanel.stx +109 -0
  98. package/src/dashboard/components/Header.stx +39 -0
  99. package/src/dashboard/components/HeatmapChart.stx +140 -0
  100. package/src/dashboard/components/LiveActivityFeed.stx +172 -0
  101. package/src/dashboard/components/MetricComparison.stx +106 -0
  102. package/src/dashboard/components/MiniStats.stx +96 -0
  103. package/src/dashboard/components/OSBreakdown.stx +124 -0
  104. package/src/dashboard/components/PageDetailCard.stx +102 -0
  105. package/src/dashboard/components/PagesTable.stx +127 -0
  106. package/src/dashboard/components/ProgressRing.stx +89 -0
  107. package/src/dashboard/components/RealtimeCounter.stories.ts +45 -0
  108. package/src/dashboard/components/RealtimeCounter.stx +46 -0
  109. package/src/dashboard/components/ReferrersTable.stx +180 -0
  110. package/src/dashboard/components/SparklineChart.stx +160 -0
  111. package/src/dashboard/components/StatCard.stories.ts +58 -0
  112. package/src/dashboard/components/StatCard.stx +90 -0
  113. package/src/dashboard/components/SummaryStats.stx +81 -0
  114. package/src/dashboard/components/TabNav.stx +66 -0
  115. package/src/dashboard/components/ThemeSwitcher.stx +124 -0
  116. package/src/dashboard/components/TimeSeriesChart.stx +106 -0
  117. package/src/dashboard/components/TopList.stories.ts +58 -0
  118. package/src/dashboard/components/TopList.stx +74 -0
  119. package/src/dashboard/components/TrendIndicator.stx +84 -0
  120. package/src/dashboard/components/heatmap/ClickHeatmap.stx +264 -0
  121. package/src/dashboard/components/heatmap/ElementClickList.stx +125 -0
  122. package/src/dashboard/components/heatmap/HeatmapControls.stx +125 -0
  123. package/src/dashboard/components/heatmap/HeatmapLegend.stx +69 -0
  124. package/src/dashboard/components/heatmap/PageHeatmap.stx +264 -0
  125. package/src/dashboard/components/heatmap/ScrollHeatmap.stx +127 -0
  126. package/src/dashboard/components/heatmap/index.ts +12 -0
  127. package/src/dashboard/components/index.ts +86 -0
  128. package/src/dashboard/composables/useAnalytics.ts +465 -0
  129. package/src/dashboard/demo/DemoApp.stx +370 -0
  130. package/src/dashboard/demo/index.ts +8 -0
  131. package/src/dashboard/demo/mockData.ts +234 -0
  132. package/src/dashboard/index.ts +117 -0
  133. package/src/dashboard/stx-shim.d.ts +4 -0
  134. package/src/dashboard/types/index.ts +203 -0
  135. package/src/dashboard/utils/index.ts +426 -0
  136. package/src/dynamodb.ts +344 -0
  137. package/src/funnels.ts +534 -0
  138. package/src/geolocation.ts +515 -0
  139. package/src/handlers/alerts.ts +328 -0
  140. package/src/handlers/annotations.ts +128 -0
  141. package/src/handlers/api-keys.ts +240 -0
  142. package/src/handlers/auth.ts +1020 -0
  143. package/src/handlers/authz.ts +137 -0
  144. package/src/handlers/collect.ts +724 -0
  145. package/src/handlers/data.ts +625 -0
  146. package/src/handlers/experiments.ts +138 -0
  147. package/src/handlers/funnels.ts +216 -0
  148. package/src/handlers/goals.ts +218 -0
  149. package/src/handlers/heatmaps.ts +272 -0
  150. package/src/handlers/index.ts +56 -0
  151. package/src/handlers/lib/read-cache.ts +63 -0
  152. package/src/handlers/misc.ts +486 -0
  153. package/src/handlers/oauth.ts +233 -0
  154. package/src/handlers/performance.ts +388 -0
  155. package/src/handlers/sessions.ts +413 -0
  156. package/src/handlers/sharing.ts +198 -0
  157. package/src/handlers/stats.ts +1368 -0
  158. package/src/handlers/team.ts +161 -0
  159. package/src/handlers/uptime.ts +283 -0
  160. package/src/handlers/views.ts +390 -0
  161. package/src/handlers/webhooks.ts +226 -0
  162. package/src/heatmap/index.ts +32 -0
  163. package/src/heatmap/tracking-script.ts +452 -0
  164. package/src/heatmap/types.ts +79 -0
  165. package/src/index.ts +387 -0
  166. package/src/infrastructure/cdk.ts +496 -0
  167. package/src/infrastructure/cloudformation.ts +595 -0
  168. package/src/infrastructure/index.ts +48 -0
  169. package/src/infrastructure/setup.ts +611 -0
  170. package/src/integrations/cloudflare.ts +732 -0
  171. package/src/integrations/hono.ts +589 -0
  172. package/src/integrations/index.ts +27 -0
  173. package/src/integrations/nuxt.ts +78 -0
  174. package/src/integrations/runtime/use-ts-analytics.ts +32 -0
  175. package/src/integrations/stx.ts +138 -0
  176. package/src/jobs/index.ts +127 -0
  177. package/src/lib/crypto-random.ts +41 -0
  178. package/src/lib/ddb-errors.ts +20 -0
  179. package/src/lib/dynamodb.ts +216 -0
  180. package/src/lib/email.ts +38 -0
  181. package/src/lib/ga-import.ts +471 -0
  182. package/src/lib/ga4-api.ts +244 -0
  183. package/src/lib/goals.ts +205 -0
  184. package/src/lib/index.ts +6 -0
  185. package/src/lib/ingest-counters.ts +123 -0
  186. package/src/lib/log.ts +36 -0
  187. package/src/lib/membership.ts +98 -0
  188. package/src/lib/plans.ts +90 -0
  189. package/src/lib/quota.ts +46 -0
  190. package/src/lib/rate-limit.ts +27 -0
  191. package/src/lib/rollups.ts +472 -0
  192. package/src/lib/salt.ts +97 -0
  193. package/src/lib/scheduler.ts +97 -0
  194. package/src/lib/significance.ts +66 -0
  195. package/src/lib/site-retention.ts +56 -0
  196. package/src/local.ts +360 -0
  197. package/src/model-connector.ts +616 -0
  198. package/src/models/AggregatedStats.ts +162 -0
  199. package/src/models/CampaignStats.ts +141 -0
  200. package/src/models/Conversion.ts +135 -0
  201. package/src/models/CustomEvent.ts +123 -0
  202. package/src/models/DeviceStats.ts +105 -0
  203. package/src/models/EventStats.ts +108 -0
  204. package/src/models/GeoStats.ts +112 -0
  205. package/src/models/Goal.ts +105 -0
  206. package/src/models/GoalStats.ts +106 -0
  207. package/src/models/PageStats.ts +152 -0
  208. package/src/models/PageView.ts +277 -0
  209. package/src/models/RealtimeStats.ts +96 -0
  210. package/src/models/ReferrerStats.ts +115 -0
  211. package/src/models/Session.ts +235 -0
  212. package/src/models/Site.ts +115 -0
  213. package/src/models/index.ts +50 -0
  214. package/src/models/orm/index.ts +2043 -0
  215. package/src/models/types.ts +71 -0
  216. package/src/router.ts +521 -0
  217. package/src/sqs-buffering.ts +806 -0
  218. package/src/stacks-integration.ts +527 -0
  219. package/src/tracking-script.ts +944 -0
  220. package/src/tracking.ts +27 -0
  221. package/src/types/analytics.ts +219 -0
  222. package/src/types/api.ts +75 -0
  223. package/src/types/bun-router.d.ts +15 -0
  224. package/src/types/dashboard.ts +62 -0
  225. package/src/types/index.ts +66 -0
  226. package/src/types/stx.d.ts +27 -0
  227. package/src/types/window.d.ts +101 -0
  228. package/src/types.ts +911 -0
  229. package/src/utils/cache.ts +148 -0
  230. package/src/utils/date.ts +118 -0
  231. package/src/utils/filters.ts +71 -0
  232. package/src/utils/geolocation.ts +323 -0
  233. package/src/utils/index.ts +9 -0
  234. package/src/utils/response.ts +180 -0
  235. package/src/utils/timezone-country.ts +242 -0
  236. package/src/utils/user-agent.ts +110 -0
  237. package/src/version.ts +7 -0
@@ -0,0 +1,264 @@
1
+ <script>
2
+ /**
3
+ * ClickHeatmap Component
4
+ * Canvas-based click density overlay for page heatmap visualization.
5
+ * Uses STX signals for reactive state management.
6
+ */
7
+ const clicks = state($props.clicks || [])
8
+ const width = $props.width || 800
9
+ const height = $props.height || 600
10
+ const radius = $props.radius || 25
11
+ const blur = $props.blur || 15
12
+ const maxOpacity = $props.maxOpacity || 0.6
13
+ const loading = state($props.loading || false)
14
+
15
+ // Reactive tooltip state
16
+ const hoveredPoint = state(null)
17
+ const tooltipX = state(0)
18
+ const tooltipY = state(0)
19
+
20
+ // Computed: max click count for normalization
21
+ const maxCount = derived(() => {
22
+ const c = clicks()
23
+ return c.length > 0 ? Math.max(...c.map(p => p.count)) : 1
24
+ })
25
+
26
+ // Initialize canvas on mount
27
+ function initCanvas() {
28
+ const canvas = document.getElementById('heatmap-canvas')
29
+ if (!canvas) return
30
+
31
+ const ctx = canvas.getContext('2d')
32
+ if (!ctx) return
33
+
34
+ // Clear canvas
35
+ ctx.clearRect(0, 0, width, height)
36
+
37
+ // Draw all points with intensity based on count
38
+ const max = maxCount()
39
+ for (const point of clicks()) {
40
+ const intensity = (point.count / max) * maxOpacity
41
+ drawPoint(ctx, point.x, point.y, intensity)
42
+ }
43
+
44
+ // Apply blur
45
+ if (blur > 0) {
46
+ ctx.filter = `blur(${blur}px)`
47
+ ctx.drawImage(canvas, 0, 0)
48
+ ctx.filter = 'none'
49
+ }
50
+
51
+ // Colorize using gradient
52
+ colorizeHeatmap(ctx)
53
+ }
54
+
55
+ function drawPoint(ctx, x, y, intensity) {
56
+ const gradient = ctx.createRadialGradient(x, y, 0, x, y, radius)
57
+ gradient.addColorStop(0, `rgba(0, 0, 0, ${intensity})`)
58
+ gradient.addColorStop(1, 'rgba(0, 0, 0, 0)')
59
+ ctx.fillStyle = gradient
60
+ ctx.beginPath()
61
+ ctx.arc(x, y, radius, 0, Math.PI * 2)
62
+ ctx.fill()
63
+ }
64
+
65
+ function colorizeHeatmap(ctx) {
66
+ const palette = createGradientPalette()
67
+ const imageData = ctx.getImageData(0, 0, width, height)
68
+ const data = imageData.data
69
+
70
+ for (let i = 0; i < data.length; i += 4) {
71
+ const alpha = data[i + 3]
72
+ if (alpha > 0) {
73
+ const paletteIndex = Math.min(255, Math.floor((alpha / 255) * 255)) * 4
74
+ data[i] = palette[paletteIndex]
75
+ data[i + 1] = palette[paletteIndex + 1]
76
+ data[i + 2] = palette[paletteIndex + 2]
77
+ data[i + 3] = alpha * 2
78
+ }
79
+ }
80
+
81
+ ctx.putImageData(imageData, 0, 0)
82
+ }
83
+
84
+ function createGradientPalette() {
85
+ const canvas = document.createElement('canvas')
86
+ canvas.width = 256
87
+ canvas.height = 1
88
+ const ctx = canvas.getContext('2d')
89
+ const gradient = ctx.createLinearGradient(0, 0, 256, 0)
90
+
91
+ gradient.addColorStop(0.0, 'rgba(0, 0, 255, 0)')
92
+ gradient.addColorStop(0.25, 'rgba(0, 0, 255, 0.5)')
93
+ gradient.addColorStop(0.5, 'rgba(0, 255, 0, 0.5)')
94
+ gradient.addColorStop(0.75, 'rgba(255, 255, 0, 0.75)')
95
+ gradient.addColorStop(1.0, 'rgba(255, 0, 0, 1)')
96
+
97
+ ctx.fillStyle = gradient
98
+ ctx.fillRect(0, 0, 256, 1)
99
+ return ctx.getImageData(0, 0, 256, 1).data
100
+ }
101
+
102
+ function handleMouseMove(event) {
103
+ const canvas = event.target
104
+ const rect = canvas.getBoundingClientRect()
105
+ const x = event.clientX - rect.left
106
+ const y = event.clientY - rect.top
107
+
108
+ // Find nearest point within threshold
109
+ const threshold = radius * 2
110
+ let nearest = null
111
+ let minDistance = threshold
112
+
113
+ for (const point of clicks()) {
114
+ const distance = Math.sqrt((point.x - x) ** 2 + (point.y - y) ** 2)
115
+ if (distance < minDistance) {
116
+ minDistance = distance
117
+ nearest = point
118
+ }
119
+ }
120
+
121
+ hoveredPoint.set(nearest)
122
+ if (nearest) {
123
+ tooltipX.set(nearest.x + 15)
124
+ tooltipY.set(nearest.y - 10)
125
+ }
126
+
127
+ // Call optional callback
128
+ if ($props.onHover) $props.onHover(nearest)
129
+ }
130
+
131
+ function handleMouseLeave() {
132
+ hoveredPoint.set(null)
133
+ if ($props.onHover) $props.onHover(null)
134
+ }
135
+
136
+ function handleClick() {
137
+ const point = hoveredPoint()
138
+ if (point && $props.onClick) {
139
+ $props.onClick(point)
140
+ }
141
+ }
142
+
143
+ onMount(() => {
144
+ initCanvas()
145
+ })
146
+
147
+ // Re-render when clicks change
148
+ effect(() => {
149
+ clicks()
150
+ initCanvas()
151
+ })
152
+ </script>
153
+
154
+ <div class="click-heatmap" @style="{ width: width + 'px', height: height + 'px', position: 'relative', overflow: 'hidden' }">
155
+ <!-- Loading overlay -->
156
+ <div x-if="loading()" class="heatmap-loading-overlay">
157
+ <div class="heatmap-spinner"></div>
158
+ <span>Loading heatmap data...</span>
159
+ </div>
160
+
161
+ <!-- Main heatmap canvas -->
162
+ <canvas
163
+ id="heatmap-canvas"
164
+ x-width="width"
165
+ x-height="height"
166
+ class="heatmap-canvas"
167
+ ></canvas>
168
+
169
+ <!-- Interaction overlay -->
170
+ <canvas
171
+ id="heatmap-interaction"
172
+ x-width="width"
173
+ x-height="height"
174
+ class="heatmap-interaction"
175
+ @mousemove="handleMouseMove"
176
+ @mouseleave="handleMouseLeave"
177
+ @click="handleClick"
178
+ ></canvas>
179
+
180
+ <!-- Tooltip -->
181
+ <div
182
+ x-if="hoveredPoint()"
183
+ class="heatmap-tooltip"
184
+ @style="{ left: tooltipX() + 'px', top: tooltipY() + 'px' }"
185
+ >
186
+ <div class="heatmap-tooltip-content">
187
+ <strong class="heatmap-tooltip-count">{{ hoveredPoint().count }}</strong> clicks
188
+ <div x-if="hoveredPoint().topElement" class="heatmap-tooltip-selector">
189
+ {{ hoveredPoint().topElement.selector }}
190
+ </div>
191
+ </div>
192
+ </div>
193
+ </div>
194
+
195
+ <style>
196
+ .click-heatmap {
197
+ position: relative;
198
+ overflow: hidden;
199
+ }
200
+ .heatmap-loading-overlay {
201
+ position: absolute;
202
+ inset: 0;
203
+ display: flex;
204
+ flex-direction: column;
205
+ align-items: center;
206
+ justify-content: center;
207
+ background: rgba(255, 255, 255, 0.9);
208
+ z-index: 20;
209
+ gap: 0.75rem;
210
+ color: #6b7280;
211
+ font-size: 0.875rem;
212
+ }
213
+ .heatmap-spinner {
214
+ width: 32px;
215
+ height: 32px;
216
+ border: 2px solid #e5e7eb;
217
+ border-top-color: #6366f1;
218
+ border-radius: 50%;
219
+ animation: spin 1s linear infinite;
220
+ }
221
+ @keyframes spin {
222
+ to { transform: rotate(-360deg); }
223
+ }
224
+ .heatmap-canvas {
225
+ position: absolute;
226
+ top: 0;
227
+ left: 0;
228
+ pointer-events: none;
229
+ }
230
+ .heatmap-interaction {
231
+ position: absolute;
232
+ top: 0;
233
+ left: 0;
234
+ cursor: crosshair;
235
+ background: transparent;
236
+ }
237
+ .heatmap-tooltip {
238
+ position: absolute;
239
+ z-index: 30;
240
+ pointer-events: none;
241
+ transform: translateY(-100%);
242
+ }
243
+ .heatmap-tooltip-content {
244
+ background: #1f2937;
245
+ color: white;
246
+ padding: 0.5rem 0.75rem;
247
+ border-radius: 6px;
248
+ font-size: 0.75rem;
249
+ white-space: nowrap;
250
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
251
+ }
252
+ .heatmap-tooltip-count {
253
+ color: #fbbf24;
254
+ }
255
+ .heatmap-tooltip-selector {
256
+ font-size: 0.625rem;
257
+ color: #9ca3af;
258
+ font-family: monospace;
259
+ margin-top: 0.25rem;
260
+ overflow: hidden;
261
+ text-overflow: ellipsis;
262
+ max-width: 200px;
263
+ }
264
+ </style>
@@ -0,0 +1,125 @@
1
+ <script>
2
+ /**
3
+ * ElementClickList Component
4
+ *
5
+ * Displays a table of top clicked elements with their selectors and counts.
6
+ */
7
+ export const elements = $props.elements || []
8
+ export const loading = $props.loading || false
9
+ export const maxItems = $props.maxItems || 10
10
+ export const onHighlight = $props.onHighlight
11
+ export const onClick = $props.onClick
12
+
13
+ const displayElements = elements.slice(0, maxItems)
14
+ const totalClicks = elements.reduce((sum, el) => sum + el.count, 0)
15
+
16
+ export function formatSelector(selector) {
17
+ if (selector.length > 50) {
18
+ return selector.slice(0, 47) + '...'
19
+ }
20
+ return selector
21
+ }
22
+
23
+ export function getPercentage(count) {
24
+ if (totalClicks === 0) return 0
25
+ return Math.round((count / totalClicks) * 100)
26
+ }
27
+
28
+ export function formatCount(count) {
29
+ if (count >= 1000) {
30
+ return `${(count / 1000).toFixed(1)}k`
31
+ }
32
+ return count.toString()
33
+ }
34
+
35
+ export function handleMouseEnter(selector) {
36
+ onHighlight?.(selector)
37
+ }
38
+
39
+ export function handleMouseLeave() {
40
+ onHighlight?.(null)
41
+ }
42
+
43
+ export function handleClick(element) {
44
+ onClick?.(element)
45
+ }
46
+ </script>
47
+
48
+ <div class="element-click-list bg-white rounded-lg border border-gray-200 overflow-hidden">
49
+ <div class="flex justify-between items-center p-4 border-b border-gray-200">
50
+ <h3 class="text-sm font-semibold text-gray-900">Top Clicked Elements</h3>
51
+ @if(!loading)
52
+ <span class="text-xs text-gray-500">{{ formatCount(totalClicks) }} total clicks</span>
53
+ @endif
54
+ </div>
55
+
56
+ <!-- Loading state -->
57
+ @if(loading)
58
+ <div class="p-4">
59
+ @each(i in [1, 2, 3, 4, 5])
60
+ <div class="flex justify-between py-3">
61
+ <div class="h-4 w-3/5 bg-gray-200 rounded animate-pulse" />
62
+ <div class="h-4 w-12 bg-gray-200 rounded animate-pulse" />
63
+ </div>
64
+ @endeach
65
+ </div>
66
+ @elseif(elements.length === 0)
67
+ <!-- Empty state -->
68
+ <div class="flex flex-col items-center py-8 text-gray-500">
69
+ <Icon name="mouse-pointer" size="40" class="mb-3" />
70
+ <p>No click data available</p>
71
+ </div>
72
+ @else
73
+ <!-- Element list -->
74
+ <div class="text-sm">
75
+ <div class="flex justify-between px-4 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider bg-gray-50 border-b border-gray-200">
76
+ <span>Element</span>
77
+ <span>Clicks</span>
78
+ </div>
79
+
80
+ @each(element, index in displayElements)
81
+ <div
82
+ class="flex justify-between items-center px-4 py-3 cursor-pointer hover:bg-gray-50 transition-colors border-b border-gray-100 last:border-b-0"
83
+ @mouseenter="handleMouseEnter('{{ element.selector }}')"
84
+ @mouseleave="handleMouseLeave"
85
+ @click="handleClick(element)"
86
+ >
87
+ <div class="flex items-start gap-3 flex-1 min-w-0">
88
+ <span class="flex-shrink-0 w-6 h-6 flex items-center justify-center text-xs font-semibold text-gray-500 bg-gray-100 rounded">
89
+ {{ index + 1 }}
90
+ </span>
91
+ <div class="flex flex-col gap-0.5 min-w-0">
92
+ <span class="text-[10px] font-semibold text-indigo-600 uppercase tracking-wider">
93
+ {{ element.tag }}
94
+ </span>
95
+ <span class="text-xs text-gray-700 font-mono truncate" title="{{ element.selector }}">
96
+ {{ formatSelector(element.selector) }}
97
+ </span>
98
+ @if(element.text)
99
+ <span class="text-xs text-gray-400 truncate max-w-[200px]">
100
+ "{{ element.text }}"
101
+ </span>
102
+ @endif
103
+ </div>
104
+ </div>
105
+ <div class="flex items-center gap-2 flex-shrink-0 w-28">
106
+ <span class="font-semibold text-gray-900 w-10 text-right">{{ formatCount(element.count) }}</span>
107
+ <div class="flex-1 h-1.5 bg-gray-200 rounded-full overflow-hidden">
108
+ <div
109
+ class="h-full bg-indigo-600 rounded-full transition-all duration-300"
110
+ style="width: {{ getPercentage(element.count) }}%"
111
+ />
112
+ </div>
113
+ <span class="text-xs text-gray-500 w-8 text-right">{{ getPercentage(element.count) }}%</span>
114
+ </div>
115
+ </div>
116
+ @endeach
117
+ </div>
118
+
119
+ @if(elements.length > maxItems)
120
+ <div class="py-3 text-center text-xs text-gray-500 bg-gray-50 border-t border-gray-200">
121
+ {{ elements.length - maxItems }} more elements
122
+ </div>
123
+ @endif
124
+ @endif
125
+ </div>
@@ -0,0 +1,125 @@
1
+ <script>
2
+ /**
3
+ * HeatmapControls Component
4
+ *
5
+ * Filter controls for heatmap visualization: date range, device type, view mode.
6
+ */
7
+ export const viewMode = $props.viewMode || 'click'
8
+ export const deviceFilter = $props.deviceFilter || 'all'
9
+ export const startDate = $props.startDate || ''
10
+ export const endDate = $props.endDate || ''
11
+ export const loading = $props.loading || false
12
+ export const onViewModeChange = $props.onViewModeChange
13
+ export const onDeviceChange = $props.onDeviceChange
14
+ export const onDateRangeChange = $props.onDateRangeChange
15
+ export const onRefresh = $props.onRefresh
16
+
17
+ const viewModes = [
18
+ { value: 'click', label: 'Clicks' },
19
+ { value: 'scroll', label: 'Scroll' },
20
+ { value: 'movement', label: 'Movement' },
21
+ ]
22
+
23
+ const deviceFilters = [
24
+ { value: 'all', label: 'All Devices' },
25
+ { value: 'desktop', label: 'Desktop' },
26
+ { value: 'mobile', label: 'Mobile' },
27
+ { value: 'tablet', label: 'Tablet' },
28
+ ]
29
+
30
+ export function handleViewMode(mode) {
31
+ onViewModeChange?.(mode)
32
+ }
33
+
34
+ export function handleDevice(e) {
35
+ onDeviceChange?.(e.target.value)
36
+ }
37
+
38
+ export function handleDateChange(type, e) {
39
+ const value = e.target.value
40
+ if (type === 'start') {
41
+ onDateRangeChange?.({ start: new Date(value), end: new Date(endDate) })
42
+ } else {
43
+ onDateRangeChange?.({ start: new Date(startDate), end: new Date(value) })
44
+ }
45
+ }
46
+
47
+ export function handleRefresh() {
48
+ onRefresh?.()
49
+ }
50
+ </script>
51
+
52
+ <div class="heatmap-controls flex flex-wrap items-end gap-4 p-4 bg-white rounded-lg border border-gray-200">
53
+ <!-- View Mode Toggle -->
54
+ <div class="flex flex-col gap-1">
55
+ <label class="text-xs font-medium text-gray-500 uppercase tracking-wider">View</label>
56
+ <div class="flex gap-1 bg-gray-100 p-1 rounded-lg">
57
+ @each(mode in viewModes)
58
+ <button
59
+ type="button"
60
+ class="flex items-center gap-1.5 px-3 py-2 text-sm rounded-md transition-all {{ viewMode === mode.value ? 'text-indigo-600 bg-white shadow-sm' : 'text-gray-500 hover:text-gray-700' }}"
61
+ @if(loading)disabled@endif
62
+ @click="handleViewMode('{{ mode.value }}')"
63
+ >
64
+ @if(mode.value === 'click')
65
+ <Icon name="mouse-pointer" size="16" />
66
+ @elseif(mode.value === 'scroll')
67
+ <Icon name="arrow-up-down" size="16" />
68
+ @else
69
+ <Icon name="trending-up" size="16" />
70
+ @endif
71
+ <span>{{ mode.label }}</span>
72
+ </button>
73
+ @endeach
74
+ </div>
75
+ </div>
76
+
77
+ <!-- Device Filter -->
78
+ <div class="flex flex-col gap-1">
79
+ <label class="text-xs font-medium text-gray-500 uppercase tracking-wider" for="device-filter">Device</label>
80
+ <select
81
+ id="device-filter"
82
+ class="px-3 py-2 text-sm border border-gray-300 rounded-md bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
83
+ @if(loading)disabled@endif
84
+ @change="handleDevice"
85
+ >
86
+ @each(device in deviceFilters)
87
+ <option value="{{ device.value }}" @if(deviceFilter === device.value)selected@endif>
88
+ {{ device.label }}
89
+ </option>
90
+ @endeach
91
+ </select>
92
+ </div>
93
+
94
+ <!-- Date Range -->
95
+ <div class="flex flex-col gap-1">
96
+ <label class="text-xs font-medium text-gray-500 uppercase tracking-wider">Date Range</label>
97
+ <div class="flex items-center gap-2">
98
+ <input
99
+ type="date"
100
+ value="{{ startDate }}"
101
+ class="px-3 py-2 text-sm border border-gray-300 rounded-md bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500"
102
+ @if(loading)disabled@endif
103
+ @change="handleDateChange('start', event)"
104
+ />
105
+ <span class="text-sm text-gray-400">to</span>
106
+ <input
107
+ type="date"
108
+ value="{{ endDate }}"
109
+ class="px-3 py-2 text-sm border border-gray-300 rounded-md bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500"
110
+ @if(loading)disabled@endif
111
+ @change="handleDateChange('end', event)"
112
+ />
113
+ </div>
114
+ </div>
115
+
116
+ <!-- Refresh Button -->
117
+ <button
118
+ type="button"
119
+ class="flex items-center justify-center w-10 h-10 text-gray-500 bg-gray-100 rounded-lg hover:bg-gray-200 hover:text-gray-700 transition-all"
120
+ @if(loading)disabled@endif
121
+ @click="handleRefresh"
122
+ >
123
+ <Icon name="refresh-cw" size="16" class="{{ loading ? 'animate-spin' : '' }}" />
124
+ </button>
125
+ </div>
@@ -0,0 +1,69 @@
1
+ <script>
2
+ /**
3
+ * HeatmapLegend Component
4
+ *
5
+ * Displays a color scale legend for heatmap visualization.
6
+ */
7
+ export const colorStart = $props.colorStart || 'rgba(79, 70, 229, 0.1)'
8
+ export const colorEnd = $props.colorEnd || 'rgba(79, 70, 229, 1)'
9
+ export const minLabel = $props.minLabel || 'Less'
10
+ export const maxLabel = $props.maxLabel || 'More'
11
+ export const showValues = $props.showValues || false
12
+ export const minValue = $props.minValue || 0
13
+ export const maxValue = $props.maxValue || 100
14
+
15
+ // Generate gradient stops
16
+ const gradientStops = [0, 0.25, 0.5, 0.75, 1]
17
+
18
+ function interpolateColor(ratio) {
19
+ const startMatch = colorStart.match(/[\d.]+/g)
20
+ const endMatch = colorEnd.match(/[\d.]+/g)
21
+
22
+ if (!startMatch || !endMatch) return colorStart
23
+
24
+ const [sr, sg, sb, sa] = startMatch.map(Number)
25
+ const [er, eg, eb, ea] = endMatch.map(Number)
26
+
27
+ const r = Math.round(sr + (er - sr) * ratio)
28
+ const g = Math.round(sg + (eg - sg) * ratio)
29
+ const b = Math.round(sb + (eb - sb) * ratio)
30
+ const a = (sa + (ea - sa) * ratio).toFixed(2)
31
+
32
+ return `rgba(${r}, ${g}, ${b}, ${a})`
33
+ }
34
+
35
+ function formatValue(ratio) {
36
+ const value = minValue + (maxValue - minValue) * ratio
37
+ if (value >= 1000) {
38
+ return `${(value / 1000).toFixed(1)}k`
39
+ }
40
+ return Math.round(value).toString()
41
+ }
42
+
43
+ export const stops = gradientStops.map(stop => ({
44
+ ratio: stop,
45
+ color: interpolateColor(stop),
46
+ value: formatValue(stop)
47
+ }))
48
+ </script>
49
+
50
+ <div class="heatmap-legend flex items-center gap-2 text-xs">
51
+ <span class="text-gray-500">{{ minLabel }}</span>
52
+ <div class="flex gap-0.5">
53
+ @each(stop in stops)
54
+ <div
55
+ class="w-4 h-4 rounded-sm"
56
+ style="background-color: {{ stop.color }}"
57
+ @if(showValues)title="{{ stop.value }}"@endif
58
+ />
59
+ @endeach
60
+ </div>
61
+ <span class="text-gray-500">{{ maxLabel }}</span>
62
+
63
+ @if(showValues)
64
+ <div class="flex justify-between mt-1 text-[10px] text-gray-400">
65
+ <span>{{ formatValue(0) }}</span>
66
+ <span>{{ formatValue(1) }}</span>
67
+ </div>
68
+ @endif
69
+ </div>