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:
parent
09e11c6a29
commit
60154cfa89
9 changed files with 297 additions and 22 deletions
|
@ -41,9 +41,10 @@
|
|||
"prepublishOnly": "yarn build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ebay/nice-modal-react": "^1.2.10",
|
||||
"@tryghost/timezone-data": "0.3.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"@tryghost/timezone-data": "0.3.0"
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-essentials": "7.0.15",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Button from './admin-x-ds/global/Button';
|
||||
import Heading from './admin-x-ds/global/Heading';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import Settings from './components/Settings';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import {SettingsProvider} from './components/SettingsProvider';
|
||||
|
@ -7,28 +8,30 @@ import {SettingsProvider} from './components/SettingsProvider';
|
|||
function App() {
|
||||
return (
|
||||
<div className="admin-x-settings">
|
||||
<div className='fixed left-6 top-4'>
|
||||
<Button label='← Done' onClick={() => window.history.back()} />
|
||||
</div>
|
||||
<NiceModal.Provider>
|
||||
<div className='fixed left-6 top-4'>
|
||||
<Button label='← Done' link={true} onClick={() => window.history.back()} />
|
||||
</div>
|
||||
|
||||
{/* 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]">
|
||||
{/* 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]">
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="relative min-w-[240px] grow-0 md:fixed md:top-[8vmin] md:basis-[240px]">
|
||||
<div className='h-[84px]'>
|
||||
<Heading>Settings</Heading>
|
||||
{/* Sidebar */}
|
||||
<div className="relative min-w-[240px] grow-0 md:fixed md:top-[8vmin] md:basis-[240px]">
|
||||
<div className='h-[84px]'>
|
||||
<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 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 className="flex-auto pt-[3vmin] md:ml-[280px] md:pt-[84px]">
|
||||
<SettingsProvider>
|
||||
<Settings />
|
||||
</SettingsProvider>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-auto pt-[3vmin] md:ml-[280px] md:pt-[84px]">
|
||||
<SettingsProvider>
|
||||
<Settings />
|
||||
</SettingsProvider>
|
||||
</div>
|
||||
</div>
|
||||
</NiceModal.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ export interface IButton {
|
|||
fullWidth?: boolean;
|
||||
link?: boolean;
|
||||
disabled?: boolean;
|
||||
styles?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
|
@ -19,13 +20,14 @@ const Button: React.FC<IButton> = ({
|
|||
link,
|
||||
disabled,
|
||||
onClick,
|
||||
styles,
|
||||
...props
|
||||
}) => {
|
||||
if (!color) {
|
||||
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 ? ' px-4 h-9' : '';
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ interface ButtonGroupProps {
|
|||
|
||||
const ButtonGroup: React.FC<ButtonGroupProps> = ({buttons, link}) => {
|
||||
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}) => (
|
||||
<Button key={key} link={link} {...props} />
|
||||
))}
|
||||
|
|
111
ghost/admin-x-settings/src/admin-x-ds/global/Modal.stories.tsx
Normal file
111
ghost/admin-x-settings/src/admin-x-ds/global/Modal.stories.tsx
Normal 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
|
||||
}
|
||||
};
|
104
ghost/admin-x-settings/src/admin-x-ds/global/Modal.tsx
Normal file
104
ghost/admin-x-settings/src/admin-x-ds/global/Modal.tsx
Normal 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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -1,13 +1,21 @@
|
|||
import Button from '../../../admin-x-ds/global/Button';
|
||||
import InviteUserModal from '../../modals/InviteUserModal';
|
||||
import List from '../../../admin-x-ds/global/List';
|
||||
import ListItem from '../../../admin-x-ds/global/ListItem';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import React from 'react';
|
||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
import TabView from '../../../admin-x-ds/global/TabView';
|
||||
|
||||
const Users: React.FC = () => {
|
||||
const showAddModal = () => {
|
||||
NiceModal.show(InviteUserModal);
|
||||
};
|
||||
|
||||
const buttons = (
|
||||
<Button color='green' label='Invite users' link={true} />
|
||||
<Button color='green' label='Invite users' link={true} onClick={() => {
|
||||
showAddModal();
|
||||
}} />
|
||||
);
|
||||
|
||||
const owner = (
|
||||
|
|
Loading…
Add table
Reference in a new issue