diff --git a/packages/react-code-editor/src/components/CodeEditor/CodeEditor.tsx b/packages/react-code-editor/src/components/CodeEditor/CodeEditor.tsx index 14026b20177..bae31a405a0 100644 --- a/packages/react-code-editor/src/components/CodeEditor/CodeEditor.tsx +++ b/packages/react-code-editor/src/components/CodeEditor/CodeEditor.tsx @@ -13,7 +13,7 @@ import { import { Popover, PopoverProps } from '@patternfly/react-core/dist/esm/components/Popover'; import { TooltipPosition } from '@patternfly/react-core/dist/esm/components/Tooltip'; import { getResizeObserver } from '@patternfly/react-core/dist/esm/helpers/resizeObserver'; -import Editor, { EditorProps, Monaco } from '@monaco-editor/react'; +import Editor, { BeforeMount, EditorProps, Monaco } from '@monaco-editor/react'; import type { editor } from 'monaco-editor'; import CopyIcon from '@patternfly/react-icons/dist/esm/icons/copy-icon'; import UploadIcon from '@patternfly/react-icons/dist/esm/icons/upload-icon'; @@ -23,6 +23,7 @@ import HelpIcon from '@patternfly/react-icons/dist/esm/icons/help-icon'; import Dropzone, { FileRejection } from 'react-dropzone'; import { CodeEditorContext } from './CodeEditorUtils'; import { CodeEditorControl } from './CodeEditorControl'; +import { defineThemes } from './CodeEditorTheme'; export type ChangeHandler = (value: string, event: editor.IModelContentChangedEvent) => void; export type EditorDidMount = (editor: editor.IStandaloneCodeEditor, monaco: Monaco) => void; @@ -366,6 +367,11 @@ export const CodeEditor = ({ }; }, []); + const editorBeforeMount: BeforeMount = (monaco) => { + defineThemes(monaco.editor); + editorProps?.beforeMount?.(monaco); + }; + const editorDidMount: EditorDidMount = (editor, monaco) => { // eslint-disable-next-line no-bitwise editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Tab, () => wrapperRef.current.focus()); @@ -428,6 +434,7 @@ export const CodeEditor = ({ }; const editorOptions: editor.IStandaloneEditorConstructionOptions = { + fontFamily: 'var(--pf-t--global--font--family--mono)', scrollBeyondLastLine: height !== 'sizeToFit', readOnly: isReadOnly, cursorStyle: 'line', @@ -571,8 +578,9 @@ export const CodeEditor = ({ onChange={onModelChange} onMount={editorDidMount} loading={loading} - theme={isDarkTheme ? 'vs-dark' : 'vs-light'} + theme={isDarkTheme ? 'pf-v6-theme-dark' : 'pf-v6-theme-light'} {...editorProps} + beforeMount={editorBeforeMount} /> ); diff --git a/packages/react-code-editor/src/components/CodeEditor/CodeEditorTheme.ts b/packages/react-code-editor/src/components/CodeEditor/CodeEditorTheme.ts new file mode 100644 index 00000000000..fef32258260 --- /dev/null +++ b/packages/react-code-editor/src/components/CodeEditor/CodeEditorTheme.ts @@ -0,0 +1,81 @@ +import { colours } from './themeTokenMapping'; +import type { editor as monacoEditor } from 'monaco-editor/esm/vs/editor/editor.api'; + +const createTheme = (mode: 'light' | 'dark'): monacoEditor.IStandaloneThemeData => ({ + base: (mode === 'light' ? 'vs' : 'vs-dark') as monacoEditor.BuiltinTheme, + inherit: true, + colors: { + 'editor.background': colours.background[mode], + 'editor.foreground': colours.foreground[mode], + 'editorLineNumber.activeForeground': colours.foreground[mode], + 'editorLineNumber.foreground': colours.secondaryForeground[mode] + }, + rules: [ + { token: '', foreground: colours.foreground[mode], background: colours.background[mode] }, + { token: 'invalid', foreground: colours.red[mode] }, + { token: 'emphasis', fontStyle: 'italic' }, + { token: 'strong', fontStyle: 'bold' }, + + { token: 'variable', foreground: colours.blue[mode] }, + { token: 'variable.predefined', foreground: colours.teal[mode] }, + { token: 'constant', foreground: colours.orange[mode] }, + { token: 'comment', foreground: colours.gray[mode] }, + { token: 'number', foreground: colours.orange[mode] }, + { token: 'number.hex', foreground: colours.blue[mode] }, + { token: 'regexp', foreground: colours.red[mode] }, + { token: 'annotation', foreground: colours.purple[mode] }, + { token: 'type', foreground: colours.yellow[mode] }, + + { token: 'delimiter', foreground: colours.foreground[mode] }, + { token: 'delimiter.html', foreground: colours.gray[mode] }, + { token: 'delimiter.xml', foreground: colours.blue[mode] }, + + { token: 'tag', foreground: colours.red[mode] }, + { token: 'tag.id.jade', foreground: colours.teal[mode] }, + { token: 'tag.class.jade', foreground: colours.teal[mode] }, + { token: 'meta.scss', foreground: colours.red[mode] }, + { token: 'metatag', foreground: colours.orange[mode] }, + { token: 'metatag.content.html', foreground: colours.red[mode] }, + { token: 'metatag.html', foreground: colours.gray[mode] }, + { token: 'metatag.xml', foreground: colours.gray[mode] }, + { token: 'metatag.php', fontStyle: 'bold' }, + + { token: 'key', foreground: colours.orange[mode] }, + { token: 'string.key.json', foreground: colours.red[mode] }, + { token: 'string.value.json', foreground: colours.blue[mode] }, + + { token: 'attribute.name', foreground: colours.red[mode] }, + { token: 'attribute.value', foreground: colours.blue[mode] }, + { token: 'attribute.value.number', foreground: colours.orange[mode] }, + { token: 'attribute.value.unit', foreground: colours.orange[mode] }, + { token: 'attribute.value.html', foreground: colours.blue[mode] }, + { token: 'attribute.value.xml', foreground: colours.blue[mode] }, + + { token: 'string', foreground: colours.green[mode] }, + { token: 'string.html', foreground: colours.green[mode] }, + { token: 'string.sql', foreground: colours.green[mode] }, + { token: 'string.yaml', foreground: colours.green[mode] }, + + { token: 'keyword', foreground: colours.purple[mode] }, + { token: 'keyword.json', foreground: colours.purple[mode] }, + { token: 'keyword.flow', foreground: colours.purple[mode] }, + { token: 'keyword.flow.scss', foreground: colours.purple[mode] }, + + { token: 'operator.scss', foreground: colours.foreground[mode] }, + { token: 'operator.sql', foreground: colours.foreground[mode] }, + { token: 'operator.swift', foreground: colours.foreground[mode] }, + { token: 'predefined.sql', foreground: colours.purple[mode] } + ] +}); + +/** + * Define the themes `pf-v6-theme-light` and + * `pf-v6-theme-dark` for an instance of Monaco editor. + * + * Note that base tokens must be used as Monaco will throw a runtime + * error for CSS variables. + */ +export const defineThemes = (editor: typeof monacoEditor) => { + editor.defineTheme('pf-v6-theme-light', createTheme('light')); + editor.defineTheme('pf-v6-theme-dark', createTheme('dark')); +}; diff --git a/packages/react-code-editor/src/components/CodeEditor/themeTokenMapping.ts b/packages/react-code-editor/src/components/CodeEditor/themeTokenMapping.ts new file mode 100644 index 00000000000..461edea37fc --- /dev/null +++ b/packages/react-code-editor/src/components/CodeEditor/themeTokenMapping.ts @@ -0,0 +1,81 @@ +import backgroundDark from '@patternfly/react-tokens/dist/esm/t_color_gray_90'; +import textColorDark from '@patternfly/react-tokens/dist/esm/t_color_white'; +import textColorSubtleDark from '@patternfly/react-tokens/dist/esm/t_color_gray_30'; + +import redDark from '@patternfly/react-tokens/dist/esm/t_color_red_30'; +import orangeredDark from '@patternfly/react-tokens/dist/esm/t_color_red_orange_30'; +import orangeDark from '@patternfly/react-tokens/dist/esm/t_color_orange_30'; +import yellowDark from '@patternfly/react-tokens/dist/esm/t_color_yellow_30'; +import greenDark from '@patternfly/react-tokens/dist/esm/t_color_green_30'; +import tealDark from '@patternfly/react-tokens/dist/esm/t_color_teal_30'; +import blueDark from '@patternfly/react-tokens/dist/esm/t_color_blue_30'; +import purpleDark from '@patternfly/react-tokens/dist/esm/t_color_purple_30'; +import grayDark from '@patternfly/react-tokens/dist/esm/t_color_gray_30'; + +import backgroundLight from '@patternfly/react-tokens/dist/esm/t_color_white'; +import textColorLight from '@patternfly/react-tokens/dist/esm/t_global_text_color_100'; +import textColorSubtleLight from '@patternfly/react-tokens/dist/esm/t_global_text_color_200'; + +import redLight from '@patternfly/react-tokens/dist/esm/t_color_red_50'; +import orangeredLight from '@patternfly/react-tokens/dist/esm/t_color_red_orange_50'; +import orangeLight from '@patternfly/react-tokens/dist/esm/t_color_orange_50'; +import yellowLight from '@patternfly/react-tokens/dist/esm/t_color_yellow_50'; +import greenLight from '@patternfly/react-tokens/dist/esm/t_color_green_50'; +import tealLight from '@patternfly/react-tokens/dist/esm/t_color_teal_50'; +import blueLight from '@patternfly/react-tokens/dist/esm/t_color_blue_50'; +import purpleLight from '@patternfly/react-tokens/dist/esm/t_color_purple_50'; +import grayLight from '@patternfly/react-tokens/dist/esm/t_color_gray_50'; + +/** + * This file maps the theme tokens from PatternFly to a format that can be used in the Monaco editor. + */ +export const colours = { + background: { + dark: backgroundDark.value, + light: backgroundLight.value + }, + foreground: { + dark: textColorDark.value, + light: textColorLight.value + }, + secondaryForeground: { + dark: textColorSubtleDark.value, + light: textColorSubtleLight.value + }, + red: { + dark: redDark.value, + light: redLight.value + }, + orangered: { + dark: orangeredLight.value, + light: orangeredDark.value + }, + orange: { + dark: orangeDark.value, + light: orangeLight.value + }, + yellow: { + dark: yellowDark.value, + light: yellowLight.value + }, + green: { + dark: greenDark.value, + light: greenLight.value + }, + teal: { + dark: tealDark.value, + light: tealLight.value + }, + blue: { + dark: blueDark.value, + light: blueLight.value + }, + purple: { + dark: purpleDark.value, + light: purpleLight.value + }, + gray: { + dark: grayDark.value, + light: grayLight.value + } +};