diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 6b8bc26..2fde283 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -31,6 +31,10 @@ const coreRoutes: Routes = [ { path: 'platform', loadComponent: () => import('./pages/public/platform-home.component').then(m => m.PlatformHomeComponent) + }, + { + path: 'builder-sandbox', + loadComponent: () => import('./pages/builder/builder-sandbox.component').then(m => m.BuilderSandboxComponent) } ]; diff --git a/src/app/facades/builder/builder-config.facade.ts b/src/app/facades/builder/builder-config.facade.ts new file mode 100644 index 0000000..4d1dd44 --- /dev/null +++ b/src/app/facades/builder/builder-config.facade.ts @@ -0,0 +1,52 @@ +import { Injectable, computed, signal } from '@angular/core'; +import { take } from 'rxjs/operators'; +import { BootstrapConfig } from '../../shared/models/config'; +import { ConfigService } from '../../core/config/config.service'; + +@Injectable({ providedIn: 'root' }) +export class BuilderConfigFacade { + private readonly bootstrapState = signal(null); + + readonly bootstrap = this.bootstrapState.asReadonly(); + readonly branding = computed(() => this.bootstrapState()?.branding ?? null); + readonly featureFlags = computed(() => this.bootstrapState()?.featureFlags ?? null); + + constructor(private readonly configService: ConfigService) {} + + load(): void { + this.configService.loadBootstrap().pipe(take(1)).subscribe({ + next: (config) => this.bootstrapState.set(JSON.parse(JSON.stringify(config)) as BootstrapConfig), + error: () => this.bootstrapState.set(null) + }); + } + + updateBrandName(nextBrandName: string): void { + const current = this.bootstrapState(); + if (!current) { + return; + } + + this.bootstrapState.set({ + ...current, + branding: { + ...current.branding, + brandName: nextBrandName + } + }); + } + + setFeatureFlag(flag: string, enabled: boolean): void { + const current = this.bootstrapState(); + if (!current) { + return; + } + + this.bootstrapState.set({ + ...current, + featureFlags: { + ...current.featureFlags, + [flag]: enabled + } + }); + } +} diff --git a/src/app/pages/builder/builder-sandbox.component.ts b/src/app/pages/builder/builder-sandbox.component.ts new file mode 100644 index 0000000..969ec98 --- /dev/null +++ b/src/app/pages/builder/builder-sandbox.component.ts @@ -0,0 +1,67 @@ +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BuilderConfigFacade } from '../../facades/builder/builder-config.facade'; + +@Component({ + selector: 'app-builder-sandbox', + standalone: true, + imports: [CommonModule, FormsModule], + template: ` +
+

Builder Sandbox

+

Configuration-only editing surface.

+ + @if (branding(); as brand) { +
+

Branding

+ +
+ } + + @if (featureFlags(); as flags) { +
+

Feature Flags

+ @for (flag of featureFlagKeys(); track flag) { + + } +
+ } +
+ `, + styles: [ + ` + .builder-sandbox { padding: 1.5rem; display: grid; gap: 1rem; } + .builder-card { border: 1px solid var(--border-color, #d3dad9); border-radius: 12px; padding: 1rem; display: grid; gap: 0.75rem; } + input[type='text'] { width: 100%; max-width: 420px; padding: 0.5rem; border-radius: 8px; border: 1px solid var(--border-color, #d3dad9); } + .flag-row { display: flex; align-items: center; gap: 0.5rem; } + ` + ], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class BuilderSandboxComponent { + private readonly facade = inject(BuilderConfigFacade); + + readonly branding = this.facade.branding; + readonly featureFlags = this.facade.featureFlags; + readonly featureFlagKeys = computed(() => Object.keys(this.featureFlags() ?? {}).sort()); + + constructor() { + this.facade.load(); + } + + onBrandNameChange(value: string): void { + this.facade.updateBrandName(value); + } + + onFlagToggle(flag: string, event: Event): void { + const input = event.target as HTMLInputElement; + this.facade.setFeatureFlag(flag, input.checked); + } +}