This commit is contained in:
Tizian.Breuch
2025-09-09 17:20:21 +02:00
parent b97fc21024
commit 6d9b8bc96b
18 changed files with 310 additions and 149 deletions

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#666666"><path d="M560-240 320-480l240-240 56 56-184 184 184 184-56 56Z"/></svg>

After

Width:  |  Height:  |  Size: 178 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#666666"><path d="M504-480 320-664l56-56 240 240-240 240-56-56 184-184Z"/></svg>

After

Width:  |  Height:  |  Size: 178 B

View File

@@ -75,25 +75,11 @@
<h3 class="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>
<app-form-select label="Stadt" [options]="cityOptions" ></app-form-select>
<app-form-textarea label="Nachricht"></app-form-textarea>
<app-slide-toggle></app-slide-toggle>
</div>
</app-card>
@@ -101,11 +87,14 @@
<h3 class="card-header">Buttons, Chips & Interaktion</h3>
<div class="component-grid">
<div class="button-group">
<app-button buttonType="primary">Primary</app-button>
<app-button buttonType="primary" tooltip="Primary"
>Primary</app-button
>
<app-button buttonType="secondary">Secondary</app-button>
<app-button buttonType="stroked">Stroked</app-button>
<app-button buttonType="flat">Flat</app-button>
<app-button buttonType="icon"
<app-button
buttonType="icon"
tooltip="Favorit hinzufügen"
iconName="favorite"
svgColor="red"
@@ -118,7 +107,6 @@
></app-button>
</div>
<div class="chip-set">
<app-chip label="Technologie" [removable]="true"></app-chip>
<app-chip
@@ -266,7 +254,9 @@
<p>Sind Sie sicher? Diese Aktion kann nicht rückgängig gemacht werden.</p>
</div>
<div dialog-actions>
<app-button buttonType="stroked" (click)="closeDialog()">Abbrechen</app-button>
<app-button buttonType="stroked" (click)="closeDialog()"
>Abbrechen</app-button
>
<app-button buttonType="primary" (click)="closeDialog()"
>Ja, endgültig löschen</app-button
>

View File

@@ -23,7 +23,8 @@ import { ExpansionPanelComponent } from '../../../../shared/components/layout/ex
import { SidebarComponent } from '../../../../shared/components/layout/sidebar/sidebar.component';
import { OrdersTableComponent, Order } from '../../../../shared/components/data-display/orders-table/orders-table.component';
import { FormSelectComponent, SelectOption } from '../../../../shared/components/form/form-select/form-select.component';
import { FormTextareaComponent } from '../../../../shared/components/form/form-textarea/form-textarea.component';
@Component({
selector: 'app-demo2',
standalone: true,
@@ -44,7 +45,9 @@ import { OrdersTableComponent, Order } from '../../../../shared/components/data-
SkeletonComponent,
ExpansionPanelComponent,
SidebarComponent,
OrdersTableComponent
OrdersTableComponent,
FormSelectComponent,
FormTextareaComponent
],
templateUrl: './demo2.component.html',
styleUrl: './demo2.component.css',
@@ -101,6 +104,13 @@ export class Demo2Component implements OnInit {
}
];
cityOptions: SelectOption[] = [
{ value: 'berlin', label: 'Berlin' },
{ value: 'munich', label: 'München' },
{ value: 'hamburg', label: 'Hamburg' }
];
constructor(private snackbarService: SnackbarService) {}
ngOnInit(): void {

View File

@@ -4,18 +4,20 @@
</span>
<div class="paginator-controls">
<app-button
color="icon"
buttonType="icon"
iconName="chevron_backward"
(click)="goToPrevious()"
[disabled]="currentPage === 1"
tooltip="Vorherige Seite">
&lt;
</app-button>
<app-button
color="icon"
buttonType="icon"
iconName="chevron_forward"
(click)="goToNext()"
[disabled]="currentPage === totalPages"
tooltip="Nächste Seite">
&gt;
</app-button>
</div>
</div>

View File

@@ -1,37 +1,46 @@
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 {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
FormsModule,
} from '@angular/forms'; // <-- FormsModule hier importieren
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-form-field',
standalone: true,
imports: [
CommonModule,
FormsModule
],
imports: [CommonModule, FormsModule],
templateUrl: './form-field.component.html',
styleUrl: './form-field.component.css',
styleUrl: '../form.css',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FormFieldComponent),
multi: true
}
]
multi: true,
},
],
})
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; }
}
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;
}
}

