fix: DockerSecretsResolver - don't normalize absolute paths like /var/www/html/...
Some checks failed
Deploy Application / deploy (push) Has been cancelled

This commit is contained in:
2025-11-24 21:28:25 +01:00
parent 4eb7134853
commit 77abc65cd7
1327 changed files with 91915 additions and 9909 deletions

View File

@@ -0,0 +1,288 @@
/**
* Toast Queue Manager
*
* Manages multiple toast notifications with:
* - Queue system for multiple toasts
* - Position management (stacking toasts)
* - Auto-dismiss with queue management
* - Maximum number of simultaneous toasts
*/
export class ToastQueue {
constructor(maxToasts = 5) {
this.maxToasts = maxToasts;
this.queue = [];
this.activeToasts = new Map(); // componentId → toast element
this.positionOffsets = new Map(); // position → current offset
}
/**
* Add toast to queue
* @param {string} componentId - Component ID
* @param {Object} options - Toast options
* @returns {HTMLElement|null} Toast element or null if queue full
*/
add(componentId, options = {}) {
const {
message = '',
type = 'info',
duration = 5000,
position = 'top-right'
} = options;
// Check if we already have a toast for this component
if (this.activeToasts.has(componentId)) {
// Replace existing toast
this.remove(componentId);
}
// Check if we've reached max toasts for this position
const positionCount = this.getPositionCount(position);
if (positionCount >= this.maxToasts) {
// Remove oldest toast at this position
this.removeOldestAtPosition(position);
}
// Create toast element
const toast = this.createToastElement(message, type, position, componentId);
// Calculate offset for stacking
const offset = this.getNextOffset(position);
this.setToastPosition(toast, position, offset);
// Add to DOM
document.body.appendChild(toast);
// Animate in
requestAnimationFrame(() => {
toast.style.opacity = '1';
toast.style.transform = 'translateY(0)';
});
// Store reference
this.activeToasts.set(componentId, toast);
this.updatePositionOffset(position, offset);
// Setup auto-dismiss
if (duration > 0) {
setTimeout(() => {
this.remove(componentId);
}, duration);
}
// Setup close button
const closeBtn = toast.querySelector('.toast-close');
if (closeBtn) {
closeBtn.addEventListener('click', () => {
this.remove(componentId);
});
}
return toast;
}
/**
* Remove toast
* @param {string} componentId - Component ID
*/
remove(componentId) {
const toast = this.activeToasts.get(componentId);
if (!toast) {
return;
}
// Animate out
toast.style.opacity = '0';
const position = this.getToastPosition(toast);
toast.style.transform = `translateY(${position.includes('top') ? '-20px' : '20px'})`;
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
this.activeToasts.delete(componentId);
this.recalculateOffsets(position);
}, 300);
}
/**
* Remove all toasts
*/
clear() {
for (const componentId of this.activeToasts.keys()) {
this.remove(componentId);
}
}
/**
* Remove oldest toast at position
* @param {string} position - Position
*/
removeOldestAtPosition(position) {
let oldestToast = null;
let oldestComponentId = null;
let oldestTimestamp = Infinity;
for (const [componentId, toast] of this.activeToasts.entries()) {
if (this.getToastPosition(toast) === position) {
const timestamp = parseInt(toast.dataset.timestamp || '0');
if (timestamp < oldestTimestamp) {
oldestTimestamp = timestamp;
oldestToast = toast;
oldestComponentId = componentId;
}
}
}
if (oldestComponentId) {
this.remove(oldestComponentId);
}
}
/**
* Get count of toasts at position
* @param {string} position - Position
* @returns {number}
*/
getPositionCount(position) {
let count = 0;
for (const toast of this.activeToasts.values()) {
if (this.getToastPosition(toast) === position) {
count++;
}
}
return count;
}
/**
* Get next offset for position
* @param {string} position - Position
* @returns {number}
*/
getNextOffset(position) {
const currentOffset = this.positionOffsets.get(position) || 0;
return currentOffset + 80; // 80px spacing between toasts
}
/**
* Update position offset
* @param {string} position - Position
* @param {number} offset - Offset value
*/
updatePositionOffset(position, offset) {
this.positionOffsets.set(position, offset);
}
/**
* Recalculate offsets for position after removal
* @param {string} position - Position
*/
recalculateOffsets(position) {
const toastsAtPosition = [];
for (const [componentId, toast] of this.activeToasts.entries()) {
if (this.getToastPosition(toast) === position) {
toastsAtPosition.push({ componentId, toast });
}
}
// Sort by DOM order
toastsAtPosition.sort((a, b) => {
const positionA = a.toast.compareDocumentPosition(b.toast);
return positionA & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
});
// Recalculate offsets
let offset = 0;
for (const { toast } of toastsAtPosition) {
this.setToastPosition(toast, position, offset);
offset += 80;
}
this.positionOffsets.set(position, offset);
}
/**
* Create toast element
* @param {string} message - Toast message
* @param {string} type - Toast type
* @param {string} position - Position
* @param {string} componentId - Component ID
* @returns {HTMLElement}
*/
createToastElement(message, type, position, componentId) {
const toast = document.createElement('div');
toast.className = `toast-queue-toast toast-queue-toast--${type} toast-queue-toast--${position}`;
toast.setAttribute('role', 'alert');
toast.setAttribute('aria-live', 'polite');
toast.dataset.componentId = componentId;
toast.dataset.timestamp = Date.now().toString();
const colors = {
info: '#3b82f6',
success: '#10b981',
warning: '#f59e0b',
error: '#ef4444'
};
toast.innerHTML = `
<div class="toast-content">
<span class="toast-message">${message}</span>
<button class="toast-close" aria-label="Close">×</button>
</div>
`;
toast.style.cssText = `
position: fixed;
${position.includes('top') ? 'top' : 'bottom'}: 1rem;
${position.includes('left') ? 'left' : 'right'}: 1rem;
max-width: 400px;
padding: 1rem;
background: ${colors[type] || colors.info};
color: white;
border-radius: 0.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
z-index: 10000;
opacity: 0;
transform: translateY(${position.includes('top') ? '-20px' : '20px'});
transition: opacity 0.3s ease, transform 0.3s ease;
`;
return toast;
}
/**
* Set toast position with offset
* @param {HTMLElement} toast - Toast element
* @param {string} position - Position
* @param {number} offset - Offset in pixels
*/
setToastPosition(toast, position, offset) {
if (position.includes('top')) {
toast.style.top = `${1 + offset / 16}rem`; // Convert px to rem (assuming 16px base)
} else {
toast.style.bottom = `${1 + offset / 16}rem`;
}
}
/**
* Get toast position from element
* @param {HTMLElement} toast - Toast element
* @returns {string}
*/
getToastPosition(toast) {
if (toast.classList.contains('toast-queue-toast--top-right')) {
return 'top-right';
}
if (toast.classList.contains('toast-queue-toast--top-left')) {
return 'top-left';
}
if (toast.classList.contains('toast-queue-toast--bottom-right')) {
return 'bottom-right';
}
if (toast.classList.contains('toast-queue-toast--bottom-left')) {
return 'bottom-left';
}
return 'top-right'; // default
}
}