Initial 1.3.x rework

This commit is contained in:
Sacha Weatherstone
2022-07-26 08:52:23 +10:00
parent ff6019098b
commit a2280ab96e
163 changed files with 6383 additions and 14397 deletions
-48
View File
@@ -1,48 +0,0 @@
import type React from 'react';
import { m } from 'framer-motion';
export interface CardProps {
className?: string;
title?: string;
actions?: React.ReactNode;
children: React.ReactNode;
border?: boolean;
}
export const Card = ({
className,
title,
actions,
border,
children,
}: CardProps): JSX.Element => {
return (
<div
className={`flex h-full w-full flex-col rounded-md drop-shadow-md ${
border ? 'border border-gray-400 dark:border-gray-600' : ''
} ${className ?? ''}`}
>
{(title || actions) && (
<div className="w-full select-none justify-between rounded-t-md border-b border-gray-400 bg-gray-200 p-2 px-2 text-lg font-medium dark:border-gray-600 dark:bg-tertiaryDark dark:text-white">
<div className="handle flex h-8 justify-between">
<div className="my-auto ml-2 truncate">{title}</div>
{actions}
</div>
</div>
)}
<m.div
className={`flex flex-grow select-none flex-col gap-4 bg-white p-4 dark:bg-primaryDark ${
title || actions ? 'rounded-b-md' : 'rounded-md'
}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.1 }}
>
{children}
</m.div>
</div>
);
};
-23
View File
@@ -1,23 +0,0 @@
import type React from 'react';
import { m } from 'framer-motion';
export interface ContextItem {
title: string;
icon: JSX.Element;
}
export const ContextItem = ({ title, icon }: ContextItem): JSX.Element => {
return (
<div className="cursor-pointer first:rounded-t-md last:rounded-b-md hover:dark:bg-secondaryDark">
<m.div
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.99 }}
className="flex gap-2 p-2"
>
<div className="my-auto">{icon}</div>
<div className="truncate">{title}</div>
</m.div>
</div>
);
};
-57
View File
@@ -1,57 +0,0 @@
import type React from 'react';
import { useState } from 'react';
import { FiActivity, FiAperture, FiTag } from 'react-icons/fi';
import { ContextItem } from '@components/generic/ContextItem';
export interface ContextMenuProps {
items?: JSX.Element;
children: React.ReactNode;
}
export const ContextMenu = ({
items,
children,
}: ContextMenuProps): JSX.Element => {
const [visible, setVisible] = useState(false);
const [position, setPosition] = useState({ x: 0, y: 0 });
return (
<div
className="h-full"
onContextMenu={(e): void => {
e.preventDefault();
setVisible(false);
const newPosition = {
x: e.pageX,
y: e.pageY,
};
setPosition(newPosition);
setVisible(true);
}}
onClick={(): void => {
setVisible(false);
}}
>
{children}
{visible && (
<div
style={{ top: position.y, left: position.x }}
className="fixed z-50 w-60 gap-2 divide-y divide-gray-300 rounded-md border border-gray-400 font-medium drop-shadow-md backdrop-blur-xl dark:divide-gray-600 dark:border-gray-600 dark:text-gray-400"
>
{items}
<ContextItem title="Menu item" icon={<FiActivity />} />
<ContextItem title="Menu item 2" icon={<FiAperture />} />
<ContextItem
title="Menu item 3 with a very long name that should wrap"
icon={<FiTag />}
/>
</div>
)}
</div>
);
};
-9
View File
@@ -1,9 +0,0 @@
import type React from 'react';
export const Loading = (): JSX.Element => {
return (
<div className="absolute top-0 bottom-0 left-0 right-0 z-10 flex rounded-md backdrop-blur-sm backdrop-filter">
<div className="m-auto text-lg font-medium text-gray-400">Loading</div>
</div>
);
};
-72
View File
@@ -1,72 +0,0 @@
import type React from 'react';
import { AnimatePresence, m } from 'framer-motion';
import { FiX } from 'react-icons/fi';
import { IconButton } from '@components/generic/button/IconButton';
import { Card, CardProps } from '@components/generic/Card';
import { useAppSelector } from '@hooks/useAppSelector';
export interface ModalProps extends CardProps {
open: boolean;
bgDismiss?: boolean;
onClose: () => void;
}
export const Modal = ({
open,
bgDismiss,
onClose,
actions,
...props
}: ModalProps): JSX.Element => {
const darkMode = useAppSelector((state) => state.app.darkMode);
return (
<AnimatePresence>
{open && (
<m.div
className={`fixed inset-0 ${darkMode ? 'dark' : ''} ${
open ? 'z-30' : 'z-0'
}`}
>
<m.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.1 }}
className="fixed h-full w-full backdrop-blur-md backdrop-brightness-75 backdrop-filter"
onClick={(): void => {
bgDismiss && onClose();
}}
/>
<m.div className="text-center ">
<span
className="inline-block h-screen align-middle "
aria-hidden="true"
>
&#8203;
</span>
<div className="inline-block w-full max-w-3xl align-middle">
<Card
border
actions={
<div className="flex gap-2">
{actions}
<IconButton
tooltip="Close"
icon={<FiX />}
onClick={onClose}
/>
</div>
}
className="relative flex-col"
{...props}
/>
</div>
</m.div>
</m.div>
)}
</AnimatePresence>
);
};
@@ -1,85 +0,0 @@
import type React from 'react';
import { useState } from 'react';
import { AnimatePresence, m } from 'framer-motion';
import { FiArrowUp } from 'react-icons/fi';
export interface CollapsibleSectionProps {
title: string;
icon?: JSX.Element;
status?: boolean;
children: JSX.Element;
}
export const CollapsibleSection = ({
title,
icon,
status,
children,
}: CollapsibleSectionProps): JSX.Element => {
const [open, setOpen] = useState(false);
const toggleOpen = (): void => setOpen(!open);
return (
<m.div>
<m.div
layout
onClick={toggleOpen}
className={`w-full cursor-pointer select-none overflow-hidden border-l-4 border-b bg-gray-200 p-2 text-sm font-medium dark:border-primaryDark dark:bg-tertiaryDark dark:text-gray-400 ${
open
? 'border-l-primary dark:border-l-primary'
: 'border-gray-400 dark:border-secondaryDark'
}`}
>
<m.div
layout
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.99 }}
className="my-auto flex justify-between gap-2"
>
<m.div className="flex gap-2">
<m.div className="my-auto flex gap-2">
{status !== undefined ? (
<>
<div
className={`my-auto h-2 w-2 rounded-full ${
status ? 'bg-green-500' : 'bg-red-500'
}`}
/>
{icon}
</>
) : (
<>{icon}</>
)}
</m.div>
{title}
</m.div>
<m.div
animate={open ? 'open' : 'closed'}
initial={{ rotate: 180 }}
variants={{
open: { rotate: 0 },
closed: { rotate: 180 },
}}
transition={{ type: 'just' }}
className="my-auto"
>
<FiArrowUp />
</m.div>
</m.div>
</m.div>
<AnimatePresence>
{open && (
<m.div
className="p-2"
layout
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
{children}
</m.div>
)}
</AnimatePresence>
</m.div>
);
};
@@ -1,54 +0,0 @@
import type React from 'react';
import { useState } from 'react';
import { m } from 'framer-motion';
import { FiChevronRight } from 'react-icons/fi';
export interface ExternalSectionProps {
title: string;
icon?: JSX.Element;
active?: boolean;
onClick: () => void;
}
export const ExternalSection = ({
title,
icon,
active,
onClick,
}: ExternalSectionProps): JSX.Element => {
const [open, setOpen] = useState(false);
const toggleOpen = (): void => setOpen(!open);
return (
<m.div
onClick={(): void => {
onClick();
}}
>
<m.div
layout
className={`w-full cursor-pointer select-none overflow-hidden border-l-4 bg-gray-200 dark:bg-tertiaryDark dark:text-gray-400 ${
active
? 'border-l-primary dark:border-l-primary'
: 'border-gray-400 dark:border-secondaryDark'
}`}
>
<m.div
layout
onClick={toggleOpen}
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.99 }}
className="flex justify-between gap-2 border-b border-gray-400 p-2 text-sm font-medium dark:border-primaryDark"
>
<m.div className="flex gap-2 ">
<m.div className="my-auto">{icon}</m.div>
{title}
</m.div>
<m.div className="my-auto">
<FiChevronRight />
</m.div>
</m.div>
</m.div>
</m.div>
);
};
@@ -1,61 +0,0 @@
import type React from 'react';
import { AnimatePresence, AnimateSharedLayout, m } from 'framer-motion';
import { FiArrowLeft } from 'react-icons/fi';
import { IconButton } from '@components/generic/button/IconButton';
export interface SidebarOverlayProps {
title: string;
open: boolean;
close: () => void;
direction: 'x' | 'y';
children: React.ReactNode;
}
export const SidebarOverlay = ({
title,
open,
close,
direction,
children,
}: SidebarOverlayProps): JSX.Element => {
return (
<AnimatePresence>
{open && (
<m.div
className="absolute z-30 flex h-full w-full flex-col bg-white dark:bg-primaryDark"
animate={direction === 'x' ? { translateX: 0 } : { translateY: 0 }}
initial={
direction === 'x' ? { translateX: '-100%' } : { translateY: '100%' }
}
exit={
direction === 'x' ? { translateX: '-100%' } : { translateY: '100%' }
}
transition={{ type: 'just' }}
>
{/* @ts-expect-error */}
<AnimateSharedLayout>
{/* <div className="flex gap-2 border-b border-gray-400 p-2 dark:border-gray-600"> */}
<div className="bg-white px-1 pt-1 drop-shadow-md dark:bg-primaryDark">
<div className="flex h-10 gap-1">
<div className="my-auto">
<IconButton
onClick={(): void => {
close();
}}
icon={<FiArrowLeft />}
/>
</div>
<div className="my-auto text-lg font-medium dark:text-white">
{title}
</div>
</div>
</div>
<div className="flex-grow overflow-y-auto">{children}</div>
</AnimateSharedLayout>
</m.div>
)}
</AnimatePresence>
);
};
-17
View File
@@ -1,17 +0,0 @@
import 'tippy.js/dist/tippy.css';
import type React from 'react';
import Tippy, { TippyProps } from '@tippyjs/react';
export const Tooltip = ({
children,
content,
...props
}: TippyProps): JSX.Element => {
return (
<Tippy content={content} {...props}>
<div>{children}</div>
</Tippy>
);
};
-79
View File
@@ -1,79 +0,0 @@
import type React from 'react';
import { useState } from 'react';
import { m } from 'framer-motion';
import { FiCheck } from 'react-icons/fi';
export enum ButtonSize {
Small = 'small',
Medium = 'medium',
Large = 'large',
}
export interface ButtonProps {
icon?: JSX.Element;
border?: boolean;
className?: string;
disabled?: boolean;
children?: React.ReactNode;
size?: ButtonSize;
onClick?: () => void;
confirmAction?: () => void;
}
export const Button = ({
icon,
className,
border,
size = ButtonSize.Medium,
confirmAction,
onClick,
disabled,
children,
}: ButtonProps): JSX.Element => {
const [hasConfirmed, setHasConfirmed] = useState(false);
const handleConfirm = (): void => {
if (typeof confirmAction == 'function') {
if (hasConfirmed) {
void confirmAction();
}
setHasConfirmed(true);
setTimeout(() => {
setHasConfirmed(false);
}, 3000);
}
};
return (
<m.button
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.97 }}
onClick={handleConfirm}
className={`flex select-none items-center space-x-3 rounded-md border border-transparent text-sm focus-within:border-primary focus-within:shadow-border dark:text-white dark:focus-within:border-primary
${
size === ButtonSize.Small
? 'p-0'
: size === ButtonSize.Medium
? 'p-2'
: 'p-4'
}
${
disabled
? 'cursor-not-allowed bg-white dark:bg-primaryDark'
: 'cursor-pointer hover:bg-white hover:drop-shadow-md dark:hover:bg-secondaryDark'
} ${border ? 'border-gray-400 dark:border-gray-200' : ''} ${
className ?? ''
}`}
onClickCapture={onClick}
>
{icon && (
<div className="text-gray-500 dark:text-gray-400">
{hasConfirmed ? <FiCheck /> : icon}
</div>
)}
<span>{children}</span>
</m.button>
);
};
@@ -1,50 +0,0 @@
import type React from 'react';
import { m } from 'framer-motion';
import { Tooltip } from '@components/generic/Tooltip';
type DefaulButtonProps = JSX.IntrinsicElements['button'];
export interface IconButtonProps extends DefaulButtonProps {
icon: React.ReactNode;
tooltip?: string;
nested?: boolean;
active?: boolean;
}
export const IconButton = ({
icon,
tooltip,
nested,
active,
disabled,
className,
...props
}: IconButtonProps): JSX.Element => {
return (
<Tooltip disabled={!tooltip} content={tooltip}>
<button
type="button"
disabled={disabled}
className={`rounded-md p-2 hover:bg-gray-300 ${
active ? 'bg-gray-300 dark:bg-secondaryDark' : ''
} ${
nested ? 'dark:hover:bg-primaryDark' : 'dark:hover:bg-secondaryDark'
} ${
disabled ? 'cursor-not-allowed text-gray-400 dark:text-gray-700' : ''
} ${className ?? ''}`}
{...props}
>
<m.div
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.95 }}
className="my-auto text-gray-600 dark:text-gray-400"
>
{icon}
</m.div>
<span className="sr-only">Refresh</span>
</button>
</Tooltip>
);
};
-49
View File
@@ -1,49 +0,0 @@
import type React from 'react';
import { forwardRef } from 'react';
import { Label } from '@components/generic/form/Label';
type DefaultInputProps = JSX.IntrinsicElements['input'];
export interface CheckboxProps extends DefaultInputProps {
action?: (enabled: boolean) => void;
label: string;
valid?: boolean;
validationMessage?: string;
error?: boolean;
}
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
function Input(
{ label, valid, validationMessage, id, error, ...props }: CheckboxProps,
ref,
) {
return (
<div className="flex w-full flex-col">
<Label label={label} />
<div className="ml-auto">
<input
ref={ref}
type="checkbox"
id={id}
className={`h-8 w-8 appearance-none rounded-md border border-gray-400 transition duration-200 ease-in-out checked:border-transparent checked:bg-primary focus-within:shadow-border focus:outline-none dark:border-gray-200 ${
props.disabled
? 'border-gray-400 bg-gray-300 text-gray-500 dark:border-gray-700 dark:bg-secondaryDark dark:text-gray-400'
: ''
} ${
error
? 'border-red-500'
: props.disabled
? 'border-gray-200'
: 'focus-within:border-primary hover:border-primary dark:focus-within:border-primary dark:hover:border-primary'
}`}
{...props}
/>
</div>
{!valid && (
<div className="text-sm text-gray-600">{validationMessage}</div>
)}
</div>
);
},
);
-36
View File
@@ -1,36 +0,0 @@
import type React from 'react';
import { FiSave } from 'react-icons/fi';
import { IconButton } from '@components/generic/button/IconButton';
import { Loading } from '@components/generic/Loading';
export interface FormProps {
submit: () => Promise<void>;
loading: boolean;
dirty: boolean;
children: React.ReactNode;
}
export const Form = ({
submit,
loading,
dirty,
children,
}: FormProps): JSX.Element => {
return (
<form
onSubmit={(e): void => {
e.preventDefault();
}}
>
{loading && <Loading />}
{children}
<div className="flex w-full bg-white dark:bg-secondaryDark">
<div className="ml-auto p-2">
<IconButton disabled={dirty} onClick={submit} icon={<FiSave />} />
</div>
</div>
</form>
);
};
-41
View File
@@ -1,41 +0,0 @@
import type React from 'react';
import { forwardRef } from 'react';
import { InputWrapper } from '@components/generic/form/InputWrapper';
import { Label } from '@components/generic/form/Label';
type DefaultInputProps = JSX.IntrinsicElements['input'];
export interface InputProps extends DefaultInputProps {
label?: string;
error?: string;
action?: JSX.Element;
prefix?: string;
suffix?: string;
}
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
{ label, error, action, suffix, className, disabled, ...props }: InputProps,
ref,
) {
return (
<div className="w-full">
{label && <Label label={label} error={error} />}
<InputWrapper error={error} disabled={disabled}>
<input
ref={ref}
className={`h-10 w-full bg-transparent px-3 py-2 focus:outline-none disabled:cursor-not-allowed dark:text-white ${
className ?? ''
}`}
{...props}
/>
{suffix && (
<span className="my-auto mr-3 text-sm font-medium text-gray-500 dark:text-gray-400">
{suffix}
</span>
)}
{action && <div className="mr-1 flex">{action}</div>}
</InputWrapper>
</div>
);
});
@@ -1,29 +0,0 @@
import type React from 'react';
export interface LabelProps {
error?: string;
disabled?: boolean;
children: React.ReactNode;
}
export const InputWrapper = ({
error,
disabled,
children,
}: LabelProps): JSX.Element => (
<div
className={`flex w-full rounded-md border border-gray-400 transition duration-200 ease-in-out dark:border-gray-200 ${
disabled
? 'border-gray-400 bg-gray-300 text-gray-500 dark:border-gray-700 dark:bg-secondaryDark dark:text-gray-400'
: ''
} ${
error
? 'border-red-500 dark:border-red-500'
: disabled
? ''
: ' focus-within:border-primary focus-within:shadow-border hover:border-primary dark:focus-within:border-primary dark:hover:border-primary'
}`}
>
{children}
</div>
);
-14
View File
@@ -1,14 +0,0 @@
import type React from 'react';
export interface LabelProps {
label: string;
error?: string;
}
export const Label = ({ label, error }: LabelProps): JSX.Element => (
<label className="flex py-1 text-xs font-semibold text-gray-500 dark:text-gray-400">
{label}
{error && <span className="ml-2 text-red-500">{error}</span>}
<div className="my-auto ml-2 h-0.5 flex-grow rounded-full bg-gray-300 dark:bg-gray-700" />
</label>
);
-69
View File
@@ -1,69 +0,0 @@
import type React from 'react';
import { forwardRef } from 'react';
import { InputWrapper } from '@components/generic/form/InputWrapper';
import { Label } from '@components/generic/form/Label';
type DefaultSelectProps = JSX.IntrinsicElements['select'];
export interface SelectProps extends DefaultSelectProps {
options?: {
name: string | number;
value: string | number;
}[];
optionsEnum?: { [s: string]: string | number };
label?: string;
error?: string;
small?: boolean;
}
export const Select = forwardRef<HTMLSelectElement, SelectProps>(
({ options, optionsEnum, label, error, small, ...props }, ref) => {
const optionsEnumValues = optionsEnum
? Object.entries(optionsEnum).filter(
(value) => typeof value[1] === 'number',
)
: [];
return (
<div>
{label && <Label label={label} error={error} />}
<InputWrapper error={error} disabled={props.disabled}>
<select
ref={ref}
className={`w-full rounded-md bg-transparent focus:border-primary focus:outline-none disabled:cursor-not-allowed dark:text-white ${
small ? 'm-1' : 'mx-2 h-10'
}`}
disabled={
props.disabled
? true
: !(optionsEnumValues.length || options?.length)
}
{...props}
>
{!(optionsEnumValues.length || options?.length) && (
<option key="loading" className="dark:bg-gray-700">
Loading
</option>
)}
{optionsEnumValues.length &&
optionsEnumValues.map(([name, value], index) => (
<option key={index} className="dark:bg-gray-700" value={value}>
{name}
</option>
))}
{options &&
options.map((option, index) => (
<option
key={index}
className="dark:bg-gray-700"
value={option.value}
>
{option.name}
</option>
))}
</select>
</InputWrapper>
</div>
);
},
);