Compare commits
2 Commits
eb7f87dfbc
...
9402b6c40f
| Author | SHA1 | Date |
|---|---|---|
|
|
9402b6c40f | |
|
|
87be2526ba |
|
|
@ -75,6 +75,10 @@ server {
|
|||
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
|
||||
location = / {
|
||||
return 301 /velxio/editor;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ interface ComponentPropertyDialogProps {
|
|||
onClose: () => void;
|
||||
onRotate: (componentId: string) => void;
|
||||
onDelete: (componentId: string) => void;
|
||||
lockComponents?: boolean;
|
||||
}
|
||||
|
||||
export const ComponentPropertyDialog: React.FC<ComponentPropertyDialogProps> = ({
|
||||
|
|
@ -29,6 +30,7 @@ export const ComponentPropertyDialog: React.FC<ComponentPropertyDialogProps> = (
|
|||
onClose,
|
||||
onRotate,
|
||||
onDelete,
|
||||
lockComponents = false,
|
||||
}) => {
|
||||
const dialogRef = useRef<HTMLDivElement>(null);
|
||||
const [dialogPosition, setDialogPosition] = useState({ x: 0, y: 0 });
|
||||
|
|
@ -151,17 +153,19 @@ export const ComponentPropertyDialog: React.FC<ComponentPropertyDialogProps> = (
|
|||
>
|
||||
Rotate
|
||||
</button>
|
||||
<button
|
||||
className="property-action-button delete-button"
|
||||
onClick={() => {
|
||||
if (window.confirm(`Delete ${componentMetadata.name}?`)) {
|
||||
onDelete(componentId);
|
||||
}
|
||||
}}
|
||||
title="Delete component"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
{!lockComponents && (
|
||||
<button
|
||||
className="property-action-button delete-button"
|
||||
onClick={() => {
|
||||
if (window.confirm(`Delete ${componentMetadata.name}?`)) {
|
||||
onDelete(componentId);
|
||||
}
|
||||
}}
|
||||
title="Delete component"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ export const SimulatorCanvas = () => {
|
|||
updateComponent,
|
||||
serialMonitorOpen,
|
||||
toggleSerialMonitor,
|
||||
lockComponents,
|
||||
} = useSimulatorStore();
|
||||
|
||||
// Active board (for WiFi/BLE status display)
|
||||
|
|
@ -992,6 +993,8 @@ export const SimulatorCanvas = () => {
|
|||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||
if (lockComponents) return; // Prevent deletion if locked
|
||||
|
||||
if (selectedComponentId) {
|
||||
removeComponent(selectedComponentId);
|
||||
setSelectedComponentId(null);
|
||||
|
|
@ -1004,7 +1007,7 @@ export const SimulatorCanvas = () => {
|
|||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [selectedComponentId, removeComponent, activeBoardId, boards.length]);
|
||||
}, [selectedComponentId, removeComponent, activeBoardId, boards.length, lockComponents]);
|
||||
|
||||
// Handle component selection from modal
|
||||
const handleSelectComponent = (metadata: ComponentMetadata) => {
|
||||
|
|
@ -1607,18 +1610,20 @@ export const SimulatorCanvas = () => {
|
|||
</span>
|
||||
|
||||
{/* Add Component */}
|
||||
<button
|
||||
className="add-component-btn"
|
||||
onClick={() => setShowComponentPicker(true)}
|
||||
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" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
Add
|
||||
</button>
|
||||
{!lockComponents && (
|
||||
<button
|
||||
className="add-component-btn"
|
||||
onClick={() => setShowComponentPicker(true)}
|
||||
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" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
Add
|
||||
</button>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1831,6 +1836,7 @@ export const SimulatorCanvas = () => {
|
|||
removeComponent(id);
|
||||
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 }}>
|
||||
{label}
|
||||
</div>
|
||||
<button
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
width: '100%', padding: '7px 14px', background: 'none', border: 'none',
|
||||
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}
|
||||
onMouseEnter={(e) => { if (boards.length > 1) (e.currentTarget.style.background = '#2a2d2e'); }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.background = 'none'; }}
|
||||
onClick={() => {
|
||||
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" />
|
||||
<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" />
|
||||
</svg>
|
||||
Remove board
|
||||
{connectedWires > 0 && <span style={{ color: '#888', fontSize: 11 }}>({connectedWires} wire{connectedWires > 1 ? 's' : ''})</span>}
|
||||
</button>
|
||||
{!lockComponents && (
|
||||
<button
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
width: '100%', padding: '7px 14px', background: 'none', border: 'none',
|
||||
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}
|
||||
onMouseEnter={(e) => { if (boards.length > 1) (e.currentTarget.style.background = '#2a2d2e'); }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.background = 'none'; }}
|
||||
onClick={() => {
|
||||
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" />
|
||||
<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" />
|
||||
</svg>
|
||||
Remove board
|
||||
{connectedWires > 0 && <span style={{ color: '#888', fontSize: 11 }}>({connectedWires} wire{connectedWires > 1 ? 's' : ''})</span>}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -61,9 +61,18 @@ export const EditorPage: React.FC = () => {
|
|||
return {
|
||||
enabled: params.get('embed') === 'true',
|
||||
hideEditor: params.get('hideEditor') === 'true',
|
||||
lockComponents: params.get('lockComponents') === 'true',
|
||||
};
|
||||
});
|
||||
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
|
||||
useEffect(() => {
|
||||
|
|
@ -71,6 +80,7 @@ export const EditorPage: React.FC = () => {
|
|||
const handler = (e: Event) => {
|
||||
const detail = (e as CustomEvent).detail || {};
|
||||
if (detail.hideComponentPicker) setEmbedHideComponentPicker(true);
|
||||
if (detail.lockComponents !== undefined) setLockComponents(detail.lockComponents);
|
||||
};
|
||||
window.addEventListener('velxio-embed-mode', handler);
|
||||
// Notify parent that Velxio is ready
|
||||
|
|
|
|||
|
|
@ -222,6 +222,10 @@ interface SimulatorState {
|
|||
esp32CrashBoardId: string | null;
|
||||
dismissEsp32Crash: () => void;
|
||||
|
||||
// ── Lock components ─────────────────────────────────────────────────────
|
||||
lockComponents: boolean;
|
||||
setLockComponents: (locked: boolean) => void;
|
||||
|
||||
// ── Components ──────────────────────────────────────────────────────────
|
||||
components: Component[];
|
||||
addComponent: (component: Component) => void;
|
||||
|
|
@ -786,6 +790,9 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
|||
esp32CrashBoardId: null,
|
||||
dismissEsp32Crash: () => set({ esp32CrashBoardId: null }),
|
||||
|
||||
lockComponents: false,
|
||||
setLockComponents: (locked: boolean) => set({ lockComponents: locked }),
|
||||
|
||||
setBoardType: (type: BoardType) => {
|
||||
const { activeBoardId, running, stopSimulation } = get();
|
||||
if (running) stopSimulation();
|
||||
|
|
|
|||
Loading…
Reference in New Issue