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,19 +1,26 @@
import { createContext, useContext, useCallback, useState, ReactNode } from 'react'
interface TabsCtx {
kept: ReadonlySet<string>
keepRoute: (r: string) => void
forgetRoute: (r: string) => void
kept: ReadonlySet<string>
keepRoute: (r: string) => void
forgetRoute: (r: string) => void
instrumentIds: string[]
openInstrument: (id: string) => void
closeInstrument: (id: string) => void
}
const Ctx = createContext<TabsCtx>({
kept: new Set(),
keepRoute: () => {},
forgetRoute: () => {},
keepRoute: () => {},
forgetRoute: () => {},
instrumentIds: [],
openInstrument: () => {},
closeInstrument: () => {},
})
export function TabsProvider({ children }: { children: ReactNode }) {
const [kept, setKept] = useState<Set<string>>(new Set())
const [instrumentIds, setInstrumentIds] = useState<string[]>([])
const keepRoute = useCallback((r: string) =>
setKept(p => p.has(r) ? p : new Set([...p, r])), [])
@@ -21,7 +28,17 @@ export function TabsProvider({ children }: { children: ReactNode }) {
const forgetRoute = useCallback((r: string) =>
setKept(p => { const n = new Set(p); n.delete(r); return n }), [])
return <Ctx.Provider value={{ kept, keepRoute, forgetRoute }}>{children}</Ctx.Provider>
const openInstrument = useCallback((id: string) =>
setInstrumentIds(p => p.includes(id) ? p : [...p, id]), [])
const closeInstrument = useCallback((id: string) =>
setInstrumentIds(p => p.filter(x => x !== id)), [])
return (
<Ctx.Provider value={{ kept, keepRoute, forgetRoute, instrumentIds, openInstrument, closeInstrument }}>
{children}
</Ctx.Provider>
)
}
export const useTabs = () => useContext(Ctx)