incognitolm commited on
Commit
2595e43
·
1 Parent(s): 2b73dbf

Roll Back

Browse files
client/package.json CHANGED
@@ -35,7 +35,6 @@
35
  "@types/prismjs": "^1.26.3",
36
  "@types/react-syntax-highlighter": "^15.5.11",
37
  "@vitejs/plugin-react": "^4.2.1",
38
- "prettier": "^3.2.0",
39
  "autoprefixer": "^10.4.16",
40
  "postcss": "^8.4.32",
41
  "tailwindcss": "^3.4.0",
 
35
  "@types/prismjs": "^1.26.3",
36
  "@types/react-syntax-highlighter": "^15.5.11",
37
  "@vitejs/plugin-react": "^4.2.1",
 
38
  "autoprefixer": "^10.4.16",
39
  "postcss": "^8.4.32",
40
  "tailwindcss": "^3.4.0",
client/src/api/client.ts CHANGED
@@ -47,7 +47,7 @@ class ApiClient {
47
  }
48
  }
49
 
50
- // Auth (kept as REST; auth flows through REST endpoints)
51
  async register(email: string, username: string, password: string) {
52
  return this.request<{ token: string; user: any }>('POST', '/auth/register', { email, username, password });
53
  }
@@ -76,7 +76,7 @@ class ApiClient {
76
  return this.request<{ user: any }>('GET', '/auth/me');
77
  }
78
 
79
- // Projects (kept as REST fallback for when WS is not connected)
80
  async getProjects() {
81
  return this.request<{ projects: any[] }>('GET', '/projects');
82
  }
 
47
  }
48
  }
49
 
50
+ // Auth
51
  async register(email: string, username: string, password: string) {
52
  return this.request<{ token: string; user: any }>('POST', '/auth/register', { email, username, password });
53
  }
 
76
  return this.request<{ user: any }>('GET', '/auth/me');
77
  }
78
 
79
+ // Projects
80
  async getProjects() {
81
  return this.request<{ projects: any[] }>('GET', '/projects');
82
  }
client/src/components/CodeView/CodeView.tsx CHANGED
@@ -10,10 +10,9 @@ import { compileElectron } from '../../compilers/electron';
10
  import { compileMaui } from '../../compilers/maui';
11
  import { compileNodeJS } from '../../compilers/nodejs';
12
  import { SyntaxHighlight } from './SyntaxHighlight';
13
- import { formatCode, getFileLanguage } from '../../utils/formatCode';
14
  import {
15
  Download, Copy, FileCode,
16
- Eye, Code2, SplitSquareHorizontal, Save
17
  } from 'lucide-react';
18
  import { saveAs } from 'file-saver';
19
  import JSZip from 'jszip';
@@ -27,31 +26,6 @@ export default function CodeView() {
27
  const { emitCursorMove, emitFileChanged } = useWebSocket(id);
28
  const editorRef = useRef<HTMLDivElement>(null);
29
  const [isEditing, setIsEditing] = useState(false);
30
- const [formattedContent, setFormattedContent] = useState('');
31
-
32
- const activeFile = useMemo(() => {
33
- return fileTree.find(f => f.id === activeFileId) || null;
34
- }, [fileTree, activeFileId]);
35
-
36
- // Format on file load
37
- useEffect(() => {
38
- if (activeFile?.content && isEditing) {
39
- const lang = getFileLanguage(activeFile.type);
40
- formatCode(activeFile.content, lang).then(setFormattedContent);
41
- } else {
42
- setFormattedContent(activeFileContent);
43
- }
44
- }, [activeFile?.content, activeFile?.type, isEditing]);
45
-
46
- // Format on save handler
47
- const handleFormatSave = useCallback(async () => {
48
- if (!activeFile || !isEditing) return;
49
- const lang = getFileLanguage(activeFile.type);
50
- const formatted = await formatCode(activeFileContent, lang);
51
- useEditorStore.getState().saveGeneratedCodeToFile(activeFile.id, formatted);
52
- useEditorStore.getState().setActiveFileContent(formatted);
53
- setFormattedContent(formatted);
54
- }, [activeFile, activeFileContent, isEditing]);
55
 
56
  const compiledCode = useMemo(() => {
57
  if (!currentProject) return '';
@@ -110,19 +84,6 @@ export default function CodeView() {
110
  }, 100);
111
  }, [activeFileId, emitCursorMove, emitFileChanged, setActiveFileContent]);
112
 
113
- // Ctrl+S handler in edit mode
114
- useEffect(() => {
115
- if (!isEditing) return;
116
- const handler = (e: KeyboardEvent) => {
117
- if ((e.ctrlKey || e.metaKey) && e.key === 's') {
118
- e.preventDefault();
119
- handleFormatSave();
120
- }
121
- };
122
- window.addEventListener('keydown', handler);
123
- return () => window.removeEventListener('keydown', handler);
124
- }, [handleFormatSave, isEditing]);
125
-
126
  const handleDownload = useCallback(async () => {
127
  if (!currentProject) return;
128
  const zip = new JSZip();
@@ -171,14 +132,6 @@ export default function CodeView() {
171
  </button>
172
  </div>
173
  )}
174
- {isEditing && (
175
- <button onClick={handleFormatSave}
176
- className="btn-ghost px-2 py-1 text-xs text-green-400"
177
- title="Format & Save (Ctrl+S)">
178
- <Save className="w-3.5 h-3.5" />
179
- Format & Save
180
- </button>
181
- )}
182
  <button onClick={() => setIsEditing(!isEditing)}
183
  className={`btn-ghost px-2 py-1 text-xs ${isEditing ? 'text-primary-400 bg-primary-500/10' : ''}`}
184
  title={isEditing ? 'View compiled code' : 'Edit source directly'}>
 
10
  import { compileMaui } from '../../compilers/maui';
11
  import { compileNodeJS } from '../../compilers/nodejs';
12
  import { SyntaxHighlight } from './SyntaxHighlight';
 
13
  import {
14
  Download, Copy, FileCode,
15
+ Eye, Code2, SplitSquareHorizontal
16
  } from 'lucide-react';
17
  import { saveAs } from 'file-saver';
18
  import JSZip from 'jszip';
 
26
  const { emitCursorMove, emitFileChanged } = useWebSocket(id);
27
  const editorRef = useRef<HTMLDivElement>(null);
28
  const [isEditing, setIsEditing] = useState(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  const compiledCode = useMemo(() => {
31
  if (!currentProject) return '';
 
84
  }, 100);
85
  }, [activeFileId, emitCursorMove, emitFileChanged, setActiveFileContent]);
86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  const handleDownload = useCallback(async () => {
88
  if (!currentProject) return;
89
  const zip = new JSZip();
 
132
  </button>
133
  </div>
134
  )}
 
 
 
 
 
 
 
 
135
  <button onClick={() => setIsEditing(!isEditing)}
136
  className={`btn-ghost px-2 py-1 text-xs ${isEditing ? 'text-primary-400 bg-primary-500/10' : ''}`}
137
  title={isEditing ? 'View compiled code' : 'Edit source directly'}>
