Chamada Final
Painel largo e bem arredondado para fechar a página: convite em letras grandes, texto de apoio e até dois botões. Sobe e aparece ao entrar na tela. Tons de destaque, escuro e superfície, legíveis nos dois modos.
Playground
Props
Geradas dos tipos TypeScript do componente; a descrição vem dos comentários /** */.
<ChamadaFinal>
Chamada final antes do rodapé: um painel largo, bem arredondado, com o convite em letras grandes, um texto de apoio e até dois botões. Sobe e aparece ao entrar na tela (sem animação com "reduzir movimento").
| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
| tituloobrigatória | ReactNode | — | |
| texto | 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> | AcaoDaChamada[] | — | Botões abaixo do texto (pensado para até dois), ou um conteúdo próprio (ReactNode) no lugar deles. |
| tom | "superficie" | "destaque" | "escuro" | "destaque" | Cor do painel: "destaque" (cor de destaque do tema), "escuro" (escuro nos dois modos) ou "superficie" (discreto, na cor de superfície). |
Código
stories.tsx tem os exemplos usados no preview e no playground.
"use client";
import { isValidElement, type ReactNode } from "react";
import { Revelar } from "@/library/revelar/Revelar";
/** Botão da chamada: vira <a> com `href` e <button> sem ele. */
export type AcaoDaChamada = {
rotulo: string;
href?: string;
aoClicar?: () => void;
/** Padrão: a primeira ação é "primaria" e as outras, "secundaria". */
variante?: "primaria" | "secundaria";
/** Abre o link em nova aba (com rel="noopener noreferrer"). */
novaAba?: boolean;
};
/**
* Pares de cores de cada tom, escolhidos para manter o contraste nos dois modos.
* "escuro" usa `color-scheme: dark`: os tokens do tema são `light-dark()`, então
* dentro do painel eles valem os valores do modo escuro, mesmo no modo claro.
*/
const TONS = {
destaque: {
painel: "bg-accent text-page",
texto: "text-page/80",
primaria: "bg-page text-ink hover:bg-page/90",
secundaria: "border border-page/50 text-page hover:bg-page/10",
foco: "outline-page",
},
escuro: {
painel: "scheme-dark border border-rule bg-surface text-ink",
texto: "text-ink-2",
primaria: "bg-accent text-page hover:bg-accent/90",
secundaria: "border border-ink/30 text-ink hover:bg-ink/10",
foco: "outline-accent",
},
superficie: {
painel: "bg-surface text-ink",
texto: "text-ink-2",
primaria: "bg-accent text-page hover:bg-accent/90",
secundaria: "border border-rule-strong text-ink hover:bg-ink/5",
foco: "outline-accent",
},
} as const;
export type TomDaChamada = keyof typeof TONS;
const ehAcao = (x: unknown): x is AcaoDaChamada =>
typeof x === "object" && x !== null && !isValidElement(x) && "rotulo" in x;
/**
* Chamada final antes do rodapé: um painel largo, bem arredondado, com o convite
* em letras grandes, um texto de apoio e até dois botões. Sobe e aparece ao
* entrar na tela (sem animação com "reduzir movimento").
*/
export function ChamadaFinal({
titulo,
texto,
acoes,
tom = "destaque",
}: {
titulo: ReactNode;
texto?: ReactNode;
/**
* Botões abaixo do texto (pensado para até dois), ou um conteúdo próprio
* (ReactNode) no lugar deles.
*/
acoes?: AcaoDaChamada[] | ReactNode;
/**
* Cor do painel: "destaque" (cor de destaque do tema), "escuro" (escuro nos
* dois modos) ou "superficie" (discreto, na cor de superfície).
*/
tom?: TomDaChamada;
}) {
const cores = TONS[tom];
const lista = Array.isArray(acoes) && acoes.every(ehAcao) ? acoes : null;
return (
<section className="w-full">
<Revelar deslocamento={40} duracao={900} margem="0px 0px -25% 0px">
<div
className={`mx-auto flex max-w-[100rem] flex-col items-center rounded-3xl px-6 py-20 text-center sm:px-12 lg:rounded-4xl lg:py-28 ${cores.painel}`}
>
<h2 className="max-w-3xl font-display text-4xl leading-[1.1] font-bold tracking-[-0.01em] sm:text-5xl">
{titulo}
</h2>
{texto && <p className={`mt-6 max-w-2xl text-lg leading-relaxed ${cores.texto}`}>{texto}</p>}
{lista
? lista.length > 0 && (
<div className="mt-10 flex w-full flex-col gap-3 sm:w-auto sm:flex-row sm:gap-4">
{lista.map((acao, i) => {
const variante = acao.variante ?? (i === 0 ? "primaria" : "secundaria");
const classes = `inline-flex h-12 cursor-pointer items-center justify-center rounded-2xl px-7 text-sm font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 ${cores.foco} ${cores[variante]}`;
return acao.href ? (
<a
key={`${acao.rotulo}-${i}`}
href={acao.href}
onClick={acao.aoClicar}
className={classes}
{...(acao.novaAba ? { target: "_blank", rel: "noopener noreferrer" } : {})}
>
{acao.rotulo}
</a>
) : (
<button key={`${acao.rotulo}-${i}`} type="button" onClick={acao.aoClicar} className={classes}>
{acao.rotulo}
</button>
);
})}
</div>
)
: acoes != null && acoes !== false && <div className="mt-10">{acoes as ReactNode}</div>}
</div>
</Revelar>
</section>
);
}
import { defineStories } from "@/registry/stories";
import { ChamadaFinal, type TomDaChamada } from "./ChamadaFinal";
export default defineStories({
args: {
titulo: "Pronto para acompanhar as vendas em tempo real?",
texto: "Fale com a nossa equipe e veja como montar o painel da sua empresa em uma tarde.",
tom: "destaque",
primaria: "Agendar uma demonstração",
secundaria: "Falar no chat",
},
controls: {
titulo: { type: "text" },
texto: { type: "text", label: "texto (vazio = sem)" },
tom: { type: "select", options: ["destaque", "escuro", "superficie"] },
primaria: { type: "text", label: "1ª ação (vazio = sem)" },
secundaria: { type: "text", label: "2ª ação (vazio = sem)" },
},
render: (a) => (
<div className="w-full">
<ChamadaFinal
titulo={a.titulo}
texto={a.texto || undefined}
tom={a.tom as TomDaChamada}
acoes={[
...(a.primaria ? [{ rotulo: a.primaria, href: "#demonstracao" }] : []),
...(a.secundaria ? [{ rotulo: a.secundaria, aoClicar: () => {}, variante: "secundaria" as const }] : []),
]}
/>
</div>
),
stories: [
{ name: "Destaque" },
{ name: "Escuro", args: { tom: "escuro" } },
{ name: "Superfície", args: { tom: "superficie" } },
{ name: "Um botão só", args: { secundaria: "", texto: "" } },
{
name: "Ação própria",
render: () => (
<div className="w-full">
<ChamadaFinal
tom="superficie"
titulo="Receba o resumo semanal das vendas"
texto="Um e-mail por semana, com os números que mudaram e o porquê."
acoes={
<form className="flex w-full max-w-md flex-col gap-3 sm:flex-row" onSubmit={(e) => e.preventDefault()}>
<label className="sr-only" htmlFor="email-chamada">
E-mail
</label>
<input
id="email-chamada"
type="email"
placeholder="voce@empresa.com"
className="h-12 min-w-0 flex-1 rounded-2xl border border-rule-strong bg-page px-4 text-ink outline-accent placeholder:text-muted focus-visible:outline-2 focus-visible:outline-offset-2"
/>
<button
type="submit"
className="h-12 cursor-pointer rounded-2xl bg-accent px-6 text-sm font-medium text-page outline-accent hover:bg-accent/90 focus-visible:outline-2 focus-visible:outline-offset-2"
>
Assinar
</button>
</form>
}
/>
</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.
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);
}