# GbWizard Component — In-Depth Analysis

**Files Analyzed:**
- `features/gbwizard/gbwizard.component.ts` (~204 lines)
- `features/gbwizard/gbwizard.component.html` (66 lines)
- `features/gbwizard/gbwizard.component.scss` (308 lines)
- `features/gbwizard/gbwizard-config.service.ts` (59 lines)
- `features/gbwizard/gbwizard.component.spec.ts` (empty)
- `projects/gbhost/public/wizardjson/wizardmanpowerrequest.json` (34 lines)
- `projects/recruitment/transaction/planning/manpowerrequest/manpowerrequest.component.ts` (consumer — 274 lines)
- `projects/recruitment/transaction/planning/manpowerrequest/manpowerrequest.component.html` (consumer — 232 lines)

**Purpose:** General-purpose multi-step wizard component. Currently used in one place (ManPowerRequest).
**Analysis Date:** 2026-02-24

---

## Overall Assessment

The wizard has a reasonable UI shell but is **architecturally incomplete**. Critical features expected of a reusable wizard — step validation hooks, initial step emission, programmatic control API — are missing or broken. The `loadStepContent` / `ViewContainerRef` infrastructure is entirely dead code. Consumers are forced to duplicate wizard state and logic to work around these gaps.

---

## Issues

### 🔴 Critical (P0)

#### C1. `ChangeDetectionStrategy.OnPush` Missing from `@Component` Decorator
**File:** `gbwizard.component.ts:35`

The `@Component` decorator has no `changeDetection` property. The component runs with **Default CD** — triggered on every DOM event, interval timer, and promise resolution across the entire app. For a wizard that hosts complex form steps (picklists, grids, etc.), Default CD is expensive.

```typescript
// Current — missing entirely:
@Component({ selector: 'gb-wizard', standalone: true, ... })

// Fix:
@Component({
  selector: 'gb-wizard',
  standalone: true,
  imports: [CommonModule, MatIconModule],
  templateUrl: './gbwizard.component.html',
  styleUrl: './gbwizard.component.scss',       // also fix to singular
  changeDetection: ChangeDetectionStrategy.OnPush
})
```

---

#### C2. `[innerHTML]` Binding on Untrusted `tag` Field — XSS Risk
**File:** `gbwizard.component.html:35`

```html
<div *ngIf="wizardSteps[currentStepIndex]?.tag" [innerHTML]="wizardSteps[currentStepIndex]?.tag">
</div>
```

The `tag` value comes from `step.tags` in the JSON config. If wizard JSON is ever sourced from a server or user-configurable source, this is an XSS vector. Angular's built-in sanitizer strips most but not all dangerous patterns. Per CLAUDE.md, DOMPurify must be used for any HTML rendering.

The `tag` field is also semantically unclear — what is it? An HTML snippet? Inline help text? This needs a defined purpose or removal.

**Fix:** Either pipe through DOMPurify before binding, or redesign `tag` as plain text only (use `textContent` binding instead of `[innerHTML]`).

---

#### C3. `sessionStorage.getItem('LoginDTO')` in Consumer Constructor — P0 Security
**File:** `manpowerrequest.component.ts:81`

```typescript
this.LoginDTO = JSON.parse(sessionStorage.getItem('LoginDTO') as any)
console.log("this.LoginDTO", this.LoginDTO)  // also logs sensitive data
```

This reads sensitive session data from sessionStorage — a P0 violation per CLAUDE.md. It also logs the full `LoginDTO` to console (including user credentials/IDs). Fix: inject `GbAppStateService` or auth service to access user context.

---

#### C4. Nested Subscribes Without `takeUntil` in Consumer — Permanent Memory Leak
**File:** `manpowerrequest.component.ts:153–166`

```typescript
this.service.BizTransactionClassService(...)
    .subscribe((BizTransactionSelectlist: any) => {   // no takeUntil
        this.service.BizTransactionType(this.bizTransactionTypeId)
            .subscribe((response: any) => {           // nested, no takeUntil
                ...
            });
    });
```

