CALCULATION BASED RESPONSIVE SYSTEM

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

🎯 Calculation-Based Responsive System Enhancement

Session: 1.3
Date: November 14, 2025
Status: 📋 Planning
Priority: 🔥 Critical Foundation


Vision: Platform-Agnostic Responsive Design

Core Philosophy

Instead of relying on CSS units (%, vw, vh, rem) which are platform-specific, we use mathematical calculations based on container dimensions. This enables:

  1. ✅ Perfect Flutter/React Native export - numeric calculations work everywhere
  2. ✅ Predictable behavior - no CSS quirks or browser differences
  3. ✅ Single canvas - one design adapts to all breakpoints automatically
  4. ✅ Intelligent conversions - "Fill Width" button calculates exact pixels

Current Architecture (What We Have)

✅ Already Implemented

Our ResponsiveWidgetPropsDirective already has:

// libs/widgets/src/lib/directives/responsive-widget-props.directive.ts:143-179

convertToPixels(value: string | number, dimensionType: 'width' | 'height'): number {
  if (typeof value === 'number') return value;

  const numValue = parseFloat(value);
  if (isNaN(numValue)) return 0;

  if (value.includes('px')) return numValue;

  const containerRect = this.getContainerRect();

  if (value.includes('%')) {
    return dimensionType === 'width'
      ? (numValue / 100) * containerRect.width
      : (numValue / 100) * containerRect.height;
  }

  if (value.includes('vw')) {
    return (numValue / 100) * window.innerWidth;
  }

  if (value.includes('vh')) {
    return (numValue / 100) * window.innerHeight;
  }

  // rem and em conversions...
  return numValue;
}

✅ This is BRILLIANT! The foundation is already there.


🎯 Enhancement Goals

Phase 1: Separate Width/Height Units (Backward Compatible)

Problem: Current Size interface uses single units for both dimensions:

export interface Size {
  width: number | string;
  height: number | string;
  units?: Units; // ❌ Single unit for both
}

Solution: Add optional per-dimension units while maintaining backward compatibility:

export interface Size {
  width: number | string;
  height: number | string;
  
  // Legacy (still supported)
  units?: Units; // Falls back to this if widthUnits/heightUnits not set
  
  // New (preferred)
  widthUnits?: Units;
  heightUnits?: Units;
}

Migration Strategy:

  1. Keep units as fallback (no breaking changes)
  2. Add widthUnits and heightUnits as optional
  3. Update ResponsiveWidgetPropsDirective to check both
  4. Migrate widgets one by one (starting with divider)

Phase 2: Intelligent "Fill Width" Button

Feature: Add button to properties panel that calculates container width in pixels.

// New button in widget-size.component.ts

fillWidth(): void {
  const containerWidth = this.getContainerWidth(); // From directive
  this.updateSize('width', containerWidth);
  this.updateUnits('px');
}

fillHeight(): void {
  const containerHeight = this.getContainerHeight();
  this.updateSize('height', containerHeight);
  this.updateUnits('px');
}

UI Enhancement:

<div class="property-field">
  <label for="size-width">Ancho</label>
  <div class="input-with-actions">
    <input type="number" id="size-width" [(ngModel)]="width" />
    <button (click)="fillWidth()" title="Llenar ancho del contenedor">
      <i class="fa fa-arrows-h"></i>
    </button>
  </div>
</div>

Phase 3: Enhanced Breakpoint System

Current: Already excellent! ✅

<select class="breakpoint-selector">
  <option value="base">Base (Móvil)</option>
  <option value="sm">SM (640px+)</option>
  <option value="md">MD (768px+)</option>
  <option value="lg">LG (1024px+)</option>
  <option value="xl">XL (1280px+)</option>
  <option value="2xl">2XL (1536px+)</option>
</select>

Enhancement: Add visual preview panel showing multiple breakpoints simultaneously.

<div class="devices-grid">
  <div class="device-preview" data-breakpoint="base">
    <div class="device-frame mobile-small">
      <div class="device-screen">
        <!-- Rendered canvas at 375x667 -->
      </div>
    </div>
    <span>Móvil pequeño (375x667)</span>
  </div>
  
  <div class="device-preview" data-breakpoint="sm">
    <div class="device-frame mobile-large">
      <div class="device-screen">
        <!-- Rendered canvas at 640x960 -->
      </div>
    </div>
    <span>Móvil grande (640x960)</span>
  </div>
  
  <!-- Additional breakpoints... -->
