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

@@ -174,12 +174,18 @@
"person_merge_warning": "Achtung: Diese Aktion ist nicht rückgängig zu machen.", "person_merge_warning": "Achtung: Diese Aktion ist nicht rückgängig zu machen.",
"person_label_notes": "Notizen", "person_label_notes": "Notizen",
"person_placeholder_notes": "Biographische Hinweise, Besonderheiten…", "person_placeholder_notes": "Biographische Hinweise, Besonderheiten…",
"person_label_birth_year": "Geburtsjahr", "person_label_birth_date": "Geburtsdatum",
"person_label_death_year": "Todesjahr", "person_label_death_date": "Sterbedatum",
"person_label_birth_date_precision": "Genauigkeit",
"person_label_death_date_precision": "Genauigkeit",
"person_precision_hint": "Wie genau ist dieses Datum bekannt?",
"person_precision_day": "Genaues Datum (Tag)",
"person_precision_month": "Monat bekannt",
"person_precision_year": "Nur Jahreszahl",
"person_date_placeholder_hint": "Leer lassen, wenn unbekannt",
"person_label_generation": "Generation", "person_label_generation": "Generation",
"person_option_generation_unset": "(keine)", "person_option_generation_unset": "(keine)",
"person_hint_generation": "Generation in der Familie (G 0 = älteste Generation)", "person_hint_generation": "Generation in der Familie (G 0 = älteste Generation)",
"person_placeholder_year": "z.B. 1923",
"person_year_error": "Bitte eine vierstellige Jahreszahl eingeben", "person_year_error": "Bitte eine vierstellige Jahreszahl eingeben",
"person_years_error_order": "Geburtsjahr muss vor dem Todesjahr liegen", "person_years_error_order": "Geburtsjahr muss vor dem Todesjahr liegen",
"person_docs_heading": "Gesendete Dokumente", "person_docs_heading": "Gesendete Dokumente",

View File

@@ -174,12 +174,18 @@
"person_merge_warning": "Warning: This action cannot be undone.", "person_merge_warning": "Warning: This action cannot be undone.",
"person_label_notes": "Notes", "person_label_notes": "Notes",
"person_placeholder_notes": "Biographical notes, remarks…", "person_placeholder_notes": "Biographical notes, remarks…",
"person_label_birth_year": "Birth year", "person_label_birth_date": "Date of birth",
"person_label_death_year": "Death year", "person_label_death_date": "Date of death",
"person_label_birth_date_precision": "Precision",
"person_label_death_date_precision": "Precision",
"person_precision_hint": "How precisely is this date known?",
"person_precision_day": "Exact date (day)",
"person_precision_month": "Month known",
"person_precision_year": "Year only",
"person_date_placeholder_hint": "Leave empty if unknown",
"person_label_generation": "Generation", "person_label_generation": "Generation",
"person_option_generation_unset": "(none)", "person_option_generation_unset": "(none)",
"person_hint_generation": "Generation within the family (G 0 = oldest generation)", "person_hint_generation": "Generation within the family (G 0 = oldest generation)",
"person_placeholder_year": "e.g. 1923",
"person_year_error": "Please enter a four-digit year", "person_year_error": "Please enter a four-digit year",
"person_years_error_order": "Birth year must be before death year", "person_years_error_order": "Birth year must be before death year",
"person_docs_heading": "Sent documents", "person_docs_heading": "Sent documents",

View File

@@ -174,12 +174,18 @@
"person_merge_warning": "Atención: Esta acción no se puede deshacer.", "person_merge_warning": "Atención: Esta acción no se puede deshacer.",
"person_label_notes": "Notas", "person_label_notes": "Notas",
"person_placeholder_notes": "Notas biográficas, observaciones…", "person_placeholder_notes": "Notas biográficas, observaciones…",
"person_label_birth_year": "Año de nacimiento", "person_label_birth_date": "Fecha de nacimiento",
"person_label_death_year": "Año de fallecimiento", "person_label_death_date": "Fecha de defunción",
"person_label_birth_date_precision": "Precisión",
"person_label_death_date_precision": "Precisión",
"person_precision_hint": "¿Con qué precisión se conoce esta fecha?",
"person_precision_day": "Fecha exacta (día)",
"person_precision_month": "Mes conocido",
"person_precision_year": "Solo año",
"person_date_placeholder_hint": "Dejar vacío si es desconocido",
"person_label_generation": "Generación", "person_label_generation": "Generación",
"person_option_generation_unset": "(ninguna)", "person_option_generation_unset": "(ninguna)",
"person_hint_generation": "Generación dentro de la familia (G 0 = generación más antigua)", "person_hint_generation": "Generación dentro de la familia (G 0 = generación más antigua)",
"person_placeholder_year": "p.ej. 1923",
"person_year_error": "Introduzca un año de cuatro dígitos", "person_year_error": "Introduzca un año de cuatro dígitos",
"person_years_error_order": "El año de nacimiento debe ser anterior al año de fallecimiento", "person_years_error_order": "El año de nacimiento debe ser anterior al año de fallecimiento",
"person_docs_heading": "Documentos enviados", "person_docs_heading": "Documentos enviados",

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; firstName?: string | null;
lastName: string; lastName: string;
alias?: string | null; alias?: string | null;
birthYear?: number | null; birthDate?: string | null;
deathYear?: number | null; birthDatePrecision?: string | null;
deathDate?: string | null;
deathDatePrecision?: string | null;
generation?: number | null; generation?: number | null;
notes?: string | null; notes?: string | null;
}; };

