44 * GlobalSidebar
55 *
66 * Top-level navigation shell rendered on routes that are NOT scoped to a
7- * specific package (i.e. outside `/$package/*` and
8- * `/environments/:envId/:package/*`). Provides stable entry points to
9- * organizations, environments, packages, and the library of templates &
10- * examples — mirroring the v0.app-style left rail.
7+ * specific package — i.e. the home page, organization management, the
8+ * environments list, an environment's overview page, and the per-environment
9+ * packages management page.
1110 *
12- * When the user drills into a package, the package-scoped `AppSidebar`
13- * takes over instead. The two sidebars are mutually exclusive and share
14- * the same `SidebarProvider` in `routes/__root.tsx`.
11+ * The sidebar deliberately exposes only two navigation entries:
12+ *
13+ * 1. **Environments** — links to `/environments` (browse / pick an env).
14+ * 2. **Packages** — links to `/environments/:envId/packages`. Disabled
15+ * until the user has selected an environment.
16+ *
17+ * Once the user drills into a specific package
18+ * (`/environments/:envId/:package/*`), the package-scoped {@link AppSidebar}
19+ * takes over instead. The two sidebars are mutually exclusive and share the
20+ * same `SidebarProvider` in `routes/__root.tsx`.
1521 */
1622
1723import { useMemo } from 'react' ;
@@ -21,30 +27,20 @@ import {
2127 Check ,
2228 ChevronsUpDown ,
2329 Plus ,
24- Home ,
2530 Boxes ,
2631 Package as PackageIcon ,
27- LayoutTemplate ,
28- Sparkles ,
2932 Settings ,
30- Terminal ,
31- MapPin ,
3233} from 'lucide-react' ;
3334
3435import {
3536 Sidebar ,
3637 SidebarContent ,
3738 SidebarGroup ,
3839 SidebarGroupContent ,
39- SidebarGroupLabel ,
4040 SidebarHeader ,
4141 SidebarMenu ,
4242 SidebarMenuButton ,
4343 SidebarMenuItem ,
44- SidebarMenuSkeleton ,
45- SidebarMenuSub ,
46- SidebarMenuSubButton ,
47- SidebarMenuSubItem ,
4844 SidebarSeparator ,
4945} from '@/components/ui/sidebar' ;
5046import {
@@ -56,11 +52,8 @@ import {
5652 DropdownMenuTrigger ,
5753} from '@/components/ui/dropdown-menu' ;
5854import { useOrganizations , useSession } from '@/hooks/useSession' ;
59- import { useEnvironments } from '@/hooks/useEnvironments' ;
6055import { toast } from '@/hooks/use-toast' ;
6156
62- const MAX_ENV_ITEMS = 6 ;
63-
6457/** Header: active organization + switcher. */
6558function OrgHeader ( ) {
6659 const { organizations, loading, reload } = useOrganizations ( ) ;
@@ -163,159 +156,91 @@ function OrgHeader() {
163156 ) ;
164157}
165158
166- interface NavItem {
167- label : string ;
168- to : string ;
169- icon : React . ComponentType < { className ?: string } > ;
170- matchPrefix ?: string ;
171- }
172-
173- const WORKSPACE_ITEMS : NavItem [ ] = [
174- { label : 'Home' , to : '/' , icon : Home , matchPrefix : '/' } ,
175- { label : 'Organizations' , to : '/orgs' , icon : Building2 , matchPrefix : '/orgs' } ,
176- ] ;
177-
178- const PACKAGE_ITEMS : NavItem [ ] = [
179- { label : 'Packages' , to : '/packages' , icon : PackageIcon , matchPrefix : '/packages' } ,
180- {
181- label : 'API Console' ,
182- to : '/api-console' ,
183- icon : Terminal ,
184- matchPrefix : '/api-console' ,
185- } ,
186- ] ;
187-
188- const LIBRARY_ITEMS : NavItem [ ] = [
189- { label : 'Templates' , to : '/templates' , icon : LayoutTemplate , matchPrefix : '/templates' } ,
190- { label : 'Examples' , to : '/examples' , icon : Sparkles , matchPrefix : '/examples' } ,
191- ] ;
192-
193- function isActive ( pathname : string , item : NavItem ) : boolean {
194- if ( item . matchPrefix === '/' ) return pathname === '/' ;
195- if ( ! item . matchPrefix ) return pathname === item . to ;
196- return pathname === item . matchPrefix || pathname . startsWith ( item . matchPrefix + '/' ) ;
197- }
198-
199- function NavSection ( {
200- label,
201- items,
202- pathname,
203- } : {
204- label : string ;
205- items : NavItem [ ] ;
206- pathname : string ;
207- } ) {
208- return (
209- < SidebarGroup >
210- < SidebarGroupLabel > { label } </ SidebarGroupLabel >
211- < SidebarGroupContent >
212- < SidebarMenu >
213- { items . map ( ( item ) => {
214- const Icon = item . icon ;
215- return (
216- < SidebarMenuItem key = { item . to } >
217- < SidebarMenuButton asChild isActive = { isActive ( pathname , item ) } >
218- < Link to = { item . to } >
219- < Icon className = "size-4" />
220- < span > { item . label } </ span >
221- </ Link >
222- </ SidebarMenuButton >
223- </ SidebarMenuItem >
224- ) ;
225- } ) }
226- </ SidebarMenu >
227- </ SidebarGroupContent >
228- </ SidebarGroup >
229- ) ;
230- }
231-
232- function EnvironmentsSection ( { pathname } : { pathname : string } ) {
233- const { environments, loading } = useEnvironments ( ) ;
234- const envsActive =
235- pathname === '/environments' || pathname . startsWith ( '/environments/' ) ;
236-
237- const shown = environments . slice ( 0 , MAX_ENV_ITEMS ) ;
238- const hasMore = environments . length > MAX_ENV_ITEMS ;
239-
240- return (
241- < SidebarGroup >
242- < SidebarGroupLabel > Environments</ SidebarGroupLabel >
243- < SidebarGroupContent >
244- < SidebarMenu >
245- < SidebarMenuItem >
246- < SidebarMenuButton asChild isActive = { envsActive && shown . length === 0 } >
247- < Link to = "/environments" >
248- < Boxes className = "size-4" />
249- < span > All environments</ span >
250- </ Link >
251- </ SidebarMenuButton >
252- </ SidebarMenuItem >
253- { loading && environments . length === 0 && (
254- < >
255- < SidebarMenuSkeleton showIcon />
256- < SidebarMenuSkeleton showIcon />
257- </ >
258- ) }
259- { shown . length > 0 && (
260- < SidebarMenuSub >
261- { shown . map ( ( env ) => {
262- const href = `/environments/${ env . id } ` ;
263- const active = pathname === href || pathname . startsWith ( href + '/' ) ;
264- return (
265- < SidebarMenuSubItem key = { env . id } >
266- < SidebarMenuSubButton asChild isActive = { active } >
267- < Link
268- to = "/environments/$environmentId"
269- params = { { environmentId : env . id } }
270- >
271- < MapPin className = "size-3.5 opacity-60" />
272- < span className = "truncate" >
273- { env . displayName || env . name || env . id }
274- </ span >
275- </ Link >
276- </ SidebarMenuSubButton >
277- </ SidebarMenuSubItem >
278- ) ;
279- } ) }
280- { hasMore && (
281- < SidebarMenuSubItem >
282- < SidebarMenuSubButton asChild >
283- < Link to = "/environments" >
284- < span className = "text-muted-foreground" >
285- View all ({ environments . length } )…
286- </ span >
287- </ Link >
288- </ SidebarMenuSubButton >
289- </ SidebarMenuSubItem >
290- ) }
291- </ SidebarMenuSub >
292- ) }
293- </ SidebarMenu >
294- </ SidebarGroupContent >
295- </ SidebarGroup >
296- ) ;
159+ /**
160+ * Extract the `:envId` segment from the current pathname when the user is
161+ * anywhere under `/environments/:envId(...)`. Returns undefined on the
162+ * environments list page (`/environments`) or any non-environment route.
163+ */
164+ function useActiveEnvironmentId ( ) : string | undefined {
165+ const location = useLocation ( ) ;
166+ return useMemo ( ( ) => {
167+ const m = location . pathname . match ( / ^ \/ e n v i r o n m e n t s \/ ( [ ^ / ] + ) / ) ;
168+ return m ?. [ 1 ] ;
169+ } , [ location . pathname ] ) ;
297170}
298171
299172export function GlobalSidebar ( ) {
300173 const location = useLocation ( ) ;
301174 const pathname = location . pathname ;
302175 const { session } = useSession ( ) ;
303176 const activeOrgId = session ?. activeOrganizationId ?? undefined ;
177+ const envId = useActiveEnvironmentId ( ) ;
178+
179+ const envsActive = pathname === '/environments' ;
180+ const packagesHref = envId ? `/environments/${ envId } /packages` : undefined ;
181+ const packagesActive = ! ! packagesHref && pathname === packagesHref ;
304182
305183 return (
306184 < Sidebar collapsible = "icon" >
307185 < OrgHeader />
308186 < SidebarContent >
309- < NavSection label = "Workspace" items = { WORKSPACE_ITEMS } pathname = { pathname } />
310- < EnvironmentsSection pathname = { pathname } />
311- < NavSection label = "Packages" items = { PACKAGE_ITEMS } pathname = { pathname } />
312- < NavSection label = "Library" items = { LIBRARY_ITEMS } pathname = { pathname } />
187+ < SidebarGroup >
188+ < SidebarGroupContent >
189+ < SidebarMenu >
190+ { /* Environments — single-row entry, no expansion. */ }
191+ < SidebarMenuItem >
192+ < SidebarMenuButton asChild isActive = { envsActive } tooltip = "Environments" >
193+ < Link to = "/environments" >
194+ < Boxes className = "size-4" />
195+ < span > Environments</ span >
196+ </ Link >
197+ </ SidebarMenuButton >
198+ </ SidebarMenuItem >
199+
200+ { /* Packages — single-row entry. Depends on a selected environment;
201+ disabled and tooltipped when none is selected. */ }
202+ < SidebarMenuItem >
203+ { envId ? (
204+ < SidebarMenuButton
205+ asChild
206+ isActive = { packagesActive }
207+ tooltip = "Packages"
208+ >
209+ < Link
210+ to = "/environments/$environmentId/packages"
211+ params = { { environmentId : envId } }
212+ >
213+ < PackageIcon className = "size-4" />
214+ < span > Packages</ span >
215+ </ Link >
216+ </ SidebarMenuButton >
217+ ) : (
218+ < SidebarMenuButton
219+ disabled
220+ aria-disabled = "true"
221+ tooltip = "Select an environment first"
222+ className = "cursor-not-allowed opacity-50"
223+ >
224+ < PackageIcon className = "size-4" />
225+ < span > Packages</ span >
226+ </ SidebarMenuButton >
227+ ) }
228+ </ SidebarMenuItem >
229+ </ SidebarMenu >
230+ </ SidebarGroupContent >
231+ </ SidebarGroup >
232+
313233 < SidebarSeparator />
234+
314235 < SidebarGroup >
315236 < SidebarGroupContent >
316237 < SidebarMenu >
317238 < SidebarMenuItem >
318- < SidebarMenuButton asChild isActive = { pathname === `/orgs/${ activeOrgId } ` } >
239+ < SidebarMenuButton
240+ asChild
241+ isActive = { pathname === `/orgs/${ activeOrgId } ` }
242+ tooltip = "Settings"
243+ >
319244 { activeOrgId ? (
320245 < Link to = "/orgs/$orgId" params = { { orgId : activeOrgId } } >
321246 < Settings className = "size-4" />
0 commit comments