Sparkline Grid

Grille de KPI composee de tuiles de valeurs etiquetees, chacune associee a une tendance sparkline compacte. Installez Sparkline Grid depuis le registre VLLNT UI avec la CLI shadcn.

Web
Signaler un bug

Grille de KPI composee de tuiles de valeurs etiquetees, chacune associee a une tendance sparkline compacte. 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 Sparkline Grid 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.com/r/sparkline-grid.json

Source

import * as React from "react"; import { ArrowDownRight, ArrowUpRight } from "lucide-react"; import { formatChange } from "../../lib/format"; import { cn } from "../../lib/utils"; export type SparklineGridItem = { change: number; data: number[]; label: string; value: string; }; export type SparklineGridProps = { columns?: 2 | 3 | 4; items: SparklineGridItem[]; } & React.HTMLAttributes<HTMLDivElement> & { ref?: React.Ref<HTMLDivElement>; }; function buildSparklinePath(data: number[], width: number, height: number) { const min = Math.min(...data); const max = Math.max(...data); const range = max - min || 1; return data .map((value, index) => { const x = data.length === 1 ? width / 2 : (index / (data.length - 1)) * width; const y = height - ((value - min) / range) * height; return `${index === 0 ? "M" : "L"}${x},${y}`; }) .join(" "); } const gridColumns = { 2: "md:grid-cols-2", 3: "md:grid-cols-2 xl:grid-cols-3", 4: "md:grid-cols-2 xl:grid-cols-4", }; export const SparklineGrid = (props: SparklineGridProps) => { const { className, columns = 2, items, ref: reference, ...rest } = props; if (items.length === 0) { return null; } return ( <div className={cn("grid grid-cols-1 gap-4", gridColumns[columns], className)} ref={reference} {...rest} > {items.map((item) => { const isPositive = item.change >= 0; const TrendIcon = isPositive ? ArrowUpRight : ArrowDownRight; const strokeClass = isPositive ? "text-emerald-600 dark:text-emerald-400" : "text-rose-600 dark:text-rose-400"; return ( <section className="rounded-2xl border border-border bg-card/80 p-4 shadow-sm" key={item.label} > <div className="mb-4 flex items-start justify-between gap-3"> <div> <p className="text-sm font-medium text-muted-foreground"> {item.label} </p> <p className="mt-1 text-2xl font-semibold tracking-tight text-foreground"> {item.value} </p> </div> <div className={cn( "inline-flex items-center gap-1 rounded-full border px-2.5 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.5" /> {formatChange(item.change)} </div> </div> <div className="rounded-xl border border-border/60 bg-muted/20 p-3"> <svg aria-label={`${item.label} sparkline`} className="h-16 w-full" role="img" viewBox="0 0 120 48" > <path className={strokeClass} d={buildSparklinePath(item.data, 120, 48)} fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5" vectorEffect="non-scaling-stroke" /> </svg> </div> </section> ); })} </div> ); }; SparklineGrid.displayName = "SparklineGrid";

Stories

Explorez chaque variante et etat dans le Storybook interactif :

Apercu

Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.

Dependances

  • @vllnt/ui@^0.3.0