mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-14 09:25:47 +01:00
✨ Add new config format
This commit is contained in:
43
src/components/Settings/Customization/BackgroundChanger.tsx
Normal file
43
src/components/Settings/Customization/BackgroundChanger.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { TextInput } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ChangeEventHandler, useState } from 'react';
|
||||
import { useConfigContext } from '../../../config/provider';
|
||||
import { useConfigStore } from '../../../config/store';
|
||||
|
||||
interface BackgroundChangerProps {
|
||||
defaultValue: string | undefined;
|
||||
}
|
||||
|
||||
export const BackgroundChanger = ({ defaultValue }: BackgroundChangerProps) => {
|
||||
const { t } = useTranslation('settings/customization/page-appearance');
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
const { name: configName } = useConfigContext();
|
||||
const [backgroundImageUrl, setBackgroundImageUrl] = useState(defaultValue);
|
||||
|
||||
if (!configName) return null;
|
||||
|
||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
||||
const value = ev.currentTarget.value;
|
||||
const backgroundImageUrl = value.trim().length === 0 ? undefined : value;
|
||||
setBackgroundImageUrl(backgroundImageUrl);
|
||||
updateConfig(configName, (prev) => ({
|
||||
...prev,
|
||||
settings: {
|
||||
...prev.settings,
|
||||
customization: {
|
||||
...prev.settings.customization,
|
||||
backgroundImageUrl,
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
label={t('background.label')}
|
||||
placeholder="/imgs/background.png"
|
||||
value={backgroundImageUrl}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
104
src/components/Settings/Customization/ColorSelector.tsx
Normal file
104
src/components/Settings/Customization/ColorSelector.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
ColorSwatch,
|
||||
Grid,
|
||||
Group,
|
||||
MantineTheme,
|
||||
Popover,
|
||||
Text,
|
||||
useMantineTheme,
|
||||
} from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useColorTheme } from '../../../tools/color';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { useConfigStore } from '../../../config/store';
|
||||
import { useConfigContext } from '../../../config/provider';
|
||||
|
||||
interface ColorControlProps {
|
||||
defaultValue: MantineTheme['primaryColor'] | undefined;
|
||||
type: 'primary' | 'secondary';
|
||||
}
|
||||
|
||||
export function ColorSelector({ type, defaultValue }: ColorControlProps) {
|
||||
const { t } = useTranslation('settings/customization/color-selector');
|
||||
const [color, setColor] = useState(defaultValue);
|
||||
const [popoverOpened, popover] = useDisclosure(false);
|
||||
const { setPrimaryColor, setSecondaryColor } = useColorTheme();
|
||||
const { name: configName } = useConfigContext();
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
|
||||
const theme = useMantineTheme();
|
||||
const colors = Object.keys(theme.colors).map((color) => ({
|
||||
swatch: theme.colors[color][6],
|
||||
color,
|
||||
}));
|
||||
|
||||
if (!color || !configName) return null;
|
||||
|
||||
const handleSelection = (color: MantineTheme['primaryColor']) => {
|
||||
setColor(color);
|
||||
if (type === 'primary') setPrimaryColor(color);
|
||||
else setSecondaryColor(color);
|
||||
updateConfig(configName, (prev) => {
|
||||
const colors = prev.settings.customization.colors;
|
||||
colors[type] = color;
|
||||
return {
|
||||
...prev,
|
||||
settings: {
|
||||
...prev.settings,
|
||||
customization: {
|
||||
...prev.settings.customization,
|
||||
colors,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const swatches = colors.map(({ color, swatch }) => (
|
||||
<Grid.Col span={2} key={color}>
|
||||
<ColorSwatch
|
||||
component="button"
|
||||
type="button"
|
||||
onClick={() => handleSelection(color)}
|
||||
color={swatch}
|
||||
size={22}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
</Grid.Col>
|
||||
));
|
||||
|
||||
return (
|
||||
<Group>
|
||||
<Popover
|
||||
width={250}
|
||||
withinPortal
|
||||
opened={popoverOpened}
|
||||
onClose={popover.close}
|
||||
position="left"
|
||||
withArrow
|
||||
>
|
||||
<Popover.Target>
|
||||
<ColorSwatch
|
||||
component="button"
|
||||
type="button"
|
||||
color={theme.colors[color][6]}
|
||||
onClick={popover.toggle}
|
||||
size={22}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<Grid gutter="lg" columns={14}>
|
||||
{swatches}
|
||||
</Grid>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
<Text>
|
||||
{t('suffix', {
|
||||
color: type[0].toUpperCase() + type.slice(1),
|
||||
})}
|
||||
</Text>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
44
src/components/Settings/Customization/CustomCssChanger.tsx
Normal file
44
src/components/Settings/Customization/CustomCssChanger.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Textarea } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ChangeEventHandler, useState } from 'react';
|
||||
import { useConfigContext } from '../../../config/provider';
|
||||
import { useConfigStore } from '../../../config/store';
|
||||
|
||||
interface CustomCssChangerProps {
|
||||
defaultValue: string | undefined;
|
||||
}
|
||||
|
||||
export const CustomCssChanger = ({ defaultValue }: CustomCssChangerProps) => {
|
||||
const { t } = useTranslation('settings/customization/page-appearance');
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
const { name: configName } = useConfigContext();
|
||||
const [customCss, setCustomCss] = useState(defaultValue);
|
||||
|
||||
if (!configName) return null;
|
||||
|
||||
const handleChange: ChangeEventHandler<HTMLTextAreaElement> = (ev) => {
|
||||
const value = ev.currentTarget.value;
|
||||
const customCss = value.trim().length === 0 ? undefined : value;
|
||||
setCustomCss(customCss);
|
||||
updateConfig(configName, (prev) => ({
|
||||
...prev,
|
||||
settings: {
|
||||
...prev.settings,
|
||||
customization: {
|
||||
...prev.settings.customization,
|
||||
customCss,
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<Textarea
|
||||
minRows={5}
|
||||
label={t('customCSS.label')}
|
||||
placeholder={t('customCSS.placeholder')}
|
||||
value={customCss}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
43
src/components/Settings/Customization/FaviconChanger.tsx
Normal file
43
src/components/Settings/Customization/FaviconChanger.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { TextInput } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ChangeEventHandler, useState } from 'react';
|
||||
import { useConfigContext } from '../../../config/provider';
|
||||
import { useConfigStore } from '../../../config/store';
|
||||
|
||||
interface FaviconChangerProps {
|
||||
defaultValue: string | undefined;
|
||||
}
|
||||
|
||||
export const FaviconChanger = ({ defaultValue }: FaviconChangerProps) => {
|
||||
const { t } = useTranslation('settings/customization/page-appearance');
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
const { name: configName } = useConfigContext();
|
||||
const [faviconUrl, setFaviconUrl] = useState(defaultValue);
|
||||
|
||||
if (!configName) return null;
|
||||
|
||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
||||
const value = ev.currentTarget.value;
|
||||
const faviconUrl = value.trim().length === 0 ? undefined : value;
|
||||
setFaviconUrl(faviconUrl);
|
||||
updateConfig(configName, (prev) => ({
|
||||
...prev,
|
||||
settings: {
|
||||
...prev.settings,
|
||||
customization: {
|
||||
...prev.settings.customization,
|
||||
faviconUrl,
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
label={t('favicon.label')}
|
||||
placeholder="/imgs/favicon/favicon.svg"
|
||||
value={faviconUrl}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
164
src/components/Settings/Customization/LayoutSelector.tsx
Normal file
164
src/components/Settings/Customization/LayoutSelector.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import {
|
||||
Box,
|
||||
Center,
|
||||
Checkbox,
|
||||
createStyles,
|
||||
Group,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { IconBrandDocker, IconLayout, IconSearch } from '@tabler/icons';
|
||||
import { ChangeEvent, Dispatch, SetStateAction, useState } from 'react';
|
||||
import { useConfigContext } from '../../../config/provider';
|
||||
import { useConfigStore } from '../../../config/store';
|
||||
import { CustomizationSettingsType } from '../../../types/settings';
|
||||
import { Logo } from '../../layout/Logo';
|
||||
|
||||
interface LayoutSelectorProps {
|
||||
defaultLayout: CustomizationSettingsType['layout'] | undefined;
|
||||
}
|
||||
|
||||
// TODO: add translations
|
||||
export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => {
|
||||
const { classes } = useStyles();
|
||||
|
||||
const { name: configName } = useConfigContext();
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
|
||||
const [leftSidebar, setLeftSidebar] = useState(defaultLayout?.enabledLeftSidebar ?? true);
|
||||
const [rightSidebar, setRightSidebar] = useState(defaultLayout?.enabledRightSidebar ?? true);
|
||||
const [docker, setDocker] = useState(defaultLayout?.enabledDocker ?? false);
|
||||
const [ping, setPing] = useState(defaultLayout?.enabledPing ?? false);
|
||||
const [searchBar, setSearchBar] = useState(defaultLayout?.enabledSearchbar ?? false);
|
||||
|
||||
if (!configName) return null;
|
||||
|
||||
const handleChange = (
|
||||
key: keyof CustomizationSettingsType['layout'],
|
||||
event: ChangeEvent<HTMLInputElement>,
|
||||
setState: Dispatch<SetStateAction<boolean>>
|
||||
) => {
|
||||
const value = event.target.checked;
|
||||
setState(value);
|
||||
updateConfig(configName, (prev) => {
|
||||
const layout = prev.settings.customization.layout;
|
||||
|
||||
layout[key] = value;
|
||||
|
||||
return {
|
||||
...prev,
|
||||
settings: {
|
||||
...prev.settings,
|
||||
customization: {
|
||||
...prev.settings.customization,
|
||||
layout,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className={classes.box} p="xl" pb="sm">
|
||||
<Stack spacing="xs">
|
||||
<Group spacing={5}>
|
||||
<IconLayout size={20} />
|
||||
<Title order={6}>Dashboard layout</Title>
|
||||
</Group>
|
||||
|
||||
<Text color="dimmed" size="sm">
|
||||
You can adjust the layout of the Dashboard to your preferences. The main are cannot be
|
||||
turned on or off
|
||||
</Text>
|
||||
|
||||
<Paper px="xs" py={2} withBorder>
|
||||
<Group position="apart">
|
||||
<Logo size="xs" />
|
||||
<Group spacing={4}>
|
||||
{searchBar ? (
|
||||
<Paper withBorder p={2} w={60}>
|
||||
<Group spacing={2} align="center">
|
||||
<IconSearch size={8} />
|
||||
<Text size={8} color="dimmed">
|
||||
Search
|
||||
</Text>
|
||||
</Group>
|
||||
</Paper>
|
||||
) : null}
|
||||
{docker ? <IconBrandDocker size={18} color="#0db7ed" /> : null}
|
||||
</Group>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
<Group align="stretch">
|
||||
{leftSidebar && (
|
||||
<Paper p="xs" withBorder>
|
||||
<Center style={{ height: '100%' }}>
|
||||
<Text align="center">Sidebar</Text>
|
||||
</Center>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
<Paper className={classes.main} p="xs" withBorder>
|
||||
<Text align="center">Main</Text>
|
||||
<Text color="dimmed" size="xs" align="center">
|
||||
Can be used for categories,
|
||||
<br />
|
||||
services and integrations
|
||||
</Text>
|
||||
</Paper>
|
||||
|
||||
{rightSidebar && (
|
||||
<Paper p="xs" withBorder>
|
||||
<Center style={{ height: '100%' }}>
|
||||
<Text align="center">Sidebar</Text>
|
||||
</Center>
|
||||
</Paper>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<Stack spacing="xs">
|
||||
<Checkbox
|
||||
label="Enable left sidebar"
|
||||
description="Optional. Can be used for services and integrations only"
|
||||
checked={leftSidebar}
|
||||
onChange={(ev) => handleChange('enabledLeftSidebar', ev, setLeftSidebar)}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Enable right sidebar"
|
||||
description="Optional. Can be used for services and integrations only"
|
||||
checked={rightSidebar}
|
||||
onChange={(ev) => handleChange('enabledRightSidebar', ev, setRightSidebar)}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Enable search bar"
|
||||
checked={searchBar}
|
||||
onChange={(ev) => handleChange('enabledSearchbar', ev, setSearchBar)}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Enable docker"
|
||||
checked={docker}
|
||||
onChange={(ev) => handleChange('enabledDocker', ev, setDocker)}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Enable pings"
|
||||
checked={ping}
|
||||
onChange={(ev) => handleChange('enabledPing', ev, setPing)}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
main: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
box: {
|
||||
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[1],
|
||||
borderRadius: theme.radius.md,
|
||||
},
|
||||
}));
|
||||
43
src/components/Settings/Customization/LogoImageChanger.tsx
Normal file
43
src/components/Settings/Customization/LogoImageChanger.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { TextInput } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ChangeEventHandler, useState } from 'react';
|
||||
import { useConfigContext } from '../../../config/provider';
|
||||
import { useConfigStore } from '../../../config/store';
|
||||
|
||||
interface LogoImageChangerProps {
|
||||
defaultValue: string | undefined;
|
||||
}
|
||||
|
||||
export const LogoImageChanger = ({ defaultValue }: LogoImageChangerProps) => {
|
||||
const { t } = useTranslation('settings/customization/page-appearance');
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
const { name: configName } = useConfigContext();
|
||||
const [logoImageSrc, setLogoImageSrc] = useState(defaultValue);
|
||||
|
||||
if (!configName) return null;
|
||||
|
||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
||||
const value = ev.currentTarget.value;
|
||||
const logoImageSrc = value.trim().length === 0 ? undefined : value;
|
||||
setLogoImageSrc(logoImageSrc);
|
||||
updateConfig(configName, (prev) => ({
|
||||
...prev,
|
||||
settings: {
|
||||
...prev.settings,
|
||||
customization: {
|
||||
...prev.settings.customization,
|
||||
logoImageUrl: logoImageSrc,
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
label={t('logo.label')}
|
||||
placeholder="/imgs/logo/logo.png"
|
||||
value={logoImageSrc}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
43
src/components/Settings/Customization/MetaTitleChanger.tsx
Normal file
43
src/components/Settings/Customization/MetaTitleChanger.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { TextInput } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ChangeEventHandler, useState } from 'react';
|
||||
import { useConfigContext } from '../../../config/provider';
|
||||
import { useConfigStore } from '../../../config/store';
|
||||
|
||||
interface MetaTitleChangerProps {
|
||||
defaultValue: string | undefined;
|
||||
}
|
||||
|
||||
export const MetaTitleChanger = ({ defaultValue }: MetaTitleChangerProps) => {
|
||||
const { t } = useTranslation('settings/customization/page-appearance');
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
const { name: configName } = useConfigContext();
|
||||
const [metaTitle, setMetaTitle] = useState(defaultValue);
|
||||
|
||||
if (!configName) return null;
|
||||
|
||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
||||
const value = ev.currentTarget.value;
|
||||
const metaTitle = value.trim().length === 0 ? undefined : value;
|
||||
setMetaTitle(metaTitle);
|
||||
updateConfig(configName, (prev) => ({
|
||||
...prev,
|
||||
settings: {
|
||||
...prev.settings,
|
||||
customization: {
|
||||
...prev.settings.customization,
|
||||
metaTitle,
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
label={t('metaTitle.label')}
|
||||
placeholder={t('metaTitle.placeholder')}
|
||||
value={metaTitle}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
60
src/components/Settings/Customization/OpacitySelector.tsx
Normal file
60
src/components/Settings/Customization/OpacitySelector.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Text, Slider, Stack } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfigContext } from '../../../config/provider';
|
||||
import { useConfigStore } from '../../../config/store';
|
||||
|
||||
interface OpacitySelectorProps {
|
||||
defaultValue: number | undefined;
|
||||
}
|
||||
|
||||
export function OpacitySelector({ defaultValue }: OpacitySelectorProps) {
|
||||
const [opacity, setOpacity] = useState(defaultValue || 100);
|
||||
const { t } = useTranslation('settings/customization/opacity-selector');
|
||||
|
||||
const { name: configName } = useConfigContext();
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
|
||||
if (!configName) return null;
|
||||
|
||||
const handleChange = (opacity: number) => {
|
||||
setOpacity(opacity);
|
||||
updateConfig(configName, (prev) => ({
|
||||
...prev,
|
||||
settings: {
|
||||
...prev.settings,
|
||||
customization: {
|
||||
...prev.settings.customization,
|
||||
appOpacity: opacity,
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack spacing="xs">
|
||||
<Text>{t('label')}</Text>
|
||||
<Slider
|
||||
defaultValue={opacity}
|
||||
step={10}
|
||||
min={10}
|
||||
marks={MARKS}
|
||||
styles={{ markLabel: { fontSize: 'xx-small' } }}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const MARKS = [
|
||||
{ value: 10, label: '10' },
|
||||
{ value: 20, label: '20' },
|
||||
{ value: 30, label: '30' },
|
||||
{ value: 40, label: '40' },
|
||||
{ value: 50, label: '50' },
|
||||
{ value: 60, label: '60' },
|
||||
{ value: 70, label: '70' },
|
||||
{ value: 80, label: '80' },
|
||||
{ value: 90, label: '90' },
|
||||
{ value: 100, label: '100' },
|
||||
];
|
||||
43
src/components/Settings/Customization/PageTitleChanger.tsx
Normal file
43
src/components/Settings/Customization/PageTitleChanger.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { TextInput } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ChangeEventHandler, useState } from 'react';
|
||||
import { useConfigContext } from '../../../config/provider';
|
||||
import { useConfigStore } from '../../../config/store';
|
||||
|
||||
interface PageTitleChangerProps {
|
||||
defaultValue: string | undefined;
|
||||
}
|
||||
|
||||
export const PageTitleChanger = ({ defaultValue }: PageTitleChangerProps) => {
|
||||
const { t } = useTranslation('settings/customization/page-appearance');
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
const { name: configName } = useConfigContext();
|
||||
const [pageTitle, setPageTitle] = useState(defaultValue);
|
||||
|
||||
if (!configName) return null;
|
||||
|
||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
||||
const value = ev.currentTarget.value;
|
||||
const pageTitle = value.trim().length === 0 ? undefined : value;
|
||||
setPageTitle(pageTitle);
|
||||
updateConfig(configName, (prev) => ({
|
||||
...prev,
|
||||
settings: {
|
||||
...prev.settings,
|
||||
customization: {
|
||||
...prev.settings.customization,
|
||||
pageTitle,
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
label={t('pageTitle.label')}
|
||||
placeholder={t('pageTitle.placeholder')}
|
||||
value={pageTitle}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
101
src/components/Settings/Customization/ShadeSelector.tsx
Normal file
101
src/components/Settings/Customization/ShadeSelector.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
ColorSwatch,
|
||||
Group,
|
||||
Popover,
|
||||
Text,
|
||||
useMantineTheme,
|
||||
MantineTheme,
|
||||
Stack,
|
||||
Grid,
|
||||
} from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useColorTheme } from '../../../tools/color';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { useConfigStore } from '../../../config/store';
|
||||
import { useConfigContext } from '../../../config/provider';
|
||||
|
||||
interface ShadeSelectorProps {
|
||||
defaultValue: MantineTheme['primaryShade'] | undefined;
|
||||
}
|
||||
|
||||
export function ShadeSelector({ defaultValue }: ShadeSelectorProps) {
|
||||
const { t } = useTranslation('settings/customization/shade-selector');
|
||||
const [shade, setShade] = useState(defaultValue);
|
||||
const [popoverOpened, popover] = useDisclosure(false);
|
||||
const { primaryColor, setPrimaryShade } = useColorTheme();
|
||||
|
||||
const { name: configName } = useConfigContext();
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
|
||||
const theme = useMantineTheme();
|
||||
const primaryShades = theme.colors[primaryColor].map((s, i) => ({
|
||||
swatch: theme.colors[primaryColor][i],
|
||||
shade: i as MantineTheme['primaryShade'],
|
||||
}));
|
||||
|
||||
if (shade === undefined || !configName) return null;
|
||||
|
||||
const handleSelection = (shade: MantineTheme['primaryShade']) => {
|
||||
setPrimaryShade(shade);
|
||||
setShade(shade);
|
||||
updateConfig(configName, (prev) => ({
|
||||
...prev,
|
||||
settings: {
|
||||
...prev.settings,
|
||||
customization: {
|
||||
...prev.settings.customization,
|
||||
colors: {
|
||||
...prev.settings.customization.colors,
|
||||
shade,
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const primarySwatches = primaryShades.map(({ swatch, shade }) => (
|
||||
<Grid.Col span={1} key={Number(shade)}>
|
||||
<ColorSwatch
|
||||
component="button"
|
||||
type="button"
|
||||
onClick={() => handleSelection(shade)}
|
||||
color={swatch}
|
||||
size={22}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
</Grid.Col>
|
||||
));
|
||||
|
||||
return (
|
||||
<Group>
|
||||
<Popover
|
||||
width={350}
|
||||
withinPortal
|
||||
opened={popoverOpened}
|
||||
onClose={popover.close}
|
||||
position="left"
|
||||
withArrow
|
||||
>
|
||||
<Popover.Target>
|
||||
<ColorSwatch
|
||||
component="button"
|
||||
type="button"
|
||||
color={theme.colors[primaryColor][Number(shade)]}
|
||||
onClick={popover.toggle}
|
||||
size={22}
|
||||
style={{ display: 'block', cursor: 'pointer' }}
|
||||
/>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<Stack spacing="xs">
|
||||
<Grid gutter="lg" columns={10}>
|
||||
{primarySwatches}
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
<Text>{t('label')}</Text>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user