mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-11 02:12:21 -05:00
Regrouped design setting tabs
REF DES-831 - Changed tabs from `Brand`, `Site wide`, `Homepage` and `Post` to `Global` and `Theme settings`. - Added `Site wide`, `Homepage` and `Post` grouping inside the `Theme settings` tab.
This commit is contained in:
parent
4bf7b86ef4
commit
7725d78670
6 changed files with 380 additions and 122 deletions
|
@ -28,7 +28,7 @@ const ToggleGroup: React.FC<ToggleGroupProps> = ({children, gap = 'md', classNam
|
|||
}
|
||||
|
||||
className = clsx(
|
||||
'flex flex-col gap-3',
|
||||
'flex flex-col',
|
||||
gapClass,
|
||||
className
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import BrandSettings, {BrandSettingValues} from './designAndBranding/BrandSettings';
|
||||
import GlobalSettings, {GlobalSettingValues} from './designAndBranding/GlobalSettings';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import ThemePreview from './designAndBranding/ThemePreview';
|
||||
import ThemeSettings from './designAndBranding/ThemeSettings';
|
||||
|
@ -13,32 +13,32 @@ import {useGlobalData} from '../../providers/GlobalDataProvider';
|
|||
import {useRouting} from '@tryghost/admin-x-framework/routing';
|
||||
|
||||
const Sidebar: React.FC<{
|
||||
brandSettings: BrandSettingValues
|
||||
globalSettings: GlobalSettingValues
|
||||
themeSettingSections: Array<{id: string, title: string, settings: CustomThemeSetting[]}>
|
||||
updateBrandSetting: (key: string, value: SettingValue) => void
|
||||
updateGlobalSetting: (key: string, value: SettingValue) => void
|
||||
updateThemeSetting: (updated: CustomThemeSetting) => void
|
||||
onTabChange: (id: string) => void
|
||||
handleSave: () => Promise<boolean>
|
||||
}> = ({
|
||||
brandSettings,
|
||||
globalSettings,
|
||||
themeSettingSections,
|
||||
updateBrandSetting,
|
||||
updateGlobalSetting,
|
||||
updateThemeSetting,
|
||||
onTabChange
|
||||
}) => {
|
||||
const [selectedTab, setSelectedTab] = useState('brand');
|
||||
const [selectedTab, setSelectedTab] = useState('global');
|
||||
|
||||
const tabs: Tab[] = [
|
||||
{
|
||||
id: 'brand',
|
||||
title: 'Brand',
|
||||
contents: <BrandSettings updateSetting={updateBrandSetting} values={brandSettings} />
|
||||
id: 'global',
|
||||
title: 'Global',
|
||||
contents: <GlobalSettings updateSetting={updateGlobalSetting} values={globalSettings} />
|
||||
},
|
||||
...themeSettingSections.map(({id, title, settings}) => ({
|
||||
id,
|
||||
title,
|
||||
contents: <ThemeSettings settings={settings} updateSetting={updateThemeSetting} />
|
||||
}))
|
||||
{
|
||||
id: 'theme-settings',
|
||||
title: 'Theme settings',
|
||||
contents: <ThemeSettings sections={themeSettingSections} updateSetting={updateThemeSetting} />
|
||||
}
|
||||
];
|
||||
|
||||
const handleTabChange = (id: string) => {
|
||||
|
@ -52,7 +52,7 @@ const Sidebar: React.FC<{
|
|||
{tabs.length > 1 ?
|
||||
<TabView selectedTab={selectedTab} tabs={tabs} onTabChange={handleTabChange} />
|
||||
:
|
||||
<BrandSettings updateSetting={updateBrandSetting} values={brandSettings} />
|
||||
<GlobalSettings updateSetting={updateGlobalSetting} values={globalSettings} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -111,7 +111,7 @@ const DesignModal: React.FC = () => {
|
|||
}
|
||||
}, [setFormState, themeSettings]);
|
||||
|
||||
const updateBrandSetting = (key: string, value: SettingValue) => {
|
||||
const updateGlobalSetting = (key: string, value: SettingValue) => {
|
||||
updateForm(state => ({...state, settings: state.settings.map(setting => (
|
||||
setting.key === key ? {...setting, value, dirty: true} : setting
|
||||
))}));
|
||||
|
@ -188,10 +188,10 @@ const DesignModal: React.FC = () => {
|
|||
/>;
|
||||
const sidebarContent =
|
||||
<Sidebar
|
||||
brandSettings={{description, accentColor, icon, logo, coverImage, headingFont, bodyFont}}
|
||||
globalSettings={{description, accentColor, icon, logo, coverImage, headingFont, bodyFont}}
|
||||
handleSave={handleSave}
|
||||
themeSettingSections={themeSettingSections}
|
||||
updateBrandSetting={updateBrandSetting}
|
||||
updateGlobalSetting={updateGlobalSetting}
|
||||
updateThemeSetting={updateThemeSetting}
|
||||
onTabChange={onTabChange}
|
||||
/>;
|
||||
|
|
|
@ -0,0 +1,230 @@
|
|||
import React, {useState} from 'react';
|
||||
import UnsplashSelector from '../../../selectors/UnsplashSelector';
|
||||
import usePinturaEditor from '../../../../hooks/usePinturaEditor';
|
||||
import {APIError} from '@tryghost/admin-x-framework/errors';
|
||||
import {CUSTOM_FONTS} from '@tryghost/custom-fonts';
|
||||
import {ColorPickerField, Form, Heading, Hint, ImageUpload, Select} from '@tryghost/admin-x-design-system';
|
||||
import {SettingValue, getSettingValues} from '@tryghost/admin-x-framework/api/settings';
|
||||
import {getImageUrl, useUploadImage} from '@tryghost/admin-x-framework/api/images';
|
||||
import {useFramework} from '@tryghost/admin-x-framework';
|
||||
import {useGlobalData} from '../../../providers/GlobalDataProvider';
|
||||
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
|
||||
import type {Font} from '@tryghost/custom-fonts';
|
||||
|
||||
// TODO: create custom types for heading and body fonts in @tryghost/custom-fonts, so we can extend
|
||||
// them separately
|
||||
type BodyFontOption = {
|
||||
value: Font | 'Theme default',
|
||||
label: Font | 'Theme default'
|
||||
};
|
||||
type HeadingFontOption = BodyFontOption;
|
||||
|
||||
export interface GlobalSettingValues {
|
||||
description: string
|
||||
accentColor: string
|
||||
icon: string | null
|
||||
logo: string | null
|
||||
coverImage: string | null
|
||||
headingFont: string
|
||||
bodyFont: string
|
||||
}
|
||||
|
||||
const DEFAULT_FONT = 'Theme default';
|
||||
|
||||
const GlobalSettings: React.FC<{ values: GlobalSettingValues, updateSetting: (key: string, value: SettingValue) => void }> = ({values,updateSetting}) => {
|
||||
const {mutateAsync: uploadImage} = useUploadImage();
|
||||
const {settings} = useGlobalData();
|
||||
const [unsplashEnabled] = getSettingValues<boolean>(settings, ['unsplash']);
|
||||
const [showUnsplash, setShowUnsplash] = useState<boolean>(false);
|
||||
const {unsplashConfig} = useFramework();
|
||||
const handleError = useHandleError();
|
||||
|
||||
const editor = usePinturaEditor();
|
||||
|
||||
const [headingFont, setHeadingFont] = useState(values.headingFont || DEFAULT_FONT);
|
||||
const [bodyFont, setBodyFont] = useState(values.bodyFont || DEFAULT_FONT);
|
||||
|
||||
// TODO: replace with getCustomFonts() once custom-fonts is updated and differentiates
|
||||
// between heading and body fonts
|
||||
const customHeadingFonts: HeadingFontOption[] = CUSTOM_FONTS.map(x => ({label: x, value: x}));
|
||||
customHeadingFonts.unshift({label: DEFAULT_FONT, value: DEFAULT_FONT});
|
||||
|
||||
const customBodyFonts: BodyFontOption[] = CUSTOM_FONTS.map(x => ({label: x, value: x}));
|
||||
customBodyFonts.unshift({label: DEFAULT_FONT, value: DEFAULT_FONT});
|
||||
|
||||
const selectedHeadingFont = {label: headingFont, value: headingFont};
|
||||
const selectedBodyFont = {label: bodyFont, value: bodyFont};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form className='mt-4' gap='sm' margins='lg' title=''>
|
||||
<ColorPickerField
|
||||
debounceMs={200}
|
||||
direction='rtl'
|
||||
title={<Heading className='mt-[3px]' level={6}>Accent color</Heading>}
|
||||
value={values.accentColor}
|
||||
// we debounce this because the color picker fires a lot of events.
|
||||
onChange={value => updateSetting('accent_color', value)}
|
||||
/>
|
||||
<div className='flex items-start justify-between'>
|
||||
<div>
|
||||
<Heading level={6}>Publication icon</Heading>
|
||||
<Hint className='!mt-0 mr-5 max-w-[160px]'>A square, social icon, at least 60x60px</Hint>
|
||||
</div>
|
||||
<div className='flex gap-3'>
|
||||
<ImageUpload
|
||||
deleteButtonClassName='!top-1 !right-1'
|
||||
editButtonClassName='!top-1 !right-1'
|
||||
height={values.icon ? '66px' : '36px'}
|
||||
id='logo'
|
||||
imageBWCheckedBg={true}
|
||||
imageURL={values.icon || ''}
|
||||
width={values.icon ? '66px' : '160px'}
|
||||
onDelete={() => updateSetting('icon', null)}
|
||||
onUpload={async (file) => {
|
||||
try {
|
||||
updateSetting('icon', getImageUrl(await uploadImage({file})));
|
||||
} catch (e) {
|
||||
const error = e as APIError;
|
||||
if (error.response!.status === 415) {
|
||||
error.message = 'Unsupported file type';
|
||||
}
|
||||
handleError(error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Upload icon
|
||||
</ImageUpload>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`flex items-start justify-between ${values.icon && 'mt-2'}`}>
|
||||
<div>
|
||||
<Heading level={6}>Publication logo</Heading>
|
||||
<Hint className='!mt-0 mr-5 max-w-[160px]'>Appears usually in the main header of your theme</Hint>
|
||||
</div>
|
||||
<div>
|
||||
<ImageUpload
|
||||
deleteButtonClassName='!top-1 !right-1'
|
||||
height='60px'
|
||||
id='site-logo'
|
||||
imageBWCheckedBg={true}
|
||||
imageFit='contain'
|
||||
imageURL={values.logo || ''}
|
||||
width='160px'
|
||||
onDelete={() => updateSetting('logo', null)}
|
||||
onUpload={async (file) => {
|
||||
try {
|
||||
updateSetting('logo', getImageUrl(await uploadImage({file})));
|
||||
} catch (e) {
|
||||
const error = e as APIError;
|
||||
if (error.response!.status === 415) {
|
||||
error.message = 'Unsupported file type';
|
||||
}
|
||||
handleError(error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Upload logo
|
||||
</ImageUpload>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-2 flex items-start justify-between'>
|
||||
<div>
|
||||
<Heading level={6}>Publication cover</Heading>
|
||||
<Hint className='!mt-0 mr-5 max-w-[160px]'>Usually as a large banner image on your index pages</Hint>
|
||||
</div>
|
||||
<ImageUpload
|
||||
deleteButtonClassName='!top-1 !right-1'
|
||||
editButtonClassName='!top-1 !right-10'
|
||||
height='95px'
|
||||
id='cover'
|
||||
imageURL={values.coverImage || ''}
|
||||
openUnsplash={() => setShowUnsplash(true)}
|
||||
pintura={
|
||||
{
|
||||
isEnabled: editor.isEnabled,
|
||||
openEditor: async () => editor.openEditor({
|
||||
image: values.coverImage || '',
|
||||
handleSave: async (file:File) => {
|
||||
try {
|
||||
updateSetting('cover_image', getImageUrl(await uploadImage({file})));
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
unsplashButtonClassName='!bg-transparent !h-6 !top-1.5 !w-6 !right-1.5 z-50'
|
||||
unsplashEnabled={unsplashEnabled}
|
||||
width='160px'
|
||||
onDelete={() => updateSetting('cover_image', null)}
|
||||
onUpload={async (file: any) => {
|
||||
try {
|
||||
updateSetting('cover_image', getImageUrl(await uploadImage({file})));
|
||||
} catch (e) {
|
||||
const error = e as APIError;
|
||||
if (error.response!.status === 415) {
|
||||
error.message = 'Unsupported file type';
|
||||
}
|
||||
handleError(error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Upload cover
|
||||
</ImageUpload>
|
||||
{
|
||||
showUnsplash && unsplashConfig && unsplashEnabled && (
|
||||
<UnsplashSelector
|
||||
unsplashProviderConfig={unsplashConfig}
|
||||
onClose={() => {
|
||||
setShowUnsplash(false);
|
||||
}}
|
||||
onImageInsert={(image) => {
|
||||
if (image.src) {
|
||||
updateSetting('cover_image', image.src);
|
||||
}
|
||||
setShowUnsplash(false);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</Form>
|
||||
<Form className='-mt-4' gap='sm' margins='lg' title='Typography'>
|
||||
<Select
|
||||
hint={''}
|
||||
options={customHeadingFonts}
|
||||
selectedOption={selectedHeadingFont}
|
||||
title={'Heading font'}
|
||||
onSelect={(option) => {
|
||||
if (option?.value === DEFAULT_FONT) {
|
||||
setHeadingFont(DEFAULT_FONT);
|
||||
updateSetting('heading_font', '');
|
||||
} else {
|
||||
setHeadingFont(option?.value || '');
|
||||
updateSetting('heading_font', option?.value || '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
hint={''}
|
||||
options={customBodyFonts}
|
||||
selectedOption={selectedBodyFont}
|
||||
title={'Body font'}
|
||||
onSelect={(option) => {
|
||||
if (option?.value === DEFAULT_FONT) {
|
||||
setBodyFont(DEFAULT_FONT);
|
||||
updateSetting('body_font', '');
|
||||
} else {
|
||||
setBodyFont(option?.value || '');
|
||||
updateSetting('body_font', option?.value || '');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalSettings;
|
|
@ -3,7 +3,7 @@ import React, {useCallback} from 'react';
|
|||
import {CustomThemeSetting, hiddenCustomThemeSettingValue} from '@tryghost/admin-x-framework/api/customThemeSettings';
|
||||
import {isCustomThemeSettingVisible} from '../../../../utils/isCustomThemeSettingsVisible';
|
||||
|
||||
type BrandSettings = {
|
||||
type GlobalSettings = {
|
||||
description: string;
|
||||
accentColor: string;
|
||||
icon: string;
|
||||
|
@ -15,7 +15,7 @@ type BrandSettings = {
|
|||
}
|
||||
|
||||
interface ThemePreviewProps {
|
||||
settings: BrandSettings
|
||||
settings: GlobalSettings
|
||||
url: string
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
import React, {useEffect, useState} from 'react';
|
||||
import {ColorPickerField, Heading, Hint, ImageUpload, Select, TextField, Toggle} from '@tryghost/admin-x-design-system';
|
||||
import {CustomThemeSetting} from '@tryghost/admin-x-framework/api/customThemeSettings';
|
||||
import {getImageUrl, useUploadImage} from '@tryghost/admin-x-framework/api/images';
|
||||
import {humanizeSettingKey} from '@tryghost/admin-x-framework/api/settings';
|
||||
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
|
||||
|
||||
interface ThemeSettingProps {
|
||||
setting: CustomThemeSetting;
|
||||
setSetting: <Setting extends CustomThemeSetting>(value: Setting['value']) => void;
|
||||
}
|
||||
|
||||
const ThemeSetting: React.FC<ThemeSettingProps> = ({setting, setSetting}) => {
|
||||
const [fieldValues, setFieldValues] = useState<{ [key: string]: string | null }>({});
|
||||
useEffect(() => {
|
||||
const valueAsString = setting.value === null ? '' : String(setting.value);
|
||||
setFieldValues(values => ({...values, [setting.key]: valueAsString}));
|
||||
}, [setting]);
|
||||
|
||||
const handleBlur = (key: string) => {
|
||||
if (fieldValues[key] !== undefined) {
|
||||
setSetting(fieldValues[key]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (key: string, value: string) => {
|
||||
setFieldValues(values => ({...values, [key]: value}));
|
||||
};
|
||||
const {mutateAsync: uploadImage} = useUploadImage();
|
||||
const handleError = useHandleError();
|
||||
|
||||
const handleImageUpload = async (file: File) => {
|
||||
try {
|
||||
const imageUrl = getImageUrl(await uploadImage({file}));
|
||||
setSetting(imageUrl);
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
};
|
||||
|
||||
switch (setting.type) {
|
||||
case 'text':
|
||||
return (
|
||||
<TextField
|
||||
hint={setting.description}
|
||||
title={humanizeSettingKey(setting.key)}
|
||||
value={fieldValues[setting.key] || ''}
|
||||
onBlur={() => handleBlur(setting.key)}
|
||||
onChange={event => handleChange(setting.key, event.target.value)}
|
||||
/>
|
||||
);
|
||||
case 'boolean':
|
||||
return (
|
||||
<Toggle
|
||||
checked={setting.value}
|
||||
direction="rtl"
|
||||
hint={setting.description}
|
||||
label={humanizeSettingKey(setting.key)}
|
||||
onChange={event => setSetting(event.target.checked)}
|
||||
/>
|
||||
);
|
||||
case 'select':
|
||||
return (
|
||||
<Select
|
||||
hint={setting.description}
|
||||
options={setting.options.map(option => ({label: option, value: option}))}
|
||||
selectedOption={{label: setting.value, value: setting.value}}
|
||||
testId={`setting-select-${setting.key}`}
|
||||
title={humanizeSettingKey(setting.key)}
|
||||
onSelect={option => setSetting(option?.value || null)}
|
||||
/>
|
||||
);
|
||||
case 'color':
|
||||
return (
|
||||
<ColorPickerField
|
||||
debounceMs={200}
|
||||
direction='rtl'
|
||||
hint={setting.description}
|
||||
title={humanizeSettingKey(setting.key)}
|
||||
value={setting.value || ''}
|
||||
onChange={value => setSetting(value)}
|
||||
/>
|
||||
);
|
||||
case 'image':
|
||||
return <>
|
||||
<Heading useLabelTag>{humanizeSettingKey(setting.key)}</Heading>
|
||||
<ImageUpload
|
||||
height={setting.value ? '100px' : '32px'}
|
||||
id={`custom-${setting.key}`}
|
||||
imageURL={setting.value || ''}
|
||||
onDelete={() => setSetting(null)}
|
||||
onUpload={file => handleImageUpload(file)}
|
||||
>Upload image</ImageUpload>
|
||||
{setting.description && <Hint>{setting.description}</Hint>}
|
||||
</>;
|
||||
}
|
||||
};
|
||||
|
||||
export default ThemeSetting;
|
|
@ -1,109 +1,38 @@
|
|||
import React, {useEffect, useState} from 'react';
|
||||
import {ColorPickerField, Heading, Hint, ImageUpload, Select, SettingGroupContent, TextField, Toggle} from '@tryghost/admin-x-design-system';
|
||||
import React from 'react';
|
||||
import ThemeSetting from './ThemeSetting';
|
||||
import {CustomThemeSetting} from '@tryghost/admin-x-framework/api/customThemeSettings';
|
||||
import {getImageUrl, useUploadImage} from '@tryghost/admin-x-framework/api/images';
|
||||
import {humanizeSettingKey} from '@tryghost/admin-x-framework/api/settings';
|
||||
import {Form} from '@tryghost/admin-x-design-system';
|
||||
import {isCustomThemeSettingVisible} from '../../../../utils/isCustomThemeSettingsVisible';
|
||||
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
|
||||
|
||||
const ThemeSetting: React.FC<{
|
||||
setting: CustomThemeSetting,
|
||||
setSetting: <Setting extends CustomThemeSetting>(value: Setting['value']) => void
|
||||
}> = ({setting, setSetting}) => {
|
||||
const [fieldValues, setFieldValues] = useState<{ [key: string]: string | null }>({});
|
||||
useEffect(() => {
|
||||
const valueAsString = setting.value === null ? '' : String(setting.value);
|
||||
setFieldValues(values => ({...values, [setting.key]: valueAsString}));
|
||||
}, [setting]);
|
||||
|
||||
const handleBlur = (key: string) => {
|
||||
if (fieldValues[key] !== undefined) {
|
||||
setSetting(fieldValues[key]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (key: string, value: string) => {
|
||||
setFieldValues(values => ({...values, [key]: value}));
|
||||
};
|
||||
const {mutateAsync: uploadImage} = useUploadImage();
|
||||
const handleError = useHandleError();
|
||||
|
||||
const handleImageUpload = async (file: File) => {
|
||||
try {
|
||||
const imageUrl = getImageUrl(await uploadImage({file}));
|
||||
setSetting(imageUrl);
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
};
|
||||
|
||||
switch (setting.type) {
|
||||
case 'text':
|
||||
return (
|
||||
<TextField
|
||||
hint={setting.description}
|
||||
title={humanizeSettingKey(setting.key)}
|
||||
value={fieldValues[setting.key] || ''}
|
||||
onBlur={() => handleBlur(setting.key)}
|
||||
onChange={event => handleChange(setting.key, event.target.value)}
|
||||
/>
|
||||
);
|
||||
case 'boolean':
|
||||
return (
|
||||
<Toggle
|
||||
checked={setting.value}
|
||||
direction="rtl"
|
||||
hint={setting.description}
|
||||
label={humanizeSettingKey(setting.key)}
|
||||
onChange={event => setSetting(event.target.checked)}
|
||||
/>
|
||||
);
|
||||
case 'select':
|
||||
return (
|
||||
<Select
|
||||
hint={setting.description}
|
||||
options={setting.options.map(option => ({label: option, value: option}))}
|
||||
selectedOption={{label: setting.value, value: setting.value}}
|
||||
testId={`setting-select-${setting.key}`}
|
||||
title={humanizeSettingKey(setting.key)}
|
||||
onSelect={option => setSetting(option?.value || null)}
|
||||
/>
|
||||
);
|
||||
case 'color':
|
||||
return (
|
||||
<ColorPickerField
|
||||
debounceMs={200}
|
||||
direction='rtl'
|
||||
hint={setting.description}
|
||||
title={humanizeSettingKey(setting.key)}
|
||||
value={setting.value || ''}
|
||||
onChange={value => setSetting(value)}
|
||||
/>
|
||||
);
|
||||
case 'image':
|
||||
return <>
|
||||
<Heading useLabelTag>{humanizeSettingKey(setting.key)}</Heading>
|
||||
<ImageUpload
|
||||
height={setting.value ? '100px' : '32px'}
|
||||
id={`custom-${setting.key}`}
|
||||
imageURL={setting.value || ''}
|
||||
onDelete={() => setSetting(null)}
|
||||
onUpload={file => handleImageUpload(file)}
|
||||
>Upload image</ImageUpload>
|
||||
{setting.description && <Hint>{setting.description}</Hint>}
|
||||
</>;
|
||||
}
|
||||
};
|
||||
|
||||
const ThemeSettings: React.FC<{ settings: CustomThemeSetting[], updateSetting: (setting: CustomThemeSetting) => void }> = ({settings, updateSetting}) => {
|
||||
// Filter out custom theme settings that should not be visible
|
||||
const settingsKeyValueObj = settings.reduce((obj, {key, value}) => ({...obj, [key]: value}), {});
|
||||
const filteredSettings = settings.filter(setting => isCustomThemeSettingVisible(setting, settingsKeyValueObj));
|
||||
interface ThemeSettingsProps {
|
||||
sections: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
settings: CustomThemeSetting[];
|
||||
}>;
|
||||
updateSetting: (setting: CustomThemeSetting) => void;
|
||||
}
|
||||
|
||||
const ThemeSettings: React.FC<ThemeSettingsProps> = ({sections, updateSetting}) => {
|
||||
return (
|
||||
<SettingGroupContent className='mt-7'>
|
||||
{filteredSettings.map(setting => <ThemeSetting key={setting.key} setSetting={value => updateSetting({...setting, value} as CustomThemeSetting)} setting={setting} />)}
|
||||
</SettingGroupContent>
|
||||
<>
|
||||
{sections.map((section) => {
|
||||
const filteredSettings = section.settings.filter(setting => isCustomThemeSettingVisible(setting, section.settings.reduce((obj, {key, value}) => ({...obj, [key]: value}), {}))
|
||||
);
|
||||
|
||||
return (
|
||||
<Form key={section.id} className='first-of-type:mt-6' gap='sm' margins='lg' title={section.title}>
|
||||
{filteredSettings.map(setting => (
|
||||
<ThemeSetting
|
||||
key={setting.key}
|
||||
setSetting={value => updateSetting({...setting, value} as CustomThemeSetting)}
|
||||
setting={setting}
|
||||
/>
|
||||
))}
|
||||
</Form>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue