11/**
22 * HomeTopNav
33 *
4- * Top navigation bar for the Home (workspace) landing page.
4+ * Single-row top navigation bar for the Home (workspace) landing page.
55 *
6- * Rationale: The Home page deliberately uses a horizontal top-menu layout
7- * (instead of the left sidebar used by individual applications) so users can
8- * visually distinguish the workspace landing page from in-app routes. The
9- * left sidebar is reserved for `/apps/:appName/*` where rich, contextual
10- * navigation is needed.
6+ * [Logo + Product] [Global search (⌘K)]
7+ * [Activity | Help] [Theme | Locale] [Workspace | User]
118 *
12- * Layout:
13- * - Left: Workspace switcher (organization) + "Home" link
14- * - Right: Search (⌘K), Activity feed, Help, Theme, Locale, User profile
9+ * The page title (rendered by `HomePage`) and any primary page actions live
10+ * in the page body, keeping the chrome compact. See
11+ * { @link ./HomeLayout.tsx} for composition.
1512 *
1613 * @module
1714 */
@@ -29,8 +26,17 @@ import {
2926 DropdownMenuLabel ,
3027 DropdownMenuSeparator ,
3128 DropdownMenuGroup ,
29+ Separator ,
3230} from '@object-ui/components' ;
33- import { Home , Search , HelpCircle , LogOut , Settings , User as UserIcon , ChevronsUpDown } from 'lucide-react' ;
31+ import {
32+ Search ,
33+ HelpCircle ,
34+ LogOut ,
35+ Settings ,
36+ User as UserIcon ,
37+ ChevronsUpDown ,
38+ Boxes ,
39+ } from 'lucide-react' ;
3440import { useAuth , getUserInitials } from '@object-ui/auth' ;
3541import { useObjectTranslation } from '@object-ui/i18n' ;
3642import { WorkspaceSwitcher } from '../../components/WorkspaceSwitcher' ;
@@ -43,11 +49,7 @@ interface HomeTopNavProps {
4349 activities ?: ActivityItem [ ] ;
4450}
4551
46- /**
47- * Opens the global command palette via the same keyboard shortcut the
48- * `CommandPalette` component listens for. This keeps the search UX
49- * consistent with the in-app `AppHeader`.
50- */
52+ /** Opens the global command palette via the ⌘K/CtrlK shortcut it listens for. */
5153function openCommandPalette ( ) {
5254 document . dispatchEvent ( new KeyboardEvent ( 'keydown' , { key : 'k' , metaKey : true } ) ) ;
5355}
@@ -59,139 +61,174 @@ export function HomeTopNav({ activities }: HomeTopNavProps) {
5961
6062 return (
6163 < header
62- className = "sticky top-0 z-40 flex h-14 sm:h-16 w-full shrink-0 items-center gap-2 border-b bg-background px-3 sm:px-4 md:px-6 "
64+ className = "sticky top-0 z-40 w-full shrink-0 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 shadow-[0_1px_0_rgba(0,0,0,0.04)] "
6365 data-testid = "home-top-nav"
6466 >
65- { /* Left: Workspace switcher + primary Home link */ }
66- < div className = "flex items-center gap-2 min-w-0" >
67- < div className = "w-56 max-w-[14rem] shrink-0" >
68- < WorkspaceSwitcher
69- onWorkspaceChange = { ( ) => {
70- // Reload so adapter / permissions re-initialize with the new
71- // tenant context, matching the sidebar's prior behavior.
72- window . location . href = `${ window . location . origin } /console/home` ;
73- } }
74- />
75- </ div >
76-
77- < nav className = "hidden md:flex items-center gap-1 pl-1" aria-label = { t ( 'home.nav' , { defaultValue : 'Home' } ) } >
78- < Link
79- to = "/home"
80- className = "inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium text-foreground bg-accent/50 hover:bg-accent transition-colors"
81- data-testid = "home-nav-home"
82- >
83- < Home className = "h-4 w-4" />
84- < span > { t ( 'home.nav' , { defaultValue : 'Home' } ) } </ span >
85- </ Link >
86- </ nav >
87- </ div >
67+ { /* ──────────────────────────── Row 1: Global ──────────────────────────── */ }
68+ < div className = "flex h-12 w-full items-center gap-2 px-3 sm:px-4 md:px-6" >
69+ { /* Brand */ }
70+ < Link
71+ to = "/home"
72+ className = "flex items-center gap-2 shrink-0 rounded-md px-1.5 py-1 hover:bg-accent/50 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
73+ data-testid = "home-brand"
74+ aria-label = "ObjectStack"
75+ >
76+ < div className = "flex h-7 w-7 items-center justify-center rounded-md bg-primary text-primary-foreground" >
77+ < Boxes className = "h-4 w-4" />
78+ </ div >
79+ < span className = "hidden sm:inline text-sm font-semibold tracking-tight" >
80+ ObjectStack
81+ </ span >
82+ </ Link >
8883
89- { /* Right: actions */ }
90- < div className = "ml-auto flex items-center gap-0.5 sm:gap-1 md:gap-2 shrink-0" >
91- { /* Search — Desktop */ }
84+ { /* Search */ }
9285 < button
9386 onClick = { openCommandPalette }
94- className = "hidden lg:flex relative items-center gap-2 w-50 xl:w-70 h-8 px-3 text-sm rounded-md border bg-muted/50 text-muted-foreground hover:bg-muted transition-colors"
87+ className = "hidden lg:flex relative items-center gap-2 ml-auto w-72 xl:w-96 h-8 px-3 text-sm rounded-md border bg-muted/40 text-muted-foreground hover:bg-muted transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring "
9588 data-testid = "home-search-trigger"
9689 >
97- < Search className = "h-4 w-4" />
98- < span className = "flex-1 text-left" > { t ( 'console.search' , { defaultValue : 'Search...' } ) } </ span >
99- < kbd className = "pointer-events-none inline-flex h-5 items-center gap-0.5 rounded border bg-muted px-1.5 text-[10px] font-medium text-muted-foreground" >
90+ < Search className = "h-4 w-4 shrink-0" />
91+ < span className = "flex-1 text-left truncate" >
92+ { t ( 'console.searchRich' , {
93+ defaultValue : 'Search apps, objects, records…' ,
94+ } ) }
95+ </ span >
96+ < kbd className = "pointer-events-none inline-flex h-5 items-center gap-0.5 rounded border bg-background px-1.5 text-[10px] font-medium text-muted-foreground" >
10097 < span className = "text-xs" > ⌘</ span > K
10198 </ kbd >
10299 </ button >
103100
104- { /* Search — Mobile / Tablet */ }
105- < Button
106- variant = "ghost"
107- size = "icon"
108- className = "lg:hidden h-8 w-8 shrink-0"
109- onClick = { openCommandPalette }
110- aria-label = { t ( 'console.search' , { defaultValue : 'Search...' } ) }
111- >
112- < Search className = "h-4 w-4" />
113- </ Button >
101+ { /* Trailing icon cluster */ }
102+ < div className = "ml-auto lg:ml-2 flex items-center gap-0.5 sm:gap-1 shrink-0" >
103+ { /* Search — mobile/tablet */ }
104+ < Button
105+ variant = "ghost"
106+ size = "icon"
107+ className = "lg:hidden h-8 w-8 shrink-0"
108+ onClick = { openCommandPalette }
109+ aria-label = { t ( 'console.search' , { defaultValue : 'Search...' } ) }
110+ >
111+ < Search className = "h-4 w-4" />
112+ </ Button >
114113
115- { /* Activity feed */ }
116- < div className = "hidden sm:flex shrink-0 relative" >
117- < ActivityFeed activities = { activities ?? [ ] } />
118- </ div >
114+ { /* Activity */ }
115+ < div className = "hidden sm:flex shrink-0 relative" >
116+ < ActivityFeed activities = { activities ?? [ ] } />
117+ </ div >
119118
120- { /* Help */ }
121- < Button
122- variant = "ghost"
123- size = "icon"
124- asChild
125- className = "h-8 w-8 hidden md:inline-flex shrink-0"
126- aria-label = { t ( 'sidebar.helpTooltip' , { defaultValue : 'Help & Documentation' } ) }
127- >
128- < a href = "https://docs.objectstack.ai" target = "_blank" rel = "noopener noreferrer" >
129- < HelpCircle className = "h-4 w-4" />
130- </ a >
131- </ Button >
119+ { /* Help */ }
120+ < Button
121+ variant = "ghost"
122+ size = "icon"
123+ asChild
124+ className = "h-8 w-8 hidden md:inline-flex shrink-0"
125+ aria-label = { t ( 'sidebar.helpTooltip' , {
126+ defaultValue : 'Help & Documentation' ,
127+ } ) }
128+ >
129+ < a
130+ href = "https://docs.objectstack.ai"
131+ target = "_blank"
132+ rel = "noopener noreferrer"
133+ >
134+ < HelpCircle className = "h-4 w-4" />
135+ </ a >
136+ </ Button >
132137
133- { /* Theme toggle */ }
134- < div className = "hidden sm:flex shrink-0" >
135- < ModeToggle />
136- </ div >
138+ < Separator
139+ orientation = "vertical"
140+ className = "hidden sm:block mx-0.5 h-5"
141+ / >
137142
138- { /* Language switcher */ }
139- < div className = "hidden sm:flex shrink-0" >
140- < LocaleSwitcher />
141- </ div >
143+ { /* Theme + Locale */ }
144+ < div className = "hidden sm:flex shrink-0" >
145+ < ModeToggle />
146+ </ div >
147+ < div className = "hidden sm:flex shrink-0" >
148+ < LocaleSwitcher />
149+ </ div >
142150
143- { /* User profile dropdown */ }
144- < DropdownMenu >
145- < DropdownMenuTrigger asChild >
146- < button
147- className = "flex items-center gap-2 rounded-md px-1.5 py-1 hover:bg-accent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
148- data-testid = "home-user-menu-trigger"
149- >
150- < Avatar className = "h-8 w-8 rounded-lg" >
151- < AvatarImage src = { user ?. image ?? '/avatars/user.jpg' } alt = { user ?. name ?? 'User' } />
152- < AvatarFallback className = "rounded-lg bg-primary text-primary-foreground text-xs" >
153- { getUserInitials ( user ) }
154- </ AvatarFallback >
155- </ Avatar >
156- < ChevronsUpDown className = "hidden md:block h-4 w-4 text-muted-foreground" />
157- </ button >
158- </ DropdownMenuTrigger >
159- < DropdownMenuContent align = "end" sideOffset = { 4 } className = "w-56" >
160- < DropdownMenuLabel className = "p-0 font-normal" >
161- < div className = "flex items-center gap-2 px-1 py-1.5 text-left text-sm" >
151+ < Separator
152+ orientation = "vertical"
153+ className = "hidden sm:block mx-0.5 h-5"
154+ />
155+
156+ { /* Workspace switcher — self-sizing; collapses when it renders null */ }
157+ < div className = "hidden md:flex shrink-0 min-w-0 max-w-52" >
158+ < WorkspaceSwitcher
159+ onWorkspaceChange = { ( ) => {
160+ // Reload so adapter / permissions re-initialize with the new
161+ // tenant context, matching the sidebar's prior behavior.
162+ window . location . href = `${ window . location . origin } /console/home` ;
163+ } }
164+ />
165+ </ div >
166+
167+ { /* User profile */ }
168+ < DropdownMenu >
169+ < DropdownMenuTrigger asChild >
170+ < button
171+ className = "flex items-center gap-2 rounded-md px-1.5 py-1 hover:bg-accent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
172+ data-testid = "home-user-menu-trigger"
173+ >
162174 < Avatar className = "h-8 w-8 rounded-lg" >
163- < AvatarImage src = { user ?. image ?? '/avatars/user.jpg' } alt = { user ?. name ?? 'User' } />
164- < AvatarFallback className = "rounded-lg bg-primary text-primary-foreground" >
175+ < AvatarImage
176+ src = { user ?. image ?? '/avatars/user.jpg' }
177+ alt = { user ?. name ?? 'User' }
178+ />
179+ < AvatarFallback className = "rounded-lg bg-primary text-primary-foreground text-xs" >
165180 { getUserInitials ( user ) }
166181 </ AvatarFallback >
167182 </ Avatar >
168- < div className = "grid flex-1 text-left text-sm leading-tight" >
169- < span className = "truncate font-semibold" > { user ?. name ?? 'User' } </ span >
170- < span className = "truncate text-xs text-muted-foreground" > { user ?. email ?? '' } </ span >
183+ < ChevronsUpDown className = "hidden md:block h-4 w-4 text-muted-foreground" />
184+ </ button >
185+ </ DropdownMenuTrigger >
186+ < DropdownMenuContent align = "end" sideOffset = { 4 } className = "w-56" >
187+ < DropdownMenuLabel className = "p-0 font-normal" >
188+ < div className = "flex items-center gap-2 px-1 py-1.5 text-left text-sm" >
189+ < Avatar className = "h-8 w-8 rounded-lg" >
190+ < AvatarImage
191+ src = { user ?. image ?? '/avatars/user.jpg' }
192+ alt = { user ?. name ?? 'User' }
193+ />
194+ < AvatarFallback className = "rounded-lg bg-primary text-primary-foreground" >
195+ { getUserInitials ( user ) }
196+ </ AvatarFallback >
197+ </ Avatar >
198+ < div className = "grid flex-1 text-left text-sm leading-tight" >
199+ < span className = "truncate font-semibold" >
200+ { user ?. name ?? 'User' }
201+ </ span >
202+ < span className = "truncate text-xs text-muted-foreground" >
203+ { user ?. email ?? '' }
204+ </ span >
205+ </ div >
171206 </ div >
172- </ div >
173- </ DropdownMenuLabel >
174- < DropdownMenuSeparator />
175- < DropdownMenuGroup >
176- < DropdownMenuItem onClick = { ( ) => navigate ( '/apps/setup/system/profile' ) } >
177- < UserIcon className = "mr-2 h-4 w-4" />
178- { t ( 'user.profile' , { defaultValue : 'Profile' } ) }
207+ </ DropdownMenuLabel >
208+ < DropdownMenuSeparator />
209+ < DropdownMenuGroup >
210+ < DropdownMenuItem
211+ onClick = { ( ) => navigate ( '/apps/setup/system/profile' ) }
212+ >
213+ < UserIcon className = "mr-2 h-4 w-4" />
214+ { t ( 'user.profile' , { defaultValue : 'Profile' } ) }
215+ </ DropdownMenuItem >
216+ < DropdownMenuItem onClick = { ( ) => navigate ( '/apps/setup' ) } >
217+ < Settings className = "mr-2 h-4 w-4" />
218+ { t ( 'sidebar.settings' , { defaultValue : 'Settings' } ) }
219+ </ DropdownMenuItem >
220+ </ DropdownMenuGroup >
221+ < DropdownMenuSeparator />
222+ < DropdownMenuItem
223+ className = "text-destructive focus:text-destructive"
224+ onClick = { ( ) => signOut ( ) }
225+ >
226+ < LogOut className = "mr-2 h-4 w-4" />
227+ { t ( 'user.logout' , { defaultValue : 'Log out' } ) }
179228 </ DropdownMenuItem >
180- < DropdownMenuItem onClick = { ( ) => navigate ( '/apps/setup' ) } >
181- < Settings className = "mr-2 h-4 w-4" />
182- { t ( 'sidebar.settings' , { defaultValue : 'Settings' } ) }
183- </ DropdownMenuItem >
184- </ DropdownMenuGroup >
185- < DropdownMenuSeparator />
186- < DropdownMenuItem
187- className = "text-destructive focus:text-destructive"
188- onClick = { ( ) => signOut ( ) }
189- >
190- < LogOut className = "mr-2 h-4 w-4" />
191- { t ( 'user.logout' , { defaultValue : 'Log out' } ) }
192- </ DropdownMenuItem >
193- </ DropdownMenuContent >
194- </ DropdownMenu >
229+ </ DropdownMenuContent >
230+ </ DropdownMenu >
231+ </ div >
195232 </ div >
196233 </ header >
197234 ) ;
0 commit comments