Files
wursor/packages/renderer/src/protocol.ts
T
SinachPatandClaude Sonnet 4.6 82dddb1801 feat: Figma-parity design editing — visual panel, live resize, route grid
Inspector:
- Design tab rebuilt with semantic Figma-style layout: W/H stepper inputs
  at top, Fill with colour-picker swatch, Typography row (size/weight/
  line-height), text-align toggle group, Layout section with flex controls
  + padding 4-corner grid, Appearance (radius/opacity/border/shadow)
- All fields fire patchStyleEdit on every keystroke / arrow-key step
- Sections conditionally render: Fill hidden when bg is transparent, etc.
- NumInput: strips/restores CSS unit, Shift+↑↓ for ×10 step
- ColorInput: native colour picker behind swatch + hex text input

SelectionOverlay:
- Resize handles call patchStyleEdit on every mousemove → live iframe preview
- 8 handles: 4 corners (nwse/nesw) + 4 edge midpoints (ns/ew)
- Delete button in label bar + Delete/Backspace keyboard shortcut
- Dimension tooltip: ComponentName W × H

Canvas / Artboard:
- ROUTES_DISCOVERED handler: auto-creates artboards in a row for every
  undiscovered route, pendingRouteCreation ref prevents duplicates
- buildSrc() helper resolves route-aware iframe src

live-sdk / hook:
- discoverRoutes(): scans <a href> + fiber tree for Link/NavLink components
- removeElement(), patchElementStyle(), respondWithStyles() handlers
- Route re-discovery on popstate

protocol: REQUEST_ELEMENT_STYLES, PATCH_ELEMENT_STYLE, REMOVE_ELEMENT host
messages; ELEMENT_STYLES, ROUTES_DISCOVERED renderer messages

canvas store: styleEditEvent + removeElementEvent mailboxes (Zustand v5
compatible — useEffect selectors, no 2-arg subscribe)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 01:05:16 +01:00

99 lines
4.0 KiB
TypeScript

// ── 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<string, unknown>;
children: FiberNode[];
domRect?: DOMRectLike;
}
export interface DOMRectLike {
x: number;
y: number;
width: number;
height: number;
}
// ── Host → Renderer messages ──────────────────────────────────────────────────
export type HostMessage =
| { type: 'SET_DESIGN_TOKENS'; tokens: Record<string, string> }
| { 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 }
/** Hide a component's DOM element (sets display:none). Non-destructive. */
| { type: 'REMOVE_ELEMENT'; nodeId: string };
export interface HostEnvelope {
source: typeof HOST_SOURCE;
artboardId: string;
message: HostMessage;
}
// ── Renderer → Host messages ──────────────────────────────────────────────────
export type RendererMessage =
| { type: 'READY' }
| { type: 'FIBER_TREE_UPDATE'; root: FiberNode }
| { type: 'COMPONENT_SELECTED'; nodeId: string; rect: DOMRectLike }
| { type: 'COMPONENT_DESELECTED' }
| { type: 'ERROR'; message: string }
/** Response to REQUEST_ELEMENT_STYLES — computed CSS properties for the node. */
| { type: 'ELEMENT_STYLES'; nodeId: string; styles: Record<string, string> }
/** 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 }> };
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 };
}