Server Components e SSR
O Camaleo renderiza no servidor com o estilo já aplicado. Sem script de tema, sem piscar, e a maior parte das peças de layout continua Server Component.
// você vai aprender
- Quais componentes são Server Components e quais abrem uma fronteira
"use client" - Como definir estilo, modo e marca no
<html>a partir do layout raiz - Como guardar a escolha de claro ou escuro sem a página piscar
- Como os textos embutidos chegam aos Server Components pelo
Message
Componentes de servidor e de cliente
Cada componente fica no próprio arquivo em packages/react/src, e o build mantém esses arquivos separados, então um "use client" nunca vaza para um arquivo que não precisa dele. Você pode importar tudo de @camaleo/react num Server Component: o React transforma cada componente de cliente numa fronteira sozinho.
Estes arquivos não têm "use client". Os componentes deles renderizam só no servidor e não mandam JavaScript:
| Arquivo | Componentes |
|---|---|
alert.tsx | Alert, AlertTitle, AlertDescription |
app-shell.tsx | AppShell, AppShellHeader, AppShellBody, AppShellContent, AppShellFooter, SkipLink |
data-display.tsx | Stat e partes, DataList e partes, Timeline e partes, EmptyState e partes, ProgressCircle |
display.tsx | Badge, Kbd, Card e partes, Table e partes, Skeleton |
layout.tsx | Box, Stack, Grid, Container, AspectRatio |
spinner.tsx | Spinner |
typography.tsx | Heading, Text |
visually-hidden.tsx | VisuallyHidden |
style-scope.ts | styleScopeProps() (uma função comum) |
Todo o resto é componente de cliente: toda parte interativa (feita sobre Base UI ou React Aria), mais StyleProvider, MessagesProvider e DirectionProvider, que guardam contexto do React. Componentes de cliente também viram HTML no servidor; eles só hidratam no navegador.
O Table continua Server Component mesmo rolando: só o pequeno contêiner de rolagem (table-scroll.tsx) é de cliente, e ele só vira uma região focável quando a tabela transborda.
Defina o estilo no <html>
No layout raiz, espalhe styleScopeProps() no <html>. É uma função comum, segura num Server Component, que devolve data-style, data-mode e a escala da marca como custom properties inline. O estilo está no primeiro byte do HTML, então nada espera o JavaScript.
import "@camaleo/styles/base.css";
import "@camaleo/styles/glass.css";
import { styleScopeProps } from "@camaleo/react/style-scope";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="pt-BR" {...styleScopeProps({ styleName: "glass", brand: "#3fa34d" })}>
<body>{children}</body>
</html>
);
}
Importe o base.css uma vez e o CSS de cada estilo que você renderiza. Diálogos, menus e toasts vão por portal para o <body>, então herdam o estilo do <html> sem trabalho extra.
Guarde o modo da pessoa sem piscar
As cores do Camaleo usam light-dark(), e o data-mode só define o color-scheme. Sem mode, a página segue o sistema com CSS puro: não há script rodando antes da pintura, então não há o que piscar.
Quando a pessoa escolhe claro ou escuro, guarde a escolha num cookie e leia no layout. O servidor já manda o data-mode certo na primeira resposta.
import { cookies } from "next/headers";
import { styleScopeProps } from "@camaleo/react/style-scope";
const MODES = ["light", "dark", "system"] as const;
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const saved = (await cookies()).get("cml-mode")?.value;
// Só aceite valores conhecidos: o cookie vem do navegador.
const mode = MODES.find((m) => m === saved) ?? "system";
return (
<html lang="pt-BR" {...styleScopeProps({ styleName: "glass", mode })}>
<body>{children}</body>
</html>
);
}
Grave o cookie numa Server Function. Usada como action de um formulário, ela devolve a página renderizada de novo na mesma ida ao servidor:
"use server";
import { cookies } from "next/headers";
export async function setMode(form: FormData) {
const mode = form.get("mode");
if (mode !== "light" && mode !== "dark" && mode !== "system") return;
(await cookies()).set("cml-mode", mode, { maxAge: 60 * 60 * 24 * 365 });
}
import { Button } from "@camaleo/react";
import { setMode } from "./actions";
export function ModeSwitch() {
return (
<form action={setMode}>
<Button type="submit" name="mode" value="dark" variant="outline">
Escuro
</Button>
</form>
);
}
Nesta versão do Next.js, cookies() é assíncrono, e ler cookies deixa a rota dinâmica. Se a página precisa ser estática, deixe o mode de fora e o sistema decide.
Crie ilhas com StyleProvider
O StyleProvider é componente de cliente, mas renderiza o seu <div data-style> no servidor como qualquer outro. Uma seção em outro estilo já está no HTML desde o início:
import { Button, StyleProvider } from "@camaleo/react";
export default function Page() {
return (
<StyleProvider styleName="neubrutalism" mode="light">
<Button>Começar</Button>
</StyleProvider>
);
}
Popups abertos dentro da ilha mantêm o estilo dela: o DialogContent e as outras partes em portal leem o escopo do provider pelo contexto e o aplicam de novo.
Defina o idioma no servidor
O MessagesProvider aceita um pacote embutido pelo nome, que é um texto simples e atravessa a fronteira servidor–cliente sem problema:
import { MessagesProvider } from "@camaleo/react";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="pt-BR">
<body>
<MessagesProvider locale="pt-BR">{children}</MessagesProvider>
</body>
</html>
);
}
Trocas feitas só de textos também funcionam aqui. Algumas mensagens são funções (pagination.page, carousel.goToSlide…), e funções não passam de um Server Component para um de cliente, então troque essas num componente de cliente. O guia de Idiomas e RTL traz todas as chaves.
O auxiliar Message
Server Components não leem contexto do React, então Spinner, SkipLink e ProgressCircle não podem chamar useMessages(). O rótulo padrão deles é <Message pick="…" />: um componente de cliente minúsculo que imprime uma mensagem do MessagesProvider mais próximo. O resto de cada componente fica no servidor.
Você pode usá-lo nos seus Server Components para as mesmas três chaves:
import { Message } from "@camaleo/react";
// Um "Carregando…" visível, no idioma do MessagesProvider mais próximo.
export default function Loading() {
return (
<p role="status">
<Message pick="spinner.label" />…
</p>
);
}
O pick aceita "skipLink.label", "spinner.label" e "progressCircle.label". Para qualquer outro texto, use useMessages() num componente de cliente.
Antes de publicar
- No código-fonte da página, o
<html>(ou o<div>do provider) já temdata-style - Todo estilo renderizado tem o CSS importado no layout raiz
- O modo lido do cookie é conferido contra os valores conhecidos
- Nenhum layout ganhou
"use client"só por causa do Camaleo - O
MessagesProvidernum Server Component recebelocaleoumessagessó com textos