import { KeyValueRow } from "../../../components"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { DataTableStackView, EmptyState, PaneStatusBody, QueryBar, Tabs, usePaneFooter, usePaneHeaderTabs, usePaneMenuItems, type DataTableCell, type DataTableKeyEvent, type DataTableRootKeyContext, type PaneFooterSegment } from "../../../components"; import { useShortcut } from "../../../react/input"; import { usePaneInstance } from "../../../state/app/context"; import { colors } from "../../../theme/colors"; import type { PaneProps } from "../../../types/plugin"; import { Box, ScrollBox, Text, TextAttributes, type InputRenderable } from "../../../ui"; import { formatCompact } from "../../../utils/format"; import { isPlainKey } from "../../../utils/keyboard"; import { formatRelativeAge } from "../../../utils/relative-time"; import { isPlainArrowUp, stopSearchFocusNavigation } from "../../../utils/search-focus-navigation"; import { cycleSortPreference } from "../../../utils/sort-values"; import { usePluginPaneState } from "../../runtime"; import { useAutoRefresh } from "../shared/auto-refresh"; import { loadTreasuryAuctions } from "./cache"; import { AUCTION_FILTERS, AUCTION_SORT_COLUMN_IDS, DEFAULT_AUCTION_SORT, auctionHistoryDays, auctionSize, buildAuctionColumns, formatAuctionRate, indirectPct, isPendingAuction, nextAuctionSort, nextFilter, rateLabel, rateValue, visibleAuctions, type AuctionColumn, type AuctionColumnId, type AuctionFilter, type AuctionSortPreference, } from "./model"; import { TREASURY_AUCTIONS_PANE_ID, type LoadStatus, type TreasuryAuction, } from "./types"; function formatAuctionDate(value: string, withYear = false): string { const timestamp = Date.parse(`${value}T00:00:00Z`); if (!Number.isFinite(timestamp)) return "—"; return new Date(timestamp).toLocaleDateString("en-US", { month: "short", day: "numeric", year: withYear ? "numeric" : undefined, timeZone: "UTC", }); } function formatRate(value: number | null): string { return value == null ? "—" : `${value.toFixed(3)}%`; } function formatRatio(value: number | null): string { return value == null ? "—" : value.toFixed(2); } function formatPct(value: number | null): string { return value == null ? "—" : `${value.toFixed(1)}%`; } function formatMoney(value: number | null): string { return value == null ? "—" : `$${formatCompact(value)}`; } function secTypeColor(secType: string, selected: boolean): string { if (selected) return colors.selectedText; switch (secType.toLowerCase()) { case "bill": case "cmb": return colors.positive; case "note": case "frn": return colors.textBright; case "bond": case "tips": return colors.warning; default: return colors.text; } } // Stable table adapters so memoized rows survive pane re-renders. const auctionKey = (auction: TreasuryAuction) => auction.id; const renderAuctionRow = ( auction: TreasuryAuction, column: AuctionColumn, _index: number, rowState: { selected: boolean }, ) => renderAuctionCell(auction, column, rowState); function renderAuctionCell( auction: TreasuryAuction, column: AuctionColumn, rowState: { selected: boolean }, ): DataTableCell { const selected = rowState.selected ? colors.selectedText : undefined; const dimmed = selected ?? colors.textDim; switch (column.id) { case "date": // Announced auctions have no results yet; every metric cell reads "—", // so the date carries the distinction instead of a second placeholder. return { text: formatAuctionDate(auction.auctionDate), color: rowState.selected ? colors.selectedText : isPendingAuction(auction) ? colors.textBright : colors.textDim, }; case "type": return { text: auction.secType, color: secTypeColor(auction.secType, rowState.selected), attributes: TextAttributes.BOLD, }; case "term": return { text: auction.securityTerm, color: selected ?? colors.text }; case "rate": return { text: formatAuctionRate(auction, rateValue(auction), "—"), color: selected ?? colors.textBright }; case "btc": return { text: formatRatio(auction.bidToCoverRatio), color: selected ?? colors.text }; case "indirect": return { text: formatPct(indirectPct(auction)), color: selected ?? colors.text }; case "size": return { text: formatMoney(auctionSize(auction)), color: dimmed }; } } function TreasuryAuctionDetail({ auction, width }: { auction: TreasuryAuction; width: number }) { const share = (value: number | null, total: number | null): string => ( value == null || !total ? formatMoney(value) : `${formatMoney(value)} (${((value / total) * 100).toFixed(1)}%)` ); return ( {formatAuctionDate(auction.auctionDate, true)} {isPendingAuction(auction) && results pending} {auction.avgMedYield != null && ( )} ); } export function TreasuryAuctionsPane({ focused, width, height }: PaneProps) { const historyDays = auctionHistoryDays(usePaneInstance()?.settings); const [auctions, setAuctions] = useState([]); const [status, setStatus] = useState("idle"); const [error, setError] = useState(null); const [stale, setStale] = useState(false); const [fetchedAt, setFetchedAt] = useState(null); const [filter, setFilter] = usePluginPaneState("activeTab", "all"); const [selectedId, setSelectedId] = usePluginPaneState("selectedId", null); const [detailOpen, setDetailOpen] = usePluginPaneState("detailOpen", false); const [sortPreference, setSortPreference] = useState(DEFAULT_AUCTION_SORT); const [searchQuery, setSearchQuery] = useState(""); const [searchFocused, setSearchFocused] = useState(false); const [searchFocusToken, setSearchFocusToken] = useState(0); const searchInputRef = useRef(null); const fetchGenRef = useRef(0); const focusSearch = useCallback(() => { setSearchFocused(true); setSearchFocusToken((current) => current + 1); }, []); const blurSearch = useCallback(() => setSearchFocused(false), []); const load = useCallback((force = false) => { fetchGenRef.current += 1; const generation = fetchGenRef.current; setStatus((current) => (current === "loaded" ? "loaded" : "loading")); setError(null); loadTreasuryAuctions(force, undefined, historyDays) .then((result) => { if (fetchGenRef.current !== generation) return; setAuctions(result.auctions); setFetchedAt(result.fetchedAt); setStale(result.stale); setError(result.refreshError ?? null); setStatus("loaded"); }) .catch((loadError: unknown) => { if (fetchGenRef.current !== generation) return; setError(loadError instanceof Error ? loadError.message : String(loadError)); setStatus("error"); }); }, [historyDays]); useEffect(() => { load(false); }, [load]); // The cache decides whether a tick becomes a request; only [r] forces it. const refresh = useCallback(() => load(false), [load]); useAutoRefresh(stale ? null : fetchedAt, refresh); const rows = useMemo( () => visibleAuctions(auctions, { filter, query: searchQuery, sort: sortPreference }), [auctions, filter, searchQuery, sortPreference], ); const selected = rows.find((auction) => auction.id === selectedId) ?? null; useEffect(() => { // Before the first answer there is nothing to match a restored selection // against, so leave it alone rather than clear it. if (auctions.length === 0 && (status === "idle" || status === "loading")) return; if (rows.length === 0) { if (selectedId !== null) setSelectedId(null); setDetailOpen(false); return; } if (!selectedId || !rows.some((auction) => auction.id === selectedId)) { setSelectedId(rows[0]!.id); } }, [auctions.length, rows, selectedId, status]); const selectFilter = useCallback((next: AuctionFilter) => { setFilter(next); setDetailOpen(false); }, []); const cycleFilter = useCallback(() => { setFilter((current) => nextFilter(current)); setDetailOpen(false); }, []); const cycleSort = useCallback((step: 1 | -1) => { setSortPreference((current) => { const next = cycleSortPreference(AUCTION_SORT_COLUMN_IDS, current, step); return { columnId: next.columnId ?? DEFAULT_AUCTION_SORT.columnId, direction: next.direction }; }); }, []); // Refresh answers in every state, including the failed load that mounts no table. useShortcut((event) => { if (!isPlainKey(event, "r")) return; event.preventDefault(); load(true); }, { enabled: focused && !searchFocused }); const handlePaneKey = useCallback((event: DataTableKeyEvent): boolean => { if (isPlainKey(event, "f")) { stopSearchFocusNavigation(event); cycleFilter(); return true; } if (isPlainKey(event, "]") || isPlainKey(event, "[")) { stopSearchFocusNavigation(event); cycleSort(event.name === "]" ? 1 : -1); return true; } return false; }, [cycleFilter, cycleSort]); const handleRootKeyDown = useCallback(( event: DataTableKeyEvent, context: DataTableRootKeyContext, ) => { if (context.selectedIndex <= 0 && isPlainArrowUp(event)) { stopSearchFocusNavigation(event); focusSearch(); return true; } if (isPlainKey(event, "/")) { stopSearchFocusNavigation(event); focusSearch(); return true; } return handlePaneKey(event); }, [focusSearch, handlePaneKey]); const columns = useMemo(() => buildAuctionColumns(), []); usePaneFooter(TREASURY_AUCTIONS_PANE_ID, () => { const info: PaneFooterSegment[] = []; if (status === "loading") info.push({ id: "loading", parts: [{ text: "loading", tone: "muted" }] }); if (error) info.push({ id: "error", parts: [{ text: error, tone: "warning" }] }); if (stale) info.push({ id: "stale", parts: [{ text: "stale cache", tone: "warning" }] }); if (fetchedAt) { info.push({ id: "updated", parts: [{ text: formatRelativeAge(fetchedAt), tone: "muted" }] }); } return { info, hints: detailOpen || auctions.length === 0 ? [] : [ { id: "search", key: "/", label: "search", onPress: focusSearch }, { id: "filter", key: "f", label: "ilter", onPress: cycleFilter }, ], }; }, [ auctions.length, cycleFilter, detailOpen, error, fetchedAt, focusSearch, stale, status, ]); // The pane menu names the sort keys; "Sort by…" there picks a column directly. usePaneMenuItems("treasury-auctions:sort-keys", () => detailOpen || auctions.length === 0 ? null : [ { id: "sort-next", label: "Next Sort Column", accelerator: "]", onSelect: () => cycleSort(1) }, { id: "sort-previous", label: "Previous Sort Column", accelerator: "[", onSelect: () => cycleSort(-1) }, ], [auctions.length, cycleSort, detailOpen]); const filterTabs = useMemo( () => AUCTION_FILTERS.map((entry) => ({ label: entry.label, value: entry.value })), [], ); const tabsInHeader = usePaneHeaderTabs({ tabs: filterTabs, activeValue: filter, onSelect: (value) => selectFilter(value as AuctionFilter), focused: focused && !detailOpen && !searchFocused, }); const tabRows = tabsInHeader ? 0 : 1; const tabs = tabsInHeader ? null : ( selectFilter(value as AuctionFilter)} compact variant="bare" focused={focused && !detailOpen && !searchFocused} /> ); if (status === "loading" && auctions.length === 0) { return ( {tabs} ); } if (error && auctions.length === 0) { return ( {tabs} ); } return ( {tabs} focused={focused && !searchFocused} detailOpen={detailOpen && !!selected} onBack={() => setDetailOpen(false)} detailContent={selected ? : null} detailTitle={selected ? `${selected.secType} ${selected.securityTerm}` : undefined} rootBefore={( { if (active) focusSearch(); else blurSearch(); }, focusToken: searchFocusToken, inputRef: searchInputRef, onNavigateDown: blurSearch, }} /> )} onRootKeyDown={handleRootKeyDown} onDetailKeyDown={handlePaneKey} selection={{ kind: "id", selectedId, getId: (auction) => auction.id, onChange: (id) => setSelectedId(id), }} onActivate={() => { blurSearch(); setDetailOpen(true); }} rootWidth={width} rootHeight={Math.max(1, height - tabRows)} columns={columns} items={rows} sortColumnId={sortPreference.columnId} sortDirection={sortPreference.direction} onHeaderClick={(columnId) => setSortPreference((current) => ( nextAuctionSort(current, columnId as AuctionColumnId) ))} getItemKey={auctionKey} renderCell={renderAuctionRow} emptyStateTitle={searchQuery.trim() ? "No matching auctions." : "No recent auctions."} emptyStateHint={searchQuery.trim() ? "Clear search." : undefined} /> ); }