feat(manacore/web): add undo toasts for delete and tag removal

Extend toast system with action buttons and toastStore.undo() helper.
After deleting a task/event/contact or removing a tag, a toast with
"Rückgängig" button appears for 5 seconds. Clicking it restores the
item (clears deletedAt) or re-adds the tag.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-03 14:50:32 +02:00
parent a673a69972
commit 81716725f2
6 changed files with 92 additions and 18 deletions

View file

@ -31,6 +31,17 @@
<div class="toast-item {colors[toast.type]}" role="alert">
<Icon size={20} weight="fill" class="toast-icon" />
<p class="toast-message">{toast.message}</p>
{#if toast.action}
<button
onclick={() => {
toast.action?.onClick();
handleDismiss(toast.id);
}}
class="toast-action"
>
{toast.action.label}
</button>
{/if}
<button
onclick={() => handleDismiss(toast.id)}
class="toast-dismiss"
@ -92,6 +103,23 @@
margin: 0;
}
.toast-action {
flex-shrink: 0;
padding: 0.25rem 0.625rem;
border-radius: 0.375rem;
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
cursor: pointer;
font-size: 0.75rem;
font-weight: 600;
color: inherit;
transition: all 0.15s ease;
white-space: nowrap;
}
.toast-action:hover {
background: rgba(255, 255, 255, 0.35);
}
.toast-dismiss {
flex-shrink: 0;
padding: 0.25rem;

View file

@ -1,5 +1,5 @@
export { toastStore, toast, handleApiError } from './toast.svelte';
export type { Toast, ToastType } from './toast.svelte';
export type { Toast, ToastType, ToastAction } from './toast.svelte';
export { default as ToastContainer } from './ToastContainer.svelte';
export { setupGlobalErrorHandler, GLOBAL_ERROR_TRANSLATIONS } from './globalErrorHandler';
export type {

View file

@ -20,11 +20,17 @@
export type ToastType = 'success' | 'error' | 'warning' | 'info';
export interface ToastAction {
label: string;
onClick: () => void;
}
export interface Toast {
id: string;
type: ToastType;
message: string;
duration: number;
action?: ToastAction;
}
// State
@ -50,11 +56,12 @@ export const toastStore = {
* @param message - The message to display
* @param type - Toast type: 'success' | 'error' | 'warning' | 'info'
* @param duration - Duration in ms (0 = permanent, default: 4000)
* @param action - Optional action button { label, onClick }
* @returns The toast ID for manual dismissal
*/
show(message: string, type: ToastType = 'info', duration = 4000): string {
show(message: string, type: ToastType = 'info', duration = 4000, action?: ToastAction): string {
const id = generateId();
const toast: Toast = { id, type, message, duration };
const toast: Toast = { id, type, message, duration, action };
toasts = [...toasts, toast];
@ -104,6 +111,24 @@ export const toastStore = {
return this.show(message, 'info', duration);
},
/**
* Show a success toast with an undo action button.
* @param message - The message to display
* @param onUndo - Callback when "Rückgängig" is clicked
* @param duration - Duration in ms (default: 5000)
*/
undo(message: string, onUndo: () => void, duration = 5000): string {
return this.show(message, 'success', duration, {
label: 'Rückgängig',
onClick: () => {
onUndo();
// Find and dismiss this toast after undo
const id = toasts.find((t) => t.action?.onClick === onUndo)?.id;
if (id) this.dismiss(id);
},
});
},
/**
* Dismiss a specific toast by ID
* @param id - The toast ID to dismiss