Ir para o conteúdo

Modo C — transpile (Python → JavaScript nativo) 🚀

O que você vai aprender

Como transformar o seu app.py Python num bundle estático de JavaScript nativo — sem runtime Python no browser — e servi-lo por qualquer CDN, já com PWA instalável e offline de fábrica.

Os Modos A (WASM) e B (servidor) mantêm o Python vivo em tempo de execução — no browser (Pyodide) ou no servidor. O Modo C faz diferente: um compilador transcreve a camada de app do seu Python tipado para JavaScript nativo. Zero runtime Python, hospedagem estática, first-paint e SEO ótimos. É a "história do TypeScript" para Python. 🚀

Modo de primeira classe

O Modo C é maduro e de primeira classe — cobre 100% dos widgets do tempest_core, um subset amplo de Python tipado (veja O subset suportado) e uma história completa de PWA (instalável, offline, fila de mutações, WebPush ponta a ponta). É a escolha recomendada para sites e PWAs públicos. Só um punhado de construtos avançados fica de fora do subset — o compilador falha cedo, com arquivo:linha, quando você encosta neles.

Por que existe

Modo A (WASM) Modo B (servidor) Modo C (transpile)
Runtime Python browser (~6 MB Pyodide) servidor vivo nenhum
First paint / SEO ruim bom ótimo
Hospedagem estática servidor + WS/cliente estática
Custo de escala zero servidor stateful por cliente zero servidor

O segredo: o cliente JS (dom.js, style.js, events.js) já é nativo e compartilhado pelos três modos. O Modo C não transpila Python inteiro — só a camada de app; todo o renderizador continua sendo o mesmo JS.

Seu primeiro build

Pegue o app do contador (o mesmo que roda nos Modos A/B, sem mudar uma linha):

from dataclasses import dataclass

from tempest_core import App, Button, Column, Row, Style, Text, Widget
from tempest_core import Edge


@dataclass
class CounterState:
    value: int = 0


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


def view(app: App[CounterState]) -> Widget:
    def increment() -> None:
        app.set_state(lambda s: setattr(s, "value", s.value + 1))

    def decrement() -> None:
        app.set_state(lambda s: setattr(s, "value", s.value - 1))

    return Column(
        style=Style(gap=8.0, padding=Edge.all(16)),
        children=[
            Text(content=f"Count: {app.state.value}", key="label"),
            Row(
                style=Style(gap=4.0),
                children=[
                    Button(label="-", on_click=decrement, key="dec"),
                    Button(label="+", on_click=increment, key="inc"),
                ],
            ),
        ],
    )

Gere o bundle estático:

tempestweb build --mode transpile --path examples/counter

Isso escreve um diretório dist/transpile/ totalmente estático — sem Python:

dist/transpile/
├── index.html                     # monta o app via mountApp
└── client/
    ├── tempestweb.js dom.js style.js events.js …   # o cliente compartilhado
    └── transpile/
        ├── app.gen.js             # o seu app.py transcrito para JS nativo
        ├── runtime.js widgets.js diff.js
        └── widget-styles.gen.js   # estilos MD3 resolvidos do core

Sirva com qualquer host estático (ou localmente):

tempestweb dev --mode transpile --path examples/counter

Durante o desenvolvimento, use o loop com livereload — edite o app.py e o browser recarrega com o bundle recompilado:

tempestweb dev --mode transpile --path examples/counter

O que aconteceu

Seu view() virou app.gen.js — JavaScript nativo. O runtime segura o estado, roda view(), faz o diff em JS e aplica patches granulares ao DOM. Nenhum Python é baixado ou executado no browser.

O que o compilador emite

O app.py acima vira, essencialmente:

import { State } from "./runtime.js";
import { Button, Column, Edge, Row, Style, Text } from "./widgets.js";

export class CounterState extends State {
  constructor() {
    super();
    this.value = 0;
  }
}

export function makeState() {
  return new CounterState();
}

export function view(app) {
  const increment = () => {
    app.setState((s) => {
      s.value = (s.value + 1);
    });
  };
  // …
  return Column({
    style: Style({ gap: 8.0, padding: Edge.all(16) }),
    children: [
      Text({ content: `Count: ${app.state.value}`, key: "label" }),
      // …
    ],
  });
}

