Implementation Plan
Ecosistema completo para organización y gestión de contenido con interfaces espaciales
Responsive Implementation Plan for Widget Editor
Overview
This plan details the process of implementing complete responsiveness for the Angular 20-based widget editor, following the existing Nx monorepo architecture. The goal is to develop a native responsiveness solution that allows both the editor and the widgets created to be fully adaptable to any device.
Fundamental Principles
- Mobile-first: Design for mobile first and expand to larger screens
- Dual responsiveness: The editor must be responsive and the widgets created as well
- Optimized performance: Minimize re-renders and change detection
- Use of signals for reactive state
- Standalone components and OnPush
- Integration with existing architecture
Implementation Phases
Phase 1: Base Infrastructure (Weeks 1-2)
1.1 Responsive Data Models
// Instructions: Update core models to support responsiveness
// File: libs/core-models/src/lib/models/responsive/responsive-value.model.ts
// Create base model for responsive values
export interface ResponsiveValue<T> {
base: T; // Default value
sm?: T; // mobile (>= 640px)
md?: T; // tablet (>= 768px)
lg?: T; // desktop (>= 1024px)
xl?: T; // large desktop (>= 1280px)
'2xl'?: T; // extra large (>= 1536px)
}
// File: libs/core-models/src/lib/models/widget.model.ts
// Update widget model with responsive properties
export interface Widget {
id: string;
type: string;
name: string;
position: Position | ResponsiveValue<Position>;
size: Size | ResponsiveValue<Size>;
styles?: WidgetStyles | ResponsiveValue<WidgetStyles>;
content?: any;
config?: any;
}
export interface Position {
x: number;
y: number;
z?: number;
}
export interface Size {
width: number | string;
height: number | string;
minWidth?: number | string;
minHeight?: number | string;
maxWidth?: number | string;
maxHeight?: number | string;
}
export interface WidgetStyles {
backgroundColor?: string;
color?: string;
borderRadius?: string | number;
border?: string;
padding?: string | number;
margin?: string | number;
// Additional styles as needed
}
1.2 Viewport Services and Responsiveness
// Instructions: Implement services for viewport detection and responsive values
// File: libs/core-models/src/lib/services/responsive/viewport.service.ts
@Injectable({
providedIn: 'root',
})
export class ViewportService {
// Signals for reactive state
private _currentBreakpoint = signal<BreakpointKey>('base');
private _viewportSize = signal<{ width: number; height: number }>({ width: 0, height: 0 });
private _orientation = signal<'portrait' | 'landscape'>('landscape');
private _deviceType = signal<'mobile' | 'tablet' | 'desktop'>('desktop');
private _isTouch = signal<boolean>(false);
// Public readonly signals
public readonly currentBreakpoint = this._currentBreakpoint.asReadonly();
public readonly viewportSize = this._viewportSize.asReadonly();
constructor() {
// Initialize viewport size
this._viewportSize.set({
width: window.innerWidth,
height: window.innerHeight,
});
this.updateCurrentBreakpoint(window.innerWidth);
// Configure ResizeObserver
const resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) {
if (entry.target === document.documentElement) {
const { width, height } = entry.contentRect;
this._viewportSize.set({ width, height });
this.updateCurrentBreakpoint(width);
}
}
});
resizeObserver.observe(document.documentElement);
}
// Methods to determine breakpoint and get responsive values
public getValueForBreakpoint<T>(responsiveValue: ResponsiveValue<T>): T {
// Implement logic to get value based on current breakpoint
// ...
}
}
// File: libs/core-models/src/lib/services/responsive-widget.service.ts
// Service to calculate responsive styles for widgets
@Injectable({
providedIn: 'root',
})
export class ResponsiveWidgetService {
constructor(private viewportService: ViewportService) {}
// Method to get widget styles for current breakpoint
getWidgetStyles(widget: Widget): Record<string, any> {
// Implement logic to calculate styles based on breakpoint
// ...
}
}
1.3 Viewport State
// Instructions: Implement state for viewport information
// File: libs/core-models/src/lib/state/viewport/viewport.model.ts
export interface ViewportStateModel {
currentBreakpoint: BreakpointKey;
viewportSize: {
width: number;
height: number;
};
deviceType: 'mobile' | 'tablet' | 'desktop';
orientation: 'portrait' | 'landscape';
breakpoints: Breakpoints;
}
// File: libs/core-models/src/lib/state/viewport/viewport.actions.ts
export class UpdateViewport {
static readonly type = '[Viewport] Update';
constructor(
public payload: {
width: number;
height: number;
}
) {}
}
export class SetBreakpoint {
static readonly type = '[Viewport] Set Breakpoint';
constructor(public payload: BreakpointKey) {}
}
// File: libs/core-models/src/lib/state/viewport/viewport.state.ts
@State<ViewportStateModel>({
name: 'viewport',
defaults: {
currentBreakpoint: 'base',
viewportSize: { width: 0, height: 0 },
deviceType: 'desktop',
orientation: 'landscape',
breakpoints: {
sm: 640,
md: 768,
lg: 1024,
xl: 1280,
'2xl': 1536,
},
},
})
@Injectable()
export class ViewportState {
// Implement selectors and actions
// ...
}
1.4 Editor State Update
// Instructions: Update editor state to support responsive editing
// File: libs/core-models/src/lib/state/editor/editor.model.ts
// Add to EditorStateModel
export interface EditorStateModel {
// Existing fields...
activeBreakpoint: BreakpointKey; // Breakpoint being edited
showResponsiveControls: boolean; // Show responsive controls
responsivePreviewMode: 'single' | 'multiple'; // Preview mode
// Rest of fields...
}
// File: libs/core-models/src/lib/state/editor/editor.actions.ts
// Add actions
export class SetActiveBreakpoint {
static readonly type = '[Editor] Set Active Breakpoint';
constructor(public payload: BreakpointKey) {}
}
export class ToggleResponsiveControls {
static readonly type = '[Editor] Toggle Responsive Controls';
constructor(public visible?: boolean) {}
}
Phase 2: Base Directives and Components (Weeks 3-4)
2.1 Responsive Directives
// Instructions: Implement directives for responsive manipulation
// File: libs/shared-ui/src/lib/directives/responsive-draggable.directive.ts
@Directive({
standalone: true,
selector: '[responsiveDraggable]',
})
export class ResponsiveDraggableDirective implements OnInit, OnDestroy {
@Input() widget!: Widget;
@Input() activeBreakpoint: BreakpointKey = 'base';
// Implement logic to drag that updates only the active breakpoint
// ...
}
// File: libs/shared-ui/src/lib/directives/responsive-resizable.directive.ts
@Directive({
standalone: true,
selector: '[responsiveResizable]',
})
export class ResponsiveResizableDirective implements OnInit, OnDestroy {
@Input() widget!: Widget;
@Input() activeBreakpoint: BreakpointKey = 'base';
// Implement logic to resize for specific breakpoint
// ...
}
2.2 Base Widget Component Update
// Instructions: Update base widgets to support responsiveness
// File: libs/widgets/src/lib/components/container-widget/container-widget.component.ts
@Component({
standalone: true,
selector: 'app-container-widget',
templateUrl: './container-widget.component.html',
styleUrls: ['./container-widget.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ContainerWidgetComponent implements OnInit {
widget = input<Widget>();
activeBreakpoint = input<BreakpointKey>('base');
private responsiveWidgetService = inject(ResponsiveWidgetService);
private viewportService = inject(ViewportService);
styles = computed(() => {
return this.responsiveWidgetService.getWidgetStyles(this.widget());
});
// Implement additional logic for responsive container
// ...
}
// Update similarly other basic widgets (TextWidget, ImageWidget, etc.)
2.3 UI Components for Responsive Editing
// Instructions: Create UI components for responsive editing
// File: libs/editor-ui/src/lib/components/responsive-toolbar/responsive-toolbar.component.ts
@Component({
standalone: true,
selector: 'app-responsive-toolbar',
template: `
<div class="responsive-toolbar">
<button
*ngFor="let bp of breakpoints"
[class.active]="bp === activeBreakpoint()"
(click)="setActiveBreakpoint(bp)"
>
{{ bp }}
</button>
<!-- More controls -->
</div>
`,
styleUrls: ['./responsive-toolbar.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ResponsiveToolbarComponent {
private editorState = inject(EditorState);
activeBreakpoint = this.editorState.select(state => state.activeBreakpoint);
breakpoints: BreakpointKey[] = ['base', 'sm', 'md', 'lg', 'xl', '2xl'];
setActiveBreakpoint(bp: BreakpointKey) {
this.editorState.dispatch(new SetActiveBreakpoint(bp));
}
}
// File: libs/editor-ui/src/lib/components/device-preview/device-preview.component.ts
@Component({
standalone: true,
selector: 'app-device-preview',
templateUrl: './device-preview.component.html',
styleUrls: ['./device-preview.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DevicePreviewComponent {
// Implement component to simulate different devices
// ...
}
Phase 3: Integration with Editor (Weeks 5-6)
3.1 Canvas and Workspace Update
// Instructions: Update editor canvas to support responsive editing
// File: libs/widgets/src/lib/widgets/editor-canvas.component.ts
// Update with responsive support
@Component({
//...
})
export class EditorCanvasComponent {
private editorState = inject(EditorState);
private viewportState = inject(ViewportState);
activeBreakpoint = this.editorState.select(state => state.activeBreakpoint);
currentViewport = this.viewportState.select();
// Implement logic to render widgets based on active breakpoint
// ...
}
// File: apps/editor-web/src/app/features/editor/components/editor-workspace/editor-workspace.component.ts
// Update workspace to include responsive controls
@Component({
//...
})
export class EditorWorkspaceComponent {
// Add logic to integrate responsive toolbar and preview
// ...
}
3.2 Properties Panel Update
// Instructions: Update properties panel to edit by breakpoint
// File: libs/editor-ui/src/lib/components/properties-panel/properties-panel.component.ts
@Component({
//...
})
export class PropertiesPanelComponent {
private editorState = inject(EditorState);
selectedWidget = this.editorState.select(state => state.selectedWidget);
activeBreakpoint = this.editorState.select(state => state.activeBreakpoint);
// Implement UI to edit properties specific to breakpoint
// ...
updateWidgetProperty(widget: Widget, property: string, value: any) {
// Logic to update only active breakpoint
// ...
}
}
3.3 WidgetTree Update
// Instructions: Update widget tree with responsive indicators
// File: libs/editor-ui/src/lib/components/widget-tree/widget-tree.component.ts
@Component({
//...
})
export class WidgetTreeComponent {
// Add responsive configuration indicators
// ...
hasResponsiveOverrides(widget: Widget): boolean {
// Verify if widget has specific configurations for breakpoint
// ...
}
}
Phase 4: Actions and Editor Improvements (Weeks 7-8)
4.1 Propagation Between Breakpoints
// Instructions: Implement actions to propagate changes between breakpoints
// File: libs/core-models/src/lib/state/widgets/widgets.actions.ts
// Add actions
export class PropagateWidgetProperty {
static readonly type = '[Widgets] Propagate Property';
constructor(
public payload: {
widgetId: string;
property: string;
fromBreakpoint: BreakpointKey;
toBreakpoints: BreakpointKey[];
}
) {}
}
export class ResetWidgetToBaseBreakpoint {
static readonly type = '[Widgets] Reset To Base';
constructor(
public payload: {
widgetId: string;
properties?: string[];
}
) {}
}
4.2 Multi-Device Preview Tools
// Instructions: Implement multi-device preview tools
// File: libs/editor-ui/src/lib/components/multi-device-preview/multi-device-preview.component.ts
@Component({
standalone: true,
selector: 'app-multi-device-preview',
templateUrl: './multi-device-preview.component.html',
styleUrls: ['./multi-device-preview.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MultiDevicePreviewComponent {
// Implement simultaneous view of multiple devices
// ...
}
4.3 Responsive Validation
// Instructions: Implement responsive validation tools
// File: libs/core-models/src/lib/services/responsive-validator.service.ts
@Injectable({
providedIn: 'root',
})
export class ResponsiveValidatorService {
validatePage(page: Page): ResponsiveValidationResult {
// Implement validation rules:
// - Minimum sizes for touch elements
// - Legible text
// - Elements not cut
// - etc.
// ...
}
}
Phase 5: Optimization and Refinement (Weeks 9-10)
5.1 Performance Optimization
// Instructions: Optimize performance for responsive manipulation
// File: libs/core-models/src/lib/services/viewport.service.ts
// Add throttling to avoid excessive updates
// ...
// Optimize change detection in components
// ...
5.2 Tailwind Configuration
// Instructions: Update Tailwind configuration for consistent breakpoints
// File: apps/editor-web/tailwind.config.js
module.exports = {
theme: {
screens: {
sm: '640px',
md: '768px',
lg: '1024px',
xl: '1280px',
'2xl': '1536px',
},
// Rest of the configuration...
},
// ...
};
5.3 Documentation and Usage Guides
// Instructions: Create documentation for developers and users
// 1. Development Guide: How to extend widgets with responsive support
// 2. User Guide: How to use responsive tools
// 3. Examples and best practices
Success Criteria
The implementation will be considered successful when:
- Responsive Editor: The editor works correctly on all common screen resolutions (from 320px to 2560px wide)
- Responsive Widgets: Widgets can be specifically configured for different breakpoints
- Intuitive Editing: The user interface facilitates editing by breakpoint with clear visual tools
- Optimal Performance: The application maintains a fluid performance on all devices
- Exact Preview: The preview accurately shows how widgets will appear on different devices
- Controlled Propagation: Changes can selectively propagate between breakpoints
Tests
Unit Tests
// Instructions: Implement tests for responsive components and services
// File: libs/core-models/src/lib/services/viewport.service.spec.ts
describe('ViewportService', () => {
// Test breakpoint detection
// Test responsive value calculation
// ...
});
// File: libs/shared-ui/src/lib/directives/responsive-draggable.directive.spec.ts
describe('ResponsiveDraggableDirective', () => {
// Test that only updates active breakpoint
// ...
});
E2E Tests
// Instructions: Implement E2E tests to verify responsive experience
// File: apps/editor-web-e2e/src/e2e/responsive.cy.ts
describe('Responsive Editing', () => {
beforeEach(() => {
cy.visit('/');
});
it('should display responsive toolbar', () => {
// ...
});
it('should update widget only for active breakpoint', () => {
// ...
});
it('should preview correctly in different devices', () => {
// ...
});
});
Technical Recommendations
- Prefer Signals over RxJS: Use signals for local reactive state whenever possible for better performance
- Change Detection OnPush: Keep ChangeDetectionStrategy.OnPush in all components
- Immutability: Always create new objects when updating responsive properties
- Memoization: Use
computed()to derive values and avoid repeated calculations - Lazy Loading: Implement lazy loading of heavy components
- Virtualization: Use virtualization for long lists (especially in widget tree)
- Device Testing: Regularly test on real devices, not just simulators
Continuous Integration
- Visual Regression Testing: Implement visual regression tests for different breakpoints
- Lighthouse CI: Configure automated performance tests
- Device Matrix: Define device matrix for automated tests
This implementation plan provides a clear path to integrate complete responsive capabilities into the existing widget editor, maintaining the current architecture and following the project's mandatory guidelines.
Appendices
Appendix A: Responsive System Architecture Diagram
┌─────────────────────────────────┐
│ Responsive Widget System │
└───────────────┬─────────────────┘
│
┌───────────┴───────────┐
│ │
┌───▼───┐ ┌─────▼─────┐
│Models │ │ Services │
└───┬───┘ └─────┬─────┘
│ │
│ ┌───────────┐ │
└────►│ Signals │◄────┘
└─────┬─────┘
│
┌───────────┴───────────┐
│ │
┌───▼────┐ ┌─────▼─────┐
│ States │ │ Components │
└────────┘ └───────────┘
Appendix B: Responsive Data Flow
- Initial Data Loading: The widget configuration is loaded from storage
- Viewport Detection: The active breakpoint is determined based on viewport size
- Value Resolution: Responsive values for the active breakpoint are applied to the widget
- Component Rendering: UI components render widget with breakpoint-specific values
- User Interaction: User modifies widget properties for the active breakpoint
- Model Update: Only values for the active breakpoint are updated in the model
- Persistence: The updated responsive configuration is saved to storage
Appendix C: Example Widget Implementation
A simple example of how a text widget would use the responsive system:
// Text widget component
@Component({
selector: 'app-text-widget',
standalone: true,
imports: [CommonModule, ResponsiveWidgetPropsDirective],
template: `
<div
class="text-widget"
libResponsiveWidgetProps
[widget]="widget"
[activeBreakpoint]="activeBreakpoint"
#widgetProps="responsiveWidgetProps"
>
<div
[style.font-size.px]="widgetProps.getStyleProp('fontSize')"
[style.color]="widgetProps.getStyleProp('color')"
[style.font-weight]="widgetProps.getStyleProp('fontWeight')"
>
{{ widget.content }}
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TextWidgetComponent {
@Input() widget!: Widget;
@Input() activeBreakpoint: BreakpointKey = 'base';
@Output() widgetChanged = new EventEmitter<Widget>();
// Using the responsiveness system with style calculations
readonly textStyles = computed(() => {
const styles = this.widgetProps?.getWidgetStyles() || {};
return {
fontSize: styles.fontSize || '16px',
fontWeight: styles.fontWeight || 'normal',
color: styles.color || '#000000',
// Other styles...
};
});
}
Appendix D: Browser/Device Testing Matrix
| Type | Display Size | Devices to Test | Priority |
|---|---|---|---|
| Mobile Small | 320px-375px | iPhone SE, Galaxy S9 | High |
| Mobile Medium | 375px-428px | iPhone 13, Google Pixel 6 | High |
| Mobile Large | 428px-576px | iPhone 13 Pro Max, Galaxy S22 Ultra | Medium |
| Tablet Portrait | 576px-768px | iPad Mini, Galaxy Tab | High |
| Tablet Landscape | 768px-992px | iPad, Surface Go | Medium |
| Laptop | 992px-1200px | MacBook Air, Surface Laptop | High |
| Desktop | 1200px-1600px | 24" Monitors | High |
| Large Desktop | 1600px+ | 27"+ Monitors | Medium |
Appendix E: Performance Benchmarks
| Metric | Target Value | Measurement Method |
|---|---|---|
| Time to Interactive | < 3s | Lighthouse |
| First Contentful Paint | < 1.5s | Lighthouse |
| Input Latency | < 50ms | DevTools Performance |
| Widget Rendering (10 widgets) | < 100ms | Angular Profiler |
| Widget Dragging FPS | > 45fps | DevTools Performance |
| Memory Usage | < 100MB | Chrome Task Manager |
| Bundle Size (main) | < 500KB | Webpack Bundle Analyzer |
Appendix F: Migration Strategy
For existing widgets without responsive properties:
- Automatic Base Conversion: Convert existing scalar properties to responsive objects with base value
- Property Wrapping: Wrap all relevant widget properties in ResponsiveValue
- Schema Migration: Update database schemas to store responsive values
- Validation: Implement validation tools to ensure consistent experiences across breakpoints
Appendix G: Support Material
- Interactive Tutorials: Step-by-step guides for working with responsive editing
- UI Cheat Sheet: Quick reference for responsive editing controls
- Best Practices Guide: Recommendations for creating responsive widgets
- Troubleshooting Guide: Solutions to common responsive issues
Appendix H: Common Patterns
Widget Configuration Panel Pattern
@Component({
selector: 'app-widget-config-panel',
template: `
<div class="config-panel">
<div class="breakpoint-selector">
<button
*ngFor="let bp of breakpoints"
[class.active]="bp === activeBreakpoint()"
(click)="setActiveBreakpoint(bp)"
>
{{ bp }}
</button>
</div>
<ng-container *ngIf="activeWidget()">
<!-- Form controls that update only active breakpoint -->
<div class="form-group">
<label>Width</label>
<input
type="text"
[value]="getWidgetSizeProp('width')"
(input)="updateWidgetSizeProp('width', $event)"
/>
<label>Height</label>
<input
type="text"
[value]="getWidgetSizeProp('height')"
(input)="updateWidgetSizeProp('height', $event)"
/>
<!-- More properties -->
</div>
<div class="property-controls">
<button (click)="propagateToLargerBreakpoints()">Apply to Larger Screens</button>
<button (click)="propagateToSmallerBreakpoints()">Apply to Smaller Screens</button>
</div>
</ng-container>
</div>
`,
})
export class WidgetConfigPanelComponent {
// Reactive state with signals
private _activeBreakpoint = signal<BreakpointKey>('base');
activeBreakpoint = this._activeBreakpoint.asReadonly();
activeWidget = this.widgetState.selectedWidget;
breakpoints: BreakpointKey[] = ['base', 'sm', 'md', 'lg', 'xl', '2xl'];
constructor(private widgetState: WidgetStateService) {}
setActiveBreakpoint(bp: BreakpointKey) {
this._activeBreakpoint.set(bp);
}
getWidgetSizeProp(prop: keyof Size): any {
const widget = this.activeWidget();
if (!widget) return null;
// Get value for the active breakpoint
// ...
}
updateWidgetSizeProp(prop: keyof Size, event: Event) {
const widget = this.activeWidget();
if (!widget) return;
const value = (event.target as HTMLInputElement).value;
// Create immutable update for just the active breakpoint
// ...
}
propagateToLargerBreakpoints() {
// Implementation to propagate current values to larger breakpoints
// ...
}
propagateToSmallerBreakpoints() {
// Implementation to propagate current values to smaller breakpoints
// ...
}
}