Add landing pages library with comprehensive components and demos

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
skyai_dev
2025-09-06 13:52:41 +10:00
parent 5346d6d0c9
commit 246c62fd49
113 changed files with 13015 additions and 165 deletions

View File

@@ -0,0 +1,335 @@
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation, signal, computed, inject, OnInit, OnDestroy, HostListener } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { Subject, takeUntil } from 'rxjs';
import { faBars, faXmark } from '@fortawesome/free-solid-svg-icons';
import { LandingHeaderConfig, NavigationItem, LogoConfig } from '../../interfaces/navigation.interfaces';
import { CTAButton } from '../../interfaces/shared.interfaces';
import { ButtonComponent, ContainerComponent, FlexComponent, IconButtonComponent } from 'ui-essentials';
@Component({
selector: 'ui-lp-header',
standalone: true,
imports: [
CommonModule,
RouterModule,
ButtonComponent,
ContainerComponent,
FlexComponent,
IconButtonComponent
],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
template: `
<header
class="ui-lp-header"
[class.ui-lp-header--transparent]="config().transparent"
[class.ui-lp-header--sticky]="config().sticky"
[class.ui-lp-header--scrolled]="isScrolled()"
[class.ui-lp-header--mobile-menu-open]="mobileMenuOpen()"
[class.ui-lp-header--theme-dark]="config().theme === 'dark'"
[attr.aria-label]="'Main navigation'">
<ui-container [size]="config().size || 'xl'" [padding]="'md'">
<ui-flex [justify]="'between'" [align]="'center'" class="ui-lp-header__content">
<!-- Logo Section -->
<div class="ui-lp-header__logo-section">
@if (config().logo.imageUrl) {
<a
[href]="config().logo.url || '/'"
[routerLink]="config().logo.url || '/'"
class="ui-lp-header__logo-link">
<img
[src]="config().logo.imageUrl"
[alt]="config().logo.text || 'Logo'"
[width]="config().logo.width || 120"
[height]="config().logo.height || 40"
class="ui-lp-header__logo-image">
</a>
} @else if (config().logo.text) {
<a
[href]="config().logo.url || '/'"
[routerLink]="config().logo.url || '/'"
class="ui-lp-header__logo-text">
{{ config().logo.text }}
</a>
}
</div>
<!-- Desktop Navigation -->
<nav class="ui-lp-header__nav ui-lp-header__nav--desktop" [attr.aria-label]="'Primary navigation'">
<ul class="ui-lp-header__nav-list">
@for (item of config().navigation; track item.id) {
<li class="ui-lp-header__nav-item">
@if (item.children && item.children.length > 0) {
<!-- Dropdown Menu Item -->
<button
class="ui-lp-header__nav-link ui-lp-header__nav-link--dropdown"
[attr.aria-expanded]="false"
[attr.aria-haspopup]="'true'"
(click)="toggleDropdown(item.id)"
type="button">
{{ item.label }}
<span class="ui-lp-header__nav-arrow" aria-hidden="true">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M4.427 7.427l3.396 3.396a.25.25 0 00.354 0l3.396-3.396A.25.25 0 0011.396 7H4.604a.25.25 0 00-.177.427z"/>
</svg>
</span>
</button>
@if (openDropdown() === item.id) {
<div class="ui-lp-header__dropdown" role="menu">
@for (child of item.children; track child.id) {
<a
[href]="child.url"
[routerLink]="child.route"
[target]="child.target || '_self'"
class="ui-lp-header__dropdown-item"
role="menuitem"
(click)="handleNavClick(child)">
@if (child.icon) {
<span class="ui-lp-header__nav-icon">{{ child.icon }}</span>
}
{{ child.label }}
@if (child.badge) {
<span class="ui-lp-header__nav-badge">{{ child.badge }}</span>
}
</a>
}
</div>
}
} @else {
<!-- Regular Menu Item -->
<a
[href]="item.url"
[routerLink]="item.route"
[target]="item.target || '_self'"
class="ui-lp-header__nav-link"
(click)="handleNavClick(item)">
@if (item.icon) {
<span class="ui-lp-header__nav-icon">{{ item.icon }}</span>
}
{{ item.label }}
@if (item.badge) {
<span class="ui-lp-header__nav-badge">{{ item.badge }}</span>
}
</a>
}
</li>
}
</ul>
</nav>
<!-- Actions Section -->
<div class="ui-lp-header__actions">
@if (config().ctaButton) {
<ui-button
[variant]="config().ctaButton!.variant"
[size]="config().ctaButton!.size || 'medium'"
class="ui-lp-header__cta-button"
(clicked)="handleCTAClick()">
@if (config().ctaButton!.icon) {
<span class="ui-lp-header__cta-icon">{{ config().ctaButton!.icon }}</span>
}
{{ config().ctaButton!.text }}
</ui-button>
}
<!-- Mobile Menu Toggle -->
@if (config().showMobileMenu !== false) {
<ui-icon-button
[icon]="mobileMenuOpen() ? faXmark : faBars"
[size]="'medium'"
class="ui-lp-header__mobile-toggle"
[attr.aria-expanded]="mobileMenuOpen()"
[attr.aria-label]="mobileMenuOpen() ? 'Close menu' : 'Open menu'"
(clicked)="toggleMobileMenu()">
</ui-icon-button>
}
</div>
</ui-flex>
</ui-container>
<!-- Mobile Navigation -->
@if (mobileMenuOpen() && config().showMobileMenu !== false) {
<nav class="ui-lp-header__nav ui-lp-header__nav--mobile" [attr.aria-label]="'Mobile navigation'">
<ui-container [size]="config().size || 'xl'" [padding]="'md'">
<ul class="ui-lp-header__mobile-list">
@for (item of config().navigation; track item.id) {
<li class="ui-lp-header__mobile-item">
@if (item.children && item.children.length > 0) {
<!-- Mobile Dropdown -->
<button
class="ui-lp-header__mobile-link ui-lp-header__mobile-link--dropdown"
[attr.aria-expanded]="openMobileDropdown() === item.id"
(click)="toggleMobileDropdown(item.id)"
type="button">
{{ item.label }}
</button>
@if (openMobileDropdown() === item.id) {
<ul class="ui-lp-header__mobile-submenu">
@for (child of item.children; track child.id) {
<li>
<a
[href]="child.url"
[routerLink]="child.route"
[target]="child.target || '_self'"
class="ui-lp-header__mobile-sublink"
(click)="handleNavClick(child)">
{{ child.label }}
</a>
</li>
}
</ul>
}
} @else {
<!-- Regular Mobile Item -->
<a
[href]="item.url"
[routerLink]="item.route"
[target]="item.target || '_self'"
class="ui-lp-header__mobile-link"
(click)="handleNavClick(item)">
{{ item.label }}
</a>
}
</li>
}
@if (config().ctaButton) {
<li class="ui-lp-header__mobile-cta">
<ui-button
[variant]="config().ctaButton!.variant"
[size]="'large'"
[fullWidth]="true"
(clicked)="handleCTAClick()">
{{ config().ctaButton!.text }}
</ui-button>
</li>
}
</ul>
</ui-container>
</nav>
}
<!-- Mobile Menu Backdrop -->
@if (mobileMenuOpen()) {
<div
class="ui-lp-header__backdrop"
(click)="closeMobileMenu()"
aria-hidden="true">
</div>
}
</header>
`,
styleUrl: './landing-header.component.scss'
})
export class LandingHeaderComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
// FontAwesome icons
faXmark = faXmark;
faBars = faBars;
config = signal<LandingHeaderConfig>({
logo: { text: 'Logo' },
navigation: [],
transparent: false,
sticky: true,
showMobileMenu: true,
size: 'xl',
theme: 'light'
});
isScrolled = signal<boolean>(false);
mobileMenuOpen = signal<boolean>(false);
openDropdown = signal<string | null>(null);
openMobileDropdown = signal<string | null>(null);
@Input() set configuration(value: LandingHeaderConfig) {
this.config.set(value);
}
@Output() navigationClicked = new EventEmitter<NavigationItem>();
@Output() ctaClicked = new EventEmitter<CTAButton>();
ngOnInit(): void {
if (this.config().sticky) {
this.setupScrollListener();
}
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
@HostListener('window:scroll', [])
onWindowScroll(): void {
if (this.config().sticky) {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
this.isScrolled.set(scrollTop > 10);
}
}
@HostListener('document:click', ['$event'])
onDocumentClick(event: Event): void {
const target = event.target as HTMLElement;
if (!target.closest('.ui-lp-header__nav-link--dropdown')) {
this.openDropdown.set(null);
}
}
@HostListener('window:resize', [])
onWindowResize(): void {
if (window.innerWidth > 768) {
this.mobileMenuOpen.set(false);
}
}
private setupScrollListener(): void {
// Initial scroll check
this.onWindowScroll();
}
toggleMobileMenu(): void {
this.mobileMenuOpen.set(!this.mobileMenuOpen());
this.openMobileDropdown.set(null);
// Prevent body scroll when mobile menu is open
document.body.style.overflow = this.mobileMenuOpen() ? 'hidden' : '';
}
closeMobileMenu(): void {
this.mobileMenuOpen.set(false);
this.openMobileDropdown.set(null);
document.body.style.overflow = '';
}
toggleDropdown(itemId: string): void {
this.openDropdown.set(this.openDropdown() === itemId ? null : itemId);
}
toggleMobileDropdown(itemId: string): void {
this.openMobileDropdown.set(this.openMobileDropdown() === itemId ? null : itemId);
}
handleNavClick(item: NavigationItem): void {
if (item.action) {
item.action();
}
this.navigationClicked.emit(item);
this.closeMobileMenu();
this.openDropdown.set(null);
}
handleCTAClick(): void {
const cta = this.config().ctaButton;
if (cta) {
cta.action();
this.ctaClicked.emit(cta);
}
}
}