This commit is contained in:
Tizian.Breuch
2025-09-08 19:27:46 +02:00
parent ee8974e2ff
commit c65aef11ca
66 changed files with 1691 additions and 108 deletions

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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
}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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)">&times;</span>
</span>

View File

@@ -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();
}
}

View File

@@ -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%;
}

View File

@@ -1 +0,0 @@
<p>icon works!</p>

View File

@@ -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');
}
});
}
}
}

View 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%;
}

View File

@@ -0,0 +1,6 @@
<div
class="skeleton-item"
[ngClass]="'skeleton-' + type"
[style.width]="type === 'line' ? width : '50px'"
[style.height]="type === 'line' ? height : '50px'">
</div>

View 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';
}

View File

@@ -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; }

View File

@@ -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>

View File

@@ -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';
}