Ir para o conteúdo

Componentes prontos

Você não precisa montar um formulário de login campo a campo. O tempestweb traz componentes prontos, validados e bonitos por padrão — você escreve o mínimo, o componente cuida do resto. 🚀

Tudo vem de um lugar óbvio:

from tempestweb.components import EmailField, PasswordField, LoginForm, validate_email

De onde vêm os componentes

tempestweb.components reúne duas origens num único import:

  • Do tempest-core — o catálogo Material 3 (scaffolds, app bars, navegação, cards, tabelas, gráficos, etc.). O tempestweb reexporta essas classes sem reimplementá-las: cada nome é a própria classe do tempest_core.components, então comportamento e tipagem batem com o core.
  • Nativos do tempestweb — os ajudantes de mais alto nível construídos aqui: os campos (EmailField/PasswordField/TextField + os campos BR), os formulários (LoginForm/SignupForm) e os construtores de botão MD3 (filled_button e amigos).

O catálogo transparente mais abaixo lista cada item e de onde ele vem. As primitivas (Column, Row, Text, Button, Container, Input…) você importa direto de tempest_core (from tempest_core import Column, Row, Text).

Campos prontos

Cada campo é controlado: você passa o value atual e um on_change que guarda o novo texto; passe error para mostrar uma mensagem de validação.

from tempest_core import App, Column, Widget
from tempestweb.components import EmailField


def view(app: App[State]) -> Widget:
    def set_email(value: str) -> None:
        app.set_state(lambda s: setattr(s, "email", value))

    return Column(
        children=[
            EmailField(
                value=app.state.email,
                on_change=set_email,
                error=app.state.email_error,  # "" quando válido
                key="email",
            ),
        ],
    )

Os campos disponíveis:

Campo Para quê Validador
EmailField E-mail (teclado de e-mail, ícone) validate_email
PasswordField Senha (campo seguro)
PhoneField Telefone BR mascarado (99) 99999-9999 validate_phone
CPFField CPF mascarado validate_cpf
CNPJField CNPJ mascarado validate_cnpj
AddressField Endereço

Validadores devolvem None quando OK

validate_email("você@exemplo.com") devolve None; um valor inválido devolve a mensagem de erro (string). Guarde essa string no error do campo:

error = validate_email(app.state.email) or ""

Quem dá o nome ao campo

Um campo desce para uma Column — uma <div> sem role — em volta de um Input, e a legenda é um Text irmão, não um <label for=…>. Nada associa os dois: até a 0.113.0 o controle era nomeado pelo que o placeholder por acaso dizia. Um PasswordField não tem placeholder default, então ele era um controle anônimo — e o LoginForm entregava essa violação (axe label, crítico) para todo app que o usava.

Hoje o campo sempre nomeia o controle, nesta ordem:

  1. o semantics que você passou;
  2. a legenda visível.
from tempest_core import App, Column, Row, Semantics, Text, Widget
from tempestweb.components import TextField


def view(app: App[State]) -> Widget:
    """Render one header row over caption-less, named cells."""

    def set_quantity(value: str) -> None:
        app.set_state(lambda s: setattr(s, "quantity", value))

    return Column(
        children=[
            Row(children=[Text(content="Qtd."), Text(content="R$ unit.")]),
            Row(
                children=[
                    TextField(
                        value=app.state.quantity,
                        label="",
                        semantics=Semantics(label="Quantidade do item 3"),
                        on_change=set_quantity,
                        key="i3-quantity",
                    ),
                ],
            ),
        ],
    )

O nome vai para o <input>, não para a coluna em volta dele:

<div data-tw-key="i3-quantity">
  <input aria-label="Quantidade do item 3" value="1.200" />
</div>

É isso que libera a grade com uma linha de cabeçalho

Uma grade densa não repete a legenda em cada célula — vinte itens dividem as mesmas oito colunas. Sem semantics, um campo sem legenda visível não tem nome acessível nenhum, e quem usa leitor de tela ouve "caixa de edição" vinte vezes.

Não é onde o aria-label cairia sozinho

aria-label numa <div> sem role é atributo proibido (aria-prohibited-attr) e nomeia um elemento em que nenhum leitor para: o leitor para no controle dentro dela, que continuaria anônimo (label, crítico). Por isso o campo escolhe o destino em vez de repassar cru.

Legenda e semantics juntos

O semantics ganha — é o que o app disse explicitamente. Mantenha o texto da legenda dentro dele (WCAG 2.5.3, Label in Name): Semantics(label="Quantidade do item 3") sobre a legenda Qtd. é bom; um nome que não contém a legenda deixa quem usa comando de voz sem como chamar o campo.

Vale para TextField, EmailField e PasswordField; LoginForm e SignupForm levam o semantics para a raiz do formulário, que é o lugar de um role="form".

Formulário de login completo

LoginForm compõe e-mail + senha + botão de envio em uma chamada. Você só mantém os valores no estado; o formulário cuida do layout, dos rótulos e dos erros.

