refactor(shared-ui): centralize toast system across all web apps

- Add toast module to @manacore/shared-ui (toastStore, ToastContainer)
- Remove local toast implementations from:
  - calendar/web
  - chat/web
  - clock/web
  - contacts/web
  - picture/web
  - storage/web
- Update all apps to import toast from shared-ui
- Consistent toast API: toast.success(), toast.error(), toast.info()

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-01-29 14:03:29 +01:00
parent 6d0d9d4f67
commit 14ce457c7b
56 changed files with 487 additions and 1249 deletions

View file

@ -1,184 +0,0 @@
<script lang="ts">
import { toastStore, type Toast } from '$lib/stores/toast.svelte';
import { fly } from 'svelte/transition';
// Reactive getter from the runes-based store
let toasts = $derived(toastStore.toasts);
function handleClose(id: string) {
toastStore.remove(id);
}
function getIcon(type: Toast['type']) {
switch (type) {
case 'success':
return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
<polyline points="22 4 12 14.01 9 11.01"></polyline>
</svg>`;
case 'error':
return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<line x1="15" y1="9" x2="9" y2="15"></line>
<line x1="9" y1="9" x2="15" y2="15"></line>
</svg>`;
case 'warning':
return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
<line x1="12" y1="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>`;
case 'info':
default:
return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="16" x2="12" y2="12"></line>
<line x1="12" y1="8" x2="12.01" y2="8"></line>
</svg>`;
}
}
</script>
<div class="toast-container">
{#each toasts as toastItem (toastItem.id)}
<div
class="toast toast-{toastItem.type}"
transition:fly={{ y: 20, duration: 300 }}
role="alert"
>
<div class="toast-icon">
{@html getIcon(toastItem.type)}
</div>
<p class="toast-message">{toastItem.message}</p>
<button
class="toast-close"
onclick={() => handleClose(toastItem.id)}
aria-label="Close notification"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
{/each}
</div>
<style>
.toast-container {
position: fixed;
bottom: 2rem;
right: 2rem;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 0.5rem;
pointer-events: none;
}
.toast {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem 1.5rem;
background: hsl(var(--card));
border-radius: 0.75rem;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
border: 1px solid hsl(var(--border));
min-width: 300px;
max-width: 400px;
pointer-events: auto;
backdrop-filter: blur(10px);
}
.toast-icon {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.toast-success {
border-left: 4px solid hsl(142.1 76.2% 36.3%);
}
.toast-success .toast-icon {
color: hsl(142.1 76.2% 36.3%);
}
.toast-error {
border-left: 4px solid hsl(var(--destructive));
}
.toast-error .toast-icon {
color: hsl(var(--destructive));
}
.toast-warning {
border-left: 4px solid hsl(47.9 95.8% 53.1%);
}
.toast-warning .toast-icon {
color: hsl(47.9 95.8% 53.1%);
}
.toast-info {
border-left: 4px solid hsl(var(--primary));
}
.toast-info .toast-icon {
color: hsl(var(--primary));
}
.toast-message {
flex: 1;
margin: 0;
font-size: 0.9375rem;
color: hsl(var(--foreground));
line-height: 1.5;
}
.toast-close {
flex-shrink: 0;
background: none;
border: none;
padding: 0.25rem;
cursor: pointer;
color: hsl(var(--muted-foreground));
transition: all 150ms ease;
border-radius: 0.25rem;
display: flex;
align-items: center;
justify-content: center;
}
.toast-close:hover {
background: hsl(var(--muted));
color: hsl(var(--foreground));
}
@media (max-width: 768px) {
.toast-container {
bottom: 6rem;
right: 1rem;
left: 1rem;
align-items: stretch;
}
.toast {
min-width: auto;
max-width: none;
}
.toast-message {
font-size: 0.875rem;
}
}
</style>

View file

@ -1,10 +1,9 @@
<script lang="ts">
import { ContextMenu, type ContextMenuItem } from '@manacore/shared-ui';
import { ContextMenu, type ContextMenuItem, toastStore } from '@manacore/shared-ui';
import { Pencil, Copy, Trash, Palette, CalendarBlank, Export } from '@manacore/shared-icons';
import { eventContextMenuStore } from '$lib/stores/eventContextMenu.svelte';
import { eventsStore } from '$lib/stores/events.svelte';
import { calendarsStore } from '$lib/stores/calendars.svelte';
import { toastStore } from '$lib/stores/toast.svelte';
import type { CalendarEvent } from '@calendar/shared';
interface Props {

View file

@ -2,9 +2,8 @@
import { goto } from '$app/navigation';
import { eventsStore } from '$lib/stores/events.svelte';
import { calendarsStore } from '$lib/stores/calendars.svelte';
import { toast } from '$lib/stores/toast.svelte';
import EventForm from './EventForm.svelte';
import { TagBadge } from '@manacore/shared-ui';
import { TagBadge, toastStore as toast } from '@manacore/shared-ui';
import type { CalendarEvent, UpdateEventInput } from '@calendar/shared';
import * as api from '$lib/api/events';
import { format } from 'date-fns';

View file

@ -3,7 +3,6 @@
import { eventsStore } from '$lib/stores/events.svelte';
import { settingsStore } from '$lib/stores/settings.svelte';
import { contactsStore } from '$lib/stores/contacts.svelte';
import { toast } from '$lib/stores/toast.svelte';
import type {
LocationDetails,
CalendarEvent,
@ -16,6 +15,7 @@
ContactAvatar,
ConfirmationPopover,
FilterDropdown,
toastStore as toast,
type FilterDropdownOption,
} from '@manacore/shared-ui';
import { Users } from '@manacore/shared-icons';

View file

@ -6,8 +6,8 @@
import { settingsStore } from '$lib/stores/settings.svelte';
import type { TimeFormat, AllDayDisplayMode, SttLanguage } from '$lib/stores/settings.svelte';
import { calendarsStore } from '$lib/stores/calendars.svelte';
import { toast } from '$lib/stores/toast.svelte';
import {
toastStore as toast,
GlobalSettingsSection,
SettingsSection,
SettingsCard,

View file

@ -2,7 +2,7 @@
import { todosStore } from '$lib/stores/todos.svelte';
import type { Task, UpdateTaskInput, TaskPriority } from '$lib/api/todos';
import { PRIORITY_LABELS, PRIORITY_COLORS } from '$lib/api/todos';
import { toast } from '$lib/stores/toast.svelte';
import { toastStore as toast } from '@manacore/shared-ui';
import TodoCheckbox from './TodoCheckbox.svelte';
import PriorityBadge from './PriorityBadge.svelte';
import {

View file

@ -8,7 +8,7 @@ import type { CalendarEvent, CreateEventInput, UpdateEventInput } from '@calenda
import * as api from '$lib/api/events';
import { format, isWithinInterval, isSameDay } from 'date-fns';
import { toDate } from '$lib/utils/eventDateHelpers';
import { toastStore } from './toast.svelte';
import { toastStore } from '@manacore/shared-ui';
import { authStore } from './auth.svelte';
import { generateDemoEvents, isDemoEvent } from '$lib/data/demo-events';

View file

@ -1,57 +0,0 @@
/**
* Toast Store - Svelte 5 Runes version
* Manages toast notifications
*/
export type ToastType = 'success' | 'error' | 'warning' | 'info';
export interface Toast {
id: string;
type: ToastType;
message: string;
duration?: number;
}
// State
let toasts = $state<Toast[]>([]);
function add(message: string, type: ToastType = 'info', duration: number = 4000): string {
const id = crypto.randomUUID();
const toast: Toast = { id, type, message, duration };
toasts = [...toasts, toast];
if (duration > 0) {
setTimeout(() => {
remove(id);
}, duration);
}
return id;
}
function remove(id: string) {
toasts = toasts.filter((t) => t.id !== id);
}
function clear() {
toasts = [];
}
export const toastStore = {
get toasts() {
return toasts;
},
add,
remove,
clear,
success: (message: string, duration?: number) => add(message, 'success', duration),
error: (message: string, duration?: number) => add(message, 'error', duration ?? 6000),
warning: (message: string, duration?: number) => add(message, 'warning', duration),
info: (message: string, duration?: number) => add(message, 'info', duration),
};
// Keep old export for backwards compatibility
export const toast = toastStore;

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { SubscriptionPage } from '@manacore/shared-subscription-ui';
import { toast } from '$lib/stores/toast.svelte';
import { toastStore as toast } from '@manacore/shared-ui';
function handleSubscribe(planId: string) {
toast.info(`Abo "${planId}" ausgewählt. Bezahlsystem wird noch integriert.`);

View file

@ -6,8 +6,8 @@
import { settingsStore } from '$lib/stores/settings.svelte';
import type { TimeFormat, AllDayDisplayMode } from '$lib/stores/settings.svelte';
import { calendarsStore } from '$lib/stores/calendars.svelte';
import { toast } from '$lib/stores/toast.svelte';
import {
toastStore as toast,
GlobalSettingsSection,
SettingsSection,
SettingsCard,

View file

@ -3,8 +3,7 @@
import '$lib/i18n';
import { theme } from '$lib/stores/theme';
import { authStore } from '$lib/stores/auth.svelte';
import { toastStore } from '$lib/stores/toast.svelte';
import ToastContainer from '$lib/components/ToastContainer.svelte';
import { ToastContainer } from '@manacore/shared-ui';
import { AppLoadingSkeleton } from '$lib/components/skeletons';
import { isLoading as i18nLoading } from 'svelte-i18n';
import { onMount } from 'svelte';

View file

@ -1,66 +0,0 @@
<script lang="ts">
import { toastStore } from '$lib/stores/toast.svelte';
import type { Toast } from '$lib/stores/toast.svelte';
import { X, CheckCircle, XCircle, Warning, Info } from '@manacore/shared-icons';
let toasts = $derived(toastStore.toasts);
const icons = {
success: CheckCircle,
error: XCircle,
warning: Warning,
info: Info,
};
const colors = {
success: 'bg-green-500/90 text-white',
error: 'bg-destructive/90 text-destructive-foreground',
warning: 'bg-amber-500/90 text-white',
info: 'bg-primary/90 text-primary-foreground',
};
function handleDismiss(id: string) {
toastStore.dismiss(id);
}
</script>
{#if toasts.length > 0}
<div class="fixed bottom-6 right-6 z-[100] flex flex-col gap-3 max-w-sm">
{#each toasts as toast (toast.id)}
{@const Icon = icons[toast.type]}
<div
class="flex items-start gap-3 px-4 py-3 rounded-xl shadow-lg backdrop-blur-xl border border-white/20 animate-slide-in {colors[
toast.type
]}"
role="alert"
>
<Icon size={20} weight="fill" class="flex-shrink-0 mt-0.5" />
<p class="flex-1 text-sm font-medium">{toast.message}</p>
<button
onclick={() => handleDismiss(toast.id)}
class="flex-shrink-0 p-1 rounded-lg hover:bg-white/20 transition-colors"
aria-label="Schließen"
>
<X size={16} weight="bold" />
</button>
</div>
{/each}
</div>
{/if}
<style>
@keyframes slide-in {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.animate-slide-in {
animation: slide-in 0.3s ease-out;
}
</style>

View file

@ -4,7 +4,7 @@
*/
import { conversationService } from '$lib/services/conversation';
import { toastStore } from './toast.svelte';
import { toastStore } from '@manacore/shared-ui';
import { sessionConversationsStore } from './session-conversations.svelte';
import { authStore } from './auth.svelte';
import type { Conversation } from '@chat/types';

View file

@ -1,103 +0,0 @@
/**
* Toast Store - Centralized notification system using Svelte 5 runes
*/
export type ToastType = 'success' | 'error' | 'warning' | 'info';
export interface Toast {
id: string;
type: ToastType;
message: string;
duration: number;
}
// State
let toasts = $state<Toast[]>([]);
// Auto-incrementing ID
let nextId = 0;
function generateId(): string {
return `toast-${++nextId}-${Date.now()}`;
}
export const toastStore = {
// Getter for reading toasts
get toasts() {
return toasts;
},
/**
* Show a toast notification
*/
show(message: string, type: ToastType = 'info', duration: number = 4000) {
const id = generateId();
const toast: Toast = { id, type, message, duration };
toasts = [...toasts, toast];
// Auto-remove after duration
if (duration > 0) {
setTimeout(() => {
this.dismiss(id);
}, duration);
}
return id;
},
/**
* Show success toast
*/
success(message: string, duration?: number) {
return this.show(message, 'success', duration);
},
/**
* Show error toast
*/
error(message: string, duration: number = 6000) {
return this.show(message, 'error', duration);
},
/**
* Show warning toast
*/
warning(message: string, duration?: number) {
return this.show(message, 'warning', duration);
},
/**
* Show info toast
*/
info(message: string, duration?: number) {
return this.show(message, 'info', duration);
},
/**
* Dismiss a specific toast
*/
dismiss(id: string) {
toasts = toasts.filter((t) => t.id !== id);
},
/**
* Dismiss all toasts
*/
dismissAll() {
toasts = [];
},
};
/**
* Helper function for API error handling
* Use this in services/stores to show user-friendly error messages
*/
export function handleApiError(
error: unknown,
fallbackMessage: string = 'Ein Fehler ist aufgetreten'
): string {
const message = error instanceof Error ? error.message : fallbackMessage;
toastStore.error(message);
return message;
}

View file

@ -5,7 +5,7 @@
import { documentService } from '$lib/services/document';
import { authStore } from '$lib/stores/auth.svelte';
import { conversationsStore } from '$lib/stores/conversations.svelte';
import { toastStore } from '$lib/stores/toast.svelte';
import { toastStore } from '@manacore/shared-ui';
import MessageList from '$lib/components/chat/MessageList.svelte';
import ChatInput from '$lib/components/chat/ChatInput.svelte';
import ChatLayout from '$lib/components/chat/ChatLayout.svelte';

View file

@ -2,7 +2,7 @@
import '../app.css';
import { onMount } from 'svelte';
import { theme } from '$lib/stores/theme';
import Toast from '$lib/components/Toast.svelte';
import { ToastContainer } from '@manacore/shared-ui';
let { children } = $props();
@ -17,4 +17,4 @@
</div>
<!-- Global Toast notifications -->
<Toast />
<ToastContainer />

View file

@ -1,72 +0,0 @@
<script lang="ts">
import { toasts } from '$lib/stores/toast';
import type { Toast } from '$lib/stores/toast';
function getIcon(type: Toast['type']) {
switch (type) {
case 'success':
return '✓';
case 'error':
return '✕';
case 'warning':
return '⚠';
case 'info':
default:
return '';
}
}
function getColorClass(type: Toast['type']) {
switch (type) {
case 'success':
return 'bg-green-500';
case 'error':
return 'bg-red-500';
case 'warning':
return 'bg-yellow-500';
case 'info':
default:
return 'bg-blue-500';
}
}
</script>
<div class="fixed bottom-4 right-4 z-50 flex flex-col gap-2">
{#each $toasts as toast (toast.id)}
<div
class="flex items-center gap-3 rounded-lg bg-card px-4 py-3 shadow-lg border border-border animate-in slide-in-from-right duration-200"
>
<span
class="{getColorClass(
toast.type
)} flex h-6 w-6 items-center justify-center rounded-full text-white text-sm"
>
{getIcon(toast.type)}
</span>
<span class="text-foreground">{toast.message}</span>
<button
onclick={() => toasts.remove(toast.id)}
class="ml-2 text-muted-foreground hover:text-foreground"
>
</button>
</div>
{/each}
</div>
<style>
@keyframes slide-in-from-right {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.animate-in {
animation: slide-in-from-right 0.2s ease-out;
}
</style>

View file

@ -1,47 +0,0 @@
import { writable } from 'svelte/store';
export interface Toast {
id: string;
type: 'success' | 'error' | 'info' | 'warning';
message: string;
duration?: number;
}
function createToastStore() {
const { subscribe, update } = writable<Toast[]>([]);
function addToast(toast: Omit<Toast, 'id'>) {
const id = crypto.randomUUID();
const newToast = { ...toast, id };
update((toasts) => [...toasts, newToast]);
// Auto-remove after duration
const duration = toast.duration || 5000;
setTimeout(() => {
removeToast(id);
}, duration);
return id;
}
function removeToast(id: string) {
update((toasts) => toasts.filter((t) => t.id !== id));
}
return {
subscribe,
success: (message: string, duration?: number) =>
addToast({ type: 'success', message, duration }),
error: (message: string, duration?: number) => addToast({ type: 'error', message, duration }),
info: (message: string, duration?: number) => addToast({ type: 'info', message, duration }),
warning: (message: string, duration?: number) =>
addToast({ type: 'warning', message, duration }),
remove: removeToast,
};
}
export const toasts = createToastStore();
// Alias for compatibility with different import styles
export const toast = toasts;

View file

@ -1,10 +1,9 @@
<script lang="ts">
import { onMount } from 'svelte';
import { _ } from 'svelte-i18n';
import { PageHeader } from '@manacore/shared-ui';
import { PageHeader, toast } from '@manacore/shared-ui';
import { alarmsStore } from '$lib/stores/alarms.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { toast } from '$lib/stores/toast';
import type { CreateAlarmInput, Alarm } from '@clock/shared';
import { ALARM_SOUNDS, DEFAULT_ALARM_PRESETS } from '@clock/shared';
import { AlarmsSkeleton } from '$lib/components/skeletons';

View file

@ -2,10 +2,9 @@
import { onMount, onDestroy } from 'svelte';
import { _ } from 'svelte-i18n';
import { browser } from '$app/environment';
import { PageHeader } from '@manacore/shared-ui';
import { PageHeader, toast } from '@manacore/shared-ui';
import { timersStore } from '$lib/stores/timers.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { toast } from '$lib/stores/toast';
import { QUICK_TIMER_PRESETS, formatDuration } from '@clock/shared';
import { TimersSkeleton } from '$lib/components/skeletons';

View file

@ -1,10 +1,9 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { _ } from 'svelte-i18n';
import { PageHeader } from '@manacore/shared-ui';
import { PageHeader, toast } from '@manacore/shared-ui';
import { worldClocksStore } from '$lib/stores/world-clocks.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { toast } from '$lib/stores/toast';
import { POPULAR_TIMEZONES } from '@clock/shared';
import WorldMap from '$lib/components/WorldMap.svelte';
import { WorldClockSkeleton } from '$lib/components/skeletons';

View file

@ -5,7 +5,7 @@
import { theme } from '$lib/stores/theme.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { waitLocale } from '$lib/i18n';
import ToastContainer from '$lib/components/ToastContainer.svelte';
import { ToastContainer } from '@manacore/shared-ui';
import { AppLoadingSkeleton } from '$lib/components/skeletons';
let { children } = $props();

View file

@ -9,7 +9,7 @@
import ContactAlphabetView from '$lib/components/views/ContactAlphabetView.svelte';
import { ContactListSkeleton, ContactGridSkeleton } from '$lib/components/skeletons';
import { batchApi } from '$lib/api/batch';
import { toasts } from '$lib/stores/toast';
import { toastStore } from '@manacore/shared-ui';
import { newContactModalStore } from '$lib/stores/new-contact-modal.svelte';
// Infinite scroll
@ -183,12 +183,12 @@
batchLoading = true;
try {
const result = await batchApi.deleteMany([...selectedIds]);
toasts.success(`${result.success} Kontakte gelöscht`);
toastStore.success(`${result.success} Kontakte gelöscht`);
selectedIds = new Set();
selectionMode = false;
await contactsStore.loadContacts();
} catch (e) {
toasts.error(e instanceof Error ? e.message : 'Fehler beim Löschen');
toastStore.error(e instanceof Error ? e.message : 'Fehler beim Löschen');
} finally {
batchLoading = false;
}
@ -200,12 +200,12 @@
batchLoading = true;
try {
const result = await batchApi.archiveMany([...selectedIds], true);
toasts.success(`${result.success} Kontakte archiviert`);
toastStore.success(`${result.success} Kontakte archiviert`);
selectedIds = new Set();
selectionMode = false;
await contactsStore.loadContacts();
} catch (e) {
toasts.error(e instanceof Error ? e.message : 'Fehler beim Archivieren');
toastStore.error(e instanceof Error ? e.message : 'Fehler beim Archivieren');
} finally {
batchLoading = false;
}
@ -217,12 +217,12 @@
batchLoading = true;
try {
const result = await batchApi.favoriteMany([...selectedIds], true);
toasts.success(`${result.success} Kontakte zu Favoriten hinzugefügt`);
toastStore.success(`${result.success} Kontakte zu Favoriten hinzugefügt`);
selectedIds = new Set();
selectionMode = false;
await contactsStore.loadContacts();
} catch (e) {
toasts.error(e instanceof Error ? e.message : 'Fehler');
toastStore.error(e instanceof Error ? e.message : 'Fehler');
} finally {
batchLoading = false;
}

View file

@ -1,72 +0,0 @@
<script lang="ts">
import { toasts } from '$lib/stores/toast';
import type { Toast } from '$lib/stores/toast';
function getIcon(type: Toast['type']) {
switch (type) {
case 'success':
return '✓';
case 'error':
return '✕';
case 'warning':
return '⚠';
case 'info':
default:
return '';
}
}
function getColorClass(type: Toast['type']) {
switch (type) {
case 'success':
return 'bg-green-500';
case 'error':
return 'bg-red-500';
case 'warning':
return 'bg-yellow-500';
case 'info':
default:
return 'bg-blue-500';
}
}
</script>
<div class="fixed bottom-4 right-4 z-50 flex flex-col gap-2">
{#each $toasts as toast (toast.id)}
<div
class="flex items-center gap-3 rounded-lg bg-card px-4 py-3 shadow-lg border border-border animate-in slide-in-from-right duration-200"
>
<span
class="{getColorClass(
toast.type
)} flex h-6 w-6 items-center justify-center rounded-full text-white text-sm"
>
{getIcon(toast.type)}
</span>
<span class="text-foreground">{toast.message}</span>
<button
onclick={() => toasts.remove(toast.id)}
class="ml-2 text-muted-foreground hover:text-foreground"
>
</button>
</div>
{/each}
</div>
<style>
@keyframes slide-in-from-right {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.animate-in {
animation: slide-in-from-right 0.2s ease-out;
}
</style>

View file

@ -1,44 +0,0 @@
import { writable } from 'svelte/store';
export interface Toast {
id: string;
type: 'success' | 'error' | 'info' | 'warning';
message: string;
duration?: number;
}
function createToastStore() {
const { subscribe, update } = writable<Toast[]>([]);
function addToast(toast: Omit<Toast, 'id'>) {
const id = crypto.randomUUID();
const newToast = { ...toast, id };
update((toasts) => [...toasts, newToast]);
// Auto-remove after duration
const duration = toast.duration || 5000;
setTimeout(() => {
removeToast(id);
}, duration);
return id;
}
function removeToast(id: string) {
update((toasts) => toasts.filter((t) => t.id !== id));
}
return {
subscribe,
success: (message: string, duration?: number) =>
addToast({ type: 'success', message, duration }),
error: (message: string, duration?: number) => addToast({ type: 'error', message, duration }),
info: (message: string, duration?: number) => addToast({ type: 'info', message, duration }),
warning: (message: string, duration?: number) =>
addToast({ type: 'warning', message, duration }),
remove: removeToast,
};
}
export const toasts = createToastStore();

View file

@ -4,7 +4,7 @@
import { duplicatesApi, type DuplicateGroup } from '$lib/api/duplicates';
import MergeModal from '$lib/components/duplicates/MergeModal.svelte';
import { DuplicateListSkeleton } from '$lib/components/skeletons';
import { toasts } from '$lib/stores/toast';
import { toastStore } from '@manacore/shared-ui';
let duplicates = $state<DuplicateGroup[]>([]);
let loading = $state(true);
@ -75,14 +75,14 @@
async function handleMerge(primaryId: string, mergeIds: string[]) {
try {
await duplicatesApi.mergeContacts(primaryId, mergeIds);
toasts.success(`${mergeIds.length + 1} Kontakte wurden zusammengeführt`);
toastStore.success(`${mergeIds.length + 1} Kontakte wurden zusammengeführt`);
// Remove the merged group from the list
if (selectedGroup) {
duplicates = duplicates.filter((d) => d.id !== selectedGroup!.id);
}
handleCloseMergeModal();
} catch (e) {
toasts.error(e instanceof Error ? e.message : 'Fehler beim Zusammenführen');
toastStore.error(e instanceof Error ? e.message : 'Fehler beim Zusammenführen');
}
}
@ -91,10 +91,10 @@
try {
await duplicatesApi.dismissDuplicate(selectedGroup.id);
duplicates = duplicates.filter((d) => d.id !== selectedGroup!.id);
toasts.info('Duplikat-Gruppe wurde ignoriert');
toastStore.info('Duplikat-Gruppe wurde ignoriert');
handleCloseMergeModal();
} catch (e) {
toasts.error('Fehler beim Ignorieren');
toastStore.error('Fehler beim Ignorieren');
}
}

View file

@ -1,15 +1,15 @@
<script lang="ts">
import { SubscriptionPage } from '@manacore/shared-subscription-ui';
import { toasts } from '$lib/stores/toast';
import { toastStore } from '@manacore/shared-ui';
function handleSubscribe(planId: string) {
console.log('Subscribe to plan:', planId);
toasts.info(`Abo "${planId}" ausgewählt. Bezahlsystem wird noch integriert.`);
toastStore.info(`Abo "${planId}" ausgewählt. Bezahlsystem wird noch integriert.`);
}
function handleBuyPackage(packageId: string) {
console.log('Buy package:', packageId);
toasts.info(`Paket "${packageId}" ausgewählt. Bezahlsystem wird noch integriert.`);
toastStore.info(`Paket "${packageId}" ausgewählt. Bezahlsystem wird noch integriert.`);
}
</script>

View file

@ -6,8 +6,7 @@
import { isLoading as i18nLoading } from 'svelte-i18n';
import { theme } from '$lib/stores/theme';
import { authStore } from '$lib/stores/auth.svelte';
import { toasts } from '$lib/stores/toast';
import ToastContainer from '$lib/components/ToastContainer.svelte';
import { toastStore, ToastContainer } from '@manacore/shared-ui';
import { AppLoadingSkeleton } from '$lib/components/skeletons';
let { children } = $props();
@ -49,7 +48,7 @@
}
// Show toast notification
toasts.error(message);
toastStore.error(message);
// Prevent default browser error handling
event.preventDefault();
@ -59,17 +58,17 @@
window.addEventListener('error', (event) => {
// Only handle non-script errors (network failures for resources, etc.)
if (event.message && !event.filename) {
toasts.error('Ein Fehler ist aufgetreten');
toastStore.error('Ein Fehler ist aufgetreten');
}
});
// Handle offline/online status
window.addEventListener('offline', () => {
toasts.warning('Keine Internetverbindung', 10000);
toastStore.warning('Keine Internetverbindung', 10000);
});
window.addEventListener('online', () => {
toasts.success('Verbindung wiederhergestellt');
toastStore.success('Verbindung wiederhergestellt');
});
}

View file

@ -5,7 +5,7 @@
import { canvasItems, addCanvasItem } from '$lib/stores/canvas';
import { getImages } from '$lib/api/images';
import { addBoardItem } from '$lib/api/boardItems';
import { showToast } from '$lib/stores/toast';
import { toastStore } from '@manacore/shared-ui';
import Modal from '$lib/components/ui/Modal.svelte';
import Button from '$lib/components/ui/Button.svelte';
import { MagnifyingGlass, Image as ImageIcon, Check } from '@manacore/shared-icons';
@ -47,7 +47,7 @@
hasMore = data.length === 50;
} catch (error) {
console.error('Error loading images:', error);
showToast('Fehler beim Laden der Bilder', 'error');
toastStore.show('Fehler beim Laden der Bilder', 'error');
} finally {
isLoadingImages.set(false);
}
@ -104,11 +104,14 @@
selectedImages.clear();
selectedImages = new Set();
showToast(`${addedCount} ${addedCount === 1 ? 'Bild' : 'Bilder'} hinzugefügt`, 'success');
toastStore.show(
`${addedCount} ${addedCount === 1 ? 'Bild' : 'Bilder'} hinzugefügt`,
'success'
);
onClose();
} catch (error) {
console.error('Error adding images:', error);
showToast('Fehler beim Hinzufügen', 'error');
toastStore.show('Fehler beim Hinzufügen', 'error');
} finally {
isAdding = false;
}

View file

@ -2,7 +2,7 @@
import { selectedItems, updateCanvasItem, removeSelectedItems } from '$lib/stores/canvas';
import { updateBoardItem, changeBoardItemZIndex, isImageItem } from '$lib/api/boardItems';
import Button from '$lib/components/ui/Button.svelte';
import { showToast } from '$lib/stores/toast';
import { toastStore } from '@manacore/shared-ui';
import {
Image,
CaretDoubleUp,
@ -51,7 +51,7 @@
await updateBoardItem(selectedItem.id, updates);
} catch (error) {
console.error('Error updating position:', error);
showToast('Fehler beim Speichern', 'error');
toastStore.show('Fehler beim Speichern', 'error');
}
}
@ -75,7 +75,7 @@
await updateBoardItem(selectedItem.id, updates);
} catch (error) {
console.error('Error updating scale:', error);
showToast('Fehler beim Speichern', 'error');
toastStore.show('Fehler beim Speichern', 'error');
}
}
@ -89,7 +89,7 @@
await updateBoardItem(selectedItem.id, updates);
} catch (error) {
console.error('Error updating rotation:', error);
showToast('Fehler beim Speichern', 'error');
toastStore.show('Fehler beim Speichern', 'error');
}
}
@ -104,7 +104,7 @@
await updateBoardItem(selectedItem.id, updates);
} catch (error) {
console.error('Error updating opacity:', error);
showToast('Fehler beim Speichern', 'error');
toastStore.show('Fehler beim Speichern', 'error');
}
}
@ -113,10 +113,10 @@
try {
await changeBoardItemZIndex(selectedItem.id, direction);
showToast('Layer-Reihenfolge geändert', 'success');
toastStore.show('Layer-Reihenfolge geändert', 'success');
} catch (error) {
console.error('Error changing layer:', error);
showToast('Fehler beim Ändern der Layer-Reihenfolge', 'error');
toastStore.show('Fehler beim Ändern der Layer-Reihenfolge', 'error');
}
}
@ -139,7 +139,7 @@
function handleDelete() {
removeSelectedItems();
showToast('Bild entfernt', 'success');
toastStore.show('Bild entfernt', 'success');
}
</script>

View file

@ -9,7 +9,7 @@
unpublishImage,
} from '$lib/api/images';
import { images, selectedImage } from '$lib/stores/images';
import { showToast } from '$lib/stores/toast';
import { toastStore } from '@manacore/shared-ui';
import { fade, fly } from 'svelte/transition';
import { getImageTags, getAllTags, addTagToImage, removeTagFromImage } from '$lib/api/tags';
import {
@ -104,11 +104,11 @@
await archiveImage(imageId);
// Update store
images.update((current) => current.filter((img) => img.id !== imageId));
showToast('Bild erfolgreich archiviert', 'success');
toastStore.show('Bild erfolgreich archiviert', 'success');
onClose();
} catch (error) {
console.error('Error archiving image:', error);
showToast('Fehler beim Archivieren des Bildes', 'error');
toastStore.show('Fehler beim Archivieren des Bildes', 'error');
} finally {
isArchiving = false;
}
@ -129,11 +129,11 @@
await deleteImage(imageId);
// Update store
images.update((current) => current.filter((img) => img.id !== imageId));
showToast('Bild erfolgreich gelöscht', 'success');
toastStore.show('Bild erfolgreich gelöscht', 'success');
onClose();
} catch (error) {
console.error('Error deleting image:', error);
showToast('Fehler beim Löschen des Bildes', 'error');
toastStore.show('Fehler beim Löschen des Bildes', 'error');
} finally {
isDeleting = false;
}
@ -143,7 +143,7 @@
if (!image || !image.publicUrl) return;
const filename = `picture-${image.id}.png`;
downloadImage(image.publicUrl, filename);
showToast('Download gestartet', 'success');
toastStore.show('Download gestartet', 'success');
}
function formatDate(dateString: string) {
@ -164,7 +164,7 @@
allTags = await getAllTags();
} catch (error) {
console.error('Error loading tags:', error);
showToast('Fehler beim Laden der Tags', 'error');
toastStore.show('Fehler beim Laden der Tags', 'error');
} finally {
isLoadingTags = false;
}
@ -183,15 +183,15 @@
if (isTagged) {
await removeTagFromImage(image.id, tag.id);
imageTags = imageTags.filter((t) => t.id !== tag.id);
showToast('Tag entfernt', 'success');
toastStore.show('Tag entfernt', 'success');
} else {
await addTagToImage(image.id, tag.id);
imageTags = [...imageTags, tag];
showToast('Tag hinzugefügt', 'success');
toastStore.show('Tag hinzugefügt', 'success');
}
} catch (error) {
console.error('Error toggling tag:', error);
showToast('Fehler beim Aktualisieren des Tags', 'error');
toastStore.show('Fehler beim Aktualisieren des Tags', 'error');
}
}
@ -213,11 +213,11 @@
if (image) {
image = { ...image, isPublic: true };
}
showToast('Bild erfolgreich veröffentlicht!', 'success');
toastStore.show('Bild erfolgreich veröffentlicht!', 'success');
closePublishModal();
} catch (error) {
console.error('Error publishing image:', error);
showToast('Fehler beim Veröffentlichen des Bildes', 'error');
toastStore.show('Fehler beim Veröffentlichen des Bildes', 'error');
} finally {
isPublishing = false;
}
@ -233,11 +233,11 @@
if (image) {
image = { ...image, isPublic: false };
}
showToast('Bild nicht mehr öffentlich', 'success');
toastStore.show('Bild nicht mehr öffentlich', 'success');
closePublishModal();
} catch (error) {
console.error('Error unpublishing image:', error);
showToast('Fehler beim Entfernen der Veröffentlichung', 'error');
toastStore.show('Fehler beim Entfernen der Veröffentlichung', 'error');
} finally {
isPublishing = false;
}

View file

@ -6,7 +6,7 @@
import { isSidebarCollapsed } from '$lib/stores/sidebar';
import { getActiveModels } from '$lib/api/models';
import { generateImageAsync, subscribeToGenerationUpdates } from '$lib/api/generate-async';
import { showToast } from '$lib/stores/toast';
import { toastStore } from '@manacore/shared-ui';
import { onMount } from 'svelte';
import AdvancedSettingsModal, {
type AdvancedSettings,
@ -59,7 +59,7 @@
}
} catch (error) {
console.error('Error loading models:', error);
showToast('Fehler beim Laden der Modelle', 'error');
toastStore.show('Fehler beim Laden der Modelle', 'error');
} finally {
isLoadingModels.set(false);
}
@ -124,7 +124,7 @@
// Success
generationProgress.set('Fertig!');
showToast(
toastStore.show(
totalImages > 1
? `${totalImages} Bilder erfolgreich generiert!`
: 'Bild erfolgreich generiert!',
@ -137,7 +137,7 @@
console.error('Generation error:', error);
const errorMessage = error instanceof Error ? error.message : 'Generierung fehlgeschlagen';
generationError.set(errorMessage);
showToast(errorMessage, 'error');
toastStore.show(errorMessage, 'error');
} finally {
setTimeout(() => {
isGenerating.set(false);

View file

@ -12,7 +12,7 @@
import { archiveImage, unarchiveImage, deleteImage, toggleFavorite } from '$lib/api/images';
import { images } from '$lib/stores/images';
import { archivedImages } from '$lib/stores/archive';
import { showToast } from '$lib/stores/toast';
import { toastStore } from '@manacore/shared-ui';
import type { Tag } from '$lib/api/tags';
import {
DownloadSimple,
@ -76,10 +76,10 @@
try {
await addTagToImage($contextMenu.image.id, tag.id);
await loadImageTags($contextMenu.image.id);
showToast(`Tag "${tag.name}" hinzugefügt`, 'success');
toastStore.show(`Tag "${tag.name}" hinzugefügt`, 'success');
} catch (error) {
console.error('Error adding tag:', error);
showToast('Fehler beim Hinzufügen des Tags', 'error');
toastStore.show('Fehler beim Hinzufügen des Tags', 'error');
}
}
@ -89,10 +89,10 @@
try {
await removeTagFromImage($contextMenu.image.id, tag.id);
await loadImageTags($contextMenu.image.id);
showToast(`Tag "${tag.name}" entfernt`, 'success');
toastStore.show(`Tag "${tag.name}" entfernt`, 'success');
} catch (error) {
console.error('Error removing tag:', error);
showToast('Fehler beim Entfernen des Tags', 'error');
toastStore.show('Fehler beim Entfernen des Tags', 'error');
}
}
@ -104,7 +104,7 @@
link.download = $contextMenu.image.filename || 'image.png';
link.click();
hideContextMenu();
showToast('Download gestartet', 'success');
toastStore.show('Download gestartet', 'success');
}
function handleCopyLink() {
@ -112,7 +112,7 @@
navigator.clipboard.writeText($contextMenu.image.publicUrl);
hideContextMenu();
showToast('Link kopiert', 'success');
toastStore.show('Link kopiert', 'success');
}
async function handleDelete() {
@ -124,10 +124,10 @@
// Remove from store
images.update((current) => current.filter((img) => img.id !== $contextMenu.image?.id));
hideContextMenu();
showToast('Bild gelöscht', 'success');
toastStore.show('Bild gelöscht', 'success');
} catch (error) {
console.error('Error deleting image:', error);
showToast('Fehler beim Löschen des Bildes', 'error');
toastStore.show('Fehler beim Löschen des Bildes', 'error');
}
}
}
@ -144,18 +144,18 @@
current.filter((img) => img.id !== $contextMenu.image?.id)
);
hideContextMenu();
showToast('Bild wiederhergestellt', 'success');
toastStore.show('Bild wiederhergestellt', 'success');
} else {
// Archive: Move to archive
await archiveImage($contextMenu.image.id);
// Remove from gallery store
images.update((current) => current.filter((img) => img.id !== $contextMenu.image?.id));
hideContextMenu();
showToast('Bild archiviert', 'success');
toastStore.show('Bild archiviert', 'success');
}
} catch (error) {
console.error('Error archiving/unarchiving image:', error);
showToast('Fehler beim Archivieren des Bildes', 'error');
toastStore.show('Fehler beim Archivieren des Bildes', 'error');
}
}
@ -179,13 +179,13 @@
);
hideContextMenu();
showToast(
toastStore.show(
newFavoriteStatus ? 'Zu Favoriten hinzugefügt' : 'Aus Favoriten entfernt',
'success'
);
} catch (error) {
console.error('Error toggling favorite:', error);
showToast('Fehler beim Aktualisieren der Favoriten', 'error');
toastStore.show('Fehler beim Aktualisieren der Favoriten', 'error');
}
}

View file

@ -1,53 +0,0 @@
<script lang="ts">
import { toasts, dismissToast } from '$lib/stores/toast';
import type { Toast } from '$lib/stores/toast';
import { fly, fade } from 'svelte/transition';
import { CheckCircle, XCircle, Warning, Info, X } from '@manacore/shared-icons';
function getToastBgColor(type: Toast['type']) {
switch (type) {
case 'success':
return 'bg-green-50 border-green-200';
case 'error':
return 'bg-red-50 border-red-200';
case 'warning':
return 'bg-yellow-50 border-yellow-200';
case 'info':
default:
return 'bg-blue-50 border-blue-200';
}
}
</script>
<div class="pointer-events-none fixed bottom-4 right-4 z-[100] flex flex-col gap-2">
{#each $toasts as toast (toast.id)}
{@const bgColor = getToastBgColor(toast.type)}
<div
transition:fly={{ y: 50, duration: 300 }}
class="pointer-events-auto flex min-w-[320px] items-start gap-3 rounded-lg border p-4 shadow-lg {bgColor}"
role="alert"
>
<span class="flex-shrink-0">
{#if toast.type === 'success'}
<CheckCircle size={24} weight="regular" class="text-green-500" />
{:else if toast.type === 'error'}
<XCircle size={24} weight="regular" class="text-red-500" />
{:else if toast.type === 'warning'}
<Warning size={24} weight="regular" class="text-yellow-500" />
{:else}
<Info size={24} weight="regular" class="text-blue-500" />
{/if}
</span>
<p class="flex-1 text-sm font-medium text-gray-900">{toast.message}</p>
<button
onclick={() => dismissToast(toast.id)}
class="flex-shrink-0 text-gray-400 hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2"
aria-label="Schließen"
>
<X size={20} weight="bold" />
</button>
</div>
{/each}
</div>

View file

@ -1,80 +0,0 @@
/**
* Toast Store - Svelte 5 Runes Version
*/
export type ToastType = 'success' | 'error' | 'info' | 'warning';
export interface Toast {
id: string;
message: string;
type: ToastType;
duration?: number;
}
let toasts = $state<Toast[]>([]);
let toastId = 0;
export const toastStore = {
get toasts() {
return toasts;
},
show(message: string, type: ToastType = 'info', duration = 5000): string {
const id = `toast-${toastId++}`;
const toast: Toast = { id, message, type, duration };
toasts = [...toasts, toast];
if (duration > 0) {
setTimeout(() => {
toastStore.dismiss(id);
}, duration);
}
return id;
},
dismiss(id: string) {
toasts = toasts.filter((toast) => toast.id !== id);
},
clear() {
toasts = [];
},
success(message: string, duration = 5000) {
return toastStore.show(message, 'success', duration);
},
error(message: string, duration = 5000) {
return toastStore.show(message, 'error', duration);
},
warning(message: string, duration = 5000) {
return toastStore.show(message, 'warning', duration);
},
info(message: string, duration = 5000) {
return toastStore.show(message, 'info', duration);
},
};
// Export for backwards compatibility
export function showToast(message: string, type: ToastType = 'info', duration = 5000) {
return toastStore.show(message, type, duration);
}
export function dismissToast(id: string) {
toastStore.dismiss(id);
}
export function clearToasts() {
toastStore.clear();
}
export function getToasts() {
return toasts;
}
// Re-export for compatibility
export { toasts };

View file

@ -1,37 +0,0 @@
import { writable } from 'svelte/store';
export type ToastType = 'success' | 'error' | 'info' | 'warning';
export interface Toast {
id: string;
message: string;
type: ToastType;
duration?: number;
}
export const toasts = writable<Toast[]>([]);
let toastId = 0;
export function showToast(message: string, type: ToastType = 'info', duration = 5000) {
const id = `toast-${toastId++}`;
const toast: Toast = { id, message, type, duration };
toasts.update((current) => [...current, toast]);
if (duration > 0) {
setTimeout(() => {
dismissToast(id);
}, duration);
}
return id;
}
export function dismissToast(id: string) {
toasts.update((current) => current.filter((toast) => toast.id !== id));
}
export function clearToasts() {
toasts.set([]);
}

View file

@ -2,7 +2,7 @@
import '../app.css';
import favicon from '$lib/assets/favicon.svg';
import { authStore } from '$lib/stores/auth.svelte';
import Toast from '$lib/components/ui/Toast.svelte';
import { ToastContainer } from '@manacore/shared-ui';
import { onMount } from 'svelte';
// Import and initialize theme
@ -43,4 +43,4 @@
{@render children?.()}
<!-- Global Toast Notifications -->
<Toast />
<ToastContainer />

View file

@ -17,7 +17,7 @@
import { PageHeader } from '@manacore/shared-ui';
import Button from '$lib/components/ui/Button.svelte';
import Modal from '$lib/components/ui/Modal.svelte';
import { showToast } from '$lib/stores/toast';
import { toastStore } from '@manacore/shared-ui';
import { Plus, SquaresFour, Image, Trash } from '@manacore/shared-icons';
let loadingMore = $state(false);
@ -70,7 +70,7 @@
hasBoardsMore.set(data.length === 20);
} catch (error) {
console.error('Error loading boards:', error);
showToast('Fehler beim Laden der Boards', 'error');
toastStore.show('Fehler beim Laden der Boards', 'error');
} finally {
isLoadingBoards.set(false);
}
@ -112,10 +112,10 @@
showCreateBoardModal.set(false);
boardName = '';
boardDescription = '';
showToast('Board erstellt', 'success');
toastStore.show('Board erstellt', 'success');
} catch (error) {
console.error('Error creating board:', error);
showToast('Fehler beim Erstellen', 'error');
toastStore.show('Fehler beim Erstellen', 'error');
} finally {
isCreating = false;
}
@ -129,10 +129,10 @@
removeBoardFromList(deletingBoard);
showDeleteModal = false;
deletingBoard = null;
showToast('Board gelöscht', 'success');
toastStore.show('Board gelöscht', 'success');
} catch (error) {
console.error('Error deleting board:', error);
showToast('Fehler beim Löschen', 'error');
toastStore.show('Fehler beim Löschen', 'error');
}
}
@ -142,10 +142,10 @@
try {
const newBoard = await duplicateBoard(boardId);
addBoard({ ...newBoard, itemCount: 0 });
showToast('Board dupliziert', 'success');
toastStore.show('Board dupliziert', 'success');
} catch (error) {
console.error('Error duplicating board:', error);
showToast('Fehler beim Duplizieren', 'error');
toastStore.show('Fehler beim Duplizieren', 'error');
}
}

View file

@ -14,7 +14,7 @@
} from '$lib/stores/canvas';
import { getBoardById } from '$lib/api/boards';
import { getBoardItems, addTextToBoard } from '$lib/api/boardItems';
import { showToast } from '$lib/stores/toast';
import { toastStore } from '@manacore/shared-ui';
import BoardCanvas from '$lib/components/board/BoardCanvas.svelte';
import CanvasToolbar from '$lib/components/board/CanvasToolbar.svelte';
import ImagePickerModal from '$lib/components/board/ImagePickerModal.svelte';
@ -50,7 +50,7 @@
// Check if user has access
if (board.userId !== authStore.user.id && !board.isPublic) {
showToast('Zugriff verweigert', 'error');
toastStore.show('Zugriff verweigert', 'error');
goto('/app/board');
return;
}
@ -63,7 +63,7 @@
canvasItems.set(items);
} catch (error) {
console.error('Error loading board:', error);
showToast('Fehler beim Laden des Boards', 'error');
toastStore.show('Fehler beim Laden des Boards', 'error');
goto('/app/board');
} finally {
isLoading = false;
@ -88,10 +88,10 @@
// Add to canvas
addCanvasItem(text);
showToast('Text hinzugefügt', 'success');
toastStore.show('Text hinzugefügt', 'success');
} catch (error) {
console.error('Error adding text:', error);
showToast('Fehler beim Hinzufügen des Textes', 'error');
toastStore.show('Fehler beim Hinzufügen des Textes', 'error');
}
}

View file

@ -1,15 +1,19 @@
<script lang="ts">
import { SubscriptionPage } from '@manacore/shared-subscription-ui';
import { showToast } from '$lib/stores/toast';
import { toastStore } from '@manacore/shared-ui';
function handleSubscribe(planId: string) {
console.log('Subscribe to plan:', planId);
showToast(`Abo "${planId}" ausgewählt. Bezahlsystem wird noch integriert.`, 'info', 5000);
toastStore.show(`Abo "${planId}" ausgewählt. Bezahlsystem wird noch integriert.`, 'info', 5000);
}
function handleBuyPackage(packageId: string) {
console.log('Buy package:', packageId);
showToast(`Paket "${packageId}" ausgewählt. Bezahlsystem wird noch integriert.`, 'info', 5000);
toastStore.show(
`Paket "${packageId}" ausgewählt. Bezahlsystem wird noch integriert.`,
'info',
5000
);
}
</script>

View file

@ -3,7 +3,7 @@
import { tags, isLoadingTags } from '$lib/stores/tags';
import { getAllTags, createTag, updateTag, deleteTag } from '$lib/api/tags';
import type { Tag } from '$lib/api/tags';
import { showToast } from '$lib/stores/toast';
import { toastStore } from '@manacore/shared-ui';
import { PageHeader } from '@manacore/shared-ui';
import { Plus, Tag as TagIcon, PencilSimple, Trash } from '@manacore/shared-icons';
@ -37,7 +37,7 @@
tags.set(data);
} catch (error) {
console.error('Error loading tags:', error);
showToast('Fehler beim Laden der Tags', 'error');
toastStore.show('Fehler beim Laden der Tags', 'error');
} finally {
isLoadingTags.set(false);
}
@ -52,13 +52,13 @@
color: newTagColor,
});
await loadTags();
showToast('Tag erfolgreich erstellt', 'success');
toastStore.show('Tag erfolgreich erstellt', 'success');
newTagName = '';
newTagColor = '#3B82F6';
showCreateModal = false;
} catch (error) {
console.error('Error creating tag:', error);
showToast('Fehler beim Erstellen des Tags', 'error');
toastStore.show('Fehler beim Erstellen des Tags', 'error');
}
}
@ -78,12 +78,12 @@
color: editTagColor,
});
await loadTags();
showToast('Tag erfolgreich aktualisiert', 'success');
toastStore.show('Tag erfolgreich aktualisiert', 'success');
showEditModal = false;
editingTag = null;
} catch (error) {
console.error('Error updating tag:', error);
showToast('Fehler beim Aktualisieren des Tags', 'error');
toastStore.show('Fehler beim Aktualisieren des Tags', 'error');
}
}
@ -93,10 +93,10 @@
try {
await deleteTag(tagId);
await loadTags();
showToast('Tag erfolgreich gelöscht', 'success');
toastStore.show('Tag erfolgreich gelöscht', 'success');
} catch (error) {
console.error('Error deleting tag:', error);
showToast('Fehler beim Löschen des Tags', 'error');
toastStore.show('Fehler beim Löschen des Tags', 'error');
}
}
</script>

View file

@ -3,7 +3,7 @@
import { goto } from '$app/navigation';
import { uploadMultipleImages } from '$lib/api/upload';
import type { UploadProgress } from '$lib/api/upload';
import { showToast } from '$lib/stores/toast';
import { toastStore } from '@manacore/shared-ui';
import { PageHeader } from '@manacore/shared-ui';
import DropZone from '$lib/components/upload/DropZone.svelte';
import { images } from '$lib/stores/images';
@ -15,7 +15,7 @@
async function handleFilesSelected(files: File[]) {
if (!authStore.user) {
showToast('Bitte melde dich an', 'error');
toastStore.show('Bitte melde dich an', 'error');
return;
}
@ -33,12 +33,15 @@
images.update((current) => [...uploadedImages, ...current]);
if (successCount === files.length) {
showToast(
toastStore.show(
`${successCount} ${successCount === 1 ? 'Bild' : 'Bilder'} erfolgreich hochgeladen`,
'success'
);
} else {
showToast(`${successCount} von ${files.length} Bildern erfolgreich hochgeladen`, 'warning');
toastStore.show(
`${successCount} von ${files.length} Bildern erfolgreich hochgeladen`,
'warning'
);
}
// Redirect to gallery after successful upload
@ -47,7 +50,7 @@
}, 2000);
} catch (error) {
console.error('Upload error:', error);
showToast('Fehler beim Hochladen der Bilder', 'error');
toastStore.show('Fehler beim Hochladen der Bilder', 'error');
} finally {
uploading = false;
}

View file

@ -1,188 +0,0 @@
<script lang="ts">
import { toast } from '$lib/stores/toast';
import type { Toast } from '$lib/stores/toast';
import { fly } from 'svelte/transition';
let toasts = $state<Toast[]>([]);
toast.subscribe((value) => {
toasts = value;
});
function handleClose(id: string) {
toast.remove(id);
}
function getIcon(type: Toast['type']) {
switch (type) {
case 'success':
return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
<polyline points="22 4 12 14.01 9 11.01"></polyline>
</svg>`;
case 'error':
return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<line x1="15" y1="9" x2="9" y2="15"></line>
<line x1="9" y1="9" x2="15" y2="15"></line>
</svg>`;
case 'warning':
return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
<line x1="12" y1="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>`;
case 'info':
default:
return `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="16" x2="12" y2="12"></line>
<line x1="12" y1="8" x2="12.01" y2="8"></line>
</svg>`;
}
}
</script>
<div class="toast-container">
{#each toasts as toastItem (toastItem.id)}
<div
class="toast toast-{toastItem.type}"
transition:fly={{ y: 20, duration: 300 }}
role="alert"
>
<div class="toast-icon">
{@html getIcon(toastItem.type)}
</div>
<p class="toast-message">{toastItem.message}</p>
<button
class="toast-close"
onclick={() => handleClose(toastItem.id)}
aria-label="Close notification"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
{/each}
</div>
<style>
.toast-container {
position: fixed;
bottom: 2rem;
right: 2rem;
z-index: 9999;
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
pointer-events: none;
}
.toast {
display: flex;
align-items: center;
gap: var(--spacing-md);
padding: var(--spacing-md) var(--spacing-lg);
background: rgb(var(--color-surface-elevated));
border-radius: var(--radius-lg);
box-shadow: var(--shadow-xl);
border: 1px solid rgb(var(--color-border));
min-width: 300px;
max-width: 400px;
pointer-events: auto;
backdrop-filter: blur(10px);
}
.toast-icon {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.toast-success {
border-left: 4px solid rgb(var(--color-success));
}
.toast-success .toast-icon {
color: rgb(var(--color-success));
}
.toast-error {
border-left: 4px solid rgb(var(--color-error));
}
.toast-error .toast-icon {
color: rgb(var(--color-error));
}
.toast-warning {
border-left: 4px solid rgb(var(--color-warning));
}
.toast-warning .toast-icon {
color: rgb(var(--color-warning));
}
.toast-info {
border-left: 4px solid rgb(var(--color-info));
}
.toast-info .toast-icon {
color: rgb(var(--color-info));
}
.toast-message {
flex: 1;
margin: 0;
font-size: 0.9375rem;
color: rgb(var(--color-text-primary));
line-height: 1.5;
}
.toast-close {
flex-shrink: 0;
background: none;
border: none;
padding: var(--spacing-xs);
cursor: pointer;
color: rgb(var(--color-text-secondary));
transition: all var(--transition-fast);
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
}
.toast-close:hover {
background: rgba(var(--color-border), 0.5);
color: rgb(var(--color-text-primary));
}
@media (max-width: 768px) {
.toast-container {
bottom: 6rem;
right: 1rem;
left: 1rem;
align-items: stretch;
}
.toast {
min-width: auto;
max-width: none;
}
.toast-message {
font-size: 0.875rem;
}
}
</style>

View file

@ -1,63 +0,0 @@
/**
* Toast Store - Manages toast notifications
*/
import { writable } from 'svelte/store';
export interface Toast {
id: string;
type: 'success' | 'error' | 'warning' | 'info';
message: string;
duration?: number;
}
function createToastStore() {
const { subscribe, update } = writable<Toast[]>([]);
function add(toast: Omit<Toast, 'id'>) {
const id = crypto.randomUUID();
const duration = toast.duration ?? 5000;
update((toasts) => [...toasts, { ...toast, id }]);
if (duration > 0) {
setTimeout(() => {
remove(id);
}, duration);
}
return id;
}
function remove(id: string) {
update((toasts) => toasts.filter((t) => t.id !== id));
}
function success(message: string, duration?: number) {
return add({ type: 'success', message, duration });
}
function error(message: string, duration?: number) {
return add({ type: 'error', message, duration });
}
function warning(message: string, duration?: number) {
return add({ type: 'warning', message, duration });
}
function info(message: string, duration?: number) {
return add({ type: 'info', message, duration });
}
return {
subscribe,
add,
remove,
success,
error,
warning,
info,
};
}
export const toast = createToastStore();

View file

@ -16,7 +16,7 @@
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
import { getPillAppItems } from '@manacore/shared-branding';
import { setLocale, supportedLocales } from '$lib/i18n';
import ToastContainer from '$lib/components/ToastContainer.svelte';
import { ToastContainer } from '@manacore/shared-ui';
import '../app.css';
// App switcher items

View file

@ -5,7 +5,7 @@
import { searchApi } from '$lib/api/client';
import type { StorageFile, StorageFolder } from '$lib/api/client';
import { filesStore } from '$lib/stores/files.svelte';
import { toast } from '$lib/stores/toast';
import { toastStore } from '@manacore/shared-ui';
import FileGrid from '$lib/components/files/FileGrid.svelte';
import FileList from '$lib/components/files/FileList.svelte';
@ -47,7 +47,7 @@
const result = await filesStore.toggleFileFavorite(file.id);
if (!result.error) {
files = files.filter((f) => f.id !== file.id);
toast.success('Favorit entfernt');
toastStore.success('Favorit entfernt');
}
}
}
@ -57,7 +57,7 @@
const result = await filesStore.toggleFolderFavorite(folder.id);
if (!result.error) {
folders = folders.filter((f) => f.id !== folder.id);
toast.success('Favorit entfernt');
toastStore.success('Favorit entfernt');
}
}
}

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { ChatCircle, PaperPlaneTilt } from '@manacore/shared-icons';
import { toast } from '$lib/stores/toast';
import { toastStore } from '@manacore/shared-ui';
let type = $state<'bug' | 'feature' | 'other'>('feature');
let message = $state('');
@ -14,7 +14,7 @@
// Simulate sending feedback
await new Promise((resolve) => setTimeout(resolve, 1000));
toast.success('Feedback gesendet! Vielen Dank.');
toastStore.success('Feedback gesendet! Vielen Dank.');
message = '';
type = 'feature';
sending = false;

