# Person Title & Type Fields — Design Spec

Surface the existing `title` and `personType` fields in the person edit/create forms, detail card, and list cards. Adds conditional field visibility based on entity type.

Final · Ready for implementation
Title field
Backend only
→ Editable in forms
Type selector
Import only
→ Segmented control
SKIP value
Hidden from UI
(import-only type)
Conditional fields
Institution/Group hide
title, firstName, birth/death
Detail card
Title above name
small-caps label
List card
No change needed
displayName includes title
Mobile
Title + firstName share row
Type stacks vertically
Backend DTO
Add `personType`
to PersonUpdateDTO
📐 Mockup scale notice — all font-size, height, padding, and spacing values in the mockup CSS below are scaled to ~55% of their real implementation values so they fit on screen. Do not copy sizes from the mockup HTML/CSS. Each section ends with an ⚙ Implementation Reference table listing the exact Tailwind classes and real pixel values to use in code.

## What changes vs. current implementation

### New / changed

- PersonEditForm: segmented type control added above name fields
- PersonEditForm: `title` input (narrow column) added before firstName
- PersonEditForm: conditional visibility — INSTITUTION/GROUP hide title, firstName, birthYear, deathYear; lastName relabeled to "Name"
- PersonEditForm: UNKNOWN shows lastName + notes only
- New person form: same type selector + title field + conditional logic
- PersonCard (detail): title rendered above displayName as small-caps label
- PersonUpdateDTO (backend): add `personType` field
- +page.server.ts (edit & new): read and submit `title` + `personType`

### Kept unchanged

- PersonTypeBadge component — still used as-is in detail + list
- List card layout — displayName already includes title via backend formatter
- Avatar logic — type-based icons vs. initials unchanged
- NameHistoryEditCard — no changes
- PersonDangerZone — no changes
- PersonEditSaveBar — no changes

1 Edit Form — Type Selector & Title Field
Person selected max-w-2xl changed
/persons/abc-123/edit
FAMILIENARCHIVDokumentePersonen
← Prof. Dr. Heinrich Raddatz
Person bearbeiten
Details
Person
Institution
Gruppe
Unbekannt
Titel
Prof. Dr.
Vorname *
Heinrich
Nachname *
Raddatz
Alias
z.B. Spitzname, Geburtsname…
Geburtsjahr
1878
Sterbejahr
1944
Notizen
Biografische Notizen…
Verwerfen Speichern
PERSON type: all fields visible. Title field is a narrow input (max-w-[120px]) before firstName. Three columns on md+.
Institution selected max-w-2xl new behavior
/persons/def-456/edit
FAMILIENARCHIVDokumentePersonen
← Reichsfechtschule Leipzig
Person bearbeiten
Details
Person
Institution
Gruppe
Unbekannt
Name *
Reichsfechtschule Leipzig
Alias
z.B. Kurzname, früherer Name…
Notizen
Historische Anmerkungen…
Conditional fields: Title, Vorname, Geburtsjahr, and Sterbejahr are hidden when type is INSTITUTION or GROUP. The "Nachname" label changes to "Name".
Verwerfen Speichern
INSTITUTION type: title, firstName, birthYear, deathYear hidden. "Nachname" relabeled to "Name". Same layout for GROUP type.
Implementation Reference — Type Selector & Form Fields Real values · mockup above is ~55% scale

