ui-lib.tsx 5.5 KB

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