feat: Design tab, live style editing, route-aware artboards, canvas onboarding

Design tab (4-tab inspector: Design / Props / Diff / Graph)
- Click any component in a live artboard to inspect computed CSS
- Typography, Layout, Visual sections auto-populated from getComputedStyle
- Every property is inline-editable; changes apply live via PATCH_ELEMENT_STYLE
- Zero source-code modifications — purely in-browser style overrides

Protocol additions (packages/renderer)
- REQUEST_ELEMENT_STYLES / ELEMENT_STYLES round-trip for CSS inspection
- PATCH_ELEMENT_STYLE for live style patching

Live SDK (packages/live-sdk)
- respondWithStyles(): reads 30 curated computed CSS properties
- patchElementStyle(): applies inline style override to fiber's DOM element

Canvas store
- selectedComponentStyles: current element's CSS map
- styleEditEvent mailbox: Design tab → LiveArtboard style dispatch (Zustand v5 compatible)

Route-aware artboards
- Each artboard now has an optional route (e.g. /dashboard)
- buildSrc(base, route) helper in Artboard.tsx
- Route field editable in Inspector Props tab, persisted to metadata_jsonb

On-canvas onboarding
- Empty canvas now shows 3-step guide: press A → CLI command → paste URL
- CLI command shown inline in a highlighted code block

Changelog and Handoff updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
SinachPat
2026-04-30 22:04:37 +01:00
co-authored by Claude Sonnet 4.6
parent 8514ffdc1a
commit edada3091e
10 changed files with 669 additions and 35 deletions
@@ -6,6 +6,7 @@ import {
isRendererEnvelope,
} from '@originmain/renderer';
import type { FiberNode, RendererMessage } from '@originmain/renderer';
import { useCanvas } from '@/store/canvas';
// ── Types ─────────────────────────────────────────────────────────────────────
@@ -26,6 +27,10 @@ export interface LiveArtboardProps {
onReady?: () => void;
onFiberTreeUpdate?: (root: FiberNode) => void;
onComponentSelected?: (nodeId: string) => void;
/** Called when the iframe responds with computed CSS properties for a selected element. */
onComponentStylesUpdate?: (nodeId: string, styles: Record<string, string>) => void;
/** Forwards a CSS property patch from the Design tab into the iframe. */
patchElementStyle?: (nodeId: string, property: string, value: string) => void;
style?: React.CSSProperties;
}
@@ -41,6 +46,7 @@ export function LiveArtboard({
onReady,
onFiberTreeUpdate,
onComponentSelected,
onComponentStylesUpdate,
style,
}: LiveArtboardProps) {
const iframeRef = useRef<HTMLIFrameElement>(null);
@@ -91,17 +97,22 @@ export function LiveArtboard({
break;
case 'COMPONENT_SELECTED':
onComponentSelected?.(msg.nodeId);
// Request computed styles so the Design tab can populate immediately.
sendMessage('REQUEST_ELEMENT_STYLES', { nodeId: msg.nodeId });
break;
case 'COMPONENT_DESELECTED':
// Renderer clicked empty space — clear the host-side selection.
onComponentSelected?.('');
break;
case 'ELEMENT_STYLES':
onComponentStylesUpdate?.(msg.nodeId, msg.styles);
break;
}
}
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [id, designTokens, selectedComponentId, sendMessage, onReady, onFiberTreeUpdate, onComponentSelected]);
}, [id, designTokens, selectedComponentId, sendMessage, onReady, onFiberTreeUpdate, onComponentSelected, onComponentStylesUpdate]);
// ── Push updated design tokens whenever they change ───────────────────────
useEffect(() => {
@@ -109,6 +120,23 @@ export function LiveArtboard({
sendMessage('SET_DESIGN_TOKENS', { tokens: designTokens });
}, [designTokens, sendMessage]);
// ── Style edit mailbox ─────────────────────────────────────────────────────
// Watches the Zustand mailbox for PATCH_ELEMENT_STYLE events addressed to
// this artboard and forwards them to the iframe immediately.
const styleEditEvent = useCanvas((s) => s.styleEditEvent);
const clearStyleEdit = useCanvas((s) => s.clearStyleEdit);
useEffect(() => {
if (styleEditEvent?.artboardId === id && isReadyRef.current) {
sendMessage('PATCH_ELEMENT_STYLE', {
nodeId: styleEditEvent.nodeId,
property: styleEditEvent.property,
value: styleEditEvent.value,
});
clearStyleEdit();
}
}, [id, styleEditEvent, sendMessage, clearStyleEdit]);
// ── Sync selection changes into the iframe ────────────────────────────────
// Sends SELECT_COMPONENT on every selectedComponentId change so the blue
// highlight ring stays in sync with the canvas selection store.