Two nested raw `.subscribe()` calls with no `takeUntil(this.destroy$)`. Since this component is a form (opened repeatedly in dialogs), each open creates a permanent subscription pair. These never clean up, accumulating over the application lifetime.

---

### 🟠 High Priority — Functional Gaps

#### H1. `onFormGroupChange` Never Emitted for the Initial Step
**File:** `gbwizard.component.ts:74–78`, `117–123`

The wizard emits `onFormGroupChange` only inside `goToStep()`. Neither `ngAfterViewInit` nor `initializeFromConfig` emits it for step 0. The consumer is forced to hardcode the initial step name:

```typescript
// manpowerrequest.component.ts:72
activeFormGroupName: string = 'basicDetails';  // duplicates JSON config knowledge
```

This breaks the single-source-of-truth principle — the consumer must know the first step's `stepformgroupname` without being told by the wizard. If the JSON changes which step comes first, the consumer silently renders the wrong content.

**Fix:** Emit `onFormGroupChange` with step 0's `stepformgroupname` from `ngAfterViewInit` (after `loadStepContent(0)`), or emit it in `initializeFromConfig`.

---

#### H2. No Step Validation Hook — `Next` Advances Without Validation
**File:** `gbwizard.component.ts:170–176`

```typescript
next() {
    if (this.currentStepIndex < this.wizardSteps.length - 1) {
        this.wizardSteps[this.currentStepIndex].isCompleted = true;
        this.wizardSteps[this.currentStepIndex + 1].isDisabled = false;
        this.goToStep(this.currentStepIndex + 1);
    }
}
```

Pressing Next unconditionally advances regardless of whether the current step's form is valid. For a general-purpose wizard, this is a critical functional gap. There is no:
- `@Input() canProceed: boolean` for the parent to block advancement
- `@Output() beforeStepChange: EventEmitter<StepChangeEvent>` with a cancellable event
- `validate()` method parents can implement

**Fix:** Add `@Input() canAdvance: boolean = true`. In `next()`: `if (!this.canAdvance) return;`. Or use an `@Output() onBeforeNext` event emitter where the parent can prevent advancement.

---

#### H3. `loadStepContent()` Is a Non-Functional Stub
**File:** `gbwizard.component.ts:117–123`

```typescript
loadStepContent(stepIndex: number) {
    if (!this.dynamicContent) return;
    this.clearDynamicContent();
    const step = this.wizardSteps[stepIndex];   // ← variable declared but NEVER used
}
```

The method is called on every step navigation but does nothing except clear the ViewContainerRef. The `step` variable is read but discarded. The `componentRefs: ComponentRef<any>[]` array (line 46) and the entire `@ViewChild('dynamicContent')` ViewContainerRef are dead infrastructure — never populated.

The actual content rendering is driven entirely by `<ng-content>` + `*ngIf` on the consumer side. This means the wizard was originally designed for dynamic component injection (a more powerful API) but was never implemented. The dead infrastructure is misleading.

**Fix options:**
- **Option A (remove):** Delete `componentRefs`, `clearDynamicContent()`, and the `#dynamicContent` ViewChild. Acknowledge the `<ng-content>` pattern as the intended API.
- **Option B (implement):** Implement true dynamic component injection via `@Input() stepComponents: Type<unknown>[]` and `this.dynamicContent.createComponent(step)`.

---

#### H4. Config Input Setter Overwrites Separately Bound `@Input()` Properties
**File:** `gbwizard.component.ts:85–115`

```typescript
initializeFromConfig(config: WizardConfig) {
    if (config.nextButtonText) {
        this.nextButtonText = config.nextButtonText;   // ← overwrites @Input()
    }
    ...
}
```

If a consumer binds `[nextButtonText]="'Save & Continue'"` separately from `[config]="wizardConfig"`, Angular processes `@Input()` setters in template declaration order. If `config` is bound before `nextButtonText` in the template, the config setter fires first and sets `nextButtonText` — then the explicit binding overrides it correctly. But if `config` is rebound dynamically after init (e.g. async load), the setter fires again and clobbers the explicit binding.