Convenções de nome

O compilador traduz a API para o JS idiomático: make_statemakeState, set_statesetState, on_clickonClick, color_schemecolorScheme. setattr(s, "x", v) vira s.x = v; f-strings viram template literals.

Estado com métodos

Você não precisa se limitar a lambdas setattr. Um @dataclass com métodos transpila para uma classe JS — self vira this:

@dataclass
class Counter:
    value: int = 0

    def increment(self) -> None:
        self.value += 1


def view(app: App[Counter]) -> Widget:
    def inc() -> None:
        app.set_state(lambda s: s.increment())

    return Button(label="+", on_click=inc, key="inc")

Campos de formulário reativos

Input resolve o estilo Material 3 e conecta o on_change. O binding é de duas vias: digitar dispara o handler, que atualiza o estado e re-renderiza.

from tempest_core import App, Column, Style, Text, Widget
from tempest_core import Edge
from tempest_core import Input


@dataclass
class FormState:
    name: str = ""


def view(app: App[FormState]) -> Widget:
    def on_name(event) -> None:
        app.set_state(lambda s: setattr(s, "name", event.payload["value"]))

    return Column(
        style=Style(gap=12.0, padding=Edge.all(24)),
        children=[
            Text(content=f"Hello, {app.state.name or 'stranger'}!", key="greet"),
            Input(value=app.state.name, placeholder="Your name", on_change=on_name, key="name"),
        ],
    )

Digite no campo e a saudação atualiza ao vivo — sem servidor, sem Python. ✨

Capacidades nativas (requests, storage, cookies…)

O mesmo native tipado dos Modos A/B funciona no Modo C — chamadas async transcritas para chamadas JS em processo à glue de browser compartilhada (fetch, IndexedDB/localStorage, document.cookie). Zero Python, zero rede.

As três formas de import que o Python escreve chegam no mesmo lugar:

from tempestweb import native                          # o namespace
from tempestweb.native import storage, get_position    # grupo e função
from tempestweb.native.geolocation import get_position  # o grupo como módulo

Capacidade que o Modo C não tem, dita na hora do build

camera não tem fachada em processo — camera.capture precisa do Modo A (Pyodide) ou B (servidor). Importá-la em Modo C é erro de compilação com arquivo:linha dizendo isso, e não uma página que carrega e quebra ao clicar. A lista viva do que a fachada serve é gerada do próprio client/transpile/native.js.

from tempestweb import native


@dataclass
class DataState:
    body: str = ""


def view(app: App[DataState]) -> Widget:
    async def fetch_it() -> None:
        res = await native.http.request("GET", "/api/items")
        await native.storage.put("last", res.body)
        await native.cookies.set("seen", "1")
        app.set_state(lambda s: setattr(s, "body", res.body))

    return Button(label="fetch", on_click=fetch_it, key="go")

Handlers async

Um handler pode ser async def e usar await. O re-render acontece quando o set_state roda (depois do await), então a UI reflete o resultado assim que a capacidade resolve. Capacidades disponíveis no Modo C: http, storage (IndexedDB/localStorage), clipboard, geolocation, cookies, share, audio, file, notifications (incl. WebPush subscribe/unsubscribe), install (prompt de instalação PWA), offline (fila de mutações durável).

Instalar o PWA (native.install)

await native.install.state() informa {can_install, installed}; após um gesto do usuário, await native.install.prompt() dispara o prompt nativo de instalação e resolve com "accepted", "dismissed" ou "unavailable". O controller já suprime o mini-infobar frio do browser, então você mostra um botão "Instalar" no momento certo.

Push (native.notifications)

await native.notifications.push_state() informa {supported, permission} sem disparar prompt — use pra decidir mostrar o botão. await native.notifications.request_permission() pede permissão; await native.notifications.subscribe(vapid_public_key) roda o fluxo WebPush do browser e devolve o JSON da assinatura — você o envia ao seu backend (via native.http ou enfileirado com native.offline). unsubscribe() cancela. O framework não decide seu schema de endpoint nem o servidor de push: só entrega a assinatura crua.

Fila offline (native.offline)

