ui-lib.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. import styles from "./ui-lib.module.scss";
  2. import LoadingIcon from "../icons/three-dots.svg";
  3. import CloseIcon from "../icons/close.svg";
  4. import EyeIcon from "../icons/eye.svg";
  5. import EyeOffIcon from "../icons/eye-off.svg";
  6. import DownIcon from "../icons/down.svg";
  7. import { createRoot } from "react-dom/client";
  8. import React, { HTMLProps, useEffect, useState } from "react";
  9. import { IconButton } from "./button";
  10. export function Popover(props: {
  11. children: JSX.Element;
  12. content: JSX.Element;
  13. open?: boolean;
  14. onClose?: () => void;
  15. }) {
  16. return (
  17. <div className={styles.popover}>
  18. {props.children}
  19. {props.open && (
  20. <div className={styles["popover-content"]}>
  21. <div className={styles["popover-mask"]} onClick={props.onClose}></div>
  22. {props.content}
  23. </div>
  24. )}
  25. </div>
  26. );
  27. }
  28. export function Card(props: { children: JSX.Element[]; className?: string }) {
  29. return (
  30. <div className={styles.card + " " + props.className}>{props.children}</div>
  31. );
  32. }
  33. export function ListItem(props: {
  34. title: string;
  35. subTitle?: string;
  36. children?: JSX.Element | JSX.Element[];
  37. icon?: JSX.Element;
  38. className?: string;
  39. }) {
  40. return (
  41. <div className={styles["list-item"] + ` ${props.className}`}>
  42. <div className={styles["list-header"]}>
  43. {props.icon && <div className={styles["list-icon"]}>{props.icon}</div>}
  44. <div className={styles["list-item-title"]}>
  45. <div>{props.title}</div>
  46. {props.subTitle && (
  47. <div className={styles["list-item-sub-title"]}>
  48. {props.subTitle}
  49. </div>
  50. )}
  51. </div>
  52. </div>
  53. {props.children}
  54. </div>
  55. );
  56. }
  57. export function List(props: {
  58. children:
  59. | Array<JSX.Element | null | undefined>
  60. | JSX.Element
  61. | null
  62. | undefined;
  63. }) {
  64. return <div className={styles.list}>{props.children}</div>;
  65. }
  66. export function Loading() {
  67. return (
  68. <div
  69. style={{
  70. height: "100vh",
  71. width: "100vw",
  72. display: "flex",
  73. alignItems: "center",
  74. justifyContent: "center",
  75. }}
  76. >
  77. <LoadingIcon />
  78. </div>
  79. );
  80. }
  81. interface ModalProps {
  82. title: string;
  83. children?: JSX.Element | JSX.Element[];
  84. actions?: JSX.Element[];
  85. onClose?: () => void;
  86. }
  87. export function Modal(props: ModalProps) {
  88. useEffect(() => {
  89. const onKeyDown = (e: KeyboardEvent) => {
  90. if (e.key === "Escape") {
  91. props.onClose?.();
  92. }
  93. };
  94. window.addEventListener("keydown", onKeyDown);
  95. return () => {
  96. window.removeEventListener("keydown", onKeyDown);
  97. };
  98. // eslint-disable-next-line react-hooks/exhaustive-deps
  99. }, []);
  100. return (
  101. <div className={styles["modal-container"]}>
  102. <div className={styles["modal-header"]}>
  103. <div className={styles["modal-title"]}>{props.title}</div>
  104. <div className={styles["modal-close-btn"]} onClick={props.onClose}>
  105. <CloseIcon />
  106. </div>
  107. </div>
  108. <div className={styles["modal-content"]}>{props.children}</div>
  109. <div className={styles["modal-footer"]}>
  110. <div className={styles["modal-actions"]}>
  111. {props.actions?.map((action, i) => (
  112. <div key={i} className={styles["modal-action"]}>
  113. {action}
  114. </div>
  115. ))}
  116. </div>
  117. </div>
  118. </div>
  119. );
  120. }
  121. export function showModal(props: ModalProps) {
  122. const div = document.createElement("div");
  123. div.className = "modal-mask";
  124. document.body.appendChild(div);
  125. const root = createRoot(div);
  126. const closeModal = () => {
  127. props.onClose?.();
  128. root.unmount();
  129. div.remove();
  130. };
  131. div.onclick = (e) => {
  132. if (e.target === div) {
  133. closeModal();
  134. }
  135. };
  136. root.render(<Modal {...props} onClose={closeModal}></Modal>);
  137. }
  138. export type ToastProps = {
  139. content: string;
  140. action?: {
  141. text: string;
  142. onClick: () => void;
  143. };
  144. onClose?: () => void;
  145. };
  146. export function Toast(props: ToastProps) {
  147. return (
  148. <div className={styles["toast-container"]}>
  149. <div className={styles["toast-content"]}>
  150. <span>{props.content}</span>
  151. {props.action && (
  152. <button
  153. onClick={() => {
  154. props.action?.onClick?.();
  155. props.onClose?.();
  156. }}
  157. className={styles["toast-action"]}
  158. >
  159. {props.action.text}
  160. </button>
  161. )}
  162. </div>
  163. </div>
  164. );
  165. }
  166. export function showToast(
  167. content: string,
  168. action?: ToastProps["action"],
  169. delay = 3000,
  170. ) {
  171. const div = document.createElement("div");
  172. div.className = styles.show;
  173. document.body.appendChild(div);
  174. const root = createRoot(div);
  175. const close = () => {
  176. div.classList.add(styles.hide);
  177. setTimeout(() => {
  178. root.unmount();
  179. div.remove();
  180. }, 300);
  181. };
  182. setTimeout(() => {
  183. close();
  184. }, delay);
  185. root.render(<Toast content={content} action={action} onClose={close} />);
  186. }
  187. export type InputProps = React.HTMLProps<HTMLTextAreaElement> & {
  188. autoHeight?: boolean;
  189. rows?: number;
  190. };
  191. export function Input(props: InputProps) {
  192. return (
  193. <textarea
  194. {...props}
  195. className={`${styles["input"]} ${props.className}`}
  196. ></textarea>
  197. );
  198. }
  199. export function PasswordInput(props: HTMLProps<HTMLInputElement>) {
  200. const [visible, setVisible] = useState(false);
  201. function changeVisibility() {
  202. setVisible(!visible);
  203. }
  204. return (
  205. <div className={"password-input-container"}>
  206. <IconButton
  207. icon={visible ? <EyeIcon /> : <EyeOffIcon />}
  208. onClick={changeVisibility}
  209. className={"password-eye"}
  210. />
  211. <input
  212. {...props}
  213. type={visible ? "text" : "password"}
  214. className={"password-input"}
  215. />
  216. </div>
  217. );
  218. }
  219. export function Select(
  220. props: React.DetailedHTMLProps<
  221. React.SelectHTMLAttributes<HTMLSelectElement>,
  222. HTMLSelectElement
  223. >,
  224. ) {
  225. const { className, children, ...otherProps } = props;
  226. return (
  227. <div className={`${styles["select-with-icon"]} ${className}`}>
  228. <select className={styles["select-with-icon-select"]} {...otherProps}>
  229. {children}
  230. </select>
  231. <DownIcon className={styles["select-with-icon-icon"]} />
  232. </div>
  233. );
  234. }