astryx-dracula 0.1.3 → 0.2.0

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 (94) hide show
  1. package/BRAND.md +1 -1
  2. package/README.md +2 -0
  3. package/USAGE.md +13 -0
  4. package/astryx-dracula.js +4 -4
  5. package/astryx.integration.mjs +3 -0
  6. package/package.json +4 -2
  7. package/templates/ai-chat-landing.template.mjs +6 -0
  8. package/templates/ai-chat-landing.tsx +476 -0
  9. package/templates/ai-chat.template.mjs +6 -0
  10. package/templates/ai-chat.tsx +762 -0
  11. package/templates/blank.template.mjs +6 -0
  12. package/templates/blank.tsx +16 -0
  13. package/templates/centered-hero.template.mjs +6 -0
  14. package/templates/centered-hero.tsx +188 -0
  15. package/templates/classic-gallery.template.mjs +6 -0
  16. package/templates/classic-gallery.tsx +181 -0
  17. package/templates/contact-form.template.mjs +6 -0
  18. package/templates/contact-form.tsx +292 -0
  19. package/templates/dashboard-portfolio.template.mjs +6 -0
  20. package/templates/dashboard-portfolio.tsx +800 -0
  21. package/templates/dashboard.template.mjs +6 -0
  22. package/templates/dashboard.tsx +774 -0
  23. package/templates/detail-page.template.mjs +6 -0
  24. package/templates/detail-page.tsx +695 -0
  25. package/templates/documentation-design.template.mjs +6 -0
  26. package/templates/documentation-design.tsx +751 -0
  27. package/templates/documentation-technical.template.mjs +6 -0
  28. package/templates/documentation-technical.tsx +292 -0
  29. package/templates/documentation.template.mjs +6 -0
  30. package/templates/documentation.tsx +347 -0
  31. package/templates/editor.template.mjs +6 -0
  32. package/templates/editor.tsx +959 -0
  33. package/templates/file-explorer.template.mjs +6 -0
  34. package/templates/file-explorer.tsx +577 -0
  35. package/templates/form-two-column.template.mjs +6 -0
  36. package/templates/form-two-column.tsx +314 -0
  37. package/templates/gallery-hero.template.mjs +6 -0
  38. package/templates/gallery-hero.tsx +287 -0
  39. package/templates/ide.template.mjs +6 -0
  40. package/templates/ide.tsx +533 -0
  41. package/templates/incident-console.template.mjs +6 -0
  42. package/templates/incident-console.tsx +584 -0
  43. package/templates/kanban-board.template.mjs +6 -0
  44. package/templates/kanban-board.tsx +746 -0
  45. package/templates/library.template.mjs +6 -0
  46. package/templates/library.tsx +489 -0
  47. package/templates/login-card.template.mjs +6 -0
  48. package/templates/login-card.tsx +200 -0
  49. package/templates/login-split.template.mjs +6 -0
  50. package/templates/login-split.tsx +318 -0
  51. package/templates/login-sso.template.mjs +6 -0
  52. package/templates/login-sso.tsx +277 -0
  53. package/templates/login.template.mjs +6 -0
  54. package/templates/login.tsx +98 -0
  55. package/templates/messaging-shell.template.mjs +6 -0
  56. package/templates/messaging-shell.tsx +675 -0
  57. package/templates/mixed-gallery.template.mjs +6 -0
  58. package/templates/mixed-gallery.tsx +190 -0
  59. package/templates/payment-form.template.mjs +6 -0
  60. package/templates/payment-form.tsx +990 -0
  61. package/templates/product-detail.template.mjs +6 -0
  62. package/templates/product-detail.tsx +366 -0
  63. package/templates/product-gallery.template.mjs +6 -0
  64. package/templates/product-gallery.tsx +173 -0
  65. package/templates/settings-dialog.template.mjs +6 -0
  66. package/templates/settings-dialog.tsx +920 -0
  67. package/templates/settings-sidebar.template.mjs +6 -0
  68. package/templates/settings-sidebar.tsx +952 -0
  69. package/templates/settings.template.mjs +6 -0
  70. package/templates/settings.tsx +235 -0
  71. package/templates/shell-nav.template.mjs +6 -0
  72. package/templates/shell-nav.tsx +306 -0
  73. package/templates/shell-side-nav.template.mjs +6 -0
  74. package/templates/shell-side-nav.tsx +238 -0
  75. package/templates/shell-top-nav.template.mjs +6 -0
  76. package/templates/shell-top-nav.tsx +243 -0
  77. package/templates/side-gallery.template.mjs +6 -0
  78. package/templates/side-gallery.tsx +179 -0
  79. package/templates/table-grouped.template.mjs +6 -0
  80. package/templates/table-grouped.tsx +1174 -0
  81. package/templates/table-page-chart.template.mjs +6 -0
  82. package/templates/table-page-chart.tsx +693 -0
  83. package/templates/table-page-heatmap-status.template.mjs +6 -0
  84. package/templates/table-page-heatmap-status.tsx +547 -0
  85. package/templates/table-page-shoe-store-heatmap.template.mjs +6 -0
  86. package/templates/table-page-shoe-store-heatmap.tsx +1038 -0
  87. package/templates/table-page.template.mjs +6 -0
  88. package/templates/table-page.tsx +445 -0
  89. package/templates/table.template.mjs +6 -0
  90. package/templates/table.tsx +84 -0
  91. package/templates/theme-showcase.template.mjs +6 -0
  92. package/templates/theme-showcase.tsx +1282 -0
  93. package/theme.css +4 -4
  94. package/tokens.css +4 -4