**Fix:** Do not write to `@Input()` properties from inside another setter. Read config values with fallback to the `@Input()`:
```typescript
get resolvedNextButtonText(): string {
  return this._config?.nextButtonText || this.nextButtonText;
}
```

---

#### H5. `'null'` String Comparison Bug in Config Service
**File:** `gbwizard-config.service.ts:36–37`

```typescript
tag: step.tags !== 'null' ? step.tags : undefined,
stepstyles: step.styles !== 'null' ? this.parseStyles(step.styles) : undefined
```

This checks for the string `'null'` (6 characters), not the value `null`. If the JSON has `"tags": null` (a proper JSON null value), then `step.tags === null`, and `null !== 'null'` is `true` — so `tag` is set to `null`. In the template, `*ngIf="wizardSteps[currentStepIndex]?.tag"` treats `null` as falsy (correct), but `[innerHTML]="null"` would render "null" as text in other cases. The `parseStyles(null)` call would then fail with a type error since `parseStyles` expects a string.

**Fix:** `step.tags ?? undefined` (nullish coalescing — handles both `null` and `undefined`).

---

#### H6. Consumer Duplicates All Step Content (~130 Lines)
**File:** `manpowerrequest.component.html:27–115` (wizard mode) and `135–224` (form mode)

All four steps' form field markup is copy-pasted verbatim between wizard mode and form mode. A change to any field must be made in two places. This is a structural design issue: the wizard should render `<ng-content>` with content projection regardless of "wizard vs form" mode. The parent should not need two separate copies.

**Fix:** Use a single set of step content templates with `<ng-template>`. Toggle the wizard wrapper visibility via CSS or a single flag. The content renders once, projected into `<ng-content>`.

---

#### H7. Consumer Maintains Duplicate Navigation State
**File:** `manpowerrequest.component.ts:44–71, 185–218`

The consumer defines its own `steps[]` array, `CurrentFormStep` counter, `nextStep()`, `previousStep()`, and `goToStep()` — all parallel to the wizard's internal state. The wizard emits events (`onStepChange`, `onFormGroupChange`) but the consumer ignores `onStepChange` entirely and has its own step counter. The two can desync.

This also reveals that the wizard's `goToStep()` API doesn't expose a way for the parent to programmatically drive step navigation — there's no `@ViewChild` or signal-based control.

**Fix:** The consumer should be a thin wrapper that only handles form data. All step navigation state belongs in the wizard. Add a `currentStep` signal output or provide a wizard controller interface.

---

### 🟡 Medium Priority

#### M1. All Default String Values Are Hardcoded English — i18n Violation
**Files:** `gbwizard.component.ts:61–63`, `gbwizard.component.html:57`

```typescript
@Input() nextButtonText: string = 'Next Step';
@Input() previousButtonText: string = 'Previous';
@Input() submitButtonText: string = 'Submit';
```
```html
<button ... (click)="cancel()">Cancel</button>   <!-- hardcoded, no @Input -->
```

The "Cancel" button has no `@Input()` override at all. The "Step X of Y" label (template line 60) has hardcoded "Step" and "of" text.

**Fix:**
- Add `@Input() cancelButtonText: string` with a Transloco key default
- Template: `{{ 'wizard.actions.next' | transloco }}` (with `@Input()` override taking precedence)
- `Step {{ currentStepIndex + 1 }} of {{ wizardSteps.length }}` → use Transloco ICU: `{{ 'wizard.stepCounter' | transloco: { current: currentStepIndex + 1, total: wizardSteps.length } }}`

---

#### M2. `.wizard-content` Has Fixed `width: 920px` and `height: 300px` — Responsive Violation
**File:** `gbwizard.component.scss:147–152`

```scss
.wizard-content {
  width: 920px;
  max-width: 920px;
  max-height: 300px;
  height: 300px;
  overflow-y: scroll;   // always shows scrollbar even when not needed
}
```

