Component Library

Ecosistema completo para organización y gestión de contenido con interfaces espaciales

Component Library

The Component Library provides a comprehensive collection of general-purpose UI components for building modern web applications. It offers reusable, accessible, and customizable components that form the foundation of our user interfaces.

📍 Library Details

🌟 Core Purpose

The Component Library serves as our general UI foundation by providing:

  1. Basic UI Components - Essential building blocks for user interfaces
  2. Form Elements - Input controls, buttons, and form management
  3. Display Components - Data presentation and information display
  4. Navigation Elements - Links, breadcrumbs, and navigation patterns
  5. Utility Components - Helper components for common UI patterns

🏗️ Architecture Position

Component Library sits in the Framework Libraries layer of our dependency hierarchy:

Feature Libraries (widgets, editor-ui, shared-ui, auth, etc.)
    ↑ depends on  
Framework Libraries (ux-framework, theme-system, component-library, etc.) ← You are here
    ↑ depends on
Core Libraries (permissions)
    ↑ depends on
Foundation Libraries (core-types, core)

🧩 Key Components

Button Component

Versatile button component with multiple variants and states:

import type { SafeAny } from '@libs-foundational/core-models'; 

@Component({
  selector: 'cl-button',
  standalone: true,
  template: `
    <button 
      [type]="type"
      [disabled]="disabled"
      [class]="buttonClasses"
      (click)="handleClick($event)">
      <cl-icon 
        *ngIf="icon" 
        [name]="icon" 
        [size]="iconSize">
      </cl-icon>
      <span *ngIf="label" class="button-label">{{ label }}</span>
      <ng-content></ng-content>
      <cl-spinner 
        *ngIf="loading" 
        class="button-spinner"
        [size]="'sm'">
      </cl-spinner>
    </button>
  `,
  styleUrls: ['./button.component.scss']
})
export class ButtonComponent {
  @Input() label?: string;
  @Input() type: 'button' | 'submit' | 'reset' = 'button';
  @Input() variant: ButtonVariant = 'primary';
  @Input() size: ButtonSize = 'md';
  @Input() disabled = false;
  @Input() loading = false;
  @Input() icon?: string;
  @Input() iconPosition: 'left' | 'right' = 'left';
  @Input() fullWidth = false;
  @Output() clickEvent = new EventEmitter<MouseEvent>();
  
  get buttonClasses(): string {
    return [
      'cl-button',
      `cl-button--${this.variant}`,
      `cl-button--${this.size}`,
      this.disabled ? 'cl-button--disabled' : '',
      this.loading ? 'cl-button--loading' : '',
      this.fullWidth ? 'cl-button--full-width' : '',
      this.icon && !this.label ? 'cl-button--icon-only' : ''
    ].filter(Boolean).join(' ');
  }
  
  get iconSize(): string {
    const sizeMap = {
      'sm': 'xs',
      'md': 'sm',
      'lg': 'md'
    };
    return sizeMap[this.size];
  }
  
  handleClick(event: MouseEvent): void {
    if (!this.disabled && !this.loading) {
      this.clickEvent.emit(event);
    }
  }
}

type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger' | 'success';
type ButtonSize = 'sm' | 'md' | 'lg';

Input Component

Flexible input component with validation and states:

@Component({
  selector: 'cl-input',
  standalone: true,
  imports: [CommonModule, FormsModule],
  template: `
    <div class="cl-input-wrapper">
      <label 
        *ngIf="label" 
        [for]="inputId" 
        class="cl-input-label"
        [class.required]="required">
        {{ label }}
      </label>
      
      <div class="cl-input-container" [class]="containerClasses">
        <cl-icon 
          *ngIf="prefixIcon" 
          [name]="prefixIcon" 
          class="cl-input-prefix-icon">
        </cl-icon>
        
        <input 
          [id]="inputId"
          [type]="type"
          [placeholder]="placeholder"
          [disabled]="disabled"
          [readonly]="readonly"
          [required]="required"
          [value]="value"
          [class]="inputClasses"
          (input)="onInput($event)"
          (blur)="onBlur($event)"
          (focus)="onFocus($event)"
          #inputElement>
        
        <cl-icon 
          *ngIf="suffixIcon" 
          [name]="suffixIcon" 
          class="cl-input-suffix-icon"
          (click)="onSuffixIconClick($event)">
        </cl-icon>
        
        <button 
          *ngIf="clearable && value && !disabled"
          type="button"
          class="cl-input-clear"
          (click)="clearValue()">
          <cl-icon name="x" size="xs"></cl-icon>
        </button>
      </div>
      
      <div 
        *ngIf="helpText || errorMessage" 
        class="cl-input-help"
        [class.error]="hasError">
        {{ errorMessage || helpText }}
      </div>
    </div>
  `,
  styleUrls: ['./input.component.scss']
})
export class InputComponent implements ControlValueAccessor {
  @Input() label?: string;
  @Input() type: InputType = 'text';
  @Input() placeholder?: string;
  @Input() helpText?: string;
  @Input() errorMessage?: string;
  @Input() disabled = false;
  @Input() readonly = false;
  @Input() required = false;
  @Input() clearable = false;
  @Input() prefixIcon?: string;
  @Input() suffixIcon?: string;
  @Input() size: InputSize = 'md';
  @Input() variant: InputVariant = 'default';
  
