From e4da5999e0929dff053bb1e1c017f45bba243233 Mon Sep 17 00:00:00 2001 From: Kacper Wojciechowski <39823706+jog1t@users.noreply.github.com> Date: Thu, 30 Jul 2026 00:54:43 +0200 Subject: [PATCH] feat(inspector-tabs): send resolved theme tokens and mounted surface to custom tab iframes --- .../inspector-tabs/counter/index.html | 67 ++++++-- .../inspector-tabs/info/index.html | 44 +++++- .../scripts/generate-inspector-tab-css.mjs | 13 +- frontend/scripts/inspector-tab-aliases.css | 13 +- .../actors/actor-details-iframe.tsx | 11 ++ .../components/actors/inspector-tab-tokens.ts | 49 ++++++ .../rivetkit/src/inspector-tab/mod.ts | 149 ++++++++++++++++++ .../content/docs/actors/inspector-tabs.mdx | 119 ++++++++++++-- 8 files changed, 428 insertions(+), 37 deletions(-) create mode 100644 frontend/src/components/actors/inspector-tab-tokens.ts diff --git a/examples/inspector-tabs/inspector-tabs/counter/index.html b/examples/inspector-tabs/inspector-tabs/counter/index.html index 5063aa3134..d4c85e19f4 100644 --- a/examples/inspector-tabs/inspector-tabs/counter/index.html +++ b/examples/inspector-tabs/inspector-tabs/counter/index.html @@ -20,8 +20,9 @@ 3c. HELPERS — DOM helpers (errors, busy state) 3d. INSPECTOR HTTP — fetchState / invokeAction wrappers 3e. RENDERING — translate actor state into DOM - 3f. HANDSHAKE — postMessage receiver + `ready` signal (REQUIRED) - 3g. WIRING — connect buttons to actions (UI-specific) + 3f. THEME — mirror the dashboard's theme + tokens + 3g. HANDSHAKE — postMessage receiver + `ready` signal (REQUIRED) + 3h. WIRING — connect buttons to actions (UI-specific) REQUIRED markers below highlight the contract between every tab and the dashboard. Everything else is demo-specific. @@ -44,9 +45,9 @@ The stylesheet exposes the dashboard's design tokens as `--rivet-*` CSS variables, mirrors the dashboard's light + dark token blocks, and ships sensible body/button/input - defaults. The tab applies the dashboard's active theme by - toggling the `dark` class on `` from the `v1Init` - handler below. + defaults, including painting `body` with `--rivet-surface` + (the panel the tab is mounted on). The tab applies the + dashboard's active theme in section 3f below. You can omit this link and bring your own CSS — the inspector tab API doesn't require it. --> @@ -54,6 +55,13 @@

Counter:

