Hero com Mídia
Abertura de página sobre vídeo, imagem ou fundo próprio. A mídia se revela por faixas que recuam para as bordas e o título entra palavra por palavra, tudo em CSS (sem piscar antes da hidratação). Véu e texto ficam legíveis nos modos claro e escuro.
Playground
Props
Geradas dos tipos TypeScript do componente; a descrição vem dos comentários /** */.
<HeroComMidia>
Abertura de página sobre vídeo, imagem ou um fundo qualquer. A mídia se revela por quatro faixas da cor da página recuando para as bordas, com um leve zoom de afastamento; o título entra palavra por palavra, subindo na diagonal, e depois o subtítulo e as ações. Tudo em CSS, para o primeiro quadro já sair certo antes da hidratação. Com "reduzir movimento" (do sistema ou de um ProvedorDeMovimento) aparece direto no estado final.
| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
| chapeu | string | — | Linha curta acima do título: categoria, contexto, chamada. |
| tituloobrigatória | string | TrechoDoTitulo[] | — | Texto do título, ou trechos `{ texto, enfase }` quando parte dele deve sair em itálico. O título entra palavra por palavra. |
| subtitulo | string | number | bigint | boolean | ReactElement<unknown, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | Promise<AwaitedReactNode> | — | |
| acoes | string | number | bigint | boolean | ReactElement<unknown, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | Promise<AwaitedReactNode> | — | Botões e links abaixo do subtítulo. Ficam no esquema de cores do hero (veja `esquema`). |
| midia | MidiaDoHero | — | Vídeo ou imagem de fundo, cobrindo a área toda. Tem prioridade sobre `fundo`. |
| fundo | string | number | bigint | boolean | ReactElement<unknown, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | Promise<AwaitedReactNode> | — | Fundo próprio (gradiente, ilustração, canvas...). Deve ocupar a área toda (`absolute inset-0` ou `size-full`). |
| veu | number | 0.7 | Densidade do véu sobre a mídia, de 0 (sem véu) a 1. Garante a leitura do texto. |
| esquema | "escuro" | "claro" | "escuro" | Esquema de cores do véu e do texto: "escuro" (véu escuro, texto claro, nos dois modos) ou "claro" (véu claro, texto escuro), para fundos claros. |
| altura | "tela" | "auto" | "tela" | "tela" ocupa a altura da janela; "auto" segue o conteúdo, com respiro. |
| alinhamento | "esquerda" | "centro" | "esquerda" | |
| corDaCortina | string | — | Cor das faixas da abertura (qualquer cor CSS). O padrão é a cor da página (`var(--page)`), para a mídia se abrir a partir dela; mude se o hero estiver sobre outro fundo. |
| inicioDoTexto | number | 500 | Quando (ms) o texto começa a entrar, contado da abertura da página. |
| intervaloEntrePalavras | number | 55 | Espera (ms) entre uma palavra do título e a seguinte. |
| duracaoDaAbertura | number | 1000 | Duração (ms) da abertura das faixas; o zoom da mídia dura 1,8× isso. |
| duracaoDoTexto | number | 850 | Duração (ms) da subida de cada peça de texto. |
| textos | Partial<TextosDoHero> | — | Rótulos do botão que pausa o vídeo de fundo. |
| className | string | "" | Classes extras no <section> (cantos arredondados, margem...). |
Código
stories.tsx tem os exemplos usados no preview e no playground.
/*
* Entrada do HeroComMidia, toda em CSS: acima da dobra o HTML pinta antes da
* hidratação, então uma animação em JS mostraria o estado final por um instante
* antes de saltar para o inicial. Aqui o primeiro quadro já sai certo.
*
* - A mídia se revela do centro para fora: quatro faixas da cor da página
* recuam cada uma para a sua borda. É `transform` (o compositor anima
* sozinho) e não `clip-path` animado, que roda na thread principal e engasga
* enquanto o vídeo decodifica.
* - A mídia assenta com um leve zoom de afastamento enquanto a janela abre.
* - O texto sobe com deriva diagonal: sobe `--sobe` e deriva `--sobe / -3`
* na horizontal. O título entra palavra por palavra (atraso em cada palavra).
*
* As durações chegam por variáveis no <section>: --hero-abertura (faixas) e
* --hero-texto (cada peça de texto).
*/
/* Repouso das faixas: recolhidas. Fica fora da media query porque quem pede
menos movimento não recebe animação nenhuma e, sem isso, as faixas ficariam
cobrindo a mídia para sempre. */
.faixa {
transform: scale(0);
}
@media (prefers-reduced-motion: no-preference) {
.faixaVertical {
animation: recolheVertical var(--hero-abertura, 1000ms) cubic-bezier(0.65, 0, 0.35, 1) both;
}
.faixaHorizontal {
animation: recolheHorizontal var(--hero-abertura, 1000ms) cubic-bezier(0.65, 0, 0.35, 1) both;
}
.midia {
animation: assenta calc(var(--hero-abertura, 1000ms) * 1.8) cubic-bezier(0.16, 1, 0.3, 1) both;
}
.sobe {
animation: sobe var(--hero-texto, 850ms) cubic-bezier(0.16, 1, 0.3, 1) both;
}
}
/* useSemMovimento() (ProvedorDeMovimento, miniaturas do catálogo): estado final direto. */
.parado .faixaVertical,
.parado .faixaHorizontal,
.parado .midia,
.parado .sobe {
animation: none;
}
@keyframes recolheVertical {
from {
transform: scaleY(1);
}
to {
transform: scaleY(0);
}
}
@keyframes recolheHorizontal {
from {
transform: scaleX(1);
}
to {
transform: scaleX(0);
}
}
@keyframes assenta {
from {
transform: scale(1.08);
}
to {
transform: scale(1);
}
}
@keyframes sobe {
from {
opacity: 0;
transform: translate(calc(var(--sobe, 20px) / -3), var(--sobe, 20px));
}
to {
opacity: 1;
transform: translate(0, 0);
}
}
"use client";
import { Fragment, useEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";
import { useSemMovimento } from "@/library/revelar/Movimento";
import estilos from "./HeroComMidia.module.css";
/** Trecho do título. Com `enfase`, o trecho sai em itálico (o realce da frase). */
export type TrechoDoTitulo = { texto: string; enfase?: boolean };
/** Mídia de fundo do hero. */
export type MidiaDoHero =
| {
tipo: "video";
/** Endereço do vídeo: toca mudo, em loop e sem controles nativos. */
src: string;
/** Imagem mostrada enquanto o vídeo carrega. */
poster?: string;
}
| {
tipo: "imagem";
src: string;
/** Texto alternativo. Vazio (padrão) quando a imagem é só decorativa. */
alt?: string;
};
export type TextosDoHero = { pausarVideo: string; reproduzirVideo: string };
const TEXTOS_PADRAO: TextosDoHero = {
pausarVideo: "Pausar vídeo de fundo",
reproduzirVideo: "Reproduzir vídeo de fundo",
};
// O véu acompanha o texto: mais denso do lado dele. No celular, onde o texto
// ocupa a largura toda, é uniforme.
const VEU = {
esquerda: "bg-page/75 sm:bg-transparent sm:bg-linear-to-r sm:from-page sm:via-page/65 sm:to-page/30",
centro: "bg-page/75 sm:bg-transparent sm:bg-radial sm:from-page sm:via-page/80 sm:to-page/50",
} as const;
/**
* Esquema de cores fixo sobre a mídia. Os tokens do tema são `light-dark()`,
* então `color-scheme: dark` faz `bg-page` e `text-ink` valerem os valores do
* modo escuro aqui dentro, nos dois modos: véu escuro e texto claro sem cor
* fixa, ainda com as cores do tema. "claro" faz o inverso, para fundos claros.
*/
const ESQUEMA = { escuro: "scheme-dark", claro: "scheme-light" } as const;
/**
* Abertura de página sobre vídeo, imagem ou um fundo qualquer. A mídia se
* revela por quatro faixas da cor da página recuando para as bordas, com um
* leve zoom de afastamento; o título entra palavra por palavra, subindo na
* diagonal, e depois o subtítulo e as ações. Tudo em CSS, para o primeiro
* quadro já sair certo antes da hidratação. Com "reduzir movimento" (do sistema
* ou de um ProvedorDeMovimento) aparece direto no estado final.
*/
export function HeroComMidia({
chapeu,
titulo,
subtitulo,
acoes,
midia,
fundo,
veu = 0.7,
esquema = "escuro",
altura = "tela",
alinhamento = "esquerda",
corDaCortina,
inicioDoTexto = 500,
intervaloEntrePalavras = 55,
duracaoDaAbertura = 1000,
duracaoDoTexto = 850,
textos: textosParciais,
className = "",
}: {
/** Linha curta acima do título: categoria, contexto, chamada. */
chapeu?: string;
/**
* Texto do título, ou trechos `{ texto, enfase }` quando parte dele deve sair
* em itálico. O título entra palavra por palavra.
*/
titulo: string | TrechoDoTitulo[];
subtitulo?: ReactNode;
/** Botões e links abaixo do subtítulo. Ficam no esquema de cores do hero (veja `esquema`). */
acoes?: ReactNode;
/** Vídeo ou imagem de fundo, cobrindo a área toda. Tem prioridade sobre `fundo`. */
midia?: MidiaDoHero;
/** Fundo próprio (gradiente, ilustração, canvas...). Deve ocupar a área toda (`absolute inset-0` ou `size-full`). */
fundo?: ReactNode;
/** Densidade do véu sobre a mídia, de 0 (sem véu) a 1. Garante a leitura do texto. */
veu?: number;
/**
* Esquema de cores do véu e do texto: "escuro" (véu escuro, texto claro, nos
* dois modos) ou "claro" (véu claro, texto escuro), para fundos claros.
*/
esquema?: keyof typeof ESQUEMA;
/** "tela" ocupa a altura da janela; "auto" segue o conteúdo, com respiro. */
altura?: "tela" | "auto";
alinhamento?: "esquerda" | "centro";
/**
* Cor das faixas da abertura (qualquer cor CSS). O padrão é a cor da página
* (`var(--page)`), para a mídia se abrir a partir dela; mude se o hero estiver
* sobre outro fundo.
*/
corDaCortina?: string;
/** Quando (ms) o texto começa a entrar, contado da abertura da página. */
inicioDoTexto?: number;
/** Espera (ms) entre uma palavra do título e a seguinte. */
intervaloEntrePalavras?: number;
/** Duração (ms) da abertura das faixas; o zoom da mídia dura 1,8× isso. */
duracaoDaAbertura?: number;
/** Duração (ms) da subida de cada peça de texto. */
duracaoDoTexto?: number;
/** Rótulos do botão que pausa o vídeo de fundo. */
textos?: Partial<TextosDoHero>;
/** Classes extras no <section> (cantos arredondados, margem...). */
className?: string;
}) {
const textos = { ...TEXTOS_PADRAO, ...textosParciais };
const semMovimento = useSemMovimento();
const video = useRef<HTMLVideoElement>(null);
// Escolha de quem clicou no botão; sem ela, o vídeo fica parado só com "reduzir movimento".
const [escolha, setEscolha] = useState<"tocar" | "pausar" | null>(null);
const videoParado = escolha ? escolha === "pausar" : semMovimento;
useEffect(() => {
const el = video.current;
if (!el) return;
if (videoParado) el.pause();
// O navegador pode bloquear o play automático: o vídeo só fica no pôster.
else el.play().catch(() => {});
}, [videoParado, midia?.src]);
const trechos = typeof titulo === "string" ? [{ texto: titulo }] : titulo;
const palavras = trechos.flatMap(({ texto, enfase }) =>
texto
.split(/\s+/)
.filter(Boolean)
.map((palavra) => ({ palavra, enfase: Boolean(enfase) })),
);
// Compasso da entrada: chapéu, palavras do título, subtítulo, ações.
const inicioDoTitulo = inicioDoTexto + (chapeu ? 80 : 0);
const fimDoTitulo = inicioDoTitulo + palavras.length * intervaloEntrePalavras;
const subir = (distancia: number, atraso: number) =>
({ "--sobe": `${distancia}px`, animationDelay: `${atraso}ms` }) as CSSProperties;
const centro = alinhamento === "centro";
const cortina = { background: corDaCortina ?? "var(--page)" };
return (
<section
className={`relative isolate flex w-full flex-col overflow-hidden ${altura === "tela" ? "min-h-svh" : ""} ${
semMovimento ? estilos.parado : ""
} ${className}`}
style={{ "--hero-abertura": `${duracaoDaAbertura}ms`, "--hero-texto": `${duracaoDoTexto}ms` } as CSSProperties}
>
{/* Fundo: mídia e véu, no esquema fixo (o bg-page é o que aparece enquanto a mídia carrega). */}
<div className={`absolute inset-0 bg-page ${ESQUEMA[esquema]}`}>
<div className={`absolute inset-0 ${estilos.midia}`}>
{midia?.tipo === "video" ? (
<video
ref={video}
className="absolute inset-0 size-full object-cover"
src={midia.src}
poster={midia.poster}
autoPlay
muted
loop
playsInline
preload="auto"
aria-hidden="true"
/>
) : midia?.tipo === "imagem" ? (
// eslint-disable-next-line @next/next/no-img-element -- a biblioteca não depende de next/image
<img
className="absolute inset-0 size-full object-cover"
src={midia.src}
alt={midia.alt ?? ""}
fetchPriority="high"
/>
) : (
fundo
)}
</div>
{veu > 0 && (
<div
aria-hidden="true"
className={`absolute inset-0 ${VEU[alinhamento]}`}
style={{ opacity: Math.min(1, veu) }}
/>
)}
</div>
{/* Abertura: as quatro faixas partem cobrindo tudo menos uma janela central
e recuam cada uma para a sua borda. Ficam fora do esquema fixo para
terem a cor da página de verdade. */}
<div aria-hidden="true" className="pointer-events-none absolute inset-0">
<div
className={`absolute inset-x-0 top-0 h-[27%] origin-top ${estilos.faixa} ${estilos.faixaVertical}`}
style={cortina}
/>
<div
className={`absolute inset-x-0 bottom-0 h-[27%] origin-bottom ${estilos.faixa} ${estilos.faixaVertical}`}
style={cortina}
/>
<div
className={`absolute inset-y-0 left-0 w-[21%] origin-left ${estilos.faixa} ${estilos.faixaHorizontal}`}
style={cortina}
/>
<div
className={`absolute inset-y-0 right-0 w-[21%] origin-right ${estilos.faixa} ${estilos.faixaHorizontal}`}
style={cortina}
/>
</div>
<div
className={`relative z-10 flex w-full flex-1 flex-col text-ink ${ESQUEMA[esquema]} ${
altura === "tela" ? "pt-32 pb-24" : "py-24 sm:py-32"
}`}
>
<div
className={`mx-auto flex w-full max-w-[100rem] flex-1 flex-col justify-center px-6 sm:px-10 lg:px-16 ${
centro ? "items-center text-center" : "items-start"
}`}
>
<div className="max-w-3xl">
{chapeu && (
<p
className={`${estilos.sobe} mb-5 text-xs font-semibold tracking-[0.14em] text-ink/80 uppercase`}
style={subir(20, inicioDoTexto)}
>
{chapeu}
</p>
)}
<h1 className="font-display text-5xl leading-[1.05] font-bold tracking-[-0.01em] sm:text-6xl lg:text-7xl">
{palavras.map(({ palavra, enfase }, i) => (
<Fragment key={`${i}-${palavra}`}>
<span
className={`${estilos.sobe} inline-block ${enfase ? "italic" : ""}`}
style={subir(40, inicioDoTitulo + i * intervaloEntrePalavras)}
>
{palavra}
</span>{" "}
</Fragment>
))}
</h1>
{subtitulo && (
<p
className={`${estilos.sobe} mt-6 max-w-2xl text-lg leading-relaxed text-ink/85 sm:text-xl ${
centro ? "mx-auto" : ""
}`}
style={subir(20, fimDoTitulo)}
>
{subtitulo}
</p>
)}
{acoes && (
<div
className={`${estilos.sobe} mt-10 flex flex-wrap gap-3 ${centro ? "justify-center" : ""}`}
style={subir(20, fimDoTitulo + 100)}
>
{acoes}
</div>
)}
</div>
</div>
{/* Vídeo que toca sozinho por mais de 5 s precisa de um jeito de parar (WCAG 2.2.2). */}
{midia?.tipo === "video" && (
<button
type="button"
onClick={() => setEscolha(videoParado ? "tocar" : "pausar")}
aria-label={videoParado ? textos.reproduzirVideo : textos.pausarVideo}
className={`${estilos.sobe} absolute right-4 bottom-4 grid size-10 cursor-pointer place-items-center rounded-full bg-page/60 text-ink outline-accent backdrop-blur-sm transition-colors hover:bg-page/80 focus-visible:outline-2 focus-visible:outline-offset-2 sm:right-6 sm:bottom-6`}
style={subir(0, fimDoTitulo + 200)}
>
<svg aria-hidden="true" viewBox="0 0 24 24" className="size-4" fill="currentColor">
{videoParado ? <path d="M8 5.5v13l10.5-6.5L8 5.5Z" /> : <path d="M7 5h3.5v14H7zM13.5 5H17v14h-3.5z" />}
</svg>
</button>
)}
</div>
</section>
);
}
import type { CSSProperties } from "react";
import { defineStories } from "@/registry/stories";
import { HeroComMidia } from "./HeroComMidia";
// Fundos abstratos feitos só com as cores do tema (sem imagens nem vídeos externos).
const FUNDOS: Record<string, CSSProperties> = {
aurora: {
background: [
"radial-gradient(55% 75% at 82% 18%, var(--series-1), transparent 70%)",
"radial-gradient(45% 65% at 28% 92%, var(--series-3), transparent 70%)",
"radial-gradient(40% 55% at 62% 62%, var(--series-7), transparent 72%)",
"linear-gradient(135deg, var(--accent), var(--series-1))",
].join(", "),
},
entardecer: {
background: [
"radial-gradient(35% 45% at 70% 72%, var(--series-4), transparent 75%)",
"linear-gradient(170deg, var(--series-7) 0%, var(--series-5) 45%, var(--series-2) 100%)",
].join(", "),
},
conico: {
background:
"conic-gradient(from 210deg at 68% 42%, var(--series-1), var(--series-7), var(--series-5), var(--series-2), var(--series-4), var(--series-1))",
},
};
function FundoAbstrato({ nome }: { nome: string }) {
return <div className="absolute inset-0" style={FUNDOS[nome] ?? FUNDOS.aurora} />;
}
function Acoes() {
return (
<>
<a
href="#comecar"
className="inline-flex h-12 items-center gap-3 rounded-lg bg-accent px-6 text-base font-semibold text-page outline-accent transition-transform hover:scale-[1.02] focus-visible:outline-2 focus-visible:outline-offset-2"
>
Começar agora
<svg
aria-hidden="true"
viewBox="0 0 24 24"
className="size-4"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M5 12h14" />
<path d="m12 5 7 7-7 7" />
</svg>
</a>
<a
href="#como-funciona"
className="inline-flex h-12 items-center rounded-lg border border-ink/40 px-6 text-base font-semibold text-ink outline-accent transition-colors hover:bg-ink/10 focus-visible:outline-2 focus-visible:outline-offset-2"
>
Ver como funciona
</a>
</>
);
}
export default defineStories({
args: {
chapeu: "Gestão de vendas",
titulo: "Tudo o que sua equipe precisa",
enfase: "para vender mais todo mês",
subtitulo: "Metas, funil e relatórios no mesmo lugar, atualizados enquanto as vendas acontecem.",
comAcoes: true,
fundo: "aurora",
veu: 0.7,
esquema: "escuro",
alinhamento: "esquerda",
intervaloEntrePalavras: 55,
},
controls: {
chapeu: { type: "text", label: "chapeu (vazio = sem)" },
titulo: { type: "text", label: "titulo (trecho normal)" },
enfase: { type: "text", label: "trecho em itálico (vazio = sem)" },
subtitulo: { type: "text", label: "subtitulo (vazio = sem)" },
comAcoes: { type: "boolean", label: "acoes (botões de exemplo)" },
fundo: { type: "select", label: "fundo (exemplos abstratos)", options: ["aurora", "entardecer", "conico"] },
veu: { type: "number", min: 0, max: 1, step: 0.05 },
esquema: { type: "select", options: ["escuro", "claro"] },
alinhamento: { type: "select", options: ["esquerda", "centro"] },
intervaloEntrePalavras: { type: "number", label: "intervaloEntrePalavras (ms)", min: 0, max: 200, step: 5 },
},
// altura="auto" nos exemplos: "tela" usa a altura da janela, e o preview cresce com o conteúdo.
render: (a) => (
<HeroComMidia
altura="auto"
className="rounded-2xl"
chapeu={a.chapeu || undefined}
titulo={a.enfase ? [{ texto: a.titulo }, { texto: a.enfase, enfase: true }] : a.titulo}
subtitulo={a.subtitulo || undefined}
acoes={a.comAcoes ? <Acoes /> : undefined}
fundo={<FundoAbstrato nome={a.fundo} />}
veu={a.veu}
esquema={a.esquema as "escuro" | "claro"}
alinhamento={a.alinhamento as "esquerda" | "centro"}
intervaloEntrePalavras={a.intervaloEntrePalavras}
/>
),
stories: [
{ name: "Sobre fundo abstrato" },
{
name: "Centralizado",
args: {
alinhamento: "centro",
fundo: "entardecer",
chapeu: "Novo",
titulo: "Relatórios que se",
enfase: "explicam sozinhos",
subtitulo: "Cada gráfico chega com um resumo do que mudou e por quê.",
},
},
{ name: "Véu leve", args: { fundo: "conico", veu: 0.35 } },
{
name: "Esquema claro",
args: { esquema: "claro", fundo: "entardecer", veu: 0.75 },
},
{
name: "Só o título",
args: {
chapeu: "",
enfase: "",
subtitulo: "",
comAcoes: false,
titulo: "Vendas, metas e equipes em um painel só.",
},
},
],
});
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);
}