ui-lib.tsx 7.0 KB

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