View File

@@ -1,6 +1,7 @@
import { error, fail, redirect } from '@sveltejs/kit'; import { error, fail, redirect } from '@sveltejs/kit';
import { createApiClient, extractErrorCode } from '$lib/shared/api.server'; import { createApiClient, extractErrorCode } from '$lib/shared/api.server';
import { getErrorMessage } from '$lib/shared/errors'; import { getErrorMessage } from '$lib/shared/errors';
import type { DatePrecision } from '$lib/shared/utils/documentDate';
import { import {
normalizePersonType, normalizePersonType,
validatePersonFields, validatePersonFields,
@@ -47,10 +48,17 @@ export const actions = {
const lastName = formData.get('lastName')?.toString().trim(); const lastName = formData.get('lastName')?.toString().trim();
const alias = formData.get('alias')?.toString().trim() || undefined; const alias = formData.get('alias')?.toString().trim() || undefined;
const notes = formData.get('notes')?.toString().trim() || undefined; const notes = formData.get('notes')?.toString().trim() || undefined;
const birthYearStr = formData.get('birthYear')?.toString().trim(); // Empty date input → omit date AND precision: the backend normalises the
const deathYearStr = formData.get('deathYear')?.toString().trim(); // absent pair to null/UNKNOWN, and a lone precision would fail the
const birthYear = birthYearStr ? parseInt(birthYearStr, 10) : undefined; // coherence check (INVALID_DATE_PRECISION).
const deathYear = deathYearStr ? parseInt(deathYearStr, 10) : undefined; const birthDate = formData.get('birthDate')?.toString().trim() || undefined;
const birthDatePrecision = birthDate
? (formData.get('birthDatePrecision')?.toString() as DatePrecision)
: undefined;
const deathDate = formData.get('deathDate')?.toString().trim() || undefined;
const deathDatePrecision = deathDate
? (formData.get('deathDatePrecision')?.toString() as DatePrecision)
: undefined;
// Must NOT use the conditional-spread idiom for generation: G 0 is a // Must NOT use the conditional-spread idiom for generation: G 0 is a
// valid family-tree-root value. The key always travels in the body so // valid family-tree-root value. The key always travels in the body so
// an explicit clear (empty option) reaches the backend as null. // an explicit clear (empty option) reaches the backend as null.
@@ -73,8 +81,8 @@ export const actions = {
lastName, lastName,
...(alias ? { alias } : {}), ...(alias ? { alias } : {}),
...(notes ? { notes } : {}), ...(notes ? { notes } : {}),
...(birthYear ? { birthYear } : {}), ...(birthDate ? { birthDate, birthDatePrecision } : {}),
...(deathYear ? { deathYear } : {}), ...(deathDate ? { deathDate, deathDatePrecision } : {}),
generation generation
} }
}); });

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { untrack } from 'svelte'; import { untrack } from 'svelte';
import { m } from '$lib/paraglide/messages.js'; import { m } from '$lib/paraglide/messages.js';
import PersonLifeDateField from '$lib/person/PersonLifeDateField.svelte';
import PersonTypeSelector from '$lib/person/PersonTypeSelector.svelte'; import PersonTypeSelector from '$lib/person/PersonTypeSelector.svelte';
import { import {
PERSON_TYPES as TYPES, PERSON_TYPES as TYPES,
@@ -88,32 +89,20 @@ const inputCls =
<label for="alias" class={labelCls}>{m.form_label_alias()}</label> <label for="alias" class={labelCls}>{m.form_label_alias()}</label>
<input id="alias" name="alias" type="text" value={person.alias ?? ''} class={inputCls} /> <input id="alias" name="alias" type="text" value={person.alias ?? ''} class={inputCls} />
</div> </div>
<div> <PersonLifeDateField
<label for="birthYear" class={labelCls}>{m.person_label_birth_year()}</label> name="birthDate"
<input legend={m.person_label_birth_date()}
id="birthYear" precisionLabel={m.person_label_birth_date_precision()}
name="birthYear" initialIso={person.birthDate ?? ''}
type="number" initialPrecision={person.birthDatePrecision ?? null}
min="1" />
max="2100" <PersonLifeDateField
placeholder={m.person_placeholder_year()} name="deathDate"
value={person.birthYear ?? ''} legend={m.person_label_death_date()}
class={inputCls} precisionLabel={m.person_label_death_date_precision()}
/> initialIso={person.deathDate ?? ''}
</div> initialPrecision={person.deathDatePrecision ?? null}
<div> />
<label for="deathYear" class={labelCls}>{m.person_label_death_year()}</label>
<input
id="deathYear"
name="deathYear"
type="number"
min="1"
max="2100"
placeholder={m.person_placeholder_year()}
value={person.deathYear ?? ''}
class={inputCls}
/>
</div>
<div class="md:col-span-2"> <div class="md:col-span-2">
<label for="generation" class={labelCls}>{m.person_label_generation()}</label> <label for="generation" class={labelCls}>{m.person_label_generation()}</label>
<select <select

View File

@@ -12,8 +12,10 @@ const personPersonal = {
firstName: 'Anna', firstName: 'Anna',
lastName: 'Schmidt', lastName: 'Schmidt',
alias: 'Anni', alias: 'Anni',
birthYear: 1899 as number | null, birthDate: '1899-03-14' as string | null,
deathYear: 1972 as number | null, birthDatePrecision: 'DAY' as string | null,
deathDate: '1972-01-01' as string | null,
deathDatePrecision: 'YEAR' as string | null,
notes: 'Wohnte in Berlin.' notes: 'Wohnte in Berlin.'
}; };
@@ -24,8 +26,10 @@ const personInstitution = {
firstName: null, firstName: null,
lastName: 'Acme GmbH', lastName: 'Acme GmbH',
alias: null, alias: null,
birthYear: null, birthDate: null,
deathYear: null, birthDatePrecision: null,
deathDate: null,
deathDatePrecision: null,
notes: null notes: null
}; };
@@ -42,14 +46,14 @@ describe('PersonEditForm', () => {
await expect.element(page.getByLabelText(/titel/i)).toBeVisible(); await expect.element(page.getByLabelText(/titel/i)).toBeVisible();
}); });
it('hides the firstName / title / alias / year fields for INSTITUTION', async () => { it('hides the firstName / title / alias / life-date fields for INSTITUTION', async () => {
render(PersonEditForm, { props: { person: personInstitution } }); render(PersonEditForm, { props: { person: personInstitution } });
await expect.element(page.getByLabelText(/vorname/i)).not.toBeInTheDocument(); await expect.element(page.getByLabelText(/vorname/i)).not.toBeInTheDocument();
await expect.element(page.getByLabelText(/^titel$/i)).not.toBeInTheDocument(); await expect.element(page.getByLabelText(/^titel$/i)).not.toBeInTheDocument();
await expect.element(page.getByLabelText(/rufname/i)).not.toBeInTheDocument(); await expect.element(page.getByLabelText(/rufname/i)).not.toBeInTheDocument();
await expect.element(page.getByLabelText(/geburtsjahr/i)).not.toBeInTheDocument(); await expect.element(page.getByLabelText(/^geburtsdatum$/i)).not.toBeInTheDocument();
await expect.element(page.getByLabelText(/todesjahr/i)).not.toBeInTheDocument(); await expect.element(page.getByLabelText(/^sterbedatum$/i)).not.toBeInTheDocument();
}); });
it('uses the "Nachname" label for PERSON', async () => { it('uses the "Nachname" label for PERSON', async () => {
@@ -77,13 +81,63 @@ describe('PersonEditForm', () => {
expect(title.value).toBe('Frau Dr.'); expect(title.value).toBe('Frau Dr.');
}); });
it('renders birthYear and deathYear inputs with prior values', async () => { it('renders an existing DAY-precision birth date as dd.mm.yyyy in the date input', async () => {
render(PersonEditForm, { props: { person: personPersonal } }); render(PersonEditForm, { props: { person: personPersonal } });
const birthYear = (await page.getByLabelText(/geburtsjahr/i).element()) as HTMLInputElement; const birthInput = (await page.getByLabelText(/^geburtsdatum$/i).element()) as HTMLInputElement;
const deathYear = (await page.getByLabelText(/todesjahr/i).element()) as HTMLInputElement; expect(birthInput.value).toBe('14.03.1899');
expect(birthYear.value).toBe('1899'); });
expect(deathYear.value).toBe('1972');
it('submits the ISO date via the hidden birthDate input', async () => {
const { container } = render(PersonEditForm, { props: { person: personPersonal } });
const hidden = container.querySelector('input[type="hidden"][name="birthDate"]');
expect((hidden as HTMLInputElement).value).toBe('1899-03-14');
});
it('offers only DAY / MONTH / YEAR precisions for life dates', async () => {
const { container } = render(PersonEditForm, { props: { person: personPersonal } });
const select = container.querySelector(
'select[name="birthDatePrecision"]'
) as HTMLSelectElement;
const values = Array.from(select.options).map((o) => o.value);
expect(values).toEqual(['DAY', 'MONTH', 'YEAR']);
});
it('hydrates the precision selects from the person prop', async () => {
const { container } = render(PersonEditForm, { props: { person: personPersonal } });
const birth = container.querySelector('select[name="birthDatePrecision"]') as HTMLSelectElement;
const death = container.querySelector('select[name="deathDatePrecision"]') as HTMLSelectElement;
expect(birth.value).toBe('DAY');
expect(death.value).toBe('YEAR');
});
it('seeds a non-form precision (APPROX legacy import) as YEAR instead of DAY', async () => {
const { container } = render(PersonEditForm, {
props: {
person: { ...personPersonal, birthDate: '1899-01-01', birthDatePrecision: 'APPROX' }
}
});
const birth = container.querySelector('select[name="birthDatePrecision"]') as HTMLSelectElement;
expect(birth.value).toBe('YEAR');
});
it('stacks the date input and precision select without overflow at 320px', async () => {
await page.viewport(320, 640);
const { container } = render(PersonEditForm, { props: { person: personPersonal } });
const input = container.querySelector('#birthDate') as HTMLElement;
const select = container.querySelector('select[name="birthDatePrecision"]') as HTMLElement;
expect(input.getBoundingClientRect().right).toBeLessThanOrEqual(320);
expect(select.getBoundingClientRect().right).toBeLessThanOrEqual(320);
// Stacked, not side by side: the select starts below the input.
expect(select.getBoundingClientRect().top).toBeGreaterThanOrEqual(
input.getBoundingClientRect().bottom
);
await page.viewport(1280, 720);
}); });
it('renders the notes textarea pre-filled with prior content', async () => { it('renders the notes textarea pre-filled with prior content', async () => {
@@ -103,15 +157,23 @@ describe('PersonEditForm', () => {
it('renders empty inputs when nullable fields are null', async () => { it('renders empty inputs when nullable fields are null', async () => {
render(PersonEditForm, { render(PersonEditForm, {
props: { person: { ...personPersonal, title: null, alias: null, birthYear: null } } props: {
person: {
...personPersonal,
title: null,
alias: null,
birthDate: null,
birthDatePrecision: null
}
}
}); });
const title = (await page.getByLabelText(/^titel/i).element()) as HTMLInputElement; const title = (await page.getByLabelText(/^titel/i).element()) as HTMLInputElement;
const alias = (await page.getByLabelText(/rufname/i).element()) as HTMLInputElement; const alias = (await page.getByLabelText(/rufname/i).element()) as HTMLInputElement;
const birthYear = (await page.getByLabelText(/geburtsjahr/i).element()) as HTMLInputElement; const birthInput = (await page.getByLabelText(/^geburtsdatum$/i).element()) as HTMLInputElement;
expect(title.value).toBe(''); expect(title.value).toBe('');
expect(alias.value).toBe(''); expect(alias.value).toBe('');
expect(birthYear.value).toBe(''); expect(birthInput.value).toBe('');
}); });
// ─── generation dropdown (#689) ───────────────────────────────────────────── // ─── generation dropdown (#689) ─────────────────────────────────────────────

View File

@@ -1,6 +1,7 @@
import { error, fail, redirect } from '@sveltejs/kit'; import { error, fail, redirect } from '@sveltejs/kit';
import { createApiClient, extractErrorCode } from '$lib/shared/api.server'; import { createApiClient, extractErrorCode } from '$lib/shared/api.server';
import { getErrorMessage } from '$lib/shared/errors'; import { getErrorMessage } from '$lib/shared/errors';
import type { DatePrecision } from '$lib/shared/utils/documentDate';
import { import {
normalizePersonType, normalizePersonType,
validatePersonFields, validatePersonFields,
@@ -23,8 +24,17 @@ export const actions = {
const firstName = formData.get('firstName')?.toString().trim(); const firstName = formData.get('firstName')?.toString().trim();
const lastName = formData.get('lastName')?.toString().trim(); const lastName = formData.get('lastName')?.toString().trim();
const alias = formData.get('alias')?.toString().trim() || undefined; const alias = formData.get('alias')?.toString().trim() || undefined;
const birthYearStr = formData.get('birthYear')?.toString().trim(); // Empty date input → omit date AND precision: the backend normalises the
const deathYearStr = formData.get('deathYear')?.toString().trim(); // absent pair to null/UNKNOWN, and a lone precision would fail the
// coherence check (INVALID_DATE_PRECISION).
const birthDate = formData.get('birthDate')?.toString().trim() || undefined;
const birthDatePrecision = birthDate
? (formData.get('birthDatePrecision')?.toString() as DatePrecision)
: undefined;
const deathDate = formData.get('deathDate')?.toString().trim() || undefined;
const deathDatePrecision = deathDate
? (formData.get('deathDatePrecision')?.toString() as DatePrecision)
: undefined;
const notes = formData.get('notes')?.toString().trim() || undefined; const notes = formData.get('notes')?.toString().trim() || undefined;
// Must NOT use the conditional-spread idiom for generation: G 0 is a // Must NOT use the conditional-spread idiom for generation: G 0 is a
// valid family-tree-root value. Always travels in the body so an // valid family-tree-root value. Always travels in the body so an
@@ -45,9 +55,6 @@ export const actions = {
}); });
} }
const birthYear = birthYearStr ? parseInt(birthYearStr, 10) : undefined;
const deathYear = deathYearStr ? parseInt(deathYearStr, 10) : undefined;
const api = createApiClient(fetch); const api = createApiClient(fetch);
const result = await api.POST('/api/persons', { const result = await api.POST('/api/persons', {
body: { body: {
@@ -56,8 +63,8 @@ export const actions = {
...(firstName ? { firstName } : {}), ...(firstName ? { firstName } : {}),
lastName: lastName!, lastName: lastName!,
...(alias ? { alias } : {}), ...(alias ? { alias } : {}),
...(birthYear ? { birthYear } : {}), ...(birthDate ? { birthDate, birthDatePrecision } : {}),
...(deathYear ? { deathYear } : {}), ...(deathDate ? { deathDate, deathDatePrecision } : {}),
...(notes ? { notes } : {}), ...(notes ? { notes } : {}),
generation generation
} }

View File

@@ -2,6 +2,7 @@
import { untrack } from 'svelte'; import { untrack } from 'svelte';
import { m } from '$lib/paraglide/messages.js'; import { m } from '$lib/paraglide/messages.js';
import BackButton from '$lib/shared/primitives/BackButton.svelte'; import BackButton from '$lib/shared/primitives/BackButton.svelte';
import PersonLifeDateField from '$lib/person/PersonLifeDateField.svelte';
import PersonTypeSelector from '$lib/person/PersonTypeSelector.svelte'; import PersonTypeSelector from '$lib/person/PersonTypeSelector.svelte';
import { PERSON_TYPES as TYPES, type PersonType } from '$lib/person/person-validation'; import { PERSON_TYPES as TYPES, type PersonType } from '$lib/person/person-validation';
@@ -102,30 +103,16 @@ const labelCls = 'mb-1 block text-sm font-medium text-ink-2';
class={inputCls} class={inputCls}
/> />
</div> </div>
<div> <PersonLifeDateField
<label for="birthYear" class={labelCls}>{m.person_label_birth_year()}</label> name="birthDate"
<input legend={m.person_label_birth_date()}
id="birthYear" precisionLabel={m.person_label_birth_date_precision()}
name="birthYear" />
type="number" <PersonLifeDateField
min="1" name="deathDate"
max="2100" legend={m.person_label_death_date()}
placeholder={m.person_placeholder_year()} precisionLabel={m.person_label_death_date_precision()}
class={inputCls} />
/>
</div>
<div>
<label for="deathYear" class={labelCls}>{m.person_label_death_year()}</label>
<input
id="deathYear"
name="deathYear"
type="number"
min="1"
max="2100"
placeholder={m.person_placeholder_year()}
class={inputCls}
/>
</div>
{/if} {/if}
<div class="md:col-span-2"> <div class="md:col-span-2">