Ir para o conteúdo

Avançados: layout & UX

Rolagem estilizada, painéis redimensionáveis, calendário e agenda. Dão forma ao espaço em volta do conteúdo, sem dependência externa.

ScrollArea

Contêiner de rolagem estilizado que transborda no eixo escolhido e renderiza uma barra de rolagem fina (WebKit). Repassa className, style e ref ao <div>.

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

<ScrollArea maxHeight={240} orientation="vertical">
  <ul>
    {items.map((item) => (
      <li key={item.id}>{item.name}</li>
    ))}
  </ul>
</ScrollArea>;
Prop Tipo Default Descrição
maxHeight number \| string Limita a altura; números viram pixels
orientation "vertical" \| "horizontal" \| "both" "vertical" Qual eixo rola
scrollLabel string "Área rolável" Nome acessível da região rolável

Demais props de <div> são repassadas.

Enquanto transborda, vira um grupo focável

Uma área cujo conteúdo é texto puro não tem nada focável dentro. Sem um ponto de tabulação próprio, quem navega por teclado vê a barra de rolagem e não tem como movê-la — o foco nunca pousa onde as setas rolariam. Por isso a área recebe tabIndex={0} + role="group" + aria-label só enquanto o conteúdo de fato transborda, e o perde de volta quando cabe. Uma área que não rola nunca adiciona parada de tab. role e tabIndex passados pelo chamador continuam vencendo.

Resizable

Layout de dois painéis com divisor arrastável. O primeiro painel é dimensionado via flex-basis em porcentagem; o segundo preenche o resto. Arraste com o ponteiro ou foque o divisor e use as setas (passo de 2%).

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

<Resizable direction="horizontal" defaultSize={40} min={20} max={80}>
  <aside>Painel lateral</aside>
  <main>Conteúdo principal</main>
</Resizable>;
Prop Tipo Default Descrição
direction "horizontal" \| "vertical" "horizontal" horizontal coloca os painéis lado a lado
defaultSize number (%) 50 Tamanho inicial do primeiro painel, em porcentagem
min number (%) 10 Clamp inferior do primeiro painel
max number (%) 90 Clamp superior do primeiro painel
children [ReactNode, ReactNode] Exatamente dois painéis — [paneA, paneB]

Exatamente dois filhos

children é uma tupla [ReactNode, ReactNode]. O tamanho é sempre fixado em [min, max].

Calendar

Seletor de data em grade mensal. Cabeçalho com mês/ano + botões prev/next, linha de dias da semana e grade 6×7 de botões de dia. Seleção e mês visível controláveis ou não-controlados. Aritmética com Date puro — sem bibliotecas externas.

import { Calendar } from "tempest-react-sdk";
import { useState } from "react";

const [date, setDate] = useState<Date>();

<Calendar value={date} onChange={setDate} weekStartsOn={1} minDate={new Date(2026, 0, 1)} />;
Prop Tipo Default Descrição
value Date Data selecionada controlada
defaultValue Date Data inicial no caso não-controlado
onChange (date: Date) => void Chamado com a nova data selecionada
month Date Mês visível controlado (qualquer dia dentro dele)
onMonthChange (month: Date) => void Chamado quando o mês visível muda (prev/next)
minDate Date Data mínima selecionável (inclusiva)
maxDate Date Data máxima selecionável (inclusiva)
weekStartsOn 0 \| 1 0 Primeira coluna — 0 domingo, 1 segunda

Teclado

Setas movem o foco por dia (←/→) ou por semana (↑/↓); Enter/Espaço seleciona o dia focado.

Scheduler

Agenda: eventos posicionados numa grade de tempo ao longo de dias consecutivos. O Calendar acima é um seletor de data — responde "qual dia?". Este responde "o que tem nesses dias, e quando", o que exige estrutura diferente: eixo vertical de tempo, evento dimensionado pela duração e eventos sobrepostos lado a lado.