`920px` wide + `300px` tall is a hardcoded box that breaks in every viewport under 1000px wide and clips content in almost every real form. Per CLAUDE.md, hardcoded viewport offsets are prohibited — use `dvh` or CSS custom properties.

Also mirrored in the consumer: `manpowerrequest.component.html:131` repeats `width: 920px; max-width: 920px; max-height: 350px; height: 350px` inline. Inline styles for layout are an anti-pattern.

**Fix:**
```scss
.wizard-content {
  flex: 1;
  min-height: 200px;
  max-height: 70dvh;
  overflow-y: auto;   // only shows scrollbar when needed
}
```

---

#### M3. `*ngFor` on `wizardSteps` Has No `trackBy`
**File:** `gbwizard.component.html:10`

```html
<ng-container *ngFor="let step of wizardSteps; let i = index; let last = last">
```

Without `trackBy`, Angular destroys and recreates all step DOM elements on every CD cycle that causes `wizardSteps` reference change (e.g. on `initializeFromConfig` which creates a new array). With Default CD (issue C1) this happens frequently.

**Fix:** `*ngFor="let step of wizardSteps; trackBy: trackByStepNumber"` and add:
```typescript
trackByStepNumber(_: number, step: WizardStep): number {
  return step.stepNumber;
}
```

---

#### M4. Method Calls in Template Execute on Every CD Cycle
**File:** `gbwizard.component.html:33, 43, 47, 50`

```html
[ngStyle]="getCurrentStepStyles()"   <!-- method call -->
*ngIf="!isLastStep()"                <!-- method call -->
*ngIf="isLastStep()"                 <!-- method call -->
[disabled]="isFirstStep()"           <!-- method call -->
*ngIf="!isFirstStep()"               <!-- method call -->
```

Five method calls on every change detection cycle. With Default CD, this runs on every keystroke in any child form.

**Fix:** Convert to getters or Angular signals:
```typescript
get isFirst(): boolean { return this.currentStepIndex === 0; }
get isLast(): boolean { return this.currentStepIndex === this.wizardSteps.length - 1; }
get currentStepStyles(): Record<string, string> { return this.wizardSteps[this.currentStepIndex]?.stepstyles ?? {}; }
```

---

#### M5. No Error Handling in Config Service HTTP Call
**File:** `gbwizard-config.service.ts:17–21`

```typescript
loadWizardConfig(jsonPath: string): Observable<WizardConfig> {
  return this.http.get<any>(jsonPath).pipe(
    map(jsonData => this.transformJsonToWizardConfig(jsonData))
  );
}
```

No `catchError`. If the JSON file is missing or network fails, the observable errors and the wizard silently renders nothing (or throws an unhandled error). Add `catchError` that logs the error and returns a default empty config.

---

#### M6. No Config Caching in Service
**File:** `gbwizard-config.service.ts:17`

Each component instantiation calls `loadWizardConfig()`, making a new HTTP request. Static JSON configs should be cached. If a module opens 10 wizard instances (e.g. in a list), 10 identical HTTP requests are made.

**Fix:** Use a `Map<string, Observable<WizardConfig>>` cache with `shareReplay(1)`.

---

#### M7. `any` Types in Config Service — Violates No-`any` Rule
**File:** `gbwizard-config.service.ts:18, 23, 31`

```typescript
loadWizardConfig(jsonPath: string): Observable<WizardConfig>  // ok
  return this.http.get<any>(jsonPath)...                       // ← any
private transformJsonToWizardConfig(jsonData: any): WizardConfig  // ← any
  steps: jsonData.steps.map((step: any) => ...                // ← any
```

Per CLAUDE.md, no `any`. Define a `WizardConfigJson` interface:
```typescript
interface WizardStepJson {
  stepno?: string;
  stepTitle: string;
  stepSubtitle?: string;
  stepformgroupname?: string;
  stepid?: string;
  tags?: string | null;
  styles?: string | null;
}
interface WizardConfigJson {
  title?: string;
  subtitle?: string;
  showWizardPanel?: boolean;
  nextButtonText?: string;
  previousButtonText?: string;
  submitButtonText?: string;
  steps: WizardStepJson[];
}
```

