import { useRef, useState } from "react"; import { Button, NumberField, TextField, SelectButton, Notice, type SelectControl } from "../../../components"; import { useInputCapture, useShortcut } from "../../../public/react"; import { Box, ScrollBox } from "../../../ui"; import { useDialogState } from "../../../ui/dialog"; import { isPlainKey } from "../../../utils/keyboard"; import { parseScenarioInputFields } from "./state"; import { parseLegs, type ScenarioLeg, type ScenarioPosition, type ScenarioControls } from "./model"; export function ScenarioLegEditor({ leg, focused, width, onSave, onCancel }: { leg: ScenarioLeg; focused: boolean; width: number; onSave: (leg: ScenarioLeg) => void; onCancel: () => void; }) { const [side, setSide] = useState(leg.side); const [direction, setDirection] = useState(leg.quantity < 0 ? "sell" : "buy"); const [fields, setFields] = useState(() => ({ strike: String(leg.strike), expiration: new Date(leg.expiration * 1000).toISOString().slice(0, 10), quantity: String(Math.abs(leg.quantity)), price: String(leg.price), volatility: String(leg.volatility * 100), multiplier: String(leg.multiplier) })); type Field = keyof typeof fields; const fieldIds: Field[] = ["strike", "expiration", "quantity", "price", "volatility", "multiplier"]; const [active, setActive] = useState("strike"); // Tab walks the form in reading order: the Option and Position row sits on top. const focusIds = ["side", "direction", ...fieldIds] as const; const sideControl = useRef(null), directionControl = useRef(null); const [error, setError] = useState(null); // While the Option or Position choice dialog is open it owns Escape, Enter and // Tab; the form must not cancel itself or move focus underneath it. const dialogOpen = useDialogState((state) => state.isOpen); useInputCapture(focused); const submit = () => { try { const parsed = parseLegs([side, fields.strike, fields.expiration, `${direction === "sell" ? "-" : ""}${fields.quantity}`, fields.price, fields.volatility, fields.multiplier].join(","))[0]!; onSave({ ...parsed, id: leg.id }); } catch (error) { setError(error instanceof Error ? error.message : String(error)); } }; useShortcut((event) => { if (event.name === "tab") { event.preventDefault(); event.stopPropagation(); setActive((current) => focusIds[(focusIds.indexOf(current) + (event.shift ? -1 : 1) + focusIds.length) % focusIds.length]!); } else if (["enter", "return", "space"].includes(event.name ?? "") && (active === "side" || active === "direction")) { event.preventDefault(); event.stopPropagation(); (active === "side" ? sideControl : directionControl).current?.open(); } else if ((active === "side" || active === "direction") && isPlainKey(event, "left", "right", "h", "l")) { // Both choices have two values, so stepping either way flips it in place. event.preventDefault(); event.stopPropagation(); if (active === "side") setSide((current) => current === "call" ? "put" : "call"); else setDirection((current) => current === "sell" ? "buy" : "sell"); } else if (event.name === "escape") { event.preventDefault(); event.stopPropagation(); onCancel(); } }, { enabled: focused && !dialogOpen, phase: "before", allowEditable: true, scope: "osa-leg-form" }); const labels: Record = { strike: "Strike", expiration: "Expiry (YYYY-MM-DD)", quantity: "Contracts", price: "Entry price / unit", volatility: "IV %", multiplier: "Units / contract" }; const fieldWidth = Math.max(16, Math.min(32, Math.floor((width - 5) / 2))); return setActive("side")} emphasized={active === "side"} value={side} onChange={(value) => setSide(value as ScenarioLeg["side"])} options={[{ value: "call", label: "Call" }, { value: "put", label: "Put" }]} /> setActive("direction")} emphasized={active === "direction"} value={direction} onChange={setDirection} options={[{ value: "buy", label: "Buy" }, { value: "sell", label: "Sell" }]} /> {[0, 2, 4].map((offset) => {fieldIds.slice(offset, offset + 2).map((id) => { const props = { label: labels[id], width: fieldWidth, value: fields[id], focused: focused && active === id, onMouseDown: () => setActive(id), onChange: (value: string) => setFields((current) => ({ ...current, [id]: value })), onSubmit: submit }; return id === "expiration" ? : ; })} )} {error && {error}}