Compare commits

...

12 commits

Author SHA1 Message Date
f1901022b1 Merge remote-tracking branch 'pch/develop' into frontend-rw
Some checks are pending
pl-api CI / Test for a successful build (push) Waiting to run
pl-fe CI / Test and upload artifacts (push) Waiting to run
pl-fe CI / deploy (push) Blocked by required conditions
pl-hooks CI / Test for a successful build (push) Waiting to run
2024-12-06 13:44:33 +01:00
f71fc68cc3 Merge branch 'develop' into frontend-rw 2024-12-06 13:44:19 +01:00
1800b73359 pl-fe: fix sign up panel label logic
Signed-off-by: mkljczk <git@mkljczk.pl>
2024-12-06 13:44:06 +01:00
25fa4cf7a2 Merge branch 'develop' into frontend-rw 2024-12-06 13:31:32 +01:00
bec1f8b3a9 pl-fe: only update if changed(?)
Signed-off-by: mkljczk <git@mkljczk.pl>
2024-12-06 11:32:40 +01:00
5881c592d7 pl-fe: fix instance favicon in profile dropdown
Signed-off-by: mkljczk <git@mkljczk.pl>
2024-12-06 11:25:06 +01:00
b35567574a pl-fe: lint
Signed-off-by: mkljczk <git@mkljczk.pl>
2024-12-06 11:22:33 +01:00
8717842a5e pl-fe: do not pretend users can edit individual tints
Signed-off-by: mkljczk <git@mkljczk.pl>
2024-12-06 11:16:59 +01:00
156a0b0826 pl-fe: allow user to change ui color
Signed-off-by: mkljczk <git@mkljczk.pl>
2024-12-06 11:12:02 +01:00
a535b80546 pl-fe: use theme css hook
Signed-off-by: mkljczk <git@mkljczk.pl>
2024-12-06 10:43:11 +01:00
13ec182390 pl-fe: rename legacy colors to basic colors
Signed-off-by: mkljczk <git@mkljczk.pl>
2024-12-06 10:27:52 +01:00
cf4b6cbda2 Merge pull request 'frontend-rw' (#1) from frontend-rw into develop
Some checks are pending
pl-api CI / Test for a successful build (push) Waiting to run
pl-fe CI / Test and upload artifacts (push) Waiting to run
pl-fe CI / deploy (push) Blocked by required conditions
pl-hooks CI / Test for a successful build (push) Waiting to run
Reviewed-on: #1
2024-12-05 15:32:17 -08:00
19 changed files with 191 additions and 118 deletions

View file

@ -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) =>

View file

@ -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}

View file

@ -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>

View file

@ -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 };

View file

@ -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'>

View file

@ -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]'

View file

@ -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}
/>
);
};

View file

@ -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>

View file

@ -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 };

View file

@ -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>

View 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 };

View file

@ -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', {

View file

@ -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",

View file

@ -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>;

View file

@ -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),

View file

@ -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);

View file

@ -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);
});
});

View file

@ -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,
};

View file

@ -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);