Skip to content

Commit 6f4d54f

Browse files
authored
Merge pull request #1204 from objectstack-ai/claude/restructure-studio-layout
[WIP] Refactor Studio layout to match Supabase style
2 parents cf6244a + ec671fd commit 6f4d54f

19 files changed

Lines changed: 499 additions & 399 deletions

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

Lines changed: 11 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@ import {
44
Database,
55
Package,
66
LayoutDashboard,
7-
ChevronsUpDown,
8-
Sparkles,
97
Search,
10-
Check,
118
Zap,
129
BarChart3,
1310
FileText,
@@ -33,6 +30,7 @@ import {
3330
ChevronRight,
3431
Settings,
3532
Wrench,
33+
Sparkles,
3634
type LucideIcon,
3735
} from "lucide-react"
3836
import { useState, useEffect, useCallback, useMemo } from "react"
@@ -43,33 +41,26 @@ import type { InstalledPackage } from '@objectstack/spec/kernel';
4341
import {
4442
Sidebar,
4543
SidebarContent,
44+
SidebarFooter,
4645
SidebarGroup,
4746
SidebarGroupContent,
4847
SidebarGroupLabel,
4948
SidebarMenu,
5049
SidebarMenuButton,
5150
SidebarMenuItem,
5251
SidebarMenuSkeleton,
53-
SidebarHeader,
5452
SidebarSeparator,
5553
SidebarInput,
5654
SidebarMenuSub,
5755
SidebarMenuSubItem,
5856
SidebarMenuSubButton,
57+
SidebarTrigger,
5958
} from "@/components/ui/sidebar"
6059
import {
6160
Collapsible,
6261
CollapsibleContent,
6362
CollapsibleTrigger,
6463
} from "@/components/ui/collapsible"
65-
import {
66-
DropdownMenu,
67-
DropdownMenuContent,
68-
DropdownMenuItem,
69-
DropdownMenuLabel,
70-
DropdownMenuSeparator,
71-
DropdownMenuTrigger,
72-
} from "@/components/ui/dropdown-menu"
7364

7465
// ─── Icon & label hints ──────────────────────────────────────────────
7566
const META_TYPE_HINTS: Record<string, { label: string; icon: LucideIcon }> = {
@@ -306,59 +297,10 @@ export function AppSidebar({
306297
return { ...group, visibleTypes, totalItems };
307298
}).filter(g => g.totalItems > 0);
308299

309-
// Package switcher state
310-
const SelectedPkgIcon = selectedPackage ? (PKG_TYPE_ICONS[selectedPackage.manifest?.type] || Package) : Sparkles;
300+
// Package switcher state (no longer used in AppSidebar, moved to TopBar)
311301

312302
return (
313303
<Sidebar {...props}>
314-
{/* ── Package Switcher ── */}
315-
<SidebarHeader className="border-b">
316-
<DropdownMenu>
317-
<DropdownMenuTrigger asChild>
318-
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left hover:bg-sidebar-accent transition-colors">
319-
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary text-primary-foreground">
320-
<SelectedPkgIcon className="h-4 w-4" />
321-
</div>
322-
<div className="flex flex-1 min-w-0 flex-col gap-0.5 leading-none overflow-hidden">
323-
<span className="truncate font-semibold text-sm">
324-
{selectedPackage ? (selectedPackage.manifest?.name || selectedPackage.manifest?.id) : 'ObjectStack'}
325-
</span>
326-
<span className="truncate text-xs text-muted-foreground">
327-
{selectedPackage ? `v${selectedPackage.manifest?.version} · ${selectedPackage.manifest?.type}` : 'Loading packages...'}
328-
</span>
329-
</div>
330-
<ChevronsUpDown className="ml-auto h-4 w-4 shrink-0 text-muted-foreground" />
331-
</button>
332-
</DropdownMenuTrigger>
333-
<DropdownMenuContent className="w-[--radix-dropdown-menu-trigger-width] min-w-64" align="start" sideOffset={4}>
334-
<DropdownMenuLabel>Installed Packages</DropdownMenuLabel>
335-
<DropdownMenuSeparator />
336-
{packages.map((pkg) => {
337-
const Icon = PKG_TYPE_ICONS[pkg.manifest?.type] || Package;
338-
const isSelected = selectedPackage?.manifest?.id === pkg.manifest?.id;
339-
return (
340-
<DropdownMenuItem key={pkg.manifest?.id} onClick={() => onSelectPackage(pkg)} className="gap-2 py-2">
341-
<div className="flex h-6 w-6 shrink-0 items-center justify-center rounded bg-primary/10 text-primary">
342-
<Icon className="h-3.5 w-3.5" />
343-
</div>
344-
<div className="flex flex-1 flex-col leading-tight">
345-
<span className="text-sm font-medium">{pkg.manifest?.name || pkg.manifest?.id}</span>
346-
<span className="text-xs text-muted-foreground">
347-
v{pkg.manifest?.version} · {pkg.manifest?.type}
348-
{!pkg.enabled && ' · disabled'}
349-
</span>
350-
</div>
351-
{isSelected && <Check className="h-4 w-4 text-primary" />}
352-
</DropdownMenuItem>
353-
);
354-
})}
355-
{packages.length === 0 && (
356-
<div className="px-2 py-4 text-center text-xs text-muted-foreground">No packages installed</div>
357-
)}
358-
</DropdownMenuContent>
359-
</DropdownMenu>
360-
</SidebarHeader>
361-
362304
<SidebarContent>
363305
{/* ── Overview ── */}
364306
<SidebarGroup>
@@ -607,6 +549,13 @@ export function AppSidebar({
607549
</SidebarGroupContent>
608550
</SidebarGroup>
609551
</SidebarContent>
552+
<SidebarFooter>
553+
<SidebarMenu>
554+
<SidebarMenuItem>
555+
<SidebarTrigger />
556+
</SidebarMenuItem>
557+
</SidebarMenu>
558+
</SidebarFooter>
610559
</Sidebar>
611560
)
612561
}

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

Lines changed: 13 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,14 @@
1818
* (`/environments/:envId/:package/*`), the package-scoped {@link AppSidebar}
1919
* takes over instead. The two sidebars are mutually exclusive and share the
2020
* same `SidebarProvider` in `routes/__root.tsx`.
21+
*
22+
* Organization switching is now handled in the TopBar, so this sidebar only
23+
* focuses on functional navigation.
2124
*/
2225

2326
import { useMemo } from 'react';
2427
import { Link, useLocation } from '@tanstack/react-router';
2528
import {
26-
Building2,
27-
Check,
28-
ChevronsUpDown,
29-
Plus,
3029
Boxes,
3130
Globe,
3231
Package as PackageIcon,
@@ -36,126 +35,16 @@ import {
3635
import {
3736
Sidebar,
3837
SidebarContent,
38+
SidebarFooter,
3939
SidebarGroup,
4040
SidebarGroupContent,
41-
SidebarHeader,
4241
SidebarMenu,
4342
SidebarMenuButton,
4443
SidebarMenuItem,
4544
SidebarSeparator,
45+
SidebarTrigger,
4646
} from '@/components/ui/sidebar';
47-
import {
48-
DropdownMenu,
49-
DropdownMenuContent,
50-
DropdownMenuItem,
51-
DropdownMenuLabel,
52-
DropdownMenuSeparator,
53-
DropdownMenuTrigger,
54-
} from '@/components/ui/dropdown-menu';
55-
import { useOrganizations, useSession } from '@/hooks/useSession';
56-
import { toast } from '@/hooks/use-toast';
57-
58-
/** Header: active organization + switcher. */
59-
function OrgHeader() {
60-
const { organizations, loading, reload } = useOrganizations();
61-
const { session, setActiveOrganization } = useSession();
62-
const activeId = session?.activeOrganizationId ?? undefined;
63-
const active = useMemo(
64-
() => organizations.find((o) => o.id === activeId) ?? null,
65-
[organizations, activeId],
66-
);
67-
68-
const handleSelect = async (id: string) => {
69-
if (id === activeId) return;
70-
try {
71-
await setActiveOrganization(id);
72-
await reload();
73-
toast({ title: 'Organization switched' });
74-
} catch (err) {
75-
toast({
76-
title: 'Failed to switch organization',
77-
description: (err as Error).message,
78-
variant: 'destructive',
79-
});
80-
}
81-
};
82-
83-
return (
84-
<SidebarHeader className="border-b">
85-
<SidebarMenu>
86-
<SidebarMenuItem>
87-
<DropdownMenu>
88-
<DropdownMenuTrigger asChild>
89-
<SidebarMenuButton
90-
size="lg"
91-
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
92-
>
93-
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
94-
<Building2 className="size-4" />
95-
</div>
96-
<div className="grid flex-1 text-left text-sm leading-tight">
97-
<span className="truncate font-semibold">
98-
{active?.name ?? (loading ? 'Loading…' : 'Select organization')}
99-
</span>
100-
{active?.slug && (
101-
<span className="truncate text-xs text-muted-foreground">
102-
{active.slug}
103-
</span>
104-
)}
105-
</div>
106-
<ChevronsUpDown className="ml-auto size-4 opacity-60" />
107-
</SidebarMenuButton>
108-
</DropdownMenuTrigger>
109-
<DropdownMenuContent
110-
className="w-[--radix-dropdown-menu-trigger-width] min-w-56"
111-
align="start"
112-
side="bottom"
113-
sideOffset={4}
114-
>
115-
<DropdownMenuLabel className="text-[10px] uppercase tracking-wider text-muted-foreground">
116-
Organizations
117-
</DropdownMenuLabel>
118-
{organizations.map((org) => (
119-
<DropdownMenuItem
120-
key={org.id}
121-
onSelect={(e) => {
122-
e.preventDefault();
123-
handleSelect(org.id);
124-
}}
125-
className="gap-2"
126-
>
127-
<div className="min-w-0 flex-1">
128-
<div className="truncate font-medium">{org.name}</div>
129-
{org.slug && (
130-
<code className="font-mono text-[11px] text-muted-foreground">
131-
{org.slug}
132-
</code>
133-
)}
134-
</div>
135-
{org.id === activeId && (
136-
<Check className="size-3.5 text-primary" />
137-
)}
138-
</DropdownMenuItem>
139-
))}
140-
<DropdownMenuSeparator />
141-
<DropdownMenuItem asChild>
142-
<Link to="/orgs/new" className="gap-2">
143-
<Plus className="size-3.5" />
144-
New organization…
145-
</Link>
146-
</DropdownMenuItem>
147-
<DropdownMenuItem asChild>
148-
<Link to="/orgs" className="gap-2 text-muted-foreground">
149-
Manage organizations
150-
</Link>
151-
</DropdownMenuItem>
152-
</DropdownMenuContent>
153-
</DropdownMenu>
154-
</SidebarMenuItem>
155-
</SidebarMenu>
156-
</SidebarHeader>
157-
);
158-
}
47+
import { useSession } from '@/hooks/useSession';
15948

16049
/**
16150
* Extract the `:envId` segment from the current pathname when the user is
@@ -184,7 +73,6 @@ export function GlobalSidebar() {
18473

18574
return (
18675
<Sidebar collapsible="icon">
187-
<OrgHeader />
18876
<SidebarContent>
18977
<SidebarGroup>
19078
<SidebarGroupContent>
@@ -271,6 +159,13 @@ export function GlobalSidebar() {
271159
</SidebarGroupContent>
272160
</SidebarGroup>
273161
</SidebarContent>
162+
<SidebarFooter>
163+
<SidebarMenu>
164+
<SidebarMenuItem>
165+
<SidebarTrigger />
166+
</SidebarMenuItem>
167+
</SidebarMenu>
168+
</SidebarFooter>
274169
</Sidebar>
275170
);
276171
}

0 commit comments

Comments
 (0)