from dataclasses import dataclass

from tempest_core import App, Column, Text, Widget
from tempestweb.components import LoginForm, validate_email


@dataclass
class LoginState:
    email: str = ""
    password: str = ""
    email_error: str = ""
    status: str = ""


def make_state() -> LoginState:
    return LoginState()


def view(app: App[LoginState]) -> Widget:
    def set_email(value: str) -> None:
        app.set_state(lambda s: setattr(s, "email", value))

    def set_password(value: str) -> None:
        app.set_state(lambda s: setattr(s, "password", value))

    def submit() -> None:
        error = validate_email(app.state.email) or ""

        def commit(s: LoginState) -> None:
            s.email_error = error
            s.status = "" if error else f"Bem-vindo, {s.email}!"

        app.set_state(commit)

    return Column(
        children=[
            LoginForm(
                email=app.state.email,
                password=app.state.password,
                on_email_change=set_email,
                on_password_change=set_password,
                on_submit=submit,
                email_error=app.state.email_error,
                title="Entrar",
            ),
            Text(content=app.state.status, key="status"),
        ],
    )

É isso. Esse é o exemplo examples/login_demo — roda igual nos dois modos:

tempestweb dev --mode wasm     # Python no browser (Pyodide)
tempestweb dev --mode server   # Python no servidor (FastAPI + WebSocket)

Por que controlado?

O estado vive no App (uma fonte de verdade), então o formulário não guarda estado escondido. Você sempre sabe o que tem nos campos — e pode pré-preencher, limpar ou validar de fora quando quiser.

Cadastro

SignupForm segue a mesma ideia, com e-mail + senha + confirmação de senha. Mostre o erro de confirmação quando as senhas diferem:

from tempestweb.components import SignupForm

SignupForm(
    email=app.state.email,
    password=app.state.password,
    confirm=app.state.confirm,
    on_email_change=set_email,
    on_password_change=set_password,
    on_confirm_change=set_confirm,
    on_submit=do_signup,
    confirm_error="" if app.state.password == app.state.confirm else "As senhas não conferem",
    title="Criar conta",
)

Catálogo transparente

Aqui está o que é usado e de onde vem. Tudo é importado de tempestweb.components; a coluna Origem diz se o nome é nativo do tempestweb ou reexportado do tempest-core.

Nativos do tempestweb

Construídos neste pacote (tempestweb/components/{fields,forms,buttons}.py):

Nome O que faz Origem
EmailField · PasswordField · TextField Campos controlados estilizados para o Material 3 (label + erro embutidos). tempestweb
PhoneField · CPFField · CNPJField · AddressField Campos BR: envolvem os inputs mascarados do core com validação. tempestweb
validate_email · validate_phone · validate_cpf · validate_cnpj Validadores; devolvem None quando OK, senão a mensagem de erro. tempestweb
LoginForm · SignupForm Formulários completos numa chamada (campos + botão + erros). tempestweb
filled_button · tonal_button · elevated_button · outlined_button · text_button Construtores das 5 variantes de botão MD3. tempestweb

Reexportados do tempest-core

O catálogo Material 3 do core, reexportado sem reimplementação. Agrupado por função:

Grupo Componentes Origem
Layout Grid · HStack · VStack · StyledContainer · Surface · Scaffold · Divider · Header · Footer · Sidebar tempest-core
Navegação AppBar · CollapsingAppBar · NavBar · Drawer · Burger · Breadcrumb · Tabs · SegmentedControl · Stepper · ProgressStepper tempest-core
Exibição de dados Card · ListTile · Table · TableRow · TableCell · DataTable · Avatar · Chip · Tag · Badge · Stat · StatCard · MetricCard · Rating · Clock · Calendar tempest-core
Feedback Alert · Banner · EmptyState tempest-core
Divulgação Accordion tempest-core
Inputs (baixo nível) EmailInput · PasswordInput · PhoneInput · AddressInput · CPFInput · CNPJInput · RadioGroup · SearchBar tempest-core
Mídia ImagePicker · ImagePicture · DocumentPicker tempest-core
Gráficos BarChart · LineChart · ChartSeries tempest-core
Visão DetectionBox · DetectionOverlay · ConfidenceBadge · ResultView · confidence_scheme tempest-core

Duas instâncias do mesmo componente convivem — desde tempest-core 0.15.0

Todo componente agora deriva a chave de cada filho da própria: a base é o key que você passou (ou o nome do componente, quando você não passa), e cada filho vira <base>-<papel>. Dois SegmentedControl na mesma tela emitem filtro-item-0 e ordem-item-0, não seg-0 duas vezes — e como o evento é roteado por chave, o clique chega no controle certo.

SegmentedControl(key="filtro", options=["Tudo", "Ativos"], on_select=set_filtro)
SegmentedControl(key="ordem", options=["A-Z", "Z-A"], on_select=set_ordem)

Vale para SegmentedControl, RadioGroup, Rating, NavBar, Breadcrumb, Stepper, SearchBar, Tabs, Accordion, Card, Grid e os campos — nos três modos.

