UI contributions
Your bundle contributes UI by returning component specs — plain
JSON like { type: "Pill", props: { … } } — from slot renderers. The
host validates every tree and renders it with first-party markup, so
contributions inherit the product’s typography, spacing, and theming
automatically. Extensions never pick colors, fonts, or pixel layouts.
| Slot id | Called with | Must return |
|---|---|---|
photo-tile.badge |
{ workspaceId, projectId, assetIds[], api } — once per photo grid |
A map of assetId → Badge | ProgressRing | null |
project.header.status |
{ workspaceId, projectId, api } |
Pill or null |
dashboard.card |
{ workspaceId, api } |
Card or null |
activity.feed |
— (no renderer) | Declared in the catalog; rows are emitted by your sync, not rendered by the bundle. See Activity events. |
Root component types are enforced per slot — a Card returned to the
pill slot is a validation failure for that render, not a broken page.
Components
Section titled “Components”All constructors come from @homestage/extension-kit and return
ComponentSpec JSON.
Badge({ shape: "dot" | "count", tone, tooltip?, count? })ProgressRing({ value: /* 0–1, or null = indeterminate */, tone, tooltip? })Pill({ text, tone, icon? })Card({ title }, children)Row({ gap?: "sm" | "md" }, children)Stack({ gap?: "sm" | "md" }, children)Text({ text, variant?: "body" | "caption", truncate? })Icon({ name })Button({ label, onPress })"neutral" | "success" | "pending" | "warning" | "danger" — mapped to
theme-consistent colors on the host side, correct in light and dark
mode. This is the only way a contribution expresses color.
A fixed nine-name set, rendered from the product’s own icon system:
images · calendar · folder · plug · send · users · receipt · megaphone · chart
Unknown names fall back to plug rather than failing the render.
Buttons and events
Section titled “Buttons and events”onPress never crosses the worker boundary. The spec carries only a
generated handler id; when the customer clicks, the host routes the
event back into your worker and your callback runs there. A callback
that throws is contained — it cannot crash in-flight renders.
The api bridge
Section titled “The api bridge”ctx.api.get(path) is your only network access. It resolves page-side
with the customer’s session, against an allowlist scoped to your app —
your worker never sees a token, a cookie, or an unlisted endpoint.
ctx.api.theme.mode ("light" | "dark") is sampled fresh for every
render.
Hard limits
Section titled “Hard limits”| Limit | Value | On overflow |
|---|---|---|
| Render soft budget | 100 ms | logged warning |
| Render hard budget | 1 000 ms | worker terminated + restarted, render fails |
| Nodes per slot tree | 200 | that render fails |
| Tree depth | 8 | that render fails |
| Response size | 64 KB | that render fails |
| Crash strikes | 3 per 10 min | UI disabled for the session |
Every failure renders as a small muted glyph with a tooltip in place of your contribution — no toast, no layout shift, page intact.