ThemeEditor: store colors as HSL

theme-editor-hsl
Alex Gleason 2022-12-17 23:15:29 -06:00
rodzic fc360cbac4
commit b8e987b8df
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 7211D1F99744FBB7
6 zmienionych plików z 68 dodań i 36 usunięć

Wyświetl plik

@ -8,6 +8,7 @@ import { Provider } from 'react-redux';
import { BrowserRouter, Switch, Redirect, Route } from 'react-router-dom';
// @ts-ignore: it doesn't have types
import { ScrollContext } from 'react-router-scroll-4';
import { updateSoapboxConfig } from 'soapbox/actions/admin';
import { loadInstance } from 'soapbox/actions/instance';
import { fetchMe } from 'soapbox/actions/me';

Wyświetl plik

@ -2,8 +2,9 @@ import React, { useEffect, useRef, useState } from 'react';
import { HStack, Stack, Slider } from 'soapbox/components/ui';
import { usePrevious } from 'soapbox/hooks';
import { Hsl, HslColorPalette } from 'soapbox/types/colors';
import { compareId } from 'soapbox/utils/comparators';
import { hslShift } from 'soapbox/utils/theme';
import { hexToHsl, hslShift, hslToHex } from 'soapbox/utils/theme';
import Color from './color';
import HSLToggler from './hsl-toggler';
@ -13,8 +14,8 @@ interface ColorGroup {
}
interface IPalette {
palette: ColorGroup,
onChange: (palette: ColorGroup) => void,
palette: HslColorPalette,
onChange: (palette: HslColorPalette) => void,
resetKey?: string,
}
@ -29,10 +30,10 @@ const Palette: React.FC<IPalette> = ({ palette, onChange, resetKey }) => {
const skipUpdate = useRef(false);
const handleChange = (tint: string) => {
return (color: string) => {
return (hex: string) => {
onChange({
...palette,
[tint]: color,
[tint]: hexToHsl(hex)!,
});
};
};
@ -45,8 +46,8 @@ const Palette: React.FC<IPalette> = ({ palette, onChange, resetKey }) => {
const delta = slider - (lastSlider || 0);
const adjusted = Object.entries(palette).reduce<ColorGroup>((result, [tint, hex]) => {
result[tint] = hslShift(hex, {
const adjusted = Object.entries(palette).reduce<HslColorPalette>((result, [tint, hsl]) => {
result[tint] = hslShift(hsl as Hsl, {
h: hslKey === 'h' ? delta * 360 : 0,
s: hslKey === 's' ? delta * 200 : 0,
l: hslKey === 'l' ? delta * 200 : 0,
@ -70,7 +71,7 @@ const Palette: React.FC<IPalette> = ({ palette, onChange, resetKey }) => {
<Stack space={1} className='w-full'>
<HStack className='h-8 rounded-md overflow-hidden'>
{tints.map(tint => (
<Color color={palette[tint]} onChange={handleChange(tint)} />
<Color color={hslToHex(palette[tint] as Hsl)} onChange={handleChange(tint)} />
))}
</HStack>

Wyświetl plik

@ -13,10 +13,13 @@ import ColorWithPicker from 'soapbox/features/soapbox-config/components/color-wi
import { useAppDispatch, useAppSelector, useSoapboxConfig } from 'soapbox/hooks';
import { normalizeSoapboxConfig } from 'soapbox/normalizers';
import { download } from 'soapbox/utils/download';
import { hexToHslPalette } from 'soapbox/utils/tailwind';
import { hexToHsl, hslToHex } from 'soapbox/utils/theme';
import Palette, { ColorGroup } from './components/palette';
import Palette from './components/palette';
import type { ColorChangeHandler } from 'react-color';
import type { Hsl, HslColorPalette } from 'soapbox/types/colors';
const messages = defineMessages({
title: { id: 'admin.theme.title', defaultMessage: 'Theme' },
@ -39,14 +42,14 @@ const ThemeEditor: React.FC<IThemeEditor> = () => {
const host = useAppSelector(state => getHost(state));
const rawConfig = useAppSelector(state => state.soapbox);
const [colors, setColors] = useState(soapbox.colors.toJS() as any);
const [colors, setColors] = useState(hexToHslPalette(soapbox.colors.toJS() as any));
const [submitting, setSubmitting] = useState(false);
const [resetKey, setResetKey] = useState(uuidv4());
const fileInput = useRef<HTMLInputElement>(null);
const updateColors = (key: string) => {
return (newColors: ColorGroup) => {
return (newColors: HslColorPalette) => {
setColors({
...colors,
[key]: {
@ -61,7 +64,7 @@ const ThemeEditor: React.FC<IThemeEditor> = () => {
return (hex: string) => {
setColors({
...colors,
[key]: hex,
[key]: hexToHsl(hex)!,
});
};
};
@ -72,16 +75,19 @@ const ThemeEditor: React.FC<IThemeEditor> = () => {
};
const resetTheme = () => {
setTheme(soapbox.colors.toJS() as any);
setTheme(hexToHslPalette(soapbox.colors.toJS() as any));
};
dispatch(updateSoapboxConfig(rawConfig.set('colors', {})));
const updateTheme = async () => {
const params = rawConfig.set('colors', colors).toJS();
await dispatch(updateSoapboxConfig(params));
// FIXME: convert HSL back to Hex
// const params = rawConfig.set('colors', colors).toJS();
// await dispatch(updateSoapboxConfig(params));
};
const restoreDefaultTheme = () => {
const colors = normalizeSoapboxConfig({ brandColor: '#0482d8' }).colors.toJS();
const colors = hexToHslPalette(normalizeSoapboxConfig({ brandColor: '#0482d8' }).colors.toJS() as any);
setTheme(colors);
};
@ -126,42 +132,42 @@ const ThemeEditor: React.FC<IThemeEditor> = () => {
<List>
<PaletteListItem
label='Primary'
palette={colors.primary}
palette={colors.primary as HslColorPalette}
onChange={updateColors('primary')}
resetKey={resetKey}
/>
<PaletteListItem
label='Secondary'
palette={colors.secondary}
palette={colors.secondary as HslColorPalette}
onChange={updateColors('secondary')}
resetKey={resetKey}
/>
<PaletteListItem
label='Accent'
palette={colors.accent}
palette={colors.accent as HslColorPalette}
onChange={updateColors('accent')}
resetKey={resetKey}
/>
<PaletteListItem
label='Gray'
palette={colors.gray}
palette={colors.gray as HslColorPalette}
onChange={updateColors('gray')}
resetKey={resetKey}
/>
<PaletteListItem
label='Success'
palette={colors.success}
palette={colors.success as HslColorPalette}
onChange={updateColors('success')}
resetKey={resetKey}
/>
<PaletteListItem
label='Danger'
palette={colors.danger}
palette={colors.danger as HslColorPalette}
onChange={updateColors('danger')}
resetKey={resetKey}
/>
@ -170,25 +176,25 @@ const ThemeEditor: React.FC<IThemeEditor> = () => {
<List>
<ColorListItem
label='Greentext'
value={colors.greentext}
value={hslToHex(colors.greentext as Hsl)}
onChange={updateColor('greentext')}
/>
<ColorListItem
label='Accent Blue'
value={colors['accent-blue']}
value={hslToHex(colors['accent-blue'] as Hsl)}
onChange={updateColor('accent-blue')}
/>
<ColorListItem
label='Gradient Start'
value={colors['gradient-start']}
value={hslToHex(colors['gradient-start'] as Hsl)}
onChange={updateColor('gradient-start')}
/>
<ColorListItem
label='Gradient End'
value={colors['gradient-end']}
value={hslToHex(colors['gradient-end'] as Hsl)}
onChange={updateColor('gradient-end')}
/>
</List>
@ -233,8 +239,8 @@ const ThemeEditor: React.FC<IThemeEditor> = () => {
interface IPaletteListItem {
label: React.ReactNode,
palette: ColorGroup,
onChange: (palette: ColorGroup) => void,
palette: HslColorPalette,
onChange: (palette: HslColorPalette) => void,
resetKey?: string,
}

Wyświetl plik

@ -8,3 +8,7 @@ export type TailwindColorObject = {
export type TailwindColorPalette = {
[key: string]: TailwindColorObject | string,
}
export type HslColorPalette = {
[key: string]: HslColorPalette | Hsl,
}

Wyświetl plik

@ -1,9 +1,9 @@
import { Map as ImmutableMap, fromJS } from 'immutable';
import tintify from 'soapbox/utils/colors';
import { generateAccent, generateNeutral } from 'soapbox/utils/theme';
import { generateAccent, generateNeutral, hexToHsl } from 'soapbox/utils/theme';
import type { TailwindColorPalette } from 'soapbox/types/colors';
import type { HslColorPalette, TailwindColorPalette } from 'soapbox/types/colors';
type SoapboxConfig = ImmutableMap<string, any>;
type SoapboxColors = ImmutableMap<string, any>;
@ -54,3 +54,18 @@ export const toTailwind = (soapboxConfig: SoapboxConfig): SoapboxConfig => {
return soapboxConfig.set('colors', legacyColors.mergeDeep(colors));
};
export const hexToHslPalette = (palette: TailwindColorPalette): HslColorPalette => {
return Object.entries(palette).reduce<HslColorPalette>((result, [key, value]) => {
if (typeof value === 'string') {
const hsl = hexToHsl(value);
if (hsl) {
result[key] = hsl;
}
} else {
result[key] = hexToHslPalette(value);
}
return result;
}, {});
};

Wyświetl plik

@ -43,7 +43,7 @@ const rgbToHsl = (value: Rgb): Hsl => {
};
// https://stackoverflow.com/a/44134328
function hslToHex(color: Hsl): string {
export function hslToHex(color: Hsl): string {
const { h, s } = color;
let { l } = color;
@ -117,17 +117,22 @@ export const generateThemeCss = (soapboxConfig: SoapboxConfig): string => {
return colorsToCss(soapboxConfig.colors.toJS() as TailwindColorPalette);
};
const hexToHsl = (hex: string): Hsl | null => {
export const hexToHsl = (hex: string): Hsl | null => {
const rgb = hexToRgb(hex);
return rgb ? rgbToHsl(rgb) : null;
};
export const hslShift = (hex: string, delta: Hsl): string => {
const { h, s, l } = hexToHsl(hex)!;
export const isHsl = (value: any): value is Hsl => {
return typeof value === 'object'
&& typeof value.h === 'number'
&& typeof value.s === 'number'
&& typeof value.l === 'number';
};
return hslToHex({
export const hslShift = ({ h, s, l }: Hsl, delta: Hsl): Hsl => {
return {
h: (h + delta.h) % 360,
s: Math.max(Math.min(s + delta.s, 100), 0),
l: Math.max(Math.min(l + delta.l, 100), 0),
});
};
};