// ── Source discriminants ────────────────────────────────────────────────────── // All postMessage envelopes carry a `source` field so the host and renderer can // ignore messages from unrelated parties (browser extensions, devtools, etc.). export const HOST_SOURCE = 'originmain-host' as const; export const RENDERER_SOURCE = 'originmain-renderer' as const; // ── Fiber node ──────────────────────────────────────────────────────────────── export interface FiberNode { id: string; name: string; props: Record; children: FiberNode[]; domRect?: DOMRectLike; /** JSX call site: the file + line where was written (not its definition). * Only present in React dev builds (__DEV__ = true). Absent in production. * Renamed from `sourceFile` to `callSite` for accuracy — see Phase 1 §4.2. */ callSite?: { fileName: string; lineNumber: number; columnNumber?: number; }; } export interface DOMRectLike { x: number; y: number; width: number; height: number; } // ── Host → Renderer messages ────────────────────────────────────────────────── export type HostMessage = | { type: 'SET_DESIGN_TOKENS'; tokens: Record } | { type: 'NAVIGATE'; path: string } | { type: 'SELECT_COMPONENT'; nodeId: string } | { type: 'DESELECT' } /** Ask the renderer to respond with computed CSS properties for a fiber node. */ | { type: 'REQUEST_ELEMENT_STYLES'; nodeId: string } /** Apply a single CSS property override directly to the component's DOM element. * Non-destructive — sets inline style only; source files are unchanged. */ | { type: 'PATCH_ELEMENT_STYLE'; nodeId: string; property: string; value: string } /** Apply a CSS property to all matching direct children of a node. * Used for paragraph-spacing: patches margin-bottom on each direct

child. */ | { type: 'PATCH_CHILDREN_STYLE'; parentNodeId: string; selector: string; property: string; value: string } /** Hide a component's DOM element (sets display:none). Non-destructive. */ | { type: 'REMOVE_ELEMENT'; nodeId: string } /** Phase 0: Ask the renderer to capture a JPEG thumbnail via html2canvas and post THUMBNAIL_READY. * Sent when an artboard transitions from Active → Near/Far in the viewport culling system. */ | { type: 'CAPTURE_THUMBNAIL' } /** Phase 0: Re-render a component isolation artboard with new props (live preview, no code change). * The iframe sets window.__OM_ISO_PROPS__ and calls window.__OM_ISO_RENDER__(). */ | { type: 'UPDATE_ISOLATION_PROPS'; props: Record } /** Phase 4: Ask the renderer to capture a PNG snapshot of the selected element via html2canvas. * Sent when the user hovers a component > 200ms or clicks "Preview Code Change". */ | { type: 'CAPTURE_SNAPSHOT'; nodeId: string } /** Phase 4: Cancel an in-flight snapshot capture (superseded by a newer request). */ | { type: 'CANCEL_SNAPSHOT' }; export interface HostEnvelope { source: typeof HOST_SOURCE; artboardId: string; message: HostMessage; } // ── Renderer → Host messages ────────────────────────────────────────────────── export type RendererMessage = /** Phase 0/6: Sent once the fiber hook is initialised and the React runtime is detected. * rootFontSizePx is read via getComputedStyle(document.documentElement).fontSize so the * canvas can normalise rem values to px for token matching (Phase 6). */ | { type: 'READY'; rootFontSizePx?: number } | { type: 'FIBER_TREE_UPDATE'; root: FiberNode } | { type: 'COMPONENT_SELECTED'; nodeId: string; nodeName?: string; rect: DOMRectLike } | { type: 'COMPONENT_DESELECTED' } | { type: 'ERROR'; message: string } /** Response to REQUEST_ELEMENT_STYLES — computed CSS properties for the node. * Also includes structural flags used by the Typography section of the Design Panel. */ | { type: 'ELEMENT_STYLES'; nodeId: string; styles: Record; /** true if the DOM element has a direct TEXT_NODE child with non-whitespace content */ hasDirectText: boolean; /** true if the DOM element has at least one direct

child */ hasParagraphChildren: boolean; } /** All discoverable routes found in the running app — sent once after READY * and again after each SPA navigation. */ | { type: 'ROUTES_DISCOVERED'; routes: Array<{ path: string; label: string }> } /** Phase 0: Response to CAPTURE_THUMBNAIL — base64 JPEG data URL, or null on failure. * The canvas stores the data URL in Zustand and uploads to Supabase Storage. */ | { type: 'THUMBNAIL_READY'; dataUrl: string | null } /** Phase 4: Response to CAPTURE_SNAPSHOT — base64 PNG of the selected element, or null. */ | { type: 'SNAPSHOT_READY'; dataUrl: string | null; nodeId: string }; export interface RendererEnvelope { source: typeof RENDERER_SOURCE; artboardId: string; message: RendererMessage; } // ── Type guards ─────────────────────────────────────────────────────────────── export function isHostEnvelope(data: unknown): data is HostEnvelope { return ( typeof data === 'object' && data !== null && (data as HostEnvelope).source === HOST_SOURCE ); } export function isRendererEnvelope(data: unknown): data is RendererEnvelope { return ( typeof data === 'object' && data !== null && (data as RendererEnvelope).source === RENDERER_SOURCE ); } // ── Helpers ─────────────────────────────────────────────────────────────────── export function createHostEnvelope( artboardId: string, message: HostMessage ): HostEnvelope { return { source: HOST_SOURCE, artboardId, message }; } export function createRendererEnvelope( artboardId: string, message: RendererMessage ): RendererEnvelope { return { source: RENDERER_SOURCE, artboardId, message }; }