feat(web): add shared light dark theme system
This commit is contained in:
parent
9021df19ad
commit
266b367322
@ -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',
|
||||
|
||||
@ -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,
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
64
web/src/components/theme/ThemeProvider.tsx
Normal file
64
web/src/components/theme/ThemeProvider.tsx
Normal 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;
|
||||
}
|
||||
45
web/src/components/ui/button.tsx
Normal file
45
web/src/components/ui/button.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
45
web/src/components/ui/card.tsx
Normal file
45
web/src/components/ui/card.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
14
web/src/components/ui/input.tsx
Normal file
14
web/src/components/ui/input.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
28
web/src/components/ui/page-header.tsx
Normal file
28
web/src/components/ui/page-header.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
16
web/src/components/ui/select.tsx
Normal file
16
web/src/components/ui/select.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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
3
web/src/lib/utils.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function cn(...values: Array<string | false | null | undefined>) {
|
||||
return values.filter(Boolean).join(' ');
|
||||
}
|
||||
@ -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 }}>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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} />
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)} />
|
||||
))}
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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} />}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user