import { Box, Text, useTickerContextMenu, useUiCapabilities } from "../../../ui"; import { TextAttributes } from "../../../ui"; import { blendHex, colors, priceColor } from "../../../theme/colors"; import { getSharedRegistry } from "../../../plugins/registry"; import type { Quote } from "../../../types/financials"; import { useInlineTickerQuote, type InlineTickerCatalogEntry } from "../../../state/hooks/inline-tickers"; import { getTickerBadgeText, type TickerBadgeStatus } from "./format"; import { tf } from "../../../i18n"; import { usePaneLinkMenuEntry } from "../../ui/external-link"; export interface TickerBadgeProps { symbol: string; status: TickerBadgeStatus; quote: Quote | null; liveQuote?: boolean; hovered?: boolean; /** Text budget for the chip, so a narrow column drops the price, not the badge. */ maxTextWidth?: number; /** Trailing gap to the next chip, which the last chip in a row does not need. */ trailingGap?: boolean; onHoverStart?: () => void; onHoverEnd?: () => void; onOpen: (symbol: string) => void; } const NATIVE_CHIP_STYLE = { height: "calc(100% - 4px)", borderRadius: 3 }; export function TickerBadge({ symbol, status, quote, liveQuote = true, hovered = false, maxTextWidth, trailingGap = true, onHoverStart, onHoverEnd, onOpen, }: TickerBadgeProps) { const registry = getSharedRegistry(); const { nativeContextMenu, nativePaneChrome } = useUiCapabilities(); const ticker = typeof registry?.getTickerFn === "function" ? registry.getTickerFn(symbol) : null; const financials = typeof registry?.getDataFn === "function" ? registry.getDataFn(symbol) : null; const openTickerContextMenu = useTickerContextMenu({ ticker, financials, onOpen, }); const quoteForDisplay = liveQuote ? quote : null; const tone = status === "ready" && quoteForDisplay ? priceColor(quoteForDisplay.changePercent) : colors.borderFocused; const text = getTickerBadgeText({ symbol, status, quote, liveQuote, hovered, maxTextWidth }); const color = hovered ? colors.textBright : tone; const backgroundColor = hovered ? blendHex(colors.bg, tone, 0.42) : blendHex(colors.bg, tone, 0.18); // An ambiguous symbol is worth opening: that is how the listing gets picked. const interactive = status !== "loading"; usePaneLinkMenuEntry(interactive ? `ticker:${symbol}` : null, tf("Open {ticker}", { ticker: symbol }), () => onOpen(symbol)); return ( { onHoverStart?.(); }} onMouseOut={() => { onHoverEnd?.(); }} onMouseDown={(event: any) => { if (event.button === 2) { if (nativeContextMenu !== true) { void openTickerContextMenu(event); } return; } event.stopPropagation?.(); event.preventDefault?.(); if (!interactive) return; onOpen(symbol); }} onContextMenu={(event: any) => { void openTickerContextMenu(event); }} > {text} ); } export interface InlineTickerBadgeProps extends Omit { entry: InlineTickerCatalogEntry; } function inlineBadgeStatus(entry: InlineTickerCatalogEntry): TickerBadgeStatus { if (entry.status === "ambiguous" || entry.status === "loading") return entry.status; return "ready"; } /** * The badge for one catalog entry in running text. A catalog built with * `badgeQuotes` leaves the price to this badge, which follows its own symbol's * quote, so a tick re-renders the chip instead of the message or document * around it. */ export function InlineTickerBadge({ entry, ...props }: InlineTickerBadgeProps) { const liveQuote = useInlineTickerQuote(entry.liveBadge ? props.symbol : null, entry.ticker); return ; }