mirror of
https://github.com/Kong/insomnia.git
synced 2026-10-24 15:49:08 -04:00
Improvements to theme selection (#3506)
This commit is contained in:
1 parent
769d6c16d4
commit
ca0f6eb9d0
9 files changed
+434
-405
No files matched your search
@@ -68,17 +68,14 @@ interface BaseSettings {
|
||||
}
|
||||
|
||||
export type Settings = BaseModel & BaseSettings;
|
||||
|
||||
export const name = 'Settings';
|
||||
|
||||
export const type = 'Settings';
|
||||
|
||||
export const prefix = 'set';
|
||||
|
||||
export const canDuplicate = false;
|
||||
|
||||
export const canSync = false;
|
||||
|
||||
export type ThemeSettings = Pick<Settings, 'autoDetectColorScheme' | 'lightTheme' | 'darkTheme' | 'theme'>;
|
||||
|
||||
export const isSettings = (model: Pick<BaseModel, 'type'>): model is Settings => (
|
||||
model.type === type
|
||||
);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import Color from 'color';
|
||||
import { render, THROW_ON_ERROR } from '../common/render';
|
||||
import { getThemes } from './index';
|
||||
import { ColorScheme, getThemes } from './index';
|
||||
import type { Theme } from './index';
|
||||
import { getAppDefaultTheme } from '../common/constants';
|
||||
import { Settings } from '../models/settings';
|
||||
import { ThemeSettings } from '../models/settings';
|
||||
import { unreachableCase } from 'ts-assert-unreachable';
|
||||
|
||||
interface ThemeBlock {
|
||||
background?: {
|
||||
@@ -222,8 +223,8 @@ function wrapStyles(theme: string, selector: string, styles: string) {
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function getColorScheme(settings: Settings) {
|
||||
if (!settings.autoDetectColorScheme) {
|
||||
export function getColorScheme({ autoDetectColorScheme }: ThemeSettings): ColorScheme {
|
||||
if (!autoDetectColorScheme) {
|
||||
return 'default';
|
||||
}
|
||||
|
||||
@@ -238,15 +239,24 @@ export function getColorScheme(settings: Settings) {
|
||||
return 'default';
|
||||
}
|
||||
|
||||
export async function applyColorScheme(settings: Settings) {
|
||||
export async function applyColorScheme(settings: ThemeSettings) {
|
||||
const scheme = getColorScheme(settings);
|
||||
|
||||
if (scheme === 'light') {
|
||||
await setTheme(settings.lightTheme);
|
||||
} else if (scheme === 'dark') {
|
||||
await setTheme(settings.darkTheme);
|
||||
} else {
|
||||
await setTheme(settings.theme);
|
||||
switch (scheme) {
|
||||
case 'light':
|
||||
await setTheme(settings.lightTheme);
|
||||
break;
|
||||
|
||||
case 'dark':
|
||||
await setTheme(settings.darkTheme);
|
||||
break;
|
||||
|
||||
case 'default':
|
||||
await setTheme(settings.theme);
|
||||
break;
|
||||
|
||||
default:
|
||||
unreachableCase(scheme);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { ButtonHTMLAttributes, PureComponent, ReactNode } from 'react';
|
||||
import React, { ButtonHTMLAttributes, CSSProperties, PureComponent, ReactNode } from 'react';
|
||||
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
||||
import { AUTOBIND_CFG } from '../../../common/constants';
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface ButtonProps<T> {
|
||||
id?: string,
|
||||
// TODO(TSCONVERSION) figure out why so many components pass this yet it isn't used
|
||||
title?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
@autoBindMethodsForReact(AUTOBIND_CFG)
|
||||
@@ -32,7 +33,7 @@ class Button<T> extends PureComponent<ButtonProps<T>> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children, disabled, tabIndex, className, type, id } = this.props;
|
||||
const { children, disabled, tabIndex, className, type, id, style } = this.props;
|
||||
return (
|
||||
<button
|
||||
disabled={disabled}
|
||||
@@ -41,6 +42,7 @@ class Button<T> extends PureComponent<ButtonProps<T>> {
|
||||
tabIndex={tabIndex}
|
||||
className={className}
|
||||
onClick={this._handleClick}
|
||||
style={style}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
|
||||
@@ -10,14 +10,14 @@ import SettingsShortcuts from '../settings/shortcuts';
|
||||
import General from '../settings/general';
|
||||
import { ImportExport } from '../settings/import-export';
|
||||
import Plugins from '../settings/plugins';
|
||||
import Theme from '../settings/theme';
|
||||
import { ThemePanel } from '../settings/theme-panel';
|
||||
import * as models from '../../../models/index';
|
||||
import { Curl } from 'node-libcurl';
|
||||
import Tooltip from '../tooltip';
|
||||
import { applyColorScheme } from '../../../plugins/misc';
|
||||
import * as session from '../../../account/session';
|
||||
import Account from '../settings/account';
|
||||
import { showModal } from './index';
|
||||
import { Settings } from '../../../models/settings';
|
||||
|
||||
export const TAB_INDEX_EXPORT = 1;
|
||||
export const TAB_INDEX_SHORTCUTS = 3;
|
||||
@@ -25,7 +25,7 @@ export const TAB_INDEX_THEMES = 2;
|
||||
export const TAB_INDEX_PLUGINS = 5;
|
||||
|
||||
interface Props {
|
||||
settings: any;
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
interface State {
|
||||
@@ -50,53 +50,6 @@ class SettingsModal extends PureComponent<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
hideSettingsModal() {
|
||||
this.modal?.hide();
|
||||
}
|
||||
|
||||
async _handleChangeTheme(themeName, colorScheme, persist = true) {
|
||||
const { settings } = this.props;
|
||||
let patch;
|
||||
|
||||
switch (colorScheme) {
|
||||
case 'light':
|
||||
patch = {
|
||||
lightTheme: themeName,
|
||||
};
|
||||
break;
|
||||
|
||||
case 'dark':
|
||||
patch = {
|
||||
darkTheme: themeName,
|
||||
};
|
||||
break;
|
||||
|
||||
case 'default':
|
||||
default:
|
||||
patch = {
|
||||
theme: themeName,
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
applyColorScheme({ ...settings, ...patch });
|
||||
|
||||
if (persist) {
|
||||
await models.settings.update(settings, patch);
|
||||
}
|
||||
}
|
||||
|
||||
async _handleAutoDetectColorSchemeChange(autoDetectColorScheme, persist = true) {
|
||||
const { settings } = this.props;
|
||||
applyColorScheme({ ...settings, autoDetectColorScheme });
|
||||
|
||||
if (persist) {
|
||||
models.settings.update(settings, {
|
||||
autoDetectColorScheme,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async _handleUpdateKeyBindings(hotKeyRegistry) {
|
||||
models.settings.update(this.props.settings, {
|
||||
hotKeyRegistry,
|
||||
@@ -165,18 +118,11 @@ class SettingsModal extends PureComponent<Props, State> {
|
||||
</TabPanel>
|
||||
<TabPanel className="react-tabs__tab-panel pad scrollable">
|
||||
<ImportExport
|
||||
hideSettingsModal={this.hideSettingsModal}
|
||||
hideSettingsModal={this.hide}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel className="react-tabs__tab-panel pad scrollable">
|
||||
<Theme
|
||||
handleChangeTheme={this._handleChangeTheme}
|
||||
activeTheme={settings.theme}
|
||||
handleAutoDetectColorSchemeChange={this._handleAutoDetectColorSchemeChange}
|
||||
autoDetectColorScheme={settings.autoDetectColorScheme}
|
||||
activeLightTheme={settings.lightTheme}
|
||||
activeDarkTheme={settings.darkTheme}
|
||||
/>
|
||||
<ThemePanel />
|
||||
</TabPanel>
|
||||
<TabPanel className="react-tabs__tab-panel pad scrollable">
|
||||
<SettingsShortcuts
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
import React, { FC } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { ColorScheme } from '../../../plugins';
|
||||
import { PluginTheme } from '../../../plugins/misc';
|
||||
import HelpTooltip from '../help-tooltip';
|
||||
import { useThemes } from '../../hooks/theme';
|
||||
|
||||
const THEMES_PER_ROW = 5;
|
||||
|
||||
const isDark = (mode: 'dark' | 'light') => mode === 'dark';
|
||||
const isLight = (mode: 'dark' | 'light') => mode === 'light';
|
||||
|
||||
const RootWrapper = styled.div({});
|
||||
|
||||
const CheckboxWrapper = styled.div({
|
||||
marginLeft: 'var(--padding-md)',
|
||||
});
|
||||
|
||||
const Themes = styled.div({
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
});
|
||||
|
||||
const ThemeButton = styled.div<{ $isActive: boolean; $isInOsThemeMode: boolean }>(({ $isActive, $isInOsThemeMode }) => ({
|
||||
position: 'relative',
|
||||
margin: 'var(--padding-md) var(--padding-md)',
|
||||
fontSize: 0,
|
||||
borderRadius: 'var(--radius-md)',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 0 0 1px var(--hl-sm)',
|
||||
transition: 'all 150ms ease-out',
|
||||
...($isActive ? {
|
||||
boxShadow: '0 0 0 var(--padding-xs) var(--color-surprise)',
|
||||
transform: 'scale(1.05)',
|
||||
} : {}),
|
||||
'&:hover': {
|
||||
transform: 'scale(1.05)',
|
||||
...($isInOsThemeMode ? { boxShadow: 'none' } : {}),
|
||||
},
|
||||
...($isInOsThemeMode ? {
|
||||
'&:hover .overlay-wrapper': {
|
||||
display: 'flex',
|
||||
},
|
||||
'&:hover .theme-preview': {
|
||||
visibility: 'hidden', // this prevents alpha-blending problems with the underlying svg bleeding through
|
||||
},
|
||||
} : {}),
|
||||
}));
|
||||
|
||||
const ThemeTitle = styled.h2({
|
||||
marginTop: 0,
|
||||
marginBottom: 'var(--padding-xs)',
|
||||
fontSize: 'var(--font-size-md)',
|
||||
});
|
||||
|
||||
const ThemeWrapper = styled.div({
|
||||
maxWidth: `${100 / THEMES_PER_ROW}%`,
|
||||
minWidth: 110,
|
||||
marginBottom: 'var(--padding-md)',
|
||||
marginTop: 'var(--padding-md)',
|
||||
textAlign: 'center',
|
||||
});
|
||||
|
||||
const ColorSchemeBadge = styled.div<{ $theme: 'dark' | 'light'}>(({ $theme }) => ({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
width: 12,
|
||||
height: 12,
|
||||
fill: 'white',
|
||||
padding: 4,
|
||||
background: 'var(--color-surprise)',
|
||||
...(isDark($theme) ? {
|
||||
right: 0,
|
||||
borderBottomLeftRadius: 'var(--radius-md)',
|
||||
} : {}),
|
||||
...(isLight($theme) ? {
|
||||
left: 0,
|
||||
borderBottomRightRadius: 'var(--radius-md)',
|
||||
} : {}),
|
||||
}));
|
||||
|
||||
const OverlayWrapper = styled.div({
|
||||
position: 'absolute',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'nowrap',
|
||||
justifyContent: 'space-evenly',
|
||||
boxSizing: 'content-box',
|
||||
|
||||
display: 'none', // controlled by hover on the ThemeWrapper
|
||||
});
|
||||
|
||||
const OverlaySide = styled.div<{ $theme: 'dark' | 'light' }>(({ $theme }) => ({
|
||||
display: 'flex',
|
||||
cursor: 'pointer',
|
||||
flexGrow: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
background: 'var(--color-bg)',
|
||||
'& svg': {
|
||||
opacity: 0.4,
|
||||
fill: 'var(--color-fg)',
|
||||
height: 20,
|
||||
},
|
||||
'&:hover svg': {
|
||||
opacity: 1,
|
||||
fill: 'var(--color-surprise)',
|
||||
},
|
||||
boxSizing: 'border-box',
|
||||
border: '1px solid var(--hl-sm)',
|
||||
...(isDark($theme) ? {
|
||||
borderTopRightRadius: 'var(--radius-md)',
|
||||
borderBottomRightRadius: 'var(--radius-md)',
|
||||
borderLeftStyle: 'none',
|
||||
} : {}),
|
||||
...(isLight($theme) ? {
|
||||
borderTopLeftRadius: 'var(--radius-md)',
|
||||
borderBottomLeftRadius: 'var(--radius-md)',
|
||||
borderRightStyle: 'none',
|
||||
} : {}),
|
||||
'&:hover': {
|
||||
border: '1px solid var(--color-surprise)',
|
||||
},
|
||||
}));
|
||||
|
||||
const SunSvg = () => (
|
||||
<svg viewBox="0 0 14 14">
|
||||
<circle cx="7" cy="7" r="4" />
|
||||
<rect x="6.5" y="12" width="1" height="2" rx="0.5" />
|
||||
<rect x="6.5" width="1" height="2" rx="0.5" />
|
||||
<rect x="2" y="6.5" width="1" height="2" rx="0.5" transform="rotate(90 2 6.5)" />
|
||||
<rect x="14" y="6.5" width="1" height="2" rx="0.5" transform="rotate(90 14 6.5)" />
|
||||
<rect x="3.81799" y="3.11084" width="1" height="2" rx="0.5" transform="rotate(135 3.81799 3.11084)" />
|
||||
<rect x="12.3033" y="11.5962" width="1" height="2" rx="0.5" transform="rotate(135 12.3033 11.5962)" />
|
||||
<rect x="10.889" y="3.81799" width="1" height="2" rx="0.5" transform="rotate(-135 10.889 3.81799)" />
|
||||
<rect x="2.40381" y="12.3033" width="1" height="2" rx="0.5" transform="rotate(-135 2.40381 12.3033)" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const MoonSvg = () => (
|
||||
<svg viewBox="0 0 14 14">
|
||||
<path
|
||||
d="M4.00005 11C7.86604 11 11 7.86604 11 4.00005C11 2.83978 10.7178 1.74545 10.2181 0.781982C12.4649 1.94714 14 4.29432 14 7.00005C14 10.866 10.866 14 7.00005 14C4.29432 14 1.94714 12.4649 0.781982 10.2181C1.74545 10.7178 2.83978 11 4.00005 11Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const ThemePreview: FC<{ theme: PluginTheme }> = ({ theme: { name: themeName } }) => (
|
||||
<svg
|
||||
/* @ts-expect-error -- TSCONVERSION */
|
||||
theme={themeName}
|
||||
className="theme-preview"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 500 300"
|
||||
>
|
||||
{/*
|
||||
A WORD TO THE WISE: If you, dear traveler from the future, are here
|
||||
for the purpose of theming things due to changes in the app structure,
|
||||
please remember to add `--sub` to your classes or else the selected class'
|
||||
theme variables will apply to all theme previews. Search your codebase
|
||||
for `--sub` to see more.
|
||||
*/}
|
||||
|
||||
{/* @ts-expect-error -- TSCONVERSION */}
|
||||
<g subtheme={themeName}>
|
||||
{/* App Header */}
|
||||
<g className="theme--app-header--sub">
|
||||
<rect x="0" y="0" width="100%" height="10%" style={{ fill: 'var(--color-bg)' }} />
|
||||
</g>
|
||||
|
||||
{/* Panes */}
|
||||
<g className="theme--pane--sub">
|
||||
{/* Response Area */}
|
||||
<rect x="0" y="10%" width="100%" height="100%" style={{ fill: 'var(--color-bg)' }} />
|
||||
|
||||
{/* URL Bars */}
|
||||
<rect
|
||||
x="25%"
|
||||
y="10%"
|
||||
width="100%"
|
||||
height="10%"
|
||||
className="theme--pane__header--sub"
|
||||
style={{ fill: 'var(--color-bg)' }}
|
||||
/>
|
||||
{/* Send Button */}
|
||||
<g>
|
||||
<rect x="53%" y="10%" width="9%" height="10%" style={{ fill: 'var(--color-surprise)' }} />
|
||||
</g>
|
||||
</g>
|
||||
|
||||
{/* Sidebar */}
|
||||
<g className="theme--sidebar--sub">
|
||||
<rect x="0" y="10%" width="25%" height="100%" style={{ fill: 'var(--color-bg)' }} />
|
||||
</g>
|
||||
|
||||
{/* Lines */}
|
||||
<line x1="0%" x2="100%" y1="10%" y2="10%" strokeWidth="1" style={{ stroke: 'var(--hl-md)' }} />
|
||||
<line x1="25%" x2="100%" y1="20%" y2="20%" strokeWidth="1" style={{ stroke: 'var(--hl-md)' }} />
|
||||
<line x1="62%" x2="62%" y1="10%" y2="100%" strokeWidth="1" style={{ stroke: 'var(--hl-md)' }} />
|
||||
<line x1="25%" x2="25%" y1="10%" y2="100%" strokeWidth="1" style={{ stroke: 'var(--hl-md)' }} />
|
||||
|
||||
{/* Color Squares */}
|
||||
<rect x="40%" y="85%" width="5%" height="8%" style={{ fill: 'var(--color-success)' }} />
|
||||
<rect x="50%" y="85%" width="5%" height="8%" style={{ fill: 'var(--color-info)' }} />
|
||||
<rect x="60%" y="85%" width="5%" height="8%" style={{ fill: 'var(--color-warning)' }} />
|
||||
<rect x="70%" y="85%" width="5%" height="8%" style={{ fill: 'var(--color-danger)' }} />
|
||||
<rect x="80%" y="85%" width="5%" height="8%" style={{ fill: 'var(--color-surprise)' }} />
|
||||
<rect x="90%" y="85%" width="5%" height="8%" style={{ fill: 'var(--color-info)' }} />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IndividualTheme: FC<{
|
||||
isActive: boolean;
|
||||
isDark: boolean;
|
||||
isLight: boolean;
|
||||
isInOsThemeMode: boolean;
|
||||
onChangeTheme: (name: string, mode: ColorScheme) => Promise<void>;
|
||||
theme: PluginTheme
|
||||
}> = ({
|
||||
isActive,
|
||||
isDark,
|
||||
isLight,
|
||||
isInOsThemeMode,
|
||||
onChangeTheme,
|
||||
theme,
|
||||
}) => {
|
||||
const { displayName, name } = theme;
|
||||
|
||||
const onClickThemeButton = () => {
|
||||
if (isInOsThemeMode) {
|
||||
// The overlays handle this behavior in OS theme mode.
|
||||
// React's event bubbling means that this will be fired when you click on an overlay, so we need to turn it off when in this mode.
|
||||
// Even still, we don't want to risk some potnetial subpixel or z-index nonsense accidentally setting the default when know we shouldn't.
|
||||
return;
|
||||
}
|
||||
return onChangeTheme(name, 'default');
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeWrapper>
|
||||
<ThemeTitle>{displayName}</ThemeTitle>
|
||||
|
||||
<ThemeButton
|
||||
onClick={onClickThemeButton}
|
||||
$isActive={isActive}
|
||||
$isInOsThemeMode={isInOsThemeMode}
|
||||
>
|
||||
{isInOsThemeMode ? (
|
||||
<>
|
||||
<OverlayWrapper className="overlay-wrapper">
|
||||
<OverlaySide $theme="light" onClick={() => { onChangeTheme(name, 'light'); }}><SunSvg /></OverlaySide>
|
||||
<OverlaySide $theme="dark" onClick={() => { onChangeTheme(name, 'dark'); }}><MoonSvg /></OverlaySide>
|
||||
</OverlayWrapper>
|
||||
|
||||
{isActive && isDark ? (
|
||||
<ColorSchemeBadge $theme="dark"><MoonSvg /></ColorSchemeBadge>
|
||||
) : null}
|
||||
|
||||
{isActive && isLight ? (
|
||||
<ColorSchemeBadge $theme="light"><SunSvg /></ColorSchemeBadge>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<ThemePreview theme={theme} />
|
||||
</ThemeButton>
|
||||
</ThemeWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export const ThemePanel: FC = () => {
|
||||
const {
|
||||
themes,
|
||||
activate,
|
||||
changeAutoDetect,
|
||||
isActive,
|
||||
isActiveDark,
|
||||
isActiveLight,
|
||||
autoDetectColorScheme,
|
||||
} = useThemes();
|
||||
|
||||
return (
|
||||
<RootWrapper>
|
||||
<CheckboxWrapper className="form-control form-control--thin">
|
||||
<label className="inline-block">
|
||||
Use OS color scheme
|
||||
<HelpTooltip className="space-left">
|
||||
Pick your prefered themes for light and dark
|
||||
</HelpTooltip>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="autoDetectColorScheme"
|
||||
checked={autoDetectColorScheme}
|
||||
onChange={changeAutoDetect}
|
||||
/>
|
||||
</label>
|
||||
</CheckboxWrapper>
|
||||
|
||||
<Themes>
|
||||
{themes.map(theme => (
|
||||
<IndividualTheme
|
||||
key={theme.name}
|
||||
theme={theme}
|
||||
isActive={isActive(theme)}
|
||||
onChangeTheme={activate}
|
||||
isDark={isActiveDark(theme)}
|
||||
isLight={isActiveLight(theme)}
|
||||
isInOsThemeMode={autoDetectColorScheme}
|
||||
/>
|
||||
))}
|
||||
</Themes>
|
||||
</RootWrapper>
|
||||
);
|
||||
};
|
||||
@@ -1,218 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
||||
import { AUTOBIND_CFG } from '../../../common/constants';
|
||||
import Button from '../base/button';
|
||||
import type { Theme as ThemeType, ColorScheme } from '../../../plugins';
|
||||
import { getThemes } from '../../../plugins';
|
||||
import HelpTooltip from '../help-tooltip';
|
||||
const THEMES_PER_ROW = 5;
|
||||
|
||||
interface Props {
|
||||
handleChangeTheme: (arg0: string, arg1: ColorScheme) => void;
|
||||
activeTheme: string;
|
||||
handleAutoDetectColorSchemeChange: (arg0: boolean) => void;
|
||||
autoDetectColorScheme: boolean;
|
||||
activeLightTheme: string;
|
||||
activeDarkTheme: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
themes: ThemeType[];
|
||||
}
|
||||
|
||||
@autoBindMethodsForReact(AUTOBIND_CFG)
|
||||
class Theme extends PureComponent<Props, State> {
|
||||
state: State = {
|
||||
themes: [],
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._loadThemes();
|
||||
}
|
||||
|
||||
async _loadThemes() {
|
||||
const themes = await getThemes();
|
||||
this.setState({
|
||||
themes,
|
||||
});
|
||||
}
|
||||
|
||||
renderThemePreview(themeName: string) {
|
||||
return (
|
||||
/* @ts-expect-error -- TSCONVERSION */
|
||||
<svg theme={themeName} width="100%" height="100%" viewBox="0 0 500 300">
|
||||
{/*
|
||||
A WORD TO THE WISE: If you, dear traveler from the future, are here
|
||||
for the purpose of theming things due to changes in the app structure,
|
||||
please remember to add `--sub` to your classes or else the selected class'
|
||||
theme variables will apply to all theme previews. Search your codebase
|
||||
for `--sub` to see more.
|
||||
*/}
|
||||
|
||||
{/* @ts-expect-error -- TSCONVERSION */}
|
||||
<g subtheme={themeName}>
|
||||
{/* App Header */}
|
||||
<g className="theme--app-header--sub">
|
||||
<rect x="0" y="0" width="100%" height="10%" className="bg-fill" />
|
||||
</g>
|
||||
|
||||
{/* Panes */}
|
||||
<g className="theme--pane--sub">
|
||||
{/* Response Area */}
|
||||
<rect x="0" y="10%" width="100%" height="100%" className="bg-fill" />
|
||||
|
||||
{/* URL Bars */}
|
||||
<rect
|
||||
x="25%"
|
||||
y="10%"
|
||||
width="100%"
|
||||
height="10%"
|
||||
className="theme--pane__header--sub bg-fill"
|
||||
/>
|
||||
{/* Send Button */}
|
||||
<g>
|
||||
<rect x="53%" y="10%" width="9%" height="10%" className="surprise-fill" />
|
||||
</g>
|
||||
</g>
|
||||
|
||||
{/* Sidebar */}
|
||||
<g className="theme--sidebar--sub">
|
||||
<rect x="0" y="10%" width="25%" height="100%" className="bg-fill" />
|
||||
</g>
|
||||
|
||||
{/* Lines */}
|
||||
<line x1="0%" x2="100%" y1="10%" y2="10%" strokeWidth="1" className="hl-stroke" />
|
||||
<line x1="25%" x2="100%" y1="20%" y2="20%" strokeWidth="1" className="hl-stroke" />
|
||||
<line x1="62%" x2="62%" y1="10%" y2="100%" strokeWidth="1" className="hl-stroke" />
|
||||
<line x1="25%" x2="25%" y1="10%" y2="100%" strokeWidth="1" className="hl-stroke" />
|
||||
|
||||
{/* Color Squares */}
|
||||
<rect x="40%" y="85%" width="5%" height="8%" className="success-fill" />
|
||||
<rect x="50%" y="85%" width="5%" height="8%" className="info-fill" />
|
||||
<rect x="60%" y="85%" width="5%" height="8%" className="warning-fill" />
|
||||
<rect x="70%" y="85%" width="5%" height="8%" className="danger-fill" />
|
||||
<rect x="80%" y="85%" width="5%" height="8%" className="surprise-fill" />
|
||||
<rect x="90%" y="85%" width="5%" height="8%" className="info-fill" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
renderTheme(theme: ThemeType) {
|
||||
const { handleChangeTheme, activeTheme, autoDetectColorScheme } = this.props;
|
||||
const isActive = activeTheme === theme.theme.name;
|
||||
const disabled = autoDetectColorScheme;
|
||||
return (
|
||||
<div
|
||||
key={theme.theme.name}
|
||||
className="themes__theme"
|
||||
style={{
|
||||
maxWidth: `${100 / THEMES_PER_ROW}%`,
|
||||
}}>
|
||||
<h2 className="txt-lg">{theme.theme.displayName}</h2>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={() => handleChangeTheme(theme.theme.name, 'default')}
|
||||
className={isActive ? 'active' : ''}>
|
||||
{this.renderThemePreview(theme.theme.name)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderThemeRows() {
|
||||
const { themes } = this.state;
|
||||
const rows: ThemeType[][] = [];
|
||||
let row: ThemeType[] = [];
|
||||
|
||||
for (const theme of themes) {
|
||||
row.push(theme);
|
||||
|
||||
if (row.length === THEMES_PER_ROW) {
|
||||
rows.push(row);
|
||||
row = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Push the last row if it wasn't finished
|
||||
if (row.length) {
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
return rows.map((row, i) => (
|
||||
<div key={i} className="themes__row">
|
||||
{row.map(this.renderTheme)}
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
renderThemeSelect(scheme: 'light' | 'dark') {
|
||||
const {
|
||||
activeLightTheme,
|
||||
activeDarkTheme,
|
||||
handleChangeTheme,
|
||||
autoDetectColorScheme,
|
||||
} = this.props;
|
||||
const { themes } = this.state;
|
||||
const activeColorTheme =
|
||||
scheme === 'light' ? activeLightTheme : activeDarkTheme;
|
||||
return (
|
||||
<div>
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
Preferred {scheme} color scheme
|
||||
<HelpTooltip className="space-left">
|
||||
Lets you choose the color scheme that is used in {scheme} mode.
|
||||
</HelpTooltip>
|
||||
<select
|
||||
value={activeColorTheme}
|
||||
onChange={(e) => handleChangeTheme(e.target.value, scheme)}
|
||||
>
|
||||
{themes.map((theme) => (
|
||||
<option key={theme.theme.name} value={theme.theme.name}>
|
||||
{theme.theme.displayName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div className="themes select__preview">
|
||||
<div className="themes__theme" style={{ maxWidth: '50%' }}>
|
||||
<Button disabled={!autoDetectColorScheme}>
|
||||
{this.renderThemePreview(activeColorTheme)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { autoDetectColorScheme, handleAutoDetectColorSchemeChange } = this.props;
|
||||
return (
|
||||
<>
|
||||
<div className="themes general__preview">{this.renderThemeRows()}</div>
|
||||
<div className="form-control form-control--thin">
|
||||
<label className="inline-block">
|
||||
Use OS color scheme
|
||||
<HelpTooltip className="space-left">
|
||||
Lets you choose one theme for light mode and one for dark mode.
|
||||
</HelpTooltip>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="autoDetectColorScheme"
|
||||
checked={autoDetectColorScheme}
|
||||
onChange={e => handleAutoDetectColorSchemeChange(e.target.checked)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
{this.renderThemeSelect('light')}
|
||||
{this.renderThemeSelect('dark')}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Theme;
|
||||
@@ -1,109 +0,0 @@
|
||||
.themes {
|
||||
&.general__preview {
|
||||
padding-top: var(--padding-lg);
|
||||
|
||||
}
|
||||
|
||||
&.select__preview {
|
||||
padding-top: var(--padding-xxs);
|
||||
padding-bottom: var(--padding-xxs);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.themes__row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: var(--padding-lg);
|
||||
margin-left: calc(var(--padding-md) * -1);
|
||||
margin-right: calc(var(--padding-md) * -1);
|
||||
}
|
||||
|
||||
.themes__theme {
|
||||
text-align: center;
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: var(--padding-xs);
|
||||
font-size: var(--font-size-md) !important;
|
||||
}
|
||||
|
||||
svg {
|
||||
.bg-fill {
|
||||
fill: var(--color-bg);
|
||||
}
|
||||
|
||||
.success-fill {
|
||||
fill: var(--color-success);
|
||||
}
|
||||
|
||||
.notice-fill {
|
||||
fill: var(--color-notice);
|
||||
}
|
||||
|
||||
.warning-fill {
|
||||
fill: var(--color-warning);
|
||||
}
|
||||
|
||||
.danger-fill {
|
||||
fill: var(--color-danger);
|
||||
}
|
||||
|
||||
.surprise-fill {
|
||||
fill: var(--color-surprise);
|
||||
}
|
||||
|
||||
.info-fill {
|
||||
fill: var(--color-info);
|
||||
}
|
||||
|
||||
.fg-fill {
|
||||
fill: var(--color-fg);
|
||||
}
|
||||
|
||||
.hl-stroke {
|
||||
stroke: var(--hl-md);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
position: relative;
|
||||
margin: var(--padding-md) var(--padding-md);
|
||||
font-size: 0;
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 0 1px var(--hl-sm);
|
||||
|
||||
&.active:enabled {
|
||||
box-shadow: 0 0 0 var(--padding-xs) var(--color-surprise);
|
||||
}
|
||||
|
||||
&.active:enabled,
|
||||
&:hover:enabled {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transition: all 50ms ease-out;
|
||||
}
|
||||
|
||||
&::after {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
position: absolute;
|
||||
font-size: var(--font-size-md);
|
||||
text-transform: uppercase;
|
||||
color: var(--hl-xl);
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,6 @@
|
||||
@import 'components/unit-tests';
|
||||
@import 'components/tabs';
|
||||
@import 'components/tag';
|
||||
@import 'components/themes';
|
||||
@import 'components/toast';
|
||||
@import 'components/request-url-bar';
|
||||
@import 'components/workspace-dropdown';
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { ChangeEvent, useCallback, useEffect, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { unreachableCase } from 'ts-assert-unreachable';
|
||||
import * as models from '../../models';
|
||||
import { ThemeSettings } from '../../models/settings';
|
||||
import { ColorScheme, getThemes } from '../../plugins';
|
||||
import { applyColorScheme, PluginTheme } from '../../plugins/misc';
|
||||
import { selectSettings } from '../redux/selectors';
|
||||
|
||||
export const useThemes = () => {
|
||||
const {
|
||||
lightTheme,
|
||||
darkTheme,
|
||||
autoDetectColorScheme,
|
||||
theme,
|
||||
pluginConfig,
|
||||
} = useSelector(selectSettings);
|
||||
|
||||
const [themes, setThemes] = useState<PluginTheme[]>([]);
|
||||
|
||||
// Reload themes if pluginConfig changes
|
||||
useEffect(() => {
|
||||
const func = async () => {
|
||||
const pluginThemes = await getThemes();
|
||||
setThemes(pluginThemes.map(({ theme }) => theme));
|
||||
};
|
||||
func();
|
||||
}, [pluginConfig]);
|
||||
|
||||
// Check if the theme is active
|
||||
const isActiveDark = useCallback(({ name }: PluginTheme) => name === darkTheme, [darkTheme]);
|
||||
const isActiveLight = useCallback(({ name }: PluginTheme) => name === lightTheme, [lightTheme]);
|
||||
|
||||
const isActive = useCallback((pluginTheme : PluginTheme) => {
|
||||
if (autoDetectColorScheme) {
|
||||
return isActiveLight(pluginTheme) || isActiveDark(pluginTheme);
|
||||
}
|
||||
return pluginTheme.name === theme;
|
||||
}, [autoDetectColorScheme, isActiveDark, isActiveLight, theme]);
|
||||
|
||||
// Apply the theme and update settings
|
||||
const apply = useCallback(async (patch: Partial<ThemeSettings>) => {
|
||||
applyColorScheme({
|
||||
theme,
|
||||
autoDetectColorScheme,
|
||||
darkTheme,
|
||||
lightTheme,
|
||||
...patch,
|
||||
});
|
||||
await models.settings.patch(patch);
|
||||
}, [autoDetectColorScheme, darkTheme, lightTheme, theme]);
|
||||
|
||||
const changeAutoDetect = useCallback(({ target: { checked } }: ChangeEvent<HTMLInputElement>) => apply({ autoDetectColorScheme: checked }), [apply]);
|
||||
|
||||
// Activate the theme for the selected color scheme
|
||||
const activate = useCallback(async (themeName: string, colorScheme: ColorScheme) => {
|
||||
switch (colorScheme) {
|
||||
case 'light':
|
||||
await apply({ lightTheme: themeName });
|
||||
break;
|
||||
|
||||
case 'dark':
|
||||
await apply({ darkTheme: themeName });
|
||||
break;
|
||||
|
||||
case 'default':
|
||||
await apply({ theme: themeName });
|
||||
break;
|
||||
|
||||
default:
|
||||
unreachableCase(colorScheme);
|
||||
}
|
||||
}, [apply]);
|
||||
|
||||
return {
|
||||
themes,
|
||||
isActive,
|
||||
isActiveLight,
|
||||
isActiveDark,
|
||||
activate,
|
||||
changeAutoDetect,
|
||||
autoDetectColorScheme,
|
||||
};
|
||||
};
|
||||
Reference in new issue
Block a user