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.
Files changed (83) hide show
  1. package/BRAND.md +30 -18
  2. package/README.md +22 -11
  3. package/USAGE.md +29 -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 +174 -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 +94 -0
  29. package/shared/sso-icons.tsx +30 -0
  30. package/templates/ai-chat-landing.tsx +67 -32
  31. package/templates/ai-chat.tsx +41 -20
  32. package/templates/blank.tsx +5 -2
  33. package/templates/centered-hero.tsx +15 -124
  34. package/templates/classic-gallery.tsx +21 -65
  35. package/templates/contact-form.tsx +62 -40
  36. package/templates/dashboard-portfolio.tsx +146 -227
  37. package/templates/dashboard.tsx +123 -121
  38. package/templates/detail-page.tsx +101 -95
  39. package/templates/documentation-design.template.mjs +1 -1
  40. package/templates/documentation-design.tsx +85 -96
  41. package/templates/documentation-technical.template.mjs +1 -1
  42. package/templates/documentation-technical.tsx +33 -21
  43. package/templates/documentation.template.mjs +1 -1
  44. package/templates/documentation.tsx +54 -74
  45. package/templates/editor.tsx +119 -89
  46. package/templates/file-explorer.tsx +133 -84
  47. package/templates/form-two-column.tsx +62 -124
  48. package/templates/gallery-hero.tsx +17 -171
  49. package/templates/ide.tsx +27 -21
  50. package/templates/incident-console.tsx +105 -69
  51. package/templates/kanban-board.tsx +100 -41
  52. package/templates/library.template.mjs +2 -2
  53. package/templates/library.tsx +38 -41
  54. package/templates/login-card.tsx +59 -86
  55. package/templates/login-split.tsx +88 -128
  56. package/templates/login-sso.tsx +107 -68
  57. package/templates/login.tsx +108 -52
  58. package/templates/messaging-shell.tsx +172 -143
  59. package/templates/mixed-gallery.tsx +31 -130
  60. package/templates/payment-form.tsx +369 -289
  61. package/templates/product-detail.tsx +40 -57
  62. package/templates/product-gallery.tsx +31 -40
  63. package/templates/product-tour.template.mjs +6 -0
  64. package/templates/product-tour.tsx +916 -0
  65. package/templates/settings-dialog.tsx +145 -286
  66. package/templates/settings-sidebar.tsx +179 -267
  67. package/templates/settings.tsx +42 -13
  68. package/templates/shell-nav.tsx +32 -23
  69. package/templates/shell-side-nav.tsx +30 -19
  70. package/templates/shell-top-nav.tsx +81 -43
  71. package/templates/side-gallery.tsx +39 -93
  72. package/templates/table-grouped.tsx +63 -33
  73. package/templates/table-page-chart.tsx +24 -297
  74. package/templates/table-page-heatmap-status.tsx +157 -91
  75. package/templates/table-page-shoe-store-heatmap.tsx +19 -288
  76. package/templates/table-page.tsx +24 -6
  77. package/templates/table.tsx +24 -7
  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 +313 -249
  82. package/theme.css +76 -14
  83. 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, GridSpan} from '@astryxdesign/core/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 {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';
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
- ): number[] {
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: number[] = [];
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: number[];
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
- {yTicks.map(t => (
376
+ {PORTFOLIO_Y_TICKS.map(t => (
371
377
  <g key={t}>
372
378
  <line
373
- x1={left}
374
- y1={yFor(t)}
375
- x2={left + width}
376
- y2={yFor(t)}
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
- <text
382
- x={left - 8}
383
- y={yFor(t) + 4}
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
- </text>
392
+ </ChartLabel>
390
393
  </g>
391
394
  ))}
392
- {bars.map((d, i) => {
393
- 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));
394
397
  return (
395
398
  <rect
396
- key={i}
397
- x={left + i * step + 1}
398
- y={base - h}
399
- 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)}
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
- <text
410
+ <ChartLabel
408
411
  key={m}
409
- x={left + (m / 12) * width}
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
- </text>
416
+ </ChartLabel>
417
417
  ))}
418
- <text
419
- x={left + width - 4}
420
- y={yFor(294200) - 8}
418
+ <ChartLabel
419
+ x={PORTFOLIO_LEFT + PORTFOLIO_WIDTH - 4}
420
+ y={portfolioYFor(294200) - 8}
421
421
  textAnchor="end"
422
- fontSize={13}
423
- fill="var(--color-text-highlight)"
424
- fontFamily="var(--font-family-mono)">
422
+ fill="var(--color-text-highlight)">
425
423
  $294k
426
- </text>
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: number[];
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 data={spark} positive={positive} />
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="bold" hasTabularNumbers>
467
+ <Text type="display-3" weight="semibold" hasTabularNumbers>
497
468
  {price}
498
469
  </Text>
499
- <HStack gap={1} vAlign="center">
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
- function TrendSparkline({data, positive}: {data: number[]; positive: boolean}) {
516
- const max = Math.max(...data);
517
- const min = Math.min(...data);
518
- const range = Math.max(1, max - min);
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: proportional(1),
486
+ width: pixel(88),
562
487
  renderCell: (row: StockRow) => (
563
- <Text type="body" weight="bold">
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: proportional(1),
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: 'Daily Chg (pts)',
581
- width: proportional(1),
582
- renderCell: (row: StockRow) => {
583
- const isPos = row.dailyPts >= 0;
584
- const formatted = (isPos ? '+' : '') + row.dailyPts.toFixed(2);
585
- return <ColoredValue value={formatted} isPositive={isPos} />;
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: 'Daily Chg (%)',
591
- width: proportional(1),
592
- renderCell: (row: StockRow) => {
593
- const isPos = row.dailyPct >= 0;
594
- const formatted = (isPos ? '+' : '') + row.dailyPct.toFixed(2) + '%';
595
- return <ColoredValue value={formatted} isPositive={isPos} />;
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 Chg (%)',
601
- width: proportional(1),
602
- renderCell: (row: StockRow) => {
603
- const isPos = row.weekChg >= 0;
604
- const formatted = (isPos ? '+' : '') + row.weekChg.toFixed(1) + '%';
605
- return <ColoredValue value={formatted} isPositive={isPos} />;
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
- <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
+ />
614
548
  ),
615
549
  },
616
550
  ];
@@ -630,19 +564,14 @@ function MetricCard({
630
564
  return (
631
565
  <Card>
632
566
  <VStack gap={2}>
633
- <Heading level={4}>{label}</Heading>
567
+ <Text type="supporting" color="secondary">
568
+ {label}
569
+ </Text>
634
570
  <HStack gap={2} vAlign="center">
635
- <Heading level={2}>{value}</Heading>
636
- <HStack gap={1} vAlign="center">
637
- <Icon
638
- icon={positive ? ArrowUp : ArrowDown}
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="bold">{ticker}</Text>}
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
- <Badge
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
- {Array.from({length: Math.ceil(metrics.length / 2)}, (_, i) => (
724
- <Grid key={i} columns={{minWidth: 280, repeat: 'fit'}} gap={4}>
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 assets */}
733
- <Grid columns={{minWidth: 280, max: 4}} gap={4}>
734
- <GridSpan columns={3}>
735
- <Card>
736
- <VStack gap={4}>
737
- <HStack hAlign="between" vAlign="center">
738
- <Heading level={3}>Vault value</Heading>
739
- <Link href="#/templates/dashboard-portfolio">View details</Link>
740
- </HStack>
741
- <PortfolioChart />
742
- </VStack>
743
- </Card>
744
- </GridSpan>
745
- <GridSpan columns={1}>
746
- <Card>
747
- <VStack gap={4}>
748
- <HStack hAlign="between" vAlign="center">
749
- <Heading level={3}>Top holdings</Heading>
750
- <Link href="#/templates/dashboard-portfolio">View all</Link>
751
- </HStack>
752
- <List density="spacious">
753
- {topAssets.map(asset => (
754
- <AssetRow key={asset.ticker} {...asset} />
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="lg" />
690
+ <Button label="View more" variant="secondary" size="md" />
773
691
  </HStack>
774
692
 
775
693
  {/* Market index cards */}
776
- <Grid columns={{minWidth: 320, repeat: 'fit'}} gap={4}>
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 Stocks</Heading>
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
  />