Element | Tailwind classes | Real size | Notes
--- | --- | --- | ---
Type selector container | `flex rounded-lg border border-line overflow-hidden mb-5` | h ~44px per button | Use `role="radiogroup"`, each button `role="radio"` with `aria-checked`. Arrow key navigation. Hidden `<input type="hidden" name="personType">` holds the value.
Type button (inactive) | `flex-1 flex items-center justify-center gap-1.5 text-sm font-bold text-ink-3 bg-muted border-r border-line cursor-pointer py-2.5` | 14px / 700, py 10px | Last button: no `border-r`. Hover: `bg-surface`.
Type button (active) | `flex-1 flex items-center justify-center gap-1.5 text-sm font-bold text-primary-fg bg-primary py-2.5` | 14px / 700 | Active state replaces bg + text color. Icon inherits `currentColor`.
Type button icon | `w-4 h-4` | 16px | Same SVG paths as PersonTypeBadge + person silhouette for PERSON.
Title input | `block w-full max-w-[120px] rounded border border-line px-3 py-2 font-serif text-ink focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring` | max-w 120px, py 8px | `aria-label` = i18n key for "Akademischer Titel". Only visible when type = PERSON.
Title label | `mb-1 block text-xs font-bold tracking-widest text-ink-3 uppercase` | 12px / 700 | Matches existing label pattern. Label text: paraglide key `form_label_title`.
Name row (PERSON) | `grid grid-cols-[120px_1fr_1fr] gap-4 md:grid-cols-[120px_1fr_1fr]` | gap 16px | Mobile (<md): `grid-cols-[80px_1fr]` — title + firstName share row, lastName goes below full-width.
Name row (INSTITUTION/GROUP) | `grid grid-cols-1 gap-4` | full width | Single "Name" field using the `lastName` input. Label changes via conditional: `form_label_name` i18n key.
Conditional visibility | Svelte `{#if selectedType === 'PERSON'}` | Hide: title, firstName, birthYear, deathYear for INSTITUTION/GROUP. Hide: title, firstName, birthYear, deathYear, alias for UNKNOWN. Use `$state` for reactive type.

2 New Person Form
Default state — Person max-w-2xl changed
/persons/new
FAMILIENARCHIVDokumentePersonen
← Zurück zur Übersicht
Neue Person
Details
Person
Institution
Gruppe
Unbekannt
Titel
Dr.
Vorname *
Vorname
Nachname *
Nachname
Alias
z.B. Spitzname, Geburtsname…
Geburtsjahr
z.B. 1920
Sterbejahr
z.B. 1995
Notizen
Biografische Notizen…
Abbrechen Erstellen
New person form defaults to PERSON type. Same segmented control + title field as edit form. All fields empty with placeholders.
Mobile — Person 375px responsive
9:41
FAMILIENARCHIV
← Zurück
Neue Person
Details
Person
Institution
Gruppe
Unbekannt
Titel
Dr.
Vorname *
Nachname *
Alias
Geburtsjahr
Sterbejahr
Notizen
Abbrechen Erstellen
Mobile: type selector wraps to 2×2 grid. Title + firstName share a row (title ~80px, firstName fills rest). lastName goes full-width below.
Implementation Reference — New Person Form Real values · mockup above is ~55% scale

Element | Tailwind classes | Real size | Notes
--- | --- | --- | ---
Type selector (mobile) | `grid grid-cols-2 rounded-lg border border-line overflow-hidden mb-5` | min-h-[44px] per button | Below `md` breakpoint: 2×2 grid. Above `md`: single row (flex). Same `role="radiogroup"` pattern.
Title + firstName row (mobile) | `flex gap-4` | title w-[80px], firstName flex-1 | Below `md`: title shrinks to 80px. Above `md`: title gets 120px in the 3-col grid.
Default personType | `let selectedType = $state(person?.personType ?? 'PERSON')` | New person defaults to PERSON. Edit form uses existing value.
Hidden input | `<input type="hidden" name="personType" value={selectedType}>` | — | Submits type with the form. Read in +page.server.ts via `formData.get('personType')`.

3 Person Detail Card — Title Display
Person with title changed
HR
Prof. Dr.
Heinrich Raddatz
„Der Professor“
1878 – 1944
Notizen
Professor der Germanistik in Leipzig.
✎ Bearbeiten
Title shown as small-caps label above the serif display name. Color: text-ink-3.
Person, no title unchanged look
MR
Martha Raddatz
1882 – 1961
✎ Bearbeiten
No title field → no label rendered. Layout collapses naturally.
Institution unchanged look
🏛
Reichsfechtschule Leipzig
Institution
✎ Bearbeiten
Institutions never show a title line. PersonTypeBadge already renders.
Group unchanged look
👥
Familie Müller
Gruppe
✎ Bearbeiten
Groups never show a title line either. Badge + icon avatar as before.
Implementation Reference — PersonCard Title Display Real values · mockup above is ~55% scale

Element | Tailwind classes | Real size | Notes
--- | --- | --- | ---
Title label | `text-xs font-bold uppercase tracking-widest text-ink-3 text-center` | 12px / 700 | Only render when `person.title` is truthy AND `personType === 'PERSON'`. Placed between avatar and displayName.
Title spacing | `mb-0.5` | 2px | Tight spacing to displayName below. When absent, avatar's mb-4 flows directly to name.
PersonCard prop | `title?: string | null` added to the person type | Already in PersonSummaryDTO. Pass from +page.server.ts load.

