Spaces:
Sleeping
Sleeping
incognitolm commited on
Commit ·
2b73dbf
1
Parent(s): 72b3fd1
Phase B
Browse files
client/src/components/VisualEditor/LiveCanvas.tsx
CHANGED
|
@@ -1,6 +1,13 @@
|
|
| 1 |
-
import { useRef, useEffect, useCallback, useState } from 'react';
|
| 2 |
import { generateEditorPage } from './editorSandbox';
|
| 3 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
interface LiveCanvasProps {
|
| 5 |
html: string;
|
| 6 |
css: string;
|
|
@@ -9,12 +16,14 @@ interface LiveCanvasProps {
|
|
| 9 |
selectedSelector: string | null;
|
| 10 |
onElementClick: (selector: string) => void;
|
| 11 |
onElementDrop: (parentSelector: string, slotIndex: number) => void;
|
|
|
|
| 12 |
onCanvasClick: () => void;
|
|
|
|
| 13 |
}
|
| 14 |
|
| 15 |
export default function LiveCanvas({
|
| 16 |
html, css, js, showOutlines, selectedSelector,
|
| 17 |
-
onElementClick, onElementDrop, onCanvasClick,
|
| 18 |
}: LiveCanvasProps) {
|
| 19 |
const iframeRef = useRef<HTMLIFrameElement>(null);
|
| 20 |
const overlayRef = useRef<HTMLDivElement>(null);
|
|
@@ -57,20 +66,40 @@ export default function LiveCanvas({
|
|
| 57 |
}, '*');
|
| 58 |
}, [showOutlines, ready]);
|
| 59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
// Receive messages from iframe
|
| 61 |
useEffect(() => {
|
| 62 |
const handler = (event: MessageEvent) => {
|
| 63 |
if (event.source !== iframeRef.current?.contentWindow) return;
|
| 64 |
const data = event.data;
|
|
|
|
| 65 |
switch (data.type) {
|
| 66 |
case 'ELEMENT_CLICK':
|
| 67 |
-
onElementClick(data.selector);
|
|
|
|
|
|
|
|
|
|
| 68 |
break;
|
| 69 |
case 'ELEMENT_DROP_IN_SLOT':
|
| 70 |
-
onElementDrop(data.parentSelector, data.slotIndex);
|
| 71 |
break;
|
| 72 |
case 'CANVAS_CLICK':
|
| 73 |
-
onCanvasClick();
|
| 74 |
break;
|
| 75 |
case 'READY':
|
| 76 |
setReady(true);
|
|
@@ -79,7 +108,24 @@ export default function LiveCanvas({
|
|
| 79 |
};
|
| 80 |
window.addEventListener('message', handler);
|
| 81 |
return () => window.removeEventListener('message', handler);
|
| 82 |
-
}, [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
return (
|
| 85 |
<div className="relative w-full h-full">
|
|
@@ -92,8 +138,8 @@ export default function LiveCanvas({
|
|
| 92 |
<div
|
| 93 |
ref={overlayRef}
|
| 94 |
className="absolute inset-0 z-10"
|
| 95 |
-
onDragOver={
|
| 96 |
-
onDrop={
|
| 97 |
onClick={() => onCanvasClick()}
|
| 98 |
/>
|
| 99 |
</div>
|
|
|
|
| 1 |
+
import { useRef, useEffect, useCallback, useState, MutableRefObject } from 'react';
|
| 2 |
import { generateEditorPage } from './editorSandbox';
|
| 3 |
|
| 4 |
+
interface ElementPreset {
|
| 5 |
+
tagName: string;
|
| 6 |
+
label: string;
|
| 7 |
+
icon: any;
|
| 8 |
+
defaultProps: any;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
interface LiveCanvasProps {
|
| 12 |
html: string;
|
| 13 |
css: string;
|
|
|
|
| 16 |
selectedSelector: string | null;
|
| 17 |
onElementClick: (selector: string) => void;
|
| 18 |
onElementDrop: (parentSelector: string, slotIndex: number) => void;
|
| 19 |
+
onElementDropFromToolbox: (preset: ElementPreset, parentSelector: string, slotIndex: number) => void;
|
| 20 |
onCanvasClick: () => void;
|
| 21 |
+
dragDataRef: MutableRefObject<ElementPreset | null>;
|
| 22 |
}
|
| 23 |
|
| 24 |
export default function LiveCanvas({
|
| 25 |
html, css, js, showOutlines, selectedSelector,
|
| 26 |
+
onElementClick, onElementDrop, onElementDropFromToolbox, onCanvasClick, dragDataRef,
|
| 27 |
}: LiveCanvasProps) {
|
| 28 |
const iframeRef = useRef<HTMLIFrameElement>(null);
|
| 29 |
const overlayRef = useRef<HTMLDivElement>(null);
|
|
|
|
| 66 |
}, '*');
|
| 67 |
}, [showOutlines, ready]);
|
| 68 |
|
| 69 |
+
// Store callbacks in refs for stable message handler
|
| 70 |
+
const callbacksRef = useRef({ onElementClick, onElementDrop, onElementDropFromToolbox, onCanvasClick, dragDataRef });
|
| 71 |
+
callbacksRef.current = { onElementClick, onElementDrop, onElementDropFromToolbox, onCanvasClick, dragDataRef };
|
| 72 |
+
|
| 73 |
+
// Handle drop response from iframe (sent after GET_ELEMENT_AT query)
|
| 74 |
+
const handleIframeDrop = useCallback((selector: string) => {
|
| 75 |
+
const { dragDataRef: ddr, onElementDrop: oed, onElementDropFromToolbox: oedft } = callbacksRef.current;
|
| 76 |
+
const preset = ddr.current;
|
| 77 |
+
if (!preset) {
|
| 78 |
+
oed(selector, 0);
|
| 79 |
+
return;
|
| 80 |
+
}
|
| 81 |
+
oedft(preset, selector, 0);
|
| 82 |
+
ddr.current = null;
|
| 83 |
+
}, []);
|
| 84 |
+
|
| 85 |
// Receive messages from iframe
|
| 86 |
useEffect(() => {
|
| 87 |
const handler = (event: MessageEvent) => {
|
| 88 |
if (event.source !== iframeRef.current?.contentWindow) return;
|
| 89 |
const data = event.data;
|
| 90 |
+
const cbs = callbacksRef.current;
|
| 91 |
switch (data.type) {
|
| 92 |
case 'ELEMENT_CLICK':
|
| 93 |
+
cbs.onElementClick(data.selector);
|
| 94 |
+
break;
|
| 95 |
+
case 'ELEMENT_DROP':
|
| 96 |
+
handleIframeDrop(data.selector);
|
| 97 |
break;
|
| 98 |
case 'ELEMENT_DROP_IN_SLOT':
|
| 99 |
+
cbs.onElementDrop(data.parentSelector, data.slotIndex);
|
| 100 |
break;
|
| 101 |
case 'CANVAS_CLICK':
|
| 102 |
+
cbs.onCanvasClick();
|
| 103 |
break;
|
| 104 |
case 'READY':
|
| 105 |
setReady(true);
|
|
|
|
| 108 |
};
|
| 109 |
window.addEventListener('message', handler);
|
| 110 |
return () => window.removeEventListener('message', handler);
|
| 111 |
+
}, [handleIframeDrop]);
|
| 112 |
+
|
| 113 |
+
// Handle overlay drop: query iframe for element at drop point
|
| 114 |
+
const handleOverlayDrop = useCallback((e: React.DragEvent) => {
|
| 115 |
+
e.preventDefault();
|
| 116 |
+
const rect = iframeRef.current?.getBoundingClientRect();
|
| 117 |
+
if (!rect || !iframeRef.current?.contentWindow) return;
|
| 118 |
+
const x = e.clientX - rect.left;
|
| 119 |
+
const y = e.clientY - rect.top;
|
| 120 |
+
iframeRef.current.contentWindow.postMessage({
|
| 121 |
+
type: 'GET_ELEMENT_AT',
|
| 122 |
+
x, y, action: 'drop',
|
| 123 |
+
}, '*');
|
| 124 |
+
}, []);
|
| 125 |
+
|
| 126 |
+
const handleOverlayDragOver = useCallback((e: React.DragEvent) => {
|
| 127 |
+
e.preventDefault();
|
| 128 |
+
}, []);
|
| 129 |
|
| 130 |
return (
|
| 131 |
<div className="relative w-full h-full">
|
|
|
|
| 138 |
<div
|
| 139 |
ref={overlayRef}
|
| 140 |
className="absolute inset-0 z-10"
|
| 141 |
+
onDragOver={handleOverlayDragOver}
|
| 142 |
+
onDrop={handleOverlayDrop}
|
| 143 |
onClick={() => onCanvasClick()}
|
| 144 |
/>
|
| 145 |
</div>
|
client/src/components/VisualEditor/Toolbox.tsx
CHANGED
|
@@ -145,13 +145,18 @@ const ELEMENT_CATEGORIES: { name: string; icon: any; items: ElementPreset[] }[]
|
|
| 145 |
},
|
| 146 |
];
|
| 147 |
|
| 148 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(() => new Set(ELEMENT_CATEGORIES.slice(0, 3).map(c => c.name)));
|
| 150 |
|
| 151 |
const handleDragStart = useCallback((e: React.DragEvent, element: ElementPreset) => {
|
| 152 |
e.dataTransfer.setData('application/visual-element', JSON.stringify(element));
|
| 153 |
e.dataTransfer.effectAllowed = 'copy';
|
| 154 |
-
|
|
|
|
| 155 |
|
| 156 |
const toggleCategory = useCallback((name: string) => {
|
| 157 |
setExpandedCategories(prev => {
|
|
|
|
| 145 |
},
|
| 146 |
];
|
| 147 |
|
| 148 |
+
interface ToolboxProps {
|
| 149 |
+
onDragStart?: (preset: ElementPreset) => void;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
export default function Toolbox({ onDragStart }: ToolboxProps) {
|
| 153 |
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(() => new Set(ELEMENT_CATEGORIES.slice(0, 3).map(c => c.name)));
|
| 154 |
|
| 155 |
const handleDragStart = useCallback((e: React.DragEvent, element: ElementPreset) => {
|
| 156 |
e.dataTransfer.setData('application/visual-element', JSON.stringify(element));
|
| 157 |
e.dataTransfer.effectAllowed = 'copy';
|
| 158 |
+
onDragStart?.(element);
|
| 159 |
+
}, [onDragStart]);
|
| 160 |
|
| 161 |
const toggleCategory = useCallback((name: string) => {
|
| 162 |
setExpandedCategories(prev => {
|
client/src/components/VisualEditor/VisualEditor.tsx
CHANGED
|
@@ -12,7 +12,6 @@ import {
|
|
| 12 |
Plus, Code, Palette, Layout
|
| 13 |
} from 'lucide-react';
|
| 14 |
import { getColor } from '../Collaboration/CollaboratorAvatars';
|
| 15 |
-
import { getSocket } from '../../hooks/useWebSocket';
|
| 16 |
|
| 17 |
type DeviceMode = 'desktop' | 'tablet' | 'mobile';
|
| 18 |
|
|
@@ -104,6 +103,11 @@ export default function VisualEditor() {
|
|
| 104 |
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; elementId: string } | null>(null);
|
| 105 |
const [showTagModal, setShowTagModal] = useState(false);
|
| 106 |
const [iframeSelector, setIframeSelector] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
// Collaborator hovers
|
| 109 |
const collaborators = useCollaborationStore((s) => s.collaborators);
|
|
@@ -141,9 +145,8 @@ export default function VisualEditor() {
|
|
| 141 |
}, [fileTree]);
|
| 142 |
|
| 143 |
const liveJs = useMemo(() => {
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
}, [activeFileId, visualElements]);
|
| 147 |
|
| 148 |
// Map iframe selector to element ID
|
| 149 |
const handleElementClick = useCallback((selector: string) => {
|
|
@@ -179,35 +182,45 @@ export default function VisualEditor() {
|
|
| 179 |
setIframeSelector(null);
|
| 180 |
}, [setSelectedElement]);
|
| 181 |
|
| 182 |
-
//
|
| 183 |
-
const
|
| 184 |
-
|
| 185 |
-
const
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
if (e.props?.id === parentIdAttr || e.attributes?.id === parentIdAttr) return e;
|
| 191 |
-
if (e.children) {
|
| 192 |
-
const found = searchByAttr(e.children);
|
| 193 |
-
if (found) return found;
|
| 194 |
-
}
|
| 195 |
}
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
if (!parent) return;
|
| 200 |
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
});
|
| 204 |
-
|
| 205 |
-
const autoId = incrementCounter('div');
|
| 206 |
child.props = { ...child.props, id: autoId };
|
| 207 |
child.attributes = { ...child.attributes, id: autoId };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
|
| 209 |
-
|
| 210 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
|
| 212 |
const handleElementClickLocal = useCallback((e: React.MouseEvent, elementId: string) => {
|
| 213 |
e.stopPropagation();
|
|
@@ -237,6 +250,18 @@ export default function VisualEditor() {
|
|
| 237 |
const selectedElement = selectedElementId ? findElementById(visualElements || [], selectedElementId) : null;
|
| 238 |
const device = DEVICE_SIZES[deviceMode];
|
| 239 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
// Context menu actions
|
| 241 |
const cmDuplicate = useCallback(() => {
|
| 242 |
if (!contextMenu) return;
|
|
@@ -246,10 +271,15 @@ export default function VisualEditor() {
|
|
| 246 |
const autoId = incrementCounter(el.tagName);
|
| 247 |
dup.props = { ...dup.props, id: autoId };
|
| 248 |
dup.attributes = { ...dup.attributes, id: autoId };
|
| 249 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
}
|
| 251 |
setContextMenu(null);
|
| 252 |
-
}, [contextMenu, visualElements, addVisualElement, incrementCounter]);
|
| 253 |
|
| 254 |
const cmDelete = useCallback(() => {
|
| 255 |
if (contextMenu) { removeVisualElement(contextMenu.elementId); setContextMenu(null); }
|
|
@@ -337,7 +367,7 @@ export default function VisualEditor() {
|
|
| 337 |
Elements
|
| 338 |
</div>
|
| 339 |
<div className="flex-1 overflow-y-auto px-2">
|
| 340 |
-
<Toolbox />
|
| 341 |
</div>
|
| 342 |
</div>
|
| 343 |
|
|
@@ -392,7 +422,9 @@ export default function VisualEditor() {
|
|
| 392 |
selectedSelector={iframeSelector}
|
| 393 |
onElementClick={handleElementClick}
|
| 394 |
onElementDrop={handleElementDrop}
|
|
|
|
| 395 |
onCanvasClick={handleCanvasClick}
|
|
|
|
| 396 |
/>
|
| 397 |
)}
|
| 398 |
</div>
|
|
|
|
| 12 |
Plus, Code, Palette, Layout
|
| 13 |
} from 'lucide-react';
|
| 14 |
import { getColor } from '../Collaboration/CollaboratorAvatars';
|
|
|
|
| 15 |
|
| 16 |
type DeviceMode = 'desktop' | 'tablet' | 'mobile';
|
| 17 |
|
|
|
|
| 103 |
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; elementId: string } | null>(null);
|
| 104 |
const [showTagModal, setShowTagModal] = useState(false);
|
| 105 |
const [iframeSelector, setIframeSelector] = useState<string | null>(null);
|
| 106 |
+
const dragDataRef = useRef<any>(null);
|
| 107 |
+
|
| 108 |
+
// Subscribe to blockCode changes reactively
|
| 109 |
+
const blockCode = useEditorStore((s) => s.blockCode);
|
| 110 |
+
const currentBlockCode = blockCode[activeFileId || ''] || '';
|
| 111 |
|
| 112 |
// Collaborator hovers
|
| 113 |
const collaborators = useCollaborationStore((s) => s.collaborators);
|
|
|
|
| 145 |
}, [fileTree]);
|
| 146 |
|
| 147 |
const liveJs = useMemo(() => {
|
| 148 |
+
return currentBlockCode;
|
| 149 |
+
}, [currentBlockCode]);
|
|
|
|
| 150 |
|
| 151 |
// Map iframe selector to element ID
|
| 152 |
const handleElementClick = useCallback((selector: string) => {
|
|
|
|
| 182 |
setIframeSelector(null);
|
| 183 |
}, [setSelectedElement]);
|
| 184 |
|
| 185 |
+
// Search for element by HTML id attribute
|
| 186 |
+
const searchByAttr = useCallback((els: VisualElement[] | undefined, idAttr: string): VisualElement | null => {
|
| 187 |
+
if (!els) return null;
|
| 188 |
+
for (const e of els) {
|
| 189 |
+
if (e.props?.id === idAttr || e.attributes?.id === idAttr) return e;
|
| 190 |
+
if (e.children) {
|
| 191 |
+
const found = searchByAttr(e.children, idAttr);
|
| 192 |
+
if (found) return found;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
}
|
| 194 |
+
}
|
| 195 |
+
return null;
|
| 196 |
+
}, []);
|
|
|
|
| 197 |
|
| 198 |
+
// Create a new element from preset and add to parent
|
| 199 |
+
const createElementFromPreset = useCallback((preset: any, parentId: string, slotIndex: number) => {
|
| 200 |
+
const child = createVisualElement(preset.tagName, preset.defaultProps || {});
|
| 201 |
+
const autoId = incrementCounter(preset.tagName);
|
|
|
|
| 202 |
child.props = { ...child.props, id: autoId };
|
| 203 |
child.attributes = { ...child.attributes, id: autoId };
|
| 204 |
+
addVisualElement(child, parentId, slotIndex);
|
| 205 |
+
}, [addVisualElement, incrementCounter]);
|
| 206 |
+
|
| 207 |
+
// Handle drop from within canvas (no preset, use generic div)
|
| 208 |
+
const handleElementDrop = useCallback((parentSelector: string, slotIndex: number) => {
|
| 209 |
+
const match = parentSelector.match(/#([\w-]+)/);
|
| 210 |
+
if (!match) return;
|
| 211 |
+
const parent = searchByAttr(visualElements, match[1]);
|
| 212 |
+
if (!parent) return;
|
| 213 |
+
createElementFromPreset({ tagName: 'div', defaultProps: { style: { padding: '12px', border: '1px dashed #6366f1', borderRadius: '4px' } } }, parent.id, slotIndex);
|
| 214 |
+
}, [visualElements, searchByAttr, createElementFromPreset]);
|
| 215 |
|
| 216 |
+
// Handle drop from toolbox (carries element preset data)
|
| 217 |
+
const handleElementDropFromToolbox = useCallback((preset: any, parentSelector: string, slotIndex: number) => {
|
| 218 |
+
const match = parentSelector.match(/#([\w-]+)/);
|
| 219 |
+
if (!match) return;
|
| 220 |
+
const parent = searchByAttr(visualElements, match[1]);
|
| 221 |
+
if (!parent) return;
|
| 222 |
+
createElementFromPreset(preset, parent.id, slotIndex);
|
| 223 |
+
}, [visualElements, searchByAttr, createElementFromPreset]);
|
| 224 |
|
| 225 |
const handleElementClickLocal = useCallback((e: React.MouseEvent, elementId: string) => {
|
| 226 |
e.stopPropagation();
|
|
|
|
| 250 |
const selectedElement = selectedElementId ? findElementById(visualElements || [], selectedElementId) : null;
|
| 251 |
const device = DEVICE_SIZES[deviceMode];
|
| 252 |
|
| 253 |
+
// Find parent and index of an element in the tree
|
| 254 |
+
const findParentAndIndex = useCallback((id: string, els: VisualElement[], parentId?: string, sibIdx?: number): { parentId: string | null; index: number } | null => {
|
| 255 |
+
for (let i = 0; i < (els || []).length; i++) {
|
| 256 |
+
if (els[i].id === id) return { parentId: parentId || null, index: i };
|
| 257 |
+
if (els[i].children) {
|
| 258 |
+
const found = findParentAndIndex(id, els[i].children, els[i].id, i);
|
| 259 |
+
if (found) return found;
|
| 260 |
+
}
|
| 261 |
+
}
|
| 262 |
+
return null;
|
| 263 |
+
}, []);
|
| 264 |
+
|
| 265 |
// Context menu actions
|
| 266 |
const cmDuplicate = useCallback(() => {
|
| 267 |
if (!contextMenu) return;
|
|
|
|
| 271 |
const autoId = incrementCounter(el.tagName);
|
| 272 |
dup.props = { ...dup.props, id: autoId };
|
| 273 |
dup.attributes = { ...dup.attributes, id: autoId };
|
| 274 |
+
const location = findParentAndIndex(contextMenu.elementId, visualElements || []);
|
| 275 |
+
if (location) {
|
| 276 |
+
addVisualElement(dup, location.parentId || undefined, location.index + 1);
|
| 277 |
+
} else {
|
| 278 |
+
addVisualElement(dup);
|
| 279 |
+
}
|
| 280 |
}
|
| 281 |
setContextMenu(null);
|
| 282 |
+
}, [contextMenu, visualElements, addVisualElement, incrementCounter, findParentAndIndex]);
|
| 283 |
|
| 284 |
const cmDelete = useCallback(() => {
|
| 285 |
if (contextMenu) { removeVisualElement(contextMenu.elementId); setContextMenu(null); }
|
|
|
|
| 367 |
Elements
|
| 368 |
</div>
|
| 369 |
<div className="flex-1 overflow-y-auto px-2">
|
| 370 |
+
<Toolbox onDragStart={(preset) => { dragDataRef.current = preset; }} />
|
| 371 |
</div>
|
| 372 |
</div>
|
| 373 |
|
|
|
|
| 422 |
selectedSelector={iframeSelector}
|
| 423 |
onElementClick={handleElementClick}
|
| 424 |
onElementDrop={handleElementDrop}
|
| 425 |
+
onElementDropFromToolbox={handleElementDropFromToolbox}
|
| 426 |
onCanvasClick={handleCanvasClick}
|
| 427 |
+
dragDataRef={dragDataRef}
|
| 428 |
/>
|
| 429 |
)}
|
| 430 |
</div>
|
client/src/store/editorStore.ts
CHANGED
|
@@ -42,7 +42,7 @@ interface EditorStore {
|
|
| 42 |
toggleSnapToGrid: () => void;
|
| 43 |
toggleMiniMap: () => void;
|
| 44 |
setVisualElements: (elements: VisualElement[]) => void;
|
| 45 |
-
addVisualElement: (element: VisualElement, parentId?: string) => void;
|
| 46 |
updateVisualElement: (id: string, updates: Partial<VisualElement>) => void;
|
| 47 |
removeVisualElement: (id: string) => void;
|
| 48 |
setFileTree: (files: ProjectFile[]) => void;
|
|
@@ -70,7 +70,7 @@ function getChildCount(elements: VisualElement[], id: string): number {
|
|
| 70 |
if (found !== -1) return found;
|
| 71 |
}
|
| 72 |
}
|
| 73 |
-
return
|
| 74 |
}
|
| 75 |
|
| 76 |
function getLinkedFiles(fileId: string, fileTree: ProjectFile[]): string[] {
|
|
@@ -132,17 +132,29 @@ export const useEditorStore = create<EditorStore>((set, get) => ({
|
|
| 132 |
|
| 133 |
setVisualElements: (elements) => set({ visualElements: elements || [] }),
|
| 134 |
|
| 135 |
-
addVisualElement: (element, parentId) =>
|
| 136 |
set((state) => {
|
| 137 |
const elements = state.visualElements || [];
|
| 138 |
let newElements: VisualElement[];
|
| 139 |
if (!parentId) {
|
| 140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
} else {
|
| 142 |
const addToParent = (els: VisualElement[]): VisualElement[] =>
|
| 143 |
(els || []).map((el) => {
|
| 144 |
if (el.id === parentId) {
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
}
|
| 147 |
if (el.children) {
|
| 148 |
return { ...el, children: addToParent(el.children) };
|
|
@@ -151,7 +163,6 @@ export const useEditorStore = create<EditorStore>((set, get) => ({
|
|
| 151 |
});
|
| 152 |
newElements = addToParent(elements);
|
| 153 |
}
|
| 154 |
-
// Recalculate slots for parent
|
| 155 |
setTimeout(() => {
|
| 156 |
if (parentId) get().recalculateSlots(parentId);
|
| 157 |
get().recomputeRegistry();
|
|
@@ -334,7 +345,7 @@ export const useEditorStore = create<EditorStore>((set, get) => ({
|
|
| 334 |
|
| 335 |
setSlotCount: (elementId, count) => {
|
| 336 |
const state = get();
|
| 337 |
-
const childCount = getChildCount(state.visualElements, elementId);
|
| 338 |
const clampedCount = Math.max(count, childCount, 1);
|
| 339 |
set((s) => ({
|
| 340 |
slotCounts: { ...s.slotCounts, [elementId]: clampedCount },
|
|
|
|
| 42 |
toggleSnapToGrid: () => void;
|
| 43 |
toggleMiniMap: () => void;
|
| 44 |
setVisualElements: (elements: VisualElement[]) => void;
|
| 45 |
+
addVisualElement: (element: VisualElement, parentId?: string, insertIndex?: number) => void;
|
| 46 |
updateVisualElement: (id: string, updates: Partial<VisualElement>) => void;
|
| 47 |
removeVisualElement: (id: string) => void;
|
| 48 |
setFileTree: (files: ProjectFile[]) => void;
|
|
|
|
| 70 |
if (found !== -1) return found;
|
| 71 |
}
|
| 72 |
}
|
| 73 |
+
return -1;
|
| 74 |
}
|
| 75 |
|
| 76 |
function getLinkedFiles(fileId: string, fileTree: ProjectFile[]): string[] {
|
|
|
|
| 132 |
|
| 133 |
setVisualElements: (elements) => set({ visualElements: elements || [] }),
|
| 134 |
|
| 135 |
+
addVisualElement: (element, parentId, insertIndex) =>
|
| 136 |
set((state) => {
|
| 137 |
const elements = state.visualElements || [];
|
| 138 |
let newElements: VisualElement[];
|
| 139 |
if (!parentId) {
|
| 140 |
+
if (insertIndex !== undefined) {
|
| 141 |
+
const copy = [...elements];
|
| 142 |
+
copy.splice(insertIndex, 0, element);
|
| 143 |
+
newElements = copy;
|
| 144 |
+
} else {
|
| 145 |
+
newElements = [...elements, element];
|
| 146 |
+
}
|
| 147 |
} else {
|
| 148 |
const addToParent = (els: VisualElement[]): VisualElement[] =>
|
| 149 |
(els || []).map((el) => {
|
| 150 |
if (el.id === parentId) {
|
| 151 |
+
const children = [...(el.children || [])];
|
| 152 |
+
if (insertIndex !== undefined) {
|
| 153 |
+
children.splice(insertIndex, 0, element);
|
| 154 |
+
} else {
|
| 155 |
+
children.push(element);
|
| 156 |
+
}
|
| 157 |
+
return { ...el, children };
|
| 158 |
}
|
| 159 |
if (el.children) {
|
| 160 |
return { ...el, children: addToParent(el.children) };
|
|
|
|
| 163 |
});
|
| 164 |
newElements = addToParent(elements);
|
| 165 |
}
|
|
|
|
| 166 |
setTimeout(() => {
|
| 167 |
if (parentId) get().recalculateSlots(parentId);
|
| 168 |
get().recomputeRegistry();
|
|
|
|
| 345 |
|
| 346 |
setSlotCount: (elementId, count) => {
|
| 347 |
const state = get();
|
| 348 |
+
const childCount = Math.max(0, getChildCount(state.visualElements, elementId));
|
| 349 |
const clampedCount = Math.max(count, childCount, 1);
|
| 350 |
set((s) => ({
|
| 351 |
slotCounts: { ...s.slotCounts, [elementId]: clampedCount },
|