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.
This commit is contained in:
@@ -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<string | null>(null);
|
||||
const [toast, setToast] = useState<string | null>(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 (
|
||||
<div className="flex h-screen w-screen overflow-hidden">
|
||||
<Sidebar
|
||||
selectedId={selectedId}
|
||||
onSelect={setSelectedId}
|
||||
onAddNode={() => setShowAddNode(true)}
|
||||
/>
|
||||
<div className="flex-1 relative">
|
||||
<GraphView selectedId={selectedId} onSelect={setSelectedId} />
|
||||
<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>
|
||||
{selectedId && (
|
||||
<NodePanel
|
||||
nodeId={selectedId}
|
||||
onClose={() => setSelectedId(null)}
|
||||
onLink={(id) => setLinkFromId(id)}
|
||||
onRefresh={refresh}
|
||||
onNotify={notify}
|
||||
/>
|
||||
|
||||
{/* 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)}
|
||||
|
||||
Reference in New Issue
Block a user