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>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
edada3091e
commit
82dddb1801
@@ -29,8 +29,8 @@ export interface LiveArtboardProps {
|
||||
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;
|
||||
/** Called when the iframe discovers routes in the running app. */
|
||||
onRoutesDiscovered?: (routes: Array<{ path: string; label: string }>) => void;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ export function LiveArtboard({
|
||||
onFiberTreeUpdate,
|
||||
onComponentSelected,
|
||||
onComponentStylesUpdate,
|
||||
onRoutesDiscovered,
|
||||
style,
|
||||
}: LiveArtboardProps) {
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
@@ -107,12 +108,15 @@ export function LiveArtboard({
|
||||
case 'ELEMENT_STYLES':
|
||||
onComponentStylesUpdate?.(msg.nodeId, msg.styles);
|
||||
break;
|
||||
case 'ROUTES_DISCOVERED':
|
||||
onRoutesDiscovered?.(msg.routes);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', handleMessage);
|
||||
return () => window.removeEventListener('message', handleMessage);
|
||||
}, [id, designTokens, selectedComponentId, sendMessage, onReady, onFiberTreeUpdate, onComponentSelected, onComponentStylesUpdate]);
|
||||
}, [id, designTokens, selectedComponentId, sendMessage, onReady, onFiberTreeUpdate, onComponentSelected, onComponentStylesUpdate, onRoutesDiscovered]);
|
||||
|
||||
// ── Push updated design tokens whenever they change ───────────────────────
|
||||
useEffect(() => {
|
||||
@@ -125,6 +129,8 @@ export function LiveArtboard({
|
||||
// this artboard and forwards them to the iframe immediately.
|
||||
const styleEditEvent = useCanvas((s) => s.styleEditEvent);
|
||||
const clearStyleEdit = useCanvas((s) => s.clearStyleEdit);
|
||||
const removeElementEvent = useCanvas((s) => s.removeElementEvent);
|
||||
const clearRemoveElement = useCanvas((s) => s.clearRemoveElement);
|
||||
|
||||
useEffect(() => {
|
||||
if (styleEditEvent?.artboardId === id && isReadyRef.current) {
|
||||
@@ -137,6 +143,13 @@ export function LiveArtboard({
|
||||
}
|
||||
}, [id, styleEditEvent, sendMessage, clearStyleEdit]);
|
||||
|
||||
useEffect(() => {
|
||||
if (removeElementEvent?.artboardId === id && isReadyRef.current) {
|
||||
sendMessage('REMOVE_ELEMENT', { nodeId: removeElementEvent.nodeId });
|
||||
clearRemoveElement();
|
||||
}
|
||||
}, [id, removeElementEvent, sendMessage, clearRemoveElement]);
|
||||
|
||||
// ── 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.
|
||||
|
||||
Reference in New Issue
Block a user