@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,94 @@
1
+ <script server>
2
+ /**
3
+ * DeviceBreakdown Component
4
+ *
5
+ * Displays device type breakdown with icons and percentages.
6
+ */
7
+ import { defineProps, withDefaults } from 'stx'
8
+ import type { TopItem } from '../types'
9
+ import { formatPercentage } from '../utils'
10
+
11
+ interface DeviceBreakdownProps {
12
+ devices: TopItem[]
13
+ loading?: boolean
14
+ }
15
+
16
+ const { devices, loading } = withDefaults(
17
+ defineProps<DeviceBreakdownProps>(),
18
+ {
19
+ loading: false,
20
+ }
21
+ )
22
+
23
+ const deviceIcons: Record<string, string> = {
24
+ desktop: `<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /></svg>`,
25
+ mobile: `<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" /></svg>`,
26
+ tablet: `<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z" /></svg>`,
27
+ unknown: `<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" /></svg>`,
28
+ }
29
+
30
+ const deviceColors: Record<string, { bg: string, fill: string }> = {
31
+ desktop: { bg: 'bg-blue-100 text-blue-600', fill: 'bg-blue-500' },
32
+ mobile: { bg: 'bg-green-100 text-green-600', fill: 'bg-green-500' },
33
+ tablet: { bg: 'bg-purple-100 text-purple-600', fill: 'bg-purple-500' },
34
+ unknown: { bg: 'bg-gray-100 text-gray-600', fill: 'bg-gray-500' },
35
+ }
36
+
37
+ const sortedDevices = [...devices].sort((a, b) => b.value - a.value)
38
+
39
+ function getDeviceIcon(name: string): string {
40
+ const key = name.toLowerCase()
41
+ return deviceIcons[key] || deviceIcons.unknown
42
+ }
43
+
44
+ function getDeviceColor(name: string): { bg: string, fill: string } {
45
+ const key = name.toLowerCase()
46
+ return deviceColors[key] || deviceColors.unknown
47
+ }
48
+ </script>
49
+
50
+ <div class="device-breakdown card-hover p-6">
51
+ <h3 class="text-sm font-medium text-gray-900 mb-4">
52
+ Devices
53
+ </h3>
54
+
55
+ @if (loading)
56
+ <div class="space-y-4">
57
+ @foreach ([1, 2, 3] as i)
58
+ <div class="animate-pulse flex items-center gap-4">
59
+ <div class="skeleton w-12 h-12 rounded-lg"></div>
60
+ <div class="flex-1">
61
+ <div class="skeleton h-4 w-20 mb-2"></div>
62
+ <div class="skeleton h-3 w-12"></div>
63
+ </div>
64
+ </div>
65
+ @endforeach
66
+ </div>
67
+ @elseif (!devices.length)
68
+ <div class="text-center py-8 text-gray-500">
69
+ No device data available
70
+ </div>
71
+ @else
72
+ <div class="space-y-4">
73
+ @foreach (sortedDevices as device)
74
+ <div class="flex items-center gap-4">
75
+ <div class="w-12 h-12 rounded-lg flex items-center justify-center {{ getDeviceColor(device.name).bg }}">
76
+ {!! getDeviceIcon(device.name) !!}
77
+ </div>
78
+ <div class="flex-1">
79
+ <div class="flex items-center justify-between">
80
+ <span class="font-medium text-gray-900 capitalize">{{ device.name }}</span>
81
+ <span class="text-sm text-gray-600">{{ formatPercentage(device.percentage) }}</span>
82
+ </div>
83
+ <div class="progress-bar mt-1">
84
+ <div
85
+ class="progress-fill {{ getDeviceColor(device.name).fill }}"
86
+ style="width: {{ device.percentage * 100 }}%"
87
+ ></div>
88
+ </div>
89
+ </div>
90
+ </div>
91
+ @endforeach
92
+ </div>
93
+ @endif
94
+ </div>
@@ -0,0 +1,163 @@
1
+ {{-- DevicesTable Component --}}
2
+ {{-- Props: deviceTypes, operatingSystems, activeTab --}}
3
+ <div class="data-panel">
4
+ <div class="tabs">
5
+ <button class="tab-btn @if(activeTab === 'types') active @endif" data-tab="types" type="button">
6
+ Device Types
7
+ </button>
8
+ <button class="tab-btn @if(activeTab === 'os') active @endif" data-tab="os" type="button">
9
+ OS
10
+ </button>
11
+ </div>
12
+
13
+ <div class="tab-content" data-content="types" @if(activeTab !== 'types') style="display:none" @endif>
14
+ <table class="data-table">
15
+ <thead>
16
+ <tr>
17
+ <th>Type</th>
18
+ <th class="text-right">Visitors</th>
19
+ </tr>
20
+ </thead>
21
+ <tbody>
22
+ @forelse(deviceTypes as device)
23
+ <tr>
24
+ <td class="name">
25
+ @switch(device.type)
26
+ @case('desktop')
27
+ <Icon name="monitor" size="16" class="device-icon" />
28
+ @break
29
+ @case('mobile')
30
+ <Icon name="smartphone" size="16" class="device-icon" />
31
+ @break
32
+ @case('tablet')
33
+ <Icon name="tablet" size="16" class="device-icon" />
34
+ @break
35
+ @default
36
+ <Icon name="help-circle" size="16" class="device-icon" />
37
+ @endswitch
38
+ {{ device.type }}
39
+ </td>
40
+ <td class="value">{{ device.visitors | fmt }}</td>
41
+ </tr>
42
+ @empty
43
+ <tr>
44
+ <td colspan="2" class="empty-cell">No device data</td>
45
+ </tr>
46
+ @endforelse
47
+ </tbody>
48
+ </table>
49
+ </div>
50
+
51
+ <div class="tab-content" data-content="os" @if(activeTab !== 'os') style="display:none" @endif>
52
+ <table class="data-table">
53
+ <thead>
54
+ <tr>
55
+ <th>Operating System</th>
56
+ <th class="text-right">Visitors</th>
57
+ </tr>
58
+ </thead>
59
+ <tbody>
60
+ @forelse(operatingSystems as os)
61
+ <tr>
62
+ <td class="name">{{ os.name }}</td>
63
+ <td class="value">{{ os.visitors | fmt }}</td>
64
+ </tr>
65
+ @empty
66
+ <tr>
67
+ <td colspan="2" class="empty-cell">No OS data</td>
68
+ </tr>
69
+ @endforelse
70
+ </tbody>
71
+ </table>
72
+ </div>
73
+ </div>
74
+
75
+
76
+ <style>
77
+ .data-panel {
78
+ background: var(--bg-secondary);
79
+ border-radius: 8px;
80
+ padding: 1rem;
81
+ }
82
+
83
+ .tabs {
84
+ display: flex;
85
+ gap: 0.25rem;
86
+ border-bottom: 1px solid var(--border);
87
+ margin-bottom: 1rem;
88
+ }
89
+
90
+ .tab-btn {
91
+ background: transparent;
92
+ border: none;
93
+ color: var(--text-secondary);
94
+ padding: 0.5rem 0.75rem;
95
+ font-size: 0.75rem;
96
+ font-weight: 500;
97
+ cursor: pointer;
98
+ border-bottom: 2px solid transparent;
99
+ transition: all 0.15s;
100
+ }
101
+
102
+ .tab-btn:hover {
103
+ color: var(--text-primary);
104
+ }
105
+
106
+ .tab-btn.active {
107
+ color: var(--accent);
108
+ border-bottom-color: var(--accent);
109
+ }
110
+
111
+ .data-table {
112
+ width: 100%;
113
+ font-size: 0.8125rem;
114
+ border-collapse: collapse;
115
+ }
116
+
117
+ .data-table th {
118
+ text-align: left;
119
+ color: var(--text-muted);
120
+ font-weight: 500;
121
+ padding: 0.5rem 0;
122
+ border-bottom: 1px solid var(--border);
123
+ font-size: 0.6875rem;
124
+ text-transform: uppercase;
125
+ letter-spacing: 0.05em;
126
+ }
127
+
128
+ .data-table th.text-right {
129
+ text-align: right;
130
+ }
131
+
132
+ .data-table td {
133
+ padding: 0.625rem 0;
134
+ border-bottom: 1px solid var(--bg-tertiary);
135
+ }
136
+
137
+ .data-table tr:last-child td {
138
+ border-bottom: none;
139
+ }
140
+
141
+ .data-table .name {
142
+ color: var(--text-primary);
143
+ display: flex;
144
+ align-items: center;
145
+ gap: 0.5rem;
146
+ text-transform: capitalize;
147
+ }
148
+
149
+ .device-icon {
150
+ color: var(--text-muted);
151
+ }
152
+
153
+ .data-table .value {
154
+ color: var(--text-secondary);
155
+ text-align: right;
156
+ }
157
+
158
+ .empty-cell {
159
+ text-align: center;
160
+ color: var(--text-muted);
161
+ padding: 1.5rem 0;
162
+ }
163
+ </style>
@@ -0,0 +1,55 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+ import DonutChart from './DonutChart.stx'
3
+
4
+ const meta = {
5
+ title: 'Components/Charts/DonutChart',
6
+ component: DonutChart,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ title: { control: 'text' },
10
+ loading: { control: 'boolean' },
11
+ showLegend: { control: 'boolean' },
12
+ },
13
+ } satisfies Meta<typeof DonutChart>
14
+
15
+ export default meta
16
+ type Story = StoryObj<typeof meta>
17
+
18
+ const sampleData = [
19
+ { label: 'Direct', value: 4234, color: '#3b82f6' },
20
+ { label: 'Organic Search', value: 3456, color: '#10b981' },
21
+ { label: 'Social', value: 2134, color: '#8b5cf6' },
22
+ { label: 'Referral', value: 1234, color: '#f59e0b' },
23
+ { label: 'Email', value: 567, color: '#ef4444' },
24
+ ]
25
+
26
+ export const Default: Story = {
27
+ args: {
28
+ data: sampleData,
29
+ title: 'Traffic Sources',
30
+ },
31
+ }
32
+
33
+ export const WithoutTitle: Story = {
34
+ args: {
35
+ data: sampleData,
36
+ },
37
+ }
38
+
39
+ export const Loading: Story = {
40
+ args: {
41
+ data: [],
42
+ title: 'Traffic Sources',
43
+ loading: true,
44
+ },
45
+ }
46
+
47
+ export const TwoSegments: Story = {
48
+ args: {
49
+ data: [
50
+ { label: 'Desktop', value: 6500, color: '#3b82f6' },
51
+ { label: 'Mobile', value: 3500, color: '#10b981' },
52
+ ],
53
+ title: 'Device Split',
54
+ },
55
+ }
@@ -0,0 +1,157 @@
1
+ <script server>
2
+ /**
3
+ * DonutChart Component
4
+ *
5
+ * Displays data as a donut/pie chart with legend.
6
+ * Uses Canvas for rendering - fast and lightweight.
7
+ */
8
+ import { defineProps, withDefaults } from 'stx'
9
+ import type { DashboardTheme } from '../types'
10
+ import { defaultTheme } from '../types'
11
+ import { formatCompact, formatPercentage } from '../utils'
12
+
13
+ export interface DonutChartItem {
14
+ name: string
15
+ value: number
16
+ color?: string
17
+ }
18
+
19
+ interface DonutChartProps {
20
+ items: DonutChartItem[]
21
+ title?: string
22
+ size?: number
23
+ thickness?: number
24
+ loading?: boolean
25
+ showLegend?: boolean
26
+ showTotal?: boolean
27
+ totalLabel?: string
28
+ theme?: DashboardTheme
29
+ }
30
+
31
+ const { items, title, size, thickness, loading, showLegend, showTotal, totalLabel, theme } = withDefaults(
32
+ defineProps<DonutChartProps>(),
33
+ {
34
+ size: 180,
35
+ thickness: 24,
36
+ loading: false,
37
+ showLegend: true,
38
+ showTotal: true,
39
+ totalLabel: 'Total',
40
+ theme: () => defaultTheme,
41
+ }
42
+ )
43
+
44
+ const defaultColors = [
45
+ '#6366f1', // indigo
46
+ '#22c55e', // green
47
+ '#f59e0b', // amber
48
+ '#ef4444', // red
49
+ '#8b5cf6', // purple
50
+ '#06b6d4', // cyan
51
+ '#ec4899', // pink
52
+ '#14b8a6', // teal
53
+ ]
54
+
55
+ const total = items.reduce((sum, item) => sum + item.value, 0)
56
+
57
+ const itemsWithColors = items.map((item, index) => ({
58
+ ...item,
59
+ color: item.color || defaultColors[index % defaultColors.length],
60
+ percentage: total > 0 ? item.value / total : 0,
61
+ }))
62
+
63
+ const itemsJson = JSON.stringify(itemsWithColors)
64
+ </script>
65
+
66
+ <div class="donut-chart card-hover p-6">
67
+ @if (title)
68
+ <h3 class="text-sm font-medium text-gray-900 mb-4">
69
+ {{ title }}
70
+ </h3>
71
+ @endif
72
+
73
+ @if (loading)
74
+ <div class="animate-pulse flex items-center gap-6">
75
+ <div class="skeleton rounded-full" style="width: {{ size }}px; height: {{ size }}px"></div>
76
+ <div class="flex-1 space-y-2">
77
+ @foreach ([1, 2, 3, 4] as i)
78
+ <div class="skeleton h-4 w-24"></div>
79
+ @endforeach
80
+ </div>
81
+ </div>
82
+ @elseif (!items.length)
83
+ <div class="text-center py-8 text-gray-500">
84
+ No data available
85
+ </div>
86
+ @else
87
+ <div class="flex items-center gap-6">
88
+ <div class="relative" style="width: {{ size }}px; height: {{ size }}px">
89
+ <script client>
90
+ onMount(() => {
91
+ const canvas = document.getElementById('donutChartCanvas')
92
+ const ctx = canvas?.getContext('2d')
93
+ if (!ctx) return
94
+
95
+ const donutItems = {{ itemsJson }}
96
+ const donutSize = {{ size }}
97
+ const donutThickness = {{ thickness }}
98
+
99
+ const dpr = window.devicePixelRatio || 1
100
+ canvas.width = donutSize * dpr
101
+ canvas.height = donutSize * dpr
102
+ ctx.scale(dpr, dpr)
103
+ ctx.clearRect(0, 0, donutSize, donutSize)
104
+
105
+ const centerX = donutSize / 2
106
+ const centerY = donutSize / 2
107
+ const radius = (donutSize - donutThickness) / 2
108
+ const innerRadius = radius - donutThickness
109
+
110
+ let startAngle = -Math.PI / 2
111
+
112
+ for (const item of donutItems) {
113
+ const sliceAngle = item.percentage * 2 * Math.PI
114
+
115
+ ctx.beginPath()
116
+ ctx.arc(centerX, centerY, radius, startAngle, startAngle + sliceAngle)
117
+ ctx.arc(centerX, centerY, innerRadius, startAngle + sliceAngle, startAngle, true)
118
+ ctx.closePath()
119
+
120
+ ctx.fillStyle = item.color
121
+ ctx.fill()
122
+
123
+ startAngle += sliceAngle
124
+ }
125
+ })
126
+ </script>
127
+ <canvas
128
+ id="donutChartCanvas"
129
+ style="width: {{ size }}px; height: {{ size }}px"
130
+ ></canvas>
131
+ @if (showTotal)
132
+ <div class="absolute inset-0 flex flex-col items-center justify-center">
133
+ <span class="text-2xl font-bold text-gray-900">{{ formatCompact(total) }}</span>
134
+ <span class="text-xs text-gray-500">{{ totalLabel }}</span>
135
+ </div>
136
+ @endif
137
+ </div>
138
+
139
+ @if (showLegend)
140
+ <div class="flex-1 space-y-2">
141
+ @foreach (itemsWithColors as item)
142
+ <div class="flex items-center justify-between text-sm">
143
+ <div class="flex items-center gap-2">
144
+ <div class="w-3 h-3 rounded-full" style="background-color: {{ item.color }}"></div>
145
+ <span class="text-gray-700">{{ item.name }}</span>
146
+ </div>
147
+ <div class="text-gray-500 tabular-nums">
148
+ {{ formatCompact(item.value) }}
149
+ <span class="text-gray-400 ml-1">({{ formatPercentage(item.percentage) }})</span>
150
+ </div>
151
+ </div>
152
+ @endforeach
153
+ </div>
154
+ @endif
155
+ </div>
156
+ @endif
157
+ </div>
@@ -0,0 +1,90 @@
1
+ <script server>
2
+ /**
3
+ * EmptyState Component
4
+ *
5
+ * Placeholder for empty data states with customizable icon and actions.
6
+ */
7
+ import { defineProps, withDefaults } from 'stx'
8
+
9
+ export type EmptyStateVariant = 'default' | 'no-data' | 'no-results' | 'error' | 'no-access'
10
+
11
+ interface EmptyStateProps {
12
+ title?: string
13
+ description?: string
14
+ variant?: EmptyStateVariant
15
+ actionLabel?: string
16
+ showIcon?: boolean
17
+ }
18
+
19
+ const { title, description, variant, actionLabel, showIcon } = withDefaults(
20
+ defineProps<EmptyStateProps>(),
21
+ {
22
+ title: 'No data',
23
+ description: 'There is no data to display at this time.',
24
+ variant: 'default',
25
+ showIcon: true,
26
+ }
27
+ )
28
+
29
+ const variantConfig: Record<EmptyStateVariant, { icon: string, defaultTitle: string, defaultDesc: string }> = {
30
+ 'default': {
31
+ icon: 'M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4',
32
+ defaultTitle: 'No data',
33
+ defaultDesc: 'There is no data to display at this time.',
34
+ },
35
+ 'no-data': {
36
+ icon: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z',
37
+ defaultTitle: 'No analytics data',
38
+ defaultDesc: 'Start tracking your website to see analytics data here.',
39
+ },
40
+ 'no-results': {
41
+ icon: 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z',
42
+ defaultTitle: 'No results found',
43
+ defaultDesc: 'Try adjusting your search or filter criteria.',
44
+ },
45
+ 'error': {
46
+ icon: 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z',
47
+ defaultTitle: 'Something went wrong',
48
+ defaultDesc: 'We encountered an error loading this data. Please try again.',
49
+ },
50
+ 'no-access': {
51
+ icon: 'M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z',
52
+ defaultTitle: 'Access restricted',
53
+ defaultDesc: 'You don\'t have permission to view this data.',
54
+ },
55
+ }
56
+
57
+ const config = variantConfig[variant || 'default']
58
+ const displayTitle = title || config.defaultTitle
59
+ const displayDesc = description || config.defaultDesc
60
+ const isError = variant === 'error'
61
+ </script>
62
+
63
+ <div class="empty-state flex flex-col items-center justify-center py-12 px-4 text-center" style="min-height: 200px">
64
+ @if (showIcon)
65
+ <div class="w-16 h-16 rounded-full flex items-center justify-center mb-4 {{ isError ? 'bg-red-100' : 'bg-gray-100' }}">
66
+ <svg
67
+ class="w-8 h-8 {{ isError ? 'text-red-400' : 'text-gray-400' }}"
68
+ fill="none"
69
+ stroke="currentColor"
70
+ viewBox="0 0 24 24"
71
+ >
72
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="{{ config.icon }}" />
73
+ </svg>
74
+ </div>
75
+ @endif
76
+
77
+ <h3 class="text-lg font-medium text-gray-900 mb-1">
78
+ {{ displayTitle }}
79
+ </h3>
80
+
81
+ <p class="text-sm text-gray-500 max-w-sm mb-6">
82
+ {{ displayDesc }}
83
+ </p>
84
+
85
+ @if (actionLabel)
86
+ <button type="button" class="btn-primary">
87
+ {{ actionLabel }}
88
+ </button>
89
+ @endif
90
+ </div>