Add underlying design component for a dialog

This commit is contained in:
Dane Everitt 2022-02-26 17:05:30 -05:00
parent f4119df0aa
commit 0bab962337
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
13 changed files with 233 additions and 18 deletions

View file

@ -3,7 +3,16 @@ import http from '@/api/http';
import { User } from '@/api/admin/user'; import { User } from '@/api/admin/user';
import { AdminTransformers } from '@/api/admin/transformers'; import { AdminTransformers } from '@/api/admin/transformers';
import { Dropdown } from '@/components/elements/dropdown'; import { Dropdown } from '@/components/elements/dropdown';
import { DotsVerticalIcon, LockClosedIcon, PaperAirplaneIcon, PencilIcon, TrashIcon } from '@heroicons/react/solid'; import {
DotsVerticalIcon,
LockClosedIcon,
PaperAirplaneIcon,
PencilIcon,
PlusIcon,
TrashIcon,
} from '@heroicons/react/solid';
import { Button } from '@/components/elements/button/index';
import { Dialog } from '@/components/elements/dialog';
const UsersContainerV2 = () => { const UsersContainerV2 = () => {
const [ users, setUsers ] = useState<User[]>([]); const [ users, setUsers ] = useState<User[]>([]);
@ -11,6 +20,8 @@ const UsersContainerV2 = () => {
document.title = 'Admin | Users'; document.title = 'Admin | Users';
}, []); }, []);
const [ visible, setVisible ] = useState(false);
useEffect(() => { useEffect(() => {
http.get('/api/application/users') http.get('/api/application/users')
.then(({ data }) => { .then(({ data }) => {
@ -20,8 +31,21 @@ const UsersContainerV2 = () => {
}, []); }, []);
return ( return (
<div className={'bg-neutral-700'}> <div>
<table className={'min-w-full rounded'}> <div className={'flex justify-end mb-4'}>
<Button className={'shadow focus:ring-offset-2 focus:ring-offset-neutral-800'}>
Add User <PlusIcon className={'ml-2 w-5 h-5'}/>
</Button>
</div>
<Dialog title={'Delete account'} visible={visible} onDismissed={() => setVisible(false)}>
<Dialog.Icon type={'danger'}/>
This account will be permanently deleted.
<Dialog.Buttons>
<Button.Text onClick={() => setVisible(false)}>Cancel</Button.Text>
<Button.Danger>Delete</Button.Danger>
</Dialog.Buttons>
</Dialog>
<table className={'min-w-full rounded bg-neutral-700'}>
<thead className={'bg-neutral-900'}> <thead className={'bg-neutral-900'}>
<tr> <tr>
<th scope={'col'} className={'w-8'}/> <th scope={'col'} className={'w-8'}/>
@ -59,13 +83,13 @@ const UsersContainerV2 = () => {
<td className={'px-6 py-4 whitespace-nowrap'}> <td className={'px-6 py-4 whitespace-nowrap'}>
<Dropdown> <Dropdown>
<Dropdown.Button className={'px-2'}> <Dropdown.Button className={'px-2'}>
<DotsVerticalIcon /> <DotsVerticalIcon/>
</Dropdown.Button> </Dropdown.Button>
<Dropdown.Item icon={<PencilIcon />}>Edit</Dropdown.Item> <Dropdown.Item icon={<PencilIcon/>}>Edit</Dropdown.Item>
<Dropdown.Item icon={<PaperAirplaneIcon />}>Reset Password</Dropdown.Item> <Dropdown.Item icon={<PaperAirplaneIcon/>}>Reset Password</Dropdown.Item>
<Dropdown.Item icon={<LockClosedIcon />}>Suspend</Dropdown.Item> <Dropdown.Item icon={<LockClosedIcon/>}>Suspend</Dropdown.Item>
<Dropdown.Gap /> <Dropdown.Gap/>
<Dropdown.Item icon={<TrashIcon />} danger>Delete Account</Dropdown.Item> <Dropdown.Item icon={<TrashIcon/>} onClick={() => setVisible(true)} danger>Delete Account</Dropdown.Item>
</Dropdown> </Dropdown>
</td> </td>
</tr> </tr>

View file

@ -0,0 +1,36 @@
import React, { forwardRef } from 'react';
import classNames from 'classnames';
import styles from './button.module.css';
export type ButtonProps = JSX.IntrinsicElements['button'] & {
square?: boolean;
small?: boolean;
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ children, square, small, className, ...rest }, ref) => {
return (
<button
ref={ref}
className={classNames(styles.button, { [styles.square]: square, [styles.small]: small }, className)}
{...rest}
>
{children}
</button>
);
},
);
const TextButton = forwardRef<HTMLButtonElement, ButtonProps>(({ className, ...props }, ref) => (
// @ts-expect-error
<Button ref={ref} className={classNames(styles.text, className)} {...props} />
));
const DangerButton = forwardRef<HTMLButtonElement, ButtonProps>(({ className, ...props }, ref) => (
// @ts-expect-error
<Button ref={ref} className={classNames(styles.danger, className)} {...props} />
));
const _Button = Object.assign(Button, { Text: TextButton, Danger: DangerButton });
export default _Button;

View file

@ -0,0 +1,30 @@
.button {
@apply px-4 py-2 inline-flex items-center justify-center;
@apply bg-primary-500 rounded text-base font-semibold text-primary-50 transition-all duration-100;
@apply hover:bg-primary-600 active:bg-primary-600;
&.square {
@apply p-2;
}
&:focus {
@apply ring-[3px] ring-opacity-75 ring-primary-300;
}
/* Sizing Controls */
&.small {
@apply px-3 py-1 font-normal focus:ring-2;
&.square {
@apply p-1;
}
}
}
.text {
@apply bg-transparent focus:ring-neutral-300 focus:ring-opacity-50 hover:bg-neutral-600 active:bg-neutral-500;
}
.danger {
@apply bg-red-500 hover:bg-red-600 active:bg-red-600 focus:ring-red-400 text-red-50;
}

View file

@ -0,0 +1,2 @@
export { default as Button } from './Button';
export { default as styles } from './button.module.css';

View file

@ -0,0 +1,116 @@
import React, { Fragment } from 'react';
import { Dialog as HeadlessDialog, Transition } from '@headlessui/react';
import { Button } from '@/components/elements/button/index';
import styles from './dialog.module.css';
import { XIcon } from '@heroicons/react/solid';
import { CheckIcon, ExclamationIcon, InformationCircleIcon, ShieldExclamationIcon } from '@heroicons/react/outline';
import classNames from 'classnames';
interface Props {
visible: boolean;
onDismissed: () => void;
title?: string;
children?: React.ReactNode;
}
interface DialogIconProps {
type: 'danger' | 'info' | 'success' | 'warning';
className?: string;
}
const DialogIcon = ({ type, className }: DialogIconProps) => {
const [ Component, styles ] = (function (): [(props: React.ComponentProps<'svg'>) => JSX.Element, string] {
switch (type) {
case 'danger':
return [ ShieldExclamationIcon, 'bg-red-500 text-red-50' ];
case 'warning':
return [ ExclamationIcon, 'bg-yellow-600 text-yellow-50' ];
case 'success':
return [ CheckIcon, 'bg-green-600 text-green-50' ];
case 'info':
return [ InformationCircleIcon, 'bg-primary-500 text-primary-50' ];
}
})();
return (
<div className={classNames('flex items-center justify-center w-10 h-10 rounded-full', styles, className)}>
<Component className={'w-6 h-6'} />
</div>
);
};
const DialogButtons = ({ children }: { children: React.ReactNode }) => (
<>{children}</>
);
const Dialog = ({ visible, title, onDismissed, children }: Props) => {
const items = React.Children.toArray(children || []);
const [ buttons, icon, content ] = [
// @ts-expect-error
items.find(child => child.type === DialogButtons),
// @ts-expect-error
items.find(child => child.type === DialogIcon),
// @ts-expect-error
items.filter(child => ![ DialogIcon, DialogButtons ].includes(child.type)),
];
return (
<Transition show={visible} as={Fragment}>
<HeadlessDialog onClose={() => onDismissed()} className={styles.wrapper}>
<div className={'flex items-center justify-center min-h-screen'}>
<Transition.Child
as={Fragment}
enter={'ease-out duration-200'}
enterFrom={'opacity-0'}
enterTo={'opacity-100'}
leave={'ease-in duration-100'}
leaveFrom={'opacity-100'}
leaveTo={'opacity-0'}
>
<HeadlessDialog.Overlay className={styles.overlay}/>
</Transition.Child>
<Transition.Child
as={Fragment}
enter={'ease-out duration-200'}
enterFrom={'opacity-0 scale-95'}
enterTo={'opacity-100 scale-100'}
leave={'ease-in duration-100'}
leaveFrom={'opacity-100 scale-100'}
leaveTo={'opacity-0 scale-95'}
>
<div className={'relative bg-neutral-700 rounded max-w-xl w-full mx-auto shadow-lg ring-4 ring-neutral-800 ring-opacity-50'}>
<div className={'flex p-6'}>
{icon && <div className={'mr-4'}>{icon}</div>}
<div className={'flex-1'}>
{title &&
<HeadlessDialog.Title className={'font-header text-xl font-medium mb-2 text-white pr-4'}>
{title}
</HeadlessDialog.Title>
}
<HeadlessDialog.Description className={'pr-4'}>
{content}
</HeadlessDialog.Description>
</div>
</div>
{buttons &&
<div className={'px-6 py-3 bg-neutral-800 flex items-center justify-end space-x-3 rounded-b'}>
{buttons}
</div>
}
{/* Keep this below the other buttons so that it isn't the default focus if they're present. */}
<div className={'absolute right-0 top-0 m-4'}>
<Button.Text square small onClick={() => onDismissed()} className={'hover:rotate-90'}>
<XIcon className={'w-5 h-5'}/>
</Button.Text>
</div>
</div>
</Transition.Child>
</div>
</HeadlessDialog>
</Transition>
);
};
const _Dialog = Object.assign(Dialog, { Buttons: DialogButtons, Icon: DialogIcon });
export default _Dialog;

View file

@ -0,0 +1,7 @@
.wrapper {
@apply fixed z-10 inset-0 overflow-y-auto;
}
.overlay {
@apply fixed inset-0 bg-neutral-900 opacity-75;
}

View file

@ -0,0 +1,2 @@
export { default as Dialog } from './Dialog';
export { default as styles } from './dialog.module.css';

View file

@ -1,6 +1,6 @@
import React, { ElementType, forwardRef, useMemo } from 'react'; import React, { ElementType, forwardRef, useMemo } from 'react';
import { Menu, Transition } from '@headlessui/react'; import { Menu, Transition } from '@headlessui/react';
import styles from './styles.module.css'; import styles from './dropdown.module.css';
import classNames from 'classnames'; import classNames from 'classnames';
import DropdownItem from '@/components/elements/dropdown/DropdownItem'; import DropdownItem from '@/components/elements/dropdown/DropdownItem';
import DropdownButton from '@/components/elements/dropdown/DropdownButton'; import DropdownButton from '@/components/elements/dropdown/DropdownButton';

View file

@ -1,5 +1,5 @@
import classNames from 'classnames'; import classNames from 'classnames';
import styles from '@/components/elements/dropdown/styles.module.css'; import styles from '@/components/elements/dropdown/dropdown.module.css';
import { ChevronDownIcon } from '@heroicons/react/solid'; import { ChevronDownIcon } from '@heroicons/react/solid';
import { Menu } from '@headlessui/react'; import { Menu } from '@headlessui/react';
import React from 'react'; import React from 'react';

View file

@ -1,6 +1,6 @@
import React, { forwardRef } from 'react'; import React, { forwardRef } from 'react';
import { Menu } from '@headlessui/react'; import { Menu } from '@headlessui/react';
import styles from './styles.module.css'; import styles from './dropdown.module.css';
import classNames from 'classnames'; import classNames from 'classnames';
interface Props { interface Props {

View file

@ -1,2 +1,2 @@
export { default as Dropdown } from './Dropdown'; export { default as Dropdown } from './Dropdown';
export * as styles from './styles.module.css'; export * as styles from './dropdown.module.css';

View file

@ -5,12 +5,10 @@ module.exports = {
'./resources/scripts/**/*.{js,ts,tsx}', './resources/scripts/**/*.{js,ts,tsx}',
], ],
theme: { theme: {
fontFamily: {
sans: [ 'Rubik', '-apple-system', 'BlinkMacSystemFont', '"Helvetica Neue"', '"Roboto"', 'system-ui', 'sans-serif' ],
header: [ '"IBM Plex Sans"', '"Roboto"', 'system-ui', 'sans-serif' ],
mono: [ '"IBM Plex Mono"', '"Source Code Pro"', 'SourceCodePro', 'Menlo', 'Monaco', 'Consolas', 'monospace' ],
},
extend: { extend: {
fontFamily: {
header: [ '"IBM Plex Sans"', '"Roboto"', 'system-ui', 'sans-serif' ],
},
colors: { colors: {
black: '#131a20', black: '#131a20',
primary: { primary: {