Files
familienarchiv/frontend/src/lib/document/transcription/ShortcutCheatsheet.svelte
Marcel ee728e3522 feat(transcribe): add ShortcutCheatsheet dialog overlay (#327)
Native <dialog aria-modal> cheatsheet: showModal()/close() bridge, close
button focused on open, eight grouped <kbd> rows (nav/edit/utility), an
autosave footer line, and a reduced-motion-guarded fade. Closes on Esc,
backdrop click, and the close button; "?" while open is a no-op. Adds the
shortcut_close_panel i18n key. 8 component tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 16:45:25 +02:00

105 lines
2.6 KiB
Svelte

<script lang="ts">
import { m } from '$lib/paraglide/messages.js';
let { open = false, onClose }: { open?: boolean; onClose: () => void } = $props();
let dialogEl = $state<HTMLDialogElement>();
let closeButton = $state<HTMLButtonElement>();
// Grouped navigation / editing / utility — whitespace dividers, no headers.
const groups = [
[
{ cap: 'j', label: m.shortcut_next_region() },
{ cap: 'k', label: m.shortcut_prev_region() }
],
[
{ cap: 'e', label: m.shortcut_toggle_mode() },
{ cap: 'n', label: m.shortcut_new_region() },
{ cap: 't', label: m.shortcut_toggle_training() },
{ cap: 'Entf', label: m.shortcut_delete_region() }
],
[
{ cap: 'Esc', label: m.shortcut_close_panel() },
{ cap: '?', label: m.shortcut_help() }
]
];
$effect(() => {
const el = dialogEl;
if (!el) return;
if (open && !el.open) {
el.showModal();
closeButton?.focus();
} else if (!open && el.open) {
el.close();
}
});
function handleBackdropClick(event: MouseEvent) {
if (event.target === dialogEl) onClose();
}
</script>
<dialog
bind:this={dialogEl}
aria-modal="true"
aria-labelledby="cheatsheet-title"
class="w-[calc(100%-2rem)] max-w-md rounded-sm border border-line bg-surface p-6 shadow-lg backdrop:bg-black/40"
onclose={onClose}
onclick={handleBackdropClick}
>
<div class="mb-5 flex items-center justify-between">
<h2 id="cheatsheet-title" class="font-serif text-lg font-bold text-ink">
{m.cheatsheet_title()}
</h2>
<button
bind:this={closeButton}
type="button"
onclick={onClose}
aria-label={m.cheatsheet_close()}
class="flex h-11 w-11 items-center justify-center rounded-sm text-ink-2 hover:bg-muted"
>
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-width="2" d="M6 6l12 12M18 6L6 18" />
</svg>
</button>
</div>
<div class="divide-y divide-line">
{#each groups as group, i (i)}
<div class="flex flex-col gap-2 py-3 first:pt-0 last:pb-0">
{#each group as shortcut (shortcut.cap)}
<div class="flex items-center justify-between gap-4">
<kbd
class="rounded border border-line bg-muted px-1.5 py-0.5 font-mono text-xs text-ink shadow-sm"
>{shortcut.cap}</kbd
>
<span class="flex-1 text-right font-serif text-sm text-ink">{shortcut.label}</span>
</div>
{/each}
</div>
{/each}
</div>
<p class="mt-5 border-t border-line pt-3.5 font-sans text-xs text-ink-3">
{m.cheatsheet_autosave_hint()}
</p>
</dialog>
<style>
@media (prefers-reduced-motion: no-preference) {
dialog[open] {
animation: fadeIn 150ms ease;
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>