Escritas feitas offline sobrevivem: await native.offline.enqueue("POST", url, body) grava uma mutação durável no IndexedDB (com chave de idempotência) e o replay acontece em ordem FIFO quando a conexão volta — via o evento online, via Background Sync (aba fechada) ou explicitamente com await native.offline.replay(). Inspecione com native.offline.size() e native.offline.pending(). O servidor deduplica pela chave de idempotência, então um replay nunca aplica duas vezes.

Validadores de campo

from tempest_core.validators import validate_email, validate_cpf, validate_cnpj, validate_phone roda client-side no Modo C, com o mesmo algoritmo e as mesmas mensagens PT-BR do core (port fiel, travado por fixture). Combina com Input + estado para forms validados sem servidor.

O Modo C fala a mesma navegação dos Modos A/B: app.push(Route(...)), app.pop(), app.replace(...), app.nav.top — sincronizados com a URL do browser (deep-link + voltar/avançar) automaticamente.

from tempest_core import App, Button, Column, Route, Text, Widget


def view(app: App[MyState]) -> Widget:
    def open_product() -> None:
        app.push(Route(name="/products/42"))

    route = app.nav.top
    return Column(children=[
        Text(content=f"rota: {route.name}", key="r"),
        Button(label="abrir produto", on_click=open_product, key="p"),
        Button(label="voltar", on_click=lambda e: app.pop(), key="b"),
    ])

URL ↔ stack

app.push/pop empurram/voltam a URL (pushState); um deep-link ou o botão voltar do browser resetam a stack a partir do path (routes_from_path) — idêntico aos Modos A/B. Path/query params: o name da rota carrega o path completo (incl. ?query), como no core; leia os segmentos por app.nav.stack. Um router com params tipados é evolução no nível do core.

Localização (i18n)

translate / t + Locale do core funcionam no Modo C: busca a chave na tabela {idioma: {chave: template}} pelo idioma do locale e interpola {name} — mesma semântica e fallbacks do core (chave/idioma ausente → a própria chave).

from tempest_core import App, Locale, Text, Widget, t

MESSAGES = {
    "pt": {"greet": "Olá, {name}!"},
    "en": {"greet": "Hello, {name}!"},
}


def view(app: App[MyState]) -> Widget:
    loc = Locale(language=app.state.lang)
    return Text(content=t("greet", locale=loc, translations=MESSAGES, name="Ana"), key="g")

Troque app.state.lang num handler e a UI re-renderiza no novo idioma — verificado no Playwright (PT → EN ao vivo). A tabela MESSAGES é uma constante de módulo (agora suportada no subset).

Tema + responsividade

O Modo C expõe app.theme e app.media como nos Modos A/B. app.theme.is_dark() resolve claro/escuro (DARK/LIGHT absolutos; SYSTEM segue o SO); app.media carrega width/height/platform_dark_mode/orientation — sincronizado com o browser (matchMedia + resize) → a UI re-renderiza responsivamente.

from tempest_core import App, Column, Text, Theme, ThemeMode, Widget


def view(app: App[MyState]) -> Widget:
    dark = app.theme.is_dark(platform_dark_mode=app.media.platform_dark_mode)
    wide = app.media.width >= 600.0

    def toggle() -> None:
        app.set_theme(Theme(mode=ThemeMode.LIGHT if dark else ThemeMode.DARK))

    return Column(children=[
        Text(content=("escuro" if dark else "claro"), key="s"),
        Text(content=("largo" if wide else "estreito"), key="l"),
    ])

Responsividade adaptativa

Redimensione a janela ou mude o prefers-color-scheme do SO e o view re-renderiza — verificado no Playwright (400px→estreito, 900px→largo; toggle de tema claro↔escuro). Os breakpoints do core (Breakpoints: sm/md/lg/xl) também estão disponíveis.

Animação (transições)

Anime declarativamente: dê ao Style de um widget um Transition e o browser faz o tween quando um campo estilizado muda (largura, cor, opacidade) — sem runtime Python, sem driver de frame.

from tempest_core import App, Container, Style, Widget
from tempest_core import Color, Curve, Transition


def view(app: App[MyState]) -> Widget:
    w = 320.0 if app.state.big else 120.0
    return Container(key="box", style=Style(
        width=w,
        background=Color(r=103, g=80, b=164, a=1.0),
        transition=Transition(duration_ms=400, curve=Curve.EASE_IN_OUT),
    ))

