1010 * - Right segment: Global search placeholder, mode badge, ThemeToggle, UserMenu
1111 */
1212
13- import { Link , useLocation , useParams } from '@tanstack/react-router' ;
14- import { useMemo } from 'react' ;
13+ import { Link , useLocation , useNavigate , useParams } from '@tanstack/react-router' ;
14+ import { useCallback , useMemo } from 'react' ;
15+ import type { InstalledPackage } from '@objectstack/spec/kernel' ;
1516import { Separator } from '@/components/ui/separator' ;
1617import {
1718 Breadcrumb ,
@@ -28,9 +29,11 @@ import { Boxes, Cpu, Search } from 'lucide-react';
2829import { config } from '@/lib/config' ;
2930import { ProjectSwitcher } from '@/components/project-switcher' ;
3031import { OrganizationSwitcher } from '@/components/organization-switcher' ;
32+ import { PackageSwitcher } from '@/components/package-switcher' ;
3133import { UserMenu } from '@/components/user-menu' ;
3234import { SidebarTrigger } from '@/components/ui/sidebar' ;
3335import { useActiveOrganizationId } from '@/hooks/useSession' ;
36+ import { useEnvAwarePackages } from '@/hooks/useProjectAwarePackages' ;
3437
3538const META_TYPE_LABELS : Record < string , string > = {
3639 action : 'Actions' ,
@@ -75,6 +78,7 @@ function SlashDivider() {
7578
7679export function TopBar ( ) {
7780 const location = useLocation ( ) ;
81+ const navigate = useNavigate ( ) ;
7882 const activeOrgId = useActiveOrganizationId ( ) ;
7983 const params = useParams ( { strict : false } ) as {
8084 package ?: string ;
@@ -84,6 +88,38 @@ export function TopBar() {
8488 orgId ?: string ;
8589 } ;
8690
91+ // Load packages installed in the current project so users can switch
92+ // between them from the top-bar (e.g. while viewing metadata).
93+ const { packages } = useEnvAwarePackages ( params . projectId ) ;
94+
95+ // Resolve the current package from the URL segment. Match either the
96+ // full reverse-domain id (e.g. com.example.crm) or the last segment (crm).
97+ const selectedPackage = useMemo < InstalledPackage | null > ( ( ) => {
98+ if ( ! params . package || ! packages . length ) return null ;
99+ return (
100+ packages . find (
101+ ( p ) =>
102+ p . manifest ?. id === params . package ||
103+ p . manifest ?. id ?. split ( '.' ) . pop ( ) === params . package ,
104+ ) ?? null
105+ ) ;
106+ } , [ packages , params . package ] ) ;
107+
108+ const handleSelectPackage = useCallback (
109+ ( pkg : InstalledPackage ) => {
110+ const nextId = pkg . manifest ?. id ;
111+ if ( ! nextId || ! params . projectId ) return ;
112+ // Switching package invalidates the current metadata path (the same
113+ // type/name may not exist in the target package), so land on the
114+ // package overview.
115+ navigate ( {
116+ to : '/projects/$projectId/$package' ,
117+ params : { projectId : params . projectId , package : nextId } ,
118+ } ) ;
119+ } ,
120+ [ navigate , params . projectId ] ,
121+ ) ;
122+
87123 // Infer view type from pathname
88124 const viewType = useMemo ( ( ) => {
89125 const pathname = location . pathname ;
@@ -182,6 +218,16 @@ export function TopBar() {
182218 { ! config . singleProject && < OrganizationSwitcher /> }
183219 { ( ! config . singleProject && activeOrgId ) && < SlashDivider /> }
184220 { ! config . singleProject && < ProjectSwitcher /> }
221+ { params . projectId && params . package && (
222+ < >
223+ < SlashDivider />
224+ < PackageSwitcher
225+ packages = { packages }
226+ selectedPackage = { selectedPackage }
227+ onSelectPackage = { handleSelectPackage }
228+ />
229+ </ >
230+ ) }
185231 </ div >
186232 { /* Mobile: Show only current page breadcrumb */ }
187233 < div className = "sm:hidden min-w-0 flex-1" >
0 commit comments