From a0484f686ac1a7b31cc4c3f36b92f72735bb8498 Mon Sep 17 00:00:00 2001
From: Abdul Rahman <81605929+abdulrahmancodes@users.noreply.github.com>
Date: Wed, 8 Apr 2026 06:57:08 +0530
Subject: [PATCH] Add color to object icon picker in data model (#19368)
Closes [#2291](https://github.com/twentyhq/core-team-issues/issues/2291)
---
.../components/SidePanelEditColorOption.tsx | 44 +---
...SettingsUpdateDataModelObjectAboutForm.tsx | 15 +-
.../SettingsDataModelObjectAboutForm.tsx | 26 ++-
...ataModelObjectAboutFormSchema.test.ts.snap | 39 ++++
...ingsDataModelObjectAboutFormSchema.test.ts | 11 +
.../settingsDataModelObjectAboutFormSchema.ts | 3 +
.../ui/input/components/IconPicker.tsx | 204 +++++++++++++++---
.../input/components/ThemeColorPickerMenu.tsx | 55 +++++
.../settings/data-model/SettingsNewObject.tsx | 1 +
.../display/color/components/ColorSample.tsx | 12 +-
.../__stories__/ColorSample.stories.tsx | 4 +
.../MenuItemSelectColor.stories.tsx | 2 +-
12 files changed, 337 insertions(+), 79 deletions(-)
create mode 100644 packages/twenty-front/src/modules/ui/input/components/ThemeColorPickerMenu.tsx
diff --git a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelEditColorOption.tsx b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelEditColorOption.tsx
index 2389abdecd..b4f93ef891 100644
--- a/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelEditColorOption.tsx
+++ b/packages/twenty-front/src/modules/navigation-menu-item/edit/side-panel/components/SidePanelEditColorOption.tsx
@@ -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={{colorLabel}}
dropdownComponents={
- setSearchValue(event.target.value)}
+
-
-
- {filteredColorNames.map((colorName) => (
- handleSelectColor(colorName)}
- color={colorName}
- selected={colorName === themeColor}
- colorLabels={DEFAULT_COLOR_LABELS}
- />
- ))}
-
}
/>
diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm.tsx
index 154453b9c5..a794bcd54c 100644
--- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm.tsx
+++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm.tsx
@@ -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;
diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx
index 233525938e..72643819f2 100644
--- a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx
+++ b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx
@@ -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 = ({
{
+ setValue('color', nextColor, {
+ shouldDirty: true,
+ shouldValidate: true,
+ });
+ onNewDirtyField?.();
+ },
+ }
+ : undefined
+ }
onChange={({ iconKey }) => {
if (disableEdition) {
return;
diff --git a/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/__snapshots__/settingsDataModelObjectAboutFormSchema.test.ts.snap b/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/__snapshots__/settingsDataModelObjectAboutFormSchema.test.ts.snap
index 346d195422..568598a3aa 100644
--- a/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/__snapshots__/settingsDataModelObjectAboutFormSchema.test.ts.snap
+++ b/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/__snapshots__/settingsDataModelObjectAboutFormSchema.test.ts.snap
@@ -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: [
{
diff --git a/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/settingsDataModelObjectAboutFormSchema.test.ts b/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/settingsDataModelObjectAboutFormSchema.test.ts
index a7e282cbfe..89d55c9197 100644
--- a/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/settingsDataModelObjectAboutFormSchema.test.ts
+++ b/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/settingsDataModelObjectAboutFormSchema.test.ts
@@ -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])(
diff --git a/packages/twenty-front/src/modules/settings/data-model/validation-schemas/settingsDataModelObjectAboutFormSchema.ts b/packages/twenty-front/src/modules/settings/data-model/validation-schemas/settingsDataModelObjectAboutFormSchema.ts
index ec60508a31..44bf1d8e46 100644
--- a/packages/twenty-front/src/modules/settings/data-model/validation-schemas/settingsDataModelObjectAboutFormSchema.ts
+++ b/packages/twenty-front/src/modules/settings/data-model/validation-schemas/settingsDataModelObjectAboutFormSchema.ts
@@ -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,
diff --git a/packages/twenty-front/src/modules/ui/input/components/IconPicker.tsx b/packages/twenty-front/src/modules/ui/input/components/IconPicker.tsx
index 37850c18ff..e2bac03182 100644
--- a/packages/twenty-front/src/modules/ui/input/components/IconPicker.tsx
+++ b/packages/twenty-front/src/modules/ui/input/components/IconPicker.tsx
@@ -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 = (
+ {
+ onSearchChange(event.target.value);
+ }}
+ />
+ );
+
+ if (!isDefined(iconColorPicker)) {
+ return searchInput;
+ }
+
+ return (
+
+ {searchInput}
+
+ (
+
+ )}
+ size="small"
+ />
+ }
+ dropdownComponents={
+
+ {
+ iconColorPicker.onColorChange(nextColor);
+ closeDropdown(iconColorPickerDropdownId);
+ }}
+ />
+
+ }
+ />
+
+
+ );
};
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) => (
+
+ )}
onClick={onSelect}
/>
@@ -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) => (
+
+ );
+
+ const iconColorPickerDropdownId = `${dropdownId}-icon-color-picker`;
const selectableListInstanceId = 'icon-list';
@@ -307,12 +437,15 @@ export const IconPicker = ({
@@ -330,12 +463,12 @@ export const IconPicker = ({
selectableItemIdMatrix={iconKeys2d}
focusId={dropdownId}
>
- {
- setSearchString(event.target.value);
- }}
+
))}
diff --git a/packages/twenty-front/src/modules/ui/input/components/ThemeColorPickerMenu.tsx b/packages/twenty-front/src/modules/ui/input/components/ThemeColorPickerMenu.tsx
new file mode 100644
index 0000000000..c8e9410bf9
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/input/components/ThemeColorPickerMenu.tsx
@@ -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 (
+ <>
+ setSearchValue(event.target.value)}
+ />
+
+
+ {filteredColorNames.map((colorName) => (
+ onSelectColor(colorName)}
+ color={colorName}
+ selected={colorName === selectedColor}
+ colorLabels={DEFAULT_COLOR_LABELS}
+ />
+ ))}
+
+ >
+ );
+};
diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsNewObject.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsNewObject.tsx
index a27133042b..49c61f2961 100644
--- a/packages/twenty-front/src/pages/settings/data-model/SettingsNewObject.tsx
+++ b/packages/twenty-front/src/pages/settings/data-model/SettingsNewObject.tsx
@@ -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,
},
diff --git a/packages/twenty-ui/src/display/color/components/ColorSample.tsx b/packages/twenty-ui/src/display/color/components/ColorSample.tsx
index 9c03fdce81..0453640a4e 100644
--- a/packages/twenty-ui/src/display/color/components/ColorSample.tsx
+++ b/packages/twenty-ui/src/display/color/components/ColorSample.tsx
@@ -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`
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')};
diff --git a/packages/twenty-ui/src/display/color/components/__stories__/ColorSample.stories.tsx b/packages/twenty-ui/src/display/color/components/__stories__/ColorSample.stories.tsx
index deab220b24..22f966796b 100644
--- a/packages/twenty-ui/src/display/color/components/__stories__/ColorSample.stories.tsx
+++ b/packages/twenty-ui/src/display/color/components/__stories__/ColorSample.stories.tsx
@@ -19,3 +19,7 @@ export const Default: Story = {};
export const Pipeline: Story = {
args: { variant: 'pipeline' },
};
+
+export const Circle: Story = {
+ args: { variant: 'circle' },
+};
diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemSelectColor.stories.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemSelectColor.stories.tsx
index df16924626..0c12adf42f 100644
--- a/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemSelectColor.stories.tsx
+++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/__stories__/MenuItemSelectColor.stories.tsx
@@ -67,7 +67,7 @@ export const Catalog: CatalogStory = {
},
{
name: 'variant',
- values: ['default', 'pipeline'],
+ values: ['circle', 'default', 'pipeline'],
props: (variant: ColorSampleVariant) => ({ variant }),
labels: (variant: ColorSampleVariant) => variant,
},