View file

@ -3,7 +3,7 @@
import { onMount } from 'svelte';
import { GridFour, List, Plus, FolderPlus, UploadSimple } from '@manacore/shared-icons';
import { filesStore } from '$lib/stores/files.svelte';
import { toast } from '$lib/stores/toast';
import { toastStore } from '@manacore/shared-ui';
import type { StorageFile, StorageFolder } from '$lib/api/client';
import FileGrid from '$lib/components/files/FileGrid.svelte';
import FileList from '$lib/components/files/FileList.svelte';
@ -41,42 +41,42 @@
switch (action) {
case 'download':
await filesStore.downloadFile(file.id, file.name);
toast.success('Download gestartet');
toastStore.success('Download gestartet');
break;
case 'rename':
const newName = prompt('Neuer Name:', file.name);
if (newName && newName !== file.name) {
const result = await filesStore.renameFile(file.id, newName);
if (result.error) {
toast.error(result.error);
toastStore.error(result.error);
} else {
toast.success('Datei umbenannt');
toastStore.success('Datei umbenannt');
}
}
break;
case 'favorite':
const favResult = await filesStore.toggleFileFavorite(file.id);
if (!favResult.error) {
toast.success(file.isFavorite ? 'Favorit entfernt' : 'Als Favorit markiert');
toastStore.success(file.isFavorite ? 'Favorit entfernt' : 'Als Favorit markiert');
}
break;
case 'delete':
if (confirm('Datei in den Papierkorb verschieben?')) {
const delResult = await filesStore.deleteFile(file.id);
if (delResult.error) {
toast.error(delResult.error);
toastStore.error(delResult.error);
} else {
toast.success('In den Papierkorb verschoben');
toastStore.success('In den Papierkorb verschoben');
}
}
break;
case 'share':
// TODO: Open share modal
toast.info('Teilen-Funktion kommt bald');
toastStore.info('Teilen-Funktion kommt bald');
break;
case 'move':
// TODO: Open move modal
toast.info('Verschieben-Funktion kommt bald');
toastStore.info('Verschieben-Funktion kommt bald');
break;
}
}
@ -88,33 +88,33 @@
if (newName && newName !== folder.name) {
const result = await filesStore.renameFolder(folder.id, newName);
if (result.error) {
toast.error(result.error);
toastStore.error(result.error);
} else {
toast.success('Ordner umbenannt');
toastStore.success('Ordner umbenannt');
}
}
break;
case 'favorite':
const favResult = await filesStore.toggleFolderFavorite(folder.id);
if (!favResult.error) {
toast.success(folder.isFavorite ? 'Favorit entfernt' : 'Als Favorit markiert');
toastStore.success(folder.isFavorite ? 'Favorit entfernt' : 'Als Favorit markiert');
}
break;
case 'delete':
if (confirm('Ordner und Inhalt in den Papierkorb verschieben?')) {
const delResult = await filesStore.deleteFolder(folder.id);
if (delResult.error) {
toast.error(delResult.error);
toastStore.error(delResult.error);
} else {
toast.success('In den Papierkorb verschoben');
toastStore.success('In den Papierkorb verschoben');
}
}
break;
case 'share':
toast.info('Teilen-Funktion kommt bald');
toastStore.info('Teilen-Funktion kommt bald');
break;
case 'move':
toast.info('Verschieben-Funktion kommt bald');
toastStore.info('Verschieben-Funktion kommt bald');
break;
}
}
@ -129,7 +129,7 @@
for (const file of files) {
const result = await filesStore.uploadFile(file);
if (result.error) {
toast.error(`Fehler beim Hochladen von ${file.name}: ${result.error}`);
toastStore.error(`Fehler beim Hochladen von ${file.name}: ${result.error}`);
}
completed++;
uploadProgress = Math.round((completed / totalFiles) * 100);
@ -138,15 +138,15 @@
uploading = false;
uploadProgress = 0;
showUploadZone = false;
toast.success(`${totalFiles} Datei(en) hochgeladen`);
toastStore.success(`${totalFiles} Datei(en) hochgeladen`);
}
async function handleCreateFolder(name: string, color?: string) {
const result = await filesStore.createFolder(name, color);
if (result.error) {
toast.error(result.error);
toastStore.error(result.error);
} else {
toast.success('Ordner erstellt');
toastStore.success('Ordner erstellt');
}
}

