CSS tipado (StyleSheet e tokens)¶
CSS escrito em Python, conferido pelo type checker e servido pelo próprio
app. Sem arquivo .css solto, sem build de frontend, sem CDN.
Quando usar
- Você precisa de seletor, pseudo-classe ou media query — coisas que estilo inline não expressa.
- Você quer paleta, espaçamento e tipografia consistentes, com modo escuro, sem manter uma tabela de cores na mão.
- Você quer que um nome de classe errado falhe, em vez de renderizar um elemento sem estilo.
O Style do tempest_core continua sendo o certo para layout local
de um widget. Esta receita é sobre a folha.
Uma regra é um objeto¶
from tempest_core import Style
from tempest_core.style import Edge
from tempest_fastapi_sdk.ui.css import Media, Rule, StyleSheet
sheet: StyleSheet = StyleSheet(
rules=[
Rule(".card", style=Style(padding=Edge.all(16), radius=8.0)),
Rule(".card:hover", declarations={"cursor": "pointer"}),
Media.min_width(768, [Rule(".card", declarations={"padding": "24px"})]),
],
)
print(sheet.to_css())
Sai exatamente o que você espera:
.card {
padding: 16px 16px 16px 16px;
border-radius: 8px;
}
.card:hover {
cursor: pointer;
}
@media (min-width: 768px) {
.card {
padding: 24px;
}
}
Uma Rule recebe declarações de dois lugares, e a divisão é
intencional:
style=— umStyletipado, convertido pela mesma função que o renderizador usa nos widgets. Regra e estilo inline com os mesmos valores emitem declarações idênticas.declarations=— mapa cru, para o queStylenão modela:display: grid,cursor,content, e toda referência a token.
Vale também layout="column" / "row", que aplica display: flex +
flex-direction do mesmo jeito que Column e Row fazem — assim
gap, justify e align não ficam inertes.
Cor em Style só aceita hexadecimal
Style(color="var(--t-color-primary)") levanta invalid hex color
— medido contra o validador do tempest_core. Referência a token vai
em declarations: Rule(".btn", declarations={"color":
theme.color("primary")}).
Design tokens, do tempest_core para o CSS¶
A paleta não é reinventada aqui: ThemeTokens adapta o TokenSet do
tempest_core (o mesmo que o cliente usa) para custom properties.
from tempest_fastapi_sdk.ui.css import Rule, StyleSheet, ThemeTokens
theme: ThemeTokens = ThemeTokens()
sheet: StyleSheet = StyleSheet(
theme=theme,
rules=[
Rule(
".card",
declarations={
"padding": theme.space("md"),
"border-radius": theme.radius("md"),
"background": theme.color("surface"),
"color": theme.color("on_surface"),
"font-size": theme.font_size("body_medium"),
},
),
],
)
Isso emite três blocos: :root com o esquema claro e todas as escalas,
@media (prefers-color-scheme: dark) (protegido para não vencer uma
escolha explícita por claro) e :root[data-theme="dark"] para o botão de
alternar. Escrever theme.color("surface") uma vez resolve os dois
modos.
Grupos disponíveis: color (39 papéis — primary, on_surface,
error_container, …), space, radius, font-size, line-height,
font-weight, letter-spacing, duration, easing.
Breakpoint é a exceção, e por um motivo: media query não lê var().
Então ele volta como número:
from tempest_fastapi_sdk.ui.css import Media, Rule, ThemeTokens
theme: ThemeTokens = ThemeTokens()
wide = Media.min_width(
theme.breakpoint("lg"),
[Rule(".sidebar", declarations={"display": "block"})],
)
Além de min_width, há max_width, dark() e reduced_motion().
Servindo a folha¶
from fastapi import FastAPI
from tempest_fastapi_sdk.ui import app_stylesheet
from tempest_fastapi_sdk.ui.css import make_css_router
app: FastAPI = FastAPI()
app.include_router(make_css_router(app_stylesheet(), path="/static/app.css"))
O CSS é renderizado uma vez, quando o router é construído — nenhuma
requisição paga o custo de percorrer as regras. A resposta leva um
ETag derivado do conteúdo, e um If-None-Match que bata recebe 304
sem corpo.
Do lado da página, aponte o <link>:
from tempest_core import Text
from tempest_fastapi_sdk.ssr import html_response
response = html_response(
Text(content="Olá", tag="h1"),
title="Início",
stylesheets=["/static/app.css"],
)
A folha pronta, e a sua por cima¶
app_stylesheet() compõe o que quase todo serviço quer: tokens, reset
mínimo, regras de formulário e regras de componente.
from tempest_fastapi_sdk.ui import app_stylesheet
from tempest_fastapi_sdk.ui.css import Rule, StyleSheet, ThemeTokens
theme: ThemeTokens = ThemeTokens()
own: StyleSheet = StyleSheet(
reset=False,
rules=[
Rule(
".page-title",
declarations={
"margin": "0",
"font-size": theme.font_size("headline_small"),
"color": theme.color("on_background"),
},
),
],
)
sheet: StyleSheet = app_stylesheet(theme=theme, extra=own)
As suas regras entram por último, então vencem no empate de
especificidade — a cascata normal. merge() faz o mesmo entre duas
folhas quaisquer, e StyleSheet(reset=False) desliga o reset.
Nome de classe errado deve doer¶
Um typo em class="crad" não quebra nada: o elemento simplesmente
aparece sem estilo, e alguém descobre em produção. StyleSheet.cls()
transforma isso em erro na hora do render:
from tempest_core import Column, Text
from tempest_fastapi_sdk.ui.css import Rule, StyleSheet, cls
sheet: StyleSheet = StyleSheet(rules=[Rule(".card", declarations={"padding": "16px"})])
Column(tag="section", attrs=sheet.cls("card"), children=[Text(content="oi")])
Column(tag="section", attrs=cls("card", "card--wide"), children=[])
sheet.cls("crad") levanta KeyError listando as classes que existem.
A função solta cls() não valida — use quando a classe vier de outra
folha (a de um design system externo, por exemplo).
Dá para levar isso ao teste, sem depender de disciplina:
import re
from tempest_fastapi_sdk.ui import app_stylesheet
def test_page_uses_only_defined_classes(html: str) -> None:
"""Falha se a página usar uma classe que a folha não define."""
used = {
name
for attribute in re.findall(r'class="([^"]+)"', html)
for name in attribute.split()
}
assert used <= app_stylesheet().class_names()
Esse teste existe na suíte do próprio SDK, e achou duas classes sem regra na primeira vez que rodou.
Recap¶
Rule+Media+StyleSheetcobrem seletor, pseudo-classe e media query — o que estilo inline não alcança.style=para valores tipados,declarations=para o resto e para todo token (Stylesó aceita cor hexadecimal).ThemeTokenstraduz o token set dotempest_coreem custom properties, com claro e escuro de uma vez.make_css_routerserve a folha renderizada uma única vez, comETage304.app_stylesheet()já traz tokens, reset, formulários e componentes; suas regras entram porextra=.sheet.cls(...)transforma typo de classe emKeyError.
Veja também: Camada UI » e Formulários a partir de schemas Pydantic ».