Barra Lateral
Navegação lateral que recolhe para uma faixa só de ícones, com grupos em sanfona, indicador da página atual, contadores, espaço para marca e conta e botão de recolher na borda. Os links são <a> ou o componente do seu roteador.
Playground
Props
Geradas dos tipos TypeScript do componente; a descrição vem dos comentários /** */.
<BarraLateral>
Barra lateral de navegação que recolhe para uma faixa só de ícones (com o nome no `title`). Tem grupos que abrem como sanfona, indicador do item atual, contadores, cabeçalho (marca) e rodapé (conta) e o botão de recolher na borda. Os links são `<a>`; para usar o link do seu roteador, passe `renderizarLink`. `recolhida` pode ser controlada (com `aoMudarRecolhida`) ou não (`recolhidaInicial`). Ocupa a altura do contêiner: coloque-a num pai com altura definida.
| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
| itensobrigatória | ItemDaBarraLateral[] | — | |
| cabecalho | string | number | bigint | boolean | ReactElement<unknown, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | Promise<AwaitedReactNode> | ((recolhida: boolean) => ReactNode) | — | Topo (marca). Pode ser uma função que recebe `recolhida` para mostrar só o símbolo. |
| rodape | string | number | bigint | boolean | ReactElement<unknown, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | Promise<AwaitedReactNode> | ((recolhida: boolean) => ReactNode) | — | Pé (conta, ajuda). Também aceita uma função de `recolhida`. |
| recolhida | boolean | — | Estado controlado. Use com `aoMudarRecolhida`. |
| recolhidaInicial | boolean | false | Estado inicial quando não controlada. |
| aoMudarRecolhida | (recolhida: boolean) => void | — | |
| renderizarLink | (props: PropsDoLink) => ReactNode | linkPadrao | Troca o `<a>` pelo link do roteador: `(p) => <Link {...p} />`. |
| umGrupoPorVez | boolean | true | Abrir um grupo fecha o outro. |
| rotulo | string | "Navegação principal" | Nome da navegação para leitores de tela. |
| textos | { recolher: string; expandir: string; } | { recolher: "Recolher menu", expandir: "Expandir menu" } |
Código
stories.tsx tem os exemplos usados no preview e no playground.
"use client";
import { useId, useState, type ReactNode } from "react";
import { useSemMovimento } from "@/library/revelar/Movimento";
export type ItemDaBarraLateral = {
rotulo: string;
/** Destino do link. Sem `href` (e sem filhos), o item vira um botão que chama `aoClicar`. */
href?: string;
/** Ícone (SVG de ~20px). Sem ícone, aparece a inicial do rótulo. */
icone?: ReactNode;
/** Página atual. Num grupo, marcar um filho como ativo abre o grupo e destaca o pai. */
ativo?: boolean;
/** Número ou selo à direita ("12", "Novo"). Recolhida, vira um ponto no ícone. */
contador?: number | string;
/** Subitens: o item vira um grupo que abre e fecha. Um nível só. */
filhos?: ItemDaBarraLateral[];
aoClicar?: () => void;
/** Linha divisória antes do item, para separar seções (ex.: administração). */
separarAntes?: boolean;
};
/** O que `renderizarLink` recebe: repasse tudo ao seu componente de link. */
export type PropsDoLink = {
href: string;
className: string;
children: ReactNode;
title?: string;
"aria-label"?: string;
"aria-current"?: "page";
};
type Conteudo = ReactNode | ((recolhida: boolean) => ReactNode);
const linkPadrao = (props: PropsDoLink) => <a {...props} />;
const LINHA =
"relative flex w-full cursor-pointer items-center overflow-hidden rounded-lg text-left outline-accent focus-visible:outline-2 focus-visible:-outline-offset-2";
const ATIVA =
"bg-accent/10 font-semibold text-accent before:absolute before:inset-y-2 before:left-0 before:w-0.5 before:rounded-full before:bg-accent";
const INATIVA = "text-ink-2 hover:bg-surface hover:text-ink";
/** Chave de um grupo: o rótulo basta (dois grupos com o mesmo nome confundiriam a pessoa também). */
const chave = (item: ItemDaBarraLateral) => item.rotulo;
/**
* Barra lateral de navegação que recolhe para uma faixa só de ícones (com o nome
* no `title`). Tem grupos que abrem como sanfona, indicador do item atual,
* contadores, cabeçalho (marca) e rodapé (conta) e o botão de recolher na borda.
*
* Os links são `<a>`; para usar o link do seu roteador, passe `renderizarLink`.
* `recolhida` pode ser controlada (com `aoMudarRecolhida`) ou não (`recolhidaInicial`).
* Ocupa a altura do contêiner: coloque-a num pai com altura definida.
*/
export function BarraLateral({
itens,
cabecalho,
rodape,
recolhida: recolhidaControlada,
recolhidaInicial = false,
aoMudarRecolhida,
renderizarLink = linkPadrao,
umGrupoPorVez = true,
rotulo = "Navegação principal",
textos = { recolher: "Recolher menu", expandir: "Expandir menu" },
}: {
itens: ItemDaBarraLateral[];
/** Topo (marca). Pode ser uma função que recebe `recolhida` para mostrar só o símbolo. */
cabecalho?: Conteudo;
/** Pé (conta, ajuda). Também aceita uma função de `recolhida`. */
rodape?: Conteudo;
/** Estado controlado. Use com `aoMudarRecolhida`. */
recolhida?: boolean;
/** Estado inicial quando não controlada. */
recolhidaInicial?: boolean;
aoMudarRecolhida?: (recolhida: boolean) => void;
/** Troca o `<a>` pelo link do roteador: `(p) => <Link {...p} />`. */
renderizarLink?: (props: PropsDoLink) => ReactNode;
/** Abrir um grupo fecha o outro. */
umGrupoPorVez?: boolean;
/** Nome da navegação para leitores de tela. */
rotulo?: string;
textos?: { recolher: string; expandir: string };
}) {
const semMovimento = useSemMovimento();
const idNav = useId();
const [recolhidaInterna, setRecolhidaInterna] = useState(recolhidaInicial);
const recolhida = recolhidaControlada ?? recolhidaInterna;
function mudarRecolhida(valor: boolean) {
if (recolhidaControlada === undefined) setRecolhidaInterna(valor);
aoMudarRecolhida?.(valor);
}
// O grupo do item ativo começa aberto e reabre quando a página atual muda para outro grupo
// (ajuste durante a renderização, sem efeito: não pinta um quadro com o grupo errado).
const grupoAtivo = itens.find((i) => i.filhos?.some((f) => f.ativo));
const chaveAtiva = grupoAtivo ? chave(grupoAtivo) : null;
const [abertos, setAbertos] = useState<string[]>(chaveAtiva ? [chaveAtiva] : []);
const [chaveAnterior, setChaveAnterior] = useState(chaveAtiva);
if (chaveAtiva !== chaveAnterior) {
setChaveAnterior(chaveAtiva);
if (chaveAtiva && !abertos.includes(chaveAtiva)) setAbertos(umGrupoPorVez ? [chaveAtiva] : [...abertos, chaveAtiva]);
}
function alternarGrupo(item: ItemDaBarraLateral) {
const k = chave(item);
if (recolhida) {
// Recolhida, os filhos não cabem: abre a barra já com o grupo aberto.
mudarRecolhida(false);
setAbertos((a) => (umGrupoPorVez ? [k] : a.includes(k) ? a : [...a, k]));
return;
}
setAbertos((a) => (a.includes(k) ? a.filter((x) => x !== k) : umGrupoPorVez ? [k] : [...a, k]));
}
const transicao = semMovimento ? "" : "transition-opacity";
// Ao recolher, o texto some rápido, antes de a faixa ficar estreita e cortá-lo; ao abrir, espera o espaço aparecer.
const someRecolhida = recolhida ? `opacity-0 ${semMovimento ? "" : "duration-100"}` : `opacity-100 ${semMovimento ? "" : "delay-100 duration-200"}`;
const resolver = (c: Conteudo | undefined) => (typeof c === "function" ? c(recolhida) : c);
function conteudoDaLinha(item: ItemDaBarraLateral, extra?: ReactNode) {
return (
<>
<span aria-hidden="true" className="relative flex size-5 shrink-0 items-center justify-center [&_svg]:size-5">
{item.icone ?? <span className="text-sm font-bold uppercase">{item.rotulo.slice(0, 1)}</span>}
{recolhida && item.contador !== undefined && (
<span className="absolute -top-1 -right-1 size-2 rounded-full bg-accent ring-2 ring-page" />
)}
</span>
<span className={`min-w-0 flex-1 truncate whitespace-nowrap ${transicao} ${someRecolhida}`}>{item.rotulo}</span>
{item.contador !== undefined && (
<span
className={`shrink-0 rounded-[max(var(--radius-sm),calc(var(--radius-lg)_-_var(--spacing)*3))] px-2 py-0.5 text-xs leading-none font-semibold tabular-nums ${transicao} ${someRecolhida} ${
item.ativo ? "bg-accent text-page" : "bg-surface text-ink-2"
}`}
>
{item.contador}
</span>
)}
{extra}
</>
);
}
/** Nome acessível quando recolhida (só o ícone aparece): inclui o contador. */
const nomeRecolhido = (item: ItemDaBarraLateral) =>
recolhida ? `${item.rotulo}${item.contador !== undefined ? ` (${item.contador})` : ""}` : undefined;
function linhaSimples(item: ItemDaBarraLateral, filho = false) {
const classe = `${LINHA} ${filho ? "h-9 gap-2 pr-3 pl-11.5 text-sm [&_svg]:size-4" : "h-11 gap-3 px-3.5 text-sm"} ${item.ativo ? ATIVA : INATIVA}`;
// Filhos só aparecem com a barra aberta: ícone pequeno opcional, rótulo e contador discreto.
const conteudo = filho ? (
<>
{item.icone && (
<span aria-hidden="true" className="flex shrink-0">
{item.icone}
</span>
)}
<span className="min-w-0 flex-1 truncate">{item.rotulo}</span>
{item.contador !== undefined && <span className="shrink-0 text-xs text-muted tabular-nums">{item.contador}</span>}
</>
) : (
conteudoDaLinha(item)
);
if (item.href !== undefined) {
return renderizarLink({
href: item.href,
className: classe,
title: recolhida ? item.rotulo : undefined,
"aria-label": nomeRecolhido(item),
"aria-current": item.ativo ? "page" : undefined,
children: conteudo,
});
}
return (
<button
type="button"
onClick={item.aoClicar}
title={recolhida ? item.rotulo : undefined}
aria-label={nomeRecolhido(item)}
aria-current={item.ativo ? "page" : undefined}
className={classe}
>
{conteudo}
</button>
);
}
return (
<div
className={`relative flex h-full shrink-0 flex-col border-r border-rule bg-page py-4 ${recolhida ? "w-20" : "w-64"} ${
semMovimento ? "" : "transition-[width] duration-300 ease-in-out"
}`}
>
<button
type="button"
onClick={() => mudarRecolhida(!recolhida)}
aria-label={recolhida ? textos.expandir : textos.recolher}
title={recolhida ? textos.expandir : textos.recolher}
aria-expanded={!recolhida}
aria-controls={idNav}
className="absolute top-6 -right-3.5 z-10 flex size-7 cursor-pointer items-center justify-center rounded-full border border-rule bg-page text-muted outline-accent hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-2"
>
<svg
aria-hidden="true"
viewBox="0 0 24 24"
className={`size-4 ${recolhida ? "rotate-180" : ""} ${semMovimento ? "" : "transition-transform duration-300"}`}
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m15 6-6 6 6 6" />
</svg>
</button>
{cabecalho !== undefined && <div className="mb-4 flex min-h-10 shrink-0 items-center overflow-hidden px-4">{resolver(cabecalho)}</div>}
<nav id={idNav} aria-label={rotulo} className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto px-4">
<ul className="flex flex-col gap-1">
{itens.map((item, indice) => {
const separador = item.separarAntes ? "mt-2 border-t border-rule pt-3" : "";
if (!item.filhos?.length) {
return (
<li key={item.rotulo} className={separador}>
{linhaSimples(item)}
</li>
);
}
const aberto = !recolhida && abertos.includes(chave(item));
const contemAtivo = item.filhos.some((f) => f.ativo);
const idFilhos = `${idNav}-grupo-${indice}`;
return (
<li key={item.rotulo} className={separador}>
<button
type="button"
onClick={() => alternarGrupo(item)}
aria-expanded={aberto}
aria-controls={aberto ? idFilhos : undefined}
title={recolhida ? item.rotulo : undefined}
aria-label={nomeRecolhido(item)}
className={`${LINHA} h-11 gap-3 px-3.5 text-sm ${contemAtivo && !aberto ? ATIVA : INATIVA}`}
>
{conteudoDaLinha(
item,
<svg
aria-hidden="true"
viewBox="0 0 24 24"
className={`size-4 shrink-0 ${transicao} ${someRecolhida} ${aberto ? "" : "-rotate-90"}`}
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m6 9 6 6 6-6" />
</svg>,
)}
</button>
{aberto && (
<ul id={idFilhos} className="mt-0.5 mb-1 flex flex-col gap-0.5">
{item.filhos.map((filho) => (
<li key={filho.rotulo}>{linhaSimples(filho, true)}</li>
))}
</ul>
)}
</li>
);
})}
</ul>
</nav>
{rodape !== undefined && (
<div className="mt-3 shrink-0 overflow-hidden border-t border-rule px-4 pt-3">{resolver(rodape)}</div>
)}
</div>
);
}
import { useState, type ReactNode } from "react";
import { defineStories } from "@/registry/stories";
import { BarraLateral, type ItemDaBarraLateral } from "./BarraLateral";
const icone = (d: string) => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round">
<path d={d} />
</svg>
);
const I = {
inicio: icone("M3 11 12 4l9 7M5 10v10h5v-6h4v6h5V10"),
tarefas: icone("M9 6h11M9 12h11M9 18h11M4 6l1 1 2-2M4 12l1 1 2-2M4 18l1 1 2-2"),
mensagens: icone("M4 5h16v11H8l-4 4z"),
relatorios: icone("M4 20V10M10 20V4M16 20v-7M22 20H2"),
clientes: icone("M16 19v-1a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v1M9 10a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM22 19v-1a4 4 0 0 0-3-3.9M16 4.1a3 3 0 0 1 0 5.8"),
config: icone("M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"),
};
const PAGINAS: Record<string, string> = {
"/inicio": "Início",
"/tarefas": "Tarefas",
"/mensagens": "Mensagens",
"/relatorios/vendas": "Relatório de vendas",
"/relatorios/equipes": "Relatório de equipes",
"/relatorios/estoque": "Relatório de estoque",
"/clientes": "Clientes",
"/configuracoes": "Configurações",
};
function itens(atual: string, comContadores: boolean): ItemDaBarraLateral[] {
const item = (rotulo: string, href: string, ic?: ReactNode, contador?: number | string): ItemDaBarraLateral => ({
rotulo,
href,
icone: ic,
ativo: atual === href,
contador: comContadores ? contador : undefined,
});
return [
item("Início", "/inicio", I.inicio),
item("Tarefas", "/tarefas", I.tarefas, 8),
item("Mensagens", "/mensagens", I.mensagens, 3),
{
rotulo: "Relatórios",
icone: I.relatorios,
filhos: [
item("Vendas", "/relatorios/vendas"),
item("Equipes", "/relatorios/equipes"),
item("Estoque", "/relatorios/estoque", undefined, "Novo"),
],
},
item("Clientes", "/clientes", I.clientes),
{ ...item("Configurações", "/configuracoes", I.config), separarAntes: true },
];
}
function Marca({ recolhida }: { recolhida: boolean }) {
return (
<span className="flex items-center gap-3">
<span aria-hidden="true" className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-accent font-display text-sm font-bold text-page">
A
</span>
<span className={`font-display font-bold whitespace-nowrap transition-opacity ${recolhida ? "opacity-0" : "opacity-100"}`}>Acme Painel</span>
</span>
);
}
function Conta({ recolhida }: { recolhida: boolean }) {
return (
<span className="flex items-center gap-3 py-1" title={recolhida ? "Ana Souza" : undefined}>
<span aria-hidden="true" className="flex size-8 shrink-0 items-center justify-center rounded-full bg-surface text-xs font-semibold text-ink-2">
AS
</span>
<span className={`min-w-0 transition-opacity ${recolhida ? "opacity-0" : "opacity-100"}`}>
<span className="block truncate text-sm font-medium whitespace-nowrap">Ana Souza</span>
<span className="block truncate text-xs whitespace-nowrap text-muted">ana@exemplo.com</span>
</span>
</span>
);
}
function Exemplo({
inicial = "/relatorios/vendas",
recolhida: recolhidaInicial,
comContadores,
comCabecalho,
comRodape,
umGrupoPorVez,
}: {
inicial?: string;
recolhida: boolean;
comContadores: boolean;
comCabecalho: boolean;
comRodape: boolean;
umGrupoPorVez: boolean;
}) {
const [atual, setAtual] = useState(inicial);
return (
<div className="flex h-[34rem] w-full overflow-hidden rounded-xl border border-rule bg-surface">
<BarraLateral
itens={itens(atual, comContadores)}
recolhidaInicial={recolhidaInicial}
umGrupoPorVez={umGrupoPorVez}
cabecalho={comCabecalho ? (r) => <Marca recolhida={r} /> : undefined}
rodape={comRodape ? (r) => <Conta recolhida={r} /> : undefined}
// Exemplo de link próprio: aqui ele só troca a página do exemplo, como faria um roteador.
renderizarLink={(p) => (
<a
{...p}
onClick={(e) => {
e.preventDefault();
setAtual(p.href);
}}
/>
)}
/>
<main className="flex min-w-0 flex-1 flex-col gap-4 p-8">
<p className="text-sm text-muted">Página atual</p>
<h2 className="font-display text-3xl font-bold">{PAGINAS[atual]}</h2>
<div className="grid gap-4 sm:grid-cols-3">
{[0, 1, 2].map((i) => (
<div key={i} className="h-24 rounded-xl border border-rule bg-page" />
))}
</div>
<div className="min-h-0 flex-1 rounded-xl border border-rule bg-page" />
</main>
</div>
);
}
export default defineStories({
args: { recolhida: false, comContadores: true, comCabecalho: true, comRodape: true, umGrupoPorVez: true },
controls: {
recolhida: { type: "boolean", label: "recolhidaInicial" },
comContadores: { type: "boolean", label: "contadores (exemplo)" },
comCabecalho: { type: "boolean", label: "cabecalho (exemplo)" },
comRodape: { type: "boolean", label: "rodape (exemplo)" },
umGrupoPorVez: { type: "boolean" },
},
render: (a) => <Exemplo key={String(a.recolhida)} {...a} />,
stories: [
{ name: "Aberta" },
{ name: "Recolhida", args: { recolhida: true } },
{ name: "Sem cabeçalho e rodapé", args: { comCabecalho: false, comRodape: false, comContadores: false } },
],
});
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.
Componentes da biblioteca (diretos e indiretos)
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);
}