feat: multi-tab instrument analysis

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 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-30 09:17:05 +02:00
parent 230ce3ff2b
commit 9a1e943be0
4 changed files with 146 additions and 45 deletions

View File

@@ -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 (
<div className={isActive ? 'flex-1 min-h-0 overflow-y-auto' : 'hidden'}>
<InstrumentDashboard instrumentIdProp={id} />
</div>
)
}
// ── 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 (
<div className="flex-1 overflow-auto">
<div className={isInstrument ? 'hidden' : 'flex-1 overflow-auto'}>
<Routes>
{/* InstrumentDashboard kept here: useParams() reads :id from URL */}
<Route path="/instruments" element={<Navigate to="/instruments/SPY" replace />} />
<Route path="/instruments/:id" element={<InstrumentDashboard />} />
<Route path="/instruments" element={<Navigate to="/instruments/EURUSD" replace />} />
<Route path="/instruments/:id" element={<InstrumentRoute />} />
{/* Legacy redirects */}
<Route path="/impact" element={<Navigate to="/market-events" replace />} />
<Route path="/timeline" element={<Navigate to="/market-events" replace />} />
@@ -125,23 +148,33 @@ function GlobalWatcher() {
// ── App ───────────────────────────────────────────────────────────────────────
function AppInner() {
const { instrumentIds } = useTabs()
return (
<div className="flex h-screen overflow-hidden">
<GlobalWatcher />
<Sidebar />
<div className="flex-1 flex flex-col min-h-0 overflow-hidden">
<TabBar />
<div className="flex-1 flex flex-col min-h-0">
{KEEP_ALIVE_DEFS.map(def => (
<KeepAlivePage key={def.path} path={def.path} component={def.component} />
))}
{instrumentIds.map(id => (
<InstrumentKeepAlive key={id} id={id} />
))}
<NormalRoutes />
</div>
</div>
</div>
)
}
export default function App() {
return (
<BrowserRouter>
<TabsProvider>
<div className="flex h-screen overflow-hidden">
<GlobalWatcher />
<Sidebar />
<div className="flex-1 flex flex-col min-h-0 overflow-hidden">
<TabBar />
<div className="flex-1 flex flex-col min-h-0">
{KEEP_ALIVE_DEFS.map(def => (
<KeepAlivePage key={def.path} path={def.path} component={def.component} />
))}
<NormalRoutes />
</div>
</div>
</div>
<AppInner />
</TabsProvider>
</BrowserRouter>
)