This commit is contained in:
Tizian.Breuch
2025-09-08 12:11:30 +02:00
parent 99dee65a79
commit dd4e2b7d9d
7 changed files with 672 additions and 25 deletions

View File

@@ -1 +1 @@
<router-outlet></router-outlet> <router-outlet style="height: 100vh; width: 100vw;"></router-outlet>

View File

@@ -1,9 +1,15 @@
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router'; import { provideRouter } from '@angular/router';
import { provideAnimations } from '@angular/platform-browser/animations'; // Importieren
import { routes } from './app.routes'; import { routes } from './app.routes';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser'; import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideClientHydration(withEventReplay())] providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideClientHydration(withEventReplay()),
provideAnimations() // Hier hinzufügen
]
}; };

View File

@@ -674,3 +674,122 @@
<!-- Ende .dashboard-grid --> <!-- Ende .dashboard-grid -->
</main> </main>
</div> </div>
<!-- =================================================================== -->
<!-- DYNAMISCHER DIALOG (Standardmäßig unsichtbar) -->
<!-- =================================================================== -->
<div
class="dialog-backdrop"
*ngIf="isDialogOpen"
(click)="closeDialog()"
[@fade]> <!-- Angular Animation -->
<!-- (click.stop) verhindert, dass der Klick auf den Container den Backdrop schließt -->
<div class="dialog-container card" (click)="$event.stopPropagation()" [@slideIn]>
<div class="dialog-header">
<h3 class="dialog-title">Aktion bestätigen</h3>
<!-- (click) ruft die closeDialog Methode auf -->
<button class="btn btn-icon" (click)="closeDialog()" data-tooltip="Schließen">&times;</button>
</div>
<div class="dialog-content">
<div class="dialog-icon-container">
<!-- Ein Icon, um die Art des Dialogs zu verdeutlichen -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
</div>
<p>Sind Sie sicher, dass Sie die Bestellung <strong>#10543</strong> löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.</p>
</div>
<div class="dialog-actions">
<!-- (click) ruft die closeDialog Methode auf -->
<button class="btn btn-stroked" (click)="closeDialog()">Abbrechen</button>
<button class="btn btn-danger" (click)="closeDialog()">Ja, endgültig löschen</button>
</div>
</div>
</div>
<div class="menu-container">
<button class="btn btn-icon">...</button> <!-- Der Auslöser -->
<div class="menu-panel card"> <!-- Das Panel, das erscheint -->
<a href="#" class="menu-item">Bearbeiten</a>
<a href="#" class="menu-item">Kopieren</a>
<hr class="divider">
<a href="#" class="menu-item menu-item-danger">Löschen</a>
</div>
</div>
<div class="skeleton-card card">
<div class="skeleton-item skeleton-avatar"></div>
<div class="skeleton-content">
<div class="skeleton-item skeleton-line"></div>
<div class="skeleton-item skeleton-line" style="width: 70%;"></div>
</div>
</div>
<div class="stepper">
<div class="step step-complete"><div class="step-icon"></div><span>Profil</span></div>
<div class="step-connector"></div>
<div class="step step-active"><div class="step-icon">2</div><span>Konto</span></div>
<div class="step-connector"></div>
<div class="step"><div class="step-icon">3</div><span>Abschluss</span></div>
</div>
<div class="paginator">
<span class="paginator-info">1-10 von 100</span>
<div class="paginator-controls">
<button class="btn btn-icon">&lt;</button>
<button class="btn btn-icon">&gt;</button>
</div>
</div>
<ul class="tree">
<li class="tree-node">
<details open>
<summary class="tree-node-label">Ordner 1</summary>
<ul class="tree-node-children">
<li class="tree-node"><span class="tree-node-label">Datei 1.1</span></li>
<li class="tree-node"><span class="tree-node-label">Datei 1.2</span></li>
</ul>
</details>
</li>
</ul>
<div *ngIf="isLoading; else content">
<div class="skeleton-card">...</div>
</div>
<ng-template #content>
<!-- Echte Daten hier -->
</ng-template>
<button class="btn btn-icon btn-icon-danger" data-tooltip="Löschen" (click)="openDialog()">
<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"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
</button>
<!-- =================================================================== -->
<!-- DYNAMISCHE SNACKBAR (Standardmäßig unsichtbar) -->
<!-- =================================================================== -->
<div
class="snackbar"
*ngIf="isSnackbarVisible"
[@fadeSlideIn]> <!-- Angular Animation -->
<!-- Das Icon ist optional, wertet die Snackbar aber auf -->
<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">{{ snackbarMessage }}</span>
<button class="btn btn-icon snackbar-close-btn" (click)="closeSnackbar()" data-tooltip="Schließen">
&times;
</button>
</div>

