All templates
Sidebar layout
Shell - Left Sidebar
Application frame with a collapsible navigation sidebar and a main area that renders the current page as children. Shell, chrome, layout frame, sidebar, left rail, slot, or navigation.
npx @adaptavant/edscli templates sidebar-layoutPreview
Open in new tabCode
'use client';
import {
Avatar,
CalDynamicIcon,
IconButton,
type IconProps,
MenuItem,
Text,
Tooltip,
} from '@adaptavant/eds-core';
import {
ChevronLeftIcon,
ConnectIcon,
CustomerIcon,
GrowYourBrandIcon,
IntegrationsIcon,
MenuIcon,
MoneyIcon,
ServicesIcon,
SetmoreIcon,
SettingsIcon,
UpdatesIcon,
} from '@adaptavant/eds-icons/icons';
import * as React from 'react';
const NAV_ITEMS = [
{
label: 'Calendar',
icon: ({ size }: IconProps) => (
<CalDynamicIcon size={size}>{new Date().getDate()}</CalDynamicIcon>
),
},
{ label: 'Services', icon: ServicesIcon },
{ label: 'Connect', icon: ConnectIcon },
{ label: 'Customers', icon: CustomerIcon },
{ label: 'Payments', icon: MoneyIcon },
{ label: 'Integrations', icon: IntegrationsIcon },
{ label: 'Updates', icon: UpdatesIcon },
{ label: 'Settings', icon: SettingsIcon },
];
/**
* The app frame: a collapsible navigation sidebar beside a main area that
* scrolls on its own. Pass the current page as `children`.
*/
export function AppShell({ children }: { children: React.ReactNode }) {
const [activeNav, setActiveNav] = React.useState('Calendar');
const [isNavCollapsed, setIsNavCollapsed] = React.useState(false);
return (
<div className="flex h-screen overflow-hidden bg-canvas text-primary">
<NavSidebar
activeNav={activeNav}
isCollapsed={isNavCollapsed}
onNavigate={setActiveNav}
onToggleCollapsed={() => setIsNavCollapsed((collapsed) => !collapsed)}
/>
{/* The sidebar is fixed, so the main area is offset by its width. */}
<main
className={`min-w-0 flex-1 overflow-y-auto transition-[margin] duration-200 ease-in-out ${
isNavCollapsed ? 'ms-[57px]' : 'ms-[216px]'
}`}
>
{children}
</main>
</div>
);
}
/** Preview: the shell with placeholder page content in its slot. */
export default function SidebarLayoutTemplate() {
return (
<AppShell>
<div aria-hidden className="flex h-full flex-col gap-4 p-6">
<div className="h-10 w-1/3 rounded-8px bg-neutral-secondary" />
<div className="flex-1 rounded-8px bg-neutral-secondary" />
</div>
</AppShell>
);
}
function NavSidebar({
activeNav,
isCollapsed,
onNavigate,
onToggleCollapsed,
}: {
activeNav: string;
isCollapsed: boolean;
onNavigate: (label: string) => void;
onToggleCollapsed: () => void;
}) {
const navItem = (
label: string,
Icon: (typeof NAV_ITEMS)[number]['icon'],
isActive = false
) => {
const item = (triggerProps = {}) => (
<MenuItem
{...triggerProps}
aria-current={isActive ? 'page' : undefined}
aria-label={isCollapsed ? label : undefined}
as="a"
className={
isCollapsed
? 'justify-center origin-left overflow-hidden whitespace-nowrap rounded-8px px-2 text-body-12'
: 'origin-left overflow-hidden whitespace-nowrap rounded-8px text-body-12'
}
classNames={isCollapsed ? { center: 'hidden' } : undefined}
href={`#${label.toLowerCase()}`}
isHighlighted={isActive}
key={label}
onClick={() => onNavigate(label)}
railStart={<Icon size="16" />}
size="large"
>
{label}
</MenuItem>
);
// Collapsed, only the icon shows: the tooltip names it.
return isCollapsed ? (
<Tooltip content={label} key={label} placement="right">
{({ triggerProps }) => item(triggerProps)}
</Tooltip>
) : (
item()
);
};
// Collapsed, the rail's inner width is 32px: padding sizes each row to it.
return (
<aside
className={`fixed left-0 top-0 z-[2] flex h-screen origin-left flex-col items-start gap-6 border-r border-solid border-tertiary bg-neutral pb-4 pe-3 ps-3 pt-4 transition-[width] duration-200 ease-in-out ${
isCollapsed ? 'w-[57px]' : 'w-[216px]'
}`}
>
{/* Collapsed, the header is only the menu button that expands it. */}
<div
className={`flex w-full items-center gap-2 overflow-hidden whitespace-nowrap ${
isCollapsed ? 'justify-center' : 'px-2'
}`}
>
{isCollapsed ? (
<IconButton
aria-label="Expand sidebar"
icon={MenuIcon}
onClick={onToggleCollapsed}
variant="neutralTertiary"
/>
) : (
<>
<SetmoreIcon className="shrink-0" size="24" />
<Text className="where:block text-primary text-body-12 font-stronger line-clamp-2 w-[110px] flex-shrink-0 !leading-4">
AnywhereWorks
</Text>
<IconButton
aria-label="Collapse sidebar"
icon={ChevronLeftIcon}
onClick={onToggleCollapsed}
variant="neutralTertiary"
/>
</>
)}
</div>
<nav aria-label="Primary" className="flex w-full flex-col gap-1">
{navItem('Grow your brand', GrowYourBrandIcon)}
<div className="my-2" />
{NAV_ITEMS.map((item) =>
navItem(item.label, item.icon, item.label === activeNav)
)}
</nav>
<div className="mt-auto flex w-full flex-col gap-1">
<MenuItem
aria-label={isCollapsed ? 'Alex Morgan' : undefined}
className={
isCollapsed
? 'justify-start overflow-hidden whitespace-nowrap rounded-8px px-1 text-body-12'
: ' overflow-hidden justify-start whitespace-nowrap rounded-8px text-body-12'
}
classNames={{ center: isCollapsed ? 'w-0' : 'w-full ' }}
railStart={<Avatar name="Alex Morgan" size="24" status="available" />}
size="large"
>
{isCollapsed ? null : 'Alex Morgan'}
</MenuItem>
</div>
</aside>
);
}