astryx-dracula 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BRAND.md +30 -18
- package/README.md +17 -12
- package/USAGE.md +30 -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 +181 -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 +102 -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 +433 -30
|
@@ -1,24 +1,29 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
// XLE (canonical structure, validated with `bunx astryx layout check`):
|
|
3
|
-
// L > LC > V[g=6] > (H[j=between a=center] > Hd"The night vault"[level=1] + DM"1 year") + (G[c=
|
|
3
|
+
// L[h=fill] > LC[p=6] > V[g=6] > (H[j=between a=center] > Hd"The night vault"[level=1] + DM"1 year") + (G[c={min:280} g=4] > (C > V[g=2] > Tx"Total value"[t=supporting] + (H[g=2] > Tx"$294,200"[t=display-3] + Tx"+14.8%"[t=body]))*4) + (G[c={min:280} g=4] > (C > V[g=4] > (H[j=between] > Hd"Vault value"[level=2] + Lk"View details") + (C > V[g=3] > Tx"Weekly closes"[t=supporting])) + (V[g=4] > (H[j=between] > Hd"Top holdings"[level=2] + Lk"View all") + UL)) + D + (H[j=between a=start] > (V[g=1] > Hd"Market at midnight"[level=2] + Tx"Past 24 hours under moonlight"[t=body]) + B"View more") + (G[c={min:280} g=4] > (C > V[g=3] > Hd"Index"[level=3] + Tx"$5,200"[t=body])*8) + (C > V[g=4] > Hd"Trending stocks"[level=3] + T)
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Portfolio Dashboard — the night vault: KPI tiles, a weekly value chart, the holdings list, and the market board. (Frame/responsive/container: see XLE header above.)
|
|
7
|
+
*/
|
|
4
8
|
|
|
5
9
|
import {useState} from 'react';
|
|
6
10
|
|
|
7
11
|
import {VStack, HStack, Layout, LayoutContent} from '@astryxdesign/core/Layout';
|
|
8
12
|
import {Text, Heading} from '@astryxdesign/core/Text';
|
|
9
13
|
import {Card} from '@astryxdesign/core/Card';
|
|
10
|
-
import {Grid
|
|
11
|
-
import {Icon} from '@astryxdesign/core/Icon';
|
|
14
|
+
import {Grid} from '@astryxdesign/core/Grid';
|
|
12
15
|
import {Link} from '@astryxdesign/core/Link';
|
|
13
16
|
import {Avatar} from '@astryxdesign/core/Avatar';
|
|
14
17
|
import {List, ListItem} from '@astryxdesign/core/List';
|
|
15
18
|
import {DropdownMenu} from '@astryxdesign/core/DropdownMenu';
|
|
16
|
-
import {Badge} from '@astryxdesign/core/Badge';
|
|
17
19
|
import {Button} from '@astryxdesign/core/Button';
|
|
18
|
-
import {Table, proportional} from '@astryxdesign/core/Table';
|
|
20
|
+
import {Table, proportional, pixel} from '@astryxdesign/core/Table';
|
|
19
21
|
import type {TableColumn} from '@astryxdesign/core/Table';
|
|
20
22
|
import {Divider} from '@astryxdesign/core/Divider';
|
|
21
|
-
import {
|
|
23
|
+
import {MetricDelta} from 'astryx-dracula/shared/metric-delta';
|
|
24
|
+
import {Sparkline, type SparkPoint} from 'astryx-dracula/shared/sparkline';
|
|
25
|
+
import {CHART_PANEL_STYLE} from 'astryx-dracula/shared/chart-panel-style';
|
|
26
|
+
import {ChartLabel} from 'astryx-dracula/shared/chart-labels';
|
|
22
27
|
|
|
23
28
|
// ============= DATA =============
|
|
24
29
|
|
|
@@ -137,20 +142,22 @@ const topAssets = [
|
|
|
137
142
|
];
|
|
138
143
|
|
|
139
144
|
// 96 points per series = one tick every 15 minutes across a 24h window.
|
|
140
|
-
// Deterministic LCG so the sparklines are stable across renders.
|
|
145
|
+
// Deterministic LCG so the sparklines are stable across renders. Each point
|
|
146
|
+
// carries the slot it was generated for, so the bars key off data rather than
|
|
147
|
+
// off the array position.
|
|
141
148
|
function genSpark(
|
|
142
149
|
seed: number,
|
|
143
150
|
start: number,
|
|
144
151
|
end: number,
|
|
145
152
|
volatility: number,
|
|
146
153
|
N: number = 96,
|
|
147
|
-
):
|
|
154
|
+
): SparkPoint[] {
|
|
148
155
|
let s = seed >>> 0;
|
|
149
156
|
const rand = () => {
|
|
150
157
|
s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
|
|
151
158
|
return s / 0x100000000;
|
|
152
159
|
};
|
|
153
|
-
const points:
|
|
160
|
+
const points: SparkPoint[] = [];
|
|
154
161
|
let drift = 0;
|
|
155
162
|
for (let i = 0; i < N; i++) {
|
|
156
163
|
const t = i / (N - 1);
|
|
@@ -165,7 +172,7 @@ function genSpark(
|
|
|
165
172
|
drift = drift * 0.55 + (rand() - 0.5) * volatility * 2.2;
|
|
166
173
|
// Occasional sharper spike to mimic news-driven moves.
|
|
167
174
|
const spike = rand() < 0.04 ? (rand() - 0.5) * volatility * 4 : 0;
|
|
168
|
-
points.push(trend + wave + drift + spike);
|
|
175
|
+
points.push({id: `${seed}-${i}`, value: trend + wave + drift + spike});
|
|
169
176
|
}
|
|
170
177
|
return points;
|
|
171
178
|
}
|
|
@@ -246,7 +253,7 @@ interface StockRow extends Record<string, unknown> {
|
|
|
246
253
|
dailyPts: number;
|
|
247
254
|
dailyPct: number;
|
|
248
255
|
weekChg: number;
|
|
249
|
-
spark:
|
|
256
|
+
spark: SparkPoint[];
|
|
250
257
|
}
|
|
251
258
|
|
|
252
259
|
const trendingStocks: StockRow[] = [
|
|
@@ -344,61 +351,55 @@ const trendingStocks: StockRow[] = [
|
|
|
344
351
|
|
|
345
352
|
// ============= CHART COMPONENTS =============
|
|
346
353
|
|
|
354
|
+
// Downsample the 407 daily points to one bar per week
|
|
355
|
+
const portfolioBars = portfolioData.filter((_, i) => i % 7 === 0);
|
|
356
|
+
const PORTFOLIO_MIN = 200000;
|
|
357
|
+
const PORTFOLIO_MAX = 320000;
|
|
358
|
+
const PORTFOLIO_BASE = 168;
|
|
359
|
+
const PORTFOLIO_PLOT = 138;
|
|
360
|
+
const PORTFOLIO_LEFT = 52;
|
|
361
|
+
const PORTFOLIO_WIDTH = 500;
|
|
362
|
+
const PORTFOLIO_STEP = PORTFOLIO_WIDTH / portfolioBars.length;
|
|
363
|
+
const portfolioYFor = (v: number) =>
|
|
364
|
+
PORTFOLIO_BASE - ((v - PORTFOLIO_MIN) / (PORTFOLIO_MAX - PORTFOLIO_MIN)) * PORTFOLIO_PLOT;
|
|
365
|
+
const PORTFOLIO_Y_TICKS = [200000, 240000, 280000, 320000];
|
|
366
|
+
|
|
347
367
|
function PortfolioChart() {
|
|
348
|
-
// Downsample the 407 daily points to one bar per week
|
|
349
|
-
const bars = portfolioData.filter((_, i) => i % 7 === 0);
|
|
350
|
-
const min = 200000;
|
|
351
|
-
const max = 320000;
|
|
352
|
-
const base = 168;
|
|
353
|
-
const plot = 138;
|
|
354
|
-
const left = 52;
|
|
355
|
-
const width = 500;
|
|
356
|
-
const step = width / bars.length;
|
|
357
|
-
const yFor = (v: number) => base - ((v - min) / (max - min)) * plot;
|
|
358
|
-
const yTicks = [200000, 240000, 280000, 320000];
|
|
359
368
|
return (
|
|
360
369
|
<VStack gap={3}>
|
|
361
|
-
<Card
|
|
362
|
-
padding={3}
|
|
363
|
-
style={{
|
|
364
|
-
backgroundColor: 'var(--color-background)',
|
|
365
|
-
border: 'var(--border-width) solid var(--color-separator)',
|
|
366
|
-
}}>
|
|
370
|
+
<Card padding={4} style={CHART_PANEL_STYLE}>
|
|
367
371
|
<svg
|
|
368
372
|
viewBox="0 0 560 210"
|
|
369
373
|
width="100%"
|
|
370
374
|
role="img"
|
|
371
375
|
aria-label="Vault value by week, October to October">
|
|
372
|
-
{
|
|
376
|
+
{PORTFOLIO_Y_TICKS.map(t => (
|
|
373
377
|
<g key={t}>
|
|
374
378
|
<line
|
|
375
|
-
x1={
|
|
376
|
-
y1={
|
|
377
|
-
x2={
|
|
378
|
-
y2={
|
|
379
|
+
x1={PORTFOLIO_LEFT}
|
|
380
|
+
y1={portfolioYFor(t)}
|
|
381
|
+
x2={PORTFOLIO_LEFT + PORTFOLIO_WIDTH}
|
|
382
|
+
y2={portfolioYFor(t)}
|
|
379
383
|
stroke="var(--color-separator)"
|
|
380
384
|
strokeDasharray="3 3"
|
|
381
385
|
opacity={0.5}
|
|
382
386
|
/>
|
|
383
|
-
<
|
|
384
|
-
x={
|
|
385
|
-
y={
|
|
386
|
-
textAnchor="end"
|
|
387
|
-
fontSize={13}
|
|
388
|
-
fill="var(--color-text-paragraph)"
|
|
389
|
-
fontFamily="var(--font-family-mono)">
|
|
387
|
+
<ChartLabel
|
|
388
|
+
x={PORTFOLIO_LEFT - 8}
|
|
389
|
+
y={portfolioYFor(t) + 4}
|
|
390
|
+
textAnchor="end">
|
|
390
391
|
${(t / 1000).toFixed(0)}k
|
|
391
|
-
</
|
|
392
|
+
</ChartLabel>
|
|
392
393
|
</g>
|
|
393
394
|
))}
|
|
394
|
-
{
|
|
395
|
-
const h = Math.max(3,
|
|
395
|
+
{portfolioBars.map((d, i) => {
|
|
396
|
+
const h = Math.max(3, PORTFOLIO_BASE - portfolioYFor(d.value));
|
|
396
397
|
return (
|
|
397
398
|
<rect
|
|
398
|
-
key={
|
|
399
|
-
x={
|
|
400
|
-
y={
|
|
401
|
-
width={Math.max(2,
|
|
399
|
+
key={d.label}
|
|
400
|
+
x={PORTFOLIO_LEFT + i * PORTFOLIO_STEP + 1}
|
|
401
|
+
y={PORTFOLIO_BASE - h}
|
|
402
|
+
width={Math.max(2, PORTFOLIO_STEP - 2)}
|
|
402
403
|
height={h}
|
|
403
404
|
rx={4}
|
|
404
405
|
fill="var(--dracula-green)"
|
|
@@ -406,26 +407,21 @@ function PortfolioChart() {
|
|
|
406
407
|
);
|
|
407
408
|
})}
|
|
408
409
|
{xAxisTicks.map(m => (
|
|
409
|
-
<
|
|
410
|
+
<ChartLabel
|
|
410
411
|
key={m}
|
|
411
|
-
x={
|
|
412
|
+
x={PORTFOLIO_LEFT + (m / 12) * PORTFOLIO_WIDTH}
|
|
412
413
|
y={192}
|
|
413
|
-
textAnchor={m === 12 ? 'end' : 'middle'}
|
|
414
|
-
fontSize={13}
|
|
415
|
-
fill="var(--color-text-paragraph)"
|
|
416
|
-
fontFamily="var(--font-family-mono)">
|
|
414
|
+
textAnchor={m === 12 ? 'end' : 'middle'}>
|
|
417
415
|
{xAxisLabels[m] ?? ''}
|
|
418
|
-
</
|
|
416
|
+
</ChartLabel>
|
|
419
417
|
))}
|
|
420
|
-
<
|
|
421
|
-
x={
|
|
422
|
-
y={
|
|
418
|
+
<ChartLabel
|
|
419
|
+
x={PORTFOLIO_LEFT + PORTFOLIO_WIDTH - 4}
|
|
420
|
+
y={portfolioYFor(294200) - 8}
|
|
423
421
|
textAnchor="end"
|
|
424
|
-
|
|
425
|
-
fill="var(--color-text-highlight)"
|
|
426
|
-
fontFamily="var(--font-family-mono)">
|
|
422
|
+
fill="var(--color-text-highlight)">
|
|
427
423
|
$294k
|
|
428
|
-
</
|
|
424
|
+
</ChartLabel>
|
|
429
425
|
</svg>
|
|
430
426
|
</Card>
|
|
431
427
|
<Text type="supporting" color="secondary">
|
|
@@ -437,38 +433,6 @@ function PortfolioChart() {
|
|
|
437
433
|
|
|
438
434
|
// ============= CARD COMPONENTS =============
|
|
439
435
|
|
|
440
|
-
function Sparkline({data, positive}: {data: number[]; positive: boolean}) {
|
|
441
|
-
const max = Math.max(...data);
|
|
442
|
-
const min = Math.min(...data);
|
|
443
|
-
const range = Math.max(1, max - min);
|
|
444
|
-
const step = 300 / data.length;
|
|
445
|
-
return (
|
|
446
|
-
<svg
|
|
447
|
-
viewBox="0 0 300 40"
|
|
448
|
-
width="100%"
|
|
449
|
-
height={40}
|
|
450
|
-
role="img"
|
|
451
|
-
aria-label="Twenty-four hour trend">
|
|
452
|
-
{data.map((v, i) => {
|
|
453
|
-
const h = Math.max(3, ((v - min) / range) * 32);
|
|
454
|
-
return (
|
|
455
|
-
<rect
|
|
456
|
-
key={i}
|
|
457
|
-
x={i * step}
|
|
458
|
-
y={36 - h}
|
|
459
|
-
width={Math.max(1.5, step - 1)}
|
|
460
|
-
height={h}
|
|
461
|
-
rx={4}
|
|
462
|
-
fill={
|
|
463
|
-
positive ? 'var(--dracula-green)' : 'var(--dracula-red)'
|
|
464
|
-
}
|
|
465
|
-
/>
|
|
466
|
-
);
|
|
467
|
-
})}
|
|
468
|
-
</svg>
|
|
469
|
-
);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
436
|
function MarketCard({
|
|
473
437
|
name,
|
|
474
438
|
ticker,
|
|
@@ -482,7 +446,7 @@ function MarketCard({
|
|
|
482
446
|
price: string;
|
|
483
447
|
change: string;
|
|
484
448
|
positive: boolean;
|
|
485
|
-
spark:
|
|
449
|
+
spark: SparkPoint[];
|
|
486
450
|
}) {
|
|
487
451
|
return (
|
|
488
452
|
<Card>
|
|
@@ -493,76 +457,35 @@ function MarketCard({
|
|
|
493
457
|
{ticker}
|
|
494
458
|
</Text>
|
|
495
459
|
</VStack>
|
|
496
|
-
<Sparkline
|
|
460
|
+
<Sparkline
|
|
461
|
+
data={spark}
|
|
462
|
+
label={`${name} 24-hour trend`}
|
|
463
|
+
positive={positive}
|
|
464
|
+
mode="range"
|
|
465
|
+
/>
|
|
497
466
|
<HStack gap={3} vAlign="center">
|
|
498
467
|
<Text type="display-3" weight="semibold" hasTabularNumbers>
|
|
499
468
|
{price}
|
|
500
469
|
</Text>
|
|
501
|
-
<
|
|
502
|
-
<Icon
|
|
503
|
-
icon={positive ? ArrowUp : ArrowDown}
|
|
504
|
-
size="xsm"
|
|
505
|
-
color={positive ? 'success' : 'error'}
|
|
506
|
-
/>
|
|
507
|
-
<Text type="body" color="secondary" hasTabularNumbers>
|
|
508
|
-
{change}
|
|
509
|
-
</Text>
|
|
510
|
-
</HStack>
|
|
470
|
+
<MetricDelta value={change} positive={positive} />
|
|
511
471
|
</HStack>
|
|
512
472
|
</VStack>
|
|
513
473
|
</Card>
|
|
514
474
|
);
|
|
515
475
|
}
|
|
516
476
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const step = 300 / data.length;
|
|
522
|
-
return (
|
|
523
|
-
<svg
|
|
524
|
-
viewBox="0 0 300 24"
|
|
525
|
-
width="100%"
|
|
526
|
-
height={24}
|
|
527
|
-
role="img"
|
|
528
|
-
aria-label="Daily trend">
|
|
529
|
-
{data.map((v, i) => {
|
|
530
|
-
const h = Math.max(2, ((v - min) / range) * 18);
|
|
531
|
-
return (
|
|
532
|
-
<rect
|
|
533
|
-
key={i}
|
|
534
|
-
x={i * step}
|
|
535
|
-
y={21 - h}
|
|
536
|
-
width={Math.max(1.5, step - 1)}
|
|
537
|
-
height={h}
|
|
538
|
-
rx={4}
|
|
539
|
-
fill={
|
|
540
|
-
positive ? 'var(--dracula-green)' : 'var(--dracula-red)'
|
|
541
|
-
}
|
|
542
|
-
/>
|
|
543
|
-
);
|
|
544
|
-
})}
|
|
545
|
-
</svg>
|
|
546
|
-
);
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
function ColoredValue({
|
|
550
|
-
value,
|
|
551
|
-
isPositive,
|
|
552
|
-
}: {
|
|
553
|
-
value: string;
|
|
554
|
-
isPositive: boolean;
|
|
555
|
-
}) {
|
|
556
|
-
return <Badge label={value} variant={isPositive ? 'green' : 'red'} />;
|
|
557
|
-
}
|
|
477
|
+
// Signed figures render with an explicit sign, so the value reads as a move
|
|
478
|
+
// even before the tone does.
|
|
479
|
+
const formatSigned = (value: number, digits: number, suffix = '') =>
|
|
480
|
+
`${value >= 0 ? '+' : ''}${value.toFixed(digits)}${suffix}`;
|
|
558
481
|
|
|
559
482
|
const trendingColumns: TableColumn<StockRow>[] = [
|
|
560
483
|
{
|
|
561
484
|
key: 'ticker',
|
|
562
485
|
header: 'Ticker',
|
|
563
|
-
width:
|
|
486
|
+
width: pixel(88),
|
|
564
487
|
renderCell: (row: StockRow) => (
|
|
565
|
-
<Text type="body" weight="semibold">
|
|
488
|
+
<Text type="body" weight="semibold" maxLines={1}>
|
|
566
489
|
{row.ticker}
|
|
567
490
|
</Text>
|
|
568
491
|
),
|
|
@@ -570,49 +493,58 @@ const trendingColumns: TableColumn<StockRow>[] = [
|
|
|
570
493
|
{
|
|
571
494
|
key: 'price',
|
|
572
495
|
header: 'Price',
|
|
573
|
-
width:
|
|
496
|
+
width: pixel(88),
|
|
574
497
|
renderCell: (row: StockRow) => (
|
|
575
|
-
<Text type="body" hasTabularNumbers>
|
|
498
|
+
<Text type="body" hasTabularNumbers maxLines={1}>
|
|
576
499
|
{row.price}
|
|
577
500
|
</Text>
|
|
578
501
|
),
|
|
579
502
|
},
|
|
580
503
|
{
|
|
581
504
|
key: 'dailyPts',
|
|
582
|
-
header: '
|
|
583
|
-
width:
|
|
584
|
-
renderCell: (row: StockRow) =>
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
505
|
+
header: 'Chg (pts)',
|
|
506
|
+
width: pixel(104),
|
|
507
|
+
renderCell: (row: StockRow) => (
|
|
508
|
+
<MetricDelta
|
|
509
|
+
value={formatSigned(row.dailyPts, 2)}
|
|
510
|
+
positive={row.dailyPts >= 0}
|
|
511
|
+
/>
|
|
512
|
+
),
|
|
589
513
|
},
|
|
590
514
|
{
|
|
591
515
|
key: 'dailyPct',
|
|
592
|
-
header: '
|
|
593
|
-
width:
|
|
594
|
-
renderCell: (row: StockRow) =>
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
516
|
+
header: 'Chg (%)',
|
|
517
|
+
width: pixel(96),
|
|
518
|
+
renderCell: (row: StockRow) => (
|
|
519
|
+
<MetricDelta
|
|
520
|
+
value={formatSigned(row.dailyPct, 2, '%')}
|
|
521
|
+
positive={row.dailyPct >= 0}
|
|
522
|
+
/>
|
|
523
|
+
),
|
|
599
524
|
},
|
|
600
525
|
{
|
|
601
526
|
key: 'weekChg',
|
|
602
|
-
header: '52W
|
|
603
|
-
width:
|
|
604
|
-
renderCell: (row: StockRow) =>
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
527
|
+
header: '52W (%)',
|
|
528
|
+
width: pixel(96),
|
|
529
|
+
renderCell: (row: StockRow) => (
|
|
530
|
+
<MetricDelta
|
|
531
|
+
value={formatSigned(row.weekChg, 1, '%')}
|
|
532
|
+
positive={row.weekChg >= 0}
|
|
533
|
+
/>
|
|
534
|
+
),
|
|
609
535
|
},
|
|
610
536
|
{
|
|
611
537
|
key: 'spark',
|
|
612
538
|
header: '24h Trend',
|
|
613
|
-
width: proportional(1),
|
|
539
|
+
width: proportional(1, {minWidth: 96}),
|
|
614
540
|
renderCell: (row: StockRow) => (
|
|
615
|
-
<
|
|
541
|
+
<Sparkline
|
|
542
|
+
data={row.spark}
|
|
543
|
+
label={`${row.ticker} 24-hour trend`}
|
|
544
|
+
positive={row.dailyPct >= 0}
|
|
545
|
+
mode="range"
|
|
546
|
+
isCompact
|
|
547
|
+
/>
|
|
616
548
|
),
|
|
617
549
|
},
|
|
618
550
|
];
|
|
@@ -632,19 +564,14 @@ function MetricCard({
|
|
|
632
564
|
return (
|
|
633
565
|
<Card>
|
|
634
566
|
<VStack gap={2}>
|
|
635
|
-
<
|
|
567
|
+
<Text type="supporting" color="secondary">
|
|
568
|
+
{label}
|
|
569
|
+
</Text>
|
|
636
570
|
<HStack gap={2} vAlign="center">
|
|
637
|
-
<
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
size="xsm"
|
|
642
|
-
color={positive ? 'success' : 'error'}
|
|
643
|
-
/>
|
|
644
|
-
<Text type="body" color="secondary" hasTabularNumbers>
|
|
645
|
-
{change}
|
|
646
|
-
</Text>
|
|
647
|
-
</HStack>
|
|
571
|
+
<Text type="display-3" weight="semibold" hasTabularNumbers>
|
|
572
|
+
{value}
|
|
573
|
+
</Text>
|
|
574
|
+
<MetricDelta value={change} positive={positive} />
|
|
648
575
|
</HStack>
|
|
649
576
|
<Text type="supporting" color="secondary">
|
|
650
577
|
{caption}
|
|
@@ -676,10 +603,7 @@ function AssetRow({
|
|
|
676
603
|
<Text type="body" hasTabularNumbers>
|
|
677
604
|
{value}
|
|
678
605
|
</Text>
|
|
679
|
-
<
|
|
680
|
-
label={change}
|
|
681
|
-
variant={change.startsWith('-') ? 'red' : 'green'}
|
|
682
|
-
/>
|
|
606
|
+
<MetricDelta value={change} positive={!change.startsWith('-')} />
|
|
683
607
|
</VStack>
|
|
684
608
|
}
|
|
685
609
|
/>
|
|
@@ -722,43 +646,35 @@ export default function DashboardPortfolio() {
|
|
|
722
646
|
|
|
723
647
|
{/* KPI metric cards */}
|
|
724
648
|
<Grid columns={{minWidth: 280, repeat: 'fit'}} gap={4}>
|
|
725
|
-
{
|
|
726
|
-
<
|
|
727
|
-
{metrics.slice(i * 2, i * 2 + 2).map(m => (
|
|
728
|
-
<MetricCard key={m.label} {...m} />
|
|
729
|
-
))}
|
|
730
|
-
</Grid>
|
|
649
|
+
{metrics.map(m => (
|
|
650
|
+
<MetricCard key={m.label} {...m} />
|
|
731
651
|
))}
|
|
732
652
|
</Grid>
|
|
733
653
|
|
|
734
|
-
{/* Chart + Top
|
|
735
|
-
<Grid columns={{minWidth: 280,
|
|
736
|
-
<
|
|
737
|
-
<
|
|
738
|
-
<
|
|
739
|
-
<
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
</
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
</
|
|
746
|
-
</
|
|
747
|
-
<
|
|
748
|
-
<
|
|
749
|
-
<
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
<
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
</List>
|
|
759
|
-
</VStack>
|
|
760
|
-
</Card>
|
|
761
|
-
</GridSpan>
|
|
654
|
+
{/* Chart + Top holdings */}
|
|
655
|
+
<Grid columns={{minWidth: 280, repeat: 'fit'}} gap={4}>
|
|
656
|
+
<Card>
|
|
657
|
+
<VStack gap={4}>
|
|
658
|
+
<HStack hAlign="between" vAlign="center">
|
|
659
|
+
<Heading level={2}>Vault value</Heading>
|
|
660
|
+
<Link href="#/templates/dashboard-portfolio">
|
|
661
|
+
View details
|
|
662
|
+
</Link>
|
|
663
|
+
</HStack>
|
|
664
|
+
<PortfolioChart />
|
|
665
|
+
</VStack>
|
|
666
|
+
</Card>
|
|
667
|
+
<VStack gap={4}>
|
|
668
|
+
<HStack hAlign="between" vAlign="center">
|
|
669
|
+
<Heading level={2}>Top holdings</Heading>
|
|
670
|
+
<Link href="#/templates/dashboard-portfolio">View all</Link>
|
|
671
|
+
</HStack>
|
|
672
|
+
<List density="spacious">
|
|
673
|
+
{topAssets.map(asset => (
|
|
674
|
+
<AssetRow key={asset.ticker} {...asset} />
|
|
675
|
+
))}
|
|
676
|
+
</List>
|
|
677
|
+
</VStack>
|
|
762
678
|
</Grid>
|
|
763
679
|
|
|
764
680
|
<Divider />
|
|
@@ -771,11 +687,11 @@ export default function DashboardPortfolio() {
|
|
|
771
687
|
Past 24 hours under moonlight
|
|
772
688
|
</Text>
|
|
773
689
|
</VStack>
|
|
774
|
-
<Button label="View more" variant="secondary" size="
|
|
690
|
+
<Button label="View more" variant="secondary" size="md" />
|
|
775
691
|
</HStack>
|
|
776
692
|
|
|
777
693
|
{/* Market index cards */}
|
|
778
|
-
<Grid columns={{minWidth:
|
|
694
|
+
<Grid columns={{minWidth: 280, repeat: 'fit'}} gap={4}>
|
|
779
695
|
{marketIndices.map(m => (
|
|
780
696
|
<MarketCard key={m.ticker} {...m} />
|
|
781
697
|
))}
|
|
@@ -784,11 +700,12 @@ export default function DashboardPortfolio() {
|
|
|
784
700
|
{/* Trending stocks table */}
|
|
785
701
|
<Card>
|
|
786
702
|
<VStack gap={4}>
|
|
787
|
-
<Heading level={3}>Trending
|
|
703
|
+
<Heading level={3}>Trending stocks</Heading>
|
|
788
704
|
<Table<StockRow>
|
|
789
705
|
data={trendingStocks}
|
|
790
706
|
columns={trendingColumns}
|
|
791
707
|
idKey="id"
|
|
708
|
+
textOverflow="truncate"
|
|
792
709
|
hasHover
|
|
793
710
|
dividers="rows"
|
|
794
711
|
/>
|