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:
+9
-4
@@ -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>
|
||||
|
||||
+2
-2
@@ -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}
|
||||
|
||||
+2
-2
@@ -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>
|
||||
);
|
||||
};
|
||||
-33
@@ -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} />],
|
||||
},
|
||||
};
|
||||
+9
-23
@@ -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>
|
||||
);
|
||||
};
|
||||
+7
-22
@@ -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],
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user