Building widgets

Patterns & gotchas

Practical conventions for widgets that stay calm, correct, and responsive.

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.PERCH.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.

Lay out for both orientations

Use the perch-landscape / perch-portrait body classes (not CSS media queries) and size with viewport units (vmin, vh, vw) so the widget fills whatever orientation the stand holds.

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.PERCH.trigger("volset", { vol: v });
}
window.PERCH.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.