feat(web): add shared light dark theme system
This commit is contained in:
parent
9021df19ad
commit
266b367322
@ -130,7 +130,7 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex', justifyContent: 'center', alignItems: 'center',
|
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',
|
fontSize: 15, fontFamily: 'Inter, system-ui, sans-serif',
|
||||||
}}>
|
}}>
|
||||||
Loading…
|
Loading…
|
||||||
@ -160,7 +160,7 @@ function App() {
|
|||||||
{hasCriticalEvents && (
|
{hasCriticalEvents && (
|
||||||
<div style={{
|
<div style={{
|
||||||
position: 'fixed', top: 0, left: 0, right: 0, zIndex: 9999,
|
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',
|
color: '#fff',
|
||||||
padding: '6px 20px',
|
padding: '6px 20px',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export function SkeletonBlock({
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
borderRadius: radius,
|
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,
|
...style,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { Search } from 'lucide-react';
|
import { Moon, Search, Sun } from 'lucide-react';
|
||||||
import { useAppContext } from '../../context/AppContext';
|
import { useAppContext } from '../../context/AppContext';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { fetchMarketIndices, type IndexSnapshot } from '../../lib/marketApi';
|
import { fetchMarketIndices, type IndexSnapshot } from '../../lib/marketApi';
|
||||||
|
import { useTheme } from '../theme/ThemeProvider';
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
@ -11,6 +13,7 @@ export function Header() {
|
|||||||
const [indices, setIndices] = useState<IndexSnapshot[]>([]);
|
const [indices, setIndices] = useState<IndexSnapshot[]>([]);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const { theme, toggleTheme } = useTheme();
|
||||||
|
|
||||||
// Fetch live market indices while visible; hidden tabs should not burn API quota.
|
// Fetch live market indices while visible; hidden tabs should not burn API quota.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -60,14 +63,15 @@ export function Header() {
|
|||||||
return (
|
return (
|
||||||
<header style={{
|
<header style={{
|
||||||
height: 56,
|
height: 56,
|
||||||
background: '#ffffff',
|
background: 'var(--header)',
|
||||||
borderBottom: '1px solid #E5E7EB',
|
borderBottom: '1px solid var(--border)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingLeft: 20,
|
paddingLeft: 20,
|
||||||
paddingRight: 24,
|
paddingRight: 24,
|
||||||
gap: 16,
|
gap: 16,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
|
backdropFilter: 'blur(18px)',
|
||||||
}}>
|
}}>
|
||||||
{/* Search bar */}
|
{/* Search bar */}
|
||||||
<div style={{ position: 'relative', width: 300 }}>
|
<div style={{ position: 'relative', width: 300 }}>
|
||||||
@ -78,7 +82,7 @@ export function Header() {
|
|||||||
left: 11,
|
left: 11,
|
||||||
top: '50%',
|
top: '50%',
|
||||||
transform: 'translateY(-50%)',
|
transform: 'translateY(-50%)',
|
||||||
color: '#9CA3AF',
|
color: 'var(--muted-foreground)',
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -99,12 +103,12 @@ export function Header() {
|
|||||||
paddingRight: 12,
|
paddingRight: 12,
|
||||||
paddingTop: 8,
|
paddingTop: 8,
|
||||||
paddingBottom: 8,
|
paddingBottom: 8,
|
||||||
border: '1px solid #E5E7EB',
|
border: '1px solid var(--border)',
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
outline: 'none',
|
outline: 'none',
|
||||||
color: '#374151',
|
color: 'var(--foreground)',
|
||||||
background: '#F9FAFB',
|
background: 'var(--input)',
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
fontFamily: 'inherit',
|
fontFamily: 'inherit',
|
||||||
}}
|
}}
|
||||||
@ -121,8 +125,8 @@ export function Header() {
|
|||||||
['S&P 500','Dow Jones','Nasdaq'].map(label => (
|
['S&P 500','Dow Jones','Nasdaq'].map(label => (
|
||||||
<div key={label} style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
<div key={label} style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: 11, color: '#6B7280', fontWeight: 500 }}>{label}</div>
|
<div style={{ fontSize: 11, color: 'var(--muted-foreground)', fontWeight: 500 }}>{label}</div>
|
||||||
<div style={{ fontSize: 13, fontWeight: 700, color: '#D1D5DB' }}>—</div>
|
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--muted-foreground)' }}>—</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
@ -131,11 +135,11 @@ export function Header() {
|
|||||||
return (
|
return (
|
||||||
<div key={idx.label} style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
<div key={idx.label} style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||||
<div>
|
<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}
|
{idx.label}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 4 }}>
|
<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)}
|
${idx.price.toFixed(2)}
|
||||||
</span>
|
</span>
|
||||||
<span style={{
|
<span style={{
|
||||||
@ -151,17 +155,27 @@ export function Header() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</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 */}
|
{/* Live indicator */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 5, marginLeft: 8 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 5, marginLeft: 8 }}>
|
||||||
<span style={{
|
<span style={{
|
||||||
width: 7,
|
width: 7,
|
||||||
height: 7,
|
height: 7,
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
background: connected ? '#22C55E' : '#EF4444',
|
background: connected ? '#22C55E' : 'var(--destructive)',
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
}} />
|
}} />
|
||||||
<span style={{ fontSize: 11, color: '#9CA3AF', whiteSpace: 'nowrap' }}>
|
<span style={{ fontSize: 11, color: 'var(--muted-foreground)', whiteSpace: 'nowrap' }}>
|
||||||
{connected ? 'Live' : 'Reconnecting'}
|
{connected ? 'Live' : 'Reconnecting'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -30,13 +30,13 @@ export function Sidebar() {
|
|||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
borderRadius: 10,
|
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',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
marginBottom: 24,
|
marginBottom: 24,
|
||||||
flexShrink: 0,
|
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',
|
cursor: 'pointer',
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
}}>
|
}}>
|
||||||
@ -58,10 +58,10 @@ export function Sidebar() {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
padding: '10px 0 8px',
|
padding: '10px 0 8px',
|
||||||
gap: 3,
|
gap: 3,
|
||||||
borderLeft: isActive ? '3px solid #2563EB' : '3px solid transparent',
|
borderLeft: isActive ? '3px solid var(--accent)' : '3px solid transparent',
|
||||||
color: isActive ? '#2563EB' : '#6B7280',
|
color: isActive ? 'var(--accent)' : 'var(--muted-foreground)',
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
background: isActive ? '#EFF6FF' : 'transparent',
|
background: isActive ? 'var(--sidebar-active)' : 'transparent',
|
||||||
transition: 'all 0.15s',
|
transition: 'all 0.15s',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
@ -91,7 +91,7 @@ export function Sidebar() {
|
|||||||
width: 36,
|
width: 36,
|
||||||
height: 36,
|
height: 36,
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
background: '#2563EB',
|
background: 'var(--accent)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@ -113,7 +113,7 @@ export function Sidebar() {
|
|||||||
height: 9,
|
height: 9,
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
background: '#22C55E',
|
background: '#22C55E',
|
||||||
border: '2px solid #fff',
|
border: '2px solid var(--card)',
|
||||||
}} />
|
}} />
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</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;
|
@tailwind utilities;
|
||||||
|
|
||||||
:root {
|
: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;
|
line-height: 1.5;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
color: #111827;
|
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;
|
font-synthesis: none;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
@ -17,10 +37,38 @@
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-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 {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background-color: #F3F4F6;
|
background-color: var(--background);
|
||||||
|
color: var(--foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
@ -76,7 +124,7 @@ body {
|
|||||||
.dashboard-shell {
|
.dashboard-shell {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: #F3F4F6;
|
background: var(--background);
|
||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,8 +160,9 @@ body {
|
|||||||
.trading-sidebar {
|
.trading-sidebar {
|
||||||
width: 72px;
|
width: 72px;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: #ffffff;
|
background: var(--sidebar);
|
||||||
border-right: 1px solid #E5E7EB;
|
border-right: 1px solid var(--border);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -126,6 +175,49 @@ body {
|
|||||||
z-index: 50;
|
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 {
|
.trading-sidebar-nav {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -173,7 +265,7 @@ body {
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
border-right: 0;
|
border-right: 0;
|
||||||
border-top: 1px solid #E5E7EB;
|
border-top: 1px solid var(--border);
|
||||||
overflow-x: auto;
|
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 App from './App.tsx'
|
||||||
import { AuthProvider } from './components/AuthContext';
|
import { AuthProvider } from './components/AuthContext';
|
||||||
import { ProductAccessibilityGate } from './components/ProductAccessibilityGate';
|
import { ProductAccessibilityGate } from './components/ProductAccessibilityGate';
|
||||||
import { tradingTelemetry } from './lib/runtime';
|
import { ThemeProvider } from './components/theme/ThemeProvider';
|
||||||
|
|
||||||
const telemetryClient = tradingTelemetry.init();
|
|
||||||
telemetryClient.trackEvent('info', 'app_shell', 'trading_web_bootstrap', {
|
|
||||||
feature: 'bootstrap',
|
|
||||||
message: window.location.pathname,
|
|
||||||
tags: { surface: 'web' },
|
|
||||||
});
|
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<ProductAccessibilityGate>
|
<ProductAccessibilityGate>
|
||||||
|
<ThemeProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<App />
|
<App />
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
</ThemeProvider>
|
||||||
</ProductAccessibilityGate>
|
</ProductAccessibilityGate>
|
||||||
{import.meta.env.DEV && (
|
{import.meta.env.DEV && (
|
||||||
<div style={{ position: 'relative', zIndex: 2147483647 }}>
|
<div style={{ position: 'relative', zIndex: 2147483647 }}>
|
||||||
|
|||||||
@ -1,11 +1,15 @@
|
|||||||
import { useAppContext } from '../context/AppContext';
|
import { useAppContext } from '../context/AppContext';
|
||||||
import { AlertFeed } from '../components/AlertFeed';
|
import { AlertFeed } from '../components/AlertFeed';
|
||||||
|
import { PageHeader } from '../components/ui/page-header';
|
||||||
|
|
||||||
export function AlertsView() {
|
export function AlertsView() {
|
||||||
const { botState } = useAppContext();
|
const { botState } = useAppContext();
|
||||||
return (
|
return (
|
||||||
<div>
|
<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} />
|
<AlertFeed alerts={botState.alerts} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import {
|
|||||||
type OHLCVBar,
|
type OHLCVBar,
|
||||||
} from '../lib/marketApi';
|
} from '../lib/marketApi';
|
||||||
import { SkeletonBlock, SkeletonText } from '../components/Skeleton';
|
import { SkeletonBlock, SkeletonText } from '../components/Skeleton';
|
||||||
|
import { Button } from '../components/ui/button';
|
||||||
|
|
||||||
// ─── Time period config ───────────────────────────────────────────────────────
|
// ─── Time period config ───────────────────────────────────────────────────────
|
||||||
const PERIODS = ['1D', '5D', '1M', '3M', '6M', 'YTD', '1Y', '5Y', 'MAX'] as const;
|
const PERIODS = ['1D', '5D', '1M', '3M', '6M', 'YTD', '1Y', '5Y', 'MAX'] as const;
|
||||||
@ -230,43 +231,39 @@ export function TickerHeader({
|
|||||||
return (
|
return (
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 4 }}>
|
<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}
|
{symbol}
|
||||||
</h1>
|
</h1>
|
||||||
<span style={{ fontSize: 13, color: '#6B7280', fontWeight: 500, marginTop: 4 }}>
|
<span style={{ fontSize: 13, color: 'var(--muted-foreground)', fontWeight: 500, marginTop: 4 }}>
|
||||||
{companyName}
|
{companyName}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div style={{ marginLeft: 'auto', display: 'flex', gap: 8 }}>
|
<div style={{ marginLeft: 'auto', display: 'flex', gap: 8 }}>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={`Open watchlist for ${symbol}`}
|
aria-label={`Open watchlist for ${symbol}`}
|
||||||
onClick={() => navigate('/watchlist')}
|
onClick={() => navigate('/watchlist')}
|
||||||
style={{
|
variant="outline"
|
||||||
display: 'flex', alignItems: 'center', gap: 6,
|
size="sm"
|
||||||
padding: '5px 12px', borderRadius: 20,
|
style={{ borderRadius: 999 }}
|
||||||
background: '#F0FDF4', border: '1px solid #86EFAC',
|
>
|
||||||
color: '#16A34A', fontSize: 12, fontWeight: 600, cursor: 'pointer',
|
<Star size={13} /> Watchlist
|
||||||
}}>
|
</Button>
|
||||||
<Star size={13} fill="#16A34A" /> Watchlist
|
<Button
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={`Open alerts for ${symbol}`}
|
aria-label={`Open alerts for ${symbol}`}
|
||||||
onClick={() => navigate('/alerts')}
|
onClick={() => navigate('/alerts')}
|
||||||
style={{
|
variant="outline"
|
||||||
width: 32, height: 32, borderRadius: '50%',
|
size="icon"
|
||||||
border: '1px solid #E5E7EB', background: '#fff',
|
style={{ borderRadius: '50%' }}
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
>
|
||||||
cursor: 'pointer', color: '#6B7280',
|
|
||||||
}}>
|
|
||||||
<Bell size={15} />
|
<Bell size={15} />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 10 }}>
|
<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) : '—'}
|
{price > 0 ? price.toFixed(2) : '—'}
|
||||||
</span>
|
</span>
|
||||||
{price > 0 && (
|
{price > 0 && (
|
||||||
@ -276,7 +273,7 @@ export function TickerHeader({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ fontSize: 11, color: '#9CA3AF', marginTop: 3 }}>
|
<div style={{ fontSize: 11, color: 'var(--muted-foreground)', marginTop: 3 }}>
|
||||||
{formatAsOfTimestamp(latestBarTimestamp ?? null)} ET · {exchange}
|
{formatAsOfTimestamp(latestBarTimestamp ?? null)} ET · {exchange}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -344,7 +341,7 @@ function StockChart({
|
|||||||
const firstPrice = chartData[0]?.price ?? 0;
|
const firstPrice = chartData[0]?.price ?? 0;
|
||||||
const lastPrice = chartData[chartData.length - 1]?.price ?? 0;
|
const lastPrice = chartData[chartData.length - 1]?.price ?? 0;
|
||||||
const positive = lastPrice >= firstPrice;
|
const positive = lastPrice >= firstPrice;
|
||||||
const lineColor = positive ? '#2563EB' : '#DC2626';
|
const lineColor = positive ? 'var(--primary)' : 'var(--destructive)';
|
||||||
|
|
||||||
const priceYValues = chartData.flatMap(d => [
|
const priceYValues = chartData.flatMap(d => [
|
||||||
d.price,
|
d.price,
|
||||||
@ -367,9 +364,9 @@ function StockChart({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
background: '#fff',
|
background: 'var(--card)',
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
border: '1px solid #E5E7EB',
|
border: '1px solid var(--border)',
|
||||||
padding: '16px 20px 12px',
|
padding: '16px 20px 12px',
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
}}>
|
}}>
|
||||||
@ -386,8 +383,8 @@ function StockChart({
|
|||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: period === p ? 700 : 500,
|
fontWeight: period === p ? 700 : 500,
|
||||||
background: period === p ? '#EFF6FF' : 'transparent',
|
background: period === p ? 'var(--accent-soft)' : 'transparent',
|
||||||
color: period === p ? '#2563EB' : '#6B7280',
|
color: period === p ? 'var(--primary)' : 'var(--muted-foreground)',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: 'all 0.15s',
|
transition: 'all 0.15s',
|
||||||
}}
|
}}
|
||||||
@ -396,7 +393,7 @@ function StockChart({
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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 }}>
|
<div style={{ display: 'flex', gap: 6 }}>
|
||||||
{INDICATORS.map(indicator => {
|
{INDICATORS.map(indicator => {
|
||||||
const active = enabledIndicators[indicator.key];
|
const active = enabledIndicators[indicator.key];
|
||||||
@ -409,13 +406,13 @@ function StockChart({
|
|||||||
style={{
|
style={{
|
||||||
padding: '5px 9px',
|
padding: '5px 9px',
|
||||||
borderRadius: 999,
|
borderRadius: 999,
|
||||||
border: active ? '1px solid #93C5FD' : '1px solid #E5E7EB',
|
border: active ? '1px solid var(--primary)' : '1px solid var(--border)',
|
||||||
background: active ? '#EFF6FF' : '#fff',
|
background: active ? 'var(--accent-soft)' : 'var(--card)',
|
||||||
color: active ? '#1D4ED8' : '#6B7280',
|
color: active ? 'var(--primary)' : 'var(--muted-foreground)',
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
cursor: 'pointer',
|
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}
|
{indicator.label}
|
||||||
@ -429,10 +426,10 @@ function StockChart({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 12 }}>
|
<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'}
|
Indicators: {enabledCount > 0 ? `${enabledCount} active` : 'none'}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 11, color: '#9CA3AF' }}>
|
<div style={{ fontSize: 11, color: 'var(--muted-foreground)' }}>
|
||||||
RSI 14 · MACD 12/26/9 · Bollinger 20/2
|
RSI 14 · MACD 12/26/9 · Bollinger 20/2
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -441,9 +438,9 @@ function StockChart({
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<div style={{
|
<div style={{
|
||||||
height: 220, display: 'flex', flexDirection: 'column',
|
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>
|
<span style={{ fontSize: 13 }}>Loading chart…</span>
|
||||||
</div>
|
</div>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
@ -461,11 +458,11 @@ function StockChart({
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
color: '#9CA3AF',
|
color: 'var(--muted-foreground)',
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
gap: 8,
|
gap: 8,
|
||||||
}}>
|
}}>
|
||||||
<BarChart2 size={32} color="#D1D5DB" />
|
<BarChart2 size={32} color="var(--muted-foreground)" />
|
||||||
<span>No price data available for {symbol}</span>
|
<span>No price data available for {symbol}</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -478,17 +475,17 @@ function StockChart({
|
|||||||
<stop offset="95%" stopColor={lineColor} stopOpacity={0.01} />
|
<stop offset="95%" stopColor={lineColor} stopOpacity={0.01} />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#F3F4F6" vertical={false} />
|
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" vertical={false} />
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="label"
|
dataKey="label"
|
||||||
tick={{ fontSize: 10, fill: '#9CA3AF' }}
|
tick={{ fontSize: 10, fill: 'var(--muted-foreground)' }}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
interval="preserveStartEnd"
|
interval="preserveStartEnd"
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis
|
||||||
domain={[minY - pad, maxY + pad]}
|
domain={[minY - pad, maxY + pad]}
|
||||||
tick={{ fontSize: 10, fill: '#9CA3AF' }}
|
tick={{ fontSize: 10, fill: 'var(--muted-foreground)' }}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
width={55}
|
width={55}
|
||||||
@ -496,11 +493,11 @@ function StockChart({
|
|||||||
/>
|
/>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
contentStyle={{
|
contentStyle={{
|
||||||
background: '#fff',
|
background: 'var(--card-elevated)',
|
||||||
border: '1px solid #E5E7EB',
|
border: '1px solid var(--border)',
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
boxShadow: '0 4px 12px rgba(0,0,0,0.08)',
|
boxShadow: 'var(--card-shadow)',
|
||||||
}}
|
}}
|
||||||
formatter={(val: unknown, name: unknown) => {
|
formatter={(val: unknown, name: unknown) => {
|
||||||
const labels: Record<string, string> = {
|
const labels: Record<string, string> = {
|
||||||
@ -512,7 +509,7 @@ function StockChart({
|
|||||||
const key = String(name ?? '');
|
const key = String(name ?? '');
|
||||||
return [`$${Number(val).toFixed(2)}`, labels[key] ?? key];
|
return [`$${Number(val).toFixed(2)}`, labels[key] ?? key];
|
||||||
}}
|
}}
|
||||||
labelStyle={{ color: '#6B7280', fontSize: 11 }}
|
labelStyle={{ color: 'var(--muted-foreground)', fontSize: 11 }}
|
||||||
/>
|
/>
|
||||||
{enabledIndicators.bollinger && (
|
{enabledIndicators.bollinger && (
|
||||||
<>
|
<>
|
||||||
@ -534,20 +531,20 @@ function StockChart({
|
|||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
|
|
||||||
{enabledIndicators.rsi && (
|
{enabledIndicators.rsi && (
|
||||||
<div style={{ marginTop: 12, paddingTop: 10, borderTop: '1px solid #F3F4F6' }}>
|
<div style={{ marginTop: 12, paddingTop: 10, borderTop: '1px solid var(--border)' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4, fontSize: 11, color: '#6B7280', fontWeight: 700 }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4, fontSize: 11, color: 'var(--muted-foreground)', fontWeight: 700 }}>
|
||||||
<span>RSI (14)</span>
|
<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>
|
</div>
|
||||||
<ResponsiveContainer width="100%" height={86}>
|
<ResponsiveContainer width="100%" height={86}>
|
||||||
<LineChart data={chartData} margin={{ top: 4, right: 4, bottom: 0, left: 0 }}>
|
<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 />
|
<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
|
<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']}
|
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={70} stroke="#FCA5A5" strokeDasharray="3 3" />
|
||||||
<ReferenceLine y={30} stroke="#93C5FD" strokeDasharray="3 3" />
|
<ReferenceLine y={30} stroke="#93C5FD" strokeDasharray="3 3" />
|
||||||
@ -558,23 +555,23 @@ function StockChart({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{enabledIndicators.macd && (
|
{enabledIndicators.macd && (
|
||||||
<div style={{ marginTop: 12, paddingTop: 10, borderTop: '1px solid #F3F4F6' }}>
|
<div style={{ marginTop: 12, paddingTop: 10, borderTop: '1px solid var(--border)' }}>
|
||||||
<div style={{ marginBottom: 4, fontSize: 11, color: '#6B7280', fontWeight: 700 }}>
|
<div style={{ marginBottom: 4, fontSize: 11, color: 'var(--muted-foreground)', fontWeight: 700 }}>
|
||||||
MACD (12, 26, 9)
|
MACD (12, 26, 9)
|
||||||
</div>
|
</div>
|
||||||
<ResponsiveContainer width="100%" height={106}>
|
<ResponsiveContainer width="100%" height={106}>
|
||||||
<ComposedChart data={chartData} margin={{ top: 4, right: 4, bottom: 0, left: 0 }}>
|
<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 />
|
<XAxis dataKey="label" hide />
|
||||||
<YAxis
|
<YAxis
|
||||||
domain={[-macdMaxAbs * 1.2, macdMaxAbs * 1.2]}
|
domain={[-macdMaxAbs * 1.2, macdMaxAbs * 1.2]}
|
||||||
tick={{ fontSize: 10, fill: '#9CA3AF' }}
|
tick={{ fontSize: 10, fill: 'var(--muted-foreground)' }}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
width={55}
|
width={55}
|
||||||
/>
|
/>
|
||||||
<Tooltip
|
<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) => {
|
formatter={(val: unknown, name: unknown) => {
|
||||||
const labels: Record<string, string> = {
|
const labels: Record<string, string> = {
|
||||||
macdHistogram: 'Histogram',
|
macdHistogram: 'Histogram',
|
||||||
@ -584,7 +581,7 @@ function StockChart({
|
|||||||
const key = String(name ?? '');
|
const key = String(name ?? '');
|
||||||
return [Number(val).toFixed(3), labels[key] ?? key];
|
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" />
|
<ReferenceLine y={0} stroke="#CBD5E1" />
|
||||||
<Bar dataKey="macdHistogram" fill="#BFDBFE" radius={[2, 2, 0, 0]} />
|
<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 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))', gap: 12, marginBottom: 20 }}>
|
||||||
{stats.map(s => (
|
{stats.map(s => (
|
||||||
<div key={s.label} style={{
|
<div key={s.label} style={{
|
||||||
background: '#fff',
|
background: 'var(--card)',
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
border: '1px solid #E5E7EB',
|
border: '1px solid var(--border)',
|
||||||
padding: '12px 14px',
|
padding: '12px 14px',
|
||||||
}}>
|
}}>
|
||||||
<div style={{ fontSize: 11, color: '#9CA3AF', fontWeight: 500, marginBottom: 4 }}>{s.label}</div>
|
<div style={{ fontSize: 11, color: 'var(--muted-foreground)', fontWeight: 500, marginBottom: 4 }}>{s.label}</div>
|
||||||
<div style={{ fontSize: 16, fontWeight: 700, color: '#111827' }}>{s.value}</div>
|
<div style={{ fontSize: 16, fontWeight: 700, color: 'var(--foreground)' }}>{s.value}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -693,8 +690,8 @@ function ResearchCards({
|
|||||||
return (
|
return (
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 16 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 16 }}>
|
||||||
{/* Company Profile */}
|
{/* Company Profile */}
|
||||||
<div style={{ background: '#fff', borderRadius: 12, border: '1px solid #E5E7EB', padding: 16 }}>
|
<div style={{ background: 'var(--card)', borderRadius: 12, border: '1px solid var(--border)', padding: 16 }}>
|
||||||
<div style={{ fontSize: 13, fontWeight: 700, color: '#111827', marginBottom: 10 }}>
|
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--foreground)', marginBottom: 10 }}>
|
||||||
📋 Company
|
📋 Company
|
||||||
</div>
|
</div>
|
||||||
{profileLoading ? (
|
{profileLoading ? (
|
||||||
@ -706,40 +703,40 @@ function ResearchCards({
|
|||||||
</div>
|
</div>
|
||||||
) : profile ? (
|
) : 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>
|
<strong>{profile.companyName ?? symbol}</strong>
|
||||||
{profile.sector && <> · {profile.sector}</>}
|
{profile.sector && <> · {profile.sector}</>}
|
||||||
{profile.industry && <> · {profile.industry}</>}
|
{profile.industry && <> · {profile.industry}</>}
|
||||||
</div>
|
</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',
|
display: '-webkit-box', WebkitLineClamp: 4, WebkitBoxOrient: 'vertical', overflow: 'hidden',
|
||||||
}}>
|
}}>
|
||||||
{profile.description ?? ''}
|
{profile.description ?? ''}
|
||||||
</div>
|
</div>
|
||||||
{profile.website && (
|
{profile.website && (
|
||||||
<a href={profile.website} target="_blank" rel="noreferrer"
|
<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}
|
{profile.website}
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ fontSize: 12, color: '#9CA3AF' }}>No profile data</div>
|
<div style={{ fontSize: 12, color: 'var(--muted-foreground)' }}>No profile data</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Financials */}
|
{/* Financials */}
|
||||||
<div style={{ background: '#fff', borderRadius: 12, border: '1px solid #E5E7EB', padding: 16 }}>
|
<div style={{ background: 'var(--card)', borderRadius: 12, border: '1px solid var(--border)', padding: 16 }}>
|
||||||
<div style={{ fontSize: 13, fontWeight: 700, color: '#111827', marginBottom: 10 }}>
|
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--foreground)', marginBottom: 10 }}>
|
||||||
📊 Financials
|
📊 Financials
|
||||||
</div>
|
</div>
|
||||||
{financialRows.map(([label, val]) => (
|
{financialRows.map(([label, val]) => (
|
||||||
<div key={label} style={{
|
<div key={label} style={{
|
||||||
display: 'flex', justifyContent: 'space-between',
|
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, color: 'var(--muted-foreground)' }}>{label}</span>
|
||||||
<span style={{ fontSize: 12, fontWeight: 600, color: '#111827' }}>
|
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--foreground)' }}>
|
||||||
{loading || profileLoading ? <ValueSkeleton width={label.length > 10 ? 64 : 46} /> : val}
|
{loading || profileLoading ? <ValueSkeleton width={label.length > 10 ? 64 : 46} /> : val}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -747,8 +744,8 @@ function ResearchCards({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Events / Earnings */}
|
{/* Events / Earnings */}
|
||||||
<div style={{ background: '#fff', borderRadius: 12, border: '1px solid #E5E7EB', padding: 16 }}>
|
<div style={{ background: 'var(--card)', borderRadius: 12, border: '1px solid var(--border)', padding: 16 }}>
|
||||||
<div style={{ fontSize: 13, fontWeight: 700, color: '#111827', marginBottom: 10 }}>
|
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--foreground)', marginBottom: 10 }}>
|
||||||
📅 Events
|
📅 Events
|
||||||
</div>
|
</div>
|
||||||
{[
|
{[
|
||||||
@ -759,19 +756,19 @@ function ResearchCards({
|
|||||||
].map(([label, val]) => (
|
].map(([label, val]) => (
|
||||||
<div key={label} style={{
|
<div key={label} style={{
|
||||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
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, color: 'var(--muted-foreground)' }}>{label}</span>
|
||||||
<span style={{ fontSize: 12, fontWeight: 600, color: '#111827' }}>
|
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--foreground)' }}>
|
||||||
{val === '…' ? <ValueSkeleton width={label === 'Next Earnings' ? 82 : 52} /> : val}
|
{val === '…' ? <ValueSkeleton width={label === 'Next Earnings' ? 82 : 52} /> : val}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{!loading && earnings.length > 0 && (
|
{!loading && earnings.length > 0 && (
|
||||||
<div style={{ marginTop: 10 }}>
|
<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) => (
|
{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>{fmtDate(e.date)}</span>
|
||||||
<span style={{ color: e.eps >= (e.epsEstimated ?? e.eps) ? '#16A34A' : '#DC2626' }}>
|
<span style={{ color: e.eps >= (e.epsEstimated ?? e.eps) ? '#16A34A' : '#DC2626' }}>
|
||||||
EPS {e.eps != null ? `$${e.eps.toFixed(2)}` : '—'}
|
EPS {e.eps != null ? `$${e.eps.toFixed(2)}` : '—'}
|
||||||
@ -799,17 +796,17 @@ function EmptyState({
|
|||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
||||||
justifyContent: 'center', height: '60vh', gap: 16,
|
justifyContent: 'center', height: '60vh', gap: 16,
|
||||||
color: '#9CA3AF',
|
color: 'var(--muted-foreground)',
|
||||||
}}>
|
}}>
|
||||||
<div style={{ fontSize: 56 }}>📈</div>
|
<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
|
Search an asset to get started
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 14, textAlign: 'center', maxWidth: 360 }}>
|
<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.
|
Type a ticker symbol, crypto pair, or company name in the search bar above to view charts, financials, and news.
|
||||||
</div>
|
</div>
|
||||||
{cryptoMode && (
|
{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
|
Suggested from your crypto bot configuration
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -820,9 +817,9 @@ function EmptyState({
|
|||||||
onClick={() => onSelect(t)}
|
onClick={() => onSelect(t)}
|
||||||
style={{
|
style={{
|
||||||
padding: '4px 12px',
|
padding: '4px 12px',
|
||||||
background: '#EFF6FF',
|
background: 'var(--accent-soft)',
|
||||||
color: '#2563EB',
|
color: 'var(--primary)',
|
||||||
border: '1px solid #BFDBFE',
|
border: '1px solid var(--border)',
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { useAppContext } from '../context/AppContext';
|
|||||||
import { MarketplaceTab } from '../tabs/MarketplaceTab';
|
import { MarketplaceTab } from '../tabs/MarketplaceTab';
|
||||||
import { TopVolatile, AISetups } from '../components/MarketOpportunities';
|
import { TopVolatile, AISetups } from '../components/MarketOpportunities';
|
||||||
import type { StrategyPreset } from '../lib/PresetRegistry';
|
import type { StrategyPreset } from '../lib/PresetRegistry';
|
||||||
|
import { PageHeader } from '../components/ui/page-header';
|
||||||
|
|
||||||
export function MarketsView() {
|
export function MarketsView() {
|
||||||
const { botState, showMarketplaceTab } = useAppContext();
|
const { botState, showMarketplaceTab } = useAppContext();
|
||||||
@ -12,7 +13,10 @@ export function MarketsView() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<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 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20, marginBottom: 24 }}>
|
||||||
<TopVolatile botState={botState} />
|
<TopVolatile botState={botState} />
|
||||||
<AISetups botState={botState} />
|
<AISetups botState={botState} />
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { useState } from 'react';
|
|||||||
import { useAppContext } from '../context/AppContext';
|
import { useAppContext } from '../context/AppContext';
|
||||||
import { PositionsTab } from '../tabs/PositionsTab';
|
import { PositionsTab } from '../tabs/PositionsTab';
|
||||||
import { HistoryTab } from '../tabs/HistoryTab';
|
import { HistoryTab } from '../tabs/HistoryTab';
|
||||||
|
import { PageHeader } from '../components/ui/page-header';
|
||||||
|
|
||||||
const TABS = ['Positions & Orders', 'Trade History'] as const;
|
const TABS = ['Positions & Orders', 'Trade History'] as const;
|
||||||
type Tab = typeof TABS[number];
|
type Tab = typeof TABS[number];
|
||||||
@ -12,29 +13,18 @@ export function PortfolioView() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<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 className="tab-strip" style={{ marginBottom: 20 }}>
|
||||||
<div style={{
|
|
||||||
display: 'flex', gap: 4, marginBottom: 20,
|
|
||||||
borderBottom: '1px solid #E5E7EB', paddingBottom: 0,
|
|
||||||
}}>
|
|
||||||
{TABS.map(t => (
|
{TABS.map(t => (
|
||||||
<button
|
<button
|
||||||
key={t}
|
key={t}
|
||||||
onClick={() => setTab(t)}
|
onClick={() => setTab(t)}
|
||||||
style={{
|
className="tab-button"
|
||||||
padding: '8px 16px',
|
data-active={tab === t}
|
||||||
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',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{t}
|
{t}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { MyStrategiesTab } from '../tabs/MyStrategiesTab';
|
|||||||
import { VisualRuleBuilder, type VisualRule } from '../components/strategy/VisualRuleBuilder';
|
import { VisualRuleBuilder, type VisualRule } from '../components/strategy/VisualRuleBuilder';
|
||||||
import { createTradeProfile } from '../lib/profileApi';
|
import { createTradeProfile } from '../lib/profileApi';
|
||||||
import { BacktestRunnerPanel } from '../backtest/components/BacktestRunnerPanel';
|
import { BacktestRunnerPanel } from '../backtest/components/BacktestRunnerPanel';
|
||||||
|
import { PageHeader } from '../components/ui/page-header';
|
||||||
|
|
||||||
type ResearchTab = 'Strategies' | 'Visual Builder' | 'Code Editor' | 'Signals' | 'Backtesting';
|
type ResearchTab = 'Strategies' | 'Visual Builder' | 'Code Editor' | 'Signals' | 'Backtesting';
|
||||||
|
|
||||||
@ -35,19 +36,8 @@ function SubTab({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
style={{
|
className="tab-button"
|
||||||
padding: '8px 16px',
|
data-active={active}
|
||||||
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',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</button>
|
</button>
|
||||||
@ -93,12 +83,12 @@ export function ResearchView() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<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={{
|
<div className="tab-strip">
|
||||||
display: 'flex', gap: 4, marginBottom: 20,
|
|
||||||
borderBottom: '1px solid #E5E7EB',
|
|
||||||
}}>
|
|
||||||
{tabs.map(t => (
|
{tabs.map(t => (
|
||||||
<SubTab key={t} label={t} active={tab === t} onClick={() => setTab(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 { tradingRuntime } from '../lib/runtime';
|
||||||
import { createRequestId } from '../../../shared/request-id.js';
|
import { createRequestId } from '../../../shared/request-id.js';
|
||||||
import { SkeletonBlock } from '../components/Skeleton';
|
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 ────────────────────────────────────────────────────────────────────
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||||
interface ScreenerRow {
|
interface ScreenerRow {
|
||||||
@ -122,7 +127,7 @@ export function ScreenerView() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const SortIcon = ({ k }: { k: keyof ScreenerRow }) => (
|
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 ? '▲' : '▼') : '⇅'}
|
{sortKey === k ? (sortAsc ? '▲' : '▼') : '⇅'}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@ -137,7 +142,7 @@ export function ScreenerView() {
|
|||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: '100px 1fr 90px 90px 110px 80px 110px',
|
gridTemplateColumns: '100px 1fr 90px 90px 110px 80px 110px',
|
||||||
padding: '12px 16px',
|
padding: '12px 16px',
|
||||||
borderBottom: i < 5 ? '1px solid #F9FAFB' : 'none',
|
borderBottom: i < 5 ? '1px solid var(--border)' : 'none',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
columnGap: 0,
|
columnGap: 0,
|
||||||
}}
|
}}
|
||||||
@ -161,65 +166,46 @@ export function ScreenerView() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 20 }}>
|
<PageHeader
|
||||||
<h2 style={{ fontSize: 22, fontWeight: 800, color: '#111827', margin: 0 }}>Stock Screener</h2>
|
title="Stock Screener"
|
||||||
<div style={{ flex: 1 }} />
|
description="Filter liquid names by sector and market cap, then jump directly into charting and research."
|
||||||
<button
|
action={
|
||||||
onClick={fetchResults}
|
<Button onClick={fetchResults} disabled={loading} variant="outline" size="sm">
|
||||||
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' }} />
|
<RefreshCw size={13} style={{ animation: loading ? 'spin 1s linear infinite' : 'none' }} />
|
||||||
Refresh
|
Refresh
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Filters */}
|
<Card style={{ marginBottom: 16 }}>
|
||||||
<div style={{ display: 'flex', gap: 12, marginBottom: 16, flexWrap: 'wrap', alignItems: 'center' }}>
|
<CardContent style={{ padding: 16 }}>
|
||||||
{/* Search */}
|
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||||
<div style={{ position: 'relative', minWidth: 200, maxWidth: 280 }}>
|
<div style={{ position: 'relative', minWidth: 200, maxWidth: 280 }}>
|
||||||
<Search size={14} style={{
|
<Search size={14} style={{
|
||||||
position: 'absolute', left: 10, top: '50%',
|
position: 'absolute', left: 10, top: '50%',
|
||||||
transform: 'translateY(-50%)', color: '#9CA3AF',
|
transform: 'translateY(-50%)', color: 'var(--muted-foreground)',
|
||||||
}} />
|
}} />
|
||||||
<input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Filter by name or ticker…"
|
placeholder="Filter by name or ticker…"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={e => setQuery(e.target.value)}
|
onChange={e => setQuery(e.target.value)}
|
||||||
style={{
|
style={{ paddingLeft: 32 }}
|
||||||
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>
|
</div>
|
||||||
|
|
||||||
{/* Market cap */}
|
<Select
|
||||||
<select
|
value={String(capIdx)}
|
||||||
value={capIdx}
|
|
||||||
onChange={e => setCapIdx(Number(e.target.value))}
|
onChange={e => setCapIdx(Number(e.target.value))}
|
||||||
style={{
|
style={{ width: 180 }}
|
||||||
padding: '8px 12px', border: '1px solid #E5E7EB', borderRadius: 8,
|
|
||||||
fontSize: 12, background: '#fff', color: '#374151', fontFamily: 'inherit',
|
|
||||||
cursor: 'pointer',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{CAP_OPTIONS.map((c, i) => (
|
{CAP_OPTIONS.map((c, i) => (
|
||||||
<option key={c.label} value={i}>{c.label}</option>
|
<option key={c.label} value={i}>{c.label}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</Select>
|
||||||
|
|
||||||
{/* Sector pills */}
|
|
||||||
<div style={{ display: 'flex', gap: 5, flexWrap: 'wrap', alignItems: 'center' }}>
|
<div style={{ display: 'flex', gap: 5, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||||
<SlidersHorizontal size={13} color="#6B7280" />
|
<SlidersHorizontal size={13} color="var(--muted-foreground)" />
|
||||||
{SECTORS.slice(0, 6).map(s => (
|
{SECTORS.slice(0, 6).map(s => (
|
||||||
<button
|
<button
|
||||||
key={s}
|
key={s}
|
||||||
@ -228,59 +214,59 @@ export function ScreenerView() {
|
|||||||
padding: '5px 10px', borderRadius: 20,
|
padding: '5px 10px', borderRadius: 20,
|
||||||
border: '1px solid', fontSize: 11, fontWeight: 600,
|
border: '1px solid', fontSize: 11, fontWeight: 600,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
borderColor: sector === s ? '#2563EB' : '#E5E7EB',
|
borderColor: sector === s ? 'var(--primary)' : 'var(--border)',
|
||||||
background: sector === s ? '#EFF6FF' : '#fff',
|
background: sector === s ? 'var(--accent-soft)' : 'var(--card)',
|
||||||
color: sector === s ? '#2563EB' : '#6B7280',
|
color: sector === s ? 'var(--primary)' : 'var(--muted-foreground)',
|
||||||
fontFamily: 'inherit',
|
fontFamily: 'inherit',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{s}
|
{s}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
{/* Sector dropdown for rest */}
|
<Select
|
||||||
<select
|
|
||||||
aria-label="More sectors"
|
aria-label="More sectors"
|
||||||
value={SECTORS.indexOf(sector) >= 6 ? sector : ''}
|
value={SECTORS.indexOf(sector) >= 6 ? sector : ''}
|
||||||
onChange={e => e.target.value && setSector(e.target.value)}
|
onChange={e => e.target.value && setSector(e.target.value)}
|
||||||
style={{
|
style={{
|
||||||
padding: '5px 8px',
|
width: 140,
|
||||||
border: `1px solid ${moreSectorSelected ? '#2563EB' : '#E5E7EB'}`,
|
borderRadius: 999,
|
||||||
borderRadius: 20,
|
borderColor: moreSectorSelected ? 'var(--primary)' : 'var(--border)',
|
||||||
fontSize: 11,
|
background: moreSectorSelected ? 'var(--accent-soft)' : 'var(--card)',
|
||||||
background: moreSectorSelected ? '#EFF6FF' : '#fff',
|
color: moreSectorSelected ? 'var(--primary)' : 'var(--muted-foreground)',
|
||||||
color: moreSectorSelected ? '#2563EB' : '#6B7280',
|
|
||||||
fontWeight: moreSectorSelected ? 700 : 500,
|
fontWeight: moreSectorSelected ? 700 : 500,
|
||||||
fontFamily: 'inherit',
|
|
||||||
cursor: 'pointer',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="">More sectors…</option>
|
<option value="">More sectors…</option>
|
||||||
{SECTORS.slice(6).map(s => (
|
{SECTORS.slice(6).map(s => (
|
||||||
<option key={s} value={s}>{s}</option>
|
<option key={s} value={s}>{s}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* Error */}
|
|
||||||
{error && !loading && (
|
{error && !loading && (
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: 12, background: '#FEF2F2', border: '1px solid #FCA5A5',
|
padding: 12,
|
||||||
borderRadius: 8, fontSize: 13, color: '#DC2626', marginBottom: 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}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Results table */}
|
<div style={{ background: 'var(--card)', borderRadius: 12, border: '1px solid var(--border)', overflowX: 'auto', overflowY: 'hidden' }}>
|
||||||
<div style={{ background: '#fff', borderRadius: 12, border: '1px solid #E5E7EB', overflowX: 'auto', overflowY: 'hidden' }}>
|
|
||||||
{/* Header */}
|
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: '100px 1fr 90px 90px 110px 80px 110px',
|
gridTemplateColumns: '100px 1fr 90px 90px 110px 80px 110px',
|
||||||
padding: '10px 16px',
|
padding: '10px 16px',
|
||||||
borderBottom: '1px solid #F3F4F6',
|
borderBottom: '1px solid var(--border)',
|
||||||
background: '#F9FAFB',
|
background: 'var(--muted)',
|
||||||
}}>
|
}}>
|
||||||
{([
|
{([
|
||||||
['symbol', 'Symbol'],
|
['symbol', 'Symbol'],
|
||||||
@ -295,7 +281,7 @@ export function ScreenerView() {
|
|||||||
key={key}
|
key={key}
|
||||||
onClick={() => handleSort(key)}
|
onClick={() => handleSort(key)}
|
||||||
style={{
|
style={{
|
||||||
fontSize: 11, color: '#9CA3AF', fontWeight: 700,
|
fontSize: 11, color: 'var(--muted-foreground)', fontWeight: 700,
|
||||||
textTransform: 'uppercase', letterSpacing: '0.05em',
|
textTransform: 'uppercase', letterSpacing: '0.05em',
|
||||||
cursor: 'pointer', userSelect: 'none',
|
cursor: 'pointer', userSelect: 'none',
|
||||||
}}
|
}}
|
||||||
@ -305,10 +291,8 @@ export function ScreenerView() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Loading */}
|
|
||||||
{loading && <ScreenerSkeletonRows />}
|
{loading && <ScreenerSkeletonRows />}
|
||||||
|
|
||||||
{/* Rows */}
|
|
||||||
{!loading && filtered.map((row, i) => (
|
{!loading && filtered.map((row, i) => (
|
||||||
<div
|
<div
|
||||||
key={row.symbol}
|
key={row.symbol}
|
||||||
@ -317,16 +301,17 @@ export function ScreenerView() {
|
|||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: '100px 1fr 90px 90px 110px 80px 110px',
|
gridTemplateColumns: '100px 1fr 90px 90px 110px 80px 110px',
|
||||||
padding: '11px 16px',
|
padding: '11px 16px',
|
||||||
borderBottom: i < filtered.length - 1 ? '1px solid #F9FAFB' : 'none',
|
borderBottom: i < filtered.length - 1 ? '1px solid var(--border)' : 'none',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
alignItems: 'center',
|
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')}
|
onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}
|
||||||
>
|
>
|
||||||
<span style={{ fontSize: 13, fontWeight: 700, color: '#2563EB' }}>{row.symbol}</span>
|
<span style={{ fontSize: 13, fontWeight: 700, color: 'var(--primary)' }}>{row.symbol}</span>
|
||||||
<span style={{ fontSize: 12, color: '#374151' }}>{row.companyName}</span>
|
<span style={{ fontSize: 12, color: 'var(--foreground)' }}>{row.companyName}</span>
|
||||||
<span style={{ fontSize: 13, fontWeight: 600, color: '#111827' }}>
|
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--foreground)' }}>
|
||||||
{row.price != null ? `$${row.price.toFixed(2)}` : '—'}
|
{row.price != null ? `$${row.price.toFixed(2)}` : '—'}
|
||||||
</span>
|
</span>
|
||||||
<span style={{
|
<span style={{
|
||||||
@ -335,27 +320,27 @@ export function ScreenerView() {
|
|||||||
}}>
|
}}>
|
||||||
{row.changesPercentage >= 0 ? '+' : ''}{row.changesPercentage?.toFixed(2)}%
|
{row.changesPercentage >= 0 ? '+' : ''}{row.changesPercentage?.toFixed(2)}%
|
||||||
</span>
|
</span>
|
||||||
<span style={{ fontSize: 12, color: '#374151' }}>
|
<span style={{ fontSize: 12, color: 'var(--foreground)' }}>
|
||||||
{row.marketCap ? fmtCap(row.marketCap) : '—'}
|
{row.marketCap ? fmtCap(row.marketCap) : '—'}
|
||||||
</span>
|
</span>
|
||||||
<span style={{ fontSize: 12, color: '#374151' }}>
|
<span style={{ fontSize: 12, color: 'var(--foreground)' }}>
|
||||||
{row.pe != null && row.pe > 0 ? row.pe.toFixed(1) : '—'}
|
{row.pe != null && row.pe > 0 ? row.pe.toFixed(1) : '—'}
|
||||||
</span>
|
</span>
|
||||||
<span style={{ fontSize: 12, color: '#374151' }}>
|
<span style={{ fontSize: 12, color: 'var(--foreground)' }}>
|
||||||
{row.volume ? fmtCap(row.volume).replace('$', '') : '—'}
|
{row.volume ? fmtCap(row.volume).replace('$', '') : '—'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{!loading && filtered.length === 0 && !error && (
|
{!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
|
No results match your filters
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!loading && filtered.length > 0 && (
|
{!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
|
{filtered.length} companies · Click any row to view chart & research
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { useAppContext } from '../context/AppContext';
|
|||||||
import { SettingsTab } from '../tabs/SettingsTab';
|
import { SettingsTab } from '../tabs/SettingsTab';
|
||||||
import { AdminTab } from '../tabs/AdminTab';
|
import { AdminTab } from '../tabs/AdminTab';
|
||||||
import { ConfigTab } from '../tabs/ConfigTab';
|
import { ConfigTab } from '../tabs/ConfigTab';
|
||||||
|
import { PageHeader } from '../components/ui/page-header';
|
||||||
|
|
||||||
type SettingsSection = 'Account' | 'Bot Config' | 'Admin Panel';
|
type SettingsSection = 'Account' | 'Bot Config' | 'Admin Panel';
|
||||||
|
|
||||||
@ -17,28 +18,18 @@ export function SettingsView() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 style={{ fontSize: 22, fontWeight: 800, color: '#111827', margin: '0 0 20px' }}>Settings</h2>
|
<PageHeader
|
||||||
|
title="Settings"
|
||||||
|
description="Manage your account, credentials, and runtime configuration from one place."
|
||||||
|
/>
|
||||||
|
|
||||||
<div style={{
|
<div className="tab-strip" style={{ marginBottom: 24 }}>
|
||||||
display: 'flex', gap: 4, marginBottom: 24,
|
|
||||||
borderBottom: '1px solid #E5E7EB',
|
|
||||||
}}>
|
|
||||||
{sections.map(s => (
|
{sections.map(s => (
|
||||||
<button
|
<button
|
||||||
key={s}
|
key={s}
|
||||||
onClick={() => setSection(s)}
|
onClick={() => setSection(s)}
|
||||||
style={{
|
className="tab-button"
|
||||||
padding: '8px 16px',
|
data-active={section === s}
|
||||||
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',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{s}
|
{s}
|
||||||
</button>
|
</button>
|
||||||
@ -48,12 +39,12 @@ export function SettingsView() {
|
|||||||
<div
|
<div
|
||||||
className="settings-legacy-surface"
|
className="settings-legacy-surface"
|
||||||
style={{
|
style={{
|
||||||
background: 'linear-gradient(145deg, #101116 0%, #171922 100%)',
|
background: 'var(--card)',
|
||||||
border: '1px solid rgba(255, 255, 255, 0.08)',
|
border: '1px solid var(--border)',
|
||||||
borderRadius: 24,
|
borderRadius: 24,
|
||||||
padding: 24,
|
padding: 24,
|
||||||
boxShadow: '0 24px 70px rgba(15, 23, 42, 0.22)',
|
boxShadow: 'var(--card-shadow)',
|
||||||
color: '#F9FAFB',
|
color: 'var(--foreground)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{section === 'Account' && <SettingsTab botState={botState} />}
|
{section === 'Account' && <SettingsTab botState={botState} />}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import type { FormEvent } 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 { useAppContext } from '../context/AppContext';
|
||||||
import { fetchChartBars } from '../lib/marketApi';
|
import { fetchChartBars } from '../lib/marketApi';
|
||||||
import {
|
import {
|
||||||
@ -11,6 +11,11 @@ import {
|
|||||||
type ManualEntryPayload,
|
type ManualEntryPayload,
|
||||||
} from '../lib/manualEntriesApi';
|
} from '../lib/manualEntriesApi';
|
||||||
import { fetchTradeProfiles, type TradeProfilePayload } from '../lib/profileApi';
|
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 SimpleSide = 'buy' | 'sell';
|
||||||
type TriggerMode = 'dollar' | 'percent';
|
type TriggerMode = 'dollar' | 'percent';
|
||||||
@ -457,33 +462,19 @@ export function SimpleView() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<header className="space-y-3">
|
<PageHeader
|
||||||
<div className="flex items-center gap-3">
|
title="Simple"
|
||||||
<div className="h-12 w-12 rounded-2xl bg-[#0f2a1f] text-[#71f6a8] flex items-center justify-center">
|
description="Create saved dip-buy and profit-exit setups with the same workspace patterns used across Research, Markets, and Settings."
|
||||||
<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>
|
|
||||||
|
|
||||||
<div className="grid gap-8 xl:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)]">
|
<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)]">
|
<Card>
|
||||||
<div className="mb-6 flex items-center justify-between gap-4">
|
<CardHeader>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-black uppercase tracking-tight text-zinc-950">
|
<CardTitle className="uppercase">{editingSetupId ? 'Edit setup' : 'New setup'}</CardTitle>
|
||||||
{editingSetupId ? 'Edit setup' : 'New setup'}
|
<CardDescription>Saved trigger workflow, not an immediate broker order</CardDescription>
|
||||||
</h2>
|
|
||||||
<p className="text-xs uppercase tracking-[0.24em] text-zinc-600">
|
|
||||||
Saved trigger workflow, not an immediate broker order
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditingSetupId(null);
|
setEditingSetupId(null);
|
||||||
@ -494,17 +485,20 @@ export function SimpleView() {
|
|||||||
setMessage(null);
|
setMessage(null);
|
||||||
setError(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
|
Reset
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<label className="space-y-2">
|
<label className="space-y-2">
|
||||||
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">Symbol</span>
|
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">Symbol</span>
|
||||||
<input
|
<Input
|
||||||
value={draft.symbol}
|
value={draft.symbol}
|
||||||
onChange={(e) => setDraft((prev) => ({
|
onChange={(e) => setDraft((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@ -512,43 +506,42 @@ export function SimpleView() {
|
|||||||
currentMarketPrice: '',
|
currentMarketPrice: '',
|
||||||
}))}
|
}))}
|
||||||
placeholder="AAPL"
|
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>
|
||||||
|
|
||||||
<label className="space-y-2">
|
<label className="space-y-2">
|
||||||
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">Setup type</span>
|
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">Setup type</span>
|
||||||
<select
|
<Select
|
||||||
value={draft.side}
|
value={draft.side}
|
||||||
onChange={(e) => updateDraft('side', e.target.value as SimpleSide)}
|
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="buy">Buy the dip + profit exit</option>
|
||||||
<option value="sell">Sell existing Simple holding at profit</option>
|
<option value="sell">Sell existing Simple holding at profit</option>
|
||||||
</select>
|
</Select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-[minmax(0,1fr)_auto]">
|
<div className="grid gap-4 md:grid-cols-[minmax(0,1fr)_auto]">
|
||||||
<label className="space-y-2">
|
<label className="space-y-2">
|
||||||
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">Market price (auto-fetched)</span>
|
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">Market price (auto-fetched)</span>
|
||||||
<input
|
<Input
|
||||||
value={draft.currentMarketPrice}
|
value={draft.currentMarketPrice}
|
||||||
readOnly
|
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>
|
</label>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => void handleLoadMarketPrice()}
|
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}
|
disabled={loadingPrice || !normalizedSymbol}
|
||||||
>
|
>
|
||||||
<span className="inline-flex items-center gap-2">
|
<span className="inline-flex items-center gap-2">
|
||||||
<RefreshCw size={14} className={loadingPrice ? 'animate-spin' : ''} />
|
<RefreshCw size={14} className={loadingPrice ? 'animate-spin' : ''} />
|
||||||
Refresh
|
Refresh
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<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">
|
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-500">
|
||||||
{draft.side === 'buy' ? 'Planned quantity' : 'Holding size'}
|
{draft.side === 'buy' ? 'Planned quantity' : 'Holding size'}
|
||||||
</span>
|
</span>
|
||||||
<input
|
<Input
|
||||||
value={draft.side === 'sell' && matchingHolding ? String(matchingHolding.size) : draft.quantity}
|
value={draft.side === 'sell' && matchingHolding ? String(matchingHolding.size) : draft.quantity}
|
||||||
onChange={(e) => updateDraft('quantity', e.target.value)}
|
onChange={(e) => updateDraft('quantity', e.target.value)}
|
||||||
readOnly={draft.side === 'sell' && !!matchingHolding}
|
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"
|
placeholder="10"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label className="space-y-2">
|
<label className="space-y-2">
|
||||||
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">Notes</span>
|
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">Notes</span>
|
||||||
<input
|
<Input
|
||||||
value={draft.notes}
|
value={draft.notes}
|
||||||
onChange={(e) => updateDraft('notes', e.target.value)}
|
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"
|
placeholder="Optional context"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{draft.side === 'buy' && (
|
{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">
|
<div className="space-y-3">
|
||||||
<p className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">Drop trigger</p>
|
<p className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">Drop trigger</p>
|
||||||
<select
|
<Select
|
||||||
value={draft.dropMode}
|
value={draft.dropMode}
|
||||||
onChange={(e) => updateDraft('dropMode', e.target.value as TriggerMode)}
|
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="dollar">Dollar drop from current market</option>
|
||||||
<option value="percent">Percent drop from current market</option>
|
<option value="percent">Percent drop from current market</option>
|
||||||
</select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<label className="space-y-3">
|
<label className="space-y-3">
|
||||||
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">
|
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">
|
||||||
{draft.dropMode === 'dollar' ? 'Drop amount ($)' : 'Drop amount (%)'}
|
{draft.dropMode === 'dollar' ? 'Drop amount ($)' : 'Drop amount (%)'}
|
||||||
</span>
|
</span>
|
||||||
<input
|
<Input
|
||||||
value={draft.dropValue}
|
value={draft.dropValue}
|
||||||
onChange={(e) => updateDraft('dropValue', e.target.value)}
|
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'}
|
placeholder={draft.dropMode === 'dollar' ? '5.00' : '8'}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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">
|
<div className="space-y-3">
|
||||||
<p className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">Profit exit</p>
|
<p className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">Profit exit</p>
|
||||||
<select
|
<Select
|
||||||
value={draft.profitMode}
|
value={draft.profitMode}
|
||||||
onChange={(e) => updateDraft('profitMode', e.target.value as TriggerMode)}
|
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="dollar">Dollar gain from purchase</option>
|
||||||
<option value="percent">Percent gain from purchase</option>
|
<option value="percent">Percent gain from purchase</option>
|
||||||
</select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<label className="space-y-3">
|
<label className="space-y-3">
|
||||||
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">
|
<span className="text-[11px] font-black uppercase tracking-[0.24em] text-zinc-700">
|
||||||
{draft.profitMode === 'dollar' ? 'Profit target ($)' : 'Profit target (%)'}
|
{draft.profitMode === 'dollar' ? 'Profit target ($)' : 'Profit target (%)'}
|
||||||
</span>
|
</span>
|
||||||
<input
|
<Input
|
||||||
value={draft.profitValue}
|
value={draft.profitValue}
|
||||||
onChange={(e) => updateDraft('profitValue', e.target.value)}
|
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'}
|
placeholder={draft.profitMode === 'dollar' ? '7.50' : '10'}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
@ -631,8 +619,8 @@ export function SimpleView() {
|
|||||||
{draft.side === 'sell' && (
|
{draft.side === 'sell' && (
|
||||||
<div className={`rounded-[1.5rem] border px-4 py-4 text-sm ${
|
<div className={`rounded-[1.5rem] border px-4 py-4 text-sm ${
|
||||||
matchingHolding
|
matchingHolding
|
||||||
? 'border-[#71f6a8]/30 bg-[#0f2a1f] text-[#a4ffca]'
|
? 'border-emerald-500/20 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300'
|
||||||
: 'border-[#ef4444]/30 bg-[#261214] text-[#fca5a5]'
|
: 'border-red-500/20 bg-red-500/10 text-red-700 dark:text-red-300'
|
||||||
}`}>
|
}`}>
|
||||||
{matchingHolding
|
{matchingHolding
|
||||||
? `Simple holding ready: ${matchingHolding.symbol} · ${matchingHolding.size} shares at ${matchingHolding.entryPrice.toFixed(4)}`
|
? `Simple holding ready: ${matchingHolding.symbol} · ${matchingHolding.size} shares at ${matchingHolding.entryPrice.toFixed(4)}`
|
||||||
@ -641,44 +629,45 @@ export function SimpleView() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{previewText && (
|
{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}
|
{previewText}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{message && (
|
{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}
|
{message}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && (
|
{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}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={saveButtonDisabled}
|
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}
|
{submitting ? 'Saving...' : saveButtonLabel}
|
||||||
</button>
|
</Button>
|
||||||
</form>
|
</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)]">
|
<Card>
|
||||||
<div className="mb-6">
|
<CardHeader className="block">
|
||||||
<h2 className="text-lg font-black uppercase tracking-tight text-white">Saved setups</h2>
|
<CardTitle className="uppercase">Saved setups</CardTitle>
|
||||||
<p className="text-xs uppercase tracking-[0.24em] text-zinc-500">
|
<CardDescription>Review and update armed simple workflows in the same layout style used across the app.</CardDescription>
|
||||||
One dedicated Simple auto profile routes every triggered order
|
</CardHeader>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{savedSetups.length === 0 && (
|
{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.
|
No Simple setups saved yet.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -688,63 +677,63 @@ export function SimpleView() {
|
|||||||
const side = normalizeSetupSide(entry.simple_side);
|
const side = normalizeSetupSide(entry.simple_side);
|
||||||
const isEditing = editingSetupId === entryId;
|
const isEditing = editingSetupId === entryId;
|
||||||
return (
|
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 className="mb-3 flex items-start justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-3">
|
<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] ${
|
<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}
|
{side}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleEdit(entry)}
|
onClick={() => handleEdit(entry)}
|
||||||
className={`rounded-2xl border px-3 py-2 text-xs font-black uppercase tracking-[0.18em] ${
|
variant="outline"
|
||||||
isEditing
|
size="sm"
|
||||||
? 'border-[#71f6a8]/40 text-[#71f6a8]'
|
className={isEditing ? 'border-emerald-500/30 text-emerald-700 dark:text-emerald-300' : 'uppercase tracking-[0.18em]'}
|
||||||
: 'border-white/10 text-zinc-400 hover:border-white/20 hover:text-white'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<span className="inline-flex items-center gap-2">
|
<span className="inline-flex items-center gap-2">
|
||||||
<Pencil size={14} />
|
<Pencil size={14} />
|
||||||
Edit
|
Edit
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => void handleDelete(entryId)}
|
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">
|
<span className="inline-flex items-center gap-2">
|
||||||
<Trash2 size={14} />
|
<Trash2 size={14} />
|
||||||
Delete
|
Delete
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-3 text-[11px] uppercase tracking-[0.2em] text-zinc-500">
|
<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-white/10 px-3 py-1">
|
<span className="rounded-full border border-[var(--border)] px-3 py-1">
|
||||||
{formatSetupStatus(entry.status)}
|
{formatSetupStatus(entry.status)}
|
||||||
</span>
|
</span>
|
||||||
{entry.reference_price ? (
|
{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)}
|
Ref {Number(entry.reference_price).toFixed(4)}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
{entry.entry_price ? (
|
{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)}
|
Entry {Number(entry.entry_price).toFixed(4)}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
{entry.linked_trade_id ? (
|
{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
|
Trade linked
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
@ -753,7 +742,8 @@ export function SimpleView() {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,11 +1,15 @@
|
|||||||
import { useAppContext } from '../context/AppContext';
|
import { useAppContext } from '../context/AppContext';
|
||||||
import { EntriesTab } from '../tabs/EntriesTab';
|
import { EntriesTab } from '../tabs/EntriesTab';
|
||||||
|
import { PageHeader } from '../components/ui/page-header';
|
||||||
|
|
||||||
export function WatchlistView() {
|
export function WatchlistView() {
|
||||||
const { botState } = useAppContext();
|
const { botState } = useAppContext();
|
||||||
return (
|
return (
|
||||||
<div>
|
<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} />
|
<EntriesTab botState={botState} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user