Files
cortex/portal/src/App.tsx
omigamedev fc075a377b Make portal mobile-friendly with floating buttons and slide-over panels
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.
2026-02-02 17:10:32 +01:00

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>
);
}