phase-7: add reusable input-output widget library and default registry
This commit is contained in:
44
src/app/widgets/ui/footer-navigation-widget.component.ts
Normal file
44
src/app/widgets/ui/footer-navigation-widget.component.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
|
||||
export interface FooterNavigationLink {
|
||||
id: string;
|
||||
label: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-footer-navigation-widget',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
template: `
|
||||
<footer class="footer-nav-widget">
|
||||
<nav>
|
||||
<ul>
|
||||
@for (link of links; track link.id) {
|
||||
<li>
|
||||
<button type="button" (click)="onLinkClick(link)">{{ link.label }}</button>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
</footer>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
.footer-nav-widget { padding: 1rem; border-top: 1px solid var(--border-color, #d3dad9); }
|
||||
ul { display: flex; gap: 1rem; list-style: none; margin: 0; padding: 0; flex-wrap: wrap; }
|
||||
button { background: transparent; border: none; color: var(--text-secondary, #667a77); cursor: pointer; }
|
||||
`
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class FooterNavigationWidgetComponent {
|
||||
@Input() links: FooterNavigationLink[] = [];
|
||||
|
||||
@Output() linkSelected = new EventEmitter<FooterNavigationLink>();
|
||||
|
||||
onLinkClick(link: FooterNavigationLink): void {
|
||||
this.linkSelected.emit(link);
|
||||
}
|
||||
}
|
||||
39
src/app/widgets/ui/hero-widget.component.ts
Normal file
39
src/app/widgets/ui/hero-widget.component.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hero-widget',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
template: `
|
||||
<section class="hero-widget">
|
||||
<h1 class="hero-widget__title">{{ title }}</h1>
|
||||
@if (subtitle) {
|
||||
<p class="hero-widget__subtitle">{{ subtitle }}</p>
|
||||
}
|
||||
@if (ctaLabel) {
|
||||
<button type="button" class="hero-widget__cta" (click)="onCtaClick()">{{ ctaLabel }}</button>
|
||||
}
|
||||
</section>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
.hero-widget { padding: 2rem; border-radius: var(--radius-lg, 16px); background: var(--bg-secondary, #f5f5f5); }
|
||||
.hero-widget__title { margin: 0 0 0.5rem; font-size: 2rem; color: var(--text-primary, #1e3c38); }
|
||||
.hero-widget__subtitle { margin: 0 0 1rem; color: var(--text-secondary, #667a77); }
|
||||
.hero-widget__cta { border: none; border-radius: var(--radius-md, 12px); padding: 0.75rem 1rem; cursor: pointer; background: var(--primary-color, #497671); color: #fff; }
|
||||
`
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class HeroWidgetComponent {
|
||||
@Input() title: string = '';
|
||||
@Input() subtitle: string = '';
|
||||
@Input() ctaLabel: string = '';
|
||||
|
||||
@Output() ctaClicked = new EventEmitter<void>();
|
||||
|
||||
onCtaClick(): void {
|
||||
this.ctaClicked.emit();
|
||||
}
|
||||
}
|
||||
3
src/app/widgets/ui/index.ts
Normal file
3
src/app/widgets/ui/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './footer-navigation-widget.component';
|
||||
export * from './hero-widget.component';
|
||||
export * from './product-carousel-widget.component';
|
||||
53
src/app/widgets/ui/product-carousel-widget.component.ts
Normal file
53
src/app/widgets/ui/product-carousel-widget.component.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { ProductCardConfig } from '../../shared/models/ui';
|
||||
|
||||
@Component({
|
||||
selector: 'app-product-carousel-widget',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
template: `
|
||||
<section class="product-carousel-widget">
|
||||
@if (title) {
|
||||
<h2>{{ title }}</h2>
|
||||
}
|
||||
|
||||
<div class="product-carousel-widget__grid">
|
||||
@for (item of items; track item.id) {
|
||||
<article class="product-card">
|
||||
<img [src]="item.imageUrl" [alt]="item.title" loading="lazy" />
|
||||
<h3>{{ item.title }}</h3>
|
||||
@if (item.subtitle) {
|
||||
<p>{{ item.subtitle }}</p>
|
||||
}
|
||||
<strong>{{ item.price.amount }} {{ item.price.currency }}</strong>
|
||||
<button type="button" (click)="onProductClick(item)">{{ actionLabel }}</button>
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
.product-carousel-widget { padding: 1rem; }
|
||||
.product-carousel-widget__grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; }
|
||||
.product-card { padding: 0.75rem; border: 1px solid var(--border-color, #d3dad9); border-radius: var(--radius-md, 12px); }
|
||||
img { width: 100%; height: 140px; object-fit: cover; border-radius: 8px; margin-bottom: 0.5rem; }
|
||||
h3 { margin: 0 0 0.25rem; font-size: 1rem; }
|
||||
p { margin: 0 0 0.5rem; color: var(--text-secondary, #667a77); }
|
||||
button { margin-top: 0.5rem; border: none; border-radius: 8px; padding: 0.5rem 0.75rem; cursor: pointer; background: var(--primary-color, #497671); color: #fff; }
|
||||
`
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ProductCarouselWidgetComponent {
|
||||
@Input() title: string = '';
|
||||
@Input() actionLabel: string = 'Select';
|
||||
@Input() items: ProductCardConfig[] = [];
|
||||
|
||||
@Output() productSelected = new EventEmitter<ProductCardConfig>();
|
||||
|
||||
onProductClick(item: ProductCardConfig): void {
|
||||
this.productSelected.emit(item);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user