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.
This commit is contained in:
committed by
GitHub
parent
ce92a1f1e1
commit
a67fdf2dac
@@ -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]};
|
||||
`;
|
||||
|
||||
@@ -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]};
|
||||
`;
|
||||
|
||||
|
||||
+1
@@ -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`
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<svg width="180" height="180" viewBox="0 0 180 180" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_19_13)">
|
||||
<path d="M18 84.8528L85.8822 16.9706C95.2548 7.59798 110.451 7.59798 119.823 16.9706C129.196 26.3431 129.196 41.5391 119.823 50.9117L68.5581 102.177" stroke="currentColor" stroke-width="12" stroke-linecap="round" />
|
||||
<path d="M69.2652 101.47L119.823 50.9117C129.196 41.5391 144.392 41.5391 153.765 50.9117L154.118 51.2652C163.491 60.6378 163.491 75.8338 154.118 85.2063L92.7248 146.6C89.6006 149.724 89.6006 154.789 92.7248 157.913L105.331 170.52" stroke="currentColor" stroke-width="12" stroke-linecap="round" />
|
||||
<path d="M102.853 33.9411L52.6482 84.1457C43.2756 93.5183 43.2756 108.714 52.6482 118.087C62.0208 127.459 77.2167 127.459 86.5893 118.087L136.794 67.8822" stroke="currentColor" stroke-width="12" stroke-linecap="round" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_19_13">
|
||||
<rect width="180" height="180" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1016 B |
+42
-108
@@ -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 = '<YOUR_API_KEY>';
|
||||
|
||||
if (!token.includes(apiKeyPlaceholder)) {
|
||||
return <StyledJsonString key={key}>{token}</StyledJsonString>;
|
||||
}
|
||||
|
||||
const [beforePlaceholder, afterPlaceholder] = token.split(apiKeyPlaceholder);
|
||||
|
||||
return (
|
||||
<span key={key}>
|
||||
<StyledJsonString>{beforePlaceholder}</StyledJsonString>
|
||||
<StyledJsonPlaceholder>{apiKeyPlaceholder}</StyledJsonPlaceholder>
|
||||
<StyledJsonString>{afterPlaceholder}</StyledJsonString>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
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(
|
||||
<StyledJsonKey key={`json-key-${tokenIndex}`}>
|
||||
{quotedString}
|
||||
</StyledJsonKey>,
|
||||
);
|
||||
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 (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Connect your AI assistant`}
|
||||
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.`}
|
||||
description={t`Paste this configuration into MCP-aware clients like Claude, Codex, or Cursor.`}
|
||||
/>
|
||||
<CoreEditorHeader
|
||||
leftNodes={[
|
||||
<StyledMcpEditorHeaderTitle>
|
||||
<StyledMcpIcon aria-hidden focusable={false} />
|
||||
<span>{t`MCP client configuration`}</span>
|
||||
</StyledMcpEditorHeaderTitle>,
|
||||
]}
|
||||
rightNodes={[<LightCopyIconButton copyText={mcpConfig} />]}
|
||||
/>
|
||||
<CodeEditor
|
||||
value={mcpConfig}
|
||||
language="json"
|
||||
variant="with-header"
|
||||
contentPadding="comfortable"
|
||||
autoHeight
|
||||
options={{
|
||||
readOnly: true,
|
||||
domReadOnly: true,
|
||||
lineNumbers: 'off',
|
||||
lineNumbersMinChars: 0,
|
||||
folding: false,
|
||||
glyphMargin: false,
|
||||
scrollBeyondLastLine: false,
|
||||
renderLineHighlight: 'none',
|
||||
wordWrap: 'on',
|
||||
}}
|
||||
/>
|
||||
<Card rounded>
|
||||
<CardContent>
|
||||
<StyledMcpConfigContainer>
|
||||
<StyledPre>{getHighlightedMcpConfig(mcpConfig)}</StyledPre>
|
||||
<StyledCopyButtonContainer>
|
||||
<LightIconButton
|
||||
Icon={IconCopy}
|
||||
size="small"
|
||||
accent="tertiary"
|
||||
aria-label={t`Copy MCP config`}
|
||||
title={t`Copy MCP config`}
|
||||
onClick={() =>
|
||||
copyToClipboard(mcpConfig, t`MCP config copied to clipboard`)
|
||||
}
|
||||
/>
|
||||
</StyledCopyButtonContainer>
|
||||
</StyledMcpConfigContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
-2
@@ -358,7 +358,6 @@ export const WorkflowEditActionCode = ({
|
||||
readOnly: actionOptions.readonly,
|
||||
domReadOnly: actionOptions.readonly,
|
||||
scrollBeyondLastLine: false,
|
||||
padding: { top: 4, bottom: 4 },
|
||||
}}
|
||||
/>
|
||||
</StyledFullScreenCodeEditorContainer>
|
||||
@@ -397,7 +396,6 @@ export const WorkflowEditActionCode = ({
|
||||
readOnly: actionOptions.readonly,
|
||||
domReadOnly: actionOptions.readonly,
|
||||
scrollBeyondLastLine: false,
|
||||
padding: { top: 4, bottom: 4 },
|
||||
lineNumbersMinChars: 2,
|
||||
fixedOverflowWidgets: true,
|
||||
}}
|
||||
|
||||
@@ -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]};
|
||||
`;
|
||||
|
||||
@@ -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<Monaco | undefined>(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 ? (
|
||||
<StyledEditorLoader height={currentHeight} variant={variant}>
|
||||
<Loader />
|
||||
@@ -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,
|
||||
}}
|
||||
/>
|
||||
</StyledEditorWrapper>
|
||||
|
||||
+75
-9
@@ -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),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<Monaco | undefined>(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 ? (
|
||||
<div
|
||||
className={styles.editorLoader}
|
||||
@@ -115,11 +193,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);
|
||||
@@ -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,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user