made some updates
This commit is contained in:
@@ -68,17 +68,24 @@ export function Artboard({ id, label, x, y, width, height, renderUrl, route, onR
|
|||||||
setComponentStyles(null);
|
setComponentStyles(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!localFiberRoot) return;
|
// If the fiber tree hasn't arrived yet, still commit the selection so
|
||||||
|
// REQUEST_ELEMENT_STYLES fires — just pass null for the fiber data.
|
||||||
|
if (!localFiberRoot) {
|
||||||
|
selectComponent(nodeId, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const node = findFiberNode(localFiberRoot, nodeId);
|
const node = findFiberNode(localFiberRoot, nodeId);
|
||||||
selectComponent(nodeId, node ?? null);
|
selectComponent(nodeId, node ?? null);
|
||||||
}, [localFiberRoot, selectComponent, setComponentStyles]);
|
}, [localFiberRoot, selectComponent, setComponentStyles]);
|
||||||
|
|
||||||
const handleComponentStylesUpdate = useCallback((_nodeId: string, styles: Record<string, string>) => {
|
const handleComponentStylesUpdate = useCallback((nodeId: string, styles: Record<string, string>) => {
|
||||||
// Only update styles if this artboard is the one currently selected
|
// Guard against stale responses arriving after the user has already clicked
|
||||||
if (selectedArtboardId === id) {
|
// a different component — only apply if artboard and node both still match.
|
||||||
|
const { selectedComponentId: currentId, selectedArtboardId: currentArtboard } = useCanvas.getState();
|
||||||
|
if (currentArtboard === id && currentId === nodeId) {
|
||||||
setComponentStyles(styles);
|
setComponentStyles(styles);
|
||||||
}
|
}
|
||||||
}, [id, selectedArtboardId, setComponentStyles]);
|
}, [id, setComponentStyles]);
|
||||||
|
|
||||||
// ── Drag to reposition ─────────────────────────────────────────────────────
|
// ── Drag to reposition ─────────────────────────────────────────────────────
|
||||||
const isDragging = useRef(false);
|
const isDragging = useRef(false);
|
||||||
@@ -359,6 +366,7 @@ export function Artboard({ id, label, x, y, width, height, renderUrl, route, onR
|
|||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
onSelectionChange={(sel) => {
|
onSelectionChange={(sel) => {
|
||||||
|
if (sel) selectArtboard(id);
|
||||||
handleComponentSelected(sel?.nodeId ?? '');
|
handleComponentSelected(sel?.nodeId ?? '');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -288,15 +288,75 @@ export function Canvas() {
|
|||||||
|
|
||||||
{/* Empty canvas onboarding — shown only when the project has no artboards yet */}
|
{/* Empty canvas onboarding — shown only when the project has no artboards yet */}
|
||||||
{artboards.length === 0 && (
|
{artboards.length === 0 && (
|
||||||
|
<UrlOnboardingOverlay
|
||||||
|
workspaceId={workspaceId}
|
||||||
|
projectId={projectId}
|
||||||
|
queryClient={queryClient}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── URL onboarding overlay ───────────────────────────────── */
|
||||||
|
// Shown when the canvas has no artboards. Lets the user paste their CLI proxy
|
||||||
|
// URL to auto-create the first artboard; route discovery will then fire and
|
||||||
|
// populate the remaining pages automatically.
|
||||||
|
function UrlOnboardingOverlay({
|
||||||
|
workspaceId,
|
||||||
|
projectId,
|
||||||
|
queryClient,
|
||||||
|
}: {
|
||||||
|
workspaceId: string | null;
|
||||||
|
projectId: string | null;
|
||||||
|
queryClient: ReturnType<typeof useQueryClient>;
|
||||||
|
}) {
|
||||||
|
const [url, setUrl] = useState('');
|
||||||
|
const [status, setStatus] = useState<'idle' | 'loading' | 'error'>('idle');
|
||||||
|
const [errorMsg, setErrorMsg] = useState('');
|
||||||
|
|
||||||
|
const handleConnect = useCallback(async () => {
|
||||||
|
if (!workspaceId) return;
|
||||||
|
const trimmed = url.trim();
|
||||||
|
try {
|
||||||
|
const parsed = new URL(trimmed);
|
||||||
|
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') throw new Error('bad protocol');
|
||||||
|
} catch {
|
||||||
|
setErrorMsg('Enter a valid http:// or https:// URL');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStatus('loading');
|
||||||
|
setErrorMsg('');
|
||||||
|
try {
|
||||||
|
await createArtboardMutation({
|
||||||
|
workspace_id: workspaceId,
|
||||||
|
project_id: projectId ?? null,
|
||||||
|
name: 'Home',
|
||||||
|
origin_id: null,
|
||||||
|
parent_artboard_id: null,
|
||||||
|
metadata_jsonb: { x: 100, y: 100, width: 1280, height: 800, renderUrl: trimmed, route: '/' },
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['artboards', workspaceId, projectId ?? undefined] });
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Canvas] Failed to create artboard', e);
|
||||||
|
setStatus('error');
|
||||||
|
setErrorMsg('Failed to create artboard — try again');
|
||||||
|
}
|
||||||
|
}, [url, workspaceId, projectId, queryClient]);
|
||||||
|
|
||||||
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
position: 'absolute', inset: 0, display: 'flex',
|
position: 'absolute', inset: 0, display: 'flex',
|
||||||
alignItems: 'center', justifyContent: 'center',
|
alignItems: 'center', justifyContent: 'center',
|
||||||
pointerEvents: 'none', zIndex: 3,
|
pointerEvents: 'none', zIndex: 3,
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div
|
||||||
|
style={{
|
||||||
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 20,
|
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 20,
|
||||||
opacity: 0.55, maxWidth: 320,
|
maxWidth: 360, pointerEvents: 'auto',
|
||||||
}}>
|
}}
|
||||||
|
onMouseDown={e => e.stopPropagation()}
|
||||||
|
>
|
||||||
{/* Icon */}
|
{/* Icon */}
|
||||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none">
|
<svg width="40" height="40" viewBox="0 0 40 40" fill="none">
|
||||||
<rect x="3" y="3" width="15" height="15" rx="3" stroke="rgba(255,255,255,0.5)" strokeWidth="1.3" strokeDasharray="3.5 2"/>
|
<rect x="3" y="3" width="15" height="15" rx="3" stroke="rgba(255,255,255,0.5)" strokeWidth="1.3" strokeDasharray="3.5 2"/>
|
||||||
@@ -309,61 +369,77 @@ export function Canvas() {
|
|||||||
<div style={{
|
<div style={{
|
||||||
fontFamily: "'JetBrains Mono', monospace",
|
fontFamily: "'JetBrains Mono', monospace",
|
||||||
fontSize: '0.65rem', color: 'rgba(255,255,255,0.7)',
|
fontSize: '0.65rem', color: 'rgba(255,255,255,0.7)',
|
||||||
letterSpacing: '0.1em', textTransform: 'uppercase',
|
letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: 4,
|
||||||
marginBottom: 4,
|
|
||||||
}}>
|
}}>
|
||||||
Get started
|
Connect your app
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontFamily: "'Inter', sans-serif", fontSize: '0.7rem', color: 'rgba(255,255,255,0.35)', lineHeight: 1.5 }}>
|
<div style={{ fontFamily: "'Inter', sans-serif", fontSize: '0.7rem', color: 'rgba(255,255,255,0.35)', lineHeight: 1.5 }}>
|
||||||
Live render your running app into design frames
|
Paste the CLI proxy URL — all your app's pages will be auto-rendered as artboards
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Steps */}
|
{/* CLI hint */}
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 14, width: '100%' }}>
|
|
||||||
{[
|
|
||||||
{ n: '1', text: 'Press A and click the canvas to place a screen' },
|
|
||||||
{ n: '2', text: 'Start the CLI proxy pointing at your dev server', code: 'npx @originmain/cli dev --target http://localhost:3000' },
|
|
||||||
{ n: '3', text: "Paste the proxy URL into the screen's Connect field" },
|
|
||||||
].map(({ n, text, code }) => (
|
|
||||||
<div key={n} style={{ display: 'flex', gap: 10, alignItems: 'flex-start' }}>
|
|
||||||
<div style={{
|
<div style={{
|
||||||
width: 18, height: 18, borderRadius: '50%', flexShrink: 0,
|
width: '100%', padding: '6px 10px',
|
||||||
border: '1px solid rgba(51,133,255,0.5)',
|
background: 'rgba(51,133,255,0.08)', border: '1px solid rgba(51,133,255,0.18)',
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
borderRadius: 6, fontFamily: "'JetBrains Mono', monospace",
|
||||||
fontFamily: "'JetBrains Mono', monospace",
|
fontSize: '0.5625rem', color: 'rgba(51,133,255,0.75)', letterSpacing: '-0.01em',
|
||||||
fontSize: '0.5rem', color: 'rgba(51,133,255,0.9)',
|
|
||||||
fontWeight: 600,
|
|
||||||
}}>
|
}}>
|
||||||
{n}
|
npx @originmain/cli dev --target http://localhost:3000
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<div style={{ fontFamily: "'Inter', sans-serif", fontSize: '0.6875rem', color: 'rgba(255,255,255,0.55)', lineHeight: 1.45 }}>
|
{/* URL input */}
|
||||||
{text}
|
<div style={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||||
</div>
|
<div style={{ display: 'flex', gap: 6 }}>
|
||||||
{code && (
|
<input
|
||||||
<div style={{
|
type="url"
|
||||||
marginTop: 5,
|
value={url}
|
||||||
padding: '4px 8px',
|
onChange={e => { setUrl(e.target.value); setErrorMsg(''); }}
|
||||||
background: 'rgba(51,133,255,0.1)',
|
onKeyDown={e => { if (e.key === 'Enter') void handleConnect(); e.stopPropagation(); }}
|
||||||
border: '1px solid rgba(51,133,255,0.2)',
|
placeholder="http://localhost:4170"
|
||||||
borderRadius: 5,
|
style={{
|
||||||
|
flex: 1, background: 'rgba(255,255,255,0.05)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.12)', borderRadius: 6,
|
||||||
|
padding: '8px 10px', fontSize: '0.75rem',
|
||||||
|
color: 'rgba(255,255,255,0.85)',
|
||||||
fontFamily: "'JetBrains Mono', monospace",
|
fontFamily: "'JetBrains Mono', monospace",
|
||||||
fontSize: '0.5625rem',
|
outline: 'none', letterSpacing: '-0.01em',
|
||||||
color: 'rgba(51,133,255,0.85)',
|
}}
|
||||||
letterSpacing: '-0.01em',
|
onFocus={e => (e.currentTarget.style.borderColor = '#3385FF')}
|
||||||
}}>
|
onBlur={e => (e.currentTarget.style.borderColor = 'rgba(255,255,255,0.12)')}
|
||||||
{code}
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => void handleConnect()}
|
||||||
|
disabled={status === 'loading' || !url.trim()}
|
||||||
|
style={{
|
||||||
|
padding: '8px 14px', borderRadius: 6,
|
||||||
|
background: !url.trim() ? 'rgba(51,133,255,0.3)' : '#3385FF',
|
||||||
|
border: 'none', color: '#fff', fontSize: '0.75rem', fontWeight: 600,
|
||||||
|
cursor: status === 'loading' || !url.trim() ? 'not-allowed' : 'pointer',
|
||||||
|
fontFamily: "'Inter', sans-serif",
|
||||||
|
opacity: status === 'loading' ? 0.7 : 1, whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{status === 'loading' ? 'Connecting…' : 'Connect →'}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{errorMsg && (
|
||||||
|
<p style={{ margin: 0, fontSize: '0.625rem', color: '#FF8080', fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
{errorMsg}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
))}
|
<div style={{
|
||||||
|
fontFamily: "'Inter', sans-serif", fontSize: '0.625rem',
|
||||||
|
color: 'rgba(255,255,255,0.2)', lineHeight: 1.5, textAlign: 'center',
|
||||||
|
}}>
|
||||||
|
Or press{' '}
|
||||||
|
<kbd style={{ fontFamily: "'JetBrains Mono', monospace", padding: '1px 4px', background: 'rgba(255,255,255,0.08)', borderRadius: 3 }}>A</kbd>
|
||||||
|
{' '}and click the canvas to place a screen manually
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,12 +53,17 @@ export function LiveArtboard({
|
|||||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
// Track whether the iframe has sent READY so we don't send messages too early.
|
// Track whether the iframe has sent READY so we don't send messages too early.
|
||||||
const isReadyRef = useRef(false);
|
const isReadyRef = useRef(false);
|
||||||
|
// After READY fires the nodeMap inside the iframe is still empty — we can't
|
||||||
|
// request styles until after the first FIBER_TREE_UPDATE (which populates it).
|
||||||
|
// This ref holds the nodeId to re-request styles for after that first commit.
|
||||||
|
const pendingStylesFetchRef = useRef<string | null>(null);
|
||||||
|
|
||||||
// Reset ready state whenever src changes. Without this, isReadyRef stays
|
// Reset ready state whenever src changes. Without this, isReadyRef stays
|
||||||
// true from the previous page, causing design-token / selection effects to
|
// true from the previous page, causing design-token / selection effects to
|
||||||
// fire against a half-loaded iframe between navigation and the new READY.
|
// fire against a half-loaded iframe between navigation and the new READY.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
isReadyRef.current = false;
|
isReadyRef.current = false;
|
||||||
|
pendingStylesFetchRef.current = null;
|
||||||
}, [src]);
|
}, [src]);
|
||||||
|
|
||||||
// ── Send a typed message to the iframe ───────────────────────────────────
|
// ── Send a typed message to the iframe ───────────────────────────────────
|
||||||
@@ -87,19 +92,26 @@ export function LiveArtboard({
|
|||||||
isReadyRef.current = true;
|
isReadyRef.current = true;
|
||||||
// Push current design tokens into the iframe immediately.
|
// Push current design tokens into the iframe immediately.
|
||||||
if (designTokens) sendMessage('SET_DESIGN_TOKENS', { tokens: designTokens });
|
if (designTokens) sendMessage('SET_DESIGN_TOKENS', { tokens: designTokens });
|
||||||
// Restore any active selection that existed before the iframe loaded.
|
// Restore the highlight ring for any active selection.
|
||||||
|
// We cannot send REQUEST_ELEMENT_STYLES here — nodeMap is empty until
|
||||||
|
// the first React commit fires FIBER_TREE_UPDATE. Defer it via ref.
|
||||||
if (selectedComponentId) {
|
if (selectedComponentId) {
|
||||||
sendMessage('SELECT_COMPONENT', { nodeId: selectedComponentId });
|
sendMessage('SELECT_COMPONENT', { nodeId: selectedComponentId });
|
||||||
|
pendingStylesFetchRef.current = selectedComponentId;
|
||||||
}
|
}
|
||||||
onReady?.();
|
onReady?.();
|
||||||
break;
|
break;
|
||||||
case 'FIBER_TREE_UPDATE':
|
case 'FIBER_TREE_UPDATE':
|
||||||
onFiberTreeUpdate?.(msg.root);
|
onFiberTreeUpdate?.(msg.root);
|
||||||
|
// If READY deferred a style fetch (nodeMap was empty at that point),
|
||||||
|
// the first commit has now populated nodeMap — request styles now.
|
||||||
|
if (pendingStylesFetchRef.current) {
|
||||||
|
sendMessage('REQUEST_ELEMENT_STYLES', { nodeId: pendingStylesFetchRef.current });
|
||||||
|
pendingStylesFetchRef.current = null;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'COMPONENT_SELECTED':
|
case 'COMPONENT_SELECTED':
|
||||||
onComponentSelected?.(msg.nodeId);
|
onComponentSelected?.(msg.nodeId);
|
||||||
// Request computed styles so the Design tab can populate immediately.
|
|
||||||
sendMessage('REQUEST_ELEMENT_STYLES', { nodeId: msg.nodeId });
|
|
||||||
break;
|
break;
|
||||||
case 'COMPONENT_DESELECTED':
|
case 'COMPONENT_DESELECTED':
|
||||||
// Renderer clicked empty space — clear the host-side selection.
|
// Renderer clicked empty space — clear the host-side selection.
|
||||||
@@ -124,24 +136,23 @@ export function LiveArtboard({
|
|||||||
sendMessage('SET_DESIGN_TOKENS', { tokens: designTokens });
|
sendMessage('SET_DESIGN_TOKENS', { tokens: designTokens });
|
||||||
}, [designTokens, sendMessage]);
|
}, [designTokens, sendMessage]);
|
||||||
|
|
||||||
// ── Style edit mailbox ─────────────────────────────────────────────────────
|
// ── Style edit queue ───────────────────────────────────────────────────────
|
||||||
// Watches the Zustand mailbox for PATCH_ELEMENT_STYLE events addressed to
|
// Drains all PATCH_ELEMENT_STYLE events addressed to this artboard and
|
||||||
// this artboard and forwards them to the iframe immediately.
|
// forwards them to the iframe. Uses a queue (not a single slot) so that
|
||||||
const styleEditEvent = useCanvas((s) => s.styleEditEvent);
|
// simultaneous width + height patches from resize are both delivered.
|
||||||
const clearStyleEdit = useCanvas((s) => s.clearStyleEdit);
|
const styleEditQueue = useCanvas((s) => s.styleEditQueue);
|
||||||
|
const clearStyleEdits = useCanvas((s) => s.clearStyleEdits);
|
||||||
const removeElementEvent = useCanvas((s) => s.removeElementEvent);
|
const removeElementEvent = useCanvas((s) => s.removeElementEvent);
|
||||||
const clearRemoveElement = useCanvas((s) => s.clearRemoveElement);
|
const clearRemoveElement = useCanvas((s) => s.clearRemoveElement);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (styleEditEvent?.artboardId === id && isReadyRef.current) {
|
const mine = styleEditQueue.filter((e) => e.artboardId === id);
|
||||||
sendMessage('PATCH_ELEMENT_STYLE', {
|
if (mine.length === 0 || !isReadyRef.current) return;
|
||||||
nodeId: styleEditEvent.nodeId,
|
for (const e of mine) {
|
||||||
property: styleEditEvent.property,
|
sendMessage('PATCH_ELEMENT_STYLE', { nodeId: e.nodeId, property: e.property, value: e.value });
|
||||||
value: styleEditEvent.value,
|
|
||||||
});
|
|
||||||
clearStyleEdit();
|
|
||||||
}
|
}
|
||||||
}, [id, styleEditEvent, sendMessage, clearStyleEdit]);
|
clearStyleEdits(id);
|
||||||
|
}, [id, styleEditQueue, sendMessage, clearStyleEdits]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (removeElementEvent?.artboardId === id && isReadyRef.current) {
|
if (removeElementEvent?.artboardId === id && isReadyRef.current) {
|
||||||
|
|||||||
@@ -36,12 +36,14 @@ interface CanvasStore {
|
|||||||
selectedComponentStyles: Record<string, string> | null;
|
selectedComponentStyles: Record<string, string> | null;
|
||||||
setComponentStyles: (styles: Record<string, string> | null) => void;
|
setComponentStyles: (styles: Record<string, string> | null) => void;
|
||||||
|
|
||||||
// ── Style edit mailbox ──────────────────────────────────────────────────────
|
// ── Style edit queue ────────────────────────────────────────────────────────
|
||||||
// The Design tab drops a patch here; the owning LiveArtboard picks it up,
|
// The Design tab and resize handles push patches here; each owning
|
||||||
// forwards it to the iframe, then clears it.
|
// LiveArtboard drains entries addressed to it, then removes them.
|
||||||
styleEditEvent: { artboardId: string; nodeId: string; property: string; value: string } | null;
|
// A queue (not a single slot) is required because resize sends width + height
|
||||||
|
// in the same synchronous event — a single slot would silently drop the first.
|
||||||
|
styleEditQueue: Array<{ artboardId: string; nodeId: string; property: string; value: string }>;
|
||||||
patchStyleEdit: (artboardId: string, nodeId: string, property: string, value: string) => void;
|
patchStyleEdit: (artboardId: string, nodeId: string, property: string, value: string) => void;
|
||||||
clearStyleEdit: () => void;
|
clearStyleEdits: (artboardId: string) => void;
|
||||||
|
|
||||||
// ── Element removal mailbox ─────────────────────────────────────────────────
|
// ── Element removal mailbox ─────────────────────────────────────────────────
|
||||||
removeElementEvent: { artboardId: string; nodeId: string } | null;
|
removeElementEvent: { artboardId: string; nodeId: string } | null;
|
||||||
@@ -81,10 +83,11 @@ export const useCanvas = create<CanvasStore>((set) => ({
|
|||||||
selectedComponentStyles: null,
|
selectedComponentStyles: null,
|
||||||
setComponentStyles: (styles) => set({ selectedComponentStyles: styles }),
|
setComponentStyles: (styles) => set({ selectedComponentStyles: styles }),
|
||||||
|
|
||||||
styleEditEvent: null,
|
styleEditQueue: [],
|
||||||
patchStyleEdit: (artboardId, nodeId, property, value) =>
|
patchStyleEdit: (artboardId, nodeId, property, value) =>
|
||||||
set({ styleEditEvent: { artboardId, nodeId, property, value } }),
|
set((s) => ({ styleEditQueue: [...s.styleEditQueue, { artboardId, nodeId, property, value }] })),
|
||||||
clearStyleEdit: () => set({ styleEditEvent: null }),
|
clearStyleEdits: (artboardId) =>
|
||||||
|
set((s) => ({ styleEditQueue: s.styleEditQueue.filter((e) => e.artboardId !== artboardId) })),
|
||||||
|
|
||||||
removeElementEvent: null,
|
removeElementEvent: null,
|
||||||
dispatchRemoveElement: (artboardId, nodeId) =>
|
dispatchRemoveElement: (artboardId, nodeId) =>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@originmain/cli",
|
"name": "@originmain/cli",
|
||||||
"version": "0.0.4",
|
"version": "0.0.5",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Originmain CLI — reverse proxy for live React component inspection.",
|
"description": "Originmain CLI — reverse proxy for live React component inspection.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -216,13 +216,16 @@ export function buildProxyFiberHookScript(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Walk a fiber subtree to find the nearest host DOM node (div, span, etc.).
|
// Walk a fiber subtree to find the nearest host DOM node (div, span, etc.).
|
||||||
// Composite components have stateNode = null or class instance; host elements
|
// Traverses both .child and .sibling so that components whose first child
|
||||||
// have stateNode = actual DOM element with a .style property.
|
// branch is a non-DOM composite (e.g. a Context.Provider sibling to a div)
|
||||||
|
// are handled correctly.
|
||||||
function findDomElement(fiber) {
|
function findDomElement(fiber) {
|
||||||
if (!fiber) return null;
|
if (!fiber) return null;
|
||||||
var sn = fiber.stateNode;
|
var sn = fiber.stateNode;
|
||||||
if (sn && typeof sn.style !== 'undefined') return sn;
|
if (sn && typeof sn.style !== 'undefined') return sn;
|
||||||
return findDomElement(fiber.child);
|
var fromChild = findDomElement(fiber.child);
|
||||||
|
if (fromChild) return fromChild;
|
||||||
|
return findDomElement(fiber.sibling);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect all NAMED descendants of fiber.child into out[], transparently
|
// Collect all NAMED descendants of fiber.child into out[], transparently
|
||||||
@@ -439,7 +442,16 @@ export function buildProxyFiberHookScript(): string {
|
|||||||
var dInfo = nodeMap[msg.nodeId];
|
var dInfo = nodeMap[msg.nodeId];
|
||||||
if (dInfo) {
|
if (dInfo) {
|
||||||
var dEl = findDomElement(dInfo.fiber);
|
var dEl = findDomElement(dInfo.fiber);
|
||||||
if (dEl) dEl.style.setProperty('display', 'none');
|
if (dEl) {
|
||||||
|
dEl.style.setProperty('display', 'none');
|
||||||
|
// Clear the selection so the highlight ring doesn't linger over
|
||||||
|
// the now-invisible element on the next React commit.
|
||||||
|
if (selectedNodeId === msg.nodeId) {
|
||||||
|
selectedNodeId = null;
|
||||||
|
removeHighlight();
|
||||||
|
post({ type: 'COMPONENT_DESELECTED' });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -465,8 +477,59 @@ export function buildProxyFiberHookScript(): string {
|
|||||||
} catch (e) { /* navigation not available in this context */ }
|
} catch (e) { /* navigation not available in this context */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Route discovery ───────────────────────────────────────────────────────
|
||||||
|
// Scans <a href> links and the fiber nodeMap for same-origin routes, then
|
||||||
|
// posts ROUTES_DISCOVERED so the host canvas can auto-create artboards for
|
||||||
|
// every page. Called once 800 ms after READY (giving React time to paint)
|
||||||
|
// and again on every SPA popstate so navigation is reflected in the canvas.
|
||||||
|
function humanLabel(path) {
|
||||||
|
var seg = path.replace(/\/+$/, '').split('/').filter(function(s) { return s.length > 0; });
|
||||||
|
if (seg.length === 0) return 'Home';
|
||||||
|
var last = seg[seg.length - 1];
|
||||||
|
return last.replace(/[-_]/g, ' ').replace(/\b\w/g, function(c) { return c.toUpperCase(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function discoverRoutes() {
|
||||||
|
var seen = {};
|
||||||
|
var routes = [];
|
||||||
|
|
||||||
|
function addRoute(path, hint) {
|
||||||
|
if (!path || seen[path]) return;
|
||||||
|
if (path.charAt(0) === '#') return;
|
||||||
|
seen[path] = true;
|
||||||
|
var label = (hint && typeof hint === 'string' && hint.trim().slice(0, 50)) || humanLabel(path);
|
||||||
|
routes.push({ path: path, label: label });
|
||||||
|
}
|
||||||
|
|
||||||
|
addRoute(window.location.pathname, document.title || undefined);
|
||||||
|
|
||||||
|
var anchors = document.querySelectorAll('a[href]');
|
||||||
|
for (var i = 0; i < anchors.length; i++) {
|
||||||
|
try {
|
||||||
|
var url = new URL(anchors[i].href, window.location.href);
|
||||||
|
if (url.origin !== window.location.origin) continue;
|
||||||
|
addRoute(url.pathname, anchors[i].textContent || undefined);
|
||||||
|
} catch (e) { /* skip malformed hrefs */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(nodeMap).forEach(function(nid) {
|
||||||
|
var entry = nodeMap[nid];
|
||||||
|
var fiber = entry && entry.fiber;
|
||||||
|
var name = fiber ? getDisplayName(fiber) : null;
|
||||||
|
if (name && /^(Link|NavLink|NextLink|RouterLink|a)$/.test(name)) {
|
||||||
|
var props = fiber.memoizedProps;
|
||||||
|
var href = props && (props.href || props.to);
|
||||||
|
if (typeof href === 'string' && href.charAt(0) === '/') addRoute(href);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (routes.length > 0) post({ type: 'ROUTES_DISCOVERED', routes: routes });
|
||||||
|
}
|
||||||
|
|
||||||
// ── Ready signal ──────────────────────────────────────────────────────────
|
// ── Ready signal ──────────────────────────────────────────────────────────
|
||||||
post({ type: 'READY' });
|
post({ type: 'READY' });
|
||||||
|
setTimeout(discoverRoutes, 800);
|
||||||
|
window.addEventListener('popstate', function() { setTimeout(discoverRoutes, 100); });
|
||||||
})();`;
|
})();`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user