Update react, add some V2 components for V1 usage
This commit is contained in:
parent
921da09a63
commit
1a5465dc34
21 changed files with 564 additions and 43 deletions
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 './style.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;
|
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 './style.module.css';
|
|
@ -0,0 +1,30 @@
|
|||
.button {
|
||||
@apply px-4 py-2 inline-flex items-center justify-center;
|
||||
@apply bg-blue-600 rounded text-base font-semibold text-blue-50 transition-all duration-100;
|
||||
@apply hover:bg-blue-500 active:bg-blue-500;
|
||||
|
||||
&.square {
|
||||
@apply p-2;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
@apply ring-[3px] ring-blue-500 ring-offset-2 ring-offset-neutral-700;
|
||||
}
|
||||
|
||||
/* 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-500 active:bg-neutral-500;
|
||||
}
|
||||
|
||||
.danger {
|
||||
@apply bg-red-600 hover:bg-red-500 active:bg-red-500 focus:ring-red-500 text-red-50;
|
||||
}
|
112
resources/scripts/components/elements/dialog/Dialog.tsx
Normal file
112
resources/scripts/components/elements/dialog/Dialog.tsx
Normal file
|
@ -0,0 +1,112 @@
|
|||
import React, { Fragment } from 'react';
|
||||
import { Dialog as HeadlessDialog, Transition } from '@headlessui/react';
|
||||
import { Button } from '@/components/elements/button/index';
|
||||
import styles from './style.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={styles.container}>
|
||||
<div className={'flex p-6'}>
|
||||
{icon && <div className={'mr-4'}>{icon}</div>}
|
||||
<div className={'flex-1'}>
|
||||
{title &&
|
||||
<HeadlessDialog.Title className={styles.title}>
|
||||
{title}
|
||||
</HeadlessDialog.Title>
|
||||
}
|
||||
<HeadlessDialog.Description className={'pr-4'}>
|
||||
{content}
|
||||
</HeadlessDialog.Description>
|
||||
</div>
|
||||
</div>
|
||||
{buttons && <div className={styles.button_bar}>{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;
|
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 './style.module.css';
|
|
@ -0,0 +1,20 @@
|
|||
.wrapper {
|
||||
@apply fixed z-10 inset-0 overflow-y-auto;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
@apply fixed inset-0 bg-gray-900 opacity-50;
|
||||
}
|
||||
|
||||
.container {
|
||||
@apply relative bg-gray-600 rounded max-w-xl w-full mx-auto shadow-lg;
|
||||
@apply ring-4 ring-gray-800 ring-opacity-80;
|
||||
|
||||
& .title {
|
||||
@apply font-header text-xl font-medium mb-2 text-white pr-4;
|
||||
}
|
||||
|
||||
& > .button_bar {
|
||||
@apply px-6 py-3 bg-gray-700 flex items-center justify-end space-x-3 rounded-b;
|
||||
}
|
||||
}
|
62
resources/scripts/components/elements/dropdown/Dropdown.tsx
Normal file
62
resources/scripts/components/elements/dropdown/Dropdown.tsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
import React, { ElementType, forwardRef, useMemo } from 'react';
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
import styles from './style.module.css';
|
||||
import classNames from 'classnames';
|
||||
import DropdownItem from '@/components/elements/dropdown/DropdownItem';
|
||||
import DropdownButton from '@/components/elements/dropdown/DropdownButton';
|
||||
|
||||
interface Props {
|
||||
as?: ElementType;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const DropdownGap = ({ invisible }: { invisible?: boolean }) => (
|
||||
<div className={classNames('border m-2', { 'border-neutral-700': !invisible, 'border-transparent': invisible })}/>
|
||||
);
|
||||
|
||||
type TypedChild = (React.ReactChild | React.ReactFragment | React.ReactPortal) & {
|
||||
type?: JSX.Element;
|
||||
}
|
||||
|
||||
const Dropdown = forwardRef<typeof Menu, Props>(({ as, children }, ref) => {
|
||||
const [ Button, items ] = useMemo(() => {
|
||||
const list = React.Children.toArray(children) as unknown as TypedChild[];
|
||||
|
||||
return [
|
||||
list.filter(child => child.type === DropdownButton),
|
||||
list.filter(child => child.type !== DropdownButton),
|
||||
];
|
||||
}, [ children ]);
|
||||
|
||||
if (!Button) {
|
||||
throw new Error('Cannot mount <Dropdown /> component without a child <Dropdown.Button />.');
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu as={as || 'div'} className={styles.menu} ref={ref}>
|
||||
{Button}
|
||||
<Transition
|
||||
enter={'transition duration-100 ease-out'}
|
||||
enterFrom={'transition scale-95 opacity-0'}
|
||||
enterTo={'transform scale-100 opacity-100'}
|
||||
leave={'transition duration-75 ease-out'}
|
||||
leaveFrom={'transform scale-100 opacity-100'}
|
||||
leaveTo={'transform scale-95 opacity-0'}
|
||||
>
|
||||
<Menu.Items className={classNames(styles.items_container, 'w-56')}>
|
||||
<div className={'px-1 py-1'}>
|
||||
{items}
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
});
|
||||
|
||||
const _Dropdown = Object.assign(Dropdown, {
|
||||
Button: DropdownButton,
|
||||
Item: DropdownItem,
|
||||
Gap: DropdownGap,
|
||||
});
|
||||
|
||||
export { _Dropdown as default };
|
|
@ -0,0 +1,24 @@
|
|||
import classNames from 'classnames';
|
||||
import styles from '@/components/elements/dropdown/style.module.css';
|
||||
import { ChevronDownIcon } from '@heroicons/react/solid';
|
||||
import { Menu } from '@headlessui/react';
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
animate?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default ({ className, animate = true, children }: Props) => (
|
||||
<Menu.Button className={classNames(styles.button, className || 'px-4')}>
|
||||
{typeof children === 'string' ?
|
||||
<>
|
||||
<span className={'mr-2'}>{children}</span>
|
||||
<ChevronDownIcon aria-hidden={'true'} data-animated={animate.toString()}/>
|
||||
</>
|
||||
:
|
||||
children
|
||||
}
|
||||
</Menu.Button>
|
||||
);
|
|
@ -0,0 +1,43 @@
|
|||
import React, { forwardRef } from 'react';
|
||||
import { Menu } from '@headlessui/react';
|
||||
import styles from './style.module.css';
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode | ((opts: { active: boolean; disabled: boolean }) => JSX.Element);
|
||||
danger?: boolean;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
icon?: JSX.Element;
|
||||
onClick?: (e: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
const DropdownItem = forwardRef<HTMLAnchorElement, Props>(({
|
||||
disabled,
|
||||
danger,
|
||||
className,
|
||||
onClick,
|
||||
children,
|
||||
icon: IconComponent,
|
||||
}, ref) => {
|
||||
return (
|
||||
<Menu.Item disabled={disabled}>
|
||||
{({ disabled, active }) => (
|
||||
<a
|
||||
ref={ref}
|
||||
href={'#'}
|
||||
className={classNames(styles.menu_item, {
|
||||
[styles.danger]: danger,
|
||||
[styles.disabled]: disabled,
|
||||
}, className)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{IconComponent}
|
||||
{typeof children === 'function' ? children({ disabled, active }) : children}
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
);
|
||||
});
|
||||
|
||||
export default DropdownItem;
|
2
resources/scripts/components/elements/dropdown/index.ts
Normal file
2
resources/scripts/components/elements/dropdown/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default as Dropdown } from './Dropdown';
|
||||
export * as styles from './style.module.css';
|
|
@ -0,0 +1,58 @@
|
|||
.menu {
|
||||
@apply relative inline-block text-left;
|
||||
|
||||
& .button {
|
||||
@apply inline-flex justify-center items-center w-full py-2 text-neutral-100 rounded-md;
|
||||
@apply transition-all duration-100;
|
||||
|
||||
&:hover, &[aria-expanded="true"] {
|
||||
@apply bg-neutral-600 text-white;
|
||||
}
|
||||
|
||||
&:focus, &:focus-within, &:active {
|
||||
@apply ring-2 ring-opacity-50 ring-neutral-300 text-white;
|
||||
}
|
||||
|
||||
& svg {
|
||||
@apply w-5 h-5 transition-transform duration-75;
|
||||
}
|
||||
|
||||
&[aria-expanded="true"] svg[data-animated="true"] {
|
||||
@apply rotate-180;
|
||||
}
|
||||
}
|
||||
|
||||
& .items_container {
|
||||
@apply absolute right-0 mt-2 origin-top-right bg-neutral-900 rounded z-10;
|
||||
}
|
||||
}
|
||||
|
||||
.menu_item {
|
||||
@apply flex items-center rounded w-full px-2 py-2;
|
||||
|
||||
& svg {
|
||||
@apply w-4 h-4 mr-4 text-neutral-300;
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
@apply bg-blue-500 text-blue-50;
|
||||
|
||||
& svg {
|
||||
@apply text-blue-50;
|
||||
}
|
||||
}
|
||||
|
||||
&.danger {
|
||||
&:hover, &:focus {
|
||||
@apply bg-red-500 text-red-50;
|
||||
|
||||
& svg {
|
||||
@apply text-red-50;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
@apply cursor-not-allowed hover:bg-neutral-800 opacity-30 focus:bg-transparent focus:hover:bg-neutral-800;
|
||||
}
|
||||
}
|
11
resources/scripts/components/elements/inputs/Checkbox.tsx
Normal file
11
resources/scripts/components/elements/inputs/Checkbox.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import React, { forwardRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
export default forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(({ className, ...props }, ref) => (
|
||||
<input
|
||||
ref={ref}
|
||||
className={classNames('form-input', styles.text_input, className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
11
resources/scripts/components/elements/inputs/InputField.tsx
Normal file
11
resources/scripts/components/elements/inputs/InputField.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import React, { forwardRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
export default forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(({ className, ...props }, ref) => (
|
||||
<input
|
||||
ref={ref}
|
||||
className={classNames('form-input', styles.text_input, className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
3
resources/scripts/components/elements/inputs/index.ts
Normal file
3
resources/scripts/components/elements/inputs/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export { default as Checkbox } from './Checkbox';
|
||||
export { default as InputField } from './InputField';
|
||||
export { default as styles } from './styles.module.css';
|
|
@ -0,0 +1,21 @@
|
|||
.checkbox {
|
||||
@apply w-4 h-4 rounded-sm border-neutral-500 bg-neutral-600 text-primary-500;
|
||||
|
||||
&:focus, &:active {
|
||||
@apply ring-2 ring-primary-500 ring-offset-2 ring-offset-neutral-700;
|
||||
}
|
||||
|
||||
&.indeterminate:checked {
|
||||
@apply text-primary-500/50 border border-primary-500;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='white'%3E%3Cpath fill-rule='evenodd' d='M5 10a1 1 0 011-1h8a1 1 0 110 2H6a1 1 0 01-1-1z' clip-rule='evenodd' /%3E%3C/svg%3E");
|
||||
}
|
||||
}
|
||||
|
||||
.text_input {
|
||||
@apply transition-all duration-75;
|
||||
@apply bg-neutral-800 border-neutral-600 rounded px-4 py-2 outline-none;
|
||||
|
||||
&:focus {
|
||||
@apply border-blue-600 ring-2 ring-blue-500;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
import { Transition } from '@headlessui/react';
|
||||
|
||||
type Duration = `duration-${number}`;
|
||||
|
||||
interface Props {
|
||||
as?: React.ElementType;
|
||||
duration?: Duration | [ Duration, Duration ];
|
||||
show: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default ({ children, duration, ...props }: Props) => {
|
||||
const [ enterDuration, exitDuration ] = Array.isArray(duration)
|
||||
? duration
|
||||
: (!duration ? [ 'duration-200', 'duration-100' ] : [ duration, duration ]);
|
||||
|
||||
return (
|
||||
<Transition
|
||||
{...props}
|
||||
enter={`ease-out ${enterDuration}`}
|
||||
enterFrom={'opacity-0'}
|
||||
enterTo={'opacity-100'}
|
||||
leave={`ease-in ${exitDuration}`}
|
||||
leaveFrom={'opacity-100'}
|
||||
leaveTo={'opacity-0'}
|
||||
>
|
||||
{children}
|
||||
</Transition>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
import { Transition as TransitionComponent } from '@headlessui/react';
|
||||
import FadeTransition from '@/components/elements/transitions/FadeTransition';
|
||||
|
||||
const Transition = Object.assign(TransitionComponent, {
|
||||
Fade: FadeTransition,
|
||||
});
|
||||
|
||||
export { Transition };
|
Loading…
Add table
Add a link
Reference in a new issue