Verificado

Trocar app.state.big num handler anima a largura de 120→320px em 400ms (Playwright confirmou a transição CSS aplicada). Curvas: linear, ease, ease-in, ease-out, ease-in-out, bounce, elastic.

Animação imperativa (AnimationController)

Para controle por frame, use AnimationController + Tween — o runtime dirige os controllers num loop requestAnimationFrame, computando o valor a cada frame e re-renderizando.

from tempest_core import AnimationController, Tween
from tempest_core import Curve


def make_state() -> S:
    s = S()
    s.anim = AnimationController(0.6, curve=Curve.EASE_OUT)
    return s


def view(app: App[S]) -> Widget:
    w = Tween(begin=100.0, end=340.0).at(app.state.anim.value)

    def go() -> None:
        app.state.anim.forward()
        app.register_animation(app.state.anim)

    return Container(key="box", style=Style(width=w))

forward()/reverse()/stop(), curvas eased e springs (Spring) — mesma matemática do core. Verificado no Playwright: a largura anima 100→340 (ease-out) e assenta. Isto fecha 100% da cobertura do tempest-core no Modo C.

O tour completo

Tudo acima — estado com métodos, navegação, i18n, tema + responsividade, um formulário validado e uma animação imperativa — vive junto num único app de referência, examples/transpile-tour:

tempestweb build --mode transpile --path examples/transpile-tour
tempestweb dev   --mode transpile --path examples/transpile-tour   # livereload

Um só view, todos os modos

O mesmo view() do tour roda inalterado nos Modos A e B. O build valida isso renderizando pelo core real — uma API que só existisse no Modo C quebraria o build, então o tour é prova viva de portabilidade.

PWA: instalável e offline

Comece uma PWA em um comando

tempestweb new meuapp --template pwa
Gera um projeto Modo C já configurado (mode = "transpile" + bloco [pwa]) com um contador e um botão Install — pronto pra tempestweb build --mode transpile.

Você já tem um bundle 100% estático e sem Python — o alvo perfeito para uma PWA. Por isso o build --mode transpile já emite a camada PWA inteira sozinho: o usuário pode instalar seu app na tela inicial e, depois da primeira visita, abri-lo offline. Sem passo extra, sem configurar nada. 🚀

Basta o build de sempre:

tempestweb build --mode transpile --path examples/transpile-tour

Além do bundle do app, o Modo C agora escreve a camada PWA junto:

dist/transpile/
├── index.html               # linka o manifest, theme-color, apple-touch-icon
│                            #   e registra o service worker
├── manifest.webmanifest     # metadados de instalação (nome, ícones, cores)
├── sw.js                    # service worker cache-first (app shell)
├── register.js              # registra o sw.js no carregamento
├── icons/                   # o conjunto de ícones (maskable + apple-touch)
└── client/ …                # o cliente compartilhado + o seu app.gen.js

