@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,127 @@
1
+ {{-- PagesTable Component --}}
2
+ {{-- Props: pages (array of {path, entries, visitors, views}) --}}
3
+ <div class="data-panel">
4
+ <div class="panel-header">
5
+ <span class="panel-title">Pages</span>
6
+ </div>
7
+ <table class="data-table">
8
+ <thead>
9
+ <tr>
10
+ <th>Path</th>
11
+ <th class="text-right">Entries</th>
12
+ <th class="text-right">Visitors</th>
13
+ <th class="text-right">Views</th>
14
+ </tr>
15
+ </thead>
16
+ <tbody>
17
+ @forelse(pages as page)
18
+ <tr>
19
+ <td class="name" title="{{ page.path }}">{{ page.path }}</td>
20
+ <td class="value">{{ page.entries | fmt }}</td>
21
+ <td class="value">{{ page.visitors | fmt }}</td>
22
+ <td class="value">{{ page.views | fmt }}</td>
23
+ </tr>
24
+ @empty
25
+ <tr>
26
+ <td colspan="4" class="empty-cell">No page data available</td>
27
+ </tr>
28
+ @endforelse
29
+ </tbody>
30
+ </table>
31
+ @if(pages.length >= 10)
32
+ <div class="panel-footer">
33
+ <button class="view-more-btn" type="button">View all pages</button>
34
+ </div>
35
+ @endif
36
+ </div>
37
+
38
+
39
+ <style>
40
+ .data-panel {
41
+ background: var(--bg-secondary);
42
+ border-radius: 8px;
43
+ padding: 1rem;
44
+ }
45
+
46
+ .panel-header {
47
+ display: flex;
48
+ align-items: center;
49
+ gap: 0.5rem;
50
+ margin-bottom: 1rem;
51
+ }
52
+
53
+ .panel-title {
54
+ font-size: 0.875rem;
55
+ font-weight: 500;
56
+ color: var(--text-primary);
57
+ }
58
+
59
+ .data-table {
60
+ width: 100%;
61
+ font-size: 0.8125rem;
62
+ border-collapse: collapse;
63
+ }
64
+
65
+ .data-table th {
66
+ text-align: left;
67
+ color: var(--text-muted);
68
+ font-weight: 500;
69
+ padding: 0.5rem 0;
70
+ border-bottom: 1px solid var(--border);
71
+ font-size: 0.6875rem;
72
+ text-transform: uppercase;
73
+ letter-spacing: 0.05em;
74
+ }
75
+
76
+ .data-table th.text-right,
77
+ .data-table td.value {
78
+ text-align: right;
79
+ }
80
+
81
+ .data-table td {
82
+ padding: 0.625rem 0;
83
+ border-bottom: 1px solid var(--bg-tertiary);
84
+ }
85
+
86
+ .data-table tr:last-child td {
87
+ border-bottom: none;
88
+ }
89
+
90
+ .data-table .name {
91
+ color: var(--text-primary);
92
+ max-width: 180px;
93
+ overflow: hidden;
94
+ text-overflow: ellipsis;
95
+ white-space: nowrap;
96
+ }
97
+
98
+ .data-table .value {
99
+ color: var(--text-secondary);
100
+ }
101
+
102
+ .empty-cell {
103
+ text-align: center;
104
+ color: var(--text-muted);
105
+ padding: 1.5rem 0;
106
+ }
107
+
108
+ .panel-footer {
109
+ margin-top: 1rem;
110
+ padding-top: 1rem;
111
+ border-top: 1px solid var(--border);
112
+ text-align: center;
113
+ }
114
+
115
+ .view-more-btn {
116
+ background: transparent;
117
+ border: none;
118
+ color: var(--accent);
119
+ font-size: 0.75rem;
120
+ cursor: pointer;
121
+ padding: 0.25rem 0.5rem;
122
+ }
123
+
124
+ .view-more-btn:hover {
125
+ text-decoration: underline;
126
+ }
127
+ </style>
@@ -0,0 +1,89 @@
1
+ <script server>
2
+ /**
3
+ * ProgressRing Component
4
+ *
5
+ * Circular progress indicator with optional center content.
6
+ */
7
+ import { defineProps, withDefaults } from 'stx'
8
+
9
+ interface ProgressRingProps {
10
+ value: number // 0-100
11
+ size?: number
12
+ strokeWidth?: number
13
+ color?: string
14
+ bgColor?: string
15
+ showValue?: boolean
16
+ label?: string
17
+ loading?: boolean
18
+ }
19
+
20
+ const { value, size, strokeWidth, color, bgColor, showValue, label, loading } = withDefaults(
21
+ defineProps<ProgressRingProps>(),
22
+ {
23
+ size: 120,
24
+ strokeWidth: 8,
25
+ color: '#6366f1',
26
+ bgColor: '#e5e7eb',
27
+ showValue: true,
28
+ loading: false,
29
+ }
30
+ )
31
+
32
+ const radius = (size - strokeWidth) / 2
33
+ const circumference = 2 * Math.PI * radius
34
+ const progress = Math.min(Math.max(value, 0), 100)
35
+ const offset = circumference - (progress / 100) * circumference
36
+ const center = size / 2
37
+ </script>
38
+
39
+ <div
40
+ class="progress-ring relative inline-flex items-center justify-center"
41
+ style="width: {{ size }}px; height: {{ size }}px"
42
+ >
43
+ @if (loading)
44
+ <div class="absolute inset-0 animate-pulse">
45
+ <div class="w-full h-full rounded-full bg-gray-200"></div>
46
+ </div>
47
+ @else
48
+ <svg
49
+ width="{{ size }}"
50
+ height="{{ size }}"
51
+ class="transform -rotate-90"
52
+ >
53
+ <circle
54
+ cx="{{ center }}"
55
+ cy="{{ center }}"
56
+ r="{{ radius }}"
57
+ stroke="{{ bgColor }}"
58
+ stroke-width="{{ strokeWidth }}"
59
+ fill="none"
60
+ />
61
+
62
+ <circle
63
+ cx="{{ center }}"
64
+ cy="{{ center }}"
65
+ r="{{ radius }}"
66
+ stroke="{{ color }}"
67
+ stroke-width="{{ strokeWidth }}"
68
+ stroke-dasharray="{{ circumference }}"
69
+ stroke-dashoffset="{{ offset }}"
70
+ stroke-linecap="round"
71
+ fill="none"
72
+ class="transition-all duration-500 ease-out"
73
+ />
74
+ </svg>
75
+
76
+ <div class="absolute inset-0 flex flex-col items-center justify-center">
77
+ @if (showValue)
78
+ <span class="text-2xl font-bold text-gray-900">
79
+ {{ Math.round(value) }}%
80
+ </span>
81
+ @endif
82
+ @if (label)
83
+ <span class="text-xs text-gray-500 mt-0.5">
84
+ {{ label }}
85
+ </span>
86
+ @endif
87
+ </div>
88
+ @endif
89
+ </div>
@@ -0,0 +1,45 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+ import RealtimeCounter from './RealtimeCounter.stx'
3
+
4
+ const meta = {
5
+ title: 'Components/Core/RealtimeCounter',
6
+ component: RealtimeCounter,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ count: { control: { type: 'number', min: 0, max: 1000 } },
10
+ label: { control: 'text' },
11
+ loading: { control: 'boolean' },
12
+ },
13
+ } satisfies Meta<typeof RealtimeCounter>
14
+
15
+ export default meta
16
+ type Story = StoryObj<typeof meta>
17
+
18
+ export const Default: Story = {
19
+ args: {
20
+ count: 42,
21
+ label: 'Active Visitors',
22
+ },
23
+ }
24
+
25
+ export const HighCount: Story = {
26
+ args: {
27
+ count: 847,
28
+ label: 'Current Users',
29
+ },
30
+ }
31
+
32
+ export const ZeroCount: Story = {
33
+ args: {
34
+ count: 0,
35
+ label: 'Active Sessions',
36
+ },
37
+ }
38
+
39
+ export const Loading: Story = {
40
+ args: {
41
+ count: 0,
42
+ label: 'Active Visitors',
43
+ loading: true,
44
+ },
45
+ }
@@ -0,0 +1,46 @@
1
+ <script server>
2
+ /**
3
+ * RealtimeCounter Component
4
+ *
5
+ * Displays current visitor count with pulsing indicator.
6
+ */
7
+ import { defineProps, withDefaults } from 'stx'
8
+ import type { RealtimeCounterProps } from '../types'
9
+ import { formatCompact } from '../utils'
10
+
11
+ const { count, label, pulseColor, loading } = withDefaults(
12
+ defineProps<RealtimeCounterProps>(),
13
+ {
14
+ label: 'visitors right now',
15
+ pulseColor: '#22c55e',
16
+ loading: false,
17
+ }
18
+ )
19
+
20
+ const formattedCount = formatCompact(count)
21
+ </script>
22
+
23
+ <div class="realtime-counter card-hover p-6">
24
+ <div class="flex items-center gap-3">
25
+ <div class="relative">
26
+ <div class="pulse-dot animate-pulse" style="background-color: {{ pulseColor }}"></div>
27
+ <div class="pulse-ring" style="background-color: {{ pulseColor }}"></div>
28
+ </div>
29
+
30
+ @if (loading)
31
+ <div class="animate-pulse">
32
+ <div class="skeleton h-10 w-16"></div>
33
+ <div class="skeleton h-4 w-24 mt-1"></div>
34
+ </div>
35
+ @else
36
+ <div>
37
+ <span class="text-4xl font-bold text-gray-900">
38
+ {{ formattedCount }}
39
+ </span>
40
+ <span class="ml-2 text-gray-500 text-sm">
41
+ {{ label }}
42
+ </span>
43
+ </div>
44
+ @endif
45
+ </div>
46
+ </div>
@@ -0,0 +1,180 @@
1
+ {{-- ReferrersTable Component --}}
2
+ {{-- Props: referrers, sources, activeTab --}}
3
+ <div class="data-panel">
4
+ <div class="tabs">
5
+ <button class="tab-btn @if(activeTab === 'referrers') active @endif" data-tab="referrers" type="button">
6
+ Referrers
7
+ </button>
8
+ <button class="tab-btn @if(activeTab === 'sources') active @endif" data-tab="sources" type="button">
9
+ Sources
10
+ </button>
11
+ </div>
12
+
13
+ <div class="tab-content" data-content="referrers" @if(activeTab !== 'referrers') style="display:none" @endif>
14
+ <table class="data-table">
15
+ <thead>
16
+ <tr>
17
+ <th>Referrer</th>
18
+ <th class="text-right">Visitors</th>
19
+ <th class="text-right">Views</th>
20
+ </tr>
21
+ </thead>
22
+ <tbody>
23
+ @forelse(referrers as ref)
24
+ <tr>
25
+ <td class="name" title="{{ ref.url }}">
26
+ @if(ref.favicon)
27
+ <img src="{{ ref.favicon }}" alt="" class="favicon" />
28
+ @endif
29
+ {{ ref.source }}
30
+ </td>
31
+ <td class="value">{{ ref.visitors | fmt }}</td>
32
+ <td class="value">{{ ref.views | fmt }}</td>
33
+ </tr>
34
+ @empty
35
+ <tr>
36
+ <td colspan="3" class="empty-cell">No referrer data</td>
37
+ </tr>
38
+ @endforelse
39
+ </tbody>
40
+ </table>
41
+ </div>
42
+
43
+ <div class="tab-content" data-content="sources" @if(activeTab !== 'sources') style="display:none" @endif>
44
+ <table class="data-table">
45
+ <thead>
46
+ <tr>
47
+ <th>Source</th>
48
+ <th class="text-right">Visitors</th>
49
+ <th class="text-right">Views</th>
50
+ </tr>
51
+ </thead>
52
+ <tbody>
53
+ @forelse(sources as src)
54
+ <tr>
55
+ <td class="name">{{ src.name }}</td>
56
+ <td class="value">{{ src.visitors | fmt }}</td>
57
+ <td class="value">{{ src.views | fmt }}</td>
58
+ </tr>
59
+ @empty
60
+ <tr>
61
+ <td colspan="3" class="empty-cell">No source data</td>
62
+ </tr>
63
+ @endforelse
64
+ </tbody>
65
+ </table>
66
+ </div>
67
+ </div>
68
+
69
+
70
+ <script>
71
+ document.querySelectorAll('.data-panel').forEach(panel => {
72
+ const tabs = panel.querySelectorAll('.tab-btn')
73
+ const contents = panel.querySelectorAll('.tab-content')
74
+
75
+ tabs.forEach(tab => {
76
+ tab.addEventListener('click', () => {
77
+ const targetTab = tab.dataset.tab
78
+ tabs.forEach(t => t.classList.remove('active'))
79
+ tab.classList.add('active')
80
+ contents.forEach(c => {
81
+ c.style.display = c.dataset.content === targetTab ? 'block' : 'none'
82
+ })
83
+ })
84
+ })
85
+ })
86
+ </script>
87
+
88
+ <style>
89
+ .data-panel {
90
+ background: var(--bg-secondary);
91
+ border-radius: 8px;
92
+ padding: 1rem;
93
+ }
94
+
95
+ .tabs {
96
+ display: flex;
97
+ gap: 0.25rem;
98
+ border-bottom: 1px solid var(--border);
99
+ margin-bottom: 1rem;
100
+ }
101
+
102
+ .tab-btn {
103
+ background: transparent;
104
+ border: none;
105
+ color: var(--text-secondary);
106
+ padding: 0.5rem 0.75rem;
107
+ font-size: 0.75rem;
108
+ font-weight: 500;
109
+ cursor: pointer;
110
+ border-bottom: 2px solid transparent;
111
+ transition: all 0.15s;
112
+ }
113
+
114
+ .tab-btn:hover {
115
+ color: var(--text-primary);
116
+ }
117
+
118
+ .tab-btn.active {
119
+ color: var(--accent);
120
+ border-bottom-color: var(--accent);
121
+ }
122
+
123
+ .data-table {
124
+ width: 100%;
125
+ font-size: 0.8125rem;
126
+ border-collapse: collapse;
127
+ }
128
+
129
+ .data-table th {
130
+ text-align: left;
131
+ color: var(--text-muted);
132
+ font-weight: 500;
133
+ padding: 0.5rem 0;
134
+ border-bottom: 1px solid var(--border);
135
+ font-size: 0.6875rem;
136
+ text-transform: uppercase;
137
+ letter-spacing: 0.05em;
138
+ }
139
+
140
+ .data-table th.text-right {
141
+ text-align: right;
142
+ }
143
+
144
+ .data-table td {
145
+ padding: 0.625rem 0;
146
+ border-bottom: 1px solid var(--bg-tertiary);
147
+ }
148
+
149
+ .data-table tr:last-child td {
150
+ border-bottom: none;
151
+ }
152
+
153
+ .data-table .name {
154
+ color: var(--text-primary);
155
+ max-width: 180px;
156
+ overflow: hidden;
157
+ text-overflow: ellipsis;
158
+ white-space: nowrap;
159
+ display: flex;
160
+ align-items: center;
161
+ gap: 0.5rem;
162
+ }
163
+
164
+ .favicon {
165
+ width: 16px;
166
+ height: 16px;
167
+ border-radius: 2px;
168
+ }
169
+
170
+ .data-table .value {
171
+ color: var(--text-secondary);
172
+ text-align: right;
173
+ }
174
+
175
+ .empty-cell {
176
+ text-align: center;
177
+ color: var(--text-muted);
178
+ padding: 1.5rem 0;
179
+ }
180
+ </style>
@@ -0,0 +1,160 @@
1
+ <script server>
2
+ /**
3
+ * SparklineChart Component
4
+ *
5
+ * A minimal inline sparkline chart for showing trends.
6
+ * Uses SVG for crisp rendering at any size.
7
+ */
8
+ import { defineProps, withDefaults } from 'stx'
9
+
10
+ interface SparklineChartProps {
11
+ data: number[]
12
+ width?: number
13
+ height?: number
14
+ color?: string
15
+ fillColor?: string
16
+ showArea?: boolean
17
+ strokeWidth?: number
18
+ loading?: boolean
19
+ }
20
+
21
+ const { data, width, height, color, fillColor, showArea, strokeWidth, loading } = withDefaults(
22
+ defineProps<SparklineChartProps>(),
23
+ {
24
+ width: 100,
25
+ height: 32,
26
+ color: '#6366f1',
27
+ fillColor: '#6366f120',
28
+ showArea: true,
29
+ strokeWidth: 2,
30
+ loading: false,
31
+ }
32
+ )
33
+
34
+ function computePathData(): string {
35
+ if (!data.length)
36
+ return ''
37
+
38
+ const min = Math.min(...data)
39
+ const max = Math.max(...data)
40
+ const range = max - min || 1
41
+
42
+ const padding = strokeWidth
43
+ const chartWidth = width - padding * 2
44
+ const chartHeight = height - padding * 2
45
+
46
+ const points = data.map((value, index) => {
47
+ const x = padding + (index / (data.length - 1)) * chartWidth
48
+ const y = padding + chartHeight - ((value - min) / range) * chartHeight
49
+ return `${x},${y}`
50
+ })
51
+
52
+ return `M${points.join(' L')}`
53
+ }
54
+
55
+ function computeAreaPath(): string {
56
+ if (!data.length || !showArea)
57
+ return ''
58
+
59
+ const min = Math.min(...data)
60
+ const max = Math.max(...data)
61
+ const range = max - min || 1
62
+
63
+ const padding = strokeWidth
64
+ const chartWidth = width - padding * 2
65
+ const chartHeight = height - padding * 2
66
+
67
+ const points = data.map((value, index) => {
68
+ const x = padding + (index / (data.length - 1)) * chartWidth
69
+ const y = padding + chartHeight - ((value - min) / range) * chartHeight
70
+ return `${x},${y}`
71
+ })
72
+
73
+ const firstX = padding
74
+ const lastX = padding + chartWidth
75
+ const bottomY = padding + chartHeight
76
+
77
+ return `M${firstX},${bottomY} L${points.join(' L')} L${lastX},${bottomY} Z`
78
+ }
79
+
80
+ function computeTrend(): number {
81
+ if (data.length < 2)
82
+ return 0
83
+
84
+ const first = data[0]
85
+ const last = data[data.length - 1]
86
+
87
+ if (first === 0)
88
+ return last > 0 ? 100 : 0
89
+
90
+ return ((last - first) / first) * 100
91
+ }
92
+
93
+ function getTrendColor(trend: number): string {
94
+ if (trend > 0)
95
+ return '#22c55e'
96
+ if (trend < 0)
97
+ return '#ef4444'
98
+ return '#6b7280'
99
+ }
100
+
101
+ const pathData = computePathData()
102
+ const areaPath = computeAreaPath()
103
+ const trend = computeTrend()
104
+ const trendColor = getTrendColor(trend)
105
+ </script>
106
+
107
+ <div class="sparkline-chart inline-flex items-center gap-2">
108
+ @if (loading)
109
+ <div class="animate-pulse">
110
+ <div class="skeleton" style="width: {{ width }}px; height: {{ height }}px"></div>
111
+ </div>
112
+ @elseif (!data.length)
113
+ <div
114
+ class="flex items-center justify-center text-gray-300"
115
+ style="width: {{ width }}px; height: {{ height }}px"
116
+ >
117
+ <Icon name="trending-up" size="16" />
118
+ </div>
119
+ @else
120
+ <svg
121
+ width="{{ width }}"
122
+ height="{{ height }}"
123
+ class="overflow-visible"
124
+ >
125
+ @if (showArea && areaPath)
126
+ <path
127
+ d="{{ areaPath }}"
128
+ fill="{{ fillColor }}"
129
+ />
130
+ @endif
131
+
132
+ <path
133
+ d="{{ pathData }}"
134
+ stroke="{{ color }}"
135
+ stroke-width="{{ strokeWidth }}"
136
+ fill="none"
137
+ stroke-linecap="round"
138
+ stroke-linejoin="round"
139
+ />
140
+
141
+ @if (data.length)
142
+ <circle
143
+ cx="{{ width - strokeWidth }}"
144
+ cy="{{ height / 2 }}"
145
+ r="{{ strokeWidth }}"
146
+ fill="{{ color }}"
147
+ />
148
+ @endif
149
+ </svg>
150
+
151
+ @if (data.length >= 2)
152
+ <span
153
+ class="text-xs font-medium"
154
+ style="color: {{ trendColor }}"
155
+ >
156
+ {{ trend >= 0 ? '+' : '' }}{{ trend.toFixed(1) }}%
157
+ </span>
158
+ @endif
159
+ @endif
160
+ </div>
@@ -0,0 +1,58 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+ import StatCard from './StatCard.stx'
3
+
4
+ const meta = {
5
+ title: 'Components/Core/StatCard',
6
+ component: StatCard,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ label: { control: 'text' },
10
+ value: { control: 'text' },
11
+ change: { control: { type: 'number', min: -1, max: 1, step: 0.01 } },
12
+ icon: { control: 'text' },
13
+ loading: { control: 'boolean' },
14
+ },
15
+ } satisfies Meta<typeof StatCard>
16
+
17
+ export default meta
18
+ type Story = StoryObj<typeof meta>
19
+
20
+ export const Default: Story = {
21
+ args: {
22
+ label: 'Total Visitors',
23
+ value: '24,847',
24
+ },
25
+ }
26
+
27
+ export const WithChange: Story = {
28
+ args: {
29
+ label: 'Page Views',
30
+ value: '89,234',
31
+ change: 0.12,
32
+ },
33
+ }
34
+
35
+ export const NegativeChange: Story = {
36
+ args: {
37
+ label: 'Bounce Rate',
38
+ value: '42.3%',
39
+ change: -0.05,
40
+ },
41
+ }
42
+
43
+ export const Loading: Story = {
44
+ args: {
45
+ label: 'Total Visitors',
46
+ value: '24,847',
47
+ loading: true,
48
+ },
49
+ }
50
+
51
+ export const WithIcon: Story = {
52
+ args: {
53
+ label: 'Active Users',
54
+ value: '1,234',
55
+ change: 0.08,
56
+ icon: 'M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z',
57
+ },
58
+ }