View file

@ -4,7 +4,7 @@
import { onMount } from 'svelte';
import { GridFour, List, FolderPlus, UploadSimple, ArrowLeft } from '@manacore/shared-icons';
import { filesStore } from '$lib/stores/files.svelte';
import { toast } from '$lib/stores/toast';
import { toastStore } from '@manacore/shared-ui';
import type { StorageFile, StorageFolder } from '$lib/api/client';
import FileGrid from '$lib/components/files/FileGrid.svelte';
import FileList from '$lib/components/files/FileList.svelte';
@ -48,40 +48,40 @@
switch (action) {
case 'download':
await filesStore.downloadFile(file.id, file.name);
toast.success('Download gestartet');
toastStore.success('Download gestartet');
break;
case 'rename':
const newName = prompt('Neuer Name:', file.name);
if (newName && newName !== file.name) {
const result = await filesStore.renameFile(file.id, newName);
if (result.error) {
toast.error(result.error);
toastStore.error(result.error);
} else {
toast.success('Datei umbenannt');
toastStore.success('Datei umbenannt');
}
}
break;
case 'favorite':
const favResult = await filesStore.toggleFileFavorite(file.id);
if (!favResult.error) {
toast.success(file.isFavorite ? 'Favorit entfernt' : 'Als Favorit markiert');
toastStore.success(file.isFavorite ? 'Favorit entfernt' : 'Als Favorit markiert');
}
break;
case 'delete':
if (confirm('Datei in den Papierkorb verschieben?')) {
const delResult = await filesStore.deleteFile(file.id);
if (delResult.error) {
toast.error(delResult.error);
toastStore.error(delResult.error);
} else {
toast.success('In den Papierkorb verschoben');
toastStore.success('In den Papierkorb verschoben');
}
}
break;
case 'share':
toast.info('Teilen-Funktion kommt bald');
toastStore.info('Teilen-Funktion kommt bald');
break;
case 'move':
toast.info('Verschieben-Funktion kommt bald');
toastStore.info('Verschieben-Funktion kommt bald');
break;
}
}
@ -93,33 +93,33 @@
if (newName && newName !== folder.name) {
const result = await filesStore.renameFolder(folder.id, newName);
if (result.error) {
toast.error(result.error);
toastStore.error(result.error);
} else {
toast.success('Ordner umbenannt');
toastStore.success('Ordner umbenannt');
}
}
break;
case 'favorite':
const favResult = await filesStore.toggleFolderFavorite(folder.id);
if (!favResult.error) {
toast.success(folder.isFavorite ? 'Favorit entfernt' : 'Als Favorit markiert');
toastStore.success(folder.isFavorite ? 'Favorit entfernt' : 'Als Favorit markiert');
}
break;
case 'delete':
if (confirm('Ordner und Inhalt in den Papierkorb verschieben?')) {
const delResult = await filesStore.deleteFolder(folder.id);
if (delResult.error) {
toast.error(delResult.error);
toastStore.error(delResult.error);
} else {
toast.success('In den Papierkorb verschoben');
toastStore.success('In den Papierkorb verschoben');
}
}
break;
case 'share':
toast.info('Teilen-Funktion kommt bald');
toastStore.info('Teilen-Funktion kommt bald');
break;
case 'move':
toast.info('Verschieben-Funktion kommt bald');
toastStore.info('Verschieben-Funktion kommt bald');
break;
}
}
@ -134,7 +134,7 @@
for (const file of files) {
const result = await filesStore.uploadFile(file);
if (result.error) {
toast.error(`Fehler beim Hochladen von ${file.name}: ${result.error}`);
toastStore.error(`Fehler beim Hochladen von ${file.name}: ${result.error}`);
}
completed++;
uploadProgress = Math.round((completed / totalFiles) * 100);
@ -143,15 +143,15 @@
uploading = false;
uploadProgress = 0;
showUploadZone = false;
toast.success(`${totalFiles} Datei(en) hochgeladen`);
toastStore.success(`${totalFiles} Datei(en) hochgeladen`);
}
async function handleCreateFolder(name: string, color?: string) {
const result = await filesStore.createFolder(name, color);
if (result.error) {
toast.error(result.error);
toastStore.error(result.error);
} else {
toast.success('Ordner erstellt');
toastStore.success('Ordner erstellt');
}
}

