Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 55 additions & 21 deletions src/web-ui/src/infrastructure/theme/integrations/MonacoThemeSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,28 @@ const log = createLogger('MonacoThemeSync');

const SEMANTIC_HIGHLIGHTING_RULES = BitFunDarkTheme.rules;

function getBitfunLightMonacoTheme(): monaco.editor.IStandaloneThemeData {
return {
base: 'vs',
inherit: true,
rules: SEMANTIC_HIGHLIGHTING_RULES,
colors: convertColorsToHex({
'focusBorder': '#00000000',
'contrastBorder': '#00000000',
'diffEditor.insertedTextBorder': '#00000000',
'diffEditor.removedTextBorder': '#00000000',

'editor.selectionBackground': 'rgba(15, 23, 42, 0.14)',
'editor.selectionForeground': '#1e293b',
'editor.inactiveSelectionBackground': 'rgba(15, 23, 42, 0.09)',
'editor.selectionHighlightBackground': 'rgba(15, 23, 42, 0.10)',
'editor.selectionHighlightBorder': 'rgba(15, 23, 42, 0.22)',
'editor.wordHighlightBackground': 'rgba(15, 23, 42, 0.07)',
'editor.wordHighlightStrongBackground': 'rgba(15, 23, 42, 0.11)',
}),
};
}


function convertToHexColor(color: string): string {
if (!color) return color;
Expand Down Expand Up @@ -85,28 +107,8 @@ export class MonacoThemeSync {
if (theme.type === 'dark') {
targetThemeId = 'bitfun-dark';
} else {

targetThemeId = 'bitfun-light';

monaco.editor.defineTheme('bitfun-light', {
base: 'vs',
inherit: true,
rules: SEMANTIC_HIGHLIGHTING_RULES,
colors: convertColorsToHex({
'focusBorder': '#00000000',
'contrastBorder': '#00000000',
'diffEditor.insertedTextBorder': '#00000000',
'diffEditor.removedTextBorder': '#00000000',

'editor.selectionBackground': 'rgba(15, 23, 42, 0.14)',
'editor.selectionForeground': '#1e293b',
'editor.inactiveSelectionBackground': 'rgba(15, 23, 42, 0.09)',
'editor.selectionHighlightBackground': 'rgba(15, 23, 42, 0.10)',
'editor.selectionHighlightBorder': 'rgba(15, 23, 42, 0.22)',
'editor.wordHighlightBackground': 'rgba(15, 23, 42, 0.07)',
'editor.wordHighlightStrongBackground': 'rgba(15, 23, 42, 0.11)',
})
});
monaco.editor.defineTheme('bitfun-light', getBitfunLightMonacoTheme());
}
log.debug('Using builtin theme', { themeId: targetThemeId });
}
Expand Down Expand Up @@ -145,6 +147,38 @@ export class MonacoThemeSync {
getCurrentThemeId(): string | null {
return this.currentThemeId;
}

/**
* Resolves which Monaco theme id should be active for the given app theme
* (same rules as {@link syncTheme}).
*/
getTargetMonacoThemeId(theme: ThemeConfig): string {
if (theme.monaco) {
return theme.id;
}
return theme.type === 'dark' ? 'bitfun-dark' : 'bitfun-light';
}

/**
* Registers BitFun built-in and optional custom Monaco themes on the given Monaco instance.
* Use from the Monaco React wrapper `beforeMount` hook so themes exist on the loader's Monaco
* before the editor is created (avoids falling back to the default light theme).
*/
registerThemesForEditorInstance(monacoInstance: typeof monaco, theme: ThemeConfig): string {
try {
monacoInstance.editor.defineTheme('bitfun-dark', BitFunDarkTheme);
monacoInstance.editor.defineTheme('bitfun-light', getBitfunLightMonacoTheme());

if (theme.monaco) {
monacoInstance.editor.defineTheme(theme.id, this.convertToMonacoTheme(theme));
return theme.id;
}
return this.getTargetMonacoThemeId(theme);
} catch (error) {
log.error('registerThemesForEditorInstance failed', error);
return 'bitfun-dark';
}
}


private convertToMonacoTheme(theme: ThemeConfig): monaco.editor.IStandaloneThemeData {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import Editor from '@monaco-editor/react';
import type * as Monaco from 'monaco-editor';
import { monacoInitManager } from '@/tools/editor/services/MonacoInitManager';
import { useTheme, themeService, monacoThemeSync } from '@/infrastructure/theme';

export interface MermaidSyntaxHighlighterProps {
value: string;
Expand All @@ -26,6 +27,13 @@ export const MermaidSyntaxHighlighter: React.FC<MermaidSyntaxHighlighterProps> =
showLineNumbers = true,
onCursorPositionChange
}) => {
const { theme: appTheme } = useTheme();

const monacoThemeId = useMemo(() => {
const t = appTheme ?? themeService.getCurrentTheme();
return t ? monacoThemeSync.getTargetMonacoThemeId(t) : 'bitfun-dark';
}, [appTheme]);

const [isReady, setIsReady] = useState(monacoInitManager.isInitialized());
const [initError, setInitError] = useState<string | null>(null);
const editorRef = useRef<Monaco.editor.IStandaloneCodeEditor | null>(null);
Expand Down Expand Up @@ -60,6 +68,23 @@ export const MermaidSyntaxHighlighter: React.FC<MermaidSyntaxHighlighterProps> =
};
}, [isReady]);

const handleBeforeMount = useCallback((monaco: typeof Monaco) => {
const t = appTheme ?? themeService.getCurrentTheme();
if (t) {
monacoThemeSync.registerThemesForEditorInstance(monaco, t);
}
}, [appTheme]);

useEffect(() => {
const t = appTheme ?? themeService.getCurrentTheme();
const m = monacoInitManager.getMonaco();
if (!t || !m) {
return;
}
monacoThemeSync.registerThemesForEditorInstance(m, t);
m.editor.setTheme(monacoThemeSync.getTargetMonacoThemeId(t));
}, [appTheme]);

const options = useMemo<Monaco.editor.IStandaloneEditorConstructionOptions>(() => ({
readOnly,
lineNumbers: showLineNumbers ? 'on' : 'off',
Expand Down Expand Up @@ -127,9 +152,10 @@ export const MermaidSyntaxHighlighter: React.FC<MermaidSyntaxHighlighterProps> =
<Editor
path={modelPathRef.current}
language="mermaid"
theme="bitfun-dark"
theme={monacoThemeId}
value={value}
onChange={handleChange}
beforeMount={handleBeforeMount}
onMount={handleMount}
options={options}
loading=""
Expand Down
Loading