From 1ab1ba8a1ede2a7f67977a3dc4993d7ff5d47297 Mon Sep 17 00:00:00 2001 From: linghaoSu Date: Tue, 24 Sep 2024 22:51:49 +0800 Subject: [PATCH 1/3] feat(theme): support auto theme Signed-off-by: linghaoSu --- ui/src/app/app.tsx | 4 +- .../appearance-list/appearance-list.scss | 14 ++-- .../appearance-list/appearance-list.tsx | 21 +++--- .../app/shared/components/layout/layout.tsx | 15 ++-- .../app/shared/components/monaco-editor.tsx | 18 ++++- .../version-info/version-info-panel.tsx | 5 +- .../services/view-preferences-service.ts | 2 +- ui/src/app/shared/utils.ts | 72 +++++++++++++++++++ 8 files changed, 125 insertions(+), 26 deletions(-) diff --git a/ui/src/app/app.tsx b/ui/src/app/app.tsx index 7a9bfb21635bb..7b73bc50ac70c 100644 --- a/ui/src/app/app.tsx +++ b/ui/src/app/app.tsx @@ -8,7 +8,7 @@ import applications from './applications'; import help from './help'; import login from './login'; import settings from './settings'; -import {Layout} from './shared/components/layout/layout'; +import {Layout, ThemeWrapper} from './shared/components/layout/layout'; import {Page} from './shared/components/page/page'; import {VersionPanel} from './shared/components/version-info/version-info-panel'; import {AuthSettingsCtx, Provider} from './shared/context'; @@ -194,7 +194,7 @@ export class App extends React.Component< services.viewPreferences.getPreferences()}> - {pref =>
{this.state.popupProps && }
} + {pref => {this.state.popupProps && }}
diff --git a/ui/src/app/settings/components/appearance-list/appearance-list.scss b/ui/src/app/settings/components/appearance-list/appearance-list.scss index dc70dcd77696c..44c8be678f60f 100644 --- a/ui/src/app/settings/components/appearance-list/appearance-list.scss +++ b/ui/src/app/settings/components/appearance-list/appearance-list.scss @@ -13,11 +13,13 @@ } border-radius: 4px; box-shadow: 1px 2px 3px rgba(#000, 0.1); - } + & .row { + justify-content: space-between; + align-items: center; - &__button { - position: absolute; - top: 25%; - right: 30px; + .select { + min-width: 160px; + } + } } -} \ No newline at end of file +} diff --git a/ui/src/app/settings/components/appearance-list/appearance-list.tsx b/ui/src/app/settings/components/appearance-list/appearance-list.tsx index 5e6fb06502f84..493f8bcf69ce7 100644 --- a/ui/src/app/settings/components/appearance-list/appearance-list.tsx +++ b/ui/src/app/settings/components/appearance-list/appearance-list.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import {DataLoader, Page} from '../../../shared/components'; import {services} from '../../../shared/services'; +import {Select, SelectOption} from 'argo-ui'; require('./appearance-list.scss'); @@ -16,16 +17,16 @@ export const AppearanceList = () => {
-
Dark Theme
-
- +
+ Dark Theme +
diff --git a/ui/src/app/shared/components/layout/layout.tsx b/ui/src/app/shared/components/layout/layout.tsx index e08297cb4e9e6..fc7c6b07d5e5b 100644 --- a/ui/src/app/shared/components/layout/layout.tsx +++ b/ui/src/app/shared/components/layout/layout.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import {Sidebar} from '../../../sidebar/sidebar'; import {ViewPreferences} from '../../services'; +import {useTheme} from '../../utils'; require('./layout.scss'); @@ -13,15 +14,21 @@ export interface LayoutProps { const getBGColor = (theme: string): string => (theme === 'light' ? '#dee6eb' : '#100f0f'); +export const ThemeWrapper = (props: {children: React.ReactNode; theme: string}) => { + const [theme] = useTheme({theme: 'auto'}); + return
{props.children}
; +}; + export const Layout = (props: LayoutProps) => { + const [theme] = useTheme({theme: props.pref.theme}); React.useEffect(() => { - if (props.pref.theme) { - document.body.style.background = getBGColor(props.pref.theme); + if (theme) { + document.body.style.background = getBGColor(theme); } - }, [props.pref.theme]); + }, [theme]); return ( -
+
diff --git a/ui/src/app/shared/components/monaco-editor.tsx b/ui/src/app/shared/components/monaco-editor.tsx index a30381638f0b5..e0a34a06e9440 100644 --- a/ui/src/app/shared/components/monaco-editor.tsx +++ b/ui/src/app/shared/components/monaco-editor.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import * as monacoEditor from 'monaco-editor'; import {services} from '../services'; +import {getTheme, useSystemTheme} from '../utils'; export interface EditorInput { text: string; @@ -28,10 +29,25 @@ const MonacoEditorLazy = React.lazy(() => import('monaco-editor').then(monaco => { const Component = (props: MonacoProps) => { const [height, setHeight] = React.useState(0); + const [theme, setTheme] = React.useState('dark'); + + React.useEffect(() => { + const destroySystemThemeListener = useSystemTheme(systemTheme => { + if (theme === 'auto') { + monaco.editor.setTheme(systemTheme === 'dark' ? 'vs-dark' : 'vs'); + } + }); + + return () => { + destroySystemThemeListener(); + }; + }, [theme]); React.useEffect(() => { const subscription = services.viewPreferences.getPreferences().subscribe(preferences => { - monaco.editor.setTheme(preferences.theme === 'dark' ? 'vs-dark' : 'vs'); + setTheme(preferences.theme); + + monaco.editor.setTheme(getTheme(preferences.theme) === 'dark' ? 'vs-dark' : 'vs'); }); return () => { diff --git a/ui/src/app/shared/components/version-info/version-info-panel.tsx b/ui/src/app/shared/components/version-info/version-info-panel.tsx index 8622b762c8a5a..15edf463aae73 100644 --- a/ui/src/app/shared/components/version-info/version-info-panel.tsx +++ b/ui/src/app/shared/components/version-info/version-info-panel.tsx @@ -2,6 +2,7 @@ import {DataLoader, SlidingPanel, Tooltip} from 'argo-ui'; import * as React from 'react'; import {VersionMessage} from '../../models'; import {services} from '../../services'; +import {ThemeWrapper} from '../layout/layout'; interface VersionPanelProps { isShown: boolean; @@ -26,14 +27,14 @@ export class VersionPanel extends React.Component this.props.version}> {version => { return ( -
+ this.props.onClose()} hasCloseButton={true} isNarrow={true}>
{this.buildVersionTable(version)}
{this.getCopyButton(version)}
-
+ ); }} diff --git a/ui/src/app/shared/services/view-preferences-service.ts b/ui/src/app/shared/services/view-preferences-service.ts index b6cdbbdc08a46..da1cc5ed0640f 100644 --- a/ui/src/app/shared/services/view-preferences-service.ts +++ b/ui/src/app/shared/services/view-preferences-service.ts @@ -148,7 +148,7 @@ const DEFAULT_PREFERENCES: ViewPreferences = { hideBannerContent: '', hideSidebar: false, position: '', - theme: 'light' + theme: 'auto' }; export class ViewPreferencesService { diff --git a/ui/src/app/shared/utils.ts b/ui/src/app/shared/utils.ts index c57715a8f933d..10e55722b8a3c 100644 --- a/ui/src/app/shared/utils.ts +++ b/ui/src/app/shared/utils.ts @@ -1,3 +1,5 @@ +import React from 'react'; + export function hashCode(str: string) { let hash = 0; for (let i = 0; i < str.length; i++) { @@ -34,3 +36,73 @@ export function isValidURL(url: string): boolean { } } } + +export const colorSchemes = { + light: '(prefers-color-scheme: light)', + dark: '(prefers-color-scheme: dark)' +}; + +/** + * quick method to check system theme + * @param theme auto, light, dark + * @returns dark or light + */ +export function getTheme(theme: string) { + if (theme !== 'auto') { + return theme; + } + + const dark = window.matchMedia(colorSchemes.dark); + + return dark.matches ? 'dark' : 'light'; +} + +/** + * create a listener for system theme + * @param cb callback for theme change + * @returns destroy listener + */ +export const useSystemTheme = (cb: (theme: string) => void) => { + const dark = window.matchMedia(colorSchemes.dark); + const light = window.matchMedia(colorSchemes.light); + + const listener = () => { + cb(dark.matches ? 'dark' : 'light'); + }; + + dark.addEventListener('change', listener); + light.addEventListener('change', listener); + + return () => { + dark.removeEventListener('change', listener); + light.removeEventListener('change', listener); + }; +}; + +export const useTheme = (props: {theme: string}) => { + const [theme, setTheme] = React.useState(getTheme(props.theme)); + + React.useEffect(() => { + // change theme by system + const destroyListener = useSystemTheme(systemTheme => { + setTheme(currentTheme => { + if (props.theme === 'auto') { + return systemTheme; + } + + return currentTheme; + }); + }); + + // change theme manually + if (props.theme !== theme) { + setTheme(getTheme(props.theme)); + } + + return () => { + destroyListener(); + }; + }, [props.theme]); + + return [theme]; +}; From f3100dd5e68eaa70207a5aff25196e8406f764f7 Mon Sep 17 00:00:00 2001 From: linghaoSu Date: Thu, 26 Sep 2024 17:29:24 +0800 Subject: [PATCH 2/3] fix(ui): set default theme as light Signed-off-by: linghaoSu --- ui/src/app/shared/services/view-preferences-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/app/shared/services/view-preferences-service.ts b/ui/src/app/shared/services/view-preferences-service.ts index da1cc5ed0640f..b6cdbbdc08a46 100644 --- a/ui/src/app/shared/services/view-preferences-service.ts +++ b/ui/src/app/shared/services/view-preferences-service.ts @@ -148,7 +148,7 @@ const DEFAULT_PREFERENCES: ViewPreferences = { hideBannerContent: '', hideSidebar: false, position: '', - theme: 'auto' + theme: 'light' }; export class ViewPreferencesService { From 16d7775d2c4ec794b1dbdd2406015dffad83fe38 Mon Sep 17 00:00:00 2001 From: linghaoSu Date: Thu, 26 Sep 2024 17:45:13 +0800 Subject: [PATCH 3/3] fix(ui): only register listener when theme is auto Signed-off-by: linghaoSu --- ui/src/app/shared/components/layout/layout.tsx | 6 ++++-- ui/src/app/shared/utils.ts | 18 ++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ui/src/app/shared/components/layout/layout.tsx b/ui/src/app/shared/components/layout/layout.tsx index fc7c6b07d5e5b..215120f9d8380 100644 --- a/ui/src/app/shared/components/layout/layout.tsx +++ b/ui/src/app/shared/components/layout/layout.tsx @@ -15,8 +15,10 @@ export interface LayoutProps { const getBGColor = (theme: string): string => (theme === 'light' ? '#dee6eb' : '#100f0f'); export const ThemeWrapper = (props: {children: React.ReactNode; theme: string}) => { - const [theme] = useTheme({theme: 'auto'}); - return
{props.children}
; + const [systemTheme] = useTheme({ + theme: props.theme + }); + return
{props.children}
; }; export const Layout = (props: LayoutProps) => { diff --git a/ui/src/app/shared/utils.ts b/ui/src/app/shared/utils.ts index 10e55722b8a3c..129b8e769a951 100644 --- a/ui/src/app/shared/utils.ts +++ b/ui/src/app/shared/utils.ts @@ -83,16 +83,14 @@ export const useTheme = (props: {theme: string}) => { const [theme, setTheme] = React.useState(getTheme(props.theme)); React.useEffect(() => { - // change theme by system - const destroyListener = useSystemTheme(systemTheme => { - setTheme(currentTheme => { - if (props.theme === 'auto') { - return systemTheme; - } - - return currentTheme; + let destroyListener: (() => void) | undefined; + + // change theme by system, only register listener when theme is auto + if (props.theme === 'auto') { + destroyListener = useSystemTheme(systemTheme => { + setTheme(systemTheme); }); - }); + } // change theme manually if (props.theme !== theme) { @@ -100,7 +98,7 @@ export const useTheme = (props: {theme: string}) => { } return () => { - destroyListener(); + destroyListener?.(); }; }, [props.theme]);