From c3a3ba824d9f5649c85ad36b6658184770d006e3 Mon Sep 17 00:00:00 2001 From: Vikrant Rajendra Kulkarni Date: Thu, 28 May 2026 01:39:18 +0530 Subject: [PATCH 1/4] feat: add keyboard shortcuts and accessibility navigation support --- app/components/navbar.tsx | 61 ++++++++-- app/layout.tsx | 80 ++++--------- components/GlobalShortcutListener.tsx | 20 ++++ components/ui/CommandPalette.tsx | 163 ++++++++++++++++++++++++++ components/ui/KeyboardHelpModal.tsx | 85 ++++++++++++++ components/ui/Modal.tsx | 75 ++++++++++++ context/ShortcutContext.tsx | 63 ++++++++++ hooks/useKeyboardShortcuts.ts | 113 ++++++++++++++++++ 8 files changed, 590 insertions(+), 70 deletions(-) create mode 100644 components/GlobalShortcutListener.tsx create mode 100644 components/ui/CommandPalette.tsx create mode 100644 components/ui/KeyboardHelpModal.tsx create mode 100644 components/ui/Modal.tsx create mode 100644 context/ShortcutContext.tsx create mode 100644 hooks/useKeyboardShortcuts.ts diff --git a/app/components/navbar.tsx b/app/components/navbar.tsx index 8268667b..3bc49186 100644 --- a/app/components/navbar.tsx +++ b/app/components/navbar.tsx @@ -1,8 +1,8 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useEffect, useState, useRef } from 'react'; import Link from 'next/link'; -import { Menu, X, Activity, Moon, Sun } from 'lucide-react'; +import { Menu, X, Activity, Moon, Sun, Search } from 'lucide-react'; import { useGlowEffect } from '@/hooks/useGlowEffect'; function GithubMark() { @@ -22,16 +22,25 @@ const NAV_LINKS = [ export default function Navbar() { const [open, setOpen] = useState(false); + const searchInputRef = useRef(null); + + // Mounted state to prevent hydration mismatch + const [mounted, setMounted] = useState(false); const [isDark, setIsDark] = useState(() => { if (typeof window === 'undefined') return true; - return localStorage.getItem('theme') !== 'light'; }); const { shellRef, shellVars, handleMouseEnter, handleMouseMove, handleMouseLeave } = useGlowEffect(); + // Set mounted to true once the client takes over + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect + setMounted(true); + }, []); + useEffect(() => { document.documentElement.classList.toggle('dark', isDark); localStorage.setItem('theme', isDark ? 'dark' : 'light'); @@ -43,29 +52,34 @@ export default function Navbar() { useEffect(() => { const mediaQuery = window.matchMedia('(min-width: 768px)'); - const handleBreakpointChange = (event: MediaQueryListEvent) => { if (event.matches) { setOpen(false); } }; - - // Defer the initial check so it doesn't cause a synchronous setState - // inside the effect body (which would trigger cascading re-renders). const initialCheckTimer = setTimeout(() => { if (mediaQuery.matches) { setOpen(false); } }, 0); - mediaQuery.addEventListener('change', handleBreakpointChange); - return () => { clearTimeout(initialCheckTimer); mediaQuery.removeEventListener('change', handleBreakpointChange); }; }, []); + // Listen for the global '/' shortcut + useEffect(() => { + const handleFocusSearch = () => { + searchInputRef.current?.focus(); + }; + document.addEventListener('focusSearch', handleFocusSearch as EventListener); + return () => { + document.removeEventListener('focusSearch', handleFocusSearch as EventListener); + }; + }, []); + const handleLogoClick = () => { setOpen(false); window.scrollTo({ top: 0, behavior: 'smooth' }); @@ -118,13 +132,27 @@ export default function Navbar() {
+
+ + + + / + +
+ {NAV_LINKS.map((link) => ( @@ -154,7 +182,18 @@ export default function Navbar() { {open ? (
-