styling
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
/* Stile, die NUR für den Container gelten */
|
||||
.snackbar-container-wrapper {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1050;
|
||||
width: 350px;
|
||||
max-width: 90vw;
|
||||
height: 300px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.snackbar-container-wrapper.position-top {
|
||||
bottom: auto;
|
||||
top: 2rem;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="snackbar-container-wrapper" [class.position-top]="(position$ | async) === 'top'">
|
||||
<app-snackbar
|
||||
*ngFor="let snack of (snackbars$ | async); let i = index"
|
||||
[message]="snack.message"
|
||||
[style]="getSnackbarStyle(i, snack.state, (position$ | async)!)"
|
||||
(close)="closeSnackbar(snack.id)">
|
||||
</app-snackbar>
|
||||
</div>
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Snackbar, SnackbarService } from '../../services/snackbar.service';
|
||||
import { SnackbarComponent } from '../snackbar/snackbar.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-snackbar-container',
|
||||
standalone: true,
|
||||
imports: [CommonModule, SnackbarComponent],
|
||||
templateUrl: './snackbar-container.component.html',
|
||||
styleUrl: './snackbar-container.component.css'
|
||||
})
|
||||
export class SnackbarContainerComponent {
|
||||
snackbars$: Observable<Snackbar[]>;
|
||||
position$: Observable<'top' | 'bottom'>;
|
||||
|
||||
constructor(private snackbarService: SnackbarService) {
|
||||
this.snackbars$ = this.snackbarService.snackbars$;
|
||||
this.position$ = this.snackbarService.position$;
|
||||
}
|
||||
|
||||
// Diese Methode gehört hierher, da der Container die Position aller Kinder kennen muss.
|
||||
getSnackbarStyle(index: number, state: 'visible' | 'fading', position: 'top' | 'bottom'): { [key: string]: any } {
|
||||
const snackbarHeight = 75;
|
||||
const verticalOffset = index * snackbarHeight;
|
||||
const positionProperty = position === 'bottom' ? 'bottom' : 'top';
|
||||
|
||||
const opacity = 1 - (index * 0.2);
|
||||
const scale = 1 - (index * 0.02);
|
||||
|
||||
const visibleStyle = {
|
||||
[positionProperty]: `${verticalOffset}px`,
|
||||
'opacity': opacity,
|
||||
'transform': `scale(${scale})`,
|
||||
'z-index': 1000 - index
|
||||
};
|
||||
const fadingStyle = { ...visibleStyle, 'opacity': 0, 'transform': 'scale(0.8)' };
|
||||
|
||||
return state === 'visible' ? visibleStyle : fadingStyle;
|
||||
}
|
||||
|
||||
closeSnackbar(id: number): void {
|
||||
this.snackbarService.close(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/* Stile, die NUR für eine einzelne Snackbar gelten */
|
||||
:host {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: var(--border-radius-md);
|
||||
background-color: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: var(--box-shadow-md);
|
||||
pointer-events: auto;
|
||||
transition: all 0.5s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.snackbar-icon-container {
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background-color: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
:host-context(body.dark-theme) .snackbar-icon-container {
|
||||
background-color: #166534;
|
||||
color: #dcfce7;
|
||||
}
|
||||
.snackbar-message {
|
||||
flex-grow: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
.snackbar-close-btn {
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 1.5rem;
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
.snackbar-close-btn:hover {
|
||||
background-color: var(--color-body-bg);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<div class="snackbar-icon-container">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<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>
|
||||
</div>
|
||||
<span class="snackbar-message">{{ message }}</span>
|
||||
<button class="btn btn-icon snackbar-close-btn" (click)="onClose()" data-tooltip="Schließen">
|
||||
×
|
||||
</button>
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-snackbar',
|
||||
standalone: true,
|
||||
imports: [],
|
||||
templateUrl: './snackbar.component.html',
|
||||
styleUrl: './snackbar.component.css'
|
||||
})
|
||||
export class SnackbarComponent {
|
||||
@Input() message: string = '';
|
||||
@Output() close = new EventEmitter<void>();
|
||||
|
||||
onClose() {
|
||||
this.close.emit();
|
||||
}
|
||||
}
|
||||
55
src/app/shared/snackbar/services/snackbar.service.ts
Normal file
55
src/app/shared/snackbar/services/snackbar.service.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
|
||||
// Exportiere das Interface, damit es wiederverwendbar ist
|
||||
export interface Snackbar {
|
||||
id: number;
|
||||
message: string;
|
||||
state: 'visible' | 'fading';
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class SnackbarService {
|
||||
// BehaviorSubject hält den aktuellen Zustand und benachrichtigt alle "Zuhörer"
|
||||
private snackbarsSubject = new BehaviorSubject<Snackbar[]>([]);
|
||||
snackbars$: Observable<Snackbar[]> = this.snackbarsSubject.asObservable();
|
||||
|
||||
// Die Position wird ebenfalls hier als globaler Zustand verwaltet
|
||||
private positionSubject = new BehaviorSubject<'top' | 'bottom'>('top');
|
||||
position$: Observable<'top' | 'bottom'> = this.positionSubject.asObservable();
|
||||
|
||||
private nextId = 0;
|
||||
|
||||
// Öffentliche Methode zum Anzeigen einer Snackbar
|
||||
show(message: string): void {
|
||||
const newSnackbar: Snackbar = { id: this.nextId++, message, state: 'visible' };
|
||||
const currentSnackbars = [newSnackbar, ...this.snackbarsSubject.value];
|
||||
this.snackbarsSubject.next(currentSnackbars);
|
||||
|
||||
// Timer zum automatischen Schließen
|
||||
setTimeout(() => this.close(newSnackbar.id), 5000);
|
||||
}
|
||||
|
||||
// Öffentliche Methode zum Schließen einer Snackbar
|
||||
close(id: number): void {
|
||||
let currentSnackbars = this.snackbarsSubject.value;
|
||||
const index = currentSnackbars.findIndex(s => s.id === id);
|
||||
|
||||
if (index > -1 && currentSnackbars[index].state === 'visible') {
|
||||
currentSnackbars[index].state = 'fading';
|
||||
this.snackbarsSubject.next([...currentSnackbars]);
|
||||
|
||||
setTimeout(() => {
|
||||
currentSnackbars = this.snackbarsSubject.value.filter(s => s.id !== id);
|
||||
this.snackbarsSubject.next(currentSnackbars);
|
||||
}, 100); // Muss zur CSS-Dauer passen
|
||||
}
|
||||
}
|
||||
|
||||
// Öffentliche Methode zum Ändern der Position
|
||||
setPosition(position: 'top' | 'bottom'): void {
|
||||
this.positionSubject.next(position);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user