diff options
Diffstat (limited to 'frontend')
| -rw-r--r-- | frontend/app/prism-shell.css | 184 | ||||
| -rw-r--r-- | frontend/components/prism/FinancialsCard.tsx | 136 |
2 files changed, 320 insertions, 0 deletions
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<StatementKey, string> = { + 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 ( + <tr className="psm-fin-section-row"> + <td className="psm-fin-section-label" colSpan={lastColIdx + 2}> + {row.label} + </td> + </tr> + ); + } + + const cls = [ + "psm-fin-row", + row.is_total ? "is-total" : "", + row.is_margin ? "is-margin" : "", + row.indent === 1 ? "is-indent" : "", + ] + .filter(Boolean) + .join(" "); + + return ( + <tr className={cls}> + <td className="psm-fin-label">{row.label}</td> + {row.values.map((val, i) => ( + <td + key={i} + className={[ + "psm-fin-val", + i === lastColIdx ? "accent" : "", + val !== null && val < 0 && !row.is_margin ? "neg" : "", + ] + .filter(Boolean) + .join(" ")} + > + {fmtFinVal(val, row.is_margin)} + </td> + ))} + </tr> + ); +} + +export function FinancialsCard({ + data, + statement, + period, + onChangeStatement, + onChangePeriod, +}: Props) { + const stmt = data[statement]; + const lastColIdx = stmt.columns.length - 1; + + return ( + <section className="psm-card psm-financials-card"> + <div className="psm-fin-header"> + <div className="psm-fin-tabs"> + {(["income", "balance", "cash_flow"] as StatementKey[]).map((key) => ( + <button + key={key} + type="button" + className={`psm-fin-tab${statement === key ? " active" : ""}`} + onClick={() => onChangeStatement(key)} + > + {STMT_LABELS[key]} + </button> + ))} + </div> + <div className="psm-fin-period"> + {(["annual", "quarterly"] as PeriodKey[]).map((p) => ( + <button + key={p} + type="button" + className={`psm-fin-period-btn${period === p ? " active" : ""}`} + onClick={() => onChangePeriod(p)} + > + {p === "annual" ? "ANNUAL" : "QUARTERLY"} + </button> + ))} + </div> + </div> + + {stmt.columns.length === 0 ? ( + <p className="psm-muted-copy psm-fin-empty">Statement data unavailable.</p> + ) : ( + <div className="psm-fin-table-wrap"> + <table className="psm-fin-table"> + <thead> + <tr> + <th className="psm-fin-label-col">USD (millions)</th> + {stmt.columns.map((col, i) => ( + <th + key={i} + className={`psm-fin-val-col${i === lastColIdx ? " accent" : ""}`} + > + {col} + </th> + ))} + </tr> + </thead> + <tbody> + {stmt.rows.map((row, i) => ( + <FinRow key={i} row={row} lastColIdx={lastColIdx} /> + ))} + </tbody> + </table> + </div> + )} + </section> + ); +} |