</div>

📊 Calculation System Architecture

Storage Format (JSON)

{
  "id": "widget-123",
  "type": "divider",
  "size": {
    "base": {
      "width": 360,
      "height": 2,
      "widthUnits": "px",
      "heightUnits": "px"
    },
    "md": {
      "width": 720,
      "height": 2,
      "widthUnits": "px",
      "heightUnits": "px"
    }
  }
}

Runtime Calculation

// When rendering widget
const containerWidth = 1024; // Current container width
const widgetWidth = 360; // Stored value
const actualPixels = widgetWidth; // Direct use - no conversion needed!

// For export to Flutter
Widget buildDivider() {
  return Container(
    width: 360.0, // ✅ Direct numeric value
    height: 2.0,
    color: Color(0xFFDF4949),
  );
}

Key Benefit: Numeric values work across ALL platforms!


🚀 Implementation Plan

Step 1: Update Core Models (Non-Breaking)

File: libs/libs-foundational/core-models/src/lib/types/size.types.ts

export interface Size {
  width: number | string;
  height: number | string;
  minWidth: number | string;
  minHeight: number | string;
  maxWidth?: number | string;
  maxHeight?: number | string;
  responsive: boolean;
  
  // Legacy (backward compatible)
  units?: Units;
  
  // New (preferred) - per-dimension units
  widthUnits?: Units;
  heightUnits?: Units;
}

Step 2: Update ResponsiveWidgetPropsDirective

File: libs/widgets/src/lib/directives/responsive-widget-props.directive.ts

getComputedStyles(): Record<string, string> {
  const size = getValueForBreakpoint(this.widget.size, this.activeBreakpoint);
  
  // Check for per-dimension units (new) or fallback to legacy units
  const widthUnit = size.widthUnits || size.units || 'px';
  const heightUnit = size.heightUnits || size.units || 'px';
  
  // Calculate width
  if (typeof size.width === 'number') {
    styles['width'] = this.formatWithUnit(size.width, widthUnit);
  } else {
    styles['width'] = size.width as string;
  }
  
  // Calculate height
  if (typeof size.height === 'number') {
    styles['height'] = this.formatWithUnit(size.height, heightUnit);
  } else {
    styles['height'] = size.height as string;
  }
  
  return styles;
}

private formatWithUnit(value: number, unit: Units): string {
  if (unit === 'px') return `${value}px`;
  if (unit === '%') return `${value}%`;
  // ... other units
  return `${value}px`; // Default
}

Step 3: Update WidgetSizeComponent

File: libs/widgets/src/lib/properties-components/widget-size.component.ts

<!-- Enhanced template with per-dimension units -->
<div class="property-row">
  <div class="property-field">
    <label>Ancho</label>
    <div class="input-with-actions">
      <input type="number" [(ngModel)]="width" />
      <select [(ngModel)]="widthUnits">
        <option value="px">px</option>
        <option value="%">%</option>
        <option value="auto">auto</option>
      </select>
      <button (click)="fillWidth()" title="Llenar ancho">
        <i class="fa fa-arrows-h"></i>
      </button>
    </div>
  </div>

  <div class="property-field">
    <label>Alto</label>
    <div class="input-with-actions">
      <input type="number" [(ngModel)]="height" />
      <select [(ngModel)]="heightUnits">
        <option value="px">px</option>
        <option value="%">%</option>
        <option value="auto">auto</option>
      </select>
      <button (click)="fillHeight()" title="Llenar alto">
        <i class="fa fa-arrows-v"></i>
      </button>
    </div>
  </div>
</div>

Step 4: Widget-by-Widget Migration

Priority Order (Start with structural elements):

  1. ✅ Divider Widget - Simple, good test case
  2. ✅ Spacer Widget - Similar to divider
  3. 📦 Container Widget - Critical structural element
  4. 📦 Card Widget - Common layout component
  5. 📦 Text Widget - Most used widget
  6. 📦 Button Widget - Interactive element
  7. 📦 Image Widget - Media handling
  8. 📦 All remaining widgets - One by one

