import type { Meta, StoryObj } from 'storybook-solidjs-vite'; import { createSignal, createEffect, onMount, onCleanup, type JSX } from 'solid-js'; import './remote'; import type { CardEnvelope, CardEvent, CardHost } from '../primitives/card-contract'; import { formRenderer, infoRenderer } from '../../examples/remote-provider/renderers'; declare module 'solid-js' { // eslint-disable-next-line @typescript-eslint/no-namespace namespace JSX { interface IntrinsicElements { 'kc-remote': JSX.HTMLAttributes & { src?: string; 'provider-origin'?: string; ref?: (el: HTMLElement) => void; }; } } } type RemoteEl = HTMLElement & { envelope?: CardEnvelope; policy?: Record }; /** * The live cross-origin demo only works in **local Storybook dev** (`npm run storybook`): * there, `.storybook/main.ts`'s Vite pipeline serves the reference provider at * `/remote-provider/`, and we frame it via the `127.0.0.1` alias of the SAME dev server * — a genuinely different origin from the `localhost` preview, so ``'s * cross-origin precondition (provider ≠ host) holds. * * A **static, single-origin deploy** (e.g. the GitHub Pages docs) cannot do this: there * is no second origin and the provider isn't served. So `liveProvider()` returns `null` * there and the stories render an explanatory panel instead of a doomed mount. The real * cross-origin behavior is verified by the standalone Playwright suite (H-L). */ function liveProvider(): { origin: string; src: string } | null { if (typeof window === 'undefined') return null; // The deployed/built Storybook (PROD) is single-origin + has no provider middleware. if (import.meta.env.PROD) return null; const here = new URL(window.location.href); if ((here.hostname !== 'localhost' && here.hostname !== '127.0.0.1') || !here.port) return null; // Swap localhost ↔ 127.0.0.1 to obtain a cross-origin sibling of the dev server. const alias = here.hostname === '127.0.0.1' ? 'localhost' : '127.0.0.1'; const origin = `${here.protocol}//${alias}:${here.port}`; return { origin, src: `${origin}/remote-provider/` }; } const FORM_ENVELOPE: CardEnvelope = { type: 'form', id: 'remote-form-1', title: 'Quick question', data: { type: 'object', required: ['email'], properties: { email: { type: 'string', title: 'Email', format: 'email' }, role: { type: 'string', title: 'Role', enum: ['Engineer', 'Designer', 'PM'] }, }, 'x-kc-submitLabel': 'Send', }, }; const WEATHER_ENVELOPE: CardEnvelope = { type: 'info', id: 'remote-info-1', title: 'San Francisco', data: { location: 'San Francisco', temperature: 18, unit: '°C', condition: 'Partly cloudy', humidity: 64, wind: 12, feelsLike: 17, forecast: [ { day: 'Mon', high: 19, low: 12 }, { day: 'Tue', high: 21, low: 13 }, { day: 'Wed', high: 17, low: 11 }, ], }, }; /** The Storybook theme mode, read the way the kit elements do (preview.ts mirrors * the resolved Storybook theme by toggling `.dark` on the ). Falls back to * the OS preference when not in the Storybook shell. */ function readThemeMode(): 'light' | 'dark' { if (typeof document === 'undefined') return 'light'; if (document.documentElement.classList.contains('dark')) return 'dark'; if (document.documentElement.classList.contains('light')) return 'light'; if (typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches) return 'dark'; return 'light'; } /** Pick the matching provider renderer for a card type (the SAME renderers the live * cross-origin provider registers), or null for an unhandled type. */ function rendererFor(type: string) { if (type === 'form') return formRenderer; if (type === 'info') return infoRenderer; return null; } /** Mounts a when a live cross-origin provider is available (local dev), * logs every routed CardEvent (via the bubbling kc-card). On a static/deployed * Storybook (no second origin) it renders the SAME provider renderer's output * DIRECTLY — so the docs show the real, interactive card — with a slim honest * banner explaining the production transport. */ function RemoteDemo(props: { envelope: CardEnvelope; src?: string; providerOrigin?: string; showEnvelope?: boolean }) { const live = props.providerOrigin && props.src ? { origin: props.providerOrigin, src: props.src } : liveProvider(); const [log, setLog] = createSignal([]); const [mode, setMode] = createSignal<'light' | 'dark'>(readThemeMode()); let el: RemoteEl | undefined; let container: HTMLDivElement | undefined; const renderer = rendererFor(props.envelope.type); // ── Live (local dev): cross-origin , exactly as before. ── onMount(() => { if (!live || !el) return; el.envelope = props.envelope; const onCard = (e: Event) => { const detail = (e as CustomEvent).detail; setLog((prev) => [...prev, detail]); }; el.addEventListener('kc-card', onCard); onCleanup(() => el?.removeEventListener('kc-card', onCard)); }); // ── Static (deployed): render the real card content directly, reusing the // provider renderer. Re-mount when the theme mode changes so the ThemePush // story still means something on static. ── if (!live && renderer) { // Track Storybook's light/dark toggle (preview.ts flips `.dark` on ). onMount(() => { if (typeof MutationObserver === 'undefined') return; const obs = new MutationObserver(() => setMode(readThemeMode())); obs.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); onCleanup(() => obs.disconnect()); }); createEffect(() => { const m = mode(); // re-run on theme change if (!container) return; const stubHost: CardHost = { context: () => ({ theme: { mode: m }, locale: 'en' }), emit: (e) => setLog((prev) => [...prev, e]), }; const dispose = renderer.mount(container, props.envelope, stubHost); onCleanup(() => { try { dispose(); } catch { /* renderer disposer threw — best-effort */ } if (container) container.replaceChildren(); }); }); } return (
{live ? ( (el = e as RemoteEl)} provider-origin={live.origin} src={live.src} /> ) : renderer ? (
(container = e)} style={{ border: '1px solid var(--color-border, #e4e4e7)', 'border-radius': '12px', overflow: 'hidden' }} />

Rendered directly for these static docs. In production <kc-remote> delivers this over a sandboxed cross-origin iframe — run npm run storybook for the live transport; the cross-origin model is verified by the Playwright suite.

) : (
Live cross-origin demo runs in local Storybook

<kc-remote> delivers this card over a sandboxed{' '} cross-origin <iframe>, which needs a second origin and the reference provider served by the dev server. Run{' '} npm run storybook to see it live; the cross-origin transport is verified by the Playwright e2e matrix.

)}
{props.showEnvelope && (
            {`// CardEnvelope sent down the wire (inside a WireFrame):\n` + JSON.stringify(props.envelope, null, 2)}
          
)}
          {log().length === 0 ? '// routed CardEvents appear here' : JSON.stringify(log(), null, 2)}
        
); } const HTML_SNIPPET = (envelope: CardEnvelope) => ` `; const meta = { title: 'Generative UI/Remote/kc-remote', tags: ['autodocs'], parameters: { layout: 'padded', // axe can't read a CROSS-ORIGIN frame's tree (that's the provider's a11y // obligation, proven by the provider's own stories/tests); scope axe to the // HOST page only — the iframe boundary + any inline fallback regions (H-L). a11y: { context: { exclude: [['iframe']] } }, docs: { description: { component: "`` mounts a **sandboxed cross-origin `