mirror of https://github.com/buster-so/buster.git
sidebar component completed
This commit is contained in:
parent
5169b4eb92
commit
f59225addb
|
@ -0,0 +1,150 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Sidebar } from './Sidebar';
|
||||
import { BusterRoutes } from '@/routes';
|
||||
import { Window, WindowUser, WindowSettings, WindowAlert } from '../icons/NucleoIconOutlined';
|
||||
|
||||
const meta: Meta<typeof Sidebar> = {
|
||||
title: 'UI/Sidebar/Sidebar',
|
||||
component: Sidebar,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="h-[500px] w-64 bg-transparent">
|
||||
<Story />
|
||||
</div>
|
||||
)
|
||||
]
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Sidebar>;
|
||||
|
||||
const mockItems = [
|
||||
{
|
||||
id: '1',
|
||||
label: 'Home',
|
||||
icon: <Window />,
|
||||
route: BusterRoutes.APP_ROOT
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
label: 'Profile',
|
||||
icon: <WindowUser />,
|
||||
route: BusterRoutes.SETTINGS_PROFILE
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
label: 'Settings',
|
||||
icon: <WindowSettings />,
|
||||
route: BusterRoutes.SETTINGS
|
||||
}
|
||||
];
|
||||
|
||||
const mockGroupedContent = [
|
||||
{
|
||||
items: [
|
||||
{
|
||||
id: '4',
|
||||
label: 'Notifications',
|
||||
icon: <WindowAlert width="1.25em" height="1.25em" />,
|
||||
route: BusterRoutes.SETTINGS_NOTIFICATIONS
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
label: 'Notifications',
|
||||
icon: <WindowAlert width="1.25em" height="1.25em" />,
|
||||
route: BusterRoutes.SETTINGS_NOTIFICATIONS
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Main Menu',
|
||||
items: mockItems
|
||||
}
|
||||
];
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
header: <div className="text-xl font-semibold">My App</div>,
|
||||
content: mockGroupedContent,
|
||||
footer: (
|
||||
<div className="flex h-full items-center justify-center text-sm text-gray-500">Footer</div>
|
||||
),
|
||||
activeItem: '1'
|
||||
}
|
||||
};
|
||||
|
||||
export const WithLongContent: Story = {
|
||||
args: {
|
||||
header: <div className="text-xl font-semibold">My App</div>,
|
||||
content: [
|
||||
{
|
||||
label: 'Main Menu',
|
||||
items: [...Array(20)].map((_, i) => ({
|
||||
id: `item-${i}`,
|
||||
label: `Menu Item ${i + 1}`,
|
||||
icon: <Window width="1.25em" height="1.25em" />,
|
||||
route: BusterRoutes.APP_ROOT
|
||||
}))
|
||||
}
|
||||
],
|
||||
footer: <div className="text-sm text-gray-500">Sticky Footer</div>,
|
||||
activeItem: 'item-1'
|
||||
}
|
||||
};
|
||||
|
||||
export const NoFooter: Story = {
|
||||
args: {
|
||||
header: <div className="text-xl font-semibold">My App</div>,
|
||||
content: mockGroupedContent,
|
||||
activeItem: '1'
|
||||
}
|
||||
};
|
||||
|
||||
export const ScrollAndTruncationTest: Story = {
|
||||
args: {
|
||||
header: <div className="text-xl font-semibold">Scroll & Truncation Test</div>,
|
||||
activeItem: 'long-4',
|
||||
content: [
|
||||
{
|
||||
items: mockItems
|
||||
},
|
||||
{
|
||||
label: 'Short Items',
|
||||
items: [...Array(20)].map((_, i) => ({
|
||||
id: `short-${i}`,
|
||||
label: `Item ${i + 1}`,
|
||||
icon: <Window width="1.25em" height="1.25em" />,
|
||||
route: BusterRoutes.APP_ROOT
|
||||
}))
|
||||
},
|
||||
{
|
||||
label: 'Long Items',
|
||||
items: [
|
||||
{
|
||||
id: 'long-1',
|
||||
label:
|
||||
'This is an extremely long menu item that should definitely get truncated in the UI because it is way too long to fit',
|
||||
icon: <WindowSettings width="1.25em" height="1.25em" />,
|
||||
route: BusterRoutes.SETTINGS
|
||||
},
|
||||
{
|
||||
id: 'long-2',
|
||||
label:
|
||||
'Another very long label that contains some technical terms like Implementation Documentation Requirements',
|
||||
icon: <WindowUser width="1.25em" height="1.25em" />,
|
||||
route: BusterRoutes.SETTINGS_PROFILE
|
||||
},
|
||||
...Array(30)
|
||||
.fill(null)
|
||||
.map((_, i) => ({
|
||||
id: `long-${i + 3}`,
|
||||
label: `Somewhat Long Menu Item ${i + 1} with Additional Description Text`,
|
||||
icon: <WindowAlert width="1.25em" height="1.25em" />,
|
||||
route: BusterRoutes.SETTINGS_NOTIFICATIONS
|
||||
}))
|
||||
]
|
||||
}
|
||||
],
|
||||
footer: <div className="text-sm text-gray-500">Footer for Scroll Test</div>
|
||||
}
|
||||
};
|
|
@ -0,0 +1,50 @@
|
|||
import React from 'react';
|
||||
import { ISidebarGroup, ISidebarList, SidebarProps } from './interfaces';
|
||||
import { SidebarCollapsible } from './SidebarCollapsible';
|
||||
import { SidebarItem } from './SidebarItem';
|
||||
|
||||
export const Sidebar: React.FC<SidebarProps> = React.memo(
|
||||
({ header, content, footer, activeItem }) => {
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-hidden px-3.5 pt-4.5">
|
||||
<div className="flex flex-col space-y-4.5 overflow-hidden">
|
||||
<div className="mb-5"> {header}</div>
|
||||
<div className="flex flex-grow flex-col space-y-4.5 overflow-y-auto pb-3">
|
||||
{content.map((item, index) => (
|
||||
<ContentSelector key={index} content={item} activeItem={activeItem} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{footer && <div className="mt-auto bg-red-200 pt-5">{footer}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const ContentSelector: React.FC<{
|
||||
content: SidebarProps['content'][number];
|
||||
activeItem: SidebarProps['activeItem'];
|
||||
}> = React.memo(({ content, activeItem }) => {
|
||||
if (isSidebarGroup(content)) {
|
||||
return <SidebarCollapsible {...content} activeItem={activeItem} />;
|
||||
}
|
||||
|
||||
return <SidebarList items={content.items} activeItem={activeItem} />;
|
||||
});
|
||||
|
||||
const SidebarList: React.FC<{
|
||||
items: ISidebarList['items'];
|
||||
activeItem: SidebarProps['activeItem'];
|
||||
}> = React.memo(({ items, activeItem }) => {
|
||||
return (
|
||||
<div className="flex flex-col space-y-0.5">
|
||||
{items.map((item) => (
|
||||
<SidebarItem key={item.id} {...item} active={activeItem === item.id || item.active} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const isSidebarGroup = (content: SidebarProps['content'][number]): content is ISidebarGroup => {
|
||||
return 'label' in content && content.label !== undefined;
|
||||
};
|
|
@ -1,12 +1,12 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { SidebarGroup } from './SidebarCollapsible';
|
||||
import { SidebarCollapsible } from './SidebarCollapsible';
|
||||
//import { Home, Settings, User } from 'lucide-react';
|
||||
import { HouseModern, MapSettings, User } from '../icons/NucleoIconOutlined';
|
||||
import { BusterRoutes } from '@/routes';
|
||||
|
||||
const meta: Meta<typeof SidebarGroup> = {
|
||||
title: 'UI/Sidebar/SidebarGroup',
|
||||
component: SidebarGroup,
|
||||
const meta: Meta<typeof SidebarCollapsible> = {
|
||||
title: 'UI/Sidebar/SidebarCollapsible',
|
||||
component: SidebarCollapsible,
|
||||
parameters: {
|
||||
layout: 'centered'
|
||||
},
|
||||
|
@ -20,7 +20,7 @@ const meta: Meta<typeof SidebarGroup> = {
|
|||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof SidebarGroup>;
|
||||
type Story = StoryObj<typeof SidebarCollapsible>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
|
|
|
@ -19,15 +19,14 @@ const SidebarTrigger: React.FC<SidebarTriggerProps> = React.memo(({ label, isOpe
|
|||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-1 rounded px-1.5 py-1 text-sm transition-colors',
|
||||
'hover:text-text-default text-text-secondary',
|
||||
'text-text-secondary hover:bg-nav-item-hover',
|
||||
'group cursor-pointer'
|
||||
)}>
|
||||
<span className="">{label}</span>
|
||||
<div
|
||||
className={cn(
|
||||
'text-icon-color text-2xs -rotate-90 transition-transform duration-200',
|
||||
isOpen && 'rotate-0',
|
||||
'group-hover:text-text-default'
|
||||
isOpen && 'rotate-0'
|
||||
)}>
|
||||
<CaretDown />
|
||||
</div>
|
||||
|
@ -35,23 +34,27 @@ const SidebarTrigger: React.FC<SidebarTriggerProps> = React.memo(({ label, isOpe
|
|||
);
|
||||
});
|
||||
|
||||
export const SidebarGroup: React.FC<ISidebarGroup> = React.memo(({ label, items }) => {
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
export const SidebarCollapsible: React.FC<ISidebarGroup & { activeItem?: string }> = React.memo(
|
||||
({ label, items, activeItem, defaultOpen = true }) => {
|
||||
const [isOpen, setIsOpen] = React.useState(defaultOpen);
|
||||
|
||||
return (
|
||||
<Collapsible open={isOpen} onOpenChange={setIsOpen} className="space-y-0.5">
|
||||
<CollapsibleTrigger asChild className="w-full">
|
||||
<button className="w-full text-left">
|
||||
<SidebarTrigger label={label} isOpen={isOpen} />
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="data-[state=open]:animate-collapsible-down data-[state=closed]:animate-collapsible-up pl-0">
|
||||
<div className="space-y-0.5">
|
||||
{items.map((item) => (
|
||||
<SidebarItem key={item.id} {...item} />
|
||||
))}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<Collapsible open={isOpen} onOpenChange={setIsOpen} className="space-y-0.5">
|
||||
<CollapsibleTrigger asChild className="w-full">
|
||||
<button className="w-full text-left">
|
||||
<SidebarTrigger label={label} isOpen={isOpen} />
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="data-[state=open]:animate-collapsible-down data-[state=closed]:animate-collapsible-up pl-0">
|
||||
<div className="space-y-0.5">
|
||||
{items.map((item) => (
|
||||
<SidebarItem key={item.id} {...item} active={activeItem === item.id || item.active} />
|
||||
))}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
SidebarCollapsible.displayName = 'SidebarCollapsible';
|
||||
|
|
|
@ -9,11 +9,11 @@ const itemVariants = cva(
|
|||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'hover:bg-item-hover text-text-default',
|
||||
default: 'hover:bg-nav-item-hover text-text-default',
|
||||
emphasized: 'shadow bg-background border border-border text-text-default'
|
||||
},
|
||||
active: {
|
||||
true: '',
|
||||
true: 'cursor-default',
|
||||
false: ''
|
||||
},
|
||||
disabled: {
|
||||
|
|
|
@ -13,6 +13,7 @@ export interface ISidebarItem {
|
|||
export interface ISidebarGroup {
|
||||
label: string;
|
||||
items: ISidebarItem[];
|
||||
defaultOpen?: boolean; //will default to true
|
||||
}
|
||||
|
||||
export interface ISidebarList {
|
||||
|
|
Loading…
Reference in New Issue