this.onChangeContent(newContent)}
onContentSelected={(selectedContent: string, selection: monaco.Selection) =>
this.onSelectedContent(selectedContent, selection)
diff --git a/src/Explorer/Tabs/UserDefinedFunctionTabContent.tsx b/src/Explorer/Tabs/UserDefinedFunctionTabContent.tsx
index 30d64e750..e981104ae 100644
--- a/src/Explorer/Tabs/UserDefinedFunctionTabContent.tsx
+++ b/src/Explorer/Tabs/UserDefinedFunctionTabContent.tsx
@@ -3,7 +3,7 @@ import { Label, TextField } from "@fluentui/react";
import { FluentProvider, webDarkTheme, webLightTheme } from "@fluentui/react-components";
import { KeyboardAction } from "KeyboardShortcuts";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
-import { isDarkMode } from "hooks/useTheme";
+import { useThemeStore } from "hooks/useTheme";
import React, { Component } from "react";
import DiscardIcon from "../../../images/discard.svg";
import SaveIcon from "../../../images/save-cosmos.svg";
@@ -259,7 +259,7 @@ export default class UserDefinedFunctionTabContent extends Component<
render(): JSX.Element {
const { udfId, udfBody, isUdfIdEditable } = this.state;
- const currentTheme = isDarkMode ? webDarkTheme : webLightTheme;
+ const currentTheme = useThemeStore.getState().isDarkMode ? webDarkTheme : webLightTheme;
return (
diff --git a/src/Main.tsx b/src/Main.tsx
index f96fb68d0..60a30a912 100644
--- a/src/Main.tsx
+++ b/src/Main.tsx
@@ -63,7 +63,7 @@ import { appThemeFabric } from "./Platform/Fabric/FabricTheme";
import "./Shared/appInsights";
import { useConfig } from "./hooks/useConfig";
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
-import { isDarkMode } from "./hooks/useTheme";
+import { useThemeStore } from "./hooks/useTheme";
import "./less/DarkModeMenus.less";
import "./less/ThemeSystem.less";
// Initialize icons before React is loaded
@@ -153,7 +153,18 @@ const App = (): JSX.Element => {
};
const Root: React.FC = () => {
+ // Use React state to track isDarkMode and subscribe to changes
+ const [isDarkMode, setIsDarkMode] = React.useState(useThemeStore.getState().isDarkMode);
const currentTheme = isDarkMode ? webDarkTheme : webLightTheme;
+
+ // Subscribe to theme changes
+ React.useEffect(() => {
+ return useThemeStore.subscribe(
+ (state) => {
+ setIsDarkMode(state.isDarkMode);
+ }
+ );
+ }, []);
useEffect(() => {
if (isDarkMode) {
diff --git a/src/hooks/useTheme.tsx b/src/hooks/useTheme.tsx
index d5370c454..087e6d901 100644
--- a/src/hooks/useTheme.tsx
+++ b/src/hooks/useTheme.tsx
@@ -1,5 +1,30 @@
import { useFluent } from "@fluentui/react-components";
import React, { createContext, FC, ReactNode, useEffect, useState } from "react";
+import create from "zustand";
+
+interface ThemeSettings {
+ mode: number;
+}
+
+interface FxsTheme {
+ (): ThemeSettings;
+ subscribe: (context: null, callback: (update: ThemeSettings) => void) => void;
+}
+
+interface MsPortalFxSettings {
+ "fxs-theme"?: FxsTheme;
+ [key: string]: unknown;
+}
+
+declare global {
+ interface Window {
+ MsPortalFx?: {
+ Services: {
+ getSettings: () => Promise;
+ };
+ };
+ }
+}
interface ThemeContextType {
theme: "Light" | "Dark";
@@ -17,19 +42,129 @@ export const CustomThemeProvider: FC = ({ children, theme })
const isDarkMode = theme === "Dark";
return {children};
};
-export const isDarkMode = true;
-export const monacoTheme = isDarkMode ? "vs-dark" : "vs";
+
+export interface ThemeStore {
+ isDarkMode: boolean;
+ themeMode: number;
+ toggleTheme: () => void;
+}
+
+export const useThemeStore = create((set) => ({
+ isDarkMode: false,
+ themeMode: 0,
+ toggleTheme: () =>
+ set((state) => {
+ const newIsDarkMode = !state.isDarkMode;
+ const newThemeMode = newIsDarkMode ? 1 : 0;
+
+ if (newIsDarkMode) {
+ document.body.classList.add("isDarkMode");
+ } else {
+ document.body.classList.remove("isDarkMode");
+ }
+
+ // Save to localStorage for persistence
+ localStorage.setItem("cosmos-explorer-theme", newIsDarkMode ? "dark" : "light");
+ localStorage.setItem("cosmos-explorer-theme-mode", String(newThemeMode));
+
+ return {
+ isDarkMode: newIsDarkMode,
+ themeMode: newThemeMode,
+ };
+ }),
+}));
+
+// Initialize the theme from localStorage or MsPortalFx if available
+if (typeof window !== "undefined") {
+ // Try to initialize from MsPortalFx.Services if available
+ try {
+ if (window.MsPortalFx && window.MsPortalFx.Services) {
+ window.MsPortalFx.Services.getSettings()
+ .then((settings: MsPortalFxSettings) => {
+ if (settings["fxs-theme"]) {
+ const theme = settings["fxs-theme"];
+
+ // Initial theme value
+ const initialTheme = theme();
+ if (initialTheme && typeof initialTheme.mode === "number") {
+ const isDark = initialTheme.mode === 1;
+ useThemeStore.setState({
+ isDarkMode: isDark,
+ themeMode: initialTheme.mode,
+ });
+
+ if (isDark) {
+ document.body.classList.add("isDarkMode");
+ } else {
+ document.body.classList.remove("isDarkMode");
+ }
+ }
+
+ theme.subscribe(null, (themeUpdate: ThemeSettings) => {
+ if (themeUpdate && typeof themeUpdate.mode === "number") {
+ const isDark = themeUpdate.mode === 1;
+ useThemeStore.setState({
+ isDarkMode: isDark,
+ themeMode: themeUpdate.mode,
+ });
+
+ if (isDark) {
+ document.body.classList.add("isDarkMode");
+ } else {
+ document.body.classList.remove("isDarkMode");
+ }
+ }
+ });
+ }
+ })
+ .catch(() => {
+ fallbackToLocalStorage();
+ });
+ } else {
+ fallbackToLocalStorage();
+ }
+ } catch (error) {
+ fallbackToLocalStorage();
+ }
+}
+
+function fallbackToLocalStorage() {
+ const savedTheme = localStorage.getItem("cosmos-explorer-theme");
+ const savedThemeMode = localStorage.getItem("cosmos-explorer-theme-mode");
+
+ if (savedTheme || savedThemeMode) {
+ const isDark = savedTheme === "dark" || savedThemeMode === "1";
+ const themeMode = isDark ? 1 : 0;
+
+ useThemeStore.setState({
+ isDarkMode: isDark,
+ themeMode: themeMode,
+ });
+
+ if (isDark) {
+ document.body.classList.add("isDarkMode");
+ }
+ }
+}
+
+// Dynamic exports that use the theme store
+export const isDarkMode = () => useThemeStore.getState().isDarkMode;
+
+export const useMonacoTheme = () => {
+ const { isDarkMode } = useThemeStore();
+ return isDarkMode ? "vs-dark" : "vs";
+};
+
+export const monacoTheme = () => (useThemeStore.getState().isDarkMode ? "vs-dark" : "vs");
export const useTheme = () => {
const { targetDocument } = useFluent();
const context = React.useContext(ThemeContext);
const [isDarkMode, setIsDarkMode] = useState(() => {
- // First check if we're in a theme context
if (context) {
return context.isDarkMode;
}
- // Fallback to checking body class
return targetDocument?.body.classList.contains("isDarkMode") ?? true;
});
@@ -38,20 +173,16 @@ export const useTheme = () => {
return undefined;
}
const checkTheme = () => {
- // First check if we're in a theme context
if (context) {
setIsDarkMode(context.isDarkMode);
return;
}
- // Fallback to checking body class
const hasDarkMode = targetDocument.body.classList.contains("isDarkMode");
setIsDarkMode(hasDarkMode);
};
- // Initial check
checkTheme();
- // Create a MutationObserver to watch for class changes
const observer = new MutationObserver(() => {
checkTheme();
});