generated from corrad-software/corrad-af-2024
292 lines
8.9 KiB
Vue
292 lines
8.9 KiB
Vue
<script setup>
|
|
import { computed } from 'vue';
|
|
import { useDesignSystem } from '~/composables/useDesignSystem';
|
|
import { useNotifications } from '~/composables/useNotifications';
|
|
import RsButton from '~/components/RsButton.vue';
|
|
|
|
const { statusColors, utils, animations } = useDesignSystem();
|
|
const { notifications, removeNotification } = useNotifications();
|
|
|
|
// Position classes for different notification positions
|
|
const positionClasses = {
|
|
'top-right': 'fixed top-4 right-4 z-50',
|
|
'top-left': 'fixed top-4 left-4 z-50',
|
|
'top-center': 'fixed top-4 left-1/2 transform -translate-x-1/2 z-50',
|
|
'bottom-right': 'fixed bottom-4 right-4 z-50',
|
|
'bottom-left': 'fixed bottom-4 left-4 z-50',
|
|
'bottom-center': 'fixed bottom-4 left-1/2 transform -translate-x-1/2 z-50'
|
|
};
|
|
|
|
// Get notification classes based on type
|
|
const getNotificationClasses = (notification) => {
|
|
const baseClasses = [
|
|
'notification-item',
|
|
'max-w-sm w-full',
|
|
'bg-white dark:bg-gray-800',
|
|
'border border-gray-200 dark:border-gray-600',
|
|
'rounded-lg shadow-lg',
|
|
'p-4 mb-3',
|
|
'transform transition-all duration-300 ease-in-out'
|
|
];
|
|
|
|
// Add type-specific styling
|
|
if (notification.type && statusColors[notification.type]) {
|
|
baseClasses.push(`border-l-4 border-l-${notification.type === 'success' ? 'green' :
|
|
notification.type === 'error' ? 'red' :
|
|
notification.type === 'warning' ? 'yellow' : 'blue'}-500`);
|
|
}
|
|
|
|
return utils.composeClasses(...baseClasses);
|
|
};
|
|
|
|
// Get icon for notification type
|
|
const getTypeIcon = (type) => {
|
|
switch (type) {
|
|
case 'success': return 'mdi:check-circle';
|
|
case 'error': return 'mdi:alert-circle';
|
|
case 'warning': return 'mdi:alert';
|
|
case 'info': return 'mdi:information';
|
|
case 'loading': return 'mdi:loading';
|
|
case 'confirm': return 'mdi:help-circle';
|
|
default: return 'mdi:information';
|
|
}
|
|
};
|
|
|
|
// Group notifications by position
|
|
const notificationsByPosition = computed(() => {
|
|
const groups = {};
|
|
notifications.value.forEach(notification => {
|
|
const position = notification.position || 'top-right';
|
|
if (!groups[position]) {
|
|
groups[position] = [];
|
|
}
|
|
groups[position].push(notification);
|
|
});
|
|
return groups;
|
|
});
|
|
|
|
// Handle action click
|
|
const handleActionClick = (action, notification) => {
|
|
if (action.action) {
|
|
action.action();
|
|
}
|
|
};
|
|
|
|
// Auto-remove timer display
|
|
const getTimeRemaining = (notification) => {
|
|
if (notification.persistent || !notification.duration) return null;
|
|
|
|
const elapsed = Date.now() - notification.startTime;
|
|
const remaining = Math.max(0, notification.duration - elapsed);
|
|
return Math.ceil(remaining / 1000);
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div class="notification-container">
|
|
<!-- Render notification groups by position -->
|
|
<div
|
|
v-for="(notificationList, position) in notificationsByPosition"
|
|
:key="position"
|
|
:class="positionClasses[position]"
|
|
>
|
|
<TransitionGroup
|
|
:enter-active-class="animations.transitions.slideDown.enter"
|
|
:enter-from-class="animations.transitions.slideDown.enterFrom"
|
|
:enter-to-class="animations.transitions.slideDown.enterTo"
|
|
:leave-active-class="animations.transitions.fadeIn.leave"
|
|
:leave-from-class="animations.transitions.fadeIn.leaveFrom"
|
|
:leave-to-class="animations.transitions.fadeIn.leaveTo"
|
|
tag="div"
|
|
class="space-y-3"
|
|
>
|
|
<div
|
|
v-for="notification in notificationList"
|
|
:key="notification.id"
|
|
:class="getNotificationClasses(notification)"
|
|
>
|
|
<!-- Main Content -->
|
|
<div class="flex items-start space-x-3">
|
|
<!-- Icon -->
|
|
<div
|
|
v-if="notification.showIcon"
|
|
class="flex-shrink-0 mt-0.5"
|
|
>
|
|
<Icon
|
|
:name="notification.icon || getTypeIcon(notification.type)"
|
|
:class="[
|
|
'w-5 h-5',
|
|
notification.type === 'loading' ? 'animate-spin' : '',
|
|
notification.type === 'success' ? 'text-green-500' :
|
|
notification.type === 'error' ? 'text-red-500' :
|
|
notification.type === 'warning' ? 'text-yellow-500' :
|
|
notification.type === 'info' ? 'text-blue-500' :
|
|
'text-gray-500 dark:text-gray-400'
|
|
]"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div class="flex-1 min-w-0">
|
|
<!-- Title -->
|
|
<h4
|
|
v-if="notification.title"
|
|
class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-1"
|
|
>
|
|
{{ notification.title }}
|
|
</h4>
|
|
|
|
<!-- Message -->
|
|
<div class="text-sm text-gray-700 dark:text-gray-300">
|
|
<p v-if="!notification.allowHtml">{{ notification.message }}</p>
|
|
<div v-else v-html="notification.message"></div>
|
|
</div>
|
|
|
|
<!-- Actions for confirmation type -->
|
|
<div
|
|
v-if="notification.type === 'confirm' && notification.actions"
|
|
class="flex items-center space-x-2 mt-3"
|
|
>
|
|
<RsButton
|
|
v-for="action in notification.actions"
|
|
:key="action.label"
|
|
:variant="action.variant || 'secondary-outline'"
|
|
size="sm"
|
|
@click="handleActionClick(action, notification)"
|
|
>
|
|
{{ action.label }}
|
|
</RsButton>
|
|
</div>
|
|
|
|
<!-- Progress bar for loading -->
|
|
<div
|
|
v-if="notification.type === 'loading' && notification.progress !== undefined"
|
|
class="mt-3"
|
|
>
|
|
<div class="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400 mb-1">
|
|
<span>Progress</span>
|
|
<span>{{ notification.progress }}%</span>
|
|
</div>
|
|
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
|
<div
|
|
class="bg-primary h-2 rounded-full transition-all duration-300"
|
|
:style="{ width: `${notification.progress}%` }"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Time remaining indicator -->
|
|
<div
|
|
v-if="!notification.persistent && notification.duration && getTimeRemaining(notification)"
|
|
class="mt-2 text-xs text-gray-400 dark:text-gray-500"
|
|
>
|
|
Auto-close in {{ getTimeRemaining(notification) }}s
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Close button -->
|
|
<button
|
|
v-if="notification.showClose"
|
|
@click="removeNotification(notification.id)"
|
|
class="flex-shrink-0 p-1 rounded-md text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors duration-200"
|
|
aria-label="Close notification"
|
|
>
|
|
<Icon name="mdi:close" class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</TransitionGroup>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.notification-container {
|
|
pointer-events: none;
|
|
}
|
|
|
|
.notification-item {
|
|
pointer-events: auto;
|
|
backdrop-filter: blur(8px);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
/* Enhanced animations */
|
|
.notification-enter-active {
|
|
transition: all 0.3s ease-out;
|
|
}
|
|
|
|
.notification-leave-active {
|
|
transition: all 0.3s ease-in;
|
|
}
|
|
|
|
.notification-enter-from {
|
|
opacity: 0;
|
|
transform: translateX(100%) scale(0.95);
|
|
}
|
|
|
|
.notification-leave-to {
|
|
opacity: 0;
|
|
transform: translateX(100%) scale(0.95);
|
|
}
|
|
|
|
/* Position-specific enter animations */
|
|
.top-left .notification-enter-from,
|
|
.bottom-left .notification-enter-from {
|
|
transform: translateX(-100%) scale(0.95);
|
|
}
|
|
|
|
.top-left .notification-leave-to,
|
|
.bottom-left .notification-leave-to {
|
|
transform: translateX(-100%) scale(0.95);
|
|
}
|
|
|
|
.top-center .notification-enter-from,
|
|
.bottom-center .notification-enter-from {
|
|
transform: translateY(-20px) scale(0.95);
|
|
}
|
|
|
|
.top-center .notification-leave-to,
|
|
.bottom-center .notification-leave-to {
|
|
transform: translateY(-20px) scale(0.95);
|
|
}
|
|
|
|
/* Mobile responsive */
|
|
@media (max-width: 640px) {
|
|
.notification-item {
|
|
@apply max-w-[calc(100vw-2rem)] mx-4;
|
|
}
|
|
|
|
/* Adjust positioning for mobile */
|
|
.fixed.top-4.right-4,
|
|
.fixed.top-4.left-4 {
|
|
@apply top-2 right-2 left-2;
|
|
}
|
|
|
|
.fixed.bottom-4.right-4,
|
|
.fixed.bottom-4.left-4 {
|
|
@apply bottom-2 right-2 left-2;
|
|
}
|
|
}
|
|
|
|
/* Dark mode enhancements */
|
|
.dark .notification-item {
|
|
background: rgba(31, 41, 55, 0.95);
|
|
border-color: rgba(75, 85, 99, 0.3);
|
|
}
|
|
|
|
/* Loading spinner animation */
|
|
@keyframes spin {
|
|
from { transform: rotate(0deg); }
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.animate-spin {
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
/* Progress bar animation */
|
|
.progress-bar {
|
|
transition: width 0.3s ease-in-out;
|
|
}
|
|
</style> |