Device panel — Tier 1 capabilities 📱¶
A small control panel wiring four native capabilities from Track T to buttons: buzz the device, keep the screen awake, go fullscreen, and read the connection state. One screen, four Web APIs, the same Python in both modes. 🚀
What you'll build¶
- 🔵 Buzz button — vibrates in a pattern (
native.vibration) - 🟢 Keep awake button — holds/releases a screen wake lock (
native.wakelock) - 🟣 Fullscreen button — enters fullscreen (
native.fullscreen) - 🟠 Network button — reads the connection type and online status (
native.network) - 💬 Two status lines reflecting the last action and the network summary
A Tier 1 showcase
Track T's Tier 1 is the set of universal capabilities — widely supported,
cheap, high value. This example picks four of them and shows that each is just an
await native.<group>.<verb>() inside a handler. No JavaScript.
The full app¶
Here is examples/device-panel/app.py in full, ready to copy:
"""Device panel — Tier-1 web-platform capabilities in one screen.
The same ``view`` runs unchanged in both interactive modes::
tempestweb dev --mode wasm # Python in the browser (Pyodide)
tempestweb dev --mode server # Python on the server (FastAPI + WebSocket)
A small control panel wiring several of the new ``native`` capabilities to
buttons: buzz the device (``vibration``), keep the screen awake
(``wakelock``), go fullscreen (``fullscreen``), and read the connection and
storage-quota state (``network`` / ``quota``). Each is a typed Python awaitable
that resolves the same way in Mode A (in-process) and Mode B (proxied to the
browser and back) — the app code never knows the difference.
The initial mount only reads state, so ``build(view(app))`` is green with no
native bridge installed; the async handlers call the capabilities, and the test
drives them through a scripted bridge.
"""
from __future__ import annotations
from dataclasses import dataclass
from tempest_core import App, Style, Widget
from tempest_core import Edge
from tempest_core import Button, Column, Row, Text
from tempestweb import native
@dataclass
class PanelState:
"""State for the device panel.
Attributes:
status: A short human-readable line describing the last action.
awake: Whether a screen wake lock is currently held.
network: The last connection summary read from ``network.state``.
"""
status: str = "ready"
awake: bool = False
network: str = ""
def make_state() -> PanelState:
"""Build the initial state.
Returns:
A fresh :class:`PanelState`.
"""
return PanelState()
def view(app: App[PanelState]) -> Widget:
"""Render the panel and wire each button to a native capability.
Args:
app: The application handle exposing ``state`` and ``set_state``.
Returns:
The widget tree for the current state.
"""
async def buzz() -> None:
await native.vibration.vibrate([100, 50, 100])
app.set_state(lambda s: setattr(s, "status", "buzzed"))
async def toggle_awake() -> None:
if app.state.awake:
app.set_state(lambda s: setattr(s, "status", "screen released"))
app.set_state(lambda s: setattr(s, "awake", False))
else:
await native.wakelock.request()
app.set_state(lambda s: setattr(s, "status", "screen kept awake"))
app.set_state(lambda s: setattr(s, "awake", True))
async def go_fullscreen() -> None:
active = await native.fullscreen.enter()
app.set_state(lambda s: setattr(s, "status", f"fullscreen={active}"))
async def read_network() -> None:
state = await native.network.state()
summary = f"{state.effective_type} · online={state.online}"
app.set_state(lambda s: setattr(s, "network", summary))
app.set_state(lambda s: setattr(s, "status", "network read"))
return Column(
style=Style(gap=10.0, padding=Edge.all(16)),
children=[
Text(content=f"Status: {app.state.status}", key="status"),
Text(content=f"Network: {app.state.network or '—'}", key="network"),
Row(
style=Style(gap=6.0),
children=[
Button(label="Buzz", on_click=buzz, key="buzz"),
Button(label="Keep awake", on_click=toggle_awake, key="awake"),
Button(label="Fullscreen", on_click=go_fullscreen, key="fs"),
Button(label="Network", on_click=read_network, key="net"),
],
),
],
)
Piece by piece¶
The state¶
Three simple fields: a status line, an awake boolean to remember whether the
wake lock is held, and a network summary from the last read. The initial render
only reads these fields — it never calls a capability — so build(view(app)) is
green with no bridge installed. This is the same discipline as the other native
examples: the mount doesn't depend on a browser.
The handlers — one await per capability¶
Each button wires to an async handler defined inside view() (to capture
app). Notice how each is just one capability line + set_state:
async def buzz() -> None:
await native.vibration.vibrate([100, 50, 100])
app.set_state(lambda s: setattr(s, "status", "buzzed"))
vibration.vibrate takes an on/off pattern in milliseconds — [100, 50, 100]
buzzes for 100 ms, pauses 50 ms, buzzes another 100 ms. It's fire-and-forget: no
return value.
async def toggle_awake() -> None:
if app.state.awake:
app.set_state(lambda s: setattr(s, "status", "screen released"))
app.set_state(lambda s: setattr(s, "awake", False))
else:
await native.wakelock.request()
app.set_state(lambda s: setattr(s, "status", "screen kept awake"))
app.set_state(lambda s: setattr(s, "awake", True))
wakelock.request() keeps the screen awake and returns an opaque id (which a
fuller version would keep to call wakelock.release(id)). Here, to keep the example
lean, the toggle just flips the visual state.
async def go_fullscreen() -> None:
active = await native.fullscreen.enter()
app.set_state(lambda s: setattr(s, "status", f"fullscreen={active}"))
fullscreen.enter() returns a bool — whether the document is in fullscreen after
the call. Good habit: reflect the real result in state, don't assume success.
async def read_network() -> None:
state = await native.network.state()
summary = f"{state.effective_type} · online={state.online}"
app.set_state(lambda s: setattr(s, "network", summary))
app.set_state(lambda s: setattr(s, "status", "network read"))
network.state() returns a typed NetworkState (online, effective_type,
downlink, rtt, save_data). Here we build a short summary from two fields.
The same line, two mechanisms
No handler names a transport. In Mode A each await native.… resolves
in-process via FFIBridge; in Mode B it is proxied through a WebSocket
round-trip (ProxyBridge). The code is identical — that's what Track T
delivers.
The widget tree¶
return Column(
style=Style(gap=10.0, padding=Edge.all(16)),
children=[
Text(content=f"Status: {app.state.status}", key="status"),
Text(content=f"Network: {app.state.network or '—'}", key="network"),
Row(
style=Style(gap=6.0),
children=[
Button(label="Buzz", on_click=buzz, key="buzz"),
Button(label="Keep awake", on_click=toggle_awake, key="awake"),
Button(label="Fullscreen", on_click=go_fullscreen, key="fs"),
Button(label="Network", on_click=read_network, key="net"),
],
),
],
)
Two Text lines derived from state, and a Row with the four buttons. Each
Button takes its async handler directly in on_click. Stable keys keep the
reconciler (and the tests) precise.
Running the example ▶¶
This example runs in Modes A/B — the same app.py, without changing a line:
tempestweb dev --mode wasm --path examples/device-panel # Python in the browser (Pyodide)
tempestweb dev --mode server --path examples/device-panel # Python on the server (FastAPI + WS)
Secure context and user gesture
fullscreen.enter(), wakelock.request(), and vibration.vibrate() only work
from a real user gesture (the button click counts) and, in production, under
HTTPS. On localhost with the dev server it all works. Outside a secure
context, the bridge returns a NativeError (insecure_context or
permission_denied) — treat it as a normal flow.
Verify
In either mode you should see two status lines and four buttons. Clicking:
- Buzz → the device vibrates (on capable hardware) and the status becomes
buzzed. - Keep awake → status
screen kept awake; clicking again →screen released. - Fullscreen → the page enters fullscreen; status
fullscreen=True. - Network → the Network line shows something like
4g · online=True.
Recap¶
- Four Tier 1 capabilities (
vibration,wakelock,fullscreen,network) wired to four buttons, each one anawait native.<group>.<verb>(). - The initial render only reads state, so
build(view(app))is green with no bridge — the testable pattern of the native examples. - Reflect the real return in state (
fullscreen.enter()returns abool) instead of assuming success. - The same code runs in Mode A (in-process) and Mode B (proxied) — the handler never knows which bridge is installed.
Next steps¶
- 💡 Keep the id from
wakelock.request()in state and callwakelock.release(id)in the toggle, to actually release it. - 💡 Add a button that watches the network as a stream with
native.network.watch()— see the native event channel. - 💡 Explore the full catalog in the Native capability reference.
- 💡 Compare with Copy & Share, which injects the capabilities into state to test without a browser.