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: {
|
||||
storySort: {
|
||||
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 NiceModal, {NiceModalHandler, useModal} from '@ebay/nice-modal-react';
|
||||
import OfficialThemes from './theme/OfficialThemes';
|
||||
import PageHeader from '../../../admin-x-ds/global/layout/PageHeader';
|
||||
import React, {useState} from 'react';
|
||||
import TabView from '../../../admin-x-ds/global/TabView';
|
||||
import {OfficialTheme} from '../../../models/themes';
|
||||
|
@ -40,85 +41,86 @@ const ThemeToolbar: React.FC<ThemeToolbarProps> = ({
|
|||
setThemes
|
||||
}) => {
|
||||
const api = useApi();
|
||||
|
||||
let left, right;
|
||||
|
||||
if (selectedTheme) {
|
||||
const installedTheme = themes.find(theme => theme.name.toLowerCase() === selectedTheme.name.toLowerCase());
|
||||
|
||||
return (
|
||||
<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'>
|
||||
<button
|
||||
className={`text-sm`}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setCurrentTab('official');
|
||||
setSelectedTheme(null);
|
||||
}}>
|
||||
Official themes
|
||||
</button>
|
||||
→
|
||||
<span className='text-sm font-bold'>{selectedTheme?.name}</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'
|
||||
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'}
|
||||
left =
|
||||
<div className='flex w-[33%] items-center gap-2'>
|
||||
<button
|
||||
className={`text-sm`}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setCurrentTab('official');
|
||||
setSelectedTheme(null);
|
||||
}}>
|
||||
Official themes
|
||||
</button>
|
||||
→
|
||||
<span className='text-sm font-bold'>{selectedTheme?.name}</span>
|
||||
</div>;
|
||||
|
||||
right =
|
||||
<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'}
|
||||
]}
|
||||
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'>
|
||||
<FileUpload id='theme-uplaod' onUpload={async (file: File) => {
|
||||
const data = await api.themes.upload({file});
|
||||
const uploadedTheme = data.themes[0];
|
||||
setThemes([...themes, uploadedTheme]);
|
||||
showToast({
|
||||
message: `Theme uploaded - ${uploadedTheme.name}`
|
||||
});
|
||||
}}>Upload theme</FileUpload>
|
||||
<Button
|
||||
className='min-w-[75px]'
|
||||
color='black'
|
||||
label='OK'
|
||||
onClick = {() => {
|
||||
modal.remove();
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
right =
|
||||
<div className='flex items-center gap-3'>
|
||||
<FileUpload id='theme-uplaod' onUpload={async (file: File) => {
|
||||
const data = await api.themes.upload({file});
|
||||
const uploadedTheme = data.themes[0];
|
||||
setThemes([...themes, uploadedTheme]);
|
||||
showToast({
|
||||
message: `Theme uploaded - ${uploadedTheme.name}`
|
||||
});
|
||||
}}>Upload theme</FileUpload>
|
||||
<Button
|
||||
className='min-w-[75px]'
|
||||
color='black'
|
||||
label='OK'
|
||||
onClick = {() => {
|
||||
modal.remove();
|
||||
}} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <PageHeader containerClassName={selectedTheme! && 'bg-grey-50'} left={left} right={right} />;
|
||||
};
|
||||
|
||||
const ThemeModalContent: React.FC<ThemeModalContentProps> = ({
|
||||
|
|
Loading…
Add table
Reference in a new issue