From 17de3dd72dab707ccad743e90e43039bbaa1948a Mon Sep 17 00:00:00 2001 From: francy51 Date: Wed, 18 Mar 2026 23:40:28 -0400 Subject: [PATCH] Add history window controls and expand taxonomy pack support - add 3Y/5Y/10Y financial history filtering and reorganize normalization details UI - add new fiscal taxonomy surface/income bridge/KPI packs and update Rust taxonomy loading - auto-detect Homebrew SQLite for native `sqlite-vec` in local dev/e2e with docs and env guidance --- .env.example | 4 + README.md | 6 + app/financials/page.tsx | 569 ++++---- app/globals.css | 236 ++-- components/dashboard/metric-card.tsx | 8 +- components/financials/control-bar.tsx | 35 +- components/financials/financials-toolbar.tsx | 25 + .../financials/normalization-summary.tsx | 35 +- components/financials/statement-matrix.tsx | 496 +++---- .../financials/statement-row-inspector.tsx | 36 +- components/shell/app-shell.tsx | 42 +- components/ui/button.tsx | 2 +- components/ui/input.tsx | 2 +- components/ui/panel.tsx | 2 +- components/ui/status-pill.tsx | 19 +- doc/sqlite-vec-local-setup.md | 48 + doc/taxonomy-pack-authoring.md | 73 + docs/architecture/taxonomy.md | 93 +- lib/financials/history-window.test.ts | 120 ++ lib/financials/history-window.ts | 42 + lib/financials/statement-view-model.ts | 342 +++-- lib/server/db/index.ts | 100 +- rust/fiscal-xbrl-core/src/pack_selector.rs | 1175 +++++++++++++---- rust/fiscal-xbrl-core/src/surface_mapper.rs | 126 ++ rust/fiscal-xbrl-core/src/taxonomy_loader.rs | 54 + rust/fiscal-xbrl-core/src/universal_income.rs | 107 ++ .../fiscal/v1/agriculture.income-bridge.json | 295 +++++ .../fiscal/v1/agriculture.surface.json | 232 ++++ ...ontractors_construction.income-bridge.json | 293 ++++ .../v1/contractors_construction.surface.json | 237 ++++ ...tors_federal_government.income-bridge.json | 295 +++++ ...ontractors_federal_government.surface.json | 197 +++ .../v1/development_stage.income-bridge.json | 274 ++++ .../fiscal/v1/development_stage.surface.json | 118 ++ ...ertainment_broadcasters.income-bridge.json | 302 +++++ .../entertainment_broadcasters.surface.json | 216 +++ ...inment_cable_television.income-bridge.json | 302 +++++ ...ntertainment_cable_television.surface.json | 218 +++ .../entertainment_casinos.income-bridge.json | 304 +++++ .../v1/entertainment_casinos.surface.json | 238 ++++ .../v1/entertainment_films.income-bridge.json | 299 +++++ .../v1/entertainment_films.surface.json | 198 +++ .../v1/entertainment_music.income-bridge.json | 292 ++++ .../v1/entertainment_music.surface.json | 176 +++ .../v1/extractive_mining.income-bridge.json | 300 +++++ .../fiscal/v1/extractive_mining.surface.json | 216 +++ .../fiscal/v1/franchisors.income-bridge.json | 302 +++++ .../fiscal/v1/franchisors.surface.json | 236 ++++ .../fiscal/v1/kpis/agriculture.kpis.json | 5 + .../kpis/contractors_construction.kpis.json | 5 + .../contractors_federal_government.kpis.json | 5 + .../v1/kpis/development_stage.kpis.json | 5 + .../kpis/entertainment_broadcasters.kpis.json | 5 + .../entertainment_cable_television.kpis.json | 5 + .../v1/kpis/entertainment_casinos.kpis.json | 5 + .../v1/kpis/entertainment_films.kpis.json | 5 + .../v1/kpis/entertainment_music.kpis.json | 5 + .../v1/kpis/extractive_mining.kpis.json | 5 + .../fiscal/v1/kpis/franchisors.kpis.json | 5 + .../fiscal/v1/kpis/mortgage_banking.kpis.json | 5 + .../fiscal/v1/kpis/not_for_profit.kpis.json | 5 + .../v1/kpis/plan_defined_benefit.kpis.json | 5 + .../kpis/plan_defined_contribution.kpis.json | 5 + .../v1/kpis/plan_health_welfare.kpis.json | 5 + .../real_estate_common_interest.kpis.json | 5 + .../v1/kpis/real_estate_general.kpis.json | 5 + .../v1/kpis/real_estate_retail_land.kpis.json | 5 + .../kpis/real_estate_time_sharing.kpis.json | 5 + .../fiscal/v1/kpis/software.kpis.json | 5 + .../fiscal/v1/kpis/steamship.kpis.json | 5 + .../fiscal/v1/kpis/title_plant.kpis.json | 5 + .../v1/mortgage_banking.income-bridge.json | 300 +++++ .../fiscal/v1/mortgage_banking.surface.json | 235 ++++ .../v1/not_for_profit.income-bridge.json | 274 ++++ .../fiscal/v1/not_for_profit.surface.json | 220 +++ .../plan_defined_benefit.income-bridge.json | 273 ++++ .../v1/plan_defined_benefit.surface.json | 142 ++ ...an_defined_contribution.income-bridge.json | 273 ++++ .../v1/plan_defined_contribution.surface.json | 104 ++ .../v1/plan_health_welfare.income-bridge.json | 271 ++++ .../v1/plan_health_welfare.surface.json | 122 ++ ..._estate_common_interest.income-bridge.json | 292 ++++ .../real_estate_common_interest.surface.json | 196 +++ .../v1/real_estate_general.income-bridge.json | 300 +++++ .../v1/real_estate_general.surface.json | 236 ++++ ...real_estate_retail_land.income-bridge.json | 295 +++++ .../v1/real_estate_retail_land.surface.json | 196 +++ ...eal_estate_time_sharing.income-bridge.json | 302 +++++ .../v1/real_estate_time_sharing.surface.json | 256 ++++ .../fiscal/v1/software.income-bridge.json | 305 +++++ rust/taxonomy/fiscal/v1/software.surface.json | 234 ++++ .../fiscal/v1/steamship.income-bridge.json | 302 +++++ .../taxonomy/fiscal/v1/steamship.surface.json | 258 ++++ .../fiscal/v1/title_plant.income-bridge.json | 295 +++++ .../fiscal/v1/title_plant.surface.json | 196 +++ scripts/dev.ts | 169 ++- scripts/e2e-prepare.ts | 28 +- scripts/e2e-webserver.ts | 52 +- scripts/generate-taxonomy.ts | 72 +- scripts/sqlite-vector-env.test.ts | 146 ++ scripts/sqlite-vector-env.ts | 144 ++ scripts/validate-taxonomy-packs.ts | 354 +++-- 102 files changed, 14978 insertions(+), 1316 deletions(-) create mode 100644 doc/sqlite-vec-local-setup.md create mode 100644 doc/taxonomy-pack-authoring.md create mode 100644 lib/financials/history-window.test.ts create mode 100644 lib/financials/history-window.ts create mode 100644 rust/taxonomy/fiscal/v1/agriculture.income-bridge.json create mode 100644 rust/taxonomy/fiscal/v1/agriculture.surface.json create mode 100644 rust/taxonomy/fiscal/v1/contractors_construction.income-bridge.json create mode 100644 rust/taxonomy/fiscal/v1/contractors_construction.surface.json create mode 100644 rust/taxonomy/fiscal/v1/contractors_federal_government.income-bridge.json create mode 100644 rust/taxonomy/fiscal/v1/contractors_federal_government.surface.json create mode 100644 rust/taxonomy/fiscal/v1/development_stage.income-bridge.json create mode 100644 rust/taxonomy/fiscal/v1/development_stage.surface.json create mode 100644 rust/taxonomy/fiscal/v1/entertainment_broadcasters.income-bridge.json create mode 100644 rust/taxonomy/fiscal/v1/entertainment_broadcasters.surface.json create mode 100644 rust/taxonomy/fiscal/v1/entertainment_cable_television.income-bridge.json create mode 100644 rust/taxonomy/fiscal/v1/entertainment_cable_television.surface.json create mode 100644 rust/taxonomy/fiscal/v1/entertainment_casinos.income-bridge.json create mode 100644 rust/taxonomy/fiscal/v1/entertainment_casinos.surface.json create mode 100644 rust/taxonomy/fiscal/v1/entertainment_films.income-bridge.json create mode 100644 rust/taxonomy/fiscal/v1/entertainment_films.surface.json create mode 100644 rust/taxonomy/fiscal/v1/entertainment_music.income-bridge.json create mode 100644 rust/taxonomy/fiscal/v1/entertainment_music.surface.json create mode 100644 rust/taxonomy/fiscal/v1/extractive_mining.income-bridge.json create mode 100644 rust/taxonomy/fiscal/v1/extractive_mining.surface.json create mode 100644 rust/taxonomy/fiscal/v1/franchisors.income-bridge.json create mode 100644 rust/taxonomy/fiscal/v1/franchisors.surface.json create mode 100644 rust/taxonomy/fiscal/v1/kpis/agriculture.kpis.json create mode 100644 rust/taxonomy/fiscal/v1/kpis/contractors_construction.kpis.json create mode 100644 rust/taxonomy/fiscal/v1/kpis/contractors_federal_government.kpis.json create mode 100644 rust/taxonomy/fiscal/v1/kpis/development_stage.kpis.json create mode 100644 rust/taxonomy/fiscal/v1/kpis/entertainment_broadcasters.kpis.json create mode 100644 rust/taxonomy/fiscal/v1/kpis/entertainment_cable_television.kpis.json create mode 100644 rust/taxonomy/fiscal/v1/kpis/entertainment_casinos.kpis.json create mode 100644 rust/taxonomy/fiscal/v1/kpis/entertainment_films.kpis.json create mode 100644 rust/taxonomy/fiscal/v1/kpis/entertainment_music.kpis.json create mode 100644 rust/taxonomy/fiscal/v1/kpis/extractive_mining.kpis.json create mode 100644 rust/taxonomy/fiscal/v1/kpis/franchisors.kpis.json create mode 100644 rust/taxonomy/fiscal/v1/kpis/mortgage_banking.kpis.json create mode 100644 rust/taxonomy/fiscal/v1/kpis/not_for_profit.kpis.json create mode 100644 rust/taxonomy/fiscal/v1/kpis/plan_defined_benefit.kpis.json create mode 100644 rust/taxonomy/fiscal/v1/kpis/plan_defined_contribution.kpis.json create mode 100644 rust/taxonomy/fiscal/v1/kpis/plan_health_welfare.kpis.json create mode 100644 rust/taxonomy/fiscal/v1/kpis/real_estate_common_interest.kpis.json create mode 100644 rust/taxonomy/fiscal/v1/kpis/real_estate_general.kpis.json create mode 100644 rust/taxonomy/fiscal/v1/kpis/real_estate_retail_land.kpis.json create mode 100644 rust/taxonomy/fiscal/v1/kpis/real_estate_time_sharing.kpis.json create mode 100644 rust/taxonomy/fiscal/v1/kpis/software.kpis.json create mode 100644 rust/taxonomy/fiscal/v1/kpis/steamship.kpis.json create mode 100644 rust/taxonomy/fiscal/v1/kpis/title_plant.kpis.json create mode 100644 rust/taxonomy/fiscal/v1/mortgage_banking.income-bridge.json create mode 100644 rust/taxonomy/fiscal/v1/mortgage_banking.surface.json create mode 100644 rust/taxonomy/fiscal/v1/not_for_profit.income-bridge.json create mode 100644 rust/taxonomy/fiscal/v1/not_for_profit.surface.json create mode 100644 rust/taxonomy/fiscal/v1/plan_defined_benefit.income-bridge.json create mode 100644 rust/taxonomy/fiscal/v1/plan_defined_benefit.surface.json create mode 100644 rust/taxonomy/fiscal/v1/plan_defined_contribution.income-bridge.json create mode 100644 rust/taxonomy/fiscal/v1/plan_defined_contribution.surface.json create mode 100644 rust/taxonomy/fiscal/v1/plan_health_welfare.income-bridge.json create mode 100644 rust/taxonomy/fiscal/v1/plan_health_welfare.surface.json create mode 100644 rust/taxonomy/fiscal/v1/real_estate_common_interest.income-bridge.json create mode 100644 rust/taxonomy/fiscal/v1/real_estate_common_interest.surface.json create mode 100644 rust/taxonomy/fiscal/v1/real_estate_general.income-bridge.json create mode 100644 rust/taxonomy/fiscal/v1/real_estate_general.surface.json create mode 100644 rust/taxonomy/fiscal/v1/real_estate_retail_land.income-bridge.json create mode 100644 rust/taxonomy/fiscal/v1/real_estate_retail_land.surface.json create mode 100644 rust/taxonomy/fiscal/v1/real_estate_time_sharing.income-bridge.json create mode 100644 rust/taxonomy/fiscal/v1/real_estate_time_sharing.surface.json create mode 100644 rust/taxonomy/fiscal/v1/software.income-bridge.json create mode 100644 rust/taxonomy/fiscal/v1/software.surface.json create mode 100644 rust/taxonomy/fiscal/v1/steamship.income-bridge.json create mode 100644 rust/taxonomy/fiscal/v1/steamship.surface.json create mode 100644 rust/taxonomy/fiscal/v1/title_plant.income-bridge.json create mode 100644 rust/taxonomy/fiscal/v1/title_plant.surface.json create mode 100644 scripts/sqlite-vector-env.test.ts create mode 100644 scripts/sqlite-vector-env.ts diff --git a/.env.example b/.env.example index 0d4be1a..a65ba7c 100644 --- a/.env.example +++ b/.env.example @@ -8,6 +8,10 @@ APP_PORT=3000 # For app running outside Docker, this resolves to ./data/fiscal.sqlite # In Docker Compose deployment, default path is /app/data/fiscal.sqlite DATABASE_URL=file:data/fiscal.sqlite +# Optional native sqlite-vec setup +# On macOS local dev/e2e, bun run dev and bun run e2e:webserver auto-detect Homebrew SQLite if present. +# SQLITE_CUSTOM_LIB_PATH=/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib +# SQLITE_VEC_EXTENSION_PATH=/absolute/path/to/node_modules/sqlite-vec-darwin-arm64/vec0.dylib BETTER_AUTH_SECRET=replace-with-a-long-random-secret BETTER_AUTH_BASE_URL=http://localhost:3000 BETTER_AUTH_TRUSTED_ORIGINS=http://localhost:3000 diff --git a/README.md b/README.md index eec8ae9..0c2b4f0 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ bun run dev Open [http://localhost:3000](http://localhost:3000). `bun run dev` is the local-safe entrypoint. It bootstraps the local SQLite schema from `drizzle/` when needed, forces Better Auth to a localhost origin, uses same-origin API calls, and falls back to local SQLite + Workflow local runtime even if `.env` still contains deployment-oriented values. If port `3000` is already in use and you did not set `PORT`, it automatically picks the next open local port and keeps Better Auth in sync with that port. +On macOS, `bun run dev` also auto-detects Homebrew SQLite and enables native `sqlite-vec` when `/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib` or `/usr/local/opt/sqlite/lib/libsqlite3.dylib` exists. If no compatible SQLite library is found, the app falls back to table-backed vector storage and search still works. See [doc/sqlite-vec-local-setup.md](doc/sqlite-vec-local-setup.md). If you need raw `next dev` behavior without those overrides, use: @@ -80,6 +81,7 @@ bun run test:e2e:ui ``` The Playwright web server boot path uses an isolated SQLite database at `data/e2e.sqlite`, forces local Better Auth origins for the test port, and stores artifacts under `output/playwright/`. +On macOS, `bun run e2e:webserver` uses the same Homebrew SQLite auto-detection so `sqlite-vec` can load natively when available. ## Docker deployment @@ -146,6 +148,10 @@ Use root `.env` or root `.env.local`: # leave blank for same-origin API NEXT_PUBLIC_API_URL= DATABASE_URL=file:data/fiscal.sqlite +# Optional native sqlite-vec setup +# macOS local dev/e2e auto-detects Homebrew SQLite if present. +# SQLITE_CUSTOM_LIB_PATH=/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib +# SQLITE_VEC_EXTENSION_PATH=/absolute/path/to/node_modules/sqlite-vec-darwin-arm64/vec0.dylib BETTER_AUTH_SECRET=replace-with-a-long-random-secret BETTER_AUTH_BASE_URL=http://localhost:3000 BETTER_AUTH_TRUSTED_ORIGINS=http://localhost:3000 diff --git a/app/financials/page.tsx b/app/financials/page.tsx index 77ba663..0141f17 100644 --- a/app/financials/page.tsx +++ b/app/financials/page.tsx @@ -14,7 +14,6 @@ import { import { format } from "date-fns"; import { useSearchParams } from "next/navigation"; import { - Bar, CartesianGrid, Line, LineChart, @@ -25,6 +24,7 @@ import { } from "recharts"; import { AlertTriangle, + ChevronRight, ChevronDown, Download, RefreshCcw, @@ -57,6 +57,11 @@ import { resolveStatementSelection, type StatementInspectorSelection, } from "@/lib/financials/statement-view-model"; +import { + filterPeriodsByHistoryWindow, + financialHistoryLimit, + type FinancialHistoryWindow, +} from "@/lib/financials/history-window"; import { queryKeys } from "@/lib/query/keys"; import { companyFinancialStatementsQueryOptions } from "@/lib/query/options"; import type { @@ -65,7 +70,6 @@ import type { DerivedFinancialRow, FinancialCadence, FinancialDisplayMode, - FinancialStatementPeriod, FinancialSurfaceKind, FinancialUnit, RatioRow, @@ -73,7 +77,6 @@ import type { StructuredKpiRow, SurfaceFinancialRow, TaxonomyStatementRow, - TrendSeries, } from "@/lib/types"; type LoadOptions = { @@ -111,6 +114,15 @@ const CADENCE_OPTIONS = [ { value: "ltm", label: "LTM" }, ]; +const HISTORY_WINDOW_OPTIONS: Array<{ + value: `${FinancialHistoryWindow}`; + label: string; +}> = [ + { value: "3", label: "3Y" }, + { value: "5", label: "5Y" }, + { value: "10", label: "10Y" }, +]; + const DISPLAY_MODE_OPTIONS = [ { value: "standardized", label: "Standard" }, { value: "faithful", label: "Faithful" }, @@ -410,6 +422,59 @@ function ChartFrame({ children }: { children: React.ReactNode }) { ); } +function CollapsibleSection(props: { + title: string; + subtitle?: string; + children: React.ReactNode; + defaultOpen?: boolean; +}) { + const [open, setOpen] = useState(props.defaultOpen ?? false); + + return ( +
+ + {open ?
{props.children}
: null} +
+ ); +} + +function SummarySubsection(props: { + title: string; + children: React.ReactNode; +}) { + return ( +
+

+ {props.title} +

+ {props.children} +
+ ); +} + function FinancialsPageContent() { const { isPending, isAuthenticated } = useAuthGuard(); const searchParams = useSearchParams(); @@ -423,6 +488,7 @@ function FinancialsPageContent() { const [cadence, setCadence] = useState("annual"); const [displayMode, setDisplayMode] = useState("standardized"); + const [historyWindow, setHistoryWindow] = useState(3); const [valueScale, setValueScale] = useState("millions"); const [rowSearch, setRowSearch] = useState(""); const [showPercentChange, setShowPercentChange] = useState(false); @@ -438,10 +504,14 @@ function FinancialsPageContent() { () => new Set(), ); const [loading, setLoading] = useState(true); - const [loadingMore, setLoadingMore] = useState(false); + const [, setLoadingMore] = useState(false); const [syncingFinancials, setSyncingFinancials] = useState(false); const [error, setError] = useState(null); const [showTrendChart, setShowTrendChart] = useState(false); + const historyFetchLimit = useMemo( + () => financialHistoryLimit(cadence, historyWindow), + [cadence, historyWindow], + ); useEffect(() => { const fromQuery = searchParams.get("ticker"); @@ -509,7 +579,7 @@ function FinancialsPageContent() { includeDimensions, includeFacts: false, cursor: nextCursor, - limit: 12, + limit: historyFetchLimit, }), ); @@ -532,7 +602,14 @@ function FinancialsPageContent() { setLoadingMore(false); } }, - [cadence, queryClient, selectedFlatRowKey, selectedRowRef, surfaceKind], + [ + cadence, + historyFetchLimit, + queryClient, + selectedFlatRowKey, + selectedRowRef, + surfaceKind, + ], ); const syncFinancials = useCallback(async () => { @@ -578,6 +655,10 @@ function FinancialsPageContent() { Date.parse(right.periodEnd ?? right.filingDate), ); }, [financials?.periods]); + const visiblePeriods = useMemo( + () => filterPeriodsByHistoryWindow(periods, cadence, historyWindow), + [cadence, historyWindow, periods], + ); const isTreeStatementMode = displayMode === "standardized" && isStatementSurfaceKind(surfaceKind); @@ -708,11 +789,23 @@ function FinancialsPageContent() { surfaceKind === "income_statement" || surfaceKind === "cash_flow_statement" ) { - return standardizedRows.find((row) => row.key === "revenue") ?? null; + return ( + standardizedRows.find((row) => row.key === "revenue") ?? + standardizedRows.find((row) => row.key === "net_cash_from_operating") ?? + standardizedRows.find((row) => row.key === "operating_income") ?? + null + ); } if (surfaceKind === "balance_sheet") { - return standardizedRows.find((row) => row.key === "total_assets") ?? null; + return ( + standardizedRows.find((row) => row.key === "total_assets") ?? + standardizedRows.find( + (row) => row.key === "total_liabilities_and_equity", + ) ?? + standardizedRows.find((row) => row.key === "stockholders_equity") ?? + null + ); } return null; @@ -724,7 +817,7 @@ function FinancialsPageContent() { const trendSeries = financials?.trendSeries ?? []; const chartData = useMemo(() => { - return periods.map((period) => ({ + return visiblePeriods.map((period) => ({ label: formatLongDate(period.periodEnd ?? period.filingDate), ...Object.fromEntries( trendSeries.map((series) => [ @@ -733,7 +826,7 @@ function FinancialsPageContent() { ]), ), })); - }, [periods, trendSeries]); + }, [trendSeries, visiblePeriods]); const controlSections = useMemo(() => { const sections: FinancialControlSection[] = [ @@ -761,6 +854,16 @@ function FinancialsPageContent() { setExpandedRowKeys(new Set()); }, }, + { + key: "history", + label: "History", + options: HISTORY_WINDOW_OPTIONS, + value: String(historyWindow), + onChange: (value) => + setHistoryWindow( + Number.parseInt(value, 10) as FinancialHistoryWindow, + ), + }, ]; if ( @@ -791,7 +894,7 @@ function FinancialsPageContent() { }); return sections; - }, [cadence, displayMode, surfaceKind, valueScale]); + }, [cadence, displayMode, historyWindow, surfaceKind, valueScale]); const toggleExpandedRow = useCallback((key: string) => { setExpandedRowKeys((current) => { @@ -1069,7 +1172,7 @@ function FinancialsPageContent() { edits or derived rows are available yet.

- ) : periods.length === 0 || + ) : visiblePeriods.length === 0 || (isTreeStatementMode ? (treeModel?.visibleNodeCount ?? 0) === 0 : filteredRows.length === 0) ? ( @@ -1081,7 +1184,7 @@ function FinancialsPageContent() { {isStatementSurfaceKind(surfaceKind) ? (

- USD · {valueScaleLabel} + USD · {valueScaleLabel} · Latest {historyWindow} years

{isTreeStatementMode && hasUnmappedResidualRows ? (

@@ -1096,7 +1199,7 @@ function FinancialsPageContent() { ) : null} {isTreeStatementMode && treeModel ? ( 50} /> ) : (

- - +
+ - - {periods.map((period) => ( + + {visiblePeriods.map((period) => ( setSelectedFlatRowKey(row.key)} > - - {periods.map((period, index) => ( + {visiblePeriods.map((period, index) => ( + + {financials.metrics.validation?.checks.map((check) => ( + + + + + + + + ))} + +
- Metric - Metric
@@ -1136,7 +1236,7 @@ function FinancialsPageContent() { {group.label ? (
{group.label} @@ -1146,14 +1246,9 @@ function FinancialsPageContent() { {group.rows.map((row) => (
+
{buildDisplayValue({ row, periodId: period.id, previousPeriodId: index > 0 - ? (periods[index - 1]?.id ?? null) + ? (visiblePeriods[index - 1]?.id ?? null) : null, commonSizeRow, displayMode, @@ -1210,207 +1305,235 @@ function FinancialsPageContent() { )} - {isTreeStatementMode && isStatementSurfaceKind(surfaceKind) ? ( - - ) : ( - - {!selectedRow ? ( -

- Select a row to inspect details. -

- ) : ( -
-
-
-

Label

-

- {selectedRow.label} -

-
-
-

Key

-

- {selectedRow.key} -

-
-
- - {isTaxonomyRow(selectedRow) ? ( -
-

- Taxonomy Concept -

-

- {selectedRow.qname} -

-
+ {financials ? ( + +
+ + {isTreeStatementMode && isStatementSurfaceKind(surfaceKind) ? ( + ) : ( -
-
-

- Category + + {!selectedRow ? ( +

+ Select a row to inspect details.

-

- {selectedRow.category} -

-
-
-

Unit

-

- {selectedRow.unit} -

-
-
+ ) : ( +
+
+
+

+ Label +

+

+ {selectedRow.label} +

+
+
+

+ Key +

+

+ {selectedRow.key} +

+
+
+ + {isTaxonomyRow(selectedRow) ? ( +
+

+ Taxonomy Concept +

+

+ {selectedRow.qname} +

+
+ ) : ( +
+
+

+ Category +

+

+ {selectedRow.category} +

+
+
+

+ Unit +

+

+ {selectedRow.unit} +

+
+
+ )} + + {isDerivedRow(selectedRow) ? ( +
+

+ Source Row Keys +

+

+ {(selectedRow.sourceRowKeys ?? []).join(", ") || + "n/a"} +

+

+ Source Concepts +

+

+ {(selectedRow.sourceConcepts ?? []).join(", ") || + "n/a"} +

+

+ Source Fact IDs +

+

+ {(selectedRow.sourceFactIds ?? []).join(", ") || + "n/a"} +

+
+ ) : null} + + {!selectedRow || + !("hasDimensions" in selectedRow) || + !selectedRow.hasDimensions ? ( +

+ No dimensional drill-down is available for this row. +

+ ) : dimensionRows.length === 0 ? ( +

+ No dimensional facts were returned for the selected + row. +

+ ) : ( +
+ + + + + + + + + + + {dimensionRows.map((row, index) => ( + + + + + + + ))} + +
PeriodAxisMemberValue
+ {visiblePeriods.find( + (period) => period.id === row.periodId, + )?.periodLabel ?? row.periodId} + {row.axis}{row.member} + {formatMetricValue({ + value: row.value, + unit: isTaxonomyRow(selectedRow) + ? "currency" + : selectedRow.unit, + scale: valueScale, + rowKey: selectedRow.key, + surfaceKind, + })} +
+
+ )} +
+ )} + )} +
- {isDerivedRow(selectedRow) ? ( -
-

- Source Row Keys -

-

- {(selectedRow.sourceRowKeys ?? []).join(", ") || "n/a"} -

-

- Source Concepts -

-

- {(selectedRow.sourceConcepts ?? []).join(", ") || "n/a"} -

-

- Source Fact IDs -

-

- {(selectedRow.sourceFactIds ?? []).join(", ") || "n/a"} -

+ {(surfaceKind === "income_statement" || + surfaceKind === "balance_sheet" || + surfaceKind === "cash_flow_statement") && ( + +
+ + + Overall status:{" "} + {financials.metrics.validation?.status ?? "not_run"} +
- ) : null} - - {!selectedRow || - !("hasDimensions" in selectedRow) || - !selectedRow.hasDimensions ? ( -

- No dimensional drill-down is available for this row. -

- ) : dimensionRows.length === 0 ? ( -

- No dimensional facts were returned for the selected row. -

- ) : ( -
- - - - - - - - - - - {dimensionRows.map((row, index) => ( - - - - - + {(financials.metrics.validation?.checks.length ?? 0) === 0 ? ( +

+ No validation checks available yet. +

+ ) : ( +
+
PeriodAxisMemberValue
- {periods.find( - (period) => period.id === row.periodId, - )?.periodLabel ?? row.periodId} - {row.axis}{row.member} - {formatMetricValue({ - value: row.value, - unit: isTaxonomyRow(selectedRow) - ? "currency" - : selectedRow.unit, - scale: valueScale, - rowKey: selectedRow.key, - surfaceKind, - })} -
+ + + + + + + - ))} - -
MetricTaxonomyLLM (PDF)StatusPages
-
- )} -
- )} - - )} +
{check.metricKey} + {formatMetricValue({ + value: check.taxonomyValue, + unit: "currency", + scale: valueScale, + rowKey: check.metricKey, + surfaceKind, + })} + + {formatMetricValue({ + value: check.llmValue, + unit: "currency", + scale: valueScale, + rowKey: check.metricKey, + surfaceKind, + })} + {check.status} + {(check.evidencePages ?? []).join(", ") || "n/a"} +
+
+ )} + + )} - {(surfaceKind === "income_statement" || - surfaceKind === "balance_sheet" || - surfaceKind === "cash_flow_statement") && - financials ? ( - -
- - - Overall status:{" "} - {financials.metrics.validation?.status ?? "not_run"} - + {isTreeStatementMode ? ( + + + + ) : null}
- {(financials.metrics.validation?.checks.length ?? 0) === 0 ? ( -

- No validation checks available yet. -

- ) : ( -
- - - - - - - - - - - - {financials.metrics.validation?.checks.map((check) => ( - - - - - - - - ))} - -
MetricTaxonomyLLM (PDF)StatusPages
{check.metricKey} - {formatMetricValue({ - value: check.taxonomyValue, - unit: "currency", - scale: valueScale, - rowKey: check.metricKey, - surfaceKind, - })} - - {formatMetricValue({ - value: check.llmValue, - unit: "currency", - scale: valueScale, - rowKey: check.metricKey, - surfaceKind, - })} - {check.status}{(check.evidencePages ?? []).join(", ") || "n/a"}
-
- )} -
- ) : null} - - {financials && isTreeStatementMode ? ( - + ) : null} ); diff --git a/app/globals.css b/app/globals.css index 7ba5479..96c00e1 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,25 +1,23 @@ @import "tailwindcss"; :root { - --font-display: - "Avenir Next", "Segoe UI", "Helvetica Neue", Arial, sans-serif; - --font-mono: - "Menlo", "SFMono-Regular", "Consolas", "Liberation Mono", monospace; - --bg-0: #121417; - --bg-1: #181b20; - --bg-2: #21252b; - --panel: rgba(28, 31, 36, 0.84); - --panel-soft: rgba(36, 39, 45, 0.72); - --panel-bright: rgba(49, 53, 60, 0.94); - --line-weak: rgba(196, 202, 211, 0.18); - --line-strong: rgba(220, 226, 234, 0.34); - --accent: #d9dee5; - --accent-strong: #f4f7fb; - --danger: #ff8e8e; - --danger-soft: rgba(111, 46, 46, 0.42); - --terminal-bright: #f3f5f7; - --terminal-muted: #a1a9b3; - --focus-ring: rgba(229, 231, 235, 0.14); + --font-display: system-ui, -apple-system, sans-serif; + --font-mono: "SF Mono", "Fira Code", monospace; + --bg-0: #0f0f0f; + --bg-1: #161616; + --bg-2: #1c1c1c; + --panel: #1a1a1a; + --panel-soft: #222222; + --panel-bright: #2a2a2a; + --line-weak: rgba(255, 255, 255, 0.08); + --line-strong: rgba(255, 255, 255, 0.16); + --accent: #e0e0e0; + --accent-strong: #ffffff; + --danger: #ff6b6b; + --danger-soft: rgba(255, 107, 107, 0.15); + --terminal-bright: #f0f0f0; + --terminal-muted: #888888; + --focus-ring: rgba(255, 255, 255, 0.1); } * { @@ -58,18 +56,7 @@ body { overflow-x: hidden; font-family: var(--font-display), sans-serif; color: var(--terminal-bright); - background: - radial-gradient( - circle at 18% -10%, - rgba(170, 178, 188, 0.16), - transparent 35% - ), - radial-gradient( - circle at 84% 0%, - rgba(121, 128, 138, 0.14), - transparent 30% - ), - linear-gradient(140deg, var(--bg-0), var(--bg-1) 50%, var(--bg-2)); + background: var(--bg-0); } .app-surface, @@ -79,36 +66,12 @@ body { overflow: hidden; } -.ambient-grid { - position: absolute; - inset: 0; - background-image: - linear-gradient(rgba(204, 210, 218, 0.06) 1px, transparent 1px), - linear-gradient(90deg, rgba(204, 210, 218, 0.05) 1px, transparent 1px); - background-size: 34px 34px; - mask-image: radial-gradient(ellipse at center, black 20%, transparent 75%); - pointer-events: none; -} - -.noise-layer { - position: absolute; - inset: 0; - pointer-events: none; - opacity: 0.24; - background-image: radial-gradient( - rgba(220, 226, 234, 0.1) 0.7px, - transparent 0.7px - ); - background-size: 4px 4px; -} - .terminal-caption { font-family: var(--font-mono), monospace; } .panel-heading { font-family: var(--font-mono), monospace; - letter-spacing: 0.08em; } a, @@ -126,23 +89,15 @@ textarea { .data-surface { border: 1px solid var(--line-weak); - border-radius: 1rem; - background: linear-gradient( - 180deg, - rgba(40, 43, 49, 0.92), - rgba(24, 27, 32, 0.78) - ); + border-radius: 0.5rem; + background: var(--panel); } .data-table-wrap { overflow-x: auto; border: 1px solid var(--line-weak); - border-radius: 1rem; - background: linear-gradient( - 180deg, - rgba(34, 37, 42, 0.9), - rgba(20, 23, 27, 0.76) - ); + border-radius: 0.5rem; + background: var(--panel); } .data-table th, @@ -163,54 +118,31 @@ textarea { } .data-table tbody tr:hover { - background-color: rgba(63, 68, 76, 0.32); -} - -@media (prefers-reduced-motion: no-preference) { - .ambient-grid { - animation: subtle-grid-shift 18s linear infinite; - } - - @keyframes subtle-grid-shift { - 0% { - transform: translateY(0px); - } - 50% { - transform: translateY(-8px); - } - 100% { - transform: translateY(0px); - } - } -} - -@media (max-width: 1024px) { - .ambient-grid { - background-size: 26px 26px; - } + background-color: rgba(255, 255, 255, 0.04); } @media (max-width: 640px) { - body { - background: - radial-gradient( - circle at 24% -4%, - rgba(170, 178, 188, 0.14), - transparent 36% - ), - radial-gradient( - circle at 82% 2%, - rgba(121, 128, 138, 0.12), - transparent 30% - ), - linear-gradient(155deg, var(--bg-0), var(--bg-1) 54%, var(--bg-2)); - } - .data-table th, .data-table td { padding: 0.65rem 0.55rem; font-size: 0.8125rem; } + + .financial-matrix-metric-col { + width: 18rem; + } + + .financial-matrix-period-col { + width: 8rem; + } + + .financial-matrix-header, + .financial-matrix-section-cell, + .financial-matrix-sticky-cell, + .financial-matrix-value-cell { + padding-left: 0.65rem; + padding-right: 0.65rem; + } } .panel-compact { @@ -244,7 +176,87 @@ textarea { } .data-table-dense tbody tr:hover { - background-color: rgba(63, 68, 76, 0.32); + background-color: rgba(255, 255, 255, 0.04); +} + +.financials-table tbody tr:hover { + background-color: transparent; +} + +.financial-matrix-wrap { + overflow: auto; + border: 1px solid var(--line-weak); + border-radius: 0.75rem; + background: var(--panel); +} + +.financial-matrix { + width: max-content; + min-width: 100%; + border-collapse: separate; + border-spacing: 0; + table-layout: fixed; +} + +.financial-matrix-metric-col { + width: 24rem; +} + +.financial-matrix-period-col { + width: 9rem; +} + +.financial-matrix-header { + position: sticky; + top: 0; + z-index: 10; + padding: 0.85rem 0.9rem; + border-bottom: 1px solid var(--line-strong); + background: color-mix(in srgb, var(--panel) 92%, black 8%); + text-align: right; + white-space: nowrap; + font-family: var(--font-mono), monospace; +} + +.financial-matrix-section-row td { + border-bottom: 1px solid var(--line-strong); +} + +.financial-matrix-section-cell { + padding: 0.7rem 0.9rem; + background: rgba(255, 255, 255, 0.04); + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--terminal-muted); +} + +.financial-matrix-row-detail td { + background: rgba(255, 255, 255, 0.015); +} + +.financial-matrix-sticky-cell, +.financial-matrix-value-cell { + padding: 0.45rem 0.9rem; + border-bottom: 1px solid var(--line-weak); + white-space: nowrap; + vertical-align: top; +} + +.financial-matrix-sticky-cell { + border-right: 1px solid var(--line-weak); + background: var(--panel); +} + +.financial-matrix-sticky-cell-detail { + background: color-mix(in srgb, var(--panel) 92%, white 8%); +} + +.financial-matrix-value-cell { + text-align: right; + font-variant-numeric: tabular-nums; + color: var(--terminal-bright); } .metric-compact { @@ -382,12 +394,8 @@ textarea { .screener-table-wrap { overflow-x: auto; border: 1px solid var(--line-weak); - border-radius: 0.75rem; - background: linear-gradient( - 180deg, - rgba(34, 37, 42, 0.9), - rgba(20, 23, 27, 0.76) - ); + border-radius: 0.5rem; + background: var(--panel); } .screener-table-wrap thead { diff --git a/components/dashboard/metric-card.tsx b/components/dashboard/metric-card.tsx index 220a475..afa1eec 100644 --- a/components/dashboard/metric-card.tsx +++ b/components/dashboard/metric-card.tsx @@ -41,7 +41,7 @@ export function MetricCard({ className, )} > -

+

{label}

@@ -68,10 +68,8 @@ export function MetricCard({ className, )} > -

- {label} -

-

+

{label}

+

{value}

{delta ? ( diff --git a/components/financials/control-bar.tsx b/components/financials/control-bar.tsx index b5cbbd0..f6a309d 100644 --- a/components/financials/control-bar.tsx +++ b/components/financials/control-bar.tsx @@ -1,7 +1,7 @@ -import { Button } from '@/components/ui/button'; -import { cn } from '@/lib/utils'; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; -type ControlButtonVariant = 'primary' | 'ghost' | 'secondary' | 'danger'; +type ControlButtonVariant = "primary" | "ghost" | "secondary" | "danger"; export type FinancialControlOption = { value: string; @@ -34,19 +34,25 @@ type FinancialControlBarProps = { }; export function FinancialControlBar({ - title = 'Control Bar', + title = "Control Bar", subtitle, sections, actions, - className + className, }: FinancialControlBarProps) { return ( -
+
-

{title}

+

+ {title} +

{subtitle ? ( -

{subtitle}

+

+ {subtitle} +

) : null}
@@ -56,7 +62,7 @@ export function FinancialControlBar({ + ); + })} + + ))} + + )} + {groupedSections.mode.length > 0 && ( {groupedSections.mode.map((section) => ( diff --git a/components/financials/normalization-summary.tsx b/components/financials/normalization-summary.tsx index f7af55e..377d732 100644 --- a/components/financials/normalization-summary.tsx +++ b/components/financials/normalization-summary.tsx @@ -6,6 +6,8 @@ import { cn } from "@/lib/utils"; type NormalizationSummaryProps = { normalization: NormalizationMetadata; + showHeader?: boolean; + className?: string; }; function SummaryField(props: { @@ -20,7 +22,7 @@ function SummaryField(props: { props.tone === "warning" && "border-l-2 border-[#7f6250] pl-3", )} > -

+

{props.label}

0; const hasWarnings = normalization.warnings.length > 0; return ( -

-
-

- Normalization Summary -

-

- Pack, parser, and residual mapping health for the compact statement - surface. -

-
+
+ {showHeader ? ( +
+

+ Normalization Summary +

+

+ Pack, parser, and residual mapping health for the compact statement + surface. +

+
+ ) : null}
-

+

Parser Warnings

diff --git a/components/financials/statement-matrix.tsx b/components/financials/statement-matrix.tsx index 0620899..83ff40f 100644 --- a/components/financials/statement-matrix.tsx +++ b/components/financials/statement-matrix.tsx @@ -1,15 +1,18 @@ -'use client'; +"use client"; -import { Fragment, memo, useMemo, useRef } from 'react'; -import { useVirtualizer } from '@tanstack/react-virtual'; -import { ChevronDown, ChevronRight } from 'lucide-react'; -import type { FinancialStatementPeriod, SurfaceFinancialRow, DetailFinancialRow } from '@/lib/types'; -import { cn } from '@/lib/utils'; +import { Fragment } from "react"; +import { ChevronDown, ChevronRight } from "lucide-react"; +import type { + DetailFinancialRow, + FinancialStatementPeriod, + SurfaceFinancialRow, +} from "@/lib/types"; +import { cn } from "@/lib/utils"; import type { StatementInspectorSelection, StatementTreeNode, - StatementTreeSection -} from '@/lib/financials/statement-view-model'; + StatementTreeSection, +} from "@/lib/financials/statement-view-model"; type MatrixRow = SurfaceFinancialRow | DetailFinancialRow; @@ -19,172 +22,183 @@ type StatementMatrixProps = { selectedRowRef: StatementInspectorSelection | null; onToggleRow: (key: string) => void; onSelectRow: (selection: StatementInspectorSelection) => void; - renderCellValue: (row: MatrixRow, periodId: string, previousPeriodId: string | null) => string; + renderCellValue: ( + row: MatrixRow, + periodId: string, + previousPeriodId: string | null, + ) => string; periodLabelFormatter: (value: string) => string; dense?: boolean; - virtualized?: boolean; }; -function isSurfaceNode(node: StatementTreeNode): node is Extract { - return node.kind === 'surface'; -} - -function rowSelected( +function isSurfaceNode( node: StatementTreeNode, - selectedRowRef: StatementInspectorSelection | null -) { - if (!selectedRowRef) { - return false; - } - - if (node.kind === 'surface') { - return selectedRowRef.kind === 'surface' && selectedRowRef.key === node.row.key; - } - - return selectedRowRef.kind === 'detail' - && selectedRowRef.key === node.row.key - && selectedRowRef.parentKey === node.parentSurfaceKey; +): node is Extract { + return node.kind === "surface"; } -function surfaceBadges(node: Extract) { - const badges: Array<{ label: string; tone: 'default' | 'warning' | 'muted' }> = []; +function surfaceBadges(node: Extract) { + const badges: Array<{ + label: string; + tone: "default" | "warning" | "muted"; + }> = []; - if (node.row.resolutionMethod === 'formula_derived') { - badges.push({ label: 'Formula', tone: node.row.confidence === 'low' ? 'warning' : 'default' }); + if (node.row.resolutionMethod === "formula_derived") { + badges.push({ + label: "Formula", + tone: node.row.confidence === "low" ? "warning" : "default", + }); } - if (node.row.resolutionMethod === 'not_meaningful') { - badges.push({ label: 'N/M', tone: 'muted' }); + if (node.row.resolutionMethod === "not_meaningful") { + badges.push({ label: "N/M", tone: "muted" }); } - if (node.row.confidence === 'low') { - badges.push({ label: 'Low confidence', tone: 'warning' }); + if (node.row.confidence === "low") { + badges.push({ label: "Low confidence", tone: "warning" }); } const detailCount = node.row.detailCount ?? node.directDetailCount; if (detailCount > 0) { - badges.push({ label: `${detailCount} details`, tone: 'default' }); + badges.push({ label: `${detailCount} details`, tone: "default" }); } return badges; } -function badgeClass(tone: 'default' | 'warning' | 'muted', dense?: boolean) { +function badgeClass(tone: "default" | "warning" | "muted", dense?: boolean) { const baseClasses = dense - ? 'rounded border px-1 py-0.5 text-[9px] uppercase tracking-[0.12em]' - : 'rounded-full border px-2 py-0.5 text-[10px] uppercase tracking-[0.14em]'; + ? "rounded border px-1 py-0.5 text-[9px]" + : "rounded border px-2 py-0.5 text-[10px]"; - if (tone === 'warning') { - return cn(baseClasses, 'border-[#84614f] bg-[rgba(112,76,54,0.22)] text-[#ffd7bf]'); + if (tone === "warning") { + return cn( + baseClasses, + "border-[#84614f] bg-[rgba(112,76,54,0.22)] text-[#ffd7bf]", + ); } - if (tone === 'muted') { - return cn(baseClasses, 'border-[color:var(--line-weak)] bg-[rgba(80,85,92,0.16)] text-[color:var(--terminal-muted)]'); + if (tone === "muted") { + return cn( + baseClasses, + "border-[color:var(--line-weak)] bg-[rgba(80,85,92,0.16)] text-[color:var(--terminal-muted)]", + ); } - return cn(baseClasses, 'border-[color:var(--line-weak)] bg-[rgba(88,102,122,0.16)] text-[color:var(--terminal-bright)]'); + return cn( + baseClasses, + "border-[color:var(--line-weak)] bg-[rgba(88,102,122,0.16)] text-[color:var(--terminal-bright)]", + ); } -// Flatten tree nodes for virtualization -type FlattenedNode = { - node: StatementTreeNode; - sectionKey: string; - sectionLabel?: string; - isSectionHeader?: boolean; -}; - -function flattenSections(sections: StatementTreeSection[]): FlattenedNode[] { - const result: FlattenedNode[] = []; - - function flattenNodes(nodes: StatementTreeNode[], sectionKey: string): void { - for (const node of nodes) { - result.push({ node, sectionKey }); - - if (node.kind === 'surface' && node.expanded && node.children.length > 0) { - flattenNodes(node.children, sectionKey); - } - } - } - - for (const section of sections) { - if (section.label) { - result.push({ - node: {} as StatementTreeNode, - sectionKey: section.key, - sectionLabel: section.label, - isSectionHeader: true - }); - } - flattenNodes(section.nodes, section.key); - } - - return result; -} - -function renderNodes(props: StatementMatrixProps & { nodes: StatementTreeNode[]; dense?: boolean }) { +function renderNodes( + props: StatementMatrixProps & { nodes: StatementTreeNode[]; dense?: boolean }, +) { const { dense = false } = props; - const buttonSize = dense ? 'size-7' : 'size-11'; - const buttonClass = dense ? 'rounded' : 'rounded-lg'; - const labelSize = dense ? 'text-[13px]' : 'text-sm'; - const detailLabelSize = dense ? 'text-xs' : 'text-[13px]'; - const paddingY = dense ? 'py-1.5' : 'py-2'; - const gapClass = dense ? 'gap-1' : 'gap-2'; + const buttonSize = dense ? "size-6" : "size-8"; + const buttonClass = dense ? "rounded" : "rounded-lg"; + const labelSize = dense ? "text-[13px]" : "text-sm"; + const detailLabelSize = dense ? "text-[11px]" : "text-xs"; + const paddingY = dense ? "py-1.5" : "py-2"; + const gapClass = dense ? "gap-1.5" : "gap-2"; return props.nodes.map((node) => { - const isSelected = rowSelected(node, props.selectedRowRef); - const labelIndent = node.kind === 'detail' ? node.level * 16 + 16 : node.level * 16; + const labelIndent = + node.kind === "detail" ? node.level * 16 + 16 : node.level * 16; const canToggle = isSurfaceNode(node) && node.expandable; - const nextSelection: StatementInspectorSelection = node.kind === 'surface' - ? { kind: 'surface', key: node.row.key } - : { kind: 'detail', key: node.row.key, parentKey: node.parentSurfaceKey }; + const nextSelection: StatementInspectorSelection = + node.kind === "surface" + ? { kind: "surface", key: node.row.key } + : { + kind: "detail", + key: node.row.key, + parentKey: node.parentSurfaceKey, + }; + const rowClass = cn( + "financial-matrix-row", + node.kind === "detail" && "financial-matrix-row-detail", + ); + const stickyCellClass = cn( + "financial-matrix-sticky-cell", + node.kind === "detail" && "financial-matrix-sticky-cell-detail", + ); return ( - - -
+ + +
{canToggle ? ( ) : ( -
{props.periods.map((period, index) => ( - - {props.renderCellValue(node.row, period.id, index > 0 ? props.periods[index - 1]?.id ?? null : null)} + + {props.renderCellValue( + node.row, + period.id, + index > 0 ? (props.periods[index - 1]?.id ?? null) : null, + )} ))} {isSurfaceNode(node) && node.expanded ? ( <> - Expanded children for {node.row.label} + + Expanded children for {node.row.label} + {renderNodes({ ...props, nodes: node.children, - dense + dense, })} ) : null} @@ -222,193 +248,73 @@ function renderNodes(props: StatementMatrixProps & { nodes: StatementTreeNode[]; }); } -export function StatementMatrix(props: StatementMatrixProps) { - const { dense = false, virtualized = false } = props; - const tableClass = dense ? 'data-table-dense min-w-[960px]' : 'data-table min-w-[1040px]'; - - // Hooks must be called unconditionally - const parentRef = useRef(null); - const flatRows = useMemo(() => flattenSections(props.sections), [props.sections]); - - const virtualizer = useVirtualizer({ - count: flatRows.length, - getScrollElement: () => parentRef.current, - estimateSize: (index) => { - const item = flatRows[index]; - if (item.isSectionHeader) return dense ? 32 : 40; - if (item.node.kind === 'surface' && surfaceBadges(item.node as any).length > 0) return dense ? 44 : 56; - return dense ? 36 : 48; - }, - overscan: 10, - }); - - // Non-virtualized version (original implementation with dense support) - if (!virtualized) { - return ( -
- - - - - {props.periods.map((period) => ( - - ))} - - - - {props.sections.map((section) => ( - - {section.label ? ( - - - - ) : null} - {renderNodes({ - ...props, - nodes: section.nodes, - dense - })} - - ))} - -
Metric -
- {props.periodLabelFormatter(period.periodEnd ?? period.filingDate)} - {period.filingType} · {period.periodLabel} -
-
- {section.label} -
-
- ); - } - - // Virtualized version for large datasets - +export function StatementMatrix({ + periods, + sections, + selectedRowRef, + onToggleRow, + onSelectRow, + renderCellValue, + periodLabelFormatter, + dense = false, +}: StatementMatrixProps) { return ( -
- - +
+
+ + + {periods.map((period) => ( + + ))} + + - - {props.periods.map((period) => ( - + {periods.map((period) => ( + ))} - - {virtualizer.getVirtualItems().map((virtualRow) => { - const item = flatRows[virtualRow.index]; - - if (item.isSectionHeader) { - return ( - - + {sections.map((section) => ( + + {section.label ? ( + + - ); - } - - const node = item.node; - const isSelected = rowSelected(node, props.selectedRowRef); - const labelIndent = node.kind === 'detail' ? node.level * 16 + 16 : node.level * 16; - const canToggle = isSurfaceNode(node) && node.expandable; - const nextSelection: StatementInspectorSelection = node.kind === 'surface' - ? { kind: 'surface', key: node.row.key } - : { kind: 'detail', key: node.row.key, parentKey: node.parentSurfaceKey }; - - const buttonSize = dense ? 'size-7' : 'size-11'; - const buttonClass = dense ? 'rounded' : 'rounded-lg'; - const labelSize = dense ? 'text-[13px]' : 'text-sm'; - const detailLabelSize = dense ? 'text-xs' : 'text-[13px]'; - const paddingY = dense ? 'py-1.5' : 'py-2'; - const gapClass = dense ? 'gap-1' : 'gap-2'; - - return ( - - - {props.periods.map((period, index) => ( - - ))} - - ); - })} + ) : null} + {renderNodes({ + periods, + sections, + selectedRowRef, + onToggleRow, + onSelectRow, + renderCellValue, + periodLabelFormatter, + dense, + nodes: section.nodes, + })} + + ))}
Metric + Metric
- {props.periodLabelFormatter(period.periodEnd ?? period.filingDate)} - {period.filingType} · {period.periodLabel} + + {periodLabelFormatter( + period.periodEnd ?? period.filingDate, + )} + + + {period.filingType} · {period.periodLabel} +
- {item.sectionLabel} +
+ {section.label}
-
- {canToggle ? ( - - ) : ( - - )} - -
-
- {props.renderCellValue(node.row, period.id, index > 0 ? props.periods[index - 1]?.id ?? null : null)} -
diff --git a/components/financials/statement-row-inspector.tsx b/components/financials/statement-row-inspector.tsx index db8075a..05c00cf 100644 --- a/components/financials/statement-row-inspector.tsx +++ b/components/financials/statement-row-inspector.tsx @@ -1,5 +1,6 @@ "use client"; +import { cn } from "@/lib/utils"; import type { DetailFinancialRow, DimensionBreakdownRow, @@ -27,12 +28,14 @@ type StatementRowInspectorProps = { rowKey: string, unit: SurfaceFinancialRow["unit"], ) => string; + showHeader?: boolean; + className?: string; }; function InspectorField(props: { label: string; value: string }) { return (
-

+

{props.label}

@@ -57,16 +60,25 @@ export function StatementRowInspector(props: StatementRowInspectorProps) { : null; return ( -

-
-

- Row Details -

-

- Inspect compact-surface resolution, raw drill-down rows, and - dimensional evidence. -

-
+
+ {props.showHeader === false ? null : ( +
+

+ Row Details +

+

+ Inspect compact-surface resolution, raw drill-down rows, and + dimensional evidence. +

+
+ )} {!selection ? (

@@ -132,7 +144,7 @@ export function StatementRowInspector(props: StatementRowInspectorProps) { {selection.detailRows.length > 0 ? (

-

+

Raw Detail Labels

diff --git a/components/shell/app-shell.tsx b/components/shell/app-shell.tsx index 1608380..29f9520 100644 --- a/components/shell/app-shell.tsx +++ b/components/shell/app-shell.tsx @@ -518,8 +518,6 @@ export function AppShell({ return (
-