Skip to content

Commit c6f1028

Browse files
Claudehotlong
andauthored
feat(studio): restyle top bar left segment to Supabase style
Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/09f59abf-0adb-4259-89a2-6184be8c9618 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 1b85503 commit c6f1028

4 files changed

Lines changed: 26 additions & 96 deletions

File tree

apps/studio/src/components/organization-switcher.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
import { useMemo, useState } from 'react';
1313
import { useNavigate } from '@tanstack/react-router';
14-
import { Building2, Check, ChevronsUpDown, Plus } from 'lucide-react';
14+
import { Check, ChevronsUpDown, Plus } from 'lucide-react';
1515
import {
1616
DropdownMenu,
1717
DropdownMenuContent,
@@ -68,15 +68,14 @@ export function OrganizationSwitcher() {
6868
className="h-8 gap-2 px-2 text-sm font-medium"
6969
disabled={switching}
7070
>
71-
<Building2 className="h-3.5 w-3.5 text-muted-foreground" />
7271
{active ? (
7372
<span className="max-w-[140px] truncate">{active.name}</span>
7473
) : (
7574
<span className="text-muted-foreground">
7675
{loading ? 'Loading…' : 'Select organization'}
7776
</span>
7877
)}
79-
<ChevronsUpDown className="h-3.5 w-3.5 opacity-60" />
78+
<ChevronsUpDown className="h-3 w-3 opacity-50" />
8079
</Button>
8180
</DropdownMenuTrigger>
8281
<DropdownMenuContent align="start" className="w-[280px]" sideOffset={4}>

apps/studio/src/components/project-switcher.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
import { useMemo, useState } from 'react';
2222
import { useNavigate, useParams } from '@tanstack/react-router';
23-
import { ChevronsUpDown, Layers, Plus, Search, Check } from 'lucide-react';
23+
import { ChevronsUpDown, Plus, Search, Check } from 'lucide-react';
2424
import type { Project, ProjectType } from '@objectstack/spec/cloud';
2525
import {
2626
DropdownMenu,
@@ -110,7 +110,6 @@ export function ProjectSwitcher() {
110110
size="sm"
111111
className="h-8 gap-2 px-2 text-sm font-medium"
112112
>
113-
<Layers className="h-3.5 w-3.5 text-muted-foreground" />
114113
{active ? (
115114
<>
116115
<span className="max-w-[160px] truncate">
@@ -123,7 +122,7 @@ export function ProjectSwitcher() {
123122
{loading ? 'Loading projects…' : 'Select project'}
124123
</span>
125124
)}
126-
<ChevronsUpDown className="h-3.5 w-3.5 opacity-60" />
125+
<ChevronsUpDown className="h-3 w-3 opacity-50" />
127126
</Button>
128127
</DropdownMenuTrigger>
129128
<DropdownMenuContent

apps/studio/src/components/top-bar.tsx

Lines changed: 19 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* - Right segment: Global search placeholder, mode badge, ThemeToggle, UserMenu
1111
*/
1212

13-
import { useLocation, useParams } from '@tanstack/react-router';
13+
import { Link, useLocation, useParams } from '@tanstack/react-router';
1414
import { useMemo } from 'react';
1515
import { Separator } from '@/components/ui/separator';
1616
import {
@@ -24,13 +24,11 @@ import {
2424
import { ThemeToggle } from '@/components/theme-toggle';
2525
import { Badge } from '@/components/ui/badge';
2626
import { Input } from '@/components/ui/input';
27-
import { Cpu, Search } from 'lucide-react';
27+
import { Boxes, Cpu, Search } from 'lucide-react';
2828
import { config } from '@/lib/config';
2929
import { ProjectSwitcher } from '@/components/project-switcher';
3030
import { OrganizationSwitcher } from '@/components/organization-switcher';
3131
import { UserMenu } from '@/components/user-menu';
32-
import { PackageSwitcher } from '@/components/package-switcher';
33-
import type { InstalledPackage } from '@objectstack/spec/kernel';
3432

3533
const META_TYPE_LABELS: Record<string, string> = {
3634
action: 'Actions',
@@ -61,20 +59,19 @@ const META_TYPE_LABELS: Record<string, string> = {
6159
theme: 'Themes',
6260
};
6361

64-
interface TopBarProps {
65-
/** List of installed packages for the PackageSwitcher dropdown */
66-
packages?: InstalledPackage[];
67-
/** Currently selected package */
68-
selectedPackage?: InstalledPackage | null;
69-
/** Callback when a package is selected from the dropdown */
70-
onSelectPackage?: (pkg: InstalledPackage) => void;
62+
function StudioBrand() {
63+
return (
64+
<Link to="/" className="flex h-7 w-7 items-center justify-center rounded-md bg-primary text-primary-foreground hover:opacity-90">
65+
<Boxes className="h-4 w-4" />
66+
</Link>
67+
);
68+
}
69+
70+
function SlashDivider() {
71+
return <span aria-hidden className="text-muted-foreground/50 select-none">/</span>;
7172
}
7273

73-
export function TopBar({
74-
packages = [],
75-
selectedPackage = null,
76-
onSelectPackage = () => {},
77-
}: TopBarProps) {
74+
export function TopBar() {
7875
const location = useLocation();
7976
const params = useParams({ strict: false }) as {
8077
package?: string;
@@ -130,24 +127,15 @@ export function TopBar({
130127
items.push({ label: 'Overview' });
131128
break;
132129
case 'package-overview':
133-
if (selectedPackage?.manifest?.name) {
134-
items.push({ label: selectedPackage.manifest.name });
135-
}
136130
items.push({ label: 'Overview' });
137131
break;
138132
case 'object':
139-
if (selectedPackage?.manifest?.name) {
140-
items.push({ label: selectedPackage.manifest.name });
141-
}
142133
items.push({ label: 'Objects' });
143134
if (params.name) {
144135
items.push({ label: params.name });
145136
}
146137
break;
147138
case 'metadata':
148-
if (selectedPackage?.manifest?.name) {
149-
items.push({ label: selectedPackage.manifest.name });
150-
}
151139
if (params.type) {
152140
items.push({ label: META_TYPE_LABELS[params.type] || params.type });
153141
}
@@ -160,7 +148,7 @@ export function TopBar({
160148
}
161149

162150
return items;
163-
}, [viewType, params, selectedPackage]);
151+
}, [viewType, params]);
164152

165153
// Compute API badge for object/metadata views
166154
const apiBadge = useMemo(() => {
@@ -173,26 +161,15 @@ export function TopBar({
173161
return null;
174162
}, [viewType, params]);
175163

176-
// Show PackageSwitcher only when we're in a package context
177-
const showPackageSwitcher = params.package && packages.length > 0;
178-
179164
return (
180165
<header className="flex h-12 shrink-0 items-center justify-between gap-2 border-b px-4">
181-
{/* Left segment: Org + Env + Package switchers */}
182-
<div className="flex items-center gap-2">
166+
{/* Left segment: Brand + Org + Project switchers */}
167+
<div className="flex items-center gap-1.5">
168+
<StudioBrand />
169+
<SlashDivider />
183170
<OrganizationSwitcher />
184-
<Separator orientation="vertical" className="mx-1 h-4" />
171+
<SlashDivider />
185172
<ProjectSwitcher />
186-
{showPackageSwitcher && (
187-
<>
188-
<Separator orientation="vertical" className="mx-1 h-4" />
189-
<PackageSwitcher
190-
packages={packages}
191-
selectedPackage={selectedPackage}
192-
onSelectPackage={onSelectPackage}
193-
/>
194-
</>
195-
)}
196173
<Separator orientation="vertical" className="mx-2 h-4" />
197174
<Breadcrumb>
198175
<BreadcrumbList>

apps/studio/src/routes/__root.tsx

Lines changed: 3 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

3-
import { createRootRoute, Outlet, useLocation, useNavigate, useParams } from '@tanstack/react-router';
4-
import { useEffect, useMemo } from 'react';
3+
import { createRootRoute, Outlet, useLocation, useNavigate } from '@tanstack/react-router';
4+
import { useEffect } from 'react';
55
import { ObjectStackProvider } from '@objectstack/client-react';
66
import { ErrorBoundary } from '../components/ErrorBoundary';
77
import { SidebarProvider } from '@/components/ui/sidebar';
@@ -13,8 +13,6 @@ import { PluginRegistryProvider } from '../plugins';
1313
import { builtInPlugins } from '../plugins/built-in';
1414
import { useObjectStackClient } from '../hooks/useObjectStackClient';
1515
import { SessionProvider, useSession } from '../hooks/useSession';
16-
import { useEnvAwarePackages } from '../hooks/useProjectAwarePackages';
17-
import type { InstalledPackage } from '@objectstack/spec/kernel';
1816

1917
/** Routes that don't require authentication. */
2018
const PUBLIC_ROUTES = new Set(['/login', '/register']);
@@ -39,47 +37,8 @@ function RequireAuth({ children }: { children: React.ReactNode }) {
3937
const { user, loading } = useSession();
4038
const navigate = useNavigate();
4139
const location = useLocation();
42-
const params = useParams({ strict: false }) as { projectId?: string; package?: string };
4340
const isPublic = PUBLIC_ROUTES.has(location.pathname);
4441

45-
// Get packages for TopBar PackageSwitcher
46-
const { packages, selectedPackage, setSelectedPackage } =
47-
useEnvAwarePackages(params.projectId);
48-
49-
// Extract the $package segment from the URL
50-
const activePackageId = useMemo(() => {
51-
if (!params.package) return undefined;
52-
// Reserved segments that are NOT package ids
53-
if (params.package === 'packages') return undefined;
54-
return params.package;
55-
}, [params.package]);
56-
57-
// Sync selectedPackage with URL
58-
useEffect(() => {
59-
if (!activePackageId) {
60-
if (selectedPackage) setSelectedPackage(null);
61-
return;
62-
}
63-
if (!packages.length) return;
64-
const pkg = packages.find(
65-
(p) =>
66-
p.manifest?.id === activePackageId ||
67-
p.manifest?.id?.endsWith('.' + activePackageId),
68-
);
69-
if (pkg && pkg !== selectedPackage) setSelectedPackage(pkg);
70-
}, [activePackageId, packages, selectedPackage, setSelectedPackage]);
71-
72-
const handleSelectPackage = (pkg: InstalledPackage) => {
73-
const nextId = pkg.manifest?.id;
74-
if (!nextId) return;
75-
if (params.projectId) {
76-
navigate({
77-
to: '/projects/$projectId/$package',
78-
params: { projectId: params.projectId, package: nextId },
79-
});
80-
}
81-
};
82-
8342
// Redirect to environment picker when the user hits a route that requires
8443
// an environment context (e.g. /$package/*) but isn't already under /projects.
8544
useEffect(() => {
@@ -113,11 +72,7 @@ function RequireAuth({ children }: { children: React.ReactNode }) {
11372
return (
11473
<SidebarProvider>
11574
<div className="flex min-h-screen w-full flex-col">
116-
<TopBar
117-
packages={packages}
118-
selectedPackage={selectedPackage}
119-
onSelectPackage={handleSelectPackage}
120-
/>
75+
<TopBar />
12176
<div className="flex flex-1 w-full overflow-hidden">
12277
<main className="flex flex-1 min-w-0 overflow-hidden">
12378
{children}

0 commit comments

Comments
 (0)