diff options
| -rw-r--r-- | frontend/app/page.tsx | 2 | ||||
| -rw-r--r-- | frontend/app/prism-shell.css | 51 | ||||
| -rw-r--r-- | frontend/components/prism/VolumeCard.tsx | 51 |
3 files changed, 104 insertions, 0 deletions
diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 7ccb781..d6bde2a 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -11,6 +11,7 @@ import { KPIStrip } from "@/components/prism/KPIStrip"; import { Sidebar } from "@/components/prism/Sidebar"; import { TickerHeader } from "@/components/prism/TickerHeader"; import { TopBar } from "@/components/prism/TopBar"; +import { VolumeCard } from "@/components/prism/VolumeCard"; import { ApiError, api } from "@/lib/api"; import { deltaClass, fmtCurrency, fmtLarge, fmtNumber, fmtPct } from "@/lib/format"; import { buildKpis, limitIndices, marketClock, OVERVIEW_NAV_ITEMS, signalTone } from "@/lib/overview"; @@ -309,6 +310,7 @@ function OverviewClient() { <div className="psm-main-grid"> <div className="psm-column"> <ChartCard symbol={overview.profile.symbol} period={period} points={history} chartState={chartState} chartError={chartError} onChangePeriod={setPeriod} /> + <VolumeCard overview={overview} /> <SignalCard overview={overview} /> </div> <div className="psm-column"> diff --git a/frontend/app/prism-shell.css b/frontend/app/prism-shell.css index 7b9ccf4..ce9b3bd 100644 --- a/frontend/app/prism-shell.css +++ b/frontend/app/prism-shell.css @@ -1829,3 +1829,54 @@ width: 64px; } } + +/* ── Volume card ────────────────────────────────── */ + +.psm-vol-list { + display: flex; + flex-direction: column; + gap: var(--sp-3); +} + +.psm-vol-row { + display: grid; + grid-template-columns: 52px 1fr auto; + gap: var(--sp-3); + align-items: center; +} + +.psm-vol-label { + color: var(--fg-4); + font-size: var(--fs-12); + font-weight: 600; + letter-spacing: var(--tr-wider); + text-transform: uppercase; +} + +.psm-vol-track { + height: 4px; + border-radius: var(--r-full); + background: var(--ink-3); + overflow: hidden; +} + +.psm-vol-fill { + height: 100%; + border-radius: var(--r-full); + background: var(--info); + transition: width 300ms ease; +} + +.psm-vol-fill.accent { + background: var(--brass); +} + +.psm-vol-value { + color: var(--fg-2); + font-family: var(--font-mono); + font-size: var(--fs-13); + font-variant-numeric: tabular-nums; + text-align: right; + white-space: nowrap; + min-width: 60px; +} diff --git a/frontend/components/prism/VolumeCard.tsx b/frontend/components/prism/VolumeCard.tsx new file mode 100644 index 0000000..08b4059 --- /dev/null +++ b/frontend/components/prism/VolumeCard.tsx @@ -0,0 +1,51 @@ +import type { TickerOverview } from "@/types/api"; +import { fmtNumber } from "@/lib/format"; + +function activityLabel(ratio: number): string { + if (ratio >= 1.5) return "elevated activity"; + if (ratio < 0.7) return "below average"; + return "normal activity"; +} + +export function VolumeCard({ overview }: { overview: TickerOverview }) { + const today = overview.stats.volume; + const avg = overview.stats.average_volume; + if (today == null && avg == null) return null; + + const max = Math.max(today ?? 0, avg ?? 0); + const todayPct = today != null && max > 0 ? Math.round((today / max) * 100) : 0; + const avgPct = avg != null && max > 0 ? Math.round((avg / max) * 100) : 0; + const ratio = today != null && avg != null && avg > 0 ? today / avg : null; + + return ( + <section className="psm-card"> + <div className="psm-card-head"> + <div> + <div className="psm-eyebrow">Volume</div> + <h2 className="psm-card-title">Volume</h2> + </div> + </div> + <div className="psm-vol-list"> + <div className="psm-vol-row"> + <span className="psm-vol-label">Today</span> + <div className="psm-vol-track"> + <div className="psm-vol-fill accent" style={{ width: `${todayPct}%` }} /> + </div> + <span className="psm-vol-value">{today != null ? fmtNumber(today, 0) : "—"}</span> + </div> + <div className="psm-vol-row"> + <span className="psm-vol-label">30d avg</span> + <div className="psm-vol-track"> + <div className="psm-vol-fill" style={{ width: `${avgPct}%` }} /> + </div> + <span className="psm-vol-value">{avg != null ? fmtNumber(avg, 0) : "—"}</span> + </div> + </div> + {ratio != null && ( + <p className="psm-muted-copy" style={{ marginTop: "var(--sp-3)" }}> + {ratio >= 1 ? "↑" : "↓"} {ratio.toFixed(2)}× average · {activityLabel(ratio)} + </p> + )} + </section> + ); +} |
