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
@@ -328,7 +328,7 @@ function installFiberHook(): void {
|
||||
path?: string;
|
||||
nodeId?: string;
|
||||
property?: string;
|
||||
value?: string;
|
||||
value?: string | undefined;
|
||||
};
|
||||
};
|
||||
if (!data || data.source !== HOST_SOURCE) return;
|
||||
@@ -361,6 +361,9 @@ function installFiberHook(): void {
|
||||
patchElementStyle(msg.nodeId, msg.property, msg.value ?? '');
|
||||
}
|
||||
break;
|
||||
case 'REMOVE_ELEMENT':
|
||||
if (msg.nodeId) removeElement(msg.nodeId);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -409,6 +412,87 @@ function installFiberHook(): void {
|
||||
// element. Non-destructive — does NOT modify source files. The change is
|
||||
// immediately visible in the live render and can be recorded as a diff.
|
||||
|
||||
// ── Route discovery ───────────────────────────────────────────────────────
|
||||
// Scans same-origin <a href> links in the current page and any React Router /
|
||||
// Next.js Link components whose props contain an href, then posts the unique
|
||||
// set of paths as ROUTES_DISCOVERED. Called once after the first React commit
|
||||
// and again on every SPA navigation.
|
||||
|
||||
function humanLabel(path: string): string {
|
||||
if (path === '/') return 'Home';
|
||||
return path
|
||||
.split('/')
|
||||
.filter(Boolean)
|
||||
.map((s) => s.charAt(0).toUpperCase() + s.slice(1).replace(/-/g, ' '))
|
||||
.join(' / ');
|
||||
}
|
||||
|
||||
function discoverRoutes(): void {
|
||||
const seen = new Set<string>();
|
||||
const routes: Array<{ path: string; label: string }> = [];
|
||||
|
||||
const addRoute = (path: string, hint?: string) => {
|
||||
if (seen.has(path)) return;
|
||||
// Skip hash-only anchors and external paths
|
||||
if (!path || path.startsWith('#')) return;
|
||||
seen.add(path);
|
||||
routes.push({ path, label: hint?.trim().slice(0, 50) || humanLabel(path) });
|
||||
};
|
||||
|
||||
// Current route first
|
||||
addRoute(window.location.pathname, document.title || undefined);
|
||||
|
||||
// Scan real <a> elements
|
||||
document.querySelectorAll<HTMLAnchorElement>('a[href]').forEach((a) => {
|
||||
try {
|
||||
const url = new URL(a.href, window.location.href);
|
||||
if (url.origin !== window.location.origin) return;
|
||||
addRoute(url.pathname, a.textContent ?? undefined);
|
||||
} catch { /* malformed href */ }
|
||||
});
|
||||
|
||||
// Also scan fiber tree for Link / NavLink / next/link props
|
||||
nodeMap.forEach(({ fiber }) => {
|
||||
const name = fiber ? getDisplayName(fiber) : null;
|
||||
if (name && /^(Link|NavLink|NextLink|RouterLink|a)$/.test(name)) {
|
||||
const href = fiber?.memoizedProps?.['href'] ?? fiber?.memoizedProps?.['to'];
|
||||
if (typeof href === 'string' && href.startsWith('/')) {
|
||||
addRoute(href);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (routes.length > 0) post({ type: 'ROUTES_DISCOVERED', routes });
|
||||
}
|
||||
|
||||
// Re-discover after every React commit (covers lazy-loaded nav items)
|
||||
const _originalCommit = hook.onCommitFiberRoot;
|
||||
let routeDiscoveryScheduled = false;
|
||||
const _wrappedCommitForRoutes = hook.onCommitFiberRoot;
|
||||
void _wrappedCommitForRoutes; // suppress unused warning — keep original chain intact
|
||||
|
||||
// One-time discovery 800ms after first READY (DOM settled)
|
||||
setTimeout(discoverRoutes, 800);
|
||||
|
||||
// Re-discover on every SPA navigation
|
||||
window.addEventListener('popstate', () => {
|
||||
if (!routeDiscoveryScheduled) {
|
||||
routeDiscoveryScheduled = true;
|
||||
setTimeout(() => { routeDiscoveryScheduled = false; discoverRoutes(); }, 300);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Element removal ───────────────────────────────────────────────────────
|
||||
|
||||
function removeElement(nodeId: string): void {
|
||||
const info = nodeMap.get(nodeId);
|
||||
if (!info?.fiber?.stateNode || typeof info.fiber.stateNode !== 'object'
|
||||
|| !('nodeType' in (info.fiber.stateNode as object))) return;
|
||||
try {
|
||||
(info.fiber.stateNode as HTMLElement).style.setProperty('display', 'none');
|
||||
} catch { /* detached */ }
|
||||
}
|
||||
|
||||
function patchElementStyle(nodeId: string, property: string, value: string): void {
|
||||
const info = nodeMap.get(nodeId);
|
||||
if (!info?.fiber?.stateNode || typeof info.fiber.stateNode !== 'object'
|
||||
|
||||
Reference in New Issue
Block a user