From bcef981955db69f9465f92e0c9bd177a391ee311 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Thu, 5 Jun 2025 18:11:53 +0900 Subject: [PATCH] [Update] Change the notification UI. (Change the base notification library from MUI to React-Toastify.) --- package-lock.json | 14 ++ package.json | 1 + .../app/error_boundary/AppErrorBoundary.jsx | 1 + .../AppErrorBoundary.module.scss | 1 + .../ReactToastifyOverrideClass.scss | 109 +++++++++++++++ .../SnackbarController.jsx | 130 +++++++++++++----- .../SnackbarController.module.scss | 94 +++++++++++-- .../app/splash_component/SplashComponent.jsx | 2 +- .../SplashComponent.module.scss | 2 +- src-ui/assets/error.svg | 1 + src-ui/logics/common/useNotificationStatus.js | 3 +- 11 files changed, 308 insertions(+), 50 deletions(-) create mode 100644 src-ui/app/snackbar_controller/ReactToastifyOverrideClass.scss create mode 100644 src-ui/assets/error.svg diff --git a/package-lock.json b/package-lock.json index f131d4a8..cbe6e142 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "react-error-boundary": "5.0.0", "react-i18next": "15.5.1", "react-resizable-layout": "0.7.2", + "react-toastify": "11.0.5", "sass": "1.79.4", "semver": "7.7.1" }, @@ -5534,6 +5535,19 @@ "react-dom": ">=17.0.0" } }, + "node_modules/react-toastify": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz", + "integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", diff --git a/package.json b/package.json index a2d9030f..c15471e5 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "react-error-boundary": "5.0.0", "react-i18next": "15.5.1", "react-resizable-layout": "0.7.2", + "react-toastify": "11.0.5", "sass": "1.79.4", "semver": "7.7.1" }, diff --git a/src-ui/app/error_boundary/AppErrorBoundary.jsx b/src-ui/app/error_boundary/AppErrorBoundary.jsx index 4aeb34a2..37a1cc23 100644 --- a/src-ui/app/error_boundary/AppErrorBoundary.jsx +++ b/src-ui/app/error_boundary/AppErrorBoundary.jsx @@ -65,6 +65,7 @@ const ErrorContainer = ({error}) => { ); }; +// Duplicated const CloseButtonContainer = () => { const { asyncCloseApp } = useWindow(); return ( diff --git a/src-ui/app/error_boundary/AppErrorBoundary.module.scss b/src-ui/app/error_boundary/AppErrorBoundary.module.scss index 2abb2710..2ff85390 100644 --- a/src-ui/app/error_boundary/AppErrorBoundary.module.scss +++ b/src-ui/app/error_boundary/AppErrorBoundary.module.scss @@ -72,6 +72,7 @@ +// Duplicated .close_button_wrapper { position: absolute; top: 0; diff --git a/src-ui/app/snackbar_controller/ReactToastifyOverrideClass.scss b/src-ui/app/snackbar_controller/ReactToastifyOverrideClass.scss new file mode 100644 index 00000000..9325d747 --- /dev/null +++ b/src-ui/app/snackbar_controller/ReactToastifyOverrideClass.scss @@ -0,0 +1,109 @@ +:root { + --toastify-color-light: #fff; + --toastify-color-dark: var(--dark_950_color); + --toastify-color-info: var(--sent_400_color); + --toastify-color-success: var(--primary_400_color); + --toastify-color-warning: var(--waring_bc_color); + --toastify-color-error: var(--error_bc_color); + --toastify-color-transparent: rgba(255, 255, 255, 0.7); + + --toastify-icon-color-info: var(--toastify-color-info); + --toastify-icon-color-success: var(--toastify-color-success); + --toastify-icon-color-warning: var(--toastify-color-warning); + --toastify-icon-color-error: var(--toastify-color-error); + + --toastify-container-width: fit-content; + --toastify-toast-width: 32rem; + --toastify-toast-offset: 1.6rem; + --toastify-toast-top: max(var(--toastify-toast-offset), env(safe-area-inset-top)); + --toastify-toast-right: max(var(--toastify-toast-offset), env(safe-area-inset-right)); + --toastify-toast-left: max(var(--toastify-toast-offset), env(safe-area-inset-left)); + --toastify-toast-bottom: max(var(--toastify-toast-offset), env(safe-area-inset-bottom)); + --toastify-toast-background: #fff; + --toastify-toast-padding: 1.4rem; + --toastify-toast-min-height: 6.4rem; + --toastify-toast-max-height: 80rem; + --toastify-toast-bd-radius: 0.6rem; + --toastify-toast-shadow: .0 0.4rem 1.2rem rgba(0, 0, 0, 0.1); + --toastify-font-family: var(--font_family); + --toastify-z-index: 9999; + --toastify-text-color-light: #757575; + --toastify-text-color-dark: var(--dark_basic_text_color); + + /* Used only for colored theme */ + --toastify-text-color-info: var(--dark_basic_text_color); + --toastify-text-color-success: var(--dark_basic_text_color); + --toastify-text-color-warning: var(--dark_basic_text_color); + --toastify-text-color-error: var(--dark_basic_text_color); + + --toastify-spinner-color: #616161; + --toastify-spinner-color-empty-area: #e0e0e0; + --toastify-color-progress-light: linear-gradient(to right, #4cd964, #5ac8fa, #007aff, #34aadc, #5856d6, #ff2d55); + --toastify-color-progress-dark: #bb86fc; + --toastify-color-progress-info: var(--toastify-color-info); + --toastify-color-progress-success: var(--toastify-color-success); + --toastify-color-progress-warning: var(--toastify-color-warning); + --toastify-color-progress-error: var(--toastify-color-error); + /* used to control the opacity of the progress trail */ + --toastify-color-progress-bgo: 0.2; +} + + +.Toastify__toast { + // -------------------------------------------------------- + // Default Settings + // -------------------------------------------------------- + position: relative; + touch-action: none; + // width: var(--toastify-toast-width); + min-height: var(--toastify-toast-min-height); + box-sizing: border-box; + margin-bottom: 1rem; + // padding: var(--toastify-toast-padding); + border-radius: 0.6rem; + box-shadow: none; + max-height: var(--toastify-toast-max-height); + // font-family: "Yu Gothic UI"; + // font-family: var(--toastify-font-family); + // z-index: 0; + // display: flex; + // flex: 1 auto; + // align-items: center; + word-break: break-word; + // -------------------------------------------------------- + // -------------------------------------------------------- + + + // Comment out above and override. Commented out is just for memorization. + overflow: hidden; + display: flex; + justify-content: start; + align-items: center; + font-size: 1.4rem; + width: fit-content; + max-width: 50vw; + padding-right: 4rem; + background-color: var(--dark_950_color); + gap: 0.6rem; +} + +.Toastify__progress-bar--success { + background: var(--success_bc_color); +} + +.Toastify__progress-bar--warning { + background: var(--warning_bc_color); +} + +.Toastify__progress-bar--error { + background: var(--error_bc_color); +} + + +.Toastify__toast-icon { + width: fit-content; + max-width: 2.8rem; + min-width: 2.8rem; + justify-content: center; + align-items: center; +} \ No newline at end of file diff --git a/src-ui/app/snackbar_controller/SnackbarController.jsx b/src-ui/app/snackbar_controller/SnackbarController.jsx index e503e695..28bd6325 100644 --- a/src-ui/app/snackbar_controller/SnackbarController.jsx +++ b/src-ui/app/snackbar_controller/SnackbarController.jsx @@ -1,46 +1,114 @@ +import React, { useEffect, useState } from "react"; +import { ToastContainer, toast, Bounce } from "react-toastify"; import clsx from "clsx"; -import Snackbar from "@mui/material/Snackbar"; -import Slide from "@mui/material/Slide"; +import "./ReactToastifyOverrideClass.scss"; import styles from "./SnackbarController.module.scss"; + +import XMarkSvg from "@images/cancel.svg?react"; +import WarningSvg from "@images/warning.svg?react"; +import MegaphoneSvg from "@images/megaphone.svg?react"; +import CheckMarkSvg from "@images/check_mark.svg?react"; +import ErrorSvg from "@images/error.svg?react"; + import { useNotificationStatus } from "@logics_common"; export const SnackbarController = () => { const { currentNotificationStatus, closeNotification } = useNotificationStatus(); - - const handleClose = (event, reason) => { - closeNotification(event, reason); - }; - - const snackbar_classname = clsx(styles.snackbar_content, { - [styles.is_success]: currentNotificationStatus.data.status === "success", - [styles.is_warning]: currentNotificationStatus.data.status === "warning", - [styles.is_error]: currentNotificationStatus.data.status === "error", - }); + const [containerKey, setContainerKey] = useState(0); const settings = currentNotificationStatus.data; - let hide_duration = 5000; - if (settings.options?.hide_duration === null) hide_duration = null; - if (Number(settings.options?.hide_duration)) hide_duration = settings.options.hide_duration; + const snackbar_classname = clsx( + styles.snackbar_content, + { + [styles.is_success]: settings.status === "success", + [styles.is_warning]: settings.status === "warning", + [styles.is_error]: settings.status === "error", + } + ); + + let hideDuration = 5000; + if (settings.options?.hide_duration === null) { + hideDuration = false; + } else if (Number(settings.options?.hide_duration)) { + hideDuration = Number(settings.options?.hide_duration); + } + + useEffect(() => { + if (!settings.is_open) return; + + const message_text = settings.message; + + if (toast.isActive(message_text)) { + setContainerKey(prevKey => prevKey + 1); + + setTimeout(() => { + toast(message_text, { + toastId: message_text, + type: settings.status, + autoClose: hideDuration, + transition: Bounce, + toastClassName: snackbar_classname, + progressClassName: styles.toast_progress, + closeButton: , + onClose: () => { + closeNotification(); + }, + }); + }, 50); + } else { + toast(message_text, { + toastId: message_text, + type: settings.status, + autoClose: hideDuration, + transition: Bounce, + toastClassName: snackbar_classname, + progressClassName: styles.toast_progress, + closeButton: , + onClose: () => { + closeNotification(); + }, + }); + } + }, [settings, hideDuration, closeNotification, snackbar_classname]); return ( -
- -
-

