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.
Files changed (83) hide show
  1. package/BRAND.md +30 -18
  2. package/README.md +17 -12
  3. package/USAGE.md +30 -10
  4. package/astryx-dracula.js +19 -13
  5. package/astryx-dracula.variants.d.ts +15 -0
  6. package/package.json +59 -17
  7. package/shared/auth-chrome-config.ts +18 -0
  8. package/shared/auth-chrome.tsx +25 -0
  9. package/shared/auth-copy.ts +22 -0
  10. package/shared/chaptered-doc-config.ts +92 -0
  11. package/shared/chaptered-doc.tsx +332 -0
  12. package/shared/chart-hues.ts +27 -0
  13. package/shared/chart-labels.tsx +44 -0
  14. package/shared/chart-legend.tsx +100 -0
  15. package/shared/chart-panel-style.ts +13 -0
  16. package/shared/data-bar.tsx +160 -0
  17. package/shared/gallery-image.ts +20 -0
  18. package/shared/login-demo.ts +7 -0
  19. package/shared/metric-delta.tsx +36 -0
  20. package/shared/order-desk.tsx +273 -0
  21. package/shared/revenue-chart.tsx +181 -0
  22. package/shared/scene-castle.tsx +322 -0
  23. package/shared/scene-frame.tsx +88 -0
  24. package/shared/scene-hues.ts +49 -0
  25. package/shared/scene-tile.tsx +128 -0
  26. package/shared/settings-data.ts +125 -0
  27. package/shared/settings-rows.tsx +179 -0
  28. package/shared/sparkline.tsx +102 -0
  29. package/shared/sso-icons.tsx +30 -0
  30. package/templates/ai-chat-landing.tsx +66 -33
  31. package/templates/ai-chat.tsx +36 -19
  32. package/templates/blank.tsx +4 -3
  33. package/templates/centered-hero.tsx +14 -125
  34. package/templates/classic-gallery.tsx +20 -66
  35. package/templates/contact-form.tsx +59 -39
  36. package/templates/dashboard-portfolio.tsx +143 -226
  37. package/templates/dashboard.tsx +122 -122
  38. package/templates/detail-page.tsx +95 -91
  39. package/templates/documentation-design.template.mjs +1 -1
  40. package/templates/documentation-design.tsx +83 -96
  41. package/templates/documentation-technical.template.mjs +1 -1
  42. package/templates/documentation-technical.tsx +29 -19
  43. package/templates/documentation.template.mjs +1 -1
  44. package/templates/documentation.tsx +52 -74
  45. package/templates/editor.tsx +112 -84
  46. package/templates/file-explorer.tsx +131 -84
  47. package/templates/form-two-column.tsx +61 -125
  48. package/templates/gallery-hero.tsx +15 -171
  49. package/templates/ide.tsx +22 -20
  50. package/templates/incident-console.tsx +104 -70
  51. package/templates/kanban-board.tsx +99 -42
  52. package/templates/library.template.mjs +2 -2
  53. package/templates/library.tsx +37 -42
  54. package/templates/login-card.tsx +58 -87
  55. package/templates/login-split.tsx +88 -126
  56. package/templates/login-sso.tsx +105 -64
  57. package/templates/login.tsx +107 -53
  58. package/templates/messaging-shell.tsx +171 -144
  59. package/templates/mixed-gallery.tsx +29 -130
  60. package/templates/payment-form.tsx +368 -290
  61. package/templates/product-detail.tsx +39 -58
  62. package/templates/product-gallery.tsx +30 -41
  63. package/templates/product-tour.template.mjs +6 -0
  64. package/templates/product-tour.tsx +916 -0
  65. package/templates/settings-dialog.tsx +135 -282
  66. package/templates/settings-sidebar.tsx +178 -268
  67. package/templates/settings.tsx +41 -14
  68. package/templates/shell-nav.tsx +31 -24
  69. package/templates/shell-side-nav.tsx +29 -20
  70. package/templates/shell-top-nav.tsx +80 -44
  71. package/templates/side-gallery.tsx +38 -94
  72. package/templates/table-grouped.tsx +62 -34
  73. package/templates/table-page-chart.tsx +23 -302
  74. package/templates/table-page-heatmap-status.tsx +156 -92
  75. package/templates/table-page-shoe-store-heatmap.tsx +18 -293
  76. package/templates/table-page.tsx +23 -7
  77. package/templates/table.tsx +23 -8
  78. package/templates/tech-report.template.mjs +6 -0
  79. package/templates/tech-report.tsx +1486 -0
  80. package/templates/theme-showcase.template.mjs +1 -1
  81. package/templates/theme-showcase.tsx +299 -237
  82. package/theme.css +76 -14
  83. 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=4 g=4] > (C > V[g=2] > Hd"Label"[level=4] + (H[g=2] > Hd"$1.2M"[level=2] + Tx"+2.4%"[t=body]))*4) + (G[c=4 g=4] > (GS[c=3] > C > V[g=4] > (H[j=between] > Hd"Vault value"[level=3] + Lk"View details") + AR) + (GS[c=1] > C > V[g=4] > (H[j=between] > Hd"Top holdings"[level=3] + 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=3 g=4] > (C > V[g=3] > Hd"Index"[level=4] + Tx"$5,200"[t=body])*3) + (C > V[g=4] > Hd"Trending Stocks"[level=3] + T)
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, GridSpan} from '@astryxdesign/core/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 {ArrowUp, ArrowDown} from 'lucide-react';
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
- ): number[] {
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: number[] = [];
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: number[];
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
- {yTicks.map(t => (
376
+ {PORTFOLIO_Y_TICKS.map(t => (
373
377
  <g key={t}>
374
378
  <line
375
- x1={left}
376
- y1={yFor(t)}
377
- x2={left + width}
378
- y2={yFor(t)}
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
- <text
384
- x={left - 8}
385
- y={yFor(t) + 4}
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
- </text>
392
+ </ChartLabel>
392
393
  </g>
393
394
  ))}
394
- {bars.map((d, i) => {
395
- const h = Math.max(3, base - yFor(d.value));
395
+ {portfolioBars.map((d, i) => {
396
+ const h = Math.max(3, PORTFOLIO_BASE - portfolioYFor(d.value));
396
397
  return (
397
398
  <rect
398
- key={i}
399
- x={left + i * step + 1}
400
- y={base - h}
401
- width={Math.max(2, step - 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
- <text
410
+ <ChartLabel
410
411
  key={m}
411
- x={left + (m / 12) * width}
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
- </text>
416
+ </ChartLabel>
419
417
  ))}
420
- <text
421
- x={left + width - 4}
422
- y={yFor(294200) - 8}
418
+ <ChartLabel
419
+ x={PORTFOLIO_LEFT + PORTFOLIO_WIDTH - 4}
420
+ y={portfolioYFor(294200) - 8}
423
421
  textAnchor="end"
424
- fontSize={13}
425
- fill="var(--color-text-highlight)"
426
- fontFamily="var(--font-family-mono)">
422
+ fill="var(--color-text-highlight)">
427
423
  $294k
428
- </text>
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: number[];
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 data={spark} positive={positive} />
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
- <HStack gap={1} vAlign="center">
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
- function TrendSparkline({data, positive}: {data: number[]; positive: boolean}) {
518
- const max = Math.max(...data);
519
- const min = Math.min(...data);
520
- const range = Math.max(1, max - min);
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: proportional(1),
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: proportional(1),
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: 'Daily Chg (pts)',
583
- width: proportional(1),
584
- renderCell: (row: StockRow) => {
585
- const isPos = row.dailyPts >= 0;
586
- const formatted = (isPos ? '+' : '') + row.dailyPts.toFixed(2);
587
- return <ColoredValue value={formatted} isPositive={isPos} />;
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: 'Daily Chg (%)',
593
- width: proportional(1),
594
- renderCell: (row: StockRow) => {
595
- const isPos = row.dailyPct >= 0;
596
- const formatted = (isPos ? '+' : '') + row.dailyPct.toFixed(2) + '%';
597
- return <ColoredValue value={formatted} isPositive={isPos} />;
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 Chg (%)',
603
- width: proportional(1),
604
- renderCell: (row: StockRow) => {
605
- const isPos = row.weekChg >= 0;
606
- const formatted = (isPos ? '+' : '') + row.weekChg.toFixed(1) + '%';
607
- return <ColoredValue value={formatted} isPositive={isPos} />;
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
- <TrendSparkline data={row.spark} positive={row.dailyPct >= 0} />
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
- <Heading level={4}>{label}</Heading>
567
+ <Text type="supporting" color="secondary">
568
+ {label}
569
+ </Text>
636
570
  <HStack gap={2} vAlign="center">
637
- <Heading level={2}>{value}</Heading>
638
- <HStack gap={1} vAlign="center">
639
- <Icon
640
- icon={positive ? ArrowUp : ArrowDown}
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
- <Badge
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
- {Array.from({length: Math.ceil(metrics.length / 2)}, (_, i) => (
726
- <Grid key={i} columns={{minWidth: 280, repeat: 'fit'}} gap={4}>
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 assets */}
735
- <Grid columns={{minWidth: 280, max: 4}} gap={4}>
736
- <GridSpan columns={3}>
737
- <Card>
738
- <VStack gap={4}>
739
- <HStack hAlign="between" vAlign="center">
740
- <Heading level={3}>Vault value</Heading>
741
- <Link href="#/templates/dashboard-portfolio">View details</Link>
742
- </HStack>
743
- <PortfolioChart />
744
- </VStack>
745
- </Card>
746
- </GridSpan>
747
- <GridSpan columns={1}>
748
- <Card>
749
- <VStack gap={4}>
750
- <HStack hAlign="between" vAlign="center">
751
- <Heading level={3}>Top holdings</Heading>
752
- <Link href="#/templates/dashboard-portfolio">View all</Link>
753
- </HStack>
754
- <List density="spacious">
755
- {topAssets.map(asset => (
756
- <AssetRow key={asset.ticker} {...asset} />
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="lg" />
690
+ <Button label="View more" variant="secondary" size="md" />
775
691
  </HStack>
776
692
 
777
693
  {/* Market index cards */}
778
- <Grid columns={{minWidth: 320, repeat: 'fit'}} gap={4}>
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 Stocks</Heading>
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
  />