diff --git a/webui/src/common/components/Toast/Toast.jsx b/webui/src/common/components/Toast/Toast.jsx new file mode 100644 index 0000000..3bc8aed --- /dev/null +++ b/webui/src/common/components/Toast/Toast.jsx @@ -0,0 +1,85 @@ +import React, { useEffect, useState } from 'react'; +import { + XIcon, + CheckCircleIcon, + XCircleIcon, + WarningIcon, + InfoIcon +} from '@phosphor-icons/react'; +import Button from '@/common/components/Button'; +import './styles.sass'; + +const TOAST_ICONS = { + success: , + error: , + warning: , + info: +}; + +export const Toast = ({ + id, + type = 'info', + message, + title, + duration = 5000, + onClose, + actions +}) => { + const [isVisible, setIsVisible] = useState(false); + const [isRemoving, setIsRemoving] = useState(false); + + useEffect(() => { + const timer = setTimeout(() => setIsVisible(true), 10); + return () => clearTimeout(timer); + }, []); + + const handleClose = () => { + setIsRemoving(true); + setTimeout(() => { + onClose?.(); + }, 150); + }; + + const toastClasses = [ + 'toast', + `toast--${type}`, + isVisible && 'toast--visible', + isRemoving && 'toast--removing' + ].filter(Boolean).join(' '); + + return ( +
+
+ {TOAST_ICONS[type]} +
+ +
+ {title &&
{title}
} +
{message}
+ + {actions && ( +
+ {actions.map((action, index) => ( + + ))} +
+ )} +
+ +
+ ); +}; \ No newline at end of file diff --git a/webui/src/common/components/Toast/index.js b/webui/src/common/components/Toast/index.js new file mode 100644 index 0000000..c06480c --- /dev/null +++ b/webui/src/common/components/Toast/index.js @@ -0,0 +1 @@ +export { Toast as default } from './Toast.jsx'; \ No newline at end of file diff --git a/webui/src/common/components/Toast/styles.sass b/webui/src/common/components/Toast/styles.sass new file mode 100644 index 0000000..bb4029b --- /dev/null +++ b/webui/src/common/components/Toast/styles.sass @@ -0,0 +1,92 @@ +.toast-container + position: fixed + top: 1rem + right: 1rem + z-index: 2000 + display: flex + flex-direction: column + gap: 0.75rem + max-width: 400px + pointer-events: none + +.toast + background: var(--bg-alt) + border: 1px solid var(--border) + border-radius: var(--radius-lg) + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15) + padding: 1rem + display: flex + gap: 0.75rem + align-items: flex-start + min-width: 300px + pointer-events: auto + transform: translateX(100%) + opacity: 0 + transition: all 0.15s ease-out + + &--visible + transform: translateX(0) + opacity: 1 + + &--removing + transform: translateX(100%) + opacity: 0 + + &--success + border-left: 4px solid #16a34a + .toast-icon + color: #16a34a + + &--error + border-left: 4px solid #d93025 + .toast-icon + color: #d93025 + + &--warning + border-left: 4px solid #f59e0b + .toast-icon + color: #f59e0b + + &--info + border-left: 4px solid #0f62fe + .toast-icon + color: #0f62fe + +.toast-icon + flex-shrink: 0 + margin-top: 0.125rem + +.toast-content + flex: 1 + min-width: 0 + +.toast-title + font-weight: 600 + color: var(--text) + margin-bottom: 0.25rem + font-size: 0.9rem + +.toast-message + color: var(--text-dim) + font-size: 0.85rem + line-height: 1.4 + word-wrap: break-word + +.toast-actions + display: flex + gap: 0.5rem + margin-top: 0.75rem + +.toast-close + flex-shrink: 0 + margin-left: 0.5rem + +@media (max-width: 768px) + .toast-container + left: 1rem + right: 1rem + top: 1rem + max-width: none + + .toast + min-width: auto \ No newline at end of file