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