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:
- Widget Guide = Core UI Widgets + Variant Creator
- Widget Gallery = Custom Widgets only
📋 Implementation Checklist
✅ Phase 1: Flash Persistence (Priority 1)
Files to modify:
widget-builder-workspace.component.tswidget-storage.service.ts
Steps:
- 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;
};
}
- 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);
});
- 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:
- Remove custom widgets from guide
- Add variant creator to widget-demo component
- Store variants in storage-system with keys:
widget-variants:${userId}:${baseWidgetId}:${variantId}
Widget Gallery Changes:
- Only show custom widgets (from widget-builder)
- Filter out core UI widgets
- Add "Create from Core UI Widget" button
🎯 Priority Order
Flash Persistence (1-2 days)
- Critical for UX
- Easy to implement
- Immediate value
Three-Panel Layout (2-3 days)
- Major UX improvement
- Foundation for other features
- Visible progress
Component Palette (2-3 days)
- Makes sidebar useful
- Enables drag-and-drop
- Connects to Core UI widgets
Grid-Based Visual Designer (3-4 days)
- Most complex
- Inspired by modular-universe
- Requires grid-canvas adaptation
Core UI vs Custom Separation (2-3 days)
- Clean architecture
- Better organization
- Variant system foundation
📝 Testing Checklist
Flash Persistence
- Active tab persists across page refresh
- Sidebar collapse state persists
- Property group expansion persists
- Selected variant persists
- Canvas zoom persists
- Breakpoint selection persists
Three-Panel Layout
- Sidebar collapses/expands smoothly
- Live panel collapses/expands smoothly
- Canvas resizes properly
- All panels work on mobile (stack vertically)
Component Palette
- Drag component to canvas works
- Search filters correctly
- Sections expand/collapse
- Variant count displays
- Custom widgets appear
Grid-Based Designer
- Can add components to grid
- Can resize components
- Can reorder components
- Grid snapping works
- Responsive preview works
Next: Start with Flash Persistence (easiest win)