Watchlist

Tracked-symbol list with price, change, and advancing/declining summary.

Report a bug

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Installation

pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/watchlist.json

Storybook

Explore all variants, controls, and accessibility checks in the interactive Storybook playground.

View in Storybook

3 stories available:

Code

import * as React from "react"; import { ArrowDownRight, ArrowUpRight, Star } from "lucide-react"; import type { HeadingTag } from "../../lib/types"; import { cn } from "../../lib/utils"; export type WatchlistItem = { change: number; name?: string; price: number | string; starred?: boolean; symbol: string; volume?: string; }; export type WatchlistProps = { /** Heading tag for the title. Defaults to `h2`. */ as?: HeadingTag; eyebrow?: string; items: WatchlistItem[]; title?: string; } & React.HTMLAttributes<HTMLDivElement>; function formatPrice(price: number | string): string { return typeof price === "number" ? price.toLocaleString(undefined, { maximumFractionDigits: 2, minimumFractionDigits: 2, }) : price; } function formatChange(change: number): string { const sign = change > 0 ? "+" : ""; return `${sign}${change.toFixed(2)}%`; } function WatchlistRow({ item }: { item: WatchlistItem }): React.JSX.Element { const isPositive = item.change >= 0; const TrendIcon = isPositive ? ArrowUpRight : ArrowDownRight; return ( <li className="grid grid-cols-[auto_minmax(0,1fr)_auto_auto] items-center gap-3 rounded-xl px-3 py-2.5 transition-colors hover:bg-muted/40"> <span aria-hidden="true" className={cn( "flex size-7 items-center justify-center rounded-full border", item.starred ? "border-amber-400/40 bg-amber-400/10 text-amber-500" : "border-border bg-background text-muted-foreground", )} > <Star className={cn("size-3.5", item.starred && "fill-current")} strokeWidth={1.75} /> </span> <div className="min-w-0"> <p className="truncate text-sm font-semibold text-foreground"> {item.symbol} </p> {item.name ? ( <p className="truncate text-xs text-muted-foreground">{item.name}</p> ) : null} </div> <div className="text-right"> <p className="text-sm font-semibold tabular-nums text-foreground"> {formatPrice(item.price)} </p> {item.volume ? ( <p className="text-[11px] text-muted-foreground tabular-nums"> {item.volume} </p> ) : null} </div> <span className={cn( "inline-flex items-center gap-1 rounded-full border px-2 py-1 text-xs font-medium tabular-nums", isPositive ? "border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400" : "border-rose-500/30 bg-rose-500/10 text-rose-600 dark:text-rose-400", )} > <TrendIcon className="size-3" /> {formatChange(item.change)} </span> </li> ); } export const Watchlist = React.forwardRef<HTMLDivElement, WatchlistProps>( ( { as: Heading = "h2", className, eyebrow = "Tracked symbols", items, title = "Watchlist", ...props }, reference, ) => { if (items.length === 0) { return null; } const advancing = items.filter((item) => item.change >= 0).length; const declining = items.length - advancing; return ( <section aria-label={title} className={cn( "rounded-2xl border border-border bg-card/80 p-4 shadow-sm", className, )} ref={reference} {...props} > <header className="mb-3 flex flex-wrap items-start justify-between gap-3"> <div> <p className="text-xs font-medium uppercase tracking-[0.28em] text-muted-foreground"> {eyebrow} </p> <Heading className="text-lg font-semibold text-foreground"> {title} </Heading> </div> <div className="flex items-center gap-2 text-xs text-muted-foreground"> <span className="inline-flex items-center gap-1 rounded-full border border-emerald-500/30 bg-emerald-500/10 px-2 py-1 text-emerald-600 dark:text-emerald-400"> <ArrowUpRight className="size-3" /> {advancing} up </span> <span className="inline-flex items-center gap-1 rounded-full border border-rose-500/30 bg-rose-500/10 px-2 py-1 text-rose-600 dark:text-rose-400"> <ArrowDownRight className="size-3" /> {declining} down </span> </div> </header> <ul className="divide-y divide-border/60"> {items.map((item) => ( <WatchlistRow item={item} key={item.symbol} /> ))} </ul> </section> ); }, ); Watchlist.displayName = "Watchlist";

Dependencies

  • @vllnt/ui@^0.2.1