View file

@ -3,7 +3,7 @@
import { ShareNetwork, Link, Copy, Trash } from '@manacore/shared-icons';
import { sharesApi } from '$lib/api/client';
import type { Share } from '$lib/api/client';
import { toast } from '$lib/stores/toast';
import { toastStore } from '@manacore/shared-ui';
let shares = $state<Share[]>([]);
let loading = $state(true);
@ -30,7 +30,7 @@
async function copyShareLink(token: string) {
const url = `${window.location.origin}/s/${token}`;
await navigator.clipboard.writeText(url);
toast.success('Link kopiert!');
toastStore.success('Link kopiert!');
}
async function deleteShare(id: string) {
@ -38,10 +38,10 @@
const result = await sharesApi.delete(id);
if (result.error) {
toast.error(result.error);
toastStore.error(result.error);
} else {
shares = shares.filter((s) => s.id !== id);
toast.success('Share-Link gelöscht');
toastStore.success('Share-Link gelöscht');
}
}

View file

@ -3,7 +3,7 @@
import { Trash, ArrowCounterClockwise, Warning } from '@manacore/shared-icons';
import { trashApi } from '$lib/api/client';
import type { StorageFile, StorageFolder } from '$lib/api/client';
import { toast } from '$lib/stores/toast';
import { toastStore } from '@manacore/shared-ui';
let files = $state<StorageFile[]>([]);
let folders = $state<StorageFolder[]>([]);
@ -32,14 +32,14 @@
async function handleRestore(id: string, type: 'file' | 'folder') {
const result = await trashApi.restore(id, type);
if (result.error) {
toast.error(result.error);
toastStore.error(result.error);
} else {
if (type === 'file') {
files = files.filter((f) => f.id !== id);
} else {
folders = folders.filter((f) => f.id !== id);
}
toast.success('Wiederhergestellt');
toastStore.success('Wiederhergestellt');
}
}
@ -48,14 +48,14 @@
const result = await trashApi.permanentDelete(id, type);
if (result.error) {
toast.error(result.error);
toastStore.error(result.error);
} else {
if (type === 'file') {
files = files.filter((f) => f.id !== id);
} else {
folders = folders.filter((f) => f.id !== id);
}
toast.success('Endgültig gelöscht');
toastStore.success('Endgültig gelöscht');
}
}
@ -64,11 +64,11 @@
const result = await trashApi.empty();
if (result.error) {
toast.error(result.error);
toastStore.error(result.error);
} else {
files = [];
folders = [];
toast.success('Papierkorb geleert');
toastStore.success('Papierkorb geleert');
}
}

