UNIVERSAL PROPERTY SYSTEM

Comprehensive guidelines for AI-assisted development within the Editor platform

🎯 Universal Property System Architecture

Problem Statement

Current Issue: Property updates are widget-specific and inconsistent.

User Requirement:

"We must have an independent set of editors based on the property group or property - otherwise, it will take forever to ensure each widget has the correct styles/structure plus our core architecture of reusability will not work."


🏗️ Solution: Universal Property Update System

Core Principle

ONE property update mechanism that works for ALL properties across ALL widgets.


Architecture

1. Property Value Path System

Every property has a canonical path that defines where it's stored:

interface PropertyPath {
  // Where the value lives
  location: 'properties' | 'styles' | 'position' | 'size';
  
  // Path within that location
  path: string;  // e.g., 'fontFamily', 'backgroundColor', 'base.backgroundColor'
  
  // Whether it's responsive
  responsive: boolean;
  
  // Type for validation
  type: 'string' | 'number' | 'boolean' | 'color' | 'select' | 'custom';
}

Examples:

// Font Family
{
  location: 'properties',
  path: 'fontFamily',
  responsive: false,
  type: 'string'
}

// Background Color (Custom Styles)
{
  location: 'styles',
  path: 'backgroundColor',  // Will become 'base.backgroundColor' for responsive
  responsive: true,
  type: 'color'
}

// Button Text
{
  location: 'properties',
  path: 'text',
  responsive: false,
  type: 'string'
}

2. Universal Property Update Service

@Injectable({ providedIn: 'root' })
export class UniversalPropertyService {
  
  /**
   * Updates ANY property on ANY widget using a standard format
   */
  updateProperty(params: PropertyUpdateParams): Widget {
    const { widget, location, path, value, responsive, breakpoint } = params;
    
    // Validate
    if (!widget) throw new Error('Widget is required');
    
    // Update based on location
    switch (location) {
      case 'properties':
        return this.updateWidgetProperties(widget, path, value);
      
      case 'styles':
        return responsive
          ? this.updateResponsiveStyle(widget, path, value, breakpoint || 'base')
          : this.updateNonResponsiveStyle(widget, path, value);
      
      case 'position':
        return this.updatePosition(widget, path, value, responsive, breakpoint);
      
      case 'size':
        return this.updateSize(widget, path, value, responsive, breakpoint);
    }
  }
  
  /**
   * Gets ANY property value from ANY widget
   */
  getProperty(params: PropertyGetParams): any {
    const { widget, location, path, responsive, breakpoint } = params;
    
    switch (location) {
      case 'properties':
        return this.getNestedValue(widget.properties, path);
      
      case 'styles':
        return responsive
          ? this.getResponsiveValue(widget.styles, path, breakpoint || 'base')
          : this.getNestedValue(widget.styles, path);
      
      // ... similar for position, size
    }
  }
  
  // Helper methods
  private updateWidgetProperties(widget: Widget, path: string, value: any): Widget {
    const newProperties = this.setNestedValue(widget.properties || {}, path, value);
    return { ...widget, properties: newProperties };
  }
  
  private updateResponsiveStyle(widget: Widget, path: string, value: any, breakpoint: BreakpointKey): Widget {
    const styles = widget.styles || {};
    
    if (typeof styles === 'object' && 'base' in styles) {
      // Already responsive
      const breakpointStyles = styles[breakpoint] || styles.base || {};
      const updatedBreakpointStyles = this.setNestedValue(breakpointStyles, path, value);
      
      return {
        ...widget,
        styles: {
          ...(styles as ResponsiveValue<WidgetStyles>),
          [breakpoint]: updatedBreakpointStyles
        } as ResponsiveValue<WidgetStyles>
      };
    } else {
      // Convert to responsive
      return {
        ...widget,
        styles: {
          base: this.setNestedValue(styles as WidgetStyles, path, value)
        } as ResponsiveValue<WidgetStyles>
      };
    }
  }
  
