astryx-dracula 0.4.0 → 0.5.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 (50) hide show
  1. package/package.json +6 -2
  2. package/shared/auth-chrome.tsx +156 -6
  3. package/shared/chaptered-doc.tsx +3 -4
  4. package/shared/chart-hues.ts +0 -5
  5. package/shared/chart-legend.tsx +2 -3
  6. package/shared/data-bar.tsx +5 -5
  7. package/shared/format-money.ts +3 -0
  8. package/shared/metric-card.tsx +127 -0
  9. package/shared/scene-hues.ts +0 -8
  10. package/shared/scene-tile.tsx +3 -11
  11. package/shared/settings-data.ts +28 -0
  12. package/shared/settings-rows.tsx +1 -6
  13. package/shared/stars.tsx +28 -0
  14. package/templates/ai-chat-landing.tsx +88 -58
  15. package/templates/ai-chat.tsx +65 -78
  16. package/templates/centered-hero.tsx +2 -2
  17. package/templates/dashboard-portfolio.tsx +77 -155
  18. package/templates/dashboard.tsx +17 -134
  19. package/templates/detail-page.tsx +14 -18
  20. package/templates/documentation-design.tsx +66 -519
  21. package/templates/documentation-technical.tsx +2 -22
  22. package/templates/editor.tsx +168 -185
  23. package/templates/file-explorer.tsx +10 -24
  24. package/templates/ide.tsx +12 -18
  25. package/templates/incident-console.tsx +48 -46
  26. package/templates/kanban-board.tsx +36 -85
  27. package/templates/library.tsx +42 -66
  28. package/templates/login-card.tsx +33 -126
  29. package/templates/login-split.tsx +91 -123
  30. package/templates/login-sso.tsx +12 -33
  31. package/templates/login.tsx +21 -121
  32. package/templates/messaging-shell.tsx +162 -168
  33. package/templates/payment-form.tsx +24 -26
  34. package/templates/product-detail.tsx +7 -40
  35. package/templates/product-tour.tsx +38 -60
  36. package/templates/settings-dialog.tsx +10 -32
  37. package/templates/settings-sidebar.tsx +44 -77
  38. package/templates/settings.tsx +208 -121
  39. package/templates/shell-nav.tsx +20 -75
  40. package/templates/shell-side-nav.tsx +16 -90
  41. package/templates/shell-top-nav.tsx +20 -53
  42. package/templates/side-gallery.tsx +19 -19
  43. package/templates/table-grouped.tsx +17 -164
  44. package/templates/table-page-chart.tsx +12 -11
  45. package/templates/table-page-heatmap-status.tsx +0 -17
  46. package/templates/table-page-shoe-store-heatmap.tsx +12 -12
  47. package/templates/table-page.tsx +4 -31
  48. package/templates/table.tsx +7 -10
  49. package/templates/tech-report.tsx +58 -72
  50. package/templates/theme-showcase.tsx +124 -415
@@ -1,13 +1,10 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
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)
3
+ // L[h=fill] > LC[p=6] > V[g=6] > (H[j=between a=center] > Hd"The night vault"[level=1] + Tx"1 year"[t=body]) + (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
4
 
5
5
  /**
6
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
7
  */
8
-
9
- import {useState} from 'react';
10
-
11
8
  import {VStack, HStack, Layout, LayoutContent} from '@astryxdesign/core/Layout';
12
9
  import {Text, Heading} from '@astryxdesign/core/Text';
13
10
  import {Card} from '@astryxdesign/core/Card';
@@ -15,10 +12,10 @@ import {Grid} from '@astryxdesign/core/Grid';
15
12
  import {Link} from '@astryxdesign/core/Link';
16
13
  import {Avatar} from '@astryxdesign/core/Avatar';
17
14
  import {List, ListItem} from '@astryxdesign/core/List';
18
- import {DropdownMenu} from '@astryxdesign/core/DropdownMenu';
19
15
  import {Button} from '@astryxdesign/core/Button';
20
- import {Table, proportional, pixel} from '@astryxdesign/core/Table';
21
- import type {TableColumn} from '@astryxdesign/core/Table';
16
+ import {proportional, pixel} from '@astryxdesign/core/Table';
17
+ import type {TableColumn, PixelWidth} from '@astryxdesign/core/Table';
18
+ import {MetricCard, TableCard} from 'astryx-dracula/shared/metric-card';
22
19
  import {Divider} from '@astryxdesign/core/Divider';
23
20
  import {MetricDelta} from 'astryx-dracula/shared/metric-delta';
24
21
  import {Sparkline, type SparkPoint} from 'astryx-dracula/shared/sparkline';
@@ -142,39 +139,13 @@ const topAssets = [
142
139
  ];
143
140
 
