Diálogo
Diálogo modal sobre <dialog> nativo, com título e ações sempre fixos e só o meio rolando. Status com ícone, quatro tamanhos, posição (folha no celular), três fundos, modo alerta que exige escolha explícita e arredondamento concêntrico.
Playground
Props
Geradas dos tipos TypeScript do componente; a descrição vem dos comentários /** */.
<BotaoDoDialogo>
O `Botao` da biblioteca com `fecha`, que fecha o diálogo depois do clique, e largura total no celular. O raio concêntrico vem do painel, então um `Botao` comum dentro dele também acerta.
Também aceita os 287 atributos nativos do elemento HTML (onClick, aria-*, disabled...), repassados ao elemento.
| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
| variante | "perigo" | "primario" | "secundario" | "terciario" | "contorno" | "fantasma" | "perigo-suave" | "link" | "discreto" | "discreto-perigo" | — | |
| tamanho | "sm" | "md" | "xs" | "lg" | — | |
| larguraTotal | boolean | — | |
| soIcone | boolean | — | |
| arredondado | boolean | — | Cantos totalmente redondos (formato de pílula). |
| ref | ((instance: HTMLButtonElement | null) => void | (() => VoidOrUndefinedOnly) | DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES[keyof DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES]) | RefObject<HTMLButtonElement | null> | — | Ref do elemento (útil para gatilhos de menus e popovers). |
| href | string | — | Com `href`, vira um link (`<a>`) com o visual de botão. |
| novaAba | boolean | — | Abre o link em nova aba (com rel="noopener noreferrer"). Só vale com `href`. |
| type | "button" | "submit" | "reset" | — | Tipo do botão. Padrão "button", para não enviar formulários sem querer. |
| icone | string | number | bigint | boolean | ReactElement<unknown, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | Promise<AwaitedReactNode> | — | Ícone antes do texto. |
| iconeFinal | string | number | bigint | boolean | ReactElement<unknown, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | Promise<AwaitedReactNode> | — | Ícone depois do texto. |
| carregando | boolean | — | Mostra um indicador de carga no lugar do ícone e ignora cliques, mas mantém o foco. |
| textoCarregando | string | number | bigint | boolean | ReactElement<unknown, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | Promise<AwaitedReactNode> | — | Texto no lugar de `children` enquanto carrega ("Enviando..."). |
| fecha | boolean | false | Fecha o diálogo depois do clique (cancelar, confirmar sem espera). |
<Dialogo>
Diálogo modal sobre `<dialog>` nativo: prende o foco, deixa o resto da página inerte e devolve o foco ao gatilho. Título e ações ficam fixos; só o meio rola, com uma linha que aparece quando há conteúdo escondido acima ou abaixo. Com `alerta`, vira confirmação obrigatória.
| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
| tituloobrigatória | ReactNode | — | Título, sempre visível no topo. |
| descricao | string | number | bigint | boolean | ReactElement<unknown, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | Promise<AwaitedReactNode> | — | Linha de apoio abaixo do título, também fixa. |
| children | string | number | bigint | boolean | ReactElement<unknown, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | Promise<AwaitedReactNode> | — | Conteúdo do meio: é a única parte que rola quando o diálogo fica comprido. |
| acoes | string | number | bigint | boolean | ReactElement<unknown, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | Promise<AwaitedReactNode> | — | Botões do rodapé, sempre visíveis (use `BotaoDoDialogo`). Ficam em linha à direita no desktop e empilhados no celular, com o último (a ação principal) em cima. |
| status | "info" | "sucesso" | "alerta" | "perigo" | — | Mostra um ícone de status ao lado do título. |
| icone | string | number | bigint | boolean | ReactElement<unknown, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | Promise<AwaitedReactNode> | — | Ícone próprio no lugar do ícone de status (mantém a cor do status). |
| gatilho | string | number | bigint | boolean | ReactElement<unknown, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | Promise<AwaitedReactNode> | ((abrir: () => void) => ReactNode) | — | Elemento que abre o diálogo (um botão). Também aceita uma função que recebe `abrir`. |
| aberto | boolean | — | Estado controlado. Use junto com `aoMudar`. |
| abertoNoInicio | boolean | false | Estado inicial quando não é controlado. |
| aoMudar | (aberto: boolean) => void | — | |
| tamanho | "sm" | "md" | "lg" | "xl" | "md" | |
| posicao | "topo" | "auto" | "centro" | "base" | "auto" | |
| fundo | "escuro" | "desfoque" | "transparente" | "escuro" | |
| alerta | boolean | false | Confirmação que exige uma escolha explícita (`role="alertdialog"`): por padrão não fecha clicando fora nem com Esc, e o foco começa no último botão do rodapé. |
| fecharAoClicarFora | boolean | !alerta | Fecha ao clicar no fundo. Padrão: verdadeiro, ou falso com `alerta`. |
| fecharComEsc | boolean | !alerta | Fecha com Esc. Padrão: verdadeiro, ou falso com `alerta`. |
| botaoFechar | boolean | !alerta | Mostra o X no canto. |
| rotuloFechar | string | "Fechar" | Rótulo do X para leitores de tela. |
| incorporado | boolean | false | Mostra o painel no lugar, sem fundo nem sobreposição (para documentação e miniaturas). |
Código
stories.tsx tem os exemplos usados no preview e no playground.
"use client";
import {
createContext,
useCallback,
useContext,
useEffect,
useId,
useLayoutEffect,
useRef,
useState,
type ReactNode,
} from "react";
import { Botao, type BotaoProps } from "@/library/botao/Botao";
import { CURVA_SUAVE, useSemMovimento } from "@/library/revelar/Movimento";
const STATUS = {
info: { cor: "bg-accent/10 text-accent", icone: "info" },
sucesso: { cor: "bg-success/10 text-success", icone: "ok" },
alerta: { cor: "bg-warning/10 text-warning", icone: "triangulo" },
perigo: { cor: "bg-critical/10 text-critical", icone: "triangulo" },
} as const;
const TAMANHOS = {
sm: "sm:max-w-sm",
md: "sm:max-w-md",
lg: "sm:max-w-xl",
xl: "sm:max-w-3xl",
} as const;
const FUNDOS = {
escuro: "bg-ink/50",
desfoque: "bg-ink/25 backdrop-blur-sm",
transparente: "bg-transparent",
} as const;
// "auto": encosta embaixo no celular (como uma folha) e centraliza a partir do sm
const POSICOES = {
auto: "items-end sm:items-center",
centro: "items-center",
topo: "items-start",
base: "items-end",
} as const;
export type StatusDoDialogo = keyof typeof STATUS;
export type TamanhoDoDialogo = keyof typeof TAMANHOS;
export type FundoDoDialogo = keyof typeof FUNDOS;
export type PosicaoDoDialogo = keyof typeof POSICOES;
/**
* Arredondamento concêntrico, de dentro para fora: o raio dos botões é o do tema (`--radius-lg`),
* limitado à metade da altura deles (h-10), e o painel é esse raio mais a folga de 3 unidades até a
* borda (só quando o tema tem arredondamento: com raio 0, o painel fica reto). Calculado ao contrário (painel fixo menos a folga), um tema bem arredondado pedia um raio
* maior que o botão comporta, e o canto dele escapava da curva do painel.
*/
const RAIOS =
"[--raio-do-botao:min(var(--radius-lg),var(--spacing)*5)] rounded-[calc(min(var(--radius-lg),var(--spacing)*5)_+_min(var(--radius-lg)*100,var(--spacing)*3))]";
const DURACAO = 220;
type Contexto = { fechar: () => void };
const ContextoDoDialogo = createContext<Contexto>({ fechar: () => {} });
/** Fecha o diálogo de dentro dele (útil depois de uma ação assíncrona). */
export function useDialogo() {
return useContext(ContextoDoDialogo);
}
export type BotaoDoDialogoProps = BotaoProps & {
/** Fecha o diálogo depois do clique (cancelar, confirmar sem espera). */
fecha?: boolean;
};
/**
* O `Botao` da biblioteca com `fecha`, que fecha o diálogo depois do clique, e largura total no
* celular. O raio concêntrico vem do painel, então um `Botao` comum dentro dele também acerta.
*/
export function BotaoDoDialogo({ fecha = false, onClick, className = "", ...resto }: BotaoDoDialogoProps) {
const { fechar } = useDialogo();
return (
<Botao
{...resto}
className={`w-full sm:w-auto ${className}`}
onClick={(e) => {
onClick?.(e);
if (fecha && !e.defaultPrevented) fechar();
}}
/>
);
}
function IconeDeStatus({ status }: { status: StatusDoDialogo }) {
const icone = STATUS[status].icone;
return (
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
{icone === "triangulo" && (
<>
<path d="M12 3 2.5 20h19L12 3z" />
<path d="M12 10v4" />
<path d="M12 17.5h.01" />
</>
)}
{icone === "info" && (
<>
<circle cx="12" cy="12" r="9" />
<path d="M12 11v5" />
<path d="M12 7.5h.01" />
</>
)}
{icone === "ok" && (
<>
<circle cx="12" cy="12" r="9" />
<path d="m8 12.5 2.5 2.5L16 9.5" />
</>
)}
</svg>
);
}
export type DialogoProps = {
/** Título, sempre visível no topo. */
titulo: ReactNode;
/** Linha de apoio abaixo do título, também fixa. */
descricao?: ReactNode;
/** Conteúdo do meio: é a única parte que rola quando o diálogo fica comprido. */
children?: ReactNode;
/** Botões do rodapé, sempre visíveis (use `BotaoDoDialogo`). Ficam em linha à direita no desktop e empilhados no celular, com o último (a ação principal) em cima. */
acoes?: ReactNode;
/** Mostra um ícone de status ao lado do título. */
status?: StatusDoDialogo;
/** Ícone próprio no lugar do ícone de status (mantém a cor do status). */
icone?: ReactNode;
/** Elemento que abre o diálogo (um botão). Também aceita uma função que recebe `abrir`. */
gatilho?: ReactNode | ((abrir: () => void) => ReactNode);
/** Estado controlado. Use junto com `aoMudar`. */
aberto?: boolean;
/** Estado inicial quando não é controlado. */
abertoNoInicio?: boolean;
aoMudar?: (aberto: boolean) => void;
tamanho?: TamanhoDoDialogo;
posicao?: PosicaoDoDialogo;
fundo?: FundoDoDialogo;
/**
* Confirmação que exige uma escolha explícita (`role="alertdialog"`): por padrão não fecha
* clicando fora nem com Esc, e o foco começa no último botão do rodapé.
*/
alerta?: boolean;
/** Fecha ao clicar no fundo. Padrão: verdadeiro, ou falso com `alerta`. */
fecharAoClicarFora?: boolean;
/** Fecha com Esc. Padrão: verdadeiro, ou falso com `alerta`. */
fecharComEsc?: boolean;
/** Mostra o X no canto. */
botaoFechar?: boolean;
/** Rótulo do X para leitores de tela. */
rotuloFechar?: string;
/** Mostra o painel no lugar, sem fundo nem sobreposição (para documentação e miniaturas). */
incorporado?: boolean;
};
/**
* Diálogo modal sobre `<dialog>` nativo: prende o foco, deixa o resto da página inerte e devolve
* o foco ao gatilho. Título e ações ficam fixos; só o meio rola, com uma linha que aparece
* quando há conteúdo escondido acima ou abaixo. Com `alerta`, vira confirmação obrigatória.
*/
export function Dialogo({
titulo,
descricao,
children,
acoes,
status,
icone,
gatilho,
aberto: abertoControlado,
abertoNoInicio = false,
aoMudar,
tamanho = "md",
posicao = "auto",
fundo = "escuro",
alerta = false,
fecharAoClicarFora = !alerta,
fecharComEsc = !alerta,
botaoFechar = !alerta,
rotuloFechar = "Fechar",
incorporado = false,
}: DialogoProps) {
const semMovimento = useSemMovimento();
const [abertoInterno, setAbertoInterno] = useState(abertoNoInicio);
const aberto = abertoControlado ?? abertoInterno;
const dialogo = useRef<HTMLDialogElement>(null);
const idTitulo = useId();
const idDescricao = useId();
const mudar = useCallback(
(valor: boolean) => {
if (abertoControlado === undefined) setAbertoInterno(valor);
aoMudar?.(valor);
},
[abertoControlado, aoMudar],
);
const abrir = useCallback(() => mudar(true), [mudar]);
const fechar = useCallback(() => mudar(false), [mudar]);
useLayoutEffect(() => {
const el = dialogo.current;
if (incorporado || !el) return;
if (aberto && !el.open) {
el.showModal();
if (alerta) el.querySelector<HTMLElement>("[data-acoes] button:last-of-type")?.focus();
// data-visivel controla a animação; o <dialog> só fecha de verdade quando ela termina
if (semMovimento) el.dataset.visivel = "true";
else requestAnimationFrame(() => requestAnimationFrame(() => (el.dataset.visivel = "true")));
} else if (!aberto && el.open) {
el.dataset.visivel = "false";
const t = setTimeout(() => el.close(), semMovimento ? 0 : DURACAO);
return () => clearTimeout(t);
}
}, [aberto, alerta, incorporado, semMovimento]);
// Trava a rolagem da página enquanto está aberto
useEffect(() => {
if (incorporado || !aberto) return;
const raiz = document.documentElement;
const antes = raiz.style.overflow;
raiz.style.overflow = "hidden";
return () => {
raiz.style.overflow = antes;
};
}, [aberto, incorporado]);
const painel = (
<Painel
titulo={titulo}
descricao={descricao}
acoes={acoes}
status={status}
icone={icone}
tamanho={tamanho}
botaoFechar={botaoFechar}
rotuloFechar={rotuloFechar}
idTitulo={idTitulo}
idDescricao={idDescricao}
incorporado={incorporado}
role={alerta ? "alertdialog" : undefined}
>
{children}
</Painel>
);
const disparador =
typeof gatilho === "function" ? (
gatilho(abrir)
) : gatilho ? (
// O clique (ou Enter/Espaço) no botão de dentro sobe até aqui
<span className="contents" onClick={abrir}>
{gatilho}
</span>
) : null;
if (incorporado) {
return (
<ContextoDoDialogo.Provider value={{ fechar: () => {} }}>
<div className="flex w-full justify-center">{painel}</div>
</ContextoDoDialogo.Provider>
);
}
return (
<ContextoDoDialogo.Provider value={{ fechar }}>
{disparador}
<dialog
ref={dialogo}
role={alerta ? "alertdialog" : undefined}
aria-labelledby={idTitulo}
aria-describedby={descricao ? idDescricao : undefined}
aria-modal="true"
onCancel={(e) => {
// Esc: o próprio componente decide se fecha, para animar a saída
e.preventDefault();
if (fecharComEsc) fechar();
}}
className={`fixed inset-0 m-0 h-dvh max-h-none w-full max-w-none overflow-hidden bg-transparent p-0 text-ink outline-none backdrop:bg-transparent open:flex group ${POSICOES[posicao]} justify-center`}
>
<div
aria-hidden="true"
onClick={fecharAoClicarFora ? fechar : undefined}
className={`absolute inset-0 transition-opacity ${FUNDOS[fundo]} opacity-0 group-data-[visivel=true]:opacity-100`}
style={{ transitionDuration: `${DURACAO}ms` }}
/>
<div
className={`relative flex max-h-full w-full justify-center p-3 transition-[opacity,translate,scale] sm:p-6 translate-y-4 scale-[0.97] opacity-0 group-data-[visivel=true]:translate-y-0 group-data-[visivel=true]:scale-100 group-data-[visivel=true]:opacity-100`}
style={{ transitionDuration: `${DURACAO}ms`, transitionTimingFunction: CURVA_SUAVE }}
>
{painel}
</div>
</dialog>
</ContextoDoDialogo.Provider>
);
}
function Painel({
titulo,
descricao,
acoes,
status,
icone,
tamanho,
botaoFechar,
rotuloFechar,
idTitulo,
idDescricao,
incorporado,
role,
children,
}: Pick<DialogoProps, "titulo" | "descricao" | "acoes" | "status" | "icone" | "botaoFechar" | "rotuloFechar" | "children"> & {
tamanho: TamanhoDoDialogo;
idTitulo: string;
idDescricao: string;
incorporado: boolean;
role?: string;
}) {
const { fechar } = useDialogo();
const corpo = useRef<HTMLDivElement>(null);
// Linhas de divisão só quando há conteúdo escondido daquele lado
const [sombra, setSombra] = useState({ topo: false, base: false });
const medir = useCallback(() => {
const el = corpo.current;
if (!el) return;
const topo = el.scrollTop > 1;
const base = el.scrollTop + el.clientHeight < el.scrollHeight - 1;
setSombra((s) => (s.topo === topo && s.base === base ? s : { topo, base }));
}, []);
useEffect(() => {
const el = corpo.current;
if (!el) return;
medir();
const observador = new ResizeObserver(medir);
observador.observe(el);
if (el.firstElementChild) observador.observe(el.firstElementChild);
return () => observador.disconnect();
}, [medir]);
const temIcone = Boolean(status || icone);
return (
<section
role={incorporado ? role : undefined}
aria-labelledby={incorporado ? idTitulo : undefined}
className={`relative flex max-h-full w-full flex-col overflow-hidden border border-rule bg-page text-ink shadow-flutuante ${RAIOS} ${TAMANHOS[tamanho]}`}
>
<header
className={`flex shrink-0 items-start gap-3 border-b px-5 pt-5 pb-4 transition-colors sm:px-6 sm:pt-6 ${
sombra.topo ? "border-rule" : "border-transparent"
} ${botaoFechar ? "pr-14 sm:pr-14" : ""}`}
>
{temIcone && (
<span
className={`grid size-10 shrink-0 place-items-center rounded-[max(var(--radius-sm),calc(var(--radius-lg)_-_var(--spacing)*2))] [&_svg]:size-5 ${
STATUS[status ?? "info"].cor
}`}
>
{icone ?? (status && <IconeDeStatus status={status} />)}
</span>
)}
<div className={`min-w-0 flex-1 ${temIcone ? "pt-1.5" : ""}`}>
<h2 id={idTitulo} className="font-display text-lg leading-snug font-semibold text-balance">
{titulo}
</h2>
{descricao && (
<p id={idDescricao} className="mt-1 text-sm text-pretty text-ink-2">
{descricao}
</p>
)}
</div>
</header>
{botaoFechar && (
<Botao variante="fantasma" tamanho="sm" soIcone aria-label={rotuloFechar} onClick={fechar} className="absolute top-3 right-3">
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round">
<path d="M6 6l12 12M18 6 6 18" />
</svg>
</Botao>
)}
{children !== undefined && (
<div
ref={corpo}
onScroll={medir}
// Focável para rolar pelo teclado quando o conteúdo passa da altura
tabIndex={sombra.topo || sombra.base ? 0 : undefined}
className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-5 text-sm leading-relaxed text-ink-2 outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-inset sm:px-6"
>
<div className="pb-2">{children}</div>
</div>
)}
{acoes && (
<footer
data-acoes
className={`flex shrink-0 flex-col-reverse gap-2 border-t p-3 transition-colors sm:flex-row sm:justify-end ${
sombra.base ? "border-rule bg-surface" : "border-transparent"
}`}
>
{acoes}
</footer>
)}
{!acoes && <div className="h-3 shrink-0" />}
</section>
);
}
import { useState } from "react";
import { defineStories } from "@/registry/stories";
import { Botao } from "@/library/botao/Botao";
import {
BotaoDoDialogo,
Dialogo,
type FundoDoDialogo,
type PosicaoDoDialogo,
type StatusDoDialogo,
type TamanhoDoDialogo,
} from "./Dialogo";
const TERMOS = Array.from({ length: 14 }, (_, i) => (
<p key={i} className="mt-3 first:mt-0">
<strong className="text-ink">{i + 1}. Cláusula {i + 1}.</strong> O serviço é oferecido como está, e cada parte
responde pelos dados que envia. As condições podem mudar com aviso prévio de 30 dias por e-mail.
</p>
));
function ConfirmarComEspera() {
const [salvando, setSalvando] = useState(false);
const [aberto, setAberto] = useState(false);
return (
<Dialogo
aberto={aberto}
aoMudar={setAberto}
alerta
status="perigo"
titulo="Excluir o projeto?"
descricao="Os arquivos e o histórico serão apagados. Não dá para desfazer."
gatilho={<Botao variante="perigo">Excluir com espera</Botao>}
acoes={
<>
<BotaoDoDialogo variante="secundario" fecha disabled={salvando}>
Cancelar
</BotaoDoDialogo>
<BotaoDoDialogo
variante="perigo"
carregando={salvando}
textoCarregando="Excluindo..."
onClick={() => {
setSalvando(true);
setTimeout(() => {
setSalvando(false);
setAberto(false);
}, 1200);
}}
>
Excluir
</BotaoDoDialogo>
</>
}
/>
);
}
export default defineStories({
args: {
titulo: "Excluir o projeto?",
descricao: "Os arquivos e o histórico serão apagados. Não dá para desfazer.",
status: "perigo",
tamanho: "md",
posicao: "auto",
fundo: "escuro",
alerta: false,
longo: false,
botaoFechar: true,
incorporado: true,
},
controls: {
titulo: { type: "text" },
descricao: { type: "text" },
status: { type: "select", options: ["", "info", "sucesso", "alerta", "perigo"] },
tamanho: { type: "select", options: ["sm", "md", "lg", "xl"] },
posicao: { type: "select", options: ["auto", "centro", "topo", "base"] },
fundo: { type: "select", options: ["escuro", "desfoque", "transparente"] },
alerta: { type: "boolean" },
longo: { type: "boolean", label: "conteúdo longo (rola o meio)" },
botaoFechar: { type: "boolean" },
incorporado: { type: "boolean", label: "incorporado (desligue para abrir pelo botão)" },
},
render: (a) => (
<Dialogo
titulo={a.titulo as string}
descricao={(a.descricao as string) || undefined}
status={(a.status as StatusDoDialogo) || undefined}
tamanho={a.tamanho as TamanhoDoDialogo}
posicao={a.posicao as PosicaoDoDialogo}
fundo={a.fundo as FundoDoDialogo}
alerta={a.alerta as boolean}
botaoFechar={a.botaoFechar as boolean}
incorporado={a.incorporado as boolean}
gatilho={<Botao variante="perigo">Abrir diálogo</Botao>}
acoes={
<>
<BotaoDoDialogo variante="secundario" fecha>
Cancelar
</BotaoDoDialogo>
<BotaoDoDialogo variante={a.status === "perigo" ? "perigo" : "primario"} fecha>
Confirmar
</BotaoDoDialogo>
</>
}
>
{a.longo ? TERMOS : <p>O projeto e tudo o que está nele deixam de existir para toda a equipe.</p>}
</Dialogo>
),
stories: [
{ name: "Confirmação" },
{ name: "Aberto pelo botão", args: { incorporado: false } },
{
name: "Conteúdo longo",
args: {
incorporado: false,
status: "",
titulo: "Termos de uso",
descricao: "Leia antes de aceitar. Título e botões ficam no lugar.",
longo: true,
tamanho: "lg",
},
},
{ name: "Alerta obrigatório", args: { incorporado: false, alerta: true, botaoFechar: false } },
{
name: "Status",
render: () => (
<div className="flex flex-wrap gap-3">
{(
[
["info", "Sair da conta?", "Você vai precisar entrar de novo.", "primario"],
["sucesso", "Concluir a tarefa?", "A equipe recebe um aviso.", "primario"],
["alerta", "Descartar alterações?", "O que não foi salvo se perde.", "primario"],
["perigo", "Excluir a conta?", "Seus dados saem dos servidores.", "perigo"],
] as const
).map(([status, titulo, descricao, variante]) => (
<Dialogo
key={status}
status={status}
titulo={titulo}
descricao={descricao}
gatilho={<Botao variante="secundario">{status}</Botao>}
acoes={
<>
<BotaoDoDialogo variante="secundario" fecha>
Cancelar
</BotaoDoDialogo>
<BotaoDoDialogo variante={variante} fecha>
Confirmar
</BotaoDoDialogo>
</>
}
/>
))}
</div>
),
},
{ name: "Ação assíncrona", render: () => <ConfirmarComEspera /> },
],
});
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);
}