Skip to content

Commit 219bd7a

Browse files
committed
Enhance user action menu and sidebar in LayoutRenderer
Expanded the user action in app.json to include a dropdown menu with profile, billing, settings, and logout options. Updated LayoutRenderer to support a collapsible sidebar, improved sidebar and header UI, and implemented a dropdown menu for user actions using @object-ui/components. Extended AppAction type to support avatar, description, items, shortcut, variant, and size for richer user and dropdown actions.
1 parent 89d282d commit 219bd7a

File tree

3 files changed

+152
-32
lines changed

3 files changed

+152
-32
lines changed

examples/crm-app/app.json

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,38 @@
3636
},
3737
{
3838
"type": "user",
39-
"label": "John Doe",
40-
"avatar": "https://ui.shadcn.com/avatars/01.png"
39+
"label": "Alicia Koch",
40+
"description": "[email protected]",
41+
"avatar": "https://ui.shadcn.com/avatars/02.png",
42+
"items": [
43+
{
44+
"type": "item",
45+
"label": "Profile",
46+
"shortcut": "⇧⌘P"
47+
},
48+
{
49+
"type": "item",
50+
"label": "Billing",
51+
"shortcut": "⌘B"
52+
},
53+
{
54+
"type": "item",
55+
"label": "Settings",
56+
"shortcut": "⌘S"
57+
},
58+
{
59+
"type": "item",
60+
"label": "New Team"
61+
},
62+
{
63+
"type": "separator"
64+
},
65+
{
66+
"type": "item",
67+
"label": "Log out",
68+
"shortcut": "⇧⌘Q"
69+
}
70+
]
4171
}
4272
]
4373
}

packages/runner/src/LayoutRenderer.tsx

Lines changed: 96 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
import React from 'react';
22
import type { AppSchema } from '@object-ui/types';
33
import * 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

518
interface LayoutRendererProps {
619
app: AppSchema;
@@ -25,6 +38,7 @@ const getIcon = (name?: string) => {
2538

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

packages/types/src/app.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,28 @@ export interface AppAction {
111111
label?: string;
112112
icon?: string;
113113
onClick?: string;
114+
/**
115+
* User Avatar URL (for type='user')
116+
*/
117+
avatar?: string;
118+
/**
119+
* Additional description (e.g. email for user)
120+
*/
121+
description?: string;
122+
/**
123+
* Dropdown Menu Items (for type='dropdown' or 'user')
124+
*/
125+
items?: MenuItem[];
126+
/**
127+
* Keyboard shortcut
128+
*/
129+
shortcut?: string;
130+
/**
131+
* Button variant
132+
*/
133+
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
134+
/**
135+
* Button size
136+
*/
137+
size?: 'default' | 'sm' | 'lg' | 'icon';
114138
}

0 commit comments

Comments
 (0)