4 Conditional Field Visibility Matrix

Field | Person | Institution | Group | Unknown
--- | --- | --- | --- | ---
Title | ✓ visible | hidden | hidden | hidden
firstName | ✓ "Vorname" | hidden | hidden | hidden
lastName | ✓ "Nachname" | ✓ "Name" | ✓ "Gruppenname" | ✓ "Name"
alias | ✓ visible | ✓ visible | ✓ visible | hidden
birthYear | ✓ "Geburtsjahr" | hidden | hidden | hidden
deathYear | ✓ "Sterbejahr" | hidden | hidden | hidden
notes | ✓ visible | ✓ visible | ✓ visible | ✓ visible

Key behavioral note: When switching type in the segmented control, hidden fields are not cleared — their values are preserved in case the user switches back. The backend already handles null/empty values gracefully. Only the `personType` value changes on submission.
Implementation Reference — Conditional Field Logic Real values · Svelte reactivity

Element | Tailwind classes | Real size | Notes
--- | --- | --- | ---
Type state | `let selectedType = $state<PersonType>('PERSON')` | — | Reactive state drives all conditionals. Type imported from generated API types.
showPersonFields | `const showPersonFields = $derived(selectedType === 'PERSON')` | — | Controls title, firstName, birthYear, deathYear visibility.
showAlias | `const showAlias = $derived(selectedType !== 'UNKNOWN')` | — | UNKNOWN hides alias (too little info to have one).
lastNameLabel | `const lastNameLabel = $derived.by(() => { switch... })` | — | PERSON: `form_label_last_name`, INSTITUTION: `form_label_name`, GROUP: `form_label_group_name`, UNKNOWN: `form_label_name`. Needs 2 new i18n keys.
firstName required | `required` attribute only when `selectedType === 'PERSON'` | Remove `required` on firstName when hidden. lastName stays required always.

5 Accessibility Requirements

Requirement | WCAG Criterion | Implementation
--- | --- | ---
Segmented control semantics | 4.1.2 Name, Role, Value | `role="radiogroup"` on container, `role="radio"` + `aria-checked` on each button. `aria-label="Typ der Person"` on container.
Keyboard navigation | 2.1.1 Keyboard | Arrow keys cycle through options. Tab moves out of the group. Space/Enter selects.
Touch target size | 2.5.8 Target Size (Minimum) | Each segment button: `min-h-[44px]`. Mobile 2×2 grid: same minimum per cell.
Title input label | 1.3.1 Info and Relationships | Visible `<label for="title">`. Also `aria-describedby` linking to help text if placeholder alone is ambiguous.
Hidden field announcement | 4.1.2 Name, Role, Value | When type changes, use `aria-live="polite"` region to announce which fields are now visible. E.g., "Felder für Person angezeigt".
Focus management | 2.4.3 Focus Order | Tab order: type selector → title (if visible) → firstName (if visible) → lastName → alias (if visible) → birthYear (if visible) → deathYear (if visible) → notes.

## Implementation Notes

### Backend

- Add `personType` field to `PersonUpdateDTO` with `@NotNull` validation
- `PersonService.update()`: set `person.setPersonType(dto.getPersonType())`
- `PersonService.create()`: same — accept type from DTO
- Regenerate OpenAPI spec + frontend types after DTO change
- `SKIP` value: exclude from API validation (backend-only, used by import)

### Frontend

- Refactor `PersonEditForm.svelte`: add `selectedType` state + segmented control
- Add `title` input to name row (narrow, max-w-[120px])
- Wrap conditional fields in `{#if showPersonFields}`
- Update `PersonCard.svelte` prop type: add `title`
- Update both `+page.server.ts` files: read/submit `title` + `personType`
- New person form: inline the same type selector + title + conditionals

### i18n Keys (new)

- `form_label_title` — "Titel" / "Title" / "Título"
- `form_label_name` — "Name" / "Name" / "Nombre"
- `form_label_group_name` — "Gruppenname" / "Group name" / "Nombre del grupo"
- `form_label_person_type` — "Typ" / "Type" / "Tipo"
- `person_type_PERSON` — "Person" / "Person" / "Persona"
- `a11y_type_fields_visible` — "Felder für {type} angezeigt"