From 9a1e943be070ef2dc7ca2e61e6c9c8a2d9607d8a Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Tue, 30 Jun 2026 09:17:05 +0200 Subject: [PATCH] feat: multi-tab instrument analysis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each /instruments/:id navigation opens a persistent tab in the TabBar. Multiple instruments can be open simultaneously — state is preserved when switching. - TabsContext: adds instrumentIds[], openInstrument(), closeInstrument() - TabBar: renders instrument tabs (teal, monospaced, TrendingUp icon) after static tabs, separated by a divider - InstrumentDashboard: accepts instrumentIdProp so keep-alive instances use the right id regardless of URL - App: InstrumentRoute registers the tab on navigation; InstrumentKeepAlive mounts one dashboard per open instrument; NormalRoutes hides on instrument paths (hidden div, Routes still fires) Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/App.tsx | 71 +++++++++++++----- frontend/src/components/layout/TabBar.tsx | 84 +++++++++++++++++----- frontend/src/context/TabsContext.tsx | 29 ++++++-- frontend/src/pages/InstrumentDashboard.tsx | 7 +- 4 files changed, 146 insertions(+), 45 deletions(-) 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 ( +