extraído de climatempo
Ranking
Ranking com posição, nome, barra proporcional ao valor, linha de detalhe e destaque dos itens selecionados.
Variações
Playground
Props
Geradas dos tipos TypeScript do componente; a descrição vem dos comentários /** */.
<Ranking>
Ranking: posição, nome, barra proporcional ao valor e o número. Os itens aparecem na ordem recebida (ordene antes). Os destacados ganham fundo e etiqueta.
| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
| itensobrigatória | ItemDoRanking[] | — | |
| maximo | number | 100 | Valor que enche a barra inteira. |
| destacados | string[] | — | Ids a destacar. |
| rotuloDoDestaque | string | "selecionado" | |
| formatarValor | (valor: number) => string | formatarPadrao |
Código
stories.tsx tem os exemplos usados no preview e no playground.
ranking/Ranking.tsx
import type { ReactNode } from "react";
export type ItemDoRanking = {
id: string;
nome: string;
/** Texto menor ao lado do nome: sigla, categoria, região... */
complemento?: string;
valor: number;
/** Cor da barra (padrão: a cor de destaque do tema). */
cor?: string;
/** Linha abaixo da barra, por exemplo as notas que compõem o valor. */
detalhe?: ReactNode;
};
const formatarPadrao = (v: number) =>
Number.isNaN(v) ? "—" : v.toLocaleString("pt-BR", { minimumFractionDigits: 1, maximumFractionDigits: 1 });
/**
* Ranking: posição, nome, barra proporcional ao valor e o número. Os itens
* aparecem na ordem recebida (ordene antes). Os destacados ganham fundo e etiqueta.
*/
export function Ranking({
itens,
maximo = 100,
destacados,
rotuloDoDestaque = "selecionado",
formatarValor = formatarPadrao,
}: {
itens: ItemDoRanking[];
/** Valor que enche a barra inteira. */
maximo?: number;
/** Ids a destacar. */
destacados?: string[];
rotuloDoDestaque?: string;
formatarValor?: (valor: number) => string;
}) {
const largura = (v: number) => `${Number.isNaN(v) ? 0 : Math.min(100, Math.max(0, (v / maximo) * 100))}%`;
return (
<ol>
{itens.map((item, i) => {
const destaque = destacados?.includes(item.id) ?? false;
return (
<li
key={item.id}
aria-current={destaque ? "true" : undefined}
className={`grid grid-cols-[2.25rem_minmax(0,1fr)_4.5rem] items-center gap-4 border-t border-rule py-5 last:border-b sm:grid-cols-[3rem_minmax(0,1fr)_6rem] ${
destaque ? "-mx-4 bg-surface px-4" : ""
}`}
>
<span className={`font-display text-2xl font-bold sm:text-3xl ${i === 0 ? "text-accent" : "text-muted"}`}>
{i + 1}
</span>
<div className="flex min-w-0 flex-col gap-2.5">
<div className="flex flex-wrap items-baseline gap-x-2.5">
<span className="font-display text-xl font-bold sm:text-2xl">{item.nome}</span>
{item.complemento && <span className="text-sm text-muted">{item.complemento}</span>}
{destaque && (
<span className="text-xs font-semibold tracking-wider text-accent uppercase">{rotuloDoDestaque}</span>
)}
</div>
<div aria-hidden="true" className="h-3.5 rounded-r-sm bg-track">
<div
className="h-3.5 rounded-r-sm"
style={{ width: largura(item.valor), background: item.cor ?? "var(--accent)" }}
/>
</div>
{item.detalhe && <span className="text-sm text-ink-2">{item.detalhe}</span>}
</div>
<span className="text-right font-display text-3xl font-bold tracking-tight tabular-nums sm:text-4xl">
{formatarValor(item.valor)}
</span>
</li>
);
})}
</ol>
);
}
ranking/stories.tsx
import { defineStories } from "@/registry/stories";
import { Ranking } from "./Ranking";
const EQUIPES = [
{ id: "norte", nome: "Equipe Norte", complemento: "AM", valor: 91.4, detalhe: "Vendas 95 · Retenção 88 · NPS 90" },
{ id: "sul", nome: "Equipe Sul", complemento: "RS", valor: 84.2, detalhe: "Vendas 80 · Retenção 91 · NPS 82" },
{ id: "leste", nome: "Equipe Leste", complemento: "BA", valor: 72.8, detalhe: "Vendas 70 · Retenção 77 · NPS 71" },
{ id: "oeste", nome: "Equipe Oeste", complemento: "MT", valor: 58.1, detalhe: "Vendas 52 · Retenção 66 · NPS 57" },
{ id: "centro", nome: "Equipe Centro", complemento: "DF", valor: 44.6, detalhe: "Vendas 41 · Retenção 50 · NPS 43" },
];
export default defineStories({
args: {
quantidade: 4,
destacado: "sul",
rotuloDoDestaque: "sua equipe",
maximo: 100,
coresPorItem: true,
comDetalhe: true,
},
controls: {
quantidade: { type: "number", label: "itens (quantidade)", min: 1, max: 5 },
destacado: { type: "select", label: "destacados", options: ["nenhum", "norte", "sul", "leste", "oeste", "centro"] },
rotuloDoDestaque: { type: "text" },
maximo: { type: "number", min: 50, max: 200, step: 10 },
coresPorItem: { type: "boolean", label: "cor própria em cada item" },
comDetalhe: { type: "boolean", label: "detalhe abaixo da barra" },
},
render: (a) => (
<div className="w-full">
<Ranking
maximo={a.maximo}
destacados={a.destacado === "nenhum" ? undefined : [a.destacado]}
rotuloDoDestaque={a.rotuloDoDestaque}
itens={EQUIPES.slice(0, a.quantidade).map((e, i) => ({
...e,
cor: a.coresPorItem ? `var(--series-${i + 1})` : undefined,
detalhe: a.comDetalhe ? e.detalhe : undefined,
}))}
/>
</div>
),
stories: [
{ name: "Com destaque" },
{ name: "Uma cor só, sem detalhes", args: { coresPorItem: false, comDetalhe: false, destacado: "nenhum", quantidade: 5 } },
],
});
Para usar em outro projeto, copie também
Os componentes de que este depende e o tema. O CSS do tema é um arquivo só: importe depois do Tailwind.
Tema escolhido no cabeçalho (com a ponte para o Tailwind)
Ver o CSS do tema
tema-editorial.css · Editorial
/* Tema "Editorial", gerado no Component Vault.
Importe depois de @import "tailwindcss". Carregue as fontes "Archivo" e "Inter" no projeto. */
@theme inline {
--color-page: var(--page);
--color-surface: var(--surface);
--color-track: var(--track);
--color-grid: var(--grid);
--color-ink: var(--ink);
--color-ink-2: var(--ink-2);
--color-muted: var(--muted);
--color-rule: var(--rule);
--color-rule-strong: var(--rule-strong);
--color-axis: var(--axis);
--color-accent: var(--accent);
--color-success: var(--success);
--color-warning: var(--warning);
--color-critical: var(--critical);
--font-display: var(--fonte-titulo);
--shadow-flutuante: var(--sombra-flutuante);
}
:root {
color-scheme: light dark;
--page: light-dark(#ffffff, #0f0f0e);
--surface: light-dark(#f5f5f5, #1a1a19);
--track: light-dark(#f0f0f0, #262624);
--grid: light-dark(#ededed, #232321);
--ink: light-dark(#111111, #f0efec);
--ink-2: light-dark(#4a4a4a, #c3c2b7);
--muted: light-dark(#666666, #a3a29a);
--rule: light-dark(#e3e3e3, #2c2c2a);
--rule-strong: light-dark(#111111, #f0efec);
--axis: light-dark(#9a9a9a, #6b6a64);
--accent: light-dark(#1a56db, #7aa2f7);
--success: light-dark(#13865c, #3fc48f);
--warning: #fab219;
--critical: #d03b3b;
--series-1: light-dark(#2a78d6, #3987e5);
--series-2: light-dark(#eb6834, #d95926);
--series-3: light-dark(#1baf7a, #199e70);
--series-4: light-dark(#eda100, #c98500);
--series-5: light-dark(#e87ba4, #d55181);
--series-6: #008300;
--series-7: light-dark(#6250d6, #9085e9);
--series-8: light-dark(#e34948, #e66767);
--fonte-titulo: "Archivo", system-ui, -apple-system, "Segoe UI", sans-serif;
--fonte-texto: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
--spacing: 0.25rem;
--text-xs: 0.8rem;
--text-sm: 0.8944rem;
--text-base: 1rem;
--text-lg: 1.118rem;
--text-xl: 1.25rem;
--text-2xl: 1.5625rem;
--text-3xl: 1.9531rem;
--text-4xl: 2.4414rem;
--text-5xl: 3.0518rem;
--text-6xl: 3.8147rem;
--text-7xl: 4.7684rem;
--radius-xs: 2px;
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-xl: 12px;
--radius-2xl: 16px;
--radius-3xl: 24px;
--radius-4xl: 32px;
--sombra-flutuante: 0 8px 24px -6px rgb(0 0 0 / 0.14), 0 2px 6px -2px rgb(0 0 0 / 0.08);
color: var(--ink);
font-family: var(--fonte-texto);
}
Na vitrine, a ponte com o Tailwind fica em src/themes/tokens.css:
src/themes/tokens.css
/* Ponte entre os tokens do tema e o Tailwind.
Os componentes da biblioteca usam classes comuns do Tailwind (p-5, text-sm,
rounded-xl...) e as cores semânticas abaixo (bg-page, text-ink...). Dentro de
[data-theme="vault"], o tema redefine as variáveis que essas classes leem:
--spacing, --text-*, --radius-* e as cores. Os valores vêm do tema escolhido
na vitrine (src/themes/presets.ts) e são gerados por themeCss(). */
@theme inline {
--color-page: var(--page);
--color-surface: var(--surface);
--color-ink: var(--ink);
--color-ink-2: var(--ink-2);
--color-muted: var(--muted);
--color-rule: var(--rule);
--color-rule-strong: var(--rule-strong);
--color-track: var(--track);
--color-grid: var(--grid);
--color-axis: var(--axis);
--color-accent: var(--accent);
--color-success: var(--success);
--color-warning: var(--warning);
--color-critical: var(--critical);
--font-display: var(--fonte-titulo);
--shadow-flutuante: var(--sombra-flutuante);
}