Ir para o conteúdo

Utilitários & headless

Componentes pequenos e focados: alguns renderizam pedaços de UI (Money, RelativeTime, CopyButton), outros são headless — controlam comportamento/lógica sem opinar sobre o visual (Portal, ClickOutside, For). Todos importados de tempest-react-sdk.

Display

CopyButton

Stat · Tag · Money · Banner na gallery

Seção data-display da gallery — rode localmente para interagir.

Botão que copia uma string pra clipboard e mostra um estado transiente de "copiado".

import { CopyButton } from "tempest-react-sdk";

<CopyButton value="npm i tempest-react-sdk" />;

<CopyButton value={token} timeout={3000} onCopied={() => toast("Token copiado")}>
  Copiar token
</CopyButton>;
Prop Tipo Default Notas
value string — Texto escrito no clipboard.
timeout number (ms) 2000 Quanto tempo o estado "copiado" fica ativo.
children ReactNode "Copy"/"Copied" Label fixo nos dois estados; sem children o texto alterna.
onCopied () => void — Chamado após escrita bem-sucedida.

Estende ButtonHTMLAttributes. Falha de clipboard é silenciada; o timer é limpo no unmount.

RelativeTime

Renderiza uma data como string relativa ("5 min atrás") dentro de um <time> semântico com dateTime legível por máquina.

import { RelativeTime } from "tempest-react-sdk";

<RelativeTime date={post.createdAt} />; // pt-BR
<RelativeTime date={post.createdAt} locale="en" />;
Prop Tipo Default Notas
date Date \| string \| number — Instante a renderizar.
locale "pt" \| "en" "pt" "pt" mapeia pra "pt-BR".

Estende HTMLAttributes<HTMLTimeElement>.

Money

Renderiza um valor monetário em centavos como string de moeda localizada num <span>.

import { Money } from "tempest-react-sdk";

<Money cents={1990} />; // "R$ 19,90"
<Money cents={500} currency="USD" locale="en-US" />; // "$5.00"
Prop Tipo Default Notas
cents number — Valor na menor unidade (centavos).
currency string "BRL" Código ISO 4217.
locale string "pt-BR" Locale BCP 47 usado na formatação.

Estende HTMLAttributes<HTMLSpanElement>. Internamente divide cents por 100 e usa Intl.NumberFormat.

TruncateText

Limita o texto a um número fixo de linhas via CSS line-clamp, com reticências no overflow.

import { TruncateText } from "tempest-react-sdk";

<TruncateText lines={2}>{longDescription}</TruncateText>;
Prop Tipo Default Notas
lines number 1 Linhas antes de clampar (--tempest-clamp-lines).
children ReactNode — Conteúdo a clampar.

Estende HTMLAttributes<HTMLDivElement>.

VisuallyHidden

Headless & render-props na gallery

Seção headless da gallery — rode localmente para interagir.

Conteúdo escondido visualmente mas acessível a leitores de tela — o padrão sr-only.

import { VisuallyHidden } from "tempest-react-sdk";

<button>
  <Icon />
  <VisuallyHidden>Fechar</VisuallyHidden>
</button>;
Prop Tipo Default Notas
as keyof JSX.IntrinsicElements "span" Elemento intrínseco a renderizar.

Estende HTMLAttributes<HTMLElement>.


Movimento

Reveal

Reveal na gallery

Seção reveal da gallery — rode localmente para interagir.

Conteúdo que entra ao chegar na viewport — fade ou deslize ao rolar, o padrão de toda landing page. Revela uma vez e para de observar.

import { Card, Reveal } from "tempest-react-sdk";

const etapas = [
  { id: "1", titulo: "Diagnóstico" },
  { id: "2", titulo: "Protótipo" },
  { id: "3", titulo: "Piloto" },
];

export function Etapas() {
  return (
    <ol>
      {etapas.map((etapa, index) => (
        <Reveal key={etapa.id} as="li" variant="up" delay={index * 90}>
          <Card title={etapa.titulo} />
        </Reveal>
      ))}
    </ol>
  );
}

O escalonamento é o index do map vezes um passo: não existe RevealGroup, porque o índice já está na sua mão e um grupo só esconderia essa conta atrás de cloneElement.

Prop Tipo Default Notas
variant "fade" \| "up" \| "down" \| "left" \| "right" \| "scale" "up" De onde o conteúdo vem. up sobe de baixo; left vem da direita para a esquerda.
delay number (ms) 0 Espera antes de entrar (--tempest-reveal-delay).
duration number (ms) --tempest-duration-slower Duração da entrada (--tempest-reveal-duration).
as keyof JSX.IntrinsicElements "div" Elemento a renderizar — "li" dentro de <ul>/<ol> mantém a semântica da lista.
once boolean true false esconde de novo quando o elemento sai inteiro de vista, e anima na próxima volta.
threshold number 0.15 Fração do elemento — ou da viewport, se ele for mais alto que ela — que precisa aparecer.
rootMargin string "0px" Margem do root, como no IntersectionObserver.

