@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.
- package/CHANGELOG.md +68 -0
- package/LICENSE.md +21 -0
- package/README.md +361 -0
- package/bin/cli.ts +169 -0
- package/dist/Analytics.d.ts +558 -0
- package/dist/api.d.ts +109 -0
- package/dist/batching.d.ts +93 -0
- package/dist/chunk-2mx7fq49.js +4 -0
- package/dist/chunk-3z29508k.js +204 -0
- package/dist/chunk-deephkz6.js +56 -0
- package/dist/chunk-j261vgyp.js +305 -0
- package/dist/chunk-xga17tz7.js +207 -0
- package/dist/config.d.ts +132 -0
- package/dist/dashboard/components/index.d.ts +80 -0
- package/dist/dashboard/composables/useAnalytics.d.ts +99 -0
- package/dist/dashboard/index.d.ts +110 -0
- package/dist/dashboard/types/index.d.ts +144 -0
- package/dist/dashboard/utils/index.d.ts +81 -0
- package/dist/dynamodb.d.ts +86 -0
- package/dist/funnels.d.ts +104 -0
- package/dist/geolocation.d.ts +96 -0
- package/dist/index.d.ts +371 -0
- package/dist/index.js +10211 -0
- package/dist/infrastructure/cdk.d.ts +60 -0
- package/dist/infrastructure/cloudformation.d.ts +44 -0
- package/dist/infrastructure/index.d.ts +45 -0
- package/dist/infrastructure/setup.d.ts +133 -0
- package/dist/integrations/cloudflare.d.ts +76 -0
- package/dist/integrations/hono.d.ts +60 -0
- package/dist/integrations/index.d.ts +24 -0
- package/dist/integrations/nuxt.d.ts +7 -0
- package/dist/integrations/nuxt.js +42 -0
- package/dist/integrations/runtime/use-ts-analytics.d.ts +18 -0
- package/dist/integrations/runtime/use-ts-analytics.js +17 -0
- package/dist/integrations/stx.d.ts +88 -0
- package/dist/integrations/stx.js +18 -0
- package/dist/lib/crypto-random.d.ts +4 -0
- package/dist/lib/salt.d.ts +16 -0
- package/dist/local.d.ts +56 -0
- package/dist/model-connector.d.ts +145 -0
- package/dist/models/AggregatedStats.d.ts +9 -0
- package/dist/models/CampaignStats.d.ts +9 -0
- package/dist/models/Conversion.d.ts +11 -0
- package/dist/models/CustomEvent.d.ts +11 -0
- package/dist/models/DeviceStats.d.ts +9 -0
- package/dist/models/EventStats.d.ts +9 -0
- package/dist/models/GeoStats.d.ts +9 -0
- package/dist/models/Goal.d.ts +9 -0
- package/dist/models/GoalStats.d.ts +9 -0
- package/dist/models/PageStats.d.ts +11 -0
- package/dist/models/PageView.d.ts +13 -0
- package/dist/models/RealtimeStats.d.ts +9 -0
- package/dist/models/ReferrerStats.d.ts +9 -0
- package/dist/models/Session.d.ts +11 -0
- package/dist/models/Site.d.ts +11 -0
- package/dist/models/index.d.ts +28 -0
- package/dist/models/types.d.ts +60 -0
- package/dist/sqs-buffering.d.ts +243 -0
- package/dist/stacks-integration.d.ts +159 -0
- package/dist/tracking-script.d.ts +71 -0
- package/dist/tracking.d.ts +17 -0
- package/dist/tracking.js +26 -0
- package/dist/types.d.ts +595 -0
- package/dist/utils/geolocation.d.ts +111 -0
- package/dist/utils/user-agent.d.ts +17 -0
- package/dist/version.d.ts +7 -0
- package/package.json +119 -0
- package/src/Analytics.ts +3349 -0
- package/src/api.ts +1286 -0
- package/src/assets/crosswind.css +2220 -0
- package/src/batching.ts +452 -0
- package/src/components/dashboard/index.ts +18 -0
- package/src/config.ts +456 -0
- package/src/dashboard/Dashboard.stx +1517 -0
- package/src/dashboard/components/AlertCard.stx +177 -0
- package/src/dashboard/components/AnalyticsDashboard.stx +354 -0
- package/src/dashboard/components/AnimatedNumber.stx +86 -0
- package/src/dashboard/components/BarChart.stx +220 -0
- package/src/dashboard/components/BrowserBreakdown.stx +98 -0
- package/src/dashboard/components/BrowsersTable.stx +125 -0
- package/src/dashboard/components/CampaignBreakdown.stx +163 -0
- package/src/dashboard/components/CampaignTable.stx +238 -0
- package/src/dashboard/components/CountryList.stx +101 -0
- package/src/dashboard/components/DataTable.stx +226 -0
- package/src/dashboard/components/DateRangePicker.stx +77 -0
- package/src/dashboard/components/DeviceBreakdown.stx +94 -0
- package/src/dashboard/components/DevicesTable.stx +163 -0
- package/src/dashboard/components/DonutChart.stories.ts +55 -0
- package/src/dashboard/components/DonutChart.stx +157 -0
- package/src/dashboard/components/EmptyState.stx +90 -0
- package/src/dashboard/components/EngagementMetrics.stx +183 -0
- package/src/dashboard/components/EventsSection.stx +192 -0
- package/src/dashboard/components/FilterBar.stx +104 -0
- package/src/dashboard/components/FullAnalyticsDashboard.stx +455 -0
- package/src/dashboard/components/FunnelChart.stx +142 -0
- package/src/dashboard/components/GeoTable.stx +337 -0
- package/src/dashboard/components/GoalsPanel.stx +109 -0
- package/src/dashboard/components/Header.stx +39 -0
- package/src/dashboard/components/HeatmapChart.stx +140 -0
- package/src/dashboard/components/LiveActivityFeed.stx +172 -0
- package/src/dashboard/components/MetricComparison.stx +106 -0
- package/src/dashboard/components/MiniStats.stx +96 -0
- package/src/dashboard/components/OSBreakdown.stx +124 -0
- package/src/dashboard/components/PageDetailCard.stx +102 -0
- package/src/dashboard/components/PagesTable.stx +127 -0
- package/src/dashboard/components/ProgressRing.stx +89 -0
- package/src/dashboard/components/RealtimeCounter.stories.ts +45 -0
- package/src/dashboard/components/RealtimeCounter.stx +46 -0
- package/src/dashboard/components/ReferrersTable.stx +180 -0
- package/src/dashboard/components/SparklineChart.stx +160 -0
- package/src/dashboard/components/StatCard.stories.ts +58 -0
- package/src/dashboard/components/StatCard.stx +90 -0
- package/src/dashboard/components/SummaryStats.stx +81 -0
- package/src/dashboard/components/TabNav.stx +66 -0
- package/src/dashboard/components/ThemeSwitcher.stx +124 -0
- package/src/dashboard/components/TimeSeriesChart.stx +106 -0
- package/src/dashboard/components/TopList.stories.ts +58 -0
- package/src/dashboard/components/TopList.stx +74 -0
- package/src/dashboard/components/TrendIndicator.stx +84 -0
- package/src/dashboard/components/heatmap/ClickHeatmap.stx +264 -0
- package/src/dashboard/components/heatmap/ElementClickList.stx +125 -0
- package/src/dashboard/components/heatmap/HeatmapControls.stx +125 -0
- package/src/dashboard/components/heatmap/HeatmapLegend.stx +69 -0
- package/src/dashboard/components/heatmap/PageHeatmap.stx +264 -0
- package/src/dashboard/components/heatmap/ScrollHeatmap.stx +127 -0
- package/src/dashboard/components/heatmap/index.ts +12 -0
- package/src/dashboard/components/index.ts +86 -0
- package/src/dashboard/composables/useAnalytics.ts +465 -0
- package/src/dashboard/demo/DemoApp.stx +370 -0
- package/src/dashboard/demo/index.ts +8 -0
- package/src/dashboard/demo/mockData.ts +234 -0
- package/src/dashboard/index.ts +117 -0
- package/src/dashboard/stx-shim.d.ts +4 -0
- package/src/dashboard/types/index.ts +203 -0
- package/src/dashboard/utils/index.ts +426 -0
- package/src/dynamodb.ts +344 -0
- package/src/funnels.ts +534 -0
- package/src/geolocation.ts +515 -0
- package/src/handlers/alerts.ts +328 -0
- package/src/handlers/annotations.ts +128 -0
- package/src/handlers/api-keys.ts +240 -0
- package/src/handlers/auth.ts +1020 -0
- package/src/handlers/authz.ts +137 -0
- package/src/handlers/collect.ts +724 -0
- package/src/handlers/data.ts +625 -0
- package/src/handlers/experiments.ts +138 -0
- package/src/handlers/funnels.ts +216 -0
- package/src/handlers/goals.ts +218 -0
- package/src/handlers/heatmaps.ts +272 -0
- package/src/handlers/index.ts +56 -0
- package/src/handlers/lib/read-cache.ts +63 -0
- package/src/handlers/misc.ts +486 -0
- package/src/handlers/oauth.ts +233 -0
- package/src/handlers/performance.ts +388 -0
- package/src/handlers/sessions.ts +413 -0
- package/src/handlers/sharing.ts +198 -0
- package/src/handlers/stats.ts +1368 -0
- package/src/handlers/team.ts +161 -0
- package/src/handlers/uptime.ts +283 -0
- package/src/handlers/views.ts +390 -0
- package/src/handlers/webhooks.ts +226 -0
- package/src/heatmap/index.ts +32 -0
- package/src/heatmap/tracking-script.ts +452 -0
- package/src/heatmap/types.ts +79 -0
- package/src/index.ts +387 -0
- package/src/infrastructure/cdk.ts +496 -0
- package/src/infrastructure/cloudformation.ts +595 -0
- package/src/infrastructure/index.ts +48 -0
- package/src/infrastructure/setup.ts +611 -0
- package/src/integrations/cloudflare.ts +732 -0
- package/src/integrations/hono.ts +589 -0
- package/src/integrations/index.ts +27 -0
- package/src/integrations/nuxt.ts +78 -0
- package/src/integrations/runtime/use-ts-analytics.ts +32 -0
- package/src/integrations/stx.ts +138 -0
- package/src/jobs/index.ts +127 -0
- package/src/lib/crypto-random.ts +41 -0
- package/src/lib/ddb-errors.ts +20 -0
- package/src/lib/dynamodb.ts +216 -0
- package/src/lib/email.ts +38 -0
- package/src/lib/ga-import.ts +471 -0
- package/src/lib/ga4-api.ts +244 -0
- package/src/lib/goals.ts +205 -0
- package/src/lib/index.ts +6 -0
- package/src/lib/ingest-counters.ts +123 -0
- package/src/lib/log.ts +36 -0
- package/src/lib/membership.ts +98 -0
- package/src/lib/plans.ts +90 -0
- package/src/lib/quota.ts +46 -0
- package/src/lib/rate-limit.ts +27 -0
- package/src/lib/rollups.ts +472 -0
- package/src/lib/salt.ts +97 -0
- package/src/lib/scheduler.ts +97 -0
- package/src/lib/significance.ts +66 -0
- package/src/lib/site-retention.ts +56 -0
- package/src/local.ts +360 -0
- package/src/model-connector.ts +616 -0
- package/src/models/AggregatedStats.ts +162 -0
- package/src/models/CampaignStats.ts +141 -0
- package/src/models/Conversion.ts +135 -0
- package/src/models/CustomEvent.ts +123 -0
- package/src/models/DeviceStats.ts +105 -0
- package/src/models/EventStats.ts +108 -0
- package/src/models/GeoStats.ts +112 -0
- package/src/models/Goal.ts +105 -0
- package/src/models/GoalStats.ts +106 -0
- package/src/models/PageStats.ts +152 -0
- package/src/models/PageView.ts +277 -0
- package/src/models/RealtimeStats.ts +96 -0
- package/src/models/ReferrerStats.ts +115 -0
- package/src/models/Session.ts +235 -0
- package/src/models/Site.ts +115 -0
- package/src/models/index.ts +50 -0
- package/src/models/orm/index.ts +2043 -0
- package/src/models/types.ts +71 -0
- package/src/router.ts +521 -0
- package/src/sqs-buffering.ts +806 -0
- package/src/stacks-integration.ts +527 -0
- package/src/tracking-script.ts +944 -0
- package/src/tracking.ts +27 -0
- package/src/types/analytics.ts +219 -0
- package/src/types/api.ts +75 -0
- package/src/types/bun-router.d.ts +15 -0
- package/src/types/dashboard.ts +62 -0
- package/src/types/index.ts +66 -0
- package/src/types/stx.d.ts +27 -0
- package/src/types/window.d.ts +101 -0
- package/src/types.ts +911 -0
- package/src/utils/cache.ts +148 -0
- package/src/utils/date.ts +118 -0
- package/src/utils/filters.ts +71 -0
- package/src/utils/geolocation.ts +323 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/response.ts +180 -0
- package/src/utils/timezone-country.ts +242 -0
- package/src/utils/user-agent.ts +110 -0
- 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>
|