feat(web): add shared light dark theme system

This commit is contained in:
root 2026-05-06 02:56:27 +00:00
parent 9021df19ad
commit 266b367322
22 changed files with 655 additions and 384 deletions

View File

@ -130,7 +130,7 @@ function App() {
return (
<div style={{
display: 'flex', justifyContent: 'center', alignItems: 'center',
height: '100vh', background: '#F3F4F6', color: '#374151',
height: '100vh', background: 'var(--background)', color: 'var(--foreground)',
fontSize: 15, fontFamily: 'Inter, system-ui, sans-serif',
}}>
Loading
@ -160,7 +160,7 @@ function App() {
{hasCriticalEvents && (
<div style={{
position: 'fixed', top: 0, left: 0, right: 0, zIndex: 9999,
background: 'linear-gradient(90deg, #991b1b 0%, #dc2626 50%, #991b1b 100%)',
background: 'linear-gradient(90deg, #7f1d1d 0%, #dc2626 50%, #7f1d1d 100%)',
color: '#fff',
padding: '6px 20px',
textAlign: 'center',

View File

@ -21,7 +21,7 @@ export function SkeletonBlock({
width,
height,
borderRadius: radius,
background: 'linear-gradient(90deg, #F3F4F6 0%, #E5E7EB 45%, #F3F4F6 100%)',
background: 'linear-gradient(90deg, var(--muted) 0%, var(--border) 45%, var(--muted) 100%)',
...style,
}}
/>

View File

@ -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<IndexSnapshot[]>([]);
const navigate = useNavigate();
const inputRef = useRef<HTMLInputElement>(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 (
<header style={{
height: 56,
background: '#ffffff',
borderBottom: '1px solid #E5E7EB',
background: 'var(--header)',
borderBottom: '1px solid var(--border)',
display: 'flex',
alignItems: 'center',
paddingLeft: 20,
paddingRight: 24,
gap: 16,
flexShrink: 0,
backdropFilter: 'blur(18px)',
}}>
{/* Search bar */}
<div style={{ position: 'relative', width: 300 }}>
@ -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 => (
<div key={label} style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div>
<div style={{ fontSize: 11, color: '#6B7280', fontWeight: 500 }}>{label}</div>
<div style={{ fontSize: 13, fontWeight: 700, color: '#D1D5DB' }}></div>
<div style={{ fontSize: 11, color: 'var(--muted-foreground)', fontWeight: 500 }}>{label}</div>
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--muted-foreground)' }}></div>
</div>
</div>
))
@ -131,11 +135,11 @@ export function Header() {
return (
<div key={idx.label} style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div>
<div style={{ fontSize: 11, color: '#6B7280', fontWeight: 500, lineHeight: 1.3 }}>
<div style={{ fontSize: 11, color: 'var(--muted-foreground)', fontWeight: 500, lineHeight: 1.3 }}>
{idx.label}
</div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 4 }}>
<span style={{ fontSize: 12, fontWeight: 600, color: '#111827', lineHeight: 1.3 }}>
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--foreground)', lineHeight: 1.3 }}>
${idx.price.toFixed(2)}
</span>
<span style={{
@ -151,17 +155,27 @@ export function Header() {
})}
</div>
<Button
type="button"
variant="outline"
size="icon"
aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} theme`}
onClick={toggleTheme}
>
{theme === 'dark' ? <Sun size={16} /> : <Moon size={16} />}
</Button>
{/* Live indicator */}
<div style={{ display: 'flex', alignItems: 'center', gap: 5, marginLeft: 8 }}>
<span style={{
width: 7,
height: 7,
borderRadius: '50%',
background: connected ? '#22C55E' : '#EF4444',
background: connected ? '#22C55E' : 'var(--destructive)',
display: 'inline-block',
flexShrink: 0,
}} />
<span style={{ fontSize: 11, color: '#9CA3AF', whiteSpace: 'nowrap' }}>
<span style={{ fontSize: 11, color: 'var(--muted-foreground)', whiteSpace: 'nowrap' }}>
{connected ? 'Live' : 'Reconnecting'}
</span>
</div>

View File

@ -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)',
}} />
</div>
</aside>

View File

@ -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<ThemeContextValue | null>(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<Theme>(() => 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<ThemeContextValue>(() => ({
theme,
setTheme: setThemeState,
toggleTheme: () => setThemeState(prev => (prev === 'dark' ? 'light' : 'dark')),
}), [theme]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) {
throw new Error('useTheme must be used within ThemeProvider');
}
return ctx;
}

View File

@ -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<ButtonVariant, string> = {
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<ButtonSize, string> = {
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<HTMLButtonElement> & {
variant?: ButtonVariant;
size?: ButtonSize;
children: ReactNode;
}) {
return (
<button
className={cn(
'inline-flex items-center justify-center gap-2 rounded-xl font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)] disabled:cursor-not-allowed disabled:opacity-50',
variantClasses[variant],
sizeClasses[size],
className,
)}
{...props}
>
{children}
</button>
);
}

View File

@ -0,0 +1,45 @@
import type { HTMLAttributes, ReactNode } from 'react';
import { cn } from '../../lib/utils';
export function Card({ className, children, ...props }: HTMLAttributes<HTMLDivElement> & { children: ReactNode }) {
return (
<div
className={cn('rounded-3xl border border-[var(--border)] bg-[var(--card)] shadow-[var(--card-shadow)]', className)}
{...props}
>
{children}
</div>
);
}
export function CardHeader({ className, children, ...props }: HTMLAttributes<HTMLDivElement> & { children: ReactNode }) {
return (
<div className={cn('flex items-start justify-between gap-4 p-6 pb-0', className)} {...props}>
{children}
</div>
);
}
export function CardTitle({ className, children, ...props }: HTMLAttributes<HTMLHeadingElement> & { children: ReactNode }) {
return (
<h2 className={cn('text-lg font-bold tracking-tight text-[var(--foreground)]', className)} {...props}>
{children}
</h2>
);
}
export function CardDescription({ className, children, ...props }: HTMLAttributes<HTMLParagraphElement> & { children: ReactNode }) {
return (
<p className={cn('text-sm text-[var(--muted-foreground)]', className)} {...props}>
{children}
</p>
);
}
export function CardContent({ className, children, ...props }: HTMLAttributes<HTMLDivElement> & { children: ReactNode }) {
return (
<div className={cn('p-6', className)} {...props}>
{children}
</div>
);
}

View File

@ -0,0 +1,14 @@
import type { InputHTMLAttributes } from 'react';
import { cn } from '../../lib/utils';
export function Input({ className, ...props }: InputHTMLAttributes<HTMLInputElement>) {
return (
<input
className={cn(
'h-11 w-full rounded-xl border border-[var(--border)] bg-[var(--input)] px-4 text-sm text-[var(--foreground)] outline-none transition placeholder:text-[var(--muted-foreground)] focus:border-[var(--ring)] focus:ring-2 focus:ring-[var(--ring-soft)] disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
{...props}
/>
);
}

View File

@ -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 (
<div className={cn('mb-6 flex items-start justify-between gap-4', className)}>
<div className="min-w-0">
<h1 className="text-2xl font-bold tracking-tight text-[var(--foreground)] md:text-3xl">{title}</h1>
{description ? (
<p className="mt-2 max-w-3xl text-sm text-[var(--muted-foreground)] md:text-[15px]">
{description}
</p>
) : null}
</div>
{action ? <div className="shrink-0">{action}</div> : null}
</div>
);
}

View File

@ -0,0 +1,16 @@
import type { SelectHTMLAttributes } from 'react';
import { cn } from '../../lib/utils';
export function Select({ className, children, ...props }: SelectHTMLAttributes<HTMLSelectElement>) {
return (
<select
className={cn(
'h-11 w-full rounded-xl border border-[var(--border)] bg-[var(--input)] px-4 text-sm text-[var(--foreground)] outline-none transition focus:border-[var(--ring)] focus:ring-2 focus:ring-[var(--ring-soft)] disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
{...props}
>
{children}
</select>
);
}

View File

@ -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;
}

3
web/src/lib/utils.ts Normal file
View File

@ -0,0 +1,3 @@
export function cn(...values: Array<string | false | null | undefined>) {
return values.filter(Boolean).join(' ');
}

View File

@ -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(
<StrictMode>
<ProductAccessibilityGate>
<AuthProvider>
<App />
</AuthProvider>
<ThemeProvider>
<AuthProvider>
<App />
</AuthProvider>
</ThemeProvider>
</ProductAccessibilityGate>
{import.meta.env.DEV && (
<div style={{ position: 'relative', zIndex: 2147483647 }}>

View File

@ -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 (
<div>
<h2 style={{ fontSize: 22, fontWeight: 800, color: '#111827', margin: '0 0 20px' }}>Alerts</h2>
<PageHeader
title="Alerts"
description="Review recent signals, warnings, and execution events across your trading activity."
/>
<AlertFeed alerts={botState.alerts} />
</div>
);

View File

@ -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 (
<div style={{ marginBottom: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 4 }}>
<h1 style={{ fontSize: 28, fontWeight: 800, color: '#111827', margin: 0 }}>
<h1 style={{ fontSize: 28, fontWeight: 800, color: 'var(--foreground)', margin: 0 }}>
{symbol}
</h1>
<span style={{ fontSize: 13, color: '#6B7280', fontWeight: 500, marginTop: 4 }}>
<span style={{ fontSize: 13, color: 'var(--muted-foreground)', fontWeight: 500, marginTop: 4 }}>
{companyName}
</span>
<div style={{ marginLeft: 'auto', display: 'flex', gap: 8 }}>
<button
<Button
type="button"
aria-label={`Open watchlist for ${symbol}`}
onClick={() => navigate('/watchlist')}
style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '5px 12px', borderRadius: 20,
background: '#F0FDF4', border: '1px solid #86EFAC',
color: '#16A34A', fontSize: 12, fontWeight: 600, cursor: 'pointer',
}}>
<Star size={13} fill="#16A34A" /> Watchlist
</button>
<button
variant="outline"
size="sm"
style={{ borderRadius: 999 }}
>
<Star size={13} /> Watchlist
</Button>
<Button
type="button"
aria-label={`Open alerts for ${symbol}`}
onClick={() => navigate('/alerts')}
style={{
width: 32, height: 32, borderRadius: '50%',
border: '1px solid #E5E7EB', background: '#fff',
display: 'flex', alignItems: 'center', justifyContent: 'center',
cursor: 'pointer', color: '#6B7280',
}}>
variant="outline"
size="icon"
style={{ borderRadius: '50%' }}
>
<Bell size={15} />
</button>
</Button>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 10 }}>
<span style={{ fontSize: 32, fontWeight: 800, color: '#111827', letterSpacing: '-1px' }}>
<span style={{ fontSize: 32, fontWeight: 800, color: 'var(--foreground)', letterSpacing: '-1px' }}>
{price > 0 ? price.toFixed(2) : '—'}
</span>
{price > 0 && (
@ -276,7 +273,7 @@ export function TickerHeader({
)}
</div>
<div style={{ fontSize: 11, color: '#9CA3AF', marginTop: 3 }}>
<div style={{ fontSize: 11, color: 'var(--muted-foreground)', marginTop: 3 }}>
{formatAsOfTimestamp(latestBarTimestamp ?? null)} ET · {exchange}
</div>
</div>
@ -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 (
<div style={{
background: '#fff',
background: 'var(--card)',
borderRadius: 12,
border: '1px solid #E5E7EB',
border: '1px solid var(--border)',
padding: '16px 20px 12px',
marginBottom: 20,
}}>
@ -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({
</button>
))}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, color: '#6B7280', fontSize: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, color: 'var(--muted-foreground)', fontSize: 12 }}>
<div style={{ display: 'flex', gap: 6 }}>
{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({
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 12 }}>
<div style={{ fontSize: 11, color: '#9CA3AF' }}>
<div style={{ fontSize: 11, color: 'var(--muted-foreground)' }}>
Indicators: {enabledCount > 0 ? `${enabledCount} active` : 'none'}
</div>
<div style={{ fontSize: 11, color: '#9CA3AF' }}>
<div style={{ fontSize: 11, color: 'var(--muted-foreground)' }}>
RSI 14 · MACD 12/26/9 · Bollinger 20/2
</div>
</div>
@ -441,9 +438,9 @@ function StockChart({
{loading ? (
<div style={{
height: 220, display: 'flex', flexDirection: 'column',
alignItems: 'center', justifyContent: 'center', gap: 10, color: '#9CA3AF',
alignItems: 'center', justifyContent: 'center', gap: 10, color: 'var(--muted-foreground)',
}}>
<Loader2 size={28} color="#93C5FD" style={{ animation: 'spin 1s linear infinite' }} />
<Loader2 size={28} color="var(--primary)" style={{ animation: 'spin 1s linear infinite' }} />
<span style={{ fontSize: 13 }}>Loading chart</span>
</div>
) : error ? (
@ -461,11 +458,11 @@ function StockChart({
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
color: '#9CA3AF',
color: 'var(--muted-foreground)',
fontSize: 13,
gap: 8,
}}>
<BarChart2 size={32} color="#D1D5DB" />
<BarChart2 size={32} color="var(--muted-foreground)" />
<span>No price data available for {symbol}</span>
</div>
) : (
@ -478,17 +475,17 @@ function StockChart({
<stop offset="95%" stopColor={lineColor} stopOpacity={0.01} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#F3F4F6" vertical={false} />
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" vertical={false} />
<XAxis
dataKey="label"
tick={{ fontSize: 10, fill: '#9CA3AF' }}
tick={{ fontSize: 10, fill: 'var(--muted-foreground)' }}
tickLine={false}
axisLine={false}
interval="preserveStartEnd"
/>
<YAxis
domain={[minY - pad, maxY + pad]}
tick={{ fontSize: 10, fill: '#9CA3AF' }}
tick={{ fontSize: 10, fill: 'var(--muted-foreground)' }}
tickLine={false}
axisLine={false}
width={55}
@ -496,11 +493,11 @@ function StockChart({
/>
<Tooltip
contentStyle={{
background: '#fff',
border: '1px solid #E5E7EB',
background: 'var(--card-elevated)',
border: '1px solid var(--border)',
borderRadius: 8,
fontSize: 12,
boxShadow: '0 4px 12px rgba(0,0,0,0.08)',
boxShadow: 'var(--card-shadow)',
}}
formatter={(val: unknown, name: unknown) => {
const labels: Record<string, string> = {
@ -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({
</ResponsiveContainer>
{enabledIndicators.rsi && (
<div style={{ marginTop: 12, paddingTop: 10, borderTop: '1px solid #F3F4F6' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4, fontSize: 11, color: '#6B7280', fontWeight: 700 }}>
<div style={{ marginTop: 12, paddingTop: 10, borderTop: '1px solid var(--border)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4, fontSize: 11, color: 'var(--muted-foreground)', fontWeight: 700 }}>
<span>RSI (14)</span>
<span style={{ color: '#9CA3AF', fontWeight: 500 }}>70 overbought · 30 oversold</span>
<span style={{ color: 'var(--muted-foreground)', fontWeight: 500 }}>70 overbought · 30 oversold</span>
</div>
<ResponsiveContainer width="100%" height={86}>
<LineChart data={chartData} margin={{ top: 4, right: 4, bottom: 0, left: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#F3F4F6" vertical={false} />
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" vertical={false} />
<XAxis dataKey="label" hide />
<YAxis domain={[0, 100]} tick={{ fontSize: 10, fill: '#9CA3AF' }} tickLine={false} axisLine={false} width={55} />
<YAxis domain={[0, 100]} tick={{ fontSize: 10, fill: 'var(--muted-foreground)' }} tickLine={false} axisLine={false} width={55} />
<Tooltip
contentStyle={{ background: '#fff', border: '1px solid #E5E7EB', borderRadius: 8, fontSize: 12 }}
contentStyle={{ background: 'var(--card-elevated)', border: '1px solid var(--border)', borderRadius: 8, fontSize: 12 }}
formatter={(val: unknown) => [Number(val).toFixed(1), 'RSI']}
labelStyle={{ color: '#6B7280', fontSize: 11 }}
labelStyle={{ color: 'var(--muted-foreground)', fontSize: 11 }}
/>
<ReferenceLine y={70} stroke="#FCA5A5" strokeDasharray="3 3" />
<ReferenceLine y={30} stroke="#93C5FD" strokeDasharray="3 3" />
@ -558,23 +555,23 @@ function StockChart({
)}
{enabledIndicators.macd && (
<div style={{ marginTop: 12, paddingTop: 10, borderTop: '1px solid #F3F4F6' }}>
<div style={{ marginBottom: 4, fontSize: 11, color: '#6B7280', fontWeight: 700 }}>
<div style={{ marginTop: 12, paddingTop: 10, borderTop: '1px solid var(--border)' }}>
<div style={{ marginBottom: 4, fontSize: 11, color: 'var(--muted-foreground)', fontWeight: 700 }}>
MACD (12, 26, 9)
</div>
<ResponsiveContainer width="100%" height={106}>
<ComposedChart data={chartData} margin={{ top: 4, right: 4, bottom: 0, left: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#F3F4F6" vertical={false} />
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" vertical={false} />
<XAxis dataKey="label" hide />
<YAxis
domain={[-macdMaxAbs * 1.2, macdMaxAbs * 1.2]}
tick={{ fontSize: 10, fill: '#9CA3AF' }}
tick={{ fontSize: 10, fill: 'var(--muted-foreground)' }}
tickLine={false}
axisLine={false}
width={55}
/>
<Tooltip
contentStyle={{ background: '#fff', border: '1px solid #E5E7EB', borderRadius: 8, fontSize: 12 }}
contentStyle={{ background: 'var(--card-elevated)', border: '1px solid var(--border)', borderRadius: 8, fontSize: 12 }}
formatter={(val: unknown, name: unknown) => {
const labels: Record<string, string> = {
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 }}
/>
<ReferenceLine y={0} stroke="#CBD5E1" />
<Bar dataKey="macdHistogram" fill="#BFDBFE" radius={[2, 2, 0, 0]} />
@ -623,13 +620,13 @@ function QuickStats({ symbol, bars }: { symbol: string; bars: OHLCVBar[] }) {
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))', gap: 12, marginBottom: 20 }}>
{stats.map(s => (
<div key={s.label} style={{
background: '#fff',
background: 'var(--card)',
borderRadius: 10,
border: '1px solid #E5E7EB',
border: '1px solid var(--border)',
padding: '12px 14px',
}}>
<div style={{ fontSize: 11, color: '#9CA3AF', fontWeight: 500, marginBottom: 4 }}>{s.label}</div>
<div style={{ fontSize: 16, fontWeight: 700, color: '#111827' }}>{s.value}</div>
<div style={{ fontSize: 11, color: 'var(--muted-foreground)', fontWeight: 500, marginBottom: 4 }}>{s.label}</div>
<div style={{ fontSize: 16, fontWeight: 700, color: 'var(--foreground)' }}>{s.value}</div>
</div>
))}
</div>
@ -693,8 +690,8 @@ function ResearchCards({
return (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 16 }}>
{/* Company Profile */}
<div style={{ background: '#fff', borderRadius: 12, border: '1px solid #E5E7EB', padding: 16 }}>
<div style={{ fontSize: 13, fontWeight: 700, color: '#111827', marginBottom: 10 }}>
<div style={{ background: 'var(--card)', borderRadius: 12, border: '1px solid var(--border)', padding: 16 }}>
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--foreground)', marginBottom: 10 }}>
📋 Company
</div>
{profileLoading ? (
@ -706,40 +703,40 @@ function ResearchCards({
</div>
) : profile ? (
<>
<div style={{ fontSize: 12, color: '#374151', marginBottom: 6, lineHeight: 1.5 }}>
<div style={{ fontSize: 12, color: 'var(--foreground)', marginBottom: 6, lineHeight: 1.5 }}>
<strong>{profile.companyName ?? symbol}</strong>
{profile.sector && <> · {profile.sector}</>}
{profile.industry && <> · {profile.industry}</>}
</div>
<div style={{ fontSize: 11, color: '#6B7280', lineHeight: 1.6, marginBottom: 8,
<div style={{ fontSize: 11, color: 'var(--muted-foreground)', lineHeight: 1.6, marginBottom: 8,
display: '-webkit-box', WebkitLineClamp: 4, WebkitBoxOrient: 'vertical', overflow: 'hidden',
}}>
{profile.description ?? ''}
</div>
{profile.website && (
<a href={profile.website} target="_blank" rel="noreferrer"
style={{ fontSize: 11, color: '#2563EB', textDecoration: 'none' }}>
style={{ fontSize: 11, color: 'var(--primary)', textDecoration: 'none' }}>
{profile.website}
</a>
)}
</>
) : (
<div style={{ fontSize: 12, color: '#9CA3AF' }}>No profile data</div>
<div style={{ fontSize: 12, color: 'var(--muted-foreground)' }}>No profile data</div>
)}
</div>
{/* Financials */}
<div style={{ background: '#fff', borderRadius: 12, border: '1px solid #E5E7EB', padding: 16 }}>
<div style={{ fontSize: 13, fontWeight: 700, color: '#111827', marginBottom: 10 }}>
<div style={{ background: 'var(--card)', borderRadius: 12, border: '1px solid var(--border)', padding: 16 }}>
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--foreground)', marginBottom: 10 }}>
📊 Financials
</div>
{financialRows.map(([label, val]) => (
<div key={label} style={{
display: 'flex', justifyContent: 'space-between',
padding: '5px 0', borderBottom: '1px solid #F9FAFB',
padding: '5px 0', borderBottom: '1px solid var(--border)',
}}>
<span style={{ fontSize: 12, color: '#6B7280' }}>{label}</span>
<span style={{ fontSize: 12, fontWeight: 600, color: '#111827' }}>
<span style={{ fontSize: 12, color: 'var(--muted-foreground)' }}>{label}</span>
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--foreground)' }}>
{loading || profileLoading ? <ValueSkeleton width={label.length > 10 ? 64 : 46} /> : val}
</span>
</div>
@ -747,8 +744,8 @@ function ResearchCards({
</div>
{/* Events / Earnings */}
<div style={{ background: '#fff', borderRadius: 12, border: '1px solid #E5E7EB', padding: 16 }}>
<div style={{ fontSize: 13, fontWeight: 700, color: '#111827', marginBottom: 10 }}>
<div style={{ background: 'var(--card)', borderRadius: 12, border: '1px solid var(--border)', padding: 16 }}>
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--foreground)', marginBottom: 10 }}>
📅 Events
</div>
{[
@ -759,19 +756,19 @@ function ResearchCards({
].map(([label, val]) => (
<div key={label} style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '5px 0', borderBottom: '1px solid #F9FAFB',
padding: '5px 0', borderBottom: '1px solid var(--border)',
}}>
<span style={{ fontSize: 12, color: '#6B7280' }}>{label}</span>
<span style={{ fontSize: 12, fontWeight: 600, color: '#111827' }}>
<span style={{ fontSize: 12, color: 'var(--muted-foreground)' }}>{label}</span>
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--foreground)' }}>
{val === '…' ? <ValueSkeleton width={label === 'Next Earnings' ? 82 : 52} /> : val}
</span>
</div>
))}
{!loading && earnings.length > 0 && (
<div style={{ marginTop: 10 }}>
<div style={{ fontSize: 11, fontWeight: 600, color: '#374151', marginBottom: 4 }}>Past Earnings</div>
<div style={{ fontSize: 11, fontWeight: 600, color: 'var(--foreground)', marginBottom: 4 }}>Past Earnings</div>
{earnings.slice(0,3).map((e, i) => (
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11, color: '#6B7280', padding: '2px 0' }}>
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11, color: 'var(--muted-foreground)', padding: '2px 0' }}>
<span>{fmtDate(e.date)}</span>
<span style={{ color: e.eps >= (e.epsEstimated ?? e.eps) ? '#16A34A' : '#DC2626' }}>
EPS {e.eps != null ? `$${e.eps.toFixed(2)}` : '—'}
@ -799,17 +796,17 @@ function EmptyState({
<div style={{
display: 'flex', flexDirection: 'column', alignItems: 'center',
justifyContent: 'center', height: '60vh', gap: 16,
color: '#9CA3AF',
color: 'var(--muted-foreground)',
}}>
<div style={{ fontSize: 56 }}>📈</div>
<div style={{ fontSize: 20, fontWeight: 700, color: '#374151' }}>
<div style={{ fontSize: 20, fontWeight: 700, color: 'var(--foreground)' }}>
Search an asset to get started
</div>
<div style={{ fontSize: 14, textAlign: 'center', maxWidth: 360 }}>
Type a ticker symbol, crypto pair, or company name in the search bar above to view charts, financials, and news.
</div>
{cryptoMode && (
<div style={{ fontSize: 12, color: '#6B7280', fontWeight: 600 }}>
<div style={{ fontSize: 12, color: 'var(--muted-foreground)', fontWeight: 600 }}>
Suggested from your crypto bot configuration
</div>
)}
@ -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,

View File

@ -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 (
<div>
<h2 style={{ fontSize: 22, fontWeight: 800, color: '#111827', margin: '0 0 20px' }}>Markets</h2>
<PageHeader
title="Markets"
description="Scan live opportunities and reusable setups from the current market without leaving the workspace."
/>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20, marginBottom: 24 }}>
<TopVolatile botState={botState} />
<AISetups botState={botState} />

View File

@ -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 (
<div>
<h2 style={{ fontSize: 22, fontWeight: 800, color: '#111827', margin: '0 0 20px' }}>Portfolio</h2>
<PageHeader
title="Portfolio"
description="Review live positions, order activity, and completed trades in one consistent operational view."
/>
{/* Sub-tabs */}
<div style={{
display: 'flex', gap: 4, marginBottom: 20,
borderBottom: '1px solid #E5E7EB', paddingBottom: 0,
}}>
<div className="tab-strip" style={{ marginBottom: 20 }}>
{TABS.map(t => (
<button
key={t}
onClick={() => setTab(t)}
style={{
padding: '8px 16px',
border: 'none',
borderBottom: tab === t ? '2px solid #2563EB' : '2px solid transparent',
background: 'transparent',
color: tab === t ? '#2563EB' : '#6B7280',
fontSize: 13,
fontWeight: tab === t ? 700 : 500,
cursor: 'pointer',
marginBottom: -1,
transition: 'all 0.15s',
}}
className="tab-button"
data-active={tab === t}
>
{t}
</button>

View File

@ -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 (
<button
onClick={onClick}
style={{
padding: '8px 16px',
border: 'none',
borderBottom: active ? '2px solid #2563EB' : '2px solid transparent',
background: 'transparent',
color: active ? '#2563EB' : '#6B7280',
fontSize: 13,
fontWeight: active ? 700 : 500,
cursor: 'pointer',
marginBottom: -1,
transition: 'all 0.15s',
fontFamily: 'inherit',
}}
className="tab-button"
data-active={active}
>
{label}
</button>
@ -93,12 +83,12 @@ export function ResearchView() {
return (
<div>
<h2 style={{ fontSize: 22, fontWeight: 800, color: '#111827', margin: '0 0 20px' }}>Research</h2>
<PageHeader
title="Research"
description="Design, test, and refine strategies before they go anywhere near live execution."
/>
<div style={{
display: 'flex', gap: 4, marginBottom: 20,
borderBottom: '1px solid #E5E7EB',
}}>
<div className="tab-strip">
{tabs.map(t => (
<SubTab key={t} label={t} active={tab === t} onClick={() => setTab(t)} />
))}

View File

@ -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 }) => (
<span style={{ color: sortKey === k ? '#2563EB' : '#D1D5DB', marginLeft: 3, fontSize: 10 }}>
<span style={{ color: sortKey === k ? 'var(--primary)' : 'var(--muted-foreground)', marginLeft: 3, fontSize: 10 }}>
{sortKey === k ? (sortAsc ? '▲' : '▼') : '⇅'}
</span>
);
@ -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 (
<div>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 20 }}>
<h2 style={{ fontSize: 22, fontWeight: 800, color: '#111827', margin: 0 }}>Stock Screener</h2>
<div style={{ flex: 1 }} />
<button
onClick={fetchResults}
disabled={loading}
style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '7px 12px', border: '1px solid #E5E7EB', borderRadius: 8,
background: '#fff', color: '#374151', fontSize: 12, fontWeight: 600,
cursor: loading ? 'wait' : 'pointer',
}}
>
<RefreshCw size={13} style={{ animation: loading ? 'spin 1s linear infinite' : 'none' }} />
Refresh
</button>
</div>
<PageHeader
title="Stock Screener"
description="Filter liquid names by sector and market cap, then jump directly into charting and research."
action={
<Button onClick={fetchResults} disabled={loading} variant="outline" size="sm">
<RefreshCw size={13} style={{ animation: loading ? 'spin 1s linear infinite' : 'none' }} />
Refresh
</Button>
}
/>
{/* Filters */}
<div style={{ display: 'flex', gap: 12, marginBottom: 16, flexWrap: 'wrap', alignItems: 'center' }}>
{/* Search */}
<div style={{ position: 'relative', minWidth: 200, maxWidth: 280 }}>
<Search size={14} style={{
position: 'absolute', left: 10, top: '50%',
transform: 'translateY(-50%)', color: '#9CA3AF',
}} />
<input
type="text"
placeholder="Filter by name or ticker…"
value={query}
onChange={e => 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',
}}
/>
</div>
<Card style={{ marginBottom: 16 }}>
<CardContent style={{ padding: 16 }}>
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
<div style={{ position: 'relative', minWidth: 200, maxWidth: 280 }}>
<Search size={14} style={{
position: 'absolute', left: 10, top: '50%',
transform: 'translateY(-50%)', color: 'var(--muted-foreground)',
}} />
<Input
type="text"
placeholder="Filter by name or ticker…"
value={query}
onChange={e => setQuery(e.target.value)}
style={{ paddingLeft: 32 }}
/>
</div>
{/* Market cap */}
<select
value={capIdx}
onChange={e => setCapIdx(Number(e.target.value))}
style={{
padding: '8px 12px', border: '1px solid #E5E7EB', borderRadius: 8,
fontSize: 12, background: '#fff', color: '#374151', fontFamily: 'inherit',
cursor: 'pointer',
}}
>
{CAP_OPTIONS.map((c, i) => (
<option key={c.label} value={i}>{c.label}</option>
))}
</select>
<Select
value={String(capIdx)}
onChange={e => setCapIdx(Number(e.target.value))}
style={{ width: 180 }}
>
{CAP_OPTIONS.map((c, i) => (
<option key={c.label} value={i}>{c.label}</option>
))}
</Select>
{/* Sector pills */}
<div style={{ display: 'flex', gap: 5, flexWrap: 'wrap', alignItems: 'center' }}>
<SlidersHorizontal size={13} color="#6B7280" />
<div style={{ display: 'flex', gap: 5, flexWrap: 'wrap', alignItems: 'center' }}>
<SlidersHorizontal size={13} color="var(--muted-foreground)" />
{SECTORS.slice(0, 6).map(s => (
<button
key={s}
@ -228,59 +214,59 @@ export function ScreenerView() {
padding: '5px 10px', borderRadius: 20,
border: '1px solid', fontSize: 11, fontWeight: 600,
cursor: 'pointer',
borderColor: sector === s ? '#2563EB' : '#E5E7EB',
background: sector === s ? '#EFF6FF' : '#fff',
color: sector === s ? '#2563EB' : '#6B7280',
borderColor: sector === s ? 'var(--primary)' : 'var(--border)',
background: sector === s ? 'var(--accent-soft)' : 'var(--card)',
color: sector === s ? 'var(--primary)' : 'var(--muted-foreground)',
fontFamily: 'inherit',
}}
>
{s}
</button>
))}
{/* Sector dropdown for rest */}
<select
aria-label="More sectors"
value={SECTORS.indexOf(sector) >= 6 ? sector : ''}
onChange={e => e.target.value && setSector(e.target.value)}
style={{
padding: '5px 8px',
border: `1px solid ${moreSectorSelected ? '#2563EB' : '#E5E7EB'}`,
borderRadius: 20,
fontSize: 11,
background: moreSectorSelected ? '#EFF6FF' : '#fff',
color: moreSectorSelected ? '#2563EB' : '#6B7280',
fontWeight: moreSectorSelected ? 700 : 500,
fontFamily: 'inherit',
cursor: 'pointer',
}}
>
<option value="">More sectors</option>
{SECTORS.slice(6).map(s => (
<option key={s} value={s}>{s}</option>
))}
</select>
</div>
</div>
<Select
aria-label="More sectors"
value={SECTORS.indexOf(sector) >= 6 ? sector : ''}
onChange={e => e.target.value && setSector(e.target.value)}
style={{
width: 140,
borderRadius: 999,
borderColor: moreSectorSelected ? 'var(--primary)' : 'var(--border)',
background: moreSectorSelected ? 'var(--accent-soft)' : 'var(--card)',
color: moreSectorSelected ? 'var(--primary)' : 'var(--muted-foreground)',
fontWeight: moreSectorSelected ? 700 : 500,
}}
>
<option value="">More sectors</option>
{SECTORS.slice(6).map(s => (
<option key={s} value={s}>{s}</option>
))}
</Select>
</div>
</div>
</CardContent>
</Card>
{/* Error */}
{error && !loading && (
<div style={{
padding: 12, background: '#FEF2F2', border: '1px solid #FCA5A5',
borderRadius: 8, fontSize: 13, color: '#DC2626', marginBottom: 12,
padding: 12,
background: 'color-mix(in oklab, var(--destructive) 10%, var(--card) 90%)',
border: '1px solid color-mix(in oklab, var(--destructive) 45%, var(--border) 55%)',
borderRadius: 8,
fontSize: 13,
color: 'var(--destructive)',
marginBottom: 12,
}}>
{error}
</div>
)}
{/* Results table */}
<div style={{ background: '#fff', borderRadius: 12, border: '1px solid #E5E7EB', overflowX: 'auto', overflowY: 'hidden' }}>
{/* Header */}
<div style={{ background: 'var(--card)', borderRadius: 12, border: '1px solid var(--border)', overflowX: 'auto', overflowY: 'hidden' }}>
<div style={{
display: 'grid',
gridTemplateColumns: '100px 1fr 90px 90px 110px 80px 110px',
padding: '10px 16px',
borderBottom: '1px solid #F3F4F6',
background: '#F9FAFB',
borderBottom: '1px solid var(--border)',
background: 'var(--muted)',
}}>
{([
['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() {
))}
</div>
{/* Loading */}
{loading && <ScreenerSkeletonRows />}
{/* Rows */}
{!loading && filtered.map((row, i) => (
<div
key={row.symbol}
@ -317,16 +301,17 @@ export function ScreenerView() {
display: 'grid',
gridTemplateColumns: '100px 1fr 90px 90px 110px 80px 110px',
padding: '11px 16px',
borderBottom: i < filtered.length - 1 ? '1px solid #F9FAFB' : 'none',
borderBottom: i < filtered.length - 1 ? '1px solid var(--border)' : 'none',
cursor: 'pointer',
alignItems: 'center',
transition: 'background 0.15s ease',
}}
onMouseEnter={e => (e.currentTarget.style.background = '#F9FAFB')}
onMouseEnter={e => (e.currentTarget.style.background = 'var(--muted)')}
onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}
>
<span style={{ fontSize: 13, fontWeight: 700, color: '#2563EB' }}>{row.symbol}</span>
<span style={{ fontSize: 12, color: '#374151' }}>{row.companyName}</span>
<span style={{ fontSize: 13, fontWeight: 600, color: '#111827' }}>
<span style={{ fontSize: 13, fontWeight: 700, color: 'var(--primary)' }}>{row.symbol}</span>
<span style={{ fontSize: 12, color: 'var(--foreground)' }}>{row.companyName}</span>
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--foreground)' }}>
{row.price != null ? `$${row.price.toFixed(2)}` : '—'}
</span>
<span style={{
@ -335,27 +320,27 @@ export function ScreenerView() {
}}>
{row.changesPercentage >= 0 ? '+' : ''}{row.changesPercentage?.toFixed(2)}%
</span>
<span style={{ fontSize: 12, color: '#374151' }}>
<span style={{ fontSize: 12, color: 'var(--foreground)' }}>
{row.marketCap ? fmtCap(row.marketCap) : '—'}
</span>
<span style={{ fontSize: 12, color: '#374151' }}>
<span style={{ fontSize: 12, color: 'var(--foreground)' }}>
{row.pe != null && row.pe > 0 ? row.pe.toFixed(1) : '—'}
</span>
<span style={{ fontSize: 12, color: '#374151' }}>
<span style={{ fontSize: 12, color: 'var(--foreground)' }}>
{row.volume ? fmtCap(row.volume).replace('$', '') : '—'}
</span>
</div>
))}
{!loading && filtered.length === 0 && !error && (
<div style={{ padding: 32, textAlign: 'center', color: '#9CA3AF', fontSize: 13 }}>
<div style={{ padding: 32, textAlign: 'center', color: 'var(--muted-foreground)', fontSize: 13 }}>
No results match your filters
</div>
)}
</div>
{!loading && filtered.length > 0 && (
<div style={{ marginTop: 8, fontSize: 11, color: '#9CA3AF' }}>
<div style={{ marginTop: 8, fontSize: 11, color: 'var(--muted-foreground)' }}>
{filtered.length} companies · Click any row to view chart & research
</div>
)}

View File

@ -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<SettingsSection>('Account');
return (
<div>
<h2 style={{ fontSize: 22, fontWeight: 800, color: '#111827', margin: '0 0 20px' }}>Settings</h2>
<div>
<PageHeader
title="Settings"
description="Manage your account, credentials, and runtime configuration from one place."
/>
<div style={{
display: 'flex', gap: 4, marginBottom: 24,
borderBottom: '1px solid #E5E7EB',
}}>
<div className="tab-strip" style={{ marginBottom: 24 }}>
{sections.map(s => (
<button
key={s}
onClick={() => setSection(s)}
style={{
padding: '8px 16px',
border: 'none',
borderBottom: section === s ? '2px solid #2563EB' : '2px solid transparent',
background: 'transparent',
color: section === s ? '#2563EB' : '#6B7280',
fontSize: 13,
fontWeight: section === s ? 700 : 500,
cursor: 'pointer',
marginBottom: -1,
transition: 'all 0.15s',
}}
className="tab-button"
data-active={section === s}
>
{s}
</button>
@ -48,12 +39,12 @@ export function SettingsView() {
<div
className="settings-legacy-surface"
style={{
background: 'linear-gradient(145deg, #101116 0%, #171922 100%)',
border: '1px solid rgba(255, 255, 255, 0.08)',
background: 'var(--card)',
border: '1px solid var(--border)',
borderRadius: 24,
padding: 24,
boxShadow: '0 24px 70px rgba(15, 23, 42, 0.22)',
color: '#F9FAFB',
boxShadow: 'var(--card-shadow)',
color: 'var(--foreground)',
}}
>
{section === 'Account' && <SettingsTab botState={botState} />}

View File

@ -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 (
<div className="space-y-8">
<header className="space-y-3">
<div className="flex items-center gap-3">
<div className="h-12 w-12 rounded-2xl bg-[#0f2a1f] text-[#71f6a8] flex items-center justify-center">
<Target size={24} />
</div>
<div>
<h1 className="text-3xl font-black tracking-tight text-white uppercase">Simple</h1>
<p className="text-sm text-zinc-400 max-w-3xl">
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.
</p>
</div>
</div>
</header>
<PageHeader
title="Simple"
description="Create saved dip-buy and profit-exit setups with the same workspace patterns used across Research, Markets, and Settings."
/>
<div className="grid gap-8 xl:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)]">
<section className="rounded-[2rem] border border-black/10 bg-zinc-50 p-6 shadow-[0_30px_80px_rgba(0,0,0,0.16)]">
<div className="mb-6 flex items-center justify-between gap-4">
<Card>
<CardHeader>
<div>
<h2 className="text-lg font-black uppercase tracking-tight text-zinc-950">
{editingSetupId ? 'Edit setup' : 'New setup'}
</h2>
<p className="text-xs uppercase tracking-[0.24em] text-zinc-600">
Saved trigger workflow, not an immediate broker order
</p>
<CardTitle className="uppercase">{editingSetupId ? 'Edit setup' : 'New setup'}</CardTitle>
<CardDescription>Saved trigger workflow, not an immediate broker order</CardDescription>
</div>
<button
<Button
type="button"
onClick={() => {
setEditingSetupId(null);
@ -494,17 +485,20 @@ export function SimpleView() {
setMessage(null);
setError(null);
}}
className="rounded-full border border-black/10 bg-white px-4 py-2 text-xs font-bold uppercase tracking-[0.2em] text-zinc-700 hover:border-black/20 hover:text-zinc-950"
variant="outline"
size="sm"
className="uppercase tracking-[0.2em]"
>
Reset
</button>
</div>
</Button>
</CardHeader>
<CardContent>
<form className="space-y-6" onSubmit={handleSubmit}>
<div className="grid gap-4 md:grid-cols-2">
<label className="space-y-2">
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">Symbol</span>
<input
<Input
value={draft.symbol}
onChange={(e) => setDraft((prev) => ({
...prev,
@ -512,43 +506,42 @@ export function SimpleView() {
currentMarketPrice: '',
}))}
placeholder="AAPL"
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]"
/>
</label>
<label className="space-y-2">
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">Setup type</span>
<select
<Select
value={draft.side}
onChange={(e) => updateDraft('side', e.target.value as SimpleSide)}
className="w-full rounded-2xl border border-black/10 bg-white px-4 py-3 text-zinc-950 outline-none transition focus:border-[#2f9e62]"
>
<option value="buy">Buy the dip + profit exit</option>
<option value="sell">Sell existing Simple holding at profit</option>
</select>
</Select>
</label>
</div>
<div className="grid gap-4 md:grid-cols-[minmax(0,1fr)_auto]">
<label className="space-y-2">
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">Market price (auto-fetched)</span>
<input
<Input
value={draft.currentMarketPrice}
readOnly
className="w-full rounded-2xl border border-black/10 bg-zinc-100 px-4 py-3 text-zinc-800 outline-none"
className="bg-[var(--muted)] text-[var(--foreground)]"
/>
</label>
<button
<Button
type="button"
onClick={() => void handleLoadMarketPrice()}
className="self-end rounded-2xl border border-black/10 bg-white px-4 py-3 text-xs font-black uppercase tracking-[0.2em] text-zinc-700 hover:border-black/20 hover:text-zinc-950 disabled:opacity-50"
className="self-end uppercase tracking-[0.2em]"
variant="outline"
disabled={loadingPrice || !normalizedSymbol}
>
<span className="inline-flex items-center gap-2">
<RefreshCw size={14} className={loadingPrice ? 'animate-spin' : ''} />
Refresh
</span>
</button>
</Button>
</div>
<div className="grid gap-4 md:grid-cols-2">
@ -556,73 +549,68 @@ export function SimpleView() {
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-500">
{draft.side === 'buy' ? 'Planned quantity' : 'Holding size'}
</span>
<input
<Input
value={draft.side === 'sell' && matchingHolding ? String(matchingHolding.size) : draft.quantity}
onChange={(e) => 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"
/>
</label>
<label className="space-y-2">
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">Notes</span>
<input
<Input
value={draft.notes}
onChange={(e) => updateDraft('notes', e.target.value)}
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]"
placeholder="Optional context"
/>
</label>
</div>
{draft.side === 'buy' && (
<div className="grid gap-4 rounded-[1.75rem] border border-black/10 bg-white p-5 md:grid-cols-[0.55fr_0.45fr]">
<div className="grid gap-4 rounded-[1.5rem] border border-[var(--border)] bg-[var(--card-elevated)] p-5 md:grid-cols-[0.55fr_0.45fr]">
<div className="space-y-3">
<p className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">Drop trigger</p>
<select
<Select
value={draft.dropMode}
onChange={(e) => updateDraft('dropMode', e.target.value as TriggerMode)}
className="w-full rounded-2xl border border-black/10 bg-white px-4 py-3 text-zinc-950 outline-none transition focus:border-[#2f9e62]"
>
<option value="dollar">Dollar drop from current market</option>
<option value="percent">Percent drop from current market</option>
</select>
</Select>
</div>
<label className="space-y-3">
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">
{draft.dropMode === 'dollar' ? 'Drop amount ($)' : 'Drop amount (%)'}
</span>
<input
<Input
value={draft.dropValue}
onChange={(e) => updateDraft('dropValue', e.target.value)}
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]"
placeholder={draft.dropMode === 'dollar' ? '5.00' : '8'}
/>
</label>
</div>
)}
<div className="grid gap-4 rounded-[1.75rem] border border-black/10 bg-white p-5 md:grid-cols-[0.55fr_0.45fr]">
<div className="grid gap-4 rounded-[1.5rem] border border-[var(--border)] bg-[var(--card-elevated)] p-5 md:grid-cols-[0.55fr_0.45fr]">
<div className="space-y-3">
<p className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">Profit exit</p>
<select
<Select
value={draft.profitMode}
onChange={(e) => updateDraft('profitMode', e.target.value as TriggerMode)}
className="w-full rounded-2xl border border-black/10 bg-white px-4 py-3 text-zinc-950 outline-none transition focus:border-[#2f9e62]"
>
<option value="dollar">Dollar gain from purchase</option>
<option value="percent">Percent gain from purchase</option>
</select>
</Select>
</div>
<label className="space-y-3">
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">
{draft.profitMode === 'dollar' ? 'Profit target ($)' : 'Profit target (%)'}
</span>
<input
<Input
value={draft.profitValue}
onChange={(e) => updateDraft('profitValue', e.target.value)}
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]"
placeholder={draft.profitMode === 'dollar' ? '7.50' : '10'}
/>
</label>
@ -631,8 +619,8 @@ export function SimpleView() {
{draft.side === 'sell' && (
<div className={`rounded-[1.5rem] border px-4 py-4 text-sm ${
matchingHolding
? 'border-[#71f6a8]/30 bg-[#0f2a1f] text-[#a4ffca]'
: 'border-[#ef4444]/30 bg-[#261214] text-[#fca5a5]'
? 'border-emerald-500/20 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300'
: 'border-red-500/20 bg-red-500/10 text-red-700 dark:text-red-300'
}`}>
{matchingHolding
? `Simple holding ready: ${matchingHolding.symbol} · ${matchingHolding.size} shares at ${matchingHolding.entryPrice.toFixed(4)}`
@ -641,44 +629,45 @@ export function SimpleView() {
)}
{previewText && (
<div className="rounded-[1.75rem] border border-[#71f6a8]/20 bg-[#eefbf3] px-5 py-4 text-sm text-zinc-900">
<div className="rounded-[1.5rem] border border-[var(--border)] bg-[var(--accent-soft)] px-5 py-4 text-sm text-[var(--foreground)]">
{previewText}
</div>
)}
{message && (
<div className="rounded-2xl border border-[#71f6a8]/20 bg-[#0d1d14] px-4 py-3 text-sm text-[#9ff6c3]">
<div className="rounded-2xl border border-emerald-500/20 bg-emerald-500/10 px-4 py-3 text-sm text-emerald-700 dark:text-emerald-300">
{message}
</div>
)}
{error && (
<div className="rounded-2xl border border-[#ef4444]/20 bg-[#221112] px-4 py-3 text-sm text-[#f5b1b1]">
<div className="rounded-2xl border border-red-500/20 bg-red-500/10 px-4 py-3 text-sm text-red-700 dark:text-red-300">
{error}
</div>
)}
<button
<Button
type="submit"
disabled={saveButtonDisabled}
className="w-full rounded-[1.5rem] bg-[#71f6a8] px-5 py-4 text-sm font-black uppercase tracking-[0.24em] text-black transition hover:brightness-105 disabled:cursor-not-allowed disabled:opacity-55"
className="w-full uppercase tracking-[0.24em]"
size="lg"
>
{submitting ? 'Saving...' : saveButtonLabel}
</button>
</Button>
</form>
</section>
</CardContent>
</Card>
<section className="rounded-[2rem] border border-white/8 bg-[#0b0d10] p-6 shadow-[0_30px_80px_rgba(0,0,0,0.35)]">
<div className="mb-6">
<h2 className="text-lg font-black uppercase tracking-tight text-white">Saved setups</h2>
<p className="text-xs uppercase tracking-[0.24em] text-zinc-500">
One dedicated Simple auto profile routes every triggered order
</p>
</div>
<Card>
<CardHeader className="block">
<CardTitle className="uppercase">Saved setups</CardTitle>
<CardDescription>Review and update armed simple workflows in the same layout style used across the app.</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{savedSetups.length === 0 && (
<div className="rounded-[1.75rem] border border-dashed border-white/10 bg-white/[0.02] px-5 py-8 text-sm text-zinc-500">
<div className="rounded-[1.5rem] border border-dashed border-[var(--border)] bg-[var(--card-elevated)] px-5 py-8 text-sm text-[var(--muted-foreground)]">
No Simple setups saved yet.
</div>
)}
@ -688,63 +677,63 @@ export function SimpleView() {
const side = normalizeSetupSide(entry.simple_side);
const isEditing = editingSetupId === entryId;
return (
<div key={entryId} className="rounded-[1.75rem] border border-white/8 bg-white/[0.02] p-5">
<div key={entryId} className="rounded-[1.5rem] border border-[var(--border)] bg-[var(--card-elevated)] p-5">
<div className="mb-3 flex items-start justify-between gap-4">
<div>
<div className="flex items-center gap-3">
<h3 className="text-lg font-black uppercase text-white">{entry.symbol}</h3>
<h3 className="text-lg font-black uppercase text-[var(--foreground)]">{entry.symbol}</h3>
<span className={`rounded-full px-3 py-1 text-[10px] font-black uppercase tracking-[0.2em] ${
side === 'buy' ? 'bg-[#102b1f] text-[#71f6a8]' : 'bg-[#1f1a2c] text-[#c4a3ff]'
side === 'buy' ? 'bg-emerald-500/10 text-emerald-700 dark:text-emerald-300' : 'bg-violet-500/10 text-violet-700 dark:text-violet-300'
}`}>
{side}
</span>
</div>
<p className="mt-2 text-sm text-zinc-400">{describeSavedSetup(entry)}</p>
<p className="mt-2 text-sm text-[var(--muted-foreground)]">{describeSavedSetup(entry)}</p>
</div>
<div className="flex items-center gap-2">
<button
<Button
type="button"
onClick={() => handleEdit(entry)}
className={`rounded-2xl border px-3 py-2 text-xs font-black uppercase tracking-[0.18em] ${
isEditing
? 'border-[#71f6a8]/40 text-[#71f6a8]'
: 'border-white/10 text-zinc-400 hover:border-white/20 hover:text-white'
}`}
variant="outline"
size="sm"
className={isEditing ? 'border-emerald-500/30 text-emerald-700 dark:text-emerald-300' : 'uppercase tracking-[0.18em]'}
>
<span className="inline-flex items-center gap-2">
<Pencil size={14} />
Edit
</span>
</button>
<button
</Button>
<Button
type="button"
onClick={() => void handleDelete(entryId)}
className="rounded-2xl border border-[#ef4444]/20 px-3 py-2 text-xs font-black uppercase tracking-[0.18em] text-[#f5b1b1] hover:border-[#ef4444]/40"
variant="destructive"
size="sm"
className="uppercase tracking-[0.18em]"
>
<span className="inline-flex items-center gap-2">
<Trash2 size={14} />
Delete
</span>
</button>
</Button>
</div>
</div>
<div className="flex flex-wrap items-center gap-3 text-[11px] uppercase tracking-[0.2em] text-zinc-500">
<span className="rounded-full border border-white/10 px-3 py-1">
<div className="flex flex-wrap items-center gap-3 text-[11px] uppercase tracking-[0.2em] text-[var(--muted-foreground)]">
<span className="rounded-full border border-[var(--border)] px-3 py-1">
{formatSetupStatus(entry.status)}
</span>
{entry.reference_price ? (
<span className="rounded-full border border-white/10 px-3 py-1">
<span className="rounded-full border border-[var(--border)] px-3 py-1">
Ref {Number(entry.reference_price).toFixed(4)}
</span>
) : null}
{entry.entry_price ? (
<span className="rounded-full border border-white/10 px-3 py-1">
<span className="rounded-full border border-[var(--border)] px-3 py-1">
Entry {Number(entry.entry_price).toFixed(4)}
</span>
) : null}
{entry.linked_trade_id ? (
<span className="rounded-full border border-white/10 px-3 py-1">
<span className="rounded-full border border-[var(--border)] px-3 py-1">
Trade linked
</span>
) : null}
@ -753,7 +742,8 @@ export function SimpleView() {
);
})}
</div>
</section>
</CardContent>
</Card>
</div>
</div>
);

View File

@ -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 (
<div>
<h2 style={{ fontSize: 22, fontWeight: 800, color: '#111827', margin: '0 0 20px' }}>Watchlist</h2>
<PageHeader
title="Watchlist"
description="Manage saved manual entries, simple setups, and symbols you want to monitor closely."
/>
<EntriesTab botState={botState} />
</div>
);