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

[web] Add new system theme to the editor

GitOrigin-RevId: b65083c5e96abc493556e901c861689cb7e3bbf7
This commit is contained in:
Mathias Jakobsen
2025-07-11 10:07:25 +01:00
committed by Copybot
parent 72aca352fc
commit 39b4581e1d
8 changed files with 85 additions and 22 deletions

View File

@@ -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()

View File

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

View File

@@ -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': {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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
}

View File

@@ -1,4 +1,4 @@
export type OverallTheme = '' | 'light-'
export type OverallTheme = '' | 'light-' | 'system'
export const fontFamilies = {
monaco: ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'monospace'],

View File

@@ -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')} />