astryx-dracula 0.1.3 → 0.2.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 +1 -1
- package/README.md +2 -0
- package/USAGE.md +13 -0
- package/astryx-dracula.js +4 -4
- package/astryx.integration.mjs +3 -0
- package/package.json +4 -2
- package/templates/ai-chat-landing.template.mjs +6 -0
- package/templates/ai-chat-landing.tsx +476 -0
- package/templates/ai-chat.template.mjs +6 -0
- package/templates/ai-chat.tsx +762 -0
- package/templates/blank.template.mjs +6 -0
- package/templates/blank.tsx +16 -0
- package/templates/centered-hero.template.mjs +6 -0
- package/templates/centered-hero.tsx +188 -0
- package/templates/classic-gallery.template.mjs +6 -0
- package/templates/classic-gallery.tsx +181 -0
- package/templates/contact-form.template.mjs +6 -0
- package/templates/contact-form.tsx +292 -0
- package/templates/dashboard-portfolio.template.mjs +6 -0
- package/templates/dashboard-portfolio.tsx +800 -0
- package/templates/dashboard.template.mjs +6 -0
- package/templates/dashboard.tsx +774 -0
- package/templates/detail-page.template.mjs +6 -0
- package/templates/detail-page.tsx +695 -0
- package/templates/documentation-design.template.mjs +6 -0
- package/templates/documentation-design.tsx +751 -0
- package/templates/documentation-technical.template.mjs +6 -0
- package/templates/documentation-technical.tsx +292 -0
- package/templates/documentation.template.mjs +6 -0
- package/templates/documentation.tsx +347 -0
- package/templates/editor.template.mjs +6 -0
- package/templates/editor.tsx +959 -0
- package/templates/file-explorer.template.mjs +6 -0
- package/templates/file-explorer.tsx +577 -0
- package/templates/form-two-column.template.mjs +6 -0
- package/templates/form-two-column.tsx +314 -0
- package/templates/gallery-hero.template.mjs +6 -0
- package/templates/gallery-hero.tsx +287 -0
- package/templates/ide.template.mjs +6 -0
- package/templates/ide.tsx +533 -0
- package/templates/incident-console.template.mjs +6 -0
- package/templates/incident-console.tsx +584 -0
- package/templates/kanban-board.template.mjs +6 -0
- package/templates/kanban-board.tsx +746 -0
- package/templates/library.template.mjs +6 -0
- package/templates/library.tsx +489 -0
- package/templates/login-card.template.mjs +6 -0
- package/templates/login-card.tsx +200 -0
- package/templates/login-split.template.mjs +6 -0
- package/templates/login-split.tsx +318 -0
- package/templates/login-sso.template.mjs +6 -0
- package/templates/login-sso.tsx +277 -0
- package/templates/login.template.mjs +6 -0
- package/templates/login.tsx +98 -0
- package/templates/messaging-shell.template.mjs +6 -0
- package/templates/messaging-shell.tsx +675 -0
- package/templates/mixed-gallery.template.mjs +6 -0
- package/templates/mixed-gallery.tsx +190 -0
- package/templates/payment-form.template.mjs +6 -0
- package/templates/payment-form.tsx +990 -0
- package/templates/product-detail.template.mjs +6 -0
- package/templates/product-detail.tsx +366 -0
- package/templates/product-gallery.template.mjs +6 -0
- package/templates/product-gallery.tsx +173 -0
- package/templates/settings-dialog.template.mjs +6 -0
- package/templates/settings-dialog.tsx +920 -0
- package/templates/settings-sidebar.template.mjs +6 -0
- package/templates/settings-sidebar.tsx +952 -0
- package/templates/settings.template.mjs +6 -0
- package/templates/settings.tsx +235 -0
- package/templates/shell-nav.template.mjs +6 -0
- package/templates/shell-nav.tsx +306 -0
- package/templates/shell-side-nav.template.mjs +6 -0
- package/templates/shell-side-nav.tsx +238 -0
- package/templates/shell-top-nav.template.mjs +6 -0
- package/templates/shell-top-nav.tsx +243 -0
- package/templates/side-gallery.template.mjs +6 -0
- package/templates/side-gallery.tsx +179 -0
- package/templates/table-grouped.template.mjs +6 -0
- package/templates/table-grouped.tsx +1174 -0
- package/templates/table-page-chart.template.mjs +6 -0
- package/templates/table-page-chart.tsx +693 -0
- package/templates/table-page-heatmap-status.template.mjs +6 -0
- package/templates/table-page-heatmap-status.tsx +547 -0
- package/templates/table-page-shoe-store-heatmap.template.mjs +6 -0
- package/templates/table-page-shoe-store-heatmap.tsx +1038 -0
- package/templates/table-page.template.mjs +6 -0
- package/templates/table-page.tsx +445 -0
- package/templates/table.template.mjs +6 -0
- package/templates/table.tsx +84 -0
- package/templates/theme-showcase.template.mjs +6 -0
- package/templates/theme-showcase.tsx +1282 -0
- package/theme.css +4 -4
- package/tokens.css +4 -4
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {useState} from 'react';
|
|
4
|
+
|
|
5
|
+
import {VStack, HStack, Layout, LayoutContent} from '@astryxdesign/core/Layout';
|
|
6
|
+
import {Text, Heading} from '@astryxdesign/core/Text';
|
|
7
|
+
import {Card} from '@astryxdesign/core/Card';
|
|
8
|
+
import {Grid, GridSpan} from '@astryxdesign/core/Grid';
|
|
9
|
+
import {Icon} from '@astryxdesign/core/Icon';
|
|
10
|
+
import {Link} from '@astryxdesign/core/Link';
|
|
11
|
+
import {Avatar} from '@astryxdesign/core/Avatar';
|
|
12
|
+
import {List, ListItem} from '@astryxdesign/core/List';
|
|
13
|
+
import {DropdownMenu} from '@astryxdesign/core/DropdownMenu';
|
|
14
|
+
import {Badge} from '@astryxdesign/core/Badge';
|
|
15
|
+
import {Button} from '@astryxdesign/core/Button';
|
|
16
|
+
import {Table, proportional} from '@astryxdesign/core/Table';
|
|
17
|
+
import type {TableColumn} from '@astryxdesign/core/Table';
|
|
18
|
+
import {Divider} from '@astryxdesign/core/Divider';
|
|
19
|
+
import {ArrowUp, ArrowDown} from 'lucide-react';
|
|
20
|
+
|
|
21
|
+
// ============= DATA =============
|
|
22
|
+
|
|
23
|
+
// Portfolio value over ~12 months (Oct 2024 → Oct 2025), one data point per day.
|
|
24
|
+
// Realistic fluctuations: dips in Feb–Mar, recovery in summer, climb into fall.
|
|
25
|
+
const portfolioData = (() => {
|
|
26
|
+
const anchors: Array<[number, number]> = [
|
|
27
|
+
[0, 230000],
|
|
28
|
+
[14, 238000],
|
|
29
|
+
[28, 245000],
|
|
30
|
+
[42, 250000],
|
|
31
|
+
[56, 245000],
|
|
32
|
+
[70, 258000],
|
|
33
|
+
[84, 252000],
|
|
34
|
+
[98, 260000],
|
|
35
|
+
[112, 255000],
|
|
36
|
+
[126, 245000],
|
|
37
|
+
[140, 222000],
|
|
38
|
+
[154, 218000],
|
|
39
|
+
[168, 225000],
|
|
40
|
+
[182, 232000],
|
|
41
|
+
[196, 225000],
|
|
42
|
+
[210, 235000],
|
|
43
|
+
[224, 240000],
|
|
44
|
+
[238, 245000],
|
|
45
|
+
[252, 235000],
|
|
46
|
+
[266, 248000],
|
|
47
|
+
[280, 255000],
|
|
48
|
+
[294, 260000],
|
|
49
|
+
[308, 268000],
|
|
50
|
+
[322, 275000],
|
|
51
|
+
[336, 278000],
|
|
52
|
+
[350, 285000],
|
|
53
|
+
[364, 288000],
|
|
54
|
+
[378, 290000],
|
|
55
|
+
[392, 292000],
|
|
56
|
+
[406, 294200],
|
|
57
|
+
];
|
|
58
|
+
const totalDays = anchors[anchors.length - 1][0];
|
|
59
|
+
const monthPerDay = 12 / totalDays;
|
|
60
|
+
const out: Array<{month: number; label: string; value: number}> = [];
|
|
61
|
+
let ai = 0;
|
|
62
|
+
for (let day = 0; day <= totalDays; day++) {
|
|
63
|
+
while (ai < anchors.length - 2 && day >= anchors[ai + 1][0]) {
|
|
64
|
+
ai++;
|
|
65
|
+
}
|
|
66
|
+
const [d0, v0] = anchors[ai];
|
|
67
|
+
const [d1, v1] = anchors[ai + 1];
|
|
68
|
+
const t = (day - d0) / (d1 - d0);
|
|
69
|
+
const base = v0 + (v1 - v0) * t;
|
|
70
|
+
const seed = Math.sin(day * 12.9898 + 78.233) * 43758.5453;
|
|
71
|
+
const noise = (seed - Math.floor(seed) - 0.5) * 3600;
|
|
72
|
+
out.push({
|
|
73
|
+
month: day * monthPerDay,
|
|
74
|
+
label: `Day ${day + 1}`,
|
|
75
|
+
value: Math.round(base + noise),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
})();
|
|
80
|
+
|
|
81
|
+
const xAxisTicks = [0, 3, 6, 9, 12];
|
|
82
|
+
const xAxisLabels: Record<number, string> = {
|
|
83
|
+
0: 'Oct',
|
|
84
|
+
3: 'Jan',
|
|
85
|
+
6: 'Apr',
|
|
86
|
+
9: 'Jul',
|
|
87
|
+
12: 'Oct',
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// KPI summary metrics
|
|
91
|
+
const metrics = [
|
|
92
|
+
{
|
|
93
|
+
value: '$294,200',
|
|
94
|
+
change: '+14.8%',
|
|
95
|
+
label: 'Total value',
|
|
96
|
+
caption: 'Weekly closes · trailing 12 months',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
value: '14.8%',
|
|
100
|
+
change: '+2.1%',
|
|
101
|
+
label: 'Annual return',
|
|
102
|
+
caption: 'Trailing 12 months under moonlight',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
value: '2.8%',
|
|
106
|
+
change: '$2,060/qtr',
|
|
107
|
+
label: 'Dividend yield',
|
|
108
|
+
caption: 'Paid quarterly under moonlight',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
value: '23',
|
|
112
|
+
change: '+4 YTD',
|
|
113
|
+
label: 'Total asset holdings',
|
|
114
|
+
caption: 'Across the night vault',
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
// Top holdings
|
|
119
|
+
const topAssets = [
|
|
120
|
+
{ticker: 'AAPL', name: 'Apple Inc.', value: '$87,200', change: '+18.4%'},
|
|
121
|
+
{ticker: 'MSFT', name: 'Microsoft Corp.', value: '$72,500', change: '+14.7%'},
|
|
122
|
+
{ticker: 'NVDA', name: 'NVIDIA Corp.', value: '$63,800', change: '+31.2%'},
|
|
123
|
+
{
|
|
124
|
+
ticker: 'VTI',
|
|
125
|
+
name: 'Vanguard Total Stock',
|
|
126
|
+
value: '$58,400',
|
|
127
|
+
change: '+11.3%',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
ticker: 'BND',
|
|
131
|
+
name: 'Vanguard Total Bond',
|
|
132
|
+
value: '$45,600',
|
|
133
|
+
change: '+4.2%',
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
// 96 points per series = one tick every 15 minutes across a 24h window.
|
|
138
|
+
// Deterministic LCG so the sparklines are stable across renders.
|
|
139
|
+
function genSpark(
|
|
140
|
+
seed: number,
|
|
141
|
+
start: number,
|
|
142
|
+
end: number,
|
|
143
|
+
volatility: number,
|
|
144
|
+
N: number = 96,
|
|
145
|
+
): number[] {
|
|
146
|
+
let s = seed >>> 0;
|
|
147
|
+
const rand = () => {
|
|
148
|
+
s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
|
|
149
|
+
return s / 0x100000000;
|
|
150
|
+
};
|
|
151
|
+
const points: number[] = [];
|
|
152
|
+
let drift = 0;
|
|
153
|
+
for (let i = 0; i < N; i++) {
|
|
154
|
+
const t = i / (N - 1);
|
|
155
|
+
const trend = start + (end - start) * t;
|
|
156
|
+
// Multiple overlaid waves at different frequencies for session-like rhythm
|
|
157
|
+
// plus high-frequency chop.
|
|
158
|
+
const wave =
|
|
159
|
+
Math.sin(t * Math.PI * 2.3 + seed * 0.13) * volatility * 1.4 +
|
|
160
|
+
Math.sin(t * Math.PI * 5.7 + seed * 0.41) * volatility * 0.9 +
|
|
161
|
+
Math.sin(t * Math.PI * 13.1 + seed * 0.07) * volatility * 0.5;
|
|
162
|
+
// Loosely-correlated random walk so adjacent ticks jitter rather than glide.
|
|
163
|
+
drift = drift * 0.55 + (rand() - 0.5) * volatility * 2.2;
|
|
164
|
+
// Occasional sharper spike to mimic news-driven moves.
|
|
165
|
+
const spike = rand() < 0.04 ? (rand() - 0.5) * volatility * 4 : 0;
|
|
166
|
+
points.push(trend + wave + drift + spike);
|
|
167
|
+
}
|
|
168
|
+
return points;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Market index cards — 24h sparkline data (every 15min, 96 points)
|
|
172
|
+
const marketIndices = [
|
|
173
|
+
{
|
|
174
|
+
name: 'Dow Jones',
|
|
175
|
+
ticker: 'DJI',
|
|
176
|
+
price: '43,821.67',
|
|
177
|
+
change: '+0.42%',
|
|
178
|
+
positive: true,
|
|
179
|
+
spark: genSpark(1337, 78, 84, 3.2),
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: 'NASDAQ',
|
|
183
|
+
ticker: 'IXIC',
|
|
184
|
+
price: '18,942.18',
|
|
185
|
+
change: '-0.50%',
|
|
186
|
+
positive: false,
|
|
187
|
+
spark: genSpark(2042, 86, 78, 3.8),
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: 'S&P 500',
|
|
191
|
+
ticker: 'SPX',
|
|
192
|
+
price: '5,918.33',
|
|
193
|
+
change: '+0.21%',
|
|
194
|
+
positive: true,
|
|
195
|
+
spark: genSpark(3155, 71, 75, 2.8),
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: 'NYSE Composite',
|
|
199
|
+
ticker: 'NYA',
|
|
200
|
+
price: '19,752.41',
|
|
201
|
+
change: '-0.20%',
|
|
202
|
+
positive: false,
|
|
203
|
+
spark: genSpark(4287, 70, 65, 3.0),
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: 'NVIDIA Corp.',
|
|
207
|
+
ticker: 'NVDA',
|
|
208
|
+
price: '$177.39',
|
|
209
|
+
change: '+0.93%',
|
|
210
|
+
positive: true,
|
|
211
|
+
spark: genSpark(5519, 60, 67, 3.6),
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: 'Intel Corp.',
|
|
215
|
+
ticker: 'INTC',
|
|
216
|
+
price: '$50.38',
|
|
217
|
+
change: '+4.89%',
|
|
218
|
+
positive: true,
|
|
219
|
+
spark: genSpark(6701, 40, 51, 2.6),
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: 'Nokia Corp.',
|
|
223
|
+
ticker: 'NOK',
|
|
224
|
+
price: '$8.82',
|
|
225
|
+
change: '+6.65%',
|
|
226
|
+
positive: true,
|
|
227
|
+
spark: genSpark(7823, 30, 41, 2.4),
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: 'Tesla, Inc.',
|
|
231
|
+
ticker: 'TSLA',
|
|
232
|
+
price: '$360.59',
|
|
233
|
+
change: '-5.42%',
|
|
234
|
+
positive: false,
|
|
235
|
+
spark: genSpark(8945, 90, 76, 4.2),
|
|
236
|
+
},
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
// Trending stocks table data
|
|
240
|
+
interface StockRow extends Record<string, unknown> {
|
|
241
|
+
id: string;
|
|
242
|
+
ticker: string;
|
|
243
|
+
price: string;
|
|
244
|
+
dailyPts: number;
|
|
245
|
+
dailyPct: number;
|
|
246
|
+
weekChg: number;
|
|
247
|
+
spark: number[];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const trendingStocks: StockRow[] = [
|
|
251
|
+
{
|
|
252
|
+
id: '1',
|
|
253
|
+
ticker: 'AAPL',
|
|
254
|
+
price: '$188.72',
|
|
255
|
+
dailyPts: 1.35,
|
|
256
|
+
dailyPct: 0.72,
|
|
257
|
+
weekChg: 22.4,
|
|
258
|
+
spark: genSpark(11023, 60, 70, 2.6, 48),
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
id: '2',
|
|
262
|
+
ticker: 'MSFT',
|
|
263
|
+
price: '$415.6',
|
|
264
|
+
dailyPts: 3.2,
|
|
265
|
+
dailyPct: 0.78,
|
|
266
|
+
weekChg: 18.6,
|
|
267
|
+
spark: genSpark(12591, 55, 65, 2.4, 48),
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
id: '3',
|
|
271
|
+
ticker: 'NVDA',
|
|
272
|
+
price: '$177.39',
|
|
273
|
+
dailyPts: 1.65,
|
|
274
|
+
dailyPct: 0.93,
|
|
275
|
+
weekChg: 45.2,
|
|
276
|
+
spark: genSpark(13744, 40, 68, 3.8, 48),
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
id: '4',
|
|
280
|
+
ticker: 'AMZN',
|
|
281
|
+
price: '$186.5',
|
|
282
|
+
dailyPts: -0.8,
|
|
283
|
+
dailyPct: -0.43,
|
|
284
|
+
weekChg: 15.3,
|
|
285
|
+
spark: genSpark(14882, 70, 63, 2.5, 48),
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
id: '5',
|
|
289
|
+
ticker: 'GOOGL',
|
|
290
|
+
price: '$155.72',
|
|
291
|
+
dailyPts: 2.1,
|
|
292
|
+
dailyPct: 1.37,
|
|
293
|
+
weekChg: 12.8,
|
|
294
|
+
spark: genSpark(15967, 50, 60, 2.7, 48),
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
id: '6',
|
|
298
|
+
ticker: 'META',
|
|
299
|
+
price: '$505.3',
|
|
300
|
+
dailyPts: 4.5,
|
|
301
|
+
dailyPct: 0.9,
|
|
302
|
+
weekChg: 35.1,
|
|
303
|
+
spark: genSpark(17105, 45, 61, 3.0, 48),
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
id: '7',
|
|
307
|
+
ticker: 'TSLA',
|
|
308
|
+
price: '$360.59',
|
|
309
|
+
dailyPts: -20.67,
|
|
310
|
+
dailyPct: -5.42,
|
|
311
|
+
weekChg: -8.3,
|
|
312
|
+
spark: genSpark(18249, 90, 64, 4.4, 48),
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
id: '8',
|
|
316
|
+
ticker: 'INTC',
|
|
317
|
+
price: '$50.38',
|
|
318
|
+
dailyPts: 2.35,
|
|
319
|
+
dailyPct: 4.89,
|
|
320
|
+
weekChg: -12.5,
|
|
321
|
+
spark: genSpark(19388, 65, 57, 2.8, 48),
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
id: '9',
|
|
325
|
+
ticker: 'AMD',
|
|
326
|
+
price: '$162.45',
|
|
327
|
+
dailyPts: -1.2,
|
|
328
|
+
dailyPct: -0.73,
|
|
329
|
+
weekChg: 28.7,
|
|
330
|
+
spark: genSpark(20471, 50, 63, 2.9, 48),
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
id: '10',
|
|
334
|
+
ticker: 'NFLX',
|
|
335
|
+
price: '$628.9',
|
|
336
|
+
dailyPts: 5.4,
|
|
337
|
+
dailyPct: 0.87,
|
|
338
|
+
weekChg: 42.1,
|
|
339
|
+
spark: genSpark(21556, 42, 58, 2.6, 48),
|
|
340
|
+
},
|
|
341
|
+
];
|
|
342
|
+
|
|
343
|
+
// ============= CHART COMPONENTS =============
|
|
344
|
+
|
|
345
|
+
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
|
+
return (
|
|
358
|
+
<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
|
+
}}>
|
|
365
|
+
<svg
|
|
366
|
+
viewBox="0 0 560 210"
|
|
367
|
+
width="100%"
|
|
368
|
+
role="img"
|
|
369
|
+
aria-label="Vault value by week, October to October">
|
|
370
|
+
{yTicks.map(t => (
|
|
371
|
+
<g key={t}>
|
|
372
|
+
<line
|
|
373
|
+
x1={left}
|
|
374
|
+
y1={yFor(t)}
|
|
375
|
+
x2={left + width}
|
|
376
|
+
y2={yFor(t)}
|
|
377
|
+
stroke="var(--color-separator)"
|
|
378
|
+
strokeDasharray="3 3"
|
|
379
|
+
opacity={0.5}
|
|
380
|
+
/>
|
|
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)">
|
|
388
|
+
${(t / 1000).toFixed(0)}k
|
|
389
|
+
</text>
|
|
390
|
+
</g>
|
|
391
|
+
))}
|
|
392
|
+
{bars.map((d, i) => {
|
|
393
|
+
const h = Math.max(3, base - yFor(d.value));
|
|
394
|
+
return (
|
|
395
|
+
<rect
|
|
396
|
+
key={i}
|
|
397
|
+
x={left + i * step + 1}
|
|
398
|
+
y={base - h}
|
|
399
|
+
width={Math.max(2, step - 2)}
|
|
400
|
+
height={h}
|
|
401
|
+
rx={4}
|
|
402
|
+
fill="var(--dracula-green)"
|
|
403
|
+
/>
|
|
404
|
+
);
|
|
405
|
+
})}
|
|
406
|
+
{xAxisTicks.map(m => (
|
|
407
|
+
<text
|
|
408
|
+
key={m}
|
|
409
|
+
x={left + (m / 12) * width}
|
|
410
|
+
y={192}
|
|
411
|
+
textAnchor={m === 12 ? 'end' : 'middle'}
|
|
412
|
+
fontSize={13}
|
|
413
|
+
fill="var(--color-text-paragraph)"
|
|
414
|
+
fontFamily="var(--font-family-mono)">
|
|
415
|
+
{xAxisLabels[m] ?? ''}
|
|
416
|
+
</text>
|
|
417
|
+
))}
|
|
418
|
+
<text
|
|
419
|
+
x={left + width - 4}
|
|
420
|
+
y={yFor(294200) - 8}
|
|
421
|
+
textAnchor="end"
|
|
422
|
+
fontSize={13}
|
|
423
|
+
fill="var(--color-text-highlight)"
|
|
424
|
+
fontFamily="var(--font-family-mono)">
|
|
425
|
+
$294k
|
|
426
|
+
</text>
|
|
427
|
+
</svg>
|
|
428
|
+
</Card>
|
|
429
|
+
<Text type="supporting" color="secondary">
|
|
430
|
+
Weekly closes · trailing 12 months
|
|
431
|
+
</Text>
|
|
432
|
+
</VStack>
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ============= CARD COMPONENTS =============
|
|
437
|
+
|
|
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
|
+
function MarketCard({
|
|
471
|
+
name,
|
|
472
|
+
ticker,
|
|
473
|
+
price,
|
|
474
|
+
change,
|
|
475
|
+
positive,
|
|
476
|
+
spark,
|
|
477
|
+
}: {
|
|
478
|
+
name: string;
|
|
479
|
+
ticker: string;
|
|
480
|
+
price: string;
|
|
481
|
+
change: string;
|
|
482
|
+
positive: boolean;
|
|
483
|
+
spark: number[];
|
|
484
|
+
}) {
|
|
485
|
+
return (
|
|
486
|
+
<Card>
|
|
487
|
+
<VStack gap={4}>
|
|
488
|
+
<VStack gap={0}>
|
|
489
|
+
<Heading level={3}>{name}</Heading>
|
|
490
|
+
<Text type="supporting" color="secondary">
|
|
491
|
+
{ticker}
|
|
492
|
+
</Text>
|
|
493
|
+
</VStack>
|
|
494
|
+
<Sparkline data={spark} positive={positive} />
|
|
495
|
+
<HStack gap={3} vAlign="center">
|
|
496
|
+
<Text type="display-3" weight="bold" hasTabularNumbers>
|
|
497
|
+
{price}
|
|
498
|
+
</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>
|
|
509
|
+
</HStack>
|
|
510
|
+
</VStack>
|
|
511
|
+
</Card>
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
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
|
+
}
|
|
556
|
+
|
|
557
|
+
const trendingColumns: TableColumn<StockRow>[] = [
|
|
558
|
+
{
|
|
559
|
+
key: 'ticker',
|
|
560
|
+
header: 'Ticker',
|
|
561
|
+
width: proportional(1),
|
|
562
|
+
renderCell: (row: StockRow) => (
|
|
563
|
+
<Text type="body" weight="bold">
|
|
564
|
+
{row.ticker}
|
|
565
|
+
</Text>
|
|
566
|
+
),
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
key: 'price',
|
|
570
|
+
header: 'Price',
|
|
571
|
+
width: proportional(1),
|
|
572
|
+
renderCell: (row: StockRow) => (
|
|
573
|
+
<Text type="body" hasTabularNumbers>
|
|
574
|
+
{row.price}
|
|
575
|
+
</Text>
|
|
576
|
+
),
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
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
|
+
},
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
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
|
+
},
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
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
|
+
},
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
key: 'spark',
|
|
610
|
+
header: '24h Trend',
|
|
611
|
+
width: proportional(1),
|
|
612
|
+
renderCell: (row: StockRow) => (
|
|
613
|
+
<TrendSparkline data={row.spark} positive={row.dailyPct >= 0} />
|
|
614
|
+
),
|
|
615
|
+
},
|
|
616
|
+
];
|
|
617
|
+
|
|
618
|
+
function MetricCard({
|
|
619
|
+
value,
|
|
620
|
+
change,
|
|
621
|
+
label,
|
|
622
|
+
caption,
|
|
623
|
+
}: {
|
|
624
|
+
value: string;
|
|
625
|
+
change: string;
|
|
626
|
+
label: string;
|
|
627
|
+
caption: string;
|
|
628
|
+
}) {
|
|
629
|
+
const positive = !change.startsWith('-');
|
|
630
|
+
return (
|
|
631
|
+
<Card>
|
|
632
|
+
<VStack gap={2}>
|
|
633
|
+
<Heading level={4}>{label}</Heading>
|
|
634
|
+
<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>
|
|
646
|
+
</HStack>
|
|
647
|
+
<Text type="supporting" color="secondary">
|
|
648
|
+
{caption}
|
|
649
|
+
</Text>
|
|
650
|
+
</VStack>
|
|
651
|
+
</Card>
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function AssetRow({
|
|
656
|
+
ticker,
|
|
657
|
+
name,
|
|
658
|
+
value,
|
|
659
|
+
change,
|
|
660
|
+
}: {
|
|
661
|
+
ticker: string;
|
|
662
|
+
name: string;
|
|
663
|
+
value: string;
|
|
664
|
+
change: string;
|
|
665
|
+
}) {
|
|
666
|
+
return (
|
|
667
|
+
<ListItem
|
|
668
|
+
label={<Text weight="bold">{ticker}</Text>}
|
|
669
|
+
description={name}
|
|
670
|
+
href="#/templates/dashboard-portfolio"
|
|
671
|
+
startContent={<Avatar name={ticker} size="md" />}
|
|
672
|
+
endContent={
|
|
673
|
+
<VStack gap={0} hAlign="end">
|
|
674
|
+
<Text type="body" hasTabularNumbers>
|
|
675
|
+
{value}
|
|
676
|
+
</Text>
|
|
677
|
+
<Badge
|
|
678
|
+
label={change}
|
|
679
|
+
variant={change.startsWith('-') ? 'red' : 'green'}
|
|
680
|
+
/>
|
|
681
|
+
</VStack>
|
|
682
|
+
}
|
|
683
|
+
/>
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// ============= SIDENAV =============
|
|
688
|
+
|
|
689
|
+
// ============= MAIN COMPONENT =============
|
|
690
|
+
|
|
691
|
+
export default function DashboardPortfolio() {
|
|
692
|
+
const [timeRange, setTimeRange] = useState('1 year');
|
|
693
|
+
|
|
694
|
+
return (
|
|
695
|
+
<Layout
|
|
696
|
+
height="fill"
|
|
697
|
+
content={
|
|
698
|
+
<LayoutContent padding={6}>
|
|
699
|
+
<VStack gap={6}>
|
|
700
|
+
{/* Page header */}
|
|
701
|
+
<HStack hAlign="between" vAlign="center">
|
|
702
|
+
<Heading level={1}>The night vault</Heading>
|
|
703
|
+
<DropdownMenu
|
|
704
|
+
button={{
|
|
705
|
+
label: timeRange,
|
|
706
|
+
variant: 'secondary',
|
|
707
|
+
size: 'lg',
|
|
708
|
+
}}
|
|
709
|
+
hasChevron
|
|
710
|
+
items={[
|
|
711
|
+
{label: '1 month', onClick: () => setTimeRange('1 month')},
|
|
712
|
+
{label: '3 months', onClick: () => setTimeRange('3 months')},
|
|
713
|
+
{label: '6 months', onClick: () => setTimeRange('6 months')},
|
|
714
|
+
{label: '1 year', onClick: () => setTimeRange('1 year')},
|
|
715
|
+
{label: '5 years', onClick: () => setTimeRange('5 years')},
|
|
716
|
+
{label: 'All time', onClick: () => setTimeRange('All time')},
|
|
717
|
+
]}
|
|
718
|
+
/>
|
|
719
|
+
</HStack>
|
|
720
|
+
|
|
721
|
+
{/* KPI metric cards */}
|
|
722
|
+
<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>
|
|
729
|
+
))}
|
|
730
|
+
</Grid>
|
|
731
|
+
|
|
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>
|
|
760
|
+
</Grid>
|
|
761
|
+
|
|
762
|
+
<Divider />
|
|
763
|
+
|
|
764
|
+
{/* Market section */}
|
|
765
|
+
<HStack hAlign="between" vAlign="start">
|
|
766
|
+
<VStack gap={1}>
|
|
767
|
+
<Heading level={2}>Market at midnight</Heading>
|
|
768
|
+
<Text type="body" color="secondary">
|
|
769
|
+
Past 24 hours under moonlight
|
|
770
|
+
</Text>
|
|
771
|
+
</VStack>
|
|
772
|
+
<Button label="View more" variant="secondary" size="lg" />
|
|
773
|
+
</HStack>
|
|
774
|
+
|
|
775
|
+
{/* Market index cards */}
|
|
776
|
+
<Grid columns={{minWidth: 320, repeat: 'fit'}} gap={4}>
|
|
777
|
+
{marketIndices.map(m => (
|
|
778
|
+
<MarketCard key={m.ticker} {...m} />
|
|
779
|
+
))}
|
|
780
|
+
</Grid>
|
|
781
|
+
|
|
782
|
+
{/* Trending stocks table */}
|
|
783
|
+
<Card>
|
|
784
|
+
<VStack gap={4}>
|
|
785
|
+
<Heading level={3}>Trending Stocks</Heading>
|
|
786
|
+
<Table<StockRow>
|
|
787
|
+
data={trendingStocks}
|
|
788
|
+
columns={trendingColumns}
|
|
789
|
+
idKey="id"
|
|
790
|
+
hasHover
|
|
791
|
+
dividers="rows"
|
|
792
|
+
/>
|
|
793
|
+
</VStack>
|
|
794
|
+
</Card>
|
|
795
|
+
</VStack>
|
|
796
|
+
</LayoutContent>
|
|
797
|
+
}
|
|
798
|
+
/>
|
|
799
|
+
);
|
|
800
|
+
}
|