diff --git a/services/web/app/src/infrastructure/ExpressLocals.js b/services/web/app/src/infrastructure/ExpressLocals.js index 51c2c2186b..b4d62e1c12 100644 --- a/services/web/app/src/infrastructure/ExpressLocals.js +++ b/services/web/app/src/infrastructure/ExpressLocals.js @@ -201,6 +201,9 @@ module.exports = function (webRouter, privateApiRouter, publicApiRouter) { if (res.locals.isIEEE(brandVariation?.brand_id)) { return enableIeeeBranding ? 'ieee-' : '' } else if (userSettings && userSettings.overallTheme != null) { + if (!['', 'light-'].includes(userSettings.overallTheme)) { + return '' + } return userSettings.overallTheme } } @@ -350,6 +353,11 @@ module.exports = function (webRouter, privateApiRouter, publicApiRouter) { val: 'light-', path: res.locals.buildCssPath('light-'), }, + { + name: 'System', + val: 'system', + path: res.locals.buildCssPath(), + }, ] } next() diff --git a/services/web/frontend/js/features/editor-left-menu/hooks/use-set-overall-theme.tsx b/services/web/frontend/js/features/editor-left-menu/hooks/use-set-overall-theme.tsx index 6701c78d1f..a026da61e5 100644 --- a/services/web/frontend/js/features/editor-left-menu/hooks/use-set-overall-theme.tsx +++ b/services/web/frontend/js/features/editor-left-menu/hooks/use-set-overall-theme.tsx @@ -4,7 +4,7 @@ import { saveUserSettings } from '../utils/api' import { UserSettings } from '../../../../../types/user-settings' import { useUserSettingsContext } from '@/shared/context/user-settings-context' import getMeta from '@/utils/meta' -import { isIEEEBranded } from '@/utils/is-ieee-branded' +import { useActiveOverallTheme } from '@/shared/hooks/use-active-overall-theme' export default function useSetOverallTheme() { const { userSettings, setUserSettings } = useUserSettingsContext() @@ -16,13 +16,13 @@ export default function useSetOverallTheme() { }, [setUserSettings] ) + const activeOverallTheme = useActiveOverallTheme() useEffect(() => { // Sets the body's data-theme attribute for theming - const theme = - overallTheme === 'light-' && !isIEEEBranded() ? 'light' : 'default' - document.body.dataset.theme = theme - }, [overallTheme]) + document.body.dataset.theme = + activeOverallTheme === 'dark' ? 'default' : 'light' + }, [activeOverallTheme]) return useCallback( (newOverallTheme: UserSettings['overallTheme']) => { diff --git a/services/web/frontend/js/features/pdf-preview/components/visual-preview.tsx b/services/web/frontend/js/features/pdf-preview/components/visual-preview.tsx index b166ca4563..074d26501a 100644 --- a/services/web/frontend/js/features/pdf-preview/components/visual-preview.tsx +++ b/services/web/frontend/js/features/pdf-preview/components/visual-preview.tsx @@ -69,7 +69,7 @@ export const VisualPreview: FC<{ view: EditorView }> = ({ view }) => { fontSize: 14, fontFamily: 'monaco', lineHeight: 'normal', - overallTheme: 'light-', + activeOverallTheme: 'light', }), EditorView.theme({ '&.cm-editor': { diff --git a/services/web/frontend/js/features/source-editor/extensions/theme.ts b/services/web/frontend/js/features/source-editor/extensions/theme.ts index 2fd6a77533..3b3ac4afdf 100644 --- a/services/web/frontend/js/features/source-editor/extensions/theme.ts +++ b/services/web/frontend/js/features/source-editor/extensions/theme.ts @@ -3,12 +3,8 @@ import { Annotation, Compartment, TransactionSpec } from '@codemirror/state' import { syntaxHighlighting } from '@codemirror/language' import { classHighlighter } from './class-highlighter' import classNames from 'classnames' -import { - FontFamily, - LineHeight, - OverallTheme, - userStyles, -} from '@/shared/utils/styles' +import { FontFamily, LineHeight, userStyles } from '@/shared/utils/styles' +import { ActiveOverallTheme } from '@/shared/hooks/use-active-overall-theme' const optionsThemeConf = new Compartment() const selectedThemeConf = new Compartment() @@ -18,7 +14,7 @@ type Options = { fontSize: number fontFamily: FontFamily lineHeight: LineHeight - overallTheme: OverallTheme + activeOverallTheme: ActiveOverallTheme } export const theme = (options: Options) => [ @@ -58,7 +54,7 @@ const createThemeFromOptions = ({ fontSize = 12, fontFamily = 'monaco', lineHeight = 'normal', - overallTheme = '', + activeOverallTheme = 'dark', }: Options) => { // Theme styles that depend on settings. const styles = userStyles({ fontSize, fontFamily, lineHeight }) @@ -66,7 +62,9 @@ const createThemeFromOptions = ({ return [ EditorView.editorAttributes.of({ class: classNames( - overallTheme === '' ? 'overall-theme-dark' : 'overall-theme-light' + activeOverallTheme === 'dark' + ? 'overall-theme-dark' + : 'overall-theme-light' ), style: Object.entries({ '--font-size': styles.fontSize, diff --git a/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts b/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts index 8e46bdd7c5..fb29613f7f 100644 --- a/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts +++ b/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts @@ -59,6 +59,7 @@ import { usePermissionsContext } from '@/features/ide-react/context/permissions- import { useEditorPropertiesContext } from '@/features/ide-react/context/editor-properties-context' import { SearchQuery } from '@codemirror/search' import { beforeChangeDocEffect } from '@/features/source-editor/extensions/before-change-doc' +import { useActiveOverallTheme } from '@/shared/hooks/use-active-overall-theme' function useCodeMirrorScope(view: EditorView) { const { fileTreeData } = useFileTreeData() @@ -79,7 +80,6 @@ function useCodeMirrorScope(view: EditorView) { fontFamily, fontSize, lineHeight, - overallTheme, autoComplete, editorTheme, autoPairDelimiters, @@ -89,6 +89,7 @@ function useCodeMirrorScope(view: EditorView) { referencesSearchMode, enableNewEditor, } = userSettings + const activeOverallTheme = useActiveOverallTheme() const { onlineUserCursorHighlights } = useOnlineUsersContext() @@ -119,7 +120,7 @@ function useCodeMirrorScope(view: EditorView) { fontFamily, fontSize, lineHeight, - overallTheme, + activeOverallTheme, editorTheme, }) @@ -128,7 +129,7 @@ function useCodeMirrorScope(view: EditorView) { fontFamily, fontSize, lineHeight, - overallTheme, + activeOverallTheme, editorTheme, } @@ -137,14 +138,14 @@ function useCodeMirrorScope(view: EditorView) { fontFamily, fontSize, lineHeight, - overallTheme, + activeOverallTheme, }) ) setEditorTheme(editorTheme).then(spec => { view.dispatch(spec) }) - }, [view, fontFamily, fontSize, lineHeight, overallTheme, editorTheme]) + }, [view, fontFamily, fontSize, lineHeight, activeOverallTheme, editorTheme]) const settingsRef = useRef({ autoComplete, diff --git a/services/web/frontend/js/shared/hooks/use-active-overall-theme.tsx b/services/web/frontend/js/shared/hooks/use-active-overall-theme.tsx new file mode 100644 index 0000000000..388c78cf38 --- /dev/null +++ b/services/web/frontend/js/shared/hooks/use-active-overall-theme.tsx @@ -0,0 +1,54 @@ +import { useUserSettingsContext } from '@/shared/context/user-settings-context' +import { OverallTheme } from '@/shared/utils/styles' +import { isIEEEBranded } from '@/utils/is-ieee-branded' +import { useEffect, useMemo, useState } from 'react' + +export type ActiveOverallTheme = 'dark' | 'light' + +const mediaWatcher = window.matchMedia?.('(prefers-color-scheme: dark)') ?? { + // If matchMedia is not supported, use the default (dark) theme + matches: true, + addEventListener: () => {}, + removeEventListener: () => {}, +} + +function getTheme( + overallTheme: OverallTheme, + prefersDark: boolean +): ActiveOverallTheme { + if (isIEEEBranded()) { + return 'dark' + } + if (overallTheme === 'light-') { + return 'light' + } + if (overallTheme === 'system') { + return prefersDark ? 'dark' : 'light' + } + return 'dark' +} + +export const useActiveOverallTheme = (): ActiveOverallTheme => { + const [browserPrefersDarkMode, setBrowserPrefersDarkMode] = useState( + mediaWatcher.matches + ) + const { + userSettings: { overallTheme }, + } = useUserSettingsContext() + + const activeOverallTheme = useMemo(() => { + return getTheme(overallTheme, browserPrefersDarkMode) + }, [overallTheme, browserPrefersDarkMode]) + + useEffect(() => { + const listener = (e: MediaQueryListEvent) => { + setBrowserPrefersDarkMode(e.matches) + } + mediaWatcher.addEventListener('change', listener) + return () => { + mediaWatcher.removeEventListener('change', listener) + } + }, []) + + return activeOverallTheme +} diff --git a/services/web/frontend/js/shared/utils/styles.ts b/services/web/frontend/js/shared/utils/styles.ts index 2b30b3ed7c..c03f3052f7 100644 --- a/services/web/frontend/js/shared/utils/styles.ts +++ b/services/web/frontend/js/shared/utils/styles.ts @@ -1,4 +1,4 @@ -export type OverallTheme = '' | 'light-' +export type OverallTheme = '' | 'light-' | 'system' export const fontFamilies = { monaco: ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'monospace'], diff --git a/services/web/modules/full-project-search/frontend/js/components/full-project-search-ui.tsx b/services/web/modules/full-project-search/frontend/js/components/full-project-search-ui.tsx index db42a927a4..69acc9e38c 100644 --- a/services/web/modules/full-project-search/frontend/js/components/full-project-search-ui.tsx +++ b/services/web/modules/full-project-search/frontend/js/components/full-project-search-ui.tsx @@ -37,6 +37,7 @@ import { FullProjectSearchResults } from './full-project-search-results' import { signalWithTimeout } from '@/utils/abort-signal' import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils' import RailPanelHeader from '@/features/ide-redesign/components/rail-panel-header' +import { useActiveOverallTheme } from '@/shared/hooks/use-active-overall-theme' const FullProjectSearchUI: FC = () => { const { t } = useTranslation() @@ -57,6 +58,7 @@ const FullProjectSearchUI: FC = () => { () => userStyles(userSettings), [userSettings] ) + const activeOverallTheme = useActiveOverallTheme() const abortControllerRef = useRef(null) @@ -183,7 +185,7 @@ const FullProjectSearchUI: FC = () => {
{newEditor ? (