From d06e687b77857ee363199cbaa973986d59dfd956 Mon Sep 17 00:00:00 2001 From: Thomas des Francs Date: Sun, 14 Jun 2026 06:15:49 +0200 Subject: [PATCH] Fix settings UI polish pass 2 (#21540) ## Summary This PR groups the requested second UI polish pass across the app shell, settings pages, data model, community, AI tools, and developer setup surfaces. ### Shell and navigation polish - Lets the app content card fill the available viewport and removes the bottom-left radius on the main container. - Uses an outside-looking container border treatment so the rounded top-left edge matches the Figma frame without a double border. - Restores the separator between the AI chat side panel and the record container. - Updates navigation drawer background/skeleton tones to use gray/3 consistently. - Simplifies the full-screen backend error fallback so the white screen fills the viewport. ### Settings page polish - Moves the layout customization action from the settings navbar into a settings card under the hero. - Tightens layout page copy now that the customization surfaces are not directly manageable yet. - Aligns role detail title sizing and hover treatment with other settings headers. - Supports settings card brand icon colors, then restores Discord and X brand logos on the Community page. - Adds the Community discovery cover, moves community/social content to the top, and reorganizes Partners and Features. - Keeps setting card subtitle and separator behavior available for the updated settings cards. ### Data model and object settings polish - Adds a shared data-model table body wrapper so object, field, and relation tables all keep the expected bottom border. - Displays the object icon in the object settings navbar. - Shares the new-field wizard parent object icon wrapper and keeps the object icon at 64% opacity in wizard headers. - Updates the new-field type selector icon treatment to match the wizard header opacity behavior. ### Integrations, tools, and API polish - Reworks the MCP setup config card with light syntax coloring and a top-right copy icon button instead of the bottom copy section. - Fixes email import icons to match the Figma asset shape. - Ensures AI tool table icons and chevrons never fall back to black. ## Validation - `npx nx lint:diff-with-main twenty-front` - `npx tsc -p packages/twenty-front/tsconfig.json --noEmit` - `git diff --check` - Manual local visual pass on `apple.localhost:3001` for companies, settings/community, data model, object settings, new-field wizard, layout settings, account import, MCP setup, AI tools, and role detail pages. ## Visual QA ### Shell and settings frame Shell and settings frame
before/after board ### Community and data model Community and data model
before/after board ### Integrations and tools Integrations and tools before/after
board Review in cubic --- .../components/AppFullScreenErrorFallback.tsx | 19 +-- .../NavigationMenuItemSkeletonLoader.tsx | 2 +- .../SettingsAccountsMessageFolderIcon.tsx | 87 +++++----- .../settings/components/SettingsCard.tsx | 15 +- .../components/SettingsDiscoveryHeroCard.tsx | 17 +- .../components/layout/SettingsPageLayout.tsx | 3 + .../SettingsDataModelTableBodyContainer.tsx | 6 + .../SettingsObjectNewFieldHeaderIcon.tsx | 23 +++ .../SettingsObjectNewFieldSelector.tsx | 15 +- .../SettingsObjectRelationsTable.tsx | 20 ++- .../components/SettingsMcpSetup.tsx | 111 +++++++++++-- .../components/SettingsRoleLabelContainer.tsx | 21 ++- .../components/SidePanelForDesktop.tsx | 1 + .../layout/page/components/DefaultLayout.tsx | 2 +- .../layout/page/components/PageCardLayout.tsx | 7 +- .../components/NavigationDrawerItem.tsx | 2 +- ...gationDrawerSectionTitleSkeletonLoader.tsx | 2 +- .../ai/components/SettingsAgentToolsTable.tsx | 1 + .../ai/components/SettingsToolIcon.tsx | 10 +- .../settings/community/SettingsCommunity.tsx | 148 ++++++++++++++---- .../settings/community/assets/cover-dark.png | Bin 0 -> 556578 bytes .../settings/community/assets/cover-light.png | Bin 0 -> 573773 bytes .../data-model/SettingsObjectDetailPage.tsx | 2 + .../data-model/SettingsObjectFieldTable.tsx | 32 ++-- .../data-model/SettingsObjectTable.tsx | 106 +++++++------ .../SettingsObjectNewFieldConfigure.tsx | 6 + .../SettingsObjectNewFieldSelect.tsx | 6 + .../pages/settings/layout/SettingsLayout.tsx | 28 ++-- 28 files changed, 497 insertions(+), 195 deletions(-) create mode 100644 packages/twenty-front/src/modules/settings/data-model/components/SettingsDataModelTableBodyContainer.tsx create mode 100644 packages/twenty-front/src/modules/settings/data-model/fields/components/SettingsObjectNewFieldHeaderIcon.tsx create mode 100644 packages/twenty-front/src/pages/settings/community/assets/cover-dark.png create mode 100644 packages/twenty-front/src/pages/settings/community/assets/cover-light.png diff --git a/packages/twenty-front/src/modules/error-handler/components/AppFullScreenErrorFallback.tsx b/packages/twenty-front/src/modules/error-handler/components/AppFullScreenErrorFallback.tsx index d0f075c390..5224aa0a7d 100644 --- a/packages/twenty-front/src/modules/error-handler/components/AppFullScreenErrorFallback.tsx +++ b/packages/twenty-front/src/modules/error-handler/components/AppFullScreenErrorFallback.tsx @@ -1,6 +1,5 @@ import { AppErrorDisplay } from '@/error-handler/components/internal/AppErrorDisplay'; import { type AppErrorDisplayProps } from '@/error-handler/types/AppErrorDisplayProps'; -import { PageBody } from '@/ui/layout/page/components/PageBody'; import { styled } from '@linaria/react'; import { t } from '@lingui/core/macro'; import { themeCssVariables } from 'twenty-ui-deprecated/theme-constants'; @@ -8,12 +7,10 @@ import { themeCssVariables } from 'twenty-ui-deprecated/theme-constants'; type AppFullScreenErrorFallbackProps = AppErrorDisplayProps; const StyledContainer = styled.div` - background: ${themeCssVariables.background.noisy}; + background: ${themeCssVariables.background.primary}; box-sizing: border-box; display: flex; - height: 100vh; - padding-left: ${themeCssVariables.spacing[3]}; - padding-top: ${themeCssVariables.spacing[3]}; + height: 100dvh; width: 100vw; `; @@ -24,13 +21,11 @@ export const AppFullScreenErrorFallback = ({ }: AppFullScreenErrorFallbackProps) => { return ( - - - + ); }; diff --git a/packages/twenty-front/src/modules/navigation-menu-item/display/components/NavigationMenuItemSkeletonLoader.tsx b/packages/twenty-front/src/modules/navigation-menu-item/display/components/NavigationMenuItemSkeletonLoader.tsx index e1a354f55b..93cb7f7135 100644 --- a/packages/twenty-front/src/modules/navigation-menu-item/display/components/NavigationMenuItemSkeletonLoader.tsx +++ b/packages/twenty-front/src/modules/navigation-menu-item/display/components/NavigationMenuItemSkeletonLoader.tsx @@ -32,7 +32,7 @@ export const NavigationMenuItemSkeletonLoader = () => { const { theme } = useContext(ThemeContext); return ( diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageFolderIcon.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageFolderIcon.tsx index 375552346c..a055b60baf 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageFolderIcon.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageFolderIcon.tsx @@ -9,43 +9,58 @@ type SettingsAccountsMessageFolderIconProps = { }; const StyledCardMedia = styled.div` - align-items: stretch; - border: 2px solid ${themeCssVariables.border.color.medium}; + align-items: center; + background-color: ${themeCssVariables.border.color.medium}; border-radius: ${themeCssVariables.border.radius.sm}; - color: ${themeCssVariables.font.color.light}; + box-sizing: border-box; display: flex; - flex-direction: column; - gap: ${themeCssVariables.spacing['0.5']}; - height: ${themeCssVariables.spacing[8]}; + height: 40px; justify-content: center; - padding: ${themeCssVariables.spacing['0.5']}; - width: ${themeCssVariables.spacing[6]}; + padding: 2px; + width: 32px; +`; + +const StyledCardMediaContent = styled.div` + align-items: stretch; + background-color: ${themeCssVariables.background.secondary}; + border-radius: 2px; + box-sizing: border-box; + display: flex; + flex: 1; + flex-direction: column; + gap: 2px; + height: 100%; + justify-content: center; + min-width: 0; + padding: 2px; `; const StyledFolderRow = styled.div` align-items: center; display: flex; + flex: 1 0 0; gap: 2px; + min-height: 0; `; const StyledFolderIcon = styled.div<{ isDisabled?: boolean }>` background-color: ${({ isDisabled }) => isDisabled - ? themeCssVariables.background.quaternary - : themeCssVariables.accent.accent4060}; + ? themeCssVariables.border.color.medium + : themeCssVariables.accent.accent7}; border-radius: 1px; - height: 5px; - width: 5px; + height: 100%; + width: 4.8px; `; const StyledFolderLabel = styled.div<{ isDisabled?: boolean }>` background-color: ${({ isDisabled }) => isDisabled - ? themeCssVariables.background.quaternary - : themeCssVariables.accent.accent4060}; + ? themeCssVariables.border.color.medium + : themeCssVariables.accent.accent7}; border-radius: 1px; flex: 1; - height: 5px; + height: 100%; `; export const SettingsAccountsMessageFolderIcon = ({ @@ -57,26 +72,28 @@ export const SettingsAccountsMessageFolderIcon = ({ return ( - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + ); }; diff --git a/packages/twenty-front/src/modules/settings/components/SettingsCard.tsx b/packages/twenty-front/src/modules/settings/components/SettingsCard.tsx index da59d90558..441f98cbb2 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsCard.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsCard.tsx @@ -16,6 +16,7 @@ type SettingsCardProps = { disabled?: boolean; soon?: boolean; Icon: ReactNode; + iconColor?: string; onClick?: () => void; title: string; className?: string; @@ -82,8 +83,15 @@ const StyledDescription = styled.div` padding-left: ${themeCssVariables.spacing[7]}; `; -const StyledIconContainer = styled.div` +const StyledIconContainer = styled.div<{ + disabled?: boolean; + iconColor?: string; +}>` align-items: center; + color: ${({ disabled, iconColor }) => + disabled + ? themeCssVariables.font.color.extraLight + : (iconColor ?? 'inherit')}; display: flex; height: 24px; justify-content: center; @@ -95,6 +103,7 @@ export const SettingsCard = ({ soon, disabled = soon, Icon, + iconColor, onClick, title, className, @@ -112,7 +121,9 @@ export const SettingsCard = ({ - {Icon} + + {Icon} + {title} {soon && } diff --git a/packages/twenty-front/src/modules/settings/components/SettingsDiscoveryHeroCard.tsx b/packages/twenty-front/src/modules/settings/components/SettingsDiscoveryHeroCard.tsx index 5120af73f2..7b4c9a624d 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsDiscoveryHeroCard.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsDiscoveryHeroCard.tsx @@ -7,7 +7,7 @@ import { useModal } from '@/ui/layout/modal/hooks/useModal'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { styled } from '@linaria/react'; import { useLingui } from '@lingui/react/macro'; -import { useContext } from 'react'; +import { type ReactNode, useContext } from 'react'; import { Card } from 'twenty-ui-deprecated/layout'; import { ThemeContext, @@ -43,11 +43,17 @@ const StyledOverlay = styled.div` position: absolute; `; +const StyledFooter = styled.div` + background: ${themeCssVariables.background.secondary}; + border-top: 1px solid ${themeCssVariables.border.color.medium}; +`; + type SettingsDiscoveryHeroCardProps = { lightSrc: string; darkSrc: string; instanceIdPrefix: string; tabs: SettingsCustomizeVideoModalTab[]; + footer?: ReactNode; playButtonAriaLabel?: string; }; @@ -56,6 +62,7 @@ export const SettingsDiscoveryHeroCard = ({ darkSrc, instanceIdPrefix, tabs, + footer, playButtonAriaLabel, }: SettingsDiscoveryHeroCardProps) => { const { t } = useLingui(); @@ -64,6 +71,7 @@ export const SettingsDiscoveryHeroCard = ({ const isDiscoveryVideoEnabled = useIsFeatureEnabled( FeatureFlagKey.IS_SETTINGS_DISCOVERY_HERO_ENABLED, ); + const shouldDisplayVideo = isDiscoveryVideoEnabled && tabs.length > 0; const modalInstanceId = `${instanceIdPrefix}-modal`; const tabsInstanceId = `${instanceIdPrefix}-tabs`; @@ -75,7 +83,7 @@ export const SettingsDiscoveryHeroCard = ({ - {isDiscoveryVideoEnabled && ( + {shouldDisplayVideo && ( openModal(modalInstanceId)} @@ -84,8 +92,11 @@ export const SettingsDiscoveryHeroCard = ({ )} + {footer !== undefined && footer !== null && ( + {footer} + )} - {isDiscoveryVideoEnabled && ( + {shouldDisplayVideo && ( ( + + + +); diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsObjectNewFieldSelector.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsObjectNewFieldSelector.tsx index 1f891726a4..6728e50beb 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsObjectNewFieldSelector.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsObjectNewFieldSelector.tsx @@ -63,6 +63,11 @@ const StyledSearchInputContainer = styled.div` width: 100%; `; +const StyledFieldTypeIconContainer = styled.span` + display: flex; + opacity: 0.64; +`; + export const SettingsObjectNewFieldSelector = ({ excludedFieldTypes = [], objectNamePlural, @@ -164,10 +169,12 @@ export const SettingsObjectNewFieldSelector = ({ + + + } title={config.label} /> diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectRelationsTable.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectRelationsTable.tsx index 530c779407..70ba60a77c 100644 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectRelationsTable.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectRelationsTable.tsx @@ -7,11 +7,13 @@ import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { SortableTableHeader } from '@/ui/layout/table/components/SortableTableHeader'; import { Table } from '@/ui/layout/table/components/Table'; +import { TableBody } from '@/ui/layout/table/components/TableBody'; import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { useSortedArray } from '@/ui/layout/table/hooks/useSortedArray'; import { type TableMetadata } from '@/ui/layout/table/types/TableMetadata'; import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { StyledSettingsDataModelTableBodyContainer } from '@/settings/data-model/components/SettingsDataModelTableBodyContainer'; import { styled } from '@linaria/react'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react/macro'; @@ -182,13 +184,17 @@ export const SettingsObjectRelationsTable = ({ ))} - {filteredRelationFields.map((fieldMetadataItem) => ( - - ))} + + + {filteredRelationFields.map((fieldMetadataItem) => ( + + ))} + + ); diff --git a/packages/twenty-front/src/modules/settings/playground/components/SettingsMcpSetup.tsx b/packages/twenty-front/src/modules/settings/playground/components/SettingsMcpSetup.tsx index 9b0a64d7bb..b28ec8a043 100644 --- a/packages/twenty-front/src/modules/settings/playground/components/SettingsMcpSetup.tsx +++ b/packages/twenty-front/src/modules/settings/playground/components/SettingsMcpSetup.tsx @@ -1,17 +1,44 @@ import { styled } from '@linaria/react'; import { useLingui } from '@lingui/react/macro'; +import { type ReactNode } from 'react'; import { H2Title, IconCopy } from 'twenty-ui-deprecated/display'; -import { Button } from 'twenty-ui-deprecated/input'; +import { LightIconButton } from 'twenty-ui-deprecated/input'; import { Card, CardContent, Section } from 'twenty-ui-deprecated/layout'; +import { themeCssVariables } from 'twenty-ui-deprecated/theme-constants'; import { REACT_APP_SERVER_BASE_URL } from '~/config'; import { useCopyToClipboard } from '~/hooks/useCopyToClipboard'; +const StyledMcpConfigContainer = styled.div` + position: relative; +`; + +const StyledCopyButtonContainer = styled.div` + position: absolute; + right: 0; + top: 0; +`; + const StyledPre = styled.pre` + color: ${themeCssVariables.font.color.secondary}; font-family: monospace; + line-height: 1.5; margin: 0; + overflow-x: auto; white-space: pre; `; +const StyledJsonKey = styled.span` + color: ${themeCssVariables.color.blue}; +`; + +const StyledJsonString = styled.span` + color: ${themeCssVariables.color.green}; +`; + +const StyledJsonPlaceholder = styled.span` + color: ${themeCssVariables.color.orange}; +`; + const buildMcpConfig = (serverUrl: string) => `{ "mcpServers": { @@ -24,6 +51,61 @@ const buildMcpConfig = (serverUrl: string) => } }`; +const renderJsonString = (token: string, key: string) => { + const apiKeyPlaceholder = ''; + + if (!token.includes(apiKeyPlaceholder)) { + return {token}; + } + + const [beforePlaceholder, afterPlaceholder] = token.split(apiKeyPlaceholder); + + return ( + + {beforePlaceholder} + {apiKeyPlaceholder} + {afterPlaceholder} + + ); +}; + +const getHighlightedMcpConfig = (mcpConfig: string) => { + const jsonStringTokenRegex = /("(?:[^"\\]|\\.)*")(\s*:)?/g; + const tokens: ReactNode[] = []; + let previousMatchEndIndex = 0; + let tokenIndex = 0; + let match: RegExpExecArray | null; + + while ((match = jsonStringTokenRegex.exec(mcpConfig)) !== null) { + const [fullMatch, quotedString, trailingColon] = match; + const matchIndex = match.index; + + if (matchIndex > previousMatchEndIndex) { + tokens.push(mcpConfig.slice(previousMatchEndIndex, matchIndex)); + } + + if (trailingColon) { + tokens.push( + + {quotedString} + , + ); + tokens.push(trailingColon); + } else { + tokens.push(renderJsonString(quotedString, `json-string-${tokenIndex}`)); + } + + previousMatchEndIndex = matchIndex + fullMatch.length; + tokenIndex += 1; + } + + if (previousMatchEndIndex < mcpConfig.length) { + tokens.push(mcpConfig.slice(previousMatchEndIndex)); + } + + return tokens; +}; + export const SettingsMcpSetup = () => { const { t } = useLingui(); const { copyToClipboard } = useCopyToClipboard(); @@ -36,19 +118,22 @@ export const SettingsMcpSetup = () => { description={t`Add Twenty as a Model Context Protocol (MCP) server. Paste this config into Claude Desktop, Cursor, Cline, Continue, Zed, or any other MCP-aware client.`} /> - - {mcpConfig} - -