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
@@ -17,6 +17,17 @@ interface ArtboardProps {
width: number;
height: number;
renderUrl?: string;
/** Route path appended to renderUrl so each artboard can show a different screen. */
route?: string;
}
/** Builds the iframe src from a base URL + optional route path.
* e.g. ("http://localhost:4170/", "/dashboard") → "http://localhost:4170/dashboard" */
function buildSrc(base: string, route?: string): string {
if (!route || route === '/' || route === '') return base;
const trimmed = base.replace(/\/$/, '');
const path = route.startsWith('/') ? route : '/' + route;
return trimmed + path;
}
// Status color map matching the inspector
@@ -27,10 +38,10 @@ const DIFF_STATUS_BADGE: Record<string, { color: string; bg: string; label: stri
REJECTED: { color: '#FF8080', bg: 'rgba(255,128,128,0.15)', label: 'blocked' },
};
export function Artboard({ id, label, x, y, width, height, renderUrl }: ArtboardProps) {
export function Artboard({ id, label, x, y, width, height, renderUrl, route }: ArtboardProps) {
const {
selectedArtboardId, selectArtboard, workspaceId, projectId,
setArtboardLive, setFiberRoot, selectComponent,
setArtboardLive, setFiberRoot, selectComponent, setComponentStyles,
selectedComponentId,
} = useCanvas();
const selected = selectedArtboardId === id;
@@ -49,10 +60,23 @@ export function Artboard({ id, label, x, y, width, height, renderUrl }: Artboard
}, [id, setFiberRoot, setArtboardLive]);
const handleComponentSelected = useCallback((nodeId: string) => {
if (!nodeId) {
// Empty nodeId = COMPONENT_DESELECTED — clear selection + styles
selectComponent(null, null);
setComponentStyles(null);
return;
}
if (!localFiberRoot) return;
const node = findFiberNode(localFiberRoot, nodeId);
selectComponent(nodeId, node ?? null);
}, [localFiberRoot, selectComponent]);
}, [localFiberRoot, selectComponent, setComponentStyles]);
const handleComponentStylesUpdate = useCallback((_nodeId: string, styles: Record<string, string>) => {
// Only update styles if this artboard is the one currently selected
if (selectedArtboardId === id) {
setComponentStyles(styles);
}
}, [id, selectedArtboardId, setComponentStyles]);
// ── Drag to reposition ─────────────────────────────────────────────────────
const isDragging = useRef(false);
@@ -318,12 +342,13 @@ export function Artboard({ id, label, x, y, width, height, renderUrl }: Artboard
<>
<LiveArtboard
id={id}
src={renderUrl}
src={buildSrc(renderUrl, route)}
width={width}
height={height}
selectedComponentId={selectedComponentId}
onFiberTreeUpdate={handleFiberUpdate}
onComponentSelected={handleComponentSelected}
onComponentStylesUpdate={handleComponentStylesUpdate}
/>
<SelectionOverlay
artboardId={id}