Tasks

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

Widget System - Implementation Tasks

Roadmap de implementación para @gograb-space/widget-system MVP

📋 Overview

Este documento detalla las tareas de implementación organizadas por fases. El enfoque es iterativo e incremental, permitiendo validación temprana de integración.

Estimación Total MVP: 8-10 días laborables

🎯 Implementation Strategy

Principles

  1. Integration First: Validar integración con tipos existentes desde día 1
  2. Incremental Testing: Tests unitarios en cada fase
  3. Early Validation: Dashboard integration en fase temprana
  4. Documentation Continuous: JSDoc y ejemplos durante desarrollo

Success Metrics


Phase 1: Foundation & Setup

Duración: 1 día Objetivo: Configurar librería e interfaces base

Task 1.1: Library Generation

Prioridad: CRITICAL Tiempo: 30 min

# Generate library
npx nx g @nx/angular:lib widget-system --directory=libs/core --standalone

# Update tsconfig.base.json
"@gograb-space/widget-system": ["libs/core/widget-system/src/index.ts"]

Acceptance Criteria:

Dependencies: None


Task 1.2: Define Core Interfaces

Prioridad: CRITICAL Tiempo: 2 horas

Files to create:

Implementation:

// grid-config.interface.ts
export interface GridConfig {
  columns: number;
  gap: number;
  rowHeight: number | 'auto';
  breakpoints: {
    mobile: GridBreakpointConfig;
    tablet: GridBreakpointConfig;
    desktop: GridBreakpointConfig;
  };
}

export interface GridBreakpointConfig {
  columns: number;
  gap: number;
  rowHeight: number | 'auto';
}

export const DEFAULT_GRID_CONFIG: GridConfig = {
  columns: 12,
  gap: 16,
  rowHeight: 100,
  breakpoints: {
    mobile: { columns: 1, gap: 8, rowHeight: 'auto' },
    tablet: { columns: 6, gap: 12, rowHeight: 100 },
    desktop: { columns: 12, gap: 16, rowHeight: 100 }
  }
};
// widget-state.interface.ts
export interface WidgetState {
  widgetId: string;
  loading: boolean;
  error: Error | null;
  ready: boolean;
  data?: unknown;
}
// widget-lifecycle.interface.ts
export interface WidgetLifecycle {
  onWidgetInit(): void;
  onWidgetDestroy(): void;
  onWidgetConfigChange?(config: ResolvedWidgetConfig): void;
  onWidgetRefresh?(): void;
}

Acceptance Criteria:

Dependencies: None


Task 1.3: Setup Dependencies

Prioridad: HIGH Tiempo: 30 min

Update: libs/core/widget-system/project.json

{
  "implicitDependencies": [
    "module-base",
    "module-config",
    "event-system",
    "log-system",
    "utilities"
  ]
}

Acceptance Criteria:

Dependencies: Task 1.1


Task 1.4: Create Barrel Exports

Prioridad: HIGH Tiempo: 15 min

File: libs/core/widget-system/src/index.ts

// Models
export * from './lib/models/grid-config.interface';
export * from './lib/models/widget-state.interface';
export * from './lib/models/widget-lifecycle.interface';

// Components (to be added in later phases)
export * from './lib/components/widget-grid.component';
export * from './lib/components/widget-container.component';
export * from './lib/components/widget-placeholder.component';
export * from './lib/components/widget-error.component';

// Base Classes
export * from './lib/base/base-widget.component';

// Services
export * from './lib/services/widget-layout.service';
export * from './lib/services/widget-communication.service';

Acceptance Criteria:

Dependencies: Task 1.2


Phase 2: Core UI Components

Duración: 3 días Objetivo: Implementar componentes UI base

Task 2.1: WidgetPlaceholderComponent

Prioridad: HIGH Tiempo: 2 horas

File: libs/core/widget-system/src/lib/components/widget-placeholder/widget-placeholder.component.ts

