Grade Numerada
Seção de destaques em cartões marcados por números grandes (“01”, “02”...) no lugar de ícones, com título e texto. As colunas seguem a quantidade de itens e a variação alternada troca os fundos para seções em sequência.
Playground
Props
Geradas dos tipos TypeScript do componente; a descrição vem dos comentários /** */.
<GradeNumerada>
Seção de destaques em cartões, cada um marcado por um número grande ("01", "02"...) no lugar de ícones, com título e texto curto. As colunas se ajustam à quantidade de itens; `alternada` troca os fundos para seções em sequência.
| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
| titulo | string | number | bigint | boolean | ReactElement<unknown, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | Promise<AwaitedReactNode> | — | Título da seção. Sem ele, só a grade. |
| itensobrigatória | ItemNumerado[] | — | |
| alternada | boolean | false | Troca os fundos: seção em `surface` e cartões em `page`. Use em seções seguidas para elas se alternarem. |
Código
stories.tsx tem os exemplos usados no preview e no playground.
"use client";
import { useId, type ReactNode } from "react";
/**
* Colunas pela quantidade de itens. As classes precisam existir inteiras no
* código: o Tailwind varre o texto dos arquivos, então `lg:grid-cols-${n}`
* montado na hora não geraria nada.
*/
const COLUNAS: Record<number, string> = {
1: "sm:grid-cols-1",
2: "sm:grid-cols-2",
3: "sm:grid-cols-2 lg:grid-cols-3",
4: "sm:grid-cols-2 lg:grid-cols-4",
5: "sm:grid-cols-2 lg:grid-cols-3",
6: "sm:grid-cols-2 lg:grid-cols-3",
};
export type ItemNumerado = { titulo: string; texto: ReactNode };
/**
* Seção de destaques em cartões, cada um marcado por um número grande ("01",
* "02"...) no lugar de ícones, com título e texto curto. As colunas se ajustam
* à quantidade de itens; `alternada` troca os fundos para seções em sequência.
*/
export function GradeNumerada({
titulo,
itens,
alternada = false,
}: {
/** Título da seção. Sem ele, só a grade. */
titulo?: ReactNode;
itens: ItemNumerado[];
/**
* Troca os fundos: seção em `surface` e cartões em `page`. Use em seções
* seguidas para elas se alternarem.
*/
alternada?: boolean;
}) {
const idTitulo = useId();
return (
<section
aria-labelledby={titulo ? idTitulo : undefined}
className={`w-full py-20 lg:py-28 ${alternada ? "bg-surface" : "bg-page"}`}
>
<div className="mx-auto max-w-7xl px-6 sm:px-10 lg:px-16">
{titulo && (
<h2
id={idTitulo}
className="mb-12 max-w-3xl font-display text-4xl leading-[1.1] font-bold tracking-[-0.01em] sm:text-5xl lg:mb-16"
>
{titulo}
</h2>
)}
<ul className={`grid gap-4 ${COLUNAS[itens.length] ?? COLUNAS[4]}`}>
{itens.map((item, i) => (
<li
key={item.titulo}
className={`flex flex-col rounded-3xl p-7 lg:p-8 ${alternada ? "bg-page" : "bg-surface"}`}
>
{/* O índice faz o papel de marcador, no lugar de um ícone */}
<span aria-hidden className="font-display text-2xl leading-none text-accent lg:text-3xl">
{String(i + 1).padStart(2, "0")}
</span>
<h3 className="mt-6 text-lg leading-snug font-medium text-ink">{item.titulo}</h3>
<p className="mt-2 text-sm leading-relaxed text-ink-2">{item.texto}</p>
</li>
))}
</ul>
</div>
</section>
);
}
import { defineStories } from "@/registry/stories";
import { GradeNumerada } from "./GradeNumerada";
const DIFERENCIAIS = [
{ titulo: "Atendimento dedicado", texto: "Uma pessoa da nossa equipe acompanha a sua conta do começo ao fim, sem fila de chamados." },
{ titulo: "Relatórios mensais", texto: "Todo mês, um resumo claro do que foi feito, do que mudou e dos próximos passos." },
{ titulo: "Integração com o seu sistema", texto: "Conectamos aos programas que você já usa, sem retrabalho nem planilhas paralelas." },
{ titulo: "Implantação em duas semanas", texto: "Do contrato ao uso diário em poucos dias, com treinamento para toda a equipe." },
{ titulo: "Dados protegidos", texto: "Criptografia, cópias de segurança diárias e acesso controlado por pessoa." },
{ titulo: "Suporte em português", texto: "Chat e telefone em dias úteis, com resposta em até duas horas." },
];
export default defineStories({
args: { titulo: "Por que trabalhar com a gente", quantidade: 4, alternada: false },
controls: {
titulo: { type: "text", label: "titulo (vazio = sem)" },
quantidade: { type: "number", label: "itens (quantidade)", min: 1, max: 6 },
alternada: { type: "boolean" },
},
render: (a) => (
<GradeNumerada
titulo={a.titulo || undefined}
itens={DIFERENCIAIS.slice(0, a.quantidade)}
alternada={a.alternada}
/>
),
stories: [
{ name: "Quatro itens" },
{ name: "Três itens, alternada", args: { quantidade: 3, alternada: true } },
{ name: "Dois itens", args: { titulo: "O que está incluído", quantidade: 2 } },
{ name: "Seis itens, sem título", args: { titulo: "", quantidade: 6 } },
{
name: "Seções em sequência",
render: () => (
<div className="w-full">
<GradeNumerada titulo="Para a sua equipe" itens={DIFERENCIAIS.slice(0, 3)} />
<GradeNumerada titulo="Para a sua empresa" itens={DIFERENCIAIS.slice(3, 6)} alternada />
</div>
),
},
],
});
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", 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:
/* 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);
}