refactor(ui): migrate visual strategy controls

This commit is contained in:
Saravana Achu Mac 2026-05-06 14:09:49 -07:00
parent a622326be6
commit bb4efc2b0d

View File

@ -19,8 +19,8 @@ import {
} from '@dnd-kit/sortable'; } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
import { GripVertical, Plus, Trash2, Save, Play } from 'lucide-react'; import { GripVertical, Plus, Trash2, Save, Play } from 'lucide-react';
import { Button } from '../ui/button';
import { Card, CardContent } from '../ui/card'; import { Card, CardContent } from '../ui/card';
import { Button, IconButton, Input, Select } from '../ui/Primitives';
// ─── Types ──────────────────────────────────────────────────────────────────── // ─── Types ────────────────────────────────────────────────────────────────────
export type Indicator = 'RSI' | 'MACD' | 'EMA_50' | 'EMA_200' | 'Price' | 'Volume'; export type Indicator = 'RSI' | 'MACD' | 'EMA_50' | 'EMA_200' | 'Price' | 'Volume';
@ -130,29 +130,35 @@ function RuleCard({
<span style={{ fontSize: 12, fontWeight: 600, color: '#374151' }}>IF</span> <span style={{ fontSize: 12, fontWeight: 600, color: '#374151' }}>IF</span>
{/* Indicator */} {/* Indicator */}
<select value={rule.indicator} style={sel} <Select
value={rule.indicator}
style={sel}
controlSize="sm"
variant="surface"
options={(Object.keys(INDICATOR_LABELS) as Indicator[]).map(k => ({ value: k, label: INDICATOR_LABELS[k] }))}
onChange={e => onChange(rule.id, { onChange={e => onChange(rule.id, {
indicator: e.target.value as Indicator, indicator: e.target.value as Indicator,
value: INDICATOR_DEFAULTS[e.target.value as Indicator], value: INDICATOR_DEFAULTS[e.target.value as Indicator],
})}> })}
{(Object.keys(INDICATOR_LABELS) as Indicator[]).map(k => ( />
<option key={k} value={k}>{INDICATOR_LABELS[k]}</option>
))}
</select>
{/* Condition */} {/* Condition */}
<select value={rule.condition} style={sel} <Select
onChange={e => onChange(rule.id, { condition: e.target.value as Condition })}> value={rule.condition}
{(Object.keys(CONDITION_LABELS) as Condition[]).map(k => ( style={sel}
<option key={k} value={k}>{CONDITION_LABELS[k]}</option> controlSize="sm"
))} variant="surface"
</select> options={(Object.keys(CONDITION_LABELS) as Condition[]).map(k => ({ value: k, label: CONDITION_LABELS[k] }))}
onChange={e => onChange(rule.id, { condition: e.target.value as Condition })}
/>
{/* Value */} {/* Value */}
<input <Input
type="number" type="number"
value={rule.value} value={rule.value}
style={numInp} style={numInp}
controlSize="sm"
variant="surface"
onChange={e => onChange(rule.id, { value: parseFloat(e.target.value) || 0 })} onChange={e => onChange(rule.id, { value: parseFloat(e.target.value) || 0 })}
/> />
@ -160,36 +166,51 @@ function RuleCard({
<span style={{ fontSize: 12, fontWeight: 600, color: '#374151' }}></span> <span style={{ fontSize: 12, fontWeight: 600, color: '#374151' }}></span>
{/* Action */} {/* Action */}
<select value={rule.action} style={{ <Select value={rule.action} style={{
...sel, ...sel,
color: rule.action === 'BUY' ? '#16A34A' : '#DC2626', color: rule.action === 'BUY' ? '#16A34A' : '#DC2626',
fontWeight: 700, fontWeight: 700,
background: rule.action === 'BUY' ? '#F0FDF4' : '#FEF2F2', background: rule.action === 'BUY' ? '#F0FDF4' : '#FEF2F2',
border: `1px solid ${rule.action === 'BUY' ? '#86EFAC' : '#FCA5A5'}`, border: `1px solid ${rule.action === 'BUY' ? '#86EFAC' : '#FCA5A5'}`,
}} }}
onChange={e => onChange(rule.id, { action: e.target.value as TradeAction })}> controlSize="sm"
<option value="BUY">BUY</option> variant="surface"
<option value="SELL">SELL</option> options={[
</select> { value: 'BUY', label: 'BUY' },
{ value: 'SELL', label: 'SELL' },
]}
onChange={e => onChange(rule.id, { action: e.target.value as TradeAction })}
/>
{/* Quantity */} {/* Quantity */}
<input <Input
type="number" type="number"
value={rule.quantity} value={rule.quantity}
style={numInp} style={numInp}
min={1} min={1}
controlSize="sm"
variant="surface"
onChange={e => onChange(rule.id, { quantity: parseFloat(e.target.value) || 1 })} onChange={e => onChange(rule.id, { quantity: parseFloat(e.target.value) || 1 })}
/> />
{/* Qty type */} {/* Qty type */}
<select value={rule.quantityType} style={sel} <Select
onChange={e => onChange(rule.id, { quantityType: e.target.value as QtyType })}> value={rule.quantityType}
<option value="shares">shares</option> style={sel}
<option value="percent">% of capital</option> controlSize="sm"
</select> variant="surface"
options={[
{ value: 'shares', label: 'shares' },
{ value: 'percent', label: '% of capital' },
]}
onChange={e => onChange(rule.id, { quantityType: e.target.value as QtyType })}
/>
{/* Delete */} {/* Delete */}
<button <IconButton
type="button"
label="Remove rule"
icon={<Trash2 size={14} />}
onClick={() => onDelete(rule.id)} onClick={() => onDelete(rule.id)}
style={{ style={{
marginLeft: 'auto', background: 'none', border: 'none', marginLeft: 'auto', background: 'none', border: 'none',
@ -197,10 +218,7 @@ function RuleCard({
display: 'flex', alignItems: 'center', display: 'flex', alignItems: 'center',
flexShrink: 0, flexShrink: 0,
}} }}
title="Remove rule" />
>
<Trash2 size={14} />
</button>
</div> </div>
); );
} }
@ -295,9 +313,11 @@ export function VisualRuleBuilder({ symbol, onSave, onBacktest }: Props) {
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 20 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 20 }}>
<div> <div>
<div style={{ fontSize: 11, color: 'var(--muted-foreground)', fontWeight: 500, marginBottom: 3 }}>Strategy name</div> <div style={{ fontSize: 11, color: 'var(--muted-foreground)', fontWeight: 500, marginBottom: 3 }}>Strategy name</div>
<input <Input
value={name} value={name}
onChange={e => setName(e.target.value)} onChange={e => setName(e.target.value)}
controlSize="sm"
variant="surface"
style={{ style={{
border: '1px solid var(--border)', borderRadius: 12, border: '1px solid var(--border)', borderRadius: 12,
padding: '7px 12px', fontSize: 14, fontWeight: 600, padding: '7px 12px', fontSize: 14, fontWeight: 600,
@ -362,8 +382,11 @@ export function VisualRuleBuilder({ symbol, onSave, onBacktest }: Props) {
</DndContext> </DndContext>
{/* Add rule */} {/* Add rule */}
<button <Button
type="button"
onClick={() => setRules(prev => [...prev, makeRule()])} onClick={() => setRules(prev => [...prev, makeRule()])}
variant="outline"
size="sm"
style={{ style={{
display: 'flex', alignItems: 'center', gap: 7, display: 'flex', alignItems: 'center', gap: 7,
width: '100%', padding: '10px 0', border: '1px dashed var(--border-strong)', width: '100%', padding: '10px 0', border: '1px dashed var(--border-strong)',
@ -373,7 +396,7 @@ export function VisualRuleBuilder({ symbol, onSave, onBacktest }: Props) {
}} }}
> >
<Plus size={15} /> Add Rule <Plus size={15} /> Add Rule
</button> </Button>
{/* Rule summary */} {/* Rule summary */}
{rules.length > 0 && ( {rules.length > 0 && (