From b04f744e931c518fe342aa5e43530925bbead4ab Mon Sep 17 00:00:00 2001 From: Tyler Hoang Date: Mon, 18 May 2026 00:19:54 -0700 Subject: feat: add FinancialsCard component with statement tabs and period toggle Renders income/balance/cash_flow statements with annual/quarterly toggle, section headers, indent levels, total rows, margin rows, and negative-value coloring via --negative. Co-Authored-By: Claude Sonnet 4.6 --- frontend/app/prism-shell.css | 184 +++++++++++++++++++++++++++ frontend/components/prism/FinancialsCard.tsx | 136 ++++++++++++++++++++ 2 files changed, 320 insertions(+) create mode 100644 frontend/components/prism/FinancialsCard.tsx diff --git a/frontend/app/prism-shell.css b/frontend/app/prism-shell.css index 6ccf9ca..cd0023a 100644 --- a/frontend/app/prism-shell.css +++ b/frontend/app/prism-shell.css @@ -1105,3 +1105,187 @@ grid-column: 3; } } + +/* ── Financials Card ─────────────────────────────── */ + +.psm-financials-card { + padding: 0; + display: flex; + flex-direction: column; + min-height: 480px; + overflow: hidden; +} + +.psm-fin-header { + display: flex; + align-items: stretch; + border-bottom: 1px solid var(--line-1); + padding: 0 var(--sp-4); + flex-shrink: 0; +} + +.psm-fin-tabs { + display: flex; + margin-right: auto; +} + +.psm-fin-tab { + padding: var(--sp-3) var(--sp-3); + background: none; + border: none; + border-bottom: 2px solid transparent; + color: var(--fg-4); + font-family: var(--font-mono); + font-size: var(--fs-12); + letter-spacing: 0.04em; + cursor: pointer; + transition: color 150ms ease; + margin-bottom: -1px; +} + +.psm-fin-tab:hover { + color: var(--fg-2); +} + +.psm-fin-tab.active { + border-bottom-color: var(--brass); + color: var(--brass); +} + +.psm-fin-period { + display: flex; + align-items: center; + gap: var(--sp-1); +} + +.psm-fin-period-btn { + padding: 3px var(--sp-2); + background: none; + border: 1px solid var(--line-1); + border-radius: var(--r-1); + color: var(--fg-4); + font-family: var(--font-mono); + font-size: var(--fs-12); + letter-spacing: 0.04em; + cursor: pointer; + transition: all 150ms ease; +} + +.psm-fin-period-btn:hover { + color: var(--fg-2); + border-color: var(--line-2); +} + +.psm-fin-period-btn.active { + background: rgba(194, 170, 122, 0.1); + border-color: rgba(194, 170, 122, 0.3); + color: var(--brass); +} + +.psm-fin-table-wrap { + overflow: auto; + flex: 1; +} + +.psm-fin-table { + width: 100%; + border-collapse: collapse; + font-size: var(--fs-13); +} + +.psm-fin-table thead tr { + border-bottom: 1px solid var(--line-1); +} + +.psm-fin-label-col { + text-align: left; + padding: var(--sp-2) var(--sp-4); + color: var(--fg-4); + font-family: var(--font-sans); + font-weight: 400; + min-width: 180px; +} + +.psm-fin-val-col { + text-align: right; + padding: var(--sp-2) var(--sp-3); + color: var(--fg-4); + font-family: var(--font-mono); + font-weight: 400; + white-space: nowrap; +} + +.psm-fin-val-col.accent { + color: var(--brass); +} + +.psm-fin-section-row td { + padding: var(--sp-3) var(--sp-4) var(--sp-1); +} + +.psm-fin-section-label { + color: var(--fg-4); + font-family: var(--font-sans); + font-size: var(--fs-12); + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.psm-fin-row td { + border-bottom: 1px solid var(--ink-2); +} + +.psm-fin-row.is-total td { + border-bottom-color: var(--line-1); +} + +.psm-fin-label { + padding: var(--sp-2) var(--sp-4); + color: var(--fg-3); + font-family: var(--font-sans); + white-space: nowrap; +} + +.psm-fin-row.is-indent .psm-fin-label { + padding-left: calc(var(--sp-4) + 12px); +} + +.psm-fin-row.is-total .psm-fin-label { + color: var(--fg-1); + font-weight: 500; +} + +.psm-fin-row.is-margin .psm-fin-label { + font-style: italic; + color: var(--fg-4); + font-size: var(--fs-12); +} + +.psm-fin-val { + text-align: right; + padding: var(--sp-2) var(--sp-3); + color: var(--fg-2); + font-family: var(--font-mono); + white-space: nowrap; +} + +.psm-fin-val.accent { + color: var(--brass); +} + +.psm-fin-val.neg { + color: var(--negative); +} + +.psm-fin-row.is-total .psm-fin-val { + color: var(--fg-1); +} + +.psm-fin-row.is-margin .psm-fin-val { + color: var(--fg-4); + font-size: var(--fs-12); +} + +.psm-fin-empty { + padding: var(--sp-4); +} diff --git a/frontend/components/prism/FinancialsCard.tsx b/frontend/components/prism/FinancialsCard.tsx new file mode 100644 index 0000000..94a6618 --- /dev/null +++ b/frontend/components/prism/FinancialsCard.tsx @@ -0,0 +1,136 @@ +"use client"; +import type { FinancialRow, FinancialsResponse } from "@/types/api"; +import { fmtLarge } from "@/lib/format"; + +type StatementKey = "income" | "balance" | "cash_flow"; +type PeriodKey = "annual" | "quarterly"; + +type Props = { + data: FinancialsResponse; + statement: StatementKey; + period: PeriodKey; + onChangeStatement: (s: StatementKey) => void; + onChangePeriod: (p: PeriodKey) => void; +}; + +const STMT_LABELS: Record = { + income: "INCOME", + balance: "BALANCE", + cash_flow: "CASH FLOW", +}; + +function fmtFinVal(val: number | null | undefined, isMargin: boolean): string { + if (val === null || val === undefined) return "—"; + if (isMargin) return `${(val * 100).toFixed(1)}%`; + return fmtLarge(val); +} + +function FinRow({ row, lastColIdx }: { row: FinancialRow; lastColIdx: number }) { + if (row.is_section) { + return ( + + + {row.label} + + + ); + } + + const cls = [ + "psm-fin-row", + row.is_total ? "is-total" : "", + row.is_margin ? "is-margin" : "", + row.indent === 1 ? "is-indent" : "", + ] + .filter(Boolean) + .join(" "); + + return ( + + {row.label} + {row.values.map((val, i) => ( + + {fmtFinVal(val, row.is_margin)} + + ))} + + ); +} + +export function FinancialsCard({ + data, + statement, + period, + onChangeStatement, + onChangePeriod, +}: Props) { + const stmt = data[statement]; + const lastColIdx = stmt.columns.length - 1; + + return ( +
+
+
+ {(["income", "balance", "cash_flow"] as StatementKey[]).map((key) => ( + + ))} +
+
+ {(["annual", "quarterly"] as PeriodKey[]).map((p) => ( + + ))} +
+
+ + {stmt.columns.length === 0 ? ( +

Statement data unavailable.

+ ) : ( +
+ + + + + {stmt.columns.map((col, i) => ( + + ))} + + + + {stmt.rows.map((row, i) => ( + + ))} + +
USD (millions) + {col} +
+
+ )} +
+ ); +} -- cgit v1.3-2-g0d8e