A few conventions keep widgets robust given the one-way data model.
Render defensively
onData hands you whatever JSON was last pushed — it may be partial, or a metric
may be unavailable on some Macs. Default everything and bail early:
window.PREEN.onData((d) => {
d = d || {};
if (d.available === false) return; // source explicitly has nothing
setText("cpu", typeof d.cpu === "number" ? Math.round(d.cpu) + "%" : "—");
});
Show a neutral placeholder (—) before the first payload, not a spinner — the
widget mounts before any data arrives.
Re-render, don’t accumulate
Each onData call carries the full payload, not a diff. Treat your render
function as a pure projection of the latest data; don’t append or mutate
history-derived state unless you keep it yourself.
Size in vmin, reflow with the orientation classes
Size elements in vmin (the short edge) — font-size: 5vmin, not 5vw —
so they stay the same physical size in portrait and landscape. vw/vh sizes
balloon or collapse when the phone rotates; vmin is constant.
Layout is the part that should change on rotation, and that’s pure CSS: the
runtime keeps preen-landscape / preen-portrait in sync on <html> and
<body> for you.
.stats { display: flex; flex-direction: column; gap: 9vmin; }
.preen-landscape .stats { flex-direction: row; } /* reflow, don't resize */
Never add your own resize or orientationchange listeners — the runtime
handles both, and the CSS orientation media feature is unreliable inside the
widget’s web view. Inside a .preen-landscape rule, vh equals vmin (height
is the short edge), so either works there.
Optimistic UI for actions
Interactive widgets feel laggy if they wait for the next data poll to reflect a tap. Apply the change locally on tap, then reconcile when fresh data confirms it:
function setVolume(v) {
vol = v; render(); // optimistic
expectVol = v; volPending = true; // remember what we asked for
window.PREEN.trigger("volset", { vol: v });
}
window.PREEN.onData((d) => {
if (volPending && Math.abs(d.vol - expectVol) <= 1) volPending = false;
if (!volPending) vol = d.vol; // trust the source once it agrees
render();
});
Keep it self-contained
No external scripts, stylesheets, fonts, or images (the CSP blocks them). Inline
everything; use system fonts or data: URIs. If a widget renders blank, an
external asset request blocked by CSP is the usual cause.
Don’t poll or fetch
There’s no setInterval polling to write and no endpoint to call — the phone
polls for you at the widget’s refreshMs. Your only job is to render what
onData gives you.