Widget Enhancement - Implementation Guide

Step-by-step guide for implementing the widget system enhancements

🚀 Widget Enhancement - Implementation Guide

Prerequisites: Read Widget System Architecture Master Plan


🎯 Summary of Changes

Key Issues Addressed

Issue 1: Persistence (Flash-Persistence Missing)

Problem: UI state not persisted (active tabs, collapsed panels, etc.)
Solution: Complete WidgetBuilderWorkspaceState with UI state tracking

Issue 2.1: Sidebar Navigation Not Useful

Problem: Static list of widget types, not actionable
Solution: Component palette with drag-and-drop, grouped by category, shows variants

Issue 2.2: Design Tab Not Creative

Problem: Can't design visually, just stacking widgets
Solution: Grid-based canvas with modes (design, property-bind, flow-attach, preview)

Issue 2.3: Property Groups Not Visible

Problem: Configuration hidden in tabs, no live preview
Solution: Always-visible live panel showing preview + current configuration

Issue 2.4: Core UI vs Custom Widgets Confusion

Problem: Everything mixed together
Solution:


📋 Implementation Checklist

✅ Phase 1: Flash Persistence (Priority 1)

Files to modify:

Steps:

  1. Extend WidgetBuilderWorkspaceState interface:
// Add to widget-builder-workspace.component.ts
interface WidgetBuilderWorkspaceState {
  // ... existing fields ...
  
  // NEW: UI State
  uiState: {
    activePanel: 'design' | 'properties' | 'variants' | 'code' | 'preview';
    sidebarCollapsed: boolean;
    livePanelCollapsed: boolean;
    expandedPropertyGroups: string[];
    expandedSidebarSections: string[];
    selectedComponentId: string | null;
    activeVariantId: string | null;
    codeViewMode: 'component' | 'template' | 'styles' | 'all';
    previewBreakpoint: 'base' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
    canvasZoom: number;
  };
}
  1. Add effect for auto-save with UI state:
private autoSaveEffect = effect(() => {
  const state: WidgetBuilderWorkspaceState = {
    widgetId: this.widgetId(),
    widgetName: this.widgetName(),
    widgetType: this.widgetType(),
    widgetDescription: this.widgetDescription(),
    propertyGroups: this.propertyGroups(),
    propertyValues: this.propertyValues(),
    variants: this.variants(),
    
    // NEW: Capture UI state
    uiState: {
      activePanel: this.activePanel(),
      sidebarCollapsed: this.sidebarCollapsed(),
      livePanelCollapsed: this.livePanelCollapsed(),
      expandedPropertyGroups: this.expandedPropertyGroups(),
      expandedSidebarSections: this.expandedSidebarSections(),
      selectedComponentId: this.selectedComponentId(),
      activeVariantId: this.selectedVariantId(),
      codeViewMode: this.codeViewMode(),
      previewBreakpoint: this.previewBreakpoint(),
      canvasZoom: this.canvasZoom(),
    },
    
    lastSaved: Date.now(),
    lastModified: Date.now()
  };
  
  // Flash persistence
  this.storage.set(`widget-builder:workspace:${this.widgetId()}`, state);
});
  1. Restore UI state on load:
async ngOnInit() {
  const widgetId = this.route.snapshot.queryParams['widgetId'];
  
  if (widgetId) {
    const state = await this.storage.get<WidgetBuilderWorkspaceState>(
      `widget-builder:workspace:${widgetId}`
    );
    
    if (state?.uiState) {
      // Restore UI state
      this.activePanel.set(state.uiState.activePanel);
      this.sidebarCollapsed.set(state.uiState.sidebarCollapsed);
      this.livePanelCollapsed.set(state.uiState.livePanelCollapsed);
      // ... restore all UI state
    }
  }
}

🎨 Phase 2: Three-Panel Layout (Priority 2)

Goal: [Sidebar] [Canvas] [Live Panel] layout

Template structure:

