UNIFIED STYLING ARCHITECTURE

Documentation for Sessions

๐ŸŽจ Unified Styling Architecture - Page Builder

Session: 12.22
Date: December 19, 2025
Status: โœ… Complete


๐Ÿ“‹ Overview

Refactored the page-builder to use existing, battle-tested property systems instead of duplicating logic. The new architecture connects:

  1. WidgetStylingService (NEW) - Variant management
  2. getWidgetStyle() helper (EXISTING) - Responsive value resolution
  3. PropertyTemplatesService (EXISTING) - Property definitions from backend
  4. Direct DOM styling (PATTERN) - Like WidgetStylesHandlerService

๐Ÿ—๏ธ Architecture

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    UNIFIED STYLING PIPELINE                         โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                     โ”‚
โ”‚  USER ACTION: Selects "Primary" variant in properties panel        โ”‚
โ”‚         โ†“                                                           โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”‚
โ”‚  โ”‚  PropertiesPanelComponent.onVariantChange('primary')     โ”‚      โ”‚
โ”‚  โ”‚    โ†’ stylingService.applyVariant(widget, 'primary')      โ”‚      โ”‚
โ”‚  โ”‚    โ†’ Returns merged config: variant + user overrides     โ”‚      โ”‚
โ”‚  โ”‚    โ†’ Emits updateWidget with new config                  โ”‚      โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜      โ”‚
โ”‚         โ†“                                                           โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”‚
โ”‚  โ”‚  PageStateService updates widget.config                  โ”‚      โ”‚
โ”‚  โ”‚    โ†’ widget.config._variant = 'primary'                  โ”‚      โ”‚
โ”‚  โ”‚    โ†’ widget.config gets variant styles                   โ”‚      โ”‚
โ”‚  โ”‚    โ†’ Persists to localStorage                            โ”‚      โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜      โ”‚
โ”‚         โ†“                                                           โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”‚
โ”‚  โ”‚  CanvasWidgetComponent.styles computed signal            โ”‚      โ”‚
โ”‚  โ”‚                                                           โ”‚      โ”‚
โ”‚  โ”‚  1. Get effective config:                                โ”‚      โ”‚
โ”‚  โ”‚     config = stylingService.getEffectiveConfig(widget)   โ”‚      โ”‚
โ”‚  โ”‚                                                           โ”‚      โ”‚
โ”‚  โ”‚  2. Resolve responsive values using getWidgetStyle():    โ”‚      โ”‚
โ”‚  โ”‚     bgColor = getWidgetStyle(config, 'backgroundColor')  โ”‚      โ”‚
โ”‚  โ”‚                                                           โ”‚      โ”‚
โ”‚  โ”‚  3. Build CSS styles object                              โ”‚      โ”‚
โ”‚  โ”‚     return { backgroundColor: bgColor, ... }             โ”‚      โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜      โ”‚
โ”‚         โ†“                                                           โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”‚
โ”‚  โ”‚  Template: [ngStyle]="styles()"                          โ”‚      โ”‚
โ”‚  โ”‚    โ†’ Applies styles directly to DOM                      โ”‚      โ”‚
โ”‚  โ”‚    โ†’ Widget renders with variant appearance              โ”‚      โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜      โ”‚
โ”‚                                                                     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

๐Ÿ”ง Key Components

1. WidgetStylingService

Purpose: Manages widget variants and provides effective configuration.

Location: libs/libs-structural/page-builder/src/lib/services/widget-styling.service.ts

Key Methods:

// Get all variants for a widget type
getVariantsForWidgetType(widgetType: string): UnifiedVariant[]

// Get a specific variant
getVariant(widgetType: string, variantId: string): UnifiedVariant | undefined

// Apply variant to widget config
applyVariant(widget: WidgetInstance, variantId: string): Record<string, unknown>

// Get effective config (variant + user overrides)
getEffectiveConfig(widget: WidgetInstance): Record<string, unknown>

// Get theme-aware style options
getColorOptions(): StylePreset[]
getSpacingOptions(): StylePreset[]
getBorderRadiusOptions(): StylePreset[]

Built-in Variants (Self-contained, no circular deps):

2. getWidgetStyle() Helper

Purpose: Resolves responsive property values with breakpoint inheritance.

