Skip to content

Commit 3088906

Browse files
linghaoSutodaywasawesome
andauthoredOct 3, 2024··
feat(ui): support auto theme (#20080)
* feat(theme): support auto theme Signed-off-by: linghaoSu <linghao.su@daocloud.io> * fix(ui): set default theme as light Signed-off-by: linghaoSu <linghao.su@daocloud.io> * fix(ui): only register listener when theme is auto Signed-off-by: linghaoSu <linghao.su@daocloud.io> --------- Signed-off-by: linghaoSu <linghao.su@daocloud.io> Co-authored-by: Dan Garfield <dan@codefresh.io>
1 parent 8d268e7 commit 3088906

File tree

7 files changed

+124
-25
lines changed

7 files changed

+124
-25
lines changed
 

‎ui/src/app/app.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import applications from './applications';
88
import help from './help';
99
import login from './login';
1010
import settings from './settings';
11-
import {Layout} from './shared/components/layout/layout';
11+
import {Layout, ThemeWrapper} from './shared/components/layout/layout';
1212
import {Page} from './shared/components/page/page';
1313
import {VersionPanel} from './shared/components/version-info/version-info-panel';
1414
import {AuthSettingsCtx, Provider} from './shared/context';
@@ -194,7 +194,7 @@ export class App extends React.Component<
194194
<PageContext.Provider value={{title: 'Argo CD'}}>
195195
<Provider value={{history, popup: this.popupManager, notifications: this.notificationsManager, navigation: this.navigationManager, baseHref: base}}>
196196
<DataLoader load={() => services.viewPreferences.getPreferences()}>
197-
{pref => <div className={pref.theme ? 'theme-' + pref.theme : 'theme-light'}>{this.state.popupProps && <Popup {...this.state.popupProps} />}</div>}
197+
{pref => <ThemeWrapper theme={pref.theme}>{this.state.popupProps && <Popup {...this.state.popupProps} />}</ThemeWrapper>}
198198
</DataLoader>
199199
<AuthSettingsCtx.Provider value={this.state.authSettings}>
200200
<Router history={history}>

‎ui/src/app/settings/components/appearance-list/appearance-list.scss

+8-6
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313
}
1414
border-radius: 4px;
1515
box-shadow: 1px 2px 3px rgba(#000, 0.1);
16-
}
16+
& .row {
17+
justify-content: space-between;
18+
align-items: center;
1719

18-
&__button {
19-
position: absolute;
20-
top: 25%;
21-
right: 30px;
20+
.select {
21+
min-width: 160px;
22+
}
23+
}
2224
}
23-
}
25+
}

‎ui/src/app/settings/components/appearance-list/appearance-list.tsx

+11-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as React from 'react';
22
import {DataLoader, Page} from '../../../shared/components';
33
import {services} from '../../../shared/services';
4+
import {Select, SelectOption} from 'argo-ui';
45

56
require('./appearance-list.scss');
67

