Skip to content

Commit

Permalink
Follow system dark theme preferences (#150)
Browse files Browse the repository at this point in the history
* ui: add follow system theme setting prop

* ui: map use system theme settings to state

* ui: hide theme toggle if system theme used

* ui: listen for system theme change

* ui: listen for system theme change

* ui: set proper theme depending on system theme settings

* ui: fix cool start of userPrefersColorScheme hook

* ui: use theme from context

* ui: sync dark mode state when system theme is used

* ui: fix min width for resizable panel
  • Loading branch information
x1unix committed Apr 10, 2022
1 parent f7e9c50 commit 639bef3
Show file tree
Hide file tree
Showing 16 changed files with 283 additions and 85 deletions.
94 changes: 54 additions & 40 deletions web/src/components/core/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from 'react';
import { getTheme } from '@fluentui/react';
import { CommandBar, ICommandBarItemProps } from '@fluentui/react/lib/CommandBar';

import SettingsModal, { SettingsChanges } from '~/components/settings/SettingsModal';
import ThemeableComponent from '@components/utils/ThemeableComponent';
import AboutModal from '~/components/modals/AboutModal';
import config from '~/services/config';
import { getSnippetsMenuItems, SnippetMenuItem } from '~/utils/headerutils';
Expand All @@ -13,9 +13,12 @@ import {
Dispatcher,
dispatchToggleTheme,
formatFileDispatcher,
newBuildParamsChangeDispatcher, newCodeImportDispatcher,
newBuildParamsChangeDispatcher,
newCodeImportDispatcher,
newImportFileDispatcher,
newMonacoParamsChangeDispatcher, newSnippetLoadDispatcher,
newMonacoParamsChangeDispatcher,
newSnippetLoadDispatcher,
newSettingsChangeDispatcher,
runFileDispatcher,
saveFileDispatcher,
shareSnippetDispatcher
Expand All @@ -39,15 +42,17 @@ interface Props {
darkMode: boolean
loading: boolean
snippetName?: string
hideThemeToggle?: boolean,
dispatch: (d: Dispatcher) => void
}

@Connect(({ settings, status, ui }) => ({
darkMode: settings.darkMode,
loading: status?.loading,
hideThemeToggle: settings.useSystemTheme,
snippetName: ui?.shareCreated && ui?.snippetId
}))
export class Header extends React.Component<any, HeaderState> {
export class Header extends ThemeableComponent<any, HeaderState> {
private fileInput?: HTMLInputElement;
private snippetMenuItems = getSnippetsMenuItems(i => this.onSnippetMenuItemClick(i));

Expand Down Expand Up @@ -80,10 +85,9 @@ export class Header extends React.Component<any, HeaderState> {
}

onSnippetMenuItemClick(item: SnippetMenuItem) {
// if (item.snippet) {
// this.setState({ showShareMessage: true });
// }
const dispatcher = item.snippet ? newSnippetLoadDispatcher(item.snippet) : newCodeImportDispatcher(item.label, item.text as string);
const dispatcher = item.snippet ?
newSnippetLoadDispatcher(item.snippet) :
newCodeImportDispatcher(item.label, item.text as string);
this.props.dispatch(dispatcher);
}

Expand Down Expand Up @@ -173,6 +177,7 @@ export class Header extends React.Component<any, HeaderState> {
text: 'Toggle Dark Mode',
ariaLabel: 'Toggle Dark Mode',
iconOnly: true,
hidden: this.props.hideThemeToggle,
iconProps: { iconName: this.props.darkMode ? 'Brightness' : 'ClearNight' },
onClick: () => {
this.props.dispatch(dispatchToggleTheme)
Expand Down Expand Up @@ -209,14 +214,6 @@ export class Header extends React.Component<any, HeaderState> {
]
}

get styles() {
// Apply the same colors as rest of Fabric components
const theme = getTheme();
return {
backgroundColor: theme.palette.white
}
}

private onSettingsClose(changes: SettingsChanges) {
if (changes.monaco) {
// Update monaco state if some of it's settings were changed
Expand All @@ -229,36 +226,53 @@ export class Header extends React.Component<any, HeaderState> {
this.props.dispatch(newBuildParamsChangeDispatcher(runtime, autoFormat));
}

if (changes.settings) {
this.props.dispatch(newSettingsChangeDispatcher(changes.settings));
}

this.setState({ showSettings: false });
}

render() {
const { showShareMessage } = this.state;
const { snippetName } = this.props;

return <header className='header' style={this.styles}>
<img
src='/go-logo-blue.svg'
className='header__logo'
alt='Golang Logo'
/>
<CommandBar
className='header__commandBar'
items={this.menuItems}
farItems={this.asideItems}
overflowItems={this.overflowItems}
ariaLabel='CodeEditor menu'
/>
<SharePopup
visible={!!(showShareMessage && snippetName)}
target={`.${BTN_SHARE_CLASSNAME}`}
snippetId={snippetName}
onDismiss={() => this.setState({ showShareMessage: false })}
/>
<SettingsModal onClose={(args) => this.onSettingsClose(args)} isOpen={this.state.showSettings} />
<AboutModal onClose={() => this.setState({ showAbout: false })} isOpen={this.state.showAbout} />
<ChangeLogModal onClose={() => this.setState({ showChangelog: false })} isOpen={this.state.showChangelog} />
</header>;
return (
<header
className='header'
style={{backgroundColor: this.theme.palette.white}}
>
<img
src='/go-logo-blue.svg'
className='header__logo'
alt='Golang Logo'
/>
<CommandBar
className='header__commandBar'
items={this.menuItems}
farItems={this.asideItems.filter(({hidden}) => !hidden)}
overflowItems={this.overflowItems}
ariaLabel='CodeEditor menu'
/>
<SharePopup
visible={!!(showShareMessage && snippetName)}
target={`.${BTN_SHARE_CLASSNAME}`}
snippetId={snippetName}
onDismiss={() => this.setState({ showShareMessage: false })}
/>
<SettingsModal
onClose={(args) => this.onSettingsClose(args)}
isOpen={this.state.showSettings}
/>
<AboutModal
onClose={() => this.setState({ showAbout: false })}
isOpen={this.state.showAbout}
/>
<ChangeLogModal
onClose={() => this.setState({ showChangelog: false })}
isOpen={this.state.showChangelog}
/>
</header>
);
}
}

10 changes: 8 additions & 2 deletions web/src/components/modals/AboutModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import React from 'react';
import { getTheme, IconButton, FontWeights, FontSizes, mergeStyleSets } from '@fluentui/react';
import {
IconButton,
FontWeights,
FontSizes,
mergeStyleSets,
useTheme
} from '@fluentui/react';
import { Modal } from '@fluentui/react/lib/Modal';
import { Link } from '@fluentui/react/lib/Link';

Expand Down Expand Up @@ -29,7 +35,7 @@ const modalStyles = mergeStyleSets({
});

export default function AboutModal(props: AboutModalProps) {
const theme = getTheme();
const theme = useTheme();
const contentStyles = getContentStyles(theme);
const iconButtonStyles = getIconButtonStyles(theme);

Expand Down
4 changes: 2 additions & 2 deletions web/src/components/modals/ChangeLogModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { getTheme, IconButton } from '@fluentui/react';
import { useTheme, IconButton } from '@fluentui/react';
import { Modal } from '@fluentui/react/lib/Modal';
import { Link } from '@fluentui/react/lib/Link';

Expand All @@ -18,7 +18,7 @@ interface ChangeLogModalProps {
}

export default function ChangeLogModal(props: ChangeLogModalProps) {
const theme = getTheme();
const theme = useTheme();
const contentStyles = getContentStyles(theme);
const iconButtonStyles = getIconButtonStyles(theme);

Expand Down
21 changes: 12 additions & 9 deletions web/src/components/preview/Preview.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import { MessageBar, MessageBarType, getTheme } from '@fluentui/react';
import { MessageBar, MessageBarType } from '@fluentui/react';

import ThemeableComponent from '@components/utils/ThemeableComponent';
import { getDefaultFontFamily } from '~/services/fonts';
import { Connect } from '~/store';
import { RuntimeType } from '~/services/config';
Expand All @@ -16,9 +17,9 @@ export interface PreviewProps {
}

@Connect(s => ({ darkMode: s.settings.darkMode, runtime: s.settings.runtime, ...s.status }))
export default class Preview extends React.Component<PreviewProps> {
export default class Preview extends ThemeableComponent<PreviewProps> {
get styles() {
const { palette } = getTheme();
const { palette } = this.theme;
return {
backgroundColor: palette.neutralLight,
color: palette.neutralDark,
Expand All @@ -35,12 +36,14 @@ export default class Preview extends React.Component<PreviewProps> {
const isWasm = this.props.runtime === RuntimeType.WebAssembly;
let content;
if (this.props.lastError) {
content = <MessageBar messageBarType={MessageBarType.error} isMultiline={true}>
<b className='app-preview__label'>Error</b>
<pre className='app-preview__errors'>
{this.props.lastError}
</pre>
</MessageBar>
content = (
<MessageBar messageBarType={MessageBarType.error} isMultiline={true}>
<b className='app-preview__label'>Error</b>
<pre className='app-preview__errors'>
{this.props.lastError}
</pre>
</MessageBar>
)
} else if (this.props.events) {
content = this.props.events.map(({Message, Delay, Kind}, k) => (
<EvalEventView
Expand Down
10 changes: 6 additions & 4 deletions web/src/components/preview/ResizablePreview.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, {useCallback} from 'react';
import {getTheme} from '@fluentui/react';
import {Resizable} from 're-resizable';
import React, { useCallback } from 'react';
import { useTheme } from '@fluentui/react';
import { Resizable } from 're-resizable';
import clsx from 'clsx';
import {
VscChevronDown,
Expand All @@ -15,6 +15,7 @@ import {LayoutType, DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH} from '~/styles/la
import './ResizablePreview.css';

const MIN_HEIGHT = 36;
const MIN_WIDTH = 120;
const handleClasses = {
top: 'ResizablePreview__handle--top',
left: 'ResizablePreview__handle--left',
Expand All @@ -38,7 +39,7 @@ const ResizablePreview: React.FC<Props> = ({
collapsed,
onViewChange
}) => {
const {palette: { accent }, semanticColors: { buttonBorder }} = getTheme();
const {palette: { accent }, semanticColors: { buttonBorder }} = useTheme();
const onResize = useCallback((e, direction, ref, size) => {
switch (layout) {
case LayoutType.Vertical:
Expand Down Expand Up @@ -83,6 +84,7 @@ const ResizablePreview: React.FC<Props> = ({
enable={enabledCorners}
onResizeStop={onResize}
minHeight={MIN_HEIGHT}
minWidth={MIN_WIDTH}
style={{
'--pg-handle-active-color': accent,
'--pg-handle-default-color': buttonBorder,
Expand Down
35 changes: 30 additions & 5 deletions web/src/components/settings/SettingsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React from 'react';
import {
Checkbox,
Dropdown,
getTheme,
IconButton,
IDropdownOption,
Modal
Expand All @@ -11,6 +10,7 @@ import {Pivot, PivotItem} from '@fluentui/react/lib/Pivot';
import {MessageBar, MessageBarType} from '@fluentui/react/lib/MessageBar';
import {Link} from '@fluentui/react/lib/Link';

import ThemeableComponent from '@components/utils/ThemeableComponent';
import {getContentStyles, getIconButtonStyles} from '~/styles/modal';
import SettingsProperty from './SettingsProperty';
import {MonacoSettings, RuntimeType} from '~/services/config';
Expand Down Expand Up @@ -63,6 +63,7 @@ const FONT_OPTS: IDropdownOption[] = [
export interface SettingsChanges {
monaco?: MonacoParamsChanges
args?: BuildParamsArgs,
settings?: Partial<SettingsState>
}

export interface SettingsProps {
Expand All @@ -83,7 +84,7 @@ interface SettingsModalState {
settings: state.settings,
monaco: state.monaco,
}))
export default class SettingsModal extends React.Component<SettingsProps, SettingsModalState> {
export default class SettingsModal extends ThemeableComponent<SettingsProps, SettingsModalState> {
private titleID = 'Settings';
private subtitleID = 'SettingsSubText';
private changes: SettingsChanges = {};
Expand All @@ -110,10 +111,21 @@ export default class SettingsModal extends React.Component<SettingsProps, Settin
this.changes.monaco[key] = val;
}

private touchSettingsProperty(changes: Partial<SettingsState>) {
if (!this.changes.settings) {
this.changes.settings = changes;
return;
}

this.changes.settings = {
...this.changes.settings,
...changes,
};
}

render() {
const theme = getTheme();
const contentStyles = getContentStyles(theme);
const iconButtonStyles = getIconButtonStyles(theme);
const contentStyles = getContentStyles(this.theme);
const iconButtonStyles = getIconButtonStyles(this.theme);
const { showGoTipMessage, showWarning } = this.state;
return (
<Modal
Expand Down Expand Up @@ -147,6 +159,19 @@ export default class SettingsModal extends React.Component<SettingsProps, Settin
}}
/>}
/>
<SettingsProperty
key='autoDetectTheme'
title='Use System Theme'
control={<Checkbox
label='Follow system dark mode preference instead of manual toggle.'
defaultChecked={this.props.settings?.useSystemTheme}
onChange={(_, val) => {
this.touchSettingsProperty({
useSystemTheme: val
});
}}
/>}
/>
<SettingsProperty
key='fontLigatures'
title='Font Ligatures'
Expand Down

0 comments on commit 639bef3

Please sign in to comment.