import { useEffect, useRef, useState, type RefObject } from "react"; import { Box, Text, TextAttributes, useUiHost, type InputRenderable } from "../../../ui"; import { useThemeColors } from "../../../theme/theme-context"; import { blendHex, priceColor } from "../../../theme/colors"; import { t, tf } from "../../../i18n"; import { formatMarketPrice } from "../../../market-data/market/format"; import { formatPercentRaw } from "../../../utils/format"; import { Button, NumberField, TextField } from "../../ui"; import { ONBOARDING_DESKTOP } from "../onboarding-frame"; import { POSITION_FIELDS, type OnboardingPositionRow, type OnboardingPositionsState, type PositionFieldId, } from "../wizard-positions"; /** Removes the position under the keyboard cursor; the wizard binds it. */ export const REMOVE_POSITION_KEY = "d"; /** Rows the terminal card has room for. */ const TUI_VISIBLE_ROWS = 4; const FIELD_LABELS: Record = { ticker: "Ticker", shares: "Shares", avgCost: "Avg cost", }; const FIELD_PLACEHOLDERS: Record = { ticker: "e.g. AAPL", shares: "optional", avgCost: "current price", }; /** Company name and last price for the symbol being typed, or why it will not resolve. */ function PreviewLine({ preview, error }: Pick) { const colors = useThemeColors(); if (error) return {error}; if (preview.status === "idle") return null; if (preview.status === "checking") { return {tf("{query} checking...", { query: preview.query })}; } if (preview.status === "missing") return {preview.message}; const price = preview.quote?.price; const change = preview.quote?.changePercent; return ( {preview.symbol} {preview.name ? {` ${preview.name}`} : null} {price != null ? {` ${formatMarketPrice(price, { maxWidth: 12 })}`} : null} {change != null ? {` ${formatPercentRaw(change)}`} : null} {preview.duplicate ? {t(" already added")} : null} ); } const wholeCurrencyFormatters = new Map(); /** Position value rounded to whole units: cents add noise next to a share count. */ function formatWholeCurrency(value: number, currency: string): string { let formatter = wholeCurrencyFormatters.get(currency); if (!formatter) { try { formatter = new Intl.NumberFormat("en-US", { style: "currency", currency, minimumFractionDigits: 0, maximumFractionDigits: 0, }); } catch { formatter = new Intl.NumberFormat("en-US", { minimumFractionDigits: 0, maximumFractionDigits: 0 }); } wholeCurrencyFormatters.set(currency, formatter); } return formatter.format(value); } function positionSummary(row: OnboardingPositionRow): string { if (row.shares === null) return t("following"); const shares = Number.isInteger(row.shares) ? String(row.shares) : row.shares.toFixed(4).replace(/\.?0+$/, ""); const cost = row.avgCost === null ? "" : ` @ ${formatMarketPrice(row.avgCost, { maxWidth: 12 })}`; return `${shares}${cost}`; } type ScrollTarget = { matches?(selector: string): boolean; scrollIntoView?(options: { block: "nearest" }): void; }; function DesktopPositionRow({ row, selected, onRemove, }: { row: OnboardingPositionRow; /** Under the keyboard cursor: lit like a hovered row, with its Remove key shown. */ selected: boolean; onRemove: (symbol: string) => void; }) { const colors = useThemeColors(); const [hovered, setHovered] = useState(false); const rowRef = useRef(null); useEffect(() => { // The pointer lit it: scrolling now would slide another row under it. if (!selected || rowRef.current?.matches?.(":hover")) return; rowRef.current?.scrollIntoView?.({ block: "nearest" }); }, [selected]); const lit = hovered || selected; return ( setHovered(true)} onMouseOut={() => setHovered(false)} style={{ height: 32, padding: "0 10px", borderRadius: 6, backgroundColor: lit ? blendHex(colors.panel, colors.textBright, 0.05) : "transparent", }} data-gloom-role="onboarding-position" > {row.symbol} {row.name} {positionSummary(row)} {row.value === null ? "" : formatWholeCurrency(row.value, row.currency)} {lit ? (