@@ -16,16 +17,16 @@ export const AppearanceList = () => {
1617
<div className='appearance-list'>
1718
<div className='argo-container'>
1819
<div className='appearance-list__panel'>
19-
<div className='columns'>Dark Theme</div>
20-
<div className='columns'>
21-
<button
22-
className='argo-button argo-button--base appearance-list__button'
23-
onClick={() => {
24-
const targetTheme = pref.theme === 'light' ? 'dark' : 'light';
25-
services.viewPreferences.updatePreferences({theme: targetTheme});
26-
}}>
27-
{pref.theme === 'light' ? 'Enable' : 'Disable'}
28-
</button>
20+
<div className='row'>
21+
<span>Dark Theme</span>
22+
<Select
23+
value={pref.theme}
24+
onChange={(value: SelectOption) => services.viewPreferences.updatePreferences({theme: value.value})}
25+
options={[
26+
{value: 'auto', title: 'Auto'},
27+
{value: 'light', title: 'Light'},
28+
{value: 'dark', title: 'Dark'}
29+
]}></Select>
2930
</div>
3031
</div>
3132
</div>

‎ui/src/app/shared/components/layout/layout.tsx

+13-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as React from 'react';
22
import {Sidebar} from '../../../sidebar/sidebar';
33
import {ViewPreferences} from '../../services';
4+
import {useTheme} from '../../utils';
45

56
require('./layout.scss');
67

@@ -13,15 +14,23 @@ export interface LayoutProps {
1314

1415
const getBGColor = (theme: string): string => (theme === 'light' ? '#dee6eb' : '#100f0f');
1516

17+
export const ThemeWrapper = (props: {children: React.ReactNode; theme: string}) => {
18+
const [systemTheme] = useTheme({
19+
theme: props.theme
20+
});
21+
return <div className={'theme-' + systemTheme}>{props.children}</div>;
22+
};
23+
1624
export const Layout = (props: LayoutProps) => {
25+
const [theme] = useTheme({theme: props.pref.theme});
1726
React.useEffect(() => {
18-
if (props.pref.theme) {
19-
document.body.style.background = getBGColor(props.pref.theme);
27+
if (theme) {
28+
document.body.style.background = getBGColor(theme);
2029
}
21-
}, [props.pref.theme]);
30+
}, [theme]);
2231

2332
return (
24-
<div className={props.pref.theme ? 'theme-' + props.pref.theme : 'theme-light'}>
33+
<div className={`theme-${theme}`}>
2534
<div className={'cd-layout'}>
2635
<Sidebar onVersionClick={props.onVersionClick} navItems={props.navItems} pref={props.pref} />
2736
<div className={`cd-layout__content ${props.pref.hideSidebar ? 'cd-layout__content--sb-collapsed' : 'cd-layout__content--sb-expanded'} custom-styles`}>

‎ui/src/app/shared/components/monaco-editor.tsx

+17-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as React from 'react';
22

33
import * as monacoEditor from 'monaco-editor';
44
import {services} from '../services';
5+
import {getTheme, useSystemTheme} from '../utils';
56

67
export interface EditorInput {
78
text: string;
@@ -28,10 +29,25 @@ const MonacoEditorLazy = React.lazy(() =>
2829
import('monaco-editor').then(monaco => {
2930
const Component = (props: MonacoProps) => {
3031
const [height, setHeight] = React.useState(0);
32+
const [theme, setTheme] = React.useState('dark');
33+
34+
React.useEffect(() => {
35+
const destroySystemThemeListener = useSystemTheme(systemTheme => {
36+
if (theme === 'auto') {
37+
monaco.editor.setTheme(systemTheme === 'dark' ? 'vs-dark' : 'vs');
38+
}
39+
});
40+
41+
return () => {
42+
destroySystemThemeListener();
43+
};
44+
}, [theme]);
3145

3246
React.useEffect(() => {
3347
const subscription = services.viewPreferences.getPreferences().subscribe(preferences => {
34-
monaco.editor.setTheme(preferences.theme === 'dark' ? 'vs-dark' : 'vs');
48+
setTheme(preferences.theme);
49+
50+
monaco.editor.setTheme(getTheme(preferences.theme) === 'dark' ? 'vs-dark' : 'vs');
3551
});
3652

3753
return () => {

‎ui/src/app/shared/components/version-info/version-info-panel.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {DataLoader, SlidingPanel, Tooltip} from 'argo-ui';
22
import * as React from 'react';
33
import {VersionMessage} from '../../models';
44
import {services} from '../../services';
5+
import {ThemeWrapper} from '../layout/layout';
56

67
interface VersionPanelProps {
78
isShown: boolean;
@@ -26,14 +27,14 @@ export class VersionPanel extends React.Component<VersionPanelProps, {copyState:
2627
<DataLoader load={() => this.props.version}>
2728
{version => {
2829
return (
29-
<div className={'theme-' + pref.theme}>
30+
<ThemeWrapper theme={pref.theme}>
3031
<SlidingPanel header={this.header} isShown={this.props.isShown} onClose={() => this.props.onClose()} hasCloseButton={true} isNarrow={true}>
3132
<div className='argo-table-list'>{this.buildVersionTable(version)}</div>
3233
<div>
3334
<Tooltip content='Copy all version info as JSON'>{this.getCopyButton(version)}</Tooltip>
3435
</div>
3536
</SlidingPanel>
36-
</div>
37+
</ThemeWrapper>
3738
);
3839
}}
3940
</DataLoader>

‎ui/src/app/shared/utils.ts

+70
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import React from 'react';
2+
13
export function hashCode(str: string) {
24
let hash = 0;
35
for (let i = 0; i < str.length; i++) {
@@ -34,3 +36,71 @@ export function isValidURL(url: string): boolean {
3436
}
3537
}
3638
}
39+
40+
export const colorSchemes = {
41+
light: '(prefers-color-scheme: light)',
42+
dark: '(prefers-color-scheme: dark)'
43+
};
44+
45+
/**
46+
* quick method to check system theme
47+
* @param theme auto, light, dark
48+
* @returns dark or light
49+
*/
50+
export function getTheme(theme: string) {
51+
if (theme !== 'auto') {
52+
return theme;
53+
}
54+
55+
const dark = window.matchMedia(colorSchemes.dark);
56+
57+
return dark.matches ? 'dark' : 'light';
58+
}
59+
60+
/**
61+
* create a listener for system theme
62+
* @param cb callback for theme change
63+
* @returns destroy listener
64+
*/
65+
export const useSystemTheme = (cb: (theme: string) => void) => {
66+
const dark = window.matchMedia(colorSchemes.dark);
67+
const light = window.matchMedia(colorSchemes.light);
68+
69+
const listener = () => {
70+
cb(dark.matches ? 'dark' : 'light');
71+
};
72+
73+
dark.addEventListener('change', listener);
74+
light.addEventListener('change', listener);
75+
76+
return () => {
77+
dark.removeEventListener('change', listener);
78+
light.removeEventListener('change', listener);
79+
};
80+
};
81+
82+
export const useTheme = (props: {theme: string}) => {
83+
const [theme, setTheme] = React.useState(getTheme(props.theme));
84+
85+
React.useEffect(() => {
86+
let destroyListener: (() => void) | undefined;
87+
88+
// change theme by system, only register listener when theme is auto
89+
if (props.theme === 'auto') {
90+
destroyListener = useSystemTheme(systemTheme => {
91+
setTheme(systemTheme);
92+
});
93+
}
94+
95+
// change theme manually
96+
if (props.theme !== theme) {
97+
setTheme(getTheme(props.theme));
98+
}
99+
100+
return () => {
101+
destroyListener?.();
102+
};
103+
}, [props.theme]);
104+
105+
return [theme];
106+
};

0 commit comments

Comments
 (0)
Please sign in to comment.