Files
familienarchiv/frontend/src/routes/stammbaum/+page.svelte
Marcel 1d14c32c23 fix(stammbaum): WCAG min font-size and 44px touch targets
Raise chip labels from 10px to 12px (text-xs) in StammbaumCard,
StammbaumSidePanel and StammbaumTree SVG text. Widen zoom buttons
from 32px to 44px for senior-audience touch targets.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00

119 lines
3.5 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import { m } from '$lib/paraglide/messages.js';
import { page } from '$app/state';
import StammbaumTree from '$lib/components/StammbaumTree.svelte';
import StammbaumSidePanel from '$lib/components/StammbaumSidePanel.svelte';
import type { components } from '$lib/generated/api';
type PersonNodeDTO = components['schemas']['PersonNodeDTO'];
type RelationshipDTO = components['schemas']['RelationshipDTO'];
interface Props {
data: { nodes: PersonNodeDTO[]; edges: RelationshipDTO[] };
}
let { data }: Props = $props();
const focusId = $derived(page.url.searchParams.get('focus'));
const canWrite = $derived<boolean>(page.data.canWrite ?? false);
let selectedId = $state<string | null>(null);
$effect(() => {
if (focusId && data.nodes.some((n) => n.id === focusId)) {
selectedId = focusId;
}
});
const selectedNode = $derived(data.nodes.find((n) => n.id === selectedId) ?? null);
let zoom = $state(1);
function zoomIn() {
zoom = Math.min(2, zoom + 0.1);
}
function zoomOut() {
zoom = Math.max(0.4, zoom - 0.1);
}
</script>
<!-- 4.25rem = 4rem navbar (h-16) + 0.25rem accent strip (h-1).
-my-6 cancels <main>'s py-6 so the canvas sits flush against the navbar
on top and the viewport edge on the bottom. -->
<div class="-my-6 flex h-[calc(100dvh-4.25rem)] flex-col">
<header
class="flex shrink-0 items-center justify-between border-b border-line bg-surface px-6 py-4"
>
<h1 class="font-serif text-2xl text-ink">{m.nav_stammbaum()}</h1>
{#if data.nodes.length > 0}
<div class="flex items-center gap-2">
<button
type="button"
onclick={zoomOut}
aria-label={m.stammbaum_zoom_out()}
class="inline-flex h-11 w-11 items-center justify-center rounded-sm border border-line bg-surface text-ink-2 transition hover:bg-muted"
>
</button>
<button
type="button"
onclick={zoomIn}
aria-label={m.stammbaum_zoom_in()}
class="inline-flex h-11 w-11 items-center justify-center rounded-sm border border-line bg-surface text-ink-2 transition hover:bg-muted"
>
+
</button>
</div>
{/if}
</header>
{#if data.nodes.length === 0}
<div class="flex flex-1 items-center justify-center p-8">
<div
class="mx-auto max-w-md rounded-sm border border-line bg-surface p-10 text-center shadow-sm"
>
<svg
class="mx-auto mb-4 h-12 w-12 text-ink-3"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
aria-hidden="true"
>
<circle cx="12" cy="5" r="2.5" />
<circle cx="6" cy="14" r="2.5" />
<circle cx="18" cy="14" r="2.5" />
<path stroke-linecap="round" d="M12 7.5v3M9.5 12.5L9 14M14.5 12.5l.5 1.5" />
</svg>
<h2 class="mb-2 font-serif text-xl text-ink">{m.stammbaum_empty_heading()}</h2>
<p class="mb-4 font-serif text-sm text-ink-2">{m.stammbaum_empty_body()}</p>
<a
href="/persons"
class="inline-block font-sans text-sm font-medium text-primary hover:underline"
>
{m.stammbaum_empty_link()}
</a>
</div>
</div>
{:else}
<div class="flex flex-1 overflow-hidden">
<div class="flex-1 overflow-auto bg-muted/20">
<StammbaumTree
nodes={data.nodes}
edges={data.edges}
selectedId={selectedId}
zoom={zoom}
onSelect={(id) => (selectedId = id)}
/>
</div>
{#if selectedNode}
<aside class="w-[320px] shrink-0 overflow-y-auto border-l border-line bg-surface">
<StammbaumSidePanel
node={selectedNode}
canWrite={canWrite}
onClose={() => (selectedId = null)}
/>
</aside>
{/if}
</div>
{/if}
</div>