nteract
componentisolated-renderersource: ok · refreshed
Isolated Renderer
Complete isolated iframe renderer bundle for Jupyter outputs and widgets. Runs inside a sandboxed iframe with its own React app, receiving render commands via postMessage. Includes widget bridge client for two-way comm protocol support. This is a standalone entry point that can be built separately (includes vite.config.ts).
npx shadcn add "https://nteract-elements.vercel.app/r/isolated-renderer.json"free
No selection tags yet — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightRegistry dependencies
@nteract/error-boundary@nteract/ansi-output@nteract/html-output@nteract/image-output@nteract/isolated-frame@nteract/json-output@nteract/markdown-output@nteract/svg-output@nteract/widget-view@nteract/widget-controls
Files5 · source cached 10/8/2026, 3:31:49 AM
registry/isolated-renderer/index.tsx
/**
* Isolated Renderer Entry Point
*
* This module runs inside an isolated iframe and renders Jupyter outputs
* using React and the existing output components. It receives render
* commands from the parent via postMessage and displays them.
*
* Security: This code runs in a sandboxed iframe with an opaque origin.
* It cannot access Tauri APIs, the parent DOM, or localStorage.
*/
import { StrictMode, useCallback, useEffect, useState } from "react";
import { createRoot, type Root } from "react-dom/client";
// Import styles (Tailwind + theme variables)
import "./styles.css";
// Import error boundary for fault isolation
import { ErrorBoundary } from "@/registry/lib/error-boundary";
import { OutputErrorFallback } from "@/registry/lib/output-error-fallback";
// Import output components directly (not through MediaRouter's lazy loading)
// This ensures all components are bundled inline for the isolated iframe
import {
AnsiErrorOutput,
AnsiOutput,
AnsiStreamOutput,
} from "@/registry/outputs/ansi-output";
import { HtmlOutput } from "@/registry/outputs/html-output";
import { ImageOutput } from "@/registry/outputs/image-output";
import type { RenderPayload } from "@/registry/outputs/isolated/frame-bridge";
import { JsonOutput } from "@/registry/outputs/json-output";
import { MarkdownOutput } from "@/registry/outputs/markdown-output";
import { SvgOutput } from "@/registry/outputs/svg-output";
import { WidgetView } from "@/registry/widgets/widget-view";
// Import widget support
import { IframeWidgetStoreProvider } from "./widget-provider";
// Import widget controls to register them in the widget registry
// This import has side effects that register all built-in widgets
import "@/registry/widgets/controls";
// --- Types ---
interface OutputEntry {
id: string;
payload: RenderPayload;
}
interface RendererState {
outputs: OutputEntry[];
isDark: boolean;
}
// --- Theme Management ---
/**
* Update the document theme so components can detect it via isDarkMode().
* Sets class and data-theme on documentElement (html tag).
*/
function updateDocumentTheme(isDark: boolean) {
const root = document.documentElement;
// Set class for Tailwind dark: variant detection
if (isDark) {
root.classList.add("dark");
root.classList.remove("light");
} else {
root.classList.add("light");
root.classList.remove("dark");
}
// Set data-theme for components that check this attribute
root.setAttribute("data-theme", isDark ? "dark" : "light");
// Set color-scheme to influence prefers-color-scheme media queries
// Some widgets (like drawdata) use @media (prefers-color-scheme: dark)
root.style.colorScheme = isDark ? "dark" : "light";
// Update CSS variables for base styles (background kept transparent for cell focus colors to show through)
if (isDark) {
root.style.setProperty("--bg-primary", "#0a0a0a");
root.style.setProperty("--bg-secondary", "#1a1a1a");
root.style.setProperty("--text-primary", "#e0e0e0");
root.style.setProperty("--text-secondary", "#a0a0a0");
root.style.setProperty("--foreground", "#e0e0e0");
} else {
root.style.setProperty("--bg-primary", "#ffffff");
root.style.setProperty("--bg-secondary", "#f5f5f5");
root.style.setProperty("--text-primary", "#1a1a1a");
root.style.setProperty("--text-secondary", "#666666");
root.style.setProperty("--foreground", "#1a1a1a");
}
}
// --- Message Handling ---
type MessageHandler = (type: string, payload: unknown) => void;
let messageHandler: MessageHandler | null = null;
function setupMessageListener() {
window.addEventListener("message", (event) => {
// Only accept messages from parent
if (event.source !== window.parent) return;
const { type, payload } = event.data || {};
if (messageHandler) {
messageHandler(type, payload);
}
});
}
// --- React App ---
function IsolatedRendererApp() {
const [state, setState] = useState<RendererState>({
outputs: [],
isDark: true,
});
// Handle messages from parent
const handleMessage = useCallback((type: string, payload: unknown) => {
switch (type) {
case "render": {
const renderPayload = payload as RenderPayload;
// Generate stable ID when cellId is provided for better React reconciliation
const id = renderPayload.cellId
? `${renderPayload.cellId}-${renderPayload.outputIndex ?? 0}`
: `output-${Date.now()}-${Math.random().toString(36).slice(2)}`;
setState((prev) => {
if (renderPayload.replace) {
// Replace all outputs with this single new output
return { ...prev, outputs: [{ id, payload: renderPayload }] };
}
// Default: append to existing outputs
return {
...prev,
outputs: [...prev.outputs, { id, payload: renderPayload }],
};
});
// Notify parent of render completion after next paint
requestAnimationFrame(() => {
window.parent.postMessage(
{
type: "render_complete",
payload: { height: document.body.scrollHeight },
},
"*",
);
});
break;
}
case "clear":
setState((prev) => ({ ...prev, outputs: [] }));
requestAnimationFrame(() => {
window.parent.postMessage(
{
type: "render_complete",
payload: { height: document.body.scrollHeight },
},
"*",
);
});
break;
case "theme": {
const themePayload = payload as { isDark?: boolean };
if (themePayload?.isDark !== undefined) {
setState((prev) => ({ ...prev, isDark: themePayload.isDark! }));
// Update theme on document.documentElement so theme detection works
updateDocumentTheme(themePayload.isDark);
}
break;
}
}
}, []);
// Register message handler and notify parent when ready
useEffect(() => {
messageHandler = handleMessage;
// Now that the handler is registered, notify parent that renderer is ready
// This ensures messages won't be dropped due to race conditions
window.parent.postMessage({ type: "renderer_ready" }, "*");
return () => {
messageHandler = null;
};
}, [handleMessage]);
return (
<div
className="isolated-renderer"
data-theme={state.isDark ? "dark" : "light"}
>
{state.outputs.map((entry, index) => (
<ErrorBoundary
key={entry.id}
resetKeys={[JSON.stringify(entry.payload)]}
fallback={(error, reset) => (
<OutputErrorFallback
error={error}
outputIndex={index}
onRetry={reset}
/>
)}
onError={(error, errorInfo) => {
// Send error back to parent
window.parent.postMessage(
{
type: "error",
payload: {
message: error.message,
stack: errorInfo.componentStack,
},
},
"*",
);
}}
>
<OutputRenderer payload={entry.payload} />
</ErrorBoundary>
))}
</div>
);
}
/**
* Render a single output based on its MIME type.
* Uses direct component imports (not lazy loading) for isolated iframe compatibility.
*/
function OutputRenderer({ payload }: { payload: RenderPayload }) {
const { mimeType, data, metadata } = payload;
const content = data;
// Handle stream output (plain text with potential ANSI)
if (mimeType === "text/plain" && metadata?.streamName) {
return (
<AnsiStreamOutput
text={String(data)}
streamName={metadata.streamName as "stdout" | "stderr"}
/>
);
}
// Handle error output
if (mimeType === "text/plain" && metadata?.isError) {
return (
<AnsiErrorOutput
ename={String(metadata.ename || "Error")}
evalue={String(metadata.evalue || "")}
traceback={
Array.isArray(metadata.traceback)
? metadata.traceback.map(String)
: [String(data)]
}
/>
);
}
// Route to appropriate component based on MIME type
// (Direct rendering without MediaRouter's lazy loading)
// Widget view - render interactive Jupyter widget
if (mimeType === "application/vnd.jupyter.widget-view+json") {
const widgetData = data as { model_id: string };
return <WidgetView modelId={widgetData.model_id} />;
}
// Markdown
if (mimeType === "text/markdown") {
return <MarkdownOutput content={String(content)} />;
}
// HTML
if (mimeType === "text/html") {
return <HtmlOutput content={String(content)} />;
}
// SVG
if (mimeType === "image/svg+xml") {
return <SvgOutput data={String(content)} />;
}
// Images (PNG, JPEG, GIF, WebP)
if (mimeType.startsWith("image/")) {
return (
<ImageOutput
data={String(content)}
mediaType={
mimeType as "image/png" | "image/jpeg" | "image/gif" | "image/webp"
}
width={metadata?.width as number | undefined}
height={metadata?.height as number | undefined}
/>
);
}
// JSON
if (mimeType === "application/json") {
const jsonData =
typeof content === "string" ? JSON.parse(content) : content;
return <JsonOutput data={jsonData} />;
}
// Plain text / ANSI
if (mimeType === "text/plain") {
return <AnsiOutput>{String(content)}</AnsiOutput>;
}
// Fallback: render as plain text
return (
<pre style={{ whiteSpace: "pre-wrap", wordWrap: "break-word" }}>
{typeof content === "string" ? content : JSON.stringify(content, null, 2)}
</pre>
);
}
// --- Bootstrap ---
let root: Root | null = null;
/**
* Initialize the renderer. Called when the bundle is eval'd in the iframe.
*/
// Declare the global flag type for TypeScript
declare global {
interface Window {
__REACT_RENDERER_ACTIVE__?: boolean;
}
}
export function init() {
// Signal to the inline handler that React is taking over
// This prevents the inline handler from processing render/theme/clear messages
window.__REACT_RENDERER_ACTIVE__ = true;
// Set up message listener
setupMessageListener();
// Initialize theme to dark (default) - will be updated by parent if needed
updateDocumentTheme(true);
// Create root element if needed
let rootEl = document.getElementById("root");
if (!rootEl) {
rootEl = document.createElement("div");
rootEl.id = "root";
document.body.appendChild(rootEl);
}
// Create React root and render with widget provider
root = createRoot(rootEl);
root.render(
<StrictMode>
<IframeWidgetStoreProvider>
<IsolatedRendererApp />
</IframeWidgetStoreProvider>
</StrictMode>,
);
// Set up resize observer
const resizeObserver = new ResizeObserver(() => {
window.parent.postMessage(
{ type: "resize", payload: { height: document.body.scrollHeight } },
"*",
);
});
resizeObserver.observe(document.body);
// Note: "renderer_ready" is sent from the React component's useEffect
// to ensure the message handler is registered before parent sends messages
}
// Auto-init if this is the main module being eval'd
// The parent will send us via eval, so we auto-start
if (typeof window !== "undefined") {
// Check if we're being eval'd (window.currentMessage exists from bootstrap)
// If so, init immediately. Otherwise, export for manual init.
init();
}
registry/isolated-renderer/widget-provider.tsx
/**
* Widget Provider for Isolated Iframe
*
* This module provides the same React context interface as the main app's
* WidgetStoreProvider, but uses the CommBridgeClient to proxy communication
* through postMessage to the parent window.
*
* This allows existing widget components to work unchanged inside the iframe.
* We provide to both the main WidgetStoreContext (so WidgetView works) and
* our own IframeWidgetStoreContext (for iframe-specific code if needed).
*/
import {
createContext,
type ReactNode,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useSyncExternalStore,
} from "react";
import { createCanvasManagerRouter } from "@/registry/widgets/canvas-manager-subscriptions";
import { createLinkManager } from "@/registry/widgets/link-subscriptions";
import {
resolveModelRef,
type WidgetModel,
type WidgetStore,
} from "@/registry/widgets/widget-store";
import { WidgetStoreContext } from "@/registry/widgets/widget-store-context";
import {
createWidgetBridgeClient,
type WidgetBridgeClient,
} from "./widget-bridge-client";
// === Context Types ===
interface IframeWidgetStoreContextValue {
store: WidgetStore;
/** Send a state update to the parent (which forwards to kernel) */
sendUpdate: (
commId: string,
state: Record<string, unknown>,
buffers?: ArrayBuffer[],
) => void;
/** Send a custom message to the parent (which forwards to kernel) */
sendCustom: (
commId: string,
content: Record<string, unknown>,
buffers?: ArrayBuffer[],
) => void;
/** Close a comm channel via parent */
closeComm: (commId: string) => void;
}
// === Context ===
const IframeWidgetStoreContext =
createContext<IframeWidgetStoreContextValue | null>(null);
// === Provider ===
interface IframeWidgetStoreProviderProps {
children: ReactNode;
}
/**
* Provider component for widgets in the isolated iframe.
*
* Creates a CommBridgeClient that:
* - Maintains a local WidgetStore for state
* - Receives comm messages from parent via postMessage
* - Sends state updates/custom messages back to parent
*
* Also provides to WidgetStoreContext so existing widget components work unchanged.
*/
export function IframeWidgetStoreProvider({
children,
}: IframeWidgetStoreProviderProps) {
// Create bridge client once
const clientRef = useRef<WidgetBridgeClient | null>(null);
if (!clientRef.current) {
clientRef.current = createWidgetBridgeClient();
}
const client = clientRef.current;
// Clean up on unmount
useEffect(() => {
return () => {
client.dispose();
};
}, [client]);
// Set up link subscriptions (jslink/jsdlink)
// Pass sendUpdate so linked values sync back to the kernel via the parent
useEffect(
() => createLinkManager(client.store, client.sendUpdate),
[client.store, client.sendUpdate],
);
// Set up canvas manager router (ipycanvas)
useEffect(() => createCanvasManagerRouter(client.store), [client.store]);
// Value for iframe-specific context
const iframeValue = useMemo<IframeWidgetStoreContextValue>(
() => ({
store: client.store,
sendUpdate: client.sendUpdate,
sendCustom: client.sendCustom,
closeComm: client.closeComm,
}),
[client],
);
// Value for main WidgetStoreContext (so existing widget components work)
// sendMessage parses Jupyter protocol and routes to bridge functions
const mainContextValue = useMemo(
() => ({
store: client.store,
handleMessage: () => {
// No-op: messages come through postMessage in iframe
},
sendMessage: (msg: {
content: {
comm_id?: string;
data?: {
method?: string;
state?: Record<string, unknown>;
content?: Record<string, unknown>;
};
};
buffers?: ArrayBuffer[];
}) => {
// Route Jupyter protocol messages to bridge functions
const commId = msg.content?.comm_id;
const method = msg.content?.data?.method;
const buffers = msg.buffers;
if (!commId) return;
if (method === "update" && msg.content?.data?.state) {
client.sendUpdate(commId, msg.content.data.state, buffers);
} else if (method === "custom" && msg.content?.data?.content) {
client.sendCustom(commId, msg.content.data.content, buffers);
}
},
sendUpdate: client.sendUpdate,
sendCustom: client.sendCustom,
closeComm: client.closeComm,
}),
[client],
);
return (
<WidgetStoreContext.Provider value={mainContextValue}>
<IframeWidgetStoreContext.Provider value={iframeValue}>
{children}
</IframeWidgetStoreContext.Provider>
</WidgetStoreContext.Provider>
);
}
// === Hooks ===
/**
* Access the iframe widget store context.
* Returns null if used outside of IframeWidgetStoreProvider.
*/
export function useIframeWidgetStore(): IframeWidgetStoreContextValue | null {
return useContext(IframeWidgetStoreContext);
}
/**
* Access the iframe widget store context, throwing if not available.
*/
export function useIframeWidgetStoreRequired(): IframeWidgetStoreContextValue {
const ctx = useContext(IframeWidgetStoreContext);
if (!ctx) {
throw new Error(
"useIframeWidgetStoreRequired must be used within IframeWidgetStoreProvider",
);
}
return ctx;
}
/**
* Subscribe to all widget models.
*/
export function useIframeWidgetModels(): Map<string, WidgetModel> {
const { store } = useIframeWidgetStoreRequired();
return useSyncExternalStore(
store.subscribe,
store.getSnapshot,
store.getSnapshot,
);
}
/**
* Subscribe to a specific widget model.
*/
export function useIframeWidgetModel(modelId: string): WidgetModel | undefined {
const { store } = useIframeWidgetStoreRequired();
const subscribe = useCallback(
(callback: () => void) => store.subscribe(callback),
[store],
);
const getSnapshot = useCallback(
() => store.getModel(modelId),
[store, modelId],
);
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
}
/**
* Subscribe to a specific key in a widget model's state.
*/
export function useIframeWidgetModelValue<T = unknown>(
modelId: string,
key: string,
): T | undefined {
const { store } = useIframeWidgetStoreRequired();
const subscribe = useCallback(
(callback: () => void) => store.subscribeToKey(modelId, key, callback),
[store, modelId, key],
);
const getSnapshot = useCallback(
() => store.getModel(modelId)?.state[key] as T | undefined,
[store, modelId, key],
);
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
}
/**
* Get a value from a widget model, resolving IPY_MODEL_ references.
*/
export function useIframeResolvedModelValue<T = unknown>(
modelId: string,
key: string,
): T | WidgetModel | undefined {
const { store } = useIframeWidgetStoreRequired();
const value = useIframeWidgetModelValue(modelId, key);
const resolved = resolveModelRef(value, (id) => store.getModel(id));
return resolved as T | WidgetModel | undefined;
}
/**
* Check if a widget model was explicitly closed.
*/
export function useIframeWasWidgetClosed(modelId: string): boolean {
const { store } = useIframeWidgetStoreRequired();
return store.wasModelClosed(modelId);
}
registry/isolated-renderer/widget-bridge-client.ts
/**
* Widget Bridge Client - Iframe Side
*
* This module runs inside the isolated iframe and manages widget communication
* with the parent window. It:
* - Creates a local WidgetStore for widget state management
* - Listens for comm_open/comm_msg/comm_close from parent via postMessage
* - Provides methods to send state updates and custom messages back to parent
* - Sends `widget_ready` when initialized
*
* Security: This code runs in a sandboxed iframe with an opaque origin.
* It cannot access Tauri APIs, the parent DOM, or localStorage.
*/
import type {
CommCloseMessage,
CommMsgMessage,
CommOpenMessage,
CommSyncMessage,
WidgetCommCloseMessage,
WidgetCommMsgMessage,
} from "@/registry/outputs/isolated/frame-bridge";
import {
createWidgetStore,
type WidgetStore,
} from "@/registry/widgets/widget-store";
// Type for method parameter in comm messages
type CommMethod = "update" | "custom";
/**
* Interface for the widget bridge client.
* Provides access to the local store and methods to communicate with parent.
*/
export interface WidgetBridgeClient {
/** The local widget store for this iframe */
store: WidgetStore;
/**
* Send a state update to the parent (to be forwarded to kernel).
* Called when a widget's state changes due to user interaction.
*/
sendUpdate: (
commId: string,
state: Record<string, unknown>,
buffers?: ArrayBuffer[],
) => void;
/**
* Send a custom message to the parent (to be forwarded to kernel).
* Used for widget-specific protocols (e.g., ipycanvas draw commands).
*/
sendCustom: (
commId: string,
content: Record<string, unknown>,
buffers?: ArrayBuffer[],
) => void;
/**
* Request to close a comm (to be forwarded to kernel).
*/
closeComm: (commId: string) => void;
/**
* Clean up the bridge (remove event listeners).
*/
dispose: () => void;
}
/**
* Create a widget bridge client for the iframe.
* This sets up:
* - A local WidgetStore instance
* - Message listener for parent → iframe comm messages
* - Methods to send iframe → parent messages
*/
export function createWidgetBridgeClient(): WidgetBridgeClient {
// Create local widget store
const store = createWidgetStore();
// Message handler for parent → iframe messages
function handleMessage(event: MessageEvent) {
// Only accept messages from parent
if (event.source !== window.parent) return;
const message = event.data;
if (!message || typeof message.type !== "string") return;
switch (message.type) {
case "bridge_ready":
// Parent's comm bridge is ready, re-send widget_ready to trigger sync
sendWidgetReady();
break;
case "comm_open":
handleCommOpen(message as CommOpenMessage);
break;
case "comm_msg":
handleCommMsg(message as CommMsgMessage);
break;
case "comm_close":
handleCommClose(message as CommCloseMessage);
break;
case "comm_sync":
handleCommSync(message as CommSyncMessage);
break;
}
}
function sendWidgetReady() {
window.parent.postMessage({ type: "widget_ready" }, "*");
}
function handleCommOpen(msg: CommOpenMessage) {
const { commId, state, buffers } = msg.payload;
store.createModel(commId, state, buffers);
}
function handleCommMsg(msg: CommMsgMessage) {
const { commId, method, data, buffers } = msg.payload;
if (method === "update") {
// State update from kernel
store.updateModel(commId, data, buffers);
} else if (method === "custom") {
// Custom message from kernel (e.g., ipycanvas commands)
store.emitCustomMessage(commId, data, buffers);
}
}
function handleCommClose(msg: CommCloseMessage) {
const { commId } = msg.payload;
store.deleteModel(commId);
}
function handleCommSync(msg: CommSyncMessage) {
const { models } = msg.payload;
for (const model of models) {
store.createModel(model.commId, model.state, model.buffers);
}
}
// Set up message listener
window.addEventListener("message", handleMessage);
// Send initial widget_ready to parent
// (Parent may not be listening yet; it will send bridge_ready when ready, and we'll re-send)
sendWidgetReady();
return {
store,
sendUpdate(
commId: string,
state: Record<string, unknown>,
buffers?: ArrayBuffer[],
) {
// Update local store immediately for responsive UI (optimistic update)
store.updateModel(commId, state, buffers);
const msg: WidgetCommMsgMessage = {
type: "widget_comm_msg",
payload: {
commId,
method: "update" as CommMethod,
data: state,
buffers,
},
};
// Transfer buffers for efficiency (note: buffers are consumed by transfer)
window.parent.postMessage(msg, "*", buffers ?? []);
},
sendCustom(
commId: string,
content: Record<string, unknown>,
buffers?: ArrayBuffer[],
) {
const msg: WidgetCommMsgMessage = {
type: "widget_comm_msg",
payload: {
commId,
method: "custom" as CommMethod,
data: content,
buffers,
},
};
// Transfer buffers for efficiency
window.parent.postMessage(msg, "*", buffers ?? []);
},
closeComm(commId: string) {
const msg: WidgetCommCloseMessage = {
type: "widget_comm_close",
payload: { commId },
};
window.parent.postMessage(msg, "*");
},
dispose() {
window.removeEventListener("message", handleMessage);
},
};
}
registry/isolated-renderer/styles.css
/**
* Styles for the isolated renderer.
* Includes Tailwind CSS and theme variables matching the main app.
*/
@import "tailwindcss";
/* Source paths for Tailwind to scan for class names */
@source "../outputs/**/*.{ts,tsx}";
@source "../widgets/**/*.{ts,tsx}";
@source "../../components/ui/**/*.{ts,tsx}";
@source "./**/*.{ts,tsx}";
/* Dark mode variant */
@custom-variant dark (&:is(.dark *, [data-theme="dark"] *));
/* Tailwind v4 theme mapping (maps --color-* to CSS variables) */
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
}
/* Light mode theme variables (matching main app) */
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--radius: 0.625rem;
}
/* Dark mode theme variables (matching main app) */
.dark,
[data-theme="dark"] {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
}
/* Base styles */
* {
border-color: var(--border);
}
html, body {
margin: 0;
padding: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
line-height: 1.5;
background: transparent;
color: var(--foreground);
}
body {
padding: 8px;
}
/* Code font - uses Tailwind's text-sm to match main app output styling */
pre, code {
@apply font-mono text-sm;
}
/* Output item spacing */
.output-item {
margin-bottom: 8px;
}
/* Ensure images don't overflow */
img {
max-width: 100%;
height: auto;
}
/* Links */
a {
color: oklch(0.546 0.245 262.881);
text-decoration: none;
}
.dark a,
[data-theme="dark"] a {
color: oklch(0.707 0.165 254.624);
}
a:hover {
text-decoration: underline;
}
/* Error styling */
.error pre {
color: #ef4444;
background: #1a1010;
}
registry/isolated-renderer/vite.config.ts
import path from "node:path";
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
/**
* Vite configuration for building the isolated-renderer bundle.
*
* This creates a single IIFE bundle that can be:
* 1. Imported as a string
* 2. Sent to an isolated iframe via postMessage eval
* 3. Self-executes to initialize the React renderer
*
* The bundle includes React and all dependencies inline.
*/
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@/": `${path.resolve(__dirname, "../..")}/`,
},
},
build: {
// Output as a library in IIFE format (self-executing)
lib: {
entry: path.resolve(__dirname, "index.tsx"),
name: "IsolatedRenderer",
formats: ["iife"],
fileName: () => "isolated-renderer.js",
},
outDir: path.resolve(__dirname, "../../public/isolated"),
emptyOutDir: true,
// Inline all dependencies (don't externalize anything)
rollupOptions: {
output: {
// Ensure everything is in one file
inlineDynamicImports: true,
// Control asset naming (for CSS)
assetFileNames: "isolated-renderer.[ext]",
},
},
// Don't minify for easier debugging (can enable in production)
minify: false,
// Generate source maps for debugging
sourcemap: true,
},
// Define to help with React production build
define: {
"process.env.NODE_ENV": JSON.stringify("production"),
},
});