Mark-as-taken opens a modal dialog instead of expanding inline
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 1m1s
Deploy / deploy (push) Successful in 35s

The inline take form under the price rail pushed the card apart. It is
now a focused overlay dialog (portaled to body - the unified panel's
backdrop-filter/overflow would trap a fixed element):

- symbol + direction header, stop/target/suggested-size summary line
- shares (autofocused), entry, and target selector
- Confirm trade / Cancel; closes on success, Escape, or backdrop click
- the card and panel never move

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 13:10:13 +02:00
co-authored by Claude Fable 5
parent 16121042cd
commit 6e33897c1a
@@ -1,4 +1,5 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import type { TradeSetup } from '../../lib/types';
import { formatPrice, formatPercent } from '../../lib/format';
import { useCreatePaperTrade } from '../../hooks/usePaperTrades';
@@ -220,6 +221,16 @@ function SetupCard({ setup, action, currentPrice, risk, regime, selectedPrice, o
const activeRR = selected?.rr_ratio ?? setup.rr_ratio;
const activeProb = selected?.probability ?? prob;
// Close the take dialog on Escape.
useEffect(() => {
if (!taking) return;
const onKey = (e: globalThis.KeyboardEvent) => {
if (e.key === 'Escape') setTaking(false);
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [taking]);
const confirmTake = () => {
createTrade.mutate(
{
@@ -343,66 +354,85 @@ function SetupCard({ setup, action, currentPrice, risk, regime, selectedPrice, o
scaleTo={(setup.targets ?? []).map((t) => t.price)}
/>
{taking && (
<div className="mt-3 rounded-lg border border-white/[0.08] p-3 space-y-2.5">
<div className="grid grid-cols-2 gap-2">
<label className="block space-y-1">
<span className="num text-[10px] uppercase tracking-wider text-gray-500">Shares</span>
<input
type="number"
min={0}
value={takeShares}
onChange={(e) => setTakeShares(Number(e.target.value))}
className="w-full input-glass px-2 py-1 text-sm num"
/>
</label>
<label className="block space-y-1">
<span className="num text-[10px] uppercase tracking-wider text-gray-500">Entry</span>
<input
type="number"
min={0}
step="0.01"
value={takeEntry}
onChange={(e) => setTakeEntry(Number(e.target.value))}
className="w-full input-glass px-2 py-1 text-sm num"
/>
</label>
</div>
{setup.targets && setup.targets.length > 1 ? (
<label className="block space-y-1">
<span className="num text-[10px] uppercase tracking-wider text-gray-500">Target</span>
<select
value={takeTarget}
onChange={(e) => setTakeTarget(Number(e.target.value))}
className="input-glass w-full px-2 py-1.5 text-sm num"
{/* Take dialog — portaled overlay so the panel structure stays put */}
{taking && createPortal(
<div
className="fixed inset-0 z-50 flex items-center justify-center p-4 animate-fade-in"
role="dialog"
aria-modal="true"
aria-label={`Mark ${setup.symbol} ${setup.direction} as taken`}
>
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={() => setTaking(false)} />
<div className="glass relative z-10 w-full max-w-md p-6 shadow-2xl animate-slide-up">
<div className="flex items-center gap-3">
<h3 className="font-display text-xl font-bold tracking-tight text-gray-100">{setup.symbol}</h3>
<DirTag direction={setup.direction} />
<span className="num ml-auto text-[10px] uppercase tracking-[0.16em] text-gray-500">paper trade</span>
</div>
<p className="num mt-1.5 text-[11px] text-gray-500">
stop {formatPrice(setup.stop_loss)} · target {formatPrice(takeTarget)}
{sizing && <> · suggested {sizing.shares} sh, max loss {formatPrice(sizing.dollarRisk)}</>}
</p>
<div className="mt-4 grid grid-cols-2 gap-3">
<label className="block space-y-1">
<span className="num text-[10px] uppercase tracking-wider text-gray-500">Shares</span>
<input
type="number"
min={0}
autoFocus
value={takeShares}
onChange={(e) => setTakeShares(Number(e.target.value))}
className="w-full input-glass px-2.5 py-1.5 text-sm num"
/>
</label>
<label className="block space-y-1">
<span className="num text-[10px] uppercase tracking-wider text-gray-500">Entry</span>
<input
type="number"
min={0}
step="0.01"
value={takeEntry}
onChange={(e) => setTakeEntry(Number(e.target.value))}
className="w-full input-glass px-2.5 py-1.5 text-sm num"
/>
</label>
</div>
{setup.targets && setup.targets.length > 1 ? (
<label className="mt-3 block space-y-1">
<span className="num text-[10px] uppercase tracking-wider text-gray-500">Target</span>
<select
value={takeTarget}
onChange={(e) => setTakeTarget(Number(e.target.value))}
className="input-glass w-full px-2.5 py-1.5 text-sm num"
>
{setup.targets.map((t) => (
<option key={`${t.sr_level_id}-${t.price}`} value={t.price} className="bg-[#14161f]">
{formatPrice(t.price)} · {t.probability.toFixed(0)}% · {t.classification}{t.is_primary ? ' · primary' : ''}
</option>
))}
</select>
</label>
) : null}
<div className="mt-5 flex gap-2">
<button
onClick={confirmTake}
disabled={createTrade.isPending || !(takeShares > 0) || !(takeEntry > 0)}
className="flex-1 rounded-lg border border-blue-500/35 bg-blue-500/15 px-3 py-2 text-sm font-semibold text-blue-300 transition-colors hover:bg-blue-500/25 disabled:opacity-50"
>
{setup.targets.map((t) => (
<option key={`${t.sr_level_id}-${t.price}`} value={t.price} className="bg-[#14161f]">
{formatPrice(t.price)} · {t.probability.toFixed(0)}% · {t.classification}{t.is_primary ? ' · primary' : ''}
</option>
))}
</select>
</label>
) : null}
<p className="num text-[10px] text-gray-500">
Stop {formatPrice(setup.stop_loss)} · Target {formatPrice(takeTarget)} · {setup.direction.toUpperCase()} paper trade
</p>
<div className="flex gap-2">
<button
onClick={confirmTake}
disabled={createTrade.isPending || !(takeShares > 0) || !(takeEntry > 0)}
className="flex-1 rounded-lg border border-blue-500/35 bg-blue-500/15 px-3 py-1.5 text-xs font-semibold text-blue-300 transition-colors hover:bg-blue-500/25 disabled:opacity-50"
>
{createTrade.isPending ? 'Taking…' : 'Confirm'}
</button>
<button
onClick={() => setTaking(false)}
className="rounded-lg border border-white/[0.08] px-3 py-1.5 text-xs text-gray-400 transition-colors hover:text-gray-200"
>
Cancel
</button>
{createTrade.isPending ? 'Taking…' : 'Confirm trade'}
</button>
<button
onClick={() => setTaking(false)}
className="rounded-lg border border-white/[0.08] px-4 py-2 text-sm text-gray-400 transition-colors hover:text-gray-200"
>
Cancel
</button>
</div>
</div>
</div>
</div>,
document.body,
)}
{/* Target ladder — open by default; clicking a row previews it on the rail */}