insomnia/packages/insomnia-app/app/plugins/misc.js
Florian Stellbrink 565f389d36
Add OS dark mode support (#2868)
* Add settings for color scheme detection and themes

Default light and dark themes can still be changed.
For now its studio-light and default for core, and studio-dark and studio-light for designer.

* Add color scheme type and supporting methods

The detection of dark scheme is based on the background color at the moment.
This seems to work pretty well, but is not an ideal solution.
I think themes should at least get to override this.

* Add support for choosing light and dark theme to settings

This adds a checkbox to the theme settings that determines whether we use the OS color scheme.
If we don't (default) everything stays the same as before.
If we do, themes are rendered in two groups. One for the light themes and one for the dark themes. They can be chosen independently. None of this overrides the default theme choice.

* Add padding to the theme settings tab

Themes are still aligned by adding negative margin.
A bit of a hack, open for suggestions.

* Update theme on OS color scheme change

* Replace usages of setTheme with applyColorScheme

This makes sure that we don't override the user's choice.

* Update packages/insomnia-app/app/plugins/misc.js

Co-authored-by: Opender Singh <opender94@gmail.com>

* Remove dark mode heuristic

* Remove unused button value

* Update theme settings design

* Update packages/insomnia-app/app/ui/components/settings/theme.js

Co-authored-by: Opender Singh <opender94@gmail.com>

* Update packages/insomnia-app/app/ui/components/settings/theme.js

Co-authored-by: Opender Singh <opender94@gmail.com>

* Replace object literal lookups

Do not use object literal lookups to make code more readable

* Remove unused parameter

* Disable default theme select when auto detection is enabled

* Fix imports after rebase

* Update packages/insomnia-app/app/ui/components/modals/settings-modal.js

Co-authored-by: Opender Singh <opender94@gmail.com>

* Update packages/insomnia-app/app/ui/components/modals/settings-modal.js

Co-authored-by: Opender Singh <opender94@gmail.com>

* Remove theme header

* Disable hover animation and border on disabled theme buttons

* Clean up double negation in css

Replace :not(:disabled) with :enabled. Not sure what I was thinking there.

* Update index.js

Co-authored-by: Opender Singh <opender94@gmail.com>
Co-authored-by: Opender Singh <opender.singh@konghq.com>
2021-03-13 16:38:54 +13:00

351 lines
8.7 KiB
JavaScript

// @flow
import Color from 'color';
import { render, THROW_ON_ERROR } from '../common/render';
import { getThemes } from './index';
import type { Theme } from './index';
import { getAppDefaultTheme } from '../common/constants';
type ThemeBlock = {
background?: {
default: string,
success?: string,
notice?: string,
warning?: string,
danger?: string,
surprise?: string,
info?: string,
},
foreground?: {
default: string,
success?: string,
notice?: string,
warning?: string,
danger?: string,
surprise?: string,
info?: string,
},
highlight?: {
default: string,
xxs?: string,
xs?: string,
sm?: string,
md?: string,
lg?: string,
xl?: string,
},
};
type ThemeInner = {
...ThemeBlock,
rawCss?: string,
styles: ?{
dialog?: ThemeBlock,
dialogFooter?: ThemeBlock,
dialogHeader?: ThemeBlock,
dropdown?: ThemeBlock,
editor?: ThemeBlock,
link?: ThemeBlock,
overlay?: ThemeBlock,
pane?: ThemeBlock,
paneHeader?: ThemeBlock,
sidebar?: ThemeBlock,
sidebarHeader?: ThemeBlock,
sidebarList?: ThemeBlock,
tooltip?: ThemeBlock,
transparentOverlay?: ThemeBlock,
},
};
export type PluginTheme = {
name: string,
displayName: string,
theme: ThemeInner,
};
export async function generateThemeCSS(theme: PluginTheme): Promise<string> {
const renderedTheme: ThemeInner = await render(
theme.theme,
theme.theme,
null,
THROW_ON_ERROR,
theme.name,
);
const n = theme.name;
let css = '';
// For the top-level variables, merge with the base theme to ensure that
// we have everything we need.
css += wrapStyles(
n,
'',
getThemeBlockCSS({
...renderedTheme,
background: { ..._baseTheme.background, ...renderedTheme.background },
foreground: { ..._baseTheme.foreground, ...renderedTheme.foreground },
highlight: { ..._baseTheme.highlight, ...renderedTheme.highlight },
}),
);
if (renderedTheme.styles) {
const styles = renderedTheme.styles;
// Dropdown Menus
css += wrapStyles(
n,
'.theme--dropdown__menu',
getThemeBlockCSS(styles.dropdown || styles.dialog),
);
// Tooltips
css += wrapStyles(n, '.theme--tooltip', getThemeBlockCSS(styles.tooltip || styles.dialog));
// Overlay
css += wrapStyles(
n,
'.theme--transparent-overlay',
getThemeBlockCSS(styles.transparentOverlay),
);
// Dialogs
css += wrapStyles(n, '.theme--dialog', getThemeBlockCSS(styles.dialog));
css += wrapStyles(n, '.theme--dialog__header', getThemeBlockCSS(styles.dialogHeader));
css += wrapStyles(n, '.theme--dialog__footer', getThemeBlockCSS(styles.dialogFooter));
// Panes
css += wrapStyles(n, '.theme--pane', getThemeBlockCSS(styles.pane));
css += wrapStyles(n, '.theme--pane__header', getThemeBlockCSS(styles.paneHeader));
css += wrapStyles(n, '.theme--app-header', getThemeBlockCSS(styles.appHeader));
// Sidebar Styles
css += wrapStyles(n, '.theme--sidebar', getThemeBlockCSS(styles.sidebar));
css += wrapStyles(n, '.theme--sidebar__list', getThemeBlockCSS(styles.sidebarList));
css += wrapStyles(n, '.theme--sidebar__header', getThemeBlockCSS(styles.sidebarHeader));
// Link
css += wrapStyles(n, '.theme--link', getThemeBlockCSS(styles.link));
// Code Editors
css += wrapStyles(n, '.theme--editor', getThemeBlockCSS(styles.editor));
// HACK: Dialog styles for CodeMirror dialogs too
css += wrapStyles(n, '.CodeMirror-info', getThemeBlockCSS(styles.dialog));
}
return css;
}
function getThemeBlockCSS(block?: ThemeBlock): string {
if (!block) {
return '';
}
const indent = '\t';
let css = '';
const addColorVar = (variable: string, value?: string) => {
if (!value) {
return;
}
try {
const parsedColor = Color(value);
const rgb = parsedColor.rgb();
addVar(variable, rgb.string());
addVar(`${variable}-rgb`, rgb.array().join(', '));
} catch (err) {
console.log('Failed to parse theme color', value);
}
};
const addVar = (variable: string, value?: string) => {
if (!value) {
return;
}
css += `${indent}--${variable}: ${value};\n`;
};
const addComment = comment => {
css += `${indent}/* ${comment} */\n`;
};
const addNewLine = () => {
css += '\n';
};
if (block.background) {
const { background } = block;
addComment('Background');
addColorVar('color-bg', background.default);
addColorVar('color-success', background.success);
addColorVar('color-notice', background.notice);
addColorVar('color-warning', background.warning);
addColorVar('color-danger', background.danger);
addColorVar('color-surprise', background.surprise);
addColorVar('color-info', background.info);
addNewLine();
}
if (block.foreground) {
const { foreground } = block;
addComment('Foreground');
addColorVar('color-font', foreground.default);
addColorVar('color-font-success', foreground.success);
addColorVar('color-font-notice', foreground.notice);
addColorVar('color-font-warning', foreground.warning);
addColorVar('color-font-danger', foreground.danger);
addColorVar('color-font-surprise', foreground.surprise);
addColorVar('color-font-info', foreground.info);
addNewLine();
}
if (block.highlight) {
const { highlight } = block;
addComment('Highlight');
addColorVar('hl', highlight.default);
addColorVar('hl-xxs', highlight.xxs);
addColorVar('hl-xs', highlight.xs);
addColorVar('hl-sm', highlight.sm);
addColorVar('hl-md', highlight.md);
addColorVar('hl-lg', highlight.lg);
addColorVar('hl-xl', highlight.xl);
addNewLine();
}
return css.replace(/\s+$/, '');
}
function wrapStyles(theme: string, selector: string, styles: string) {
if (!styles) {
return '';
}
return [
`[theme="${theme}"] ${selector}, `,
`[subtheme="${theme}"] ${selector ? selector + '--sub' : ''} {`,
styles,
'}',
'',
'',
].join('\n');
}
export function getColorScheme(settings: Settings): ColorScheme {
if (!settings.autoDetectColorScheme) {
return 'default';
}
if (window.matchMedia(`(prefers-color-scheme: light)`).matches) {
return 'light';
}
if (window.matchMedia(`(prefers-color-scheme: dark)`).matches) {
return 'dark';
}
return 'default';
}
export async function applyColorScheme(settings: Settings): Promise<void> {
const scheme = getColorScheme(settings);
if (scheme === 'light') {
await setTheme(settings.lightTheme);
} else if (scheme === 'dark') {
await setTheme(settings.darkTheme);
} else {
await setTheme(settings.theme);
}
}
export async function setTheme(themeName: string) {
if (!document) {
return;
}
const head = document.head;
const body = document.body;
if (!head || !body) {
return;
}
const themes: Array<Theme> = await getThemes();
// If theme isn't installed for some reason, set to the default
if (!themes.find(t => t.theme.name === themeName)) {
console.log(`[theme] Theme not found ${themeName}`);
themeName = getAppDefaultTheme();
}
body.setAttribute('theme', themeName);
for (const theme of themes) {
let themeCSS = (await generateThemeCSS(theme.theme)) + '\n';
const { name } = theme.theme;
const { rawCss } = theme.theme.theme;
let s = document.querySelector(`style[data-theme-name="${name}"]`);
if (!s) {
s = document.createElement('style');
s.setAttribute('data-theme-name', name);
head.appendChild(s);
}
if (typeof rawCss === 'string' && name === themeName) {
themeCSS += '\n\n' + rawCss;
}
s.innerHTML = themeCSS;
}
}
export async function setFont(settings: Object) {
if (!document) {
return;
}
const html = document.querySelector('html');
if (!html) {
return;
}
html.style.setProperty('--font-default', settings.fontInterface);
html.style.setProperty('--font-monospace', settings.fontMonospace);
html.style.setProperty('--font-ligatures', settings.fontVariantLigatures ? 'normal' : 'none');
html.style.setProperty('font-size', `${settings.fontSize}px`);
}
const _baseTheme = {
background: {
default: '#fff',
success: '#75ba24',
notice: '#d8c84d',
warning: '#ec8702',
danger: '#e15251',
surprise: '#6030BF',
info: '#20aed9',
},
foreground: {
default: '#666',
success: '#fff',
notice: '#fff',
warning: '#fff',
danger: '#fff',
surprise: '#fff',
info: '#fff',
},
highlight: {
default: 'rgba(130, 130, 130, 1)',
xxs: 'rgba(130, 130, 130, 0.05)',
xs: 'rgba(130, 130, 130, 0.1)',
sm: 'rgba(130, 130, 130, 0.25)',
md: 'rgba(130, 130, 130, 0.35)',
lg: 'rgba(130, 130, 130, 0.5)',
xl: 'rgba(130, 130, 130, 0.8)',
},
};