ui-lib.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  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 { createRoot } from "react-dom/client";
  5. import React, { useEffect } from "react";
  6. export function Popover(props: {
  7. children: JSX.Element;
  8. content: JSX.Element;
  9. open?: boolean;
  10. onClose?: () => void;
  11. }) {
  12. return (
  13. <div className={styles.popover}>
  14. {props.children}
  15. {props.open && (
  16. <div className={styles["popover-content"]}>
  17. <div className={styles["popover-mask"]} onClick={props.onClose}></div>
  18. {props.content}
  19. </div>
  20. )}
  21. </div>
  22. );
  23. }
  24. export function Card(props: { children: JSX.Element[]; className?: string }) {
  25. return (
  26. <div className={styles.card + " " + props.className}>{props.children}</div>
  27. );
  28. }
  29. export function ListItem(props: { children: JSX.Element[] }) {
  30. if (props.children.length > 2) {
  31. throw Error("Only Support Two Children");
  32. }
  33. return <div className={styles["list-item"]}>{props.children}</div>;
  34. }
  35. export function List(props: { children: JSX.Element[] | JSX.Element }) {
  36. return <div className={styles.list}>{props.children}</div>;
  37. }
  38. export function Loading() {
  39. return (
  40. <div
  41. style={{
  42. height: "100vh",
  43. width: "100vw",
  44. display: "flex",
  45. alignItems: "center",
  46. justifyContent: "center",
  47. }}
  48. >
  49. <LoadingIcon />
  50. </div>
  51. );
  52. }
  53. interface ModalProps {
  54. title: string;
  55. children?: JSX.Element;
  56. actions?: JSX.Element[];
  57. onClose?: () => void;
  58. }
  59. export function Modal(props: ModalProps) {
  60. useEffect(() => {
  61. const onKeyDown = (e: KeyboardEvent) => {
  62. if (e.key === "Escape") {
  63. props.onClose?.();
  64. }
  65. };
  66. window.addEventListener("keydown", onKeyDown);
  67. return () => {
  68. window.removeEventListener("keydown", onKeyDown);
  69. };
  70. // eslint-disable-next-line react-hooks/exhaustive-deps
  71. }, []);
  72. return (
  73. <div className={styles["modal-container"]}>
  74. <div className={styles["modal-header"]}>
  75. <div className={styles["modal-title"]}>{props.title}</div>
  76. <div className={styles["modal-close-btn"]} onClick={props.onClose}>
  77. <CloseIcon />
  78. </div>
  79. </div>
  80. <div className={styles["modal-content"]}>{props.children}</div>
  81. <div className={styles["modal-footer"]}>
  82. <div className={styles["modal-actions"]}>
  83. {props.actions?.map((action, i) => (
  84. <div key={i} className={styles["modal-action"]}>
  85. {action}
  86. </div>
  87. ))}
  88. </div>
  89. </div>
  90. </div>
  91. );
  92. }
  93. export function showModal(props: ModalProps) {
  94. const div = document.createElement("div");
  95. div.className = "modal-mask";
  96. document.body.appendChild(div);
  97. const root = createRoot(div);
  98. const closeModal = () => {
  99. props.onClose?.();
  100. root.unmount();
  101. div.remove();
  102. };
  103. div.onclick = (e) => {
  104. if (e.target === div) {
  105. closeModal();
  106. }
  107. };
  108. root.render(<Modal {...props} onClose={closeModal}></Modal>);
  109. }
  110. export type ToastProps = {
  111. content: string;
  112. action?: {
  113. text: string;
  114. onClick: () => void;
  115. };
  116. };
  117. export function Toast(props: ToastProps) {
  118. return (
  119. <div className={styles["toast-container"]}>
  120. <div className={styles["toast-content"]}>
  121. <span>{props.content}</span>
  122. {props.action && (
  123. <button
  124. onClick={props.action.onClick}
  125. className={styles["toast-action"]}
  126. >
  127. {props.action.text}
  128. </button>
  129. )}
  130. </div>
  131. </div>
  132. );
  133. }
  134. export function showToast(
  135. content: string,
  136. action?: ToastProps["action"],
  137. delay = 3000,
  138. ) {
  139. const div = document.createElement("div");
  140. div.className = styles.show;
  141. document.body.appendChild(div);
  142. const root = createRoot(div);
  143. const close = () => {
  144. div.classList.add(styles.hide);
  145. setTimeout(() => {
  146. root.unmount();
  147. div.remove();
  148. }, 300);
  149. };
  150. setTimeout(() => {
  151. close();
  152. }, delay);
  153. root.render(<Toast content={content} action={action} />);
  154. }
  155. export type InputProps = React.HTMLProps<HTMLTextAreaElement> & {
  156. autoHeight?: boolean;
  157. rows?: number;
  158. };
  159. export function Input(props: InputProps) {
  160. return (
  161. <textarea
  162. {...props}
  163. className={`${styles["input"]} ${props.className}`}
  164. ></textarea>
  165. );
  166. }