Compare commits

..

2 Commits

5 changed files with 80 additions and 47 deletions

View File

@ -75,6 +75,10 @@ server {
gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml application/rss+xml; gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml application/rss+xml;
# Frontend SPA routing must be last so specific locations above take precedence # Frontend SPA routing must be last so specific locations above take precedence
location = / {
return 301 /velxio/editor;
}
location / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
} }

View File

@ -18,6 +18,7 @@ interface ComponentPropertyDialogProps {
onClose: () => void; onClose: () => void;
onRotate: (componentId: string) => void; onRotate: (componentId: string) => void;
onDelete: (componentId: string) => void; onDelete: (componentId: string) => void;
lockComponents?: boolean;
} }
export const ComponentPropertyDialog: React.FC<ComponentPropertyDialogProps> = ({ export const ComponentPropertyDialog: React.FC<ComponentPropertyDialogProps> = ({
@ -29,6 +30,7 @@ export const ComponentPropertyDialog: React.FC<ComponentPropertyDialogProps> = (
onClose, onClose,
onRotate, onRotate,
onDelete, onDelete,
lockComponents = false,
}) => { }) => {
const dialogRef = useRef<HTMLDivElement>(null); const dialogRef = useRef<HTMLDivElement>(null);
const [dialogPosition, setDialogPosition] = useState({ x: 0, y: 0 }); const [dialogPosition, setDialogPosition] = useState({ x: 0, y: 0 });
@ -151,17 +153,19 @@ export const ComponentPropertyDialog: React.FC<ComponentPropertyDialogProps> = (
> >
Rotate Rotate
</button> </button>
<button {!lockComponents && (
className="property-action-button delete-button" <button
onClick={() => { className="property-action-button delete-button"
if (window.confirm(`Delete ${componentMetadata.name}?`)) { onClick={() => {
onDelete(componentId); if (window.confirm(`Delete ${componentMetadata.name}?`)) {
} onDelete(componentId);
}} }
title="Delete component" }}
> title="Delete component"
Delete >
</button> Delete
</button>
)}
</div> </div>
</div> </div>
); );

View File

@ -59,6 +59,7 @@ export const SimulatorCanvas = () => {
updateComponent, updateComponent,
serialMonitorOpen, serialMonitorOpen,
toggleSerialMonitor, toggleSerialMonitor,
lockComponents,
} = useSimulatorStore(); } = useSimulatorStore();
// Active board (for WiFi/BLE status display) // Active board (for WiFi/BLE status display)
@ -992,6 +993,8 @@ export const SimulatorCanvas = () => {
useEffect(() => { useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Delete' || e.key === 'Backspace') { if (e.key === 'Delete' || e.key === 'Backspace') {
if (lockComponents) return; // Prevent deletion if locked
if (selectedComponentId) { if (selectedComponentId) {
removeComponent(selectedComponentId); removeComponent(selectedComponentId);
setSelectedComponentId(null); setSelectedComponentId(null);
@ -1004,7 +1007,7 @@ export const SimulatorCanvas = () => {
window.addEventListener('keydown', handleKeyDown); window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown);
}, [selectedComponentId, removeComponent, activeBoardId, boards.length]); }, [selectedComponentId, removeComponent, activeBoardId, boards.length, lockComponents]);
// Handle component selection from modal // Handle component selection from modal
const handleSelectComponent = (metadata: ComponentMetadata) => { const handleSelectComponent = (metadata: ComponentMetadata) => {
@ -1607,18 +1610,20 @@ export const SimulatorCanvas = () => {
</span> </span>
{/* Add Component */} {/* Add Component */}
<button {!lockComponents && (
className="add-component-btn" <button
onClick={() => setShowComponentPicker(true)} className="add-component-btn"
title="Add Component" onClick={() => setShowComponentPicker(true)}
disabled={running} title="Add Component"
> disabled={running}
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"> >
<line x1="12" y1="5" x2="12" y2="19" /> <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<line x1="5" y1="12" x2="19" y2="12" /> <line x1="12" y1="5" x2="12" y2="19" />
</svg> <line x1="5" y1="12" x2="19" y2="12" />
Add </svg>
</button> Add
</button>
)}
</div> </div>
</div> </div>
@ -1831,6 +1836,7 @@ export const SimulatorCanvas = () => {
removeComponent(id); removeComponent(id);
setShowPropertyDialog(false); setShowPropertyDialog(false);
}} }}
lockComponents={lockComponents}
/> />
); );
})()} })()}
@ -1884,29 +1890,31 @@ export const SimulatorCanvas = () => {
<div style={{ padding: '6px 14px', color: '#888', fontSize: 11, borderBottom: '1px solid #3c3c3c', marginBottom: 2 }}> <div style={{ padding: '6px 14px', color: '#888', fontSize: 11, borderBottom: '1px solid #3c3c3c', marginBottom: 2 }}>
{label} {label}
</div> </div>
<button {!lockComponents && (
style={{ <button
display: 'flex', alignItems: 'center', gap: 8, style={{
width: '100%', padding: '7px 14px', background: 'none', border: 'none', display: 'flex', alignItems: 'center', gap: 8,
color: boards.length <= 1 ? '#555' : '#e06c75', cursor: boards.length <= 1 ? 'default' : 'pointer', width: '100%', padding: '7px 14px', background: 'none', border: 'none',
fontSize: 13, textAlign: 'left', color: boards.length <= 1 ? '#555' : '#e06c75', cursor: boards.length <= 1 ? 'default' : 'pointer',
}} fontSize: 13, textAlign: 'left',
disabled={boards.length <= 1} }}
title={boards.length <= 1 ? 'Cannot remove the last board' : undefined} disabled={boards.length <= 1}
onMouseEnter={(e) => { if (boards.length > 1) (e.currentTarget.style.background = '#2a2d2e'); }} title={boards.length <= 1 ? 'Cannot remove the last board' : undefined}
onMouseLeave={(e) => { e.currentTarget.style.background = 'none'; }} onMouseEnter={(e) => { if (boards.length > 1) (e.currentTarget.style.background = '#2a2d2e'); }}
onClick={() => { onMouseLeave={(e) => { e.currentTarget.style.background = 'none'; }}
setBoardContextMenu(null); onClick={() => {
setBoardToRemove(boardContextMenu.boardId); setBoardContextMenu(null);
}} setBoardToRemove(boardContextMenu.boardId);
> }}
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> >
<polyline points="3 6 5 6 21 6" /> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /> <polyline points="3 6 5 6 21 6" />
</svg> <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
Remove board </svg>
{connectedWires > 0 && <span style={{ color: '#888', fontSize: 11 }}>({connectedWires} wire{connectedWires > 1 ? 's' : ''})</span>} Remove board
</button> {connectedWires > 0 && <span style={{ color: '#888', fontSize: 11 }}>({connectedWires} wire{connectedWires > 1 ? 's' : ''})</span>}
</button>
)}
</div> </div>
</> </>
); );

