feat(person): date + precision controls on person new/edit forms

New PersonLifeDateField (German date input + hidden ISO + DAY/MONTH/YEAR
precision select, min-h-44px, sm: side-by-side) used for birth and death
in both forms. Legacy APPROX precision seeds the select as YEAR so an
untouched save never claims DAY. Server actions send date+precision
pairs or omit both; obsolete year i18n keys removed, 9 form keys added.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-12 18:20:18 +02:00
parent 4dcf8e2242
commit 9664a83dae
10 changed files with 258 additions and 89 deletions

View File

@@ -0,0 +1,96 @@
<script lang="ts">
import { onMount } from 'svelte';
import { m } from '$lib/paraglide/messages.js';
import { isoToGerman, handleGermanDateInput } from '$lib/shared/utils/date';
import type { DatePrecision } from '$lib/shared/utils/documentDate';
// Only DAY / MONTH / YEAR are offered for life dates: RANGE and SEASON make no
// sense for a birth or death, and APPROX stays display-only for legacy imports (#773).
const PERSON_DATE_PRECISIONS: { value: DatePrecision; label: () => string }[] = [
{ value: 'DAY', label: m.person_precision_day },
{ value: 'MONTH', label: m.person_precision_month },
{ value: 'YEAR', label: m.person_precision_year }
];
let {
name,
legend,
precisionLabel,
initialIso = '',
initialPrecision = null
}: {
name: string;
legend: string;
precisionLabel: string;
initialIso?: string | null;
initialPrecision?: string | null;
} = $props();
let display = $state('');
let iso = $state('');
let precision = $state<DatePrecision>('DAY');
// Seed once at mount (WhoWhenSection pattern): a later load() rerun must not
// stomp the user's in-progress edit.
onMount(() => {
if (initialIso) {
iso = initialIso;
display = isoToGerman(initialIso);
}
const offered = PERSON_DATE_PRECISIONS.some((p) => p.value === initialPrecision);
if (offered) {
precision = initialPrecision as DatePrecision;
} else if (initialIso) {
// Legacy APPROX/SEASON/RANGE precision is not editable here — seed YEAR so an
// untouched save does not silently claim DAY precision for the stored date.
precision = 'YEAR';
}
});
function handleInput(e: Event) {
const result = handleGermanDateInput(e);
display = result.display;
iso = result.iso;
}
const controlCls =
'block min-h-[44px] w-full rounded border border-line px-3 py-2 font-serif text-ink focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring';
</script>
<fieldset>
<legend class="mb-1 block text-xs font-bold tracking-widest text-ink-3 uppercase">
{legend}
</legend>
<div class="flex flex-col gap-2 sm:flex-row">
<div class="flex-1">
<input
id={name}
type="text"
inputmode="numeric"
placeholder="TT.MM.JJJJ"
maxlength="10"
aria-label={legend}
value={display}
oninput={handleInput}
class={controlCls}
/>
<input type="hidden" name={name} value={iso} />
</div>
<div class="flex-1">
<select
id="{name}Precision"
name="{name}Precision"
aria-label="{legend}: {precisionLabel}"
bind:value={precision}
class="{controlCls} bg-surface"
>
{#each PERSON_DATE_PRECISIONS as p (p.value)}
<option value={p.value}>{p.label()}</option>
{/each}
</select>
</div>
</div>
<p class="mt-1 font-sans text-xs text-ink-3">
{m.person_precision_hint()} · {m.person_date_placeholder_hint()}
</p>
</fieldset>

View File

@@ -9,8 +9,10 @@ export type PersonFormData = {
firstName?: string | null;
lastName: string;
alias?: string | null;
birthYear?: number | null;
deathYear?: number | null;
birthDate?: string | null;
birthDatePrecision?: string | null;
deathDate?: string | null;
deathDatePrecision?: string | null;
generation?: number | null;
notes?: string | null;
};