---

#### M8. No RTL Support
**File:** `gbwizard.component.scss`

Per CLAUDE.md, all layout components must include `[dir="rtl"]` CSS selectors for Arabic support. The wizard has:
- `.step-connector` grows left-to-right — will render backwards in RTL
- `arrow_forward` / `arrow_back` icons on Next/Previous buttons — semantically reversed in RTL
- `.wizard-main-content` flex row direction — needs `flex-direction: row-reverse` in RTL
- Step numbers increment left-to-right — acceptable but connector direction needs flip

No RTL rules exist at all in the SCSS.

---

#### M9. Multiple `console.log` in Consumer
**File:** `manpowerrequest.component.ts:79, 82, 103, 143, 160, 161, 170`

Seven `console.log` calls, including one that logs the full `LoginDTO` (line 82 — logs sensitive user session data to the browser console). Replace all with `GbConsoleService`.

---

#### M10. `setTimeout` Without Cleanup in Consumer
**File:** `manpowerrequest.component.ts:108–112`

```typescript
setTimeout(() => {
    this.BiztransactionService(true);
    this.DateRefreshfun();
    this.cdr.detectChanges();
}, 100);
```

The `setTimeout` handle is not stored. If the component is destroyed in under 100ms, the callback fires on a destroyed component. Store handle in a variable and clear in `ngOnDestroy`.

---

#### M11. `MatDialog` Width Hardcoded to `'600px'`
**File:** `manpowerrequest.component.ts:257`

```typescript
this.dialog.open(GbDialogBoxComponent, {
    width: '600px'   // ← must be min(600px, 95vw)
});
```

Per CLAUDE.md: all MatDialog widths must use `min(Xpx, 95vw)` with `maxWidth: '95vw'`.

---

#### M12. Nested `<form [formGroup]>` in Consumer Template
**File:** `manpowerrequest.component.html:1 and 132`

The outer template opens with `<form [formGroup]="form">` on line 1. Inside the form-mode section (line 132), another `<form [formGroup]="form">` is opened. This creates nested `<form>` elements in the DOM — invalid HTML5. The inner form group binding is redundant and should be removed.

---

### 🟢 Minor / Style

#### Mi1. `styleUrls` Array Syntax Deprecated in Angular 17+
**File:** `gbwizard.component.ts:40`

```typescript
styleUrls: ['./gbwizard.component.scss']
```
Should be: `styleUrl: './gbwizard.component.scss'` (singular, no array).

---

#### Mi2. `@Output()` Names Use `on` Prefix — Breaks Angular Convention
**File:** `gbwizard.component.ts:65–68`

```typescript
@Output() onStepChange = new EventEmitter<number>();
@Output() onComplete = new EventEmitter<void>();
@Output() onCancel = new EventEmitter<void>();
@Output() onFormGroupChange = new EventEmitter<string>();
```

Angular convention: `@Output()` names should be verbs or noun+Change, no `on` prefix (the `on` is added in the template: `(stepChange)="..."`). Should be: `stepChange`, `complete`, `cancel`, `formGroupChange`.

---

#### Mi3. `overflow-y: scroll` Should Be `auto`
**File:** `gbwizard.component.scss:152`

`overflow-y: scroll` always shows a scrollbar track even when content fits. Use `overflow-y: auto`.

---

#### Mi4. Gray Scale Colors Hardcoded — Not Using Theme Variables
**File:** `gbwizard.component.scss:81–258`

Colors `#f3f4f6`, `#9ca3af`, `#6b7280`, `#111827`, `#374151`, `#e5e7eb`, `rgba(59, 130, 246, 0.1)`, `rgba(0, 0, 0, 0.1)` are all hardcoded. Primary color correctly uses `var(--action-primary-default)` but all neutrals bypass the theme system. Define CSS custom properties for these or use a theme token map.