View File

@ -61,9 +61,18 @@ export const EditorPage: React.FC = () => {
return { return {
enabled: params.get('embed') === 'true', enabled: params.get('embed') === 'true',
hideEditor: params.get('hideEditor') === 'true', hideEditor: params.get('hideEditor') === 'true',
lockComponents: params.get('lockComponents') === 'true',
}; };
}); });
const [embedHideComponentPicker, setEmbedHideComponentPicker] = useState(false); const [embedHideComponentPicker, setEmbedHideComponentPicker] = useState(false);
const setLockComponents = useSimulatorStore((s) => s.setLockComponents);
// Initial URL params sync
useEffect(() => {
if (embedMode.lockComponents) {
setLockComponents(true);
}
}, [embedMode.lockComponents, setLockComponents]);
// Listen for embed mode commands from parent // Listen for embed mode commands from parent
useEffect(() => { useEffect(() => {
@ -71,6 +80,7 @@ export const EditorPage: React.FC = () => {
const handler = (e: Event) => { const handler = (e: Event) => {
const detail = (e as CustomEvent).detail || {}; const detail = (e as CustomEvent).detail || {};
if (detail.hideComponentPicker) setEmbedHideComponentPicker(true); if (detail.hideComponentPicker) setEmbedHideComponentPicker(true);
if (detail.lockComponents !== undefined) setLockComponents(detail.lockComponents);
}; };
window.addEventListener('velxio-embed-mode', handler); window.addEventListener('velxio-embed-mode', handler);
// Notify parent that Velxio is ready // Notify parent that Velxio is ready

View File

@ -222,6 +222,10 @@ interface SimulatorState {
esp32CrashBoardId: string | null; esp32CrashBoardId: string | null;
dismissEsp32Crash: () => void; dismissEsp32Crash: () => void;
// ── Lock components ─────────────────────────────────────────────────────
lockComponents: boolean;
setLockComponents: (locked: boolean) => void;
// ── Components ────────────────────────────────────────────────────────── // ── Components ──────────────────────────────────────────────────────────
components: Component[]; components: Component[];
addComponent: (component: Component) => void; addComponent: (component: Component) => void;
@ -786,6 +790,9 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
esp32CrashBoardId: null, esp32CrashBoardId: null,
dismissEsp32Crash: () => set({ esp32CrashBoardId: null }), dismissEsp32Crash: () => set({ esp32CrashBoardId: null }),
lockComponents: false,
setLockComponents: (locked: boolean) => set({ lockComponents: locked }),
setBoardType: (type: BoardType) => { setBoardType: (type: BoardType) => {
const { activeBoardId, running, stopSimulation } = get(); const { activeBoardId, running, stopSimulation } = get();
if (running) stopSimulation(); if (running) stopSimulation();