<div class="widget-builder-workspace">
  <header class="workspace-header">
    <!-- Existing header -->
  </header>

  <div class="workspace-body">
    <!-- LEFT: Compact Sidebar -->
    <aside 
      class="workspace-sidebar" 
      [class.collapsed]="sidebarCollapsed()">
      <!-- Component Palette (NEW) -->
      <lib-component-palette
        [coreWidgets]="coreWidgets()"
        [customWidgets]="customWidgets()"
        [flows]="availableFlows()"
        (componentSelected)="onComponentSelected($event)">
      </lib-component-palette>
      
      <!-- Quick Stats -->
      <div class="quick-stats">
        <div class="stat">
          <i class="fas fa-sliders-h"></i>
          <span>{{ propertyGroups().length }} Property Groups</span>
        </div>
        <div class="stat">
          <i class="fas fa-layer-group"></i>
          <span>{{ variants().length }} Variants</span>
        </div>
      </div>
      
      <!-- Quick Navigation -->
      <div class="quick-nav">
        <button (click)="activePanel.set('properties')">
          <i class="fas fa-cog"></i> Properties →
        </button>
        <button (click)="activePanel.set('variants')">
          <i class="fas fa-palette"></i> Variants →
        </button>
      </div>
    </aside>

    <!-- CENTER: Main Canvas -->
    <main class="workspace-canvas">
      <!-- Mode Selector (inspired by modular-universe) -->
      <div class="canvas-toolbar">
        <div class="mode-selector">
          <button 
            *ngFor="let mode of modes"
            [class.active]="canvasMode() === mode.id"
            (click)="setCanvasMode(mode.id)">
            <i [class]="mode.icon"></i>
            <span>{{ mode.label }}</span>
          </button>
        </div>
      </div>

      <!-- Canvas Content (mode-dependent) -->
      <div class="canvas-content">
        @if (canvasMode() === 'design') {
          <lib-widget-visual-designer
            [gridConfig]="gridConfig()"
            [components]="designComponents()"
            (componentsChanged)="onDesignChanged($event)">
          </lib-widget-visual-designer>
        }
        
        @if (canvasMode() === 'property-bind') {
          <lib-property-binding-canvas
            [components]="designComponents()"
            [propertyGroups]="propertyGroups()"
            (bindingsChanged)="onBindingsChanged($event)">
          </lib-property-binding-canvas>
        }
        
        @if (canvasMode() === 'flow-attach') {
          <lib-widget-flow-designer
            [widgetId]="widgetId()"
            [components]="designComponents()"
            (flowBindingsChange)="onFlowBindingsChange($event)">
          </lib-widget-flow-designer>
        }
        
        @if (canvasMode() === 'preview') {
          <!-- Large preview mode -->
        }
      </div>
    </main>

    <!-- RIGHT: Live Panel (Always Visible) -->
    <aside 
      class="live-panel" 
      [class.collapsed]="livePanelCollapsed()">
      <!-- Live Preview (always at top) -->
      <div class="live-preview-section">
        <h3>Live Preview</h3>
        <lib-widget-live-preview
          [widget]="buildPreviewWidget()"
          [breakpoint]="previewBreakpoint()"
          [zoom]="canvasZoom()">
        </lib-widget-live-preview>
        
        <!-- Breakpoint selector -->
        <div class="breakpoint-selector">
          <button 
            *ngFor="let bp of breakpoints"
            [class.active]="previewBreakpoint() === bp"
            (click)="previewBreakpoint.set(bp)">
            <i [class]="bp.icon"></i>
          </button>
        </div>
      </div>

      <!-- Configuration Tabs -->
      <div class="config-tabs">
        <button 
          [class.active]="configTab() === 'properties'"
          (click)="configTab.set('properties')">
          Properties
        </button>
        <button 
          [class.active]="configTab() === 'variants'"
          (click)="configTab.set('variants')">
          Variants
        </button>
      </div>

      <!-- Config Content -->
      <div class="config-content">
        @if (configTab() === 'properties') {
          <!-- Show selected component's properties -->
          <lib-property-editor
            [propertyGroups]="propertyGroups()"
            [selectedComponentId]="selectedComponentId()"
            (propertyChanged)="onPropertyChanged($event)">
          </lib-property-editor>
        }
        
        @if (configTab() === 'variants') {
          <!-- Variant list with quick edit -->
          <lib-variant-list
            [variants]="variants()"
            [activeVariantId]="activeVariantId()"
            (variantSelected)="selectVariant($event)"
            (variantEdited)="updateVariant($event)">
          </lib-variant-list>
        }
      </div>

      <!-- Quick Info Footer -->
      <div class="quick-info">
        <div class="info-item">
          <i class="fas fa-save"></i>
          Last saved: {{ lastSavedTime() }}
        </div>
        <div class="info-item">
          <i class="fas fa-clock"></i>
          Auto-save: {{ autoSaveEnabled() ? 'ON' : 'OFF' }}
        </div>
      </div>
    </aside>
  </div>
</div>

🎨 Phase 3: Component Palette (Priority 3)

Create new component: component-palette.component.ts