---

#### Mi5. `stepno` Field in JSON Is Never Read by Service
**File:** `wizardmanpowerrequest.json:10,15,20,25`

```json
"stepno": "1",
```

The `transformJsonToWizardConfig` method in the service does not read `stepno`. It's a dead config field. The service also references `step.stepid` (not in JSON) and `step.tags` / `step.styles` (not in JSON). The JSON schema and service are out of sync.

---

#### Mi6. Cancel Button Has No `showCancel` Input
**File:** `gbwizard.component.html:55`

The Cancel button is always shown. There's no `@Input() showCancel: boolean = true`. Some wizard use cases (dialogs with their own cancel controls, embedded wizards) don't need a cancel button inside the wizard itself.

---

#### Mi7. No Accessibility (ARIA) on Step Indicators
**File:** `gbwizard.component.html:11–29`

Step indicators are interactive `<div>` elements with `(click)` but no:
- `role="button"` or `role="tab"`
- `aria-current="step"` on active step
- `aria-label` on step number
- `aria-disabled="true"` on disabled steps
- Keyboard navigation (`tabindex`, `(keydown.enter)`)

This makes the wizard inaccessible for keyboard users and screen readers.

---

#### Mi8. Empty Test File
**File:** `gbwizard.component.spec.ts`

Zero tests. For a general-purpose shared component, at minimum:
- Step navigation state transitions
- `onFormGroupChange` emission
- `canAdvance` guard (once implemented)
- Config input → `wizardSteps` mapping

---

#### Mi9. `componentRefs` Dead Infrastructure
**File:** `gbwizard.component.ts:46, 125–131`

```typescript
private componentRefs: ComponentRef<any>[] = [];  // always empty

clearDynamicContent() {
  this.componentRefs.forEach(ref => ref.destroy()); // iterates empty array
  this.componentRefs = [];
  if (this.dynamicContent) { this.dynamicContent.clear(); }
}
```

`componentRefs` is never populated anywhere. The `forEach` and re-assignment are dead operations. Either implement dynamic component injection or remove this infrastructure.

---

#### Mi10. `goToStep()` Backward Navigation Leaves `isDisabled` Inconsistent
**File:** `gbwizard.component.ts:147–151`

When navigating backward, `isCompleted` is reset for all steps from target to end, but `isDisabled` is NOT reset. This means previously-visited steps remain `isDisabled: false` even after backward navigation clears their `isCompleted`. A user could click forward past un-completed steps by clicking the step indicators. This may be intentional but is undocumented.

---

## Summary Table

| Category | Count | Priority Items |
|---|---|---|
| 🔴 Critical (P0) | 4 | C1 (no OnPush), C2 (innerHTML XSS), C3 (sessionStorage), C4 (nested sub leak) |
| 🟠 High — Functional | 7 | H1 (no initial emit), H2 (no validation hook), H3 (stub loadStepContent), H4 (setter overwrites input), H5 (null string bug), H6 (duplicate content), H7 (parallel state) |
| 🟡 Medium | 12 | M1 (i18n), M2 (fixed dimensions), M3 (no trackBy), M4 (method calls in template), M5 (no error handling), M6 (no caching), M7 (any types), M8 (no RTL), M9 (console.log), M10 (setTimeout), M11 (dialog width), M12 (nested form) |
| 🟢 Minor / Style | 10 | Mi1–Mi10 |

---

## Recommended Fix Order

### Sprint 1 — Must Fix Now
1. **Add `changeDetection: ChangeDetectionStrategy.OnPush` to `@Component` decorator** (C1)
2. **Remove `[innerHTML]` on `tag` — use plain text or DOMPurify** (C2)
3. **Emit `onFormGroupChange` for step 0 in `ngAfterViewInit`** (H1) — breaks all consumers otherwise
4. **Add `@Input() canAdvance: boolean = true` and guard in `next()`** (H2) — wizard is unusable without validation
5. **Fix `'null'` string check → `?? undefined`** in config service (H5)

