fix(web): convert <Button> picker cards to native <button> (UI audit #5)

The @bytelyst/ui Button primitive applies whitespace-nowrap + fixed
h-{size} which collapses multi-line stacked content (Pattern A in
docs/ui/UI_AUDIT.md). Affected sites use Button as a card-shaped
option/action picker with stacked title + description spans.

Converted to native <button> + new .card-button utility class:
- StrategyWizard:188 — risk style picker (3 cards)
- StrategyWizard:342 — trading hours picker
- SimpleView:1009 — "new short-term buy plan" card
- SimpleView:1028 — "manage existing holding" card
- MyStrategiesTab:153 — diagnostic accordion toggle

Adds layout-fixes.css §25 .card-button — resets button defaults,
preserves focus-visible ring tied to --bl-focus-ring/--bl-accent.

Skipped: ChatControl:543 (FAB) — single-icon child, not affected.

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:
Devin 2026-05-10 09:02:54 +00:00
parent 1807dc0d30
commit 67c9ecb589
4 changed files with 542 additions and 513 deletions

View File

@ -1,19 +1,19 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { import {
ShieldCheck, ShieldCheck,
Scale, Scale,
Zap, Zap,
DollarSign, DollarSign,
Clock, Clock,
ChevronRight, ChevronRight,
ChevronLeft, ChevronLeft,
CheckCircle2, CheckCircle2,
AlertTriangle, AlertTriangle,
Info, Info,
Wallet, Wallet,
Target, Target,
Lock Lock
} from 'lucide-react'; } from 'lucide-react';
import { RISK_STYLE_TEMPLATES } from '../lib/RiskStyleTemplates'; import { RISK_STYLE_TEMPLATES } from '../lib/RiskStyleTemplates';
import type { RiskStyleTemplate } from '../lib/RiskStyleTemplates'; import type { RiskStyleTemplate } from '../lib/RiskStyleTemplates';
import { getUserTier, TIER_POLICIES, isFeatureAllowed } from '../lib/TierPolicy'; import { getUserTier, TIER_POLICIES, isFeatureAllowed } from '../lib/TierPolicy';
@ -24,22 +24,22 @@ import { createTradeProfile, updateTradeProfile } from '../lib/profileApi';
import { Button } from './ui/button'; import { Button } from './ui/button';
import { Card } from './ui/card'; import { Card } from './ui/card';
import { Input } from './ui/input'; import { Input } from './ui/input';
interface WizardState { interface WizardState {
step: number; step: number;
riskStyle: RiskStyleTemplate | null; riskStyle: RiskStyleTemplate | null;
assets: string[]; assets: string[];
capital: number; capital: number;
profitTarget: number; profitTarget: number;
hours: '24/7' | 'London + New York' | 'Asia only'; hours: '24/7' | 'London + New York' | 'Asia only';
} }
const ASSETS = [ const ASSETS = [
{ id: 'BTC/USDT', label: 'Bitcoin (BTC)' }, { id: 'BTC/USDT', label: 'Bitcoin (BTC)' },
{ id: 'ETH/USDT', label: 'Ethereum (ETH)' }, { id: 'ETH/USDT', label: 'Ethereum (ETH)' },
{ id: 'SOL/USDT', label: 'Solana (SOL)' }, { id: 'SOL/USDT', label: 'Solana (SOL)' },
]; ];
const SESSION_MAP = { const SESSION_MAP = {
'24/7': 'LDN,NY,TOK,SYD', '24/7': 'LDN,NY,TOK,SYD',
'London + New York': 'LDN,NY', 'London + New York': 'LDN,NY',
@ -75,89 +75,89 @@ const buildStrategyConfig = (state: WizardState) => ({
entryMode: 'both' entryMode: 'both'
} }
}); });
export const StrategyWizard: React.FC<{ export const StrategyWizard: React.FC<{
onComplete: () => void; onComplete: () => void;
editingProfile?: any; editingProfile?: any;
profile?: any; profile?: any;
previewAsCustomer?: boolean; previewAsCustomer?: boolean;
}> = ({ onComplete, editingProfile, profile: userProfile, previewAsCustomer = false }) => { }> = ({ onComplete, editingProfile, profile: userProfile, previewAsCustomer = false }) => {
const { user } = useAuth(); const { user } = useAuth();
const tier = getUserTier(userProfile); const tier = getUserTier(userProfile);
const policy = TIER_POLICIES[tier]; const policy = TIER_POLICIES[tier];
// Initialize state from existing profile if editing // Initialize state from existing profile if editing
const getInitialState = (): WizardState => { const getInitialState = (): WizardState => {
if (editingProfile) { if (editingProfile) {
const config = editingProfile.strategy_config; const config = editingProfile.strategy_config;
const passRatio = config?.execution?.minRulePassRatio || 1.0; const passRatio = config?.execution?.minRulePassRatio || 1.0;
const style = RISK_STYLE_TEMPLATES.find(t => t.minRulePassRatio === passRatio) || RISK_STYLE_TEMPLATES[1]; const style = RISK_STYLE_TEMPLATES.find(t => t.minRulePassRatio === passRatio) || RISK_STYLE_TEMPLATES[1];
let hours: WizardState['hours'] = '24/7'; let hours: WizardState['hours'] = '24/7';
const sessions = config?.rules?.find((r: any) => r.ruleId === 'SessionRule')?.params?.sessions; const sessions = config?.rules?.find((r: any) => r.ruleId === 'SessionRule')?.params?.sessions;
if (sessions === 'LDN,NY') hours = 'London + New York'; if (sessions === 'LDN,NY') hours = 'London + New York';
if (sessions === 'TOK,SYD') hours = 'Asia only'; if (sessions === 'TOK,SYD') hours = 'Asia only';
return { return {
step: 1, step: 1,
riskStyle: style, riskStyle: style,
assets: String(editingProfile.symbols).split(','), assets: String(editingProfile.symbols).split(','),
capital: editingProfile.allocated_capital, capital: editingProfile.allocated_capital,
profitTarget: config?.riskLimits?.dailyProfitTargetUsd || 100, profitTarget: config?.riskLimits?.dailyProfitTargetUsd || 100,
hours hours
}; };
} }
return { return {
step: 1, step: 1,
riskStyle: null, riskStyle: null,
assets: ['BTC/USDT'], assets: ['BTC/USDT'],
capital: 1000, capital: 1000,
profitTarget: 100, profitTarget: 100,
hours: '24/7' hours: '24/7'
}; };
}; };
const [state, setState] = useState<WizardState>(getInitialState()); const [state, setState] = useState<WizardState>(getInitialState());
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [showBacktest, setShowBacktest] = useState(false); const [showBacktest, setShowBacktest] = useState(false);
const { enabled: backtestEnabled, loading: backtestGateLoading } = useBacktestFeatureGate({ previewAsCustomer }); const { enabled: backtestEnabled, loading: backtestGateLoading } = useBacktestFeatureGate({ previewAsCustomer });
const next = () => setState(s => ({ ...s, step: s.step + 1 })); const next = () => setState(s => ({ ...s, step: s.step + 1 }));
const back = () => setState(s => ({ ...s, step: s.step - 1 })); const back = () => setState(s => ({ ...s, step: s.step - 1 }));
const handleSave = async () => { const handleSave = async () => {
if (!user || !state.riskStyle) return; if (!user || !state.riskStyle) return;
setLoading(true); setLoading(true);
const strategy_config = buildStrategyConfig(state); const strategy_config = buildStrategyConfig(state);
const payload = { const payload = {
name: editingProfile?.name || `${state.riskStyle.label.split(' ')[1]} Bot (${state.assets.join(',')})`, name: editingProfile?.name || `${state.riskStyle.label.split(' ')[1]} Bot (${state.assets.join(',')})`,
user_id: user.id, user_id: user.id,
allocated_capital: state.capital, allocated_capital: state.capital,
risk_per_trade_percent: state.riskStyle.riskPerTrade, risk_per_trade_percent: state.riskStyle.riskPerTrade,
symbols: state.assets.join(','), symbols: state.assets.join(','),
is_active: editingProfile ? editingProfile.is_active : false, // Preserve status if editing is_active: editingProfile ? editingProfile.is_active : false, // Preserve status if editing
strategy_config strategy_config
}; };
let result; let result;
if (editingProfile) { if (editingProfile) {
result = await updateTradeProfile(editingProfile.id, payload).then(() => ({ error: null as any })).catch((error) => ({ error })); result = await updateTradeProfile(editingProfile.id, payload).then(() => ({ error: null as any })).catch((error) => ({ error }));
} else { } else {
result = await createTradeProfile(payload).then(() => ({ error: null as any })).catch((error) => ({ error })); result = await createTradeProfile(payload).then(() => ({ error: null as any })).catch((error) => ({ error }));
} }
setLoading(false); setLoading(false);
if (result.error) { if (result.error) {
alert('Error saving strategy: ' + result.error.message); alert('Error saving strategy: ' + result.error.message);
} else { } else {
onComplete(); onComplete();
} }
}; };
return ( return (
<div className="mx-auto max-w-3xl px-4 py-8"> <div className="mx-auto max-w-3xl px-4 py-8">
{/* Progress Bar */} {/* Progress Bar */}
<div className="relative mb-12 flex justify-between"> <div className="relative mb-12 flex justify-between">
@ -168,62 +168,61 @@ export const StrategyWizard: React.FC<{
className={`relative z-10 flex h-10 w-10 items-center justify-center rounded-full text-sm font-bold transition-all duration-300 ${state.step >= i ? 'scale-110 bg-[var(--primary)] text-[var(--primary-foreground)]' : 'border border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)]' className={`relative z-10 flex h-10 w-10 items-center justify-center rounded-full text-sm font-bold transition-all duration-300 ${state.step >= i ? 'scale-110 bg-[var(--primary)] text-[var(--primary-foreground)]' : 'border border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)]'
}`} }`}
> >
{state.step > i ? <CheckCircle2 size={18} /> : i} {state.step > i ? <CheckCircle2 size={18} /> : i}
</div> </div>
))} ))}
</div> </div>
{/* Step 1: Risk Style */} {/* Step 1: Risk Style */}
{state.step === 1 && ( {state.step === 1 && (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500"> <div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="mb-8 text-center"> <div className="mb-8 text-center">
<h2 className={sectionTitleClass}>How should this bot behave?</h2> <h2 className={sectionTitleClass}>How should this bot behave?</h2>
<p className={sectionDescriptionClass}>Select a pre-configured risk style. High-frequency options seek more opportunities but require more flexibility.</p> <p className={sectionDescriptionClass}>Select a pre-configured risk style. High-frequency options seek more opportunities but require more flexibility.</p>
</div> </div>
<div className="grid grid-cols-1 gap-4"> <div className="grid grid-cols-1 gap-4">
{RISK_STYLE_TEMPLATES.map(style => { {RISK_STYLE_TEMPLATES.map(style => {
const isLocked = !isFeatureAllowed(tier, 'risk_style', style.id); const isLocked = !isFeatureAllowed(tier, 'risk_style', style.id);
return ( return (
<div key={style.id} className="relative"> <div key={style.id} className="relative">
<Button <button
type="button" type="button"
variant="ghost"
disabled={isLocked} disabled={isLocked}
onClick={() => setState({ ...state, riskStyle: style })} onClick={() => setState({ ...state, riskStyle: style })}
className={`${optionBaseClass} ${isLocked ? 'cursor-not-allowed opacity-40 grayscale' : 'hover:scale-[1.01] active:scale-[0.99]' className={`card-button ${optionBaseClass} ${isLocked ? 'cursor-not-allowed opacity-40 grayscale' : 'hover:scale-[1.01] active:scale-[0.99]'
} ${state.riskStyle?.id === style.id } ${state.riskStyle?.id === style.id
? optionSelectedClass ? optionSelectedClass
: optionIdleClass : optionIdleClass
}`} }`}
> >
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className={`w-12 h-12 rounded-xl flex items-center justify-center ${style.id === 'safe' ? 'bg-blue-500/10 text-blue-400' : <div className={`w-12 h-12 rounded-xl flex items-center justify-center ${style.id === 'safe' ? 'bg-blue-500/10 text-blue-400' :
style.id === 'balanced' ? 'bg-[var(--accent-soft)] text-[var(--accent)]' : 'bg-orange-500/10 text-orange-400' style.id === 'balanced' ? 'bg-[var(--accent-soft)] text-[var(--accent)]' : 'bg-orange-500/10 text-orange-400'
}`}> }`}>
{style.id === 'safe' ? <ShieldCheck size={24} /> : style.id === 'balanced' ? <Scale size={24} /> : <Zap size={24} />} {style.id === 'safe' ? <ShieldCheck size={24} /> : style.id === 'balanced' ? <Scale size={24} /> : <Zap size={24} />}
</div> </div>
<div className="flex-1"> <div className="flex-1">
<div className="flex justify-between items-center mb-1"> <div className="flex justify-between items-center mb-1">
<span className="flex items-center gap-2 text-lg font-bold text-[var(--foreground)]"> <span className="flex items-center gap-2 text-lg font-bold text-[var(--foreground)]">
{style.label} {style.label}
{isLocked && <Lock size={14} className="text-[var(--muted-foreground)]" />} {isLocked && <Lock size={14} className="text-[var(--muted-foreground)]" />}
</span> </span>
<span className="text-[10px] uppercase tracking-widest font-black opacity-40">{style.tradeFrequency}</span> <span className="text-[10px] uppercase tracking-widest font-black opacity-40">{style.tradeFrequency}</span>
</div> </div>
<p className="text-sm leading-relaxed text-[var(--muted-foreground)]">{style.description}</p> <p className="text-sm leading-relaxed text-[var(--muted-foreground)]">{style.description}</p>
</div> </div>
</div> </div>
</Button> </button>
{isLocked && ( {isLocked && (
<div className="absolute top-4 right-4 text-[9px] font-black uppercase text-amber-500/80 tracking-tighter bg-amber-500/10 px-2 py-0.5 rounded border border-amber-500/20"> <div className="absolute top-4 right-4 text-[9px] font-black uppercase text-amber-500/80 tracking-tighter bg-amber-500/10 px-2 py-0.5 rounded border border-amber-500/20">
Pro/Elite Only Pro/Elite Only
</div> </div>
)} )}
</div> </div>
); );
})} })}
</div> </div>
<div className="flex justify-end pt-4"> <div className="flex justify-end pt-4">
<Button <Button
disabled={!state.riskStyle} disabled={!state.riskStyle}
onClick={next} onClick={next}
@ -231,33 +230,33 @@ export const StrategyWizard: React.FC<{
> >
Continue <ChevronRight size={18} /> Continue <ChevronRight size={18} />
</Button> </Button>
</div> </div>
</div> </div>
)} )}
{/* Step 2: Assets & Capital */} {/* Step 2: Assets & Capital */}
{state.step === 2 && ( {state.step === 2 && (
<div className="space-y-8 animate-in fade-in slide-in-from-right-4 duration-500"> <div className="space-y-8 animate-in fade-in slide-in-from-right-4 duration-500">
<div className="text-center"> <div className="text-center">
<h2 className={sectionTitleClass}>Assets & Capital</h2> <h2 className={sectionTitleClass}>Assets & Capital</h2>
<p className={sectionDescriptionClass}>Define what to trade and how much capital to use.</p> <p className={sectionDescriptionClass}>Define what to trade and how much capital to use.</p>
</div> </div>
<div className="space-y-6"> <div className="space-y-6">
<div className="space-y-3"> <div className="space-y-3">
<label className={labelClass}>Select Trading Assets</label> <label className={labelClass}>Select Trading Assets</label>
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
{ASSETS.map(asset => ( {ASSETS.map(asset => (
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
key={asset.id} key={asset.id}
onClick={() => { onClick={() => {
const newAssets = state.assets.includes(asset.id) const newAssets = state.assets.includes(asset.id)
? state.assets.filter(a => a !== asset.id) ? state.assets.filter(a => a !== asset.id)
: [...state.assets, asset.id]; : [...state.assets, asset.id];
if (newAssets.length > 0) setState({ ...state, assets: newAssets }); if (newAssets.length > 0) setState({ ...state, assets: newAssets });
}} }}
className={`rounded-xl border-2 px-5 py-3 text-sm font-bold transition-all ${state.assets.includes(asset.id) className={`rounded-xl border-2 px-5 py-3 text-sm font-bold transition-all ${state.assets.includes(asset.id)
? 'border-[var(--accent)] bg-[var(--accent-soft)] text-[var(--foreground)]' ? 'border-[var(--accent)] bg-[var(--accent-soft)] text-[var(--foreground)]'
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border-strong)]' : 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border-strong)]'
@ -265,12 +264,12 @@ export const StrategyWizard: React.FC<{
> >
{asset.label} {asset.label}
</Button> </Button>
))} ))}
</div> </div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-3"> <div className="space-y-3">
<label className={labelClass}>Capital Allocation</label> <label className={labelClass}>Capital Allocation</label>
<div className="relative"> <div className="relative">
<div className="pointer-events-none absolute inset-y-0 left-4 flex items-center text-[var(--muted-foreground)]"> <div className="pointer-events-none absolute inset-y-0 left-4 flex items-center text-[var(--muted-foreground)]">
@ -284,109 +283,108 @@ export const StrategyWizard: React.FC<{
/> />
</div> </div>
<p className="text-[10px] text-[var(--muted-foreground)]">Total USD balance this bot is allowed to manage.</p> <p className="text-[10px] text-[var(--muted-foreground)]">Total USD balance this bot is allowed to manage.</p>
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
<label className="flex items-center justify-between text-xs font-bold uppercase tracking-widest text-[var(--muted-foreground)]"> <label className="flex items-center justify-between text-xs font-bold uppercase tracking-widest text-[var(--muted-foreground)]">
Daily Profit Target Daily Profit Target
{!isFeatureAllowed(tier, 'profit_target', policy.maxDailyProfitTargetUsd + 1) && ( {!isFeatureAllowed(tier, 'profit_target', policy.maxDailyProfitTargetUsd + 1) && (
<span className="text-[9px] text-amber-500 flex items-center gap-1"><Lock size={10} /> Limited to ${policy.maxDailyProfitTargetUsd}</span> <span className="text-[9px] text-amber-500 flex items-center gap-1"><Lock size={10} /> Limited to ${policy.maxDailyProfitTargetUsd}</span>
)} )}
</label> </label>
<div className="relative"> <div className="relative">
<div className="pointer-events-none absolute inset-y-0 left-4 flex items-center text-[var(--muted-foreground)]"> <div className="pointer-events-none absolute inset-y-0 left-4 flex items-center text-[var(--muted-foreground)]">
<Target size={16} /> <Target size={16} />
</div> </div>
<Input <Input
type="number" type="number"
disabled={tier === 'free'} disabled={tier === 'free'}
value={state.profitTarget} value={state.profitTarget}
onChange={e => { onChange={e => {
const val = Number(e.target.value); const val = Number(e.target.value);
if (isFeatureAllowed(tier, 'profit_target', val)) { if (isFeatureAllowed(tier, 'profit_target', val)) {
setState({ ...state, profitTarget: val }); setState({ ...state, profitTarget: val });
} }
}} }}
className={`h-14 pl-10 font-bold ${tier === 'free' ? 'cursor-not-allowed opacity-50' : ''}`} className={`h-14 pl-10 font-bold ${tier === 'free' ? 'cursor-not-allowed opacity-50' : ''}`}
/> />
</div> </div>
<div className="flex gap-2 items-start mt-1"> <div className="flex gap-2 items-start mt-1">
<Info size={12} className="mt-0.5 shrink-0 text-[var(--accent)]" /> <Info size={12} className="mt-0.5 shrink-0 text-[var(--accent)]" />
<p className="text-[10px] italic text-[var(--muted-foreground)]">Once this profit is reached, the bot automatically pauses for the day to lock in gains.</p> <p className="text-[10px] italic text-[var(--muted-foreground)]">Once this profit is reached, the bot automatically pauses for the day to lock in gains.</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div className="flex justify-between pt-4"> <div className="flex justify-between pt-4">
<Button onClick={back} variant="ghost" className="px-6"> <Button onClick={back} variant="ghost" className="px-6">
<ChevronLeft size={18} /> Back <ChevronLeft size={18} /> Back
</Button> </Button>
<Button onClick={next} className="px-8"> <Button onClick={next} className="px-8">
Continue <ChevronRight size={18} /> Continue <ChevronRight size={18} />
</Button> </Button>
</div> </div>
</div> </div>
)} )}
{/* Step 3: Trading Hours */} {/* Step 3: Trading Hours */}
{state.step === 3 && ( {state.step === 3 && (
<div className="space-y-8 animate-in fade-in slide-in-from-right-4 duration-500"> <div className="space-y-8 animate-in fade-in slide-in-from-right-4 duration-500">
<div className="text-center"> <div className="text-center">
<h2 className={sectionTitleClass}>Trading Hours</h2> <h2 className={sectionTitleClass}>Trading Hours</h2>
<p className={sectionDescriptionClass}>When should the bot look for signals?</p> <p className={sectionDescriptionClass}>When should the bot look for signals?</p>
</div> </div>
<div className="grid grid-cols-1 gap-4"> <div className="grid grid-cols-1 gap-4">
{(['24/7', 'London + New York', 'Asia only'] as const).map(option => ( {(['24/7', 'London + New York', 'Asia only'] as const).map(option => (
<Button <button
type="button" type="button"
variant="ghost"
key={option} key={option}
onClick={() => setState({ ...state, hours: option })} onClick={() => setState({ ...state, hours: option })}
className={`rounded-2xl border-2 p-6 text-left transition-all ${state.hours === option className={`card-button rounded-2xl border-2 p-6 text-left transition-all ${state.hours === option
? optionSelectedClass ? optionSelectedClass
: optionIdleClass : optionIdleClass
}`} }`}
> >
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className={`flex h-10 w-10 items-center justify-center rounded-lg ${state.hours === option ? 'bg-[var(--accent-soft)] text-[var(--accent)]' : 'bg-[var(--muted)] text-[var(--muted-foreground)]'}`}> <div className={`flex h-10 w-10 items-center justify-center rounded-lg ${state.hours === option ? 'bg-[var(--accent-soft)] text-[var(--accent)]' : 'bg-[var(--muted)] text-[var(--muted-foreground)]'}`}>
<Clock size={20} /> <Clock size={20} />
</div> </div>
<div> <div>
<span className="block text-lg font-bold text-[var(--foreground)]">{option}</span> <span className="block text-lg font-bold text-[var(--foreground)]">{option}</span>
<span className="text-xs text-[var(--muted-foreground)]"> <span className="text-xs text-[var(--muted-foreground)]">
{option === '24/7' ? 'Universal coverage, trades any time signals appear.' : {option === '24/7' ? 'Universal coverage, trades any time signals appear.' :
option === 'London + New York' ? 'Focuses on the most liquid market overlap (07:00 - 21:00 UTC).' : option === 'London + New York' ? 'Focuses on the most liquid market overlap (07:00 - 21:00 UTC).' :
'Optimized for Tokoyo and Sydney sessions.'} 'Optimized for Tokoyo and Sydney sessions.'}
</span> </span>
</div> </div>
</div> </div>
</Button> </button>
))} ))}
</div> </div>
<div className="flex justify-between pt-4"> <div className="flex justify-between pt-4">
<Button onClick={back} variant="ghost" className="px-6"> <Button onClick={back} variant="ghost" className="px-6">
<ChevronLeft size={18} /> Back <ChevronLeft size={18} /> Back
</Button> </Button>
<Button onClick={next} className="px-8"> <Button onClick={next} className="px-8">
Continue <ChevronRight size={18} /> Continue <ChevronRight size={18} />
</Button> </Button>
</div> </div>
</div> </div>
)} )}
{/* Step 4: Safety Confirmation */} {/* Step 4: Safety Confirmation */}
{state.step === 4 && ( {state.step === 4 && (
<div className="space-y-8 animate-in fade-in slide-in-from-right-4 duration-500"> <div className="space-y-8 animate-in fade-in slide-in-from-right-4 duration-500">
<div className="text-center"> <div className="text-center">
<h2 className={sectionTitleClass}>Safety Confirmation</h2> <h2 className={sectionTitleClass}>Safety Confirmation</h2>
<p className={sectionDescriptionClass}>Review the built-in safeguards protecting your capital.</p> <p className={sectionDescriptionClass}>Review the built-in safeguards protecting your capital.</p>
</div> </div>
<Card className="overflow-hidden rounded-2xl"> <Card className="overflow-hidden rounded-2xl">
<div className="p-6 space-y-4"> <div className="p-6 space-y-4">
<div className="flex items-center justify-between border-b border-[var(--border)] py-3"> <div className="flex items-center justify-between border-b border-[var(--border)] py-3">
<span className="text-sm font-medium text-[var(--muted-foreground)]">Auto-Protective Stop Loss</span> <span className="text-sm font-medium text-[var(--muted-foreground)]">Auto-Protective Stop Loss</span>
<span className="text-sm font-bold text-[var(--accent)]">Enabled (Mandatory)</span> <span className="text-sm font-bold text-[var(--accent)]">Enabled (Mandatory)</span>
@ -397,36 +395,36 @@ export const StrategyWizard: React.FC<{
</div> </div>
<div className="flex items-center justify-between border-b border-[var(--border)] py-3"> <div className="flex items-center justify-between border-b border-[var(--border)] py-3">
<span className="text-sm font-medium text-[var(--muted-foreground)]">Daily Recovery Halt</span> <span className="text-sm font-medium text-[var(--muted-foreground)]">Daily Recovery Halt</span>
<span className="text-rose-400 text-sm font-bold font-mono">-${Math.floor(state.capital * 0.05)} (5%)</span> <span className="text-rose-400 text-sm font-bold font-mono">-${Math.floor(state.capital * 0.05)} (5%)</span>
</div> </div>
<div className="flex items-center justify-between py-3"> <div className="flex items-center justify-between py-3">
<span className="text-sm font-medium text-[var(--muted-foreground)]">Market Volatility Guard</span> <span className="text-sm font-medium text-[var(--muted-foreground)]">Market Volatility Guard</span>
<span className="text-blue-400 text-sm font-bold">Smart ATR Check</span> <span className="text-blue-400 text-sm font-bold">Smart ATR Check</span>
</div> </div>
</div> </div>
<div className="bg-amber-500/10 border-t border-amber-500/20 p-5 flex gap-4"> <div className="bg-amber-500/10 border-t border-amber-500/20 p-5 flex gap-4">
<AlertTriangle className="text-amber-500 shrink-0" size={20} /> <AlertTriangle className="text-amber-500 shrink-0" size={20} />
<p className="text-xs text-amber-500/90 leading-relaxed font-medium"> <p className="text-xs text-amber-500/90 leading-relaxed font-medium">
Risk management and safety rules are coded into the engine core. These cannot be disabled or bypassed, ensuring your bot always operates within safety boundaries. Risk management and safety rules are coded into the engine core. These cannot be disabled or bypassed, ensuring your bot always operates within safety boundaries.
</p> </p>
</div> </div>
</Card> </Card>
<div className="flex justify-between pt-4"> <div className="flex justify-between pt-4">
<Button onClick={back} variant="ghost" className="px-6"> <Button onClick={back} variant="ghost" className="px-6">
<ChevronLeft size={18} /> Back <ChevronLeft size={18} /> Back
</Button> </Button>
<Button onClick={next} className="px-8"> <Button onClick={next} className="px-8">
I Understand <ChevronRight size={18} /> I Understand <ChevronRight size={18} />
</Button> </Button>
</div> </div>
</div> </div>
)} )}
{/* Step 5: Review & Create */} {/* Step 5: Review & Create */}
{state.step === 5 && ( {state.step === 5 && (
<div className="space-y-8 animate-in fade-in zoom-in-95 duration-500"> <div className="space-y-8 animate-in fade-in zoom-in-95 duration-500">
<div className="text-center"> <div className="text-center">
<h2 className="mb-2 text-3xl font-bold text-[var(--foreground)]">Ready to Deploy</h2> <h2 className="mb-2 text-3xl font-bold text-[var(--foreground)]">Ready to Deploy</h2>
<p className={sectionDescriptionClass}>Verify your bot strategy one last time.</p> <p className={sectionDescriptionClass}>Verify your bot strategy one last time.</p>
</div> </div>
@ -444,41 +442,41 @@ export const StrategyWizard: React.FC<{
))} ))}
</div> </div>
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-8 pt-4"> <div className="grid grid-cols-2 gap-8 pt-4">
<div className="space-y-1"> <div className="space-y-1">
<span className="text-[10px] font-black uppercase tracking-widest text-[var(--muted-foreground)]">Target Capital</span> <span className="text-[10px] font-black uppercase tracking-widest text-[var(--muted-foreground)]">Target Capital</span>
<div className="flex items-baseline gap-1 text-xl font-bold text-[var(--foreground)]"> <div className="flex items-baseline gap-1 text-xl font-bold text-[var(--foreground)]">
<span className="text-sm opacity-50 opacity-40">$</span>{state.capital} <span className="text-sm opacity-50 opacity-40">$</span>{state.capital}
</div> </div>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<span className="text-[10px] font-black uppercase tracking-widest text-[var(--muted-foreground)]">Profit Threshold</span> <span className="text-[10px] font-black uppercase tracking-widest text-[var(--muted-foreground)]">Profit Threshold</span>
<div className="flex items-baseline gap-1 text-xl font-bold text-[var(--accent)]"> <div className="flex items-baseline gap-1 text-xl font-bold text-[var(--accent)]">
<span className="text-sm opacity-40">$</span>{state.profitTarget}<span className="text-[10px] opacity-60 ml-1">/day</span> <span className="text-sm opacity-40">$</span>{state.profitTarget}<span className="text-[10px] opacity-60 ml-1">/day</span>
</div> </div>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<span className="text-[10px] font-black uppercase tracking-widest text-[var(--muted-foreground)]">Risk Level</span> <span className="text-[10px] font-black uppercase tracking-widest text-[var(--muted-foreground)]">Risk Level</span>
<div className="text-xl font-bold text-[var(--foreground)]">{(state.riskStyle?.riskPerTrade || 0)}% <span className="text-xs font-normal opacity-50">per trade</span></div> <div className="text-xl font-bold text-[var(--foreground)]">{(state.riskStyle?.riskPerTrade || 0)}% <span className="text-xs font-normal opacity-50">per trade</span></div>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<span className="text-[10px] font-black uppercase tracking-widest text-[var(--muted-foreground)]">Active Schedule</span> <span className="text-[10px] font-black uppercase tracking-widest text-[var(--muted-foreground)]">Active Schedule</span>
<div className="text-xl font-bold text-[var(--foreground)]">{state.hours}</div> <div className="text-xl font-bold text-[var(--foreground)]">{state.hours}</div>
</div> </div>
</div> </div>
<div className="flex items-center gap-4 border-t border-[var(--border)] pt-6"> <div className="flex items-center gap-4 border-t border-[var(--border)] pt-6">
<div className="flex h-12 w-12 items-center justify-center rounded-full border border-[var(--border)] text-[var(--muted-foreground)]"> <div className="flex h-12 w-12 items-center justify-center rounded-full border border-[var(--border)] text-[var(--muted-foreground)]">
<Wallet size={20} /> <Wallet size={20} />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<p className="text-sm text-[var(--muted-foreground)]">Bots are created in <span className="font-bold text-[var(--foreground)]">PAUSED</span> mode by default. You must manually enable trading from your dashboard.</p> <p className="text-sm text-[var(--muted-foreground)]">Bots are created in <span className="font-bold text-[var(--foreground)]">PAUSED</span> mode by default. You must manually enable trading from your dashboard.</p>
</div> </div>
</div> </div>
</Card> </Card>
<div className="flex justify-between pt-4"> <div className="flex justify-between pt-4">
<Button onClick={back} variant="ghost" className="px-6"> <Button onClick={back} variant="ghost" className="px-6">
<ChevronLeft size={18} /> Back <ChevronLeft size={18} /> Back

View File

@ -493,3 +493,36 @@
word-break: break-word; word-break: break-word;
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
/* ---------------------------------------------------------------------------
Section 25 card-button utility (UI audit Pattern A)
Use on a native <button> when you need a button-shaped CARD with stacked
block content. The @bytelyst/ui Button primitive has whitespace-nowrap +
fixed h-{size} that collapses multi-line content. This class restores the
focus ring + reset behavior without those constraints.
--------------------------------------------------------------------------- */
.card-button {
appearance: none;
-webkit-appearance: none;
background: transparent;
border: 0;
font: inherit;
color: inherit;
cursor: pointer;
display: block;
width: 100%;
text-align: left;
white-space: normal;
height: auto;
transition: background-color 150ms ease, border-color 150ms ease, box-shadow 150ms ease;
}
.card-button:disabled {
cursor: not-allowed;
opacity: 0.4;
}
.card-button:focus-visible {
outline: none;
box-shadow: 0 0 0 2px var(--bl-bg-canvas, #0b0f17),
0 0 0 4px var(--bl-focus-ring, var(--bl-accent, #5A8CFF));
border-radius: inherit;
}

View File

@ -5,21 +5,21 @@ 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';
@ -35,8 +35,8 @@ function getStrategyKindLabel(config: any) {
} }
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;
@ -49,57 +49,57 @@ const ActiveStrategyCard: React.FC<{
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 && (
@ -108,128 +108,128 @@ const ActiveStrategyCard: React.FC<{
<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 style={{ marginBottom: '24px' }}>
<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 style={{ marginBottom: '32px' }}>
<Button <button
type="button" type="button"
onClick={() => onToggleExpand(profile.id)} onClick={() => onToggleExpand(profile.id)}
variant="ghost" className="card-button"
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 style={{ display: 'flex', justifyContent: 'space-between', alignItems: '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>
)} )}
</Button> </button>
</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;
@ -239,23 +239,23 @@ const ActiveStrategyCard: React.FC<{
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>>({});
@ -263,7 +263,7 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA
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);
@ -271,13 +271,13 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA
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);
@ -296,11 +296,11 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA
// 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 style={{ marginBottom: '24px' }}>
<Button <Button
type="button" type="button"
onClick={() => { onClick={() => {
@ -312,7 +312,7 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA
> >
Back to My Strategies Back to My Strategies
</Button> </Button>
</div> </div>
<StrategyWizard <StrategyWizard
editingProfile={editingProfile} editingProfile={editingProfile}
profile={userProfile} profile={userProfile}
@ -321,12 +321,12 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA
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">
@ -353,67 +353,67 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA
</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 style={{ fontSize: '11px', fontWeight: 900, color: 'var(--bl-text-quiet)', textTransform: 'uppercase', letterSpacing: '2px' }}>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 style={{ textAlign: 'center', padding: '16px', color: 'var(--bl-text-tertiary)', fontSize: '12px', fontStyle: 'italic' }}>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 style={{ fontSize: '11px', fontWeight: 900, color: 'var(--bl-text-quiet)', textTransform: 'uppercase', letterSpacing: '2px' }}>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 style={{ textAlign: 'center', padding: '16px', color: 'var(--bl-text-tertiary)', fontSize: '12px', fontStyle: 'italic' }}>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))',
@ -449,20 +449,20 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA
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)" style={{ marginBottom: '24px' }} />
<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>
@ -474,16 +474,16 @@ export const MyStrategiesTab: React.FC<{ botState: any; alerts?: any[]; previewA
> >
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>
); );
}; };

View File

@ -1006,15 +1006,14 @@ export function SimpleView() {
</p> </p>
</div> </div>
<div className="grid gap-3 md:grid-cols-2"> <div className="grid gap-3 md:grid-cols-2">
<Button <button
type="button" type="button"
onClick={() => { onClick={() => {
dispatch({ type: 'clear-feedback' }); dispatch({ type: 'clear-feedback' });
dispatch({ type: 'set-selected-holding-trade-id', value: null }); dispatch({ type: 'set-selected-holding-trade-id', value: null });
updateDraft('side', 'buy'); updateDraft('side', 'buy');
}} }}
variant="ghost" className={`card-button h-auto justify-start rounded-[1.25rem] border px-5 py-5 text-left transition ${
className={`h-auto justify-start rounded-[1.25rem] border px-5 py-5 text-left transition ${
draft.side === 'buy' draft.side === 'buy'
? 'border-[var(--primary)] bg-[var(--accent-soft)]' ? 'border-[var(--primary)] bg-[var(--accent-soft)]'
: 'border-[var(--border)] bg-[var(--card-elevated)]' : 'border-[var(--border)] bg-[var(--card-elevated)]'
@ -1024,8 +1023,8 @@ export function SimpleView() {
<div className="text-sm font-bold text-[var(--foreground)]">New short-term buy plan</div> <div className="text-sm font-bold text-[var(--foreground)]">New short-term buy plan</div>
<div className="mt-2 text-sm font-normal leading-6 text-[var(--muted-foreground)]">Arm a dip-buy trigger and let the app manage the profit exit after fill.</div> <div className="mt-2 text-sm font-normal leading-6 text-[var(--muted-foreground)]">Arm a dip-buy trigger and let the app manage the profit exit after fill.</div>
</div> </div>
</Button> </button>
<Button <button
type="button" type="button"
onClick={() => { onClick={() => {
dispatch({ type: 'clear-feedback' }); dispatch({ type: 'clear-feedback' });
@ -1035,8 +1034,7 @@ export function SimpleView() {
updateDraft('side', 'sell'); updateDraft('side', 'sell');
} }
}} }}
variant="ghost" className={`card-button h-auto justify-start rounded-[1.25rem] border px-5 py-5 text-left transition ${
className={`h-auto justify-start rounded-[1.25rem] border px-5 py-5 text-left transition ${
draft.side === 'sell' draft.side === 'sell'
? 'border-[var(--primary)] bg-[var(--accent-soft)]' ? 'border-[var(--primary)] bg-[var(--accent-soft)]'
: 'border-[var(--border)] bg-[var(--card-elevated)]' : 'border-[var(--border)] bg-[var(--card-elevated)]'
@ -1046,7 +1044,7 @@ export function SimpleView() {
<div className="text-sm font-bold text-[var(--foreground)]">Manage an existing holding</div> <div className="text-sm font-bold text-[var(--foreground)]">Manage an existing holding</div>
<div className="mt-2 text-sm font-normal leading-6 text-[var(--muted-foreground)]">Choose a filled holding and place it back under managed profit-taking.</div> <div className="mt-2 text-sm font-normal leading-6 text-[var(--muted-foreground)]">Choose a filled holding and place it back under managed profit-taking.</div>
</div> </div>
</Button> </button>
</div> </div>
</section> </section>