diff --git a/Microsoft/Windows/PowerShell/ModuleAnalysisCache b/Microsoft/Windows/PowerShell/ModuleAnalysisCache new file mode 100644 index 00000000..fa9dde86 Binary files /dev/null and b/Microsoft/Windows/PowerShell/ModuleAnalysisCache differ diff --git a/docs/MOBILE_NAVIGATION_RESPONSIVE_DESIGN.md b/docs/MOBILE_NAVIGATION_RESPONSIVE_DESIGN.md new file mode 100644 index 00000000..59c796cd --- /dev/null +++ b/docs/MOBILE_NAVIGATION_RESPONSIVE_DESIGN.md @@ -0,0 +1,7 @@ +# Mobile Navigation Responsive Design + +`MobileNavigation` renders as a bottom tab bar on compact and portrait screens. It switches to a left-side rail only when the viewport is at least `640px` wide and in landscape orientation. + +This keeps the navigation reachable on phones while avoiding an unexpected side rail on narrow portrait layouts. The component also preserves safe-area padding with `env(safe-area-inset-*)`, remains hidden at the `lg` breakpoint, and uses WAI-ARIA tab semantics with roving keyboard focus. + +Responsive behavior is covered by `src/components/mobile/__tests__/MobileNavigation.test.tsx`. diff --git a/src/components/mobile/MobileNavigation.tsx b/src/components/mobile/MobileNavigation.tsx index 112f4643..495435ad 100644 --- a/src/components/mobile/MobileNavigation.tsx +++ b/src/components/mobile/MobileNavigation.tsx @@ -1,18 +1,52 @@ -import React, { useState } from 'react'; +'use client'; + +import React, { useState, useEffect, useRef } from 'react'; import { Home, Search, BookOpen, User } from 'lucide-react'; interface NavItem { id: string; label: string; icon: React.ReactNode; - onClick?: () => void; } +const railNavClasses = [ + '[@media_(min-width:640px)_and_(orientation:landscape)]:top-0', + '[@media_(min-width:640px)_and_(orientation:landscape)]:bottom-0', + '[@media_(min-width:640px)_and_(orientation:landscape)]:left-0', + '[@media_(min-width:640px)_and_(orientation:landscape)]:right-auto', + '[@media_(min-width:640px)_and_(orientation:landscape)]:h-dvh', + '[@media_(min-width:640px)_and_(orientation:landscape)]:w-20', + '[@media_(min-width:640px)_and_(orientation:landscape)]:border-t-0', + '[@media_(min-width:640px)_and_(orientation:landscape)]:border-r', +].join(' '); +const railListClasses = [ + '[@media_(min-width:640px)_and_(orientation:landscape)]:h-full', + '[@media_(min-width:640px)_and_(orientation:landscape)]:flex-col', + '[@media_(min-width:640px)_and_(orientation:landscape)]:justify-start', + '[@media_(min-width:640px)_and_(orientation:landscape)]:space-y-6', + '[@media_(min-width:640px)_and_(orientation:landscape)]:px-0', + '[@media_(min-width:640px)_and_(orientation:landscape)]:pt-8', +].join(' '); +const railListItemClasses = [ + '[@media_(min-width:640px)_and_(orientation:landscape)]:flex-none', + '[@media_(min-width:640px)_and_(orientation:landscape)]:px-2', +].join(' '); +const railButtonClasses = [ + '[@media_(min-width:640px)_and_(orientation:landscape)]:mx-auto', + '[@media_(min-width:640px)_and_(orientation:landscape)]:h-14', + '[@media_(min-width:640px)_and_(orientation:landscape)]:w-14', + '[@media_(min-width:640px)_and_(orientation:landscape)]:max-w-none', + '[@media_(min-width:640px)_and_(orientation:landscape)]:py-0', +].join(' '); +const railHiddenClass = '[@media_(min-width:640px)_and_(orientation:landscape)]:hidden'; +const railBlockClass = '[@media_(min-width:640px)_and_(orientation:landscape)]:block'; + export const MobileNavigation: React.FC<{ initialActive?: string; onNavChange?: (id: string) => void; }> = ({ initialActive = 'home', onNavChange }) => { const [activeTab, setActiveTab] = useState(initialActive); + const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]); const navItems: NavItem[] = [ { id: 'home', label: 'Home', icon: }, @@ -21,40 +55,107 @@ export const MobileNavigation: React.FC<{ { id: 'profile', label: 'Profile', icon: }, ]; + // Sync state with prop + useEffect(() => { + setActiveTab(initialActive); + }, [initialActive]); + const handleTabClick = (id: string) => { setActiveTab(id); if (onNavChange) onNavChange(id); }; + const handleKeyDown = (e: React.KeyboardEvent, index: number) => { + let nextIndex = -1; + const length = navItems.length; + + if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { + nextIndex = (index + 1) % length; + } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { + nextIndex = (index - 1 + length) % length; + } else if (e.key === 'Home') { + nextIndex = 0; + } else if (e.key === 'End') { + nextIndex = length - 1; + } + + if (nextIndex !== -1) { + e.preventDefault(); + buttonRefs.current[nextIndex]?.focus(); + } + }; + return (