### Sprint 2 — Before Adding New Consumers
6. **Replace all button default strings with Transloco keys** (M1)
7. **Remove fixed `width: 920px` / `height: 300px` from `.wizard-content`** (M2)
8. **Add `trackBy` to `*ngFor` on wizardSteps** (M3)
9. **Convert template method calls to getters** (M4)
10. **Add `catchError` and config caching to `GbWizardConfigService`** (M5, M6)
11. **Replace `any` in service with typed interfaces** (M7)
12. **Add ARIA attributes and keyboard navigation** (Mi7)
13. **Add RTL CSS rules** (M8)

### Sprint 3 — Clean Up
14. **Remove dead `componentRefs` / `clearDynamicContent` / `@ViewChild` infrastructure** (Mi9, H3)
15. **Fix `@Output()` naming convention** (Mi2)
16. **Fix `styleUrls` → `styleUrl`** (Mi1)
17. **Add `@Input() showCancel`** (Mi6)
18. **Replace `console.log` in consumer with `GbConsoleService`** (M9)
19. **Eliminate consumer step state duplication** (H7)
20. **Eliminate duplicate step content in consumer template** (H6)
21. **Fix nested `<form>` in consumer template** (M12)
22. **Fix consumer `sessionStorage.getItem('LoginDTO')`** (C3)
23. **Fix consumer nested subscribes** (C4)
24. **Add unit tests** (Mi8)

---

## Quick Fixes (Copy-Paste Ready)

```typescript
// FIX C1: Add OnPush to @Component decorator
@Component({
  selector: 'gb-wizard',
  standalone: true,
  imports: [CommonModule, MatIconModule],
  templateUrl: './gbwizard.component.html',
  styleUrl: './gbwizard.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush
})

// FIX H1: Emit initial formGroupChange
ngAfterViewInit() {
  if (this.wizardSteps.length > 0) {
    this.loadStepContent(0);
    const firstStep = this.wizardSteps[0];
    if (firstStep?.stepformgroupname) {
      this.onFormGroupChange.emit(firstStep.stepformgroupname);
    }
  }
}

// FIX H2: Validation guard on next()
@Input() canAdvance: boolean = true;
next() {
  if (!this.canAdvance) return;
  if (this.currentStepIndex < this.wizardSteps.length - 1) {
    this.wizardSteps[this.currentStepIndex].isCompleted = true;
    this.wizardSteps[this.currentStepIndex + 1].isDisabled = false;
    this.goToStep(this.currentStepIndex + 1);
  }
}

// FIX H5: Null check in config service
tag: step.tags ?? undefined,
stepstyles: step.styles ? this.parseStyles(step.styles) : undefined

// FIX M3: trackBy for wizard steps
trackByStepNumber(_: number, step: WizardStep): number {
  return step.stepNumber;
}
// Template: *ngFor="let step of wizardSteps; trackBy: trackByStepNumber"

// FIX M4: Getters instead of methods in template
get isFirst(): boolean { return this.currentStepIndex === 0; }
get isLast(): boolean { return this.currentStepIndex === this.wizardSteps.length - 1; }
get currentStepStyles(): Record<string, string> {
  return this.wizardSteps[this.currentStepIndex]?.stepstyles ?? {};
}

// FIX M2: Responsive content area
// .wizard-content scss: replace fixed dimensions with:
//   flex: 1; min-height: 200px; max-height: 70dvh; overflow-y: auto;

// FIX Mi2: Output naming convention (breaking change — coordinate with consumers)
@Output() stepChange = new EventEmitter<number>();
@Output() complete = new EventEmitter<void>();
@Output() cancel = new EventEmitter<void>();
@Output() formGroupChange = new EventEmitter<string>();

// FIX M11: Consumer dialog width
this.dialog.open(GbDialogBoxComponent, {
    data: { ... },
    width: 'min(600px, 95vw)',
    maxWidth: '95vw'
});
```
