Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
}
},
];

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@internxt/ui",
"version": "0.1.4",
"version": "0.1.5",
"description": "Library of Internxt components",
"repository": {
"type": "git",
Expand Down
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ export * from './switch';
export * from './table/Table';
export * from './textArea';
export * from './tooltip';
export * from './sidenav';
6 changes: 4 additions & 2 deletions src/components/popover/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface PopoverProps {
panel: (closePopover: () => void) => ReactNode;
className?: string;
classButton?: string;
align?: 'left' | 'right';
}

/**
Expand All @@ -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<HTMLDivElement | null>(null);
const [showContent, setShowContent] = useState(isOpen);
Expand Down Expand Up @@ -87,7 +88,8 @@ const Popover = ({ childrenButton, panel, className, classButton }: PopoverProps
<div
ref={panelRef}
className={
'absolute right-0 z-50 mt-1 origin-top-right transform rounded-md border border-gray-10 ' +
'absolute z-50 mt-1 transform rounded-md border border-gray-10 ' +
`${align === 'left' ? 'left-0 origin-top-left' : 'right-0 origin-top-right'} ` +
`bg-surface py-1.5 shadow-subtle duration-100 ease-out dark:bg-gray-5 ${transitionOpacity} ${transitionScale}`
}
>
Expand Down
108 changes: 108 additions & 0 deletions src/components/sidenav/Sidenav.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
className={`relative flex flex-col p-2 h-full justify-between bg-gray-1 border-r border-gray-10 transition-all duration-300 group ${
isCollapsed ? 'w-16' : 'w-64'
}`}
>
<div className="flex flex-col">
<SidenavHeader
logo={header.logo}
title={header.title}
onClick={header.onClick}
isCollapsed={isCollapsed}
onToggleCollapse={onToggleCollapse}
suiteLauncher={suiteLauncher}
className={header.className}
/>

<div className="flex flex-col gap-4">
{isCollapsed ? collapsedPrimaryAction : primaryAction}
<SidenavOptions options={options} isCollapsed={isCollapsed} showSubsections={showSubsections} />
</div>
</div>

{!isCollapsed && storage && (
<SidenavStorage
usage={storage.usage}
limit={storage.limit}
percentage={storage.percentage}
onUpgradeClick={storage.onUpgradeClick}
upgradeLabel={storage.upgradeLabel}
isLoading={storage.isLoading}
/>
)}
</div>
);
};

export default Sidenav;
85 changes: 85 additions & 0 deletions src/components/sidenav/SidenavHeader.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
className={`flex flex-row justify-between w-full py-5 px-2 ${className} ${isCollapsed ? 'justify-center' : ''}`}
>
{isCollapsed ? (
<div className="relative flex items-center justify-center w-full">
<button className="flex flex-row gap-2 items-center" onClick={onClick}>
<img src={logo} width={28} alt={title} className="group-hover:hidden" />
{onToggleCollapse && (
<button
onClick={(e) => {
e.stopPropagation();
onToggleCollapse();
}}
className="hidden group-hover:flex items-center justify-center text-gray-50"
>
<SidebarIcon size={28} />
</button>
)}
</button>
</div>
) : (
<>
<button className="flex flex-row gap-2 items-center" onClick={onClick}>
<img src={logo} width={28} alt={title} />
<p className="text-xl font-medium text-gray-100">{title}</p>
</button>
<div className="flex flex-row gap-2 items-center">
{suiteLauncher && (
<SuiteLauncher
suiteArray={suiteLauncher?.suiteArray}
soonText={suiteLauncher?.soonText}
className={suiteLauncher?.className}
align="left"
/>
)}
{onToggleCollapse && (
<button
onClick={onToggleCollapse}
className="flex items-center justify-center text-gray-50 hover:text-gray-70"
>
<SidebarIcon size={28} />
</button>
)}
</div>
</>
)}
</div>
);
};

export default SidenavHeader;
52 changes: 52 additions & 0 deletions src/components/sidenav/SidenavItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { IconProps } from '@phosphor-icons/react';

interface SidenavItemProps {
label: string;
notifications?: number;
Icon: React.ForwardRefExoticComponent<IconProps & React.RefAttributes<SVGSVGElement>>;
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 (
<button
onClick={onClick}
data-cy={iconDataCy}
className={`flex w-full flex-col focus-visible:bg-gray-10 rounded-lg ${
isActive ? 'bg-primary/20' : 'hover:bg-gray-5'
} ${subsection ? 'pl-5' : ''}`}
title={isCollapsed ? label : undefined}
>
<div
className={`flex flex-row px-2.5 py-2 w-full items-center ${isCollapsed ? 'justify-center' : 'justify-between'}`}
>
<div className={`flex flex-row gap-3 items-center ${isActive ? 'text-primary' : 'text-gray-80'}`}>
<Icon size={20} weight={isActive ? 'fill' : 'regular'} />
{!isCollapsed && <p className="font-medium">{label}</p>}
</div>
{!isCollapsed && notifications && (
<div
className={`flex rounded-full px-2 py-1 ${isActive ? 'text-white bg-primary' : 'bg-gray-10 text-gray-60'}`}
>
<p className="text-xs font-medium">{notifications}</p>
</div>
)}
</div>
</button>
);
};

export default SidenavItem;
53 changes: 53 additions & 0 deletions src/components/sidenav/SidenavOptions.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex flex-col w-full">
{options
.filter((option) => option.isVisible)
.map((option, index) => {
if (option.subsection && !showSubsections) {
return null;
}

if (isCollapsed && option.subsection) {
return null;
}

return (
<SidenavItem
key={`${option.iconDataCy}-${index}`}
label={option.label}
Icon={option.icon}
iconDataCy={option.iconDataCy}
isActive={option.isActive}
notifications={option.notifications}
onClick={option.onClick}
isCollapsed={isCollapsed}
subsection={option.subsection}
/>
);
})}
</div>
);
};

export default SidenavOptions;
Loading