Spaces:
Running
Running
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 DownIcon from "../icons/down.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); | |
}; | |
// eslint-disable-next-line react-hooks/exhaustive-deps | |
}, []); | |
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> | |
); | |
} | |
export function Select( | |
props: React.DetailedHTMLProps< | |
React.SelectHTMLAttributes<HTMLSelectElement>, | |
HTMLSelectElement | |
>, | |
) { | |
const { className, children, ...otherProps } = props; | |
return ( | |
<div className={`${styles["select-with-icon"]} ${className}`}> | |
<select className={styles["select-with-icon-select"]} {...otherProps}> | |
{children} | |
</select> | |
<DownIcon className={styles["select-with-icon-icon"]} /> | |
</div> | |
); | |
} |