Add branching intake wizard (derived steps + radio-group atom)

A second wizard demonstrating a BRANCHING flow: the visible steps are derived
from the answers by a pure `visibleSteps` function rather than stored, so
answering "buiten Nederland gewerkt? -> ja" or reporting few hours adds steps
and the progress denominator changes live. Same Elm-style store + RemoteData
patterns as the fixed wizard; answers persist to localStorage.

- intake.machine.ts: IntakeState union + Answers + visibleSteps + pure reduce (+spec)
- intake-wizard organism, intake.page, submit-intake command
- new radio-group atom (ControlValueAccessor) in shared/ui
- /intake route + dashboard link + concepts showcase section
- tighten Aantekening.type to a 'Specialisme' | 'Aantekening' union
- README + ARCHITECTURE updated

Verified live end-to-end (branches add steps 4->5->6, review, submit) with no
console errors; build, unit tests, and Storybook all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 09:38:26 +02:00
parent 164d20a10d
commit 7463efdc2d
14 changed files with 960 additions and 85 deletions

View File

@@ -0,0 +1,50 @@
import { Component, forwardRef, input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
export interface RadioOption {
value: string;
label: string;
}
/** Atom: a radio group. Thin wrapper over the Utrecht/RHC radio CSS, wired as a
form control so it works with ngModel just like the text-input atom. */
@Component({
selector: 'app-radio-group',
template: `
<div class="utrecht-form-field-radio-group" role="radiogroup">
@for (opt of options(); track opt.value) {
<label class="utrecht-form-label utrecht-form-label--radio-button" style="display:flex;align-items:center;gap:0.5rem;padding:0.25rem 0">
<input
class="utrecht-radio-button"
type="radio"
[name]="name()"
[value]="opt.value"
[checked]="value === opt.value"
[disabled]="disabled"
(change)="select(opt.value)"
(blur)="onTouched()" />
{{ opt.label }}
</label>
}
</div>
`,
providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RadioGroupComponent), multi: true }],
})
export class RadioGroupComponent implements ControlValueAccessor {
options = input.required<RadioOption[]>();
name = input.required<string>();
value = '';
disabled = false;
onChange: (v: string) => void = () => {};
onTouched: () => void = () => {};
select(v: string) {
this.value = v;
this.onChange(v);
}
writeValue(v: string) { this.value = v ?? ''; }
registerOnChange(fn: (v: string) => void) { this.onChange = fn; }
registerOnTouched(fn: () => void) { this.onTouched = fn; }
setDisabledState(d: boolean) { this.disabled = d; }
}

View File

@@ -0,0 +1,17 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { RadioGroupComponent } from './radio-group.component';
const meta: Meta<RadioGroupComponent> = {
title: 'Atoms/RadioGroup',
component: RadioGroupComponent,
render: (args) => ({
props: args,
template: `<app-radio-group [options]="options" [name]="name" />`,
}),
};
export default meta;
type Story = StoryObj<RadioGroupComponent>;
export const JaNee: Story = {
args: { name: 'voorbeeld', options: [{ value: 'ja', label: 'Ja' }, { value: 'nee', label: 'Nee' }] },
};