View file

@ -195,3 +195,7 @@ export type {
// Immersive Mode
export { default as ImmersiveModeToggle } from './components/ImmersiveModeToggle.svelte';
// Toast
export { toastStore, toast, handleApiError, ToastContainer } from './toast';
export type { Toast, ToastType } from './toast';

View file

@ -1,5 +1,14 @@
<script lang="ts">
import { Search, ZoomIn, ZoomOut, RotateCcw, Filter, X, Focus, Keyboard } from 'lucide-svelte';
import {
MagnifyingGlass,
MagnifyingGlassPlus,
MagnifyingGlassMinus,
ArrowCounterClockwise,
Funnel,
X,
Crosshair,
Keyboard,
} from '@manacore/shared-icons';
import type { NetworkTag } from './network.types';
interface Props {
@ -126,7 +135,7 @@
<!-- Search bar -->
{#if showSearch}
<div class="search-container">
<Search size={18} class="search-icon" />
<MagnifyingGlass size={18} class="search-icon" />
<input
bind:this={searchInputElement}
type="text"
@ -152,7 +161,7 @@
aria-label="Filter anzeigen"
title="Filter"
>
<Filter size={18} />
<Funnel size={18} />
{#if hasActiveFilters}
<span class="filter-badge"></span>
{/if}
@ -162,7 +171,7 @@
<!-- Zoom controls -->
<div class="zoom-controls">
<button onclick={onZoomIn} class="control-btn" aria-label="Vergrößern" title="Vergrößern (+)">
<ZoomIn size={18} />
<MagnifyingGlassPlus size={18} />
</button>
<button
onclick={onZoomOut}
@ -170,7 +179,7 @@
aria-label="Verkleinern"
title="Verkleinern (-)"
>
<ZoomOut size={18} />
<MagnifyingGlassMinus size={18} />
</button>
<button
onclick={onResetZoom}
@ -178,7 +187,7 @@
aria-label="Ansicht zurücksetzen"
title="Zurücksetzen (0)"
>
<RotateCcw size={18} />
<ArrowCounterClockwise size={18} />
</button>
<button
onclick={onFocusSelected}
@ -186,7 +195,7 @@
aria-label="Auf Auswahl fokussieren"
title="Fokus auf Auswahl (F)"
>
<Focus size={18} />
<Crosshair size={18} />
</button>
</div>

View file

@ -0,0 +1,139 @@
<script lang="ts">
import { toastStore } from './toast.svelte';
import type { Toast } from './toast.svelte';
import { X, CheckCircle, XCircle, Warning, Info } from '@manacore/shared-icons';
let toasts = $derived(toastStore.toasts);
const icons = {
success: CheckCircle,
error: XCircle,
warning: Warning,
info: Info,
};
const colors = {
success: 'bg-green-500/90 text-white',
error: 'bg-destructive/90 text-destructive-foreground',
warning: 'bg-amber-500/90 text-white',
info: 'bg-primary/90 text-primary-foreground',
};
function handleDismiss(id: string) {
toastStore.dismiss(id);
}
</script>
{#if toasts.length > 0}
<div class="toast-container">
{#each toasts as toast (toast.id)}
{@const Icon = icons[toast.type]}
<div class="toast-item {colors[toast.type]}" role="alert">
<Icon size={20} weight="fill" class="toast-icon" />
<p class="toast-message">{toast.message}</p>
<button
onclick={() => handleDismiss(toast.id)}
class="toast-dismiss"
aria-label="Schließen"
>
<X size={16} weight="bold" />
</button>
</div>
{/each}
</div>
{/if}
<style>
.toast-container {
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
z-index: 100;
display: flex;
flex-direction: column;
gap: 0.75rem;
max-width: 24rem;
}
/* Mobile: full width at bottom */
@media (max-width: 640px) {
.toast-container {
left: 1rem;
right: 1rem;
bottom: 1rem;
max-width: none;
}
}
.toast-item {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 0.75rem 1rem;
border-radius: 0.75rem;
box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.2);
animation: toast-slide-in 0.3s ease-out;
}
.toast-icon {
flex-shrink: 0;
margin-top: 0.125rem;
}
.toast-message {
flex: 1;
font-size: 0.875rem;
font-weight: 500;
line-height: 1.4;
margin: 0;
}
.toast-dismiss {
flex-shrink: 0;
padding: 0.25rem;
border-radius: 0.5rem;
background: transparent;
border: none;
cursor: pointer;
opacity: 0.8;
transition: all 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
color: inherit;
}
.toast-dismiss:hover {
opacity: 1;
background: rgba(255, 255, 255, 0.2);
}
@keyframes toast-slide-in {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* Mobile: slide up instead of from right */
@media (max-width: 640px) {
@keyframes toast-slide-in {
from {
opacity: 0;
transform: translateY(100%);
}
to {
opacity: 1;
transform: translateY(0);
}
}
}
</style>

View file

@ -0,0 +1,3 @@
export { toastStore, toast, handleApiError } from './toast.svelte';
export type { Toast, ToastType } from './toast.svelte';
export { default as ToastContainer } from './ToastContainer.svelte';

View file

@ -0,0 +1,146 @@
/**
* Toast Store - Centralized notification system using Svelte 5 runes
*
* Usage:
* ```ts
* import { toastStore } from '@manacore/shared-ui';
*
* // Show notifications
* toastStore.success('Saved successfully');
* toastStore.error('Something went wrong');
* toastStore.warning('Please check your input');
* toastStore.info('New update available');
*
* // Manual control
* const id = toastStore.show('Custom message', 'info', 5000);
* toastStore.dismiss(id);
* toastStore.dismissAll();
* ```
*/
export type ToastType = 'success' | 'error' | 'warning' | 'info';
export interface Toast {
id: string;
type: ToastType;
message: string;
duration: number;
}
// State
let toasts = $state<Toast[]>([]);
// Auto-incrementing ID with timestamp for uniqueness
let nextId = 0;
function generateId(): string {
return `toast-${++nextId}-${Date.now()}`;
}
export const toastStore = {
/**
* Get all active toasts (reactive)
*/
get toasts() {
return toasts;
},
/**
* Show a toast notification
* @param message - The message to display
* @param type - Toast type: 'success' | 'error' | 'warning' | 'info'
* @param duration - Duration in ms (0 = permanent, default: 4000)
* @returns The toast ID for manual dismissal
*/
show(message: string, type: ToastType = 'info', duration = 4000): string {
const id = generateId();
const toast: Toast = { id, type, message, duration };
toasts = [...toasts, toast];
// Auto-remove after duration (unless permanent)
if (duration > 0) {
setTimeout(() => {
this.dismiss(id);
}, duration);
}
return id;
},
/**
* Show a success toast (green)
* @param message - The message to display
* @param duration - Duration in ms (default: 4000)
*/
success(message: string, duration?: number): string {
return this.show(message, 'success', duration);
},
/**
* Show an error toast (red) - longer default duration
* @param message - The message to display
* @param duration - Duration in ms (default: 6000)
*/
error(message: string, duration = 6000): string {
return this.show(message, 'error', duration);
},
/**
* Show a warning toast (amber)
* @param message - The message to display
* @param duration - Duration in ms (default: 4000)
*/
warning(message: string, duration?: number): string {
return this.show(message, 'warning', duration);
},
/**
* Show an info toast (blue)
* @param message - The message to display
* @param duration - Duration in ms (default: 4000)
*/
info(message: string, duration?: number): string {
return this.show(message, 'info', duration);
},
/**
* Dismiss a specific toast by ID
* @param id - The toast ID to dismiss
*/
dismiss(id: string): void {
toasts = toasts.filter((t) => t.id !== id);
},
/**
* Dismiss all active toasts
*/
dismissAll(): void {
toasts = [];
},
};
/**
* Helper function for API error handling
* Shows an error toast and returns the error message
*
* @example
* ```ts
* try {
* await api.save(data);
* } catch (error) {
* handleApiError(error, 'Could not save data');
* }
* ```
*/
export function handleApiError(
error: unknown,
fallbackMessage = 'Ein Fehler ist aufgetreten'
): string {
const message = error instanceof Error ? error.message : fallbackMessage;
toastStore.error(message);
return message;
}
// Backwards compatible alias
export const toast = toastStore;