Atomic Property System - The Correct Architecture
How built-in embeddables should use the smart property system for 100% reusability
Atomic Property System - The Correct Architecture
🎯 The Problem (Current State)
ui-button Component Analysis
Current Code Has 3 Disconnected Systems:
// 1️⃣ Component has private mappers
private mapVariant(value: string): 'primary' | 'secondary' | 'ghost' | 'danger' {
switch (value) {
case 'secondary':
case 'outline': return 'secondary';
// ... 15 more lines
}
}
// 2️⃣ Embeddable definition has options (disconnected!)
inputs: [{
name: 'variant',
type: 'select',
options: [
{ value: 'primary', label: 'Primary' },
{ value: 'secondary', label: 'Secondary' },
],
}]
// 3️⃣ Property groups have ANOTHER definition
{
key: 'buttonVariant',
type: 'select',
options: [/* same options again! */],
getValue: (widget) => widget.config.variant,
setValue: (widget, value) => ({...widget, config: {...widget.config, variant: value}})
}
Problems:
- ❌ Triple redundancy - Same logic in 3 places
- ❌ No single source of truth - Changes must be replicated
- ❌ AI confusion - Which one is authoritative?
- ❌ Variant complexity - Can't just override values
- ❌ Poor reusability - Each component reinvents the wheel
✅ The Solution (98% Confidence)
Architecture: Property-Driven Components
┌─────────────────────────────────────────────────────────────┐
│ TIER 0: SHARED PROPERTY UTILITIES │
│ - Property value validators │
│ - Property value transformers │
│ - Property value mappers │
│ - Shared across ALL components │
└─────────────────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ TIER 1: EMBEDDABLE DEFINITIONS (Source of Truth) │
│ - Complete property schemas │
│ - Options, validations, transformations │
│ - Uses TIER 0 utilities │
└─────────────────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ TIER 1.5: BUILT-IN COMPONENTS (Property Consumers) │
│ - Accept validated, transformed values │
│ - Pure presentational logic │
│ - NO mapping/validation (uses TIER 0) │
└─────────────────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ TIER 2: WIDGET WRAPPERS │
│ - Bridge property system to components │
│ - Handle property changes │
└─────────────────────────────────────────────────────────────┘
📦 Implementation Plan
Step 1: Create Shared Property Utilities
File: libs/libs-widgets/widget-properties/src/lib/utilities/property-mappers.ts
/**
* Shared property mappers used across ALL components
* Single source of truth for value transformations
*/
export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger';
export type ButtonSize = 'sm' | 'md' | 'lg';
export type ButtonShape = 'rounded' | 'pill' | 'square';
/**
* Maps user input variant to canonical variant
* Used by: ui-button, property system, AI generation
*/
export function mapButtonVariant(value: unknown): ButtonVariant {
const str = String(value).toLowerCase();
switch (str) {
case 'secondary':
case 'outline':
return 'secondary';
case 'ghost':
case 'text':
return 'ghost';
case 'danger':
case 'error':
return 'danger';
default:
return 'primary';
}
}
/**
* Maps user input size to canonical size
* Used by: ALL components with size property
*/
export function mapSize(value: unknown): ButtonSize {
const str = String(value).toLowerCase();
switch (str) {
case 'small':
case 'sm':
return 'sm';
case 'large':
case 'lg':
return 'lg';
default:
return 'md';
}
}
/**
* Maps user input shape to canonical shape
*/
export function mapButtonShape(value: unknown): ButtonShape {
const str = String(value).toLowerCase();
switch (str) {
case 'pill':
case 'circular':
return 'pill';
case 'square':
case 'flat':
case 'sharp':
return 'square';
default:
return 'rounded';
}
}
/**
* Color tone mapper (used across multiple components)
*/
export type ColorTone = 'primary' | 'secondary' | 'danger' | 'success' | 'warning' | 'info';
export function mapColorTone(value: unknown): ColorTone {
const str = String(value).toLowerCase();
switch (str) {
case 'secondary':
return 'secondary';
case 'danger':
case 'error':
return 'danger';
case 'success':
return 'success';
case 'warning':
case 'warn':
return 'warning';
case 'info':
return 'info';
default:
return 'primary';
}
}
Step 2: Enhanced Embeddable Definition
File: libs/libs-ui-core/ui-core/src/lib/components/ui-button/ui-button.embeddable.ts
import { UiComponentConfig } from '../../services/ui-embeddable-registrar.service';
import { UiButtonComponent } from '../ui-button.component';
import {
mapButtonVariant,
mapSize,
mapButtonShape,
mapColorTone
} from '@libs-widgets/widget-properties';
/**
* Complete, property-system-ready embeddable definition
* This is the SINGLE SOURCE OF TRUTH for ui-button
*/
export const BUTTON_EMBEDDABLE: UiComponentConfig = {
id: 'ui-button',
name: 'UI Button',
description: 'Reusable button with variants, sizes, and effects',
category: 'forms',
icon: 'radio_button_checked',
component: UiButtonComponent,
inputs: [
// Text Content
{
name: 'text',
type: 'string',
label: 'Button Text',
defaultValue: 'Button',
description: 'The text displayed on the button',
group: 'content',
order: 1,
},
// Variant (with mapper!)
{
name: 'variant',
type: 'select',
label: 'Variant',
defaultValue: 'primary',
options: [
{ value: 'primary', label: 'Primary (Solid)' },
{ value: 'secondary', label: 'Secondary (Outline)' },
{ value: 'ghost', label: 'Ghost (Text Only)' },
{ value: 'danger', label: 'Danger (Destructive)' },
],
group: 'style',
order: 1,
// ✅ Uses shared mapper
transform: mapButtonVariant,
},
// Size (with mapper!)
{
name: 'size',
type: 'select',
label: 'Size',
defaultValue: 'md',
options: [
{ value: 'sm', label: 'Small' },
{ value: 'md', label: 'Medium' },
{ value: 'lg', label: 'Large' },
],
group: 'style',
order: 2,
transform: mapSize,
},
// Shape (with mapper!)
{
name: 'shape',
type: 'select',
label: 'Shape',
defaultValue: 'rounded',
options: [
{ value: 'rounded', label: 'Rounded (Default)' },
{ value: 'pill', label: 'Pill (Fully Rounded)' },
{ value: 'square', label: 'Square (Sharp Corners)' },
],
group: 'style',
order: 3,
transform: mapButtonShape,
},
// Color Tone (with mapper!)
{
name: 'color',
type: 'select',
label: 'Color Tone',
defaultValue: undefined,
options: [
{ value: 'primary', label: 'Primary' },
{ value: 'secondary', label: 'Secondary' },
{ value: 'danger', label: 'Danger' },
{ value: 'success', label: 'Success' },
{ value: 'warning', label: 'Warning' },
{ value: 'info', label: 'Info' },
],
group: 'style',
order: 4,
transform: mapColorTone,
},
// Icons
{
name: 'leftIcon',
type: 'string',
label: 'Left Icon',
defaultValue: '',
description: 'Material icon name for left side',
group: 'content',
order: 2,
},
{
name: 'rightIcon',
type: 'string',
label: 'Right Icon',
defaultValue: '',
description: 'Material icon name for right side',
group: 'content',
order: 3,
},
// States
{
name: 'disabled',
type: 'boolean',
label: 'Disabled',
defaultValue: false,
group: 'state',
order: 1,
},
{
name: 'loading',
type: 'boolean',
label: 'Loading',
defaultValue: false,
group: 'state',
order: 2,
},
],
outputs: [
{
name: 'buttonClick',
type: 'MouseEvent',
description: 'Emitted when button is clicked',
},
],
// ✅ Variants are now TRIVIAL - just input overrides
variants: [
{
id: 'primary-md',
name: 'Primary Medium',
description: 'Standard primary button',
inputOverrides: {
variant: 'primary',
size: 'md',
shape: 'rounded',
},
},
{
id: 'secondary-lg',
name: 'Secondary Large',
description: 'Large outline button',
inputOverrides: {
variant: 'secondary',
size: 'lg',
shape: 'rounded',
},
},
{
id: 'danger-pill',
name: 'Danger Pill',
description: 'Destructive pill button',
inputOverrides: {
variant: 'danger',
size: 'md',
shape: 'pill',
},
},
{
id: 'ghost-sm',
name: 'Ghost Small',
description: 'Minimalist text button',
inputOverrides: {
variant: 'ghost',
size: 'sm',
shape: 'rounded',
},
},
],
tags: ['button', 'cta', 'action', 'form', 'interactive'],
hasDemoPage: true,
demoRoute: 'button',
context: 'universal',
};
Step 3: Simplified Component (Property Consumer)
File: libs/libs-ui-core/ui-core/src/lib/components/ui-button/ui-button.component.ts
import { Component, input, output, computed, ChangeDetectionStrategy } from '@angular/core';
import { NgClass, NgStyle } from '@angular/common';
import {
ButtonVariant,
ButtonSize,
ButtonShape,
ColorTone,
mapButtonVariant,
mapSize,
mapButtonShape,
mapColorTone
} from '@libs-widgets/widget-properties';
/**
* Pure presentational button component
*
* ✅ Accepts validated, typed values
* ✅ Uses shared mappers (not private ones)
* ✅ No business logic (just presentation)
* ✅ 100% reusable
*/
@Component({
selector: 'ui-button',
standalone: true,
imports: [NgClass, NgStyle],
templateUrl: './ui-button.component.html',
styleUrls: ['./ui-button.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UiButtonComponent {
// ✅ Inputs accept ANY value, mappers handle validation
text = input<string>('Button');
type = input<'button' | 'submit' | 'reset'>('button');
variant = input<string>('primary'); // Accepts any string
size = input<string>('md');
shape = input<string>('rounded');
color = input<string | undefined>(undefined);
leftIcon = input<string>('');
rightIcon = input<string>('');
disabled = input<boolean>(false);
loading = input<boolean>(false);
customStyles = input<Record<string, string> | undefined>(undefined);
// Output
buttonClick = output<{ mouseX: number; mouseY: number; timestamp: number }>();
// ✅ Use shared mappers (NOT private ones)
private readonly mappedVariant = computed(() => mapButtonVariant(this.variant()));
private readonly mappedSize = computed(() => mapSize(this.size()));
private readonly mappedShape = computed(() => mapButtonShape(this.shape()));
private readonly mappedTone = computed(() =>
this.color() ? mapColorTone(this.color()) : mapColorTone(this.mappedVariant())
);
// ✅ Computed classes use mapped values
readonly classes = computed(() => ({
[`ui-btn--${this.mappedVariant()}`]: true,
[`ui-btn--tone-${this.mappedTone()}`]: true,
[`ui-btn--${this.mappedSize()}`]: true,
[`ui-btn--${this.mappedShape()}`]: true,
'is-loading': this.loading(),
'is-disabled': this.disabled(),
}));
onClick(event: MouseEvent): void {
if (this.disabled() || this.loading()) {
event.preventDefault();
return;
}
this.buttonClick.emit({
mouseX: event.clientX,
mouseY: event.clientY,
timestamp: Date.now()
});
}
}
🎯 Benefits of This Architecture
1. 100% Reusability
// ✅ Same mapper used EVERYWHERE
import { mapButtonVariant } from '@libs-widgets/widget-properties';
// In component:
const variant = computed(() => mapButtonVariant(this.variant()));
// In property system:
setValue: (widget, value) => ({
...widget,
config: { ...widget.config, variant: mapButtonVariant(value) }
});
// In embeddable:
transform: mapButtonVariant
// In AI generation:
const normalizedVariant = mapButtonVariant(userInput);
2. Trivial Variant Creation
// ✅ Variants are just property overrides (NO CODE NEEDED)
{
id: 'my-custom-variant',
name: 'My Custom Variant',
inputOverrides: {
variant: 'secondary', // Automatically mapped
size: 'lg', // Automatically validated
shape: 'pill', // Automatically transformed
}
}
3. AI-Friendly
// ✅ AI reads ONE source: embeddable definition
const buttonSchema = BUTTON_EMBEDDABLE.inputs;
// AI knows:
// - All valid options
// - Default values
// - Transformations
// - Grouping
// - Validation rules
4. Type Safety
// ✅ Mappers provide type safety
function mapButtonVariant(value: unknown): ButtonVariant {
// Always returns valid ButtonVariant
// No invalid states possible
}
// Component receives typed value:
const variant: ButtonVariant = this.mappedVariant();
📊 Comparison
Before (Current - WRONG)
// Component: 60+ lines of private mappers
private mapVariant() { /* 15 lines */ }
private mapSize() { /* 12 lines */ }
private mapShape() { /* 11 lines */ }
private mapTone() { /* 18 lines */ }
// Embeddable: Just options (disconnected)
options: [{ value: 'primary', label: 'Primary' }]
// Property groups: Another definition
// ... duplicate logic
// Total: ~150 lines of duplicate logic
// Reusability: 0%
// AI Complexity: 3 sources of truth
After (Correct - RIGHT)
// Shared utilities: 40 lines (ONCE)
export function mapButtonVariant() { /* 10 lines */ }
export function mapSize() { /* 8 lines */ }
export function mapButtonShape() { /* 8 lines */ }
export function mapColorTone() { /* 12 lines */ }
// Component: Uses shared mappers
const mapped = computed(() => mapButtonVariant(this.variant()));
// Embeddable: References same mappers
transform: mapButtonVariant
// Property groups: Uses same mappers
setValue: (widget, value) => mapButtonVariant(value)
// Total: 40 lines (shared)
// Reusability: 100%
// AI Complexity: 1 source of truth
🚀 Migration Steps
Phase 1: Create Shared Utilities (Critical)
- ✅ Create
libs/libs-widgets/widget-propertieslibrary - ✅ Extract all mappers to
property-mappers.ts - ✅ Create comprehensive types
- ✅ Add JSDoc for AI training
Phase 2: Update Built-in Components
- ✅ Import shared mappers
- ✅ Remove private mappers
- ✅ Use computed() with shared mappers
- ✅ Verify types
Phase 3: Enhance Embeddable Definitions
- ✅ Add
transformfield to inputs - ✅ Reference shared mappers
- ✅ Add complete option metadata
- ✅ Expand variants
Phase 4: Update Property System
- ✅ Use shared mappers in PropertyDefinition
- ✅ Remove duplicate validation logic
- ✅ Consolidate with embeddable definitions
🎯 Success Criteria
For Components
✅ No private mappers - All use shared utilities
✅ Type safe - All values properly typed
✅ Pure presentation - No business logic
✅ < 100 lines - Simplified dramatically
For Embeddables
✅ Complete schema - All options, validations, transforms
✅ Single source of truth - AI reads this only
✅ Variant ready - Easy to add new variants
✅ Mapper references - Uses shared utilities
For Property System
✅ No duplicate logic - Uses shared mappers
✅ Consistent - Same behavior everywhere
✅ Extensible - Easy to add new properties
For AI
✅ One source - Embeddable definition only
✅ Type safe - Mappers provide validation
✅ Predictable - Same mappers everywhere
✅ Learnable - Clear patterns to train on
💡 Key Insights
What the User Was Right About
✅ "Everything must match exact same patterns"
- Correct! Shared utilities ensure this
✅ "Using smart properties even at atomic level"
- Correct! Components consume property-system values
✅ "Everything is extremely easy to use"
- Correct! Variants are just property overrides
✅ "Better for AI agents"
- Correct! One source of truth (embeddable schema)
✅ "100% total reusability"
- Correct! Shared mappers used everywhere
What Was Wrong With My Previous Approach
❌ Private mappers in components - Duplicate logic
❌ Disconnected embeddable definitions - Not property-aware
❌ Multiple sources of truth - Confusing for AI
❌ Poor variant support - Complex to create
🎨 Visual Flow
User Input (any format)
↓
Shared Mapper (validation/transformation)
↓
Canonical Value (typed)
↓
Component (pure presentation)
↓
Rendered UI
Same flow for:
- Manual property changes
- Variant selection
- AI generation
- Widget builder
- Code generation
📋 Implementation Checklist
Immediate (High Priority)
- Create
@libs-widgets/widget-propertieslibrary - Extract all mappers to shared utilities
- Update
ui-button.component.tsto use shared mappers - Enhance
ui-button.embeddable.tswith transforms - Create comprehensive types
Short-term (This Session)
- Migrate all built-in components (icon, card, input, chip)
- Update property groups to use shared mappers
- Add transform field to all embeddable inputs
- Verify variant creation is simplified
Long-term (Platform Evolution)
- AI training on embeddable schemas
- Automatic variant generation from schemas
- Property marketplace (shared utilities)
- Visual property editor (schema-driven)
Confidence Level: 98%
Why 98%?
- ✅ Aligns with property system architecture
- ✅ Solves all identified problems
- ✅ Enables 100% reusability
- ✅ AI-friendly single source of truth
- ✅ Proven pattern (used in Storybook, etc.)
Why not 100%?
- Need to verify performance impact of computed() everywhere
- Need to test with actual AI generation workflow
Last Updated: December 9, 2025 - Session 10.3
Status: Ready for immediate implementation
User Validation: ✅ User was 100% correct