2. Estilo¶
O estilo é inline e tipado — sem folhas de estilo, sem cascata, sem
especificidade. Um Style é um objeto Pydantic que cada renderizador traduz para
o seu alvo (CSS no web, propriedades Qt/Compose no nativo).
from tempest_core import Container, Style, Text, Widget
from tempest_core import Color, Edge
def cartao() -> Widget:
return Container(
key="cartao",
style=Style(
padding=Edge.all(16), # (1)!
gap=8.0,
background=Color.from_hex("#f5f5f5"), # (2)!
radius=12.0,
),
children=[Text(content="Num cartão", key="t")],
)
Edge.all(16)= 16px nos quatro lados. Há tambémEdge.symmetric(vertical=…, horizontal=…)eEdge(top=…, right=…, …).Color.from_hex("#f5f5f5")→Color(r, g, b, a). No web isso virargba(...); o valor cruza a fronteira como{r, g, b, a}.
Todo número é finito¶
Um campo numérico de Style — e de qualquer widget — recusa nan, inf e
-inf:
from tempest_core import Style
Style(width=float("nan"))
# ValidationError: width — Input should be a finite number
Parece severo para um valor que ninguém digita de propósito. Mas ninguém digita: ele chega, de dado de fora.
metricas = await backend.get("/metrics") # {"carga_pct": "NaN"}
Style(width=float(metricas["carga_pct"])) # float("NaN") == nan
Uma divisão por zero, um sensor sem leitura, um campo que o backend serializou
como a string "NaN" — e o nan entra na árvore sem nenhum sinal.
Por que isso não pode passar
nan e inf não têm token em JSON, e todo renderizador que este core
alimenta é alcançado por JSON. O encoder do Python escreve as palavras cruas
NaN/Infinity, e nenhum JSON.parse de browser aceita.
O estrago não é a propriedade errada: é o lote inteiro que a carrega. Um
nan num width derruba o batch de patches que ia junto — inclusive as
mudanças de widgets que não têm nada a ver com ele.
Foi medido na issue #160 do tempestweb: uma métrica que chegou como "NaN"
matou o lote dentro da decodificação do cliente — antes do transporte, antes
do renderizador, antes de qualquer diagnóstico —, e o erro visível apareceu
um rebuild depois, como patch path out of range, num app bar cuja
segunda ação simplesmente nunca tinha sido entregue. Em 3 de 7 reproduções
não houve nenhuma linha de console.
Limite não substitui finitude
Style.opacity (ge=0.0, le=1.0) já recusava inf — mas só porque
inf <= 1.0 é falso. Um limite de um lado só não segura: text_scale e
aspect_ratio têm gt=0.0, e inf > 0.0 é verdadeiro. Por isso a guarda é
de finitude, não de faixa.
Recusar na construção é o ponto: o ValidationError nomeia o campo na linha que
montou o widget. Valide onde o número entra:
import math
from tempest_core import Style
def largura_da_barra(bruto: str) -> Style:
"""Converte o número do backend, caindo para 0 quando não é finito."""
carga = float(bruto)
return Style(width=carga if math.isfinite(carga) else 0.0)
print(largura_da_barra("42.5").width) # 42.5
print(largura_da_barra("NaN").width) # 0.0
Animação implícita¶
Declare uma Transition e a mudança de propriedades é animada em vez de saltar:
from tempest_core import Curve, Transition
Style(transition=Transition(duration_ms=300, curve=Curve.EASE_IN_OUT))
Tema = valores de Style
Não há motor de tema mágico: um tema é só um conjunto de Color/Style que a
sua view aplica. Trocar de tema é a view produzindo Styles diferentes.
Escape hatch de HTML (tag / attrs)¶
Todo Widget carrega dois campos opcionais de "dica de renderizador" — tag e
attrs — para o renderizador HTML/SSR (tempestweb). Eles são ignorados pelos
renderizadores nativos (Qt/Compose), assim como semantics/focusable já são.
from tempest_core import Container, Text, Widget
def barra_de_navegacao() -> Widget:
return Container(
key="nav",
tag="nav", # (1)!
attrs={"id": "topo", "aria-label": "Principal"}, # (2)!
children=[
Text(
content="Início",
tag="a",
attrs={"href": "/", "hx-get": "/", "hx-target": "#main"}, # (3)!
),
],
)
tagsobrescreve o elemento HTML semântico emitido (<nav>em vez do<div>padrão);Nonedeixa o renderizador escolher o elemento natural.attrsé umdict[str, str]de atributos HTML arbitrários (id,class,data-*,aria-*).- É por aqui que atributos HTMX (
hx-*) entram na saída SSR sem precisar de um campo dedicado no core.
Por que um escape hatch tipado
O core é agnóstico de renderizador; ele não modela toda tag/atributo HTML.
tag/attrs fluem pelo build() para as props do nó (como qualquer outro
campo), então o renderizador HTML os consome e os demais os ignoram — sem
caso especial.
Recapitulando¶
Styleé tipado e inline; sem cascata CSS.Color.from_hex,Edge.all/symmetric,Transitioncobrem o dia a dia.- Cada renderizador traduz o mesmo
Stylepara o seu alvo. - Todo número é finito:
nan/infsão recusados na construção, porque JSON não os representa e um só deles derruba o lote de patches inteiro. - Veja a referência da API para tudo.