diff --git a/web/src/App.tsx b/web/src/App.tsx index 607fd68..0af8656 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -130,7 +130,7 @@ function App() { return (
Loading… @@ -160,7 +160,7 @@ function App() { {hasCriticalEvents && (
diff --git a/web/src/components/layout/Header.tsx b/web/src/components/layout/Header.tsx index 68ba94a..6c00f1a 100644 --- a/web/src/components/layout/Header.tsx +++ b/web/src/components/layout/Header.tsx @@ -1,8 +1,10 @@ import { useState, useEffect, useRef } from 'react'; -import { Search } from 'lucide-react'; +import { Moon, Search, Sun } from 'lucide-react'; import { useAppContext } from '../../context/AppContext'; import { useNavigate } from 'react-router-dom'; import { fetchMarketIndices, type IndexSnapshot } from '../../lib/marketApi'; +import { useTheme } from '../theme/ThemeProvider'; +import { Button } from '../ui/button'; export function Header() { @@ -11,6 +13,7 @@ export function Header() { const [indices, setIndices] = useState([]); const navigate = useNavigate(); const inputRef = useRef(null); + const { theme, toggleTheme } = useTheme(); // Fetch live market indices while visible; hidden tabs should not burn API quota. useEffect(() => { @@ -60,14 +63,15 @@ export function Header() { return (
{/* Search bar */}
@@ -78,7 +82,7 @@ export function Header() { left: 11, top: '50%', transform: 'translateY(-50%)', - color: '#9CA3AF', + color: 'var(--muted-foreground)', pointerEvents: 'none', }} /> @@ -99,12 +103,12 @@ export function Header() { paddingRight: 12, paddingTop: 8, paddingBottom: 8, - border: '1px solid #E5E7EB', + border: '1px solid var(--border)', borderRadius: 8, fontSize: 13, outline: 'none', - color: '#374151', - background: '#F9FAFB', + color: 'var(--foreground)', + background: 'var(--input)', boxSizing: 'border-box', fontFamily: 'inherit', }} @@ -121,8 +125,8 @@ export function Header() { ['S&P 500','Dow Jones','Nasdaq'].map(label => (
-
{label}
-
+
{label}
+
)) @@ -131,11 +135,11 @@ export function Header() { return (
-
+
{idx.label}
- + ${idx.price.toFixed(2)} + + {/* Live indicator */}
- + {connected ? 'Live' : 'Reconnecting'}
diff --git a/web/src/components/layout/Sidebar.tsx b/web/src/components/layout/Sidebar.tsx index 50781b8..d787b44 100644 --- a/web/src/components/layout/Sidebar.tsx +++ b/web/src/components/layout/Sidebar.tsx @@ -30,13 +30,13 @@ export function Sidebar() { width: 40, height: 40, borderRadius: 10, - background: 'linear-gradient(135deg, #2563EB, #1D4ED8)', + background: 'linear-gradient(135deg, var(--accent), color-mix(in oklab, var(--accent) 70%, #1d4ed8 30%))', display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 24, flexShrink: 0, - boxShadow: '0 2px 8px rgba(37,99,235,0.35)', + boxShadow: '0 2px 8px color-mix(in oklab, var(--accent) 35%, transparent 65%)', cursor: 'pointer', fontSize: 20, }}> @@ -58,10 +58,10 @@ export function Sidebar() { justifyContent: 'center', padding: '10px 0 8px', gap: 3, - borderLeft: isActive ? '3px solid #2563EB' : '3px solid transparent', - color: isActive ? '#2563EB' : '#6B7280', + borderLeft: isActive ? '3px solid var(--accent)' : '3px solid transparent', + color: isActive ? 'var(--accent)' : 'var(--muted-foreground)', textDecoration: 'none', - background: isActive ? '#EFF6FF' : 'transparent', + background: isActive ? 'var(--sidebar-active)' : 'transparent', transition: 'all 0.15s', })} > @@ -91,7 +91,7 @@ export function Sidebar() { width: 36, height: 36, borderRadius: '50%', - background: '#2563EB', + background: 'var(--accent)', display: 'flex', alignItems: 'center', justifyContent: 'center', @@ -113,7 +113,7 @@ export function Sidebar() { height: 9, borderRadius: '50%', background: '#22C55E', - border: '2px solid #fff', + border: '2px solid var(--card)', }} />
diff --git a/web/src/components/theme/ThemeProvider.tsx b/web/src/components/theme/ThemeProvider.tsx new file mode 100644 index 0000000..fbfbaba --- /dev/null +++ b/web/src/components/theme/ThemeProvider.tsx @@ -0,0 +1,64 @@ +import { createContext, useContext, useEffect, useMemo, useState } from 'react'; +import type { ReactNode } from 'react'; + +type Theme = 'light' | 'dark'; + +type ThemeContextValue = { + theme: Theme; + setTheme: (theme: Theme) => void; + toggleTheme: () => void; +}; + +const STORAGE_KEY = 'trading-dashboard-theme'; + +const ThemeContext = createContext(null); + +function applyTheme(theme: Theme) { + const root = document.documentElement; + root.classList.toggle('dark', theme === 'dark'); + root.dataset.theme = theme; +} + +function resolveInitialTheme(): Theme { + if (typeof window === 'undefined') return 'light'; + const stored = window.localStorage.getItem(STORAGE_KEY); + if (stored === 'light' || stored === 'dark') return stored; + return window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; +} + +export function ThemeProvider({ children }: { children: ReactNode }) { + const [theme, setThemeState] = useState(() => resolveInitialTheme()); + + useEffect(() => { + applyTheme(theme); + window.localStorage.setItem(STORAGE_KEY, theme); + }, [theme]); + + useEffect(() => { + const media = window.matchMedia?.('(prefers-color-scheme: dark)'); + if (!media) return; + const handleChange = () => { + const stored = window.localStorage.getItem(STORAGE_KEY); + if (stored === 'light' || stored === 'dark') return; + setThemeState(media.matches ? 'dark' : 'light'); + }; + media.addEventListener?.('change', handleChange); + return () => media.removeEventListener?.('change', handleChange); + }, []); + + const value = useMemo(() => ({ + theme, + setTheme: setThemeState, + toggleTheme: () => setThemeState(prev => (prev === 'dark' ? 'light' : 'dark')), + }), [theme]); + + return {children}; +} + +export function useTheme() { + const ctx = useContext(ThemeContext); + if (!ctx) { + throw new Error('useTheme must be used within ThemeProvider'); + } + return ctx; +} diff --git a/web/src/components/ui/button.tsx b/web/src/components/ui/button.tsx new file mode 100644 index 0000000..cebe76b --- /dev/null +++ b/web/src/components/ui/button.tsx @@ -0,0 +1,45 @@ +import type { ButtonHTMLAttributes, ReactNode } from 'react'; +import { cn } from '../../lib/utils'; + +type ButtonVariant = 'default' | 'outline' | 'ghost' | 'destructive'; +type ButtonSize = 'sm' | 'md' | 'lg' | 'icon'; + +const variantClasses: Record = { + default: 'bg-[var(--primary)] text-[var(--primary-foreground)] hover:brightness-95 shadow-sm', + outline: 'border border-[var(--border)] bg-[var(--card)] text-[var(--foreground)] hover:bg-[var(--accent-soft)]', + ghost: 'text-[var(--muted-foreground)] hover:bg-[var(--accent-soft)] hover:text-[var(--foreground)]', + destructive: 'bg-[var(--destructive)] text-white hover:brightness-95 shadow-sm', +}; + +const sizeClasses: Record = { + sm: 'h-9 px-3 text-xs', + md: 'h-10 px-4 text-sm', + lg: 'h-12 px-5 text-sm', + icon: 'h-10 w-10', +}; + +export function Button({ + className, + variant = 'default', + size = 'md', + children, + ...props +}: ButtonHTMLAttributes & { + variant?: ButtonVariant; + size?: ButtonSize; + children: ReactNode; +}) { + return ( + + ); +} diff --git a/web/src/components/ui/card.tsx b/web/src/components/ui/card.tsx new file mode 100644 index 0000000..b8fc5ac --- /dev/null +++ b/web/src/components/ui/card.tsx @@ -0,0 +1,45 @@ +import type { HTMLAttributes, ReactNode } from 'react'; +import { cn } from '../../lib/utils'; + +export function Card({ className, children, ...props }: HTMLAttributes & { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} + +export function CardHeader({ className, children, ...props }: HTMLAttributes & { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} + +export function CardTitle({ className, children, ...props }: HTMLAttributes & { children: ReactNode }) { + return ( +

+ {children} +

+ ); +} + +export function CardDescription({ className, children, ...props }: HTMLAttributes & { children: ReactNode }) { + return ( +

+ {children} +

+ ); +} + +export function CardContent({ className, children, ...props }: HTMLAttributes & { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/web/src/components/ui/input.tsx b/web/src/components/ui/input.tsx new file mode 100644 index 0000000..c401b78 --- /dev/null +++ b/web/src/components/ui/input.tsx @@ -0,0 +1,14 @@ +import type { InputHTMLAttributes } from 'react'; +import { cn } from '../../lib/utils'; + +export function Input({ className, ...props }: InputHTMLAttributes) { + return ( + + ); +} diff --git a/web/src/components/ui/page-header.tsx b/web/src/components/ui/page-header.tsx new file mode 100644 index 0000000..b45ece5 --- /dev/null +++ b/web/src/components/ui/page-header.tsx @@ -0,0 +1,28 @@ +import type { ReactNode } from 'react'; +import { cn } from '../../lib/utils'; + +export function PageHeader({ + title, + description, + action, + className, +}: { + title: string; + description?: string; + action?: ReactNode; + className?: string; +}) { + return ( +
+
+

{title}

+ {description ? ( +

+ {description} +

+ ) : null} +
+ {action ?
{action}
: null} +
+ ); +} diff --git a/web/src/components/ui/select.tsx b/web/src/components/ui/select.tsx new file mode 100644 index 0000000..e185e45 --- /dev/null +++ b/web/src/components/ui/select.tsx @@ -0,0 +1,16 @@ +import type { SelectHTMLAttributes } from 'react'; +import { cn } from '../../lib/utils'; + +export function Select({ className, children, ...props }: SelectHTMLAttributes) { + return ( + + ); +} diff --git a/web/src/index.css b/web/src/index.css index 0681657..7cb4aee 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -3,13 +3,33 @@ @tailwind utilities; :root { - font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; + font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; line-height: 1.5; font-weight: 400; - color-scheme: light; color: #111827; - background-color: #F3F4F6; + background-color: #f4f7fb; + --background: #f4f7fb; + --foreground: #0f172a; + --card: #ffffff; + --card-elevated: #fcfdff; + --input: #ffffff; + --muted: #e8edf5; + --muted-foreground: #64748b; + --border: rgba(15, 23, 42, 0.10); + --border-strong: rgba(15, 23, 42, 0.18); + --primary: #0f172a; + --primary-foreground: #f8fafc; + --accent: #2563eb; + --accent-soft: rgba(37, 99, 235, 0.10); + --ring: #2563eb; + --ring-soft: rgba(37, 99, 235, 0.18); + --destructive: #dc2626; + --sidebar: rgba(255, 255, 255, 0.88); + --sidebar-active: rgba(37, 99, 235, 0.12); + --header: rgba(255, 255, 255, 0.88); + --card-shadow: 0 24px 70px rgba(15, 23, 42, 0.10); + --hero-gradient: linear-gradient(135deg, #eff6ff 0%, #ffffff 45%, #eefbf5 100%); font-synthesis: none; text-rendering: optimizeLegibility; @@ -17,10 +37,38 @@ -moz-osx-font-smoothing: grayscale; } +html.dark { + color-scheme: dark; + color: #e5edf7; + background-color: #0b1220; + --background: #0b1220; + --foreground: #e5edf7; + --card: #101827; + --card-elevated: #0f172a; + --input: #0d1524; + --muted: #162133; + --muted-foreground: #94a3b8; + --border: rgba(148, 163, 184, 0.14); + --border-strong: rgba(148, 163, 184, 0.24); + --primary: #f8fafc; + --primary-foreground: #0f172a; + --accent: #60a5fa; + --accent-soft: rgba(96, 165, 250, 0.14); + --ring: #60a5fa; + --ring-soft: rgba(96, 165, 250, 0.2); + --destructive: #f87171; + --sidebar: rgba(10, 15, 26, 0.92); + --sidebar-active: rgba(96, 165, 250, 0.16); + --header: rgba(10, 15, 26, 0.78); + --card-shadow: 0 28px 90px rgba(2, 6, 23, 0.45); + --hero-gradient: linear-gradient(135deg, rgba(37, 99, 235, 0.10) 0%, rgba(15, 23, 42, 0.94) 55%, rgba(34, 197, 94, 0.08) 100%); +} + body { margin: 0; min-height: 100vh; - background-color: #F3F4F6; + background-color: var(--background); + color: var(--foreground); } #root { @@ -76,7 +124,7 @@ body { .dashboard-shell { display: flex; min-height: 100vh; - background: #F3F4F6; + background: var(--background); font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; } @@ -112,8 +160,9 @@ body { .trading-sidebar { width: 72px; min-height: 100vh; - background: #ffffff; - border-right: 1px solid #E5E7EB; + background: var(--sidebar); + border-right: 1px solid var(--border); + backdrop-filter: blur(18px); display: flex; flex-direction: column; align-items: center; @@ -126,6 +175,49 @@ body { z-index: 50; } +.page-section-grid { + display: grid; + gap: 24px; +} + +.tab-strip { + display: flex; + gap: 4px; + margin-bottom: 20px; + border-bottom: 1px solid var(--border); +} + +.tab-button { + padding: 8px 16px; + border: none; + border-bottom: 2px solid transparent; + background: transparent; + color: var(--muted-foreground); + font-size: 13px; + font-weight: 500; + cursor: pointer; + margin-bottom: -1px; + transition: all 0.15s; + font-family: inherit; +} + +.tab-button:hover { + color: var(--foreground); +} + +.tab-button[data-active="true"] { + color: var(--accent); + border-bottom-color: var(--accent); + font-weight: 700; +} + +.hero-surface { + background: var(--hero-gradient); + border: 1px solid var(--border); + border-radius: 28px; + box-shadow: var(--card-shadow); +} + .trading-sidebar-nav { flex: 1; display: flex; @@ -173,7 +265,7 @@ body { align-items: stretch; padding: 0 8px; border-right: 0; - border-top: 1px solid #E5E7EB; + border-top: 1px solid var(--border); overflow-x: auto; } diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts new file mode 100644 index 0000000..f716c28 --- /dev/null +++ b/web/src/lib/utils.ts @@ -0,0 +1,3 @@ +export function cn(...values: Array) { + return values.filter(Boolean).join(' '); +} diff --git a/web/src/main.tsx b/web/src/main.tsx index df14df1..dd67b07 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -5,21 +5,16 @@ import './index.css' import App from './App.tsx' import { AuthProvider } from './components/AuthContext'; import { ProductAccessibilityGate } from './components/ProductAccessibilityGate'; -import { tradingTelemetry } from './lib/runtime'; - -const telemetryClient = tradingTelemetry.init(); -telemetryClient.trackEvent('info', 'app_shell', 'trading_web_bootstrap', { - feature: 'bootstrap', - message: window.location.pathname, - tags: { surface: 'web' }, -}); +import { ThemeProvider } from './components/theme/ThemeProvider'; createRoot(document.getElementById('root')!).render( - - - + + + + + {import.meta.env.DEV && (
diff --git a/web/src/views/AlertsView.tsx b/web/src/views/AlertsView.tsx index 3921f46..ca03123 100644 --- a/web/src/views/AlertsView.tsx +++ b/web/src/views/AlertsView.tsx @@ -1,11 +1,15 @@ import { useAppContext } from '../context/AppContext'; import { AlertFeed } from '../components/AlertFeed'; +import { PageHeader } from '../components/ui/page-header'; export function AlertsView() { const { botState } = useAppContext(); return (
-

Alerts

+
); diff --git a/web/src/views/HomeView.tsx b/web/src/views/HomeView.tsx index 990271c..ae5f168 100644 --- a/web/src/views/HomeView.tsx +++ b/web/src/views/HomeView.tsx @@ -11,6 +11,7 @@ import { type OHLCVBar, } from '../lib/marketApi'; import { SkeletonBlock, SkeletonText } from '../components/Skeleton'; +import { Button } from '../components/ui/button'; // ─── Time period config ─────────────────────────────────────────────────────── const PERIODS = ['1D', '5D', '1M', '3M', '6M', 'YTD', '1Y', '5Y', 'MAX'] as const; @@ -230,43 +231,39 @@ export function TickerHeader({ return (
-

+

{symbol}

- + {companyName}
- - + +
- + {price > 0 ? price.toFixed(2) : '—'} {price > 0 && ( @@ -276,7 +273,7 @@ export function TickerHeader({ )}
-
+
{formatAsOfTimestamp(latestBarTimestamp ?? null)} ET · {exchange}
@@ -344,7 +341,7 @@ function StockChart({ const firstPrice = chartData[0]?.price ?? 0; const lastPrice = chartData[chartData.length - 1]?.price ?? 0; const positive = lastPrice >= firstPrice; - const lineColor = positive ? '#2563EB' : '#DC2626'; + const lineColor = positive ? 'var(--primary)' : 'var(--destructive)'; const priceYValues = chartData.flatMap(d => [ d.price, @@ -367,9 +364,9 @@ function StockChart({ return (
@@ -386,8 +383,8 @@ function StockChart({ borderRadius: 6, fontSize: 12, fontWeight: period === p ? 700 : 500, - background: period === p ? '#EFF6FF' : 'transparent', - color: period === p ? '#2563EB' : '#6B7280', + background: period === p ? 'var(--accent-soft)' : 'transparent', + color: period === p ? 'var(--primary)' : 'var(--muted-foreground)', cursor: 'pointer', transition: 'all 0.15s', }} @@ -396,7 +393,7 @@ function StockChart({ ))}
-
+
{INDICATORS.map(indicator => { const active = enabledIndicators[indicator.key]; @@ -409,13 +406,13 @@ function StockChart({ style={{ padding: '5px 9px', borderRadius: 999, - border: active ? '1px solid #93C5FD' : '1px solid #E5E7EB', - background: active ? '#EFF6FF' : '#fff', - color: active ? '#1D4ED8' : '#6B7280', + border: active ? '1px solid var(--primary)' : '1px solid var(--border)', + background: active ? 'var(--accent-soft)' : 'var(--card)', + color: active ? 'var(--primary)' : 'var(--muted-foreground)', fontSize: 11, fontWeight: 700, cursor: 'pointer', - boxShadow: active ? '0 2px 8px rgba(37,99,235,0.10)' : 'none', + boxShadow: active ? '0 2px 8px color-mix(in oklab, var(--primary) 18%, transparent 82%)' : 'none', }} > {indicator.label} @@ -429,10 +426,10 @@ function StockChart({
-
+
Indicators: {enabledCount > 0 ? `${enabledCount} active` : 'none'}
-
+
RSI 14 · MACD 12/26/9 · Bollinger 20/2
@@ -441,9 +438,9 @@ function StockChart({ {loading ? (
- + Loading chart…
) : error ? ( @@ -461,11 +458,11 @@ function StockChart({ flexDirection: 'column', alignItems: 'center', justifyContent: 'center', - color: '#9CA3AF', + color: 'var(--muted-foreground)', fontSize: 13, gap: 8, }}> - + No price data available for {symbol}
) : ( @@ -478,17 +475,17 @@ function StockChart({ - + { const labels: Record = { @@ -512,7 +509,7 @@ function StockChart({ const key = String(name ?? ''); return [`$${Number(val).toFixed(2)}`, labels[key] ?? key]; }} - labelStyle={{ color: '#6B7280', fontSize: 11 }} + labelStyle={{ color: 'var(--muted-foreground)', fontSize: 11 }} /> {enabledIndicators.bollinger && ( <> @@ -534,20 +531,20 @@ function StockChart({ {enabledIndicators.rsi && ( -
-
+
+
RSI (14) - 70 overbought · 30 oversold + 70 overbought · 30 oversold
- + - + [Number(val).toFixed(1), 'RSI']} - labelStyle={{ color: '#6B7280', fontSize: 11 }} + labelStyle={{ color: 'var(--muted-foreground)', fontSize: 11 }} /> @@ -558,23 +555,23 @@ function StockChart({ )} {enabledIndicators.macd && ( -
-
+
+
MACD (12, 26, 9)
- + { const labels: Record = { macdHistogram: 'Histogram', @@ -584,7 +581,7 @@ function StockChart({ const key = String(name ?? ''); return [Number(val).toFixed(3), labels[key] ?? key]; }} - labelStyle={{ color: '#6B7280', fontSize: 11 }} + labelStyle={{ color: 'var(--muted-foreground)', fontSize: 11 }} /> @@ -623,13 +620,13 @@ function QuickStats({ symbol, bars }: { symbol: string; bars: OHLCVBar[] }) {
{stats.map(s => (
-
{s.label}
-
{s.value}
+
{s.label}
+
{s.value}
))}
@@ -693,8 +690,8 @@ function ResearchCards({ return (
{/* Company Profile */} -
-
+
+
📋 Company
{profileLoading ? ( @@ -706,40 +703,40 @@ function ResearchCards({
) : profile ? ( <> -
+
{profile.companyName ?? symbol} {profile.sector && <> · {profile.sector}} {profile.industry && <> · {profile.industry}}
-
{profile.description ?? ''}
{profile.website && ( + style={{ fontSize: 11, color: 'var(--primary)', textDecoration: 'none' }}> {profile.website} )} ) : ( -
No profile data
+
No profile data
)}
{/* Financials */} -
-
+
+
📊 Financials
{financialRows.map(([label, val]) => (
- {label} - + {label} + {loading || profileLoading ? 10 ? 64 : 46} /> : val}
@@ -747,8 +744,8 @@ function ResearchCards({
{/* Events / Earnings */} -
-
+
+
📅 Events
{[ @@ -759,19 +756,19 @@ function ResearchCards({ ].map(([label, val]) => (
- {label} - + {label} + {val === '…' ? : val}
))} {!loading && earnings.length > 0 && (
-
Past Earnings
+
Past Earnings
{earnings.slice(0,3).map((e, i) => ( -
+
{fmtDate(e.date)} = (e.epsEstimated ?? e.eps) ? '#16A34A' : '#DC2626' }}> EPS {e.eps != null ? `$${e.eps.toFixed(2)}` : '—'} @@ -799,17 +796,17 @@ function EmptyState({
📈
-
+
Search an asset to get started
Type a ticker symbol, crypto pair, or company name in the search bar above to view charts, financials, and news.
{cryptoMode && ( -
+
Suggested from your crypto bot configuration
)} @@ -820,9 +817,9 @@ function EmptyState({ onClick={() => onSelect(t)} style={{ padding: '4px 12px', - background: '#EFF6FF', - color: '#2563EB', - border: '1px solid #BFDBFE', + background: 'var(--accent-soft)', + color: 'var(--primary)', + border: '1px solid var(--border)', borderRadius: 20, fontSize: 13, fontWeight: 600, diff --git a/web/src/views/MarketsView.tsx b/web/src/views/MarketsView.tsx index ca73190..3f82341 100644 --- a/web/src/views/MarketsView.tsx +++ b/web/src/views/MarketsView.tsx @@ -3,6 +3,7 @@ import { useAppContext } from '../context/AppContext'; import { MarketplaceTab } from '../tabs/MarketplaceTab'; import { TopVolatile, AISetups } from '../components/MarketOpportunities'; import type { StrategyPreset } from '../lib/PresetRegistry'; +import { PageHeader } from '../components/ui/page-header'; export function MarketsView() { const { botState, showMarketplaceTab } = useAppContext(); @@ -12,7 +13,10 @@ export function MarketsView() { return (
-

Markets

+
diff --git a/web/src/views/PortfolioView.tsx b/web/src/views/PortfolioView.tsx index a630de6..f79b18f 100644 --- a/web/src/views/PortfolioView.tsx +++ b/web/src/views/PortfolioView.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { useAppContext } from '../context/AppContext'; import { PositionsTab } from '../tabs/PositionsTab'; import { HistoryTab } from '../tabs/HistoryTab'; +import { PageHeader } from '../components/ui/page-header'; const TABS = ['Positions & Orders', 'Trade History'] as const; type Tab = typeof TABS[number]; @@ -12,29 +13,18 @@ export function PortfolioView() { return (
-

Portfolio

+ - {/* Sub-tabs */} -
+
{TABS.map(t => ( diff --git a/web/src/views/ResearchView.tsx b/web/src/views/ResearchView.tsx index 4bb7299..83795bc 100644 --- a/web/src/views/ResearchView.tsx +++ b/web/src/views/ResearchView.tsx @@ -6,6 +6,7 @@ import { MyStrategiesTab } from '../tabs/MyStrategiesTab'; import { VisualRuleBuilder, type VisualRule } from '../components/strategy/VisualRuleBuilder'; import { createTradeProfile } from '../lib/profileApi'; import { BacktestRunnerPanel } from '../backtest/components/BacktestRunnerPanel'; +import { PageHeader } from '../components/ui/page-header'; type ResearchTab = 'Strategies' | 'Visual Builder' | 'Code Editor' | 'Signals' | 'Backtesting'; @@ -35,19 +36,8 @@ function SubTab({ return ( @@ -93,12 +83,12 @@ export function ResearchView() { return (
-

Research

+ -
+
{tabs.map(t => ( setTab(t)} /> ))} diff --git a/web/src/views/ScreenerView.tsx b/web/src/views/ScreenerView.tsx index 6363ba5..eeb6be8 100644 --- a/web/src/views/ScreenerView.tsx +++ b/web/src/views/ScreenerView.tsx @@ -6,6 +6,11 @@ import { getPlatformAccessToken } from '../lib/authSession'; import { tradingRuntime } from '../lib/runtime'; import { createRequestId } from '../../../shared/request-id.js'; import { SkeletonBlock } from '../components/Skeleton'; +import { PageHeader } from '../components/ui/page-header'; +import { Button } from '../components/ui/button'; +import { Input } from '../components/ui/input'; +import { Select } from '../components/ui/select'; +import { Card, CardContent } from '../components/ui/card'; // ─── Types ──────────────────────────────────────────────────────────────────── interface ScreenerRow { @@ -122,7 +127,7 @@ export function ScreenerView() { }; const SortIcon = ({ k }: { k: keyof ScreenerRow }) => ( - + {sortKey === k ? (sortAsc ? '▲' : '▼') : '⇅'} ); @@ -137,7 +142,7 @@ export function ScreenerView() { display: 'grid', gridTemplateColumns: '100px 1fr 90px 90px 110px 80px 110px', padding: '12px 16px', - borderBottom: i < 5 ? '1px solid #F9FAFB' : 'none', + borderBottom: i < 5 ? '1px solid var(--border)' : 'none', alignItems: 'center', columnGap: 0, }} @@ -161,65 +166,46 @@ export function ScreenerView() { return (
-
-

Stock Screener

-
- -
+ + + Refresh + + } + /> - {/* Filters */} -
- {/* Search */} -
- - setQuery(e.target.value)} - style={{ - width: '100%', paddingLeft: 32, paddingRight: 12, - paddingTop: 8, paddingBottom: 8, - border: '1px solid #E5E7EB', borderRadius: 8, - fontSize: 13, outline: 'none', background: '#fff', - color: '#374151', boxSizing: 'border-box', fontFamily: 'inherit', - }} - /> -
+ + +
+
+ + setQuery(e.target.value)} + style={{ paddingLeft: 32 }} + /> +
- {/* Market cap */} - + - {/* Sector pills */} -
- +
+ {SECTORS.slice(0, 6).map(s => ( ))} - {/* Sector dropdown for rest */} - -
-
+ +
+
+ + - {/* Error */} {error && !loading && (
{error}
)} - {/* Results table */} -
- {/* Header */} +
{([ ['symbol', 'Symbol'], @@ -295,7 +281,7 @@ export function ScreenerView() { key={key} onClick={() => handleSort(key)} style={{ - fontSize: 11, color: '#9CA3AF', fontWeight: 700, + fontSize: 11, color: 'var(--muted-foreground)', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em', cursor: 'pointer', userSelect: 'none', }} @@ -305,10 +291,8 @@ export function ScreenerView() { ))}
- {/* Loading */} {loading && } - {/* Rows */} {!loading && filtered.map((row, i) => (
(e.currentTarget.style.background = '#F9FAFB')} + onMouseEnter={e => (e.currentTarget.style.background = 'var(--muted)')} onMouseLeave={e => (e.currentTarget.style.background = 'transparent')} > - {row.symbol} - {row.companyName} - + {row.symbol} + {row.companyName} + {row.price != null ? `$${row.price.toFixed(2)}` : '—'} {row.changesPercentage >= 0 ? '+' : ''}{row.changesPercentage?.toFixed(2)}% - + {row.marketCap ? fmtCap(row.marketCap) : '—'} - + {row.pe != null && row.pe > 0 ? row.pe.toFixed(1) : '—'} - + {row.volume ? fmtCap(row.volume).replace('$', '') : '—'}
))} {!loading && filtered.length === 0 && !error && ( -
+
No results match your filters
)}
{!loading && filtered.length > 0 && ( -
+
{filtered.length} companies · Click any row to view chart & research
)} diff --git a/web/src/views/SettingsView.tsx b/web/src/views/SettingsView.tsx index f524077..461a34b 100644 --- a/web/src/views/SettingsView.tsx +++ b/web/src/views/SettingsView.tsx @@ -3,6 +3,7 @@ import { useAppContext } from '../context/AppContext'; import { SettingsTab } from '../tabs/SettingsTab'; import { AdminTab } from '../tabs/AdminTab'; import { ConfigTab } from '../tabs/ConfigTab'; +import { PageHeader } from '../components/ui/page-header'; type SettingsSection = 'Account' | 'Bot Config' | 'Admin Panel'; @@ -16,29 +17,19 @@ export function SettingsView() { const [section, setSection] = useState('Account'); return ( -
-

Settings

+
+ -
+
{sections.map(s => ( @@ -48,12 +39,12 @@ export function SettingsView() {
{section === 'Account' && } diff --git a/web/src/views/SimpleView.tsx b/web/src/views/SimpleView.tsx index 08b579c..e826992 100644 --- a/web/src/views/SimpleView.tsx +++ b/web/src/views/SimpleView.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useState } from 'react'; import type { FormEvent } from 'react'; -import { Pencil, RefreshCw, Target, Trash2 } from 'lucide-react'; +import { Pencil, RefreshCw, Trash2 } from 'lucide-react'; import { useAppContext } from '../context/AppContext'; import { fetchChartBars } from '../lib/marketApi'; import { @@ -11,6 +11,11 @@ import { type ManualEntryPayload, } from '../lib/manualEntriesApi'; import { fetchTradeProfiles, type TradeProfilePayload } from '../lib/profileApi'; +import { Button } from '../components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card'; +import { Input } from '../components/ui/input'; +import { PageHeader } from '../components/ui/page-header'; +import { Select } from '../components/ui/select'; type SimpleSide = 'buy' | 'sell'; type TriggerMode = 'dollar' | 'percent'; @@ -457,33 +462,19 @@ export function SimpleView() { return (
-
-
-
- -
-
-

Simple

-

- Save dip-buy setups for selective long-term names. Buy after a dollar or percent drop, - then keep a profit exit armed so the worst-case path is holding a name you already like. -

-
-
-
+
-
-
+ +
-

- {editingSetupId ? 'Edit setup' : 'New setup'} -

-

- Saved trigger workflow, not an immediate broker order -

+ {editingSetupId ? 'Edit setup' : 'New setup'} + Saved trigger workflow, not an immediate broker order
- -
+ + +
- +
@@ -556,73 +549,68 @@ export function SimpleView() { {draft.side === 'buy' ? 'Planned quantity' : 'Holding size'} - updateDraft('quantity', e.target.value)} readOnly={draft.side === 'sell' && !!matchingHolding} - className="w-full rounded-2xl border border-black/10 bg-white px-4 py-3 text-zinc-950 outline-none transition placeholder:text-zinc-400 focus:border-[#2f9e62] read-only:bg-zinc-100 read-only:text-zinc-700" + className="read-only:bg-[var(--muted)]" placeholder="10" />
{draft.side === 'buy' && ( -
+

Drop trigger

- +
)} -
+

Profit exit

- +
@@ -631,8 +619,8 @@ export function SimpleView() { {draft.side === 'sell' && (
{matchingHolding ? `Simple holding ready: ${matchingHolding.symbol} · ${matchingHolding.size} shares at ${matchingHolding.entryPrice.toFixed(4)}` @@ -641,44 +629,45 @@ export function SimpleView() { )} {previewText && ( -
+
{previewText}
)} {message && ( -
+
{message}
)} {error && ( -
+
{error}
)} - + -
+ + -
-
-

Saved setups

-

- One dedicated Simple auto profile routes every triggered order -

-
+ + + Saved setups + Review and update armed simple workflows in the same layout style used across the app. + +
{savedSetups.length === 0 && ( -
+
No Simple setups saved yet.
)} @@ -688,63 +677,63 @@ export function SimpleView() { const side = normalizeSetupSide(entry.simple_side); const isEditing = editingSetupId === entryId; return ( -
+
-

{entry.symbol}

+

{entry.symbol}

{side}
-

{describeSavedSetup(entry)}

+

{describeSavedSetup(entry)}

- - +
-
- +
+ {formatSetupStatus(entry.status)} {entry.reference_price ? ( - + Ref {Number(entry.reference_price).toFixed(4)} ) : null} {entry.entry_price ? ( - + Entry {Number(entry.entry_price).toFixed(4)} ) : null} {entry.linked_trade_id ? ( - + Trade linked ) : null} @@ -753,7 +742,8 @@ export function SimpleView() { ); })}
-
+ +
); diff --git a/web/src/views/WatchlistView.tsx b/web/src/views/WatchlistView.tsx index 6882702..d5a1232 100644 --- a/web/src/views/WatchlistView.tsx +++ b/web/src/views/WatchlistView.tsx @@ -1,11 +1,15 @@ import { useAppContext } from '../context/AppContext'; import { EntriesTab } from '../tabs/EntriesTab'; +import { PageHeader } from '../components/ui/page-header'; export function WatchlistView() { const { botState } = useAppContext(); return (
-

Watchlist

+
);