  private setNestedValue(obj: any, path: string, value: any): any {
    const keys = path.split('.');
    if (keys.length === 1) {
      return { ...obj, [path]: value };
    }
    
    const [first, ...rest] = keys;
    return {
      ...obj,
      [first]: this.setNestedValue(obj[first] || {}, rest.join('.'), value)
    };
  }
  
  private getNestedValue(obj: any, path: string): any {
    if (!obj) return undefined;
    const keys = path.split('.');
    return keys.reduce((current, key) => current?.[key], obj);
  }
  
  private getResponsiveValue(styles: any, path: string, breakpoint: BreakpointKey): any {
    if (typeof styles === 'object' && 'base' in styles) {
      const breakpointStyles = styles[breakpoint] || styles.base || {};
      return this.getNestedValue(breakpointStyles, path);
    }
    return this.getNestedValue(styles, path);
  }
}

3. Property Definition Enhancement

Enhance PropertyDefinition to include the path information:

export interface PropertyDefinition {
  key: string;  // Display key (e.g., 'properties.fontSize')
  label: string;
  type: PropertyControlType;
  
  // ✅ NEW: Canonical location and path
  location: 'properties' | 'styles' | 'position' | 'size';
  path: string;  // Path within location (e.g., 'fontSize', 'backgroundColor')
  
  responsive?: boolean;
  
  // DEPRECATED: getValue/setValue (use UniversalPropertyService instead)
  getValue?: (widget: Widget, breakpoint?: BreakpointKey) => any;
  setValue?: (widget: Widget, value: any, breakpoint?: BreakpointKey) => Widget;
  
  // ... other properties
}

4. Standard Property Editor

Create ONE standard property editor that works for all property types:

@Component({
  selector: 'lib-universal-property-editor',
  template: `
    <div class="property-editor">
      <label>{{ property().label }}</label>
      
      @switch (property().type) {
        @case ('string') {
          <input type="text" 
            [ngModel]="currentValue()" 
            (ngModelChange)="onValueChange($event)" />
        }
        @case ('number') {
          <input type="number"
            [ngModel]="currentValue()"
            (ngModelChange)="onValueChange($event)"
            [min]="property().min"
            [max]="property().max"
            [step]="property().step || 1" />
        }
        @case ('color') {
          <lib-color-picker
            [value]="currentValue()"
            (valueChange)="onValueChange($event)" />
        }
        @case ('select') {
          <select [ngModel]="currentValue()" (ngModelChange)="onValueChange($event)">
            @for (option of property().options; track option.value) {
              <option [value]="option.value">{{ option.label }}</option>
            }
          </select>
        }
        @case ('custom') {
          <ng-container [ngComponentOutlet]="property().component"
            [ngComponentOutletInputs]="getCustomInputs()" />
        }
      }
    </div>
  `
})
export class UniversalPropertyEditorComponent {
  private readonly universalPropService = inject(UniversalPropertyService);
  private readonly editorService = inject(EditorService);
  
  readonly widget = input.required<Widget>();
  readonly property = input.required<PropertyDefinition>();
  readonly breakpoint = input<BreakpointKey>('base');
  
  // ✅ Use UniversalPropertyService to get value
  readonly currentValue = computed(() => {
    return this.universalPropService.getProperty({
      widget: this.widget(),
      location: this.property().location,
      path: this.property().path,
      responsive: this.property().responsive,
      breakpoint: this.breakpoint()
    });
  });
  
  // ✅ Use UniversalPropertyService to set value
  onValueChange(value: any): void {
    const updatedWidget = this.universalPropService.updateProperty({
      widget: this.widget(),
      location: this.property().location,
      path: this.property().path,
      value,
      responsive: this.property().responsive,
      breakpoint: this.breakpoint()
    });
    
    // Save via EditorService (flash-persisted)
    this.editorService.updateWidget(updatedWidget);
    
    console.log(`✅ [UniversalPropertyEditor] Updated ${this.property().path}:`, value);
  }
}

