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-systemMVP
📋 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
- Integration First: Validar integración con tipos existentes desde día 1
- Incremental Testing: Tests unitarios en cada fase
- Early Validation: Dashboard integration en fase temprana
- Documentation Continuous: JSDoc y ejemplos durante desarrollo
Success Metrics
- ✅ Zero breaking changes en código existente
- ✅ Test coverage > 100%
- ✅ Dashboard renderiza widgets en grid
- ✅ Responsive layout funcional
- ✅ Error boundaries aíslan fallos
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:
- ✅ Library generates successfully
- ✅ Path mapping in tsconfig.base.json
- ✅ Can import from library in tests
Dependencies: None
Task 1.2: Define Core Interfaces
Prioridad: CRITICAL Tiempo: 2 horas
Files to create:
libs/core/widget-system/src/lib/models/grid-config.interface.tslibs/core/widget-system/src/lib/models/widget-state.interface.tslibs/core/widget-system/src/lib/models/widget-lifecycle.interface.tslibs/core/widget-system/src/lib/models/index.ts
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:
- ✅ All interfaces defined with JSDoc
- ✅ Default config exported
- ✅ Barrel export in index.ts
- ✅ No TypeScript errors
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 declared
- ✅ Nx dependency graph shows correct links
- ✅ Can import from dependency libraries
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:
- ✅ Barrel exports structured
- ✅ Exports commented for clarity
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:
- ✅ Skeleton UI with animation
- ✅ Accessible (aria-busy, aria-label)
- ✅ Responsive styling
- ✅ Unit tests
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:
- ✅ Error display with retry
- ✅ Accessible (role, aria-live)
- ✅ Retry emits event
- ✅ Unit tests
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:
- ✅ Error boundary functional
- ✅ Loading states work
- ✅ NgComponentOutlet renders widget
- ✅ Inputs passed correctly
- ✅ Unit tests > 90%
- ✅ Integration test with mock widget
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:
- ✅ Grid renders widgets correctly
- ✅ Positioning based on ResolvedWidgetConfig.position
- ✅ Responsive breakpoints work
- ✅ CSS Grid layout functional
- ✅ Unit tests > 90%
- ✅ Responsive tests (mobile/tablet/desktop)
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:
- ✅ Abstract class compiles
- ✅ Helper methods functional
- ✅ Event integration works
- ✅ Logger integration works
- ✅ Unit tests with mock implementation
- ✅ Example widget using base class
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:
- ✅ Validation logic works
- ✅ Grid style calculation correct
- ✅ Edge cases handled
- ✅ Unit tests > 95%
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:
- ✅ Event emission works
- ✅ Event subscription works
- ✅ Namespace convention followed
- ✅ Unit tests
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:
- ✅ Dashboard compiles without errors
- ✅ Widgets render in grid
- ✅ Positioning works correctly
- ✅ Responsive layout functional
- ✅ No regressions in existing functionality
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:
- ✅ Integration tests pass
- ✅ End-to-end widget rendering validated
- ✅ Error scenarios covered
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:
- ✅ All public APIs documented
- ✅ Examples provided
- ✅ @see links to related components
Dependencies: Phase 4 complete
Task 5.2: README Examples
Prioridad: MEDIUM Tiempo: 2 horas
File: libs/core/widget-system/README.md
Content:
- Quick start guide
- Usage examples
- API reference
- Integration patterns
- Migration guide (if needed)
Acceptance Criteria:
- ✅ README complete
- ✅ Examples runnable
- ✅ Clear and concise
Dependencies: Task 5.1
Task 5.3: Styling Polish
Prioridad: LOW Tiempo: 2 horas
Tasks:
- Refine widget-grid styles
- Refine widget-container styles
- Ensure design token usage
- Responsive polish
- Accessibility review
Acceptance Criteria:
- ✅ Styles consistent with ui-framework
- ✅ No custom tokens defined
- ✅ Accessible (WCAG 2.1 AA)
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:
- WidgetGridComponent: > 90%
- WidgetContainerComponent: > 90%
- WidgetLayoutService: > 95%
- BaseWidgetComponent: > 85%
Acceptance Criteria:
- ✅ Overall coverage > 90%
- ✅ Edge cases covered
- ✅ Error scenarios tested
Dependencies: Phase 5 complete
Task 6.2: Integration Test Suite
Prioridad: HIGH Tiempo: 3 horas
Tests:
- Dashboard integration
- AppConfigService integration
- EventBusService integration
- ComponentRegistryService integration
Acceptance Criteria:
- ✅ All integration points tested
- ✅ No integration issues
Dependencies: Task 6.1
Task 6.3: Manual QA
Prioridad: MEDIUM Tiempo: 2 horas
Test Cases:
- Dashboard loads with widgets
- Grid responsive on mobile/tablet/desktop
- Widget error doesn't crash dashboard
- Loading states show correctly
- Widget communication works
- Keyboard navigation functional
- Screen reader accessible
Acceptance Criteria:
- ✅ All manual tests pass
- ✅ No critical bugs found
Dependencies: Task 6.2
Post-MVP Tasks (Future)
Feature: Drag & Drop
Prioridad: LOW (Post-MVP) Tiempo: 3 días
- Integrate Angular CDK Drag-Drop
- Widget rearrangement
- Position persistence
- Collision detection
Feature: Widget Resize
Prioridad: LOW (Post-MVP) Tiempo: 2 días
- Resize handles
- Min/max size constraints
- Grid snap behavior
- Size persistence
Feature: Widget Maximize
Prioridad: LOW (Post-MVP) Tiempo: 1 día
- Fullscreen mode
- Escape key to restore
- Z-index management
Feature: Lazy Loading
Prioridad: LOW (Post-MVP) Tiempo: 2 días
- Dynamic widget import
- Code splitting
- Loading optimization
📊 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
- ✅ Code implemented following Angular 20 best practices
- ✅ Unit tests written with > 90% coverage
- ✅ JSDoc documentation complete
- ✅ Code review passed
- ✅ No TypeScript errors
- ✅ No console errors/warnings
- ✅ Accessibility validated
Per Phase
- ✅ All phase tasks complete
- ✅ Integration tests pass
- ✅ Documentation updated
- ✅ Demo working (if applicable)
For MVP
- ✅ All critical/high tasks complete
- ✅ Dashboard integration functional
- ✅ Test coverage > 90%
- ✅ Zero breaking changes
- ✅ Documentation complete
- ✅ Manual QA passed
🚀 Deployment Checklist
- All tests passing (unit + integration)
- Code reviewed and approved
- Documentation complete
- CHANGELOG updated
- Version bumped
- Library built successfully
- Dashboard integration validated
- Performance benchmarks met
- Accessibility audit passed
- No breaking changes
- Migration guide (if needed)
📝 Notes
- Agile Approach: Tasks can be reordered based on feedback
- Early Integration: Dashboard integration in Phase 4 provides early validation
- Incremental Testing: Tests written alongside implementation
- Documentation Continuous: JSDoc during development, finalized in Phase 5
- Post-MVP Planning: Drag-and-drop and other features planned but not in MVP scope