O sw.js pré-cacheia o bundle estático inteiroindex.html, o cliente compartilhado, client/transpile/* (incl. o seu app.gen.js), a árvore nativa, os ícones e o manifest. Depois da primeira carga, o app abre e roda sem rede.

Offline de verdade ✅

Isso não é offline "meia-boca": com o servidor HTTP desligado, recarregar a página ainda renderiza o tour e a navegação continua funcionando — verificado ao vivo no Playwright (servidor morto, reload, tour intacto). Como o Modo C é um bundle estático sem Python, não há nada que dependa do servidor depois do primeiro fetch.

Configurando o manifest com [pwa]

Os metadados de instalação vêm de uma seção opcional [pwa] no seu tempestweb.toml. Todos os campos são opcionais — sem a seção, o build usa padrões sensatos derivados do nome do projeto:

[pwa]
name = "Weather Pro"
short_name = "WPro"
theme_color = "#0a84ff"
display = "standalone"
Campo Tipo Padrão O que faz
enabled booleano true O default que manifest e service_worker seguem.
manifest booleano enabled Emite manifest.webmanifest e o <link> dele.
service_worker booleano enabled Emite e registra o worker cache-first.
name string nome do projeto Nome completo exibido na instalação/splash.
short_name string Nome curto para o ícone da tela inicial.
description string Descrição do app no prompt de instalação.
theme_color string "#111111" Cor do tema (barra do navegador + <meta name="theme-color">).
background_color string "#ffffff" Cor de fundo da splash de abertura.
display string "standalone" Modo de exibição: standalone, fullscreen ou minimal-ui.
orientation string Orientação preferida (ex.: portrait, landscape).
lang string "pt-BR" Idioma primário do app.
categories lista de string Categorias da app store (ex.: ["productivity"]).

Valor de display válido

display aceita apenas "standalone", "fullscreen" ou "minimal-ui". Um valor fora dessa lista é erro de build — falha cedo, no espírito do resto do compilador do Modo C.

Automático no Modo C

Você não precisa escrever service worker, manifest nem código de registro à mão: o build --mode transpile gera tudo. A seção [pwa] ajusta os metadados de instalação — o comportamento offline vem de graça porque o bundle é estático.

App que não quer PWA

enabled = false desliga o layer inteiro; manifest e service_worker desligam uma metade só. Desligar o worker emite um worker de teardown no lugar do de cache, para quem já registrou não ficar preso ao precache antigo — o porquê está em PWA e offline.

Prompt de atualização

Quando você publica uma versão nova, o service worker antigo continua no ar até a aba fechar. O shell detecta o worker em espera e mostra um banner discreto "nova versão disponível → Atualizar"; ao confirmar, o worker novo assume e a página recarrega uma vez. Automático — nada a escrever no app.

O subset suportado

O Modo C aceita um subset tipado de Python — o suficiente para a camada de app. Um construto fora dele vira erro de compilação claro (arquivo:linha), no espírito do mypy --strict.

Dentro do subset hoje

  • Expressões: aritmética (+ - * / % ** //), comparação (== != < <= > >=, encadeada a < b < c), booleanos (and/or), unários (not/-), ternário (a if c else b), comprehensions de lista e de dict (inclusive alvo em tupla for k, v in …), literais list/tuple/set/dict, in/not in, indexação e slices (x[a:b]), f-strings (formatos {x:.2f}, {x:,}, {x:,.2f}, {x:.1%}, {x:d}; conversões {x!s}, {x!r}), lambdas de expressão.
  • Builtins: len, str/int/float/bool, abs, round(x[, n]), min/max (variádico ou sobre um iterável), sum(it), range(...), enumerate(it), zip(a, b).
  • Métodos stdlib: string/list (.upper/.lower/.strip/.startswith/ .endswith/.append), views de dict (.items/.keys/.valuesObject.entries/keys/values), sep.join(it). Métodos de objetos do runtime (app.replace, native.storage.get, ctrl.forward) passam intactos — use subscrito d[k] no lugar de dict.get.
  • Statements: if/elif/else, for … in (com alvo em tupla), while, break/continue, try/except/finally (um except pega tudo; vários fazem dispatch por nome da classe de exceção), with … as x (protocolo __enter__/__exit__), raise Exc("msg") / raise (re-raise dentro de except), assert cond[, msg], atribuição (inclusive unpacking a, b = par e encadeada a = b = x), += e afins, return.
  • Estruturas: @dataclass de estado (campos + métodos), herança de dataclass (class B(A)extends), make_state(), view() com closures de handler.
  • Componentes de layout: HStack / VStack (aliases ergonômicos estilo SwiftUI) — gap por token ("md") ou px, align/justify diretos.
  • Widgets: todos os ~64 widgets do tempest_core — layout (Column, Row, Container, Stack, Wrap, ScrollView, SafeArea, Spacer), exibição (Text, Icon, Image, Svg, Spinner, Skeleton, ProgressBar), entrada (Button, Input, TextArea, Switch, Checkbox, Slider, RangeSlider, Dropdown, DatePicker, …), overlays (Dialog, BottomSheet, Popover, Toast, Tooltip), gestos (GestureDetector, Draggable, PanHandler, …) e mais. Os builders JS são gerados por introspecção do core (widgets.gen.js), com o estilo MD3 resolvido dos 14 widgets estilizados.

Eventos por widget

Cada handler é ligado ao evento DOM que o renderizador (dom.js) emite para aquele widget: Button.on_click → clique; Input/Checkbox (controles nativos) → input/change; um Switch (div) → clique. Handlers de widgets cujo evento o cliente ainda não emite (ex.: on_scan, on_reorder) ficam registrados mas inertes por ora.

O kwarg de widget é validado no build

O Modo C não tem Python em runtime: o builder gerado desestrutura o objeto que recebe e ignora chave que não nomeia. Por isso o compilador confere cada chamada de modelo do core contra os campos reais e falha com arquivo:linhaContainer(children=[...]) viraria uma caixa vazia aqui e ValidationError nos Modos A/B. O slot de filho usa o nome do core: child no Container/Draggable, children no Column/Row, fields no Form.

Dark mode não alcança widget nem componente em Modo C

As tabelas de estilo geradas não têm eixo de modo, então o estilo resolvido que viaja inline é sempre o do tema claro — e inline ganha da folha base. Trocar o tema muda o que a folha pinta, não o que o widget carrega. #106 discute as saídas.

Componentes estruturais estão portados

Os 42 componentes estruturais do core rodam em Modo C:

  • superfície e estrutura: Surface, StyledContainer, Card, Scaffold, Grid, Sidebar, Drawer, Divider, mais os aliases HStack/VStack;
  • barras e navegação: AppBar, Header, Footer, NavBar, Breadcrumb, Burger, SegmentedControl;
  • conteúdo: ListTile, Avatar, Chip, Tag, Rating, Stepper, SearchBar, RadioGroup;
  • divulgação e seleção de painel: Accordion (o corpo só existe na árvore quando open, então fechar é remove, não esconder) e Tabs (a aba ativa leva o sublinhado por SideBorder, sem campo de estilo novo);
  • feedback: Banner, Alert, Badge, EmptyState, Stat, ProgressStepper;
  • campos brasileiros: EmailInput, PasswordInput, PhoneInput, CPFInput, CNPJInput e AddressInput — cada um é o rótulo mudo, o Input/MaskedInput com a máscara certa e a linha de erro, e o on_change recebe a string nova, não o evento;
  • composição: MetricCard, StatCard, ConfidenceBadge — mais a função pura confidence_scheme, que é como a app escolhe o esquema do badge.

A composição de cada um foi reescrita em components.js e o resultado dos resolvedores de estilo do core viaja em tabela gerada (component-styles.gen.js), do mesmo jeito que widget-styles.gen.js faz pelos widgets. Cada builder é fixado por uma matriz de props construída do core real — 356 casos — então drift de composição ou de estilo falha no teste. São 151 componentes, cada um com um par __dark (o eixo de modo que a #106 trouxe: um port que esquece de repassar o tema para um filho falha na cor daquele filho), mais trinta e quatro pares __keyed: o mesmo componente construído com key= explícita, porque o build sem chave esconde justamente a derivação (Accordion() emite accordion-header derivando ou não, enquanto Accordion(key="faq-3") só emite faq-3-header se o builder deriva de verdade). O comparador também passou a olhar a chave de cada descendente — antes só comparava forma e estilo, e por isso um port com chave literal atravessou um release inteiro.

E os componentes do próprio tempestweb (tempestweb.components) também: TextField, EmailField, PasswordField, os formulários prontos LoginForm e SignupForm, e os apelidos PhoneField/CPFField/ CNPJField/AddressField sobre os campos do core. Eles derivam a chave de cada filho da chave do componente, então dois na mesma tela não disputam o nome do Input que emite o evento — e o Modo C carrega essa derivação.

examples/mode-c-components exercita o lote inteiro num app só.

O componente carrega as props da base — inclusive aqui

Todo widget declara semantics, focusable, focus_order, tag e attrs. Nos Modos A e B quem as carrega para a raiz que o componente renderiza é o build do core (tempest-core 0.17.0). Em Modo C um componente é uma função, não um nó que alguém expande: prop que o builder não lê chegaria a nó nenhum, e uma tela acessível no browser ficaria muda na build transpilada de si mesma.

Cada builder de components.js passa a carregar, com a regra do core: o render é dono do que ele tocou. Prop que a árvore construída já define em qualquer nó fica intacta — é o que mantém um campo correto, porque ele põe o nome acessível no <input> em que o leitor de tela para, e uma segunda cópia no wrapper sem role anunciaria o mesmo controle duas vezes.

Card(semantics=Semantics(label="Totais"), tag="section", children=[total])

Nos três modos, esse Card sai como uma <section> que se anuncia "Totais". Fixado por seis pares __named na matriz de paridade (os dois ramos da regra) e por um sweep sobre todos os builders em tests/client/component-carry.test.js.

As formas que o app real escreve

O subset aceita o que um app de verdade escreve, não só o mínimo do counter:

  • import só de anotação: from collections.abc import Callable e from typing import Any passam e não custam import nenhum no JS — mas usar o nome como valor é erro ('Any' is a type-only name). Alias de tipo em nível de módulo (Fetcher = Callable[[], None]) também é descartado.
  • from tempestweb.components import …: é o import que o tutorial de componentes ensina, e roteia para os mesmos nomes servidos. Nome que o cliente não tem é recusado pelo nome, não pelo módulo.
  • [a, *rest] (o idioma de "nova lista sem mutar"), alvo destructurado (for i, (q, a) in enumerate(pairs)), is / is not (contra None vira == null, que responde certo para campo nunca atribuído) e f"{n:02d}" (zero-pad de relógio e placar — com o sinal fora do preenchimento, como o Python faz).
  • dataclass do jeito real: campo sem default (fica undefined até o make_state preencher), @dataclass(frozen=True), e field(default_factory=…) com callable próprio.
  • conversão de container: list(xs), tuple(xs), set(xs), dict(pairs).
  • módulos da stdlib que o browser tem: re, json, math, base64 e asyncio, nas duas formas de import (import re / from math import ceil). Pattern.match ancora no início como no Python, re.sub troca todas, e asyncio.sleep(0.4) espera 400 ms. Membro fora da tabela é recusado pelo nome (re.escape), e módulo fora da lista diz o que fazer no lugar.
  • enum do app: class Phase(StrEnum) vira objeto congelado, como os enums do core já viajam em values.gen.js.
  • generator expression (any(x for x in xs)), any/all, dict.get com default, e os predicados de str (c.isdigit()).
  • {**old, k: v}, o idioma de "novo dict sem mutar" (irmão do [a, *rest]), e xs[:] = [...], a substituição no lugar — que vira splice, não uma atribuição a uma cópia.
  • dict como dict: dict(outro) copia e dict(pares) constrói — a mesma chamada em Python, operações diferentes em JS, resolvidas em runtime. E d.pop(chave, default) remove de verdade, em vez de cair no pop de array.
  • a verdade do Python, não a do JS: "", 0, None e False as duas linguagens concordam; container vazio não[] e {} são falsy em Python e truthy em JS. Posição booleana (if, elif, while, not, o ternário) passa por truthy$, então if s.errors: responde o que o Python responderia. Comparação, not, literal booleano e nome que o módulo só liga a booleano ficam sem embrulho, para o teste continuar legível. len(d) conta as chaves e "k" in d lê chave, em vez de cair no .length e no .includes de array.

and/or em posição de valor ficam como estão

nome or "—" devolve um operando nas duas linguagens, não um booleano, então || já é o comportamento certo. A diferença aparece só quando o operando da esquerda é container vazio — [] or x devolve x em Python e [] em JS. Nenhum exemplo do corpus escreve isso, e embrulhar mudaria o valor de todo or para ganhar um caso que ninguém usa.

  • predicados de caso: c.isupper() / c.islower() exigem ao menos um caractere com caixa, como o Python — "1".isupper() é False. As classes são ASCII, como as dos outros predicados.
  • Form.validate(values) — o único método de widget que o Modo C porta. Ele cabe porque o insumo sobrevive: validators nunca atravessa fio em Modo C, então as funções vivas estão no nó quando a validação roda. Qualquer outro método continua recusado.
  • f"{x:+.1f}": o + força o sinal no positivo, como o Python. O valor é formatado primeiro e o prefixo decidido do resultado, senão um negativo viraria +-3.0. Combina com ,, % e d; com 0Nd é recusado, porque o Python conta o sinal dentro da largura.
  • if __name__ == "__main__": é pulado, não recusado: o bloco é guarda de script e nunca roda quando o arquivo é importado como módulo — que é exatamente como o Modo C o compila. Um else nele ainda é recusado, porque esse roda.
  • construtor de evento do core: ThemeChangeEvent(mode=ThemeMode.DARK) e os outros 32 eventos são gerados para values.gen.js. A app constrói um quando simula um evento do host.

  • capacidade nativa, nas três formas de import: from tempestweb import native, from tempestweb.native import storage e from tempestweb.native.geolocation import get_position caem todas no mesmo objeto de ./native.js. Grupo que a fachada não carrega (camera) é recusado dizendo qual modo o tem, e membro desconhecido é recusado pelo nome (geolocation.triangulate).

  • listas virtualizadas: LazyColumn, LazyRow e LazyGrid materializam a janela visível chamando item_builder(índice), com o item re-chaveado pelo índice absoluto — o que faz a janela deslizar virar remove/reorder/insert mínimo em vez de rebuild. O evento de scroll desliza a janela no runtime, como o servidor faz no Modo B, e ela sobrevive ao view re-rodando.

Medido no corpus: 44 dos 57 exemplos transpilam (eram 14).

Componente sempre com key explícita

A chave default de um componente é o nome dele (card, alert, navbar), então dois Card sob o mesmo pai respondem os dois por card e o patch endereça o errado. Vale nos três modos; em Modo C você percebe igual.

Enums, objetos de valor e tokens do core estão servidos

TextAlign.CENTER, FontWeight.BOLD, KeyboardType.EMAIL, Semantics(label=…), Border, Shadow, Gradient, ACCENT, ON_SURFACE, HOVER_OPACITY — os 32 enums do core, os objetos de valor não-widget e os tokens de design são gerados para values.gen.js a partir do core, na forma do fio. Style/Color/Edge continuam onde sempre estiveram (widget-support.js).

Ainda fora do subset — e agora falha no build

Método de widget do core (form.validate(values), e qualquer outro): o cliente porta o builder de cada widget, não os métodos Python da classe. Chamar um é erro de compilação com arquivo:linha, e não uma página que carrega e morre na primeira renderização.

Os componentes dirigidos por dados de tempest_core.components (DataTable, Table, BarChart/LineChart, DetectionOverlay, ResultView, Calendar/Clock, os pickers de mídia e de formulário, e o CollapsingAppBar, que depende do scroll): a forma da árvore deles depende dos dados recebidos — uma linha de células por registro, uma barra por dado — então não há composição fixa para portar. Percorrer uma lista plana de rótulos não é isso: Tabs e Accordion são composição fixa e estão servidos, como os estruturais acima (#107 acompanha o que falta). Também fora: comprehension com mais de um for, e format-specs de f-string além dos suportados (ex.: alinhamento {x:>5}, sinal {x:+.2f}, hex/bin {x:x}, dinâmico {x:.{n}f}, conversão !a).

Usar um desses nomes agora é erro de compilação com arquivo:linha:

app.py:12: `Card` is not available in Mode C (the transpile client exports no such name)

Antes o compilador emitia import { Card } from "./widgets.js" mesmo assim — um import que o browser não resolve, então o módulo nunca era avaliado e a página ficava em branco, sem nada no log do build. Importar um tipo só para anotação (DragEvent, TextChangeHandler) continua livre: anotação é descartada, o nome nunca é referenciado, nenhum import é emitido.

Recapitulando

  • Modo C transcreve a camada de app Python para JS nativo — zero runtime Python, bundle estático, first-paint/SEO ótimos.
  • tempestweb build --mode transpile gera um diretório servível por qualquer CDN; run --mode transpile serve localmente.
  • O mesmo view() dos Modos A/B roda aqui — estado, handlers, Button/Input estilizados, binding reativo, navegação, i18n, tema, animação.
  • É um modo maduro e de primeira classe: 100% dos widgets do core, subset amplo de Python tipado e PWA turnkey. Detalhes de design em docs/modo-c-transpile.md.

Continue para PWA e offline para o fluxo completo de instalação e WebPush, ou volte ao Tutorial para revisar o counter nos outros modos. 🚀