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