Location: @libs-foundational/core-models

Usage:

import { getWidgetStyle, type BreakpointKey } from '@libs-foundational/core-models';

const bgColor = getWidgetStyle(config, 'backgroundColor', breakpoint);
// Returns: Value for current breakpoint, or falls back to base

Benefits:

3. PropertyTemplatesService

Purpose: Loads property definitions from backend API.

Location: libs/libs-structural/page-builder/src/lib/services/property-templates.service.ts

Key Methods:

// Load templates from backend (with built-in fallback)
loadTemplates(spaceId: string): void

// Get templates for a widget type
getTemplatesForWidgetType(widgetType: string): PropertyTemplate[]

// Get all properties for a widget type (flattened)
getPropertiesForWidgetType(widgetType: string): PropertyDefinition[]

// Get properties grouped by category
getPropertiesGroupedByCategory(widgetType: string): Map<PropertyCategory, PropertyDefinition[]>

Property Categories:

4. CanvasWidgetComponent

Purpose: Renders widgets on canvas with proper styling.

Location: libs/libs-structural/page-builder/src/lib/components/canvas-widget/canvas-widget.component.ts

Refactored styles computed:

readonly styles = computed(() => {
  const widget = this.widget();
  const bp = this.breakpoint() as BreakpointKey;
  
  // Get effective config (variant + user overrides)
  const config = this.stylingService.getEffectiveConfig(widget);
  
  // Position styles
  const positionStyles = { left, top, width, height, ... };
  
  // Visual styles using getWidgetStyle helper
  const visualStyles = {};
  const bgColor = getWidgetStyle(config, 'backgroundColor', bp);
  if (bgColor) visualStyles['backgroundColor'] = String(bgColor);
  // ... more properties
  
  return { ...positionStyles, ...visualStyles };
});

Benefits:

5. PropertiesPanelComponent

Purpose: Dynamic property editor with variant selector.

Location: libs/libs-structural/page-builder/src/lib/components/properties-panel/properties-panel.component.ts

Key Features:

Variant Selection Flow:

onVariantChange(variantId: string): void {
  const widget = this.widget();
  
  // Apply variant using styling service
  const newConfig = this.stylingService.applyVariant(widget, variantId);
  
  // Emit update
  this.updateWidget.emit({
    ...widget,
    config: newConfig,
    updatedAt: Date.now(),
  });
}

๐Ÿ“Š Data Flow

Property Update Flow

User edits property (e.g., backgroundColor = "#ff0000")
  โ†“
PropertiesPanelComponent.updateConfig('backgroundColor', '#ff0000')
  โ†“
Emit updateWidget({ ...widget, config: { ...config, backgroundColor: '#ff0000' } })
  โ†“
PageStateService.updateWidget(widget)
  โ†“
localStorage persistence
  โ†“
CanvasWidgetComponent.styles recomputes
  โ†“
getWidgetStyle(config, 'backgroundColor', bp) โ†’ '#ff0000'
  โ†“
[ngStyle]="{ backgroundColor: '#ff0000' }"
  โ†“
DOM updates, widget renders with new color

Variant Selection Flow

User selects "Primary" variant
  โ†“
PropertiesPanelComponent.onVariantChange('primary')
  โ†“
WidgetStylingService.applyVariant(widget, 'primary')
  โ†’ Returns: {
      _variant: 'primary',
      backgroundColor: '#3b82f6',
      color: '#ffffff',
      borderRadius: '6px',
      padding: '12px 24px',
      ...existingConfig  // User overrides preserved
    }
  โ†“
PageStateService.updateWidget(widget)
  โ†“
CanvasWidgetComponent.styles recomputes
  โ†“
getWidgetStyle resolves each property with breakpoint support
  โ†“
Widget renders with variant appearance

๐ŸŽฏ Benefits

1. Reusability

2. Performance

3. Maintainability

4. Extensibility

5. DRY Principle


๐Ÿš€ Adding New Variants

// In widget-styling.service.ts, update BUILT_IN_VARIANTS_REGISTRY:

