shadimage
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 · light

Registry 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"),
  },
});

More from nteract

Browse all components in this library →