0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-10 23:36:14 -05:00

Added basic modal to AdminX Design System

refs. https://github.com/TryGhost/Team/issues/3150
This commit is contained in:
Peter Zimon 2023-05-24 16:31:02 +02:00
parent 09e11c6a29
commit 60154cfa89
9 changed files with 297 additions and 22 deletions

View file

@ -41,9 +41,10 @@
"prepublishOnly": "yarn build" "prepublishOnly": "yarn build"
}, },
"dependencies": { "dependencies": {
"@ebay/nice-modal-react": "^1.2.10",
"@tryghost/timezone-data": "0.3.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0"
"@tryghost/timezone-data": "0.3.0"
}, },
"devDependencies": { "devDependencies": {
"@storybook/addon-essentials": "7.0.15", "@storybook/addon-essentials": "7.0.15",

View file

@ -1,5 +1,6 @@
import Button from './admin-x-ds/global/Button'; import Button from './admin-x-ds/global/Button';
import Heading from './admin-x-ds/global/Heading'; import Heading from './admin-x-ds/global/Heading';
import NiceModal from '@ebay/nice-modal-react';
import Settings from './components/Settings'; import Settings from './components/Settings';
import Sidebar from './components/Sidebar'; import Sidebar from './components/Sidebar';
import {SettingsProvider} from './components/SettingsProvider'; import {SettingsProvider} from './components/SettingsProvider';
@ -7,28 +8,30 @@ import {SettingsProvider} from './components/SettingsProvider';
function App() { function App() {
return ( return (
<div className="admin-x-settings"> <div className="admin-x-settings">
<div className='fixed left-6 top-4'> <NiceModal.Provider>
<Button label='&larr; Done' onClick={() => window.history.back()} /> <div className='fixed left-6 top-4'>
</div> <Button label='&larr; Done' link={true} onClick={() => window.history.back()} />
</div>
{/* Main container */} {/* Main container */}
<div className="mx-auto flex max-w-[1080px] flex-col px-[5vmin] py-[12vmin] md:flex-row md:items-start md:gap-x-10 md:py-[8vmin]"> <div className="mx-auto flex max-w-[1080px] flex-col px-[5vmin] py-[12vmin] md:flex-row md:items-start md:gap-x-10 md:py-[8vmin]">
{/* Sidebar */} {/* Sidebar */}
<div className="relative min-w-[240px] grow-0 md:fixed md:top-[8vmin] md:basis-[240px]"> <div className="relative min-w-[240px] grow-0 md:fixed md:top-[8vmin] md:basis-[240px]">
<div className='h-[84px]'> <div className='h-[84px]'>
<Heading>Settings</Heading> <Heading>Settings</Heading>
</div>
<div className="relative mt-[-32px] w-[240px] overflow-x-hidden after:absolute after:inset-x-0 after:top-0 after:block after:h-[40px] after:bg-gradient-to-b after:from-white after:to-transparent after:content-['']">
<Sidebar />
</div>
</div> </div>
<div className="relative mt-[-32px] w-[240px] overflow-x-hidden after:absolute after:inset-x-0 after:top-0 after:block after:h-[40px] after:bg-gradient-to-b after:from-white after:to-transparent after:content-['']"> <div className="flex-auto pt-[3vmin] md:ml-[280px] md:pt-[84px]">
<Sidebar /> <SettingsProvider>
<Settings />
</SettingsProvider>
</div> </div>
</div> </div>
<div className="flex-auto pt-[3vmin] md:ml-[280px] md:pt-[84px]"> </NiceModal.Provider>
<SettingsProvider>
<Settings />
</SettingsProvider>
</div>
</div>
</div> </div>
); );
} }

View file

@ -9,6 +9,7 @@ export interface IButton {
fullWidth?: boolean; fullWidth?: boolean;
link?: boolean; link?: boolean;
disabled?: boolean; disabled?: boolean;
styles?: string;
onClick?: () => void; onClick?: () => void;
} }
@ -19,13 +20,14 @@ const Button: React.FC<IButton> = ({
link, link,
disabled, disabled,
onClick, onClick,
styles,
...props ...props
}) => { }) => {
if (!color) { if (!color) {
color = 'clear'; color = 'clear';
} }
let styles = 'transition flex items-center justify-center rounded-sm text-sm'; styles += ' transition flex items-center justify-center rounded-sm text-sm';
styles += ((link && color !== 'clear' && color !== 'black') || (!link && color !== 'clear')) ? ' font-bold' : ' font-semibold'; styles += ((link && color !== 'clear' && color !== 'black') || (!link && color !== 'clear')) ? ' font-bold' : ' font-semibold';
styles += !link ? ' px-4 h-9' : ''; styles += !link ? ' px-4 h-9' : '';

View file

@ -10,7 +10,7 @@ interface ButtonGroupProps {
const ButtonGroup: React.FC<ButtonGroupProps> = ({buttons, link}) => { const ButtonGroup: React.FC<ButtonGroupProps> = ({buttons, link}) => {
return ( return (
<div className={`flex items-center ${link ? 'gap-5' : 'gap-2'}`}> <div className={`flex items-center ${link ? 'gap-5' : 'gap-3'}`}>
{buttons.map(({key, ...props}) => ( {buttons.map(({key, ...props}) => (
<Button key={key} link={link} {...props} /> <Button key={key} link={link} {...props} />
))} ))}

View file

@ -0,0 +1,111 @@
import type {Meta, StoryObj} from '@storybook/react';
import Modal from './Modal';
import ModalContainer from './ModalContainer';
import NiceModal from '@ebay/nice-modal-react';
const meta = {
title: 'Global / Modal',
component: Modal,
tags: ['autodocs'],
decorators: [(_story: any, context: any) => (
<NiceModal.Provider>
<ModalContainer {...context.args} />
</NiceModal.Provider>
)]
} satisfies Meta<typeof Modal>;
export default meta;
type Story = StoryObj<typeof Modal>;
const modalContent = (<div>Modal content</div>);
export const Default: Story = {
args: {
onOk: () => {
alert('Clicked OK!');
},
title: 'Modal dialog',
children: modalContent
}
};
export const Small: Story = {
args: {
size: 'sm',
onOk: () => {
alert('Clicked OK!');
},
title: 'Small modal',
children: modalContent
}
};
export const Medium: Story = {
args: {
size: 'md',
onOk: () => {
alert('Clicked OK!');
},
title: 'Medium modal (default size)',
children: modalContent
}
};
export const Large: Story = {
args: {
size: 'lg',
onOk: () => {
alert('Clicked OK!');
},
title: 'Large modal',
children: modalContent
}
};
export const ExtraLarge: Story = {
args: {
size: 'xl',
onOk: () => {
alert('Clicked OK!');
},
title: 'Extra large modal',
children: modalContent
}
};
export const full: Story = {
args: {
size: 'full',
onOk: () => {
alert('Clicked OK!');
},
title: 'Full modal',
children: modalContent
}
};
export const Bleed: Story = {
args: {
size: 'bleed',
onOk: () => {
alert('Clicked OK!');
},
title: 'Full bleed modal',
children: modalContent
}
};
export const CustomButtons: Story = {
args: {
leftButtonLabel: 'Extra action',
cancelLabel: 'Nope',
okLabel: 'Yep',
onOk: () => {
alert('Clicked Yep!');
},
title: 'Custom buttons',
children: modalContent
}
};

View file

@ -0,0 +1,104 @@
import Button, {IButton} from './Button';
import ButtonGroup from './ButtonGroup';
import Heading from './Heading';
import React from 'react';
import {useModal} from '@ebay/nice-modal-react';
export type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'full' | 'bleed';
export interface ModalProps {
size?: ModalSize;
title?: string;
okLabel?: string;
cancelLabel?: string;
leftButtonLabel?: string;
customFooter?: React.ReactNode;
onOk?: () => void;
onCancel?: () => void;
children?: React.ReactNode;
}
const Modal: React.FC<ModalProps> = ({size = 'md', title, okLabel, cancelLabel, customFooter, leftButtonLabel, onOk, onCancel, children}) => {
const modal = useModal();
let buttons: IButton[] = [];
if (!customFooter) {
buttons.push({
key: 'cancel-modal',
label: cancelLabel ? cancelLabel : 'Cancel',
onClick: (onCancel ? onCancel : () => {
modal.remove();
})
});
buttons.push({
key: 'ok-modal',
label: okLabel ? okLabel : 'OK',
color: 'black',
styles: 'min-w-[80px]',
onClick: onOk
});
}
let modalStyles = 'z-50 mx-auto flex flex-col justify-between bg-white p-8 shadow-xl w-full';
let backdropStyles = 'fixed inset-0 h-[100vh] w-[100vw] overflow-y-scroll bg-[rgba(0,0,0,0.1)]';
switch (size) {
case 'sm':
modalStyles += ' max-w-[480px]';
break;
case 'md':
modalStyles += ' max-w-[720px]';
break;
case 'lg':
modalStyles += ' max-w-[940px]';
break;
case 'xl':
modalStyles += ' max-w-[1180px] ';
break;
case 'full':
case 'bleed':
modalStyles += ' h-full';
break;
}
if (size !== 'bleed') {
modalStyles += ' rounded';
}
if (size !== 'bleed' && size !== 'full') {
backdropStyles += ' p-[8vmin]';
} else if (size === 'full') {
backdropStyles += ' p-[2vmin]';
}
return (
<div className={backdropStyles}>
<section className={modalStyles}>
<div>
{title && <Heading level={4}>{title}</Heading>}
<div>{children}</div>
</div>
{customFooter ? customFooter :
<div className='w-100 flex items-center justify-between gap-6'>
<div>
{leftButtonLabel &&
<Button label={leftButtonLabel} link={true} />
}
</div>
<div className='flex gap-3'>
<ButtonGroup buttons={buttons}/>
</div>
</div>
}
</section>
</div>
);
};
export default Modal;

View file

@ -0,0 +1,26 @@
import NiceModal from '@ebay/nice-modal-react';
import React from 'react';
import Button from './Button';
import Modal, {ModalProps} from './Modal';
const ModalContainer: React.FC<ModalProps> = ({children, onCancel, ...props}) => {
const modal = NiceModal.create<ModalProps>(() => {
return (
<Modal {...props}>
<div className='py-4'>
{children}
</div>
</Modal>
);
});
return (
<div>
<Button color='black' label='Open modal' onClick={() => {
NiceModal.show(modal);
}} />
</div>
);
};
export default ModalContainer;

View file

@ -0,0 +1,20 @@
import Modal from '../../admin-x-ds/global/Modal';
import NiceModal from '@ebay/nice-modal-react';
const InviteUserModal = NiceModal.create(() => {
return (
<Modal
size='md'
title='Invite users'
onOk={() => {
alert('Clicked OK');
}}
>
<div className='py-4'>
[TBD: invite user contents]
</div>
</Modal>
);
});
export default InviteUserModal;

View file

@ -1,13 +1,21 @@
import Button from '../../../admin-x-ds/global/Button'; import Button from '../../../admin-x-ds/global/Button';
import InviteUserModal from '../../modals/InviteUserModal';
import List from '../../../admin-x-ds/global/List'; import List from '../../../admin-x-ds/global/List';
import ListItem from '../../../admin-x-ds/global/ListItem'; import ListItem from '../../../admin-x-ds/global/ListItem';
import NiceModal from '@ebay/nice-modal-react';
import React from 'react'; import React from 'react';
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup'; import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
import TabView from '../../../admin-x-ds/global/TabView'; import TabView from '../../../admin-x-ds/global/TabView';
const Users: React.FC = () => { const Users: React.FC = () => {
const showAddModal = () => {
NiceModal.show(InviteUserModal);
};
const buttons = ( const buttons = (
<Button color='green' label='Invite users' link={true} /> <Button color='green' label='Invite users' link={true} onClick={() => {
showAddModal();
}} />
); );
const owner = ( const owner = (