diff --git a/portal/src/App.tsx b/portal/src/App.tsx index fa4a354..68588e3 100644 --- a/portal/src/App.tsx +++ b/portal/src/App.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback } from 'react'; +import { useState, useCallback, useEffect } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import GraphView from './components/GraphView'; import Sidebar from './components/Sidebar'; @@ -12,6 +12,7 @@ export default function App() { const [showAddNode, setShowAddNode] = useState(false); const [linkFromId, setLinkFromId] = useState(null); const [toast, setToast] = useState(null); + const [drawerOpen, setDrawerOpen] = useState(false); const qc = useQueryClient(); const refresh = useCallback(() => { @@ -24,25 +25,85 @@ export default function App() { setTimeout(() => setToast(null), 3000); }, []); + const selectNode = useCallback((id: string) => { + setSelectedId(id); + setDrawerOpen(false); + }, []); + + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + if (selectedId) setSelectedId(null); + else if (drawerOpen) setDrawerOpen(false); + } + }; + window.addEventListener('keydown', handler); + return () => window.removeEventListener('keydown', handler); + }, [selectedId, drawerOpen]); + return ( -
- setShowAddNode(true)} - /> -
- +
+ {/* Full-screen graph */} + + + {/* Floating action buttons — bottom-left */} +
+ +
- {selectedId && ( - setSelectedId(null)} - onLink={(id) => setLinkFromId(id)} - onRefresh={refresh} - onNotify={notify} - /> + + {/* Sidebar drawer — slides in from left */} + {drawerOpen && ( +
setDrawerOpen(false)}> +
+
e.stopPropagation()} + > + { setDrawerOpen(false); setShowAddNode(true); }} + onClose={() => setDrawerOpen(false)} + /> +
+
)} + + {/* Node detail panel — slides in from right */} + {selectedId && ( +
+
setSelectedId(null)} + /> +
e.stopPropagation()} + > + setSelectedId(null)} + onLink={(id) => setLinkFromId(id)} + onRefresh={refresh} + onNotify={notify} + /> +
+
+ )} + {showAddNode && ( setShowAddNode(false)} diff --git a/portal/src/app.css b/portal/src/app.css index f13308b..d02560f 100644 --- a/portal/src/app.css +++ b/portal/src/app.css @@ -1,2 +1,20 @@ @import "tailwindcss"; @import "@xyflow/react/dist/style.css"; + +@keyframes slide-in-left { + from { transform: translateX(-100%); } + to { transform: translateX(0); } +} + +@keyframes slide-in-right { + from { transform: translateX(100%); } + to { transform: translateX(0); } +} + +.animate-slide-in-left { + animation: slide-in-left 0.2s ease-out; +} + +.animate-slide-in-right { + animation: slide-in-right 0.2s ease-out; +} diff --git a/portal/src/components/NodePanel.tsx b/portal/src/components/NodePanel.tsx index 88f10cb..7b8573c 100644 --- a/portal/src/components/NodePanel.tsx +++ b/portal/src/components/NodePanel.tsx @@ -1,7 +1,6 @@ -import { useState, useEffect } from 'react'; +import { useState } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { api } from '../api'; -import type { NodeWithConnections } from '../types'; export default function NodePanel({ nodeId, @@ -25,17 +24,9 @@ export default function NodePanel({ const [editing, setEditing] = useState(null); const [editValue, setEditValue] = useState(''); - useEffect(() => { - const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; - window.addEventListener('keydown', handler); - return () => window.removeEventListener('keydown', handler); - }, [onClose]); - if (isLoading || !node) { return ( -
- Loading... -
+
Loading...
); } @@ -85,7 +76,7 @@ export default function NodePanel({ ); return ( -