astryx-dracula 0.2.1 → 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 +17 -12
- 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 +66 -33
- package/templates/ai-chat.tsx +36 -19
- package/templates/blank.tsx +4 -3
- package/templates/centered-hero.tsx +14 -125
- package/templates/classic-gallery.tsx +20 -66
- package/templates/contact-form.tsx +59 -39
- package/templates/dashboard-portfolio.tsx +143 -226
- package/templates/dashboard.tsx +122 -122
- package/templates/detail-page.tsx +95 -91
- package/templates/documentation-design.template.mjs +1 -1
- package/templates/documentation-design.tsx +83 -96
- package/templates/documentation-technical.template.mjs +1 -1
- package/templates/documentation-technical.tsx +29 -19
- package/templates/documentation.template.mjs +1 -1
- package/templates/documentation.tsx +52 -74
- package/templates/editor.tsx +112 -84
- package/templates/file-explorer.tsx +131 -84
- package/templates/form-two-column.tsx +61 -125
- package/templates/gallery-hero.tsx +15 -171
- package/templates/ide.tsx +22 -20
- package/templates/incident-console.tsx +104 -70
- package/templates/kanban-board.tsx +99 -42
- package/templates/library.template.mjs +2 -2
- package/templates/library.tsx +37 -42
- package/templates/login-card.tsx +58 -87
- package/templates/login-split.tsx +88 -126
- package/templates/login-sso.tsx +105 -64
- package/templates/login.tsx +107 -53
- package/templates/messaging-shell.tsx +171 -144
- package/templates/mixed-gallery.tsx +29 -130
- package/templates/payment-form.tsx +368 -290
- package/templates/product-detail.tsx +39 -58
- package/templates/product-gallery.tsx +30 -41
- package/templates/product-tour.template.mjs +6 -0
- package/templates/product-tour.tsx +916 -0
- package/templates/settings-dialog.tsx +135 -282
- package/templates/settings-sidebar.tsx +178 -268
- package/templates/settings.tsx +41 -14
- package/templates/shell-nav.tsx +31 -24
- package/templates/shell-side-nav.tsx +29 -20
- package/templates/shell-top-nav.tsx +80 -44
- package/templates/side-gallery.tsx +38 -94
- package/templates/table-grouped.tsx +62 -34
- package/templates/table-page-chart.tsx +23 -302
- package/templates/table-page-heatmap-status.tsx +156 -92
- package/templates/table-page-shoe-store-heatmap.tsx +18 -293
- package/templates/table-page.tsx +23 -7
- package/templates/table.tsx +23 -8
- 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 +299 -237
- package/theme.css +76 -14
- package/tokens.css +425 -30
package/templates/dashboard.tsx
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
// XLE (canonical structure, validated with `bunx astryx layout check`):
|
|
3
|
-
// L > LC[p=6] > V[g=6] > (V[g=6] > (H[j=between a=center] > Hd"Awake after dark"[level=
|
|
3
|
+
// 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)
|
|
4
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';
|
|
5
10
|
import {VStack, HStack, Layout, LayoutContent} from '@astryxdesign/core/Layout';
|
|
6
11
|
import {Text, Heading} from '@astryxdesign/core/Text';
|
|
7
12
|
import {Card} from '@astryxdesign/core/Card';
|
|
@@ -16,7 +21,12 @@ import {Icon} from '@astryxdesign/core/Icon';
|
|
|
16
21
|
|
|
17
22
|
// ============= ICONS =============
|
|
18
23
|
|
|
19
|
-
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';
|
|
20
30
|
|
|
21
31
|
// ============= DATA =============
|
|
22
32
|
|
|
@@ -173,20 +183,26 @@ const metrics = [
|
|
|
173
183
|
},
|
|
174
184
|
];
|
|
175
185
|
|
|
176
|
-
// Sparkline data for each metric card (30 days, weekends at indices 5-6, 12-13,
|
|
177
|
-
|
|
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[][] = [
|
|
178
194
|
// Monthly Visitors: +18.2% — declining first 2 weeks, hits bottom around day 14, then sharp recovery
|
|
179
195
|
// prettier-ignore
|
|
180
|
-
[48, 46, 44, 42, 40, 18, 16, 38, 36, 34, 32, 30, 12, 10, 28, 26, 28, 32, 36, 14, 12, 40, 44, 48, 52, 56, 28, 24, 58, 62],
|
|
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]),
|
|
181
197
|
// Monthly Page Views: +12.5% — flat/choppy first 3 weeks, then kicks up sharply in final week
|
|
182
198
|
// prettier-ignore
|
|
183
|
-
[36, 38, 35, 37, 36, 14, 12, 38, 36, 34, 37, 35, 12, 10, 36, 34, 36, 35, 38, 14, 12, 40, 44, 50, 54, 56, 26, 22, 58, 60],
|
|
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]),
|
|
184
200
|
// Avg. Session: -14.3% — strong start, holds through week 2, then clear drop-off week 3-4
|
|
185
201
|
// prettier-ignore
|
|
186
|
-
[58, 56, 60, 58, 62, 30, 26, 60, 58, 62, 60, 58, 28, 24, 56, 54, 50, 46, 42, 18, 14, 38, 36, 34, 32, 30, 10, 8, 28, 26],
|
|
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]),
|
|
187
203
|
// Bounce Rate: -8.7% — high and volatile first half, starts dropping around day 16, steady decline
|
|
188
204
|
// prettier-ignore
|
|
189
|
-
[52, 56, 50, 54, 58, 62, 60, 54, 52, 56, 50, 54, 60, 58, 50, 48, 46, 44, 40, 46, 44, 38, 36, 34, 36, 32, 38, 36, 30, 28],
|
|
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]),
|
|
190
206
|
];
|
|
191
207
|
|
|
192
208
|
// Demographics
|
|
@@ -194,48 +210,48 @@ const regionData = [
|
|
|
194
210
|
{
|
|
195
211
|
label: 'NORAM',
|
|
196
212
|
value: 38,
|
|
197
|
-
color:
|
|
213
|
+
color: CHART_HUES.cyan,
|
|
198
214
|
},
|
|
199
215
|
{
|
|
200
216
|
label: 'EMEA',
|
|
201
217
|
value: 28,
|
|
202
|
-
color:
|
|
218
|
+
color: CHART_HUES.orange,
|
|
203
219
|
},
|
|
204
220
|
{
|
|
205
221
|
label: 'APAC',
|
|
206
222
|
value: 22,
|
|
207
|
-
color:
|
|
223
|
+
color: CHART_HUES.green,
|
|
208
224
|
},
|
|
209
225
|
{
|
|
210
226
|
label: 'LATAM',
|
|
211
227
|
value: 8,
|
|
212
|
-
color:
|
|
228
|
+
color: CHART_HUES.pink,
|
|
213
229
|
},
|
|
214
|
-
{label: 'Other', value: 4, color:
|
|
230
|
+
{label: 'Other', value: 4, color: CHART_HUES.muted},
|
|
215
231
|
];
|
|
216
232
|
|
|
217
233
|
const roleData = [
|
|
218
234
|
{
|
|
219
235
|
label: 'Engineer',
|
|
220
236
|
value: 45,
|
|
221
|
-
color:
|
|
237
|
+
color: CHART_HUES.cyan,
|
|
222
238
|
},
|
|
223
239
|
{
|
|
224
240
|
label: 'Manager',
|
|
225
241
|
value: 20,
|
|
226
|
-
color:
|
|
242
|
+
color: CHART_HUES.orange,
|
|
227
243
|
},
|
|
228
244
|
{
|
|
229
245
|
label: 'Designer',
|
|
230
246
|
value: 15,
|
|
231
|
-
color:
|
|
247
|
+
color: CHART_HUES.green,
|
|
232
248
|
},
|
|
233
249
|
{
|
|
234
250
|
label: 'Data Scientist',
|
|
235
251
|
value: 12,
|
|
236
|
-
color:
|
|
252
|
+
color: CHART_HUES.pink,
|
|
237
253
|
},
|
|
238
|
-
{label: 'Other', value: 8, color:
|
|
254
|
+
{label: 'Other', value: 8, color: CHART_HUES.muted},
|
|
239
255
|
];
|
|
240
256
|
|
|
241
257
|
// Engagement — Top pages
|
|
@@ -320,10 +336,13 @@ const topPagesData: PageRow[] = [
|
|
|
320
336
|
},
|
|
321
337
|
];
|
|
322
338
|
|
|
339
|
+
// Counts render with thousands separators in every table cell.
|
|
340
|
+
const formatCount = (value: number) => value.toLocaleString();
|
|
341
|
+
|
|
323
342
|
const topPagesMaxViews = Math.max(...topPagesData.map(d => d.views));
|
|
324
343
|
|
|
325
344
|
const topPagesColumns: TableColumn<PageRow>[] = [
|
|
326
|
-
{key: 'page', header: 'Page', width: pixel(
|
|
345
|
+
{key: 'page', header: 'Page', width: pixel(112)},
|
|
327
346
|
{
|
|
328
347
|
key: 'views',
|
|
329
348
|
header: 'Views',
|
|
@@ -336,8 +355,8 @@ const topPagesColumns: TableColumn<PageRow>[] = [
|
|
|
336
355
|
label={`${item.page} views`}
|
|
337
356
|
isLabelHidden
|
|
338
357
|
/>
|
|
339
|
-
<Text type="
|
|
340
|
-
{item.views
|
|
358
|
+
<Text type="body" hasTabularNumbers maxLines={1}>
|
|
359
|
+
{formatCount(item.views)}
|
|
341
360
|
</Text>
|
|
342
361
|
</VStack>
|
|
343
362
|
),
|
|
@@ -345,17 +364,21 @@ const topPagesColumns: TableColumn<PageRow>[] = [
|
|
|
345
364
|
{
|
|
346
365
|
key: 'newUsers',
|
|
347
366
|
header: 'New Users',
|
|
348
|
-
width: pixel(
|
|
367
|
+
width: pixel(104),
|
|
349
368
|
renderCell: (item: PageRow) => (
|
|
350
|
-
<Text hasTabularNumbers
|
|
369
|
+
<Text hasTabularNumbers maxLines={1}>
|
|
370
|
+
{item.newUsers}
|
|
371
|
+
</Text>
|
|
351
372
|
),
|
|
352
373
|
},
|
|
353
374
|
{
|
|
354
375
|
key: 'avgTime',
|
|
355
376
|
header: 'Avg. Time',
|
|
356
|
-
width: pixel(
|
|
377
|
+
width: pixel(104),
|
|
357
378
|
renderCell: (item: PageRow) => (
|
|
358
|
-
<Text hasTabularNumbers
|
|
379
|
+
<Text hasTabularNumbers maxLines={1}>
|
|
380
|
+
{item.avgTime}
|
|
381
|
+
</Text>
|
|
359
382
|
),
|
|
360
383
|
},
|
|
361
384
|
];
|
|
@@ -385,7 +408,7 @@ const topEventsData: EventRow[] = [
|
|
|
385
408
|
const topEventsMaxCount = Math.max(...topEventsData.map(d => d.count));
|
|
386
409
|
|
|
387
410
|
const topEventsColumns: TableColumn<EventRow>[] = [
|
|
388
|
-
{key: 'event', header: 'Event', width: pixel(
|
|
411
|
+
{key: 'event', header: 'Event', width: pixel(112)},
|
|
389
412
|
{
|
|
390
413
|
key: 'count',
|
|
391
414
|
header: 'Count',
|
|
@@ -398,8 +421,8 @@ const topEventsColumns: TableColumn<EventRow>[] = [
|
|
|
398
421
|
label={`${item.count}`}
|
|
399
422
|
isLabelHidden
|
|
400
423
|
/>
|
|
401
|
-
<Text type="
|
|
402
|
-
{item.count
|
|
424
|
+
<Text type="body" hasTabularNumbers maxLines={1}>
|
|
425
|
+
{formatCount(item.count)}
|
|
403
426
|
</Text>
|
|
404
427
|
</VStack>
|
|
405
428
|
),
|
|
@@ -407,39 +430,36 @@ const topEventsColumns: TableColumn<EventRow>[] = [
|
|
|
407
430
|
{
|
|
408
431
|
key: 'users',
|
|
409
432
|
header: 'Users',
|
|
410
|
-
width: pixel(
|
|
433
|
+
width: pixel(72),
|
|
411
434
|
renderCell: (item: EventRow) => (
|
|
412
|
-
<Text hasTabularNumbers
|
|
435
|
+
<Text hasTabularNumbers maxLines={1}>
|
|
436
|
+
{formatCount(item.users)}
|
|
437
|
+
</Text>
|
|
413
438
|
),
|
|
414
439
|
},
|
|
415
440
|
{
|
|
416
441
|
key: 'newUsers',
|
|
417
442
|
header: 'New Users',
|
|
418
|
-
width: pixel(
|
|
443
|
+
width: pixel(104),
|
|
419
444
|
renderCell: (item: EventRow) => (
|
|
420
|
-
<Text hasTabularNumbers
|
|
445
|
+
<Text hasTabularNumbers maxLines={1}>
|
|
446
|
+
{formatCount(item.newUsers)}
|
|
447
|
+
</Text>
|
|
421
448
|
),
|
|
422
449
|
},
|
|
423
450
|
];
|
|
424
451
|
|
|
425
452
|
// ============= CHART COMPONENTS =============
|
|
426
453
|
|
|
427
|
-
// 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.
|
|
428
456
|
const chartColors = {
|
|
429
|
-
desktop:
|
|
430
|
-
mobile:
|
|
457
|
+
desktop: CHART_HUES.orange,
|
|
458
|
+
mobile: CHART_HUES.cyan,
|
|
431
459
|
};
|
|
432
460
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
<HStack gap={2} vAlign="center">
|
|
436
|
-
<Icon icon={Square} size="xsm" style={{color}} />
|
|
437
|
-
<Text type="supporting" color="secondary">
|
|
438
|
-
{label}
|
|
439
|
-
</Text>
|
|
440
|
-
</HStack>
|
|
441
|
-
);
|
|
442
|
-
}
|
|
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.
|
|
443
463
|
|
|
444
464
|
function ActiveUsersChart() {
|
|
445
465
|
// Downsample the 96 quarter-hour points to one bar per hour, stacking
|
|
@@ -447,15 +467,11 @@ function ActiveUsersChart() {
|
|
|
447
467
|
const bars = activeUsersData.filter((_, i) => i % 4 === 0);
|
|
448
468
|
const max = 130;
|
|
449
469
|
const tickHours = [0, 32, 64, 92];
|
|
450
|
-
// 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.
|
|
451
472
|
return (
|
|
452
473
|
<VStack gap={3}>
|
|
453
|
-
<Card
|
|
454
|
-
padding={3}
|
|
455
|
-
style={{
|
|
456
|
-
backgroundColor: 'var(--color-background)',
|
|
457
|
-
border: 'var(--border-width) solid var(--color-separator)',
|
|
458
|
-
}}>
|
|
474
|
+
<Card padding={4} style={CHART_PANEL_STYLE}>
|
|
459
475
|
<svg
|
|
460
476
|
viewBox="0 0 540 180"
|
|
461
477
|
width="100%"
|
|
@@ -513,44 +529,22 @@ function ActiveUsersChart() {
|
|
|
513
529
|
})}
|
|
514
530
|
</svg>
|
|
515
531
|
</Card>
|
|
516
|
-
<
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
+
/>
|
|
523
544
|
</VStack>
|
|
524
545
|
);
|
|
525
546
|
}
|
|
526
547
|
|
|
527
|
-
function Sparkline({data, label}: {data: number[]; label: string}) {
|
|
528
|
-
const max = Math.max(...data);
|
|
529
|
-
return (
|
|
530
|
-
<svg
|
|
531
|
-
viewBox="0 0 300 40"
|
|
532
|
-
width="100%"
|
|
533
|
-
height={40}
|
|
534
|
-
role="img"
|
|
535
|
-
aria-label={`${label} thirty-day trend`}>
|
|
536
|
-
{data.map((v, i) => {
|
|
537
|
-
const h = Math.max(3, (v / max) * 32);
|
|
538
|
-
return (
|
|
539
|
-
<rect
|
|
540
|
-
key={i}
|
|
541
|
-
x={i * 10}
|
|
542
|
-
y={36 - h}
|
|
543
|
-
width={7}
|
|
544
|
-
height={h}
|
|
545
|
-
rx={4}
|
|
546
|
-
fill="var(--dracula-purple)"
|
|
547
|
-
/>
|
|
548
|
-
);
|
|
549
|
-
})}
|
|
550
|
-
</svg>
|
|
551
|
-
);
|
|
552
|
-
}
|
|
553
|
-
|
|
554
548
|
// ============= CARD COMPONENTS =============
|
|
555
549
|
|
|
556
550
|
function MetricCard({
|
|
@@ -564,29 +558,28 @@ function MetricCard({
|
|
|
564
558
|
value: string;
|
|
565
559
|
change: string;
|
|
566
560
|
positive: boolean;
|
|
567
|
-
sparkline:
|
|
561
|
+
sparkline: SparkPoint[];
|
|
568
562
|
}) {
|
|
569
563
|
return (
|
|
570
564
|
<Card>
|
|
571
565
|
<VStack gap={2}>
|
|
572
|
-
<
|
|
566
|
+
<Text type="supporting" color="secondary">
|
|
567
|
+
{label}
|
|
568
|
+
</Text>
|
|
573
569
|
<HStack gap={2} vAlign="center">
|
|
574
|
-
<
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
size="xsm"
|
|
579
|
-
color={positive ? 'success' : 'error'}
|
|
580
|
-
/>
|
|
581
|
-
<Text type="body" color="secondary" hasTabularNumbers>
|
|
582
|
-
{change}
|
|
583
|
-
</Text>
|
|
584
|
-
</HStack>
|
|
570
|
+
<Text type="display-3" weight="semibold" hasTabularNumbers>
|
|
571
|
+
{value}
|
|
572
|
+
</Text>
|
|
573
|
+
<MetricDelta value={change} positive={positive} />
|
|
585
574
|
</HStack>
|
|
586
575
|
<Text type="supporting" color="secondary">
|
|
587
576
|
Last 30 days vs. Previous
|
|
588
577
|
</Text>
|
|
589
|
-
<Sparkline
|
|
578
|
+
<Sparkline
|
|
579
|
+
data={sparkline}
|
|
580
|
+
label={`${label} thirty-day trend`}
|
|
581
|
+
positive={positive}
|
|
582
|
+
/>
|
|
590
583
|
</VStack>
|
|
591
584
|
</Card>
|
|
592
585
|
);
|
|
@@ -611,7 +604,7 @@ function StackedBarCard({
|
|
|
611
604
|
return (
|
|
612
605
|
<Card>
|
|
613
606
|
<VStack gap={4}>
|
|
614
|
-
<Heading level={
|
|
607
|
+
<Heading level={3}>{title}</Heading>
|
|
615
608
|
<svg
|
|
616
609
|
viewBox="0 0 540 24"
|
|
617
610
|
width="100%"
|
|
@@ -642,7 +635,7 @@ function StackedBarCard({
|
|
|
642
635
|
<VStack key={d.label} gap={0}>
|
|
643
636
|
<HStack gap={2} vAlign="center">
|
|
644
637
|
<Icon icon={Square} size="xsm" style={{color: d.color}} />
|
|
645
|
-
<Text type="supporting">{d.label}</Text>
|
|
638
|
+
<Text type="supporting" color="secondary">{d.label}</Text>
|
|
646
639
|
</HStack>
|
|
647
640
|
<Text type="supporting" color="secondary" hasTabularNumbers>
|
|
648
641
|
{((d.value / total) * 100).toFixed(0)}%
|
|
@@ -674,7 +667,7 @@ function TableCard<T extends {id: string}>({
|
|
|
674
667
|
<Card>
|
|
675
668
|
<VStack gap={6}>
|
|
676
669
|
<HStack hAlign="between" vAlign="center">
|
|
677
|
-
<Heading level={
|
|
670
|
+
<Heading level={3}>{title}</Heading>
|
|
678
671
|
<Link href={linkHref}>{linkLabel}</Link>
|
|
679
672
|
</HStack>
|
|
680
673
|
<Table<T>
|
|
@@ -683,6 +676,7 @@ function TableCard<T extends {id: string}>({
|
|
|
683
676
|
idKey="id"
|
|
684
677
|
density="compact"
|
|
685
678
|
dividers="rows"
|
|
679
|
+
textOverflow="truncate"
|
|
686
680
|
hasHover
|
|
687
681
|
/>
|
|
688
682
|
</VStack>
|
|
@@ -694,17 +688,34 @@ function TableCard<T extends {id: string}>({
|
|
|
694
688
|
|
|
695
689
|
// ============= MAIN COMPONENT =============
|
|
696
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
|
+
|
|
697
704
|
export default function DashboardTemplate() {
|
|
698
705
|
return (
|
|
699
706
|
<Layout
|
|
707
|
+
style={pageStyle}
|
|
700
708
|
height="fill"
|
|
701
709
|
content={
|
|
702
710
|
<LayoutContent padding={6}>
|
|
703
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
|
+
|
|
704
715
|
{/* Active Users Chart */}
|
|
705
716
|
<VStack gap={6}>
|
|
706
717
|
<HStack hAlign="between" vAlign="center">
|
|
707
|
-
<Heading level={
|
|
718
|
+
<Heading level={2}>Awake after dark</Heading>
|
|
708
719
|
<Button
|
|
709
720
|
label="Reload"
|
|
710
721
|
variant="secondary"
|
|
@@ -716,20 +727,9 @@ export default function DashboardTemplate() {
|
|
|
716
727
|
</VStack>
|
|
717
728
|
|
|
718
729
|
{/* Metric Cards */}
|
|
719
|
-
<Grid columns={{minWidth:
|
|
720
|
-
{
|
|
721
|
-
<
|
|
722
|
-
key={start}
|
|
723
|
-
columns={{minWidth: 240, repeat: 'fit'}}
|
|
724
|
-
gap={4}>
|
|
725
|
-
{metrics.slice(start, start + 2).map((m, i) => (
|
|
726
|
-
<MetricCard
|
|
727
|
-
key={m.label}
|
|
728
|
-
{...m}
|
|
729
|
-
sparkline={sparklines[start + i]}
|
|
730
|
-
/>
|
|
731
|
-
))}
|
|
732
|
-
</Grid>
|
|
730
|
+
<Grid columns={{minWidth: 280, repeat: 'fit'}} gap={4}>
|
|
731
|
+
{metrics.map((m, index) => (
|
|
732
|
+
<MetricCard key={m.label} {...m} sparkline={sparklines[index]} />
|
|
733
733
|
))}
|
|
734
734
|
</Grid>
|
|
735
735
|
|
|
@@ -737,10 +737,10 @@ export default function DashboardTemplate() {
|
|
|
737
737
|
|
|
738
738
|
{/* Demographics */}
|
|
739
739
|
<HStack hAlign="between" vAlign="center">
|
|
740
|
-
<Heading level={
|
|
740
|
+
<Heading level={2}>Night denizens</Heading>
|
|
741
741
|
<Button label="View more" variant="secondary" size="md" />
|
|
742
742
|
</HStack>
|
|
743
|
-
<Grid columns={{minWidth:
|
|
743
|
+
<Grid columns={{minWidth: 280, repeat: 'fit'}} gap={4}>
|
|
744
744
|
<StackedBarCard title="Territory" data={regionData} />
|
|
745
745
|
<StackedBarCard title="Role" data={roleData} />
|
|
746
746
|
</Grid>
|
|
@@ -749,10 +749,10 @@ export default function DashboardTemplate() {
|
|
|
749
749
|
|
|
750
750
|
{/* Engagement */}
|
|
751
751
|
<HStack hAlign="between" vAlign="center">
|
|
752
|
-
<Heading level={
|
|
752
|
+
<Heading level={2}>Engagement</Heading>
|
|
753
753
|
<Button label="View more" variant="secondary" size="md" />
|
|
754
754
|
</HStack>
|
|
755
|
-
<Grid columns={{minWidth:
|
|
755
|
+
<Grid columns={{minWidth: 280, repeat: 'fit'}} gap={4}>
|
|
756
756
|
<TableCard
|
|
757
757
|
title="Top pages"
|
|
758
758
|
linkLabel="All pages"
|