Brazilian forms & media pickers¶
A Brazilian sign-up form is always the same story: e-mail, password, phone, CPF
or CNPJ, address — and, almost always, a photo and a document to attach.
tempest-core ships those fields ready-made, labelled and masked, plus the
three media pickers. Each is a Component that lowers to the primitive
widgets (Input / MaskedInput / FilePicker / Image), so it behaves
identically in both renderers (Qt and Compose) with no renderer change. 🚀
What you'll learn here
- The six labelled Brazilian fields and which mask each one applies.
- Why every masked/validated field pairs with a validator from
tempest_core.validators— the mask formats, the validator checks. - How
AddressInputgroups seven fields under a singleon_change. - The three media pickers and why
button_labelis localisable. - Why
ImagePictureis the circular profile-photo picker, not anAvatar.
Brazilian fields¶
The six fields share the same base (_BRField): each takes a controlled value,
a label above, a placeholder, an error line below (in the theme's error
color) and an on_change that receives the new string — you never touch the
event object. All of them also thread field_variant / size / color_scheme /
theme / media into the inner input, which resolves its Material 3 look against
the theme (dark mode + brand color for free).
The mask formats; the validator checks
Masking and validation are different jobs. MaskedInput only formats the
digits as the user types — it does not guarantee the CPF is valid. For that,
pair each field with the sibling validator from
tempest_core.validators: validate_email,
validate_phone, validate_cpf, validate_cnpj. The mask is UX; the
validator is business rule.
EmailInput¶
A labelled e-mail field, with the e-mail keyboard, a mail icon and the
built-in EMAIL_PATTERN. In the minimal case you pass only on_change:
from tempest_core import EmailInput
email = EmailInput(
value="",
on_change=lambda v: app.set_state(email=v), # (1)!
)
- The handler receives the new string directly — not an event. It may be
sync or
async.
Validate with validate_email from tempest_core.validators,
and surface the message by feeding it back into error:
from tempest_core import EmailInput
from tempest_core import validate_email
def on_email(value: str) -> None:
error = "" if validate_email(value) else "Invalid e-mail"
app.set_state(email=value, email_error=error)
email = EmailInput(
value=app.state.email,
label="Your e-mail",
placeholder="you@example.com",
error=app.state.email_error,
on_change=on_email,
)
Props¶
| Prop | Type | Default | What it does |
|---|---|---|---|
value |
str |
"" |
The current value (controlled). |
label |
str |
"E-mail" |
The label shown above the field (omitted when empty). |
placeholder |
str |
"" |
The empty-field hint. |
error |
str |
"" |
The validation message; in the theme's error color. |
on_change |
Callable[[str], Any] |
(required) | Called with the new string on each edit. |
field_variant |
FieldVariant |
OUTLINE |
The inner input treatment (outline / filled / flushed). |
size |
ResponsiveSize |
MD |
The density — a single Size or a per-breakpoint map. |
color_scheme |
str |
"primary" |
The M3 role family the field's focus paints with. |
theme |
Theme |
Theme() |
The theme resolving the field + label/error colors. |
media |
MediaQueryData \| None |
None |
Viewport snapshot for a responsive size. |
The keyboard and the icon come pre-set
EmailInput pins keyboard=EMAIL, leading_icon="mail" and
pattern=EMAIL_PATTERN on the inner input — you don't (and can't) swap them
via the component API. Those three defaults are exactly why an EmailInput
exists instead of a raw Input.
PasswordInput¶
A labelled password field: secure (masked text), with a lock icon and the
built-in eye toggle to show/hide. Both label and placeholder default to
"Senha":
from tempest_core import PasswordInput
password = PasswordInput(
value=app.state.password,
error=app.state.password_error,
on_change=lambda v: app.set_state(password=v),
)
Props¶
| Prop | Type | Default | What it does |
|---|---|---|---|
value |
str |
"" |
The current value (controlled). |
label |
str |
"Senha" |
The label shown above the field (omitted when empty). |
placeholder |
str |
"Senha" |
The empty-field hint. |
error |
str |
"" |
The validation message; in the theme's error color. |
on_change |
Callable[[str], Any] |
(required) | Called with the new string on each edit. |
field_variant |
FieldVariant |
OUTLINE |
The inner input treatment. |
size |
ResponsiveSize |
MD |
The density. |
color_scheme |
str |
"primary" |
The M3 role family. |
theme |
Theme |
Theme() |
The theme resolving the field + label/error colors. |
media |
MediaQueryData \| None |
None |
Viewport snapshot for a responsive size. |
Secure by construction
PasswordInput pins secure=True and leading_icon="lock" on the inner
input. The eye toggle comes from the secure Input itself — you don't wire
anything for the user to reveal the password.
PhoneInput¶
A Brazilian phone field, masked (99) 99999-9999, with the phone number
keyboard. Each 9 is a digit; parentheses, space and dash are inserted
automatically as the user types:
from tempest_core import PhoneInput
from tempest_core import validate_phone
def on_phone(value: str) -> None:
error = "" if validate_phone(value) else "Invalid phone"
app.set_state(phone=value, phone_error=error)
phone = PhoneInput(
value=app.state.phone,
error=app.state.phone_error,
on_change=on_phone,
)
Props¶
| Prop | Type | Default | What it does |
|---|---|---|---|
value |
str |
"" |
The current value (controlled). |
label |
str |
"Telefone" |
The label shown above the field (omitted when empty). |
placeholder |
str |
"" |
The empty-field hint. |
error |
str |
"" |
The validation message; in the theme's error color. |
on_change |
Callable[[str], Any] |
(required) | Called with the new string on each edit. |
field_variant |
FieldVariant |
OUTLINE |
The inner input treatment. |
size |
ResponsiveSize |
MD |
The density. |
color_scheme |
str |
"primary" |
The M3 role family. |
theme |
Theme |
Theme() |
The theme resolving the field + label/error colors. |
media |
MediaQueryData \| None |
None |
Viewport snapshot for a responsive size. |
The (99) 99999-9999 mask covers the 9-digit mobile
The mask is fixed and designed for the Brazilian mobile number (area code +
nine digits). on_change reports the value as it sits in the field (with
the mask) — use validate_phone to check it, and if you need the raw number,
strip the mask in your handler before persisting.
CPFInput¶
A labelled CPF field, masked 999.999.999-99, numeric keyboard. Pair it with
validate_cpf (which checks the verifier digits, not just the format):
from tempest_core import CPFInput
from tempest_core import validate_cpf
def on_cpf(value: str) -> None:
error = "" if validate_cpf(value) else "Invalid CPF"
app.set_state(cpf=value, cpf_error=error)
cpf = CPFInput(
value=app.state.cpf,
error=app.state.cpf_error,
on_change=on_cpf,
)
Props¶
| Prop | Type | Default | What it does |
|---|---|---|---|
value |
str |
"" |
The current value (controlled). |
label |
str |
"CPF" |
The label shown above the field (omitted when empty). |
placeholder |
str |
"" |
The empty-field hint. |
error |
str |
"" |
The validation message; in the theme's error color. |
on_change |
Callable[[str], Any] |
(required) | Called with the new string on each edit. |
field_variant |
FieldVariant |
OUTLINE |
The inner input treatment. |
size |
ResponsiveSize |
MD |
The density. |
color_scheme |
str |
"primary" |
The M3 role family. |
theme |
Theme |
Theme() |
The theme resolving the field + label/error colors. |
media |
MediaQueryData \| None |
None |
Viewport snapshot for a responsive size. |
A mask is not CPF validation
999.999.999-99 only formats eleven digits — 111.111.111-11 passes the mask
and is an invalid CPF. Always pair CPFInput with validate_cpf, which
runs the verifier-digit algorithm. Trusting the mask alone lets fake CPFs into
your database.
CNPJInput¶
A labelled CNPJ field, masked 99.999.999/9999-99, numeric keyboard. Pair it
with validate_cnpj:
from tempest_core import CNPJInput
from tempest_core import validate_cnpj
def on_cnpj(value: str) -> None:
error = "" if validate_cnpj(value) else "Invalid CNPJ"
app.set_state(cnpj=value, cnpj_error=error)
cnpj = CNPJInput(
value=app.state.cnpj,
error=app.state.cnpj_error,
on_change=on_cnpj,
)
Props¶
| Prop | Type | Default | What it does |
|---|---|---|---|
value |
str |
"" |
The current value (controlled). |
label |
str |
"CNPJ" |
The label shown above the field (omitted when empty). |
placeholder |
str |
"" |
The empty-field hint. |
error |
str |
"" |
The validation message; in the theme's error color. |
on_change |
Callable[[str], Any] |
(required) | Called with the new string on each edit. |
field_variant |
FieldVariant |
OUTLINE |
The inner input treatment. |
size |
ResponsiveSize |
MD |
The density. |
color_scheme |
str |
"primary" |
The M3 role family. |
theme |
Theme |
Theme() |
The theme resolving the field + label/error colors. |
media |
MediaQueryData \| None |
None |
Viewport snapshot for a responsive size. |
Same goes for the CNPJ
The 99.999.999/9999-99 mask formats fourteen digits but doesn't check the
verifiers. Always pair it with validate_cnpj.
AddressInput¶
A grouped Brazilian address block: instead of seven loose components,
AddressInput renders a labelled Column with CEP (masked 99999-999), street,
number, complement, neighborhood, city and UF all at once. A single
on_change is called as on_change(field_name, new_value), where field_name
is one of "cep", "street", "number", "complement", "neighborhood",
"city" or "state":
from tempest_core import AddressInput
def on_address(field_name: str, value: str) -> None: # (1)!
app.set_state(**{field_name: value})
address = AddressInput(
cep=app.state.cep,
street=app.state.street,
number=app.state.number,
complement=app.state.complement,
neighborhood=app.state.neighborhood,
city=app.state.city,
state=app.state.state,
on_change=on_address,
)
- Note the two-argument signature:
AddressInputtells you which field changed, so a single handler updates the right field's state.
Props¶
| Prop | Type | Default | What it does |
|---|---|---|---|
cep |
str |
"" |
The current postal code (masked 99999-999). |
street |
str |
"" |
The current street value. |
number |
str |
"" |
The current house/building number. |
complement |
str |
"" |
The current address complement. |
neighborhood |
str |
"" |
The current neighborhood value. |
city |
str |
"" |
The current city value. |
state |
str |
"" |
The current state (UF) value. |
label |
str |
"Endereço" |
The block heading (omitted when empty). |
on_change |
Callable[[str, str], Any] |
(required) | Called as on_change(field_name, new_value) on each edit. |
field_variant |
FieldVariant |
OUTLINE |
The inner inputs' treatment. |
size |
ResponsiveSize |
MD |
The inputs' density. |
color_scheme |
str |
"primary" |
The M3 role family. |
theme |
Theme |
Theme() |
The theme resolving the fields + heading color. |
media |
MediaQueryData \| None |
None |
Viewport snapshot for a responsive size. |
One handler, many fields
Unlike the simple fields (which call on_change(value)), AddressInput calls
on_change(field_name, value). It's the only BR field with this two-argument
signature — it exists because the block gathers seven inputs under a single
handler. Only the CEP is masked; street, number, complement, neighborhood,
city and UF are plain text inputs.
Media pickers¶
The three pickers lower to FilePicker (and, for the ones that preview, to
Image). All expose an on_pick that receives the picked file's URI — never
the event object. on_pick may be async: the returned value is forwarded to the
dispatcher and awaited, so loading/analyzing the picked file inside the handler
works without stranding a coroutine.
ImagePicker¶
A labelled image picker with an inline preview: as soon as a URI is chosen it shows the image (160×160, rounded corners) above the button:
from tempest_core import ImagePicker
photo = ImagePicker(
value=app.state.photo_uri,
label="Product photo",
button_label="Choose image", # (1)!
on_pick=lambda uri: app.set_state(photo_uri=uri),
)
button_labelis localisable — pass the text in the app's language. The default"Choose image"is English on purpose, to force the locale decision.
Props¶
| Prop | Type | Default | What it does |
|---|---|---|---|
value |
str |
"" |
The picked image URI (empty until one is chosen). When set, a preview appears. |
label |
str |
"" |
An optional heading above the picker (omitted when empty). |
button_label |
str |
"Choose image" |
The picker button's caption (localise it per app locale). |
on_pick |
Callable[[str], Any] |
(required) | Called with the picked image URI. |
Localise the button_label
Both ImagePicker and DocumentPicker expose button_label precisely so you
can translate the button caption. The default is English ("Choose image" /
"Choose document") — swap it for the app's locale string, like
"Escolher imagem".
DocumentPicker¶
A labelled document picker. Like ImagePicker, but without a preview — a
document isn't an image to render:
from tempest_core import DocumentPicker
attachment = DocumentPicker(
value=app.state.doc_uri,
label="Proof of address",
button_label="Choose document",
on_pick=lambda uri: app.set_state(doc_uri=uri),
)
Props¶
| Prop | Type | Default | What it does |
|---|---|---|---|
value |
str |
"" |
The picked document URI (empty until one is chosen). |
label |
str |
"" |
An optional heading above the picker (omitted when empty). |
button_label |
str |
"Choose document" |
The picker button's caption (localise it per app locale). |
on_pick |
Callable[[str], Any] |
(required) | Called with the picked document URI. |
ImagePicture¶
The circular profile-photo picker: a round photo over a change affordance.
Unlike Avatar (which shows initials), ImagePicture clips the chosen image to a
circle and, with no photo, falls back to a user icon placeholder. size is
the circle's diameter in logical pixels:
from tempest_core import ImagePicture
profile = ImagePicture(
src=app.state.avatar_uri,
size=96.0,
on_pick=lambda uri: app.set_state(avatar_uri=uri),
)
Props¶
| Prop | Type | Default | What it does |
|---|---|---|---|
src |
str |
"" |
The current photo URI (empty shows the placeholder). |
size |
float |
96.0 |
The circle's diameter in logical pixels. |
on_pick |
Callable[[str], Any] |
(required) | Called with the picked photo URI. |
ImagePicture picks; Avatar displays
Both are round, but they solve different problems. Avatar displays an
identity (photo or initials) and doesn't let you change it. ImagePicture
collects a photo: circular clip when src is set, a user icon when not,
and always a "change" FilePicker below. Use ImagePicture on the
profile-edit screen; use Avatar anywhere that only shows.
Recap¶
- Six labelled Brazilian fields:
EmailInput,PasswordInput,PhoneInput,CPFInput,CNPJInputand the groupedAddressInput— all with a controlledvalue, alabel, anerrorandon_change(value). - Mask formats, validator checks: pair the masked/validated fields with
validate_email/validate_phone/validate_cpf/validate_cnpjfromtempest_core.validators. The mask is UX; validation is the rule. AddressInputis grouped: seven fields (masked CEP + six text) under a singleon_change(field_name, new_value).- Three media pickers:
ImagePicker(with inline preview),DocumentPicker(no preview) andImagePicture(circular profile photo). All takeon_pick(uri), which acceptsasync. button_labelis localisable onImagePicker/DocumentPicker— the English default exists to force per-locale translation.ImagePicturecollects,Avatardisplays: the circular picker clips the photo to a circle with auser-icon fallback;Avataronly shows.