Added toasters to AdminX Design System
refs. https://github.com/TryGhost/Team/issues/3351
This commit is contained in:
parent
6c3517f67a
commit
5ad8e71cba
@ -76,6 +76,7 @@
|
||||
"postcss": "8.4.24",
|
||||
"postcss-import": "^15.1.0",
|
||||
"prop-types": "15.8.1",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-select": "^5.7.3",
|
||||
"rollup-plugin-node-builtins": "2.1.2",
|
||||
"storybook": "7.0.18",
|
||||
|
@ -5,16 +5,33 @@ import NiceModal from '@ebay/nice-modal-react';
|
||||
import Settings from './components/Settings';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import {ServicesProvider} from './components/providers/ServiceProvider';
|
||||
import {Toaster} from 'react-hot-toast';
|
||||
import {showToast} from './admin-x-ds/global/Toast';
|
||||
|
||||
interface AppProps {
|
||||
ghostVersion: string;
|
||||
}
|
||||
|
||||
function App({ghostVersion}: AppProps) {
|
||||
// const notify = () => toast.success('Here is your toast.', {
|
||||
// duration: 10000000,
|
||||
// position: 'bottom-left'
|
||||
// });
|
||||
|
||||
const notify = () => showToast({
|
||||
message: 'Hello toast',
|
||||
options: {
|
||||
duration: 1000000,
|
||||
position: 'bottom-left'
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<ServicesProvider ghostVersion={ghostVersion}>
|
||||
<DataProvider>
|
||||
<div className="admin-x-settings">
|
||||
<Toaster />
|
||||
<button type='button' onClick={notify}>Make me a toast</button>
|
||||
<NiceModal.Provider>
|
||||
<div className='fixed left-6 top-4'>
|
||||
<Button label='← Done' link={true} onClick={() => window.history.back()} />
|
||||
|
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs></defs><title>close</title><line x1="0.75" y1="23.249" x2="23.25" y2="0.749" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></line><line x1="23.25" y1="23.249" x2="0.75" y2="0.749" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></line></svg>
|
After Width: | Height: | Size: 417 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13.313 2.27521C13.1833 2.04051 12.9931 1.84486 12.7622 1.70861C12.5313 1.57235 12.2681 1.50049 12 1.50049C11.7318 1.50049 11.4686 1.57235 11.2377 1.70861C11.0068 1.84486 10.8166 2.04051 10.687 2.27521L0.936968 20.2752C0.810886 20.5036 0.746538 20.7609 0.750276 21.0217C0.754014 21.2825 0.825708 21.5379 0.958282 21.7625C1.09086 21.9872 1.27972 22.1734 1.50625 22.3028C1.73277 22.4321 1.98911 22.5002 2.24997 22.5002H21.75C22.0108 22.5002 22.2672 22.4321 22.4937 22.3028C22.7202 22.1734 22.9091 21.9872 23.0417 21.7625C23.1742 21.5379 23.2459 21.2825 23.2497 21.0217C23.2534 20.7609 23.189 20.5036 23.063 20.2752L13.313 2.27521Z"></path><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 15V8.25"></path><path stroke="currentColor" stroke-width="1.5" d="M12 18.75C11.7929 18.75 11.625 18.5821 11.625 18.375C11.625 18.1679 11.7929 18 12 18"></path><path stroke="currentColor" stroke-width="1.5" d="M12 18.75C12.2071 18.75 12.375 18.5821 12.375 18.375C12.375 18.1679 12.2071 18 12 18"></path></svg>
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,64 @@
|
||||
import type {Meta, StoryObj} from '@storybook/react';
|
||||
|
||||
import ToastContainer from './ToastContainer';
|
||||
import {Toaster} from 'react-hot-toast';
|
||||
|
||||
const meta = {
|
||||
title: 'Global / Toast',
|
||||
component: ToastContainer,
|
||||
tags: ['autodocs'],
|
||||
decorators: [(_story: any) => (
|
||||
<>
|
||||
<Toaster />
|
||||
{_story()}
|
||||
</>
|
||||
)]
|
||||
} satisfies Meta<typeof ToastContainer>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ToastContainer>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
message: 'Hello notification in a toast'
|
||||
}
|
||||
};
|
||||
|
||||
export const Success: Story = {
|
||||
args: {
|
||||
message: 'Hello success message in a toast',
|
||||
type: 'success'
|
||||
}
|
||||
};
|
||||
|
||||
export const Error: Story = {
|
||||
args: {
|
||||
message: 'Hello error message in a toast',
|
||||
type: 'error'
|
||||
}
|
||||
};
|
||||
|
||||
export const Icon: Story = {
|
||||
args: {
|
||||
message: 'Custom icon in a toast',
|
||||
icon: 'user-add'
|
||||
}
|
||||
};
|
||||
|
||||
export const Custom: Story = {
|
||||
args: {
|
||||
message: (
|
||||
<div>
|
||||
And here is one with a longer notification and a <a className='underline' href="https://ghost.org" rel="noreferrer" target="_blank">link</a>, custom <strong>formatting</strong>, icon and duration.
|
||||
</div>
|
||||
),
|
||||
icon: (
|
||||
<>
|
||||
👋
|
||||
</>
|
||||
),
|
||||
options: {
|
||||
duration: 10000
|
||||
}
|
||||
}
|
||||
};
|
96
ghost/admin-x-settings/src/admin-x-ds/global/Toast.tsx
Normal file
96
ghost/admin-x-settings/src/admin-x-ds/global/Toast.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import Icon from './Icon';
|
||||
import React from 'react';
|
||||
import {ToastOptions, toast} from 'react-hot-toast';
|
||||
|
||||
export type ToastType = 'neutral' | 'success' | 'error';
|
||||
|
||||
export interface ShowToastProps {
|
||||
message?: React.ReactNode;
|
||||
type?: ToastType;
|
||||
icon?: React.ReactNode | string;
|
||||
options?: ToastOptions
|
||||
}
|
||||
|
||||
interface ToastProps {
|
||||
t: any;
|
||||
|
||||
/**
|
||||
* Can be a name of an icon from the icon library or a react component
|
||||
*/
|
||||
children?: React.ReactNode;
|
||||
props?: ShowToastProps;
|
||||
}
|
||||
|
||||
const Toast: React.FC<ToastProps> = ({
|
||||
t,
|
||||
children,
|
||||
props
|
||||
}) => {
|
||||
let classNames = `flex w-[300px] items-start justify-between rounded py-3 px-4 text-sm font-medium text-white gap-6`;
|
||||
|
||||
if (t.visible) {
|
||||
classNames += ' animate-toaster-in';
|
||||
} else {
|
||||
classNames += ' animate-toaster-out';
|
||||
}
|
||||
|
||||
switch (props?.type) {
|
||||
case 'success':
|
||||
classNames += ' bg-black';
|
||||
props.icon = props.icon || 'check-circle';
|
||||
break;
|
||||
case 'error':
|
||||
classNames += ' bg-red';
|
||||
props.icon = props.icon || 'warning';
|
||||
break;
|
||||
default:
|
||||
classNames += ' bg-black';
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames}>
|
||||
<div className='flex items-start gap-3'>
|
||||
{props?.icon && (typeof props.icon === 'string' ?
|
||||
<div className='mt-0.5'><Icon className='grow' color={props.type === 'success' ? 'green' : 'white'} name={props.icon} size='sm' /></div> : props.icon)}
|
||||
{children}
|
||||
</div>
|
||||
<button className='cursor-pointer' type='button' onClick={() => {
|
||||
toast.dismiss(t.id);
|
||||
}}>
|
||||
<div className='mt-1'>
|
||||
<Icon color='white' name='close' size='xs' />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Toast;
|
||||
|
||||
export const showToast = ({
|
||||
message,
|
||||
type = 'neutral',
|
||||
icon = '',
|
||||
options = {
|
||||
position: 'bottom-left',
|
||||
duration: 5000
|
||||
}
|
||||
}: ShowToastProps): void => {
|
||||
if (!options.position) {
|
||||
options.position = 'bottom-left';
|
||||
}
|
||||
|
||||
toast.custom(t => (
|
||||
<Toast props={{
|
||||
type: type,
|
||||
icon: icon
|
||||
}} t={t}>
|
||||
{message}
|
||||
</Toast>
|
||||
),
|
||||
{
|
||||
...options
|
||||
}
|
||||
);
|
||||
};
|
@ -0,0 +1,15 @@
|
||||
import Button from './Button';
|
||||
import React from 'react';
|
||||
import {ShowToastProps, showToast} from './Toast';
|
||||
|
||||
const ToastContainer: React.FC<ShowToastProps> = ({...props}) => {
|
||||
return (
|
||||
<>
|
||||
<Button color='black' label='Toast me!' onClick={() => {
|
||||
showToast({...props});
|
||||
}} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToastContainer;
|
@ -6,6 +6,7 @@ import useRoles from '../../../../hooks/useRoles';
|
||||
import useStaffUsers from '../../../../hooks/useStaffUsers';
|
||||
import validator from 'validator';
|
||||
import {ServicesContext} from '../../../providers/ServiceProvider';
|
||||
import {toast} from 'react-hot-toast';
|
||||
import {useContext, useEffect, useRef, useState} from 'react';
|
||||
|
||||
type RoleType = 'administrator' | 'editor' | 'author' | 'contributor';
|
||||
@ -68,6 +69,8 @@ const InviteUserModal = NiceModal.create(() => {
|
||||
setInvites([...invites, res.invites[0]]);
|
||||
|
||||
setSaveState('saved');
|
||||
|
||||
toast.success('Invitation sent!');
|
||||
} catch (e: any) {
|
||||
setSaveState('error');
|
||||
return;
|
||||
|
@ -93,6 +93,37 @@ module.exports = {
|
||||
none: '0 0 #0000'
|
||||
},
|
||||
extend: {
|
||||
keyframes: {
|
||||
toasterIn: {
|
||||
'0.00%': {
|
||||
opacity: '0',
|
||||
transform: 'translateX(-232.05px)'
|
||||
},
|
||||
'26.52%': {
|
||||
opacity: '0.5',
|
||||
transform: 'translateX(5.90px)'
|
||||
},
|
||||
'63.26%': {
|
||||
opacity: '1',
|
||||
transform: 'translateX(-1.77px)'
|
||||
},
|
||||
'100.00%': {
|
||||
transform: 'translateX(0px)'
|
||||
}
|
||||
},
|
||||
toasterOut: {
|
||||
'0%': {
|
||||
opacity: '1'
|
||||
},
|
||||
'100%': {
|
||||
opacity: '0'
|
||||
}
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
'toaster-in': 'toasterIn 0.8s cubic-bezier(0.445, 0.050, 0.550, 0.950)',
|
||||
'toaster-out': 'toasterOut 0.4s 0s 1 ease forwards'
|
||||
},
|
||||
spacing: {
|
||||
px: '1px',
|
||||
0: '0px',
|
||||
|
Loading…
Reference in New Issue
Block a user