@Component({
  selector: 'lib-component-palette',
  standalone: true,
  template: `
    <div class="component-palette">
      <!-- Search -->
      <div class="palette-search">
        <input 
          type="text" 
          placeholder="Search components..."
          [value]="searchTerm()"
          (input)="searchTerm.set($any($event.target).value)">
      </div>

      <!-- Core UI Widgets -->
      <div class="palette-section">
        <button 
          class="section-header"
          (click)="toggleSection('core-widgets')">
          <i class="fas" [class.fa-chevron-down]="isSectionExpanded('core-widgets')" [class.fa-chevron-right]="!isSectionExpanded('core-widgets')"></i>
          🎨 Core UI Widgets
        </button>
        
        @if (isSectionExpanded('core-widgets')) {
          <div class="palette-items">
            @for (category of coreWidgetCategories(); track category.id) {
              <div class="category-group">
                <div class="category-label">{{ category.label }}</div>
                @for (widget of category.widgets; track widget.id) {
                  <div 
                    class="palette-item"
                    draggable="true"
                    (dragstart)="onDragStart($event, widget)">
                    <i [class]="widget.icon"></i>
                    <span>{{ widget.label }}</span>
                    @if (widget.variantCount > 0) {
                      <span class="variant-badge">{{ widget.variantCount }}</span>
                    }
                  </div>
                }
              </div>
            }
          </div>
        }
      </div>

      <!-- Custom Widgets -->
      <div class="palette-section">
        <button 
          class="section-header"
          (click)="toggleSection('custom-widgets')">
          <i class="fas" [class.fa-chevron-down]="isSectionExpanded('custom-widgets')" [class.fa-chevron-right]="!isSectionExpanded('custom-widgets')"></i>
          🏗️ Custom Widgets
        </button>
        
        @if (isSectionExpanded('custom-widgets')) {
          <div class="palette-items">
            @for (widget of customWidgets(); track widget.id) {
              <div 
                class="palette-item custom"
                draggable="true"
                (dragstart)="onDragStart($event, widget)">
                <i class="fas fa-puzzle-piece"></i>
                <span>{{ widget.name }}</span>
              </div>
            }
          </div>
        }
      </div>

      <!-- Singularity Flows -->
      <div class="palette-section">
        <button 
          class="section-header"
          (click)="toggleSection('flows')">
          <i class="fas" [class.fa-chevron-down]="isSectionExpanded('flows')" [class.fa-chevron-right]="!isSectionExpanded('flows')"></i>
          ⚡ Singularity Flows
        </button>
        
        @if (isSectionExpanded('flows')) {
          <div class="palette-items">
            @for (flow of availableFlows(); track flow.id) {
              <div 
                class="palette-item flow"
                draggable="true"
                (dragstart)="onDragStart($event, flow)">
                <i class="fas fa-project-diagram"></i>
                <span>{{ flow.name }}</span>
              </div>
            }
          </div>
        }
      </div>
    </div>
  `
})
export class ComponentPaletteComponent {
  coreWidgets = input.required<CoreWidget[]>();
  customWidgets = input.required<CustomWidget[]>();
  flows = input.required<Flow[]>();
  
  componentSelected = output<{ type: string; id: string }>();
  
  searchTerm = signal('');
  expandedSections = signal<string[]>(['core-widgets']);
  
  coreWidgetCategories = computed(() => {
    const widgets = this.coreWidgets();
    // Group by category: Basics, Forms, Feedback, etc.
    return groupWidgetsByCategory(widgets);
  });
  
  toggleSection(sectionId: string) {
    this.expandedSections.update(sections => 
      sections.includes(sectionId)
        ? sections.filter(s => s !== sectionId)
        : [...sections, sectionId]
    );
  }
  
  isSectionExpanded(sectionId: string): boolean {
    return this.expandedSections().includes(sectionId);
  }
  
  onDragStart(event: DragEvent, item: any) {
    event.dataTransfer!.setData('component', JSON.stringify(item));
  }
}

🔄 Phase 4: Separate Core UI vs Custom Widgets

Widget Guide Changes:

  1. Remove custom widgets from guide
  2. Add variant creator to widget-demo component
  3. Store variants in storage-system with keys:
    widget-variants:${userId}:${baseWidgetId}:${variantId}
    

Widget Gallery Changes:

  1. Only show custom widgets (from widget-builder)
  2. Filter out core UI widgets
  3. Add "Create from Core UI Widget" button

🎯 Priority Order

  1. Flash Persistence (1-2 days)

    • Critical for UX
    • Easy to implement
    • Immediate value
  2. Three-Panel Layout (2-3 days)

    • Major UX improvement
    • Foundation for other features
    • Visible progress
  3. Component Palette (2-3 days)

    • Makes sidebar useful
    • Enables drag-and-drop
    • Connects to Core UI widgets
  4. Grid-Based Visual Designer (3-4 days)

    • Most complex
    • Inspired by modular-universe
    • Requires grid-canvas adaptation
  5. Core UI vs Custom Separation (2-3 days)

    • Clean architecture
    • Better organization
    • Variant system foundation

📝 Testing Checklist

Flash Persistence

Three-Panel Layout

Component Palette

Grid-Based Designer


Next: Start with Flash Persistence (easiest win)