feat: add notification animations and improve UI across pages Add enter/exit animations to NotificationContainer with smooth slide effects Refactor ConfigPage search bar to float over editor with improved UX Enhance AuthFilesPage type badges with proper light/dark theme color support Fix grid layout in AuthFilesPage to use consistent 3-column layout Update icon button sizing and loading state handlin Update i18n translations for search functionality

This commit is contained in:
Supra4E8C
2025-12-13 00:46:07 +08:00
parent 7c0a2280a4
commit bcf82252ea
8 changed files with 373 additions and 100 deletions

View File

@@ -1,16 +1,89 @@
import { useEffect, useRef, useState } from 'react';
import { useNotificationStore } from '@/stores';
import type { Notification } from '@/types';
interface AnimatedNotification extends Notification {
isExiting?: boolean;
}
const ANIMATION_DURATION = 300; // ms
export function NotificationContainer() {
const { notifications, removeNotification } = useNotificationStore();
const [animatedNotifications, setAnimatedNotifications] = useState<AnimatedNotification[]>([]);
const prevNotificationsRef = useRef<Notification[]>([]);
if (!notifications.length) return null;
// Track notifications and manage animation states
useEffect(() => {
const prevNotifications = prevNotificationsRef.current;
const prevIds = new Set(prevNotifications.map((n) => n.id));
const currentIds = new Set(notifications.map((n) => n.id));
// Find new notifications (for enter animation)
const newNotifications = notifications.filter((n) => !prevIds.has(n.id));
// Find removed notifications (for exit animation)
const removedIds = new Set(
prevNotifications.filter((n) => !currentIds.has(n.id)).map((n) => n.id)
);
setAnimatedNotifications((prev) => {
// Mark removed notifications as exiting
let updated = prev.map((n) =>
removedIds.has(n.id) ? { ...n, isExiting: true } : n
);
// Add new notifications
newNotifications.forEach((n) => {
if (!updated.find((an) => an.id === n.id)) {
updated.push({ ...n, isExiting: false });
}
});
// Remove notifications that are not in current and not exiting
// (they've already completed their exit animation)
updated = updated.filter(
(n) => currentIds.has(n.id) || n.isExiting
);
return updated;
});
// Clean up exited notifications after animation
if (removedIds.size > 0) {
setTimeout(() => {
setAnimatedNotifications((prev) =>
prev.filter((n) => !removedIds.has(n.id))
);
}, ANIMATION_DURATION);
}
prevNotificationsRef.current = notifications;
}, [notifications]);
const handleClose = (id: string) => {
// Start exit animation
setAnimatedNotifications((prev) =>
prev.map((n) => (n.id === id ? { ...n, isExiting: true } : n))
);
// Actually remove after animation
setTimeout(() => {
removeNotification(id);
}, ANIMATION_DURATION);
};
if (!animatedNotifications.length) return null;
return (
<div className="notification-container">
{notifications.map((notification) => (
<div key={notification.id} className={`notification ${notification.type}`}>
{animatedNotifications.map((notification) => (
<div
key={notification.id}
className={`notification ${notification.type} ${notification.isExiting ? 'exiting' : 'entering'}`}
>
<div className="message">{notification.message}</div>
<button className="close-btn" onClick={() => removeNotification(notification.id)}>
<button className="close-btn" onClick={() => handleClose(notification.id)}>
×
</button>
</div>