View File

@@ -1,17 +1,47 @@
import { Component, Renderer2 } from '@angular/core'; import { Component, Renderer2 } from '@angular/core';
import { CommonModule } from '@angular/common';
import { trigger, state, style, transition, animate } from '@angular/animations';
@Component({ @Component({
selector: 'app-demo', selector: 'app-demo',
imports: [], imports: [CommonModule],
templateUrl: './demo.component.html', templateUrl: './demo.component.html',
styleUrl: './demo.component.css' styleUrl: './demo.component.css',
animations: [
trigger('fade', [
transition(':enter', [
style({ opacity: 0 }),
animate('300ms ease-out', style({ opacity: 1 }))
]),
transition(':leave', [
animate('300ms ease-in', style({ opacity: 0 }))
])
]),
trigger('slideIn', [
transition(':enter', [
style({ transform: 'translateY(20px)', opacity: 0 }),
animate('300ms ease-out', style({ transform: 'translateY(0)', opacity: 1 }))
]),
transition(':leave', [
animate('300ms ease-in', style({ transform: 'translateY(20px)', opacity: 0 }))
])
]),
trigger('fadeSlideIn', [
transition(':enter', [
style({ bottom: '-100px', opacity: 0 }),
animate('300ms ease-out', style({ bottom: '2rem', opacity: 1 }))
]),
transition(':leave', [
animate('300ms ease-in', style({ bottom: '-100px', opacity: 0 }))
])
])
]
}) })
export class DemoComponent { export class DemoComponent {
// 1. Eine Eigenschaft, die den aktuellen Zustand des Dark Mode speichert. // 1. Eine Eigenschaft, die den aktuellen Zustand des Dark Mode speichert.
// Sie ist an [checked]="isDarkMode" in der HTML gebunden. // Sie ist an [checked]="isDarkMode" in der HTML gebunden.
isDarkMode = false; isDarkMode = false;
isLoading = true;
// 2. Wir "injizieren" den Renderer2. Das ist der sichere Weg in Angular, // 2. Wir "injizieren" den Renderer2. Das ist der sichere Weg in Angular,
// um das DOM (z.B. den <body>-Tag) zu manipulieren. // um das DOM (z.B. den <body>-Tag) zu manipulieren.
constructor(private renderer: Renderer2) {} constructor(private renderer: Renderer2) {}
@@ -29,4 +59,55 @@ export class DemoComponent {
this.renderer.removeClass(document.body, 'dark-theme'); this.renderer.removeClass(document.body, 'dark-theme');
} }
} }
isDialogOpen = false;
// 2. Eine Methode, um den Dialog zu öffnen
openDialog(): void {
this.isDialogOpen = true;
// Optional: Verhindert das Scrollen der Seite im Hintergrund
this.renderer.addClass(document.body, 'no-scroll');
}
// 3. Eine Methode, um den Dialog zu schließen
closeDialog(): void {
this.isDialogOpen = false;
// Optional: Gibt das Scrollen der Seite wieder frei
this.renderer.removeClass(document.body, 'no-scroll');
}
isSnackbarVisible = false;
snackbarMessage = '';
private snackbarTimer: any; // Hier speichern wir den Timer
// 2. Eine Methode, um die Snackbar anzuzeigen
// Wir übergeben eine Nachricht, um sie wiederverwendbar zu machen
showSnackbar(message: string): void {
// Falls bereits eine Snackbar sichtbar ist, schließen wir sie zuerst
if (this.isSnackbarVisible) {
this.closeSnackbar();
// Kurze Verzögerung, damit die Animation sauber neustartet
setTimeout(() => this.displaySnackbar(message), 300);
} else {
this.displaySnackbar(message);
}
}
private displaySnackbar(message: string): void {
this.snackbarMessage = message;
this.isSnackbarVisible = true;
// Setze einen Timer, um die Snackbar nach 5 Sekunden automatisch zu schließen
this.snackbarTimer = setTimeout(() => {
this.closeSnackbar();
}, 5000); // 5000 Millisekunden = 5 Sekunden
}
// 3. Eine Methode, um die Snackbar zu schließen (manuell oder durch Timer)
closeSnackbar(): void {
// Lösche den Timer, falls der Benutzer manuell schließt
clearTimeout(this.snackbarTimer);
this.isSnackbarVisible = false;
}
} }