144
141
  // 96 points per series = one tick every 15 minutes across a 24h window.
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.
148
- function genSpark(
149
- seed: number,
150
- start: number,
151
- end: number,
152
- volatility: number,
153
- N: number = 96,
154
- ): SparkPoint[] {
155
- let s = seed >>> 0;
156
- const rand = () => {
157
- s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
158
- return s / 0x100000000;
159
- };
160
- const points: SparkPoint[] = [];
161
- let drift = 0;
162
- for (let i = 0; i < N; i++) {
163
- const t = i / (N - 1);
164
- const trend = start + (end - start) * t;
165
- // Multiple overlaid waves at different frequencies for session-like rhythm
166
- // plus high-frequency chop.
167
- const wave =
168
- Math.sin(t * Math.PI * 2.3 + seed * 0.13) * volatility * 1.4 +
169
- Math.sin(t * Math.PI * 5.7 + seed * 0.41) * volatility * 0.9 +
170
- Math.sin(t * Math.PI * 13.1 + seed * 0.07) * volatility * 0.5;
171
- // Loosely-correlated random walk so adjacent ticks jitter rather than glide.
172
- drift = drift * 0.55 + (rand() - 0.5) * volatility * 2.2;
173
- // Occasional sharper spike to mimic news-driven moves.
174
- const spike = rand() < 0.04 ? (rand() - 0.5) * volatility * 4 : 0;
175
- points.push({id: `${seed}-${i}`, value: trend + wave + drift + spike});
176
- }
177
- return points;
142
+ // Static arrays: the dashboard metric tiles already establish the toSparkSeries
143
+ // precedent (static number rows + stable ids), so these print the same LCG
144
+ // output the generator produced instead of rerunning it per render. Every
145
+ // sample carries the slot it was generated for, so the bars key off data
146
+ // rather than off the array position.
147
+ function toSparkSeries(seed: number, values: number[]): SparkPoint[] {
148
+ return values.map((value, i) => ({id: `${seed}-${i}`, value}));
178
149
  }
179
150
 
180
151
  // Market index cards — 24h sparkline data (every 15min, 96 points)
@@ -185,7 +156,8 @@ const marketIndices = [
185
156
  price: '43,821.67',
186
157
  change: '+0.42%',
187
158
  positive: true,
188
- spark: genSpark(1337, 78, 84, 3.2),
159
+ // prettier-ignore
160
+ spark: toSparkSeries(1337, [77.87, 75.96, 76.35, 77.99, 78.95, 77.57, 78.14, 75.12, 74.24, 74.03, 72.88, 71.93, 74.65, 71.43, 69.56, 74.19, 72.66, 72.02, 76.09, 74.07, 75.82, 78.14, 77.91, 80.15, 79.18, 76.96, 76.57, 81.07, 83.59, 83.21, 81.58, 83.82, 84.82, 88.56, 87.03, 89.64, 87.35, 89.63, 90.67, 87.45, 87.4, 86.09, 87.85, 82.63, 81.81, 84.78, 82.26, 83.25, 80.04, 80.63, 83.56, 84.28, 83.96, 84.49, 80.85, 79.3, 80.45, 84.58, 81.83, 83.45, 87.92, 90.46, 89.1, 88.1, 90.6, 87.26, 84.82, 84.27, 86.7, 82.89, 84.38, 89.24, 82.39, 82.18, 82.24, 81.38, 82.38, 78.55, 76.91, 75.31, 78.55, 76.46, 73.89, 76.36, 77.01, 79.15, 78.98, 79.79, 78.68, 77.35, 76.01, 84.41, 82.31, 81.97, 80.42, 80.29]),
189
161
  },
190
162
  {
191
163
  name: 'NASDAQ',
@@ -193,7 +165,8 @@ const marketIndices = [
193
165
  price: '18,942.18',
194
166
  change: '-0.50%',
195
167
  positive: false,
196
- spark: genSpark(2042, 86, 78, 3.8),
168
+ // prettier-ignore
169
+ spark: toSparkSeries(2042, [88.89, 94.82, 92.53, 93.22, 91.88, 91.07, 90.86, 89.3, 89.42, 85.69, 88.02, 82.52, 84.79, 86.08, 81.15, 78.83, 79.56, 80.01, 83.14, 84.1, 88.09, 84.29, 82.48, 84.9, 84.15, 86.04, 87.12, 86.85, 82.29, 84.8, 81.87, 79.88, 79.57, 78.77, 82.32, 83.19, 85.35, 86.71, 87.68, 80, 74.7, 73.28, 77.07, 71.96, 68.62, 70.7, 73.72, 74.67, 68.28, 71.29, 69.49, 71.73, 72.22, 73.75, 77.29, 79.86, 76.32, 78.94, 78.77, 77.52, 78.62, 80.01, 85.09, 86.36, 83.71, 88.49, 89.99, 86.02, 89.11, 83.46, 84.72, 85.43, 85.97, 84.73, 86.18, 88.06, 82.82, 82.32, 87.12, 82.77, 79.32, 84.78, 84.46, 80.3, 84.21, 82.15, 84.84, 75.1, 82.07, 85.92, 78.74, 82.77, 82.99, 87.84, 88.57, 90.07]),
197
170
  },
198
171
  {
199
172
  name: 'S&P 500',
@@ -201,7 +174,8 @@ const marketIndices = [
201
174
  price: '5,918.33',
202
175
  change: '+0.21%',
203
176
  positive: true,
204
- spark: genSpark(3155, 71, 75, 2.8),
177
+ // prettier-ignore
178
+ spark: toSparkSeries(3155, [73.95, 72.03, 71.08, 71.6, 73.2, 72.67, 74.14, 73.45, 76.38, 75.1, 77.61, 79.72, 77.52, 79.27, 78.25, 75.63, 76.23, 76.58, 72.42, 72.4, 69.84, 66.76, 67.12, 67.73, 64.69, 68.27, 65.01, 64.02, 66.05, 69.6, 68.7, 70.05, 69.54, 65.52, 66.42, 66.58, 69.36, 71.28, 71.66, 73.08, 70.32, 69.85, 72.45, 72.94, 72.02, 74.88, 71.61, 71.7, 72.69, 74.3, 74.46, 71.99, 70.45, 70.11, 72.53, 69.96, 71.6, 72, 74.4, 70.78, 72.74, 71.44, 71.37, 73.54, 71.05, 72.86, 70.79, 69.5, 73.43, 72.36, 73.76, 75.05, 77.58, 77.17, 78.5, 82.25, 78.78, 78.42, 78.07, 82.01, 78.82, 78.48, 78.36, 79.8, 79, 76.6, 75.18, 75.82, 78.39, 80.59, 80.93, 79.71, 78.44, 74.41, 74.09, 75.98]),
205
179
  },
206
180
  {
207
181
  name: 'NYSE Composite',
@@ -209,7 +183,8 @@ const marketIndices = [
209
183
  price: '19,752.41',
210
184
  change: '-0.20%',
211
185
  positive: false,
212
- spark: genSpark(4287, 70, 65, 3.0),
186
+ // prettier-ignore
187
+ spark: toSparkSeries(4287, [64.45, 61.11, 62.04, 60.88, 61.84, 66.42, 65.95, 68.68, 66.53, 66.2, 64.61, 66.78, 65.37, 64.69, 64.48, 60.48, 70.11, 69.62, 67.37, 70.31, 73.61, 74.74, 71.95, 68.83, 68.85, 66.69, 65.72, 64.01, 65.91, 64.38, 68.51, 63.62, 68.71, 66.45, 66.61, 70.22, 67.86, 68.96, 72.44, 74.53, 74.4, 75.52, 72.63, 69.2, 67.72, 72.46, 75.34, 74.64, 76.13, 73.74, 73.53, 74.47, 76.73, 76.4, 74.91, 74.34, 73.92, 70.94, 71.5, 68.68, 68.44, 65.97, 68.07, 69.63, 70.45, 68.45, 65.12, 63.93, 64.71, 66.68, 61.08, 58, 61.79, 63.61, 65.05, 62.4, 68.06, 65.87, 66.63, 68.18, 68.06, 67.32, 64.23, 65.72, 63.78, 62.09, 63.94, 61.55, 59.14, 59.95, 58.74, 59.44, 61.25, 63.96, 60.62, 59.61]),
213
188
  },
214
189
  {
215
190
  name: 'NVIDIA Corp.',
@@ -217,7 +192,8 @@ const marketIndices = [
217
192
  price: '$177.39',
218
193
  change: '+0.93%',
219
194
  positive: true,
220
- spark: genSpark(5519, 60, 67, 3.6),
195
+ // prettier-ignore
196
+ spark: toSparkSeries(5519, [66.26, 68.28, 68.39, 65.49, 62.97, 68.15, 71.28, 67.14, 71.53, 68.43, 69.39, 73.97, 67.92, 57.1, 61.58, 64.45, 59.15, 55.38, 56.78, 60.73, 63.46, 59.3, 56.58, 56.55, 59.43, 58.93, 57.88, 57.25, 56.11, 61.79, 62.87, 65.09, 66.15, 59.93, 60.64, 63.97, 65.3, 65.5, 66.02, 67.5, 67.43, 61.9, 59.69, 59.48, 59.88, 61.71, 59.95, 54.99, 52.18, 51.09, 52.02, 53.2, 55.88, 60.58, 56.65, 58.89, 55.9, 58.03, 60.31, 62.14, 57.94, 61.24, 60.94, 59.09, 64.39, 63.59, 64.45, 65.84, 66.83, 69.01, 68.41, 71.9, 74.79, 69.82, 67.76, 68.77, 67.7, 65.13, 62.97, 66.86, 69.31, 69.64, 67.68, 67.14, 67.12, 68.13, 61.27, 66.8, 63.53, 68.48, 64.28, 68.53, 69.09, 66.14, 66.67, 72.57]),
221
197
  },
222
198
  {
223
199
  name: 'Intel Corp.',
@@ -225,7 +201,8 @@ const marketIndices = [
225
201
  price: '$50.38',
226
202
  change: '+4.89%',
227
203
  positive: true,
228
- spark: genSpark(6701, 40, 51, 2.6),
204
+ // prettier-ignore
205
+ spark: toSparkSeries(6701, [40.29, 36.52, 38.65, 38.66, 38.73, 39.59, 39.74, 40.27, 37.68, 37.74, 38.19, 38.45, 34.69, 33.53, 36.19, 35.7, 37.18, 36.76, 36.8, 38.82, 38.54, 36.97, 38.21, 39.09, 43.08, 45.5, 42.94, 40.59, 40.6, 43.51, 42.51, 44.7, 47.77, 46.53, 44.78, 45.66, 45.29, 47.54, 45.72, 47.71, 47.55, 49.13, 47.53, 43.52, 41.69, 40.58, 41.74, 46.68, 47.53, 49.99, 46.64, 49.91, 49.01, 51.21, 47.53, 45.66, 47.51, 48.7, 50.76, 53.95, 48.88, 50.92, 51.45, 51.69, 49.34, 52.86, 55.07, 51.08, 50.1, 49.78, 49.18, 49.55, 49.15, 45.34, 47.77, 45.99, 43.61, 44.84, 48.01, 46.33, 43.42, 46.93, 46.35, 47.6, 45.09, 45.85, 43.72, 41.2, 42.89, 42.83, 43.48, 46.41, 49.01, 48.49, 48.68, 51.78]),
229
206
  },
230
207
  {
231
208
  name: 'Nokia Corp.',
@@ -233,7 +210,8 @@ const marketIndices = [
233
210
  price: '$8.82',
234
211
  change: '+6.65%',
235
212
  positive: true,
236
- spark: genSpark(7823, 30, 41, 2.4),
213
+ // prettier-ignore
214
+ spark: toSparkSeries(7823, [27.46, 26.98, 25.8, 25.95, 22.92, 23.25, 27.1, 28.27, 26.12, 28.96, 28.95, 27.78, 29.41, 30.28, 30.85, 32.49, 31.78, 32.11, 34.47, 36.23, 36.59, 37.96, 35.38, 36.79, 37.14, 37.87, 36.61, 35.2, 39.04, 40.18, 41.09, 41.92, 38.86, 36.21, 33.95, 35.27, 35.61, 35.38, 37.29, 37.02, 36.76, 34.27, 34.9, 34.79, 33.48, 34.53, 35.99, 38.24, 37.76, 39.23, 38.1, 36.08, 35.04, 36.38, 37.29, 39.2, 41.21, 42.11, 41.35, 38.23, 36.64, 37.97, 39.03, 39.17, 36.98, 35.69, 33.04, 31.86, 38.61, 35.08, 35.33, 36.35, 34.24, 36.73, 32.08, 32.11, 32.87, 31.23, 33.2, 33.36, 32.8, 31.44, 32.34, 29.65, 36.02, 38.33, 39.14, 42.58, 44.72, 43.06, 40.97, 40.47, 41.21, 40.21, 40.21, 40.11]),
237
215
  },
238
216
  {
239
217
  name: 'Tesla, Inc.',
@@ -241,7 +219,8 @@ const marketIndices = [
241
219
  price: '$360.59',
242
220
  change: '-5.42%',
243
221
  positive: false,
244
- spark: genSpark(8945, 90, 76, 4.2),
222
+ // prettier-ignore
223
+ spark: toSparkSeries(8945, [89.21, 88.42, 91.95, 94.9, 97.2, 93.76, 95.02, 93.99, 96.41, 93.71, 92.92, 95.4, 91.94, 97.48, 96.94, 95.45, 92.4, 93.76, 96.33, 97.28, 96.74, 92.99, 92.26, 90.48, 93.76, 92.11, 86.82, 83.2, 87.28, 88.08, 86.09, 81.46, 77.09, 79.91, 85.21, 80.13, 85.23, 80.03, 81.33, 84.31, 84.52, 84.57, 81.19, 81.81, 75.98, 77.99, 77.95, 75.93, 76.87, 81.09, 83.04, 82.12, 78.12, 78.69, 77.63, 79.19, 74.18, 75.44, 77.03, 77.73, 71.89, 76.98, 74.25, 77.95, 77.4, 81.04, 78.54, 77.9, 78.6, 76.53, 73.58, 71.02, 71.9, 70.44, 76.85, 77.69, 76.22, 81.78, 82.78, 85.32, 87.59, 91.37, 88.55, 86.27, 84.09, 86.52, 85.12, 84.64, 86.29, 82.58, 81.71, 86.28, 88.26, 83.47, 80.1, 79.92]),
245
224
  },
246
225
  ];
247
226
 
@@ -264,7 +243,8 @@ const trendingStocks: StockRow[] = [
264
243
  dailyPts: 1.35,
265
244
  dailyPct: 0.72,
266
245
  weekChg: 22.4,
267
- spark: genSpark(11023, 60, 70, 2.6, 48),
246
+ // prettier-ignore
247
+ spark: toSparkSeries(11023, [62.59, 62.87, 66.28, 66.94, 63.05, 60.71, 60.87, 63.01, 65.63, 66.72, 66.14, 65.67, 63.28, 63.24, 64.03, 65.31, 69.18, 67.03, 67.02, 65.16, 63.09, 63.23, 59.79, 57.91, 60.58, 62.7, 59.12, 60.59, 60.07, 64.53, 64.61, 68.12, 66.44, 67.6, 65.99, 65.37, 67.56, 64.35, 65.35, 66.14, 65.38, 65.53, 63.89, 68.35, 70.22, 72.78, 73.31, 77.18]),
268
248
  },
269
249
  {
270
250
  id: '2',
@@ -273,7 +253,8 @@ const trendingStocks: StockRow[] = [
273
253
  dailyPts: 3.2,
274
254
  dailyPct: 0.78,
275
255
  weekChg: 18.6,
276
- spark: genSpark(12591, 55, 65, 2.4, 48),
256
+ // prettier-ignore
257
+ spark: toSparkSeries(12591, [52.61, 54.67, 54.37, 52.39, 53.13, 54.24, 53.77, 56.96, 54.21, 56.36, 52.87, 52.91, 52.91, 53.06, 54.05, 56.77, 55.22, 53.5, 55.24, 55.21, 55.57, 57.52, 58.11, 60.48, 62.67, 64.05, 67.62, 67.64, 70.11, 69.7, 67.71, 66.07, 64.31, 61.5, 60.25, 60.01, 62.04, 63.71, 64.69, 65.02, 63.57, 64.63, 65.9, 61.94, 64.71, 63.25, 60.84, 62.99]),
277
258
  },
278
259
  {
279
260
  id: '3',
@@ -282,7 +263,8 @@ const trendingStocks: StockRow[] = [
282
263
  dailyPts: 1.65,
283
264
  dailyPct: 0.93,
284
265
  weekChg: 45.2,
285
- spark: genSpark(13744, 40, 68, 3.8, 48),
266
+ // prettier-ignore
267
+ spark: toSparkSeries(13744, [42.97, 43.84, 42.55, 43.07, 41.45, 45.69, 47.46, 45.15, 47.75, 43.08, 44.46, 37.8, 37.94, 35.72, 35.14, 32.33, 41.42, 45.22, 46.61, 45, 44.99, 48.42, 56.57, 59.5, 54.67, 55.56, 53.34, 54.15, 56.29, 57.95, 59.47, 59.02, 58.39, 61.6, 59.14, 66.24, 68.92, 69.13, 73.86, 76.15, 73.14, 74.89, 71.4, 71.95, 74.65, 69.61, 63.98, 63.68]),
286
268
  },
287
269
  {
288
270
  id: '4',
@@ -291,7 +273,8 @@ const trendingStocks: StockRow[] = [
291
273
  dailyPts: -0.8,
292
274
  dailyPct: -0.43,
293
275
  weekChg: 15.3,
294
- spark: genSpark(14882, 70, 63, 2.5, 48),
276
+ // prettier-ignore
277
+ spark: toSparkSeries(14882, [65.57, 69.23, 72.54, 70.58, 69.7, 67.79, 66.66, 69.84, 72.05, 73.69, 71.33, 72.56, 68.97, 67.02, 69.25, 68.04, 70.12, 73.38, 71.49, 70.24, 70.59, 69.81, 70.5, 66.45, 63.72, 61.63, 63.06, 60.22, 61.87, 61.91, 61.4, 61.43, 62.03, 63.27, 65.56, 61.76, 62.77, 62.52, 63.4, 65.86, 61.54, 60.71, 61.09, 66, 62.46, 64, 67.5, 65.32]),
295
278
  },
296
279
  {
297
280
  id: '5',
@@ -300,7 +283,8 @@ const trendingStocks: StockRow[] = [
300
283
  dailyPts: 2.1,
301
284
  dailyPct: 1.37,
302
285
  weekChg: 12.8,
303
- spark: genSpark(15967, 50, 60, 2.7, 48),
286
+ // prettier-ignore
287
+ spark: toSparkSeries(15967, [50.19, 52.48, 56.14, 57.13, 53.58, 52.4, 49.76, 51.71, 53.73, 52.83, 47.01, 48.64, 46.49, 44.71, 45.84, 47.51, 51.21, 51.75, 47.07, 53.25, 53.02, 56.04, 53.9, 54.27, 53.09, 56.7, 54.86, 53.61, 54.39, 55.27, 58.44, 59.21, 58.89, 60.97, 63.5, 64.41, 64.28, 65.48, 63.86, 62.88, 65.82, 64.06, 60.89, 57.93, 60.15, 57.94, 57.86, 54.45]),
304
288
  },
305
289
  {
306
290
  id: '6',
@@ -309,7 +293,8 @@ const trendingStocks: StockRow[] = [
309
293
  dailyPts: 4.5,
310
294
  dailyPct: 0.9,
311
295
  weekChg: 35.1,
312
- spark: genSpark(17105, 45, 61, 3.0, 48),
296
+ // prettier-ignore
297
+ spark: toSparkSeries(17105, [46.75, 47.76, 46.13, 45.91, 45.34, 49.3, 50.87, 48.9, 46.85, 49.07, 50.88, 54.37, 54.34, 54.17, 52.62, 52.71, 55.65, 55.18, 57.28, 59.71, 56.07, 57.36, 56.8, 52.95, 49.59, 50.35, 50.11, 48.92, 51.6, 52.4, 50.29, 52.17, 54.87, 55.44, 54.27, 52.27, 53.76, 55.31, 55.75, 56.16, 57.8, 58.73, 54.6, 53.58, 55.27, 55.62, 62.45, 62.94]),
313
298
  },
314
299
  {
315
300
  id: '7',
@@ -318,7 +303,8 @@ const trendingStocks: StockRow[] = [
318
303
  dailyPts: -20.67,
319
304
  dailyPct: -5.42,
320
305
  weekChg: -8.3,
321
- spark: genSpark(18249, 90, 64, 4.4, 48),
306
+ // prettier-ignore
307
+ spark: toSparkSeries(18249, [83.75, 78.44, 81.1, 79.12, 76.61, 84.38, 84.64, 87.7, 86.31, 80.02, 74.82, 75.1, 75.36, 73.06, 74.86, 75.38, 78.07, 78.02, 75.42, 81.25, 85.54, 86.68, 84.46, 86.89, 80.57, 84.1, 82.48, 85.83, 87.06, 79.95, 74.26, 68.41, 64.98, 70.44, 71.17, 74.21, 69.56, 73.59, 70.47, 70.24, 65.12, 64.41, 69.18, 62.39, 63.5, 59.78, 59.96, 60.64]),
322
308
  },
323
309
  {
324
310
  id: '8',
@@ -327,7 +313,8 @@ const trendingStocks: StockRow[] = [
327
313
  dailyPts: 2.35,
328
314
  dailyPct: 4.89,
329
315
  weekChg: -12.5,
330
- spark: genSpark(19388, 65, 57, 2.8, 48),
316
+ // prettier-ignore
317
+ spark: toSparkSeries(19388, [71.45, 75.23, 76.21, 71.96, 69.34, 69.92, 69.21, 65.68, 62.87, 64.07, 64.44, 62.98, 59.15, 61.12, 62.74, 64.13, 62.29, 65.14, 60.86, 58.23, 56.22, 57.6, 60.39, 57.87, 57.04, 54.84, 55.29, 56.38, 56.92, 58.48, 61.09, 59.01, 58.65, 61.44, 61.55, 58.75, 61.48, 60.6, 58.99, 61.4, 56.7, 57.42, 59.98, 57.74, 64.44, 59.11, 59.16, 59.45]),
331
318
  },
332
319
  {
333
320
  id: '9',
@@ -336,7 +323,8 @@ const trendingStocks: StockRow[] = [
336
323
  dailyPts: -1.2,
337
324
  dailyPct: -0.73,
338
325
  weekChg: 28.7,
339
- spark: genSpark(20471, 50, 63, 2.9, 48),
326
+ // prettier-ignore
327
+ spark: toSparkSeries(20471, [44.8, 46.66, 43.7, 46.12, 47.65, 50.65, 48.05, 52.96, 54.41, 54.91, 53.77, 49.18, 50.01, 52.14, 49.46, 49.81, 52.08, 52.56, 50.02, 50.61, 56.27, 56.79, 62.63, 61.99, 65.99, 62.88, 59.6, 57.44, 58.61, 62.78, 62, 59.58, 57.36, 54.92, 59.1, 55.37, 64.03, 63.13, 63.88, 65.3, 65.22, 60.4, 62.84, 60.12, 59.42, 65.11, 58, 57.8]),
340
328
  },
341
329
  {
342
330
  id: '10',
@@ -345,7 +333,8 @@ const trendingStocks: StockRow[] = [
345
333
  dailyPts: 5.4,
346
334
  dailyPct: 0.87,
347
335
  weekChg: 42.1,
348
- spark: genSpark(21556, 42, 58, 2.6, 48),
336
+ // prettier-ignore
337
+ spark: toSparkSeries(21556, [42.07, 41.72, 43.08, 43.95, 42.71, 43, 47.8, 51.53, 49.82, 51.72, 51.75, 54.86, 54.64, 54.28, 55.52, 55.2, 53.51, 49.34, 45.58, 51.95, 44.7, 46.57, 48.92, 47.16, 49.62, 47.2, 47.47, 49.84, 51.12, 53.9, 53.39, 51.34, 51.56, 47.93, 50.31, 50.69, 50.32, 50.04, 53.93, 52.8, 52.46, 55.01, 56.66, 58.11, 59.39, 60.81, 62.81, 64]),
349
338
  },
350
339
  ];
351
340
 
@@ -479,6 +468,17 @@ function MarketCard({
479
468
  const formatSigned = (value: number, digits: number, suffix = '') =>
480
469
  `${value >= 0 ? '+' : ''}${value.toFixed(digits)}${suffix}`;
481
470
 
471
+ const chgColumns: Array<{
472
+ key: 'dailyPts' | 'dailyPct' | 'weekChg';
473
+ header: string;
474
+ width: PixelWidth;
475
+ digits: number;
476
+ suffix: string;
477
+ }> = [
478
+ {key: 'dailyPts', header: 'Chg (pts)', width: pixel(104), digits: 2, suffix: ''},
479
+ {key: 'dailyPct', header: 'Chg (%)', width: pixel(96), digits: 2, suffix: '%'},
480
+ {key: 'weekChg', header: '52W (%)', width: pixel(96), digits: 1, suffix: '%'},
481
+ ];
482
482
  const trendingColumns: TableColumn<StockRow>[] = [
483
483
  {
484
484
  key: 'ticker',
@@ -500,39 +500,17 @@ const trendingColumns: TableColumn<StockRow>[] = [
500
500
  </Text>
501
501
  ),
502
502
  },
503
- {
504
- key: 'dailyPts',
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
- ),
513
- },
514
- {
515
- key: 'dailyPct',
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
- ),
524
- },
525
- {
526
- key: 'weekChg',
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
- ),
535
- },
503
+ ...chgColumns.map(column => ({
504
+ key: column.key,
505
+ header: column.header,
506
+ width: column.width,
507
+ renderCell: (row: StockRow) => {
508
+ const value = row[column.key] as number;
509
+ return (
510
+ <MetricDelta value={formatSigned(value, column.digits, column.suffix)} positive={value >= 0} />
511
+ );
512
+ },
513
+ })),
536
514
  {
537
515
  key: 'spark',
538
516
  header: '24h Trend',
@@ -549,37 +527,6 @@ const trendingColumns: TableColumn<StockRow>[] = [
549
527
  },
550
528
  ];
551
529
 
552
- function MetricCard({
553
- value,
554
- change,
555
- label,
556
- caption,
557
- }: {
558
- value: string;
559
- change: string;
560
- label: string;
561
- caption: string;
562
- }) {
563
- const positive = !change.startsWith('-');
564
- return (
565
- <Card>
566
- <VStack gap={2}>
567
- <Text type="supporting" color="secondary">
568
- {label}
569
- </Text>
570
- <HStack gap={2} vAlign="center">
571
- <Text type="display-3" weight="semibold" hasTabularNumbers>
572
- {value}
573
- </Text>
574
- <MetricDelta value={change} positive={positive} />
575
- </HStack>
576
- <Text type="supporting" color="secondary">
577
- {caption}
578
- </Text>
579
- </VStack>
580
- </Card>
581
- );
582
- }
583
530
 
584
531
  function AssetRow({
585
532
  ticker,
@@ -610,13 +557,9 @@ function AssetRow({
610
557
  );
611
558
  }
612
559
 
613
- // ============= SIDENAV =============
614
-
615
560
  // ============= MAIN COMPONENT =============
616
561
 
617
562
  export default function DashboardPortfolio() {
618
- const [timeRange, setTimeRange] = useState('1 year');
619
-
620
563
  return (
621
564
  <Layout
622
565
  height="fill"
@@ -626,22 +569,9 @@ export default function DashboardPortfolio() {
626
569
  {/* Page header */}
627
570
  <HStack hAlign="between" vAlign="center">
628
571
  <Heading level={1}>The night vault</Heading>
629
- <DropdownMenu
630
- button={{
631
- label: timeRange,
632
- variant: 'secondary',
633
- size: 'lg',
634
- }}
635
- hasChevron
636
- items={[
637
- {label: '1 month', onClick: () => setTimeRange('1 month')},
638
- {label: '3 months', onClick: () => setTimeRange('3 months')},
639
- {label: '6 months', onClick: () => setTimeRange('6 months')},
640
- {label: '1 year', onClick: () => setTimeRange('1 year')},
641
- {label: '5 years', onClick: () => setTimeRange('5 years')},
642
- {label: 'All time', onClick: () => setTimeRange('All time')},
643
- ]}
644
- />
572
+ <Text type="body" color="secondary">
573
+ 1 year
574
+ </Text>
645
575
  </HStack>
646
576
 
647
577
  {/* KPI metric cards */}
@@ -698,19 +628,11 @@ export default function DashboardPortfolio() {
698
628
  </Grid>
699
629
 
700
630
  {/* Trending stocks table */}
701
- <Card>
702
- <VStack gap={4}>
703
- <Heading level={3}>Trending stocks</Heading>
704
- <Table<StockRow>
705
- data={trendingStocks}
706
- columns={trendingColumns}
707
- idKey="id"
708
- textOverflow="truncate"
709
- hasHover
710
- dividers="rows"
711
- />
712
- </VStack>
713
- </Card>
631
+ <TableCard
632
+ title="Trending stocks"
633
+ data={trendingStocks}
634
+ columns={trendingColumns}
635
+ />
714
636
  </VStack>
715
637
  </LayoutContent>
716
638
  }
@@ -11,12 +11,10 @@ import {VStack, HStack, Layout, LayoutContent} from '@astryxdesign/core/Layout';
11
11
  import {Text, Heading} from '@astryxdesign/core/Text';
12
12
  import {Card} from '@astryxdesign/core/Card';
13
13
  import {Button} from '@astryxdesign/core/Button';
14
- import {ProgressBar} from '@astryxdesign/core/ProgressBar';
15
14
  import {Grid} from '@astryxdesign/core/Grid';
16
- import {Table, proportional, pixel} from '@astryxdesign/core/Table';
15
+ import {proportional, pixel} from '@astryxdesign/core/Table';
17
16
  import type {TableColumn} from '@astryxdesign/core/Table';
18
17
  import {Divider} from '@astryxdesign/core/Divider';
19
- import {Link} from '@astryxdesign/core/Link';
20
18
  import {Icon} from '@astryxdesign/core/Icon';
21
19
 
22
20
  // Same-route hash: demo links stay focusable anchors without escaping the
@@ -28,8 +26,8 @@ const SELF_HASH = '#/templates/dashboard';
28
26
  import {RefreshCw, Square} from 'lucide-react';
29
27
  import {ChartLegend} from 'astryx-dracula/shared/chart-legend';
30
28
  import {CHART_HUES} from 'astryx-dracula/shared/chart-hues';
31
- import {MetricDelta} from 'astryx-dracula/shared/metric-delta';
32
- import {Sparkline, type SparkPoint} from 'astryx-dracula/shared/sparkline';
29
+ import type {SparkPoint} from 'astryx-dracula/shared/sparkline';
30
+ import {MetricCard, TableCard, BarCell, CountCell} from 'astryx-dracula/shared/metric-card';
33
31
  import {CHART_PANEL_STYLE} from 'astryx-dracula/shared/chart-panel-style';
34
32
 
35
33
  // ============= DATA =============
@@ -352,38 +350,22 @@ const topPagesColumns: TableColumn<PageRow>[] = [
352
350
  header: 'Views',
353
351
  width: proportional(1),
354
352
  renderCell: (item: PageRow) => (
355
- <VStack gap={1}>
356
- <ProgressBar
357
- value={item.views}
358
- max={topPagesMaxViews}
359
- label={`${item.page} views`}
360
- isLabelHidden
361
- />
362
- <Text type="body" hasTabularNumbers maxLines={1}>
363
- {formatCount(item.views)}
364
- </Text>
365
- </VStack>
353
+ <BarCell label={`${item.page} views`} value={item.views} max={topPagesMaxViews}>
354
+ {formatCount(item.views)}
355
+ </BarCell>
366
356
  ),
367
357
  },
368
358
  {
369
359
  key: 'newUsers',
370
360
  header: 'New Users',
371
361
  width: pixel(104),
372
- renderCell: (item: PageRow) => (
373
- <Text hasTabularNumbers maxLines={1}>
374
- {item.newUsers}
375
- </Text>
376
- ),
362
+ renderCell: (item: PageRow) => <CountCell>{item.newUsers}</CountCell>,
377
363
  },
378
364
  {
379
365
  key: 'avgTime',
380
366
  header: 'Avg. Time',
381
367
  width: pixel(104),
382
- renderCell: (item: PageRow) => (
383
- <Text hasTabularNumbers maxLines={1}>
384
- {item.avgTime}
385
- </Text>
386
- ),
368
+ renderCell: (item: PageRow) => <CountCell>{item.avgTime}</CountCell>,
387
369
  },
388
370
  ];
389
371
 
@@ -418,50 +400,27 @@ const topEventsColumns: TableColumn<EventRow>[] = [
418
400
  header: 'Count',
419
401
  width: proportional(1),
420
402
  renderCell: (item: EventRow) => (
421
- <VStack gap={1}>
422
- <ProgressBar
423
- value={item.count}
424
- max={topEventsMaxCount}
425
- label={`${item.count}`}
426
- isLabelHidden
427
- />
428
- <Text type="body" hasTabularNumbers maxLines={1}>
429
- {formatCount(item.count)}
430
- </Text>
431
- </VStack>
403
+ <BarCell label={`${item.count}`} value={item.count} max={topEventsMaxCount}>
404
+ {formatCount(item.count)}
405
+ </BarCell>
432
406
  ),
433
407
  },
434
408
  {
435
409
  key: 'users',
436
410
  header: 'Users',
437
411
  width: pixel(72),
438
- renderCell: (item: EventRow) => (
439
- <Text hasTabularNumbers maxLines={1}>
440
- {formatCount(item.users)}
441
- </Text>
442
- ),
412
+ renderCell: (item: EventRow) => <CountCell>{formatCount(item.users)}</CountCell>,
443
413
  },
444
414
  {
445
415
  key: 'newUsers',
446
416
  header: 'New Users',
447
417
  width: pixel(104),
448
- renderCell: (item: EventRow) => (
449
- <Text hasTabularNumbers maxLines={1}>
450
- {formatCount(item.newUsers)}
451
- </Text>
452
- ),
418
+ renderCell: (item: EventRow) => <CountCell>{formatCount(item.newUsers)}</CountCell>,
453
419
  },
454
420
  ];
455
421
 
456
422
  // ============= CHART COMPONENTS =============
457
423
 
458
- // Chart series colors: desktop glows orange, mobile glows cyan. Sourced from
459
- // the shared categorical hues so data encoding can never reach for purple.
460
- const chartColors = {
461
- desktop: CHART_HUES.orange,
462
- mobile: CHART_HUES.cyan,
463
- };
464
-
465
424
  // Legend rows go through shared/chart-legend.tsx, which makes `label` a
466
425
  // REQUIRED field so a hue can never ship without the word that names it.
467
426
 
@@ -496,7 +455,7 @@ function ActiveUsersChart() {
496
455
  width={14}
497
456
  height={hAll - hDesktop}
498
457
  rx={4}
499
- fill={chartColors.mobile}
458
+ fill={CHART_HUES.cyan}
500
459
  />
501
460
  <rect
502
461
  x={x}
@@ -504,7 +463,7 @@ function ActiveUsersChart() {
504
463
  width={14}
505
464
  height={hDesktop}
506
465
  rx={4}
507
- fill={chartColors.desktop}
466
+ fill={CHART_HUES.orange}
508
467
  />
509
468
  {(i % 6 === 0 || i === bars.length - 1) && (
510
469
  <text
@@ -535,8 +494,8 @@ function ActiveUsersChart() {
535
494
  </Card>
536
495
  <ChartLegend
537
496
  entries={[
538
- {label: 'Desktop', color: chartColors.desktop},
539
- {label: 'Mobile', color: chartColors.mobile},
497
+ {label: 'Desktop', color: CHART_HUES.orange},
498
+ {label: 'Mobile', color: CHART_HUES.cyan},
540
499
  ]}
541
500
  gap={4}
542
501
  caption={
@@ -551,44 +510,6 @@ function ActiveUsersChart() {
551
510
 
552
511
  // ============= CARD COMPONENTS =============
553
512
 
554
- function MetricCard({
555
- label,
556
- value,
557
- change,
558
- positive,
559
- sparkline,
560
- }: {
561
- label: string;
562
- value: string;
563
- change: string;
564
- positive: boolean;
565
- sparkline: SparkPoint[];
566
- }) {
567
- return (
568
- <Card>
569
- <VStack gap={2}>
570
- <Text type="supporting" color="secondary">
571
- {label}
572
- </Text>
573
- <HStack gap={2} vAlign="center">
574
- <Text type="display-3" weight="semibold" hasTabularNumbers>
575
- {value}
576
- </Text>
577
- <MetricDelta value={change} positive={positive} />
578
- </HStack>
579
- <Text type="supporting" color="secondary">
580
- Last 30 days vs. Previous
581
- </Text>
582
- <Sparkline
583
- data={sparkline}
584
- label={`${label} thirty-day trend`}
585
- positive={positive}
586
- />
587
- </VStack>
588
- </Card>
589
- );
590
- }
591
-
592
513
  function StackedBarCard({
593
514
  title,
594
515
  data,
@@ -652,44 +573,6 @@ function StackedBarCard({
652
573
  );
653
574
  }
654
575
 
655
- // ============= TABLE COMPONENTS =============
656
-
657
- function TableCard<T extends {id: string}>({
658
- title,
659
- linkLabel,
660
- linkHref,
661
- data,
662
- columns,
663
- }: {
664
- title: string;
665
- linkLabel: string;
666
- linkHref: string;
667
- data: T[];
668
- columns: TableColumn<T>[];
669
- }) {
670
- return (
671
- <Card>
672
- <VStack gap={6}>
673
- <HStack hAlign="between" vAlign="center">
674
- <Heading level={3}>{title}</Heading>
675
- <Link href={linkHref}>{linkLabel}</Link>
676
- </HStack>
677
- <Table<T>
678
- data={data}
679
- columns={columns}
680
- idKey="id"
681
- density="compact"
682
- dividers="rows"
683
- textOverflow="truncate"
684
- hasHover
685
- />
686
- </VStack>
687
- </Card>
688
- );
689
- }
690
-
691
- // ============= SIDENAV =============
692
-
693
576
  // ============= MAIN COMPONENT =============
694
577
 
695
578
  // A definite ancestor, so `Layout height="fill"` (Layout.tsx:58-63, height: