diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 02f62df..23c5864 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import { BrowserRouter, Routes, Route, useLocation, Navigate } from 'react-router-dom' +import { BrowserRouter, Routes, Route, useLocation, useNavigate, useParams, Navigate } from 'react-router-dom' import { useEffect, ComponentType } from 'react' import Sidebar from './components/layout/Sidebar' import TabBar from './components/layout/TabBar' @@ -96,18 +96,41 @@ function KeepAlivePage({ path, component: Component }: { path: string; component ) } -// ── NormalRoutes — only InstrumentDashboard (uses :id param) + redirects ───── +// ── Instrument keep-alive tabs ──────────────────────────────────────────────── + +// Registers the instrument tab on navigation (renders nothing itself) +function InstrumentRoute() { + const { id = 'EURUSD' } = useParams<{ id: string }>() + const { openInstrument } = useTabs() + useEffect(() => { openInstrument(id.toUpperCase()) }, [id, openInstrument]) + return null +} + +// Keeps one InstrumentDashboard mounted per open instrument tab +function InstrumentKeepAlive({ id }: { id: string }) { + const location = useLocation() + const isActive = location.pathname.toLowerCase() === `/instruments/${id.toLowerCase()}` + return ( +
+ +
+ ) +} + +// ── NormalRoutes — legacy redirects + instrument tab registration ───────────── function NormalRoutes() { const location = useLocation() if (KEEP_ALIVE_PATHS.has(location.pathname)) return null + // On instrument paths the keep-alive layer renders the UI; this Routes just registers the tab + const isInstrument = /^\/instruments\/\w/.test(location.pathname) + return ( -
+
- {/* InstrumentDashboard kept here: useParams() reads :id from URL */} - } /> - } /> + } /> + } /> {/* Legacy redirects */} } /> } /> @@ -125,23 +148,33 @@ function GlobalWatcher() { // ── App ─────────────────────────────────────────────────────────────────────── +function AppInner() { + const { instrumentIds } = useTabs() + return ( +
+ + +
+ +
+ {KEEP_ALIVE_DEFS.map(def => ( + + ))} + {instrumentIds.map(id => ( + + ))} + +
+
+
+ ) +} + export default function App() { return ( -
- - -
- -
- {KEEP_ALIVE_DEFS.map(def => ( - - ))} - -
-
-
+
) diff --git a/frontend/src/components/layout/TabBar.tsx b/frontend/src/components/layout/TabBar.tsx index 3b290cf..c91d6d7 100644 --- a/frontend/src/components/layout/TabBar.tsx +++ b/frontend/src/components/layout/TabBar.tsx @@ -1,22 +1,27 @@ import clsx from 'clsx' -import { X, ExternalLink } from 'lucide-react' +import { X, ExternalLink, TrendingUp } from 'lucide-react' import { useLocation, useNavigate } from 'react-router-dom' import { useTabs } from '../../context/TabsContext' import { KEEP_ALIVE_META } from '../../config/keepAliveMeta' export default function TabBar() { - const { kept, forgetRoute } = useTabs() + const { kept, forgetRoute, instrumentIds, closeInstrument } = useTabs() const location = useLocation() const navigate = useNavigate() - const visible = KEEP_ALIVE_META.filter(m => kept.has(m.path)) - if (visible.length === 0) return null + const staticVisible = KEEP_ALIVE_META.filter(m => kept.has(m.path)) + const hasAny = staticVisible.length > 0 || instrumentIds.length > 0 + if (!hasAny) return null + + const activePath = location.pathname return (
- {visible.map(({ path, label, icon: Icon }) => { - const isActive = location.pathname === path || location.pathname.startsWith(path + '/') + + {/* Static keep-alive tabs */} + {staticVisible.map(({ path, label, icon: Icon }) => { + const isActive = activePath === path || activePath.startsWith(path + '/') return ( + ) + })} + + {/* Separator between static and instrument tabs */} + {staticVisible.length > 0 && instrumentIds.length > 0 && ( +
+ )} + + {/* Dynamic instrument tabs */} + {instrumentIds.map(id => { + const path = `/instruments/${id}` + const isActive = activePath.toLowerCase() === path.toLowerCase() + return ( +