Polish exit timeline and always show S/R levels.
Deploy / lint (push) Successful in 8s
Deploy / test (push) Successful in 1m8s
Deploy / deploy (push) Successful in 36s

Keep support/resistance visible even near trade lines, tighten the how-this-exits spine so steps connect, unify step styling, and add space above the panel.
This commit is contained in:
2026-07-14 10:55:27 +02:00
parent d5b0ebf895
commit f309bd5691
2 changed files with 18 additions and 28 deletions
@@ -354,8 +354,8 @@ export function CandlestickChart({
ctx.restore(); ctx.restore();
} }
// Left-side role labels only; prices live on the right axis. // Exterior left-side role labels only; prices live on the right axis.
// Priority when levels crowd: Now > Stop > Gate > Entry > S/R. // Priority when levels crowd: Now > Stop > Gate > Entry.
// Best practice: hide Entry when it sits on top of Now — at market, Now // Best practice: hide Entry when it sits on top of Now — at market, Now
// *is* the fill; Entry only matters once price has drifted from the scan. // *is* the fill; Entry only matters once price has drifted from the scan.
type LineLabel = { type LineLabel = {
@@ -385,27 +385,20 @@ export function CandlestickChart({
&& (entryDriftPx >= 12 || entryDriftR >= 0.25), && (entryDriftPx >= 12 || entryDriftR >= 0.25),
); );
const tradeYs: number[] = [nowY];
if (tradeSetup) {
tradeYs.push(yScale(tradeSetup.stop_loss), yScale(tradeSetup.target));
if (showEntry) tradeYs.push(yScale(tradeSetup.entry_price));
}
// In-plot S/R labels (drawn after candles so they stay readable). Not // In-plot S/R labels (drawn after candles so they stay readable). Not
// exterior margin labels — they ride the line itself, left-aligned. // exterior margin labels — they ride the line itself, left-aligned.
type InPlotLabel = { y: number; text: string; color: string; bg: string }; type InPlotLabel = { y: number; text: string; color: string; bg: string };
const inPlotLabels: InPlotLabel[] = []; const inPlotLabels: InPlotLabel[] = [];
// Nearest support/resistance only (band if it came from a zone). // Nearest support/resistance only (band if it came from a zone).
// Drop S/R entirely when it sits on a trade line — the trade label wins. // Always draw them — labels live in-plot on the line, so proximity to
// Now/Stop no longer forces a hide (that was only to free the left gutter).
markers.forEach((m) => { markers.forEach((m) => {
const isSupport = m.role === 'support'; const isSupport = m.role === 'support';
// Support stays cyan; resistance is a soft coral (warmer than neutral ink, // Support stays cyan; resistance is a soft coral (warmer than neutral ink,
// cooler/softer than the stop so the two never read as the same thing). // cooler/softer than the stop so the two never read as the same thing).
const color = isSupport ? '#2f9db2' : '#e89b8c'; const color = isSupport ? '#2f9db2' : '#e89b8c';
const yMid = yScale(m.price); const yMid = yScale(m.price);
const collidesTrade = tradeYs.some((ty) => Math.abs(ty - yMid) < LABEL_MIN_GAP_PX);
if (collidesTrade) return;
if (m.high > m.low) { if (m.high > m.low) {
const yTop = yScale(m.high); const yTop = yScale(m.high);
@@ -16,35 +16,34 @@ export function ExitPlanPanel({ plan, direction }: { plan: ExitPlan; direction:
const steps = buildSteps(plan, isLong); const steps = buildSteps(plan, isLong);
return ( return (
<div className="mt-5 rounded-xl border border-white/[0.07] bg-white/[0.02] p-4"> <div className="mt-8 rounded-xl border border-white/[0.07] bg-white/[0.02] p-4">
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-1"> <div className="flex flex-wrap items-baseline gap-x-2 gap-y-1">
<span className="num text-[10px] uppercase tracking-[0.16em] text-gray-500">how this exits</span> <span className="num text-[10px] uppercase tracking-[0.16em] text-gray-500">how this exits</span>
<span className="text-[11.5px] font-medium text-gray-300">{plan.headline}</span> <span className="text-[11.5px] font-medium text-gray-300">{plan.headline}</span>
</div> </div>
<ol className="relative mt-4 space-y-0"> <ol className="relative mt-4">
{steps.map((step, i) => { {steps.map((step, i) => {
const isLast = i === steps.length - 1; const isLast = i === steps.length - 1;
return ( return (
<li key={step.title} className="relative flex gap-3 pb-4 last:pb-0"> <li key={step.title} className="relative flex gap-3">
{/* Spine */} {/* Spine column: continuous connector from this node into the next */}
<div className="flex w-5 shrink-0 flex-col items-center"> <div className="relative flex w-5 shrink-0 flex-col items-center self-stretch">
<span <span
className={`num mt-0.5 flex h-5 w-5 items-center justify-center rounded-full border text-[10px] font-semibold ${ className="num relative z-10 mt-0.5 flex h-5 w-5 items-center justify-center rounded-full border border-blue-400/35 bg-blue-400/10 text-[10px] font-semibold text-blue-200"
step.tone === 'muted'
? 'border-white/[0.12] text-gray-500'
: 'border-blue-400/35 bg-blue-400/10 text-blue-200'
}`}
aria-hidden aria-hidden
> >
{step.icon ?? i + 1} {i + 1}
</span> </span>
{!isLast && ( {!isLast && (
<span className="mt-1 w-px flex-1 min-h-[12px] bg-white/[0.08]" aria-hidden /> <span
className="absolute left-1/2 top-5 bottom-0 w-px -translate-x-1/2 bg-blue-400/25"
aria-hidden
/>
)} )}
</div> </div>
<div className="min-w-0 flex-1 pt-0.5"> <div className={`min-w-0 flex-1 pt-0.5 ${isLast ? 'pb-0' : 'pb-5'}`}>
<p className="text-[12px] font-medium text-gray-200">{step.title}</p> <p className="text-[12px] font-medium text-gray-200">{step.title}</p>
{step.primary && ( {step.primary && (
<p className="num mt-0.5 text-[13px] font-semibold tracking-tight text-gray-100"> <p className="num mt-0.5 text-[13px] font-semibold tracking-tight text-gray-100">
@@ -73,9 +72,6 @@ interface ExitStep {
title: string; title: string;
primary?: string; primary?: string;
detail?: string; detail?: string;
/** Optional glyph instead of a step number (e.g. clock for time stop). */
icon?: string;
tone?: 'default' | 'muted';
} }
function buildSteps(plan: ExitPlan, isLong: boolean): ExitStep[] { function buildSteps(plan: ExitPlan, isLong: boolean): ExitStep[] {
@@ -114,11 +110,12 @@ function buildSteps(plan: ExitPlan, isLong: boolean): ExitStep[] {
}); });
} }
// Same visual weight as the other steps — time stop is a real exit rule,
// not a footnote.
steps.push({ steps.push({
title: 'Time stop', title: 'Time stop',
primary: `${plan.maxHoldDays} trading days`, primary: `${plan.maxHoldDays} trading days`,
detail: 'Flat if still open after the max hold.', detail: 'Flat if still open after the max hold.',
tone: 'muted',
}); });
return steps; return steps;