Migration Checklist per Widget:


🧪 Testing Strategy

1. Unit Tests

describe('ResponsiveWidgetPropsDirective', () => {
  it('should use widthUnits when available', () => {
    const size = { width: 100, widthUnits: '%', heightUnits: 'px' };
    const styles = directive.getComputedStyles();
    expect(styles.width).toBe('100%');
  });

  it('should fall back to legacy units', () => {
    const size = { width: 100, units: 'px' };
    const styles = directive.getComputedStyles();
    expect(styles.width).toBe('100px');
  });
});

2. Visual Regression Tests

3. Export Tests

describe('Flutter Export', () => {
  it('should export divider with numeric values', () => {
    const flutter = exportToFlutter(dividerWidget);
    expect(flutter).toContain('width: 360.0');
    expect(flutter).not.toContain('%'); // No CSS units
  });
});

🎨 UI/UX Enhancements

Enhanced Property Panel

┌─────────────────────────────────────┐
│ 📐 Tamaño                           │
├─────────────────────────────────────┤
│ Ancho                               │
│ ┌────┬─────┬────┐                   │
│ │ 100│ [px▼] │[↔]│                  │
│ └────┴─────┴────┘                   │
│                                     │
│ Alto                                │
│ ┌────┬─────┬────┐                   │
│ │  50│ [px▼] │[↕]│                  │
│ └────┴─────┴────┘                   │
│                                     │
│ ☑ Mantener proporciones            │
│ ☑ Bloquear relación de aspecto     │
└─────────────────────────────────────┘

New Features:

Responsive Preview Panel

┌────────────────────────────────────────────┐
│ 📱 Vista Responsiva                        │
├────────────────────────────────────────────┤
│ Breakpoint actual: [MD (768px+) ▼]        │
│                                            │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐   │
│ │ 📱 Base  │ │ 📱 SM    │ │ 💻 MD    │   │
│ │ 375x667  │ │ 640x960  │ │ 768x1024 │   │
│ │ [preview]│ │ [preview]│ │ [preview]│   │
│ └──────────┘ └──────────┘ └──────────┘   │
│                                            │
│ Todos los tamaños se ajustan               │
│ automáticamente ✨                         │
└────────────────────────────────────────────┘

📦 Export System

Flutter Export Example

// Generated from GoGrab Space
import 'package:flutter/material.dart';

class GeneratedPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        // Responsive breakpoint detection
        final width = constraints.maxWidth;
        final isMobile = width < 640;
        final isTablet = width >= 640 && width < 1024;
        
        return Container(
          child: Column(
            children: [
              // Divider widget
              Container(
                width: isMobile ? 360.0 : 720.0, // ✅ Calculated values
                height: 2.0,
                color: Color(0xFFDF4949),
              ),
              // More widgets...
            ],
          ),
        );
      },
    );
  }
}

React Native Export Example

// Generated from GoGrab Space
import React from 'react';
import { View, useWindowDimensions } from 'react-native';

export default function GeneratedScreen() {
  const { width } = useWindowDimensions();
  const isMobile = width < 640;
  
  return (
    <View>
      {/* Divider widget */}
      <View style={{
        width: isMobile ? 360 : 720, // ✅ Numeric calculations
        height: 2,
        backgroundColor: '#DF4949',
      }} />
      {/* More widgets... */}
    </View>
  );
}

Key Benefit: Pure calculations = platform-agnostic! 🎉


⚠️ Critical Considerations

1. Don't Break Existing Widgets

2. Maintain Responsive Excellence

The editor-web-lib team spent significant time perfecting:

DO NOT break any of this! Only enhance.

3. Performance Considerations


📈 Success Metrics

Phase 1 Complete When:

Phase 2 Complete When:

Phase 3 Complete When:


🎯 Next Steps

  1. Review this plan with the team
  2. Update Size interface (non-breaking change)
  3. Enhance ResponsiveWidgetPropsDirective (add per-dimension unit support)
  4. Test with divider widget (simplest case)
  5. Add "Fill Width" button to property panel
  6. Migrate remaining widgets one by one
  7. Implement export system for Flutter/React Native

🔗 Related Documentation


Status: 📋 Planning Phase
Next Review: After Phase 1 implementation
Team: editor-web-lib + AI Enhancement