replaced alerts with toast notifications
This commit is contained in:
parent
ea399ea0c4
commit
16b4747eb6
132
frontend/app.js
132
frontend/app.js
|
|
@ -10,6 +10,11 @@ const CONFIG = {
|
||||||
SCROLL_SYNC_RETRY_INTERVAL: 100, // ms - Time between setupScrollSync retries
|
SCROLL_SYNC_RETRY_INTERVAL: 100, // ms - Time between setupScrollSync retries
|
||||||
MAX_UNDO_HISTORY: 50, // Maximum number of undo steps to keep
|
MAX_UNDO_HISTORY: 50, // Maximum number of undo steps to keep
|
||||||
DEFAULT_SIDEBAR_WIDTH: 256, // px - Default sidebar width (w-64 in Tailwind)
|
DEFAULT_SIDEBAR_WIDTH: 256, // px - Default sidebar width (w-64 in Tailwind)
|
||||||
|
TOAST_MAX_VISIBLE: 4, // Max stacked toasts; oldest dropped when exceeded
|
||||||
|
TOAST_DURATION_ERROR_MS: 7000,
|
||||||
|
TOAST_DURATION_WARNING_MS: 5500,
|
||||||
|
TOAST_DURATION_INFO_MS: 4500,
|
||||||
|
TOAST_DURATION_SUCCESS_MS: 3500,
|
||||||
};
|
};
|
||||||
|
|
||||||
// localStorage settings configuration - centralized definition of all persisted settings
|
// localStorage settings configuration - centralized definition of all persisted settings
|
||||||
|
|
@ -38,17 +43,15 @@ const ErrorHandler = {
|
||||||
* Handle errors consistently across the app
|
* Handle errors consistently across the app
|
||||||
* @param {string} operation - The operation that failed (e.g., "load notes", "save note")
|
* @param {string} operation - The operation that failed (e.g., "load notes", "save note")
|
||||||
* @param {Error} error - The error object
|
* @param {Error} error - The error object
|
||||||
* @param {boolean} showAlert - Whether to show an alert to the user
|
* @param {boolean} notify - Whether to show a toast (listens on notediscovery:toast)
|
||||||
*/
|
*/
|
||||||
handle(operation, error, showAlert = true) {
|
handle(operation, error, notify = true) {
|
||||||
// Always log to console for debugging
|
|
||||||
console.error(`Failed to ${operation}:`, error);
|
console.error(`Failed to ${operation}:`, error);
|
||||||
|
if (notify) {
|
||||||
// Show user-friendly alert if requested
|
const message = `Failed to ${operation}. Please try again.`;
|
||||||
if (showAlert) {
|
window.dispatchEvent(new CustomEvent('notediscovery:toast', {
|
||||||
// Note: ErrorHandler doesn't have access to Alpine's t() function
|
detail: { message, type: 'error' }
|
||||||
// This message remains in English as a fallback
|
}));
|
||||||
alert(`Failed to ${operation}. Please try again.`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -310,6 +313,10 @@ function noteApp() {
|
||||||
quickSwitcherIndex: 0,
|
quickSwitcherIndex: 0,
|
||||||
quickSwitcherResults: [],
|
quickSwitcherResults: [],
|
||||||
|
|
||||||
|
// Non-blocking notifications (replaces window.alert)
|
||||||
|
toasts: [],
|
||||||
|
_toastIdSeq: 0,
|
||||||
|
|
||||||
// Homepage state
|
// Homepage state
|
||||||
selectedHomepageFolder: '',
|
selectedHomepageFolder: '',
|
||||||
_homepageCache: {
|
_homepageCache: {
|
||||||
|
|
@ -725,6 +732,19 @@ function noteApp() {
|
||||||
this.viewMode = this.previousViewMode;
|
this.viewMode = this.previousViewMode;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Toasts from ErrorHandler and code paths without Alpine `this`
|
||||||
|
if (!window.__noteapp_toast_listener) {
|
||||||
|
window.__noteapp_toast_listener = true;
|
||||||
|
window.addEventListener('notediscovery:toast', (e) => {
|
||||||
|
const d = e.detail || {};
|
||||||
|
if (typeof d.message !== 'string' || !d.message) return;
|
||||||
|
this.toast(d.message, {
|
||||||
|
type: d.type || 'error',
|
||||||
|
durationMs: d.durationMs
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Load app configuration
|
// Load app configuration
|
||||||
|
|
@ -1130,6 +1150,46 @@ function noteApp() {
|
||||||
return value.replace(/\{\{(\w+)\}\}/g, (_, name) => params[name] ?? `{{${name}}}`);
|
return value.replace(/\{\{(\w+)\}\}/g, (_, name) => params[name] ?? `{{${name}}}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Non-blocking toast (replaces window.alert). Types map to theme CSS variables.
|
||||||
|
* @param {string} message
|
||||||
|
* @param {{ type?: 'error'|'warning'|'info'|'success', durationMs?: number }} [options]
|
||||||
|
* @returns {number|undefined} toast id
|
||||||
|
*/
|
||||||
|
toast(message, options = {}) {
|
||||||
|
if (typeof message !== 'string' || !message) return;
|
||||||
|
const type = ['error', 'warning', 'info', 'success'].includes(options.type)
|
||||||
|
? options.type
|
||||||
|
: 'info';
|
||||||
|
let durationMs = options.durationMs;
|
||||||
|
if (durationMs == null || typeof durationMs !== 'number' || durationMs < 0) {
|
||||||
|
durationMs = type === 'error' ? CONFIG.TOAST_DURATION_ERROR_MS
|
||||||
|
: type === 'warning' ? CONFIG.TOAST_DURATION_WARNING_MS
|
||||||
|
: type === 'success' ? CONFIG.TOAST_DURATION_SUCCESS_MS
|
||||||
|
: CONFIG.TOAST_DURATION_INFO_MS;
|
||||||
|
}
|
||||||
|
const id = ++this._toastIdSeq;
|
||||||
|
const entry = { id, message, type, _tid: null };
|
||||||
|
this.toasts.push(entry);
|
||||||
|
while (this.toasts.length > CONFIG.TOAST_MAX_VISIBLE) {
|
||||||
|
const first = this.toasts.shift();
|
||||||
|
if (first && first._tid) clearTimeout(first._tid);
|
||||||
|
}
|
||||||
|
entry._tid = setTimeout(() => this.dismissToast(id), durationMs);
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
|
||||||
|
dismissToast(id) {
|
||||||
|
const idx = this.toasts.findIndex((t) => t.id === id);
|
||||||
|
if (idx === -1) return;
|
||||||
|
const entry = this.toasts[idx];
|
||||||
|
if (entry._tid) {
|
||||||
|
clearTimeout(entry._tid);
|
||||||
|
entry._tid = null;
|
||||||
|
}
|
||||||
|
this.toasts.splice(idx, 1);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get localized error message from FilenameValidator result
|
* Get localized error message from FilenameValidator result
|
||||||
* @param {object} validation - The validation result from FilenameValidator
|
* @param {object} validation - The validation result from FilenameValidator
|
||||||
|
|
@ -1338,7 +1398,7 @@ function noteApp() {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
this.allTags = data.tags || {};
|
this.allTags = data.tags || {};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorHandler.handle('load tags', error, false); // Don't show alert, tags are optional
|
ErrorHandler.handle('load tags', error, false); // Optional: no toast
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -1379,7 +1439,7 @@ function noteApp() {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
this.availableTemplates = data.templates || [];
|
this.availableTemplates = data.templates || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorHandler.handle('load templates', error, false); // Don't show alert, templates are optional
|
ErrorHandler.handle('load templates', error, false); // Optional: no toast
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -1393,7 +1453,7 @@ function noteApp() {
|
||||||
// Validate the note name
|
// Validate the note name
|
||||||
const validation = FilenameValidator.validateFilename(this.newTemplateNoteName);
|
const validation = FilenameValidator.validateFilename(this.newTemplateNoteName);
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
alert(this.getValidationErrorMessage(validation, 'note'));
|
this.toast(this.getValidationErrorMessage(validation, 'note'), { type: 'warning' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1419,7 +1479,7 @@ function noteApp() {
|
||||||
// CRITICAL: Check if note already exists
|
// CRITICAL: Check if note already exists
|
||||||
const existingNote = this.notes.find(note => note.path === notePath);
|
const existingNote = this.notes.find(note => note.path === notePath);
|
||||||
if (existingNote) {
|
if (existingNote) {
|
||||||
alert(this.t('notes.already_exists', { name: validation.sanitized }));
|
this.toast(this.t('notes.already_exists', { name: validation.sanitized }), { type: 'warning' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1435,7 +1495,7 @@ function noteApp() {
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
alert(error.detail || this.t('templates.create_failed'));
|
this.toast(error.detail || this.t('templates.create_failed'), { type: 'error' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2409,7 +2469,7 @@ function noteApp() {
|
||||||
// Handle media files dropped into editor
|
// Handle media files dropped into editor
|
||||||
async handleMediaDrop(event) {
|
async handleMediaDrop(event) {
|
||||||
if (!this.currentNote) {
|
if (!this.currentNote) {
|
||||||
alert(this.t('notes.open_first'));
|
this.toast(this.t('notes.open_first'), { type: 'info' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2429,7 +2489,7 @@ function noteApp() {
|
||||||
const mediaFiles = files.filter(file => allowedTypes.includes(file.type.toLowerCase()));
|
const mediaFiles = files.filter(file => allowedTypes.includes(file.type.toLowerCase()));
|
||||||
|
|
||||||
if (mediaFiles.length === 0) {
|
if (mediaFiles.length === 0) {
|
||||||
alert(this.t('media.no_valid_files'));
|
this.toast(this.t('media.no_valid_files'), { type: 'warning' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2768,7 +2828,7 @@ function noteApp() {
|
||||||
// Prevent dropping folder into itself or its subfolders
|
// Prevent dropping folder into itself or its subfolders
|
||||||
if (targetFolderPath === draggedPath ||
|
if (targetFolderPath === draggedPath ||
|
||||||
targetFolderPath.startsWith(draggedPath + '/')) {
|
targetFolderPath.startsWith(draggedPath + '/')) {
|
||||||
alert(this.t('folders.cannot_move_into_self'));
|
this.toast(this.t('folders.cannot_move_into_self'), { type: 'warning' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2813,11 +2873,11 @@ function noteApp() {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const errorData = await response.json().catch(() => ({}));
|
const errorData = await response.json().catch(() => ({}));
|
||||||
alert(errorData.detail || this.t('move.failed_folder'));
|
this.toast(errorData.detail || this.t('move.failed_folder'), { type: 'error' });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to move folder:', error);
|
console.error('Failed to move folder:', error);
|
||||||
alert(this.t('move.failed_folder'));
|
this.toast(this.t('move.failed_folder'), { type: 'error' });
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -2866,12 +2926,12 @@ function noteApp() {
|
||||||
} else {
|
} else {
|
||||||
const errorData = await response.json().catch(() => ({}));
|
const errorData = await response.json().catch(() => ({}));
|
||||||
const errorKey = isMedia ? 'move.failed_media' : 'move.failed_note';
|
const errorKey = isMedia ? 'move.failed_media' : 'move.failed_note';
|
||||||
alert(errorData.detail || this.t(errorKey));
|
this.toast(errorData.detail || this.t(errorKey), { type: 'error' });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to move ${isMedia ? 'media' : 'note'}:`, error);
|
console.error(`Failed to move ${isMedia ? 'media' : 'note'}:`, error);
|
||||||
const errorKey = isMedia ? 'move.failed_media' : 'move.failed_note';
|
const errorKey = isMedia ? 'move.failed_media' : 'move.failed_note';
|
||||||
alert(this.t(errorKey));
|
this.toast(this.t(errorKey), { type: 'error' });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -3313,7 +3373,7 @@ function noteApp() {
|
||||||
: FilenameValidator.validatePath(noteName);
|
: FilenameValidator.validatePath(noteName);
|
||||||
|
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
alert(this.getValidationErrorMessage(validation, 'note'));
|
this.toast(this.getValidationErrorMessage(validation, 'note'), { type: 'warning' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3329,7 +3389,7 @@ function noteApp() {
|
||||||
// CRITICAL: Check if note already exists (applies to both prompt and direct path)
|
// CRITICAL: Check if note already exists (applies to both prompt and direct path)
|
||||||
const existingNote = this.notes.find(note => note.path === notePath);
|
const existingNote = this.notes.find(note => note.path === notePath);
|
||||||
if (existingNote) {
|
if (existingNote) {
|
||||||
alert(this.t('notes.already_exists', { name: notePath }));
|
this.toast(this.t('notes.already_exists', { name: notePath }), { type: 'warning' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3381,7 +3441,7 @@ function noteApp() {
|
||||||
: FilenameValidator.validatePath(folderName);
|
: FilenameValidator.validatePath(folderName);
|
||||||
|
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
alert(this.getValidationErrorMessage(validation, 'folder'));
|
this.toast(this.getValidationErrorMessage(validation, 'folder'), { type: 'warning' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3391,7 +3451,7 @@ function noteApp() {
|
||||||
// Check if folder already exists
|
// Check if folder already exists
|
||||||
const existingFolder = this.allFolders.find(folder => folder === folderPath);
|
const existingFolder = this.allFolders.find(folder => folder === folderPath);
|
||||||
if (existingFolder) {
|
if (existingFolder) {
|
||||||
alert(this.t('folders.already_exists', { name: validatedName }));
|
this.toast(this.t('folders.already_exists', { name: validatedName }), { type: 'warning' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3427,7 +3487,7 @@ function noteApp() {
|
||||||
// Validate the new name (single segment, no path separators)
|
// Validate the new name (single segment, no path separators)
|
||||||
const validation = FilenameValidator.validateFilename(newName);
|
const validation = FilenameValidator.validateFilename(newName);
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
alert(this.getValidationErrorMessage(validation, 'folder'));
|
this.toast(this.getValidationErrorMessage(validation, 'folder'), { type: 'warning' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3917,14 +3977,14 @@ function noteApp() {
|
||||||
const newName = this.currentNoteName.trim();
|
const newName = this.currentNoteName.trim();
|
||||||
|
|
||||||
if (!newName) {
|
if (!newName) {
|
||||||
alert(this.t('notes.empty_name'));
|
this.toast(this.t('notes.empty_name'), { type: 'warning' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the new name (single segment, no path separators)
|
// Validate the new name (single segment, no path separators)
|
||||||
const validation = FilenameValidator.validateFilename(newName);
|
const validation = FilenameValidator.validateFilename(newName);
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
alert(this.getValidationErrorMessage(validation, 'note'));
|
this.toast(this.getValidationErrorMessage(validation, 'note'), { type: 'warning' });
|
||||||
// Reset the name in the UI
|
// Reset the name in the UI
|
||||||
this.currentNoteName = oldPath.split('/').pop().replace('.md', '');
|
this.currentNoteName = oldPath.split('/').pop().replace('.md', '');
|
||||||
return;
|
return;
|
||||||
|
|
@ -3939,7 +3999,7 @@ function noteApp() {
|
||||||
// Check if a note with the new name already exists
|
// Check if a note with the new name already exists
|
||||||
const existingNote = this.notes.find(n => n.path.toLowerCase() === newPath.toLowerCase());
|
const existingNote = this.notes.find(n => n.path.toLowerCase() === newPath.toLowerCase());
|
||||||
if (existingNote) {
|
if (existingNote) {
|
||||||
alert(this.t('notes.already_exists', { name: validatedName }));
|
this.toast(this.t('notes.already_exists', { name: validatedName }), { type: 'warning' });
|
||||||
// Reset the name in the UI
|
// Reset the name in the UI
|
||||||
this.currentNoteName = oldPath.split('/').pop().replace('.md', '');
|
this.currentNoteName = oldPath.split('/').pop().replace('.md', '');
|
||||||
return;
|
return;
|
||||||
|
|
@ -5199,7 +5259,7 @@ function noteApp() {
|
||||||
// Export current note as HTML via backend API
|
// Export current note as HTML via backend API
|
||||||
async exportToHTML() {
|
async exportToHTML() {
|
||||||
if (!this.currentNote || !this.noteContent) {
|
if (!this.currentNote || !this.noteContent) {
|
||||||
alert(this.t('notes.no_content'));
|
this.toast(this.t('notes.no_content'), { type: 'info' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -5241,14 +5301,14 @@ function noteApp() {
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('HTML export failed:', error);
|
console.error('HTML export failed:', error);
|
||||||
alert(this.t('export.failed', { error: error.message }));
|
this.toast(this.t('export.failed', { error: error.message }), { type: 'error' });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Open print preview in new window
|
// Open print preview in new window
|
||||||
printPreview() {
|
printPreview() {
|
||||||
if (!this.currentNote || !this.noteContent) {
|
if (!this.currentNote || !this.noteContent) {
|
||||||
alert(this.t('notes.no_content'));
|
this.toast(this.t('notes.no_content'), { type: 'info' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -5468,11 +5528,11 @@ function noteApp() {
|
||||||
this._sharedNotePaths.add(this.currentNote);
|
this._sharedNotePaths.add(this.currentNote);
|
||||||
} else {
|
} else {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
alert(this.t('share.error_creating', { error: error.detail || 'Unknown error' }));
|
this.toast(this.t('share.error_creating', { error: error.detail || 'Unknown error' }), { type: 'error' });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create share link:', error);
|
console.error('Failed to create share link:', error);
|
||||||
alert(this.t('share.error_creating', { error: error.message }));
|
this.toast(this.t('share.error_creating', { error: error.message }), { type: 'error' });
|
||||||
} finally {
|
} finally {
|
||||||
this.shareLoading = false;
|
this.shareLoading = false;
|
||||||
}
|
}
|
||||||
|
|
@ -5521,11 +5581,11 @@ function noteApp() {
|
||||||
this._sharedNotePaths.delete(this.currentNote);
|
this._sharedNotePaths.delete(this.currentNote);
|
||||||
} else {
|
} else {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
alert(this.t('share.error_revoking', { error: error.detail || 'Unknown error' }));
|
this.toast(this.t('share.error_revoking', { error: error.detail || 'Unknown error' }), { type: 'error' });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to revoke share link:', error);
|
console.error('Failed to revoke share link:', error);
|
||||||
alert(this.t('share.error_revoking', { error: error.message }));
|
this.toast(this.t('share.error_revoking', { error: error.message }), { type: 'error' });
|
||||||
} finally {
|
} finally {
|
||||||
this.shareLoading = false;
|
this.shareLoading = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1351,6 +1351,47 @@
|
||||||
<span>Exit Zen</span>
|
<span>Exit Zen</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- Toast notifications (non-blocking; z-index above modals at 1100–1200; desktop-first top-right) -->
|
||||||
|
<div
|
||||||
|
class="fixed z-[1300] flex w-[min(28rem,calc(100vw-2rem))] flex-col items-stretch gap-2 pointer-events-none"
|
||||||
|
style="top: max(1rem, env(safe-area-inset-top, 0px)); right: max(1rem, env(safe-area-inset-right, 0px));"
|
||||||
|
role="region"
|
||||||
|
:aria-label="t('toast.region_label')"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
<template x-for="item in toasts" :key="item.id">
|
||||||
|
<div
|
||||||
|
x-transition:enter="transition ease-out duration-200"
|
||||||
|
x-transition:enter-start="opacity-0 -translate-y-2"
|
||||||
|
x-transition:enter-end="opacity-100 translate-y-0"
|
||||||
|
x-transition:leave="transition ease-in duration-150"
|
||||||
|
x-transition:leave-start="opacity-100"
|
||||||
|
x-transition:leave-end="opacity-0 -translate-y-1"
|
||||||
|
class="pointer-events-auto flex items-start gap-2 rounded-lg border shadow-lg px-3 py-2.5 text-sm"
|
||||||
|
:role="item.type === 'error' ? 'alert' : 'status'"
|
||||||
|
:style="{
|
||||||
|
backgroundColor: 'var(--bg-secondary)',
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
borderColor: 'var(--border-primary)',
|
||||||
|
borderLeftWidth: '4px',
|
||||||
|
borderLeftStyle: 'solid',
|
||||||
|
borderLeftColor: item.type === 'error' ? 'var(--error)' : (item.type === 'warning' ? 'var(--warning)' : (item.type === 'success' ? 'var(--success)' : 'var(--accent-primary)'))
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span class="flex-1 min-w-0 break-words leading-snug" x-text="item.message"></span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex-shrink-0 rounded p-0.5 opacity-70 hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-opacity-50"
|
||||||
|
style="color: var(--text-secondary); --tw-ring-color: var(--accent-primary);"
|
||||||
|
@click="dismissToast(item.id)"
|
||||||
|
:aria-label="t('toast.dismiss')"
|
||||||
|
>
|
||||||
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Mobile Overlay -->
|
<!-- Mobile Overlay -->
|
||||||
<div
|
<div
|
||||||
@click="mobileSidebarOpen = false"
|
@click="mobileSidebarOpen = false"
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,11 @@
|
||||||
"failed": "{{action}} fehlgeschlagen. Bitte versuche es erneut."
|
"failed": "{{action}} fehlgeschlagen. Bitte versuche es erneut."
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"toast": {
|
||||||
|
"region_label": "Benachrichtigungen",
|
||||||
|
"dismiss": "Schließen"
|
||||||
|
},
|
||||||
|
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"title": "DATEIEN",
|
"title": "DATEIEN",
|
||||||
"favorites_title": "Favoriten",
|
"favorites_title": "Favoriten",
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,11 @@
|
||||||
"failed": "Failed to {{action}}. Please try again."
|
"failed": "Failed to {{action}}. Please try again."
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"toast": {
|
||||||
|
"region_label": "Notifications",
|
||||||
|
"dismiss": "Dismiss"
|
||||||
|
},
|
||||||
|
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"title": "FILES",
|
"title": "FILES",
|
||||||
"favorites_title": "Favourites",
|
"favorites_title": "Favourites",
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,11 @@
|
||||||
"failed": "Failed to {{action}}. Please try again."
|
"failed": "Failed to {{action}}. Please try again."
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"toast": {
|
||||||
|
"region_label": "Notifications",
|
||||||
|
"dismiss": "Dismiss"
|
||||||
|
},
|
||||||
|
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"title": "FILES",
|
"title": "FILES",
|
||||||
"favorites_title": "Favorites",
|
"favorites_title": "Favorites",
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,11 @@
|
||||||
"failed": "Error al {{action}}. Por favor, inténtalo de nuevo."
|
"failed": "Error al {{action}}. Por favor, inténtalo de nuevo."
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"toast": {
|
||||||
|
"region_label": "Notificaciones",
|
||||||
|
"dismiss": "Cerrar"
|
||||||
|
},
|
||||||
|
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"title": "ARCHIVOS",
|
"title": "ARCHIVOS",
|
||||||
"favorites_title": "Favoritos",
|
"favorites_title": "Favoritos",
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,11 @@
|
||||||
"failed": "Échec de {{action}}. Veuillez réessayer."
|
"failed": "Échec de {{action}}. Veuillez réessayer."
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"toast": {
|
||||||
|
"region_label": "Notifications",
|
||||||
|
"dismiss": "Fermer"
|
||||||
|
},
|
||||||
|
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"title": "FICHIERS",
|
"title": "FICHIERS",
|
||||||
"favorites_title": "Favoris",
|
"favorites_title": "Favoris",
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,11 @@
|
||||||
"failed": "Hiba a(z) {{action}} során. Kérlek, próbáld újra."
|
"failed": "Hiba a(z) {{action}} során. Kérlek, próbáld újra."
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"toast": {
|
||||||
|
"region_label": "Értesítések",
|
||||||
|
"dismiss": "Bezárás"
|
||||||
|
},
|
||||||
|
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"title": "FÁJLOK",
|
"title": "FÁJLOK",
|
||||||
"favorites_title": "Kedvencek",
|
"favorites_title": "Kedvencek",
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,11 @@
|
||||||
"failed": "Impossibile {{action}}. Riprova."
|
"failed": "Impossibile {{action}}. Riprova."
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"toast": {
|
||||||
|
"region_label": "Notifiche",
|
||||||
|
"dismiss": "Chiudi"
|
||||||
|
},
|
||||||
|
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"title": "FILE",
|
"title": "FILE",
|
||||||
"favorites_title": "Preferiti",
|
"favorites_title": "Preferiti",
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,11 @@
|
||||||
"failed": "{{action}}に失敗しました。もう一度お試しください。"
|
"failed": "{{action}}に失敗しました。もう一度お試しください。"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"toast": {
|
||||||
|
"region_label": "通知",
|
||||||
|
"dismiss": "閉じる"
|
||||||
|
},
|
||||||
|
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"title": "ファイル",
|
"title": "ファイル",
|
||||||
"favorites_title": "お気に入り",
|
"favorites_title": "お気に入り",
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,11 @@
|
||||||
"failed": "Не удалось {{action}}. Попробуйте снова."
|
"failed": "Не удалось {{action}}. Попробуйте снова."
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"toast": {
|
||||||
|
"region_label": "Уведомления",
|
||||||
|
"dismiss": "Закрыть"
|
||||||
|
},
|
||||||
|
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"title": "ФАЙЛЫ",
|
"title": "ФАЙЛЫ",
|
||||||
"favorites_title": "Избранное",
|
"favorites_title": "Избранное",
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,11 @@
|
||||||
"failed": "Napaka pri {{action}}. Prosimo, poskusite znova."
|
"failed": "Napaka pri {{action}}. Prosimo, poskusite znova."
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"toast": {
|
||||||
|
"region_label": "Obvestila",
|
||||||
|
"dismiss": "Zapri"
|
||||||
|
},
|
||||||
|
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"title": "DATOTEKE",
|
"title": "DATOTEKE",
|
||||||
"favorites_title": "Priljubljene",
|
"favorites_title": "Priljubljene",
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,11 @@
|
||||||
"failed": "{{action}} 失败。请重试。"
|
"failed": "{{action}} 失败。请重试。"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"toast": {
|
||||||
|
"region_label": "通知",
|
||||||
|
"dismiss": "关闭"
|
||||||
|
},
|
||||||
|
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"title": "文件",
|
"title": "文件",
|
||||||
"favorites_title": "收藏夹",
|
"favorites_title": "收藏夹",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue