diff --git a/app/globals.css b/app/globals.css index eb60e10..7ba5479 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,8 +1,10 @@ @import "tailwindcss"; :root { - --font-display: "Avenir Next", "Segoe UI", "Helvetica Neue", Arial, sans-serif; - --font-mono: "Menlo", "SFMono-Regular", "Consolas", "Liberation Mono", monospace; + --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; @@ -57,8 +59,16 @@ body { 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%), + 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)); } @@ -85,7 +95,10 @@ body { inset: 0; pointer-events: none; opacity: 0.24; - background-image: radial-gradient(rgba(220, 226, 234, 0.1) 0.7px, transparent 0.7px); + background-image: radial-gradient( + rgba(220, 226, 234, 0.1) 0.7px, + transparent 0.7px + ); background-size: 4px 4px; } @@ -114,14 +127,22 @@ 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)); + background: linear-gradient( + 180deg, + rgba(40, 43, 49, 0.92), + rgba(24, 27, 32, 0.78) + ); } .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)); + background: linear-gradient( + 180deg, + rgba(34, 37, 42, 0.9), + rgba(20, 23, 27, 0.76) + ); } .data-table th, @@ -172,8 +193,16 @@ textarea { @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%), + 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)); } @@ -183,3 +212,207 @@ textarea { font-size: 0.8125rem; } } + +.panel-compact { + padding: 0.75rem; +} + +.panel-dense { + padding: 0.5rem 0.75rem; +} + +.data-table-dense { + width: 100%; + border-collapse: collapse; +} + +.data-table-dense th, +.data-table-dense td { + border-bottom: 1px solid var(--line-weak); + padding: 0.5rem 0.6rem; + text-align: left; + font-size: 0.8125rem; + vertical-align: top; +} + +.data-table-dense th { + font-family: var(--font-mono), monospace; + font-size: 0.6875rem; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--terminal-muted); +} + +.data-table-dense tbody tr:hover { + background-color: rgba(63, 68, 76, 0.32); +} + +.metric-compact { + padding-top: 0.5rem; +} + +.metric-compact .metric-value { + font-size: 1.25rem; +} + +.metric-compact .metric-label { + font-size: 0.625rem; +} + +.index-card-row { + display: flex; + gap: 0.5rem; + overflow-x: auto; + padding-bottom: 0.25rem; + scrollbar-width: thin; +} + +.index-card-row::-webkit-scrollbar { + height: 4px; +} + +.index-card-row::-webkit-scrollbar-track { + background: transparent; +} + +.index-card-row::-webkit-scrollbar-thumb { + background: var(--line-weak); + border-radius: 2px; +} + +.index-card { + flex-shrink: 0; + min-width: 140px; + padding: 0.625rem 0.75rem; + border-right: 1px solid var(--line-weak); +} + +.index-card:last-child { + border-right: none; +} + +.index-card .label { + font-family: var(--font-mono), monospace; + font-size: 0.625rem; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--terminal-muted); +} + +.index-card .value { + font-size: 1.125rem; + font-weight: 600; + color: var(--terminal-bright); + margin-top: 0.25rem; +} + +.index-card .delta { + font-size: 0.75rem; + margin-top: 0.125rem; +} + +.index-card .delta.positive { + color: #96f5bf; +} + +.index-card .delta.negative { + color: #ff9f9f; +} + +.filter-chip { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.25rem 0.5rem; + border-radius: 0.5rem; + border: 1px solid var(--line-weak); + background: var(--panel-soft); + font-size: 0.6875rem; + font-family: var(--font-mono), monospace; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--terminal-bright); + transition: + border-color 0.15s, + background-color 0.15s; +} + +.filter-chip:hover { + border-color: var(--line-strong); +} + +.filter-chip .remove { + display: flex; + align-items: center; + justify-content: center; + width: 14px; + height: 14px; + border-radius: 3px; + cursor: pointer; + opacity: 0.6; + transition: + opacity 0.15s, + background-color 0.15s; +} + +.filter-chip .remove:hover { + opacity: 1; + background: rgba(255, 255, 255, 0.1); +} + +.section-divider { + border-top: 1px solid var(--line-weak); + margin-top: 1rem; + padding-top: 1rem; +} + +.section-divider-compact { + border-top: 1px solid var(--line-weak); + margin-top: 0.75rem; + padding-top: 0.75rem; +} + +.toolbar-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; +} + +.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) + ); +} + +.screener-table-wrap thead { + position: sticky; + top: 0; + background: rgba(28, 31, 36, 0.95); + z-index: 1; +} + +.screener-table-wrap thead::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 1px; + background: var(--line-weak); +} + +.control-compact { + min-height: 32px; + padding: 0.375rem 0.625rem; + font-size: 0.75rem; +} + +.control-compact.rounded-xl { + border-radius: 0.625rem; +} diff --git a/app/page.tsx b/app/page.tsx index d7ec763..155c682 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,31 +1,32 @@ -'use client'; +"use client"; -import { useQueryClient } from '@tanstack/react-query'; -import Link from 'next/link'; -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { Activity, Bot, RefreshCw, Sparkles } from 'lucide-react'; -import { AppShell } from '@/components/shell/app-shell'; -import { Panel } from '@/components/ui/panel'; -import { Button } from '@/components/ui/button'; -import { MetricCard } from '@/components/dashboard/metric-card'; -import { TaskFeed } from '@/components/dashboard/task-feed'; -import { useAuthGuard } from '@/hooks/use-auth-guard'; -import { useLinkPrefetch } from '@/hooks/use-link-prefetch'; +import { useQueryClient } from "@tanstack/react-query"; +import Link from "next/link"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Activity, Bot, RefreshCw, Sparkles } from "lucide-react"; +import { AppShell } from "@/components/shell/app-shell"; +import { Panel } from "@/components/ui/panel"; +import { Button } from "@/components/ui/button"; +import { TaskFeed } from "@/components/dashboard/task-feed"; +import { IndexCardRow } from "@/components/dashboard/index-card-row"; +import { useAuthGuard } from "@/hooks/use-auth-guard"; +import { useLinkPrefetch } from "@/hooks/use-link-prefetch"; +import { queuePortfolioInsights, queuePriceRefresh } from "@/lib/api"; +import { buildGraphingHref } from "@/lib/graphing/catalog"; +import type { PortfolioInsight, PortfolioSummary, Task } from "@/lib/types"; import { - queuePortfolioInsights, - queuePriceRefresh -} from '@/lib/api'; -import { buildGraphingHref } from '@/lib/graphing/catalog'; -import type { PortfolioInsight, PortfolioSummary, Task } from '@/lib/types'; -import { formatCompactCurrency, formatCurrency, formatPercent } from '@/lib/format'; -import { queryKeys } from '@/lib/query/keys'; + formatCompactCurrency, + formatCurrency, + formatPercent, +} from "@/lib/format"; +import { queryKeys } from "@/lib/query/keys"; import { filingsQueryOptions, latestPortfolioInsightQueryOptions, portfolioSummaryQueryOptions, recentTasksQueryOptions, - watchlistQueryOptions -} from '@/lib/query/options'; + watchlistQueryOptions, +} from "@/lib/query/options"; type DashboardState = { summary: PortfolioSummary; @@ -38,15 +39,15 @@ type DashboardState = { const EMPTY_STATE: DashboardState = { summary: { positions: 0, - total_value: '0', - total_gain_loss: '0', - total_cost_basis: '0', - avg_return_pct: '0' + total_value: "0", + total_gain_loss: "0", + total_cost_basis: "0", + avg_return_pct: "0", }, filingsCount: 0, watchlistCount: 0, tasks: [], - latestInsight: null + latestInsight: null, }; export default function CommandCenterPage() { @@ -71,23 +72,24 @@ export default function CommandCenterPage() { setError(null); try { - const [summaryRes, filingsRes, watchlistRes, tasksRes, insightRes] = await Promise.all([ - queryClient.ensureQueryData(summaryOptions), - queryClient.ensureQueryData(filingsOptions), - queryClient.ensureQueryData(watchlistOptions), - queryClient.ensureQueryData(tasksOptions), - queryClient.ensureQueryData(insightOptions) - ]); + const [summaryRes, filingsRes, watchlistRes, tasksRes, insightRes] = + await Promise.all([ + queryClient.ensureQueryData(summaryOptions), + queryClient.ensureQueryData(filingsOptions), + queryClient.ensureQueryData(watchlistOptions), + queryClient.ensureQueryData(tasksOptions), + queryClient.ensureQueryData(insightOptions), + ]); setState({ summary: summaryRes.summary, filingsCount: filingsRes.filings.length, watchlistCount: watchlistRes.items.length, tasks: tasksRes.tasks, - latestInsight: insightRes.insight + latestInsight: insightRes.insight, }); } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to load dashboard'); + setError(err instanceof Error ? err.message : "Failed to load dashboard"); } finally { setLoading(false); } @@ -100,39 +102,55 @@ export default function CommandCenterPage() { }, [isPending, isAuthenticated, loadData]); const headerActions = ( - <> +
{error}
-Loading tasks...
++ Loading... +
) : (Loading intelligence output...
- ) : state.latestInsight ? ( - <> -{state.latestInsight.content}
- > + ) : null} ++ Loading... +
+ ) : state.latestInsight ? ( ++ {state.latestInsight.content} +
) : ( -No AI brief yet. Queue one from the action bar.
++ No AI brief yet. Queue one from the action bar. +
)} - +Overview
-Inspect one company across price, SEC context, valuation, and recent developments.
- - -Financials
-Focus on multi-period filing metrics, margins, leverage, and balance sheet composition.
- - -Graphing
-Compare one normalized metric across multiple companies with shareable chart state.
++ Overview +
++ Company analysis, price, valuation, and developments. +
++ Financials +
++ Multi-period metrics, margins, and balance sheet. +
+ + ++ Graphing +
++ Compare normalized metrics across companies. +
+ + { - void queryClient.prefetchQuery(filingsQueryOptions({ limit: 120 })); + void queryClient.prefetchQuery( + filingsQueryOptions({ limit: 120 }), + ); }} onFocus={() => { - void queryClient.prefetchQuery(filingsQueryOptions({ limit: 120 })); + void queryClient.prefetchQuery( + filingsQueryOptions({ limit: 120 }), + ); }} > -Filings
-Sync SEC filings and trigger AI memo analysis.
++ Filings +
++ SEC filings and AI memo analysis. +
prefetchPortfolioSurfaces()} onFocus={() => prefetchPortfolioSurfaces()} > -Portfolio
-Manage the active private portfolio and mark positions to market.
++ Portfolio +
++ Manage positions and mark to market. +
prefetchPortfolioSurfaces()} onFocus={() => prefetchPortfolioSurfaces()} > -Coverage
-Track research status, review cadence, and filing freshness per company.
++ Coverage +
++ Track research status and filing freshness. +
{label}
+{value}
+ {delta ? ( ++ {delta} +
+ ) : null} +{label}
+{value}
+ {delta ? ( ++ {delta} +
+ ) : null} ++ {label} +
++ {value} +
+ {delta ? ( ++ {delta} +
+ ) : null} +{label}
-{value}
++ {label} +
++ {value} +
{delta ? ( -+
{delta}
) : null} diff --git a/components/shell/app-shell.tsx b/components/shell/app-shell.tsx index 3bcea89..1608380 100644 --- a/components/shell/app-shell.tsx +++ b/components/shell/app-shell.tsx @@ -530,7 +530,7 @@ export function AppShell({ className={cn( "hidden shrink-0 flex-col gap-4 border-r border-[color:var(--line-weak)] lg:flex", hasMounted ? "transition-[width,padding] duration-200" : "", - isSidebarCollapsed ? "w-16 pr-1" : "w-72 pr-4", + isSidebarCollapsed ? "w-16 pr-1" : "w-56 pr-4", )} >{subtitle}
: null} + {title ? ( ++ {subtitle} +
+ ) : null}