Просмотр исходного кода

Merge pull request #26735 from overleaf/mj-system-theme

[web] Add new system theme to the editor

GitOrigin-RevId: b65083c5e96abc493556e901c861689cb7e3bbf7
Mathias Jakobsen 1 год назад
Родитель
Сommit
39b4581e1d

+ 8 - 0
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()

+ 5 - 5
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']) => {

+ 1 - 1
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': {

+ 7 - 9
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,

+ 6 - 5
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,

+ 54 - 0
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<ActiveOverallTheme>(() => {
+    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
+}

+ 1 - 1
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'],

+ 3 - 1
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<AbortController | null>(null)
 
@@ -183,7 +185,7 @@ const FullProjectSearchUI: FC = () => {
     <div
       className="full-project-search"
       style={variableStyle}
-      data-bs-theme={userSettings.overallTheme === 'light-' ? 'light' : 'dark'}
+      data-bs-theme={activeOverallTheme === 'light' ? 'light' : 'dark'}
     >
       {newEditor ? (
         <RailPanelHeader title={t('search')} />