Enlarge the centre-on-person, panel-close, and affordance-dismiss icon buttons to 44x44 hit areas (WCAG 2.5.8, UX review) while keeping the small glyphs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
91 lines
2.4 KiB
Svelte
91 lines
2.4 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from 'svelte';
|
|
import { m } from '$lib/paraglide/messages.js';
|
|
|
|
interface Props {
|
|
/** Set true once the canvas receives its first pointer interaction. */
|
|
dismissed?: boolean;
|
|
/**
|
|
* Force touch mode on/off. When undefined, falls back to a
|
|
* `matchMedia('(pointer: coarse)')` check so the hint only appears on touch
|
|
* devices (OQ-008). Tests pass an explicit boolean.
|
|
*/
|
|
touch?: boolean;
|
|
}
|
|
|
|
let { dismissed = false, touch }: Props = $props();
|
|
|
|
// Boolean gate only — the stored timestamp is compared, never rendered to the
|
|
// DOM (Nora #692). 30-day re-show window (NFR-USE-001).
|
|
const STORAGE_KEY = 'stammbaumAffordanceDismissedAt';
|
|
const RESHOW_AFTER_MS = 30 * 24 * 60 * 60 * 1000;
|
|
|
|
function recentlyDismissed(): boolean {
|
|
try {
|
|
const raw = localStorage.getItem(STORAGE_KEY);
|
|
if (!raw) return false;
|
|
return Date.now() - Number(raw) < RESHOW_AFTER_MS;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function isTouch(): boolean {
|
|
if (touch !== undefined) return touch;
|
|
return (
|
|
typeof window !== 'undefined' &&
|
|
typeof window.matchMedia === 'function' &&
|
|
window.matchMedia('(pointer: coarse)').matches
|
|
);
|
|
}
|
|
|
|
let visible = $state(false);
|
|
onMount(() => {
|
|
visible = isTouch() && !recentlyDismissed();
|
|
});
|
|
|
|
function hide() {
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, String(Date.now()));
|
|
} catch {
|
|
/* storage unavailable — hide anyway for this session */
|
|
}
|
|
visible = false;
|
|
}
|
|
|
|
// First canvas interaction auto-dismisses the hint (Leonie).
|
|
$effect(() => {
|
|
if (dismissed && visible) hide();
|
|
});
|
|
</script>
|
|
|
|
{#if visible}
|
|
<div
|
|
class="pointer-events-none absolute inset-x-0 bottom-4 z-20 flex justify-center px-4"
|
|
role="status"
|
|
>
|
|
<div
|
|
class="pointer-events-auto flex items-center gap-2 rounded-full border border-line bg-surface/95 px-4 py-2 text-sm text-ink-2 shadow-sm"
|
|
>
|
|
<span>{m.stammbaum_affordance_hint()}</span>
|
|
<button
|
|
type="button"
|
|
onclick={hide}
|
|
aria-label={m.stammbaum_affordance_dismiss()}
|
|
class="-my-2 inline-flex h-11 w-11 items-center justify-center rounded-sm text-ink-3 transition hover:text-ink focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:outline-none"
|
|
>
|
|
<svg
|
|
class="h-3.5 w-3.5"
|
|
viewBox="0 0 16 16"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
aria-hidden="true"
|
|
>
|
|
<path stroke-linecap="round" d="M3 3l10 10M13 3L3 13" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{/if}
|