Porównaj commity

...

2 Commity

Autor SHA1 Wiadomość Data
Alex Gleason b8e987b8df
ThemeEditor: store colors as HSL 2022-12-17 23:15:29 -06:00
Alex Gleason fc360cbac4
ThemeEditor: add HSL togglers 2022-12-17 22:34:39 -06:00
8 zmienionych plików z 152 dodań i 50 usunięć

Wyświetl plik

@ -53,7 +53,7 @@ const Slider: React.FC<ISlider> = ({ value, onChange }) => {
return (
<div
className='inline-flex cursor-pointer h-6 relative transition'
className='inline-flex w-full cursor-pointer h-6 relative transition'
onMouseDown={handleMouseDown}
ref={node}
>

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

@ -0,0 +1,49 @@
import classNames from 'clsx';
import React from 'react';
import { HStack } from 'soapbox/components/ui';
type HSLKey = 'h' | 's' | 'l'
interface IHSLToggler {
value: HSLKey
onChange(key: HSLKey): void
}
/** Tiny 3-way toggler between H, S, an L.. */
const HSLToggler: React.FC<IHSLToggler> = ({ value, onChange }) => {
return (
<HStack space={1} className='px-1 py-0.5 bg-primary-200 dark:bg-primary-900 rounded-sm'>
<HSLButton value='h' active={value === 'h'} onClick={onChange} />
<HSLButton value='s' active={value === 's'} onClick={onChange} />
<HSLButton value='l' active={value === 'l'} onClick={onChange} />
</HStack>
);
};
interface IHSLButton {
active: boolean
value: HSLKey
onClick(key: HSLKey): void
}
const HSLButton: React.FC<IHSLButton> = ({ active, value, onClick }) => {
const handleClick = () => {
onClick(value);
};
return (
<button
type='button'
className={classNames('px-1 rounded-sm text-xs', {
'bg-primary-600 text-white': active,
'text-black dark:text-white': !active,
})}
onClick={handleClick}
>
{value.toUpperCase()}
</button>
);
};
export default HSLToggler;

Wyświetl plik

@ -1,19 +1,21 @@
import React, { useEffect, useState } from 'react';
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 { hueShift } from 'soapbox/utils/theme';
import { hexToHsl, hslShift, hslToHex } from 'soapbox/utils/theme';
import Color from './color';
import HSLToggler from './hsl-toggler';
interface ColorGroup {
[tint: string]: string,
}
interface IPalette {
palette: ColorGroup,
onChange: (palette: ColorGroup) => void,
palette: HslColorPalette,
onChange: (palette: HslColorPalette) => void,
resetKey?: string,
}
@ -21,42 +23,62 @@ interface IPalette {
const Palette: React.FC<IPalette> = ({ palette, onChange, resetKey }) => {
const tints = Object.keys(palette).sort(compareId);
const [hue, setHue] = useState(0);
const lastHue = usePrevious(hue);
const [hslKey, setHslKey] = useState<'h' | 's' | 'l'>('h');
const [slider, setSlider] = useState(0);
const lastSlider = usePrevious(slider);
const skipUpdate = useRef(false);
const handleChange = (tint: string) => {
return (color: string) => {
return (hex: string) => {
onChange({
...palette,
[tint]: color,
[tint]: hexToHsl(hex)!,
});
};
};
useEffect(() => {
const delta = hue - (lastHue || 0);
if (skipUpdate.current) {
skipUpdate.current = false;
return;
}
const adjusted = Object.entries(palette).reduce<ColorGroup>((result, [tint, hex]) => {
result[tint] = hueShift(hex, delta * 360);
const delta = slider - (lastSlider || 0);
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,
});
return result;
}, {});
onChange(adjusted);
}, [hue]);
}, [slider]);
useEffect(() => {
setHue(0);
}, [resetKey]);
skipUpdate.current = true;
if (['s', 'l'].includes(hslKey)) {
setSlider(0.5);
} else {
setSlider(0);
}
}, [resetKey, hslKey]);
return (
<Stack className='w-full'>
<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>
<Slider value={hue} onChange={setHue} />
<HStack space={2}>
<Slider value={slider} onChange={setSlider} />
<HSLToggler value={hslKey} onChange={setHslKey} />
</HStack>
</Stack>
);
};

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 hueShift = (hex: string, delta: number): 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({
h: (h + delta) % 360,
s,
l,
});
};
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),
};
};