ui-lib.tsx 5.0 KB

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