diff --git a/eslint.config.mjs b/eslint.config.mjs index 909af66..bb7ced0 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -7,13 +7,13 @@ export default [ ...eslintConfigInternxt, { languageOptions: { - parser: typescriptParser + parser: typescriptParser, }, rules: { '@typescript-eslint/no-explicit-any': 'warn', 'react/react-in-jsx-scope': 'off', + 'max-len': 'off', }, ignores: ['dist', 'tmp', 'scripts', 'node_modules', '!.storybook'], - } + }, ]; - diff --git a/package.json b/package.json index 503d3a9..7fb537c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@internxt/ui", - "version": "0.1.4", + "version": "0.1.5", "description": "Library of Internxt components", "repository": { "type": "git", diff --git a/src/components/index.ts b/src/components/index.ts index 0a49e0b..f68b021 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -27,3 +27,4 @@ export * from './switch'; export * from './table/Table'; export * from './textArea'; export * from './tooltip'; +export * from './sidenav'; diff --git a/src/components/popover/Popover.tsx b/src/components/popover/Popover.tsx index 8651d49..c9879a8 100644 --- a/src/components/popover/Popover.tsx +++ b/src/components/popover/Popover.tsx @@ -6,6 +6,7 @@ export interface PopoverProps { panel: (closePopover: () => void) => ReactNode; className?: string; classButton?: string; + align?: 'left' | 'right'; } /** @@ -29,7 +30,7 @@ export interface PopoverProps { * - The rendered Popover component. */ -const Popover = ({ childrenButton, panel, className, classButton }: PopoverProps): JSX.Element => { +const Popover = ({ childrenButton, panel, className, classButton, align = 'right' }: PopoverProps): JSX.Element => { const [isOpen, setIsOpen] = useState(false); const panelRef = useRef(null); const [showContent, setShowContent] = useState(isOpen); @@ -87,7 +88,8 @@ const Popover = ({ childrenButton, panel, className, classButton }: PopoverProps
diff --git a/src/components/sidenav/Sidenav.tsx b/src/components/sidenav/Sidenav.tsx new file mode 100644 index 0000000..a6e9bd2 --- /dev/null +++ b/src/components/sidenav/Sidenav.tsx @@ -0,0 +1,108 @@ +import { ReactNode } from 'react'; +import SidenavOptions, { SidenavOption } from './SidenavOptions'; +import SidenavHeader from './SidenavHeader'; +import SidenavStorage from './SidenavStorage'; + +export interface SidenavHeader { + logo: string; + title: string; + onClick: () => void; + className?: string; +} + +export interface SidenavStorage { + usage: string; + limit: string; + percentage: number; + onUpgradeClick: () => void; + upgradeLabel?: string; + isLoading?: boolean; +} + +export interface SidenavProps { + header: SidenavHeader; + primaryAction?: ReactNode; + suiteLauncher?: { + className?: string; + suiteArray: { + icon: JSX.Element; + title: string; + onClick: () => void; + isMain?: boolean; + availableSoon?: boolean; + isLocked?: boolean; + }[]; + soonText: string; + }; + collapsedPrimaryAction?: ReactNode; + options: SidenavOption[]; + showSubsections?: boolean; + isCollapsed?: boolean; + storage?: SidenavStorage; + onToggleCollapse?: () => void; +} + +/** + * Sidenav component + * + * A custom sidenav component that provides a sidebar with options for navigation and interaction. + * + * @property {SidenavHeader} header - Header configuration with logo, title, and onClick handler + * @property {ReactNode} primaryAction - The primary action displayed at the top of the sidenav + * @property {object} suiteLauncher - The suite launcher configuration + * @property {ReactNode} collapsedPrimaryAction - The primary action displayed when the sidenav is collapsed + * @property {SidenavOption[]} options - An array of options to be displayed in the sidenav. Each option can specify an 'as' prop to use a custom component (e.g., NavLink) + * @property {boolean} showSubsections - Determines whether to display the subsections of the sidenav + * @property {boolean} isCollapsed - Determines whether the sidenav is collapsed or not + * @property {SidenavStorage} storage - The storage information displayed at the bottom of the sidenav + * @property {() => void} onToggleCollapse - A callback function triggered when the collapse button is clicked + */ +const Sidenav = ({ + header, + primaryAction, + suiteLauncher, + collapsedPrimaryAction, + options, + showSubsections, + isCollapsed = false, + storage, + onToggleCollapse, +}: SidenavProps) => { + return ( +
+
+ + +
+ {isCollapsed ? collapsedPrimaryAction : primaryAction} + +
+
+ + {!isCollapsed && storage && ( + + )} +
+ ); +}; + +export default Sidenav; diff --git a/src/components/sidenav/SidenavHeader.tsx b/src/components/sidenav/SidenavHeader.tsx new file mode 100644 index 0000000..b988524 --- /dev/null +++ b/src/components/sidenav/SidenavHeader.tsx @@ -0,0 +1,85 @@ +import { SidebarIcon } from '@phosphor-icons/react'; +import { SuiteLauncher } from '../suiteLauncher'; + +interface SidenavHeaderProps { + logo: string; + title: string; + onClick: () => void; + isCollapsed: boolean; + className?: string; + onToggleCollapse?: () => void; + suiteLauncher?: { + className?: string; + suiteArray: { + icon: JSX.Element; + title: string; + onClick: () => void; + isMain?: boolean; + availableSoon?: boolean; + isLocked?: boolean; + }[]; + soonText: string; + }; +} + +const SidenavHeader = ({ + logo, + title, + onClick, + isCollapsed, + className, + onToggleCollapse, + suiteLauncher, +}: SidenavHeaderProps): JSX.Element => { + return ( +
+ {isCollapsed ? ( +
+ + )} + +
+ ) : ( + <> + +
+ {suiteLauncher && ( + + )} + {onToggleCollapse && ( + + )} +
+ + )} +
+ ); +}; + +export default SidenavHeader; diff --git a/src/components/sidenav/SidenavItem.tsx b/src/components/sidenav/SidenavItem.tsx new file mode 100644 index 0000000..0e380f6 --- /dev/null +++ b/src/components/sidenav/SidenavItem.tsx @@ -0,0 +1,52 @@ +import { IconProps } from '@phosphor-icons/react'; + +interface SidenavItemProps { + label: string; + notifications?: number; + Icon: React.ForwardRefExoticComponent>; + onClick?: () => void; + iconDataCy?: string; + isActive?: boolean; + isCollapsed?: boolean; + subsection?: boolean; +} + +const SidenavItem = ({ + label, + Icon, + onClick, + notifications, + iconDataCy, + isActive = false, + isCollapsed = false, + subsection = false, +}: SidenavItemProps): JSX.Element => { + return ( + + ); +}; + +export default SidenavItem; diff --git a/src/components/sidenav/SidenavOptions.tsx b/src/components/sidenav/SidenavOptions.tsx new file mode 100644 index 0000000..5296487 --- /dev/null +++ b/src/components/sidenav/SidenavOptions.tsx @@ -0,0 +1,53 @@ +import { Icon } from '@phosphor-icons/react'; +import SidenavItem from './SidenavItem'; + +export interface SidenavOption { + label: string; + icon: Icon; + iconDataCy: string; + isVisible: boolean; + isActive?: boolean; + notifications?: number; + onClick?: () => void; + subsection?: boolean; +} + +interface SidenavOptionsProps { + options: SidenavOption[]; + isCollapsed: boolean; + showSubsections?: boolean; +} + +const SidenavOptions = ({ options, isCollapsed, showSubsections }: SidenavOptionsProps): JSX.Element => { + return ( +
+ {options + .filter((option) => option.isVisible) + .map((option, index) => { + if (option.subsection && !showSubsections) { + return null; + } + + if (isCollapsed && option.subsection) { + return null; + } + + return ( + + ); + })} +
+ ); +}; + +export default SidenavOptions; diff --git a/src/components/sidenav/SidenavStorage.tsx b/src/components/sidenav/SidenavStorage.tsx new file mode 100644 index 0000000..94a8c00 --- /dev/null +++ b/src/components/sidenav/SidenavStorage.tsx @@ -0,0 +1,54 @@ +interface SidenavStorageProps { + usage: string; + limit: string; + percentage: number; + onUpgradeClick: () => void; + upgradeLabel?: string; + isLoading?: boolean; +} + +const SidenavStorage = ({ + usage, + limit, + percentage, + onUpgradeClick, + upgradeLabel, + isLoading = true, +}: SidenavStorageProps): JSX.Element => { + return ( +
+
+
+ {isLoading ? ( +
+
+
+
+
+ ) : ( + <> +

{usage}

+

/

+

{limit}

+ + )} +
+ {upgradeLabel && ( + + )} +
+
+
+
+
+ ); +}; + +export default SidenavStorage; diff --git a/src/components/sidenav/__test__/Sidenav.test.tsx b/src/components/sidenav/__test__/Sidenav.test.tsx new file mode 100644 index 0000000..9d90136 --- /dev/null +++ b/src/components/sidenav/__test__/Sidenav.test.tsx @@ -0,0 +1,284 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import '@testing-library/jest-dom'; +import Sidenav, { SidenavProps } from '../Sidenav'; +import { SidenavOption } from '../SidenavOptions'; + +const MockIcon = React.forwardRef(({ size = 20 }, ref) => ( + +)); + +describe('Sidenav Component', () => { + const onHeaderClick = vi.fn(); + const onOptionClick = vi.fn(); + const onToggleCollapse = vi.fn(); + const onUpgradeClick = vi.fn(); + + const mockOptions: SidenavOption[] = [ + { label: 'Inbox', icon: MockIcon, iconDataCy: 'inbox', isVisible: true, notifications: 5, onClick: onOptionClick }, + { label: 'Sent', icon: MockIcon, iconDataCy: 'sent', isVisible: true, onClick: onOptionClick }, + { + label: 'Drafts', + icon: MockIcon, + iconDataCy: 'drafts', + isVisible: true, + notifications: 2, + onClick: onOptionClick, + }, + { label: 'Labels', icon: MockIcon, iconDataCy: 'labels', isVisible: true, onClick: onOptionClick }, + { + label: 'Important', + icon: MockIcon, + iconDataCy: 'important', + isVisible: true, + subsection: true, + onClick: onOptionClick, + }, + { label: 'Work', icon: MockIcon, iconDataCy: 'work', isVisible: true, subsection: true, onClick: onOptionClick }, + ]; + + const defaultProps: SidenavProps = { + header: { + logo: 'https://example.com/logo.png', + title: 'Mail', + onClick: onHeaderClick, + }, + options: mockOptions, + showSubsections: false, + }; + + const renderSidenav = (props: Partial = {}) => render(); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should match snapshot', () => { + const { container } = renderSidenav(); + expect(container).toMatchSnapshot(); + }); + + it('should match snapshot when collapsed', () => { + const { container } = renderSidenav({ isCollapsed: true, onToggleCollapse }); + expect(container).toMatchSnapshot(); + }); + + it('should match snapshot with storage', () => { + const { container } = renderSidenav({ + storage: { + usage: '2.8 GB', + limit: '4 GB', + percentage: 70, + upgradeLabel: 'Upgrade', + onUpgradeClick, + isLoading: false, + }, + }); + expect(container).toMatchSnapshot(); + }); + + it('should match snapshot with primary action', () => { + const { container } = renderSidenav({ + primaryAction: , + }); + expect(container).toMatchSnapshot(); + }); + + it('renders header with logo and title', () => { + const { getByAltText, getByText } = renderSidenav(); + expect(getByAltText('Mail')).toBeInTheDocument(); + expect(getByText('Mail')).toBeInTheDocument(); + }); + + it('renders all non-subsection options', () => { + const { getByText, queryByText } = renderSidenav(); + expect(getByText('Inbox')).toBeInTheDocument(); + expect(getByText('Sent')).toBeInTheDocument(); + expect(getByText('Drafts')).toBeInTheDocument(); + expect(getByText('Labels')).toBeInTheDocument(); + expect(queryByText('Important')).not.toBeInTheDocument(); + expect(queryByText('Work')).not.toBeInTheDocument(); + }); + + it('renders subsections when showSubsections is true', () => { + const { getByText } = renderSidenav({ showSubsections: true }); + expect(getByText('Important')).toBeInTheDocument(); + expect(getByText('Work')).toBeInTheDocument(); + }); + + it('renders notifications badge', () => { + const { getByText } = renderSidenav(); + expect(getByText('5')).toBeInTheDocument(); + expect(getByText('2')).toBeInTheDocument(); + }); + + it('calls onClick when option is clicked', () => { + const { getByText } = renderSidenav(); + fireEvent.click(getByText('Inbox')); + expect(onOptionClick).toHaveBeenCalled(); + }); + + it('calls onClick for subsection items', () => { + const { getByText } = renderSidenav({ showSubsections: true }); + fireEvent.click(getByText('Important')); + expect(onOptionClick).toHaveBeenCalled(); + }); + + it('calls header onClick when logo is clicked', () => { + const { getByAltText } = renderSidenav(); + fireEvent.click(getByAltText('Mail')); + expect(onHeaderClick).toHaveBeenCalled(); + }); + + it('renders primary action when provided', () => { + const { getByTestId } = renderSidenav({ + primaryAction: , + }); + expect(getByTestId('primary-action')).toBeInTheDocument(); + }); + + it('renders storage information when provided', () => { + const { getByText } = renderSidenav({ + storage: { + usage: '2.8 GB', + limit: '4 GB', + percentage: 70, + upgradeLabel: 'Upgrade', + onUpgradeClick, + isLoading: false, + }, + }); + expect(getByText('2.8 GB')).toBeInTheDocument(); + expect(getByText('4 GB')).toBeInTheDocument(); + expect(getByText('Upgrade')).toBeInTheDocument(); + }); + + it('calls onUpgradeClick when upgrade button is clicked', () => { + const { getByText } = renderSidenav({ + storage: { + usage: '2.8 GB', + limit: '4 GB', + percentage: 70, + upgradeLabel: 'Upgrade', + onUpgradeClick, + isLoading: false, + }, + }); + fireEvent.click(getByText('Upgrade')); + expect(onUpgradeClick).toHaveBeenCalled(); + }); + + it('applies active styles to selected option', () => { + const activeOptions = mockOptions.map((opt, idx) => ({ ...opt, isActive: idx === 0 })); + const { getByText } = renderSidenav({ options: activeOptions }); + const inboxButton = getByText('Inbox').closest('button'); + expect(inboxButton).toHaveClass('bg-primary/20'); + }); + + it('shows loading skeleton when storage is loading', () => { + const { container, queryByText } = renderSidenav({ + storage: { + usage: '2.8 GB', + limit: '4 GB', + percentage: 70, + upgradeLabel: 'Upgrade', + onUpgradeClick, + isLoading: true, + }, + }); + + expect(queryByText('2.8 GB')).not.toBeInTheDocument(); + expect(queryByText('4 GB')).not.toBeInTheDocument(); + + const skeletons = container.querySelectorAll('.animate-pulse'); + expect(skeletons.length).toBeGreaterThan(0); + }); + + describe('Collapsed state', () => { + it('hides title when collapsed', () => { + const { queryByText } = renderSidenav({ isCollapsed: true }); + expect(queryByText('Mail')).not.toBeInTheDocument(); + }); + + it('hides option titles when collapsed', () => { + const { queryByText } = renderSidenav({ isCollapsed: true }); + expect(queryByText('Inbox')).not.toBeInTheDocument(); + expect(queryByText('Sent')).not.toBeInTheDocument(); + }); + + it('hides notifications when collapsed', () => { + const { queryByText } = renderSidenav({ isCollapsed: true }); + expect(queryByText('5')).not.toBeInTheDocument(); + }); + + it('hides storage when collapsed', () => { + const { queryByText } = renderSidenav({ + isCollapsed: true, + storage: { + usage: '2.8 GB', + limit: '4 GB', + percentage: 70, + upgradeLabel: 'Upgrade', + onUpgradeClick, + isLoading: false, + }, + }); + expect(queryByText('2.8 GB')).not.toBeInTheDocument(); + expect(queryByText('Upgrade')).not.toBeInTheDocument(); + }); + + it('hides subsections when collapsed even if showSubsections is true', () => { + const { queryByText } = renderSidenav({ + isCollapsed: true, + showSubsections: true, + }); + expect(queryByText('Important')).not.toBeInTheDocument(); + expect(queryByText('Work')).not.toBeInTheDocument(); + }); + + it('shows collapsed primary action when collapsed', () => { + const { getByTestId, queryByTestId } = renderSidenav({ + isCollapsed: true, + primaryAction: , + collapsedPrimaryAction: , + }); + expect(queryByTestId('primary-action')).not.toBeInTheDocument(); + expect(getByTestId('collapsed-primary-action')).toBeInTheDocument(); + }); + + it('adds title attribute to options when collapsed for tooltip', () => { + const { container } = renderSidenav({ isCollapsed: true }); + const optionButtons = container.querySelectorAll('button[title="Inbox"]'); + expect(optionButtons.length).toBeGreaterThan(0); + }); + }); + + describe('Collapse toggle button', () => { + it('does not render collapse button when onToggleCollapse is not provided', () => { + const { container } = renderSidenav(); + + const buttons = container.querySelectorAll('button'); + + expect(buttons.length).toBe(5); + }); + + it('renders collapse button when onToggleCollapse is provided', () => { + const { container } = renderSidenav({ onToggleCollapse }); + const buttons = container.querySelectorAll('button'); + + expect(buttons.length).toBeGreaterThan(5); + }); + + it('calls onToggleCollapse when collapse button is clicked', () => { + const { container } = renderSidenav({ onToggleCollapse }); + + const collapseButton = Array.from(container.querySelectorAll('button')).find( + (btn) => btn.querySelector('svg') && !btn.querySelector('img'), + ); + expect(collapseButton).toBeDefined(); + fireEvent.click(collapseButton!); + expect(onToggleCollapse).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/components/sidenav/__test__/__snapshots__/Sidenav.test.tsx.snap b/src/components/sidenav/__test__/__snapshots__/Sidenav.test.tsx.snap new file mode 100644 index 0000000..8369c0c --- /dev/null +++ b/src/components/sidenav/__test__/__snapshots__/Sidenav.test.tsx.snap @@ -0,0 +1,632 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Sidenav Component > should match snapshot 1`] = ` +
+
+
+
+ +
+
+
+
+ + + + +
+
+
+
+
+`; + +exports[`Sidenav Component > should match snapshot when collapsed 1`] = ` +
+
+
+
+
+ + +
+
+
+
+ + + + +
+
+
+
+
+`; + +exports[`Sidenav Component > should match snapshot with primary action 1`] = ` +
+
+
+
+ +
+
+
+ +
+ + + + +
+
+
+
+
+`; + +exports[`Sidenav Component > should match snapshot with storage 1`] = ` +
+
+
+
+ +
+
+
+
+ + + + +
+
+
+
+
+
+

+ 2.8 GB +

+

+ / +

+

+ 4 GB +

+
+ +
+
+
+
+
+
+
+`; diff --git a/src/components/sidenav/index.ts b/src/components/sidenav/index.ts new file mode 100644 index 0000000..47479fc --- /dev/null +++ b/src/components/sidenav/index.ts @@ -0,0 +1,4 @@ +export { default as Sidenav } from './Sidenav'; +export { default as SidenavItem } from './SidenavItem'; +export type { SidenavOption } from './SidenavOptions'; +export type { SidenavHeader, SidenavProps, SidenavStorage } from './Sidenav'; diff --git a/src/components/suiteLauncher/SuiteLauncher.tsx b/src/components/suiteLauncher/SuiteLauncher.tsx index e0f5a75..22e84aa 100644 --- a/src/components/suiteLauncher/SuiteLauncher.tsx +++ b/src/components/suiteLauncher/SuiteLauncher.tsx @@ -1,4 +1,4 @@ -import { DotsNine, Lock } from '@phosphor-icons/react'; +import { DotsNineIcon, LockIcon } from '@phosphor-icons/react'; import { cloneElement, isValidElement } from 'react'; import { Popover } from '../popover'; @@ -13,6 +13,7 @@ export interface SuiteLauncherProps { isLocked?: boolean; }[]; soonText?: string; + align?: 'left' | 'right'; } /** @@ -37,10 +38,11 @@ export default function SuiteLauncher({ className = '', suiteArray, soonText, + align = 'right', }: Readonly): JSX.Element { const SuiteButton = ( -
- +
+
); @@ -54,7 +56,7 @@ export default function SuiteLauncher({
{suiteApp.isLocked ? ( - + ) : isValidElement(suiteApp.icon as JSX.Element) ? ( cloneElement(suiteApp.icon as JSX.Element, { size: 26, @@ -102,6 +109,12 @@ export default function SuiteLauncher({ ); return ( - panel} data-testid="app-suite-dropdown" /> + panel} + align={align} + data-testid="app-suite-dropdown" + /> ); } diff --git a/src/components/suiteLauncher/__test__/__snapshots__/SuiteLauncher.test.tsx.snap b/src/components/suiteLauncher/__test__/__snapshots__/SuiteLauncher.test.tsx.snap index ddd6c33..c62320f 100644 --- a/src/components/suiteLauncher/__test__/__snapshots__/SuiteLauncher.test.tsx.snap +++ b/src/components/suiteLauncher/__test__/__snapshots__/SuiteLauncher.test.tsx.snap @@ -15,7 +15,7 @@ exports[`SuiteLauncher > should match snapshot 1`] = ` data-testid="popover-button" >
should match snapshot 1`] = ` data-testid="popover-button" >
, + title: 'Drive', + onClick: () => console.log('Drive clicked'), + }, + { + icon: , + title: 'Mail', + onClick: () => console.log('Mail clicked'), + isMain: true, + }, + { + icon: , + title: 'VPN', + onClick: () => console.log('VPN clicked'), + availableSoon: true, + }, + { + icon: , + title: 'Pass', + onClick: () => console.log('Pass clicked'), + isLocked: true, + }, +]; + +const meta: Meta = { + title: 'Components/Sidenav', + component: Sidenav, + parameters: { + layout: 'fullscreen', + }, + tags: ['autodocs'], + argTypes: { + onToggleCollapse: { action: 'toggleCollapse' }, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +export default meta; +type Story = StoryObj; + +const MAIL_OPTIONS = [ + { + label: 'Inbox', + icon: TrayIcon, + iconDataCy: 'sidenav-inbox', + isVisible: true, + notifications: 12, + }, + { + label: 'Sent', + icon: PaperPlaneTiltIcon, + iconDataCy: 'sidenav-sent', + isVisible: true, + }, + { + label: 'Drafts', + icon: FileIcon, + iconDataCy: 'sidenav-drafts', + isVisible: true, + notifications: 3, + }, + { + label: 'Trash', + icon: TrashIcon, + iconDataCy: 'sidenav-trash', + isVisible: true, + }, + { + label: 'Labels', + icon: CaretDownIcon, + iconDataCy: 'sidenav-labels', + isVisible: true, + }, + { + label: 'Important', + icon: StarIcon, + iconDataCy: 'sidenav-important', + isVisible: true, + subsection: true, + }, + { + label: 'Work', + icon: TagIcon, + iconDataCy: 'sidenav-work', + isVisible: true, + subsection: true, + }, + { + label: 'Personal', + icon: TagIcon, + iconDataCy: 'sidenav-personal', + isVisible: true, + subsection: true, + }, +]; + +const InteractiveSidenav = (args: SidenavProps) => { + const [{ showSubsections, isCollapsed, options }, setArgs] = useArgs(); + + const handleOptionClick = (optionIndex: number, isSubsection: boolean) => { + const updatedOptions = options.map((opt: any, idx: number) => ({ + ...opt, + isActive: idx === optionIndex, + })); + + if (isSubsection) { + setArgs({ options: updatedOptions }); + return; + } + + // If clicking "Labels" option (index 4), toggle subsections + if (optionIndex === 4) { + setArgs({ options: updatedOptions, showSubsections: !showSubsections }); + } else { + setArgs({ options: updatedOptions, showSubsections: false }); + } + }; + + const handleToggleCollapse = args.onToggleCollapse + ? () => { + setArgs({ isCollapsed: !isCollapsed }); + args.onToggleCollapse?.(); + } + : undefined; + + const optionsWithHandlers = options.map((option: any, index: number) => ({ + ...option, + onClick: () => handleOptionClick(index, !!option.subsection), + })); + + return ( + + ); +}; + +export const Default: Story = { + render: InteractiveSidenav, + args: { + header: { + logo: 'https://internxt.com/favicon.ico', + title: 'Mail', + onClick: () => console.log('Header clicked'), + }, + primaryAction: ( + + ), + collapsedPrimaryAction: ( + + ), + suiteLauncher: { + suiteArray: SUITE_ARRAY, + soonText: 'Soon', + }, + options: MAIL_OPTIONS.map((opt, idx) => ({ ...opt, isActive: idx === 0 })), + showSubsections: false, + isCollapsed: false, + storage: { + usage: '2.8 GB', + limit: '4 GB', + percentage: 70, + upgradeLabel: 'Upgrade', + onUpgradeClick: () => console.log('Upgrade clicked'), + isLoading: false, + }, + onToggleCollapse: () => {}, + }, +}; + +export const Collapsed: Story = { + render: InteractiveSidenav, + args: { + ...Default.args, + isCollapsed: true, + }, +}; + +export const WithSubsectionsExpanded: Story = { + render: InteractiveSidenav, + args: { + ...Default.args, + options: MAIL_OPTIONS.map((opt, idx) => ({ ...opt, isActive: idx === 4 })), + showSubsections: true, + }, +}; + +export const WithoutPrimaryAction: Story = { + render: InteractiveSidenav, + args: { + ...Default.args, + primaryAction: undefined, + collapsedPrimaryAction: undefined, + }, +}; + +export const WithoutStorage: Story = { + render: InteractiveSidenav, + args: { + ...Default.args, + storage: undefined, + }, +}; + +export const HighStorageUsage: Story = { + render: InteractiveSidenav, + args: { + ...Default.args, + storage: { + usage: '9.5 GB', + limit: '10 GB', + percentage: 95, + upgradeLabel: 'Upgrade now', + onUpgradeClick: () => console.log('Upgrade clicked'), + isLoading: false, + }, + }, +}; + +export const Minimal: Story = { + render: InteractiveSidenav, + args: { + header: { + logo: 'https://internxt.com/favicon.ico', + title: 'Drive', + onClick: () => console.log('Header clicked'), + }, + options: [ + { + label: 'All files', + icon: FileIcon, + iconDataCy: 'sidenav-all-files', + isVisible: true, + isActive: true, + }, + { + label: 'Trash', + icon: TrashIcon, + iconDataCy: 'sidenav-trash-minimal', + isVisible: true, + isActive: false, + }, + ], + showSubsections: false, + isCollapsed: false, + onToggleCollapse: () => {}, + }, +}; + +export const WithoutCollapseButton: Story = { + render: InteractiveSidenav, + args: { + ...Default.args, + onToggleCollapse: undefined, + }, +}; + +export const LoadingStorage: Story = { + render: InteractiveSidenav, + args: { + ...Default.args, + storage: { + usage: '0 GB', + limit: '0 GB', + percentage: 0, + upgradeLabel: 'Upgrade', + onUpgradeClick: () => console.log('Upgrade clicked'), + isLoading: true, + }, + }, +}; diff --git a/tailwind.config.ts b/tailwind.config.ts index 459c576..4161036 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -2,4 +2,4 @@ import config from '@internxt/css-config'; export default { ...config, -} \ No newline at end of file +};