From a67fdf2dac6e5fc4d9223a894226c2ad053dd848 Mon Sep 17 00:00:00 2001 From: Thomas des Francs Date: Tue, 16 Jun 2026 13:57:44 +0200 Subject: [PATCH] Clean up MCP Monaco editor (#21643) Rebuild the MCP setup block on the shared Monaco editor with theme-driven colors, padding, and auto-height; add the settings description line-height token; revert the clipboard util to the shared navigator.clipboard implementation; and consolidate the code editor's auto-height to a single disposed, reactive effect. --- .../settings/components/SettingsCard.tsx | 1 + .../SettingsListItemCardContent.tsx | 1 + .../SettingsMorphRelationMultiSelect.tsx | 1 + .../SettingsCardContentBase.tsx | 2 +- .../settings/components/SettingsRadioCard.tsx | 1 + .../assets/model-context-protocol-logo.svg | 12 ++ .../components/SettingsMcpSetup.tsx | 150 +++++------------- .../components/WorkflowEditActionCode.tsx | 2 - .../display/typography/components/H2Title.tsx | 1 + .../code-editor/components/CodeEditor.tsx | 103 ++++++++++-- .../theme/utils/getBaseCodeEditorTheme.ts | 84 ++++++++-- .../typography/components/H2Title.module.scss | 1 + .../code-editor/components/CodeEditor.tsx | 99 ++++++++++-- .../theme/utils/getBaseCodeEditorTheme.ts | 84 ++++++++-- 14 files changed, 393 insertions(+), 149 deletions(-) create mode 100644 packages/twenty-front/src/modules/settings/playground/assets/model-context-protocol-logo.svg diff --git a/packages/twenty-front/src/modules/settings/components/SettingsCard.tsx b/packages/twenty-front/src/modules/settings/components/SettingsCard.tsx index 441f98cbb2..a3c2ac60d3 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsCard.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsCard.tsx @@ -79,6 +79,7 @@ const StyledIconChevronRightContainer = styled.span` `; const StyledDescription = styled.div` + line-height: ${themeCssVariables.text.lineHeight.lg}; padding-bottom: ${themeCssVariables.spacing[2]}; padding-left: ${themeCssVariables.spacing[7]}; `; diff --git a/packages/twenty-front/src/modules/settings/components/SettingsListItemCardContent.tsx b/packages/twenty-front/src/modules/settings/components/SettingsListItemCardContent.tsx index 2ce92fbaa3..34cdb5fa71 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsListItemCardContent.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsListItemCardContent.tsx @@ -48,6 +48,7 @@ const StyledLabel = styled.span` const StyledDescription = styled.span` color: ${themeCssVariables.font.color.light}; font-weight: ${themeCssVariables.font.weight.regular}; + line-height: ${themeCssVariables.text.lineHeight.lg}; padding-left: ${themeCssVariables.spacing[1]}; `; diff --git a/packages/twenty-front/src/modules/settings/components/SettingsMorphRelationMultiSelect.tsx b/packages/twenty-front/src/modules/settings/components/SettingsMorphRelationMultiSelect.tsx index 612f1f501e..af4b57827f 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsMorphRelationMultiSelect.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsMorphRelationMultiSelect.tsx @@ -70,6 +70,7 @@ const StyledLabel = styled.span` const StyledDescription = styled.span` color: ${themeCssVariables.font.color.light}; font-size: ${themeCssVariables.font.size.sm}; + line-height: ${themeCssVariables.text.lineHeight.lg}; `; const StyledError = styled.span` diff --git a/packages/twenty-front/src/modules/settings/components/SettingsOptions/SettingsCardContentBase.tsx b/packages/twenty-front/src/modules/settings/components/SettingsOptions/SettingsCardContentBase.tsx index f4c49725c6..9b5ffd6d3a 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsOptions/SettingsCardContentBase.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsOptions/SettingsCardContentBase.tsx @@ -41,7 +41,7 @@ export const StyledSettingsCardTextContainer = styled.div` export const StyledSettingsCardDescription = styled.div` color: ${themeCssVariables.font.color.secondary}; font-size: ${themeCssVariables.font.size.sm}; - line-height: 1.5; + line-height: ${themeCssVariables.text.lineHeight.lg}; overflow: hidden; a { diff --git a/packages/twenty-front/src/modules/settings/components/SettingsRadioCard.tsx b/packages/twenty-front/src/modules/settings/components/SettingsRadioCard.tsx index c72c8cae35..a8ec337805 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsRadioCard.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsRadioCard.tsx @@ -40,6 +40,7 @@ const StyledTitle = styled.div` const StyledDescription = styled.div` color: ${themeCssVariables.font.color.tertiary}; font-size: ${themeCssVariables.font.size.sm}; + line-height: ${themeCssVariables.text.lineHeight.lg}; `; type SettingsRadioCardProps = { diff --git a/packages/twenty-front/src/modules/settings/playground/assets/model-context-protocol-logo.svg b/packages/twenty-front/src/modules/settings/playground/assets/model-context-protocol-logo.svg new file mode 100644 index 0000000000..c09e149a9a --- /dev/null +++ b/packages/twenty-front/src/modules/settings/playground/assets/model-context-protocol-logo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + 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 b28ec8a043..a24e9e8124 100644 --- a/packages/twenty-front/src/modules/settings/playground/components/SettingsMcpSetup.tsx +++ b/packages/twenty-front/src/modules/settings/playground/components/SettingsMcpSetup.tsx @@ -1,42 +1,24 @@ 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 { LightIconButton } from 'twenty-ui-deprecated/input'; -import { Card, CardContent, Section } from 'twenty-ui-deprecated/layout'; +import { LightCopyIconButton } from '@/object-record/record-field/ui/components/LightCopyIconButton'; +import ModelContextProtocolLogo from '@/settings/playground/assets/model-context-protocol-logo.svg?react'; +import { H2Title } from 'twenty-ui-deprecated/display'; +import { CodeEditor, CoreEditorHeader } from 'twenty-ui-deprecated/input'; +import { 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 StyledMcpEditorHeaderTitle = styled.div` + align-items: center; + display: flex; + gap: ${themeCssVariables.spacing[2]}; `; -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 StyledMcpIcon = styled(ModelContextProtocolLogo)` + color: inherit; + flex-shrink: 0; + height: calc(${themeCssVariables.icon.size.md} * 1px); + width: calc(${themeCssVariables.icon.size.md} * 1px); `; const buildMcpConfig = (serverUrl: string) => @@ -51,91 +33,43 @@ 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(); const mcpConfig = buildMcpConfig(REACT_APP_SERVER_BASE_URL); return (
+ + + {t`MCP client configuration`} + , + ]} + rightNodes={[]} + /> + - - - - {getHighlightedMcpConfig(mcpConfig)} - - - copyToClipboard(mcpConfig, t`MCP config copied to clipboard`) - } - /> - - - -
); }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCode.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCode.tsx index 2fcc242966..caedd81935 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCode.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionCode.tsx @@ -358,7 +358,6 @@ export const WorkflowEditActionCode = ({ readOnly: actionOptions.readonly, domReadOnly: actionOptions.readonly, scrollBeyondLastLine: false, - padding: { top: 4, bottom: 4 }, }} /> @@ -397,7 +396,6 @@ export const WorkflowEditActionCode = ({ readOnly: actionOptions.readonly, domReadOnly: actionOptions.readonly, scrollBeyondLastLine: false, - padding: { top: 4, bottom: 4 }, lineNumbersMinChars: 2, fixedOverflowWidgets: true, }} diff --git a/packages/twenty-ui-deprecated/src/display/typography/components/H2Title.tsx b/packages/twenty-ui-deprecated/src/display/typography/components/H2Title.tsx index 2468951aca..8df77d9136 100644 --- a/packages/twenty-ui-deprecated/src/display/typography/components/H2Title.tsx +++ b/packages/twenty-ui-deprecated/src/display/typography/components/H2Title.tsx @@ -32,6 +32,7 @@ const StyledDescription = styled.h3` color: ${themeCssVariables.font.color.tertiary}; font-size: ${themeCssVariables.font.size.md}; font-weight: ${themeCssVariables.font.weight.regular}; + line-height: ${themeCssVariables.text.lineHeight.lg}; margin: 0; margin-top: ${themeCssVariables.spacing[2]}; `; diff --git a/packages/twenty-ui-deprecated/src/input/code-editor/components/CodeEditor.tsx b/packages/twenty-ui-deprecated/src/input/code-editor/components/CodeEditor.tsx index 441cd51f10..ed72ae5ae3 100644 --- a/packages/twenty-ui-deprecated/src/input/code-editor/components/CodeEditor.tsx +++ b/packages/twenty-ui-deprecated/src/input/code-editor/components/CodeEditor.tsx @@ -5,12 +5,29 @@ import { ResizeHandle } from '@ui/layout/resize-handle/components/ResizeHandle'; import { BASE_CODE_EDITOR_THEME_ID } from '@ui/input/code-editor/constants/BaseCodeEditorThemeId'; import { useResizeHandle } from '@ui/layout/resize-handle/hooks/useResizeHandle'; import { getBaseCodeEditorTheme } from '@ui/input/code-editor/theme/utils/getBaseCodeEditorTheme'; -import { ThemeContext, themeCssVariables } from '@ui/theme-constants'; +import { + ThemeContext, + themeCssVariables, + type ThemeType, +} from '@ui/theme-constants'; import { type editor } from 'monaco-editor'; -import { type KeyboardEvent, useContext, useState } from 'react'; +import { type KeyboardEvent, useContext, useEffect, useState } from 'react'; import { isDefined } from 'twenty-shared/utils'; type CodeEditorVariant = 'default' | 'with-header' | 'borderless'; +type CodeEditorContentPadding = 'default' | 'comfortable'; + +const setCodeEditorTheme = ( + monaco: Monaco, + theme: ThemeType, + colorScheme: 'light' | 'dark', +) => { + monaco.editor.defineTheme( + BASE_CODE_EDITOR_THEME_ID, + getBaseCodeEditorTheme(theme, colorScheme), + ); + monaco.editor.setTheme(BASE_CODE_EDITOR_THEME_ID); +}; type CodeEditorProps = Pick< EditorProps, @@ -22,6 +39,8 @@ type CodeEditorProps = Pick< isLoading?: boolean; transparentBackground?: boolean; resizable?: boolean; + contentPadding?: CodeEditorContentPadding; + autoHeight?: boolean; }; const StyledEditorLoader = styled.div<{ @@ -121,13 +140,18 @@ export const CodeEditor = ({ isLoading = false, options, resizable = false, + contentPadding = 'default', + autoHeight = false, }: CodeEditorProps) => { - const { theme } = useContext(ThemeContext); + const { theme, colorScheme } = useContext(ThemeContext); const [monaco, setMonaco] = useState(undefined); const [editor, setEditor] = useState< editor.IStandaloneCodeEditor | undefined >(undefined); const [isEditorFocused, setIsEditorFocused] = useState(false); + const [autoHeightContentHeight, setAutoHeightContentHeight] = useState< + number | undefined + >(undefined); const numericHeight = typeof height === 'number' ? height : 450; const { @@ -139,7 +163,28 @@ export const CodeEditor = ({ initialSize: numericHeight, }); - const currentHeight = resizable ? resizableHeight : height; + const shouldAutoHeight = autoHeight && !resizable; + const codeEditorPadding = + typeof theme.spacingMultiplicator === 'number' + ? theme.spacingMultiplicator * 4 + : undefined; + const currentHeight = shouldAutoHeight + ? (autoHeightContentHeight ?? height) + : resizable + ? resizableHeight + : height; + const contentPaddingOptions = { + ...(contentPadding === 'comfortable' + ? { + lineDecorationsWidth: codeEditorPadding, + } + : {}), + padding: { + bottom: codeEditorPadding, + top: codeEditorPadding, + }, + }; + const { padding: _callerPadding, ...editorOptions } = options ?? {}; const setModelMarkers = ( editor: editor.IStandaloneCodeEditor | undefined, @@ -161,6 +206,43 @@ export const CodeEditor = ({ } }; + useEffect(() => { + if (!isDefined(monaco)) { + return; + } + + setCodeEditorTheme(monaco, theme, colorScheme); + }, [colorScheme, monaco, theme]); + + // Drive the container height from Monaco's content height; the editor's + // default automaticLayout then re-fits the canvas (no manual layout needed). + useEffect(() => { + if (!shouldAutoHeight || !isDefined(editor)) { + setAutoHeightContentHeight(undefined); + return; + } + + const updateAutoHeight = () => { + const nextHeight = editor.getContentHeight(); + + if (!Number.isFinite(nextHeight) || nextHeight <= 0) { + return; + } + + setAutoHeightContentHeight((currentHeight) => + currentHeight === nextHeight ? currentHeight : nextHeight, + ); + }; + + updateAutoHeight(); + + const disposable = editor.onDidContentSizeChange(updateAutoHeight); + + return () => { + disposable.dispose(); + }; + }, [editor, shouldAutoHeight]); + return isLoading ? ( @@ -186,11 +268,7 @@ export const CodeEditor = ({ setMonaco(monaco); setEditor(editor); - monaco.editor.defineTheme( - BASE_CODE_EDITOR_THEME_ID, - getBaseCodeEditorTheme(theme), - ); - monaco.editor.setTheme(BASE_CODE_EDITOR_THEME_ID); + setCodeEditorTheme(monaco, theme, colorScheme); editor.onDidFocusEditorWidget(() => { setIsEditorFocused(true); @@ -214,6 +292,10 @@ export const CodeEditor = ({ options={{ formatOnPaste: true, formatOnType: true, + fontFamily: theme.code.font.family, + bracketPairColorization: { + enabled: false, + }, overviewRulerLanes: 0, scrollBeyondLastLine: false, scrollbar: { @@ -223,7 +305,8 @@ export const CodeEditor = ({ minimap: { enabled: false, }, - ...options, + ...editorOptions, + ...contentPaddingOptions, }} /> diff --git a/packages/twenty-ui-deprecated/src/input/code-editor/theme/utils/getBaseCodeEditorTheme.ts b/packages/twenty-ui-deprecated/src/input/code-editor/theme/utils/getBaseCodeEditorTheme.ts index 26fc71322e..ca750d808b 100644 --- a/packages/twenty-ui-deprecated/src/input/code-editor/theme/utils/getBaseCodeEditorTheme.ts +++ b/packages/twenty-ui-deprecated/src/input/code-editor/theme/utils/getBaseCodeEditorTheme.ts @@ -3,6 +3,10 @@ import { type editor } from 'monaco-editor'; import { isDefined } from 'twenty-shared/utils'; const convertColorToHex = (color: string): string => { + if (color.trim() === 'transparent') { + return '#00000000'; + } + const displayP3Match = color.match( /color\(display-p3\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)(?:\s+\/\s+([\d.]+))?\)/, ); @@ -33,35 +37,81 @@ const convertColorToHex = (color: string): string => { export const getBaseCodeEditorTheme = ( theme: ThemeType, + colorScheme: 'light' | 'dark', ): editor.IStandaloneThemeData => { return { - base: 'vs', + base: colorScheme === 'dark' ? 'vs-dark' : 'vs', inherit: true, rules: [ { token: '', - foreground: convertColorToHex(theme.code.text.gray), - fontStyle: 'bold', + foreground: convertColorToHex(theme.font.color.secondary), }, { token: 'keyword', - foreground: convertColorToHex(theme.code.text.sky), + foreground: convertColorToHex(theme.color.pink11), }, { - token: 'delimiter', - foreground: convertColorToHex(theme.code.text.gray), + token: 'keyword.control', + foreground: convertColorToHex(theme.color.pink11), + }, + { + token: 'keyword.json', + foreground: convertColorToHex(theme.color.orange11), + }, + { + token: 'number', + foreground: convertColorToHex(theme.color.orange11), + }, + { + token: 'number.json', + foreground: convertColorToHex(theme.color.orange11), + }, + { + token: 'regexp', + foreground: convertColorToHex(theme.color.orange11), + }, + { + token: 'type', + foreground: convertColorToHex(theme.color.green11), + }, + { + token: 'attribute.name', + foreground: convertColorToHex(theme.color.blue11), + }, + { + token: 'tag', + foreground: convertColorToHex(theme.color.pink11), }, { token: 'string', - foreground: convertColorToHex(theme.code.text.pink), + foreground: convertColorToHex(theme.color.green11), + }, + { + token: 'string.key.json', + foreground: convertColorToHex(theme.color.blue11), + }, + { + token: 'delimiter', + foreground: convertColorToHex(theme.font.color.light), + }, + { + token: 'delimiter.bracket.json', + foreground: convertColorToHex(theme.font.color.light), + }, + { + token: 'string.value.json', + foreground: convertColorToHex(theme.color.green11), }, { token: 'comment', - foreground: convertColorToHex(theme.code.text.orange), + foreground: convertColorToHex(theme.font.color.light), + fontStyle: 'italic', }, ], colors: { - 'editor.background': '#00000000', + 'editor.background': convertColorToHex('transparent'), + 'editor.foreground': convertColorToHex(theme.font.color.secondary), 'editorCursor.foreground': convertColorToHex(theme.font.color.primary), 'editorLineNumber.foreground': convertColorToHex( theme.font.color.extraLight, @@ -72,6 +122,22 @@ export const getBaseCodeEditorTheme = ( 'editor.lineHighlightBackground': convertColorToHex( theme.background.tertiary, ), + 'editor.selectionBackground': convertColorToHex( + theme.background.transparent.blue, + ), + 'editor.inactiveSelectionBackground': convertColorToHex( + theme.background.transparent.light, + ), + 'editorIndentGuide.background1': convertColorToHex( + theme.border.color.light, + ), + 'editorIndentGuide.activeBackground1': convertColorToHex( + theme.border.color.medium, + ), + 'editorBracketMatch.background': convertColorToHex( + theme.background.transparent.light, + ), + 'editorBracketMatch.border': convertColorToHex(theme.border.color.medium), }, }; }; diff --git a/packages/twenty-ui/src/display/typography/components/H2Title.module.scss b/packages/twenty-ui/src/display/typography/components/H2Title.module.scss index a0129ea151..6c86e03d1d 100644 --- a/packages/twenty-ui/src/display/typography/components/H2Title.module.scss +++ b/packages/twenty-ui/src/display/typography/components/H2Title.module.scss @@ -21,6 +21,7 @@ color: var(--t-font-color-tertiary); font-size: var(--t-font-size-md); font-weight: var(--t-font-weight-regular); + line-height: var(--t-text-line-height-lg); margin: 0; margin-top: var(--t-spacing-2); } diff --git a/packages/twenty-ui/src/input/code-editor/components/CodeEditor.tsx b/packages/twenty-ui/src/input/code-editor/components/CodeEditor.tsx index 0665b8c47f..54e7734c70 100644 --- a/packages/twenty-ui/src/input/code-editor/components/CodeEditor.tsx +++ b/packages/twenty-ui/src/input/code-editor/components/CodeEditor.tsx @@ -4,14 +4,27 @@ import { BASE_CODE_EDITOR_THEME_ID } from '@ui/input/code-editor/constants/BaseC import { getBaseCodeEditorTheme } from '@ui/input/code-editor/theme/utils/getBaseCodeEditorTheme'; import { ResizeHandle } from '@ui/layout/resize-handle/components/ResizeHandle'; import { useResizeHandle } from '@ui/layout/resize-handle/hooks/useResizeHandle'; -import { ThemeContext } from '@ui/theme-constants'; +import { ThemeContext, type ThemeType } from '@ui/theme-constants'; import { type editor } from 'monaco-editor'; -import { type KeyboardEvent, useContext, useState } from 'react'; +import { type KeyboardEvent, useContext, useEffect, useState } from 'react'; import { isDefined } from '@ui/utilities/utils/isDefined'; import styles from './CodeEditor.module.scss'; type CodeEditorVariant = 'default' | 'with-header' | 'borderless'; +type CodeEditorContentPadding = 'default' | 'comfortable'; + +const setCodeEditorTheme = ( + monaco: Monaco, + theme: ThemeType, + colorScheme: 'light' | 'dark', +) => { + monaco.editor.defineTheme( + BASE_CODE_EDITOR_THEME_ID, + getBaseCodeEditorTheme(theme, colorScheme), + ); + monaco.editor.setTheme(BASE_CODE_EDITOR_THEME_ID); +}; type CodeEditorProps = Pick< EditorProps, @@ -23,6 +36,8 @@ type CodeEditorProps = Pick< isLoading?: boolean; transparentBackground?: boolean; resizable?: boolean; + contentPadding?: CodeEditorContentPadding; + autoHeight?: boolean; }; export const CodeEditor = ({ @@ -38,13 +53,18 @@ export const CodeEditor = ({ isLoading = false, options, resizable = false, + contentPadding = 'default', + autoHeight = false, }: CodeEditorProps) => { - const { theme } = useContext(ThemeContext); + const { theme, colorScheme } = useContext(ThemeContext); const [monaco, setMonaco] = useState(undefined); const [editor, setEditor] = useState< editor.IStandaloneCodeEditor | undefined >(undefined); const [isEditorFocused, setIsEditorFocused] = useState(false); + const [autoHeightContentHeight, setAutoHeightContentHeight] = useState< + number | undefined + >(undefined); const numericHeight = typeof height === 'number' ? height : 450; const { @@ -56,7 +76,28 @@ export const CodeEditor = ({ initialSize: numericHeight, }); - const currentHeight = resizable ? resizableHeight : height; + const shouldAutoHeight = autoHeight && !resizable; + const codeEditorPadding = + typeof theme.spacingMultiplicator === 'number' + ? theme.spacingMultiplicator * 4 + : undefined; + const currentHeight = shouldAutoHeight + ? (autoHeightContentHeight ?? height) + : resizable + ? resizableHeight + : height; + const contentPaddingOptions = { + ...(contentPadding === 'comfortable' + ? { + lineDecorationsWidth: codeEditorPadding, + } + : {}), + padding: { + bottom: codeEditorPadding, + top: codeEditorPadding, + }, + }; + const { padding: _callerPadding, ...editorOptions } = options ?? {}; const setModelMarkers = ( editor: editor.IStandaloneCodeEditor | undefined, @@ -78,6 +119,43 @@ export const CodeEditor = ({ } }; + useEffect(() => { + if (!isDefined(monaco)) { + return; + } + + setCodeEditorTheme(monaco, theme, colorScheme); + }, [colorScheme, monaco, theme]); + + // Drive the container height from Monaco's content height; the editor's + // default automaticLayout then re-fits the canvas (no manual layout needed). + useEffect(() => { + if (!shouldAutoHeight || !isDefined(editor)) { + setAutoHeightContentHeight(undefined); + return; + } + + const updateAutoHeight = () => { + const nextHeight = editor.getContentHeight(); + + if (!Number.isFinite(nextHeight) || nextHeight <= 0) { + return; + } + + setAutoHeightContentHeight((currentHeight) => + currentHeight === nextHeight ? currentHeight : nextHeight, + ); + }; + + updateAutoHeight(); + + const disposable = editor.onDidContentSizeChange(updateAutoHeight); + + return () => { + disposable.dispose(); + }; + }, [editor, shouldAutoHeight]); + return isLoading ? (
{ setIsEditorFocused(true); @@ -143,6 +217,10 @@ export const CodeEditor = ({ options={{ formatOnPaste: true, formatOnType: true, + fontFamily: theme.code.font.family, + bracketPairColorization: { + enabled: false, + }, overviewRulerLanes: 0, scrollBeyondLastLine: false, scrollbar: { @@ -152,7 +230,8 @@ export const CodeEditor = ({ minimap: { enabled: false, }, - ...options, + ...editorOptions, + ...contentPaddingOptions, }} />
diff --git a/packages/twenty-ui/src/input/code-editor/theme/utils/getBaseCodeEditorTheme.ts b/packages/twenty-ui/src/input/code-editor/theme/utils/getBaseCodeEditorTheme.ts index 9a5134d865..433674776b 100644 --- a/packages/twenty-ui/src/input/code-editor/theme/utils/getBaseCodeEditorTheme.ts +++ b/packages/twenty-ui/src/input/code-editor/theme/utils/getBaseCodeEditorTheme.ts @@ -3,6 +3,10 @@ import { type editor } from 'monaco-editor'; import { isDefined } from '@ui/utilities/utils/isDefined'; const convertColorToHex = (color: string): string => { + if (color.trim() === 'transparent') { + return '#00000000'; + } + const displayP3Match = color.match( /color\(display-p3\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)(?:\s+\/\s+([\d.]+))?\)/, ); @@ -33,35 +37,81 @@ const convertColorToHex = (color: string): string => { export const getBaseCodeEditorTheme = ( theme: ThemeType, + colorScheme: 'light' | 'dark', ): editor.IStandaloneThemeData => { return { - base: 'vs', + base: colorScheme === 'dark' ? 'vs-dark' : 'vs', inherit: true, rules: [ { token: '', - foreground: convertColorToHex(theme.code.text.gray), - fontStyle: 'bold', + foreground: convertColorToHex(theme.font.color.secondary), }, { token: 'keyword', - foreground: convertColorToHex(theme.code.text.sky), + foreground: convertColorToHex(theme.color.pink11), }, { - token: 'delimiter', - foreground: convertColorToHex(theme.code.text.gray), + token: 'keyword.control', + foreground: convertColorToHex(theme.color.pink11), + }, + { + token: 'keyword.json', + foreground: convertColorToHex(theme.color.orange11), + }, + { + token: 'number', + foreground: convertColorToHex(theme.color.orange11), + }, + { + token: 'number.json', + foreground: convertColorToHex(theme.color.orange11), + }, + { + token: 'regexp', + foreground: convertColorToHex(theme.color.orange11), + }, + { + token: 'type', + foreground: convertColorToHex(theme.color.green11), + }, + { + token: 'attribute.name', + foreground: convertColorToHex(theme.color.blue11), + }, + { + token: 'tag', + foreground: convertColorToHex(theme.color.pink11), }, { token: 'string', - foreground: convertColorToHex(theme.code.text.pink), + foreground: convertColorToHex(theme.color.green11), + }, + { + token: 'string.key.json', + foreground: convertColorToHex(theme.color.blue11), + }, + { + token: 'delimiter', + foreground: convertColorToHex(theme.font.color.light), + }, + { + token: 'delimiter.bracket.json', + foreground: convertColorToHex(theme.font.color.light), + }, + { + token: 'string.value.json', + foreground: convertColorToHex(theme.color.green11), }, { token: 'comment', - foreground: convertColorToHex(theme.code.text.orange), + foreground: convertColorToHex(theme.font.color.light), + fontStyle: 'italic', }, ], colors: { - 'editor.background': '#00000000', + 'editor.background': convertColorToHex('transparent'), + 'editor.foreground': convertColorToHex(theme.font.color.secondary), 'editorCursor.foreground': convertColorToHex(theme.font.color.primary), 'editorLineNumber.foreground': convertColorToHex( theme.font.color.extraLight, @@ -72,6 +122,22 @@ export const getBaseCodeEditorTheme = ( 'editor.lineHighlightBackground': convertColorToHex( theme.background.tertiary, ), + 'editor.selectionBackground': convertColorToHex( + theme.background.transparent.blue, + ), + 'editor.inactiveSelectionBackground': convertColorToHex( + theme.background.transparent.light, + ), + 'editorIndentGuide.background1': convertColorToHex( + theme.border.color.light, + ), + 'editorIndentGuide.activeBackground1': convertColorToHex( + theme.border.color.medium, + ), + 'editorBracketMatch.background': convertColorToHex( + theme.background.transparent.light, + ), + 'editorBracketMatch.border': convertColorToHex(theme.border.color.medium), }, }; };