View File

@@ -0,0 +1,22 @@
<div class="form-field">
<select
class="form-input"
[id]="controlId"
[(ngModel)]="value"
(ngModelChange)="onChange($event)"
(blur)="onTouched()"
[disabled]="disabled">
<!-- Platzhalter-Option, die angezeigt wird, wenn kein Wert ausgewählt ist -->
<option [ngValue]="null" disabled>Bitte wählen...</option>
<!-- Schleife, die die übergebenen Optionen rendert -->
<option *ngFor="let option of options" [ngValue]="option.value">
{{ option.label }}
</option>
</select>
<!-- Das Label ist immer vorhanden -->
<label [for]="controlId" class="form-label">{{ label }}</label>
</div>

View File

@@ -0,0 +1,44 @@
import { Component, Input, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, ReactiveFormsModule, FormsModule } from '@angular/forms'; // FormsModule importieren
import { CommonModule } from '@angular/common';
// Interface für die Select-Optionen
export interface SelectOption {
value: any;
label: string;
}
@Component({
selector: 'app-form-select',
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
FormsModule // <-- HIER IST DIE KORREKTUR
],
templateUrl: './form-select.component.html',
styleUrl: '../form.css',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FormSelectComponent),
multi: true
}
]
})
export class FormSelectComponent implements ControlValueAccessor {
@Input() label: string = '';
@Input() options: SelectOption[] = [];
// ... (der Rest der Klasse bleibt unverändert)
controlId = `form-select-${Math.random().toString(36).substring(2)}`;
value: any = null;
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; }
}

View File

@@ -0,0 +1,13 @@
<div class="form-field">
<textarea
class="form-input"
[id]="controlId"
placeholder=" "
[rows]="rows"
[(ngModel)]="value"
(ngModelChange)="onChange($event)"
(blur)="onTouched()"
[disabled]="disabled"></textarea>
<label [for]="controlId" class="form-label">{{ label }}</label>
</div>

View File

@@ -0,0 +1,40 @@
import { Component, Input, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, ReactiveFormsModule, FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-form-textarea',
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
FormsModule // Wichtig für [(ngModel)]
],
templateUrl: './form-textarea.component.html',
styleUrl: '../form.css',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FormTextareaComponent),
multi: true
}
]
})
export class FormTextareaComponent implements ControlValueAccessor {
@Input() label: string = '';
@Input() rows = 3; // Standardanzahl der Zeilen
// Eindeutige ID für die Verknüpfung
controlId = `form-textarea-${Math.random().toString(36).substring(2)}`;
// --- 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; }
}

View File

@@ -0,0 +1,73 @@
/* =================================================================================
* GEMEINSAME BASIS-STILE FÜR ALLE FORMULAR-KOMPONENTEN
* ================================================================================= */
/* Stellt sicher, dass die Host-Komponente als Block-Element gerendert wird */
:host {
display: block;
}
/* Der Wrapper für das Label und das Eingabefeld */
.form-field {
position: relative;
}
/* Das "schwebende" Label */
.form-label {
position: absolute;
top: 50%;
left: 1rem;
transform: translateY(-50%);
color: var(--color-text-light);
background-color: var(--color-surface);
padding: 0 0.25rem;
transition: all 0.2s ease-out;
pointer-events: none;
border-radius: var(--border-radius-sm);
}
/* Passt die Hintergrundfarbe des Labels für das Dark Theme an */
:host-context(body.dark-theme) .form-label {
background-color: var(--color-surface);
}
/* Gemeinsame Stile für <input>, <select> und <textarea> */
.form-input {
width: 100%;
padding: 0.85rem 1rem;
border: 1px solid var(--color-border);
border-radius: var(--border-radius-md);
background-color: var(--color-surface);
color: var(--color-text);
font-size: 1rem;
font-family: inherit; /* Stellt eine einheitliche Schriftart sicher */
transition: border-color var(--transition-speed);
}
/* Fokus-Zustand für alle Eingabefelder */
.form-input:focus {
outline: none;
border-color: var(--color-primary);
}
/* Logik, die das Label nach oben bewegt */
.form-input:focus ~ .form-label,
.form-input:not(:placeholder-shown) ~ .form-label {
top: 0;
font-size: 0.8rem;
color: var(--color-primary);
}
/* Spezieller Fix für den Autofill-Hintergrund in Webkit-Browsern (Chrome, Edge) */
.form-input:-webkit-autofill ~ .form-label {
top: 0;
font-size: 0.8rem;
color: var(--color-primary);
}
/* Layout-Helfer für Formulargruppen */
.form-group-inline {
display: flex;
justify-content: space-between;
align-items: center;
}