  @Output() inputEvent = new EventEmitter<string>();
  @Output() blurEvent = new EventEmitter<Event>();
  @Output() focusEvent = new EventEmitter<Event>();
  @Output() suffixIconClick = new EventEmitter<Event>();
  
  @ViewChild('inputElement') inputElement!: ElementRef<HTMLInputElement>;
  
  value = '';
  inputId = `cl-input-${Math.random().toString(36).substr(2, 9)}`;
  
  // ControlValueAccessor implementation
  private onChange: (value: SafeAny) => void = () => {};
  private onTouched: () => void = () => {};
  
  get hasError(): boolean {
    return !!this.errorMessage;
  }
  
  get containerClasses(): string {
    return [
      'cl-input-container',
      `cl-input-container--${this.size}`,
      `cl-input-container--${this.variant}`,
      this.disabled ? 'cl-input-container--disabled' : '',
      this.hasError ? 'cl-input-container--error' : '',
      this.prefixIcon ? 'cl-input-container--has-prefix' : '',
      this.suffixIcon || this.clearable ? 'cl-input-container--has-suffix' : ''
    ].filter(Boolean).join(' ');
  }
  
  get inputClasses(): string {
    return [
      'cl-input',
      `cl-input--${this.size}`,
      this.readonly ? 'cl-input--readonly' : ''
    ].filter(Boolean).join(' ');
  }
  
  onInput(event: Event): void {
    const target = event.target as HTMLInputElement;
    this.value = target.value;
    this.onChange(this.value);
    this.inputEvent.emit(this.value);
  }
  
  onBlur(event: Event): void {
    this.onTouched();
    this.blurEvent.emit(event);
  }
  
  onFocus(event: Event): void {
    this.focusEvent.emit(event);
  }
  
  onSuffixIconClick(event: Event): void {
    this.suffixIconClick.emit(event);
  }
  
  clearValue(): void {
    this.value = '';
    this.onChange(this.value);
    this.inputEvent.emit(this.value);
    this.inputElement.nativeElement.focus();
  }
  
  // ControlValueAccessor methods
  writeValue(value: SafeAny): void {
    this.value = value || '';
  }
  
  registerOnChange(fn: (value: SafeAny) => void): void {
    this.onChange = fn;
  }
  
  registerOnTouched(fn: () => void): void {
    this.onTouched = fn;
  }
  
  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }
}

type InputType = 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'search';
type InputSize = 'sm' | 'md' | 'lg';
type InputVariant = 'default' | 'filled' | 'outlined';

Card Component

Flexible card component for content organization:

@Component({
  selector: 'cl-card',
  standalone: true,
  template: `
    <div [class]="cardClasses">
      <div *ngIf="hasHeader" class="cl-card-header">
        <div class="cl-card-title" *ngIf="title">
          <h3>{{ title }}</h3>
          <p *ngIf="subtitle" class="cl-card-subtitle">{{ subtitle }}</p>
        </div>
        <div class="cl-card-actions" *ngIf="hasHeaderActions">
          <ng-content select="[slot=header-actions]"></ng-content>
        </div>
      </div>
      
      <div class="cl-card-content" [class.padded]="padded">
        <ng-content></ng-content>
      </div>
      
      <div *ngIf="hasFooter" class="cl-card-footer">
        <ng-content select="[slot=footer]"></ng-content>
      </div>
    </div>
  `,
  styleUrls: ['./card.component.scss']
})
export class CardComponent implements AfterContentInit {
  @Input() title?: string;
  @Input() subtitle?: string;
  @Input() variant: CardVariant = 'default';
  @Input() elevation: CardElevation = 'md';
  @Input() padded = true;
  @Input() hoverable = false;
  @Input() clickable = false;
  
