astryx-dracula 0.5.0 → 0.6.2

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