Order Book
Carnet de profondeur achat/vente de niveau II avec barres de taille cumulee et affichage du spread. Installez Order Book depuis le registre VLLNT UI avec la CLI shadcn.
Carnet de profondeur achat/vente de niveau II avec barres de taille cumulee et affichage du spread. Fait partie de la famille data dans VLLNT UI, il est publie comme entree de registre lisible par machine — copiez la source directement dans votre application avec la CLI shadcn et possedez-la, sans dependance d'execution a une bibliotheque de composants.
Apercu
Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.
Installation
Ajoutez Order Book a votre projet avec la CLI shadcn. La source arrive dans votre code, prete a etre adaptee :
pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/order-book.jsonSource
import * as React from "react";
import type { HeadingTag } from "../../lib/types";
import { cn } from "../../lib/utils";
export type OrderBookLevel = {
price: number;
size: number;
total?: number;
};
export type OrderBookProps = {
/** Heading tag for the main "Order book" title. Defaults to `h2`. */
as?: HeadingTag;
asks: OrderBookLevel[];
bids: OrderBookLevel[];
precision?: number;
} & React.HTMLAttributes<HTMLDivElement>;
function withCumulativeTotal(levels: OrderBookLevel[]) {
let runningTotal = 0;
return levels.map((level) => {
runningTotal += level.total ?? level.size;
return {
...level,
total: level.total ?? runningTotal,
};
});
}
function formatNumber(value: number, precision = 2) {
return value.toLocaleString(undefined, {
maximumFractionDigits: precision,
minimumFractionDigits: precision,
});
}
function BookSide({
accent,
levels,
precision,
title,
}: {
accent: "ask" | "bid";
levels: OrderBookLevel[];
precision: number;
title: string;
}) {
const maxTotal = Math.max(...levels.map((level) => level.total ?? 0), 1);
const barClassName =
accent === "ask"
? "bg-rose-500/12 border-rose-500/15"
: "bg-emerald-500/12 border-emerald-500/15";
const priceClassName =
accent === "ask"
? "text-rose-600 dark:text-rose-400"
: "text-emerald-600 dark:text-emerald-400";
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold uppercase tracking-[0.24em] text-muted-foreground">
{title}
</h3>
<span className="text-xs text-muted-foreground">
Depth by total size
</span>
</div>
<div className="rounded-2xl border border-border/70 bg-background/60 p-2">
<div className="grid grid-cols-[1.2fr_1fr_1fr] gap-3 px-3 py-2 text-[11px] font-medium uppercase tracking-[0.2em] text-muted-foreground">
<span>Price</span>
<span className="text-right">Size</span>
<span className="text-right">Total</span>
</div>
<div className="space-y-1">
{levels.map((level) => {
const width = `${((level.total ?? 0) / maxTotal) * 100}%`;
return (
<div
className="relative overflow-hidden rounded-xl border border-transparent px-3 py-2"
key={`${accent}-${level.price}-${level.size}`}
>
<div
className={cn(
"absolute inset-y-0 right-0 rounded-xl border",
barClassName,
)}
style={{ width }}
/>
<div className="relative grid grid-cols-[1.2fr_1fr_1fr] gap-3 text-sm tabular-nums">
<span className={cn("font-medium", priceClassName)}>
{formatNumber(level.price, precision)}
</span>
<span className="text-right text-foreground">
{formatNumber(level.size, 3)}
</span>
<span className="text-right text-muted-foreground">
{formatNumber(level.total ?? 0, 3)}
</span>
</div>
</div>
);
})}
</div>
</div>
</div>
);
}
export const OrderBook = ({
as: Heading = "h2",
asks,
bids,
className,
precision = 2,
ref: reference,
...props
}: OrderBookProps & { ref?: React.Ref<HTMLDivElement> }) => {
if (asks.length === 0 && bids.length === 0) {
return null;
}
const askLevels = withCumulativeTotal(asks);
const bidLevels = withCumulativeTotal(bids);
const bestAsk = askLevels[0];
const bestBid = bidLevels[0];
const spread =
bestAsk && bestBid ? Math.max(bestAsk.price - bestBid.price, 0) : 0;
return (
<div
className={cn(
"rounded-2xl border border-border bg-card/80 p-4 shadow-sm",
className,
)}
ref={reference}
{...props}
>
<div className="mb-4 flex flex-wrap items-start justify-between gap-3">
<div>
<p className="text-xs font-medium uppercase tracking-[0.28em] text-muted-foreground">
Level II
</p>
<Heading className="text-lg font-semibold text-foreground">
Order book
</Heading>
</div>
<div className="rounded-full border border-border bg-background/70 px-3 py-1 text-sm text-muted-foreground tabular-nums">
Spread {formatNumber(spread, precision)}
</div>
</div>
<div className="grid gap-4 lg:grid-cols-2">
<BookSide
accent="ask"
levels={askLevels}
precision={precision}
title="Asks"
/>
<BookSide
accent="bid"
levels={bidLevels}
precision={precision}
title="Bids"
/>
</div>
</div>
);
};
OrderBook.displayName = "OrderBook";
Stories
Explorez chaque variante et etat dans le Storybook interactif :
Apercu
Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.