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.

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.ai/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