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 @@ + + +
+ + +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"