@Component({
  selector: 'gs-widget-placeholder',
  standalone: true,
  template: `
    <div class="widget-placeholder" aria-busy="true" aria-label="Loading widget">
      <div class="placeholder-animation">
        <div class="placeholder-line"></div>
        <div class="placeholder-line"></div>
        <div class="placeholder-line"></div>
      </div>
    </div>
  `,
  styleUrl: './widget-placeholder.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class WidgetPlaceholderComponent {}

Acceptance Criteria:

Dependencies: Phase 1 complete


Task 2.2: WidgetErrorComponent

Prioridad: HIGH Tiempo: 2 horas

File: libs/core/widget-system/src/lib/components/widget-error/widget-error.component.ts

@Component({
  selector: 'gs-widget-error',
  standalone: true,
  imports: [CommonModule],
  template: `
    <div class="widget-error" role="alert" aria-live="assertive">
      <div class="error-icon">⚠️</div>
      <div class="error-content">
        <h4 class="error-title">{{ widgetName() }} Error</h4>
        <p class="error-message">{{ error().message }}</p>
      </div>
      <button
        class="error-retry"
        type="button"
        (click)="retry.emit()"
        aria-label="Retry loading widget">
        Retry
      </button>
    </div>
  `,
  styleUrl: './widget-error.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class WidgetErrorComponent {
  error = input.required<Error>();
  widgetName = input<string>('Widget');
  retry = output<void>();
}

Acceptance Criteria:

Dependencies: Task 2.1


Task 2.3: WidgetContainerComponent

Prioridad: CRITICAL Tiempo: 4 horas

File: libs/core/widget-system/src/lib/components/widget-container/widget-container.component.ts

@Component({
  selector: 'lib-gs-widget-container',
  standalone: true,
  imports: [
    NgComponentOutlet,
    CommonModule,
    WidgetPlaceholderComponent,
    WidgetErrorComponent
  ],
  templateUrl: './widget-container.component.html',
  styleUrl: './widget-container.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class WidgetContainerComponent {
  widgetConfig = input.required<ResolvedWidgetConfig>();

  private logger = inject(LoggerService).child({ source: 'WidgetContainer' });

  state = signal<WidgetState>({
    widgetId: '',
    loading: false,
    ready: false,
    error: null
  });

  componentInputs = computed(() => ({
    widgetId: this.widgetConfig().id,
    widgetConfig: this.widgetConfig()
  }));

  constructor() {
    effect(() => {
      const config = this.widgetConfig();
      this.state.update(s => ({ ...s, widgetId: config.id }));
    });
  }

  handleRetry(): void {
    this.state.update(s => ({ ...s, error: null, loading: true }));
  }

  onWidgetError(error: Error): void {
    this.logger.error(`Widget error`, { widgetId: this.widgetConfig().id, error });
    this.state.update(s => ({ ...s, error, loading: false }));
  }
}

Acceptance Criteria:

Dependencies: Task 2.1, Task 2.2


Task 2.4: WidgetGridComponent (Basic)

Prioridad: CRITICAL Tiempo: 6 hours

File: libs/core/widget-system/src/lib/components/widget-grid/widget-grid.component.ts

@Component({
  selector: 'lib-gs-widget-grid',
  standalone: true,
  imports: [WidgetContainerComponent, CommonModule],
  templateUrl: './widget-grid.component.html',
  styleUrl: './widget-grid.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class WidgetGridComponent implements OnInit {
  widgets = input.required<ResolvedWidgetConfig[]>();
  gridConfig = input<GridConfig>(DEFAULT_GRID_CONFIG);

  private breakpointObserver = inject(BreakpointObserver);
  private destroyRef = inject(DestroyRef);

  private currentBreakpoint = signal<'mobile' | 'tablet' | 'desktop'>('desktop');

  enabledWidgets = computed(() =>
    this.widgets().filter(w => w.enabled)
  );

  gridColumns = computed(() => {
    const bp = this.currentBreakpoint();
    return this.gridConfig().breakpoints[bp].columns;
  });

  rowHeight = computed(() => {
    const bp = this.currentBreakpoint();
    const height = this.gridConfig().breakpoints[bp].rowHeight;
    return typeof height === 'number' ? `${height}px` : 'auto';
  });

  ngOnInit(): void {
    this.setupResponsiveBreakpoints();
  }

  private setupResponsiveBreakpoints(): void {
    this.breakpointObserver
      .observe([
        Breakpoints.XSmall,
        Breakpoints.Small,
        Breakpoints.Medium,
        Breakpoints.Large
      ])
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(result => {
        if (result.breakpoints[Breakpoints.XSmall] || result.breakpoints[Breakpoints.Small]) {
          this.currentBreakpoint.set('mobile');
        } else if (result.breakpoints[Breakpoints.Medium]) {
          this.currentBreakpoint.set('tablet');
        } else {
          this.currentBreakpoint.set('desktop');
        }
      });
  }

  getGridColumn(position: ResolvedWidgetConfig['position']): string {
    return `${position.x + 1} / span ${position.width}`;
  }

  getGridRow(position: ResolvedWidgetConfig['position']): string {
    return `${position.y + 1} / span ${position.height}`;
  }
}

Template:

<div class="widget-grid"
     [style.--grid-columns]="gridColumns()"
     [style.--grid-gap]="gridConfig().gap + 'px'"
     [style.--row-height]="rowHeight()">

  @for (widget of enabledWidgets(); track widget.id) {
    <lib-gs-widget-container
      [widgetConfig]="widget"
      [style.grid-column]="getGridColumn(widget.position)"
      [style.grid-row]="getGridRow(widget.position)" />
  }
</div>

Acceptance Criteria:

Dependencies: Task 2.3


Phase 3: Base Widget Component & Services

Duración: 2 días Objetivo: Implementar clase base opcional y servicios

Task 3.1: BaseWidgetComponent

Prioridad: MEDIUM Tiempo: 3 horas

File: libs/core/widget-system/src/lib/base/base-widget.component.ts

@Component({
  standalone: true,
  template: ''
})
export abstract class BaseWidgetComponent
  implements WidgetLifecycle, OnInit, OnDestroy {

  widgetId = input.required<string>();
  widgetConfig = input<ResolvedWidgetConfig>();

  widgetError = output<Error>();
  widgetReady = output<void>();

  protected eventBus = inject(EventBusService);
  protected logger = inject(LoggerService);
  protected destroyRef = inject(DestroyRef);

  protected loading = signal(false);
  protected error = signal<Error | null>(null);

  ngOnInit(): void {
    this.logger.debug(`Widget ${this.widgetId()} initializing`);
    this.onWidgetInit();
  }

  ngOnDestroy(): void {
    this.logger.debug(`Widget ${this.widgetId()} destroying`);
    this.onWidgetDestroy();
  }

  abstract onWidgetInit(): void;
  abstract onWidgetDestroy(): void;

  protected emitWidgetEvent<T = unknown>(eventType: string, data?: T): void {
    this.eventBus.emit({
      type: `widget.${eventType}`,
      payload: data,
      metadata: {
        widgetId: this.widgetId(),
        timestamp: Date.now()
      }
    });
  }

  protected subscribeToWidgetEvent<T = unknown>(
    eventType: string,
    handler: (data: T) => void
  ): void {
    this.eventBus.on(`widget.${eventType}`)
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(event => handler(event.payload as T));
  }

  protected reportError(error: Error): void {
    this.error.set(error);
    this.widgetError.emit(error);
    this.logger.error(`Widget ${this.widgetId()} error`, error);
  }

  protected markReady(): void {
    this.widgetReady.emit();
    this.logger.debug(`Widget ${this.widgetId()} ready`);
  }
}

Acceptance Criteria:

Dependencies: Phase 2 complete


Task 3.2: WidgetLayoutService

Prioridad: MEDIUM Tiempo: 2 horas

File: libs/core/widget-system/src/lib/services/widget-layout.service.ts

@Injectable({ providedIn: 'root' })
export class WidgetLayoutService {
  private logger = inject(LoggerService).child({ source: 'WidgetLayoutService' });

  validatePlacement(
    placement: ResolvedWidgetConfig['position'],
    gridConfig: GridConfig
  ): boolean {
    const { x, y, width, height } = placement;

    if (x < 0 || y < 0 || width <= 0 || height <= 0) {
      return false;
    }

    if (x + width > gridConfig.columns) {
      this.logger.warn('Widget placement exceeds grid columns', {
        placement,
        gridColumns: gridConfig.columns
      });
      return false;
    }

    return true;
  }

  calculateGridStyles(position: ResolvedWidgetConfig['position']): {
    gridColumn: string;
    gridRow: string;
  } {
    return {
      gridColumn: `${position.x + 1} / span ${position.width}`,
      gridRow: `${position.y + 1} / span ${position.height}`
    };
  }
}

Acceptance Criteria:

Dependencies: Task 3.1


Task 3.3: WidgetCommunicationService

Prioridad: LOW Tiempo: 2 horas

File: libs/core/widget-system/src/lib/services/widget-communication.service.ts

@Injectable({ providedIn: 'root' })
export class WidgetCommunicationService {
  private eventBus = inject(EventBusService);
  private logger = inject(LoggerService).child({ source: 'WidgetCommunication' });

  emitFromWidget<T = unknown>(
    widgetId: string,
    eventType: string,
    data?: T
  ): void {
    this.eventBus.emit({
      type: `widget.${eventType}`,
      payload: data,
      metadata: {
        source: 'widget',
        widgetId,
        timestamp: Date.now()
      }
    });
  }

  onWidgetEvent<T = unknown>(
    eventType: string,
    handler: (data: T, widgetId: string) => void
  ): Observable<void> {
    return this.eventBus.on(`widget.${eventType}`).pipe(
      map(event => {
        handler(
          event.payload as T,
          event.metadata?.widgetId as string
        );
      })
    );
  }
}

Acceptance Criteria:

Dependencies: Task 3.2


Phase 4: Dashboard Integration

Duración: 1 día Objetivo: Integrar en dashboard real

Task 4.1: Update Dashboard Component

Prioridad: CRITICAL Tiempo: 2 horas

File: apps/gograb-admin/src/app/features/dashboard/dashboard.component.ts

Changes:

// Add import
import { WidgetGridComponent } from '@libs-core/widget-system';

@Component({
  // Add to imports
  imports: [
    // ... existing imports
    WidgetGridComponent
  ]
})
export class DashboardComponent {
  // Add grid config
  gridConfig: GridConfig = {
    columns: 12,
    gap: 16,
    rowHeight: 100,
    breakpoints: {
      mobile: { columns: 1, gap: 8, rowHeight: 'auto' },
      tablet: { columns: 6, gap: 12, rowHeight: 100 },
      desktop: { columns: 12, gap: 16, rowHeight: 100 }
    }
  };

  // enabledWidgets already exists - no changes needed
}

Template Changes:

<!-- BEFORE -->
<div class="dashboard-widgets">
  @for (widget of enabledWidgets(); track widget.id) {
    <div class="widget-container">
      <ng-container *ngComponentOutlet="widget.componentType" />
    </div>
  }
</div>

<!-- AFTER -->
<lib-gs-widget-grid
  [widgets]="enabledWidgets()"
  [gridConfig]="gridConfig" />

Acceptance Criteria:

Dependencies: Phase 3 complete


Task 4.2: Integration Testing

Prioridad: HIGH Tiempo: 3 horas

File: apps/gograb-admin/src/app/features/dashboard/dashboard.component.integration.spec.ts

Tests:

describe('Dashboard Widget Grid Integration', () => {
  it('should render widgets from AppConfigService', () => {
    // Test integration with AppConfigService
  });

  it('should handle widget errors without crashing', () => {
    // Test error boundary
  });

  it('should adapt to responsive breakpoints', () => {
    // Test responsive behavior
  });

  it('should communicate between widgets via events', () => {
    // Test widget communication
  });
});

Acceptance Criteria:

Dependencies: Task 4.1


Phase 5: Documentation & Polish

Duración: 1 día Objetivo: Finalizar documentación y pulir implementación

Task 5.1: JSDoc Documentation

Prioridad: MEDIUM Tiempo: 3 horas

Files: All components, services, interfaces

Example:

/**
 * Grid layout component for rendering widgets in a responsive grid.
 *
 * @example
 * ```typescript
 * <lib-gs-widget-grid
 *   [widgets]="enabledWidgets()"
 *   [gridConfig]="gridConfig" />
 * ```
 *
 * @see {@link WidgetContainerComponent}
 * @see {@link GridConfig}
 */
@Component({...})
export class WidgetGridComponent {...}

Acceptance Criteria:

Dependencies: Phase 4 complete


Task 5.2: README Examples

Prioridad: MEDIUM Tiempo: 2 horas

File: libs/core/widget-system/README.md

Content:

Acceptance Criteria:

Dependencies: Task 5.1


Task 5.3: Styling Polish

Prioridad: LOW Tiempo: 2 horas

Tasks:

Acceptance Criteria:

Dependencies: Task 5.2


Phase 6: Testing & Quality Assurance

Duración: 1 día Objetivo: Garantizar calidad y cobertura

Task 6.1: Unit Test Completion

Prioridad: HIGH Tiempo: 4 horas

Coverage Goals:

Acceptance Criteria:

Dependencies: Phase 5 complete


Task 6.2: Integration Test Suite

Prioridad: HIGH Tiempo: 3 horas

Tests:

Acceptance Criteria:

Dependencies: Task 6.1


Task 6.3: Manual QA

Prioridad: MEDIUM Tiempo: 2 horas

Test Cases:

Acceptance Criteria:

Dependencies: Task 6.2


Post-MVP Tasks (Future)

Feature: Drag & Drop

Prioridad: LOW (Post-MVP) Tiempo: 3 días


Feature: Widget Resize

Prioridad: LOW (Post-MVP) Tiempo: 2 días


Feature: Widget Maximize

Prioridad: LOW (Post-MVP) Tiempo: 1 día


Feature: Lazy Loading

Prioridad: LOW (Post-MVP) Tiempo: 2 días


📊 Task Summary

By Phase

Phase Tasks Estimated Time Priority
Phase 1: Foundation 4 1 día CRITICAL
Phase 2: Core Components 4 3 días CRITICAL
Phase 3: Base & Services 3 2 días MEDIUM
Phase 4: Integration 2 1 día CRITICAL
Phase 5: Documentation 3 1 día MEDIUM
Phase 6: QA 3 1 día HIGH
TOTAL MVP 19 9 días -

By Priority

Priority Tasks Time
CRITICAL 8 5 días
HIGH 4 2 días
MEDIUM 6 2 días
LOW 1 < 1 día

✅ Definition of Done

Per Task

Per Phase

For MVP


🚀 Deployment Checklist


📝 Notes