Complete 4.3.1: Notification System Redesign - Enhanced toast notification system with variants, queue management, and accessibility improvements
This commit is contained in:
parent
54db215848
commit
60a5cbb576
4 changed files with 746 additions and 7 deletions
17
UI_UPDATE.MD
17
UI_UPDATE.MD
|
|
@ -480,10 +480,19 @@ All utilities follow the established pattern of using CSS custom properties from
|
||||||
|
|
||||||
### Sub-task 4.3: Enhanced User Feedback Systems
|
### Sub-task 4.3: Enhanced User Feedback Systems
|
||||||
|
|
||||||
#### 4.3.1: Notification Redesign
|
#### 4.3.1: Notification Redesign - COMPLETED 9/29/2025
|
||||||
- [ ] Redesign toast notification system
|
- [x] Redesign toast notification system
|
||||||
- [ ] Implement notification variants
|
- [x] Implement notification variants
|
||||||
- [ ] Add notification queue management
|
- [x] Add notification queue management
|
||||||
|
|
||||||
|
**Notes:** Comprehensive notification system redesign completed with:
|
||||||
|
- **Enhanced Notification System**: Replaced simple toast with advanced notification system supporting multiple variants (success, error, warning, info), custom actions, progress bars, and smooth animations
|
||||||
|
- **Queue Management**: Implemented intelligent queue system that handles up to 5 simultaneous notifications with automatic stacking and dismissal
|
||||||
|
- **Variants and Styling**: Four notification variants with distinct colors, icons, and animations (.notification--success, .notification--error, .notification--warning, .notification--info)
|
||||||
|
- **Advanced Features**: Hover-to-pause, keyboard navigation (Escape key), ARIA accessibility, progress indicators for auto-dismissing notifications, and smooth slide-in/out animations
|
||||||
|
- **Legacy Support**: Maintains backward compatibility with existing showToast() calls while redirecting to the new system
|
||||||
|
- **Mobile Responsive**: Full responsive design with mobile-specific positioning and touch-friendly interactions
|
||||||
|
- **Api Integration**: New Notifications API (show(), success(), error(), warning(), info()) with configuration options
|
||||||
|
|
||||||
#### 4.3.2: Loading States
|
#### 4.3.2: Loading States
|
||||||
- [ ] Implement loading indicators
|
- [ ] Implement loading indicators
|
||||||
|
|
|
||||||
420
assets/js/notifications.js
Normal file
420
assets/js/notifications.js
Normal file
|
|
@ -0,0 +1,420 @@
|
||||||
|
// Notification System Module
|
||||||
|
// Advanced notifications with variants, queue management, and accessibility
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
AppModules.require('ui-controls');
|
||||||
|
AppModules.register('notifications');
|
||||||
|
|
||||||
|
// Notification configuration
|
||||||
|
const NotificationConfig = {
|
||||||
|
maxNotifications: 5, // Maximum notifications shown simultaneously
|
||||||
|
defaultDuration: 4000, // Default auto-dismiss duration (ms)
|
||||||
|
duration: {
|
||||||
|
success: 3000,
|
||||||
|
error: 7000,
|
||||||
|
warning: 5000,
|
||||||
|
info: 4000
|
||||||
|
},
|
||||||
|
position: 'top-right', // top-right, top-left, bottom-right, bottom-left, center
|
||||||
|
positionClasses: {
|
||||||
|
'top-right': '',
|
||||||
|
'top-left': 'notifications-container--left',
|
||||||
|
'bottom-right': 'notifications-container--bottom',
|
||||||
|
'bottom-left': 'notifications-container--bottom notifications-container--left',
|
||||||
|
'center': 'notifications-container--center'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Notification queue and state
|
||||||
|
let notificationQueue = [];
|
||||||
|
let activeNotifications = [];
|
||||||
|
let notificationCounter = 0;
|
||||||
|
|
||||||
|
// DOM elements
|
||||||
|
let notificationsContainer = null;
|
||||||
|
|
||||||
|
// Notification class
|
||||||
|
class Notification {
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.id = `notification-${++notificationCounter}`;
|
||||||
|
this.title = options.title || '';
|
||||||
|
this.message = options.message || '';
|
||||||
|
this.type = options.type || 'info'; // success, error, warning, info, default
|
||||||
|
this.duration = options.duration === 0 ? 0 : (options.duration || NotificationConfig.duration[this.type] || NotificationConfig.defaultDuration);
|
||||||
|
this.actions = options.actions || [];
|
||||||
|
this.position = options.position || NotificationConfig.position;
|
||||||
|
this.persistent = options.persistent || false;
|
||||||
|
this.icon = options.icon !== undefined ? options.icon : true;
|
||||||
|
this.timestamp = Date.now();
|
||||||
|
this.element = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
createElement() {
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.id = this.id;
|
||||||
|
notification.className = `notification notification--${this.type}`;
|
||||||
|
notification.setAttribute('role', 'alert');
|
||||||
|
notification.setAttribute('aria-live', 'assertive');
|
||||||
|
notification.setAttribute('aria-atomic', 'true');
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
// Icon
|
||||||
|
if (this.icon) {
|
||||||
|
html += '<div class="notification__icon" aria-hidden="true"></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content
|
||||||
|
html += '<div class="notification__content">';
|
||||||
|
if (this.title) {
|
||||||
|
html += `<div class="notification__title">${escapeHtml(this.title)}</div>`;
|
||||||
|
}
|
||||||
|
html += `<div class="notification__message">${escapeHtml(this.message)}</div>`;
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
if (this.actions.length > 0) {
|
||||||
|
html += '<div class="notification__actions">';
|
||||||
|
this.actions.forEach(action => {
|
||||||
|
const actionId = `action-${this.id}-${action.id || action.label.toLowerCase().replace(/\s+/g, '-')}`;
|
||||||
|
html += `<button class="notification__action" data-action-id="${actionId}" aria-label="${escapeHtml(action.label)}">${escapeHtml(action.label)}</button>`;
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Progress bar for auto-dismissing notifications
|
||||||
|
if (this.duration > 0) {
|
||||||
|
html += '<div class="notification__progress"><div class="notification__progress-bar"></div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.innerHTML = html;
|
||||||
|
|
||||||
|
// Add close button if no custom actions
|
||||||
|
if (this.actions.length === 0) {
|
||||||
|
const closeBtn = document.createElement('button');
|
||||||
|
closeBtn.className = 'notification__action';
|
||||||
|
closeBtn.setAttribute('aria-label', 'Close notification');
|
||||||
|
closeBtn.innerHTML = '×';
|
||||||
|
closeBtn.addEventListener('click', () => this.dismiss());
|
||||||
|
notification.appendChild(closeBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set progress bar animation duration
|
||||||
|
if (this.duration > 0) {
|
||||||
|
const progressBar = notification.querySelector('.notification__progress-bar');
|
||||||
|
if (progressBar) {
|
||||||
|
progressBar.style.animationDuration = `${this.duration}ms`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind action handlers
|
||||||
|
this.actions.forEach((action, index) => {
|
||||||
|
const actionBtn = notification.querySelector(`[data-action-id="action-${this.id}-${action.id || action.label.toLowerCase().replace(/\s+/g, '-')}"]`);
|
||||||
|
if (actionBtn && action.handler) {
|
||||||
|
actionBtn.addEventListener('click', () => {
|
||||||
|
action.handler(this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.element = notification;
|
||||||
|
return notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
if (!notificationsContainer) return;
|
||||||
|
|
||||||
|
// Create element if not exists
|
||||||
|
if (!this.element) {
|
||||||
|
this.createElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add position class
|
||||||
|
const positionClass = NotificationConfig.positionClasses[this.position] || '';
|
||||||
|
notificationsContainer.className = `notifications-container ${positionClass}`;
|
||||||
|
|
||||||
|
// Add to container
|
||||||
|
notificationsContainer.appendChild(this.element);
|
||||||
|
|
||||||
|
// Set up auto-dismiss if duration is set
|
||||||
|
if (this.duration > 0) {
|
||||||
|
this.timeoutId = setTimeout(() => {
|
||||||
|
this.dismiss();
|
||||||
|
}, this.duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pause timeout on hover
|
||||||
|
this.element.addEventListener('mouseenter', () => {
|
||||||
|
if (this.timeoutId) {
|
||||||
|
clearTimeout(this.timeoutId);
|
||||||
|
this.timeoutId = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.element.addEventListener('mouseleave', () => {
|
||||||
|
if (this.duration > 0 && !this.dismissed) {
|
||||||
|
this.timeoutId = setTimeout(() => {
|
||||||
|
this.dismiss();
|
||||||
|
}, this.duration);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
dismiss() {
|
||||||
|
if (this.dismissed) return;
|
||||||
|
this.dismissed = true;
|
||||||
|
|
||||||
|
// Clear timeout
|
||||||
|
if (this.timeoutId) {
|
||||||
|
clearTimeout(this.timeoutId);
|
||||||
|
this.timeoutId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add exit animation
|
||||||
|
if (this.element) {
|
||||||
|
this.element.classList.add('exiting');
|
||||||
|
|
||||||
|
// Remove after animation
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.element && this.element.parentNode) {
|
||||||
|
this.element.parentNode.removeChild(this.element);
|
||||||
|
}
|
||||||
|
removeFromActive(this);
|
||||||
|
processQueue();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update(options = {}) {
|
||||||
|
if (options.message !== undefined) {
|
||||||
|
this.message = options.message;
|
||||||
|
const messageEl = this.element.querySelector('.notification__message');
|
||||||
|
if (messageEl) {
|
||||||
|
messageEl.textContent = this.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.title !== undefined) {
|
||||||
|
this.title = options.title;
|
||||||
|
const titleEl = this.element.querySelector('.notification__title');
|
||||||
|
if (titleEl) {
|
||||||
|
titleEl.textContent = this.title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue management functions
|
||||||
|
function addToQueue(notification) {
|
||||||
|
notificationQueue.push(notification);
|
||||||
|
|
||||||
|
// Process queue if not at max capacity
|
||||||
|
if (activeNotifications.length < NotificationConfig.maxNotifications) {
|
||||||
|
processQueue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFromActive(notification) {
|
||||||
|
const index = activeNotifications.indexOf(notification);
|
||||||
|
if (index > -1) {
|
||||||
|
activeNotifications.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processQueue() {
|
||||||
|
// Remove any notifications that have been dismissed
|
||||||
|
activeNotifications = activeNotifications.filter(n => !n.dismissed);
|
||||||
|
|
||||||
|
// Show queued notifications if space available
|
||||||
|
while (activeNotifications.length < NotificationConfig.maxNotifications && notificationQueue.length > 0) {
|
||||||
|
const notification = notificationQueue.shift();
|
||||||
|
activeNotifications.push(notification);
|
||||||
|
notification.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public API functions
|
||||||
|
function show(options = {}) {
|
||||||
|
if (typeof options === 'string') {
|
||||||
|
options = { message: options };
|
||||||
|
}
|
||||||
|
|
||||||
|
const notification = new Notification(options);
|
||||||
|
addToQueue(notification);
|
||||||
|
return notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
function success(message, options = {}) {
|
||||||
|
return show(Object.assign({
|
||||||
|
message,
|
||||||
|
type: 'success'
|
||||||
|
}, options));
|
||||||
|
}
|
||||||
|
|
||||||
|
function error(message, options = {}) {
|
||||||
|
return show(Object.assign({
|
||||||
|
message,
|
||||||
|
type: 'error'
|
||||||
|
}, options));
|
||||||
|
}
|
||||||
|
|
||||||
|
function warning(message, options = {}) {
|
||||||
|
return show(Object.assign({
|
||||||
|
message,
|
||||||
|
type: 'warning'
|
||||||
|
}, options));
|
||||||
|
}
|
||||||
|
|
||||||
|
function info(message, options = {}) {
|
||||||
|
return show(Object.assign({
|
||||||
|
message,
|
||||||
|
type: 'info'
|
||||||
|
}, options));
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismissAll() {
|
||||||
|
// Clear queue
|
||||||
|
notificationQueue = [];
|
||||||
|
|
||||||
|
// Dismiss all active notifications
|
||||||
|
activeNotifications.forEach(notification => {
|
||||||
|
notification.dismiss();
|
||||||
|
});
|
||||||
|
activeNotifications = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearQueue() {
|
||||||
|
notificationQueue = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function setConfig(newConfig = {}) {
|
||||||
|
Object.assign(NotificationConfig, newConfig);
|
||||||
|
|
||||||
|
// Update container class if position changed
|
||||||
|
if (notificationsContainer && newConfig.position) {
|
||||||
|
const positionClass = NotificationConfig.positionClasses[newConfig.position] || '';
|
||||||
|
notificationsContainer.className = `notifications-container ${positionClass}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility functions
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const map = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": '''
|
||||||
|
};
|
||||||
|
return (text || '').replace(/[&<>"']/g, m => map[m]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the notification system
|
||||||
|
function initialize() {
|
||||||
|
// Get container element
|
||||||
|
notificationsContainer = document.getElementById('notificationsContainer');
|
||||||
|
|
||||||
|
if (!notificationsContainer) {
|
||||||
|
console.warn('Notification container element not found. Notifications will not be displayed.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set initial position class
|
||||||
|
const positionClass = NotificationConfig.positionClasses[NotificationConfig.position] || '';
|
||||||
|
notificationsContainer.className = `notifications-container ${positionClass}`;
|
||||||
|
|
||||||
|
// Add keyboard navigation support
|
||||||
|
document.addEventListener('keydown', handleKeyboardNavigation);
|
||||||
|
|
||||||
|
// Add container click delegation
|
||||||
|
notificationsContainer.addEventListener('click', handleContainerClick);
|
||||||
|
|
||||||
|
AppLogger.log('Notification system initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard navigation handler
|
||||||
|
function handleKeyboardNavigation(event) {
|
||||||
|
// Close notification on Escape
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
const activeNotification = activeNotifications[activeNotifications.length - 1];
|
||||||
|
if (activeNotification) {
|
||||||
|
activeNotification.dismiss();
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Container click handler for delegation
|
||||||
|
function handleContainerClick(event) {
|
||||||
|
// Handle close button clicks
|
||||||
|
if (event.target.classList.contains('notification__action') &&
|
||||||
|
event.target.textContent === '×') {
|
||||||
|
const notificationEl = event.target.closest('.notification');
|
||||||
|
if (notificationEl) {
|
||||||
|
const notificationId = notificationEl.id;
|
||||||
|
const notification = activeNotifications.find(n => n.id === notificationId);
|
||||||
|
if (notification) {
|
||||||
|
notification.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy toast support - redirect to new system
|
||||||
|
function legacyShowToast(message, duration) {
|
||||||
|
const toastElement = document.getElementById('toast');
|
||||||
|
if (toastElement) {
|
||||||
|
toastElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show using new system instead
|
||||||
|
show({
|
||||||
|
message,
|
||||||
|
duration: duration === 0 ? 0 : duration,
|
||||||
|
type: 'info',
|
||||||
|
position: 'bottom'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function legacyHideToast() {
|
||||||
|
// Legacy function - kept for compatibility, but new system is auto-managing
|
||||||
|
AppLogger.warn('hideToast() called - this is deprecated. Notifications auto-manage themselves now.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update ui-controls module to use new notification system
|
||||||
|
function updateUIControls() {
|
||||||
|
if (window.UIControls && window.UIControls.showToast) {
|
||||||
|
window.UIControls.showToast = legacyShowToast;
|
||||||
|
window.UIControls.hideToast = legacyHideToast;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public API
|
||||||
|
window.Notifications = {
|
||||||
|
show: show,
|
||||||
|
success: success,
|
||||||
|
error: error,
|
||||||
|
warning: warning,
|
||||||
|
info: info,
|
||||||
|
dismissAll: dismissAll,
|
||||||
|
clearQueue: clearQueue,
|
||||||
|
setConfig: setConfig,
|
||||||
|
config: NotificationConfig
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize on DOM ready
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initialize);
|
||||||
|
} else {
|
||||||
|
initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update UI controls after a short delay to ensure they load
|
||||||
|
setTimeout(updateUIControls, 100);
|
||||||
|
|
||||||
|
AppLogger.log('Notification system module loaded');
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
@ -495,8 +495,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||||
</aside>
|
</aside>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Toast Notification -->
|
<!-- Enhanced Notification System -->
|
||||||
<div class="toast" id="toast"></div>
|
<div class="notifications-container" role="region" aria-live="assertive" aria-label="Notifications" id="notificationsContainer"></div>
|
||||||
|
|
||||||
|
<!-- Legacy Toast Notification (deprecated) -->
|
||||||
|
<div class="toast" id="toast" style="display: none;"></div>
|
||||||
|
|
||||||
<!-- Mobile Bottom Navigation -->
|
<!-- Mobile Bottom Navigation -->
|
||||||
<nav class="mobile-nav" id="mobileNav" role="navigation" aria-label="Mobile navigation">
|
<nav class="mobile-nav" id="mobileNav" role="navigation" aria-label="Mobile navigation">
|
||||||
|
|
@ -533,5 +536,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||||
<script defer src="assets/js/ui-controls.js?v=1.4.4"></script>
|
<script defer src="assets/js/ui-controls.js?v=1.4.4"></script>
|
||||||
<script defer src="assets/js/chat.js?v=1.4.4"></script>
|
<script defer src="assets/js/chat.js?v=1.4.4"></script>
|
||||||
<script defer src="assets/js/video-player.js?v=1.4.4"></script>
|
<script defer src="assets/js/video-player.js?v=1.4.4"></script>
|
||||||
|
<script defer src="assets/js/notifications.js?v=1.4.4"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1927,9 +1927,310 @@ a:active {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =================================================================
|
/* =================================================================
|
||||||
NOTIFICATIONS & TOASTS
|
NOTIFICATION SYSTEM - Enhanced with Variants & Queue
|
||||||
================================================================= */
|
================================================================= */
|
||||||
|
|
||||||
|
/* Notification container for stacking multiple notifications */
|
||||||
|
.notifications-container {
|
||||||
|
position: fixed;
|
||||||
|
top: var(--spacing-5);
|
||||||
|
right: var(--spacing-5);
|
||||||
|
z-index: var(--z-toast);
|
||||||
|
pointer-events: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-3);
|
||||||
|
align-items: flex-end;
|
||||||
|
max-width: min(400px, calc(100vw - var(--spacing-5) * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-container--bottom {
|
||||||
|
top: auto;
|
||||||
|
bottom: var(--spacing-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-container--center {
|
||||||
|
right: auto;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Base notification styles */
|
||||||
|
.notification {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-3);
|
||||||
|
padding: var(--spacing-4);
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
font-family: var(--font-family-base);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
line-height: var(--line-height-normal);
|
||||||
|
box-shadow: var(--elevation-4);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: 100%;
|
||||||
|
pointer-events: auto;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
animation: notification-slide-in var(--transition-normal) ease-out;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification.exiting {
|
||||||
|
animation: notification-slide-out 0.3s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Notification variants */
|
||||||
|
.notification--success {
|
||||||
|
background: var(--color-success-container);
|
||||||
|
color: var(--color-on-success-container);
|
||||||
|
border-color: var(--color-success-outline);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification--success::before {
|
||||||
|
content: '✓';
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background: var(--color-success);
|
||||||
|
color: var(--color-on-success);
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification--error {
|
||||||
|
background: var(--color-error-container);
|
||||||
|
color: var(--color-on-error-container);
|
||||||
|
border-color: var(--color-error-outline);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification--error::before {
|
||||||
|
content: '✕';
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background: var(--color-error);
|
||||||
|
color: var(--color-on-error);
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification--warning {
|
||||||
|
background: var(--color-warning-container);
|
||||||
|
color: var(--color-on-warning-container);
|
||||||
|
border-color: var(--color-warning-outline);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification--warning::before {
|
||||||
|
content: '⚠';
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background: var(--color-warning);
|
||||||
|
color: var(--color-on-warning);
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification--info {
|
||||||
|
background: var(--color-primary-container);
|
||||||
|
color: var(--color-on-primary-container);
|
||||||
|
border-color: var(--color-primary-outline);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification--info::before {
|
||||||
|
content: 'ℹ';
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: var(--color-on-primary);
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification--default {
|
||||||
|
background: var(--color-surface);
|
||||||
|
color: var(--color-on-surface);
|
||||||
|
border-color: var(--color-outline-variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Notification content area */
|
||||||
|
.notification__content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification__title {
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
margin-bottom: var(--spacing-1);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification__message {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
line-height: var(--line-height-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Notification actions */
|
||||||
|
.notification__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-2);
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification__action {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: var(--spacing-1);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
opacity: 0.7;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification__action:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background: rgba(var(--color-on-surface-rgb), 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification__action:focus-visible {
|
||||||
|
outline: 2px solid var(--color-focus);
|
||||||
|
outline-offset: 2px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress indicator for auto-dismissing notifications */
|
||||||
|
.notification__progress {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: rgba(var(--color-on-surface-rgb), 0.2);
|
||||||
|
border-radius: 0 0 var(--border-radius-lg) var(--border-radius-lg);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification__progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background: currentColor;
|
||||||
|
animation: notification-progress linear forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Notification animations */
|
||||||
|
@keyframes notification-slide-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes notification-slide-out {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0) scale(1);
|
||||||
|
max-height: 200px;
|
||||||
|
margin-bottom: var(--spacing-3);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(100%) scale(0.9);
|
||||||
|
max-height: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes notification-progress {
|
||||||
|
from { width: 100%; }
|
||||||
|
to { width: 0%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Notification enter animations for different positions */
|
||||||
|
.notifications-container--bottom .notification {
|
||||||
|
animation: notification-slide-up var(--transition-normal) ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-container--center .notification {
|
||||||
|
animation: notification-fade-in var(--transition-normal) ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes notification-slide-up {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes notification-fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile responsive adjustments */
|
||||||
|
@media (max-width: calc(var(--breakpoint-sm) - 1px)) {
|
||||||
|
.notifications-container {
|
||||||
|
top: var(--spacing-4);
|
||||||
|
right: var(--spacing-4);
|
||||||
|
left: var(--spacing-4);
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-container--center {
|
||||||
|
right: var(--spacing-4);
|
||||||
|
left: var(--spacing-4);
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
min-width: auto;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification__content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Legacy toast support - deprecated, use notifications instead */
|
||||||
.toast {
|
.toast {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: var(--spacing-5);
|
bottom: var(--spacing-5);
|
||||||
|
|
@ -1949,6 +2250,11 @@ a:active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toast:active {
|
||||||
|
transform: translateX(-50%) scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Video player notification badge - unchanged */
|
||||||
.video-player__notification-badge {
|
.video-player__notification-badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(var(--spacing-2) * -1);
|
top: calc(var(--spacing-2) * -1);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue