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 { AdminTransformers } from '@/api/admin/transformers';
|
||||
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 [ users, setUsers ] = useState<User[]>([]);
|
||||
|
@ -11,6 +20,8 @@ const UsersContainerV2 = () => {
|
|||
document.title = 'Admin | Users';
|
||||
}, []);
|
||||
|
||||
const [ visible, setVisible ] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
http.get('/api/application/users')
|
||||
.then(({ data }) => {
|
||||
|
@ -20,8 +31,21 @@ const UsersContainerV2 = () => {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<div className={'bg-neutral-700'}>
|
||||
<table className={'min-w-full rounded'}>
|
||||
<div>
|
||||
<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'}>
|
||||
<tr>
|
||||
<th scope={'col'} className={'w-8'}/>
|
||||
|
@ -65,7 +89,7 @@ const UsersContainerV2 = () => {
|
|||
<Dropdown.Item icon={<PaperAirplaneIcon/>}>Reset Password</Dropdown.Item>
|
||||
<Dropdown.Item icon={<LockClosedIcon/>}>Suspend</Dropdown.Item>
|
||||
<Dropdown.Gap/>
|
||||
<Dropdown.Item icon={<TrashIcon />} danger>Delete Account</Dropdown.Item>
|
||||
<Dropdown.Item icon={<TrashIcon/>} onClick={() => setVisible(true)} danger>Delete Account</Dropdown.Item>
|
||||
</Dropdown>
|
||||
</td>
|
||||
</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 { Menu, Transition } from '@headlessui/react';
|
||||
import styles from './styles.module.css';
|
||||
import styles from './dropdown.module.css';
|
||||
import classNames from 'classnames';
|
||||
import DropdownItem from '@/components/elements/dropdown/DropdownItem';
|
||||
import DropdownButton from '@/components/elements/dropdown/DropdownButton';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { Menu } from '@headlessui/react';
|
||||
import React from 'react';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { forwardRef } from 'react';
|
||||
import { Menu } from '@headlessui/react';
|
||||
import styles from './styles.module.css';
|
||||
import styles from './dropdown.module.css';
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface Props {
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
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}',
|
||||
],
|
||||
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: {
|
||||
fontFamily: {
|
||||
header: [ '"IBM Plex Sans"', '"Roboto"', 'system-ui', 'sans-serif' ],
|
||||
},
|
||||
colors: {
|
||||
black: '#131a20',
|
||||
primary: {
|
||||
|
|
Loading…
Reference in a new issue