From 1ad34a9ecf1be3581795a31a14985045f55415e2 Mon Sep 17 00:00:00 2001 From: "Tizian.Breuch" Date: Tue, 16 Sep 2025 14:48:36 +0200 Subject: [PATCH] styling --- src/app/app.component.html | 6 +- .../components/demo2/demo2.component.html | 100 +++++++-- .../demo/components/demo2/demo2.component.ts | 141 +++++++++++-- .../form/form-field/form-field.component.css | 103 +++++++++ .../form/form-field/form-field.component.html | 7 +- .../form/form-field/form-field.component.ts | 54 ++--- .../form/form-group/form-group.component.css | 33 +++ .../form/form-group/form-group.component.html | 21 ++ .../form/form-group/form-group.component.ts | 17 ++ .../form-select/form-select.component.css | 197 ++++++++++++++++++ .../form-select/form-select.component.html | 60 ++++-- .../form/form-select/form-select.component.ts | 80 +++++-- .../form-textarea/form-textarea.component.css | 79 +++++++ .../form-textarea/form-textarea.component.ts | 2 +- .../slide-toggle/slide-toggle.component.css | 90 ++++++++ .../slide-toggle/slide-toggle.component.html | 33 ++- .../slide-toggle/slide-toggle.component.ts | 25 ++- src/styles.css | 22 +- 18 files changed, 929 insertions(+), 141 deletions(-) create mode 100644 src/app/shared/components/form/form-field/form-field.component.css create mode 100644 src/app/shared/components/form/form-group/form-group.component.css create mode 100644 src/app/shared/components/form/form-group/form-group.component.html create mode 100644 src/app/shared/components/form/form-group/form-group.component.ts create mode 100644 src/app/shared/components/form/form-select/form-select.component.css create mode 100644 src/app/shared/components/form/form-textarea/form-textarea.component.css create mode 100644 src/app/shared/components/form/slide-toggle/slide-toggle.component.css diff --git a/src/app/app.component.html b/src/app/app.component.html index 2057532..83f5d9d 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,5 +1,3 @@ -
- -
+ - \ No newline at end of file + diff --git a/src/app/features/demo/components/demo2/demo2.component.html b/src/app/features/demo/components/demo2/demo2.component.html index d55d1ee..944e553 100644 --- a/src/app/features/demo/components/demo2/demo2.component.html +++ b/src/app/features/demo/components/demo2/demo2.component.html @@ -1,19 +1,83 @@ -
- - -
+
+ - - +
+ + +
+ +
+ + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + +
+
diff --git a/src/app/features/demo/components/demo2/demo2.component.ts b/src/app/features/demo/components/demo2/demo2.component.ts index 00deffd..9d92457 100644 --- a/src/app/features/demo/components/demo2/demo2.component.ts +++ b/src/app/features/demo/components/demo2/demo2.component.ts @@ -1,4 +1,11 @@ -import { Component } from '@angular/core'; +import { + Component, + Inject, + OnInit, + PLATFORM_ID, + Renderer2, +} from '@angular/core'; +import { DOCUMENT } from '@angular/common'; import { CommonModule } from '@angular/common'; import { CardComponent } from '../../../../shared/components/ui/card/card.component'; // Wir müssen KpiColor hier importieren, um es als Typ verwenden zu können @@ -10,7 +17,20 @@ import { OrdersTableComponent, Order, } from '../../../../shared/components/data-display/orders-table/orders-table.component'; -import { PaginatorComponent } from '../../../../shared/components/data-display/paginator/paginator.component'; +import { FormFieldComponent } from '../../../../shared/components/form/form-field/form-field.component'; +import { FormGroupComponent } from '../../../../shared/components/form/form-group/form-group.component'; +import { + FormSelectComponent, + SelectOption, +} from '../../../../shared/components/form/form-select/form-select.component'; + +import { FormsModule } from '@angular/forms'; + +import { FormTextareaComponent } from '../../../../shared/components/form/form-textarea/form-textarea.component'; + +import { SlideToggleComponent } from '../../../../shared/components/form/slide-toggle/slide-toggle.component'; + +import { isPlatformBrowser } from '@angular/common'; // Wir definieren ein Interface für unsere KPI-Daten für Typsicherheit interface Kpi { @@ -25,10 +45,15 @@ interface Kpi { standalone: true, imports: [ CommonModule, - CardComponent, + KpiCardComponent, OrdersTableComponent, - PaginatorComponent, + FormGroupComponent, + FormFieldComponent, + FormSelectComponent, + FormsModule, + FormTextareaComponent, + SlideToggleComponent, ], templateUrl: './demo2.component.html', }) @@ -127,21 +152,111 @@ export class Demo2Component { status: 'danger', statusText: 'Storniert', }, + { + id: '10543', + user: { + name: 'Max Mustermann', + email: 'max.m@example.com', + avatarUrl: 'https://i.pravatar.cc/40?u=max', + }, + amount: '€ 129,99', + status: 'success', + statusText: 'Abgeschlossen', + }, + { + id: '10542', + user: { + name: 'Erika Mustermann', + email: 'erika.m@example.com', + avatarUrl: 'https://i.pravatar.cc/40?u=erika', + }, + amount: '€ 49,50', + status: 'warning', + statusText: 'In Bearbeitung', + }, + { + id: '10541', + user: { + name: 'Peter Pan', + email: 'peter.p@example.com', + avatarUrl: 'https://i.pravatar.cc/40?u=peter', + }, + amount: '€ 87,00', + status: 'danger', + statusText: 'Storniert', + }, ]; - // Eigenschaften für den Paginator - currentPage = 1; - totalItems = this.ordersData.length; - itemsPerPage = 10; + benutzername: string = ''; + email: string = ''; + aktuellesPasswort: any = ''; + neuesPasswort: any = ''; - // --- EVENT-HANDLER FÜR DIE KIND-KOMPONENTEN --- + selectedCountry: string | null = null; - onPageChange(newPage: number): void { - this.currentPage = newPage; - console.log('Seite gewechselt zu:', newPage); - // In einer echten Anwendung würden Sie hier die Daten neu laden + countryOptions: SelectOption[] = [ + { value: 'de', label: 'Deutschland' }, + { value: 'at', label: 'Österreich' }, + { value: 'ch', label: 'Schweiz' }, + ]; + + biografie: string = + 'Dies ist eine Beispiel-Biografie, die über mehrere Zeilen gehen kann.'; + + benachrichtigungenAktiv: boolean = true; + profilIstPrivat: boolean = false; + + private readonly darkModeKey = 'app-dark-mode-setting'; + darkModeAktiv: boolean = false; + + constructor( + private renderer: Renderer2, + @Inject(DOCUMENT) private document: Document, + @Inject(PLATFORM_ID) private platformId: Object + ) {} + + ngOnInit(): void { + this.loadThemeSetting(); } + private loadThemeSetting(): void { + if (isPlatformBrowser(this.platformId)) { + try { + const storedValue = localStorage.getItem(this.darkModeKey); + // Setze den Zustand der Komponente basierend auf dem gespeicherten Wert. + this.darkModeAktiv = storedValue === 'true'; + } catch (e) { + console.error('Could not access localStorage:', e); + } + } + // Wende das Theme an - entweder den Standardwert (false) oder den geladenen Wert. + this.updateTheme(this.darkModeAktiv); + } + + onDarkModeChange(isEnabled: boolean): void { + this.darkModeAktiv = isEnabled; + + // --- KORREKTUR 2: Speichere die neue Einstellung --- + if (isPlatformBrowser(this.platformId)) { + try { + localStorage.setItem(this.darkModeKey, String(isEnabled)); + } catch (e) { + console.error('Could not write to localStorage:', e); + } + } + + // Wende die visuelle Änderung an. + this.updateTheme(isEnabled); + } + + // This method is CORRECT. It just needs to be called. + private updateTheme(isEnabled: boolean): void { + if (isEnabled) { + this.renderer.addClass(this.document.body, 'dark-theme'); + } else { + this.renderer.removeClass(this.document.body, 'dark-theme'); + } + } handleDeleteOrder(orderId: string): void { console.log('Lösche Bestellung mit ID:', orderId); // Hier könnten Sie z.B. einen Bestätigungs-Dialog öffnen diff --git a/src/app/shared/components/form/form-field/form-field.component.css b/src/app/shared/components/form/form-field/form-field.component.css new file mode 100644 index 0000000..475211f --- /dev/null +++ b/src/app/shared/components/form/form-field/form-field.component.css @@ -0,0 +1,103 @@ +/* ================================================================================= + * STILE FÜR DIE FORM-FIELD KOMPONENTE (FLOATING LABEL) + * ================================================================================= */ + +/* Stellt sicher, dass die Komponente den Raum korrekt einnimmt. */ +:host { + display: block; +} + +/* + * Der Hauptcontainer. + * `position: relative` ist ZWINGEND ERFORDERLICH, um den visuellen + * Bezugspunkt für das absolut positionierte Label zu schaffen. + */ +.form-field { + position: relative; +} + +/* Das eigentliche Eingabefeld */ +.form-input { + /* Layout & Box-Modell */ + width: 100%; + padding: 0.85rem 1rem; + border: 1px solid var(--color-border); + border-radius: var(--border-radius-md); + + /* Aussehen & Typografie */ + background-color: var(--color-surface); + color: var(--color-text); + font-size: 1rem; + font-family: inherit; /* Erbt die Schriftart von der Seite */ + + /* Verhalten */ + transition: border-color var(--transition-speed); +} + +/* Visuelles Feedback, wenn das Feld aktiv ist. */ +.form-input:focus { + outline: none; /* Entfernt den Standard-Browser-Rahmen */ + border-color: var(--color-primary); +} + +/* Das Label, das über dem Input "schwebt" */ +.form-label { + /* Positionierung */ + position: absolute; + top: 50%; /* Vertikal zentrieren (Schritt 1) */ + left: 1rem; /* Linken Abstand wie beim Input-Padding halten */ + transform: translateY(-50%); /* Vertikal zentrieren (Schritt 2 - Feinjustierung) */ + + border-radius: 4px; + /* Aussehen & Typografie */ + color: var(--color-text-light); + background-color: var(--color-surface); /* WICHTIG: Überdeckt die Input-Linie beim Schweben */ + padding: 0 0.25rem; + + /* Verhalten */ + transition: all 0.2s ease-out; /* Animiert alle Änderungen (top, font-size, color) */ + pointer-events: none; /* Erlaubt Klicks "durch" das Label auf das Input-Feld darunter */ +} + +/* ================================================================================= + * "FLOATING"-LOGIK: Wann bewegt sich das Label nach oben? + * ================================================================================= */ + +/* + * Der `~` (General Sibling Combinator) ist der Schlüssel. Er wählt das `.form-label` aus, + * das ein Geschwister von `.form-input` ist und nach ihm im HTML kommt. + * + * Das Label bewegt sich nach oben, WENN: + * 1. Der Input den Fokus hat ODER + * 2. Der Input NICHT seinen Platzhalter anzeigt (also Text enthält). + * (Funktioniert nur, weil im HTML `placeholder=" "` gesetzt ist) +*/ +.form-input:focus ~ .form-label, +.form-input:not(:placeholder-shown) ~ .form-label { + top: 0; + transform: translateY(-50%); /* Bleibt für die Zentrierung auf der Border-Linie wichtig */ + font-size: 0.8rem; + color: var(--color-primary); +} + +/* + * ZUSÄTZLICHE REGEL FÜR BROWSER-AUTOFILL + * Chrome & Co. färben den Hintergrund von automatisch ausgefüllten Feldern. + * Diese Regel sorgt dafür, dass unser Label auch in diesem Fall nach oben wandert + * und die richtige Farbe bekommt. +*/ +.form-input:-webkit-autofill, +.form-input:-webkit-autofill:hover, +.form-input:-webkit-autofill:focus, +.form-input:-webkit-autofill:active { + /* OPTIONAL: Überschreibt den unschönen gelben/blauen Autofill-Hintergrund */ + -webkit-box-shadow: 0 0 0 30px var(--color-surface) inset !important; + -webkit-text-fill-color: var(--color-text) !important; +} + +.form-input:-webkit-autofill ~ .form-label { + top: 0; + transform: translateY(-50%); + font-size: 0.8rem; + color: var(--color-primary); +} \ No newline at end of file diff --git a/src/app/shared/components/form/form-field/form-field.component.html b/src/app/shared/components/form/form-field/form-field.component.html index 3e65cec..8d8f568 100644 --- a/src/app/shared/components/form/form-field/form-field.component.html +++ b/src/app/shared/components/form/form-field/form-field.component.html @@ -1,12 +1,11 @@
-
\ No newline at end of file diff --git a/src/app/shared/components/form/form-field/form-field.component.ts b/src/app/shared/components/form/form-field/form-field.component.ts index ac16c01..08f6529 100644 --- a/src/app/shared/components/form/form-field/form-field.component.ts +++ b/src/app/shared/components/form/form-field/form-field.component.ts @@ -1,46 +1,32 @@ -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 { Component, Input, Output, EventEmitter } from '@angular/core'; import { CommonModule } from '@angular/common'; @Component({ selector: 'app-form-field', standalone: true, - imports: [CommonModule, FormsModule], + imports: [CommonModule], // Kein FormsModule mehr nötig templateUrl: './form-field.component.html', - styleUrl: '../form.css', - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => FormFieldComponent), - multi: true, - }, - ], + styleUrl: './form-field.component.css', + // Kein 'providers'-Block für ControlValueAccessor mehr }) -export class FormFieldComponent implements ControlValueAccessor { +export class FormFieldComponent { + // EINGÄNGE: Werte, die von außen gesetzt werden @Input() label: string = ''; @Input() type: 'text' | 'email' | 'password' = 'text'; + @Input() value: string = ''; // Der aktuelle Wert des Feldes + @Input() disabled: boolean = false; - // Interne Logik für ControlValueAccessor - value: string = ''; - onChange: (value: any) => void = () => {}; - onTouched: () => void = () => {}; - disabled = false; + // AUSGANG: Ein Event, das ausgelöst wird, wenn sich der Wert ändert + // WICHTIG: Der Name muss `[InputName]Change` sein, also `valueChange` + @Output() valueChange = new EventEmitter(); - writeValue(value: any): void { - this.value = value; + /** + * Diese Methode wird bei jeder Tastatureingabe im Input-Feld aufgerufen. + */ + onInput(event: Event): void { + // 1. Hole den neuen Wert aus dem HTML-Input-Element + const newValue = (event.target as HTMLInputElement).value; + // 2. Sende den neuen Wert über den EventEmitter nach außen + this.valueChange.emit(newValue); } - registerOnChange(fn: any): void { - this.onChange = fn; - } - registerOnTouched(fn: any): void { - this.onTouched = fn; - } - setDisabledState?(isDisabled: boolean): void { - this.disabled = isDisabled; - } -} +} \ No newline at end of file diff --git a/src/app/shared/components/form/form-group/form-group.component.css b/src/app/shared/components/form/form-group/form-group.component.css new file mode 100644 index 0000000..7696d2b --- /dev/null +++ b/src/app/shared/components/form/form-group/form-group.component.css @@ -0,0 +1,33 @@ +:host { + display: block; +} + +.form-group { + /* Stil für den Kasten um die Gruppe */ + border: 1px solid var(--color-border); + border-radius: var(--border-radius-md); + padding: 1.5rem; +} + +.form-group-title { + /* Stil für die Überschrift */ + font-size: 1rem; + font-weight: 600; + color: var(--color-text); + padding: 0 0.5rem; /* Sorgt dafür, dass der Titel die Linie schön unterbricht */ + margin-left: 0.5rem; /* Kleine Einrückung */ +} + +.form-group-description { + /* Stil für die optionale Beschreibung */ + font-size: 0.9rem; + color: var(--color-text-light); + margin-top: -0.75rem; /* Rücken wir näher an den Titel */ + margin-bottom: 1.5rem; +} + +.form-group-content { + /* Sorgt für einen einheitlichen Abstand zwischen den Feldern in der Gruppe */ + display: grid; + gap: 1.5rem; +} \ No newline at end of file diff --git a/src/app/shared/components/form/form-group/form-group.component.html b/src/app/shared/components/form/form-group/form-group.component.html new file mode 100644 index 0000000..d6c4290 --- /dev/null +++ b/src/app/shared/components/form/form-group/form-group.component.html @@ -0,0 +1,21 @@ +
+ + + {{ title }} + + + +

+ {{ description }} +

+ + +
+ +
+
diff --git a/src/app/shared/components/form/form-group/form-group.component.ts b/src/app/shared/components/form/form-group/form-group.component.ts new file mode 100644 index 0000000..f64c175 --- /dev/null +++ b/src/app/shared/components/form/form-group/form-group.component.ts @@ -0,0 +1,17 @@ +import { Component, Input } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-form-group', + standalone: true, + imports: [CommonModule], + templateUrl: './form-group.component.html', + styleUrl: './form-group.component.css', +}) +export class FormGroupComponent { + // EINGANG: Der Titel, der über der Gruppe angezeigt wird. + @Input() title: string = ''; + + // EINGANG (Optional): Eine kleine Beschreibung unter dem Titel. + @Input() description: string = ''; +} diff --git a/src/app/shared/components/form/form-select/form-select.component.css b/src/app/shared/components/form/form-select/form-select.component.css new file mode 100644 index 0000000..229ffbc --- /dev/null +++ b/src/app/shared/components/form/form-select/form-select.component.css @@ -0,0 +1,197 @@ +/* ================================================================================= + * STILE FÜR DIE CUSTOM SELECT KOMPONENTE + * ================================================================================= */ + +:host { + display: block; +} + +/* Der Haupt-Wrapper */ +.custom-select-wrapper { + position: relative; +} + +.form-input { + /* Layout & Box-Modell */ + width: 100%; + padding: 0.85rem 1rem; + border: 1px solid var(--color-border); + border-radius: var(--border-radius-md); + + /* Aussehen & Typografie */ + background-color: var(--color-surface); + color: var(--color-text); + font-size: 1rem; + font-family: inherit; /* Erbt die Schriftart von der Seite */ + + /* Verhalten */ + transition: border-color var(--transition-speed); +} + +/* Das "schwebende" Label (angepasste Logik) */ +.form-label { + /* Positionierung */ + position: absolute; + top: 50%; /* Vertikal zentrieren (Schritt 1) */ + left: 1rem; /* Linken Abstand wie beim Input-Padding halten */ + transform: translateY( + -50% + ); /* Vertikal zentrieren (Schritt 2 - Feinjustierung) */ +border-radius: 4px; + /* Aussehen & Typografie */ + color: var(--color-text-light); + background-color: var( + --color-surface + ); /* WICHTIG: Überdeckt die Input-Linie beim Schweben */ + padding: 0 0.25rem; + + /* Verhalten */ + transition: all 0.2s ease-out; /* Animiert alle Änderungen (top, font-size, color) */ + pointer-events: none; /* Erlaubt Klicks "durch" das Label auf das Input-Feld darunter */ +} + +/* Basis-Stile für den sichtbaren Teil (der Button) */ +.form-input.select-display { + width: 100%; + text-align: left; + display: flex; + justify-content: space-between; + align-items: center; + padding-right: 2.5rem; /* Platz für den Pfeil */ +} + +/* Button-Reset, damit er wie ein Input aussieht */ +button.form-input { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-color: var(--color-surface); + border: 1px solid var(--color-border); + font-family: inherit; + font-size: inherit; + margin: 0; + color: var(--color-text); +} + +/* ================================================================================= + * KORRIGIERTER BLOCK FÜR DIE NACHRÜCK-ANIMATION + * ================================================================================= */ + +/* + * KORREKTUR 1: Machen den Container zu einer Flexbox. + * Das ist der Schlüssel, damit der Browser die Position des zweiten Elements animiert, + * während das erste schrumpft. +*/ +.placeholder-text { + color: var(--color-text-light); + display: flex; + align-items: center; +} + +/* + * KORREKTUR 2: Wir stellen sicher, dass das erste span (das Label) auch ein Flex-Item ist, + * das schrumpfen kann. Der Rest der Animation bleibt gleich. +*/ +.placeholder-text .label { + display: inline-block; + vertical-align: middle; + white-space: nowrap; + overflow: hidden; + color: transparent; + padding-right: 8px; + max-width: 100px; + transition: 0.3s; +} + +/* + * Der Trigger bleibt gleich: Bei Fokus schrumpft das Label auf 0, + * und die Flexbox schiebt den "wählen..."-Text sanft an seine neue Position. +*/ +.select-display:focus .placeholder-text .label { + max-width: 0; + padding-right: 0; +} +/* ================================================================================= */ + +/* Ausgewählter Text */ +.selected-text { + color: var(--color-text); +} + +/* Fokus-Zustand */ +.select-display:focus { + outline: none; + border-color: var(--color-primary); +} + +/* "Floating"-Label-Logik */ +.select-display:focus ~ .form-label, +.form-label.has-value { + top: 0; + font-size: 0.8rem; + color: var(--color-primary); +} + +/* ================================================================================= + * STILE FÜR DEN PFEIL UND DAS DROPDOWN-MENÜ + * ================================================================================= */ +/* ... (der Rest der CSS-Datei bleibt unverändert) ... */ +.select-display::after { + content: ""; + position: absolute; + top: 50%; + right: 1rem; + transform: translateY(-50%); + width: 0.8em; + height: 0.5em; + background-color: var(--color-text-light); + clip-path: polygon(100% 0%, 0 0%, 50% 100%); + transition: transform 0.2s ease-out, background-color var(--transition-speed); +} + +.select-display:focus::after { + background-color: var(--color-primary); +} + +.custom-select-wrapper.is-open .select-display::after { + transform: translateY(-50%) rotate(180deg); +} + +.options-list { + position: absolute; + top: calc(100% + 4px); + left: 0; + right: 0; + list-style: none; + margin: 0; + padding: 0.5rem 0; + background-color: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-md); + box-shadow: var(--box-shadow-md); + z-index: 1000; + max-height: 220px; + overflow-y: auto; + animation: fadeInDown 0.2s ease-out forwards; +} + +.option-item { + padding: 0.75rem 1rem; + cursor: pointer; + transition: background-color 0.15s ease-out; +} + +.option-item:hover { + background-color: var(--color-body-bg-hover); +} + +@keyframes fadeInDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/src/app/shared/components/form/form-select/form-select.component.html b/src/app/shared/components/form/form-select/form-select.component.html index 14bdc1f..78b5db3 100644 --- a/src/app/shared/components/form/form-select/form-select.component.html +++ b/src/app/shared/components/form/form-select/form-select.component.html @@ -1,22 +1,42 @@ -
- - - - -
\ No newline at end of file + + + diff --git a/src/app/shared/components/form/form-select/form-select.component.ts b/src/app/shared/components/form/form-select/form-select.component.ts index f5ba575..dd23378 100644 --- a/src/app/shared/components/form/form-select/form-select.component.ts +++ b/src/app/shared/components/form/form-select/form-select.component.ts @@ -1,8 +1,9 @@ -import { Component, Input, forwardRef } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR, ReactiveFormsModule, FormsModule } from '@angular/forms'; // FormsModule importieren +import { Component, Input, forwardRef, HostListener, ElementRef } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, ReactiveFormsModule, FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; +import { animate, style, transition, trigger } from '@angular/animations'; -// Interface für die Select-Optionen +// Interface bleibt unverändert export interface SelectOption { value: any; label: string; @@ -11,34 +12,87 @@ export interface SelectOption { @Component({ selector: 'app-form-select', standalone: true, - imports: [ - CommonModule, - ReactiveFormsModule, - FormsModule // <-- HIER IST DIE KORREKTUR - ], + imports: [CommonModule, ReactiveFormsModule, FormsModule], templateUrl: './form-select.component.html', - styleUrl: '../form.css', + styleUrl: './form-select.component.css', providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => FormSelectComponent), - multi: true - } + multi: true, + }, + ], + animations: [ + trigger('dropdownAnimation', [ + // :enter ist ein Alias für den Übergang von "void => *" (wenn *ngIf true wird) + transition(':enter', [ + style({ opacity: 0, transform: 'translateY(-10px)' }), // Start-Zustand (unsichtbar) + animate('0.2s ease-out', style({ opacity: 1, transform: 'translateY(0)' })) // End-Zustand (sichtbar) + ]), + // :leave ist ein Alias für den Übergang von "* => void" (wenn *ngIf false wird) + transition(':leave', [ + animate('0.2s ease-in', style({ opacity: 0, transform: 'translateY(-10px)' })) // Animiert zum unsichtbaren Zustand + ]) + ]) ] }) export class FormSelectComponent implements ControlValueAccessor { @Input() label: string = ''; @Input() options: SelectOption[] = []; - // ... (der Rest der Klasse bleibt unverändert) + // NEU: Zustand für das Dropdown-Menü und das angezeigte Label + isOpen = false; + selectedLabel: string | null = null; + 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; } + constructor(private elementRef: ElementRef) {} + + // NEU: Schließt das Dropdown, wenn außerhalb des Elements geklickt wird + @HostListener('document:click', ['$event']) + onDocumentClick(event: MouseEvent): void { + // Diese Logik ist jetzt sicher, weil Klicks innerhalb der Komponente sie nie erreichen + if (!this.elementRef.nativeElement.contains(event.target)) { + this.isOpen = false; + } + } + + // ÜBERARBEITET: writeValue aktualisiert jetzt auch das sichtbare Label + writeValue(value: any): void { + this.value = value; + const selectedOption = this.options.find(opt => opt.value === value); + this.selectedLabel = selectedOption ? selectedOption.label : null; + } + registerOnChange(fn: any): void { this.onChange = fn; } registerOnTouched(fn: any): void { this.onTouched = fn; } setDisabledState?(isDisabled: boolean): void { this.disabled = isDisabled; } + + // NEU: Methode zum Öffnen/Schließen des Menüs + // ÜBERARBEITET: Nimmt das Event entgegen und stoppt es + toggleDropdown(event: MouseEvent): void { + event.stopPropagation(); // <-- WICHTIGSTE KORREKTUR + if (!this.disabled) { + this.isOpen = !this.isOpen; + if (!this.isOpen) { + this.onTouched(); + } + } + } + + // ÜBERARBEITET: Nimmt das Event entgegen und stoppt es + selectOption(option: SelectOption, event: MouseEvent): void { + event.stopPropagation(); // <-- WICHTIGE KORREKTUR + if (!this.disabled) { + this.value = option.value; + this.selectedLabel = option.label; + this.onChange(this.value); + this.onTouched(); + this.isOpen = false; + } + } } \ No newline at end of file diff --git a/src/app/shared/components/form/form-textarea/form-textarea.component.css b/src/app/shared/components/form/form-textarea/form-textarea.component.css new file mode 100644 index 0000000..89e3a9b --- /dev/null +++ b/src/app/shared/components/form/form-textarea/form-textarea.component.css @@ -0,0 +1,79 @@ +/* ================================================================================= + * STILE FÜR DIE FORM-TEXTAREA KOMPONENTE + * ================================================================================= */ + +/* Stellt sicher, dass die Host-Komponente als Block-Element gerendert wird. */ +:host { + display: block; +} + +/* Der Container, der das Textarea-Feld und das Label umschließt. */ +.form-field { + position: relative; +} + +/* Gemeinsame Stile, die sowohl für als auch für