refactor(web): extract repeated inline-style patterns to classes (UI audit #8)
Per-pattern, per-file analysis found 191 inline-style blocks across the
3 landing-view files (OverviewTab 72, MyStrategiesTab 64, HomeView 55).
155 are unique one-offs already using design tokens (var(--bl-*)) — those
stay inline since converting them to single-use classes would just relocate
code without reducing drift.
This commit extracts the **repeated patterns** (count >= 2 plus structural
container shapes) — 39 occurrences — into either Tailwind utilities or a
new web/src/styles/landing-views.css partial:
Tailwind replacements:
textAlign: 'right' → text-right
marginBottom: '32px' / '24px' → mb-8 / mb-6
fontWeight: 700 → font-bold
flex+justify-between+items-center → flex justify-between items-center
CSS partial (.lv-* classes, all token-driven):
.lv-card, .lv-card-lg, .lv-icon-tag, .lv-surface, .lv-eyebrow,
.lv-section-title, .lv-section-sub, .lv-empty-text,
.lv-divider-row, .lv-meta-faint
Per-file deltas:
tabs/OverviewTab.tsx 72 -> 53 inline blocks (19 conversions)
tabs/MyStrategiesTab.tsx 64 -> 55 inline blocks ( 9 conversions)
views/HomeView.tsx 55 -> 44 inline blocks (11 conversions)
The remaining 152 inline-style blocks are unique one-offs (one usage each)
and already token-driven — extracting them yields no drift reduction.
docs/ui/UI_AUDIT.md §5 #8 updated with this rationale.
Generated with [Devin](https://cli.devin.ai/docs)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
063afdbff6
commit
a48187da83
@ -4,6 +4,7 @@ import { Agentation } from 'agentation'
|
|||||||
import './index.css'
|
import './index.css'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
import './layout-fixes.css'
|
import './layout-fixes.css'
|
||||||
|
import './styles/landing-views.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';
|
||||||
|
|||||||
91
web/src/styles/landing-views.css
Normal file
91
web/src/styles/landing-views.css
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
/* ---------------------------------------------------------------------------
|
||||||
|
landing-views.css — extracted patterns from OverviewTab, HomeView, MyStrategiesTab
|
||||||
|
(UI audit #8, Pattern B). Keeps token-driven structural styles in CSS so they
|
||||||
|
can have @media queries and so consumers can override via class composition.
|
||||||
|
One-off styles remain inline (already var(--bl-*) driven).
|
||||||
|
--------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/* Card / panel container — used across overview metric cards, strategy
|
||||||
|
profile cards, and home preview panels. */
|
||||||
|
.lv-card {
|
||||||
|
background: var(--card);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Larger structural card — overview "tile" / strategy profile shell. */
|
||||||
|
.lv-card-lg {
|
||||||
|
background: var(--bl-surface-overlay);
|
||||||
|
border: 1px solid var(--bl-border-subtle);
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Highlight chip / icon tag — small square holder for an icon + accent. */
|
||||||
|
.lv-icon-tag {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--bl-surface-highlight);
|
||||||
|
border: 1px solid var(--bl-border-subtle);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Soft 24-radius surface — used by strategy profile sections, education panels. */
|
||||||
|
.lv-surface {
|
||||||
|
background: var(--bl-surface-highlight);
|
||||||
|
border: 1px solid var(--bl-border-subtle);
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Eyebrow / section label — small, bold, uppercase, letter-spaced. */
|
||||||
|
.lv-eyebrow {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: var(--bl-text-quiet);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section title — bold token-foreground at fixed metric size. */
|
||||||
|
.lv-section-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--foreground);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section subtitle — muted, fixed metric size. */
|
||||||
|
.lv-section-sub {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty-state text — italic, centered, padded, tertiary color. */
|
||||||
|
.lv-empty-text {
|
||||||
|
text-align: center;
|
||||||
|
padding: 16px;
|
||||||
|
color: var(--bl-text-tertiary);
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Top-bordered divider with breathing room — used for footer-like rows
|
||||||
|
inside cards (totals, last-updated stamps). */
|
||||||
|
.lv-divider-row {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tiny meta value — used for fine-print metrics next to the eyebrow. */
|
||||||
|
.lv-meta-faint {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: var(--bl-text-faint);
|
||||||
|
}
|
||||||
@ -3,24 +3,24 @@ import { useAuth } from '../components/AuthContext';
|
|||||||
import { getStrategyExplanation } from '../lib/StrategyExplanationService';
|
import { getStrategyExplanation } from '../lib/StrategyExplanationService';
|
||||||
import { getUserTier } from '../lib/TierPolicy';
|
import { getUserTier } from '../lib/TierPolicy';
|
||||||
import {
|
import {
|
||||||
Play,
|
Play,
|
||||||
Pause,
|
Pause,
|
||||||
Trash2,
|
Trash2,
|
||||||
Activity,
|
Activity,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
Plus,
|
Plus,
|
||||||
Shield,
|
Shield,
|
||||||
Zap,
|
Zap,
|
||||||
Scale,
|
Scale,
|
||||||
Settings,
|
Settings,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
Lightbulb,
|
Lightbulb,
|
||||||
Cpu,
|
Cpu,
|
||||||
Fingerprint,
|
Fingerprint,
|
||||||
Target,
|
Target,
|
||||||
DollarSign,
|
DollarSign,
|
||||||
Lock
|
Lock
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { StrategyWizard } from '../components/StrategyWizard';
|
import { StrategyWizard } from '../components/StrategyWizard';
|
||||||
import { BacktestRunnerPanel } from '../backtest/components/BacktestRunnerPanel';
|
import { BacktestRunnerPanel } from '../backtest/components/BacktestRunnerPanel';
|
||||||
@ -30,459 +30,459 @@ import { Button, IconButton } from '../components/ui/Primitives';
|
|||||||
import { CardButton } from '@bytelyst/ui';
|
import { CardButton } from '@bytelyst/ui';
|
||||||
|
|
||||||
function getStrategyKindLabel(config: any) {
|
function getStrategyKindLabel(config: any) {
|
||||||
if (config?.type === 'visual') return 'Visual Builder';
|
if (config?.type === 'visual') return 'Visual Builder';
|
||||||
if (config?.type === 'code') return 'Code Strategy';
|
if (config?.type === 'code') return 'Code Strategy';
|
||||||
return 'V4.0 Core';
|
return 'V4.0 Core';
|
||||||
}
|
}
|
||||||
|
|
||||||
const ActiveStrategyCard: React.FC<{
|
const ActiveStrategyCard: React.FC<{
|
||||||
profile: any;
|
profile: any;
|
||||||
botState: any;
|
botState: any;
|
||||||
tier: string;
|
tier: string;
|
||||||
onToggle: (p: any) => void;
|
onToggle: (p: any) => void;
|
||||||
onEdit: (p: any) => void;
|
onEdit: (p: any) => void;
|
||||||
onBacktest?: (p: any) => void;
|
onBacktest?: (p: any) => void;
|
||||||
onDelete: (id: string) => void;
|
onDelete: (id: string) => void;
|
||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
onToggleExpand: (id: string) => void;
|
onToggleExpand: (id: string) => void;
|
||||||
}> = ({ profile, botState, tier, onToggle, onEdit, onBacktest, onDelete, isExpanded, onToggleExpand }) => {
|
}> = ({ profile, botState, tier, onToggle, onEdit, onBacktest, onDelete, isExpanded, onToggleExpand }) => {
|
||||||
const config = profile.strategy_config;
|
const config = profile.strategy_config;
|
||||||
const isAggressive = config?.execution?.minRulePassRatio < 0.9;
|
const isAggressive = config?.execution?.minRulePassRatio < 0.9;
|
||||||
const isSafe = config?.execution?.minRulePassRatio >= 1.0;
|
const isSafe = config?.execution?.minRulePassRatio >= 1.0;
|
||||||
const strategyKindLabel = getStrategyKindLabel(config);
|
const strategyKindLabel = getStrategyKindLabel(config);
|
||||||
|
|
||||||
const explanation = getStrategyExplanation(profile, botState);
|
const explanation = getStrategyExplanation(profile, botState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
background: 'var(--card-elevated)',
|
background: 'var(--card-elevated)',
|
||||||
borderRadius: '28px',
|
borderRadius: '28px',
|
||||||
border: `1px solid ${profile.is_active ? 'var(--bl-success-muted)' : 'var(--bl-border-subtle)'}`,
|
border: `1px solid ${profile.is_active ? 'var(--bl-success-muted)' : 'var(--bl-border-subtle)'}`,
|
||||||
padding: '32px',
|
padding: '32px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
transition: 'all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275)',
|
transition: 'all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275)',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
minHeight: '620px'
|
minHeight: '620px'
|
||||||
}} className="strategy-card-hover">
|
}} className="strategy-card-hover">
|
||||||
|
|
||||||
{/* 1. Direct Status Strip */}
|
{/* 1. Direct Status Strip */}
|
||||||
<div style={{
|
<div style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
width: '4px',
|
width: '4px',
|
||||||
background: profile.is_active ? 'var(--bl-success)' : 'var(--bl-border-subtle)',
|
background: profile.is_active ? 'var(--bl-success)' : 'var(--bl-border-subtle)',
|
||||||
opacity: profile.is_active ? 1 : 0.3
|
opacity: profile.is_active ? 1 : 0.3
|
||||||
}} />
|
}} />
|
||||||
|
|
||||||
{/* 2. Header Area */}
|
{/* 2. Header Area */}
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '28px' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '28px' }}>
|
||||||
<div style={{ display: 'flex', gap: '14px', alignItems: 'center' }}>
|
<div style={{ display: 'flex', gap: '14px', alignItems: 'center' }}>
|
||||||
<div style={{
|
<div style={{
|
||||||
width: '44px',
|
width: '44px',
|
||||||
height: '44px',
|
height: '44px',
|
||||||
borderRadius: '14px',
|
borderRadius: '14px',
|
||||||
background: 'var(--bl-surface-highlight)',
|
background: 'var(--bl-surface-highlight)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
border: '1px solid var(--bl-border-subtle)',
|
border: '1px solid var(--bl-border-subtle)',
|
||||||
color: isSafe ? 'var(--bl-info-strong)' : isAggressive ? 'var(--bl-danger)' : 'var(--bl-success)'
|
color: isSafe ? 'var(--bl-info-strong)' : isAggressive ? 'var(--bl-danger)' : 'var(--bl-success)'
|
||||||
}}>
|
}}>
|
||||||
{isSafe ? <Shield size={20} /> : isAggressive ? <Zap size={20} /> : <Scale size={20} />}
|
{isSafe ? <Shield size={20} /> : isAggressive ? <Zap size={20} /> : <Scale size={20} />}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '2px' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '2px' }}>
|
||||||
<span style={{ fontSize: '10px', fontWeight: 900, color: 'var(--bl-text-quiet)', textTransform: 'uppercase', letterSpacing: '2px' }}>Active Strategy</span>
|
<span style={{ fontSize: '10px', fontWeight: 900, color: 'var(--bl-text-quiet)', textTransform: 'uppercase', letterSpacing: '2px' }}>Active Strategy</span>
|
||||||
<span style={{ fontSize: '13px', fontWeight: 800, color: 'white', textTransform: 'uppercase' }}>
|
<span style={{ fontSize: '13px', fontWeight: 800, color: 'white', textTransform: 'uppercase' }}>
|
||||||
{profile.is_active ? 'Running' : 'Paused'}
|
{profile.is_active ? 'Running' : 'Paused'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: '8px' }}>
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
{onBacktest && (
|
{onBacktest && (
|
||||||
<IconButton type="button" label={`Backtest ${profile.name}`} icon={<Activity size={18} />} onClick={() => onBacktest(profile)} style={{ width: '38px', height: '38px', borderRadius: '10px', background: 'var(--bl-surface-highlight)', border: '1px solid var(--bl-border-subtle)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--bl-text-quiet)', cursor: 'pointer' }} className="icon-btn-hover" />
|
<IconButton type="button" label={`Backtest ${profile.name}`} icon={<Activity size={18} />} onClick={() => onBacktest(profile)} style={{ width: '38px', height: '38px', borderRadius: '10px', background: 'var(--bl-surface-highlight)', border: '1px solid var(--bl-border-subtle)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--bl-text-quiet)', cursor: 'pointer' }} className="icon-btn-hover" />
|
||||||
)}
|
)}
|
||||||
<IconButton type="button" label={`Edit ${profile.name}`} icon={<Settings size={18} />} onClick={() => onEdit(profile)} style={{ width: '38px', height: '38px', borderRadius: '10px', background: 'var(--bl-surface-highlight)', border: '1px solid var(--bl-border-subtle)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--bl-text-quiet)', cursor: 'pointer' }} className="icon-btn-hover" />
|
<IconButton type="button" label={`Edit ${profile.name}`} icon={<Settings size={18} />} onClick={() => onEdit(profile)} style={{ width: '38px', height: '38px', borderRadius: '10px', background: 'var(--bl-surface-highlight)', border: '1px solid var(--bl-border-subtle)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--bl-text-quiet)', cursor: 'pointer' }} className="icon-btn-hover" />
|
||||||
<IconButton type="button" label={`Delete ${profile.name}`} icon={<Trash2 size={18} />} onClick={() => onDelete(profile.id)} style={{ width: '38px', height: '38px', borderRadius: '10px', background: 'var(--bl-surface-highlight)', border: '1px solid var(--bl-border-subtle)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--bl-text-quiet)', cursor: 'pointer' }} className="icon-btn-hover" />
|
<IconButton type="button" label={`Delete ${profile.name}`} icon={<Trash2 size={18} />} onClick={() => onDelete(profile.id)} style={{ width: '38px', height: '38px', borderRadius: '10px', background: 'var(--bl-surface-highlight)', border: '1px solid var(--bl-border-subtle)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--bl-text-quiet)', cursor: 'pointer' }} className="icon-btn-hover" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 3. Identity */}
|
{/* 3. Identity */}
|
||||||
<div style={{ marginBottom: '24px' }}>
|
<div className="mb-6">
|
||||||
<h3 style={{ fontSize: '24px', fontWeight: 950, color: 'white', letterSpacing: '-0.02em' }}>{profile.name}</h3>
|
<h3 style={{ fontSize: '24px', fontWeight: 950, color: 'white', letterSpacing: '-0.02em' }}>{profile.name}</h3>
|
||||||
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', marginTop: '12px' }}>
|
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', marginTop: '12px' }}>
|
||||||
<span style={{ background: 'var(--bl-success-muted)', color: 'var(--bl-success)', fontSize: '10px', fontWeight: 900, padding: '4px 10px', borderRadius: '6px', border: '1px solid var(--bl-success-muted)', textTransform: 'uppercase' }}>
|
<span style={{ background: 'var(--bl-success-muted)', color: 'var(--bl-success)', fontSize: '10px', fontWeight: 900, padding: '4px 10px', borderRadius: '6px', border: '1px solid var(--bl-success-muted)', textTransform: 'uppercase' }}>
|
||||||
{profile.symbols}
|
{profile.symbols}
|
||||||
</span>
|
</span>
|
||||||
<span style={{ background: 'var(--bl-surface-highlight)', color: 'var(--bl-text-quiet)', fontSize: '10px', fontWeight: 900, padding: '4px 10px', borderRadius: '6px', border: '1px solid var(--bl-border-subtle)', textTransform: 'uppercase' }}>
|
<span style={{ background: 'var(--bl-surface-highlight)', color: 'var(--bl-text-quiet)', fontSize: '10px', fontWeight: 900, padding: '4px 10px', borderRadius: '6px', border: '1px solid var(--bl-border-subtle)', textTransform: 'uppercase' }}>
|
||||||
{strategyKindLabel}
|
{strategyKindLabel}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 4. Operational DNA (Specs) */}
|
{/* 4. Operational DNA (Specs) */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(0, 1fr)', gap: '14px', marginBottom: '28px' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(0, 1fr)', gap: '14px', marginBottom: '28px' }}>
|
||||||
{[
|
{[
|
||||||
{ label: 'Allocation', value: `$${profile.allocated_capital.toLocaleString()}`, icon: <DollarSign size={14} />, color: 'var(--bl-success)' },
|
{ label: 'Allocation', value: `$${profile.allocated_capital.toLocaleString()}`, icon: <DollarSign size={14} />, color: 'var(--bl-success)' },
|
||||||
{ label: 'PnL (Global)', value: '$0.00', icon: <TrendingUp size={14} />, color: 'var(--bl-info-strong)' },
|
{ label: 'PnL (Global)', value: '$0.00', icon: <TrendingUp size={14} />, color: 'var(--bl-info-strong)' },
|
||||||
{ label: 'Target', value: `$${config?.riskLimits?.dailyProfitTargetUsd || 0}`, icon: <Target size={14} />, color: 'var(--bl-warning)' },
|
{ label: 'Target', value: `$${config?.riskLimits?.dailyProfitTargetUsd || 0}`, icon: <Target size={14} />, color: 'var(--bl-warning)' },
|
||||||
{ label: 'Latency', value: '5ms', icon: <Cpu size={14} />, color: 'var(--bl-danger)' }
|
{ label: 'Latency', value: '5ms', icon: <Cpu size={14} />, color: 'var(--bl-danger)' }
|
||||||
].map((spec, i) => (
|
].map((spec, i) => (
|
||||||
<div key={i} style={{
|
<div key={i} style={{
|
||||||
background: 'var(--bl-surface-overlay)',
|
background: 'var(--bl-surface-overlay)',
|
||||||
border: '1px solid var(--bl-border-subtle)',
|
border: '1px solid var(--bl-border-subtle)',
|
||||||
padding: '16px',
|
padding: '16px',
|
||||||
borderRadius: '20px',
|
borderRadius: '20px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: '6px'
|
gap: '6px'
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: 'var(--bl-text-quiet)', fontSize: '10px', fontWeight: 900, textTransform: 'uppercase' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: 'var(--bl-text-quiet)', fontSize: '10px', fontWeight: 900, textTransform: 'uppercase' }}>
|
||||||
{spec.icon} {spec.label}
|
{spec.icon} {spec.label}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ color: 'white', fontWeight: 900, fontSize: '16px' }}>{spec.value}</div>
|
<div style={{ color: 'white', fontWeight: 900, fontSize: '16px' }}>{spec.value}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 5. Health Diagnostic (Education Layer) */}
|
{/* 5. Health Diagnostic (Education Layer) */}
|
||||||
<div style={{ marginBottom: '32px' }}>
|
<div className="mb-8">
|
||||||
<CardButton
|
<CardButton
|
||||||
onClick={() => onToggleExpand(profile.id)}
|
onClick={() => onToggleExpand(profile.id)}
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
padding: '16px',
|
padding: '16px',
|
||||||
borderRadius: '20px',
|
borderRadius: '20px',
|
||||||
background: 'var(--bl-surface-highlight)',
|
background: 'var(--bl-surface-highlight)',
|
||||||
border: '1px solid var(--bl-border-subtle)',
|
border: '1px solid var(--bl-border-subtle)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: '8px',
|
gap: '8px',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
textAlign: 'left'
|
textAlign: 'left'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div className="flex justify-between items-center">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: 'var(--bl-success)', fontSize: '10px', fontWeight: 900, textTransform: 'uppercase' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: 'var(--bl-success)', fontSize: '10px', fontWeight: 900, textTransform: 'uppercase' }}>
|
||||||
<Fingerprint size={14} /> Diagnostic Intelligence {tier === 'free' && <Lock size={12} style={{ marginLeft: '4px', opacity: 0.5 }} />}
|
<Fingerprint size={14} /> Diagnostic Intelligence {tier === 'free' && <Lock size={12} style={{ marginLeft: '4px', opacity: 0.5 }} />}
|
||||||
</div>
|
</div>
|
||||||
{isExpanded ? <ChevronUp size={14} color="var(--bl-text-quiet)" /> : <ChevronDown size={14} color="var(--bl-text-quiet)" />}
|
{isExpanded ? <ChevronUp size={14} color="var(--bl-text-quiet)" /> : <ChevronDown size={14} color="var(--bl-text-quiet)" />}
|
||||||
</div>
|
</div>
|
||||||
<p style={{ fontSize: '12px', color: 'var(--bl-text-secondary)', margin: 0, fontWeight: 500 }}>
|
<p style={{ fontSize: '12px', color: 'var(--bl-text-secondary)', margin: 0, fontWeight: 500 }}>
|
||||||
{explanation.reason}
|
{explanation.reason}
|
||||||
</p>
|
</p>
|
||||||
{isExpanded && explanation.recommendation && (
|
{isExpanded && explanation.recommendation && (
|
||||||
<div style={{
|
<div style={{
|
||||||
marginTop: '12px',
|
marginTop: '12px',
|
||||||
padding: '12px',
|
padding: '12px',
|
||||||
background: 'var(--bl-warning-muted)',
|
background: 'var(--bl-warning-muted)',
|
||||||
border: '1px solid var(--bl-warning-muted)',
|
border: '1px solid var(--bl-warning-muted)',
|
||||||
borderRadius: '12px'
|
borderRadius: '12px'
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', color: 'var(--bl-warning)', fontSize: '10px', fontWeight: 900, textTransform: 'uppercase', marginBottom: '4px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', color: 'var(--bl-warning)', fontSize: '10px', fontWeight: 900, textTransform: 'uppercase', marginBottom: '4px' }}>
|
||||||
<Lightbulb size={12} /> Optimization
|
<Lightbulb size={12} /> Optimization
|
||||||
</div>
|
</div>
|
||||||
<p style={{ fontSize: '11px', color: 'var(--bl-warning)', margin: 0, fontStyle: 'italic', fontWeight: 600 }}>{explanation.recommendation}</p>
|
<p style={{ fontSize: '11px', color: 'var(--bl-warning)', margin: 0, fontStyle: 'italic', fontWeight: 600 }}>{explanation.recommendation}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardButton>
|
</CardButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 6. Action */}
|
{/* 6. Action */}
|
||||||
<div style={{ marginTop: 'auto' }}>
|
<div style={{ marginTop: 'auto' }}>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onToggle(profile)}
|
onClick={() => onToggle(profile)}
|
||||||
variant={profile.is_active ? 'outline' : 'primary'}
|
variant={profile.is_active ? 'outline' : 'primary'}
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '56px',
|
height: '56px',
|
||||||
background: profile.is_active ? 'var(--bl-surface-highlight)' : 'var(--bl-success)',
|
background: profile.is_active ? 'var(--bl-surface-highlight)' : 'var(--bl-success)',
|
||||||
color: profile.is_active ? 'var(--bl-text-secondary)' : 'black',
|
color: profile.is_active ? 'var(--bl-text-secondary)' : 'black',
|
||||||
borderRadius: '18px',
|
borderRadius: '18px',
|
||||||
border: profile.is_active ? '1px solid var(--bl-border-subtle)' : 'none',
|
border: profile.is_active ? '1px solid var(--bl-border-subtle)' : 'none',
|
||||||
fontWeight: 900,
|
fontWeight: 900,
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
gap: '12px',
|
gap: '12px',
|
||||||
boxShadow: profile.is_active ? 'none' : '0 12px 36px -12px color-mix(in oklab, var(--bl-success) 40%, transparent)',
|
boxShadow: profile.is_active ? 'none' : '0 12px 36px -12px color-mix(in oklab, var(--bl-success) 40%, transparent)',
|
||||||
transition: 'all 0.2s',
|
transition: 'all 0.2s',
|
||||||
letterSpacing: '1.5px'
|
letterSpacing: '1.5px'
|
||||||
}}
|
}}
|
||||||
className="action-btn-hover"
|
className="action-btn-hover"
|
||||||
>
|
>
|
||||||
{profile.is_active ? (
|
{profile.is_active ? (
|
||||||
<><Pause size={18} fill="currentColor" /> PAUSE TRADING</>
|
<><Pause size={18} fill="currentColor" /> PAUSE TRADING</>
|
||||||
) : (
|
) : (
|
||||||
<><Play size={18} fill="currentColor" /> START TRADING</>
|
<><Play size={18} fill="currentColor" /> START TRADING</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>{`
|
<style>{`
|
||||||
.strategy-card-hover:hover {
|
.strategy-card-hover:hover {
|
||||||
border-color: var(--bl-success-muted) !important;
|
border-color: var(--bl-success-muted) !important;
|
||||||
transform: translateY(-8px);
|
transform: translateY(-8px);
|
||||||
box-shadow: 0 40px 80px -20px color-mix(in oklab, var(--background) 80%, black) !important;
|
box-shadow: 0 40px 80px -20px color-mix(in oklab, var(--background) 80%, black) !important;
|
||||||
background: var(--bl-surface-strong) !important;
|
background: var(--bl-surface-strong) !important;
|
||||||
}
|
}
|
||||||
.icon-btn-hover:hover {
|
.icon-btn-hover:hover {
|
||||||
background: color-mix(in oklab, var(--card) 88%, var(--foreground)) !important;
|
background: color-mix(in oklab, var(--card) 88%, var(--foreground)) !important;
|
||||||
color: white !important;
|
color: white !important;
|
||||||
}
|
}
|
||||||
.action-btn-hover:hover {
|
.action-btn-hover:hover {
|
||||||
filter: brightness(1.1);
|
filter: brightness(1.1);
|
||||||
transform: scale(1.02);
|
transform: scale(1.02);
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewAsCustomer?: boolean }> = ({
|
export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewAsCustomer?: boolean }> = ({
|
||||||
botState,
|
botState,
|
||||||
alerts = [],
|
alerts = [],
|
||||||
previewAsCustomer = false
|
previewAsCustomer = false
|
||||||
}) => {
|
}) => {
|
||||||
const { user, profile: userProfile } = useAuth();
|
const { user, profile: userProfile } = useAuth();
|
||||||
const [profiles, setProfiles] = useState<any[]>([]);
|
const [profiles, setProfiles] = useState<any[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [showWizard, setShowWizard] = useState(false);
|
const [showWizard, setShowWizard] = useState(false);
|
||||||
const [editingProfile, setEditingProfile] = useState<any>(null);
|
const [editingProfile, setEditingProfile] = useState<any>(null);
|
||||||
const [expandedExplanations, setExpandedExplanations] = useState<Record<string, boolean>>({});
|
const [expandedExplanations, setExpandedExplanations] = useState<Record<string, boolean>>({});
|
||||||
const [backtestProfile, setBacktestProfile] = useState<any>(null);
|
const [backtestProfile, setBacktestProfile] = useState<any>(null);
|
||||||
|
|
||||||
const tier = getUserTier(userProfile);
|
const tier = getUserTier(userProfile);
|
||||||
const { enabled: backtestEnabled, loading: backtestGateLoading } = useBacktestFeatureGate({ previewAsCustomer });
|
const { enabled: backtestEnabled, loading: backtestGateLoading } = useBacktestFeatureGate({ previewAsCustomer });
|
||||||
|
|
||||||
const fetchProfiles = async () => {
|
const fetchProfiles = async () => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const data = await fetchTradeProfiles();
|
const data = await fetchTradeProfiles();
|
||||||
setProfiles(data || []);
|
setProfiles(data || []);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchProfiles();
|
fetchProfiles();
|
||||||
window.addEventListener('profiles-updated', fetchProfiles);
|
window.addEventListener('profiles-updated', fetchProfiles);
|
||||||
return () => window.removeEventListener('profiles-updated', fetchProfiles);
|
return () => window.removeEventListener('profiles-updated', fetchProfiles);
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
const toggleBot = async (profile: any) => {
|
const toggleBot = async (profile: any) => {
|
||||||
try {
|
try {
|
||||||
await setTradeProfileActive(profile.id, !profile.is_active);
|
await setTradeProfileActive(profile.id, !profile.is_active);
|
||||||
fetchProfiles();
|
fetchProfiles();
|
||||||
} catch {
|
} catch {
|
||||||
// existing UI remains silent on toggle failure
|
// existing UI remains silent on toggle failure
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteBot = async (id: string) => {
|
const deleteBot = async (id: string) => {
|
||||||
if (!confirm('Are you sure you want to delete this strategy?')) return;
|
if (!confirm('Are you sure you want to delete this strategy?')) return;
|
||||||
try {
|
try {
|
||||||
await deleteTradeProfile(id);
|
await deleteTradeProfile(id);
|
||||||
fetchProfiles();
|
fetchProfiles();
|
||||||
} catch {
|
} catch {
|
||||||
// existing UI remains silent on delete failure
|
// existing UI remains silent on delete failure
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (showWizard) {
|
if (showWizard) {
|
||||||
return (
|
return (
|
||||||
<div style={{ animation: 'fadeIn 0.5s ease-out' }}>
|
<div style={{ animation: 'fadeIn 0.5s ease-out' }}>
|
||||||
<div style={{ marginBottom: '24px' }}>
|
<div className="mb-6">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowWizard(false);
|
setShowWizard(false);
|
||||||
setEditingProfile(null);
|
setEditingProfile(null);
|
||||||
}}
|
}}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
style={{ background: 'none', border: 'none', color: 'var(--bl-text-quiet)', fontWeight: 900, fontSize: '12px', textTransform: 'uppercase', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px' }}
|
style={{ background: 'none', border: 'none', color: 'var(--bl-text-quiet)', fontWeight: 900, fontSize: '12px', textTransform: 'uppercase', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px' }}
|
||||||
>
|
>
|
||||||
← Back to My Strategies
|
← Back to My Strategies
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<StrategyWizard
|
<StrategyWizard
|
||||||
editingProfile={editingProfile}
|
editingProfile={editingProfile}
|
||||||
profile={userProfile}
|
profile={userProfile}
|
||||||
previewAsCustomer={previewAsCustomer}
|
previewAsCustomer={previewAsCustomer}
|
||||||
onComplete={() => {
|
onComplete={() => {
|
||||||
setShowWizard(false);
|
setShowWizard(false);
|
||||||
setEditingProfile(null);
|
setEditingProfile(null);
|
||||||
fetchProfiles();
|
fetchProfiles();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="strategy-workspace">
|
<div className="strategy-workspace">
|
||||||
<div className="strategy-workspace-header">
|
<div className="strategy-workspace-header">
|
||||||
<div>
|
<div>
|
||||||
<div className="strategy-workspace-eyebrow">
|
<div className="strategy-workspace-eyebrow">
|
||||||
Strategy operations
|
Strategy operations
|
||||||
</div>
|
</div>
|
||||||
<h2>My strategies</h2>
|
<h2>My strategies</h2>
|
||||||
<p>
|
<p>
|
||||||
Monitor active profiles, review recent signals, and create new automated trading workflows.
|
Monitor active profiles, review recent signals, and create new automated trading workflows.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="strategy-workspace-actions">
|
<div className="strategy-workspace-actions">
|
||||||
<div className="strategy-connection-pill" data-connected={botState?.connected ? 'true' : 'false'}>
|
<div className="strategy-connection-pill" data-connected={botState?.connected ? 'true' : 'false'}>
|
||||||
{botState?.connected ? 'Systems online' : 'Systems disconnected'}
|
{botState?.connected ? 'Systems online' : 'Systems disconnected'}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowWizard(true)}
|
onClick={() => setShowWizard(true)}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
>
|
>
|
||||||
<Plus size={16} /> New strategy
|
<Plus size={16} /> New strategy
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Contextual Intelligence Row: Recent Activity + Symbol Volatility */}
|
{/* Contextual Intelligence Row: Recent Activity + Symbol Volatility */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const activeSymbols = [...new Set(profiles.flatMap(p => p.symbols?.split(',').map((s: string) => s.trim()) || []))];
|
const activeSymbols = [...new Set(profiles.flatMap(p => p.symbols?.split(',').map((s: string) => s.trim()) || []))];
|
||||||
const recentAlerts = [...alerts].reverse().slice(0, 5);
|
const recentAlerts = [...alerts].reverse().slice(0, 5);
|
||||||
const symbolVolatility = activeSymbols
|
const symbolVolatility = activeSymbols
|
||||||
.filter(s => botState?.symbols?.[s])
|
.filter(s => botState?.symbols?.[s])
|
||||||
.map(s => ({ symbol: s, change: botState.symbols[s].change24h || 0 }))
|
.map(s => ({ symbol: s, change: botState.symbols[s].change24h || 0 }))
|
||||||
.sort((a, b) => Math.abs(b.change) - Math.abs(a.change));
|
.sort((a, b) => Math.abs(b.change) - Math.abs(a.change));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(0, 1fr)', gap: '20px', marginBottom: '48px' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(0, 1fr)', gap: '20px', marginBottom: '48px' }}>
|
||||||
{/* Recent Activity */}
|
{/* Recent Activity */}
|
||||||
<div style={{ background: 'var(--bl-surface-highlight)', border: '1px solid var(--bl-border-subtle)', borderRadius: '24px', padding: '24px 28px' }}>
|
<div style={{ background: 'var(--bl-surface-highlight)', border: '1px solid var(--bl-border-subtle)', borderRadius: '24px', padding: '24px 28px' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '16px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '16px' }}>
|
||||||
<div style={{ width: '8px', height: '8px', borderRadius: '50%', background: 'var(--bl-success)', boxShadow: '0 0 8px var(--bl-success)' }} />
|
<div style={{ width: '8px', height: '8px', borderRadius: '50%', background: 'var(--bl-success)', boxShadow: '0 0 8px var(--bl-success)' }} />
|
||||||
<span style={{ fontSize: '11px', fontWeight: 900, color: 'var(--bl-text-quiet)', textTransform: 'uppercase', letterSpacing: '2px' }}>Recent Activity</span>
|
<span className="lv-eyebrow">Recent Activity</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||||||
{recentAlerts.map((alert, i) => {
|
{recentAlerts.map((alert, i) => {
|
||||||
const mins = Math.floor((Date.now() - alert.timestamp) / 60000);
|
const mins = Math.floor((Date.now() - alert.timestamp) / 60000);
|
||||||
const timeAgo = mins < 1 ? 'just now' : mins < 60 ? `${mins}m ago` : `${Math.floor(mins / 60)}h ago`;
|
const timeAgo = mins < 1 ? 'just now' : mins < 60 ? `${mins}m ago` : `${Math.floor(mins / 60)}h ago`;
|
||||||
return (
|
return (
|
||||||
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: '10px', padding: '8px 12px', background: 'var(--bl-surface-overlay)', borderRadius: '10px' }}>
|
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: '10px', padding: '8px 12px', background: 'var(--bl-surface-overlay)', borderRadius: '10px' }}>
|
||||||
<Activity size={13} color="var(--bl-info)" />
|
<Activity size={13} color="var(--bl-info)" />
|
||||||
<span style={{ fontSize: '12px', color: 'var(--bl-text-quiet)', fontWeight: 700, minWidth: '60px' }}>{alert.symbol}</span>
|
<span style={{ fontSize: '12px', color: 'var(--bl-text-quiet)', fontWeight: 700, minWidth: '60px' }}>{alert.symbol}</span>
|
||||||
<span style={{ fontSize: '11px', color: 'var(--bl-text-quiet)', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{alert.message}</span>
|
<span style={{ fontSize: '11px', color: 'var(--bl-text-quiet)', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{alert.message}</span>
|
||||||
<span style={{ fontSize: '10px', color: 'var(--bl-text-tertiary)', fontWeight: 700, whiteSpace: 'nowrap' }}>{timeAgo}</span>
|
<span style={{ fontSize: '10px', color: 'var(--bl-text-tertiary)', fontWeight: 700, whiteSpace: 'nowrap' }}>{timeAgo}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{recentAlerts.length === 0 && <div style={{ textAlign: 'center', padding: '16px', color: 'var(--bl-text-tertiary)', fontSize: '12px', fontStyle: 'italic' }}>No activity yet...</div>}
|
{recentAlerts.length === 0 && <div className="lv-empty-text">No activity yet...</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Symbol-Specific Volatility */}
|
{/* Symbol-Specific Volatility */}
|
||||||
<div style={{ background: 'var(--bl-surface-highlight)', border: '1px solid var(--bl-border-subtle)', borderRadius: '24px', padding: '24px 28px' }}>
|
<div style={{ background: 'var(--bl-surface-highlight)', border: '1px solid var(--bl-border-subtle)', borderRadius: '24px', padding: '24px 28px' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '16px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '16px' }}>
|
||||||
<TrendingUp size={15} color="var(--bl-info)" />
|
<TrendingUp size={15} color="var(--bl-info)" />
|
||||||
<span style={{ fontSize: '11px', fontWeight: 900, color: 'var(--bl-text-quiet)', textTransform: 'uppercase', letterSpacing: '2px' }}>Your Markets (24h)</span>
|
<span className="lv-eyebrow">Your Markets (24h)</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||||
{symbolVolatility.map(({ symbol, change }) => (
|
{symbolVolatility.map(({ symbol, change }) => (
|
||||||
<div key={symbol} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 14px', background: 'var(--bl-surface-overlay)', borderRadius: '12px' }}>
|
<div key={symbol} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 14px', background: 'var(--bl-surface-overlay)', borderRadius: '12px' }}>
|
||||||
<span style={{ fontWeight: 800, fontSize: '13px', color: 'white' }}>{symbol}</span>
|
<span style={{ fontWeight: 800, fontSize: '13px', color: 'white' }}>{symbol}</span>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||||
<div style={{ width: '60px', height: '3px', background: 'var(--bl-border-subtle)', borderRadius: '99px', overflow: 'hidden' }}>
|
<div style={{ width: '60px', height: '3px', background: 'var(--bl-border-subtle)', borderRadius: '99px', overflow: 'hidden' }}>
|
||||||
<div style={{ width: `${Math.min(Math.abs(change) * 5, 60)}px`, height: '100%', background: change >= 0 ? 'var(--bl-success)' : 'var(--bl-danger)', borderRadius: '99px' }} />
|
<div style={{ width: `${Math.min(Math.abs(change) * 5, 60)}px`, height: '100%', background: change >= 0 ? 'var(--bl-success)' : 'var(--bl-danger)', borderRadius: '99px' }} />
|
||||||
</div>
|
</div>
|
||||||
<span style={{ fontSize: '12px', fontWeight: 900, fontFamily: 'monospace', color: change >= 0 ? 'var(--bl-success)' : 'var(--bl-danger)', minWidth: '55px', textAlign: 'right' }}>
|
<span style={{ fontSize: '12px', fontWeight: 900, fontFamily: 'monospace', color: change >= 0 ? 'var(--bl-success)' : 'var(--bl-danger)', minWidth: '55px', textAlign: 'right' }}>
|
||||||
{change >= 0 ? '+' : ''}{change.toFixed(2)}%
|
{change >= 0 ? '+' : ''}{change.toFixed(2)}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{symbolVolatility.length === 0 && <div style={{ textAlign: 'center', padding: '16px', color: 'var(--bl-text-tertiary)', fontSize: '12px', fontStyle: 'italic' }}>Deploy a strategy to see its market data</div>}
|
{symbolVolatility.length === 0 && <div className="lv-empty-text">Deploy a strategy to see its market data</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: 'repeat(auto-fill, minmax(350px, 1fr))',
|
gridTemplateColumns: 'repeat(auto-fill, minmax(350px, 1fr))',
|
||||||
gap: '40px',
|
gap: '40px',
|
||||||
width: '100%'
|
width: '100%'
|
||||||
}}>
|
}}>
|
||||||
{!backtestGateLoading && backtestEnabled && backtestProfile && (
|
{!backtestGateLoading && backtestEnabled && backtestProfile && (
|
||||||
<div style={{ gridColumn: '1 / -1' }}>
|
<div style={{ gridColumn: '1 / -1' }}>
|
||||||
<BacktestRunnerPanel
|
<BacktestRunnerPanel
|
||||||
profileId={backtestProfile.id}
|
profileId={backtestProfile.id}
|
||||||
strategyConfig={backtestProfile.strategy_config}
|
strategyConfig={backtestProfile.strategy_config}
|
||||||
symbols={String(backtestProfile.symbols || '').split(',').map((s: string) => s.trim()).filter(Boolean)}
|
symbols={String(backtestProfile.symbols || '').split(',').map((s: string) => s.trim()).filter(Boolean)}
|
||||||
initialCapitalUsd={Number(backtestProfile.allocated_capital || 1000)}
|
initialCapitalUsd={Number(backtestProfile.allocated_capital || 1000)}
|
||||||
title={`Backtest: ${backtestProfile.name}`}
|
title={`Backtest: ${backtestProfile.name}`}
|
||||||
onClose={() => setBacktestProfile(null)}
|
onClose={() => setBacktestProfile(null)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{profiles.map(profile => (
|
{profiles.map(profile => (
|
||||||
<ActiveStrategyCard
|
<ActiveStrategyCard
|
||||||
key={profile.id}
|
key={profile.id}
|
||||||
profile={profile}
|
profile={profile}
|
||||||
botState={botState}
|
botState={botState}
|
||||||
tier={tier}
|
tier={tier}
|
||||||
onToggle={toggleBot}
|
onToggle={toggleBot}
|
||||||
onEdit={(p) => {
|
onEdit={(p) => {
|
||||||
setEditingProfile(p);
|
setEditingProfile(p);
|
||||||
setShowWizard(true);
|
setShowWizard(true);
|
||||||
}}
|
}}
|
||||||
onBacktest={!backtestGateLoading && backtestEnabled ? ((p) => setBacktestProfile(p)) : undefined}
|
onBacktest={!backtestGateLoading && backtestEnabled ? ((p) => setBacktestProfile(p)) : undefined}
|
||||||
onDelete={deleteBot}
|
onDelete={deleteBot}
|
||||||
isExpanded={!!expandedExplanations[profile.id]}
|
isExpanded={!!expandedExplanations[profile.id]}
|
||||||
onToggleExpand={(id) => setExpandedExplanations(prev => ({ ...prev, [id]: !prev[id] }))}
|
onToggleExpand={(id) => setExpandedExplanations(prev => ({ ...prev, [id]: !prev[id] }))}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{profiles.length === 0 && !isLoading && (
|
{profiles.length === 0 && !isLoading && (
|
||||||
<div style={{
|
<div style={{
|
||||||
gridColumn: '1 / -1',
|
gridColumn: '1 / -1',
|
||||||
padding: '100px',
|
padding: '100px',
|
||||||
background: 'var(--bl-surface-highlight)',
|
background: 'var(--bl-surface-highlight)',
|
||||||
border: '2px dashed var(--bl-border-subtle)',
|
border: '2px dashed var(--bl-border-subtle)',
|
||||||
borderRadius: '40px',
|
borderRadius: '40px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
textAlign: 'center'
|
textAlign: 'center'
|
||||||
}}>
|
}}>
|
||||||
<Activity size={60} color="var(--bl-text-quiet)" style={{ marginBottom: '24px' }} />
|
<Activity size={60} color="var(--bl-text-quiet)" className="mb-6" />
|
||||||
<h3 style={{ color: 'var(--bl-text-quiet)', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '4px' }}>No strategies yet</h3>
|
<h3 style={{ color: 'var(--bl-text-quiet)', fontWeight: 900, textTransform: 'uppercase', letterSpacing: '4px' }}>No strategies yet</h3>
|
||||||
<p style={{ color: 'var(--bl-text-quiet)', fontSize: '14px', marginTop: '12px', maxWidth: '300px' }}>Create your first strategy to start monitoring markets and testing automated execution.</p>
|
<p style={{ color: 'var(--bl-text-quiet)', fontSize: '14px', marginTop: '12px', maxWidth: '300px' }}>Create your first strategy to start monitoring markets and testing automated execution.</p>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowWizard(true)}
|
onClick={() => setShowWizard(true)}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
style={{ marginTop: '32px', background: 'white', border: 'none', color: 'black', padding: '16px 40px', borderRadius: '16px', fontWeight: 900, cursor: 'pointer' }}
|
style={{ marginTop: '32px', background: 'white', border: 'none', color: 'black', padding: '16px 40px', borderRadius: '16px', fontWeight: 900, cursor: 'pointer' }}
|
||||||
>
|
>
|
||||||
GET STARTED
|
GET STARTED
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>{`
|
<style>{`
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from { opacity: 0; transform: translateY(20px); }
|
from { opacity: 0; transform: translateY(20px); }
|
||||||
to { opacity: 1; transform: translateY(0); }
|
to { opacity: 1; transform: translateY(0); }
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user