ui-lib.tsx 5.2 KB

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