Replace fixed sidebar with floating action buttons (menu + add node) in bottom-left corner. Sidebar and node panel now slide in as overlay drawers with backdrop dismiss, capped width for small screens, and slide animations.
124 lines
4.3 KiB
TypeScript
124 lines
4.3 KiB
TypeScript
import { useState, useCallback, useEffect } from 'react';
|
|
import { useQueryClient } from '@tanstack/react-query';
|
|
import GraphView from './components/GraphView';
|
|
import Sidebar from './components/Sidebar';
|
|
import NodePanel from './components/NodePanel';
|
|
import AddNodeModal from './components/AddNodeModal';
|
|
import LinkModal from './components/LinkModal';
|
|
import Toast from './components/Toast';
|
|
|
|
export default function App() {
|
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
const [showAddNode, setShowAddNode] = useState(false);
|
|
const [linkFromId, setLinkFromId] = useState<string | null>(null);
|
|
const [toast, setToast] = useState<string | null>(null);
|
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
const qc = useQueryClient();
|
|
|
|
const refresh = useCallback(() => {
|
|
qc.invalidateQueries({ queryKey: ['graph'] });
|
|
qc.invalidateQueries({ queryKey: ['nodes'] });
|
|
}, [qc]);
|
|
|
|
const notify = useCallback((msg: string) => {
|
|
setToast(msg);
|
|
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 (
|
|
<div className="h-screen w-screen overflow-hidden relative">
|
|
{/* Full-screen graph */}
|
|
<GraphView selectedId={selectedId} onSelect={selectNode} />
|
|
|
|
{/* Floating action buttons — bottom-left */}
|
|
<div className="fixed bottom-5 left-5 flex flex-col gap-2 z-30">
|
|
<button
|
|
onClick={() => setDrawerOpen(!drawerOpen)}
|
|
className="w-12 h-12 rounded-full bg-gray-800/90 backdrop-blur border border-gray-700 text-gray-300 hover:bg-gray-700 hover:text-white shadow-lg flex items-center justify-center text-lg"
|
|
title="Node list"
|
|
>
|
|
☰
|
|
</button>
|
|
<button
|
|
onClick={() => setShowAddNode(true)}
|
|
className="w-12 h-12 rounded-full bg-indigo-600/90 backdrop-blur border border-indigo-500 text-white hover:bg-indigo-500 shadow-lg flex items-center justify-center text-xl font-light"
|
|
title="Add node"
|
|
>
|
|
+
|
|
</button>
|
|
</div>
|
|
|
|
{/* Sidebar drawer — slides in from left */}
|
|
{drawerOpen && (
|
|
<div className="fixed inset-0 z-40" onClick={() => setDrawerOpen(false)}>
|
|
<div className="absolute inset-0 bg-black/40" />
|
|
<div
|
|
className="absolute top-0 left-0 h-full w-80 max-w-[85vw] animate-slide-in-left"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<Sidebar
|
|
selectedId={selectedId}
|
|
onSelect={selectNode}
|
|
onAddNode={() => { setDrawerOpen(false); setShowAddNode(true); }}
|
|
onClose={() => setDrawerOpen(false)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Node detail panel — slides in from right */}
|
|
{selectedId && (
|
|
<div className="fixed inset-0 z-40 pointer-events-none">
|
|
<div
|
|
className="absolute inset-0 bg-black/40 pointer-events-auto"
|
|
onClick={() => setSelectedId(null)}
|
|
/>
|
|
<div
|
|
className="absolute top-0 right-0 h-full w-80 max-w-[90vw] pointer-events-auto animate-slide-in-right"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<NodePanel
|
|
nodeId={selectedId}
|
|
onClose={() => setSelectedId(null)}
|
|
onLink={(id) => setLinkFromId(id)}
|
|
onRefresh={refresh}
|
|
onNotify={notify}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{showAddNode && (
|
|
<AddNodeModal
|
|
onClose={() => setShowAddNode(false)}
|
|
onCreated={() => { refresh(); notify('Node created'); }}
|
|
/>
|
|
)}
|
|
{linkFromId && (
|
|
<LinkModal
|
|
fromId={linkFromId}
|
|
onClose={() => setLinkFromId(null)}
|
|
onCreated={() => { refresh(); notify('Edge created'); }}
|
|
/>
|
|
)}
|
|
{toast && <Toast message={toast} />}
|
|
</div>
|
|
);
|
|
}
|