@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,90 @@
1
+ <script server>
2
+ /**
3
+ * StatCard Component
4
+ *
5
+ * Displays a single stat with optional change indicator.
6
+ */
7
+ import { defineProps, withDefaults } from 'stx'
8
+
9
+ interface StatCardProps {
10
+ title: string
11
+ value: string | number
12
+ format?: 'number' | 'percentage' | 'duration' | 'currency'
13
+ change?: number
14
+ changeLabel?: string
15
+ icon?: string
16
+ loading?: boolean
17
+ }
18
+
19
+ const { title, value, format, change, changeLabel, icon, loading } = withDefaults(
20
+ defineProps<StatCardProps>(),
21
+ {
22
+ format: 'number',
23
+ loading: false,
24
+ }
25
+ )
26
+
27
+ function formatValue(val: string | number, fmt: string): string {
28
+ if (typeof val === 'string') return val
29
+
30
+ switch (fmt) {
31
+ case 'percentage':
32
+ return `${val}%`
33
+ case 'duration':
34
+ const mins = Math.floor(val / 60)
35
+ const secs = val % 60
36
+ return `${mins}:${secs.toString().padStart(2, '0')}`
37
+ case 'currency':
38
+ return `$${val.toLocaleString()}`
39
+ case 'number':
40
+ default:
41
+ if (val >= 1e6) return `${(val / 1e6).toFixed(1)}M`
42
+ if (val >= 1e3) return `${(val / 1e3).toFixed(1)}k`
43
+ return val.toLocaleString()
44
+ }
45
+ }
46
+
47
+ function formatChange(val: number): string {
48
+ const sign = val >= 0 ? '+' : ''
49
+ return `${sign}${val}%`
50
+ }
51
+
52
+ const formattedValue = formatValue(value, format || 'number')
53
+ const formattedChange = change !== undefined ? formatChange(change) : ''
54
+ const isPositiveGood = title.toLowerCase() !== 'bounce rate'
55
+ const changeClass = change !== undefined
56
+ ? (isPositiveGood ? (change >= 0 ? 'text-green-600' : 'text-red-600') : (change <= 0 ? 'text-green-600' : 'text-red-600'))
57
+ : ''
58
+ </script>
59
+
60
+ <div class="stat-card card-hover p-6">
61
+ <div class="flex items-center justify-between">
62
+ <h3 class="stat-title text-sm font-medium text-gray-500">
63
+ {{ title }}
64
+ </h3>
65
+ @if (icon)
66
+ <span class="text-gray-400">{{ icon }}</span>
67
+ @endif
68
+ </div>
69
+
70
+ <div class="mt-2">
71
+ @if (loading)
72
+ <div class="animate-pulse">
73
+ <div class="skeleton h-8 w-24 bg-gray-200 rounded"></div>
74
+ <div class="skeleton h-4 w-16 mt-2 bg-gray-200 rounded"></div>
75
+ </div>
76
+ @else
77
+ <p class="stat-value text-2xl font-semibold text-gray-900">
78
+ {{ formattedValue }}
79
+ </p>
80
+ @if (change !== undefined)
81
+ <p class="mt-1 text-sm {{ changeClass }}">
82
+ <span>{{ formattedChange }}</span>
83
+ @if (changeLabel)
84
+ <span class="text-gray-500 ml-1">{{ changeLabel }}</span>
85
+ @endif
86
+ </p>
87
+ @endif
88
+ @endif
89
+ </div>
90
+ </div>
@@ -0,0 +1,81 @@
1
+ {{-- SummaryStats Component --}}
2
+ {{-- Props: stats (object with realtime, people, views, avgTime, bounceRate, events) --}}
3
+ <div class="summary-stats">
4
+ <div class="stat-card">
5
+ <div class="stat-value">{{ stats.realtime }}</div>
6
+ <div class="stat-label">Realtime</div>
7
+ </div>
8
+ <div class="stat-card">
9
+ <div class="stat-value">{{ stats.people | fmt }}</div>
10
+ <div class="stat-label">People</div>
11
+ </div>
12
+ <div class="stat-card">
13
+ <div class="stat-value">{{ stats.views | fmt }}</div>
14
+ <div class="stat-label">Views</div>
15
+ </div>
16
+ <div class="stat-card">
17
+ <div class="stat-value">{{ stats.avgTime }}</div>
18
+ <div class="stat-label">Avg time</div>
19
+ </div>
20
+ <div class="stat-card">
21
+ <div class="stat-value">{{ stats.bounceRate }}%</div>
22
+ <div class="stat-label">Bounce</div>
23
+ </div>
24
+ <div class="stat-card">
25
+ <div class="stat-value">{{ stats.events | fmt }}</div>
26
+ <div class="stat-label">Events</div>
27
+ </div>
28
+ </div>
29
+
30
+
31
+ <style>
32
+ .summary-stats {
33
+ display: grid;
34
+ grid-template-columns: repeat(6, 1fr);
35
+ gap: 1rem;
36
+ margin-bottom: 1.5rem;
37
+ }
38
+
39
+ .stat-card {
40
+ background: var(--bg-secondary);
41
+ padding: 1.25rem;
42
+ border-radius: 8px;
43
+ text-align: center;
44
+ transition: transform 0.15s, box-shadow 0.15s;
45
+ }
46
+
47
+ .stat-card:hover {
48
+ transform: translateY(-2px);
49
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
50
+ }
51
+
52
+ .stat-value {
53
+ font-size: 1.75rem;
54
+ font-weight: 600;
55
+ color: var(--text-primary);
56
+ margin-bottom: 0.25rem;
57
+ }
58
+
59
+ .stat-label {
60
+ font-size: 0.75rem;
61
+ color: var(--text-secondary);
62
+ text-transform: uppercase;
63
+ letter-spacing: 0.05em;
64
+ }
65
+
66
+ @media (max-width: 1024px) {
67
+ .summary-stats {
68
+ grid-template-columns: repeat(3, 1fr);
69
+ }
70
+ }
71
+
72
+ @media (max-width: 640px) {
73
+ .summary-stats {
74
+ grid-template-columns: repeat(2, 1fr);
75
+ }
76
+
77
+ .stat-value {
78
+ font-size: 1.5rem;
79
+ }
80
+ }
81
+ </style>
@@ -0,0 +1,66 @@
1
+ {{-- TabNav Component --}}
2
+ {{-- Props: tabs (array of {id, label}), activeTab --}}
3
+ <div class="tabs" data-tab-group="{{ tabGroup || 'default' }}">
4
+ @foreach(tabs as tab)
5
+ <button
6
+ class="tab-btn @if(tab.id === activeTab) active @endif"
7
+ data-tab="{{ tab.id }}"
8
+ type="button"
9
+ >
10
+ {{ tab.label }}
11
+ </button>
12
+ @endforeach
13
+ </div>
14
+
15
+ <script>
16
+ (function() {
17
+ var d = window['document'];
18
+ d['querySelectorAll']('.tabs').forEach(tabContainer => {
19
+ const buttons = tabContainer.querySelectorAll('.tab-btn')
20
+ buttons.forEach(btn => {
21
+ btn.addEventListener('click', () => {
22
+ buttons.forEach(b => b.classList.remove('active'))
23
+ btn.classList.add('active')
24
+
25
+ const tabId = btn.dataset.tab
26
+ const group = tabContainer.dataset.tabGroup
27
+ const event = new CustomEvent('tabchange', {
28
+ detail: { tabId, group },
29
+ bubbles: true
30
+ })
31
+ tabContainer.dispatchEvent(event)
32
+ })
33
+ })
34
+ })
35
+ })();
36
+ </script>
37
+
38
+ <style>
39
+ .tabs {
40
+ display: flex;
41
+ gap: 0.25rem;
42
+ border-bottom: 1px solid var(--border);
43
+ margin-bottom: 1rem;
44
+ }
45
+
46
+ .tab-btn {
47
+ background: transparent;
48
+ border: none;
49
+ color: var(--text-secondary);
50
+ padding: 0.5rem 0.75rem;
51
+ font-size: 0.75rem;
52
+ font-weight: 500;
53
+ cursor: pointer;
54
+ border-bottom: 2px solid transparent;
55
+ transition: all 0.15s;
56
+ }
57
+
58
+ .tab-btn:hover {
59
+ color: var(--text-primary);
60
+ }
61
+
62
+ .tab-btn.active {
63
+ color: var(--accent);
64
+ border-bottom-color: var(--accent);
65
+ }
66
+ </style>
@@ -0,0 +1,124 @@
1
+ <script server>
2
+ /**
3
+ * ThemeSwitcher Component
4
+ *
5
+ * Toggle between light, dark, and system theme modes.
6
+ */
7
+ import { defineProps, withDefaults } from 'stx'
8
+
9
+ export type ThemeMode = 'light' | 'dark' | 'system'
10
+
11
+ interface ThemeSwitcherProps {
12
+ initialMode?: ThemeMode
13
+ showLabels?: boolean
14
+ persist?: boolean
15
+ storageKey?: string
16
+ }
17
+
18
+ const { initialMode, showLabels, persist, storageKey } = withDefaults(
19
+ defineProps<ThemeSwitcherProps>(),
20
+ {
21
+ initialMode: 'system',
22
+ showLabels: false,
23
+ persist: true,
24
+ storageKey: 'analytics-theme',
25
+ }
26
+ )
27
+ </script>
28
+
29
+ <script client>
30
+ const mode = state('{{ initialMode }}')
31
+ const shouldPersist = {{ persist ? 'true' : 'false' }}
32
+ const themeStorageKey = '{{ storageKey }}'
33
+
34
+ const isDark = derived(() => {
35
+ if (mode() === 'system') {
36
+ return window.matchMedia('(prefers-color-scheme: dark)').matches
37
+ }
38
+ return mode() === 'dark'
39
+ })
40
+
41
+ function updateDocumentClass() {
42
+ if (isDark()) {
43
+ document.documentElement.classList.add('dark')
44
+ } else {
45
+ document.documentElement.classList.remove('dark')
46
+ }
47
+ }
48
+
49
+ function setMode(newMode) {
50
+ mode.set(newMode)
51
+ if (shouldPersist) {
52
+ localStorage.setItem(themeStorageKey, newMode)
53
+ }
54
+ updateDocumentClass()
55
+ }
56
+
57
+ function cycleMode() {
58
+ const modes = ['light', 'dark', 'system']
59
+ const currentIndex = modes.indexOf(mode())
60
+ const nextIndex = (currentIndex + 1) % modes.length
61
+ setMode(modes[nextIndex])
62
+ }
63
+
64
+ onMount(() => {
65
+ if (shouldPersist) {
66
+ const stored = localStorage.getItem(themeStorageKey)
67
+ if (stored && ['light', 'dark', 'system'].includes(stored)) {
68
+ mode.set(stored)
69
+ }
70
+ }
71
+ updateDocumentClass()
72
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
73
+ if (mode() === 'system') {
74
+ updateDocumentClass()
75
+ }
76
+ })
77
+ })
78
+ </script>
79
+ <div class="theme-switcher">
80
+ @if (!showLabels)
81
+ <button
82
+ type="button"
83
+ class="btn-icon relative"
84
+ :title="'Theme: ' + mode()"
85
+ @click="cycleMode()"
86
+ >
87
+ <Icon name="sun" size="20" :show="mode() === 'light'" />
88
+ <Icon name="moon" size="20" :show="mode() === 'dark'" />
89
+ <Icon name="monitor" size="20" :show="mode() === 'system'" />
90
+ </button>
91
+ @else
92
+ <div class="inline-flex rounded-lg border border-gray-200 p-1 bg-white">
93
+ <button
94
+ type="button"
95
+ class="px-3 py-1.5 text-sm rounded-md transition-colors"
96
+ x-class="mode() === 'light' ? 'bg-gray-100 text-gray-900' : 'text-gray-500 hover:text-gray-700'"
97
+ @click="setMode('light')"
98
+ >
99
+ <Icon name="sun" size="16" class="inline-block mr-1" />
100
+ Light
101
+ </button>
102
+
103
+ <button
104
+ type="button"
105
+ class="px-3 py-1.5 text-sm rounded-md transition-colors"
106
+ x-class="mode() === 'dark' ? 'bg-gray-100 text-gray-900' : 'text-gray-500 hover:text-gray-700'"
107
+ @click="setMode('dark')"
108
+ >
109
+ <Icon name="moon" size="16" class="inline-block mr-1" />
110
+ Dark
111
+ </button>
112
+
113
+ <button
114
+ type="button"
115
+ class="px-3 py-1.5 text-sm rounded-md transition-colors"
116
+ x-class="mode() === 'system' ? 'bg-gray-100 text-gray-900' : 'text-gray-500 hover:text-gray-700'"
117
+ @click="setMode('system')"
118
+ >
119
+ <Icon name="monitor" size="16" class="inline-block mr-1" />
120
+ System
121
+ </button>
122
+ </div>
123
+ @endif
124
+ </div>
@@ -0,0 +1,106 @@
1
+ <script server>
2
+ /**
3
+ * TimeSeriesChart Component
4
+ *
5
+ * Displays time series data as a line chart.
6
+ */
7
+ import { defineProps, withDefaults } from 'stx'
8
+ import type { TimeSeriesDataPoint } from '../types'
9
+
10
+ interface TimeSeriesChartProps {
11
+ data: TimeSeriesDataPoint[]
12
+ metrics?: string[]
13
+ height?: number
14
+ loading?: boolean
15
+ }
16
+
17
+ const { data, metrics, height, loading } = withDefaults(
18
+ defineProps<TimeSeriesChartProps>(),
19
+ {
20
+ metrics: () => ['pageViews', 'uniqueVisitors'],
21
+ height: 300,
22
+ loading: false,
23
+ }
24
+ )
25
+ </script>
26
+
27
+ <div class="chart-container card-hover p-6">
28
+ @if (loading)
29
+ <div class="animate-pulse">
30
+ <div class="skeleton" style="height: {{ height }}px"></div>
31
+ </div>
32
+ @elseif (data.length > 0)
33
+ <div class="chart-header mb-4">
34
+ <div class="chart-legend flex justify-end gap-6 text-xs">
35
+ <span class="legend-item views flex items-center gap-2 text-gray-600">
36
+ <span class="legend-dot w-2.5 h-2.5 rounded-sm bg-indigo-500"></span>
37
+ Views
38
+ </span>
39
+ <span class="legend-item visitors flex items-center gap-2 text-gray-600">
40
+ <span class="legend-dot w-2.5 h-2.5 rounded-sm bg-green-500"></span>
41
+ Visitors
42
+ </span>
43
+ </div>
44
+ </div>
45
+ <script client>
46
+ onMount(() => {
47
+ const canvas = document.getElementById('timeSeriesChart')
48
+ const ctx = canvas?.getContext('2d')
49
+ if (!ctx) return
50
+
51
+ const chartData = {{ JSON.stringify(data) }}
52
+ const labels = chartData.map(d => new Date(d.date).toLocaleDateString())
53
+
54
+ new Chart(ctx, {
55
+ type: 'line',
56
+ data: {
57
+ labels,
58
+ datasets: [
59
+ {
60
+ label: 'Page Views',
61
+ data: chartData.map(d => d.pageViews || d.views || 0),
62
+ borderColor: '#6366f1',
63
+ backgroundColor: '#6366f120',
64
+ fill: true,
65
+ tension: 0.3,
66
+ },
67
+ {
68
+ label: 'Unique Visitors',
69
+ data: chartData.map(d => d.uniqueVisitors || d.visitors || 0),
70
+ borderColor: '#22c55e',
71
+ backgroundColor: '#22c55e20',
72
+ fill: true,
73
+ tension: 0.3,
74
+ }
75
+ ]
76
+ },
77
+ options: {
78
+ responsive: true,
79
+ maintainAspectRatio: false,
80
+ plugins: {
81
+ legend: { display: false }
82
+ },
83
+ scales: {
84
+ y: {
85
+ beginAtZero: true,
86
+ grid: { color: '#e5e7eb' }
87
+ },
88
+ x: {
89
+ grid: { display: false }
90
+ }
91
+ }
92
+ }
93
+ })
94
+ })
95
+ </script>
96
+ <canvas
97
+ id="timeSeriesChart"
98
+ style="height: {{ height }}px"
99
+ ></canvas>
100
+ @else
101
+ <div class="chart-placeholder flex flex-col items-center justify-center text-gray-400 gap-4" style="height: {{ height }}px">
102
+ <Icon name="bar-chart-2" size="48" />
103
+ <p>No data available for selected period</p>
104
+ </div>
105
+ @endif
106
+ </div>
@@ -0,0 +1,58 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+ import TopList from './TopList.stx'
3
+
4
+ const meta = {
5
+ title: 'Components/Lists/TopList',
6
+ component: TopList,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ title: { control: 'text' },
10
+ valueLabel: { control: 'text' },
11
+ maxItems: { control: { type: 'number', min: 1, max: 20 } },
12
+ loading: { control: 'boolean' },
13
+ },
14
+ } satisfies Meta<typeof TopList>
15
+
16
+ export default meta
17
+ type Story = StoryObj<typeof meta>
18
+
19
+ const sampleItems = [
20
+ { name: '/home', value: 12847, change: 0.12 },
21
+ { name: '/products', value: 8923, change: 0.08 },
22
+ { name: '/about', value: 5634, change: 0.24 },
23
+ { name: '/blog', value: 4521, change: -0.05 },
24
+ { name: '/contact', value: 2134, change: 0.03 },
25
+ ]
26
+
27
+ export const Default: Story = {
28
+ args: {
29
+ items: sampleItems,
30
+ title: 'Top Pages',
31
+ valueLabel: 'Views',
32
+ },
33
+ }
34
+
35
+ export const WithoutChanges: Story = {
36
+ args: {
37
+ items: sampleItems.map(({ name, value }) => ({ name, value })),
38
+ title: 'Top Referrers',
39
+ valueLabel: 'Visitors',
40
+ },
41
+ }
42
+
43
+ export const Loading: Story = {
44
+ args: {
45
+ items: [],
46
+ title: 'Top Pages',
47
+ loading: true,
48
+ },
49
+ }
50
+
51
+ export const LimitedItems: Story = {
52
+ args: {
53
+ items: sampleItems,
54
+ title: 'Top 3 Pages',
55
+ valueLabel: 'Views',
56
+ maxItems: 3,
57
+ },
58
+ }
@@ -0,0 +1,74 @@
1
+ <script server>
2
+ /**
3
+ * TopList Component
4
+ *
5
+ * Displays a list of top items (pages, referrers, etc.) with bars.
6
+ */
7
+ import { defineProps, withDefaults } from 'stx'
8
+ import type { TopListProps } from '../types'
9
+ import { formatCompact, formatPercentage } from '../utils'
10
+
11
+ const { title, items, showPercentage, maxItems, emptyMessage, loading } = withDefaults(
12
+ defineProps<TopListProps>(),
13
+ {
14
+ showPercentage: true,
15
+ maxItems: 10,
16
+ emptyMessage: 'No data available',
17
+ loading: false,
18
+ }
19
+ )
20
+
21
+ const displayItems = items.slice(0, maxItems)
22
+ const maxValue = Math.max(...items.map(item => item.value), 1)
23
+ </script>
24
+
25
+ <div class="top-list card-hover p-6">
26
+ <h3 class="text-sm font-medium text-gray-900 mb-4">
27
+ {{ title }}
28
+ </h3>
29
+
30
+ @if (loading)
31
+ <div class="space-y-3">
32
+ @foreach ([1, 2, 3, 4, 5] as i)
33
+ <div class="animate-pulse">
34
+ <div class="flex justify-between mb-1">
35
+ <div class="skeleton h-4 w-32"></div>
36
+ <div class="skeleton h-4 w-12"></div>
37
+ </div>
38
+ <div class="skeleton h-2"></div>
39
+ </div>
40
+ @endforeach
41
+ </div>
42
+ @elseif (items.length === 0)
43
+ <div class="text-center py-8 text-gray-500">
44
+ {{ emptyMessage }}
45
+ </div>
46
+ @else
47
+ <div class="space-y-3">
48
+ @foreach (displayItems as item)
49
+ <div class="group">
50
+ <div class="list-item mb-1">
51
+ <span class="list-label" title="{{ item.name }}">
52
+ {{ item.name }}
53
+ </span>
54
+ <span class="list-value">
55
+ {{ formatCompact(item.value) }}
56
+ @if (showPercentage)
57
+ <span class="text-gray-400 ml-1">
58
+ ({{ formatPercentage(item.percentage) }})
59
+ </span>
60
+ @endif
61
+ </span>
62
+ </div>
63
+
64
+ <div class="progress-bar">
65
+ <div
66
+ class="progress-fill bg-indigo-500"
67
+ style="width: {{ (item.value / maxValue) * 100 }}%"
68
+ ></div>
69
+ </div>
70
+ </div>
71
+ @endforeach
72
+ </div>
73
+ @endif
74
+ </div>
@@ -0,0 +1,84 @@
1
+ <script server>
2
+ /**
3
+ * TrendIndicator Component
4
+ *
5
+ * Compact trend indicator showing direction and percentage change.
6
+ */
7
+ import { defineProps, withDefaults } from 'stx'
8
+
9
+ interface TrendIndicatorProps {
10
+ value: number // Percentage change (-100 to +100+)
11
+ size?: 'sm' | 'md' | 'lg'
12
+ showValue?: boolean
13
+ invertColors?: boolean // For metrics where decrease is good
14
+ neutral?: number // Threshold for neutral (default 0)
15
+ }
16
+
17
+ const { value, size, showValue, invertColors, neutral } = withDefaults(
18
+ defineProps<TrendIndicatorProps>(),
19
+ {
20
+ size: 'md',
21
+ showValue: true,
22
+ invertColors: false,
23
+ neutral: 0,
24
+ }
25
+ )
26
+
27
+ const isPositive = value > neutral
28
+ const isNegative = value < neutral
29
+ const isNeutral = value === neutral
30
+
31
+ function getColorClass(): string {
32
+ if (isNeutral)
33
+ return 'text-gray-500 bg-gray-100'
34
+
35
+ const positive = isPositive
36
+ const goodTrend = invertColors ? !positive : positive
37
+
38
+ return goodTrend
39
+ ? 'text-green-600 bg-green-100'
40
+ : 'text-red-600 bg-red-100'
41
+ }
42
+
43
+ function getSizeClasses(): { wrapper: string, icon: string } {
44
+ switch (size) {
45
+ case 'sm':
46
+ return { wrapper: 'text-xs px-1.5 py-0.5 gap-0.5', icon: 'w-3 h-3' }
47
+ case 'lg':
48
+ return { wrapper: 'text-base px-3 py-1.5 gap-1.5', icon: 'w-5 h-5' }
49
+ default:
50
+ return { wrapper: 'text-sm px-2 py-1 gap-1', icon: 'w-4 h-4' }
51
+ }
52
+ }
53
+
54
+ function formatValue(): string {
55
+ const abs = Math.abs(value)
56
+ if (abs >= 1000)
57
+ return `${(abs / 1000).toFixed(1)}k`
58
+ if (abs >= 100)
59
+ return abs.toFixed(0)
60
+ if (abs >= 10)
61
+ return abs.toFixed(1)
62
+ return abs.toFixed(2)
63
+ }
64
+
65
+ const colorClass = getColorClass()
66
+ const sizeClasses = getSizeClasses()
67
+ const formattedValue = formatValue()
68
+ </script>
69
+
70
+ <span class="inline-flex items-center rounded-full font-medium {{ colorClass }} {{ sizeClasses.wrapper }}">
71
+ @if (isPositive)
72
+ <Icon name="arrow-up" class="{{ sizeClasses.icon }}" />
73
+ @elseif (isNegative)
74
+ <Icon name="arrow-down" class="{{ sizeClasses.icon }}" />
75
+ @else
76
+ <Icon name="minus" class="{{ sizeClasses.icon }}" />
77
+ @endif
78
+
79
+ @if (showValue)
80
+ <span>
81
+ {{ isPositive ? '+' : '' }}{{ formattedValue }}%
82
+ </span>
83
+ @endif
84
+ </span>