mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-20 22:42:53 -05:00
Added page header component in AdminX
refs. https://github.com/TryGhost/Team/issues/3432
This commit is contained in:
parent
52af10fef5
commit
15ec9f0d14
5 changed files with 218 additions and 72 deletions
|
@ -15,7 +15,7 @@ const preview: Preview = {
|
||||||
options: {
|
options: {
|
||||||
storySort: {
|
storySort: {
|
||||||
mathod: 'alphabetical',
|
mathod: 'alphabetical',
|
||||||
order: ['Global', ['Chrome', 'Form', 'Modal', 'List', '*'], 'Settings', ['Setting Section', 'Setting Group', '*'], 'Experimental'],
|
order: ['Global', ['Chrome', 'Form', 'Modal', 'Layout', 'List', '*'], 'Settings', ['Setting Section', 'Setting Group', '*'], 'Experimental'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
import type {Meta, StoryObj} from '@storybook/react';
|
||||||
|
|
||||||
|
import PageHeader from './PageHeader';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Global / Layout / Page Header',
|
||||||
|
component: PageHeader,
|
||||||
|
tags: ['autodocs']
|
||||||
|
} satisfies Meta<typeof PageHeader>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof PageHeader>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
left: 'Left content',
|
||||||
|
center: 'Center content',
|
||||||
|
right: 'Right content'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CustomContainer: Story = {
|
||||||
|
args: {
|
||||||
|
left: 'Left content',
|
||||||
|
center: 'Center content',
|
||||||
|
right: 'Right content',
|
||||||
|
containerClassName: 'bg-grey-50'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LeftAndRight: Story = {
|
||||||
|
args: {
|
||||||
|
left: 'Left content',
|
||||||
|
right: 'Right content'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LeftOnly: Story = {
|
||||||
|
args: {
|
||||||
|
left: 'Left content'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CenterOnly: Story = {
|
||||||
|
args: {
|
||||||
|
center: 'Center content'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RightOnly: Story = {
|
||||||
|
args: {
|
||||||
|
right: 'Right content'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CustomContent: Story = {
|
||||||
|
args: {
|
||||||
|
children: (
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<div className='basis-1/4'>This</div>
|
||||||
|
<div className='basis-1/4'>is</div>
|
||||||
|
<div className='basis-1/4'>custom</div>
|
||||||
|
<div className='basis-1/4'>content!</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,77 @@
|
||||||
|
import React from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
interface PageHeaderProps {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use these to specifically place elements on the left | center | right of the header.
|
||||||
|
*/
|
||||||
|
left?: React.ReactNode;
|
||||||
|
center?: React.ReactNode;
|
||||||
|
right?: React.ReactNode;
|
||||||
|
|
||||||
|
sticky?: boolean;
|
||||||
|
containerClassName?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Or you can simply use the whole container to make sure header spacing is consistent. `children` takes precedence over `left`, `center` and `right`.
|
||||||
|
*/
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PageHeader: React.FC<PageHeaderProps> = ({
|
||||||
|
left,
|
||||||
|
center,
|
||||||
|
right,
|
||||||
|
sticky = true,
|
||||||
|
containerClassName,
|
||||||
|
children
|
||||||
|
}) => {
|
||||||
|
const containerClasses = clsx(
|
||||||
|
'h-[74px] p-5 px-7',
|
||||||
|
!children && 'flex items-center justify-between gap-3',
|
||||||
|
sticky && 'sticky top-0',
|
||||||
|
containerClassName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!children) {
|
||||||
|
if (left) {
|
||||||
|
const leftClasses = clsx(
|
||||||
|
'flex flex-auto items-center',
|
||||||
|
(right && center) && 'basis-1/3',
|
||||||
|
((right && !center) || (!right && center)) && 'basis-1/2'
|
||||||
|
);
|
||||||
|
left = <div className={leftClasses}>{left}</div>;
|
||||||
|
}
|
||||||
|
if (center) {
|
||||||
|
const centerClasses = clsx(
|
||||||
|
'flex flex-auto items-center justify-center',
|
||||||
|
(left && right) && 'basis-1/3',
|
||||||
|
((left && !right) || (!left && right)) && 'basis-1/2'
|
||||||
|
);
|
||||||
|
center = <div className={centerClasses}>{center}</div>;
|
||||||
|
}
|
||||||
|
if (right) {
|
||||||
|
const rightClasses = clsx(
|
||||||
|
'flex flex-auto items-center justify-end',
|
||||||
|
(left && center) && 'basis-1/3',
|
||||||
|
((left && !center) || (!left && center)) && 'basis-1/2'
|
||||||
|
);
|
||||||
|
right = <div className={rightClasses}>{right}</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={containerClasses}>
|
||||||
|
{children ? children :
|
||||||
|
<>
|
||||||
|
{left}
|
||||||
|
{center}
|
||||||
|
{right}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PageHeader;
|
|
@ -6,6 +6,7 @@ import Modal from '../../../admin-x-ds/global/modal/Modal';
|
||||||
import NewThemePreview from './theme/ThemePreview';
|
import NewThemePreview from './theme/ThemePreview';
|
||||||
import NiceModal, {NiceModalHandler, useModal} from '@ebay/nice-modal-react';
|
import NiceModal, {NiceModalHandler, useModal} from '@ebay/nice-modal-react';
|
||||||
import OfficialThemes from './theme/OfficialThemes';
|
import OfficialThemes from './theme/OfficialThemes';
|
||||||
|
import PageHeader from '../../../admin-x-ds/global/layout/PageHeader';
|
||||||
import React, {useState} from 'react';
|
import React, {useState} from 'react';
|
||||||
import TabView from '../../../admin-x-ds/global/TabView';
|
import TabView from '../../../admin-x-ds/global/TabView';
|
||||||
import {OfficialTheme} from '../../../models/themes';
|
import {OfficialTheme} from '../../../models/themes';
|
||||||
|
@ -40,85 +41,86 @@ const ThemeToolbar: React.FC<ThemeToolbarProps> = ({
|
||||||
setThemes
|
setThemes
|
||||||
}) => {
|
}) => {
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
|
|
||||||
|
let left, right;
|
||||||
|
|
||||||
if (selectedTheme) {
|
if (selectedTheme) {
|
||||||
const installedTheme = themes.find(theme => theme.name.toLowerCase() === selectedTheme.name.toLowerCase());
|
const installedTheme = themes.find(theme => theme.name.toLowerCase() === selectedTheme.name.toLowerCase());
|
||||||
|
|
||||||
return (
|
left =
|
||||||
<div className='sticky top-0 flex justify-between gap-3 bg-grey-50 p-5 px-7'>
|
<div className='flex w-[33%] items-center gap-2'>
|
||||||
<div className='flex w-[33%] items-center gap-2'>
|
<button
|
||||||
<button
|
className={`text-sm`}
|
||||||
className={`text-sm`}
|
type="button"
|
||||||
type="button"
|
onClick={() => {
|
||||||
onClick={() => {
|
setCurrentTab('official');
|
||||||
setCurrentTab('official');
|
setSelectedTheme(null);
|
||||||
setSelectedTheme(null);
|
}}>
|
||||||
}}>
|
Official themes
|
||||||
Official themes
|
</button>
|
||||||
</button>
|
→
|
||||||
→
|
<span className='text-sm font-bold'>{selectedTheme?.name}</span>
|
||||||
<span className='text-sm font-bold'>{selectedTheme?.name}</span>
|
</div>;
|
||||||
</div>
|
|
||||||
<div className='flex w-[33%] justify-end gap-8'>
|
right =
|
||||||
<ButtonGroup
|
<div className='flex w-[33%] justify-end gap-8'>
|
||||||
buttons={[
|
<ButtonGroup
|
||||||
{icon: 'laptop', link: true, size: 'sm'},
|
buttons={[
|
||||||
{icon: 'mobile', iconColorClass: 'text-grey-500', link: true, size: 'sm'}
|
{icon: 'laptop', link: true, size: 'sm'},
|
||||||
]}
|
{icon: 'mobile', iconColorClass: 'text-grey-500', link: true, size: 'sm'}
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
color='green'
|
|
||||||
disabled={Boolean(installedTheme)}
|
|
||||||
label={installedTheme?.active ? 'Activated' : (installedTheme ? 'Installed' : `Install ${selectedTheme?.name}`)}
|
|
||||||
onClick={async () => {
|
|
||||||
const data = await api.themes.install(selectedTheme.ref);
|
|
||||||
const newlyInstalledTheme = data.themes[0];
|
|
||||||
setThemes([
|
|
||||||
...themes.map(theme => ({...theme, active: false})),
|
|
||||||
newlyInstalledTheme
|
|
||||||
]);
|
|
||||||
showToast({
|
|
||||||
message: `Theme installed - ${newlyInstalledTheme.name}`
|
|
||||||
});
|
|
||||||
setCurrentTab('installed');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<div className='sticky top-0 flex justify-between gap-3 bg-white p-5 px-7'>
|
|
||||||
<TabView
|
|
||||||
border={false}
|
|
||||||
tabs={[
|
|
||||||
{id: 'official', title: 'Official themes'},
|
|
||||||
{id: 'installed', title: 'Installed'}
|
|
||||||
]}
|
]}
|
||||||
onTabChange={(id: string) => {
|
/>
|
||||||
setCurrentTab(id);
|
<Button
|
||||||
|
color='green'
|
||||||
|
disabled={Boolean(installedTheme)}
|
||||||
|
label={installedTheme?.active ? 'Activated' : (installedTheme ? 'Installed' : `Install ${selectedTheme?.name}`)}
|
||||||
|
onClick={async () => {
|
||||||
|
const data = await api.themes.install(selectedTheme.ref);
|
||||||
|
const newlyInstalledTheme = data.themes[0];
|
||||||
|
setThemes([
|
||||||
|
...themes.map(theme => ({...theme, active: false})),
|
||||||
|
newlyInstalledTheme
|
||||||
|
]);
|
||||||
|
showToast({
|
||||||
|
message: `Theme installed - ${newlyInstalledTheme.name}`
|
||||||
|
});
|
||||||
|
setCurrentTab('installed');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</div>;
|
||||||
|
} else {
|
||||||
|
left =
|
||||||
|
<TabView
|
||||||
|
border={false}
|
||||||
|
tabs={[
|
||||||
|
{id: 'official', title: 'Official themes'},
|
||||||
|
{id: 'installed', title: 'Installed'}
|
||||||
|
]}
|
||||||
|
onTabChange={(id: string) => {
|
||||||
|
setCurrentTab(id);
|
||||||
|
}} />;
|
||||||
|
|
||||||
<div className='flex items-center gap-3'>
|
right =
|
||||||
<FileUpload id='theme-uplaod' onUpload={async (file: File) => {
|
<div className='flex items-center gap-3'>
|
||||||
const data = await api.themes.upload({file});
|
<FileUpload id='theme-uplaod' onUpload={async (file: File) => {
|
||||||
const uploadedTheme = data.themes[0];
|
const data = await api.themes.upload({file});
|
||||||
setThemes([...themes, uploadedTheme]);
|
const uploadedTheme = data.themes[0];
|
||||||
showToast({
|
setThemes([...themes, uploadedTheme]);
|
||||||
message: `Theme uploaded - ${uploadedTheme.name}`
|
showToast({
|
||||||
});
|
message: `Theme uploaded - ${uploadedTheme.name}`
|
||||||
}}>Upload theme</FileUpload>
|
});
|
||||||
<Button
|
}}>Upload theme</FileUpload>
|
||||||
className='min-w-[75px]'
|
<Button
|
||||||
color='black'
|
className='min-w-[75px]'
|
||||||
label='OK'
|
color='black'
|
||||||
onClick = {() => {
|
label='OK'
|
||||||
modal.remove();
|
onClick = {() => {
|
||||||
}} />
|
modal.remove();
|
||||||
</div>
|
}} />
|
||||||
</div>
|
</div>;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return <PageHeader containerClassName={selectedTheme! && 'bg-grey-50'} left={left} right={right} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ThemeModalContent: React.FC<ThemeModalContentProps> = ({
|
const ThemeModalContent: React.FC<ThemeModalContentProps> = ({
|
||||||
|
|
Loading…
Add table
Reference in a new issue