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 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) (limited to 'frontend/app') 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); +} -- cgit v1.3-2-g0d8e From 7cb2492c748556af99f2b155a434b92f19461095 Mon Sep 17 00:00:00 2001 From: Tyler Hoang Date: Mon, 18 May 2026 00:33:39 -0700 Subject: feat: wire financials tab routing in Sidebar and page.tsx Co-Authored-By: Claude Sonnet 4.6 --- frontend/app/page.tsx | 63 +++++++++++++++++++++++++---------- frontend/components/prism/Sidebar.tsx | 5 ++- 2 files changed, 49 insertions(+), 19 deletions(-) (limited to 'frontend/app') diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 013b93d..6f7e5b6 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -4,6 +4,7 @@ import { FormEvent, Suspense, startTransition, useCallback, useEffect, useMemo, import Image from "next/image"; import { useRouter, useSearchParams } from "next/navigation"; import { AppShell } from "@/components/prism/AppShell"; +import { FinancialsPage } from "@/components/prism/FinancialsPage"; import { ChartCard } from "@/components/prism/ChartCard"; import { KPIStrip } from "@/components/prism/KPIStrip"; import { Sidebar } from "@/components/prism/Sidebar"; @@ -29,6 +30,7 @@ function OverviewClient() { const router = useRouter(); const searchParams = useSearchParams(); const selectedTicker = (searchParams.get("ticker") || "").toUpperCase(); + const tab = searchParams.get("tab") || "overview"; const lastTickerRef = useRef(""); const [query, setQuery] = useState(""); @@ -72,11 +74,26 @@ function OverviewClient() { setOverviewState("loading"); setChartState("loading"); + const params = new URLSearchParams(); + params.set("ticker", normalized); + if (tab !== "overview") params.set("tab", tab); startTransition(() => { - router.push(`/?ticker=${encodeURIComponent(normalized)}`); + router.push(`/?${params.toString()}`); }); }, - [router] + [router, tab] + ); + + const navigateToTab = useCallback( + (key: string) => { + const params = new URLSearchParams(); + if (selectedTicker) params.set("ticker", selectedTicker); + if (key !== "overview") params.set("tab", key); + startTransition(() => { + router.push(`/?${params.toString()}`); + }); + }, + [router, selectedTicker] ); const clearTicker = useCallback(() => { @@ -245,12 +262,13 @@ function OverviewClient() { sidebar={ } topbar={ @@ -271,22 +289,31 @@ function OverviewClient() { {selectedTicker && overviewState === "invalid" ? : null} {selectedTicker && overviewState === "error" ? : null} {overview && overviewState === "ready" ? ( - <> - - -
-
- - -
-
- - - - + tab === "financials" ? ( + + ) : ( + <> + + +
+
+ + +
+
+ + + + +
-
- + + ) ) : null} ); diff --git a/frontend/components/prism/Sidebar.tsx b/frontend/components/prism/Sidebar.tsx index 7f106d8..fb8ebcf 100644 --- a/frontend/components/prism/Sidebar.tsx +++ b/frontend/components/prism/Sidebar.tsx @@ -12,6 +12,7 @@ type Props = { watchlistError: string | null; onSelectTicker: (symbol: string) => void; onRemoveTicker: (symbol: string) => void; + onSelectTab: (key: string) => void; }; export function Sidebar({ @@ -21,7 +22,8 @@ export function Sidebar({ watchlist, watchlistError, onSelectTicker, - onRemoveTicker + onRemoveTicker, + onSelectTab }: Props) { return (