8
common/resources/client/admin/appearance/sections/themes/color-icon.tsx
Executable file
8
common/resources/client/admin/appearance/sections/themes/color-icon.tsx
Executable file
@@ -0,0 +1,8 @@
|
||||
import {createSvgIcon} from '../../../../icons/create-svg-icon';
|
||||
|
||||
export const ColorIcon = createSvgIcon(
|
||||
<path
|
||||
stroke="#E0E0E0"
|
||||
d="M24,44c-7.168,0-13-5.816-13-12.971C11,24,24,4,24,4s13,20,13,27.029C37,38.184,31.168,44,24,44z"
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,81 @@
|
||||
import {message} from '@common/i18n/message';
|
||||
import {useParams} from 'react-router-dom';
|
||||
import {useFormContext} from 'react-hook-form';
|
||||
import {AppearanceValues} from '@common/admin/appearance/appearance-store';
|
||||
import {Menu, MenuTrigger} from '@common/ui/navigation/menu/menu-trigger';
|
||||
import {AppearanceButton} from '@common/admin/appearance/appearance-button';
|
||||
import {ColorIcon} from '@common/admin/appearance/sections/themes/color-icon';
|
||||
import clsx from 'clsx';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import {Item} from '@common/ui/forms/listbox/item';
|
||||
|
||||
const navbarColorMap = [
|
||||
{
|
||||
label: message('Accent'),
|
||||
value: 'primary',
|
||||
bgColor: 'bg-primary',
|
||||
previewBgColor: 'text-primary',
|
||||
},
|
||||
{
|
||||
label: message('Background'),
|
||||
value: 'bg',
|
||||
bgColor: 'bg-background',
|
||||
previewBgColor: 'text-background',
|
||||
},
|
||||
{
|
||||
label: message('Background alt'),
|
||||
value: 'bg-alt',
|
||||
bgColor: 'bg-alt',
|
||||
previewBgColor: 'text-background-alt',
|
||||
},
|
||||
{
|
||||
label: message('Transparent'),
|
||||
value: 'transparent',
|
||||
bgColor: 'bg-transparent',
|
||||
previewBgColor: 'text-transparent',
|
||||
},
|
||||
];
|
||||
|
||||
export function NavbarColorPicker() {
|
||||
const {themeIndex} = useParams();
|
||||
const {watch, setValue} = useFormContext<AppearanceValues>();
|
||||
const key =
|
||||
`appearance.themes.all.${themeIndex!}.values.--be-navbar-color` as 'appearance.themes.all.1.values.--be-navbar-color';
|
||||
const selectedValue = watch(key);
|
||||
const previewColor = navbarColorMap.find(({value}) => value === selectedValue)
|
||||
?.previewBgColor;
|
||||
return (
|
||||
<MenuTrigger
|
||||
placement="right"
|
||||
selectionMode="single"
|
||||
selectedValue={selectedValue}
|
||||
onSelectionChange={value => {
|
||||
setValue(key, value as string, {shouldDirty: true});
|
||||
}}
|
||||
>
|
||||
<AppearanceButton
|
||||
startIcon={
|
||||
<ColorIcon
|
||||
viewBox="0 0 48 48"
|
||||
className={clsx('icon-lg', previewColor)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Trans message="Navbar" />
|
||||
</AppearanceButton>
|
||||
<Menu>
|
||||
{navbarColorMap.map(({label, value, bgColor}) => (
|
||||
<Item
|
||||
key={value}
|
||||
value={value}
|
||||
startIcon={
|
||||
<div className={clsx('h-20 w-20 rounded border', bgColor)} />
|
||||
}
|
||||
>
|
||||
<Trans {...label} />
|
||||
</Item>
|
||||
))}
|
||||
</Menu>
|
||||
</MenuTrigger>
|
||||
);
|
||||
}
|
||||
184
common/resources/client/admin/appearance/sections/themes/theme-editor.tsx
Executable file
184
common/resources/client/admin/appearance/sections/themes/theme-editor.tsx
Executable file
@@ -0,0 +1,184 @@
|
||||
import {Link, useNavigate, useParams} from 'react-router-dom';
|
||||
import {Fragment, ReactNode, useEffect, useState} from 'react';
|
||||
import {
|
||||
appearanceState,
|
||||
AppearanceValues,
|
||||
} from '@common/admin/appearance/appearance-store';
|
||||
import {AppearanceButton} from '@common/admin/appearance/appearance-button';
|
||||
import {ColorIcon} from '@common/admin/appearance/sections/themes/color-icon';
|
||||
import {CssTheme} from '@common/ui/themes/css-theme';
|
||||
import {colorToThemeValue} from '@common/ui/themes/utils/color-to-theme-value';
|
||||
import {ThemeSettingsDialogTrigger} from '@common/admin/appearance/sections/themes/theme-settings-dialog-trigger';
|
||||
import {ThemeMoreOptionsButton} from '@common/admin/appearance/sections/themes/theme-more-options-button';
|
||||
import {ColorPickerDialog} from '@common/ui/color-picker/color-picker-dialog';
|
||||
import {DialogTrigger} from '@common/ui/overlays/dialog/dialog-trigger';
|
||||
import {useFormContext} from 'react-hook-form';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import {NavbarColorPicker} from '@common/admin/appearance/sections/themes/navbar-color-picker';
|
||||
import {message} from '@common/i18n/message';
|
||||
import {themeValueToHex} from '@common/ui/themes/utils/theme-value-to-hex';
|
||||
|
||||
const colorList = [
|
||||
{
|
||||
label: message('Background'),
|
||||
key: '--be-background',
|
||||
},
|
||||
{
|
||||
label: message('Background alt'),
|
||||
key: '--be-background-alt',
|
||||
},
|
||||
{
|
||||
label: message('Foreground'),
|
||||
key: '--be-foreground-base',
|
||||
},
|
||||
{
|
||||
label: message('Accent light'),
|
||||
key: '--be-primary-light',
|
||||
},
|
||||
{
|
||||
label: message('Accent'),
|
||||
key: '--be-primary',
|
||||
},
|
||||
{
|
||||
label: message('Accent dark'),
|
||||
key: '--be-primary-dark',
|
||||
},
|
||||
{
|
||||
label: message('Text on accent'),
|
||||
key: '--be-on-primary',
|
||||
},
|
||||
{
|
||||
label: message('Chip'),
|
||||
key: '--be-background-chip',
|
||||
},
|
||||
];
|
||||
|
||||
export function ThemeEditor() {
|
||||
const navigate = useNavigate();
|
||||
const {themeIndex} = useParams();
|
||||
const {getValues, watch} = useFormContext<AppearanceValues>();
|
||||
|
||||
const theme = getValues(`appearance.themes.all.${+themeIndex!}`);
|
||||
const selectedFont = watch(
|
||||
`appearance.themes.all.${+themeIndex!}.font.family`,
|
||||
);
|
||||
|
||||
// go to theme list, if theme can't be found
|
||||
useEffect(() => {
|
||||
if (!theme) {
|
||||
navigate('/admin/appearance/themes');
|
||||
}
|
||||
}, [navigate, theme]);
|
||||
|
||||
// select theme in preview on initial render
|
||||
useEffect(() => {
|
||||
if (theme?.id) {
|
||||
appearanceState().preview.setActiveTheme(theme.id);
|
||||
}
|
||||
}, [theme?.id]);
|
||||
|
||||
if (!theme) return null;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="mb-20 flex items-center justify-between gap-10">
|
||||
<ThemeSettingsDialogTrigger />
|
||||
<ThemeMoreOptionsButton />
|
||||
</div>
|
||||
<div>
|
||||
<AppearanceButton
|
||||
elementType={Link}
|
||||
to="font"
|
||||
description={selectedFont ? selectedFont : <Trans message="System" />}
|
||||
>
|
||||
<Trans message="Font" />
|
||||
</AppearanceButton>
|
||||
<AppearanceButton elementType={Link} to="radius">
|
||||
<Trans message="Rounding" />
|
||||
</AppearanceButton>
|
||||
<div className="mb-6 mt-22 text-sm font-semibold">
|
||||
<Trans message="Colors" />
|
||||
</div>
|
||||
<NavbarColorPicker />
|
||||
{colorList.map(color => (
|
||||
<ColorPickerTrigger
|
||||
key={color.key}
|
||||
colorName={color.key}
|
||||
label={<Trans {...color.label} />}
|
||||
initialThemeValue={theme.values[color.key]}
|
||||
theme={theme}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
interface ColorPickerTriggerProps {
|
||||
label: ReactNode;
|
||||
theme: CssTheme;
|
||||
colorName: string;
|
||||
initialThemeValue: string;
|
||||
}
|
||||
function ColorPickerTrigger({
|
||||
label,
|
||||
theme,
|
||||
colorName,
|
||||
initialThemeValue,
|
||||
}: ColorPickerTriggerProps) {
|
||||
const {setValue} = useFormContext<AppearanceValues>();
|
||||
const {themeIndex} = useParams();
|
||||
const [selectedThemeValue, setSelectedThemeValue] =
|
||||
useState<string>(initialThemeValue);
|
||||
|
||||
// set color as css variable in preview and on button preview, but not in appearance values
|
||||
// this way color change can be canceled when color picker is closed and applied explicitly via apply button
|
||||
const selectThemeValue = (themeValue: string) => {
|
||||
setSelectedThemeValue(themeValue);
|
||||
appearanceState().preview.setThemeValue(colorName, themeValue);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// need to update the color here so changes via "reset colors" button are reflected
|
||||
setSelectedThemeValue(initialThemeValue);
|
||||
}, [initialThemeValue]);
|
||||
|
||||
return (
|
||||
<DialogTrigger
|
||||
value={themeValueToHex(selectedThemeValue)}
|
||||
type="popover"
|
||||
placement="right"
|
||||
offset={10}
|
||||
onValueChange={newColor => {
|
||||
selectThemeValue(colorToThemeValue(newColor));
|
||||
}}
|
||||
onClose={(newColor, {valueChanged, initialValue}) => {
|
||||
if (newColor && valueChanged) {
|
||||
setValue(
|
||||
`appearance.themes.all.${+themeIndex!}.values.${colorName}`,
|
||||
colorToThemeValue(newColor),
|
||||
{shouldDirty: true},
|
||||
);
|
||||
setValue('appearance.themes.selectedThemeId', theme.id);
|
||||
} else {
|
||||
// reset to initial value, if apply button was not clicked
|
||||
selectThemeValue(initialValue);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AppearanceButton
|
||||
className="capitalize"
|
||||
startIcon={
|
||||
<ColorIcon
|
||||
viewBox="0 0 48 48"
|
||||
className="icon-lg"
|
||||
style={{fill: `rgb(${selectedThemeValue})`}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</AppearanceButton>
|
||||
<ColorPickerDialog />
|
||||
</DialogTrigger>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import {FontSelector} from '@common/ui/font-selector/font-selector';
|
||||
import {useFormContext} from 'react-hook-form';
|
||||
import {
|
||||
appearanceState,
|
||||
AppearanceValues,
|
||||
} from '@common/admin/appearance/appearance-store';
|
||||
import {useParams} from 'react-router-dom';
|
||||
|
||||
type Font = 'appearance.themes.all.1.font';
|
||||
|
||||
export function ThemeFontPanel() {
|
||||
const {setValue, watch} = useFormContext<AppearanceValues>();
|
||||
const {themeIndex} = useParams();
|
||||
const key = `appearance.themes.all.${themeIndex}.font` as Font;
|
||||
return (
|
||||
<FontSelector
|
||||
value={watch(key)}
|
||||
onChange={font => {
|
||||
setValue(key, font, {shouldDirty: true});
|
||||
appearanceState().preview.setThemeFont(font);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
64
common/resources/client/admin/appearance/sections/themes/theme-list.tsx
Executable file
64
common/resources/client/admin/appearance/sections/themes/theme-list.tsx
Executable file
@@ -0,0 +1,64 @@
|
||||
import {NavLink, useNavigate} from 'react-router-dom';
|
||||
import {Fragment, useEffect} from 'react';
|
||||
import {appearanceState, AppearanceValues} from '../../appearance-store';
|
||||
import {AppearanceButton} from '../../appearance-button';
|
||||
import {Button} from '../../../../ui/buttons/button';
|
||||
import {AddIcon} from '../../../../icons/material/Add';
|
||||
import {randomNumber} from '../../../../utils/string/random-number';
|
||||
import {Trans} from '../../../../i18n/trans';
|
||||
import {useFieldArray} from 'react-hook-form';
|
||||
import {useTrans} from '../../../../i18n/use-trans';
|
||||
import {message} from '../../../../i18n/message';
|
||||
import {useBootstrapData} from '../../../../core/bootstrap-data/bootstrap-data-context';
|
||||
|
||||
export function ThemeList() {
|
||||
const {trans} = useTrans();
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
data: {themes},
|
||||
} = useBootstrapData();
|
||||
const {fields, append} = useFieldArray<
|
||||
AppearanceValues,
|
||||
'appearance.themes.all',
|
||||
'key'
|
||||
>({
|
||||
name: 'appearance.themes.all',
|
||||
keyName: 'key',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (themes.selectedThemeId) {
|
||||
appearanceState().preview.setActiveTheme(themes.selectedThemeId);
|
||||
}
|
||||
}, [themes.selectedThemeId]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="mb-20">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="outline"
|
||||
color="primary"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => {
|
||||
const lightThemeColors =
|
||||
appearanceState().defaults?.appearance.themes.light!;
|
||||
append({
|
||||
id: randomNumber(),
|
||||
name: trans(message('New theme')),
|
||||
values: lightThemeColors,
|
||||
});
|
||||
navigate(`${fields.length + 1}`);
|
||||
}}
|
||||
>
|
||||
<Trans message="New theme" />
|
||||
</Button>
|
||||
</div>
|
||||
{fields.map((field, index) => (
|
||||
<AppearanceButton key={field.key} to={`${index}`} elementType={NavLink}>
|
||||
{field.name}
|
||||
</AppearanceButton>
|
||||
))}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import {Fragment, useState} from 'react';
|
||||
import {DeleteIcon} from '../../../../icons/material/Delete';
|
||||
import {ConfirmationDialog} from '../../../../ui/overlays/dialog/confirmation-dialog';
|
||||
import {IconButton} from '../../../../ui/buttons/icon-button';
|
||||
import {MoreVertIcon} from '../../../../icons/material/MoreVert';
|
||||
import {RestartAltIcon} from '../../../../icons/material/RestartAlt';
|
||||
import {appearanceState, AppearanceValues} from '../../appearance-store';
|
||||
import {toast} from '../../../../ui/toast/toast';
|
||||
import {
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuTrigger,
|
||||
} from '../../../../ui/navigation/menu/menu-trigger';
|
||||
import {DialogTrigger} from '../../../../ui/overlays/dialog/dialog-trigger';
|
||||
import {message} from '../../../../i18n/message';
|
||||
import {Trans} from '../../../../i18n/trans';
|
||||
import {useNavigate} from '../../../../utils/hooks/use-navigate';
|
||||
import {useFieldArray, useFormContext} from 'react-hook-form';
|
||||
import {useParams} from 'react-router-dom';
|
||||
|
||||
export function ThemeMoreOptionsButton() {
|
||||
const navigate = useNavigate();
|
||||
const {themeIndex} = useParams();
|
||||
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
|
||||
const {setValue, getValues} = useFormContext<AppearanceValues>();
|
||||
const {fields, remove} = useFieldArray<AppearanceValues>({
|
||||
name: 'appearance.themes.all',
|
||||
});
|
||||
|
||||
const deleteTheme = () => {
|
||||
if (fields.length <= 1) {
|
||||
toast.danger(message('At least one theme is required'));
|
||||
return;
|
||||
}
|
||||
if (themeIndex) {
|
||||
navigate('/admin/appearance/themes');
|
||||
remove(+themeIndex);
|
||||
setValue('appearance.themes.selectedThemeId', null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<MenuTrigger
|
||||
onItemSelected={key => {
|
||||
if (key === 'delete') {
|
||||
setConfirmDialogOpen(true);
|
||||
} else if (key === 'reset') {
|
||||
const path =
|
||||
`appearance.themes.all.${+themeIndex!}` as 'appearance.themes.all.0';
|
||||
const defaultColors = getValues(`${path}.is_dark`)
|
||||
? appearanceState().defaults!.appearance.themes.dark
|
||||
: appearanceState().defaults!.appearance.themes.light;
|
||||
|
||||
Object.entries(defaultColors).forEach(([colorName, themeValue]) => {
|
||||
appearanceState().preview.setThemeValue(colorName, themeValue);
|
||||
});
|
||||
appearanceState().preview.setThemeFont(null);
|
||||
|
||||
setValue(`${path}.values`, defaultColors, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
setValue(`${path}.font`, undefined, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<IconButton size="md" className="text-muted">
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
<Menu>
|
||||
<MenuItem value="reset" startIcon={<RestartAltIcon />}>
|
||||
<Trans message="Reset colors" />
|
||||
</MenuItem>
|
||||
<MenuItem value="delete" startIcon={<DeleteIcon />}>
|
||||
<Trans message="Delete" />
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</MenuTrigger>
|
||||
<DialogTrigger
|
||||
type="modal"
|
||||
isOpen={confirmDialogOpen}
|
||||
onClose={isConfirmed => {
|
||||
if (isConfirmed) {
|
||||
deleteTheme();
|
||||
}
|
||||
setConfirmDialogOpen(false);
|
||||
}}
|
||||
>
|
||||
<ConfirmationDialog
|
||||
isDanger
|
||||
title={<Trans message="Delete theme" />}
|
||||
body={<Trans message="Are you sure you want to delete this theme?" />}
|
||||
confirm={<Trans message="Delete" />}
|
||||
/>
|
||||
</DialogTrigger>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
116
common/resources/client/admin/appearance/sections/themes/theme-radius-panel.tsx
Executable file
116
common/resources/client/admin/appearance/sections/themes/theme-radius-panel.tsx
Executable file
@@ -0,0 +1,116 @@
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import {ButtonBase} from '@common/ui/buttons/button-base';
|
||||
import {ReactNode} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {useFormContext} from 'react-hook-form';
|
||||
import {AppearanceValues} from '@common/admin/appearance/appearance-store';
|
||||
import {message} from '@common/i18n/message';
|
||||
import {useParams} from 'react-router-dom';
|
||||
|
||||
const radiusMap = {
|
||||
'rounded-none': {
|
||||
label: message('Square'),
|
||||
value: '0px',
|
||||
},
|
||||
rounded: {
|
||||
label: message('Small'),
|
||||
value: '0.25rem',
|
||||
},
|
||||
'rounded-md': {
|
||||
label: message('Medium'),
|
||||
value: '0.375rem',
|
||||
},
|
||||
'rounded-lg': {
|
||||
label: message('Large'),
|
||||
value: '0.5rem',
|
||||
},
|
||||
'rounded-xl': {
|
||||
label: message('Larger'),
|
||||
value: '0.75rem',
|
||||
},
|
||||
'rounded-full': {
|
||||
label: message('Pill'),
|
||||
value: '9999px',
|
||||
},
|
||||
};
|
||||
|
||||
export function ThemeRadiusPanel() {
|
||||
return (
|
||||
<div className="space-y-24">
|
||||
<RadiusSelector
|
||||
label={<Trans message="Button rounding" />}
|
||||
name="button-radius"
|
||||
/>
|
||||
<RadiusSelector
|
||||
label={<Trans message="Input rounding" />}
|
||||
name="input-radius"
|
||||
/>
|
||||
<RadiusSelector
|
||||
label={<Trans message="Panel rounding" />}
|
||||
name="panel-radius"
|
||||
hidePill
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface RadiusSelectorProps {
|
||||
label: ReactNode;
|
||||
name: string;
|
||||
hidePill?: boolean;
|
||||
}
|
||||
function RadiusSelector({label, name, hidePill}: RadiusSelectorProps) {
|
||||
const {themeIndex} = useParams();
|
||||
const {watch, setValue} = useFormContext<AppearanceValues>();
|
||||
const formKey =
|
||||
`appearance.themes.all.${themeIndex}.values.--be-${name}` as 'appearance.themes.all.1.values.--be-button-radius';
|
||||
const currentValue = watch(formKey);
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-10 text-sm font-semibold">{label}</div>
|
||||
<div className="grid grid-cols-3 gap-10 text-sm">
|
||||
{Object.entries(radiusMap)
|
||||
.filter(([key]) => !hidePill || !key.includes('full'))
|
||||
.map(([key, {label, value}]) => (
|
||||
<PreviewButton
|
||||
key={key}
|
||||
radius={key}
|
||||
isActive={value === currentValue}
|
||||
onClick={() => {
|
||||
setValue(formKey, value, {shouldDirty: true});
|
||||
}}
|
||||
>
|
||||
<Trans {...label} />
|
||||
</PreviewButton>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface PreviewButtonProps {
|
||||
radius: string;
|
||||
children: ReactNode;
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
function PreviewButton({
|
||||
radius,
|
||||
children,
|
||||
isActive,
|
||||
onClick,
|
||||
}: PreviewButtonProps) {
|
||||
return (
|
||||
<ButtonBase
|
||||
display="block"
|
||||
className={clsx(
|
||||
'h-36 border-2 hover:bg-hover',
|
||||
radius,
|
||||
isActive && 'border-primary',
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</ButtonBase>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import {useForm, useFormContext} from 'react-hook-form';
|
||||
import {useEffect} from 'react';
|
||||
import {TuneIcon} from '../../../../icons/material/Tune';
|
||||
import {Button} from '../../../../ui/buttons/button';
|
||||
import {CssTheme} from '../../../../ui/themes/css-theme';
|
||||
import {FormTextField} from '../../../../ui/forms/input-field/text-field/text-field';
|
||||
import {FormSwitch} from '../../../../ui/forms/toggle/switch';
|
||||
import {AppearanceValues} from '../../appearance-store';
|
||||
import {DialogTrigger} from '../../../../ui/overlays/dialog/dialog-trigger';
|
||||
import {DialogFooter} from '../../../../ui/overlays/dialog/dialog-footer';
|
||||
import {useDialogContext} from '../../../../ui/overlays/dialog/dialog-context';
|
||||
import {Dialog} from '../../../../ui/overlays/dialog/dialog';
|
||||
import {DialogHeader} from '../../../../ui/overlays/dialog/dialog-header';
|
||||
import {DialogBody} from '../../../../ui/overlays/dialog/dialog-body';
|
||||
import {Trans} from '../../../../i18n/trans';
|
||||
import {Form} from '../../../../ui/forms/form';
|
||||
import {useParams} from 'react-router-dom';
|
||||
|
||||
export function ThemeSettingsDialogTrigger() {
|
||||
const {getValues, setValue} = useFormContext<AppearanceValues>();
|
||||
const {themeIndex} = useParams();
|
||||
const theme = getValues(`appearance.themes.all.${+themeIndex!}`);
|
||||
|
||||
return (
|
||||
<DialogTrigger
|
||||
type="modal"
|
||||
onClose={(value?: CssTheme) => {
|
||||
if (!value) return;
|
||||
|
||||
getValues('appearance.themes.all').forEach((currentTheme, index) => {
|
||||
// update changed theme
|
||||
if (currentTheme.id === value.id) {
|
||||
setValue(`appearance.themes.all.${index}`, value, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// unset "default_light" and "default_dark" on other themes
|
||||
if (value.default_light) {
|
||||
setValue(
|
||||
`appearance.themes.all.${index}`,
|
||||
{...currentTheme, default_light: false},
|
||||
{shouldDirty: true}
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (value.default_dark) {
|
||||
setValue(
|
||||
`appearance.themes.all.${index}`,
|
||||
{...currentTheme, default_dark: false},
|
||||
{shouldDirty: true}
|
||||
);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="outline"
|
||||
color="primary"
|
||||
startIcon={<TuneIcon />}
|
||||
>
|
||||
<Trans message="Settings" />
|
||||
</Button>
|
||||
<SettingsDialog theme={theme} />
|
||||
</DialogTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
interface SettingsDialogProps {
|
||||
theme: CssTheme;
|
||||
}
|
||||
function SettingsDialog({theme}: SettingsDialogProps) {
|
||||
const form = useForm<CssTheme>({defaultValues: theme});
|
||||
const {close, formId} = useDialogContext();
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = form.watch((value, {name}) => {
|
||||
// theme can only be set as either light or dark default
|
||||
if (name === 'default_light' && value.default_light) {
|
||||
form.setValue('default_dark', false);
|
||||
}
|
||||
if (name === 'default_dark' && value.default_dark) {
|
||||
form.setValue('default_light', false);
|
||||
}
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}, [form]);
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogHeader>
|
||||
<Trans message="Update settings" />
|
||||
</DialogHeader>
|
||||
<DialogBody>
|
||||
<Form
|
||||
form={form}
|
||||
id={formId}
|
||||
onSubmit={values => {
|
||||
close(values);
|
||||
}}
|
||||
>
|
||||
<FormTextField
|
||||
name="name"
|
||||
label={<Trans message="Name" />}
|
||||
className="mb-30"
|
||||
autoFocus
|
||||
/>
|
||||
<FormSwitch
|
||||
name="is_dark"
|
||||
className="mb-20 pb-20 border-b"
|
||||
description={
|
||||
<Trans message="Whether this theme has light text on dark background." />
|
||||
}
|
||||
>
|
||||
<Trans message="Dark theme" />
|
||||
</FormSwitch>
|
||||
<FormSwitch
|
||||
name="default_light"
|
||||
className="mb-30"
|
||||
description={
|
||||
<Trans message="When light mode is selected, this theme will be used." />
|
||||
}
|
||||
>
|
||||
<Trans message="Default for light mode" />
|
||||
</FormSwitch>
|
||||
<FormSwitch
|
||||
name="default_dark"
|
||||
description={
|
||||
<Trans message="When dark mode is selected, this theme will be used." />
|
||||
}
|
||||
>
|
||||
<Trans message="Default for dark mode" />
|
||||
</FormSwitch>
|
||||
</Form>
|
||||
</DialogBody>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={() => {
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<Trans message="Cancel" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="flat"
|
||||
color="primary"
|
||||
type="submit"
|
||||
form={formId}
|
||||
disabled={!form.formState.isDirty}
|
||||
>
|
||||
<Trans message="Save" />
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user