@@ -58,12 +65,23 @@ Drop an `index.html` in the `source` directory: }); const { state } = await r.json(); document.getElementById("value").textContent = state.value; - document.documentElement.classList.toggle( - "dark", - (e.data.theme ?? "dark") === "dark", - ); + applyTheme(e.data); }); + // Mirror the dashboard's theme. Re-run on every `init` — the + // dashboard re-sends it when the user toggles the theme. + function applyTheme(msg) { + const root = document.documentElement; + root.classList.toggle("dark", (msg.theme ?? "dark") === "dark"); + if (!msg.tokens) return; + for (const [name, value] of Object.entries(msg.tokens)) { + const varName = name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`); + root.style.setProperty(`--rivet-${varName}`, value); + } + const surface = msg.tokens[msg.surface ?? "card"]; + if (surface) root.style.setProperty("--rivet-surface", surface); + } + window.parent.postMessage({ type: "ready", v: 1 }, SHELL_ORIGIN); @@ -135,6 +153,8 @@ arrives. actorId: string, authToken: string, // Per-actor inspector bearer token theme?: "light" | "dark", + tokens?: Record, // Resolved CSS colors — see Styling + surface?: string, // Key in `tokens` for the host panel's color activeTab?: string, // For multi-view tabs } ``` @@ -144,6 +164,11 @@ seed their initial sub-view. The dashboard does not send a separate message when the user switches custom tabs — it navigates the iframe `src` instead, so the tab reloads and receives a fresh `init`. +`init` is also the theme channel. The dashboard re-sends it whenever the +user toggles the theme, so a tab that applies `theme` / `tokens` on every +`init` follows theme changes without reloading. See +[Styling](#styling). + ### From the tab Send `ready` once your message listener is registered: @@ -219,17 +244,93 @@ custom CSS: } ``` -Toggle dark mode by adding the `dark` class to `` — the dashboard -sends the active theme in the `init` message. +### Which surface the tab is on + +The tab iframe is mounted on the dashboard's **`card`** panel, not on the +page background. Use `--rivet-surface` (an alias of `--rivet-card`) for +the tab's own backdrop so it composites seamlessly with the panel around +it. The stylesheet already paints `html` and `body` with it, so a tab that +doesn't override the body background needs no extra work. + +Do **not** paint the tab with `--rivet-background`. That is the color of +the dashboard shell *around* the panel, and using it leaves a visible +seam. + +If your tab sets its own body background, state the surface explicitly. +The fallback keeps it correct against an older stylesheet that predates +`--rivet-surface`: + +```css +html, body { background: var(--rivet-surface, var(--rivet-card)); } +``` + +### Token names + +Every dashboard token is exposed under a `--rivet-` prefix, and the same +names arrive as `init.tokens` keys in camelCase (`--rivet-muted-foreground` +↔ `tokens.mutedForeground`). These names are stable: + +`background`, `foreground`, `card`, `cardForeground`, `popover`, +`popoverForeground`, `primary`, `primaryForeground`, `secondary`, +`secondaryForeground`, `muted`, `mutedForeground`, `accent`, +`accentForeground`, `destructive`, `destructiveForeground`, `border`, +`input`, `ring`. + +Plus `surface` / `--rivet-surface` for the mounted surface described +above. New tokens may be added; existing names won't change meaning. +`init.tokens` may carry keys your version of the stylesheet doesn't know, +so iterate it rather than reading a fixed list. -Color tokens come in both pre-wrapped (`--rivet-card`) and raw HSL -(`--rivet-card-raw`) forms, so you can compose with alpha: +Color tokens also come in raw HSL form (`--rivet-card-raw`) so you can +compose with alpha: ```css .overlay { background: hsl(var(--rivet-background-raw) / 0.6); } ``` -You're free to skip the stylesheet entirely and bring your own. +### Reacting to theme changes + +The dashboard signals the theme two ways on `init`, and re-sends `init` +every time the user toggles the theme — so a tab that applies both on +every `init` updates live, with no reload: + +- **`theme`** (`"light" | "dark"`) — toggle the `dark` class on ``. + The stylesheet's token blocks key off it. Default to `"dark"` if absent. +- **`tokens`** and **`surface`** — the dashboard's resolved colors, as CSS + color values ready to use in a declaration. Pinning them as inline + `--rivet-*` custom properties makes the tab render the dashboard's exact + colors even if the stylesheet it vendored is older than the dashboard. + +```js +function applyTheme(msg) { + const root = document.documentElement; + root.classList.toggle("dark", (msg.theme ?? "dark") === "dark"); + if (!msg.tokens) return; + for (const [name, value] of Object.entries(msg.tokens)) { + const varName = name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`); + root.style.setProperty(`--rivet-${varName}`, value); + } + const surface = msg.tokens[msg.surface ?? "card"]; + if (surface) root.style.setProperty("--rivet-surface", surface); +} +``` + +Tabs built with a bundler can import that logic instead of copying it: + +```ts @nocheck +import { applyInspectorTabTheme } from "rivetkit/inspector-tab"; + +applyInspectorTabTheme(init, document.documentElement); +``` + +`tokens` and `surface` are both optional. An older dashboard sends only +`theme`, and the stylesheet resolves the right colors on its own — never +hardcode hex values as a fallback, or the tab drifts the next time the +dashboard's tokens change. + +You're free to skip the stylesheet entirely and bring your own. If you do, +`init.tokens` is the whole palette, and `init.tokens[init.surface]` is the +color your body must be for the tab to sit flush in its panel. ## Security