kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
ThemeEditor: store colors as HSL
rodzic
fc360cbac4
commit
b8e987b8df
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -8,3 +8,7 @@ export type TailwindColorObject = {
|
|||
export type TailwindColorPalette = {
|
||||
[key: string]: TailwindColorObject | string,
|
||||
}
|
||||
|
||||
export type HslColorPalette = {
|
||||
[key: string]: HslColorPalette | Hsl,
|
||||
}
|
|
@ -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;
|
||||
}, {});
|
||||
};
|
|
@ -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),
|
||||
});
|
||||
};
|
||||
};
|
||||
|
|
Ładowanie…
Reference in New Issue