ui-lib.tsx 9.0 KB

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