feat(person): generation dropdown on Person edit/new forms (#689)

PersonEditForm.svelte gains a G 0…G 6 select inside the {#if isPerson}
block. min-h-[44px] meets WCAG 2.5.8 / dual-audience touch target.
generationStr is initialised via $state(untrack(...)) so prop reruns
never reset an in-progress edit (same pattern as selectedType).

Both /persons/[id]/edit and /persons/new form actions read the field
without the conditional-spread idiom — generation always lands in the
PUT/POST body. G 0 is a valid family-tree-root value the spread would
silently drop, and an empty option sends null so a human can clear the
field back to "unset".

i18n adds person_label_generation / person_option_generation_unset /
person_hint_generation in de/en/es. Drops the dead stammbaum_generations
key (zero callsites after the filter-chip removal in the spec).

Tests: dropdown render + hydration in the component, generation=0/3/null
arriving in the API body in the server actions.

Refs #689

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-28 15:55:25 +02:00
parent c0b500b692
commit 577dd3fcb1
10 changed files with 270 additions and 8 deletions

View File

@@ -16,6 +16,12 @@ let selectedType = $state<PersonType>(
)
);
// Match the selectedType initialiser pattern: untrack so a subsequent prop
// update (e.g. load() rerun) does not reset the user's in-progress edit.
let generationStr = $state(
untrack(() => (person.generation == null ? '' : String(person.generation)))
);
const isPerson = $derived(selectedType === 'PERSON');
const lastNameLabel = $derived(
selectedType === 'INSTITUTION' || selectedType === 'GROUP'
@@ -108,6 +114,28 @@ const inputCls =
class={inputCls}
/>
</div>
<div class="md:col-span-2">
<label for="generation" class={labelCls}>{m.person_label_generation()}</label>
<select
id="generation"
name="generation"
bind:value={generationStr}
class="block min-h-[44px] w-full rounded border border-line bg-surface px-3 py-2 font-serif text-ink focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
aria-describedby="generation-hint"
>
<option value="">{m.person_option_generation_unset()}</option>
<option value="0">G 0</option>
<option value="1">G 1</option>
<option value="2">G 2</option>
<option value="3">G 3</option>
<option value="4">G 4</option>
<option value="5">G 5</option>
<option value="6">G 6</option>
</select>
<p id="generation-hint" class="mt-1 font-sans text-xs text-ink-3">
{m.person_hint_generation()}
</p>
</div>
{/if}
<div class="md:col-span-2">