feat: new tab list (#12384)
closes #9904 --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
122
packages/twenty-ui/src/input/button/components/TabButton.tsx
Normal file
122
packages/twenty-ui/src/input/button/components/TabButton.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -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],
|
||||
};
|
||||
@ -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';
|
||||
|
||||
Reference in New Issue
Block a user