Refactor chip component hierarchy: AvatarChip → AvatarOrIcon (#18313)

## Summary

Cleans up the chip component hierarchy in `twenty-ui`:

- **Fix twenty-ui Storybook** — The `wyw-in-js` Vite plugin crashed on
`/@react-refresh` virtual module. Fixed by setting `enforce: 'pre'` so
it runs before the React refresh plugin injects virtual imports.
- **Rename `AvatarChip` → `AvatarOrIcon`** — The old name was
misleading. This component is not a chip — it's a polymorphic renderer
that displays either an `Avatar` (image/initials) or an `Icon` (plain or
with colored background). It's typically slotted into `Chip`/`LinkChip`
as `leftComponent`.
- **Move `rightComponentDivider` to `Chip`/`LinkChip`** — The vertical
separator between chip content and a right action (e.g. a close button)
is a chip layout concern, not an avatar concern. Added
`rightComponentDivider` boolean prop to `Chip` and `LinkChip`.
- **Remove `MultipleAvatarChip`** — Zero consumers in the codebase. The
command menu implements its own overlapping avatar layout.
- **Migrate raw icon usages** — `CalendarEventDetails` and `FileIcon`
(small size) now use `AvatarOrIcon` for consistent Chip icon rendering.
- **Enhance stories** — Full `CatalogDecorator` coverage for `Chip` and
`LinkChip` showing all variants, sizes, accents, and states.

## Component hierarchy

```
AvatarOrIcon (twenty-ui)
  ├── No Icon → renders Avatar (image or initials)
  ├── Icon + background → renders icon in colored square
  └── Icon only → renders plain icon
  Used as leftComponent/rightComponent in Chip or standalone

Chip (twenty-ui)
  ├── leftComponent (typically AvatarOrIcon)
  ├── label (with overflow tooltip)
  ├── rightComponentDivider (optional vertical separator)
  └── rightComponent (e.g. close icon via AvatarOrIcon)

LinkChip (twenty-ui)
  └── Wraps Chip inside a react-router <Link>

RecordChip (twenty-front)
  └── Composes Chip/LinkChip + AvatarOrIcon with record data
```

## `Chip` API additions

| Prop | Type | Description |
|------|------|-------------|
| `rightComponentDivider` | `boolean` | Renders a vertical separator
before `rightComponent` |

## Stories

<img width="1032" height="576" alt="image"
src="https://github.com/user-attachments/assets/fe7c7666-9b16-4545-b87e-1b53e22d462d"
/>
This commit is contained in:
Charles Bochet
2026-03-02 15:48:49 +01:00
committed by GitHub
parent 37bcb35391
commit 9c4b0f526c
21 changed files with 413 additions and 205 deletions
@@ -1,4 +1,4 @@
import { css, useTheme } from '@emotion/react';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useCallback, useState } from 'react';
@@ -25,7 +25,13 @@ import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecord
import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordFieldReadOnly';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { isDefined } from 'twenty-shared/utils';
import { Chip, ChipAccent, ChipSize, ChipVariant } from 'twenty-ui/components';
import {
AvatarOrIcon,
Chip,
ChipAccent,
ChipSize,
ChipVariant,
} from 'twenty-ui/components';
import { IconCalendarEvent } from 'twenty-ui/display';
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
@@ -88,7 +94,6 @@ export const CalendarEventDetails = ({
calendarEvent,
}: CalendarEventDetailsProps) => {
const { t } = useLingui();
const theme = useTheme();
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.CalendarEvent,
});
@@ -202,7 +207,7 @@ export const CalendarEventDetails = ({
size={ChipSize.Large}
variant={ChipVariant.Highlighted}
clickable={false}
leftComponent={<IconCalendarEvent size={theme.icon.size.md} />}
leftComponent={<AvatarOrIcon Icon={IconCalendarEvent} />}
label={t`Event`}
/>
<StyledHeader>
@@ -4,7 +4,7 @@ import { IconMapping } from '@/file/utils/fileIconMappings';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { type WorkflowAttachment } from 'twenty-shared/workflow';
import { AvatarChip } from 'twenty-ui/components';
import { AvatarOrIcon } from 'twenty-ui/components';
import { IconX } from 'twenty-ui/display';
type WorkflowAttachmentChipProps = {
@@ -68,7 +68,7 @@ export const WorkflowAttachmentChip = ({
return (
<StyledChip data-chip deletable={!readonly}>
<AvatarChip
<AvatarOrIcon
Icon={IconMapping[getFileType(file.name)]}
IconBackgroundColor={iconColors[getFileType(file.name)]}
/>
@@ -2,7 +2,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { getLinkToShowPage } from '@/object-metadata/utils/getLinkToShowPage';
import { t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { AvatarChip, ChipVariant, LinkChip } from 'twenty-ui/components';
import { AvatarOrIcon, ChipVariant, LinkChip } from 'twenty-ui/components';
type RecordLinkProps = {
objectNameSingular: string;
@@ -34,7 +34,7 @@ export const RecordLink = ({
to={linkToShowPage}
variant={ChipVariant.Highlighted}
leftComponent={
<AvatarChip
<AvatarOrIcon
placeholder={displayName}
placeholderColorSeed={recordId}
avatarType="rounded"
@@ -6,7 +6,12 @@ import { useTheme } from '@emotion/react';
import { t } from '@lingui/core/macro';
import { type FileUIPart } from 'ai';
import { isDefined } from 'twenty-shared/utils';
import { AvatarChip, Chip, ChipVariant, LinkChip } from 'twenty-ui/components';
import {
AvatarOrIcon,
Chip,
ChipVariant,
LinkChip,
} from 'twenty-ui/components';
import { type IconComponent, IconX } from 'twenty-ui/display';
import { Loader } from 'twenty-ui/feedback';
@@ -36,21 +41,22 @@ export const AgentChatFilePreview = ({
const leftComponent = isUploading ? (
<Loader color="yellow" />
) : (
<AvatarChip
<AvatarOrIcon
Icon={FileCategoryIcon}
IconBackgroundColor={iconBackgroundColor}
/>
);
const rightComponent = onRemove ? (
<AvatarChip
<AvatarOrIcon
Icon={IconX}
IconColor={theme.font.color.secondary}
onClick={onRemove}
divider="left"
/>
) : undefined;
const hasRightDivider = isDefined(onRemove);
if (isDefined(fileUrl)) {
return (
<LinkChip
@@ -61,6 +67,7 @@ export const AgentChatFilePreview = ({
target="_blank"
leftComponent={leftComponent}
rightComponent={rightComponent}
rightComponentDivider={hasRightDivider}
/>
);
}
@@ -73,6 +80,7 @@ export const AgentChatFilePreview = ({
clickable={false}
leftComponent={leftComponent}
rightComponent={rightComponent}
rightComponentDivider={hasRightDivider}
/>
);
};
@@ -3,12 +3,12 @@ import { IconMapping } from '@/file/utils/fileIconMappings';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { type FileCategory } from 'twenty-shared/types';
import { AvatarOrIcon } from 'twenty-ui/components';
type FileIconSize = 'small' | 'medium';
const StyledIconContainer = styled.div<{
background: string;
size: FileIconSize;
}>`
align-items: center;
background: ${({ background }) => background};
@@ -16,22 +16,14 @@ const StyledIconContainer = styled.div<{
color: ${({ theme }) => theme.grayScale.gray1};
display: flex;
flex-shrink: 0;
height: ${({ size }) => (size === 'small' ? '14px' : 'auto')};
justify-content: center;
padding: ${({ size }) => (size === 'small' ? '0' : '5px')};
width: ${({ size }) => (size === 'small' ? '14px' : 'auto')};
padding: 5px;
`;
export const FileIcon = ({
fileCategory,
size = 'medium',
}: {
fileCategory: AttachmentFileCategory | FileCategory;
size?: FileIconSize;
}) => {
const useFileIconColors = () => {
const theme = useTheme();
const iconColors = {
return {
ARCHIVE: theme.color.gray,
AUDIO: theme.color.pink,
IMAGE: theme.color.amber,
@@ -41,13 +33,31 @@ export const FileIcon = ({
VIDEO: theme.color.purple,
OTHER: theme.color.gray,
};
};
export const FileIcon = ({
fileCategory,
size = 'medium',
}: {
fileCategory: AttachmentFileCategory | FileCategory;
size?: FileIconSize;
}) => {
const theme = useTheme();
const iconColors = useFileIconColors();
const Icon = IconMapping[fileCategory];
if (size === 'small') {
return (
<AvatarOrIcon
Icon={Icon}
IconBackgroundColor={iconColors[fileCategory] ?? theme.color.gray}
/>
);
}
return (
<StyledIconContainer
background={iconColors[fileCategory] ?? theme.color.gray}
size={size}
>
{Icon && <Icon size={theme.icon.size.sm} stroke={theme.icon.stroke.sm} />}
</StyledIconContainer>
@@ -1,7 +1,12 @@
import { getLinkToShowPage } from '@/object-metadata/utils/getLinkToShowPage';
import { t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { AvatarChip, Chip, ChipVariant, LinkChip } from 'twenty-ui/components';
import {
AvatarOrIcon,
Chip,
ChipVariant,
LinkChip,
} from 'twenty-ui/components';
type MentionRecordChipProps = {
recordId: string;
@@ -50,7 +55,7 @@ export const MentionRecordChip = ({
variant={ChipVariant.Highlighted}
className={className}
leftComponent={
<AvatarChip
<AvatarOrIcon
placeholder={label}
placeholderColorSeed={recordId}
avatarType="rounded"
@@ -1,5 +1,5 @@
import { forwardRef } from 'react';
import { AvatarChip } from 'twenty-ui/components';
import { AvatarOrIcon } from 'twenty-ui/components';
import { MenuItemSuggestion } from 'twenty-ui/navigation';
import type { MentionSearchResult } from '@/mention/types/MentionSearchResult';
@@ -16,7 +16,7 @@ const renderItem = (
) => (
<MenuItemSuggestion
LeftIcon={() => (
<AvatarChip
<AvatarOrIcon
placeholder={item.label}
placeholderColorSeed={item.recordId}
avatarType="rounded"
@@ -11,7 +11,7 @@ import { t } from '@lingui/core/macro';
import { type MouseEvent } from 'react';
import { isDefined } from 'twenty-shared/utils';
import {
AvatarChip,
AvatarOrIcon,
Chip,
type ChipSize,
ChipVariant,
@@ -91,7 +91,7 @@ export const RecordChip = ({
variant={ChipVariant.Transparent}
leftComponent={
isIconHidden ? null : (
<AvatarChip
<AvatarOrIcon
placeholder={recordChipData.name}
placeholderColorSeed={record.id}
avatarType={recordChipData.avatarType}
@@ -112,7 +112,7 @@ export const RecordChip = ({
isLabelHidden={isLabelHidden}
leftComponent={
isIconHidden ? null : (
<AvatarChip
<AvatarOrIcon
placeholder={recordChipData.name}
placeholderColorSeed={record.id}
avatarType={recordChipData.avatarType}
@@ -22,7 +22,7 @@ import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomState
import { useState } from 'react';
import { getImageAbsoluteURI, isDefined } from 'twenty-shared/utils';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { AvatarChip, Chip } from 'twenty-ui/components';
import { AvatarOrIcon, Chip } from 'twenty-ui/components';
import {
H2Title,
IconEyeShare,
@@ -151,7 +151,7 @@ export const SettingsAdminWorkspaceContent = ({
label={activeWorkspace?.name ?? ''}
emptyLabel={t`Untitled`}
leftComponent={
<AvatarChip
<AvatarOrIcon
avatarUrl={
getImageAbsoluteURI({
imageUrl: isNonEmptyString(activeWorkspace?.logo)
@@ -3,7 +3,7 @@ import { type FieldActorValue } from '@/object-record/record-field/ui/types/Fiel
import { t } from '@lingui/core/macro';
import { useMemo } from 'react';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import { AvatarChip, Chip } from 'twenty-ui/components';
import { AvatarOrIcon, Chip } from 'twenty-ui/components';
import {
IconApi,
IconCalendar,
@@ -79,7 +79,7 @@ export const ActorDisplay = ({
label={name ?? ''}
emptyLabel={t`Untitled`}
leftComponent={
<AvatarChip
<AvatarOrIcon
placeholderColorSeed={workspaceMemberId ?? undefined}
avatarType={workspaceMemberId ? 'rounded' : 'squared'}
placeholder={name}
@@ -1,63 +0,0 @@
import { styled } from '@linaria/react';
import { isNonEmptyString } from '@sniptt/guards';
import { Fragment } from 'react/jsx-runtime';
import { isDefined } from 'twenty-shared/utils';
import { Chip, ChipVariant } from '@ui/components/chip/Chip';
import { themeCssVariables } from '@ui/theme';
const StyledIconsContainer = styled.div`
align-items: center;
display: flex;
`;
const StyledChipContainer = styled.div`
display: inline-flex;
font-size: ${themeCssVariables.font.size.sm};
`;
export type MultipleAvatarChipProps = {
Icons: React.ReactNode[];
text?: string;
onClick?: () => void;
testId?: string;
maxWidth?: number;
forceEmptyText?: boolean;
variant?: ChipVariant;
rightComponent?: React.ReactNode;
emptyLabel?: string;
};
export const MultipleAvatarChip = ({
Icons,
text,
onClick,
testId,
maxWidth,
rightComponent,
variant = ChipVariant.Static,
forceEmptyText = false,
emptyLabel,
}: MultipleAvatarChipProps) => {
return (
<StyledChipContainer onClick={onClick} data-testid={testId}>
<Chip
label={text || ''}
forceEmptyText={forceEmptyText}
isLabelHidden={!isNonEmptyString(text) && forceEmptyText}
variant={variant}
leftComponent={
<StyledIconsContainer>
{Icons.map((Icon, index) => (
<Fragment key={index}>{Icon}</Fragment>
))}
</StyledIconsContainer>
}
rightComponent={rightComponent}
clickable={isDefined(onClick)}
maxWidth={maxWidth}
emptyLabel={emptyLabel}
/>
</StyledChipContainer>
);
};
@@ -1,33 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { MultipleAvatarChip } from '@ui/components';
import { IconBuildingSkyscraper, IconUser } from '@ui/display';
import { ComponentDecorator } from '@ui/testing';
const meta: Meta<typeof MultipleAvatarChip> = {
title: 'UI/Components/MultipleAvatarChip',
component: MultipleAvatarChip,
decorators: [ComponentDecorator],
};
export default meta;
type Story = StoryObj<typeof MultipleAvatarChip>;
export const SingleIcon: Story = {
args: {
Icons: [<IconUser size={16} />],
text: 'Person',
},
};
export const MultipleIcons: Story = {
args: {
Icons: [<IconUser size={16} />, <IconBuildingSkyscraper size={16} />],
text: 'Person & Company',
},
};
export const IconsOnly: Story = {
args: {
Icons: [<IconUser size={16} />, <IconBuildingSkyscraper size={16} />],
},
};
@@ -2,7 +2,7 @@ import { styled } from '@linaria/react';
import { Avatar } from '@ui/display/avatar/components/Avatar';
import { type AvatarType } from '@ui/display/avatar/types/AvatarType';
import { type IconComponent } from '@ui/display/icon/types/IconComponent';
import { ThemeContext, themeCssVariables } from '@ui/theme';
import { ThemeContext } from '@ui/theme';
import { type Nullable } from '@ui/utilities';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
@@ -19,24 +19,14 @@ const StyledIconWithBackgroundContainer = styled.div<{
background-color: ${({ backgroundColor }) => backgroundColor};
`;
const StyledAvatarChipWrapper = styled.div<{
const StyledAvatarOrIconWrapper = styled.div<{
isClickable: boolean;
divider: AvatarChipProps['divider'];
}>`
border-left: ${({ divider }) =>
divider === 'left'
? `1px solid ${themeCssVariables.border.color.light}`
: 'none'};
border-right: ${({ divider }) =>
divider === 'right'
? `1px solid ${themeCssVariables.border.color.light}`
: 'none'};
cursor: ${({ isClickable }) => (isClickable ? 'pointer' : 'inherit')};
display: flex;
`;
export type AvatarChipProps = {
export type AvatarOrIconProps = {
placeholder?: string;
avatarUrl?: string;
avatarType?: Nullable<AvatarType>;
@@ -45,11 +35,10 @@ export type AvatarChipProps = {
IconBackgroundColor?: string;
isIconInverted?: boolean;
placeholderColorSeed?: string;
divider?: 'right' | 'left';
onClick?: () => void;
};
export const AvatarChip = ({
export const AvatarOrIcon = ({
Icon,
placeholderColorSeed,
avatarType,
@@ -59,8 +48,7 @@ export const AvatarChip = ({
IconColor,
IconBackgroundColor,
onClick,
divider,
}: AvatarChipProps) => {
}: AvatarOrIconProps) => {
const { theme } = useContext(ThemeContext);
if (!isDefined(Icon)) {
return (
@@ -79,9 +67,8 @@ export const AvatarChip = ({
if (isIconInverted || isDefined(IconBackgroundColor)) {
return (
<StyledAvatarChipWrapper
<StyledAvatarOrIconWrapper
isClickable={isClickable}
divider={divider}
onClick={onClick}
>
<StyledIconWithBackgroundContainer
@@ -95,14 +82,13 @@ export const AvatarChip = ({
stroke={theme.icon.stroke.sm}
/>
</StyledIconWithBackgroundContainer>
</StyledAvatarChipWrapper>
</StyledAvatarOrIconWrapper>
);
}
return (
<StyledAvatarChipWrapper
<StyledAvatarOrIconWrapper
isClickable={isClickable}
divider={divider}
onClick={onClick}
>
<Icon
@@ -110,6 +96,6 @@ export const AvatarChip = ({
stroke={theme.icon.stroke.sm}
color={IconColor || 'currentColor'}
/>
</StyledAvatarChipWrapper>
</StyledAvatarOrIconWrapper>
);
};
@@ -1,16 +1,16 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { IconBuildingSkyscraper, IconUser } from '@ui/display';
import { ComponentDecorator, JotaiRootDecorator } from '@ui/testing';
import { AvatarChip } from '../AvatarChip';
import { AvatarOrIcon } from '../AvatarOrIcon';
const meta: Meta<typeof AvatarChip> = {
title: 'UI/Components/AvatarChip',
component: AvatarChip,
const meta: Meta<typeof AvatarOrIcon> = {
title: 'UI/Components/AvatarOrIcon',
component: AvatarOrIcon,
decorators: [ComponentDecorator, JotaiRootDecorator],
};
export default meta;
type Story = StoryObj<typeof AvatarChip>;
type Story = StoryObj<typeof AvatarOrIcon>;
export const Default: Story = {
args: {
@@ -47,26 +47,11 @@ export const WithInvertedIcon: Story = {
},
};
export const WithRightDivider: Story = {
args: {
placeholder: 'JD',
placeholderColorSeed: 'John Doe',
divider: 'right',
},
};
export const WithLeftDivider: Story = {
args: {
Icon: IconUser,
divider: 'left',
},
};
export const Clickable: Story = {
args: {
placeholder: 'JD',
placeholderColorSeed: 'John Doe',
onClick: () => alert('AvatarChip clicked'),
onClick: () => alert('AvatarOrIcon clicked'),
},
};
@@ -74,6 +59,6 @@ export const ClickableIcon: Story = {
args: {
Icon: IconBuildingSkyscraper,
isIconInverted: true,
onClick: () => alert('Icon AvatarChip clicked'),
onClick: () => alert('Icon AvatarOrIcon clicked'),
},
};
@@ -33,6 +33,7 @@ export type ChipProps = {
accent?: ChipAccent;
leftComponent?: ReactNode | null;
rightComponent?: (() => ReactNode) | ReactNode | null;
rightComponentDivider?: boolean;
className?: string;
forceEmptyText?: boolean;
emptyLabel?: string;
@@ -131,16 +132,32 @@ const StyledContainer = styled.div<
: 'var(--chip-horizontal-padding)'};
`;
const StyledRightComponentDivider = styled.div`
border-left: 1px solid ${themeCssVariables.border.color.light};
align-self: stretch;
`;
const renderRightComponent = (
rightComponent: (() => ReactNode) | ReactNode | null,
rightComponentDivider?: boolean,
) => {
if (!rightComponent) {
return null;
}
return typeof rightComponent === 'function'
? rightComponent()
: rightComponent;
const rendered =
typeof rightComponent === 'function' ? rightComponent() : rightComponent;
if (rightComponentDivider) {
return (
<>
<StyledRightComponentDivider />
{rendered}
</>
);
}
return rendered;
};
export const Chip = ({
@@ -152,6 +169,7 @@ export const Chip = ({
variant = ChipVariant.Regular,
leftComponent = null,
rightComponent = null,
rightComponentDivider = false,
accent = ChipAccent.TextPrimary,
className,
maxWidth,
@@ -177,7 +195,7 @@ export const Chip = ({
) : (
''
)}
{renderRightComponent(rightComponent)}
{renderRightComponent(rightComponent, rightComponentDivider)}
</StyledContainer>
);
};
@@ -37,6 +37,7 @@ export const LinkChip = ({
variant = ChipVariant.Regular,
leftComponent = null,
rightComponent = null,
rightComponentDivider = false,
accent = ChipAccent.TextPrimary,
className,
maxWidth,
@@ -72,6 +73,7 @@ export const LinkChip = ({
variant={variant}
leftComponent={leftComponent}
rightComponent={rightComponent}
rightComponentDivider={rightComponentDivider}
accent={accent}
className={className}
maxWidth={maxWidth}
@@ -1,4 +1,6 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { IconUser, IconX } from '@ui/display';
import { Avatar } from '@ui/display/avatar/components/Avatar';
import {
CatalogDecorator,
@@ -9,7 +11,7 @@ import {
import { Chip, ChipAccent, ChipSize, ChipVariant } from '../Chip';
const meta: Meta<typeof Chip> = {
title: 'UI/Display/Chip/Chip',
title: 'UI/Components/Chip/Chip',
component: Chip,
};
@@ -29,11 +31,51 @@ export const Default: Story = {
decorators: [ComponentDecorator],
};
export const WithLeftAvatar: Story = {
args: {
label: 'John Doe',
clickable: true,
variant: ChipVariant.Regular,
leftComponent: (
<Avatar
placeholder="JD"
placeholderColorSeed="John Doe"
size="sm"
type="rounded"
/>
),
},
decorators: [ComponentDecorator],
};
export const WithLeftIcon: Story = {
args: {
label: 'Company',
clickable: true,
variant: ChipVariant.Regular,
leftComponent: <IconUser size={14} />,
},
decorators: [ComponentDecorator],
};
export const EmptyLabel: Story = {
args: {
label: '',
clickable: true,
variant: ChipVariant.Regular,
leftComponent: (
<Avatar placeholder="?" placeholderColorSeed="empty" size="sm" />
),
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof Chip> = {
args: { clickable: true, label: 'Hello' },
argTypes: {
size: { control: false },
variant: { control: false },
accent: { control: false },
disabled: { control: false },
className: { control: false },
rightComponent: { control: false },
@@ -43,12 +85,6 @@ export const Catalog: CatalogStory<Story, typeof Chip> = {
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),
@@ -59,6 +95,21 @@ export const Catalog: CatalogStory<Story, typeof Chip> = {
values: Object.values(ChipSize),
props: (size: ChipSize) => ({ size }),
},
{
name: 'states',
values: ['default', 'hover', 'active', 'disabled'],
props: (state: string) => {
switch (state) {
case 'hover':
case 'active':
return { className: state };
case 'disabled':
return { disabled: true };
default:
return {};
}
},
},
{
name: 'accents',
values: Object.values(ChipAccent),
@@ -69,3 +120,79 @@ export const Catalog: CatalogStory<Story, typeof Chip> = {
},
decorators: [CatalogDecorator],
};
export const WithAvatarCatalog: CatalogStory<Story, typeof Chip> = {
args: {
clickable: true,
label: 'John Doe',
leftComponent: (
<Avatar
placeholder="JD"
placeholderColorSeed="John Doe"
size="sm"
type="rounded"
/>
),
},
argTypes: {
size: { control: false },
variant: { control: false },
accent: { control: false },
disabled: { control: false },
className: { control: false },
rightComponent: { control: false },
leftComponent: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.active'] },
catalog: {
dimensions: [
{
name: 'variants',
values: Object.values(ChipVariant),
props: (variant: ChipVariant) => ({ variant }),
},
{
name: 'sizes',
values: Object.values(ChipSize),
props: (size: ChipSize) => ({ size }),
},
{
name: 'states',
values: ['default', 'hover', 'active', 'disabled'],
props: (state: string) => {
switch (state) {
case 'hover':
case 'active':
return { className: state };
case 'disabled':
return { disabled: true };
default:
return {};
}
},
},
],
},
},
decorators: [CatalogDecorator],
};
export const WithRightComponentDivider: Story = {
args: {
label: 'document.pdf',
variant: ChipVariant.Static,
clickable: false,
leftComponent: (
<Avatar
placeholder="D"
placeholderColorSeed="document"
size="sm"
type="squared"
/>
),
rightComponent: <IconX size={14} />,
rightComponentDivider: true,
},
decorators: [ComponentDecorator],
};
@@ -1,12 +1,18 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { ComponentDecorator } from '@ui/testing';
import { IconUser, IconX } from '@ui/display';
import { Avatar } from '@ui/display/avatar/components/Avatar';
import {
CatalogDecorator,
type CatalogStory,
ComponentDecorator,
} from '@ui/testing';
import { MemoryRouter } from 'react-router-dom';
import { ChipAccent, ChipSize, ChipVariant } from '../Chip';
import { LinkChip } from '../LinkChip';
const meta: Meta<typeof LinkChip> = {
title: 'UI/Display/Chip/LinkChip',
title: 'UI/Components/Chip/LinkChip',
component: LinkChip,
decorators: [
(Story) => (
@@ -14,7 +20,6 @@ const meta: Meta<typeof LinkChip> = {
<Story />
</MemoryRouter>
),
ComponentDecorator,
],
};
@@ -29,4 +34,153 @@ export const Default: Story = {
variant: ChipVariant.Regular,
accent: ChipAccent.TextPrimary,
},
decorators: [ComponentDecorator],
};
export const WithAvatar: Story = {
args: {
label: 'John Doe',
to: '/users/john-doe',
variant: ChipVariant.Regular,
leftComponent: (
<Avatar
placeholder="JD"
placeholderColorSeed="John Doe"
size="sm"
type="rounded"
/>
),
},
decorators: [ComponentDecorator],
};
export const WithIcon: Story = {
args: {
label: 'Company',
to: '/companies/1',
variant: ChipVariant.Regular,
leftComponent: <IconUser size={14} />,
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof LinkChip> = {
args: { label: 'Link Chip', to: '/example' },
argTypes: {
size: { control: false },
variant: { control: false },
accent: { control: false },
className: { control: false },
rightComponent: { control: false },
leftComponent: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.active'] },
catalog: {
dimensions: [
{
name: 'variants',
values: Object.values(ChipVariant),
props: (variant: ChipVariant) => ({ variant }),
},
{
name: 'sizes',
values: Object.values(ChipSize),
props: (size: ChipSize) => ({ size }),
},
{
name: 'states',
values: ['default', 'hover', 'active'],
props: (state: string) => {
switch (state) {
case 'hover':
case 'active':
return { className: state };
default:
return {};
}
},
},
{
name: 'accents',
values: Object.values(ChipAccent),
props: (accent: ChipAccent) => ({ accent }),
},
],
},
},
decorators: [CatalogDecorator],
};
export const WithAvatarCatalog: CatalogStory<Story, typeof LinkChip> = {
args: {
label: 'John Doe',
to: '/users/john-doe',
leftComponent: (
<Avatar
placeholder="JD"
placeholderColorSeed="John Doe"
size="sm"
type="rounded"
/>
),
},
argTypes: {
size: { control: false },
variant: { control: false },
accent: { control: false },
className: { control: false },
rightComponent: { control: false },
leftComponent: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.active'] },
catalog: {
dimensions: [
{
name: 'variants',
values: Object.values(ChipVariant),
props: (variant: ChipVariant) => ({ variant }),
},
{
name: 'sizes',
values: Object.values(ChipSize),
props: (size: ChipSize) => ({ size }),
},
{
name: 'states',
values: ['default', 'hover', 'active'],
props: (state: string) => {
switch (state) {
case 'hover':
case 'active':
return { className: state };
default:
return {};
}
},
},
],
},
},
decorators: [CatalogDecorator],
};
export const WithRightComponentDivider: Story = {
args: {
label: 'document.pdf',
to: '/files/document.pdf',
variant: ChipVariant.Static,
leftComponent: (
<Avatar
placeholder="D"
placeholderColorSeed="document"
size="sm"
type="squared"
/>
),
rightComponent: <IconX size={14} />,
rightComponentDivider: true,
},
decorators: [ComponentDecorator],
};
+2 -4
View File
@@ -7,10 +7,8 @@
* |___/
*/
export type { AvatarChipProps } from './avatar-chip/AvatarChip';
export { AvatarChip } from './avatar-chip/AvatarChip';
export type { MultipleAvatarChipProps } from './avatar-chip/MultipleAvatarChip';
export { MultipleAvatarChip } from './avatar-chip/MultipleAvatarChip';
export type { AvatarOrIconProps } from './avatar-or-icon/AvatarOrIcon';
export { AvatarOrIcon } from './avatar-or-icon/AvatarOrIcon';
export type { ChipProps } from './chip/Chip';
export { ChipSize, ChipAccent, ChipVariant, Chip } from './chip/Chip';
export { LINK_CHIP_CLICK_OUTSIDE_ID } from './chip/constants/LinkChipClickOutsideId';
+8 -5
View File
@@ -35,11 +35,14 @@ export default defineConfig(() => {
projects: ['tsconfig.json'],
}),
svgr(),
wyw({
babelOptions: {
presets: ['@babel/preset-typescript', '@babel/preset-react'],
},
}),
{
...wyw({
babelOptions: {
presets: ['@babel/preset-typescript', '@babel/preset-react'],
},
}),
enforce: 'pre',
},
],
build: {
cssCodeSplit: false,
+8 -5
View File
@@ -81,11 +81,14 @@ export default defineConfig(({ command }) => {
svgr(),
dts(dtsConfig),
checker(checkersConfig),
wyw({
babelOptions: {
presets: ['@babel/preset-typescript', '@babel/preset-react'],
},
}),
{
...wyw({
babelOptions: {
presets: ['@babel/preset-typescript', '@babel/preset-react'],
},
}),
enforce: 'pre',
},
],
build: {
cssCodeSplit: false,