Estende HTMLAttributes<HTMLElement>. O estado atual sai em data-state ("static", "hidden" ou "shown") e a variante em data-variant.

As três armadilhas de um Reveal escrito à mão já vêm resolvidas no componente:

  • prefers-reduced-motion: reduce → o conteúdo fica visível e parado, sem transform nem transição (WCAG 2.3.3). É CSS, então vale também se a preferência mudar com a página aberta.
  • Sem IntersectionObserver → o elemento fica em data-state="static", que é conteúdo visível. Ele nunca fica escondido esperando uma observação que não vai chegar.
  • Elemento mais alto que a viewport → um bloco de três telas nunca chega a 50% visível, então threshold={0.5} cru nunca revelaria. O Reveal lê a geometria na primeira observação e reescala o threshold pela fração máxima alcançável: o bloco revela quando ocupa metade da viewport.

Markup sem JavaScript fica visível

O estado escondido só é aplicado no cliente que consegue observar. Renderizado sem JS — ou antes da hidratação —, o elemento sai como data-state="static". A transição mora só no estado "shown", então a troca para "hidden" na montagem é instantânea, sem fade-out.

Personalize sem prop nova pelas custom properties --tempest-reveal-distance (default 1.5rem), --tempest-reveal-scale (0.95) e --tempest-reveal-easing.


Headless / lógicos

Sem CSS próprio: encapsulam comportamento e te deixam fornecer a marcação.

Portal

Renderiza os filhos em outra parte da árvore DOM via React portal — ideal pra overlays que precisam escapar de overflow/stacking contexts.

import { Portal } from "tempest-react-sdk";

<Portal>
  <div className="toast">Salvo!</div>
</Portal>;

<Portal container={drawerRoot}>{menu}</Portal>;
Prop Tipo Default Notas
children ReactNode — Conteúdo renderizado através do portal.
container Element \| null document.body Nó DOM alvo.

SSR-safe

Renderiza null no servidor e no primeiro render do cliente; monta o portal só depois da hidratação.

ClickOutside

Embrulha os filhos num <div> e dispara onOutside quando um mousedown/touchstart acontece fora da subárvore. Útil pra fechar popovers e menus.

import { ClickOutside } from "tempest-react-sdk";

<ClickOutside onOutside={() => setOpen(false)}>
  <Menu />
</ClickOutside>;
Prop Tipo Default Notas
onOutside (event: MouseEvent \| TouchEvent) => void — Chamado em interação externa.
children ReactNode — Conteúdo dentro da fronteira.

Estende HTMLAttributes<HTMLDivElement> (passa props pro <div> wrapper).

ConditionalWrapper

Embrulha os filhos com wrapper só quando condition é true — evita duplicar a subárvore só pra adicionar um wrapper opcional (link, tooltip, boundary).

import { ConditionalWrapper } from "tempest-react-sdk";

<ConditionalWrapper condition={Boolean(href)} wrapper={(children) => <a href={href}>{children}</a>}>
  <CardBody />
</ConditionalWrapper>;
Prop Tipo Default Notas
condition boolean — Quando true, aplica o wrapper.
wrapper (children: ReactNode) => ReactNode — Função de embrulho.
children ReactNode — Conteúdo que pode ser embrulhado.

For

Renderizador de listas tipado e JSX-friendly, com fallback pra coleção vazia. O tipo do item é inferido de each.

import { For } from "tempest-react-sdk";

<For each={users} fallback={<p>Nenhum usuário</p>}>
  {(user, index) => (
    <li key={user.id}>
      {index + 1}. {user.name}
    </li>
  )}
</For>;
Prop Tipo Default Notas
each readonly T[] — Coleção a iterar.
children (item: T, index: number) => ReactNode — Render por item.
fallback ReactNode null Renderizado quando each está vazio.

ErrorText

Inputs avançados (Date · Pin · Slider) na gallery

Seção inputs-extra da gallery — rode localmente para interagir.

Mensagem de erro de campo de formulário como <p role="alert">. Renderiza null quando não há children — pode ficar fixo abaixo do campo e só aparece quando há erro.

import { ErrorText } from "tempest-react-sdk";

<input aria-invalid={Boolean(error)} />
<ErrorText>{error}</ErrorText>;
Prop Tipo Default Notas
children ReactNode — Mensagem; null/""/false → não renderiza nada.

Estende HTMLAttributes<HTMLParagraphElement>. Estilizado com o token --tempest-danger.


Mídia / conteúdo

Image

Avatar · Image · Carousel na gallery

Seção display-media da gallery — rode localmente para interagir.