View File

@@ -1,38 +0,0 @@
/* 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;
}

View File

@@ -10,7 +10,7 @@ import { CommonModule } from '@angular/common';
FormsModule
],
templateUrl: './slide-toggle.component.html',
styleUrl: './slide-toggle.component.css',
styleUrl: '../form.css',
providers: [
{
provide: NG_VALUE_ACCESSOR,

View File

@@ -1,20 +1,21 @@
/* Stile, die NUR für diese Button-Komponente gelten */
:host {
display: inline-block; /* Sorgt für korrektes Layout-Verhalten */
display: inline-block;
position: relative;
}
/* Basis-Stil für alle Buttons */
.btn {
position: relative; /* Wichtig für den Ripple-Effekt */
overflow: hidden; /* Verhindert, dass der Ripple über den Button hinausgeht */
overflow: hidden; /* Verhindert, dass der Ripple über den Button hinausgeht */
border: none;
border-radius: var(--border-radius-md);
cursor: pointer;
font-weight: 600;
padding: 0.6rem 1.2rem;
transition: all 0.2s ease-out;
/* Stellt sicher, dass der Inhalt (Text/Icon) zentriert ist */
display: inline-flex;
align-items: center;
@@ -95,7 +96,10 @@
animation: ripple-effect 0.6s linear;
background-color: rgba(255, 255, 255, 0.7);
}
.btn-secondary .ripple, .btn-stroked .ripple, .btn-flat .ripple, .btn-icon .ripple {
.btn-secondary .ripple,
.btn-stroked .ripple,
.btn-flat .ripple,
.btn-icon .ripple {
background-color: rgba(0, 0, 0, 0.1);
}
@@ -104,4 +108,29 @@
transform: scale(4);
opacity: 0;
}
}
}
[data-tooltip] {
position: relative;
}
[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%) translateY(-8px);
background-color: #2c3e50;
color: #fff;
padding: 0.25rem 0.75rem;
border-radius: var(--border-radius-sm);
font-size: 0.85rem;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: opacity var(--transition-speed), transform var(--transition-speed);
}
[data-tooltip]:hover::after {
opacity: 1;
visibility: visible;
transform: translateX(-50%) translateY(-12px);
}

View File

@@ -2,15 +2,14 @@
[type]="submitType"
[disabled]="disabled"
class="btn"
[class.btn-primary]="buttonType === 'primary'"
[class.btn-secondary]="buttonType === 'secondary'"
[class.btn-stroked]="buttonType === 'stroked'"
[class.btn-flat]="buttonType === 'flat'"
[class.btn-icon]="buttonType === 'icon' || buttonType === 'icon-danger'"
[class.btn-icon-danger]="buttonType === 'icon-danger'"
[class.btn-primary]="buttonType === 'primary'"
[class.btn-secondary]="buttonType === 'secondary'"
[class.btn-stroked]="buttonType === 'stroked'"
[class.btn-flat]="buttonType === 'flat'"
[class.btn-icon]="buttonType === 'icon' || buttonType === 'icon-danger'"
[class.btn-icon-danger]="buttonType === 'icon-danger'"
[class.btn-full-width]="fullWidth"
[attr.data-tooltip]="tooltip">
>
<app-icon *ngIf="iconName" [name]="iconName" [svgColor]="svgColor"></app-icon>
<ng-content></ng-content>
</button>
</button>

