astryx-dracula 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BRAND.md +30 -18
- package/README.md +22 -11
- package/USAGE.md +29 -10
- package/astryx-dracula.js +19 -13
- package/astryx-dracula.variants.d.ts +15 -0
- package/package.json +59 -17
- package/shared/auth-chrome-config.ts +18 -0
- package/shared/auth-chrome.tsx +25 -0
- package/shared/auth-copy.ts +22 -0
- package/shared/chaptered-doc-config.ts +92 -0
- package/shared/chaptered-doc.tsx +332 -0
- package/shared/chart-hues.ts +27 -0
- package/shared/chart-labels.tsx +44 -0
- package/shared/chart-legend.tsx +100 -0
- package/shared/chart-panel-style.ts +13 -0
- package/shared/data-bar.tsx +160 -0
- package/shared/gallery-image.ts +20 -0
- package/shared/login-demo.ts +7 -0
- package/shared/metric-delta.tsx +36 -0
- package/shared/order-desk.tsx +273 -0
- package/shared/revenue-chart.tsx +174 -0
- package/shared/scene-castle.tsx +322 -0
- package/shared/scene-frame.tsx +88 -0
- package/shared/scene-hues.ts +49 -0
- package/shared/scene-tile.tsx +128 -0
- package/shared/settings-data.ts +125 -0
- package/shared/settings-rows.tsx +179 -0
- package/shared/sparkline.tsx +94 -0
- package/shared/sso-icons.tsx +30 -0
- package/templates/ai-chat-landing.tsx +67 -32
- package/templates/ai-chat.tsx +41 -20
- package/templates/blank.tsx +5 -2
- package/templates/centered-hero.tsx +15 -124
- package/templates/classic-gallery.tsx +21 -65
- package/templates/contact-form.tsx +62 -40
- package/templates/dashboard-portfolio.tsx +146 -227
- package/templates/dashboard.tsx +123 -121
- package/templates/detail-page.tsx +101 -95
- package/templates/documentation-design.template.mjs +1 -1
- package/templates/documentation-design.tsx +85 -96
- package/templates/documentation-technical.template.mjs +1 -1
- package/templates/documentation-technical.tsx +33 -21
- package/templates/documentation.template.mjs +1 -1
- package/templates/documentation.tsx +54 -74
- package/templates/editor.tsx +119 -89
- package/templates/file-explorer.tsx +133 -84
- package/templates/form-two-column.tsx +62 -124
- package/templates/gallery-hero.tsx +17 -171
- package/templates/ide.tsx +27 -21
- package/templates/incident-console.tsx +105 -69
- package/templates/kanban-board.tsx +100 -41
- package/templates/library.template.mjs +2 -2
- package/templates/library.tsx +38 -41
- package/templates/login-card.tsx +59 -86
- package/templates/login-split.tsx +88 -128
- package/templates/login-sso.tsx +107 -68
- package/templates/login.tsx +108 -52
- package/templates/messaging-shell.tsx +172 -143
- package/templates/mixed-gallery.tsx +31 -130
- package/templates/payment-form.tsx +369 -289
- package/templates/product-detail.tsx +40 -57
- package/templates/product-gallery.tsx +31 -40
- package/templates/product-tour.template.mjs +6 -0
- package/templates/product-tour.tsx +916 -0
- package/templates/settings-dialog.tsx +145 -286
- package/templates/settings-sidebar.tsx +179 -267
- package/templates/settings.tsx +42 -13
- package/templates/shell-nav.tsx +32 -23
- package/templates/shell-side-nav.tsx +30 -19
- package/templates/shell-top-nav.tsx +81 -43
- package/templates/side-gallery.tsx +39 -93
- package/templates/table-grouped.tsx +63 -33
- package/templates/table-page-chart.tsx +24 -297
- package/templates/table-page-heatmap-status.tsx +157 -91
- package/templates/table-page-shoe-store-heatmap.tsx +19 -288
- package/templates/table-page.tsx +24 -6
- package/templates/table.tsx +24 -7
- package/templates/tech-report.template.mjs +6 -0
- package/templates/tech-report.tsx +1486 -0
- package/templates/theme-showcase.template.mjs +1 -1
- package/templates/theme-showcase.tsx +313 -249
- package/theme.css +76 -14
- package/tokens.css +425 -30
package/templates/dashboard.tsx
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
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
4
|
|
|
5
|
+
/**
|
|
6
|
+
* 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
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {CSSProperties} from 'react';
|
|
3
10
|
import {VStack, HStack, Layout, LayoutContent} from '@astryxdesign/core/Layout';
|
|
4
11
|
import {Text, Heading} from '@astryxdesign/core/Text';
|
|
5
12
|
import {Card} from '@astryxdesign/core/Card';
|
|
@@ -14,7 +21,12 @@ import {Icon} from '@astryxdesign/core/Icon';
|
|
|
14
21
|
|
|
15
22
|
// ============= ICONS =============
|
|
16
23
|
|
|
17
|
-
import {RefreshCw,
|
|
24
|
+
import {RefreshCw, Square} from 'lucide-react';
|
|
25
|
+
import {ChartLegend} from 'astryx-dracula/shared/chart-legend';
|
|
26
|
+
import {CHART_HUES} from 'astryx-dracula/shared/chart-hues';
|
|
27
|
+
import {MetricDelta} from 'astryx-dracula/shared/metric-delta';
|
|
28
|
+
import {Sparkline, type SparkPoint} from 'astryx-dracula/shared/sparkline';
|
|
29
|
+
import {CHART_PANEL_STYLE} from 'astryx-dracula/shared/chart-panel-style';
|
|
18
30
|
|
|
19
31
|
// ============= DATA =============
|
|
20
32
|
|
|
@@ -171,20 +183,26 @@ const metrics = [
|
|
|
171
183
|
},
|
|
172
184
|
];
|
|
173
185
|
|
|
174
|
-
// Sparkline data for each metric card (30 days, weekends at indices 5-6, 12-13,
|
|
175
|
-
|
|
186
|
+
// Sparkline data for each metric card (30 days, weekends at indices 5-6, 12-13,
|
|
187
|
+
// 19-20, 26-27). Every sample carries its day, so the bars key off the series
|
|
188
|
+
// rather than off the array slot.
|
|
189
|
+
function toSparkSeries(values: number[]): SparkPoint[] {
|
|
190
|
+
return values.map((value, day) => ({id: `day-${day + 1}`, value}));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const sparklines: SparkPoint[][] = [
|
|
176
194
|
// Monthly Visitors: +18.2% — declining first 2 weeks, hits bottom around day 14, then sharp recovery
|
|
177
195
|
// prettier-ignore
|
|
178
|
-
[48, 46, 44, 42, 40, 18, 16, 38, 36, 34, 32, 30, 12, 10, 28, 26, 28, 32, 36, 14, 12, 40, 44, 48, 52, 56, 28, 24, 58, 62],
|
|
196
|
+
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]),
|
|
179
197
|
// Monthly Page Views: +12.5% — flat/choppy first 3 weeks, then kicks up sharply in final week
|
|
180
198
|
// prettier-ignore
|
|
181
|
-
[36, 38, 35, 37, 36, 14, 12, 38, 36, 34, 37, 35, 12, 10, 36, 34, 36, 35, 38, 14, 12, 40, 44, 50, 54, 56, 26, 22, 58, 60],
|
|
199
|
+
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]),
|
|
182
200
|
// Avg. Session: -14.3% — strong start, holds through week 2, then clear drop-off week 3-4
|
|
183
201
|
// prettier-ignore
|
|
184
|
-
[58, 56, 60, 58, 62, 30, 26, 60, 58, 62, 60, 58, 28, 24, 56, 54, 50, 46, 42, 18, 14, 38, 36, 34, 32, 30, 10, 8, 28, 26],
|
|
202
|
+
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]),
|
|
185
203
|
// Bounce Rate: -8.7% — high and volatile first half, starts dropping around day 16, steady decline
|
|
186
204
|
// prettier-ignore
|
|
187
|
-
[52, 56, 50, 54, 58, 62, 60, 54, 52, 56, 50, 54, 60, 58, 50, 48, 46, 44, 40, 46, 44, 38, 36, 34, 36, 32, 38, 36, 30, 28],
|
|
205
|
+
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]),
|
|
188
206
|
];
|
|
189
207
|
|
|
190
208
|
// Demographics
|
|
@@ -192,48 +210,48 @@ const regionData = [
|
|
|
192
210
|
{
|
|
193
211
|
label: 'NORAM',
|
|
194
212
|
value: 38,
|
|
195
|
-
color:
|
|
213
|
+
color: CHART_HUES.cyan,
|
|
196
214
|
},
|
|
197
215
|
{
|
|
198
216
|
label: 'EMEA',
|
|
199
217
|
value: 28,
|
|
200
|
-
color:
|
|
218
|
+
color: CHART_HUES.orange,
|
|
201
219
|
},
|
|
202
220
|
{
|
|
203
221
|
label: 'APAC',
|
|
204
222
|
value: 22,
|
|
205
|
-
color:
|
|
223
|
+
color: CHART_HUES.green,
|
|
206
224
|
},
|
|
207
225
|
{
|
|
208
226
|
label: 'LATAM',
|
|
209
227
|
value: 8,
|
|
210
|
-
color:
|
|
228
|
+
color: CHART_HUES.pink,
|
|
211
229
|
},
|
|
212
|
-
{label: 'Other', value: 4, color:
|
|
230
|
+
{label: 'Other', value: 4, color: CHART_HUES.muted},
|
|
213
231
|
];
|
|
214
232
|
|
|
215
233
|
const roleData = [
|
|
216
234
|
{
|
|
217
235
|
label: 'Engineer',
|
|
218
236
|
value: 45,
|
|
219
|
-
color:
|
|
237
|
+
color: CHART_HUES.cyan,
|
|
220
238
|
},
|
|
221
239
|
{
|
|
222
240
|
label: 'Manager',
|
|
223
241
|
value: 20,
|
|
224
|
-
color:
|
|
242
|
+
color: CHART_HUES.orange,
|
|
225
243
|
},
|
|
226
244
|
{
|
|
227
245
|
label: 'Designer',
|
|
228
246
|
value: 15,
|
|
229
|
-
color:
|
|
247
|
+
color: CHART_HUES.green,
|
|
230
248
|
},
|
|
231
249
|
{
|
|
232
250
|
label: 'Data Scientist',
|
|
233
251
|
value: 12,
|
|
234
|
-
color:
|
|
252
|
+
color: CHART_HUES.pink,
|
|
235
253
|
},
|
|
236
|
-
{label: 'Other', value: 8, color:
|
|
254
|
+
{label: 'Other', value: 8, color: CHART_HUES.muted},
|
|
237
255
|
];
|
|
238
256
|
|
|
239
257
|
// Engagement — Top pages
|
|
@@ -318,10 +336,13 @@ const topPagesData: PageRow[] = [
|
|
|
318
336
|
},
|
|
319
337
|
];
|
|
320
338
|
|
|
339
|
+
// Counts render with thousands separators in every table cell.
|
|
340
|
+
const formatCount = (value: number) => value.toLocaleString();
|
|
341
|
+
|
|
321
342
|
const topPagesMaxViews = Math.max(...topPagesData.map(d => d.views));
|
|
322
343
|
|
|
323
344
|
const topPagesColumns: TableColumn<PageRow>[] = [
|
|
324
|
-
{key: 'page', header: 'Page', width: pixel(
|
|
345
|
+
{key: 'page', header: 'Page', width: pixel(112)},
|
|
325
346
|
{
|
|
326
347
|
key: 'views',
|
|
327
348
|
header: 'Views',
|
|
@@ -334,8 +355,8 @@ const topPagesColumns: TableColumn<PageRow>[] = [
|
|
|
334
355
|
label={`${item.page} views`}
|
|
335
356
|
isLabelHidden
|
|
336
357
|
/>
|
|
337
|
-
<Text type="
|
|
338
|
-
{item.views
|
|
358
|
+
<Text type="body" hasTabularNumbers maxLines={1}>
|
|
359
|
+
{formatCount(item.views)}
|
|
339
360
|
</Text>
|
|
340
361
|
</VStack>
|
|
341
362
|
),
|
|
@@ -343,17 +364,21 @@ const topPagesColumns: TableColumn<PageRow>[] = [
|
|
|
343
364
|
{
|
|
344
365
|
key: 'newUsers',
|
|
345
366
|
header: 'New Users',
|
|
346
|
-
width: pixel(
|
|
367
|
+
width: pixel(104),
|
|
347
368
|
renderCell: (item: PageRow) => (
|
|
348
|
-
<Text hasTabularNumbers
|
|
369
|
+
<Text hasTabularNumbers maxLines={1}>
|
|
370
|
+
{item.newUsers}
|
|
371
|
+
</Text>
|
|
349
372
|
),
|
|
350
373
|
},
|
|
351
374
|
{
|
|
352
375
|
key: 'avgTime',
|
|
353
376
|
header: 'Avg. Time',
|
|
354
|
-
width: pixel(
|
|
377
|
+
width: pixel(104),
|
|
355
378
|
renderCell: (item: PageRow) => (
|
|
356
|
-
<Text hasTabularNumbers
|
|
379
|
+
<Text hasTabularNumbers maxLines={1}>
|
|
380
|
+
{item.avgTime}
|
|
381
|
+
</Text>
|
|
357
382
|
),
|
|
358
383
|
},
|
|
359
384
|
];
|
|
@@ -383,7 +408,7 @@ const topEventsData: EventRow[] = [
|
|
|
383
408
|
const topEventsMaxCount = Math.max(...topEventsData.map(d => d.count));
|
|
384
409
|
|
|
385
410
|
const topEventsColumns: TableColumn<EventRow>[] = [
|
|
386
|
-
{key: 'event', header: 'Event', width: pixel(
|
|
411
|
+
{key: 'event', header: 'Event', width: pixel(112)},
|
|
387
412
|
{
|
|
388
413
|
key: 'count',
|
|
389
414
|
header: 'Count',
|
|
@@ -396,8 +421,8 @@ const topEventsColumns: TableColumn<EventRow>[] = [
|
|
|
396
421
|
label={`${item.count}`}
|
|
397
422
|
isLabelHidden
|
|
398
423
|
/>
|
|
399
|
-
<Text type="
|
|
400
|
-
{item.count
|
|
424
|
+
<Text type="body" hasTabularNumbers maxLines={1}>
|
|
425
|
+
{formatCount(item.count)}
|
|
401
426
|
</Text>
|
|
402
427
|
</VStack>
|
|
403
428
|
),
|
|
@@ -405,39 +430,36 @@ const topEventsColumns: TableColumn<EventRow>[] = [
|
|
|
405
430
|
{
|
|
406
431
|
key: 'users',
|
|
407
432
|
header: 'Users',
|
|
408
|
-
width: pixel(
|
|
433
|
+
width: pixel(72),
|
|
409
434
|
renderCell: (item: EventRow) => (
|
|
410
|
-
<Text hasTabularNumbers
|
|
435
|
+
<Text hasTabularNumbers maxLines={1}>
|
|
436
|
+
{formatCount(item.users)}
|
|
437
|
+
</Text>
|
|
411
438
|
),
|
|
412
439
|
},
|
|
413
440
|
{
|
|
414
441
|
key: 'newUsers',
|
|
415
442
|
header: 'New Users',
|
|
416
|
-
width: pixel(
|
|
443
|
+
width: pixel(104),
|
|
417
444
|
renderCell: (item: EventRow) => (
|
|
418
|
-
<Text hasTabularNumbers
|
|
445
|
+
<Text hasTabularNumbers maxLines={1}>
|
|
446
|
+
{formatCount(item.newUsers)}
|
|
447
|
+
</Text>
|
|
419
448
|
),
|
|
420
449
|
},
|
|
421
450
|
];
|
|
422
451
|
|
|
423
452
|
// ============= CHART COMPONENTS =============
|
|
424
453
|
|
|
425
|
-
// Chart series colors: desktop glows orange, mobile
|
|
454
|
+
// Chart series colors: desktop glows orange, mobile glows cyan. Sourced from
|
|
455
|
+
// the shared categorical hues so data encoding can never reach for purple.
|
|
426
456
|
const chartColors = {
|
|
427
|
-
desktop:
|
|
428
|
-
mobile:
|
|
457
|
+
desktop: CHART_HUES.orange,
|
|
458
|
+
mobile: CHART_HUES.cyan,
|
|
429
459
|
};
|
|
430
460
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
<HStack gap={2} vAlign="center">
|
|
434
|
-
<Icon icon={Square} size="xsm" style={{color}} />
|
|
435
|
-
<Text type="supporting" color="secondary">
|
|
436
|
-
{label}
|
|
437
|
-
</Text>
|
|
438
|
-
</HStack>
|
|
439
|
-
);
|
|
440
|
-
}
|
|
461
|
+
// Legend rows go through shared/chart-legend.tsx, which makes `label` a
|
|
462
|
+
// REQUIRED field so a hue can never ship without the word that names it.
|
|
441
463
|
|
|
442
464
|
function ActiveUsersChart() {
|
|
443
465
|
// Downsample the 96 quarter-hour points to one bar per hour, stacking
|
|
@@ -445,15 +467,11 @@ function ActiveUsersChart() {
|
|
|
445
467
|
const bars = activeUsersData.filter((_, i) => i % 4 === 0);
|
|
446
468
|
const max = 130;
|
|
447
469
|
const tickHours = [0, 32, 64, 92];
|
|
448
|
-
// SVG type scales up with the viewBox, so unit sizes stay small
|
|
470
|
+
// SVG type scales up with the viewBox, so unit sizes stay small: raw <text>
|
|
471
|
+
// (not ChartLabel) — 13px labels would render ~2.4x oversize in this 540-wide viewBox.
|
|
449
472
|
return (
|
|
450
473
|
<VStack gap={3}>
|
|
451
|
-
<Card
|
|
452
|
-
padding={3}
|
|
453
|
-
style={{
|
|
454
|
-
backgroundColor: 'var(--color-background)',
|
|
455
|
-
border: 'var(--border-width) solid var(--color-separator)',
|
|
456
|
-
}}>
|
|
474
|
+
<Card padding={4} style={CHART_PANEL_STYLE}>
|
|
457
475
|
<svg
|
|
458
476
|
viewBox="0 0 540 180"
|
|
459
477
|
width="100%"
|
|
@@ -511,44 +529,22 @@ function ActiveUsersChart() {
|
|
|
511
529
|
})}
|
|
512
530
|
</svg>
|
|
513
531
|
</Card>
|
|
514
|
-
<
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
532
|
+
<ChartLegend
|
|
533
|
+
entries={[
|
|
534
|
+
{label: 'Desktop', color: chartColors.desktop},
|
|
535
|
+
{label: 'Mobile', color: chartColors.mobile},
|
|
536
|
+
]}
|
|
537
|
+
gap={4}
|
|
538
|
+
caption={
|
|
539
|
+
<Text type="supporting" color="secondary">
|
|
540
|
+
Hourly intervals · trailing 24 hours
|
|
541
|
+
</Text>
|
|
542
|
+
}
|
|
543
|
+
/>
|
|
521
544
|
</VStack>
|
|
522
545
|
);
|
|
523
546
|
}
|
|
524
547
|
|
|
525
|
-
function Sparkline({data, label}: {data: number[]; label: string}) {
|
|
526
|
-
const max = Math.max(...data);
|
|
527
|
-
return (
|
|
528
|
-
<svg
|
|
529
|
-
viewBox="0 0 300 40"
|
|
530
|
-
width="100%"
|
|
531
|
-
height={40}
|
|
532
|
-
role="img"
|
|
533
|
-
aria-label={`${label} thirty-day trend`}>
|
|
534
|
-
{data.map((v, i) => {
|
|
535
|
-
const h = Math.max(3, (v / max) * 32);
|
|
536
|
-
return (
|
|
537
|
-
<rect
|
|
538
|
-
key={i}
|
|
539
|
-
x={i * 10}
|
|
540
|
-
y={36 - h}
|
|
541
|
-
width={7}
|
|
542
|
-
height={h}
|
|
543
|
-
rx={4}
|
|
544
|
-
fill="var(--dracula-purple)"
|
|
545
|
-
/>
|
|
546
|
-
);
|
|
547
|
-
})}
|
|
548
|
-
</svg>
|
|
549
|
-
);
|
|
550
|
-
}
|
|
551
|
-
|
|
552
548
|
// ============= CARD COMPONENTS =============
|
|
553
549
|
|
|
554
550
|
function MetricCard({
|
|
@@ -562,29 +558,28 @@ function MetricCard({
|
|
|
562
558
|
value: string;
|
|
563
559
|
change: string;
|
|
564
560
|
positive: boolean;
|
|
565
|
-
sparkline:
|
|
561
|
+
sparkline: SparkPoint[];
|
|
566
562
|
}) {
|
|
567
563
|
return (
|
|
568
564
|
<Card>
|
|
569
565
|
<VStack gap={2}>
|
|
570
|
-
<
|
|
566
|
+
<Text type="supporting" color="secondary">
|
|
567
|
+
{label}
|
|
568
|
+
</Text>
|
|
571
569
|
<HStack gap={2} vAlign="center">
|
|
572
|
-
<
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
size="xsm"
|
|
577
|
-
color={positive ? 'success' : 'error'}
|
|
578
|
-
/>
|
|
579
|
-
<Text type="body" color="secondary" hasTabularNumbers>
|
|
580
|
-
{change}
|
|
581
|
-
</Text>
|
|
582
|
-
</HStack>
|
|
570
|
+
<Text type="display-3" weight="semibold" hasTabularNumbers>
|
|
571
|
+
{value}
|
|
572
|
+
</Text>
|
|
573
|
+
<MetricDelta value={change} positive={positive} />
|
|
583
574
|
</HStack>
|
|
584
575
|
<Text type="supporting" color="secondary">
|
|
585
576
|
Last 30 days vs. Previous
|
|
586
577
|
</Text>
|
|
587
|
-
<Sparkline
|
|
578
|
+
<Sparkline
|
|
579
|
+
data={sparkline}
|
|
580
|
+
label={`${label} thirty-day trend`}
|
|
581
|
+
positive={positive}
|
|
582
|
+
/>
|
|
588
583
|
</VStack>
|
|
589
584
|
</Card>
|
|
590
585
|
);
|
|
@@ -609,7 +604,7 @@ function StackedBarCard({
|
|
|
609
604
|
return (
|
|
610
605
|
<Card>
|
|
611
606
|
<VStack gap={4}>
|
|
612
|
-
<Heading level={
|
|
607
|
+
<Heading level={3}>{title}</Heading>
|
|
613
608
|
<svg
|
|
614
609
|
viewBox="0 0 540 24"
|
|
615
610
|
width="100%"
|
|
@@ -640,7 +635,7 @@ function StackedBarCard({
|
|
|
640
635
|
<VStack key={d.label} gap={0}>
|
|
641
636
|
<HStack gap={2} vAlign="center">
|
|
642
637
|
<Icon icon={Square} size="xsm" style={{color: d.color}} />
|
|
643
|
-
<Text type="supporting">{d.label}</Text>
|
|
638
|
+
<Text type="supporting" color="secondary">{d.label}</Text>
|
|
644
639
|
</HStack>
|
|
645
640
|
<Text type="supporting" color="secondary" hasTabularNumbers>
|
|
646
641
|
{((d.value / total) * 100).toFixed(0)}%
|
|
@@ -672,7 +667,7 @@ function TableCard<T extends {id: string}>({
|
|
|
672
667
|
<Card>
|
|
673
668
|
<VStack gap={6}>
|
|
674
669
|
<HStack hAlign="between" vAlign="center">
|
|
675
|
-
<Heading level={
|
|
670
|
+
<Heading level={3}>{title}</Heading>
|
|
676
671
|
<Link href={linkHref}>{linkLabel}</Link>
|
|
677
672
|
</HStack>
|
|
678
673
|
<Table<T>
|
|
@@ -681,6 +676,7 @@ function TableCard<T extends {id: string}>({
|
|
|
681
676
|
idKey="id"
|
|
682
677
|
density="compact"
|
|
683
678
|
dividers="rows"
|
|
679
|
+
textOverflow="truncate"
|
|
684
680
|
hasHover
|
|
685
681
|
/>
|
|
686
682
|
</VStack>
|
|
@@ -692,17 +688,34 @@ function TableCard<T extends {id: string}>({
|
|
|
692
688
|
|
|
693
689
|
// ============= MAIN COMPONENT =============
|
|
694
690
|
|
|
691
|
+
// A definite ancestor, so `Layout height="fill"` (Layout.tsx:58-63, height:
|
|
692
|
+
// calc(100% + ...)) has something definite to resolve against instead of
|
|
693
|
+
// collapsing to auto and handing the scroll to the document. 100dvh rather
|
|
694
|
+
// than minHeight: '100%' -- a percentage min-height against an indefinite
|
|
695
|
+
// containing block computes to 0, so the soft failure would survive. The
|
|
696
|
+
// anchor is on the Layout, not a wrapper: LayoutContent is overflow:auto
|
|
697
|
+
// (LayoutContent.tsx:36-39), so a definite height is what makes the content
|
|
698
|
+
// pane SCROLL rather than clip. Same shape as editor.tsx:302 and
|
|
699
|
+
// kanban-board.tsx:704. Not applied to shell-nav / shell-side-nav, which
|
|
700
|
+
// already sit inside an AppShell's own 100dvh and would overflow by the
|
|
701
|
+
// header height.
|
|
702
|
+
const pageStyle: CSSProperties = {height: '100dvh'};
|
|
703
|
+
|
|
695
704
|
export default function DashboardTemplate() {
|
|
696
705
|
return (
|
|
697
706
|
<Layout
|
|
707
|
+
style={pageStyle}
|
|
698
708
|
height="fill"
|
|
699
709
|
content={
|
|
700
710
|
<LayoutContent padding={6}>
|
|
701
711
|
<VStack gap={6}>
|
|
712
|
+
{/* Page header — the only h1; section titles sit at level 2 */}
|
|
713
|
+
<Heading level={1}>The night shift</Heading>
|
|
714
|
+
|
|
702
715
|
{/* Active Users Chart */}
|
|
703
716
|
<VStack gap={6}>
|
|
704
717
|
<HStack hAlign="between" vAlign="center">
|
|
705
|
-
<Heading level={
|
|
718
|
+
<Heading level={2}>Awake after dark</Heading>
|
|
706
719
|
<Button
|
|
707
720
|
label="Reload"
|
|
708
721
|
variant="secondary"
|
|
@@ -714,20 +727,9 @@ export default function DashboardTemplate() {
|
|
|
714
727
|
</VStack>
|
|
715
728
|
|
|
716
729
|
{/* Metric Cards */}
|
|
717
|
-
<Grid columns={{minWidth:
|
|
718
|
-
{
|
|
719
|
-
<
|
|
720
|
-
key={start}
|
|
721
|
-
columns={{minWidth: 240, repeat: 'fit'}}
|
|
722
|
-
gap={4}>
|
|
723
|
-
{metrics.slice(start, start + 2).map((m, i) => (
|
|
724
|
-
<MetricCard
|
|
725
|
-
key={m.label}
|
|
726
|
-
{...m}
|
|
727
|
-
sparkline={sparklines[start + i]}
|
|
728
|
-
/>
|
|
729
|
-
))}
|
|
730
|
-
</Grid>
|
|
730
|
+
<Grid columns={{minWidth: 280, repeat: 'fit'}} gap={4}>
|
|
731
|
+
{metrics.map((m, index) => (
|
|
732
|
+
<MetricCard key={m.label} {...m} sparkline={sparklines[index]} />
|
|
731
733
|
))}
|
|
732
734
|
</Grid>
|
|
733
735
|
|
|
@@ -735,10 +737,10 @@ export default function DashboardTemplate() {
|
|
|
735
737
|
|
|
736
738
|
{/* Demographics */}
|
|
737
739
|
<HStack hAlign="between" vAlign="center">
|
|
738
|
-
<Heading level={
|
|
740
|
+
<Heading level={2}>Night denizens</Heading>
|
|
739
741
|
<Button label="View more" variant="secondary" size="md" />
|
|
740
742
|
</HStack>
|
|
741
|
-
<Grid columns={{minWidth:
|
|
743
|
+
<Grid columns={{minWidth: 280, repeat: 'fit'}} gap={4}>
|
|
742
744
|
<StackedBarCard title="Territory" data={regionData} />
|
|
743
745
|
<StackedBarCard title="Role" data={roleData} />
|
|
744
746
|
</Grid>
|
|
@@ -747,10 +749,10 @@ export default function DashboardTemplate() {
|
|
|
747
749
|
|
|
748
750
|
{/* Engagement */}
|
|
749
751
|
<HStack hAlign="between" vAlign="center">
|
|
750
|
-
<Heading level={
|
|
752
|
+
<Heading level={2}>Engagement</Heading>
|
|
751
753
|
<Button label="View more" variant="secondary" size="md" />
|
|
752
754
|
</HStack>
|
|
753
|
-
<Grid columns={{minWidth:
|
|
755
|
+
<Grid columns={{minWidth: 280, repeat: 'fit'}} gap={4}>
|
|
754
756
|
<TableCard
|
|
755
757
|
title="Top pages"
|
|
756
758
|
linkLabel="All pages"
|