(null);
const isApplyingExternalContentRef = React.useRef(false);
@@ -50,6 +55,9 @@ export const TextEditor = ({
readOnly: isReadOnly,
automaticLayout: true,
renderLineHighlight: "none",
+ ...(resolvedColorMode && {
+ theme: resolvedColorMode === "dark" ? "vs-dark" : "vs",
+ }),
});
editorRef.current = editor;
@@ -103,6 +111,13 @@ export const TextEditor = ({
editorRef.current?.updateOptions({ readOnly: isReadOnly });
}, [isReadOnly]);
+ React.useEffect(() => {
+ if (!editorRef.current) {
+ return;
+ }
+ monaco.editor.setTheme(resolvedColorMode === "dark" ? "vs-dark" : "vs");
+ }, [resolvedColorMode]);
+
return (
void) => {
+ if (normalized !== "system") {
+ return noopUnsubscribe;
+ }
+
+ const mediaQuery = getMediaQueryList();
+ if (mediaQuery == null) {
+ return noopUnsubscribe;
+ }
+ mediaQuery.addEventListener("change", onStoreChanges);
+ return () => {
+ mediaQuery.removeEventListener("change", onStoreChanges);
+ };
+ },
+ [normalized],
+ );
+
+ return useSyncExternalStore(
+ subscribe,
+ () => (normalized === "system" ? getSystemColorMode() : normalized),
+ () => (normalized === "system" ? getServerColorMode() : normalized),
+ );
+}
diff --git a/packages/text-editor/src/types/colorMode.ts b/packages/text-editor/src/types/colorMode.ts
new file mode 100644
index 00000000..650b6f3d
--- /dev/null
+++ b/packages/text-editor/src/types/colorMode.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2021-Present The Open Workflow Specification Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export type ColorMode = "light" | "dark" | "system";
+export type ResolvedColorMode = "light" | "dark";
diff --git a/packages/text-editor/stories/features/TextEditor.tsx b/packages/text-editor/stories/features/TextEditor.tsx
index 03c07c56..ddb75805 100644
--- a/packages/text-editor/stories/features/TextEditor.tsx
+++ b/packages/text-editor/stories/features/TextEditor.tsx
@@ -25,6 +25,7 @@ export const TextEditor = ({ ...props }: TextEditorProps) => {
language={props.language}
onContentChange={props.onContentChange}
isReadOnly={props.isReadOnly}
+ colorMode={props.colorMode}
/>
);
diff --git a/packages/text-editor/tests/__mocks__/monaco-editor.ts b/packages/text-editor/tests/__mocks__/monaco-editor.ts
index 245b1d01..607c28ca 100644
--- a/packages/text-editor/tests/__mocks__/monaco-editor.ts
+++ b/packages/text-editor/tests/__mocks__/monaco-editor.ts
@@ -72,9 +72,12 @@ export const simulateEditorContentChange = (value: string) => {
state.listener?.();
};
+export const mockSetTheme = vi.fn();
+
export default {
editor: {
create: mockEditorCreate,
+ setTheme: mockSetTheme,
setModelLanguage: mockSetModelLanguage,
},
};
diff --git a/packages/text-editor/tests/text-editor/TextEditor.test.tsx b/packages/text-editor/tests/text-editor/TextEditor.test.tsx
index 8883fe24..ac04080a 100644
--- a/packages/text-editor/tests/text-editor/TextEditor.test.tsx
+++ b/packages/text-editor/tests/text-editor/TextEditor.test.tsx
@@ -26,6 +26,7 @@ import {
mockModel,
mockSetModelLanguage,
simulateEditorContentChange,
+ mockSetTheme,
} from "../__mocks__/monaco-editor";
import { TextEditor, type TextEditorProps } from "../../src/TextEditor";
@@ -185,4 +186,44 @@ describe("TextEditor", () => {
expect(mockEditorCreate).toHaveBeenCalledTimes(2);
});
});
+ describe("theme", () => {
+ it("uses the light Monaco theme for light color mode", () => {
+ renderEditor({ colorMode: "light" });
+
+ expect(mockSetTheme).toHaveBeenCalledTimes(1);
+ expect(mockSetTheme).toHaveBeenCalledWith("vs");
+ });
+
+ it("uses the dark Monaco theme for dark color mode", () => {
+ renderEditor({ colorMode: "dark" });
+
+ expect(mockSetTheme).toHaveBeenCalledTimes(1);
+ expect(mockSetTheme).toHaveBeenCalledWith("vs-dark");
+ });
+
+ it("updates the Monaco theme when color mode changes", () => {
+ const { rerenderEditor } = renderEditor({
+ colorMode: "light",
+ });
+
+ expect(mockSetTheme).toHaveBeenCalledWith("vs");
+
+ mockSetTheme.mockClear();
+
+ rerenderEditor({ colorMode: "dark" });
+
+ expect(mockSetTheme).toHaveBeenCalledTimes(1);
+ expect(mockSetTheme).toHaveBeenCalledWith("vs-dark");
+ });
+
+ it("does not recreate Monaco when color mode changes", () => {
+ const { rerenderEditor } = renderEditor({
+ colorMode: "light",
+ });
+
+ rerenderEditor({ colorMode: "dark" });
+
+ expect(mockEditorCreate).toHaveBeenCalledTimes(1);
+ });
+ });
});