mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-23 06:26:41 +02:00
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:
parent
6d0d9d4f67
commit
14ce457c7b
56 changed files with 487 additions and 1249 deletions
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
139
packages/shared-ui/src/toast/ToastContainer.svelte
Normal file
139
packages/shared-ui/src/toast/ToastContainer.svelte
Normal 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>
|
||||
3
packages/shared-ui/src/toast/index.ts
Normal file
3
packages/shared-ui/src/toast/index.ts
Normal 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';
|
||||
146
packages/shared-ui/src/toast/toast.svelte.ts
Normal file
146
packages/shared-ui/src/toast/toast.svelte.ts
Normal 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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue