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:
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 dotempest_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_buttone 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:
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:
- o
semanticsque você passou; - 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:
É 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:
keyenamesão obrigatórios para isso funcionar: okeyé o que o cliente usa para reportar, e onameé o que chega emevent.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ãostr | 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(+erroropcional). LoginFormé um formulário inteiro numa chamada; você só liga ao seu estado.- Validadores (
validate_email,validate_phone, …) devolvemNonequando OK. on_validatenumFormFieldvalida ao sair do campo;on_completenumPinInputdispara quando o código fica completo.- Os componentes do core (incl.
BarChart/LineChartviaCanvas) renderizam igual no Modo A (WASM) e no Modo B (servidor).
Referência de API
Assinatura de cada componente: tempestweb.components.