feat: new tab list (#12384)

closes #9904

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
nitin
2025-06-06 00:14:21 +05:30
committed by GitHub
parent a86b5fb9b2
commit 6f156a69b0
51 changed files with 1136 additions and 439 deletions

View File

@ -0,0 +1,122 @@
import styled from '@emotion/styled';
import { Pill } from '@ui/components/Pill/Pill';
import { Avatar, IconComponent } from '@ui/display';
import { ThemeContext } from '@ui/theme';
import { ReactElement, useContext } from 'react';
import { Link } from 'react-router-dom';
const StyledTabButton = styled.button<{
active?: boolean;
disabled?: boolean;
to?: string;
}>`
all: unset;
align-items: center;
color: ${({ theme, active, disabled }) =>
active
? theme.font.color.primary
: disabled
? theme.font.color.light
: theme.font.color.secondary};
cursor: pointer;
background-color: transparent;
border: none;
font-family: inherit;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
justify-content: center;
pointer-events: ${({ disabled }) => (disabled ? 'none' : '')};
text-decoration: none;
position: relative;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 1px;
background-color: ${({ theme, active }) =>
active ? theme.border.color.inverted : 'transparent'};
z-index: 1;
}
`;
const StyledTabHover = styled.span<{
contentSize?: 'sm' | 'md';
}>`
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
padding: ${({ theme, contentSize }) =>
contentSize === 'sm'
? `${theme.spacing(1)} ${theme.spacing(2)}`
: `${theme.spacing(2)} ${theme.spacing(2)}`};
font-weight: ${({ theme }) => theme.font.weight.medium};
width: 100%;
white-space: nowrap;
border-radius: ${({ theme }) => theme.border.radius.sm};
&:hover {
background: ${({ theme }) => theme.background.tertiary};
}
&:active {
background: ${({ theme }) => theme.background.quaternary};
}
`;
type TabButtonProps = {
id: string;
active?: boolean;
disabled?: boolean;
to?: string;
LeftIcon?: IconComponent;
className?: string;
title?: string;
onClick?: () => void;
logo?: string;
RightIcon?: IconComponent;
pill?: string | ReactElement;
contentSize?: 'sm' | 'md';
disableTestId?: boolean;
};
export const TabButton = ({
id,
active,
disabled,
to,
LeftIcon,
className,
title,
onClick,
logo,
RightIcon,
pill,
contentSize = 'sm',
disableTestId = false,
}: TabButtonProps) => {
const { theme } = useContext(ThemeContext);
const iconColor = active
? theme.font.color.primary
: disabled
? theme.font.color.extraLight
: theme.font.color.secondary;
return (
<StyledTabButton
data-testid={disableTestId ? undefined : `tab-${id}`}
active={active}
disabled={disabled}
as={to ? Link : 'button'}
to={to}
className={className}
onClick={onClick}
>
<StyledTabHover contentSize={contentSize}>
{LeftIcon && <LeftIcon color={iconColor} size={theme.icon.size.md} />}
{logo && <Avatar avatarUrl={logo} size="md" placeholder={title} />}
{title}
{RightIcon && <RightIcon color={iconColor} size={theme.icon.size.md} />}
{pill && (typeof pill === 'string' ? <Pill label={pill} /> : pill)}
</StyledTabHover>
</StyledTabButton>
);
};

View File

@ -0,0 +1,377 @@
import styled from '@emotion/styled';
import { Meta, StoryObj } from '@storybook/react';
import {
IconCheckbox,
IconChevronDown,
IconMail,
IconSearch,
IconSettings,
IconUser,
} from '@ui/display';
import {
CatalogDecorator,
CatalogStory,
ComponentWithRouterDecorator,
RecoilRootDecorator,
} from '@ui/testing';
import { TabButton } from '../TabButton';
// Mimic the TabList container styling for proper positioning
const StyledTabContainer = styled.div`
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
height: 40px;
user-select: none;
position: relative;
align-items: stretch;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 1px;
background-color: ${({ theme }) => theme.border.color.light};
}
`;
const meta: Meta<typeof TabButton> = {
title: 'UI/Input/Button/TabButton',
component: TabButton,
decorators: [ComponentWithRouterDecorator, RecoilRootDecorator],
args: {
id: 'tab-button',
title: 'Tab Title',
active: false,
disabled: false,
contentSize: 'sm',
},
argTypes: {
LeftIcon: { control: false },
RightIcon: { control: false },
pill: { control: 'text' },
contentSize: {
control: 'select',
options: ['sm', 'md'],
},
},
};
export default meta;
type Story = StoryObj<typeof TabButton>;
export const Default: Story = {
args: {
title: 'General',
LeftIcon: IconSettings,
},
render: (args) => (
<StyledTabContainer>
<TabButton
id={args.id}
title={args.title}
LeftIcon={args.LeftIcon}
RightIcon={args.RightIcon}
active={args.active}
disabled={args.disabled}
pill={args.pill}
to={args.to}
logo={args.logo}
onClick={args.onClick}
className={args.className}
contentSize={args.contentSize}
/>
</StyledTabContainer>
),
};
export const Active: Story = {
args: {
title: 'Active Tab',
LeftIcon: IconUser,
active: true,
},
render: (args) => (
<StyledTabContainer>
<TabButton
id={args.id}
title={args.title}
LeftIcon={args.LeftIcon}
RightIcon={args.RightIcon}
active={args.active}
disabled={args.disabled}
pill={args.pill}
to={args.to}
logo={args.logo}
onClick={args.onClick}
className={args.className}
contentSize={args.contentSize}
/>
</StyledTabContainer>
),
};
export const Disabled: Story = {
args: {
title: 'Disabled Tab',
LeftIcon: IconCheckbox,
disabled: true,
},
render: (args) => (
<StyledTabContainer>
<TabButton
id={args.id}
title={args.title}
LeftIcon={args.LeftIcon}
RightIcon={args.RightIcon}
active={args.active}
disabled={args.disabled}
pill={args.pill}
to={args.to}
logo={args.logo}
onClick={args.onClick}
className={args.className}
contentSize={args.contentSize}
/>
</StyledTabContainer>
),
};
export const WithLogo: Story = {
args: {
title: 'Company',
logo: 'https://picsum.photos/192/192',
},
render: (args) => (
<StyledTabContainer>
<TabButton
id={args.id}
title={args.title}
LeftIcon={args.LeftIcon}
RightIcon={args.RightIcon}
active={args.active}
disabled={args.disabled}
pill={args.pill}
to={args.to}
logo={args.logo}
onClick={args.onClick}
className={args.className}
contentSize={args.contentSize}
/>
</StyledTabContainer>
),
};
export const WithStringPill: Story = {
args: {
title: 'Messages',
LeftIcon: IconMail,
pill: '12',
},
render: (args) => (
<StyledTabContainer>
<TabButton
id={args.id}
title={args.title}
LeftIcon={args.LeftIcon}
RightIcon={args.RightIcon}
active={args.active}
disabled={args.disabled}
pill={args.pill}
to={args.to}
logo={args.logo}
onClick={args.onClick}
className={args.className}
contentSize={args.contentSize}
/>
</StyledTabContainer>
),
};
export const WithBothIcons: Story = {
args: {
title: 'Search',
LeftIcon: IconSearch,
RightIcon: IconChevronDown,
},
render: (args) => (
<StyledTabContainer>
<TabButton
id={args.id}
title={args.title}
LeftIcon={args.LeftIcon}
RightIcon={args.RightIcon}
active={args.active}
disabled={args.disabled}
pill={args.pill}
to={args.to}
logo={args.logo}
onClick={args.onClick}
className={args.className}
contentSize={args.contentSize}
/>
</StyledTabContainer>
),
};
export const AsLink: Story = {
args: {
title: 'Link Tab',
LeftIcon: IconUser,
to: '/profile',
},
render: (args) => (
<StyledTabContainer>
<TabButton
id={args.id}
title={args.title}
LeftIcon={args.LeftIcon}
RightIcon={args.RightIcon}
active={args.active}
disabled={args.disabled}
pill={args.pill}
to={args.to}
logo={args.logo}
onClick={args.onClick}
className={args.className}
contentSize={args.contentSize}
/>
</StyledTabContainer>
),
};
export const SmallContent: Story = {
args: {
title: 'Small',
LeftIcon: IconSettings,
contentSize: 'sm',
},
render: (args) => (
<StyledTabContainer>
<TabButton
id={args.id}
title={args.title}
LeftIcon={args.LeftIcon}
RightIcon={args.RightIcon}
active={args.active}
disabled={args.disabled}
pill={args.pill}
to={args.to}
logo={args.logo}
onClick={args.onClick}
className={args.className}
contentSize={args.contentSize}
/>
</StyledTabContainer>
),
};
export const MediumContent: Story = {
args: {
title: 'Medium',
LeftIcon: IconSettings,
contentSize: 'md',
},
render: (args) => (
<StyledTabContainer>
<TabButton
id={args.id}
title={args.title}
LeftIcon={args.LeftIcon}
RightIcon={args.RightIcon}
active={args.active}
disabled={args.disabled}
pill={args.pill}
to={args.to}
logo={args.logo}
onClick={args.onClick}
className={args.className}
contentSize={args.contentSize}
/>
</StyledTabContainer>
),
};
export const Catalog: CatalogStory<Story, typeof TabButton> = {
args: {
title: 'Tab title',
LeftIcon: IconCheckbox,
},
argTypes: {
active: { control: false },
disabled: { control: false },
onClick: { control: false },
to: { control: false },
},
render: (args) => (
<StyledTabContainer>
<TabButton
id={args.id}
title={args.title}
LeftIcon={args.LeftIcon}
RightIcon={args.RightIcon}
active={args.active}
disabled={args.disabled}
pill={args.pill}
to={args.to}
logo={args.logo}
onClick={args.onClick}
className={args.className}
contentSize={args.contentSize}
/>
</StyledTabContainer>
),
parameters: {
pseudo: { hover: ['.hover'], active: ['.active'] },
catalog: {
dimensions: [
{
name: 'states',
values: ['default', 'hover', 'active'],
props: (state: string) =>
state === 'default' ? {} : { className: state },
},
{
name: 'State',
values: ['active', 'inactive', 'disabled'],
labels: (state: string) => state,
props: (state: string) => ({
active: state === 'active',
disabled: state === 'disabled',
}),
},
{
name: 'Content Size',
values: ['sm', 'md'],
labels: (size: string) => size,
props: (size: string) => ({ contentSize: size as 'sm' | 'md' }),
},
{
name: 'Content',
values: ['icon', 'logo', 'pill'],
props: (content: string) => {
switch (content) {
case 'icon':
return { LeftIcon: IconSettings };
case 'logo':
return {
logo: 'https://picsum.photos/192/192',
LeftIcon: undefined,
};
case 'pill':
return { LeftIcon: IconMail, pill: '5' };
default:
return {};
}
},
},
],
},
layout: 'centered',
viewport: {
defaultViewport: 'responsive',
},
},
decorators: [CatalogDecorator],
};

View File

@ -65,6 +65,7 @@ export { LightIconButtonGroup } from './button/components/LightIconButtonGroup';
export type { MainButtonVariant } from './button/components/MainButton';
export { MainButton } from './button/components/MainButton';
export { RoundedIconButton } from './button/components/RoundedIconButton';
export { TabButton } from './button/components/TabButton';
export { CodeEditor } from './code-editor/components/CodeEditor';
export type { CoreEditorHeaderProps } from './code-editor/components/CodeEditorHeader';
export { CoreEditorHeader } from './code-editor/components/CodeEditorHeader';