diff --git a/UI_LANDING_PAGES_PLAN.md b/UI_LANDING_PAGES_PLAN.md
new file mode 100644
index 0000000..6ccf4f3
--- /dev/null
+++ b/UI_LANDING_PAGES_PLAN.md
@@ -0,0 +1,942 @@
+# 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, integrating seamlessly with existing libraries: `ui-design-system`, `ui-essentials`, `ui-animations`, and `shared-utils`.
+
+## 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
+│ │ └── styles/ # Component-specific styles
+│ ├── public-api.ts
+│ └── test.ts
+├── ng-package.json
+├── package.json
+├── tsconfig.lib.json
+└── README.md
+```
+
+### Design System Integration
+
+#### Typography (Google Fonts)
+- **Primary Font**: Inter (headings, UI elements)
+- **Secondary Font**: Source Sans Pro (body text)
+- **Accent Font**: Roboto Mono (code, technical content)
+
+#### Design Token Usage
+```scss
+// Import design system tokens
+@use 'ui-design-system/src/styles/tokens' as tokens;
+
+.hero-section {
+ padding: tokens.$spacing-section-large tokens.$spacing-container;
+ background: tokens.$color-surface-primary;
+
+ .hero-title {
+ font-family: tokens.$font-family-heading;
+ font-size: tokens.$font-size-display-large;
+ color: tokens.$color-text-primary;
+ margin-bottom: tokens.$spacing-content-medium;
+ }
+}
+```
+
+#### Spacing Hierarchy
+- **Section Padding**: `$spacing-section-large` (96px), `$spacing-section-medium` (64px)
+- **Content Spacing**: `$spacing-content-large` (48px), `$spacing-content-medium` (32px)
+- **Element Spacing**: `$spacing-element-large` (24px), `$spacing-element-medium` (16px)
+
+## Implementation Phases
+
+---
+
+## Phase 1: Foundation & Hero Components
+
+### Objective
+Establish library foundation and implement essential hero section components.
+
+### Timeline: 2-3 weeks
+
+### Components to Implement
+
+#### 1.1 HeroSection Component
+```typescript
+interface HeroSectionConfig {
+ title: string;
+ subtitle?: string;
+ ctaPrimary?: CTAButton;
+ ctaSecondary?: CTAButton;
+ backgroundType?: 'solid' | 'gradient' | 'image' | 'video';
+ alignment?: 'left' | 'center' | 'right';
+}
+```
+
+**Features:**
+- Responsive typography scaling
+- Multiple background options
+- Flexible CTA button layout
+- Integration with ui-animations for entrance effects
+
+#### 1.2 HeroWithImage Component
+```typescript
+interface HeroWithImageConfig extends HeroSectionConfig {
+ image: {
+ src: string;
+ alt: string;
+ position: 'left' | 'right' | 'background';
+ };
+ layout: 'split' | 'overlay' | 'background';
+}
+```
+
+**Features:**
+- 50/50 split layouts
+- Image overlay with opacity controls
+- Lazy loading integration
+- Responsive image handling
+
+#### 1.3 HeroSplitScreen Component
+```typescript
+interface HeroSplitScreenConfig {
+ leftContent: HeroContent;
+ rightContent: HeroContent | MediaContent;
+ verticalAlignment?: 'top' | 'center' | 'bottom';
+ reverseOnMobile?: boolean;
+}
+```
+
+**Features:**
+- Equal split responsive layout
+- Content/media combinations
+- Mobile stacking options
+- Vertical alignment controls
+
+### Integration Requirements
+
+#### UI Essentials Integration
+```typescript
+import { ButtonComponent } from 'ui-essentials';
+import { ContainerComponent } from 'ui-essentials';
+import { FlexComponent } from 'ui-essentials';
+
+// Use existing button component for CTAs
+@Component({
+ template: `
+
+
+
+
+ {{ ctaPrimary.text }}
+
+
+
+ `
+})
+```
+
+#### Design System Integration
+```scss
+@use 'ui-design-system/src/styles/tokens' as tokens;
+@use 'ui-design-system/src/styles/mixins' as mixins;
+
+.hero-section {
+ @include mixins.section-padding();
+ background: linear-gradient(
+ 135deg,
+ tokens.$color-primary-500,
+ tokens.$color-primary-700
+ );
+
+ &__title {
+ @include mixins.typography-display-large();
+ color: tokens.$color-text-on-primary;
+ margin-bottom: tokens.$spacing-content-medium;
+ }
+
+ &__subtitle {
+ @include mixins.typography-body-large();
+ color: tokens.$color-text-on-primary-muted;
+ }
+}
+```
+
+### Demo Implementation
+- Create `hero-sections-demo` component
+- Showcase all hero variants
+- Include interactive configuration panel
+- Demonstrate responsive behavior
+
+### Deliverables
+1. Library project structure
+2. Three hero components with full functionality
+3. Comprehensive demo page
+4. Unit tests for all components
+5. Documentation with usage examples
+
+---
+
+## Phase 2: Feature Sections & Social Proof
+
+### Objective
+Build components for showcasing product features and establishing credibility.
+
+### Timeline: 3-4 weeks
+
+### Components to Implement
+
+#### 2.1 FeatureGrid Component
+```typescript
+interface FeatureGridConfig {
+ features: FeatureItem[];
+ columns: 2 | 3 | 4;
+ iconStyle: 'outline' | 'filled' | 'duotone';
+ layout: 'card' | 'minimal' | 'centered';
+}
+
+interface FeatureItem {
+ icon: string; // FontAwesome icon name
+ title: string;
+ description: string;
+ link?: { text: string; url: string; };
+}
+```
+
+**Integration:**
+- Use `ui-essentials` Card component as base
+- Apply `ui-design-system` grid system
+- Integrate FontAwesome icons
+- Add hover animations from `ui-animations`
+
+#### 2.2 FeatureShowcase Component
+```typescript
+interface FeatureShowcaseConfig {
+ features: FeatureShowcaseItem[];
+ alternateLayout: boolean;
+ imageAspectRatio: '16:9' | '4:3' | '1:1';
+}
+
+interface FeatureShowcaseItem {
+ title: string;
+ description: string;
+ image: string;
+ benefits?: string[];
+ cta?: CTAButton;
+}
+```
+
+**Features:**
+- Alternating left/right layouts
+- Image lazy loading
+- Smooth scroll animations
+- Responsive stacking
+
+#### 2.3 TestimonialCarousel Component
+```typescript
+interface TestimonialCarouselConfig {
+ testimonials: Testimonial[];
+ autoplay: boolean;
+ showAvatars: boolean;
+ variant: 'card' | 'quote' | 'minimal';
+}
+
+interface Testimonial {
+ quote: string;
+ author: string;
+ title?: string;
+ company?: string;
+ avatar?: string;
+ rating?: number;
+}
+```
+
+**Integration:**
+- Extend existing carousel functionality
+- Use `ui-essentials` Avatar component
+- Apply smooth transitions from `ui-animations`
+- Implement touch/swipe gestures
+
+#### 2.4 LogoCloud Component
+```typescript
+interface LogoCloudConfig {
+ logos: CompanyLogo[];
+ layout: 'grid' | 'marquee' | 'carousel';
+ grayscale: boolean;
+ hoverEffects: boolean;
+}
+
+interface CompanyLogo {
+ name: string;
+ logo: string;
+ url?: string;
+ width?: number;
+}
+```
+
+#### 2.5 StatisticsDisplay Component
+```typescript
+interface StatisticsDisplayConfig {
+ stats: StatisticItem[];
+ layout: 'horizontal' | 'grid' | 'centered';
+ animateNumbers: boolean;
+ backgroundVariant: 'transparent' | 'subtle' | 'accent';
+}
+
+interface StatisticItem {
+ value: string | number;
+ label: string;
+ suffix?: string;
+ prefix?: string;
+ description?: string;
+}
+```
+
+### Styling Architecture
+```scss
+// Feature components base styles
+.feature-grid {
+ @include mixins.responsive-grid(
+ $mobile: 1,
+ $tablet: 2,
+ $desktop: var(--columns, 3)
+ );
+ gap: tokens.$spacing-content-large;
+
+ &__item {
+ @include mixins.card-base();
+ padding: tokens.$spacing-content-medium;
+ border-radius: tokens.$border-radius-large;
+
+ &:hover {
+ @include mixins.elevation-hover();
+ transform: translateY(-2px);
+ transition: tokens.$transition-medium;
+ }
+ }
+
+ &__icon {
+ width: 48px;
+ height: 48px;
+ color: tokens.$color-primary-500;
+ margin-bottom: tokens.$spacing-element-medium;
+ }
+
+ &__title {
+ @include mixins.typography-heading-small();
+ margin-bottom: tokens.$spacing-element-small;
+ }
+
+ &__description {
+ @include mixins.typography-body-medium();
+ color: tokens.$color-text-secondary;
+ }
+}
+```
+
+### Demo Implementation
+- `feature-sections-demo` with grid and showcase variants
+- `social-proof-demo` with testimonials and logos
+- Interactive configuration panels
+- Performance optimization examples
+
+---
+
+## Phase 3: Conversion & Call-to-Action
+
+### Objective
+Implement high-converting components focused on user actions and conversions.
+
+### Timeline: 3-4 weeks
+
+### Components to Implement
+
+#### 3.1 CTASection Component
+```typescript
+interface CTASectionConfig {
+ title: string;
+ description?: string;
+ ctaPrimary: CTAButton;
+ ctaSecondary?: CTAButton;
+ backgroundType: 'gradient' | 'pattern' | 'image' | 'solid';
+ urgency?: UrgencyConfig;
+}
+
+interface UrgencyConfig {
+ type: 'countdown' | 'limited-offer' | 'social-proof';
+ text: string;
+ endDate?: Date;
+ remaining?: number;
+}
+```
+
+**Features:**
+- Multiple background variants
+- Urgency indicators
+- A/B testing data attributes
+- Conversion tracking hooks
+
+#### 3.2 PricingTable Component
+```typescript
+interface PricingTableConfig {
+ plans: PricingPlan[];
+ billingToggle: BillingToggle;
+ featuresComparison: boolean;
+ highlightedPlan?: string;
+}
+
+interface PricingPlan {
+ id: string;
+ name: string;
+ price: PriceStructure;
+ features: PricingFeature[];
+ cta: CTAButton;
+ badge?: string;
+ popular?: boolean;
+}
+
+interface PriceStructure {
+ monthly: number;
+ yearly: number;
+ currency: string;
+ suffix?: string; // per user, per month, etc.
+}
+```
+
+**Integration:**
+- Use `ui-essentials` Table component for feature comparison
+- Implement smooth toggle animations
+- Apply pricing card hover effects
+- Include form validation for CTAs
+
+#### 3.3 NewsletterSignup Component
+```typescript
+interface NewsletterSignupConfig {
+ title: string;
+ description?: string;
+ placeholder: string;
+ ctaText: string;
+ privacyText?: string;
+ successMessage: string;
+ variant: 'inline' | 'modal' | 'sidebar' | 'footer';
+}
+```
+
+**Features:**
+- Email validation
+- GDPR compliance options
+- Success/error states
+- Integration with email services
+
+#### 3.4 ContactForm Component
+```typescript
+interface ContactFormConfig {
+ fields: FormField[];
+ submitText: string;
+ successMessage: string;
+ layout: 'single-column' | 'two-column' | 'inline';
+ validation: ValidationRules;
+}
+
+interface FormField {
+ type: 'text' | 'email' | 'tel' | 'textarea' | 'select' | 'checkbox';
+ name: string;
+ label: string;
+ placeholder?: string;
+ required: boolean;
+ options?: SelectOption[];
+}
+```
+
+**Integration:**
+- Extend `ui-essentials` form components
+- Apply `shared-utils` validation
+- Include accessibility features
+- Support for custom field types
+
+### Advanced Styling
+```scss
+// CTA Section with gradient backgrounds
+.cta-section {
+ @include mixins.section-padding();
+ position: relative;
+ overflow: hidden;
+
+ &--gradient {
+ background: linear-gradient(
+ 135deg,
+ tokens.$color-primary-500 0%,
+ tokens.$color-primary-700 50%,
+ tokens.$color-accent-500 100%
+ );
+ }
+
+ &--pattern {
+ background-color: tokens.$color-surface-secondary;
+ background-image: url('data:image/svg+xml,...'); // Pattern SVG
+ background-size: 60px 60px;
+ }
+
+ &__content {
+ position: relative;
+ z-index: 2;
+ max-width: 800px;
+ margin: 0 auto;
+ text-align: center;
+ }
+
+ &__title {
+ @include mixins.typography-display-medium();
+ color: tokens.$color-text-on-primary;
+ margin-bottom: tokens.$spacing-content-medium;
+ }
+}
+
+// Pricing table responsive design
+.pricing-table {
+ @include mixins.responsive-grid(
+ $mobile: 1,
+ $tablet: 2,
+ $desktop: 3
+ );
+ gap: tokens.$spacing-content-medium;
+
+ &__plan {
+ @include mixins.card-elevated();
+ position: relative;
+ padding: tokens.$spacing-content-large;
+ border-radius: tokens.$border-radius-large;
+
+ &--popular {
+ border: 2px solid tokens.$color-primary-500;
+ transform: scale(1.05);
+
+ &::before {
+ content: 'Most Popular';
+ position: absolute;
+ top: -12px;
+ left: 50%;
+ transform: translateX(-50%);
+ background: tokens.$color-primary-500;
+ color: tokens.$color-text-on-primary;
+ padding: tokens.$spacing-element-small tokens.$spacing-element-medium;
+ border-radius: tokens.$border-radius-medium;
+ font-size: tokens.$font-size-small;
+ font-weight: tokens.$font-weight-semibold;
+ }
+ }
+ }
+}
+```
+
+---
+
+## Phase 4: Navigation & Footer
+
+### Objective
+Create comprehensive navigation and footer systems for complete website layouts.
+
+### Timeline: 2-3 weeks
+
+### Components to Implement
+
+#### 4.1 LandingHeader Component
+```typescript
+interface LandingHeaderConfig {
+ logo: LogoConfig;
+ navigation: NavigationItem[];
+ cta?: CTAButton;
+ variant: 'transparent' | 'solid' | 'blur';
+ sticky: boolean;
+ mobileBreakpoint: number;
+}
+
+interface LogoConfig {
+ src?: string;
+ text?: string;
+ href: string;
+ width?: number;
+ height?: number;
+}
+
+interface NavigationItem {
+ label: string;
+ href?: string;
+ children?: NavigationItem[];
+ megaMenu?: MegaMenuConfig;
+}
+```
+
+**Features:**
+- Smooth scroll spy
+- Mobile hamburger menu
+- Transparent/solid transitions
+- Mega menu support
+
+#### 4.2 MegaMenu Component
+```typescript
+interface MegaMenuConfig {
+ columns: MegaMenuColumn[];
+ width: 'container' | 'full' | 'auto';
+ showOnHover: boolean;
+}
+
+interface MegaMenuColumn {
+ title?: string;
+ items: NavigationItem[];
+ featured?: FeaturedContent;
+}
+```
+
+#### 4.3 FooterSection Component
+```typescript
+interface FooterSectionConfig {
+ columns: FooterColumn[];
+ newsletter?: NewsletterSignupConfig;
+ socialLinks?: SocialLink[];
+ copyright: string;
+ logo?: LogoConfig;
+ variant: 'simple' | 'comprehensive' | 'minimal';
+}
+
+interface FooterColumn {
+ title: string;
+ links: FooterLink[];
+}
+
+interface FooterLink {
+ text: string;
+ href: string;
+ external?: boolean;
+}
+```
+
+#### 4.4 StickyNavbar Component
+```typescript
+interface StickyNavbarConfig extends LandingHeaderConfig {
+ hideOnScroll: boolean;
+ showAfter: number; // pixels
+ shrinkOnScroll: boolean;
+}
+```
+
+### Navigation Styling
+```scss
+.landing-header {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 1000;
+ transition: tokens.$transition-medium;
+
+ &--transparent {
+ background: transparent;
+ backdrop-filter: blur(10px);
+ }
+
+ &--solid {
+ background: tokens.$color-surface-primary;
+ box-shadow: tokens.$shadow-small;
+ }
+
+ &__container {
+ @include mixins.container-max-width();
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: tokens.$spacing-element-medium tokens.$spacing-container;
+ }
+
+ &__nav {
+ display: flex;
+ align-items: center;
+ gap: tokens.$spacing-element-large;
+
+ @media (max-width: tokens.$breakpoint-tablet) {
+ display: none;
+ }
+ }
+
+ &__nav-item {
+ @include mixins.typography-body-medium();
+ color: tokens.$color-text-primary;
+ text-decoration: none;
+ position: relative;
+ padding: tokens.$spacing-element-small 0;
+
+ &:hover {
+ color: tokens.$color-primary-500;
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 0;
+ height: 2px;
+ background: tokens.$color-primary-500;
+ transition: width tokens.$transition-fast;
+ }
+
+ &:hover::after {
+ width: 100%;
+ }
+ }
+}
+```
+
+---
+
+## Phase 5: Content Sections & Templates
+
+### Objective
+Complete the library with supplementary content components and ready-to-use page templates.
+
+### Timeline: 4-5 weeks
+
+### Components to Implement
+
+#### 5.1 Content Components
+
+##### FAQSection Component
+```typescript
+interface FAQSectionConfig {
+ title: string;
+ description?: string;
+ faqs: FAQItem[];
+ searchable: boolean;
+ categories?: string[];
+}
+
+interface FAQItem {
+ id: string;
+ question: string;
+ answer: string;
+ category?: string;
+}
+```
+
+##### TeamGrid Component
+```typescript
+interface TeamGridConfig {
+ title: string;
+ members: TeamMember[];
+ columns: 2 | 3 | 4;
+ showSocial: boolean;
+}
+
+interface TeamMember {
+ name: string;
+ role: string;
+ bio?: string;
+ avatar: string;
+ social?: SocialLinks;
+}
+```
+
+##### TimelineSection Component
+```typescript
+interface TimelineSectionConfig {
+ title: string;
+ events: TimelineEvent[];
+ orientation: 'vertical' | 'horizontal';
+ variant: 'minimal' | 'detailed' | 'milestone';
+}
+
+interface TimelineEvent {
+ date: string;
+ title: string;
+ description: string;
+ image?: string;
+ milestone?: boolean;
+}
+```
+
+#### 5.2 Page Templates
+
+##### SaaSLandingPage Template
+```typescript
+interface SaaSLandingPageConfig {
+ hero: HeroSectionConfig;
+ features: FeatureGridConfig;
+ socialProof: TestimonialCarouselConfig;
+ pricing: PricingTableConfig;
+ cta: CTASectionConfig;
+ footer: FooterSectionConfig;
+}
+```
+
+**Template Structure:**
+1. Header with transparent navigation
+2. Hero section with product demo
+3. Feature showcase (3-column grid)
+4. Social proof (testimonials + logo cloud)
+5. Pricing section
+6. FAQ section
+7. Final CTA section
+8. Comprehensive footer
+
+##### ProductLandingPage Template
+```typescript
+interface ProductLandingPageConfig {
+ hero: HeroWithImageConfig;
+ features: FeatureShowcaseConfig;
+ gallery: ProductGallery;
+ reviews: ReviewSection;
+ purchase: PurchaseSection;
+}
+```
+
+##### AgencyLandingPage Template
+```typescript
+interface AgencyLandingPageConfig {
+ hero: HeroSectionConfig;
+ services: FeatureGridConfig;
+ portfolio: PortfolioSection;
+ team: TeamGridConfig;
+ testimonials: TestimonialCarouselConfig;
+ contact: ContactFormConfig;
+}
+```
+
+### Template Implementation
+```typescript
+@Component({
+ selector: 'ui-saas-landing-page',
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ styleUrls: ['./saas-landing-page.component.scss']
+})
+export class SaaSLandingPageComponent {
+ @Input() config!: SaaSLandingPageConfig;
+}
+```
+
+## Technical Specifications
+
+### Performance Requirements
+- Lazy loading for all images
+- Code splitting by component category
+- Tree-shakeable exports
+- Optimal bundle size (<50kb per component category)
+- Lighthouse score >90 for demo pages
+
+### Accessibility Standards
+- WCAG 2.1 AA compliance
+- Proper ARIA labels and roles
+- Keyboard navigation support
+- Screen reader optimization
+- Color contrast ratios >4.5:1
+
+### Browser Support
+- Chrome/Edge 90+
+- Firefox 88+
+- Safari 14+
+- Mobile browsers (iOS Safari, Chrome Mobile)
+
+### Testing Strategy
+- Unit tests: >90% coverage
+- Component integration tests
+- Visual regression tests
+- Performance benchmarks
+- Accessibility automated testing
+
+### Documentation Requirements
+- Storybook integration
+- API documentation with examples
+- Usage guidelines
+- Design system integration guide
+- Migration guides
+
+## Integration Checklist
+
+### Design System Integration
+- [ ] Import design tokens correctly
+- [ ] Use consistent spacing hierarchy
+- [ ] Apply typography scale
+- [ ] Implement color system
+- [ ] Follow component naming conventions
+
+### UI Essentials Integration
+- [ ] Extend existing components where applicable
+- [ ] Reuse layout components (Container, Flex, Grid)
+- [ ] Apply consistent button styles
+- [ ] Use existing form components
+- [ ] Maintain consistent component API patterns
+
+### Animation Integration
+- [ ] Apply entrance animations
+- [ ] Add hover/interaction effects
+- [ ] Implement scroll-triggered animations
+- [ ] Use consistent easing curves
+- [ ] Optimize for performance
+
+### Utilities Integration
+- [ ] Use shared validation logic
+- [ ] Apply common helper functions
+- [ ] Implement consistent error handling
+- [ ] Use shared interfaces where applicable
+
+## Success Metrics
+
+### Development Metrics
+- All components implement consistent APIs
+- 100% TypeScript coverage
+- Zero accessibility violations
+- Performance budgets met
+- Documentation completeness
+
+### Usage Metrics
+- Component adoption rate
+- Bundle size impact
+- Build time impact
+- Developer satisfaction scores
+- Community contributions
+
+## Conclusion
+
+This implementation plan provides a structured approach to building a comprehensive ui-landing-pages library that integrates seamlessly with the existing SSuite ecosystem. By following this phased approach, the team can deliver value incrementally while maintaining high quality standards and consistency across all components.
+
+The resulting library will enable rapid development of professional landing pages and marketing websites while ensuring accessibility, performance, and maintainability standards are met.
\ No newline at end of file
diff --git a/angular.json b/angular.json
index 722b9c1..a160f16 100644
--- a/angular.json
+++ b/angular.json
@@ -196,6 +196,237 @@
}
}
}
+ },
+ "ui-animations": {
+ "projectType": "library",
+ "root": "projects/ui-animations",
+ "sourceRoot": "projects/ui-animations/src",
+ "prefix": "lib",
+ "architect": {
+ "build": {
+ "builder": "@angular-devkit/build-angular:ng-packagr",
+ "options": {
+ "project": "projects/ui-animations/ng-package.json"
+ },
+ "configurations": {
+ "production": {
+ "tsConfig": "projects/ui-animations/tsconfig.lib.prod.json"
+ },
+ "development": {
+ "tsConfig": "projects/ui-animations/tsconfig.lib.json"
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "test": {
+ "builder": "@angular-devkit/build-angular:karma",
+ "options": {
+ "tsConfig": "projects/ui-animations/tsconfig.spec.json",
+ "polyfills": [
+ "zone.js",
+ "zone.js/testing"
+ ]
+ }
+ }
+ }
+ },
+ "ui-accessibility": {
+ "projectType": "library",
+ "root": "projects/ui-accessibility",
+ "sourceRoot": "projects/ui-accessibility/src",
+ "prefix": "lib",
+ "architect": {
+ "build": {
+ "builder": "@angular-devkit/build-angular:ng-packagr",
+ "options": {
+ "project": "projects/ui-accessibility/ng-package.json"
+ },
+ "configurations": {
+ "production": {
+ "tsConfig": "projects/ui-accessibility/tsconfig.lib.prod.json"
+ },
+ "development": {
+ "tsConfig": "projects/ui-accessibility/tsconfig.lib.json"
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "test": {
+ "builder": "@angular-devkit/build-angular:karma",
+ "options": {
+ "tsConfig": "projects/ui-accessibility/tsconfig.spec.json",
+ "polyfills": [
+ "zone.js",
+ "zone.js/testing"
+ ]
+ }
+ }
+ }
+ },
+ "ui-data-utils": {
+ "projectType": "library",
+ "root": "projects/ui-data-utils",
+ "sourceRoot": "projects/ui-data-utils/src",
+ "prefix": "lib",
+ "architect": {
+ "build": {
+ "builder": "@angular-devkit/build-angular:ng-packagr",
+ "options": {
+ "project": "projects/ui-data-utils/ng-package.json"
+ },
+ "configurations": {
+ "production": {
+ "tsConfig": "projects/ui-data-utils/tsconfig.lib.prod.json"
+ },
+ "development": {
+ "tsConfig": "projects/ui-data-utils/tsconfig.lib.json"
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "test": {
+ "builder": "@angular-devkit/build-angular:karma",
+ "options": {
+ "tsConfig": "projects/ui-data-utils/tsconfig.spec.json",
+ "polyfills": [
+ "zone.js",
+ "zone.js/testing"
+ ]
+ }
+ }
+ }
+ },
+ "hcl-studio": {
+ "projectType": "library",
+ "root": "projects/hcl-studio",
+ "sourceRoot": "projects/hcl-studio/src",
+ "prefix": "lib",
+ "architect": {
+ "build": {
+ "builder": "@angular-devkit/build-angular:ng-packagr",
+ "options": {
+ "project": "projects/hcl-studio/ng-package.json"
+ },
+ "configurations": {
+ "production": {
+ "tsConfig": "projects/hcl-studio/tsconfig.lib.prod.json"
+ },
+ "development": {
+ "tsConfig": "projects/hcl-studio/tsconfig.lib.json"
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "test": {
+ "builder": "@angular-devkit/build-angular:karma",
+ "options": {
+ "tsConfig": "projects/hcl-studio/tsconfig.spec.json",
+ "polyfills": [
+ "zone.js",
+ "zone.js/testing"
+ ]
+ }
+ }
+ }
+ },
+ "ui-font-manager": {
+ "projectType": "library",
+ "root": "projects/ui-font-manager",
+ "sourceRoot": "projects/ui-font-manager/src",
+ "prefix": "lib",
+ "architect": {
+ "build": {
+ "builder": "@angular-devkit/build-angular:ng-packagr",
+ "options": {
+ "project": "projects/ui-font-manager/ng-package.json"
+ },
+ "configurations": {
+ "production": {
+ "tsConfig": "projects/ui-font-manager/tsconfig.lib.prod.json"
+ },
+ "development": {
+ "tsConfig": "projects/ui-font-manager/tsconfig.lib.json"
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "test": {
+ "builder": "@angular-devkit/build-angular:karma",
+ "options": {
+ "tsConfig": "projects/ui-font-manager/tsconfig.spec.json",
+ "polyfills": [
+ "zone.js",
+ "zone.js/testing"
+ ]
+ }
+ }
+ }
+ },
+ "ui-code-display": {
+ "projectType": "library",
+ "root": "projects/ui-code-display",
+ "sourceRoot": "projects/ui-code-display/src",
+ "prefix": "ui",
+ "architect": {
+ "build": {
+ "builder": "@angular-devkit/build-angular:ng-packagr",
+ "options": {
+ "project": "projects/ui-code-display/ng-package.json"
+ },
+ "configurations": {
+ "production": {
+ "tsConfig": "projects/ui-code-display/tsconfig.lib.prod.json"
+ },
+ "development": {
+ "tsConfig": "projects/ui-code-display/tsconfig.lib.json"
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "test": {
+ "builder": "@angular-devkit/build-angular:karma",
+ "options": {
+ "tsConfig": "projects/ui-code-display/tsconfig.spec.json",
+ "polyfills": [
+ "zone.js",
+ "zone.js/testing"
+ ]
+ }
+ }
+ }
+ },
+ "ui-backgrounds": {
+ "projectType": "library",
+ "root": "projects/ui-backgrounds",
+ "sourceRoot": "projects/ui-backgrounds/src",
+ "prefix": "lib",
+ "architect": {
+ "build": {
+ "builder": "@angular-devkit/build-angular:ng-packagr",
+ "options": {
+ "project": "projects/ui-backgrounds/ng-package.json"
+ },
+ "configurations": {
+ "production": {
+ "tsConfig": "projects/ui-backgrounds/tsconfig.lib.prod.json"
+ },
+ "development": {
+ "tsConfig": "projects/ui-backgrounds/tsconfig.lib.json"
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "test": {
+ "builder": "@angular-devkit/build-angular:karma",
+ "options": {
+ "tsConfig": "projects/ui-backgrounds/tsconfig.spec.json",
+ "polyfills": [
+ "zone.js",
+ "zone.js/testing"
+ ]
+ }
+ }
+ }
}
}
}
diff --git a/package-lock.json b/package-lock.json
index 1fffbd8..6e607ea 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,6 +20,7 @@
"@fortawesome/free-brands-svg-icons": "^7.0.0",
"@fortawesome/free-regular-svg-icons": "^7.0.0",
"@fortawesome/free-solid-svg-icons": "^7.0.0",
+ "prismjs": "^1.30.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
@@ -29,6 +30,7 @@
"@angular/cli": "^19.2.15",
"@angular/compiler-cli": "^19.2.0",
"@types/jasmine": "~5.1.0",
+ "@types/prismjs": "^1.26.5",
"jasmine-core": "~5.6.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
@@ -5252,6 +5254,12 @@
"@types/node": "*"
}
},
+ "node_modules/@types/prismjs": {
+ "version": "1.26.5",
+ "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz",
+ "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==",
+ "dev": true
+ },
"node_modules/@types/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
@@ -11266,6 +11274,14 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true
},
+ "node_modules/prismjs": {
+ "version": "1.30.0",
+ "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
+ "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/proc-log": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz",
diff --git a/package.json b/package.json
index d8a95ac..a42f0c5 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
"@fortawesome/free-brands-svg-icons": "^7.0.0",
"@fortawesome/free-regular-svg-icons": "^7.0.0",
"@fortawesome/free-solid-svg-icons": "^7.0.0",
+ "prismjs": "^1.30.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
@@ -31,6 +32,7 @@
"@angular/cli": "^19.2.15",
"@angular/compiler-cli": "^19.2.0",
"@types/jasmine": "~5.1.0",
+ "@types/prismjs": "^1.26.5",
"jasmine-core": "~5.6.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
diff --git a/projects/demo-ui-essentials/src/app/app.config.ts b/projects/demo-ui-essentials/src/app/app.config.ts
index a1e7d6f..5bf1b48 100644
--- a/projects/demo-ui-essentials/src/app/app.config.ts
+++ b/projects/demo-ui-essentials/src/app/app.config.ts
@@ -1,8 +1,48 @@
-import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
+import { ApplicationConfig, provideZoneChangeDetection, importProvidersFrom } from '@angular/core';
import { provideRouter } from '@angular/router';
+import { UiAccessibilityModule } from 'ui-accessibility';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
- providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes)]
+ providers: [
+ provideZoneChangeDetection({ eventCoalescing: true }),
+ provideRouter(routes),
+ importProvidersFrom(
+ UiAccessibilityModule.forRoot({
+ announcer: {
+ defaultPoliteness: 'polite',
+ defaultDuration: 4000,
+ enabled: true
+ },
+ focusManagement: {
+ trapFocus: true,
+ restoreFocus: true,
+ focusVisibleEnabled: true
+ },
+ keyboard: {
+ enableShortcuts: true,
+ enableArrowNavigation: true
+ },
+ skipLinks: {
+ enabled: true,
+ position: 'top',
+ links: [
+ { href: '#main', text: 'Skip to main content' },
+ { href: '#nav', text: 'Skip to navigation' }
+ ]
+ },
+ accessibility: {
+ respectReducedMotion: true,
+ respectHighContrast: true,
+ injectAccessibilityStyles: true,
+ addBodyClasses: true
+ },
+ development: {
+ warnings: true,
+ logging: true
+ }
+ })
+ )
+ ]
};
diff --git a/projects/demo-ui-essentials/src/app/demos/accessibility-demo/accessibility-demo.component.html b/projects/demo-ui-essentials/src/app/demos/accessibility-demo/accessibility-demo.component.html
new file mode 100644
index 0000000..f7c2f9e
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/accessibility-demo/accessibility-demo.component.html
@@ -0,0 +1,274 @@
+
+
+
+
+
+
+
+
+ Accessibility Preferences
+
+
High Contrast: {{ currentPreferences.prefersHighContrast ? 'Enabled' : 'Disabled' }}
+
Reduced Motion: {{ currentPreferences.prefersReducedMotion ? 'Enabled' : 'Disabled' }}
+
Color Scheme: {{ currentPreferences.colorScheme || 'Not detected' }}
+
+ Refresh Preferences
+
+
+
+
+
+
+ Live Announcements
+
+
+ Announcement Text:
+
+
+
+
+ Politeness Level:
+
+ Polite
+ Assertive
+
+
+
+
+ Make Announcement
+ Sequence Announcement
+
+
+
+ Success Message
+ Error Message
+ Loading Message
+ Simulate Validation
+
+
+
+
+
+
+ Focus Management
+
+
+
+ Monitored Input (Focus Origin: {{ focusOrigin }}):
+
+
+
+
+ Focus via Keyboard
+ Focus via Mouse
+ Focus via Program
+
+
+
+
+
+
+ Focus Trap
+
+
+
+ {{ trapEnabled ? 'Disable' : 'Enable' }} Focus Trap
+
+
+
+
+
Focus Trap Container {{ trapEnabled ? '(Active)' : '(Inactive)' }}
+
When enabled, Tab navigation will be trapped within this container.
+
+
+
+
Trapped Button 1
+
Trapped Button 2
+
+ Option 1
+ Option 2
+
+
+
+
+
+
+
+ Arrow Navigation
+
+
+
Vertical Navigation (Use ↑↓ arrows):
+
+ @for (item of navigationItems; track item.id) {
+
+ {{ item.label }}
+
+ }
+
+
+
Grid Navigation (Use ↑↓←→ arrows, 3 columns):
+
+ @for (item of gridItems; track item.id) {
+
+ {{ item.label }}
+
+ }
+
+
+
+
+
+
+ Modal with Focus Trap
+
+ Open Modal
+
+ @if (showModal) {
+
+
+
+
+
+
This modal demonstrates focus trapping. Tab navigation is contained within the modal.
+
+
+
Modal Button
+
+
+
+
+
+ }
+
+
+
+
+ Screen Reader Components
+
+
+
Screen Reader Only Text:
+
+ This text is visible.
+ This text is only for screen readers.
+ This text is visible again.
+
+
+
Focusable Screen Reader Text:
+
+ This text becomes visible when focused (try tabbing through the page).
+
+
+
Dynamic Status Messages:
+
+
+ Form submitted successfully!
+
+
+
+ Error: Please check your input.
+
+
+
+
+
+
+
+ Keyboard Shortcuts
+
+
+
+ Ctrl + Shift + A
+ Make announcement
+
+ Ctrl + M
+ Toggle modal
+
+ Tab / Shift + Tab
+ Navigate through interactive elements
+
+ Arrow Keys
+ Navigate within lists and grids (when focused)
+
+ Home / End
+ Jump to first/last item in navigable containers
+
+ Escape
+ Close modal or exit focus trap
+
+
+
+
+
+
+
diff --git a/projects/demo-ui-essentials/src/app/demos/accessibility-demo/accessibility-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/accessibility-demo/accessibility-demo.component.scss
new file mode 100644
index 0000000..d7e6d66
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/accessibility-demo/accessibility-demo.component.scss
@@ -0,0 +1,476 @@
+@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
+
+.accessibility-demo {
+ padding: $semantic-spacing-layout-md;
+ max-width: 1200px;
+ margin: 0 auto;
+ font-family: system-ui, -apple-system, sans-serif;
+ line-height: 1.5;
+}
+
+.demo-header {
+ text-align: center;
+ margin-bottom: $semantic-spacing-component-lg * 2;
+
+ h1 {
+ color: #1a1a1a;
+ font-size: 2rem;
+ font-weight: 600;
+ margin-bottom: $semantic-spacing-component-lg;
+ }
+
+ p {
+ color: #666;
+ font-size: 1.125rem;
+ }
+}
+
+.demo-section {
+ margin-bottom: $semantic-spacing-component-lg * 2;
+ padding: $semantic-spacing-component-md $semantic-spacing-component-md;
+ background: #f8f9fa;
+ border-radius: 8px;
+ border: 1px solid #e0e0e0;
+
+ h2 {
+ color: #1a1a1a;
+ font-size: 1.5rem;
+ font-weight: 600;
+ margin-bottom: $semantic-spacing-component-lg;
+ border-bottom: 2px solid #007bff;
+ padding-bottom: $semantic-spacing-component-md / 2;
+ }
+
+ h3 {
+ color: #1a1a1a;
+ font-size: 1.25rem;
+ font-weight: 600;
+ margin: $semantic-spacing-component-lg 0 $semantic-spacing-component-lg / 2 0;
+ }
+}
+
+// Preferences Display
+.preferences-display {
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-component-sm;
+ padding: $semantic-spacing-component-md;
+ background: #fff;
+ border-radius: 4px;
+ border: 1px solid #e0e0e0;
+
+ p {
+ margin: 0;
+ font-size: 1rem;
+ color: #666;
+ }
+}
+
+// Demo Controls
+.demo-controls {
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-component-md;
+}
+
+.input-group {
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-component-sm;
+
+ label {
+ font-weight: 500;
+ color: #1a1a1a;
+ font-size: 1rem;
+ }
+}
+
+.button-group {
+ display: flex;
+ gap: $semantic-spacing-component-sm;
+ flex-wrap: wrap;
+ align-items: center;
+}
+
+.demo-input {
+ padding: $semantic-spacing-component-md $semantic-spacing-component-md;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ background: #fff;
+ color: #1a1a1a;
+ font-size: 1rem;
+ transition: border-color 0.15s ease-out;
+
+ &:focus {
+ outline: 2px solid #007bff;
+ outline-offset: 2px;
+ border-color: #007bff;
+ }
+
+ &::placeholder {
+ color: #999;
+ }
+}
+
+.demo-select {
+ @extend .demo-input;
+ cursor: pointer;
+}
+
+.demo-textarea {
+ @extend .demo-input;
+ min-height: 80px;
+ resize: vertical;
+}
+
+// Focus Demo
+.focus-demo {
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-component-md;
+}
+
+// Focus Trap Demo
+.trap-controls {
+ margin-bottom: $semantic-spacing-component-lg;
+}
+
+.focus-trap-container {
+ padding: $semantic-spacing-component-md * 1.5;
+ border: 2px dashed #e0e0e0;
+ border-radius: 8px;
+ background: #fff;
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-component-md;
+ transition: all 0.3s ease-out;
+
+ &.trap-enabled {
+ border-color: #007bff;
+ background: #f0f8ff;
+ box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
+ }
+
+ h3 {
+ margin-top: 0;
+ color: #1a1a1a;
+ }
+
+ p {
+ color: #666;
+ margin: 0;
+ }
+}
+
+// Navigation Demo
+.navigation-demo {
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-component-lg;
+}
+
+.navigation-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ border: 1px solid #e0e0e0;
+ border-radius: 4px;
+ overflow: hidden;
+
+ li {
+ padding: $semantic-spacing-component-md $semantic-spacing-component-md;
+ background: #fff;
+ border-bottom: 1px solid #e0e0e0;
+ cursor: pointer;
+ transition: all 0.15s ease-out;
+ color: #1a1a1a;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ &:hover {
+ background: #f5f5f5;
+ }
+
+ &:focus {
+ outline: 2px solid #007bff;
+ outline-offset: -2px;
+ background: #f0f8ff;
+ }
+
+ &.disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ color: #999;
+ }
+ }
+}
+
+.navigation-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: $semantic-spacing-component-sm;
+ padding: $semantic-spacing-component-md;
+ border: 1px solid #e0e0e0;
+ border-radius: 4px;
+ background: #fff;
+
+ .grid-item {
+ padding: $semantic-spacing-component-md;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ background: #e9ecef;
+ color: #495057;
+ cursor: pointer;
+ transition: all 0.15s ease-out;
+ font-size: 0.875rem;
+
+ &:hover:not(:disabled) {
+ background: #dee2e6;
+ }
+
+ &:focus {
+ outline: 2px solid #007bff;
+ outline-offset: 2px;
+ }
+
+ &:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ background: #f8f9fa;
+ color: #999;
+ }
+ }
+}
+
+// Modal Demo
+.modal-backdrop {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ animation: fadeIn 0.3s ease-out;
+}
+
+.modal-content {
+ background: #fff;
+ border-radius: 12px;
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
+ min-width: 400px;
+ max-width: 600px;
+ max-height: 80vh;
+ overflow: hidden;
+ animation: slideIn 0.3s ease-out;
+
+ .modal-header {
+ padding: $semantic-spacing-component-md $semantic-spacing-component-md;
+ background: #f8f9fa;
+ border-bottom: 1px solid #e0e0e0;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ h3 {
+ margin: 0;
+ color: #1a1a1a;
+ }
+
+ .modal-close {
+ background: none;
+ border: none;
+ font-size: 24px;
+ cursor: pointer;
+ color: #666;
+ padding: 4px 8px;
+ border-radius: 4px;
+ transition: all 0.15s ease-out;
+
+ &:hover {
+ background: #f5f5f5;
+ color: #1a1a1a;
+ }
+
+ &:focus {
+ outline: 2px solid #007bff;
+ outline-offset: 2px;
+ }
+ }
+ }
+
+ .modal-body {
+ padding: $semantic-spacing-component-md * 1.5;
+
+ p {
+ color: #666;
+ margin-bottom: $semantic-spacing-component-lg;
+ }
+
+ .demo-input {
+ width: 100%;
+ margin-bottom: $semantic-spacing-component-lg;
+ }
+ }
+
+ .modal-footer {
+ padding: $semantic-spacing-component-md $semantic-spacing-component-md;
+ background: #f8f9fa;
+ border-top: 1px solid #e0e0e0;
+ display: flex;
+ gap: $semantic-spacing-component-sm;
+ justify-content: flex-end;
+ }
+}
+
+// Screen Reader Demo
+.sr-demo {
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-component-md;
+
+ p {
+ color: #666;
+ margin: $semantic-spacing-component-lg / 2 0;
+ }
+}
+
+.status-demo {
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-component-sm;
+}
+
+// Keyboard Shortcuts
+.shortcuts-info {
+ background: #f5f5f5;
+ padding: $semantic-spacing-component-md;
+ border-radius: 4px;
+ border: 1px solid #e0e0e0;
+}
+
+.shortcuts-list {
+ display: grid;
+ grid-template-columns: 200px 1fr;
+ gap: $semantic-spacing-component-sm $semantic-spacing-component-md;
+ margin: 0;
+
+ dt {
+ font-weight: 500;
+ color: #1a1a1a;
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+ font-size: 0.875rem;
+ background: #e9ecef;
+ padding: 4px 8px;
+ border-radius: 4px;
+ text-align: center;
+ }
+
+ dd {
+ margin: 0;
+ color: #666;
+ display: flex;
+ align-items: center;
+ }
+}
+
+// Footer
+.demo-footer {
+ text-align: center;
+ padding: $semantic-spacing-component-md * 2;
+ border-top: 1px solid #e0e0e0;
+ margin-top: $semantic-spacing-component-lg * 2;
+
+ p {
+ color: #666;
+ font-size: 0.875rem;
+ margin: 0;
+ }
+}
+
+// Animations
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+@keyframes slideIn {
+ from {
+ opacity: 0;
+ transform: translateY(-20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+// Responsive Design
+@media (max-width: 768px) {
+ .accessibility-demo {
+ padding: $semantic-spacing-component-md / 2;
+ }
+
+ .demo-section {
+ padding: $semantic-spacing-component-md / 2;
+ }
+
+ .button-group {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .navigation-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .shortcuts-list {
+ grid-template-columns: 1fr;
+ gap: $semantic-spacing-component-sm;
+
+ dt {
+ text-align: left;
+ }
+ }
+
+ .modal-content {
+ min-width: 90vw;
+ margin: $semantic-spacing-component-md;
+ }
+}
+
+// High Contrast Mode Support
+@media (prefers-contrast: high) {
+ .demo-section {
+ border-color: ButtonText;
+ }
+
+ .focus-trap-container.trap-enabled {
+ border-color: Highlight;
+ }
+
+ .navigation-list li:focus {
+ outline-color: Highlight;
+ }
+
+ .grid-item:focus {
+ outline-color: Highlight;
+ }
+}
+
+// Reduced Motion Support
+@media (prefers-reduced-motion: reduce) {
+ * {
+ animation-duration: 0.01s !important;
+ transition-duration: 0.01s !important;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/accessibility-demo/accessibility-demo.component.spec.ts b/projects/demo-ui-essentials/src/app/demos/accessibility-demo/accessibility-demo.component.spec.ts
new file mode 100644
index 0000000..7747f64
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/accessibility-demo/accessibility-demo.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AccessibilityDemoComponent } from './accessibility-demo.component';
+
+describe('AccessibilityDemoComponent', () => {
+ let component: AccessibilityDemoComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [AccessibilityDemoComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(AccessibilityDemoComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/projects/demo-ui-essentials/src/app/demos/accessibility-demo/accessibility-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/accessibility-demo/accessibility-demo.component.ts
new file mode 100644
index 0000000..3eb7424
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/accessibility-demo/accessibility-demo.component.ts
@@ -0,0 +1,213 @@
+import { Component, OnInit, OnDestroy, ViewChild, ElementRef, inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+
+// 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',
+ imports: [
+ CommonModule,
+ FormsModule,
+ UiAccessibilityModule,
+ ButtonComponent
+ ],
+ templateUrl: './accessibility-demo.component.html',
+ styleUrl: './accessibility-demo.component.scss'
+})
+export class AccessibilityDemoComponent implements OnInit, OnDestroy {
+ private liveAnnouncer = inject(LiveAnnouncerService);
+ private focusMonitor = inject(FocusMonitorService);
+ private keyboardManager = inject(KeyboardManagerService);
+ private highContrast = inject(HighContrastService);
+ private a11yConfig = inject(A11yConfigService);
+
+ @ViewChild('trapContainer') trapContainer!: ElementRef;
+ @ViewChild('monitoredInput') monitoredInput!: ElementRef;
+
+ // Demo state
+ trapEnabled = false;
+ showModal = false;
+ currentPreferences: any = {};
+ focusOrigin = '';
+ announcementText = 'Hello, this is a test announcement!';
+ selectedPoliteness: 'polite' | 'assertive' = 'polite';
+
+ // Navigation demo items
+ navigationItems = [
+ { id: 1, label: 'Profile Settings', disabled: false },
+ { id: 2, label: 'Account Details', disabled: false },
+ { id: 3, label: 'Privacy Controls', disabled: true },
+ { id: 4, label: 'Notifications', disabled: false },
+ { id: 5, label: 'Security Options', disabled: false }
+ ];
+
+ // Grid navigation items
+ gridItems = Array.from({ length: 12 }, (_, i) => ({
+ id: i + 1,
+ label: `Item ${i + 1}`,
+ disabled: i === 5 // Make item 6 disabled
+ }));
+
+ ngOnInit(): void {
+ this.setupKeyboardShortcuts();
+ this.setupFocusMonitoring();
+ this.getCurrentPreferences();
+ }
+
+ ngOnDestroy(): void {
+ this.keyboardManager.unregisterAll();
+ }
+
+ private setupKeyboardShortcuts(): void {
+ // Global shortcuts
+ this.keyboardManager.registerGlobalShortcut('announce', {
+ key: 'a',
+ ctrlKey: true,
+ shiftKey: true,
+ description: 'Make announcement',
+ handler: () => this.makeAnnouncement()
+ });
+
+ this.keyboardManager.registerGlobalShortcut('toggleModal', {
+ key: 'm',
+ ctrlKey: true,
+ description: 'Toggle modal',
+ handler: () => this.toggleModal()
+ });
+ }
+
+ private setupFocusMonitoring(): void {
+ // Monitor focus on the input field
+ setTimeout(() => {
+ if (this.monitoredInput) {
+ this.focusMonitor.monitor(this.monitoredInput).subscribe(focusEvent => {
+ this.focusOrigin = focusEvent.origin || 'none';
+ if (focusEvent.origin) {
+ this.liveAnnouncer.announce(
+ `Input focused via ${focusEvent.origin}`,
+ 'polite'
+ );
+ }
+ });
+ }
+ }, 100);
+ }
+
+ private getCurrentPreferences(): void {
+ this.currentPreferences = this.highContrast.getCurrentPreferences();
+ }
+
+ // Demo actions
+ makeAnnouncement(): void {
+ this.liveAnnouncer.announce(this.announcementText, this.selectedPoliteness);
+ }
+
+ makeSequenceAnnouncement(): void {
+ const messages = [
+ 'Starting process...',
+ 'Processing data...',
+ 'Validating information...',
+ 'Process completed successfully!'
+ ];
+ this.liveAnnouncer.announceSequence(messages, 'polite', 1500);
+ }
+
+ toggleFocusTrap(): void {
+ this.trapEnabled = !this.trapEnabled;
+ this.liveAnnouncer.announce(
+ `Focus trap ${this.trapEnabled ? 'enabled' : 'disabled'}`,
+ 'assertive'
+ );
+ }
+
+ toggleModal(): void {
+ this.showModal = !this.showModal;
+ if (this.showModal) {
+ this.liveAnnouncer.announce('Modal opened', 'assertive');
+ } else {
+ this.liveAnnouncer.announce('Modal closed', 'assertive');
+ }
+ }
+
+ focusViaKeyboard(): void {
+ if (this.monitoredInput) {
+ this.focusMonitor.focusVia(this.monitoredInput, 'keyboard');
+ }
+ }
+
+ focusViaMouse(): void {
+ if (this.monitoredInput) {
+ this.focusMonitor.focusVia(this.monitoredInput, 'mouse');
+ }
+ }
+
+ focusViaProgram(): void {
+ if (this.monitoredInput) {
+ this.focusMonitor.focusVia(this.monitoredInput, 'program');
+ }
+ }
+
+ onNavigationChange(event: any): void {
+ this.liveAnnouncer.announce(
+ `Navigated to ${this.navigationItems[event.nextIndex]?.label}`,
+ 'polite'
+ );
+ }
+
+ onGridNavigationChange(event: any): void {
+ this.liveAnnouncer.announce(
+ `Navigated to ${this.gridItems[event.nextIndex]?.label}`,
+ 'polite'
+ );
+ }
+
+ announceError(): void {
+ this.liveAnnouncer.announce(
+ 'Error: Please fix the required fields before continuing',
+ 'assertive'
+ );
+ }
+
+ announceSuccess(): void {
+ this.liveAnnouncer.announce(
+ 'Success: Your changes have been saved',
+ 'polite'
+ );
+ }
+
+ announceLoading(): void {
+ this.liveAnnouncer.announce(
+ 'Loading content, please wait...',
+ 'polite'
+ );
+ }
+
+ refreshPreferences(): void {
+ this.getCurrentPreferences();
+ this.liveAnnouncer.announce('Accessibility preferences updated', 'polite');
+ }
+
+ simulateFormValidation(): void {
+ // Simulate a form validation scenario
+ this.liveAnnouncer.announce('Validating form...', 'polite');
+
+ setTimeout(() => {
+ this.liveAnnouncer.announce(
+ 'Form validation complete. 2 errors found.',
+ 'assertive'
+ );
+ }, 2000);
+ }
+}
diff --git a/projects/demo-ui-essentials/src/app/demos/animations-demo/animations-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/animations-demo/animations-demo.component.ts
new file mode 100644
index 0000000..6450147
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/animations-demo/animations-demo.component.ts
@@ -0,0 +1,223 @@
+import { Component, ElementRef, ViewChild } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { UiAnimationsService, AnimateDirective } from 'ui-animations';
+
+@Component({
+ selector: 'app-animations-demo',
+ standalone: true,
+ imports: [CommonModule, AnimateDirective],
+ template: `
+
+
UI Animations Demo
+
+
+
+ Entrance Animations
+
+
+
Fade In
+
Basic fade in animation
+
+
+
+
Fade In Up
+
Fade in with upward movement
+
+
+
+
Slide In Up
+
Slide in from bottom
+
+
+
+
Zoom In
+
Scale up animation
+
+
+
+
+
+
+ Emphasis Animations
+
+
+ Bounce
+
+
+
+ Shake
+
+
+
+ Pulse
+
+
+
+ Wobble
+
+
+
+
+ Click buttons above to animate me!
+
+
+
+
+
+ Animation Directive Examples
+
+
+
Hover to Animate
+
Uses directive with hover trigger
+
+
+
+
Click to Animate
+
Uses directive with click trigger
+
+
+
+
+
+
+ Animation Service Examples
+
+ Tada
+ Jello
+ Heartbeat
+ Rubber Band
+
+
+
+ Animation Service Target
+
+
+
+ `,
+ styles: [`
+ .animations-demo {
+ padding: 2rem;
+ max-width: 1200px;
+ margin: 0 auto;
+ }
+
+ .demo-section {
+ margin-bottom: 3rem;
+
+ h3 {
+ margin-bottom: 1.5rem;
+ color: #333;
+ border-bottom: 2px solid #007bff;
+ padding-bottom: 0.5rem;
+ }
+ }
+
+ .animation-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 1.5rem;
+ margin-bottom: 2rem;
+ }
+
+ .demo-card {
+ padding: 1.5rem;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ transition: transform 0.2s ease;
+
+ h4 {
+ margin: 0 0 0.5rem 0;
+ font-size: 1.2rem;
+ }
+
+ p {
+ margin: 0;
+ opacity: 0.9;
+ }
+
+ &:hover {
+ transform: translateY(-2px);
+ }
+ }
+
+ .demo-button {
+ padding: 0.75rem 1.5rem;
+ border: none;
+ border-radius: 6px;
+ background: #007bff;
+ color: white;
+ cursor: pointer;
+ font-size: 1rem;
+ transition: background-color 0.2s ease;
+
+ &:hover {
+ background: #0056b3;
+ }
+ }
+
+ .demo-target {
+ padding: 2rem;
+ border: 2px dashed #007bff;
+ border-radius: 8px;
+ text-align: center;
+ font-size: 1.2rem;
+ font-weight: bold;
+ background: #f8f9fa;
+ margin-top: 1rem;
+ }
+
+ .service-target {
+ background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
+ color: white;
+ border: none;
+ }
+
+ .service-controls {
+ display: flex;
+ gap: 1rem;
+ flex-wrap: wrap;
+ margin-bottom: 1rem;
+ }
+
+ /* Responsive */
+ @media (max-width: 768px) {
+ .animation-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .service-controls {
+ justify-content: center;
+ }
+ }
+ `]
+})
+export class AnimationsDemoComponent {
+ @ViewChild('animationTarget', { static: true }) animationTarget!: ElementRef;
+ @ViewChild('serviceTarget', { static: true }) serviceTarget!: ElementRef;
+
+ constructor(private animationService: UiAnimationsService) {}
+
+ animateElement(animationType: string) {
+ const element = this.animationTarget.nativeElement;
+ const animationClass = `animate-${animationType}`;
+
+ // Remove any existing animation classes first
+ element.className = element.className.replace(/animate-\w+/g, '');
+
+ // Add animation class
+ this.animationService.animateOnce(element, animationClass);
+ }
+
+ serviceAnimate(animationClass: string) {
+ const element = this.serviceTarget.nativeElement;
+
+ // Remove any existing animation classes first
+ element.className = element.className.replace(/animate-\w+/g, '');
+
+ // Add animation class with service
+ this.animationService.animateOnce(element, animationClass);
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/backgrounds-demo/backgrounds-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/backgrounds-demo/backgrounds-demo.component.scss
new file mode 100644
index 0000000..29bb517
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/backgrounds-demo/backgrounds-demo.component.scss
@@ -0,0 +1,294 @@
+@use 'ui-design-system/src/styles' as ui;
+
+.backgrounds-demo {
+ padding: 2rem;
+ max-width: 1200px;
+ margin: 0 auto;
+
+ .demo-header {
+ text-align: center;
+ margin-bottom: 3rem;
+
+ h1 {
+ font-size: 3rem;
+ font-weight: 700;
+ color: var(--primary-900, #1e293b);
+ margin-bottom: 1rem;
+ }
+
+ p {
+ font-size: 1.2rem;
+ color: var(--neutral-600, #64748b);
+ }
+ }
+
+ .demo-section {
+ margin-bottom: 4rem;
+
+ h2 {
+ font-size: 2rem;
+ font-weight: 600;
+ color: var(--primary-800, #334155);
+ margin-bottom: 2rem;
+ border-bottom: 2px solid var(--primary-200, #e2e8f0);
+ padding-bottom: 0.5rem;
+ }
+ }
+
+ .example-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 2rem;
+ margin-bottom: 2rem;
+ }
+
+ .demo-card {
+ padding: 2rem;
+ border-radius: 1rem;
+ min-height: 200px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+ color: white;
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
+
+ &:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
+ }
+
+ h3 {
+ font-size: 1.5rem;
+ font-weight: 600;
+ margin-bottom: 1rem;
+ }
+
+ p {
+ font-size: 1rem;
+ opacity: 0.9;
+ line-height: 1.5;
+ }
+ }
+
+ .pattern-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
+ gap: 1rem;
+ }
+
+ .pattern-demo {
+ aspect-ratio: 1;
+ border-radius: 0.5rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ overflow: hidden;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ transition: transform 0.2s ease;
+
+ &:hover {
+ transform: scale(1.05);
+ }
+
+ .pattern-name {
+ background: rgba(0, 0, 0, 0.7);
+ color: white;
+ padding: 0.5rem 1rem;
+ border-radius: 0.25rem;
+ font-size: 0.875rem;
+ font-weight: 500;
+ text-transform: capitalize;
+ }
+ }
+
+ .gradient-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 2rem;
+ }
+
+ .gradient-demo {
+ aspect-ratio: 16/9;
+ border-radius: 1rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: white;
+ font-size: 1.2rem;
+ font-weight: 600;
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
+ transition: transform 0.3s ease;
+
+ &:hover {
+ transform: translateY(-3px);
+ }
+ }
+
+ .interactive-demo {
+ padding: 3rem;
+ border-radius: 1rem;
+ min-height: 300px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ color: white;
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
+ box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
+ position: relative;
+ transition: all 0.5s ease;
+
+ .controls {
+ display: flex;
+ gap: 1rem;
+ margin-bottom: 2rem;
+ flex-wrap: wrap;
+ justify-content: center;
+ }
+
+ h3 {
+ font-size: 2rem;
+ margin-bottom: 1rem;
+ }
+
+ p {
+ font-size: 1.1rem;
+ opacity: 0.9;
+ }
+ }
+
+ .fullscreen-controls {
+ display: flex;
+ gap: 1rem;
+ flex-wrap: wrap;
+ justify-content: center;
+ align-items: center;
+ }
+
+ .action-btn {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ border: none;
+ padding: 1rem 2rem;
+ border-radius: 0.5rem;
+ font-weight: 600;
+ font-size: 1rem;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
+
+ &:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
+ }
+
+ &:active {
+ transform: translateY(0);
+ }
+ }
+
+ .preset-btn {
+ background: rgba(255, 255, 255, 0.1);
+ border: 2px solid rgba(255, 255, 255, 0.2);
+ color: var(--primary-700, #475569);
+ padding: 0.75rem 1.5rem;
+ border-radius: 0.5rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ backdrop-filter: blur(10px);
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.2);
+ border-color: rgba(255, 255, 255, 0.4);
+ transform: translateY(-1px);
+ }
+
+ &:active {
+ transform: translateY(0);
+ }
+ }
+}
+
+// Responsive adjustments
+@media (max-width: 768px) {
+ .backgrounds-demo {
+ padding: 1rem;
+
+ .demo-header h1 {
+ font-size: 2rem;
+ }
+
+ .example-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .pattern-grid {
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
+ }
+
+ .gradient-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .interactive-demo {
+ padding: 2rem;
+
+ .controls {
+ flex-direction: column;
+ width: 100%;
+ }
+ }
+
+ .fullscreen-controls {
+ flex-direction: column;
+ }
+ }
+}
+
+// Dark mode support
+@media (prefers-color-scheme: dark) {
+ .backgrounds-demo {
+ .demo-header h1 {
+ color: var(--neutral-100, #f8fafc);
+ }
+
+ .demo-section h2 {
+ color: var(--neutral-200, #e2e8f0);
+ border-bottom-color: var(--primary-700, #475569);
+ }
+
+ .preset-btn {
+ color: var(--neutral-200, #e2e8f0);
+ }
+ }
+}
+
+// High contrast mode
+@media (prefers-contrast: high) {
+ .backgrounds-demo {
+ .demo-card,
+ .pattern-demo,
+ .gradient-demo,
+ .interactive-demo {
+ border: 2px solid rgba(0, 0, 0, 0.8);
+ }
+ }
+}
+
+// Reduced motion
+@media (prefers-reduced-motion: reduce) {
+ .backgrounds-demo {
+ * {
+ transition: none !important;
+ animation: none !important;
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/backgrounds-demo/backgrounds-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/backgrounds-demo/backgrounds-demo.component.ts
new file mode 100644
index 0000000..e57683f
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/backgrounds-demo/backgrounds-demo.component.ts
@@ -0,0 +1,307 @@
+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';
+
+@Component({
+ selector: 'app-backgrounds-demo',
+ standalone: true,
+ imports: [
+ CommonModule,
+ BackgroundDirective,
+ SolidBackgroundComponent,
+ GradientBackgroundComponent,
+ PatternBackgroundComponent,
+ ImageBackgroundComponent
+ ],
+ template: `
+
+
+
+
+
+ Background Directive Examples
+
+
+
+
Solid Background
+
Using the background directive with solid color
+
+
+
+
Linear Gradient
+
Beautiful gradient backgrounds
+
+
+
+
Pattern Background
+
Geometric patterns
+
+
+
+
+
+
+ Background Components
+
+
+
+ Solid Component
+ Green background using component
+
+
+
+ Gradient Component
+ Purple to cyan gradient
+
+
+
+ Pattern Component
+ Orange dots pattern
+
+
+
+
+
+
+
+
+
+ Gradient Types
+
+
+
+ Linear Gradient
+
+
+
+ Radial Gradient
+
+
+
+ Conic Gradient
+
+
+
+
+
+
+ Interactive Background
+
+
+
+
+ {{ config.name }}
+
+
+
Interactive Background Demo
+
Click buttons to change the background
+
+
+
+
+
+ Full Screen Backgrounds
+
+
+
+ {{ hasFullScreenBg() ? 'Remove' : 'Add' }} Full Screen Background
+
+
+ {{ bg.name }}
+
+
+
+
+ `,
+ styleUrl: './backgrounds-demo.component.scss'
+})
+export class BackgroundsDemoComponent {
+ private readonly backgroundService = signal(new BackgroundService());
+ private fullScreenBackgroundId = signal(null);
+
+ // Configuration signals
+ solidConfig = signal({
+ type: 'solid',
+ color: '#3b82f6'
+ });
+
+ gradientConfig = signal({
+ type: 'linear-gradient',
+ direction: 'to bottom right',
+ colors: [
+ { color: '#ec4899' },
+ { color: '#8b5cf6' }
+ ]
+ });
+
+ patternConfig = signal({
+ type: 'pattern',
+ pattern: 'grid',
+ primaryColor: '#1f2937',
+ secondaryColor: 'transparent',
+ size: 20,
+ opacity: 0.3
+ });
+
+ interactiveConfig = signal(this.solidConfig());
+
+ // Pattern list for showcase
+ patterns = [
+ 'dots', 'grid', 'stripes', 'diagonal-stripes',
+ 'chevron', 'waves', 'circles', 'checkerboard'
+ ];
+
+ // Preset configurations
+ presetConfigs = [
+ {
+ name: 'Ocean',
+ config: {
+ type: 'linear-gradient' as const,
+ direction: 'to bottom',
+ colors: [{ color: '#667eea' }, { color: '#764ba2' }]
+ }
+ },
+ {
+ name: 'Sunset',
+ config: {
+ type: 'linear-gradient' as const,
+ direction: 'to right',
+ colors: [{ color: '#ff6b6b' }, { color: '#feca57' }]
+ }
+ },
+ {
+ name: 'Forest',
+ config: {
+ type: 'solid' as const,
+ color: '#10b981'
+ }
+ },
+ {
+ name: 'Dots',
+ config: {
+ type: 'pattern' as const,
+ pattern: 'dots' as const,
+ primaryColor: '#6366f1',
+ secondaryColor: 'transparent',
+ size: 25,
+ opacity: 0.6
+ }
+ }
+ ];
+
+ // Full screen presets
+ fullScreenPresets = [
+ {
+ name: 'Purple Mesh',
+ config: {
+ type: 'linear-gradient' as const,
+ direction: 'to bottom right',
+ colors: [{ color: '#667eea' }, { color: '#764ba2' }, { color: '#f093fb' }]
+ }
+ },
+ {
+ name: 'Geometric',
+ config: {
+ type: 'pattern' as const,
+ pattern: 'chevron' as const,
+ primaryColor: '#1f2937',
+ secondaryColor: 'transparent',
+ size: 40,
+ opacity: 0.1
+ }
+ }
+ ];
+
+ getPatternConfig(pattern: string): PatternConfig {
+ return {
+ type: 'pattern',
+ pattern: pattern as any,
+ primaryColor: '#6366f1',
+ secondaryColor: 'transparent',
+ size: 25,
+ opacity: 0.4
+ };
+ }
+
+ setInteractiveConfig(preset: any) {
+ this.interactiveConfig.set(preset.config);
+ }
+
+ toggleFullScreenBackground() {
+ const currentId = this.fullScreenBackgroundId();
+ if (currentId) {
+ this.backgroundService().removeBackground(currentId);
+ this.fullScreenBackgroundId.set(null);
+ }
+ }
+
+ setFullScreenBackground(preset: any) {
+ // Remove existing full screen background
+ const currentId = this.fullScreenBackgroundId();
+ if (currentId) {
+ this.backgroundService().removeBackground(currentId);
+ }
+
+ // Add new full screen background
+ const id = this.backgroundService().applyFullScreenBackground(preset.config, {
+ zIndex: -1
+ });
+ this.fullScreenBackgroundId.set(id);
+ }
+
+ hasFullScreenBg() {
+ return this.fullScreenBackgroundId() !== null;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/code-display-demo/code-display-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/code-display-demo/code-display-demo.component.scss
new file mode 100644
index 0000000..cada4cd
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/code-display-demo/code-display-demo.component.scss
@@ -0,0 +1,71 @@
+@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
+
+.demo-container {
+ padding: $semantic-spacing-layout-section-sm;
+ max-width: 1200px;
+ margin: 0 auto;
+
+ h1 {
+ color: $semantic-color-text-primary;
+ margin-bottom: $semantic-spacing-layout-section-sm;
+ border-bottom: 2px solid $semantic-color-border-primary;
+ padding-bottom: $semantic-spacing-component-md;
+ }
+}
+
+.demo-section {
+ margin-bottom: $semantic-spacing-layout-section-md;
+
+ h2 {
+ color: $semantic-color-text-primary;
+ margin-bottom: $semantic-spacing-component-lg;
+ font-size: $base-typography-font-size-xl;
+ }
+
+ h3 {
+ color: $semantic-color-text-secondary;
+ margin: $semantic-spacing-component-lg 0 $semantic-spacing-component-md 0;
+ font-size: $base-typography-font-size-lg;
+ }
+
+ p {
+ margin-bottom: $semantic-spacing-component-md;
+ line-height: $base-typography-line-height-relaxed;
+ color: $semantic-color-text-primary;
+ }
+}
+
+.theme-controls {
+ display: flex;
+ gap: $semantic-spacing-component-sm;
+ margin-bottom: $semantic-spacing-component-lg;
+ flex-wrap: wrap;
+}
+
+.theme-btn {
+ padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
+ border: $semantic-border-button-width $semantic-border-button-style $semantic-color-border-primary;
+ border-radius: $semantic-border-button-radius;
+ background: $semantic-color-surface-primary;
+ color: $semantic-color-text-primary;
+ cursor: pointer;
+ font-size: $base-typography-font-size-sm;
+ text-transform: capitalize;
+ transition: all $semantic-duration-fast $semantic-easing-standard;
+
+ &:hover {
+ background: $semantic-color-surface-elevated;
+ border-color: $semantic-color-brand-primary;
+ }
+
+ &.active {
+ background: $semantic-color-brand-primary;
+ color: $semantic-color-on-brand-primary;
+ border-color: $semantic-color-brand-primary;
+ }
+
+ &:focus-visible {
+ outline: $semantic-border-focus-width solid $semantic-color-focus;
+ outline-offset: 2px;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/code-display-demo/code-display-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/code-display-demo/code-display-demo.component.ts
new file mode 100644
index 0000000..66cfe4f
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/code-display-demo/code-display-demo.component.ts
@@ -0,0 +1,214 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import {
+ CodeSnippetComponent,
+ InlineCodeComponent,
+ CodeBlockComponent,
+ CodeThemeService,
+ CodeTheme
+} from 'ui-code-display';
+
+@Component({
+ selector: 'app-code-display-demo',
+ standalone: true,
+ imports: [
+ CommonModule,
+ CodeSnippetComponent,
+ InlineCodeComponent,
+ CodeBlockComponent
+ ],
+ template: `
+
+
Code Display Components Demo
+
+
+ Theme Selector
+
+ @for (theme of themes; track theme) {
+
+ {{ theme }}
+
+ }
+
+
+
+
+ Inline Code
+
+ Use
+ to output to the console.
+
+
+ Install dependencies with
+
+
+
+
+
+
+
+
+ Different Sizes
+ Small
+
+
+
+ Medium (Default)
+
+
+
+ Large
+
+
+
+
+
+ Different Languages
+
+ Python
+
+
+
+ JSON
+
+
+
+ HTML
+
+
+
+
+ `,
+ styleUrl: './code-display-demo.component.scss'
+})
+export class CodeDisplayDemoComponent {
+
+ constructor(private themeService: CodeThemeService) {
+ this.themes = this.themeService.getAvailableThemes();
+ this.currentTheme = this.themeService.theme;
+ }
+
+ readonly themes: any[];
+ readonly currentTheme: any;
+
+ readonly typescriptCode = `import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-example',
+ template: \`
+
+
{{ title }}
+
{{ description }}
+
+ \`
+})
+export class ExampleComponent {
+ title = 'Hello World';
+ description = 'This is a demo component';
+}`;
+
+ readonly cssCode = `.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 2rem;
+
+ h1 {
+ color: var(--primary-color);
+ font-size: 2rem;
+ margin-bottom: 1rem;
+ }
+
+ p {
+ line-height: 1.6;
+ color: var(--text-color);
+ }
+}`;
+
+ readonly shortCode = `const greeting = 'Hello, World!';
+console.log(greeting);`;
+
+ readonly pythonCode = `def fibonacci(n):
+ if n <= 1:
+ return n
+ return fibonacci(n-1) + fibonacci(n-2)
+
+# Generate first 10 Fibonacci numbers
+for i in range(10):
+ print(f"F({i}) = {fibonacci(i)}")`;
+
+ readonly jsonCode = `{
+ "name": "ui-code-display",
+ "version": "1.0.0",
+ "description": "Code display components for Angular",
+ "dependencies": {
+ "@angular/core": "^19.0.0",
+ "prismjs": "^1.30.0"
+ },
+ "keywords": [
+ "angular",
+ "code",
+ "syntax-highlighting",
+ "prism"
+ ]
+}`;
+
+ readonly htmlCode = `
+
+
+
+
This is the card content.
+
Action
+
+
`;
+
+ setTheme(theme: CodeTheme): void {
+ this.themeService.setTheme(theme);
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/data-utils-demo/data-utils-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/data-utils-demo/data-utils-demo.component.ts
new file mode 100644
index 0000000..ab9e975
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/data-utils-demo/data-utils-demo.component.ts
@@ -0,0 +1,741 @@
+import { Component, ChangeDetectionStrategy, signal, computed } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+
+// 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;
+ name: string;
+ department: string;
+ salary: number;
+ hireDate: string;
+ active: boolean;
+ skills: string[];
+}
+
+@Component({
+ selector: 'ui-data-utils-demo',
+ standalone: true,
+ imports: [CommonModule, FormsModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
UI Data Utils Demo
+
Comprehensive demonstration of data manipulation utilities
+
+
+
+ Sample Employee Data ({{ sampleData.length }} records)
+
+
Raw Data Preview:
+
{{ getSampleDataPreview() }}
+
+
+
+
+
+ 🔄 Sorting Utilities
+
+
+
+
+
Single Property Sort
+
+ Sort by:
+
+ Name
+ Department
+ Salary
+ Hire Date
+
+
+
+ Descending
+
+
+
+
{{ getSortedDataPreview() }}
+
+
+
+
+
+
Multi-column Sort
+
Department → Salary → Name
+
+
{{ getMultiSortedDataPreview() }}
+
+
+
+
+
+
+
+ 🔍 Filtering Utilities
+
+
+
+
+
Search Filter
+
+
+
{{ getSearchFilteredDataPreview() }}
+
+
+
+
+
+
Property Filters
+
+ Department:
+
+ All Departments
+ Engineering
+ Marketing
+ Sales
+ HR
+
+
+
+ Min Salary:
+
+
+
+
{{ getPropertyFilteredDataPreview() }}
+
+
+
+
+
+
+
+ 📄 Pagination Utilities
+
+
+
+ Page Size:
+
+ 3
+ 5
+ 7
+ 10
+
+
+ Page:
+
+
+ of {{ paginationResult().totalPages }}
+
+
+ Previous
+
+
+ Next
+
+
+
+
+ Page Navigation:
+ @for (page of pageRange(); track page) {
+ @if (page === 'ellipsis') {
+ ...
+ } @else {
+
+ {{ page }}
+
+ }
+ }
+
+
+
+
Showing: {{ getItemRangeText() }}
+
{{ getPaginatedDataPreview() }}
+
+
+
+
+
+
+ 🔄 Transformation Utilities
+
+
+
+
+
Group By Department
+
+
{{ getGroupedDataPreview() }}
+
+
+
+
+
+
Salary Statistics by Department
+
+
{{ getAggregationPreview() }}
+
+
+
+
+
+
Extract Names & Salaries
+
+
{{ getPluckedDataPreview() }}
+
+
+
+
+
+
Pivot: Department vs Active Status
+
+
{{ getPivotDataPreview() }}
+
+
+
+
+
+
+
+ 🔄 Combined Operations
+ Demonstrate chaining multiple operations: Filter → Sort → Paginate
+
+
+
+ Filter Active:
+
+ All
+ Active Only
+ Inactive Only
+
+
+
+
+ Sort By:
+
+ Name
+ Salary
+ Hire Date
+
+
+
+
+
+ Descending
+
+
+
+
+
Pipeline:
+ {{ sampleData.length }} records →
+ {{ combinedFiltered().length }} filtered →
+ {{ combinedSorted().length }} sorted →
+ {{ combinedPaginated().data.length }} on page {{ combinedPaginated().page }}
+
+
{{ getCombinedResultPreview() }}
+
+
+
+
+
+ 📊 Performance & Statistics
+
+
+
+
Total Records
+
{{ sampleData.length }}
+
+
+
+
Departments
+
{{ getDepartmentCount() }}
+
+
+
+
Avg Salary
+
{{ getAverageSalary() }}
+
+
+
+
Active Employees
+
{{ getActiveCount() }}%
+
+
+
+
+
+
+ 💻 Code Examples
+
+
+
Sorting Examples:
+
// Single sort
+const sorted = sortBy(data, {{ '{' }} key: 'salary', direction: 'desc', dataType: 'number' {{ '}' }});
+
+// Multi-column sort
+const multiSorted = sortByMultiple(data, {{ '{' }}
+ sorts: [
+ {{ '{' }} key: 'department', direction: 'asc', dataType: 'string' {{ '}' }},
+ {{ '{' }} key: 'salary', direction: 'desc', dataType: 'number' {{ '}' }}
+ ]
+{{ '}' }});
+
+
Filtering Examples:
+
// Property filter
+const filtered = filterBy(data, {{ '{' }}
+ key: 'salary',
+ operator: 'greater_than',
+ value: 50000,
+ dataType: 'number'
+{{ '}' }});
+
+// Search across multiple properties
+const searched = searchFilter(data, 'engineer', ['name', 'department']);
+
+
Pagination Examples:
+
// Paginate data
+const paginatedResult = paginate(data, {{ '{' }} page: 1, pageSize: 10 {{ '}' }});
+
+// Get page range for UI
+const range = getPaginationRange(currentPage, totalPages, 7);
+
+
Transformation Examples:
+
// Group by property
+const groups = groupBy(data, 'department');
+
+// Aggregate values
+const stats = aggregate(data, 'salary', ['sum', 'avg', 'min', 'max']);
+
+// Extract specific properties
+const names = pluck(data, ['name', 'salary']);
+
+
+
+
+
+ 📚 Library Information
+
+
UI Data Utils Features:
+
+
+
🔄 Sorting
+
+ Single & multi-column sorting
+ Type-aware comparisons
+ Stable sort algorithms
+ Custom comparator functions
+
+
+
+
🔍 Filtering
+
+ Multiple filter operators
+ Search across properties
+ Complex filter combinations
+ Type-specific filtering
+
+
+
+
📄 Pagination
+
+ Client-side pagination
+ Page range calculations
+ Pagination metadata
+ Navigation utilities
+
+
+
+
🔄 Transformation
+
+ Group by operations
+ Data aggregations
+ Property extraction
+ Pivot tables
+
+
+
+
+
+
+ `,
+ styles: [`
+ h2 {
+ color: hsl(279, 14%, 11%);
+ font-size: 2rem;
+ margin-bottom: 1rem;
+ border-bottom: 2px solid hsl(258, 100%, 47%);
+ padding-bottom: 0.5rem;
+ }
+
+ h3 {
+ color: hsl(279, 14%, 25%);
+ font-size: 1.5rem;
+ margin-bottom: 1rem;
+ }
+
+ h4 {
+ color: hsl(279, 14%, 35%);
+ font-size: 1.2rem;
+ margin-bottom: 0.5rem;
+ }
+
+ h5 {
+ color: hsl(279, 14%, 45%);
+ font-size: 1rem;
+ margin-bottom: 0.5rem;
+ }
+
+ section {
+ border: 1px solid #e9ecef;
+ padding: 1.5rem;
+ border-radius: 8px;
+ background: #ffffff;
+ }
+
+ pre {
+ margin: 0;
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+ font-size: 0.85rem;
+ line-height: 1.4;
+ }
+
+ code {
+ color: hsl(279, 14%, 15%);
+ }
+
+ button:hover:not(:disabled) {
+ opacity: 0.8;
+ }
+
+ button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+
+ input, select {
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ padding: 0.25rem;
+ }
+
+ label {
+ font-weight: 500;
+ }
+ `]
+})
+export class DataUtilsDemoComponent {
+ // Sample data
+ sampleData: SampleData[] = [
+ { id: 1, name: 'Alice Johnson', department: 'Engineering', salary: 95000, hireDate: '2022-01-15', active: true, skills: ['JavaScript', 'Angular', 'TypeScript'] },
+ { id: 2, name: 'Bob Smith', department: 'Marketing', salary: 65000, hireDate: '2021-03-22', active: true, skills: ['SEO', 'Content Marketing'] },
+ { id: 3, name: 'Charlie Brown', department: 'Engineering', salary: 87000, hireDate: '2023-02-10', active: false, skills: ['React', 'Node.js'] },
+ { id: 4, name: 'Diana Davis', department: 'Sales', salary: 72000, hireDate: '2020-11-05', active: true, skills: ['CRM', 'Negotiation'] },
+ { id: 5, name: 'Eve Wilson', department: 'Engineering', salary: 102000, hireDate: '2021-08-18', active: true, skills: ['Python', 'Machine Learning'] },
+ { id: 6, name: 'Frank Miller', department: 'HR', salary: 58000, hireDate: '2022-05-30', active: false, skills: ['Recruiting', 'Employee Relations'] },
+ { id: 7, name: 'Grace Lee', department: 'Marketing', salary: 69000, hireDate: '2023-01-12', active: true, skills: ['Brand Management', 'Social Media'] },
+ { id: 8, name: 'Henry Taylor', department: 'Sales', salary: 78000, hireDate: '2021-12-03', active: true, skills: ['Sales Strategy', 'Lead Generation'] },
+ { id: 9, name: 'Ivy Chen', department: 'Engineering', salary: 91000, hireDate: '2022-09-14', active: true, skills: ['Vue.js', 'GraphQL'] },
+ { id: 10, name: 'Jack White', department: 'HR', salary: 62000, hireDate: '2020-04-25', active: false, skills: ['Payroll', 'Benefits Administration'] }
+ ];
+
+ // Sorting controls
+ sortField = signal('name');
+ sortDesc = signal(false);
+
+ // Filtering controls
+ searchTerm = signal('');
+ filterDepartment = signal('');
+ minSalary = signal(null);
+
+ // Pagination controls
+ currentPage = signal(1);
+ pageSize = signal(5);
+
+ // Combined demo controls
+ combinedActiveFilter = signal('');
+ combinedSortField = signal('name');
+ combinedSortDesc = signal(false);
+
+ // Computed data
+ sortedData = computed(() => {
+ const config: SortConfig = {
+ key: this.sortField(),
+ direction: this.sortDesc() ? 'desc' : 'asc',
+ dataType: this.getDataType(this.sortField())
+ };
+ return sortBy(this.sampleData, config);
+ });
+
+ multiSortedData = computed(() => {
+ return sortByMultiple(this.sampleData, {
+ sorts: [
+ { key: 'department', direction: 'asc', dataType: 'string' },
+ { key: 'salary', direction: 'desc', dataType: 'number' },
+ { key: 'name', direction: 'asc', dataType: 'string' }
+ ]
+ });
+ });
+
+ searchFilteredData = computed(() => {
+ if (!this.searchTerm()) return this.sampleData;
+ return searchFilter(this.sampleData, this.searchTerm(), ['name', 'department']);
+ });
+
+ propertyFilteredData = computed(() => {
+ let filtered = this.sampleData;
+
+ const filters: FilterConfig[] = [];
+
+ if (this.filterDepartment()) {
+ filters.push({
+ key: 'department',
+ operator: 'equals',
+ value: this.filterDepartment(),
+ dataType: 'string'
+ });
+ }
+
+ if (this.minSalary() !== null) {
+ filters.push({
+ key: 'salary',
+ operator: 'greater_than_equal',
+ value: this.minSalary(),
+ dataType: 'number'
+ });
+ }
+
+ return filters.length > 0 ? filterByMultiple(filtered, filters) : filtered;
+ });
+
+ paginationResult = computed(() => {
+ return paginate(this.sampleData, {
+ page: this.currentPage(),
+ pageSize: this.pageSize()
+ });
+ });
+
+ pageRange = computed(() => {
+ return getPaginationRange(this.currentPage(), this.paginationResult().totalPages, 7);
+ });
+
+ // Combined operations
+ combinedFiltered = computed(() => {
+ if (!this.combinedActiveFilter()) return this.sampleData;
+ return filterBy(this.sampleData, {
+ key: 'active',
+ operator: 'equals',
+ value: this.combinedActiveFilter() === 'true',
+ dataType: 'boolean'
+ });
+ });
+
+ combinedSorted = computed(() => {
+ return sortBy(this.combinedFiltered(), {
+ key: this.combinedSortField(),
+ direction: this.combinedSortDesc() ? 'desc' : 'asc',
+ dataType: this.getDataType(this.combinedSortField())
+ });
+ });
+
+ combinedPaginated = computed(() => {
+ return paginate(this.combinedSorted(), { page: 1, pageSize: 5 });
+ });
+
+ // Helper methods
+ private getDataType(key: keyof SampleData): 'string' | 'number' | 'date' | 'boolean' {
+ switch (key) {
+ case 'salary': case 'id': return 'number';
+ case 'hireDate': return 'date';
+ case 'active': return 'boolean';
+ default: return 'string';
+ }
+ }
+
+ getSampleDataPreview(): string {
+ return JSON.stringify(this.sampleData.slice(0, 3), null, 2);
+ }
+
+ getSortedDataPreview(): string {
+ return JSON.stringify(this.sortedData().slice(0, 3).map(item => ({ name: item.name, [this.sortField()]: item[this.sortField()] })), null, 2);
+ }
+
+ getMultiSortedDataPreview(): string {
+ return JSON.stringify(this.multiSortedData().slice(0, 4).map(item => ({
+ name: item.name,
+ department: item.department,
+ salary: item.salary
+ })), null, 2);
+ }
+
+ getSearchFilteredDataPreview(): string {
+ return JSON.stringify(this.searchFilteredData().slice(0, 3).map(item => ({
+ name: item.name,
+ department: item.department
+ })), null, 2);
+ }
+
+ getPropertyFilteredDataPreview(): string {
+ return JSON.stringify(this.propertyFilteredData().slice(0, 3).map(item => ({
+ name: item.name,
+ department: item.department,
+ salary: item.salary
+ })), null, 2);
+ }
+
+ getPaginatedDataPreview(): string {
+ return JSON.stringify(this.paginationResult().data.map(item => ({
+ name: item.name,
+ department: item.department
+ })), null, 2);
+ }
+
+ getGroupedDataPreview(): string {
+ const grouped = groupBy(this.sampleData, 'department');
+ return JSON.stringify(grouped.map(group => ({
+ department: group.key,
+ count: group.count,
+ employees: group.items.map(emp => emp.name)
+ })), null, 2);
+ }
+
+ getAggregationPreview(): string {
+ const grouped = groupBy(this.sampleData, 'department');
+ const result = grouped.map(group => ({
+ department: group.key,
+ ...aggregate(group.items, 'salary', ['sum', 'avg', 'min', 'max', 'count'])
+ }));
+ return JSON.stringify(result, null, 2);
+ }
+
+ getPluckedDataPreview(): string {
+ const plucked = pluck(this.sampleData, ['name', 'salary']);
+ return JSON.stringify(plucked.slice(0, 5), null, 2);
+ }
+
+ getPivotDataPreview(): string {
+ const pivotData = this.sampleData.map(emp => ({
+ department: emp.department,
+ status: emp.active ? 'Active' : 'Inactive',
+ count: 1
+ }));
+
+ const pivotResult = pivot(pivotData, 'department', 'status', 'count', 'sum');
+ return JSON.stringify(pivotResult, null, 2);
+ }
+
+ getCombinedResultPreview(): string {
+ return JSON.stringify(this.combinedPaginated().data.map(item => ({
+ name: item.name,
+ department: item.department,
+ salary: item.salary,
+ active: item.active
+ })), null, 2);
+ }
+
+ getItemRangeText(): string {
+ const result = this.paginationResult();
+ if (result.totalItems === 0) return '0 of 0';
+ return `${result.startIndex + 1}-${result.endIndex + 1} of ${result.totalItems}`;
+ }
+
+ getDepartmentCount(): number {
+ return unique(this.sampleData, 'department').length;
+ }
+
+ getAverageSalary(): string {
+ const avg = aggregate(this.sampleData, 'salary', ['avg']);
+ return '$' + Math.round(avg['avg'] || 0).toLocaleString();
+ }
+
+ getActiveCount(): number {
+ const activeCount = this.sampleData.filter(emp => emp.active).length;
+ return Math.round((activeCount / this.sampleData.length) * 100);
+ }
+
+ // Event handlers
+ applySingleSort(): void {
+ // Triggers computed update
+ }
+
+ applySearchFilter(): void {
+ // Triggers computed update
+ }
+
+ applyPropertyFilter(): void {
+ // Triggers computed update
+ }
+
+ updatePagination(): void {
+ // Ensure page is within bounds
+ const maxPage = this.paginationResult().totalPages;
+ if (this.currentPage() > maxPage) {
+ this.currentPage.set(maxPage || 1);
+ }
+ }
+
+ previousPage(): void {
+ if (this.paginationResult().hasPrevious) {
+ this.currentPage.update(page => page - 1);
+ }
+ }
+
+ nextPage(): void {
+ if (this.paginationResult().hasNext) {
+ this.currentPage.update(page => page + 1);
+ }
+ }
+
+ goToPage(page: number | string): void {
+ if (typeof page === 'number') {
+ this.currentPage.set(page);
+ }
+ }
+
+ updateCombinedDemo(): void {
+ // Triggers computed update
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/demos.routes.ts b/projects/demo-ui-essentials/src/app/demos/demos.routes.ts
index aa69dbe..c17a605 100644
--- a/projects/demo-ui-essentials/src/app/demos/demos.routes.ts
+++ b/projects/demo-ui-essentials/src/app/demos/demos.routes.ts
@@ -9,6 +9,7 @@ import { TableDemoComponent } from './table-demo/table-demo.component';
import { BadgeDemoComponent } from './badge-demo/badge-demo.component';
import { MenuDemoComponent } from './menu-demo/menu-demo.component';
import { InputDemoComponent } from './input-demo/input-demo.component';
+import { KanbanBoardDemoComponent } from './kanban-board-demo/kanban-board-demo.component';
import { RadioDemoComponent } from './radio-demo/radio-demo.component';
import { CheckboxDemoComponent } from './checkbox-demo/checkbox-demo.component';
import { SearchDemoComponent } from './search-demo/search-demo.component';
@@ -77,6 +78,20 @@ import { GridContainerDemoComponent } from './grid-container-demo/grid-container
import { FeedLayoutDemoComponent } from './feed-layout-demo/feed-layout-demo.component';
import { ListDetailLayoutDemoComponent } from './list-detail-layout-demo/list-detail-layout-demo.component';
import { SupportingPaneLayoutDemoComponent } from './supporting-pane-layout-demo/supporting-pane-layout-demo.component';
+import { MasonryDemoComponent } from './masonry-demo/masonry-demo.component';
+import { InfiniteScrollContainerDemoComponent } from './infinite-scroll-container-demo/infinite-scroll-container-demo.component';
+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';
@Component({
@@ -128,6 +143,10 @@ import { SupportingPaneLayoutDemoComponent } from './supporting-pane-layout-demo
@case ("input") {
}
+
+ @case ("kanban-board") {
+
+ }
@case ("radio") {
}
@@ -140,6 +159,14 @@ import { SupportingPaneLayoutDemoComponent } from './supporting-pane-layout-demo
}
+ @case ("select") {
+
+ }
+
+ @case ("textarea") {
+
+ }
+
@case ("switch") {
}
@@ -360,6 +387,10 @@ import { SupportingPaneLayoutDemoComponent } from './supporting-pane-layout-demo
}
+ @case ("infinite-scroll-container") {
+
+ }
+
@case ("tabs-container") {
}
@@ -384,12 +415,56 @@ import { SupportingPaneLayoutDemoComponent } from './supporting-pane-layout-demo
}
+ @case ("masonry") {
+
+ }
+
+ @case ("sticky-layout") {
+
+ }
+
+ @case ("split-view") {
+
+ }
+
+ @case ("gallery-grid") {
+
+ }
+
+ @case ("animations") {
+
+ }
+
+ @case ("accessibility") {
+
+ }
+
+ @case ("data-utils") {
+
+ }
+
+ @case ("hcl-studio") {
+
+ }
+
+ @case ("font-manager") {
+
+ }
+
+ @case ("code-display") {
+
+ }
+
+ @case ("backgrounds") {
+
+ }
+
}
`,
imports: [AvatarDemoComponent, ButtonDemoComponent, IconButtonDemoComponent, CardDemoComponent,
ChipDemoComponent, TableDemoComponent, BadgeDemoComponent,
- MenuDemoComponent, InputDemoComponent,
+ MenuDemoComponent, InputDemoComponent, KanbanBoardDemoComponent,
RadioDemoComponent, CheckboxDemoComponent,
SearchDemoComponent, SwitchDemoComponent, ProgressDemoComponent,
AppbarDemoComponent, BottomNavigationDemoComponent, FontAwesomeDemoComponent, ImageContainerDemoComponent,
@@ -399,7 +474,7 @@ import { SupportingPaneLayoutDemoComponent } from './supporting-pane-layout-demo
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, TabsContainerDemoComponent, DashboardShellDemoComponent, GridContainerDemoComponent, FeedLayoutDemoComponent, ListDetailLayoutDemoComponent, SupportingPaneLayoutDemoComponent]
+ 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]
})
diff --git a/projects/demo-ui-essentials/src/app/demos/font-manager-demo/font-manager-demo.component.html b/projects/demo-ui-essentials/src/app/demos/font-manager-demo/font-manager-demo.component.html
new file mode 100644
index 0000000..1a1e45f
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/font-manager-demo/font-manager-demo.component.html
@@ -0,0 +1,178 @@
+
+
UI Font Manager Demo
+
+ Dynamically load Google Fonts and switch between font themes with smooth transitions.
+ Select a font preset to apply it globally to the entire application.
+
+
+
+
+ Font Preset Selection
+
+ Choose from curated font combinations. The selected preset will be applied to the entire application.
+
+
+
+
+ Choose Font Preset:
+
+ Select a preset...
+
+ {{preset.name}} - {{preset.description}}
+
+
+
+
+
+ Transition Effect:
+
+ Fade
+ Scale
+ Typewriter
+
+
+ Apply with {{selectedTransition | titlecase}} Effect
+
+
+
+
+
+
+
Currently Applied: {{currentPreset.name}}
+
+
+ Sans-serif:
+ {{currentPreset.fonts.sans}}
+
+
+ Serif:
+ {{currentPreset.fonts.serif}}
+
+
+ Monospace:
+ {{currentPreset.fonts.mono}}
+
+
+ Display:
+ {{currentPreset.fonts.display}}
+
+
+
+
+
+
+
+ Typography Preview
+ See how the selected fonts look in a real application context.
+
+
+
+
Display Heading (H1)
+
Sans-serif Heading (H2)
+
Serif Heading (H3)
+
+
+ This is a paragraph using the sans-serif font. It demonstrates how readable body text
+ appears with the currently selected font combination. Lorem ipsum dolor sit amet,
+ consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+
+
+ "This is a quote using the serif font, which often provides better readability
+ for longer text passages and adds elegance to quoted content."
+
+
+
// Code example with monospace font
+function applyFontTheme(theme) {
+ document.documentElement.style.setProperty('--font-family-sans', theme.sans);
+ document.documentElement.style.setProperty('--font-family-serif', theme.serif);
+ document.documentElement.style.setProperty('--font-family-mono', theme.mono);
+}
+
+
+ Current fonts: Sans (var(--font-family-sans)) | Serif (var(--font-family-serif)) |
+ Mono (var(--font-family-mono)) | Display (var(--font-family-display))
+
+
+
+
+
+
+
+ Available Font Presets
+
+
+
+
{{preset.name}}
+
{{preset.description}}
+
+
+
+ Sans:
+ {{preset.fonts.sans}}
+
+
+ Serif:
+ {{preset.fonts.serif}}
+
+
+ Mono:
+ {{preset.fonts.mono}}
+
+
+ Display:
+ {{preset.fonts.display}}
+
+
+
+
+ Apply {{preset.name}}
+
+
+
+
+
+
+
+ Popular Individual Fonts
+ Load individual fonts to test and preview them.
+
+
+
+
+
+ {{font}}
+ ✓
+
+
+
+
+
+
+
0">
+ Event Log
+
+
+ {{event.time}}
+ {{event.message}}
+
+
+
+
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/font-manager-demo/font-manager-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/font-manager-demo/font-manager-demo.component.scss
new file mode 100644
index 0000000..63a59ef
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/font-manager-demo/font-manager-demo.component.scss
@@ -0,0 +1,580 @@
+.demo-container {
+ padding: 2rem;
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+.demo-description {
+ font-size: 1.1rem;
+ color: #666;
+ margin-bottom: 2rem;
+ line-height: 1.6;
+}
+
+.demo-section {
+ margin: 3rem 0;
+ padding: 2rem;
+ border: 1px solid #e0e0e0;
+ border-radius: 12px;
+ background: #fafafa;
+
+ h2, h3 {
+ margin-top: 0;
+ color: #333;
+ }
+
+ h2 {
+ font-size: 1.5rem;
+ margin-bottom: 1.5rem;
+ }
+
+ h3 {
+ font-size: 1.25rem;
+ margin-bottom: 1rem;
+ }
+}
+
+// Loading State Display
+.state-info {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 1rem;
+ margin-top: 1rem;
+}
+
+.state-item {
+ padding: 1rem;
+ background: white;
+ border-radius: 8px;
+ border-left: 4px solid #2196F3;
+
+ strong {
+ color: #333;
+ display: block;
+ margin-bottom: 0.5rem;
+ }
+
+ .loading {
+ color: #FF9800;
+ font-weight: 500;
+ }
+
+ .idle {
+ color: #4CAF50;
+ font-weight: 500;
+ }
+}
+
+// Manual Font Controls
+.font-controls {
+ margin-top: 1rem;
+}
+
+.font-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
+ gap: 1rem;
+}
+
+.font-button {
+ position: relative;
+ padding: 1rem;
+ border: 2px solid #e0e0e0;
+ border-radius: 8px;
+ background: white;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ font-size: 0.9rem;
+
+ &:hover {
+ border-color: #2196F3;
+ background: #f5f5f5;
+ }
+
+ &.loaded {
+ border-color: #4CAF50;
+ background: #e8f5e8;
+ color: #2e7d32;
+ font-weight: 500;
+ }
+}
+
+.load-status {
+ position: absolute;
+ top: 0.5rem;
+ right: 0.5rem;
+ color: #4CAF50;
+ font-weight: bold;
+}
+
+// Transition Controls
+.transition-controls {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
+ gap: 1rem;
+ margin-top: 1rem;
+}
+
+.transition-btn {
+ padding: 1rem;
+ border: 2px solid #2196F3;
+ border-radius: 8px;
+ background: white;
+ color: #2196F3;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ font-weight: 500;
+
+ &:hover {
+ background: #2196F3;
+ color: white;
+ }
+
+ &.scale {
+ border-color: #FF9800;
+ color: #FF9800;
+
+ &:hover {
+ background: #FF9800;
+ color: white;
+ }
+ }
+
+ &.typewriter {
+ border-color: #9C27B0;
+ color: #9C27B0;
+
+ &:hover {
+ background: #9C27B0;
+ color: white;
+ }
+ }
+}
+
+// Typography Showcase
+.typography-showcase {
+ display: grid;
+ gap: 2rem;
+ margin-top: 1.5rem;
+}
+
+.type-sample {
+ padding: 2rem;
+ background: white;
+ border-radius: 12px;
+ border: 1px solid #e0e0e0;
+ transition: all 0.3s ease;
+
+ h1, h2, h3, h4 {
+ margin: 0 0 1rem 0;
+ line-height: 1.3;
+ }
+
+ h1 {
+ font-size: 2.5rem;
+ font-weight: 700;
+ }
+
+ h2 {
+ font-size: 2rem;
+ font-weight: 600;
+ }
+
+ h3 {
+ font-size: 1.75rem;
+ font-weight: 500;
+ }
+
+ h4 {
+ font-size: 1.5rem;
+ font-weight: 500;
+ }
+
+ p {
+ line-height: 1.6;
+ margin-bottom: 1rem;
+ }
+
+ pre {
+ background: #f5f5f5;
+ padding: 1rem;
+ border-radius: 8px;
+ overflow-x: auto;
+ margin: 1rem 0;
+
+ code {
+ font-size: 0.9rem;
+ line-height: 1.5;
+ }
+ }
+}
+
+.font-info {
+ font-size: 0.8rem;
+ color: #666;
+ font-style: italic;
+ margin-top: 1rem;
+ padding-top: 1rem;
+ border-top: 1px solid #e0e0e0;
+}
+
+// Available Themes
+.themes-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 1.5rem;
+ margin-top: 1.5rem;
+}
+
+.theme-card {
+ padding: 1.5rem;
+ background: white;
+ border: 1px solid #e0e0e0;
+ border-radius: 12px;
+ transition: all 0.3s ease;
+
+ &:hover {
+ border-color: #2196F3;
+ box-shadow: 0 4px 12px rgba(33, 150, 243, 0.1);
+ }
+
+ h4 {
+ margin: 0 0 0.5rem 0;
+ color: #333;
+ font-size: 1.1rem;
+ }
+}
+
+.theme-description {
+ color: #666;
+ font-size: 0.9rem;
+ margin-bottom: 1rem;
+ line-height: 1.4;
+}
+
+.theme-fonts {
+ margin-bottom: 1.5rem;
+}
+
+.theme-font {
+ display: flex;
+ justify-content: space-between;
+ padding: 0.25rem 0;
+ font-size: 0.85rem;
+
+ strong {
+ color: #333;
+ min-width: 60px;
+ }
+
+ & + .theme-font {
+ border-top: 1px solid #f0f0f0;
+ }
+}
+
+.apply-theme-btn {
+ width: 100%;
+ padding: 0.75rem;
+ border: 1px solid #2196F3;
+ border-radius: 6px;
+ background: #2196F3;
+ color: white;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ font-weight: 500;
+
+ &:hover {
+ background: #1976D2;
+ border-color: #1976D2;
+ }
+}
+
+// Event Log
+.event-log {
+ max-height: 300px;
+ overflow-y: auto;
+ background: white;
+ border: 1px solid #e0e0e0;
+ border-radius: 8px;
+ padding: 1rem;
+}
+
+.event-item {
+ display: flex;
+ gap: 1rem;
+ padding: 0.5rem 0;
+ font-size: 0.9rem;
+ border-bottom: 1px solid #f0f0f0;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ &.success {
+ color: #4CAF50;
+ }
+
+ &.error {
+ color: #f44336;
+ }
+
+ &.info {
+ color: #2196F3;
+ }
+}
+
+.event-time {
+ color: #666;
+ font-size: 0.8rem;
+ min-width: 80px;
+ font-family: var(--font-family-mono, 'Courier New', monospace);
+}
+
+.event-message {
+ flex: 1;
+}
+
+// Font Preset Controls
+.preset-controls {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+ margin-top: 1rem;
+}
+
+.preset-selector {
+ label {
+ display: block;
+ margin-bottom: 0.5rem;
+ font-weight: 600;
+ color: #333;
+ }
+
+ select {
+ width: 100%;
+ padding: 0.75rem;
+ border: 2px solid #e0e0e0;
+ border-radius: 8px;
+ font-size: 1rem;
+ background: white;
+ transition: border-color 0.3s ease;
+
+ &:focus {
+ outline: none;
+ border-color: #2196F3;
+ }
+
+ &:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ }
+ }
+}
+
+.transition-selector {
+ display: flex;
+ gap: 1rem;
+ align-items: end;
+ flex-wrap: wrap;
+
+ label {
+ font-weight: 600;
+ color: #333;
+ margin-bottom: 0.5rem;
+ display: block;
+ }
+
+ select {
+ padding: 0.5rem;
+ border: 1px solid #ccc;
+ border-radius: 6px;
+ font-size: 0.9rem;
+ min-width: 120px;
+ }
+}
+
+.apply-btn {
+ padding: 0.75rem 1.5rem;
+ background: #2196F3;
+ color: white;
+ border: none;
+ border-radius: 8px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ white-space: nowrap;
+
+ &:hover:not(:disabled) {
+ background: #1976D2;
+ transform: translateY(-2px);
+ }
+
+ &:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ transform: none;
+ }
+}
+
+// Current Preset Display
+.current-preset {
+ margin-top: 2rem;
+ padding: 1.5rem;
+ background: #e8f5e8;
+ border: 1px solid #4CAF50;
+ border-radius: 12px;
+
+ h4 {
+ margin: 0 0 1rem 0;
+ color: #2e7d32;
+ font-size: 1.1rem;
+ }
+}
+
+.preset-fonts {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 0.5rem;
+}
+
+.font-assignment {
+ display: flex;
+ justify-content: space-between;
+ padding: 0.5rem;
+ background: white;
+ border-radius: 6px;
+ font-size: 0.9rem;
+
+ .font-type {
+ font-weight: 600;
+ color: #333;
+ }
+
+ .font-name {
+ color: #666;
+ font-style: italic;
+ }
+}
+
+// Theme card active state
+.theme-card.active {
+ border-color: #4CAF50;
+ background: #f1f8e9;
+
+ h4 {
+ color: #2e7d32;
+ }
+}
+
+// Section descriptions
+.preset-description,
+.preview-description,
+.section-description {
+ color: #666;
+ font-size: 1rem;
+ margin-bottom: 1rem;
+ line-height: 1.5;
+}
+
+// Font transition animations
+:global(html.font-transition-fade) {
+ * {
+ transition: font-family 0.8s ease-in-out;
+ }
+}
+
+:global(html.font-transition-scale) {
+ * {
+ transition: font-family 0.6s ease-in-out, transform 0.6s ease-in-out;
+ }
+
+ body {
+ transform: scale(1.02);
+ transition: transform 0.3s ease-in-out;
+ }
+
+ &.font-transition-scale body {
+ transform: scale(1);
+ }
+}
+
+:global(html.font-transition-typewriter) {
+ * {
+ transition: font-family 0.5s steps(10, end);
+ }
+}
+
+// Enhanced blockquote styling
+blockquote {
+ margin: 1.5rem 0;
+ padding: 1rem 1.5rem;
+ border-left: 4px solid #2196F3;
+ background: #f8f9fa;
+ font-style: italic;
+ position: relative;
+
+ &::before {
+ content: '"';
+ font-size: 3rem;
+ color: #2196F3;
+ position: absolute;
+ top: -0.5rem;
+ left: 1rem;
+ opacity: 0.3;
+ }
+}
+
+// Responsive Design
+@media (max-width: 768px) {
+ .demo-container {
+ padding: 1rem;
+ }
+
+ .demo-section {
+ padding: 1.5rem;
+ }
+
+ .font-grid {
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
+ }
+
+ .transition-controls {
+ grid-template-columns: 1fr;
+ }
+
+ .themes-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .state-info {
+ grid-template-columns: 1fr;
+ }
+
+ .type-sample {
+ padding: 1.5rem;
+
+ h1 { font-size: 2rem; }
+ h2 { font-size: 1.75rem; }
+ h3 { font-size: 1.5rem; }
+ h4 { font-size: 1.25rem; }
+ }
+
+ .preset-controls {
+ gap: 1rem;
+ }
+
+ .transition-selector {
+ flex-direction: column;
+ align-items: stretch;
+ gap: 1rem;
+
+ select,
+ .apply-btn {
+ width: 100%;
+ }
+ }
+
+ .preset-fonts {
+ grid-template-columns: 1fr;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/font-manager-demo/font-manager-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/font-manager-demo/font-manager-demo.component.ts
new file mode 100644
index 0000000..fff9520
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/font-manager-demo/font-manager-demo.component.ts
@@ -0,0 +1,448 @@
+import { Component, OnInit, OnDestroy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { Subject, takeUntil } from 'rxjs';
+
+// Mock font combinations since ui-font-manager is not yet available
+const FONT_COMBINATIONS = {
+ 'Modern Clean': {
+ sans: 'Inter',
+ serif: 'Playfair Display',
+ mono: 'JetBrains Mono',
+ display: 'Inter'
+ },
+ 'Classic Editorial': {
+ sans: 'Source Sans Pro',
+ serif: 'Lora',
+ mono: 'Source Code Pro',
+ display: 'Source Sans Pro'
+ },
+ 'Friendly Modern': {
+ sans: 'Poppins',
+ serif: 'Merriweather',
+ mono: 'Fira Code',
+ display: 'Poppins'
+ },
+ 'Professional': {
+ sans: 'Roboto',
+ serif: 'EB Garamond',
+ mono: 'Roboto Mono',
+ display: 'Roboto'
+ }
+};
+
+@Component({
+ selector: 'app-font-manager-demo',
+ standalone: true,
+ imports: [CommonModule, FormsModule],
+ template: `
+
+
UI Font Manager Demo
+
+ Dynamically load Google Fonts and switch between font themes with smooth transitions.
+ Select a font preset to apply it globally to the entire application.
+
+
+
+
+ Font Preset Selection
+
+ Choose from curated font combinations. The selected preset will be applied to the entire application.
+
+
+
+
+ Choose Font Preset:
+
+ Select a preset...
+
+ {{preset.name}} - {{preset.description}}
+
+
+
+
+
+ Transition Effect:
+
+ Fade
+ Scale
+ Typewriter
+
+
+ Apply with {{selectedTransition | titlecase}} Effect
+
+
+
+
+
+
+
Currently Applied: {{currentPreset.name}}
+
+
+ Sans-serif:
+ {{currentPreset.fonts.sans}}
+
+
+ Serif:
+ {{currentPreset.fonts.serif}}
+
+
+ Monospace:
+ {{currentPreset.fonts.mono}}
+
+
+ Display:
+ {{currentPreset.fonts.display}}
+
+
+
+
+
+
+
+ Typography Preview
+ See how the selected fonts look in a real application context.
+
+
+
+
Display Heading (H1)
+
Sans-serif Heading (H2)
+
Serif Heading (H3)
+
+
+ This is a paragraph using the sans-serif font. It demonstrates how readable body text
+ appears with the currently selected font combination. Lorem ipsum dolor sit amet,
+ consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+
+
+ "This is a quote using the serif font, which often provides better readability
+ for longer text passages and adds elegance to quoted content."
+
+
+
+ Current fonts: Sans (var(--font-family-sans)) | Serif (var(--font-family-serif)) |
+ Mono (var(--font-family-mono)) | Display (var(--font-family-display))
+
+
+
+
+
+
+
+ Available Font Presets
+
+
+
+
{{preset.name}}
+
{{preset.description}}
+
+
+
+ Sans:
+ {{preset.fonts.sans}}
+
+
+ Serif:
+ {{preset.fonts.serif}}
+
+
+ Mono:
+ {{preset.fonts.mono}}
+
+
+ Display:
+ {{preset.fonts.display}}
+
+
+
+
+ Apply {{preset.name}}
+
+
+
+
+
+
+
+ Popular Individual Fonts
+ Load individual fonts to test and preview them.
+
+
+
+
+
+ {{font}}
+ ✓
+
+
+
+
+
+
+
0">
+ Event Log
+
+
+ {{event.time}}
+ {{event.message}}
+
+
+
+
+ `,
+ styleUrl: './font-manager-demo.component.scss'
+})
+export class FontManagerDemoComponent implements OnInit, OnDestroy {
+ popularFonts = ['Inter', 'Roboto', 'Playfair Display', 'JetBrains Mono', 'Montserrat', 'Lora'];
+ loadedFonts = new Set();
+
+ // Font preset management
+ fontPresets = [
+ {
+ key: 'modern-clean',
+ name: 'Modern Clean',
+ description: 'Contemporary and minimal design with Inter and Playfair Display',
+ fonts: FONT_COMBINATIONS['Modern Clean']
+ },
+ {
+ key: 'classic-editorial',
+ name: 'Classic Editorial',
+ description: 'Traditional publishing style with Source Sans Pro and Lora',
+ fonts: FONT_COMBINATIONS['Classic Editorial']
+ },
+ {
+ key: 'friendly-modern',
+ name: 'Friendly Modern',
+ description: 'Approachable and warm with Poppins and Merriweather',
+ fonts: FONT_COMBINATIONS['Friendly Modern']
+ },
+ {
+ key: 'professional',
+ name: 'Professional',
+ description: 'Corporate and reliable with Roboto and EB Garamond',
+ fonts: FONT_COMBINATIONS['Professional']
+ },
+ {
+ key: 'system',
+ name: 'System Default',
+ description: 'Use system fonts for optimal performance',
+ fonts: {
+ sans: 'system-ui',
+ serif: 'ui-serif',
+ mono: 'ui-monospace',
+ display: 'system-ui'
+ }
+ }
+ ];
+
+ selectedPreset: string = '';
+ selectedTransition: 'fade' | 'scale' | 'typewriter' = 'fade';
+ currentPreset: any = null;
+
+ eventLog: Array<{time: string, message: string, type: 'success' | 'error' | 'info'}> = [];
+
+ private destroy$ = new Subject();
+
+ constructor() {}
+
+ ngOnInit(): void {
+ this.logEvent('Component initialized', 'info');
+ }
+
+ ngOnDestroy(): void {
+ this.destroy$.next();
+ this.destroy$.complete();
+ }
+
+ onThemeChanged(themeName: string): void {
+ this.logEvent(`Theme changed to: ${themeName}`, 'success');
+ }
+
+ onFontLoaded(fontName: string): void {
+ this.logEvent(`Font loaded: ${fontName}`, 'success');
+ this.loadedFonts.add(fontName);
+ }
+
+ onFontError(event: {fontName: string, error: string}): void {
+ this.logEvent(`Font error - ${event.fontName}: ${event.error}`, 'error');
+ }
+
+ loadSingleFont(fontName: string): void {
+ this.logEvent(`Loading font: ${fontName}`, 'info');
+
+ // Load font via Google Fonts API
+ this.loadGoogleFont(fontName);
+ this.loadedFonts.add(fontName);
+ this.logEvent(`Successfully loaded: ${fontName}`, 'success');
+ }
+
+ applyTheme(themeName: string): void {
+ this.logEvent(`Applying theme: ${themeName}`, 'info');
+ this.logEvent(`Successfully applied theme: ${themeName}`, 'success');
+ }
+
+ isLoaded(fontName: string): boolean {
+ return this.loadedFonts.has(fontName);
+ }
+
+ private loadGoogleFont(fontName: string): void {
+ const link = document.createElement('link');
+ link.href = `https://fonts.googleapis.com/css2?family=${fontName.replace(' ', '+')}&display=swap`;
+ link.rel = 'stylesheet';
+ document.head.appendChild(link);
+ }
+
+ onPresetChange(event: Event): void {
+ const target = event.target as HTMLSelectElement;
+ this.selectedPreset = target.value;
+
+ if (this.selectedPreset) {
+ const preset = this.fontPresets.find(p => p.key === this.selectedPreset);
+ if (preset) {
+ this.logEvent(`Selected preset: ${preset.name}`, 'info');
+ }
+ }
+ }
+
+ applyPreset(presetKey: string): void {
+ const preset = this.fontPresets.find(p => p.key === presetKey);
+ if (!preset) {
+ this.logEvent(`Preset not found: ${presetKey}`, 'error');
+ return;
+ }
+
+ this.selectedPreset = presetKey;
+ this.logEvent(`Applying preset: ${preset.name}`, 'info');
+
+ // Apply fonts to document root for global effect
+ this.applyFontsGlobally(preset.fonts);
+ this.currentPreset = preset;
+ }
+
+ applyPresetWithTransition(): void {
+ const preset = this.fontPresets.find(p => p.key === this.selectedPreset);
+ if (!preset) return;
+
+ this.logEvent(`Applying preset "${preset.name}" with ${this.selectedTransition} transition`, 'info');
+
+ // Apply fonts with transition effect
+ const fontVariables = {
+ '--font-family-sans': this.buildGlobalFontStack(preset.fonts.sans),
+ '--font-family-serif': this.buildGlobalFontStack(preset.fonts.serif),
+ '--font-family-mono': this.buildGlobalFontStack(preset.fonts.mono),
+ '--font-family-display': this.buildGlobalFontStack(preset.fonts.display)
+ };
+
+ this.applyFontsWithTransition(fontVariables, this.selectedTransition);
+ this.currentPreset = preset;
+ }
+
+ private applyFontsGlobally(fonts: any): void {
+ const root = document.documentElement;
+
+ // Set CSS custom properties on the document root
+ root.style.setProperty('--font-family-sans', this.buildGlobalFontStack(fonts.sans));
+ root.style.setProperty('--font-family-serif', this.buildGlobalFontStack(fonts.serif));
+ root.style.setProperty('--font-family-mono', this.buildGlobalFontStack(fonts.mono));
+ root.style.setProperty('--font-family-display', this.buildGlobalFontStack(fonts.display));
+
+ // Also load the fonts if they're Google Fonts
+ if (fonts.sans && fonts.sans !== 'system-ui') {
+ this.loadSingleFont(fonts.sans);
+ }
+ if (fonts.serif && fonts.serif !== 'ui-serif') {
+ this.loadSingleFont(fonts.serif);
+ }
+ if (fonts.mono && fonts.mono !== 'ui-monospace') {
+ this.loadSingleFont(fonts.mono);
+ }
+ if (fonts.display && fonts.display !== 'system-ui') {
+ this.loadSingleFont(fonts.display);
+ }
+
+ this.logEvent(`Applied fonts globally to document root`, 'success');
+ }
+
+ private applyFontsWithTransition(fontVariables: Record, transitionType: 'fade' | 'scale' | 'typewriter'): void {
+ const root = document.documentElement;
+
+ // Add transition class
+ root.classList.add(`font-transition-${transitionType}`);
+
+ // Apply font changes
+ Object.entries(fontVariables).forEach(([property, value]) => {
+ root.style.setProperty(property, value);
+ });
+
+ // Remove transition class after animation
+ setTimeout(() => {
+ root.classList.remove(`font-transition-${transitionType}`);
+ this.logEvent(`${transitionType} transition completed`, 'success');
+ }, 1000);
+ }
+
+ private buildGlobalFontStack(fontName: string): string {
+ // Handle system fonts
+ if (fontName === 'system-ui') {
+ return 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
+ }
+ if (fontName === 'ui-serif') {
+ return 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif';
+ }
+ if (fontName === 'ui-monospace') {
+ return 'ui-monospace, SFMono-Regular, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
+ }
+
+ // For Google Fonts, add appropriate fallbacks
+ const fontMap: Record = {
+ 'Inter': `"Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`,
+ 'Roboto': `"Roboto", Arial, Helvetica, sans-serif`,
+ 'Poppins': `"Poppins", Arial, Helvetica, sans-serif`,
+ 'Source Sans Pro': `"Source Sans Pro", Arial, Helvetica, sans-serif`,
+ 'Montserrat': `"Montserrat", Arial, Helvetica, sans-serif`,
+
+ 'Playfair Display': `"Playfair Display", Georgia, "Times New Roman", serif`,
+ 'Lora': `"Lora", Georgia, "Times New Roman", serif`,
+ 'Merriweather': `"Merriweather", Georgia, "Times New Roman", serif`,
+ 'EB Garamond': `"EB Garamond", Georgia, "Times New Roman", serif`,
+
+ 'JetBrains Mono': `"JetBrains Mono", Consolas, Monaco, "Courier New", monospace`,
+ 'Source Code Pro': `"Source Code Pro", Consolas, Monaco, "Courier New", monospace`,
+ 'Fira Code': `"Fira Code", Consolas, Monaco, "Courier New", monospace`,
+ 'Roboto Mono': `"Roboto Mono", Consolas, Monaco, "Courier New", monospace`,
+
+ 'Oswald': `"Oswald", Impact, "Arial Black", sans-serif`,
+ 'Raleway': `"Raleway", Arial, Helvetica, sans-serif`
+ };
+
+ return fontMap[fontName] || `"${fontName}", sans-serif`;
+ }
+
+ private logEvent(message: string, type: 'success' | 'error' | 'info'): void {
+ const time = new Date().toLocaleTimeString();
+ this.eventLog.push({ time, message, type });
+
+ // Keep only last 50 events
+ if (this.eventLog.length > 50) {
+ this.eventLog = this.eventLog.slice(-50);
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/font-manager-demo/index.ts b/projects/demo-ui-essentials/src/app/demos/font-manager-demo/index.ts
new file mode 100644
index 0000000..35020af
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/font-manager-demo/index.ts
@@ -0,0 +1 @@
+export * from './font-manager-demo.component';
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/gallery-grid-demo/gallery-grid-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/gallery-grid-demo/gallery-grid-demo.component.scss
new file mode 100644
index 0000000..681d196
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/gallery-grid-demo/gallery-grid-demo.component.scss
@@ -0,0 +1,274 @@
+@use '../../../../../ui-design-system/src/styles/semantic' as *;
+
+.demo-container {
+ padding: $semantic-spacing-component-lg;
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+.demo-section {
+ margin-bottom: $semantic-spacing-layout-section-xl;
+
+ h3 {
+ 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-component-md;
+ border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ padding-bottom: $semantic-spacing-component-sm;
+ }
+
+ h4 {
+ 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-secondary;
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+
+ 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-bottom: $semantic-spacing-component-md;
+ }
+}
+
+.demo-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: $semantic-spacing-grid-gap-lg;
+ margin-bottom: $semantic-spacing-layout-section-md;
+}
+
+.demo-item {
+ padding: $semantic-spacing-component-md;
+ background: $semantic-color-surface-secondary;
+ border-radius: $semantic-border-radius-md;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+}
+
+.demo-controls {
+ display: flex;
+ flex-wrap: wrap;
+ gap: $semantic-spacing-component-sm;
+ margin-bottom: $semantic-spacing-component-lg;
+ padding: $semantic-spacing-component-md;
+ background: $semantic-color-surface-elevated;
+ border-radius: $semantic-border-radius-md;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+
+ button {
+ padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-sm;
+ background: $semantic-color-surface-primary;
+ color: $semantic-color-text-primary;
+ cursor: pointer;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
+
+ 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);
+
+ &:hover {
+ background: $semantic-color-surface-elevated;
+ box-shadow: $semantic-shadow-button-hover;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ }
+
+ &.active {
+ background: $semantic-color-primary;
+ color: $semantic-color-on-primary;
+ border-color: $semantic-color-primary;
+ }
+
+ &:disabled {
+ opacity: $semantic-opacity-disabled;
+ cursor: not-allowed;
+ pointer-events: none;
+ }
+ }
+}
+
+.selection-info {
+ margin-top: $semantic-spacing-component-lg;
+ padding: $semantic-spacing-component-md;
+ background: $semantic-color-surface-elevated;
+ border-radius: $semantic-border-radius-md;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+
+ p {
+ font-weight: $semantic-typography-font-weight-semibold;
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+
+ ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+
+ li {
+ padding: $semantic-spacing-content-line-tight 0;
+ border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ 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);
+
+ &:last-child {
+ border-bottom: none;
+ }
+ }
+ }
+}
+
+.event-log {
+ background: $semantic-color-surface-elevated;
+ border-radius: $semantic-border-radius-md;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ overflow: hidden;
+
+ .event-controls {
+ padding: $semantic-spacing-component-md;
+ border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ background: $semantic-color-surface-secondary;
+
+ button {
+ padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-sm;
+ background: $semantic-color-surface-primary;
+ color: $semantic-color-text-primary;
+ cursor: pointer;
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
+
+ font-family: map-get($semantic-typography-button-small, font-family);
+ font-size: map-get($semantic-typography-button-small, font-size);
+ font-weight: map-get($semantic-typography-button-small, font-weight);
+ line-height: map-get($semantic-typography-button-small, line-height);
+
+ &:hover {
+ background: $semantic-color-surface-elevated;
+ box-shadow: $semantic-shadow-button-hover;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ }
+ }
+ }
+
+ .event-list {
+ max-height: 300px;
+ overflow-y: auto;
+
+ .no-events {
+ padding: $semantic-spacing-component-lg;
+ text-align: center;
+ color: $semantic-color-text-tertiary;
+ font-style: italic;
+ }
+
+ .event-item {
+ display: grid;
+ grid-template-columns: 120px 1fr auto;
+ gap: $semantic-spacing-component-sm;
+ padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
+ border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ .event-type {
+ font-weight: $semantic-typography-font-weight-semibold;
+ color: $semantic-color-primary;
+ font-family: map-get($semantic-typography-body-small, font-family);
+ font-size: map-get($semantic-typography-body-small, font-size);
+ line-height: map-get($semantic-typography-body-small, line-height);
+ }
+
+ .event-details {
+ color: $semantic-color-text-primary;
+ 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);
+ }
+
+ .event-time {
+ color: $semantic-color-text-tertiary;
+ 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);
+ white-space: nowrap;
+ }
+ }
+ }
+}
+
+// Responsive Design
+@media (max-width: $semantic-breakpoint-md - 1) {
+ .demo-container {
+ padding: $semantic-spacing-component-md;
+ }
+
+ .demo-grid {
+ grid-template-columns: 1fr;
+ gap: $semantic-spacing-grid-gap-md;
+ }
+
+ .demo-controls {
+ flex-direction: column;
+ align-items: stretch;
+
+ button {
+ width: 100%;
+ text-align: center;
+ }
+ }
+
+ .event-item {
+ grid-template-columns: 1fr;
+ gap: $semantic-spacing-content-line-tight;
+
+ .event-time {
+ justify-self: end;
+ grid-row: 1;
+ grid-column: 1;
+ }
+
+ .event-type {
+ grid-row: 2;
+ grid-column: 1;
+ }
+
+ .event-details {
+ grid-row: 3;
+ grid-column: 1;
+ }
+ }
+}
+
+@media (max-width: $semantic-breakpoint-sm - 1) {
+ .demo-container {
+ padding: $semantic-spacing-component-sm;
+ }
+
+ .demo-section {
+ margin-bottom: $semantic-spacing-layout-section-md;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/gallery-grid-demo/gallery-grid-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/gallery-grid-demo/gallery-grid-demo.component.ts
new file mode 100644
index 0000000..7ea6afe
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/gallery-grid-demo/gallery-grid-demo.component.ts
@@ -0,0 +1,430 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { GalleryGridComponent, GalleryGridItem } from 'ui-essentials';
+
+@Component({
+ selector: 'ui-gallery-grid-demo',
+ standalone: true,
+ imports: [CommonModule, GalleryGridComponent],
+ template: `
+
+
Gallery Grid Demo
+
A responsive media grid with lightbox functionality for organizing and displaying images.
+
+
+
+ Column Variants
+
+ @for (columnCount of columnVariants; track columnCount) {
+
+
{{ columnCount }} Columns
+
+
+
+ }
+
+
+
+
+
+ Gap Sizes
+
+ @for (gap of gapSizes; track gap) {
+
+
{{ gap | titlecase }} Gap
+
+
+
+ }
+
+
+
+
+
+ Object Fit Options
+
+ @for (fit of objectFitOptions; track fit) {
+
+
{{ fit | titlecase }} Fit
+
+
+
+ }
+
+
+
+
+
+ Auto-fit Responsive
+ Automatically adjusts columns based on available space (resize window to see effect):
+
+
+
+
+
+
+ Masonry Layout
+ Items align to top with varying heights:
+
+
+
+
+
+
+ Interactive Features
+
+
+
+ {{ showOverlay ? 'Hide' : 'Show' }} Overlay
+
+
+ {{ showZoomIndicator ? 'Hide' : 'Show' }} Zoom Indicator
+
+
+ {{ selectionEnabled ? 'Disable' : 'Enable' }} Selection
+
+
+ Select All
+
+
+ Deselect All
+
+
+
+
+
+
+ @if (selectedItems.length > 0) {
+
+
Selected Items: {{ selectedItems.length }}
+
+ @for (item of selectedItems; track item.id) {
+ {{ item.title || item.alt || 'Item ' + item.id }}
+ }
+
+
+ }
+
+
+
+
+
+
+
+
+
+
+ Event Log
+
+
+ Clear Log
+
+
+ @if (eventLog.length === 0) {
+
No events yet. Interact with the gallery above to see events.
+ } @else {
+ @for (event of eventLog; track event.id) {
+
+ {{ event.type }}
+ {{ event.details }}
+ {{ event.timestamp | date:'HH:mm:ss' }}
+
+ }
+ }
+
+
+
+
+ `,
+ styleUrl: './gallery-grid-demo.component.scss'
+})
+export class GalleryGridDemoComponent {
+ columnVariants = [2, 3, 4] as const;
+ gapSizes = ['sm', 'md', 'lg'] as const;
+ objectFitOptions = ['cover', 'contain', 'fill'] as const;
+
+ showOverlay = true;
+ showZoomIndicator = true;
+ selectionEnabled = false;
+ selectedItems: GalleryGridItem[] = [];
+ eventLog: Array<{id: number; type: string; details: string; timestamp: Date}> = [];
+
+ sampleItems: GalleryGridItem[] = [
+ {
+ id: 1,
+ src: 'https://picsum.photos/400/300?random=1',
+ thumbnail: 'https://picsum.photos/200/150?random=1',
+ alt: 'Sample image 1',
+ title: 'Mountain Landscape',
+ caption: 'Beautiful mountain vista at sunset'
+ },
+ {
+ id: 2,
+ src: 'https://picsum.photos/400/300?random=2',
+ thumbnail: 'https://picsum.photos/200/150?random=2',
+ alt: 'Sample image 2',
+ title: 'Ocean Waves',
+ caption: 'Crashing waves on a sandy beach'
+ },
+ {
+ id: 3,
+ src: 'https://picsum.photos/400/300?random=3',
+ thumbnail: 'https://picsum.photos/200/150?random=3',
+ alt: 'Sample image 3',
+ title: 'Forest Path',
+ caption: 'Winding trail through dense woods'
+ },
+ {
+ id: 4,
+ src: 'https://picsum.photos/400/300?random=4',
+ thumbnail: 'https://picsum.photos/200/150?random=4',
+ alt: 'Sample image 4',
+ title: 'City Skyline',
+ caption: 'Urban architecture at night'
+ },
+ {
+ id: 5,
+ src: 'https://picsum.photos/400/300?random=5',
+ thumbnail: 'https://picsum.photos/200/150?random=5',
+ alt: 'Sample image 5',
+ title: 'Desert Dunes',
+ caption: 'Rolling sand dunes in morning light'
+ },
+ {
+ id: 6,
+ src: 'https://picsum.photos/400/300?random=6',
+ thumbnail: 'https://picsum.photos/200/150?random=6',
+ alt: 'Sample image 6',
+ title: 'Lake Reflection',
+ caption: 'Mirror-like water surface'
+ },
+ {
+ id: 7,
+ src: 'https://picsum.photos/400/300?random=7',
+ thumbnail: 'https://picsum.photos/200/150?random=7',
+ alt: 'Sample image 7',
+ title: 'Flower Field',
+ caption: 'Colorful wildflowers in bloom'
+ },
+ {
+ id: 8,
+ src: 'https://picsum.photos/400/300?random=8',
+ thumbnail: 'https://picsum.photos/200/150?random=8',
+ alt: 'Sample image 8',
+ title: 'Snow Peaks',
+ caption: 'Snow-capped mountain range'
+ }
+ ];
+
+ landscapeItems: GalleryGridItem[] = [
+ {
+ id: 'l1',
+ src: 'https://picsum.photos/800/400?random=11',
+ thumbnail: 'https://picsum.photos/200/100?random=11',
+ alt: 'Landscape image 1',
+ title: 'Wide Landscape',
+ caption: 'Panoramic view'
+ },
+ {
+ id: 'l2',
+ src: 'https://picsum.photos/800/400?random=12',
+ thumbnail: 'https://picsum.photos/200/100?random=12',
+ alt: 'Landscape image 2',
+ title: 'River Valley',
+ caption: 'Flowing water through hills'
+ },
+ {
+ id: 'l3',
+ src: 'https://picsum.photos/800/400?random=13',
+ thumbnail: 'https://picsum.photos/200/100?random=13',
+ alt: 'Landscape image 3',
+ title: 'Coastal View',
+ caption: 'Rocky coastline'
+ }
+ ];
+
+ masonryItems: GalleryGridItem[] = [
+ {
+ id: 'm1',
+ src: 'https://picsum.photos/300/400?random=21',
+ thumbnail: 'https://picsum.photos/150/200?random=21',
+ alt: 'Tall image 1',
+ title: 'Portrait 1'
+ },
+ {
+ id: 'm2',
+ src: 'https://picsum.photos/300/200?random=22',
+ thumbnail: 'https://picsum.photos/150/100?random=22',
+ alt: 'Wide image 1',
+ title: 'Landscape 1'
+ },
+ {
+ id: 'm3',
+ src: 'https://picsum.photos/300/500?random=23',
+ thumbnail: 'https://picsum.photos/150/250?random=23',
+ alt: 'Very tall image',
+ title: 'Portrait 2'
+ },
+ {
+ id: 'm4',
+ src: 'https://picsum.photos/300/300?random=24',
+ thumbnail: 'https://picsum.photos/150/150?random=24',
+ alt: 'Square image',
+ title: 'Square 1'
+ },
+ {
+ id: 'm5',
+ src: 'https://picsum.photos/300/350?random=25',
+ thumbnail: 'https://picsum.photos/150/175?random=25',
+ alt: 'Tall image 2',
+ title: 'Portrait 3'
+ },
+ {
+ id: 'm6',
+ src: 'https://picsum.photos/300/250?random=26',
+ thumbnail: 'https://picsum.photos/150/125?random=26',
+ alt: 'Medium image',
+ title: 'Landscape 2'
+ }
+ ];
+
+ interactiveItems: GalleryGridItem[] = this.sampleItems.slice(0, 8).map(item => ({
+ ...item,
+ selected: false
+ }));
+
+ loadingItems: GalleryGridItem[] = [
+ {
+ id: 'loading1',
+ src: 'https://picsum.photos/400/300?random=31',
+ thumbnail: 'https://picsum.photos/200/150?random=31',
+ alt: 'Loading image 1',
+ title: 'Loading Item 1',
+ loading: true
+ },
+ {
+ id: 'loading2',
+ src: 'https://picsum.photos/400/300?random=32',
+ thumbnail: 'https://picsum.photos/200/150?random=32',
+ alt: 'Loading image 2',
+ title: 'Loading Item 2',
+ loading: true
+ },
+ {
+ id: 'loading3',
+ src: 'https://picsum.photos/400/300?random=33',
+ thumbnail: 'https://picsum.photos/200/150?random=33',
+ alt: 'Loading image 3',
+ title: 'Loaded Item',
+ loading: false
+ }
+ ];
+
+ private eventCounter = 0;
+
+ toggleOverlay(): void {
+ this.showOverlay = !this.showOverlay;
+ }
+
+ toggleZoomIndicator(): void {
+ this.showZoomIndicator = !this.showZoomIndicator;
+ }
+
+ toggleSelection(): void {
+ this.selectionEnabled = !this.selectionEnabled;
+ if (!this.selectionEnabled) {
+ this.deselectAll();
+ }
+ }
+
+ selectAll(): void {
+ this.interactiveItems.forEach(item => item.selected = true);
+ this.updateSelectedItems();
+ }
+
+ deselectAll(): void {
+ this.interactiveItems.forEach(item => item.selected = false);
+ this.updateSelectedItems();
+ }
+
+ handleItemClick(event: { item: GalleryGridItem; event: MouseEvent }): void {
+ this.addEvent('Item Click', `Clicked item: ${event.item.title || event.item.id}`);
+
+ if (this.selectionEnabled) {
+ event.item.selected = !event.item.selected;
+ this.updateSelectedItems();
+ this.addEvent('Item Selection', `${event.item.selected ? 'Selected' : 'Deselected'} item: ${event.item.title || event.item.id}`);
+ }
+ }
+
+ handleItemSelect(item: GalleryGridItem): void {
+ this.updateSelectedItems();
+ this.addEvent('Item Select', `Selected item: ${item.title || item.id}`);
+ }
+
+ handleImageLoad(event: { item: GalleryGridItem; event: Event }): void {
+ this.addEvent('Image Load', `Loaded image: ${event.item.title || event.item.id}`);
+ }
+
+ handleImageError(event: { item: GalleryGridItem; event: Event }): void {
+ this.addEvent('Image Error', `Failed to load image: ${event.item.title || event.item.id}`);
+ }
+
+ clearEventLog(): void {
+ this.eventLog = [];
+ }
+
+ private updateSelectedItems(): void {
+ this.selectedItems = this.interactiveItems.filter(item => item.selected);
+ }
+
+ private addEvent(type: string, details: string): void {
+ this.eventLog.unshift({
+ id: ++this.eventCounter,
+ type,
+ details,
+ timestamp: new Date()
+ });
+
+ // Keep only the last 20 events
+ if (this.eventLog.length > 20) {
+ this.eventLog = this.eventLog.slice(0, 20);
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/hcl-studio-demo/hcl-studio-demo.component.html b/projects/demo-ui-essentials/src/app/demos/hcl-studio-demo/hcl-studio-demo.component.html
new file mode 100644
index 0000000..1850057
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/hcl-studio-demo/hcl-studio-demo.component.html
@@ -0,0 +1 @@
+hcl-studio-demo works!
diff --git a/projects/demo-ui-essentials/src/app/demos/hcl-studio-demo/hcl-studio-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/hcl-studio-demo/hcl-studio-demo.component.scss
new file mode 100644
index 0000000..d9384dd
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/hcl-studio-demo/hcl-studio-demo.component.scss
@@ -0,0 +1,478 @@
+/**
+ * HCL Studio Demo Styles
+ * Showcases dynamic theming using CSS custom properties
+ */
+
+.hcl-studio-demo {
+ padding: 2rem;
+ background: var(--color-surface);
+ color: var(--color-on-surface);
+ min-height: 100vh;
+ transition: background-color 0.3s ease, color 0.3s ease;
+}
+
+// ==========================================================================
+// HEADER STYLES
+// ==========================================================================
+
+.demo-header {
+ text-align: center;
+ margin-bottom: 3rem;
+ padding: 2rem;
+ background: var(--color-surface-container);
+ border-radius: 1rem;
+
+ h1 {
+ margin: 0 0 0.5rem 0;
+ color: var(--color-primary);
+ font-size: 2.5rem;
+ font-weight: 600;
+ }
+
+ p {
+ margin: 0;
+ color: var(--color-on-surface-variant);
+ font-size: 1.2rem;
+ }
+}
+
+// ==========================================================================
+// CURRENT THEME INFO
+// ==========================================================================
+
+.current-theme-info {
+ margin-bottom: 2rem;
+ padding: 1.5rem;
+ background: var(--color-surface-high);
+ border-radius: 0.5rem;
+ border: 1px solid var(--color-outline-variant);
+
+ h3 {
+ margin: 0 0 1rem 0;
+ color: var(--color-on-surface);
+ }
+
+ .color-swatches {
+ display: flex;
+ gap: 1rem;
+
+ .swatch {
+ flex: 1;
+ padding: 1rem;
+ border-radius: 0.5rem;
+ text-align: center;
+ color: white;
+ font-weight: 600;
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
+
+ span {
+ font-size: 0.9rem;
+ }
+ }
+ }
+}
+
+// ==========================================================================
+// MODE CONTROLS
+// ==========================================================================
+
+.mode-controls {
+ margin-bottom: 2rem;
+ text-align: center;
+
+ .mode-toggle-btn {
+ padding: 0.75rem 2rem;
+ border: 2px solid var(--color-outline);
+ border-radius: 2rem;
+ background: var(--color-surface);
+ color: var(--color-on-surface);
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+
+ &:hover {
+ background: var(--color-surface-high);
+ border-color: var(--color-primary);
+ }
+
+ &.dark {
+ background: var(--color-primary);
+ color: var(--color-on-primary);
+ border-color: var(--color-primary);
+ }
+ }
+}
+
+// ==========================================================================
+// THEMES SECTION
+// ==========================================================================
+
+.themes-section {
+ margin-bottom: 3rem;
+
+ h3 {
+ margin: 0 0 1.5rem 0;
+ color: var(--color-on-surface);
+ font-size: 1.5rem;
+ }
+
+ .theme-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+ gap: 1rem;
+
+ .theme-card {
+ padding: 1rem;
+ background: var(--color-surface-container);
+ border: 2px solid var(--color-outline-variant);
+ border-radius: 0.75rem;
+ cursor: pointer;
+ transition: all 0.3s ease;
+
+ &:hover {
+ border-color: var(--color-primary);
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ }
+
+ &.active {
+ border-color: var(--color-primary);
+ background: var(--color-primary-container);
+
+ .theme-name {
+ color: var(--color-on-primary-container);
+ font-weight: 600;
+ }
+ }
+
+ .theme-preview {
+ display: flex;
+ height: 40px;
+ border-radius: 0.5rem;
+ overflow: hidden;
+ margin-bottom: 0.75rem;
+
+ .color-strip {
+ flex: 1;
+ }
+ }
+
+ .theme-name {
+ display: block;
+ text-align: center;
+ color: var(--color-on-surface);
+ font-size: 0.9rem;
+ font-weight: 500;
+ }
+ }
+ }
+}
+
+// ==========================================================================
+// CUSTOM THEME SECTION
+// ==========================================================================
+
+.custom-theme-section {
+ margin-bottom: 3rem;
+ padding: 2rem;
+ background: var(--color-surface-high);
+ border-radius: 1rem;
+ border: 1px solid var(--color-outline-variant);
+
+ h3 {
+ margin: 0 0 1.5rem 0;
+ color: var(--color-on-surface);
+ font-size: 1.5rem;
+ }
+
+ .custom-inputs {
+ display: grid;
+ gap: 1.5rem;
+ margin-bottom: 2rem;
+
+ @media (min-width: 768px) {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ .color-input-group {
+ label {
+ display: block;
+ margin-bottom: 0.5rem;
+ color: var(--color-on-surface);
+ font-weight: 600;
+ font-size: 0.9rem;
+ }
+
+ input[type="color"] {
+ width: 50px;
+ height: 40px;
+ border: 2px solid var(--color-outline);
+ border-radius: 0.5rem;
+ margin-right: 0.5rem;
+ cursor: pointer;
+ }
+
+ input[type="text"] {
+ flex: 1;
+ padding: 0.75rem;
+ border: 2px solid var(--color-outline);
+ border-radius: 0.5rem;
+ background: var(--color-surface);
+ color: var(--color-on-surface);
+ font-size: 1rem;
+
+ &:focus {
+ outline: none;
+ border-color: var(--color-primary);
+ }
+ }
+ }
+ }
+
+ .custom-actions {
+ display: flex;
+ gap: 1rem;
+ margin-bottom: 2rem;
+
+ button {
+ padding: 0.75rem 1.5rem;
+ border-radius: 0.5rem;
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+
+ &.preview-btn {
+ background: var(--color-secondary);
+ color: var(--color-on-secondary);
+ border: none;
+
+ &:hover {
+ opacity: 0.9;
+ transform: translateY(-1px);
+ }
+ }
+
+ &.apply-btn {
+ background: var(--color-primary);
+ color: var(--color-on-primary);
+ border: none;
+
+ &:hover {
+ opacity: 0.9;
+ transform: translateY(-1px);
+ }
+ }
+ }
+ }
+
+ .color-preview {
+ h4 {
+ margin: 0 0 1rem 0;
+ color: var(--color-on-surface);
+ }
+
+ .preview-swatches {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
+ gap: 0.5rem;
+
+ .preview-swatch {
+ padding: 0.75rem 0.5rem;
+ border-radius: 0.5rem;
+ text-align: center;
+ font-size: 0.8rem;
+ font-weight: 600;
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
+ }
+ }
+ }
+}
+
+// ==========================================================================
+// DEMO COMPONENTS
+// ==========================================================================
+
+.demo-components {
+ h3 {
+ margin: 0 0 1.5rem 0;
+ color: var(--color-on-surface);
+ font-size: 1.5rem;
+ }
+
+ .component-showcase {
+ display: grid;
+ gap: 2rem;
+
+ @media (min-width: 768px) {
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ }
+
+ .showcase-section {
+ padding: 1.5rem;
+ background: var(--color-surface-container);
+ border-radius: 0.75rem;
+ border: 1px solid var(--color-outline-variant);
+
+ h4 {
+ margin: 0 0 1rem 0;
+ color: var(--color-on-surface);
+ font-size: 1.2rem;
+ }
+
+ // Button Styles
+ .button-group {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+
+ button {
+ padding: 0.75rem 1.5rem;
+ border-radius: 0.5rem;
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ border: none;
+ transition: all 0.3s ease;
+
+ &.btn-primary {
+ background: var(--color-primary);
+ color: var(--color-on-primary);
+
+ &:hover {
+ opacity: 0.9;
+ transform: translateY(-1px);
+ }
+ }
+
+ &.btn-secondary {
+ background: var(--color-secondary);
+ color: var(--color-on-secondary);
+
+ &:hover {
+ opacity: 0.9;
+ transform: translateY(-1px);
+ }
+ }
+
+ &.btn-tertiary {
+ background: var(--color-tertiary);
+ color: var(--color-on-tertiary);
+
+ &:hover {
+ opacity: 0.9;
+ transform: translateY(-1px);
+ }
+ }
+ }
+ }
+
+ // Card Styles
+ .card-group {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+
+ .demo-card {
+ padding: 1.5rem;
+ border-radius: 0.5rem;
+
+ &.primary {
+ background: var(--color-primary-container);
+ color: var(--color-on-primary-container);
+ }
+
+ &.secondary {
+ background: var(--color-secondary-container);
+ color: var(--color-on-secondary-container);
+ }
+
+ h5 {
+ margin: 0 0 0.5rem 0;
+ font-size: 1.1rem;
+ }
+
+ p {
+ margin: 0;
+ opacity: 0.8;
+ }
+ }
+ }
+
+ // Surface Styles
+ .surface-group {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+
+ .demo-surface {
+ padding: 1rem;
+ border-radius: 0.5rem;
+ text-align: center;
+ font-weight: 600;
+
+ &.surface-low {
+ background: var(--color-surface-low);
+ color: var(--color-on-surface);
+ }
+
+ &.surface-container {
+ background: var(--color-surface-container);
+ color: var(--color-on-surface);
+ }
+
+ &.surface-high {
+ background: var(--color-surface-high);
+ color: var(--color-on-surface);
+ }
+ }
+ }
+ }
+ }
+}
+
+// ==========================================================================
+// RESPONSIVE ADJUSTMENTS
+// ==========================================================================
+
+@media (max-width: 768px) {
+ .hcl-studio-demo {
+ padding: 1rem;
+ }
+
+ .demo-header {
+ padding: 1.5rem;
+
+ h1 {
+ font-size: 2rem;
+ }
+
+ p {
+ font-size: 1rem;
+ }
+ }
+
+ .theme-grid {
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+ }
+
+ .custom-inputs {
+ grid-template-columns: 1fr !important;
+ }
+
+ .custom-actions {
+ flex-direction: column;
+ }
+}
+
+// ==========================================================================
+// TRANSITIONS FOR SMOOTH THEME CHANGES
+// ==========================================================================
+
+* {
+ transition: background-color 0.3s ease,
+ color 0.3s ease,
+ border-color 0.3s ease,
+ opacity 0.3s ease;
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/hcl-studio-demo/hcl-studio-demo.component.spec.ts b/projects/demo-ui-essentials/src/app/demos/hcl-studio-demo/hcl-studio-demo.component.spec.ts
new file mode 100644
index 0000000..2746919
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/hcl-studio-demo/hcl-studio-demo.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { HclStudioDemoComponent } from './hcl-studio-demo.component';
+
+describe('HclStudioDemoComponent', () => {
+ let component: HclStudioDemoComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [HclStudioDemoComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(HclStudioDemoComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/projects/demo-ui-essentials/src/app/demos/hcl-studio-demo/hcl-studio-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/hcl-studio-demo/hcl-studio-demo.component.ts
new file mode 100644
index 0000000..7b8811b
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/hcl-studio-demo/hcl-studio-demo.component.ts
@@ -0,0 +1,288 @@
+import { Component, OnInit, OnDestroy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+
+import {
+ HCLStudioService,
+ DEFAULT_THEMES,
+ ThemePreview,
+ BrandColors,
+ HCLConverter,
+ PaletteGenerator
+} from 'hcl-studio';
+
+@Component({
+ selector: 'app-hcl-studio-demo',
+ standalone: true,
+ imports: [CommonModule, FormsModule],
+ template: `
+
+
+
+
+
+
Current Theme: {{ currentTheme.name }}
+
+
+ Primary
+
+
+ Secondary
+
+
+ Tertiary
+
+
+
+
+
+
+
+ {{ isDarkMode ? 'Switch to Light' : 'Switch to Dark' }}
+
+
+
+
+
+
+
+
+
Create Custom Theme
+
+
+
+
+ Preview Theme
+
+
+ Apply Theme
+
+
+
+
+
+
Generated Palette Preview
+
+
+
+
+
+
+
Theme Demo Components
+
+
+
+
+
Buttons
+
+ Primary Button
+ Secondary Button
+ Tertiary Button
+
+
+
+
+
+
Cards
+
+
+
Primary Card
+
This card uses primary colors
+
+
+
Secondary Card
+
This card uses secondary colors
+
+
+
+
+
+
+
Surface Variations
+
+
Surface Low
+
Surface Container
+
Surface High
+
+
+
+
+
+ `,
+ styleUrls: ['./hcl-studio-demo.component.scss']
+})
+export class HclStudioDemoComponent implements OnInit, OnDestroy {
+ private destroy$ = new Subject();
+
+ // State
+ availableThemes: ThemePreview[] = [];
+ currentTheme: ThemePreview | null = null;
+ isDarkMode = false;
+
+ // Custom theme inputs
+ customColors: BrandColors = {
+ primary: '#6750A4',
+ secondary: '#625B71',
+ tertiary: '#7D5260'
+ };
+
+ colorPreview: { [key: string]: string } | null = null;
+
+ constructor(private hclStudio: HCLStudioService) {}
+
+ ngOnInit(): void {
+ // Initialize HCL Studio with default themes
+ this.hclStudio.initialize({
+ themes: DEFAULT_THEMES,
+ defaultTheme: 'material-purple',
+ autoMode: false
+ });
+
+ // Subscribe to theme changes
+ this.hclStudio.themeState$
+ .pipe(takeUntil(this.destroy$))
+ .subscribe(state => {
+ this.availableThemes = state.availableThemes;
+ if (state.currentTheme) {
+ this.currentTheme = {
+ id: state.currentTheme.config.id,
+ name: state.currentTheme.config.name,
+ description: state.currentTheme.config.description,
+ primary: state.currentTheme.config.colors.primary,
+ secondary: state.currentTheme.config.colors.secondary,
+ tertiary: state.currentTheme.config.colors.tertiary
+ };
+ }
+ });
+
+ // Subscribe to mode changes
+ this.hclStudio.currentMode$
+ .pipe(takeUntil(this.destroy$))
+ .subscribe(mode => {
+ this.isDarkMode = mode === 'dark';
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.destroy$.next();
+ this.destroy$.complete();
+ }
+
+ switchTheme(themeId: string): void {
+ this.hclStudio.switchTheme(themeId);
+ }
+
+ toggleMode(): void {
+ this.hclStudio.toggleMode();
+ }
+
+ previewCustomTheme(): void {
+ try {
+ this.colorPreview = this.hclStudio.generateColorPreview(this.customColors);
+ } catch (error) {
+ console.warn('Failed to generate color preview:', error);
+ this.colorPreview = null;
+ }
+ }
+
+ applyCustomTheme(): void {
+ const customTheme = this.hclStudio.createCustomTheme(
+ 'custom-theme',
+ 'My Custom Theme',
+ this.customColors,
+ 'User-created custom theme'
+ );
+
+ this.hclStudio.switchTheme('custom-theme');
+ }
+
+ getContrastColor(backgroundColor: string): string {
+ try {
+ const ratio1 = HCLConverter.getContrastRatio(backgroundColor, '#000000');
+ const ratio2 = HCLConverter.getContrastRatio(backgroundColor, '#FFFFFF');
+ return ratio1 > ratio2 ? '#000000' : '#FFFFFF';
+ } catch {
+ return '#000000';
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/infinite-scroll-container-demo/infinite-scroll-container-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/infinite-scroll-container-demo/infinite-scroll-container-demo.component.scss
new file mode 100644
index 0000000..888b1a1
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/infinite-scroll-container-demo/infinite-scroll-container-demo.component.scss
@@ -0,0 +1,494 @@
+@use '../../../../../ui-design-system/src/styles/semantic' as *;
+
+.demo-container {
+ padding: $semantic-spacing-component-lg;
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+.demo-section {
+ margin-bottom: $semantic-spacing-layout-section-md;
+
+ h2 {
+ 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;
+ }
+
+ h3 {
+ 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-component-md;
+ }
+}
+
+.demo-showcase {
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-md;
+ background: $semantic-color-surface-secondary;
+ padding: $semantic-spacing-component-md;
+ height: 400px;
+ position: relative;
+}
+
+.demo-scroll-container {
+ width: 100%;
+ height: 100%;
+ border-radius: $semantic-border-radius-sm;
+ background: $semantic-color-surface-primary;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+}
+
+// Basic demo items
+.demo-item {
+ padding: $semantic-spacing-component-md;
+ margin-bottom: $semantic-spacing-component-sm;
+ background: $semantic-color-surface-elevated;
+ border-radius: $semantic-border-radius-sm;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+
+ h4 {
+ 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-content-line-normal 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 0 $semantic-spacing-component-xs 0;
+ }
+
+ small {
+ font-family: map-get($semantic-typography-caption, font-family);
+ font-size: map-get($semantic-typography-caption, font-size);
+ font-weight: map-get($semantic-typography-caption, font-weight);
+ line-height: map-get($semantic-typography-caption, line-height);
+ color: $semantic-color-text-tertiary;
+ }
+}
+
+// Template demo items
+.demo-template-item {
+ padding: $semantic-spacing-component-lg;
+ background: $semantic-color-surface-elevated;
+ border-radius: $semantic-border-radius-md;
+ 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-elevation-2;
+ transform: translateY(-2px);
+ }
+
+ &--last {
+ border-color: $semantic-color-primary;
+ box-shadow: $semantic-shadow-elevation-1;
+ }
+
+ &__header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+
+ &__id {
+ font-family: map-get($semantic-typography-body-small, font-family);
+ font-size: map-get($semantic-typography-body-small, font-size);
+ font-weight: $semantic-typography-font-weight-semibold;
+ line-height: map-get($semantic-typography-body-small, line-height);
+ color: $semantic-color-primary;
+ }
+
+ &__category {
+ font-family: map-get($semantic-typography-caption, font-family);
+ font-size: map-get($semantic-typography-caption, font-size);
+ font-weight: map-get($semantic-typography-caption, font-weight);
+ line-height: map-get($semantic-typography-caption, line-height);
+ background: $semantic-color-surface-container;
+ color: $semantic-color-text-secondary;
+ padding: $semantic-spacing-1 $semantic-spacing-2;
+ border-radius: $semantic-border-radius-sm;
+ }
+
+ &__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-xs 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 0 $semantic-spacing-component-sm 0;
+ }
+
+ &__footer {
+ border-top: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ padding-top: $semantic-spacing-component-xs;
+
+ small {
+ font-family: map-get($semantic-typography-caption, font-family);
+ font-size: map-get($semantic-typography-caption, font-size);
+ font-weight: map-get($semantic-typography-caption, font-weight);
+ line-height: map-get($semantic-typography-caption, line-height);
+ color: $semantic-color-text-tertiary;
+ }
+ }
+}
+
+// Bidirectional demo items
+.demo-bidirectional-item {
+ padding: $semantic-spacing-component-sm;
+
+ &__content {
+ background: $semantic-color-surface-elevated;
+ padding: $semantic-spacing-component-md;
+ border-radius: $semantic-border-radius-lg;
+ border-left: 4px solid $semantic-color-primary;
+
+ strong {
+ font-family: map-get($semantic-typography-body-medium, font-family);
+ font-size: map-get($semantic-typography-body-medium, font-size);
+ font-weight: $semantic-typography-font-weight-semibold;
+ line-height: map-get($semantic-typography-body-medium, line-height);
+ color: $semantic-color-text-primary;
+ display: block;
+ margin-bottom: $semantic-spacing-content-line-tight;
+ }
+
+ p {
+ 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 0 $semantic-spacing-content-line-tight 0;
+ }
+
+ small {
+ font-family: map-get($semantic-typography-caption, font-family);
+ font-size: map-get($semantic-typography-caption, font-size);
+ font-weight: map-get($semantic-typography-caption, font-weight);
+ line-height: map-get($semantic-typography-caption, line-height);
+ color: $semantic-color-text-tertiary;
+ }
+ }
+}
+
+// Custom templates
+.demo-custom-item {
+ display: flex;
+ gap: $semantic-spacing-component-md;
+ padding: $semantic-spacing-component-md;
+ background: $semantic-color-surface-elevated;
+ border-radius: $semantic-border-radius-md;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+
+ &__avatar {
+ flex-shrink: 0;
+ width: $semantic-sizing-touch-target;
+ height: $semantic-sizing-touch-target;
+ background: $semantic-color-primary;
+ color: $semantic-color-on-primary;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-family: map-get($semantic-typography-body-medium, font-family);
+ font-size: map-get($semantic-typography-body-medium, font-size);
+ font-weight: $semantic-typography-font-weight-bold;
+ line-height: map-get($semantic-typography-body-medium, line-height);
+ }
+
+ &__content {
+ flex: 1;
+ min-width: 0;
+
+ h4 {
+ 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-content-line-tight 0;
+ }
+
+ p {
+ 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 0 $semantic-spacing-content-line-tight 0;
+ }
+ }
+
+ &__time {
+ font-family: map-get($semantic-typography-caption, font-family);
+ font-size: map-get($semantic-typography-caption, font-size);
+ font-weight: map-get($semantic-typography-caption, font-weight);
+ line-height: map-get($semantic-typography-caption, line-height);
+ color: $semantic-color-text-tertiary;
+ }
+}
+
+.demo-custom-loading {
+ display: flex;
+ align-items: center;
+ gap: $semantic-spacing-component-md;
+ padding: $semantic-spacing-component-lg;
+ background: linear-gradient(45deg, $semantic-color-primary, $semantic-color-secondary);
+ color: $semantic-color-on-primary;
+ border-radius: $semantic-border-radius-lg;
+
+ &__spinner {
+ width: $semantic-sizing-icon-button;
+ height: $semantic-sizing-icon-button;
+ border: 3px solid rgba(255, 255, 255, 0.3);
+ border-radius: 50%;
+ border-top-color: white;
+ animation: spin $semantic-motion-duration-slow linear infinite;
+ }
+
+ span {
+ font-family: map-get($semantic-typography-body-medium, font-family);
+ font-size: map-get($semantic-typography-body-medium, font-size);
+ font-weight: $semantic-typography-font-weight-medium;
+ line-height: map-get($semantic-typography-body-medium, line-height);
+ }
+}
+
+.demo-custom-error {
+ text-align: center;
+ padding: $semantic-spacing-component-xl;
+ background: rgba($semantic-color-danger, 0.1);
+ border: $semantic-border-width-1 solid $semantic-color-danger;
+ border-radius: $semantic-border-radius-lg;
+
+ &__icon {
+ font-size: $semantic-typography-font-size-2xl;
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+
+ h4 {
+ 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-danger;
+ margin: 0 0 $semantic-spacing-component-xs 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;
+ }
+}
+
+.demo-custom-end {
+ text-align: center;
+ padding: $semantic-spacing-component-xl;
+ background: linear-gradient(135deg, $semantic-color-success, $semantic-color-primary);
+ color: $semantic-color-on-success;
+ border-radius: $semantic-border-radius-lg;
+
+ &__icon {
+ font-size: $semantic-typography-font-size-3xl;
+ margin-bottom: $semantic-spacing-component-sm;
+ }
+
+ h4 {
+ 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);
+ margin: 0 0 $semantic-spacing-component-xs 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);
+ margin: 0;
+ opacity: 0.9;
+ }
+}
+
+// Configuration demo
+.demo-config-item {
+ display: flex;
+ align-items: center;
+ gap: $semantic-spacing-component-md;
+ padding: $semantic-spacing-component-md;
+ background: $semantic-color-surface-elevated;
+ border-radius: $semantic-border-radius-sm;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+
+ &__index {
+ flex-shrink: 0;
+ width: $semantic-sizing-touch-minimum;
+ height: $semantic-sizing-touch-minimum;
+ background: $semantic-color-surface-container;
+ color: $semantic-color-text-secondary;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-family: map-get($semantic-typography-body-small, font-family);
+ font-size: map-get($semantic-typography-body-small, font-size);
+ font-weight: $semantic-typography-font-weight-semibold;
+ line-height: map-get($semantic-typography-body-small, line-height);
+ }
+
+ &__content {
+ flex: 1;
+
+ h4 {
+ font-family: map-get($semantic-typography-body-medium, font-family);
+ font-size: map-get($semantic-typography-body-medium, font-size);
+ font-weight: $semantic-typography-font-weight-semibold;
+ line-height: map-get($semantic-typography-body-medium, line-height);
+ color: $semantic-color-text-primary;
+ margin: 0 0 $semantic-spacing-content-line-tight 0;
+ }
+
+ p {
+ 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;
+ }
+ }
+}
+
+// Controls
+.demo-controls {
+ display: flex;
+ flex-wrap: wrap;
+ gap: $semantic-spacing-component-md;
+ margin-bottom: $semantic-spacing-component-lg;
+ padding: $semantic-spacing-component-md;
+ background: $semantic-color-surface-secondary;
+ border-radius: $semantic-border-radius-md;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+
+ button {
+ padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
+ background: $semantic-color-primary;
+ color: $semantic-color-on-primary;
+ border: none;
+ border-radius: $semantic-border-radius-sm;
+ cursor: pointer;
+
+ 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);
+
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
+
+ &:hover {
+ box-shadow: $semantic-shadow-elevation-2;
+ transform: translateY(-1px);
+ }
+
+ &:active {
+ transform: translateY(0);
+ box-shadow: $semantic-shadow-elevation-1;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ }
+ }
+}
+
+.demo-control {
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-component-xs;
+
+ label {
+ font-family: map-get($semantic-typography-label, font-family);
+ font-size: map-get($semantic-typography-label, font-size);
+ font-weight: map-get($semantic-typography-label, font-weight);
+ line-height: map-get($semantic-typography-label, line-height);
+ color: $semantic-color-text-primary;
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-component-xs;
+ }
+
+ input[type="checkbox"] {
+ width: auto;
+ margin-right: $semantic-spacing-component-xs;
+ }
+
+ input[type="range"] {
+ width: 150px;
+ }
+}
+
+@keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+// Reduced motion support
+@media (prefers-reduced-motion: reduce) {
+ .demo-template-item {
+ transition: none;
+
+ &:hover {
+ transform: none;
+ }
+ }
+
+ .demo-custom-loading__spinner {
+ animation: none;
+ }
+
+ button {
+ transition: none;
+
+ &:hover {
+ transform: none;
+ }
+
+ &:active {
+ transform: none;
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/infinite-scroll-container-demo/infinite-scroll-container-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/infinite-scroll-container-demo/infinite-scroll-container-demo.component.ts
new file mode 100644
index 0000000..80d68a9
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/infinite-scroll-container-demo/infinite-scroll-container-demo.component.ts
@@ -0,0 +1,501 @@
+import { Component, TemplateRef, ViewChild, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { InfiniteScrollContainerComponent, InfiniteScrollEvent, InfiniteScrollConfig } from 'ui-essentials';
+
+interface DemoItem {
+ id: number;
+ title: string;
+ description: string;
+ timestamp: Date;
+ category: string;
+}
+
+@Component({
+ selector: 'ui-infinite-scroll-container-demo',
+ standalone: true,
+ imports: [CommonModule, InfiniteScrollContainerComponent],
+ template: `
+
+
Infinite Scroll Container Demo
+
+
+
+ Basic Infinite Scroll (Content Projection)
+
+
+
+ @for (item of basicItems(); track item.id) {
+
+
{{ item.title }}
+
{{ item.description }}
+
{{ item.timestamp | date:'short' }} - {{ item.category }}
+
+ }
+
+
+
+
+
+
+ Template-Based Rendering
+
+
+
+
+
+
+
+
{{ item.title }}
+
{{ item.description }}
+
+
+
+
+
+
+
+
+ Bidirectional Infinite Scroll
+
+
+
+
+
+
+
+
Message {{ item.id }}
+
{{ item.description }}
+
{{ item.timestamp | date:'short' }}
+
+
+
+
+
+
+
+
+ Custom Loading & Error Templates
+
+
+
+
+
+
+
+ {{ item.title.charAt(0) }}
+
+
+
{{ item.title }}
+
{{ item.description }}
+
{{ getTimeAgo(item.timestamp) }}
+
+
+
+
+
+
+
+
Loading amazing content...
+
+
+
+
+
+
⚠️
+
Oops! Something went wrong
+
{{ error }}
+
+
+
+
+
+
🎉
+
That's all folks!
+
You've reached the end of our amazing content.
+
+
+
+
+
+
+
+ Configuration Options
+
+
+
+
+
+
+
+
+
{{ index + 1 }}
+
+
{{ item.title }}
+
{{ item.description }}
+
+
+
+
+
+
+
+
+ API Methods
+
+ Scroll to Top
+ Scroll to Bottom
+ Scroll to Item 50
+ Reset Scroll
+
+
+
+ `,
+ styleUrl: './infinite-scroll-container-demo.component.scss'
+})
+export class InfiniteScrollContainerDemoComponent {
+ @ViewChild('itemTemplate') itemTemplate!: TemplateRef;
+
+ // Basic example
+ basicItems = signal([]);
+ basicLoading = signal(false);
+ basicHasMore = signal(true);
+ private basicPage = 1;
+
+ // Template example
+ templateItems = signal([]);
+ templateLoading = signal(false);
+ templateHasMore = signal(true);
+ private templatePage = 1;
+
+ templateConfig: InfiniteScrollConfig = {
+ threshold: 100,
+ debounceTime: 150,
+ itemHeight: 120
+ };
+
+ // Bidirectional example
+ bidirectionalItems = signal([]);
+ bidirectionalLoadingUp = signal(false);
+ bidirectionalLoadingDown = signal(false);
+ bidirectionalHasMore = signal(true);
+ private bidirectionalTopPage = 0;
+ private bidirectionalBottomPage = 1;
+
+ bidirectionalConfig: InfiniteScrollConfig = {
+ threshold: 50,
+ debounceTime: 200
+ };
+
+ // Custom templates example
+ customItems = signal([]);
+ customLoading = signal(false);
+ customHasMore = signal(true);
+ customError = signal(undefined);
+ private customPage = 1;
+ private customFailCount = 0;
+
+ customConfig: InfiniteScrollConfig = {
+ threshold: 150,
+ debounceTime: 100
+ };
+
+ // Configuration example
+ configItems = signal([]);
+ configLoading = signal(false);
+ configHasMore = signal(true);
+ private configPage = 1;
+
+ // Configuration controls
+ threshold = signal(200);
+ debounceTime = signal(300);
+ disableScroll = signal(false);
+
+ currentConfig = signal({
+ threshold: 200,
+ debounceTime: 300,
+ disabled: false
+ });
+
+ constructor() {
+ // Initialize with some data
+ this.loadInitialData();
+ }
+
+ // Track by function for template rendering
+ trackByFn = (index: number, item: DemoItem) => item.id;
+
+ // Basic example methods
+ loadMoreBasic(event: InfiniteScrollEvent): void {
+ if (this.basicLoading()) return;
+
+ this.basicLoading.set(true);
+
+ // Simulate API call
+ setTimeout(() => {
+ const newItems = this.generateItems(this.basicPage * 20, 20, 'basic');
+ this.basicItems.update(items => [...items, ...newItems]);
+ this.basicPage++;
+ this.basicLoading.set(false);
+
+ // Stop loading after 5 pages
+ if (this.basicPage > 5) {
+ this.basicHasMore.set(false);
+ }
+ }, 1000);
+ }
+
+ // Template example methods
+ loadMoreTemplate(event: InfiniteScrollEvent): void {
+ if (this.templateLoading()) return;
+
+ this.templateLoading.set(true);
+
+ setTimeout(() => {
+ const newItems = this.generateItems(this.templatePage * 15, 15, 'template');
+ this.templateItems.update(items => [...items, ...newItems]);
+ this.templatePage++;
+ this.templateLoading.set(false);
+
+ if (this.templatePage > 6) {
+ this.templateHasMore.set(false);
+ }
+ }, 800);
+ }
+
+ // Bidirectional example methods
+ loadMoreBidirectional(event: InfiniteScrollEvent): void {
+ if (event.direction === 'down') {
+ this.loadMoreBidirectionalDown();
+ } else {
+ this.loadMoreBidirectionalUp();
+ }
+ }
+
+ private loadMoreBidirectionalDown(): void {
+ if (this.bidirectionalLoadingDown()) return;
+
+ this.bidirectionalLoadingDown.set(true);
+
+ setTimeout(() => {
+ const newItems = this.generateItems(this.bidirectionalBottomPage * 10, 10, 'chat');
+ this.bidirectionalItems.update(items => [...items, ...newItems]);
+ this.bidirectionalBottomPage++;
+ this.bidirectionalLoadingDown.set(false);
+ }, 600);
+ }
+
+ private loadMoreBidirectionalUp(): void {
+ if (this.bidirectionalLoadingUp()) return;
+
+ this.bidirectionalLoadingUp.set(true);
+
+ setTimeout(() => {
+ const startId = this.bidirectionalTopPage * 10;
+ const newItems = this.generateItems(startId, 10, 'history');
+ this.bidirectionalItems.update(items => [...newItems, ...items]);
+ this.bidirectionalTopPage--;
+ this.bidirectionalLoadingUp.set(false);
+ }, 800);
+ }
+
+ // Custom templates example methods
+ loadMoreCustom(event: InfiniteScrollEvent): void {
+ if (this.customLoading()) return;
+
+ this.customLoading.set(true);
+ this.customError.set(undefined);
+
+ // Simulate occasional failures
+ const shouldFail = Math.random() < 0.3 && this.customFailCount < 2;
+
+ setTimeout(() => {
+ if (shouldFail) {
+ this.customError.set('Network error: Failed to load more items');
+ this.customLoading.set(false);
+ this.customFailCount++;
+ } else {
+ const newItems = this.generateItems(this.customPage * 12, 12, 'custom');
+ this.customItems.update(items => [...items, ...newItems]);
+ this.customPage++;
+ this.customLoading.set(false);
+
+ if (this.customPage > 4) {
+ this.customHasMore.set(false);
+ }
+ }
+ }, 1200);
+ }
+
+ retryCustom(): void {
+ this.loadMoreCustom({ direction: 'down', currentIndex: 0, scrollPosition: 0 });
+ }
+
+ // Configuration example methods
+ loadMoreConfig(event: InfiniteScrollEvent): void {
+ if (this.configLoading()) return;
+
+ this.configLoading.set(true);
+
+ setTimeout(() => {
+ const newItems = this.generateItems(this.configPage * 10, 10, 'config');
+ this.configItems.update(items => [...items, ...newItems]);
+ this.configPage++;
+ this.configLoading.set(false);
+
+ if (this.configPage > 10) {
+ this.configHasMore.set(false);
+ }
+ }, 500);
+ }
+
+ // Configuration controls
+ toggleDisabled(event: Event): void {
+ const disabled = (event.target as HTMLInputElement).checked;
+ this.disableScroll.set(disabled);
+ this.updateCurrentConfig();
+ }
+
+ updateThreshold(event: Event): void {
+ const value = parseInt((event.target as HTMLInputElement).value);
+ this.threshold.set(value);
+ this.updateCurrentConfig();
+ }
+
+ updateDebounceTime(event: Event): void {
+ const value = parseInt((event.target as HTMLInputElement).value);
+ this.debounceTime.set(value);
+ this.updateCurrentConfig();
+ }
+
+ private updateCurrentConfig(): void {
+ this.currentConfig.set({
+ threshold: this.threshold(),
+ debounceTime: this.debounceTime(),
+ disabled: this.disableScroll()
+ });
+ }
+
+ // API methods - These would be called on component references
+ scrollToTop(): void {
+ console.log('Scroll to top - would call component method');
+ }
+
+ scrollToBottom(): void {
+ console.log('Scroll to bottom - would call component method');
+ }
+
+ scrollToIndex(index: number): void {
+ console.log(`Scroll to index ${index} - would call component method`);
+ }
+
+ resetApiScroll(): void {
+ console.log('Reset scroll - would call component method');
+ }
+
+ // Utility methods
+ getTimeAgo(date: Date): string {
+ const now = new Date();
+ const diff = now.getTime() - date.getTime();
+ const minutes = Math.floor(diff / (1000 * 60));
+
+ if (minutes < 1) return 'Just now';
+ if (minutes < 60) return `${minutes}m ago`;
+ if (minutes < 1440) return `${Math.floor(minutes / 60)}h ago`;
+ return `${Math.floor(minutes / 1440)}d ago`;
+ }
+
+ private loadInitialData(): void {
+ // Load initial data for all examples
+ this.basicItems.set(this.generateItems(1, 20, 'basic'));
+ this.templateItems.set(this.generateItems(1, 15, 'template'));
+ this.bidirectionalItems.set(this.generateItems(1, 20, 'chat'));
+ this.customItems.set(this.generateItems(1, 12, 'custom'));
+ this.configItems.set(this.generateItems(1, 10, 'config'));
+ }
+
+ private generateItems(startId: number, count: number, type: string): DemoItem[] {
+ const categories = ['Technology', 'Science', 'Arts', 'Sports', 'Travel', 'Food', 'Music', 'Literature'];
+
+ return Array.from({ length: count }, (_, i) => ({
+ id: startId + i,
+ title: `${type.charAt(0).toUpperCase() + type.slice(1)} Item ${startId + i}`,
+ description: `This is a sample description for ${type} item ${startId + i}. It contains some interesting content that demonstrates the infinite scroll functionality.`,
+ timestamp: new Date(Date.now() - Math.random() * 86400000 * 30), // Random date within last 30 days
+ category: categories[Math.floor(Math.random() * categories.length)]
+ }));
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/kanban-board-demo/kanban-board-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/kanban-board-demo/kanban-board-demo.component.scss
new file mode 100644
index 0000000..f94c2ac
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/kanban-board-demo/kanban-board-demo.component.scss
@@ -0,0 +1,128 @@
+@use "../../../../../ui-design-system/src/styles/semantic" as *;
+
+.demo-container {
+ padding: $semantic-spacing-component-lg;
+ max-width: 100%;
+
+ h2 {
+ margin: 0 0 $semantic-spacing-component-lg 0;
+ 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;
+ }
+
+ h3 {
+ margin: $semantic-spacing-component-xl 0 $semantic-spacing-component-md 0;
+ 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;
+ }
+
+ h4 {
+ margin: 0 0 $semantic-spacing-component-sm 0;
+ 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;
+ }
+}
+
+.demo-section {
+ margin-bottom: $semantic-spacing-component-xl;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+}
+
+.demo-row {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: $semantic-spacing-grid-gap-lg;
+
+ @media (max-width: 768px) {
+ grid-template-columns: 1fr;
+ gap: $semantic-spacing-grid-gap-md;
+ }
+}
+
+.demo-item {
+ background: $semantic-color-surface-secondary;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ border-radius: $semantic-border-radius-md;
+ padding: $semantic-spacing-component-md;
+ box-shadow: $semantic-shadow-elevation-1;
+}
+
+.event-log {
+ margin-top: $semantic-spacing-component-lg;
+ padding: $semantic-spacing-component-md;
+ background: $semantic-color-surface-secondary;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ border-radius: $semantic-border-radius-md;
+
+ h4 {
+ margin: 0 0 $semantic-spacing-component-sm 0;
+ 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;
+ }
+}
+
+.events {
+ max-height: 200px;
+ overflow-y: auto;
+
+ // Scroll styling
+ scrollbar-width: thin;
+ scrollbar-color: $semantic-color-border-primary $semantic-color-surface-container;
+
+ &::-webkit-scrollbar {
+ width: 8px;
+ }
+
+ &::-webkit-scrollbar-track {
+ background: $semantic-color-surface-container;
+ border-radius: $semantic-border-radius-sm;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-sm;
+ }
+}
+
+.event-item {
+ display: flex;
+ flex-direction: column;
+ gap: $semantic-spacing-component-xs;
+ padding: $semantic-spacing-component-sm;
+ border-bottom: $semantic-border-width-1 solid $semantic-color-border-subtle;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ strong {
+ font-family: map-get($semantic-typography-body-medium, font-family);
+ font-size: map-get($semantic-typography-body-medium, font-size);
+ font-weight: $semantic-typography-font-weight-semibold;
+ line-height: map-get($semantic-typography-body-medium, line-height);
+ color: $semantic-color-text-primary;
+ }
+
+ small {
+ font-family: map-get($semantic-typography-caption, font-family);
+ font-size: map-get($semantic-typography-caption, font-size);
+ font-weight: map-get($semantic-typography-caption, font-weight);
+ line-height: map-get($semantic-typography-caption, line-height);
+ color: $semantic-color-text-tertiary;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/kanban-board-demo/kanban-board-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/kanban-board-demo/kanban-board-demo.component.ts
new file mode 100644
index 0000000..fe80904
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/kanban-board-demo/kanban-board-demo.component.ts
@@ -0,0 +1,354 @@
+import { Component, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { KanbanBoardComponent, KanbanColumn, KanbanItem, KanbanDragEvent } from 'ui-essentials';
+
+@Component({
+ selector: 'ui-kanban-board-demo',
+ standalone: true,
+ imports: [CommonModule, KanbanBoardComponent],
+ template: `
+
+
Kanban Board Layout Demo
+
+
+
+ Sizes
+
+
+
+
+
Medium (Default)
+
+
+
+
+
+
+
+
+
+
+
+
+ Full Featured Kanban Board
+
+
+
+
+
+
+
+
+ States
+
+
+
+
+
Column with Limits
+
+
+
+
+
+
+
+
+
+
+ Interactive Events
+
+
+
+
+
+
+
Event Log:
+
+ @for (event of eventLog(); track event.id) {
+
+ {{ event.type }}: {{ event.message }}
+ {{ event.timestamp | date:'medium' }}
+
+ }
+
+
+
+
+ `,
+ styleUrl: './kanban-board-demo.component.scss'
+})
+export class KanbanBoardDemoComponent {
+ eventLog = signal>([]);
+
+ basicColumns(): KanbanColumn[] {
+ return [
+ {
+ id: 'todo',
+ title: 'To Do',
+ items: [
+ { id: '1', title: 'Design homepage', description: 'Create wireframes and mockups' },
+ { id: '2', title: 'Setup database', priority: 'high' }
+ ]
+ },
+ {
+ id: 'in-progress',
+ title: 'In Progress',
+ items: [
+ { id: '3', title: 'Build API endpoints', priority: 'medium', assignee: 'John' }
+ ]
+ },
+ {
+ id: 'done',
+ title: 'Done',
+ items: [
+ { id: '4', title: 'Project setup', description: 'Initialize repository and dependencies' }
+ ]
+ }
+ ];
+ }
+
+ fullColumns(): KanbanColumn[] {
+ return [
+ {
+ id: 'backlog',
+ title: 'Backlog',
+ items: [
+ {
+ id: 'item-1',
+ title: 'User Authentication System',
+ description: 'Implement login, registration, and password reset functionality',
+ priority: 'high',
+ tags: ['backend', 'security', 'user-management'],
+ assignee: 'Sarah Connor',
+ dueDate: new Date(2024, 11, 15)
+ },
+ {
+ id: 'item-2',
+ title: 'Dashboard Analytics',
+ description: 'Create comprehensive analytics dashboard with charts and metrics',
+ priority: 'medium',
+ tags: ['frontend', 'analytics', 'charts'],
+ assignee: 'John Doe',
+ dueDate: new Date(2024, 11, 20)
+ },
+ {
+ id: 'item-3',
+ title: 'Mobile Responsiveness',
+ description: 'Ensure all pages work correctly on mobile devices',
+ priority: 'low',
+ tags: ['frontend', 'responsive', 'css'],
+ assignee: 'Jane Smith'
+ }
+ ]
+ },
+ {
+ id: 'todo',
+ title: 'To Do',
+ items: [
+ {
+ id: 'item-4',
+ title: 'API Rate Limiting',
+ description: 'Implement rate limiting for API endpoints to prevent abuse',
+ priority: 'high',
+ tags: ['backend', 'security', 'performance'],
+ assignee: 'Bob Wilson',
+ dueDate: new Date(2024, 11, 10)
+ },
+ {
+ id: 'item-5',
+ title: 'Email Notifications',
+ description: 'Set up email notification system for important events',
+ priority: 'medium',
+ tags: ['backend', 'email', 'notifications'],
+ assignee: 'Alice Brown'
+ }
+ ]
+ },
+ {
+ id: 'in-progress',
+ title: 'In Progress',
+ items: [
+ {
+ id: 'item-6',
+ title: 'Database Optimization',
+ description: 'Optimize database queries and add proper indexing',
+ priority: 'high',
+ tags: ['database', 'performance', 'optimization'],
+ assignee: 'Mike Johnson',
+ dueDate: new Date(2024, 11, 8)
+ }
+ ]
+ },
+ {
+ id: 'review',
+ title: 'In Review',
+ items: [
+ {
+ id: 'item-7',
+ title: 'Unit Test Coverage',
+ description: 'Increase unit test coverage to 90%',
+ priority: 'medium',
+ tags: ['testing', 'quality', 'coverage'],
+ assignee: 'Lisa Davis',
+ dueDate: new Date(2024, 11, 12)
+ }
+ ]
+ },
+ {
+ id: 'done',
+ title: 'Done',
+ items: [
+ {
+ id: 'item-8',
+ title: 'Project Setup',
+ description: 'Initialize project structure and dependencies',
+ priority: 'high',
+ tags: ['setup', 'infrastructure'],
+ assignee: 'Team Lead'
+ },
+ {
+ id: 'item-9',
+ title: 'Design System',
+ description: 'Create component library and design tokens',
+ priority: 'medium',
+ tags: ['design', 'components', 'ui'],
+ assignee: 'Design Team'
+ }
+ ]
+ }
+ ];
+ }
+
+ limitedColumns(): KanbanColumn[] {
+ return [
+ {
+ id: 'todo',
+ title: 'To Do',
+ items: [
+ { id: '1', title: 'Task 1' },
+ { id: '2', title: 'Task 2' }
+ ]
+ },
+ {
+ id: 'limited',
+ title: 'Limited (Max 2)',
+ maxItems: 2,
+ items: [
+ { id: '3', title: 'Limited Task 1' },
+ { id: '4', title: 'Limited Task 2' }
+ ]
+ },
+ {
+ id: 'done',
+ title: 'Done',
+ items: []
+ }
+ ];
+ }
+
+ interactiveColumns(): KanbanColumn[] {
+ return [
+ {
+ id: 'new',
+ title: 'New',
+ items: [
+ { id: 'i1', title: 'Interactive Item 1', description: 'Click me or drag me!' },
+ { id: 'i2', title: 'Interactive Item 2', priority: 'high', assignee: 'You' }
+ ]
+ },
+ {
+ id: 'active',
+ title: 'Active',
+ items: [
+ { id: 'i3', title: 'Interactive Item 3', priority: 'medium' }
+ ]
+ },
+ {
+ id: 'complete',
+ title: 'Complete',
+ items: []
+ }
+ ];
+ }
+
+ onItemMoved(event: KanbanDragEvent): void {
+ this.logEvent('Item Moved',
+ `"${event.item.title}" moved from "${event.fromColumn}" to "${event.toColumn}"`
+ );
+
+ // In a real application, you would update your data here
+ console.log('Item moved:', event);
+ }
+
+ onItemClicked(event: { item: KanbanItem; columnId: string }): void {
+ this.logEvent('Item Clicked',
+ `"${event.item.title}" clicked in column "${event.columnId}"`
+ );
+
+ console.log('Item clicked:', event);
+ }
+
+ private logEvent(type: string, message: string): void {
+ const currentLog = this.eventLog();
+ const newEvent = {
+ id: Date.now().toString(),
+ type,
+ message,
+ timestamp: new Date()
+ };
+
+ // Keep only the last 10 events
+ const updatedLog = [newEvent, ...currentLog].slice(0, 10);
+ this.eventLog.set(updatedLog);
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/masonry-demo/masonry-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/masonry-demo/masonry-demo.component.scss
new file mode 100644
index 0000000..70fce15
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/masonry-demo/masonry-demo.component.scss
@@ -0,0 +1,30 @@
+.demo-container {
+ .demo-section {
+ margin-bottom: 2rem;
+
+ h3 {
+ margin-bottom: 1rem;
+ color: #333;
+ }
+
+ .demo-row {
+ display: grid;
+ gap: 1.5rem;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+
+ .demo-item {
+ &.full-width {
+ grid-column: 1 / -1;
+ }
+
+ h4 {
+ margin-bottom: 0.5rem;
+ color: #555;
+ font-size: 0.9rem;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/masonry-demo/masonry-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/masonry-demo/masonry-demo.component.ts
new file mode 100644
index 0000000..d29ee33
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/masonry-demo/masonry-demo.component.ts
@@ -0,0 +1,225 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { MasonryComponent } from 'ui-essentials';
+
+@Component({
+ selector: 'ui-masonry-demo',
+ standalone: true,
+ imports: [CommonModule, MasonryComponent],
+ template: `
+
+
Masonry Layout Demo
+
Pinterest-style dynamic grid layout that arranges items in columns with optimal vertical spacing.
+
+
+
+ Column Count
+
+ @for (columns of columnCounts; track columns) {
+
+
{{ columns }} Columns
+
+ @for (item of sampleItems.slice(0, 8); track item.id) {
+
+
{{ item.title }}
+
{{ item.content }}
+
+ }
+
+
+ }
+
+
+
+
+
+ Auto Responsive
+
+
+
Auto Fit (250px min width)
+
+ @for (item of sampleItems.slice(0, 12); track item.id) {
+
+
{{ item.title }}
+
{{ item.content }}
+
+ }
+
+
+
+
+
Auto Fill (200px min width)
+
+ @for (item of sampleItems.slice(0, 12); track item.id) {
+
+
{{ item.title }}
+
{{ item.content }}
+
+ }
+
+
+
+
+
+
+
+ Gap Sizes
+
+ @for (gap of gaps; track gap) {
+
+
Gap: {{ gap }}
+
+ @for (item of sampleItems.slice(0, 6); track item.id) {
+
+
{{ item.title }}
+
{{ item.content }}
+
+ }
+
+
+ }
+
+
+
+
+
+ Padding Options
+
+ @for (padding of paddings; track padding) {
+
+
Padding: {{ padding }}
+
+ @for (item of sampleItems.slice(0, 6); track item.id) {
+
+
{{ item.title }}
+
{{ item.content }}
+
+ }
+
+
+ }
+
+
+
+
+
+ Alignment
+
+ @for (alignment of alignments; track alignment) {
+
+
Align: {{ alignment }}
+
+ @for (item of sampleItems.slice(0, 4); track item.id) {
+
+
{{ item.title }}
+
{{ item.content }}
+
+ }
+
+
+ }
+
+
+
+
+
+ Item Variants
+
+
+
Mixed Item Sizes
+
+
+
Compact Item
+
Compact padding for dense layouts
+
+
+
Comfortable Item
+
Standard comfortable padding for most use cases
+
+
+
Spacious Item
+
Extra padding for premium content
+
+
+
+
Full Bleed Item
+
No padding for full-width content
+
+
+ @for (item of sampleItems.slice(0, 6); track item.id) {
+
+
{{ item.title }}
+
{{ item.content }}
+
+ }
+
+
+
+
+
+
+
+ Pinterest-style Gallery
+
+
+
+ @for (item of extendedItems; track item.id) {
+
+
{{ item.title }}
+
{{ item.content }}
+
+ }
+
+
+
+
+
+
+ `,
+ styleUrl: './masonry-demo.component.scss'
+})
+export class MasonryDemoComponent {
+ columnCounts = [2, 3, 4] as const;
+ gaps = ['sm', 'md', 'lg'] as const;
+ paddings = ['none', 'sm', 'md', 'lg'] as const;
+ alignments = ['start', 'center', 'end'] as const;
+
+ sampleItems = [
+ { id: 1, title: 'Card 1', content: 'Short content', height: 120 },
+ { id: 2, title: 'Card 2', content: 'Medium length content with more details', height: 180 },
+ { id: 3, title: 'Card 3', content: 'Brief', height: 100 },
+ { id: 4, title: 'Card 4', content: 'Longer content with multiple lines of text to demonstrate variable heights', height: 220 },
+ { id: 5, title: 'Card 5', content: 'Standard content', height: 140 },
+ { id: 6, title: 'Card 6', content: 'Extended content with even more text to show how masonry handles varying content lengths', height: 260 },
+ { id: 7, title: 'Card 7', content: 'Compact', height: 90 },
+ { id: 8, title: 'Card 8', content: 'Regular content block', height: 160 },
+ { id: 9, title: 'Card 9', content: 'Another piece of content', height: 130 },
+ { id: 10, title: 'Card 10', content: 'Different sized content', height: 200 },
+ { id: 11, title: 'Card 11', content: 'Small content', height: 110 },
+ { id: 12, title: 'Card 12', content: 'Large content with lots of text and information', height: 240 }
+ ];
+
+ extendedItems = [
+ { id: 1, title: 'Ocean Waves', content: 'Beautiful ocean scenery with crashing waves', height: 180, background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
+ { id: 2, title: 'Mountain Vista', content: 'Majestic mountain range at sunset', height: 220, background: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' },
+ { id: 3, title: 'Forest Path', content: 'Winding path through dense forest', height: 160, background: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' },
+ { id: 4, title: 'Desert Dunes', content: 'Golden sand dunes stretching to horizon', height: 200, background: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)' },
+ { id: 5, title: 'City Skyline', content: 'Modern city with glowing lights', height: 140, background: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)' },
+ { id: 6, title: 'Northern Lights', content: 'Aurora borealis dancing across night sky', height: 250, background: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)' },
+ { id: 7, title: 'Tropical Beach', content: 'Palm trees and crystal clear water', height: 180, background: 'linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)' },
+ { id: 8, title: 'Snow Peaks', content: 'Snow-capped mountain peaks', height: 190, background: 'linear-gradient(135deg, #a1c4fd 0%, #c2e9fb 100%)' },
+ { id: 9, title: 'Autumn Leaves', content: 'Colorful fall foliage', height: 170, background: 'linear-gradient(135deg, #fad0c4 0%, #fad0c4 1%, #ffd1ff 100%)' },
+ { id: 10, title: 'Starry Night', content: 'Countless stars in clear night sky', height: 210, background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
+ { id: 11, title: 'Wildflowers', content: 'Field of colorful wildflowers', height: 150, background: 'linear-gradient(135deg, #ff9a9e 0%, #fecfef 50%, #fecfef 100%)' },
+ { id: 12, title: 'Waterfall', content: 'Powerful waterfall cascading down rocks', height: 230, background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }
+ ];
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/select-demo/select-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/select-demo/select-demo.component.ts
new file mode 100644
index 0000000..61e66f7
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/select-demo/select-demo.component.ts
@@ -0,0 +1,505 @@
+import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { SelectComponent, SelectOption } from '../../../../../ui-essentials/src/lib/components/forms/select/select.component';
+
+@Component({
+ selector: 'ui-select-demo',
+ standalone: true,
+ imports: [
+ CommonModule,
+ FormsModule,
+ SelectComponent
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
Select Component Showcase
+
+
+
+ Basic Select
+
+
+
+
+
+
+
+
+
+ Size Variants
+
+
+
+
+
+
+
+
+
+ Style Variants
+
+
+
+
+
+
+
+
+ States and Validation
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Complex Options with Disabled Items
+
+
+
+
+
+
+
+
+ Interactive Controls
+
+
+ Set Default Values
+
+
+ Clear All
+
+
+ {{ globalDisabled() ? 'Enable All' : 'Disable All' }}
+
+
+
+ @if (lastSelection()) {
+
+ Last selection: {{ lastSelection() }}
+
+ }
+
+
+
+
+ Code Examples
+
+
Basic Usage:
+
<ui-select
+ label="Choose a country"
+ placeholder="Select a country"
+ [options]="countryOptions"
+ [(ngModel)]="selectedCountry"
+ (valueChange)="onCountryChange($event)">
+</ui-select>
+
+
With Helper Text:
+
<ui-select
+ label="Framework"
+ placeholder="Select framework"
+ helperText="Choose your preferred frontend framework"
+ size="large"
+ variant="filled"
+ [options]="frameworks"
+ [(ngModel)]="selectedFramework">
+</ui-select>
+
+
Required with Error State:
+
<ui-select
+ label="Required field"
+ [required]="true"
+ errorText="Please select an option"
+ [options]="options"
+ [(ngModel)]="selectedValue">
+</ui-select>
+
+
Options Format:
+
const options: SelectOption[] = [
+ {{ '{' }} value: 'option1', label: 'Option 1' {{ '}' }},
+ {{ '{' }} value: 'option2', label: 'Option 2' {{ '}' }},
+ {{ '{' }} value: 'option3', label: 'Option 3 (Disabled)', disabled: true {{ '}' }}
+];
+
+
+
+
+
+ Current Values
+
+
{{ getCurrentValues() }}
+
+
+
+ `,
+ styles: [`
+ h2 {
+ color: hsl(279, 14%, 11%);
+ font-size: 2rem;
+ margin-bottom: 2rem;
+ border-bottom: 2px solid hsl(258, 100%, 47%);
+ padding-bottom: 0.5rem;
+ }
+
+ h3 {
+ color: hsl(279, 14%, 25%);
+ font-size: 1.5rem;
+ margin-bottom: 1rem;
+ }
+
+ h4 {
+ color: hsl(279, 14%, 35%);
+ font-size: 1.2rem;
+ margin-bottom: 0.5rem;
+ }
+
+ pre {
+ margin: 0;
+ font-family: 'Courier New', monospace;
+ font-size: 0.9rem;
+ line-height: 1.4;
+ }
+
+ code {
+ color: hsl(279, 14%, 15%);
+ }
+
+ section {
+ border: 1px solid #e9ecef;
+ padding: 1.5rem;
+ border-radius: 8px;
+ background: #ffffff;
+ }
+
+ button:hover {
+ opacity: 0.9;
+ transform: translateY(-1px);
+ }
+ `]
+})
+export class SelectDemoComponent {
+ // Basic selects
+ basicSelect1 = signal('');
+ basicSelect2 = signal('en');
+ basicSelect3 = signal('');
+
+ // Size variants
+ sizeSmall = signal('');
+ sizeMedium = signal('medium');
+ sizeLarge = signal('');
+
+ // Style variants
+ variantOutlined = signal('');
+ variantFilled = signal('filled');
+
+ // States
+ stateDisabled = signal('disabled');
+ stateRequired = signal('');
+ stateError = signal('');
+ stateHelper = signal('');
+
+ // Full width
+ fullWidthSelect = signal('');
+
+ // Complex selects
+ complexSelect1 = signal('');
+ complexSelect2 = signal('medium');
+
+ // Control states
+ globalDisabled = signal(false);
+ lastSelection = signal('');
+
+ // Option arrays
+ countryOptions: SelectOption[] = [
+ { value: 'us', label: 'United States' },
+ { value: 'ca', label: 'Canada' },
+ { value: 'uk', label: 'United Kingdom' },
+ { value: 'de', label: 'Germany' },
+ { value: 'fr', label: 'France' },
+ { value: 'jp', label: 'Japan' },
+ { value: 'au', label: 'Australia' }
+ ];
+
+ languageOptions: SelectOption[] = [
+ { value: 'en', label: 'English' },
+ { value: 'es', label: 'Spanish' },
+ { value: 'fr', label: 'French' },
+ { value: 'de', label: 'German' },
+ { value: 'it', label: 'Italian' },
+ { value: 'pt', label: 'Portuguese' },
+ { value: 'zh', label: 'Chinese' }
+ ];
+
+ frameworkOptions: SelectOption[] = [
+ { value: 'angular', label: 'Angular' },
+ { value: 'react', label: 'React' },
+ { value: 'vue', label: 'Vue.js' },
+ { value: 'svelte', label: 'Svelte' },
+ { value: 'solid', label: 'SolidJS' }
+ ];
+
+ sizeOptions: SelectOption[] = [
+ { value: 'small', label: 'Small Size' },
+ { value: 'medium', label: 'Medium Size' },
+ { value: 'large', label: 'Large Size' }
+ ];
+
+ variantOptions: SelectOption[] = [
+ { value: 'outlined', label: 'Outlined Style' },
+ { value: 'filled', label: 'Filled Style' }
+ ];
+
+ stateOptions: SelectOption[] = [
+ { value: 'active', label: 'Active' },
+ { value: 'inactive', label: 'Inactive' },
+ { value: 'pending', label: 'Pending' }
+ ];
+
+ fullWidthOptions: SelectOption[] = [
+ { value: 'option1', label: 'This is a full width select option' },
+ { value: 'option2', label: 'Another full width option with longer text' },
+ { value: 'option3', label: 'Third option demonstrating full width behavior' }
+ ];
+
+ roleOptions: SelectOption[] = [
+ { value: 'user', label: 'User' },
+ { value: 'moderator', label: 'Moderator' },
+ { value: 'admin', label: 'Administrator' },
+ { value: 'super-admin', label: 'Super Administrator', disabled: true },
+ { value: 'guest', label: 'Guest', disabled: true }
+ ];
+
+ priorityOptions: SelectOption[] = [
+ { value: 'low', label: 'Low Priority' },
+ { value: 'medium', label: 'Medium Priority' },
+ { value: 'high', label: 'High Priority' },
+ { value: 'urgent', label: 'Urgent' },
+ { value: 'critical', label: 'Critical', disabled: true }
+ ];
+
+ onSelectionChange(selectId: string, value: any): void {
+ const selectedOption = this.findOptionByValue(value);
+ const displayValue = selectedOption ? selectedOption.label : value;
+ this.lastSelection.set(`${selectId}: ${displayValue} (${value})`);
+ console.log(`Select ${selectId} changed:`, value);
+ }
+
+ private findOptionByValue(value: any): SelectOption | undefined {
+ const allOptions = [
+ ...this.countryOptions,
+ ...this.languageOptions,
+ ...this.frameworkOptions,
+ ...this.sizeOptions,
+ ...this.variantOptions,
+ ...this.stateOptions,
+ ...this.fullWidthOptions,
+ ...this.roleOptions,
+ ...this.priorityOptions
+ ];
+ return allOptions.find(option => option.value === value);
+ }
+
+ setDefaultValues(): void {
+ this.basicSelect1.set('us');
+ this.basicSelect2.set('en');
+ this.basicSelect3.set('angular');
+ this.sizeSmall.set('small');
+ this.sizeMedium.set('medium');
+ this.sizeLarge.set('large');
+ this.variantOutlined.set('outlined');
+ this.variantFilled.set('filled');
+ this.stateRequired.set('active');
+ this.stateError.set('pending');
+ this.stateHelper.set('inactive');
+ this.fullWidthSelect.set('option2');
+ this.complexSelect1.set('admin');
+ this.complexSelect2.set('high');
+
+ this.lastSelection.set('All selects set to default values');
+ }
+
+ clearAllSelections(): void {
+ this.basicSelect1.set('');
+ this.basicSelect2.set('');
+ this.basicSelect3.set('');
+ this.sizeSmall.set('');
+ this.sizeMedium.set('');
+ this.sizeLarge.set('');
+ this.variantOutlined.set('');
+ this.variantFilled.set('');
+ this.stateRequired.set('');
+ this.stateError.set('');
+ this.stateHelper.set('');
+ this.fullWidthSelect.set('');
+ this.complexSelect1.set('');
+ this.complexSelect2.set('');
+
+ this.lastSelection.set('All selections cleared');
+ }
+
+ toggleDisabled(): void {
+ this.globalDisabled.update(disabled => !disabled);
+ this.lastSelection.set(`Global disabled: ${this.globalDisabled() ? 'enabled' : 'disabled'}`);
+ }
+
+ getCurrentValues(): string {
+ const values = {
+ basic: {
+ country: this.basicSelect1(),
+ language: this.basicSelect2(),
+ framework: this.basicSelect3()
+ },
+ sizes: {
+ small: this.sizeSmall(),
+ medium: this.sizeMedium(),
+ large: this.sizeLarge()
+ },
+ variants: {
+ outlined: this.variantOutlined(),
+ filled: this.variantFilled()
+ },
+ states: {
+ disabled: this.stateDisabled(),
+ required: this.stateRequired(),
+ error: this.stateError(),
+ helper: this.stateHelper()
+ },
+ layout: {
+ fullWidth: this.fullWidthSelect()
+ },
+ complex: {
+ roles: this.complexSelect1(),
+ priority: this.complexSelect2()
+ },
+ controls: {
+ globalDisabled: this.globalDisabled(),
+ lastSelection: this.lastSelection()
+ }
+ };
+
+ return JSON.stringify(values, null, 2);
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/split-view-demo/split-view-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/split-view-demo/split-view-demo.component.scss
new file mode 100644
index 0000000..c651957
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/split-view-demo/split-view-demo.component.scss
@@ -0,0 +1,157 @@
+@use '../../../../../ui-design-system/src/styles/semantic' as *;
+
+.demo-container {
+ padding: $semantic-spacing-component-lg;
+
+ .demo-section {
+ margin-bottom: $semantic-spacing-layout-section-lg;
+
+ h3 {
+ 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-component-md;
+ }
+
+ .demo-split-container {
+ height: 400px;
+ margin-bottom: $semantic-spacing-component-lg;
+ border: $semantic-border-width-1 solid $semantic-color-border-secondary;
+ border-radius: $semantic-border-radius-md;
+ }
+
+ .demo-panel-content {
+ padding: $semantic-spacing-component-md;
+ background: $semantic-color-surface-primary;
+ height: 100%;
+
+ h4 {
+ 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;
+ }
+
+ 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-bottom: $semantic-spacing-content-paragraph;
+ }
+
+ &--primary {
+ background: $semantic-color-surface-primary;
+ }
+
+ &--secondary {
+ background: $semantic-color-surface-secondary;
+ }
+
+ &--elevated {
+ background: $semantic-color-surface-elevated;
+ box-shadow: $semantic-shadow-elevation-1;
+ }
+ }
+
+ .demo-controls {
+ display: flex;
+ gap: $semantic-spacing-component-sm;
+ margin-bottom: $semantic-spacing-component-md;
+ flex-wrap: wrap;
+
+ button {
+ padding: $semantic-spacing-interactive-button-padding-y $semantic-spacing-interactive-button-padding-x;
+ background: $semantic-color-primary;
+ color: $semantic-color-on-primary;
+ border: none;
+ border-radius: $semantic-border-button-radius;
+ cursor: pointer;
+ 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);
+ transition: all $semantic-motion-duration-fast $semantic-motion-easing-ease;
+
+ &:hover {
+ background: $semantic-color-primary;
+ box-shadow: $semantic-shadow-button-hover;
+ opacity: $semantic-opacity-hover;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-focus;
+ outline-offset: 2px;
+ }
+
+ &--secondary {
+ background: $semantic-color-secondary;
+ color: $semantic-color-on-secondary;
+ }
+
+ &--success {
+ background: $semantic-color-success;
+ color: $semantic-color-on-success;
+ }
+
+ &--danger {
+ background: $semantic-color-danger;
+ color: $semantic-color-on-danger;
+ }
+ }
+ }
+
+ .demo-info {
+ padding: $semantic-spacing-component-md;
+ background: $semantic-color-surface-container;
+ border: $semantic-border-width-1 solid $semantic-color-border-subtle;
+ border-radius: $semantic-border-radius-sm;
+ margin-top: $semantic-spacing-component-md;
+
+ p {
+ 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-bottom: $semantic-spacing-content-line-tight;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ strong {
+ color: $semantic-color-text-primary;
+ font-weight: $semantic-typography-font-weight-semibold;
+ }
+ }
+ }
+}
+
+@media (max-width: $semantic-breakpoint-md - 1) {
+ .demo-container {
+ padding: $semantic-spacing-component-md;
+
+ .demo-section .demo-split-container {
+ height: 300px;
+ }
+
+ .demo-section .demo-controls {
+ gap: $semantic-spacing-component-xs;
+
+ button {
+ padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
+ font-family: map-get($semantic-typography-button-small, font-family);
+ font-size: map-get($semantic-typography-button-small, font-size);
+ font-weight: map-get($semantic-typography-button-small, font-weight);
+ line-height: map-get($semantic-typography-button-small, line-height);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/split-view-demo/split-view-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/split-view-demo/split-view-demo.component.ts
new file mode 100644
index 0000000..090b2d2
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/split-view-demo/split-view-demo.component.ts
@@ -0,0 +1,315 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { SplitViewComponent, PanelConfig, ResizeEvent } from 'ui-essentials';
+
+@Component({
+ selector: 'ui-split-view-demo',
+ standalone: true,
+ imports: [CommonModule, SplitViewComponent],
+ template: `
+
+
Split View / Resizable Panels Demo
+
+
+
+ Basic Horizontal Split
+
+
+
+
Left Panel
+
This is the left panel content. Drag the divider to resize!
+
You can add any content here including forms, lists, or other components.
+
+
+
Right Panel
+
This is the right panel content.
+
Panels maintain their content during resize operations.
+
+
+
+
+
Usage: Drag the vertical divider to resize panels horizontally.
+
+
+
+
+
+ Basic Vertical Split
+
+
+
+
Top Panel
+
This is the top panel in a vertical split layout.
+
+
+
Bottom Panel
+
This is the bottom panel. The divider can be dragged vertically.
+
+
+
+
+
Usage: Drag the horizontal divider to resize panels vertically.
+
+
+
+
+
+ Three Panel Layout
+
+
+
+
Sidebar
+
Navigation or menu content
+
+
+
Main Content
+
Primary content area with the most space.
+
This panel takes up the majority of the available space.
+
+
+
Details Panel
+
Additional information or tools
+
+
+
+
+ Collapse Sidebar
+ Expand Sidebar
+ Collapse Details
+ Expand Details
+ Reset Sizes
+
+
+
Features: Multiple panels with programmatic collapse/expand controls.
+
+
+
+
+
+ Size Variants
+
+ Small Size
+
+
+
+
Small Split View
+
Compact layout with reduced spacing.
+
+
+
Right Panel
+
Smaller padding and gaps.
+
+
+
+
+ Large Size
+
+
+
+
Large Split View
+
Spacious layout with generous spacing and padding.
+
Perfect for complex layouts requiring more visual breathing room.
+
+
+
Right Panel
+
Enhanced spacing creates a more luxurious feel.
+
+
+
+
+
Sizes: sm (compact), md (default), lg (spacious) - affects padding and spacing.
+
+
+
+
+
+ Panel Size Constraints
+
+
+
+
Constrained Panel
+
Min: 150px, Max: 400px
+
Try resizing - this panel has size constraints!
+
+
+
Flexible Panel
+
No constraints - adapts to available space.
+
+
+
+
+
Constraints: Left panel has min-width of 150px and max-width of 400px.
+
+
+
+
+
+ Disabled State
+
+
+
+
Disabled Split View
+
Resizing is disabled - dividers cannot be dragged.
+
+
+
Static Panel
+
All interaction is disabled when the component is in disabled state.
+
+
+
+
+
Disabled: All resize functionality is disabled.
+
+
+
+
+
+ Nested Split Views
+
+
+
+
Left Panel
+
This is the outer left panel.
+
+
+
+
+
Nested Top
+
This is a nested split view inside the right panel.
+
+
+
Nested Bottom
+
Fully functional nested resizable panels.
+
+
+
+
+
+
+
Nesting: Split views can be nested to create complex layouts.
+
+
+
+
+
+ Resize Events
+
+
Last Resize Event: {{ lastResizeEvent || 'None' }}
+
Resize Count: {{ resizeCount }}
+
+
+
+ `,
+ styleUrl: './split-view-demo.component.scss'
+})
+export class SplitViewDemoComponent {
+ resizeCount = 0;
+ lastResizeEvent = '';
+
+ // Basic horizontal panels
+ basicHorizontalPanels: PanelConfig[] = [
+ { id: 'left-panel', size: '40%' },
+ { id: 'right-panel', size: '60%' }
+ ];
+
+ // Basic vertical panels
+ basicVerticalPanels: PanelConfig[] = [
+ { id: 'top-panel', size: '30%' },
+ { id: 'bottom-panel', size: '70%' }
+ ];
+
+ // Three panel layout
+ threePanelLayout: PanelConfig[] = [
+ { id: 'sidebar', size: '250px', minSize: 200, maxSize: 400, variant: 'secondary' },
+ { id: 'main-content', size: '1fr', variant: 'primary' },
+ { id: 'details-panel', size: '300px', minSize: 250, maxSize: 500, variant: 'elevated' }
+ ];
+
+ // Size variant panels
+ sizeVariantPanels: PanelConfig[] = [
+ { id: 'size-left', size: '50%' },
+ { id: 'size-right', size: '50%' }
+ ];
+
+ sizeVariantPanels2: PanelConfig[] = [
+ { id: 'size-left-lg', size: '40%' },
+ { id: 'size-right-lg', size: '60%' }
+ ];
+
+ // Constrained panels
+ constrainedPanels: PanelConfig[] = [
+ { id: 'constrained-left', size: '300px', minSize: 150, maxSize: 400 },
+ { id: 'constrained-right', size: '1fr' }
+ ];
+
+ // Nested panels
+ outerNestedPanels: PanelConfig[] = [
+ { id: 'outer-left', size: '30%' },
+ { id: 'outer-right', size: '70%' }
+ ];
+
+ innerNestedPanels: PanelConfig[] = [
+ { id: 'inner-top', size: '40%' },
+ { id: 'inner-bottom', size: '60%' }
+ ];
+
+ onPanelResized(event: ResizeEvent): void {
+ this.resizeCount++;
+ this.lastResizeEvent = `Panel ${event.panelId} resized to ${Math.round(event.newSize)}px (${event.direction})`;
+ console.log('Panel resized:', event);
+ }
+
+ collapsePanel(panelId: string): void {
+ const panel = this.threePanelLayout.find(p => p.id === panelId);
+ if (panel) {
+ panel.collapsed = true;
+ }
+ }
+
+ expandPanel(panelId: string): void {
+ const panel = this.threePanelLayout.find(p => p.id === panelId);
+ if (panel) {
+ panel.collapsed = false;
+ }
+ }
+
+ resetThreePanelSizes(): void {
+ this.threePanelLayout = [
+ { id: 'sidebar', size: '250px', minSize: 200, maxSize: 400, variant: 'secondary' },
+ { id: 'main-content', size: '1fr', variant: 'primary' },
+ { id: 'details-panel', size: '300px', minSize: 250, maxSize: 500, variant: 'elevated' }
+ ];
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/sticky-layout-demo/sticky-layout-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/sticky-layout-demo/sticky-layout-demo.component.scss
new file mode 100644
index 0000000..3afdc37
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/sticky-layout-demo/sticky-layout-demo.component.scss
@@ -0,0 +1,189 @@
+.demo-container {
+ padding: 2rem;
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+.demo-section {
+ margin-bottom: 3rem;
+
+ h3 {
+ margin-bottom: 1.5rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 1px solid #e0e0e0;
+ }
+
+ h4 {
+ margin-bottom: 1rem;
+ font-size: 1.1rem;
+ }
+}
+
+.demo-scroll-container {
+ height: 400px;
+ overflow-y: auto;
+ border: 2px solid #f0f0f0;
+ border-radius: 8px;
+ position: relative;
+ background: linear-gradient(to bottom, #fafafa, #f0f0f0);
+}
+
+.demo-content-spacer {
+ padding: 2rem;
+ min-height: 600px;
+
+ p {
+ margin-bottom: 2rem;
+ text-align: center;
+ font-weight: 500;
+ }
+}
+
+.demo-filler {
+ height: 100px;
+ margin: 1rem 0;
+ background: linear-gradient(135deg, #e3f2fd, #bbdefb);
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 500;
+ color: #1976d2;
+}
+
+.demo-content {
+ padding: 1rem 2rem;
+ background: #fff;
+ border-radius: 4px;
+ font-weight: 500;
+ text-align: center;
+ color: #333;
+}
+
+.demo-content-small {
+ padding: 0.75rem 1.5rem;
+ background: #fff;
+ border-radius: 4px;
+ font-weight: 500;
+ text-align: center;
+ color: #333;
+ font-size: 0.9rem;
+}
+
+.demo-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: 2rem;
+ margin-top: 1rem;
+}
+
+.demo-row {
+ display: flex;
+ gap: 1rem;
+ flex-wrap: wrap;
+ margin-top: 1rem;
+}
+
+.demo-variant-example {
+ padding: 1.5rem;
+ border: 1px solid #e0e0e0;
+ border-radius: 8px;
+ background: #fafafa;
+ position: relative;
+ min-height: 120px;
+
+ h4 {
+ position: absolute;
+ top: 0.5rem;
+ left: 1rem;
+ margin: 0;
+ font-size: 0.9rem;
+ color: #666;
+ }
+}
+
+.demo-effect-example {
+ padding: 1.5rem;
+ border: 1px solid #e0e0e0;
+ border-radius: 8px;
+ background: linear-gradient(135deg, #f8f9fa, #e9ecef);
+ position: relative;
+ min-height: 120px;
+
+ h4 {
+ position: absolute;
+ top: 0.5rem;
+ left: 1rem;
+ margin: 0;
+ font-size: 0.9rem;
+ color: #666;
+ }
+}
+
+.demo-controls {
+ display: flex;
+ gap: 1rem;
+ margin-bottom: 2rem;
+ flex-wrap: wrap;
+
+ label {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ font-weight: 500;
+
+ select {
+ padding: 0.5rem;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ min-width: 120px;
+ }
+
+ input[type="checkbox"] {
+ width: 18px;
+ height: 18px;
+ }
+ }
+}
+
+.demo-preview {
+ height: 300px;
+ border: 2px dashed #ddd;
+ border-radius: 8px;
+ position: relative;
+ overflow: auto;
+ background: linear-gradient(45deg, #f9f9f9 25%, transparent 25%),
+ linear-gradient(-45deg, #f9f9f9 25%, transparent 25%),
+ linear-gradient(45deg, transparent 75%, #f9f9f9 75%),
+ linear-gradient(-45deg, transparent 75%, #f9f9f9 75%);
+ background-size: 20px 20px;
+ background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
+}
+
+@media (max-width: 768px) {
+ .demo-container {
+ padding: 1rem;
+ }
+
+ .demo-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .demo-row {
+ flex-direction: column;
+ }
+
+ .demo-controls {
+ flex-direction: column;
+
+ label {
+ flex-direction: row;
+ align-items: center;
+
+ select {
+ min-width: auto;
+ flex: 1;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/sticky-layout-demo/sticky-layout-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/sticky-layout-demo/sticky-layout-demo.component.ts
new file mode 100644
index 0000000..0f2fade
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/sticky-layout-demo/sticky-layout-demo.component.ts
@@ -0,0 +1,229 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { StickyLayoutComponent } from 'ui-essentials';
+
+@Component({
+ selector: 'ui-sticky-layout-demo',
+ standalone: true,
+ imports: [CommonModule, FormsModule, StickyLayoutComponent],
+ template: `
+
+
Sticky Layout Demo
+
Scroll down to see the sticky positioning in action.
+
+
+
+
+
+
+ Variant Styles
+
+ @for (variant of variants; track variant.name) {
+
+
{{ variant.name }}
+
+
+ {{ variant.name }} Sticky
+
+
+
+ }
+
+
+
+
+
+ Offset Variants
+
+ @for (offset of offsets; track offset) {
+
+
+ Offset: {{ offset }}
+
+
+ }
+
+
+
+
+
+ Background & Effects
+
+
+
Blur Effect
+
+ Blurred Background
+
+
+
+
+
Backdrop
+
+ Backdrop Filter
+
+
+
+
+
Floating
+
+ Floating Style
+
+
+
+
+
Toolbar
+
+ Toolbar Style
+
+
+
+
+
+
+
+ Size Variants
+
+ @for (size of sizes; track size) {
+
+
+ Size: {{ size }}
+
+
+ }
+
+
+
+
+
+
+ `,
+ styleUrl: './sticky-layout-demo.component.scss'
+})
+export class StickyLayoutDemoComponent {
+ positions = ['top', 'bottom', 'left', 'right'] as const;
+ offsets = ['xs', 'sm', 'md', 'lg', 'xl'] as const;
+ sizes = ['auto', 'full', 'content'] as const;
+ variantOptions = ['default', 'header', 'sidebar', 'footer', 'floating', 'toolbar'] as const;
+
+ variants = [
+ { name: 'Header', variant: 'header' as const, position: 'top' as const, shadow: true, background: 'surface' as const },
+ { name: 'Sidebar', variant: 'sidebar' as const, position: 'left' as const, shadow: false, background: 'surface-secondary' as const },
+ { name: 'Footer', variant: 'footer' as const, position: 'bottom' as const, shadow: true, background: 'surface' as const },
+ { name: 'Floating', variant: 'floating' as const, position: 'top' as const, shadow: true, background: 'surface-elevated' as const },
+ { name: 'Toolbar', variant: 'toolbar' as const, position: 'top' as const, shadow: true, background: 'surface-elevated' as const }
+ ];
+
+ // Interactive controls
+ selectedPosition: typeof this.positions[number] = 'top';
+ selectedVariant: typeof this.variantOptions[number] = 'header';
+ enableShadow = true;
+ enableBorder = false;
+ enableBlur = false;
+
+ updateConfig(): void {
+ console.log('Sticky layout config updated:', {
+ position: this.selectedPosition,
+ variant: this.selectedVariant,
+ shadow: this.enableShadow,
+ border: this.enableBorder,
+ blur: this.enableBlur
+ });
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/textarea-demo/textarea-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/textarea-demo/textarea-demo.component.ts
new file mode 100644
index 0000000..9388333
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/textarea-demo/textarea-demo.component.ts
@@ -0,0 +1,591 @@
+import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { TextareaComponent } from '../../../../../ui-essentials/src/lib/components/forms/input/textarea.component';
+import { faComment, faEdit, faEnvelope } from '@fortawesome/free-solid-svg-icons';
+
+@Component({
+ selector: 'ui-textarea-demo',
+ standalone: true,
+ imports: [
+ CommonModule,
+ FormsModule,
+ TextareaComponent
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
Textarea Component Showcase
+
+
+
+ Basic Textarea
+
+
+
+
+
+
+
+
+
+ Size Variants
+
+
+
+
+
+
+
+
+
+ Style Variants
+
+
+
+
+
+
+
+
+
+ States and Validation
+
+
+
+
+
+
+
+
+
+
+ Interactive States
+
+
+
+
+
+
+
+
+
+
+ Character Limits and Counters
+
+
+
+
+
+
+
+
+
+ Rows and Resize Options
+
+
+
+
+
+
+
+
+
+
+
+ Icons and Clearable Features
+
+
+
+
+
+
+
+
+
+ Interactive Controls
+
+
+ Fill Sample Text
+
+
+ Clear All
+
+
+ {{ globalDisabled() ? 'Enable All' : 'Disable All' }}
+
+
+
+ @if (lastChange()) {
+
+ Last change: {{ lastChange() }}
+
+ }
+
+
+
+
+ Code Examples
+
+
Basic Usage:
+
<ui-textarea
+ label="Message"
+ placeholder="Enter your message..."
+ [(ngModel)]="message"
+ (textareaChange)="onMessageChange($event)">
+</ui-textarea>
+
+
With Character Limit:
+
<ui-textarea
+ label="Tweet"
+ placeholder="What's happening?"
+ [maxLength]="280"
+ [showCharacterCount]="true"
+ size="lg"
+ [(ngModel)]="tweetText">
+</ui-textarea>
+
+
Auto-resize with Validation:
+
<ui-textarea
+ label="Feedback"
+ [required]="true"
+ [autoResize]="true"
+ [clearable]="true"
+ state="error"
+ errorMessage="Please provide feedback"
+ [prefixIcon]="faComment"
+ [(ngModel)]="feedback">
+</ui-textarea>
+
+
Resize Options:
+
// Resize options:
+resize="none" // No resize
+resize="vertical" // Vertical only (default)
+resize="horizontal" // Horizontal only
+resize="both" // Both directions
+
+
+
+
+
+ Current Values
+
+
{{ getCurrentValues() }}
+
+
+
+ `,
+ styles: [`
+ h2 {
+ color: hsl(279, 14%, 11%);
+ font-size: 2rem;
+ margin-bottom: 2rem;
+ border-bottom: 2px solid hsl(258, 100%, 47%);
+ padding-bottom: 0.5rem;
+ }
+
+ h3 {
+ color: hsl(279, 14%, 25%);
+ font-size: 1.5rem;
+ margin-bottom: 1rem;
+ }
+
+ h4 {
+ color: hsl(279, 14%, 35%);
+ font-size: 1.2rem;
+ margin-bottom: 0.5rem;
+ }
+
+ pre {
+ margin: 0;
+ font-family: 'Courier New', monospace;
+ font-size: 0.9rem;
+ line-height: 1.4;
+ }
+
+ code {
+ color: hsl(279, 14%, 15%);
+ }
+
+ section {
+ border: 1px solid #e9ecef;
+ padding: 1.5rem;
+ border-radius: 8px;
+ background: #ffffff;
+ }
+
+ button:hover {
+ opacity: 0.9;
+ transform: translateY(-1px);
+ }
+ `]
+})
+export class TextareaDemoComponent {
+ // FontAwesome icons
+ readonly faComment = faComment;
+ readonly faEdit = faEdit;
+ readonly faEnvelope = faEnvelope;
+
+ // Basic textareas
+ basicTextarea1 = signal('');
+ basicTextarea2 = signal('');
+ basicTextarea3 = signal('This is pre-filled content that demonstrates how the textarea looks with existing text.');
+
+ // Size variants
+ sizeSmall = signal('');
+ sizeMedium = signal('');
+ sizeLarge = signal('');
+
+ // Style variants
+ variantOutlined = signal('');
+ variantFilled = signal('');
+ variantUnderlined = signal('');
+
+ // States
+ stateDefault = signal('');
+ stateSuccess = signal('Great job on completing this form!');
+ stateWarning = signal('Please review this content carefully.');
+ stateError = signal('This content has some issues.');
+
+ // Interactive states
+ stateDisabled = signal('This content cannot be edited');
+ stateReadonly = signal('This is read-only information that you can view but not modify');
+ stateRequired = signal('');
+ stateLoading = signal('');
+
+ // Character limits
+ charLimit1 = signal('');
+ charLimit2 = signal('');
+ charLimit3 = signal('');
+
+ // Rows and resize
+ rowsCompact = signal('');
+ rowsExpanded = signal('');
+ autoResizeTextarea = signal('Start typing and watch this textarea grow automatically as you add more content...');
+ horizontalResize = signal('');
+ bothResize = signal('');
+
+ // Icons and clearable
+ iconPrefix = signal('');
+ iconSuffix = signal('');
+ clearableTextarea = signal('You can clear this text by clicking the X button');
+
+ // Control states
+ globalDisabled = signal(false);
+ lastChange = signal('');
+
+ onTextareaChange(textareaId: string, value: string): void {
+ const preview = value.length > 50 ? value.substring(0, 50) + '...' : value;
+ this.lastChange.set(`${textareaId}: "${preview}" (${value.length} chars)`);
+ console.log(`Textarea ${textareaId} changed:`, value);
+ }
+
+ onClear(textareaId: string): void {
+ this.lastChange.set(`${textareaId}: cleared`);
+ console.log(`Textarea ${textareaId} cleared`);
+ }
+
+ fillSampleText(): void {
+ const sampleTexts = [
+ 'This is sample text for demonstration purposes.',
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
+ 'Here is some example content to show how the textarea handles different amounts of text.',
+ 'Sample content with multiple lines.\nSecond line of content.\nThird line for testing.',
+ 'Short text',
+ 'A longer piece of sample text that demonstrates how the textarea component handles various lengths of content and provides a good example for testing purposes.'
+ ];
+
+ this.basicTextarea1.set(sampleTexts[0]);
+ this.basicTextarea2.set(sampleTexts[1]);
+ this.sizeSmall.set(sampleTexts[4]);
+ this.sizeMedium.set(sampleTexts[2]);
+ this.sizeLarge.set(sampleTexts[5]);
+ this.variantOutlined.set(sampleTexts[0]);
+ this.variantFilled.set(sampleTexts[1]);
+ this.variantUnderlined.set(sampleTexts[2]);
+ this.stateDefault.set(sampleTexts[3]);
+ this.stateRequired.set(sampleTexts[0]);
+ this.stateLoading.set(sampleTexts[1]);
+ this.charLimit1.set('Sample text within limit');
+ this.charLimit2.set('This is a tweet-style message that fits within the character limit.');
+ this.charLimit3.set(sampleTexts[5]);
+ this.rowsCompact.set('Compact');
+ this.rowsExpanded.set(sampleTexts[3]);
+ this.iconPrefix.set('Comment with icon');
+ this.iconSuffix.set('Email content here');
+ this.clearableTextarea.set('This text can be cleared');
+
+ this.lastChange.set('All textareas filled with sample text');
+ }
+
+ clearAllTextareas(): void {
+ this.basicTextarea1.set('');
+ this.basicTextarea2.set('');
+ this.sizeSmall.set('');
+ this.sizeMedium.set('');
+ this.sizeLarge.set('');
+ this.variantOutlined.set('');
+ this.variantFilled.set('');
+ this.variantUnderlined.set('');
+ this.stateDefault.set('');
+ this.stateRequired.set('');
+ this.stateLoading.set('');
+ this.charLimit1.set('');
+ this.charLimit2.set('');
+ this.charLimit3.set('');
+ this.rowsCompact.set('');
+ this.rowsExpanded.set('');
+ this.autoResizeTextarea.set('');
+ this.horizontalResize.set('');
+ this.bothResize.set('');
+ this.iconPrefix.set('');
+ this.iconSuffix.set('');
+ this.clearableTextarea.set('');
+
+ this.lastChange.set('All textareas cleared');
+ }
+
+ toggleDisabled(): void {
+ this.globalDisabled.update(disabled => !disabled);
+ this.lastChange.set(`Global disabled: ${this.globalDisabled() ? 'enabled' : 'disabled'}`);
+ }
+
+ getCurrentValues(): string {
+ const values = {
+ basic: {
+ textarea1: this.basicTextarea1(),
+ textarea2: this.basicTextarea2(),
+ textarea3: this.basicTextarea3()
+ },
+ sizes: {
+ small: this.sizeSmall(),
+ medium: this.sizeMedium(),
+ large: this.sizeLarge()
+ },
+ variants: {
+ outlined: this.variantOutlined(),
+ filled: this.variantFilled(),
+ underlined: this.variantUnderlined()
+ },
+ states: {
+ default: this.stateDefault(),
+ success: this.stateSuccess(),
+ warning: this.stateWarning(),
+ error: this.stateError(),
+ disabled: this.stateDisabled(),
+ readonly: this.stateReadonly(),
+ required: this.stateRequired(),
+ loading: this.stateLoading()
+ },
+ characterLimits: {
+ limit100: this.charLimit1(),
+ limit280: this.charLimit2(),
+ limit1000: this.charLimit3()
+ },
+ rowsAndResize: {
+ compact: this.rowsCompact(),
+ expanded: this.rowsExpanded(),
+ autoResize: this.autoResizeTextarea(),
+ horizontal: this.horizontalResize(),
+ both: this.bothResize()
+ },
+ iconsAndClearable: {
+ prefix: this.iconPrefix(),
+ suffix: this.iconSuffix(),
+ clearable: this.clearableTextarea()
+ },
+ controls: {
+ globalDisabled: this.globalDisabled(),
+ lastChange: this.lastChange()
+ }
+ };
+
+ return JSON.stringify(values, null, 2);
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.component.ts b/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.component.ts
index d1efb3e..11b94f9 100644
--- a/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.component.ts
+++ b/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.component.ts
@@ -11,7 +11,7 @@ import {
faCloudUploadAlt, faFileText, faListAlt, faCircle, faExpandArrowsAlt, faCircleNotch, faSliders,
faMinus, faInfoCircle, faChevronDown, faCaretUp, faExclamationCircle, faSitemap, faStream,
faBell, faRoute, faChevronUp, faEllipsisV, faCut, faPalette, faExchangeAlt, faTools, faHeart, faCircleDot, faColumns,
- faFolderOpen, faDesktop, faListUl
+ faFolderOpen, faDesktop, faListUl, faArrowDown, faThumbtack
} from '@fortawesome/free-solid-svg-icons';
import { DemoRoutes } from '../../demos';
import { ScrollContainerComponent } from "../../../../../ui-essentials/src/public-api";
@@ -115,6 +115,8 @@ export class DashboardComponent {
faDesktop = faDesktop;
faNewspaper = faNewspaper;
faListUl = faListUl;
+ faArrowDown = faArrowDown;
+ faThumbtack = faThumbtack;
menuItems: any = []
@@ -235,15 +237,21 @@ export class DashboardComponent {
this.createChildItem("container", "Container", this.faBoxOpen),
this.createChildItem("feed-layout", "Feed Layout", this.faNewspaper),
this.createChildItem("flex", "Flex (Flexbox Container)", this.faArrowsAlt),
+ this.createChildItem("gallery-grid", "Gallery Grid", this.faImages),
this.createChildItem("grid-system", "Grid System", this.faGripVertical),
this.createChildItem("grid-container", "Grid Container", this.faThLarge),
+ this.createChildItem("infinite-scroll-container", "Infinite Scroll Container", this.faArrowDown),
+ this.createChildItem("kanban-board", "Kanban Board Layout", this.faColumns),
this.createChildItem("list-detail-layout", "List Detail Layout", this.faListUl),
+ this.createChildItem("masonry", "Masonry Layout", this.faThLarge),
this.createChildItem("scroll-container", "Scroll Container", this.faListAlt),
this.createChildItem("supporting-pane-layout", "Supporting Pane Layout", this.faWindowMaximize),
this.createChildItem("section", "Section (Semantic Wrapper)", this.faLayerGroup),
this.createChildItem("sidebar-layout", "Sidebar Layout", this.faBars),
this.createChildItem("spacer", "Spacer", this.faArrowsAlt),
+ this.createChildItem("split-view", "Split View / Resizable Panels", this.faColumns),
this.createChildItem("stack", "Stack (VStack/HStack)", this.faStream),
+ this.createChildItem("sticky-layout", "Sticky Layout", this.faThumbtack),
this.createChildItem("tabs-container", "Tabs Container", this.faFolderOpen),
this.createChildItem("dashboard-shell", "Dashboard Shell", this.faDesktop)
];
@@ -263,8 +271,18 @@ export class DashboardComponent {
// Utilities (standalone)
this.addMenuItem("fontawesome", "FontAwesome Icons", this.faCheckSquare);
+ this.addMenuItem("accessibility", "Accessability", this.faCheckSquare);
+ this.addMenuItem("animations", "Animations", this.faCheckSquare);
+ this.addMenuItem("hcl-studio", "HCL Themes", this.faCheckSquare);
+ this.addMenuItem("font-manager", "Font Manager", this.faCheckSquare);
+ this.addMenuItem("data-utils", "Data Utils", this.faCheckSquare);
+ this.addMenuItem("code-display", "Code Display", this.faCheckSquare);
+
+
}
+
+
menuClick(id: string) {
console.log(JSON.stringify(id))
diff --git a/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.sidebar.component.scss b/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.sidebar.component.scss
index 0fd2adb..f7c89c0 100644
--- a/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.sidebar.component.scss
+++ b/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.sidebar.component.scss
@@ -47,7 +47,6 @@
.children-container {
margin-left: 1rem;
padding-left: 1rem;
- border-left: 2px solid semantic.$semantic-color-border-secondary;
margin-top: 0.25rem;
::ng-deep .child-menu-item {
diff --git a/projects/demo-ui-essentials/src/styles.scss b/projects/demo-ui-essentials/src/styles.scss
index 3df943c..f8fc6ee 100644
--- a/projects/demo-ui-essentials/src/styles.scss
+++ b/projects/demo-ui-essentials/src/styles.scss
@@ -11,9 +11,19 @@ $comfortaa-font-path: "/fonts/comfortaa/" !default;
// Import project variables (which now has semantic tokens available)
@import 'scss/_variables';
+// Import UI Animations
+@import '../../ui-animations/src/styles/index';
+
html {
height: 100%;
font-size: 16px; // Base font size for rem calculations
+
+ // Define default CSS custom properties for fonts
+ // These can be overridden by the font manager
+ --font-family-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ --font-family-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
+ --font-family-mono: ui-monospace, SFMono-Regular, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+ --font-family-display: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
body {
@@ -23,8 +33,8 @@ body {
background-color: $semantic-color-surface-primary;
color: $semantic-color-text-primary;
- // Apply base typography using semantic tokens
- font-family: map-get($semantic-typography-body-medium, font-family);
+ // Apply base typography using CSS custom properties first, then fallback to semantic tokens
+ font-family: var(--font-family-sans, 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);
@@ -32,4 +42,37 @@ body {
// Improve text rendering
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
+
+ // Support font transitions
+ transition: font-family 0.3s ease-in-out;
+}
+
+// Global typography classes that respect font variables
+h1, h2, h3, h4, h5, h6 {
+ font-family: var(--font-family-display, var(--font-family-sans));
+ transition: font-family 0.3s ease-in-out;
+}
+
+// Serif elements
+blockquote, .serif {
+ font-family: var(--font-family-serif);
+ transition: font-family 0.3s ease-in-out;
+}
+
+// Monospace elements
+code, pre, kbd, samp, .mono {
+ font-family: var(--font-family-mono);
+ transition: font-family 0.3s ease-in-out;
+}
+
+// Sans-serif elements
+.sans {
+ font-family: var(--font-family-sans);
+ transition: font-family 0.3s ease-in-out;
+}
+
+// Display elements
+.display {
+ font-family: var(--font-family-display);
+ transition: font-family 0.3s ease-in-out;
}
diff --git a/projects/hcl-studio/README.md b/projects/hcl-studio/README.md
new file mode 100644
index 0000000..abaae42
--- /dev/null
+++ b/projects/hcl-studio/README.md
@@ -0,0 +1,63 @@
+# HclStudio
+
+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 hcl-studio
+```
+
+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/hcl-studio
+ ```
+
+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.
diff --git a/projects/hcl-studio/ng-package.json b/projects/hcl-studio/ng-package.json
new file mode 100644
index 0000000..c0a0993
--- /dev/null
+++ b/projects/hcl-studio/ng-package.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
+ "dest": "../../dist/hcl-studio",
+ "lib": {
+ "entryFile": "src/public-api.ts"
+ }
+}
\ No newline at end of file
diff --git a/projects/hcl-studio/package.json b/projects/hcl-studio/package.json
new file mode 100644
index 0000000..388b76f
--- /dev/null
+++ b/projects/hcl-studio/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "hcl-studio",
+ "version": "0.0.1",
+ "peerDependencies": {
+ "@angular/common": "^19.2.0",
+ "@angular/core": "^19.2.0"
+ },
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "sideEffects": false
+}
diff --git a/projects/hcl-studio/src/lib/core/hcl-converter.ts b/projects/hcl-studio/src/lib/core/hcl-converter.ts
new file mode 100644
index 0000000..4f3e15f
--- /dev/null
+++ b/projects/hcl-studio/src/lib/core/hcl-converter.ts
@@ -0,0 +1,324 @@
+/**
+ * ==========================================================================
+ * HCL COLOR CONVERTER
+ * ==========================================================================
+ * Accurate color space conversions between RGB, LAB, and HCL
+ * Based on CIE LAB color space for perceptual uniformity
+ * ==========================================================================
+ */
+
+import { RGB, LAB, HCL } from '../models/hcl.models';
+
+export class HCLConverter {
+
+ // ==========================================================================
+ // RGB ↔ HCL CONVERSIONS
+ // ==========================================================================
+
+ /**
+ * Convert RGB to HCL color space
+ * @param r Red component (0-255)
+ * @param g Green component (0-255)
+ * @param b Blue component (0-255)
+ * @returns HCL color
+ */
+ static rgbToHcl(r: number, g: number, b: number): HCL {
+ const lab = this.rgbToLab(r, g, b);
+ return this.labToHcl(lab.l, lab.a, lab.b);
+ }
+
+ /**
+ * Convert HCL to RGB color space
+ * @param h Hue (0-360)
+ * @param c Chroma (0-100+)
+ * @param l Lightness (0-100)
+ * @returns RGB color
+ */
+ static hclToRgb(h: number, c: number, l: number): RGB {
+ const lab = this.hclToLab(h, c, l);
+ return this.labToRgb(lab.l, lab.a, lab.b);
+ }
+
+ // ==========================================================================
+ // HEX CONVERSIONS
+ // ==========================================================================
+
+ /**
+ * Convert hex string to HCL
+ * @param hex Hex color string (#RRGGBB or #RGB)
+ * @returns HCL color
+ */
+ static hexToHcl(hex: string): HCL {
+ const rgb = this.hexToRgb(hex);
+ return this.rgbToHcl(rgb.r, rgb.g, rgb.b);
+ }
+
+ /**
+ * Convert HCL to hex string
+ * @param h Hue (0-360)
+ * @param c Chroma (0-100+)
+ * @param l Lightness (0-100)
+ * @returns Hex color string
+ */
+ static hclToHex(h: number, c: number, l: number): string {
+ const rgb = this.hclToRgb(h, c, l);
+ return this.rgbToHex(rgb.r, rgb.g, rgb.b);
+ }
+
+ // ==========================================================================
+ // RGB ↔ LAB CONVERSIONS (via XYZ)
+ // ==========================================================================
+
+ /**
+ * Convert RGB to LAB color space
+ * @param r Red component (0-255)
+ * @param g Green component (0-255)
+ * @param b Blue component (0-255)
+ * @returns LAB color
+ */
+ static rgbToLab(r: number, g: number, b: number): LAB {
+ // Convert RGB to XYZ first
+ let rNorm = r / 255;
+ let gNorm = g / 255;
+ let bNorm = b / 255;
+
+ // Apply gamma correction (sRGB)
+ rNorm = rNorm > 0.04045 ? Math.pow((rNorm + 0.055) / 1.055, 2.4) : rNorm / 12.92;
+ gNorm = gNorm > 0.04045 ? Math.pow((gNorm + 0.055) / 1.055, 2.4) : gNorm / 12.92;
+ bNorm = bNorm > 0.04045 ? Math.pow((bNorm + 0.055) / 1.055, 2.4) : bNorm / 12.92;
+
+ // Convert to XYZ using sRGB matrix
+ let x = rNorm * 0.4124564 + gNorm * 0.3575761 + bNorm * 0.1804375;
+ let y = rNorm * 0.2126729 + gNorm * 0.7151522 + bNorm * 0.0721750;
+ let z = rNorm * 0.0193339 + gNorm * 0.1191920 + bNorm * 0.9503041;
+
+ // Normalize by D65 illuminant
+ x = x / 0.95047;
+ y = y / 1.00000;
+ z = z / 1.08883;
+
+ // Convert XYZ to LAB
+ const fx = x > 0.008856 ? Math.pow(x, 1/3) : (7.787 * x + 16/116);
+ const fy = y > 0.008856 ? Math.pow(y, 1/3) : (7.787 * y + 16/116);
+ const fz = z > 0.008856 ? Math.pow(z, 1/3) : (7.787 * z + 16/116);
+
+ const l = (116 * fy) - 16;
+ const a = 500 * (fx - fy);
+ const bValue = 200 * (fy - fz);
+
+ return { l: Math.max(0, Math.min(100, l)), a, b: bValue };
+ }
+
+ /**
+ * Convert LAB to RGB color space
+ * @param l Lightness (0-100)
+ * @param a Green-Red axis
+ * @param b Blue-Yellow axis
+ * @returns RGB color
+ */
+ static labToRgb(l: number, a: number, b: number): RGB {
+ // Convert LAB to XYZ
+ const fy = (l + 16) / 116;
+ const fx = a / 500 + fy;
+ const fz = fy - b / 200;
+
+ const x = (fx > 0.206897 ? Math.pow(fx, 3) : (fx - 16/116) / 7.787) * 0.95047;
+ const y = (fy > 0.206897 ? Math.pow(fy, 3) : (fy - 16/116) / 7.787) * 1.00000;
+ const z = (fz > 0.206897 ? Math.pow(fz, 3) : (fz - 16/116) / 7.787) * 1.08883;
+
+ // Convert XYZ to RGB
+ let r = x * 3.2404542 + y * -1.5371385 + z * -0.4985314;
+ let g = x * -0.9692660 + y * 1.8760108 + z * 0.0415560;
+ let b2 = x * 0.0556434 + y * -0.2040259 + z * 1.0572252;
+
+ // Apply inverse gamma correction
+ r = r > 0.0031308 ? 1.055 * Math.pow(r, 1/2.4) - 0.055 : 12.92 * r;
+ g = g > 0.0031308 ? 1.055 * Math.pow(g, 1/2.4) - 0.055 : 12.92 * g;
+ b2 = b2 > 0.0031308 ? 1.055 * Math.pow(b2, 1/2.4) - 0.055 : 12.92 * b2;
+
+ // Clamp to valid RGB range
+ const rFinal = Math.max(0, Math.min(255, Math.round(r * 255)));
+ const gFinal = Math.max(0, Math.min(255, Math.round(g * 255)));
+ const bFinal = Math.max(0, Math.min(255, Math.round(b2 * 255)));
+
+ return { r: rFinal, g: gFinal, b: bFinal };
+ }
+
+ // ==========================================================================
+ // LAB ↔ HCL CONVERSIONS
+ // ==========================================================================
+
+ /**
+ * Convert LAB to HCL (cylindrical representation)
+ * @param l Lightness (0-100)
+ * @param a Green-Red axis
+ * @param b Blue-Yellow axis
+ * @returns HCL color
+ */
+ static labToHcl(l: number, a: number, b: number): HCL {
+ const c = Math.sqrt(a * a + b * b);
+ let h = Math.atan2(b, a) * (180 / Math.PI);
+
+ // Normalize hue to 0-360
+ if (h < 0) h += 360;
+
+ return {
+ h: isNaN(h) ? 0 : h,
+ c: isNaN(c) ? 0 : c,
+ l: Math.max(0, Math.min(100, l))
+ };
+ }
+
+ /**
+ * Convert HCL to LAB color space
+ * @param h Hue (0-360)
+ * @param c Chroma (0-100+)
+ * @param l Lightness (0-100)
+ * @returns LAB color
+ */
+ static hclToLab(h: number, c: number, l: number): LAB {
+ const hRad = (h * Math.PI) / 180;
+ const a = c * Math.cos(hRad);
+ const b = c * Math.sin(hRad);
+
+ return { l, a, b };
+ }
+
+ // ==========================================================================
+ // HEX UTILITY METHODS
+ // ==========================================================================
+
+ /**
+ * Convert hex string to RGB
+ * @param hex Hex color string
+ * @returns RGB color
+ */
+ static hexToRgb(hex: string): RGB {
+ // Remove # if present
+ hex = hex.replace('#', '');
+
+ // Handle 3-digit hex
+ if (hex.length === 3) {
+ hex = hex.split('').map(h => h + h).join('');
+ }
+
+ if (hex.length !== 6) {
+ throw new Error(`Invalid hex color: ${hex}`);
+ }
+
+ const r = parseInt(hex.substr(0, 2), 16);
+ const g = parseInt(hex.substr(2, 2), 16);
+ const b = parseInt(hex.substr(4, 2), 16);
+
+ return { r, g, b };
+ }
+
+ /**
+ * Convert RGB to hex string
+ * @param r Red component (0-255)
+ * @param g Green component (0-255)
+ * @param b Blue component (0-255)
+ * @returns Hex color string
+ */
+ static rgbToHex(r: number, g: number, b: number): string {
+ const toHex = (n: number) => {
+ const hex = Math.max(0, Math.min(255, Math.round(n))).toString(16);
+ return hex.length === 1 ? '0' + hex : hex;
+ };
+
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
+ }
+
+ // ==========================================================================
+ // COLOR MANIPULATION METHODS
+ // ==========================================================================
+
+ /**
+ * Adjust lightness of a color
+ * @param hex Original color
+ * @param amount Lightness adjustment (-100 to 100)
+ * @returns Adjusted color
+ */
+ static adjustLightness(hex: string, amount: number): string {
+ const hcl = this.hexToHcl(hex);
+ const newL = Math.max(0, Math.min(100, hcl.l + amount));
+ return this.hclToHex(hcl.h, hcl.c, newL);
+ }
+
+ /**
+ * Adjust chroma (saturation) of a color
+ * @param hex Original color
+ * @param amount Chroma adjustment (-100 to 100)
+ * @returns Adjusted color
+ */
+ static adjustChroma(hex: string, amount: number): string {
+ const hcl = this.hexToHcl(hex);
+ const newC = Math.max(0, hcl.c + amount);
+ return this.hclToHex(hcl.h, newC, hcl.l);
+ }
+
+ /**
+ * Adjust hue of a color
+ * @param hex Original color
+ * @param amount Hue adjustment in degrees (-360 to 360)
+ * @returns Adjusted color
+ */
+ static adjustHue(hex: string, amount: number): string {
+ const hcl = this.hexToHcl(hex);
+ let newH = hcl.h + amount;
+
+ // Normalize hue to 0-360
+ while (newH < 0) newH += 360;
+ while (newH >= 360) newH -= 360;
+
+ return this.hclToHex(newH, hcl.c, hcl.l);
+ }
+
+ // ==========================================================================
+ // VALIDATION METHODS
+ // ==========================================================================
+
+ /**
+ * Validate if a string is a valid hex color
+ * @param hex Color string to validate
+ * @returns True if valid hex color
+ */
+ static isValidHex(hex: string): boolean {
+ try {
+ const normalizedHex = hex.replace('#', '');
+ return /^[0-9A-Fa-f]{3}$|^[0-9A-Fa-f]{6}$/.test(normalizedHex);
+ } catch {
+ return false;
+ }
+ }
+
+ /**
+ * Calculate relative luminance of a color (for contrast calculation)
+ * @param hex Hex color string
+ * @returns Relative luminance (0-1)
+ */
+ static getRelativeLuminance(hex: string): number {
+ const rgb = this.hexToRgb(hex);
+ const [r, g, b] = [rgb.r, rgb.g, rgb.b].map(c => {
+ c = c / 255;
+ return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
+ });
+
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
+ }
+
+ /**
+ * Calculate contrast ratio between two colors
+ * @param color1 First color
+ * @param color2 Second color
+ * @returns Contrast ratio (1-21)
+ */
+ static getContrastRatio(color1: string, color2: string): number {
+ const lum1 = this.getRelativeLuminance(color1);
+ const lum2 = this.getRelativeLuminance(color2);
+ const lighter = Math.max(lum1, lum2);
+ const darker = Math.min(lum1, lum2);
+
+ return (lighter + 0.05) / (darker + 0.05);
+ }
+}
\ No newline at end of file
diff --git a/projects/hcl-studio/src/lib/core/palette-generator.ts b/projects/hcl-studio/src/lib/core/palette-generator.ts
new file mode 100644
index 0000000..f89bc48
--- /dev/null
+++ b/projects/hcl-studio/src/lib/core/palette-generator.ts
@@ -0,0 +1,353 @@
+/**
+ * ==========================================================================
+ * PALETTE GENERATOR
+ * ==========================================================================
+ * Generate Material Design 3 compliant color palettes using HCL color space
+ * Creates perceptually uniform tonal variations with proper contrast ratios
+ * ==========================================================================
+ */
+
+import { HCLConverter } from './hcl-converter';
+import { TonalPalette, MaterialPalette, BrandColors, HCL } from '../models/hcl.models';
+
+export class PaletteGenerator {
+
+ // ==========================================================================
+ // MATERIAL DESIGN 3 TONE LEVELS
+ // ==========================================================================
+
+ private static readonly TONE_LEVELS = [0, 4, 5, 6, 10, 12, 15, 17, 20, 22, 25, 30, 35, 40, 50, 60, 70, 80, 87, 90, 92, 94, 95, 96, 98, 99, 100] as const;
+
+ // ==========================================================================
+ // MAIN PALETTE GENERATION
+ // ==========================================================================
+
+ /**
+ * Generate complete Material Design 3 palette from brand colors
+ * @param brandColors Primary, secondary, tertiary seed colors
+ * @param mode Light or dark mode
+ * @returns Complete Material Design 3 palette
+ */
+ static generateMaterialPalette(brandColors: BrandColors, mode: 'light' | 'dark' = 'light'): MaterialPalette {
+ return {
+ primary: this.generateTonalPalette(brandColors.primary),
+ secondary: this.generateTonalPalette(brandColors.secondary),
+ tertiary: this.generateTonalPalette(brandColors.tertiary),
+ neutral: this.generateNeutralPalette(brandColors.primary),
+ neutralVariant: this.generateNeutralVariantPalette(brandColors.primary),
+ error: this.generateTonalPalette(brandColors.error || '#B3261E') // Material Design default error
+ };
+ }
+
+ /**
+ * Generate tonal palette from seed color
+ * @param seedColor Hex color string
+ * @returns Tonal palette with all tone levels
+ */
+ static generateTonalPalette(seedColor: string): TonalPalette {
+ const seedHcl = HCLConverter.hexToHcl(seedColor);
+
+ // Create palette object with all tone levels
+ const palette = {} as TonalPalette;
+
+ this.TONE_LEVELS.forEach(tone => {
+ palette[tone as keyof TonalPalette] = this.generateTone(seedHcl, tone);
+ });
+
+ return palette;
+ }
+
+ /**
+ * Generate neutral palette from primary color
+ * @param primaryColor Primary seed color
+ * @returns Neutral tonal palette
+ */
+ static generateNeutralPalette(primaryColor: string): TonalPalette {
+ const primaryHcl = HCLConverter.hexToHcl(primaryColor);
+
+ // Create neutral variation with reduced chroma and adjusted hue
+ const neutralHcl: HCL = {
+ h: primaryHcl.h,
+ c: Math.min(primaryHcl.c * 0.08, 8), // Very low chroma for neutral
+ l: primaryHcl.l
+ };
+
+ const palette = {} as TonalPalette;
+
+ this.TONE_LEVELS.forEach(tone => {
+ palette[tone as keyof TonalPalette] = this.generateTone(neutralHcl, tone);
+ });
+
+ return palette;
+ }
+
+ /**
+ * Generate neutral variant palette from primary color
+ * @param primaryColor Primary seed color
+ * @returns Neutral variant tonal palette
+ */
+ static generateNeutralVariantPalette(primaryColor: string): TonalPalette {
+ const primaryHcl = HCLConverter.hexToHcl(primaryColor);
+
+ // Create neutral variant with slightly more chroma than neutral
+ const neutralVariantHcl: HCL = {
+ h: primaryHcl.h,
+ c: Math.min(primaryHcl.c * 0.16, 16), // Low chroma but higher than neutral
+ l: primaryHcl.l
+ };
+
+ const palette = {} as TonalPalette;
+
+ this.TONE_LEVELS.forEach(tone => {
+ palette[tone as keyof TonalPalette] = this.generateTone(neutralVariantHcl, tone);
+ });
+
+ return palette;
+ }
+
+ // ==========================================================================
+ // TONE GENERATION
+ // ==========================================================================
+
+ /**
+ * Generate specific tone from HCL seed
+ * @param seedHcl Seed color in HCL space
+ * @param tone Target tone level (0-100)
+ * @returns Hex color string
+ */
+ private static generateTone(seedHcl: HCL, tone: number): string {
+ // Apply tone-specific chroma adjustments for better visual consistency
+ const chromaMultiplier = this.getChromaMultiplier(tone);
+ const adjustedChroma = seedHcl.c * chromaMultiplier;
+
+ // Create tone with adjusted lightness and chroma
+ const toneHcl: HCL = {
+ h: seedHcl.h,
+ c: Math.max(0, adjustedChroma),
+ l: tone
+ };
+
+ return HCLConverter.hclToHex(toneHcl.h, toneHcl.c, toneHcl.l);
+ }
+
+ /**
+ * Get chroma multiplier based on tone level
+ * Reduces chroma at very light and very dark tones for better visual balance
+ * @param tone Tone level (0-100)
+ * @returns Chroma multiplier (0-1)
+ */
+ private static getChromaMultiplier(tone: number): number {
+ // Reduce chroma at extreme lightness values
+ if (tone <= 10) return 0.6 + (tone / 10) * 0.3; // 0.6 to 0.9
+ if (tone >= 90) return 1.1 - ((tone - 90) / 10) * 0.5; // 1.1 to 0.6
+ if (tone >= 80) return 1.0 + ((tone - 80) / 10) * 0.1; // 1.0 to 1.1
+
+ // Full chroma in middle ranges
+ return 1.0;
+ }
+
+ // ==========================================================================
+ // COLOR HARMONIES
+ // ==========================================================================
+
+ /**
+ * Generate complementary color from seed
+ * @param seedColor Seed color
+ * @returns Complementary color
+ */
+ static generateComplementary(seedColor: string): string {
+ const hcl = HCLConverter.hexToHcl(seedColor);
+ const compHue = (hcl.h + 180) % 360;
+ return HCLConverter.hclToHex(compHue, hcl.c, hcl.l);
+ }
+
+ /**
+ * Generate analogous colors from seed
+ * @param seedColor Seed color
+ * @param count Number of analogous colors to generate
+ * @returns Array of analogous colors
+ */
+ static generateAnalogous(seedColor: string, count: number = 3): string[] {
+ const hcl = HCLConverter.hexToHcl(seedColor);
+ const colors: string[] = [];
+ const step = 30; // 30-degree steps
+
+ for (let i = 0; i < count; i++) {
+ const hue = (hcl.h + (i - Math.floor(count / 2)) * step) % 360;
+ const adjustedHue = hue < 0 ? hue + 360 : hue;
+ colors.push(HCLConverter.hclToHex(adjustedHue, hcl.c, hcl.l));
+ }
+
+ return colors;
+ }
+
+ /**
+ * Generate triadic colors from seed
+ * @param seedColor Seed color
+ * @returns Array of triadic colors (including original)
+ */
+ static generateTriadic(seedColor: string): string[] {
+ const hcl = HCLConverter.hexToHcl(seedColor);
+ return [
+ seedColor,
+ HCLConverter.hclToHex((hcl.h + 120) % 360, hcl.c, hcl.l),
+ HCLConverter.hclToHex((hcl.h + 240) % 360, hcl.c, hcl.l)
+ ];
+ }
+
+ /**
+ * Generate tetradic colors from seed
+ * @param seedColor Seed color
+ * @returns Array of tetradic colors
+ */
+ static generateTetradic(seedColor: string): string[] {
+ const hcl = HCLConverter.hexToHcl(seedColor);
+ return [
+ seedColor,
+ HCLConverter.hclToHex((hcl.h + 90) % 360, hcl.c, hcl.l),
+ HCLConverter.hclToHex((hcl.h + 180) % 360, hcl.c, hcl.l),
+ HCLConverter.hclToHex((hcl.h + 270) % 360, hcl.c, hcl.l)
+ ];
+ }
+
+ // ==========================================================================
+ // ACCESSIBILITY HELPERS
+ // ==========================================================================
+
+ /**
+ * Ensure minimum contrast ratio for text readability
+ * @param backgroundColor Background color
+ * @param textColor Text color
+ * @param minRatio Minimum contrast ratio (default: 4.5 for WCAG AA)
+ * @returns Adjusted text color
+ */
+ static ensureContrast(backgroundColor: string, textColor: string, minRatio: number = 4.5): string {
+ const currentRatio = HCLConverter.getContrastRatio(backgroundColor, textColor);
+
+ if (currentRatio >= minRatio) {
+ return textColor; // Already meets contrast requirements
+ }
+
+ const textHcl = HCLConverter.hexToHcl(textColor);
+ const backgroundLuminance = HCLConverter.getRelativeLuminance(backgroundColor);
+
+ // Adjust lightness to meet contrast requirements
+ let adjustedL = textHcl.l;
+ let bestColor = textColor;
+ let bestRatio = currentRatio;
+
+ // Try both lighter and darker variations
+ for (let lightness = 0; lightness <= 100; lightness += 5) {
+ const testColor = HCLConverter.hclToHex(textHcl.h, textHcl.c, lightness);
+ const testRatio = HCLConverter.getContrastRatio(backgroundColor, testColor);
+
+ if (testRatio >= minRatio && testRatio > bestRatio) {
+ bestColor = testColor;
+ bestRatio = testRatio;
+ adjustedL = lightness;
+ }
+ }
+
+ return bestColor;
+ }
+
+ /**
+ * Get optimal text color (black or white) for background
+ * @param backgroundColor Background color
+ * @returns Optimal text color (#000000 or #FFFFFF)
+ */
+ static getOptimalTextColor(backgroundColor: string): string {
+ const blackRatio = HCLConverter.getContrastRatio(backgroundColor, '#000000');
+ const whiteRatio = HCLConverter.getContrastRatio(backgroundColor, '#FFFFFF');
+
+ return blackRatio > whiteRatio ? '#000000' : '#FFFFFF';
+ }
+
+ // ==========================================================================
+ // PALETTE UTILITIES
+ // ==========================================================================
+
+ /**
+ * Get primary tone for specific use case
+ * @param palette Tonal palette
+ * @param usage Usage context ('main', 'container', 'on-container', etc.)
+ * @param mode Light or dark mode
+ * @returns Appropriate tone
+ */
+ static getPrimaryTone(palette: TonalPalette, usage: 'main' | 'container' | 'on-container' | 'surface', mode: 'light' | 'dark' = 'light'): string {
+ if (mode === 'light') {
+ switch (usage) {
+ case 'main': return palette[40];
+ case 'container': return palette[90];
+ case 'on-container': return palette[10];
+ case 'surface': return palette[98];
+ default: return palette[40];
+ }
+ } else {
+ switch (usage) {
+ case 'main': return palette[80];
+ case 'container': return palette[30];
+ case 'on-container': return palette[90];
+ case 'surface': return palette[10];
+ default: return palette[80];
+ }
+ }
+ }
+
+ /**
+ * Validate palette for accessibility and visual consistency
+ * @param palette Material palette to validate
+ * @returns Validation results
+ */
+ static validatePalette(palette: MaterialPalette): { isValid: boolean; issues: string[] } {
+ const issues: string[] = [];
+
+ // Check contrast ratios for common combinations
+ const primaryMain = palette.primary[40];
+ const primaryContainer = palette.primary[90];
+ const onPrimary = palette.primary[100];
+ const onPrimaryContainer = palette.primary[10];
+
+ // Validate primary combinations
+ const primaryContrastRatio = HCLConverter.getContrastRatio(primaryMain, onPrimary);
+ if (primaryContrastRatio < 4.5) {
+ issues.push(`Primary contrast ratio too low: ${primaryContrastRatio.toFixed(2)}`);
+ }
+
+ const containerContrastRatio = HCLConverter.getContrastRatio(primaryContainer, onPrimaryContainer);
+ if (containerContrastRatio < 4.5) {
+ issues.push(`Container contrast ratio too low: ${containerContrastRatio.toFixed(2)}`);
+ }
+
+ // Additional checks could be added here (color difference, chroma consistency, etc.)
+
+ return {
+ isValid: issues.length === 0,
+ issues
+ };
+ }
+
+ // ==========================================================================
+ // PREVIEW GENERATION
+ // ==========================================================================
+
+ /**
+ * Generate color preview for theme selection UI
+ * @param brandColors Brand colors
+ * @returns Preview color object
+ */
+ static generateColorPreview(brandColors: BrandColors): { [key: string]: string } {
+ const palette = this.generateMaterialPalette(brandColors);
+
+ return {
+ 'Primary': palette.primary[40],
+ 'Primary Container': palette.primary[90],
+ 'Secondary': palette.secondary[40],
+ 'Secondary Container': palette.secondary[90],
+ 'Tertiary': palette.tertiary[40],
+ 'Tertiary Container': palette.tertiary[90],
+ 'Surface': palette.neutral[98],
+ 'Surface Container': palette.neutral[94]
+ };
+ }
+}
\ No newline at end of file
diff --git a/projects/hcl-studio/src/lib/hcl-studio.component.spec.ts b/projects/hcl-studio/src/lib/hcl-studio.component.spec.ts
new file mode 100644
index 0000000..280a03d
--- /dev/null
+++ b/projects/hcl-studio/src/lib/hcl-studio.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { HclStudioComponent } from './hcl-studio.component';
+
+describe('HclStudioComponent', () => {
+ let component: HclStudioComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [HclStudioComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(HclStudioComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/projects/hcl-studio/src/lib/hcl-studio.component.ts b/projects/hcl-studio/src/lib/hcl-studio.component.ts
new file mode 100644
index 0000000..8397386
--- /dev/null
+++ b/projects/hcl-studio/src/lib/hcl-studio.component.ts
@@ -0,0 +1,15 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'lib-hcl-studio',
+ imports: [],
+ template: `
+
+ hcl-studio works!
+
+ `,
+ styles: ``
+})
+export class HclStudioComponent {
+
+}
diff --git a/projects/hcl-studio/src/lib/hcl-studio.service.spec.ts b/projects/hcl-studio/src/lib/hcl-studio.service.spec.ts
new file mode 100644
index 0000000..926531e
--- /dev/null
+++ b/projects/hcl-studio/src/lib/hcl-studio.service.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { HclStudioService } from './hcl-studio.service';
+
+describe('HclStudioService', () => {
+ let service: HclStudioService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(HclStudioService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/projects/hcl-studio/src/lib/hcl-studio.service.ts b/projects/hcl-studio/src/lib/hcl-studio.service.ts
new file mode 100644
index 0000000..ba8aaeb
--- /dev/null
+++ b/projects/hcl-studio/src/lib/hcl-studio.service.ts
@@ -0,0 +1,512 @@
+/**
+ * ==========================================================================
+ * HCL STUDIO SERVICE
+ * ==========================================================================
+ * Main service for HCL-based color palette generation and theme management
+ * Provides complete theme switching, persistence, and reactive updates
+ * ==========================================================================
+ */
+
+import { Injectable } from '@angular/core';
+import { BehaviorSubject, Observable } from 'rxjs';
+
+import {
+ ThemeConfig,
+ Theme,
+ BrandColors,
+ MaterialPalette,
+ ThemePreview,
+ ThemeCollection,
+ ThemeState,
+ HCLStudioOptions,
+ ColorValidation
+} from './models/hcl.models';
+
+import { PaletteGenerator } from './core/palette-generator';
+import { HCLConverter } from './core/hcl-converter';
+import { CSSVariableManager } from './themes/css-variable-manager';
+import { ThemeStorage } from './themes/theme-storage';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class HCLStudioService {
+
+ // ==========================================================================
+ // REACTIVE STATE
+ // ==========================================================================
+
+ private _themeState$ = new BehaviorSubject({
+ currentTheme: null,
+ availableThemes: [],
+ mode: 'light',
+ isInitialized: false
+ });
+
+ private _currentMode$ = new BehaviorSubject<'light' | 'dark'>('light');
+
+ // Public observables
+ public readonly themeState$: Observable = this._themeState$.asObservable();
+ public readonly currentMode$: Observable<'light' | 'dark'> = this._currentMode$.asObservable();
+
+ // ==========================================================================
+ // PRIVATE STATE
+ // ==========================================================================
+
+ private builtInThemes = new Map();
+ private customThemes = new Map();
+ private currentTheme: Theme | null = null;
+ private currentMode: 'light' | 'dark' = 'light';
+ private options: HCLStudioOptions = {};
+
+ // ==========================================================================
+ // INITIALIZATION
+ // ==========================================================================
+
+ /**
+ * Initialize HCL Studio with options and themes
+ * @param options Initialization options
+ */
+ initialize(options: HCLStudioOptions = {}): void {
+ this.options = {
+ autoMode: false,
+ storageKey: 'hcl-studio',
+ validation: {
+ enableContrastCheck: true,
+ enableColorBlindCheck: false,
+ minContrastRatio: 4.5
+ },
+ ...options
+ };
+
+ // Load built-in themes if provided
+ if (options.themes) {
+ this.registerThemes(options.themes);
+ }
+
+ // Load custom themes from storage
+ this.loadCustomThemesFromStorage();
+
+ // Restore user's last theme and mode
+ this.restoreUserPreferences();
+
+ // Apply default theme if specified
+ if (options.defaultTheme) {
+ if (typeof options.defaultTheme === 'string') {
+ this.switchTheme(options.defaultTheme);
+ } else {
+ this.applyThemeConfig(options.defaultTheme);
+ }
+ }
+
+ // Set up system theme detection if enabled
+ if (options.autoMode) {
+ this.setupAutoModeDetection();
+ }
+
+ this.updateThemeState();
+ this.markAsInitialized();
+ }
+
+ /**
+ * Quick initialize with brand colors
+ * @param brandColors Primary, secondary, tertiary colors
+ * @param mode Initial mode
+ */
+ initializeWithColors(brandColors: BrandColors, mode: 'light' | 'dark' = 'light'): void {
+ const themeConfig: ThemeConfig = {
+ id: 'brand-theme',
+ name: 'Brand Theme',
+ colors: brandColors,
+ mode,
+ createdAt: new Date()
+ };
+
+ this.initialize({
+ defaultTheme: themeConfig,
+ autoMode: false
+ });
+ }
+
+ // ==========================================================================
+ // THEME MANAGEMENT
+ // ==========================================================================
+
+ /**
+ * Register multiple themes
+ * @param themes Theme collection
+ */
+ registerThemes(themes: ThemeCollection): void {
+ Object.entries(themes).forEach(([id, config]) => {
+ const themeConfig: ThemeConfig = { id, ...config };
+ const theme = this.createCompleteTheme(themeConfig);
+ this.builtInThemes.set(id, theme);
+ });
+
+ this.updateThemeState();
+ }
+
+ /**
+ * Create and save custom theme
+ * @param id Theme identifier
+ * @param name Theme display name
+ * @param brandColors Brand colors
+ * @param description Optional description
+ * @returns Created theme
+ */
+ createCustomTheme(id: string, name: string, brandColors: BrandColors, description?: string): Theme {
+ const themeConfig: ThemeConfig = {
+ id,
+ name,
+ colors: brandColors,
+ description,
+ createdAt: new Date()
+ };
+
+ const theme = this.createCompleteTheme(themeConfig);
+ this.customThemes.set(id, theme);
+
+ // Save to storage
+ ThemeStorage.saveCustomTheme(themeConfig);
+
+ this.updateThemeState();
+ return theme;
+ }
+
+ /**
+ * Switch to different theme
+ * @param themeId Theme identifier
+ * @param mode Optional mode override
+ */
+ switchTheme(themeId: string, mode?: 'light' | 'dark'): void {
+ const theme = this.getTheme(themeId);
+ if (!theme) {
+ console.warn(`Theme not found: ${themeId}`);
+ return;
+ }
+
+ this.currentTheme = theme;
+
+ if (mode) {
+ this.currentMode = mode;
+ this._currentMode$.next(mode);
+ ThemeStorage.saveCurrentMode(mode);
+ }
+
+ // Apply the palette
+ const palette = this.currentMode === 'light' ? theme.lightPalette : theme.darkPalette;
+ CSSVariableManager.applyPaletteWithTransition(palette, this.currentMode);
+
+ // Save preference
+ ThemeStorage.saveCurrentTheme(themeId);
+
+ this.updateThemeState();
+ }
+
+ /**
+ * Apply theme configuration directly
+ * @param themeConfig Theme configuration
+ */
+ applyThemeConfig(themeConfig: ThemeConfig): void {
+ const theme = this.createCompleteTheme(themeConfig);
+ this.currentTheme = theme;
+
+ const resolvedMode = themeConfig.mode === 'auto' ? this.currentMode : (themeConfig.mode || this.currentMode);
+ const palette = resolvedMode === 'light' ? theme.lightPalette : theme.darkPalette;
+
+ CSSVariableManager.applyPaletteWithTransition(palette, resolvedMode);
+
+ this.currentMode = resolvedMode;
+ this._currentMode$.next(resolvedMode);
+
+ this.updateThemeState();
+ }
+
+ /**
+ * Toggle between light and dark mode
+ */
+ toggleMode(): void {
+ const newMode = this.currentMode === 'light' ? 'dark' : 'light';
+ this.switchMode(newMode);
+ }
+
+ /**
+ * Switch to specific mode
+ * @param mode Target mode
+ */
+ switchMode(mode: 'light' | 'dark'): void {
+ if (this.currentMode === mode) return;
+
+ this.currentMode = mode;
+ this._currentMode$.next(mode);
+
+ if (this.currentTheme) {
+ const palette = mode === 'light' ? this.currentTheme.lightPalette : this.currentTheme.darkPalette;
+ CSSVariableManager.applyPaletteWithTransition(palette, mode);
+ }
+
+ ThemeStorage.saveCurrentMode(mode);
+ this.updateThemeState();
+ }
+
+ // ==========================================================================
+ // COLOR UTILITIES
+ // ==========================================================================
+
+ /**
+ * Generate color preview for theme selection
+ * @param brandColors Brand colors
+ * @returns Preview color object
+ */
+ generateColorPreview(brandColors: BrandColors): { [key: string]: string } {
+ return PaletteGenerator.generateColorPreview(brandColors);
+ }
+
+ /**
+ * Validate color format and accessibility
+ * @param color Color string to validate
+ * @returns Validation results
+ */
+ validateColor(color: string): ColorValidation {
+ const warnings: string[] = [];
+ const recommendations: string[] = [];
+
+ // Check format validity
+ if (!HCLConverter.isValidHex(color)) {
+ warnings.push('Invalid hex color format');
+ return { isValid: false, warnings, recommendations };
+ }
+
+ // Check if color is too light/dark for accessibility
+ const hcl = HCLConverter.hexToHcl(color);
+
+ if (hcl.l < 10) {
+ warnings.push('Color may be too dark for some use cases');
+ recommendations.push('Consider using lighter tones for better contrast');
+ }
+
+ if (hcl.l > 90) {
+ warnings.push('Color may be too light for some use cases');
+ recommendations.push('Consider using darker tones for better contrast');
+ }
+
+ // Check chroma levels
+ if (hcl.c < 10) {
+ recommendations.push('Low chroma - color may appear washed out');
+ }
+
+ if (hcl.c > 80) {
+ recommendations.push('High chroma - consider reducing saturation for better balance');
+ }
+
+ return {
+ isValid: warnings.length === 0,
+ warnings,
+ recommendations: recommendations.length > 0 ? recommendations : undefined
+ };
+ }
+
+ // ==========================================================================
+ // GETTERS
+ // ==========================================================================
+
+ /**
+ * Get current theme state
+ * @returns Current theme state
+ */
+ getCurrentThemeState(): ThemeState {
+ return this._themeState$.value;
+ }
+
+ /**
+ * Get current theme
+ * @returns Current theme or null
+ */
+ getCurrentTheme(): Theme | null {
+ return this.currentTheme;
+ }
+
+ /**
+ * Get current mode
+ * @returns Current mode
+ */
+ getCurrentMode(): 'light' | 'dark' {
+ return this.currentMode;
+ }
+
+ /**
+ * Get all available themes
+ * @returns Array of theme previews
+ */
+ getAvailableThemes(): ThemePreview[] {
+ const themes: ThemePreview[] = [];
+
+ // Add built-in themes
+ this.builtInThemes.forEach(theme => {
+ themes.push({
+ id: theme.config.id,
+ name: theme.config.name,
+ description: theme.config.description,
+ primary: theme.config.colors.primary,
+ secondary: theme.config.colors.secondary,
+ tertiary: theme.config.colors.tertiary
+ });
+ });
+
+ // Add custom themes
+ this.customThemes.forEach(theme => {
+ themes.push({
+ id: theme.config.id,
+ name: theme.config.name,
+ description: theme.config.description,
+ primary: theme.config.colors.primary,
+ secondary: theme.config.colors.secondary,
+ tertiary: theme.config.colors.tertiary
+ });
+ });
+
+ return themes;
+ }
+
+ /**
+ * Check if service is initialized
+ * @returns True if initialized
+ */
+ isInitialized(): boolean {
+ return this._themeState$.value.isInitialized;
+ }
+
+ // ==========================================================================
+ // IMPORT/EXPORT
+ // ==========================================================================
+
+ /**
+ * Export theme configuration
+ * @param themeId Theme to export
+ * @returns JSON string or null
+ */
+ exportTheme(themeId: string): string | null {
+ return ThemeStorage.exportTheme(themeId);
+ }
+
+ /**
+ * Import theme from JSON
+ * @param jsonString Theme JSON data
+ * @returns Imported theme or null
+ */
+ importTheme(jsonString: string): ThemeConfig | null {
+ const imported = ThemeStorage.importTheme(jsonString);
+ if (imported) {
+ // Reload custom themes to include the new one
+ this.loadCustomThemesFromStorage();
+ this.updateThemeState();
+ }
+ return imported;
+ }
+
+ // ==========================================================================
+ // PRIVATE METHODS
+ // ==========================================================================
+
+ /**
+ * Create complete theme with both light and dark palettes
+ * @param config Theme configuration
+ * @returns Complete theme
+ */
+ private createCompleteTheme(config: ThemeConfig): Theme {
+ const lightPalette = PaletteGenerator.generateMaterialPalette(config.colors, 'light');
+ const darkPalette = PaletteGenerator.generateMaterialPalette(config.colors, 'dark');
+
+ const lightCSSVariables = CSSVariableManager.generateCSSVariables(lightPalette, 'light');
+ const darkCSSVariables = CSSVariableManager.generateCSSVariables(darkPalette, 'dark');
+
+ return {
+ config,
+ lightPalette,
+ darkPalette,
+ cssVariables: lightCSSVariables // Default to light mode variables
+ };
+ }
+
+ /**
+ * Get theme by ID (checks both built-in and custom)
+ * @param themeId Theme identifier
+ * @returns Theme or null
+ */
+ private getTheme(themeId: string): Theme | null {
+ return this.builtInThemes.get(themeId) || this.customThemes.get(themeId) || null;
+ }
+
+ /**
+ * Load custom themes from storage
+ */
+ private loadCustomThemesFromStorage(): void {
+ const customThemes = ThemeStorage.getCustomThemes();
+
+ Object.values(customThemes).forEach(config => {
+ const theme = this.createCompleteTheme(config);
+ this.customThemes.set(config.id, theme);
+ });
+ }
+
+ /**
+ * Restore user's theme preferences
+ */
+ private restoreUserPreferences(): void {
+ const savedThemeId = ThemeStorage.getCurrentTheme();
+ const savedMode = ThemeStorage.getCurrentMode();
+
+ this.currentMode = savedMode;
+ this._currentMode$.next(savedMode);
+
+ if (savedThemeId && this.getTheme(savedThemeId)) {
+ this.switchTheme(savedThemeId, savedMode);
+ }
+ }
+
+ /**
+ * Set up automatic mode detection based on system preference
+ */
+ private setupAutoModeDetection(): void {
+ if (typeof window !== 'undefined' && window.matchMedia) {
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
+
+ const handleChange = (e: MediaQueryListEvent) => {
+ const systemMode = e.matches ? 'dark' : 'light';
+ this.switchMode(systemMode);
+ };
+
+ // Set initial mode
+ const initialSystemMode = mediaQuery.matches ? 'dark' : 'light';
+ this.switchMode(initialSystemMode);
+
+ // Listen for changes
+ mediaQuery.addEventListener('change', handleChange);
+ }
+ }
+
+ /**
+ * Update reactive theme state
+ */
+ private updateThemeState(): void {
+ const state: ThemeState = {
+ currentTheme: this.currentTheme,
+ availableThemes: this.getAvailableThemes(),
+ mode: this.currentMode,
+ isInitialized: this._themeState$.value.isInitialized
+ };
+
+ this._themeState$.next(state);
+ }
+
+ /**
+ * Mark service as initialized
+ */
+ private markAsInitialized(): void {
+ const currentState = this._themeState$.value;
+ this._themeState$.next({
+ ...currentState,
+ isInitialized: true
+ });
+ }
+}
\ No newline at end of file
diff --git a/projects/hcl-studio/src/lib/models/hcl.models.ts b/projects/hcl-studio/src/lib/models/hcl.models.ts
new file mode 100644
index 0000000..2c1855c
--- /dev/null
+++ b/projects/hcl-studio/src/lib/models/hcl.models.ts
@@ -0,0 +1,239 @@
+/**
+ * ==========================================================================
+ * HCL STUDIO - TYPE DEFINITIONS
+ * ==========================================================================
+ * Core interfaces and types for HCL color manipulation and theme management
+ * ==========================================================================
+ */
+
+// ==========================================================================
+// COLOR TYPES
+// ==========================================================================
+
+/**
+ * HCL color representation
+ */
+export interface HCL {
+ /** Hue (0-360 degrees) */
+ h: number;
+ /** Chroma (0-100+) */
+ c: number;
+ /** Lightness (0-100) */
+ l: number;
+}
+
+/**
+ * RGB color representation
+ */
+export interface RGB {
+ /** Red (0-255) */
+ r: number;
+ /** Green (0-255) */
+ g: number;
+ /** Blue (0-255) */
+ b: number;
+}
+
+/**
+ * LAB color representation (intermediate for HCL conversion)
+ */
+export interface LAB {
+ /** Lightness (0-100) */
+ l: number;
+ /** Green-Red axis (-128 to 127) */
+ a: number;
+ /** Blue-Yellow axis (-128 to 127) */
+ b: number;
+}
+
+// ==========================================================================
+// PALETTE TYPES
+// ==========================================================================
+
+/**
+ * Tonal palette with Material Design 3 tone levels
+ */
+export interface TonalPalette {
+ 0: string;
+ 4: string;
+ 5: string;
+ 6: string;
+ 10: string;
+ 12: string;
+ 15: string;
+ 17: string;
+ 20: string;
+ 22: string;
+ 25: string;
+ 30: string;
+ 35: string;
+ 40: string;
+ 50: string;
+ 60: string;
+ 70: string;
+ 80: string;
+ 87: string;
+ 90: string;
+ 92: string;
+ 94: string;
+ 95: string;
+ 96: string;
+ 98: string;
+ 99: string;
+ 100: string;
+}
+
+/**
+ * Complete Material Design 3 color palette
+ */
+export interface MaterialPalette {
+ primary: TonalPalette;
+ secondary: TonalPalette;
+ tertiary: TonalPalette;
+ neutral: TonalPalette;
+ neutralVariant: TonalPalette;
+ error: TonalPalette;
+}
+
+/**
+ * Brand seed colors
+ */
+export interface BrandColors {
+ primary: string;
+ secondary: string;
+ tertiary: string;
+ error?: string;
+}
+
+// ==========================================================================
+// THEME TYPES
+// ==========================================================================
+
+/**
+ * Theme configuration
+ */
+export interface ThemeConfig {
+ id: string;
+ name: string;
+ colors: BrandColors;
+ mode?: 'light' | 'dark' | 'auto';
+ createdAt?: Date;
+ description?: string;
+}
+
+/**
+ * Complete theme with generated palettes
+ */
+export interface Theme {
+ config: ThemeConfig;
+ lightPalette: MaterialPalette;
+ darkPalette: MaterialPalette;
+ cssVariables: CSSVariables;
+}
+
+/**
+ * CSS custom properties mapping
+ */
+export interface CSSVariables {
+ [key: string]: string;
+}
+
+/**
+ * Theme preview information
+ */
+export interface ThemePreview {
+ id: string;
+ name: string;
+ description?: string;
+ primary: string;
+ secondary: string;
+ tertiary: string;
+}
+
+/**
+ * Theme collection
+ */
+export interface ThemeCollection {
+ [themeId: string]: Omit;
+}
+
+// ==========================================================================
+// VALIDATION TYPES
+// ==========================================================================
+
+/**
+ * Color validation result
+ */
+export interface ColorValidation {
+ isValid: boolean;
+ warnings: string[];
+ recommendations?: string[];
+ accessibility?: AccessibilityInfo;
+}
+
+/**
+ * Accessibility information
+ */
+export interface AccessibilityInfo {
+ contrastRatio: number;
+ wcagLevel: 'AA' | 'AAA' | 'fail';
+ colorBlindSafe: boolean;
+}
+
+// ==========================================================================
+// SERVICE CONFIGURATION TYPES
+// ==========================================================================
+
+/**
+ * HCL Studio initialization options
+ */
+export interface HCLStudioOptions {
+ /** Default theme to apply */
+ defaultTheme?: string | ThemeConfig;
+ /** Enable automatic mode switching based on system preference */
+ autoMode?: boolean;
+ /** Storage key for theme persistence */
+ storageKey?: string;
+ /** Custom theme presets */
+ themes?: ThemeCollection;
+ /** Validation options */
+ validation?: {
+ enableContrastCheck: boolean;
+ enableColorBlindCheck: boolean;
+ minContrastRatio: number;
+ };
+}
+
+/**
+ * Theme state for reactive updates
+ */
+export interface ThemeState {
+ currentTheme: Theme | null;
+ availableThemes: ThemePreview[];
+ mode: 'light' | 'dark';
+ isInitialized: boolean;
+}
+
+// ==========================================================================
+// UTILITY TYPES
+// ==========================================================================
+
+/**
+ * Color format types
+ */
+export type ColorFormat = 'hex' | 'rgb' | 'hsl' | 'hcl';
+
+/**
+ * Mode switching options
+ */
+export type ThemeMode = 'light' | 'dark' | 'auto';
+
+/**
+ * Contrast levels for WCAG compliance
+ */
+export type ContrastLevel = 'AA' | 'AAA';
+
+/**
+ * Color harmony types
+ */
+export type ColorHarmony = 'complementary' | 'analogous' | 'triadic' | 'tetradic' | 'monochromatic';
\ No newline at end of file
diff --git a/projects/hcl-studio/src/lib/themes/css-variable-manager.ts b/projects/hcl-studio/src/lib/themes/css-variable-manager.ts
new file mode 100644
index 0000000..44f2b98
--- /dev/null
+++ b/projects/hcl-studio/src/lib/themes/css-variable-manager.ts
@@ -0,0 +1,320 @@
+/**
+ * ==========================================================================
+ * CSS VARIABLE MANAGER
+ * ==========================================================================
+ * Manages CSS custom properties for dynamic theme switching
+ * Maps Material Design 3 palette to existing design system variables
+ * ==========================================================================
+ */
+
+import { MaterialPalette, CSSVariables } from '../models/hcl.models';
+import { PaletteGenerator } from '../core/palette-generator';
+
+export class CSSVariableManager {
+
+ // ==========================================================================
+ // VARIABLE MAPPING
+ // ==========================================================================
+
+ /**
+ * Generate CSS variables from Material Design 3 palette
+ * @param palette Material palette
+ * @param mode Light or dark mode
+ * @returns CSS variables object
+ */
+ static generateCSSVariables(palette: MaterialPalette, mode: 'light' | 'dark' = 'light'): CSSVariables {
+ const variables: CSSVariables = {};
+
+ // ==========================================================================
+ // PRIMARY COLORS
+ // ==========================================================================
+ variables['--color-primary-key'] = palette.primary[40];
+ variables['--color-primary'] = PaletteGenerator.getPrimaryTone(palette.primary, 'main', mode);
+ variables['--color-on-primary'] = mode === 'light' ? palette.primary[100] : palette.primary[20];
+ variables['--color-primary-container'] = PaletteGenerator.getPrimaryTone(palette.primary, 'container', mode);
+ variables['--color-on-primary-container'] = PaletteGenerator.getPrimaryTone(palette.primary, 'on-container', mode);
+ variables['--color-inverse-primary'] = mode === 'light' ? palette.primary[80] : palette.primary[40];
+
+ // ==========================================================================
+ // SECONDARY COLORS
+ // ==========================================================================
+ variables['--color-secondary-key'] = palette.secondary[40];
+ variables['--color-secondary'] = mode === 'light' ? palette.secondary[40] : palette.secondary[80];
+ variables['--color-on-secondary'] = mode === 'light' ? palette.secondary[100] : palette.secondary[20];
+ variables['--color-secondary-container'] = mode === 'light' ? palette.secondary[90] : palette.secondary[30];
+ variables['--color-on-secondary-container'] = mode === 'light' ? palette.secondary[10] : palette.secondary[90];
+
+ // ==========================================================================
+ // TERTIARY COLORS
+ // ==========================================================================
+ variables['--color-tertiary-key'] = palette.tertiary[40];
+ variables['--color-tertiary'] = mode === 'light' ? palette.tertiary[40] : palette.tertiary[80];
+ variables['--color-on-tertiary'] = mode === 'light' ? palette.tertiary[100] : palette.tertiary[20];
+ variables['--color-tertiary-container'] = mode === 'light' ? palette.tertiary[90] : palette.tertiary[30];
+ variables['--color-on-tertiary-container'] = mode === 'light' ? palette.tertiary[10] : palette.tertiary[90];
+
+ // ==========================================================================
+ // ERROR COLORS
+ // ==========================================================================
+ variables['--color-error-key'] = palette.error[40];
+ variables['--color-error'] = mode === 'light' ? palette.error[40] : palette.error[80];
+ variables['--color-on-error'] = mode === 'light' ? palette.error[100] : palette.error[20];
+ variables['--color-error-container'] = mode === 'light' ? palette.error[90] : palette.error[30];
+ variables['--color-on-error-container'] = mode === 'light' ? palette.error[10] : palette.error[90];
+
+ // ==========================================================================
+ // NEUTRAL COLORS
+ // ==========================================================================
+ variables['--color-neutral-key'] = palette.neutral[10];
+ variables['--color-neutral'] = mode === 'light' ? palette.neutral[25] : palette.neutral[80];
+ variables['--color-neutral-variant-key'] = palette.neutralVariant[15];
+ variables['--color-neutral-variant'] = mode === 'light' ? palette.neutralVariant[30] : palette.neutralVariant[70];
+
+ // ==========================================================================
+ // SURFACE COLORS (The most important for your existing system)
+ // ==========================================================================
+ if (mode === 'light') {
+ variables['--color-surface'] = palette.neutral[99];
+ variables['--color-surface-bright'] = palette.neutral[99];
+ variables['--color-surface-dim'] = palette.neutral[87];
+ variables['--color-on-surface'] = palette.neutral[10];
+ variables['--color-surface-lowest'] = palette.neutral[100];
+ variables['--color-surface-low'] = palette.neutral[96];
+ variables['--color-surface-container'] = palette.neutral[94];
+ variables['--color-surface-high'] = palette.neutral[92];
+ variables['--color-surface-highest'] = palette.neutral[90];
+ variables['--color-surface-variant'] = palette.neutralVariant[90];
+ variables['--color-on-surface-variant'] = palette.neutralVariant[30];
+ variables['--color-inverse-surface'] = palette.neutral[20];
+ variables['--color-inverse-on-surface'] = palette.neutral[95];
+ } else {
+ variables['--color-surface'] = palette.neutral[10];
+ variables['--color-surface-bright'] = palette.neutral[20];
+ variables['--color-surface-dim'] = palette.neutral[6];
+ variables['--color-on-surface'] = palette.neutral[90];
+ variables['--color-surface-lowest'] = palette.neutral[0];
+ variables['--color-surface-low'] = palette.neutral[4];
+ variables['--color-surface-container'] = palette.neutral[12];
+ variables['--color-surface-high'] = palette.neutral[17];
+ variables['--color-surface-highest'] = palette.neutral[22];
+ variables['--color-surface-variant'] = palette.neutralVariant[30];
+ variables['--color-on-surface-variant'] = palette.neutralVariant[80];
+ variables['--color-inverse-surface'] = palette.neutral[90];
+ variables['--color-inverse-on-surface'] = palette.neutral[20];
+ }
+
+ // ==========================================================================
+ // BACKGROUND COLORS
+ // ==========================================================================
+ variables['--color-background'] = variables['--color-surface'];
+ variables['--color-on-background'] = variables['--color-on-surface'];
+
+ // ==========================================================================
+ // OUTLINE COLORS
+ // ==========================================================================
+ variables['--color-outline'] = mode === 'light' ? palette.neutralVariant[50] : palette.neutralVariant[60];
+ variables['--color-outline-variant'] = mode === 'light' ? palette.neutralVariant[80] : palette.neutralVariant[30];
+
+ // ==========================================================================
+ // UTILITY COLORS
+ // ==========================================================================
+ variables['--color-shadow'] = '#000000';
+ variables['--color-surface-tint'] = variables['--color-primary'];
+ variables['--color-scrim'] = '#000000';
+
+ return variables;
+ }
+
+ // ==========================================================================
+ // APPLY METHODS
+ // ==========================================================================
+
+ /**
+ * Apply CSS variables to document root
+ * @param variables CSS variables to apply
+ */
+ static applyVariables(variables: CSSVariables): void {
+ const root = document.documentElement;
+
+ Object.entries(variables).forEach(([property, value]) => {
+ root.style.setProperty(property, value);
+ });
+ }
+
+ /**
+ * Apply palette to document root
+ * @param palette Material palette
+ * @param mode Light or dark mode
+ */
+ static applyPalette(palette: MaterialPalette, mode: 'light' | 'dark' = 'light'): void {
+ const variables = this.generateCSSVariables(palette, mode);
+ this.applyVariables(variables);
+ }
+
+ /**
+ * Remove all theme-related CSS variables
+ */
+ static clearVariables(): void {
+ const root = document.documentElement;
+ const variableNames = this.getAllVariableNames();
+
+ variableNames.forEach(name => {
+ root.style.removeProperty(name);
+ });
+ }
+
+ // ==========================================================================
+ // UTILITY METHODS
+ // ==========================================================================
+
+ /**
+ * Get all CSS variable names managed by HCL Studio
+ * @returns Array of CSS variable names
+ */
+ private static getAllVariableNames(): string[] {
+ return [
+ // Primary
+ '--color-primary-key',
+ '--color-primary',
+ '--color-on-primary',
+ '--color-primary-container',
+ '--color-on-primary-container',
+ '--color-inverse-primary',
+
+ // Secondary
+ '--color-secondary-key',
+ '--color-secondary',
+ '--color-on-secondary',
+ '--color-secondary-container',
+ '--color-on-secondary-container',
+
+ // Tertiary
+ '--color-tertiary-key',
+ '--color-tertiary',
+ '--color-on-tertiary',
+ '--color-tertiary-container',
+ '--color-on-tertiary-container',
+
+ // Error
+ '--color-error-key',
+ '--color-error',
+ '--color-on-error',
+ '--color-error-container',
+ '--color-on-error-container',
+
+ // Neutral
+ '--color-neutral-key',
+ '--color-neutral',
+ '--color-neutral-variant-key',
+ '--color-neutral-variant',
+
+ // Surface
+ '--color-surface',
+ '--color-surface-bright',
+ '--color-surface-dim',
+ '--color-on-surface',
+ '--color-surface-lowest',
+ '--color-surface-low',
+ '--color-surface-container',
+ '--color-surface-high',
+ '--color-surface-highest',
+ '--color-surface-variant',
+ '--color-on-surface-variant',
+ '--color-inverse-surface',
+ '--color-inverse-on-surface',
+
+ // Background
+ '--color-background',
+ '--color-on-background',
+
+ // Outline
+ '--color-outline',
+ '--color-outline-variant',
+
+ // Utility
+ '--color-shadow',
+ '--color-surface-tint',
+ '--color-scrim'
+ ];
+ }
+
+ /**
+ * Get current CSS variable value
+ * @param variableName CSS variable name (with or without --)
+ * @returns Current variable value
+ */
+ static getCurrentVariableValue(variableName: string): string {
+ const name = variableName.startsWith('--') ? variableName : `--${variableName}`;
+ return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
+ }
+
+ /**
+ * Check if dark mode is currently active
+ * @returns True if dark mode is active
+ */
+ static isDarkModeActive(): boolean {
+ // Check if surface color is dark (low lightness)
+ const surfaceColor = this.getCurrentVariableValue('--color-surface');
+ if (!surfaceColor) return false;
+
+ try {
+ // Simple check: if surface is closer to black than white, it's dark mode
+ const surfaceHex = surfaceColor.startsWith('#') ? surfaceColor : '#FFFFFF';
+ const surfaceRgb = this.hexToRgb(surfaceHex);
+ const brightness = (surfaceRgb.r * 299 + surfaceRgb.g * 587 + surfaceRgb.b * 114) / 1000;
+ return brightness < 128;
+ } catch {
+ return false;
+ }
+ }
+
+ /**
+ * Simple hex to RGB conversion for utility
+ * @param hex Hex color
+ * @returns RGB values
+ */
+ private static hexToRgb(hex: string): { r: number, g: number, b: number } {
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+ return result ? {
+ r: parseInt(result[1], 16),
+ g: parseInt(result[2], 16),
+ b: parseInt(result[3], 16)
+ } : { r: 0, g: 0, b: 0 };
+ }
+
+ // ==========================================================================
+ // ANIMATION SUPPORT
+ // ==========================================================================
+
+ /**
+ * Apply variables with smooth transition
+ * @param variables CSS variables to apply
+ * @param duration Transition duration in milliseconds
+ */
+ static applyVariablesWithTransition(variables: CSSVariables, duration: number = 300): void {
+ const root = document.documentElement;
+
+ // Add transition for smooth theme changes
+ const transitionProperties = Object.keys(variables).join(', ');
+ root.style.setProperty('transition', `${transitionProperties} ${duration}ms ease-in-out`);
+
+ // Apply variables
+ this.applyVariables(variables);
+
+ // Remove transition after animation completes
+ setTimeout(() => {
+ root.style.removeProperty('transition');
+ }, duration);
+ }
+
+ /**
+ * Apply palette with smooth transition
+ * @param palette Material palette
+ * @param mode Light or dark mode
+ * @param duration Transition duration in milliseconds
+ */
+ static applyPaletteWithTransition(palette: MaterialPalette, mode: 'light' | 'dark' = 'light', duration: number = 300): void {
+ const variables = this.generateCSSVariables(palette, mode);
+ this.applyVariablesWithTransition(variables, duration);
+ }
+}
\ No newline at end of file
diff --git a/projects/hcl-studio/src/lib/themes/theme-presets.ts b/projects/hcl-studio/src/lib/themes/theme-presets.ts
new file mode 100644
index 0000000..18be674
--- /dev/null
+++ b/projects/hcl-studio/src/lib/themes/theme-presets.ts
@@ -0,0 +1,357 @@
+/**
+ * ==========================================================================
+ * DEFAULT THEME PRESETS
+ * ==========================================================================
+ * Built-in theme configurations with carefully selected color combinations
+ * Provides professionally designed themes for immediate use
+ * ==========================================================================
+ */
+
+import { ThemeCollection } from '../models/hcl.models';
+
+/**
+ * Default theme collection with professionally curated color combinations
+ */
+export const DEFAULT_THEMES: ThemeCollection = {
+
+ // ==========================================================================
+ // MATERIAL DESIGN THEMES
+ // ==========================================================================
+
+ 'material-purple': {
+ name: 'Material Purple',
+ description: 'Classic Material Design purple theme',
+ colors: {
+ primary: '#6750A4',
+ secondary: '#625B71',
+ tertiary: '#7D5260'
+ }
+ },
+
+ 'material-blue': {
+ name: 'Material Blue',
+ description: 'Material Design blue theme',
+ colors: {
+ primary: '#1976D2',
+ secondary: '#455A64',
+ tertiary: '#7B1FA2'
+ }
+ },
+
+ 'material-green': {
+ name: 'Material Green',
+ description: 'Material Design green theme',
+ colors: {
+ primary: '#2E7D32',
+ secondary: '#5D4037',
+ tertiary: '#1976D2'
+ }
+ },
+
+ // ==========================================================================
+ // CORPORATE/BUSINESS THEMES
+ // ==========================================================================
+
+ 'corporate-blue': {
+ name: 'Corporate Blue',
+ description: 'Professional blue theme for business applications',
+ colors: {
+ primary: '#1565C0',
+ secondary: '#37474F',
+ tertiary: '#5E35B1'
+ }
+ },
+
+ 'executive': {
+ name: 'Executive',
+ description: 'Sophisticated theme with navy and gold accents',
+ colors: {
+ primary: '#263238',
+ secondary: '#455A64',
+ tertiary: '#FF8F00'
+ }
+ },
+
+ 'modern-corporate': {
+ name: 'Modern Corporate',
+ description: 'Contemporary corporate theme with teal accents',
+ colors: {
+ primary: '#00695C',
+ secondary: '#424242',
+ tertiary: '#7B1FA2'
+ }
+ },
+
+ // ==========================================================================
+ // NATURE-INSPIRED THEMES
+ // ==========================================================================
+
+ 'forest': {
+ name: 'Forest',
+ description: 'Deep greens inspired by forest landscapes',
+ colors: {
+ primary: '#2E7D32',
+ secondary: '#5D4037',
+ tertiary: '#FF6F00'
+ }
+ },
+
+ 'ocean': {
+ name: 'Ocean',
+ description: 'Cool blues and teals reminiscent of ocean depths',
+ colors: {
+ primary: '#006064',
+ secondary: '#263238',
+ tertiary: '#00ACC1'
+ }
+ },
+
+ 'sunset': {
+ name: 'Sunset',
+ description: 'Warm oranges and deep purples of a sunset sky',
+ colors: {
+ primary: '#FF6F00',
+ secondary: '#5D4037',
+ tertiary: '#7B1FA2'
+ }
+ },
+
+ 'lavender': {
+ name: 'Lavender Fields',
+ description: 'Soft purples and greens inspired by lavender fields',
+ colors: {
+ primary: '#7E57C2',
+ secondary: '#689F38',
+ tertiary: '#8BC34A'
+ }
+ },
+
+ // ==========================================================================
+ // VIBRANT/CREATIVE THEMES
+ // ==========================================================================
+
+ 'electric': {
+ name: 'Electric',
+ description: 'High-energy theme with electric blues and purples',
+ colors: {
+ primary: '#3F51B5',
+ secondary: '#9C27B0',
+ tertiary: '#00BCD4'
+ }
+ },
+
+ 'neon': {
+ name: 'Neon',
+ description: 'Bold neon-inspired theme for creative applications',
+ colors: {
+ primary: '#E91E63',
+ secondary: '#9C27B0',
+ tertiary: '#00BCD4'
+ }
+ },
+
+ 'cyberpunk': {
+ name: 'Cyberpunk',
+ description: 'Futuristic theme with cyan and magenta accents',
+ colors: {
+ primary: '#00E5FF',
+ secondary: '#263238',
+ tertiary: '#FF1744'
+ }
+ },
+
+ // ==========================================================================
+ // WARM/COZY THEMES
+ // ==========================================================================
+
+ 'autumn': {
+ name: 'Autumn',
+ description: 'Warm autumn colors with orange and brown tones',
+ colors: {
+ primary: '#FF5722',
+ secondary: '#5D4037',
+ tertiary: '#FF9800'
+ }
+ },
+
+ 'coffee': {
+ name: 'Coffee House',
+ description: 'Rich browns and warm tones for a cozy feel',
+ colors: {
+ primary: '#5D4037',
+ secondary: '#8D6E63',
+ tertiary: '#FF8F00'
+ }
+ },
+
+ 'golden': {
+ name: 'Golden',
+ description: 'Luxurious theme with gold and deep blue accents',
+ colors: {
+ primary: '#FF8F00',
+ secondary: '#1565C0',
+ tertiary: '#7B1FA2'
+ }
+ },
+
+ // ==========================================================================
+ // MONOCHROMATIC THEMES
+ // ==========================================================================
+
+ 'midnight': {
+ name: 'Midnight',
+ description: 'Dark theme with subtle blue undertones',
+ colors: {
+ primary: '#1A237E',
+ secondary: '#283593',
+ tertiary: '#3F51B5'
+ }
+ },
+
+ 'slate': {
+ name: 'Slate',
+ description: 'Sophisticated grays with blue-gray undertones',
+ colors: {
+ primary: '#455A64',
+ secondary: '#607D8B',
+ tertiary: '#90A4AE'
+ }
+ },
+
+ 'charcoal': {
+ name: 'Charcoal',
+ description: 'Dark charcoal theme with minimal color accents',
+ colors: {
+ primary: '#263238',
+ secondary: '#37474F',
+ tertiary: '#546E7A'
+ }
+ },
+
+ // ==========================================================================
+ // PASTEL/SOFT THEMES
+ // ==========================================================================
+
+ 'soft-pink': {
+ name: 'Soft Pink',
+ description: 'Gentle theme with soft pink and lavender tones',
+ colors: {
+ primary: '#AD1457',
+ secondary: '#7B1FA2',
+ tertiary: '#512DA8'
+ }
+ },
+
+ 'mint': {
+ name: 'Mint',
+ description: 'Fresh theme with mint greens and soft blues',
+ colors: {
+ primary: '#00695C',
+ secondary: '#00838F',
+ tertiary: '#0277BD'
+ }
+ },
+
+ 'cream': {
+ name: 'Cream',
+ description: 'Warm, soft theme with cream and beige tones',
+ colors: {
+ primary: '#8D6E63',
+ secondary: '#A1887F',
+ tertiary: '#FF8A65'
+ }
+ },
+
+ // ==========================================================================
+ // HIGH CONTRAST THEMES
+ // ==========================================================================
+
+ 'high-contrast': {
+ name: 'High Contrast',
+ description: 'Maximum contrast theme for accessibility',
+ colors: {
+ primary: '#000000',
+ secondary: '#424242',
+ tertiary: '#757575'
+ }
+ },
+
+ 'accessibility': {
+ name: 'Accessibility',
+ description: 'Optimized for maximum readability and contrast',
+ colors: {
+ primary: '#1565C0',
+ secondary: '#0D47A1',
+ tertiary: '#1A237E'
+ }
+ }
+};
+
+/**
+ * Get theme by category
+ */
+export const getThemesByCategory = () => {
+ return {
+ material: ['material-purple', 'material-blue', 'material-green'],
+ corporate: ['corporate-blue', 'executive', 'modern-corporate'],
+ nature: ['forest', 'ocean', 'sunset', 'lavender'],
+ vibrant: ['electric', 'neon', 'cyberpunk'],
+ warm: ['autumn', 'coffee', 'golden'],
+ monochrome: ['midnight', 'slate', 'charcoal'],
+ pastel: ['soft-pink', 'mint', 'cream'],
+ accessibility: ['high-contrast', 'accessibility']
+ };
+};
+
+/**
+ * Get recommended themes for specific use cases
+ */
+export const getRecommendedThemes = () => {
+ return {
+ business: ['corporate-blue', 'executive', 'modern-corporate', 'slate'],
+ creative: ['electric', 'neon', 'cyberpunk', 'sunset'],
+ healthcare: ['soft-pink', 'mint', 'ocean', 'accessibility'],
+ finance: ['corporate-blue', 'executive', 'charcoal', 'midnight'],
+ education: ['material-blue', 'forest', 'accessibility', 'mint'],
+ ecommerce: ['material-purple', 'golden', 'sunset', 'electric'],
+ dashboard: ['corporate-blue', 'slate', 'midnight', 'accessibility'],
+ mobile: ['material-purple', 'material-blue', 'ocean', 'neon']
+ };
+};
+
+/**
+ * Get theme metadata
+ * @param themeId Theme identifier
+ * @returns Theme metadata or null
+ */
+export const getThemeMetadata = (themeId: string) => {
+ const theme = DEFAULT_THEMES[themeId];
+ if (!theme) return null;
+
+ const categories = getThemesByCategory();
+ const recommendations = getRecommendedThemes();
+
+ // Find which category this theme belongs to
+ let category = 'other';
+ for (const [cat, themes] of Object.entries(categories)) {
+ if (themes.includes(themeId)) {
+ category = cat;
+ break;
+ }
+ }
+
+ // Find recommended use cases
+ const useCases: string[] = [];
+ for (const [useCase, themes] of Object.entries(recommendations)) {
+ if (themes.includes(themeId)) {
+ useCases.push(useCase);
+ }
+ }
+
+ return {
+ ...theme,
+ category,
+ useCases,
+ id: themeId
+ };
+};
\ No newline at end of file
diff --git a/projects/hcl-studio/src/lib/themes/theme-storage.ts b/projects/hcl-studio/src/lib/themes/theme-storage.ts
new file mode 100644
index 0000000..1555e3a
--- /dev/null
+++ b/projects/hcl-studio/src/lib/themes/theme-storage.ts
@@ -0,0 +1,423 @@
+/**
+ * ==========================================================================
+ * THEME STORAGE SERVICE
+ * ==========================================================================
+ * Handles persistence of themes and user preferences using localStorage
+ * Provides backup and import/export functionality
+ * ==========================================================================
+ */
+
+import { ThemeConfig, ThemeCollection } from '../models/hcl.models';
+
+export class ThemeStorage {
+
+ private static readonly STORAGE_KEYS = {
+ CURRENT_THEME: 'hcl-studio-current-theme',
+ CURRENT_MODE: 'hcl-studio-current-mode',
+ CUSTOM_THEMES: 'hcl-studio-custom-themes',
+ USER_PREFERENCES: 'hcl-studio-preferences'
+ } as const;
+
+ // ==========================================================================
+ // CURRENT THEME PERSISTENCE
+ // ==========================================================================
+
+ /**
+ * Save current theme ID
+ * @param themeId Theme identifier
+ */
+ static saveCurrentTheme(themeId: string): void {
+ try {
+ localStorage.setItem(this.STORAGE_KEYS.CURRENT_THEME, themeId);
+ } catch (error) {
+ console.warn('Failed to save current theme:', error);
+ }
+ }
+
+ /**
+ * Get current theme ID
+ * @returns Current theme ID or null
+ */
+ static getCurrentTheme(): string | null {
+ try {
+ return localStorage.getItem(this.STORAGE_KEYS.CURRENT_THEME);
+ } catch (error) {
+ console.warn('Failed to get current theme:', error);
+ return null;
+ }
+ }
+
+ /**
+ * Save current mode (light/dark)
+ * @param mode Current mode
+ */
+ static saveCurrentMode(mode: 'light' | 'dark'): void {
+ try {
+ localStorage.setItem(this.STORAGE_KEYS.CURRENT_MODE, mode);
+ } catch (error) {
+ console.warn('Failed to save current mode:', error);
+ }
+ }
+
+ /**
+ * Get current mode
+ * @returns Current mode or 'light' as default
+ */
+ static getCurrentMode(): 'light' | 'dark' {
+ try {
+ const mode = localStorage.getItem(this.STORAGE_KEYS.CURRENT_MODE);
+ return mode === 'dark' ? 'dark' : 'light';
+ } catch (error) {
+ console.warn('Failed to get current mode:', error);
+ return 'light';
+ }
+ }
+
+ // ==========================================================================
+ // CUSTOM THEMES MANAGEMENT
+ // ==========================================================================
+
+ /**
+ * Save a custom theme
+ * @param themeConfig Theme configuration
+ */
+ static saveCustomTheme(themeConfig: ThemeConfig): void {
+ try {
+ const customThemes = this.getCustomThemes();
+ customThemes[themeConfig.id] = {
+ ...themeConfig,
+ createdAt: themeConfig.createdAt || new Date()
+ };
+
+ localStorage.setItem(this.STORAGE_KEYS.CUSTOM_THEMES, JSON.stringify(customThemes));
+ } catch (error) {
+ console.warn('Failed to save custom theme:', error);
+ }
+ }
+
+ /**
+ * Get all custom themes
+ * @returns Custom themes collection
+ */
+ static getCustomThemes(): { [id: string]: ThemeConfig } {
+ try {
+ const stored = localStorage.getItem(this.STORAGE_KEYS.CUSTOM_THEMES);
+ return stored ? JSON.parse(stored) : {};
+ } catch (error) {
+ console.warn('Failed to get custom themes:', error);
+ return {};
+ }
+ }
+
+ /**
+ * Get specific custom theme
+ * @param themeId Theme ID
+ * @returns Theme configuration or null
+ */
+ static getCustomTheme(themeId: string): ThemeConfig | null {
+ const customThemes = this.getCustomThemes();
+ return customThemes[themeId] || null;
+ }
+
+ /**
+ * Delete custom theme
+ * @param themeId Theme ID to delete
+ */
+ static deleteCustomTheme(themeId: string): void {
+ try {
+ const customThemes = this.getCustomThemes();
+ delete customThemes[themeId];
+ localStorage.setItem(this.STORAGE_KEYS.CUSTOM_THEMES, JSON.stringify(customThemes));
+ } catch (error) {
+ console.warn('Failed to delete custom theme:', error);
+ }
+ }
+
+ /**
+ * Check if theme is custom (user-created)
+ * @param themeId Theme ID
+ * @returns True if theme is custom
+ */
+ static isCustomTheme(themeId: string): boolean {
+ const customThemes = this.getCustomThemes();
+ return themeId in customThemes;
+ }
+
+ // ==========================================================================
+ // USER PREFERENCES
+ // ==========================================================================
+
+ /**
+ * Save user preferences
+ * @param preferences User preference object
+ */
+ static savePreferences(preferences: any): void {
+ try {
+ localStorage.setItem(this.STORAGE_KEYS.USER_PREFERENCES, JSON.stringify(preferences));
+ } catch (error) {
+ console.warn('Failed to save preferences:', error);
+ }
+ }
+
+ /**
+ * Get user preferences
+ * @returns User preferences or default object
+ */
+ static getPreferences(): any {
+ try {
+ const stored = localStorage.getItem(this.STORAGE_KEYS.USER_PREFERENCES);
+ return stored ? JSON.parse(stored) : {
+ autoMode: false,
+ transitionDuration: 300,
+ enableSystemTheme: true
+ };
+ } catch (error) {
+ console.warn('Failed to get preferences:', error);
+ return {
+ autoMode: false,
+ transitionDuration: 300,
+ enableSystemTheme: true
+ };
+ }
+ }
+
+ // ==========================================================================
+ // IMPORT/EXPORT FUNCTIONALITY
+ // ==========================================================================
+
+ /**
+ * Export theme configuration as JSON string
+ * @param themeId Theme ID to export
+ * @returns JSON string or null if theme not found
+ */
+ static exportTheme(themeId: string): string | null {
+ const customTheme = this.getCustomTheme(themeId);
+ if (!customTheme) return null;
+
+ const exportData = {
+ version: '1.0',
+ exportDate: new Date().toISOString(),
+ theme: customTheme
+ };
+
+ return JSON.stringify(exportData, null, 2);
+ }
+
+ /**
+ * Import theme from JSON string
+ * @param jsonString JSON theme data
+ * @returns Imported theme config or null if invalid
+ */
+ static importTheme(jsonString: string): ThemeConfig | null {
+ try {
+ const importData = JSON.parse(jsonString);
+
+ // Validate import data structure
+ if (!importData.theme || !importData.theme.id || !importData.theme.name || !importData.theme.colors) {
+ throw new Error('Invalid theme format');
+ }
+
+ const themeConfig: ThemeConfig = {
+ ...importData.theme,
+ createdAt: new Date(),
+ description: importData.theme.description || 'Imported theme'
+ };
+
+ // Check for ID conflicts and rename if necessary
+ const existingThemes = this.getCustomThemes();
+ if (existingThemes[themeConfig.id]) {
+ themeConfig.id = this.generateUniqueId(themeConfig.id, existingThemes);
+ }
+
+ // Save imported theme
+ this.saveCustomTheme(themeConfig);
+
+ return themeConfig;
+ } catch (error) {
+ console.warn('Failed to import theme:', error);
+ return null;
+ }
+ }
+
+ /**
+ * Export all custom themes
+ * @returns JSON string with all custom themes
+ */
+ static exportAllThemes(): string {
+ const customThemes = this.getCustomThemes();
+
+ const exportData = {
+ version: '1.0',
+ exportDate: new Date().toISOString(),
+ themes: customThemes
+ };
+
+ return JSON.stringify(exportData, null, 2);
+ }
+
+ /**
+ * Import multiple themes from JSON string
+ * @param jsonString JSON data with multiple themes
+ * @returns Array of imported theme IDs
+ */
+ static importMultipleThemes(jsonString: string): string[] {
+ try {
+ const importData = JSON.parse(jsonString);
+ const importedIds: string[] = [];
+
+ if (!importData.themes) {
+ throw new Error('Invalid multi-theme format');
+ }
+
+ const existingThemes = this.getCustomThemes();
+
+ Object.values(importData.themes).forEach((theme: any) => {
+ if (theme.id && theme.name && theme.colors) {
+ let themeConfig: ThemeConfig = {
+ ...theme,
+ createdAt: new Date(),
+ description: theme.description || 'Imported theme'
+ };
+
+ // Handle ID conflicts
+ if (existingThemes[themeConfig.id]) {
+ themeConfig.id = this.generateUniqueId(themeConfig.id, existingThemes);
+ }
+
+ this.saveCustomTheme(themeConfig);
+ importedIds.push(themeConfig.id);
+ }
+ });
+
+ return importedIds;
+ } catch (error) {
+ console.warn('Failed to import multiple themes:', error);
+ return [];
+ }
+ }
+
+ // ==========================================================================
+ // UTILITY METHODS
+ // ==========================================================================
+
+ /**
+ * Generate unique theme ID to avoid conflicts
+ * @param baseId Base theme ID
+ * @param existingThemes Existing themes to check against
+ * @returns Unique theme ID
+ */
+ private static generateUniqueId(baseId: string, existingThemes: { [id: string]: any }): string {
+ let counter = 1;
+ let newId = `${baseId}-${counter}`;
+
+ while (existingThemes[newId]) {
+ counter++;
+ newId = `${baseId}-${counter}`;
+ }
+
+ return newId;
+ }
+
+ /**
+ * Clear all stored data (reset to defaults)
+ */
+ static clearAllData(): void {
+ try {
+ Object.values(this.STORAGE_KEYS).forEach(key => {
+ localStorage.removeItem(key);
+ });
+ } catch (error) {
+ console.warn('Failed to clear storage data:', error);
+ }
+ }
+
+ /**
+ * Get storage usage information
+ * @returns Storage usage stats
+ */
+ static getStorageInfo(): { customThemes: number; totalSize: number; isAvailable: boolean } {
+ try {
+ const customThemes = Object.keys(this.getCustomThemes()).length;
+ let totalSize = 0;
+
+ Object.values(this.STORAGE_KEYS).forEach(key => {
+ const item = localStorage.getItem(key);
+ if (item) {
+ totalSize += item.length;
+ }
+ });
+
+ return {
+ customThemes,
+ totalSize,
+ isAvailable: true
+ };
+ } catch (error) {
+ return {
+ customThemes: 0,
+ totalSize: 0,
+ isAvailable: false
+ };
+ }
+ }
+
+ /**
+ * Check if localStorage is available
+ * @returns True if localStorage is available
+ */
+ static isStorageAvailable(): boolean {
+ try {
+ const test = 'hcl-studio-test';
+ localStorage.setItem(test, test);
+ localStorage.removeItem(test);
+ return true;
+ } catch {
+ return false;
+ }
+ }
+
+ /**
+ * Create backup of all theme data
+ * @returns Backup data object
+ */
+ static createBackup(): any {
+ return {
+ version: '1.0',
+ backupDate: new Date().toISOString(),
+ currentTheme: this.getCurrentTheme(),
+ currentMode: this.getCurrentMode(),
+ customThemes: this.getCustomThemes(),
+ preferences: this.getPreferences()
+ };
+ }
+
+ /**
+ * Restore from backup data
+ * @param backupData Backup data object
+ * @returns True if restore was successful
+ */
+ static restoreFromBackup(backupData: any): boolean {
+ try {
+ if (backupData.currentTheme) {
+ this.saveCurrentTheme(backupData.currentTheme);
+ }
+
+ if (backupData.currentMode) {
+ this.saveCurrentMode(backupData.currentMode);
+ }
+
+ if (backupData.customThemes) {
+ localStorage.setItem(this.STORAGE_KEYS.CUSTOM_THEMES, JSON.stringify(backupData.customThemes));
+ }
+
+ if (backupData.preferences) {
+ this.savePreferences(backupData.preferences);
+ }
+
+ return true;
+ } catch (error) {
+ console.warn('Failed to restore from backup:', error);
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/hcl-studio/src/public-api.ts b/projects/hcl-studio/src/public-api.ts
new file mode 100644
index 0000000..de27afb
--- /dev/null
+++ b/projects/hcl-studio/src/public-api.ts
@@ -0,0 +1,35 @@
+/**
+ * ==========================================================================
+ * HCL STUDIO - PUBLIC API
+ * ==========================================================================
+ * Export all public interfaces, services, and utilities
+ * ==========================================================================
+ */
+
+// ==========================================================================
+// MAIN SERVICE
+// ==========================================================================
+export * from './lib/hcl-studio.service';
+
+// ==========================================================================
+// CORE FUNCTIONALITY
+// ==========================================================================
+export * from './lib/core/hcl-converter';
+export * from './lib/core/palette-generator';
+
+// ==========================================================================
+// THEME MANAGEMENT
+// ==========================================================================
+export * from './lib/themes/css-variable-manager';
+export * from './lib/themes/theme-storage';
+export * from './lib/themes/theme-presets';
+
+// ==========================================================================
+// TYPE DEFINITIONS
+// ==========================================================================
+export * from './lib/models/hcl.models';
+
+// ==========================================================================
+// COMPONENTS (if any are created)
+// ==========================================================================
+export * from './lib/hcl-studio.component';
\ No newline at end of file
diff --git a/projects/hcl-studio/tsconfig.lib.json b/projects/hcl-studio/tsconfig.lib.json
new file mode 100644
index 0000000..2359bf6
--- /dev/null
+++ b/projects/hcl-studio/tsconfig.lib.json
@@ -0,0 +1,15 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../out-tsc/lib",
+ "declaration": true,
+ "declarationMap": true,
+ "inlineSources": true,
+ "types": []
+ },
+ "exclude": [
+ "**/*.spec.ts"
+ ]
+}
diff --git a/projects/hcl-studio/tsconfig.lib.prod.json b/projects/hcl-studio/tsconfig.lib.prod.json
new file mode 100644
index 0000000..9215caa
--- /dev/null
+++ b/projects/hcl-studio/tsconfig.lib.prod.json
@@ -0,0 +1,11 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+ "extends": "./tsconfig.lib.json",
+ "compilerOptions": {
+ "declarationMap": false
+ },
+ "angularCompilerOptions": {
+ "compilationMode": "partial"
+ }
+}
diff --git a/projects/hcl-studio/tsconfig.spec.json b/projects/hcl-studio/tsconfig.spec.json
new file mode 100644
index 0000000..254686d
--- /dev/null
+++ b/projects/hcl-studio/tsconfig.spec.json
@@ -0,0 +1,15 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../out-tsc/spec",
+ "types": [
+ "jasmine"
+ ]
+ },
+ "include": [
+ "**/*.spec.ts",
+ "**/*.d.ts"
+ ]
+}
diff --git a/projects/ui-accessibility/README.md b/projects/ui-accessibility/README.md
new file mode 100644
index 0000000..2c9e01e
--- /dev/null
+++ b/projects/ui-accessibility/README.md
@@ -0,0 +1,364 @@
+# UI Accessibility
+
+A comprehensive Angular accessibility library that enhances your existing components with WCAG 2.1 Level AA compliant features while using semantic tokens from your design system.
+
+## Features
+
+- **🎯 Focus Management** - Advanced focus trapping, restoration, and monitoring
+- **⌨️ Keyboard Navigation** - Arrow keys, roving tabindex, custom shortcuts
+- **📢 Screen Reader Support** - Live announcements, ARIA enhancements
+- **🎨 Design System Integration** - Uses semantic motion, color, and spacing tokens
+- **♿ High Contrast & Reduced Motion** - Automatic detection and adaptation
+- **🔧 Zero Rewrites Required** - Enhance existing components with directives
+- **🧪 Developer Experience** - Debug modes, warnings, and comprehensive logging
+
+## Installation
+
+```bash
+npm install ui-accessibility
+```
+
+## Quick Start
+
+### 1. Import the Module
+
+```typescript
+import { UiAccessibilityModule } from 'ui-accessibility';
+
+@NgModule({
+ imports: [
+ UiAccessibilityModule.forRoot({
+ // Optional configuration
+ skipLinks: { enabled: true },
+ keyboard: { enableArrowNavigation: true },
+ development: { warnings: true }
+ })
+ ]
+})
+export class AppModule {}
+```
+
+### 2. Import SCSS Utilities
+
+```scss
+// Import accessibility utilities (uses your semantic tokens)
+@use 'ui-accessibility/src/lib/utilities/a11y-utilities';
+```
+
+### 3. Add Skip Links
+
+```html
+
+
+```
+
+## Core Features
+
+### Focus Trap Directive
+
+Trap focus within containers like modals and drawers:
+
+```html
+
+
+ Settings
+
+ Save
+
+
+
+
+ Settings
+
+ Save
+
+```
+
+**Features:**
+- Automatic focus on first element
+- Tab wrapping within container
+- Focus restoration on close
+- Uses semantic motion tokens for transitions
+
+### Arrow Navigation Directive
+
+Add keyboard navigation to lists, menus, and grids:
+
+```html
+
+
+ Profile
+ Settings
+ Logout
+
+
+
+
+ Profile
+ Settings
+ Logout
+
+```
+
+**Navigation Options:**
+- `vertical` - Up/Down arrows
+- `horizontal` - Left/Right arrows
+- `both` - All arrow keys
+- `grid` - 2D navigation with column support
+
+### Live Announcer Service
+
+Announce dynamic content changes to screen readers:
+
+```typescript
+import { LiveAnnouncerService } from 'ui-accessibility';
+
+constructor(private announcer: LiveAnnouncerService) {}
+
+// Announce with different politeness levels
+onItemAdded() {
+ this.announcer.announce('Item added to cart', 'polite');
+}
+
+onError() {
+ this.announcer.announce('Error: Please fix the required fields', 'assertive');
+}
+
+// Announce a sequence of messages
+onProcessComplete() {
+ this.announcer.announceSequence([
+ 'Processing started',
+ 'Validating data',
+ 'Processing complete'
+ ], 'polite', 1500);
+}
+```
+
+### Screen Reader Components
+
+Hide content visually while keeping it accessible:
+
+```html
+
+
+ Additional context for screen readers
+
+
+
+
+ Press Enter to activate
+
+
+
+
+ Please correct the errors below
+
+```
+
+### Keyboard Manager Service
+
+Register global and element-specific keyboard shortcuts:
+
+```typescript
+import { KeyboardManagerService } from 'ui-accessibility';
+
+constructor(private keyboard: KeyboardManagerService) {}
+
+ngOnInit() {
+ // Global shortcut
+ this.keyboard.registerGlobalShortcut('save', {
+ key: 's',
+ ctrlKey: true,
+ description: 'Save document',
+ handler: () => this.save()
+ });
+
+ // Element-specific shortcut
+ const element = this.elementRef.nativeElement;
+ this.keyboard.registerElementShortcut('close', element, {
+ key: 'Escape',
+ description: 'Close dialog',
+ handler: () => this.close()
+ });
+}
+```
+
+## Design System Integration
+
+All features use semantic tokens from your design system:
+
+### Focus Indicators
+
+```scss
+// Automatic integration with your focus tokens
+.my-component:focus-visible {
+ outline: $semantic-border-focus-width solid $semantic-color-focus;
+ box-shadow: $semantic-shadow-input-focus;
+ transition: outline-color $semantic-motion-duration-fast;
+}
+```
+
+### Skip Links Styling
+
+Uses your semantic spacing, colors, and typography:
+
+```scss
+.skip-link {
+ padding: $semantic-spacing-component-padding-y $semantic-spacing-component-padding-x;
+ background: $semantic-color-surface-primary;
+ color: $semantic-color-text-primary;
+ border-color: $semantic-color-border-focus;
+}
+```
+
+### Motion & Animations
+
+Respects reduced motion preferences:
+
+```typescript
+// Service automatically detects preference
+const duration = this.highContrast.getMotionDuration('300ms', '0.01s');
+
+// SCSS utilities adapt automatically
+@media (prefers-reduced-motion: reduce) {
+ .fade-in-a11y {
+ animation-duration: 0.01s;
+ }
+}
+```
+
+## Advanced Configuration
+
+### Full Configuration Example
+
+```typescript
+UiAccessibilityModule.forRoot({
+ announcer: {
+ defaultPoliteness: 'polite',
+ defaultDuration: 4000,
+ enabled: true
+ },
+ focusManagement: {
+ trapFocus: true,
+ restoreFocus: true,
+ focusVisibleEnabled: true
+ },
+ keyboard: {
+ enableShortcuts: true,
+ enableArrowNavigation: true,
+ customShortcuts: [
+ { key: '/', ctrlKey: true, description: 'Search' }
+ ]
+ },
+ skipLinks: {
+ enabled: true,
+ position: 'top',
+ links: [
+ { href: '#main', text: 'Skip to main content' },
+ { href: '#nav', text: 'Skip to navigation' },
+ { href: '#search', text: 'Skip to search' }
+ ]
+ },
+ accessibility: {
+ respectReducedMotion: true,
+ respectHighContrast: true,
+ injectAccessibilityStyles: true,
+ addBodyClasses: true
+ },
+ development: {
+ warnings: true,
+ logging: true
+ }
+})
+```
+
+### High Contrast & Reduced Motion
+
+Automatic detection and CSS class application:
+
+```html
+
+
+
+
+
+```
+
+## Available Directives
+
+| Directive | Purpose | Usage |
+|-----------|---------|--------|
+| `uiFocusTrap` | Trap focus within container | `` |
+| `uiArrowNavigation` | Arrow key navigation | `
` |
+
+## Available Services
+
+| Service | Purpose | Key Methods |
+|---------|---------|-------------|
+| `LiveAnnouncerService` | Screen reader announcements | `announce()`, `announceSequence()` |
+| `FocusMonitorService` | Focus origin tracking | `monitor()`, `focusVia()` |
+| `KeyboardManagerService` | Keyboard shortcuts | `registerGlobalShortcut()` |
+| `HighContrastService` | Accessibility preferences | `getCurrentPreferences()` |
+| `A11yConfigService` | Configuration management | `getConfig()`, `isEnabled()` |
+
+## SCSS Utilities
+
+```scss
+// Import specific utilities
+@use 'ui-accessibility/src/lib/utilities/focus-visible';
+@use 'ui-accessibility/src/lib/utilities/screen-reader';
+
+// Use utility classes
+.my-element {
+ @extend .focus-ring; // Adds focus ring on :focus-visible
+ @extend .touch-target; // Ensures minimum touch target size
+}
+
+// Screen reader utilities
+.sr-instructions {
+ @extend .sr-only; // Visually hidden, screen reader accessible
+}
+
+.status-text {
+ @extend .sr-only-focusable; // Hidden until focused
+}
+```
+
+## WCAG 2.1 Compliance
+
+This library helps achieve Level AA compliance:
+
+- ✅ **1.3.1** - Info and Relationships (ARIA attributes)
+- ✅ **1.4.3** - Contrast (High contrast mode support)
+- ✅ **1.4.13** - Content on Hover/Focus (Proper focus management)
+- ✅ **2.1.1** - Keyboard (Full keyboard navigation)
+- ✅ **2.1.2** - No Keyboard Trap (Proper focus trapping)
+- ✅ **2.4.1** - Bypass Blocks (Skip links)
+- ✅ **2.4.3** - Focus Order (Logical tab sequence)
+- ✅ **2.4.7** - Focus Visible (Enhanced focus indicators)
+- ✅ **3.2.1** - On Focus (Predictable focus behavior)
+- ✅ **4.1.3** - Status Messages (Live announcements)
+
+## Browser Support
+
+- Chrome 88+
+- Firefox 85+
+- Safari 14+
+- Edge 88+
+
+## Demo
+
+Visit `/accessibility` in your demo application to see interactive examples of all features.
+
+## Contributing
+
+This library integrates seamlessly with your existing component architecture and design system tokens, requiring no rewrites of existing components.
\ No newline at end of file
diff --git a/projects/ui-accessibility/ng-package.json b/projects/ui-accessibility/ng-package.json
new file mode 100644
index 0000000..77f6cc3
--- /dev/null
+++ b/projects/ui-accessibility/ng-package.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
+ "dest": "../../dist/ui-accessibility",
+ "lib": {
+ "entryFile": "src/public-api.ts"
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-accessibility/package.json b/projects/ui-accessibility/package.json
new file mode 100644
index 0000000..02b43b5
--- /dev/null
+++ b/projects/ui-accessibility/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "ui-accessibility",
+ "version": "0.0.1",
+ "description": "Comprehensive accessibility utilities for Angular applications with semantic token integration",
+ "keywords": ["angular", "accessibility", "a11y", "wcag", "focus-management", "screen-reader", "semantic-tokens"],
+ "peerDependencies": {
+ "@angular/common": "^19.2.0",
+ "@angular/core": "^19.2.0",
+ "rxjs": "~7.8.0"
+ },
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "sideEffects": false
+}
diff --git a/projects/ui-accessibility/src/lib/components/screen-reader-only/index.ts b/projects/ui-accessibility/src/lib/components/screen-reader-only/index.ts
new file mode 100644
index 0000000..ac13c5b
--- /dev/null
+++ b/projects/ui-accessibility/src/lib/components/screen-reader-only/index.ts
@@ -0,0 +1 @@
+export * from './screen-reader-only.component';
\ No newline at end of file
diff --git a/projects/ui-accessibility/src/lib/components/screen-reader-only/screen-reader-only.component.ts b/projects/ui-accessibility/src/lib/components/screen-reader-only/screen-reader-only.component.ts
new file mode 100644
index 0000000..4a9237c
--- /dev/null
+++ b/projects/ui-accessibility/src/lib/components/screen-reader-only/screen-reader-only.component.ts
@@ -0,0 +1,167 @@
+import { Component, Input } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+@Component({
+ selector: 'ui-screen-reader-only',
+ standalone: true,
+ imports: [CommonModule],
+ template: `
+
+
+
+ `,
+ styles: [`
+ .sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+ }
+
+ .sr-only-focusable {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+ }
+
+ .sr-only-focusable:focus,
+ .sr-only-focusable:active {
+ position: static;
+ width: auto;
+ height: auto;
+ padding: 0.5rem 1rem;
+ margin: 0;
+ overflow: visible;
+ clip: auto;
+ white-space: normal;
+ background: var(--a11y-skip-link-bg, #fff);
+ color: var(--a11y-skip-link-color, #000);
+ border: 1px solid var(--a11y-skip-link-border, #007bff);
+ border-radius: 4px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ z-index: 10000;
+ }
+
+ .status-message {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+ }
+
+ .status-message.status-visible {
+ position: static;
+ width: auto;
+ height: auto;
+ overflow: visible;
+ clip: auto;
+ white-space: normal;
+ padding: 0.5rem 1rem;
+ margin: 4px 0;
+ background: var(--a11y-skip-link-bg, #fff);
+ border-left: 4px solid var(--a11y-focus-color, #007bff);
+ border-radius: 0 4px 4px 0;
+ }
+
+ .status-message.status-error {
+ border-left-color: var(--a11y-error-color, #dc3545);
+ }
+
+ .status-message.status-success {
+ border-left-color: var(--a11y-success-color, #28a745);
+ }
+
+ .status-message.status-warning {
+ border-left-color: var(--a11y-warning-color, #ffc107);
+ }
+
+ .instructions {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+ }
+
+ @media (prefers-contrast: high) {
+ .sr-only-focusable:focus,
+ .sr-only-focusable:active {
+ background: ButtonFace;
+ color: ButtonText;
+ border-color: ButtonText;
+ }
+
+ .status-message.status-visible {
+ background: ButtonFace;
+ color: ButtonText;
+ border-color: ButtonText;
+ }
+ }
+ `]
+})
+export class ScreenReaderOnlyComponent {
+ @Input() type: 'default' | 'focusable' | 'status' | 'instructions' = 'default';
+ @Input() statusType?: 'error' | 'success' | 'warning';
+ @Input() visible = false;
+ @Input() ariaLive?: 'polite' | 'assertive' | 'off';
+ @Input() ariaAtomic?: 'true' | 'false';
+ @Input() role?: string;
+
+ /**
+ * Get CSS classes based on component configuration
+ */
+ getClasses(): string {
+ const classes: string[] = [];
+
+ switch (this.type) {
+ case 'focusable':
+ classes.push('sr-only-focusable');
+ break;
+
+ case 'status':
+ classes.push('status-message');
+ if (this.visible) {
+ classes.push('status-visible');
+ }
+ if (this.statusType) {
+ classes.push(`status-${this.statusType}`);
+ }
+ break;
+
+ case 'instructions':
+ classes.push('instructions');
+ break;
+
+ default:
+ classes.push('sr-only');
+ break;
+ }
+
+ return classes.join(' ');
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-accessibility/src/lib/components/skip-links/index.ts b/projects/ui-accessibility/src/lib/components/skip-links/index.ts
new file mode 100644
index 0000000..98a70a1
--- /dev/null
+++ b/projects/ui-accessibility/src/lib/components/skip-links/index.ts
@@ -0,0 +1 @@
+export * from './skip-links.component';
\ No newline at end of file
diff --git a/projects/ui-accessibility/src/lib/components/skip-links/skip-links.component.ts b/projects/ui-accessibility/src/lib/components/skip-links/skip-links.component.ts
new file mode 100644
index 0000000..6cf7c10
--- /dev/null
+++ b/projects/ui-accessibility/src/lib/components/skip-links/skip-links.component.ts
@@ -0,0 +1,203 @@
+import { Component, Input, OnInit, inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { A11yConfigService } from '../../services/a11y-config';
+
+export interface SkipLink {
+ href: string;
+ text: string;
+ ariaLabel?: string;
+}
+
+@Component({
+ selector: 'ui-skip-links',
+ standalone: true,
+ imports: [CommonModule],
+ template: `
+
+ @for (link of skipLinks; track link.href) {
+
+ {{ link.text }}
+
+ }
+
+ `,
+ styles: [`
+ .skip-links {
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 10000;
+ }
+
+ .skip-links--after-header {
+ position: relative;
+ display: block;
+ background: var(--a11y-skip-link-bg, #fff);
+ border-bottom: 1px solid var(--a11y-skip-link-border, #ccc);
+ padding: 0.5rem;
+ }
+
+ .skip-link {
+ position: absolute;
+ top: -40px;
+ left: 6px;
+ background: var(--a11y-skip-link-bg, #fff);
+ color: var(--a11y-skip-link-color, #000);
+ padding: 0.5rem 1rem;
+ text-decoration: none;
+ border: 1px solid var(--a11y-skip-link-border, #007bff);
+ border-radius: 4px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ opacity: 0;
+ pointer-events: none;
+ transform: translateY(-10px);
+ transition: all 0.15s ease-out;
+ font-weight: 500;
+ white-space: nowrap;
+ }
+
+ .skip-link:focus {
+ top: 6px;
+ opacity: 1;
+ pointer-events: auto;
+ transform: translateY(0);
+ outline: 2px solid var(--a11y-focus-color, #007bff);
+ outline-offset: 2px;
+ }
+
+ .skip-link:nth-child(2):focus {
+ top: 50px;
+ }
+
+ .skip-link:nth-child(3):focus {
+ top: 94px;
+ }
+
+ .skip-link:nth-child(4):focus {
+ top: 138px;
+ }
+
+ .skip-links--after-header .skip-link {
+ position: static;
+ display: inline-block;
+ margin-right: 1rem;
+ opacity: 1;
+ pointer-events: auto;
+ transform: none;
+ transition: background-color 0.15s ease-out;
+ }
+
+ .skip-links--after-header .skip-link:hover,
+ .skip-links--after-header .skip-link:focus {
+ background-color: var(--a11y-focus-ring-color, rgba(0, 123, 255, 0.1));
+ }
+
+ @media (prefers-contrast: high) {
+ .skip-link {
+ background: ButtonFace;
+ color: ButtonText;
+ border-color: ButtonText;
+ outline-color: Highlight;
+ }
+ }
+
+ @media (prefers-reduced-motion: reduce) {
+ .skip-link {
+ transition-duration: 0.01s;
+ }
+ }
+ `]
+})
+export class SkipLinksComponent implements OnInit {
+ private config = inject(A11yConfigService);
+
+ @Input() skipLinks: SkipLink[] = [];
+ @Input() position: 'top' | 'after-header' = 'top';
+ @Input() showOnFocus = true;
+
+ ngOnInit(): void {
+ // Use default links from config if none provided
+ if (this.skipLinks.length === 0) {
+ const configLinks = this.config.getSection('skipLinks').links;
+ if (configLinks) {
+ this.skipLinks = configLinks.map(link => ({
+ href: link.href,
+ text: link.text,
+ ariaLabel: `Skip to ${link.text.toLowerCase()}`
+ }));
+ }
+ }
+
+ // Use position from config if not explicitly set
+ if (this.position === 'top') {
+ const configPosition = this.config.getSection('skipLinks').position;
+ if (configPosition) {
+ this.position = configPosition;
+ }
+ }
+ }
+
+ /**
+ * Handle skip link click to ensure proper focus management
+ */
+ handleSkipLinkClick(event: Event, href: string): void {
+ event.preventDefault();
+
+ const targetId = href.replace('#', '');
+ const targetElement = document.getElementById(targetId);
+
+ if (targetElement) {
+ // Set tabindex to make element focusable if it's not naturally focusable
+ const originalTabIndex = targetElement.getAttribute('tabindex');
+ if (!this.isFocusable(targetElement)) {
+ targetElement.setAttribute('tabindex', '-1');
+ }
+
+ // Focus the target element
+ targetElement.focus();
+
+ // Scroll to the element
+ targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
+
+ // Restore original tabindex after focus
+ if (originalTabIndex === null && !this.isFocusable(targetElement)) {
+ setTimeout(() => {
+ targetElement.removeAttribute('tabindex');
+ }, 100);
+ }
+
+ this.config.log('Skip link activated', { href, targetId });
+ } else {
+ this.config.warn('Skip link target not found', { href, targetId });
+ }
+ }
+
+ /**
+ * Check if an element is naturally focusable
+ */
+ private isFocusable(element: HTMLElement): boolean {
+ const focusableSelectors = [
+ 'a[href]',
+ 'button',
+ 'textarea',
+ 'input[type="text"]',
+ 'input[type="radio"]',
+ 'input[type="checkbox"]',
+ 'select',
+ '[tabindex]:not([tabindex="-1"])'
+ ];
+
+ return focusableSelectors.some(selector => element.matches(selector)) ||
+ element.getAttribute('contenteditable') === 'true';
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-accessibility/src/lib/directives/arrow-navigation/arrow-navigation.directive.ts b/projects/ui-accessibility/src/lib/directives/arrow-navigation/arrow-navigation.directive.ts
new file mode 100644
index 0000000..dfd7b06
--- /dev/null
+++ b/projects/ui-accessibility/src/lib/directives/arrow-navigation/arrow-navigation.directive.ts
@@ -0,0 +1,382 @@
+import {
+ Directive,
+ ElementRef,
+ Input,
+ OnDestroy,
+ OnInit,
+ Output,
+ EventEmitter,
+ NgZone,
+ inject
+} from '@angular/core';
+import { fromEvent, Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+import { A11yConfigService } from '../../services/a11y-config';
+
+export type NavigationDirection = 'horizontal' | 'vertical' | 'both' | 'grid';
+
+export interface NavigationEvent {
+ direction: 'up' | 'down' | 'left' | 'right' | 'home' | 'end';
+ currentIndex: number;
+ nextIndex: number;
+ event: KeyboardEvent;
+}
+
+@Directive({
+ selector: '[uiArrowNavigation]',
+ standalone: true,
+ exportAs: 'uiArrowNavigation'
+})
+export class ArrowNavigationDirective implements OnInit, OnDestroy {
+ private elementRef = inject(ElementRef);
+ private ngZone = inject(NgZone);
+ private config = inject(A11yConfigService);
+
+ @Input('uiArrowNavigation') direction: NavigationDirection = 'vertical';
+ @Input() wrap = true;
+ @Input() skipDisabled = true;
+ @Input() itemSelector = '[tabindex]:not([tabindex="-1"]), button:not([disabled]), a[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled])';
+ @Input() gridColumns?: number;
+
+ @Output() navigationChange = new EventEmitter();
+
+ private destroy$ = new Subject();
+ private navigableItems: HTMLElement[] = [];
+ private currentIndex = 0;
+ private isGridNavigation = false;
+
+ ngOnInit(): void {
+ if (this.config.isEnabled('keyboard.enableArrowNavigation')) {
+ this.setupNavigation();
+ }
+ }
+
+ ngOnDestroy(): void {
+ this.destroy$.next();
+ this.destroy$.complete();
+ }
+
+ /**
+ * Navigate to a specific index
+ */
+ navigateToIndex(index: number): void {
+ this.updateNavigableItems();
+
+ if (index >= 0 && index < this.navigableItems.length) {
+ this.currentIndex = index;
+ this.focusCurrentItem();
+ }
+ }
+
+ /**
+ * Navigate to the first item
+ */
+ navigateToFirst(): void {
+ this.navigateToIndex(0);
+ }
+
+ /**
+ * Navigate to the last item
+ */
+ navigateToLast(): void {
+ this.updateNavigableItems();
+ this.navigateToIndex(this.navigableItems.length - 1);
+ }
+
+ /**
+ * Get the current navigation index
+ */
+ getCurrentIndex(): number {
+ return this.currentIndex;
+ }
+
+ /**
+ * Get the total number of navigable items
+ */
+ getItemCount(): number {
+ this.updateNavigableItems();
+ return this.navigableItems.length;
+ }
+
+ /**
+ * Set up navigation listeners
+ */
+ private setupNavigation(): void {
+ if (typeof document === 'undefined') return;
+
+ this.isGridNavigation = this.direction === 'grid';
+ this.updateNavigableItems();
+ this.setupKeyboardListeners();
+ this.setupFocusTracking();
+ }
+
+ /**
+ * Update the list of navigable items
+ */
+ private updateNavigableItems(): void {
+ const container = this.elementRef.nativeElement;
+ const items = Array.from(
+ container.querySelectorAll(this.itemSelector)
+ ) as HTMLElement[];
+
+ this.navigableItems = this.skipDisabled
+ ? items.filter((item: HTMLElement) => !this.isDisabled(item))
+ : items;
+
+ // Update current index if current item is no longer available
+ if (this.currentIndex >= this.navigableItems.length) {
+ this.currentIndex = Math.max(0, this.navigableItems.length - 1);
+ }
+ }
+
+ /**
+ * Set up keyboard event listeners
+ */
+ private setupKeyboardListeners(): void {
+ this.ngZone.runOutsideAngular(() => {
+ fromEvent(this.elementRef.nativeElement, 'keydown').pipe(
+ takeUntil(this.destroy$)
+ ).subscribe(event => {
+ this.handleKeyDown(event);
+ });
+
+ // Update items when DOM changes
+ const observer = new MutationObserver(() => {
+ this.updateNavigableItems();
+ });
+
+ observer.observe(this.elementRef.nativeElement, {
+ childList: true,
+ subtree: true,
+ attributes: true,
+ attributeFilter: ['disabled', 'tabindex', 'hidden']
+ });
+
+ this.destroy$.subscribe(() => observer.disconnect());
+ });
+ }
+
+ /**
+ * Set up focus tracking to maintain current index
+ */
+ private setupFocusTracking(): void {
+ this.ngZone.runOutsideAngular(() => {
+ fromEvent(this.elementRef.nativeElement, 'focusin').pipe(
+ takeUntil(this.destroy$)
+ ).subscribe((event) => {
+ const focusEvent = event as FocusEvent;
+ const target = focusEvent.target as HTMLElement;
+ const index = this.navigableItems.indexOf(target);
+ if (index >= 0) {
+ this.currentIndex = index;
+ }
+ });
+ });
+ }
+
+ /**
+ * Handle keyboard navigation
+ */
+ private handleKeyDown(event: KeyboardEvent): void {
+ if (this.navigableItems.length === 0) return;
+
+ const { key } = event;
+ let nextIndex = this.currentIndex;
+ let direction: NavigationEvent['direction'] | null = null;
+
+ switch (key) {
+ case 'ArrowUp':
+ direction = 'up';
+ nextIndex = this.isGridNavigation
+ ? this.getGridIndex('up')
+ : this.getVerticalIndex('up');
+ break;
+
+ case 'ArrowDown':
+ direction = 'down';
+ nextIndex = this.isGridNavigation
+ ? this.getGridIndex('down')
+ : this.getVerticalIndex('down');
+ break;
+
+ case 'ArrowLeft':
+ direction = 'left';
+ nextIndex = this.isGridNavigation
+ ? this.getGridIndex('left')
+ : this.getHorizontalIndex('left');
+ break;
+
+ case 'ArrowRight':
+ direction = 'right';
+ nextIndex = this.isGridNavigation
+ ? this.getGridIndex('right')
+ : this.getHorizontalIndex('right');
+ break;
+
+ case 'Home':
+ direction = 'home';
+ nextIndex = 0;
+ break;
+
+ case 'End':
+ direction = 'end';
+ nextIndex = this.navigableItems.length - 1;
+ break;
+
+ default:
+ return; // Don't handle other keys
+ }
+
+ if (direction && this.shouldHandleNavigation(key)) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const navigationEvent: NavigationEvent = {
+ direction,
+ currentIndex: this.currentIndex,
+ nextIndex,
+ event
+ };
+
+ this.ngZone.run(() => {
+ this.navigationChange.emit(navigationEvent);
+ });
+
+ if (nextIndex !== this.currentIndex && nextIndex >= 0 && nextIndex < this.navigableItems.length) {
+ this.currentIndex = nextIndex;
+ this.focusCurrentItem();
+ }
+ }
+ }
+
+ /**
+ * Check if we should handle this navigation key
+ */
+ private shouldHandleNavigation(key: string): boolean {
+ switch (this.direction) {
+ case 'horizontal':
+ return ['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(key);
+ case 'vertical':
+ return ['ArrowUp', 'ArrowDown', 'Home', 'End'].includes(key);
+ case 'both':
+ case 'grid':
+ return ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(key);
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Get next index for vertical navigation
+ */
+ private getVerticalIndex(direction: 'up' | 'down'): number {
+ const delta = direction === 'up' ? -1 : 1;
+ let nextIndex = this.currentIndex + delta;
+
+ if (this.wrap) {
+ if (nextIndex < 0) {
+ nextIndex = this.navigableItems.length - 1;
+ } else if (nextIndex >= this.navigableItems.length) {
+ nextIndex = 0;
+ }
+ } else {
+ nextIndex = Math.max(0, Math.min(this.navigableItems.length - 1, nextIndex));
+ }
+
+ return nextIndex;
+ }
+
+ /**
+ * Get next index for horizontal navigation
+ */
+ private getHorizontalIndex(direction: 'left' | 'right'): number {
+ const delta = direction === 'left' ? -1 : 1;
+ let nextIndex = this.currentIndex + delta;
+
+ if (this.wrap) {
+ if (nextIndex < 0) {
+ nextIndex = this.navigableItems.length - 1;
+ } else if (nextIndex >= this.navigableItems.length) {
+ nextIndex = 0;
+ }
+ } else {
+ nextIndex = Math.max(0, Math.min(this.navigableItems.length - 1, nextIndex));
+ }
+
+ return nextIndex;
+ }
+
+ /**
+ * Get next index for grid navigation
+ */
+ private getGridIndex(direction: 'up' | 'down' | 'left' | 'right'): number {
+ if (!this.gridColumns) {
+ // Fallback to regular navigation if no columns specified
+ return direction === 'up' || direction === 'down'
+ ? this.getVerticalIndex(direction as 'up' | 'down')
+ : this.getHorizontalIndex(direction as 'left' | 'right');
+ }
+
+ const currentRow = Math.floor(this.currentIndex / this.gridColumns);
+ const currentCol = this.currentIndex % this.gridColumns;
+ let nextIndex = this.currentIndex;
+
+ switch (direction) {
+ case 'up':
+ if (currentRow > 0) {
+ nextIndex = (currentRow - 1) * this.gridColumns + currentCol;
+ } else if (this.wrap) {
+ const lastRow = Math.floor((this.navigableItems.length - 1) / this.gridColumns);
+ nextIndex = Math.min(lastRow * this.gridColumns + currentCol, this.navigableItems.length - 1);
+ }
+ break;
+
+ case 'down':
+ const nextRow = currentRow + 1;
+ const nextRowIndex = nextRow * this.gridColumns + currentCol;
+ if (nextRowIndex < this.navigableItems.length) {
+ nextIndex = nextRowIndex;
+ } else if (this.wrap) {
+ nextIndex = currentCol < this.navigableItems.length ? currentCol : 0;
+ }
+ break;
+
+ case 'left':
+ if (currentCol > 0) {
+ nextIndex = this.currentIndex - 1;
+ } else if (this.wrap) {
+ const lastInRow = Math.min((currentRow + 1) * this.gridColumns - 1, this.navigableItems.length - 1);
+ nextIndex = lastInRow;
+ }
+ break;
+
+ case 'right':
+ if (currentCol < this.gridColumns - 1 && this.currentIndex + 1 < this.navigableItems.length) {
+ nextIndex = this.currentIndex + 1;
+ } else if (this.wrap) {
+ nextIndex = currentRow * this.gridColumns;
+ }
+ break;
+ }
+
+ return nextIndex;
+ }
+
+ /**
+ * Focus the current item
+ */
+ private focusCurrentItem(): void {
+ if (this.navigableItems[this.currentIndex]) {
+ this.navigableItems[this.currentIndex].focus();
+ }
+ }
+
+ /**
+ * Check if an item is disabled
+ */
+ private isDisabled(item: HTMLElement): boolean {
+ return item.hasAttribute('disabled') ||
+ item.getAttribute('aria-disabled') === 'true' ||
+ item.tabIndex === -1;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-accessibility/src/lib/directives/arrow-navigation/index.ts b/projects/ui-accessibility/src/lib/directives/arrow-navigation/index.ts
new file mode 100644
index 0000000..e1e8b46
--- /dev/null
+++ b/projects/ui-accessibility/src/lib/directives/arrow-navigation/index.ts
@@ -0,0 +1 @@
+export * from './arrow-navigation.directive';
\ No newline at end of file
diff --git a/projects/ui-accessibility/src/lib/directives/focus-trap/focus-trap.directive.ts b/projects/ui-accessibility/src/lib/directives/focus-trap/focus-trap.directive.ts
new file mode 100644
index 0000000..068e665
--- /dev/null
+++ b/projects/ui-accessibility/src/lib/directives/focus-trap/focus-trap.directive.ts
@@ -0,0 +1,210 @@
+import {
+ Directive,
+ ElementRef,
+ Input,
+ OnDestroy,
+ OnInit,
+ NgZone,
+ inject
+} from '@angular/core';
+import { fromEvent, Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+import { A11yConfigService } from '../../services/a11y-config';
+
+@Directive({
+ selector: '[uiFocusTrap]',
+ standalone: true,
+ exportAs: 'uiFocusTrap'
+})
+export class FocusTrapDirective implements OnInit, OnDestroy {
+ private elementRef = inject(ElementRef);
+ private ngZone = inject(NgZone);
+ private config = inject(A11yConfigService);
+
+ @Input('uiFocusTrap') enabled = true;
+ @Input() autoFocus = true;
+ @Input() restoreFocus = true;
+
+ private destroy$ = new Subject();
+ private previouslyFocusedElement: HTMLElement | null = null;
+ private focusableElements: HTMLElement[] = [];
+ private firstFocusable: HTMLElement | null = null;
+ private lastFocusable: HTMLElement | null = null;
+
+ ngOnInit(): void {
+ if (this.enabled) {
+ this.setupFocusTrap();
+ }
+ }
+
+ ngOnDestroy(): void {
+ this.destroy$.next();
+ this.destroy$.complete();
+
+ if (this.restoreFocus && this.previouslyFocusedElement) {
+ this.previouslyFocusedElement.focus();
+ }
+ }
+
+ /**
+ * Enable the focus trap
+ */
+ enable(): void {
+ this.enabled = true;
+ this.setupFocusTrap();
+ }
+
+ /**
+ * Disable the focus trap
+ */
+ disable(): void {
+ this.enabled = false;
+ this.destroy$.next();
+
+ if (this.restoreFocus && this.previouslyFocusedElement) {
+ this.previouslyFocusedElement.focus();
+ }
+ }
+
+ /**
+ * Manually focus the first focusable element
+ */
+ focusFirst(): void {
+ this.updateFocusableElements();
+ if (this.firstFocusable) {
+ this.firstFocusable.focus();
+ }
+ }
+
+ /**
+ * Manually focus the last focusable element
+ */
+ focusLast(): void {
+ this.updateFocusableElements();
+ if (this.lastFocusable) {
+ this.lastFocusable.focus();
+ }
+ }
+
+ /**
+ * Set up the focus trap
+ */
+ private setupFocusTrap(): void {
+ if (!this.enabled || typeof document === 'undefined') return;
+
+ // Store the currently focused element to restore later
+ this.previouslyFocusedElement = document.activeElement as HTMLElement;
+
+ // Set up the trap
+ setTimeout(() => {
+ this.updateFocusableElements();
+ this.setupKeyboardListeners();
+
+ if (this.autoFocus) {
+ this.focusFirst();
+ }
+ }, 0);
+ }
+
+ /**
+ * Update the list of focusable elements
+ */
+ private updateFocusableElements(): void {
+ const container = this.elementRef.nativeElement;
+
+ const focusableSelectors = [
+ 'a[href]:not([disabled])',
+ 'button:not([disabled])',
+ 'textarea:not([disabled])',
+ 'input:not([disabled])',
+ 'select:not([disabled])',
+ '[tabindex]:not([tabindex="-1"]):not([disabled])',
+ '[contenteditable="true"]:not([disabled])',
+ 'audio[controls]:not([disabled])',
+ 'video[controls]:not([disabled])',
+ 'iframe:not([disabled])',
+ 'object:not([disabled])',
+ 'embed:not([disabled])',
+ 'area[href]:not([disabled])',
+ 'summary:not([disabled])'
+ ].join(', ');
+
+ const elements = container.querySelectorAll(focusableSelectors);
+ this.focusableElements = Array.from(elements)
+ .filter((element) => {
+ const htmlElement = element as HTMLElement;
+ // Filter out hidden elements
+ return (
+ htmlElement.offsetWidth > 0 ||
+ htmlElement.offsetHeight > 0 ||
+ htmlElement.getClientRects().length > 0
+ );
+ }) as HTMLElement[];
+
+ this.firstFocusable = this.focusableElements[0] || null;
+ this.lastFocusable = this.focusableElements[this.focusableElements.length - 1] || null;
+ }
+
+ /**
+ * Set up keyboard event listeners
+ */
+ private setupKeyboardListeners(): void {
+ this.ngZone.runOutsideAngular(() => {
+ fromEvent(document, 'keydown').pipe(
+ takeUntil(this.destroy$)
+ ).subscribe(event => {
+ if (event.key === 'Tab') {
+ this.handleTabKey(event);
+ }
+ });
+
+ // Update focusable elements when DOM changes
+ const observer = new MutationObserver(() => {
+ this.updateFocusableElements();
+ });
+
+ observer.observe(this.elementRef.nativeElement, {
+ childList: true,
+ subtree: true,
+ attributes: true,
+ attributeFilter: ['tabindex', 'disabled', 'hidden']
+ });
+
+ // Clean up observer
+ this.destroy$.subscribe(() => observer.disconnect());
+ });
+ }
+
+ /**
+ * Handle Tab key press
+ */
+ private handleTabKey(event: KeyboardEvent): void {
+ if (!this.enabled || this.focusableElements.length === 0) return;
+
+ // Check if focus is within our trap
+ const activeElement = document.activeElement as HTMLElement;
+ const isWithinTrap = this.elementRef.nativeElement.contains(activeElement);
+
+ if (!isWithinTrap) {
+ // Focus escaped the trap, bring it back
+ event.preventDefault();
+ this.focusFirst();
+ return;
+ }
+
+ // Handle normal tab navigation within trap
+ if (event.shiftKey) {
+ // Shift + Tab (backward)
+ if (activeElement === this.firstFocusable) {
+ event.preventDefault();
+ this.focusLast();
+ }
+ } else {
+ // Tab (forward)
+ if (activeElement === this.lastFocusable) {
+ event.preventDefault();
+ this.focusFirst();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-accessibility/src/lib/directives/focus-trap/index.ts b/projects/ui-accessibility/src/lib/directives/focus-trap/index.ts
new file mode 100644
index 0000000..9922766
--- /dev/null
+++ b/projects/ui-accessibility/src/lib/directives/focus-trap/index.ts
@@ -0,0 +1 @@
+export * from './focus-trap.directive';
\ No newline at end of file
diff --git a/projects/ui-accessibility/src/lib/services/a11y-config/a11y-config.service.ts b/projects/ui-accessibility/src/lib/services/a11y-config/a11y-config.service.ts
new file mode 100644
index 0000000..fbf153e
--- /dev/null
+++ b/projects/ui-accessibility/src/lib/services/a11y-config/a11y-config.service.ts
@@ -0,0 +1,237 @@
+import { Injectable, InjectionToken } from '@angular/core';
+
+export interface A11yConfiguration {
+ // Live announcer settings
+ announcer?: {
+ defaultPoliteness?: 'polite' | 'assertive';
+ defaultDuration?: number;
+ enabled?: boolean;
+ };
+
+ // Focus management settings
+ focusManagement?: {
+ trapFocus?: boolean;
+ restoreFocus?: boolean;
+ focusVisibleEnabled?: boolean;
+ focusOriginDetection?: boolean;
+ };
+
+ // Keyboard navigation settings
+ keyboard?: {
+ enableShortcuts?: boolean;
+ enableArrowNavigation?: boolean;
+ enableRovingTabindex?: boolean;
+ customShortcuts?: Array<{
+ key: string;
+ ctrlKey?: boolean;
+ shiftKey?: boolean;
+ altKey?: boolean;
+ description?: string;
+ }>;
+ };
+
+ // Skip links settings
+ skipLinks?: {
+ enabled?: boolean;
+ position?: 'top' | 'after-header';
+ showOnFocus?: boolean;
+ links?: Array<{
+ href: string;
+ text: string;
+ }>;
+ };
+
+ // Auto-enhancement settings
+ autoEnhancement?: {
+ enabled?: boolean;
+ enhanceComponents?: string[]; // Component selectors to enhance
+ addAriaAttributes?: boolean;
+ addKeyboardSupport?: boolean;
+ addFocusManagement?: boolean;
+ };
+
+ // High contrast and reduced motion
+ accessibility?: {
+ respectReducedMotion?: boolean;
+ respectHighContrast?: boolean;
+ injectAccessibilityStyles?: boolean;
+ addBodyClasses?: boolean;
+ };
+
+ // Development settings
+ development?: {
+ warnings?: boolean;
+ logging?: boolean;
+ accessibilityAuditing?: boolean;
+ };
+}
+
+export const DEFAULT_A11Y_CONFIG: Required = {
+ announcer: {
+ defaultPoliteness: 'polite',
+ defaultDuration: 4000,
+ enabled: true
+ },
+ focusManagement: {
+ trapFocus: true,
+ restoreFocus: true,
+ focusVisibleEnabled: true,
+ focusOriginDetection: true
+ },
+ keyboard: {
+ enableShortcuts: true,
+ enableArrowNavigation: true,
+ enableRovingTabindex: true,
+ customShortcuts: []
+ },
+ skipLinks: {
+ enabled: true,
+ position: 'top',
+ showOnFocus: true,
+ links: [
+ { href: '#main', text: 'Skip to main content' },
+ { href: '#navigation', text: 'Skip to navigation' }
+ ]
+ },
+ autoEnhancement: {
+ enabled: false,
+ enhanceComponents: [],
+ addAriaAttributes: true,
+ addKeyboardSupport: true,
+ addFocusManagement: true
+ },
+ accessibility: {
+ respectReducedMotion: true,
+ respectHighContrast: true,
+ injectAccessibilityStyles: true,
+ addBodyClasses: true
+ },
+ development: {
+ warnings: true,
+ logging: false,
+ accessibilityAuditing: false
+ }
+};
+
+export const A11Y_CONFIG = new InjectionToken('A11Y_CONFIG');
+
+@Injectable({
+ providedIn: 'root'
+})
+export class A11yConfigService {
+ private config: Required;
+
+ constructor() {
+ this.config = { ...DEFAULT_A11Y_CONFIG };
+ }
+
+ /**
+ * Update the configuration
+ */
+ updateConfig(config: Partial): void {
+ this.config = this.mergeConfig(this.config, config);
+ }
+
+ /**
+ * Get the current configuration
+ */
+ getConfig(): Required {
+ return { ...this.config };
+ }
+
+ /**
+ * Get a specific configuration section
+ */
+ getSection(section: K): Required[K] {
+ return this.config[section];
+ }
+
+ /**
+ * Check if a feature is enabled
+ */
+ isEnabled(feature: string): boolean {
+ const parts = feature.split('.');
+ let current: any = this.config;
+
+ for (const part of parts) {
+ if (current && typeof current === 'object' && part in current) {
+ current = current[part];
+ } else {
+ return false;
+ }
+ }
+
+ return current === true;
+ }
+
+ /**
+ * Log a message if logging is enabled
+ */
+ log(message: string, ...args: any[]): void {
+ if (this.config.development.logging) {
+ console.log(`[A11y]`, message, ...args);
+ }
+ }
+
+ /**
+ * Log a warning if warnings are enabled
+ */
+ warn(message: string, ...args: any[]): void {
+ if (this.config.development.warnings) {
+ console.warn(`[A11y Warning]`, message, ...args);
+ }
+ }
+
+ /**
+ * Log an error
+ */
+ error(message: string, ...args: any[]): void {
+ console.error(`[A11y Error]`, message, ...args);
+ }
+
+ /**
+ * Deep merge configuration objects
+ */
+ private mergeConfig(
+ base: Required,
+ override: Partial
+ ): Required {
+ const result = { ...base };
+
+ for (const key in override) {
+ const value = override[key as keyof A11yConfiguration];
+ if (value !== undefined) {
+ if (typeof value === 'object' && !Array.isArray(value) && value !== null) {
+ (result as any)[key] = this.mergeObjects(
+ (result as any)[key] || {},
+ value
+ );
+ } else {
+ (result as any)[key] = value;
+ }
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Deep merge objects
+ */
+ private mergeObjects(base: any, override: any): any {
+ const result = { ...base };
+
+ for (const key in override) {
+ const value = override[key];
+ if (value !== undefined) {
+ if (typeof value === 'object' && !Array.isArray(value) && value !== null) {
+ result[key] = this.mergeObjects(result[key] || {}, value);
+ } else {
+ result[key] = value;
+ }
+ }
+ }
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-accessibility/src/lib/services/a11y-config/index.ts b/projects/ui-accessibility/src/lib/services/a11y-config/index.ts
new file mode 100644
index 0000000..5b38985
--- /dev/null
+++ b/projects/ui-accessibility/src/lib/services/a11y-config/index.ts
@@ -0,0 +1 @@
+export * from './a11y-config.service';
\ No newline at end of file
diff --git a/projects/ui-accessibility/src/lib/services/focus-monitor/focus-monitor.service.ts b/projects/ui-accessibility/src/lib/services/focus-monitor/focus-monitor.service.ts
new file mode 100644
index 0000000..edd444d
--- /dev/null
+++ b/projects/ui-accessibility/src/lib/services/focus-monitor/focus-monitor.service.ts
@@ -0,0 +1,202 @@
+import { Injectable, NgZone, OnDestroy, ElementRef } from '@angular/core';
+import { Observable, Subject, fromEvent, merge } from 'rxjs';
+import { takeUntil, map, distinctUntilChanged } from 'rxjs/operators';
+
+export type FocusOrigin = 'keyboard' | 'mouse' | 'touch' | 'program' | null;
+
+export interface FocusMonitorOptions {
+ detectSubtleFocusChanges?: boolean;
+ checkChildren?: boolean;
+}
+
+export interface FocusEvent {
+ origin: FocusOrigin;
+ event: Event | null;
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class FocusMonitorService implements OnDestroy {
+ private destroy$ = new Subject();
+ private currentFocusOrigin: FocusOrigin = null;
+ private lastFocusOrigin: FocusOrigin = null;
+ private lastTouchTarget: EventTarget | null = null;
+
+ private monitoredElements = new Map;
+ unlisten: () => void;
+ }>();
+
+ constructor(private ngZone: NgZone) {
+ this.setupGlobalListeners();
+ }
+
+ ngOnDestroy(): void {
+ this.destroy$.next();
+ this.destroy$.complete();
+
+ // Clean up all monitored elements
+ this.monitoredElements.forEach((info) => info.unlisten());
+ this.monitoredElements.clear();
+ }
+
+ /**
+ * Monitor focus changes on an element
+ */
+ monitor(
+ element: Element | ElementRef,
+ options: FocusMonitorOptions = {}
+ ): Observable {
+ const elementToMonitor = element instanceof ElementRef ? element.nativeElement : element;
+
+ if (this.monitoredElements.has(elementToMonitor)) {
+ return this.monitoredElements.get(elementToMonitor)!.subject.asObservable();
+ }
+
+ const subject = new Subject();
+
+ const focusHandler = (event: FocusEvent) => {
+ subject.next(event);
+ };
+
+ const blurHandler = () => {
+ subject.next({ origin: null, event: null });
+ };
+
+ // Set up event listeners
+ const focusListener = this.ngZone.runOutsideAngular(() => {
+ return fromEvent(elementToMonitor, 'focus', { capture: true }).pipe(
+ takeUntil(this.destroy$),
+ map((event: Event) => ({
+ origin: this.currentFocusOrigin,
+ event
+ }))
+ ).subscribe(focusHandler);
+ });
+
+ const blurListener = this.ngZone.runOutsideAngular(() => {
+ return fromEvent(elementToMonitor, 'blur', { capture: true }).pipe(
+ takeUntil(this.destroy$)
+ ).subscribe(blurHandler);
+ });
+
+ const unlisten = () => {
+ focusListener.unsubscribe();
+ blurListener.unsubscribe();
+ subject.complete();
+ };
+
+ this.monitoredElements.set(elementToMonitor, { subject, unlisten });
+
+ return subject.asObservable().pipe(
+ distinctUntilChanged((x, y) => x.origin === y.origin)
+ );
+ }
+
+ /**
+ * Stop monitoring focus changes on an element
+ */
+ stopMonitoring(element: Element | ElementRef): void {
+ const elementToStop = element instanceof ElementRef ? element.nativeElement : element;
+ const info = this.monitoredElements.get(elementToStop);
+
+ if (info) {
+ info.unlisten();
+ this.monitoredElements.delete(elementToStop);
+ }
+ }
+
+ /**
+ * Focus an element and mark it as programmatically focused
+ */
+ focusVia(element: Element | ElementRef, origin: FocusOrigin): void {
+ const elementToFocus = element instanceof ElementRef ? element.nativeElement : element;
+
+ this.setOrigin(origin);
+
+ if (typeof (elementToFocus as any).focus === 'function') {
+ (elementToFocus as HTMLElement).focus();
+ }
+ }
+
+ /**
+ * Get the current focus origin
+ */
+ getCurrentFocusOrigin(): FocusOrigin {
+ return this.currentFocusOrigin;
+ }
+
+ /**
+ * Set the focus origin for the next focus event
+ */
+ private setOrigin(origin: FocusOrigin): void {
+ this.currentFocusOrigin = origin;
+ this.lastFocusOrigin = origin;
+ }
+
+ /**
+ * Set up global event listeners to detect focus origin
+ */
+ private setupGlobalListeners(): void {
+ if (typeof document === 'undefined') return;
+
+ this.ngZone.runOutsideAngular(() => {
+ // Mouse events
+ merge(
+ fromEvent(document, 'mousedown', { capture: true }),
+ fromEvent(document, 'mouseup', { capture: true })
+ ).pipe(
+ takeUntil(this.destroy$)
+ ).subscribe(() => {
+ this.setOrigin('mouse');
+ });
+
+ // Keyboard events
+ fromEvent(document, 'keydown', { capture: true }).pipe(
+ takeUntil(this.destroy$)
+ ).subscribe((event) => {
+ const keyEvent = event as KeyboardEvent;
+ // Only set keyboard origin for navigation keys
+ if (this.isNavigationKey(keyEvent.key)) {
+ this.setOrigin('keyboard');
+ }
+ });
+
+ // Touch events
+ fromEvent(document, 'touchstart', { capture: true }).pipe(
+ takeUntil(this.destroy$)
+ ).subscribe((event) => {
+ const touchEvent = event as TouchEvent;
+ this.lastTouchTarget = touchEvent.target;
+ this.setOrigin('touch');
+ });
+
+ // Focus events that might be programmatic
+ fromEvent(document, 'focus', { capture: true }).pipe(
+ takeUntil(this.destroy$)
+ ).subscribe((event) => {
+ // If we don't have an origin, assume it's programmatic
+ if (this.currentFocusOrigin === null) {
+ this.setOrigin('program');
+ }
+
+ // Reset origin after a short delay
+ setTimeout(() => {
+ this.currentFocusOrigin = null;
+ }, 1);
+ });
+ });
+ }
+
+ /**
+ * Check if a key is a navigation key
+ */
+ private isNavigationKey(key: string): boolean {
+ const navigationKeys = [
+ 'Tab', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight',
+ 'Home', 'End', 'PageUp', 'PageDown', 'Enter', ' ', 'Escape'
+ ];
+ return navigationKeys.includes(key);
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-accessibility/src/lib/services/focus-monitor/index.ts b/projects/ui-accessibility/src/lib/services/focus-monitor/index.ts
new file mode 100644
index 0000000..5ff99e5
--- /dev/null
+++ b/projects/ui-accessibility/src/lib/services/focus-monitor/index.ts
@@ -0,0 +1 @@
+export * from './focus-monitor.service';
\ No newline at end of file
diff --git a/projects/ui-accessibility/src/lib/services/high-contrast/high-contrast.service.ts b/projects/ui-accessibility/src/lib/services/high-contrast/high-contrast.service.ts
new file mode 100644
index 0000000..60c35af
--- /dev/null
+++ b/projects/ui-accessibility/src/lib/services/high-contrast/high-contrast.service.ts
@@ -0,0 +1,224 @@
+import { Injectable, OnDestroy, signal } from '@angular/core';
+import { Observable, fromEvent, Subject } from 'rxjs';
+import { takeUntil, map, startWith, distinctUntilChanged } from 'rxjs/operators';
+
+export interface AccessibilityPreferences {
+ prefersReducedMotion: boolean;
+ prefersHighContrast: boolean;
+ prefersDarkMode: boolean;
+ prefersReducedTransparency: boolean;
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class HighContrastService implements OnDestroy {
+ private destroy$ = new Subject();
+
+ // Reactive signals for preferences
+ readonly prefersReducedMotion = signal(false);
+ readonly prefersHighContrast = signal(false);
+ readonly prefersDarkMode = signal(false);
+ readonly prefersReducedTransparency = signal(false);
+
+ // Media query lists
+ private reducedMotionQuery?: MediaQueryList;
+ private highContrastQuery?: MediaQueryList;
+ private darkModeQuery?: MediaQueryList;
+ private reducedTransparencyQuery?: MediaQueryList;
+
+ constructor() {
+ this.setupMediaQueries();
+ this.updatePreferences();
+ }
+
+ ngOnDestroy(): void {
+ this.destroy$.next();
+ this.destroy$.complete();
+ }
+
+ /**
+ * Get all accessibility preferences as an observable
+ */
+ getPreferences$(): Observable {
+ if (typeof window === 'undefined') {
+ return new Observable(subscriber => {
+ subscriber.next({
+ prefersReducedMotion: false,
+ prefersHighContrast: false,
+ prefersDarkMode: false,
+ prefersReducedTransparency: false
+ });
+ });
+ }
+
+ return fromEvent(window, 'change').pipe(
+ takeUntil(this.destroy$),
+ startWith(null),
+ map(() => this.getCurrentPreferences()),
+ distinctUntilChanged((prev, curr) =>
+ prev.prefersReducedMotion === curr.prefersReducedMotion &&
+ prev.prefersHighContrast === curr.prefersHighContrast &&
+ prev.prefersDarkMode === curr.prefersDarkMode &&
+ prev.prefersReducedTransparency === curr.prefersReducedTransparency
+ )
+ );
+ }
+
+ /**
+ * Get current accessibility preferences
+ */
+ getCurrentPreferences(): AccessibilityPreferences {
+ return {
+ prefersReducedMotion: this.prefersReducedMotion(),
+ prefersHighContrast: this.prefersHighContrast(),
+ prefersDarkMode: this.prefersDarkMode(),
+ prefersReducedTransparency: this.prefersReducedTransparency()
+ };
+ }
+
+ /**
+ * Apply CSS classes to document body based on preferences
+ */
+ applyPreferencesToBody(): void {
+ if (typeof document === 'undefined') return;
+
+ const body = document.body;
+
+ // Remove existing classes
+ body.classList.remove(
+ 'prefers-reduced-motion',
+ 'prefers-high-contrast',
+ 'prefers-dark-mode',
+ 'prefers-reduced-transparency'
+ );
+
+ // Add classes based on current preferences
+ if (this.prefersReducedMotion()) {
+ body.classList.add('prefers-reduced-motion');
+ }
+
+ if (this.prefersHighContrast()) {
+ body.classList.add('prefers-high-contrast');
+ }
+
+ if (this.prefersDarkMode()) {
+ body.classList.add('prefers-dark-mode');
+ }
+
+ if (this.prefersReducedTransparency()) {
+ body.classList.add('prefers-reduced-transparency');
+ }
+ }
+
+ /**
+ * Get CSS custom properties for motion duration based on reduced motion preference
+ */
+ getMotionDuration(normalDuration: string, reducedDuration = '0.01s'): string {
+ return this.prefersReducedMotion() ? reducedDuration : normalDuration;
+ }
+
+ /**
+ * Get appropriate animation timing based on reduced motion preference
+ */
+ getAnimationConfig(normal: any, reduced: any = { duration: '0.01s', easing: 'linear' }): any {
+ return this.prefersReducedMotion() ? reduced : normal;
+ }
+
+ /**
+ * Check if user prefers reduced motion and return appropriate CSS
+ */
+ getReducedMotionCSS(): string {
+ if (this.prefersReducedMotion()) {
+ return `
+ *, *::before, *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ scroll-behavior: auto !important;
+ }
+ `;
+ }
+ return '';
+ }
+
+ /**
+ * Setup media query listeners
+ */
+ private setupMediaQueries(): void {
+ if (typeof window === 'undefined') return;
+
+ // Reduced motion
+ this.reducedMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
+ this.reducedMotionQuery.addEventListener('change', () => {
+ this.updatePreferences();
+ this.applyPreferencesToBody();
+ });
+
+ // High contrast
+ this.highContrastQuery = window.matchMedia('(prefers-contrast: high)');
+ this.highContrastQuery.addEventListener('change', () => {
+ this.updatePreferences();
+ this.applyPreferencesToBody();
+ });
+
+ // Dark mode
+ this.darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');
+ this.darkModeQuery.addEventListener('change', () => {
+ this.updatePreferences();
+ this.applyPreferencesToBody();
+ });
+
+ // Reduced transparency
+ this.reducedTransparencyQuery = window.matchMedia('(prefers-reduced-transparency: reduce)');
+ this.reducedTransparencyQuery.addEventListener('change', () => {
+ this.updatePreferences();
+ this.applyPreferencesToBody();
+ });
+
+ // Apply initial preferences
+ this.applyPreferencesToBody();
+ }
+
+ /**
+ * Update preference signals
+ */
+ private updatePreferences(): void {
+ this.prefersReducedMotion.set(this.reducedMotionQuery?.matches ?? false);
+ this.prefersHighContrast.set(this.highContrastQuery?.matches ?? false);
+ this.prefersDarkMode.set(this.darkModeQuery?.matches ?? false);
+ this.prefersReducedTransparency.set(this.reducedTransparencyQuery?.matches ?? false);
+ }
+
+ /**
+ * Create a style element with reduced motion CSS
+ */
+ injectReducedMotionStyles(): void {
+ if (typeof document === 'undefined') return;
+
+ const existingStyle = document.getElementById('a11y-reduced-motion');
+ if (existingStyle) return;
+
+ const style = document.createElement('style');
+ style.id = 'a11y-reduced-motion';
+ style.textContent = `
+ @media (prefers-reduced-motion: reduce) {
+ *, *::before, *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ scroll-behavior: auto !important;
+ }
+ }
+
+ @media (prefers-contrast: high) {
+ * {
+ text-shadow: none !important;
+ box-shadow: none !important;
+ }
+ }
+ `;
+
+ document.head.appendChild(style);
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-accessibility/src/lib/services/high-contrast/index.ts b/projects/ui-accessibility/src/lib/services/high-contrast/index.ts
new file mode 100644
index 0000000..7ecc697
--- /dev/null
+++ b/projects/ui-accessibility/src/lib/services/high-contrast/index.ts
@@ -0,0 +1 @@
+export * from './high-contrast.service';
\ No newline at end of file
diff --git a/projects/ui-accessibility/src/lib/services/keyboard-manager/index.ts b/projects/ui-accessibility/src/lib/services/keyboard-manager/index.ts
new file mode 100644
index 0000000..4cb3aa7
--- /dev/null
+++ b/projects/ui-accessibility/src/lib/services/keyboard-manager/index.ts
@@ -0,0 +1 @@
+export * from './keyboard-manager.service';
\ No newline at end of file
diff --git a/projects/ui-accessibility/src/lib/services/keyboard-manager/keyboard-manager.service.ts b/projects/ui-accessibility/src/lib/services/keyboard-manager/keyboard-manager.service.ts
new file mode 100644
index 0000000..8d246c5
--- /dev/null
+++ b/projects/ui-accessibility/src/lib/services/keyboard-manager/keyboard-manager.service.ts
@@ -0,0 +1,251 @@
+import { Injectable, NgZone, OnDestroy } from '@angular/core';
+import { fromEvent, Subject } from 'rxjs';
+import { takeUntil, filter } from 'rxjs/operators';
+
+export interface KeyboardShortcut {
+ key: string;
+ ctrlKey?: boolean;
+ shiftKey?: boolean;
+ altKey?: boolean;
+ metaKey?: boolean;
+ preventDefault?: boolean;
+ stopPropagation?: boolean;
+ description?: string;
+ handler: (event: KeyboardEvent) => void;
+}
+
+export interface KeyboardShortcutRegistration {
+ id: string;
+ shortcut: KeyboardShortcut;
+ element?: Element;
+ priority?: number;
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class KeyboardManagerService implements OnDestroy {
+ private destroy$ = new Subject();
+ private shortcuts = new Map();
+ private globalShortcuts: KeyboardShortcutRegistration[] = [];
+ private elementShortcuts = new Map();
+
+ constructor(private ngZone: NgZone) {
+ this.setupGlobalListener();
+ }
+
+ ngOnDestroy(): void {
+ this.destroy$.next();
+ this.destroy$.complete();
+ this.shortcuts.clear();
+ this.globalShortcuts.length = 0;
+ this.elementShortcuts.clear();
+ }
+
+ /**
+ * Register a global keyboard shortcut
+ */
+ registerGlobalShortcut(
+ id: string,
+ shortcut: KeyboardShortcut,
+ priority = 0
+ ): void {
+ const registration: KeyboardShortcutRegistration = {
+ id,
+ shortcut,
+ priority
+ };
+
+ this.shortcuts.set(id, registration);
+ this.globalShortcuts.push(registration);
+
+ // Sort by priority (higher priority first)
+ this.globalShortcuts.sort((a, b) => (b.priority || 0) - (a.priority || 0));
+ }
+
+ /**
+ * Register a keyboard shortcut for a specific element
+ */
+ registerElementShortcut(
+ id: string,
+ element: Element,
+ shortcut: KeyboardShortcut,
+ priority = 0
+ ): void {
+ const registration: KeyboardShortcutRegistration = {
+ id,
+ shortcut,
+ element,
+ priority
+ };
+
+ this.shortcuts.set(id, registration);
+
+ if (!this.elementShortcuts.has(element)) {
+ this.elementShortcuts.set(element, []);
+ this.setupElementListener(element);
+ }
+
+ const elementShortcuts = this.elementShortcuts.get(element)!;
+ elementShortcuts.push(registration);
+
+ // Sort by priority (higher priority first)
+ elementShortcuts.sort((a, b) => (b.priority || 0) - (a.priority || 0));
+ }
+
+ /**
+ * Unregister a keyboard shortcut
+ */
+ unregisterShortcut(id: string): void {
+ const registration = this.shortcuts.get(id);
+ if (!registration) return;
+
+ if (registration.element) {
+ // Remove from element shortcuts
+ const elementShortcuts = this.elementShortcuts.get(registration.element);
+ if (elementShortcuts) {
+ const index = elementShortcuts.findIndex(s => s.id === id);
+ if (index >= 0) {
+ elementShortcuts.splice(index, 1);
+ }
+
+ // Clean up if no shortcuts left for this element
+ if (elementShortcuts.length === 0) {
+ this.elementShortcuts.delete(registration.element);
+ }
+ }
+ } else {
+ // Remove from global shortcuts
+ const index = this.globalShortcuts.findIndex(s => s.id === id);
+ if (index >= 0) {
+ this.globalShortcuts.splice(index, 1);
+ }
+ }
+
+ this.shortcuts.delete(id);
+ }
+
+ /**
+ * Get all registered shortcuts
+ */
+ getShortcuts(): KeyboardShortcutRegistration[] {
+ return Array.from(this.shortcuts.values());
+ }
+
+ /**
+ * Get shortcuts for a specific element
+ */
+ getElementShortcuts(element: Element): KeyboardShortcutRegistration[] {
+ return this.elementShortcuts.get(element) || [];
+ }
+
+ /**
+ * Get global shortcuts
+ */
+ getGlobalShortcuts(): KeyboardShortcutRegistration[] {
+ return [...this.globalShortcuts];
+ }
+
+ /**
+ * Unregister all keyboard shortcuts
+ */
+ unregisterAll(): void {
+ this.shortcuts.clear();
+ this.globalShortcuts.length = 0;
+ this.elementShortcuts.clear();
+ }
+
+ /**
+ * Check if a keyboard event matches a shortcut
+ */
+ private matchesShortcut(event: KeyboardEvent, shortcut: KeyboardShortcut): boolean {
+ if (event.key.toLowerCase() !== shortcut.key.toLowerCase()) {
+ return false;
+ }
+
+ return (
+ !!event.ctrlKey === !!shortcut.ctrlKey &&
+ !!event.shiftKey === !!shortcut.shiftKey &&
+ !!event.altKey === !!shortcut.altKey &&
+ !!event.metaKey === !!shortcut.metaKey
+ );
+ }
+
+ /**
+ * Handle keyboard event for shortcuts
+ */
+ private handleKeyboardEvent(
+ event: KeyboardEvent,
+ shortcuts: KeyboardShortcutRegistration[]
+ ): boolean {
+ for (const registration of shortcuts) {
+ if (this.matchesShortcut(event, registration.shortcut)) {
+ if (registration.shortcut.preventDefault) {
+ event.preventDefault();
+ }
+
+ if (registration.shortcut.stopPropagation) {
+ event.stopPropagation();
+ }
+
+ this.ngZone.run(() => {
+ registration.shortcut.handler(event);
+ });
+
+ return true; // Shortcut handled
+ }
+ }
+
+ return false; // No shortcut matched
+ }
+
+ /**
+ * Set up global keyboard event listener
+ */
+ private setupGlobalListener(): void {
+ if (typeof document === 'undefined') return;
+
+ this.ngZone.runOutsideAngular(() => {
+ fromEvent(document, 'keydown').pipe(
+ takeUntil(this.destroy$),
+ filter(event => this.globalShortcuts.length > 0)
+ ).subscribe(event => {
+ this.handleKeyboardEvent(event, this.globalShortcuts);
+ });
+ });
+ }
+
+ /**
+ * Set up keyboard event listener for a specific element
+ */
+ private setupElementListener(element: Element): void {
+ this.ngZone.runOutsideAngular(() => {
+ fromEvent(element, 'keydown').pipe(
+ takeUntil(this.destroy$)
+ ).subscribe(event => {
+ const shortcuts = this.elementShortcuts.get(element) || [];
+ this.handleKeyboardEvent(event, shortcuts);
+ });
+ });
+ }
+
+ /**
+ * Create a shortcut description for display purposes
+ */
+ static getShortcutDescription(shortcut: KeyboardShortcut): string {
+ if (shortcut.description) {
+ return shortcut.description;
+ }
+
+ const parts: string[] = [];
+
+ if (shortcut.ctrlKey) parts.push('Ctrl');
+ if (shortcut.altKey) parts.push('Alt');
+ if (shortcut.shiftKey) parts.push('Shift');
+ if (shortcut.metaKey) parts.push('Meta');
+
+ parts.push(shortcut.key.toUpperCase());
+
+ return parts.join(' + ');
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-accessibility/src/lib/services/live-announcer/index.ts b/projects/ui-accessibility/src/lib/services/live-announcer/index.ts
new file mode 100644
index 0000000..b7e4d7d
--- /dev/null
+++ b/projects/ui-accessibility/src/lib/services/live-announcer/index.ts
@@ -0,0 +1 @@
+export * from './live-announcer.service';
\ No newline at end of file
diff --git a/projects/ui-accessibility/src/lib/services/live-announcer/live-announcer.service.ts b/projects/ui-accessibility/src/lib/services/live-announcer/live-announcer.service.ts
new file mode 100644
index 0000000..2590beb
--- /dev/null
+++ b/projects/ui-accessibility/src/lib/services/live-announcer/live-announcer.service.ts
@@ -0,0 +1,152 @@
+import { Injectable, OnDestroy } from '@angular/core';
+
+export type AriaLivePoliteness = 'off' | 'polite' | 'assertive';
+
+export interface LiveAnnouncementConfig {
+ politeness?: AriaLivePoliteness;
+ duration?: number;
+ clearPrevious?: boolean;
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class LiveAnnouncerService implements OnDestroy {
+ private liveElement?: HTMLElement;
+ private timeouts: Map = new Map();
+
+ constructor() {
+ this.createLiveElement();
+ }
+
+ ngOnDestroy(): void {
+ this.timeouts.forEach(timeout => clearTimeout(timeout));
+ this.removeLiveElement();
+ }
+
+ /**
+ * Announces a message to screen readers
+ */
+ announce(
+ message: string,
+ politeness: AriaLivePoliteness = 'polite',
+ duration?: number
+ ): void {
+ this.announceWithConfig(message, {
+ politeness,
+ duration,
+ clearPrevious: true
+ });
+ }
+
+ /**
+ * Announces a message with full configuration options
+ */
+ announceWithConfig(message: string, config: LiveAnnouncementConfig = {}): void {
+ const {
+ politeness = 'polite',
+ duration = 4000,
+ clearPrevious = true
+ } = config;
+
+ if (!this.liveElement) {
+ this.createLiveElement();
+ }
+
+ if (!this.liveElement) return;
+
+ // Clear any existing timeout for this politeness level
+ if (this.timeouts.has(politeness)) {
+ clearTimeout(this.timeouts.get(politeness));
+ }
+
+ // Clear previous message if requested
+ if (clearPrevious) {
+ this.liveElement.textContent = '';
+ }
+
+ // Set the politeness level
+ this.liveElement.setAttribute('aria-live', politeness);
+
+ // Announce the message
+ setTimeout(() => {
+ if (this.liveElement) {
+ this.liveElement.textContent = message;
+ }
+ }, 100);
+
+ // Clear the message after the specified duration
+ if (duration > 0) {
+ const timeoutId = setTimeout(() => {
+ if (this.liveElement) {
+ this.liveElement.textContent = '';
+ }
+ this.timeouts.delete(politeness);
+ }, duration);
+
+ this.timeouts.set(politeness, timeoutId);
+ }
+ }
+
+ /**
+ * Clears all current announcements
+ */
+ clear(): void {
+ if (this.liveElement) {
+ this.liveElement.textContent = '';
+ }
+ this.timeouts.forEach(timeout => clearTimeout(timeout));
+ this.timeouts.clear();
+ }
+
+ /**
+ * Announces multiple messages with delays between them
+ */
+ announceSequence(
+ messages: string[],
+ politeness: AriaLivePoliteness = 'polite',
+ delayBetween = 1000
+ ): void {
+ messages.forEach((message, index) => {
+ setTimeout(() => {
+ this.announce(message, politeness);
+ }, index * delayBetween);
+ });
+ }
+
+ /**
+ * Creates the live region element
+ */
+ private createLiveElement(): void {
+ if (typeof document === 'undefined') return;
+
+ // Remove existing element if it exists
+ this.removeLiveElement();
+
+ // Create new live region
+ this.liveElement = document.createElement('div');
+ this.liveElement.setAttribute('aria-live', 'polite');
+ this.liveElement.setAttribute('aria-atomic', 'true');
+ this.liveElement.setAttribute('id', 'a11y-live-announcer');
+
+ // Make it invisible but still accessible to screen readers
+ this.liveElement.style.position = 'absolute';
+ this.liveElement.style.left = '-10000px';
+ this.liveElement.style.width = '1px';
+ this.liveElement.style.height = '1px';
+ this.liveElement.style.overflow = 'hidden';
+
+ document.body.appendChild(this.liveElement);
+ }
+
+ /**
+ * Removes the live region element
+ */
+ private removeLiveElement(): void {
+ const existing = document.getElementById('a11y-live-announcer');
+ if (existing) {
+ existing.remove();
+ }
+ this.liveElement = undefined;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-accessibility/src/lib/tokens/a11y-tokens.scss b/projects/ui-accessibility/src/lib/tokens/a11y-tokens.scss
new file mode 100644
index 0000000..7a92788
--- /dev/null
+++ b/projects/ui-accessibility/src/lib/tokens/a11y-tokens.scss
@@ -0,0 +1,77 @@
+// ==========================================================================
+// ACCESSIBILITY TOKENS
+// ==========================================================================
+// Import semantic tokens for accessibility features
+// ==========================================================================
+
+// Import semantic tokens from design system
+@use 'ui-design-system/src/styles/semantic/colors' as colors;
+@use 'ui-design-system/src/styles/semantic/motion' as motion;
+@use 'ui-design-system/src/styles/semantic/spacing' as spacing;
+@use 'ui-design-system/src/styles/semantic/shadows' as shadows;
+@use 'ui-design-system/src/styles/semantic/borders' as borders;
+@use 'ui-design-system/src/styles/semantic/typography' as typography;
+
+// FOCUS INDICATOR TOKENS
+$a11y-focus-color: colors.$semantic-color-focus !default;
+$a11y-focus-ring-color: colors.$semantic-color-focus-ring !default;
+$a11y-focus-ring-width: borders.$semantic-border-focus-width !default;
+$a11y-focus-shadow: shadows.$semantic-shadow-input-focus !default;
+$a11y-focus-transition-duration: motion.$semantic-motion-duration-fast !default;
+$a11y-focus-transition-timing: motion.$semantic-motion-easing-ease-out !default;
+
+// SKIP LINKS TOKENS
+$a11y-skip-link-bg: colors.$semantic-color-surface-primary !default;
+$a11y-skip-link-color: colors.$semantic-color-text-primary !default;
+$a11y-skip-link-border: colors.$semantic-color-border-focus !default;
+$a11y-skip-link-padding-x: spacing.$semantic-spacing-interactive-button-padding-x !default;
+$a11y-skip-link-padding-y: spacing.$semantic-spacing-interactive-button-padding-y !default;
+$a11y-skip-link-border-radius: borders.$semantic-border-radius-md !default;
+$a11y-skip-link-shadow: shadows.$semantic-shadow-button-focus !default;
+$a11y-skip-link-z-index: 10000 !default;
+
+// SCREEN READER ONLY TOKENS
+$a11y-sr-only-position: absolute !default;
+$a11y-sr-only-width: 1px !default;
+$a11y-sr-only-height: 1px !default;
+$a11y-sr-only-margin: -1px !default;
+$a11y-sr-only-padding: 0 !default;
+$a11y-sr-only-overflow: hidden !default;
+$a11y-sr-only-clip: rect(0, 0, 0, 0) !default;
+$a11y-sr-only-border: 0 !default;
+
+// HIGH CONTRAST MODE TOKENS
+$a11y-high-contrast-border-color: ButtonText !default;
+$a11y-high-contrast-bg-color: ButtonFace !default;
+$a11y-high-contrast-text-color: ButtonText !default;
+$a11y-high-contrast-focus-color: Highlight !default;
+
+// REDUCED MOTION TOKENS
+$a11y-reduced-motion-duration: motion.$semantic-motion-duration-instant !default;
+$a11y-reduced-motion-easing: motion.$semantic-motion-easing-linear !default;
+
+// TOUCH TARGET TOKENS
+$a11y-min-touch-target: spacing.$semantic-spacing-interactive-touch-target !default;
+
+// LIVE REGION TOKENS
+$a11y-live-region-position: absolute !default;
+$a11y-live-region-left: -10000px !default;
+$a11y-live-region-width: 1px !default;
+$a11y-live-region-height: 1px !default;
+$a11y-live-region-overflow: hidden !default;
+
+// ERROR AND SUCCESS COLORS FOR HIGH CONTRAST
+$a11y-error-color: colors.$semantic-color-error !default;
+$a11y-success-color: colors.$semantic-color-success !default;
+$a11y-warning-color: colors.$semantic-color-warning !default;
+
+// ANIMATION TOKENS FOR ACCESSIBILITY
+$a11y-animation-fade-duration: motion.$semantic-motion-duration-normal !default;
+$a11y-animation-slide-duration: motion.$semantic-motion-duration-normal !default;
+$a11y-animation-scale-duration: motion.$semantic-motion-duration-fast !default;
+
+// FOCUS TRAP TOKENS
+$a11y-focus-trap-outline-color: colors.$semantic-color-focus !default;
+$a11y-focus-trap-outline-width: 2px !default;
+$a11y-focus-trap-outline-style: solid !default;
+$a11y-focus-trap-outline-offset: 2px !default;
\ No newline at end of file
diff --git a/projects/ui-accessibility/src/lib/ui-accessibility.module.ts b/projects/ui-accessibility/src/lib/ui-accessibility.module.ts
new file mode 100644
index 0000000..a2b7635
--- /dev/null
+++ b/projects/ui-accessibility/src/lib/ui-accessibility.module.ts
@@ -0,0 +1,106 @@
+import { NgModule, ModuleWithProviders, inject, APP_INITIALIZER } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+// Services
+import { LiveAnnouncerService } from './services/live-announcer';
+import { FocusMonitorService } from './services/focus-monitor';
+import { KeyboardManagerService } from './services/keyboard-manager';
+import { HighContrastService } from './services/high-contrast';
+import { A11yConfigService, A11yConfiguration, A11Y_CONFIG } from './services/a11y-config';
+
+// Directives
+import { FocusTrapDirective } from './directives/focus-trap';
+import { ArrowNavigationDirective } from './directives/arrow-navigation';
+
+// Components
+import { SkipLinksComponent } from './components/skip-links';
+import { ScreenReaderOnlyComponent } from './components/screen-reader-only';
+
+const DIRECTIVES = [
+ FocusTrapDirective,
+ ArrowNavigationDirective
+];
+
+const COMPONENTS = [
+ SkipLinksComponent,
+ ScreenReaderOnlyComponent
+];
+
+/**
+ * Initialize accessibility features based on configuration
+ */
+export function initializeA11y(
+ config: A11yConfigService,
+ highContrast: HighContrastService
+): () => void {
+ return () => {
+ // Apply body classes for accessibility preferences
+ if (config.isEnabled('accessibility.addBodyClasses')) {
+ highContrast.applyPreferencesToBody();
+ }
+
+ // Inject accessibility styles
+ if (config.isEnabled('accessibility.injectAccessibilityStyles')) {
+ highContrast.injectReducedMotionStyles();
+ }
+
+ // Log initialization
+ config.log('UI Accessibility module initialized', config.getConfig());
+ };
+}
+
+@NgModule({
+ declarations: [],
+ imports: [
+ CommonModule,
+ ...DIRECTIVES,
+ ...COMPONENTS
+ ],
+ exports: [
+ ...DIRECTIVES,
+ ...COMPONENTS
+ ]
+})
+export class UiAccessibilityModule {
+ static forRoot(config?: Partial): ModuleWithProviders {
+ return {
+ ngModule: UiAccessibilityModule,
+ providers: [
+ // Core services
+ LiveAnnouncerService,
+ FocusMonitorService,
+ KeyboardManagerService,
+ HighContrastService,
+ A11yConfigService,
+
+ // Configuration
+ {
+ provide: A11Y_CONFIG,
+ useValue: config || {}
+ },
+
+ // Initialize accessibility features
+ {
+ provide: APP_INITIALIZER,
+ useFactory: initializeA11y,
+ deps: [A11yConfigService, HighContrastService],
+ multi: true
+ }
+ ]
+ };
+ }
+
+ static forChild(): ModuleWithProviders {
+ return {
+ ngModule: UiAccessibilityModule,
+ providers: []
+ };
+ }
+
+ constructor() {
+ // Module loaded
+ if (typeof console !== 'undefined') {
+ console.log('🔍 UI Accessibility module loaded');
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-accessibility/src/lib/ui-accessibility.service.spec.ts b/projects/ui-accessibility/src/lib/ui-accessibility.service.spec.ts
new file mode 100644
index 0000000..b45234f
--- /dev/null
+++ b/projects/ui-accessibility/src/lib/ui-accessibility.service.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { UiAccessibilityService } from './ui-accessibility.service';
+
+describe('UiAccessibilityService', () => {
+ let service: UiAccessibilityService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(UiAccessibilityService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/projects/ui-accessibility/src/lib/ui-accessibility.service.ts b/projects/ui-accessibility/src/lib/ui-accessibility.service.ts
new file mode 100644
index 0000000..a9df8be
--- /dev/null
+++ b/projects/ui-accessibility/src/lib/ui-accessibility.service.ts
@@ -0,0 +1,9 @@
+import { Injectable } from '@angular/core';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class UiAccessibilityService {
+
+ constructor() { }
+}
diff --git a/projects/ui-accessibility/src/lib/utilities/a11y-utilities.scss b/projects/ui-accessibility/src/lib/utilities/a11y-utilities.scss
new file mode 100644
index 0000000..97d88b1
--- /dev/null
+++ b/projects/ui-accessibility/src/lib/utilities/a11y-utilities.scss
@@ -0,0 +1,243 @@
+// ==========================================================================
+// ACCESSIBILITY UTILITIES
+// ==========================================================================
+// Main accessibility utility file that imports all utilities
+// ==========================================================================
+
+@use '../tokens/a11y-tokens' as tokens;
+
+// Import all utility modules
+@forward 'focus-visible';
+@forward 'screen-reader';
+
+// Skip links styles
+.skip-links {
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: tokens.$a11y-skip-link-z-index;
+
+ .skip-link {
+ position: absolute;
+ top: -40px;
+ left: 6px;
+ background: tokens.$a11y-skip-link-bg;
+ color: tokens.$a11y-skip-link-color;
+ padding: tokens.$a11y-skip-link-padding-y tokens.$a11y-skip-link-padding-x;
+ text-decoration: none;
+ border: 1px solid tokens.$a11y-skip-link-border;
+ border-radius: tokens.$a11y-skip-link-border-radius;
+ box-shadow: tokens.$a11y-skip-link-shadow;
+ opacity: 0;
+ pointer-events: none;
+ transform: translateY(-10px);
+ transition: all tokens.$a11y-focus-transition-duration tokens.$a11y-focus-transition-timing;
+
+ &:focus {
+ top: 6px;
+ opacity: 1;
+ pointer-events: auto;
+ transform: translateY(0);
+ }
+
+ // Multiple skip links positioning
+ &:nth-child(2):focus {
+ top: 50px;
+ }
+
+ &:nth-child(3):focus {
+ top: 94px;
+ }
+ }
+}
+
+// Touch target sizing
+.touch-target {
+ min-height: tokens.$a11y-min-touch-target;
+ min-width: tokens.$a11y-min-touch-target;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+
+// Error announcements
+.error-announcement {
+ @extend .sr-only;
+
+ &.error-visible {
+ position: static;
+ width: auto;
+ height: auto;
+ overflow: visible;
+ clip: auto;
+ white-space: normal;
+ color: tokens.$a11y-error-color;
+ font-weight: 600;
+ margin-top: 4px;
+ }
+}
+
+// Loading announcements
+.loading-announcement {
+ @extend .sr-only;
+}
+
+// Keyboard navigation indicators
+.keyboard-navigation {
+ // Hide mouse focus indicators when using keyboard navigation
+ &.using-mouse {
+ * {
+ &:focus:not(:focus-visible) {
+ outline: none;
+ box-shadow: none;
+ }
+ }
+ }
+
+ // Enhanced focus indicators when using keyboard
+ &.using-keyboard {
+ *:focus-visible {
+ outline: 2px solid tokens.$a11y-focus-color;
+ outline-offset: 2px;
+ }
+ }
+}
+
+// High contrast mode utilities
+@media (prefers-contrast: high) {
+ .high-contrast-border {
+ border: 1px solid tokens.$a11y-high-contrast-border-color !important;
+ }
+
+ .high-contrast-bg {
+ background: tokens.$a11y-high-contrast-bg-color !important;
+ color: tokens.$a11y-high-contrast-text-color !important;
+ }
+
+ // Remove shadows and transparency in high contrast
+ * {
+ text-shadow: none !important;
+ box-shadow: none !important;
+ }
+
+ // Ensure sufficient contrast for interactive elements
+ button,
+ [role="button"],
+ a,
+ input,
+ select,
+ textarea {
+ border: 1px solid tokens.$a11y-high-contrast-border-color;
+ }
+}
+
+// Reduced motion utilities
+@media (prefers-reduced-motion: reduce) {
+ .respect-reduced-motion {
+ * {
+ animation-duration: tokens.$a11y-reduced-motion-duration !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: tokens.$a11y-reduced-motion-duration !important;
+ scroll-behavior: auto !important;
+ }
+ }
+}
+
+// ARIA live region styling
+.aria-live-region {
+ @extend .live-region;
+
+ &.aria-live-assertive {
+ speak: literal;
+ }
+
+ &.aria-live-polite {
+ speak: spell-out;
+ }
+}
+
+// Focus management utilities
+.focus-within-highlight {
+ &:focus-within {
+ outline: 1px solid tokens.$a11y-focus-color;
+ outline-offset: 2px;
+ }
+}
+
+// Accessible hide/show animations
+@keyframes fadeInA11y {
+ from {
+ opacity: 0;
+ transform: translateY(-10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes fadeOutA11y {
+ from {
+ opacity: 1;
+ transform: translateY(0);
+ }
+ to {
+ opacity: 0;
+ transform: translateY(-10px);
+ }
+}
+
+.fade-in-a11y {
+ animation: fadeInA11y tokens.$a11y-animation-fade-duration tokens.$a11y-focus-transition-timing;
+}
+
+.fade-out-a11y {
+ animation: fadeOutA11y tokens.$a11y-animation-fade-duration tokens.$a11y-focus-transition-timing;
+}
+
+// Respect reduced motion for animations
+@media (prefers-reduced-motion: reduce) {
+ .fade-in-a11y,
+ .fade-out-a11y {
+ animation-duration: tokens.$a11y-reduced-motion-duration;
+ animation-timing-function: tokens.$a11y-reduced-motion-easing;
+ }
+}
+
+// Arrow navigation visual indicators (dev mode)
+.arrow-navigation-active {
+ &.arrow-navigation-debug {
+ position: relative;
+
+ &::before {
+ content: '← ↑ ↓ → Arrow Navigation';
+ position: absolute;
+ top: -20px;
+ right: 0;
+ font-size: 12px;
+ color: tokens.$a11y-focus-color;
+ background: tokens.$a11y-skip-link-bg;
+ padding: 2px 6px;
+ border-radius: 3px;
+ z-index: 1000;
+ pointer-events: none;
+ }
+ }
+
+ // Highlight navigable items
+ [tabindex="0"] {
+ position: relative;
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: -2px;
+ left: -2px;
+ right: -2px;
+ bottom: -2px;
+ border: 1px dashed tokens.$a11y-focus-color;
+ pointer-events: none;
+ opacity: 0.3;
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-accessibility/src/lib/utilities/focus-visible.scss b/projects/ui-accessibility/src/lib/utilities/focus-visible.scss
new file mode 100644
index 0000000..31a2ea8
--- /dev/null
+++ b/projects/ui-accessibility/src/lib/utilities/focus-visible.scss
@@ -0,0 +1,145 @@
+// ==========================================================================
+// FOCUS VISIBLE UTILITIES
+// ==========================================================================
+// Focus management styles using semantic tokens
+// ==========================================================================
+
+@use '../tokens/a11y-tokens' as tokens;
+
+// Base focus visible styles
+.focus-visible,
+:focus-visible {
+ outline: tokens.$a11y-focus-trap-outline-width tokens.$a11y-focus-trap-outline-style tokens.$a11y-focus-outline-color;
+ outline-offset: tokens.$a11y-focus-trap-outline-offset;
+ transition: outline-color tokens.$a11y-focus-transition-duration tokens.$a11y-focus-transition-timing;
+}
+
+// Remove default focus outline for mouse users
+:focus:not(:focus-visible) {
+ outline: none;
+ box-shadow: none;
+}
+
+// Focus ring for interactive elements
+.focus-ring {
+ position: relative;
+
+ &:focus-visible::after {
+ content: '';
+ position: absolute;
+ top: -2px;
+ left: -2px;
+ right: -2px;
+ bottom: -2px;
+ border: tokens.$a11y-focus-ring-width solid tokens.$a11y-focus-color;
+ border-radius: inherit;
+ pointer-events: none;
+ transition: opacity tokens.$a11y-focus-transition-duration tokens.$a11y-focus-transition-timing;
+ opacity: 1;
+ }
+
+ &:not(:focus-visible)::after {
+ opacity: 0;
+ }
+}
+
+// Focus styles for buttons
+.btn-focus-visible,
+button:focus-visible,
+[role="button"]:focus-visible {
+ outline: tokens.$a11y-focus-trap-outline-width solid tokens.$a11y-focus-color;
+ outline-offset: 2px;
+ box-shadow: tokens.$a11y-focus-shadow;
+}
+
+// Focus styles for form inputs
+.input-focus-visible,
+input:focus-visible,
+textarea:focus-visible,
+select:focus-visible {
+ border-color: tokens.$a11y-focus-color;
+ box-shadow: tokens.$a11y-focus-shadow;
+ outline: none;
+}
+
+// Focus styles for links
+.link-focus-visible,
+a:focus-visible {
+ outline: tokens.$a11y-focus-trap-outline-width solid tokens.$a11y-focus-color;
+ outline-offset: 2px;
+ text-decoration: underline;
+}
+
+// High contrast mode support
+@media (prefers-contrast: high) {
+ .focus-visible,
+ :focus-visible {
+ outline-color: tokens.$a11y-high-contrast-focus-color;
+ outline-width: 3px;
+ }
+
+ .focus-ring:focus-visible::after {
+ border-color: tokens.$a11y-high-contrast-focus-color;
+ border-width: 3px;
+ }
+}
+
+// Reduced motion support
+@media (prefers-reduced-motion: reduce) {
+ .focus-visible,
+ :focus-visible,
+ .focus-ring::after {
+ transition-duration: tokens.$a11y-reduced-motion-duration;
+ transition-timing-function: tokens.$a11y-reduced-motion-easing;
+ }
+}
+
+// Focus trap container styles
+.focus-trap-container {
+ position: relative;
+
+ &.focus-trap-active {
+ isolation: isolate;
+ }
+
+ // Visual indicator for focus trap (dev mode)
+ &.focus-trap-debug {
+ &::before {
+ content: 'Focus Trap Active';
+ position: absolute;
+ top: -20px;
+ left: 0;
+ font-size: 12px;
+ color: tokens.$a11y-focus-color;
+ background: tokens.$a11y-skip-link-bg;
+ padding: 2px 6px;
+ border-radius: 3px;
+ z-index: 1000;
+ }
+ }
+}
+
+// Skip to content on focus trap
+.focus-trap-skip {
+ position: absolute;
+ top: -40px;
+ left: 0;
+ background: tokens.$a11y-skip-link-bg;
+ color: tokens.$a11y-skip-link-color;
+ padding: tokens.$a11y-skip-link-padding-y tokens.$a11y-skip-link-padding-x;
+ text-decoration: none;
+ border: 1px solid tokens.$a11y-skip-link-border;
+ border-radius: tokens.$a11y-skip-link-border-radius;
+ z-index: tokens.$a11y-skip-link-z-index;
+ opacity: 0;
+ pointer-events: none;
+ transform: translateY(-10px);
+ transition: all tokens.$a11y-focus-transition-duration tokens.$a11y-focus-transition-timing;
+
+ &:focus {
+ opacity: 1;
+ pointer-events: auto;
+ transform: translateY(0);
+ box-shadow: tokens.$a11y-skip-link-shadow;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-accessibility/src/lib/utilities/screen-reader.scss b/projects/ui-accessibility/src/lib/utilities/screen-reader.scss
new file mode 100644
index 0000000..0e4b075
--- /dev/null
+++ b/projects/ui-accessibility/src/lib/utilities/screen-reader.scss
@@ -0,0 +1,167 @@
+// ==========================================================================
+// SCREEN READER UTILITIES
+// ==========================================================================
+// Screen reader specific styles using semantic tokens
+// ==========================================================================
+
+@use '../tokens/a11y-tokens' as tokens;
+
+// Screen reader only - visually hidden but accessible
+.sr-only {
+ position: tokens.$a11y-sr-only-position;
+ width: tokens.$a11y-sr-only-width;
+ height: tokens.$a11y-sr-only-height;
+ padding: tokens.$a11y-sr-only-padding;
+ margin: tokens.$a11y-sr-only-margin;
+ overflow: tokens.$a11y-sr-only-overflow;
+ clip: tokens.$a11y-sr-only-clip;
+ white-space: nowrap;
+ border: tokens.$a11y-sr-only-border;
+}
+
+// Screen reader only that becomes visible on focus
+.sr-only-focusable {
+ @extend .sr-only;
+
+ &:focus,
+ &:active {
+ position: static;
+ width: auto;
+ height: auto;
+ padding: tokens.$a11y-skip-link-padding-y tokens.$a11y-skip-link-padding-x;
+ margin: 0;
+ overflow: visible;
+ clip: auto;
+ white-space: normal;
+ background: tokens.$a11y-skip-link-bg;
+ color: tokens.$a11y-skip-link-color;
+ border: 1px solid tokens.$a11y-skip-link-border;
+ border-radius: tokens.$a11y-skip-link-border-radius;
+ box-shadow: tokens.$a11y-skip-link-shadow;
+ z-index: tokens.$a11y-skip-link-z-index;
+ }
+}
+
+// Live region for screen reader announcements
+.live-region {
+ position: tokens.$a11y-live-region-position;
+ left: tokens.$a11y-live-region-left;
+ width: tokens.$a11y-live-region-width;
+ height: tokens.$a11y-live-region-height;
+ overflow: tokens.$a11y-live-region-overflow;
+}
+
+// Status message styling
+.status-message {
+ @extend .sr-only;
+
+ &.status-visible {
+ position: static;
+ width: auto;
+ height: auto;
+ overflow: visible;
+ clip: auto;
+ white-space: normal;
+ padding: tokens.$a11y-skip-link-padding-y tokens.$a11y-skip-link-padding-x;
+ margin: 4px 0;
+ background: tokens.$a11y-skip-link-bg;
+ border-left: 4px solid tokens.$a11y-focus-color;
+ border-radius: 0 tokens.$a11y-skip-link-border-radius tokens.$a11y-skip-link-border-radius 0;
+ }
+
+ &.status-error {
+ border-left-color: tokens.$a11y-error-color;
+ }
+
+ &.status-success {
+ border-left-color: tokens.$a11y-success-color;
+ }
+
+ &.status-warning {
+ border-left-color: tokens.$a11y-warning-color;
+ }
+}
+
+// Screen reader instructions
+.sr-instructions {
+ @extend .sr-only;
+
+ // Common instruction patterns
+ &.sr-required::after {
+ content: ' (required)';
+ }
+
+ &.sr-optional::after {
+ content: ' (optional)';
+ }
+
+ &.sr-expanded::after {
+ content: ' (expanded)';
+ }
+
+ &.sr-collapsed::after {
+ content: ' (collapsed)';
+ }
+
+ &.sr-selected::after {
+ content: ' (selected)';
+ }
+
+ &.sr-current::after {
+ content: ' (current)';
+ }
+}
+
+// Accessible hide/show with transitions
+.a11y-hide {
+ opacity: 0;
+ pointer-events: none;
+ transform: scale(0.95);
+ transition: opacity tokens.$a11y-animation-fade-duration tokens.$a11y-focus-transition-timing,
+ transform tokens.$a11y-animation-scale-duration tokens.$a11y-focus-transition-timing;
+
+ // Still accessible to screen readers when hidden
+ &:not(.a11y-hide-completely) {
+ position: absolute;
+ left: -9999px;
+ }
+
+ // Completely hidden including from screen readers
+ &.a11y-hide-completely {
+ display: none;
+ }
+}
+
+.a11y-show {
+ opacity: 1;
+ pointer-events: auto;
+ transform: scale(1);
+ transition: opacity tokens.$a11y-animation-fade-duration tokens.$a11y-focus-transition-timing,
+ transform tokens.$a11y-animation-scale-duration tokens.$a11y-focus-transition-timing;
+}
+
+// High contrast mode adjustments
+@media (prefers-contrast: high) {
+ .sr-only-focusable:focus,
+ .sr-only-focusable:active {
+ background: tokens.$a11y-high-contrast-bg-color;
+ color: tokens.$a11y-high-contrast-text-color;
+ border-color: tokens.$a11y-high-contrast-border-color;
+ }
+
+ .status-message.status-visible {
+ background: tokens.$a11y-high-contrast-bg-color;
+ color: tokens.$a11y-high-contrast-text-color;
+ border-color: tokens.$a11y-high-contrast-border-color;
+ }
+}
+
+// Reduced motion support
+@media (prefers-reduced-motion: reduce) {
+ .a11y-hide,
+ .a11y-show,
+ .sr-only-focusable {
+ transition-duration: tokens.$a11y-reduced-motion-duration;
+ transition-timing-function: tokens.$a11y-reduced-motion-easing;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-accessibility/src/public-api.ts b/projects/ui-accessibility/src/public-api.ts
new file mode 100644
index 0000000..50b3499
--- /dev/null
+++ b/projects/ui-accessibility/src/public-api.ts
@@ -0,0 +1,21 @@
+/*
+ * Public API Surface of ui-accessibility
+ */
+
+// Main module
+export * from './lib/ui-accessibility.module';
+
+// Services
+export * from './lib/services/live-announcer';
+export * from './lib/services/focus-monitor';
+export * from './lib/services/keyboard-manager';
+export * from './lib/services/high-contrast';
+export * from './lib/services/a11y-config';
+
+// Directives
+export * from './lib/directives/focus-trap';
+export * from './lib/directives/arrow-navigation';
+
+// Components
+export * from './lib/components/skip-links';
+export * from './lib/components/screen-reader-only';
\ No newline at end of file
diff --git a/projects/ui-accessibility/tsconfig.lib.json b/projects/ui-accessibility/tsconfig.lib.json
new file mode 100644
index 0000000..2359bf6
--- /dev/null
+++ b/projects/ui-accessibility/tsconfig.lib.json
@@ -0,0 +1,15 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../out-tsc/lib",
+ "declaration": true,
+ "declarationMap": true,
+ "inlineSources": true,
+ "types": []
+ },
+ "exclude": [
+ "**/*.spec.ts"
+ ]
+}
diff --git a/projects/ui-accessibility/tsconfig.lib.prod.json b/projects/ui-accessibility/tsconfig.lib.prod.json
new file mode 100644
index 0000000..9215caa
--- /dev/null
+++ b/projects/ui-accessibility/tsconfig.lib.prod.json
@@ -0,0 +1,11 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+ "extends": "./tsconfig.lib.json",
+ "compilerOptions": {
+ "declarationMap": false
+ },
+ "angularCompilerOptions": {
+ "compilationMode": "partial"
+ }
+}
diff --git a/projects/ui-accessibility/tsconfig.spec.json b/projects/ui-accessibility/tsconfig.spec.json
new file mode 100644
index 0000000..254686d
--- /dev/null
+++ b/projects/ui-accessibility/tsconfig.spec.json
@@ -0,0 +1,15 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../out-tsc/spec",
+ "types": [
+ "jasmine"
+ ]
+ },
+ "include": [
+ "**/*.spec.ts",
+ "**/*.d.ts"
+ ]
+}
diff --git a/projects/ui-animations/README.md b/projects/ui-animations/README.md
new file mode 100644
index 0000000..927018d
--- /dev/null
+++ b/projects/ui-animations/README.md
@@ -0,0 +1,164 @@
+# UI Animations
+
+A comprehensive CSS animation library with Angular services and directives for programmatic control.
+
+## Features
+
+- **Pure CSS animations** - Framework agnostic, works everywhere
+- **Angular integration** - Services and directives for programmatic control
+- **Comprehensive collection** - Entrance, exit, emphasis, and transition animations
+- **Design system integration** - Uses semantic motion tokens for consistent timing and easing
+- **Utility classes** - Animation timing, delays, and control utilities
+- **SCSS mixins** - Create custom animations easily
+
+## Installation
+
+```bash
+npm install ui-animations
+```
+
+## Usage
+
+### Import Styles
+
+Add to your global styles or component styles:
+
+```scss
+// Complete animation library
+@use 'ui-animations/src/styles' as animations;
+
+// Or import specific modules
+@use 'ui-animations/src/styles/animations/entrances';
+@use 'ui-animations/src/styles/animations/emphasis';
+@use 'ui-animations/src/styles/utilities/animation-utilities';
+```
+
+### CSS Classes
+
+Simply add animation classes to your HTML elements:
+
+```html
+
+Fade in animation
+Delayed slide up
+
+
+Bouncing button
+Pulsing element
+
+
+Fade out animation
+```
+
+### Angular Service
+
+```typescript
+import { UiAnimationsService } from 'ui-animations';
+
+constructor(private animationService: UiAnimationsService) {}
+
+// Animate an element
+animateElement() {
+ const element = document.getElementById('my-element');
+ this.animationService.animateOnce(element, 'animate-bounce');
+}
+
+// Animate with callback
+animateWithCallback() {
+ const element = document.getElementById('my-element');
+ this.animationService.animate(element, 'animate-fade-out', () => {
+ console.log('Animation completed!');
+ });
+}
+```
+
+### Angular Directive
+
+```typescript
+import { AnimateDirective } from 'ui-animations';
+
+@Component({
+ imports: [AnimateDirective]
+})
+```
+
+```html
+
+Auto animates on load
+
+
+
+ Hover to animate
+
+
+
+
+ Click me!
+
+```
+
+## Animation Categories
+
+### Entrances
+- `animate-fade-in` - Basic fade in
+- `animate-fade-in-up/down/left/right` - Fade with direction
+- `animate-slide-in-up/down` - Slide animations
+- `animate-zoom-in` - Scale up animation
+- `animate-rotate-in` - Rotate entrance
+
+### Exits
+- `animate-fade-out` - Basic fade out
+- `animate-fade-out-up/down/left/right` - Fade with direction
+- `animate-slide-out-up/down` - Slide animations
+- `animate-zoom-out` - Scale down animation
+- `animate-rotate-out` - Rotate exit
+
+### Emphasis
+- `animate-bounce` - Bouncing effect
+- `animate-shake` - Horizontal shake
+- `animate-pulse` - Scaling pulse
+- `animate-wobble` - Wobble motion
+- `animate-tada` - Celebration animation
+- `animate-heartbeat` - Heart beat effect
+
+### Utilities
+- `animation-delay-100/200/300/500/1000` - Animation delays
+- `animation-duration-fast/normal/slow/slower` - Duration control
+- `animation-infinite/once/twice` - Iteration control
+- `animation-paused/running` - Playback control
+
+## SCSS Mixins
+
+Create custom animations using provided mixins with semantic tokens:
+
+```scss
+@use 'ui-animations/src/styles/mixins/animation-mixins' as mixins;
+@use 'ui-design-system/src/styles/semantic/motion' as motion;
+
+.my-custom-animation {
+ @include mixins.animate(myKeyframes, motion.$semantic-motion-duration-normal, motion.$semantic-motion-easing-ease-out);
+}
+
+.hover-effect {
+ @include mixins.hover-animation(transform, scale(1.1), motion.$semantic-motion-duration-fast);
+}
+
+.loading-spinner {
+ @include mixins.loading-animation(40px, #007bff);
+}
+```
+
+## Design System Integration
+
+All animations now use semantic motion tokens from the design system:
+
+- **Durations**: `$semantic-motion-duration-fast`, `$semantic-motion-duration-normal`, `$semantic-motion-duration-slow`, etc.
+- **Easing**: `$semantic-motion-easing-ease-out`, `$semantic-motion-easing-spring`, `$semantic-motion-easing-bounce`, etc.
+- **Spacing**: Animation distances use semantic spacing tokens for consistency
+- **Timing**: Delays use semantic transition timing tokens
+
+This ensures animations are consistent with your design system's motion principles.
+
+## Demo
+
+Visit the demo application to see all animations in action and experiment with different options.
\ No newline at end of file
diff --git a/projects/ui-animations/ng-package.json b/projects/ui-animations/ng-package.json
new file mode 100644
index 0000000..9df8d4d
--- /dev/null
+++ b/projects/ui-animations/ng-package.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
+ "dest": "../../dist/ui-animations",
+ "lib": {
+ "entryFile": "src/public-api.ts"
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-animations/package.json b/projects/ui-animations/package.json
new file mode 100644
index 0000000..f514279
--- /dev/null
+++ b/projects/ui-animations/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "ui-animations",
+ "version": "0.0.1",
+ "description": "CSS animation library with Angular services and directives for programmatic control",
+ "keywords": ["angular", "animations", "css", "ui", "transitions"],
+ "peerDependencies": {
+ "@angular/common": "^19.2.0",
+ "@angular/core": "^19.2.0"
+ },
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "sideEffects": false
+}
diff --git a/projects/ui-animations/src/lib/animate.directive.ts b/projects/ui-animations/src/lib/animate.directive.ts
new file mode 100644
index 0000000..4d8c622
--- /dev/null
+++ b/projects/ui-animations/src/lib/animate.directive.ts
@@ -0,0 +1,66 @@
+import { Directive, ElementRef, Input, OnInit, OnDestroy } from '@angular/core';
+import { UiAnimationsService } from './ui-animations.service';
+
+@Directive({
+ selector: '[uiAnimate]',
+ standalone: true
+})
+export class AnimateDirective implements OnInit, OnDestroy {
+ @Input('uiAnimate') animationClass: string = '';
+ @Input() animationTrigger: 'immediate' | 'hover' | 'click' | 'manual' = 'immediate';
+ @Input() animationOnce: boolean = false;
+
+ private hoverHandler?: () => void;
+ private clickHandler?: () => void;
+
+ constructor(
+ private elementRef: ElementRef,
+ private animationService: UiAnimationsService
+ ) {}
+
+ ngOnInit() {
+ if (this.animationTrigger === 'immediate') {
+ this.animate();
+ } else if (this.animationTrigger === 'hover') {
+ this.setupHoverTrigger();
+ } else if (this.animationTrigger === 'click') {
+ this.setupClickTrigger();
+ }
+ }
+
+ ngOnDestroy() {
+ if (this.hoverHandler) {
+ this.elementRef.nativeElement.removeEventListener('mouseenter', this.hoverHandler);
+ }
+ if (this.clickHandler) {
+ this.elementRef.nativeElement.removeEventListener('click', this.clickHandler);
+ }
+ }
+
+ private setupHoverTrigger() {
+ this.hoverHandler = () => this.animate();
+ this.elementRef.nativeElement.addEventListener('mouseenter', this.hoverHandler);
+ }
+
+ private setupClickTrigger() {
+ this.clickHandler = () => this.animate();
+ this.elementRef.nativeElement.addEventListener('click', this.clickHandler);
+ }
+
+ private animate() {
+ if (!this.animationClass) return;
+
+ if (this.animationOnce) {
+ this.animationService.animateOnce(this.elementRef.nativeElement, this.animationClass);
+ } else {
+ this.animationService.animate(this.elementRef.nativeElement, this.animationClass);
+ }
+ }
+
+ /**
+ * Manually trigger animation (for manual trigger mode)
+ */
+ public trigger() {
+ this.animate();
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-animations/src/lib/ui-animations.service.spec.ts b/projects/ui-animations/src/lib/ui-animations.service.spec.ts
new file mode 100644
index 0000000..0c9b25b
--- /dev/null
+++ b/projects/ui-animations/src/lib/ui-animations.service.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { UiAnimationsService } from './ui-animations.service';
+
+describe('UiAnimationsService', () => {
+ let service: UiAnimationsService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(UiAnimationsService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/projects/ui-animations/src/lib/ui-animations.service.ts b/projects/ui-animations/src/lib/ui-animations.service.ts
new file mode 100644
index 0000000..8fb2c34
--- /dev/null
+++ b/projects/ui-animations/src/lib/ui-animations.service.ts
@@ -0,0 +1,46 @@
+import { Injectable } from '@angular/core';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class UiAnimationsService {
+
+ /**
+ * Add animation class to element with optional completion callback
+ */
+ animate(element: HTMLElement, animationClass: string, onComplete?: () => void): Promise {
+ return new Promise((resolve) => {
+ const handleAnimationEnd = () => {
+ element.removeEventListener('animationend', handleAnimationEnd);
+ if (onComplete) onComplete();
+ resolve();
+ };
+
+ element.addEventListener('animationend', handleAnimationEnd);
+ element.classList.add(animationClass);
+ });
+ }
+
+ /**
+ * Remove animation class from element
+ */
+ removeAnimation(element: HTMLElement, animationClass: string): void {
+ element.classList.remove(animationClass);
+ }
+
+ /**
+ * Add animation class and automatically remove it after completion
+ */
+ animateOnce(element: HTMLElement, animationClass: string): Promise {
+ return this.animate(element, animationClass, () => {
+ this.removeAnimation(element, animationClass);
+ });
+ }
+
+ /**
+ * Check if element has animation class
+ */
+ hasAnimation(element: HTMLElement, animationClass: string): boolean {
+ return element.classList.contains(animationClass);
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-animations/src/public-api.ts b/projects/ui-animations/src/public-api.ts
new file mode 100644
index 0000000..248ab6d
--- /dev/null
+++ b/projects/ui-animations/src/public-api.ts
@@ -0,0 +1,6 @@
+/*
+ * Public API Surface of ui-animations
+ */
+
+export * from './lib/ui-animations.service';
+export * from './lib/animate.directive';
\ No newline at end of file
diff --git a/projects/ui-animations/src/styles/animations/_emphasis.scss b/projects/ui-animations/src/styles/animations/_emphasis.scss
new file mode 100644
index 0000000..150486a
--- /dev/null
+++ b/projects/ui-animations/src/styles/animations/_emphasis.scss
@@ -0,0 +1,142 @@
+// Emphasis Animations
+// Attention-grabbing animations for highlighting elements
+
+// Bounce
+@keyframes bounce {
+ 0%, 20%, 53%, 80%, 100% {
+ transform: translateY(0);
+ }
+ 40%, 43% {
+ transform: translateY(-24px);
+ }
+ 70% {
+ transform: translateY(-8px);
+ }
+ 90% {
+ transform: translateY(-4px);
+ }
+}
+
+.animate-bounce {
+ animation: bounce 0.6s ease-in-out;
+}
+
+// Shake
+@keyframes shake {
+ 0%, 100% { transform: translateX(0); }
+ 10%, 30%, 50%, 70%, 90% { transform: translateX(-4px); }
+ 20%, 40%, 60%, 80% { transform: translateX(4px); }
+}
+
+.animate-shake {
+ animation: shake 0.6s ease-in-out;
+}
+
+// Pulse
+@keyframes pulse {
+ 0% { transform: scale(1); }
+ 50% { transform: scale(1.05); }
+ 100% { transform: scale(1); }
+}
+
+.animate-pulse {
+ animation: pulse 0.6s ease-in-out infinite;
+}
+
+// Flash
+@keyframes flash {
+ 0%, 50%, 100% { opacity: 1; }
+ 25%, 75% { opacity: 0; }
+}
+
+.animate-flash {
+ animation: flash 0.6s ease-in-out;
+}
+
+// Wobble
+@keyframes wobble {
+ 0% { transform: translateX(0%); }
+ 15% { transform: translateX(-25%) rotate(-5deg); }
+ 30% { transform: translateX(20%) rotate(3deg); }
+ 45% { transform: translateX(-15%) rotate(-3deg); }
+ 60% { transform: translateX(10%) rotate(2deg); }
+ 75% { transform: translateX(-5%) rotate(-1deg); }
+ 100% { transform: translateX(0%); }
+}
+
+.animate-wobble {
+ animation: wobble 0.6s ease-in-out;
+}
+
+// Swing
+@keyframes swing {
+ 20% { transform: rotate(15deg); }
+ 40% { transform: rotate(-10deg); }
+ 60% { transform: rotate(5deg); }
+ 80% { transform: rotate(-5deg); }
+ 100% { transform: rotate(0deg); }
+}
+
+.animate-swing {
+ animation: swing 0.6s ease-in-out;
+ transform-origin: top center;
+}
+
+// Rubber Band
+@keyframes rubberBand {
+ 0% { transform: scaleX(1); }
+ 30% { transform: scaleX(1.25) scaleY(0.75); }
+ 40% { transform: scaleX(0.75) scaleY(1.25); }
+ 50% { transform: scaleX(1.15) scaleY(0.85); }
+ 65% { transform: scaleX(0.95) scaleY(1.05); }
+ 75% { transform: scaleX(1.05) scaleY(0.95); }
+ 100% { transform: scaleX(1) scaleY(1); }
+}
+
+.animate-rubber-band {
+ animation: rubberBand 0.6s ease-in-out;
+}
+
+// Tada
+@keyframes tada {
+ 0% { transform: scaleX(1) scaleY(1); }
+ 10%, 20% { transform: scaleX(0.9) scaleY(0.9) rotate(-3deg); }
+ 30%, 50%, 70%, 90% { transform: scaleX(1.1) scaleY(1.1) rotate(3deg); }
+ 40%, 60%, 80% { transform: scaleX(1.1) scaleY(1.1) rotate(-3deg); }
+ 100% { transform: scaleX(1) scaleY(1) rotate(0); }
+}
+
+.animate-tada {
+ animation: tada 0.6s ease-in-out;
+}
+
+// Heartbeat
+@keyframes heartbeat {
+ 0% { transform: scale(1); }
+ 14% { transform: scale(1.3); }
+ 28% { transform: scale(1); }
+ 42% { transform: scale(1.3); }
+ 70% { transform: scale(1); }
+}
+
+.animate-heartbeat {
+ animation: heartbeat 0.6s ease-in-out infinite;
+}
+
+// Jello
+@keyframes jello {
+ 11.1% { transform: skewX(-12.5deg) skewY(-12.5deg); }
+ 22.2% { transform: skewX(6.25deg) skewY(6.25deg); }
+ 33.3% { transform: skewX(-3.125deg) skewY(-3.125deg); }
+ 44.4% { transform: skewX(1.5625deg) skewY(1.5625deg); }
+ 55.5% { transform: skewX(-0.78125deg) skewY(-0.78125deg); }
+ 66.6% { transform: skewX(0.390625deg) skewY(0.390625deg); }
+ 77.7% { transform: skewX(-0.1953125deg) skewY(-0.1953125deg); }
+ 88.8% { transform: skewX(0.09765625deg) skewY(0.09765625deg); }
+ 100% { transform: skewX(0) skewY(0); }
+}
+
+.animate-jello {
+ animation: jello 0.6s ease-in-out;
+ transform-origin: center;
+}
\ No newline at end of file
diff --git a/projects/ui-animations/src/styles/animations/_entrances.scss b/projects/ui-animations/src/styles/animations/_entrances.scss
new file mode 100644
index 0000000..6bfb3cd
--- /dev/null
+++ b/projects/ui-animations/src/styles/animations/_entrances.scss
@@ -0,0 +1,151 @@
+// Entrance Animations
+// Elements appearing/entering the view
+
+
+// Fade In
+@keyframes fadeIn {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+.animate-fade-in {
+ animation: fadeIn 0.3s ease-out;
+}
+
+// Fade In Up
+@keyframes fadeInUp {
+ from {
+ opacity: 0;
+ transform: translateY(24px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.animate-fade-in-up {
+ animation: fadeInUp 0.3s ease-out;
+}
+
+// Fade In Down
+@keyframes fadeInDown {
+ from {
+ opacity: 0;
+ transform: translateY(-24px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.animate-fade-in-down {
+ animation: fadeInDown 0.3s ease-out;
+}
+
+// Fade In Left
+@keyframes fadeInLeft {
+ from {
+ opacity: 0;
+ transform: translateX(-24px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+.animate-fade-in-left {
+ animation: fadeInLeft 0.3s ease-out;
+}
+
+// Fade In Right
+@keyframes fadeInRight {
+ from {
+ opacity: 0;
+ transform: translateX(24px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+.animate-fade-in-right {
+ animation: fadeInRight 0.3s ease-out;
+}
+
+// Slide In Up
+@keyframes slideInUp {
+ from {
+ transform: translateY(100%);
+ }
+ to {
+ transform: translateY(0);
+ }
+}
+
+.animate-slide-in-up {
+ animation: slideInUp 0.3s ease-out;
+}
+
+// Slide In Down
+@keyframes slideInDown {
+ from {
+ transform: translateY(-100%);
+ }
+ to {
+ transform: translateY(0);
+ }
+}
+
+.animate-slide-in-down {
+ animation: slideInDown 0.3s ease-out;
+}
+
+// Zoom In
+@keyframes zoomIn {
+ from {
+ opacity: 0;
+ transform: scale(0.3);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+.animate-zoom-in {
+ animation: zoomIn 0.3s ease-out;
+}
+
+// Scale Up
+@keyframes scaleUp {
+ from {
+ transform: scale(0);
+ }
+ to {
+ transform: scale(1);
+ }
+}
+
+.animate-scale-up {
+ animation: scaleUp 0.15s ease-out;
+}
+
+// Rotate In
+@keyframes rotateIn {
+ from {
+ opacity: 0;
+ transform: rotate(-200deg);
+ }
+ to {
+ opacity: 1;
+ transform: rotate(0);
+ }
+}
+
+.animate-rotate-in {
+ animation: rotateIn 0.3s ease-out;
+}
\ No newline at end of file
diff --git a/projects/ui-animations/src/styles/animations/_exits.scss b/projects/ui-animations/src/styles/animations/_exits.scss
new file mode 100644
index 0000000..612d2a8
--- /dev/null
+++ b/projects/ui-animations/src/styles/animations/_exits.scss
@@ -0,0 +1,151 @@
+// Exit Animations
+// Elements disappearing/leaving the view
+
+
+// Fade Out
+@keyframes fadeOut {
+ from { opacity: 1; }
+ to { opacity: 0; }
+}
+
+.animate-fade-out {
+ animation: fadeOut 0.3s ease-in;
+}
+
+// Fade Out Up
+@keyframes fadeOutUp {
+ from {
+ opacity: 1;
+ transform: translateY(0);
+ }
+ to {
+ opacity: 0;
+ transform: translateY(-24px);
+ }
+}
+
+.animate-fade-out-up {
+ animation: fadeOutUp 0.3s ease-in;
+}
+
+// Fade Out Down
+@keyframes fadeOutDown {
+ from {
+ opacity: 1;
+ transform: translateY(0);
+ }
+ to {
+ opacity: 0;
+ transform: translateY(24px);
+ }
+}
+
+.animate-fade-out-down {
+ animation: fadeOutDown 0.3s ease-in;
+}
+
+// Fade Out Left
+@keyframes fadeOutLeft {
+ from {
+ opacity: 1;
+ transform: translateX(0);
+ }
+ to {
+ opacity: 0;
+ transform: translateX(-24px);
+ }
+}
+
+.animate-fade-out-left {
+ animation: fadeOutLeft 0.3s ease-in;
+}
+
+// Fade Out Right
+@keyframes fadeOutRight {
+ from {
+ opacity: 1;
+ transform: translateX(0);
+ }
+ to {
+ opacity: 0;
+ transform: translateX(24px);
+ }
+}
+
+.animate-fade-out-right {
+ animation: fadeOutRight 0.3s ease-in;
+}
+
+// Slide Out Up
+@keyframes slideOutUp {
+ from {
+ transform: translateY(0);
+ }
+ to {
+ transform: translateY(-100%);
+ }
+}
+
+.animate-slide-out-up {
+ animation: slideOutUp 0.3s ease-in;
+}
+
+// Slide Out Down
+@keyframes slideOutDown {
+ from {
+ transform: translateY(0);
+ }
+ to {
+ transform: translateY(100%);
+ }
+}
+
+.animate-slide-out-down {
+ animation: slideOutDown 0.3s ease-in;
+}
+
+// Zoom Out
+@keyframes zoomOut {
+ from {
+ opacity: 1;
+ transform: scale(1);
+ }
+ to {
+ opacity: 0;
+ transform: scale(0.3);
+ }
+}
+
+.animate-zoom-out {
+ animation: zoomOut 0.3s ease-in;
+}
+
+// Scale Down
+@keyframes scaleDown {
+ from {
+ transform: scale(1);
+ }
+ to {
+ transform: scale(0);
+ }
+}
+
+.animate-scale-down {
+ animation: scaleDown 0.15s ease-in;
+}
+
+// Rotate Out
+@keyframes rotateOut {
+ from {
+ opacity: 1;
+ transform: rotate(0);
+ }
+ to {
+ opacity: 0;
+ transform: rotate(200deg);
+ }
+}
+
+.animate-rotate-out {
+ animation: rotateOut 0.3s ease-in;
+}
\ No newline at end of file
diff --git a/projects/ui-animations/src/styles/animations/_transitions.scss b/projects/ui-animations/src/styles/animations/_transitions.scss
new file mode 100644
index 0000000..a94d549
--- /dev/null
+++ b/projects/ui-animations/src/styles/animations/_transitions.scss
@@ -0,0 +1,53 @@
+// Base Transitions
+// Smooth transitions for common properties
+
+// Transition utilities
+.transition-all {
+ transition: all 0.3s ease;
+}
+
+.transition-opacity {
+ transition: opacity 0.3s ease;
+}
+
+.transition-transform {
+ transition: transform 0.3s ease;
+}
+
+.transition-colors {
+ transition: color, background-color, border-color 0.3s ease;
+}
+
+// Transition durations
+.transition-fast {
+ transition-duration: 0.15s;
+}
+
+.transition-normal {
+ transition-duration: 0.3s;
+}
+
+.transition-slow {
+ transition-duration: 0.6s;
+}
+
+// Transition timing functions
+.transition-linear {
+ transition-timing-function: linear;
+}
+
+.transition-ease-in {
+ transition-timing-function: ease-in;
+}
+
+.transition-ease-out {
+ transition-timing-function: ease-out;
+}
+
+.transition-ease-in-out {
+ transition-timing-function: ease-in-out;
+}
+
+.transition-bounce {
+ transition-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55);
+}
\ No newline at end of file
diff --git a/projects/ui-animations/src/styles/index.scss b/projects/ui-animations/src/styles/index.scss
new file mode 100644
index 0000000..48e1232
--- /dev/null
+++ b/projects/ui-animations/src/styles/index.scss
@@ -0,0 +1,21 @@
+// UI Animations - Main Entry Point
+// Import all animation styles
+
+// Animations
+@forward 'animations/transitions';
+@forward 'animations/entrances';
+@forward 'animations/exits';
+@forward 'animations/emphasis';
+
+// Utilities
+@forward 'utilities/animation-utilities';
+
+// Mixins (use @use to access mixins in your components)
+@forward 'mixins/animation-mixins';
+
+// Export all animations and utilities
+@use 'animations/transitions';
+@use 'animations/entrances';
+@use 'animations/exits';
+@use 'animations/emphasis';
+@use 'utilities/animation-utilities';
\ No newline at end of file
diff --git a/projects/ui-animations/src/styles/mixins/_animation-mixins.scss b/projects/ui-animations/src/styles/mixins/_animation-mixins.scss
new file mode 100644
index 0000000..2962e94
--- /dev/null
+++ b/projects/ui-animations/src/styles/mixins/_animation-mixins.scss
@@ -0,0 +1,130 @@
+// Animation Mixins
+// Reusable SCSS mixins for creating custom animations
+
+
+// Generic animation mixin
+@mixin animate(
+ $name,
+ $duration: 0.3s,
+ $timing-function: ease-out,
+ $delay: motion.$semantic-motion-transition-delay-0,
+ $iteration-count: 1,
+ $direction: normal,
+ $fill-mode: both
+) {
+ animation-name: $name;
+ animation-duration: $duration;
+ animation-timing-function: $timing-function;
+ animation-delay: $delay;
+ animation-iteration-count: $iteration-count;
+ animation-direction: $direction;
+ animation-fill-mode: $fill-mode;
+}
+
+// Transition mixin with design system tokens
+@mixin transition(
+ $property: motion.$semantic-motion-transition-property-all,
+ $duration: 0.3s,
+ $timing-function: ease,
+ $delay: motion.$semantic-motion-transition-delay-0
+) {
+ transition-property: $property;
+ transition-duration: $duration;
+ transition-timing-function: $timing-function;
+ transition-delay: $delay;
+}
+
+// Fade animation mixin
+@mixin fade-animation($direction: 'in', $distance: 24px) {
+ @if $direction == 'in' {
+ from {
+ opacity: 0;
+ transform: translateY($distance);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+ } @else {
+ from {
+ opacity: 1;
+ transform: translateY(0);
+ }
+ to {
+ opacity: 0;
+ transform: translateY(-#{$distance});
+ }
+ }
+}
+
+// Slide animation mixin
+@mixin slide-animation($direction: 'up', $distance: 100%) {
+ @if $direction == 'up' {
+ from { transform: translateY($distance); }
+ to { transform: translateY(0); }
+ } @else if $direction == 'down' {
+ from { transform: translateY(-$distance); }
+ to { transform: translateY(0); }
+ } @else if $direction == 'left' {
+ from { transform: translateX($distance); }
+ to { transform: translateX(0); }
+ } @else if $direction == 'right' {
+ from { transform: translateX(-$distance); }
+ to { transform: translateX(0); }
+ }
+}
+
+// Scale animation mixin
+@mixin scale-animation($from: 0, $to: 1) {
+ from {
+ transform: scale($from);
+ }
+ to {
+ transform: scale($to);
+ }
+}
+
+// Rotate animation mixin
+@mixin rotate-animation($from: 0deg, $to: 360deg) {
+ from {
+ transform: rotate($from);
+ }
+ to {
+ transform: rotate($to);
+ }
+}
+
+// Hover animation mixin
+@mixin hover-animation($property: transform, $value: scale(1.05), $duration: 0.15s) {
+ transition: $property $duration ease;
+
+ &:hover {
+ #{$property}: $value;
+ }
+}
+
+// Loading animation mixin
+@mixin loading-animation($size: 40px, $color: currentColor) {
+ width: $size;
+ height: $size;
+ border: 3px solid rgba($color, 0.3);
+ border-radius: 50%;
+ border-top-color: $color;
+ animation: spin 0.6sest ease-in-out infinite;
+}
+
+// Keyframes for common animations
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+// Staggered animation mixin for lists
+@mixin staggered-animation($base-delay: motion.$semantic-motion-transition-delay-100, $animation: fadeInUp 0.3s ease-out) {
+ @for $i from 1 through 10 {
+ &:nth-child(#{$i}) {
+ animation: $animation;
+ animation-delay: #{$base-delay * $i};
+ animation-fill-mode: both;
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-animations/src/styles/utilities/_animation-utilities.scss b/projects/ui-animations/src/styles/utilities/_animation-utilities.scss
new file mode 100644
index 0000000..74a11ac
--- /dev/null
+++ b/projects/ui-animations/src/styles/utilities/_animation-utilities.scss
@@ -0,0 +1,118 @@
+// Animation Utilities
+// Helper classes for animation control
+
+
+// Animation delays
+.animation-delay-100 {
+ animation-delay: 0.1s;
+}
+
+.animation-delay-200 {
+ animation-delay: 0.2s;
+}
+
+.animation-delay-300 {
+ animation-delay: 0.3s;
+}
+
+.animation-delay-500 {
+ animation-delay: 0.5s;
+}
+
+.animation-delay-1000 {
+ animation-delay: 1s;
+}
+
+// Animation durations
+.animation-duration-fast {
+ animation-duration: 0.15s;
+}
+
+.animation-duration-normal {
+ animation-duration: 0.3s;
+}
+
+.animation-duration-slow {
+ animation-duration: 0.6s;
+}
+
+.animation-duration-slower {
+ animation-duration: 1s;
+}
+
+// Animation fill modes
+.animation-fill-none {
+ animation-fill-mode: none;
+}
+
+.animation-fill-forwards {
+ animation-fill-mode: forwards;
+}
+
+.animation-fill-backwards {
+ animation-fill-mode: backwards;
+}
+
+.animation-fill-both {
+ animation-fill-mode: both;
+}
+
+// Animation iteration counts
+.animation-infinite {
+ animation-iteration-count: infinite;
+}
+
+.animation-once {
+ animation-iteration-count: 1;
+}
+
+.animation-twice {
+ animation-iteration-count: 2;
+}
+
+// Animation play states
+.animation-paused {
+ animation-play-state: paused;
+}
+
+.animation-running {
+ animation-play-state: running;
+}
+
+// Animation directions
+.animation-normal {
+ animation-direction: normal;
+}
+
+.animation-reverse {
+ animation-direction: reverse;
+}
+
+.animation-alternate {
+ animation-direction: alternate;
+}
+
+.animation-alternate-reverse {
+ animation-direction: alternate-reverse;
+}
+
+// Animation timing functions
+.animation-linear {
+ animation-timing-function: linear;
+}
+
+.animation-ease-in {
+ animation-timing-function: ease-in;
+}
+
+.animation-ease-out {
+ animation-timing-function: ease-out;
+}
+
+.animation-ease-in-out {
+ animation-timing-function: ease-in-out;
+}
+
+.animation-bounce-timing {
+ animation-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55);
+}
\ No newline at end of file
diff --git a/projects/ui-animations/tsconfig.lib.json b/projects/ui-animations/tsconfig.lib.json
new file mode 100644
index 0000000..2359bf6
--- /dev/null
+++ b/projects/ui-animations/tsconfig.lib.json
@@ -0,0 +1,15 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../out-tsc/lib",
+ "declaration": true,
+ "declarationMap": true,
+ "inlineSources": true,
+ "types": []
+ },
+ "exclude": [
+ "**/*.spec.ts"
+ ]
+}
diff --git a/projects/ui-animations/tsconfig.lib.prod.json b/projects/ui-animations/tsconfig.lib.prod.json
new file mode 100644
index 0000000..9215caa
--- /dev/null
+++ b/projects/ui-animations/tsconfig.lib.prod.json
@@ -0,0 +1,11 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+ "extends": "./tsconfig.lib.json",
+ "compilerOptions": {
+ "declarationMap": false
+ },
+ "angularCompilerOptions": {
+ "compilationMode": "partial"
+ }
+}
diff --git a/projects/ui-animations/tsconfig.spec.json b/projects/ui-animations/tsconfig.spec.json
new file mode 100644
index 0000000..254686d
--- /dev/null
+++ b/projects/ui-animations/tsconfig.spec.json
@@ -0,0 +1,15 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../out-tsc/spec",
+ "types": [
+ "jasmine"
+ ]
+ },
+ "include": [
+ "**/*.spec.ts",
+ "**/*.d.ts"
+ ]
+}
diff --git a/projects/ui-backgrounds/README.md b/projects/ui-backgrounds/README.md
new file mode 100644
index 0000000..9bc848c
--- /dev/null
+++ b/projects/ui-backgrounds/README.md
@@ -0,0 +1,63 @@
+# UiBackgrounds
+
+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-backgrounds
+```
+
+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-backgrounds
+ ```
+
+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.
diff --git a/projects/ui-backgrounds/ng-package.json b/projects/ui-backgrounds/ng-package.json
new file mode 100644
index 0000000..31b1556
--- /dev/null
+++ b/projects/ui-backgrounds/ng-package.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
+ "dest": "../../dist/ui-backgrounds",
+ "lib": {
+ "entryFile": "src/public-api.ts"
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-backgrounds/package.json b/projects/ui-backgrounds/package.json
new file mode 100644
index 0000000..38fa354
--- /dev/null
+++ b/projects/ui-backgrounds/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "ui-backgrounds",
+ "version": "0.0.1",
+ "peerDependencies": {
+ "@angular/common": "^19.2.0",
+ "@angular/core": "^19.2.0"
+ },
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "sideEffects": false
+}
diff --git a/projects/ui-backgrounds/src/lib/components/backgrounds/gradient-background.component.ts b/projects/ui-backgrounds/src/lib/components/backgrounds/gradient-background.component.ts
new file mode 100644
index 0000000..849bcf9
--- /dev/null
+++ b/projects/ui-backgrounds/src/lib/components/backgrounds/gradient-background.component.ts
@@ -0,0 +1,97 @@
+import { Component, Input, OnInit, OnDestroy, OnChanges, SimpleChanges, ElementRef, inject, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
+import { LinearGradientConfig, RadialGradientConfig, ConicGradientConfig, ColorStop, GradientDirection } from '../../types/background.types';
+import { BackgroundService } from '../../services/background.service';
+
+@Component({
+ selector: 'ui-gradient-background',
+ standalone: true,
+ template: ' ',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ host: {
+ 'class': 'ui-background ui-background--gradient'
+ }
+})
+export class GradientBackgroundComponent implements OnInit, OnDestroy, OnChanges {
+ private readonly elementRef = inject(ElementRef);
+ private readonly backgroundService = inject(BackgroundService);
+ private backgroundId?: string;
+
+ @Input() type: 'linear' | 'radial' | 'conic' = 'linear';
+ @Input() colors: string[] = ['#000000', '#ffffff'];
+ @Input() direction?: GradientDirection | string;
+ @Input() shape?: 'circle' | 'ellipse';
+ @Input() size?: 'closest-side' | 'closest-corner' | 'farthest-side' | 'farthest-corner';
+ @Input() position?: string;
+ @Input() angle?: string | number;
+ @Input() colorStops?: ColorStop[];
+ @Input() fullScreen = false;
+ @Input() zIndex?: number;
+
+ ngOnInit(): void {
+ this.applyBackground();
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ const relevantChanges = ['type', 'colors', 'direction', 'shape', 'size', 'position', 'angle', 'colorStops', 'fullScreen', 'zIndex'];
+ if (relevantChanges.some(key => changes[key])) {
+ this.applyBackground();
+ }
+ }
+
+ ngOnDestroy(): void {
+ if (this.backgroundId) {
+ this.backgroundService.removeBackground(this.backgroundId);
+ }
+ }
+
+ private applyBackground(): void {
+ if (this.backgroundId) {
+ this.backgroundService.removeBackground(this.backgroundId);
+ }
+
+ const colorStops = this.colorStops || this.colors.map(color => ({ color }));
+ let config: LinearGradientConfig | RadialGradientConfig | ConicGradientConfig;
+
+ switch (this.type) {
+ case 'linear':
+ config = {
+ type: 'linear-gradient',
+ direction: this.direction,
+ colors: colorStops
+ };
+ break;
+ case 'radial':
+ config = {
+ type: 'radial-gradient',
+ shape: this.shape,
+ size: this.size,
+ position: this.position,
+ colors: colorStops
+ };
+ break;
+ case 'conic':
+ config = {
+ type: 'conic-gradient',
+ angle: this.angle,
+ position: this.position,
+ colors: colorStops
+ };
+ break;
+ }
+
+ if (this.fullScreen) {
+ this.backgroundId = this.backgroundService.applyFullScreenBackground(config, {
+ zIndex: this.zIndex || -1
+ });
+ } else {
+ this.backgroundId = this.backgroundService.applyBackground(
+ this.elementRef.nativeElement,
+ config
+ );
+ }
+
+ // Update host class
+ this.elementRef.nativeElement.classList.add(`ui-background--${this.type}-gradient`);
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-backgrounds/src/lib/components/backgrounds/image-background.component.ts b/projects/ui-backgrounds/src/lib/components/backgrounds/image-background.component.ts
new file mode 100644
index 0000000..caa0ea0
--- /dev/null
+++ b/projects/ui-backgrounds/src/lib/components/backgrounds/image-background.component.ts
@@ -0,0 +1,76 @@
+import { Component, Input, OnInit, OnDestroy, OnChanges, SimpleChanges, ElementRef, inject, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
+import { ImageBackgroundConfig } from '../../types/background.types';
+import { BackgroundService } from '../../services/background.service';
+
+@Component({
+ selector: 'ui-image-background',
+ standalone: true,
+ template: ' ',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ host: {
+ 'class': 'ui-background ui-background--image'
+ }
+})
+export class ImageBackgroundComponent implements OnInit, OnDestroy, OnChanges {
+ private readonly elementRef = inject(ElementRef);
+ private readonly backgroundService = inject(BackgroundService);
+ private backgroundId?: string;
+
+ @Input({ required: true }) url!: string;
+ @Input() size: 'auto' | 'cover' | 'contain' | string = 'cover';
+ @Input() position = 'center';
+ @Input() repeat: 'repeat' | 'repeat-x' | 'repeat-y' | 'no-repeat' = 'no-repeat';
+ @Input() attachment?: 'scroll' | 'fixed' | 'local';
+ @Input() opacity = 1;
+ @Input() fullScreen = false;
+ @Input() zIndex?: number;
+
+ ngOnInit(): void {
+ this.applyBackground();
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ const relevantChanges = ['url', 'size', 'position', 'repeat', 'attachment', 'opacity', 'fullScreen', 'zIndex'];
+ if (relevantChanges.some(key => changes[key])) {
+ this.applyBackground();
+ }
+ }
+
+ ngOnDestroy(): void {
+ if (this.backgroundId) {
+ this.backgroundService.removeBackground(this.backgroundId);
+ }
+ }
+
+ private applyBackground(): void {
+ if (!this.url) {
+ return;
+ }
+
+ if (this.backgroundId) {
+ this.backgroundService.removeBackground(this.backgroundId);
+ }
+
+ const config: ImageBackgroundConfig = {
+ type: 'image',
+ url: this.url,
+ size: this.size,
+ position: this.position,
+ repeat: this.repeat,
+ attachment: this.attachment,
+ opacity: this.opacity
+ };
+
+ if (this.fullScreen) {
+ this.backgroundId = this.backgroundService.applyFullScreenBackground(config, {
+ zIndex: this.zIndex || -1
+ });
+ } else {
+ this.backgroundId = this.backgroundService.applyBackground(
+ this.elementRef.nativeElement,
+ config
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-backgrounds/src/lib/components/backgrounds/index.ts b/projects/ui-backgrounds/src/lib/components/backgrounds/index.ts
new file mode 100644
index 0000000..425034d
--- /dev/null
+++ b/projects/ui-backgrounds/src/lib/components/backgrounds/index.ts
@@ -0,0 +1,4 @@
+export * from './solid-background.component';
+export * from './gradient-background.component';
+export * from './pattern-background.component';
+export * from './image-background.component';
\ No newline at end of file
diff --git a/projects/ui-backgrounds/src/lib/components/backgrounds/pattern-background.component.ts b/projects/ui-backgrounds/src/lib/components/backgrounds/pattern-background.component.ts
new file mode 100644
index 0000000..c0be398
--- /dev/null
+++ b/projects/ui-backgrounds/src/lib/components/backgrounds/pattern-background.component.ts
@@ -0,0 +1,73 @@
+import { Component, Input, OnInit, OnDestroy, OnChanges, SimpleChanges, ElementRef, inject, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
+import { PatternConfig, PatternType } from '../../types/background.types';
+import { BackgroundService } from '../../services/background.service';
+
+@Component({
+ selector: 'ui-pattern-background',
+ standalone: true,
+ template: ' ',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ host: {
+ 'class': 'ui-background ui-background--pattern'
+ }
+})
+export class PatternBackgroundComponent implements OnInit, OnDestroy, OnChanges {
+ private readonly elementRef = inject(ElementRef);
+ private readonly backgroundService = inject(BackgroundService);
+ private backgroundId?: string;
+
+ @Input() pattern: PatternType = 'dots';
+ @Input() primaryColor = '#000000';
+ @Input() secondaryColor = '#ffffff';
+ @Input() size: string | number = 20;
+ @Input() opacity = 1;
+ @Input() fullScreen = false;
+ @Input() zIndex?: number;
+
+ ngOnInit(): void {
+ this.applyBackground();
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ const relevantChanges = ['pattern', 'primaryColor', 'secondaryColor', 'size', 'opacity', 'fullScreen', 'zIndex'];
+ if (relevantChanges.some(key => changes[key])) {
+ this.applyBackground();
+ }
+ }
+
+ ngOnDestroy(): void {
+ if (this.backgroundId) {
+ this.backgroundService.removeBackground(this.backgroundId);
+ }
+ }
+
+ private applyBackground(): void {
+ if (this.backgroundId) {
+ this.backgroundService.removeBackground(this.backgroundId);
+ }
+
+ const config: PatternConfig = {
+ type: 'pattern',
+ pattern: this.pattern,
+ primaryColor: this.primaryColor,
+ secondaryColor: this.secondaryColor,
+ size: this.size,
+ opacity: this.opacity
+ };
+
+ if (this.fullScreen) {
+ this.backgroundId = this.backgroundService.applyFullScreenBackground(config, {
+ zIndex: this.zIndex || -1
+ });
+ } else {
+ this.backgroundId = this.backgroundService.applyBackground(
+ this.elementRef.nativeElement,
+ config
+ );
+ }
+
+ // Update host class
+ this.elementRef.nativeElement.classList.add(`ui-background--pattern-${this.pattern}`);
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-backgrounds/src/lib/components/backgrounds/solid-background.component.ts b/projects/ui-backgrounds/src/lib/components/backgrounds/solid-background.component.ts
new file mode 100644
index 0000000..4fe63cc
--- /dev/null
+++ b/projects/ui-backgrounds/src/lib/components/backgrounds/solid-background.component.ts
@@ -0,0 +1,61 @@
+import { Component, Input, OnInit, OnDestroy, OnChanges, SimpleChanges, ElementRef, inject, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
+import { SolidBackgroundConfig } from '../../types/background.types';
+import { BackgroundService } from '../../services/background.service';
+
+@Component({
+ selector: 'ui-solid-background',
+ standalone: true,
+ template: ' ',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ host: {
+ 'class': 'ui-background ui-background--solid'
+ }
+})
+export class SolidBackgroundComponent implements OnInit, OnDestroy, OnChanges {
+ private readonly elementRef = inject(ElementRef);
+ private readonly backgroundService = inject(BackgroundService);
+ private backgroundId?: string;
+
+ @Input() color = '#000000';
+ @Input() fullScreen = false;
+ @Input() zIndex?: number;
+
+ ngOnInit(): void {
+ this.applyBackground();
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes['color'] || changes['fullScreen'] || changes['zIndex']) {
+ this.applyBackground();
+ }
+ }
+
+ ngOnDestroy(): void {
+ if (this.backgroundId) {
+ this.backgroundService.removeBackground(this.backgroundId);
+ }
+ }
+
+ private applyBackground(): void {
+ if (this.backgroundId) {
+ this.backgroundService.removeBackground(this.backgroundId);
+ }
+
+ const config: SolidBackgroundConfig = {
+ type: 'solid',
+ color: this.color
+ };
+
+ if (this.fullScreen) {
+ this.backgroundId = this.backgroundService.applyFullScreenBackground(config, {
+ zIndex: this.zIndex || -1
+ });
+ } else {
+ this.backgroundId = this.backgroundService.applyBackground(
+ this.elementRef.nativeElement,
+ config
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-backgrounds/src/lib/components/index.ts b/projects/ui-backgrounds/src/lib/components/index.ts
new file mode 100644
index 0000000..4f91bc3
--- /dev/null
+++ b/projects/ui-backgrounds/src/lib/components/index.ts
@@ -0,0 +1 @@
+export * from './backgrounds/index';
\ No newline at end of file
diff --git a/projects/ui-backgrounds/src/lib/directives/background.directive.ts b/projects/ui-backgrounds/src/lib/directives/background.directive.ts
new file mode 100644
index 0000000..a31de8a
--- /dev/null
+++ b/projects/ui-backgrounds/src/lib/directives/background.directive.ts
@@ -0,0 +1,122 @@
+import {
+ Directive,
+ Input,
+ OnInit,
+ OnDestroy,
+ OnChanges,
+ SimpleChanges,
+ ElementRef,
+ inject
+} from '@angular/core';
+import { BackgroundConfig, BackgroundOptions } from '../types/background.types';
+import { BackgroundService } from '../services/background.service';
+
+@Directive({
+ selector: '[uiBackground]',
+ standalone: true
+})
+export class BackgroundDirective implements OnInit, OnDestroy, OnChanges {
+ private readonly elementRef = inject(ElementRef);
+ private readonly backgroundService = inject(BackgroundService);
+ private backgroundId?: string;
+
+ @Input('uiBackground') config?: BackgroundConfig;
+ @Input() backgroundOptions?: Partial;
+ @Input() backgroundDisabled = false;
+
+ ngOnInit(): void {
+ this.applyBackground();
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes['config'] || changes['backgroundOptions'] || changes['backgroundDisabled']) {
+ if (this.backgroundDisabled) {
+ this.removeBackground();
+ } else {
+ this.applyBackground();
+ }
+ }
+ }
+
+ ngOnDestroy(): void {
+ this.removeBackground();
+ }
+
+ private applyBackground(): void {
+ if (!this.config || this.backgroundDisabled) {
+ return;
+ }
+
+ // Remove existing background if present
+ this.removeBackground();
+
+ // Apply new background
+ this.backgroundId = this.backgroundService.applyBackground(
+ this.elementRef.nativeElement,
+ this.config
+ );
+
+ // Add base CSS class for styling
+ this.elementRef.nativeElement.classList.add('ui-background');
+
+ // Add specific CSS classes based on configuration
+ this.addTypeClasses();
+ }
+
+ private removeBackground(): void {
+ if (this.backgroundId) {
+ this.backgroundService.removeBackground(this.backgroundId);
+ this.backgroundId = undefined;
+ }
+
+ // Remove CSS classes
+ this.removeTypeClasses();
+ this.elementRef.nativeElement.classList.remove('ui-background');
+ }
+
+ private addTypeClasses(): void {
+ if (!this.config) return;
+
+ const element = this.elementRef.nativeElement;
+ element.classList.add(`ui-background--${this.config.type}`);
+
+ // Add additional classes based on type
+ switch (this.config.type) {
+ case 'linear-gradient':
+ case 'radial-gradient':
+ case 'conic-gradient':
+ element.classList.add('ui-background--gradient');
+ break;
+ case 'pattern':
+ element.classList.add('ui-background--pattern');
+ if (this.config.pattern) {
+ element.classList.add(`ui-background--pattern-${this.config.pattern}`);
+ }
+ break;
+ case 'image':
+ element.classList.add('ui-background--image');
+ break;
+ case 'animated':
+ element.classList.add('ui-background--animated');
+ break;
+ case 'glass':
+ element.classList.add('ui-background--glass');
+ break;
+ }
+ }
+
+ private removeTypeClasses(): void {
+ if (!this.config) return;
+
+ const element = this.elementRef.nativeElement;
+ const classesToRemove: string[] = [];
+
+ element.classList.forEach((cls: string) => {
+ if (cls.startsWith('ui-background--')) {
+ classesToRemove.push(cls);
+ }
+ });
+
+ classesToRemove.forEach(cls => element.classList.remove(cls));
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-backgrounds/src/lib/directives/index.ts b/projects/ui-backgrounds/src/lib/directives/index.ts
new file mode 100644
index 0000000..e5537d1
--- /dev/null
+++ b/projects/ui-backgrounds/src/lib/directives/index.ts
@@ -0,0 +1 @@
+export * from './background.directive';
\ No newline at end of file
diff --git a/projects/ui-backgrounds/src/lib/services/background.service.ts b/projects/ui-backgrounds/src/lib/services/background.service.ts
new file mode 100644
index 0000000..7193fb7
--- /dev/null
+++ b/projects/ui-backgrounds/src/lib/services/background.service.ts
@@ -0,0 +1,191 @@
+import { Injectable, signal, computed } from '@angular/core';
+import { BackgroundConfig, BackgroundOptions, BackgroundState } from '../types/background.types';
+import { BackgroundGenerator } from '../utils/background-generator';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class BackgroundService {
+ private _activeBackgrounds = signal>(new Map());
+
+ // Public computed signals
+ readonly activeBackgrounds = computed(() => Array.from(this._activeBackgrounds().values()));
+ readonly hasActiveBackgrounds = computed(() => this._activeBackgrounds().size > 0);
+
+ /**
+ * Apply a background to an element
+ */
+ applyBackground(element: HTMLElement, config: BackgroundConfig, id?: string): string {
+ const backgroundId = id || this.generateId();
+ const styles = BackgroundGenerator.generateCss(config);
+
+ // Apply styles to element
+ Object.entries(styles).forEach(([property, value]) => {
+ element.style.setProperty(property, value);
+ });
+
+ // Add to active backgrounds
+ this._activeBackgrounds.update(backgrounds => {
+ const newMap = new Map(backgrounds);
+ newMap.set(backgroundId, {
+ active: true,
+ config,
+ element
+ });
+ return newMap;
+ });
+
+ return backgroundId;
+ }
+
+ /**
+ * Apply a full-screen background
+ */
+ applyFullScreenBackground(config: BackgroundConfig, options?: Partial): string {
+ const backgroundId = this.generateId();
+ const element = this.createFullScreenElement(options);
+
+ const styles = BackgroundGenerator.generateCss(config);
+ Object.entries(styles).forEach(([property, value]) => {
+ element.style.setProperty(property, value);
+ });
+
+ document.body.appendChild(element);
+
+ this._activeBackgrounds.update(backgrounds => {
+ const newMap = new Map(backgrounds);
+ newMap.set(backgroundId, {
+ active: true,
+ config,
+ element
+ });
+ return newMap;
+ });
+
+ return backgroundId;
+ }
+
+ /**
+ * Update an existing background
+ */
+ updateBackground(id: string, config: BackgroundConfig): boolean {
+ const backgroundState = this._activeBackgrounds().get(id);
+ if (!backgroundState || !backgroundState.element) {
+ return false;
+ }
+
+ const styles = BackgroundGenerator.generateCss(config);
+ Object.entries(styles).forEach(([property, value]) => {
+ backgroundState.element!.style.setProperty(property, value);
+ });
+
+ this._activeBackgrounds.update(backgrounds => {
+ const newMap = new Map(backgrounds);
+ const existing = newMap.get(id);
+ if (existing) {
+ existing.config = config;
+ newMap.set(id, existing);
+ }
+ return newMap;
+ });
+
+ return true;
+ }
+
+ /**
+ * Remove a background
+ */
+ removeBackground(id: string): boolean {
+ const backgroundState = this._activeBackgrounds().get(id);
+ if (!backgroundState) {
+ return false;
+ }
+
+ // Remove full-screen element if it was created by this service
+ if (backgroundState.element?.classList.contains('ui-background-fullscreen')) {
+ backgroundState.element.remove();
+ } else if (backgroundState.element) {
+ // Clear inline styles for regular elements
+ this.clearElementStyles(backgroundState.element);
+ }
+
+ this._activeBackgrounds.update(backgrounds => {
+ const newMap = new Map(backgrounds);
+ newMap.delete(id);
+ return newMap;
+ });
+
+ return true;
+ }
+
+ /**
+ * Remove all backgrounds
+ */
+ removeAllBackgrounds(): void {
+ this._activeBackgrounds().forEach((state, id) => {
+ this.removeBackground(id);
+ });
+ }
+
+ /**
+ * Get background state
+ */
+ getBackground(id: string): BackgroundState | undefined {
+ return this._activeBackgrounds().get(id);
+ }
+
+ /**
+ * Generate CSS for a configuration without applying it
+ */
+ generateCss(config: BackgroundConfig): { [key: string]: string } {
+ return BackgroundGenerator.generateCss(config);
+ }
+
+ /**
+ * Generate a background ID
+ */
+ private generateId(): string {
+ return `bg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+ }
+
+ /**
+ * Create a full-screen background element
+ */
+ private createFullScreenElement(options?: Partial): HTMLElement {
+ const element = document.createElement('div');
+ element.className = `ui-background-fullscreen ${options?.className || ''}`.trim();
+
+ // Base full-screen styles
+ element.style.position = 'fixed';
+ element.style.top = '0';
+ element.style.left = '0';
+ element.style.width = '100%';
+ element.style.height = '100%';
+ element.style.pointerEvents = 'none';
+ element.style.zIndex = (options?.zIndex || -1).toString();
+
+ return element;
+ }
+
+ /**
+ * Clear background-related styles from an element
+ */
+ private clearElementStyles(element: HTMLElement): void {
+ const propertiesToClear = [
+ 'background',
+ 'background-image',
+ 'background-color',
+ 'background-size',
+ 'background-position',
+ 'background-repeat',
+ 'background-attachment',
+ 'backdrop-filter',
+ 'opacity',
+ 'animation'
+ ];
+
+ propertiesToClear.forEach(property => {
+ element.style.removeProperty(property);
+ });
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-backgrounds/src/lib/services/index.ts b/projects/ui-backgrounds/src/lib/services/index.ts
new file mode 100644
index 0000000..e49dab0
--- /dev/null
+++ b/projects/ui-backgrounds/src/lib/services/index.ts
@@ -0,0 +1 @@
+export * from './background.service';
\ No newline at end of file
diff --git a/projects/ui-backgrounds/src/lib/types/background.types.ts b/projects/ui-backgrounds/src/lib/types/background.types.ts
new file mode 100644
index 0000000..1b07a45
--- /dev/null
+++ b/projects/ui-backgrounds/src/lib/types/background.types.ts
@@ -0,0 +1,142 @@
+export type BackgroundType =
+ | 'solid'
+ | 'linear-gradient'
+ | 'radial-gradient'
+ | 'conic-gradient'
+ | 'pattern'
+ | 'image'
+ | 'animated'
+ | 'mesh'
+ | 'noise'
+ | 'glass';
+
+export type PatternType =
+ | 'dots'
+ | 'grid'
+ | 'stripes'
+ | 'diagonal-stripes'
+ | 'chevron'
+ | 'waves'
+ | 'hexagon'
+ | 'triangles'
+ | 'checkerboard'
+ | 'circles'
+ | 'crosshatch'
+ | 'polka-dots';
+
+export type GradientDirection =
+ | 'to-top'
+ | 'to-top-right'
+ | 'to-right'
+ | 'to-bottom-right'
+ | 'to-bottom'
+ | 'to-bottom-left'
+ | 'to-left'
+ | 'to-top-left';
+
+export interface ColorStop {
+ color: string;
+ position?: string | number;
+}
+
+export interface SolidBackgroundConfig {
+ type: 'solid';
+ color: string;
+}
+
+export interface LinearGradientConfig {
+ type: 'linear-gradient';
+ direction?: GradientDirection | string;
+ colors: ColorStop[];
+}
+
+export interface RadialGradientConfig {
+ type: 'radial-gradient';
+ shape?: 'circle' | 'ellipse';
+ size?: 'closest-side' | 'closest-corner' | 'farthest-side' | 'farthest-corner';
+ position?: string;
+ colors: ColorStop[];
+}
+
+export interface ConicGradientConfig {
+ type: 'conic-gradient';
+ angle?: string | number;
+ position?: string;
+ colors: ColorStop[];
+}
+
+export interface PatternConfig {
+ type: 'pattern';
+ pattern: PatternType;
+ primaryColor?: string;
+ secondaryColor?: string;
+ size?: string | number;
+ opacity?: number;
+}
+
+export interface ImageBackgroundConfig {
+ type: 'image';
+ url: string;
+ size?: 'auto' | 'cover' | 'contain' | string;
+ position?: string;
+ repeat?: 'repeat' | 'repeat-x' | 'repeat-y' | 'no-repeat';
+ attachment?: 'scroll' | 'fixed' | 'local';
+ opacity?: number;
+}
+
+export interface AnimatedBackgroundConfig {
+ type: 'animated';
+ baseConfig: LinearGradientConfig | RadialGradientConfig | ConicGradientConfig | PatternConfig;
+ duration?: string;
+ timing?: string;
+ iteration?: 'infinite' | number;
+}
+
+export interface MeshGradientConfig {
+ type: 'mesh';
+ colors: string[];
+ complexity?: 'low' | 'medium' | 'high';
+ seed?: number;
+}
+
+export interface NoiseConfig {
+ type: 'noise';
+ baseColor?: string;
+ intensity?: number;
+ scale?: number;
+ opacity?: number;
+}
+
+export interface GlassConfig {
+ type: 'glass';
+ backdrop?: 'blur' | 'saturate' | 'both';
+ intensity?: number;
+ tint?: string;
+ opacity?: number;
+}
+
+export type BackgroundConfig =
+ | SolidBackgroundConfig
+ | LinearGradientConfig
+ | RadialGradientConfig
+ | ConicGradientConfig
+ | PatternConfig
+ | ImageBackgroundConfig
+ | AnimatedBackgroundConfig
+ | MeshGradientConfig
+ | NoiseConfig
+ | GlassConfig;
+
+export interface BackgroundOptions {
+ config: BackgroundConfig;
+ zIndex?: number;
+ fullScreen?: boolean;
+ responsive?: boolean;
+ className?: string;
+}
+
+export interface BackgroundState {
+ active: boolean;
+ config?: BackgroundConfig;
+ element?: HTMLElement;
+}
\ No newline at end of file
diff --git a/projects/ui-backgrounds/src/lib/types/index.ts b/projects/ui-backgrounds/src/lib/types/index.ts
new file mode 100644
index 0000000..735a185
--- /dev/null
+++ b/projects/ui-backgrounds/src/lib/types/index.ts
@@ -0,0 +1 @@
+export * from './background.types';
\ No newline at end of file
diff --git a/projects/ui-backgrounds/src/lib/utils/background-generator.ts b/projects/ui-backgrounds/src/lib/utils/background-generator.ts
new file mode 100644
index 0000000..6f08f4d
--- /dev/null
+++ b/projects/ui-backgrounds/src/lib/utils/background-generator.ts
@@ -0,0 +1,234 @@
+import {
+ BackgroundConfig,
+ ColorStop,
+ LinearGradientConfig,
+ RadialGradientConfig,
+ ConicGradientConfig,
+ PatternConfig,
+ ImageBackgroundConfig,
+ SolidBackgroundConfig,
+ AnimatedBackgroundConfig,
+ MeshGradientConfig,
+ NoiseConfig,
+ GlassConfig
+} from '../types/background.types';
+
+export class BackgroundGenerator {
+
+ static generateCss(config: BackgroundConfig): { [key: string]: string } {
+ switch (config.type) {
+ case 'solid':
+ return this.generateSolid(config);
+ case 'linear-gradient':
+ return this.generateLinearGradient(config);
+ case 'radial-gradient':
+ return this.generateRadialGradient(config);
+ case 'conic-gradient':
+ return this.generateConicGradient(config);
+ case 'pattern':
+ return this.generatePattern(config);
+ case 'image':
+ return this.generateImage(config);
+ case 'animated':
+ return this.generateAnimated(config);
+ case 'mesh':
+ return this.generateMesh(config);
+ case 'noise':
+ return this.generateNoise(config);
+ case 'glass':
+ return this.generateGlass(config);
+ default:
+ return { background: 'transparent' };
+ }
+ }
+
+ private static generateSolid(config: SolidBackgroundConfig): { [key: string]: string } {
+ return {
+ background: config.color,
+ 'background-size': 'cover',
+ 'background-position': 'center',
+ 'background-repeat': 'no-repeat'
+ };
+ }
+
+ private static generateLinearGradient(config: LinearGradientConfig): { [key: string]: string } {
+ const direction = config.direction || 'to bottom';
+ const colorStops = this.formatColorStops(config.colors);
+
+ return {
+ background: `linear-gradient(${direction}, ${colorStops})`,
+ 'background-size': 'cover',
+ 'background-position': 'center',
+ 'background-repeat': 'no-repeat'
+ };
+ }
+
+ private static generateRadialGradient(config: RadialGradientConfig): { [key: string]: string } {
+ const shape = config.shape || 'ellipse';
+ const size = config.size || 'farthest-corner';
+ const position = config.position || 'center';
+ const colorStops = this.formatColorStops(config.colors);
+
+ return {
+ background: `radial-gradient(${shape} ${size} at ${position}, ${colorStops})`,
+ 'background-size': 'cover',
+ 'background-position': 'center',
+ 'background-repeat': 'no-repeat'
+ };
+ }
+
+ private static generateConicGradient(config: ConicGradientConfig): { [key: string]: string } {
+ const angle = config.angle ? `from ${config.angle}deg` : '';
+ const position = config.position ? `at ${config.position}` : '';
+ const colorStops = this.formatColorStops(config.colors);
+
+ return {
+ background: `conic-gradient(${angle} ${position}, ${colorStops})`,
+ 'background-size': 'cover',
+ 'background-position': 'center',
+ 'background-repeat': 'no-repeat'
+ };
+ }
+
+ private static generatePattern(config: PatternConfig): { [key: string]: string } {
+ const primaryColor = config.primaryColor || '#000000';
+ const secondaryColor = config.secondaryColor || '#ffffff';
+ const size = typeof config.size === 'number' ? `${config.size}px` : (config.size || '20px');
+ const opacity = config.opacity || 1;
+
+ const patternCss = this.getPatternCss(config.pattern, primaryColor, secondaryColor, size);
+
+ return {
+ background: patternCss,
+ opacity: opacity.toString(),
+ 'background-size': size,
+ 'background-repeat': 'repeat'
+ };
+ }
+
+ private static generateImage(config: ImageBackgroundConfig): { [key: string]: string } {
+ const styles: { [key: string]: string } = {
+ 'background-image': `url(${config.url})`,
+ 'background-size': config.size || 'cover',
+ 'background-position': config.position || 'center',
+ 'background-repeat': config.repeat || 'no-repeat'
+ };
+
+ if (config.attachment) {
+ styles['background-attachment'] = config.attachment;
+ }
+
+ if (config.opacity !== undefined) {
+ styles['opacity'] = config.opacity.toString();
+ }
+
+ return styles;
+ }
+
+ private static generateAnimated(config: AnimatedBackgroundConfig): { [key: string]: string } {
+ const baseStyles = this.generateCss(config.baseConfig);
+ const duration = config.duration || '5s';
+ const timing = config.timing || 'ease-in-out';
+ const iteration = config.iteration === 'infinite' ? 'infinite' : (config.iteration || 1).toString();
+
+ return {
+ ...baseStyles,
+ animation: `background-animation ${duration} ${timing} ${iteration}`,
+ };
+ }
+
+ private static generateMesh(config: MeshGradientConfig): { [key: string]: string } {
+ // Generate mesh gradient using multiple radial gradients
+ const colors = config.colors;
+ const complexity = config.complexity || 'medium';
+
+ // Create multiple overlapping radial gradients
+ const gradients = this.generateMeshGradients(colors, complexity);
+
+ return {
+ background: gradients.join(', '),
+ 'background-size': '400% 400%',
+ animation: 'mesh-animation 15s ease infinite'
+ };
+ }
+
+ private static generateNoise(config: NoiseConfig): { [key: string]: string } {
+ const baseColor = config.baseColor || '#000000';
+ const intensity = config.intensity || 0.5;
+ const scale = config.scale || 100;
+ const opacity = config.opacity || 1;
+
+ // Create noise effect using CSS filters and patterns
+ return {
+ background: baseColor,
+ 'background-image': `url("data:image/svg+xml,%3Csvg viewBox='0 0 ${scale} ${scale}' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' opacity='${intensity}'/%3E%3C/svg%3E")`,
+ opacity: opacity.toString()
+ };
+ }
+
+ private static generateGlass(config: GlassConfig): { [key: string]: string } {
+ const intensity = config.intensity || 10;
+ const tint = config.tint || 'rgba(255, 255, 255, 0.1)';
+ const opacity = config.opacity || 0.8;
+
+ const styles: { [key: string]: string } = {
+ background: tint,
+ opacity: opacity.toString(),
+ 'backdrop-filter': `blur(${intensity}px)`
+ };
+
+ if (config.backdrop === 'saturate' || config.backdrop === 'both') {
+ styles['backdrop-filter'] += ` saturate(150%)`;
+ }
+
+ return styles;
+ }
+
+ private static formatColorStops(colors: ColorStop[]): string {
+ return colors.map(stop => {
+ if (stop.position !== undefined) {
+ const position = typeof stop.position === 'number' ? `${stop.position}%` : stop.position;
+ return `${stop.color} ${position}`;
+ }
+ return stop.color;
+ }).join(', ');
+ }
+
+ private static getPatternCss(pattern: string, primary: string, secondary: string, size: string): string {
+ const patterns: { [key: string]: string } = {
+ 'dots': `radial-gradient(circle, ${primary} 20%, transparent 20%)`,
+ 'grid': `linear-gradient(${primary} 1px, transparent 1px), linear-gradient(90deg, ${primary} 1px, transparent 1px)`,
+ 'stripes': `linear-gradient(45deg, ${primary} 25%, transparent 25%, transparent 50%, ${primary} 50%, ${primary} 75%, transparent 75%)`,
+ 'diagonal-stripes': `linear-gradient(45deg, ${primary} 25%, transparent 25%, transparent 50%, ${primary} 50%, ${primary} 75%, transparent 75%)`,
+ 'chevron': `linear-gradient(45deg, ${primary} 25%, transparent 25%), linear-gradient(-45deg, ${primary} 25%, transparent 25%)`,
+ 'waves': `radial-gradient(circle at 50% 100%, transparent 40%, ${primary} 40%, ${primary} 60%, transparent 60%)`,
+ 'hexagon': `radial-gradient(circle at 50% 0%, transparent 40%, ${primary} 40%, ${primary} 60%, transparent 60%)`,
+ 'triangles': `linear-gradient(60deg, ${primary} 25%, transparent 25%), linear-gradient(120deg, ${primary} 25%, transparent 25%)`,
+ 'checkerboard': `linear-gradient(45deg, ${primary} 25%, transparent 25%, transparent 75%, ${primary} 75%), linear-gradient(45deg, ${primary} 25%, transparent 25%, transparent 75%, ${primary} 75%)`,
+ 'circles': `radial-gradient(circle, ${primary} 40%, transparent 40%)`,
+ 'crosshatch': `linear-gradient(0deg, ${primary} 1px, transparent 1px), linear-gradient(90deg, ${primary} 1px, transparent 1px), linear-gradient(45deg, ${primary} 1px, transparent 1px), linear-gradient(-45deg, ${primary} 1px, transparent 1px)`,
+ 'polka-dots': `radial-gradient(circle, ${primary} 25%, transparent 25%)`
+ };
+
+ return patterns[pattern] || patterns['dots'];
+ }
+
+ private static generateMeshGradients(colors: string[], complexity: string): string[] {
+ const gradientCount = complexity === 'high' ? 6 : complexity === 'medium' ? 4 : 2;
+ const gradients: string[] = [];
+
+ for (let i = 0; i < gradientCount; i++) {
+ const color1 = colors[i % colors.length];
+ const color2 = colors[(i + 1) % colors.length];
+ const x = Math.random() * 100;
+ const y = Math.random() * 100;
+ const size = 50 + Math.random() * 50;
+
+ gradients.push(
+ `radial-gradient(${size}% ${size}% at ${x}% ${y}%, ${color1}, transparent)`
+ );
+ }
+
+ return gradients;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-backgrounds/src/lib/utils/index.ts b/projects/ui-backgrounds/src/lib/utils/index.ts
new file mode 100644
index 0000000..f6ec435
--- /dev/null
+++ b/projects/ui-backgrounds/src/lib/utils/index.ts
@@ -0,0 +1 @@
+export * from './background-generator';
\ No newline at end of file
diff --git a/projects/ui-backgrounds/src/public-api.ts b/projects/ui-backgrounds/src/public-api.ts
new file mode 100644
index 0000000..6cb14ee
--- /dev/null
+++ b/projects/ui-backgrounds/src/public-api.ts
@@ -0,0 +1,18 @@
+/*
+ * Public API Surface of ui-backgrounds
+ */
+
+// Types
+export * from './lib/types/index';
+
+// Services
+export * from './lib/services/index';
+
+// Directives
+export * from './lib/directives/index';
+
+// Components
+export * from './lib/components/index';
+
+// Utils
+export * from './lib/utils/index';
diff --git a/projects/ui-backgrounds/src/styles/_animations.scss b/projects/ui-backgrounds/src/styles/_animations.scss
new file mode 100644
index 0000000..c6483e6
--- /dev/null
+++ b/projects/ui-backgrounds/src/styles/_animations.scss
@@ -0,0 +1,113 @@
+// Animation keyframes for ui-backgrounds library
+
+// Basic gradient animation
+@keyframes ui-background-animation {
+ 0% {
+ background-position: 0% 50%;
+ }
+ 50% {
+ background-position: 100% 50%;
+ }
+ 100% {
+ background-position: 0% 50%;
+ }
+}
+
+// Mesh gradient animation
+@keyframes ui-mesh-animation {
+ 0%, 100% {
+ background-position: 0% 0%;
+ }
+ 25% {
+ background-position: 100% 0%;
+ }
+ 50% {
+ background-position: 100% 100%;
+ }
+ 75% {
+ background-position: 0% 100%;
+ }
+}
+
+// Floating animation for patterns
+@keyframes ui-pattern-float {
+ 0%, 100% {
+ transform: translateY(0px);
+ }
+ 50% {
+ transform: translateY(-20px);
+ }
+}
+
+// Rotation animation
+@keyframes ui-background-rotate {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+// Scale animation
+@keyframes ui-background-scale {
+ 0%, 100% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.1);
+ }
+}
+
+// Pulse animation
+@keyframes ui-background-pulse {
+ 0%, 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.7;
+ }
+}
+
+// Color shifting animation
+@keyframes ui-background-hue-shift {
+ 0% {
+ filter: hue-rotate(0deg);
+ }
+ 100% {
+ filter: hue-rotate(360deg);
+ }
+}
+
+// Blur animation for glass effects
+@keyframes ui-background-blur-pulse {
+ 0%, 100% {
+ backdrop-filter: blur(10px);
+ }
+ 50% {
+ backdrop-filter: blur(20px);
+ }
+}
+
+// Slide animation
+@keyframes ui-background-slide {
+ 0% {
+ background-position: -100% 0;
+ }
+ 100% {
+ background-position: 100% 0;
+ }
+}
+
+// Wave animation
+@keyframes ui-background-wave {
+ 0%, 100% {
+ background-position: 0% 50%;
+ }
+ 25% {
+ background-position: 50% 0%;
+ }
+ 75% {
+ background-position: 50% 100%;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-backgrounds/src/styles/_base.scss b/projects/ui-backgrounds/src/styles/_base.scss
new file mode 100644
index 0000000..43aec1c
--- /dev/null
+++ b/projects/ui-backgrounds/src/styles/_base.scss
@@ -0,0 +1,180 @@
+// Base styles for ui-backgrounds library
+
+// Import animations
+@import 'animations';
+
+// Base background component styles
+.ui-background {
+ position: relative;
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+
+ // Ensure proper layering
+ z-index: 0;
+
+ &--fullscreen {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ z-index: -1;
+ }
+}
+
+// Type-specific styles
+.ui-background--solid {
+ background-image: none;
+}
+
+.ui-background--gradient {
+ // Base gradient styles
+ &.ui-background--linear-gradient {
+ background-size: 200% 200%;
+ }
+
+ &.ui-background--radial-gradient {
+ background-size: 200% 200%;
+ }
+
+ &.ui-background--conic-gradient {
+ background-size: 200% 200%;
+ }
+}
+
+.ui-background--pattern {
+ background-repeat: repeat;
+
+ &.ui-background--pattern-dots {
+ // Dots pattern specific styles
+ }
+
+ &.ui-background--pattern-grid {
+ // Grid pattern specific styles
+ }
+
+ &.ui-background--pattern-stripes {
+ // Stripes pattern specific styles
+ }
+
+ &.ui-background--pattern-diagonal-stripes {
+ // Diagonal stripes pattern specific styles
+ }
+
+ &.ui-background--pattern-chevron {
+ // Chevron pattern specific styles
+ }
+
+ &.ui-background--pattern-waves {
+ background-repeat: repeat-x;
+ }
+
+ &.ui-background--pattern-hexagon {
+ // Hexagon pattern specific styles
+ }
+
+ &.ui-background--pattern-triangles {
+ // Triangles pattern specific styles
+ }
+
+ &.ui-background--pattern-checkerboard {
+ // Checkerboard pattern specific styles
+ }
+
+ &.ui-background--pattern-circles {
+ // Circles pattern specific styles
+ }
+
+ &.ui-background--pattern-crosshatch {
+ // Crosshatch pattern specific styles
+ }
+
+ &.ui-background--pattern-polka-dots {
+ // Polka dots pattern specific styles
+ }
+}
+
+.ui-background--image {
+ // Image background specific styles
+}
+
+.ui-background--animated {
+ // Base animation styles
+ background-size: 200% 200%;
+
+ // Apply default animation
+ animation: ui-background-animation 5s ease-in-out infinite;
+
+ // Respect reduced motion preferences
+ @media (prefers-reduced-motion: reduce) {
+ animation: none;
+ }
+}
+
+.ui-background--mesh {
+ background-size: 400% 400%;
+ animation: ui-mesh-animation 15s ease infinite;
+
+ @media (prefers-reduced-motion: reduce) {
+ animation: none;
+ }
+}
+
+.ui-background--noise {
+ // Noise background specific styles
+}
+
+.ui-background--glass {
+ backdrop-filter: blur(10px);
+ -webkit-backdrop-filter: blur(10px); // Safari support
+
+ // Fallback for browsers that don't support backdrop-filter
+ @supports not (backdrop-filter: blur()) {
+ background-color: rgba(255, 255, 255, 0.8);
+ }
+}
+
+// Responsive behavior
+@media (max-width: 768px) {
+ .ui-background--animated,
+ .ui-background--mesh {
+ // Disable complex animations on mobile for performance
+ @media (prefers-reduced-motion: no-preference) {
+ animation-duration: 10s; // Slower animations
+ }
+ }
+}
+
+// High contrast mode support
+@media (prefers-contrast: high) {
+ .ui-background {
+ filter: contrast(1.2);
+ }
+
+ .ui-background--glass {
+ backdrop-filter: none;
+ background-color: rgba(255, 255, 255, 0.95);
+ }
+}
+
+// Print styles
+@media print {
+ .ui-background {
+ background: none !important;
+ backdrop-filter: none !important;
+ animation: none !important;
+ }
+
+ .ui-background--fullscreen {
+ display: none !important;
+ }
+}
+
+// Dark mode considerations
+@media (prefers-color-scheme: dark) {
+ .ui-background--glass {
+ background-color: rgba(0, 0, 0, 0.3);
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-backgrounds/src/styles/_mixins.scss b/projects/ui-backgrounds/src/styles/_mixins.scss
new file mode 100644
index 0000000..27ef1e2
--- /dev/null
+++ b/projects/ui-backgrounds/src/styles/_mixins.scss
@@ -0,0 +1,140 @@
+// Background Mixins for ui-backgrounds library
+
+// Base background mixin
+@mixin ui-background-base() {
+ position: relative;
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+// Full screen background mixin
+@mixin ui-background-fullscreen($z-index: -1) {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ z-index: $z-index;
+}
+
+// Solid color background
+@mixin ui-background-solid($color) {
+ background-color: $color;
+ background-image: none;
+}
+
+// Linear gradient background
+@mixin ui-background-linear-gradient($direction: 'to bottom', $colors...) {
+ background: linear-gradient(#{$direction}, $colors);
+}
+
+// Radial gradient background
+@mixin ui-background-radial-gradient($shape: 'ellipse', $size: 'farthest-corner', $position: 'center', $colors...) {
+ background: radial-gradient(#{$shape} #{$size} at #{$position}, $colors);
+}
+
+// Conic gradient background
+@mixin ui-background-conic-gradient($angle: 0deg, $position: 'center', $colors...) {
+ background: conic-gradient(from #{$angle} at #{$position}, $colors);
+}
+
+// Pattern backgrounds
+@mixin ui-pattern-dots($primary: #000, $secondary: transparent, $size: 20px) {
+ background-image: radial-gradient(circle, #{$primary} 20%, #{$secondary} 20%);
+ background-size: $size $size;
+ background-repeat: repeat;
+}
+
+@mixin ui-pattern-grid($color: #000, $size: 20px, $line-width: 1px) {
+ background-image:
+ linear-gradient(#{$color} #{$line-width}, transparent #{$line-width}),
+ linear-gradient(90deg, #{$color} #{$line-width}, transparent #{$line-width});
+ background-size: $size $size;
+ background-repeat: repeat;
+}
+
+@mixin ui-pattern-stripes($primary: #000, $secondary: transparent, $size: 20px) {
+ background-image: linear-gradient(45deg, #{$primary} 25%, #{$secondary} 25%, #{$secondary} 50%, #{$primary} 50%, #{$primary} 75%, #{$secondary} 75%);
+ background-size: $size $size;
+ background-repeat: repeat;
+}
+
+@mixin ui-pattern-diagonal-stripes($primary: #000, $secondary: transparent, $size: 20px) {
+ background-image: linear-gradient(-45deg, #{$primary} 25%, #{$secondary} 25%, #{$secondary} 50%, #{$primary} 50%, #{$primary} 75%, #{$secondary} 75%);
+ background-size: $size $size;
+ background-repeat: repeat;
+}
+
+@mixin ui-pattern-chevron($primary: #000, $secondary: transparent, $size: 20px) {
+ background-image:
+ linear-gradient(45deg, #{$primary} 25%, #{$secondary} 25%),
+ linear-gradient(-45deg, #{$primary} 25%, #{$secondary} 25%);
+ background-size: $size $size;
+ background-repeat: repeat;
+ background-position: 0 0, 0 ($size / 2);
+}
+
+@mixin ui-pattern-waves($primary: #000, $secondary: transparent, $size: 40px) {
+ background-image: radial-gradient(circle at 50% 100%, #{$secondary} 40%, #{$primary} 40%, #{$primary} 60%, #{$secondary} 60%);
+ background-size: $size ($size / 2);
+ background-repeat: repeat-x;
+}
+
+@mixin ui-pattern-checkerboard($primary: #000, $secondary: #fff, $size: 20px) {
+ background-image:
+ linear-gradient(45deg, #{$primary} 25%, transparent 25%, transparent 75%, #{$primary} 75%),
+ linear-gradient(45deg, #{$primary} 25%, transparent 25%, transparent 75%, #{$primary} 75%);
+ background-color: $secondary;
+ background-size: $size $size;
+ background-position: 0 0, ($size / 2) ($size / 2);
+ background-repeat: repeat;
+}
+
+// Animated backgrounds
+@mixin ui-background-animated($duration: 5s, $timing: ease-in-out, $iteration: infinite) {
+ animation: ui-background-animation $duration $timing $iteration;
+}
+
+@mixin ui-background-mesh-animated($duration: 15s) {
+ background-size: 400% 400%;
+ animation: ui-mesh-animation $duration ease infinite;
+}
+
+// Glass/blur backgrounds
+@mixin ui-background-glass($blur: 10px, $tint: rgba(255, 255, 255, 0.1), $opacity: 0.8) {
+ background: $tint;
+ backdrop-filter: blur($blur);
+ opacity: $opacity;
+}
+
+// Responsive background utilities
+@mixin ui-background-responsive($mobile-bg, $tablet-bg: null, $desktop-bg: null) {
+ background: $mobile-bg;
+
+ @if $tablet-bg {
+ @media (min-width: 768px) {
+ background: $tablet-bg;
+ }
+ }
+
+ @if $desktop-bg {
+ @media (min-width: 1024px) {
+ background: $desktop-bg;
+ }
+ }
+}
+
+// Accessibility utilities
+@mixin ui-background-high-contrast() {
+ @media (prefers-contrast: high) {
+ filter: contrast(1.5);
+ }
+}
+
+@mixin ui-background-reduced-motion() {
+ @media (prefers-reduced-motion: reduce) {
+ animation: none !important;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-backgrounds/src/styles/index.scss b/projects/ui-backgrounds/src/styles/index.scss
new file mode 100644
index 0000000..4060416
--- /dev/null
+++ b/projects/ui-backgrounds/src/styles/index.scss
@@ -0,0 +1,9 @@
+// Main entry point for ui-backgrounds styles
+
+// Import base components
+@import 'mixins';
+@import 'animations';
+@import 'base';
+
+// Export mixins for external use
+// Users can import this file to get access to all background mixins and styles
\ No newline at end of file
diff --git a/projects/ui-backgrounds/tsconfig.lib.json b/projects/ui-backgrounds/tsconfig.lib.json
new file mode 100644
index 0000000..2359bf6
--- /dev/null
+++ b/projects/ui-backgrounds/tsconfig.lib.json
@@ -0,0 +1,15 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../out-tsc/lib",
+ "declaration": true,
+ "declarationMap": true,
+ "inlineSources": true,
+ "types": []
+ },
+ "exclude": [
+ "**/*.spec.ts"
+ ]
+}
diff --git a/projects/ui-backgrounds/tsconfig.lib.prod.json b/projects/ui-backgrounds/tsconfig.lib.prod.json
new file mode 100644
index 0000000..9215caa
--- /dev/null
+++ b/projects/ui-backgrounds/tsconfig.lib.prod.json
@@ -0,0 +1,11 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+ "extends": "./tsconfig.lib.json",
+ "compilerOptions": {
+ "declarationMap": false
+ },
+ "angularCompilerOptions": {
+ "compilationMode": "partial"
+ }
+}
diff --git a/projects/ui-backgrounds/tsconfig.spec.json b/projects/ui-backgrounds/tsconfig.spec.json
new file mode 100644
index 0000000..254686d
--- /dev/null
+++ b/projects/ui-backgrounds/tsconfig.spec.json
@@ -0,0 +1,15 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../out-tsc/spec",
+ "types": [
+ "jasmine"
+ ]
+ },
+ "include": [
+ "**/*.spec.ts",
+ "**/*.d.ts"
+ ]
+}
diff --git a/projects/ui-code-display/README.md b/projects/ui-code-display/README.md
new file mode 100644
index 0000000..8388719
--- /dev/null
+++ b/projects/ui-code-display/README.md
@@ -0,0 +1,63 @@
+# UiCodeDisplay
+
+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-code-display
+```
+
+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-code-display
+ ```
+
+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.
diff --git a/projects/ui-code-display/ng-package.json b/projects/ui-code-display/ng-package.json
new file mode 100644
index 0000000..b70ab58
--- /dev/null
+++ b/projects/ui-code-display/ng-package.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
+ "dest": "../../dist/ui-code-display",
+ "lib": {
+ "entryFile": "src/public-api.ts"
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-code-display/node_modules/.package-lock.json b/projects/ui-code-display/node_modules/.package-lock.json
new file mode 100644
index 0000000..16a8460
--- /dev/null
+++ b/projects/ui-code-display/node_modules/.package-lock.json
@@ -0,0 +1,75 @@
+{
+ "name": "ui-code-display",
+ "version": "0.0.1",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "node_modules/@angular/common": {
+ "version": "19.2.14",
+ "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.2.14.tgz",
+ "integrity": "sha512-NcNklcuyqaTjOVGf7aru8APX9mjsnZ01gFZrn47BxHozhaR0EMRrotYQTdi8YdVjPkeYFYanVntSLfhyobq/jg==",
+ "peer": true,
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "engines": {
+ "node": "^18.19.1 || ^20.11.1 || >=22.0.0"
+ },
+ "peerDependencies": {
+ "@angular/core": "19.2.14",
+ "rxjs": "^6.5.3 || ^7.4.0"
+ }
+ },
+ "node_modules/@angular/core": {
+ "version": "19.2.14",
+ "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.2.14.tgz",
+ "integrity": "sha512-EVErpW9tGqJ/wNcAN3G/ErH8pHCJ8mM1E6bsJ8UJIpDTZkpqqYjBMtZS9YWH5n3KwUd1tAkAB2w8FK125AjDUQ==",
+ "peer": true,
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "engines": {
+ "node": "^18.19.1 || ^20.11.1 || >=22.0.0"
+ },
+ "peerDependencies": {
+ "rxjs": "^6.5.3 || ^7.4.0",
+ "zone.js": "~0.15.0"
+ }
+ },
+ "node_modules/@types/prismjs": {
+ "version": "1.26.5",
+ "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz",
+ "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==",
+ "peer": true
+ },
+ "node_modules/prismjs": {
+ "version": "1.30.0",
+ "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
+ "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
+ "peer": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/rxjs": {
+ "version": "7.8.2",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
+ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
+ "peer": true,
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
+ },
+ "node_modules/zone.js": {
+ "version": "0.15.1",
+ "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz",
+ "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==",
+ "peer": true
+ }
+ }
+}
diff --git a/projects/ui-code-display/node_modules/@angular/common/LICENSE b/projects/ui-code-display/node_modules/@angular/common/LICENSE
new file mode 100755
index 0000000..48adc1e
--- /dev/null
+++ b/projects/ui-code-display/node_modules/@angular/common/LICENSE
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2010-2025 Google LLC. https://angular.dev/license
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/projects/ui-code-display/node_modules/@angular/common/README.md b/projects/ui-code-display/node_modules/@angular/common/README.md
new file mode 100755
index 0000000..ef827d5
--- /dev/null
+++ b/projects/ui-code-display/node_modules/@angular/common/README.md
@@ -0,0 +1,8 @@
+Angular
+=======
+
+The sources for this package are in the main [Angular](https://github.com/angular/angular) repo. Please file issues and pull requests against that repo.
+
+Usage information and reference details can be found in [Angular documentation](https://angular.dev/overview).
+
+License: MIT
diff --git a/projects/ui-code-display/node_modules/@angular/common/common_module.d-NEF7UaHr.d.ts b/projects/ui-code-display/node_modules/@angular/common/common_module.d-NEF7UaHr.d.ts
new file mode 100755
index 0000000..4425059
--- /dev/null
+++ b/projects/ui-code-display/node_modules/@angular/common/common_module.d-NEF7UaHr.d.ts
@@ -0,0 +1,1937 @@
+/**
+ * @license Angular v19.2.14
+ * (c) 2010-2025 Google LLC. https://angular.io/
+ * License: MIT
+ */
+
+import * as i0 from '@angular/core';
+import { InjectionToken, OnDestroy, DoCheck, ElementRef, Renderer2, OnChanges, Type, Injector, NgModuleFactory, ViewContainerRef, SimpleChanges, NgIterable, TrackByFunction, TemplateRef, IterableDiffers, KeyValueDiffers, PipeTransform, ChangeDetectorRef } from '@angular/core';
+import { SubscriptionLike, Observable, Subscribable } from 'rxjs';
+import { LocationChangeListener, PlatformLocation } from './platform_location.d-Lbv6Ueec.js';
+
+/**
+ * Enables the `Location` service to read route state from the browser's URL.
+ * Angular provides two strategies:
+ * `HashLocationStrategy` and `PathLocationStrategy`.
+ *
+ * Applications should use the `Router` or `Location` services to
+ * interact with application route state.
+ *
+ * For instance, `HashLocationStrategy` produces URLs like
+ * http://example.com/#/foo,
+ * and `PathLocationStrategy` produces
+ * http://example.com/foo as an equivalent URL.
+ *
+ * See these two classes for more.
+ *
+ * @publicApi
+ */
+declare abstract class LocationStrategy {
+ abstract path(includeHash?: boolean): string;
+ abstract prepareExternalUrl(internal: string): string;
+ abstract getState(): unknown;
+ abstract pushState(state: any, title: string, url: string, queryParams: string): void;
+ abstract replaceState(state: any, title: string, url: string, queryParams: string): void;
+ abstract forward(): void;
+ abstract back(): void;
+ historyGo?(relativePosition: number): void;
+ abstract onPopState(fn: LocationChangeListener): void;
+ abstract getBaseHref(): string;
+ static ɵfac: i0.ɵɵFactoryDeclaration;
+ static ɵprov: i0.ɵɵInjectableDeclaration;
+}
+/**
+ * A predefined DI token for the base href
+ * to be used with the `PathLocationStrategy`.
+ * The base href is the URL prefix that should be preserved when generating
+ * and recognizing URLs.
+ *
+ * @usageNotes
+ *
+ * The following example shows how to use this token to configure the root app injector
+ * with a base href value, so that the DI framework can supply the dependency anywhere in the app.
+ *
+ * ```ts
+ * import {NgModule} from '@angular/core';
+ * import {APP_BASE_HREF} from '@angular/common';
+ *
+ * @NgModule({
+ * providers: [{provide: APP_BASE_HREF, useValue: '/my/app'}]
+ * })
+ * class AppModule {}
+ * ```
+ *
+ * @publicApi
+ */
+declare const APP_BASE_HREF: InjectionToken;
+/**
+ * @description
+ * A {@link LocationStrategy} used to configure the {@link Location} service to
+ * represent its state in the
+ * [path](https://en.wikipedia.org/wiki/Uniform_Resource_Locator#Syntax) of the
+ * browser's URL.
+ *
+ * If you're using `PathLocationStrategy`, you may provide a {@link APP_BASE_HREF}
+ * or add a ` ` element to the document to override the default.
+ *
+ * For instance, if you provide an `APP_BASE_HREF` of `'/my/app/'` and call
+ * `location.go('/foo')`, the browser's URL will become
+ * `example.com/my/app/foo`. To ensure all relative URIs resolve correctly,
+ * the ` ` and/or `APP_BASE_HREF` should end with a `/`.
+ *
+ * Similarly, if you add ` ` to the document and call
+ * `location.go('/foo')`, the browser's URL will become
+ * `example.com/my/app/foo`.
+ *
+ * Note that when using `PathLocationStrategy`, neither the query nor
+ * the fragment in the ` ` will be preserved, as outlined
+ * by the [RFC](https://tools.ietf.org/html/rfc3986#section-5.2.2).
+ *
+ * @usageNotes
+ *
+ * ### Example
+ *
+ * {@example common/location/ts/path_location_component.ts region='LocationComponent'}
+ *
+ * @publicApi
+ */
+declare class PathLocationStrategy extends LocationStrategy implements OnDestroy {
+ private _platformLocation;
+ private _baseHref;
+ private _removeListenerFns;
+ constructor(_platformLocation: PlatformLocation, href?: string);
+ /** @docs-private */
+ ngOnDestroy(): void;
+ onPopState(fn: LocationChangeListener): void;
+ getBaseHref(): string;
+ prepareExternalUrl(internal: string): string;
+ path(includeHash?: boolean): string;
+ pushState(state: any, title: string, url: string, queryParams: string): void;
+ replaceState(state: any, title: string, url: string, queryParams: string): void;
+ forward(): void;
+ back(): void;
+ getState(): unknown;
+ historyGo(relativePosition?: number): void;
+ static ɵfac: i0.ɵɵFactoryDeclaration;
+ static ɵprov: i0.ɵɵInjectableDeclaration;
+}
+
+/** @publicApi */
+interface PopStateEvent {
+ pop?: boolean;
+ state?: any;
+ type?: string;
+ url?: string;
+}
+/**
+ * @description
+ *
+ * A service that applications can use to interact with a browser's URL.
+ *
+ * Depending on the `LocationStrategy` used, `Location` persists
+ * to the URL's path or the URL's hash segment.
+ *
+ * @usageNotes
+ *
+ * It's better to use the `Router.navigate()` service to trigger route changes. Use
+ * `Location` only if you need to interact with or create normalized URLs outside of
+ * routing.
+ *
+ * `Location` is responsible for normalizing the URL against the application's base href.
+ * A normalized URL is absolute from the URL host, includes the application's base href, and has no
+ * trailing slash:
+ * - `/my/app/user/123` is normalized
+ * - `my/app/user/123` **is not** normalized
+ * - `/my/app/user/123/` **is not** normalized
+ *
+ * ### Example
+ *
+ * {@example common/location/ts/path_location_component.ts region='LocationComponent'}
+ *
+ * @publicApi
+ */
+declare class Location implements OnDestroy {
+ constructor(locationStrategy: LocationStrategy);
+ /** @docs-private */
+ ngOnDestroy(): void;
+ /**
+ * Normalizes the URL path for this location.
+ *
+ * @param includeHash True to include an anchor fragment in the path.
+ *
+ * @returns The normalized URL path.
+ */
+ path(includeHash?: boolean): string;
+ /**
+ * Reports the current state of the location history.
+ * @returns The current value of the `history.state` object.
+ */
+ getState(): unknown;
+ /**
+ * Normalizes the given path and compares to the current normalized path.
+ *
+ * @param path The given URL path.
+ * @param query Query parameters.
+ *
+ * @returns True if the given URL path is equal to the current normalized path, false
+ * otherwise.
+ */
+ isCurrentPathEqualTo(path: string, query?: string): boolean;
+ /**
+ * Normalizes a URL path by stripping any trailing slashes.
+ *
+ * @param url String representing a URL.
+ *
+ * @returns The normalized URL string.
+ */
+ normalize(url: string): string;
+ /**
+ * Normalizes an external URL path.
+ * If the given URL doesn't begin with a leading slash (`'/'`), adds one
+ * before normalizing. Adds a hash if `HashLocationStrategy` is
+ * in use, or the `APP_BASE_HREF` if the `PathLocationStrategy` is in use.
+ *
+ * @param url String representing a URL.
+ *
+ * @returns A normalized platform-specific URL.
+ */
+ prepareExternalUrl(url: string): string;
+ /**
+ * Changes the browser's URL to a normalized version of a given URL, and pushes a
+ * new item onto the platform's history.
+ *
+ * @param path URL path to normalize.
+ * @param query Query parameters.
+ * @param state Location history state.
+ *
+ */
+ go(path: string, query?: string, state?: any): void;
+ /**
+ * Changes the browser's URL to a normalized version of the given URL, and replaces
+ * the top item on the platform's history stack.
+ *
+ * @param path URL path to normalize.
+ * @param query Query parameters.
+ * @param state Location history state.
+ */
+ replaceState(path: string, query?: string, state?: any): void;
+ /**
+ * Navigates forward in the platform's history.
+ */
+ forward(): void;
+ /**
+ * Navigates back in the platform's history.
+ */
+ back(): void;
+ /**
+ * Navigate to a specific page from session history, identified by its relative position to the
+ * current page.
+ *
+ * @param relativePosition Position of the target page in the history relative to the current
+ * page.
+ * A negative value moves backwards, a positive value moves forwards, e.g. `location.historyGo(2)`
+ * moves forward two pages and `location.historyGo(-2)` moves back two pages. When we try to go
+ * beyond what's stored in the history session, we stay in the current page. Same behaviour occurs
+ * when `relativePosition` equals 0.
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/History_API#Moving_to_a_specific_point_in_history
+ */
+ historyGo(relativePosition?: number): void;
+ /**
+ * Registers a URL change listener. Use to catch updates performed by the Angular
+ * framework that are not detectible through "popstate" or "hashchange" events.
+ *
+ * @param fn The change handler function, which take a URL and a location history state.
+ * @returns A function that, when executed, unregisters a URL change listener.
+ */
+ onUrlChange(fn: (url: string, state: unknown) => void): VoidFunction;
+ /**
+ * Subscribes to the platform's `popState` events.
+ *
+ * Note: `Location.go()` does not trigger the `popState` event in the browser. Use
+ * `Location.onUrlChange()` to subscribe to URL changes instead.
+ *
+ * @param value Event that is triggered when the state history changes.
+ * @param exception The exception to throw.
+ *
+ * @see [onpopstate](https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onpopstate)
+ *
+ * @returns Subscribed events.
+ */
+ subscribe(onNext: (value: PopStateEvent) => void, onThrow?: ((exception: any) => void) | null, onReturn?: (() => void) | null): SubscriptionLike;
+ /**
+ * Normalizes URL parameters by prepending with `?` if needed.
+ *
+ * @param params String of URL parameters.
+ *
+ * @returns The normalized URL parameters string.
+ */
+ static normalizeQueryParams: (params: string) => string;
+ /**
+ * Joins two parts of a URL with a slash if needed.
+ *
+ * @param start URL string
+ * @param end URL string
+ *
+ *
+ * @returns The joined URL string.
+ */
+ static joinWithSlash: (start: string, end: string) => string;
+ /**
+ * Removes a trailing slash from a URL string if needed.
+ * Looks for the first occurrence of either `#`, `?`, or the end of the
+ * line as `/` characters and removes the trailing slash if one exists.
+ *
+ * @param url URL string.
+ *
+ * @returns The URL string, modified if needed.
+ */
+ static stripTrailingSlash: (url: string) => string;
+ static ɵfac: i0.ɵɵFactoryDeclaration;
+ static ɵprov: i0.ɵɵInjectableDeclaration;
+}
+
+/**
+ * @publicApi
+ */
+declare abstract class NgLocalization {
+ abstract getPluralCategory(value: any, locale?: string): string;
+ static ɵfac: i0.ɵɵFactoryDeclaration;
+ static ɵprov: i0.ɵɵInjectableDeclaration;
+}
+/**
+ * Returns the plural case based on the locale
+ *
+ * @publicApi
+ */
+declare class NgLocaleLocalization extends NgLocalization {
+ protected locale: string;
+ constructor(locale: string);
+ getPluralCategory(value: any, locale?: string): string;
+ static ɵfac: i0.ɵɵFactoryDeclaration;
+ static ɵprov: i0.ɵɵInjectableDeclaration;
+}
+
+/**
+ * @ngModule CommonModule
+ *
+ * @usageNotes
+ * ```html
+ * ...
+ *
+ * ...
+ * ```
+ *
+ * For more simple use cases you can use the [class bindings](/guide/templates/binding#css-class-and-style-property-bindings) directly.
+ * It doesn't require importing a directive.
+ *
+ * ```html
+ * ...
+ *
+ * ...
+ *
+ * ...
+ *
+ * ...
+ * ```
+ * @description
+ *
+ * Adds and removes CSS classes on an HTML element.
+ *
+ * The CSS classes are updated as follows, depending on the type of the expression evaluation:
+ * - `string` - the CSS classes listed in the string (space delimited) are added,
+ * - `Array` - the CSS classes declared as Array elements are added,
+ * - `Object` - keys are CSS classes that get added when the expression given in the value
+ * evaluates to a truthy value, otherwise they are removed.
+ *
+ *
+ * @see [Class bindings](/guide/templates/binding#css-class-and-style-property-bindings)
+ *
+ * @publicApi
+ */
+declare class NgClass implements DoCheck {
+ private _ngEl;
+ private _renderer;
+ private initialClasses;
+ private rawClass;
+ private stateMap;
+ constructor(_ngEl: ElementRef, _renderer: Renderer2);
+ set klass(value: string);
+ set ngClass(value: string | string[] | Set | {
+ [klass: string]: any;
+ } | null | undefined);
+ ngDoCheck(): void;
+ private _updateState;
+ private _applyStateDiff;
+ private _toggleClass;
+ static ɵfac: i0.ɵɵFactoryDeclaration;
+ static ɵdir: i0.ɵɵDirectiveDeclaration;
+}
+
+/**
+ * Instantiates a {@link /api/core/Component Component} type and inserts its Host View into the current View.
+ * `NgComponentOutlet` provides a declarative approach for dynamic component creation.
+ *
+ * `NgComponentOutlet` requires a component type, if a falsy value is set the view will clear and
+ * any existing component will be destroyed.
+ *
+ * @usageNotes
+ *
+ * ### Fine tune control
+ *
+ * You can control the component creation process by using the following optional attributes:
+ *
+ * * `ngComponentOutletInputs`: Optional component inputs object, which will be bind to the
+ * component.
+ *
+ * * `ngComponentOutletInjector`: Optional custom {@link Injector} that will be used as parent for
+ * the Component. Defaults to the injector of the current view container.
+ *
+ * * `ngComponentOutletContent`: Optional list of projectable nodes to insert into the content
+ * section of the component, if it exists.
+ *
+ * * `ngComponentOutletNgModule`: Optional NgModule class reference to allow loading another
+ * module dynamically, then loading a component from that module.
+ *
+ * * `ngComponentOutletNgModuleFactory`: Deprecated config option that allows providing optional
+ * NgModule factory to allow loading another module dynamically, then loading a component from that
+ * module. Use `ngComponentOutletNgModule` instead.
+ *
+ * ### Syntax
+ *
+ * Simple
+ * ```html
+ *
+ * ```
+ *
+ * With inputs
+ * ```html
+ *
+ *
+ * ```
+ *
+ * Customized injector/content
+ * ```html
+ *
+ *
+ * ```
+ *
+ * Customized NgModule reference
+ * ```html
+ *
+ *
+ * ```
+ *
+ * ### A simple example
+ *
+ * {@example common/ngComponentOutlet/ts/module.ts region='SimpleExample'}
+ *
+ * A more complete example with additional options:
+ *
+ * {@example common/ngComponentOutlet/ts/module.ts region='CompleteExample'}
+ *
+ * @publicApi
+ * @ngModule CommonModule
+ */
+declare class NgComponentOutlet implements OnChanges, DoCheck, OnDestroy {
+ private _viewContainerRef;
+ /** Component that should be rendered in the outlet. */
+ ngComponentOutlet: Type | null;
+ ngComponentOutletInputs?: Record;
+ ngComponentOutletInjector?: Injector;
+ ngComponentOutletContent?: any[][];
+ ngComponentOutletNgModule?: Type;
+ /**
+ * @deprecated This input is deprecated, use `ngComponentOutletNgModule` instead.
+ */
+ ngComponentOutletNgModuleFactory?: NgModuleFactory;
+ private _componentRef;
+ private _moduleRef;
+ /**
+ * A helper data structure that allows us to track inputs that were part of the
+ * ngComponentOutletInputs expression. Tracking inputs is necessary for proper removal of ones
+ * that are no longer referenced.
+ */
+ private _inputsUsed;
+ /**
+ * Gets the instance of the currently-rendered component.
+ * Will be null if no component has been rendered.
+ */
+ get componentInstance(): T | null;
+ constructor(_viewContainerRef: ViewContainerRef);
+ private _needToReCreateNgModuleInstance;
+ private _needToReCreateComponentInstance;
+ /** @docs-private */
+ ngOnChanges(changes: SimpleChanges): void;
+ /** @docs-private */
+ ngDoCheck(): void;
+ /** @docs-private */
+ ngOnDestroy(): void;
+ private _applyInputStateDiff;
+ static ɵfac: i0.ɵɵFactoryDeclaration, never>;
+ static ɵdir: i0.ɵɵDirectiveDeclaration, "[ngComponentOutlet]", ["ngComponentOutlet"], { "ngComponentOutlet": { "alias": "ngComponentOutlet"; "required": false; }; "ngComponentOutletInputs": { "alias": "ngComponentOutletInputs"; "required": false; }; "ngComponentOutletInjector": { "alias": "ngComponentOutletInjector"; "required": false; }; "ngComponentOutletContent": { "alias": "ngComponentOutletContent"; "required": false; }; "ngComponentOutletNgModule": { "alias": "ngComponentOutletNgModule"; "required": false; }; "ngComponentOutletNgModuleFactory": { "alias": "ngComponentOutletNgModuleFactory"; "required": false; }; }, {}, never, never, true, never>;
+}
+
+/**
+ * @publicApi
+ */
+declare class NgForOfContext = NgIterable> {
+ /** Reference to the current item from the collection. */
+ $implicit: T;
+ /**
+ * The value of the iterable expression. Useful when the expression is
+ * more complex then a property access, for example when using the async pipe
+ * (`userStreams | async`).
+ */
+ ngForOf: U;
+ /** Returns an index of the current item in the collection. */
+ index: number;
+ /** Returns total amount of items in the collection. */
+ count: number;
+ constructor(
+ /** Reference to the current item from the collection. */
+ $implicit: T,
+ /**
+ * The value of the iterable expression. Useful when the expression is
+ * more complex then a property access, for example when using the async pipe
+ * (`userStreams | async`).
+ */
+ ngForOf: U,
+ /** Returns an index of the current item in the collection. */
+ index: number,
+ /** Returns total amount of items in the collection. */
+ count: number);
+ get first(): boolean;
+ get last(): boolean;
+ get even(): boolean;
+ get odd(): boolean;
+}
+/**
+ * A [structural directive](guide/directives/structural-directives) that renders
+ * a template for each item in a collection.
+ * The directive is placed on an element, which becomes the parent
+ * of the cloned templates.
+ *
+ * The `ngForOf` directive is generally used in the
+ * [shorthand form](guide/directives/structural-directives#asterisk) `*ngFor`.
+ * In this form, the template to be rendered for each iteration is the content
+ * of an anchor element containing the directive.
+ *
+ * The following example shows the shorthand syntax with some options,
+ * contained in an `` element.
+ *
+ * ```html
+ * ...
+ * ```
+ *
+ * The shorthand form expands into a long form that uses the `ngForOf` selector
+ * on an `` element.
+ * The content of the `` element is the `` element that held the
+ * short-form directive.
+ *
+ * Here is the expanded version of the short-form example.
+ *
+ * ```html
+ *
+ * ...
+ *
+ * ```
+ *
+ * Angular automatically expands the shorthand syntax as it compiles the template.
+ * The context for each embedded view is logically merged to the current component
+ * context according to its lexical position.
+ *
+ * When using the shorthand syntax, Angular allows only [one structural directive
+ * on an element](guide/directives/structural-directives#one-per-element).
+ * If you want to iterate conditionally, for example,
+ * put the `*ngIf` on a container element that wraps the `*ngFor` element.
+ * For further discussion, see
+ * [Structural Directives](guide/directives/structural-directives#one-per-element).
+ *
+ * @usageNotes
+ *
+ * ### Local variables
+ *
+ * `NgForOf` provides exported values that can be aliased to local variables.
+ * For example:
+ *
+ * ```html
+ *
+ * {{i}}/{{users.length}}. {{user}} default
+ *
+ * ```
+ *
+ * The following exported values can be aliased to local variables:
+ *
+ * - `$implicit: T`: The value of the individual items in the iterable (`ngForOf`).
+ * - `ngForOf: NgIterable`: The value of the iterable expression. Useful when the expression is
+ * more complex then a property access, for example when using the async pipe (`userStreams |
+ * async`).
+ * - `index: number`: The index of the current item in the iterable.
+ * - `count: number`: The length of the iterable.
+ * - `first: boolean`: True when the item is the first item in the iterable.
+ * - `last: boolean`: True when the item is the last item in the iterable.
+ * - `even: boolean`: True when the item has an even index in the iterable.
+ * - `odd: boolean`: True when the item has an odd index in the iterable.
+ *
+ * ### Change propagation
+ *
+ * When the contents of the iterator changes, `NgForOf` makes the corresponding changes to the DOM:
+ *
+ * * When an item is added, a new instance of the template is added to the DOM.
+ * * When an item is removed, its template instance is removed from the DOM.
+ * * When items are reordered, their respective templates are reordered in the DOM.
+ *
+ * Angular uses object identity to track insertions and deletions within the iterator and reproduce
+ * those changes in the DOM. This has important implications for animations and any stateful
+ * controls that are present, such as ` ` elements that accept user input. Inserted rows can
+ * be animated in, deleted rows can be animated out, and unchanged rows retain any unsaved state
+ * such as user input.
+ * For more on animations, see [Transitions and Triggers](guide/animations/transition-and-triggers).
+ *
+ * The identities of elements in the iterator can change while the data does not.
+ * This can happen, for example, if the iterator is produced from an RPC to the server, and that
+ * RPC is re-run. Even if the data hasn't changed, the second response produces objects with
+ * different identities, and Angular must tear down the entire DOM and rebuild it (as if all old
+ * elements were deleted and all new elements inserted).
+ *
+ * To avoid this expensive operation, you can customize the default tracking algorithm.
+ * by supplying the `trackBy` option to `NgForOf`.
+ * `trackBy` takes a function that has two arguments: `index` and `item`.
+ * If `trackBy` is given, Angular tracks changes by the return value of the function.
+ *
+ * @see [Structural Directives](guide/directives/structural-directives)
+ * @ngModule CommonModule
+ * @publicApi
+ */
+declare class NgForOf = NgIterable> implements DoCheck {
+ private _viewContainer;
+ private _template;
+ private _differs;
+ /**
+ * The value of the iterable expression, which can be used as a
+ * [template input variable](guide/directives/structural-directives#shorthand).
+ */
+ set ngForOf(ngForOf: (U & NgIterable) | undefined | null);
+ /**
+ * Specifies a custom `TrackByFunction` to compute the identity of items in an iterable.
+ *
+ * If a custom `TrackByFunction` is not provided, `NgForOf` will use the item's [object
+ * identity](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is)
+ * as the key.
+ *
+ * `NgForOf` uses the computed key to associate items in an iterable with DOM elements
+ * it produces for these items.
+ *
+ * A custom `TrackByFunction` is useful to provide good user experience in cases when items in an
+ * iterable rendered using `NgForOf` have a natural identifier (for example, custom ID or a
+ * primary key), and this iterable could be updated with new object instances that still
+ * represent the same underlying entity (for example, when data is re-fetched from the server,
+ * and the iterable is recreated and re-rendered, but most of the data is still the same).
+ *
+ * @see {@link TrackByFunction}
+ */
+ set ngForTrackBy(fn: TrackByFunction);
+ get ngForTrackBy(): TrackByFunction;
+ private _ngForOf;
+ private _ngForOfDirty;
+ private _differ;
+ private _trackByFn;
+ constructor(_viewContainer: ViewContainerRef, _template: TemplateRef>, _differs: IterableDiffers);
+ /**
+ * A reference to the template that is stamped out for each item in the iterable.
+ * @see [template reference variable](guide/templates/variables#template-reference-variables)
+ */
+ set ngForTemplate(value: TemplateRef>);
+ /**
+ * Applies the changes when needed.
+ * @docs-private
+ */
+ ngDoCheck(): void;
+ private _applyChanges;
+ /**
+ * Asserts the correct type of the context for the template that `NgForOf` will render.
+ *
+ * The presence of this method is a signal to the Ivy template type-check compiler that the
+ * `NgForOf` structural directive renders its template with a specific context type.
+ */
+ static ngTemplateContextGuard>(dir: NgForOf, ctx: any): ctx is NgForOfContext;
+ static ɵfac: i0.ɵɵFactoryDeclaration, never>;
+ static ɵdir: i0.ɵɵDirectiveDeclaration, "[ngFor][ngForOf]", never, { "ngForOf": { "alias": "ngForOf"; "required": false; }; "ngForTrackBy": { "alias": "ngForTrackBy"; "required": false; }; "ngForTemplate": { "alias": "ngForTemplate"; "required": false; }; }, {}, never, never, true, never>;
+}
+
+/**
+ * A structural directive that conditionally includes a template based on the value of
+ * an expression coerced to Boolean.
+ * When the expression evaluates to true, Angular renders the template
+ * provided in a `then` clause, and when false or null,
+ * Angular renders the template provided in an optional `else` clause. The default
+ * template for the `else` clause is blank.
+ *
+ * A [shorthand form](guide/directives/structural-directives#asterisk) of the directive,
+ * `*ngIf="condition"`, is generally used, provided
+ * as an attribute of the anchor element for the inserted template.
+ * Angular expands this into a more explicit version, in which the anchor element
+ * is contained in an `` element.
+ *
+ * Simple form with shorthand syntax:
+ *
+ * ```html
+ * Content to render when condition is true.
+ * ```
+ *
+ * Simple form with expanded syntax:
+ *
+ * ```html
+ * Content to render when condition is
+ * true.
+ * ```
+ *
+ * Form with an "else" block:
+ *
+ * ```html
+ * Content to render when condition is true.
+ * Content to render when condition is false.
+ * ```
+ *
+ * Shorthand form with "then" and "else" blocks:
+ *
+ * ```html
+ *
+ * Content to render when condition is true.
+ * Content to render when condition is false.
+ * ```
+ *
+ * Form with storing the value locally:
+ *
+ * ```html
+ * {{value}}
+ * Content to render when value is null.
+ * ```
+ *
+ * @usageNotes
+ *
+ * The `*ngIf` directive is most commonly used to conditionally show an inline template,
+ * as seen in the following example.
+ * The default `else` template is blank.
+ *
+ * {@example common/ngIf/ts/module.ts region='NgIfSimple'}
+ *
+ * ### Showing an alternative template using `else`
+ *
+ * To display a template when `expression` evaluates to false, use an `else` template
+ * binding as shown in the following example.
+ * The `else` binding points to an `` element labeled `#elseBlock`.
+ * The template can be defined anywhere in the component view, but is typically placed right after
+ * `ngIf` for readability.
+ *
+ * {@example common/ngIf/ts/module.ts region='NgIfElse'}
+ *
+ * ### Using an external `then` template
+ *
+ * In the previous example, the then-clause template is specified inline, as the content of the
+ * tag that contains the `ngIf` directive. You can also specify a template that is defined
+ * externally, by referencing a labeled `` element. When you do this, you can
+ * change which template to use at runtime, as shown in the following example.
+ *
+ * {@example common/ngIf/ts/module.ts region='NgIfThenElse'}
+ *
+ * ### Storing a conditional result in a variable
+ *
+ * You might want to show a set of properties from the same object. If you are waiting
+ * for asynchronous data, the object can be undefined.
+ * In this case, you can use `ngIf` and store the result of the condition in a local
+ * variable as shown in the following example.
+ *
+ * {@example common/ngIf/ts/module.ts region='NgIfAs'}
+ *
+ * This code uses only one `AsyncPipe`, so only one subscription is created.
+ * The conditional statement stores the result of `userStream|async` in the local variable `user`.
+ * You can then bind the local `user` repeatedly.
+ *
+ * The conditional displays the data only if `userStream` returns a value,
+ * so you don't need to use the
+ * safe-navigation-operator (`?.`)
+ * to guard against null values when accessing properties.
+ * You can display an alternative template while waiting for the data.
+ *
+ * ### Shorthand syntax
+ *
+ * The shorthand syntax `*ngIf` expands into two separate template specifications
+ * for the "then" and "else" clauses. For example, consider the following shorthand statement,
+ * that is meant to show a loading page while waiting for data to be loaded.
+ *
+ * ```html
+ *
+ * ...
+ *
+ *
+ *
+ * Loading...
+ *
+ * ```
+ *
+ * You can see that the "else" clause references the ``
+ * with the `#loading` label, and the template for the "then" clause
+ * is provided as the content of the anchor element.
+ *
+ * However, when Angular expands the shorthand syntax, it creates
+ * another `` tag, with `ngIf` and `ngIfElse` directives.
+ * The anchor element containing the template for the "then" clause becomes
+ * the content of this unlabeled `` tag.
+ *
+ * ```html
+ *
+ *
+ * ...
+ *
+ *
+ *
+ *
+ * Loading...
+ *
+ * ```
+ *
+ * The presence of the implicit template object has implications for the nesting of
+ * structural directives. For more on this subject, see
+ * [Structural Directives](guide/directives/structural-directives#one-per-element).
+ *
+ * @ngModule CommonModule
+ * @publicApi
+ */
+declare class NgIf {
+ private _viewContainer;
+ private _context;
+ private _thenTemplateRef;
+ private _elseTemplateRef;
+ private _thenViewRef;
+ private _elseViewRef;
+ constructor(_viewContainer: ViewContainerRef, templateRef: TemplateRef>);
+ /**
+ * The Boolean expression to evaluate as the condition for showing a template.
+ */
+ set ngIf(condition: T);
+ /**
+ * A template to show if the condition expression evaluates to true.
+ */
+ set ngIfThen(templateRef: TemplateRef> | null);
+ /**
+ * A template to show if the condition expression evaluates to false.
+ */
+ set ngIfElse(templateRef: TemplateRef> | null);
+ private _updateView;
+ /**
+ * Assert the correct type of the expression bound to the `ngIf` input within the template.
+ *
+ * The presence of this static field is a signal to the Ivy template type check compiler that
+ * when the `NgIf` structural directive renders its template, the type of the expression bound
+ * to `ngIf` should be narrowed in some way. For `NgIf`, the binding expression itself is used to
+ * narrow its type, which allows the strictNullChecks feature of TypeScript to work with `NgIf`.
+ */
+ static ngTemplateGuard_ngIf: 'binding';
+ /**
+ * Asserts the correct type of the context for the template that `NgIf` will render.
+ *
+ * The presence of this method is a signal to the Ivy template type-check compiler that the
+ * `NgIf` structural directive renders its template with a specific context type.
+ */
+ static ngTemplateContextGuard(dir: NgIf, ctx: any): ctx is NgIfContext>;
+ static ɵfac: i0.ɵɵFactoryDeclaration, never>;
+ static ɵdir: i0.ɵɵDirectiveDeclaration, "[ngIf]", never, { "ngIf": { "alias": "ngIf"; "required": false; }; "ngIfThen": { "alias": "ngIfThen"; "required": false; }; "ngIfElse": { "alias": "ngIfElse"; "required": false; }; }, {}, never, never, true, never>;
+}
+/**
+ * @publicApi
+ */
+declare class NgIfContext {
+ $implicit: T;
+ ngIf: T;
+}
+
+/**
+ * @ngModule CommonModule
+ *
+ * @description
+ *
+ * Inserts an embedded view from a prepared `TemplateRef`.
+ *
+ * You can attach a context object to the `EmbeddedViewRef` by setting `[ngTemplateOutletContext]`.
+ * `[ngTemplateOutletContext]` should be an object, the object's keys will be available for binding
+ * by the local template `let` declarations.
+ *
+ * @usageNotes
+ * ```html
+ *
+ * ```
+ *
+ * Using the key `$implicit` in the context object will set its value as default.
+ *
+ * ### Example
+ *
+ * {@example common/ngTemplateOutlet/ts/module.ts region='NgTemplateOutlet'}
+ *
+ * @publicApi
+ */
+declare class NgTemplateOutlet implements OnChanges {
+ private _viewContainerRef;
+ private _viewRef;
+ /**
+ * A context object to attach to the {@link EmbeddedViewRef}. This should be an
+ * object, the object's keys will be available for binding by the local template `let`
+ * declarations.
+ * Using the key `$implicit` in the context object will set its value as default.
+ */
+ ngTemplateOutletContext: C | null;
+ /**
+ * A string defining the template reference and optionally the context object for the template.
+ */
+ ngTemplateOutlet: TemplateRef | null;
+ /** Injector to be used within the embedded view. */
+ ngTemplateOutletInjector: Injector | null;
+ constructor(_viewContainerRef: ViewContainerRef);
+ ngOnChanges(changes: SimpleChanges): void;
+ /**
+ * We need to re-create existing embedded view if either is true:
+ * - the outlet changed.
+ * - the injector changed.
+ */
+ private _shouldRecreateView;
+ /**
+ * For a given outlet instance, we create a proxy object that delegates
+ * to the user-specified context. This allows changing, or swapping out
+ * the context object completely without having to destroy/re-create the view.
+ */
+ private _createContextForwardProxy;
+ static ɵfac: i0.ɵɵFactoryDeclaration, never>;
+ static ɵdir: i0.ɵɵDirectiveDeclaration, "[ngTemplateOutlet]", never, { "ngTemplateOutletContext": { "alias": "ngTemplateOutletContext"; "required": false; }; "ngTemplateOutlet": { "alias": "ngTemplateOutlet"; "required": false; }; "ngTemplateOutletInjector": { "alias": "ngTemplateOutletInjector"; "required": false; }; }, {}, never, never, true, never>;
+}
+
+/**
+ * @ngModule CommonModule
+ *
+ * @usageNotes
+ *
+ * Set the width of the containing element to a pixel value returned by an expression.
+ *
+ * ```html
+ * ...
+ * ```
+ *
+ * Set a collection of style values using an expression that returns key-value pairs.
+ *
+ * ```html
+ * ...
+ * ```
+ *
+ * For more simple use cases you can use the [style bindings](/guide/templates/binding#css-class-and-style-property-bindings) directly.
+ * It doesn't require importing a directive.
+ *
+ * Set the font of the containing element to the result of an expression.
+ *
+ * ```html
+ * ...
+ * ```
+ *
+ * @description
+ *
+ * An attribute directive that updates styles for the containing HTML element.
+ * Sets one or more style properties, specified as colon-separated key-value pairs.
+ * The key is a style name, with an optional `.` suffix
+ * (such as 'top.px', 'font-style.em').
+ * The value is an expression to be evaluated.
+ * The resulting non-null value, expressed in the given unit,
+ * is assigned to the given style property.
+ * If the result of evaluation is null, the corresponding style is removed.
+ *
+ * @see [Style bindings](/guide/templates/binding#css-class-and-style-property-bindings)
+ *
+ * @publicApi
+ */
+declare class NgStyle implements DoCheck {
+ private _ngEl;
+ private _differs;
+ private _renderer;
+ private _ngStyle;
+ private _differ;
+ constructor(_ngEl: ElementRef, _differs: KeyValueDiffers, _renderer: Renderer2);
+ set ngStyle(values: {
+ [klass: string]: any;
+ } | null | undefined);
+ ngDoCheck(): void;
+ private _setStyle;
+ private _applyChanges;
+ static ɵfac: i0.ɵɵFactoryDeclaration;
+ static ɵdir: i0.ɵɵDirectiveDeclaration;
+}
+
+declare class SwitchView {
+ private _viewContainerRef;
+ private _templateRef;
+ private _created;
+ constructor(_viewContainerRef: ViewContainerRef, _templateRef: TemplateRef);
+ create(): void;
+ destroy(): void;
+ enforceState(created: boolean): void;
+}
+/**
+ * @ngModule CommonModule
+ *
+ * @description
+ * The `[ngSwitch]` directive on a container specifies an expression to match against.
+ * The expressions to match are provided by `ngSwitchCase` directives on views within the container.
+ * - Every view that matches is rendered.
+ * - If there are no matches, a view with the `ngSwitchDefault` directive is rendered.
+ * - Elements within the `[NgSwitch]` statement but outside of any `NgSwitchCase`
+ * or `ngSwitchDefault` directive are preserved at the location.
+ *
+ * @usageNotes
+ * Define a container element for the directive, and specify the switch expression
+ * to match against as an attribute:
+ *
+ * ```html
+ *
+ * ```
+ *
+ * Within the container, `*ngSwitchCase` statements specify the match expressions
+ * as attributes. Include `*ngSwitchDefault` as the final case.
+ *
+ * ```html
+ *
+ * ...
+ * ...
+ * ...
+ *
+ * ```
+ *
+ * ### Usage Examples
+ *
+ * The following example shows how to use more than one case to display the same view:
+ *
+ * ```html
+ *
+ *
+ * ...
+ * ...
+ * ...
+ *
+ * ...
+ *
+ * ```
+ *
+ * The following example shows how cases can be nested:
+ * ```html
+ *
+ * ...
+ * ...
+ * ...
+ *
+ *
+ *
+ *
+ *
+ * ...
+ *
+ * ```
+ *
+ * @publicApi
+ * @see {@link NgSwitchCase}
+ * @see {@link NgSwitchDefault}
+ * @see [Structural Directives](guide/directives/structural-directives)
+ *
+ */
+declare class NgSwitch {
+ private _defaultViews;
+ private _defaultUsed;
+ private _caseCount;
+ private _lastCaseCheckIndex;
+ private _lastCasesMatched;
+ private _ngSwitch;
+ set ngSwitch(newValue: any);
+ private _updateDefaultCases;
+ static ɵfac: i0.ɵɵFactoryDeclaration;
+ static ɵdir: i0.ɵɵDirectiveDeclaration;
+}
+/**
+ * @ngModule CommonModule
+ *
+ * @description
+ * Provides a switch case expression to match against an enclosing `ngSwitch` expression.
+ * When the expressions match, the given `NgSwitchCase` template is rendered.
+ * If multiple match expressions match the switch expression value, all of them are displayed.
+ *
+ * @usageNotes
+ *
+ * Within a switch container, `*ngSwitchCase` statements specify the match expressions
+ * as attributes. Include `*ngSwitchDefault` as the final case.
+ *
+ * ```html
+ *
+ * ...
+ * ...
+ * ...
+ *
+ * ```
+ *
+ * Each switch-case statement contains an in-line HTML template or template reference
+ * that defines the subtree to be selected if the value of the match expression
+ * matches the value of the switch expression.
+ *
+ * As of Angular v17 the NgSwitch directive uses strict equality comparison (`===`) instead of
+ * loose equality (`==`) to match different cases.
+ *
+ * @publicApi
+ * @see {@link NgSwitch}
+ * @see {@link NgSwitchDefault}
+ *
+ */
+declare class NgSwitchCase implements DoCheck {
+ private ngSwitch;
+ private _view;
+ /**
+ * Stores the HTML template to be selected on match.
+ */
+ ngSwitchCase: any;
+ constructor(viewContainer: ViewContainerRef, templateRef: TemplateRef, ngSwitch: NgSwitch);
+ /**
+ * Performs case matching. For internal use only.
+ * @docs-private
+ */
+ ngDoCheck(): void;
+ static ɵfac: i0.ɵɵFactoryDeclaration;
+ static ɵdir: i0.ɵɵDirectiveDeclaration;
+}
+/**
+ * @ngModule CommonModule
+ *
+ * @description
+ *
+ * Creates a view that is rendered when no `NgSwitchCase` expressions
+ * match the `NgSwitch` expression.
+ * This statement should be the final case in an `NgSwitch`.
+ *
+ * @publicApi
+ * @see {@link NgSwitch}
+ * @see {@link NgSwitchCase}
+ *
+ */
+declare class NgSwitchDefault {
+ constructor(viewContainer: ViewContainerRef, templateRef: TemplateRef, ngSwitch: NgSwitch);
+ static ɵfac: i0.ɵɵFactoryDeclaration;
+ static ɵdir: i0.ɵɵDirectiveDeclaration;
+}
+
+/**
+ * @ngModule CommonModule
+ *
+ * @usageNotes
+ * ```html
+ *
+ * there is nothing
+ * there is one
+ * there are a few
+ *
+ * ```
+ *
+ * @description
+ *
+ * Adds / removes DOM sub-trees based on a numeric value. Tailored for pluralization.
+ *
+ * Displays DOM sub-trees that match the switch expression value, or failing that, DOM sub-trees
+ * that match the switch expression's pluralization category.
+ *
+ * To use this directive you must provide a container element that sets the `[ngPlural]` attribute
+ * to a switch expression. Inner elements with a `[ngPluralCase]` will display based on their
+ * expression:
+ * - if `[ngPluralCase]` is set to a value starting with `=`, it will only display if the value
+ * matches the switch expression exactly,
+ * - otherwise, the view will be treated as a "category match", and will only display if exact
+ * value matches aren't found and the value maps to its category for the defined locale.
+ *
+ * See http://cldr.unicode.org/index/cldr-spec/plural-rules
+ *
+ * @publicApi
+ */
+declare class NgPlural {
+ private _localization;
+ private _activeView?;
+ private _caseViews;
+ constructor(_localization: NgLocalization);
+ set ngPlural(value: number);
+ addCase(value: string, switchView: SwitchView): void;
+ private _updateView;
+ private _clearViews;
+ private _activateView;
+ static ɵfac: i0.ɵɵFactoryDeclaration;
+ static ɵdir: i0.ɵɵDirectiveDeclaration;
+}
+/**
+ * @ngModule CommonModule
+ *
+ * @description
+ *
+ * Creates a view that will be added/removed from the parent {@link NgPlural} when the
+ * given expression matches the plural expression according to CLDR rules.
+ *
+ * @usageNotes
+ * ```html
+ *
+ * ...
+ * ...
+ *
+ *```
+ *
+ * See {@link NgPlural} for more details and example.
+ *
+ * @publicApi
+ */
+declare class NgPluralCase {
+ value: string;
+ constructor(value: string, template: TemplateRef, viewContainer: ViewContainerRef, ngPlural: NgPlural);
+ static ɵfac: i0.ɵɵFactoryDeclaration;
+ static ɵdir: i0.ɵɵDirectiveDeclaration;
+}
+
+/**
+ * @ngModule CommonModule
+ * @description
+ *
+ * Unwraps a value from an asynchronous primitive.
+ *
+ * The `async` pipe subscribes to an `Observable` or `Promise` and returns the latest value it has
+ * emitted. When a new value is emitted, the `async` pipe marks the component to be checked for
+ * changes. When the component gets destroyed, the `async` pipe unsubscribes automatically to avoid
+ * potential memory leaks. When the reference of the expression changes, the `async` pipe
+ * automatically unsubscribes from the old `Observable` or `Promise` and subscribes to the new one.
+ *
+ * @usageNotes
+ *
+ * ### Examples
+ *
+ * This example binds a `Promise` to the view. Clicking the `Resolve` button resolves the
+ * promise.
+ *
+ * {@example common/pipes/ts/async_pipe.ts region='AsyncPipePromise'}
+ *
+ * It's also possible to use `async` with Observables. The example below binds the `time` Observable
+ * to the view. The Observable continuously updates the view with the current time.
+ *
+ * {@example common/pipes/ts/async_pipe.ts region='AsyncPipeObservable'}
+ *
+ * @publicApi
+ */
+declare class AsyncPipe implements OnDestroy, PipeTransform {
+ private _ref;
+ private _latestValue;
+ private markForCheckOnValueUpdate;
+ private _subscription;
+ private _obj;
+ private _strategy;
+ constructor(ref: ChangeDetectorRef);
+ ngOnDestroy(): void;
+ transform(obj: Observable | Subscribable | Promise): T | null;
+ transform(obj: null | undefined): null;
+ transform(obj: Observable | Subscribable | Promise | null | undefined): T | null;
+ private _subscribe;
+ private _selectStrategy;
+ private _dispose;
+ private _updateLatestValue;
+ static ɵfac: i0.ɵɵFactoryDeclaration;
+ static ɵpipe: i0.ɵɵPipeDeclaration;
+}
+
+/**
+ * Transforms text to all lower case.
+ *
+ * @see {@link UpperCasePipe}
+ * @see {@link TitleCasePipe}
+ * @usageNotes
+ *
+ * The following example defines a view that allows the user to enter
+ * text, and then uses the pipe to convert the input text to all lower case.
+ *
+ * {@example common/pipes/ts/lowerupper_pipe.ts region='LowerUpperPipe'}
+ *
+ * @ngModule CommonModule
+ * @publicApi
+ */
+declare class LowerCasePipe implements PipeTransform {
+ /**
+ * @param value The string to transform to lower case.
+ */
+ transform(value: string): string;
+ transform(value: null | undefined): null;
+ transform(value: string | null | undefined): string | null;
+ static ɵfac: i0.ɵɵFactoryDeclaration;
+ static ɵpipe: i0.ɵɵPipeDeclaration;
+}
+/**
+ * Transforms text to title case.
+ * Capitalizes the first letter of each word and transforms the
+ * rest of the word to lower case.
+ * Words are delimited by any whitespace character, such as a space, tab, or line-feed character.
+ *
+ * @see {@link LowerCasePipe}
+ * @see {@link UpperCasePipe}
+ *
+ * @usageNotes
+ * The following example shows the result of transforming various strings into title case.
+ *
+ * {@example common/pipes/ts/titlecase_pipe.ts region='TitleCasePipe'}
+ *
+ * @ngModule CommonModule
+ * @publicApi
+ */
+declare class TitleCasePipe implements PipeTransform {
+ /**
+ * @param value The string to transform to title case.
+ */
+ transform(value: string): string;
+ transform(value: null | undefined): null;
+ transform(value: string | null | undefined): string | null;
+ static ɵfac: i0.ɵɵFactoryDeclaration;
+ static ɵpipe: i0.ɵɵPipeDeclaration;
+}
+/**
+ * Transforms text to all upper case.
+ * @see {@link LowerCasePipe}
+ * @see {@link TitleCasePipe}
+ *
+ * @ngModule CommonModule
+ * @publicApi
+ */
+declare class UpperCasePipe implements PipeTransform {
+ /**
+ * @param value The string to transform to upper case.
+ */
+ transform(value: string): string;
+ transform(value: null | undefined): null;
+ transform(value: string | null | undefined): string | null;
+ static ɵfac: i0.ɵɵFactoryDeclaration;
+ static ɵpipe: i0.ɵɵPipeDeclaration;
+}
+
+/**
+ * @ngModule CommonModule
+ * @description
+ *
+ * Converts a value into its JSON-format representation. Useful for debugging.
+ *
+ * @usageNotes
+ *
+ * The following component uses a JSON pipe to convert an object
+ * to JSON format, and displays the string in both formats for comparison.
+ *
+ * {@example common/pipes/ts/json_pipe.ts region='JsonPipe'}
+ *
+ * @publicApi
+ */
+declare class JsonPipe implements PipeTransform {
+ /**
+ * @param value A value of any type to convert into a JSON-format string.
+ */
+ transform(value: any): string;
+ static ɵfac: i0.ɵɵFactoryDeclaration;
+ static ɵpipe: i0.ɵɵPipeDeclaration;
+}
+
+/**
+ * @ngModule CommonModule
+ * @description
+ *
+ * Creates a new `Array` or `String` containing a subset (slice) of the elements.
+ *
+ * @usageNotes
+ *
+ * All behavior is based on the expected behavior of the JavaScript API `Array.prototype.slice()`
+ * and `String.prototype.slice()`.
+ *
+ * When operating on an `Array`, the returned `Array` is always a copy even when all
+ * the elements are being returned.
+ *
+ * When operating on a blank value, the pipe returns the blank value.
+ *
+ * ### List Example
+ *
+ * This `ngFor` example:
+ *
+ * {@example common/pipes/ts/slice_pipe.ts region='SlicePipe_list'}
+ *
+ * produces the following:
+ *
+ * ```html
+ * b
+ * c
+ * ```
+ *
+ * ### String Examples
+ *
+ * {@example common/pipes/ts/slice_pipe.ts region='SlicePipe_string'}
+ *
+ * @publicApi
+ */
+declare class SlicePipe implements PipeTransform {
+ /**
+ * @param value a list or a string to be sliced.
+ * @param start the starting index of the subset to return:
+ * - **a positive integer**: return the item at `start` index and all items after
+ * in the list or string expression.
+ * - **a negative integer**: return the item at `start` index from the end and all items after
+ * in the list or string expression.
+ * - **if positive and greater than the size of the expression**: return an empty list or
+ * string.
+ * - **if negative and greater than the size of the expression**: return entire list or string.
+ * @param end the ending index of the subset to return:
+ * - **omitted**: return all items until the end.
+ * - **if positive**: return all items before `end` index of the list or string.
+ * - **if negative**: return all items before `end` index from the end of the list or string.
+ */
+ transform(value: ReadonlyArray, start: number, end?: number): Array;
+ transform(value: null | undefined, start: number, end?: number): null;
+ transform(value: ReadonlyArray | null | undefined, start: number, end?: number): Array | null;
+ transform(value: string, start: number, end?: number): string;
+ transform(value: string | null | undefined, start: number, end?: number): string | null;
+ static ɵfac: i0.ɵɵFactoryDeclaration;
+ static ɵpipe: i0.ɵɵPipeDeclaration;
+}
+
+/**
+ * @ngModule CommonModule
+ * @description
+ *
+ * Formats a value according to digit options and locale rules.
+ * Locale determines group sizing and separator,
+ * decimal point character, and other locale-specific configurations.
+ *
+ * @see {@link formatNumber}
+ *
+ * @usageNotes
+ *
+ * ### digitsInfo
+ *
+ * The value's decimal representation is specified by the `digitsInfo`
+ * parameter, written in the following format:
+ *
+ * ```
+ * {minIntegerDigits}.{minFractionDigits}-{maxFractionDigits}
+ * ```
+ *
+ * - `minIntegerDigits`:
+ * The minimum number of integer digits before the decimal point.
+ * Default is 1.
+ *
+ * - `minFractionDigits`:
+ * The minimum number of digits after the decimal point.
+ * Default is 0.
+ *
+ * - `maxFractionDigits`:
+ * The maximum number of digits after the decimal point.
+ * Default is 3.
+ *
+ * If the formatted value is truncated it will be rounded using the "to-nearest" method:
+ *
+ * ```
+ * {{3.6 | number: '1.0-0'}}
+ *
+ *
+ * {{-3.6 | number:'1.0-0'}}
+ *
+ * ```
+ *
+ * ### locale
+ *
+ * `locale` will format a value according to locale rules.
+ * Locale determines group sizing and separator,
+ * decimal point character, and other locale-specific configurations.
+ *
+ * When not supplied, uses the value of `LOCALE_ID`, which is `en-US` by default.
+ *
+ * See [Setting your app locale](guide/i18n/locale-id).
+ *
+ * ### Example
+ *
+ * The following code shows how the pipe transforms values
+ * according to various format specifications,
+ * where the caller's default locale is `en-US`.
+ *
+ * {@example common/pipes/ts/number_pipe.ts region='NumberPipe'}
+ *
+ * @publicApi
+ */
+declare class DecimalPipe implements PipeTransform {
+ private _locale;
+ constructor(_locale: string);
+ /**
+ * @param value The value to be formatted.
+ * @param digitsInfo Sets digit and decimal representation.
+ * [See more](#digitsinfo).
+ * @param locale Specifies what locale format rules to use.
+ * [See more](#locale).
+ */
+ transform(value: number | string, digitsInfo?: string, locale?: string): string | null;
+ transform(value: null | undefined, digitsInfo?: string, locale?: string): null;
+ transform(value: number | string | null | undefined, digitsInfo?: string, locale?: string): string | null;
+ static ɵfac: i0.ɵɵFactoryDeclaration;
+ static ɵpipe: i0.ɵɵPipeDeclaration;
+}
+/**
+ * @ngModule CommonModule
+ * @description
+ *
+ * Transforms a number to a percentage
+ * string, formatted according to locale rules that determine group sizing and
+ * separator, decimal-point character, and other locale-specific
+ * configurations.
+ *
+ * @see {@link formatPercent}
+ *
+ * @usageNotes
+ * The following code shows how the pipe transforms numbers
+ * into text strings, according to various format specifications,
+ * where the caller's default locale is `en-US`.
+ *
+ * {@example common/pipes/ts/percent_pipe.ts region='PercentPipe'}
+ *
+ * @publicApi
+ */
+declare class PercentPipe implements PipeTransform {
+ private _locale;
+ constructor(_locale: string);
+ transform(value: number | string, digitsInfo?: string, locale?: string): string | null;
+ transform(value: null | undefined, digitsInfo?: string, locale?: string): null;
+ transform(value: number | string | null | undefined, digitsInfo?: string, locale?: string): string | null;
+ static ɵfac: i0.ɵɵFactoryDeclaration;
+ static ɵpipe: i0.ɵɵPipeDeclaration;
+}
+/**
+ * @ngModule CommonModule
+ * @description
+ *
+ * Transforms a number to a currency string, formatted according to locale rules
+ * that determine group sizing and separator, decimal-point character,
+ * and other locale-specific configurations.
+ *
+ *
+ * @see {@link getCurrencySymbol}
+ * @see {@link formatCurrency}
+ *
+ * @usageNotes
+ * The following code shows how the pipe transforms numbers
+ * into text strings, according to various format specifications,
+ * where the caller's default locale is `en-US`.
+ *
+ * {@example common/pipes/ts/currency_pipe.ts region='CurrencyPipe'}
+ *
+ * @publicApi
+ */
+declare class CurrencyPipe implements PipeTransform {
+ private _locale;
+ private _defaultCurrencyCode;
+ constructor(_locale: string, _defaultCurrencyCode?: string);
+ /**
+ *
+ * @param value The number to be formatted as currency.
+ * @param currencyCode The [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) currency code,
+ * such as `USD` for the US dollar and `EUR` for the euro. The default currency code can be
+ * configured using the `DEFAULT_CURRENCY_CODE` injection token.
+ * @param display The format for the currency indicator. One of the following:
+ * - `code`: Show the code (such as `USD`).
+ * - `symbol`(default): Show the symbol (such as `$`).
+ * - `symbol-narrow`: Use the narrow symbol for locales that have two symbols for their
+ * currency.
+ * For example, the Canadian dollar CAD has the symbol `CA$` and the symbol-narrow `$`. If the
+ * locale has no narrow symbol, uses the standard symbol for the locale.
+ * - String: Use the given string value instead of a code or a symbol.
+ * For example, an empty string will suppress the currency & symbol.
+ * - Boolean (marked deprecated in v5): `true` for symbol and false for `code`.
+ *
+ * @param digitsInfo Decimal representation options, specified by a string
+ * in the following format:
+ * {minIntegerDigits}.{minFractionDigits}-{maxFractionDigits}.
+ * - `minIntegerDigits`: The minimum number of integer digits before the decimal point.
+ * Default is `1`.
+ * - `minFractionDigits`: The minimum number of digits after the decimal point.
+ * Default is `2`.
+ * - `maxFractionDigits`: The maximum number of digits after the decimal point.
+ * Default is `2`.
+ * If not provided, the number will be formatted with the proper amount of digits,
+ * depending on what the [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) specifies.
+ * For example, the Canadian dollar has 2 digits, whereas the Chilean peso has none.
+ * @param locale A locale code for the locale format rules to use.
+ * When not supplied, uses the value of `LOCALE_ID`, which is `en-US` by default.
+ * See [Setting your app locale](guide/i18n/locale-id).
+ */
+ transform(value: number | string, currencyCode?: string, display?: 'code' | 'symbol' | 'symbol-narrow' | string | boolean, digitsInfo?: string, locale?: string): string | null;
+ transform(value: null | undefined, currencyCode?: string, display?: 'code' | 'symbol' | 'symbol-narrow' | string | boolean, digitsInfo?: string, locale?: string): null;
+ transform(value: number | string | null | undefined, currencyCode?: string, display?: 'code' | 'symbol' | 'symbol-narrow' | string | boolean, digitsInfo?: string, locale?: string): string | null;
+ static ɵfac: i0.ɵɵFactoryDeclaration;
+ static ɵpipe: i0.ɵɵPipeDeclaration;
+}
+
+/**
+ * An interface that describes the date pipe configuration, which can be provided using the
+ * `DATE_PIPE_DEFAULT_OPTIONS` token.
+ *
+ * @see {@link DATE_PIPE_DEFAULT_OPTIONS}
+ *
+ * @publicApi
+ */
+interface DatePipeConfig {
+ dateFormat?: string;
+ timezone?: string;
+}
+
+/**
+ * Optionally-provided default timezone to use for all instances of `DatePipe` (such as `'+0430'`).
+ * If the value isn't provided, the `DatePipe` will use the end-user's local system timezone.
+ *
+ * @deprecated use DATE_PIPE_DEFAULT_OPTIONS token to configure DatePipe
+ */
+declare const DATE_PIPE_DEFAULT_TIMEZONE: InjectionToken;
+/**
+ * DI token that allows to provide default configuration for the `DatePipe` instances in an
+ * application. The value is an object which can include the following fields:
+ * - `dateFormat`: configures the default date format. If not provided, the `DatePipe`
+ * will use the 'mediumDate' as a value.
+ * - `timezone`: configures the default timezone. If not provided, the `DatePipe` will
+ * use the end-user's local system timezone.
+ *
+ * @see {@link DatePipeConfig}
+ *
+ * @usageNotes
+ *
+ * Various date pipe default values can be overwritten by providing this token with
+ * the value that has this interface.
+ *
+ * For example:
+ *
+ * Override the default date format by providing a value using the token:
+ * ```ts
+ * providers: [
+ * {provide: DATE_PIPE_DEFAULT_OPTIONS, useValue: {dateFormat: 'shortDate'}}
+ * ]
+ * ```
+ *
+ * Override the default timezone by providing a value using the token:
+ * ```ts
+ * providers: [
+ * {provide: DATE_PIPE_DEFAULT_OPTIONS, useValue: {timezone: '-1200'}}
+ * ]
+ * ```
+ */
+declare const DATE_PIPE_DEFAULT_OPTIONS: InjectionToken;
+/**
+ * @ngModule CommonModule
+ * @description
+ *
+ * Formats a date value according to locale rules.
+ *
+ * `DatePipe` is executed only when it detects a pure change to the input value.
+ * A pure change is either a change to a primitive input value
+ * (such as `String`, `Number`, `Boolean`, or `Symbol`),
+ * or a changed object reference (such as `Date`, `Array`, `Function`, or `Object`).
+ *
+ * Note that mutating a `Date` object does not cause the pipe to be rendered again.
+ * To ensure that the pipe is executed, you must create a new `Date` object.
+ *
+ * Only the `en-US` locale data comes with Angular. To localize dates
+ * in another language, you must import the corresponding locale data.
+ * See the [I18n guide](guide/i18n/format-data-locale) for more information.
+ *
+ * The time zone of the formatted value can be specified either by passing it in as the second
+ * parameter of the pipe, or by setting the default through the `DATE_PIPE_DEFAULT_OPTIONS`
+ * injection token. The value that is passed in as the second parameter takes precedence over
+ * the one defined using the injection token.
+ *
+ * @see {@link formatDate}
+ *
+ *
+ * @usageNotes
+ *
+ * The result of this pipe is not reevaluated when the input is mutated. To avoid the need to
+ * reformat the date on every change-detection cycle, treat the date as an immutable object
+ * and change the reference when the pipe needs to run again.
+ *
+ * ### Pre-defined format options
+ *
+ * | Option | Equivalent to | Examples (given in `en-US` locale) |
+ * |---------------|-------------------------------------|-------------------------------------------------|
+ * | `'short'` | `'M/d/yy, h:mm a'` | `6/15/15, 9:03 AM` |
+ * | `'medium'` | `'MMM d, y, h:mm:ss a'` | `Jun 15, 2015, 9:03:01 AM` |
+ * | `'long'` | `'MMMM d, y, h:mm:ss a z'` | `June 15, 2015 at 9:03:01 AM GMT+1` |
+ * | `'full'` | `'EEEE, MMMM d, y, h:mm:ss a zzzz'` | `Monday, June 15, 2015 at 9:03:01 AM GMT+01:00` |
+ * | `'shortDate'` | `'M/d/yy'` | `6/15/15` |
+ * | `'mediumDate'`| `'MMM d, y'` | `Jun 15, 2015` |
+ * | `'longDate'` | `'MMMM d, y'` | `June 15, 2015` |
+ * | `'fullDate'` | `'EEEE, MMMM d, y'` | `Monday, June 15, 2015` |
+ * | `'shortTime'` | `'h:mm a'` | `9:03 AM` |
+ * | `'mediumTime'`| `'h:mm:ss a'` | `9:03:01 AM` |
+ * | `'longTime'` | `'h:mm:ss a z'` | `9:03:01 AM GMT+1` |
+ * | `'fullTime'` | `'h:mm:ss a zzzz'` | `9:03:01 AM GMT+01:00` |
+ *
+ * ### Custom format options
+ *
+ * You can construct a format string using symbols to specify the components
+ * of a date-time value, as described in the following table.
+ * Format details depend on the locale.
+ * Fields marked with (*) are only available in the extra data set for the given locale.
+ *
+ * | Field type | Format | Description | Example Value |
+ * |-------------------------|-------------|---------------------------------------------------------------|------------------------------------------------------------|
+ * | Era | G, GG & GGG | Abbreviated | AD |
+ * | | GGGG | Wide | Anno Domini |
+ * | | GGGGG | Narrow | A |
+ * | Year | y | Numeric: minimum digits | 2, 20, 201, 2017, 20173 |
+ * | | yy | Numeric: 2 digits + zero padded | 02, 20, 01, 17, 73 |
+ * | | yyy | Numeric: 3 digits + zero padded | 002, 020, 201, 2017, 20173 |
+ * | | yyyy | Numeric: 4 digits or more + zero padded | 0002, 0020, 0201, 2017, 20173 |
+ * | ISO Week-numbering year | Y | Numeric: minimum digits | 2, 20, 201, 2017, 20173 |
+ * | | YY | Numeric: 2 digits + zero padded | 02, 20, 01, 17, 73 |
+ * | | YYY | Numeric: 3 digits + zero padded | 002, 020, 201, 2017, 20173 |
+ * | | YYYY | Numeric: 4 digits or more + zero padded | 0002, 0020, 0201, 2017, 20173 |
+ * | Month | M | Numeric: 1 digit | 9, 12 |
+ * | | MM | Numeric: 2 digits + zero padded | 09, 12 |
+ * | | MMM | Abbreviated | Sep |
+ * | | MMMM | Wide | September |
+ * | | MMMMM | Narrow | S |
+ * | Month standalone | L | Numeric: 1 digit | 9, 12 |
+ * | | LL | Numeric: 2 digits + zero padded | 09, 12 |
+ * | | LLL | Abbreviated | Sep |
+ * | | LLLL | Wide | September |
+ * | | LLLLL | Narrow | S |
+ * | ISO Week of year | w | Numeric: minimum digits | 1... 53 |
+ * | | ww | Numeric: 2 digits + zero padded | 01... 53 |
+ * | Week of month | W | Numeric: 1 digit | 1... 5 |
+ * | Day of month | d | Numeric: minimum digits | 1 |
+ * | | dd | Numeric: 2 digits + zero padded | 01 |
+ * | Week day | E, EE & EEE | Abbreviated | Tue |
+ * | | EEEE | Wide | Tuesday |
+ * | | EEEEE | Narrow | T |
+ * | | EEEEEE | Short | Tu |
+ * | Week day standalone | c, cc | Numeric: 1 digit | 2 |
+ * | | ccc | Abbreviated | Tue |
+ * | | cccc | Wide | Tuesday |
+ * | | ccccc | Narrow | T |
+ * | | cccccc | Short | Tu |
+ * | Period | a, aa & aaa | Abbreviated | am/pm or AM/PM |
+ * | | aaaa | Wide (fallback to `a` when missing) | ante meridiem/post meridiem |
+ * | | aaaaa | Narrow | a/p |
+ * | Period* | B, BB & BBB | Abbreviated | mid. |
+ * | | BBBB | Wide | am, pm, midnight, noon, morning, afternoon, evening, night |
+ * | | BBBBB | Narrow | md |
+ * | Period standalone* | b, bb & bbb | Abbreviated | mid. |
+ * | | bbbb | Wide | am, pm, midnight, noon, morning, afternoon, evening, night |
+ * | | bbbbb | Narrow | md |
+ * | Hour 1-12 | h | Numeric: minimum digits | 1, 12 |
+ * | | hh | Numeric: 2 digits + zero padded | 01, 12 |
+ * | Hour 0-23 | H | Numeric: minimum digits | 0, 23 |
+ * | | HH | Numeric: 2 digits + zero padded | 00, 23 |
+ * | Minute | m | Numeric: minimum digits | 8, 59 |
+ * | | mm | Numeric: 2 digits + zero padded | 08, 59 |
+ * | Second | s | Numeric: minimum digits | 0... 59 |
+ * | | ss | Numeric: 2 digits + zero padded | 00... 59 |
+ * | Fractional seconds | S | Numeric: 1 digit | 0... 9 |
+ * | | SS | Numeric: 2 digits + zero padded | 00... 99 |
+ * | | SSS | Numeric: 3 digits + zero padded (= milliseconds) | 000... 999 |
+ * | Zone | z, zz & zzz | Short specific non location format (fallback to O) | GMT-8 |
+ * | | zzzz | Long specific non location format (fallback to OOOO) | GMT-08:00 |
+ * | | Z, ZZ & ZZZ | ISO8601 basic format | -0800 |
+ * | | ZZZZ | Long localized GMT format | GMT-8:00 |
+ * | | ZZZZZ | ISO8601 extended format + Z indicator for offset 0 (= XXXXX) | -08:00 |
+ * | | O, OO & OOO | Short localized GMT format | GMT-8 |
+ * | | OOOO | Long localized GMT format | GMT-08:00 |
+ *
+ *
+ * ### Format examples
+ *
+ * These examples transform a date into various formats,
+ * assuming that `dateObj` is a JavaScript `Date` object for
+ * year: 2015, month: 6, day: 15, hour: 21, minute: 43, second: 11,
+ * given in the local time for the `en-US` locale.
+ *
+ * ```
+ * {{ dateObj | date }} // output is 'Jun 15, 2015'
+ * {{ dateObj | date:'medium' }} // output is 'Jun 15, 2015, 9:43:11 PM'
+ * {{ dateObj | date:'shortTime' }} // output is '9:43 PM'
+ * {{ dateObj | date:'mm:ss' }} // output is '43:11'
+ * {{ dateObj | date:"MMM dd, yyyy 'at' hh:mm a" }} // output is 'Jun 15, 2015 at 09:43 PM'
+ * ```
+ *
+ * ### Usage example
+ *
+ * The following component uses a date pipe to display the current date in different formats.
+ *
+ * ```angular-ts
+ * @Component({
+ * selector: 'date-pipe',
+ * template: `
+ *
Today is {{today | date}}
+ *
Or if you prefer, {{today | date:'fullDate'}}
+ *
The time is {{today | date:'h:mm a z'}}
+ *
`
+ * })
+ * // Get the current date and time as a date-time value.
+ * export class DatePipeComponent {
+ * today: number = Date.now();
+ * }
+ * ```
+ *
+ * @publicApi
+ */
+declare class DatePipe implements PipeTransform {
+ private locale;
+ private defaultTimezone?;
+ private defaultOptions?;
+ constructor(locale: string, defaultTimezone?: string | null | undefined, defaultOptions?: (DatePipeConfig | null) | undefined);
+ /**
+ * @param value The date expression: a `Date` object, a number
+ * (milliseconds since UTC epoch), or an ISO string (https://www.w3.org/TR/NOTE-datetime).
+ * @param format The date/time components to include, using predefined options or a
+ * custom format string. When not provided, the `DatePipe` looks for the value using the
+ * `DATE_PIPE_DEFAULT_OPTIONS` injection token (and reads the `dateFormat` property).
+ * If the token is not configured, the `mediumDate` is used as a value.
+ * @param timezone A timezone offset (such as `'+0430'`). When not provided, the `DatePipe`
+ * looks for the value using the `DATE_PIPE_DEFAULT_OPTIONS` injection token (and reads
+ * the `timezone` property). If the token is not configured, the end-user's local system
+ * timezone is used as a value.
+ * @param locale A locale code for the locale format rules to use.
+ * When not supplied, uses the value of `LOCALE_ID`, which is `en-US` by default.
+ * See [Setting your app locale](guide/i18n/locale-id).
+ *
+ * @see {@link DATE_PIPE_DEFAULT_OPTIONS}
+ *
+ * @returns A date string in the desired format.
+ */
+ transform(value: Date | string | number, format?: string, timezone?: string, locale?: string): string | null;
+ transform(value: null | undefined, format?: string, timezone?: string, locale?: string): null;
+ transform(value: Date | string | number | null | undefined, format?: string, timezone?: string, locale?: string): string | null;
+ static ɵfac: i0.ɵɵFactoryDeclaration;
+ static ɵpipe: i0.ɵɵPipeDeclaration;
+}
+
+/**
+ * @ngModule CommonModule
+ * @description
+ *
+ * Maps a value to a string that pluralizes the value according to locale rules.
+ *
+ * @usageNotes
+ *
+ * ### Example
+ *
+ * {@example common/pipes/ts/i18n_pipe.ts region='I18nPluralPipeComponent'}
+ *
+ * @publicApi
+ */
+declare class I18nPluralPipe implements PipeTransform {
+ private _localization;
+ constructor(_localization: NgLocalization);
+ /**
+ * @param value the number to be formatted
+ * @param pluralMap an object that mimics the ICU format, see
+ * https://unicode-org.github.io/icu/userguide/format_parse/messages/.
+ * @param locale a `string` defining the locale to use (uses the current {@link LOCALE_ID} by
+ * default).
+ */
+ transform(value: number | null | undefined, pluralMap: {
+ [count: string]: string;
+ }, locale?: string): string;
+ static ɵfac: i0.ɵɵFactoryDeclaration;
+ static ɵpipe: i0.ɵɵPipeDeclaration;
+}
+
+/**
+ * @ngModule CommonModule
+ * @description
+ *
+ * Generic selector that displays the string that matches the current value.
+ *
+ * If none of the keys of the `mapping` match the `value`, then the content
+ * of the `other` key is returned when present, otherwise an empty string is returned.
+ *
+ * @usageNotes
+ *
+ * ### Example
+ *
+ * {@example common/pipes/ts/i18n_pipe.ts region='I18nSelectPipeComponent'}
+ *
+ * @publicApi
+ */
+declare class I18nSelectPipe implements PipeTransform {
+ /**
+ * @param value a string to be internationalized.
+ * @param mapping an object that indicates the text that should be displayed
+ * for different values of the provided `value`.
+ */
+ transform(value: string | null | undefined, mapping: {
+ [key: string]: string;
+ }): string;
+ static ɵfac: i0.ɵɵFactoryDeclaration;
+ static ɵpipe: i0.ɵɵPipeDeclaration;
+}
+
+/**
+ * A key value pair.
+ * Usually used to represent the key value pairs from a Map or Object.
+ *
+ * @publicApi
+ */
+interface KeyValue {
+ key: K;
+ value: V;
+}
+/**
+ * @ngModule CommonModule
+ * @description
+ *
+ * Transforms Object or Map into an array of key value pairs.
+ *
+ * The output array will be ordered by keys.
+ * By default the comparator will be by Unicode point value.
+ * You can optionally pass a compareFn if your keys are complex types.
+ * Passing `null` as the compareFn will use natural ordering of the input.
+ *
+ * @usageNotes
+ * ### Examples
+ *
+ * This examples show how an Object or a Map can be iterated by ngFor with the use of this
+ * keyvalue pipe.
+ *
+ * {@example common/pipes/ts/keyvalue_pipe.ts region='KeyValuePipe'}
+ *
+ * @publicApi
+ */
+declare class KeyValuePipe implements PipeTransform {
+ private readonly differs;
+ constructor(differs: KeyValueDiffers);
+ private differ;
+ private keyValues;
+ private compareFn;
+ transform(input: ReadonlyMap