<img> com lazy loading nativo e fallback de uma tentativa.

import { Image } from "tempest-react-sdk";

<Image src={user.avatarUrl} fallback="/avatar-placeholder.png" alt={user.name} />;
Prop Tipo Default Notas
src string — Fonte primária.
fallback string — Fonte trocada uma vez se a primária falhar.
alt string — Texto alternativo (obrigatório).
lazy boolean true true → loading="lazy"; false → eager.

Estende ImgHTMLAttributes (sem src). O fallback é guardado pra não entrar em loop de onError.

DataList

Lista genérica tipada que renderiza um <ul> com um <li> por item, com slot de vazio.

import { DataList } from "tempest-react-sdk";

<DataList
  items={notifications}
  keyExtractor={(n) => n.id}
  renderItem={(n) => <NotificationRow notification={n} />}
  empty={<p>Sem novidades</p>}
/>;
Prop Tipo Default Notas
items readonly T[] — Coleção a renderizar.
renderItem (item: T, index: number) => ReactNode — Conteúdo de cada <li>.
keyExtractor (item: T, index: number) => string \| number índice Key estável por item.
empty ReactNode — Renderizado quando items está vazio.

Estende HTMLAttributes<HTMLUListElement>.

DescriptionList

<dl> semântico de pares termo/descrição, com estilização chave/valor baseada em tokens.

import { DescriptionList } from "tempest-react-sdk";

<DescriptionList
  items={[
    { term: "Pedido", description: "#1042" },
    { term: "Status", description: <Badge variant="success">Pago</Badge> },
    { term: "Total", description: <Money cents={1990} /> },
  ]}
/>;
Prop Tipo Default Notas
items DescriptionListItem[] — Pares <dt>/<dd>.

DescriptionListItem = { term: ReactNode; description: ReactNode }. Estende HTMLAttributes<HTMLDListElement>.

CodeBlock

CodeBlock na gallery

Seção codeblock da gallery — rode localmente para interagir.

Amostra de código somente leitura: cores de sintaxe, número de linha opcional, botão de copiar.

import { CodeBlock } from "tempest-react-sdk";

<CodeBlock code={snippet} language="ts" filename="src/api.ts" showLineNumbers />
<CodeBlock code={log} language="bash" maxHeight={280} />
Prop Tipo Default Notas
code string — A fonte. Linhas em branco nas pontas são aparadas.
language string — Gramática ou apelido. Desconhecido renderiza como texto.
filename ReactNode — Mostrado no cabeçalho.
showLineNumbers boolean false Numera as linhas.
highlightLines number[] — Linhas 1-based marcadas como o ponto do trecho.
copyable boolean true Botão de copiar no cabeçalho.
maxHeight number \| string — Limita a altura; o corpo rola.
wrap boolean false Quebra linha em vez de rolar na horizontal.
label string — Nome acessível da região.

Gramáticas: typescript · javascript · tsx · jsx · json · css · html · bash · python · sql, com apelidos (ts, js, sh, py, scss, xml, shell, zsh, jsonc…).

É um scanner, não um parser — e isso é um teto escolhido

O realce reconhece comentário, string, número, palavra-chave e pontuação por padrão de texto. Ele não sabe nada de escopo, tipo ou gramática. Um parser de verdade por linguagem é uma dependência do tamanho do resto do SDK, e o ganho — acertar os cantos raros de um trecho de documentação — é pequeno. Onde não tem certeza ele emite plain, que sai como texto normal em vez de sair errado. Linguagem desconhecida vira um bloco sem cor, que é resultado normal e nunca erro.

O <pre> é sempre focável

Um bloco de código rola e não tem nada focável dentro. Sem parada de tabulação, quem navega por teclado vê a barra de rolagem e não tem como movê-la — o foco nunca pousa onde as setas rolariam. É o único contêiner de rolagem do SDK onde a parada é incondicional em vez de medida: um trecho de código existe pra ser alcançado, lido e selecionado por conta própria. Os outros (Table, VirtualList, ScrollArea) só ganham a parada enquanto de fato transbordam.

Número de linha é decoração — e some do clipboard

Ele é aria-hidden (um leitor anunciando "um const dois import" não ajuda) e user-select: none. Selecionar o bloco com o mouse e copiar devolve a fonte, sem os números. Verificado no browser: a seleção do <code> inteiro sai idêntica ao original.

As cores de sintaxe têm tokens próprios, não a rampa de chart

--tempest-code-*. A rampa de chart é validada pro piso de marca (3:1); isso aqui é texto e precisa de 4,5:1. Medindo a rampa como texto ela falha nos dois modos — uma palavra-chave deu 3,47:1 na superfície escura, uma string 2,03:1 na clara. Cada token de código foi resolvido em OKLCH contra os dois fundos em que pode cair: a superfície do bloco e a linha marcada depois que o realce compõe sobre ela. Ver tokens de estilo.

