Skip to content

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.

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.

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.

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.

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.