Added basic theme settings design in AdminX (#17000)
refs https://github.com/TryGhost/Team/issues/3432 - adds basic design structure for theme settings/management in adminX --------- Co-authored-by: Peter Zimon <peter.zimon@gmail.com>
After Width: | Height: | Size: 77 KiB |
After Width: | Height: | Size: 54 KiB |
After Width: | Height: | Size: 42 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 56 KiB |
After Width: | Height: | Size: 154 KiB |
After Width: | Height: | Size: 80 KiB |
After Width: | Height: | Size: 125 KiB |
After Width: | Height: | Size: 128 KiB |
After Width: | Height: | Size: 113 KiB |
After Width: | Height: | Size: 58 KiB |
After Width: | Height: | Size: 48 KiB |
After Width: | Height: | Size: 71 KiB |
After Width: | Height: | Size: 87 KiB |
After Width: | Height: | Size: 78 KiB |
After Width: | Height: | Size: 69 KiB |
After Width: | Height: | Size: 80 KiB |
@ -58,7 +58,7 @@ const Button: React.FC<IButton> = ({
|
||||
styles += link ? ' text-white hover:text-white' : ' bg-white text-black';
|
||||
break;
|
||||
default:
|
||||
styles += link ? ' text-black hover:text-grey-800' : ' bg-transparent text-black hover:text-grey-800';
|
||||
styles += link ? ' text-black hover:text-grey-800' : ' text-black hover:bg-grey-200';
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,7 @@ export interface ConfirmationModalProps {
|
||||
onOk?: (modal?: {
|
||||
remove: () => void;
|
||||
}) => void;
|
||||
customFooter?: React.ReactNode;
|
||||
customFooter?: boolean | React.ReactNode;
|
||||
}
|
||||
|
||||
const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
|
||||
@ -25,7 +25,7 @@ const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
|
||||
okColor = 'black',
|
||||
onCancel,
|
||||
onOk,
|
||||
customFooter
|
||||
customFooter = false
|
||||
}) => {
|
||||
const modal = useModal();
|
||||
const [taskState, setTaskState] = useState<'running' | ''>('');
|
||||
@ -33,7 +33,7 @@ const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
|
||||
<Modal
|
||||
backDropClick={false}
|
||||
cancelLabel={cancelLabel}
|
||||
customFooter={customFooter}
|
||||
footer={customFooter}
|
||||
okColor={okColor}
|
||||
okLabel={taskState === 'running' ? okRunningLabel : okLabel}
|
||||
size={540}
|
||||
|
@ -25,6 +25,12 @@ export const Small: Story = {
|
||||
}
|
||||
};
|
||||
|
||||
export const Large: Story = {
|
||||
args: {
|
||||
size: 'lg'
|
||||
}
|
||||
};
|
||||
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
toolbarLeft: <></>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
export type DesktopChromeHeaderSize = 'sm' | 'md';
|
||||
export type DesktopChromeHeaderSize = 'sm' | 'md' | 'lg';
|
||||
|
||||
interface DesktopChromeHeaderProps {
|
||||
size?: DesktopChromeHeaderSize;
|
||||
@ -17,14 +17,32 @@ const DesktopChromeHeader: React.FC<DesktopChromeHeaderProps> = ({
|
||||
toolbarRight = '',
|
||||
toolbarClasses = ''
|
||||
}) => {
|
||||
let containerSize = size === 'sm' ? 'min-h-[32px] p-2' : 'min-h-[48px] p-3';
|
||||
let containerSize;
|
||||
|
||||
switch (size) {
|
||||
case 'sm':
|
||||
containerSize = 'h-[32px] p-2';
|
||||
break;
|
||||
|
||||
case 'md':
|
||||
containerSize = 'h-[48px] px-3 py-5';
|
||||
break;
|
||||
|
||||
case 'lg':
|
||||
containerSize = 'h-[74px] px-3 py-5';
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const trafficLightSize = size === 'sm' ? 'w-[6px] h-[6px]' : 'w-[10px] h-[10px]';
|
||||
const trafficLightWidth = size === 'sm' ? 36 : 56;
|
||||
let trafficLightContainerStyle = size === 'sm' ? 'gap-[5px] ' : 'gap-2 ';
|
||||
trafficLightContainerStyle += `w-[${trafficLightWidth}px]`;
|
||||
|
||||
const trafficLights = (
|
||||
<div className={`absolute left-4 flex h-full items-center ${trafficLightContainerStyle}`}>
|
||||
<div className={`absolute left-5 flex h-full items-center ${trafficLightContainerStyle}`}>
|
||||
<div className={`rounded-full bg-grey-500 ${trafficLightSize}`}></div>
|
||||
<div className={`rounded-full bg-grey-500 ${trafficLightSize}`}></div>
|
||||
<div className={`rounded-full bg-grey-500 ${trafficLightSize}`}></div>
|
||||
@ -34,7 +52,7 @@ const DesktopChromeHeader: React.FC<DesktopChromeHeaderProps> = ({
|
||||
return (
|
||||
<header className={`relative flex items-center justify-center bg-grey-50 ${containerSize} ${toolbarClasses}`}>
|
||||
{toolbarLeft ?
|
||||
<div className='absolute left-4 flex h-full items-center'>
|
||||
<div className='absolute left-5 flex h-full items-center'>
|
||||
{toolbarLeft}
|
||||
</div>
|
||||
:
|
||||
@ -48,7 +66,7 @@ const DesktopChromeHeader: React.FC<DesktopChromeHeaderProps> = ({
|
||||
}
|
||||
</div>
|
||||
{toolbarRight &&
|
||||
<div className='absolute right-4 flex h-full items-center'>
|
||||
<div className='absolute right-5 flex h-full items-center'>
|
||||
{toolbarRight}
|
||||
</div>
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ export interface ModalProps {
|
||||
okColor?: string;
|
||||
cancelLabel?: string;
|
||||
leftButtonLabel?: string;
|
||||
customFooter?: React.ReactNode;
|
||||
footer?: boolean | React.ReactNode;
|
||||
noPadding?: boolean;
|
||||
onOk?: () => void;
|
||||
onCancel?: () => void;
|
||||
@ -37,7 +37,7 @@ const Modal: React.FC<ModalProps> = ({
|
||||
title,
|
||||
okLabel = 'OK',
|
||||
cancelLabel = 'Cancel',
|
||||
customFooter,
|
||||
footer,
|
||||
leftButtonLabel,
|
||||
noPadding = false,
|
||||
onOk,
|
||||
@ -52,7 +52,7 @@ const Modal: React.FC<ModalProps> = ({
|
||||
|
||||
let buttons: IButton[] = [];
|
||||
|
||||
if (!customFooter) {
|
||||
if (!footer) {
|
||||
if (cancelLabel) {
|
||||
buttons.push({
|
||||
key: 'cancel-modal',
|
||||
@ -135,10 +135,10 @@ const Modal: React.FC<ModalProps> = ({
|
||||
|
||||
let contentClasses = clsx(
|
||||
padding,
|
||||
size === 'full' && 'h-full'
|
||||
((size === 'full' || size === 'bleed') && 'grow')
|
||||
);
|
||||
|
||||
if (!customFooter) {
|
||||
if (!footer) {
|
||||
contentClasses += ' pb-0 ';
|
||||
}
|
||||
|
||||
@ -165,15 +165,17 @@ const Modal: React.FC<ModalProps> = ({
|
||||
</div>
|
||||
);
|
||||
|
||||
const footer = (stickyFooter ?
|
||||
<StickyFooter height={84}>
|
||||
{footerContent}
|
||||
</StickyFooter>
|
||||
:
|
||||
<>
|
||||
{footerContent}
|
||||
</>
|
||||
);
|
||||
if (footer === undefined) {
|
||||
footer = (stickyFooter ?
|
||||
<StickyFooter height={84}>
|
||||
{footerContent}
|
||||
</StickyFooter>
|
||||
:
|
||||
<>
|
||||
{footerContent}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={backdropClasses} id='modal-backdrop' onClick={handleBackdropClick}>
|
||||
@ -188,9 +190,7 @@ const Modal: React.FC<ModalProps> = ({
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
{customFooter ? customFooter :
|
||||
footer
|
||||
}
|
||||
{footer}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
@ -4,7 +4,7 @@ import Heading from './Heading';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import PreviewModal from './PreviewModal';
|
||||
import PreviewModalContainer from './PreviewModalContainer';
|
||||
import {SelectOption} from './Select';
|
||||
import {Tab} from './TabView';
|
||||
|
||||
const meta = {
|
||||
title: 'Global / Modal / Preview Modal',
|
||||
@ -14,18 +14,24 @@ const meta = {
|
||||
<NiceModal.Provider>
|
||||
<PreviewModalContainer {...context.args} />
|
||||
</NiceModal.Provider>
|
||||
)]
|
||||
)],
|
||||
argTypes: {
|
||||
sidebar: {control: 'text'},
|
||||
preview: {control: 'text'},
|
||||
sidebarButtons: {control: 'text'},
|
||||
sidebarHeader: {control: 'text'}
|
||||
}
|
||||
} satisfies Meta<typeof PreviewModal>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof PreviewModal>;
|
||||
|
||||
const selectOptions: SelectOption[] = [
|
||||
{value: 'homepage', label: 'Homepage'},
|
||||
{value: 'post', label: 'Post'},
|
||||
{value: 'page', label: 'Page'},
|
||||
{value: 'tag-archive', label: 'Tag archive'},
|
||||
{value: 'author-archive', label: 'Author archive'}
|
||||
const previewURLs: Tab[] = [
|
||||
{id: 'homepage', title: 'Homepage'},
|
||||
{id: 'post', title: 'Post'},
|
||||
{id: 'page', title: 'Page'},
|
||||
{id: 'tag-archive', title: 'Tag archive'},
|
||||
{id: 'author-archive', title: 'Author archive'}
|
||||
];
|
||||
|
||||
export const Default: Story = {
|
||||
@ -41,7 +47,10 @@ export const Default: Story = {
|
||||
Sidebar area
|
||||
</div>
|
||||
),
|
||||
previewToolbarURLs: selectOptions
|
||||
previewToolbarTabs: previewURLs,
|
||||
onSelectURL: (id: string) => {
|
||||
alert(id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -81,3 +90,10 @@ export const CustomSidebarHeader: Story = {
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
export const FullBleed: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
size: 'bleed'
|
||||
}
|
||||
};
|
@ -2,17 +2,18 @@ import ButtonGroup from './ButtonGroup';
|
||||
import DesktopChromeHeader from './DesktopChromeHeader';
|
||||
import Heading from './Heading';
|
||||
import MobileChrome from './MobileChrome';
|
||||
import Modal from './Modal';
|
||||
import Modal, {ModalSize} from './Modal';
|
||||
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||
import React, {useState} from 'react';
|
||||
import URLSelect from './URLSelect';
|
||||
import Select, {SelectOption} from './Select';
|
||||
import TabView, {Tab} from './TabView';
|
||||
import {IButton} from './Button';
|
||||
import {SelectOption} from './Select';
|
||||
|
||||
export interface PreviewModalProps {
|
||||
testId?: string;
|
||||
title?: string;
|
||||
sidebar?: React.ReactNode;
|
||||
size?: ModalSize;
|
||||
sidebar?: boolean | React.ReactNode;
|
||||
preview?: React.ReactNode;
|
||||
cancelLabel?: string;
|
||||
okLabel?: string;
|
||||
@ -21,6 +22,8 @@ export interface PreviewModalProps {
|
||||
previewToolbar?: boolean;
|
||||
previewToolbarURLs?: SelectOption[];
|
||||
selectedURL?: string;
|
||||
previewToolbarTabs?: Tab[];
|
||||
defaultTab?: string;
|
||||
sidebarButtons?: React.ReactNode;
|
||||
sidebarHeader?: React.ReactNode;
|
||||
sidebarPadding?: boolean;
|
||||
@ -35,7 +38,8 @@ export interface PreviewModalProps {
|
||||
export const PreviewModalContent: React.FC<PreviewModalProps> = ({
|
||||
testId,
|
||||
title,
|
||||
sidebar,
|
||||
size = 'full',
|
||||
sidebar = '',
|
||||
preview,
|
||||
cancelLabel = 'Cancel',
|
||||
okLabel = 'OK',
|
||||
@ -43,6 +47,8 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
|
||||
previewToolbar = true,
|
||||
previewToolbarURLs,
|
||||
selectedURL,
|
||||
previewToolbarTabs,
|
||||
defaultTab,
|
||||
buttonsDisabled,
|
||||
sidebarButtons,
|
||||
sidebarHeader,
|
||||
@ -68,11 +74,19 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
|
||||
}
|
||||
|
||||
if (previewToolbar) {
|
||||
let toolbarCenter = (<></>);
|
||||
let toolbarLeft = (<></>);
|
||||
if (previewToolbarURLs) {
|
||||
toolbarCenter = (
|
||||
<URLSelect defaultSelectedOption={selectedURL} options={previewToolbarURLs!} onSelect={onSelectURL ? onSelectURL : () => {}} />
|
||||
toolbarLeft = (
|
||||
<Select defaultSelectedOption={selectedURL} options={previewToolbarURLs!} onSelect={onSelectURL ? onSelectURL : () => {}} />
|
||||
);
|
||||
} else if (previewToolbarTabs) {
|
||||
toolbarLeft = <TabView
|
||||
border={false}
|
||||
defaultSelected={defaultTab}
|
||||
tabs={previewToolbarTabs}
|
||||
width='wide'
|
||||
onTabChange={onSelectURL}
|
||||
/>;
|
||||
}
|
||||
|
||||
const unSelectedIconColorClass = 'text-grey-500';
|
||||
@ -103,13 +117,12 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
|
||||
|
||||
preview = (
|
||||
<>
|
||||
<div className='bg-grey-50 p-2 pl-3'>
|
||||
<DesktopChromeHeader
|
||||
toolbarCenter={toolbarCenter}
|
||||
toolbarLeft={view === 'mobile' ? <></> : ''}
|
||||
toolbarRight={toolbarRight}
|
||||
/>
|
||||
</div>
|
||||
<DesktopChromeHeader
|
||||
size='lg'
|
||||
toolbarCenter={<></>}
|
||||
toolbarLeft={toolbarLeft}
|
||||
toolbarRight={toolbarRight}
|
||||
/>
|
||||
<div className='flex h-full grow items-center justify-center bg-grey-50 text-sm text-grey-400'>
|
||||
{preview}
|
||||
</div>
|
||||
@ -139,9 +152,9 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
|
||||
|
||||
return (
|
||||
<Modal
|
||||
customFooter={(<></>)}
|
||||
footer={false}
|
||||
noPadding={true}
|
||||
size='full'
|
||||
size={size}
|
||||
testId={testId}
|
||||
title=''
|
||||
>
|
||||
@ -149,19 +162,19 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
|
||||
<div className='flex grow flex-col'>
|
||||
{preview}
|
||||
</div>
|
||||
<div className='flex h-full basis-[400px] flex-col gap-3 border-l border-grey-100'>
|
||||
{sidebarHeader ? sidebarHeader : (
|
||||
<div className='flex justify-between gap-3 px-7 pt-5'>
|
||||
<>
|
||||
{sidebar &&
|
||||
<div className='flex h-full basis-[400px] flex-col border-l border-grey-100'>
|
||||
{sidebarHeader ? sidebarHeader : (
|
||||
<div className='flex max-h-[74px] items-start justify-between gap-3 px-7 py-5'>
|
||||
<Heading className='mt-1' level={4}>{title}</Heading>
|
||||
{sidebarButtons ? sidebarButtons : <ButtonGroup buttons={buttons} /> }
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
<div className={`grow ${sidebarPadding && 'p-7 pt-0'} flex flex-col justify-between overflow-y-auto`}>
|
||||
{sidebar}
|
||||
</div>
|
||||
)}
|
||||
<div className={`grow ${sidebarPadding && 'p-7'} flex flex-col justify-between overflow-y-auto`}>
|
||||
{sidebar}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
@ -71,7 +71,7 @@ const Select: React.FC<SelectProps> = ({
|
||||
let selectClasses = '';
|
||||
if (!unstyled) {
|
||||
selectClasses = clsx(
|
||||
'h-10 w-full cursor-pointer appearance-none border-b py-2 outline-none',
|
||||
'h-10 w-full cursor-pointer appearance-none border-b py-2 pr-5 outline-none',
|
||||
!clearBg && 'bg-grey-75 px-[10px]',
|
||||
error ? 'border-red' : 'border-grey-500 hover:border-grey-700 focus:border-black',
|
||||
(title && !clearBg) && 'mt-2'
|
||||
|
@ -28,4 +28,11 @@ export const DefaultSelected: Story = {
|
||||
tabs: tabs,
|
||||
defaultSelected: 'tab-2'
|
||||
}
|
||||
};
|
||||
|
||||
export const NoBorder: Story = {
|
||||
args: {
|
||||
tabs: tabs,
|
||||
border: false
|
||||
}
|
||||
};
|
@ -2,8 +2,12 @@ import React, {useState} from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export type Tab = {
|
||||
id: string,
|
||||
id: string;
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* Optional, so you can just use the tabs to other views
|
||||
*/
|
||||
contents?: React.ReactNode;
|
||||
}
|
||||
|
||||
@ -11,9 +15,17 @@ interface TabViewProps {
|
||||
tabs: Tab[];
|
||||
onTabChange?: (id: string) => void;
|
||||
defaultSelected?: string;
|
||||
border?:boolean;
|
||||
width?: 'narrow' | 'normal' | 'wide';
|
||||
}
|
||||
|
||||
const TabView: React.FC<TabViewProps> = ({tabs, onTabChange, defaultSelected}) => {
|
||||
const TabView: React.FC<TabViewProps> = ({
|
||||
tabs,
|
||||
onTabChange,
|
||||
defaultSelected,
|
||||
border = true,
|
||||
width = 'normal'
|
||||
}) => {
|
||||
if (tabs.length !== 0 && defaultSelected === undefined) {
|
||||
defaultSelected = tabs[0].id;
|
||||
}
|
||||
@ -30,16 +42,26 @@ const TabView: React.FC<TabViewProps> = ({tabs, onTabChange, defaultSelected}) =
|
||||
onTabChange?.(newTab);
|
||||
};
|
||||
|
||||
const containerClasses = clsx(
|
||||
'flex',
|
||||
width === 'narrow' && 'gap-3',
|
||||
width === 'normal' && 'gap-5',
|
||||
width === 'wide' && 'gap-7',
|
||||
border && 'border-b border-grey-300'
|
||||
);
|
||||
|
||||
return (
|
||||
<section>
|
||||
<div className='flex gap-5 border-b border-grey-300' role='tablist'>
|
||||
<div className={containerClasses} role='tablist'>
|
||||
{tabs.map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
aria-selected={selectedTab === tab.id}
|
||||
className={clsx(
|
||||
'-m-b-px cursor-pointer appearance-none border-b-[3px] py-1 text-sm transition-all after:invisible after:block after:h-px after:overflow-hidden after:font-bold after:text-transparent after:content-[attr(title)]',
|
||||
selectedTab === tab.id ? 'border-black font-bold' : 'border-transparent hover:border-grey-500'
|
||||
'-m-b-px cursor-pointer appearance-none py-1 text-sm transition-all after:invisible after:block after:h-px after:overflow-hidden after:font-bold after:text-transparent after:content-[attr(title)]',
|
||||
border && 'border-b-[3px]',
|
||||
selectedTab === tab.id && border ? 'border-black' : 'border-transparent hover:border-grey-500',
|
||||
selectedTab === tab.id && 'font-bold'
|
||||
)}
|
||||
id={tab.id}
|
||||
role='tab'
|
||||
@ -49,11 +71,17 @@ const TabView: React.FC<TabViewProps> = ({tabs, onTabChange, defaultSelected}) =
|
||||
>{tab.title}</button>
|
||||
))}
|
||||
</div>
|
||||
{tabs.map(tab => (
|
||||
<div key={tab.id} className={`${selectedTab === tab.id ? 'block' : 'hidden'}`} role='tabpanel'>
|
||||
<div>{tab.contents}</div>
|
||||
</div>
|
||||
))}
|
||||
{tabs.map((tab) => {
|
||||
return (
|
||||
<>
|
||||
{tab.contents &&
|
||||
<div key={tab.id} className={`${selectedTab === tab.id ? 'block' : 'hidden'}`} role='tabpanel'>
|
||||
<div>{tab.contents}</div>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
import BrandSettings, {BrandSettingValues} from './designAndBranding/BrandSettings';
|
||||
import ChangeThemeModal from './designAndBranding/ChangeThemeModal';
|
||||
import ConfirmationModal from '../../../admin-x-ds/global/ConfirmationModal';
|
||||
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||
import React, {useContext, useEffect, useState} from 'react';
|
||||
@ -9,7 +10,6 @@ import ThemeSettings from './designAndBranding/ThemeSettings';
|
||||
import useForm from '../../../hooks/useForm';
|
||||
import {CustomThemeSetting, Post, Setting, SettingValue, SiteData} from '../../../types/api';
|
||||
import {PreviewModalContent} from '../../../admin-x-ds/global/PreviewModal';
|
||||
import {SelectOption} from '../../../admin-x-ds/global/Select';
|
||||
import {ServicesContext} from '../../providers/ServiceProvider';
|
||||
import {SettingsContext} from '../../providers/SettingsProvider';
|
||||
import {getSettingValues} from '../../../utils/helpers';
|
||||
@ -20,7 +20,15 @@ const Sidebar: React.FC<{
|
||||
themeSettingSections: Array<{id: string, title: string, settings: CustomThemeSetting[]}>
|
||||
updateThemeSetting: (updated: CustomThemeSetting) => void
|
||||
onTabChange: (id: string) => void
|
||||
}> = ({brandSettings,updateBrandSetting,themeSettingSections,updateThemeSetting,onTabChange}) => {
|
||||
onChangeTheme: () => void
|
||||
}> = ({
|
||||
brandSettings,
|
||||
updateBrandSetting,
|
||||
themeSettingSections,
|
||||
updateThemeSetting,
|
||||
onTabChange,
|
||||
onChangeTheme
|
||||
}) => {
|
||||
const tabs: Tab[] = [
|
||||
{
|
||||
id: 'brand',
|
||||
@ -40,7 +48,7 @@ const Sidebar: React.FC<{
|
||||
<TabView tabs={tabs} onTabChange={onTabChange} />
|
||||
</div>
|
||||
<StickyFooter>
|
||||
<button className='flex w-full cursor-pointer flex-col px-7' type='button' onClick={() => {}}>
|
||||
<button className='m m-3 flex w-full cursor-pointer flex-col rounded p-4 transition-all hover:bg-grey-100' type='button' onClick={onChangeTheme}>
|
||||
<strong>Change theme</strong>
|
||||
<span className='text-sm text-grey-600'>Casper</span>
|
||||
</button>
|
||||
@ -63,7 +71,7 @@ const DesignModal: React.FC = () => {
|
||||
const {settings, siteData, saveSettings} = useContext(SettingsContext);
|
||||
const [themeSettings, setThemeSettings] = useState<Array<CustomThemeSetting>>([]);
|
||||
const [latestPost, setLatestPost] = useState<Post | null>(null);
|
||||
const [selectedUrl, setSelectedUrl] = useState(getHomepageUrl(siteData!));
|
||||
const [selectedPreviewTab, setSelectedPreviewTab] = useState('home');
|
||||
|
||||
useEffect(() => {
|
||||
api.customThemeSettings.browse().then((response) => {
|
||||
@ -130,50 +138,78 @@ const DesignModal: React.FC = () => {
|
||||
title: id === 'site-wide' ? 'Site wide' : (id === 'homepage' ? 'Homepage' : 'Post')
|
||||
}));
|
||||
|
||||
const urlOptions: SelectOption[] = [
|
||||
{value: getHomepageUrl(siteData!), label: 'Homepage'},
|
||||
latestPost && {value: latestPost.url, label: 'Post'}
|
||||
].filter((option): option is SelectOption => Boolean(option));
|
||||
// const urlOptions: SelectOption[] = [
|
||||
// {value: getHomepageUrl(siteData!), label: 'Homepage'},
|
||||
// latestPost && {value: latestPost.url, label: 'Post'}
|
||||
// ].filter((option): option is SelectOption => Boolean(option));
|
||||
|
||||
const onSelectURL = (url: string) => {
|
||||
setSelectedUrl(url);
|
||||
let previewTabs: Tab[] = [];
|
||||
if (latestPost) {
|
||||
previewTabs = [
|
||||
{id: 'homepage', title: 'Homepage'},
|
||||
{id: 'post', title: 'Post'}
|
||||
];
|
||||
}
|
||||
|
||||
const onSelectURL = (id: string) => {
|
||||
if (previewTabs.length) {
|
||||
setSelectedPreviewTab(id);
|
||||
}
|
||||
};
|
||||
|
||||
const onTabChange = (id: string) => {
|
||||
if (id === 'post' && latestPost) {
|
||||
setSelectedUrl(latestPost.url);
|
||||
setSelectedPreviewTab('post');
|
||||
} else {
|
||||
setSelectedUrl(getHomepageUrl(siteData!));
|
||||
setSelectedPreviewTab('home');
|
||||
}
|
||||
};
|
||||
|
||||
return <PreviewModalContent
|
||||
buttonsDisabled={saveState === 'saving'}
|
||||
cancelLabel='Close'
|
||||
okLabel={saveState === 'saved' ? 'Saved' : (saveState === 'saving' ? 'Saving ...' : 'Save')}
|
||||
preview={
|
||||
<ThemePreview
|
||||
settings={{
|
||||
description,
|
||||
accentColor,
|
||||
icon,
|
||||
logo,
|
||||
coverImage,
|
||||
themeSettings
|
||||
}}
|
||||
url={selectedUrl}
|
||||
/>
|
||||
}
|
||||
previewToolbarURLs={urlOptions}
|
||||
selectedURL={selectedUrl}
|
||||
sidebar={<Sidebar
|
||||
const showThemeModal = () => {
|
||||
NiceModal.show(ChangeThemeModal);
|
||||
};
|
||||
|
||||
let selectedTabURL = getHomepageUrl(siteData!);
|
||||
switch (selectedPreviewTab) {
|
||||
case 'homepage':
|
||||
selectedTabURL = getHomepageUrl(siteData!);
|
||||
break;
|
||||
case 'post':
|
||||
selectedTabURL = latestPost!.url;
|
||||
break;
|
||||
}
|
||||
|
||||
const previewContent =
|
||||
<ThemePreview
|
||||
settings={{
|
||||
description,
|
||||
accentColor,
|
||||
icon,
|
||||
logo,
|
||||
coverImage,
|
||||
themeSettings
|
||||
}}
|
||||
url={selectedTabURL}
|
||||
/>;
|
||||
const sidebarContent =
|
||||
<Sidebar
|
||||
brandSettings={{description, accentColor, icon, logo, coverImage}}
|
||||
themeSettingSections={themeSettingSections}
|
||||
updateBrandSetting={updateBrandSetting}
|
||||
updateThemeSetting={updateThemeSetting}
|
||||
onChangeTheme={showThemeModal}
|
||||
onTabChange={onTabChange}
|
||||
/>}
|
||||
/>;
|
||||
|
||||
return <PreviewModalContent
|
||||
buttonsDisabled={saveState === 'saving'}
|
||||
defaultTab='homepage'
|
||||
okLabel={saveState === 'saved' ? 'Saved' : (saveState === 'saving' ? 'Saving...' : 'Save and close')}
|
||||
preview={previewContent}
|
||||
previewToolbarTabs={previewTabs}
|
||||
sidebar={sidebarContent}
|
||||
sidebarPadding={false}
|
||||
size='full'
|
||||
testId='design-modal'
|
||||
title='Design'
|
||||
onCancel={() => {
|
||||
@ -200,7 +236,7 @@ const DesignModal: React.FC = () => {
|
||||
}}
|
||||
onOk={async () => {
|
||||
await handleSave();
|
||||
// modal.remove();
|
||||
modal.remove();
|
||||
}}
|
||||
onSelectURL={onSelectURL}
|
||||
/>;
|
||||
|
@ -1,12 +1,14 @@
|
||||
import DesignSetting from './DesignSetting';
|
||||
import React from 'react';
|
||||
import SettingSection from '../../../admin-x-ds/settings/SettingSection';
|
||||
import Theme from './Theme';
|
||||
|
||||
const SiteSettings: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<SettingSection title="Site">
|
||||
<DesignSetting />
|
||||
<Theme />
|
||||
</SettingSection>
|
||||
</>
|
||||
);
|
||||
|
@ -0,0 +1,21 @@
|
||||
import Button from '../../../admin-x-ds/global/Button';
|
||||
import ChangeThemeModal from './designAndBranding/ChangeThemeModal';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import React from 'react';
|
||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
|
||||
const Theme: React.FC = () => {
|
||||
return (
|
||||
<SettingGroup
|
||||
customButtons={<Button color='green' label='Manage themes' link onClick={() => {
|
||||
NiceModal.show(ChangeThemeModal);
|
||||
}}/>}
|
||||
description="Change or upload themes"
|
||||
navid='theme'
|
||||
testId='theme'
|
||||
title="Theme"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Theme;
|
@ -0,0 +1,15 @@
|
||||
import Heading from '../../../../admin-x-ds/global/Heading';
|
||||
import React from 'react';
|
||||
|
||||
const AdvancedThemeSettings: React.FC = () => {
|
||||
return (
|
||||
<div className='p-[8vmin] pt-5'>
|
||||
<Heading>Installed themes</Heading>
|
||||
<div className='mt-5'>
|
||||
List of installed themes
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdvancedThemeSettings;
|
@ -0,0 +1,114 @@
|
||||
import AdvancedThemeSettings from './AdvancedThemeSettings';
|
||||
import Button from '../../../../admin-x-ds/global/Button';
|
||||
import ButtonGroup from '../../../../admin-x-ds/global/ButtonGroup';
|
||||
import Modal from '../../../../admin-x-ds/global/Modal';
|
||||
import NewThemePreview from './NewThemePreview';
|
||||
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||
import OfficialThemes from './OfficialThemes';
|
||||
import {useState} from 'react';
|
||||
|
||||
const ChangeThemeModal = NiceModal.create(() => {
|
||||
const [currentTab, setCurrentTab] = useState<'official-themes' | 'advanced'>('official-themes');
|
||||
const [selectedTheme, setSelectedTheme] = useState('');
|
||||
|
||||
const modal = useModal();
|
||||
|
||||
const onSelectTheme = (theme: string) => {
|
||||
setSelectedTheme(theme);
|
||||
};
|
||||
|
||||
let content;
|
||||
switch (currentTab) {
|
||||
case 'official-themes':
|
||||
if (selectedTheme) {
|
||||
content = <NewThemePreview selectedTheme={selectedTheme} />;
|
||||
} else {
|
||||
content = <OfficialThemes onSelectTheme={onSelectTheme} />;
|
||||
}
|
||||
break;
|
||||
case 'advanced':
|
||||
content = <AdvancedThemeSettings />;
|
||||
break;
|
||||
}
|
||||
|
||||
let toolBar;
|
||||
if (selectedTheme) {
|
||||
toolBar =
|
||||
<div className='sticky top-0 flex justify-between gap-3 bg-white p-5 px-7'>
|
||||
<div className='flex w-[33%] items-center gap-2'>
|
||||
<button
|
||||
className={`text-sm`}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setCurrentTab('official-themes');
|
||||
setSelectedTheme('');
|
||||
}}>
|
||||
Official themes
|
||||
</button>
|
||||
→
|
||||
<span className='text-sm font-bold'>{selectedTheme}</span>
|
||||
</div>
|
||||
<div className='flex w-[33%] justify-end gap-8'>
|
||||
<ButtonGroup
|
||||
buttons={[
|
||||
{icon: 'laptop', link: true, size: 'sm'},
|
||||
{icon: 'mobile', iconColorClass: 'text-grey-500', link: true, size: 'sm'}
|
||||
]}
|
||||
/>
|
||||
<Button color='green' label={`Install ${selectedTheme}`} />
|
||||
</div>
|
||||
</div>;
|
||||
} else {
|
||||
toolBar =
|
||||
<div className='sticky top-0 flex justify-between gap-3 bg-white p-5 px-7'>
|
||||
<div className='flex gap-8'>
|
||||
<button
|
||||
className={`text-sm ${currentTab === 'official-themes' && 'font-bold'}`}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setCurrentTab('official-themes');
|
||||
setSelectedTheme('');
|
||||
}}>
|
||||
Official themes
|
||||
</button>
|
||||
<button
|
||||
className={`text-sm ${currentTab === 'advanced' && 'font-bold'}`}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setCurrentTab('advanced');
|
||||
}}>
|
||||
Installed
|
||||
</button>
|
||||
</div>
|
||||
<ButtonGroup
|
||||
buttons={[
|
||||
{label: 'Upload theme', onClick: () => {
|
||||
alert('Upload');
|
||||
}},
|
||||
{label: 'OK', color: 'black', className: 'min-w-[75px]', onClick: () => {
|
||||
modal.remove();
|
||||
}}
|
||||
]}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
cancelLabel=''
|
||||
footer={false}
|
||||
noPadding={true}
|
||||
size='full'
|
||||
title=''
|
||||
>
|
||||
<div className='flex h-full justify-between'>
|
||||
<div className='grow'>
|
||||
{toolBar}
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
||||
export default ChangeThemeModal;
|
@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
|
||||
const NewThemePreview: React.FC<{
|
||||
selectedTheme?: string;
|
||||
}> = ({
|
||||
selectedTheme
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
Preview {selectedTheme}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewThemePreview;
|
@ -0,0 +1,149 @@
|
||||
import Heading from '../../../../admin-x-ds/global/Heading';
|
||||
import React from 'react';
|
||||
|
||||
const OfficialThemes: React.FC<{
|
||||
onSelectTheme?: (theme: string) => void;
|
||||
}> = ({
|
||||
onSelectTheme
|
||||
}) => {
|
||||
const officialThemes = [{
|
||||
name: 'Casper',
|
||||
category: 'Blog',
|
||||
previewUrl: 'https://demo.ghost.io/',
|
||||
ref: 'default',
|
||||
image: 'assets/images/themes/Casper.png'
|
||||
}, {
|
||||
name: 'Headline',
|
||||
category: 'News',
|
||||
url: 'https://github.com/TryGhost/Headline',
|
||||
previewUrl: 'https://headline.ghost.io',
|
||||
ref: 'TryGhost/Headline',
|
||||
image: 'assets/images/themes/Headline.png'
|
||||
}, {
|
||||
name: 'Edition',
|
||||
category: 'Newsletter',
|
||||
url: 'https://github.com/TryGhost/Edition',
|
||||
previewUrl: 'https://edition.ghost.io/',
|
||||
ref: 'TryGhost/Edition',
|
||||
image: 'assets/images/themes/Edition.png'
|
||||
}, {
|
||||
name: 'Solo',
|
||||
category: 'Blog',
|
||||
url: 'https://github.com/TryGhost/Solo',
|
||||
previewUrl: 'https://solo.ghost.io',
|
||||
ref: 'TryGhost/Solo',
|
||||
image: 'assets/images/themes/Solo.png'
|
||||
}, {
|
||||
name: 'Taste',
|
||||
category: 'Blog',
|
||||
url: 'https://github.com/TryGhost/Taste',
|
||||
previewUrl: 'https://taste.ghost.io',
|
||||
ref: 'TryGhost/Taste',
|
||||
image: 'assets/images/themes/Taste.png'
|
||||
}, {
|
||||
name: 'Episode',
|
||||
category: 'Podcast',
|
||||
url: 'https://github.com/TryGhost/Episode',
|
||||
previewUrl: 'https://episode.ghost.io',
|
||||
ref: 'TryGhost/Episode',
|
||||
image: 'assets/images/themes/Episode.png'
|
||||
}, {
|
||||
name: 'Digest',
|
||||
category: 'Newsletter',
|
||||
url: 'https://github.com/TryGhost/Digest',
|
||||
previewUrl: 'https://digest.ghost.io/',
|
||||
ref: 'TryGhost/Digest',
|
||||
image: 'assets/images/themes/Digest.png'
|
||||
}, {
|
||||
name: 'Bulletin',
|
||||
category: 'Newsletter',
|
||||
url: 'https://github.com/TryGhost/Bulletin',
|
||||
previewUrl: 'https://bulletin.ghost.io/',
|
||||
ref: 'TryGhost/Bulletin',
|
||||
image: 'assets/images/themes/Bulletin.png'
|
||||
}, {
|
||||
name: 'Alto',
|
||||
category: 'Blog',
|
||||
url: 'https://github.com/TryGhost/Alto',
|
||||
previewUrl: 'https://alto.ghost.io',
|
||||
ref: 'TryGhost/Alto',
|
||||
image: 'assets/images/themes/Alto.png'
|
||||
}, {
|
||||
name: 'Dope',
|
||||
category: 'Magazine',
|
||||
url: 'https://github.com/TryGhost/Dope',
|
||||
previewUrl: 'https://dope.ghost.io',
|
||||
ref: 'TryGhost/Dope',
|
||||
image: 'assets/images/themes/Dope.png'
|
||||
}, {
|
||||
name: 'Wave',
|
||||
category: 'Podcast',
|
||||
url: 'https://github.com/TryGhost/Wave',
|
||||
previewUrl: 'https://wave.ghost.io',
|
||||
ref: 'TryGhost/Wave',
|
||||
image: 'assets/images/themes/Wave.png'
|
||||
}, {
|
||||
name: 'Edge',
|
||||
category: 'Photography',
|
||||
url: 'https://github.com/TryGhost/Edge',
|
||||
previewUrl: 'https://edge.ghost.io',
|
||||
ref: 'TryGhost/Edge',
|
||||
image: 'assets/images/themes/Edge.png'
|
||||
}, {
|
||||
name: 'Dawn',
|
||||
category: 'Newsletter',
|
||||
url: 'https://github.com/TryGhost/Dawn',
|
||||
previewUrl: 'https://dawn.ghost.io/',
|
||||
ref: 'TryGhost/Dawn',
|
||||
image: 'assets/images/themes/Dawn.png'
|
||||
}, {
|
||||
name: 'Ease',
|
||||
category: 'Documentation',
|
||||
url: 'https://github.com/TryGhost/Ease',
|
||||
previewUrl: 'https://ease.ghost.io',
|
||||
ref: 'TryGhost/Ease',
|
||||
image: 'assets/images/themes/Ease.png'
|
||||
}, {
|
||||
name: 'Ruby',
|
||||
category: 'Magazine',
|
||||
url: 'https://github.com/TryGhost/Ruby',
|
||||
previewUrl: 'https://ruby.ghost.io',
|
||||
ref: 'TryGhost/Ruby',
|
||||
image: 'assets/images/themes/Ruby.png'
|
||||
}, {
|
||||
name: 'London',
|
||||
category: 'Photography',
|
||||
url: 'https://github.com/TryGhost/London',
|
||||
previewUrl: 'https://london.ghost.io',
|
||||
ref: 'TryGhost/London',
|
||||
image: 'assets/images/themes/London.png'
|
||||
}, {
|
||||
name: 'Journal',
|
||||
category: 'Newsletter',
|
||||
url: 'https://github.com/TryGhost/Journal',
|
||||
previewUrl: 'https://journal.ghost.io/',
|
||||
ref: 'TryGhost/Journal',
|
||||
image: 'assets/images/themes/Journal.png'
|
||||
}];
|
||||
|
||||
return (
|
||||
<div className='p-[8vmin] pt-5'>
|
||||
<Heading>Themes</Heading>
|
||||
<div className='mt-6 grid grid-cols-3 gap-4'>
|
||||
{officialThemes.map((theme) => {
|
||||
return (
|
||||
<div key={theme.name} className='flex cursor-pointer flex-col gap-3' onClick={() => {
|
||||
onSelectTheme?.(theme.name);
|
||||
}}>
|
||||
{/* <img alt={theme.name} src={theme.image}/> */}
|
||||
<div className='h-[420px] w-full bg-grey-100'></div>
|
||||
<span>{theme.name}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OfficialThemes;
|
@ -22,7 +22,7 @@ test.describe('Theme settings', async () => {
|
||||
await modal.getByLabel('Site description').fill('new description');
|
||||
await modal.getByRole('button', {name: 'Save'}).click();
|
||||
|
||||
await expect(modal.getByRole('button', {name: 'Saved'})).toHaveCount(1);
|
||||
await expect(modal).not.toBeVisible();
|
||||
|
||||
expect(lastApiRequest.body).toEqual({
|
||||
settings: [
|
||||
@ -47,7 +47,7 @@ test.describe('Theme settings', async () => {
|
||||
await modal.getByLabel('Navigation layout').selectOption('Logo in the middle');
|
||||
await modal.getByRole('button', {name: 'Save'}).click();
|
||||
|
||||
await expect(modal.getByRole('button', {name: 'Saved'})).toHaveCount(1);
|
||||
await expect(modal).not.toBeVisible();
|
||||
|
||||
expect(lastApiRequest.body).toMatchObject({
|
||||
custom_theme_settings: [
|
||||
|