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.
- package/BRAND.md +3 -3
- package/README.md +7 -4
- package/USAGE.md +49 -49
- package/astryx-dracula.d.ts +2 -2
- package/astryx-dracula.js +25 -9
- package/astryx-dracula.variants.d.ts +2 -2
- package/astryx.integration.mjs +1 -1
- package/icons.mjs +21 -17
- package/icons.tsx +69 -62
- package/package.json +125 -120
- package/shared/auth-chrome-config.ts +6 -6
- package/shared/auth-chrome.tsx +143 -144
- package/shared/auth-copy.ts +14 -14
- package/shared/chaptered-doc-config.ts +48 -51
- package/shared/chaptered-doc.tsx +267 -289
- package/shared/chart-hues.ts +25 -10
- package/shared/chart-labels.tsx +31 -30
- package/shared/chart-legend.tsx +55 -56
- package/shared/chart-panel-style.ts +3 -3
- package/shared/data-bar.tsx +97 -102
- package/shared/format-money.ts +1 -1
- package/shared/gallery-image.ts +6 -6
- package/shared/login-demo.ts +1 -1
- package/shared/metric-card.tsx +101 -105
- package/shared/metric-delta.tsx +28 -27
- package/shared/order-desk.tsx +205 -216
- package/shared/revenue-chart.tsx +145 -165
- package/shared/scene-castle.tsx +206 -291
- package/shared/scene-frame.tsx +33 -32
- package/shared/scene-hues.ts +17 -17
- package/shared/scene-tile.tsx +88 -92
- package/shared/settings-data.ts +119 -125
- package/shared/settings-rows.tsx +122 -159
- package/shared/sparkline.tsx +75 -88
- package/shared/sso-icons.tsx +10 -22
- package/shared/stars.tsx +16 -25
- package/templates/ai-chat-landing.template.mjs +5 -4
- package/templates/ai-chat-landing.tsx +473 -460
- package/templates/ai-chat.template.mjs +5 -4
- package/templates/ai-chat.tsx +543 -612
- package/templates/blank.template.mjs +5 -4
- package/templates/blank.tsx +18 -14
- package/templates/centered-hero.template.mjs +5 -4
- package/templates/centered-hero.tsx +55 -68
- package/templates/classic-gallery.template.mjs +5 -4
- package/templates/classic-gallery.tsx +97 -104
- package/templates/contact-form.template.mjs +5 -4
- package/templates/contact-form.tsx +247 -282
- package/templates/dashboard-portfolio.template.mjs +5 -4
- package/templates/dashboard-portfolio.tsx +717 -548
- package/templates/dashboard.template.mjs +5 -4
- package/templates/dashboard.tsx +596 -567
- package/templates/detail-page.template.mjs +5 -4
- package/templates/detail-page.tsx +546 -603
- package/templates/documentation-design.template.mjs +5 -4
- package/templates/documentation-design.tsx +231 -243
- package/templates/documentation-technical.template.mjs +5 -4
- package/templates/documentation-technical.tsx +236 -239
- package/templates/documentation.template.mjs +5 -4
- package/templates/documentation.tsx +267 -279
- package/templates/editor.template.mjs +5 -4
- package/templates/editor.tsx +844 -877
- package/templates/file-explorer.template.mjs +5 -4
- package/templates/file-explorer.tsx +566 -590
- package/templates/form-two-column.template.mjs +5 -4
- package/templates/form-two-column.tsx +195 -210
- package/templates/gallery-hero.template.mjs +5 -4
- package/templates/gallery-hero.tsx +93 -114
- package/templates/ide.template.mjs +5 -4
- package/templates/ide.tsx +407 -453
- package/templates/incident-console.template.mjs +5 -4
- package/templates/incident-console.tsx +593 -575
- package/templates/kanban-board.template.mjs +5 -4
- package/templates/kanban-board.tsx +665 -683
- package/templates/library.template.mjs +5 -4
- package/templates/library.tsx +409 -430
- package/templates/login-card.template.mjs +5 -4
- package/templates/login-card.tsx +63 -72
- package/templates/login-split.template.mjs +5 -4
- package/templates/login-split.tsx +192 -193
- package/templates/login-sso.template.mjs +5 -4
- package/templates/login-sso.tsx +257 -258
- package/templates/login.template.mjs +5 -4
- package/templates/login.tsx +53 -49
- package/templates/messaging-shell.template.mjs +5 -4
- package/templates/messaging-shell.tsx +556 -574
- package/templates/mixed-gallery.template.mjs +5 -4
- package/templates/mixed-gallery.tsx +60 -58
- package/templates/payment-form.template.mjs +5 -4
- package/templates/payment-form.tsx +882 -991
- package/templates/product-detail.template.mjs +5 -4
- package/templates/product-detail.tsx +261 -272
- package/templates/product-gallery.template.mjs +5 -4
- package/templates/product-gallery.tsx +126 -137
- package/templates/product-tour.template.mjs +5 -4
- package/templates/product-tour.tsx +741 -773
- package/templates/settings-dialog.template.mjs +5 -4
- package/templates/settings-dialog.tsx +688 -717
- package/templates/settings-sidebar.template.mjs +5 -4
- package/templates/settings-sidebar.tsx +762 -807
- package/templates/settings.template.mjs +5 -4
- package/templates/settings.tsx +289 -304
- package/templates/shell-nav.template.mjs +5 -4
- package/templates/shell-nav.tsx +212 -219
- package/templates/shell-side-nav.template.mjs +5 -4
- package/templates/shell-side-nav.tsx +135 -157
- package/templates/shell-top-nav.template.mjs +5 -4
- package/templates/shell-top-nav.tsx +182 -203
- package/templates/side-gallery.template.mjs +5 -4
- package/templates/side-gallery.tsx +85 -90
- package/templates/table-grouped.template.mjs +5 -4
- package/templates/table-grouped.tsx +1000 -1053
- package/templates/table-page-chart.template.mjs +5 -4
- package/templates/table-page-chart.tsx +400 -398
- package/templates/table-page-heatmap-status.template.mjs +5 -4
- package/templates/table-page-heatmap-status.tsx +483 -512
- package/templates/table-page-shoe-store-heatmap.template.mjs +5 -4
- package/templates/table-page-shoe-store-heatmap.tsx +748 -746
- package/templates/table-page.template.mjs +5 -4
- package/templates/table-page.tsx +396 -405
- package/templates/table.template.mjs +5 -4
- package/templates/table.tsx +92 -93
- package/templates/tech-report.template.mjs +5 -4
- package/templates/tech-report.tsx +1316 -1304
- package/templates/theme-showcase.template.mjs +5 -4
- package/templates/theme-showcase.tsx +871 -951
- package/theme.css +35 -77
- package/theme.css.d.ts +9 -0
- package/tokens.css +373 -380
package/templates/dashboard.tsx
CHANGED
|
@@ -1,422 +1,436 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
// XLE (canonical structure
|
|
3
|
-
//
|
|
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
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
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 =
|
|
24
|
+
const SELF_HASH = "#/templates/dashboard";
|
|
23
25
|
|
|
24
26
|
// ============= ICONS =============
|
|
25
27
|
|
|
26
|
-
import {
|
|
27
|
-
import {ChartLegend} from
|
|
28
|
-
import {
|
|
29
|
-
import
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
194
|
+
return values.map((value, day) => ({ id: `day-${day + 1}`, value }));
|
|
193
195
|
}
|
|
194
196
|
|
|
195
197
|
const sparklines: SparkPoint[][] = [
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
|
|
515
|
-
|
|
547
|
+
title,
|
|
548
|
+
data,
|
|
516
549
|
}: {
|
|
517
|
-
|
|
518
|
-
|
|
550
|
+
title: string;
|
|
551
|
+
data: Array<{ label: string; value: number; color: string }>;
|
|
519
552
|
}) {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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:
|
|
618
|
+
const pageStyle: CSSProperties = { height: "100dvh" };
|
|
590
619
|
|
|
591
620
|
export default function DashboardTemplate() {
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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
|
}
|