Mark-as-taken opens a modal dialog instead of expanding inline
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:
@@ -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,17 +354,36 @@ 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">
|
||||
{/* 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 py-1 text-sm num"
|
||||
className="w-full input-glass px-2.5 py-1.5 text-sm num"
|
||||
/>
|
||||
</label>
|
||||
<label className="block space-y-1">
|
||||
@@ -364,17 +394,17 @@ function SetupCard({ setup, action, currentPrice, risk, regime, selectedPrice, o
|
||||
step="0.01"
|
||||
value={takeEntry}
|
||||
onChange={(e) => setTakeEntry(Number(e.target.value))}
|
||||
className="w-full input-glass px-2 py-1 text-sm num"
|
||||
className="w-full input-glass px-2.5 py-1.5 text-sm num"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{setup.targets && setup.targets.length > 1 ? (
|
||||
<label className="block space-y-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 py-1.5 text-sm num"
|
||||
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]">
|
||||
@@ -384,25 +414,25 @@ function SetupCard({ setup, action, currentPrice, risk, regime, selectedPrice, o
|
||||
</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">
|
||||
|
||||
<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-1.5 text-xs font-semibold text-blue-300 transition-colors hover:bg-blue-500/25 disabled:opacity-50"
|
||||
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"
|
||||
>
|
||||
{createTrade.isPending ? 'Taking…' : 'Confirm'}
|
||||
{createTrade.isPending ? 'Taking…' : 'Confirm trade'}
|
||||
</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"
|
||||
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>,
|
||||
document.body,
|
||||
)}
|
||||
|
||||
{/* Target ladder — open by default; clicking a row previews it on the rail */}
|
||||
|
||||
Reference in New Issue
Block a user