Dois sem key ainda colidem

Sem key, a base é o nome do componente (segmented), então duas instâncias anônimas voltam a disputar o nome. Dê key= a cada uma quando houver mais de uma na tela — é uma linha, e é o que o namespacing usa.

Campo vs Input

O par existe de propósito: o *Input (do core) é a primitiva de baixo nível; o *Field (nativo do tempestweb, para e-mail/senha/BR) é o campo pronto — label, erro e teclado já ligados. Prefira o *Field no dia a dia; use o *Input quando quiser montar o layout você mesmo.

Exemplo com um componente do core:

from tempestweb.components import Card, DataTable, BarChart, ChartSeries

BarChart(series=[ChartSeries(points=[3.0, 7.0, 2.0, 9.0, 5.0], label="vendas")])

Gráficos desenham via Canvas — nos dois modos

BarChart/LineChart (e overlays de detecção como DetectionOverlay) baixam para um widget Canvas com uma lista de comandos de desenho. O cliente web executa esses comandos num <canvas> real — eixos, gridlines, barras e linhas desenham de verdade, sem biblioteca de gráficos, igual no Modo A e no Modo B. Os modelos de dados que alimentam os componentes (ChartSeries, TableRow/TableCell, DetectionBox) vêm junto.

Overlays de visão pareiam com o [vision]

DetectionOverlay/DetectionBox/ConfidenceBadge/ResultView desenham as saídas de um modelo — combinam com a inferência no cliente da Visão computacional (ONNX).

Validar quando o leitor sai do campo

Um formulário que só valida no submit conta a verdade tarde: o leitor descobre que o e-mail está errado depois de preencher seis campos. FormField declara on_validate para isso — o cliente reporta a ocasião (este campo, este valor, confira agora) quando o controle perde o foco, e o handler roda os validadores de verdade:

from tempest_core import FormField, Input, Validator
from tempest_core import ValidationEvent

rules: dict[str, list[Validator]] = {"email": [_require("Email é obrigatório")]}


def validate_field(event: ValidationEvent) -> None:
    """Run one field's rules when the reader leaves it."""
    message = ""
    for rule in rules.get(event.field, []):
        failed = rule(event.value)
        if failed is not None:
            message = failed
            break
    app.set_state(lambda state: state.errors.update({event.field: message}))


FormField(
    key="field-email",
    name="email",
    label="E-mail",
    validators=rules["email"],
    error=app.state.errors.get("email", ""),
    on_validate=validate_field,
    child=Input(key="email-input", value=app.state.email, on_change=edit_email),
)

Três coisas que valem saber:

  • key e name são obrigatórios para isso funcionar: o key é o que o cliente usa para reportar, e o name é o que chega em event.field.
  • Os validadores não atravessam o fio — são callables Python. É por isso que o cliente reporta a ocasião em vez de tentar validar sozinho.
  • error é str, não str | None: string vazia significa "sem erro", e é isso que apaga a mensagem.

O erro é desenhado pela folha base sob o controle, e o campo ganha aria-invalid, então a mensagem é anunciada e não apenas exibida.

Código de uso único (PinInput)

PinInput é o campo de código: length dígitos, secure para mascarar, e on_complete disparando no instante em que o último dígito entra — sem botão:

from tempest_core import SubmitEvent
from tempest_core import PinInput


def code_completed(event: SubmitEvent) -> None:
    """Accept the code as soon as its last digit lands."""
    app.set_state(lambda state: setattr(state, "code_done", True))


PinInput(
    key="code-input",
    length=4,
    value=app.state.code,
    on_change=edit_code,
    on_complete=code_completed,
)

Ele renderiza como um <input> com autocomplete="one-time-code" e inputmode="numeric", não como quatro caixinhas: assim o browser (e o iOS/Android) oferece preencher o código do SMS, o teclado numérico aparece no celular, e colar o código inteiro funciona — três coisas que caixas separadas jogam fora. A folha base espaça os caracteres para ainda parecer um campo de código.

on_complete dispara na transição

Ele avisa quando o campo fica completo, não a cada tecla depois disso. Apagar e preencher de novo arma o próximo aviso.

Recapitulando

  • Importe de tempestweb.components — campos, formulários e a biblioteca completa do core num lugar só.
  • O catálogo transparente acima lista cada item e sua origem (nativo do tempestweb vs reexportado do tempest-core).
  • Campos são controlados: value + on_change (+ error opcional).
  • LoginForm é um formulário inteiro numa chamada; você só liga ao seu estado.
  • Validadores (validate_email, validate_phone, …) devolvem None quando OK.
  • on_validate num FormField valida ao sair do campo; on_complete num PinInput dispara quando o código fica completo.
  • Os componentes do core (incl. BarChart/LineChart via Canvas) renderizam igual no Modo A (WASM) e no Modo B (servidor).

Referência de API

Assinatura de cada componente: tempestweb.components.