Plugin UI

Build embedded widgets with the PomavoBridge: receive context and the host token, react to theme changes, and auto-size your sandboxed iframe.

How Widgets Run

A widget is a normal web page that the host loads into a sandboxed iframe. Because it's isolated, it cannot reach into the host - instead it talks to Pomavo over a small postMessage protocol. The PomavoBridge from @pomavo/sdk/client wraps that protocol so you work with promises and callbacks instead of raw messages.

TypeScripttypescript
import { PomavoBridge } from '@pomavo/sdk/client';
 
const bridge = new PomavoBridge();

The Handshake

A widget follows the same four-step handshake every time it mounts:

Signal ready

Call bridge.signalReady(). This registers the message listener and tells the host the iframe is mounted and ready to receive context. Do this first, before requesting context, so the initial theme response isn't missed.

Request context

Call await bridge.requestContext(). The host replies with the token, your saved settings, and the current ticket.

Call your backend

Use context.token as a Bearer credential when fetching your own API. Your backend verifies it; see Lifecycle & Auth.

Auto-resize

Call bridge.enableAutoResize() so the iframe grows and shrinks to fit your content instead of showing a scrollbar.

The Bridge Protocol

Under the hood the bridge exchanges these messages. You rarely touch them directly, but it helps to know what's happening:

DirectionMessageMeaning
Plugin → Hostpomavo:readyThe iframe has mounted and is listening.
Plugin → Hostpomavo:request_contextAsk the host for token + settings.
Plugin → Hostpomavo:resizeReport the content's current height.
Host → Pluginpomavo:contextThe token, settings, and ticket context.
Host → Pluginpomavo:theme-changedThe active theme's colors and fonts.

The Context Object

requestContext() resolves to a PluginContext:

TypeScripttypescript
interface PluginContext {
  token: string;                       // HS256 JWT to send to your backend
  settings: Record<string, any> | null;// Values for your manifest parameters
  ticketSequenceNumber: string | null; // e.g. "BUG-123", or null off a ticket
}

The ticketSequenceNumber is decoded from the token itself, so it is as trustworthy as the signature. Use it to scope what your widget shows.

Reacting to Themes

The host application can switch themes at any time. Subscribe so your widget always matches the surrounding UI:

TypeScripttypescript
import { applyThemeForIframe } from '@pomavo/ui/theme';
 
bridge.onThemeChanged((theme) => {
  // theme: { mode, colors, fonts }
  applyThemeForIframe(theme.colors, theme.fonts);
});

onThemeChanged fires once right after signalReady() with the current theme, and again whenever the user changes it. You can also call bridge.applyThemeToDocument(theme) to write the CSS variables directly onto your document.

A Complete Widget

Here is a full React widget that loads ticket-scoped data from its own backend, follows the host theme, and auto-resizes:

Reacttsx
import { useEffect, useState } from 'react';
import { PomavoBridge, type PluginContext } from '@pomavo/sdk/client';
import { applyThemeForIframe } from '@pomavo/ui/theme';
 
export function PrWidget() {
  const [prs, setPrs] = useState<PullRequest[]>([]);
  const [error, setError] = useState<string | null>(null);
  const [loading, setLoading] = useState(true);
 
  useEffect(() => {
    const bridge = new PomavoBridge();
 
    // Keep the widget in sync with the host theme.
    bridge.onThemeChanged((theme) => applyThemeForIframe(theme.colors, theme.fonts));
 
    // Announce readiness, then ask for context.
    bridge.signalReady();
    bridge.requestContext().then(async (ctx: PluginContext) => {
      if (!ctx.ticketSequenceNumber) {
        setError('Open this widget from a ticket.');
        setLoading(false);
        return;
      }
 
      try {
        const resp = await fetch(
          `/api/prs?ticket=${encodeURIComponent(ctx.ticketSequenceNumber)}`,
          { headers: { Authorization: `Bearer ${ctx.token}` } },
        );
        const data = await resp.json();
        setPrs(data.prs ?? []);
      } catch {
        setError('Failed to load pull requests.');
      } finally {
        setLoading(false);
      }
    });
 
    // Grow/shrink the iframe to fit the content.
    bridge.enableAutoResize();
 
    // Always clean up listeners on unmount.
    return () => bridge.destroy();
  }, []);
 
  if (loading) return <p>Loading…</p>;
  if (error) return <p>{error}</p>;
  return (
    <ul>
      {prs.map((pr) => (
        <li key={pr.number}>#{pr.number} {pr.title}</li>
      ))}
    </ul>
  );
}

PomavoBridge API

MethodDescription
signalReady()Register listeners and tell the host the widget is mounted.
requestContext()Promise resolving to { token, settings, ticketSequenceNumber }.
onThemeChanged(cb)Subscribe to theme changes; fires with the current theme immediately.
applyThemeToDocument(theme)Write a theme's CSS variables onto the document.
reportHeight(px)Manually report content height to the host.
enableAutoResize(el?)Observe the element, defaulting to document.body, and report height automatically.
getTicketSequenceNumber()The cached ticket from the last context.
getToken()The cached host token.
getSettings()The cached settings object.
destroy()Remove all listeners and observers. Call on unmount.

Pair @pomavo/sdk/client with @pomavo/ui to render widgets that look native to Pomavo. applyThemeForIframe from @pomavo/ui/theme maps the host's theme payload onto the same CSS tokens the rest of the app uses.

Registering a Widget

A widget only appears once it is declared in your manifest:

JSONjson
"widgets": [
  { "id": "prs", "title": "Pull Requests", "url": "https://my-plugin.example.com/widgets/prs", "defaultHeight": 320 }
]

The url must be reachable by the browser and serve your built widget bundle. The host embeds it with sandbox="allow-scripts allow-same-origin allow-popups" and passes the context once the iframe signals ready.

Next Steps