  @Output() cardClick = new EventEmitter<MouseEvent>();
  
  @ContentChildren('[slot=header-actions]') headerActions!: QueryList<ElementRef>;
  @ContentChildren('[slot=footer]') footerContent!: QueryList<ElementRef>;
  
  hasHeader = false;
  hasHeaderActions = false;
  hasFooter = false;
  
  ngAfterContentInit(): void {
    this.hasHeader = !!this.title || this.headerActions.length > 0;
    this.hasHeaderActions = this.headerActions.length > 0;
    this.hasFooter = this.footerContent.length > 0;
  }
  
  get cardClasses(): string {
    return [
      'cl-card',
      `cl-card--${this.variant}`,
      `cl-card--elevation-${this.elevation}`,
      this.hoverable ? 'cl-card--hoverable' : '',
      this.clickable ? 'cl-card--clickable' : ''
    ].filter(Boolean).join(' ');
  }
  
  onCardClick(event: MouseEvent): void {
    if (this.clickable) {
      this.cardClick.emit(event);
    }
  }
}

type CardVariant = 'default' | 'outlined' | 'filled';
type CardElevation = 'none' | 'sm' | 'md' | 'lg' | 'xl';

Modal Component

Accessible modal dialog component:

@Component({
  selector: 'cl-modal',
  standalone: true,
  imports: [CommonModule],
  template: `
    <div 
      *ngIf="isOpen" 
      class="cl-modal-overlay"
      [class]="overlayClasses"
      (click)="onOverlayClick($event)"
      role="dialog"
      [attr.aria-modal]="true"
      [attr.aria-labelledby]="titleId"
      [attr.aria-describedby]="hasDescription ? descriptionId : null">
      
      <div 
        class="cl-modal-container"
        [class]="containerClasses"
        (click)="$event.stopPropagation()">
        
        <div *ngIf="hasHeader" class="cl-modal-header">
          <h2 [id]="titleId" class="cl-modal-title">{{ title }}</h2>
          <button 
            *ngIf="closable"
            type="button" 
            class="cl-modal-close"
            (click)="close()"
            aria-label="Close modal">
            <cl-icon name="x"></cl-icon>
          </button>
        </div>
        
        <div 
          class="cl-modal-content"
          [id]="descriptionId">
          <ng-content></ng-content>
        </div>
        
        <div *ngIf="hasFooter" class="cl-modal-footer">
          <ng-content select="[slot=footer]"></ng-content>
        </div>
      </div>
    </div>
  `,
  styleUrls: ['./modal.component.scss']
})
export class ModalComponent implements OnInit, OnDestroy, AfterContentInit {
  @Input() title?: string;
  @Input() isOpen = false;
  @Input() closable = true;
  @Input() closeOnOverlayClick = true;
  @Input() closeOnEscape = true;
  @Input() size: ModalSize = 'md';
  @Input() centered = true;
  
  @Output() openChange = new EventEmitter<boolean>();
  @Output() beforeClose = new EventEmitter<void>();
  @Output() afterClose = new EventEmitter<void>();
  
  @ContentChildren('[slot=footer]') footerContent!: QueryList<ElementRef>;
  
  titleId = `cl-modal-title-${Math.random().toString(36).substr(2, 9)}`;
  descriptionId = `cl-modal-description-${Math.random().toString(36).substr(2, 9)}`;
  
  hasHeader = false;
  hasFooter = false;
  hasDescription = false;
  
  private keydownListener?: () => void;
  
  ngOnInit(): void {
    if (this.closeOnEscape) {
      this.keydownListener = this.onKeydown.bind(this);
      document.addEventListener('keydown', this.keydownListener);
    }
  }
  
  ngOnDestroy(): void {
    if (this.keydownListener) {
      document.removeEventListener('keydown', this.keydownListener);
    }
  }
  
  ngAfterContentInit(): void {
    this.hasHeader = !!this.title;
    this.hasFooter = this.footerContent.length > 0;
    this.hasDescription = true; // Assume content provides description
  }
  
  get overlayClasses(): string {
    return [
      'cl-modal-overlay',
      this.centered ? 'cl-modal-overlay--centered' : ''
    ].filter(Boolean).join(' ');
  }
  
  get containerClasses(): string {
    return [
      'cl-modal-container',
      `cl-modal-container--${this.size}`
    ].filter(Boolean).join(' ');
  }
  
