refactor: move Checkmark, Avatar, Chip and Tooltip to twenty-ui (#4946)
Split from https://github.com/twentyhq/twenty/pull/4518 Part of #4766
This commit is contained in:
178
packages/twenty-ui/src/display/chip/components/Chip.tsx
Normal file
178
packages/twenty-ui/src/display/chip/components/Chip.tsx
Normal file
@ -0,0 +1,178 @@
|
||||
import { MouseEvent, ReactNode } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { OverflowingTextWithTooltip } from '@ui/display/tooltip/OverflowingTextWithTooltip';
|
||||
|
||||
export enum ChipSize {
|
||||
Large = 'large',
|
||||
Small = 'small',
|
||||
}
|
||||
|
||||
export enum ChipAccent {
|
||||
TextPrimary = 'text-primary',
|
||||
TextSecondary = 'text-secondary',
|
||||
}
|
||||
|
||||
export enum ChipVariant {
|
||||
Highlighted = 'highlighted',
|
||||
Regular = 'regular',
|
||||
Transparent = 'transparent',
|
||||
Rounded = 'rounded',
|
||||
}
|
||||
|
||||
type ChipProps = {
|
||||
size?: ChipSize;
|
||||
disabled?: boolean;
|
||||
clickable?: boolean;
|
||||
label: string;
|
||||
maxWidth?: number;
|
||||
variant?: ChipVariant;
|
||||
accent?: ChipAccent;
|
||||
leftComponent?: ReactNode;
|
||||
rightComponent?: ReactNode;
|
||||
className?: string;
|
||||
onClick?: (event: MouseEvent<HTMLDivElement>) => void;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div<
|
||||
Pick<
|
||||
ChipProps,
|
||||
'accent' | 'clickable' | 'disabled' | 'maxWidth' | 'size' | 'variant'
|
||||
>
|
||||
>`
|
||||
--chip-horizontal-padding: ${({ theme }) => theme.spacing(1)};
|
||||
--chip-vertical-padding: ${({ theme }) => theme.spacing(1)};
|
||||
|
||||
align-items: center;
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
color: ${({ theme, disabled }) =>
|
||||
disabled ? theme.font.color.light : theme.font.color.secondary};
|
||||
cursor: ${({ clickable, disabled }) =>
|
||||
clickable ? 'pointer' : disabled ? 'not-allowed' : 'inherit'};
|
||||
display: inline-flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
height: ${({ theme }) => theme.spacing(3)};
|
||||
max-width: ${({ maxWidth }) =>
|
||||
maxWidth
|
||||
? `calc(${maxWidth}px - 2 * var(--chip-horizontal-padding))`
|
||||
: '200px'};
|
||||
overflow: hidden;
|
||||
padding: var(--chip-vertical-padding) var(--chip-horizontal-padding);
|
||||
user-select: none;
|
||||
|
||||
// Accent style overrides
|
||||
${({ accent, disabled, theme }) => {
|
||||
if (accent === ChipAccent.TextPrimary) {
|
||||
return (
|
||||
!disabled &&
|
||||
css`
|
||||
color: ${theme.font.color.primary};
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
if (accent === ChipAccent.TextSecondary) {
|
||||
return css`
|
||||
font-weight: ${theme.font.weight.medium};
|
||||
`;
|
||||
}
|
||||
}}
|
||||
|
||||
// Size style overrides
|
||||
${({ theme, size }) =>
|
||||
size === ChipSize.Large &&
|
||||
css`
|
||||
height: ${theme.spacing(4)};
|
||||
`}
|
||||
|
||||
// Variant style overrides
|
||||
${({ disabled, theme, variant }) => {
|
||||
if (variant === ChipVariant.Regular) {
|
||||
return (
|
||||
!disabled &&
|
||||
css`
|
||||
:hover {
|
||||
background-color: ${theme.background.transparent.light};
|
||||
}
|
||||
|
||||
:active {
|
||||
background-color: ${theme.background.transparent.medium};
|
||||
}
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === ChipVariant.Highlighted) {
|
||||
return css`
|
||||
background-color: ${theme.background.transparent.light};
|
||||
|
||||
${!disabled &&
|
||||
css`
|
||||
:hover {
|
||||
background-color: ${theme.background.transparent.medium};
|
||||
}
|
||||
|
||||
:active {
|
||||
background-color: ${theme.background.transparent.strong};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
}
|
||||
|
||||
if (variant === ChipVariant.Rounded) {
|
||||
return css`
|
||||
--chip-horizontal-padding: ${theme.spacing(2)};
|
||||
--chip-vertical-padding: 3px;
|
||||
|
||||
background-color: ${theme.background.transparent.lighter};
|
||||
border: 1px solid ${theme.border.color.medium};
|
||||
border-radius: 50px;
|
||||
`;
|
||||
}
|
||||
|
||||
if (variant === ChipVariant.Transparent) {
|
||||
return css`
|
||||
cursor: inherit;
|
||||
`;
|
||||
}
|
||||
}}
|
||||
`;
|
||||
|
||||
const StyledLabel = styled.span`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const Chip = ({
|
||||
size = ChipSize.Small,
|
||||
label,
|
||||
disabled = false,
|
||||
clickable = true,
|
||||
variant = ChipVariant.Regular,
|
||||
leftComponent,
|
||||
rightComponent,
|
||||
accent = ChipAccent.TextPrimary,
|
||||
maxWidth,
|
||||
className,
|
||||
onClick,
|
||||
}: ChipProps) => (
|
||||
<StyledContainer
|
||||
data-testid="chip"
|
||||
clickable={clickable}
|
||||
variant={variant}
|
||||
accent={accent}
|
||||
size={size}
|
||||
disabled={disabled}
|
||||
className={className}
|
||||
maxWidth={maxWidth}
|
||||
onClick={onClick}
|
||||
>
|
||||
{leftComponent}
|
||||
<StyledLabel>
|
||||
<OverflowingTextWithTooltip text={label} />
|
||||
</StyledLabel>
|
||||
{rightComponent}
|
||||
</StyledContainer>
|
||||
);
|
||||
@ -0,0 +1,81 @@
|
||||
import * as React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { Avatar, AvatarType } from '@ui/display/avatar/components/Avatar';
|
||||
import { IconComponent } from '@ui/display/icon/types/IconComponent';
|
||||
import { Nullable } from '@ui/utilities/types/Nullable';
|
||||
|
||||
import { Chip, ChipVariant } from './Chip';
|
||||
|
||||
export type EntityChipProps = {
|
||||
linkToEntity?: string;
|
||||
entityId: string;
|
||||
name: string;
|
||||
avatarUrl?: string;
|
||||
avatarType?: Nullable<AvatarType>;
|
||||
variant?: EntityChipVariant;
|
||||
LeftIcon?: IconComponent;
|
||||
className?: string;
|
||||
maxWidth?: number;
|
||||
};
|
||||
|
||||
export enum EntityChipVariant {
|
||||
Regular = 'regular',
|
||||
Transparent = 'transparent',
|
||||
}
|
||||
|
||||
export const EntityChip = ({
|
||||
linkToEntity,
|
||||
entityId,
|
||||
name,
|
||||
avatarUrl,
|
||||
avatarType = 'rounded',
|
||||
variant = EntityChipVariant.Regular,
|
||||
LeftIcon,
|
||||
className,
|
||||
maxWidth,
|
||||
}: EntityChipProps) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const handleLinkClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (isNonEmptyString(linkToEntity)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
navigate(linkToEntity);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Chip
|
||||
label={name}
|
||||
variant={
|
||||
linkToEntity
|
||||
? variant === EntityChipVariant.Regular
|
||||
? ChipVariant.Highlighted
|
||||
: ChipVariant.Regular
|
||||
: ChipVariant.Transparent
|
||||
}
|
||||
leftComponent={
|
||||
LeftIcon ? (
|
||||
<LeftIcon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
|
||||
) : (
|
||||
<Avatar
|
||||
avatarUrl={avatarUrl}
|
||||
entityId={entityId}
|
||||
placeholder={name}
|
||||
size="sm"
|
||||
type={avatarType}
|
||||
/>
|
||||
)
|
||||
}
|
||||
clickable={!!linkToEntity}
|
||||
onClick={handleLinkClick}
|
||||
className={className}
|
||||
maxWidth={maxWidth}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,71 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import {
|
||||
CatalogDecorator,
|
||||
CatalogStory,
|
||||
ComponentDecorator,
|
||||
} from '@ui/testing';
|
||||
|
||||
import { Chip, ChipAccent, ChipSize, ChipVariant } from '../Chip';
|
||||
|
||||
const meta: Meta<typeof Chip> = {
|
||||
title: 'UI/Display/Chip/Chip',
|
||||
component: Chip,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Chip>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
label: 'Chip test',
|
||||
size: ChipSize.Small,
|
||||
variant: ChipVariant.Highlighted,
|
||||
accent: ChipAccent.TextPrimary,
|
||||
disabled: false,
|
||||
clickable: true,
|
||||
maxWidth: 200,
|
||||
},
|
||||
decorators: [ComponentDecorator],
|
||||
};
|
||||
|
||||
export const Catalog: CatalogStory<Story, typeof Chip> = {
|
||||
args: { clickable: true, label: 'Hello' },
|
||||
argTypes: {
|
||||
size: { control: false },
|
||||
variant: { control: false },
|
||||
disabled: { control: false },
|
||||
className: { control: false },
|
||||
rightComponent: { control: false },
|
||||
leftComponent: { control: false },
|
||||
},
|
||||
parameters: {
|
||||
pseudo: { hover: ['.hover'], active: ['.active'] },
|
||||
catalog: {
|
||||
dimensions: [
|
||||
{
|
||||
name: 'states',
|
||||
values: ['default', 'hover', 'active', 'disabled'],
|
||||
props: (state: string) =>
|
||||
state === 'default' ? {} : { className: state },
|
||||
},
|
||||
{
|
||||
name: 'variants',
|
||||
values: Object.values(ChipVariant),
|
||||
props: (variant: ChipVariant) => ({ variant }),
|
||||
},
|
||||
{
|
||||
name: 'sizes',
|
||||
values: Object.values(ChipSize),
|
||||
props: (size: ChipSize) => ({ size }),
|
||||
},
|
||||
{
|
||||
name: 'accents',
|
||||
values: Object.values(ChipAccent),
|
||||
props: (accent: ChipAccent) => ({ accent }),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
decorators: [CatalogDecorator],
|
||||
};
|
||||
@ -0,0 +1,21 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { ComponentDecorator, RouterDecorator } from '@ui/testing';
|
||||
|
||||
import { EntityChip } from '../EntityChip';
|
||||
|
||||
const meta: Meta<typeof EntityChip> = {
|
||||
title: 'UI/Display/Chip/EntityChip',
|
||||
component: EntityChip,
|
||||
decorators: [RouterDecorator, ComponentDecorator],
|
||||
args: {
|
||||
name: 'Entity name',
|
||||
linkToEntity: '/entity-link',
|
||||
avatarType: 'squared',
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof EntityChip>;
|
||||
|
||||
export const Default: Story = {};
|
||||
Reference in New Issue
Block a user