Abertura com Imagem
Abertura de página em duas colunas: link de voltar, chapéu, título grande, resumo e chamada de um lado; imagem ou mídia de cantos bem arredondados do outro. Empilha no celular.
Playground
Props
Geradas dos tipos TypeScript do componente; a descrição vem dos comentários /** */.
<AberturaComImagem>
Abertura de página em duas colunas: link de voltar, chapéu, título grande, resumo e chamada de um lado; imagem (ou outra mídia) de cantos bem arredondados do outro. No celular, empilha com o texto primeiro. Para só a manchete, sem mídia, veja a `Abertura`.
| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
| voltar | { rotulo: string; href: string; } | — | Link de volta acima do chapéu ("← Voltar aos serviços"). |
| chapeu | string | — | Linha curta acima do título: categoria, seção, contexto. |
| tituloobrigatória | ReactNode | — | |
| resumo | string | number | bigint | boolean | ReactElement<unknown, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | Promise<AwaitedReactNode> | — | Parágrafo abaixo do título. |
| acao | string | number | bigint | boolean | ReactElement<unknown, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | Promise<AwaitedReactNode> | — | Chamada abaixo do resumo (botão ou link). |
| midia | string | number | bigint | boolean | ReactElement<unknown, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | Promise<AwaitedReactNode> | — | Mídia própria (vídeo, ilustração, mapa...); ocupa a moldura toda. Tem prioridade sobre `imagem`. |
| imagem | { src: string; alt: string; } | — | Imagem da moldura, recortada para caber (object-cover). |
| inverter | boolean | false | Põe a mídia à esquerda nas telas largas. |
| proporcao | string | "3 / 2" | Proporção da moldura da mídia (CSS aspect-ratio). |
| fundo | "superficie" | "nenhum" | "superficie" | "superficie" pinta a faixa com a cor de superfície; "nenhum" deixa transparente. |
| className | string | "" | Classes extras no <section> (por exemplo, mais respiro no topo para uma navegação flutuante). |
Código
stories.tsx tem os exemplos usados no preview e no playground.
import type { ReactNode } from "react";
/**
* Abertura de página em duas colunas: link de voltar, chapéu, título grande,
* resumo e chamada de um lado; imagem (ou outra mídia) de cantos bem
* arredondados do outro. No celular, empilha com o texto primeiro. Para só a
* manchete, sem mídia, veja a `Abertura`.
*/
export function AberturaComImagem({
voltar,
chapeu,
titulo,
resumo,
acao,
midia,
imagem,
inverter = false,
proporcao = "3 / 2",
fundo = "superficie",
className = "",
}: {
/** Link de volta acima do chapéu ("← Voltar aos serviços"). */
voltar?: { rotulo: string; href: string };
/** Linha curta acima do título: categoria, seção, contexto. */
chapeu?: string;
titulo: ReactNode;
/** Parágrafo abaixo do título. */
resumo?: ReactNode;
/** Chamada abaixo do resumo (botão ou link). */
acao?: ReactNode;
/** Mídia própria (vídeo, ilustração, mapa...); ocupa a moldura toda. Tem prioridade sobre `imagem`. */
midia?: ReactNode;
/** Imagem da moldura, recortada para caber (object-cover). */
imagem?: { src: string; alt: string };
/** Põe a mídia à esquerda nas telas largas. */
inverter?: boolean;
/** Proporção da moldura da mídia (CSS aspect-ratio). */
proporcao?: string;
/** "superficie" pinta a faixa com a cor de superfície; "nenhum" deixa transparente. */
fundo?: "superficie" | "nenhum";
/** Classes extras no <section> (por exemplo, mais respiro no topo para uma navegação flutuante). */
className?: string;
}) {
const temMidia = Boolean(midia || imagem);
return (
<section className={`w-full py-16 lg:py-24 ${fundo === "superficie" ? "bg-surface" : ""} ${className}`}>
<div
className={`mx-auto grid max-w-[100rem] items-center gap-12 px-6 sm:px-10 lg:gap-20 lg:px-16 ${
temMidia
? inverter
? "lg:grid-cols-[minmax(0,44%)_minmax(0,1fr)]"
: "lg:grid-cols-[minmax(0,1fr)_minmax(0,44%)]"
: ""
}`}
>
<div>
{voltar && (
<a
href={voltar.href}
className="inline-flex items-center gap-2 rounded-sm text-sm text-ink-2 outline-accent transition-colors hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-2"
>
<span aria-hidden="true">←</span> {voltar.rotulo}
</a>
)}
{chapeu && (
<p className={`text-xs font-semibold tracking-[0.14em] text-accent uppercase ${voltar ? "mt-8" : ""}`}>
{chapeu}
</p>
)}
<h1
className={`max-w-2xl font-display text-4xl leading-[1.1] font-bold tracking-[-0.01em] sm:text-5xl lg:text-6xl ${
voltar || chapeu ? "mt-4" : ""
}`}
>
{titulo}
</h1>
{resumo && <p className="mt-6 max-w-xl text-lg leading-relaxed text-ink-2">{resumo}</p>}
{acao && <div className="mt-10">{acao}</div>}
</div>
{temMidia && (
<div
className={`relative overflow-hidden rounded-3xl bg-track lg:rounded-4xl ${inverter ? "lg:order-first" : ""}`}
style={{ aspectRatio: proporcao }}
>
{midia ??
(imagem && (
// eslint-disable-next-line @next/next/no-img-element -- a biblioteca não depende de next/image
<img src={imagem.src} alt={imagem.alt} className="absolute inset-0 size-full object-cover" />
))}
</div>
)}
</div>
</section>
);
}
import { defineStories } from "@/registry/stories";
import { AberturaComImagem } from "./AberturaComImagem";
// Mídias abstratas feitas só com as cores do tema (sem imagens externas).
const MIDIAS = {
ondas: {
background: [
"radial-gradient(70% 90% at 85% 15%, var(--series-1), transparent 70%)",
"radial-gradient(60% 80% at 10% 95%, var(--series-3), transparent 70%)",
"linear-gradient(150deg, var(--accent), var(--series-7))",
].join(", "),
},
grade: {
background: [
"repeating-linear-gradient(90deg, color-mix(in oklab, var(--page) 18%, transparent) 0 1px, transparent 1px 48px)",
"repeating-linear-gradient(0deg, color-mix(in oklab, var(--page) 18%, transparent) 0 1px, transparent 1px 48px)",
"linear-gradient(200deg, var(--series-4), var(--series-2))",
].join(", "),
},
};
function MidiaAbstrata({ nome }: { nome: keyof typeof MIDIAS }) {
return <div aria-hidden="true" className="absolute inset-0" style={MIDIAS[nome]} />;
}
function Chamada({ texto }: { texto: string }) {
return (
<a
href="#contato"
className="inline-flex h-12 items-center rounded-2xl bg-accent px-7 text-sm font-medium text-page outline-accent transition-opacity hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2"
>
{texto}
</a>
);
}
export default defineStories({
args: {
voltar: "Voltar aos produtos",
chapeu: "Relatórios",
titulo: "Relatórios que se explicam sozinhos",
resumo:
"Cada gráfico chega com um resumo do que mudou e por quê, para a reunião de segunda começar pela decisão, e não pela planilha.",
acao: "Agendar uma demonstração",
midia: "ondas",
inverter: false,
fundo: "superficie",
},
controls: {
voltar: { type: "text", label: "voltar.rotulo (vazio = sem)" },
chapeu: { type: "text", label: "chapeu (vazio = sem)" },
titulo: { type: "text" },
resumo: { type: "text", label: "resumo (vazio = sem)" },
acao: { type: "text", label: "acao (texto do botão; vazio = sem)" },
midia: { type: "select", label: "midia (exemplos abstratos)", options: ["ondas", "grade", "nenhuma"] },
inverter: { type: "boolean" },
fundo: { type: "select", options: ["superficie", "nenhum"] },
},
render: (a) => (
<div className="w-full overflow-hidden rounded-2xl">
<AberturaComImagem
voltar={a.voltar ? { rotulo: a.voltar, href: "#produtos" } : undefined}
chapeu={a.chapeu || undefined}
titulo={a.titulo}
resumo={a.resumo || undefined}
acao={a.acao ? <Chamada texto={a.acao} /> : undefined}
midia={a.midia === "nenhuma" ? undefined : <MidiaAbstrata nome={a.midia as keyof typeof MIDIAS} />}
inverter={a.inverter}
fundo={a.fundo as "superficie" | "nenhum"}
/>
</div>
),
stories: [
{ name: "Mídia à direita" },
{ name: "Mídia à esquerda", args: { inverter: true, midia: "grade", voltar: "" } },
{ name: "Sem fundo", args: { fundo: "nenhum", chapeu: "Equipes", titulo: "Metas por pessoa, sem planilha" } },
{ name: "Sem mídia", args: { midia: "nenhuma" } },
],
});
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);
}