feat: Have theme follow system pref
This commit is contained in:
@ -23,6 +23,7 @@ type Props = {
|
||||
onClose?: () => void,
|
||||
children?: Children,
|
||||
className?: string,
|
||||
hover?: boolean,
|
||||
style?: Object,
|
||||
position?: 'left' | 'right' | 'center',
|
||||
};
|
||||
@ -72,7 +73,7 @@ class DropdownMenu extends React.Component<Props> {
|
||||
this.initPosition();
|
||||
|
||||
// attempt to keep only one flyout menu open at once
|
||||
if (previousClosePortal) {
|
||||
if (previousClosePortal && !this.props.hover) {
|
||||
previousClosePortal();
|
||||
}
|
||||
previousClosePortal = closePortal;
|
||||
@ -134,7 +135,7 @@ class DropdownMenu extends React.Component<Props> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, label, children } = this.props;
|
||||
const { className, hover, label, children } = this.props;
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
@ -146,7 +147,14 @@ class DropdownMenu extends React.Component<Props> {
|
||||
>
|
||||
{({ closePortal, openPortal, isOpen, portal }) => (
|
||||
<React.Fragment>
|
||||
<Label onClick={this.handleOpen(openPortal, closePortal)}>
|
||||
<Label
|
||||
onMouseOver={
|
||||
hover ? this.handleOpen(openPortal, closePortal) : undefined
|
||||
}
|
||||
onClick={
|
||||
hover ? undefined : this.handleOpen(openPortal, closePortal)
|
||||
}
|
||||
>
|
||||
{label || (
|
||||
<NudeButton
|
||||
id={`${this.id}button`}
|
||||
@ -216,6 +224,7 @@ const Position = styled.div`
|
||||
z-index: 1000;
|
||||
transform: ${props =>
|
||||
props.position === 'center' ? 'translateX(-50%)' : 'initial'};
|
||||
pointer-events: none;
|
||||
`;
|
||||
|
||||
const Menu = styled.div`
|
||||
@ -228,6 +237,7 @@ const Menu = styled.div`
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
box-shadow: ${props => props.theme.menuShadow};
|
||||
pointer-events: all;
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
|
@ -1,14 +1,22 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { CheckmarkIcon } from 'outline-icons';
|
||||
import styled from 'styled-components';
|
||||
|
||||
type Props = {
|
||||
onClick?: (SyntheticEvent<>) => void | Promise<void>,
|
||||
children?: React.Node,
|
||||
selected?: boolean,
|
||||
disabled?: boolean,
|
||||
};
|
||||
|
||||
const DropdownMenuItem = ({ onClick, children, disabled, ...rest }: Props) => {
|
||||
const DropdownMenuItem = ({
|
||||
onClick,
|
||||
children,
|
||||
selected,
|
||||
disabled,
|
||||
...rest
|
||||
}: Props) => {
|
||||
return (
|
||||
<MenuItem
|
||||
onClick={disabled ? undefined : onClick}
|
||||
@ -17,6 +25,9 @@ const DropdownMenuItem = ({ onClick, children, disabled, ...rest }: Props) => {
|
||||
tabIndex="-1"
|
||||
{...rest}
|
||||
>
|
||||
{selected !== undefined && (
|
||||
<CheckmarkIcon color={selected === false ? 'transparent' : undefined} />
|
||||
)}
|
||||
{children}
|
||||
</MenuItem>
|
||||
);
|
||||
@ -26,6 +37,7 @@ const MenuItem = styled.a`
|
||||
display: flex;
|
||||
margin: 0;
|
||||
padding: 6px 12px;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
|
||||
color: ${props =>
|
||||
|
@ -13,7 +13,7 @@ type Props = {
|
||||
|
||||
function Theme({ children, ui }: Props) {
|
||||
return (
|
||||
<ThemeProvider theme={ui.theme === 'dark' ? dark : light}>
|
||||
<ThemeProvider theme={ui.resolvedTheme === 'dark' ? dark : light}>
|
||||
<React.Fragment>
|
||||
<GlobalStyles />
|
||||
{children}
|
||||
|
@ -3,7 +3,7 @@ import * as React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { observable } from 'mobx';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
import { SunIcon, MoonIcon } from 'outline-icons';
|
||||
import { SunIcon, MoonIcon, EyeIcon, CheckmarkIcon } from 'outline-icons';
|
||||
import styled from 'styled-components';
|
||||
import UiStore from 'stores/UiStore';
|
||||
import AuthStore from 'stores/AuthStore';
|
||||
@ -82,19 +82,42 @@ class AccountMenu extends React.Component<Props> {
|
||||
Report a bug
|
||||
</DropdownMenuItem>
|
||||
<hr />
|
||||
<DropdownMenuItem onClick={ui.toggleDarkMode}>
|
||||
<ChangeTheme justify="space-between">
|
||||
{isLightTheme ? (
|
||||
<React.Fragment>
|
||||
Dark theme <MoonIcon />
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
Light theme <SunIcon />
|
||||
</React.Fragment>
|
||||
)}
|
||||
</ChangeTheme>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenu
|
||||
hover
|
||||
position="right"
|
||||
style={{
|
||||
left: 170,
|
||||
position: 'relative',
|
||||
top: -34,
|
||||
}}
|
||||
label={
|
||||
<DropdownMenuItem>
|
||||
<ChangeTheme justify="space-between">
|
||||
Appearance
|
||||
{ui.resolvedTheme === 'light' ? <SunIcon /> : <MoonIcon />}
|
||||
</ChangeTheme>
|
||||
</DropdownMenuItem>
|
||||
}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onClick={() => ui.setTheme('system')}
|
||||
selected={ui.theme === 'system'}
|
||||
>
|
||||
System
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => ui.setTheme('light')}
|
||||
selected={ui.theme === 'light'}
|
||||
>
|
||||
Light
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => ui.setTheme('dark')}
|
||||
selected={ui.theme === 'dark'}
|
||||
>
|
||||
Dark
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenu>
|
||||
<hr />
|
||||
<DropdownMenuItem onClick={this.handleLogout}>
|
||||
Log out
|
||||
|
@ -9,7 +9,8 @@ import type { Toast } from '../types';
|
||||
const UI_STORE = 'UI_STORE';
|
||||
|
||||
class UiStore {
|
||||
@observable theme: 'light' | 'dark';
|
||||
@observable theme: 'light' | 'dark' | 'system';
|
||||
@observable systemTheme: 'light' | 'dark';
|
||||
@observable activeModalName: ?string;
|
||||
@observable activeModalProps: ?Object;
|
||||
@observable activeDocumentId: ?string;
|
||||
@ -29,9 +30,19 @@ class UiStore {
|
||||
// no-op Safari private mode
|
||||
}
|
||||
|
||||
let colorSchemeQueryList = window.matchMedia(
|
||||
'(prefers-color-scheme: dark)'
|
||||
);
|
||||
const setSystemTheme = event => {
|
||||
this.systemTheme = event.matches ? 'dark' : 'light';
|
||||
};
|
||||
|
||||
setSystemTheme(colorSchemeQueryList);
|
||||
colorSchemeQueryList.addListener(setSystemTheme);
|
||||
|
||||
// persisted keys
|
||||
this.tocVisible = data.tocVisible;
|
||||
this.theme = data.theme || 'light';
|
||||
this.theme = data.theme || 'system';
|
||||
|
||||
autorun(() => {
|
||||
try {
|
||||
@ -43,8 +54,8 @@ class UiStore {
|
||||
}
|
||||
|
||||
@action
|
||||
toggleDarkMode = () => {
|
||||
this.theme = this.theme === 'dark' ? 'light' : 'dark';
|
||||
setTheme = (theme: 'light' | 'dark' | 'system') => {
|
||||
this.theme = theme;
|
||||
|
||||
if (window.localStorage) {
|
||||
window.localStorage.setItem('theme', this.theme);
|
||||
@ -153,6 +164,15 @@ class UiStore {
|
||||
this.toasts.delete(id);
|
||||
};
|
||||
|
||||
@computed
|
||||
get resolvedTheme(): 'dark' | 'light' {
|
||||
if (this.theme === 'system') {
|
||||
return this.systemTheme;
|
||||
}
|
||||
|
||||
return this.theme;
|
||||
}
|
||||
|
||||
@computed
|
||||
get orderedToasts(): Toast[] {
|
||||
return orderBy(Array.from(this.toasts.values()), 'createdAt', 'desc');
|
||||
|
Reference in New Issue
Block a user