11import React from 'react' ;
22import type { AppSchema } from '@object-ui/types' ;
33import * as LucideIcons from 'lucide-react' ;
4+ import {
5+ DropdownMenu ,
6+ DropdownMenuContent ,
7+ DropdownMenuGroup ,
8+ DropdownMenuItem ,
9+ DropdownMenuLabel ,
10+ DropdownMenuSeparator ,
11+ DropdownMenuTrigger ,
12+ DropdownMenuShortcut ,
13+ Avatar ,
14+ AvatarImage ,
15+ AvatarFallback
16+ } from '@object-ui/components' ;
417
518interface LayoutRendererProps {
619 app : AppSchema ;
@@ -25,6 +38,7 @@ const getIcon = (name?: string) => {
2538
2639export const LayoutRenderer = ( { app, children, currentPath, onNavigate } : LayoutRendererProps ) => {
2740 const layout = app . layout || 'sidebar' ;
41+ const [ isSidbarOpen , setSidebarOpen ] = React . useState ( true ) ;
2842
2943 const handleNavClick = ( e : React . MouseEvent < HTMLAnchorElement > , path : string ) => {
3044 e . preventDefault ( ) ;
@@ -42,19 +56,27 @@ export const LayoutRenderer = ({ app, children, currentPath, onNavigate }: Layou
4256 const LogoIcon = app . logo && ! app . logo . includes ( '/' ) && ! app . logo . includes ( '.' ) ? getIcon ( app . logo ) : null ;
4357
4458 return (
45- < div className = { `flex min-h-screen w-full bg-muted/40 ${ app . className || '' } ` } >
59+ < div className = { `flex min-h-screen w-full bg-slate-50/50 ${ app . className || '' } ` } >
4660 { /* Sidebar - Only if configured */ }
4761 { layout === 'sidebar' && (
48- < aside className = "w-64 flex-shrink-0 border-r bg-background hidden md:flex flex-col h-screen sticky top-0 z-30" >
49- < div className = "h-14 flex items-center px-6 border-b font-semibold text-lg tracking-tight" >
62+ < aside
63+ className = { `
64+ flex-shrink-0 border-r bg-background hidden md:flex flex-col h-screen sticky top-0 z-30 transition-all duration-300 ease-in-out
65+ ${ isSidbarOpen ? 'w-64' : 'w-[70px]' }
66+ ` }
67+ >
68+ < div className = { `h-14 flex items-center border-b font-semibold text-lg tracking-tight transition-all ${ isSidbarOpen ? 'px-6' : 'justify-center px-0' } ` } >
5069 { LogoIcon ? (
51- < LogoIcon className = "h-6 w-6 mr-2 " />
70+ < LogoIcon className = "h-6 w-6" />
5271 ) : app . logo ? (
53- < img src = { app . logo } alt = { app . title } className = "h-6 w-auto mr-2" />
54- ) : < LucideIcons . Box className = "h-6 w-6 mr-2" /> }
55- < span className = "" > { app . title || app . name || 'Object UI' } </ span >
72+ < img src = { app . logo } alt = { app . title } className = "h-6 w-auto" />
73+ ) : < LucideIcons . Box className = "h-6 w-6" /> }
74+
75+ < span className = { `ml-2 whitespace-nowrap overflow-hidden transition-all duration-300 ${ isSidbarOpen ? 'opacity-100 w-auto' : 'opacity-0 w-0 hidden' } ` } >
76+ { app . title || app . name || 'Object UI' }
77+ </ span >
5678 </ div >
57- < nav className = "flex-1 p-4 space-y-1 overflow-y-auto" >
79+ < nav className = "flex-1 p-2 space-y-1 overflow-y-auto overflow-x-hidden " >
5880 { app . menu ?. map ( ( item , index ) => {
5981 const isActive = currentPath === item . path ;
6082 const Icon = getIcon ( item . icon ) ;
@@ -63,19 +85,22 @@ export const LayoutRenderer = ({ app, children, currentPath, onNavigate }: Layou
6385 key = { index }
6486 href = { item . path || '#' }
6587 onClick = { ( e ) => item . path && handleNavClick ( e , item . path ) }
66- className = { `flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors ${
88+ title = { ! isSidbarOpen ? item . label : undefined }
89+ className = { `flex items-center py-2 text-sm font-medium rounded-md transition-colors ${
6790 isActive
6891 ? 'bg-primary text-primary-foreground'
6992 : 'text-muted-foreground hover:bg-muted hover:text-foreground'
70- } `}
93+ } ${ isSidbarOpen ? 'px-3' : 'justify-center px-2' } `}
7194 >
72- { Icon && < Icon className = { `mr-3 h-4 w-4 ${ isActive ? 'text-primary-foreground' : 'text-muted-foreground group-hover:text-foreground' } ` } /> }
73- { item . label }
95+ { Icon && < Icon className = { `h-4 w-4 flex-shrink-0 ${ isSidbarOpen ? 'mr-3' : '' } ${ isActive ? 'text-primary-foreground' : 'text-muted-foreground group-hover:text-foreground' } ` } /> }
96+ < span className = { `whitespace-nowrap overflow-hidden transition-all duration-300 ${ isSidbarOpen ? 'opacity-100 w-auto' : 'opacity-0 w-0 hidden' } ` } >
97+ { item . label }
98+ </ span >
7499 </ a >
75100 ) ;
76101 } ) }
77102 </ nav >
78- { app . version && (
103+ { app . version && isSidbarOpen && (
79104 < div className = "p-4 border-t text-xs text-muted-foreground" >
80105 v{ app . version }
81106 </ div >
@@ -86,19 +111,23 @@ export const LayoutRenderer = ({ app, children, currentPath, onNavigate }: Layou
86111 { /* Main Content Area */ }
87112 < div className = "flex-1 flex flex-col min-w-0 overflow-hidden" >
88113 { /* Header - Always shown in sidebar/header layouts */ }
89- < header className = "h-14 flex items-center justify-between px-6 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b z-20 sticky top-0" >
114+ < header className = "h-14 flex items-center justify-between px-4 md:px- 6 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b z-20 sticky top-0" >
90115 < div className = "flex items-center gap-4" >
91- { /* Mobile toggle would go here */ }
92- < h1 className = "text-lg font-semibold md:hidden" >
93- { app . title || 'Object UI' }
94- </ h1 >
116+ { /* Toggle Sidebar Button */ }
117+ < button
118+ onClick = { ( ) => setSidebarOpen ( ! isSidbarOpen ) }
119+ className = "p-2 -ml-2 text-muted-foreground hover:bg-muted hover:text-foreground rounded-md transition-colors"
120+ >
121+ < LucideIcons . Menu className = "h-5 w-5" />
122+ </ button >
123+
95124 { /* Breadcrumbs placeholder or Search */ }
96125 < div className = "relative hidden md:block w-96" >
97126 < LucideIcons . Search className = "absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
98127 < input
99128 type = "text"
100129 placeholder = "Search..."
101- className = "w-full h-9 pl-9 pr-4 rounded-md border border-input bg-transparent text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
130+ className = "w-full h-9 pl-9 pr-4 rounded-md border border-input bg-background text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
102131 />
103132 </ div >
104133 </ div >
@@ -109,17 +138,54 @@ export const LayoutRenderer = ({ app, children, currentPath, onNavigate }: Layou
109138 < span className = "absolute top-1.5 right-1.5 h-2 w-2 bg-red-600 rounded-full border-2 border-background" > </ span >
110139 </ button >
111140
112- { app . actions ?. find ( a => a . type === 'user' ) ?. avatar ? (
113- < img
114- src = { app . actions . find ( a => a . type === 'user' ) ?. avatar }
115- className = "h-8 w-8 rounded-full border bg-muted"
116- alt = "User"
117- />
118- ) : (
119- < div className = "h-8 w-8 rounded-full bg-secondary flex items-center justify-center text-secondary-foreground font-medium text-sm" >
120- JD
121- </ div >
122- ) }
141+ { app . actions ?. filter ( a => a . type === 'user' ) . map ( ( userAction , i ) => (
142+ < DropdownMenu key = { i } >
143+ < DropdownMenuTrigger asChild >
144+ < button className = "relative h-8 w-8 rounded-full border bg-muted overflow-hidden focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 hover:opacity-90 transition-opacity" >
145+ < Avatar className = "h-full w-full" >
146+ < AvatarImage
147+ src = { userAction . avatar }
148+ alt = { userAction . label || 'User' }
149+ />
150+ < AvatarFallback >
151+ { userAction . label ?. substring ( 0 , 2 ) . toUpperCase ( ) || 'JD' }
152+ </ AvatarFallback >
153+ </ Avatar >
154+ </ button >
155+ </ DropdownMenuTrigger >
156+ < DropdownMenuContent className = "w-56" align = "end" forceMount >
157+ < DropdownMenuLabel className = "font-normal" >
158+ < div className = "flex flex-col space-y-1" >
159+ < p className = "text-sm font-medium leading-none" > { userAction . label || 'User' } </ p >
160+ < p className = "text-xs leading-none text-muted-foreground" >
161+ { userAction . description || '[email protected] ' } 162+ </ p >
163+ </ div >
164+ </ DropdownMenuLabel >
165+ < DropdownMenuSeparator />
166+ < DropdownMenuGroup >
167+ { userAction . items ?. map ( ( item , idx ) => {
168+ if ( item . type === 'separator' ) {
169+ return < DropdownMenuSeparator key = { idx } /> ;
170+ }
171+ return (
172+ < DropdownMenuItem key = { idx } onSelect = { ( ) => {
173+ if ( ( item as any ) . onClick ) {
174+ // Handle click logic
175+ console . log ( 'Clicked' , item . label ) ;
176+ }
177+ } } >
178+ { item . label }
179+ { ( item as any ) . shortcut && (
180+ < DropdownMenuShortcut > { ( item as any ) . shortcut } </ DropdownMenuShortcut >
181+ ) }
182+ </ DropdownMenuItem >
183+ ) ;
184+ } ) }
185+ </ DropdownMenuGroup >
186+ </ DropdownMenuContent >
187+ </ DropdownMenu >
188+ ) ) }
123189 </ div >
124190 </ header >
125191
0 commit comments