Compare commits
11 commits
cf4b6cbda2
...
f1901022b1
Author | SHA1 | Date | |
---|---|---|---|
f1901022b1 | |||
f71fc68cc3 | |||
1800b73359 | |||
25fa4cf7a2 | |||
bec1f8b3a9 | |||
5881c592d7 | |||
b35567574a | |||
8717842a5e | |||
156a0b0826 | |||
a535b80546 | |||
13ec182390 |
19 changed files with 191 additions and 118 deletions
|
@ -17,13 +17,16 @@ const FE_NAME = 'pl_fe';
|
|||
type SettingOpts = {
|
||||
/** Whether to display an alert when settings are saved. */
|
||||
showAlert?: boolean;
|
||||
save?: boolean;
|
||||
}
|
||||
|
||||
const saveSuccessMessage = defineMessage({ id: 'settings.save.success', defaultMessage: 'Your preferences have been saved!' });
|
||||
|
||||
const changeSetting = (path: string[], value: any, opts?: SettingOpts) => {
|
||||
useSettingsStore.getState().changeSetting(path, value);
|
||||
return saveSettings(opts);
|
||||
|
||||
if (opts?.save !== false) return saveSettings(opts);
|
||||
return () => {};
|
||||
};
|
||||
|
||||
const saveSettings = (opts?: SettingOpts) =>
|
||||
|
|
|
@ -223,7 +223,7 @@ const Account = ({
|
|||
<Text theme='muted' size='sm' direction='ltr' truncate>@{username}</Text>
|
||||
|
||||
{account.favicon && (
|
||||
<InstanceFavicon account={account} disabled={!withLinkToProfile} />
|
||||
<InstanceFavicon account={account} disabled />
|
||||
)}
|
||||
|
||||
{items}
|
||||
|
|
|
@ -170,6 +170,7 @@ const SidebarNavigation = () => {
|
|||
account={account}
|
||||
action={<Icon src={require('@tabler/icons/outline/chevron-down.svg')} className='text-gray-600 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-500' />}
|
||||
disabled
|
||||
withLinkToProfile={false}
|
||||
/>
|
||||
</ProfileDropdown>
|
||||
</div>
|
||||
|
|
|
@ -5,27 +5,33 @@ import Popover from 'pl-fe/components/ui/popover';
|
|||
|
||||
interface IColorPicker {
|
||||
value: string;
|
||||
onChange: ColorChangeHandler;
|
||||
onChange?: ColorChangeHandler;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ColorPicker: React.FC<IColorPicker> = ({ value, onChange, className }) => (
|
||||
<div className={className}>
|
||||
<Popover
|
||||
interaction='click'
|
||||
content={
|
||||
<SketchPicker color={value} disableAlpha onChange={onChange} />
|
||||
}
|
||||
isFlush
|
||||
>
|
||||
<div
|
||||
className='size-full'
|
||||
role='presentation'
|
||||
style={{ background: value }}
|
||||
title={value}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ColorPicker: React.FC<IColorPicker> = ({ value, onChange, className }) => {
|
||||
const colorPreview = (
|
||||
<div
|
||||
className='size-full'
|
||||
role='presentation'
|
||||
style={{ background: value }}
|
||||
title={value}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<div className={className}>
|
||||
{onChange ? (
|
||||
<Popover
|
||||
interaction='click'
|
||||
content={
|
||||
<SketchPicker color={value} disableAlpha onChange={onChange} />
|
||||
}
|
||||
isFlush
|
||||
>
|
||||
{colorPreview}
|
||||
</Popover>
|
||||
) : colorPreview}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export { ColorPicker as default };
|
||||
|
|
|
@ -1,22 +1,21 @@
|
|||
import clsx from 'clsx';
|
||||
import React, { useMemo } from 'react';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import * as v from 'valibot';
|
||||
|
||||
import BackgroundShapes from 'pl-fe/features/ui/components/background-shapes';
|
||||
import { useSystemTheme } from 'pl-fe/hooks/use-system-theme';
|
||||
import { plFeConfigSchema } from 'pl-fe/normalizers/pl-fe/pl-fe-config';
|
||||
import { useThemeCss } from 'pl-fe/hooks/use-theme-css';
|
||||
import { useSettingsStore } from 'pl-fe/stores/settings';
|
||||
import { generateThemeCss } from 'pl-fe/utils/theme';
|
||||
|
||||
import type { PlFeConfig } from 'pl-fe/normalizers/pl-fe/pl-fe-config';
|
||||
|
||||
interface ISitePreview {
|
||||
/** Raw pl-fe configuration. */
|
||||
plFe: any;
|
||||
plFe: PlFeConfig;
|
||||
}
|
||||
|
||||
/** Renders a preview of the website's style with the configuration applied. */
|
||||
const SitePreview: React.FC<ISitePreview> = ({ plFe }) => {
|
||||
const plFeConfig = useMemo(() => v.parse(plFeConfigSchema, plFe), [plFe]);
|
||||
const { defaultSettings } = useSettingsStore();
|
||||
|
||||
const userTheme = defaultSettings.themeMode;
|
||||
|
@ -24,6 +23,8 @@ const SitePreview: React.FC<ISitePreview> = ({ plFe }) => {
|
|||
|
||||
const dark = ['dark', 'black'].includes(userTheme as string) || (userTheme === 'system' && systemTheme === 'black');
|
||||
|
||||
const themeCss = useThemeCss(plFe);
|
||||
|
||||
const bodyClass = clsx(
|
||||
'site-preview',
|
||||
'align-center relative flex justify-center text-base',
|
||||
|
@ -36,7 +37,7 @@ const SitePreview: React.FC<ISitePreview> = ({ plFe }) => {
|
|||
|
||||
return (
|
||||
<div className={bodyClass}>
|
||||
<style>{`.site-preview {${generateThemeCss(plFeConfig)}}`}</style>
|
||||
<style>{`.site-preview {${themeCss}}`}</style>
|
||||
<BackgroundShapes position='absolute' />
|
||||
|
||||
<div className='absolute z-[2] self-center overflow-hidden rounded-lg bg-accent-500 p-2 text-white'>
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import debounce from 'lodash/debounce';
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { changeSetting } from 'pl-fe/actions/settings';
|
||||
import { changeSetting, saveSettings } from 'pl-fe/actions/settings';
|
||||
import List, { ListItem } from 'pl-fe/components/list';
|
||||
import Form from 'pl-fe/components/ui/form';
|
||||
import { Mutliselect, SelectDropdown } from 'pl-fe/features/forms';
|
||||
|
@ -9,10 +10,15 @@ import SettingToggle from 'pl-fe/features/notifications/components/setting-toggl
|
|||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||
import { useInstance } from 'pl-fe/hooks/use-instance';
|
||||
import { usePlFeConfig } from 'pl-fe/hooks/use-pl-fe-config';
|
||||
import { useSettings } from 'pl-fe/hooks/use-settings';
|
||||
import colors from 'pl-fe/utils/colors';
|
||||
|
||||
import { PaletteListItem } from '../theme-editor';
|
||||
import ThemeToggle from '../ui/components/theme-toggle';
|
||||
|
||||
import type { AppDispatch } from 'pl-fe/store';
|
||||
|
||||
const languages = {
|
||||
en: 'English',
|
||||
ar: 'العربية',
|
||||
|
@ -91,15 +97,23 @@ const messages = defineMessages({
|
|||
content_type_plaintext: { id: 'preferences.options.content_type_plaintext', defaultMessage: 'Plain text' },
|
||||
content_type_markdown: { id: 'preferences.options.content_type_markdown', defaultMessage: 'Markdown' },
|
||||
content_type_html: { id: 'preferences.options.content_type_html', defaultMessage: 'HTML' },
|
||||
brandColor: { id: 'preferences.options.brand_color', defaultMessage: 'Base color' },
|
||||
});
|
||||
|
||||
const debouncedSave = debounce((dispatch: AppDispatch) => {
|
||||
dispatch(saveSettings({ showAlert: true }));
|
||||
}, 1000);
|
||||
|
||||
const Preferences = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const features = useFeatures();
|
||||
const settings = useSettings();
|
||||
const plFeConfig = usePlFeConfig();
|
||||
const instance = useInstance();
|
||||
|
||||
const brandColor = settings.theme?.brandColor || plFeConfig.brandColor || '#d80482';
|
||||
|
||||
const onSelectChange = (event: React.ChangeEvent<HTMLSelectElement>, path: string[]) => {
|
||||
dispatch(changeSetting(path, event.target.value, { showAlert: true }));
|
||||
};
|
||||
|
@ -109,7 +123,14 @@ const Preferences = () => {
|
|||
};
|
||||
|
||||
const onToggleChange = (key: string[], checked: boolean) => {
|
||||
dispatch(changeSetting(key, checked, { showAlert: true }));
|
||||
dispatch(changeSetting(key, checked));
|
||||
};
|
||||
|
||||
const onBrandColorChange = (newBrandColor: string) => {
|
||||
if (!settings.theme?.brandColor && newBrandColor === brandColor) return;
|
||||
|
||||
dispatch(changeSetting(['theme', 'brandColor'], newBrandColor, { showAlert: true, save: false }));
|
||||
debouncedSave(dispatch);
|
||||
};
|
||||
|
||||
const displayMediaOptions = React.useMemo(() => ({
|
||||
|
@ -152,7 +173,15 @@ const Preferences = () => {
|
|||
<ListItem label={<FormattedMessage id='preferences.fields.theme' defaultMessage='Theme' />}>
|
||||
<ThemeToggle />
|
||||
</ListItem>
|
||||
<PaletteListItem
|
||||
label={intl.formatMessage(messages.brandColor)}
|
||||
palette={colors(brandColor)}
|
||||
onChange={(palette) => onBrandColorChange(palette['500'])}
|
||||
allowTintChange={false}
|
||||
/>
|
||||
</List>
|
||||
|
||||
<List>
|
||||
<ListItem label={<FormattedMessage id='preferences.fields.language_label' defaultMessage='Display language' />}>
|
||||
<SelectDropdown
|
||||
className='max-w-[200px]'
|
||||
|
|
|
@ -6,21 +6,21 @@ import type { ColorChangeHandler } from 'react-color';
|
|||
|
||||
interface IColor {
|
||||
color: string;
|
||||
onChange: (color: string) => void;
|
||||
onChange?: (color: string) => void;
|
||||
}
|
||||
|
||||
/** Color input. */
|
||||
const Color: React.FC<IColor> = ({ color, onChange }) => {
|
||||
|
||||
const handleChange: ColorChangeHandler = (result) => {
|
||||
onChange(result.hex);
|
||||
onChange?.(result.hex);
|
||||
};
|
||||
|
||||
return (
|
||||
<ColorPicker
|
||||
className='size-full'
|
||||
value={color}
|
||||
onChange={handleChange}
|
||||
onChange={onChange ? handleChange : undefined}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -17,10 +17,11 @@ interface IPalette {
|
|||
palette: ColorGroup;
|
||||
onChange: (palette: ColorGroup) => void;
|
||||
resetKey?: string;
|
||||
allowTintChange?: boolean;
|
||||
}
|
||||
|
||||
/** Editable color palette. */
|
||||
const Palette: React.FC<IPalette> = ({ palette, onChange, resetKey }) => {
|
||||
const Palette: React.FC<IPalette> = ({ palette, onChange, resetKey, allowTintChange = true }) => {
|
||||
const tints = Object.keys(palette).sort(compareId);
|
||||
|
||||
const [hue, setHue] = useState(0);
|
||||
|
@ -52,7 +53,7 @@ const Palette: React.FC<IPalette> = ({ palette, onChange, resetKey }) => {
|
|||
<Stack className='w-full'>
|
||||
<HStack className='h-8 overflow-hidden rounded-md'>
|
||||
{tints.map(tint => (
|
||||
<Color color={palette[tint]} onChange={handleChange(tint)} />
|
||||
<Color color={palette[tint]} onChange={allowTintChange ? handleChange(tint) : undefined} />
|
||||
))}
|
||||
</HStack>
|
||||
|
||||
|
|
|
@ -249,12 +249,13 @@ interface IPaletteListItem {
|
|||
palette: ColorGroup | string;
|
||||
onChange: (palette: ColorGroup) => void;
|
||||
resetKey?: string;
|
||||
allowTintChange?: boolean;
|
||||
}
|
||||
|
||||
/** Palette editor inside a ListItem. */
|
||||
const PaletteListItem: React.FC<IPaletteListItem> = ({ label, palette, onChange, resetKey }) => typeof palette === 'string' ? null : (
|
||||
const PaletteListItem: React.FC<IPaletteListItem> = ({ label, palette, onChange, resetKey, allowTintChange }) => typeof palette === 'string' ? null : (
|
||||
<ListItem label={<div className='w-20'>{label}</div>}>
|
||||
<Palette palette={palette} onChange={onChange} resetKey={resetKey} />
|
||||
<Palette palette={palette} onChange={onChange} resetKey={resetKey} allowTintChange={allowTintChange} />
|
||||
</ListItem>
|
||||
);
|
||||
|
||||
|
@ -283,4 +284,4 @@ const ColorListItem: React.FC<IColorListItem> = ({ label, value, onChange }) =>
|
|||
);
|
||||
};
|
||||
|
||||
export { ThemeEditor as default };
|
||||
export { ThemeEditor as default, PaletteListItem };
|
||||
|
|
|
@ -109,9 +109,9 @@ const SignUpPanel = () => {
|
|||
<>
|
||||
<Text size='lg' weight='bold'>
|
||||
{isOpen ? (
|
||||
<FormattedMessage id='signup_panel.sign_in.title' defaultMessage='Sign in' />
|
||||
) : (
|
||||
<FormattedMessage id='signup_panel.sign_in.title.or' defaultMessage='Already have an account?' />
|
||||
) : (
|
||||
<FormattedMessage id='signup_panel.sign_in.title' defaultMessage='Sign in' />
|
||||
)}
|
||||
</Text>
|
||||
|
||||
|
|
85
packages/pl-fe/src/hooks/use-theme-css.ts
Normal file
85
packages/pl-fe/src/hooks/use-theme-css.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
import { useMemo } from 'react';
|
||||
|
||||
import { toTailwind } from 'pl-fe/utils/tailwind';
|
||||
import { generateAccent, generateThemeCss } from 'pl-fe/utils/theme';
|
||||
|
||||
import { usePlFeConfig } from './use-pl-fe-config';
|
||||
import { useSettings } from './use-settings';
|
||||
|
||||
import type { PlFeConfig } from 'pl-fe/normalizers/pl-fe/pl-fe-config';
|
||||
|
||||
const DEFAULT_COLORS = {
|
||||
success: {
|
||||
50: '#f0fdf4',
|
||||
100: '#dcfce7',
|
||||
200: '#bbf7d0',
|
||||
300: '#86efac',
|
||||
400: '#4ade80',
|
||||
500: '#22c55e',
|
||||
600: '#16a34a',
|
||||
700: '#15803d',
|
||||
800: '#166534',
|
||||
900: '#14532d',
|
||||
},
|
||||
danger: {
|
||||
50: '#fef2f2',
|
||||
100: '#fee2e2',
|
||||
200: '#fecaca',
|
||||
300: '#fca5a5',
|
||||
400: '#f87171',
|
||||
500: '#ef4444',
|
||||
600: '#dc2626',
|
||||
700: '#b91c1c',
|
||||
800: '#991b1b',
|
||||
900: '#7f1d1d',
|
||||
},
|
||||
'greentext': '#789922',
|
||||
};
|
||||
|
||||
const normalizeColors = (theme: Partial<Pick<PlFeConfig, 'brandColor' | 'accentColor' | 'colors'>>) => {
|
||||
const brandColor: string = theme.brandColor || theme.colors?.primary?.['500'] || '#d80482';
|
||||
const accentColor: string = theme.accentColor || theme.colors?.accent?.['500'] || '' || generateAccent(brandColor);
|
||||
|
||||
const colors = {
|
||||
...theme.colors,
|
||||
...Object.fromEntries(Object.entries(DEFAULT_COLORS).map(([key, value]) => [key, typeof value === 'string' ? value : { ...value, ...theme.colors?.[key] }])),
|
||||
};
|
||||
|
||||
const normalizedColors = toTailwind({
|
||||
brandColor,
|
||||
accentColor,
|
||||
colors,
|
||||
});
|
||||
|
||||
return {
|
||||
// @ts-ignore
|
||||
'gradient-start': normalizedColors.primary?.['500'],
|
||||
// @ts-ignore
|
||||
'gradient-end': normalizedColors.accent?.['500'],
|
||||
// @ts-ignore
|
||||
'accent-blue': normalizedColors.primary?.['600'],
|
||||
...normalizedColors,
|
||||
} as typeof normalizedColors;
|
||||
};
|
||||
|
||||
const useThemeCss = (overwriteConfig?: PlFeConfig) => {
|
||||
const { demo, theme } = useSettings();
|
||||
const plFeConfig = usePlFeConfig();
|
||||
|
||||
return useMemo(() => {
|
||||
try {
|
||||
let baseTheme: Partial<PlFeConfig>;
|
||||
if (overwriteConfig) baseTheme = overwriteConfig;
|
||||
else if (demo) baseTheme = {};
|
||||
else baseTheme = theme || plFeConfig;
|
||||
|
||||
const colors = normalizeColors(baseTheme);
|
||||
|
||||
return generateThemeCss(colors);
|
||||
} catch (_) {
|
||||
return generateThemeCss({});
|
||||
}
|
||||
}, [demo, plFeConfig, theme]);
|
||||
};
|
||||
|
||||
export { useThemeCss };
|
|
@ -1,15 +1,13 @@
|
|||
import clsx from 'clsx';
|
||||
import React, { useEffect } from 'react';
|
||||
import * as v from 'valibot';
|
||||
|
||||
import { useLocale, useLocaleDirection } from 'pl-fe/hooks/use-locale';
|
||||
import { usePlFeConfig } from 'pl-fe/hooks/use-pl-fe-config';
|
||||
import { useSettings } from 'pl-fe/hooks/use-settings';
|
||||
import { useTheme } from 'pl-fe/hooks/use-theme';
|
||||
import { plFeConfigSchema } from 'pl-fe/normalizers/pl-fe/pl-fe-config';
|
||||
import { useThemeCss } from 'pl-fe/hooks/use-theme-css';
|
||||
import { startSentry } from 'pl-fe/sentry';
|
||||
import { useModalsStore } from 'pl-fe/stores/modals';
|
||||
import { generateThemeCss } from 'pl-fe/utils/theme';
|
||||
|
||||
const Helmet = React.lazy(() => import('pl-fe/components/helmet'));
|
||||
|
||||
|
@ -17,13 +15,13 @@ const Helmet = React.lazy(() => import('pl-fe/components/helmet'));
|
|||
const PlFeHead = () => {
|
||||
const locale = useLocale();
|
||||
const direction = useLocaleDirection(locale);
|
||||
const { demo, reduceMotion, underlineLinks, demetricator, systemFont } = useSettings();
|
||||
const { reduceMotion, underlineLinks, demetricator, systemFont } = useSettings();
|
||||
const plFeConfig = usePlFeConfig();
|
||||
const theme = useTheme();
|
||||
|
||||
const withModals = useModalsStore().modals.length > 0;
|
||||
|
||||
const themeCss = generateThemeCss(demo ? v.parse(plFeConfigSchema, { brandColor: '#d80482' }) : plFeConfig);
|
||||
const themeCss = useThemeCss();
|
||||
const dsn = plFeConfig.sentryDsn;
|
||||
|
||||
const bodyClass = clsx('h-full bg-white text-base antialiased black:bg-black dark:bg-gray-800', {
|
||||
|
|
|
@ -1269,6 +1269,7 @@
|
|||
"preferences.fields.wrench_label": "Display wrench reaction button",
|
||||
"preferences.hints.demetricator": "Decrease social media anxiety by hiding all numbers from the site.",
|
||||
"preferences.notifications.advanced": "Show all notification categories",
|
||||
"preferences.options.brand_color": "Base color",
|
||||
"preferences.options.content_type_html": "HTML",
|
||||
"preferences.options.content_type_markdown": "Markdown",
|
||||
"preferences.options.content_type_mfm": "MFM",
|
||||
|
|
|
@ -2,36 +2,6 @@ import trimStart from 'lodash/trimStart';
|
|||
import * as v from 'valibot';
|
||||
|
||||
import { coerceObject, filteredArray } from 'pl-fe/schemas/utils';
|
||||
import { toTailwind } from 'pl-fe/utils/tailwind';
|
||||
import { generateAccent } from 'pl-fe/utils/theme';
|
||||
|
||||
const DEFAULT_COLORS = {
|
||||
success: {
|
||||
50: '#f0fdf4',
|
||||
100: '#dcfce7',
|
||||
200: '#bbf7d0',
|
||||
300: '#86efac',
|
||||
400: '#4ade80',
|
||||
500: '#22c55e',
|
||||
600: '#16a34a',
|
||||
700: '#15803d',
|
||||
800: '#166534',
|
||||
900: '#14532d',
|
||||
},
|
||||
danger: {
|
||||
50: '#fef2f2',
|
||||
100: '#fee2e2',
|
||||
200: '#fecaca',
|
||||
300: '#fca5a5',
|
||||
400: '#f87171',
|
||||
500: '#ef4444',
|
||||
600: '#dc2626',
|
||||
700: '#b91c1c',
|
||||
800: '#991b1b',
|
||||
900: '#7f1d1d',
|
||||
},
|
||||
'greentext': '#789922',
|
||||
};
|
||||
|
||||
const promoPanelItemSchema = coerceObject({
|
||||
icon: v.fallback(v.string(), ''),
|
||||
|
@ -67,7 +37,7 @@ const cryptoAddressSchema = v.pipe(coerceObject({
|
|||
|
||||
type CryptoAddress = v.InferOutput<typeof cryptoAddressSchema>;
|
||||
|
||||
const plFeConfigSchema = v.pipe(coerceObject({
|
||||
const plFeConfigSchema = coerceObject({
|
||||
appleAppId: v.fallback(v.nullable(v.string()), null),
|
||||
logo: v.fallback(v.string(), ''),
|
||||
logoDarkMode: v.fallback(v.nullable(v.string()), null),
|
||||
|
@ -122,36 +92,7 @@ const plFeConfigSchema = v.pipe(coerceObject({
|
|||
*/
|
||||
mediaPreview: v.fallback(v.boolean(), false),
|
||||
sentryDsn: v.fallback(v.optional(v.string()), undefined),
|
||||
}), v.transform((config) => {
|
||||
const brandColor: string = config.brandColor || config.colors?.primary?.['500'] || '';
|
||||
const accentColor: string = config.accentColor || config.colors?.accent?.['500'] || '' || generateAccent(brandColor);
|
||||
|
||||
const colors = {
|
||||
...config.colors,
|
||||
...Object.fromEntries(Object.entries(DEFAULT_COLORS).map(([key, value]) => [key, typeof value === 'string' ? value : { ...value, ...config.colors?.[key] }])),
|
||||
};
|
||||
|
||||
const normalizedColors = toTailwind({
|
||||
brandColor,
|
||||
accentColor,
|
||||
colors,
|
||||
});
|
||||
|
||||
return {
|
||||
...config,
|
||||
brandColor,
|
||||
accentColor,
|
||||
colors: {
|
||||
// @ts-ignore
|
||||
'gradient-start': normalizedColors.primary?.['500'],
|
||||
// @ts-ignore
|
||||
'gradient-end': normalizedColors.accent?.['500'],
|
||||
// @ts-ignore
|
||||
'accent-blue': normalizedColors.primary?.['600'],
|
||||
...normalizedColors,
|
||||
} as typeof normalizedColors,
|
||||
};
|
||||
}));
|
||||
});
|
||||
|
||||
type PlFeConfig = v.InferOutput<typeof plFeConfigSchema>;
|
||||
|
||||
|
|
|
@ -38,6 +38,12 @@ const settingsSchema = v.object({
|
|||
knownLanguages: v.fallback(v.array(v.string()), []),
|
||||
showWrenchButton: v.fallback(v.boolean(), false),
|
||||
|
||||
theme: v.fallback(v.optional(v.object({
|
||||
brandColor: v.fallback(v.string(), ''),
|
||||
accentColor: v.fallback(v.string(), ''),
|
||||
colors: v.any(),
|
||||
})), undefined),
|
||||
|
||||
systemFont: v.fallback(v.boolean(), false),
|
||||
demetricator: v.fallback(v.boolean(), false),
|
||||
|
||||
|
|
|
@ -62,6 +62,7 @@ const useSettingsStore = create<State>()(mutative((set) => ({
|
|||
}),
|
||||
|
||||
changeSetting: (path: string[], value: any) => set((state: State) => {
|
||||
state.userSettings.saved = false;
|
||||
changeSetting(state.userSettings, path, value);
|
||||
|
||||
mergeSettings(state);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import { toTailwind, fromLegacyColors, expandPalette } from './tailwind';
|
||||
import { toTailwind, fromBasicColors, expandPalette } from './tailwind';
|
||||
|
||||
describe('toTailwind()', () => {
|
||||
it('handles empty pl-fe config', () => {
|
||||
|
@ -68,7 +68,7 @@ describe('toTailwind()', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('fromLegacyColors()', () => {
|
||||
describe('fromBasicColors()', () => {
|
||||
it('converts only brandColor', () => {
|
||||
const plFeConfig = ImmutableMap({ brandColor: '#0482d8' });
|
||||
|
||||
|
@ -124,7 +124,7 @@ describe('fromLegacyColors()', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = fromLegacyColors(plFeConfig);
|
||||
const result = fromBasicColors(plFeConfig);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
|
@ -185,7 +185,7 @@ describe('fromLegacyColors()', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const result = fromLegacyColors(plFeConfig);
|
||||
const result = fromBasicColors(plFeConfig);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -29,7 +29,7 @@ const maybeGenerateAccentColor = (brandColor: string): string | null =>
|
|||
isHex(brandColor) ? generateAccent(brandColor) : null;
|
||||
|
||||
/** Build a color object from legacy colors */
|
||||
const fromLegacyColors = ({ brandColor, accentColor }: {
|
||||
const fromBasicColors = ({ brandColor, accentColor }: {
|
||||
brandColor: string;
|
||||
accentColor: string | null;
|
||||
}): TailwindColorPalette => {
|
||||
|
@ -50,16 +50,16 @@ const toTailwind = (config: {
|
|||
colors: Record<string, Record<string, string>>;
|
||||
}): Record<string, Record<string, string> | string> => {
|
||||
const colors: PlFeColors = config.colors;
|
||||
const legacyColors = fromLegacyColors(config);
|
||||
const basicColors = fromBasicColors(config);
|
||||
|
||||
return {
|
||||
...colors,
|
||||
...Object.fromEntries(Object.entries(legacyColors).map(([key, value]) => [key, typeof value === 'string' ? colors[key] || value : { ...value, ...colors[key] }])),
|
||||
...Object.fromEntries(Object.entries(basicColors).map(([key, value]) => [key, typeof value === 'string' ? colors[key] || value : { ...value, ...colors[key] }])),
|
||||
};
|
||||
};
|
||||
|
||||
export {
|
||||
expandPalette,
|
||||
fromLegacyColors,
|
||||
fromBasicColors,
|
||||
toTailwind,
|
||||
};
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { hexToRgb } from './colors';
|
||||
|
||||
import type { PlFeConfig } from 'pl-fe/normalizers/pl-fe/pl-fe-config';
|
||||
import type { Rgb, Hsl, TailwindColorPalette, TailwindColorObject } from 'pl-fe/types/colors';
|
||||
|
||||
// Taken from chromatism.js
|
||||
|
@ -110,8 +109,8 @@ const colorsToCss = (colors: TailwindColorPalette): string => {
|
|||
return Object.keys(parsed).reduce((css, variable) => css + `${variable}:${parsed[variable]};`, '');
|
||||
};
|
||||
|
||||
const generateThemeCss = (plFeConfig: PlFeConfig): string =>
|
||||
colorsToCss(plFeConfig.colors);
|
||||
const generateThemeCss = (colors: TailwindColorPalette): string =>
|
||||
colorsToCss(colors);
|
||||
|
||||
const hexToHsl = (hex: string): Hsl | null => {
|
||||
const rgb = hexToRgb(hex);
|
||||
|
|
Loading…
Reference in a new issue