-
+ >
);
};
diff --git a/web/src/main.tsx b/web/src/main.tsx
index 73c687e6..4415ac46 100644
--- a/web/src/main.tsx
+++ b/web/src/main.tsx
@@ -18,25 +18,37 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
-import { ConfigProvider } from 'antd';
+import { ConfigProvider, theme } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import enUS from 'antd/locale/en_US';
import { LangProvider, useLang } from './i18n/LangContext';
+import { ThemeProvider, useTheme } from './theme/ThemeContext';
import App from './App';
import './index.css';
const ThemedApp = () => {
const { lang } = useLang();
+ const { darkMode } = useTheme();
return (
{
ReactDOM.createRoot(document.getElementById('root')!).render(
-
+
+
+ ,
);
diff --git a/web/src/test/setup.ts b/web/src/test/setup.ts
index 3bcbc099..a9f1da51 100644
--- a/web/src/test/setup.ts
+++ b/web/src/test/setup.ts
@@ -1,6 +1,7 @@
///
import '@testing-library/jest-dom/vitest';
+// Clean up localStorage between tests
beforeEach(() => {
localStorage.clear();
});
diff --git a/web/src/theme/ThemeContext.tsx b/web/src/theme/ThemeContext.tsx
new file mode 100644
index 00000000..6bac20e2
--- /dev/null
+++ b/web/src/theme/ThemeContext.tsx
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+import { createContext, useContext, useState, useEffect, type ReactNode } from 'react';
+
+interface ThemeContextType {
+ darkMode: boolean;
+ setDarkMode: (dark: boolean) => void;
+ toggleTheme: () => void;
+}
+
+const THEME_STORAGE_KEY = 'rocketmq-studio-theme';
+
+const ThemeContext = createContext({
+ darkMode: false,
+ setDarkMode: () => {},
+ toggleTheme: () => {},
+});
+
+/** Read initial theme preference from localStorage, fall back to system preference. */
+const getInitialDarkMode = (): boolean => {
+ try {
+ const stored = localStorage.getItem(THEME_STORAGE_KEY);
+ if (stored !== null) return stored === 'dark';
+ } catch {
+ // localStorage unavailable
+ }
+ return window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
+};
+
+export const ThemeProvider = ({ children }: { children: ReactNode }) => {
+ const [darkMode, setDarkMode] = useState(getInitialDarkMode);
+
+ // Persist theme choice to localStorage
+ useEffect(() => {
+ try {
+ localStorage.setItem(THEME_STORAGE_KEY, darkMode ? 'dark' : 'light');
+ } catch {
+ // localStorage unavailable
+ }
+ }, [darkMode]);
+
+ const toggleTheme = () => setDarkMode((prev) => !prev);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useTheme = () => useContext(ThemeContext);
+
+export default ThemeContext;
\ No newline at end of file
diff --git a/web/src/theme/__tests__/ThemeContext.test.tsx b/web/src/theme/__tests__/ThemeContext.test.tsx
new file mode 100644
index 00000000..3feb0cc6
--- /dev/null
+++ b/web/src/theme/__tests__/ThemeContext.test.tsx
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { ThemeProvider, useTheme } from '../ThemeContext';
+
+/** Helper component that displays theme state and provides toggle button. */
+const ThemeConsumer = () => {
+ const { darkMode, toggleTheme, setDarkMode } = useTheme();
+ return (
+