astryx-dracula 0.1.5 → 0.2.1

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