|
|
import styles from "./ui-lib.module.scss"; |
|
|
import LoadingIcon from "../icons/three-dots.svg"; |
|
|
import CloseIcon from "../icons/close.svg"; |
|
|
import EyeIcon from "../icons/eye.svg"; |
|
|
import EyeOffIcon from "../icons/eye-off.svg"; |
|
|
|
|
|
import { createRoot } from "react-dom/client"; |
|
|
import React, { HTMLProps, useEffect, useState } from "react"; |
|
|
import { IconButton } from "./button"; |
|
|
|
|
|
export function Popover(props: { |
|
|
children: JSX.Element; |
|
|
content: JSX.Element; |
|
|
open?: boolean; |
|
|
onClose?: () => void; |
|
|
}) { |
|
|
return ( |
|
|
<div className={styles.popover}> |
|
|
{props.children} |
|
|
{props.open && ( |
|
|
<div className={styles["popover-content"]}> |
|
|
<div className={styles["popover-mask"]} onClick={props.onClose}></div> |
|
|
{props.content} |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
); |
|
|
} |
|
|
|
|
|
export function Card(props: { children: JSX.Element[]; className?: string }) { |
|
|
return ( |
|
|
<div className={styles.card + " " + props.className}>{props.children}</div> |
|
|
); |
|
|
} |
|
|
|
|
|
export function ListItem(props: { |
|
|
title: string; |
|
|
subTitle?: string; |
|
|
children?: JSX.Element | JSX.Element[]; |
|
|
icon?: JSX.Element; |
|
|
className?: string; |
|
|
}) { |
|
|
return ( |
|
|
<div className={styles["list-item"] + ` ${props.className}`}> |
|
|
<div className={styles["list-header"]}> |
|
|
{props.icon && <div className={styles["list-icon"]}>{props.icon}</div>} |
|
|
<div className={styles["list-item-title"]}> |
|
|
<div>{props.title}</div> |
|
|
{props.subTitle && ( |
|
|
<div className={styles["list-item-sub-title"]}> |
|
|
{props.subTitle} |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
</div> |
|
|
{props.children} |
|
|
</div> |
|
|
); |
|
|
} |
|
|
|
|
|
export function List(props: { |
|
|
children: |
|
|
| Array<JSX.Element | null | undefined> |
|
|
| JSX.Element |
|
|
| null |
|
|
| undefined; |
|
|
}) { |
|
|
return <div className={styles.list}>{props.children}</div>; |
|
|
} |
|
|
|
|
|
export function Loading() { |
|
|
return ( |
|
|
<div |
|
|
style={{ |
|
|
height: "100vh", |
|
|
width: "100vw", |
|
|
display: "flex", |
|
|
alignItems: "center", |
|
|
justifyContent: "center", |
|
|
}} |
|
|
> |
|
|
<LoadingIcon /> |
|
|
</div> |
|
|
); |
|
|
} |
|
|
|
|
|
interface ModalProps { |
|
|
title: string; |
|
|
children?: JSX.Element | JSX.Element[]; |
|
|
actions?: JSX.Element[]; |
|
|
onClose?: () => void; |
|
|
} |
|
|
export function Modal(props: ModalProps) { |
|
|
useEffect(() => { |
|
|
const onKeyDown = (e: KeyboardEvent) => { |
|
|
if (e.key === "Escape") { |
|
|
props.onClose?.(); |
|
|
} |
|
|
}; |
|
|
|
|
|
window.addEventListener("keydown", onKeyDown); |
|
|
|
|
|
return () => { |
|
|
window.removeEventListener("keydown", onKeyDown); |
|
|
}; |
|
|
|
|
|
}, []); |
|
|
|
|
|
return ( |
|
|
<div className={styles["modal-container"]}> |
|
|
<div className={styles["modal-header"]}> |
|
|
<div className={styles["modal-title"]}>{props.title}</div> |
|
|
|
|
|
<div className={styles["modal-close-btn"]} onClick={props.onClose}> |
|
|
<CloseIcon /> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div className={styles["modal-content"]}>{props.children}</div> |
|
|
|
|
|
<div className={styles["modal-footer"]}> |
|
|
<div className={styles["modal-actions"]}> |
|
|
{props.actions?.map((action, i) => ( |
|
|
<div key={i} className={styles["modal-action"]}> |
|
|
{action} |
|
|
</div> |
|
|
))} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
} |
|
|
|
|
|
export function showModal(props: ModalProps) { |
|
|
const div = document.createElement("div"); |
|
|
div.className = "modal-mask"; |
|
|
document.body.appendChild(div); |
|
|
|
|
|
const root = createRoot(div); |
|
|
const closeModal = () => { |
|
|
props.onClose?.(); |
|
|
root.unmount(); |
|
|
div.remove(); |
|
|
}; |
|
|
|
|
|
div.onclick = (e) => { |
|
|
if (e.target === div) { |
|
|
closeModal(); |
|
|
} |
|
|
}; |
|
|
|
|
|
root.render(<Modal {...props} onClose={closeModal}></Modal>); |
|
|
} |
|
|
|
|
|
export type ToastProps = { |
|
|
content: string; |
|
|
action?: { |
|
|
text: string; |
|
|
onClick: () => void; |
|
|
}; |
|
|
onClose?: () => void; |
|
|
}; |
|
|
|
|
|
export function Toast(props: ToastProps) { |
|
|
return ( |
|
|
<div className={styles["toast-container"]}> |
|
|
<div className={styles["toast-content"]}> |
|
|
<span>{props.content}</span> |
|
|
{props.action && ( |
|
|
<button |
|
|
onClick={() => { |
|
|
props.action?.onClick?.(); |
|
|
props.onClose?.(); |
|
|
}} |
|
|
className={styles["toast-action"]} |
|
|
> |
|
|
{props.action.text} |
|
|
</button> |
|
|
)} |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
} |
|
|
|
|
|
export function showToast( |
|
|
content: string, |
|
|
action?: ToastProps["action"], |
|
|
delay = 3000, |
|
|
) { |
|
|
const div = document.createElement("div"); |
|
|
div.className = styles.show; |
|
|
document.body.appendChild(div); |
|
|
|
|
|
const root = createRoot(div); |
|
|
const close = () => { |
|
|
div.classList.add(styles.hide); |
|
|
|
|
|
setTimeout(() => { |
|
|
root.unmount(); |
|
|
div.remove(); |
|
|
}, 300); |
|
|
}; |
|
|
|
|
|
setTimeout(() => { |
|
|
close(); |
|
|
}, delay); |
|
|
|
|
|
root.render(<Toast content={content} action={action} onClose={close} />); |
|
|
} |
|
|
|
|
|
export type InputProps = React.HTMLProps<HTMLTextAreaElement> & { |
|
|
autoHeight?: boolean; |
|
|
rows?: number; |
|
|
}; |
|
|
|
|
|
export function Input(props: InputProps) { |
|
|
return ( |
|
|
<textarea |
|
|
{...props} |
|
|
className={`${styles["input"]} ${props.className}`} |
|
|
></textarea> |
|
|
); |
|
|
} |
|
|
|
|
|
export function PasswordInput(props: HTMLProps<HTMLInputElement>) { |
|
|
const [visible, setVisible] = useState(false); |
|
|
|
|
|
function changeVisibility() { |
|
|
setVisible(!visible); |
|
|
} |
|
|
|
|
|
return ( |
|
|
<div className={"password-input-container"}> |
|
|
<IconButton |
|
|
icon={visible ? <EyeIcon /> : <EyeOffIcon />} |
|
|
onClick={changeVisibility} |
|
|
className={"password-eye"} |
|
|
/> |
|
|
<input |
|
|
{...props} |
|
|
type={visible ? "text" : "password"} |
|
|
className={"password-input"} |
|
|
/> |
|
|
</div> |
|
|
); |
|
|
} |
|
|
|