diff --git a/app/src/components/EmotionLogger.tsx b/app/src/components/EmotionLogger.tsx index 939eb9d..dbdf716 100644 --- a/app/src/components/EmotionLogger.tsx +++ b/app/src/components/EmotionLogger.tsx @@ -138,7 +138,7 @@ export function EmotionLogger() { key={opt.label} onClick={() => handleQuickAdd(opt.label)} title={`Log ${opt.label}`} - className={`flex items-center gap-1 px-2.5 py-1 rounded-md text-xs font-medium border transition-colors ${opt.bgColor} ${opt.color} ${opt.borderColor} hover:opacity-80`} + className={`flex items-center gap-1 px-2.5 py-1 rounded-lg text-xs font-medium border transition-colors ${opt.bgColor} ${opt.color} ${opt.borderColor} hover:opacity-80`} > {opt.emoji} {opt.label} diff --git a/app/src/components/LayoutShell.tsx b/app/src/components/LayoutShell.tsx index 76647cf..4c007ec 100644 --- a/app/src/components/LayoutShell.tsx +++ b/app/src/components/LayoutShell.tsx @@ -2,13 +2,12 @@ import { SidebarNav } from "./SidebarNav"; import { WatchlistSidebar } from "./WatchlistSidebar"; import { SymbolHeader } from "./SymbolHeader"; -import { MobileTabNav } from "./MobileTabNav"; /** * Main layout shell for the redesigned terminal. * - Desktop (>1024px): Sidebar + Watchlist + Header + Content (3-column) - * - Tablet (768-1024px): Watchlist + Header + Content (2-column, sidebar hidden) - * - Mobile (<768px): Header + Content + TabBar (single column) + * - Tablet (768-1024px): Compact watchlist bar + Header + Content (2-column, sidebar hidden, right column below center) + * - Mobile (<768px): Header + Content (single column) */ export function LayoutShell({ children }: { children: React.ReactNode }) { return ( @@ -16,6 +15,9 @@ export function LayoutShell({ children }: { children: React.ReactNode }) { {/* Symbol Header — always visible */} + {/* Tablet Compact Watchlist Bar — replaces sidebar on tablet */} + + {/* Main Content Area */}
{/* Left Sidebar Navigation — hidden on tablet/mobile */} @@ -24,16 +26,88 @@ export function LayoutShell({ children }: { children: React.ReactNode }) { {/* Watchlist Sidebar — hidden on mobile */} - {/* Content */} -
-
- {children} -
-
-
+ {/* Content + Right Analytics Column */} +
+ {/* Center Content — full width on tablet/mobile, narrower on desktop */} +
+
+ {children} +
+
- {/* Mobile Tab Navigation — shown on mobile only */} - + {/* Right Analytics Column — drops below center on tablet */} + +
+ + + ); +} + +/** + * Compact horizontal watchlist bar for tablet (768-1024px). + * Replaces the sidebar on tablet — shows 2-3 symbols with mini-overview. + * Hidden on desktop and mobile. + */ +function CompactWatchlistBar() { + return ( +
+ {/* Placeholder watchlist items — will be replaced with real data */} +
+ AAPL + +1.2% +
+
+ MSFT + +0.8% +
+
+ NVDA + -0.3% +
+
+ ); +} + +/** + * Right analytics column containing Filings and Options DD panels. + * Desktop: flex row with center + right columns side by side + * Tablet: right column drops below center content (flex column) + */ +function RightAnalyticsColumn() { + return ( +
+ {/* Collapsible Filings Panel */} +
+ +
+

Loading filings...

+
+
+ + {/* Collapsible Options DD Panel */} +
+ +
+

Loading options data...

+
+
+ + {/* Footer */} +
+

Educational analysis, not investment advice.

+
); } diff --git a/app/src/components/SidebarNav.tsx b/app/src/components/SidebarNav.tsx new file mode 100644 index 0000000..2346f1f --- /dev/null +++ b/app/src/components/SidebarNav.tsx @@ -0,0 +1,106 @@ +"use client"; +import { useState } from "react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +/** + * Left sidebar navigation with collapsible sections. + * Organized by workflow phase: Research, Execution, Analytics, Settings. + */ + +interface NavSection { + title: string; + icon: string; + items: { label: string; href: string }[]; +} + +const SECTIONS: NavSection[] = [ + { + title: "Research", + icon: "📊", + items: [ + { label: "Overview", href: "/" }, + { label: "Chart Lab", href: "/chart-lab" }, + { label: "Institutional", href: "/institutional" }, + ], + }, + { + title: "Execution", + icon: "⚡", + items: [ + { label: "Trade Plan", href: "/trade-plan" }, + { label: "Execution", href: "/execution" }, + { label: "Daily Focus", href: "/daily-focus" }, + ], + }, + { + title: "Analytics", + icon: "🔬", + items: [ + { label: "Filings", href: "/filings" }, + { label: "Options DD", href: "/options-dd" }, + ], + }, + { + title: "Settings", + icon: "⚙️", + items: [ + { label: "Profile / Auth", href: "/settings" }, + ], + }, +]; + +export function SidebarNav() { + const pathname = usePathname(); + const [collapsed, setCollapsed] = useState>({}); + + const toggleCollapse = (sectionTitle: string) => { + setCollapsed(prev => ({ ...prev, [sectionTitle]: !prev[sectionTitle] })); + }; + + return ( + + ); +} diff --git a/app/src/components/SymbolHeader.tsx b/app/src/components/SymbolHeader.tsx new file mode 100644 index 0000000..619cb46 --- /dev/null +++ b/app/src/components/SymbolHeader.tsx @@ -0,0 +1,105 @@ +"use client"; +import { useState, useEffect, useCallback } from "react"; +import { useActiveSymbol } from "@/stores/active-symbol-store"; +import { api } from "@/lib/trpc"; +import { UI_STRINGS } from "@/lib/strings"; + +/** + * Persistent top bar showing the active symbol, price, change, and search. + * This is the "central nervous system" — switching symbols updates everything globally. + */ +export function SymbolHeader() { + const symbol = useActiveSymbol((s) => s.activeSymbol); + const setActive = useActiveSymbol((s) => s.setActive); + const [draft, setDraft] = useState(symbol); + const [showSearch, setShowSearch] = useState(false); + + // Fetch current price for display + const { data: snap } = api.market.snapshot.useQuery(draft || symbol); + const quote = snap?.quote ?? null; + const sector = snap?.sector ?? null; + + const up = (quote?.change ?? 0) >= 0; + const priceLabel = quote ? `$${quote.price?.toFixed(2)}` : ""; + const changeLabel = quote && quote.change !== undefined + ? `${up ? "▲" : "▼"}${up ? "+" : ""}${quote.change.toFixed(2)} (${up ? "+" : ""}${(quote.changePercent ?? 0).toFixed(2)}%)` + : ""; + const sectorLabel = sector ? `${sector.sector} · ${sector.tickerKind}` : ""; + + const handleSubmit = useCallback((e: React.FormEvent) => { + e.preventDefault(); + const sym = draft.trim().toUpperCase() || symbol; + setActive(sym); + setShowSearch(false); + }, [draft, symbol, setActive]); + + // Keyboard shortcut: Ctrl+K to toggle search + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === "k") { + e.preventDefault(); + setShowSearch(prev => !prev); + } + if (e.key === "Escape" && showSearch) { + setShowSearch(false); + } + }; + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); + }, [showSearch]); + + // Sync draft when active symbol changes externally + useEffect(() => { + setDraft(symbol); + }, [symbol]); + + return ( +
+
+ {/* Symbol Info */} +
+

Investor Flow

+
+ {symbol} + {priceLabel && {priceLabel}} + {changeLabel && ( + + {changeLabel} + + )} +
+
+ + {/* Search Input */} +
+
+ setDraft(e.target.value)} + onFocus={() => setShowSearch(true)} + onBlur={() => setTimeout(() => setShowSearch(false), 200)} + placeholder="Search symbol (Ctrl+K)" + className="w-full bg-[#1a1b24] border border-[#2a2b3a] rounded-lg px-3 py-1.5 text-sm text-[#e6e7ec] placeholder:text-[#5a5b6a] focus:outline-none focus:border-[#4f8cff]" + /> + + ⌘K + +
+
+ + {/* Sector info */} + {sectorLabel && ( + {sectorLabel} + )} + + {/* Mobile symbol display */} +
+ {symbol} + {priceLabel && ( + {priceLabel} + )} +
+
+
+ ); +} diff --git a/app/src/components/WatchlistSidebar.tsx b/app/src/components/WatchlistSidebar.tsx new file mode 100644 index 0000000..b1e35a6 --- /dev/null +++ b/app/src/components/WatchlistSidebar.tsx @@ -0,0 +1,155 @@ +"use client"; +import { useEffect, useState } from "react"; +import { useActiveSymbol } from "@/stores/active-symbol-store"; +import { api, type WatchlistEntry, type Quote } from "@/lib/trpc"; + +/** + * Compact watchlist sidebar with mini sparklines and quick symbol switching. + * Shows portfolio summary at top. Replaces the full WatchlistPanel on the main page. + */ + +interface MiniQuote extends Quote { + sparkline?: number[]; +} + +export function WatchlistSidebar() { + const activeSymbol = useActiveSymbol((s) => s.activeSymbol); + const setActive = useActiveSymbol((s) => s.setActive); + const [entries, setEntries] = useState([]); + const [quotes, setQuotes] = useState>(new Map()); + const [draft, setDraft] = useState(""); + const [loading, setLoading] = useState(true); + + // Portfolio summary + const { data: holdings } = api.portfolio.holdings.useQuery(); + const totalValue = (holdings ?? []).reduce((sum, h) => sum + h.shares * h.avg_cost, 0); + + const load = async () => { + try { + setEntries(await api.watchlists.list()); + } catch { /* ignore */ } + setLoading(false); + }; + + useEffect(() => { load(); }, []); + + // Fetch quotes for watchlist symbols + useEffect(() => { + const fetchQuotes = async () => { + const quoteMap = new Map(); + for (const entry of entries) { + try { + const snap = await api.market.snapshot(entry.symbol); + if (snap.quote && snap.candles) { + const sparkline = snap.candles.slice(-20).map(c => c.c); + quoteMap.set(entry.symbol, { ...snap.quote, symbol: entry.symbol, sparkline }); + } + } catch { /* ignore */ } + } + setQuotes(quoteMap); + }; + if (entries.length > 0) fetchQuotes(); + }, [entries]); + + const add = async () => { + if (!draft.trim()) return; + const sym = draft.trim().toUpperCase(); + await api.watchlists.addSymbol(sym); + setDraft(""); + load(); + }; + + const remove = async (symbol: string) => { + await api.watchlists.removeSymbol(symbol); + load(); + }; + + return ( + + ); +}