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