import { Scheduler, type SchedulerEvent } from "tempest-react-sdk";

const events: SchedulerEvent[] = [
  { id: "1", title: "Daily", start: new Date(2026, 6, 27, 9, 0), end: new Date(2026, 6, 27, 9, 15) },
  { id: "2", title: "Cliente", start: new Date(2026, 6, 27, 9, 0), end: new Date(2026, 6, 27, 10, 30) },
  { id: "3", title: "Feriado", start: new Date(2026, 6, 29), end: new Date(2026, 6, 30), allDay: true },
];

<Scheduler
  events={events}
  days={7}
  startHour={7}
  endHour={21}
  onEventClick={(event) => abrir(event.id)}
  onSlotClick={(start) => criarEm(start)}
/>;
Prop Tipo Default Descrição
events SchedulerEvent[] Eventos; instantes lidos no fuso local
anchor Date hoje Qualquer dia dentro do intervalo a mostrar
days number 7 Dias consecutivos — 1 é visão de dia
startHour number 8 Primeira hora visível
endHour number 20 Última hora visível
snapMinutes number 30 Granularidade do clique em espaço vazio
onEventClick (event: SchedulerEvent) => void Evento ativado
onSlotClick (start: Date) => void Clique em espaço vazio, já snapado
renderEvent (event: SchedulerEvent) => ReactNode Conteúdo do evento
locale string "pt-BR" Rótulos de dia e hora
showCurrentTime boolean true Linha de agora
now Date relógio "Agora" fixo — use em teste e demo pra determinismo

Evento: { id, title, start, end, allDay?, data? }.

Sobreposição é o que quase toda implementação erra

Eventos sobrepostos são agrupados em clusters de sobreposição mútua — uma cadeia onde cada evento sobrepõe ao menos um outro — e todos no cluster compartilham a mesma contagem de colunas. É isso que faz as larguras baterem; atribuir coluna par a par produz o layout esfarrapado onde dois eventos ocupam metade cada e um terceiro cobre um deles silenciosamente.

A coluna é reaproveitada assim que libera: 9–10, 9–10, 10–11 usa duas colunas, não três. E encostar não é sobrepor — 9–10 seguido de 10–11 ficam os dois com largura cheia.

O layout é puro e mora em scheduler-layout.ts, com teste próprio.

Horário local, e DST não duplica dia

start/end são instantes lidos no fuso do navegador. O intervalo de dias é montado incrementando o dia do calendário, não somando 24 h em milissegundos: num limite de horário de verão o dia tem 23 ou 25 horas, e a aritmética de milissegundo produziria data duplicada ou pulada.

Evento cruzando meia-noite aparece nas duas colunas

Uma reserva 23:00–01:00 é dividida em dois segmentos, cada um clipado à janela visível do seu dia. Sem isso ela ou desaparece ou é desenhada fora da coluna.

Dia inteiro tem faixa própria

Evento com allDay sai numa faixa acima da grade, atravessando os dias que cobre — posição vertical não significaria nada pra ele. A faixa não é renderizada quando não há nenhum.

Clique em espaço vazio cria; clique no evento não

O onSlotClick só dispara quando o clique caiu na coluna, não num evento dentro dela — o instante vem snapado em snapMinutes e clampado à janela.

Não é role="grid"

Uma grade ARIA exige filhos row, e aqui os eventos são irmãos das colunas dentro de um único CSS grid: um wrapper row faria as colunas deixarem de ser itens do grid e o layout colapsaria. Cada dia é um group rotulado — o leitor de tela tabula os botões de evento e o nome do grupo dá o dia. Verificado com axe.

Recap

  • Layout & UX: ScrollArea para rolagem estilizada, Resizable para painéis divididos e Calendar para seleção de datas sem dependências externas.
  • Todos seguem os mesmos padrões controlado/não-controlado, expõem A11y por teclado e importam de tempest-react-sdk.