5. Property Group Definitions (Simplified)

With the universal system, property definitions become MUCH simpler:

// ✅ NEW: Simple, declarative
export const BUTTON_WIDGET_GROUP_CONTENT: PropertyGroup = {
  id: 'button-content',
  title: 'Content',
  properties: [
    {
      key: 'properties.text',
      label: 'Text',
      type: 'string',
      location: 'properties',  // ✅ Where it's stored
      path: 'text',           // ✅ Path within location
      responsive: false
    },
    {
      key: 'properties.fontFamily',
      label: 'Font Family',
      type: 'select',
      location: 'properties',
      path: 'fontFamily',
      responsive: false,
      options: [
        { value: 'Arial, sans-serif', label: 'Arial' },
        { value: 'Roboto, sans-serif', label: 'Roboto' }
      ]
    },
    {
      key: 'properties.fontSize',
      label: 'Font Size',
      type: 'number',
      location: 'properties',
      path: 'fontSize',
      responsive: false,
      min: 8,
      max: 96,
      step: 1
    }
  ]
};

export const BUTTON_WIDGET_GROUP_CUSTOM_STYLES: PropertyGroup = {
  id: 'button-custom-styles',
  title: 'Custom Styles',
  properties: [
    {
      key: 'styles.backgroundColor',
      label: 'Background Color',
      type: 'color',
      location: 'styles',       // ✅ Stored in styles
      path: 'backgroundColor',  // ✅ Simple path
      responsive: true          // ✅ Handles responsive automatically
    }
  ]
};

No more custom getValue/setValue functions! The UniversalPropertyService handles everything.


Benefits

For Developers

For Widgets

For Users


Migration Plan

Phase 1: Create Universal System (NEXT)

  1. ✅ Create UniversalPropertyService
  2. ✅ Create UniversalPropertyEditorComponent
  3. ✅ Add location and path to PropertyDefinition
  4. ✅ Test with simple properties (text, fontSize)

Phase 2: Migrate Button Widget

  1. Convert button property groups to use new format
  2. Remove custom getValue/setValue
  3. Test all properties work correctly
  4. Verify flash-persistence

Phase 3: Migrate All Widgets

  1. Apply pattern to remaining widgets
  2. Remove old property update mechanisms
  3. Delete deprecated code

Phase 4: Enhanced Editors

  1. Create ColorPickerComponent (theme tokens + manual)
  2. Create SpacingEditorComponent
  3. Create BorderEditorComponent
  4. Create ShadowEditorComponent

Testing

Test Scenarios

  1. Simple String Property: Button text
  2. Simple Number Property: Font size
  3. Responsive Style Property: Background color
  4. Nested Property: Border width within styles
  5. Custom Component Property: Font properties group

All should:


Current Debug Instructions

For now, test font size changes and check console for:

🔤 [FontPropertiesGroup] updateFontProperty called: { propertyKey: 'properties.fontSize', value: 18, ... }
🔤 [FontPropertiesGroup] Emitting onChange with: { propertyKey: 'font', value: { 'properties.fontSize': 18 } }
🔤 [FontGroup] setValue called with: { data: { ... }, breakpoint: 'base', currentWidget: { ... } }
🔤 [FontGroup] Processing: { propertyKey: 'properties.fontSize', propertyValue: 18 }
✅ EditorService.updateWidget: Updated widget "Button Widget test" (...)
🔤 [ButtonWidget] Font properties: { fontFamily: ..., fontSize: 18, ... }

If ANY of these logs are missing, we know WHERE the chain breaks.


Priority: BUILD THIS SYSTEM NOW

This is the foundation for:

The user is absolutely right - without this, we're just patching holes forever.


Status: 🚧 IN PROGRESS - Building universal system Next: Implement UniversalPropertyService and test with font properties