  open(): void {
    this.isOpen = true;
    this.openChange.emit(true);
    document.body.classList.add('cl-modal-open');
  }
  
  close(): void {
    this.beforeClose.emit();
    this.isOpen = false;
    this.openChange.emit(false);
    document.body.classList.remove('cl-modal-open');
    this.afterClose.emit();
  }
  
  onOverlayClick(event: MouseEvent): void {
    if (this.closeOnOverlayClick) {
      this.close();
    }
  }
  
  private onKeydown(event: KeyboardEvent): void {
    if (event.key === 'Escape' && this.isOpen) {
      this.close();
    }
  }
}

type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'full';

📦 Common Usage Patterns

Form Example

@Component({
  selector: 'app-user-form',
  standalone: true,
  imports: [CommonModule, ReactiveFormsModule, ComponentLibraryModule],
  template: `
    <cl-card title="User Information" padded="true">
      <form [formGroup]="userForm" (ngSubmit)="onSubmit()">
        <div class="form-grid">
          <cl-input
            label="First Name"
            placeholder="Enter first name"
            formControlName="firstName"
            [errorMessage]="getFieldError('firstName')"
            required>
          </cl-input>
          
          <cl-input
            label="Last Name"
            placeholder="Enter last name"
            formControlName="lastName"
            [errorMessage]="getFieldError('lastName')"
            required>
          </cl-input>
          
          <cl-input
            label="Email"
            type="email"
            placeholder="Enter email address"
            formControlName="email"
            [errorMessage]="getFieldError('email')"
            prefixIcon="mail"
            required>
          </cl-input>
          
          <cl-input
            label="Phone"
            type="tel"
            placeholder="Enter phone number"
            formControlName="phone"
            [errorMessage]="getFieldError('phone')"
            prefixIcon="phone">
          </cl-input>
        </div>
        
        <div slot="footer" class="form-actions">
          <cl-button 
            type="button" 
            variant="secondary" 
            (clickEvent)="onCancel()">
            Cancel
          </cl-button>
          <cl-button 
            type="submit" 
            variant="primary"
            [disabled]="userForm.invalid"
            [loading]="isSubmitting">
            Save User
          </cl-button>
        </div>
      </form>
    </cl-card>
  `
})
export class UserFormComponent {
  userForm = this.fb.group({
    firstName: ['', [Validators.required, Validators.minLength(2)]],
    lastName: ['', [Validators.required, Validators.minLength(2)]],
    email: ['', [Validators.required, Validators.email]],
    phone: ['', [Validators.pattern(/^\+?[\d\s-()]+$/)]]
  });
  
  isSubmitting = false;
  
  constructor(private fb: FormBuilder) {}
  
  getFieldError(fieldName: string): string | null {
    const field = this.userForm.get(fieldName);
    if (field?.errors && field.touched) {
      if (field.errors['required']) return `${fieldName} is required`;
      if (field.errors['email']) return 'Invalid email address';
      if (field.errors['minlength']) return `Minimum ${field.errors['minlength'].requiredLength} characters`;
      if (field.errors['pattern']) return 'Invalid format';
    }
    return null;
  }
  
  onSubmit(): void {
    if (this.userForm.valid) {
      this.isSubmitting = true;
      // Handle form submission
    }
  }
  
  onCancel(): void {
    this.userForm.reset();
  }
}

🛠️ Development

Building the Library

# Build component-library
npx nx build component-library

# Build with watch mode
npx nx build component-library --watch

Testing

# Run tests
npx nx test component-library

# Run tests with coverage
npx nx test component-library --coverage

# Run linting
npx nx lint component-library

Integration

// Proper import patterns
import { 
  ButtonComponent, 
  InputComponent, 
  CardComponent, 
  ModalComponent 
} from '@component-library';

// Use in your application

📋 Best Practices

Component Design

  1. Accessibility first - Follow WCAG guidelines
  2. Consistent API - Use similar patterns across components
  3. Theme integration - Support theming and customization
  4. Performance optimization - Use OnPush change detection

Implementation Guidelines

  1. Use ControlValueAccessor - For form controls
  2. Emit meaningful events - Provide useful event data
  3. Support slotted content - Use ng-content for flexibility
  4. Responsive design - Support all screen sizes

🔗 Dependencies and Usage

Used By

Integration Points

🌟 Key Benefits

Consistency

Accessibility

Developer Experience

The Component Library provides the essential building blocks for creating consistent, accessible, and maintainable user interfaces across our entire monorepo.