View File

@@ -1,8 +1,9 @@
import { Component, Input, ElementRef, Renderer2, HostListener } from '@angular/core';
// Importieren Sie HostBinding
import { Component, Input, ElementRef, Renderer2, HostListener, HostBinding } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IconComponent } from '../icon/icon.component';
type ButtonColor = 'primary' | 'secondary' | 'stroked' | 'flat' | 'icon' | 'icon-danger';
type ButtonType = 'primary' | 'secondary' | 'stroked' | 'flat' | 'icon' | 'icon-danger';
@Component({
selector: 'app-button',
@@ -12,17 +13,25 @@ type ButtonColor = 'primary' | 'secondary' | 'stroked' | 'flat' | 'icon' | 'icon
styleUrl: './button.component.css'
})
export class ButtonComponent {
@Input() buttonType: ButtonColor = 'primary';
@Input() buttonType: ButtonType = 'primary';
@Input() submitType: 'button' | 'submit' = 'button';
@Input() disabled = false;
@Input() fullWidth = false;
@Input() tooltip: string | null = null;
@Input() iconName: string | null = null;
@Input() svgColor: string | null = null;
// Der Tooltip-Input
@Input() tooltip: string | null = null;
// HIER IST DIE HINZUGEFÜGTE LOGIK:
// HostBinding verknüpft die `tooltip`-Eigenschaft mit dem `data-tooltip`-Attribut
// des <app-button>-Host-Elements.
@HostBinding('attr.data-tooltip') get tooltipAttr() {
return this.tooltip;
}
constructor(private el: ElementRef, private renderer: Renderer2) {}
// HostListener fängt Klick-Events direkt auf dem Host-Element (<app-button>) ab
@HostListener('click', ['$event'])
onClick(event: MouseEvent) {
if (this.disabled) {
@@ -32,23 +41,19 @@ export class ButtonComponent {
const button = this.el.nativeElement.querySelector('.btn');
if (!button) return;
// Entferne alte Ripple-Effekte
// --- Ihre Ripple-Logik bleibt hier unverändert ---
const existingRipple = button.querySelector('.ripple');
if (existingRipple) {
existingRipple.remove();
}
// Erzeuge den Ripple-Effekt
const circle = this.renderer.createElement('span');
const diameter = Math.max(button.clientWidth, button.clientHeight);
const radius = diameter / 2;
this.renderer.setStyle(circle, 'width', `${diameter}px`);
this.renderer.setStyle(circle, 'height', `${diameter}px`);
this.renderer.setStyle(circle, 'left', `${event.clientX - button.offsetLeft - radius}px`);
this.renderer.setStyle(circle, 'top', `${event.clientY - button.offsetTop - radius}px`);
this.renderer.addClass(circle, 'ripple');
this.renderer.appendChild(button, circle);
}
}

View File

@@ -240,11 +240,7 @@ body.dark-theme .nav-item:hover {
flex-direction: column;
gap: 2rem;
}
.form-group-inline {
display: flex;
justify-content: space-between;
align-items: center;
}
.text-right {
text-align: right;
}
@@ -309,43 +305,8 @@ body.dark-theme .nav-item:hover {
body.dark-theme .btn-icon-danger:hover {
background-color: #991b1b;
}
.form-field {
position: relative;
}
.form-label {
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 1rem;
color: var(--color-text-light);
background-color: var(--color-surface);
padding: 0 0.25rem;
transition: all 0.2s ease-out;
pointer-events: none;
border-radius: var(--border-radius-sm);
}
.form-input:focus ~ .form-label,
.form-input:not(:placeholder-shown) ~ .form-label {
top: 0;
font-size: 0.8rem;
color: var(--color-primary);
}
.form-input {
width: 100%;
padding: 0.85rem 1rem;
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: border-color var(--transition-speed);
}
.form-input:focus {
outline: none;
border-color: var(--color-primary);
}
/* -- Autofill Fix -- */
/* -- Autofill Fix in styles.css lassen-- */
.form-input:-webkit-autofill ~ .form-label {
top: 0;
font-size: 0.8rem;