import type { Meta, StoryObj } from '@storybook/react' import type React from 'react' import { expect, within } from 'storybook/test' import { SvgCoinsStack, SvgCompute, SvgCrossChain, SvgFileCode1, SvgGraphBarIncrease, SvgTaillessLineArrowDown1, SvgWrench, SvgUserCircleSingle, } from '@chainlink/blocks-icons' import { SvgBill4, SvgStartup, SvgDeployRules, SvgHelpQuestionCircle, SvgWorkflows, } from '../../../icons' import { Button } from '../../Button' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator, } from '../../DropdownMenu' import { Input } from '../../Input' import { Modal, ModalContent, ModalTrigger } from '../../Modal' import { OrgSwitcher } from '../../OrgSwitcher' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '../../Select' import { Sheet, SheetContent, SheetHeader, SheetFooter, SheetTrigger, } from '../../Sheet' import { Typography } from '../../Typography' import { CreIcon } from './CreIcon' import { SidebarProvider, Sidebar, SidebarHeader, SidebarContent, SidebarGroup, SidebarGroupLabel, SidebarGroupContent, SidebarMenu, SidebarMenuItem, SidebarMenuButton, SidebarFooter, SidebarTrigger, SidebarInset, SidebarSeparator, SidebarLogo, SidebarLinks, type SidebarNavItem, SidebarLogoIcon, SidebarLogoText, SidebarChainlinkLogo, } from './NavSidebar' type SidebarProps = React.ComponentProps const ChainlinkLogoText = () => ( ) const ChainlinkIcon = () => { return ( ) } export default { title: 'Navigation/Sidebar', component: Sidebar, argTypes: { side: { control: { type: 'select' }, options: ['left', 'right'], description: 'The side of the viewport where the sidebar is anchored.', }, variant: { control: { type: 'select' }, options: ['sidebar', 'floating', 'inset'], description: 'The visual variant of the sidebar: sidebar (default), floating (with shadow), or inset (with margin).', table: { type: { summary: 'sidebar | floating | inset' }, defaultValue: { summary: 'sidebar' }, }, }, collapsible: { control: { type: 'select' }, options: ['offcanvas', 'icon', 'none'], description: 'The collapse behavior: offcanvas (slides off), icon (collapses to icons), or none (always visible).', table: { type: { summary: 'offcanvas | icon | none' }, defaultValue: { summary: 'icon' }, }, }, }, args: { side: 'left', variant: 'sidebar', collapsible: 'icon', }, decorators: [ (Story) => (
), ], } satisfies Meta type Story = StoryObj const items: SidebarNavItem[][] = [ [ { title: 'Getting Started', icon: SvgStartup, href: '/getting-started', }, { title: 'Workflows', icon: SvgWorkflows, href: '/workflows' }, { title: 'Billing', icon: SvgBill4, items: [ { title: 'Overview', href: '/billing' }, { title: 'Consumption', href: 'www.consumption.com', external: true, }, ], }, { title: 'Deployment Access', icon: SvgDeployRules, href: '/deployment-access', }, { title: 'Help', icon: SvgHelpQuestionCircle, onClick: () => {}, }, ], [ { title: 'Data', icon: SvgGraphBarIncrease, href: 'https://data.chain.link/', external: true, }, { title: 'Cross-chain', icon: SvgCrossChain, href: 'https://ccip.chain.link/', external: true, }, ], ] export const Default: Story = { render: (_args) => { const menuItems = [ { title: 'CRE', icon: CreIcon, href: 'https://cre.chain.link', }, { title: 'Data', icon: SvgGraphBarIncrease, items: [ { title: 'Data Feeds', href: 'https://data.chain.link/feeds' }, { title: 'Data Streams', href: 'https://data.chain.link/streams' }, { title: 'Smart Data', href: 'https://data.chain.link/smartdata' }, { title: 'DataLink', href: 'https://data.chain.link/datalink' }, { title: 'CDY', href: 'https://cdy.chain.link' }, ], }, { title: 'Cross-chain', icon: SvgCrossChain, items: [ { title: 'Explorer', href: 'https://ccip.chain.link' }, { title: 'Lane Status', href: 'https://ccip.chain.link/status' }, { title: 'Token Manager', href: 'https://tokenmanager.chain.link' }, { title: 'Directory', href: 'https://docs.chain.link/ccip/directory', }, ], }, { title: 'Compute', icon: SvgCompute, items: [ { title: 'Automation', href: 'https://automation.chain.link' }, { title: 'Functions', href: 'https://functions.chain.link' }, { title: 'VRF', href: 'https://vrf.chain.link' }, ], }, { title: 'Tools', icon: SvgWrench, items: [ { title: 'Faucets', href: 'https://faucets.chain.link' }, { title: 'PegSwap', href: 'https://pegswap.chain.link' }, ], }, { title: 'Economics', icon: SvgCoinsStack, items: [ { title: 'Staking', href: 'https://staking.chain.link', }, { title: 'Rewards', href: 'https://rewards.chain.link' }, { title: 'Metrics', href: 'https://metrics.chain.link' }, { title: 'Reserve', href: 'https://metrics.chain.link/reserve', }, ], }, { title: 'Learn', icon: SvgFileCode1, items: [ { title: 'Docs', href: 'https://docs.chain.link' }, { title: 'DevHub', href: 'https://dev.chain.link' }, { title: 'Certification', href: 'https://dev.chain.link/certification', }, { title: 'Talk to Expert', href: 'https://chain.link/contact' }, ], }, ] return ( } > {menuItems.map((item) => ( ))}
Icon Sidebar with Submenus
This sidebar combines icon collapsible mode with expandable submenus. Hover over the sidebar to expand it and see the submenus. Click on menu items to toggle their submenus.
Test Components Test how these components behave with the sidebar hover functionality:
Select Component:
Modal Component:
Test Modal This is a test modal to see how it behaves with the sidebar hover functionality.
Sheet Component: Sheet Example
This is a simple sheet example showing the basic sheet structure.
Simple form example:
Name:
Email:
) }, } export const WithNavSidebarLinksAndChainlinkLogo: Story = { render: (_args) => { return ( } >
With NavSidebarLinks
This story demonstrates the SidebarLinks abstraction component that takes an array of arrays of navigation items and renders them with dividers between groups.
) }, } export const Offcanvas: Story = { render: (_args) => (
}> Application {items[0].map((item) => ( ))}
Offcanvas Sidebar
This is a basic sidebar example with offcanvas collapsible behavior.
), } export const OffcanvasWithHeaderAndFooter: Story = { render: (_args) => (
} > (
AC
)} >
Acme Corp Enterprise
Platform {items[0].map((item) => ( ))}
John Doe john@example.com
Account Billing Sign out
{/*
Offcanvas with Header & Footer
This sidebar includes a header with company branding and a footer with user account information.
*/} ), } export const Floating: Story = { render: (_args) => (
}> Platform {items[0].map((item) => ( ))}
Floating Icon Sidebar
This sidebar uses the floating variant with rounded corners and shadow.
), } export const WithSeparators: Story = { render: (_args) => (
}> Main {items[0].map((item) => ( ))} Settings {items[1].map((item) => ( ))}
Offcanvas with Separators
This sidebar uses separators to visually group related items.
), } export const NonCollapsible: Story = { render: (_args) => (
}> Non-Collapsible {items[0].map((item) => ( ))}
Non-Collapsible Sidebar
This sidebar cannot be collapsed and is always visible at full width.
), } // Custom nav items for specificity tests const specificityItems = [ [ { title: 'Billing', href: '/billing', icon: SvgCoinsStack, items: [ { title: 'Overview', href: '/billing' }, { title: 'Consumption', href: '/billing/consumption' }, { title: 'User', href: '/billing/consumption/user' }, ], }, ], ] export const ActiveStateSpecificityExact: Story = { render: () => (
} open={true} > Specificity: Exact Match Path: /billing/consumption
Expected Active: Consumption
), play: async ({ canvasElement }) => { const canvas = within(canvasElement) // "Consumption" should be active const activeLink = canvas.getByRole('link', { name: 'Consumption' }) await expect(activeLink).toHaveAttribute('data-active', 'true') // "User" should NOT be active const inactiveLink = canvas.getByRole('link', { name: 'User' }) await expect(inactiveLink).toHaveAttribute('data-active', 'false') }, } export const ActiveStateSpecificityDeep: Story = { render: () => (
} open={true} > Specificity: Deep Match Path: /billing/consumption/user
Expected Active: User
), play: async ({ canvasElement }) => { const canvas = within(canvasElement) // "User" should be active const activeLink = canvas.getByRole('link', { name: 'User' }) await expect(activeLink).toHaveAttribute('data-active', 'true') // "Consumption" should NOT be active const inactiveLink = canvas.getByRole('link', { name: 'Consumption' }) await expect(inactiveLink).toHaveAttribute('data-active', 'false') }, } export const ActiveStateSpecificityFallback: Story = { render: () => (
} open={true} > Specificity: Fallback to Parent Path: /billing/other
Expected Active: Overview
), play: async ({ canvasElement }) => { const canvas = within(canvasElement) // "Overview" (href=/billing) should be active because /billing/other matches /billing prefix // and no other item matches const activeLink = canvas.getByRole('link', { name: 'Overview' }) await expect(activeLink).toHaveAttribute('data-active', 'true') }, } export const ActiveStateBaseUrl: Story = { render: () => (
} open={true} > Base URL: Normalization Base URL: data.chain.link
Item URL: data.chain.link/feeds →{' '} /feeds
Path: /feeds
Expected Active: Data Feeds
), play: async ({ canvasElement }) => { const canvas = within(canvasElement) // "Data Feeds" should be active (normalized from https://data.chain.link/feeds to /feeds) const activeLink = canvas.getByRole('link', { name: 'Data Feeds' }) await expect(activeLink).toHaveAttribute('data-active', 'true') }, } export const ActiveStateCrossAppRoot: Story = { render: () => (
} open={true} > Cross App: Root Match Base URL: cdy.chain.link
Item URL: cdy.chain.link/
Path: /
Expected Active: CDY
), play: async ({ canvasElement }) => { const canvas = within(canvasElement) // "CDY" should be active (normalized from https://cdy.chain.link to /) const activeLink = canvas.getByRole('link', { name: 'CDY' }) await expect(activeLink).toHaveAttribute('data-active', 'true') }, } export const ActiveStateRobustness: Story = { render: () => (
} open={true} > Robustness: Query Params Path: /feeds/?q=1
Expected Active: Data Feeds
), play: async ({ canvasElement }) => { const canvas = within(canvasElement) // "Data Feeds" should be active despite query params const activeLink = canvas.getByRole('link', { name: 'Data Feeds' }) await expect(activeLink).toHaveAttribute('data-active', 'true') }, } export const ActiveStateDeepNestedPath: Story = { render: () => (
} open={true} > Deep Nested Path Path: /feeds/ethereum/mainnet/btc-usd
Expected Active: Data Feeds (Submenu) &{' '} Data (Parent)
), play: async ({ canvasElement }) => { const canvas = within(canvasElement) // "Data Feeds" should be active const activeLink = canvas.getByRole('link', { name: 'Data Feeds' }) await expect(activeLink).toHaveAttribute('data-active', 'true') // Parent "Data" should also be active const parentButton = canvas.getByRole('button', { name: 'Data' }) await expect(parentButton).toHaveAttribute('data-active', 'true') }, } export const ActiveStateParentFallbackSameDomain: Story = { render: () => (
} > Base URL: Parent Fallback Base URL: data.chain.link
Path: /path-without-submenu
Expected Active: Data (Parent)
), play: async ({ canvasElement }) => { const canvas = within(canvasElement) // "Data" parent should be active (matches / prefix from https://data.chain.link normalized) const parentButton = canvas.getByRole('button', { name: 'Data' }) await expect(parentButton).toHaveAttribute('data-active', 'true') // Submenu items should NOT be active const inactiveLink = canvas.getByRole('link', { name: 'Data Feeds' }) await expect(inactiveLink).toHaveAttribute('data-active', 'false') }, }