View File

@@ -4,11 +4,19 @@ import { DemoComponent } from './components/demo/demo.component';
export const DEMO_ROUTES: Routes = [ export const DEMO_ROUTES: Routes = [
{ {
// Diese Route ist relativ zu '/demo'.
// Ein leerer Pfad leitet direkt weiter zu '/demo/1'.
path: '', path: '',
redirectTo: '1',
pathMatch: 'full'
},
{
// Diese Route passt auf '/demo/1' und lädt die Komponente genau einmal.
path: '1',
component: DemoComponent, component: DemoComponent,
children: [ title: 'Demo'
{ path: '', redirectTo: '1', pathMatch: 'full' },
{ path: '1', component: DemoComponent, title: 'Demo' },
]
} }
// Hier könntest du weitere Routen wie '2', '3' etc. hinzufügen,
// die andere Komponenten laden.
// { path: '2', component: AnotherDemoComponent },
]; ];

View File

@@ -1,21 +1,9 @@
// src/app/material.ts // src/app/material.ts
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { MatToolbarModule } from '@angular/material/toolbar';
// ... füge hier alle Material-Module hinzu, die du häufig verwendest
export const MATERIAL_MODULES = [ export const MATERIAL_MODULES = [
CommonModule, CommonModule,
MatButtonModule,
MatCardModule,
MatIconModule,
MatTableModule,
MatTabsModule,
MatToolbarModule,
]; ];

View File