{settings.message}

-
-
-
+ { + switch (type) { + case "info": + return ; + case "error": + return ; + case "success": + return ; + case "warning": + return ; + default: + return null; + } + }} + /> ); }; -const SlideTransition = (props) => { - return ; -}; +const CloseButtonContainer = ({ closeToast }) => { + return ( + + ); +}; \ No newline at end of file diff --git a/src-ui/app/snackbar_controller/SnackbarController.module.scss b/src-ui/app/snackbar_controller/SnackbarController.module.scss index 075e4419..f048a6ba 100644 --- a/src-ui/app/snackbar_controller/SnackbarController.module.scss +++ b/src-ui/app/snackbar_controller/SnackbarController.module.scss @@ -1,19 +1,83 @@ +/* SnackbarController.module.scss */ + +/* ------------------------------------------------- + 1) トースト共通のスタイル +--------------------------------------------------*/ .snackbar_content { - width: 100%; - height: 100%; - padding: 2rem; - color: #fff; - &.is_success { - background-color: var(--success_bc_color); - } - &.is_warning { - background-color: var(--waring_bc_color); - } - &.is_error { - background-color: var(--error_bc_color); - } + position: relative; + padding: 1.2rem 1.6rem; + border-radius: 0.8rem; + font-size: 1.4rem; + box-shadow: 0 0.2rem 0.8rem rgba(0, 0, 0, 0.5); + color: #fff; /* ダークテーマ前提で文字を白に */ } -.snackbar_message { - font-size: 1.4rem; +/* ステータス別に背景色を指定 */ +.is_success { + background-color: var(--success_bc_color); +} + +.is_warning { + background-color: var(--warning_bc_color); + color: #212529; /* 黄系の背景に合わせて文字を濃く */ +} + +.is_error { + background-color: var(--error_bc_color); +} + + +.megaphone_svg { + color: var(--dark_200_color); + width: 2.4rem; +} +.error_svg { + color: var(--error_bc_color); + width: 2.4rem; +} +.check_mark_svg { + color: var(--primary_400_color); + width: 2rem; +} +.warning_svg { + color: var(--waring_color); + width: 2.4rem; +} + + + +// Duplicated (Customized) +.close_button_wrapper { + position: absolute; + top: 0; + left: 100%; + transform: translate(-50%, -50%) rotate(45deg); + display: flex; + justify-content: center; + align-items: end; + width: 5.6rem; + aspect-ratio: 1 / 1; + &:hover { + background-color: var(--error_bc_color); + & .x_mark_svg { + color: var(--dark_200_color); + transform: rotate(45deg); + } + } + &:active { + background-color: var(--error_bc_active_color); + } + transition: all 0.1s ease; +} + +.close_button { + // width: 100%; + // height: 100%; +} + +.x_mark_svg { + width: 2rem; + transform: rotate(-45deg); + color: var(--dark_700_color); + transition: transform 0.3s ease; } \ No newline at end of file diff --git a/src-ui/app/splash_component/SplashComponent.jsx b/src-ui/app/splash_component/SplashComponent.jsx index 2943bad0..809f917c 100644 --- a/src-ui/app/splash_component/SplashComponent.jsx +++ b/src-ui/app/splash_component/SplashComponent.jsx @@ -71,7 +71,7 @@ const AnnouncementsContainer = () => { }; - +// Duplicated const CloseButtonContainer = () => { const { asyncCloseApp } = useWindow(); diff --git a/src-ui/app/splash_component/SplashComponent.module.scss b/src-ui/app/splash_component/SplashComponent.module.scss index f279b21b..bae69063 100644 --- a/src-ui/app/splash_component/SplashComponent.module.scss +++ b/src-ui/app/splash_component/SplashComponent.module.scss @@ -67,7 +67,7 @@ } - +// Duplicated .close_button_wrapper { position: absolute; top: 0; diff --git a/src-ui/assets/error.svg b/src-ui/assets/error.svg new file mode 100644 index 00000000..4842a2a7 --- /dev/null +++ b/src-ui/assets/error.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src-ui/logics/common/useNotificationStatus.js b/src-ui/logics/common/useNotificationStatus.js index f4aab3ed..c623d091 100644 --- a/src-ui/logics/common/useNotificationStatus.js +++ b/src-ui/logics/common/useNotificationStatus.js @@ -35,8 +35,7 @@ export const useNotificationStatus = () => { }); }; - const closeNotification = (event, reason) => { - if (reason === "clickaway") return; + const closeNotification = () => { updateNotificationStatus((prev) => ({ ...prev.data, is_open: false,