From 6caf8256fbac8b3a722661ce336fc7c30d6330e4 Mon Sep 17 00:00:00 2001 From: Caroline Bridge Date: Wed, 8 May 2024 09:52:39 -0400 Subject: [PATCH 01/11] Adds support for prefers-color-scheme --- packages/app-core/src/ui/App/App.tsx | 10 ++- .../__snapshots__/index.test.tsx.snap | 6 -- packages/core/ui/theme.ts | 2 +- packages/product-core/src/Session/Themes.ts | 87 ++++++++++++++++++- packages/web-core/src/BaseWebSession/index.ts | 7 +- .../__image_snapshots__/lgv_snapshot.svg | 2 +- .../__snapshots__/ExportSvg.test.tsx.snap | 2 + 7 files changed, 97 insertions(+), 19 deletions(-) diff --git a/packages/app-core/src/ui/App/App.tsx b/packages/app-core/src/ui/App/App.tsx index 532e85a1fe..81064b6c31 100644 --- a/packages/app-core/src/ui/App/App.tsx +++ b/packages/app-core/src/ui/App/App.tsx @@ -1,5 +1,5 @@ -import React, { Suspense, lazy } from 'react' -import { AppBar } from '@mui/material' +import React, { Suspense, lazy, useEffect } from 'react' +import { AppBar, useMediaQuery } from '@mui/material' import { makeStyles } from 'tss-react/mui' import { observer } from 'mobx-react' import { SessionWithFocusedViewAndDrawerWidgets } from '@jbrowse/core/util' @@ -87,6 +87,12 @@ const App = observer(function (props: Props) { const d = drawerVisible ? `[drawer] ${drawerWidth}px` : undefined const grid = drawerPosition === 'right' ? ['[main] 1fr', d] : [d, '[main] 1fr'] + const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)') + + useEffect(() => { + // @ts-expect-error + session.setPrefersDarkMode(`${prefersDarkMode}`) + }, [prefersDarkMode, session]) return (
Show feature sequence -
diff --git a/packages/core/ui/theme.ts b/packages/core/ui/theme.ts index 47b07dab48..ff26646c04 100644 --- a/packages/core/ui/theme.ts +++ b/packages/core/ui/theme.ts @@ -228,8 +228,8 @@ export const defaultThemes = { default: getDefaultTheme(), lightStock: getLightStockTheme(), lightMinimal: getMinimalTheme(), - darkMinimal: getDarkMinimalTheme(), darkStock: getDarkStockTheme(), + darkMinimal: getDarkMinimalTheme(), } as ThemeMap function overwriteArrayMerge(_: unknown, sourceArray: unknown[]) { diff --git a/packages/product-core/src/Session/Themes.ts b/packages/product-core/src/Session/Themes.ts index 370c9974a3..402cc86a0b 100644 --- a/packages/product-core/src/Session/Themes.ts +++ b/packages/product-core/src/Session/Themes.ts @@ -25,15 +25,89 @@ export function ThemeManagerSessionMixin(_pluginManager: PluginManager) { .model({}) .volatile(() => ({ sessionThemeName: localStorageGetItem('themeName') || 'default', + prefersDarkMode: localStorageGetItem('prefersDarkMode') || 'false', })) .views(s => ({ /** - * #method + * #getter */ - allThemes(): ThemeMap { + get configTheme() { + const self = s as typeof s & BaseSession + const configTheme = getConf(self.jbrowse, 'theme') + // placeholder structure to identify the default config theme + return { + config: { + palette: { + ...defaultThemes.default.palette, + ...configTheme.palette, + }, + name: 'config', + }, + } as ThemeOptions + }, + /** + * #getter + */ + get extraThemes() { const self = s as typeof s & BaseSession const extraThemes = getConf(self.jbrowse, 'extraThemes') - return { ...defaultThemes, ...extraThemes } + return extraThemes + }, + /** + * #getter + */ + get lightTheme() { + const theme = Object.entries({ + ...this.configTheme, + ...this.extraThemes, + ...defaultThemes, + } as ThemeMap).find( + ([_, theme]) => + theme.palette?.mode === 'light' || + theme.palette?.mode === undefined, + ) ?? [undefined, undefined] + + return theme ?? undefined + }, + /** + * #getter + */ + get darkTheme() { + const theme = Object.entries({ + ...this.configTheme, + ...this.extraThemes, + ...defaultThemes, + } as ThemeMap).find(([_, theme]) => theme.palette?.mode === 'dark') ?? [ + undefined, + undefined, + ] + + return theme ?? undefined + }, + /** + * #getter + */ + get systemTheme() { + const [name, theme] = + s.prefersDarkMode === 'true' && this.darkTheme[1] + ? this.darkTheme + : this.lightTheme + + const sysTheme = { + ...theme, + name: `Use system setting (${name})`, + } + return sysTheme as ThemeOptions + }, + /** + * #method + */ + allThemes(): ThemeMap { + return { + ...defaultThemes, + ...this.extraThemes, + system: { ...this.systemTheme }, + } }, /** * #getter @@ -60,11 +134,18 @@ export function ThemeManagerSessionMixin(_pluginManager: PluginManager) { setThemeName(name: string) { self.sessionThemeName = name }, + /** + * #action + */ + setPrefersDarkMode(preference: string) { + self.prefersDarkMode = preference + }, afterAttach() { addDisposer( self, autorun(() => { localStorageSetItem('themeName', self.themeName) + localStorageSetItem('prefersDarkMode', self.prefersDarkMode) }), ) }, diff --git a/packages/web-core/src/BaseWebSession/index.ts b/packages/web-core/src/BaseWebSession/index.ts index e320a3347f..1257426feb 100644 --- a/packages/web-core/src/BaseWebSession/index.ts +++ b/packages/web-core/src/BaseWebSession/index.ts @@ -9,7 +9,7 @@ import { AnyConfiguration, } from '@jbrowse/core/configuration' import { AssemblyManager, JBrowsePlugin } from '@jbrowse/core/util/types' -import { localStorageGetItem, localStorageSetItem } from '@jbrowse/core/util' +import { localStorageSetItem } from '@jbrowse/core/util' import { autorun } from 'mobx' import { addDisposer, @@ -106,10 +106,6 @@ export function BaseWebSession({ sessionPlugins: types.array(types.frozen()), }) .volatile((/* self */) => ({ - /** - * #volatile - */ - sessionThemeName: localStorageGetItem('themeName') || 'default', /** * #volatile * this is the current "task" that is being performed in the UI. @@ -410,7 +406,6 @@ export function BaseWebSession({ self, autorun(() => { localStorageSetItem('drawerPosition', self.drawerPosition) - localStorageSetItem('themeName', self.themeName) }), ) }, diff --git a/products/jbrowse-web/src/tests/__image_snapshots__/lgv_snapshot.svg b/products/jbrowse-web/src/tests/__image_snapshots__/lgv_snapshot.svg index 755bd9f072..7480e05447 100644 --- a/products/jbrowse-web/src/tests/__image_snapshots__/lgv_snapshot.svg +++ b/products/jbrowse-web/src/tests/__image_snapshots__/lgv_snapshot.svg @@ -1 +1 @@ -volvox80bpctgA020406080001111GTACAGAGTGACGCTCAAAGCvolvox-sorted.bam (ctgA, canvas) \ No newline at end of file +volvoxctgA1,8001,9002,0002,100333619537636283334354210515volvox_inv_indelsvolvox_random_invctgA1,8001,9002,0002,1003345844231067567193728415volvox_inv_indels \ No newline at end of file diff --git a/products/jbrowse-web/src/tests/__snapshots__/ExportSvg.test.tsx.snap b/products/jbrowse-web/src/tests/__snapshots__/ExportSvg.test.tsx.snap index 7a541504d2..49da95eb5f 100644 --- a/products/jbrowse-web/src/tests/__snapshots__/ExportSvg.test.tsx.snap +++ b/products/jbrowse-web/src/tests/__snapshots__/ExportSvg.test.tsx.snap @@ -7,3 +7,5 @@ exports[`export svg of dotplot 1`] = `"volvox80bpctgA020406080001111GTACAGAGTGACGCTCAAAGCvolvox-sorted.bam (ctgA, canvas)"`; exports[`export svg of synteny 1`] = `"volvoxctgA1,8001,9002,0002,100333619537636283334354210515volvox_inv_indelsvolvox_random_invctgA1,8001,9002,0002,1003345844231067567193728415volvox_inv_indels"`; + +exports[`export svg of synteny 2`] = `"volvoxctgA1,8001,9002,0002,100333619537636283334354210515volvox_inv_indelsvolvox_random_invctgA1,8001,9002,0002,1003345844231067567193728415volvox_inv_indels"`; From 2f0a7fa5f63f745dfc9a0e79767a97ba3ad8d432 Mon Sep 17 00:00:00 2001 From: Caroline Bridge Date: Wed, 8 May 2024 10:46:13 -0400 Subject: [PATCH 02/11] snaps --- .../BaseFeatureWidget/__snapshots__/index.test.tsx.snap | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/core/BaseFeatureWidget/__snapshots__/index.test.tsx.snap b/packages/core/BaseFeatureWidget/__snapshots__/index.test.tsx.snap index 4fddd084b6..834b741137 100644 --- a/packages/core/BaseFeatureWidget/__snapshots__/index.test.tsx.snap +++ b/packages/core/BaseFeatureWidget/__snapshots__/index.test.tsx.snap @@ -125,6 +125,9 @@ exports[`open up a widget 1`] = ` type="button" > Show feature sequence + From 906f6e434b6da35dc5ed28d14305df628949e720 Mon Sep 17 00:00:00 2001 From: Caroline Bridge Date: Fri, 17 May 2024 17:10:09 -0400 Subject: [PATCH 03/11] Adds concept of alternate theme; adds 'mode' as a selector for dark vs light theme ; uses makeContrasting to generate a missing theme --- packages/core/ui/theme.ts | 125 ++++++++++++++++-- packages/product-core/src/Session/Themes.ts | 107 +++++---------- .../src/components/PreferencesDialog.tsx | 14 ++ .../__image_snapshots__/lgv_snapshot.svg | 2 +- .../__snapshots__/ExportSvg.test.tsx.snap | 2 - 5 files changed, 160 insertions(+), 90 deletions(-) diff --git a/packages/core/ui/theme.ts b/packages/core/ui/theme.ts index ff26646c04..fae468d317 100644 --- a/packages/core/ui/theme.ts +++ b/packages/core/ui/theme.ts @@ -3,8 +3,10 @@ import { createTheme, ThemeOptions } from '@mui/material/styles' import type { PaletteAugmentColorOptions, PaletteColor, + SimplePaletteColorOptions, } from '@mui/material/styles/createPalette' import deepmerge from 'deepmerge' +import { makeContrasting } from '../util/color' declare module '@mui/material/styles/createPalette' { interface Palette { @@ -113,6 +115,10 @@ const frames = [ const stopCodon = '#e22' const startCodon = '#3e3' +interface JBrowseThemeOptions extends ThemeOptions { + alternate?: ThemeOptions +} + function stockTheme() { return { palette: { @@ -139,7 +145,8 @@ function stockTheme() { }, }, }, - } satisfies ThemeOptions + alternate: getDarkStockTheme(), + } satisfies JBrowseThemeOptions } function getDefaultTheme() { @@ -149,10 +156,10 @@ function getDefaultTheme() { } } -function getLightStockTheme() { +function getStockTheme() { return { ...stockTheme(), - name: 'Light (stock)', + name: 'Stock', } } @@ -208,7 +215,7 @@ function getDarkMinimalTheme() { function getMinimalTheme() { return { - name: 'Light (minimal)', + name: 'Minimal', palette: { primary: { main: grey[900] }, secondary: { main: grey[800] }, @@ -221,15 +228,14 @@ function getMinimalTheme() { frames, framesCDS, }, - } satisfies ThemeOptions & { name: string } + alternate: getDarkMinimalTheme(), + } satisfies JBrowseThemeOptions & { name: string } } export const defaultThemes = { default: getDefaultTheme(), - lightStock: getLightStockTheme(), - lightMinimal: getMinimalTheme(), - darkStock: getDarkStockTheme(), - darkMinimal: getDarkMinimalTheme(), + stock: getStockTheme(), + minimal: getMinimalTheme(), } as ThemeMap function overwriteArrayMerge(_: unknown, sourceArray: unknown[]) { @@ -455,24 +461,121 @@ export function createJBrowseBaseTheme(theme?: ThemeOptions): ThemeOptions { return deepmerge(themeP, theme || {}, { arrayMerge: overwriteArrayMerge }) } -type ThemeMap = Record +type ThemeMap = Record export function createJBrowseTheme( configTheme: ThemeOptions = {}, themes = defaultThemes, themeName = 'default', + mode = 'light', ) { + const themeMode = configTheme?.palette?.mode ?? 'light' + if (themeMode !== mode) { + configTheme = getAlternateTheme(configTheme) + } return createTheme( createJBrowseBaseTheme( themeName === 'default' ? deepmerge(themes.default, augmentTheme(configTheme), { arrayMerge: overwriteArrayMerge, }) - : augmentThemePlus(themes[themeName]) || themes.default, + : augmentThemePlus(configTheme) || themes.default, ), ) } +function getAlternateTheme(theme: JBrowseThemeOptions = {}) { + const alternate = theme?.alternate ?? undefined + const themeMode = theme?.palette?.mode ?? 'light' + const altMode = alternate?.palette?.mode + + // no alternate theme has been defined, or both themes are the same mode + if (!alternate || themeMode === altMode) { + return generateAltTheme(theme) + } + + if (themeMode !== altMode) { + return { + ...alternate, + } + } + + return theme +} + +function generateAltTheme(theme: ThemeOptions = {}) { + const altMode = theme.palette?.mode === 'dark' ? 'light' : 'dark' + const background = altMode === 'dark' ? 'black' : 'white' + const contrast = 4.5 + + if (theme?.palette) { + theme = deepmerge(theme, { + palette: { + mode: altMode, + }, + }) + } + + if (theme?.palette?.primary) { + const contrastColor = { + main: makeContrasting( + (theme.palette.primary as SimplePaletteColorOptions).main, + background, + contrast, + ), + } + theme = deepmerge(theme, { + palette: { + primary: refTheme.palette.augmentColor({ color: contrastColor }), + }, + }) + } + if (theme?.palette?.secondary) { + const contrastColor = { + main: makeContrasting( + (theme.palette.secondary as SimplePaletteColorOptions).main, + background, + contrast, + ), + } + theme = deepmerge(theme, { + palette: { + secondary: refTheme.palette.augmentColor({ color: contrastColor }), + }, + }) + } + if (theme?.palette?.tertiary) { + const contrastColor = { + main: makeContrasting( + (theme.palette.tertiary as SimplePaletteColorOptions).main, + background, + contrast, + ), + } + theme = deepmerge(theme, { + palette: { + tertiary: refTheme.palette.augmentColor({ color: contrastColor }), + }, + }) + } + if (theme?.palette?.quaternary) { + const contrastColor = { + main: makeContrasting( + (theme.palette.quaternary as SimplePaletteColorOptions).main, + background, + contrast, + ), + } + theme = deepmerge(theme, { + palette: { + quaternary: refTheme.palette.augmentColor({ color: contrastColor }), + }, + }) + } + + return theme +} + function augmentTheme(theme: ThemeOptions = {}) { if (theme?.palette?.tertiary) { theme = deepmerge(theme, { diff --git a/packages/product-core/src/Session/Themes.ts b/packages/product-core/src/Session/Themes.ts index 402cc86a0b..87c904d091 100644 --- a/packages/product-core/src/Session/Themes.ts +++ b/packages/product-core/src/Session/Themes.ts @@ -15,7 +15,12 @@ import { autorun } from 'mobx' // locals import { BaseSession } from './BaseSession' -type ThemeMap = Record +interface JBrowseThemeOptions extends ThemeOptions { + name?: string + alternate?: ThemeOptions +} + +type ThemeMap = Record /** * #stateModel ThemeManagerSessionMixin @@ -26,87 +31,18 @@ export function ThemeManagerSessionMixin(_pluginManager: PluginManager) { .volatile(() => ({ sessionThemeName: localStorageGetItem('themeName') || 'default', prefersDarkMode: localStorageGetItem('prefersDarkMode') || 'false', + themeMode: localStorageGetItem('themeMode') || 'system', })) .views(s => ({ - /** - * #getter - */ - get configTheme() { - const self = s as typeof s & BaseSession - const configTheme = getConf(self.jbrowse, 'theme') - // placeholder structure to identify the default config theme - return { - config: { - palette: { - ...defaultThemes.default.palette, - ...configTheme.palette, - }, - name: 'config', - }, - } as ThemeOptions - }, - /** - * #getter - */ - get extraThemes() { - const self = s as typeof s & BaseSession - const extraThemes = getConf(self.jbrowse, 'extraThemes') - return extraThemes - }, - /** - * #getter - */ - get lightTheme() { - const theme = Object.entries({ - ...this.configTheme, - ...this.extraThemes, - ...defaultThemes, - } as ThemeMap).find( - ([_, theme]) => - theme.palette?.mode === 'light' || - theme.palette?.mode === undefined, - ) ?? [undefined, undefined] - - return theme ?? undefined - }, - /** - * #getter - */ - get darkTheme() { - const theme = Object.entries({ - ...this.configTheme, - ...this.extraThemes, - ...defaultThemes, - } as ThemeMap).find(([_, theme]) => theme.palette?.mode === 'dark') ?? [ - undefined, - undefined, - ] - - return theme ?? undefined - }, - /** - * #getter - */ - get systemTheme() { - const [name, theme] = - s.prefersDarkMode === 'true' && this.darkTheme[1] - ? this.darkTheme - : this.lightTheme - - const sysTheme = { - ...theme, - name: `Use system setting (${name})`, - } - return sysTheme as ThemeOptions - }, /** * #method */ allThemes(): ThemeMap { + const self = s as typeof s & BaseSession + const extraThemes = getConf(self.jbrowse, 'extraThemes') return { ...defaultThemes, - ...this.extraThemes, - system: { ...this.systemTheme }, + ...extraThemes, } }, /** @@ -122,9 +58,21 @@ export function ThemeManagerSessionMixin(_pluginManager: PluginManager) { */ get theme() { const self = s as typeof s & BaseSession - const configTheme = getConf(self.jbrowse, 'theme') const all = this.allThemes() - return createJBrowseTheme(configTheme, all, this.themeName) + + const desiredMode = + s.themeMode === 'system' + ? JSON.parse(s.prefersDarkMode) + ? 'dark' + : 'light' + : s.themeMode + + const theme = + this.themeName === 'default' + ? getConf(self.jbrowse, 'theme') + : all[this.themeName] + + return createJBrowseTheme(theme, all, this.themeName, desiredMode) }, })) .actions(self => ({ @@ -140,12 +88,19 @@ export function ThemeManagerSessionMixin(_pluginManager: PluginManager) { setPrefersDarkMode(preference: string) { self.prefersDarkMode = preference }, + /** + * #action + */ + setThemeMode(preference: 'light' | 'dark' | 'system') { + self.themeMode = preference + }, afterAttach() { addDisposer( self, autorun(() => { localStorageSetItem('themeName', self.themeName) localStorageSetItem('prefersDarkMode', self.prefersDarkMode) + localStorageSetItem('themeMode', self.themeMode) }), ) }, diff --git a/products/jbrowse-web/src/components/PreferencesDialog.tsx b/products/jbrowse-web/src/components/PreferencesDialog.tsx index 5c44e22a9e..e71b838285 100644 --- a/products/jbrowse-web/src/components/PreferencesDialog.tsx +++ b/products/jbrowse-web/src/components/PreferencesDialog.tsx @@ -13,6 +13,8 @@ import { Dialog } from '@jbrowse/core/ui' const useStyles = makeStyles()(() => ({ container: { width: 800, + display: 'flex', + gap: '5px', }, })) @@ -25,6 +27,8 @@ export default function PreferencesDialog({ allThemes: () => Record themeName?: string setThemeName: (arg: string) => void + themeMode: string + setThemeMode: (arg: string) => void } }) { const { classes } = useStyles() @@ -43,6 +47,16 @@ export default function PreferencesDialog({ ))} + session.setThemeMode(event.target.value)} + > + Light + Dark + System + diff --git a/products/jbrowse-web/src/tests/__image_snapshots__/lgv_snapshot.svg b/products/jbrowse-web/src/tests/__image_snapshots__/lgv_snapshot.svg index 7480e05447..755bd9f072 100644 --- a/products/jbrowse-web/src/tests/__image_snapshots__/lgv_snapshot.svg +++ b/products/jbrowse-web/src/tests/__image_snapshots__/lgv_snapshot.svg @@ -1 +1 @@ -volvoxctgA1,8001,9002,0002,100333619537636283334354210515volvox_inv_indelsvolvox_random_invctgA1,8001,9002,0002,1003345844231067567193728415volvox_inv_indels \ No newline at end of file +volvox80bpctgA020406080001111GTACAGAGTGACGCTCAAAGCvolvox-sorted.bam (ctgA, canvas) \ No newline at end of file diff --git a/products/jbrowse-web/src/tests/__snapshots__/ExportSvg.test.tsx.snap b/products/jbrowse-web/src/tests/__snapshots__/ExportSvg.test.tsx.snap index 49da95eb5f..7a541504d2 100644 --- a/products/jbrowse-web/src/tests/__snapshots__/ExportSvg.test.tsx.snap +++ b/products/jbrowse-web/src/tests/__snapshots__/ExportSvg.test.tsx.snap @@ -7,5 +7,3 @@ exports[`export svg of dotplot 1`] = `"volvox80bpctgA020406080001111GTACAGAGTGACGCTCAAAGCvolvox-sorted.bam (ctgA, canvas)"`; exports[`export svg of synteny 1`] = `"volvoxctgA1,8001,9002,0002,100333619537636283334354210515volvox_inv_indelsvolvox_random_invctgA1,8001,9002,0002,1003345844231067567193728415volvox_inv_indels"`; - -exports[`export svg of synteny 2`] = `"volvoxctgA1,8001,9002,0002,100333619537636283334354210515volvox_inv_indelsvolvox_random_invctgA1,8001,9002,0002,1003345844231067567193728415volvox_inv_indels"`; From 33d80da650a058a155e2b91506e0d7835730f336 Mon Sep 17 00:00:00 2001 From: Caroline Bridge Date: Wed, 22 May 2024 09:36:09 -0400 Subject: [PATCH 04/11] typing issue --- packages/product-core/src/Session/Themes.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/product-core/src/Session/Themes.ts b/packages/product-core/src/Session/Themes.ts index 87c904d091..c0c4c22404 100644 --- a/packages/product-core/src/Session/Themes.ts +++ b/packages/product-core/src/Session/Themes.ts @@ -15,12 +15,7 @@ import { autorun } from 'mobx' // locals import { BaseSession } from './BaseSession' -interface JBrowseThemeOptions extends ThemeOptions { - name?: string - alternate?: ThemeOptions -} - -type ThemeMap = Record +type ThemeMap = Record /** * #stateModel ThemeManagerSessionMixin From 42b4421ba9d77a9a566e87b9712d894eb348da29 Mon Sep 17 00:00:00 2001 From: Caroline Bridge Date: Wed, 22 May 2024 10:16:36 -0400 Subject: [PATCH 05/11] docs --- website/docs/config_guides/theme.md | 68 ++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 7 deletions(-) diff --git a/website/docs/config_guides/theme.md b/website/docs/config_guides/theme.md index 3fc91db615..4562477d10 100644 --- a/website/docs/config_guides/theme.md +++ b/website/docs/config_guides/theme.md @@ -43,7 +43,7 @@ The customized theme screenshot uses the below configuration: ```json { "configuration": { - "theme" :{ + "theme": { "palette": { "primary": { "main": "#311b92" @@ -62,13 +62,18 @@ The customized theme screenshot uses the below configuration: } ``` -### Extra themes and dark mode +### Extra themes, system themes, and dark mode + +Extra themes can be defined via the config. In jbrowse-web and jbrowse-desktop, +the user can select these themes from the Tools>Preferences menu. + +From this dialog, you can also select whether you would like to use light, dark, +or system mode for the theme. Selecting "system" will adopt your desktop's +system-level theming preference. -In v2.4.0 we introduced the ability to add extra themes via the config. In -jbrowse-web and jbrowse-desktop, these show up in a "Preferences" dialog that -the user can select from. We also added better support for dark mode themes. -Adding "mode": "dark" to your theme will use MUI's dark mode -https://mui.com/material-ui/customization/dark-mode/ +An administrator can define a dark mode theme by adding the `"mode": "dark"` +option to their theme. Some more information about MUI's dark mode can be found +here: https://mui.com/material-ui/customization/dark-mode/. Example @@ -99,6 +104,55 @@ Example } ``` +An administrator can define an "alternate" theme for a given theme using the +`"alternate":` slot, which will be prioritized when the user is selecting a mode +for their theme. This is particularly useful for defining a dark mode alternate +for a given theme, and can be used with the default theme defined in the config. +Without an explicitly defined alternate, JBrowse will generate an acceptable +light or dark mode theme for the user. + +Example + +```json +{ + "configuration": { + "theme": { + "palette": { + "primary": { + "main": "#311b92" + }, + "secondary": { + "main": "#0097a7" + }, + "tertiary": { + "main": "#f57c00" + }, + "quaternary": { + "main": "#d50000" + } + } + }, + "alternate": { + "mode": "dark", + "palette": { + "primary": { + "main": "#311b92" + }, + "secondary": { + "main": "#0097a7" + }, + "tertiary": { + "main": "#f57c00" + }, + "quaternary": { + "main": "#d50000" + } + } + } + } +} +``` + ### Logo It is also possible to supply a custom logo to be displayed in the top right From 51b05b466260811797c15bd8d26bd3c91b81d4d3 Mon Sep 17 00:00:00 2001 From: Caroline Bridge Date: Fri, 24 May 2024 11:03:28 -0400 Subject: [PATCH 06/11] fixes default theme not applying --- packages/core/ui/theme.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/core/ui/theme.ts b/packages/core/ui/theme.ts index fae468d317..5f46c843fc 100644 --- a/packages/core/ui/theme.ts +++ b/packages/core/ui/theme.ts @@ -469,6 +469,9 @@ export function createJBrowseTheme( themeName = 'default', mode = 'light', ) { + if (Object.keys(configTheme).length === 0) { + configTheme = themes[themeName] + } const themeMode = configTheme?.palette?.mode ?? 'light' if (themeMode !== mode) { configTheme = getAlternateTheme(configTheme) From 834b19b660e008f9f3da4957c04350a702ea4d33 Mon Sep 17 00:00:00 2001 From: Caroline Bridge Date: Fri, 24 May 2024 13:44:09 -0400 Subject: [PATCH 07/11] add light and dark spec, backwards compat --- packages/core/ui/theme.ts | 135 +++++++++--------- packages/product-core/src/Session/Themes.ts | 9 +- .../src/components/PreferencesDialog.tsx | 14 ++ .../src/components/PreferencesDialog.tsx | 14 ++ 4 files changed, 100 insertions(+), 72 deletions(-) diff --git a/packages/core/ui/theme.ts b/packages/core/ui/theme.ts index 5f46c843fc..cdde4e1021 100644 --- a/packages/core/ui/theme.ts +++ b/packages/core/ui/theme.ts @@ -115,37 +115,40 @@ const frames = [ const stopCodon = '#e22' const startCodon = '#3e3' -interface JBrowseThemeOptions extends ThemeOptions { - alternate?: ThemeOptions +export interface JBrowseThemeOptions extends ThemeOptions { + light?: ThemeOptions + dark?: ThemeOptions } function stockTheme() { return { - palette: { - mode: undefined, - primary: { main: midnight }, - secondary: { main: grape }, - tertiary: forest, - quaternary: mandarin, - highlight: mandarin, - stopCodon, - startCodon, - bases, - frames, - framesCDS, - }, - components: { - MuiLink: { - styleOverrides: { - // the default link color uses theme.palette.primary.main which is - // very bad with dark mode+midnight primary - root: ({ theme }) => ({ - color: theme.palette.tertiary.main, - }), + light: { + palette: { + mode: undefined, + primary: { main: midnight }, + secondary: { main: grape }, + tertiary: forest, + quaternary: mandarin, + highlight: mandarin, + stopCodon, + startCodon, + bases, + frames, + framesCDS, + }, + components: { + MuiLink: { + styleOverrides: { + // the default link color uses theme.palette.primary.main which is + // very bad with dark mode+midnight primary + root: ({ theme }) => ({ + color: theme.palette.tertiary.main, + }), + }, }, }, }, - alternate: getDarkStockTheme(), + dark: getDarkStockTheme(), } satisfies JBrowseThemeOptions } @@ -216,19 +219,21 @@ function getDarkMinimalTheme() { function getMinimalTheme() { return { name: 'Minimal', - palette: { - primary: { main: grey[900] }, - secondary: { main: grey[800] }, - tertiary: refTheme.palette.augmentColor({ color: { main: grey[900] } }), - quaternary: mandarin, - highlight: mandarin, - stopCodon, - startCodon, - bases, - frames, - framesCDS, + light: { + palette: { + primary: { main: grey[900] }, + secondary: { main: grey[800] }, + tertiary: refTheme.palette.augmentColor({ color: { main: grey[900] } }), + quaternary: mandarin, + highlight: mandarin, + stopCodon, + startCodon, + bases, + frames, + framesCDS, + }, }, - alternate: getDarkMinimalTheme(), + dark: getDarkMinimalTheme(), } satisfies JBrowseThemeOptions & { name: string } } @@ -464,57 +469,49 @@ export function createJBrowseBaseTheme(theme?: ThemeOptions): ThemeOptions { type ThemeMap = Record export function createJBrowseTheme( - configTheme: ThemeOptions = {}, + configTheme: JBrowseThemeOptions = defaultThemes['default'], themes = defaultThemes, themeName = 'default', mode = 'light', ) { - if (Object.keys(configTheme).length === 0) { - configTheme = themes[themeName] - } - const themeMode = configTheme?.palette?.mode ?? 'light' - if (themeMode !== mode) { - configTheme = getAlternateTheme(configTheme) + let theme = + mode === 'light' + ? configTheme?.light + : mode === 'dark' + ? configTheme?.dark + : undefined + if (!theme) { + theme = generateAltTheme( + configTheme.light ?? configTheme.dark ?? (configTheme as ThemeOptions), + mode, + ) } return createTheme( createJBrowseBaseTheme( themeName === 'default' - ? deepmerge(themes.default, augmentTheme(configTheme), { - arrayMerge: overwriteArrayMerge, - }) - : augmentThemePlus(configTheme) || themes.default, + ? deepmerge( + themes.default.light ?? themes.default, + augmentTheme(theme), + { + arrayMerge: overwriteArrayMerge, + }, + ) + : (augmentThemePlus(theme) || themes.default.light) ?? themes.default, ), ) } -function getAlternateTheme(theme: JBrowseThemeOptions = {}) { - const alternate = theme?.alternate ?? undefined - const themeMode = theme?.palette?.mode ?? 'light' - const altMode = alternate?.palette?.mode - - // no alternate theme has been defined, or both themes are the same mode - if (!alternate || themeMode === altMode) { - return generateAltTheme(theme) - } - - if (themeMode !== altMode) { - return { - ...alternate, - } +function generateAltTheme(theme: ThemeOptions = {}, mode: string) { + if (theme?.palette?.mode === 'dark' && mode === 'dark') { + return theme } - - return theme -} - -function generateAltTheme(theme: ThemeOptions = {}) { - const altMode = theme.palette?.mode === 'dark' ? 'light' : 'dark' - const background = altMode === 'dark' ? 'black' : 'white' + const background = mode === 'dark' ? 'black' : 'white' const contrast = 4.5 if (theme?.palette) { theme = deepmerge(theme, { palette: { - mode: altMode, + mode: mode, }, }) } diff --git a/packages/product-core/src/Session/Themes.ts b/packages/product-core/src/Session/Themes.ts index c0c4c22404..c05a544a08 100644 --- a/packages/product-core/src/Session/Themes.ts +++ b/packages/product-core/src/Session/Themes.ts @@ -7,15 +7,18 @@ import { import PluginManager from '@jbrowse/core/PluginManager' import { getConf } from '@jbrowse/core/configuration' -import { createJBrowseTheme, defaultThemes } from '@jbrowse/core/ui' +import { + JBrowseThemeOptions, + createJBrowseTheme, + defaultThemes, +} from '@jbrowse/core/ui' import { localStorageGetItem, localStorageSetItem } from '@jbrowse/core/util' -import { ThemeOptions } from '@mui/material' import { autorun } from 'mobx' // locals import { BaseSession } from './BaseSession' -type ThemeMap = Record +type ThemeMap = Record /** * #stateModel ThemeManagerSessionMixin diff --git a/products/jbrowse-desktop/src/components/PreferencesDialog.tsx b/products/jbrowse-desktop/src/components/PreferencesDialog.tsx index 5c44e22a9e..e71b838285 100644 --- a/products/jbrowse-desktop/src/components/PreferencesDialog.tsx +++ b/products/jbrowse-desktop/src/components/PreferencesDialog.tsx @@ -13,6 +13,8 @@ import { Dialog } from '@jbrowse/core/ui' const useStyles = makeStyles()(() => ({ container: { width: 800, + display: 'flex', + gap: '5px', }, })) @@ -25,6 +27,8 @@ export default function PreferencesDialog({ allThemes: () => Record themeName?: string setThemeName: (arg: string) => void + themeMode: string + setThemeMode: (arg: string) => void } }) { const { classes } = useStyles() @@ -43,6 +47,16 @@ export default function PreferencesDialog({ ))} + session.setThemeMode(event.target.value)} + > + Light + Dark + System + diff --git a/products/jbrowse-react-app/src/components/PreferencesDialog.tsx b/products/jbrowse-react-app/src/components/PreferencesDialog.tsx index 5c44e22a9e..e71b838285 100644 --- a/products/jbrowse-react-app/src/components/PreferencesDialog.tsx +++ b/products/jbrowse-react-app/src/components/PreferencesDialog.tsx @@ -13,6 +13,8 @@ import { Dialog } from '@jbrowse/core/ui' const useStyles = makeStyles()(() => ({ container: { width: 800, + display: 'flex', + gap: '5px', }, })) @@ -25,6 +27,8 @@ export default function PreferencesDialog({ allThemes: () => Record themeName?: string setThemeName: (arg: string) => void + themeMode: string + setThemeMode: (arg: string) => void } }) { const { classes } = useStyles() @@ -43,6 +47,16 @@ export default function PreferencesDialog({ ))} + session.setThemeMode(event.target.value)} + > + Light + Dark + System + From 0f9365f1e7a4b9ad7e8ed22999540d9585067d43 Mon Sep 17 00:00:00 2001 From: Caroline Bridge Date: Fri, 24 May 2024 13:47:24 -0400 Subject: [PATCH 08/11] lint --- packages/core/ui/theme.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/ui/theme.ts b/packages/core/ui/theme.ts index cdde4e1021..c8e3f2197c 100644 --- a/packages/core/ui/theme.ts +++ b/packages/core/ui/theme.ts @@ -469,7 +469,7 @@ export function createJBrowseBaseTheme(theme?: ThemeOptions): ThemeOptions { type ThemeMap = Record export function createJBrowseTheme( - configTheme: JBrowseThemeOptions = defaultThemes['default'], + configTheme: JBrowseThemeOptions = defaultThemes.default, themes = defaultThemes, themeName = 'default', mode = 'light', From a03a3011d84dc4748cbda2ee8a1361b310a58ce3 Mon Sep 17 00:00:00 2001 From: Caroline Bridge Date: Fri, 24 May 2024 14:03:50 -0400 Subject: [PATCH 09/11] update docs --- test_data/volvox/config.json | 32 +++++---- test_data/volvox/theme.json | 38 +++++----- test_data/volvox/theme2.json | 46 +++++++------ website/docs/config_guides/theme.md | 103 ++++++++++++++-------------- 4 files changed, 115 insertions(+), 104 deletions(-) diff --git a/test_data/volvox/config.json b/test_data/volvox/config.json index e4cf6fb8ae..885dd93aaf 100644 --- a/test_data/volvox/config.json +++ b/test_data/volvox/config.json @@ -197,21 +197,23 @@ }, "extraThemes": { "darkForest": { - "name": "Dark (forest, from config file)", - "palette": { - "primary": { - "main": "#464" - }, - "secondary": { - "main": "#376" - }, - "tertiary": { - "main": "#253" - }, - "quaternary": { - "main": "#6a4" - }, - "mode": "dark" + "name": "Forest", + "dark": { + "palette": { + "primary": { + "main": "#464" + }, + "secondary": { + "main": "#376" + }, + "tertiary": { + "main": "#253" + }, + "quaternary": { + "main": "#6a4" + }, + "mode": "dark" + } } } } diff --git a/test_data/volvox/theme.json b/test_data/volvox/theme.json index 89f117fca8..3a27a0857c 100644 --- a/test_data/volvox/theme.json +++ b/test_data/volvox/theme.json @@ -25,24 +25,26 @@ ], "configuration": { "theme": { - "palette": { - "primary": { - "main": "#311b92" - }, - "secondary": { - "main": "#0097a7" - }, - "tertiary": { - "main": "#f57c00" - }, - "quaternary": { - "main": "#d50000" - }, - "bases": { - "A": { "main": "#98FB98" }, - "C": { "main": "#87CEEB" }, - "G": { "main": "#DAA520" }, - "T": { "main": "#DC143C" } + "light": { + "palette": { + "primary": { + "main": "#311b92" + }, + "secondary": { + "main": "#0097a7" + }, + "tertiary": { + "main": "#f57c00" + }, + "quaternary": { + "main": "#d50000" + }, + "bases": { + "A": { "main": "#98FB98" }, + "C": { "main": "#87CEEB" }, + "G": { "main": "#DAA520" }, + "T": { "main": "#DC143C" } + } } } } diff --git a/test_data/volvox/theme2.json b/test_data/volvox/theme2.json index b60601afad..ebdd58c17d 100644 --- a/test_data/volvox/theme2.json +++ b/test_data/volvox/theme2.json @@ -25,32 +25,36 @@ ], "configuration": { "theme": { - "palette": { - "tertiary": { - "main": "#9da9b6" - }, - "secondary": { - "main": "#29405F" + "light": { + "palette": { + "tertiary": { + "main": "#9da9b6" + }, + "secondary": { + "main": "#29405F" + } } } }, "extraThemes": { "darkWild": { - "name": "Dark (wild)", - "palette": { - "primary": { - "main": "#444" - }, - "secondary": { - "main": "#335" - }, - "tertiary": { - "main": "#250" - }, - "quaternary": { - "main": "#535" - }, - "mode": "dark" + "name": "Wild", + "dark": { + "palette": { + "primary": { + "main": "#444" + }, + "secondary": { + "main": "#335" + }, + "tertiary": { + "main": "#250" + }, + "quaternary": { + "main": "#535" + }, + "mode": "dark" + } } } } diff --git a/website/docs/config_guides/theme.md b/website/docs/config_guides/theme.md index 4562477d10..ca2ceaaee6 100644 --- a/website/docs/config_guides/theme.md +++ b/website/docs/config_guides/theme.md @@ -60,29 +60,39 @@ The customized theme screenshot uses the below configuration: } } } +} ``` -### Extra themes, system themes, and dark mode - -Extra themes can be defined via the config. In jbrowse-web and jbrowse-desktop, -the user can select these themes from the Tools>Preferences menu. - -From this dialog, you can also select whether you would like to use light, dark, -or system mode for the theme. Selecting "system" will adopt your desktop's -system-level theming preference. +### Dark themes, extra themes, and system settings -An administrator can define a dark mode theme by adding the `"mode": "dark"` -option to their theme. Some more information about MUI's dark mode can be found -here: https://mui.com/material-ui/customization/dark-mode/. +JBrowse themes can be defined as either a `"light":` or `"dark":` theme, should +the administrator wish to define both. Without an explicitly defined light or +dark theme, JBrowse will generate an acceptable light or dark mode theme for the +user should their preference setting require it. Example ```json { "configuration": { - "extraThemes": { - "myTheme": { - "name": "My theme", + "theme": { + "light": { + "palette": { + "primary": { + "main": "#311b92" + }, + "secondary": { + "main": "#0097a7" + }, + "tertiary": { + "main": "#f57c00" + }, + "quaternary": { + "main": "#d50000" + } + } + }, + "dark": { "mode": "dark", "palette": { "primary": { @@ -104,48 +114,41 @@ Example } ``` -An administrator can define an "alternate" theme for a given theme using the -`"alternate":` slot, which will be prioritized when the user is selecting a mode -for their theme. This is particularly useful for defining a dark mode alternate -for a given theme, and can be used with the default theme defined in the config. -Without an explicitly defined alternate, JBrowse will generate an acceptable -light or dark mode theme for the user. +Extra themes can be defined via the config. In jbrowse-web and jbrowse-desktop, +the user can select these themes from the Tools>Preferences menu. + +From this dialog, you can also select whether you would like to use light, dark, +or system mode for the theme. Selecting "system" will adopt your desktop's +system-level theming preference. + +An administrator can use MUI's built-in dark mode themeing by adding the +`"mode": "dark"` option to their theme. Some more information about MUI's dark +mode can be found here: https://mui.com/material-ui/customization/dark-mode/. Example ```json { "configuration": { - "theme": { - "palette": { - "primary": { - "main": "#311b92" - }, - "secondary": { - "main": "#0097a7" - }, - "tertiary": { - "main": "#f57c00" - }, - "quaternary": { - "main": "#d50000" - } - } - }, - "alternate": { - "mode": "dark", - "palette": { - "primary": { - "main": "#311b92" - }, - "secondary": { - "main": "#0097a7" - }, - "tertiary": { - "main": "#f57c00" - }, - "quaternary": { - "main": "#d50000" + "extraThemes": { + "myTheme": { + "name": "My theme", + "dark": { + "mode": "dark", + "palette": { + "primary": { + "main": "#311b92" + }, + "secondary": { + "main": "#0097a7" + }, + "tertiary": { + "main": "#f57c00" + }, + "quaternary": { + "main": "#d50000" + } + } } } } From e8aaaff3b7a978d31a95cf965ecc5d8680abb4d5 Mon Sep 17 00:00:00 2001 From: Caroline Bridge Date: Fri, 24 May 2024 14:21:50 -0400 Subject: [PATCH 10/11] update tests --- packages/core/ui/theme.test.ts | 16 ++++++---- .../__snapshots__/jbrowseModel.test.ts.snap | 30 ++++++++++--------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/packages/core/ui/theme.test.ts b/packages/core/ui/theme.test.ts index 601d7770d4..e384ee406f 100644 --- a/packages/core/ui/theme.test.ts +++ b/packages/core/ui/theme.test.ts @@ -10,9 +10,11 @@ test('can create a default theme', () => { }) test('allows overriding primary and secondary colors', () => { const theme = createJBrowseTheme({ - palette: { - primary: { main: '#888888' }, - secondary: { main: 'rgb(137,137,137)' }, + light: { + palette: { + primary: { main: '#888888' }, + secondary: { main: 'rgb(137,137,137)' }, + }, }, }) expect(theme.palette.primary.main).toEqual('#888888') @@ -20,9 +22,11 @@ test('allows overriding primary and secondary colors', () => { }) test('allows overriding tertiary and quaternary colors', () => { const theme = createJBrowseTheme({ - palette: { - tertiary: { 500: '#888' }, - quaternary: { main: 'hsl(0,0,54)' }, + light: { + palette: { + tertiary: { 500: '#888' }, + quaternary: { main: 'hsl(0,0,54)' }, + }, }, }) const { tertiary, quaternary } = theme.palette diff --git a/products/jbrowse-web/src/__snapshots__/jbrowseModel.test.ts.snap b/products/jbrowse-web/src/__snapshots__/jbrowseModel.test.ts.snap index 6d2de5c687..fed5782546 100644 --- a/products/jbrowse-web/src/__snapshots__/jbrowseModel.test.ts.snap +++ b/products/jbrowse-web/src/__snapshots__/jbrowseModel.test.ts.snap @@ -436,22 +436,24 @@ exports[`JBrowse model creates with non-empty snapshot 1`] = ` }, "extraThemes": { "darkForest": { - "name": "Dark (forest, from config file)", - "palette": { - "mode": "dark", - "primary": { - "main": "#464", - }, - "quaternary": { - "main": "#6a4", - }, - "secondary": { - "main": "#376", - }, - "tertiary": { - "main": "#253", + "dark": { + "palette": { + "mode": "dark", + "primary": { + "main": "#464", + }, + "quaternary": { + "main": "#6a4", + }, + "secondary": { + "main": "#376", + }, + "tertiary": { + "main": "#253", + }, }, }, + "name": "Forest", }, }, "hierarchical": { From be0a99566d406c59cfe8086e333a1292619b393b Mon Sep 17 00:00:00 2001 From: Caroline Bridge Date: Fri, 24 May 2024 14:46:35 -0400 Subject: [PATCH 11/11] fix empty theme not displaying dark --- packages/core/ui/theme.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/core/ui/theme.ts b/packages/core/ui/theme.ts index c8e3f2197c..0ec86e7ed1 100644 --- a/packages/core/ui/theme.ts +++ b/packages/core/ui/theme.ts @@ -474,6 +474,9 @@ export function createJBrowseTheme( themeName = 'default', mode = 'light', ) { + if (Object.keys(configTheme).length === 0) { + configTheme = defaultThemes.default + } let theme = mode === 'light' ? configTheme?.light