const BUILT_IN_VARIANTS_REGISTRY: Record<string, ButtonVariantConfig[]> = {
  button: BUILT_IN_BUTTON_VARIANTS,
  
  // Add new widget type variants:
  card: [
    {
      id: 'elevated',
      name: 'Elevated',
      description: 'Card with shadow elevation',
      category: 'style',
      styles: {
        backgroundColor: 'var(--ux-surface)',
        boxShadow: 'var(--ux-shadow-md)',
        borderRadius: '12px',
        padding: '24px',
      },
    },
    {
      id: 'outlined',
      name: 'Outlined',
      description: 'Card with border',
      category: 'style',
      styles: {
        backgroundColor: 'transparent',
        border: '1px solid var(--ux-border)',
        borderRadius: '12px',
        padding: '24px',
      },
    },
  ],
  
  input: [
    // Add input variants...
  ],
};

๐Ÿ“ Code Examples

Example 1: Variant with Theme Variables

{
  id: 'glass-primary',
  name: 'Glassmorphic Primary',
  category: 'style',
  styles: {
    backgroundColor: 'rgba(59, 130, 246, 0.1)',  // Translucent primary
    color: 'var(--ux-primary)',                   // Theme-aware
    border: '1px solid rgba(59, 130, 246, 0.2)',
    borderRadius: 'var(--ux-radius-lg)',          // Theme radius
    backdropFilter: 'blur(10px)',
    boxShadow: 'var(--ux-shadow-md)',             // Theme shadow
  },
}

Example 2: Size Variant

{
  id: 'compact',
  name: 'Compact',
  category: 'size',
  styles: {
    padding: 'var(--ux-space-xs)',    // 4px
    fontSize: 'var(--ux-font-size-sm)', // 14px
    borderRadius: 'var(--ux-radius-sm)', // 4px
  },
}

Example 3: Responsive Property

// Widget config can have responsive values:
{
  backgroundColor: {
    base: '#3b82f6',
    tablet: '#6366f1',
    mobile: '#8b5cf6',
  }
}

// getWidgetStyle automatically resolves based on breakpoint:
const bgColor = getWidgetStyle(config, 'backgroundColor', 'mobile');
// Returns: '#8b5cf6'

๐Ÿงช Testing

Manual Testing Checklist

Test Scenarios

Scenario 1: Variant Selection

  1. Create a button widget
  2. Select "Primary" variant
  3. Expected: Blue background, white text, rounded corners
  4. Actual: โœ…

Scenario 2: Property Override

  1. Select "Primary" variant
  2. Change backgroundColor to red
  3. Expected: Red background (overrides variant)
  4. Actual: โœ…

Scenario 3: Variant Switch

  1. Select "Primary" variant
  2. Change to "Outline" variant
  3. Expected: Transparent background, blue border
  4. Actual: โœ…

Scenario 4: Persistence

  1. Select variant, save page
  2. Reload page
  3. Expected: Variant still applied
  4. Actual: โœ… (via localStorage)

๐Ÿ“š Related Systems

PropertyBridgeService (Existing)

Location: libs/libs-widgets/widgets/src/lib/services/property-bridge.service.ts

Similar approach but for the old Widget model:

Key Difference: PropertyBridgeService is for the widget-host system (old editor), PropertyTemplatesService is for page-builder (new editor).

WidgetStylesHandlerService (Existing)

Location: libs/libs-widgets/widgets/src/lib/widgets/widget-host/services/widget-styles-handler.service.ts

Direct DOM styling approach:

Key Pattern: We adopted the same pattern in CanvasWidgetComponent.


๐Ÿ”„ Future Enhancements

Phase 2: Backend Variants

// Load variants from backend API
async loadRemoteVariants(spaceId: string): Promise<void> {
  const response = await this.http.get(`/api/spaces/${spaceId}/widget-variants`);
  // Merge with built-in variants
}

Phase 3: User-Created Variants

// Allow users to save custom variants
async saveCustomVariant(
  widgetType: string,
  name: string,
  config: Record<string, unknown>
): Promise<UnifiedVariant> {
  // Save to backend and add to registry
}

Phase 4: AI-Generated Variants

// AI suggests variants based on design context
async generateVariantSuggestions(
  widgetType: string,
  context: string
): Promise<UnifiedVariant[]> {
  // Use AI to generate contextual variants
}

โœ… Status


๐Ÿ“– Documentation


Logged to: Session 12.22
MCP Decision: Architecture decision to reuse existing property systems
Next Steps: User testing, add more widget type variants