- Add architecture overview, implementation plan, and intent overview - Add intake intent process specs (gap analyse, bouwplan, testplan) - Add swift architecture specs and visualization prompts - Remove obsolete aispeedrun-manifesto template Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
34 KiB
Functioneel & Technisch Ontwerp: Cortex Shared Components
Projectnaam: Cortex Intake Intent System - Shared Components Versie: v1.0 Datum: 03-02-2026 Auteur: Colin Lit (met AI-assistentie)
1. Doel en Context
1.1 Doel van dit document
Dit gecombineerde FO/TO beschrijft de shared components die worden hergebruikt door alle Cortex Blocks. Het document combineert:
- Functioneel (FO): Wat de gebruiker ziet en ervaart
- Technisch (TO): Hoe de componenten worden gebouwd
1.2 Relatie met andere documenten
| Document | Relatie |
|---|---|
intake-process-intents.md |
Beschrijft de intents die deze components gebruiken |
gap-analyse-intake-cortex.md |
Identificeert welke components ontbreken |
block-template-pattern.md |
Beschrijft het patroon waarop deze components gebaseerd zijn |
1.3 Scope
In scope:
- 4 nieuwe shared components voor Cortex Blocks
- 2 nieuwe custom hooks voor data fetching en context
Buiten scope:
- Individuele intake blocks (apart document)
- Bestaande components (
patient-list-item.tsx,linked-evidence.tsx)
2. Overzicht Componenten
2.1 Component Hiërarchie
components/cortex/
├── blocks/
│ └── [intake-block].tsx ← Gebruikt shared components
│
├── shared/
│ ├── patient-list-item.tsx ✅ Bestaat
│ ├── linked-evidence.tsx ✅ Bestaat
│ │
│ ├── block-states.tsx 🆕 NIEUW (dit document)
│ ├── block-section.tsx 🆕 NIEUW (dit document)
│ ├── block-item.tsx 🆕 NIEUW (dit document)
│ └── block-footer.tsx 🆕 NIEUW (dit document)
lib/cortex/hooks/
├── use-patient-search.ts ✅ Bestaat
├── use-patient-selection.ts ✅ Bestaat
│
├── use-block-data.ts 🆕 NIEUW (dit document)
└── use-intake-context.ts 🆕 NIEUW (dit document)
2.2 Component Overzicht
| Component | Type | Functie | Gebruikt door |
|---|---|---|---|
BlockLoading |
State | Laad-indicator tijdens data fetch | Alle blocks |
BlockError |
State | Foutmelding met retry optie | Alle blocks |
BlockEmpty |
State | Lege staat met actie | Alle blocks |
BlockSection |
Layout | Sectie met icon en titel | Query blocks |
BlockItem |
Layout | Lijst-item met badge | Query blocks |
BlockFooter |
Layout | Footer met acties | Alle blocks |
3. User Stories
3.1 Algemene User Stories
| ID | Rol | Doel / Actie | Verwachte waarde | Prioriteit |
|---|---|---|---|---|
| US-SC-01 | Gebruiker | Zien dat data wordt geladen | Weet dat systeem bezig is | Hoog |
| US-SC-02 | Gebruiker | Foutmelding zien bij problemen | Kan retry doen of hulp zoeken | Hoog |
| US-SC-03 | Gebruiker | Lege staat zien met suggestie | Weet wat te doen als geen data | Middel |
| US-SC-04 | Gebruiker | Snel navigeren naar EPD | Kan details bekijken in dossier | Hoog |
| US-SC-05 | Gebruiker | Risico niveau direct zien | Beoordeelt urgentie in één oogopslag | Hoog |
3.2 Developer User Stories
| ID | Rol | Doel / Actie | Verwachte waarde | Prioriteit |
|---|---|---|---|---|
| US-DEV-01 | Developer | Consistente states hergebruiken | Sneller nieuwe blocks bouwen | Hoog |
| US-DEV-02 | Developer | Generieke data hook gebruiken | Minder boilerplate code | Hoog |
| US-DEV-03 | Developer | Intake context automatisch hebben | Geen prop drilling nodig | Middel |
4. Functionele Specificaties per Component
4.1 BlockLoading
Doel: Toon visuele feedback tijdens het laden van data.
Functioneel gedrag:
- Toont geanimeerde spinner (rotatie)
- Toont optionele tekst (bijv. "Risico's laden...")
- Centreert verticaal en horizontaal in block
- Verdwijnt zodra data geladen is
Visueel:
┌─────────────────────────────────────┐
│ │
│ │
│ [◌ spinner] │
│ Risico's laden... │
│ │
│ │
└─────────────────────────────────────┘
Props:
| Prop | Type | Default | Beschrijving |
|---|---|---|---|
message |
string |
"Laden..." |
Tekst onder spinner |
4.2 BlockError
Doel: Toon foutmelding met optie om opnieuw te proberen.
Functioneel gedrag:
- Toont rood/oranje waarschuwingsicoon
- Toont foutmelding (gebruikersvriendelijk, Nederlands)
- Optionele "Opnieuw proberen" knop
- Verdwijnt bij retry of bij sluiten block
Visueel:
┌─────────────────────────────────────┐
│ │
│ [⚠️ icon] │
│ │
│ Kon risico's niet ophalen. │
│ Controleer je internetverbinding. │
│ │
│ [🔄 Opnieuw proberen] │
│ │
└─────────────────────────────────────┘
Props:
| Prop | Type | Default | Beschrijving |
|---|---|---|---|
message |
string |
- | Foutmelding tekst |
onRetry |
() => void |
undefined |
Callback voor retry (toont knop indien aanwezig) |
4.3 BlockEmpty
Doel: Toon informatieve lege staat met optionele actie.
Functioneel gedrag:
- Toont context-relevant icoon (configureerbaar)
- Toont melding waarom leeg
- Optionele actie-knop (bijv. "Toevoegen in dossier")
- Visueel subtiel (niet alarmerend)
Visueel:
┌─────────────────────────────────────┐
│ │
│ [📋 icon faded] │
│ │
│ Geen risico's geregistreerd │
│ │
│ [+ Toevoegen in dossier] │
│ │
└─────────────────────────────────────┘
Props:
| Prop | Type | Default | Beschrijving |
|---|---|---|---|
icon |
LucideIcon |
- | Icoon component |
message |
string |
- | Hoofdboodschap |
action |
{ label: string; onClick: () => void } |
undefined |
Optionele actie knop |
4.4 BlockSection
Doel: Groepeer gerelateerde content met visuele header.
Functioneel gedrag:
- Toont icoon + titel + optionele count
- Bevat children content
- Consistente styling (witte achtergrond, border, padding)
- Optioneel inklapbaar (v2)
Visueel:
┌─────────────────────────────────────┐
│ [⚠️] Risicotaxatie (3) │ ← Header
├─────────────────────────────────────┤
│ │
│ [Content / children] │ ← Children
│ │
└─────────────────────────────────────┘
Props:
| Prop | Type | Default | Beschrijving |
|---|---|---|---|
icon |
LucideIcon |
- | Icoon component |
iconColor |
string |
"text-slate-600" |
Tailwind kleur class |
title |
string |
- | Sectie titel |
count |
number |
undefined |
Optionele count badge |
children |
ReactNode |
- | Sectie inhoud |
4.5 BlockItem
Doel: Toon een enkel item in een lijst met consistente styling.
Functioneel gedrag:
- Toont titel + optionele subtitle
- Toont status badge (kleur-gecodeerd)
- Klikbaar indien
onClickmeegegeven - Hover state bij klikbaar
- Geen hover bij niet-klikbaar
Badge varianten:
| Variant | Kleur | Gebruik |
|---|---|---|
default |
Grijs | Neutraal, geen status |
success |
Groen | Laag risico, compleet |
warning |
Oranje | Gemiddeld risico, aandacht |
danger |
Rood | Hoog risico, urgent |
Visueel:
┌─────────────────────────────────────────────────────┐
│ [Avatar?] Suïcidaliteit [Matig] │ ← Klikbaar
│ 12 jan 2026 • Dr. Jansen │
└─────────────────────────────────────────────────────┘
Props:
| Prop | Type | Default | Beschrijving |
|---|---|---|---|
title |
string |
- | Hoofdtekst |
subtitle |
string |
undefined |
Subtekst (datum, auteur, etc.) |
badge |
{ label: string; variant: BadgeVariant } |
undefined |
Status badge |
onClick |
() => void |
undefined |
Klik handler (maakt item klikbaar) |
4.6 BlockFooter
Doel: Consistente footer met acties onderaan een block.
Functioneel gedrag:
- Links: secundaire actie (ghost button)
- Rechts: primaire actie (solid button)
- Horizontale scheidingslijn boven footer
- Loading state op primaire actie
Visueel:
─────────────────────────────────────────
[🔗 Bekijk in dossier] [+ Toevoegen]
(secundair) (primair)
Props:
| Prop | Type | Default | Beschrijving |
|---|---|---|---|
secondaryAction |
{ label, icon?, onClick } |
undefined |
Linker actie |
primaryAction |
{ label, icon?, onClick, loading? } |
undefined |
Rechter actie |
5. UI Overzicht
5.1 Component Compositie in een Block
┌─────────────────────────────────────────────────────────────┐
│ [Icon] Risicotaxatie [X Close] │ ← BlockContainer
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ [⚠️] Huidige risico's (3) │ │ ← BlockSection
│ ├───────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ Suïcidaliteit [Matig] │ │ │ ← BlockItem
│ │ │ 12 jan 2026 │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ Agressie [Laag] │ │ │ ← BlockItem
│ │ │ 10 jan 2026 │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ───────────────────────────────────────────────────────── │
│ [🔗 Bekijk in dossier] [+ Toevoegen] │ ← BlockFooter
│ │
└─────────────────────────────────────────────────────────────┘
5.2 State Flows
Block Lifecycle:
┌─────────┐ data? ┌─────────┐
│ Loading │───────────▶│ Data │
└─────────┘ yes └─────────┘
│ │
│ error │ empty
▼ ▼
┌─────────┐ ┌─────────┐
│ Error │ │ Empty │
└─────────┘ └─────────┘
│ │
│ retry │ action
▼ ▼
┌─────────┐ ┌─────────┐
│ Loading │ │ EPD │
└─────────┘ │ Navigatie│
└─────────┘
6. Technische Architectuur
6.1 Component Structuur
components/cortex/shared/
├── block-states.tsx # BlockLoading, BlockError, BlockEmpty
├── block-section.tsx # BlockSection
├── block-item.tsx # BlockItem
├── block-footer.tsx # BlockFooter
└── index.ts # Re-exports
lib/cortex/hooks/
├── use-block-data.ts # Generic data fetching hook
└── use-intake-context.ts # Intake context hook
6.2 Dependencies
Externe dependencies:
├── lucide-react # Icons
├── framer-motion # Animaties (optioneel)
└── @/components/ui/button # shadcn Button component
Interne dependencies:
├── @/lib/utils # cn() helper
├── @/lib/cortex/error-handler # safeFetch, getErrorInfo
├── @/stores/cortex-store # useCortexStore
└── @/hooks/use-toast # Toast notifications
7. Technische Specificaties
7.1 block-states.tsx
// components/cortex/shared/block-states.tsx
'use client';
import { Loader2, AlertCircle, RefreshCw } from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
// ============================================================================
// BlockLoading
// ============================================================================
interface BlockLoadingProps {
/** Tekst onder de spinner */
message?: string;
/** Extra CSS classes */
className?: string;
}
export function BlockLoading({
message = 'Laden...',
className
}: BlockLoadingProps) {
return (
<div className={cn(
'flex flex-col items-center justify-center py-12',
className
)}>
<Loader2 className="h-6 w-6 animate-spin text-slate-400 mb-2" />
<span className="text-sm text-slate-500">{message}</span>
</div>
);
}
// ============================================================================
// BlockError
// ============================================================================
interface BlockErrorProps {
/** Foutmelding tekst */
message: string;
/** Callback voor retry knop (toont knop indien aanwezig) */
onRetry?: () => void;
/** Extra CSS classes */
className?: string;
}
export function BlockError({
message,
onRetry,
className
}: BlockErrorProps) {
return (
<div className={cn(
'flex flex-col items-center justify-center py-8 text-center',
className
)}>
<AlertCircle className="h-8 w-8 text-red-500 mb-2" />
<p className="text-sm text-red-700 mb-3 max-w-xs">{message}</p>
{onRetry && (
<Button variant="outline" size="sm" onClick={onRetry}>
<RefreshCw className="h-4 w-4 mr-1.5" />
Opnieuw proberen
</Button>
)}
</div>
);
}
// ============================================================================
// BlockEmpty
// ============================================================================
interface BlockEmptyProps {
/** Icoon component */
icon: LucideIcon;
/** Hoofdboodschap */
message: string;
/** Optionele actie knop */
action?: {
label: string;
onClick: () => void;
};
/** Extra CSS classes */
className?: string;
}
export function BlockEmpty({
icon: Icon,
message,
action,
className
}: BlockEmptyProps) {
return (
<div className={cn(
'flex flex-col items-center justify-center py-8 text-center',
className
)}>
<Icon className="h-8 w-8 text-slate-300 mb-2" />
<p className="text-sm text-slate-500 mb-3">{message}</p>
{action && (
<Button variant="outline" size="sm" onClick={action.onClick}>
{action.label}
</Button>
)}
</div>
);
}
7.2 block-section.tsx
// components/cortex/shared/block-section.tsx
'use client';
import type { ReactNode } from 'react';
import type { LucideIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
interface BlockSectionProps {
/** Icoon component */
icon: LucideIcon;
/** Tailwind kleur class voor icoon */
iconColor?: string;
/** Sectie titel */
title: string;
/** Optionele count badge */
count?: number;
/** Sectie inhoud */
children: ReactNode;
/** Extra CSS classes */
className?: string;
}
export function BlockSection({
icon: Icon,
iconColor = 'text-slate-600',
title,
count,
children,
className,
}: BlockSectionProps) {
return (
<section className={cn(
'bg-white rounded-lg border border-slate-200 p-4',
className
)}>
{/* Header */}
<div className="flex items-center gap-2 mb-3">
<Icon className={cn('h-4 w-4', iconColor)} />
<h3 className="text-sm font-medium text-slate-700">{title}</h3>
{count !== undefined && (
<span className="text-xs text-slate-500">({count})</span>
)}
</div>
{/* Content */}
{children}
</section>
);
}
7.3 block-item.tsx
// components/cortex/shared/block-item.tsx
'use client';
import { cn } from '@/lib/utils';
// ============================================================================
// Types
// ============================================================================
export type BadgeVariant = 'default' | 'success' | 'warning' | 'danger';
interface BlockItemProps {
/** Hoofdtekst */
title: string;
/** Subtekst (datum, auteur, etc.) */
subtitle?: string;
/** Status badge */
badge?: {
label: string;
variant: BadgeVariant;
};
/** Klik handler (maakt item klikbaar) */
onClick?: () => void;
/** Extra CSS classes */
className?: string;
}
// ============================================================================
// Styling
// ============================================================================
const BADGE_STYLES: Record<BadgeVariant, string> = {
default: 'bg-slate-100 text-slate-700 border-slate-200',
success: 'bg-green-50 text-green-700 border-green-200',
warning: 'bg-amber-50 text-amber-700 border-amber-200',
danger: 'bg-red-50 text-red-700 border-red-200',
};
// ============================================================================
// Component
// ============================================================================
export function BlockItem({
title,
subtitle,
badge,
onClick,
className
}: BlockItemProps) {
const isClickable = Boolean(onClick);
const Component = isClickable ? 'button' : 'div';
return (
<Component
onClick={onClick}
className={cn(
'flex items-center justify-between p-3 rounded-lg',
'bg-slate-50 border border-slate-200',
'w-full text-left',
isClickable && [
'cursor-pointer',
'hover:bg-slate-100 hover:border-slate-300',
'transition-colors',
],
className
)}
>
{/* Content */}
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-slate-900 truncate">
{title}
</p>
{subtitle && (
<p className="text-xs text-slate-500 mt-0.5 truncate">
{subtitle}
</p>
)}
</div>
{/* Badge */}
{badge && (
<span className={cn(
'ml-3 flex-shrink-0',
'px-2 py-0.5 rounded-full',
'text-xs font-medium border',
BADGE_STYLES[badge.variant]
)}>
{badge.label}
</span>
)}
</Component>
);
}
// ============================================================================
// Helper: Get variant from risk level
// ============================================================================
export function getRiskBadgeVariant(level: string): BadgeVariant {
switch (level.toLowerCase()) {
case 'laag':
return 'success';
case 'gemiddeld':
return 'warning';
case 'hoog':
case 'zeer_hoog':
return 'danger';
default:
return 'default';
}
}
7.4 block-footer.tsx
// components/cortex/shared/block-footer.tsx
'use client';
import type { LucideIcon } from 'lucide-react';
import { Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
interface ActionConfig {
/** Knop label */
label: string;
/** Optioneel icoon */
icon?: LucideIcon;
/** Click handler */
onClick: () => void;
/** Loading state (alleen voor primary) */
loading?: boolean;
/** Disabled state */
disabled?: boolean;
}
interface BlockFooterProps {
/** Linker actie (ghost button) */
secondaryAction?: ActionConfig;
/** Rechter actie (solid button) */
primaryAction?: ActionConfig;
/** Extra CSS classes */
className?: string;
}
export function BlockFooter({
secondaryAction,
primaryAction,
className
}: BlockFooterProps) {
// Don't render if no actions
if (!secondaryAction && !primaryAction) {
return null;
}
return (
<div className={cn(
'flex items-center justify-between',
'pt-4 mt-4 border-t border-slate-200',
className
)}>
{/* Secondary Action (left) */}
{secondaryAction ? (
<Button
variant="ghost"
size="sm"
onClick={secondaryAction.onClick}
disabled={secondaryAction.disabled}
>
{secondaryAction.icon && (
<secondaryAction.icon className="h-4 w-4 mr-1.5" />
)}
{secondaryAction.label}
</Button>
) : (
<div /> // Spacer
)}
{/* Primary Action (right) */}
{primaryAction && (
<Button
size="sm"
onClick={primaryAction.onClick}
disabled={primaryAction.loading || primaryAction.disabled}
>
{primaryAction.loading ? (
<Loader2 className="h-4 w-4 mr-1.5 animate-spin" />
) : primaryAction.icon ? (
<primaryAction.icon className="h-4 w-4 mr-1.5" />
) : null}
{primaryAction.label}
</Button>
)}
</div>
);
}
7.5 use-block-data.ts
// lib/cortex/hooks/use-block-data.ts
'use client';
import { useState, useEffect, useCallback } from 'react';
import { useToast } from '@/hooks/use-toast';
import { safeFetch, getErrorInfo } from '@/lib/cortex/error-handler';
// ============================================================================
// Types
// ============================================================================
interface UseBlockDataOptions<T> {
/** API endpoint (relatief pad) */
endpoint: string;
/** Query parameters */
params?: Record<string, string | undefined>;
/** Of data moet worden opgehaald */
enabled?: boolean;
/** Callback bij error */
onError?: (error: Error) => void;
/** Operatie naam voor error messages */
operationName?: string;
}
interface UseBlockDataResult<T> {
/** Opgehaalde data */
data: T | null;
/** Loading state */
isLoading: boolean;
/** Error message */
error: string | null;
/** Refetch functie */
refetch: () => Promise<void>;
}
// ============================================================================
// Hook
// ============================================================================
export function useBlockData<T>({
endpoint,
params,
enabled = true,
onError,
operationName = 'Data laden',
}: UseBlockDataOptions<T>): UseBlockDataResult<T> {
const [data, setData] = useState<T | null>(null);
const [isLoading, setIsLoading] = useState(enabled);
const [error, setError] = useState<string | null>(null);
const { toast } = useToast();
const fetchData = useCallback(async () => {
if (!enabled) return;
setIsLoading(true);
setError(null);
try {
// Build URL with params
const url = new URL(endpoint, window.location.origin);
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
url.searchParams.set(key, value);
}
});
}
const response = await safeFetch(
url.toString(),
undefined,
{ operation: operationName }
);
const result = await response.json();
setData(result);
} catch (err) {
const errorInfo = getErrorInfo(err, { operation: operationName });
setError(errorInfo.description);
onError?.(err as Error);
toast({
variant: 'destructive',
title: errorInfo.title,
description: errorInfo.description,
});
} finally {
setIsLoading(false);
}
}, [endpoint, JSON.stringify(params), enabled, onError, operationName, toast]);
useEffect(() => {
if (enabled) {
fetchData();
}
}, [fetchData, enabled]);
return { data, isLoading, error, refetch: fetchData };
}
7.6 use-intake-context.ts
// lib/cortex/hooks/use-intake-context.ts
'use client';
import { useMemo } from 'react';
import { useCortexStore } from '@/stores/cortex-store';
import type { BlockPrefillData } from '@/stores/cortex-store';
import { formatPatientName } from '@/lib/fhir/patient-mapper';
// ============================================================================
// Types
// ============================================================================
interface UseIntakeContextResult {
/** Patient ID (van prefill of activePatient) */
patientId: string | null;
/** Intake ID (van prefill) */
intakeId: string | null;
/** Patient naam (voor display) */
patientName: string | null;
/** Of er voldoende context is */
hasPatientContext: boolean;
/** Of er intake context is */
hasIntakeContext: boolean;
}
// ============================================================================
// Hook
// ============================================================================
export function useIntakeContext(
prefill?: BlockPrefillData
): UseIntakeContextResult {
const { activePatient } = useCortexStore();
return useMemo(() => {
// Patient context: van prefill of activePatient
const patientId = prefill?.patientId || activePatient?.id || null;
const patientName = prefill?.patientName ||
(activePatient ? formatPatientName(activePatient) : null);
// Intake context: alleen van prefill (TODO: activeIntake in store)
const intakeId = prefill?.intakeId || null;
return {
patientId,
intakeId,
patientName,
hasPatientContext: Boolean(patientId),
hasIntakeContext: Boolean(intakeId),
};
}, [prefill, activePatient]);
}
7.7 Index Export
// components/cortex/shared/index.ts
// Block States
export { BlockLoading, BlockError, BlockEmpty } from './block-states';
// Block Layout
export { BlockSection } from './block-section';
export { BlockItem, getRiskBadgeVariant, type BadgeVariant } from './block-item';
export { BlockFooter } from './block-footer';
// Existing
export { PatientListItem, PatientListEmpty, PatientListLoading } from './patient-list-item';
export { LinkedEvidence } from './linked-evidence';
8. API Ontwerp
8.1 Geen nieuwe API endpoints
De shared components maken gebruik van bestaande endpoints en de nieuwe intake endpoints (beschreven in apart document). De hooks abstraheren de API calls.
8.2 Error Handling
Alle API errors worden afgehandeld via de bestaande error-handler.ts:
// Voorbeeld error flow
try {
const response = await safeFetch('/api/cortex/intake/123/risks');
// ...
} catch (err) {
const errorInfo = getErrorInfo(err, { operation: 'Risico's laden' });
// errorInfo.title = "Laden mislukt"
// errorInfo.description = "Kon risico's niet ophalen. Probeer opnieuw."
}
9. Gebruikersrollen en Rechten
De shared components zijn UI-only en hebben geen eigen rechtenmodel. Rechten worden afgedwongen door:
- API routes - RLS policies in Supabase
- Store -
activePatientalleen beschikbaar na selectie - Blocks - Tonen alleen data waartoe gebruiker toegang heeft
10. Performance & Accessibility
10.1 Performance
| Aspect | Target | Implementatie |
|---|---|---|
| Render tijd | < 16ms | Geen zware berekeningen in render |
| Bundle size | < 5KB per component | Tree-shakeable exports |
| Re-renders | Minimaal | useMemo voor derived state |
10.2 Accessibility
| Aspect | Implementatie |
|---|---|
| Screen readers | aria-label op interactieve elementen |
| Keyboard nav | button elements met onClick |
| Focus visible | Tailwind focus-visible:ring |
| Color contrast | WCAG AA compliant kleuren |
| Loading state | aria-busy="true" |
11. Testing Strategie
11.1 Unit Tests
// __tests__/components/cortex/shared/block-item.test.tsx
describe('BlockItem', () => {
it('renders title and subtitle', () => {
render(<BlockItem title="Test" subtitle="Sub" />);
expect(screen.getByText('Test')).toBeInTheDocument();
expect(screen.getByText('Sub')).toBeInTheDocument();
});
it('renders badge with correct variant', () => {
render(<BlockItem title="Test" badge={{ label: 'Hoog', variant: 'danger' }} />);
const badge = screen.getByText('Hoog');
expect(badge).toHaveClass('bg-red-50');
});
it('is clickable when onClick provided', () => {
const onClick = jest.fn();
render(<BlockItem title="Test" onClick={onClick} />);
fireEvent.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalled();
});
it('is not clickable when onClick not provided', () => {
render(<BlockItem title="Test" />);
expect(screen.queryByRole('button')).not.toBeInTheDocument();
});
});
11.2 Visual Tests
Storybook stories voor elke component variant:
// stories/BlockItem.stories.tsx
export default {
title: 'Cortex/Shared/BlockItem',
component: BlockItem,
};
export const Default = () => <BlockItem title="Default item" />;
export const WithSubtitle = () => <BlockItem title="With subtitle" subtitle="Extra info" />;
export const WithBadge = () => <BlockItem title="With badge" badge={{ label: 'Hoog', variant: 'danger' }} />;
export const Clickable = () => <BlockItem title="Clickable" onClick={() => alert('Clicked!')} />;
12. Implementatie Volgorde
| # | Component | Effort | Dependencies |
|---|---|---|---|
| 1 | block-states.tsx |
S | Geen |
| 2 | block-section.tsx |
S | Geen |
| 3 | block-item.tsx |
S | Geen |
| 4 | block-footer.tsx |
S | @/components/ui/button |
| 5 | use-block-data.ts |
M | error-handler.ts |
| 6 | use-intake-context.ts |
S | cortex-store |
| 7 | index.ts exports |
S | 1-6 |
Totaal geschat: ~1 dag
13. Risico's en Mitigatie
| Risico | Impact | Mitigatie |
|---|---|---|
| Inconsistente styling | Middel | Design tokens in Tailwind config |
| Over-engineering | Laag | YAGNI - alleen bouwen wat nodig is |
| Bundle bloat | Laag | Tree-shakeable exports |
| Breaking changes | Middel | Semantic versioning, deprecation warnings |
14. Bijlagen & Referenties
Projectdocumenten:
docs/intent/intake-intent-proces/intake-process-intents.md- Intent specificatiesdocs/intent/intake-intent-proces/gap-analyse-intake-cortex.md- Gap analysedocs/intent/intake-intent-proces/block-template-pattern.md- Template patterns
Bestaande code:
components/cortex/shared/patient-list-item.tsx- Referentie voor patternscomponents/cortex/blocks/block-container.tsx- Parent containerlib/cortex/error-handler.ts- Error handling utilities
Tech documentatie:
- Lucide Icons: https://lucide.dev/icons
- Tailwind CSS: https://tailwindcss.com/docs
- shadcn/ui Button: https://ui.shadcn.com/docs/components/button