styles
This commit is contained in:
@@ -83,10 +83,3 @@ form {
|
||||
.divider-or:not(:empty)::before { margin-right: 1em; }
|
||||
.divider-or:not(:empty)::after { margin-left: 1em; }
|
||||
|
||||
/* -- Autofill Fix -- */
|
||||
.form-input:-webkit-autofill ~ .form-label {
|
||||
top: 0;
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-primary);
|
||||
border-radius: var(--border-radius-sm);
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { RouterLink } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
standalone: true,
|
||||
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterLink,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { passwordMatchValidator } from '../../../../shared/validators/password-m
|
||||
|
||||
@Component({
|
||||
selector: 'app-reset-password',
|
||||
standalone: true,
|
||||
|
||||
imports: [CommonModule, ReactiveFormsModule],
|
||||
templateUrl: './reset-password.component.html',
|
||||
styleUrl: './reset-password.component.css'
|
||||
|
||||
@@ -3,7 +3,7 @@ import { RouterLink } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-verify-email',
|
||||
standalone: true,
|
||||
|
||||
imports: [RouterLink],
|
||||
templateUrl: './verify-email.component.html',
|
||||
styleUrl: './verify-email.component.css'
|
||||
|
||||
@@ -798,19 +798,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="snackbar-container-wrapper" [class.position-top]="snackbarPosition === 'top'">
|
||||
<div
|
||||
*ngFor="let snack of snackbars; let i = index"
|
||||
class="snackbar"
|
||||
[style]="getSnackbarStyle(i, snack.state)">
|
||||
|
||||
<div class="snackbar-icon-container">icon</div>
|
||||
<span class="snackbar-message">{{ snack.message }}</span>
|
||||
<button class="btn btn-icon snackbar-close-btn" (click)="closeSnackbar(snack.id)">
|
||||
×
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
@@ -5,11 +5,11 @@ import { trigger, transition, style, animate } from '@angular/animations';
|
||||
import { SnackbarService } from '../../../../shared/services/snackbar.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-demo',
|
||||
standalone: true, // Hinzugefügt für moderne, eigenständige Komponenten
|
||||
selector: 'app-demo1',
|
||||
// Hinzugefügt für moderne, eigenständige Komponenten
|
||||
imports: [CommonModule],
|
||||
templateUrl: './demo.component.html',
|
||||
styleUrl: './demo.component.css',
|
||||
templateUrl: './demo1.component.html',
|
||||
styleUrl: './demo1.component.css',
|
||||
animations: [
|
||||
trigger('fade', [
|
||||
transition(':enter', [
|
||||
@@ -44,7 +44,7 @@ import { SnackbarService } from '../../../../shared/services/snackbar.service';
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class DemoComponent implements OnInit {
|
||||
export class Demo1Component implements OnInit {
|
||||
// ===================================================================
|
||||
// 1. ZUSTANDS-EIGENSCHAFTEN DER KOMPONENTE
|
||||
// ===================================================================
|
||||
501
src/app/features/demo/components/demo2/demo2.component.html
Normal file
501
src/app/features/demo/components/demo2/demo2.component.html
Normal file
@@ -0,0 +1,501 @@
|
||||
<!-- =================================================================================
|
||||
FINALES & VOLLSTÄNDIGES DEMO-LAYOUT (VERSION 5.0)
|
||||
Aufgebaut aus wiederverwendbaren Komponenten
|
||||
================================================================================== -->
|
||||
<div class="dashboard-container">
|
||||
<!-- =================================================================== -->
|
||||
<!-- 1. SIDEBAR (Statisch, da sehr spezifisch für dieses Layout) -->
|
||||
<!-- =================================================================== -->
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header"><h1 class="sidebar-title">CustomDash</h1></div>
|
||||
<nav class="sidebar-nav">
|
||||
<a href="#" class="nav-item active"
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
|
||||
<polyline points="9 22 9 12 15 12 15 22"></polyline></svg
|
||||
><span>Übersicht</span></a
|
||||
>
|
||||
<a href="#" class="nav-item"
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<line x1="18" y1="20" x2="18" y2="10"></line>
|
||||
<line x1="12" y1="20" x2="12" y2="4"></line>
|
||||
<line x1="6" y1="20" x2="6" y2="14"></line></svg
|
||||
><span>Analysen</span></a
|
||||
>
|
||||
<a href="#" class="nav-item"
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"
|
||||
></path></svg
|
||||
><span>Berichte</span></a
|
||||
>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<a href="#" class="nav-item"
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M10.3 21.7c.9.9 2.5.9 3.4 0l6.9-6.9c.9-.9.9-2.5 0-3.4l-6.9-6.9c-.9-.9-2.5-.9-3.4 0l-6.9 6.9c-.9.9-.9-2.5 0 3.4l6.9 6.9z"
|
||||
></path></svg
|
||||
><span>Einstellungen</span></a
|
||||
>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- =================================================================== -->
|
||||
<!-- 2. HAUPTINHALT -->
|
||||
<!-- =================================================================== -->
|
||||
<main class="main-content">
|
||||
<app-page-header></app-page-header>
|
||||
|
||||
<div class="dashboard-grid">
|
||||
<!-- Sektion: KPI-Karten -->
|
||||
<app-kpi-card value="€ 14.750" label="Umsatz (Monat)" color="sales"
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<line x1="12" y1="1" x2="12" y2="23"></line>
|
||||
<path
|
||||
d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"
|
||||
></path></svg
|
||||
></app-kpi-card>
|
||||
<app-kpi-card value="1.284" label="Neue Nutzer" color="users"
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="9" cy="7" r="4"></circle>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg
|
||||
></app-kpi-card>
|
||||
<app-kpi-card value="312" label="Bestellungen" color="orders"
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"></path>
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<path d="M16 10a4 4 0 0 1-8 0"></path></svg
|
||||
></app-kpi-card>
|
||||
<app-kpi-card value="99.8%" label="Verfügbarkeit" color="performance"
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polygon
|
||||
points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"
|
||||
></polygon></svg
|
||||
></app-kpi-card>
|
||||
|
||||
<app-card>
|
||||
<h3 card-header>Letzte Bestellungen</h3>
|
||||
<div class="table-container">
|
||||
<table class="modern-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sortable">
|
||||
<span>Kunde</span
|
||||
><svg
|
||||
class="sort-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M8 3v18l8-9L8 3z"
|
||||
transform="rotate(90 12 12)"
|
||||
></path>
|
||||
</svg>
|
||||
</th>
|
||||
<th>Bestell-ID</th>
|
||||
<th>Status</th>
|
||||
<th class="text-right">Betrag</th>
|
||||
<th class="text-center">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="user-cell">
|
||||
<img
|
||||
src="https://i.pravatar.cc/40?u=max"
|
||||
alt="Avatar Max Mustermann"
|
||||
/>
|
||||
<div>
|
||||
<div class="user-name">Max Mustermann</div>
|
||||
<div class="user-email">
|
||||
max.mustermann@example.com
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><span class="mono">#10543</span></td>
|
||||
<td>
|
||||
<app-status-pill status="success"
|
||||
>Abgeschlossen</app-status-pill
|
||||
>
|
||||
</td>
|
||||
<td class="text-right amount">€ 129,99</td>
|
||||
<td class="actions-cell">
|
||||
<app-button color="icon" tooltip="Details anzeigen"
|
||||
><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="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"
|
||||
></path>
|
||||
<circle cx="12" cy="12" r="3"></circle></svg></app-button
|
||||
><app-button color="icon" tooltip="Bearbeiten"
|
||||
><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="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
|
||||
></path>
|
||||
<path
|
||||
d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"
|
||||
></path></svg></app-button
|
||||
><app-button
|
||||
color="icon-danger"
|
||||
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
|
||||
></app-button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Weitere Tabellenzeilen hier... -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<app-paginator
|
||||
[currentPage]="currentPage"
|
||||
[totalItems]="totalItems"
|
||||
[itemsPerPage]="itemsPerPage"
|
||||
(pageChange)="onPageChange($event)"
|
||||
></app-paginator>
|
||||
</app-card>
|
||||
|
||||
<!-- Sektion: Diagramm & Tabelle -->
|
||||
<app-card class="grid-col-span-2">
|
||||
<h3 card-header>Umsatzentwicklung</h3>
|
||||
<div class="chart-container">
|
||||
<div class="chart-bar" style="height: 60%"><span>Jan</span></div>
|
||||
<div class="chart-bar" style="height: 75%"><span>Feb</span></div>
|
||||
<div class="chart-bar" style="height: 50%"><span>Mär</span></div>
|
||||
<div class="chart-bar" style="height: 85%"><span>Apr</span></div>
|
||||
<div class="chart-bar" style="height: 90%"><span>Mai</span></div>
|
||||
<div class="chart-bar" style="height: 65%"><span>Jun</span></div>
|
||||
</div>
|
||||
</app-card>
|
||||
|
||||
|
||||
|
||||
<!-- UI Komponenten Showcase -->
|
||||
<app-card class="grid-col-span-2">
|
||||
<h3 card-header>Moderne Formular-Elemente</h3>
|
||||
<div class="component-grid">
|
||||
<app-form-field label="Name" type="text"></app-form-field>
|
||||
<div class="form-field">
|
||||
<select class="form-input" id="city">
|
||||
<option>Berlin</option>
|
||||
<option>München</option></select
|
||||
><label for="city" class="form-label">Stadt</label>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<textarea
|
||||
class="form-input"
|
||||
id="message"
|
||||
placeholder=" "
|
||||
rows="3"
|
||||
></textarea
|
||||
><label for="message" class="form-label">Ihre Nachricht</label>
|
||||
</div>
|
||||
<div class="form-group-inline">
|
||||
<label>Benachrichtigungen</label
|
||||
><app-slide-toggle></app-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
</app-card>
|
||||
|
||||
<app-card class="grid-col-span-2">
|
||||
<h3 card-header>Buttons, Chips & Interaktion</h3>
|
||||
<div class="component-grid">
|
||||
<div class="button-group">
|
||||
<app-button color="primary">Primary</app-button>
|
||||
<app-button color="secondary">Secondary</app-button>
|
||||
<app-button color="icon" tooltip="Favorit hinzufügen"
|
||||
><svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"
|
||||
></path></svg
|
||||
></app-button>
|
||||
</div>
|
||||
<div class="chip-set">
|
||||
<app-chip label="Technologie" [removable]="true"></app-chip>
|
||||
<app-chip
|
||||
label="Angular"
|
||||
[active]="true"
|
||||
[removable]="true"
|
||||
></app-chip>
|
||||
</div>
|
||||
<div class="badge-showcase">
|
||||
<div class="badge-container">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
|
||||
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg
|
||||
><span class="badge-dot">3</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</app-card>
|
||||
|
||||
<app-card class="grid-col-span-2">
|
||||
<h3 card-header>Indikatoren & Feedback</h3>
|
||||
<div class="card-body component-grid">
|
||||
<app-button color="secondary" (click)="triggerSnackbar()"
|
||||
>Snackbar anzeigen</app-button
|
||||
>
|
||||
<app-alert type="success"
|
||||
><strong>Erfolg!</strong> Ihre Änderungen wurden
|
||||
gespeichert.</app-alert
|
||||
>
|
||||
<app-alert type="danger"
|
||||
><strong>Fehler!</strong> Bitte füllen Sie alle Felder
|
||||
aus.</app-alert
|
||||
>
|
||||
</div>
|
||||
</app-card>
|
||||
|
||||
<app-card class="grid-col-span-2">
|
||||
<h3 card-header>Navigation & Layout</h3>
|
||||
<div class="component-grid">
|
||||
<div class="tab-group">
|
||||
<button class="tab-link active">Profil</button
|
||||
><button class="tab-link">Konto</button>
|
||||
</div>
|
||||
<div class="form-group-inline">
|
||||
<label>Lautstärke</label
|
||||
><input
|
||||
type="range"
|
||||
class="form-slider"
|
||||
min="0"
|
||||
max="100"
|
||||
value="50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<app-expansion-panel title="Weitere Details anzeigen">
|
||||
Dieser Inhalt wird dynamisch ein- und ausgeblendet. Lorem ipsum
|
||||
dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada
|
||||
lacus ex, sit amet blandit leo lobortis eget.
|
||||
</app-expansion-panel>
|
||||
|
||||
<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>
|
||||
</app-card>
|
||||
|
||||
<app-card class="grid-col-span-2">
|
||||
<h3 card-header>Ladezustände (Skeleton)</h3>
|
||||
<div *ngIf="isLoading" class="component-grid">
|
||||
<div class="skeleton-card card">
|
||||
<app-skeleton type="avatar"></app-skeleton>
|
||||
<div class="skeleton-content">
|
||||
<app-skeleton type="line"></app-skeleton
|
||||
><app-skeleton type="line" width="70%"></app-skeleton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="skeleton-card card">
|
||||
<app-skeleton type="avatar"></app-skeleton>
|
||||
<div class="skeleton-content">
|
||||
<app-skeleton type="line" width="50%"></app-skeleton
|
||||
><app-skeleton type="line" width="80%"></app-skeleton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body" *ngIf="!isLoading">
|
||||
<app-alert type="success"
|
||||
><strong>Inhalt erfolgreich geladen!</strong></app-alert
|
||||
>
|
||||
</div>
|
||||
</app-card>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 3. OVERLAYS -->
|
||||
<app-dialog
|
||||
[isOpen]="isDialogOpen"
|
||||
(close)="closeDialog()"
|
||||
title="Bestellung löschen?"
|
||||
>
|
||||
<div dialog-content>
|
||||
<div class="dialog-icon-container">
|
||||
<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? Diese Aktion kann nicht rückgängig gemacht werden.</p>
|
||||
</div>
|
||||
<div dialog-actions>
|
||||
<app-button color="stroked" (click)="closeDialog()">Abbrechen</app-button>
|
||||
<app-button color="primary" (click)="closeDialog()"
|
||||
>Ja, endgültig löschen</app-button
|
||||
>
|
||||
</div>
|
||||
</app-dialog>
|
||||
</div>
|
||||
83
src/app/features/demo/components/demo2/demo2.component.ts
Normal file
83
src/app/features/demo/components/demo2/demo2.component.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SnackbarService } from '../../../../shared/services/snackbar.service';
|
||||
|
||||
// Importieren ALLER wiederverwendbaren Komponenten, die im Template genutzt werden
|
||||
import { ButtonComponent } from '../../../../shared/components/ui/button/button.component';
|
||||
import { CardComponent } from '../../../../shared/components/ui/card/card.component';
|
||||
import { ChipComponent } from '../../../../shared/components/ui/chip/chip.component';
|
||||
import { FormFieldComponent } from '../../../../shared/components/form/form-field/form-field.component';
|
||||
import { KpiCardComponent } from '../../../../shared/components/data-display/kpi-card/kpi-card.component';
|
||||
import { PageHeaderComponent } from '../../../../shared/components/layout/page-header/page-header.component';
|
||||
import { SlideToggleComponent } from '../../../../shared/components/form/slide-toggle/slide-toggle.component';
|
||||
import { StatusPillComponent } from '../../../../shared/components/ui/status-pill/status-pill.component';
|
||||
import { DialogComponent } from '../../../../shared/components/overlays/dialog/dialog.component';
|
||||
import { MenuComponent } from '../../../../shared/components/overlays/menu/menu.component';
|
||||
import { AlertComponent } from '../../../../shared/components/ui/alert/alert.component';
|
||||
import { PaginatorComponent } from '../../../../shared/components/data-display/paginator/paginator.component';
|
||||
import { SkeletonComponent } from '../../../../shared/components/ui/skeleton/skeleton.component';
|
||||
import { ExpansionPanelComponent } from '../../../../shared/components/layout/expansion-panel/expansion-panel.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-demo2',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ButtonComponent,
|
||||
CardComponent,
|
||||
ChipComponent,
|
||||
FormFieldComponent,
|
||||
KpiCardComponent,
|
||||
PageHeaderComponent,
|
||||
SlideToggleComponent,
|
||||
StatusPillComponent,
|
||||
DialogComponent,
|
||||
MenuComponent,
|
||||
AlertComponent,
|
||||
PaginatorComponent,
|
||||
SkeletonComponent,
|
||||
ExpansionPanelComponent,
|
||||
],
|
||||
templateUrl: './demo2.component.html',
|
||||
styleUrl: './demo2.component.css',
|
||||
})
|
||||
export class Demo2Component implements OnInit {
|
||||
// Zustands-Variablen für diese Demo-Seite
|
||||
isLoading = true;
|
||||
isDialogOpen = false;
|
||||
isComponentMenuOpen = false;
|
||||
|
||||
// Eigenschaften für den Paginator
|
||||
currentPage = 1;
|
||||
totalItems = 123;
|
||||
itemsPerPage = 10;
|
||||
|
||||
constructor(private snackbarService: SnackbarService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
setTimeout(() => {
|
||||
this.isLoading = false;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Methoden für den Dialog
|
||||
openDialog(): void {
|
||||
this.isDialogOpen = true;
|
||||
}
|
||||
closeDialog(): void {
|
||||
this.isDialogOpen = false;
|
||||
}
|
||||
|
||||
// Methode für den Snackbar-Trigger
|
||||
triggerSnackbar(): void {
|
||||
const time = new Date().toLocaleTimeString();
|
||||
this.snackbarService.show('Neues Ereignis um ' + time);
|
||||
}
|
||||
|
||||
// Methode für den Paginator
|
||||
onPageChange(newPage: number): void {
|
||||
this.currentPage = newPage;
|
||||
console.log('Wechsle zu Seite:', newPage);
|
||||
// Hier würde die Logik zum Neuladen der Tabellendaten folgen
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { DemoComponent } from './components/demo/demo.component';
|
||||
import { Demo1Component } from './components/demo1/demo1.component';
|
||||
import { Demo2Component } from './components/demo2/demo2.component';
|
||||
|
||||
export const DEMO_ROUTES: Routes = [
|
||||
{
|
||||
@@ -8,15 +9,21 @@ export const DEMO_ROUTES: Routes = [
|
||||
// Ein leerer Pfad leitet direkt weiter zu '/demo/1'.
|
||||
path: '',
|
||||
redirectTo: '1',
|
||||
pathMatch: 'full'
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
// Diese Route passt auf '/demo/1' und lädt die Komponente genau einmal.
|
||||
path: '1',
|
||||
component: DemoComponent,
|
||||
title: 'Demo'
|
||||
}
|
||||
component: Demo1Component,
|
||||
title: 'Demo',
|
||||
},
|
||||
{
|
||||
// Diese Route passt auf '/demo/1' und lädt die Komponente genau einmal.
|
||||
path: '2',
|
||||
component: Demo2Component,
|
||||
title: 'Demo',
|
||||
},
|
||||
// Hier könntest du weitere Routen wie '2', '3' etc. hinzufügen,
|
||||
// die andere Komponenten laden.
|
||||
// { path: '2', component: AnotherDemoComponent },
|
||||
];
|
||||
];
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/* WICHTIG: Diese Stile sollten aus der globalen styles.css HIERHER VERSCHOBEN werden. */
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.kpi-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.kpi-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.kpi-icon ::ng-deep svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.icon-sales { background: linear-gradient(135deg, #2ecc71, #27ae60); }
|
||||
.icon-users { background: linear-gradient(135deg, #3498db, #2980b9); }
|
||||
.icon-orders { background: linear-gradient(135deg, #f39c12, #f1c40f); }
|
||||
.icon-performance { background: linear-gradient(135deg, #9b59b6, #8e44ad); }
|
||||
|
||||
.kpi-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.kpi-value {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.kpi-label {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
@@ -1 +1,15 @@
|
||||
<p>kpi-card works!</p>
|
||||
<div class="card kpi-card">
|
||||
<div
|
||||
class="kpi-icon"
|
||||
[class.icon-sales]="color === 'sales'"
|
||||
[class.icon-users]="color === 'users'"
|
||||
[class.icon-orders]="color === 'orders'"
|
||||
[class.icon-performance]="color === 'performance'">
|
||||
<!-- ng-content erlaubt es, ein spezifisches SVG-Icon von außen einzufügen -->
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<div class="kpi-content">
|
||||
<span class="kpi-value">{{ value }}</span>
|
||||
<span class="kpi-label">{{ label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,11 +1,17 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
type KpiColor = 'sales' | 'users' | 'orders' | 'performance';
|
||||
|
||||
@Component({
|
||||
selector: 'app-kpi-card',
|
||||
imports: [],
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './kpi-card.component.html',
|
||||
styleUrl: './kpi-card.component.css'
|
||||
})
|
||||
export class KpiCardComponent {
|
||||
|
||||
}
|
||||
@Input() value: string = '';
|
||||
@Input() label: string = '';
|
||||
@Input() color: KpiColor = 'users';
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/* Verschieben Sie diese Stile aus der globalen styles.css hierher */
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<div class="paginator">
|
||||
<span class="paginator-info">
|
||||
{{ rangeStart }} - {{ rangeEnd }} von {{ totalItems }}
|
||||
</span>
|
||||
<div class="paginator-controls">
|
||||
<app-button
|
||||
color="icon"
|
||||
(click)="goToPrevious()"
|
||||
[disabled]="currentPage === 1"
|
||||
tooltip="Vorherige Seite">
|
||||
<
|
||||
</app-button>
|
||||
<app-button
|
||||
color="icon"
|
||||
(click)="goToNext()"
|
||||
[disabled]="currentPage === totalPages"
|
||||
tooltip="Nächste Seite">
|
||||
>
|
||||
</app-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ButtonComponent } from '../../ui/button/button.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-paginator',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ButtonComponent],
|
||||
templateUrl: './paginator.component.html',
|
||||
styleUrl: './paginator.component.css'
|
||||
})
|
||||
export class PaginatorComponent {
|
||||
// --- EINGABEN von außen ---
|
||||
@Input() currentPage = 1;
|
||||
@Input() totalItems = 0;
|
||||
@Input() itemsPerPage = 10;
|
||||
|
||||
// --- AUSGABE nach außen ---
|
||||
@Output() pageChange = new EventEmitter<number>();
|
||||
|
||||
// --- Interne Logik ---
|
||||
get totalPages(): number {
|
||||
return Math.ceil(this.totalItems / this.itemsPerPage);
|
||||
}
|
||||
|
||||
get rangeStart(): number {
|
||||
return (this.currentPage - 1) * this.itemsPerPage + 1;
|
||||
}
|
||||
|
||||
get rangeEnd(): number {
|
||||
const end = this.currentPage * this.itemsPerPage;
|
||||
return end > this.totalItems ? this.totalItems : end;
|
||||
}
|
||||
|
||||
goToPrevious(): void {
|
||||
if (this.currentPage > 1) {
|
||||
this.pageChange.emit(this.currentPage - 1);
|
||||
}
|
||||
}
|
||||
|
||||
goToNext(): void {
|
||||
if (this.currentPage < this.totalPages) {
|
||||
this.pageChange.emit(this.currentPage + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +1,12 @@
|
||||
<p>form-field works!</p>
|
||||
<div class="form-field">
|
||||
<input
|
||||
[type]="type"
|
||||
class="form-input"
|
||||
[id]="label"
|
||||
placeholder=" "
|
||||
[(ngModel)]="value"
|
||||
(ngModelChange)="onChange($event)"
|
||||
(blur)="onTouched()"
|
||||
[disabled]="disabled">
|
||||
<label [for]="label" class="form-label">{{ label }}</label>
|
||||
</div>
|
||||
@@ -1,11 +1,37 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, Input, forwardRef } from '@angular/core';
|
||||
// ControlValueAccessor und NG_VALUE_ACCESSOR sind Typen, kein Modul
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms'; // <-- FormsModule hier importieren
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-form-field',
|
||||
imports: [],
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule
|
||||
],
|
||||
templateUrl: './form-field.component.html',
|
||||
styleUrl: './form-field.component.css'
|
||||
styleUrl: './form-field.component.css',
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => FormFieldComponent),
|
||||
multi: true
|
||||
}
|
||||
]
|
||||
})
|
||||
export class FormFieldComponent {
|
||||
export class FormFieldComponent implements ControlValueAccessor {
|
||||
@Input() label: string = '';
|
||||
@Input() type: 'text' | 'email' | 'password' = 'text';
|
||||
|
||||
// Interne Logik für ControlValueAccessor
|
||||
value: string = '';
|
||||
onChange: (value: any) => void = () => {};
|
||||
onTouched: () => void = () => {};
|
||||
disabled = false;
|
||||
|
||||
}
|
||||
writeValue(value: any): void { this.value = value; }
|
||||
registerOnChange(fn: any): void { this.onChange = fn; }
|
||||
registerOnTouched(fn: any): void { this.onTouched = fn; }
|
||||
setDisabledState?(isDisabled: boolean): void { this.disabled = isDisabled; }
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/* Autarke Komponente. Verschieben Sie die .slide-toggle-*-Klassen aus styles.css hierher. */
|
||||
.slide-toggle {
|
||||
display: inline-block;
|
||||
}
|
||||
.slide-toggle-input {
|
||||
display: none;
|
||||
}
|
||||
.slide-toggle-label {
|
||||
display: block;
|
||||
width: 44px;
|
||||
height: 24px;
|
||||
background-color: var(--color-border);
|
||||
border-radius: 12px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: background-color var(--transition-speed);
|
||||
}
|
||||
.slide-toggle-label::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background-color: #fff;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
transition: transform var(--transition-speed);
|
||||
}
|
||||
.slide-toggle-input:checked + .slide-toggle-label {
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
.slide-toggle-input:checked + .slide-toggle-label::before {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
.slide-toggle-input:disabled + .slide-toggle-label {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
@@ -1 +1,10 @@
|
||||
<p>slide-toggle works!</p>
|
||||
<div class="slide-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="slide-toggle-input"
|
||||
[id]="id"
|
||||
[checked]="value"
|
||||
[disabled]="disabled"
|
||||
(change)="onToggle()">
|
||||
<label [for]="id" class="slide-toggle-label"></label>
|
||||
</div>
|
||||
@@ -1,11 +1,48 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, Input, forwardRef } from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-slide-toggle',
|
||||
imports: [],
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule
|
||||
],
|
||||
templateUrl: './slide-toggle.component.html',
|
||||
styleUrl: './slide-toggle.component.css'
|
||||
styleUrl: './slide-toggle.component.css',
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => SlideToggleComponent),
|
||||
multi: true
|
||||
}
|
||||
]
|
||||
})
|
||||
export class SlideToggleComponent {
|
||||
export class SlideToggleComponent implements ControlValueAccessor {
|
||||
@Input() id = `slide-toggle-${Math.random().toString(36).substring(2)}`;
|
||||
|
||||
// Interne Logik für ControlValueAccessor
|
||||
value = false;
|
||||
onChange: (value: any) => void = () => {};
|
||||
onTouched: () => void = () => {};
|
||||
disabled = false;
|
||||
|
||||
}
|
||||
writeValue(value: any): void { this.value = !!value; }
|
||||
registerOnChange(fn: any): void { this.onChange = fn; }
|
||||
registerOnTouched(fn: any): void { this.onTouched = fn; }
|
||||
setDisabledState?(isDisabled: boolean): void { this.disabled = isDisabled; }
|
||||
|
||||
// +++ HIER IST DIE FEHLENDE METHODE +++
|
||||
/**
|
||||
* Wird aufgerufen, wenn der Benutzer auf den Schalter klickt.
|
||||
* Aktualisiert den internen Wert und benachrichtigt Angular Forms.
|
||||
*/
|
||||
onToggle(): void {
|
||||
if (!this.disabled) {
|
||||
this.value = !this.value;
|
||||
this.onChange(this.value);
|
||||
this.onTouched();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
.expansion-panel {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-md);
|
||||
transition: box-shadow var(--transition-speed);
|
||||
}
|
||||
.expansion-panel.is-open {
|
||||
box-shadow: var(--box-shadow-sm);
|
||||
}
|
||||
.expansion-panel-header {
|
||||
background: none;
|
||||
border: none;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0.75rem 1.25rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
.expansion-panel-icon {
|
||||
transition: transform 0.3s ease-out;
|
||||
}
|
||||
.expansion-panel.is-open .expansion-panel-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.expansion-panel-content-wrapper {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.35s ease-in-out;
|
||||
}
|
||||
.expansion-panel.is-open .expansion-panel-content-wrapper {
|
||||
max-height: 200px;
|
||||
}
|
||||
.expansion-panel-content {
|
||||
padding: 0 1.25rem 1.25rem 1.25rem;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<div class="expansion-panel" [class.is-open]="isOpen">
|
||||
<button type="button" class="expansion-panel-header" (click)="togglePanel()">
|
||||
<span>{{ title }}</span>
|
||||
<svg class="expansion-panel-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
|
||||
</button>
|
||||
<div class="expansion-panel-content-wrapper">
|
||||
<div class="expansion-panel-content">
|
||||
<!-- Der Inhalt des Panels wird von außen über ng-content eingefügt -->
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-expansion-panel',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './expansion-panel.component.html',
|
||||
styleUrl: './expansion-panel.component.css'
|
||||
})
|
||||
export class ExpansionPanelComponent {
|
||||
@Input() title: string = 'Details anzeigen'; // Titel für den Header
|
||||
isOpen = false; // Interner Zustand, ob das Panel geöffnet ist
|
||||
|
||||
togglePanel(): void {
|
||||
this.isOpen = !this.isOpen;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/* Verschieben Sie diese Stile aus der globalen styles.css hierher */
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.main-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
/* Responsivität für den Header */
|
||||
@media (max-width: 768px) {
|
||||
.main-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
@@ -1 +1,14 @@
|
||||
<p>page-header works!</p>
|
||||
<header class="main-header">
|
||||
|
||||
<!-- Linker Bereich: Hier könnte später ein Titel oder Breadcrumbs via ng-content platziert werden -->
|
||||
<div class="header-left">
|
||||
<app-search-bar></app-search-bar>
|
||||
</div>
|
||||
|
||||
<!-- Rechter Bereich mit den Aktionen -->
|
||||
<div class="header-actions">
|
||||
<app-theme-switcher></app-theme-switcher>
|
||||
<app-user-profile></app-user-profile>
|
||||
</div>
|
||||
|
||||
</header>
|
||||
@@ -1,11 +1,21 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SearchBarComponent } from '../search-bar/search-bar.component';
|
||||
import { ThemeSwitcherComponent } from '../theme-switcher/theme-switcher.component';
|
||||
import { UserProfileComponent } from '../user-profile/user-profile.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-page-header',
|
||||
imports: [],
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
SearchBarComponent,
|
||||
ThemeSwitcherComponent,
|
||||
UserProfileComponent
|
||||
],
|
||||
templateUrl: './page-header.component.html',
|
||||
styleUrl: './page-header.component.css'
|
||||
})
|
||||
export class PageHeaderComponent {
|
||||
|
||||
}
|
||||
// Hier könnten später z.B. der Seitentitel oder Breadcrumbs übergeben werden
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/* Verschieben Sie diese Stile aus styles.css hierher */
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
position: relative;
|
||||
width: 300px;
|
||||
}
|
||||
.search-bar svg {
|
||||
position: absolute;
|
||||
left: 1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--color-text-light);
|
||||
pointer-events: none; /* Verhindert, dass das Icon Klicks abfängt */
|
||||
}
|
||||
.search-bar input {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem 0.75rem 3rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-md);
|
||||
background-color: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
font-size: 1rem;
|
||||
transition: all var(--transition-speed);
|
||||
}
|
||||
.search-bar input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.3);
|
||||
}
|
||||
@@ -1 +1,7 @@
|
||||
<p>search-bar works!</p>
|
||||
<div class="search-bar">
|
||||
<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">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
<input type="text" placeholder="Dashboard durchsuchen..." (input)="onSearch($event)" />
|
||||
</div>
|
||||
@@ -1,11 +1,18 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, Output, EventEmitter } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-search-bar',
|
||||
standalone: true,
|
||||
imports: [],
|
||||
templateUrl: './search-bar.component.html',
|
||||
styleUrl: './search-bar.component.css'
|
||||
})
|
||||
export class SearchBarComponent {
|
||||
// Gibt den Suchbegriff aus, wenn der Benutzer tippt (mit debounce) oder Enter drückt
|
||||
@Output() search = new EventEmitter<string>();
|
||||
|
||||
}
|
||||
onSearch(event: Event): void {
|
||||
const input = event.target as HTMLInputElement;
|
||||
this.search.emit(input.value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.theme-switcher {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
color: var(--color-text-light);
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class="theme-switcher">
|
||||
<label>Dark Mode</label>
|
||||
|
||||
<app-slide-toggle [(ngModel)]="isDarkMode" (ngModelChange)="toggleTheme($event)"></app-slide-toggle>
|
||||
</div>
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Component, Renderer2, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms'; // FormsModule importieren
|
||||
import { SlideToggleComponent } from '../../form/slide-toggle/slide-toggle.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-theme-switcher',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
SlideToggleComponent,
|
||||
FormsModule // <-- HIER IST DIE KORREKTUR
|
||||
],
|
||||
templateUrl: './theme-switcher.component.html',
|
||||
styleUrl: './theme-switcher.component.css'
|
||||
})
|
||||
export class ThemeSwitcherComponent implements OnInit {
|
||||
isDarkMode = false;
|
||||
|
||||
constructor(private renderer: Renderer2) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Hier könnte man den Zustand aus dem localStorage laden
|
||||
}
|
||||
|
||||
// Die Methode akzeptiert jetzt direkt den neuen boolean-Wert
|
||||
toggleTheme(isDarkMode: boolean): void {
|
||||
this.isDarkMode = isDarkMode;
|
||||
if (this.isDarkMode) {
|
||||
this.renderer.addClass(document.body, 'dark-theme');
|
||||
} else {
|
||||
this.renderer.removeClass(document.body, 'dark-theme');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/* Verschieben Sie diese Stile aus styles.css hierher */
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.user-profile img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
cursor: pointer;
|
||||
border: 2px solid var(--color-border);
|
||||
transition: border-color var(--transition-speed);
|
||||
}
|
||||
|
||||
.user-profile img:hover {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
<p>user-profile works!</p>
|
||||
<div class="user-profile">
|
||||
<img [src]="avatarUrl" alt="User Avatar" />
|
||||
</div>
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, Input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-profile',
|
||||
standalone: true,
|
||||
imports: [],
|
||||
templateUrl: './user-profile.component.html',
|
||||
styleUrl: './user-profile.component.css'
|
||||
})
|
||||
export class UserProfileComponent {
|
||||
|
||||
}
|
||||
// Später könnte hier ein User-Objekt übergeben werden
|
||||
@Input() avatarUrl = 'https://i.pravatar.cc/40';
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/* Verschieben Sie diese Stile aus styles.css hierher */
|
||||
:host { display: contents; }
|
||||
|
||||
.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;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
.dialog-container {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: var(--box-shadow-md);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.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);
|
||||
}
|
||||
|
||||
/* Style-Anpassungen für projizierten Inhalt */
|
||||
::ng-deep [dialog-content] {
|
||||
line-height: 1.7;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -1 +1,20 @@
|
||||
<p>dialog works!</p>
|
||||
<div class="dialog-backdrop" *ngIf="isOpen" (click)="onClose()" @fade>
|
||||
<div class="dialog-container card" (click)="$event.stopPropagation()" @slideIn>
|
||||
|
||||
<div class="dialog-header">
|
||||
<h3 class="dialog-title">{{ title }}</h3>
|
||||
<app-button color="icon" (click)="onClose()" tooltip="Schließen">×</app-button>
|
||||
</div>
|
||||
|
||||
<div class="dialog-content">
|
||||
<!-- Der Hauptinhalt des Dialogs wird von außen projiziert -->
|
||||
<ng-content select="[dialog-content]"></ng-content>
|
||||
</div>
|
||||
|
||||
<div class="dialog-actions">
|
||||
<!-- Die Aktions-Buttons werden ebenfalls von außen projiziert -->
|
||||
<ng-content select="[dialog-actions]"></ng-content>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,11 +1,39 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { trigger, transition, style, animate } from '@angular/animations';
|
||||
import { ButtonComponent } from '../../ui/button/button.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dialog',
|
||||
imports: [],
|
||||
standalone: true,
|
||||
imports: [CommonModule, ButtonComponent],
|
||||
templateUrl: './dialog.component.html',
|
||||
styleUrl: './dialog.component.css'
|
||||
styleUrl: './dialog.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) scale(0.95)', opacity: 0 }), animate('300ms ease-out', style({ transform: 'translateY(0) scale(1)', opacity: 1 }))]),
|
||||
transition(':leave', [animate('300ms ease-in', style({ transform: 'translateY(20px) scale(0.95)', opacity: 0 }))])
|
||||
])
|
||||
]
|
||||
})
|
||||
export class DialogComponent {
|
||||
@Input() title = 'Aktion bestätigen';
|
||||
@Input() isOpen = false;
|
||||
|
||||
// Events, um die Aktionen nach außen zu kommunizieren
|
||||
@Output() close = new EventEmitter<void>();
|
||||
@Output() confirm = new EventEmitter<void>();
|
||||
|
||||
}
|
||||
onClose() {
|
||||
this.close.emit();
|
||||
}
|
||||
|
||||
onConfirm() {
|
||||
this.confirm.emit();
|
||||
this.onClose(); // Schließt den Dialog nach der Bestätigung
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/* Verschieben Sie diese Stile aus styles.css hierher */
|
||||
:host {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.menu-panel {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
min-width: 200px;
|
||||
padding: 0.5rem 0;
|
||||
z-index: 900;
|
||||
border: 1px solid var(--color-border);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-10px);
|
||||
transition: opacity var(--transition-speed), transform var(--transition-speed);
|
||||
}
|
||||
|
||||
:host-context(.open) .menu-panel {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Wir verwenden ::ng-deep, um die Stile auf die von außen projizierten Elemente anzuwenden */
|
||||
::ng-deep .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;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::ng-deep .menu-item:hover {
|
||||
background-color: var(--color-body-bg);
|
||||
}
|
||||
|
||||
::ng-deep .menu-item-danger {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
@@ -1 +1,11 @@
|
||||
<p>menu works!</p>
|
||||
<div class="menu-container" [class.open]="isOpen">
|
||||
<!-- Hier wird der Button, der das Menü auslöst, von außen eingefügt -->
|
||||
<div (click)="toggle()">
|
||||
<ng-content select="[menu-trigger]"></ng-content>
|
||||
</div>
|
||||
|
||||
<div class="menu-panel card">
|
||||
<!-- Hier werden die Menü-Einträge von außen eingefügt -->
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,11 +1,18 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-menu',
|
||||
imports: [],
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './menu.component.html',
|
||||
styleUrl: './menu.component.css'
|
||||
})
|
||||
export class MenuComponent {
|
||||
@Input() triggerText = 'Menü öffnen';
|
||||
isOpen = false;
|
||||
|
||||
}
|
||||
toggle() {
|
||||
this.isOpen = !this.isOpen;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ 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'
|
||||
@@ -26,8 +26,8 @@ export class SnackbarContainerComponent {
|
||||
const verticalOffset = index * snackbarHeight;
|
||||
const positionProperty = position === 'bottom' ? 'bottom' : 'top';
|
||||
|
||||
const opacity = 1 - (index * 0.2);
|
||||
const scale = 1 - (index * 0.02);
|
||||
const opacity = 1 - (index * 0.1);
|
||||
const scale = 1 - (index * 0.01);
|
||||
|
||||
const visibleStyle = {
|
||||
[positionProperty]: `${verticalOffset}px`,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-snackbar',
|
||||
standalone: true,
|
||||
|
||||
imports: [],
|
||||
templateUrl: './snackbar.component.html',
|
||||
styleUrl: './snackbar.component.css'
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/* WICHTIG: Verschieben Sie die .alert-Klassen aus Ihrer globalen
|
||||
styles.css hierher und löschen Sie sie anschließend dort. */
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 1rem;
|
||||
border-radius: var(--border-radius-md);
|
||||
border: 1px solid transparent;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Success */
|
||||
.alert-success {
|
||||
background-color: #d1fae5;
|
||||
color: #065f46;
|
||||
border-color: #6ee7b7;
|
||||
}
|
||||
:host-context(body.dark-theme) .alert-success {
|
||||
background-color: #064e3b;
|
||||
color: #a7f3d0;
|
||||
border-color: #34d399;
|
||||
}
|
||||
|
||||
/* Danger */
|
||||
.alert-danger {
|
||||
background-color: #fee2e2;
|
||||
color: #991b1b;
|
||||
border-color: #fca5a5;
|
||||
}
|
||||
:host-context(body.dark-theme) .alert-danger {
|
||||
background-color: #7f1d1d;
|
||||
color: #fecaca;
|
||||
border-color: #f87171;
|
||||
}
|
||||
|
||||
/* Info */
|
||||
.alert-info {
|
||||
background-color: #dbeafe;
|
||||
color: #1e40af;
|
||||
border-color: #93c5fd;
|
||||
}
|
||||
:host-context(body.dark-theme) .alert-info {
|
||||
background-color: #1e3a8a;
|
||||
color: #bfdbfe;
|
||||
border-color: #60a5fa;
|
||||
}
|
||||
|
||||
/* Warning */
|
||||
.alert-warning {
|
||||
background-color: #fef9c3;
|
||||
color: #854d0e;
|
||||
border-color: #fde047;
|
||||
}
|
||||
:host-context(body.dark-theme) .alert-warning {
|
||||
background-color: #713f12;
|
||||
color: #fef08a;
|
||||
border-color: #facc15;
|
||||
}
|
||||
@@ -1 +1,11 @@
|
||||
<p>alert works!</p>
|
||||
<div
|
||||
class="alert"
|
||||
[ngClass]="{
|
||||
'alert-success': type === 'success',
|
||||
'alert-danger': type === 'danger',
|
||||
'alert-info': type === 'info',
|
||||
'alert-warning': type === 'warning'
|
||||
}">
|
||||
<!-- Der Inhalt des Alerts wird von außen über ng-content eingefügt -->
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
@@ -1,11 +1,16 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
// Definiert die erlaubten Alert-Typen für Typsicherheit
|
||||
type AlertType = 'success' | 'danger' | 'info' | 'warning';
|
||||
|
||||
@Component({
|
||||
selector: 'app-alert',
|
||||
imports: [],
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './alert.component.html',
|
||||
styleUrl: './alert.component.css'
|
||||
})
|
||||
export class AlertComponent {
|
||||
|
||||
}
|
||||
@Input() type: AlertType = 'info'; // 'info' ist ein guter Standardwert
|
||||
}
|
||||
@@ -1 +1,16 @@
|
||||
<p>button works!</p>
|
||||
<button
|
||||
[type]="type"
|
||||
[disabled]="disabled"
|
||||
class="btn"
|
||||
[class.btn-primary]="color === 'primary'"
|
||||
[class.btn-secondary]="color === 'secondary'"
|
||||
[class.btn-stroked]="color === 'stroked'"
|
||||
[class.btn-flat]="color === 'flat'"
|
||||
[class.btn-icon]="color === 'icon' || color === 'icon-danger'"
|
||||
[class.btn-icon-danger]="color === 'icon-danger'"
|
||||
[class.btn-full-width]="fullWidth"
|
||||
[attr.data-tooltip]="tooltip">
|
||||
|
||||
<!-- ng-content erlaubt es, Text oder SVGs von außen in den Button einzufügen -->
|
||||
<ng-content></ng-content>
|
||||
</button>
|
||||
@@ -1,11 +1,22 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
// Definiert die erlaubten Varianten für den Button
|
||||
type ButtonColor = 'primary' | 'secondary' | 'stroked' | 'flat' | 'icon' | 'icon-danger';
|
||||
|
||||
@Component({
|
||||
selector: 'app-button',
|
||||
imports: [],
|
||||
|
||||
imports: [CommonModule],
|
||||
templateUrl: './button.component.html',
|
||||
styleUrl: './button.component.css'
|
||||
})
|
||||
export class ButtonComponent {
|
||||
@Input() color: ButtonColor = 'primary';
|
||||
@Input() type: 'button' | 'submit' = 'button';
|
||||
@Input() disabled = false;
|
||||
@Input() fullWidth = false;
|
||||
|
||||
}
|
||||
// Ein spezieller Input für Tooltips
|
||||
@Input() tooltip: string | null = null;
|
||||
}
|
||||
@@ -1 +1,12 @@
|
||||
<p>card works!</p>
|
||||
<section class="card">
|
||||
<!-- Der Header wird nur gerendert, wenn Inhalt dafür bereitgestellt wird -->
|
||||
<div class="card-header" *ngIf="hasHeader">
|
||||
<!-- Hier wird der Header-Inhalt projiziert -->
|
||||
<ng-content select="[card-header]"></ng-content>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<!-- Hier wird der restliche Inhalt projiziert -->
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,11 +1,22 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, ContentChild, ElementRef, AfterContentInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-card',
|
||||
imports: [],
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './card.component.html',
|
||||
styleUrl: './card.component.css'
|
||||
})
|
||||
export class CardComponent {
|
||||
export class CardComponent implements AfterContentInit {
|
||||
// Wir prüfen, ob der Benutzer einen Header-Inhalt bereitgestellt hat.
|
||||
hasHeader = false;
|
||||
|
||||
}
|
||||
// Sucht nach einem Element, das mit dem Attribut [card-header] markiert ist.
|
||||
@ContentChild('header') headerContent: ElementRef | undefined;
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
// Wenn ein Header-Element gefunden wurde, setzen wir die Eigenschaft auf true.
|
||||
this.hasHeader = !!this.headerContent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/* Diese Komponente ist autark und benötigt KEINE globalen Klassen mehr.
|
||||
Sie können die .chip-Klassen aus styles.css hierher verschieben und dort löschen. */
|
||||
:host {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 1rem;
|
||||
background-color: var(--color-border);
|
||||
color: var(--color-text);
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-speed);
|
||||
}
|
||||
|
||||
.chip:hover {
|
||||
background-color: #d1d5db;
|
||||
}
|
||||
|
||||
:host-context(body.dark-theme) .chip:hover {
|
||||
background-color: #5a6578;
|
||||
}
|
||||
|
||||
.chip.chip-active {
|
||||
background-color: var(--color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.chip-remove {
|
||||
margin-left: 0.5rem;
|
||||
font-weight: bold;
|
||||
opacity: 0.5;
|
||||
transition: opacity var(--transition-speed);
|
||||
}
|
||||
|
||||
.chip-remove:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -1 +1,6 @@
|
||||
<p>chip works!</p>
|
||||
<!-- [class.chip-active] wendet die aktive Klasse nur an, wenn der Input 'active' true ist -->
|
||||
<span class="chip" [class.chip-active]="active">
|
||||
{{ label }}
|
||||
<!-- Der "Entfernen"-Button wird nur angezeigt, wenn 'removable' true ist -->
|
||||
<span *ngIf="removable" class="chip-remove" (click)="onRemove($event)">×</span>
|
||||
</span>
|
||||
@@ -1,11 +1,28 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chip',
|
||||
imports: [],
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './chip.component.html',
|
||||
styleUrl: './chip.component.css'
|
||||
})
|
||||
export class ChipComponent {
|
||||
// Der Text, der im Chip angezeigt wird
|
||||
@Input() label: string = '';
|
||||
// Ob der Chip als "aktiv" oder "ausgewählt" angezeigt werden soll
|
||||
@Input() active = false;
|
||||
// Ob ein "Entfernen"-Button angezeigt werden soll
|
||||
@Input() removable = false;
|
||||
|
||||
}
|
||||
// Event, das ausgelöst wird, wenn auf den "Entfernen"-Button geklickt wird
|
||||
@Output() remove = new EventEmitter<void>();
|
||||
|
||||
onRemove(event: MouseEvent) {
|
||||
// Verhindert, dass ein Klick auf den "Entfernen"-Button
|
||||
// auch ein Klick-Event auf dem gesamten Chip auslöst.
|
||||
event.stopPropagation();
|
||||
this.remove.emit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/* Globale Stile für alle per <app-icon> geladenen SVGs */
|
||||
:host {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1em; /* Passt sich an die Schriftgröße des Elternelements an */
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
/* Wir verwenden ::ng-deep, da das SVG-Element dynamisch eingefügt wird */
|
||||
::ng-deep .icon-svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<p>icon works!</p>
|
||||
|
||||
@@ -1,11 +1,35 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, Input, OnChanges, SimpleChanges, ElementRef, Renderer2 } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { HttpClient, HttpClientModule } from '@angular/common/http';
|
||||
|
||||
@Component({
|
||||
selector: 'app-icon',
|
||||
imports: [],
|
||||
templateUrl: './icon.component.html',
|
||||
standalone: true,
|
||||
imports: [CommonModule, HttpClientModule],
|
||||
template: '<ng-content></ng-content>', // Leeres Template, da wir das SVG direkt einfügen
|
||||
styleUrl: './icon.component.css'
|
||||
})
|
||||
export class IconComponent {
|
||||
export class IconComponent implements OnChanges {
|
||||
@Input() name: string = '';
|
||||
|
||||
}
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private el: ElementRef,
|
||||
private renderer: Renderer2
|
||||
) {}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['name'] && this.name) {
|
||||
// Lädt das SVG aus dem assets-Ordner und fügt es in die Komponente ein
|
||||
this.http.get(`assets/icons/${this.name}.svg`, { responseType: 'text' })
|
||||
.subscribe(svg => {
|
||||
this.el.nativeElement.innerHTML = svg;
|
||||
// Optional: Fügt dem <svg>-Element selbst eine Klasse hinzu
|
||||
const svgElement = this.el.nativeElement.querySelector('svg');
|
||||
if (svgElement) {
|
||||
this.renderer.addClass(svgElement, 'icon-svg');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/app/shared/components/ui/skeleton/skeleton.component.css
Normal file
26
src/app/shared/components/ui/skeleton/skeleton.component.css
Normal file
@@ -0,0 +1,26 @@
|
||||
/* Verschieben Sie die Skeleton-Stile hierher */
|
||||
@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-line {
|
||||
border-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
.skeleton-avatar {
|
||||
border-radius: 50%;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<div
|
||||
class="skeleton-item"
|
||||
[ngClass]="'skeleton-' + type"
|
||||
[style.width]="type === 'line' ? width : '50px'"
|
||||
[style.height]="type === 'line' ? height : '50px'">
|
||||
</div>
|
||||
15
src/app/shared/components/ui/skeleton/skeleton.component.ts
Normal file
15
src/app/shared/components/ui/skeleton/skeleton.component.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-skeleton',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './skeleton.component.html',
|
||||
styleUrl: './skeleton.component.css'
|
||||
})
|
||||
export class SkeletonComponent {
|
||||
@Input() type: 'line' | 'avatar' = 'line';
|
||||
@Input() width: string = '100%';
|
||||
@Input() height: string = '1rem';
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/* Diese Komponente ist ebenfalls autark. Verschieben Sie die .status-pill- und .pill-*-Klassen
|
||||
aus styles.css hierher und löschen Sie sie dort. */
|
||||
:host {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.2s ease-out;
|
||||
}
|
||||
|
||||
.status-pill:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.status-pill::before {
|
||||
content: '';
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: currentColor;
|
||||
}
|
||||
|
||||
/* Farbvarianten */
|
||||
.pill-success { color: #15803d; background-color: #ecfdf5; border-color: #bbf7d0; }
|
||||
:host-context(body.dark-theme) .pill-success { color: #a7f3d0; background-color: #166534; border-color: #22c55e; }
|
||||
|
||||
.pill-warning { color: #b45309; background-color: #fffbeb; border-color: #fde68a; }
|
||||
:host-context(body.dark-theme) .pill-warning { color: #fde047; background-color: #92400e; border-color: #f59e0b; }
|
||||
|
||||
.pill-danger { color: #b91c1c; background-color: #fef2f2; border-color: #fecaca; }
|
||||
:host-context(body.dark-theme) .pill-danger { color: #fca5a5; background-color: #991b1b; border-color: #ef4444; }
|
||||
|
||||
.pill-info { color: #1d4ed8; background-color: #eff6ff; border-color: #bfdbfe; }
|
||||
:host-context(body.dark-theme) .pill-info { color: #93c5fd; background-color: #1e40af; border-color: #3b82f6; }
|
||||
@@ -1 +1,11 @@
|
||||
<p>status-pill works!</p>
|
||||
<span
|
||||
class="status-pill"
|
||||
[ngClass]="{
|
||||
'pill-success': status === 'success',
|
||||
'pill-warning': status === 'warning',
|
||||
'pill-danger': status === 'danger',
|
||||
'pill-info': status === 'info'
|
||||
}">
|
||||
<!-- Der Text für die Pille wird von außen über ng-content eingefügt -->
|
||||
<ng-content></ng-content>
|
||||
</span>
|
||||
@@ -1,11 +1,16 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
// Definiert die erlaubten Status-Typen für Typsicherheit
|
||||
type PillStatus = 'success' | 'warning' | 'danger' | 'info';
|
||||
|
||||
@Component({
|
||||
selector: 'app-status-pill',
|
||||
imports: [],
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './status-pill.component.html',
|
||||
styleUrl: './status-pill.component.css'
|
||||
})
|
||||
export class StatusPillComponent {
|
||||
|
||||
}
|
||||
@Input() status: PillStatus = 'info';
|
||||
}
|
||||
@@ -44,7 +44,7 @@ export class SnackbarService {
|
||||
setTimeout(() => {
|
||||
currentSnackbars = this.snackbarsSubject.value.filter(s => s.id !== id);
|
||||
this.snackbarsSubject.next(currentSnackbars);
|
||||
}, 100); // Muss zur CSS-Dauer passen
|
||||
}, 60); // Muss zur CSS-Dauer passen
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -373,6 +373,15 @@ body.dark-theme .btn-icon-danger:hover {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* -- Autofill Fix -- */
|
||||
.form-input:-webkit-autofill ~ .form-label {
|
||||
top: 0;
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-primary);
|
||||
border-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
.slide-toggle-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user