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
- Location:
libs/component-library/ - Dependencies: None (Framework layer)
- Type: Framework Library
- Tags:
npm:public - Purpose: General-purpose UI components and patterns
🌟 Core Purpose
The Component Library serves as our general UI foundation by providing:
- Basic UI Components - Essential building blocks for user interfaces
- Form Elements - Input controls, buttons, and form management
- Display Components - Data presentation and information display
- Navigation Elements - Links, breadcrumbs, and navigation patterns
- 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
- Accessibility first - Follow WCAG guidelines
- Consistent API - Use similar patterns across components
- Theme integration - Support theming and customization
- Performance optimization - Use OnPush change detection
Implementation Guidelines
- Use ControlValueAccessor - For form controls
- Emit meaningful events - Provide useful event data
- Support slotted content - Use ng-content for flexibility
- Responsive design - Support all screen sizes
🔗 Dependencies and Usage
Used By
- UX Framework - Extended UI component patterns
- Shared UI - Application-specific components
- Editor UI - Editor-specific interfaces
- All Applications - Foundation for all UI development
Integration Points
- Theme System - Styling and theming support
- Form Libraries - Angular Reactive Forms integration
- Accessibility - ARIA support and keyboard navigation
- Testing - Component testing utilities
🌟 Key Benefits
Consistency
- Unified design language across applications
- Standardized behavior for common interactions
- Reusable patterns reduce development time
Accessibility
- WCAG compliant components out of the box
- Keyboard navigation support
- Screen reader friendly with proper ARIA attributes
Developer Experience
- Easy integration with existing applications
- Comprehensive documentation and examples
- Type safety with full TypeScript support
- Flexible customization through props and styling
The Component Library provides the essential building blocks for creating consistent, accessible, and maintainable user interfaces across our entire monorepo.