client/src/components/VisualEditor/LiveCanvas.tsx DELETED
@@ -1,147 +0,0 @@
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;
14
- js: string;
15
- showOutlines: boolean;
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);
30
- const [ready, setReady] = useState(false);
31
- const [initContent, setInitContent] = useState(true);
32
-
33
- // Initial page load
34
- useEffect(() => {
35
- if (initContent && iframeRef.current) {
36
- const page = generateEditorPage(html, css, js);
37
- iframeRef.current.srcdoc = page;
38
- setInitContent(false);
39
- }
40
- }, [initContent, html, css, js]);
41
-
42
- // Update content via postMessage after initial load
43
- useEffect(() => {
44
- if (!ready || !iframeRef.current?.contentWindow) return;
45
- iframeRef.current.contentWindow.postMessage({
46
- type: 'UPDATE_CONTENT',
47
- html, css, js,
48
- }, '*');
49
- }, [html, css, js, ready]);
50
-
51
- // Update selected element highlight
52
- useEffect(() => {
53
- if (!ready || !iframeRef.current?.contentWindow) return;
54
- iframeRef.current.contentWindow.postMessage({
55
- type: 'SELECT_ELEMENT',
56
- selector: selectedSelector,
57
- }, '*');
58
- }, [selectedSelector, ready]);
59
-
60
- // Update outline visibility
61
- useEffect(() => {
62
- if (!ready || !iframeRef.current?.contentWindow) return;
63
- iframeRef.current.contentWindow.postMessage({
64
- type: 'SHOW_OUTLINES',
65
- show: showOutlines,
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);
106
- break;
107
- }
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">
132
- <iframe
133
- ref={iframeRef}
134
- className="w-full h-full border-0"
135
- sandbox="allow-scripts allow-same-origin allow-forms allow-modals"
136
- title="Live Canvas Preview"
137
- />
138
- <div
139
- ref={overlayRef}
140
- className="absolute inset-0 z-10"
141
- onDragOver={handleOverlayDragOver}
142
- onDrop={handleOverlayDrop}
143
- onClick={() => onCanvasClick()}
144
- />
145
- </div>
146
- );
147
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
client/src/components/VisualEditor/PropertyPanel.tsx CHANGED
@@ -5,13 +5,14 @@ import {
5
  Plus, Trash2, Palette, Type, Move, Square,
6
  Bold, Italic, AlignLeft, AlignCenter, AlignRight,
7
  MousePointer, EyeOff, Maximize2, Grid3X3, Play, RotateCcw,
8
- Underline, Strikethrough, Layers, Minus
9
  } from 'lucide-react';
10
 
11
  interface PropertyPanelProps {
12
  element: VisualElement | null;
13
  }
14
 
 
15
  const SPACING_PRESETS = ['0', '4px', '8px', '12px', '16px', '24px', '32px', '48px'];
16
  const RADIUS_PRESETS = [
17
  { label: 'None', value: '0' }, { label: 'Small', value: '4px' },
@@ -148,10 +149,7 @@ const CSS_CATEGORIES: { name: string; icon: any; properties: CatProp[] }[] = [
148
  ];
149
 
150
  export default function PropertyPanel({ element }: PropertyPanelProps) {
151
- const {
152
- updateVisualElement, removeVisualElement, setSelectedElement,
153
- setSlotCount, slotCounts, setCustomId, incrementCounter,
154
- } = useEditorStore();
155
  const [expandedCat, setExpandedCat] = useState<string>('Colors');
156
  const [addingToCat, setAddingToCat] = useState<string | null>(null);
157
 
@@ -186,24 +184,6 @@ export default function PropertyPanel({ element }: PropertyPanelProps) {
186
  setAddingToCat(null);
187
  };
188
 
189
- // Slot count handler
190
- const currentSlotCount = slotCounts[element.id] ?? Math.max((element.children || []).length + 1, 1);
191
- const childCount = (element.children || []).length;
192
-
193
- const handleSlotChange = (newCount: number) => {
194
- setSlotCount(element.id, Math.max(newCount, childCount, 1));
195
- };
196
-
197
- // ID edit handler
198
- const handleIdChange = (newId: string) => {
199
- const oldId = element.props?.id || '';
200
- updateVisualElement(element.id, {
201
- props: { ...element.props, id: newId },
202
- attributes: { ...element.attributes, id: newId },
203
- });
204
- setCustomId(element.id, newId.length > 0);
205
- };
206
-
207
  const renderStyleEditor = (prop: CatProp) => {
208
  const value = (element.styles || {})[prop.key] ?? '';
209
  return (
@@ -267,8 +247,8 @@ export default function PropertyPanel({ element }: PropertyPanelProps) {
267
  <div className="grid grid-cols-2 gap-1.5">
268
  <div>
269
  <label className="text-[10px] text-surface-400 block mb-0.5">ID</label>
270
- <input type="text" value={element.props?.id || ''} onChange={(e) => handleIdChange(e.target.value)}
271
- className="w-full bg-surface-800 border border-surface-700 rounded px-1.5 py-0.5 text-[11px] text-surface-200 outline-none" placeholder="auto-generated" />
272
  </div>
273
  <div>
274
  <label className="text-[10px] text-surface-400 block mb-0.5">Classes</label>
@@ -278,34 +258,6 @@ export default function PropertyPanel({ element }: PropertyPanelProps) {
278
  </div>
279
  </div>
280
 
281
- {/* Slot Count Control */}
282
- <div className="border-t border-surface-700 pt-2">
283
- <div className="flex items-center justify-between">
284
- <label className="text-[10px] text-surface-400 flex items-center gap-1">
285
- <Layers className="w-3 h-3" /> Slots
286
- </label>
287
- <div className="flex items-center gap-1">
288
- <button
289
- onClick={() => handleSlotChange(currentSlotCount - 1)}
290
- disabled={currentSlotCount <= Math.max(childCount, 1)}
291
- className="p-0.5 rounded text-surface-400 hover:text-white hover:bg-surface-700 disabled:opacity-30 disabled:cursor-not-allowed"
292
- >
293
- <Minus className="w-3 h-3" />
294
- </button>
295
- <span className="text-[11px] text-surface-200 w-6 text-center">{currentSlotCount}</span>
296
- <button
297
- onClick={() => handleSlotChange(currentSlotCount + 1)}
298
- className="p-0.5 rounded text-surface-400 hover:text-white hover:bg-surface-700"
299
- >
300
- <Plus className="w-3 h-3" />
301
- </button>
302
- </div>
303
- </div>
304
- <p className="text-[9px] text-surface-500 mt-0.5">
305
- Drop zone slots for children {childCount > 0 ? `(${childCount} children)` : '(empty)'}
306
- </p>
307
- </div>
308
-
309
  {/* Text Content */}
310
  {['p','h1','h2','h3','h4','h5','h6','span','a','button','label','li','strong','em','b','i','u','s','mark','figcaption','legend','summary','dt','dd','th','td','caption','option','blockquote','pre','code','abbr','cite','time','address'].includes(element.tagName) && (
311
  <div className="border-t border-surface-700 pt-2">
@@ -373,7 +325,7 @@ export default function PropertyPanel({ element }: PropertyPanelProps) {
373
  </div>
374
  )}
375
 
376
- {/* CSS CATEGORIES WITH PLUS BUTTONS */}
377
  <div className="border-t border-surface-700 pt-2 space-y-1">
378
  <h4 className="text-[10px] font-semibold text-surface-400 uppercase tracking-wider mb-1">CSS Styles</h4>
379
 
@@ -383,6 +335,7 @@ export default function PropertyPanel({ element }: PropertyPanelProps) {
383
  const activeCount = cat.properties.filter(p => activeStyles.has(p.key)).length;
384
  return (
385
  <div key={cat.name} className="border border-surface-700/50 rounded-md">
 
386
  <button onClick={() => setExpandedCat(isOpen ? '' : cat.name)}
387
  className="w-full flex items-center gap-1.5 px-2 py-1.5 bg-surface-800/50 hover:bg-surface-800 text-xs text-surface-300 transition-colors rounded-t-md">
388
  <CatIcon className="w-3.5 h-3.5 text-surface-400" />
@@ -392,8 +345,10 @@ export default function PropertyPanel({ element }: PropertyPanelProps) {
392
 
393
  {isOpen && (
394
  <div className="px-2 py-1.5 space-y-1">
 
395
  {cat.properties.filter(p => activeStyles.has(p.key)).map(p => renderStyleEditor(p))}
396
 
 
397
  {cat.name === 'Spacing & Size' && activeStyles.has('padding') && (
398
  <div className="flex flex-wrap gap-1 pt-1">
399
  {SPACING_PRESETS.map(v => (
@@ -405,6 +360,7 @@ export default function PropertyPanel({ element }: PropertyPanelProps) {
405
  </div>
406
  )}
407
 
 
408
  {cat.name === 'Border & Rounding' && activeStyles.has('borderRadius') && (
409
  <div className="flex flex-wrap gap-1 pt-1">
410
  {RADIUS_PRESETS.map(r => (
@@ -416,6 +372,7 @@ export default function PropertyPanel({ element }: PropertyPanelProps) {
416
  </div>
417
  )}
418
 
 
419
  <div className="relative pt-0.5">
420
  <button onClick={() => setAddingToCat(addingToCat === cat.name ? null : cat.name)}
421
  className="w-full flex items-center gap-1 px-2 py-1 rounded text-[10px] text-surface-500 hover:text-primary-400 hover:bg-surface-800 transition-colors">
@@ -442,6 +399,7 @@ export default function PropertyPanel({ element }: PropertyPanelProps) {
442
  })}
443
  </div>
444
 
 
445
  {activeStyles.size > 0 && (
446
  <div className="flex gap-2 pt-1 border-t border-surface-700">
447
  <button onClick={() => updateVisualElement(element.id, { styles: {} })}
 
5
  Plus, Trash2, Palette, Type, Move, Square,
6
  Bold, Italic, AlignLeft, AlignCenter, AlignRight,
7
  MousePointer, EyeOff, Maximize2, Grid3X3, Play, RotateCcw,
8
+ Underline, Strikethrough
9
  } from 'lucide-react';
10
 
11
  interface PropertyPanelProps {
12
  element: VisualElement | null;
13
  }
14
 
15
+ // Spacing quick buttons
16
  const SPACING_PRESETS = ['0', '4px', '8px', '12px', '16px', '24px', '32px', '48px'];
17
  const RADIUS_PRESETS = [
18
  { label: 'None', value: '0' }, { label: 'Small', value: '4px' },
 
149
  ];
150
 
151
  export default function PropertyPanel({ element }: PropertyPanelProps) {
152
+ const { updateVisualElement, removeVisualElement, setSelectedElement } = useEditorStore();
 
 
 
153
  const [expandedCat, setExpandedCat] = useState<string>('Colors');
154
  const [addingToCat, setAddingToCat] = useState<string | null>(null);
155
 
 
184
  setAddingToCat(null);
185
  };
186
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  const renderStyleEditor = (prop: CatProp) => {
188
  const value = (element.styles || {})[prop.key] ?? '';
189
  return (
 
247
  <div className="grid grid-cols-2 gap-1.5">
248
  <div>
249
  <label className="text-[10px] text-surface-400 block mb-0.5">ID</label>
250
+ <input type="text" value={element.props?.id || ''} onChange={(e) => updateProp('id', e.target.value)}
251
+ className="w-full bg-surface-800 border border-surface-700 rounded px-1.5 py-0.5 text-[11px] text-surface-200 outline-none" placeholder="my-id" />
252
  </div>
253
  <div>
254
  <label className="text-[10px] text-surface-400 block mb-0.5">Classes</label>
 
258
  </div>
259
  </div>
260
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  {/* Text Content */}
262
  {['p','h1','h2','h3','h4','h5','h6','span','a','button','label','li','strong','em','b','i','u','s','mark','figcaption','legend','summary','dt','dd','th','td','caption','option','blockquote','pre','code','abbr','cite','time','address'].includes(element.tagName) && (
263
  <div className="border-t border-surface-700 pt-2">
 
325
  </div>
326
  )}
327
 
328
+ {/* ===== CSS CATEGORIES WITH PLUS BUTTONS ===== */}
329
  <div className="border-t border-surface-700 pt-2 space-y-1">
330
  <h4 className="text-[10px] font-semibold text-surface-400 uppercase tracking-wider mb-1">CSS Styles</h4>
331
 
 
335
  const activeCount = cat.properties.filter(p => activeStyles.has(p.key)).length;
336
  return (
337
  <div key={cat.name} className="border border-surface-700/50 rounded-md">
338
+ {/* Category header */}
339
  <button onClick={() => setExpandedCat(isOpen ? '' : cat.name)}
340
  className="w-full flex items-center gap-1.5 px-2 py-1.5 bg-surface-800/50 hover:bg-surface-800 text-xs text-surface-300 transition-colors rounded-t-md">
341
  <CatIcon className="w-3.5 h-3.5 text-surface-400" />
 
345
 
346
  {isOpen && (
347
  <div className="px-2 py-1.5 space-y-1">
348
+ {/* Active properties */}
349
  {cat.properties.filter(p => activeStyles.has(p.key)).map(p => renderStyleEditor(p))}
350
 
351
+ {/* Quick presets for Spacing */}
352
  {cat.name === 'Spacing & Size' && activeStyles.has('padding') && (
353
  <div className="flex flex-wrap gap-1 pt-1">
354
  {SPACING_PRESETS.map(v => (
 
360
  </div>
361
  )}
362
 
363
+ {/* Quick presets for border radius */}
364
  {cat.name === 'Border & Rounding' && activeStyles.has('borderRadius') && (
365
  <div className="flex flex-wrap gap-1 pt-1">
366
  {RADIUS_PRESETS.map(r => (
 
372
  </div>
373
  )}
374
 
375
+ {/* Add property button */}
376
  <div className="relative pt-0.5">
377
  <button onClick={() => setAddingToCat(addingToCat === cat.name ? null : cat.name)}
378
  className="w-full flex items-center gap-1 px-2 py-1 rounded text-[10px] text-surface-500 hover:text-primary-400 hover:bg-surface-800 transition-colors">
 
399
  })}
400
  </div>
401
 
402
+ {/* Clear all */}
403
  {activeStyles.size > 0 && (
404
  <div className="flex gap-2 pt-1 border-t border-surface-700">
405
  <button onClick={() => updateVisualElement(element.id, { styles: {} })}
client/src/components/VisualEditor/Toolbox.tsx CHANGED
@@ -145,18 +145,13 @@ const ELEMENT_CATEGORIES: { name: string; icon: any; items: ElementPreset[] }[]
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 => {
 
145
  },
146
  ];
147
 
148
+ export default function Toolbox() {
 
 
 
 
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 => {
client/src/components/VisualEditor/VisualEditor.tsx CHANGED
@@ -5,15 +5,16 @@ import { VisualElement } from '../../types/blocks';
5
  import PropertyPanel from './PropertyPanel';
6
  import ElementTree from './ElementTree';
7
  import Toolbox from './Toolbox';
8
- import LiveCanvas from './LiveCanvas';
9
  import {
10
  Monitor, Tablet, Smartphone, Grid3X3,
11
  Eye, EyeOff, Trash2, Copy,
12
  Plus, Code, Palette, Layout
13
  } from 'lucide-react';
14
  import { getColor } from '../Collaboration/CollaboratorAvatars';
 
15
 
16
  type DeviceMode = 'desktop' | 'tablet' | 'mobile';
 
17
 
18
  const DEVICE_SIZES: Record<DeviceMode, { width: number; height: number }> = {
19
  desktop: { width: 1280, height: 800 },
@@ -23,11 +24,11 @@ const DEVICE_SIZES: Record<DeviceMode, { width: number; height: number }> = {
23
 
24
  function uuid() { return crypto.randomUUID?.() || Math.random().toString(36).slice(2, 11); }
25
 
26
- export function createVisualElement(tagName: string, defaultProps: any): VisualElement {
27
  const id = uuid();
28
  return {
29
  id, tagName, type: 'element',
30
- children: [],
31
  props: Object.fromEntries(Object.entries(defaultProps || {}).filter(([k]) => k !== 'children' && k !== 'style')),
32
  styles: defaultProps?.style || {},
33
  classes: [], attributes: {},
@@ -38,6 +39,7 @@ function duplicateElement(el: VisualElement): VisualElement {
38
  return { ...el, id: uuid(), children: (el.children || []).map(duplicateElement) };
39
  }
40
 
 
41
  function findElementById(elements: VisualElement[] | undefined, id: string): VisualElement | null {
42
  if (!elements) return null;
43
  for (const el of elements) {
@@ -47,48 +49,35 @@ function findElementById(elements: VisualElement[] | undefined, id: string): Vis
47
  return null;
48
  }
49
 
50
- function generateElementHtml(elements: VisualElement[]): string {
51
- return elements.map(el => renderElementToHtml(el)).join('\n');
 
 
 
 
 
 
 
52
  }
53
 
54
- function renderElementToHtml(el: VisualElement): string {
55
- const attrs: string[] = [];
56
- attrs.push(`data-rb-editable="true"`);
57
- if (el.props?.id) attrs.push(`id="${escapeHtml(el.props.id)}"`);
58
- if (el.attributes) {
59
- Object.entries(el.attributes).forEach(([k, v]) => {
60
- if (k !== 'id') attrs.push(`${k}="${escapeHtml(v)}"`);
61
  });
62
  }
63
- if (el.classes?.length) attrs.push(`class="${escapeHtml(el.classes.join(' '))}"`);
64
- if (el.props?.src) attrs.push(`src="${escapeHtml(el.props.src)}"`);
65
- if (el.props?.href) attrs.push(`href="${escapeHtml(el.props.href)}"`);
66
- if (el.props?.alt) attrs.push(`alt="${escapeHtml(el.props.alt)}"`);
67
- if (el.props?.placeholder) attrs.push(`placeholder="${escapeHtml(el.props.placeholder)}"`);
68
- if (el.props?.type) attrs.push(`type="${escapeHtml(el.props.type)}"`);
69
- if (el.props?.value) attrs.push(`value="${escapeHtml(el.props.value)}"`);
70
- if (el.props?.name) attrs.push(`name="${escapeHtml(el.props.name)}"`);
71
-
72
- const styleStr = el.styles ? Object.entries(el.styles).map(([k, v]) => `${k}:${v}`).join(';') : '';
73
- if (styleStr) attrs.push(`style="${escapeHtml(styleStr)}"`);
74
-
75
- // Slot count attribute
76
- if (el.slots && el.slots.length > 0) {
77
- attrs.push(`data-rb-has-slots="true" data-rb-slot-count="${el.slots.length}"`);
78
- }
79
-
80
- const isVoidTag = ['br','hr','img','input','link','meta','area','base','col','embed','source','track','wbr'].includes(el.tagName);
81
- if (isVoidTag) {
82
- return `<${el.tagName} ${attrs.join(' ')} />`;
83
  }
84
-
85
- const childrenHtml = (el.children || []).map(c => renderElementToHtml(c)).join('\n');
86
- const textContent = el.props?.textContent || '';
87
- return `<${el.tagName} ${attrs.join(' ')}>${textContent}${childrenHtml}</${el.tagName}>`;
88
- }
89
-
90
- function escapeHtml(str: string): string {
91
- return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
92
  }
93
 
94
  export default function VisualEditor() {
@@ -96,20 +85,37 @@ export default function VisualEditor() {
96
  visualElements, setVisualElements, selectedElementId, setSelectedElement,
97
  viewMode, setViewMode, snapToGrid, toggleSnapToGrid,
98
  addVisualElement, updateVisualElement, removeVisualElement,
99
- activeFileId, fileTree, incrementCounter, setCustomId,
100
- setSlotCount, recalculateSlots, recomputeRegistry,
101
  } = useEditorStore();
102
  const [deviceMode, setDeviceMode] = useState<DeviceMode>('desktop');
 
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);
114
  const collaboratorHoverMap = useMemo(() => {
115
  const map = new Map<string, { userId: string; username: string; color: string }[]>();
@@ -120,9 +126,10 @@ export default function VisualEditor() {
120
  map.set(c.selectedElementId, arr);
121
  }
122
  });
 
 
123
  return map;
124
  }, [collaborators]);
125
-
126
  const cmRef = useRef<HTMLDivElement>(null);
127
 
128
  useEffect(() => {
@@ -133,113 +140,73 @@ export default function VisualEditor() {
133
  return () => document.removeEventListener('mousedown', handler);
134
  }, [contextMenu]);
135
 
136
- // Generate HTML for live canvas
137
- const liveHtml = useMemo(() => {
138
- return generateElementHtml(visualElements || []);
139
- }, [visualElements]);
140
-
141
- // Get CSS and JS from current project context
142
- const liveCss = useMemo(() => {
143
- const cssFile = fileTree.find(f => f.type === 'css');
144
- return cssFile?.content || '';
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) => {
153
- setIframeSelector(selector);
154
- // Try to find element by matching the ID part of the selector
155
- const match = selector.match(/#([\w-]+)/);
156
- if (match) {
157
- const idAttr = match[1];
158
- const el = findElementById(visualElements, selectedElementId || '');
159
- // Search all elements for matching ID attribute
160
- const searchById = (els: VisualElement[]): VisualElement | null => {
161
- for (const e of els) {
162
- if (e.props?.id === idAttr || e.attributes?.id === idAttr) return e;
163
- if (e.children) {
164
- const found = searchById(e.children);
165
- if (found) return found;
166
- }
167
- }
168
- return null;
169
- };
170
- const found = searchById(visualElements || []);
171
- if (found) {
172
- setSelectedElement(found.id);
173
- return;
174
- }
175
  }
176
- setSelectedElement(null);
177
- }, [visualElements, setSelectedElement, selectedElementId]);
178
-
179
- const handleCanvasClick = useCallback(() => {
180
- setSelectedElement(null);
181
- setContextMenu(null);
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();
227
- setContextMenu(null);
228
  setSelectedElement(elementId);
229
- const el = findElementById(visualElements || [], elementId);
230
- if (el?.props?.id) {
231
- setIframeSelector(`#${el.props.id}`);
232
- }
233
- }, [setSelectedElement, visualElements]);
234
 
235
- const handleTreeSelect = useCallback((elementId: string) => {
236
- setContextMenu(null);
237
- setSelectedElement(elementId);
238
- const el = findElementById(visualElements || [], elementId);
239
- if (el?.props?.id) {
240
- setIframeSelector(`#${el.props.id}`);
241
- }
242
- }, [setSelectedElement, visualElements]);
243
 
244
  const handleContextMenu = useCallback((e: React.MouseEvent, elementId: string) => {
245
  e.preventDefault(); e.stopPropagation();
@@ -250,36 +217,13 @@ export default function VisualEditor() {
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;
268
  const el = findElementById(visualElements || [], contextMenu.elementId);
269
- if (el) {
270
- const dup = duplicateElement(el);
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); }
@@ -295,45 +239,26 @@ export default function VisualEditor() {
295
 
296
  const cmAddChild = useCallback(() => {
297
  if (!contextMenu) return;
298
- const child = createVisualElement('div', {});
299
- const autoId = incrementCounter('div');
300
- child.props = { ...child.props, id: autoId };
301
- child.attributes = { ...child.attributes, id: autoId };
302
  addVisualElement(child, contextMenu.elementId);
303
- recalculateSlots(contextMenu.elementId);
304
  setContextMenu(null);
305
- }, [contextMenu, addVisualElement, incrementCounter, recalculateSlots]);
306
 
307
  const handleAddChild = useCallback((parentId: string) => {
308
- const child = createVisualElement('div', {});
309
- const autoId = incrementCounter('div');
310
- child.props = { ...child.props, id: autoId };
311
- child.attributes = { ...child.attributes, id: autoId };
312
- addVisualElement(child, parentId);
313
- recalculateSlots(parentId);
314
- }, [addVisualElement, incrementCounter, recalculateSlots]);
315
 
316
  const cmColorPick = useCallback(() => {
317
  if (!contextMenu) return;
318
- const el = findElementById(visualElements || [], contextMenu.elementId);
319
- if (el) {
320
- updateVisualElement(contextMenu.elementId, {
321
- styles: { ...el.styles, color: '#6366f1' }
322
- });
323
- }
324
  setContextMenu(null);
325
- }, [contextMenu, visualElements, updateVisualElement]);
326
 
327
  const cmBgColorPick = useCallback(() => {
328
  if (!contextMenu) return;
329
- const el = findElementById(visualElements || [], contextMenu.elementId);
330
- if (el) {
331
- updateVisualElement(contextMenu.elementId, {
332
- styles: { ...el.styles, backgroundColor: '#6366f1', color: '#ffffff' }
333
- });
334
- }
335
  setContextMenu(null);
336
- }, [contextMenu, visualElements, updateVisualElement]);
337
 
338
  const ALL_TAGS = ['div','span','p','h1','h2','h3','h4','h5','h6','a','button','input','img','form','section','header','footer','nav','main','aside','ul','ol','li','table','label','textarea','select','article','blockquote','pre','code','figure','figcaption','details','summary','dialog','menu','strong','em','b','i','u','s','mark','small','sub','sup','cite','time','address','video','audio','canvas','iframe','fieldset','legend','progress','meter','dl','dt','dd'];
339
 
@@ -367,7 +292,7 @@ export default function VisualEditor() {
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
 
@@ -391,6 +316,10 @@ export default function VisualEditor() {
391
  className={`p-1.5 rounded-md ${snapToGrid ? 'text-primary-400' : 'text-surface-400 hover:text-white'}`} title="Snap to Grid">
392
  <Grid3X3 className="w-3.5 h-3.5" />
393
  </button>
 
 
 
 
394
  </div>
395
  <div className="flex items-center gap-2">
396
  <span className="text-xs text-surface-500">{(visualElements || []).length} elements</span>
@@ -402,30 +331,36 @@ export default function VisualEditor() {
402
  backgroundImage: snapToGrid ? 'radial-gradient(circle, rgba(148,163,184,0.1) 1px, transparent 1px)' : 'none',
403
  backgroundSize: snapToGrid ? '20px 20px' : 'auto',
404
  }}>
405
- <div
406
- className="bg-white rounded-lg shadow-2xl transition-all duration-300 overflow-hidden"
407
- style={{ width: `${device.width}px`, minHeight: `${device.height}px`, maxWidth: '100%' }}>
 
 
408
  {(visualElements || []).length === 0 ? (
409
  <div className="flex items-center justify-center h-full min-h-[400px]" style={{ color: '#94a3b8', fontSize: '14px' }}>
410
  <div className="text-center">
411
  <Layout className="w-12 h-12 mx-auto mb-3 opacity-30" />
412
  <p>Drag elements from the toolbox</p>
413
- <p className="text-xs mt-1">to the canvas to start building</p>
414
  </div>
415
  </div>
416
  ) : (
417
- <LiveCanvas
418
- html={liveHtml}
419
- css={liveCss}
420
- js={liveJs}
421
- showOutlines={true}
422
- selectedSelector={iframeSelector}
423
- onElementClick={handleElementClick}
424
- onElementDrop={handleElementDrop}
425
- onElementDropFromToolbox={handleElementDropFromToolbox}
426
- onCanvasClick={handleCanvasClick}
427
- dragDataRef={dragDataRef}
428
- />
 
 
 
 
429
  )}
430
  </div>
431
  </div>
@@ -445,7 +380,7 @@ export default function VisualEditor() {
445
  <PropertyPanel element={selectedElement} />
446
  ) : (
447
  <ElementTree elements={visualElements || []} selectedId={selectedElementId}
448
- onSelect={handleTreeSelect} onContextMenu={handleContextMenu} />
449
  )}
450
  </div>
451
 
@@ -465,9 +400,151 @@ export default function VisualEditor() {
465
  </div>
466
  )}
467
 
 
 
 
 
 
 
 
 
 
 
 
 
 
468
  {showTagModal && contextMenu && (
469
  <TagModal currentTag={findElementById(visualElements || [], contextMenu.elementId)?.tagName || 'div'} />
470
  )}
471
  </div>
472
  );
473
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  import PropertyPanel from './PropertyPanel';
6
  import ElementTree from './ElementTree';
7
  import Toolbox from './Toolbox';
 
8
  import {
9
  Monitor, Tablet, Smartphone, Grid3X3,
10
  Eye, EyeOff, Trash2, Copy,
11
  Plus, Code, Palette, Layout
12
  } from 'lucide-react';
13
  import { getColor } from '../Collaboration/CollaboratorAvatars';
14
+ import { getSocket } from '../../hooks/useWebSocket';
15
 
16
  type DeviceMode = 'desktop' | 'tablet' | 'mobile';
17
+ type DropPosition = 'before' | 'after' | 'inside' | null;
18
 
19
  const DEVICE_SIZES: Record<DeviceMode, { width: number; height: number }> = {
20
  desktop: { width: 1280, height: 800 },
 
24
 
25
  function uuid() { return crypto.randomUUID?.() || Math.random().toString(36).slice(2, 11); }
26
 
27
+ function createVisualElement(tagName: string, defaultProps: any): VisualElement {
28
  const id = uuid();
29
  return {
30
  id, tagName, type: 'element',
31
+ children: defaultProps?.children || [],
32
  props: Object.fromEntries(Object.entries(defaultProps || {}).filter(([k]) => k !== 'children' && k !== 'style')),
33
  styles: defaultProps?.style || {},
34
  classes: [], attributes: {},
 
39
  return { ...el, id: uuid(), children: (el.children || []).map(duplicateElement) };
40
  }
41
 
42
+ // Tree helpers
43
  function findElementById(elements: VisualElement[] | undefined, id: string): VisualElement | null {
44
  if (!elements) return null;
45
  for (const el of elements) {
 
49
  return null;
50
  }
51
 
52
+ function removeFromTree(elements: VisualElement[], id: string): { result: VisualElement[]; removed: VisualElement | null } {
53
+ let removed: VisualElement | null = null;
54
+ const filter = (list: VisualElement[]): VisualElement[] =>
55
+ list.filter(el => {
56
+ if (el.id === id) { removed = el; return false; }
57
+ if (el.children) el.children = filter(el.children);
58
+ return true;
59
+ });
60
+ return { result: filter([...elements]), removed };
61
  }
62
 
63
+ function insertAt(elements: VisualElement[], targetId: string, newEl: VisualElement, pos: DropPosition): VisualElement[] {
64
+ if (pos === 'inside') {
65
+ return elements.map(el => {
66
+ if (el.id === targetId) return { ...el, children: [...(el.children || []), newEl] };
67
+ if (el.children) return { ...el, children: insertAt(el.children, targetId, newEl, pos) };
68
+ return el;
 
69
  });
70
  }
71
+ const idx = elements.findIndex(el => el.id === targetId);
72
+ if (idx === -1) {
73
+ return elements.map(el => {
74
+ if (el.children) return { ...el, children: insertAt(el.children, targetId, newEl, pos) };
75
+ return el;
76
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  }
78
+ const arr = [...elements];
79
+ arr.splice(pos === 'before' ? idx : idx + 1, 0, newEl);
80
+ return arr;
 
 
 
 
 
81
  }
82
 
83
  export default function VisualEditor() {
 
85
  visualElements, setVisualElements, selectedElementId, setSelectedElement,
86
  viewMode, setViewMode, snapToGrid, toggleSnapToGrid,
87
  addVisualElement, updateVisualElement, removeVisualElement,
 
 
88
  } = useEditorStore();
89
  const [deviceMode, setDeviceMode] = useState<DeviceMode>('desktop');
90
+ const [showGuides, setShowGuides] = useState(true);
91
  const [contextMenu, setContextMenu] = useState<{ x: number; y: number; elementId: string } | null>(null);
92
+ const [colorPicker, setColorPicker] = useState<{ elementId: string; property: string; x: number; y: number } | null>(null);
93
  const [showTagModal, setShowTagModal] = useState(false);
94
+ const [dragOverId, setDragOverId] = useState<string | null>(null);
95
+ const [dragOverPos, setDragOverPos] = useState<DropPosition>(null);
96
+ const [draggedId, setDraggedId] = useState<string | null>(null);
97
+ const canvasRef = useRef<HTMLDivElement>(null);
98
 
99
+ // Set up global WS hover emit
100
+ useEffect(() => {
101
+ (window as any).__wsElementHoverEmit = (elementId: string | null) => {
102
+ const socket = getSocket();
103
+ const projectId = (window as any).__projectId;
104
+ if (socket?.connected && projectId) {
105
+ socket.emit('visual_element_hover', { projectId, elementId });
106
+ }
107
+ };
108
+ (window as any).__wsElementHover = (elementId: string | null) => {
109
+ const fn = (window as any).__wsElementHoverEmit;
110
+ if (fn) fn(elementId);
111
+ };
112
+ return () => {
113
+ (window as any).__wsElementHover = undefined;
114
+ (window as any).__wsElementHoverEmit = undefined;
115
+ };
116
+ }, []);
117
 
118
+ // Collaborator element hovers for overlay
119
  const collaborators = useCollaborationStore((s) => s.collaborators);
120
  const collaboratorHoverMap = useMemo(() => {
121
  const map = new Map<string, { userId: string; username: string; color: string }[]>();
 
126
  map.set(c.selectedElementId, arr);
127
  }
128
  });
129
+ // Set global for child renderers
130
+ (window as any).__wsCollaboratorHoverMap = map;
131
  return map;
132
  }, [collaborators]);
 
133
  const cmRef = useRef<HTMLDivElement>(null);
134
 
135
  useEffect(() => {
 
140
  return () => document.removeEventListener('mousedown', handler);
141
  }, [contextMenu]);
142
 
143
+ // Combined drop handler: toolbox elements and reorder
144
+ const handleCanvasDrop = useCallback((e: React.DragEvent) => {
145
+ e.preventDefault();
146
+ const toolboxData = e.dataTransfer.getData('application/visual-element');
147
+ if (toolboxData) {
148
+ const preset = JSON.parse(toolboxData);
149
+ const element = createVisualElement(preset.tagName, preset.defaultProps);
150
+ const target = (e.target as HTMLElement).closest('[data-element-id]');
151
+ const parentId = target?.getAttribute('data-element-id') || undefined;
152
+ addVisualElement(element, parentId);
153
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  }
155
+ // Reorder
156
+ const dragId = draggedId;
157
+ const targetId = dragOverId;
158
+ setDraggedId(null);
159
+ setDragOverId(null);
160
+ setDragOverPos(null);
161
+ if (!dragId || !targetId || dragId === targetId) return;
162
+ const { result: afterRemove, removed } = removeFromTree(visualElements || [], dragId);
163
+ if (!removed) { setVisualElements(afterRemove); return; }
164
+ const newTree = insertAt(afterRemove, targetId, removed, dragOverPos);
165
+ setVisualElements(newTree);
166
+ }, [draggedId, dragOverId, dragOverPos, visualElements, setVisualElements, addVisualElement]);
167
+
168
+ const handleElementDragStart = useCallback((e: React.DragEvent, elementId: string) => {
169
+ e.stopPropagation();
170
+ setDraggedId(elementId);
171
+ e.dataTransfer.setData('text/plain', elementId);
172
+ e.dataTransfer.effectAllowed = 'move';
173
+ if (e.currentTarget instanceof HTMLElement) {
174
+ const rect = e.currentTarget.getBoundingClientRect();
175
+ e.dataTransfer.setDragImage(e.currentTarget, e.clientX - rect.left, e.clientY - rect.top);
176
+ }
177
+ }, []);
178
 
179
+ const handleDragOver = useCallback((e: React.DragEvent) => {
180
+ e.preventDefault();
181
+ const targetEl = (e.target as HTMLElement).closest('[data-element-id]');
182
+ const targetId = targetEl?.getAttribute('data-element-id') || null;
183
+ if (!targetId || targetId === draggedId) { setDragOverId(null); setDragOverPos(null); return; }
184
+ setDragOverId(targetId);
185
+ if (targetEl) {
186
+ const rect = targetEl.getBoundingClientRect();
187
+ const y = e.clientY - rect.top;
188
+ const h = rect.height;
189
+ const isContainer = !['INPUT','IMG','BR','HR'].includes(targetEl.tagName);
190
+ if (y < h * 0.25) setDragOverPos('before');
191
+ else if (y > h * 0.75) setDragOverPos('after');
192
+ else if (isContainer) setDragOverPos('inside');
193
+ else setDragOverPos('after');
194
  }
195
+ }, [draggedId]);
196
+
197
+ const handleDragEnd = useCallback(() => {
198
+ setDraggedId(null); setDragOverId(null); setDragOverPos(null);
199
  }, []);
200
 
201
+ const handleElementClick = useCallback((e: React.MouseEvent, elementId: string) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  e.stopPropagation();
203
+ setContextMenu(null); setColorPicker(null);
204
  setSelectedElement(elementId);
205
+ }, [setSelectedElement]);
 
 
 
 
206
 
207
+ const handleCanvasClick = useCallback(() => {
208
+ setSelectedElement(null); setContextMenu(null); setColorPicker(null);
209
+ }, [setSelectedElement]);
 
 
 
 
 
210
 
211
  const handleContextMenu = useCallback((e: React.MouseEvent, elementId: string) => {
212
  e.preventDefault(); e.stopPropagation();
 
217
  const selectedElement = selectedElementId ? findElementById(visualElements || [], selectedElementId) : null;
218
  const device = DEVICE_SIZES[deviceMode];
219
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  // Context menu actions
221
  const cmDuplicate = useCallback(() => {
222
  if (!contextMenu) return;
223
  const el = findElementById(visualElements || [], contextMenu.elementId);
224
+ if (el) addVisualElement(duplicateElement(el));
 
 
 
 
 
 
 
 
 
 
 
225
  setContextMenu(null);
226
+ }, [contextMenu, visualElements, addVisualElement]);
227
 
228
  const cmDelete = useCallback(() => {
229
  if (contextMenu) { removeVisualElement(contextMenu.elementId); setContextMenu(null); }
 
239
 
240
  const cmAddChild = useCallback(() => {
241
  if (!contextMenu) return;
242
+ const child = createVisualElement('div', { style: { padding: '12px', border: '1px dashed #94a3b8', borderRadius: '4px' } });
 
 
 
243
  addVisualElement(child, contextMenu.elementId);
 
244
  setContextMenu(null);
245
+ }, [contextMenu, addVisualElement]);
246
 
247
  const handleAddChild = useCallback((parentId: string) => {
248
+ addVisualElement(createVisualElement('div', { style: { padding: '12px', border: '1px dashed #6366f1', borderRadius: '4px' } }), parentId);
249
+ }, [addVisualElement]);
 
 
 
 
 
250
 
251
  const cmColorPick = useCallback(() => {
252
  if (!contextMenu) return;
253
+ setColorPicker({ elementId: contextMenu.elementId, property: 'color', x: contextMenu.x, y: contextMenu.y });
 
 
 
 
 
254
  setContextMenu(null);
255
+ }, [contextMenu]);
256
 
257
  const cmBgColorPick = useCallback(() => {
258
  if (!contextMenu) return;
259
+ setColorPicker({ elementId: contextMenu.elementId, property: 'backgroundColor', x: contextMenu.x, y: contextMenu.y });
 
 
 
 
 
260
  setContextMenu(null);
261
+ }, [contextMenu]);
262
 
263
  const ALL_TAGS = ['div','span','p','h1','h2','h3','h4','h5','h6','a','button','input','img','form','section','header','footer','nav','main','aside','ul','ol','li','table','label','textarea','select','article','blockquote','pre','code','figure','figcaption','details','summary','dialog','menu','strong','em','b','i','u','s','mark','small','sub','sup','cite','time','address','video','audio','canvas','iframe','fieldset','legend','progress','meter','dl','dt','dd'];
264
 
 
292
  Elements
293
  </div>
294
  <div className="flex-1 overflow-y-auto px-2">
295
+ <Toolbox />
296
  </div>
297
  </div>
298
 
 
316
  className={`p-1.5 rounded-md ${snapToGrid ? 'text-primary-400' : 'text-surface-400 hover:text-white'}`} title="Snap to Grid">
317
  <Grid3X3 className="w-3.5 h-3.5" />
318
  </button>
319
+ <button onClick={() => setShowGuides(!showGuides)}
320
+ className={`p-1.5 rounded-md ${showGuides ? 'text-primary-400' : 'text-surface-400 hover:text-white'}`} title="Guides">
321
+ {showGuides ? <Eye className="w-3.5 h-3.5" /> : <EyeOff className="w-3.5 h-3.5" />}
322
+ </button>
323
  </div>
324
  <div className="flex items-center gap-2">
325
  <span className="text-xs text-surface-500">{(visualElements || []).length} elements</span>
 
331
  backgroundImage: snapToGrid ? 'radial-gradient(circle, rgba(148,163,184,0.1) 1px, transparent 1px)' : 'none',
332
  backgroundSize: snapToGrid ? '20px 20px' : 'auto',
333
  }}>
334
+ <div ref={canvasRef}
335
+ className="bg-white rounded-lg shadow-2xl transition-all duration-300 overflow-hidden relative"
336
+ style={{ width: `${device.width}px`, minHeight: `${device.height}px`, maxWidth: '100%' }}
337
+ onDrop={handleCanvasDrop} onDragOver={handleDragOver}
338
+ onClick={handleCanvasClick}>
339
  {(visualElements || []).length === 0 ? (
340
  <div className="flex items-center justify-center h-full min-h-[400px]" style={{ color: '#94a3b8', fontSize: '14px' }}>
341
  <div className="text-center">
342
  <Layout className="w-12 h-12 mx-auto mb-3 opacity-30" />
343
  <p>Drag elements from the toolbox</p>
344
+ <p className="text-xs mt-1">or drag existing elements to reorder them</p>
345
  </div>
346
  </div>
347
  ) : (
348
+ <div className="p-4">
349
+ {(visualElements || []).map((el) => (
350
+ <VisualElementRenderer key={el.id} element={el}
351
+ selectedId={selectedElementId} onSelect={handleElementClick}
352
+ onContextMenu={handleContextMenu} depth={0}
353
+ dragOverId={dragOverPos ? dragOverId : null}
354
+ dragOverPos={dragOverPos}
355
+ onAddChild={handleAddChild}
356
+ onDragStart={handleElementDragStart}
357
+ onDrop={handleCanvasDrop}
358
+ onDragOver={handleDragOver}
359
+ onDragEnd={handleDragEnd}
360
+ draggedId={draggedId}
361
+ collaboratorHovers={collaboratorHoverMap.get(el.id) || []} />
362
+ ))}
363
+ </div>
364
  )}
365
  </div>
366
  </div>
 
380
  <PropertyPanel element={selectedElement} />
381
  ) : (
382
  <ElementTree elements={visualElements || []} selectedId={selectedElementId}
383
+ onSelect={setSelectedElement} onContextMenu={handleContextMenu} />
384
  )}
385
  </div>
386
 
 
400
  </div>
401
  )}
402
 
403
+ {/* Color Picker */}
404
+ {colorPicker && (
405
+ <div className="fixed z-50" style={{ left: colorPicker.x, top: colorPicker.y }}>
406
+ <input type="color" value="#6366f1" autoFocus
407
+ onChange={(e) => {
408
+ updateVisualElement(colorPicker.elementId, { styles: { ...(findElementById(visualElements || [], colorPicker.elementId)?.styles || {}), [colorPicker.property]: e.target.value } });
409
+ setColorPicker(null);
410
+ }}
411
+ className="w-32 h-32 cursor-pointer border-0 rounded-lg shadow-xl"
412
+ style={{ background: 'none' }} onBlur={() => setColorPicker(null)} />
413
+ </div>
414
+ )}
415
+
416
  {showTagModal && contextMenu && (
417
  <TagModal currentTag={findElementById(visualElements || [], contextMenu.elementId)?.tagName || 'div'} />
418
  )}
419
  </div>
420
  );
421
  }
422
+
423
+ // ===== VISUAL ELEMENT RENDERER =====
424
+ function VisualElementRenderer({
425
+ element, selectedId, onSelect, onContextMenu, depth, dragOverId, dragOverPos,
426
+ onAddChild, onDragStart, onDrop, onDragOver, onDragEnd, draggedId,
427
+ collaboratorHovers,
428
+ }: {
429
+ element: VisualElement; selectedId: string | null;
430
+ onSelect: (e: React.MouseEvent, id: string) => void;
431
+ onContextMenu: (e: React.MouseEvent, id: string) => void;
432
+ depth: number; dragOverId: string | null; dragOverPos: DropPosition | null;
433
+ onAddChild: (id: string) => void;
434
+ onDragStart: (e: React.DragEvent, id: string) => void;
435
+ onDrop: (e: React.DragEvent) => void;
436
+ onDragOver: (e: React.DragEvent) => void;
437
+ onDragEnd: (e: React.DragEvent) => void;
438
+ draggedId: string | null;
439
+ collaboratorHovers: { userId: string; username: string; color: string }[];
440
+ }) {
441
+ const isSelected = selectedId === element.id;
442
+ const isDragTarget = dragOverId === element.id;
443
+ const isDragged = draggedId === element.id;
444
+ const Tag = element.tagName as keyof JSX.IntrinsicElements;
445
+
446
+ // Build border/indicator for drop position
447
+ let dropIndicator: React.CSSProperties = {};
448
+ if (isDragTarget && dragOverPos === 'before') {
449
+ dropIndicator = { borderTop: '3px solid #6366f1' };
450
+ } else if (isDragTarget && dragOverPos === 'after') {
451
+ dropIndicator = { borderBottom: '3px solid #6366f1' };
452
+ } else if (isDragTarget && dragOverPos === 'inside') {
453
+ dropIndicator = { outline: '3px dashed #6366f1', outlineOffset: '2px' };
454
+ }
455
+
456
+ const style: React.CSSProperties = {
457
+ ...element.styles,
458
+ ...dropIndicator,
459
+ outline: isSelected && !isDragTarget ? '2px solid #6366f1' : isDragTarget && dragOverPos === 'inside' ? '3px dashed #6366f1' : collaboratorHovers.length > 0 ? `2px solid ${collaboratorHovers[0].color}` : 'none',
460
+ outlineOffset: isSelected || collaboratorHovers.length > 0 ? '2px' : '0',
461
+ position: 'relative',
462
+ cursor: isDragged ? 'grabbing' : 'grab',
463
+ transition: 'outline 0.1s ease, border 0.1s ease',
464
+ opacity: isDragged ? 0.5 : 1,
465
+ };
466
+
467
+ const attribs: Record<string, string> = { ...element.attributes, 'data-element-id': element.id };
468
+ if (element.props?.src) attribs.src = element.props.src;
469
+ if (element.props?.href) attribs.href = element.props.href;
470
+ if (element.props?.alt) attribs.alt = element.props.alt;
471
+ if (element.props?.placeholder) attribs.placeholder = element.props.placeholder;
472
+ if (element.props?.type) attribs.type = element.props.type;
473
+ if (element.props?.value) attribs.value = element.props.value;
474
+ if (element.props?.name) attribs.name = element.props.name;
475
+ if (element.props?.id) attribs.id = element.props.id;
476
+
477
+ const isVoidTag = ['br','hr','img','input','link','meta','area','base','col','embed','source','track','wbr'].includes(element.tagName);
478
+
479
+ if (isVoidTag) {
480
+ return (
481
+ <div style={{ position: 'relative', display: 'inline-block' }}>
482
+ <Tag {...attribs} style={style} className={element.classes.join(' ')}
483
+ draggable
484
+ onDragStart={(e: React.DragEvent) => onDragStart(e, element.id)}
485
+ onDragOver={onDragOver}
486
+ onDrop={onDrop}
487
+ onDragEnd={onDragEnd}
488
+ onClick={(e: React.MouseEvent) => onSelect(e, element.id)}
489
+ onContextMenu={(e: React.MouseEvent) => onContextMenu(e, element.id)}
490
+ onMouseEnter={() => (window as any).__wsElementHover?.(element.id)}
491
+ onMouseLeave={() => (window as any).__wsElementHover?.(null)} />
492
+ {collaboratorHovers.length > 0 && (
493
+ <div className="absolute top-0 right-0 flex -space-x-1">
494
+ {collaboratorHovers.map((c) => (
495
+ <div key={c.userId} className="w-3.5 h-3.5 rounded-full flex items-center justify-center text-[6px] font-bold text-white"
496
+ style={{ backgroundColor: c.color }} title={c.username}>
497
+ {c.username.charAt(0).toUpperCase()}
498
+ </div>
499
+ ))}
500
+ </div>
501
+ )}
502
+ </div>
503
+ );
504
+ }
505
+
506
+ return (
507
+ <div style={{ position: 'relative' }}>
508
+ <Tag {...attribs} style={style} className={element.classes.join(' ')}
509
+ draggable
510
+ onDragStart={(e: React.DragEvent) => onDragStart(e, element.id)}
511
+ onDragOver={onDragOver}
512
+
513
+ onDrop={onDrop}
514
+ onDragEnd={onDragEnd}
515
+ onClick={(e: React.MouseEvent) => onSelect(e, element.id)}
516
+ onContextMenu={(e: React.MouseEvent) => onContextMenu(e, element.id)}
517
+ onMouseEnter={() => (window as any).__wsElementHover?.(element.id)}
518
+ onMouseLeave={() => (window as any).__wsElementHover?.(null)}>
519
+ {collaboratorHovers.length > 0 && (
520
+ <div className="absolute top-0 right-0 flex -space-x-1 z-10">
521
+ {collaboratorHovers.map((c) => (
522
+ <div key={c.userId} className="w-3.5 h-3.5 rounded-full flex items-center justify-center text-[6px] font-bold text-white"
523
+ style={{ backgroundColor: c.color }} title={c.username}>
524
+ {c.username.charAt(0).toUpperCase()}
525
+ </div>
526
+ ))}
527
+ </div>
528
+ )}
529
+ {element.props?.textContent || ''}
530
+ {(element.children || []).map((child) => (
531
+ <VisualElementRenderer key={child.id} element={child}
532
+ selectedId={selectedId} onSelect={onSelect} onContextMenu={onContextMenu}
533
+ depth={depth + 1} dragOverId={dragOverId} dragOverPos={dragOverPos}
534
+ onAddChild={onAddChild} onDragStart={onDragStart}
535
+ onDrop={onDrop} onDragOver={onDragOver}
536
+ onDragEnd={onDragEnd} draggedId={draggedId}
537
+ collaboratorHovers={(window as any).__wsCollaboratorHoverMap?.get(child.id) || []} />
538
+ ))}
539
+ </Tag>
540
+ {isVoidTag ? null : (
541
+ <button onClick={(e) => { e.stopPropagation(); onAddChild(element.id); }}
542
+ className="absolute -bottom-3 left-1/2 -translate-x-1/2 w-6 h-6 flex items-center justify-center rounded-full
543
+ bg-primary-600 text-white shadow-lg hover:bg-primary-500 transition-all opacity-0 hover:opacity-100 z-10"
544
+ title="Add child element">
545
+ <Plus className="w-3.5 h-3.5" />
546
+ </button>
547
+ )}
548
+ </div>
549
+ );
550
+ }
client/src/components/VisualEditor/editorSandbox.ts DELETED
@@ -1,174 +0,0 @@
1
- export const SANDBOX_SCRIPT = `
2
- (function() {
3
- var selectedSelector = null;
4
-
5
- window.addEventListener('message', function(event) {
6
- var data = event.data;
7
- switch (data.type) {
8
- case 'UPDATE_CONTENT':
9
- updateContent(data.html, data.css, data.js);
10
- break;
11
- case 'GET_ELEMENT_AT':
12
- var el = document.elementFromPoint(data.x - window.frameElement.getBoundingClientRect().left, data.y - window.frameElement.getBoundingClientRect().top);
13
- if (el) {
14
- var selector = getUniqueSelector(el);
15
- if (data.action === 'drop') {
16
- window.parent.postMessage({ type: 'ELEMENT_DROP', selector: selector, x: data.x, y: data.y }, '*');
17
- } else {
18
- window.parent.postMessage({ type: 'ELEMENT_DRAG_OVER', selector: selector, x: data.x, y: data.y }, '*');
19
- }
20
- }
21
- break;
22
- case 'SELECT_ELEMENT':
23
- selectedSelector = data.selector;
24
- document.querySelectorAll('[data-rb-selected]').forEach(function(el) { el.removeAttribute('data-rb-selected'); });
25
- if (data.selector) {
26
- var selEl = document.querySelector(data.selector);
27
- if (selEl) selEl.setAttribute('data-rb-selected', 'true');
28
- }
29
- break;
30
- case 'UPDATE_ELEMENT_ATTRIBUTE':
31
- var target = document.querySelector(data.selector);
32
- if (target) {
33
- target.setAttribute(data.attr, data.value);
34
- if (data.attr === 'id' && data.value) {
35
- target.setAttribute('data-rb-editable', 'true');
36
- }
37
- }
38
- break;
39
- case 'SHOW_OUTLINES':
40
- if (data.show) renderDropZones();
41
- else document.querySelectorAll('.rb-drop-zone').forEach(function(el) { el.remove(); });
42
- break;
43
- }
44
- });
45
-
46
- function updateContent(html, css, js) {
47
- var parser = new DOMParser();
48
- var doc = parser.parseFromString(html, 'text/html');
49
- document.body.innerHTML = doc.body.innerHTML;
50
- document.head.innerHTML = doc.head.innerHTML;
51
-
52
- var styleEl = document.getElementById('rb-styles');
53
- if (!styleEl) {
54
- styleEl = document.createElement('style');
55
- styleEl.id = 'rb-styles';
56
- document.head.appendChild(styleEl);
57
- }
58
- styleEl.textContent = css;
59
-
60
- var scriptEl = document.getElementById('rb-script');
61
- if (!scriptEl) {
62
- scriptEl = document.createElement('script');
63
- scriptEl.id = 'rb-script';
64
- document.body.appendChild(scriptEl);
65
- }
66
- scriptEl.textContent = js;
67
-
68
- attachEditorListeners();
69
- renderDropZones();
70
- }
71
-
72
- function getUniqueSelector(el) {
73
- if (el.id) return '#' + CSS.escape(el.id);
74
- var path = [];
75
- while (el && el.nodeType === Node.ELEMENT_NODE) {
76
- var selector = el.tagName.toLowerCase();
77
- if (el.id) { path.unshift('#' + CSS.escape(el.id)); break; }
78
- var sib = el.parentNode.childNodes;
79
- var idx = 1;
80
- for (var i = 0; i < sib.length; i++) {
81
- if (sib[i] === el) { selector += ':nth-child(' + idx + ')'; break; }
82
- if (sib[i].nodeType === Node.ELEMENT_NODE && sib[i].tagName === el.tagName) idx++;
83
- }
84
- path.unshift(selector);
85
- el = el.parentNode;
86
- }
87
- return path.join(' > ');
88
- }
89
-
90
- function attachEditorListeners() {
91
- document.querySelectorAll('[data-rb-editable]').forEach(function(el) {
92
- el.addEventListener('click', function(e) {
93
- e.stopPropagation();
94
- window.parent.postMessage({ type: 'ELEMENT_CLICK', selector: getUniqueSelector(this) }, '*');
95
- });
96
- el.addEventListener('mouseenter', function() {
97
- window.parent.postMessage({ type: 'ELEMENT_HOVER', selector: getUniqueSelector(this) }, '*');
98
- });
99
- el.addEventListener('mouseleave', function() {
100
- window.parent.postMessage({ type: 'ELEMENT_HOVER', selector: null }, '*');
101
- });
102
- });
103
- document.addEventListener('click', function(e) {
104
- if (e.target === document.body || e.target === document.documentElement) {
105
- window.parent.postMessage({ type: 'CANVAS_CLICK' }, '*');
106
- }
107
- });
108
- }
109
-
110
- function renderDropZones() {
111
- document.querySelectorAll('.rb-drop-zone').forEach(function(el) { el.remove(); });
112
- document.querySelectorAll('[data-rb-has-slots]').forEach(function(parent) {
113
- var slotCount = parseInt(parent.getAttribute('data-rb-slot-count') || '1');
114
- var isForm = parent.tagName === 'FORM';
115
- var existingChildren = parent.querySelectorAll(':scope > [data-rb-editable]');
116
- var renderedSlots = 0;
117
- for (var i = 0; i < slotCount; i++) {
118
- var child = existingChildren[i];
119
- if (!child || (isForm && renderedSlots > 0)) {
120
- var zone = document.createElement('div');
121
- zone.className = 'rb-drop-zone';
122
- zone.dataset.rbSlotIndex = String(i);
123
- zone.dataset.rbParentSelector = getUniqueSelector(parent);
124
- zone.addEventListener('dragover', function(e) { e.preventDefault(); this.classList.add('rb-drop-zone-active'); });
125
- zone.addEventListener('dragleave', function() { this.classList.remove('rb-drop-zone-active'); });
126
- zone.addEventListener('drop', function(e) {
127
- e.preventDefault();
128
- this.classList.remove('rb-drop-zone-active');
129
- window.parent.postMessage({
130
- type: 'ELEMENT_DROP_IN_SLOT',
131
- parentSelector: this.dataset.rbParentSelector,
132
- slotIndex: parseInt(this.dataset.rbSlotIndex || '0'),
133
- }, '*');
134
- });
135
- parent.appendChild(zone);
136
- }
137
- if (child) renderedSlots++;
138
- }
139
- });
140
- }
141
-
142
- window.parent.postMessage({ type: 'READY' }, '*');
143
- attachEditorListeners();
144
- renderDropZones();
145
- })();
146
- `;
147
-
148
- export function generateEditorPage(html: string, css: string, js: string): string {
149
- return `<!DOCTYPE html>
150
- <html lang="en">
151
- <head>
152
- <meta charset="UTF-8">
153
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
154
- <style id="rb-styles">${css}</style>
155
- <style>
156
- [data-rb-editable] { outline: 1px solid transparent; transition: outline 0.1s; cursor: pointer; min-height: 4px; min-width: 4px; }
157
- [data-rb-editable]:hover { outline: 1px solid #6366f1; }
158
- [data-rb-selected] { outline: 2px solid #6366f1 !important; }
159
- .rb-drop-zone { outline: 2px dashed #94a3b8; min-height: 20px; min-width: 100%; transition: all 0.15s; }
160
- .rb-drop-zone-active { outline: 2px dashed #6366f1; background: rgba(99,102,241,0.05); }
161
- </style>
162
- </head>
163
- <body>
164
- ${extractBodyContent(html)}
165
- <script id="rb-script">${js}</script>
166
- <script>${SANDBOX_SCRIPT}</script>
167
- </body>
168
- </html>`;
169
- }
170
-
171
- function extractBodyContent(fullHtml: string): string {
172
- const match = fullHtml.match(/<body[^>]*>([\s\S]*)<\/body>/i);
173
- return match ? match[1] : fullHtml;
174
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
client/src/hooks/useWebSocket.ts CHANGED
@@ -16,9 +16,6 @@ export function useWebSocket(projectId: string | undefined) {
16
  updateCursor,
17
  updateBlockSelection,
18
  updateElementHover,
19
- updatePointerPosition,
20
- updateDraggingBlock,
21
- updateTextFieldCursor,
22
  setConnected,
23
  } = useCollaborationStore();
24
  const socketRef = useRef<Socket | null>(null);
@@ -75,43 +72,12 @@ export function useWebSocket(projectId: string | undefined) {
75
  updateElementHover(userId, elementId);
76
  });
77
 
78
- // NEW: pointer move tracking
79
- socket.on('pointer_move', ({ userId, x, y }: { userId: string; x: number; y: number }) => {
80
- updatePointerPosition(userId, x, y);
81
- });
82
-
83
- // NEW: block drag tracking
84
- socket.on('block_drag_start', ({ userId, blockId, x, y }: { userId: string; blockId: string; x: number; y: number }) => {
85
- updateDraggingBlock(userId, { blockId, x, y });
86
- });
87
-
88
- socket.on('block_drag_move', ({ userId, blockId, x, y }: { userId: string; blockId: string; x: number; y: number }) => {
89
- updateDraggingBlock(userId, { blockId, x, y });
90
- });
91
-
92
- socket.on('block_drag_end', ({ userId }: { userId: string }) => {
93
- updateDraggingBlock(userId, null);
94
- });
95
-
96
- // NEW: text field cursor tracking
97
- socket.on('text_field_focus', ({ userId, fieldId }: { userId: string; fieldId: string }) => {
98
- updateTextFieldCursor(userId, { fieldId, position: 0 });
99
- });
100
-
101
- socket.on('text_field_cursor', ({ userId, fieldId, position }: { userId: string; fieldId: string; position: number }) => {
102
- updateTextFieldCursor(userId, { fieldId, position });
103
- });
104
-
105
- socket.on('text_field_blur', ({ userId }: { userId: string }) => {
106
- updateTextFieldCursor(userId, null);
107
- });
108
-
109
- // File updates
110
  socket.on('file_updated', ({ fileId, content }: { fileId: string; content: string }) => {
111
  const { updateFile } = useCollaborationStore.getState();
112
  updateFile(fileId, content);
113
  });
114
 
 
115
  socket.on('file_added', ({ file, parentId }: { file: any; parentId?: string }) => {
116
  const { addFile } = useEditorStore.getState();
117
  addFile(file, parentId);
@@ -127,7 +93,7 @@ export function useWebSocket(projectId: string | undefined) {
127
  removeFile(fileId);
128
  });
129
 
130
- // Visual element CRUD
131
  socket.on('visual_element_added', ({ element, parentId }: { element: any; parentId?: string }) => {
132
  const { addVisualElement } = useEditorStore.getState();
133
  addVisualElement(element, parentId);
@@ -148,7 +114,6 @@ export function useWebSocket(projectId: string | undefined) {
148
  setVisualElements(elements);
149
  });
150
 
151
- // Project save notification
152
  socket.on('project_saved', ({ updatedAt, savedBy }: { updatedAt: number; savedBy: string }) => {
153
  const { setLastSaved, setSavedBy } = useCollaborationStore.getState();
154
  setLastSaved(updatedAt);
@@ -161,9 +126,8 @@ export function useWebSocket(projectId: string | undefined) {
161
  if (globalSocket === socket) globalSocket = null;
162
  setConnected(false);
163
  };
164
- }, [token, projectId, setCollaborators, addCollaborator, removeCollaborator, updateActiveFile, updateCursor, updateBlockSelection, updateElementHover, updatePointerPosition, updateDraggingBlock, updateTextFieldCursor, setConnected]);
165
 
166
- // ---- EMIT HELPERS ----
167
  const emitFileChanged = useCallback((fileId: string, content: string) => {
168
  if (socketRef.current?.connected && projectId) {
169
  socketRef.current.emit('file_changed', { projectId, fileId, content });
@@ -194,52 +158,6 @@ export function useWebSocket(projectId: string | undefined) {
194
  }
195
  }, [projectId]);
196
 
197
- // NEW: pointer move
198
- const emitPointerMove = useCallback((x: number, y: number) => {
199
- if (socketRef.current?.connected && projectId) {
200
- socketRef.current.emit('pointer_move', { projectId, x, y });
201
- }
202
- }, [projectId]);
203
-
204
- // NEW: block drag
205
- const emitBlockDragStart = useCallback((blockId: string, x: number, y: number) => {
206
- if (socketRef.current?.connected && projectId) {
207
- socketRef.current.emit('block_drag_start', { projectId, blockId, x, y });
208
- }
209
- }, [projectId]);
210
-
211
- const emitBlockDragMove = useCallback((blockId: string, x: number, y: number) => {
212
- if (socketRef.current?.connected && projectId) {
213
- socketRef.current.emit('block_drag_move', { projectId, blockId, x, y });
214
- }
215
- }, [projectId]);
216
-
217
- const emitBlockDragEnd = useCallback(() => {
218
- if (socketRef.current?.connected && projectId) {
219
- socketRef.current.emit('block_drag_end', { projectId });
220
- }
221
- }, [projectId]);
222
-
223
- // NEW: text field
224
- const emitTextFieldFocus = useCallback((fieldId: string) => {
225
- if (socketRef.current?.connected && projectId) {
226
- socketRef.current.emit('text_field_focus', { projectId, fieldId });
227
- }
228
- }, [projectId]);
229
-
230
- const emitTextFieldCursor = useCallback((fieldId: string, position: number) => {
231
- if (socketRef.current?.connected && projectId) {
232
- socketRef.current.emit('text_field_cursor', { projectId, fieldId, position });
233
- }
234
- }, [projectId]);
235
-
236
- const emitTextFieldBlur = useCallback(() => {
237
- if (socketRef.current?.connected && projectId) {
238
- socketRef.current.emit('text_field_blur', { projectId });
239
- }
240
- }, [projectId]);
241
-
242
- // File CRUD
243
  const emitFileAdded = useCallback((file: any, parentId?: string) => {
244
  if (socketRef.current?.connected && projectId) {
245
  socketRef.current.emit('file_added', { projectId, file, parentId });
@@ -258,7 +176,6 @@ export function useWebSocket(projectId: string | undefined) {
258
  }
259
  }, [projectId]);
260
 
261
- // Visual element CRUD
262
  const emitVisualElementAdded = useCallback((element: any, parentId?: string) => {
263
  if (socketRef.current?.connected && projectId) {
264
  socketRef.current.emit('visual_element_added', { projectId, element, parentId });
@@ -283,7 +200,6 @@ export function useWebSocket(projectId: string | undefined) {
283
  }
284
  }, [projectId]);
285
 
286
- // Save via WS
287
  const emitSave = useCallback((data: any): Promise<any> => {
288
  return new Promise((resolve, reject) => {
289
  if (!socketRef.current?.connected || !projectId) {
@@ -297,72 +213,6 @@ export function useWebSocket(projectId: string | undefined) {
297
  });
298
  }, [projectId]);
299
 
300
- // NEW: Project CRUD via WS
301
- const emitProjectList = useCallback((): Promise<any> => {
302
- return new Promise((resolve, reject) => {
303
- if (!socketRef.current?.connected) {
304
- reject(new Error('Not connected'));
305
- return;
306
- }
307
- socketRef.current.emit('project_list', {}, (response: any) => {
308
- if (response?.error) reject(new Error(response.error));
309
- else resolve(response);
310
- });
311
- });
312
- }, []);
313
-
314
- const emitProjectCreate = useCallback((name: string, framework: string, description?: string): Promise<any> => {
315
- return new Promise((resolve, reject) => {
316
- if (!socketRef.current?.connected) {
317
- reject(new Error('Not connected'));
318
- return;
319
- }
320
- socketRef.current.emit('project_create', { name, framework, description }, (response: any) => {
321
- if (response?.error) reject(new Error(response.error));
322
- else resolve(response);
323
- });
324
- });
325
- }, []);
326
-
327
- const emitProjectGet = useCallback((pid: string): Promise<any> => {
328
- return new Promise((resolve, reject) => {
329
- if (!socketRef.current?.connected) {
330
- reject(new Error('Not connected'));
331
- return;
332
- }
333
- socketRef.current.emit('project_get', { projectId: pid }, (response: any) => {
334
- if (response?.error) reject(new Error(response.error));
335
- else resolve(response);
336
- });
337
- });
338
- }, []);
339
-
340
- const emitProjectUpdate = useCallback((pid: string, updates: any): Promise<any> => {
341
- return new Promise((resolve, reject) => {
342
- if (!socketRef.current?.connected) {
343
- reject(new Error('Not connected'));
344
- return;
345
- }
346
- socketRef.current.emit('project_update', { projectId: pid, ...updates }, (response: any) => {
347
- if (response?.error) reject(new Error(response.error));
348
- else resolve(response);
349
- });
350
- });
351
- }, []);
352
-
353
- const emitProjectDelete = useCallback((pid: string): Promise<any> => {
354
- return new Promise((resolve, reject) => {
355
- if (!socketRef.current?.connected) {
356
- reject(new Error('Not connected'));
357
- return;
358
- }
359
- socketRef.current.emit('project_delete', { projectId: pid }, (response: any) => {
360
- if (response?.error) reject(new Error(response.error));
361
- else resolve(response);
362
- });
363
- });
364
- }, []);
365
-
366
  return {
367
  socket: socketRef.current,
368
  emitFileChanged,
@@ -370,13 +220,6 @@ export function useWebSocket(projectId: string | undefined) {
370
  emitCursorMove,
371
  emitBlockSelection,
372
  emitElementHover,
373
- emitPointerMove,
374
- emitBlockDragStart,
375
- emitBlockDragMove,
376
- emitBlockDragEnd,
377
- emitTextFieldFocus,
378
- emitTextFieldCursor,
379
- emitTextFieldBlur,
380
  emitFileAdded,
381
  emitFileRenamed,
382
  emitFileDeleted,
@@ -385,11 +228,6 @@ export function useWebSocket(projectId: string | undefined) {
385
  emitVisualElementRemoved,
386
  emitVisualElementsReordered,
387
  emitSave,
388
- emitProjectList,
389
- emitProjectCreate,
390
- emitProjectGet,
391
- emitProjectUpdate,
392
- emitProjectDelete,
393
  };
394
  }
395
 
 
16
  updateCursor,
17
  updateBlockSelection,
18
  updateElementHover,
 
 
 
19
  setConnected,
20
  } = useCollaborationStore();
21
  const socketRef = useRef<Socket | null>(null);
 
72
  updateElementHover(userId, elementId);
73
  });
74
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  socket.on('file_updated', ({ fileId, content }: { fileId: string; content: string }) => {
76
  const { updateFile } = useCollaborationStore.getState();
77
  updateFile(fileId, content);
78
  });
79
 
80
+ // Remote file CRUD
81
  socket.on('file_added', ({ file, parentId }: { file: any; parentId?: string }) => {
82
  const { addFile } = useEditorStore.getState();
83
  addFile(file, parentId);
 
93
  removeFile(fileId);
94
  });
95
 
96
+ // Remote visual element CRUD
97
  socket.on('visual_element_added', ({ element, parentId }: { element: any; parentId?: string }) => {
98
  const { addVisualElement } = useEditorStore.getState();
99
  addVisualElement(element, parentId);
 
114
  setVisualElements(elements);
115
  });
116
 
 
117
  socket.on('project_saved', ({ updatedAt, savedBy }: { updatedAt: number; savedBy: string }) => {
118
  const { setLastSaved, setSavedBy } = useCollaborationStore.getState();
119
  setLastSaved(updatedAt);
 
126
  if (globalSocket === socket) globalSocket = null;
127
  setConnected(false);
128
  };
129
+ }, [token, projectId, setCollaborators, addCollaborator, removeCollaborator, updateActiveFile, updateCursor, updateBlockSelection, updateElementHover, setConnected]);
130
 
 
131
  const emitFileChanged = useCallback((fileId: string, content: string) => {
132
  if (socketRef.current?.connected && projectId) {
133
  socketRef.current.emit('file_changed', { projectId, fileId, content });
 
158
  }
159
  }, [projectId]);
160
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  const emitFileAdded = useCallback((file: any, parentId?: string) => {
162
  if (socketRef.current?.connected && projectId) {
163
  socketRef.current.emit('file_added', { projectId, file, parentId });
 
176
  }
177
  }, [projectId]);
178
 
 
179
  const emitVisualElementAdded = useCallback((element: any, parentId?: string) => {
180
  if (socketRef.current?.connected && projectId) {
181
  socketRef.current.emit('visual_element_added', { projectId, element, parentId });
 
200
  }
201
  }, [projectId]);
202
 
 
203
  const emitSave = useCallback((data: any): Promise<any> => {
204
  return new Promise((resolve, reject) => {
205
  if (!socketRef.current?.connected || !projectId) {
 
213
  });
214
  }, [projectId]);
215
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  return {
217
  socket: socketRef.current,
218
  emitFileChanged,
 
220
  emitCursorMove,
221
  emitBlockSelection,
222
  emitElementHover,
 
 
 
 
 
 
 
223
  emitFileAdded,
224
  emitFileRenamed,
225
  emitFileDeleted,
 
228
  emitVisualElementRemoved,
229
  emitVisualElementsReordered,
230
  emitSave,
 
 
 
 
 
231
  };
232
  }
233
 
client/src/store/collaborationStore.ts CHANGED
@@ -10,10 +10,6 @@ export interface Collaborator {
10
  cursor: { line: number; ch: number } | null;
11
  selectedBlockId: string | null;
12
  selectedElementId: string | null;
13
- pointerPosition: { x: number; y: number } | null;
14
- draggingBlock: { blockId: string; x: number; y: number } | null;
15
- textFieldCursor: { fieldId: string; position: number } | null;
16
- typing: boolean;
17
  joinedAt: number;
18
  }
19
 
@@ -37,9 +33,6 @@ interface CollaborationStore {
37
  updateCursor: (userId: string, fileId: string, cursor: { line: number; ch: number } | null) => void;
38
  updateBlockSelection: (userId: string, blockId: string | null) => void;
39
  updateElementHover: (userId: string, elementId: string | null) => void;
40
- updatePointerPosition: (userId: string, x: number, y: number) => void;
41
- updateDraggingBlock: (userId: string, dragging: { blockId: string; x: number; y: number } | null) => void;
42
- updateTextFieldCursor: (userId: string, cursor: { fieldId: string; position: number } | null) => void;
43
  updateFile: (fileId: string, content: string) => void;
44
  setLastSaved: (timestamp: number) => void;
45
  setSavedBy: (username: string) => void;
@@ -102,27 +95,6 @@ export const useCollaborationStore = create<CollaborationStore>((set, get) => ({
102
  ),
103
  })),
104
 
105
- updatePointerPosition: (userId, x, y) =>
106
- set((state) => ({
107
- collaborators: state.collaborators.map((c) =>
108
- c.userId === userId ? { ...c, pointerPosition: { x, y } } : c
109
- ),
110
- })),
111
-
112
- updateDraggingBlock: (userId, dragging) =>
113
- set((state) => ({
114
- collaborators: state.collaborators.map((c) =>
115
- c.userId === userId ? { ...c, draggingBlock: dragging } : c
116
- ),
117
- })),
118
-
119
- updateTextFieldCursor: (userId, cursor) =>
120
- set((state) => ({
121
- collaborators: state.collaborators.map((c) =>
122
- c.userId === userId ? { ...c, textFieldCursor: cursor } : c
123
- ),
124
- })),
125
-
126
  updateFile: (fileId, content) => {
127
  const state = useEditorStore.getState();
128
  if (state.activeFileId === fileId) {
 
10
  cursor: { line: number; ch: number } | null;
11
  selectedBlockId: string | null;
12
  selectedElementId: string | null;
 
 
 
 
13
  joinedAt: number;
14
  }
15
 
 
33
  updateCursor: (userId: string, fileId: string, cursor: { line: number; ch: number } | null) => void;
34
  updateBlockSelection: (userId: string, blockId: string | null) => void;
35
  updateElementHover: (userId: string, elementId: string | null) => void;
 
 
 
36
  updateFile: (fileId: string, content: string) => void;
37
  setLastSaved: (timestamp: number) => void;
38
  setSavedBy: (username: string) => void;
 
95
  ),
96
  })),
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  updateFile: (fileId, content) => {
99
  const state = useEditorStore.getState();
100
  if (state.activeFileId === fileId) {
client/src/store/editorStore.ts CHANGED
@@ -1,18 +1,9 @@
1
  import { create } from 'zustand';
2
- import { ProjectFile, VisualElement, OutlineSlot } from '../types/blocks';
3
 
4
  type EditorMode = 'blocks' | 'visual' | 'code' | 'files' | 'preview';
5
  type ViewMode = 'design' | 'preview' | 'split';
6
 
7
- export interface ElementRef {
8
- id: string;
9
- tagName: string;
10
- classes: string[];
11
- attributes: Record<string, string>;
12
- fileId: string;
13
- linkedFiles: string[];
14
- }
15
-
16
  interface EditorStore {
17
  activeFileId: string | null;
18
  activeFileContent: string;
@@ -27,10 +18,6 @@ interface EditorStore {
27
  fileTree: ProjectFile[];
28
  blocksXml: Record<string, string>;
29
  blockCode: Record<string, string>;
30
- elementCounters: Record<string, number>;
31
- elementRegistry: ElementRef[];
32
- slotCounts: Record<string, number>;
33
- customIds: Set<string>;
34
 
35
  setActiveFile: (fileId: string | null) => void;
36
  setActiveFileContent: (content: string) => void;
@@ -42,7 +29,7 @@ interface EditorStore {
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;
@@ -54,39 +41,6 @@ interface EditorStore {
54
  setBlockCode: (fileId: string, code: string) => void;
55
  getBlockCode: (fileId: string) => string;
56
  saveGeneratedCodeToFile: (fileId: string, code: string) => void;
57
- incrementCounter: (tag: string) => string;
58
- resetCounter: (tag: string) => void;
59
- recomputeRegistry: () => void;
60
- recalculateSlots: (elementId: string) => void;
61
- setSlotCount: (elementId: string, count: number) => void;
62
- setCustomId: (elementId: string, isCustom: boolean) => void;
63
- }
64
-
65
- function getChildCount(elements: VisualElement[], id: string): number {
66
- for (const el of elements) {
67
- if (el.id === id) return (el.children || []).length;
68
- if (el.children) {
69
- const found = getChildCount(el.children, id);
70
- if (found !== -1) return found;
71
- }
72
- }
73
- return -1;
74
- }
75
-
76
- function getLinkedFiles(fileId: string, fileTree: ProjectFile[]): string[] {
77
- const file = findFileById(fileTree, fileId);
78
- return file?.linkedFiles || [];
79
- }
80
-
81
- function findFileById(files: ProjectFile[], id: string): ProjectFile | null {
82
- for (const f of files) {
83
- if (f.id === id) return f;
84
- if (f.children) {
85
- const found = findFileById(f.children, id);
86
- if (found) return found;
87
- }
88
- }
89
- return null;
90
  }
91
 
92
  export const useEditorStore = create<EditorStore>((set, get) => ({
@@ -103,10 +57,6 @@ export const useEditorStore = create<EditorStore>((set, get) => ({
103
  fileTree: [],
104
  blocksXml: {},
105
  blockCode: {},
106
- elementCounters: {},
107
- elementRegistry: [],
108
- slotCounts: {},
109
- customIds: new Set<string>(),
110
 
111
  setActiveFile: (fileId) => {
112
  const state = get();
@@ -129,89 +79,57 @@ export const useEditorStore = create<EditorStore>((set, get) => ({
129
  setZoom: (zoom) => set({ zoom: Math.max(0.25, Math.min(2, zoom)) }),
130
  toggleSnapToGrid: () => set((s) => ({ snapToGrid: !s.snapToGrid })),
131
  toggleMiniMap: () => set((s) => ({ showMiniMap: !s.showMiniMap })),
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) };
161
- }
162
- return el;
163
- });
164
- newElements = addToParent(elements);
165
  }
166
- setTimeout(() => {
167
- if (parentId) get().recalculateSlots(parentId);
168
- get().recomputeRegistry();
169
- }, 0);
170
- return { visualElements: newElements };
 
 
 
 
 
 
171
  }),
172
-
173
  updateVisualElement: (id, updates) =>
174
  set((state) => {
175
  const elements = state.visualElements || [];
176
- const updateRecursive = (els: VisualElement[]): VisualElement[] =>
177
- (els || []).map((el) => {
178
  if (el.id === id) return { ...el, ...updates };
179
  if (el.children) return { ...el, children: updateRecursive(el.children) };
180
  return el;
181
  });
182
  return { visualElements: updateRecursive(elements) };
183
  }),
184
-
185
  removeVisualElement: (id) =>
186
  set((state) => {
187
  const elements = state.visualElements || [];
188
- let parentId: string | null = null;
189
- const removeRecursive = (els: VisualElement[], parentElId?: string): VisualElement[] =>
190
- (els || []).filter((el) => {
191
- if (el.id === id) { parentId = parentElId || null; return false; }
192
- if (el.children) el.children = removeRecursive(el.children || [], el.id);
193
  return true;
194
  });
195
- const newElements = removeRecursive(elements);
196
- // Recalculate slots for parent
197
- setTimeout(() => {
198
- if (parentId) get().recalculateSlots(parentId);
199
- get().recomputeRegistry();
200
- }, 0);
201
  return {
202
- visualElements: newElements,
203
  selectedElementId: state.selectedElementId === id ? null : state.selectedElementId,
204
  };
205
  }),
206
-
207
  setFileTree: (files) => set({ fileTree: files || [] }),
208
-
209
  addFile: (file, parentId) =>
210
  set((state) => {
211
  const files = state.fileTree || [];
212
  if (!parentId) return { fileTree: [...files, file] };
213
- const addToFolder = (entries: ProjectFile[]): ProjectFile[] =>
214
- (entries || []).map((f) => {
215
  if (f.id === parentId && f.type === 'folder') {
216
  return { ...f, children: [...(f.children || []), file] };
217
  }
@@ -220,12 +138,11 @@ export const useEditorStore = create<EditorStore>((set, get) => ({
220
  });
221
  return { fileTree: addToFolder(files) };
222
  }),
223
-
224
  removeFile: (id) =>
225
  set((state) => {
226
  const files = state.fileTree || [];
227
- const removeRecursive = (entries: ProjectFile[]): ProjectFile[] =>
228
- (entries || []).filter((f) => {
229
  if (f.id === id) return false;
230
  if (f.children) f.children = removeRecursive(f.children || []);
231
  return true;
@@ -235,17 +152,20 @@ export const useEditorStore = create<EditorStore>((set, get) => ({
235
  activeFileId: state.activeFileId === id ? null : state.activeFileId,
236
  };
237
  }),
238
-
239
- getBlocksXml: (fileId: string) => get().blocksXml[fileId] || '',
240
-
241
  setBlocksXml: (fileId: string, xml: string) =>
242
- set((state) => ({ blocksXml: { ...state.blocksXml, [fileId]: xml } })),
243
-
 
244
  setBlockCode: (fileId: string, code: string) =>
245
- set((state) => ({ blockCode: { ...state.blockCode, [fileId]: code } })),
246
-
247
- getBlockCode: (fileId: string) => get().blockCode[fileId] || '',
248
-
 
 
249
  saveGeneratedCodeToFile: (fileId: string, code: string) =>
250
  set((state) => {
251
  const files = state.fileTree || [];
@@ -260,105 +180,26 @@ export const useEditorStore = create<EditorStore>((set, get) => ({
260
  activeFileContent: fileId === state.activeFileId ? code : state.activeFileContent,
261
  };
262
  }),
263
-
264
  updateFile: (id, updates) =>
265
  set((state) => {
266
  const files = state.fileTree || [];
267
- const updateRecursive = (entries: ProjectFile[]): ProjectFile[] =>
268
- (entries || []).map((f) => {
269
  if (f.id === id) return { ...f, ...updates };
270
  if (f.children) return { ...f, children: updateRecursive(f.children) };
271
  return f;
272
  });
273
  return { fileTree: updateRecursive(files) };
274
  }),
 
275
 
276
- incrementCounter: (tag) => {
277
- const state = get();
278
- const current = state.elementCounters[tag] || 0;
279
- const next = current + 1;
280
- set({ elementCounters: { ...state.elementCounters, [tag]: next } });
281
- return `${tag}_${next}`;
282
- },
283
-
284
- resetCounter: (tag) =>
285
- set((state) => {
286
- const { [tag]: _, ...rest } = state.elementCounters;
287
- return { elementCounters: rest };
288
- }),
289
-
290
- recomputeRegistry: () => {
291
- const state = get();
292
- const registry: ElementRef[] = [];
293
-
294
- const walkElements = (els: VisualElement[], fileId: string) => {
295
- (els || []).forEach((el) => {
296
- registry.push({
297
- id: el.props?.id || el.attributes?.id || el.id,
298
- tagName: el.tagName,
299
- classes: el.classes || [],
300
- attributes: el.attributes || {},
301
- fileId,
302
- linkedFiles: getLinkedFiles(fileId, state.fileTree),
303
- });
304
- if (el.children) walkElements(el.children, fileId);
305
- });
306
- };
307
-
308
- if (state.activeFileId) {
309
- walkElements(state.visualElements, state.activeFileId);
310
  }
311
-
312
- set({ elementRegistry: registry });
313
- },
314
-
315
- recalculateSlots: (elementId) => {
316
- const state = get();
317
- const updateRecursive = (elements: VisualElement[]): VisualElement[] =>
318
- (elements || []).map((el) => {
319
- if (el.id === elementId) {
320
- const childCount = (el.children || []).length;
321
- const userSlotCount = state.slotCounts[elementId];
322
- const targetSlotCount = Math.max(
323
- userSlotCount !== undefined ? Math.max(userSlotCount, childCount) : childCount + 1,
324
- 1
325
- );
326
- const slots: OutlineSlot[] = [];
327
- for (let i = 0; i < targetSlotCount; i++) {
328
- const occupied = (el.children || [])[i];
329
- slots.push({
330
- id: `slot_${el.id}_${i}`,
331
- parentId: elementId,
332
- index: i,
333
- occupiedBy: occupied?.id || null,
334
- });
335
- }
336
- return { ...el, slots };
337
- }
338
- if (el.children) return { ...el, children: updateRecursive(el.children) };
339
- return el;
340
- });
341
- set((state) => ({
342
- visualElements: updateRecursive(state.visualElements),
343
- }));
344
- },
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 },
352
- }));
353
- get().recalculateSlots(elementId);
354
- },
355
-
356
- setCustomId: (elementId, isCustom) => {
357
- set((state) => {
358
- const newSet = new Set(state.customIds);
359
- if (isCustom) newSet.add(elementId);
360
- else newSet.delete(elementId);
361
- return { customIds: newSet };
362
- });
363
- },
364
- }));
 
1
  import { create } from 'zustand';
2
+ import { ProjectFile, VisualElement } from '../types/blocks';
3
 
4
  type EditorMode = 'blocks' | 'visual' | 'code' | 'files' | 'preview';
5
  type ViewMode = 'design' | 'preview' | 'split';
6
 
 
 
 
 
 
 
 
 
 
7
  interface EditorStore {
8
  activeFileId: string | null;
9
  activeFileContent: string;
 
18
  fileTree: ProjectFile[];
19
  blocksXml: Record<string, string>;
20
  blockCode: Record<string, string>;
 
 
 
 
21
 
22
  setActiveFile: (fileId: string | null) => void;
23
  setActiveFileContent: (content: string) => void;
 
29
  toggleSnapToGrid: () => void;
30
  toggleMiniMap: () => void;
31
  setVisualElements: (elements: VisualElement[]) => void;
32
+ addVisualElement: (element: VisualElement, parentId?: string) => void;
33
  updateVisualElement: (id: string, updates: Partial<VisualElement>) => void;
34
  removeVisualElement: (id: string) => void;
35
  setFileTree: (files: ProjectFile[]) => void;
 
41
  setBlockCode: (fileId: string, code: string) => void;
42
  getBlockCode: (fileId: string) => string;
43
  saveGeneratedCodeToFile: (fileId: string, code: string) => void;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  }
45
 
46
  export const useEditorStore = create<EditorStore>((set, get) => ({
 
57
  fileTree: [],
58
  blocksXml: {},
59
  blockCode: {},
 
 
 
 
60
 
61
  setActiveFile: (fileId) => {
62
  const state = get();
 
79
  setZoom: (zoom) => set({ zoom: Math.max(0.25, Math.min(2, zoom)) }),
80
  toggleSnapToGrid: () => set((s) => ({ snapToGrid: !s.snapToGrid })),
81
  toggleMiniMap: () => set((s) => ({ showMiniMap: !s.showMiniMap })),
 
82
  setVisualElements: (elements) => set({ visualElements: elements || [] }),
83
+ addVisualElement: (element, parentId) =>
 
84
  set((state) => {
85
  const elements = state.visualElements || [];
 
86
  if (!parentId) {
87
+ return { visualElements: [...elements, element] };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  }
89
+ const addToParent = (elements: VisualElement[]): VisualElement[] =>
90
+ (elements || []).map((el) => {
91
+ if (el.id === parentId) {
92
+ return { ...el, children: [...(el.children || []), element] };
93
+ }
94
+ if (el.children) {
95
+ return { ...el, children: addToParent(el.children) };
96
+ }
97
+ return el;
98
+ });
99
+ return { visualElements: addToParent(elements) };
100
  }),
 
101
  updateVisualElement: (id, updates) =>
102
  set((state) => {
103
  const elements = state.visualElements || [];
104
+ const updateRecursive = (elements: VisualElement[]): VisualElement[] =>
105
+ (elements || []).map((el) => {
106
  if (el.id === id) return { ...el, ...updates };
107
  if (el.children) return { ...el, children: updateRecursive(el.children) };
108
  return el;
109
  });
110
  return { visualElements: updateRecursive(elements) };
111
  }),
 
112
  removeVisualElement: (id) =>
113
  set((state) => {
114
  const elements = state.visualElements || [];
115
+ const removeRecursive = (elements: VisualElement[]): VisualElement[] =>
116
+ (elements || []).filter((el) => {
117
+ if (el.id === id) return false;
118
+ if (el.children) el.children = removeRecursive(el.children || []);
 
119
  return true;
120
  });
 
 
 
 
 
 
121
  return {
122
+ visualElements: removeRecursive(elements),
123
  selectedElementId: state.selectedElementId === id ? null : state.selectedElementId,
124
  };
125
  }),
 
126
  setFileTree: (files) => set({ fileTree: files || [] }),
 
127
  addFile: (file, parentId) =>
128
  set((state) => {
129
  const files = state.fileTree || [];
130
  if (!parentId) return { fileTree: [...files, file] };
131
+ const addToFolder = (files: ProjectFile[]): ProjectFile[] =>
132
+ (files || []).map((f) => {
133
  if (f.id === parentId && f.type === 'folder') {
134
  return { ...f, children: [...(f.children || []), file] };
135
  }
 
138
  });
139
  return { fileTree: addToFolder(files) };
140
  }),
 
141
  removeFile: (id) =>
142
  set((state) => {
143
  const files = state.fileTree || [];
144
+ const removeRecursive = (files: ProjectFile[]): ProjectFile[] =>
145
+ (files || []).filter((f) => {
146
  if (f.id === id) return false;
147
  if (f.children) f.children = removeRecursive(f.children || []);
148
  return true;
 
152
  activeFileId: state.activeFileId === id ? null : state.activeFileId,
153
  };
154
  }),
155
+ getBlocksXml: (fileId: string) => {
156
+ return get().blocksXml[fileId] || '';
157
+ },
158
  setBlocksXml: (fileId: string, xml: string) =>
159
+ set((state) => ({
160
+ blocksXml: { ...state.blocksXml, [fileId]: xml },
161
+ })),
162
  setBlockCode: (fileId: string, code: string) =>
163
+ set((state) => ({
164
+ blockCode: { ...state.blockCode, [fileId]: code },
165
+ })),
166
+ getBlockCode: (fileId: string) => {
167
+ return get().blockCode[fileId] || '';
168
+ },
169
  saveGeneratedCodeToFile: (fileId: string, code: string) =>
170
  set((state) => {
171
  const files = state.fileTree || [];
 
180
  activeFileContent: fileId === state.activeFileId ? code : state.activeFileContent,
181
  };
182
  }),
 
183
  updateFile: (id, updates) =>
184
  set((state) => {
185
  const files = state.fileTree || [];
186
+ const updateRecursive = (files: ProjectFile[]): ProjectFile[] =>
187
+ (files || []).map((f) => {
188
  if (f.id === id) return { ...f, ...updates };
189
  if (f.children) return { ...f, children: updateRecursive(f.children) };
190
  return f;
191
  });
192
  return { fileTree: updateRecursive(files) };
193
  }),
194
+ }));
195
 
196
+ function findFileById(files: ProjectFile[], id: string): ProjectFile | null {
197
+ for (const f of files) {
198
+ if (f.id === id) return f;
199
+ if (f.children) {
200
+ const found = findFileById(f.children, id);
201
+ if (found) return found;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  }
203
+ }
204
+ return null;
205
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
client/src/store/projectStore.ts CHANGED
@@ -1,7 +1,6 @@
1
  import { create } from 'zustand';
2
  import { api } from '../api/client';
3
  import { Project, FrameworkType } from '../types';
4
- import { getSocket } from '../hooks/useWebSocket';
5
 
6
  interface ProjectStore {
7
  projects: Project[];
@@ -17,20 +16,6 @@ interface ProjectStore {
17
  syncProject: (id: string) => Promise<void>;
18
  }
19
 
20
- function wsCall<T>(event: string, data: any): Promise<T> {
21
- return new Promise((resolve, reject) => {
22
- const socket = getSocket();
23
- if (socket?.connected) {
24
- socket.emit(event, data, (response: any) => {
25
- if (response?.error) reject(new Error(response.error));
26
- else resolve(response);
27
- });
28
- } else {
29
- reject(new Error('WebSocket not connected'));
30
- }
31
- });
32
- }
33
-
34
  export const useProjectStore = create<ProjectStore>((set, get) => ({
35
  projects: [],
36
  currentProject: null,
@@ -40,14 +25,8 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
40
  fetchProjects: async () => {
41
  set({ loading: true, error: null });
42
  try {
43
- const socket = getSocket();
44
- if (socket?.connected) {
45
- const { projects } = await wsCall<any>('project_list', {});
46
- set({ projects, loading: false });
47
- } else {
48
- const { projects } = await api.getProjects();
49
- set({ projects, loading: false });
50
- }
51
  } catch (err: any) {
52
  set({ error: err.message, loading: false });
53
  }
@@ -56,14 +35,8 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
56
  loadProject: async (id: string) => {
57
  set({ loading: true, error: null });
58
  try {
59
- const socket = getSocket();
60
- if (socket?.connected) {
61
- const { project } = await wsCall<any>('project_get', { projectId: id });
62
- set({ currentProject: project, loading: false });
63
- } else {
64
- const { project } = await api.getProject(id);
65
- set({ currentProject: project, loading: false });
66
- }
67
  } catch (err: any) {
68
  set({ error: err.message, loading: false });
69
  }
@@ -72,15 +45,7 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
72
  createProject: async (name: string, framework: FrameworkType, description?: string) => {
73
  set({ loading: true, error: null });
74
  try {
75
- const socket = getSocket();
76
- let project: Project;
77
- if (socket?.connected) {
78
- const result = await wsCall<any>('project_create', { name, framework, description });
79
- project = result.project;
80
- } else {
81
- const result = await api.createProject(name, framework, description);
82
- project = result.project;
83
- }
84
  set((state) => ({ projects: [project, ...state.projects], loading: false }));
85
  return project;
86
  } catch (err: any) {
@@ -91,12 +56,7 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
91
 
92
  updateProject: async (id: string, updates: any) => {
93
  try {
94
- const socket = getSocket();
95
- if (socket?.connected) {
96
- await wsCall<any>('project_update', { projectId: id, ...updates });
97
- } else {
98
- await api.updateProject(id, updates);
99
- }
100
  set((state) => ({
101
  currentProject: state.currentProject?.id === id
102
  ? { ...state.currentProject, ...updates }
@@ -109,12 +69,7 @@ export const useProjectStore = create<ProjectStore>((set, get) => ({
109
 
110
  deleteProject: async (id: string) => {
111
  try {
112
- const socket = getSocket();
113
- if (socket?.connected) {
114
- await wsCall<any>('project_delete', { projectId: id });
115
- } else {
116
- await api.deleteProject(id);
117
- }
118
  set((state) => ({
119
  projects: state.projects.filter((p) => p.id !== id),
120
  currentProject: state.currentProject?.id === id ? null : state.currentProject,
 
1
  import { create } from 'zustand';
2
  import { api } from '../api/client';
3
  import { Project, FrameworkType } from '../types';
 
4
 
5
  interface ProjectStore {
6
  projects: Project[];
 
16
  syncProject: (id: string) => Promise<void>;
17
  }
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  export const useProjectStore = create<ProjectStore>((set, get) => ({
20
  projects: [],
21
  currentProject: null,
 
25
  fetchProjects: async () => {
26
  set({ loading: true, error: null });
27
  try {
28
+ const { projects } = await api.getProjects();
29
+ set({ projects, loading: false });
 
 
 
 
 
 
30
  } catch (err: any) {
31
  set({ error: err.message, loading: false });
32
  }
 
35
  loadProject: async (id: string) => {
36
  set({ loading: true, error: null });
37
  try {
38
+ const { project } = await api.getProject(id);
39
+ set({ currentProject: project, loading: false });
 
 
 
 
 
 
40
  } catch (err: any) {
41
  set({ error: err.message, loading: false });
42
  }
 
45
  createProject: async (name: string, framework: FrameworkType, description?: string) => {
46
  set({ loading: true, error: null });
47
  try {
48
+ const { project } = await api.createProject(name, framework, description);
 
 
 
 
 
 
 
 
49
  set((state) => ({ projects: [project, ...state.projects], loading: false }));
50
  return project;
51
  } catch (err: any) {
 
56
 
57
  updateProject: async (id: string, updates: any) => {
58
  try {
59
+ await api.updateProject(id, updates);
 
 
 
 
 
60
  set((state) => ({
61
  currentProject: state.currentProject?.id === id
62
  ? { ...state.currentProject, ...updates }
 
69
 
70
  deleteProject: async (id: string) => {
71
  try {
72
+ await api.deleteProject(id);
 
 
 
 
 
73
  set((state) => ({
74
  projects: state.projects.filter((p) => p.id !== id),
75
  currentProject: state.currentProject?.id === id ? null : state.currentProject,
client/src/types/blocks.ts CHANGED
@@ -15,8 +15,6 @@ export interface BlockDefinition {
15
  config: BlockConfigField[];
16
  color: string;
17
  icon: string;
18
- dangerous?: boolean;
19
- blockVariables?: { name: string; type: string }[];
20
  compile: (config: Record<string, any>, inputs: Record<string, string>) => string;
21
  }
22
 
@@ -37,7 +35,6 @@ export interface BlockConfigField {
37
  placeholder?: string;
38
  min?: number;
39
  max?: number;
40
- allowVariable?: boolean;
41
  }
42
 
43
  export interface BlockNode {
@@ -57,14 +54,6 @@ export interface VisualElement {
57
  styles: Record<string, string>;
58
  classes: string[];
59
  attributes: Record<string, string>;
60
- slots?: OutlineSlot[];
61
- }
62
-
63
- export interface OutlineSlot {
64
- id: string;
65
- parentId: string;
66
- index: number;
67
- occupiedBy: string | null;
68
  }
69
 
70
  export type { ProjectFile, BlockCategory };
 
15
  config: BlockConfigField[];
16
  color: string;
17
  icon: string;
 
 
18
  compile: (config: Record<string, any>, inputs: Record<string, string>) => string;
19
  }
20
 
 
35
  placeholder?: string;
36
  min?: number;
37
  max?: number;
 
38
  }
39
 
40
  export interface BlockNode {
 
54
  styles: Record<string, string>;
55
  classes: string[];
56
  attributes: Record<string, string>;
 
 
 
 
 
 
 
 
57
  }
58
 
59
  export type { ProjectFile, BlockCategory };
client/src/utils/formatCode.ts DELETED
@@ -1,42 +0,0 @@
1
- import prettier from 'prettier';
2
-
3
- export type CodeLanguage = 'html' | 'css' | 'javascript' | 'typescript' | 'json' | 'markdown';
4
-
5
- const parserMap: Record<CodeLanguage, string> = {
6
- html: 'html',
7
- css: 'css',
8
- javascript: 'babel',
9
- typescript: 'typescript',
10
- json: 'json',
11
- markdown: 'markdown',
12
- };
13
-
14
- export async function formatCode(code: string, language: CodeLanguage): Promise<string> {
15
- try {
16
- const parser = parserMap[language] || 'html';
17
- const formatted = await prettier.format(code, {
18
- parser,
19
- htmlWhitespaceSensitivity: 'ignore',
20
- singleQuote: true,
21
- tabWidth: 2,
22
- trailingComma: 'es5',
23
- printWidth: 100,
24
- });
25
- return formatted;
26
- } catch {
27
- return code;
28
- }
29
- }
30
-
31
- export function getFileLanguage(fileType: string): CodeLanguage {
32
- switch (fileType) {
33
- case 'html': return 'html';
34
- case 'css': return 'css';
35
- case 'js': return 'javascript';
36
- case 'ts':
37
- case 'typescript': return 'typescript';
38
- case 'json': return 'json';
39
- case 'md': return 'markdown';
40
- default: return 'html';
41
- }
42
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
client/tsconfig.json CHANGED
@@ -16,8 +16,7 @@
16
  "noUnusedParameters": false,
17
  "noFallthroughCasesInSwitch": true,
18
  "paths": {
19
- "@/*": ["./src/*"],
20
- "@shared/*": ["../shared/*"]
21
  },
22
  "baseUrl": "."
23
  },
 
16
  "noUnusedParameters": false,
17
  "noFallthroughCasesInSwitch": true,
18
  "paths": {
19
+ "@/*": ["./src/*"]
 
20
  },
21
  "baseUrl": "."
22
  },
client/vite.config.ts CHANGED
@@ -7,7 +7,6 @@ export default defineConfig({
7
  resolve: {
8
  alias: {
9
  '@': path.resolve(__dirname, './src'),
10
- '@shared': path.resolve(__dirname, '../shared'),
11
  },
12
  },
13
  server: {
 
7
  resolve: {
8
  alias: {
9
  '@': path.resolve(__dirname, './src'),
 
10
  },
11
  },
12
  server: {
server/src/services/realtime.ts CHANGED
@@ -4,10 +4,6 @@ import jwt from 'jsonwebtoken';
4
  import { config } from '../config';
5
  import { getDatabase } from '../database';
6
  import { encryptCombined } from './encryption';
7
- import {
8
- listProjects, getProject, createProject, updateProjectData,
9
- updateProjectMeta, deleteProject, checkPermission
10
- } from './wsStore';
11
 
12
  interface AuthPayload {
13
  userId: string;
@@ -24,16 +20,12 @@ interface CollaborationSession {
24
  cursor: { line: number; ch: number } | null;
25
  selectedBlockId: string | null;
26
  selectedElementId: string | null;
27
- pointerPosition: { x: number; y: number } | null;
28
- draggingBlock: { blockId: string; x: number; y: number } | null;
29
- textFieldCursor: { fieldId: string; position: number } | null;
30
- typing: boolean;
31
  joinedAt: number;
32
  permission: 'view' | 'edit' | 'admin';
33
  }
34
 
35
- const sessions = new Map<string, CollaborationSession>();
36
- const projectRooms = new Map<string, Map<string, CollaborationSession[]>>();
37
 
38
  let io: Server;
39
 
@@ -46,12 +38,14 @@ export function initRealtime(httpServer: HttpServer): Server {
46
  },
47
  pingInterval: 25000,
48
  pingTimeout: 20000,
49
- maxHttpBufferSize: 5e7, // 50MB for file uploads
50
  });
51
 
 
52
  io.use((socket, next) => {
53
  const token = socket.handshake.auth?.token || socket.handshake.query?.token;
54
- if (!token) return next(new Error('Authentication required'));
 
 
55
  try {
56
  const decoded = jwt.verify(token as string, config.jwtSecret) as { userId: string; email: string };
57
  const db = getDatabase();
@@ -70,6 +64,8 @@ export function initRealtime(httpServer: HttpServer): Server {
70
  // ---- PROJECT ROOM JOINING ----
71
  socket.on('join_project', ({ projectId }: { projectId: string }) => {
72
  if (!projectId) return;
 
 
73
  const db = getDatabase();
74
  const project = db.prepare('SELECT user_id FROM projects WHERE id = ?').get(projectId) as any;
75
  if (!project) return;
@@ -81,12 +77,13 @@ export function initRealtime(httpServer: HttpServer): Server {
81
  const collab = db.prepare(
82
  'SELECT permission FROM project_collaborators WHERE project_id = ? AND user_id = ?'
83
  ).get(projectId, user.userId) as any;
84
- if (!collab) return;
85
  permission = collab.permission;
86
  }
87
 
88
  socket.join(`project:${projectId}`);
89
 
 
90
  const session: CollaborationSession = {
91
  userId: user.userId,
92
  username: user.username,
@@ -96,10 +93,6 @@ export function initRealtime(httpServer: HttpServer): Server {
96
  cursor: null,
97
  selectedBlockId: null,
98
  selectedElementId: null,
99
- pointerPosition: null,
100
- draggingBlock: null,
101
- textFieldCursor: null,
102
- typing: false,
103
  joinedAt: Date.now(),
104
  permission,
105
  };
@@ -114,6 +107,7 @@ export function initRealtime(httpServer: HttpServer): Server {
114
  }
115
  room.get(user.userId)!.push(session);
116
 
 
117
  socket.to(`project:${projectId}`).emit('collaborator_joined', {
118
  userId: user.userId,
119
  username: user.username,
@@ -121,9 +115,10 @@ export function initRealtime(httpServer: HttpServer): Server {
121
  joinedAt: session.joinedAt,
122
  });
123
 
 
124
  const collaboratorList: any[] = [];
125
- for (const [uid, sList] of room) {
126
- for (const s of sList) {
127
  collaboratorList.push({
128
  userId: uid,
129
  username: s.username,
@@ -132,10 +127,6 @@ export function initRealtime(httpServer: HttpServer): Server {
132
  cursor: s.cursor,
133
  selectedBlockId: s.selectedBlockId,
134
  selectedElementId: s.selectedElementId,
135
- pointerPosition: s.pointerPosition,
136
- draggingBlock: s.draggingBlock,
137
- textFieldCursor: s.textFieldCursor,
138
- typing: s.typing,
139
  joinedAt: s.joinedAt,
140
  });
141
  }
@@ -143,6 +134,7 @@ export function initRealtime(httpServer: HttpServer): Server {
143
  socket.emit('collaborator_list', { collaborators: collaboratorList });
144
  });
145
 
 
146
  socket.on('leave_project', ({ projectId }: { projectId: string }) => {
147
  leaveProject(socket, projectId);
148
  });
@@ -159,9 +151,12 @@ export function initRealtime(httpServer: HttpServer): Server {
159
  });
160
  });
161
 
 
162
  socket.on('active_file_changed', (data: { projectId: string; fileId: string | null }) => {
163
  const session = sessions.get(socket.id);
164
- if (session) session.activeFileId = data.fileId;
 
 
165
  socket.to(`project:${data.projectId}`).emit('collaborator_active_file', {
166
  userId: user.userId,
167
  username: user.username,
@@ -169,6 +164,7 @@ export function initRealtime(httpServer: HttpServer): Server {
169
  });
170
  });
171
 
 
172
  socket.on('cursor_move', (data: { projectId: string; fileId: string; cursor: { line: number; ch: number } }) => {
173
  const session = sessions.get(socket.id);
174
  if (session) {
@@ -183,9 +179,12 @@ export function initRealtime(httpServer: HttpServer): Server {
183
  });
184
  });
185
 
 
186
  socket.on('block_selection_changed', (data: { projectId: string; blockId: string | null }) => {
187
  const session = sessions.get(socket.id);
188
- if (session) session.selectedBlockId = data.blockId;
 
 
189
  socket.to(`project:${data.projectId}`).emit('collaborator_block_selected', {
190
  userId: user.userId,
191
  username: user.username,
@@ -193,9 +192,12 @@ export function initRealtime(httpServer: HttpServer): Server {
193
  });
194
  });
195
 
 
196
  socket.on('visual_element_hover', (data: { projectId: string; elementId: string | null }) => {
197
  const session = sessions.get(socket.id);
198
- if (session) session.selectedElementId = data.elementId;
 
 
199
  socket.to(`project:${data.projectId}`).emit('collaborator_element_hover', {
200
  userId: user.userId,
201
  username: user.username,
@@ -203,48 +205,59 @@ export function initRealtime(httpServer: HttpServer): Server {
203
  });
204
  });
205
 
206
- // ---- FILE CRUD ----
207
  socket.on('file_added', (data: { projectId: string; file: any; parentId?: string }) => {
208
  const session = sessions.get(socket.id);
209
  if (!session || session.permission === 'view') return;
210
  socket.to(`project:${data.projectId}`).emit('file_added', {
211
- file: data.file, parentId: data.parentId, addedBy: user.username,
 
 
212
  });
213
  });
214
 
 
215
  socket.on('file_renamed', (data: { projectId: string; fileId: string; name: string }) => {
216
  const session = sessions.get(socket.id);
217
  if (!session || session.permission === 'view') return;
218
  socket.to(`project:${data.projectId}`).emit('file_renamed', {
219
- fileId: data.fileId, name: data.name, renamedBy: user.username,
 
 
220
  });
221
  });
222
 
 
223
  socket.on('file_deleted', (data: { projectId: string; fileId: string }) => {
224
  const session = sessions.get(socket.id);
225
  if (!session || session.permission === 'view') return;
226
  socket.to(`project:${data.projectId}`).emit('file_deleted', {
227
- fileId: data.fileId, deletedBy: user.username,
 
228
  });
229
  });
230
 
231
- // ---- VISUAL ELEMENT CRUD ----
232
  socket.on('visual_element_added', (data: { projectId: string; element: any; parentId?: string }) => {
233
  const session = sessions.get(socket.id);
234
  if (!session || session.permission === 'view') return;
235
  socket.to(`project:${data.projectId}`).emit('visual_element_added', {
236
- element: data.element, parentId: data.parentId,
 
237
  });
238
  });
239
 
 
240
  socket.on('visual_element_updated', (data: { projectId: string; elementId: string; updates: any }) => {
241
  const session = sessions.get(socket.id);
242
  if (!session || session.permission === 'view') return;
243
  socket.to(`project:${data.projectId}`).emit('visual_element_updated', {
244
- elementId: data.elementId, updates: data.updates,
 
245
  });
246
  });
247
 
 
248
  socket.on('visual_element_removed', (data: { projectId: string; elementId: string }) => {
249
  const session = sessions.get(socket.id);
250
  if (!session || session.permission === 'view') return;
@@ -253,6 +266,7 @@ export function initRealtime(httpServer: HttpServer): Server {
253
  });
254
  });
255
 
 
256
  socket.on('visual_elements_reordered', (data: { projectId: string; elements: any[] }) => {
257
  const session = sessions.get(socket.id);
258
  if (!session || session.permission === 'view') return;
@@ -261,124 +275,7 @@ export function initRealtime(httpServer: HttpServer): Server {
261
  });
262
  });
263
 
264
- // ---- NEW: POINTER MOVE ----
265
- socket.on('pointer_move', (data: { projectId: string; x: number; y: number }) => {
266
- const session = sessions.get(socket.id);
267
- if (session) session.pointerPosition = { x: data.x, y: data.y };
268
- socket.to(`project:${data.projectId}`).emit('pointer_move', {
269
- userId: user.userId, x: data.x, y: data.y,
270
- });
271
- });
272
-
273
- // ---- NEW: BLOCK DRAG BROADCAST ----
274
- socket.on('block_drag_start', (data: { projectId: string; blockId: string; x: number; y: number }) => {
275
- const session = sessions.get(socket.id);
276
- if (session) session.draggingBlock = { blockId: data.blockId, x: data.x, y: data.y };
277
- socket.to(`project:${data.projectId}`).emit('block_drag_start', {
278
- userId: user.userId, blockId: data.blockId, x: data.x, y: data.y,
279
- });
280
- });
281
-
282
- socket.on('block_drag_move', (data: { projectId: string; blockId: string; x: number; y: number }) => {
283
- const session = sessions.get(socket.id);
284
- if (session) session.draggingBlock = { blockId: data.blockId, x: data.x, y: data.y };
285
- socket.to(`project:${data.projectId}`).emit('block_drag_move', {
286
- userId: user.userId, blockId: data.blockId, x: data.x, y: data.y,
287
- });
288
- });
289
-
290
- socket.on('block_drag_end', (data: { projectId: string }) => {
291
- const session = sessions.get(socket.id);
292
- if (session) session.draggingBlock = null;
293
- socket.to(`project:${data.projectId}`).emit('block_drag_end', {
294
- userId: user.userId,
295
- });
296
- });
297
-
298
- // ---- NEW: TEXT FIELD CURSOR TRACKING ----
299
- socket.on('text_field_focus', (data: { projectId: string; fieldId: string }) => {
300
- socket.to(`project:${data.projectId}`).emit('text_field_focus', {
301
- userId: user.userId, fieldId: data.fieldId,
302
- });
303
- });
304
-
305
- socket.on('text_field_cursor', (data: { projectId: string; fieldId: string; position: number }) => {
306
- const session = sessions.get(socket.id);
307
- if (session) session.textFieldCursor = { fieldId: data.fieldId, position: data.position };
308
- socket.to(`project:${data.projectId}`).emit('text_field_cursor', {
309
- userId: user.userId, fieldId: data.fieldId, position: data.position,
310
- });
311
- });
312
-
313
- socket.on('text_field_blur', (data: { projectId: string }) => {
314
- const session = sessions.get(socket.id);
315
- if (session) session.textFieldCursor = null;
316
- socket.to(`project:${data.projectId}`).emit('text_field_blur', {
317
- userId: user.userId,
318
- });
319
- });
320
-
321
- // ---- NEW: PROJECT CRUD VIA WS ----
322
- socket.on('project_list', async (_data: any, callback?: (response: any) => void) => {
323
- try {
324
- const projects = listProjects(user.userId);
325
- if (callback) callback({ projects });
326
- } catch (err: any) {
327
- if (callback) callback({ error: err.message });
328
- }
329
- });
330
-
331
- socket.on('project_create', (data: { name: string; framework: string; description?: string }, callback?: (response: any) => void) => {
332
- try {
333
- const project = createProject(user.userId, data.name, data.framework, data.description);
334
- if (callback) callback({ project });
335
- } catch (err: any) {
336
- if (callback) callback({ error: err.message });
337
- }
338
- });
339
-
340
- socket.on('project_get', (data: { projectId: string }, callback?: (response: any) => void) => {
341
- try {
342
- const project = getProject(data.projectId, user.userId);
343
- if (!project) {
344
- if (callback) callback({ error: 'Project not found' });
345
- return;
346
- }
347
- if (callback) callback({ project });
348
- } catch (err: any) {
349
- if (callback) callback({ error: err.message });
350
- }
351
- });
352
-
353
- socket.on('project_update', (data: { projectId: string; name?: string; description?: string; data?: any }, callback?: (response: any) => void) => {
354
- const permission = checkPermission(data.projectId, user.userId);
355
- if (!permission || permission === 'view') {
356
- if (callback) callback({ error: 'Permission denied' });
357
- return;
358
- }
359
- try {
360
- if (data.data) {
361
- updateProjectData(data.projectId, data.data);
362
- }
363
- if (data.name !== undefined || data.description !== undefined) {
364
- updateProjectMeta(data.projectId, { name: data.name, description: data.description });
365
- }
366
- const updatedAt = Date.now();
367
- io.to(`project:${data.projectId}`).emit('project_saved', {
368
- updatedAt, savedBy: user.username,
369
- });
370
- if (callback) callback({ success: true, updated_at: updatedAt });
371
- } catch (err: any) {
372
- if (callback) callback({ error: err.message });
373
- }
374
- });
375
-
376
- socket.on('project_delete', (data: { projectId: string }, callback?: (response: any) => void) => {
377
- const ok = deleteProject(data.projectId, user.userId);
378
- if (callback) callback(ok ? { success: true } : { error: 'Permission denied or not found' });
379
- });
380
-
381
- // ---- SAVE PROJECT (legacy, kept for backward compat) ----
382
  socket.on('save_project', (data: { projectId: string; data: any }, callback?: (response: any) => void) => {
383
  const session = sessions.get(socket.id);
384
  if (!session || session.permission === 'view') {
@@ -388,57 +285,6 @@ export function initRealtime(httpServer: HttpServer): Server {
388
  saveProjectData(session, data.projectId, data.data, callback);
389
  });
390
 
391
- // ---- NEW: VERSION STUBS ----
392
- socket.on('version_save', (data: { projectId: string; message?: string }, callback?: (response: any) => void) => {
393
- const permission = checkPermission(data.projectId, user.userId);
394
- if (!permission || permission === 'view') {
395
- if (callback) callback({ error: 'Permission denied' });
396
- return;
397
- }
398
- if (callback) callback({ success: true, versionId: null, message: 'Version history not yet implemented' });
399
- });
400
-
401
- socket.on('version_list', (data: { projectId: string }, callback?: (response: any) => void) => {
402
- if (callback) callback({ versions: [] });
403
- });
404
-
405
- // ---- NEW: CSS EVENT STUBS ----
406
- socket.on('css_rule_added', (data: { projectId: string; rule: any }) => {
407
- const session = sessions.get(socket.id);
408
- if (!session || session.permission === 'view') return;
409
- socket.to(`project:${data.projectId}`).emit('css_rule_added', { rule: data.rule });
410
- });
411
-
412
- socket.on('css_rule_updated', (data: { projectId: string; ruleId: string; updates: any }) => {
413
- const session = sessions.get(socket.id);
414
- if (!session || session.permission === 'view') return;
415
- socket.to(`project:${data.projectId}`).emit('css_rule_updated', { ruleId: data.ruleId, updates: data.updates });
416
- });
417
-
418
- socket.on('css_rule_removed', (data: { projectId: string; ruleId: string }) => {
419
- const session = sessions.get(socket.id);
420
- if (!session || session.permission === 'view') return;
421
- socket.to(`project:${data.projectId}`).emit('css_rule_removed', { ruleId: data.ruleId });
422
- });
423
-
424
- socket.on('css_property_added', (data: { projectId: string; ruleId: string; property: any }) => {
425
- const session = sessions.get(socket.id);
426
- if (!session || session.permission === 'view') return;
427
- socket.to(`project:${data.projectId}`).emit('css_property_added', { ruleId: data.ruleId, property: data.property });
428
- });
429
-
430
- socket.on('css_property_updated', (data: { projectId: string; ruleId: string; propertyId: string; updates: any }) => {
431
- const session = sessions.get(socket.id);
432
- if (!session || session.permission === 'view') return;
433
- socket.to(`project:${data.projectId}`).emit('css_property_updated', { ruleId: data.ruleId, propertyId: data.propertyId, updates: data.updates });
434
- });
435
-
436
- socket.on('css_property_removed', (data: { projectId: string; ruleId: string; propertyId: string }) => {
437
- const session = sessions.get(socket.id);
438
- if (!session || session.permission === 'view') return;
439
- socket.to(`project:${data.projectId}`).emit('css_property_removed', { ruleId: data.ruleId, propertyId: data.propertyId });
440
- });
441
-
442
  // ---- DISCONNECT ----
443
  socket.on('disconnect', () => {
444
  const session = sessions.get(socket.id);
 
4
  import { config } from '../config';
5
  import { getDatabase } from '../database';
6
  import { encryptCombined } from './encryption';
 
 
 
 
7
 
8
  interface AuthPayload {
9
  userId: string;
 
20
  cursor: { line: number; ch: number } | null;
21
  selectedBlockId: string | null;
22
  selectedElementId: string | null;
 
 
 
 
23
  joinedAt: number;
24
  permission: 'view' | 'edit' | 'admin';
25
  }
26
 
27
+ const sessions = new Map<string, CollaborationSession>(); // socketId -> session
28
+ const projectRooms = new Map<string, Map<string, CollaborationSession[]>>(); // projectId -> { userId -> sessions[] }
29
 
30
  let io: Server;
31
 
 
38
  },
39
  pingInterval: 25000,
40
  pingTimeout: 20000,
 
41
  });
42
 
43
+ // Auth middleware for socket connections
44
  io.use((socket, next) => {
45
  const token = socket.handshake.auth?.token || socket.handshake.query?.token;
46
+ if (!token) {
47
+ return next(new Error('Authentication required'));
48
+ }
49
  try {
50
  const decoded = jwt.verify(token as string, config.jwtSecret) as { userId: string; email: string };
51
  const db = getDatabase();
 
64
  // ---- PROJECT ROOM JOINING ----
65
  socket.on('join_project', ({ projectId }: { projectId: string }) => {
66
  if (!projectId) return;
67
+
68
+ // Check permission
69
  const db = getDatabase();
70
  const project = db.prepare('SELECT user_id FROM projects WHERE id = ?').get(projectId) as any;
71
  if (!project) return;
 
77
  const collab = db.prepare(
78
  'SELECT permission FROM project_collaborators WHERE project_id = ? AND user_id = ?'
79
  ).get(projectId, user.userId) as any;
80
+ if (!collab) return; // not a collaborator
81
  permission = collab.permission;
82
  }
83
 
84
  socket.join(`project:${projectId}`);
85
 
86
+ // Track session
87
  const session: CollaborationSession = {
88
  userId: user.userId,
89
  username: user.username,
 
93
  cursor: null,
94
  selectedBlockId: null,
95
  selectedElementId: null,
 
 
 
 
96
  joinedAt: Date.now(),
97
  permission,
98
  };
 
107
  }
108
  room.get(user.userId)!.push(session);
109
 
110
+ // Notify others
111
  socket.to(`project:${projectId}`).emit('collaborator_joined', {
112
  userId: user.userId,
113
  username: user.username,
 
115
  joinedAt: session.joinedAt,
116
  });
117
 
118
+ // Send current collaborators to the joining user
119
  const collaboratorList: any[] = [];
120
+ for (const [uid, sessions] of room) {
121
+ for (const s of sessions) {
122
  collaboratorList.push({
123
  userId: uid,
124
  username: s.username,
 
127
  cursor: s.cursor,
128
  selectedBlockId: s.selectedBlockId,
129
  selectedElementId: s.selectedElementId,
 
 
 
 
130
  joinedAt: s.joinedAt,
131
  });
132
  }
 
134
  socket.emit('collaborator_list', { collaborators: collaboratorList });
135
  });
136
 
137
+ // ---- LEAVE PROJECT ----
138
  socket.on('leave_project', ({ projectId }: { projectId: string }) => {
139
  leaveProject(socket, projectId);
140
  });
 
151
  });
152
  });
153
 
154
+ // ---- ACTIVE FILE CHANGE ----
155
  socket.on('active_file_changed', (data: { projectId: string; fileId: string | null }) => {
156
  const session = sessions.get(socket.id);
157
+ if (session) {
158
+ session.activeFileId = data.fileId;
159
+ }
160
  socket.to(`project:${data.projectId}`).emit('collaborator_active_file', {
161
  userId: user.userId,
162
  username: user.username,
 
164
  });
165
  });
166
 
167
+ // ---- CURSOR MOVE ----
168
  socket.on('cursor_move', (data: { projectId: string; fileId: string; cursor: { line: number; ch: number } }) => {
169
  const session = sessions.get(socket.id);
170
  if (session) {
 
179
  });
180
  });
181
 
182
+ // ---- BLOCK SELECTION ----
183
  socket.on('block_selection_changed', (data: { projectId: string; blockId: string | null }) => {
184
  const session = sessions.get(socket.id);
185
+ if (session) {
186
+ session.selectedBlockId = data.blockId;
187
+ }
188
  socket.to(`project:${data.projectId}`).emit('collaborator_block_selected', {
189
  userId: user.userId,
190
  username: user.username,
 
192
  });
193
  });
194
 
195
+ // ---- VISUAL ELEMENT SELECTION / HOVER ----
196
  socket.on('visual_element_hover', (data: { projectId: string; elementId: string | null }) => {
197
  const session = sessions.get(socket.id);
198
+ if (session) {
199
+ session.selectedElementId = data.elementId;
200
+ }
201
  socket.to(`project:${data.projectId}`).emit('collaborator_element_hover', {
202
  userId: user.userId,
203
  username: user.username,
 
205
  });
206
  });
207
 
208
+ // ---- FILE ADDED ----
209
  socket.on('file_added', (data: { projectId: string; file: any; parentId?: string }) => {
210
  const session = sessions.get(socket.id);
211
  if (!session || session.permission === 'view') return;
212
  socket.to(`project:${data.projectId}`).emit('file_added', {
213
+ file: data.file,
214
+ parentId: data.parentId,
215
+ addedBy: user.username,
216
  });
217
  });
218
 
219
+ // ---- FILE RENAMED ----
220
  socket.on('file_renamed', (data: { projectId: string; fileId: string; name: string }) => {
221
  const session = sessions.get(socket.id);
222
  if (!session || session.permission === 'view') return;
223
  socket.to(`project:${data.projectId}`).emit('file_renamed', {
224
+ fileId: data.fileId,
225
+ name: data.name,
226
+ renamedBy: user.username,
227
  });
228
  });
229
 
230
+ // ---- FILE DELETED ----
231
  socket.on('file_deleted', (data: { projectId: string; fileId: string }) => {
232
  const session = sessions.get(socket.id);
233
  if (!session || session.permission === 'view') return;
234
  socket.to(`project:${data.projectId}`).emit('file_deleted', {
235
+ fileId: data.fileId,
236
+ deletedBy: user.username,
237
  });
238
  });
239
 
240
+ // ---- VISUAL ELEMENT ADDED ----
241
  socket.on('visual_element_added', (data: { projectId: string; element: any; parentId?: string }) => {
242
  const session = sessions.get(socket.id);
243
  if (!session || session.permission === 'view') return;
244
  socket.to(`project:${data.projectId}`).emit('visual_element_added', {
245
+ element: data.element,
246
+ parentId: data.parentId,
247
  });
248
  });
249
 
250
+ // ---- VISUAL ELEMENT UPDATED ----
251
  socket.on('visual_element_updated', (data: { projectId: string; elementId: string; updates: any }) => {
252
  const session = sessions.get(socket.id);
253
  if (!session || session.permission === 'view') return;
254
  socket.to(`project:${data.projectId}`).emit('visual_element_updated', {
255
+ elementId: data.elementId,
256
+ updates: data.updates,
257
  });
258
  });
259
 
260
+ // ---- VISUAL ELEMENT REMOVED ----
261
  socket.on('visual_element_removed', (data: { projectId: string; elementId: string }) => {
262
  const session = sessions.get(socket.id);
263
  if (!session || session.permission === 'view') return;
 
266
  });
267
  });
268
 
269
+ // ---- VISUAL ELEMENTS REORDERED ----
270
  socket.on('visual_elements_reordered', (data: { projectId: string; elements: any[] }) => {
271
  const session = sessions.get(socket.id);
272
  if (!session || session.permission === 'view') return;
 
275
  });
276
  });
277
 
278
+ // ---- SAVE PROJECT (via WS instead of REST) ----
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  socket.on('save_project', (data: { projectId: string; data: any }, callback?: (response: any) => void) => {
280
  const session = sessions.get(socket.id);
281
  if (!session || session.permission === 'view') {
 
285
  saveProjectData(session, data.projectId, data.data, callback);
286
  });
287
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  // ---- DISCONNECT ----
289
  socket.on('disconnect', () => {
290
  const session = sessions.get(socket.id);
server/src/services/wsStore.ts DELETED
@@ -1,195 +0,0 @@
1
- import { v4 as uuidv4 } from 'uuid';
2
- import { getDatabase } from '../database';
3
- import { encryptCombined, decryptCombined } from './encryption';
4
-
5
- export interface ProjectRecord {
6
- id: string;
7
- user_id: string;
8
- name: string;
9
- framework: string;
10
- description: string;
11
- encrypted_data: string;
12
- encryption_iv: string;
13
- created_at: number;
14
- updated_at: number;
15
- }
16
-
17
- export function getDefaultProjectData(framework: string): any {
18
- const base = {
19
- files: [] as any[],
20
- blocks: {} as Record<string, any>,
21
- visual: {} as Record<string, any>,
22
- blocksXml: {} as Record<string, string>,
23
- blockCode: {} as Record<string, string>,
24
- };
25
-
26
- switch (framework) {
27
- case 'web':
28
- return {
29
- ...base,
30
- files: [
31
- { id: 'index.html', name: 'index.html', type: 'html', content: '' },
32
- { id: 'styles.css', name: 'styles.css', type: 'css', content: '' },
33
- { id: 'app.js', name: 'app.js', type: 'js', content: '' },
34
- ],
35
- };
36
- case 'electron':
37
- return {
38
- ...base,
39
- files: [
40
- { id: 'main.ts', name: 'main.ts', type: 'typescript', content: '' },
41
- { id: 'preload.ts', name: 'preload.ts', type: 'typescript', content: '' },
42
- { id: 'index.html', name: 'index.html', type: 'html', content: '' },
43
- { id: 'styles.css', name: 'styles.css', type: 'css', content: '' },
44
- { id: 'renderer.ts', name: 'renderer.ts', type: 'typescript', content: '' },
45
- { id: 'package.json', name: 'package.json', type: 'json', content: '' },
46
- ],
47
- };
48
- case 'maui':
49
- return {
50
- ...base,
51
- files: [
52
- { id: 'MainPage.xaml', name: 'MainPage.xaml', type: 'xaml', content: '' },
53
- { id: 'MainPage.xaml.cs', name: 'MainPage.xaml.cs', type: 'csharp', content: '' },
54
- { id: 'App.xaml', name: 'App.xaml', type: 'xaml', content: '' },
55
- { id: 'App.xaml.cs', name: 'App.xaml.cs', type: 'csharp', content: '' },
56
- { id: 'MauiProgram.cs', name: 'MauiProgram.cs', type: 'csharp', content: '' },
57
- { id: 'Models', name: 'Models', type: 'folder', children: [] },
58
- { id: 'ViewModels', name: 'ViewModels', type: 'folder', children: [] },
59
- { id: 'Services', name: 'Services', type: 'folder', children: [] },
60
- ],
61
- };
62
- case 'nodejs':
63
- return {
64
- ...base,
65
- files: [
66
- { id: 'server.js', name: 'server.js', type: 'js', content: '' },
67
- { id: 'package.json', name: 'package.json', type: 'json', content: '' },
68
- { id: '.env', name: '.env', type: 'env', content: '' },
69
- { id: 'routes', name: 'routes', type: 'folder', children: [] },
70
- { id: 'models', name: 'models', type: 'folder', children: [] },
71
- { id: 'middleware', name: 'middleware', type: 'folder', children: [] },
72
- ],
73
- };
74
- default:
75
- return base;
76
- }
77
- }
78
-
79
- export function listProjects(userId: string) {
80
- const db = getDatabase();
81
- const owned = db.prepare(
82
- 'SELECT id, name, framework, description, created_at, updated_at FROM projects WHERE user_id = ? ORDER BY updated_at DESC'
83
- ).all(userId) as any[];
84
-
85
- const collab = db.prepare(`
86
- SELECT p.id, p.name, p.framework, p.description, p.created_at, p.updated_at
87
- FROM projects p
88
- JOIN project_collaborators c ON p.id = c.project_id
89
- WHERE c.user_id = ?
90
- ORDER BY p.updated_at DESC
91
- `).all(userId) as any[];
92
-
93
- const seen = new Set<string>();
94
- return [...owned, ...collab].filter((p) => {
95
- if (seen.has(p.id)) return false;
96
- seen.add(p.id);
97
- return true;
98
- });
99
- }
100
-
101
- export function getProject(projectId: string, userId: string) {
102
- const db = getDatabase();
103
- const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(projectId) as ProjectRecord | undefined;
104
- if (!project) return null;
105
-
106
- const isOwner = project.user_id === userId;
107
- const isCollab = !isOwner && db.prepare(
108
- 'SELECT 1 FROM project_collaborators WHERE project_id = ? AND user_id = ?'
109
- ).get(projectId, userId);
110
-
111
- if (!isOwner && !isCollab) return null;
112
-
113
- try {
114
- const decrypted = decryptCombined(project.encrypted_data);
115
- const data = JSON.parse(decrypted);
116
- return {
117
- id: project.id,
118
- name: project.name,
119
- framework: project.framework,
120
- description: project.description,
121
- data,
122
- created_at: project.created_at,
123
- updated_at: project.updated_at,
124
- };
125
- } catch {
126
- return null;
127
- }
128
- }
129
-
130
- export function createProject(userId: string, name: string, framework: string, description?: string) {
131
- const db = getDatabase();
132
- const defaultData = getDefaultProjectData(framework);
133
- const id = uuidv4();
134
- const now = Date.now();
135
- const combined = encryptCombined(JSON.stringify(defaultData));
136
-
137
- db.prepare(`
138
- INSERT INTO projects (id, user_id, name, framework, description, encrypted_data, encryption_iv, created_at, updated_at)
139
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
140
- `).run(id, userId, name, framework, description || '', combined, '', now, now);
141
-
142
- return {
143
- id,
144
- name,
145
- framework,
146
- description: description || '',
147
- data: defaultData,
148
- created_at: now,
149
- updated_at: now,
150
- };
151
- }
152
-
153
- export function updateProjectData(projectId: string, data: any) {
154
- const db = getDatabase();
155
- const now = Date.now();
156
- const encryptedData = encryptCombined(JSON.stringify(data));
157
- db.prepare(
158
- 'UPDATE projects SET encrypted_data = ?, encryption_iv = ?, updated_at = ? WHERE id = ?'
159
- ).run(encryptedData, '', now, projectId);
160
- return now;
161
- }
162
-
163
- export function updateProjectMeta(projectId: string, updates: { name?: string; description?: string }) {
164
- const db = getDatabase();
165
- const now = Date.now();
166
- const name = updates.name;
167
- const description = updates.description;
168
- db.prepare(`
169
- UPDATE projects SET
170
- name = COALESCE(?, name),
171
- description = COALESCE(?, description),
172
- updated_at = ?
173
- WHERE id = ?
174
- `).run(name || null, description !== undefined ? description : null, now, projectId);
175
- return now;
176
- }
177
-
178
- export function deleteProject(projectId: string, userId: string) {
179
- const db = getDatabase();
180
- const project = db.prepare('SELECT user_id FROM projects WHERE id = ?').get(projectId) as any;
181
- if (!project || project.user_id !== userId) return false;
182
- db.prepare('DELETE FROM projects WHERE id = ?').run(projectId);
183
- return true;
184
- }
185
-
186
- export function checkPermission(projectId: string, userId: string): 'owner' | 'admin' | 'edit' | 'view' | null {
187
- const db = getDatabase();
188
- const project = db.prepare('SELECT user_id FROM projects WHERE id = ?').get(projectId) as any;
189
- if (!project) return null;
190
- if (project.user_id === userId) return 'owner';
191
- const collab = db.prepare(
192
- 'SELECT permission FROM project_collaborators WHERE project_id = ? AND user_id = ?'
193
- ).get(projectId, userId) as any;
194
- return collab ? collab.permission : null;
195
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
shared/types.ts DELETED
@@ -1,255 +0,0 @@
1
- // ============================================================
2
- // ELEMENT SYSTEM
3
- // ============================================================
4
-
5
- export interface VisualElement {
6
- id: string;
7
- type: string;
8
- tagName: string;
9
- children: VisualElement[];
10
- props: Record<string, any>;
11
- styles: Record<string, string>;
12
- classes: string[];
13
- attributes: Record<string, string>;
14
- slots?: OutlineSlot[];
15
- }
16
-
17
- export interface ElementRef {
18
- id: string;
19
- tagName: string;
20
- classes: string[];
21
- attributes: Record<string, string>;
22
- fileId: string;
23
- linkedFiles: string[];
24
- }
25
-
26
- export interface OutlineSlot {
27
- id: string;
28
- parentId: string;
29
- index: number;
30
- occupiedBy: string | null;
31
- }
32
-
33
- // ============================================================
34
- // BLOCK SYSTEM
35
- // ============================================================
36
-
37
- export type BlockShape = 'hat' | 'stack' | 'boolean' | 'reporter' | 'c-block' | 'cap';
38
- export type FrameworkType = 'web' | 'electron' | 'maui' | 'nodejs';
39
-
40
- export interface BlockPort {
41
- id: string;
42
- label: string;
43
- type: 'any' | 'string' | 'number' | 'boolean' | 'object' | 'array' | 'event' | 'element' | 'code';
44
- position: 'left' | 'right' | 'top' | 'bottom';
45
- shape?: 'round' | 'square' | 'diamond';
46
- }
47
-
48
- export interface BlockConfigField {
49
- id: string;
50
- label: string;
51
- type: 'text' | 'number' | 'select' | 'boolean' | 'color' | 'code' | 'slider' | 'textarea' | 'variable' | 'element';
52
- default?: any;
53
- options?: { label: string; value: any }[];
54
- placeholder?: string;
55
- min?: number;
56
- max?: number;
57
- allowVariable?: boolean;
58
- }
59
-
60
- export interface BlockDefinition {
61
- id: string;
62
- type: string;
63
- label: string;
64
- category: string;
65
- description: string;
66
- framework: FrameworkType | 'all';
67
- shape: BlockShape;
68
- inputs: BlockPort[];
69
- outputs: BlockPort[];
70
- config: BlockConfigField[];
71
- color: string;
72
- icon: string;
73
- dangerous?: boolean;
74
- blockVariables?: { name: string; type: string }[];
75
- compile: (config: Record<string, any>, inputs: Record<string, string>) => string;
76
- }
77
-
78
- export interface BlockNode {
79
- id: string;
80
- blockId: string;
81
- position: { x: number; y: number };
82
- config: Record<string, any>;
83
- connections: Record<string, string>;
84
- }
85
-
86
- // ============================================================
87
- // CSS SYSTEM
88
- // ============================================================
89
-
90
- export interface CssRule {
91
- id: string;
92
- selector: string;
93
- type: 'id' | 'class' | 'element' | 'universal' | 'attribute' | 'pseudo-class' | 'pseudo-element';
94
- properties: CssProperty[];
95
- mediaQuery: string;
96
- enabled: boolean;
97
- order: number;
98
- }
99
-
100
- export interface CssProperty {
101
- id: string;
102
- name: string;
103
- value: string;
104
- important: boolean;
105
- unit: 'px' | '%' | 'em' | 'rem' | 'vh' | 'vw' | 'auto' | 'none' | 'ms' | 's' | 'deg' | 'rad';
106
- type: 'standard' | 'variable' | 'custom';
107
- }
108
-
109
- // ============================================================
110
- // VERSION SYSTEM
111
- // ============================================================
112
-
113
- export interface VersionDiff {
114
- fileId: string;
115
- type: 'content_change' | 'file_add' | 'file_remove' | 'file_rename' |
116
- 'css_rule_add' | 'css_rule_remove' | 'css_rule_update' |
117
- 'css_property_add' | 'css_property_remove' | 'css_property_update' |
118
- 'visual_element_add' | 'visual_element_remove' | 'visual_element_update' |
119
- 'element_registry_change' | 'blocks_xml_change' | 'block_code_change' |
120
- 'project_meta_change' | 'binary_file_add';
121
- oldValue?: any;
122
- newValue?: any;
123
- timestamp: number;
124
- }
125
-
126
- export interface VersionEntry {
127
- id: string;
128
- projectId: string;
129
- parentId: string | null;
130
- userId: string;
131
- username: string;
132
- timestamp: number;
133
- message: string;
134
- type: 'auto' | 'restore' | 'manual_branch';
135
- diffs: VersionDiff[];
136
- }
137
-
138
- // ============================================================
139
- // PROJECT SYSTEM
140
- // ============================================================
141
-
142
- export interface ProjectData {
143
- files: ProjectFile[];
144
- blocks: Record<string, BlockNode[]>;
145
- visual: Record<string, VisualElement[]>;
146
- blocksXml: Record<string, string>;
147
- blockCode: Record<string, string>;
148
- cssRules: CssRule[];
149
- elementRegistry: ElementRef[];
150
- elementCounters: Record<string, number>;
151
- }
152
-
153
- export interface ProjectFile {
154
- id: string;
155
- name: string;
156
- type: string;
157
- content?: string;
158
- children?: ProjectFile[];
159
- linkedFiles?: string[];
160
- isBinary?: boolean;
161
- binaryHash?: string;
162
- }
163
-
164
- // ============================================================
165
- // COLLABORATION SYSTEM
166
- // ============================================================
167
-
168
- export interface Collaborator {
169
- userId: string;
170
- username: string;
171
- permission: 'view' | 'edit' | 'admin';
172
- activeFileId: string | null;
173
- cursor: { line: number; ch: number } | null;
174
- selectedBlockId: string | null;
175
- selectedElementId: string | null;
176
- pointerPosition: { x: number; y: number } | null;
177
- draggingBlock: { blockId: string; x: number; y: number } | null;
178
- textFieldCursor: { fieldId: string; position: number } | null;
179
- typing: boolean;
180
- joinedAt: number;
181
- }
182
-
183
- // ============================================================
184
- // WS EVENT NAMES
185
- // ============================================================
186
-
187
- export const WS_EVENTS = {
188
- JOIN_PROJECT: 'join_project',
189
- LEAVE_PROJECT: 'leave_project',
190
-
191
- FILE_CHANGED: 'file_changed',
192
- FILE_ADDED: 'file_added',
193
- FILE_RENAMED: 'file_renamed',
194
- FILE_DELETED: 'file_deleted',
195
- FILE_UPLOAD_START: 'file_upload_start',
196
- FILE_UPLOAD_CHUNK: 'file_upload_chunk',
197
- FILE_UPLOAD_END: 'file_upload_end',
198
- FILE_UPLOAD_ERROR: 'file_upload_error',
199
- FILE_UPLOAD_PROGRESS: 'file_upload_progress',
200
-
201
- VISUAL_ELEMENT_ADDED: 'visual_element_added',
202
- VISUAL_ELEMENT_UPDATED: 'visual_element_updated',
203
- VISUAL_ELEMENT_REMOVED: 'visual_element_removed',
204
- VISUAL_ELEMENTS_REORDERED: 'visual_elements_reordered',
205
- ELEMENT_REGISTRY_UPDATED: 'element_registry_updated',
206
- ELEMENT_ID_CHANGED: 'element_id_changed',
207
-
208
- BLOCK_DRAG_START: 'block_drag_start',
209
- BLOCK_DRAG_MOVE: 'block_drag_move',
210
- BLOCK_DRAG_END: 'block_drag_end',
211
- BLOCK_SELECTION_CHANGED: 'block_selection_changed',
212
-
213
- CSS_RULE_ADDED: 'css_rule_added',
214
- CSS_RULE_UPDATED: 'css_rule_updated',
215
- CSS_RULE_REMOVED: 'css_rule_removed',
216
- CSS_RULE_REORDERED: 'css_rule_reordered',
217
- CSS_PROPERTY_ADDED: 'css_property_added',
218
- CSS_PROPERTY_UPDATED: 'css_property_updated',
219
- CSS_PROPERTY_REMOVED: 'css_property_removed',
220
-
221
- POINTER_MOVE: 'pointer_move',
222
- ACTIVE_FILE_CHANGED: 'active_file_changed',
223
- CURSOR_MOVE: 'cursor_move',
224
- TEXT_FIELD_FOCUS: 'text_field_focus',
225
- TEXT_FIELD_CURSOR: 'text_field_cursor',
226
- TEXT_FIELD_BLUR: 'text_field_blur',
227
-
228
- PROJECT_SAVE: 'project_save',
229
- PROJECT_CREATE: 'project_create',
230
- PROJECT_LIST: 'project_list',
231
- PROJECT_DELETE: 'project_delete',
232
- PROJECT_SAVED: 'project_saved',
233
- PROJECT_CREATED: 'project_created',
234
- PROJECT_LIST_RESULT: 'project_list_result',
235
- PROJECT_DELETED: 'project_deleted',
236
-
237
- VERSION_SAVE: 'version_save',
238
- VERSION_SAVED: 'version_saved',
239
- VERSION_LIST: 'version_list',
240
- VERSION_LIST_RESULT: 'version_list_result',
241
- VERSION_SNAPSHOT: 'version_snapshot',
242
- VERSION_SNAPSHOT_RESULT: 'version_snapshot_result',
243
- VERSION_RESTORE: 'version_restore',
244
- VERSION_RESTORED: 'version_restored',
245
-
246
- COLLABORATOR_JOINED: 'collaborator_joined',
247
- COLLABORATOR_LEFT: 'collaborator_left',
248
- COLLABORATOR_LIST: 'collaborator_list',
249
- COLLABORATOR_ACTIVE_FILE: 'collaborator_active_file',
250
- COLLABORATOR_CURSOR: 'collaborator_cursor',
251
- COLLABORATOR_BLOCK_SELECTED: 'collaborator_block_selected',
252
- COLLABORATOR_ELEMENT_HOVER: 'collaborator_element_hover',
253
-
254
- FILE_UPDATED: 'file_updated',
255
- } as const;