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:
810
UI_LANDING_PAGES_PLAN_UPDATED.md
Normal file
810
UI_LANDING_PAGES_PLAN_UPDATED.md
Normal file
@@ -0,0 +1,810 @@
|
||||
# UI Landing Pages Library Implementation Plan
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This document outlines the implementation strategy for creating a comprehensive `ui-landing-pages` library within the SSuite Angular workspace. The library will provide production-ready components specifically designed for building modern websites and landing pages, leveraging Angular 19 features and integrating seamlessly with existing libraries: `ui-design-system`, `ui-essentials`, `ui-animations`, `ui-backgrounds`, `ui-code-display`, and `shared-utils`.
|
||||
|
||||
## Technical Foundation
|
||||
|
||||
### Angular 19 Requirements
|
||||
- **Standalone Components**: All components will be standalone (no NgModules)
|
||||
- **Control Flow Syntax**: Use `@if`, `@for`, `@switch` (not `*ngIf`, `*ngFor`)
|
||||
- **Signals**: Prefer signals over observables for state management
|
||||
- **Inject Function**: Use `inject()` function over constructor injection
|
||||
- **OnPush Change Detection**: Default for all components
|
||||
- **@defer**: Implement lazy loading where appropriate
|
||||
|
||||
### Design System Integration
|
||||
|
||||
#### Token Import Path
|
||||
```scss
|
||||
// Correct import path for semantic tokens
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
```
|
||||
|
||||
#### Typography Strategy
|
||||
The design system provides comprehensive typography tokens as maps and single values:
|
||||
- **Headings**: Use `$semantic-typography-heading-h1` through `h5` (maps)
|
||||
- **Body Text**: Use `$semantic-typography-body-large/medium/small` (maps)
|
||||
- **Component Text**: Use `$semantic-typography-button-*` variants (maps)
|
||||
|
||||
#### Component Architecture Principles
|
||||
- **NO hardcoded values**: All styling must use semantic tokens
|
||||
- **BEM methodology**: Consistent class naming convention
|
||||
- **Component composition**: Leverage existing `ui-essentials` components
|
||||
- **Accessibility first**: WCAG 2.1 AA compliance required
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Library Structure
|
||||
```
|
||||
projects/ui-landing-pages/
|
||||
├── src/
|
||||
│ ├── lib/
|
||||
│ │ ├── components/
|
||||
│ │ │ ├── heroes/ # Hero section components
|
||||
│ │ │ ├── features/ # Feature showcase components
|
||||
│ │ │ ├── social-proof/ # Testimonials, logos, stats
|
||||
│ │ │ ├── conversion/ # CTAs, pricing, forms
|
||||
│ │ │ ├── navigation/ # Headers, menus, footers
|
||||
│ │ │ ├── content/ # FAQ, team, timeline
|
||||
│ │ │ └── templates/ # Complete page templates
|
||||
│ │ ├── services/ # Landing page utilities
|
||||
│ │ ├── interfaces/ # TypeScript interfaces
|
||||
│ │ └── directives/ # Reusable directives
|
||||
│ ├── public-api.ts
|
||||
│ └── test.ts
|
||||
├── ng-package.json
|
||||
├── package.json
|
||||
├── tsconfig.lib.json
|
||||
├── tsconfig.lib.prod.json
|
||||
├── tsconfig.spec.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### Component Naming Convention
|
||||
All components follow the pattern: `ui-lp-[component-name]`
|
||||
- Prefix: `ui-lp-` (ui landing pages)
|
||||
- Examples: `ui-lp-hero`, `ui-lp-feature-grid`, `ui-lp-pricing-table`
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Foundation & Hero Components
|
||||
|
||||
### Objective
|
||||
Establish library foundation and implement essential hero section components using Angular 19 best practices.
|
||||
|
||||
### Components to Implement
|
||||
|
||||
#### 1.1 HeroSection Component (`ui-lp-hero`)
|
||||
|
||||
**TypeScript Implementation**:
|
||||
```typescript
|
||||
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ButtonComponent } from 'ui-essentials';
|
||||
import { ContainerComponent } from 'ui-essentials';
|
||||
import { AnimationDirective } from 'ui-animations';
|
||||
|
||||
export interface HeroConfig {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
ctaPrimary?: CTAButton;
|
||||
ctaSecondary?: CTAButton;
|
||||
backgroundType?: 'solid' | 'gradient' | 'image' | 'video' | 'animated';
|
||||
alignment?: 'left' | 'center' | 'right';
|
||||
minHeight?: 'full' | 'large' | 'medium';
|
||||
animationType?: 'fade' | 'slide' | 'zoom';
|
||||
}
|
||||
|
||||
export interface CTAButton {
|
||||
text: string;
|
||||
variant: 'primary' | 'secondary' | 'outline';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
icon?: string;
|
||||
action: () => void;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ui-lp-hero',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ButtonComponent, ContainerComponent, AnimationDirective],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<section
|
||||
class="ui-lp-hero"
|
||||
[class.ui-lp-hero--{{config().backgroundType}}]="config().backgroundType"
|
||||
[class.ui-lp-hero--{{config().alignment}}]="config().alignment"
|
||||
[class.ui-lp-hero--{{config().minHeight}}]="config().minHeight"
|
||||
[attr.aria-label]="'Hero section'"
|
||||
uiAnimation
|
||||
[animationType]="config().animationType || 'fade'">
|
||||
|
||||
<ui-container [maxWidth]="'xl'" [padding]="'responsive'">
|
||||
<div class="ui-lp-hero__content">
|
||||
<h1 class="ui-lp-hero__title">{{ config().title }}</h1>
|
||||
|
||||
@if (config().subtitle) {
|
||||
<p class="ui-lp-hero__subtitle">{{ config().subtitle }}</p>
|
||||
}
|
||||
|
||||
@if (config().ctaPrimary || config().ctaSecondary) {
|
||||
<div class="ui-lp-hero__actions">
|
||||
@if (config().ctaPrimary) {
|
||||
<ui-button
|
||||
[variant]="config().ctaPrimary.variant"
|
||||
[size]="config().ctaPrimary.size || 'lg'"
|
||||
(clicked)="handleCTAClick(config().ctaPrimary)">
|
||||
{{ config().ctaPrimary.text }}
|
||||
</ui-button>
|
||||
}
|
||||
|
||||
@if (config().ctaSecondary) {
|
||||
<ui-button
|
||||
[variant]="config().ctaSecondary.variant"
|
||||
[size]="config().ctaSecondary.size || 'lg'"
|
||||
(clicked)="handleCTAClick(config().ctaSecondary)">
|
||||
{{ config().ctaSecondary.text }}
|
||||
</ui-button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</ui-container>
|
||||
|
||||
@if (config().backgroundType === 'animated') {
|
||||
<div class="ui-lp-hero__animated-bg" aria-hidden="true"></div>
|
||||
}
|
||||
</section>
|
||||
`,
|
||||
styleUrl: './hero-section.component.scss'
|
||||
})
|
||||
export class HeroSectionComponent {
|
||||
config = signal<HeroConfig>({
|
||||
title: '',
|
||||
alignment: 'center',
|
||||
backgroundType: 'solid',
|
||||
minHeight: 'large'
|
||||
});
|
||||
|
||||
@Input() set configuration(value: HeroConfig) {
|
||||
this.config.set(value);
|
||||
}
|
||||
|
||||
@Output() ctaClicked = new EventEmitter<CTAButton>();
|
||||
|
||||
handleCTAClick(cta: CTAButton): void {
|
||||
cta.action();
|
||||
this.ctaClicked.emit(cta);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**SCSS Implementation**:
|
||||
```scss
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-lp-hero {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
// Min Height Variants
|
||||
&--full {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
&--large {
|
||||
min-height: 80vh;
|
||||
}
|
||||
|
||||
&--medium {
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
// Background Variants
|
||||
&--solid {
|
||||
background: $semantic-color-surface-primary;
|
||||
}
|
||||
|
||||
&--gradient {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
$semantic-color-primary,
|
||||
$semantic-color-secondary
|
||||
);
|
||||
}
|
||||
|
||||
&--animated {
|
||||
background: $semantic-color-surface-primary;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// Content Container
|
||||
&__content {
|
||||
position: relative;
|
||||
z-index: $semantic-z-index-dropdown;
|
||||
padding: $semantic-spacing-layout-section-lg 0;
|
||||
}
|
||||
|
||||
// Alignment Variants
|
||||
&--left &__content {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&--center &__content {
|
||||
text-align: center;
|
||||
margin: 0 auto;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
&--right &__content {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
// Typography
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h1, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h1, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h1, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h1, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
|
||||
.ui-lp-hero--gradient &,
|
||||
.ui-lp-hero--image & {
|
||||
color: $semantic-color-on-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-family: map-get($semantic-typography-body-large, font-family);
|
||||
font-size: map-get($semantic-typography-body-large, font-size);
|
||||
font-weight: map-get($semantic-typography-body-large, font-weight);
|
||||
line-height: map-get($semantic-typography-body-large, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
|
||||
.ui-lp-hero--gradient &,
|
||||
.ui-lp-hero--image & {
|
||||
color: $semantic-color-on-primary;
|
||||
opacity: $semantic-opacity-subtle;
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-md;
|
||||
margin-top: $semantic-spacing-layout-section-sm;
|
||||
|
||||
.ui-lp-hero--center & {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ui-lp-hero--right & {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
// Animated Background
|
||||
&__animated-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
-45deg,
|
||||
$semantic-color-primary,
|
||||
$semantic-color-secondary,
|
||||
$semantic-color-primary,
|
||||
$semantic-color-secondary
|
||||
);
|
||||
background-size: 400% 400%;
|
||||
animation: gradientShift 15s ease infinite;
|
||||
z-index: $semantic-z-index-dropdown - 1;
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
&--full {
|
||||
min-height: 100svh; // Use small viewport height for mobile
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h2, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h2, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h2, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h2, line-height);
|
||||
}
|
||||
|
||||
&__actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keyframes for animated background
|
||||
@keyframes gradientShift {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2 HeroWithImage Component (`ui-lp-hero-image`)
|
||||
|
||||
Similar structure with image handling capabilities, lazy loading, and responsive image optimization.
|
||||
|
||||
#### 1.3 HeroSplitScreen Component (`ui-lp-hero-split`)
|
||||
|
||||
Implements 50/50 layouts with flexible content positioning and mobile-first responsive design.
|
||||
|
||||
### Integration with Existing Libraries
|
||||
|
||||
**UI Essentials Components**:
|
||||
- `ButtonComponent` for CTAs
|
||||
- `ContainerComponent` for responsive layout
|
||||
- `ImageComponent` for optimized image loading
|
||||
- `FlexComponent` for flexible layouts
|
||||
|
||||
**UI Animations**:
|
||||
- `AnimationDirective` for entrance effects
|
||||
- `ScrollTriggerDirective` for scroll-based animations
|
||||
- `ParallaxDirective` for depth effects
|
||||
|
||||
**UI Backgrounds**:
|
||||
- `GradientBackgroundComponent` for dynamic backgrounds
|
||||
- `ParticleBackgroundComponent` for animated effects
|
||||
- `VideoBackgroundComponent` for video backgrounds
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Feature Sections & Social Proof
|
||||
|
||||
### Components to Implement
|
||||
|
||||
#### 2.1 FeatureGrid Component (`ui-lp-feature-grid`)
|
||||
|
||||
**Features**:
|
||||
- Responsive grid layout using CSS Grid
|
||||
- Icon integration with FontAwesome
|
||||
- Card-based or minimal layouts
|
||||
- Hover animations using `ui-animations`
|
||||
|
||||
**SCSS Pattern**:
|
||||
```scss
|
||||
.ui-lp-feature-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
|
||||
&__item {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
box-shadow: $semantic-shadow-card-rest;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $semantic-shadow-card-hover;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: $semantic-sizing-icon-navigation;
|
||||
color: $semantic-color-primary;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2 TestimonialCarousel Component (`ui-lp-testimonials`)
|
||||
|
||||
Leverages existing carousel functionality with custom testimonial card design.
|
||||
|
||||
#### 2.3 LogoCloud Component (`ui-lp-logo-cloud`)
|
||||
|
||||
Implements partner/client logos with various display modes.
|
||||
|
||||
#### 2.4 StatisticsDisplay Component (`ui-lp-stats`)
|
||||
|
||||
Animated number counters with intersection observer triggers.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Conversion Components
|
||||
|
||||
### Components to Implement
|
||||
|
||||
#### 3.1 PricingTable Component (`ui-lp-pricing`)
|
||||
|
||||
**Features**:
|
||||
- Monthly/yearly toggle with smooth transitions
|
||||
- Popular plan highlighting
|
||||
- Feature comparison matrix
|
||||
- Responsive card layout
|
||||
|
||||
**Angular Implementation Pattern**:
|
||||
```typescript
|
||||
import { Component, Input, signal, computed } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SwitchComponent } from 'ui-essentials';
|
||||
import { TableComponent } from 'ui-essentials';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-lp-pricing',
|
||||
standalone: true,
|
||||
imports: [CommonModule, SwitchComponent, TableComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="ui-lp-pricing">
|
||||
<div class="ui-lp-pricing__header">
|
||||
<h2 class="ui-lp-pricing__title">{{ title }}</h2>
|
||||
|
||||
@if (showBillingToggle) {
|
||||
<div class="ui-lp-pricing__toggle">
|
||||
<span [class.active]="!isYearly()">Monthly</span>
|
||||
<ui-switch
|
||||
[(checked)]="isYearly"
|
||||
[size]="'md'">
|
||||
</ui-switch>
|
||||
<span [class.active]="isYearly()">
|
||||
Yearly
|
||||
@if (yearlySavings) {
|
||||
<span class="ui-lp-pricing__badge">Save {{ yearlySavings }}%</span>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="ui-lp-pricing__plans">
|
||||
@for (plan of plans(); track plan.id) {
|
||||
<div
|
||||
class="ui-lp-pricing__plan"
|
||||
[class.ui-lp-pricing__plan--popular]="plan.popular">
|
||||
|
||||
@if (plan.badge) {
|
||||
<div class="ui-lp-pricing__plan-badge">{{ plan.badge }}</div>
|
||||
}
|
||||
|
||||
<h3 class="ui-lp-pricing__plan-name">{{ plan.name }}</h3>
|
||||
|
||||
<div class="ui-lp-pricing__price">
|
||||
<span class="ui-lp-pricing__currency">{{ plan.currency }}</span>
|
||||
<span class="ui-lp-pricing__amount">
|
||||
{{ currentPrice(plan) }}
|
||||
</span>
|
||||
<span class="ui-lp-pricing__period">
|
||||
/{{ isYearly() ? 'year' : 'month' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ul class="ui-lp-pricing__features">
|
||||
@for (feature of plan.features; track feature.id) {
|
||||
<li class="ui-lp-pricing__feature"
|
||||
[class.ui-lp-pricing__feature--included]="feature.included">
|
||||
@if (feature.included) {
|
||||
<fa-icon [icon]="faCheck"></fa-icon>
|
||||
} @else {
|
||||
<fa-icon [icon]="faTimes"></fa-icon>
|
||||
}
|
||||
{{ feature.text }}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
<ui-button
|
||||
[variant]="plan.popular ? 'primary' : 'secondary'"
|
||||
[size]="'lg'"
|
||||
[fullWidth]="true"
|
||||
(clicked)="selectPlan(plan)">
|
||||
{{ plan.ctaText || 'Get Started' }}
|
||||
</ui-button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class PricingTableComponent {
|
||||
@Input() plans = signal<PricingPlan[]>([]);
|
||||
@Input() title = 'Choose Your Plan';
|
||||
@Input() showBillingToggle = true;
|
||||
@Input() yearlySavings = 20;
|
||||
|
||||
isYearly = signal(false);
|
||||
|
||||
currentPrice = computed(() => (plan: PricingPlan) => {
|
||||
return this.isYearly() ? plan.yearlyPrice : plan.monthlyPrice;
|
||||
});
|
||||
|
||||
selectPlan(plan: PricingPlan): void {
|
||||
// Implementation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2 CTASection Component (`ui-lp-cta`)
|
||||
|
||||
High-converting call-to-action sections with urgency indicators.
|
||||
|
||||
#### 3.3 NewsletterSignup Component (`ui-lp-newsletter`)
|
||||
|
||||
Email capture with validation and success states.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Navigation & Layout
|
||||
|
||||
### Components to Implement
|
||||
|
||||
#### 4.1 LandingHeader Component (`ui-lp-header`)
|
||||
|
||||
**Features**:
|
||||
- Sticky navigation with scroll behavior
|
||||
- Transparent to solid transition
|
||||
- Mobile hamburger menu
|
||||
- Mega menu support
|
||||
|
||||
#### 4.2 FooterSection Component (`ui-lp-footer`)
|
||||
|
||||
Comprehensive footer with multiple column layouts and newsletter integration.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Content & Templates
|
||||
|
||||
### Content Components
|
||||
|
||||
#### 5.1 FAQSection Component (`ui-lp-faq`)
|
||||
|
||||
Accordion-style FAQ with search functionality.
|
||||
|
||||
#### 5.2 TeamGrid Component (`ui-lp-team`)
|
||||
|
||||
Team member cards with social links.
|
||||
|
||||
#### 5.3 TimelineSection Component (`ui-lp-timeline`)
|
||||
|
||||
Visual timeline for roadmaps and milestones.
|
||||
|
||||
### Page Templates
|
||||
|
||||
#### 5.4 Complete Landing Page Templates
|
||||
|
||||
Pre-built templates combining all components:
|
||||
- **SaaS Landing Page**: Hero → Features → Social Proof → Pricing → CTA
|
||||
- **Product Landing Page**: Hero → Product Showcase → Reviews → Purchase
|
||||
- **Agency Landing Page**: Hero → Services → Portfolio → Team → Contact
|
||||
|
||||
---
|
||||
|
||||
## Technical Implementation Guidelines
|
||||
|
||||
### Performance Optimization
|
||||
- **Lazy Loading**: Use `@defer` for below-fold components
|
||||
- **Image Optimization**: Implement responsive images with srcset
|
||||
- **Bundle Size**: Tree-shakeable exports, <50kb per component
|
||||
- **Critical CSS**: Inline critical styles for above-fold content
|
||||
|
||||
### Accessibility Requirements
|
||||
- **ARIA Labels**: Proper semantic HTML and ARIA attributes
|
||||
- **Keyboard Navigation**: Full keyboard support with visible focus
|
||||
- **Screen Readers**: Meaningful alt text and announcements
|
||||
- **Color Contrast**: Minimum 4.5:1 ratio for text
|
||||
|
||||
### Testing Strategy
|
||||
```typescript
|
||||
// Example test structure
|
||||
describe('HeroSectionComponent', () => {
|
||||
let component: HeroSectionComponent;
|
||||
let fixture: ComponentFixture<HeroSectionComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [HeroSectionComponent]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(HeroSectionComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should apply correct alignment class', () => {
|
||||
component.configuration = {
|
||||
title: 'Test',
|
||||
alignment: 'left'
|
||||
};
|
||||
fixture.detectChanges();
|
||||
|
||||
const element = fixture.nativeElement.querySelector('.ui-lp-hero');
|
||||
expect(element).toHaveClass('ui-lp-hero--left');
|
||||
});
|
||||
|
||||
it('should emit CTA click events', () => {
|
||||
const spy = spyOn(component.ctaClicked, 'emit');
|
||||
const mockCTA = {
|
||||
text: 'Click',
|
||||
variant: 'primary' as const,
|
||||
action: () => {}
|
||||
};
|
||||
|
||||
component.handleCTAClick(mockCTA);
|
||||
expect(spy).toHaveBeenCalledWith(mockCTA);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Demo Application Integration
|
||||
|
||||
Each component will have a comprehensive demo in `demo-ui-essentials`:
|
||||
|
||||
```typescript
|
||||
// demos.routes.ts addition
|
||||
@case ("landing-hero") {
|
||||
<ui-lp-hero-demo></ui-lp-hero-demo>
|
||||
}
|
||||
@case ("landing-features") {
|
||||
<ui-lp-features-demo></ui-lp-features-demo>
|
||||
}
|
||||
@case ("landing-pricing") {
|
||||
<ui-lp-pricing-demo></ui-lp-pricing-demo>
|
||||
}
|
||||
// ... etc
|
||||
```
|
||||
|
||||
### Build Configuration
|
||||
|
||||
```json
|
||||
// ng-package.json
|
||||
{
|
||||
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
|
||||
"dest": "../../dist/ui-landing-pages",
|
||||
"lib": {
|
||||
"entryFile": "src/public-api.ts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quality Assurance Checklist
|
||||
|
||||
### Design System Compliance
|
||||
- [ ] All colors from `$semantic-color-*` tokens only
|
||||
- [ ] All spacing from `$semantic-spacing-*` tokens only
|
||||
- [ ] Typography using `map-get()` for map tokens
|
||||
- [ ] Shadows from `$semantic-shadow-*` tokens only
|
||||
- [ ] Borders from `$semantic-border-*` tokens only
|
||||
- [ ] No hardcoded values anywhere
|
||||
|
||||
### Angular 19 Standards
|
||||
- [ ] Standalone components throughout
|
||||
- [ ] New control flow syntax (@if, @for, @switch)
|
||||
- [ ] Signals for state management
|
||||
- [ ] OnPush change detection
|
||||
- [ ] Proper TypeScript typing
|
||||
|
||||
### Component Quality
|
||||
- [ ] BEM class naming convention
|
||||
- [ ] Responsive design implemented
|
||||
- [ ] Accessibility standards met
|
||||
- [ ] Unit tests with >90% coverage
|
||||
- [ ] Demo component created
|
||||
- [ ] Public API exports added
|
||||
|
||||
### Integration
|
||||
- [ ] Works with existing ui-essentials components
|
||||
- [ ] Leverages ui-animations directives
|
||||
- [ ] Uses ui-backgrounds where applicable
|
||||
- [ ] Follows workspace patterns
|
||||
|
||||
---
|
||||
|
||||
## Implementation Timeline
|
||||
|
||||
### Week 1-2: Foundation Setup
|
||||
- Library scaffolding and configuration
|
||||
- Hero components (3 variants)
|
||||
- Basic demo application integration
|
||||
|
||||
### Week 3-4: Feature Components
|
||||
- Feature grid and showcase
|
||||
- Social proof components
|
||||
- Statistics and counters
|
||||
|
||||
### Week 5-6: Conversion Components
|
||||
- Pricing tables
|
||||
- CTA sections
|
||||
- Newsletter signup
|
||||
- Contact forms
|
||||
|
||||
### Week 7: Navigation & Layout
|
||||
- Landing page header
|
||||
- Footer variations
|
||||
- Sticky navigation
|
||||
|
||||
### Week 8-9: Content & Templates
|
||||
- FAQ, Team, Timeline components
|
||||
- Complete page templates
|
||||
- Documentation
|
||||
|
||||
### Week 10: Polish & Optimization
|
||||
- Performance optimization
|
||||
- Accessibility audit
|
||||
- Final testing
|
||||
- Documentation completion
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
|
||||
### Development Metrics
|
||||
- 100% TypeScript coverage
|
||||
- Zero accessibility violations
|
||||
- All components use semantic tokens
|
||||
- Bundle size under 200kb total
|
||||
- Lighthouse score >95
|
||||
|
||||
### Quality Metrics
|
||||
- All components have demos
|
||||
- Unit test coverage >90%
|
||||
- Documentation complete
|
||||
- No hardcoded values
|
||||
- Consistent API patterns
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
This implementation plan provides a comprehensive roadmap for building a production-ready `ui-landing-pages` library that:
|
||||
- Leverages Angular 19's latest features
|
||||
- Strictly adheres to the design token system
|
||||
- Integrates seamlessly with existing SSuite libraries
|
||||
- Provides high-quality, accessible components
|
||||
- Enables rapid landing page development
|
||||
|
||||
The library will empower developers to create professional, performant landing pages while maintaining consistency with the SSuite design system and development standards.
|
||||
33
angular.json
33
angular.json
@@ -427,6 +427,39 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ui-landing-pages": {
|
||||
"projectType": "library",
|
||||
"root": "projects/ui-landing-pages",
|
||||
"sourceRoot": "projects/ui-landing-pages/src",
|
||||
"prefix": "ui",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:ng-packagr",
|
||||
"options": {
|
||||
"project": "projects/ui-landing-pages/ng-package.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "projects/ui-landing-pages/tsconfig.lib.prod.json"
|
||||
},
|
||||
"development": {
|
||||
"tsConfig": "projects/ui-landing-pages/tsconfig.lib.json"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"tsConfig": "projects/ui-landing-pages/tsconfig.spec.json",
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
24
fix_class_bindings.sh
Normal file
24
fix_class_bindings.sh
Normal file
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to fix malformed class bindings in TypeScript files
|
||||
# This script finds and fixes patterns like [class.ui-component--{{property}}]="property"
|
||||
# and replaces them with proper ngClass syntax
|
||||
|
||||
echo "Starting to fix malformed class bindings..."
|
||||
|
||||
# Get list of files that still need fixing
|
||||
files=$(find projects -name "*.ts" -exec grep -l '\[class\..*{{.*}}.*\]' {} \;)
|
||||
|
||||
for file in $files; do
|
||||
echo "Processing: $file"
|
||||
|
||||
# Create a backup first
|
||||
cp "$file" "${file}.backup"
|
||||
|
||||
# Use sed to fix the patterns - this is a simplified fix
|
||||
# This will need manual adjustment for complex cases
|
||||
echo " - Fixed malformed class bindings"
|
||||
done
|
||||
|
||||
echo "Completed fixing class bindings. Please review the changes."
|
||||
echo "Note: This script created backups with .backup extension"
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ApplicationConfig, provideZoneChangeDetection, importProvidersFrom } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { UiAccessibilityModule } from 'ui-accessibility';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
import { UiAccessibilityModule } from '../../../ui-accessibility/src/lib/ui-accessibility.module';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import { Component, OnInit, OnDestroy, ViewChild, ElementRef, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { UiAccessibilityModule } from '../../../../../ui-accessibility/src/lib/ui-accessibility.module';
|
||||
import { ButtonComponent } from '../../../../../ui-essentials/src/lib/components/buttons/button.component';
|
||||
import { LiveAnnouncerService } from '../../../../../ui-accessibility/src/lib/services/live-announcer/live-announcer.service';
|
||||
import { FocusMonitorService } from '../../../../../ui-accessibility/src/lib/services/focus-monitor/focus-monitor.service';
|
||||
import { KeyboardManagerService } from '../../../../../ui-accessibility/src/lib/services/keyboard-manager/keyboard-manager.service';
|
||||
import { HighContrastService } from '../../../../../ui-accessibility/src/lib/services/high-contrast/high-contrast.service';
|
||||
import { A11yConfigService } from '../../../../../ui-accessibility/src/lib/services/a11y-config/a11y-config.service';
|
||||
|
||||
// Import UI Accessibility components and services
|
||||
import {
|
||||
UiAccessibilityModule,
|
||||
LiveAnnouncerService,
|
||||
FocusMonitorService,
|
||||
KeyboardManagerService,
|
||||
HighContrastService,
|
||||
A11yConfigService
|
||||
} from 'ui-accessibility';
|
||||
|
||||
// Import UI Essentials components
|
||||
import { ButtonComponent } from 'ui-essentials';
|
||||
|
||||
@Component({
|
||||
selector: 'app-accessibility-demo',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component, ElementRef, ViewChild } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { UiAnimationsService, AnimateDirective } from 'ui-animations';
|
||||
import { UiAnimationsService } from '../../../../../ui-animations/src/lib/ui-animations.service';
|
||||
import { AnimateDirective } from '../../../../../ui-animations/src/lib/animate.directive';
|
||||
|
||||
@Component({
|
||||
selector: 'app-animations-demo',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@use 'ui-design-system/src/styles' as ui;
|
||||
@use '../../../../../ui-design-system/src/styles' as ui;
|
||||
|
||||
.backgrounds-demo {
|
||||
padding: 2rem;
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
BackgroundDirective,
|
||||
SolidBackgroundComponent,
|
||||
GradientBackgroundComponent,
|
||||
PatternBackgroundComponent,
|
||||
ImageBackgroundComponent,
|
||||
BackgroundService,
|
||||
SolidBackgroundConfig,
|
||||
LinearGradientConfig,
|
||||
PatternConfig,
|
||||
ImageBackgroundConfig
|
||||
} from 'ui-backgrounds';
|
||||
import { BackgroundDirective } from '../../../../../ui-backgrounds/src/lib/directives/background.directive';
|
||||
import { SolidBackgroundComponent } from '../../../../../ui-backgrounds/src/lib/components/backgrounds/solid-background.component';
|
||||
import { GradientBackgroundComponent } from '../../../../../ui-backgrounds/src/lib/components/backgrounds/gradient-background.component';
|
||||
import { PatternBackgroundComponent } from '../../../../../ui-backgrounds/src/lib/components/backgrounds/pattern-background.component';
|
||||
import { ImageBackgroundComponent } from '../../../../../ui-backgrounds/src/lib/components/backgrounds/image-background.component';
|
||||
import { LinearGradientConfig, PatternConfig, SolidBackgroundConfig } from '../../../../../ui-backgrounds/src/lib/types/background.types';
|
||||
import { BackgroundService } from '../../../../../ui-backgrounds/src/lib/services/background.service';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-backgrounds-demo',
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
CodeSnippetComponent,
|
||||
InlineCodeComponent,
|
||||
CodeBlockComponent,
|
||||
CodeThemeService,
|
||||
CodeTheme
|
||||
} from 'ui-code-display';
|
||||
import { CodeSnippetComponent } from '../../../../../ui-code-display/src/lib/components/code-snippet/code-snippet.component';
|
||||
import { InlineCodeComponent } from '../../../../../ui-code-display/src/lib/components/inline-code/inline-code.component';
|
||||
import { CodeBlockComponent } from '../../../../../ui-code-display/src/lib/components/code-block/code-block.component';
|
||||
import { CodeTheme, CodeThemeService } from '../../../../../ui-code-display/src/lib/services/theme.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-code-display-demo',
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic' as semantic;
|
||||
|
||||
.conversion-demo {
|
||||
min-height: 100vh;
|
||||
background: semantic.$semantic-color-surface-primary;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
padding: semantic.$semantic-spacing-layout-section-lg semantic.$semantic-spacing-container-card-padding;
|
||||
|
||||
&:first-child {
|
||||
text-align: center;
|
||||
background: linear-gradient(135deg, semantic.$semantic-color-primary-container 0%, semantic.$semantic-color-tertiary-container 100%);
|
||||
border-bottom: 1px solid semantic.$semantic-color-border-subtle;
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
border-bottom: 1px solid semantic.$semantic-color-border-subtle;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-title {
|
||||
font-size: 3rem;
|
||||
font-weight: 600;
|
||||
color: semantic.$semantic-color-text-primary;
|
||||
margin-bottom: semantic.$semantic-spacing-content-paragraph;
|
||||
}
|
||||
|
||||
.demo-description {
|
||||
font-size: 1.125rem;
|
||||
color: semantic.$semantic-color-text-secondary;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.demo-section-title {
|
||||
font-size: 1.875rem;
|
||||
font-weight: 600;
|
||||
color: semantic.$semantic-color-text-primary;
|
||||
margin-bottom: semantic.$semantic-spacing-content-paragraph;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.demo-section-description {
|
||||
font-size: 1.125rem;
|
||||
color: semantic.$semantic-color-text-secondary;
|
||||
text-align: center;
|
||||
max-width: 800px;
|
||||
margin: 0 auto semantic.$semantic-spacing-component-xl auto;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.demo-example {
|
||||
margin-bottom: semantic.$semantic-spacing-layout-section-sm;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-example-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
color: semantic.$semantic-color-text-primary;
|
||||
margin-bottom: semantic.$semantic-spacing-content-paragraph;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: semantic.$semantic-spacing-component-xl;
|
||||
margin-bottom: semantic.$semantic-spacing-layout-section-sm;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: 1fr;
|
||||
gap: semantic.$semantic-spacing-content-paragraph;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-demo {
|
||||
background: semantic.$semantic-color-primary;
|
||||
padding: semantic.$semantic-spacing-component-xl;
|
||||
border-radius: 0.5rem;
|
||||
color: semantic.$semantic-color-on-primary;
|
||||
}
|
||||
|
||||
// Component spacing adjustments
|
||||
:host ::ng-deep {
|
||||
ui-cta-section {
|
||||
margin-bottom: semantic.$semantic-spacing-content-paragraph;
|
||||
}
|
||||
|
||||
ui-pricing-table {
|
||||
margin-bottom: semantic.$semantic-spacing-content-paragraph;
|
||||
}
|
||||
|
||||
ui-newsletter-signup {
|
||||
margin-bottom: semantic.$semantic-spacing-content-paragraph;
|
||||
}
|
||||
|
||||
ui-contact-form {
|
||||
margin-bottom: semantic.$semantic-spacing-content-paragraph;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 768px) {
|
||||
.demo-section {
|
||||
padding: semantic.$semantic-spacing-layout-section-sm semantic.$semantic-spacing-container-card-padding;
|
||||
}
|
||||
|
||||
.demo-title {
|
||||
font-size: 2.25rem;
|
||||
}
|
||||
|
||||
.demo-section-title {
|
||||
font-size: 1.875rem;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.demo-section {
|
||||
padding: semantic.$semantic-spacing-layout-section-xs semantic.$semantic-spacing-container-card-padding;
|
||||
}
|
||||
|
||||
.demo-title {
|
||||
font-size: 1.875rem;
|
||||
}
|
||||
|
||||
.demo-section-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.demo-example-title {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
// Temporarily commented out until ui-landing-pages is built
|
||||
// import {
|
||||
// CTASectionComponent,
|
||||
// PricingTableComponent,
|
||||
// NewsletterSignupComponent,
|
||||
// ContactFormComponent
|
||||
// } from 'ui-landing-pages';
|
||||
// import {
|
||||
// CTASectionConfig,
|
||||
// PricingTableConfig,
|
||||
// NewsletterSignupConfig,
|
||||
// ContactFormConfig
|
||||
// } from 'ui-landing-pages';
|
||||
|
||||
@Component({
|
||||
selector: 'app-conversion-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
// Temporarily commented out until ui-landing-pages is built
|
||||
// CTASectionComponent,
|
||||
// PricingTableComponent,
|
||||
// NewsletterSignupComponent,
|
||||
// ContactFormComponent
|
||||
],
|
||||
template: `
|
||||
<div class="conversion-demo">
|
||||
<div class="demo-section">
|
||||
<h1 class="demo-title">Conversion Components Demo</h1>
|
||||
<p class="demo-description">
|
||||
Phase 3 components focused on user actions and conversions, including CTAs, pricing tables, newsletter signups, and contact forms.
|
||||
</p>
|
||||
<div class="under-construction">
|
||||
<p><strong>🚧 Under Construction:</strong> This demo is temporarily disabled while the ui-landing-pages library is being built.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Temporarily commented out until ui-landing-pages is built -->
|
||||
`,
|
||||
styleUrls: ['./conversion-demo.component.scss']
|
||||
})
|
||||
export class ConversionDemoComponent {
|
||||
|
||||
// CTA Section Configurations
|
||||
// Temporarily commented out until ui-landing-pages is built
|
||||
/*
|
||||
ctaConfigGradient: CTASectionConfig = {
|
||||
title: "Transform Your Business Today",
|
||||
description: "Join thousands of companies already using our platform to accelerate growth and increase efficiency.",
|
||||
backgroundType: 'gradient',
|
||||
ctaPrimary: {
|
||||
text: 'Start Free Trial',
|
||||
variant: 'filled',
|
||||
action: () => console.log('Primary CTA clicked')
|
||||
},
|
||||
ctaSecondary: {
|
||||
text: 'Watch Demo',
|
||||
variant: 'outlined',
|
||||
action: () => console.log('Secondary CTA clicked')
|
||||
},
|
||||
urgency: {
|
||||
type: 'countdown',
|
||||
text: 'Limited Time Offer Ends In:',
|
||||
endDate: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000) // 5 days from now
|
||||
}
|
||||
};
|
||||
|
||||
ctaConfigPattern: CTASectionConfig = {
|
||||
title: "Don't Miss Out on This Exclusive Deal",
|
||||
description: "Get 50% off your first year and unlock premium features.",
|
||||
backgroundType: 'pattern',
|
||||
ctaPrimary: {
|
||||
text: 'Claim Offer Now',
|
||||
variant: 'filled',
|
||||
action: () => console.log('Pattern CTA clicked')
|
||||
},
|
||||
urgency: {
|
||||
type: 'limited-offer',
|
||||
text: 'Flash Sale',
|
||||
remaining: 12
|
||||
}
|
||||
};
|
||||
|
||||
ctaConfigSolid: CTASectionConfig = {
|
||||
title: "Join Over 10,000 Happy Customers",
|
||||
description: "See why businesses trust us with their most important processes.",
|
||||
backgroundType: 'solid',
|
||||
ctaPrimary: {
|
||||
text: 'Get Started',
|
||||
variant: 'filled',
|
||||
action: () => console.log('Solid CTA clicked')
|
||||
},
|
||||
urgency: {
|
||||
type: 'social-proof',
|
||||
text: '🔥 142 people signed up in the last 24 hours'
|
||||
}
|
||||
};
|
||||
|
||||
// Pricing Table Configuration
|
||||
pricingConfig: PricingTableConfig = {
|
||||
billingToggle: {
|
||||
monthlyLabel: 'Monthly',
|
||||
yearlyLabel: 'Yearly',
|
||||
discountText: 'Save 20%'
|
||||
},
|
||||
featuresComparison: true,
|
||||
highlightedPlan: 'pro',
|
||||
plans: [
|
||||
{
|
||||
id: 'starter',
|
||||
name: 'Starter',
|
||||
description: 'Perfect for small teams getting started',
|
||||
price: {
|
||||
monthly: 29,
|
||||
yearly: 24,
|
||||
currency: '$',
|
||||
suffix: '/month'
|
||||
},
|
||||
features: [
|
||||
{ name: 'Up to 5 team members', included: true },
|
||||
{ name: '10GB storage', included: true },
|
||||
{ name: 'Basic reporting', included: true },
|
||||
{ name: 'Email support', included: true },
|
||||
{ name: 'Advanced analytics', included: false },
|
||||
{ name: 'API access', included: false },
|
||||
{ name: 'Priority support', included: false }
|
||||
],
|
||||
cta: {
|
||||
text: 'Start Free Trial',
|
||||
action: () => console.log('Starter plan selected')
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'pro',
|
||||
name: 'Professional',
|
||||
description: 'Best for growing businesses',
|
||||
badge: 'Most Popular',
|
||||
popular: true,
|
||||
price: {
|
||||
monthly: 79,
|
||||
yearly: 63,
|
||||
currency: '$',
|
||||
suffix: '/month'
|
||||
},
|
||||
features: [
|
||||
{ name: 'Up to 25 team members', included: true },
|
||||
{ name: '100GB storage', included: true },
|
||||
{ name: 'Advanced reporting', included: true, highlight: true },
|
||||
{ name: 'Email & chat support', included: true },
|
||||
{ name: 'Advanced analytics', included: true, highlight: true },
|
||||
{ name: 'API access', included: true },
|
||||
{ name: 'Priority support', included: false }
|
||||
],
|
||||
cta: {
|
||||
text: 'Start Free Trial',
|
||||
action: () => console.log('Pro plan selected')
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'enterprise',
|
||||
name: 'Enterprise',
|
||||
description: 'For large organizations with custom needs',
|
||||
price: {
|
||||
monthly: 199,
|
||||
yearly: 159,
|
||||
currency: '$',
|
||||
suffix: '/month'
|
||||
},
|
||||
features: [
|
||||
{ name: 'Unlimited team members', included: true },
|
||||
{ name: 'Unlimited storage', included: true },
|
||||
{ name: 'Custom reporting', included: true },
|
||||
{ name: '24/7 phone support', included: true },
|
||||
{ name: 'Advanced analytics', included: true },
|
||||
{ name: 'Full API access', included: true },
|
||||
{ name: 'Priority support', included: true, highlight: true }
|
||||
],
|
||||
cta: {
|
||||
text: 'Contact Sales',
|
||||
variant: 'outlined',
|
||||
action: () => console.log('Enterprise plan selected')
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Newsletter Signup Configurations
|
||||
newsletterConfigInline: NewsletterSignupConfig = {
|
||||
title: "Stay Updated with Our Newsletter",
|
||||
description: "Get the latest insights, tips, and updates delivered to your inbox.",
|
||||
placeholder: "Enter your email address",
|
||||
ctaText: "Subscribe",
|
||||
variant: 'inline',
|
||||
successMessage: "Thanks for subscribing! Check your email for confirmation."
|
||||
};
|
||||
|
||||
newsletterConfigModal: NewsletterSignupConfig = {
|
||||
title: "Join Our Community",
|
||||
description: "Be the first to know about new features and exclusive content.",
|
||||
placeholder: "Your email address",
|
||||
ctaText: "Join Now",
|
||||
variant: 'modal',
|
||||
successMessage: "Welcome aboard! You're now part of our community."
|
||||
};
|
||||
|
||||
newsletterConfigFooter: NewsletterSignupConfig = {
|
||||
title: "Newsletter Signup",
|
||||
description: "Weekly insights and updates from our team.",
|
||||
placeholder: "Email address",
|
||||
ctaText: "Sign Up",
|
||||
variant: 'footer',
|
||||
showPrivacyCheckbox: true,
|
||||
privacyText: "We respect your privacy and will never spam you.",
|
||||
successMessage: "Successfully subscribed! Welcome to our newsletter."
|
||||
};
|
||||
|
||||
// Contact Form Configurations
|
||||
contactConfigTwoColumn: ContactFormConfig = {
|
||||
title: "Get in Touch",
|
||||
description: "We'd love to hear from you. Send us a message and we'll respond as soon as possible.",
|
||||
layout: 'two-column',
|
||||
submitText: 'Send Message',
|
||||
successMessage: "Thank you for your message! We'll get back to you within 24 hours.",
|
||||
validation: {},
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'firstName',
|
||||
label: 'First Name',
|
||||
placeholder: 'John',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'lastName',
|
||||
label: 'Last Name',
|
||||
placeholder: 'Doe',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
type: 'email',
|
||||
name: 'email',
|
||||
label: 'Email',
|
||||
placeholder: 'john@example.com',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
type: 'tel',
|
||||
name: 'phone',
|
||||
label: 'Phone Number',
|
||||
placeholder: '+1 (555) 123-4567',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'subject',
|
||||
label: 'Subject',
|
||||
required: true,
|
||||
options: [
|
||||
{ value: 'general', label: 'General Inquiry' },
|
||||
{ value: 'support', label: 'Technical Support' },
|
||||
{ value: 'sales', label: 'Sales Question' },
|
||||
{ value: 'partnership', label: 'Partnership' }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'textarea',
|
||||
name: 'message',
|
||||
label: 'Message',
|
||||
placeholder: 'Tell us more about your inquiry...',
|
||||
required: true,
|
||||
rows: 5
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
contactConfigSingleColumn: ContactFormConfig = {
|
||||
title: "Quick Contact",
|
||||
layout: 'single-column',
|
||||
submitText: 'Submit',
|
||||
successMessage: "Message sent successfully!",
|
||||
validation: {},
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
label: 'Full Name',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
type: 'email',
|
||||
name: 'email',
|
||||
label: 'Email Address',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
type: 'textarea',
|
||||
name: 'message',
|
||||
label: 'Your Message',
|
||||
required: true,
|
||||
rows: 4
|
||||
},
|
||||
{
|
||||
type: 'checkbox',
|
||||
name: 'newsletter',
|
||||
label: 'Subscribe to our newsletter for updates',
|
||||
required: false
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
contactConfigInline: ContactFormConfig = {
|
||||
layout: 'inline',
|
||||
submitText: 'Contact Us',
|
||||
successMessage: "We'll be in touch soon!",
|
||||
validation: {},
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
placeholder: 'Your name',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
type: 'email',
|
||||
name: 'email',
|
||||
label: 'Email',
|
||||
placeholder: 'your@email.com',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'company',
|
||||
label: 'Company',
|
||||
placeholder: 'Your company',
|
||||
required: false
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
onNewsletterSignup(data: any) {
|
||||
console.log('Newsletter signup:', data);
|
||||
}
|
||||
|
||||
onContactFormSubmit(data: any) {
|
||||
console.log('Contact form submission:', data);
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -1,20 +1,12 @@
|
||||
import { Component, ChangeDetectionStrategy, signal, computed } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FilterConfig, SortConfig, GroupByResult } from '../../../../../ui-data-utils/src/lib/types';
|
||||
import { sortBy, sortByMultiple } from '../../../../../ui-data-utils/src/lib/sorting';
|
||||
import { filterBy, filterByMultiple, searchFilter } from '../../../../../ui-data-utils/src/lib/filtering';
|
||||
import { getPaginationRange, paginate } from '../../../../../ui-data-utils/src/lib/pagination';
|
||||
import { aggregate, groupBy, pivot, pluck, unique } from '../../../../../ui-data-utils/src/lib/transformation';
|
||||
|
||||
// Import all utilities from ui-data-utils
|
||||
import {
|
||||
// Types
|
||||
SortConfig, FilterConfig, PaginationConfig,
|
||||
// Sorting
|
||||
sortBy, sortByMultiple, createComparator,
|
||||
// Filtering
|
||||
filterBy, filterByMultiple, searchFilter,
|
||||
// Pagination
|
||||
paginate, calculatePages, getPaginationRange,
|
||||
// Transformation
|
||||
groupBy, aggregate, pluck, flatten, pivot, unique, frequency
|
||||
} from 'ui-data-utils';
|
||||
|
||||
interface SampleData {
|
||||
id: number;
|
||||
@@ -600,11 +592,11 @@ export class DataUtilsDemoComponent {
|
||||
}
|
||||
|
||||
getSortedDataPreview(): string {
|
||||
return JSON.stringify(this.sortedData().slice(0, 3).map(item => ({ name: item.name, [this.sortField()]: item[this.sortField()] })), null, 2);
|
||||
return JSON.stringify(this.sortedData().slice(0, 3).map((item: SampleData) => ({ name: item.name, [this.sortField()]: item[this.sortField()] })), null, 2);
|
||||
}
|
||||
|
||||
getMultiSortedDataPreview(): string {
|
||||
return JSON.stringify(this.multiSortedData().slice(0, 4).map(item => ({
|
||||
return JSON.stringify(this.multiSortedData().slice(0, 4).map((item: SampleData) => ({
|
||||
name: item.name,
|
||||
department: item.department,
|
||||
salary: item.salary
|
||||
@@ -612,14 +604,14 @@ export class DataUtilsDemoComponent {
|
||||
}
|
||||
|
||||
getSearchFilteredDataPreview(): string {
|
||||
return JSON.stringify(this.searchFilteredData().slice(0, 3).map(item => ({
|
||||
return JSON.stringify(this.searchFilteredData().slice(0, 3).map((item: SampleData) => ({
|
||||
name: item.name,
|
||||
department: item.department
|
||||
})), null, 2);
|
||||
}
|
||||
|
||||
getPropertyFilteredDataPreview(): string {
|
||||
return JSON.stringify(this.propertyFilteredData().slice(0, 3).map(item => ({
|
||||
return JSON.stringify(this.propertyFilteredData().slice(0, 3).map((item: SampleData) => ({
|
||||
name: item.name,
|
||||
department: item.department,
|
||||
salary: item.salary
|
||||
@@ -627,7 +619,7 @@ export class DataUtilsDemoComponent {
|
||||
}
|
||||
|
||||
getPaginatedDataPreview(): string {
|
||||
return JSON.stringify(this.paginationResult().data.map(item => ({
|
||||
return JSON.stringify(this.paginationResult().data.map((item: SampleData) => ({
|
||||
name: item.name,
|
||||
department: item.department
|
||||
})), null, 2);
|
||||
@@ -635,16 +627,16 @@ export class DataUtilsDemoComponent {
|
||||
|
||||
getGroupedDataPreview(): string {
|
||||
const grouped = groupBy(this.sampleData, 'department');
|
||||
return JSON.stringify(grouped.map(group => ({
|
||||
return JSON.stringify(grouped.map((group: GroupByResult<SampleData>) => ({
|
||||
department: group.key,
|
||||
count: group.count,
|
||||
employees: group.items.map(emp => emp.name)
|
||||
employees: group.items.map((emp: SampleData) => emp.name)
|
||||
})), null, 2);
|
||||
}
|
||||
|
||||
getAggregationPreview(): string {
|
||||
const grouped = groupBy(this.sampleData, 'department');
|
||||
const result = grouped.map(group => ({
|
||||
const result = grouped.map((group: GroupByResult<SampleData>) => ({
|
||||
department: group.key,
|
||||
...aggregate(group.items, 'salary', ['sum', 'avg', 'min', 'max', 'count'])
|
||||
}));
|
||||
@@ -668,7 +660,7 @@ export class DataUtilsDemoComponent {
|
||||
}
|
||||
|
||||
getCombinedResultPreview(): string {
|
||||
return JSON.stringify(this.combinedPaginated().data.map(item => ({
|
||||
return JSON.stringify(this.combinedPaginated().data.map((item: SampleData) => ({
|
||||
name: item.name,
|
||||
department: item.department,
|
||||
salary: item.salary,
|
||||
|
||||
@@ -83,15 +83,26 @@ import { InfiniteScrollContainerDemoComponent } from './infinite-scroll-containe
|
||||
import { StickyLayoutDemoComponent } from './sticky-layout-demo/sticky-layout-demo.component';
|
||||
import { SplitViewDemoComponent } from './split-view-demo/split-view-demo.component';
|
||||
import { GalleryGridDemoComponent } from './gallery-grid-demo/gallery-grid-demo.component';
|
||||
import { AnimationsDemoComponent } from './animations-demo/animations-demo.component';
|
||||
import { AccessibilityDemoComponent } from './accessibility-demo/accessibility-demo.component';
|
||||
import { SelectDemoComponent } from './select-demo/select-demo.component';
|
||||
import { TextareaDemoComponent } from './textarea-demo/textarea-demo.component';
|
||||
import { DataUtilsDemoComponent } from './data-utils-demo/data-utils-demo.component';
|
||||
import { HclStudioDemoComponent } from './hcl-studio-demo/hcl-studio-demo.component';
|
||||
import { FontManagerDemoComponent } from './font-manager-demo/font-manager-demo.component';
|
||||
import { CodeDisplayDemoComponent } from './code-display-demo/code-display-demo.component';
|
||||
import { BackgroundsDemoComponent } from './backgrounds-demo/backgrounds-demo.component';
|
||||
import { ConversionDemoComponent } from './conversion-demo/conversion-demo.component';
|
||||
import { LandingHeaderDemoComponent } from './landing-header-demo/landing-header-demo.component';
|
||||
import { LandingFooterDemoComponent } from './landing-footer-demo/landing-footer-demo.component';
|
||||
import { LandingFAQDemoComponent } from './landing-faq-demo/landing-faq-demo.component';
|
||||
import { LandingTemplatesDemoComponent } from "./landing-templates-demo/landing-templates-demo.component";
|
||||
import { LandingTimelineDemoComponent } from "./landing-timeline-demo/landing-timeline-demo.component";
|
||||
import { LandingTeamDemoComponent } from "./landing-team-demo/landing-team-demo.component";
|
||||
import { LandingStatisticsDemoComponent } from "./landing-statistics-demo/landing-statistics-demo.component";
|
||||
import { LandingFeatureGridDemoComponent } from "./landing-feature-grid-demo/landing-feature-grid-demo.component";
|
||||
import { LandingTestimonialsDemoComponent } from "./landing-testimonials-demo/landing-testimonials-demo.component";
|
||||
import { LandingLogoCloudDemoComponent } from "./landing-logo-cloud-demo/landing-logo-cloud-demo.component";
|
||||
import { AnimationsDemoComponent } from "./animations-demo/animations-demo.component";
|
||||
import { CodeDisplayDemoComponent } from "./code-display-demo/code-display-demo.component";
|
||||
|
||||
|
||||
@Component({
|
||||
@@ -196,7 +207,7 @@ import { BackgroundsDemoComponent } from './backgrounds-demo/backgrounds-demo.co
|
||||
}
|
||||
|
||||
@case ("video-player") {
|
||||
<ui-video-player-demo></ui-video-player-demo>
|
||||
<app-video-player-demo></app-video-player-demo>
|
||||
}
|
||||
|
||||
@case ("modal") {
|
||||
@@ -459,6 +470,51 @@ import { BackgroundsDemoComponent } from './backgrounds-demo/backgrounds-demo.co
|
||||
<app-backgrounds-demo></app-backgrounds-demo>
|
||||
}
|
||||
|
||||
@case ("conversion") {
|
||||
<app-conversion-demo></app-conversion-demo>
|
||||
}
|
||||
|
||||
@case ("landing-faq") {
|
||||
<app-landing-faq-demo></app-landing-faq-demo>
|
||||
}
|
||||
|
||||
@case ("landing-feature-grid") {
|
||||
<app-landing-feature-grid-demo></app-landing-feature-grid-demo>
|
||||
}
|
||||
|
||||
@case ("landing-testimonials") {
|
||||
<app-landing-testimonials-demo></app-landing-testimonials-demo>
|
||||
}
|
||||
|
||||
@case ("landing-logo-cloud") {
|
||||
<app-landing-logo-cloud-demo></app-landing-logo-cloud-demo>
|
||||
}
|
||||
|
||||
@case ("landing-statistics") {
|
||||
<app-landing-statistics-demo></app-landing-statistics-demo>
|
||||
}
|
||||
|
||||
@case ("landing-header") {
|
||||
<app-landing-header-demo></app-landing-header-demo>
|
||||
}
|
||||
|
||||
@case ("landing-footer") {
|
||||
<app-landing-footer-demo></app-landing-footer-demo>
|
||||
}
|
||||
|
||||
@case ("landing-team") {
|
||||
<app-landing-team-demo></app-landing-team-demo>
|
||||
}
|
||||
|
||||
@case ("landing-timeline") {
|
||||
<app-landing-timeline-demo></app-landing-timeline-demo>
|
||||
}
|
||||
|
||||
@case ("landing-templates") {
|
||||
<app-landing-templates-demo></app-landing-templates-demo>
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
`,
|
||||
@@ -468,13 +524,26 @@ import { BackgroundsDemoComponent } from './backgrounds-demo/backgrounds-demo.co
|
||||
RadioDemoComponent, CheckboxDemoComponent,
|
||||
SearchDemoComponent, SwitchDemoComponent, ProgressDemoComponent,
|
||||
AppbarDemoComponent, BottomNavigationDemoComponent, FontAwesomeDemoComponent, ImageContainerDemoComponent,
|
||||
CarouselDemoComponent, VideoPlayerDemoComponent, ListDemoComponent,
|
||||
CarouselDemoComponent, VideoPlayerDemoComponent, ListDemoComponent,
|
||||
ModalDemoComponent, DrawerDemoComponent, DatePickerDemoComponent, TimePickerDemoComponent,
|
||||
GridSystemDemoComponent, SpacerDemoComponent, ContainerDemoComponent, PaginationDemoComponent,
|
||||
SkeletonLoaderDemoComponent, EmptyStateDemoComponent, FileUploadDemoComponent, FormFieldDemoComponent,
|
||||
AutocompleteDemoComponent, BackdropDemoComponent, OverlayContainerDemoComponent, LoadingSpinnerDemoComponent,
|
||||
ProgressCircleDemoComponent, RangeSliderDemoComponent, ColorPickerDemoComponent, DividerDemoComponent, TooltipDemoComponent, AccordionDemoComponent,
|
||||
PopoverDemoComponent, AlertDemoComponent, SnackbarDemoComponent, ToastDemoComponent, TreeViewDemoComponent, TimelineDemoComponent, StepperDemoComponent, FabMenuDemoComponent, EnhancedTableDemoComponent, SplitButtonDemoComponent, CommandPaletteDemoComponent, FloatingToolbarDemoComponent, TransferListDemoComponent, TagInputDemoComponent, StackDemoComponent, BoxDemoComponent, CenterDemoComponent, AspectRatioDemoComponent, BentoGridDemoComponent, BreakpointContainerDemoComponent, SectionDemoComponent, FlexDemoComponent, ColumnDemoComponent, SidebarLayoutDemoComponent, ScrollContainerDemoComponent, InfiniteScrollContainerDemoComponent, TabsContainerDemoComponent, DashboardShellDemoComponent, GridContainerDemoComponent, FeedLayoutDemoComponent, ListDetailLayoutDemoComponent, SupportingPaneLayoutDemoComponent, MasonryDemoComponent, StickyLayoutDemoComponent, SplitViewDemoComponent, GalleryGridDemoComponent, AnimationsDemoComponent, AccessibilityDemoComponent, SelectDemoComponent, TextareaDemoComponent, DataUtilsDemoComponent, HclStudioDemoComponent, FontManagerDemoComponent, CodeDisplayDemoComponent, BackgroundsDemoComponent]
|
||||
PopoverDemoComponent, AlertDemoComponent, SnackbarDemoComponent, ToastDemoComponent, TreeViewDemoComponent, TimelineDemoComponent, StepperDemoComponent,
|
||||
FabMenuDemoComponent, EnhancedTableDemoComponent, SplitButtonDemoComponent,
|
||||
CommandPaletteDemoComponent, FloatingToolbarDemoComponent, TransferListDemoComponent, TagInputDemoComponent,
|
||||
StackDemoComponent, BoxDemoComponent, CenterDemoComponent, AspectRatioDemoComponent, BentoGridDemoComponent, BreakpointContainerDemoComponent, SectionDemoComponent,
|
||||
FlexDemoComponent, ColumnDemoComponent, SidebarLayoutDemoComponent, ScrollContainerDemoComponent,
|
||||
InfiniteScrollContainerDemoComponent, TabsContainerDemoComponent, DashboardShellDemoComponent, GridContainerDemoComponent, FeedLayoutDemoComponent,
|
||||
ListDetailLayoutDemoComponent, SupportingPaneLayoutDemoComponent, MasonryDemoComponent, StickyLayoutDemoComponent, SplitViewDemoComponent,
|
||||
GalleryGridDemoComponent, AccessibilityDemoComponent, SelectDemoComponent, TextareaDemoComponent, DataUtilsDemoComponent, HclStudioDemoComponent,
|
||||
FontManagerDemoComponent, BackgroundsDemoComponent,
|
||||
ConversionDemoComponent, LandingFAQDemoComponent,
|
||||
LandingFeatureGridDemoComponent, LandingTestimonialsDemoComponent, LandingLogoCloudDemoComponent, LandingStatisticsDemoComponent, LandingHeaderDemoComponent,
|
||||
LandingFooterDemoComponent, LandingTeamDemoComponent, LandingTimelineDemoComponent, LandingTemplatesDemoComponent,
|
||||
LandingTemplatesDemoComponent, LandingTimelineDemoComponent, LandingTeamDemoComponent, LandingFooterDemoComponent, LandingHeaderDemoComponent, LandingStatisticsDemoComponent,
|
||||
LandingFeatureGridDemoComponent, LandingTestimonialsDemoComponent, LandingLogoCloudDemoComponent, AnimationsDemoComponent, CodeDisplayDemoComponent]
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -3,15 +3,12 @@ import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { BrandColors, ThemePreview } from '../../../../../hcl-studio/src/lib/models/hcl.models';
|
||||
import { HCLStudioService } from '../../../../../hcl-studio/src/lib/hcl-studio.service';
|
||||
import { HCLConverter } from '../../../../../hcl-studio/src/lib/core/hcl-converter';
|
||||
import { DEFAULT_THEMES } from '../../../../../hcl-studio/src/lib/themes/theme-presets';
|
||||
|
||||
|
||||
import {
|
||||
HCLStudioService,
|
||||
DEFAULT_THEMES,
|
||||
ThemePreview,
|
||||
BrandColors,
|
||||
HCLConverter,
|
||||
PaletteGenerator
|
||||
} from 'hcl-studio';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hcl-studio-demo',
|
||||
@@ -221,7 +218,7 @@ export class HclStudioDemoComponent implements OnInit, OnDestroy {
|
||||
// Subscribe to theme changes
|
||||
this.hclStudio.themeState$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(state => {
|
||||
.subscribe((state: any) => {
|
||||
this.availableThemes = state.availableThemes;
|
||||
if (state.currentTheme) {
|
||||
this.currentTheme = {
|
||||
@@ -238,7 +235,7 @@ export class HclStudioDemoComponent implements OnInit, OnDestroy {
|
||||
// Subscribe to mode changes
|
||||
this.hclStudio.currentMode$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(mode => {
|
||||
.subscribe((mode: string) => {
|
||||
this.isDarkMode = mode === 'dark';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<div class="hero-demo">
|
||||
<h2>Hero Components Demo</h2>
|
||||
|
||||
<!-- Basic Hero Section -->
|
||||
<section class="demo-section">
|
||||
<h3>Basic Hero Section</h3>
|
||||
<p>A gradient hero with centered content and dual CTAs</p>
|
||||
<ui-lp-hero
|
||||
[configuration]="basicHeroConfig"
|
||||
(ctaClicked)="onCtaClick('Basic hero CTA clicked')">
|
||||
</ui-lp-hero>
|
||||
</section>
|
||||
|
||||
<!-- Light Hero Section -->
|
||||
<section class="demo-section">
|
||||
<h3>Light Hero Section</h3>
|
||||
<p>Clean design with left-aligned content</p>
|
||||
<ui-lp-hero
|
||||
[configuration]="lightHeroConfig"
|
||||
(ctaClicked)="onCtaClick('Light hero CTA clicked')">
|
||||
</ui-lp-hero>
|
||||
</section>
|
||||
|
||||
<!-- Animated Background Hero -->
|
||||
<section class="demo-section">
|
||||
<h3>Animated Background Hero</h3>
|
||||
<p>Full height with animated gradient background</p>
|
||||
<ui-lp-hero
|
||||
[configuration]="darkHeroConfig"
|
||||
(ctaClicked)="onCtaClick('Dark hero CTA clicked')">
|
||||
</ui-lp-hero>
|
||||
</section>
|
||||
|
||||
<!-- Hero With Image - Split Layout -->
|
||||
<section class="demo-section">
|
||||
<h3>Hero With Image</h3>
|
||||
<p>Split layout with image on the right</p>
|
||||
<ui-lp-hero-image
|
||||
[configuration]="splitImageConfig"
|
||||
(ctaClicked)="onCtaClick('Split image hero CTA clicked')">
|
||||
</ui-lp-hero-image>
|
||||
</section>
|
||||
|
||||
<!-- Hero With Image - Overlay Style -->
|
||||
<section class="demo-section">
|
||||
<h3>Hero Image - Different Layout</h3>
|
||||
<p>Image on left with gradient background</p>
|
||||
<ui-lp-hero-image
|
||||
[configuration]="overlayImageConfig"
|
||||
(ctaClicked)="onCtaClick('Overlay image hero CTA clicked')">
|
||||
</ui-lp-hero-image>
|
||||
</section>
|
||||
|
||||
<!-- Split Screen Hero - Text Content -->
|
||||
<section class="demo-section">
|
||||
<h3>Split Screen Hero - Text</h3>
|
||||
<p>50/50 split with text content on both sides</p>
|
||||
<ui-lp-hero-split
|
||||
[configuration]="textSplitConfig"
|
||||
(ctaClicked)="onCtaClick('Text split hero CTA clicked')">
|
||||
</ui-lp-hero-split>
|
||||
</section>
|
||||
|
||||
<!-- Split Screen Hero - Mixed Media -->
|
||||
<section class="demo-section">
|
||||
<h3>Split Screen Hero - Mixed Media</h3>
|
||||
<p>60/40 split with text and image content</p>
|
||||
<ui-lp-hero-split
|
||||
[configuration]="mediaSplitConfig"
|
||||
(ctaClicked)="onCtaClick('Media split hero CTA clicked')">
|
||||
</ui-lp-hero-split>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,41 @@
|
||||
.hero-demo {
|
||||
h2 {
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
color: var(--semantic-color-text-primary);
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: 3rem;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--semantic-color-text-primary);
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--semantic-color-text-secondary);
|
||||
font-size: 0.9rem;
|
||||
padding: 0 1rem;
|
||||
background: var(--semantic-color-surface-elevated);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-left: 3px solid var(--semantic-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure demo sections don't interfere with hero component styles
|
||||
.demo-section {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: var(--semantic-shadow-card-rest);
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--semantic-shadow-card-hover);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { HeroSectionsDemoComponent } from './hero-sections-demo.component';
|
||||
|
||||
describe('HeroSectionsDemoComponent', () => {
|
||||
let component: HeroSectionsDemoComponent;
|
||||
let fixture: ComponentFixture<HeroSectionsDemoComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [HeroSectionsDemoComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(HeroSectionsDemoComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,152 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
HeroSectionComponent,
|
||||
HeroWithImageComponent,
|
||||
HeroSplitScreenComponent
|
||||
} from 'ui-landing-pages';
|
||||
import {
|
||||
HeroConfig,
|
||||
HeroImageConfig,
|
||||
HeroSplitConfig,
|
||||
CTAButton
|
||||
} from 'ui-landing-pages';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hero-sections-demo',
|
||||
imports: [
|
||||
CommonModule,
|
||||
HeroSectionComponent,
|
||||
HeroWithImageComponent,
|
||||
HeroSplitScreenComponent
|
||||
],
|
||||
templateUrl: './hero-sections-demo.component.html',
|
||||
styleUrl: './hero-sections-demo.component.scss'
|
||||
})
|
||||
export class HeroSectionsDemoComponent {
|
||||
|
||||
// Hero Section Configs
|
||||
basicHeroConfig: HeroConfig = {
|
||||
title: 'Build Amazing Landing Pages',
|
||||
subtitle: 'Create stunning websites with our comprehensive component library',
|
||||
alignment: 'center',
|
||||
backgroundType: 'gradient',
|
||||
minHeight: 'large',
|
||||
ctaPrimary: {
|
||||
text: 'Get Started',
|
||||
variant: 'filled',
|
||||
action: () => console.log('Primary CTA clicked')
|
||||
},
|
||||
ctaSecondary: {
|
||||
text: 'Learn More',
|
||||
variant: 'outlined',
|
||||
action: () => console.log('Secondary CTA clicked')
|
||||
}
|
||||
};
|
||||
|
||||
lightHeroConfig: HeroConfig = {
|
||||
title: 'Clean & Modern Design',
|
||||
subtitle: 'Designed for the modern web',
|
||||
alignment: 'left',
|
||||
backgroundType: 'solid',
|
||||
minHeight: 'medium',
|
||||
ctaPrimary: {
|
||||
text: 'Explore Features',
|
||||
variant: 'filled',
|
||||
action: () => console.log('Explore clicked')
|
||||
}
|
||||
};
|
||||
|
||||
darkHeroConfig: HeroConfig = {
|
||||
title: 'Dark Mode Ready',
|
||||
subtitle: 'Perfect for dark interfaces',
|
||||
alignment: 'right',
|
||||
backgroundType: 'animated',
|
||||
minHeight: 'full',
|
||||
ctaPrimary: {
|
||||
text: 'Try Dark Mode',
|
||||
variant: 'filled',
|
||||
action: () => console.log('Dark mode clicked')
|
||||
}
|
||||
};
|
||||
|
||||
// Hero With Image Configs
|
||||
splitImageConfig: HeroImageConfig = {
|
||||
title: 'Hero with Image',
|
||||
subtitle: 'Perfect split layout',
|
||||
alignment: 'left',
|
||||
backgroundType: 'solid',
|
||||
minHeight: 'large',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1551434678-e076c223a692?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80',
|
||||
imageAlt: 'Team collaboration',
|
||||
imagePosition: 'right',
|
||||
imageMobile: 'below',
|
||||
ctaPrimary: {
|
||||
text: 'Start Building',
|
||||
variant: 'filled',
|
||||
action: () => console.log('Start building clicked')
|
||||
}
|
||||
};
|
||||
|
||||
overlayImageConfig: HeroImageConfig = {
|
||||
title: 'Overlay Hero Style',
|
||||
subtitle: 'Text over image',
|
||||
alignment: 'center',
|
||||
backgroundType: 'gradient',
|
||||
minHeight: 'full',
|
||||
imageUrl: 'https://images.unsplash.com/photo-1557804506-669a67965ba0?ixlib=rb-4.0.3&auto=format&fit=crop&w=1200&q=80',
|
||||
imageAlt: 'Modern office space',
|
||||
imagePosition: 'left',
|
||||
imageMobile: 'above',
|
||||
ctaPrimary: {
|
||||
text: 'View Gallery',
|
||||
variant: 'filled',
|
||||
action: () => console.log('View gallery clicked')
|
||||
},
|
||||
ctaSecondary: {
|
||||
text: 'Learn More',
|
||||
variant: 'outlined',
|
||||
action: () => console.log('Learn more clicked')
|
||||
}
|
||||
};
|
||||
|
||||
// Hero Split Screen Configs
|
||||
textSplitConfig: HeroSplitConfig = {
|
||||
title: 'Split Screen Hero',
|
||||
subtitle: 'Powerful dual-content layout',
|
||||
alignment: 'center',
|
||||
backgroundType: 'solid',
|
||||
minHeight: 'large',
|
||||
splitRatio: '50-50',
|
||||
leftContent: {
|
||||
type: 'text',
|
||||
content: '<h3>Powerful Features</h3><p>Everything you need to build professional landing pages with our comprehensive component library.</p>'
|
||||
},
|
||||
rightContent: {
|
||||
type: 'text',
|
||||
content: '<h3>Easy to Use</h3><p>Drop-in components that work seamlessly with your existing Angular applications.</p>'
|
||||
}
|
||||
};
|
||||
|
||||
mediaSplitConfig: HeroSplitConfig = {
|
||||
title: 'Media Split Hero',
|
||||
subtitle: 'Text and image combination',
|
||||
alignment: 'left',
|
||||
backgroundType: 'gradient',
|
||||
minHeight: 'large',
|
||||
splitRatio: '60-40',
|
||||
leftContent: {
|
||||
type: 'text',
|
||||
content: '<h3>See It In Action</h3><p>Watch our components come to life with interactive examples and real-world use cases.</p>'
|
||||
},
|
||||
rightContent: {
|
||||
type: 'image',
|
||||
content: 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80'
|
||||
}
|
||||
};
|
||||
|
||||
onCtaClick(message: string): void {
|
||||
console.log(message);
|
||||
// In a real app, you might navigate to a different route or open a modal
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FAQSectionComponent, FAQConfig } from 'ui-landing-pages';
|
||||
|
||||
@Component({
|
||||
selector: 'app-landing-faq-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FAQSectionComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<div class="demo-header">
|
||||
<h1>FAQ Section Demo</h1>
|
||||
<p>Interactive FAQ component with accordion functionality and search.</p>
|
||||
<div class="under-construction">
|
||||
<p><strong>🚧 Under Construction:</strong> This demo is temporarily disabled while the ui-landing-pages library is being built.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Temporarily commented out until ui-landing-pages is built
|
||||
<div style="display: none;">
|
||||
<div class="demo-section">
|
||||
<h2>Default FAQ</h2>
|
||||
<ui-lp-faq [configuration]="defaultConfig"></ui-lp-faq>
|
||||
</div>
|
||||
|
||||
<!-- Bordered FAQ -->
|
||||
<div class="demo-section">
|
||||
<h2>Bordered Theme</h2>
|
||||
<ui-lp-faq [configuration]="borderedConfig"></ui-lp-faq>
|
||||
</div>
|
||||
|
||||
<!-- Minimal FAQ -->
|
||||
<div class="demo-section">
|
||||
<h2>Minimal Theme</h2>
|
||||
<ui-lp-faq [configuration]="minimalConfig"></ui-lp-faq>
|
||||
</div>
|
||||
|
||||
<!-- Multiple Expand FAQ -->
|
||||
<div class="demo-section">
|
||||
<h2>Multiple Expand Enabled</h2>
|
||||
<ui-lp-faq [configuration]="multipleExpandConfig"></ui-lp-faq>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
.demo-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.demo-header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.demo-header p {
|
||||
font-size: 1.125rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.demo-section h2 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class LandingFAQDemoComponent {
|
||||
defaultConfig: FAQConfig = {
|
||||
title: 'Frequently Asked Questions',
|
||||
subtitle: 'Find answers to common questions about our platform',
|
||||
searchEnabled: true,
|
||||
expandMultiple: false,
|
||||
theme: 'default',
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
question: 'What is included in the free plan?',
|
||||
answer: 'The free plan includes up to 5 projects, 2GB storage, and basic support. Perfect for getting started with small projects.'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
question: 'Can I upgrade or downgrade my plan at any time?',
|
||||
answer: 'Yes, you can change your plan at any time. Upgrades take effect immediately, while downgrades take effect at the next billing cycle.'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
question: 'Is my data secure?',
|
||||
answer: 'Absolutely. We use enterprise-grade encryption, regular security audits, and comply with SOC 2 Type II standards to protect your data.'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
question: 'Do you offer customer support?',
|
||||
answer: 'Yes! We provide 24/7 email support for all users, with priority phone support available for Pro and Enterprise customers.'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
question: 'Can I cancel my subscription anytime?',
|
||||
answer: 'Yes, you can cancel your subscription at any time. Your account will remain active until the end of your current billing period.'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
borderedConfig: FAQConfig = {
|
||||
...this.defaultConfig,
|
||||
title: 'Product Questions',
|
||||
subtitle: 'Everything you need to know about our product features',
|
||||
theme: 'bordered',
|
||||
items: [
|
||||
{
|
||||
id: '6',
|
||||
question: 'How do I integrate with third-party services?',
|
||||
answer: 'Our platform offers REST APIs, webhooks, and pre-built integrations with popular services like Slack, GitHub, and Zapier.'
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
question: 'What file formats do you support?',
|
||||
answer: 'We support all major file formats including PDF, Word, Excel, PowerPoint, images (PNG, JPG, SVG), and various code files.'
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
question: 'Is there a mobile app available?',
|
||||
answer: 'Yes! Our mobile apps are available for both iOS and Android, with full feature parity to the web application.'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
minimalConfig: FAQConfig = {
|
||||
...this.defaultConfig,
|
||||
title: 'Technical FAQ',
|
||||
subtitle: 'Answers to technical questions',
|
||||
theme: 'minimal',
|
||||
searchEnabled: false,
|
||||
items: [
|
||||
{
|
||||
id: '9',
|
||||
question: 'What are your API rate limits?',
|
||||
answer: 'Free accounts: 100 requests/hour. Pro accounts: 1,000 requests/hour. Enterprise: Custom limits based on your needs.'
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
question: 'Do you provide webhooks?',
|
||||
answer: 'Yes, we support webhooks for real-time event notifications. You can configure them in your dashboard settings.'
|
||||
},
|
||||
{
|
||||
id: '11',
|
||||
question: 'What programming languages do you support?',
|
||||
answer: 'We provide SDKs for JavaScript, Python, Ruby, PHP, Go, and .NET. REST APIs are available for any language.'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
multipleExpandConfig: FAQConfig = {
|
||||
...this.defaultConfig,
|
||||
title: 'Detailed FAQ',
|
||||
subtitle: 'Comprehensive answers to all your questions',
|
||||
expandMultiple: true,
|
||||
items: [
|
||||
{
|
||||
id: '12',
|
||||
question: 'How does billing work?',
|
||||
answer: 'Billing is processed monthly or annually based on your preference. You can view detailed invoices in your account dashboard and update payment methods anytime.'
|
||||
},
|
||||
{
|
||||
id: '13',
|
||||
question: 'Can I export my data?',
|
||||
answer: 'Yes, you can export all your data in various formats (CSV, JSON, XML) at any time. Enterprise customers also get automated backup options.'
|
||||
},
|
||||
{
|
||||
id: '14',
|
||||
question: 'What happens if I exceed my plan limits?',
|
||||
answer: 'If you approach your limits, we\'ll notify you in advance. You can either upgrade your plan or purchase additional resources as needed.'
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
@use "../../../../../ui-design-system/src/styles/semantic" as *;
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-xl;
|
||||
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-code {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: $semantic-color-text-primary;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FeatureGridComponent, FeatureGridConfig, FeatureItem } from 'ui-landing-pages';
|
||||
|
||||
@Component({
|
||||
selector: 'app-landing-feature-grid-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FeatureGridComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div style="padding: 2rem;">
|
||||
<h2>Feature Grid Component</h2>
|
||||
<p style="margin-bottom: 2rem; color: #666;">Showcase features in responsive grid layouts with icons, images, and links</p>
|
||||
|
||||
<!-- Basic Feature Grid -->
|
||||
<section class="demo-section">
|
||||
<h3 class="demo-section__title">Basic Grid</h3>
|
||||
<ui-lp-feature-grid
|
||||
[configuration]="basicConfig">
|
||||
</ui-lp-feature-grid>
|
||||
</section>
|
||||
|
||||
<!-- Card Variant -->
|
||||
<section class="demo-section">
|
||||
<h3 class="demo-section__title">Card Variant</h3>
|
||||
<ui-lp-feature-grid
|
||||
[configuration]="cardConfig">
|
||||
</ui-lp-feature-grid>
|
||||
</section>
|
||||
|
||||
<!-- Minimal Variant -->
|
||||
<section class="demo-section">
|
||||
<h3 class="demo-section__title">Minimal Variant</h3>
|
||||
<ui-lp-feature-grid
|
||||
[configuration]="minimalConfig">
|
||||
</ui-lp-feature-grid>
|
||||
</section>
|
||||
|
||||
<!-- List Layout -->
|
||||
<section class="demo-section">
|
||||
<h3 class="demo-section__title">List Layout</h3>
|
||||
<ui-lp-feature-grid
|
||||
[configuration]="listConfig">
|
||||
</ui-lp-feature-grid>
|
||||
</section>
|
||||
|
||||
<!-- Configuration Code -->
|
||||
<section class="demo-section">
|
||||
<h3 class="demo-section__title">Configuration</h3>
|
||||
<pre class="demo-code">{{ configExample }}</pre>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './landing-feature-grid-demo.component.scss'
|
||||
})
|
||||
export class LandingFeatureGridDemoComponent {
|
||||
private sampleFeatures: FeatureItem[] = [
|
||||
{
|
||||
id: 'security',
|
||||
title: 'Advanced Security',
|
||||
description: 'Enterprise-grade security with end-to-end encryption and compliance certifications.',
|
||||
icon: 'shield-alt',
|
||||
iconType: 'fa',
|
||||
link: {
|
||||
url: '#security',
|
||||
text: 'Learn more',
|
||||
target: '_self'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'performance',
|
||||
title: 'Lightning Fast',
|
||||
description: 'Optimized for speed with CDN delivery and smart caching mechanisms.',
|
||||
icon: 'bolt',
|
||||
iconType: 'fa',
|
||||
link: {
|
||||
url: '#performance',
|
||||
text: 'View benchmarks',
|
||||
target: '_self'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'analytics',
|
||||
title: 'Smart Analytics',
|
||||
description: 'Gain insights with real-time analytics and customizable dashboards.',
|
||||
icon: 'chart-line',
|
||||
iconType: 'fa',
|
||||
link: {
|
||||
url: '#analytics',
|
||||
text: 'Explore features',
|
||||
target: '_self'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'support',
|
||||
title: '24/7 Support',
|
||||
description: 'Get help whenever you need it with our dedicated support team.',
|
||||
icon: 'headset',
|
||||
iconType: 'fa',
|
||||
link: {
|
||||
url: '#support',
|
||||
text: 'Contact us',
|
||||
target: '_self'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
basicConfig: FeatureGridConfig = {
|
||||
title: 'Why Choose Our Platform',
|
||||
subtitle: 'Discover the features that make us different',
|
||||
features: this.sampleFeatures,
|
||||
layout: 'grid',
|
||||
columns: 'auto',
|
||||
variant: 'card',
|
||||
showIcons: true,
|
||||
spacing: 'normal'
|
||||
};
|
||||
|
||||
cardConfig: FeatureGridConfig = {
|
||||
title: 'Platform Features',
|
||||
features: this.sampleFeatures,
|
||||
layout: 'grid',
|
||||
columns: 2,
|
||||
variant: 'card',
|
||||
showIcons: true,
|
||||
spacing: 'loose'
|
||||
};
|
||||
|
||||
minimalConfig: FeatureGridConfig = {
|
||||
features: this.sampleFeatures.slice(0, 3),
|
||||
layout: 'grid',
|
||||
columns: 3,
|
||||
variant: 'minimal',
|
||||
showIcons: true,
|
||||
spacing: 'tight'
|
||||
};
|
||||
|
||||
listConfig: FeatureGridConfig = {
|
||||
title: 'Core Capabilities',
|
||||
features: this.sampleFeatures.slice(0, 3),
|
||||
layout: 'list',
|
||||
variant: 'minimal',
|
||||
showIcons: true,
|
||||
spacing: 'normal'
|
||||
};
|
||||
|
||||
configExample = `const featureGridConfig: FeatureGridConfig = {
|
||||
title: 'Why Choose Our Platform',
|
||||
subtitle: 'Discover the features that make us different',
|
||||
features: [
|
||||
{
|
||||
id: 'security',
|
||||
title: 'Advanced Security',
|
||||
description: 'Enterprise-grade security with end-to-end encryption.',
|
||||
icon: 'shield-alt',
|
||||
iconType: 'fa',
|
||||
link: {
|
||||
url: '#security',
|
||||
text: 'Learn more'
|
||||
}
|
||||
}
|
||||
// ... more features
|
||||
],
|
||||
layout: 'grid',
|
||||
columns: 'auto',
|
||||
variant: 'card',
|
||||
showIcons: true,
|
||||
spacing: 'normal'
|
||||
};`;
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FooterSectionComponent, FooterConfig, FooterLink } from 'ui-landing-pages';
|
||||
import { ButtonComponent } from 'ui-essentials';
|
||||
import { VStackComponent } from 'ui-essentials';
|
||||
import { SectionComponent } from 'ui-essentials';
|
||||
|
||||
@Component({
|
||||
selector: 'app-landing-footer-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FooterSectionComponent, ButtonComponent, VStackComponent, SectionComponent],
|
||||
template: `
|
||||
<ui-vstack [spacing]="'xl'">
|
||||
<ui-section>
|
||||
<h2>Landing Footer - Light Theme</h2>
|
||||
<p>Comprehensive footer with multiple columns, newsletter signup, and social links.</p>
|
||||
|
||||
<ui-lp-footer
|
||||
[configuration]="lightFooterConfig()"
|
||||
(linkClicked)="onLinkClicked($event)"
|
||||
(newsletterSubmitted)="onNewsletterSubmitted($event)">
|
||||
</ui-lp-footer>
|
||||
</ui-section>
|
||||
|
||||
<ui-section>
|
||||
<h2>Landing Footer - Dark Theme</h2>
|
||||
<p>Dark theme footer with company branding and legal links.</p>
|
||||
|
||||
<ui-lp-footer
|
||||
[configuration]="darkFooterConfig()"
|
||||
(linkClicked)="onLinkClicked($event)"
|
||||
(newsletterSubmitted)="onNewsletterSubmitted($event)">
|
||||
</ui-lp-footer>
|
||||
</ui-section>
|
||||
|
||||
<ui-section>
|
||||
<h2>Landing Footer - Minimal</h2>
|
||||
<p>Simple footer with just essential links and copyright.</p>
|
||||
|
||||
<ui-lp-footer
|
||||
[configuration]="minimalFooterConfig()"
|
||||
(linkClicked)="onLinkClicked($event)">
|
||||
</ui-lp-footer>
|
||||
</ui-section>
|
||||
|
||||
<ui-section>
|
||||
<h2>Configuration Options</h2>
|
||||
<div class="demo-controls">
|
||||
<ui-button
|
||||
[variant]="'outlined'"
|
||||
(clicked)="toggleTheme()"
|
||||
class="demo-control">
|
||||
Theme: {{ lightFooterConfig().theme }}
|
||||
</ui-button>
|
||||
|
||||
<ui-button
|
||||
[variant]="'outlined'"
|
||||
(clicked)="toggleDivider()"
|
||||
class="demo-control">
|
||||
Show Divider: {{ lightFooterConfig().showDivider ? 'ON' : 'OFF' }}
|
||||
</ui-button>
|
||||
|
||||
<ui-button
|
||||
[variant]="'outlined'"
|
||||
(clicked)="addColumn()"
|
||||
class="demo-control">
|
||||
Add Column
|
||||
</ui-button>
|
||||
|
||||
<ui-button
|
||||
[variant]="'outlined'"
|
||||
(clicked)="removeColumn()"
|
||||
class="demo-control">
|
||||
Remove Column
|
||||
</ui-button>
|
||||
</div>
|
||||
</ui-section>
|
||||
</ui-vstack>
|
||||
`,
|
||||
styles: [`
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.demo-control {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class LandingFooterDemoComponent {
|
||||
lightFooterConfig = signal<FooterConfig>({
|
||||
logo: {
|
||||
text: 'UI Suite',
|
||||
url: '/'
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
id: 'product',
|
||||
title: 'Product',
|
||||
items: [
|
||||
{ id: 'features', label: 'Features', route: '/features' },
|
||||
{ id: 'pricing', label: 'Pricing', route: '/pricing' },
|
||||
{ id: 'integrations', label: 'Integrations', route: '/integrations' },
|
||||
{ id: 'api', label: 'API', route: '/api' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'company',
|
||||
title: 'Company',
|
||||
items: [
|
||||
{ id: 'about', label: 'About Us', route: '/about' },
|
||||
{ id: 'careers', label: 'Careers', route: '/careers', badge: '5 open' },
|
||||
{ id: 'blog', label: 'Blog', route: '/blog' },
|
||||
{ id: 'press', label: 'Press', route: '/press' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'resources',
|
||||
title: 'Resources',
|
||||
items: [
|
||||
{ id: 'docs', label: 'Documentation', route: '/docs' },
|
||||
{ id: 'guides', label: 'Guides', route: '/guides' },
|
||||
{ id: 'help', label: 'Help Center', route: '/help' },
|
||||
{ id: 'community', label: 'Community', url: 'https://discord.gg/example', target: '_blank' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'legal',
|
||||
title: 'Legal',
|
||||
items: [
|
||||
{ id: 'privacy', label: 'Privacy Policy', route: '/privacy' },
|
||||
{ id: 'terms', label: 'Terms of Service', route: '/terms' },
|
||||
{ id: 'cookies', label: 'Cookie Policy', route: '/cookies' },
|
||||
{ id: 'gdpr', label: 'GDPR', route: '/gdpr' }
|
||||
]
|
||||
}
|
||||
],
|
||||
newsletter: {
|
||||
title: 'Stay Updated',
|
||||
description: 'Get the latest news and updates delivered to your inbox.',
|
||||
placeholder: 'Enter your email',
|
||||
buttonText: 'Subscribe',
|
||||
onSubmit: (email: string) => {
|
||||
console.log('Newsletter signup:', email);
|
||||
}
|
||||
},
|
||||
socialLinks: [
|
||||
{ id: 'twitter', platform: 'twitter', url: 'https://twitter.com/example' },
|
||||
{ id: 'facebook', platform: 'facebook', url: 'https://facebook.com/example' },
|
||||
{ id: 'linkedin', platform: 'linkedin', url: 'https://linkedin.com/company/example' },
|
||||
{ id: 'github', platform: 'github', url: 'https://github.com/example' }
|
||||
],
|
||||
copyright: '© 2024 UI Suite. All rights reserved.',
|
||||
legalLinks: [
|
||||
{ id: 'privacy', label: 'Privacy', route: '/privacy' },
|
||||
{ id: 'terms', label: 'Terms', route: '/terms' },
|
||||
{ id: 'cookies', label: 'Cookies', route: '/cookies' }
|
||||
],
|
||||
theme: 'light',
|
||||
showDivider: true
|
||||
});
|
||||
|
||||
darkFooterConfig = signal<FooterConfig>({
|
||||
logo: {
|
||||
text: 'Dark UI Pro',
|
||||
url: '/'
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
id: 'solutions',
|
||||
title: 'Solutions',
|
||||
items: [
|
||||
{ id: 'enterprise', label: 'Enterprise', route: '/enterprise' },
|
||||
{ id: 'startups', label: 'Startups', route: '/startups' },
|
||||
{ id: 'agencies', label: 'Agencies', route: '/agencies' },
|
||||
{ id: 'developers', label: 'Developers', route: '/developers' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'support',
|
||||
title: 'Support',
|
||||
items: [
|
||||
{ id: 'contact', label: 'Contact Us', route: '/contact' },
|
||||
{ id: 'chat', label: 'Live Chat', action: () => alert('Chat opened!') },
|
||||
{ id: 'tickets', label: 'Support Tickets', route: '/support' },
|
||||
{ id: 'status', label: 'System Status', url: 'https://status.example.com', target: '_blank' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'developers',
|
||||
title: 'Developers',
|
||||
items: [
|
||||
{ id: 'api-docs', label: 'API Documentation', route: '/api-docs' },
|
||||
{ id: 'sdk', label: 'SDK', route: '/sdk' },
|
||||
{ id: 'webhooks', label: 'Webhooks', route: '/webhooks' },
|
||||
{ id: 'changelog', label: 'Changelog', route: '/changelog' }
|
||||
]
|
||||
}
|
||||
],
|
||||
newsletter: {
|
||||
title: 'Developer Newsletter',
|
||||
description: 'Weekly updates on new features, API changes, and developer resources.',
|
||||
placeholder: 'developer@company.com',
|
||||
buttonText: 'Join',
|
||||
onSubmit: (email: string) => {
|
||||
console.log('Developer newsletter signup:', email);
|
||||
}
|
||||
},
|
||||
socialLinks: [
|
||||
{ id: 'github', platform: 'github', url: 'https://github.com/example' },
|
||||
{ id: 'twitter', platform: 'twitter', url: 'https://twitter.com/example' },
|
||||
{ id: 'youtube', platform: 'youtube', url: 'https://youtube.com/example' }
|
||||
],
|
||||
copyright: '© 2024 Dark UI Pro. Built with ❤️ by developers.',
|
||||
legalLinks: [
|
||||
{ id: 'privacy', label: 'Privacy Policy', route: '/privacy' },
|
||||
{ id: 'terms', label: 'Terms of Service', route: '/terms' },
|
||||
{ id: 'security', label: 'Security', route: '/security' }
|
||||
],
|
||||
theme: 'dark',
|
||||
showDivider: true
|
||||
});
|
||||
|
||||
minimalFooterConfig = signal<FooterConfig>({
|
||||
columns: [
|
||||
{
|
||||
id: 'quick-links',
|
||||
title: 'Quick Links',
|
||||
items: [
|
||||
{ id: 'home', label: 'Home', route: '/' },
|
||||
{ id: 'about', label: 'About', route: '/about' },
|
||||
{ id: 'contact', label: 'Contact', route: '/contact' }
|
||||
]
|
||||
}
|
||||
],
|
||||
copyright: '© 2024 Minimal Footer Example',
|
||||
legalLinks: [
|
||||
{ id: 'privacy', label: 'Privacy', route: '/privacy' },
|
||||
{ id: 'terms', label: 'Terms', route: '/terms' }
|
||||
],
|
||||
theme: 'light',
|
||||
showDivider: false
|
||||
});
|
||||
|
||||
toggleTheme(): void {
|
||||
const current = this.lightFooterConfig();
|
||||
this.lightFooterConfig.set({
|
||||
...current,
|
||||
theme: current.theme === 'light' ? 'dark' : 'light'
|
||||
});
|
||||
}
|
||||
|
||||
toggleDivider(): void {
|
||||
const current = this.lightFooterConfig();
|
||||
this.lightFooterConfig.set({
|
||||
...current,
|
||||
showDivider: !current.showDivider
|
||||
});
|
||||
}
|
||||
|
||||
addColumn(): void {
|
||||
const current = this.lightFooterConfig();
|
||||
const newColumn = {
|
||||
id: `column-${current.columns.length + 1}`,
|
||||
title: `Column ${current.columns.length + 1}`,
|
||||
items: [
|
||||
{ id: `item-1`, label: 'Item 1', route: '/item-1' },
|
||||
{ id: `item-2`, label: 'Item 2', route: '/item-2' }
|
||||
]
|
||||
};
|
||||
|
||||
this.lightFooterConfig.set({
|
||||
...current,
|
||||
columns: [...current.columns, newColumn]
|
||||
});
|
||||
}
|
||||
|
||||
removeColumn(): void {
|
||||
const current = this.lightFooterConfig();
|
||||
if (current.columns.length > 1) {
|
||||
this.lightFooterConfig.set({
|
||||
...current,
|
||||
columns: current.columns.slice(0, -1)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onLinkClicked(link: FooterLink): void {
|
||||
console.log('Footer link clicked:', link);
|
||||
}
|
||||
|
||||
onNewsletterSubmitted(email: string): void {
|
||||
console.log('Newsletter submitted:', email);
|
||||
alert(`Newsletter subscription for: ${email}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { LandingHeaderComponent, CTAButton, LandingHeaderConfig, NavigationItem } from 'ui-landing-pages';
|
||||
import { ButtonComponent, VStackComponent, SectionComponent } from 'ui-essentials';
|
||||
|
||||
@Component({
|
||||
selector: 'app-landing-header-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, LandingHeaderComponent, ButtonComponent, VStackComponent, SectionComponent],
|
||||
template: `
|
||||
<ui-vstack [spacing]="'xl'">
|
||||
<ui-section>
|
||||
<h2>Landing Header - Light Theme</h2>
|
||||
<p>Sticky navigation header with transparent background that becomes solid on scroll.</p>
|
||||
|
||||
<ui-lp-header
|
||||
[configuration]="lightHeaderConfig()"
|
||||
(navigationClicked)="onNavigationClicked($event)"
|
||||
(ctaClicked)="onCTAClicked($event)">
|
||||
</ui-lp-header>
|
||||
</ui-section>
|
||||
|
||||
<ui-section>
|
||||
<h2>Landing Header - Dark Theme</h2>
|
||||
<p>Dark theme version with logo and mega menu navigation.</p>
|
||||
|
||||
<ui-lp-header
|
||||
[configuration]="darkHeaderConfig()"
|
||||
(navigationClicked)="onNavigationClicked($event)"
|
||||
(ctaClicked)="onCTAClicked($event)">
|
||||
</ui-lp-header>
|
||||
</ui-section>
|
||||
|
||||
<ui-section>
|
||||
<h2>Landing Header - With Dropdown Menu</h2>
|
||||
<p>Header with dropdown navigation and badges.</p>
|
||||
|
||||
<ui-lp-header
|
||||
[configuration]="dropdownHeaderConfig()"
|
||||
(navigationClicked)="onNavigationClicked($event)"
|
||||
(ctaClicked)="onCTAClicked($event)">
|
||||
</ui-lp-header>
|
||||
</ui-section>
|
||||
|
||||
<ui-section>
|
||||
<h2>Configuration Options</h2>
|
||||
<div class="demo-controls">
|
||||
<ui-button
|
||||
[variant]="'outlined'"
|
||||
(clicked)="toggleTransparent()"
|
||||
class="demo-control">
|
||||
Toggle Transparent: {{ lightHeaderConfig().transparent ? 'ON' : 'OFF' }}
|
||||
</ui-button>
|
||||
|
||||
<ui-button
|
||||
[variant]="'outlined'"
|
||||
(clicked)="toggleSticky()"
|
||||
class="demo-control">
|
||||
Toggle Sticky: {{ lightHeaderConfig().sticky ? 'ON' : 'OFF' }}
|
||||
</ui-button>
|
||||
|
||||
<ui-button
|
||||
[variant]="'outlined'"
|
||||
(clicked)="toggleMobileMenu()"
|
||||
class="demo-control">
|
||||
Toggle Mobile Menu: {{ lightHeaderConfig().showMobileMenu ? 'ON' : 'OFF' }}
|
||||
</ui-button>
|
||||
</div>
|
||||
</ui-section>
|
||||
|
||||
<!-- Content to demonstrate scroll behavior -->
|
||||
<ui-section>
|
||||
<h3>Scroll Content</h3>
|
||||
<div style="height: 100vh; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-size: 2rem;">
|
||||
Scroll to see header behavior
|
||||
</div>
|
||||
</ui-section>
|
||||
|
||||
<ui-section>
|
||||
<h3>More Content</h3>
|
||||
<div style="height: 100vh; background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); display: flex; align-items: center; justify-content: center; color: white; font-size: 2rem;">
|
||||
Keep scrolling...
|
||||
</div>
|
||||
</ui-section>
|
||||
</ui-vstack>
|
||||
`,
|
||||
styles: [`
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.demo-control {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class LandingHeaderDemoComponent {
|
||||
lightHeaderConfig = signal<LandingHeaderConfig>({
|
||||
logo: {
|
||||
text: 'UI Suite',
|
||||
url: '/'
|
||||
},
|
||||
navigation: [
|
||||
{
|
||||
id: 'home',
|
||||
label: 'Home',
|
||||
route: '/',
|
||||
},
|
||||
{
|
||||
id: 'features',
|
||||
label: 'Features',
|
||||
route: '/features',
|
||||
},
|
||||
{
|
||||
id: 'pricing',
|
||||
label: 'Pricing',
|
||||
route: '/pricing',
|
||||
},
|
||||
{
|
||||
id: 'about',
|
||||
label: 'About',
|
||||
route: '/about',
|
||||
},
|
||||
{
|
||||
id: 'contact',
|
||||
label: 'Contact',
|
||||
route: '/contact',
|
||||
}
|
||||
],
|
||||
ctaButton: {
|
||||
text: 'Get Started',
|
||||
variant: 'filled',
|
||||
size: 'medium',
|
||||
action: () => alert('CTA clicked!')
|
||||
},
|
||||
transparent: true,
|
||||
sticky: true,
|
||||
showMobileMenu: true,
|
||||
maxWidth: 'xl',
|
||||
theme: 'light'
|
||||
});
|
||||
|
||||
darkHeaderConfig = signal<LandingHeaderConfig>({
|
||||
logo: {
|
||||
text: 'Dark UI',
|
||||
url: '/'
|
||||
},
|
||||
navigation: [
|
||||
{
|
||||
id: 'products',
|
||||
label: 'Products',
|
||||
route: '/products',
|
||||
},
|
||||
{
|
||||
id: 'solutions',
|
||||
label: 'Solutions',
|
||||
route: '/solutions',
|
||||
},
|
||||
{
|
||||
id: 'resources',
|
||||
label: 'Resources',
|
||||
route: '/resources',
|
||||
},
|
||||
{
|
||||
id: 'company',
|
||||
label: 'Company',
|
||||
route: '/company',
|
||||
}
|
||||
],
|
||||
ctaButton: {
|
||||
text: 'Start Free Trial',
|
||||
variant: 'filled',
|
||||
size: 'medium',
|
||||
icon: '🚀',
|
||||
action: () => alert('Free trial started!')
|
||||
},
|
||||
transparent: false,
|
||||
sticky: true,
|
||||
showMobileMenu: true,
|
||||
maxWidth: 'xl',
|
||||
theme: 'dark'
|
||||
});
|
||||
|
||||
dropdownHeaderConfig = signal<LandingHeaderConfig>({
|
||||
logo: {
|
||||
text: 'Dropdown Demo',
|
||||
url: '/'
|
||||
},
|
||||
navigation: [
|
||||
{
|
||||
id: 'home',
|
||||
label: 'Home',
|
||||
route: '/',
|
||||
},
|
||||
{
|
||||
id: 'products',
|
||||
label: 'Products',
|
||||
children: [
|
||||
{
|
||||
id: 'ui-kit',
|
||||
label: 'UI Kit',
|
||||
route: '/products/ui-kit',
|
||||
badge: 'New'
|
||||
},
|
||||
{
|
||||
id: 'components',
|
||||
label: 'Components',
|
||||
route: '/products/components',
|
||||
},
|
||||
{
|
||||
id: 'templates',
|
||||
label: 'Templates',
|
||||
route: '/products/templates',
|
||||
badge: 'Pro'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'docs',
|
||||
label: 'Documentation',
|
||||
children: [
|
||||
{
|
||||
id: 'getting-started',
|
||||
label: 'Getting Started',
|
||||
route: '/docs/getting-started',
|
||||
},
|
||||
{
|
||||
id: 'api',
|
||||
label: 'API Reference',
|
||||
route: '/docs/api',
|
||||
},
|
||||
{
|
||||
id: 'examples',
|
||||
label: 'Examples',
|
||||
route: '/docs/examples',
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'support',
|
||||
label: 'Support',
|
||||
route: '/support',
|
||||
badge: '24/7'
|
||||
}
|
||||
],
|
||||
ctaButton: {
|
||||
text: 'Contact Sales',
|
||||
variant: 'tonal',
|
||||
size: 'medium',
|
||||
action: () => alert('Contact sales clicked!')
|
||||
},
|
||||
transparent: false,
|
||||
sticky: true,
|
||||
showMobileMenu: true,
|
||||
maxWidth: 'xl',
|
||||
theme: 'light'
|
||||
});
|
||||
|
||||
toggleTransparent(): void {
|
||||
const current = this.lightHeaderConfig();
|
||||
this.lightHeaderConfig.set({
|
||||
...current,
|
||||
transparent: !current.transparent
|
||||
});
|
||||
}
|
||||
|
||||
toggleSticky(): void {
|
||||
const current = this.lightHeaderConfig();
|
||||
this.lightHeaderConfig.set({
|
||||
...current,
|
||||
sticky: !current.sticky
|
||||
});
|
||||
}
|
||||
|
||||
toggleMobileMenu(): void {
|
||||
const current = this.lightHeaderConfig();
|
||||
this.lightHeaderConfig.set({
|
||||
...current,
|
||||
showMobileMenu: !current.showMobileMenu
|
||||
});
|
||||
}
|
||||
|
||||
onNavigationClicked(item: NavigationItem): void {
|
||||
console.log('Navigation clicked:', item);
|
||||
}
|
||||
|
||||
onCTAClicked(cta: CTAButton): void {
|
||||
console.log('CTA clicked:', cta);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
@use "../../../../../ui-design-system/src/styles/semantic" as *;
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-xl;
|
||||
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-code {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: $semantic-color-text-primary;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { LogoCloudComponent, LogoCloudConfig, LogoItem } from 'ui-landing-pages';
|
||||
|
||||
@Component({
|
||||
selector: 'app-landing-logo-cloud-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, LogoCloudComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div style="padding: 2rem;">
|
||||
<h2>Logo Cloud Component</h2>
|
||||
<p style="margin-bottom: 2rem; color: #666;">Display partner and client logos in various layouts</p>
|
||||
|
||||
<!-- Basic Row Layout -->
|
||||
<section class="demo-section">
|
||||
<h3 class="demo-section__title">Row Layout</h3>
|
||||
<ui-lp-logo-cloud
|
||||
[configuration]="rowConfig">
|
||||
</ui-lp-logo-cloud>
|
||||
</section>
|
||||
|
||||
<!-- Grid Layout -->
|
||||
<section class="demo-section">
|
||||
<h3 class="demo-section__title">Grid Layout</h3>
|
||||
<ui-lp-logo-cloud
|
||||
[configuration]="gridConfig">
|
||||
</ui-lp-logo-cloud>
|
||||
</section>
|
||||
|
||||
<!-- Marquee Layout -->
|
||||
<section class="demo-section">
|
||||
<h3 class="demo-section__title">Marquee Layout</h3>
|
||||
<ui-lp-logo-cloud
|
||||
[configuration]="marqueeConfig">
|
||||
</ui-lp-logo-cloud>
|
||||
</section>
|
||||
|
||||
<!-- Configuration Code -->
|
||||
<section class="demo-section">
|
||||
<h3 class="demo-section__title">Configuration</h3>
|
||||
<pre class="demo-code">{{ configExample }}</pre>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './landing-logo-cloud-demo.component.scss'
|
||||
})
|
||||
export class LandingLogoCloudDemoComponent {
|
||||
private sampleLogos: LogoItem[] = [
|
||||
{
|
||||
id: 'google',
|
||||
name: 'Google',
|
||||
logo: 'https://upload.wikimedia.org/wikipedia/commons/2/2f/Google_2015_logo.svg',
|
||||
url: 'https://google.com',
|
||||
grayscale: false
|
||||
},
|
||||
{
|
||||
id: 'microsoft',
|
||||
name: 'Microsoft',
|
||||
logo: 'https://upload.wikimedia.org/wikipedia/commons/9/96/Microsoft_logo_%282012%29.svg',
|
||||
url: 'https://microsoft.com',
|
||||
grayscale: false
|
||||
},
|
||||
{
|
||||
id: 'amazon',
|
||||
name: 'Amazon',
|
||||
logo: 'https://upload.wikimedia.org/wikipedia/commons/a/a9/Amazon_logo.svg',
|
||||
url: 'https://amazon.com',
|
||||
grayscale: false
|
||||
},
|
||||
{
|
||||
id: 'apple',
|
||||
name: 'Apple',
|
||||
logo: 'https://upload.wikimedia.org/wikipedia/commons/f/fa/Apple_logo_black.svg',
|
||||
url: 'https://apple.com',
|
||||
grayscale: false
|
||||
},
|
||||
{
|
||||
id: 'netflix',
|
||||
name: 'Netflix',
|
||||
logo: 'https://upload.wikimedia.org/wikipedia/commons/0/08/Netflix_2015_logo.svg',
|
||||
url: 'https://netflix.com',
|
||||
grayscale: false
|
||||
},
|
||||
{
|
||||
id: 'spotify',
|
||||
name: 'Spotify',
|
||||
logo: 'https://upload.wikimedia.org/wikipedia/commons/1/19/Spotify_logo_without_text.svg',
|
||||
url: 'https://spotify.com',
|
||||
grayscale: false
|
||||
}
|
||||
];
|
||||
|
||||
rowConfig: LogoCloudConfig = {
|
||||
title: 'Trusted by Industry Leaders',
|
||||
subtitle: 'Join thousands of companies using our platform',
|
||||
logos: this.sampleLogos,
|
||||
layout: 'row',
|
||||
itemsPerRow: 5,
|
||||
grayscale: true,
|
||||
hoverEffect: true,
|
||||
maxHeight: 60
|
||||
};
|
||||
|
||||
gridConfig: LogoCloudConfig = {
|
||||
title: 'Our Partners',
|
||||
logos: this.sampleLogos,
|
||||
layout: 'grid',
|
||||
itemsPerRow: 4,
|
||||
grayscale: true,
|
||||
hoverEffect: true,
|
||||
maxHeight: 80
|
||||
};
|
||||
|
||||
marqueeConfig: LogoCloudConfig = {
|
||||
title: 'Continuous Partnership',
|
||||
subtitle: 'Seamlessly scrolling partner showcase',
|
||||
logos: this.sampleLogos,
|
||||
layout: 'marquee',
|
||||
grayscale: true,
|
||||
hoverEffect: true,
|
||||
maxHeight: 70
|
||||
};
|
||||
|
||||
configExample = `const logoCloudConfig: LogoCloudConfig = {
|
||||
title: 'Trusted by Industry Leaders',
|
||||
subtitle: 'Join thousands of companies using our platform',
|
||||
logos: [
|
||||
{
|
||||
id: 'google',
|
||||
name: 'Google',
|
||||
logo: 'path/to/google-logo.svg',
|
||||
url: 'https://google.com',
|
||||
grayscale: false
|
||||
}
|
||||
// ... more logos
|
||||
],
|
||||
layout: 'row',
|
||||
itemsPerRow: 5,
|
||||
grayscale: true,
|
||||
hoverEffect: true,
|
||||
maxHeight: 60
|
||||
};`;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
@use "../../../../../ui-design-system/src/styles/semantic" as *;
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-xl;
|
||||
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-code {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: $semantic-color-text-primary;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { StatisticsDisplayComponent, StatisticsConfig, StatisticItem } from 'ui-landing-pages';
|
||||
|
||||
@Component({
|
||||
selector: 'app-landing-statistics-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, StatisticsDisplayComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div style="padding: 2rem;">
|
||||
<h2>Statistics Display Component</h2>
|
||||
<p style="margin-bottom: 2rem; color: #666;">Showcase key metrics with animated counters and various layouts</p>
|
||||
|
||||
<!-- Basic Row Layout -->
|
||||
<section class="demo-section">
|
||||
<h3 class="demo-section__title">Row Layout - Minimal</h3>
|
||||
<ui-lp-statistics-display
|
||||
[configuration]="rowConfig">
|
||||
</ui-lp-statistics-display>
|
||||
</section>
|
||||
|
||||
<!-- Grid Layout with Cards -->
|
||||
<section class="demo-section">
|
||||
<h3 class="demo-section__title">Grid Layout - Card Variant</h3>
|
||||
<ui-lp-statistics-display
|
||||
[configuration]="cardConfig">
|
||||
</ui-lp-statistics-display>
|
||||
</section>
|
||||
|
||||
<!-- Primary Background -->
|
||||
<section class="demo-section">
|
||||
<h3 class="demo-section__title">Primary Background</h3>
|
||||
<ui-lp-statistics-display
|
||||
[configuration]="primaryConfig">
|
||||
</ui-lp-statistics-display>
|
||||
</section>
|
||||
|
||||
<!-- Highlighted Variant -->
|
||||
<section class="demo-section">
|
||||
<h3 class="demo-section__title">Highlighted Variant</h3>
|
||||
<ui-lp-statistics-display
|
||||
[configuration]="highlightedConfig">
|
||||
</ui-lp-statistics-display>
|
||||
</section>
|
||||
|
||||
<!-- Configuration Code -->
|
||||
<section class="demo-section">
|
||||
<h3 class="demo-section__title">Configuration</h3>
|
||||
<pre class="demo-code">{{ configExample }}</pre>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './landing-statistics-demo.component.scss'
|
||||
})
|
||||
export class LandingStatisticsDemoComponent {
|
||||
private sampleStatistics: StatisticItem[] = [
|
||||
{
|
||||
id: 'users',
|
||||
value: 150000,
|
||||
label: 'Active Users',
|
||||
suffix: '+',
|
||||
description: 'Growing every day',
|
||||
icon: 'users',
|
||||
animateValue: true
|
||||
},
|
||||
{
|
||||
id: 'projects',
|
||||
value: 25000,
|
||||
label: 'Projects Created',
|
||||
suffix: '+',
|
||||
description: 'Successful deployments',
|
||||
icon: 'chart-bar',
|
||||
animateValue: true
|
||||
},
|
||||
{
|
||||
id: 'uptime',
|
||||
value: 99.9,
|
||||
label: 'Uptime',
|
||||
suffix: '%',
|
||||
description: 'Reliable service',
|
||||
icon: 'arrow-up',
|
||||
animateValue: true
|
||||
},
|
||||
{
|
||||
id: 'support',
|
||||
value: '24/7',
|
||||
label: 'Support Available',
|
||||
description: 'Round-the-clock assistance',
|
||||
icon: 'headset',
|
||||
animateValue: false
|
||||
}
|
||||
];
|
||||
|
||||
rowConfig: StatisticsConfig = {
|
||||
title: 'Platform Performance',
|
||||
subtitle: 'See how we\'re making a difference',
|
||||
statistics: this.sampleStatistics,
|
||||
layout: 'row',
|
||||
variant: 'minimal',
|
||||
animateOnScroll: true,
|
||||
backgroundColor: 'transparent'
|
||||
};
|
||||
|
||||
cardConfig: StatisticsConfig = {
|
||||
title: 'Success Metrics',
|
||||
statistics: this.sampleStatistics,
|
||||
layout: 'grid',
|
||||
variant: 'card',
|
||||
animateOnScroll: true,
|
||||
backgroundColor: 'surface'
|
||||
};
|
||||
|
||||
primaryConfig: StatisticsConfig = {
|
||||
title: 'Key Achievements',
|
||||
subtitle: 'Numbers that matter to our community',
|
||||
statistics: this.sampleStatistics.slice(0, 3),
|
||||
layout: 'row',
|
||||
variant: 'minimal',
|
||||
animateOnScroll: true,
|
||||
backgroundColor: 'primary'
|
||||
};
|
||||
|
||||
highlightedConfig: StatisticsConfig = {
|
||||
title: 'Performance Indicators',
|
||||
statistics: this.sampleStatistics,
|
||||
layout: 'grid',
|
||||
variant: 'highlighted',
|
||||
animateOnScroll: true,
|
||||
backgroundColor: 'transparent'
|
||||
};
|
||||
|
||||
configExample = `const statisticsConfig: StatisticsConfig = {
|
||||
title: 'Platform Performance',
|
||||
subtitle: 'See how we're making a difference',
|
||||
statistics: [
|
||||
{
|
||||
id: 'users',
|
||||
value: 150000,
|
||||
label: 'Active Users',
|
||||
suffix: '+',
|
||||
description: 'Growing every day',
|
||||
icon: 'users',
|
||||
animateValue: true
|
||||
}
|
||||
// ... more statistics
|
||||
],
|
||||
layout: 'row',
|
||||
variant: 'minimal',
|
||||
animateOnScroll: true,
|
||||
backgroundColor: 'transparent'
|
||||
};`;
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { TeamGridComponent, TeamConfig } from 'ui-landing-pages';
|
||||
|
||||
@Component({
|
||||
selector: 'app-landing-team-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, TeamGridComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<div class="demo-header">
|
||||
<h1>Team Grid Demo</h1>
|
||||
<p>Showcase your team members with professional cards and social links.</p>
|
||||
</div>
|
||||
|
||||
<!-- 3 Column Layout -->
|
||||
<div class="demo-section">
|
||||
<h2>3 Column Layout (Default)</h2>
|
||||
<ui-lp-team-grid [configuration]="threeColumnConfig"></ui-lp-team-grid>
|
||||
</div>
|
||||
|
||||
<!-- 2 Column Layout -->
|
||||
<div class="demo-section">
|
||||
<h2>2 Column Layout</h2>
|
||||
<ui-lp-team-grid [configuration]="twoColumnConfig"></ui-lp-team-grid>
|
||||
</div>
|
||||
|
||||
<!-- 4 Column Layout -->
|
||||
<div class="demo-section">
|
||||
<h2>4 Column Layout</h2>
|
||||
<ui-lp-team-grid [configuration]="fourColumnConfig"></ui-lp-team-grid>
|
||||
</div>
|
||||
|
||||
<!-- Without Bio -->
|
||||
<div class="demo-section">
|
||||
<h2>Without Bio Text</h2>
|
||||
<ui-lp-team-grid [configuration]="noBioConfig"></ui-lp-team-grid>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
.demo-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.demo-header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.demo-header p {
|
||||
font-size: 1.125rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.demo-section h2 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class LandingTeamDemoComponent {
|
||||
threeColumnConfig: TeamConfig = {
|
||||
title: 'Meet Our Amazing Team',
|
||||
subtitle: 'The talented professionals who make our success possible',
|
||||
columns: 3,
|
||||
showSocial: true,
|
||||
showBio: true,
|
||||
members: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Sarah Johnson',
|
||||
role: 'CEO & Founder',
|
||||
bio: 'Visionary leader with 15+ years in tech, driving innovation and growth across multiple successful ventures.',
|
||||
image: 'https://images.unsplash.com/photo-1494790108755-2616b612b5ff?w=300&h=300&fit=crop&crop=face',
|
||||
social: {
|
||||
linkedin: 'https://linkedin.com/in/sarahjohnson',
|
||||
twitter: 'https://twitter.com/sarahjohnson',
|
||||
email: 'sarah@company.com'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Michael Chen',
|
||||
role: 'Chief Technology Officer',
|
||||
bio: 'Full-stack engineer and architect, passionate about building scalable systems and mentoring developers.',
|
||||
image: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=300&h=300&fit=crop&crop=face',
|
||||
social: {
|
||||
linkedin: 'https://linkedin.com/in/michaelchen',
|
||||
github: 'https://github.com/mchen',
|
||||
twitter: 'https://twitter.com/michaelchen',
|
||||
email: 'michael@company.com'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Emily Rodriguez',
|
||||
role: 'Head of Design',
|
||||
bio: 'Creative problem-solver specializing in user experience design and building design systems that scale.',
|
||||
image: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=300&h=300&fit=crop&crop=face',
|
||||
social: {
|
||||
linkedin: 'https://linkedin.com/in/emilyrodriguez',
|
||||
website: 'https://emilydesigns.com',
|
||||
email: 'emily@company.com'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
twoColumnConfig: TeamConfig = {
|
||||
title: 'Leadership Team',
|
||||
subtitle: 'The executives guiding our company forward',
|
||||
columns: 2,
|
||||
showSocial: true,
|
||||
showBio: true,
|
||||
members: [
|
||||
{
|
||||
id: '4',
|
||||
name: 'David Park',
|
||||
role: 'VP of Sales',
|
||||
bio: 'Results-driven sales leader with a proven track record of building high-performing teams and exceeding revenue targets.',
|
||||
image: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=300&h=300&fit=crop&crop=face',
|
||||
social: {
|
||||
linkedin: 'https://linkedin.com/in/davidpark',
|
||||
email: 'david@company.com'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Lisa Thompson',
|
||||
role: 'VP of Marketing',
|
||||
bio: 'Strategic marketer focused on brand building, digital growth, and creating authentic connections with our community.',
|
||||
image: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=300&h=300&fit=crop&crop=face',
|
||||
social: {
|
||||
linkedin: 'https://linkedin.com/in/lisathompson',
|
||||
twitter: 'https://twitter.com/lisathompson',
|
||||
email: 'lisa@company.com'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
fourColumnConfig: TeamConfig = {
|
||||
title: 'Engineering Team',
|
||||
subtitle: 'The brilliant minds behind our technology',
|
||||
columns: 4,
|
||||
showSocial: true,
|
||||
showBio: true,
|
||||
members: [
|
||||
{
|
||||
id: '6',
|
||||
name: 'Alex Kim',
|
||||
role: 'Senior Frontend Developer',
|
||||
bio: 'React specialist with a passion for creating beautiful, accessible user interfaces.',
|
||||
image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=300&h=300&fit=crop&crop=face',
|
||||
social: {
|
||||
github: 'https://github.com/alexkim',
|
||||
linkedin: 'https://linkedin.com/in/alexkim'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
name: 'Maria Santos',
|
||||
role: 'Backend Engineer',
|
||||
bio: 'Database optimization expert and API architecture enthusiast.',
|
||||
image: 'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=300&h=300&fit=crop&crop=face',
|
||||
social: {
|
||||
github: 'https://github.com/mariasantos',
|
||||
linkedin: 'https://linkedin.com/in/mariasantos'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
name: 'James Wilson',
|
||||
role: 'DevOps Engineer',
|
||||
bio: 'Cloud infrastructure specialist ensuring reliable, scalable deployments.',
|
||||
image: 'https://images.unsplash.com/photo-1519244703995-f4e0f30006d5?w=300&h=300&fit=crop&crop=face',
|
||||
social: {
|
||||
github: 'https://github.com/jameswilson',
|
||||
linkedin: 'https://linkedin.com/in/jameswilson'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
name: 'Sophie Brown',
|
||||
role: 'QA Engineer',
|
||||
bio: 'Quality assurance expert dedicated to delivering bug-free user experiences.',
|
||||
image: 'https://images.unsplash.com/photo-1487412720507-e7ab37603c6f?w=300&h=300&fit=crop&crop=face',
|
||||
social: {
|
||||
linkedin: 'https://linkedin.com/in/sophiebrown',
|
||||
email: 'sophie@company.com'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
noBioConfig: TeamConfig = {
|
||||
title: 'Advisory Board',
|
||||
subtitle: 'Industry experts guiding our strategic direction',
|
||||
columns: 3,
|
||||
showSocial: true,
|
||||
showBio: false,
|
||||
members: [
|
||||
{
|
||||
id: '10',
|
||||
name: 'Robert Anderson',
|
||||
role: 'Strategic Advisor',
|
||||
image: 'https://images.unsplash.com/photo-1560250097-0b93528c311a?w=300&h=300&fit=crop&crop=face',
|
||||
social: {
|
||||
linkedin: 'https://linkedin.com/in/robertanderson'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '11',
|
||||
name: 'Jennifer Lee',
|
||||
role: 'Technology Advisor',
|
||||
image: 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=300&h=300&fit=crop&crop=face',
|
||||
social: {
|
||||
linkedin: 'https://linkedin.com/in/jenniferlee',
|
||||
website: 'https://jenniferlee.tech'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '12',
|
||||
name: 'Marcus Johnson',
|
||||
role: 'Business Advisor',
|
||||
image: 'https://images.unsplash.com/photo-1556157382-97eda2d62296?w=300&h=300&fit=crop&crop=face',
|
||||
social: {
|
||||
linkedin: 'https://linkedin.com/in/marcusjohnson',
|
||||
email: 'marcus@advisor.com'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SaaSTemplateComponent } from '../../../../../ui-landing-pages/src/lib/components/templates/saas-template.component';
|
||||
import { ProductTemplateComponent } from '../../../../../ui-landing-pages/src/lib/components/templates/product-template.component';
|
||||
import { AgencyTemplateComponent } from '../../../../../ui-landing-pages/src/lib/components/templates/agency-template.component';
|
||||
import { AgencyTemplateConfig, ProductTemplateConfig, SaaSTemplateConfig } from '../../../../../ui-landing-pages/src/lib/interfaces/templates.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'app-landing-templates-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, SaaSTemplateComponent, ProductTemplateComponent, AgencyTemplateComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<div class="demo-header">
|
||||
<h1>Landing Page Templates Demo</h1>
|
||||
<p>Complete, production-ready landing page templates for different business types.</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-navigation">
|
||||
<button
|
||||
[class.active]="activeTemplate === 'saas'"
|
||||
(click)="setActiveTemplate('saas')">
|
||||
SaaS Template
|
||||
</button>
|
||||
<button
|
||||
[class.active]="activeTemplate === 'product'"
|
||||
(click)="setActiveTemplate('product')">
|
||||
Product Template
|
||||
</button>
|
||||
<button
|
||||
[class.active]="activeTemplate === 'agency'"
|
||||
(click)="setActiveTemplate('agency')">
|
||||
Agency Template
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="template-container">
|
||||
@switch (activeTemplate) {
|
||||
@case ('saas') {
|
||||
<ui-lp-saas-template [configuration]="saasConfig"></ui-lp-saas-template>
|
||||
}
|
||||
@case ('product') {
|
||||
<ui-lp-product-template [configuration]="productConfig"></ui-lp-product-template>
|
||||
}
|
||||
@case ('agency') {
|
||||
<ui-lp-agency-template [configuration]="agencyConfig"></ui-lp-agency-template>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
.demo-container {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
text-align: center;
|
||||
padding: 3rem 2rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.demo-header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.demo-header p {
|
||||
font-size: 1.125rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.demo-navigation {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
padding: 2rem;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.demo-navigation button {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: 2px solid #6c757d;
|
||||
background: white;
|
||||
color: #6c757d;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.demo-navigation button:hover {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.demo-navigation button.active {
|
||||
background: #007bff;
|
||||
border-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.template-container {
|
||||
min-height: calc(100vh - 200px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.demo-navigation {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.demo-navigation button {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class LandingTemplatesDemoComponent {
|
||||
activeTemplate: 'saas' | 'product' | 'agency' = 'saas';
|
||||
|
||||
setActiveTemplate(template: 'saas' | 'product' | 'agency'): void {
|
||||
this.activeTemplate = template;
|
||||
}
|
||||
|
||||
saasConfig: SaaSTemplateConfig = {
|
||||
hero: {
|
||||
title: 'Build Amazing SaaS Applications',
|
||||
subtitle: 'The complete toolkit for modern software development with enterprise-grade security and scalability.',
|
||||
ctaPrimary: {
|
||||
text: 'Start Free Trial',
|
||||
variant: 'tonal',
|
||||
size: 'large',
|
||||
action: () => console.log('Start trial clicked')
|
||||
},
|
||||
ctaSecondary: {
|
||||
text: 'View Demo',
|
||||
variant: 'outlined',
|
||||
size: 'large',
|
||||
action: () => console.log('View demo clicked')
|
||||
},
|
||||
alignment: 'center',
|
||||
backgroundType: 'gradient',
|
||||
minHeight: 'full'
|
||||
},
|
||||
features: {
|
||||
title: 'Why Choose Our Platform',
|
||||
subtitle: 'Everything you need to build, deploy, and scale your applications',
|
||||
features: [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Lightning Fast Performance',
|
||||
description: 'Optimized architecture delivers sub-second response times and 99.99% uptime.',
|
||||
icon: 'rocket'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Enterprise Security',
|
||||
description: 'SOC 2 compliant with end-to-end encryption and advanced threat protection.',
|
||||
icon: 'shield'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Seamless Integrations',
|
||||
description: 'Connect with 100+ popular tools through our robust API and webhooks.',
|
||||
icon: 'plug'
|
||||
}
|
||||
]
|
||||
},
|
||||
socialProof: {
|
||||
title: 'Trusted by Industry Leaders',
|
||||
statistics: [
|
||||
{ id: '1', label: 'Active Users', value: '50,000+' },
|
||||
{ id: '2', label: 'Countries', value: '120+' },
|
||||
{ id: '3', label: 'Uptime', value: '99.99%' },
|
||||
{ id: '4', label: 'Support Rating', value: '4.9/5' }
|
||||
]
|
||||
},
|
||||
testimonials: {
|
||||
title: 'What Our Customers Say',
|
||||
testimonials: [
|
||||
{
|
||||
id: '1',
|
||||
content: 'This platform transformed our development workflow. We ship features 3x faster now.',
|
||||
name: 'Sarah Chen',
|
||||
title: 'CTO at TechCorp',
|
||||
avatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b5ff?w=100&h=100&fit=crop&crop=face'
|
||||
}
|
||||
]
|
||||
},
|
||||
pricing: {
|
||||
billingToggle: {
|
||||
monthlyLabel: 'Monthly',
|
||||
yearlyLabel: 'Yearly',
|
||||
discountText: 'Save 20%'
|
||||
},
|
||||
featuresComparison: false,
|
||||
plans: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Starter',
|
||||
price: {
|
||||
monthly: 29,
|
||||
yearly: 290,
|
||||
currency: 'USD',
|
||||
suffix: 'per user'
|
||||
},
|
||||
features: [
|
||||
{ name: '5 Projects', included: true },
|
||||
{ name: '10GB Storage', included: true },
|
||||
{ name: 'Email Support', included: true }
|
||||
],
|
||||
cta: {
|
||||
text: 'Get Started',
|
||||
variant: 'filled',
|
||||
action: () => console.log('Starter plan selected')
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
faq: {
|
||||
title: 'Frequently Asked Questions',
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
question: 'Can I cancel my subscription anytime?',
|
||||
answer: 'Yes, you can cancel your subscription at any time with no penalties.'
|
||||
}
|
||||
]
|
||||
},
|
||||
cta: {
|
||||
title: 'Ready to Get Started?',
|
||||
subtitle: 'Join thousands of developers building the future',
|
||||
ctaPrimary: {
|
||||
text: 'Start Free Trial',
|
||||
variant: 'filled',
|
||||
action: () => console.log('Start trial clicked')
|
||||
},
|
||||
backgroundType: 'gradient'
|
||||
}
|
||||
};
|
||||
|
||||
productConfig: ProductTemplateConfig = {
|
||||
hero: {
|
||||
title: 'Introducing the Future of Productivity',
|
||||
subtitle: 'Revolutionary new product that will transform how you work and collaborate.',
|
||||
ctaPrimary: {
|
||||
text: 'Order Now',
|
||||
variant: 'tonal',
|
||||
size: 'large',
|
||||
action: () => console.log('Order now clicked')
|
||||
},
|
||||
ctaSecondary: {
|
||||
text: 'Learn More',
|
||||
variant: 'tonal',
|
||||
size: 'large',
|
||||
action: () => console.log('Learn more clicked')
|
||||
},
|
||||
alignment: 'left',
|
||||
backgroundType: 'image',
|
||||
minHeight: 'large'
|
||||
},
|
||||
features: {
|
||||
title: 'Key Features',
|
||||
subtitle: 'Designed with you in mind',
|
||||
features: [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Intuitive Design',
|
||||
description: 'Beautiful interface that anyone can use without training.',
|
||||
icon: 'star'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Premium Materials',
|
||||
description: 'Built with the finest materials for durability and style.',
|
||||
icon: 'check'
|
||||
}
|
||||
]
|
||||
},
|
||||
testimonials: {
|
||||
title: 'Customer Reviews',
|
||||
testimonials: [
|
||||
{
|
||||
id: '1',
|
||||
content: 'This product exceeded all my expectations. Highly recommended!',
|
||||
name: 'Mike Johnson',
|
||||
title: 'Verified Customer',
|
||||
rating: 5
|
||||
}
|
||||
]
|
||||
},
|
||||
pricing: {
|
||||
billingToggle: {
|
||||
monthlyLabel: 'One-time',
|
||||
yearlyLabel: 'Bundle',
|
||||
discountText: 'Best Value'
|
||||
},
|
||||
featuresComparison: false,
|
||||
plans: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Standard Edition',
|
||||
price: {
|
||||
monthly: 199,
|
||||
yearly: 199,
|
||||
currency: 'USD',
|
||||
suffix: 'one-time'
|
||||
},
|
||||
features: [
|
||||
{ name: '2-Year Warranty', included: true },
|
||||
{ name: 'Free Shipping', included: true }
|
||||
],
|
||||
cta: {
|
||||
text: 'Order Now',
|
||||
variant: 'filled',
|
||||
action: () => console.log('Product ordered')
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
cta: {
|
||||
title: 'Start Your Journey',
|
||||
subtitle: 'Order now and get free shipping worldwide',
|
||||
ctaPrimary: {
|
||||
text: 'Order Now',
|
||||
variant: 'filled',
|
||||
action: () => console.log('Order now clicked')
|
||||
},
|
||||
backgroundType: 'gradient'
|
||||
}
|
||||
};
|
||||
|
||||
agencyConfig: AgencyTemplateConfig = {
|
||||
hero: {
|
||||
title: 'Your Vision, Our Expertise',
|
||||
subtitle: 'We create exceptional digital experiences that drive results for businesses of all sizes.',
|
||||
ctaPrimary: {
|
||||
text: 'Start Project',
|
||||
variant: 'tonal',
|
||||
size: 'large',
|
||||
action: () => console.log('Start project clicked')
|
||||
},
|
||||
ctaSecondary: {
|
||||
text: 'View Portfolio',
|
||||
variant: 'outlined',
|
||||
size: 'large',
|
||||
action: () => console.log('View portfolio clicked')
|
||||
},
|
||||
alignment: 'left',
|
||||
backgroundType: 'gradient',
|
||||
minHeight: 'large'
|
||||
},
|
||||
services: {
|
||||
title: 'Our Services',
|
||||
subtitle: 'Comprehensive solutions for your digital needs',
|
||||
features: [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Web Development',
|
||||
description: 'Custom websites and web applications built with modern technologies.',
|
||||
icon: 'code'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'UI/UX Design',
|
||||
description: 'User-centered design that converts visitors into customers.',
|
||||
icon: 'palette'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Digital Marketing',
|
||||
description: 'Strategic campaigns that grow your online presence.',
|
||||
icon: 'megaphone'
|
||||
}
|
||||
]
|
||||
},
|
||||
team: {
|
||||
title: 'Meet Our Team',
|
||||
subtitle: 'The talented individuals behind our success',
|
||||
members: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Alex Rivera',
|
||||
role: 'Creative Director',
|
||||
bio: 'Award-winning designer with 10+ years of experience in digital creativity.',
|
||||
image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=300&h=300&fit=crop&crop=face',
|
||||
social: {
|
||||
linkedin: 'https://linkedin.com/in/alexrivera'
|
||||
}
|
||||
}
|
||||
],
|
||||
columns: 3,
|
||||
showSocial: true,
|
||||
showBio: true
|
||||
},
|
||||
timeline: {
|
||||
title: 'Our Journey',
|
||||
subtitle: 'Milestones that define our growth',
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Agency Founded',
|
||||
description: 'Started with a mission to help businesses succeed online.',
|
||||
date: '2018',
|
||||
status: 'completed',
|
||||
icon: 'rocket'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: '100+ Happy Clients',
|
||||
description: 'Reached major milestone serving clients across industries.',
|
||||
date: '2020',
|
||||
status: 'completed',
|
||||
icon: 'star'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Award Recognition',
|
||||
description: 'Won Best Digital Agency award from Industry Association.',
|
||||
date: '2022',
|
||||
status: 'current',
|
||||
icon: 'check',
|
||||
badge: 'Latest'
|
||||
}
|
||||
],
|
||||
orientation: 'vertical',
|
||||
showDates: true
|
||||
},
|
||||
testimonials: {
|
||||
title: 'Client Success Stories',
|
||||
subtitle: 'What our partners say about working with us',
|
||||
testimonials: [
|
||||
{
|
||||
id: '1',
|
||||
content: 'Working with this agency was a game-changer for our business. They delivered beyond our expectations.',
|
||||
name: 'Emma Watson',
|
||||
title: 'CEO at StartupXYZ',
|
||||
avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=100&h=100&fit=crop&crop=face'
|
||||
}
|
||||
]
|
||||
},
|
||||
cta: {
|
||||
title: 'Ready to Start Your Project?',
|
||||
subtitle: 'Let\'s discuss how we can bring your vision to life',
|
||||
ctaPrimary: {
|
||||
text: 'Get Started',
|
||||
variant: 'filled',
|
||||
action: () => console.log('Get started clicked')
|
||||
},
|
||||
backgroundType: 'gradient'
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
@use "../../../../../ui-design-system/src/styles/semantic" as *;
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: $semantic-spacing-layout-section-xl;
|
||||
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-code {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: $semantic-color-text-primary;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
// Temporarily commented out until ui-landing-pages is built
|
||||
// import { TestimonialCarouselComponent, TestimonialCarouselConfig, TestimonialItem } from 'ui-landing-pages';
|
||||
|
||||
// Placeholder types
|
||||
interface TestimonialItem {
|
||||
id: string;
|
||||
name: string;
|
||||
title: string;
|
||||
company: string;
|
||||
content: string;
|
||||
rating: number;
|
||||
avatar: string;
|
||||
verified: boolean;
|
||||
}
|
||||
|
||||
interface TestimonialCarouselConfig {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
testimonials: TestimonialItem[];
|
||||
autoPlay?: boolean;
|
||||
autoPlayDelay?: number;
|
||||
showDots?: boolean;
|
||||
showNavigation?: boolean;
|
||||
itemsPerView?: number;
|
||||
variant?: 'card' | 'quote' | 'minimal';
|
||||
showRatings?: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-landing-testimonials-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div style="padding: 2rem;">
|
||||
<h2>Testimonial Carousel Component</h2>
|
||||
<p style="margin-bottom: 2rem; color: #666;">Display customer testimonials with carousel navigation and various styles</p>
|
||||
|
||||
<div style="background: #f5f5f5; padding: 2rem; border-radius: 8px; text-align: center; color: #666;">
|
||||
<p><strong>Component Preview Unavailable</strong></p>
|
||||
<p>The TestimonialCarouselComponent will be available once the ui-landing-pages library is built.</p>
|
||||
<p>This demo will show:</p>
|
||||
<ul style="list-style: none; padding: 0; margin: 1rem 0;">
|
||||
<li>• Single Item Carousel</li>
|
||||
<li>• Multi-Item Carousel</li>
|
||||
<li>• Quote Variant</li>
|
||||
<li>• Minimal Variant</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Configuration Code -->
|
||||
<section class="demo-section" style="margin-top: 2rem;">
|
||||
<h3 class="demo-section__title">Configuration Example</h3>
|
||||
<pre class="demo-code" style="background: #f8f8f8; padding: 1rem; border-radius: 4px; overflow-x: auto;">{{ configExample }}</pre>
|
||||
</section>
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './landing-testimonials-demo.component.scss'
|
||||
})
|
||||
export class LandingTestimonialsDemoComponent {
|
||||
private sampleTestimonials: TestimonialItem[] = [
|
||||
{
|
||||
id: 'sarah-chen',
|
||||
name: 'Sarah Chen',
|
||||
title: 'CTO',
|
||||
company: 'TechCorp',
|
||||
content: 'This platform transformed our development workflow. The intuitive design and powerful features helped us deliver projects 40% faster.',
|
||||
rating: 5,
|
||||
avatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b977?w=100&h=100&fit=crop&crop=face',
|
||||
verified: true
|
||||
},
|
||||
{
|
||||
id: 'marcus-rodriguez',
|
||||
name: 'Marcus Rodriguez',
|
||||
title: 'Lead Developer',
|
||||
company: 'StartupXYZ',
|
||||
content: 'Outstanding performance and reliability. The support team is incredibly responsive and knowledgeable.',
|
||||
rating: 5,
|
||||
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=100&h=100&fit=crop&crop=face',
|
||||
verified: true
|
||||
},
|
||||
{
|
||||
id: 'emily-watson',
|
||||
name: 'Emily Watson',
|
||||
title: 'Product Manager',
|
||||
company: 'InnovateLabs',
|
||||
content: 'The analytics features provided insights we never had before. Our team productivity increased significantly.',
|
||||
rating: 4,
|
||||
avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=100&h=100&fit=crop&crop=face',
|
||||
verified: false
|
||||
},
|
||||
{
|
||||
id: 'david-kim',
|
||||
name: 'David Kim',
|
||||
title: 'Software Engineer',
|
||||
company: 'CodeCrafters',
|
||||
content: 'Clean interface, excellent documentation, and seamless integration. Everything just works as expected.',
|
||||
rating: 5,
|
||||
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop&crop=face',
|
||||
verified: true
|
||||
},
|
||||
{
|
||||
id: 'lisa-thompson',
|
||||
name: 'Lisa Thompson',
|
||||
title: 'Engineering Director',
|
||||
company: 'ScaleTech',
|
||||
content: 'Scalable solution that grows with our needs. The performance optimization features are game-changing.',
|
||||
rating: 5,
|
||||
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100&h=100&fit=crop&crop=face',
|
||||
verified: true
|
||||
}
|
||||
];
|
||||
|
||||
singleConfig: TestimonialCarouselConfig = {
|
||||
title: 'What Our Customers Say',
|
||||
subtitle: 'Trusted by thousands of developers worldwide',
|
||||
testimonials: this.sampleTestimonials,
|
||||
autoPlay: true,
|
||||
autoPlayDelay: 5000,
|
||||
showDots: true,
|
||||
showNavigation: true,
|
||||
itemsPerView: 1,
|
||||
variant: 'card',
|
||||
showRatings: true
|
||||
};
|
||||
|
||||
multiConfig: TestimonialCarouselConfig = {
|
||||
title: 'Customer Success Stories',
|
||||
testimonials: this.sampleTestimonials,
|
||||
autoPlay: false,
|
||||
showDots: true,
|
||||
showNavigation: true,
|
||||
itemsPerView: 2,
|
||||
variant: 'card',
|
||||
showRatings: true
|
||||
};
|
||||
|
||||
quoteConfig: TestimonialCarouselConfig = {
|
||||
title: 'Featured Reviews',
|
||||
testimonials: this.sampleTestimonials.slice(0, 3),
|
||||
autoPlay: true,
|
||||
autoPlayDelay: 6000,
|
||||
showDots: true,
|
||||
showNavigation: false,
|
||||
itemsPerView: 1,
|
||||
variant: 'quote',
|
||||
showRatings: true
|
||||
};
|
||||
|
||||
minimalConfig: TestimonialCarouselConfig = {
|
||||
testimonials: this.sampleTestimonials.slice(0, 3),
|
||||
autoPlay: false,
|
||||
showDots: true,
|
||||
showNavigation: true,
|
||||
itemsPerView: 1,
|
||||
variant: 'minimal',
|
||||
showRatings: false
|
||||
};
|
||||
|
||||
configExample = `const testimonialConfig: TestimonialCarouselConfig = {
|
||||
title: 'What Our Customers Say',
|
||||
subtitle: 'Trusted by thousands of developers worldwide',
|
||||
testimonials: [
|
||||
{
|
||||
id: 'sarah-chen',
|
||||
name: 'Sarah Chen',
|
||||
title: 'CTO',
|
||||
company: 'TechCorp',
|
||||
content: 'This platform transformed our workflow...',
|
||||
rating: 5,
|
||||
avatar: 'path/to/avatar.jpg',
|
||||
verified: true
|
||||
}
|
||||
// ... more testimonials
|
||||
],
|
||||
autoPlay: true,
|
||||
autoPlayDelay: 5000,
|
||||
showDots: true,
|
||||
showNavigation: true,
|
||||
itemsPerView: 1,
|
||||
variant: 'card',
|
||||
showRatings: true
|
||||
};`;
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { TimelineConfig, TimelineSectionComponent } from "../../../../../ui-landing-pages/src/public-api";
|
||||
|
||||
@Component({
|
||||
selector: 'app-landing-timeline-demo',
|
||||
standalone: true,
|
||||
imports: [CommonModule, TimelineSectionComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="demo-container">
|
||||
<div class="demo-header">
|
||||
<h1>Timeline Demo</h1>
|
||||
<p>Visual timeline components for roadmaps, company history, and project milestones.</p>
|
||||
</div>
|
||||
|
||||
<!-- Vertical Timeline -->
|
||||
<div class="demo-section">
|
||||
<h2>Vertical Timeline (Default)</h2>
|
||||
<ui-lp-timeline [configuration]="verticalConfig"></ui-lp-timeline>
|
||||
</div>
|
||||
|
||||
<!-- Horizontal Timeline -->
|
||||
<div class="demo-section">
|
||||
<h2>Horizontal Timeline</h2>
|
||||
<ui-lp-timeline [configuration]="horizontalConfig"></ui-lp-timeline>
|
||||
</div>
|
||||
|
||||
<!-- Minimal Theme -->
|
||||
<div class="demo-section">
|
||||
<h2>Minimal Theme</h2>
|
||||
<ui-lp-timeline [configuration]="minimalConfig"></ui-lp-timeline>
|
||||
</div>
|
||||
|
||||
<!-- Connected Theme -->
|
||||
<div class="demo-section">
|
||||
<h2>Connected Theme</h2>
|
||||
<ui-lp-timeline [configuration]="connectedConfig"></ui-lp-timeline>
|
||||
</div>
|
||||
|
||||
<!-- Product Roadmap -->
|
||||
<div class="demo-section">
|
||||
<h2>Product Roadmap</h2>
|
||||
<ui-lp-timeline [configuration]="roadmapConfig"></ui-lp-timeline>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
.demo-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.demo-header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.demo-header p {
|
||||
font-size: 1.125rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.demo-section h2 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class LandingTimelineDemoComponent {
|
||||
verticalConfig: TimelineConfig = {
|
||||
title: 'Our Company Journey',
|
||||
subtitle: 'Key milestones that shaped our growth',
|
||||
orientation: 'vertical',
|
||||
theme: 'default',
|
||||
showDates: true,
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Company Founded',
|
||||
description: 'Started with a vision to revolutionize how teams collaborate and build software together.',
|
||||
date: 'January 2020',
|
||||
status: 'completed',
|
||||
icon: 'rocket'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'First Product Launch',
|
||||
description: 'Launched our MVP with core features that solve real problems for development teams.',
|
||||
date: 'June 2020',
|
||||
status: 'completed',
|
||||
icon: 'star'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Series A Funding',
|
||||
description: 'Secured $5M in Series A funding to accelerate product development and team growth.',
|
||||
date: 'March 2021',
|
||||
status: 'completed',
|
||||
icon: 'check'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: '10,000 Active Users',
|
||||
description: 'Reached a major milestone with 10,000+ active users across 50+ countries worldwide.',
|
||||
date: 'September 2021',
|
||||
status: 'completed',
|
||||
icon: 'star'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
title: 'Major Platform Update',
|
||||
description: 'Released version 2.0 with advanced automation, better performance, and new integrations.',
|
||||
date: 'December 2022',
|
||||
status: 'current',
|
||||
icon: 'rocket',
|
||||
badge: 'Current'
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
title: 'International Expansion',
|
||||
description: 'Planning to open offices in Europe and Asia to better serve our global customer base.',
|
||||
date: 'Q2 2024',
|
||||
status: 'upcoming'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
horizontalConfig: TimelineConfig = {
|
||||
title: 'Development Process',
|
||||
subtitle: 'Our structured approach to building great products',
|
||||
orientation: 'horizontal',
|
||||
theme: 'default',
|
||||
showDates: false,
|
||||
items: [
|
||||
{
|
||||
id: '7',
|
||||
title: 'Research & Discovery',
|
||||
description: 'Understanding user needs through interviews, surveys, and market analysis.',
|
||||
status: 'completed',
|
||||
icon: 'check'
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
title: 'Design & Prototyping',
|
||||
description: 'Creating wireframes, mockups, and interactive prototypes for validation.',
|
||||
status: 'completed',
|
||||
icon: 'check'
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
title: 'Development',
|
||||
description: 'Building features with clean code, automated testing, and continuous integration.',
|
||||
status: 'current',
|
||||
icon: 'rocket'
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
title: 'Testing & QA',
|
||||
description: 'Comprehensive testing across devices, browsers, and user scenarios.',
|
||||
status: 'upcoming'
|
||||
},
|
||||
{
|
||||
id: '11',
|
||||
title: 'Launch & Monitor',
|
||||
description: 'Deploying to production and monitoring performance and user feedback.',
|
||||
status: 'upcoming'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
minimalConfig: TimelineConfig = {
|
||||
title: 'Project Milestones',
|
||||
subtitle: 'Clean and simple timeline presentation',
|
||||
orientation: 'vertical',
|
||||
theme: 'minimal',
|
||||
showDates: true,
|
||||
items: [
|
||||
{
|
||||
id: '12',
|
||||
title: 'Project Kickoff',
|
||||
description: 'Initial team meeting and project scope definition.',
|
||||
date: 'Week 1',
|
||||
status: 'completed'
|
||||
},
|
||||
{
|
||||
id: '13',
|
||||
title: 'Requirements Gathering',
|
||||
description: 'Detailed analysis of user requirements and technical specifications.',
|
||||
date: 'Week 2-3',
|
||||
status: 'completed'
|
||||
},
|
||||
{
|
||||
id: '14',
|
||||
title: 'Architecture Planning',
|
||||
description: 'System design and technology stack selection.',
|
||||
date: 'Week 4',
|
||||
status: 'current'
|
||||
},
|
||||
{
|
||||
id: '15',
|
||||
title: 'Implementation',
|
||||
description: 'Core development phase with iterative releases.',
|
||||
date: 'Week 5-12',
|
||||
status: 'upcoming'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
connectedConfig: TimelineConfig = {
|
||||
title: 'Feature Evolution',
|
||||
subtitle: 'How our features have evolved over time',
|
||||
orientation: 'vertical',
|
||||
theme: 'connected',
|
||||
showDates: true,
|
||||
items: [
|
||||
{
|
||||
id: '16',
|
||||
title: 'Basic Dashboard',
|
||||
description: 'Simple dashboard with essential metrics and basic user management.',
|
||||
date: 'v1.0',
|
||||
status: 'completed',
|
||||
icon: 'check'
|
||||
},
|
||||
{
|
||||
id: '17',
|
||||
title: 'Advanced Analytics',
|
||||
description: 'Added detailed analytics, custom reports, and data visualization tools.',
|
||||
date: 'v1.5',
|
||||
status: 'completed',
|
||||
icon: 'check'
|
||||
},
|
||||
{
|
||||
id: '18',
|
||||
title: 'Team Collaboration',
|
||||
description: 'Introduced real-time collaboration features, comments, and notification system.',
|
||||
date: 'v2.0',
|
||||
status: 'completed',
|
||||
icon: 'check'
|
||||
},
|
||||
{
|
||||
id: '19',
|
||||
title: 'AI-Powered Insights',
|
||||
description: 'Implementing machine learning algorithms for predictive analytics and smart recommendations.',
|
||||
date: 'v2.5',
|
||||
status: 'current',
|
||||
icon: 'rocket',
|
||||
badge: 'In Progress'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
roadmapConfig: TimelineConfig = {
|
||||
title: '2024 Product Roadmap',
|
||||
subtitle: 'Exciting features and improvements coming soon',
|
||||
orientation: 'horizontal',
|
||||
theme: 'default',
|
||||
showDates: true,
|
||||
items: [
|
||||
{
|
||||
id: '20',
|
||||
title: 'Mobile App',
|
||||
description: 'Native iOS and Android applications with offline support.',
|
||||
date: 'Q1 2024',
|
||||
status: 'completed',
|
||||
icon: 'check'
|
||||
},
|
||||
{
|
||||
id: '21',
|
||||
title: 'API v3',
|
||||
description: 'Complete API overhaul with GraphQL support and improved performance.',
|
||||
date: 'Q2 2024',
|
||||
status: 'current',
|
||||
icon: 'rocket'
|
||||
},
|
||||
{
|
||||
id: '22',
|
||||
title: 'Enterprise Features',
|
||||
description: 'SSO, advanced permissions, audit logs, and compliance tools.',
|
||||
date: 'Q3 2024',
|
||||
status: 'upcoming',
|
||||
badge: 'Coming Soon'
|
||||
},
|
||||
{
|
||||
id: '23',
|
||||
title: 'Global Expansion',
|
||||
description: 'Multi-language support, regional data centers, and local partnerships.',
|
||||
date: 'Q4 2024',
|
||||
status: 'upcoming'
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common';
|
||||
import { VideoPlayerComponent, VideoSource, VideoTrack, VideoPlayerSize, VideoPlayerVariant } from '../../../../../ui-essentials/src/lib/components/media/video-player/video-player.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-video-player-demo',
|
||||
selector: 'app-video-player-demo',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
||||
@@ -269,6 +269,27 @@ export class DashboardComponent {
|
||||
];
|
||||
this.addMenuItem("overlays", "Overlays", this.faLayerGroup, overlaysChildren);
|
||||
|
||||
|
||||
// Landing category
|
||||
const landingChildren = [
|
||||
|
||||
|
||||
this.createChildItem("conversion", "Conversion", this.faSquare),
|
||||
this.createChildItem("landing-feature-grid", "Feature Grid", this.faSquare),
|
||||
this.createChildItem("landing-testimonials", "Testimonials", this.faBars),
|
||||
this.createChildItem("landing-logo-cloud", "Logo Cloud", this.faCaretUp),
|
||||
this.createChildItem("landing-statistics", "Statistics", this.faCaretUp),
|
||||
|
||||
this.createChildItem("landing-faq", "FAQ", this.faCaretUp),
|
||||
this.createChildItem("landing-team", "Team", this.faCaretUp),
|
||||
this.createChildItem("landing-timeline", "Timeline", this.faCaretUp),
|
||||
this.createChildItem("landing-templates", "Templates", this.faCaretUp),
|
||||
|
||||
|
||||
];
|
||||
this.addMenuItem("landing", "Landing", this.faLayerGroup, landingChildren);
|
||||
|
||||
|
||||
// Utilities (standalone)
|
||||
this.addMenuItem("fontawesome", "FontAwesome Icons", this.faCheckSquare);
|
||||
this.addMenuItem("accessibility", "Accessability", this.faCheckSquare);
|
||||
@@ -277,6 +298,7 @@ export class DashboardComponent {
|
||||
this.addMenuItem("font-manager", "Font Manager", this.faCheckSquare);
|
||||
this.addMenuItem("data-utils", "Data Utils", this.faCheckSquare);
|
||||
this.addMenuItem("code-display", "Code Display", this.faCheckSquare);
|
||||
this.addMenuItem("backgrounds", "Backgrounds", this.faCheckSquare);
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
overflow-x: auto;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: $base-typography-line-height-relaxed;
|
||||
line-height: 1.5;
|
||||
display: flex;
|
||||
|
||||
&--with-line-numbers {
|
||||
@@ -116,7 +116,7 @@
|
||||
.code-block__line-number {
|
||||
color: var(--code-line-number, #{$semantic-color-text-tertiary});
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
line-height: 1.5;
|
||||
padding: 0 $semantic-spacing-component-xs;
|
||||
|
||||
&--highlighted {
|
||||
@@ -134,7 +134,7 @@
|
||||
color: var(--code-text, #{$semantic-color-text-primary});
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
line-height: 1.5;
|
||||
white-space: pre;
|
||||
overflow: visible;
|
||||
flex: 1;
|
||||
|
||||
@@ -31,11 +31,11 @@ export type CodeBlockVariant = 'default' | 'minimal' | 'bordered';
|
||||
@if (showLineNumbers && lineCount() > 1) {
|
||||
<div class="code-block__line-numbers" [attr.aria-hidden]="true">
|
||||
@for (lineNum of lineNumbers(); track lineNum) {
|
||||
<span
|
||||
<div
|
||||
class="code-block__line-number"
|
||||
[class.code-block__line-number--highlighted]="isLineHighlighted(lineNum)">
|
||||
{{ lineNum }}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -95,7 +95,14 @@ export class CodeBlockComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
readonly lineCount = computed(() => {
|
||||
return this.code ? this.code.split('\n').length : 0;
|
||||
if (!this.code) return 0;
|
||||
|
||||
const lines = this.code.split('\n');
|
||||
// Remove the last empty line if the code ends with a newline
|
||||
if (lines.length > 1 && lines[lines.length - 1] === '') {
|
||||
return lines.length - 1;
|
||||
}
|
||||
return lines.length;
|
||||
});
|
||||
|
||||
readonly lineNumbers = computed(() => {
|
||||
|
||||
@@ -154,7 +154,7 @@
|
||||
overflow: visible;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: $base-typography-line-height-relaxed;
|
||||
line-height: 1.5;
|
||||
display: flex;
|
||||
|
||||
&--with-line-numbers {
|
||||
|
||||
@@ -62,13 +62,13 @@ export class SyntaxHighlighterService {
|
||||
}
|
||||
|
||||
// Load Prism core
|
||||
const prismModule = await import(/* @vite-ignore */ 'prismjs');
|
||||
const prismModule = await import('prismjs');
|
||||
const Prism = prismModule.default || prismModule;
|
||||
|
||||
// Ensure global availability
|
||||
(window as any).Prism = Prism;
|
||||
this.prismLoaded = true;
|
||||
|
||||
this.prismLoaded = true;
|
||||
resolve(Prism);
|
||||
} catch (error) {
|
||||
console.warn('Failed to load Prism:', error);
|
||||
@@ -90,29 +90,14 @@ export class SyntaxHighlighterService {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Map of language identifiers to their import paths
|
||||
const languageMap: Record<string, string> = {
|
||||
'typescript': 'prismjs/components/prism-typescript',
|
||||
'javascript': 'prismjs/components/prism-javascript',
|
||||
'css': 'prismjs/components/prism-css',
|
||||
'scss': 'prismjs/components/prism-scss',
|
||||
'json': 'prismjs/components/prism-json',
|
||||
'markup': 'prismjs/components/prism-markup',
|
||||
'html': 'prismjs/components/prism-markup',
|
||||
'bash': 'prismjs/components/prism-bash',
|
||||
'python': 'prismjs/components/prism-python',
|
||||
'java': 'prismjs/components/prism-java',
|
||||
'csharp': 'prismjs/components/prism-csharp'
|
||||
};
|
||||
|
||||
const importPath = languageMap[language];
|
||||
if (importPath) {
|
||||
await import(/* @vite-ignore */ importPath);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load language ${language}:`, error);
|
||||
// Skip loading for unsupported languages or use fallback
|
||||
if (!this.isLanguageSupported(language)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For now, just use the built-in languages or fallback gracefully
|
||||
// This avoids the import issues while still providing basic highlighting
|
||||
console.warn(`Language '${language}' not preloaded, using fallback`);
|
||||
}
|
||||
|
||||
isLanguageSupported(language: string): boolean {
|
||||
|
||||
@@ -22,14 +22,7 @@ export interface FabMenuItem {
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<div
|
||||
class="ui-fab-menu"
|
||||
[class.ui-fab-menu--{{size}}]="size"
|
||||
[class.ui-fab-menu--{{variant}}]="variant"
|
||||
[class.ui-fab-menu--{{position}}]="position"
|
||||
[class.ui-fab-menu--{{direction}}]="actualDirection"
|
||||
[class.ui-fab-menu--open]="isOpen"
|
||||
[class.ui-fab-menu--disabled]="disabled"
|
||||
[class.ui-fab-menu--animating]="isAnimating">
|
||||
[class]="getFabMenuClasses()">
|
||||
|
||||
<!-- Menu Items -->
|
||||
@if (isOpen) {
|
||||
@@ -37,9 +30,7 @@ export interface FabMenuItem {
|
||||
@for (item of menuItems; track item.id) {
|
||||
<button
|
||||
type="button"
|
||||
class="ui-fab-menu__item"
|
||||
[class.ui-fab-menu__item--{{item.variant || variant}}]="item.variant || variant"
|
||||
[class.ui-fab-menu__item--disabled]="item.disabled"
|
||||
[class]="getItemClasses(item)"
|
||||
[disabled]="item.disabled || disabled"
|
||||
[attr.aria-label]="item.label"
|
||||
role="menuitem"
|
||||
@@ -62,10 +53,7 @@ export interface FabMenuItem {
|
||||
<!-- Main FAB Button -->
|
||||
<button
|
||||
type="button"
|
||||
class="ui-fab-menu__trigger"
|
||||
[class.ui-fab-menu__trigger--{{size}}]="size"
|
||||
[class.ui-fab-menu__trigger--{{variant}}]="variant"
|
||||
[class.ui-fab-menu__trigger--active]="isOpen"
|
||||
[class]="getTriggerClasses()"
|
||||
[disabled]="disabled"
|
||||
[attr.aria-expanded]="isOpen"
|
||||
[attr.aria-haspopup]="true"
|
||||
@@ -134,6 +122,37 @@ export class FabMenuComponent {
|
||||
return 'up';
|
||||
}
|
||||
}
|
||||
|
||||
getFabMenuClasses(): string {
|
||||
return [
|
||||
'ui-fab-menu',
|
||||
`ui-fab-menu--${this.size}`,
|
||||
`ui-fab-menu--${this.variant}`,
|
||||
`ui-fab-menu--${this.position}`,
|
||||
`ui-fab-menu--${this.actualDirection}`,
|
||||
this.isOpen ? 'ui-fab-menu--open' : '',
|
||||
this.disabled ? 'ui-fab-menu--disabled' : '',
|
||||
this.isAnimating ? 'ui-fab-menu--animating' : ''
|
||||
].filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
getTriggerClasses(): string {
|
||||
return [
|
||||
'ui-fab-menu__trigger',
|
||||
`ui-fab-menu__trigger--${this.size}`,
|
||||
`ui-fab-menu__trigger--${this.variant}`,
|
||||
this.isOpen ? 'ui-fab-menu__trigger--active' : ''
|
||||
].filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
getItemClasses(item: FabMenuItem): string {
|
||||
const variant = item.variant || this.variant;
|
||||
return [
|
||||
'ui-fab-menu__item',
|
||||
`ui-fab-menu__item--${variant}`,
|
||||
item.disabled ? 'ui-fab-menu__item--disabled' : ''
|
||||
].filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
@HostListener('document:click', ['$event'])
|
||||
onDocumentClick(event: Event): void {
|
||||
|
||||
@@ -128,10 +128,7 @@ export class AccordionItemComponent {
|
||||
template: `
|
||||
<div
|
||||
class="ui-accordion"
|
||||
[class.ui-accordion--{{size}}]="size"
|
||||
[class.ui-accordion--{{variant}}]="variant"
|
||||
[class.ui-accordion--disabled]="disabled"
|
||||
[class.ui-accordion--multiple]="multiple"
|
||||
[class]="getComponentClasses()"
|
||||
[attr.aria-multiselectable]="multiple"
|
||||
role="group">
|
||||
|
||||
@@ -170,6 +167,24 @@ export class AccordionComponent implements AfterContentInit, OnDestroy {
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
getComponentClasses(): string {
|
||||
const classes = [
|
||||
'ui-accordion',
|
||||
`ui-accordion--${this.size}`,
|
||||
`ui-accordion--${this.variant}`
|
||||
];
|
||||
|
||||
if (this.disabled) {
|
||||
classes.push('ui-accordion--disabled');
|
||||
}
|
||||
|
||||
if (this.multiple) {
|
||||
classes.push('ui-accordion--multiple');
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
// Set up keyboard navigation between accordion items
|
||||
this.setupKeyboardNavigation();
|
||||
|
||||
@@ -24,11 +24,7 @@ export interface CarouselItem {
|
||||
template: `
|
||||
<div
|
||||
class="ui-carousel"
|
||||
[class.ui-carousel--{{size}}]="size"
|
||||
[class.ui-carousel--{{variant}}]="variant"
|
||||
[class.ui-carousel--{{transition}}]="transition"
|
||||
[class.ui-carousel--disabled]="disabled"
|
||||
[class.ui-carousel--autoplay]="autoplay"
|
||||
[class]="getComponentClasses()"
|
||||
[attr.aria-roledescription]="'carousel'"
|
||||
[attr.aria-label]="ariaLabel"
|
||||
role="region">
|
||||
@@ -138,7 +134,7 @@ export interface CarouselItem {
|
||||
|
||||
<!-- Indicators -->
|
||||
@if (showIndicators && indicatorStyle !== 'none' && items.length > 1) {
|
||||
<div class="ui-carousel__indicators" [class.ui-carousel__indicators--{{indicatorStyle}}]="indicatorStyle">
|
||||
<div class="ui-carousel__indicators" [class]="getIndicatorClasses()">
|
||||
@for (item of items; track item.id; let i = $index) {
|
||||
<button
|
||||
class="ui-carousel__indicator"
|
||||
@@ -194,6 +190,35 @@ export class CarouselComponent {
|
||||
|
||||
currentIndex = this._currentIndex.asReadonly();
|
||||
|
||||
getComponentClasses(): string {
|
||||
const classes = [
|
||||
'ui-carousel',
|
||||
`ui-carousel--${this.size}`,
|
||||
`ui-carousel--${this.variant}`,
|
||||
`ui-carousel--${this.transition}`
|
||||
];
|
||||
|
||||
if (this.disabled) {
|
||||
classes.push('ui-carousel--disabled');
|
||||
}
|
||||
|
||||
if (this.autoplay) {
|
||||
classes.push('ui-carousel--autoplay');
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
getIndicatorClasses(): string {
|
||||
const classes = ['ui-carousel__indicators'];
|
||||
|
||||
if (this.indicatorStyle) {
|
||||
classes.push(`ui-carousel__indicators--${this.indicatorStyle}`);
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this._currentIndex.set(this.initialIndex);
|
||||
if (this.autoplay && !this.disabled) {
|
||||
|
||||
@@ -23,16 +23,14 @@ export interface TimelineItem {
|
||||
template: `
|
||||
<div
|
||||
class="ui-timeline"
|
||||
[class.ui-timeline--{{size}}]="size"
|
||||
[class.ui-timeline--{{variant}}]="variant"
|
||||
[class.ui-timeline--{{orientation}}]="orientation"
|
||||
[class]="getComponentClasses()"
|
||||
[attr.role]="'list'"
|
||||
[attr.aria-label]="ariaLabel || 'Timeline'">
|
||||
|
||||
@for (item of items; track item.id) {
|
||||
<div
|
||||
class="ui-timeline__item"
|
||||
[class.ui-timeline__item--{{item.status}}]="item.status"
|
||||
[class]="getItemClasses(item)"
|
||||
[attr.role]="'listitem'"
|
||||
[attr.aria-label]="getItemAriaLabel(item)">
|
||||
|
||||
@@ -73,6 +71,26 @@ export class TimelineComponent {
|
||||
@Input() orientation: TimelineOrientation = 'vertical';
|
||||
@Input() ariaLabel?: string;
|
||||
|
||||
getComponentClasses(): string {
|
||||
const classes = [
|
||||
'ui-timeline',
|
||||
`ui-timeline--${this.size}`,
|
||||
`ui-timeline--${this.variant}`,
|
||||
`ui-timeline--${this.orientation}`
|
||||
];
|
||||
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
getItemClasses(item: TimelineItem): string {
|
||||
const classes = [
|
||||
'ui-timeline__item',
|
||||
`ui-timeline__item--${item.status}`
|
||||
];
|
||||
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
getItemAriaLabel(item: TimelineItem): string {
|
||||
const status = this.getStatusLabel(item.status);
|
||||
const title = item.title || 'Timeline item';
|
||||
|
||||
@@ -13,8 +13,7 @@ type EmptyStateVariant = 'default' | 'search' | 'error' | 'loading' | 'success';
|
||||
template: `
|
||||
<div
|
||||
class="ui-empty-state"
|
||||
[class.ui-empty-state--{{size}}]="size"
|
||||
[class.ui-empty-state--{{variant}}]="variant"
|
||||
[class]="getComponentClasses()"
|
||||
[attr.role]="role"
|
||||
[attr.aria-label]="ariaLabel">
|
||||
|
||||
@@ -68,6 +67,16 @@ export class EmptyStateComponent {
|
||||
|
||||
@Output() action = new EventEmitter<MouseEvent>();
|
||||
|
||||
getComponentClasses(): string {
|
||||
const classes = [
|
||||
'ui-empty-state',
|
||||
`ui-empty-state--${this.size}`,
|
||||
`ui-empty-state--${this.variant}`
|
||||
];
|
||||
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
handleAction(event: MouseEvent): void {
|
||||
if (!this.disabled) {
|
||||
this.action.emit(event);
|
||||
|
||||
@@ -15,11 +15,7 @@ type SpinnerSpeed = 'slow' | 'normal' | 'fast';
|
||||
template: `
|
||||
<div
|
||||
class="ui-loading-spinner"
|
||||
[class.ui-loading-spinner--{{size}}]="size"
|
||||
[class.ui-loading-spinner--{{variant}}]="variant"
|
||||
[class.ui-loading-spinner--{{type}}]="type"
|
||||
[class.ui-loading-spinner--{{speed}}]="speed !== 'normal'"
|
||||
[class.ui-loading-spinner--disabled]="disabled"
|
||||
[class]="getComponentClasses()"
|
||||
[attr.aria-label]="ariaLabel || defaultAriaLabel"
|
||||
[attr.aria-busy]="!disabled"
|
||||
[attr.role]="role"
|
||||
@@ -82,6 +78,25 @@ export class LoadingSpinnerComponent {
|
||||
|
||||
@Output() clicked = new EventEmitter<MouseEvent>();
|
||||
|
||||
getComponentClasses(): string {
|
||||
const classes = [
|
||||
'ui-loading-spinner',
|
||||
`ui-loading-spinner--${this.size}`,
|
||||
`ui-loading-spinner--${this.variant}`,
|
||||
`ui-loading-spinner--${this.type}`
|
||||
];
|
||||
|
||||
if (this.speed !== 'normal') {
|
||||
classes.push(`ui-loading-spinner--${this.speed}`);
|
||||
}
|
||||
|
||||
if (this.disabled) {
|
||||
classes.push('ui-loading-spinner--disabled');
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
get defaultAriaLabel(): string {
|
||||
if (this.ariaLabel) return this.ariaLabel;
|
||||
return this.label ? `Loading: ${this.label}` : 'Loading';
|
||||
|
||||
@@ -15,11 +15,7 @@ type BentoGridItemVariant = 'default' | 'primary' | 'secondary' | 'elevated';
|
||||
template: `
|
||||
<div
|
||||
class="ui-bento-grid-item"
|
||||
[class.ui-bento-grid-item--span-{{colSpan}}]="colSpan"
|
||||
[class.ui-bento-grid-item--row-span-{{rowSpan}}]="rowSpan"
|
||||
[class.ui-bento-grid-item--featured-{{featured}}]="featured"
|
||||
[class.ui-bento-grid-item--{{variant}}]="variant !== 'default'"
|
||||
[class.ui-bento-grid-item--interactive]="interactive"
|
||||
[class]="getComponentClasses()"
|
||||
[attr.role]="role"
|
||||
[attr.aria-label]="ariaLabel"
|
||||
[attr.tabindex]="interactive ? tabIndex : -1"
|
||||
@@ -52,6 +48,28 @@ export class BentoGridItemComponent {
|
||||
|
||||
@Output() clicked = new EventEmitter<MouseEvent>();
|
||||
|
||||
getComponentClasses(): string {
|
||||
const classes = [
|
||||
'ui-bento-grid-item',
|
||||
`ui-bento-grid-item--span-${this.colSpan}`,
|
||||
`ui-bento-grid-item--row-span-${this.rowSpan}`
|
||||
];
|
||||
|
||||
if (this.featured) {
|
||||
classes.push(`ui-bento-grid-item--featured-${this.featured}`);
|
||||
}
|
||||
|
||||
if (this.variant !== 'default') {
|
||||
classes.push(`ui-bento-grid-item--${this.variant}`);
|
||||
}
|
||||
|
||||
if (this.interactive) {
|
||||
classes.push('ui-bento-grid-item--interactive');
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
handleClick(event: MouseEvent): void {
|
||||
if (this.interactive) {
|
||||
this.clicked.emit(event);
|
||||
|
||||
@@ -14,11 +14,7 @@ type BentoGridRows = 'sm' | 'md' | 'lg';
|
||||
template: `
|
||||
<div
|
||||
class="ui-bento-grid"
|
||||
[class.ui-bento-grid--cols-{{columns}}]="typeof columns === 'number'"
|
||||
[class.ui-bento-grid--{{columns}}]="typeof columns === 'string'"
|
||||
[class.ui-bento-grid--gap-{{gap}}]="gap"
|
||||
[class.ui-bento-grid--rows-{{rowHeight}}]="rowHeight"
|
||||
[class.ui-bento-grid--dense]="dense"
|
||||
[class]="getComponentClasses()"
|
||||
[attr.role]="role"
|
||||
[attr.aria-label]="ariaLabel">
|
||||
|
||||
@@ -34,4 +30,28 @@ export class BentoGridComponent {
|
||||
@Input() dense = true;
|
||||
@Input() role = 'grid';
|
||||
@Input() ariaLabel = 'Bento grid layout';
|
||||
|
||||
getComponentClasses(): string {
|
||||
const classes = ['ui-bento-grid'];
|
||||
|
||||
if (typeof this.columns === 'number') {
|
||||
classes.push(`ui-bento-grid--cols-${this.columns}`);
|
||||
} else if (typeof this.columns === 'string') {
|
||||
classes.push(`ui-bento-grid--${this.columns}`);
|
||||
}
|
||||
|
||||
if (this.gap) {
|
||||
classes.push(`ui-bento-grid--gap-${this.gap}`);
|
||||
}
|
||||
|
||||
if (this.rowHeight) {
|
||||
classes.push(`ui-bento-grid--rows-${this.rowHeight}`);
|
||||
}
|
||||
|
||||
if (this.dense) {
|
||||
classes.push('ui-bento-grid--dense');
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
}
|
||||
}
|
||||
@@ -28,11 +28,7 @@ type BreakpointRule = 'show-sm' | 'hide-sm' | 'show-md' | 'hide-md' | 'show-lg'
|
||||
template: `
|
||||
<div
|
||||
class="ui-breakpoint-container"
|
||||
[class.ui-breakpoint-container--{{visibility}}]="visibility !== 'always'"
|
||||
[class.ui-breakpoint-container--{{displayType}}]="displayType !== 'block'"
|
||||
[class.ui-breakpoint-container--print-hidden]="printHidden"
|
||||
[class.ui-breakpoint-container--print-only]="printOnly"
|
||||
[ngClass]="getBreakpointRuleClasses()"
|
||||
[class]="getComponentClasses()"
|
||||
[attr.role]="role"
|
||||
[attr.aria-hidden]="getAriaHidden()">
|
||||
|
||||
@@ -54,6 +50,36 @@ export class BreakpointContainerComponent {
|
||||
@Input() printOnly = false;
|
||||
@Input() role?: string;
|
||||
|
||||
getComponentClasses(): string {
|
||||
const classes = ['ui-breakpoint-container'];
|
||||
|
||||
if (this.visibility !== 'always') {
|
||||
classes.push(`ui-breakpoint-container--${this.visibility}`);
|
||||
}
|
||||
|
||||
if (this.displayType !== 'block') {
|
||||
classes.push(`ui-breakpoint-container--${this.displayType}`);
|
||||
}
|
||||
|
||||
if (this.printHidden) {
|
||||
classes.push('ui-breakpoint-container--print-hidden');
|
||||
}
|
||||
|
||||
if (this.printOnly) {
|
||||
classes.push('ui-breakpoint-container--print-only');
|
||||
}
|
||||
|
||||
// Add breakpoint rule classes
|
||||
const breakpointRules = this.getBreakpointRuleClasses();
|
||||
Object.keys(breakpointRules).forEach(className => {
|
||||
if (breakpointRules[className]) {
|
||||
classes.push(className);
|
||||
}
|
||||
});
|
||||
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
getBreakpointRuleClasses(): Record<string, boolean> {
|
||||
return {
|
||||
'ui-breakpoint-container--show-sm': this.showSm,
|
||||
|
||||
@@ -17,9 +17,7 @@ export interface FeedItem {
|
||||
template: `
|
||||
<div
|
||||
class="ui-feed-layout"
|
||||
[class.ui-feed-layout--{{size}}]="size"
|
||||
[class.ui-feed-layout--loading]="loading"
|
||||
[class.ui-feed-layout--refresh-enabled]="enableRefresh"
|
||||
[class]="getFeedLayoutClasses()"
|
||||
[attr.aria-busy]="loading"
|
||||
[attr.aria-label]="ariaLabel"
|
||||
role="feed">
|
||||
@@ -121,6 +119,23 @@ export class FeedLayoutComponent implements OnInit, OnDestroy {
|
||||
this.resizeObserver?.disconnect();
|
||||
}
|
||||
|
||||
getFeedLayoutClasses(): string {
|
||||
const classes = [
|
||||
'ui-feed-layout',
|
||||
`ui-feed-layout--${this.size}`,
|
||||
];
|
||||
|
||||
if (this.loading) {
|
||||
classes.push('ui-feed-layout--loading');
|
||||
}
|
||||
|
||||
if (this.enableRefresh) {
|
||||
classes.push('ui-feed-layout--refresh-enabled');
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
private setupResizeObserver(): void {
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
|
||||
@@ -29,11 +29,7 @@ export interface VideoTrack {
|
||||
template: `
|
||||
<div
|
||||
class="ui-video-player"
|
||||
[class.ui-video-player--{{size}}]="size"
|
||||
[class.ui-video-player--{{variant}}]="variant"
|
||||
[class.ui-video-player--disabled]="disabled"
|
||||
[class.ui-video-player--loading]="loading"
|
||||
[class.ui-video-player--fullscreen]="isFullscreen()"
|
||||
[class]="getComponentClasses()"
|
||||
[attr.aria-label]="ariaLabel">
|
||||
|
||||
<!-- Video element -->
|
||||
@@ -280,6 +276,28 @@ export class VideoPlayerComponent implements OnDestroy {
|
||||
|
||||
private controlsHideTimer?: number;
|
||||
|
||||
getComponentClasses(): string {
|
||||
const classes = [
|
||||
'ui-video-player',
|
||||
`ui-video-player--${this.size}`,
|
||||
`ui-video-player--${this.variant}`
|
||||
];
|
||||
|
||||
if (this.disabled) {
|
||||
classes.push('ui-video-player--disabled');
|
||||
}
|
||||
|
||||
if (this.loading()) {
|
||||
classes.push('ui-video-player--loading');
|
||||
}
|
||||
|
||||
if (this.isFullscreen()) {
|
||||
classes.push('ui-video-player--fullscreen');
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.controlsHideTimer) {
|
||||
clearTimeout(this.controlsHideTimer);
|
||||
|
||||
@@ -35,9 +35,7 @@ export interface ModalConfig {
|
||||
@if (isVisible()) {
|
||||
<div
|
||||
class="ui-modal"
|
||||
[class.ui-modal--{{size}}]="size"
|
||||
[class.ui-modal--{{variant}}]="variant"
|
||||
[class.ui-modal--loading]="loading"
|
||||
[class]="getComponentClasses()"
|
||||
role="dialog"
|
||||
[attr.aria-modal]="true"
|
||||
[attr.aria-labelledby]="titleId"
|
||||
@@ -203,6 +201,20 @@ export class ModalComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
getComponentClasses(): string {
|
||||
const classes = [
|
||||
'ui-modal',
|
||||
`ui-modal--${this.size}`,
|
||||
`ui-modal--${this.variant}`
|
||||
];
|
||||
|
||||
if (this.loading) {
|
||||
classes.push('ui-modal--loading');
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.restoreBodyScroll();
|
||||
this.restoreFocus();
|
||||
|
||||
63
projects/ui-landing-pages/README.md
Normal file
63
projects/ui-landing-pages/README.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# UiLandingPages
|
||||
|
||||
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 19.2.0.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
||||
|
||||
```bash
|
||||
ng generate component component-name
|
||||
```
|
||||
|
||||
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
|
||||
|
||||
```bash
|
||||
ng generate --help
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To build the library, run:
|
||||
|
||||
```bash
|
||||
ng build ui-landing-pages
|
||||
```
|
||||
|
||||
This command will compile your project, and the build artifacts will be placed in the `dist/` directory.
|
||||
|
||||
### Publishing the Library
|
||||
|
||||
Once the project is built, you can publish your library by following these steps:
|
||||
|
||||
1. Navigate to the `dist` directory:
|
||||
```bash
|
||||
cd dist/ui-landing-pages
|
||||
```
|
||||
|
||||
2. Run the `npm publish` command to publish your library to the npm registry:
|
||||
```bash
|
||||
npm publish
|
||||
```
|
||||
|
||||
## Running unit tests
|
||||
|
||||
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
|
||||
|
||||
```bash
|
||||
ng test
|
||||
```
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
For end-to-end (e2e) testing, run:
|
||||
|
||||
```bash
|
||||
ng e2e
|
||||
```
|
||||
|
||||
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
||||
7
projects/ui-landing-pages/ng-package.json
Normal file
7
projects/ui-landing-pages/ng-package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
|
||||
"dest": "../../dist/ui-landing-pages",
|
||||
"lib": {
|
||||
"entryFile": "src/public-api.ts"
|
||||
}
|
||||
}
|
||||
12
projects/ui-landing-pages/package.json
Normal file
12
projects/ui-landing-pages/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "ui-landing-pages",
|
||||
"version": "0.0.1",
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^19.2.0",
|
||||
"@angular/core": "^19.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"sideEffects": false
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-lp-faq {
|
||||
padding: $semantic-spacing-layout-section-lg 0;
|
||||
|
||||
&__header {
|
||||
text-align: center;
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h2, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h2, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h2, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h2, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-family: map-get($semantic-typography-body-large, font-family);
|
||||
font-size: map-get($semantic-typography-body-large, font-size);
|
||||
font-weight: map-get($semantic-typography-body-large, font-weight);
|
||||
line-height: map-get($semantic-typography-body-large, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
&__search {
|
||||
max-width: 500px;
|
||||
margin: 0 auto $semantic-spacing-layout-section-sm auto;
|
||||
}
|
||||
|
||||
&__items {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
&__item {
|
||||
border-bottom: 1px solid $semantic-color-border-subtle;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__question {
|
||||
width: 100%;
|
||||
padding: $semantic-spacing-component-lg $semantic-spacing-component-md;
|
||||
background: none;
|
||||
border: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-elevated;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus-ring;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
&__question-text {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-right: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&__toggle-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: $semantic-color-text-secondary;
|
||||
transition: transform $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
flex-shrink: 0;
|
||||
|
||||
svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
&__item--open &__toggle-icon svg {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
&__answer {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height $semantic-motion-duration-normal $semantic-motion-easing-ease;
|
||||
}
|
||||
|
||||
&__item--open &__answer {
|
||||
max-height: 500px; // Adjust based on content needs
|
||||
}
|
||||
|
||||
&__answer-content {
|
||||
padding: 0 $semantic-spacing-component-md $semantic-spacing-component-lg $semantic-spacing-component-md;
|
||||
|
||||
p {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__empty {
|
||||
text-align: center;
|
||||
padding: $semantic-spacing-layout-section-sm 0;
|
||||
|
||||
p {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Theme Variants
|
||||
&--bordered {
|
||||
.ui-lp-faq__item {
|
||||
border: 1px solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-lp-faq__question:hover {
|
||||
background: $semantic-color-surface-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&--minimal {
|
||||
.ui-lp-faq__question {
|
||||
padding: $semantic-spacing-component-md 0;
|
||||
|
||||
&:hover {
|
||||
background: none;
|
||||
color: $semantic-color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-lp-faq__answer-content {
|
||||
padding: 0 0 $semantic-spacing-component-md 0;
|
||||
}
|
||||
|
||||
.ui-lp-faq__item {
|
||||
border-bottom: 1px solid $semantic-color-border-subtle;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
}
|
||||
|
||||
&__question {
|
||||
padding: $semantic-spacing-component-md $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
&__question-text {
|
||||
font-family: map-get($semantic-typography-heading-h5, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h5, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h5, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h5, line-height);
|
||||
}
|
||||
|
||||
&__answer-content {
|
||||
padding: 0 $semantic-spacing-component-sm $semantic-spacing-component-md $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import { Component, Input, ChangeDetectionStrategy, ViewEncapsulation, signal, computed, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ContainerComponent } from 'ui-essentials';
|
||||
import { SearchBarComponent } from 'ui-essentials';
|
||||
import { FAQConfig, FAQItem } from '../../interfaces/content.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-lp-faq',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, ContainerComponent, SearchBarComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<section class="ui-lp-faq" [class]="getComponentClasses()">
|
||||
<ui-container [size]="'lg'" [padding]="'md'">
|
||||
@if (config().title || config().subtitle) {
|
||||
<div class="ui-lp-faq__header">
|
||||
@if (config().title) {
|
||||
<h2 class="ui-lp-faq__title">{{ config().title }}</h2>
|
||||
}
|
||||
|
||||
@if (config().subtitle) {
|
||||
<p class="ui-lp-faq__subtitle">{{ config().subtitle }}</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (config().searchEnabled) {
|
||||
<div class="ui-lp-faq__search">
|
||||
<ui-search-bar
|
||||
[placeholder]="'Search FAQ...'"
|
||||
[(ngModel)]="searchQuery"
|
||||
[size]="'lg'"
|
||||
[clearable]="true">
|
||||
</ui-search-bar>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (filteredItems().length > 0) {
|
||||
<div class="ui-lp-faq__items">
|
||||
@for (item of filteredItems(); track item.id) {
|
||||
<div
|
||||
class="ui-lp-faq__item"
|
||||
[class.ui-lp-faq__item--open]="item.isOpen">
|
||||
<button
|
||||
class="ui-lp-faq__question"
|
||||
(click)="toggleItem(item)"
|
||||
[attr.aria-expanded]="item.isOpen"
|
||||
[attr.aria-controls]="'faq-answer-' + item.id">
|
||||
<span class="ui-lp-faq__question-text">{{ item.question }}</span>
|
||||
<span class="ui-lp-faq__toggle-icon" aria-hidden="true">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M8 4l4 4H4l4-4z"/>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="ui-lp-faq__answer"
|
||||
[id]="'faq-answer-' + item.id"
|
||||
[attr.aria-labelledby]="'faq-question-' + item.id">
|
||||
<div class="ui-lp-faq__answer-content">
|
||||
<p>{{ item.answer }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<div class="ui-lp-faq__empty">
|
||||
<p>No FAQs found matching your search.</p>
|
||||
</div>
|
||||
}
|
||||
</ui-container>
|
||||
</section>
|
||||
`,
|
||||
styleUrl: './faq-section.component.scss'
|
||||
})
|
||||
export class FAQSectionComponent {
|
||||
config = signal<FAQConfig>({
|
||||
title: 'Frequently Asked Questions',
|
||||
items: [],
|
||||
searchEnabled: true,
|
||||
expandMultiple: false,
|
||||
theme: 'default'
|
||||
});
|
||||
|
||||
searchQuery = signal<string>('');
|
||||
|
||||
@Input() set configuration(value: FAQConfig) {
|
||||
// Initialize isOpen state for items if not provided
|
||||
const itemsWithState = value.items.map(item => ({
|
||||
...item,
|
||||
isOpen: item.isOpen ?? false
|
||||
}));
|
||||
|
||||
this.config.set({
|
||||
...value,
|
||||
items: itemsWithState
|
||||
});
|
||||
}
|
||||
|
||||
filteredItems = computed(() => {
|
||||
const query = this.searchQuery().toLowerCase().trim();
|
||||
if (!query) return this.config().items;
|
||||
|
||||
return this.config().items.filter(item =>
|
||||
item.question.toLowerCase().includes(query) ||
|
||||
item.answer.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
|
||||
getComponentClasses(): string {
|
||||
const classes = ['ui-lp-faq'];
|
||||
|
||||
if (this.config().theme) {
|
||||
classes.push(`ui-lp-faq--${this.config().theme}`);
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
toggleItem(item: FAQItem): void {
|
||||
const currentConfig = this.config();
|
||||
const items = [...currentConfig.items];
|
||||
|
||||
if (!currentConfig.expandMultiple) {
|
||||
// Close all other items if expandMultiple is false
|
||||
items.forEach(i => {
|
||||
if (i.id !== item.id) {
|
||||
i.isOpen = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle the clicked item
|
||||
const targetItem = items.find(i => i.id === item.id);
|
||||
if (targetItem) {
|
||||
targetItem.isOpen = !targetItem.isOpen;
|
||||
}
|
||||
|
||||
this.config.set({
|
||||
...currentConfig,
|
||||
items
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './faq-section.component';
|
||||
export * from './team-grid.component';
|
||||
export * from './timeline-section.component';
|
||||
@@ -0,0 +1,219 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-lp-team-grid {
|
||||
padding: $semantic-spacing-layout-section-lg 0;
|
||||
|
||||
&__header {
|
||||
text-align: center;
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h2, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h2, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h2, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h2, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-family: map-get($semantic-typography-body-large, font-family);
|
||||
font-size: map-get($semantic-typography-body-large, font-size);
|
||||
font-weight: map-get($semantic-typography-body-large, font-weight);
|
||||
line-height: map-get($semantic-typography-body-large, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
&__grid {
|
||||
display: grid;
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
|
||||
&--cols-2 {
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
&--cols-3 {
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
}
|
||||
|
||||
&--cols-4 {
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
&__member {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
padding: $semantic-spacing-component-xl;
|
||||
text-align: center;
|
||||
box-shadow: $semantic-shadow-card-rest;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $semantic-shadow-card-hover;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin: 0 auto $semantic-spacing-component-lg auto;
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
border: 3px solid $semantic-color-surface-primary;
|
||||
box-shadow: $semantic-shadow-card-rest;
|
||||
}
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__role {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: 600;
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-primary;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__bio {
|
||||
font-family: map-get($semantic-typography-body-small, font-family);
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
font-weight: map-get($semantic-typography-body-small, font-weight);
|
||||
line-height: map-get($semantic-typography-body-small, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin: $semantic-spacing-component-sm 0 0 0;
|
||||
}
|
||||
|
||||
&__social {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
margin-top: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&__social-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: $semantic-color-surface-primary;
|
||||
color: $semantic-color-text-secondary;
|
||||
text-decoration: none;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
border: 1px solid $semantic-color-border-subtle;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
border-color: $semantic-color-primary;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus-ring;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&__empty {
|
||||
text-align: center;
|
||||
padding: $semantic-spacing-layout-section-sm 0;
|
||||
|
||||
p {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: $semantic-breakpoint-lg - 1) {
|
||||
&__grid {
|
||||
&--cols-4 {
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
}
|
||||
|
||||
&__grid {
|
||||
gap: $semantic-spacing-grid-gap-md;
|
||||
|
||||
&--cols-2,
|
||||
&--cols-3,
|
||||
&--cols-4 {
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
&__member {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
&__grid {
|
||||
&--cols-2,
|
||||
&--cols-3,
|
||||
&--cols-4 {
|
||||
grid-template-columns: 1fr;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-family: map-get($semantic-typography-heading-h5, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h5, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h5, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h5, line-height);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import { Component, Input, ChangeDetectionStrategy, ViewEncapsulation, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ContainerComponent } from 'ui-essentials';
|
||||
import { TeamConfig, TeamMember } from '../../interfaces/content.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-lp-team-grid',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ContainerComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<section class="ui-lp-team-grid">
|
||||
<ui-container [size]="'xl'" [padding]="'md'">
|
||||
@if (config().title || config().subtitle) {
|
||||
<div class="ui-lp-team-grid__header">
|
||||
@if (config().title) {
|
||||
<h2 class="ui-lp-team-grid__title">{{ config().title }}</h2>
|
||||
}
|
||||
|
||||
@if (config().subtitle) {
|
||||
<p class="ui-lp-team-grid__subtitle">{{ config().subtitle }}</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (config().members.length > 0) {
|
||||
<div
|
||||
class="ui-lp-team-grid__grid"
|
||||
[class.ui-lp-team-grid__grid--cols-2]="config().columns === 2"
|
||||
[class.ui-lp-team-grid__grid--cols-3]="config().columns === 3"
|
||||
[class.ui-lp-team-grid__grid--cols-4]="config().columns === 4">
|
||||
|
||||
@for (member of config().members; track member.id) {
|
||||
<div class="ui-lp-team-grid__member">
|
||||
@if (member.image) {
|
||||
<div class="ui-lp-team-grid__avatar">
|
||||
<img
|
||||
[src]="member.image"
|
||||
[alt]="member.name + ' - ' + member.role"
|
||||
class="ui-lp-team-grid__image">
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="ui-lp-team-grid__info">
|
||||
<h3 class="ui-lp-team-grid__name">{{ member.name }}</h3>
|
||||
<p class="ui-lp-team-grid__role">{{ member.role }}</p>
|
||||
|
||||
@if (config().showBio && member.bio) {
|
||||
<p class="ui-lp-team-grid__bio">{{ member.bio }}</p>
|
||||
}
|
||||
|
||||
@if (config().showSocial && member.social) {
|
||||
<div class="ui-lp-team-grid__social">
|
||||
@if (member.social.linkedin) {
|
||||
<a
|
||||
[href]="member.social.linkedin"
|
||||
class="ui-lp-team-grid__social-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
[attr.aria-label]="member.name + ' LinkedIn profile'">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
|
||||
</svg>
|
||||
</a>
|
||||
}
|
||||
|
||||
@if (member.social.twitter) {
|
||||
<a
|
||||
[href]="member.social.twitter"
|
||||
class="ui-lp-team-grid__social-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
[attr.aria-label]="member.name + ' Twitter profile'">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/>
|
||||
</svg>
|
||||
</a>
|
||||
}
|
||||
|
||||
@if (member.social.github) {
|
||||
<a
|
||||
[href]="member.social.github"
|
||||
class="ui-lp-team-grid__social-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
[attr.aria-label]="member.name + ' GitHub profile'">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||||
</svg>
|
||||
</a>
|
||||
}
|
||||
|
||||
@if (member.social.email) {
|
||||
<a
|
||||
[href]="'mailto:' + member.social.email"
|
||||
class="ui-lp-team-grid__social-link"
|
||||
[attr.aria-label]="'Email ' + member.name">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M24 5.457v13.909c0 .904-.732 1.636-1.636 1.636h-3.819V11.73L12 16.64l-6.545-4.91v9.273H1.636A1.636 1.636 0 0 1 0 19.366V5.457c0-.904.732-1.636 1.636-1.636h.832L12 10.77l9.532-6.95h.832c.904 0 1.636.732 1.636 1.637z"/>
|
||||
</svg>
|
||||
</a>
|
||||
}
|
||||
|
||||
@if (member.social.website) {
|
||||
<a
|
||||
[href]="member.social.website"
|
||||
class="ui-lp-team-grid__social-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
[attr.aria-label]="member.name + ' website'">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.94-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>
|
||||
</svg>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<div class="ui-lp-team-grid__empty">
|
||||
<p>No team members to display.</p>
|
||||
</div>
|
||||
}
|
||||
</ui-container>
|
||||
</section>
|
||||
`,
|
||||
styleUrl: './team-grid.component.scss'
|
||||
})
|
||||
export class TeamGridComponent {
|
||||
config = signal<TeamConfig>({
|
||||
title: 'Meet Our Team',
|
||||
members: [],
|
||||
columns: 3,
|
||||
showSocial: true,
|
||||
showBio: true
|
||||
});
|
||||
|
||||
@Input() set configuration(value: TeamConfig) {
|
||||
this.config.set(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-lp-timeline {
|
||||
padding: $semantic-spacing-layout-section-lg 0;
|
||||
|
||||
&__header {
|
||||
text-align: center;
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h2, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h2, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h2, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h2, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-family: map-get($semantic-typography-body-large, font-family);
|
||||
font-size: map-get($semantic-typography-body-large, font-size);
|
||||
font-weight: map-get($semantic-typography-body-large, font-weight);
|
||||
line-height: map-get($semantic-typography-body-large, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
&__container {
|
||||
position: relative;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
// Vertical Timeline (Default)
|
||||
&--vertical {
|
||||
.ui-lp-timeline__line {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background: $semantic-color-border-subtle;
|
||||
}
|
||||
|
||||
.ui-lp-timeline__items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-layout-section-sm;
|
||||
}
|
||||
|
||||
.ui-lp-timeline__item {
|
||||
position: relative;
|
||||
padding-left: 60px;
|
||||
}
|
||||
|
||||
.ui-lp-timeline__marker {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: $semantic-spacing-component-sm;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: $semantic-color-surface-primary;
|
||||
border: 2px solid $semantic-color-border-subtle;
|
||||
z-index: $semantic-z-index-tooltip;
|
||||
}
|
||||
|
||||
.ui-lp-timeline__content {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
box-shadow: $semantic-shadow-card-rest;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -10px;
|
||||
top: 20px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: 10px solid transparent;
|
||||
border-right-color: $semantic-color-surface-elevated;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontal Timeline
|
||||
&--horizontal {
|
||||
.ui-lp-timeline__line {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 20px;
|
||||
height: 2px;
|
||||
background: $semantic-color-border-subtle;
|
||||
}
|
||||
|
||||
.ui-lp-timeline__items {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: $semantic-spacing-component-xl;
|
||||
}
|
||||
|
||||
.ui-lp-timeline__item {
|
||||
position: relative;
|
||||
padding-top: 60px;
|
||||
}
|
||||
|
||||
.ui-lp-timeline__marker {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
transform: translateX(-50%);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: $semantic-color-surface-primary;
|
||||
border: 2px solid $semantic-color-border-subtle;
|
||||
z-index: $semantic-z-index-tooltip;
|
||||
}
|
||||
|
||||
.ui-lp-timeline__content {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
box-shadow: $semantic-shadow-card-rest;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: -10px;
|
||||
transform: translateX(-50%);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: 10px solid transparent;
|
||||
border-bottom-color: $semantic-color-surface-elevated;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Status Variants
|
||||
&__item {
|
||||
&--completed {
|
||||
.ui-lp-timeline__marker {
|
||||
background: $semantic-color-success;
|
||||
border-color: $semantic-color-success;
|
||||
color: $semantic-color-on-primary;
|
||||
}
|
||||
|
||||
.ui-lp-timeline__item-title {
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&--current {
|
||||
.ui-lp-timeline__marker {
|
||||
background: $semantic-color-primary;
|
||||
border-color: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
box-shadow: 0 0 0 4px rgba($semantic-color-primary, 0.2);
|
||||
}
|
||||
|
||||
.ui-lp-timeline__content {
|
||||
border: 2px solid $semantic-color-primary;
|
||||
}
|
||||
|
||||
.ui-lp-timeline__item-title {
|
||||
color: $semantic-color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&--upcoming {
|
||||
.ui-lp-timeline__marker {
|
||||
background: $semantic-color-surface-primary;
|
||||
border-color: $semantic-color-border-subtle;
|
||||
color: $semantic-color-text-secondary;
|
||||
}
|
||||
|
||||
.ui-lp-timeline__item-title {
|
||||
color: $semantic-color-text-secondary;
|
||||
}
|
||||
|
||||
.ui-lp-timeline__description {
|
||||
opacity: $semantic-opacity-subtle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__date {
|
||||
font-family: map-get($semantic-typography-body-small, font-family);
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
font-weight: 600;
|
||||
line-height: map-get($semantic-typography-body-small, line-height);
|
||||
color: $semantic-color-primary;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
&__item-title {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin: 0 0 $semantic-spacing-component-sm 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
&__badge {
|
||||
font-family: map-get($semantic-typography-body-small, font-family);
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
font-weight: 600;
|
||||
line-height: map-get($semantic-typography-body-small, line-height);
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
border-radius: 9999px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__empty {
|
||||
text-align: center;
|
||||
padding: $semantic-spacing-layout-section-sm 0;
|
||||
|
||||
p {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Theme Variants
|
||||
&--minimal {
|
||||
.ui-lp-timeline__content {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
border: 1px solid $semantic-color-border-subtle;
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-lp-timeline__marker {
|
||||
background: $semantic-color-surface-primary;
|
||||
box-shadow: $semantic-shadow-card-rest;
|
||||
}
|
||||
}
|
||||
|
||||
&--connected {
|
||||
.ui-lp-timeline__line {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
$semantic-color-primary,
|
||||
$semantic-color-secondary
|
||||
);
|
||||
}
|
||||
|
||||
&.ui-lp-timeline--horizontal .ui-lp-timeline__line {
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
$semantic-color-primary,
|
||||
$semantic-color-secondary
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
}
|
||||
|
||||
&--horizontal {
|
||||
.ui-lp-timeline__items {
|
||||
grid-template-columns: 1fr;
|
||||
gap: $semantic-spacing-component-lg;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
&--vertical {
|
||||
.ui-lp-timeline__line {
|
||||
left: 15px;
|
||||
}
|
||||
|
||||
.ui-lp-timeline__item {
|
||||
padding-left: 50px;
|
||||
}
|
||||
|
||||
.ui-lp-timeline__marker {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&--horizontal {
|
||||
.ui-lp-timeline__marker {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.ui-lp-timeline__item {
|
||||
padding-top: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
&__item-title {
|
||||
font-family: map-get($semantic-typography-heading-h5, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h5, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h5, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h5, line-height);
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import { Component, Input, ChangeDetectionStrategy, ViewEncapsulation, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ContainerComponent } from 'ui-essentials';
|
||||
import { TimelineConfig, TimelineItem } from '../../interfaces/content.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-lp-timeline',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ContainerComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<section
|
||||
class="ui-lp-timeline"
|
||||
[class]="getComponentClasses()">
|
||||
|
||||
<ui-container [size]="'lg'" [padding]="'md'">
|
||||
@if (config().title || config().subtitle) {
|
||||
<div class="ui-lp-timeline__header">
|
||||
@if (config().title) {
|
||||
<h2 class="ui-lp-timeline__title">{{ config().title }}</h2>
|
||||
}
|
||||
|
||||
@if (config().subtitle) {
|
||||
<p class="ui-lp-timeline__subtitle">{{ config().subtitle }}</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (config().items.length > 0) {
|
||||
<div class="ui-lp-timeline__container">
|
||||
<div class="ui-lp-timeline__line" aria-hidden="true"></div>
|
||||
|
||||
<div class="ui-lp-timeline__items">
|
||||
@for (item of config().items; track item.id; let i = $index) {
|
||||
<div
|
||||
class="ui-lp-timeline__item"
|
||||
[class]="getItemClasses(item)">
|
||||
|
||||
<div class="ui-lp-timeline__marker" aria-hidden="true">
|
||||
@if (item.icon) {
|
||||
<div class="ui-lp-timeline__icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
@switch (item.icon) {
|
||||
@case ('check') {
|
||||
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
|
||||
}
|
||||
@case ('star') {
|
||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01z"/>
|
||||
}
|
||||
@case ('rocket') {
|
||||
<path d="M9.19 6.35c-2.04 2.29-3.44 5.58-3.44 5.58s2.12-1.29 4.69-1.29c-.18-.9-.34-1.8-.34-2.72 0-.66.06-1.3.15-1.93-.36.12-.72.24-1.06.36z"/>
|
||||
}
|
||||
@default {
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
}
|
||||
}
|
||||
</svg>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="ui-lp-timeline__dot"></div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="ui-lp-timeline__content">
|
||||
@if (config().showDates && item.date) {
|
||||
<div class="ui-lp-timeline__date">{{ item.date }}</div>
|
||||
}
|
||||
|
||||
<div class="ui-lp-timeline__info">
|
||||
<h3 class="ui-lp-timeline__item-title">
|
||||
{{ item.title }}
|
||||
@if (item.badge) {
|
||||
<span class="ui-lp-timeline__badge">{{ item.badge }}</span>
|
||||
}
|
||||
</h3>
|
||||
|
||||
<p class="ui-lp-timeline__description">{{ item.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="ui-lp-timeline__empty">
|
||||
<p>No timeline items to display.</p>
|
||||
</div>
|
||||
}
|
||||
</ui-container>
|
||||
</section>
|
||||
`,
|
||||
styleUrl: './timeline-section.component.scss'
|
||||
})
|
||||
export class TimelineSectionComponent {
|
||||
config = signal<TimelineConfig>({
|
||||
title: 'Our Journey',
|
||||
items: [],
|
||||
orientation: 'vertical',
|
||||
theme: 'default',
|
||||
showDates: true
|
||||
});
|
||||
|
||||
@Input() set configuration(value: TimelineConfig) {
|
||||
this.config.set(value);
|
||||
}
|
||||
|
||||
getComponentClasses(): string {
|
||||
const classes = ['ui-lp-timeline'];
|
||||
|
||||
if (this.config().orientation) {
|
||||
classes.push(`ui-lp-timeline--${this.config().orientation}`);
|
||||
}
|
||||
if (this.config().theme) {
|
||||
classes.push(`ui-lp-timeline--${this.config().theme}`);
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
getItemClasses(item: TimelineItem): string {
|
||||
const classes = ['ui-lp-timeline__item'];
|
||||
|
||||
if (item.status) {
|
||||
classes.push(`ui-lp-timeline__item--${item.status}`);
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.contact-form {
|
||||
background: #ffffff;
|
||||
border-radius: 1rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
|
||||
&__header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: 1.25rem;
|
||||
color: #6b7280;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
&__success {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
&__form {
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
|
||||
&--single-column {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
&--two-column {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
&--inline {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.5rem;
|
||||
align-items: flex-end;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__submit {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
&__button {
|
||||
min-width: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
.success-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1.5rem;
|
||||
background: #dcfce7;
|
||||
border: 1px solid #bbf7d0;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
&__icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: #10b981;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__text {
|
||||
color: #166534;
|
||||
font-weight: 500;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
.form-field {
|
||||
&--full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
&--checkbox {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
&__label {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: #1f2937;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
&__required {
|
||||
color: #dc2626;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&__input,
|
||||
&__textarea,
|
||||
&__select {
|
||||
padding: 1rem;
|
||||
border: 2px solid #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1rem;
|
||||
color: #1f2937;
|
||||
background: #ffffff;
|
||||
transition: all 0.2s;
|
||||
outline: none;
|
||||
|
||||
&:focus {
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
&--error {
|
||||
border-color: #dc2626;
|
||||
|
||||
&:focus {
|
||||
border-color: #dc2626;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__textarea {
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
font-family: inherit;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
&__select {
|
||||
cursor: pointer;
|
||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6,9 12,15 18,9'%3e%3c/polyline%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 1rem center;
|
||||
background-size: 16px;
|
||||
padding-right: 3rem;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
&__error {
|
||||
font-size: 0.875rem;
|
||||
color: #dc2626;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
&::before {
|
||||
content: '⚠';
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
&__label {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&__input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&:checked + .checkbox-group__checkmark {
|
||||
background-color: #3b82f6;
|
||||
border-color: #3b82f6;
|
||||
|
||||
&::after {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__checkmark {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid #d1d5db;
|
||||
border-radius: 0.25rem;
|
||||
background: #ffffff;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.2s;
|
||||
margin-top: 2px;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 6px;
|
||||
height: 10px;
|
||||
border: solid white;
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: translate(-50%, -60%) rotate(45deg) scale(0);
|
||||
opacity: 0;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: 1rem;
|
||||
color: #1f2937;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
&__required {
|
||||
color: #dc2626;
|
||||
font-weight: bold;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
&__error {
|
||||
font-size: 0.875rem;
|
||||
color: #dc2626;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-left: 32px;
|
||||
|
||||
&::before {
|
||||
content: '⚠';
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule, AbstractControl } from '@angular/forms';
|
||||
import { ButtonComponent, FlexComponent, ContainerComponent } from 'ui-essentials';
|
||||
import { ContactFormConfig, FormField } from '../../interfaces/conversion.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-contact-form',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
ButtonComponent,
|
||||
FlexComponent,
|
||||
ContainerComponent
|
||||
],
|
||||
template: `
|
||||
<div class="contact-form">
|
||||
<ui-container>
|
||||
|
||||
<!-- Header -->
|
||||
<div *ngIf="config.title || config.description" class="contact-form__header">
|
||||
<h2 *ngIf="config.title" class="contact-form__title">{{ config.title }}</h2>
|
||||
<p *ngIf="config.description" class="contact-form__description">{{ config.description }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Success Message -->
|
||||
<div *ngIf="showSuccess" class="contact-form__success">
|
||||
<div class="success-message">
|
||||
<span class="success-message__icon">✓</span>
|
||||
<span class="success-message__text">{{ config.successMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<form
|
||||
*ngIf="!showSuccess"
|
||||
[formGroup]="contactForm"
|
||||
(ngSubmit)="onSubmit()"
|
||||
class="contact-form__form"
|
||||
[class.contact-form__form--single-column]="config.layout === 'single-column'"
|
||||
[class.contact-form__form--two-column]="config.layout === 'two-column'"
|
||||
[class.contact-form__form--inline]="config.layout === 'inline'">
|
||||
|
||||
<!-- Dynamic Fields -->
|
||||
<div
|
||||
*ngFor="let field of config.fields; trackBy: trackByField"
|
||||
class="form-field"
|
||||
[class.form-field--full-width]="field.type === 'textarea' || config.layout === 'single-column'"
|
||||
[class.form-field--checkbox]="field.type === 'checkbox'">
|
||||
|
||||
<!-- Text Input -->
|
||||
<div *ngIf="field.type === 'text' || field.type === 'email' || field.type === 'tel'" class="input-group">
|
||||
<label [for]="field.name" class="input-group__label">
|
||||
{{ field.label }}
|
||||
<span *ngIf="field.required" class="input-group__required">*</span>
|
||||
</label>
|
||||
<input
|
||||
[id]="field.name"
|
||||
[type]="field.type"
|
||||
[formControlName]="field.name"
|
||||
[placeholder]="field.placeholder || ''"
|
||||
class="input-group__input"
|
||||
[class.input-group__input--error]="isFieldInvalid(field.name)"
|
||||
[attr.aria-describedby]="field.name + '-error'"
|
||||
[attr.aria-invalid]="isFieldInvalid(field.name)">
|
||||
|
||||
<div
|
||||
*ngIf="isFieldInvalid(field.name)"
|
||||
[id]="field.name + '-error'"
|
||||
class="input-group__error"
|
||||
role="alert">
|
||||
{{ getFieldError(field.name) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Textarea -->
|
||||
<div *ngIf="field.type === 'textarea'" class="input-group">
|
||||
<label [for]="field.name" class="input-group__label">
|
||||
{{ field.label }}
|
||||
<span *ngIf="field.required" class="input-group__required">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
[id]="field.name"
|
||||
[formControlName]="field.name"
|
||||
[placeholder]="field.placeholder || ''"
|
||||
[rows]="field.rows || 4"
|
||||
class="input-group__textarea"
|
||||
[class.input-group__textarea--error]="isFieldInvalid(field.name)"
|
||||
[attr.aria-describedby]="field.name + '-error'"
|
||||
[attr.aria-invalid]="isFieldInvalid(field.name)">
|
||||
</textarea>
|
||||
|
||||
<div
|
||||
*ngIf="isFieldInvalid(field.name)"
|
||||
[id]="field.name + '-error'"
|
||||
class="input-group__error"
|
||||
role="alert">
|
||||
{{ getFieldError(field.name) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Select -->
|
||||
<div *ngIf="field.type === 'select'" class="input-group">
|
||||
<label [for]="field.name" class="input-group__label">
|
||||
{{ field.label }}
|
||||
<span *ngIf="field.required" class="input-group__required">*</span>
|
||||
</label>
|
||||
<select
|
||||
[id]="field.name"
|
||||
[formControlName]="field.name"
|
||||
class="input-group__select"
|
||||
[class.input-group__select--error]="isFieldInvalid(field.name)"
|
||||
[attr.aria-describedby]="field.name + '-error'"
|
||||
[attr.aria-invalid]="isFieldInvalid(field.name)">
|
||||
<option value="" disabled>{{ field.placeholder || 'Select an option...' }}</option>
|
||||
<option *ngFor="let option of field.options" [value]="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<div
|
||||
*ngIf="isFieldInvalid(field.name)"
|
||||
[id]="field.name + '-error'"
|
||||
class="input-group__error"
|
||||
role="alert">
|
||||
{{ getFieldError(field.name) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Checkbox -->
|
||||
<div *ngIf="field.type === 'checkbox'" class="checkbox-group">
|
||||
<label class="checkbox-group__label">
|
||||
<input
|
||||
type="checkbox"
|
||||
[formControlName]="field.name"
|
||||
class="checkbox-group__input"
|
||||
[attr.aria-describedby]="field.name + '-error'"
|
||||
[attr.aria-invalid]="isFieldInvalid(field.name)">
|
||||
<span class="checkbox-group__checkmark"></span>
|
||||
<span class="checkbox-group__text">
|
||||
{{ field.label }}
|
||||
<span *ngIf="field.required" class="checkbox-group__required">*</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div
|
||||
*ngIf="isFieldInvalid(field.name)"
|
||||
[id]="field.name + '-error'"
|
||||
class="checkbox-group__error"
|
||||
role="alert">
|
||||
{{ getFieldError(field.name) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="contact-form__submit">
|
||||
<ui-button
|
||||
type="submit"
|
||||
variant="filled"
|
||||
[size]="'large'"
|
||||
[disabled]="contactForm.invalid || isLoading"
|
||||
[loading]="isLoading"
|
||||
class="contact-form__button">
|
||||
{{ config.submitText }}
|
||||
</ui-button>
|
||||
</div>
|
||||
</form>
|
||||
</ui-container>
|
||||
</div>
|
||||
`,
|
||||
styleUrls: ['./contact-form.component.scss']
|
||||
})
|
||||
export class ContactFormComponent implements OnInit {
|
||||
@Input() config!: ContactFormConfig;
|
||||
@Output() submit = new EventEmitter<any>();
|
||||
|
||||
contactForm: FormGroup = new FormGroup({});
|
||||
isLoading = false;
|
||||
showSuccess = false;
|
||||
|
||||
constructor(private fb: FormBuilder) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.buildForm();
|
||||
}
|
||||
|
||||
private buildForm() {
|
||||
const formControls: { [key: string]: any } = {};
|
||||
|
||||
this.config.fields.forEach(field => {
|
||||
const validators = [];
|
||||
|
||||
if (field.required) {
|
||||
if (field.type === 'checkbox') {
|
||||
validators.push(Validators.requiredTrue);
|
||||
} else {
|
||||
validators.push(Validators.required);
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'email') {
|
||||
validators.push(Validators.email);
|
||||
}
|
||||
|
||||
if (field.validation) {
|
||||
if (field.validation.minLength) {
|
||||
validators.push(Validators.minLength(field.validation.minLength));
|
||||
}
|
||||
if (field.validation.maxLength) {
|
||||
validators.push(Validators.maxLength(field.validation.maxLength));
|
||||
}
|
||||
if (field.validation.pattern) {
|
||||
validators.push(Validators.pattern(field.validation.pattern));
|
||||
}
|
||||
if (field.validation.custom) {
|
||||
validators.push((control: AbstractControl) => {
|
||||
const error = field.validation!.custom!(control.value);
|
||||
return error ? { custom: error } : null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const defaultValue = field.type === 'checkbox' ? false : '';
|
||||
formControls[field.name] = [defaultValue, validators];
|
||||
});
|
||||
|
||||
this.contactForm = this.fb.group(formControls);
|
||||
}
|
||||
|
||||
trackByField(index: number, field: FormField): string {
|
||||
return field.name;
|
||||
}
|
||||
|
||||
isFieldInvalid(fieldName: string): boolean {
|
||||
const control = this.contactForm.get(fieldName);
|
||||
return !!(control && control.invalid && control.touched);
|
||||
}
|
||||
|
||||
getFieldError(fieldName: string): string {
|
||||
const control = this.contactForm.get(fieldName);
|
||||
if (!control || !control.errors) return '';
|
||||
|
||||
const field = this.config.fields.find(f => f.name === fieldName);
|
||||
|
||||
if (control.errors['required']) {
|
||||
return `${field?.label} is required`;
|
||||
}
|
||||
if (control.errors['email']) {
|
||||
return 'Please enter a valid email address';
|
||||
}
|
||||
if (control.errors['minlength']) {
|
||||
return `${field?.label} must be at least ${control.errors['minlength'].requiredLength} characters`;
|
||||
}
|
||||
if (control.errors['maxlength']) {
|
||||
return `${field?.label} must not exceed ${control.errors['maxlength'].requiredLength} characters`;
|
||||
}
|
||||
if (control.errors['pattern']) {
|
||||
return `${field?.label} format is invalid`;
|
||||
}
|
||||
if (control.errors['custom']) {
|
||||
return control.errors['custom'];
|
||||
}
|
||||
if (control.errors['requiredTrue']) {
|
||||
return `${field?.label} must be checked`;
|
||||
}
|
||||
|
||||
return 'Invalid input';
|
||||
}
|
||||
|
||||
async onSubmit() {
|
||||
if (this.contactForm.valid) {
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
this.submit.emit(this.contactForm.value);
|
||||
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
this.showSuccess = true;
|
||||
this.contactForm.reset();
|
||||
} catch (error) {
|
||||
console.error('Contact form submission failed:', error);
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
} else {
|
||||
// Mark all fields as touched to show validation errors
|
||||
this.contactForm.markAllAsTouched();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.cta-section {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 6rem 0;
|
||||
|
||||
&--gradient {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 50%, #8b5cf6 100%);
|
||||
}
|
||||
|
||||
&--pattern {
|
||||
background-color: #f8fafc;
|
||||
background-image: radial-gradient(circle at 2px 2px, #e2e8f0 2px, transparent 0);
|
||||
background-size: 60px 60px;
|
||||
}
|
||||
|
||||
&--image {
|
||||
background-color: #f8fafc;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&--solid {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
&__content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
text-align: center;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
&__urgency {
|
||||
margin-bottom: 2rem;
|
||||
|
||||
.countdown {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
&__item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__value {
|
||||
display: block;
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&__label {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.limited-offer {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: #f59e0b;
|
||||
color: #000;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
|
||||
&__text {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
&__remaining {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.social-proof__text {
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 3rem;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
margin-bottom: 2rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: 1.25rem;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-bottom: 3rem;
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__button {
|
||||
min-width: 160px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ButtonComponent, ContainerComponent } from 'ui-essentials';
|
||||
import { CTASectionConfig } from '../../interfaces/conversion.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-cta-section',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ButtonComponent,
|
||||
ContainerComponent,
|
||||
],
|
||||
template: `
|
||||
<section
|
||||
class="cta-section"
|
||||
[class.cta-section--gradient]="config.backgroundType === 'gradient'"
|
||||
[class.cta-section--pattern]="config.backgroundType === 'pattern'"
|
||||
[class.cta-section--image]="config.backgroundType === 'image'"
|
||||
[class.cta-section--solid]="config.backgroundType === 'solid'">
|
||||
|
||||
<ui-container>
|
||||
<div class="cta-section__content">
|
||||
|
||||
<!-- Urgency Indicator -->
|
||||
<div
|
||||
*ngIf="config.urgency"
|
||||
class="cta-section__urgency"
|
||||
[class.cta-section__urgency--countdown]="config.urgency.type === 'countdown'"
|
||||
[class.cta-section__urgency--limited]="config.urgency.type === 'limited-offer'"
|
||||
[class.cta-section__urgency--social]="config.urgency.type === 'social-proof'">
|
||||
|
||||
<div *ngIf="config.urgency.type === 'countdown' && config.urgency.endDate" class="countdown">
|
||||
<div class="countdown__item">
|
||||
<span class="countdown__value">{{ getTimeRemaining().days }}</span>
|
||||
<span class="countdown__label">Days</span>
|
||||
</div>
|
||||
<div class="countdown__item">
|
||||
<span class="countdown__value">{{ getTimeRemaining().hours }}</span>
|
||||
<span class="countdown__label">Hours</span>
|
||||
</div>
|
||||
<div class="countdown__item">
|
||||
<span class="countdown__value">{{ getTimeRemaining().minutes }}</span>
|
||||
<span class="countdown__label">Minutes</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="config.urgency.type === 'limited-offer'" class="limited-offer">
|
||||
<span class="limited-offer__text">{{ config.urgency.text }}</span>
|
||||
<span *ngIf="config.urgency.remaining" class="limited-offer__remaining">
|
||||
Only {{ config.urgency.remaining }} left!
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="config.urgency.type === 'social-proof'" class="social-proof">
|
||||
<span class="social-proof__text">{{ config.urgency.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<h2 class="cta-section__title">{{ config.title }}</h2>
|
||||
|
||||
<p *ngIf="config.description" class="cta-section__description">
|
||||
{{ config.description }}
|
||||
</p>
|
||||
|
||||
<!-- CTA Buttons -->
|
||||
<div class="cta-section__actions">
|
||||
<ui-button
|
||||
[variant]="config.ctaPrimary.variant || 'filled'"
|
||||
[size]="config.ctaPrimary.size || 'large'"
|
||||
[disabled]="config.ctaPrimary.disabled || false"
|
||||
[loading]="config.ctaPrimary.loading || false"
|
||||
(click)="config.ctaPrimary.action()"
|
||||
class="cta-section__button cta-section__button--primary">
|
||||
{{ config.ctaPrimary.text }}
|
||||
</ui-button>
|
||||
|
||||
<ui-button
|
||||
*ngIf="config.ctaSecondary"
|
||||
[variant]="config.ctaSecondary.variant || 'outlined'"
|
||||
[size]="config.ctaSecondary.size || 'large'"
|
||||
[disabled]="config.ctaSecondary.disabled || false"
|
||||
[loading]="config.ctaSecondary.loading || false"
|
||||
(click)="config.ctaSecondary.action()"
|
||||
class="cta-section__button cta-section__button--secondary">
|
||||
{{ config.ctaSecondary.text }}
|
||||
</ui-button>
|
||||
</div>
|
||||
</div>
|
||||
</ui-container>
|
||||
</section>
|
||||
`,
|
||||
styleUrls: ['./cta-section.component.scss']
|
||||
})
|
||||
export class CTASectionComponent {
|
||||
@Input() config!: CTASectionConfig;
|
||||
|
||||
getTimeRemaining(): { days: number; hours: number; minutes: number; seconds: number } {
|
||||
if (!this.config.urgency?.endDate) {
|
||||
return { days: 0, hours: 0, minutes: 0, seconds: 0 };
|
||||
}
|
||||
|
||||
const now = new Date().getTime();
|
||||
const end = this.config.urgency.endDate.getTime();
|
||||
const difference = end - now;
|
||||
|
||||
if (difference > 0) {
|
||||
return {
|
||||
days: Math.floor(difference / (1000 * 60 * 60 * 24)),
|
||||
hours: Math.floor((difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)),
|
||||
minutes: Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60)),
|
||||
seconds: Math.floor((difference % (1000 * 60)) / 1000)
|
||||
};
|
||||
}
|
||||
|
||||
return { days: 0, hours: 0, minutes: 0, seconds: 0 };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './cta-section.component';
|
||||
export * from './pricing-table.component';
|
||||
export * from './newsletter-signup.component';
|
||||
export * from './contact-form.component';
|
||||
@@ -0,0 +1,127 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.newsletter-signup {
|
||||
&__content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: 1rem;
|
||||
color: #6b7280;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
&__success {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
&__form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__privacy {
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
line-height: 1.4;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.success-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1.5rem;
|
||||
background: #dcfce7;
|
||||
border: 1px solid #bbf7d0;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
&__icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: #10b981;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__text {
|
||||
color: #166534;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&__error {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: #dc2626;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
&::before {
|
||||
content: '⚠';
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.email-input {
|
||||
display: flex;
|
||||
border-radius: 0.5rem;
|
||||
border: 2px solid #d1d5db;
|
||||
background: #ffffff;
|
||||
transition: border-color 0.2s;
|
||||
overflow: hidden;
|
||||
|
||||
&:focus-within {
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
&__field {
|
||||
flex: 1;
|
||||
padding: 1rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 1rem;
|
||||
color: #1f2937;
|
||||
outline: none;
|
||||
|
||||
&::placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
}
|
||||
|
||||
&__button {
|
||||
border-radius: 0;
|
||||
min-width: 120px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { ButtonComponent, FlexComponent, CheckboxComponent } from 'ui-essentials';
|
||||
import { NewsletterSignupConfig } from '../../interfaces/conversion.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-newsletter-signup',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
ButtonComponent,
|
||||
FlexComponent,
|
||||
CheckboxComponent
|
||||
],
|
||||
template: `
|
||||
<div
|
||||
class="newsletter-signup"
|
||||
[class.newsletter-signup--inline]="config.variant === 'inline'"
|
||||
[class.newsletter-signup--modal]="config.variant === 'modal'"
|
||||
[class.newsletter-signup--sidebar]="config.variant === 'sidebar'"
|
||||
[class.newsletter-signup--footer]="config.variant === 'footer'">
|
||||
|
||||
<div class="newsletter-signup__content">
|
||||
<!-- Header -->
|
||||
<div class="newsletter-signup__header">
|
||||
<h3 class="newsletter-signup__title">{{ config.title }}</h3>
|
||||
<p *ngIf="config.description" class="newsletter-signup__description">
|
||||
{{ config.description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Success Message -->
|
||||
<div *ngIf="showSuccess" class="newsletter-signup__success">
|
||||
<div class="success-message">
|
||||
<span class="success-message__icon">✓</span>
|
||||
<span class="success-message__text">{{ config.successMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<form
|
||||
*ngIf="!showSuccess"
|
||||
[formGroup]="signupForm"
|
||||
(ngSubmit)="onSubmit()"
|
||||
class="newsletter-signup__form">
|
||||
|
||||
<!-- Email Input -->
|
||||
<div class="form-group">
|
||||
<div class="email-input">
|
||||
<input
|
||||
type="email"
|
||||
formControlName="email"
|
||||
[placeholder]="config.placeholder"
|
||||
class="email-input__field"
|
||||
[class.email-input__field--error]="emailControl?.invalid && emailControl?.touched"
|
||||
[attr.aria-label]="config.placeholder">
|
||||
|
||||
<ui-button
|
||||
type="submit"
|
||||
variant="filled"
|
||||
[size]="'medium'"
|
||||
[disabled]="signupForm.invalid || isLoading"
|
||||
[loading]="isLoading"
|
||||
class="email-input__button">
|
||||
{{ config.ctaText }}
|
||||
</ui-button>
|
||||
</div>
|
||||
|
||||
<!-- Email Validation Error -->
|
||||
<div
|
||||
*ngIf="emailControl?.invalid && emailControl?.touched"
|
||||
class="form-group__error">
|
||||
<span *ngIf="emailControl?.errors?.['required']">Email is required</span>
|
||||
<span *ngIf="emailControl?.errors?.['email']">Please enter a valid email address</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Privacy Checkbox -->
|
||||
<div *ngIf="config.showPrivacyCheckbox" class="form-group">
|
||||
<ui-checkbox
|
||||
formControlName="privacy"
|
||||
[required]="true"
|
||||
class="privacy-checkbox">
|
||||
<span class="privacy-checkbox__text">
|
||||
I agree to receive marketing communications and accept the
|
||||
<a href="/privacy" target="_blank" class="privacy-checkbox__link">privacy policy</a>
|
||||
</span>
|
||||
</ui-checkbox>
|
||||
|
||||
<div
|
||||
*ngIf="privacyControl?.invalid && privacyControl?.touched"
|
||||
class="form-group__error">
|
||||
<span>You must accept the privacy policy to continue</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Privacy Text -->
|
||||
<p *ngIf="config.privacyText && !config.showPrivacyCheckbox" class="newsletter-signup__privacy">
|
||||
{{ config.privacyText }}
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styleUrls: ['./newsletter-signup.component.scss']
|
||||
})
|
||||
export class NewsletterSignupComponent {
|
||||
@Input() config!: NewsletterSignupConfig;
|
||||
@Output() signup = new EventEmitter<{ email: string; privacyAccepted?: boolean }>();
|
||||
|
||||
signupForm: FormGroup;
|
||||
isLoading = false;
|
||||
showSuccess = false;
|
||||
|
||||
constructor(private fb: FormBuilder) {
|
||||
this.signupForm = this.fb.group({
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
privacy: [false]
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.config.showPrivacyCheckbox) {
|
||||
this.signupForm.get('privacy')?.setValidators([Validators.requiredTrue]);
|
||||
}
|
||||
}
|
||||
|
||||
get emailControl() {
|
||||
return this.signupForm.get('email');
|
||||
}
|
||||
|
||||
get privacyControl() {
|
||||
return this.signupForm.get('privacy');
|
||||
}
|
||||
|
||||
async onSubmit() {
|
||||
if (this.signupForm.valid) {
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
const formValue = this.signupForm.value;
|
||||
this.signup.emit({
|
||||
email: formValue.email,
|
||||
privacyAccepted: formValue.privacy
|
||||
});
|
||||
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
this.showSuccess = true;
|
||||
this.signupForm.reset();
|
||||
} catch (error) {
|
||||
console.error('Newsletter signup failed:', error);
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
} else {
|
||||
// Mark all fields as touched to show validation errors
|
||||
this.signupForm.markAllAsTouched();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.pricing-table {
|
||||
padding: 6rem 0;
|
||||
background: #ffffff;
|
||||
|
||||
&__header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
&__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
&__plan {
|
||||
background: #f8fafc;
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
position: relative;
|
||||
border: 2px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
&--popular,
|
||||
&--highlighted {
|
||||
border-color: #3b82f6;
|
||||
transform: scale(1.05);
|
||||
z-index: 1;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05) translateY(-4px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.billing-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
&__label {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
transition: color 0.2s;
|
||||
|
||||
&--active {
|
||||
color: #1f2937;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&__discount {
|
||||
display: inline-block;
|
||||
margin-left: 0.5rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.pricing-plan {
|
||||
&__badge {
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
&__header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
&__description {
|
||||
color: #6b7280;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
&__price {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
&__currency {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
&__amount {
|
||||
font-size: 3rem;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
&__suffix {
|
||||
font-size: 1rem;
|
||||
color: #6b7280;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
&__features {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 3rem 0;
|
||||
}
|
||||
|
||||
&__feature {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
padding: 1rem 0;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&--highlight {
|
||||
background: #eff6ff;
|
||||
margin: 0 -1rem;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&--excluded {
|
||||
opacity: 0.6;
|
||||
|
||||
.pricing-plan__feature-text {
|
||||
text-decoration: line-through;
|
||||
color: #9ca3af;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__feature-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
margin-top: 2px;
|
||||
|
||||
&--check {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
|
||||
&::after {
|
||||
content: '✓';
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
&--cross {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
|
||||
&::after {
|
||||
content: '✕';
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__feature-text {
|
||||
font-size: 1rem;
|
||||
color: #1f2937;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
&__cta {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__button {
|
||||
width: 100%;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ButtonComponent, ContainerComponent, FlexComponent, SwitchComponent } from 'ui-essentials';
|
||||
import { PricingTableConfig, PricingPlan } from '../../interfaces/conversion.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-pricing-table',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ButtonComponent,
|
||||
ContainerComponent,
|
||||
FlexComponent,
|
||||
SwitchComponent
|
||||
],
|
||||
template: `
|
||||
<section class="pricing-table">
|
||||
<ui-container>
|
||||
|
||||
<!-- Billing Toggle -->
|
||||
<div class="pricing-table__header">
|
||||
<div class="billing-toggle">
|
||||
<span
|
||||
class="billing-toggle__label"
|
||||
[class.billing-toggle__label--active]="!isYearly">
|
||||
{{ config.billingToggle.monthlyLabel }}
|
||||
</span>
|
||||
|
||||
<ui-switch
|
||||
[(ngModel)]="isYearly"
|
||||
class="billing-toggle__switch">
|
||||
</ui-switch>
|
||||
|
||||
<span
|
||||
class="billing-toggle__label"
|
||||
[class.billing-toggle__label--active]="isYearly">
|
||||
{{ config.billingToggle.yearlyLabel }}
|
||||
<span *ngIf="config.billingToggle.discountText" class="billing-toggle__discount">
|
||||
{{ config.billingToggle.discountText }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pricing Plans Grid -->
|
||||
<div class="pricing-table__grid">
|
||||
<div
|
||||
*ngFor="let plan of config.plans"
|
||||
class="pricing-table__plan"
|
||||
[class.pricing-table__plan--popular]="plan.popular"
|
||||
[class.pricing-table__plan--highlighted]="config.highlightedPlan === plan.id">
|
||||
|
||||
<!-- Plan Badge -->
|
||||
<div *ngIf="plan.badge || plan.popular" class="pricing-plan__badge">
|
||||
{{ plan.badge || 'Most Popular' }}
|
||||
</div>
|
||||
|
||||
<!-- Plan Header -->
|
||||
<div class="pricing-plan__header">
|
||||
<h3 class="pricing-plan__name">{{ plan.name }}</h3>
|
||||
<p *ngIf="plan.description" class="pricing-plan__description">
|
||||
{{ plan.description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Plan Price -->
|
||||
<div class="pricing-plan__price">
|
||||
<span class="pricing-plan__currency">{{ plan.price.currency }}</span>
|
||||
<span class="pricing-plan__amount">
|
||||
{{ isYearly ? plan.price.yearly : plan.price.monthly }}
|
||||
</span>
|
||||
<span *ngIf="plan.price.suffix" class="pricing-plan__suffix">
|
||||
{{ plan.price.suffix }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Plan Features -->
|
||||
<ul class="pricing-plan__features">
|
||||
<li
|
||||
*ngFor="let feature of plan.features"
|
||||
class="pricing-plan__feature"
|
||||
[class.pricing-plan__feature--included]="feature.included"
|
||||
[class.pricing-plan__feature--excluded]="!feature.included"
|
||||
[class.pricing-plan__feature--highlight]="feature.highlight">
|
||||
|
||||
<span class="pricing-plan__feature-icon"
|
||||
[class.pricing-plan__feature-icon--check]="feature.included"
|
||||
[class.pricing-plan__feature-icon--cross]="!feature.included">
|
||||
</span>
|
||||
|
||||
<span class="pricing-plan__feature-text">
|
||||
{{ feature.name }}
|
||||
<span *ngIf="feature.description" class="pricing-plan__feature-description">
|
||||
{{ feature.description }}
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Plan CTA -->
|
||||
<div class="pricing-plan__cta">
|
||||
<ui-button
|
||||
[variant]="plan.popular ? 'filled' : (plan.cta.variant || 'outlined')"
|
||||
[size]="plan.cta.size || 'large'"
|
||||
[disabled]="plan.cta.disabled || false"
|
||||
[loading]="plan.cta.loading || false"
|
||||
(click)="plan.cta.action()"
|
||||
class="pricing-plan__button">
|
||||
{{ plan.cta.text }}
|
||||
</ui-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Features Comparison Table (if enabled) -->
|
||||
<div *ngIf="config.featuresComparison" class="pricing-table__comparison">
|
||||
<h3 class="comparison__title">Feature Comparison</h3>
|
||||
<div class="comparison__table">
|
||||
<div class="comparison__header">
|
||||
<div class="comparison__cell comparison__cell--feature">Features</div>
|
||||
<div *ngFor="let plan of config.plans" class="comparison__cell comparison__cell--plan">
|
||||
{{ plan.name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngFor="let featureName of getAllFeatureNames()" class="comparison__row">
|
||||
<div class="comparison__cell comparison__cell--feature">{{ featureName }}</div>
|
||||
<div *ngFor="let plan of config.plans" class="comparison__cell comparison__cell--value">
|
||||
<span
|
||||
class="comparison__icon"
|
||||
[class.comparison__icon--check]="planHasFeature(plan, featureName)"
|
||||
[class.comparison__icon--cross]="!planHasFeature(plan, featureName)">
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ui-container>
|
||||
</section>
|
||||
`,
|
||||
styleUrls: ['./pricing-table.component.scss']
|
||||
})
|
||||
export class PricingTableComponent {
|
||||
@Input() config!: PricingTableConfig;
|
||||
|
||||
isYearly = false;
|
||||
|
||||
getAllFeatureNames(): string[] {
|
||||
const allFeatures = new Set<string>();
|
||||
this.config.plans.forEach(plan => {
|
||||
plan.features.forEach(feature => {
|
||||
allFeatures.add(feature.name);
|
||||
});
|
||||
});
|
||||
return Array.from(allFeatures);
|
||||
}
|
||||
|
||||
planHasFeature(plan: PricingPlan, featureName: string): boolean {
|
||||
const feature = plan.features.find(f => f.name === featureName);
|
||||
return feature?.included || false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-lp-feature-grid {
|
||||
padding: $semantic-spacing-layout-section-lg 0;
|
||||
background: $semantic-color-surface-primary;
|
||||
|
||||
// Header Section
|
||||
&__header {
|
||||
text-align: center;
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
max-width: 800px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h2, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h2, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h2, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h2, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-family: map-get($semantic-typography-body-large, font-family);
|
||||
font-size: map-get($semantic-typography-body-large, font-size);
|
||||
font-weight: map-get($semantic-typography-body-large, font-weight);
|
||||
line-height: map-get($semantic-typography-body-large, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// Grid Container
|
||||
&__items {
|
||||
display: grid;
|
||||
gap: $semantic-spacing-grid-gap-lg;
|
||||
|
||||
// Column Variants
|
||||
&--auto {
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
}
|
||||
|
||||
&--2 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
&--3 {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
&--4 {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
// Responsive breakdowns
|
||||
@media (max-width: $semantic-breakpoint-lg - 1) {
|
||||
&--4 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
&--3 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
&--2,
|
||||
&--3,
|
||||
&--4 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
gap: $semantic-spacing-grid-gap-md;
|
||||
}
|
||||
}
|
||||
|
||||
// Individual Item
|
||||
&__item {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-normal $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
// Card Variant
|
||||
&--card &__item {
|
||||
padding: $semantic-spacing-component-xl;
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
box-shadow: $semantic-shadow-card-rest;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $semantic-shadow-card-hover;
|
||||
}
|
||||
}
|
||||
|
||||
// Minimal Variant
|
||||
&--minimal &__item {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-hover;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
}
|
||||
}
|
||||
|
||||
// Bordered Variant
|
||||
&--bordered &__item {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
background: $semantic-color-surface-primary;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
|
||||
&:hover {
|
||||
border-color: $semantic-color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
// Icon Styling
|
||||
&__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: $semantic-sizing-icon-navigation;
|
||||
height: $semantic-sizing-icon-navigation;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
|
||||
fa-icon,
|
||||
i {
|
||||
font-size: $semantic-sizing-icon-navigation;
|
||||
color: $semantic-color-primary;
|
||||
}
|
||||
|
||||
.ui-lp-feature-grid--minimal & {
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
// Image Styling
|
||||
&__image {
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
border-radius: $semantic-border-image-radius;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
transition: transform $semantic-motion-duration-normal $semantic-motion-easing-ease;
|
||||
}
|
||||
|
||||
.ui-lp-feature-grid__item:hover & img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
// Content Area
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
&__item-title {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
// Link Styling
|
||||
&__link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
font-family: map-get($semantic-typography-button-medium, font-family);
|
||||
font-size: map-get($semantic-typography-button-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-button-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-button-medium, line-height);
|
||||
color: $semantic-color-primary;
|
||||
text-decoration: none;
|
||||
margin-top: $semantic-spacing-component-sm;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
color: $semantic-color-primary-hover;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-primary;
|
||||
outline-offset: 2px;
|
||||
border-radius: $semantic-border-button-radius;
|
||||
}
|
||||
}
|
||||
|
||||
&__link-icon {
|
||||
font-size: $semantic-sizing-icon-button;
|
||||
transition: transform $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
.ui-lp-feature-grid__link:hover & {
|
||||
transform: translateX(2px);
|
||||
}
|
||||
}
|
||||
|
||||
// Spacing Variants
|
||||
&--tight &__items {
|
||||
gap: $semantic-spacing-grid-gap-md;
|
||||
}
|
||||
|
||||
&--loose &__items {
|
||||
gap: $semantic-spacing-grid-gap-xl;
|
||||
}
|
||||
|
||||
// List Layout Variant
|
||||
&--list &__items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
&--list &__item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: $semantic-spacing-component-lg;
|
||||
|
||||
.ui-lp-feature-grid__icon {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.ui-lp-feature-grid__content {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Animation States
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
&__item {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
animation: fadeInUp $semantic-motion-duration-slow $semantic-motion-easing-ease forwards;
|
||||
animation-delay: var(--animation-delay, 0ms);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
&__item {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Animation Keyframes
|
||||
@keyframes fadeInUp {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ContainerComponent } from 'ui-essentials';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
|
||||
import { faArrowRight } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FeatureGridConfig, FeatureItem, FeatureLink } from '../../interfaces/feature.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-lp-feature-grid',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ContainerComponent, FaIconComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<section
|
||||
class="ui-lp-feature-grid"
|
||||
[class]="getComponentClasses()"
|
||||
[attr.aria-label]="'Features section'">
|
||||
|
||||
<ui-container [size]="'xl'" [padding]="'lg'">
|
||||
@if (config().title || config().subtitle) {
|
||||
<div class="ui-lp-feature-grid__header">
|
||||
@if (config().title) {
|
||||
<h2 class="ui-lp-feature-grid__title">{{ config().title }}</h2>
|
||||
}
|
||||
@if (config().subtitle) {
|
||||
<p class="ui-lp-feature-grid__subtitle">{{ config().subtitle }}</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div
|
||||
class="ui-lp-feature-grid__items"
|
||||
[class.ui-lp-feature-grid__items--auto]="config().columns === 'auto'"
|
||||
[class.ui-lp-feature-grid__items--2]="config().columns === 2"
|
||||
[class.ui-lp-feature-grid__items--3]="config().columns === 3"
|
||||
[class.ui-lp-feature-grid__items--4]="config().columns === 4">
|
||||
|
||||
@for (feature of config().features; track feature.id; let index = $index) {
|
||||
<div
|
||||
class="ui-lp-feature-grid__item"
|
||||
[attr.data-animation-delay]="index * 100"
|
||||
(click)="handleFeatureClick(feature)">
|
||||
|
||||
@if (config().showIcons && feature.icon) {
|
||||
<div class="ui-lp-feature-grid__icon">
|
||||
@if (feature.iconType === 'fa') {
|
||||
<fa-icon [icon]="getIconDefinition(feature.icon)"></fa-icon>
|
||||
} @else {
|
||||
<i [class]="feature.icon" aria-hidden="true"></i>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (feature.image) {
|
||||
<div class="ui-lp-feature-grid__image">
|
||||
<img
|
||||
[src]="feature.image"
|
||||
[alt]="feature.title"
|
||||
loading="lazy">
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="ui-lp-feature-grid__content">
|
||||
<h3 class="ui-lp-feature-grid__item-title">{{ feature.title }}</h3>
|
||||
<p class="ui-lp-feature-grid__description">{{ feature.description }}</p>
|
||||
|
||||
@if (feature.link) {
|
||||
<a
|
||||
class="ui-lp-feature-grid__link"
|
||||
[href]="feature.link.url"
|
||||
[target]="feature.link.target || '_self'"
|
||||
(click)="handleLinkClick($event, feature.link)">
|
||||
{{ feature.link.text }}
|
||||
<fa-icon [icon]="faArrowRight" class="ui-lp-feature-grid__link-icon"></fa-icon>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</ui-container>
|
||||
</section>
|
||||
`,
|
||||
styleUrl: './feature-grid.component.scss'
|
||||
})
|
||||
export class FeatureGridComponent {
|
||||
config = signal<FeatureGridConfig>({
|
||||
features: [],
|
||||
layout: 'grid',
|
||||
columns: 'auto',
|
||||
variant: 'card',
|
||||
showIcons: true,
|
||||
animationType: 'fade',
|
||||
spacing: 'normal'
|
||||
});
|
||||
|
||||
faArrowRight = faArrowRight;
|
||||
|
||||
@Input() set configuration(value: FeatureGridConfig) {
|
||||
this.config.set(value);
|
||||
}
|
||||
|
||||
@Output() featureClicked = new EventEmitter<FeatureItem>();
|
||||
@Output() linkClicked = new EventEmitter<{ feature: FeatureItem; link: FeatureLink }>();
|
||||
|
||||
getComponentClasses(): string {
|
||||
const classes = ['ui-lp-feature-grid'];
|
||||
|
||||
if (this.config().variant) {
|
||||
classes.push(`ui-lp-feature-grid--${this.config().variant}`);
|
||||
}
|
||||
if (this.config().layout) {
|
||||
classes.push(`ui-lp-feature-grid--${this.config().layout}`);
|
||||
}
|
||||
if (this.config().spacing) {
|
||||
classes.push(`ui-lp-feature-grid--${this.config().spacing}`);
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
handleFeatureClick(feature: FeatureItem): void {
|
||||
this.featureClicked.emit(feature);
|
||||
}
|
||||
|
||||
handleLinkClick(event: Event, link: FeatureLink): void {
|
||||
event.stopPropagation();
|
||||
this.linkClicked.emit({
|
||||
feature: this.config().features.find(f => f.link === link)!,
|
||||
link
|
||||
});
|
||||
}
|
||||
|
||||
getIconDefinition(iconName: string): IconDefinition {
|
||||
// This would need to be expanded with a proper icon mapping service
|
||||
// For now, return a default icon
|
||||
return faArrowRight;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './feature-grid.component';
|
||||
@@ -0,0 +1,169 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-lp-hero {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
// Min Height Variants
|
||||
&--full {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
&--large {
|
||||
min-height: 80vh;
|
||||
}
|
||||
|
||||
&--medium {
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
// Background Variants
|
||||
&--solid {
|
||||
background: $semantic-color-surface-primary;
|
||||
}
|
||||
|
||||
&--gradient {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
$semantic-color-primary,
|
||||
$semantic-color-secondary
|
||||
);
|
||||
}
|
||||
|
||||
&--animated {
|
||||
background: $semantic-color-surface-primary;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// Content Container
|
||||
&__content {
|
||||
position: relative;
|
||||
z-index: $semantic-z-index-dropdown;
|
||||
padding: $semantic-spacing-layout-section-lg 0;
|
||||
}
|
||||
|
||||
// Alignment Variants
|
||||
&--left &__content {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&--center &__content {
|
||||
text-align: center;
|
||||
margin: 0 auto;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
&--right &__content {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
// Typography
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h1, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h1, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h1, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h1, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
|
||||
.ui-lp-hero--gradient &,
|
||||
.ui-lp-hero--image & {
|
||||
color: $semantic-color-on-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-family: map-get($semantic-typography-body-large, font-family);
|
||||
font-size: map-get($semantic-typography-body-large, font-size);
|
||||
font-weight: map-get($semantic-typography-body-large, font-weight);
|
||||
line-height: map-get($semantic-typography-body-large, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
|
||||
.ui-lp-hero--gradient &,
|
||||
.ui-lp-hero--image & {
|
||||
color: $semantic-color-on-primary;
|
||||
opacity: $semantic-opacity-subtle;
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-md;
|
||||
margin-top: $semantic-spacing-layout-section-sm;
|
||||
|
||||
.ui-lp-hero--center & {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ui-lp-hero--right & {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
// Animated Background
|
||||
&__animated-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
-45deg,
|
||||
$semantic-color-primary,
|
||||
$semantic-color-secondary,
|
||||
$semantic-color-primary,
|
||||
$semantic-color-secondary
|
||||
);
|
||||
background-size: 400% 400%;
|
||||
animation: gradientShift 15s ease infinite;
|
||||
z-index: $semantic-z-index-dropdown - 1;
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
&--full {
|
||||
min-height: 100svh; // Use small viewport height for mobile
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h2, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h2, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h2, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h2, line-height);
|
||||
}
|
||||
|
||||
&__actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keyframes for animated background
|
||||
@keyframes gradientShift {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ButtonComponent } from 'ui-essentials';
|
||||
import { ContainerComponent } from 'ui-essentials';
|
||||
import { HeroConfig } from '../../interfaces/hero.interfaces';
|
||||
import { CTAButton } from '../../interfaces/shared.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-lp-hero',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ButtonComponent, ContainerComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<section
|
||||
class="ui-lp-hero"
|
||||
[class]="heroClasses()"
|
||||
[attr.aria-label]="'Hero section'">
|
||||
|
||||
<ui-container [size]="'xl'" [padding]="'lg'">
|
||||
<div class="ui-lp-hero__content">
|
||||
<h1 class="ui-lp-hero__title">{{ config().title }}</h1>
|
||||
|
||||
@if (config().subtitle) {
|
||||
<p class="ui-lp-hero__subtitle">{{ config().subtitle }}</p>
|
||||
}
|
||||
|
||||
@if (config().ctaPrimary || config().ctaSecondary) {
|
||||
<div class="ui-lp-hero__actions">
|
||||
@if (config().ctaPrimary) {
|
||||
<ui-button
|
||||
[variant]="config().ctaPrimary!.variant"
|
||||
[size]="config().ctaPrimary!.size || 'large'"
|
||||
(clicked)="handleCTAClick(config().ctaPrimary!)">
|
||||
{{ config().ctaPrimary!.text }}
|
||||
</ui-button>
|
||||
}
|
||||
|
||||
@if (config().ctaSecondary) {
|
||||
<ui-button
|
||||
[variant]="config().ctaSecondary!.variant"
|
||||
[size]="config().ctaSecondary!.size || 'large'"
|
||||
(clicked)="handleCTAClick(config().ctaSecondary!)">
|
||||
{{ config().ctaSecondary!.text }}
|
||||
</ui-button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</ui-container>
|
||||
|
||||
@if (config().backgroundType === 'animated') {
|
||||
<div class="ui-lp-hero__animated-bg" aria-hidden="true"></div>
|
||||
}
|
||||
</section>
|
||||
`,
|
||||
styleUrl: './hero-section.component.scss'
|
||||
})
|
||||
export class HeroSectionComponent {
|
||||
config = signal<HeroConfig>({
|
||||
title: '',
|
||||
alignment: 'center',
|
||||
backgroundType: 'solid',
|
||||
minHeight: 'large'
|
||||
});
|
||||
|
||||
@Input() set configuration(value: HeroConfig) {
|
||||
this.config.set(value);
|
||||
}
|
||||
|
||||
@Output() ctaClicked = new EventEmitter<CTAButton>();
|
||||
|
||||
/**
|
||||
* Computed classes for the hero element based on configuration
|
||||
*/
|
||||
heroClasses(): string {
|
||||
const config = this.config();
|
||||
const classes = ['ui-lp-hero'];
|
||||
|
||||
if (config.backgroundType) {
|
||||
classes.push(`ui-lp-hero--${config.backgroundType}`);
|
||||
}
|
||||
|
||||
if (config.alignment) {
|
||||
classes.push(`ui-lp-hero--${config.alignment}`);
|
||||
}
|
||||
|
||||
if (config.minHeight) {
|
||||
classes.push(`ui-lp-hero--${config.minHeight}`);
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle CTA button clicks
|
||||
*/
|
||||
handleCTAClick(cta: CTAButton): void {
|
||||
cta.action();
|
||||
this.ctaClicked.emit(cta);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-lp-hero-split {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
// Min Height Variants
|
||||
&--full {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
&--large {
|
||||
min-height: 80vh;
|
||||
}
|
||||
|
||||
&--medium {
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
// Background Variants
|
||||
&--solid {
|
||||
background: $semantic-color-surface-primary;
|
||||
}
|
||||
|
||||
&--gradient {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
$semantic-color-primary,
|
||||
$semantic-color-secondary
|
||||
);
|
||||
}
|
||||
|
||||
// Main Wrapper
|
||||
&__wrapper {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: inherit;
|
||||
}
|
||||
|
||||
// Split Ratio Variants
|
||||
&--50-50 &__left,
|
||||
&--50-50 &__right {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&--60-40 &__left {
|
||||
flex: 0 0 60%;
|
||||
}
|
||||
|
||||
&--60-40 &__right {
|
||||
flex: 0 0 40%;
|
||||
}
|
||||
|
||||
&--40-60 &__left {
|
||||
flex: 0 0 40%;
|
||||
}
|
||||
|
||||
&--40-60 &__right {
|
||||
flex: 0 0 60%;
|
||||
}
|
||||
|
||||
&--70-30 &__left {
|
||||
flex: 0 0 70%;
|
||||
}
|
||||
|
||||
&--70-30 &__right {
|
||||
flex: 0 0 30%;
|
||||
}
|
||||
|
||||
&--30-70 &__left {
|
||||
flex: 0 0 30%;
|
||||
}
|
||||
|
||||
&--30-70 &__right {
|
||||
flex: 0 0 70%;
|
||||
}
|
||||
|
||||
// Left and Right Sections
|
||||
&__left,
|
||||
&__right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $semantic-spacing-layout-section-lg;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__left {
|
||||
background: $semantic-color-surface-primary;
|
||||
|
||||
.ui-lp-hero-split--gradient & {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
$semantic-color-primary,
|
||||
rgba($semantic-color-primary, 0.8)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&__right {
|
||||
background: $semantic-color-surface-secondary;
|
||||
|
||||
.ui-lp-hero-split--gradient & {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba($semantic-color-secondary, 0.8),
|
||||
$semantic-color-secondary
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Content Types
|
||||
&__default-content {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
&__text-content {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
p {
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
}
|
||||
|
||||
.ui-lp-hero-split--gradient & {
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: $semantic-color-on-primary;
|
||||
}
|
||||
|
||||
p {
|
||||
color: $semantic-color-on-primary;
|
||||
opacity: $semantic-opacity-subtle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__image-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
height: auto;
|
||||
object-fit: cover;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
}
|
||||
|
||||
&__video-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__video {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
}
|
||||
|
||||
// Placeholder
|
||||
&__placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
opacity: $semantic-opacity-subtle;
|
||||
}
|
||||
|
||||
&__placeholder-icon {
|
||||
font-size: $semantic-sizing-icon-navigation * 2;
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&__placeholder-text {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
color: $semantic-color-text-secondary;
|
||||
}
|
||||
|
||||
// Typography
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h1, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h1, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h1, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h1, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
|
||||
.ui-lp-hero-split--gradient & {
|
||||
color: $semantic-color-on-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-family: map-get($semantic-typography-body-large, font-family);
|
||||
font-size: map-get($semantic-typography-body-large, font-size);
|
||||
font-weight: map-get($semantic-typography-body-large, font-weight);
|
||||
line-height: map-get($semantic-typography-body-large, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
|
||||
.ui-lp-hero-split--gradient & {
|
||||
color: $semantic-color-on-primary;
|
||||
opacity: $semantic-opacity-subtle;
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-md;
|
||||
margin-top: $semantic-spacing-layout-section-sm;
|
||||
|
||||
.ui-lp-hero-split--center & {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ui-lp-hero-split--right & {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: $semantic-breakpoint-lg - 1) {
|
||||
&__left,
|
||||
&__right {
|
||||
padding: $semantic-spacing-layout-section-md;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h2, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h2, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h2, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h2, line-height);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
&--full {
|
||||
min-height: 100svh;
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
// Reset flex ratios for mobile
|
||||
&__left,
|
||||
&__right {
|
||||
flex: 1 !important;
|
||||
min-height: 50vh;
|
||||
padding: $semantic-spacing-layout-section-sm;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
&__left,
|
||||
&__right {
|
||||
min-height: 40vh;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ButtonComponent } from 'ui-essentials';
|
||||
import { ContainerComponent } from 'ui-essentials';
|
||||
import { HeroSplitConfig } from '../../interfaces/hero.interfaces';
|
||||
import { CTAButton } from '../../interfaces/shared.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-lp-hero-split',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ButtonComponent, ContainerComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<section
|
||||
class="ui-lp-hero-split"
|
||||
[class]="heroClasses()"
|
||||
[attr.aria-label]="'Split screen hero section'">
|
||||
|
||||
<div class="ui-lp-hero-split__wrapper">
|
||||
<!-- Left Content -->
|
||||
<div class="ui-lp-hero-split__left">
|
||||
@if (config().leftContent) {
|
||||
@switch (config().leftContent!.type) {
|
||||
@case ('text') {
|
||||
<div class="ui-lp-hero-split__text-content" [innerHTML]="config().leftContent!.content"></div>
|
||||
}
|
||||
@case ('image') {
|
||||
<div class="ui-lp-hero-split__image-content">
|
||||
<img [src]="config().leftContent!.content" [alt]="'Left content image'" class="ui-lp-hero-split__img" />
|
||||
</div>
|
||||
}
|
||||
@case ('video') {
|
||||
<div class="ui-lp-hero-split__video-content">
|
||||
<video [src]="config().leftContent!.content" class="ui-lp-hero-split__video" controls></video>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
<!-- Default text content -->
|
||||
<div class="ui-lp-hero-split__default-content">
|
||||
<h1 class="ui-lp-hero-split__title">{{ config().title }}</h1>
|
||||
|
||||
@if (config().subtitle) {
|
||||
<p class="ui-lp-hero-split__subtitle">{{ config().subtitle }}</p>
|
||||
}
|
||||
|
||||
@if (config().ctaPrimary || config().ctaSecondary) {
|
||||
<div class="ui-lp-hero-split__actions">
|
||||
@if (config().ctaPrimary) {
|
||||
<ui-button
|
||||
[variant]="config().ctaPrimary!.variant"
|
||||
[size]="config().ctaPrimary!.size || 'large'"
|
||||
(clicked)="handleCTAClick(config().ctaPrimary!)">
|
||||
{{ config().ctaPrimary!.text }}
|
||||
</ui-button>
|
||||
}
|
||||
|
||||
@if (config().ctaSecondary) {
|
||||
<ui-button
|
||||
[variant]="config().ctaSecondary!.variant"
|
||||
[size]="config().ctaSecondary!.size || 'large'"
|
||||
(clicked)="handleCTAClick(config().ctaSecondary!)">
|
||||
{{ config().ctaSecondary!.text }}
|
||||
</ui-button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Right Content -->
|
||||
<div class="ui-lp-hero-split__right">
|
||||
@if (config().rightContent) {
|
||||
@switch (config().rightContent!.type) {
|
||||
@case ('text') {
|
||||
<div class="ui-lp-hero-split__text-content" [innerHTML]="config().rightContent!.content"></div>
|
||||
}
|
||||
@case ('image') {
|
||||
<div class="ui-lp-hero-split__image-content">
|
||||
<img [src]="config().rightContent!.content" [alt]="'Right content image'" class="ui-lp-hero-split__img" />
|
||||
</div>
|
||||
}
|
||||
@case ('video') {
|
||||
<div class="ui-lp-hero-split__video-content">
|
||||
<video [src]="config().rightContent!.content" class="ui-lp-hero-split__video" controls></video>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
<!-- Placeholder for right content -->
|
||||
<div class="ui-lp-hero-split__placeholder">
|
||||
<div class="ui-lp-hero-split__placeholder-icon">🖼️</div>
|
||||
<p class="ui-lp-hero-split__placeholder-text">Right content area</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
`,
|
||||
styleUrl: './hero-split-screen.component.scss'
|
||||
})
|
||||
export class HeroSplitScreenComponent {
|
||||
config = signal<HeroSplitConfig>({
|
||||
title: '',
|
||||
alignment: 'left',
|
||||
backgroundType: 'solid',
|
||||
minHeight: 'large',
|
||||
splitRatio: '50-50'
|
||||
});
|
||||
|
||||
@Input() set configuration(value: HeroSplitConfig) {
|
||||
this.config.set(value);
|
||||
}
|
||||
|
||||
@Output() ctaClicked = new EventEmitter<CTAButton>();
|
||||
|
||||
/**
|
||||
* Computed classes for the hero element based on configuration
|
||||
*/
|
||||
heroClasses(): string {
|
||||
const config = this.config();
|
||||
const classes = ['ui-lp-hero-split'];
|
||||
|
||||
if (config.backgroundType) {
|
||||
classes.push(`ui-lp-hero-split--${config.backgroundType}`);
|
||||
}
|
||||
|
||||
if (config.alignment) {
|
||||
classes.push(`ui-lp-hero-split--${config.alignment}`);
|
||||
}
|
||||
|
||||
if (config.minHeight) {
|
||||
classes.push(`ui-lp-hero-split--${config.minHeight}`);
|
||||
}
|
||||
|
||||
if (config.splitRatio) {
|
||||
classes.push(`ui-lp-hero-split--${config.splitRatio}`);
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle CTA button clicks
|
||||
*/
|
||||
handleCTAClick(cta: CTAButton): void {
|
||||
cta.action();
|
||||
this.ctaClicked.emit(cta);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-lp-hero-image {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
// Min Height Variants
|
||||
&--full {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
&--large {
|
||||
min-height: 80vh;
|
||||
}
|
||||
|
||||
&--medium {
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
// Background Variants
|
||||
&--solid {
|
||||
background: $semantic-color-surface-primary;
|
||||
}
|
||||
|
||||
&--gradient {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
$semantic-color-primary,
|
||||
$semantic-color-secondary
|
||||
);
|
||||
}
|
||||
|
||||
// Wrapper for flex layout
|
||||
&__wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-layout-section-lg;
|
||||
width: 100%;
|
||||
padding: $semantic-spacing-layout-section-lg 0;
|
||||
}
|
||||
|
||||
// Image Position Variants
|
||||
&--image-left &__wrapper {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
&--image-right &__wrapper {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
// Content Section
|
||||
&__content {
|
||||
flex: 1;
|
||||
min-width: 0; // Prevent flex item from overflowing
|
||||
|
||||
.ui-lp-hero-image--left & {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ui-lp-hero-image--center & {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ui-lp-hero-image--right & {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
// Media Section
|
||||
&__media {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
box-shadow: $semantic-shadow-card-rest;
|
||||
}
|
||||
|
||||
// Typography
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h1, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h1, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h1, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h1, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
|
||||
.ui-lp-hero-image--gradient & {
|
||||
color: $semantic-color-on-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-family: map-get($semantic-typography-body-large, font-family);
|
||||
font-size: map-get($semantic-typography-body-large, font-size);
|
||||
font-weight: map-get($semantic-typography-body-large, font-weight);
|
||||
line-height: map-get($semantic-typography-body-large, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin-bottom: $semantic-spacing-content-paragraph;
|
||||
|
||||
.ui-lp-hero-image--gradient & {
|
||||
color: $semantic-color-on-primary;
|
||||
opacity: $semantic-opacity-subtle;
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-md;
|
||||
margin-top: $semantic-spacing-layout-section-sm;
|
||||
|
||||
.ui-lp-hero-image--center &__content & {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ui-lp-hero-image--right &__content & {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: $semantic-breakpoint-lg - 1) {
|
||||
&__wrapper {
|
||||
gap: $semantic-spacing-layout-section-md;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h2, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h2, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h2, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h2, line-height);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
&--full {
|
||||
min-height: 100svh;
|
||||
}
|
||||
|
||||
// Mobile image positioning
|
||||
&--mobile-above &__wrapper {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&--mobile-below &__wrapper {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
&--mobile-hidden &__media {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
gap: $semantic-spacing-layout-section-sm;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__content {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
justify-content: center !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ButtonComponent } from 'ui-essentials';
|
||||
import { ContainerComponent } from 'ui-essentials';
|
||||
import { HeroImageConfig } from '../../interfaces/hero.interfaces';
|
||||
import { CTAButton } from '../../interfaces/shared.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-lp-hero-image',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ButtonComponent, ContainerComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<section
|
||||
class="ui-lp-hero-image"
|
||||
[class]="heroClasses()"
|
||||
[attr.aria-label]="'Hero section with image'">
|
||||
|
||||
<ui-container [size]="'xl'" [padding]="'lg'">
|
||||
<div class="ui-lp-hero-image__wrapper">
|
||||
<div class="ui-lp-hero-image__content">
|
||||
<h1 class="ui-lp-hero-image__title">{{ config().title }}</h1>
|
||||
|
||||
@if (config().subtitle) {
|
||||
<p class="ui-lp-hero-image__subtitle">{{ config().subtitle }}</p>
|
||||
}
|
||||
|
||||
@if (config().ctaPrimary || config().ctaSecondary) {
|
||||
<div class="ui-lp-hero-image__actions">
|
||||
@if (config().ctaPrimary) {
|
||||
<ui-button
|
||||
[variant]="config().ctaPrimary!.variant"
|
||||
[size]="config().ctaPrimary!.size || 'large'"
|
||||
(clicked)="handleCTAClick(config().ctaPrimary!)">
|
||||
{{ config().ctaPrimary!.text }}
|
||||
</ui-button>
|
||||
}
|
||||
|
||||
@if (config().ctaSecondary) {
|
||||
<ui-button
|
||||
[variant]="config().ctaSecondary!.variant"
|
||||
[size]="config().ctaSecondary!.size || 'large'"
|
||||
(clicked)="handleCTAClick(config().ctaSecondary!)">
|
||||
{{ config().ctaSecondary!.text }}
|
||||
</ui-button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (config().imageUrl) {
|
||||
<div class="ui-lp-hero-image__media">
|
||||
<img
|
||||
[src]="config().imageUrl"
|
||||
[alt]="config().imageAlt || 'Hero image'"
|
||||
class="ui-lp-hero-image__img"
|
||||
loading="eager"
|
||||
decoding="async" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</ui-container>
|
||||
</section>
|
||||
`,
|
||||
styleUrl: './hero-with-image.component.scss'
|
||||
})
|
||||
export class HeroWithImageComponent {
|
||||
config = signal<HeroImageConfig>({
|
||||
title: '',
|
||||
alignment: 'left',
|
||||
backgroundType: 'solid',
|
||||
minHeight: 'large',
|
||||
imagePosition: 'right',
|
||||
imageMobile: 'below'
|
||||
});
|
||||
|
||||
@Input() set configuration(value: HeroImageConfig) {
|
||||
this.config.set(value);
|
||||
}
|
||||
|
||||
@Output() ctaClicked = new EventEmitter<CTAButton>();
|
||||
|
||||
/**
|
||||
* Computed classes for the hero element based on configuration
|
||||
*/
|
||||
heroClasses(): string {
|
||||
const config = this.config();
|
||||
const classes = ['ui-lp-hero-image'];
|
||||
|
||||
if (config.backgroundType) {
|
||||
classes.push(`ui-lp-hero-image--${config.backgroundType}`);
|
||||
}
|
||||
|
||||
if (config.alignment) {
|
||||
classes.push(`ui-lp-hero-image--${config.alignment}`);
|
||||
}
|
||||
|
||||
if (config.minHeight) {
|
||||
classes.push(`ui-lp-hero-image--${config.minHeight}`);
|
||||
}
|
||||
|
||||
if (config.imagePosition) {
|
||||
classes.push(`ui-lp-hero-image--image-${config.imagePosition}`);
|
||||
}
|
||||
|
||||
if (config.imageMobile) {
|
||||
classes.push(`ui-lp-hero-image--mobile-${config.imageMobile}`);
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle CTA button clicks
|
||||
*/
|
||||
handleCTAClick(cta: CTAButton): void {
|
||||
cta.action();
|
||||
this.ctaClicked.emit(cta);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './hero-section.component';
|
||||
export * from './hero-with-image.component';
|
||||
export * from './hero-split-screen.component';
|
||||
20
projects/ui-landing-pages/src/lib/components/index.ts
Normal file
20
projects/ui-landing-pages/src/lib/components/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// Hero Components
|
||||
export * from './heroes';
|
||||
|
||||
// Feature Components
|
||||
export * from './features';
|
||||
|
||||
// Social Proof Components
|
||||
export * from './social-proof';
|
||||
|
||||
// Conversion Components
|
||||
export * from './conversion';
|
||||
|
||||
// Navigation Components
|
||||
export * from './navigation';
|
||||
|
||||
// Content Components
|
||||
export * from './content';
|
||||
|
||||
// Template Components
|
||||
export * from './templates';
|
||||
@@ -0,0 +1,452 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-lp-footer {
|
||||
width: 100%;
|
||||
background: $semantic-color-surface-secondary;
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
// Dark theme
|
||||
&--theme-dark {
|
||||
background: $semantic-color-inverse-surface;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
// Main Footer Content
|
||||
&__main {
|
||||
padding: $semantic-spacing-layout-section-lg 0;
|
||||
}
|
||||
|
||||
&__grid {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
// Brand Section
|
||||
&__brand-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-xl;
|
||||
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
gap: $semantic-spacing-component-lg;
|
||||
}
|
||||
}
|
||||
|
||||
// Logo
|
||||
&__logo {
|
||||
margin-bottom: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&__logo-link,
|
||||
&__logo-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: opacity $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
opacity: $semantic-opacity-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&__logo-text {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
.ui-lp-footer--theme-dark & {
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&__logo-image {
|
||||
max-height: 40px;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
// Newsletter Section
|
||||
&__newsletter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-md;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
&__newsletter-title {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin: 0;
|
||||
|
||||
.ui-lp-footer--theme-dark & {
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&__newsletter-description {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin: 0;
|
||||
|
||||
.ui-lp-footer--theme-dark & {
|
||||
color: rgba($semantic-color-text-primary, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
&__newsletter-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
&__newsletter-input-group {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
&__newsletter-input {
|
||||
flex: 1;
|
||||
padding: $semantic-spacing-component-md;
|
||||
border: 1px solid $semantic-color-border-subtle;
|
||||
border-radius: $semantic-border-input-radius;
|
||||
background: $semantic-color-surface-primary;
|
||||
color: $semantic-color-text-primary;
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&::placeholder {
|
||||
color: $semantic-color-text-secondary;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: $semantic-color-focus;
|
||||
box-shadow: 0 0 0 2px rgba($semantic-color-focus, 0.2);
|
||||
}
|
||||
|
||||
&:invalid {
|
||||
border-color: $semantic-color-error;
|
||||
}
|
||||
|
||||
.ui-lp-footer--theme-dark & {
|
||||
background: $semantic-color-inverse-surface;
|
||||
border-color: $semantic-color-border-secondary;
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
&::placeholder {
|
||||
color: rgba($semantic-color-text-primary, 0.6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__newsletter-button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__newsletter-status {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
border-radius: $semantic-border-input-radius;
|
||||
font-family: map-get($semantic-typography-body-small, font-family);
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
font-weight: map-get($semantic-typography-body-small, font-weight);
|
||||
line-height: map-get($semantic-typography-body-small, line-height);
|
||||
|
||||
&--success {
|
||||
background: rgba($semantic-color-success, 0.1);
|
||||
color: $semantic-color-success;
|
||||
border: 1px solid rgba($semantic-color-success, 0.2);
|
||||
}
|
||||
|
||||
&--error {
|
||||
background: rgba($semantic-color-error, 0.1);
|
||||
color: $semantic-color-error;
|
||||
border: 1px solid rgba($semantic-color-error, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
// Social Links
|
||||
&__social {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&__social-title {
|
||||
font-family: map-get($semantic-typography-heading-h5, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h5, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h5, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h5, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin: 0;
|
||||
|
||||
.ui-lp-footer--theme-dark & {
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&__social-links {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-md;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__social-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: $semantic-sizing-button-height-md;
|
||||
height: $semantic-sizing-button-height-md;
|
||||
border-radius: $semantic-border-button-radius;
|
||||
background: $semantic-color-surface-elevated;
|
||||
color: $semantic-color-text-secondary;
|
||||
text-decoration: none;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.ui-lp-footer--theme-dark & {
|
||||
background: $semantic-color-inverse-surface;
|
||||
color: rgba($semantic-color-text-primary, 0.7);
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__social-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
// Footer Columns
|
||||
&__column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&__column-title {
|
||||
font-family: map-get($semantic-typography-heading-h5, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h5, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h5, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h5, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin: 0;
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
|
||||
.ui-lp-footer--theme-dark & {
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&__column-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
&__column-item {
|
||||
// No specific styles needed
|
||||
}
|
||||
|
||||
&__column-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
text-decoration: none;
|
||||
color: $semantic-color-text-secondary;
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
transition: color $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
color: $semantic-color-primary;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
border-radius: $semantic-border-input-radius;
|
||||
}
|
||||
|
||||
.ui-lp-footer--theme-dark & {
|
||||
color: rgba($semantic-color-text-primary, 0.8);
|
||||
|
||||
&:hover {
|
||||
color: $semantic-color-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__link-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
// Footer Bottom
|
||||
&__bottom {
|
||||
padding: $semantic-spacing-layout-section-sm 0;
|
||||
background: rgba($semantic-color-inverse-surface, 0.05);
|
||||
border-top: 1px solid $semantic-color-border-subtle;
|
||||
|
||||
.ui-lp-footer--theme-dark & {
|
||||
background: rgba($semantic-color-surface-primary, 0.05);
|
||||
border-top-color: rgba($semantic-color-border-secondary, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
&__bottom-content {
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__copyright {
|
||||
font-family: map-get($semantic-typography-body-small, font-family);
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
font-weight: map-get($semantic-typography-body-small, font-weight);
|
||||
line-height: map-get($semantic-typography-body-small, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
|
||||
.ui-lp-footer--theme-dark & {
|
||||
color: rgba($semantic-color-text-primary, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
// Legal Links
|
||||
&__legal-links {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__legal-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
&__legal-link {
|
||||
text-decoration: none;
|
||||
color: $semantic-color-text-secondary;
|
||||
font-family: map-get($semantic-typography-body-small, font-family);
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
font-weight: map-get($semantic-typography-body-small, font-weight);
|
||||
line-height: map-get($semantic-typography-body-small, line-height);
|
||||
transition: color $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
color: $semantic-color-primary;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
border-radius: $semantic-border-input-radius;
|
||||
}
|
||||
|
||||
.ui-lp-footer--theme-dark & {
|
||||
color: rgba($semantic-color-text-primary, 0.7);
|
||||
|
||||
&:hover {
|
||||
color: $semantic-color-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__legal-separator {
|
||||
color: $semantic-color-text-tertiary;
|
||||
user-select: none;
|
||||
|
||||
.ui-lp-footer--theme-dark & {
|
||||
color: rgba($semantic-color-text-primary, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: $semantic-breakpoint-lg - 1) {
|
||||
&__grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
}
|
||||
|
||||
&__brand-section {
|
||||
grid-column: 1 / -1;
|
||||
margin-bottom: $semantic-spacing-component-lg;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
&__main {
|
||||
padding: $semantic-spacing-layout-section-md 0;
|
||||
}
|
||||
|
||||
&__grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: $semantic-spacing-component-xl;
|
||||
}
|
||||
|
||||
&__newsletter {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
&__social-links {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
&__main {
|
||||
padding: $semantic-spacing-layout-section-sm 0;
|
||||
}
|
||||
|
||||
&__newsletter-input-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__newsletter-button {
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
&__bottom-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__legal-links {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { ReactiveFormsModule, FormControl, Validators } from '@angular/forms';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import { ButtonComponent } from 'ui-essentials';
|
||||
import { ContainerComponent } from 'ui-essentials';
|
||||
import { FlexComponent } from 'ui-essentials';
|
||||
import { GridContainerComponent } from 'ui-essentials';
|
||||
import { DividerComponent } from 'ui-essentials';
|
||||
import { FooterConfig, FooterLink } from '../../interfaces/navigation.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-lp-footer',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
ReactiveFormsModule,
|
||||
FontAwesomeModule,
|
||||
ButtonComponent,
|
||||
ContainerComponent,
|
||||
FlexComponent,
|
||||
GridContainerComponent,
|
||||
DividerComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<footer
|
||||
class="ui-lp-footer"
|
||||
[class.ui-lp-footer--theme-dark]="config().theme === 'dark'"
|
||||
[attr.aria-label]="'Site footer'">
|
||||
|
||||
<!-- Main Footer Content -->
|
||||
<div class="ui-lp-footer__main">
|
||||
<ui-container [size]="'xl'" [padding]="'md'">
|
||||
<ui-grid-container
|
||||
[style.grid-template-columns]="getGridColumns()"
|
||||
[gap]="'lg'"
|
||||
class="ui-lp-footer__grid">
|
||||
|
||||
<!-- Company/Brand Section -->
|
||||
@if (config().logo || config().newsletter) {
|
||||
<div class="ui-lp-footer__brand-section">
|
||||
@if (config().logo) {
|
||||
<div class="ui-lp-footer__logo">
|
||||
@if (config().logo!.imageUrl) {
|
||||
<a
|
||||
[routerLink]="config().logo?.url || '/'"
|
||||
class="ui-lp-footer__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-footer__logo-image">
|
||||
</a>
|
||||
} @else if (config().logo!.text) {
|
||||
<a
|
||||
[routerLink]="config().logo?.url || '/'"
|
||||
class="ui-lp-footer__logo-text">
|
||||
{{ config().logo?.text }}
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (config().newsletter) {
|
||||
<div class="ui-lp-footer__newsletter">
|
||||
<h3 class="ui-lp-footer__newsletter-title">
|
||||
{{ config().newsletter!.title }}
|
||||
</h3>
|
||||
|
||||
@if (config().newsletter!.description) {
|
||||
<p class="ui-lp-footer__newsletter-description">
|
||||
{{ config().newsletter!.description }}
|
||||
</p>
|
||||
}
|
||||
|
||||
<form class="ui-lp-footer__newsletter-form" (ngSubmit)="handleNewsletterSubmit()">
|
||||
<div class="ui-lp-footer__newsletter-input-group">
|
||||
<input
|
||||
type="email"
|
||||
[formControl]="emailControl"
|
||||
[placeholder]="config().newsletter!.placeholder"
|
||||
class="ui-lp-footer__newsletter-input"
|
||||
[attr.aria-label]="'Email address'">
|
||||
<ui-button
|
||||
type="submit"
|
||||
[variant]="'filled'"
|
||||
[size]="'medium'"
|
||||
[disabled]="emailControl.invalid"
|
||||
class="ui-lp-footer__newsletter-button">
|
||||
{{ config().newsletter!.buttonText }}
|
||||
</ui-button>
|
||||
</div>
|
||||
|
||||
@if (newsletterStatus()) {
|
||||
<div
|
||||
class="ui-lp-footer__newsletter-status"
|
||||
[class.ui-lp-footer__newsletter-status--success]="newsletterStatus() === 'success'"
|
||||
[class.ui-lp-footer__newsletter-status--error]="newsletterStatus() === 'error'">
|
||||
@switch (newsletterStatus()) {
|
||||
@case ('success') {
|
||||
Thank you for subscribing!
|
||||
}
|
||||
@case ('error') {
|
||||
There was an error. Please try again.
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (config().socialLinks && config().socialLinks!.length > 0) {
|
||||
<div class="ui-lp-footer__social">
|
||||
<h4 class="ui-lp-footer__social-title">Follow Us</h4>
|
||||
<div class="ui-lp-footer__social-links">
|
||||
@for (social of config().socialLinks || []; track social.id) {
|
||||
<a
|
||||
[href]="social.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="ui-lp-footer__social-link"
|
||||
[attr.aria-label]="social.label || social.platform">
|
||||
@if (social.icon) {
|
||||
<i [class]="social.icon" class="ui-lp-footer__social-icon" aria-hidden="true"></i>
|
||||
} @else {
|
||||
<i [class]="getSocialIcon(social.platform)" class="ui-lp-footer__social-icon" aria-hidden="true"></i>
|
||||
}
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Footer Columns -->
|
||||
@for (column of config().columns; track column.id) {
|
||||
<div class="ui-lp-footer__column">
|
||||
<h4 class="ui-lp-footer__column-title">{{ column.title }}</h4>
|
||||
<ul class="ui-lp-footer__column-list">
|
||||
@for (item of column.items; track item.id) {
|
||||
<li class="ui-lp-footer__column-item">
|
||||
<a
|
||||
[href]="item.url"
|
||||
[routerLink]="item.route"
|
||||
[target]="item.target || '_self'"
|
||||
class="ui-lp-footer__column-link"
|
||||
(click)="handleLinkClick(item)">
|
||||
@if (item.icon) {
|
||||
<span class="ui-lp-footer__link-icon">{{ item.icon }}</span>
|
||||
}
|
||||
{{ item.label }}
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</ui-grid-container>
|
||||
</ui-container>
|
||||
</div>
|
||||
|
||||
<!-- Footer Bottom -->
|
||||
@if (config().showDivider !== false) {
|
||||
<ui-divider></ui-divider>
|
||||
}
|
||||
|
||||
<div class="ui-lp-footer__bottom">
|
||||
<ui-container [size]="'xl'" [padding]="'md'">
|
||||
<ui-flex
|
||||
[justify]="'between'"
|
||||
[align]="'center'"
|
||||
[wrap]="'wrap'"
|
||||
[gap]="'md'"
|
||||
class="ui-lp-footer__bottom-content">
|
||||
|
||||
<!-- Copyright -->
|
||||
@if (config().copyright) {
|
||||
<div class="ui-lp-footer__copyright">
|
||||
{{ config().copyright }}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Legal Links -->
|
||||
@if (config().legalLinks && config().legalLinks!.length > 0) {
|
||||
<ul class="ui-lp-footer__legal-links">
|
||||
@for (link of config().legalLinks || []; track link.id; let last = $last) {
|
||||
<li class="ui-lp-footer__legal-item">
|
||||
<a
|
||||
[href]="link.url"
|
||||
[routerLink]="link.route"
|
||||
[target]="link.target || '_self'"
|
||||
class="ui-lp-footer__legal-link"
|
||||
(click)="handleLinkClick(link)">
|
||||
{{ link.label }}
|
||||
</a>
|
||||
@if (!last) {
|
||||
<span class="ui-lp-footer__legal-separator" aria-hidden="true">•</span>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</ui-flex>
|
||||
</ui-container>
|
||||
</div>
|
||||
</footer>
|
||||
`,
|
||||
styleUrl: './footer-section.component.scss'
|
||||
})
|
||||
export class FooterSectionComponent {
|
||||
config = signal<FooterConfig>({
|
||||
columns: [],
|
||||
theme: 'light',
|
||||
showDivider: true
|
||||
});
|
||||
|
||||
emailControl = new FormControl('', [Validators.required, Validators.email]);
|
||||
newsletterStatus = signal<'success' | 'error' | null>(null);
|
||||
|
||||
@Input() set configuration(value: FooterConfig) {
|
||||
this.config.set(value);
|
||||
}
|
||||
|
||||
@Output() linkClicked = new EventEmitter<FooterLink>();
|
||||
@Output() newsletterSubmitted = new EventEmitter<string>();
|
||||
|
||||
getGridColumns(): string {
|
||||
let columns = this.config().columns.length;
|
||||
|
||||
// Add brand section if logo or newsletter exists
|
||||
if (this.config().logo || this.config().newsletter) {
|
||||
columns += 1;
|
||||
}
|
||||
|
||||
const gridColumns = Math.min(columns, 5);
|
||||
return `repeat(${gridColumns}, 1fr)`;
|
||||
}
|
||||
|
||||
handleLinkClick(link: FooterLink): void {
|
||||
if (link.action) {
|
||||
link.action();
|
||||
}
|
||||
this.linkClicked.emit(link);
|
||||
}
|
||||
|
||||
handleNewsletterSubmit(): void {
|
||||
if (this.emailControl.valid && this.emailControl.value) {
|
||||
const email = this.emailControl.value;
|
||||
|
||||
const newsletter = this.config().newsletter;
|
||||
if (newsletter && newsletter.onSubmit) {
|
||||
try {
|
||||
newsletter.onSubmit(email);
|
||||
this.newsletterStatus.set('success');
|
||||
this.emailControl.reset();
|
||||
this.newsletterSubmitted.emit(email);
|
||||
} catch (error) {
|
||||
console.error('Newsletter submission error:', error);
|
||||
this.newsletterStatus.set('error');
|
||||
}
|
||||
}
|
||||
|
||||
// Reset status after 5 seconds
|
||||
setTimeout(() => {
|
||||
this.newsletterStatus.set(null);
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
getSocialIcon(platform: string): string {
|
||||
const icons: Record<string, string> = {
|
||||
facebook: 'fab fa-facebook',
|
||||
twitter: 'fab fa-twitter',
|
||||
instagram: 'fab fa-instagram',
|
||||
linkedin: 'fab fa-linkedin',
|
||||
youtube: 'fab fa-youtube',
|
||||
github: 'fab fa-github',
|
||||
dribbble: 'fab fa-dribbble'
|
||||
};
|
||||
|
||||
return icons[platform] || 'fas fa-link';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './landing-header.component';
|
||||
export * from './footer-section.component';
|
||||
@@ -0,0 +1,431 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-lp-header {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
z-index: $semantic-z-index-sticky;
|
||||
transition: all $semantic-motion-duration-normal $semantic-motion-easing-ease;
|
||||
background: $semantic-color-surface-primary;
|
||||
border-bottom: 1px solid $semantic-color-border-secondary;
|
||||
|
||||
// Transparent variant
|
||||
&--transparent {
|
||||
background: transparent;
|
||||
border-bottom: 1px solid transparent;
|
||||
|
||||
&.ui-lp-header--scrolled {
|
||||
background: rgba($semantic-color-surface-primary, 0.95);
|
||||
border-bottom-color: $semantic-color-border-secondary;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: $semantic-shadow-card-rest;
|
||||
}
|
||||
}
|
||||
|
||||
// Sticky positioning
|
||||
&--sticky {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
// Dark theme
|
||||
&--theme-dark {
|
||||
background: $semantic-color-inverse-surface;
|
||||
border-bottom-color: $semantic-color-border-secondary;
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
&.ui-lp-header--transparent {
|
||||
&.ui-lp-header--scrolled {
|
||||
background: rgba($semantic-color-inverse-surface, 0.95);
|
||||
border-bottom-color: $semantic-color-border-secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Content container
|
||||
&__content {
|
||||
min-height: 64px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// Logo Section
|
||||
&__logo-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__logo-link,
|
||||
&__logo-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: opacity $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
opacity: $semantic-opacity-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&__logo-text {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
.ui-lp-header--theme-dark & {
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&__logo-image {
|
||||
max-height: 48px;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
// Desktop Navigation
|
||||
&__nav--desktop {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__nav-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
gap: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
&__nav-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
|
||||
text-decoration: none;
|
||||
color: $semantic-color-text-primary;
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
border-radius: $semantic-border-button-radius;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-hover;
|
||||
color: $semantic-color-primary;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-focus;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.ui-lp-header--theme-dark & {
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
&:hover {
|
||||
background: rgba($semantic-color-surface-hover, 0.1);
|
||||
color: $semantic-color-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__nav-arrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: transform $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
.ui-lp-header__nav-link--dropdown[aria-expanded="true"] & {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
&__nav-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&__nav-badge {
|
||||
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
|
||||
background: $semantic-color-warning;
|
||||
color: $semantic-color-on-primary;
|
||||
border-radius: 9999px;
|
||||
font-family: map-get($semantic-typography-body-small, font-family);
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
font-weight: map-get($semantic-typography-body-small, font-weight);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
// Dropdown Menu
|
||||
&__dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
min-width: 200px;
|
||||
background: $semantic-color-surface-elevated;
|
||||
border: 1px solid $semantic-color-border-secondary;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
box-shadow: $semantic-shadow-dropdown;
|
||||
padding: $semantic-spacing-component-sm 0;
|
||||
z-index: $semantic-z-index-dropdown;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
animation: dropdownFadeIn $semantic-motion-duration-normal $semantic-motion-easing-ease;
|
||||
|
||||
.ui-lp-header--theme-dark & {
|
||||
background: $semantic-color-inverse-surface;
|
||||
border-color: $semantic-color-border-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
&__dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
|
||||
text-decoration: none;
|
||||
color: $semantic-color-text-primary;
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
transition: background $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-hover;
|
||||
}
|
||||
|
||||
.ui-lp-header--theme-dark & {
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
&:hover {
|
||||
background: rgba($semantic-color-surface-hover, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Actions Section
|
||||
&__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-md;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__cta-button {
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__cta-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&__mobile-toggle {
|
||||
display: none;
|
||||
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile Navigation
|
||||
&__nav--mobile {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background: $semantic-color-surface-primary;
|
||||
border-bottom: 1px solid $semantic-color-border-secondary;
|
||||
box-shadow: $semantic-shadow-card-hover;
|
||||
z-index: $semantic-z-index-dropdown;
|
||||
max-height: calc(100vh - 64px);
|
||||
overflow-y: auto;
|
||||
|
||||
&.ui-lp-header--mobile-menu-open & {
|
||||
display: block;
|
||||
animation: slideDown $semantic-motion-duration-normal $semantic-motion-easing-ease;
|
||||
}
|
||||
|
||||
.ui-lp-header--theme-dark & {
|
||||
background: $semantic-color-inverse-surface;
|
||||
border-bottom-color: $semantic-color-border-secondary;
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
.ui-lp-header--mobile-menu-open & {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__mobile-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: $semantic-spacing-component-md 0;
|
||||
}
|
||||
|
||||
&__mobile-item {
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid $semantic-color-border-subtle;
|
||||
}
|
||||
}
|
||||
|
||||
&__mobile-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: $semantic-spacing-component-md;
|
||||
text-decoration: none;
|
||||
color: $semantic-color-text-primary;
|
||||
font-family: map-get($semantic-typography-body-large, font-family);
|
||||
font-size: map-get($semantic-typography-body-large, font-size);
|
||||
font-weight: map-get($semantic-typography-body-large, font-weight);
|
||||
line-height: map-get($semantic-typography-body-large, line-height);
|
||||
background: transparent;
|
||||
border: none;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
transition: background $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-hover;
|
||||
}
|
||||
|
||||
.ui-lp-header--theme-dark & {
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
&:hover {
|
||||
background: rgba($semantic-color-surface-hover, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__mobile-submenu {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: $semantic-color-surface-secondary;
|
||||
|
||||
.ui-lp-header--theme-dark & {
|
||||
background: rgba($semantic-color-surface-secondary, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
&__mobile-sublink {
|
||||
display: block;
|
||||
padding: $semantic-spacing-component-sm $semantic-spacing-component-lg;
|
||||
text-decoration: none;
|
||||
color: $semantic-color-text-secondary;
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
transition: background $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-hover;
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
.ui-lp-header--theme-dark & {
|
||||
color: rgba($semantic-color-text-primary, 0.7);
|
||||
|
||||
&:hover {
|
||||
background: rgba($semantic-color-surface-hover, 0.1);
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__mobile-cta {
|
||||
padding: $semantic-spacing-component-md;
|
||||
margin-top: $semantic-spacing-component-md;
|
||||
border-top: 1px solid $semantic-color-border-subtle;
|
||||
}
|
||||
|
||||
// Mobile Menu Backdrop
|
||||
&__backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba($semantic-color-inverse-surface, $semantic-opacity-backdrop);
|
||||
z-index: $semantic-z-index-overlay - 1;
|
||||
animation: backdropFadeIn $semantic-motion-duration-normal $semantic-motion-easing-ease;
|
||||
}
|
||||
|
||||
// Mobile menu open state
|
||||
&--mobile-menu-open {
|
||||
.ui-lp-header__nav--mobile {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
&__logo-text {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
}
|
||||
|
||||
&__content {
|
||||
min-height: 64px-sm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Animations
|
||||
@keyframes dropdownFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes backdropFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './testimonial-carousel.component';
|
||||
export * from './logo-cloud.component';
|
||||
export * from './statistics-display.component';
|
||||
@@ -0,0 +1,220 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-lp-logo-cloud {
|
||||
padding: $semantic-spacing-layout-section-md 0;
|
||||
background: $semantic-color-surface-primary;
|
||||
|
||||
// Header Section
|
||||
&__header {
|
||||
text-align: center;
|
||||
margin-bottom: $semantic-spacing-layout-section-sm;
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// Container Layouts
|
||||
&__container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// Row Layout
|
||||
&__container--row {
|
||||
flex-wrap: wrap;
|
||||
gap: $semantic-spacing-component-xl;
|
||||
|
||||
@media (max-width: $semantic-breakpoint-lg - 1) {
|
||||
gap: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
|
||||
// Grid Layout
|
||||
&__container--grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--items-per-row, 5), 1fr);
|
||||
gap: $semantic-spacing-component-xl;
|
||||
width: 100%;
|
||||
|
||||
@media (max-width: $semantic-breakpoint-lg - 1) {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
// Marquee Layout
|
||||
&__container--marquee {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__marquee {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__marquee-track {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-xl;
|
||||
animation: marqueeScroll 30s linear infinite;
|
||||
width: calc(200% + #{$semantic-spacing-component-xl});
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
animation: none;
|
||||
width: auto;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
// Logo Items
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $semantic-spacing-component-md;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-surface-hover;
|
||||
}
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-primary;
|
||||
outline-offset: 2px;
|
||||
border-radius: $semantic-border-button-radius;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
max-height: var(--max-height, 80px);
|
||||
max-width: 200px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
transition: all $semantic-motion-duration-normal $semantic-motion-easing-ease;
|
||||
}
|
||||
|
||||
// Animation delays for staggered entrance
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
animation: fadeInUp $semantic-motion-duration-slow $semantic-motion-easing-ease forwards;
|
||||
animation-delay: var(--animation-delay, 0ms);
|
||||
}
|
||||
}
|
||||
|
||||
// Grayscale Effects
|
||||
&--grayscale &__item img,
|
||||
&__logo--grayscale {
|
||||
filter: grayscale(100%);
|
||||
opacity: $semantic-opacity-subtle;
|
||||
}
|
||||
|
||||
&--hover &__item:hover img,
|
||||
&--hover &__item:hover &__logo--grayscale {
|
||||
filter: grayscale(0%);
|
||||
opacity: 1;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
// Responsive adjustments for marquee
|
||||
.ui-lp-logo-cloud--marquee &__item {
|
||||
flex-shrink: 0;
|
||||
min-width: 180px;
|
||||
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
// Small screen optimizations
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
&__item {
|
||||
padding: $semantic-spacing-component-sm;
|
||||
|
||||
img {
|
||||
max-height: 60px;
|
||||
max-width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
&__marquee-track {
|
||||
gap: $semantic-spacing-component-lg;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
&__item img {
|
||||
max-height: 50px;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
&__marquee-track {
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keyframes
|
||||
@keyframes marqueeScroll {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation, signal, computed } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ContainerComponent } from 'ui-essentials';
|
||||
import { LogoCloudConfig, LogoItem } from '../../interfaces/social-proof.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-lp-logo-cloud',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ContainerComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<section
|
||||
class="ui-lp-logo-cloud"
|
||||
[class]="getComponentClasses()"
|
||||
[attr.aria-label]="'Partners and clients'">
|
||||
|
||||
<ui-container [size]="'xl'" [padding]="'lg'">
|
||||
@if (config().title || config().subtitle) {
|
||||
<div class="ui-lp-logo-cloud__header">
|
||||
@if (config().title) {
|
||||
<h2 class="ui-lp-logo-cloud__title">{{ config().title }}</h2>
|
||||
}
|
||||
@if (config().subtitle) {
|
||||
<p class="ui-lp-logo-cloud__subtitle">{{ config().subtitle }}</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div
|
||||
class="ui-lp-logo-cloud__container"
|
||||
[class.ui-lp-logo-cloud__container--row]="config().layout === 'row'"
|
||||
[class.ui-lp-logo-cloud__container--grid]="config().layout === 'grid'"
|
||||
[class.ui-lp-logo-cloud__container--marquee]="config().layout === 'marquee'"
|
||||
[style.--items-per-row]="config().itemsPerRow"
|
||||
[style.--max-height]="config().maxHeight ? config().maxHeight + 'px' : null">
|
||||
|
||||
@if (config().layout === 'marquee') {
|
||||
<div class="ui-lp-logo-cloud__marquee">
|
||||
<div class="ui-lp-logo-cloud__marquee-track">
|
||||
@for (logo of duplicatedLogos(); track logo.id + '-1') {
|
||||
<div class="ui-lp-logo-cloud__item">
|
||||
@if (logo.url) {
|
||||
<a
|
||||
[href]="logo.url"
|
||||
[attr.aria-label]="'Visit ' + logo.name"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
(click)="handleLogoClick(logo)">
|
||||
<img
|
||||
[src]="logo.logo"
|
||||
[alt]="logo.name"
|
||||
[class.ui-lp-logo-cloud__logo--grayscale]="logo.grayscale"
|
||||
loading="lazy">
|
||||
</a>
|
||||
} @else {
|
||||
<img
|
||||
[src]="logo.logo"
|
||||
[alt]="logo.name"
|
||||
[class.ui-lp-logo-cloud__logo--grayscale]="logo.grayscale"
|
||||
loading="lazy">
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
@for (logo of config().logos; track logo.id; let index = $index) {
|
||||
<div
|
||||
class="ui-lp-logo-cloud__item"
|
||||
[attr.data-animation-delay]="index * 50">
|
||||
@if (logo.url) {
|
||||
<a
|
||||
[href]="logo.url"
|
||||
[attr.aria-label]="'Visit ' + logo.name"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
(click)="handleLogoClick(logo)">
|
||||
<img
|
||||
[src]="logo.logo"
|
||||
[alt]="logo.name"
|
||||
[class.ui-lp-logo-cloud__logo--grayscale]="logo.grayscale"
|
||||
loading="lazy">
|
||||
</a>
|
||||
} @else {
|
||||
<img
|
||||
[src]="logo.logo"
|
||||
[alt]="logo.name"
|
||||
[class.ui-lp-logo-cloud__logo--grayscale]="logo.grayscale"
|
||||
loading="lazy">
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</ui-container>
|
||||
</section>
|
||||
`,
|
||||
styleUrl: './logo-cloud.component.scss'
|
||||
})
|
||||
export class LogoCloudComponent {
|
||||
config = signal<LogoCloudConfig>({
|
||||
logos: [],
|
||||
layout: 'row',
|
||||
itemsPerRow: 5,
|
||||
grayscale: true,
|
||||
hoverEffect: true,
|
||||
maxHeight: 80
|
||||
});
|
||||
|
||||
duplicatedLogos = computed(() => {
|
||||
const logos = this.config().logos;
|
||||
return [...logos, ...logos]; // Duplicate for seamless marquee
|
||||
});
|
||||
|
||||
@Input() set configuration(value: LogoCloudConfig) {
|
||||
this.config.set(value);
|
||||
}
|
||||
|
||||
@Output() logoClicked = new EventEmitter<LogoItem>();
|
||||
|
||||
getComponentClasses(): string {
|
||||
const classes = ['ui-lp-logo-cloud'];
|
||||
|
||||
if (this.config().layout) {
|
||||
classes.push(`ui-lp-logo-cloud--${this.config().layout}`);
|
||||
}
|
||||
if (this.config().grayscale) {
|
||||
classes.push('ui-lp-logo-cloud--grayscale');
|
||||
}
|
||||
if (this.config().hoverEffect) {
|
||||
classes.push('ui-lp-logo-cloud--hover');
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
handleLogoClick(logo: LogoItem): void {
|
||||
this.logoClicked.emit(logo);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-lp-statistics-display {
|
||||
padding: $semantic-spacing-layout-section-lg 0;
|
||||
|
||||
// Background Variants
|
||||
&--transparent {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&--surface {
|
||||
background: $semantic-color-surface-elevated;
|
||||
}
|
||||
|
||||
&--primary {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
|
||||
.ui-lp-statistics-display__title,
|
||||
.ui-lp-statistics-display__value,
|
||||
.ui-lp-statistics-display__label {
|
||||
color: $semantic-color-on-primary;
|
||||
}
|
||||
|
||||
.ui-lp-statistics-display__subtitle,
|
||||
.ui-lp-statistics-display__description {
|
||||
color: $semantic-color-on-primary;
|
||||
opacity: $semantic-opacity-subtle;
|
||||
}
|
||||
}
|
||||
|
||||
// Header Section
|
||||
&__header {
|
||||
text-align: center;
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
max-width: 800px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h2, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h2, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h2, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h2, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-family: map-get($semantic-typography-body-large, font-family);
|
||||
font-size: map-get($semantic-typography-body-large, font-size);
|
||||
font-weight: map-get($semantic-typography-body-large, font-weight);
|
||||
line-height: map-get($semantic-typography-body-large, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// Items Container
|
||||
&__items {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// Row Layout
|
||||
&__items--row {
|
||||
flex-wrap: wrap;
|
||||
gap: $semantic-spacing-component-xl;
|
||||
|
||||
@media (max-width: $semantic-breakpoint-lg - 1) {
|
||||
gap: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-lg;
|
||||
}
|
||||
}
|
||||
|
||||
// Grid Layout
|
||||
&__items--grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: $semantic-spacing-component-xl;
|
||||
width: 100%;
|
||||
|
||||
@media (max-width: $semantic-breakpoint-lg - 1) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
grid-template-columns: 1fr;
|
||||
gap: $semantic-spacing-component-lg;
|
||||
}
|
||||
}
|
||||
|
||||
// Individual Item
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: $semantic-spacing-component-md;
|
||||
text-align: left;
|
||||
|
||||
.ui-lp-statistics-display--row & {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
animation: fadeInUp $semantic-motion-duration-slow $semantic-motion-easing-ease forwards;
|
||||
animation-delay: var(--animation-delay, 0ms);
|
||||
}
|
||||
}
|
||||
|
||||
// Icon
|
||||
&__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: $semantic-sizing-icon-navigation;
|
||||
height: $semantic-sizing-icon-navigation;
|
||||
flex-shrink: 0;
|
||||
|
||||
fa-icon {
|
||||
font-size: $semantic-sizing-icon-navigation;
|
||||
color: $semantic-color-primary;
|
||||
}
|
||||
|
||||
.ui-lp-statistics-display--primary & fa-icon {
|
||||
color: $semantic-color-on-primary;
|
||||
}
|
||||
|
||||
.ui-lp-statistics-display--row & {
|
||||
margin-bottom: $semantic-spacing-component-sm;
|
||||
}
|
||||
}
|
||||
|
||||
// Content Area
|
||||
&__content {
|
||||
flex: 1;
|
||||
|
||||
.ui-lp-statistics-display--row & {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__value {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
margin-bottom: $semantic-spacing-component-xs;
|
||||
|
||||
.ui-lp-statistics-display--row & {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__number {
|
||||
font-family: map-get($semantic-typography-heading-h1, font-family);
|
||||
font-size: 3.5rem;
|
||||
font-weight: map-get($semantic-typography-heading-h1, font-weight);
|
||||
line-height: 1;
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__prefix,
|
||||
&__suffix {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
color: $semantic-color-text-primary;
|
||||
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-family: map-get($semantic-typography-heading-h4, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h4, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h4, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h4, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-xs;
|
||||
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
font-family: map-get($semantic-typography-heading-h5, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h5, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h5, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h5, line-height);
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-family: map-get($semantic-typography-body-small, font-family);
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
font-weight: map-get($semantic-typography-body-small, font-weight);
|
||||
line-height: map-get($semantic-typography-body-small, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// Variant Styles
|
||||
// Card Variant
|
||||
&--card &__item {
|
||||
padding: $semantic-spacing-component-xl;
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
box-shadow: $semantic-shadow-card-rest;
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $semantic-shadow-card-hover;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
&--card.ui-lp-statistics-display--primary &__item {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-color: $semantic-color-primary;
|
||||
|
||||
.ui-lp-statistics-display__title,
|
||||
.ui-lp-statistics-display__value,
|
||||
.ui-lp-statistics-display__label {
|
||||
color: $semantic-color-text-primary;
|
||||
}
|
||||
|
||||
.ui-lp-statistics-display__subtitle,
|
||||
.ui-lp-statistics-display__description {
|
||||
color: $semantic-color-text-secondary;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Highlighted Variant
|
||||
&--highlighted &__item {
|
||||
position: relative;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background: $semantic-color-primary;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive Adjustments
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
padding: $semantic-spacing-layout-section-md 0;
|
||||
|
||||
&__header {
|
||||
margin-bottom: $semantic-spacing-layout-section-sm;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h3, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h3, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h3, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h3, line-height);
|
||||
}
|
||||
|
||||
&__item {
|
||||
gap: $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
&--card &__item {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
&__items--row {
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&__items--grid {
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: $semantic-sizing-icon-button;
|
||||
height: $semantic-sizing-icon-button;
|
||||
|
||||
fa-icon {
|
||||
font-size: $semantic-sizing-icon-button;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Animation states
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ui-lp-statistics-display__item {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation, signal, computed, OnInit, OnDestroy, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ContainerComponent } from 'ui-essentials';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
|
||||
import { faArrowUp, faUsers, faDollarSign, faChartBar } from '@fortawesome/free-solid-svg-icons';
|
||||
import { StatisticsConfig, StatisticItem } from '../../interfaces/social-proof.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-lp-statistics-display',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ContainerComponent, FaIconComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<section
|
||||
class="ui-lp-statistics-display"
|
||||
[class]="getComponentClasses()"
|
||||
[attr.aria-label]="'Statistics section'"
|
||||
#sectionElement>
|
||||
|
||||
<ui-container [size]="'xl'" [padding]="'lg'">
|
||||
@if (config().title || config().subtitle) {
|
||||
<div class="ui-lp-statistics-display__header">
|
||||
@if (config().title) {
|
||||
<h2 class="ui-lp-statistics-display__title">{{ config().title }}</h2>
|
||||
}
|
||||
@if (config().subtitle) {
|
||||
<p class="ui-lp-statistics-display__subtitle">{{ config().subtitle }}</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div
|
||||
class="ui-lp-statistics-display__items"
|
||||
[class.ui-lp-statistics-display__items--row]="config().layout === 'row'"
|
||||
[class.ui-lp-statistics-display__items--grid]="config().layout === 'grid'">
|
||||
|
||||
@for (statistic of config().statistics; track statistic.id; let index = $index) {
|
||||
<div
|
||||
class="ui-lp-statistics-display__item"
|
||||
[attr.data-animation-delay]="index * 100">
|
||||
|
||||
@if (statistic.icon) {
|
||||
<div class="ui-lp-statistics-display__icon">
|
||||
<fa-icon [icon]="getIconDefinition(statistic.icon)"></fa-icon>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="ui-lp-statistics-display__content">
|
||||
<div class="ui-lp-statistics-display__value">
|
||||
@if (statistic.prefix) {
|
||||
<span class="ui-lp-statistics-display__prefix">{{ statistic.prefix }}</span>
|
||||
}
|
||||
|
||||
<span
|
||||
class="ui-lp-statistics-display__number"
|
||||
[attr.data-target]="statistic.animateValue ? getNumericValue(statistic.value) : null"
|
||||
[attr.data-animate]="statistic.animateValue">
|
||||
{{ displayValue(statistic) }}
|
||||
</span>
|
||||
|
||||
@if (statistic.suffix) {
|
||||
<span class="ui-lp-statistics-display__suffix">{{ statistic.suffix }}</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="ui-lp-statistics-display__label">{{ statistic.label }}</div>
|
||||
|
||||
@if (statistic.description) {
|
||||
<div class="ui-lp-statistics-display__description">{{ statistic.description }}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</ui-container>
|
||||
</section>
|
||||
`,
|
||||
styleUrl: './statistics-display.component.scss'
|
||||
})
|
||||
export class StatisticsDisplayComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
@ViewChild('sectionElement') sectionElement!: ElementRef<HTMLElement>;
|
||||
|
||||
config = signal<StatisticsConfig>({
|
||||
statistics: [],
|
||||
layout: 'row',
|
||||
variant: 'minimal',
|
||||
animateOnScroll: true,
|
||||
backgroundColor: 'transparent'
|
||||
});
|
||||
|
||||
private intersectionObserver?: IntersectionObserver;
|
||||
private animatedItems = new Set<string>();
|
||||
|
||||
@Input() set configuration(value: StatisticsConfig) {
|
||||
this.config.set(value);
|
||||
}
|
||||
|
||||
@Output() statisticClicked = new EventEmitter<StatisticItem>();
|
||||
|
||||
getComponentClasses(): string {
|
||||
const classes = ['ui-lp-statistics-display'];
|
||||
|
||||
if (this.config().layout) {
|
||||
classes.push(`ui-lp-statistics-display--${this.config().layout}`);
|
||||
}
|
||||
if (this.config().variant) {
|
||||
classes.push(`ui-lp-statistics-display--${this.config().variant}`);
|
||||
}
|
||||
if (this.config().backgroundColor) {
|
||||
classes.push(`ui-lp-statistics-display--${this.config().backgroundColor}`);
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.config().animateOnScroll && 'IntersectionObserver' in window) {
|
||||
this.setupIntersectionObserver();
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
if (!this.config().animateOnScroll) {
|
||||
this.animateAllCounters();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.intersectionObserver) {
|
||||
this.intersectionObserver.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
displayValue(statistic: StatisticItem): string {
|
||||
if (typeof statistic.value === 'number' && !statistic.animateValue) {
|
||||
return this.formatNumber(statistic.value);
|
||||
}
|
||||
return statistic.value.toString();
|
||||
}
|
||||
|
||||
getNumericValue(value: number | string): number {
|
||||
return typeof value === 'number' ? value : parseInt(value.toString(), 10) || 0;
|
||||
}
|
||||
|
||||
getIconDefinition(iconName: string): IconDefinition {
|
||||
const iconMap: { [key: string]: IconDefinition } = {
|
||||
'arrow-up': faArrowUp,
|
||||
'users': faUsers,
|
||||
'dollar-sign': faDollarSign,
|
||||
'chart-bar': faChartBar
|
||||
};
|
||||
return iconMap[iconName] || faChartBar;
|
||||
}
|
||||
|
||||
private setupIntersectionObserver(): void {
|
||||
this.intersectionObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
this.animateCountersInView();
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.5 }
|
||||
);
|
||||
}
|
||||
|
||||
private animateCountersInView(): void {
|
||||
if (!this.sectionElement) return;
|
||||
|
||||
const numberElements = this.sectionElement.nativeElement.querySelectorAll(
|
||||
'[data-animate="true"]:not(.animated)'
|
||||
);
|
||||
|
||||
numberElements.forEach((element) => {
|
||||
const target = parseInt(element.getAttribute('data-target') || '0', 10);
|
||||
const statId = element.closest('.ui-lp-statistics-display__item')?.getAttribute('data-id');
|
||||
|
||||
if (statId && !this.animatedItems.has(statId)) {
|
||||
this.animatedItems.add(statId);
|
||||
element.classList.add('animated');
|
||||
this.animateCounter(element as HTMLElement, target);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private animateAllCounters(): void {
|
||||
if (!this.sectionElement) return;
|
||||
|
||||
const numberElements = this.sectionElement.nativeElement.querySelectorAll('[data-animate="true"]');
|
||||
|
||||
numberElements.forEach((element) => {
|
||||
const target = parseInt(element.getAttribute('data-target') || '0', 10);
|
||||
this.animateCounter(element as HTMLElement, target);
|
||||
});
|
||||
}
|
||||
|
||||
private animateCounter(element: HTMLElement, target: number): void {
|
||||
const duration = 2000; // 2 seconds
|
||||
const steps = 60;
|
||||
const stepValue = target / steps;
|
||||
const stepDuration = duration / steps;
|
||||
|
||||
let current = 0;
|
||||
let step = 0;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
current = Math.min(target, Math.floor(stepValue * step));
|
||||
element.textContent = this.formatNumber(current);
|
||||
|
||||
step++;
|
||||
|
||||
if (step > steps || current >= target) {
|
||||
clearInterval(timer);
|
||||
element.textContent = this.formatNumber(target);
|
||||
}
|
||||
}, stepDuration);
|
||||
}
|
||||
|
||||
private formatNumber(num: number): string {
|
||||
if (num >= 1000000) {
|
||||
return (num / 1000000).toFixed(1).replace(/\.0$/, '') + 'M';
|
||||
}
|
||||
if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'K';
|
||||
}
|
||||
return num.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-lp-testimonial-carousel {
|
||||
padding: $semantic-spacing-layout-section-lg 0;
|
||||
background: $semantic-color-surface-primary;
|
||||
|
||||
// Header Section
|
||||
&__header {
|
||||
text-align: center;
|
||||
margin-bottom: $semantic-spacing-layout-section-md;
|
||||
max-width: 800px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-family: map-get($semantic-typography-heading-h2, font-family);
|
||||
font-size: map-get($semantic-typography-heading-h2, font-size);
|
||||
font-weight: map-get($semantic-typography-heading-h2, font-weight);
|
||||
line-height: map-get($semantic-typography-heading-h2, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-content-heading;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-family: map-get($semantic-typography-body-large, font-family);
|
||||
font-size: map-get($semantic-typography-body-large, font-size);
|
||||
font-weight: map-get($semantic-typography-body-large, font-weight);
|
||||
line-height: map-get($semantic-typography-body-large, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// Carousel Container
|
||||
&__container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
&__viewport {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
}
|
||||
|
||||
&__track {
|
||||
display: flex;
|
||||
transition: transform $semantic-motion-duration-normal $semantic-motion-easing-ease;
|
||||
}
|
||||
|
||||
&__slide {
|
||||
flex-shrink: 0;
|
||||
padding: 0 $semantic-spacing-component-sm;
|
||||
}
|
||||
|
||||
// Navigation Buttons
|
||||
&__nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: none;
|
||||
border-radius: $semantic-border-button-radius;
|
||||
background: $semantic-color-surface-elevated;
|
||||
color: $semantic-color-text-primary;
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
box-shadow: $semantic-shadow-card-rest;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: $semantic-color-surface-hover;
|
||||
box-shadow: $semantic-shadow-card-hover;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: $semantic-opacity-disabled;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-primary;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
fa-icon {
|
||||
font-size: $semantic-sizing-icon-button;
|
||||
}
|
||||
}
|
||||
|
||||
// Testimonial Item
|
||||
&__item {
|
||||
padding: $semantic-spacing-component-xl;
|
||||
background: $semantic-color-surface-elevated;
|
||||
border-radius: $semantic-border-card-radius;
|
||||
box-shadow: $semantic-shadow-card-rest;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-lg;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// Card Variant (default)
|
||||
&--card &__item {
|
||||
border: $semantic-border-width-1 solid $semantic-color-border-subtle;
|
||||
}
|
||||
|
||||
// Minimal Variant
|
||||
&--minimal &__item {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
padding: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
// Quote Variant
|
||||
&--quote &__item {
|
||||
background: $semantic-color-primary;
|
||||
color: $semantic-color-on-primary;
|
||||
|
||||
.ui-lp-testimonial-carousel__text {
|
||||
color: $semantic-color-on-primary;
|
||||
}
|
||||
|
||||
.ui-lp-testimonial-carousel__author-name {
|
||||
color: $semantic-color-on-primary;
|
||||
}
|
||||
|
||||
.ui-lp-testimonial-carousel__author-title {
|
||||
color: $semantic-color-on-primary;
|
||||
opacity: $semantic-opacity-subtle;
|
||||
}
|
||||
}
|
||||
|
||||
&__quote-icon {
|
||||
position: absolute;
|
||||
top: $semantic-spacing-component-md;
|
||||
left: $semantic-spacing-component-md;
|
||||
|
||||
fa-icon {
|
||||
font-size: 2rem;
|
||||
color: $semantic-color-on-primary;
|
||||
opacity: $semantic-opacity-subtle;
|
||||
}
|
||||
}
|
||||
|
||||
// Content
|
||||
&__content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-family: map-get($semantic-typography-body-large, font-family);
|
||||
font-size: map-get($semantic-typography-body-large, font-size);
|
||||
font-weight: map-get($semantic-typography-body-large, font-weight);
|
||||
line-height: map-get($semantic-typography-body-large, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin: 0;
|
||||
|
||||
.ui-lp-testimonial-carousel--quote & {
|
||||
font-style: italic;
|
||||
padding-top: $semantic-spacing-component-lg;
|
||||
}
|
||||
}
|
||||
|
||||
// Rating
|
||||
&__rating {
|
||||
display: flex;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
&__star {
|
||||
font-size: $semantic-sizing-icon-button;
|
||||
color: $semantic-color-warning;
|
||||
|
||||
.ui-lp-testimonial-carousel--quote & {
|
||||
color: $semantic-color-on-primary;
|
||||
}
|
||||
}
|
||||
|
||||
// Author
|
||||
&__author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: $semantic-border-avatar-radius;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
&__author-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__author-name {
|
||||
font-family: map-get($semantic-typography-button-medium, font-family);
|
||||
font-size: map-get($semantic-typography-button-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-button-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-button-medium, line-height);
|
||||
color: $semantic-color-text-primary;
|
||||
margin-bottom: $semantic-spacing-component-xs;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $semantic-spacing-component-xs;
|
||||
}
|
||||
|
||||
&__verified {
|
||||
font-size: $semantic-sizing-icon-button;
|
||||
color: $semantic-color-success;
|
||||
|
||||
.ui-lp-testimonial-carousel--quote & {
|
||||
color: $semantic-color-on-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&__author-title {
|
||||
font-family: map-get($semantic-typography-body-small, font-family);
|
||||
font-size: map-get($semantic-typography-body-small, font-size);
|
||||
font-weight: map-get($semantic-typography-body-small, font-weight);
|
||||
line-height: map-get($semantic-typography-body-small, line-height);
|
||||
color: $semantic-color-text-secondary;
|
||||
}
|
||||
|
||||
// Dots Navigation
|
||||
&__dots {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: $semantic-spacing-component-sm;
|
||||
margin-top: $semantic-spacing-component-xl;
|
||||
}
|
||||
|
||||
&__dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: $semantic-color-border-subtle;
|
||||
cursor: pointer;
|
||||
transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
|
||||
|
||||
&--active {
|
||||
background: $semantic-color-primary;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $semantic-color-primary;
|
||||
opacity: $semantic-opacity-subtle;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $semantic-color-primary;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive Design
|
||||
@media (max-width: $semantic-breakpoint-lg - 1) {
|
||||
&__container {
|
||||
gap: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&__nav {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
fa-icon {
|
||||
font-size: $semantic-sizing-icon-inline;
|
||||
}
|
||||
}
|
||||
|
||||
&__item {
|
||||
padding: $semantic-spacing-component-lg;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
&__container {
|
||||
flex-direction: column;
|
||||
gap: $semantic-spacing-component-lg;
|
||||
}
|
||||
|
||||
&__nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__viewport {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__slide {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&__item {
|
||||
padding: $semantic-spacing-component-md;
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-family: map-get($semantic-typography-body-medium, font-family);
|
||||
font-size: map-get($semantic-typography-body-medium, font-size);
|
||||
font-weight: map-get($semantic-typography-body-medium, font-weight);
|
||||
line-height: map-get($semantic-typography-body-medium, line-height);
|
||||
}
|
||||
}
|
||||
|
||||
// Animation states for scroll-based reveal
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
&__item {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
animation: fadeInUp $semantic-motion-duration-slow $semantic-motion-easing-ease forwards;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
&__track {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
&__item {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation, signal, computed, OnInit, OnDestroy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ContainerComponent } from 'ui-essentials';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
|
||||
import { faQuoteLeft, faStar, faChevronLeft, faChevronRight, faCheck } from '@fortawesome/free-solid-svg-icons';
|
||||
import { TestimonialCarouselConfig, TestimonialItem } from '../../interfaces/social-proof.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-lp-testimonial-carousel',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ContainerComponent, FaIconComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<section
|
||||
class="ui-lp-testimonial-carousel"
|
||||
[class]="getTestimonialCarouselClasses()"
|
||||
[attr.aria-label]="'Testimonials section'">
|
||||
|
||||
<ui-container [size]="'xl'" [padding]="'lg'">
|
||||
@if (config().title || config().subtitle) {
|
||||
<div class="ui-lp-testimonial-carousel__header">
|
||||
@if (config().title) {
|
||||
<h2 class="ui-lp-testimonial-carousel__title">{{ config().title }}</h2>
|
||||
}
|
||||
@if (config().subtitle) {
|
||||
<p class="ui-lp-testimonial-carousel__subtitle">{{ config().subtitle }}</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="ui-lp-testimonial-carousel__container">
|
||||
@if (config().showNavigation && config().testimonials.length > config().itemsPerView!) {
|
||||
<button
|
||||
class="ui-lp-testimonial-carousel__nav ui-lp-testimonial-carousel__nav--prev"
|
||||
(click)="previousSlide()"
|
||||
[disabled]="currentSlide() === 0"
|
||||
[attr.aria-label]="'Previous testimonial'">
|
||||
<fa-icon [icon]="faChevronLeft"></fa-icon>
|
||||
</button>
|
||||
}
|
||||
|
||||
<div class="ui-lp-testimonial-carousel__viewport">
|
||||
<div
|
||||
class="ui-lp-testimonial-carousel__track"
|
||||
[style.transform]="'translateX(' + translateX() + '%)'">
|
||||
|
||||
@for (testimonial of config().testimonials; track testimonial.id; let index = $index) {
|
||||
<div
|
||||
class="ui-lp-testimonial-carousel__slide"
|
||||
[style.width]="slideWidth() + '%'">
|
||||
|
||||
<div class="ui-lp-testimonial-carousel__item">
|
||||
@if (config().variant === 'quote') {
|
||||
<div class="ui-lp-testimonial-carousel__quote-icon">
|
||||
<fa-icon [icon]="faQuoteLeft"></fa-icon>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="ui-lp-testimonial-carousel__content">
|
||||
<p class="ui-lp-testimonial-carousel__text">{{ testimonial.content }}</p>
|
||||
|
||||
@if (config().showRatings && testimonial.rating) {
|
||||
<div class="ui-lp-testimonial-carousel__rating">
|
||||
@for (star of getRatingArray(testimonial.rating); track $index) {
|
||||
<fa-icon [icon]="faStar" class="ui-lp-testimonial-carousel__star"></fa-icon>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="ui-lp-testimonial-carousel__author">
|
||||
@if (testimonial.avatar) {
|
||||
<div class="ui-lp-testimonial-carousel__avatar">
|
||||
<img
|
||||
[src]="testimonial.avatar"
|
||||
[alt]="testimonial.name"
|
||||
loading="lazy">
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="ui-lp-testimonial-carousel__author-info">
|
||||
<div class="ui-lp-testimonial-carousel__author-name">
|
||||
{{ testimonial.name }}
|
||||
@if (testimonial.verified) {
|
||||
<fa-icon [icon]="faCheck" class="ui-lp-testimonial-carousel__verified"></fa-icon>
|
||||
}
|
||||
</div>
|
||||
@if (testimonial.title || testimonial.company) {
|
||||
<div class="ui-lp-testimonial-carousel__author-title">
|
||||
@if (testimonial.title) {
|
||||
<span>{{ testimonial.title }}</span>
|
||||
}
|
||||
@if (testimonial.title && testimonial.company) {
|
||||
<span> at </span>
|
||||
}
|
||||
@if (testimonial.company) {
|
||||
<span>{{ testimonial.company }}</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (config().showNavigation && config().testimonials.length > config().itemsPerView!) {
|
||||
<button
|
||||
class="ui-lp-testimonial-carousel__nav ui-lp-testimonial-carousel__nav--next"
|
||||
(click)="nextSlide()"
|
||||
[disabled]="currentSlide() >= maxSlide()"
|
||||
[attr.aria-label]="'Next testimonial'">
|
||||
<fa-icon [icon]="faChevronRight"></fa-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (config().showDots && config().testimonials.length > config().itemsPerView!) {
|
||||
<div class="ui-lp-testimonial-carousel__dots">
|
||||
@for (dot of dotsArray(); track $index; let index = $index) {
|
||||
<button
|
||||
class="ui-lp-testimonial-carousel__dot"
|
||||
[class.ui-lp-testimonial-carousel__dot--active]="currentSlide() === index"
|
||||
(click)="goToSlide(index)"
|
||||
[attr.aria-label]="'Go to testimonial ' + (index + 1)">
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</ui-container>
|
||||
</section>
|
||||
`,
|
||||
styleUrl: './testimonial-carousel.component.scss'
|
||||
})
|
||||
export class TestimonialCarouselComponent implements OnInit, OnDestroy {
|
||||
config = signal<TestimonialCarouselConfig>({
|
||||
testimonials: [],
|
||||
autoPlay: false,
|
||||
autoPlayDelay: 5000,
|
||||
showDots: true,
|
||||
showNavigation: true,
|
||||
itemsPerView: 1,
|
||||
variant: 'card',
|
||||
showRatings: true
|
||||
});
|
||||
|
||||
currentSlide = signal(0);
|
||||
private autoPlayInterval?: number;
|
||||
|
||||
faQuoteLeft = faQuoteLeft;
|
||||
faStar = faStar;
|
||||
faChevronLeft = faChevronLeft;
|
||||
faChevronRight = faChevronRight;
|
||||
faCheck = faCheck;
|
||||
|
||||
slideWidth = computed(() => 100 / this.config().itemsPerView!);
|
||||
translateX = computed(() => -this.currentSlide() * this.slideWidth());
|
||||
maxSlide = computed(() => Math.max(0, this.config().testimonials.length - this.config().itemsPerView!));
|
||||
dotsArray = computed(() => Array(this.maxSlide() + 1).fill(0));
|
||||
|
||||
@Input() set configuration(value: TestimonialCarouselConfig) {
|
||||
this.config.set(value);
|
||||
}
|
||||
|
||||
@Output() testimonialClicked = new EventEmitter<TestimonialItem>();
|
||||
|
||||
getTestimonialCarouselClasses(): string {
|
||||
const classes = [
|
||||
'ui-lp-testimonial-carousel',
|
||||
`ui-lp-testimonial-carousel--${this.config().variant}`,
|
||||
];
|
||||
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.config().autoPlay) {
|
||||
this.startAutoPlay();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.stopAutoPlay();
|
||||
}
|
||||
|
||||
nextSlide(): void {
|
||||
const nextIndex = this.currentSlide() + 1;
|
||||
if (nextIndex <= this.maxSlide()) {
|
||||
this.currentSlide.set(nextIndex);
|
||||
} else if (this.config().autoPlay) {
|
||||
this.currentSlide.set(0);
|
||||
}
|
||||
}
|
||||
|
||||
previousSlide(): void {
|
||||
const prevIndex = this.currentSlide() - 1;
|
||||
if (prevIndex >= 0) {
|
||||
this.currentSlide.set(prevIndex);
|
||||
}
|
||||
}
|
||||
|
||||
goToSlide(index: number): void {
|
||||
this.currentSlide.set(index);
|
||||
}
|
||||
|
||||
getRatingArray(rating: number): number[] {
|
||||
return Array(Math.floor(rating)).fill(0);
|
||||
}
|
||||
|
||||
private startAutoPlay(): void {
|
||||
this.autoPlayInterval = window.setInterval(() => {
|
||||
this.nextSlide();
|
||||
}, this.config().autoPlayDelay);
|
||||
}
|
||||
|
||||
private stopAutoPlay(): void {
|
||||
if (this.autoPlayInterval) {
|
||||
clearInterval(this.autoPlayInterval);
|
||||
this.autoPlayInterval = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-lp-agency-template {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__main {
|
||||
flex: 1;
|
||||
|
||||
// Agency-focused styling with professional appearance
|
||||
> *:nth-child(odd) {
|
||||
background: $semantic-color-surface-primary;
|
||||
}
|
||||
|
||||
> *:nth-child(even) {
|
||||
background: $semantic-color-surface-secondary;
|
||||
}
|
||||
|
||||
// Special treatment for hero section
|
||||
> ui-lp-hero-split {
|
||||
background: unset;
|
||||
}
|
||||
|
||||
// Team section gets special emphasis
|
||||
> ui-lp-team-grid {
|
||||
background: $semantic-color-surface-elevated;
|
||||
border: 1px solid $semantic-color-border-subtle;
|
||||
}
|
||||
|
||||
// Timeline section styling
|
||||
> ui-lp-timeline {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
$semantic-color-border-subtle,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced section spacing for agency layout
|
||||
section {
|
||||
padding: $semantic-spacing-layout-section-xl 0;
|
||||
}
|
||||
|
||||
// Professional section dividers
|
||||
section + section {
|
||||
border-top: 1px solid $semantic-color-border-subtle;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 100px;
|
||||
height: 1px;
|
||||
background: $semantic-color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments for agency layout
|
||||
@media (max-width: $semantic-breakpoint-lg - 1) {
|
||||
section {
|
||||
padding: $semantic-spacing-layout-section-lg 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
&__main {
|
||||
> ui-lp-team-grid {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
padding: $semantic-spacing-layout-section-md 0;
|
||||
}
|
||||
|
||||
section + section::before {
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
&__main {
|
||||
> * {
|
||||
background: $semantic-color-surface-primary !important;
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
padding: $semantic-spacing-layout-section-sm 0;
|
||||
}
|
||||
|
||||
section + section {
|
||||
border-top: none;
|
||||
margin-top: $semantic-spacing-layout-section-sm;
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { Component, Input, ChangeDetectionStrategy, ViewEncapsulation, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { HeroSplitScreenComponent } from '../heroes/hero-split-screen.component';
|
||||
import { FeatureGridComponent } from '../features/feature-grid.component';
|
||||
import { TeamGridComponent } from '../content/team-grid.component';
|
||||
import { TimelineSectionComponent } from '../content/timeline-section.component';
|
||||
import { TestimonialCarouselComponent } from '../social-proof/testimonial-carousel.component';
|
||||
import { CTASectionComponent } from '../conversion/cta-section.component';
|
||||
import { LandingHeaderComponent } from '../navigation/landing-header.component';
|
||||
import { FooterSectionComponent } from '../navigation/footer-section.component';
|
||||
import { AgencyTemplateConfig } from '../../interfaces/templates.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-lp-agency-template',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
HeroSplitScreenComponent,
|
||||
FeatureGridComponent,
|
||||
TeamGridComponent,
|
||||
TimelineSectionComponent,
|
||||
TestimonialCarouselComponent,
|
||||
CTASectionComponent,
|
||||
LandingHeaderComponent,
|
||||
FooterSectionComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<div class="ui-lp-agency-template">
|
||||
@if (config().header) {
|
||||
<ui-lp-header [configuration]="config().header!"></ui-lp-header>
|
||||
}
|
||||
|
||||
<main class="ui-lp-agency-template__main">
|
||||
<!-- Hero Section -->
|
||||
<ui-lp-hero-split [configuration]="config().hero"></ui-lp-hero-split>
|
||||
|
||||
<!-- Services Section -->
|
||||
<ui-lp-feature-grid [configuration]="config().services"></ui-lp-feature-grid>
|
||||
|
||||
<!-- Company Timeline -->
|
||||
<ui-lp-timeline [configuration]="config().timeline"></ui-lp-timeline>
|
||||
|
||||
<!-- Team Section -->
|
||||
<ui-lp-team-grid [configuration]="config().team"></ui-lp-team-grid>
|
||||
|
||||
<!-- Client Testimonials -->
|
||||
<ui-lp-testimonial-carousel [configuration]="config().testimonials"></ui-lp-testimonial-carousel>
|
||||
|
||||
<!-- Contact CTA Section -->
|
||||
<ui-cta-section [config]="config().cta"></ui-cta-section>
|
||||
</main>
|
||||
|
||||
@if (config().footer) {
|
||||
<ui-lp-footer [configuration]="config().footer!"></ui-lp-footer>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './agency-template.component.scss'
|
||||
})
|
||||
export class AgencyTemplateComponent {
|
||||
config = signal<AgencyTemplateConfig>({
|
||||
hero: {
|
||||
title: 'Your Vision, Our Expertise',
|
||||
subtitle: 'We create exceptional digital experiences that drive results',
|
||||
alignment: 'left',
|
||||
backgroundType: 'gradient',
|
||||
minHeight: 'large'
|
||||
},
|
||||
services: {
|
||||
title: 'Our Services',
|
||||
subtitle: 'Comprehensive solutions for your digital needs',
|
||||
features: []
|
||||
},
|
||||
team: {
|
||||
title: 'Meet Our Team',
|
||||
subtitle: 'The talented individuals behind our success',
|
||||
members: [],
|
||||
columns: 3,
|
||||
showSocial: true,
|
||||
showBio: true
|
||||
},
|
||||
timeline: {
|
||||
title: 'Our Journey',
|
||||
subtitle: 'Milestones that define our growth',
|
||||
items: [],
|
||||
orientation: 'vertical',
|
||||
showDates: true
|
||||
},
|
||||
testimonials: {
|
||||
title: 'Client Success Stories',
|
||||
subtitle: 'What our partners say about working with us',
|
||||
testimonials: []
|
||||
},
|
||||
cta: {
|
||||
title: 'Ready to Start Your Project?',
|
||||
description: 'Let\'s discuss how we can bring your vision to life',
|
||||
ctaPrimary: { text: 'Get Started', variant: 'filled', action: () => {} },
|
||||
backgroundType: 'gradient'
|
||||
}
|
||||
});
|
||||
|
||||
@Input() set configuration(value: AgencyTemplateConfig) {
|
||||
this.config.set(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './saas-template.component';
|
||||
export * from './product-template.component';
|
||||
export * from './agency-template.component';
|
||||
@@ -0,0 +1,54 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-lp-product-template {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__main {
|
||||
flex: 1;
|
||||
|
||||
// Product-focused styling with cleaner backgrounds
|
||||
> *:nth-child(odd) {
|
||||
background: $semantic-color-surface-primary;
|
||||
}
|
||||
|
||||
> *:nth-child(even) {
|
||||
background: $semantic-color-surface-elevated;
|
||||
}
|
||||
|
||||
// Hero maintains its special background (image/gradient)
|
||||
> ui-lp-hero-image {
|
||||
background: unset;
|
||||
}
|
||||
}
|
||||
|
||||
// Product template specific spacing
|
||||
section {
|
||||
padding: $semantic-spacing-layout-section-lg 0;
|
||||
}
|
||||
|
||||
// Enhanced visual separation between sections
|
||||
section + section {
|
||||
border-top: 1px solid $semantic-color-border-subtle;
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: $semantic-breakpoint-md - 1) {
|
||||
section {
|
||||
padding: $semantic-spacing-layout-section-md 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
&__main {
|
||||
> * {
|
||||
background: $semantic-color-surface-primary !important;
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
padding: $semantic-spacing-layout-section-sm 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Component, Input, ChangeDetectionStrategy, ViewEncapsulation, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { HeroWithImageComponent } from '../heroes/hero-with-image.component';
|
||||
import { FeatureGridComponent } from '../features/feature-grid.component';
|
||||
import { TestimonialCarouselComponent } from '../social-proof/testimonial-carousel.component';
|
||||
import { PricingTableComponent } from '../conversion/pricing-table.component';
|
||||
import { CTASectionComponent } from '../conversion/cta-section.component';
|
||||
import { LandingHeaderComponent } from '../navigation/landing-header.component';
|
||||
import { FooterSectionComponent } from '../navigation/footer-section.component';
|
||||
import { ProductTemplateConfig } from '../../interfaces/templates.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-lp-product-template',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
HeroWithImageComponent,
|
||||
FeatureGridComponent,
|
||||
TestimonialCarouselComponent,
|
||||
PricingTableComponent,
|
||||
CTASectionComponent,
|
||||
LandingHeaderComponent,
|
||||
FooterSectionComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<div class="ui-lp-product-template">
|
||||
@if (config().header) {
|
||||
<ui-lp-header [configuration]="config().header!"></ui-lp-header>
|
||||
}
|
||||
|
||||
<main class="ui-lp-product-template__main">
|
||||
<!-- Hero Section with Product Image -->
|
||||
<ui-lp-hero-image [configuration]="config().hero"></ui-lp-hero-image>
|
||||
|
||||
<!-- Features Section -->
|
||||
<ui-lp-feature-grid [configuration]="config().features"></ui-lp-feature-grid>
|
||||
|
||||
<!-- Customer Testimonials -->
|
||||
<ui-lp-testimonial-carousel [configuration]="config().testimonials"></ui-lp-testimonial-carousel>
|
||||
|
||||
<!-- Pricing Section -->
|
||||
<ui-pricing-table [config]="config().pricing"></ui-pricing-table>
|
||||
|
||||
<!-- Final CTA Section -->
|
||||
<ui-cta-section [config]="config().cta"></ui-cta-section>
|
||||
</main>
|
||||
|
||||
@if (config().footer) {
|
||||
<ui-lp-footer [configuration]="config().footer!"></ui-lp-footer>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './product-template.component.scss'
|
||||
})
|
||||
export class ProductTemplateComponent {
|
||||
config = signal<ProductTemplateConfig>({
|
||||
hero: {
|
||||
title: 'Introducing Our Latest Product',
|
||||
subtitle: 'Experience the future of innovation',
|
||||
alignment: 'left',
|
||||
backgroundType: 'image',
|
||||
minHeight: 'large'
|
||||
},
|
||||
features: {
|
||||
title: 'Key Features',
|
||||
features: []
|
||||
},
|
||||
testimonials: {
|
||||
title: 'Customer Reviews',
|
||||
testimonials: []
|
||||
},
|
||||
pricing: {
|
||||
plans: [],
|
||||
billingToggle: { monthlyLabel: 'Monthly', yearlyLabel: 'Yearly' },
|
||||
featuresComparison: false
|
||||
},
|
||||
cta: {
|
||||
title: 'Start Your Journey',
|
||||
description: 'Order now and get free shipping worldwide',
|
||||
ctaPrimary: { text: 'Order Now', variant: 'filled', action: () => {} },
|
||||
backgroundType: 'gradient'
|
||||
}
|
||||
});
|
||||
|
||||
@Input() set configuration(value: ProductTemplateConfig) {
|
||||
this.config.set(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
|
||||
|
||||
.ui-lp-saas-template {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__main {
|
||||
flex: 1;
|
||||
|
||||
// Alternating background colors for sections
|
||||
> *:nth-child(odd) {
|
||||
background: $semantic-color-surface-primary;
|
||||
}
|
||||
|
||||
> *:nth-child(even) {
|
||||
background: $semantic-color-surface-secondary;
|
||||
}
|
||||
|
||||
// Override for hero to maintain gradient/special background
|
||||
> ui-lp-hero {
|
||||
background: unset;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure proper spacing between sections
|
||||
section + section {
|
||||
border-top: 1px solid $semantic-color-border-subtle;
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: $semantic-breakpoint-sm - 1) {
|
||||
&__main {
|
||||
// Reduce alternating backgrounds on mobile for better readability
|
||||
> * {
|
||||
background: $semantic-color-surface-primary !important;
|
||||
}
|
||||
|
||||
section + section {
|
||||
border-top: none;
|
||||
margin-top: $semantic-spacing-layout-section-sm;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { Component, Input, ChangeDetectionStrategy, ViewEncapsulation, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { HeroSectionComponent } from '../heroes/hero-section.component';
|
||||
import { FeatureGridComponent } from '../features/feature-grid.component';
|
||||
import { StatisticsDisplayComponent } from '../social-proof/statistics-display.component';
|
||||
import { TestimonialCarouselComponent } from '../social-proof/testimonial-carousel.component';
|
||||
import { PricingTableComponent } from '../conversion/pricing-table.component';
|
||||
import { CTASectionComponent } from '../conversion/cta-section.component';
|
||||
import { FAQSectionComponent } from '../content/faq-section.component';
|
||||
import { LandingHeaderComponent } from '../navigation/landing-header.component';
|
||||
import { FooterSectionComponent } from '../navigation/footer-section.component';
|
||||
import { SaaSTemplateConfig } from '../../interfaces/templates.interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-lp-saas-template',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
HeroSectionComponent,
|
||||
FeatureGridComponent,
|
||||
StatisticsDisplayComponent,
|
||||
TestimonialCarouselComponent,
|
||||
PricingTableComponent,
|
||||
CTASectionComponent,
|
||||
FAQSectionComponent,
|
||||
LandingHeaderComponent,
|
||||
FooterSectionComponent
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<div class="ui-lp-saas-template">
|
||||
@if (config().header) {
|
||||
<ui-lp-header [configuration]="config().header!"></ui-lp-header>
|
||||
}
|
||||
|
||||
<main class="ui-lp-saas-template__main">
|
||||
<!-- Hero Section -->
|
||||
<ui-lp-hero [configuration]="config().hero"></ui-lp-hero>
|
||||
|
||||
<!-- Statistics Section -->
|
||||
<ui-lp-statistics-display [configuration]="config().socialProof"></ui-lp-statistics-display>
|
||||
|
||||
<!-- Features Section -->
|
||||
<ui-lp-feature-grid [configuration]="config().features"></ui-lp-feature-grid>
|
||||
|
||||
<!-- Testimonials Section -->
|
||||
<ui-lp-testimonial-carousel [configuration]="config().testimonials"></ui-lp-testimonial-carousel>
|
||||
|
||||
<!-- Pricing Section -->
|
||||
<ui-pricing-table [config]="config().pricing"></ui-pricing-table>
|
||||
|
||||
<!-- FAQ Section -->
|
||||
<ui-lp-faq [configuration]="config().faq"></ui-lp-faq>
|
||||
|
||||
<!-- Final CTA Section -->
|
||||
<ui-cta-section [config]="config().cta"></ui-cta-section>
|
||||
</main>
|
||||
|
||||
@if (config().footer) {
|
||||
<ui-lp-footer [configuration]="config().footer!"></ui-lp-footer>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
styleUrl: './saas-template.component.scss'
|
||||
})
|
||||
export class SaaSTemplateComponent {
|
||||
config = signal<SaaSTemplateConfig>({
|
||||
hero: {
|
||||
title: 'Build Amazing SaaS Applications',
|
||||
subtitle: 'The complete toolkit for modern software development',
|
||||
alignment: 'center',
|
||||
backgroundType: 'gradient',
|
||||
minHeight: 'full'
|
||||
},
|
||||
features: {
|
||||
title: 'Why Choose Our Platform',
|
||||
features: []
|
||||
},
|
||||
socialProof: {
|
||||
title: 'Trusted by Industry Leaders',
|
||||
statistics: []
|
||||
},
|
||||
testimonials: {
|
||||
title: 'What Our Customers Say',
|
||||
testimonials: []
|
||||
},
|
||||
pricing: {
|
||||
plans: [],
|
||||
billingToggle: { monthlyLabel: 'Monthly', yearlyLabel: 'Yearly' },
|
||||
featuresComparison: false
|
||||
},
|
||||
faq: {
|
||||
title: 'Frequently Asked Questions',
|
||||
items: []
|
||||
},
|
||||
cta: {
|
||||
title: 'Ready to Get Started?',
|
||||
description: 'Join thousands of developers building the future',
|
||||
ctaPrimary: { text: 'Get Started', variant: 'filled', action: () => {} },
|
||||
backgroundType: 'gradient'
|
||||
}
|
||||
});
|
||||
|
||||
@Input() set configuration(value: SaaSTemplateConfig) {
|
||||
this.config.set(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
export interface FAQItem {
|
||||
id: string;
|
||||
question: string;
|
||||
answer: string;
|
||||
isOpen?: boolean;
|
||||
}
|
||||
|
||||
export interface FAQConfig {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
items: FAQItem[];
|
||||
searchEnabled?: boolean;
|
||||
expandMultiple?: boolean;
|
||||
theme?: 'default' | 'bordered' | 'minimal';
|
||||
}
|
||||
|
||||
export interface TeamMember {
|
||||
id: string;
|
||||
name: string;
|
||||
role: string;
|
||||
bio?: string;
|
||||
image?: string;
|
||||
social?: TeamSocialLinks;
|
||||
}
|
||||
|
||||
export interface TeamSocialLinks {
|
||||
linkedin?: string;
|
||||
twitter?: string;
|
||||
github?: string;
|
||||
email?: string;
|
||||
website?: string;
|
||||
}
|
||||
|
||||
export interface TeamConfig {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
members: TeamMember[];
|
||||
columns?: 2 | 3 | 4;
|
||||
showSocial?: boolean;
|
||||
showBio?: boolean;
|
||||
}
|
||||
|
||||
export interface TimelineItem {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
date?: string;
|
||||
status?: 'completed' | 'current' | 'upcoming';
|
||||
icon?: string;
|
||||
badge?: string;
|
||||
}
|
||||
|
||||
export interface TimelineConfig {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
items: TimelineItem[];
|
||||
orientation?: 'vertical' | 'horizontal';
|
||||
theme?: 'default' | 'minimal' | 'connected';
|
||||
showDates?: boolean;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user