Files
vessel/frontend/src/lib/components/shared/ShortcutsModal.svelte
vikingowl a564f7ec77 fix: resolve all TypeScript errors and accessibility warnings
TypeScript error fixes:
- Fix UUID mock type in chunker.test.ts
- Remove invalid timestamp property from Message types in tests
- Fix mockFetch type in client.test.ts
- Add missing parameters property to tool definition in test

Accessibility fixes (109 → 40 warnings, remaining are CSS @apply):
- Add aria-labels to all toggle switches and icon-only buttons
- Add tabindex="-1" to all dialog elements with role="dialog"
- Add onkeydown handlers to modal backdrops for keyboard accessibility
- Fix form labels: change decorative labels to spans, use fieldset/legend for groups
- Convert fileInput variables to $state() for proper reactivity
- Fix closure captures in ThinkingBlock and HtmlPreview with $derived()
- Add role="region" to drag-and-drop zones
- Restore keyboard navigation to BranchNavigator

All 547 tests pass.
2026-01-23 15:33:25 +01:00

128 lines
3.4 KiB
Svelte

<script lang="ts">
/**
* ShortcutsModal - Display available keyboard shortcuts
*/
import { keyboardShortcuts, formatShortcut, type Shortcut } from '$lib/utils/keyboard';
interface Props {
isOpen: boolean;
onClose: () => void;
}
let { isOpen, onClose }: Props = $props();
// Get all registered shortcuts
const shortcuts = $derived(keyboardShortcuts.getShortcuts());
// Group shortcuts by category
const groupedShortcuts = $derived.by(() => {
const groups: Record<string, Shortcut[]> = {
'Navigation': [],
'Chat': [],
'General': []
};
for (const shortcut of shortcuts) {
if (['new-chat', 'search', 'toggle-sidenav'].includes(shortcut.id)) {
groups['Navigation'].push(shortcut);
} else if (['focus-input', 'send-message', 'stop-generation'].includes(shortcut.id)) {
groups['Chat'].push(shortcut);
} else {
groups['General'].push(shortcut);
}
}
return groups;
});
/**
* Handle backdrop click to close
*/
function handleBackdropClick(event: MouseEvent): void {
if (event.target === event.currentTarget) {
onClose();
}
}
/**
* Handle escape key to close
*/
function handleKeydown(event: KeyboardEvent): void {
if (event.key === 'Escape') {
onClose();
}
}
</script>
<svelte:window on:keydown={handleKeydown} />
{#if isOpen}
<!-- Backdrop -->
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
onclick={handleBackdropClick}
onkeydown={handleKeydown}
role="dialog"
aria-modal="true"
aria-labelledby="shortcuts-dialog-title"
tabindex="-1"
>
<!-- Dialog -->
<div class="mx-4 w-full max-w-md rounded-xl border border-theme bg-theme-primary shadow-2xl">
<!-- Header -->
<div class="flex items-center justify-between border-b border-theme px-6 py-4">
<h2 id="shortcuts-dialog-title" class="text-lg font-semibold text-theme-primary">
Keyboard Shortcuts
</h2>
<button
type="button"
onclick={onClose}
class="rounded-lg p-1.5 text-theme-muted transition-colors hover:bg-theme-secondary hover:text-theme-primary"
aria-label="Close dialog"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- Content -->
<div class="max-h-[60vh] overflow-y-auto px-6 py-4">
{#each Object.entries(groupedShortcuts) as [group, items]}
{#if items.length > 0}
<div class="mb-4 last:mb-0">
<h3 class="mb-2 text-xs font-semibold uppercase tracking-wider text-theme-muted">
{group}
</h3>
<div class="space-y-2">
{#each items as shortcut}
<div class="flex items-center justify-between">
<span class="text-sm text-theme-secondary">{shortcut.description}</span>
<kbd class="rounded bg-theme-secondary px-2 py-1 font-mono text-xs text-theme-muted">
{formatShortcut(shortcut.key, shortcut.modifiers)}
</kbd>
</div>
{/each}
</div>
</div>
{/if}
{/each}
</div>
<!-- Footer -->
<div class="border-t border-theme px-6 py-3">
<p class="text-center text-xs text-theme-muted">
Press <kbd class="rounded bg-theme-secondary px-1.5 py-0.5 font-mono">Shift+?</kbd> to toggle this panel
</p>
</div>
</div>
</div>
{/if}