Skip to content

Commit eb82249

Browse files
authored
Merge pull request #2 from modeitsch/interaction-improvements
feat: Add interaction improvements
2 parents 35667f1 + 08d0361 commit eb82249

6 files changed

Lines changed: 561 additions & 14 deletions

File tree

src/lib/components/Header.svelte

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
<script>
22
import { page } from '$app/stores';
33
import { theme, toggleTheme } from '../stores/theme.js';
4+
import { playHoverSound, playClickSound } from '../stores/sound.js';
45
import { Moon, Sun, Menu, X, Home, User, Briefcase, Mail, BookOpen } from 'lucide-svelte';
5-
6+
67
let mobileMenuOpen = false;
78
$: currentPath = $page.url.pathname;
8-
9+
910
function toggleMobileMenu() {
11+
playClickSound();
1012
mobileMenuOpen = !mobileMenuOpen;
1113
}
14+
15+
function handleThemeToggle() {
16+
playClickSound();
17+
toggleTheme();
18+
}
1219
</script>
1320

1421
<header class="sticky top-0 z-50 bg-white/95 dark:bg-gray-900/95 backdrop-blur-lg border-b border-gray-200/20 dark:border-gray-700/20">
@@ -24,27 +31,27 @@
2431
<!-- Desktop Navigation -->
2532
<div class="hidden md:block">
2633
<div class="ml-10 flex items-center space-x-12">
27-
<a href="/" class="relative group flex items-center space-x-2 text-sm font-medium transition-all duration-300 {currentPath === '/' ? 'text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400'}">
34+
<a href="/" on:mouseenter={playHoverSound} class="relative group flex items-center space-x-2 text-sm font-medium transition-all duration-300 {currentPath === '/' ? 'text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400'}">
2835
<Home size={16} />
2936
<span>Home</span>
3037
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-gradient-to-r from-blue-600 to-purple-600 group-hover:w-full transition-all duration-300 {currentPath === '/' ? 'w-full' : ''}"></span>
3138
</a>
32-
<a href="/about" class="relative group flex items-center space-x-2 text-sm font-medium transition-all duration-300 {currentPath === '/about' ? 'text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400'}">
39+
<a href="/about" on:mouseenter={playHoverSound} class="relative group flex items-center space-x-2 text-sm font-medium transition-all duration-300 {currentPath === '/about' ? 'text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400'}">
3340
<User size={16} />
3441
<span>About</span>
3542
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-gradient-to-r from-blue-600 to-purple-600 group-hover:w-full transition-all duration-300 {currentPath === '/about' ? 'w-full' : ''}"></span>
3643
</a>
37-
<a href="/projects" class="relative group flex items-center space-x-2 text-sm font-medium transition-all duration-300 {currentPath === '/projects' ? 'text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400'}">
44+
<a href="/projects" on:mouseenter={playHoverSound} class="relative group flex items-center space-x-2 text-sm font-medium transition-all duration-300 {currentPath === '/projects' ? 'text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400'}">
3845
<Briefcase size={16} />
3946
<span>Projects</span>
4047
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-gradient-to-r from-blue-600 to-purple-600 group-hover:w-full transition-all duration-300 {currentPath === '/projects' ? 'w-full' : ''}"></span>
4148
</a>
42-
<a href="/blog" class="relative group flex items-center space-x-2 text-sm font-medium transition-all duration-300 {currentPath.startsWith('/blog') ? 'text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400'}">
49+
<a href="/blog" on:mouseenter={playHoverSound} class="relative group flex items-center space-x-2 text-sm font-medium transition-all duration-300 {currentPath.startsWith('/blog') ? 'text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400'}">
4350
<BookOpen size={16} />
4451
<span>Blog</span>
4552
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-gradient-to-r from-blue-600 to-purple-600 group-hover:w-full transition-all duration-300 {currentPath.startsWith('/blog') ? 'w-full' : ''}"></span>
4653
</a>
47-
<a href="/contact" class="relative group flex items-center space-x-2 text-sm font-medium transition-all duration-300 {currentPath === '/contact' ? 'text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400'}">
54+
<a href="/contact" on:mouseenter={playHoverSound} class="relative group flex items-center space-x-2 text-sm font-medium transition-all duration-300 {currentPath === '/contact' ? 'text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400'}">
4855
<Mail size={16} />
4956
<span>Contact</span>
5057
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-gradient-to-r from-blue-600 to-purple-600 group-hover:w-full transition-all duration-300 {currentPath === '/contact' ? 'w-full' : ''}"></span>
@@ -55,7 +62,8 @@
5562
<!-- Theme toggle and mobile menu button -->
5663
<div class="flex items-center space-x-4">
5764
<button
58-
on:click={toggleTheme}
65+
on:click={handleThemeToggle}
66+
on:mouseenter={playHoverSound}
5967
class="p-3 text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-all duration-300 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-800 group"
6068
aria-label="Toggle theme"
6169
>
@@ -70,6 +78,7 @@
7078
<div class="md:hidden">
7179
<button
7280
on:click={toggleMobileMenu}
81+
on:mouseenter={playHoverSound}
7382
class="p-3 text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-all duration-300 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-800"
7483
aria-label="Toggle mobile menu"
7584
>
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
<script>
2+
import { onMount } from 'svelte';
3+
import { browser } from '$app/environment';
4+
import { goto } from '$app/navigation';
5+
6+
let visible = false;
7+
let showHints = false;
8+
let isMobile = false;
9+
10+
const shortcuts = [
11+
{ key: 'H', description: 'Home', action: () => goto('/') },
12+
{ key: 'A', description: 'About', action: () => goto('/about') },
13+
{ key: 'P', description: 'Projects', action: () => goto('/projects') },
14+
{ key: 'B', description: 'Blog', action: () => goto('/blog') },
15+
{ key: 'C', description: 'Contact', action: () => goto('/contact') },
16+
{ key: '?', description: 'Toggle hints', action: () => showHints = !showHints }
17+
];
18+
19+
onMount(() => {
20+
if (!browser) return;
21+
22+
// Check if mobile
23+
isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
24+
25+
if (isMobile) return;
26+
27+
// Show hints briefly on first visit
28+
const hasSeenHints = localStorage.getItem('keyboard-hints-seen');
29+
if (!hasSeenHints) {
30+
setTimeout(() => {
31+
visible = true;
32+
setTimeout(() => {
33+
visible = false;
34+
localStorage.setItem('keyboard-hints-seen', 'true');
35+
}, 5000);
36+
}, 3000);
37+
}
38+
39+
// Handle keyboard shortcuts
40+
function handleKeydown(e) {
41+
// Don't trigger if typing in input
42+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
43+
44+
const key = e.key.toUpperCase();
45+
46+
// Toggle hints with ?
47+
if (e.key === '?') {
48+
e.preventDefault();
49+
showHints = !showHints;
50+
return;
51+
}
52+
53+
// Navigation shortcuts (only when not holding modifier keys)
54+
if (!e.ctrlKey && !e.metaKey && !e.altKey) {
55+
const shortcut = shortcuts.find(s => s.key === key);
56+
if (shortcut && shortcut.key !== '?') {
57+
e.preventDefault();
58+
shortcut.action();
59+
}
60+
}
61+
}
62+
63+
document.addEventListener('keydown', handleKeydown);
64+
65+
return () => {
66+
document.removeEventListener('keydown', handleKeydown);
67+
};
68+
});
69+
</script>
70+
71+
{#if !isMobile}
72+
<!-- Hint indicator (bottom right) -->
73+
<button
74+
class="fixed bottom-4 right-4 z-40 p-2 rounded-lg bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 text-xs font-mono opacity-50 hover:opacity-100 transition-opacity duration-300 border border-gray-200 dark:border-gray-700"
75+
on:click={() => showHints = !showHints}
76+
aria-label="Toggle keyboard shortcuts"
77+
>
78+
<span class="kbd">?</span> shortcuts
79+
</button>
80+
81+
<!-- Initial hint popup -->
82+
{#if visible && !showHints}
83+
<div
84+
class="fixed bottom-16 right-4 z-40 px-4 py-2 rounded-lg bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-900 text-sm shadow-lg animate-fade-in"
85+
role="tooltip"
86+
>
87+
Press <span class="kbd kbd-dark">?</span> for keyboard shortcuts
88+
</div>
89+
{/if}
90+
91+
<!-- Full shortcuts panel -->
92+
{#if showHints}
93+
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
94+
<div
95+
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm animate-fade-in"
96+
on:click={() => showHints = false}
97+
on:keydown={(e) => e.key === 'Escape' && (showHints = false)}
98+
role="dialog"
99+
aria-modal="true"
100+
aria-label="Keyboard shortcuts"
101+
tabindex="-1"
102+
>
103+
<div
104+
class="bg-white dark:bg-gray-900 rounded-2xl shadow-2xl p-6 max-w-md mx-4 border border-gray-200 dark:border-gray-700"
105+
on:click|stopPropagation
106+
on:keydown|stopPropagation
107+
role="document"
108+
>
109+
<div class="flex items-center justify-between mb-4">
110+
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Keyboard Shortcuts</h2>
111+
<button
112+
class="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors"
113+
on:click={() => showHints = false}
114+
aria-label="Close"
115+
>
116+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
117+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
118+
</svg>
119+
</button>
120+
</div>
121+
122+
<div class="space-y-3">
123+
<div class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">Navigation</div>
124+
{#each shortcuts as shortcut}
125+
<div class="flex items-center justify-between py-1">
126+
<span class="text-gray-600 dark:text-gray-300">{shortcut.description}</span>
127+
<span class="kbd">{shortcut.key}</span>
128+
</div>
129+
{/each}
130+
<div class="border-t border-gray-200 dark:border-gray-700 pt-3 mt-3">
131+
<div class="flex items-center justify-between py-1">
132+
<span class="text-gray-600 dark:text-gray-300">Close this panel</span>
133+
<span class="kbd">Esc</span>
134+
</div>
135+
</div>
136+
</div>
137+
</div>
138+
</div>
139+
{/if}
140+
{/if}
141+
142+
<style>
143+
.kbd {
144+
display: inline-flex;
145+
align-items: center;
146+
justify-content: center;
147+
min-width: 1.5rem;
148+
padding: 0.125rem 0.375rem;
149+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
150+
font-size: 0.75rem;
151+
font-weight: 500;
152+
background: linear-gradient(180deg, #f3f4f6 0%, #e5e7eb 100%);
153+
border: 1px solid #d1d5db;
154+
border-radius: 0.25rem;
155+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.5);
156+
color: #374151;
157+
}
158+
159+
:global(html.dark) .kbd {
160+
background: linear-gradient(180deg, #374151 0%, #1f2937 100%);
161+
border-color: #4b5563;
162+
color: #e5e7eb;
163+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.1);
164+
}
165+
166+
.kbd-dark {
167+
background: linear-gradient(180deg, #374151 0%, #1f2937 100%);
168+
border-color: #4b5563;
169+
color: #e5e7eb;
170+
}
171+
172+
:global(html.dark) .kbd-dark {
173+
background: linear-gradient(180deg, #f3f4f6 0%, #e5e7eb 100%);
174+
border-color: #d1d5db;
175+
color: #374151;
176+
}
177+
178+
@keyframes fade-in {
179+
from {
180+
opacity: 0;
181+
transform: translateY(10px);
182+
}
183+
to {
184+
opacity: 1;
185+
transform: translateY(0);
186+
}
187+
}
188+
189+
.animate-fade-in {
190+
animation: fade-in 0.2s ease-out;
191+
}
192+
</style>

0 commit comments

Comments
 (0)