@@ -831,3 +831,448 @@ body.dark-theme .pill-info {
width: 100%; width: 100%;
} }
} }
/* =================================================================================
* 10. ERWEITERTE UI-KOMPONENTEN-BIBLIOTHEK (TEIL 2)
* ================================================================================= */
/* -------------------------------------------------- */
/* 10.1 DIALOG / MODAL
/* -------------------------------------------------- */
.dialog-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: grid;
place-items: center;
z-index: 1000;
opacity: 1; /* Für Animationen anpassen */
transition: opacity var(--transition-speed);
}
.dialog-container {
width: 100%;
max-width: 500px; /* Standardbreite für Dialoge */
display: flex;
flex-direction: column;
box-shadow: var(--box-shadow-md);
/* Animation: von leicht unten nach oben sliden */
transform: translateY(0);
transition: transform var(--transition-speed);
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--color-border);
}
.dialog-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0;
}
.dialog-content {
padding: 1.5rem;
line-height: 1.7;
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
padding: 1rem 1.5rem;
border-top: 1px solid var(--color-border);
background-color: var(--color-body-bg);
}
/* -------------------------------------------------- */
/* 10.2 SNACKBAR / TOAST
/* -------------------------------------------------- */
@keyframes snackbar-in {
from { transform: translate(-50%, 100px); opacity: 0; }
to { transform: translate(-50%, 0); opacity: 1; }
}
.snackbar {
position: fixed;
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
min-width: 300px;
max-width: 500px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
padding: 0.75rem 1.25rem;
border-radius: var(--border-radius-md);
background-color: #2c3e50; /* Typischerweise dunkel */
color: #fff;
box-shadow: var(--box-shadow-md);
z-index: 1050;
animation: snackbar-in 0.3s ease-out;
}
body.dark-theme .snackbar {
background-color: var(--color-surface);
color: var(--color-text);
border: 1px solid var(--color-border);
}
.snackbar-action {
color: var(--color-primary);
font-weight: 600;
}
body.dark-theme .snackbar-action {
color: #60a5fa; /* Helleres Blau für Dark Mode */
}
/* -------------------------------------------------- */
/* 10.3 MENU / DROPDOWN
/* -------------------------------------------------- */
.menu-container {
position: relative;
display: inline-block;
}
.menu-panel {
position: absolute;
top: calc(100% + 8px); /* Etwas Abstand zum Auslöser */
right: 0;
min-width: 200px;
padding: 0.5rem 0;
z-index: 900;
border: 1px solid var(--color-border);
box-shadow: var(--box-shadow-md);
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: opacity var(--transition-speed), transform var(--transition-speed);
}
/* Zustand, wenn das Menü offen ist (z.B. durch eine JS-Klasse) */
.menu-container.open .menu-panel {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.menu-item {
display: block;
padding: 0.6rem 1.25rem;
color: var(--color-text);
text-decoration: none;
font-size: 0.95rem;
transition: background-color 0.2s ease-out;
}
.menu-item:hover {
background-color: var(--color-body-bg);
}
.menu-item-danger {
color: var(--color-danger);
}
/* -------------------------------------------------- */
/* 10.4 SKELETON LOADER
/* -------------------------------------------------- */
@keyframes skeleton-shimmer {
0% { background-position: -200px 0; }
100% { background-position: calc(200px + 100%) 0; }
}
.skeleton-item {
background-color: var(--color-border);
background-image: linear-gradient(
90deg,
var(--color-border) 0px,
var(--color-body-bg) 40px,
var(--color-border) 80px
);
background-size: 200px 100%;
background-repeat: no-repeat;
animation: skeleton-shimmer 1.5s infinite;
}
.skeleton-card {
display: flex;
align-items: center;
gap: 1rem;
}
.skeleton-avatar {
width: 50px;
height: 50px;
border-radius: 50%;
}
.skeleton-content {
flex-grow: 1;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.skeleton-line {
width: 100%;
height: 1rem;
border-radius: var(--border-radius-sm);
}
/* -------------------------------------------------- */
/* 10.5 STEPPER
/* -------------------------------------------------- */
.stepper {
display: flex;
align-items: center;
width: 100%;
}
.step {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
color: var(--color-text-light);
font-weight: 500;
font-size: 0.9rem;
}
.step-icon {
width: 32px;
height: 32px;
border-radius: 50%;
border: 2px solid var(--color-border);
display: grid;
place-items: center;
font-weight: bold;
transition: all var(--transition-speed);
}
.step.step-active {
color: var(--color-primary);
}
.step.step-active .step-icon {
border-color: var(--color-primary);
color: var(--color-primary);
}
.step.step-complete {
color: var(--color-text);
}
.step.step-complete .step-icon {
background-color: var(--color-primary);
border-color: var(--color-primary);
color: #fff;
}
.step-connector {
flex-grow: 1;
height: 2px;
background-color: var(--color-border);
margin: 0 1rem;
transform: translateY(-12px); /* Auf Höhe der Icons ausrichten */
}
/* -------------------------------------------------- */
/* 10.6 PAGINATOR
/* -------------------------------------------------- */
.paginator {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 1.5rem;
padding: 1rem;
border-top: 1px solid var(--color-border);
}
.paginator-info {
font-size: 0.9rem;
color: var(--color-text-light);
font-weight: 500;
}
.paginator-controls {
display: flex;
gap: 0.5rem;
}
/* -------------------------------------------------- */
/* 10.7 TREE
/* -------------------------------------------------- */
.tree {
list-style: none;
padding-left: 1rem;
}
.tree-node {
position: relative;
}
.tree-node-label {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.5rem;
border-radius: var(--border-radius-sm);
cursor: pointer;
}
.tree-node-label:hover {
background-color: var(--color-body-bg);
}
.tree-node details > summary::-webkit-details-marker {
display: none; /* Icon entfernen */
}
.tree-node details > summary {
list-style: none;
}
.tree-node details > summary::before { /* Chevron-Icon */
content: '▶';
display: inline-block;
font-size: 0.7rem;
margin-right: 0.5rem;
transition: transform 0.2s ease-out;
}
.tree-node details[open] > summary::before {
transform: rotate(90deg);
}
.tree-node-children {
list-style: none;
padding-left: 1.75rem;
position: relative;
}
/* Verbindungs-Linie */
.tree-node-children::before {
content: '';
position: absolute;
left: 8px;
top: 0;
bottom: 0;
width: 1px;
background-color: var(--color-border);
}
/* =================================================================================
* 11. VERBESSERTES DIALOG-STYLING & HELFER-KLASSEN
* ================================================================================= */
/* Verhindert das Scrollen des Body, wenn ein Overlay aktiv ist */
body.no-scroll {
overflow: hidden;
}
/* Neuer Button-Typ für gefährliche Aktionen */
.btn-icon-danger:hover {
background-color: #fee2e2; /* Rötlicher Hover-Effekt */
color: var(--color-danger);
}
body.dark-theme .btn-icon-danger:hover {
background-color: #991b1b;
}
/* Verbessertes Dialog-Styling */
.dialog-backdrop {
/* ... bestehende Regeln (position: fixed, z-index etc.) bleiben gleich ... */
backdrop-filter: blur(4px); /* Moderner Blur-Effekt */
}
.dialog-container {
/* ... bestehende Regeln (max-width, display: flex etc.) bleiben gleich ... */
border-radius: var(--border-radius-md);
border: 1px solid var(--color-border);
}
/* Icon-Styling innerhalb des Dialogs */
.dialog-content {
text-align: center; /* Zentriert den Inhalt für einen besseren Look */
}
.dialog-icon-container {
width: 64px;
height: 64px;
margin: 0 auto 1rem auto;
border-radius: 50%;
display: grid;
place-items: center;
background-color: #fee2e2; /* Roter Hintergrund für das Icon */
color: var(--color-danger);
}
body.dark-theme .dialog-icon-container {
background-color: #7f1d1d;
}
.dialog-content p {
font-size: 1rem;
color: var(--color-text-light);
}
.dialog-content strong {
color: var(--color-text);
font-weight: 600;
}
/* =================================================================================
* 12. VERBESSERTES SNACKBAR-STYLING
* ================================================================================= */
.snackbar {
position: fixed;
bottom: 2rem;
left: 50%;
transform: translateX(-50%); /* Startposition für Animation */
min-width: 320px;
max-width: 500px;
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem; /* Etwas weniger Padding, da der Button eigenes hat */
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);
z-index: 1050;
}
/* Der @keyframes-Block wird nicht mehr benötigt, da wir Angular Animations nutzen */
.snackbar-icon-container {
flex-shrink: 0; /* Verhindert, dass das Icon schrumpft */
width: 32px;
height: 32px;
border-radius: 50%;
display: grid;
place-items: center;
background-color: #dcfce7; /* Success-Farbe */
color: #166534;
}
body.dark-theme .snackbar-icon-container {
background-color: #166534;
color: #dcfce7;
}
.snackbar-message {
flex-grow: 1; /* Nimmt den verfügbaren Platz ein */
font-weight: 500;
}
.snackbar-close-btn {
flex-shrink: 0;
width: 32px;
height: 32px;
font-size: 1.5rem; /* Größeres "x" */
color: var(--color-text-light);
}
.snackbar-close-btn:hover {
background-color: var(--color-body-bg);
}