import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { useState } from 'react'
import { fn } from 'storybook/test'
import {
BracketCurlyIcon,
ChartDonutIcon,
IdCardIcon,
MonitorIcon,
Settings02Icon
} from '../components/icons-v2-generated'
import { AppLayout } from '../components/navigation/app-layout'
import { NavigationSidebarConfig } from '../types/navigation'
const defaultNavigationItems: NavigationSidebarConfig['items'] = [
{
id: 'dashboard',
label: 'Dashboard',
icon: ,
path: '/dashboard',
isActive: true,
},
{
id: 'organizations',
label: 'Organizations',
icon: ,
path: '/organizations',
},
{
id: 'devices',
label: 'Devices',
icon: ,
path: '/devices',
},
{
id: 'scripts',
label: 'Scripts',
icon: ,
path: '/scripts',
},
]
const secondaryNavigationItems: NavigationSidebarConfig['items'] = [
{
id: 'settings',
label: 'Settings',
icon: ,
path: '/settings',
section: 'secondary',
},
]
const allNavigationItems: NavigationSidebarConfig['items'] = [
...defaultNavigationItems,
...secondaryNavigationItems,
]
const meta = {
title: 'Navigation/AppLayout',
component: AppLayout,
parameters: {
layout: 'fullscreen',
docs: {
description: {
component: `
A unified application layout component that combines the NavigationSidebar, AppHeader, and main content area.
## Layout Structure
\`\`\`
┌──────────────────────────────────────────────────────────────┐
│ ┌──────────┐ ┌─────────────────────────────────────────────┐ │
│ │ │ │ App Header │ │
│ │ │ ├─────────────────────────────────────────────┤ │
│ │ Sidebar │ │ │ │
│ │ │ │ Main Content Area │ │
│ │ │ │ (with Suspense) │ │
│ │ │ │ │ │
│ └──────────┘ └─────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
\`\`\`
## Key Features
- **Responsive Sidebar**: Collapsible navigation with mobile overlay support
- **Configurable Header**: Optional header with user menu, notifications, search
- **Suspense Integration**: Built-in loading state support for main content
- **Flexible Styling**: Customizable via className props
`
}
}
},
tags: ['autodocs'],
args: {
sidebarConfig: {
items: allNavigationItems,
onNavigate: fn(),
onToggleMinimized: fn(),
},
headerProps: {
showNotifications: true,
showUser: true,
userName: 'John Doe',
userEmail: 'john@example.com',
onProfile: fn(),
onLogout: fn(),
},
children: (
Main Content
This is where your page content goes.
),
},
} satisfies Meta
export default meta
type Story = StoryObj
/**
* Default layout with sidebar, header, and main content area.
*/
export const Default: Story = {
args: {
mobileBurgerMenuProps: {
user: {
userName: 'Alex Developer',
userEmail: 'alex@openframe.dev',
userAvatarUrl: 'https://github.com/shadcn.png',
userRole: 'Admin',
},
},
},
render: (args) => {
const [items, setItems] = useState(args.sidebarConfig.items)
const handleNavigate = (path: string) => {
fn()(path)
setItems(prevItems =>
prevItems.map(item => ({
...item,
isActive: item.path === path,
}))
)
}
return (
)
},
}
/**
* Layout without custom user info - using only headerProps user data.
*/
export const WithHeaderUserOnly: Story = {
args: {
mobileBurgerMenuProps: {
user: {
userName: 'Alex Developer',
userEmail: 'alex@openframe.dev',
userAvatarUrl: 'https://github.com/shadcn.png',
userRole: 'Admin',
},
},
children: (
Header User Only
This layout uses user info from headerProps for the sidebar.
),
},
}
/**
* Layout with header showing search and organization selector.
*/
export const WithSearchAndOrganizations: Story = {
args: {
mobileBurgerMenuProps: {
user: {
userName: 'Alex Developer',
userEmail: 'alex@openframe.dev',
userAvatarUrl: 'https://github.com/shadcn.png',
userRole: 'Admin',
},
},
headerProps: {
showSearch: true,
onSearch: fn(),
showOrganizations: true,
organizations: [
{ id: '1', name: 'Acme Corp' },
{ id: '2', name: 'Tech Startup' },
{ id: '3', name: 'Enterprise Inc' },
],
selectedOrgId: '1',
onOrgChange: fn(),
showNotifications: true,
showUser: true,
userName: 'Jane Smith',
userEmail: 'jane@acme.com',
onProfile: fn(),
onLogout: fn(),
},
children: (
Dashboard
Welcome back! Here's your overview.
),
},
}
/**
* Layout with custom main content styling.
*/
export const WithCustomMainClassName: Story = {
args: {
mainClassName: 'bg-ods-bg-card',
mobileBurgerMenuProps: {
user: {
userName: 'Alex Developer',
userEmail: 'alex@openframe.dev',
userAvatarUrl: 'https://github.com/shadcn.png',
userRole: 'Admin',
},
},
children: (
Custom Background
The main content area has a custom background color.
),
},
}
/**
* Layout with minimized sidebar by default.
*/
export const MinimizedSidebar: Story = {
args: {
mobileBurgerMenuProps: {
user: {
userName: 'Alex Developer',
userEmail: 'alex@openframe.dev',
userAvatarUrl: 'https://github.com/shadcn.png',
userRole: 'Admin',
},
},
sidebarConfig: {
items: allNavigationItems,
minimized: true,
onNavigate: fn(),
onToggleMinimized: fn(),
},
children: (
Wider Content Area
The sidebar starts minimized, giving more horizontal space.
),
},
}
/**
* Interactive example with working navigation.
*/
export const Interactive: Story = {
args: {
mobileBurgerMenuProps: {
user: {
userName: 'Alex Developer',
userEmail: 'alex@openframe.dev',
userAvatarUrl: 'https://github.com/shadcn.png',
userRole: 'Admin',
},
},
},
render: function InteractiveStory() {
const [items, setItems] = useState(allNavigationItems)
const [currentPage, setCurrentPage] = useState('Dashboard')
const handleNavigate = (path: string) => {
const pageName = path.replace('/', '').replace(/-/g, ' ')
const formattedName = pageName.charAt(0).toUpperCase() + pageName.slice(1) || 'Dashboard'
setCurrentPage(formattedName)
setItems(prevItems =>
prevItems.map(item => ({
...item,
isActive: item.path === path,
}))
)
}
return (
alert('Profile clicked'),
onLogout: () => alert('Logout clicked'),
}}
>
{currentPage}
Click on sidebar items to navigate. The sidebar can be collapsed using the toggle at the bottom.
Current Route
/{currentPage.toLowerCase().replace(/ /g, '-')}
)
},
}
/**
* Disabled state - only the mobile burger menu toggle and the sidebar collapse/expand
* button remain interactive. All other navigation, header controls, and main content
* are visually dimmed and cannot be interacted with.
*/
export const Disabled: Story = {
args: {
disabled: true,
mobileBurgerMenuProps: {
user: {
userName: 'Alex Developer',
userEmail: 'alex@openframe.dev',
userAvatarUrl: 'https://github.com/shadcn.png',
userRole: 'Admin',
},
onSearchUser: fn(),
onEditProfile: fn(),
onLogout: fn(),
},
headerProps: {
showSearch: true,
onSearch: fn(),
showOrganizations: true,
organizations: [
{ id: '1', name: 'Acme Corp' },
{ id: '2', name: 'Tech Startup' },
],
selectedOrgId: '1',
onOrgChange: fn(),
showNotifications: true,
unreadCount: 3,
showUser: true,
userName: 'Alex Developer',
userEmail: 'alex@openframe.dev',
onProfile: fn(),
onLogout: fn(),
},
children: (
Disabled Layout
Try clicking nav items, search, organizations, the user menu, or any
content here — they are all inert.
Only the burger menu (mobile) and the sidebar collapse/expand toggle at
the bottom of the sidebar remain interactive.
),
},
}
/**
* Example mimicking a typical dashboard page.
*/
export const DashboardExample: Story = {
args: {
headerProps: {
showSearch: false,
onSearch: fn(),
unreadCount: 5,
showNotifications: false,
showUser: true,
showOrganizations: true,
userName: 'Admin User',
userEmail: 'admin@company.com',
onProfile: fn(),
onLogout: fn(),
},
mobileBurgerMenuProps: {
user: {
userName: 'Admin User',
userEmail: 'admin@company.com',
userAvatarUrl: 'https://github.com/shadcn.png',
userRole: 'Admin',
},
onSearchUser: fn(),
onEditProfile: fn(),
onLogout: fn(),
},
children: (
Dashboard
Welcome back! Here's what's happening.
{[
{ label: 'Total Devices', value: '1,234' },
{ label: 'Active Users', value: '567' },
{ label: 'Scripts Run', value: '89' },
].map((stat) => (
{stat.label}
{stat.value}
))}
Recent Activity
{[
{ action: 'Script executed', target: 'backup-daily.sh', time: '2 min ago' },
{ action: 'Device added', target: 'MacBook-Pro-15', time: '15 min ago' },
{ action: 'User invited', target: 'sarah@company.com', time: '1 hour ago' },
].map((activity, index) => (
{activity.action}
{activity.target}
{activity.time}
))}
),
},
}