Skip to content

Commit 0c329df

Browse files
committed
feat: simplify global sidebar navigation; remove examples and templates routes
1 parent 0708b7b commit 0c329df

6 files changed

Lines changed: 101 additions & 291 deletions

File tree

apps/studio/src/components/global-sidebar.tsx

Lines changed: 81 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,20 @@
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

1723
import { 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

3435
import {
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';
5046
import {
@@ -56,11 +52,8 @@ import {
5652
DropdownMenuTrigger,
5753
} from '@/components/ui/dropdown-menu';
5854
import { useOrganizations, useSession } from '@/hooks/useSession';
59-
import { useEnvironments } from '@/hooks/useEnvironments';
6055
import { toast } from '@/hooks/use-toast';
6156

62-
const MAX_ENV_ITEMS = 6;
63-
6457
/** Header: active organization + switcher. */
6558
function 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(/^\/environments\/([^/]+)/);
168+
return m?.[1];
169+
}, [location.pathname]);
297170
}
298171

299172
export 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

Comments
 (0)