Add color to object icon picker in data model (#19368)

Closes [#2291](https://github.com/twentyhq/core-team-issues/issues/2291)
This commit is contained in:
Abdul Rahman
2026-04-08 06:57:08 +05:30
committed by GitHub
parent 5c8b6e395b
commit a0484f686a
12 changed files with 337 additions and 79 deletions
@@ -4,23 +4,17 @@ import { type FlatObjectMetadataItem } from '@/metadata-store/types/FlatObjectMe
import { isValidObjectNavigationMenuItem } from '@/navigation-menu-item/common/utils/isValidObjectNavigationMenuItem';
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/edit/hooks/useSelectedNavigationMenuItemEditItem';
import { useUpdateNavigationMenuItemInDraft } from '@/navigation-menu-item/edit/hooks/useUpdateNavigationMenuItemInDraft';
import { ThemeColorPickerMenu } from '@/ui/input/components/ThemeColorPickerMenu';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { useState } from 'react';
import { capitalize } from 'twenty-shared/utils';
import { IconColorSwatch } from 'twenty-ui/display';
import {
DEFAULT_COLOR_LABELS,
MenuItemSelectColor,
} from 'twenty-ui/navigation';
import { type ThemeColor, MAIN_COLOR_NAMES } from 'twenty-ui/theme';
import { DEFAULT_COLOR_LABELS } from 'twenty-ui/navigation';
import { type ThemeColor } from 'twenty-ui/theme';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const NAVIGATION_MENU_ITEM_COLOR_DROPDOWN_ID = 'navigation-menu-item-color';
const StyledMenuStyleText = styled.span`
@@ -44,20 +38,9 @@ export const SidePanelEditColorOption = ({
const { updateInDraft, applyChanges } = useUpdateMetadataStoreDraft();
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
const [searchValue, setSearchValue] = useState('');
const themeColor = color ?? 'gray';
const colorLabel = DEFAULT_COLOR_LABELS[themeColor] ?? capitalize(themeColor);
const query = searchValue.trim().toLowerCase();
const filteredColorNames = isNonEmptyString(query)
? MAIN_COLOR_NAMES.filter(
(colorName) =>
colorName.toLowerCase().includes(query) ||
(DEFAULT_COLOR_LABELS[colorName] ?? '').toLowerCase().includes(query),
)
: MAIN_COLOR_NAMES;
const handleSelectColor = (selectedColor: ThemeColor) => {
updateNavigationMenuItemInDraft(navigationMenuItemId, {
color: selectedColor,
@@ -86,23 +69,10 @@ export const SidePanelEditColorOption = ({
RightComponent={<StyledMenuStyleText>{colorLabel}</StyledMenuStyleText>}
dropdownComponents={
<DropdownContent>
<DropdownMenuSearchInput
placeholder={t`Search colors...`}
value={searchValue}
onChange={(event) => setSearchValue(event.target.value)}
<ThemeColorPickerMenu
selectedColor={themeColor}
onSelectColor={handleSelectColor}
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
{filteredColorNames.map((colorName) => (
<MenuItemSelectColor
key={colorName}
onClick={() => handleSelectColor(colorName)}
color={colorName}
selected={colorName === themeColor}
colorLabels={DEFAULT_COLOR_LABELS}
/>
))}
</DropdownMenuItemsContainer>
</DropdownContent>
}
/>
@@ -1,6 +1,7 @@
import { isDDLLockedState } from '@/client-config/states/isDDLLockedState';
import { parseThemeColor } from '@/navigation-menu-item/common/utils/parseThemeColor';
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { isDDLLockedState } from '@/client-config/states/isDDLLockedState';
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
import { computeUpdatedNavigationMemorizedUrlAfterObjectNamePluralChange } from '@/settings/data-model/object-details/utils/computeUpdatedNavigationMemorizedUrlAfterObjectNamePluralChange';
import { SettingsDataModelObjectAboutForm } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm';
@@ -60,6 +61,9 @@ export const SettingsUpdateDataModelObjectAboutForm = ({
labelSingular,
namePlural,
nameSingular,
...(objectMetadataItem.isCustom
? { color: parseThemeColor(objectMetadataItem.color) }
: {}),
},
});
@@ -99,6 +103,14 @@ export const SettingsUpdateDataModelObjectAboutForm = ({
labelSingular: updatedObject?.data?.updateOneObject.labelSingular,
namePlural: updatedObject?.data?.updateOneObject.namePlural,
nameSingular: updatedObject?.data?.updateOneObject.nameSingular,
...(objectMetadataItem.isCustom
? {
color: parseThemeColor(
updatedObject?.data?.updateOneObject.color ??
objectMetadataItem.color,
),
}
: {}),
});
} else {
formConfig.reset(formValues);
@@ -134,6 +146,7 @@ export const SettingsUpdateDataModelObjectAboutForm = ({
nameSingular: _nameSingular,
namePlural: _namePlural,
isLabelSyncedWithName: _isLabelSyncedWithName,
color: _color,
...payloadWithoutNames
} = updatePayload;
@@ -1,3 +1,4 @@
import { parseThemeColor } from '@/navigation-menu-item/common/utils/parseThemeColor';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { AdvancedSettingsWrapper } from '@/settings/components/AdvancedSettingsWrapper';
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
@@ -7,9 +8,9 @@ import { IconPicker } from '@/ui/input/components/IconPicker';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { TextArea } from '@/ui/input/components/TextArea';
import { styled } from '@linaria/react';
import { useContext } from 'react';
import { useLingui } from '@lingui/react/macro';
import { plural } from 'pluralize';
import { useContext } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { SettingsPath } from 'twenty-shared/types';
import { capitalize, isDefined } from 'twenty-shared/utils';
@@ -127,8 +128,12 @@ export const SettingsDataModelObjectAboutForm = ({
const labelPlural = watch('labelPlural');
const isStandardObject =
isDefined(objectMetadataItem?.isCustom) && !objectMetadataItem.isCustom;
const showObjectColorInIconPicker =
!isStandardObject &&
(!isDefined(objectMetadataItem) || objectMetadataItem.isCustom);
watch('description');
watch('icon');
const objectIconColor = watch('color');
const apiNameTooltipText =
!isDefined(objectMetadataItem) || objectMetadataItem.isCustom
@@ -186,6 +191,25 @@ export const SettingsDataModelObjectAboutForm = ({
<IconPicker
selectedIconKey={value}
disabled={disableEdition}
dropdownId={
isDefined(objectMetadataItem)
? `settings-object-about-icon-${objectMetadataItem.id}`
: 'settings-new-object-about-icon'
}
iconColorPicker={
showObjectColorInIconPicker
? {
selectedColor: parseThemeColor(objectIconColor),
onColorChange: (nextColor) => {
setValue('color', nextColor, {
shouldDirty: true,
shouldValidate: true,
});
onNewDirtyField?.();
},
}
: undefined
}
onChange={({ iconKey }) => {
if (disableEdition) {
return;
@@ -1,5 +1,44 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`settingsDataModelObjectAboutFormSchema fails when color is not a valid theme color 1`] = `
[ZodError: [
{
"code": "invalid_value",
"values": [
"red",
"ruby",
"crimson",
"tomato",
"orange",
"amber",
"yellow",
"lime",
"grass",
"green",
"jade",
"mint",
"turquoise",
"cyan",
"sky",
"blue",
"iris",
"violet",
"purple",
"plum",
"pink",
"bronze",
"gold",
"brown",
"gray"
],
"path": [
"color"
],
"message": "Invalid option: expected one of \\"red\\"|\\"ruby\\"|\\"crimson\\"|\\"tomato\\"|\\"orange\\"|\\"amber\\"|\\"yellow\\"|\\"lime\\"|\\"grass\\"|\\"green\\"|\\"jade\\"|\\"mint\\"|\\"turquoise\\"|\\"cyan\\"|\\"sky\\"|\\"blue\\"|\\"iris\\"|\\"violet\\"|\\"purple\\"|\\"plum\\"|\\"pink\\"|\\"bronze\\"|\\"gold\\"|\\"brown\\"|\\"gray\\""
}
]]
`;
exports[`settingsDataModelObjectAboutFormSchema fails when isLabelSyncedWithName is not a boolean 1`] = `
[ZodError: [
{
@@ -6,6 +6,7 @@ import { type EachTestingContext } from 'twenty-shared/testing';
describe('settingsDataModelObjectAboutFormSchema', () => {
const validInput: SettingsDataModelObjectAboutFormValues = {
color: 'gray',
description: 'A valid description',
icon: 'IconName',
labelPlural: 'Labels Plural',
@@ -148,6 +149,16 @@ describe('settingsDataModelObjectAboutFormSchema', () => {
expectedSuccess: false,
},
},
{
title: 'fails when color is not a valid theme color',
context: {
input: {
...validInput,
color: 'not-a-color',
},
expectedSuccess: false,
},
},
];
test.each([...passingTestsUseCase, ...failsValidationTestsUseCase])(
@@ -1,5 +1,6 @@
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { t } from '@lingui/core/macro';
import { themeColorSchema } from 'twenty-ui/utilities';
import { type ZodType, z } from 'zod';
import { type ReadonlyKeysArray } from '~/types/ReadonlyKeysArray';
import { zodNonEmptyString } from '~/types/ZodNonEmptyString';
@@ -8,6 +9,7 @@ import { camelCaseStringSchema } from '~/utils/validation-schemas/camelCaseStrin
type ZodTypeSettingsDataModelFormFields = ZodType<
Pick<
EnrichedObjectMetadataItem,
| 'color'
| 'labelSingular'
| 'labelPlural'
| 'description'
@@ -18,6 +20,7 @@ type ZodTypeSettingsDataModelFormFields = ZodType<
> & { skipNameField?: boolean }
>;
const settingsDataModelFormFieldsSchema = z.object({
color: themeColorSchema.optional(),
description: z.string().nullish(),
icon: z.string().optional(),
labelSingular: zodNonEmptyString,
@@ -1,39 +1,52 @@
import { isDefined } from 'twenty-shared/utils';
import { css } from '@linaria/core';
import { styled } from '@linaria/react';
import React, { type ReactNode, useCallback, useMemo, useState } from 'react';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { arrayToChunks } from '~/utils/array/arrayToChunks';
import { ICON_PICKER_DROPDOWN_CONTENT_WIDTH } from '@/ui/input/components/constants/IconPickerDropdownContentWidth';
import { IconPickerScrollEffect } from '@/ui/input/effect-components/IconPickerScrollEffect';
import {
ICON_PICKER_DEFAULT_VISIBLE_COUNT,
iconPickerVisibleCountState,
} from '@/ui/input/states/iconPickerVisibleCountState';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
import { type DropdownOffset } from '@/ui/layout/dropdown/types/DropdownOffset';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { t } from '@lingui/core/macro';
import { useStore } from 'jotai';
import { IconApps, type IconComponent, useIcons } from 'twenty-ui/display';
import React, {
type ReactNode,
useCallback,
useContext,
useMemo,
useState,
} from 'react';
import { isDefined } from 'twenty-shared/utils';
import {
ColorSample,
IconApps,
type IconComponent,
useIcons,
} from 'twenty-ui/display';
import {
IconButton,
type IconButtonSize,
type IconButtonVariant,
LightIconButton,
} from 'twenty-ui/input';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { type ThemeColor } from 'twenty-ui/theme';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import { ICON_PICKER_DROPDOWN_CONTENT_WIDTH } from '@/ui/input/components/constants/IconPickerDropdownContentWidth';
import { ThemeColorPickerMenu } from '@/ui/input/components/ThemeColorPickerMenu';
import { IconPickerScrollEffect } from '@/ui/input/effect-components/IconPickerScrollEffect';
import {
ICON_PICKER_DEFAULT_VISIBLE_COUNT,
iconPickerVisibleCountState,
} from '@/ui/input/states/iconPickerVisibleCountState';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { type DropdownOffset } from '@/ui/layout/dropdown/types/DropdownOffset';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { ClickOutsideListenerContext } from '@/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
import { arrayToChunks } from '~/utils/array/arrayToChunks';
export type IconPickerProps = {
disabled?: boolean;
@@ -50,6 +63,96 @@ export type IconPickerProps = {
dropdownWidth?: number;
dropdownOffset?: DropdownOffset;
maxIconsVisible?: number;
iconColorPicker?: {
selectedColor: ThemeColor;
onColorChange: (color: ThemeColor) => void;
};
};
const StyledIconPickerSearchRow = styled.div`
align-items: center;
box-sizing: border-box;
display: flex;
gap: ${themeCssVariables.spacing[1]};
padding-right: ${themeCssVariables.spacing[2]};
width: 100%;
`;
type IconPickerSearchRowProps = {
closeDropdown: (dropdownId: string) => void;
dropdownWidth: number | undefined;
iconColorPicker: IconPickerProps['iconColorPicker'];
iconColorPickerDropdownId: string;
onSearchChange: (searchString: string) => void;
};
const IconPickerSearchRow = ({
closeDropdown,
dropdownWidth,
iconColorPicker,
iconColorPickerDropdownId,
onSearchChange,
}: IconPickerSearchRowProps) => {
const searchInput = (
<DropdownMenuSearchInput
placeholder={t`Search icon`}
autoFocus
onChange={(event) => {
onSearchChange(event.target.value);
}}
/>
);
if (!isDefined(iconColorPicker)) {
return searchInput;
}
return (
<StyledIconPickerSearchRow>
{searchInput}
<ClickOutsideListenerContext.Provider
value={{
excludedClickOutsideId: iconColorPickerDropdownId,
}}
>
<Dropdown
dropdownId={iconColorPickerDropdownId}
dropdownOffset={{
x: 24,
y: -24,
}}
dropdownPlacement="right-start"
clickableComponent={
<LightIconButton
accent="secondary"
Icon={() => (
<ColorSample
colorName={iconColorPicker.selectedColor}
variant="circle"
/>
)}
size="small"
/>
}
dropdownComponents={
<DropdownContent
widthInPixels={
dropdownWidth || ICON_PICKER_DROPDOWN_CONTENT_WIDTH
}
>
<ThemeColorPickerMenu
selectedColor={iconColorPicker.selectedColor}
onSelectColor={(nextColor) => {
iconColorPicker.onColorChange(nextColor);
closeDropdown(iconColorPickerDropdownId);
}}
/>
</DropdownContent>
}
/>
</ClickOutsideListenerContext.Provider>
</StyledIconPickerSearchRow>
);
};
const StyledMenuIconItemsContainer = styled.div`
@@ -130,6 +233,7 @@ type IconPickerIconProps = {
selectedIconKey?: string;
Icon: IconComponent;
focusedIconKey?: string;
color?: ThemeColor;
};
const IconPickerIcon = ({
@@ -138,7 +242,10 @@ const IconPickerIcon = ({
selectedIconKey,
Icon,
focusedIconKey,
color,
}: IconPickerIconProps) => {
const { theme } = useContext(ThemeContext);
const selectedItemId = useAtomComponentStateValue(
selectedItemIdComponentState,
iconKey,
@@ -154,7 +261,13 @@ const IconPickerIcon = ({
title={iconKey}
isSelected={iconKey === selectedIconKey || !!selectedItemId}
isFocused={iconKey === focusedIconKey}
Icon={Icon}
Icon={(iconProps) => (
<Icon
// oxlint-disable-next-line react/jsx-props-no-spreading
{...iconProps}
color={isDefined(color) ? theme.color[color] : iconProps.color}
/>
)}
onClick={onSelect}
/>
</SelectableListItem>
@@ -177,6 +290,7 @@ export const IconPicker = ({
dropdownWidth,
dropdownOffset,
maxIconsVisible,
iconColorPicker,
}: IconPickerProps) => {
const [searchString, setSearchString] = useState('');
@@ -284,7 +398,23 @@ export const IconPicker = ({
[matchingSearchIconKeys],
);
const icon = selectedIconKey ? getIcon(selectedIconKey) : IconApps;
const { theme } = useContext(ThemeContext);
const BaseIcon = selectedIconKey ? getIcon(selectedIconKey) : IconApps;
const displayIcon: IconComponent = !isDefined(iconColorPicker)
? BaseIcon
: (iconProps) => (
<BaseIcon
className={iconProps.className}
color={theme.color[iconColorPicker.selectedColor]}
size={iconProps.size}
stroke={iconProps.stroke}
style={iconProps.style}
/>
);
const iconColorPickerDropdownId = `${dropdownId}-icon-color-picker`;
const selectableListInstanceId = 'icon-list';
@@ -307,12 +437,15 @@ export const IconPicker = ({
<Dropdown
dropdownId={dropdownId}
dropdownOffset={dropdownOffset}
excludedClickOutsideIds={
isDefined(iconColorPicker) ? [iconColorPickerDropdownId] : undefined
}
clickableComponent={
clickableComponent ?? (
<IconButton
ariaLabel={t`Click to select icon ${iconAriaLabel}`}
disabled={disabled}
Icon={icon}
Icon={displayIcon}
variant={variant}
size={size}
/>
@@ -330,12 +463,12 @@ export const IconPicker = ({
selectableItemIdMatrix={iconKeys2d}
focusId={dropdownId}
>
<DropdownMenuSearchInput
placeholder={t`Search icon`}
autoFocus
onChange={(event) => {
setSearchString(event.target.value);
}}
<IconPickerSearchRow
closeDropdown={closeDropdown}
dropdownWidth={dropdownWidth}
iconColorPicker={iconColorPicker}
iconColorPickerDropdownId={iconColorPickerDropdownId}
onSearchChange={setSearchString}
/>
<DropdownMenuSeparator />
<div
@@ -355,6 +488,7 @@ export const IconPicker = ({
selectedIconKey={selectedIconKey}
Icon={getIcon(iconKey)}
focusedIconKey={focusedIconKey}
color={iconColorPicker?.selectedColor}
/>
))}
</StyledMenuIconItemsContainer>
@@ -0,0 +1,55 @@
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { useState } from 'react';
import {
DEFAULT_COLOR_LABELS,
MenuItemSelectColor,
} from 'twenty-ui/navigation';
import { type ThemeColor, MAIN_COLOR_NAMES } from 'twenty-ui/theme';
type ThemeColorPickerMenuProps = {
selectedColor: ThemeColor;
onSelectColor: (color: ThemeColor) => void;
};
export const ThemeColorPickerMenu = ({
selectedColor,
onSelectColor,
}: ThemeColorPickerMenuProps) => {
const [searchValue, setSearchValue] = useState('');
const query = searchValue.trim().toLowerCase();
const filteredColorNames = isNonEmptyString(query)
? MAIN_COLOR_NAMES.filter(
(colorName) =>
colorName.toLowerCase().includes(query) ||
(DEFAULT_COLOR_LABELS[colorName] ?? '').toLowerCase().includes(query),
)
: MAIN_COLOR_NAMES;
return (
<>
<DropdownMenuSearchInput
placeholder={t`Search colors...`}
value={searchValue}
onChange={(event) => setSearchValue(event.target.value)}
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
{filteredColorNames.map((colorName) => (
<MenuItemSelectColor
key={colorName}
onClick={() => onSelectColor(colorName)}
color={colorName}
selected={colorName === selectedColor}
colorLabels={DEFAULT_COLOR_LABELS}
/>
))}
</DropdownMenuItemsContainer>
</>
);
};
@@ -34,6 +34,7 @@ export const SettingsNewObject = () => {
mode: 'onChange',
resolver: zodResolver(settingsDataModelObjectAboutFormSchema),
defaultValues: {
color: 'gray',
isLabelSyncedWithName:
SETTINGS_OBJECT_MODEL_IS_LABEL_SYNCED_WITH_NAME_LABEL_DEFAULT_VALUE,
},
@@ -1,10 +1,10 @@
import { styled } from '@linaria/react';
import { isDefined } from 'twenty-shared/utils';
import { type ThemeColor } from '@ui/theme';
import { themeCssVariables } from '@ui/theme-constants';
import { isDefined } from 'twenty-shared/utils';
export type ColorSampleVariant = 'default' | 'pipeline';
export type ColorSampleVariant = 'circle' | 'default' | 'pipeline';
type StyledColorSampleProps = {
colorName: ThemeColor;
@@ -32,9 +32,13 @@ const StyledColorSample = styled.div<StyledColorSampleProps>`
background-color: ${({ colorName, color }) => getColor(colorName, color)};
border: ${({ variant, colorName }) =>
variant === 'pipeline' ? '0' : `1px solid ${getBorderColor(colorName)}`};
border-radius: 60px;
border-radius: ${({ variant }) => (variant === 'circle' ? '50%' : '60px')};
flex-shrink: 0;
height: ${themeCssVariables.spacing[4]};
width: ${themeCssVariables.spacing[3]};
width: ${({ variant }) =>
variant === 'circle'
? themeCssVariables.spacing[4]
: themeCssVariables.spacing[3]};
align-items: ${({ variant }) =>
variant === 'pipeline' ? 'center' : 'initial'};
display: ${({ variant }) => (variant === 'pipeline' ? 'flex' : 'block')};
@@ -19,3 +19,7 @@ export const Default: Story = {};
export const Pipeline: Story = {
args: { variant: 'pipeline' },
};
export const Circle: Story = {
args: { variant: 'circle' },
};
@@ -67,7 +67,7 @@ export const Catalog: CatalogStory<Story, typeof MenuItemSelectColor> = {
},
{
name: 'variant',
values: ['default', 'pipeline'],
values: ['circle', 'default', 'pipeline'],
props: (variant: ColorSampleVariant) => ({ variant }),
labels: (variant: ColorSampleVariant) => variant,
},