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
DokumentePersonen
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
DokumentePersonen
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
ElementTailwind classesReal sizeNotes
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
DokumentePersonen
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
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
ElementTailwind classesReal sizeNotes
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
ElementTailwind classesReal sizeNotes
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
ElementTailwind classesReal sizeNotes
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"