@@ -0,0 +1,774 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import {VStack, HStack, Layout, LayoutContent} from '@astryxdesign/core/Layout';
4
+ import {Text, Heading} from '@astryxdesign/core/Text';
5
+ import {Card} from '@astryxdesign/core/Card';
6
+ import {Button} from '@astryxdesign/core/Button';
7
+ import {ProgressBar} from '@astryxdesign/core/ProgressBar';
8
+ import {Grid} from '@astryxdesign/core/Grid';
9
+ import {Table, proportional, pixel} from '@astryxdesign/core/Table';
10
+ import type {TableColumn} from '@astryxdesign/core/Table';
11
+ import {Divider} from '@astryxdesign/core/Divider';
12
+ import {Link} from '@astryxdesign/core/Link';
13
+ import {Icon} from '@astryxdesign/core/Icon';
14
+
15
+ // ============= ICONS =============
16
+
17
+ import {RefreshCw, ArrowUp, ArrowDown, Square} from 'lucide-react';
18
+
19
+ // ============= DATA =============
20
+
21
+ // Active users chart data (96 points over 24h at 15-min intervals: Apr 1 14:00 → Apr 2 14:00)
22
+ // Each point has an index (0–95) for even spacing, plus a label for display
23
+ const activeUsersData = [
24
+ // Apr 1 14:00 — mid-afternoon, strong work hours
25
+ {hour: 0, label: 'Apr 1 14:00', allUsers: 116, desktop: 79, mobile: 37},
26
+ {hour: 1, label: 'Apr 1 14:15', allUsers: 118, desktop: 80, mobile: 38},
27
+ {hour: 2, label: 'Apr 1 14:30', allUsers: 117, desktop: 79, mobile: 38},
28
+ {hour: 3, label: 'Apr 1 14:45', allUsers: 119, desktop: 81, mobile: 38},
29
+ // Apr 1 15:00 — afternoon lull
30
+ {hour: 4, label: 'Apr 1 15:00', allUsers: 116, desktop: 79, mobile: 37},
31
+ {hour: 5, label: 'Apr 1 15:15', allUsers: 113, desktop: 76, mobile: 37},
32
+ {hour: 6, label: 'Apr 1 15:30', allUsers: 109, desktop: 72, mobile: 37},
33
+ {hour: 7, label: 'Apr 1 15:45', allUsers: 111, desktop: 74, mobile: 37},
34
+ // Apr 1 16:00 — late afternoon, some leaving early
35
+ {hour: 8, label: 'Apr 1 16:00', allUsers: 110, desktop: 72, mobile: 38},
36
+ {hour: 9, label: 'Apr 1 16:15', allUsers: 107, desktop: 69, mobile: 38},
37
+ {hour: 10, label: 'Apr 1 16:30', allUsers: 104, desktop: 66, mobile: 38},
38
+ {hour: 11, label: 'Apr 1 16:45', allUsers: 101, desktop: 62, mobile: 39},
39
+ // Apr 1 17:00 — 5PM exodus, desktop drops fast, mobile bumps
40
+ {hour: 12, label: 'Apr 1 17:00', allUsers: 100, desktop: 57, mobile: 43},
41
+ {hour: 13, label: 'Apr 1 17:15', allUsers: 99, desktop: 54, mobile: 45},
42
+ {hour: 14, label: 'Apr 1 17:30', allUsers: 97, desktop: 50, mobile: 47},
43
+ {hour: 15, label: 'Apr 1 17:45', allUsers: 95, desktop: 46, mobile: 49},
44
+ // Apr 1 18:00 — commute, mobile overtakes desktop
45
+ {hour: 16, label: 'Apr 1 18:00', allUsers: 93, desktop: 41, mobile: 52},
46
+ {hour: 17, label: 'Apr 1 18:15', allUsers: 91, desktop: 39, mobile: 52},
47
+ {hour: 18, label: 'Apr 1 18:30', allUsers: 89, desktop: 37, mobile: 52},
48
+ {hour: 19, label: 'Apr 1 18:45', allUsers: 87, desktop: 36, mobile: 51},
49
+ // Apr 1 19:00 — dinner, decline slowing
50
+ {hour: 20, label: 'Apr 1 19:00', allUsers: 85, desktop: 34, mobile: 51},
51
+ {hour: 21, label: 'Apr 1 19:15', allUsers: 83, desktop: 32, mobile: 51},
52
+ {hour: 22, label: 'Apr 1 19:30', allUsers: 80, desktop: 30, mobile: 50},
53
+ {hour: 23, label: 'Apr 1 19:45', allUsers: 77, desktop: 28, mobile: 49},
54
+ // Apr 1 20:00 — couch browsing, mobile plateau
55
+ {hour: 24, label: 'Apr 1 20:00', allUsers: 75, desktop: 26, mobile: 49},
56
+ {hour: 25, label: 'Apr 1 20:15', allUsers: 76, desktop: 26, mobile: 50},
57
+ {hour: 26, label: 'Apr 1 20:30', allUsers: 76, desktop: 27, mobile: 49},
58
+ {hour: 27, label: 'Apr 1 20:45', allUsers: 75, desktop: 27, mobile: 48},
59
+ // Apr 1 21:00 — winding down, mobile dropping off
60
+ {hour: 28, label: 'Apr 1 21:00', allUsers: 73, desktop: 27, mobile: 46},
61
+ {hour: 29, label: 'Apr 1 21:15', allUsers: 71, desktop: 27, mobile: 44},
62
+ {hour: 30, label: 'Apr 1 21:30', allUsers: 69, desktop: 27, mobile: 42},
63
+ {hour: 31, label: 'Apr 1 21:45', allUsers: 67, desktop: 28, mobile: 39},
64
+ // Apr 1 22:00 — bedtime wave, mobile drops, desktop holds
65
+ {hour: 32, label: 'Apr 1 22:00', allUsers: 65, desktop: 29, mobile: 36},
66
+ {hour: 33, label: 'Apr 1 22:15', allUsers: 63, desktop: 29, mobile: 34},
67
+ {hour: 34, label: 'Apr 1 22:30', allUsers: 61, desktop: 30, mobile: 31},
68
+ {hour: 35, label: 'Apr 1 22:45', allUsers: 60, desktop: 31, mobile: 29},
69
+ // Apr 1 23:00 — desktop overtakes as local users sleep, other TZs active
70
+ {hour: 36, label: 'Apr 1 23:00', allUsers: 59, desktop: 33, mobile: 26},
71
+ {hour: 37, label: 'Apr 1 23:15', allUsers: 58, desktop: 34, mobile: 24},
72
+ {hour: 38, label: 'Apr 1 23:30', allUsers: 57, desktop: 35, mobile: 22},
73
+ {hour: 39, label: 'Apr 1 23:45', allUsers: 56, desktop: 36, mobile: 20},
74
+ // Apr 2 00:00 — EMEA morning starts, desktop dominant
75
+ {hour: 40, label: 'Apr 2 00:00', allUsers: 56, desktop: 38, mobile: 18},
76
+ {hour: 41, label: 'Apr 2 00:15', allUsers: 56, desktop: 39, mobile: 17},
77
+ {hour: 42, label: 'Apr 2 00:30', allUsers: 57, desktop: 40, mobile: 17},
78
+ {hour: 43, label: 'Apr 2 00:45', allUsers: 56, desktop: 40, mobile: 16},
79
+ // Apr 2 01:00 — EMEA working, plateau
80
+ {hour: 44, label: 'Apr 2 01:00', allUsers: 56, desktop: 41, mobile: 15},
81
+ {hour: 45, label: 'Apr 2 01:15', allUsers: 55, desktop: 41, mobile: 14},
82
+ {hour: 46, label: 'Apr 2 01:30', allUsers: 55, desktop: 41, mobile: 14},
83
+ {hour: 47, label: 'Apr 2 01:45', allUsers: 54, desktop: 40, mobile: 14},
84
+ // Apr 2 02:00 — EMEA mid-morning, holding steady
85
+ {hour: 48, label: 'Apr 2 02:00', allUsers: 54, desktop: 40, mobile: 14},
86
+ {hour: 49, label: 'Apr 2 02:15', allUsers: 53, desktop: 39, mobile: 14},
87
+ {hour: 50, label: 'Apr 2 02:30', allUsers: 53, desktop: 39, mobile: 14},
88
+ {hour: 51, label: 'Apr 2 02:45', allUsers: 52, desktop: 38, mobile: 14},
89
+ // Apr 2 03:00 — EMEA lunch gap, slight dip
90
+ {hour: 52, label: 'Apr 2 03:00', allUsers: 51, desktop: 37, mobile: 14},
91
+ {hour: 53, label: 'Apr 2 03:15', allUsers: 50, desktop: 36, mobile: 14},
92
+ {hour: 54, label: 'Apr 2 03:30', allUsers: 49, desktop: 35, mobile: 14},
93
+ {hour: 55, label: 'Apr 2 03:45', allUsers: 49, desktop: 35, mobile: 14},
94
+ // Apr 2 04:00 — EMEA afternoon, floor
95
+ {hour: 56, label: 'Apr 2 04:00', allUsers: 48, desktop: 34, mobile: 14},
96
+ {hour: 57, label: 'Apr 2 04:15', allUsers: 48, desktop: 33, mobile: 15},
97
+ {hour: 58, label: 'Apr 2 04:30', allUsers: 49, desktop: 33, mobile: 16},
98
+ {hour: 59, label: 'Apr 2 04:45', allUsers: 49, desktop: 32, mobile: 17},
99
+ // Apr 2 05:00 — early risers checking phones, mobile climbing
100
+ {hour: 60, label: 'Apr 2 05:00', allUsers: 50, desktop: 31, mobile: 19},
101
+ {hour: 61, label: 'Apr 2 05:15', allUsers: 51, desktop: 30, mobile: 21},
102
+ {hour: 62, label: 'Apr 2 05:30', allUsers: 52, desktop: 29, mobile: 23},
103
+ {hour: 63, label: 'Apr 2 05:45', allUsers: 54, desktop: 28, mobile: 26},
104
+ // Apr 2 06:00 — alarms going off, mobile surging
105
+ {hour: 64, label: 'Apr 2 06:00', allUsers: 56, desktop: 27, mobile: 29},
106
+ {hour: 65, label: 'Apr 2 06:15', allUsers: 58, desktop: 26, mobile: 32},
107
+ {hour: 66, label: 'Apr 2 06:30', allUsers: 61, desktop: 26, mobile: 35},
108
+ {hour: 67, label: 'Apr 2 06:45', allUsers: 64, desktop: 27, mobile: 37},
109
+ // Apr 2 07:00 — commute, mobile peaks, desktop starting
110
+ {hour: 68, label: 'Apr 2 07:00', allUsers: 67, desktop: 28, mobile: 39},
111
+ {hour: 69, label: 'Apr 2 07:15', allUsers: 70, desktop: 30, mobile: 40},
112
+ {hour: 70, label: 'Apr 2 07:30', allUsers: 73, desktop: 33, mobile: 40},
113
+ {hour: 71, label: 'Apr 2 07:45', allUsers: 76, desktop: 37, mobile: 39},
114
+ // Apr 2 08:00 — arriving at desks, desktop ramping
115
+ {hour: 72, label: 'Apr 2 08:00', allUsers: 80, desktop: 43, mobile: 37},
116
+ {hour: 73, label: 'Apr 2 08:15', allUsers: 85, desktop: 49, mobile: 36},
117
+ {hour: 74, label: 'Apr 2 08:30', allUsers: 90, desktop: 55, mobile: 35},
118
+ {hour: 75, label: 'Apr 2 08:45', allUsers: 95, desktop: 61, mobile: 34},
119
+ // Apr 2 09:00 — work day, desktop dominant
120
+ {hour: 76, label: 'Apr 2 09:00', allUsers: 99, desktop: 66, mobile: 33},
121
+ {hour: 77, label: 'Apr 2 09:15', allUsers: 102, desktop: 69, mobile: 33},
122
+ {hour: 78, label: 'Apr 2 09:30', allUsers: 104, desktop: 72, mobile: 32},
123
+ {hour: 79, label: 'Apr 2 09:45', allUsers: 104, desktop: 72, mobile: 32},
124
+ // Apr 2 10:00 — coffee break stall, then climbing
125
+ {hour: 80, label: 'Apr 2 10:00', allUsers: 106, desktop: 74, mobile: 32},
126
+ {hour: 81, label: 'Apr 2 10:15', allUsers: 109, desktop: 76, mobile: 33},
127
+ {hour: 82, label: 'Apr 2 10:30', allUsers: 112, desktop: 78, mobile: 34},
128
+ {hour: 83, label: 'Apr 2 10:45', allUsers: 114, desktop: 80, mobile: 34},
129
+ // Apr 2 11:00 — approaching peak
130
+ {hour: 84, label: 'Apr 2 11:00', allUsers: 116, desktop: 81, mobile: 35},
131
+ {hour: 85, label: 'Apr 2 11:15', allUsers: 117, desktop: 81, mobile: 36},
132
+ {hour: 86, label: 'Apr 2 11:30', allUsers: 119, desktop: 83, mobile: 36},
133
+ {hour: 87, label: 'Apr 2 11:45', allUsers: 119, desktop: 82, mobile: 37},
134
+ // Apr 2 12:00 — lunch dip, mobile ticks up
135
+ {hour: 88, label: 'Apr 2 12:00', allUsers: 120, desktop: 83, mobile: 37},
136
+ {hour: 89, label: 'Apr 2 12:15', allUsers: 115, desktop: 78, mobile: 37},
137
+ {hour: 90, label: 'Apr 2 12:30', allUsers: 111, desktop: 74, mobile: 37},
138
+ {hour: 91, label: 'Apr 2 12:45', allUsers: 110, desktop: 73, mobile: 37},
139
+ // Apr 2 13:00 — returning from lunch
140
+ {hour: 92, label: 'Apr 2 13:00', allUsers: 113, desktop: 76, mobile: 37},
141
+ {hour: 93, label: 'Apr 2 13:15', allUsers: 116, desktop: 79, mobile: 37},
142
+ {hour: 94, label: 'Apr 2 13:30', allUsers: 118, desktop: 81, mobile: 37},
143
+ {hour: 95, label: 'Apr 2 14:00', allUsers: 120, desktop: 83, mobile: 37},
144
+ ];
145
+
146
+ // Metric cards
147
+ const metrics = [
148
+ {
149
+ label: 'Moonlit visitors',
150
+ value: '27.3 k',
151
+ change: '+18.2%',
152
+ positive: true,
153
+ },
154
+ {
155
+ label: 'Midnight page views',
156
+ value: '48.2 k',
157
+ change: '+12.5%',
158
+ positive: true,
159
+ },
160
+ {
161
+ label: 'Avg. night visit',
162
+ value: '4.5 min',
163
+ change: '-14.3%',
164
+ positive: false,
165
+ },
166
+ {
167
+ label: 'Sunrise bounce rate',
168
+ value: '42.3%',
169
+ change: '-8.7%',
170
+ positive: true,
171
+ },
172
+ ];
173
+
174
+ // Sparkline data for each metric card (30 days, weekends at indices 5-6, 12-13, 19-20, 26-27)
175
+ const sparklines = [
176
+ // Monthly Visitors: +18.2% — declining first 2 weeks, hits bottom around day 14, then sharp recovery
177
+ // prettier-ignore
178
+ [48, 46, 44, 42, 40, 18, 16, 38, 36, 34, 32, 30, 12, 10, 28, 26, 28, 32, 36, 14, 12, 40, 44, 48, 52, 56, 28, 24, 58, 62],
179
+ // Monthly Page Views: +12.5% — flat/choppy first 3 weeks, then kicks up sharply in final week
180
+ // prettier-ignore
181
+ [36, 38, 35, 37, 36, 14, 12, 38, 36, 34, 37, 35, 12, 10, 36, 34, 36, 35, 38, 14, 12, 40, 44, 50, 54, 56, 26, 22, 58, 60],
182
+ // Avg. Session: -14.3% — strong start, holds through week 2, then clear drop-off week 3-4
183
+ // prettier-ignore
184
+ [58, 56, 60, 58, 62, 30, 26, 60, 58, 62, 60, 58, 28, 24, 56, 54, 50, 46, 42, 18, 14, 38, 36, 34, 32, 30, 10, 8, 28, 26],
185
+ // Bounce Rate: -8.7% — high and volatile first half, starts dropping around day 16, steady decline
186
+ // prettier-ignore
187
+ [52, 56, 50, 54, 58, 62, 60, 54, 52, 56, 50, 54, 60, 58, 50, 48, 46, 44, 40, 46, 44, 38, 36, 34, 36, 32, 38, 36, 30, 28],
188
+ ];
189
+
190
+ // Demographics
191
+ const regionData = [
192
+ {
193
+ label: 'NORAM',
194
+ value: 38,
195
+ color: 'var(--dracula-comment)',
196
+ },
197
+ {
198
+ label: 'EMEA',
199
+ value: 28,
200
+ color: 'var(--dracula-orange)',
201
+ },
202
+ {
203
+ label: 'APAC',
204
+ value: 22,
205
+ color: 'var(--dracula-green)',
206
+ },
207
+ {
208
+ label: 'LATAM',
209
+ value: 8,
210
+ color: 'var(--dracula-purple)',
211
+ },
212
+ {label: 'Other', value: 4, color: 'var(--dracula-selection)'},
213
+ ];
214
+
215
+ const roleData = [
216
+ {
217
+ label: 'Engineer',
218
+ value: 45,
219
+ color: 'var(--dracula-comment)',
220
+ },
221
+ {
222
+ label: 'Manager',
223
+ value: 20,
224
+ color: 'var(--dracula-orange)',
225
+ },
226
+ {
227
+ label: 'Designer',
228
+ value: 15,
229
+ color: 'var(--dracula-green)',
230
+ },
231
+ {
232
+ label: 'Data Scientist',
233
+ value: 12,
234
+ color: 'var(--dracula-purple)',
235
+ },
236
+ {label: 'Other', value: 8, color: 'var(--dracula-selection)'},
237
+ ];
238
+
239
+ // Engagement — Top pages
240
+ interface PageRow extends Record<string, unknown> {
241
+ id: string;
242
+ page: string;
243
+ views: number;
244
+ newUsers: string;
245
+ avgTime: string;
246
+ }
247
+
248
+ const topPagesData: PageRow[] = [
249
+ {
250
+ id: '1',
251
+ page: '/home',
252
+ views: 8420,
253
+ newUsers: '62.3%',
254
+ avgTime: '3:42',
255
+ },
256
+ {
257
+ id: '2',
258
+ page: '/products',
259
+ views: 6150,
260
+ newUsers: '45.1%',
261
+ avgTime: '4:15',
262
+ },
263
+ {
264
+ id: '3',
265
+ page: '/pricing',
266
+ views: 4830,
267
+ newUsers: '38.7%',
268
+ avgTime: '2:58',
269
+ },
270
+ {
271
+ id: '4',
272
+ page: '/blog',
273
+ views: 3920,
274
+ newUsers: '71.4%',
275
+ avgTime: '5:30',
276
+ },
277
+ {
278
+ id: '5',
279
+ page: '/docs',
280
+ views: 3410,
281
+ newUsers: '29.8%',
282
+ avgTime: '6:12',
283
+ },
284
+ {
285
+ id: '6',
286
+ page: '/about',
287
+ views: 2980,
288
+ newUsers: '55.6%',
289
+ avgTime: '2:15',
290
+ },
291
+ {
292
+ id: '7',
293
+ page: '/contact',
294
+ views: 2540,
295
+ newUsers: '48.2%',
296
+ avgTime: '1:48',
297
+ },
298
+ {
299
+ id: '8',
300
+ page: '/changelog',
301
+ views: 2210,
302
+ newUsers: '22.1%',
303
+ avgTime: '4:55',
304
+ },
305
+ {
306
+ id: '9',
307
+ page: '/support',
308
+ views: 1870,
309
+ newUsers: '59.3%',
310
+ avgTime: '3:22',
311
+ },
312
+ {
313
+ id: '10',
314
+ page: '/careers',
315
+ views: 1520,
316
+ newUsers: '83.1%',
317
+ avgTime: '2:34',
318
+ },
319
+ ];
320
+
321
+ const topPagesMaxViews = Math.max(...topPagesData.map(d => d.views));
322
+
323
+ const topPagesColumns: TableColumn<PageRow>[] = [
324
+ {key: 'page', header: 'Page', width: pixel(160)},
325
+ {
326
+ key: 'views',
327
+ header: 'Views',
328
+ width: proportional(1),
329
+ renderCell: (item: PageRow) => (
330
+ <VStack gap={1}>
331
+ <ProgressBar
332
+ value={item.views}
333
+ max={topPagesMaxViews}
334
+ label={`${item.page} views`}
335
+ isLabelHidden
336
+ />
337
+ <Text type="supporting" hasTabularNumbers>
338
+ {item.views.toLocaleString()}
339
+ </Text>
340
+ </VStack>
341
+ ),
342
+ },
343
+ {
344
+ key: 'newUsers',
345
+ header: 'New Users',
346
+ width: pixel(120),
347
+ renderCell: (item: PageRow) => (
348
+ <Text hasTabularNumbers>{item.newUsers}</Text>
349
+ ),
350
+ },
351
+ {
352
+ key: 'avgTime',
353
+ header: 'Avg. Time',
354
+ width: pixel(120),
355
+ renderCell: (item: PageRow) => (
356
+ <Text hasTabularNumbers>{item.avgTime}</Text>
357
+ ),
358
+ },
359
+ ];
360
+
361
+ // Engagement — Top events
362
+ interface EventRow extends Record<string, unknown> {
363
+ id: string;
364
+ event: string;
365
+ count: number;
366
+ users: number;
367
+ newUsers: number;
368
+ }
369
+
370
+ const topEventsData: EventRow[] = [
371
+ {id: '1', event: 'page_view', count: 18420, users: 12300, newUsers: 4920},
372
+ {id: '2', event: 'session_start', count: 14850, users: 9870, newUsers: 3950},
373
+ {id: '3', event: 'first_visit', count: 8230, users: 8230, newUsers: 8230},
374
+ {id: '4', event: 'user_engagement', count: 6120, users: 4510, newUsers: 1580},
375
+ {id: '5', event: 'click', count: 3540, users: 2680, newUsers: 940},
376
+ {id: '6', event: 'scroll', count: 2910, users: 2140, newUsers: 750},
377
+ {id: '7', event: 'form_submit', count: 1870, users: 1350, newUsers: 540},
378
+ {id: '8', event: 'video_play', count: 1240, users: 980, newUsers: 390},
379
+ {id: '9', event: 'search', count: 960, users: 720, newUsers: 290},
380
+ {id: '10', event: 'share', count: 580, users: 410, newUsers: 160},
381
+ ];
382
+
383
+ const topEventsMaxCount = Math.max(...topEventsData.map(d => d.count));
384
+
385
+ const topEventsColumns: TableColumn<EventRow>[] = [
386
+ {key: 'event', header: 'Event', width: pixel(160)},
387
+ {
388
+ key: 'count',
389
+ header: 'Count',
390
+ width: proportional(1),
391
+ renderCell: (item: EventRow) => (
392
+ <VStack gap={1}>
393
+ <ProgressBar
394
+ value={item.count}
395
+ max={topEventsMaxCount}
396
+ label={`${item.count}`}
397
+ isLabelHidden
398
+ />
399
+ <Text type="supporting" hasTabularNumbers>
400
+ {item.count.toLocaleString()}
401
+ </Text>
402
+ </VStack>
403
+ ),
404
+ },
405
+ {
406
+ key: 'users',
407
+ header: 'Users',
408
+ width: pixel(120),
409
+ renderCell: (item: EventRow) => (
410
+ <Text hasTabularNumbers>{item.users.toLocaleString()}</Text>
411
+ ),
412
+ },
413
+ {
414
+ key: 'newUsers',
415
+ header: 'New Users',
416
+ width: pixel(120),
417
+ renderCell: (item: EventRow) => (
418
+ <Text hasTabularNumbers>{item.newUsers.toLocaleString()}</Text>
419
+ ),
420
+ },
421
+ ];
422
+
423
+ // ============= CHART COMPONENTS =============
424
+
425
+ // Chart series colors: desktop glows orange, mobile purple
426
+ const chartColors = {
427
+ desktop: 'var(--dracula-orange)',
428
+ mobile: 'var(--dracula-purple)',
429
+ };
430
+
431
+ function ChartLegendItem({color, label}: {color: string; label: string}) {
432
+ return (
433
+ <HStack gap={2} vAlign="center">
434
+ <Icon icon={Square} size="xsm" style={{color}} />
435
+ <Text type="supporting" color="secondary">
436
+ {label}
437
+ </Text>
438
+ </HStack>
439
+ );
440
+ }
441
+
442
+ function ActiveUsersChart() {
443
+ // Downsample the 96 quarter-hour points to one bar per hour, stacking
444
+ // mobile atop desktop so each bar total is all users
445
+ const bars = activeUsersData.filter((_, i) => i % 4 === 0);
446
+ const max = 130;
447
+ const tickHours = [0, 32, 64, 92];
448
+ // SVG type scales up with the viewBox, so unit sizes stay small
449
+ return (
450
+ <VStack gap={3}>
451
+ <Card
452
+ padding={3}
453
+ style={{
454
+ backgroundColor: 'var(--color-background)',
455
+ border: 'var(--border-width) solid var(--color-separator)',
456
+ }}>
457
+ <svg
458
+ viewBox="0 0 540 180"
459
+ width="100%"
460
+ role="img"
461
+ aria-label="Hourly active users, desktop and mobile stacked">
462
+ <line x1="20" y1="30" x2="520" y2="30" stroke="var(--color-separator)" strokeDasharray="3 3" opacity={0.5} />
463
+ <line x1="20" y1="80" x2="520" y2="80" stroke="var(--color-separator)" strokeDasharray="3 3" opacity={0.5} />
464
+ <line x1="20" y1="130" x2="520" y2="130" stroke="var(--color-separator)" />
465
+ {bars.map((d, i) => {
466
+ const hAll = (d.allUsers / max) * 110;
467
+ const hDesktop = (d.desktop / max) * 110;
468
+ const x = 32 + i * 21;
469
+ return (
470
+ <g key={d.hour}>
471
+ <rect
472
+ x={x}
473
+ y={130 - hAll}
474
+ width={14}
475
+ height={hAll - hDesktop}
476
+ rx={4}
477
+ fill={chartColors.mobile}
478
+ />
479
+ <rect
480
+ x={x}
481
+ y={130 - hDesktop}
482
+ width={14}
483
+ height={hDesktop}
484
+ rx={4}
485
+ fill={chartColors.desktop}
486
+ />
487
+ {(i % 6 === 0 || i === bars.length - 1) && (
488
+ <text
489
+ x={x + 7}
490
+ y={120 - hAll}
491
+ textAnchor="middle"
492
+ fontSize={5.5}
493
+ fill="var(--color-text-highlight)"
494
+ fontFamily="var(--font-family-mono)">
495
+ {d.allUsers}
496
+ </text>
497
+ )}
498
+ {tickHours.includes(d.hour) && (
499
+ <text
500
+ x={x + 7}
501
+ y={150}
502
+ textAnchor="middle"
503
+ fontSize={5.5}
504
+ fill="var(--color-text-paragraph)"
505
+ fontFamily="var(--font-family-mono)">
506
+ {d.label}
507
+ </text>
508
+ )}
509
+ </g>
510
+ );
511
+ })}
512
+ </svg>
513
+ </Card>
514
+ <Text type="supporting" color="secondary">
515
+ Hourly intervals · trailing 24 hours
516
+ </Text>
517
+ <HStack gap={6} vAlign="center">
518
+ <ChartLegendItem color={chartColors.desktop} label="Desktop" />
519
+ <ChartLegendItem color={chartColors.mobile} label="Mobile" />
520
+ </HStack>
521
+ </VStack>
522
+ );
523
+ }
524
+
525
+ function Sparkline({data, label}: {data: number[]; label: string}) {
526
+ const max = Math.max(...data);
527
+ return (
528
+ <svg
529
+ viewBox="0 0 300 40"
530
+ width="100%"
531
+ height={40}
532
+ role="img"
533
+ aria-label={`${label} thirty-day trend`}>
534
+ {data.map((v, i) => {
535
+ const h = Math.max(3, (v / max) * 32);
536
+ return (
537
+ <rect
538
+ key={i}
539
+ x={i * 10}
540
+ y={36 - h}
541
+ width={7}
542
+ height={h}
543
+ rx={4}
544
+ fill="var(--dracula-purple)"
545
+ />
546
+ );
547
+ })}
548
+ </svg>
549
+ );
550
+ }
551
+
552
+ // ============= CARD COMPONENTS =============
553
+
554
+ function MetricCard({
555
+ label,
556
+ value,
557
+ change,
558
+ positive,
559
+ sparkline,
560
+ }: {
561
+ label: string;
562
+ value: string;
563
+ change: string;
564
+ positive: boolean;
565
+ sparkline: number[];
566
+ }) {
567
+ return (
568
+ <Card>
569
+ <VStack gap={2}>
570
+ <Heading level={4}>{label}</Heading>
571
+ <HStack gap={2} vAlign="center">
572
+ <Heading level={2}>{value}</Heading>
573
+ <HStack gap={1} vAlign="center">
574
+ <Icon
575
+ icon={change.trim().startsWith('-') ? ArrowDown : ArrowUp}
576
+ size="xsm"
577
+ color={positive ? 'success' : 'error'}
578
+ />
579
+ <Text type="body" color="secondary" hasTabularNumbers>
580
+ {change}
581
+ </Text>
582
+ </HStack>
583
+ </HStack>
584
+ <Text type="supporting" color="secondary">
585
+ Last 30 days vs. Previous
586
+ </Text>
587
+ <Sparkline data={sparkline} label={label} />
588
+ </VStack>
589
+ </Card>
590
+ );
591
+ }
592
+
593
+ function StackedBarCard({
594
+ title,
595
+ data,
596
+ }: {
597
+ title: string;
598
+ data: Array<{label: string; value: number; color: string}>;
599
+ }) {
600
+ const total = data.reduce((sum, d) => sum + d.value, 0);
601
+ const clipId = `stacked-${title.toLowerCase().replace(/[^a-z]+/g, '-')}`;
602
+ let offset = 0;
603
+ const segments = data.map(d => {
604
+ const x = (offset / total) * 540;
605
+ offset += d.value;
606
+ return {...d, x, width: (d.value / total) * 540};
607
+ });
608
+
609
+ return (
610
+ <Card>
611
+ <VStack gap={4}>
612
+ <Heading level={4}>{title}</Heading>
613
+ <svg
614
+ viewBox="0 0 540 24"
615
+ width="100%"
616
+ height={24}
617
+ role="img"
618
+ aria-label={`${title} distribution strip`}>
619
+ <defs>
620
+ <clipPath id={clipId}>
621
+ <rect width={540} height={24} rx={4} />
622
+ </clipPath>
623
+ </defs>
624
+ <g clipPath={`url(#${clipId})`}>
625
+ {segments.map(s => (
626
+ <rect
627
+ key={s.label}
628
+ x={s.x}
629
+ y={0}
630
+ width={s.width}
631
+ height={24}
632
+ fill={s.color}
633
+ />
634
+ ))}
635
+ </g>
636
+ </svg>
637
+ {/* Legend */}
638
+ <HStack gap={4} wrap="wrap">
639
+ {data.map(d => (
640
+ <VStack key={d.label} gap={0}>
641
+ <HStack gap={2} vAlign="center">
642
+ <Icon icon={Square} size="xsm" style={{color: d.color}} />
643
+ <Text type="supporting">{d.label}</Text>
644
+ </HStack>
645
+ <Text type="supporting" color="secondary" hasTabularNumbers>
646
+ {((d.value / total) * 100).toFixed(0)}%
647
+ </Text>
648
+ </VStack>
649
+ ))}
650
+ </HStack>
651
+ </VStack>
652
+ </Card>
653
+ );
654
+ }
655
+
656
+ // ============= TABLE COMPONENTS =============
657
+
658
+ function TableCard<T extends {id: string}>({
659
+ title,
660
+ linkLabel,
661
+ linkHref,
662
+ data,
663
+ columns,
664
+ }: {
665
+ title: string;
666
+ linkLabel: string;
667
+ linkHref: string;
668
+ data: T[];
669
+ columns: TableColumn<T>[];
670
+ }) {
671
+ return (
672
+ <Card>
673
+ <VStack gap={6}>
674
+ <HStack hAlign="between" vAlign="center">
675
+ <Heading level={4}>{title}</Heading>
676
+ <Link href={linkHref}>{linkLabel}</Link>
677
+ </HStack>
678
+ <Table<T>
679
+ data={data}
680
+ columns={columns}
681
+ idKey="id"
682
+ density="compact"
683
+ dividers="rows"
684
+ hasHover
685
+ />
686
+ </VStack>
687
+ </Card>
688
+ );
689
+ }
690
+
691
+ // ============= SIDENAV =============
692
+
693
+ // ============= MAIN COMPONENT =============
694
+
695
+ export default function DashboardTemplate() {
696
+ return (
697
+ <Layout
698
+ height="fill"
699
+ content={
700
+ <LayoutContent padding={6}>
701
+ <VStack gap={6}>
702
+ {/* Active Users Chart */}
703
+ <VStack gap={6}>
704
+ <HStack hAlign="between" vAlign="center">
705
+ <Heading level={3}>Awake after dark</Heading>
706
+ <Button
707
+ label="Reload"
708
+ variant="secondary"
709
+ size="md"
710
+ icon={<Icon icon={RefreshCw} size="sm" />}
711
+ />
712
+ </HStack>
713
+ <ActiveUsersChart />
714
+ </VStack>
715
+
716
+ {/* Metric Cards */}
717
+ <Grid columns={{minWidth: 320, repeat: 'fit'}} gap={4}>
718
+ {[0, 2].map(start => (
719
+ <Grid
720
+ key={start}
721
+ columns={{minWidth: 240, repeat: 'fit'}}
722
+ gap={4}>
723
+ {metrics.slice(start, start + 2).map((m, i) => (
724
+ <MetricCard
725
+ key={m.label}
726
+ {...m}
727
+ sparkline={sparklines[start + i]}
728
+ />
729
+ ))}
730
+ </Grid>
731
+ ))}
732
+ </Grid>
733
+
734
+ <Divider />
735
+
736
+ {/* Demographics */}
737
+ <HStack hAlign="between" vAlign="center">
738
+ <Heading level={3}>Night denizens</Heading>
739
+ <Button label="View more" variant="secondary" size="md" />
740
+ </HStack>
741
+ <Grid columns={{minWidth: 320, repeat: 'fit'}} gap={4}>
742
+ <StackedBarCard title="Territory" data={regionData} />
743
+ <StackedBarCard title="Role" data={roleData} />
744
+ </Grid>
745
+
746
+ <Divider />
747
+
748
+ {/* Engagement */}
749
+ <HStack hAlign="between" vAlign="center">
750
+ <Heading level={3}>Engagement</Heading>
751
+ <Button label="View more" variant="secondary" size="md" />
752
+ </HStack>
753
+ <Grid columns={{minWidth: 320, repeat: 'fit'}} gap={4}>
754
+ <TableCard
755
+ title="Top pages"
756
+ linkLabel="All pages"
757
+ linkHref="#"
758
+ data={topPagesData}
759
+ columns={topPagesColumns}
760
+ />
761
+ <TableCard
762
+ title="Top events"
763
+ linkLabel="All events"
764
+ linkHref="#"
765
+ data={topEventsData}
766
+ columns={topEventsColumns}
767
+ />
768
+ </Grid>
769
+ </VStack>
770
+ </LayoutContent>
771
+ }
772
+ />
773
+ );
774
+ }