Add underlying design component for a dialog
This commit is contained in:
parent
f4119df0aa
commit
0bab962337
13 changed files with 233 additions and 18 deletions
|
@ -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>
|
||||||
|
|
36
resources/scripts/components/elements/button/Button.tsx
Normal file
36
resources/scripts/components/elements/button/Button.tsx
Normal 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;
|
|
@ -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;
|
||||||
|
}
|
2
resources/scripts/components/elements/button/index.ts
Normal file
2
resources/scripts/components/elements/button/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as Button } from './Button';
|
||||||
|
export { default as styles } from './button.module.css';
|
116
resources/scripts/components/elements/dialog/Dialog.tsx
Normal file
116
resources/scripts/components/elements/dialog/Dialog.tsx
Normal 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;
|
|
@ -0,0 +1,7 @@
|
||||||
|
.wrapper {
|
||||||
|
@apply fixed z-10 inset-0 overflow-y-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
@apply fixed inset-0 bg-neutral-900 opacity-75;
|
||||||
|
}
|
2
resources/scripts/components/elements/dialog/index.ts
Normal file
2
resources/scripts/components/elements/dialog/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as Dialog } from './Dialog';
|
||||||
|
export { default as styles } from './dialog.module.css';
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
Loading…
Reference in a new issue