# 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 `onClick` meegegeven
- 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

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
// 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`:

```typescript
// 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:
1. **API routes** - RLS policies in Supabase
2. **Store** - `activePatient` alleen beschikbaar na selectie
3. **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

```typescript
// __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:

```typescript
// 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 specificaties
- `docs/intent/intake-intent-proces/gap-analyse-intake-cortex.md` - Gap analyse
- `docs/intent/intake-intent-proces/block-template-pattern.md` - Template patterns

**Bestaande code:**
- `components/cortex/shared/patient-list-item.tsx` - Referentie voor patterns
- `components/cortex/blocks/block-container.tsx` - Parent container
- `lib/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
