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.
personTypetitle input (narrow column) added before firstNamepersonType fieldtitle + personType| 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. |
|
| 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'). |
| 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. | |
personType value changes on submission.
| 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. |
|
personType field to PersonUpdateDTO with @NotNull validationPersonService.update(): set person.setPersonType(dto.getPersonType())PersonService.create(): same — accept type from DTOSKIP value: exclude from API validation (backend-only, used by import)PersonEditForm.svelte: add selectedType state + segmented controltitle input to name row (narrow, max-w-[120px]){#if showPersonFields}PersonCard.svelte prop type: add title+page.server.ts files: read/submit title + personTypeform_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"