QRCode

QRCode na gallery

Seção qrcode da gallery — rode localmente para interagir.

Um símbolo QR codificado no browser e desenhado em SVG. Sem dependência e sem ida a serviço de imagem — um gerador remoto entregaria o conteúdo (link de pagamento, token de sessão, convite) a um terceiro.

import { QRCode } from "tempest-react-sdk";

<QRCode value="https://tempest.dev" />
<QRCode value={pixPayload} level="H" size={220} label="QR do Pix — R$ 42,00" />
Prop Tipo Default Notas
value string — O conteúdo. UTF-8 quando não é só dígito ou caixa alta.
size number 160 Lado renderizado em px, zona de silêncio incluída.
level "L" \| "M" \| "Q" \| "H" "M" Correção de erro: ~7% · ~15% · ~25% · ~30% recuperável.
margin number 4 Zona de silêncio em módulos.
color string #000000 Cor dos módulos.
background string #ffffff Cor do fundo.
label string QR code: {value} Nome acessível.

Preto no branco nos dois temas — de propósito

É a única parte do SDK que ignora os tokens de tema. Leitor de QR espera escuro sobre claro, e os que lidam com símbolo invertido fazem isso devagar e sem confiança. Ligar os módulos em --tempest-text inverteria eles no dark mode e deixaria um símbolo claro sobre o fundo branco — que não lê como nada. Um QR que combina com a página escura e escaneia na terceira tentativa é pior que um que parece colado por cima. Só mexa em color/background com um leitor na mão pra testar.

O modo muda o tamanho do símbolo, e o tamanho muda a facilidade de leitura

O encoder escolhe automaticamente o modo mais denso que o conteúdo permite: numérico empacota 3 dígitos em 10 bits, alfanumérico 2 caracteres em 11, byte gasta 8 bits por byte. Um telefone em dígitos puros cabe num símbolo visivelmente mais grosso que o mesmo número em bytes — e módulo maior é módulo mais fácil de escanear. Se você controla o formato do payload, caixa alta e sem acento vale tamanho de símbolo.

Zona de silêncio abaixo de 4 módulos é onde leitor começa a errar

O margin default segue a norma. Diminuir pra ganhar espaço na tela é a otimização que mais custa taxa de leitura.

Nível de correção não é qualidade — é tolerância a dano

L basta pra QR de tela, que ninguém amassa. Q/H valem quando o símbolo vai pra papel, adesivo, vitrine ou crachá: recuperam ~25%/~30% dos codewords, então rasgo, dobra e sujeira ainda leem. O custo é um símbolo maior pro mesmo conteúdo.

Leitor de tela não escaneia

Por isso o aria-label default nomeia o conteúdo (QR code: https://…) em vez de dizer só "QR code". Quando o payload é opaco — um BR Code do Pix, por exemplo — passe label descrevendo o que ele faz, e ofereça o mesmo dado em texto ou botão de copiar ao lado.

Conteúdo grande demais

Um payload que não cabe nem numa versão 40 lança QRCapacityError em vez de desenhar um símbolo truncado que escaneia errado. É erro de programação, não estado a renderizar — se o conteúdo vem do usuário, valide antes ou embrulhe num ErrorBoundary.

import { encodeQR, QRCapacityError } from "tempest-react-sdk";

try {
  encodeQR(payload, { level: "H" });
} catch (error) {
  if (error instanceof QRCapacityError) {
    // error.length (bytes) e error.level
  }
}

encodeQR(value, { level, minVersion }) devolve a matriz crua (modules, size, version, mode, mask) pra quem precisa desenhar por conta própria — canvas, PDF, etiqueta térmica. matrixToPath(matrix, margin) gera o mesmo d que o componente usa.


Recap

  • Display: CopyButton (clipboard + estado transiente), RelativeTime (<time> relativo), Money (centavos → moeda), TruncateText (line-clamp), VisuallyHidden (sr-only).
  • Movimento: Reveal (entrada ao rolar, escalonável por delay; visível sem IntersectionObserver e parado com prefers-reduced-motion).
  • Headless/lógicos: Portal (SSR-safe), ClickOutside, ConditionalWrapper, For (lista tipada com fallback), ErrorText (erro de campo role="alert").
  • Mídia/conteúdo: Image (lazy + fallback), DataList (<ul> genérico), DescriptionList (<dl> termo/valor), CodeBlock (amostra de código com realce), QRCode (símbolo QR em SVG, codificado no browser).
  • Componentes "display" e "conteúdo" usam tokens --tempest-*; os headless não trazem CSS — você fornece a marcação.

Veja também

  • Utilitários — Money/RelativeTime são as versões em componente de helpers de formatação.
  • Dados — Table/VirtualList para coleções maiores.