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>
105 lines
2.6 KiB
Svelte
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>
|