diff --git a/docs/specs/person-title-type-fields-spec.html b/docs/specs/person-title-type-fields-spec.html new file mode 100644 index 00000000..71c447f0 --- /dev/null +++ b/docs/specs/person-title-type-fields-spec.html @@ -0,0 +1,1020 @@ + + + + + +Person Title & Type Fields — Design Spec · Familienarchiv + + + +
+ + +
+
+
+

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 containerflex rounded-lg border border-line overflow-hidden mb-5h ~44px per buttonUse 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.514px / 700, py 10pxLast 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.514px / 700Active state replaces bg + text color. Icon inherits currentColor.
Type button iconw-4 h-416pxSame SVG paths as PersonTypeBadge + person silhouette for PERSON.
Title inputblock 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-ringmax-w 120px, py 8pxaria-label = i18n key for "Akademischer Titel". Only visible when type = PERSON.
Title labelmb-1 block text-xs font-bold tracking-widest text-ink-3 uppercase12px / 700Matches 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 16pxMobile (<md): grid-cols-[80px_1fr] — title + firstName share row, lastName goes below full-width.
Name row (INSTITUTION/GROUP)grid grid-cols-1 gap-4full widthSingle "Name" field using the lastName input. Label changes via conditional: form_label_name i18n key.
Conditional visibilitySvelte {#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-5min-h-[44px] per buttonBelow md breakpoint: 2×2 grid. Above md: single row (flex). Same role="radiogroup" pattern.
Title + firstName row (mobile)flex gap-4title w-[80px], firstName flex-1Below md: title shrinks to 80px. Above md: title gets 120px in the 3-col grid.
Default personTypelet 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 labeltext-xs font-bold uppercase tracking-widest text-ink-3 text-center12px / 700Only render when person.title is truthy AND personType === 'PERSON'. Placed between avatar and displayName.
Title spacingmb-0.52pxTight spacing to displayName below. When absent, avatar's mb-4 flows directly to name.
PersonCard proptitle?: string | null added to the person typeAlready in PersonSummaryDTO. Pass from +page.server.ts load.
+
+
+ + + +
+
4 Conditional Field Visibility Matrix
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldPersonInstitutionGroupUnknown
Title✓ visiblehiddenhiddenhidden
firstName✓ "Vorname"hiddenhiddenhidden
lastName✓ "Nachname"✓ "Name"✓ "Gruppenname"✓ "Name"
alias✓ visible✓ visible✓ visiblehidden
birthYear✓ "Geburtsjahr"hiddenhiddenhidden
deathYear✓ "Sterbejahr"hiddenhiddenhidden
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 statelet selectedType = $state<PersonType>('PERSON')Reactive state drives all conditionals. Type imported from generated API types.
showPersonFieldsconst showPersonFields = $derived(selectedType === 'PERSON')Controls title, firstName, birthYear, deathYear visibility.
showAliasconst showAlias = $derived(selectedType !== 'UNKNOWN')UNKNOWN hides alias (too little info to have one).
lastNameLabelconst 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 requiredrequired attribute only when selectedType === 'PERSON'Remove required on firstName when hidden. lastName stays required always.
+
+
+ + + +
+
5 Accessibility Requirements
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
RequirementWCAG CriterionImplementation
Segmented control semantics4.1.2 Name, Role, Valuerole="radiogroup" on container, role="radio" + aria-checked on each button. aria-label="Typ der Person" on container.
Keyboard navigation2.1.1 KeyboardArrow keys cycle through options. Tab moves out of the group. Space/Enter selects.
Touch target size2.5.8 Target Size (Minimum)Each segment button: min-h-[44px]. Mobile 2×2 grid: same minimum per cell.
Title input label1.3.1 Info and RelationshipsVisible <label for="title">. Also aria-describedby linking to help text if placeholder alone is ambiguous.
Hidden field announcement4.1.2 Name, Role, ValueWhen type changes, use aria-live="polite" region to announce which fields are now visible. E.g., "Felder für Person angezeigt".
Focus management2.4.3 Focus OrderTab 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"
  • +
+
+
+
+ +
+ +