[REFACTOR] Twenty UI multi barrel (#11301)
# Introduction closes https://github.com/twentyhq/core-team-issues/issues/591 Same than for `twenty-shared` made in https://github.com/twentyhq/twenty/pull/11083. ## TODO - [x] Manual migrate twenty-website twenty-ui imports ## What's next: - Generate barrel and migration script factorization within own package + tests - Refactoring using preconstruct ? TimeBox - Lint circular dependencies - Lint import from barrel and forbid them ### Preconstruct We need custom rollup plugins addition, but preconstruct does not expose its rollup configuration. It might be possible to handle this using the babel overrides. But was a big tunnel. We could give it a try afterwards ! ( allowing cjs interop and stuff like that ) Stuck to vite lib app Closed related PRs: - https://github.com/twentyhq/twenty/pull/11294 - https://github.com/twentyhq/twenty/pull/11203
This commit is contained in:
@ -1,7 +1,7 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { Pill } from '@ui/components/Pill/Pill';
|
||||
import { ComponentDecorator } from '../../../testing/decorators/ComponentDecorator';
|
||||
import { Pill } from '../Pill';
|
||||
|
||||
const meta: Meta<typeof Pill> = {
|
||||
title: 'UI/Display/Pill',
|
||||
|
||||
37
packages/twenty-ui/src/components/avatar-chip/AvatarChip.tsx
Normal file
37
packages/twenty-ui/src/components/avatar-chip/AvatarChip.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { AvatarChipsLeftComponent } from '@ui/components/avatar-chip/AvatarChipLeftComponent';
|
||||
import { AvatarChipsCommonProps } from '@ui/components/avatar-chip/types/AvatarChipsCommonProps.type';
|
||||
import { Chip, ChipVariant } from '@ui/components/chip/Chip';
|
||||
|
||||
export type AvatarChipProps = AvatarChipsCommonProps;
|
||||
export const AvatarChip = ({
|
||||
name,
|
||||
LeftIcon,
|
||||
LeftIconColor,
|
||||
avatarType,
|
||||
avatarUrl,
|
||||
className,
|
||||
isIconInverted,
|
||||
maxWidth,
|
||||
placeholderColorSeed,
|
||||
size,
|
||||
}: AvatarChipProps) => (
|
||||
<Chip
|
||||
label={name}
|
||||
variant={ChipVariant.Transparent}
|
||||
size={size}
|
||||
leftComponent={() => (
|
||||
<AvatarChipsLeftComponent
|
||||
name={name}
|
||||
LeftIcon={LeftIcon}
|
||||
LeftIconColor={LeftIconColor}
|
||||
avatarType={avatarType}
|
||||
avatarUrl={avatarUrl}
|
||||
isIconInverted={isIconInverted}
|
||||
placeholderColorSeed={placeholderColorSeed}
|
||||
/>
|
||||
)}
|
||||
clickable={false}
|
||||
className={className}
|
||||
maxWidth={maxWidth}
|
||||
/>
|
||||
);
|
||||
@ -0,0 +1,74 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Avatar } from '@ui/display/avatar/components/Avatar';
|
||||
import { AvatarType } from '@ui/display/avatar/types/AvatarType';
|
||||
import { IconComponent } from '@ui/display/icon/types/IconComponent';
|
||||
import { Nullable } from '@ui/utilities';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const StyledInvertedIconContainer = styled.div<{ backgroundColor: string }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 4px;
|
||||
background-color: ${({ backgroundColor }) => backgroundColor};
|
||||
`;
|
||||
|
||||
export type AvatarChipsLeftComponentProps = {
|
||||
name: string;
|
||||
avatarUrl?: string;
|
||||
avatarType?: Nullable<AvatarType>;
|
||||
LeftIcon?: IconComponent;
|
||||
LeftIconColor?: string;
|
||||
isIconInverted?: boolean;
|
||||
placeholderColorSeed?: string;
|
||||
};
|
||||
|
||||
export const AvatarChipsLeftComponent: React.FC<
|
||||
AvatarChipsLeftComponentProps
|
||||
> = ({
|
||||
LeftIcon,
|
||||
placeholderColorSeed,
|
||||
avatarType,
|
||||
avatarUrl,
|
||||
name,
|
||||
isIconInverted = false,
|
||||
LeftIconColor,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
if (!isDefined(LeftIcon)) {
|
||||
return (
|
||||
<Avatar
|
||||
avatarUrl={avatarUrl}
|
||||
placeholderColorSeed={placeholderColorSeed}
|
||||
placeholder={name}
|
||||
size="sm"
|
||||
type={avatarType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isIconInverted) {
|
||||
return (
|
||||
<StyledInvertedIconContainer
|
||||
backgroundColor={theme.background.invertedSecondary}
|
||||
>
|
||||
<LeftIcon
|
||||
color="white"
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
</StyledInvertedIconContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<LeftIcon
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
color={LeftIconColor || 'currentColor'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,56 @@
|
||||
import { AvatarChipsLeftComponent } from '@ui/components/avatar-chip/AvatarChipLeftComponent';
|
||||
import { AvatarChipsCommonProps } from '@ui/components/avatar-chip/types/AvatarChipsCommonProps.type';
|
||||
import { AvatarChipVariant } from '@ui/components/avatar-chip/types/AvatarChipsVariant.type';
|
||||
import { ChipVariant } from '@ui/components/chip/Chip';
|
||||
import { LinkChip, LinkChipProps } from '@ui/components/chip/LinkChip';
|
||||
|
||||
export type LinkAvatarChipProps = Omit<AvatarChipsCommonProps, 'clickable'> & {
|
||||
to: string;
|
||||
onClick?: LinkChipProps['onClick'];
|
||||
variant?: AvatarChipVariant;
|
||||
isLabelHidden?: boolean;
|
||||
};
|
||||
|
||||
export const LinkAvatarChip = ({
|
||||
to,
|
||||
onClick,
|
||||
name,
|
||||
LeftIcon,
|
||||
LeftIconColor,
|
||||
avatarType,
|
||||
avatarUrl,
|
||||
className,
|
||||
isIconInverted,
|
||||
maxWidth,
|
||||
placeholderColorSeed,
|
||||
size,
|
||||
variant,
|
||||
isLabelHidden,
|
||||
}: LinkAvatarChipProps) => (
|
||||
<LinkChip
|
||||
to={to}
|
||||
onClick={onClick}
|
||||
label={name}
|
||||
isLabelHidden={isLabelHidden}
|
||||
variant={
|
||||
//Regular but Highlighted -> missleading
|
||||
variant === AvatarChipVariant.Regular
|
||||
? ChipVariant.Highlighted
|
||||
: ChipVariant.Regular
|
||||
}
|
||||
size={size}
|
||||
leftComponent={() => (
|
||||
<AvatarChipsLeftComponent
|
||||
name={name}
|
||||
LeftIcon={LeftIcon}
|
||||
LeftIconColor={LeftIconColor}
|
||||
avatarType={avatarType}
|
||||
avatarUrl={avatarUrl}
|
||||
isIconInverted={isIconInverted}
|
||||
placeholderColorSeed={placeholderColorSeed}
|
||||
/>
|
||||
)}
|
||||
className={className}
|
||||
maxWidth={maxWidth}
|
||||
/>
|
||||
);
|
||||
@ -0,0 +1,8 @@
|
||||
import { AvatarChipsLeftComponentProps } from '@ui/components/avatar-chip/AvatarChipLeftComponent';
|
||||
import { ChipSize } from '@ui/components/chip/Chip';
|
||||
|
||||
export type AvatarChipsCommonProps = {
|
||||
size?: ChipSize;
|
||||
className?: string;
|
||||
maxWidth?: number;
|
||||
} & AvatarChipsLeftComponentProps;
|
||||
@ -0,0 +1,4 @@
|
||||
export enum AvatarChipVariant {
|
||||
Regular = 'regular',
|
||||
Transparent = 'transparent',
|
||||
}
|
||||
156
packages/twenty-ui/src/components/chip/Chip.tsx
Normal file
156
packages/twenty-ui/src/components/chip/Chip.tsx
Normal file
@ -0,0 +1,156 @@
|
||||
import { Theme, withTheme } from '@emotion/react';
|
||||
import { styled } from '@linaria/react';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
export type ChipProps = {
|
||||
size?: ChipSize;
|
||||
disabled?: boolean;
|
||||
clickable?: boolean;
|
||||
label: string;
|
||||
isLabelHidden?: boolean;
|
||||
maxWidth?: number;
|
||||
variant?: ChipVariant;
|
||||
accent?: ChipAccent;
|
||||
leftComponent?: (() => ReactNode) | null;
|
||||
rightComponent?: (() => ReactNode) | null;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const StyledContainer = withTheme(styled.div<
|
||||
Pick<
|
||||
ChipProps,
|
||||
'accent' | 'clickable' | 'disabled' | 'maxWidth' | 'size' | 'variant'
|
||||
> & { theme: Theme }
|
||||
>`
|
||||
--chip-horizontal-padding: ${({ theme }) => theme.spacing(1)};
|
||||
--chip-vertical-padding: ${({ theme }) => theme.spacing(1)};
|
||||
|
||||
text-decoration: none;
|
||||
align-items: center;
|
||||
|
||||
color: ${({ theme, accent, disabled }) =>
|
||||
disabled
|
||||
? theme.font.color.light
|
||||
: accent === ChipAccent.TextPrimary
|
||||
? theme.font.color.primary
|
||||
: theme.font.color.secondary};
|
||||
|
||||
cursor: ${({ clickable, disabled, variant }) =>
|
||||
variant === ChipVariant.Transparent
|
||||
? 'inherit'
|
||||
: clickable
|
||||
? 'pointer'
|
||||
: disabled
|
||||
? 'not-allowed'
|
||||
: 'inherit'};
|
||||
|
||||
display: inline-flex;
|
||||
justify-content: flex-start;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
height: ${({ theme, size }) =>
|
||||
size === ChipSize.Large ? theme.spacing(4) : 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;
|
||||
|
||||
font-weight: ${({ theme, accent }) =>
|
||||
accent === ChipAccent.TextSecondary ? theme.font.weight.medium : 'inherit'};
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme, variant, disabled }) =>
|
||||
variant === ChipVariant.Regular && !disabled
|
||||
? theme.background.transparent.light
|
||||
: variant === ChipVariant.Highlighted
|
||||
? theme.background.transparent.medium
|
||||
: 'inherit'};
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: ${({ theme, disabled, variant }) =>
|
||||
variant === ChipVariant.Regular && !disabled
|
||||
? theme.background.transparent.medium
|
||||
: variant === ChipVariant.Highlighted
|
||||
? theme.background.transparent.strong
|
||||
: 'inherit'};
|
||||
}
|
||||
|
||||
background-color: ${({ theme, variant }) =>
|
||||
variant === ChipVariant.Highlighted
|
||||
? theme.background.transparent.light
|
||||
: variant === ChipVariant.Rounded
|
||||
? theme.background.transparent.lighter
|
||||
: 'inherit'};
|
||||
|
||||
border: ${({ theme, variant }) =>
|
||||
variant === ChipVariant.Rounded
|
||||
? `1px solid ${theme.border.color.medium}`
|
||||
: 'none'};
|
||||
|
||||
border-radius: ${({ theme, variant }) =>
|
||||
variant === ChipVariant.Rounded ? '50px' : theme.border.radius.sm};
|
||||
|
||||
& > svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
padding-left: ${({ theme, variant }) =>
|
||||
variant === ChipVariant.Transparent
|
||||
? theme.spacing(0)
|
||||
: 'var(--chip-horizontal-padding)'};
|
||||
`);
|
||||
|
||||
export const Chip = ({
|
||||
size = ChipSize.Small,
|
||||
label,
|
||||
isLabelHidden = false,
|
||||
disabled = false,
|
||||
clickable = true,
|
||||
variant = ChipVariant.Regular,
|
||||
leftComponent = null,
|
||||
rightComponent = null,
|
||||
accent = ChipAccent.TextPrimary,
|
||||
className,
|
||||
maxWidth,
|
||||
}: ChipProps) => {
|
||||
return (
|
||||
<StyledContainer
|
||||
data-testid="chip"
|
||||
accent={accent}
|
||||
clickable={clickable}
|
||||
disabled={disabled}
|
||||
size={size}
|
||||
variant={variant}
|
||||
className={className}
|
||||
maxWidth={maxWidth}
|
||||
>
|
||||
{leftComponent?.()}
|
||||
{!isLabelHidden && (
|
||||
<OverflowingTextWithTooltip size={size} text={label} />
|
||||
)}
|
||||
{rightComponent?.()}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
55
packages/twenty-ui/src/components/chip/LinkChip.tsx
Normal file
55
packages/twenty-ui/src/components/chip/LinkChip.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
Chip,
|
||||
ChipAccent,
|
||||
ChipProps,
|
||||
ChipSize,
|
||||
ChipVariant,
|
||||
} from '@ui/components/chip/Chip';
|
||||
import { MouseEvent } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export type LinkChipProps = Omit<
|
||||
ChipProps,
|
||||
'onClick' | 'disabled' | 'clickable'
|
||||
> & {
|
||||
to: string;
|
||||
onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
|
||||
};
|
||||
|
||||
// Ideally we would use the UndecoratedLink component from @ui/navigation
|
||||
// but it led to a bug probably linked to circular dependencies, which was hard to solve
|
||||
const StyledLink = styled(Link)`
|
||||
text-decoration: none;
|
||||
`;
|
||||
|
||||
export const LinkChip = ({
|
||||
to,
|
||||
size = ChipSize.Small,
|
||||
label,
|
||||
isLabelHidden = false,
|
||||
variant = ChipVariant.Regular,
|
||||
leftComponent = null,
|
||||
rightComponent = null,
|
||||
accent = ChipAccent.TextPrimary,
|
||||
className,
|
||||
maxWidth,
|
||||
onClick,
|
||||
}: LinkChipProps) => {
|
||||
return (
|
||||
<StyledLink to={to} onClick={onClick}>
|
||||
<Chip
|
||||
size={size}
|
||||
label={label}
|
||||
isLabelHidden={isLabelHidden}
|
||||
clickable={true}
|
||||
variant={variant}
|
||||
leftComponent={leftComponent}
|
||||
rightComponent={rightComponent}
|
||||
accent={accent}
|
||||
className={className}
|
||||
maxWidth={maxWidth}
|
||||
/>
|
||||
</StyledLink>
|
||||
);
|
||||
};
|
||||
@ -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,23 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { AvatarChip } from '@ui/components/avatar-chip/AvatarChip';
|
||||
|
||||
import {
|
||||
ComponentDecorator,
|
||||
RecoilRootDecorator,
|
||||
RouterDecorator,
|
||||
} from '@ui/testing';
|
||||
|
||||
const meta: Meta<typeof AvatarChip> = {
|
||||
title: 'UI/Display/Chip/AvatarChip',
|
||||
component: AvatarChip,
|
||||
decorators: [RouterDecorator, ComponentDecorator, RecoilRootDecorator],
|
||||
args: {
|
||||
name: 'Entity name',
|
||||
avatarType: 'squared',
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AvatarChip>;
|
||||
|
||||
export const Default: Story = {};
|
||||
@ -1 +1,24 @@
|
||||
export * from './Pill/Pill';
|
||||
/*
|
||||
* _____ _
|
||||
*|_ _|_ _____ _ __ | |_ _ _
|
||||
* | | \ \ /\ / / _ \ '_ \| __| | | | Auto-generated file
|
||||
* | | \ V V / __/ | | | |_| |_| | Any edits to this will be overridden
|
||||
* |_| \_/\_/ \___|_| |_|\__|\__, |
|
||||
* |___/
|
||||
*/
|
||||
|
||||
export type { AvatarChipProps } from './avatar-chip/AvatarChip';
|
||||
export { AvatarChip } from './avatar-chip/AvatarChip';
|
||||
export type { AvatarChipsLeftComponentProps } from './avatar-chip/AvatarChipLeftComponent';
|
||||
export { AvatarChipsLeftComponent } from './avatar-chip/AvatarChipLeftComponent';
|
||||
export type { LinkAvatarChipProps } from './avatar-chip/LinkAvatarChip';
|
||||
export { LinkAvatarChip } from './avatar-chip/LinkAvatarChip';
|
||||
export type { AvatarChipsCommonProps } from './avatar-chip/types/AvatarChipsCommonProps.type';
|
||||
export { AvatarChipVariant } from './avatar-chip/types/AvatarChipsVariant.type';
|
||||
export type { ChipProps } from './chip/Chip';
|
||||
export { ChipSize, ChipAccent, ChipVariant, Chip } from './chip/Chip';
|
||||
export type { LinkChipProps } from './chip/LinkChip';
|
||||
export { LinkChip } from './chip/LinkChip';
|
||||
export { Pill } from './Pill/Pill';
|
||||
export type { TagColor } from './tag/Tag';
|
||||
export { Tag } from './tag/Tag';
|
||||
|
||||
142
packages/twenty-ui/src/components/tag/Tag.tsx
Normal file
142
packages/twenty-ui/src/components/tag/Tag.tsx
Normal file
@ -0,0 +1,142 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useContext } from 'react';
|
||||
|
||||
// TODO prastoin We should forbid barrel import within the twenty-ui package
|
||||
import { IconComponent } from '@ui/display/icon/types/IconComponent';
|
||||
import { OverflowingTextWithTooltip } from '@ui/display/tooltip/OverflowingTextWithTooltip';
|
||||
import {
|
||||
BORDER_COMMON,
|
||||
THEME_COMMON,
|
||||
ThemeColor,
|
||||
ThemeContext,
|
||||
ThemeType,
|
||||
} from '@ui/theme';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const spacing5 = THEME_COMMON.spacing(5);
|
||||
const spacing2 = THEME_COMMON.spacing(2);
|
||||
const spacing1 = THEME_COMMON.spacing(1);
|
||||
|
||||
const StyledTag = styled.h3<{
|
||||
theme: ThemeType;
|
||||
color: TagColor;
|
||||
weight: TagWeight;
|
||||
variant: TagVariant;
|
||||
preventShrink?: boolean;
|
||||
preventPadding?: boolean;
|
||||
}>`
|
||||
align-items: center;
|
||||
background: ${({ color, theme }) => {
|
||||
if (color === 'transparent') {
|
||||
return 'transparent';
|
||||
} else {
|
||||
const themeColor = theme.tag.background[color];
|
||||
|
||||
if (!isDefined(themeColor)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`Tag color ${color} is not defined in the theme`);
|
||||
return theme.tag.background.gray;
|
||||
} else {
|
||||
return themeColor;
|
||||
}
|
||||
}
|
||||
}};
|
||||
border-radius: ${BORDER_COMMON.radius.sm};
|
||||
color: ${({ color, theme }) =>
|
||||
color === 'transparent'
|
||||
? theme.font.color.secondary
|
||||
: theme.tag.text[color]};
|
||||
display: inline-flex;
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
font-style: normal;
|
||||
font-weight: ${({ theme, weight }) =>
|
||||
weight === 'regular'
|
||||
? theme.font.weight.regular
|
||||
: theme.font.weight.medium};
|
||||
height: ${spacing5};
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
padding: ${({ preventPadding }) => (preventPadding ? '0' : `0 ${spacing2}`)};
|
||||
border: ${({ variant, theme }) =>
|
||||
variant === 'outline' || variant === 'border'
|
||||
? `1px ${variant === 'border' ? 'solid' : 'dashed'} ${theme.border.color.strong}`
|
||||
: 'none'};
|
||||
|
||||
gap: ${spacing1};
|
||||
|
||||
min-width: ${({ preventShrink }) => (preventShrink ? 'fit-content' : 'none')};
|
||||
`;
|
||||
|
||||
const StyledContent = styled.span`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const StyledNonShrinkableText = styled.span`
|
||||
white-space: nowrap;
|
||||
width: fit-content;
|
||||
`;
|
||||
|
||||
const StyledIconContainer = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
type TagWeight = 'regular' | 'medium';
|
||||
type TagVariant = 'solid' | 'outline' | 'border';
|
||||
export type TagColor = ThemeColor | 'transparent';
|
||||
|
||||
type TagProps = {
|
||||
className?: string;
|
||||
color: TagColor;
|
||||
text: string;
|
||||
Icon?: IconComponent;
|
||||
onClick?: () => void;
|
||||
weight?: TagWeight;
|
||||
variant?: TagVariant;
|
||||
preventShrink?: boolean;
|
||||
preventPadding?: boolean;
|
||||
};
|
||||
|
||||
// TODO: Find a way to have ellipsis and shrinkable tag in tag list while keeping good perf for table cells
|
||||
export const Tag = ({
|
||||
className,
|
||||
color,
|
||||
text,
|
||||
Icon,
|
||||
onClick,
|
||||
weight = 'regular',
|
||||
variant = 'solid',
|
||||
preventShrink,
|
||||
preventPadding,
|
||||
}: TagProps) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
return (
|
||||
<StyledTag
|
||||
theme={theme}
|
||||
className={className}
|
||||
color={color}
|
||||
onClick={onClick}
|
||||
weight={weight}
|
||||
variant={variant}
|
||||
preventShrink={preventShrink}
|
||||
preventPadding={preventPadding}
|
||||
>
|
||||
{isDefined(Icon) ? (
|
||||
<StyledIconContainer>
|
||||
<Icon size={theme.icon.size.sm} stroke={theme.icon.stroke.sm} />
|
||||
</StyledIconContainer>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{preventShrink ? (
|
||||
<StyledNonShrinkableText>{text}</StyledNonShrinkableText>
|
||||
) : (
|
||||
<StyledContent>
|
||||
<OverflowingTextWithTooltip text={text} />
|
||||
</StyledContent>
|
||||
)}
|
||||
</StyledTag>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,105 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { expect, fn, userEvent, within } from '@storybook/test';
|
||||
|
||||
import { IconUser } from '@ui/display/icon/components/TablerIcons';
|
||||
import {
|
||||
CatalogDecorator,
|
||||
CatalogStory,
|
||||
ComponentDecorator,
|
||||
} from '@ui/testing';
|
||||
import { MAIN_COLOR_NAMES, ThemeColor } from '@ui/theme';
|
||||
|
||||
import { Tag } from '../Tag';
|
||||
|
||||
const meta: Meta<typeof Tag> = {
|
||||
title: 'UI/Display/Tag/Tag',
|
||||
component: Tag,
|
||||
args: {
|
||||
text: 'Urgent',
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Tag>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
color: 'red',
|
||||
onClick: fn(),
|
||||
},
|
||||
decorators: [ComponentDecorator],
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const tag = canvas.getByRole('heading', { level: 3 });
|
||||
|
||||
await userEvent.click(tag);
|
||||
await expect(args.onClick).toHaveBeenCalled();
|
||||
},
|
||||
};
|
||||
|
||||
export const WithLongText: Story = {
|
||||
decorators: [ComponentDecorator],
|
||||
args: {
|
||||
color: 'green',
|
||||
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
|
||||
},
|
||||
parameters: {
|
||||
container: { width: 100 },
|
||||
},
|
||||
};
|
||||
|
||||
export const WithIcon: Story = {
|
||||
decorators: [ComponentDecorator],
|
||||
args: {
|
||||
color: 'green',
|
||||
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
|
||||
Icon: IconUser,
|
||||
},
|
||||
parameters: {
|
||||
container: { width: 100 },
|
||||
},
|
||||
};
|
||||
|
||||
export const DontShrink: Story = {
|
||||
decorators: [ComponentDecorator],
|
||||
args: {
|
||||
color: 'green',
|
||||
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
|
||||
preventShrink: true,
|
||||
},
|
||||
parameters: {
|
||||
container: { width: 100 },
|
||||
},
|
||||
};
|
||||
|
||||
export const Catalog: CatalogStory<Story, typeof Tag> = {
|
||||
argTypes: {
|
||||
color: { control: false },
|
||||
},
|
||||
parameters: {
|
||||
catalog: {
|
||||
dimensions: [
|
||||
{
|
||||
name: 'colors',
|
||||
values: MAIN_COLOR_NAMES,
|
||||
props: (color: ThemeColor) => ({ color }),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
decorators: [CatalogDecorator],
|
||||
};
|
||||
|
||||
export const EmptyTag: Story = {
|
||||
decorators: [ComponentDecorator],
|
||||
args: {
|
||||
color: 'transparent',
|
||||
text: 'No Value',
|
||||
variant: 'outline',
|
||||
weight: 'medium',
|
||||
},
|
||||
parameters: {
|
||||
container: { width: 'auto' },
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user