feat(nav): center game context with hamburger menu for utilities

Complete redesign of minimal navigation for game pages:

**Layout Changes:**
- Game context (room info + players) now centered horizontally on viewport
- Hamburger menu (☰) in top-left with all utility items
- Responsive: game context shrinks to fit narrow viewports

**Hamburger Menu Features:**
- Opens on hover (instant access) OR click (traditional)
- Stays open while hovering menu or button
- Contains three organized sections:
  1. Navigation: Home, Create, Guide, Games
  2. Controls: Fullscreen toggle, Exit Arcade
  3. Abacus Style: Dropdown integrated inline

**Benefits:**
- Clean, uncluttered interface focused on game/players
- Utilities accessible but out of the way
- Discoverable affordance (visible hamburger icon)
- Works on mobile (click) and desktop (hover)
- Maintains fullscreen badge when active

Removed old top-right utility panes and CompactNavLink component.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-10-11 11:59:32 -05:00
parent ab0d8081d3
commit a35a7d56df

View File

@@ -1,8 +1,10 @@
'use client'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import Link from 'next/link'
import { usePathname, useRouter } from 'next/navigation'
import type React from 'react'
import { useState } from 'react'
import { css } from '../../styled-system/css'
import { container, hstack } from '../../styled-system/patterns'
import { useFullscreen } from '../contexts/FullscreenContext'
@@ -13,6 +15,440 @@ interface AppNavBarProps {
navSlot?: React.ReactNode
}
/**
* Hamburger menu component for utility navigation
*/
function HamburgerMenu({
isFullscreen,
isArcadePage,
pathname,
toggleFullscreen,
router,
}: {
isFullscreen: boolean
isArcadePage: boolean
pathname: string | null
toggleFullscreen: () => void
router: any
}) {
const [open, setOpen] = useState(false)
const [hovered, setHovered] = useState(false)
// Open on hover or click
const isOpen = open || hovered
return (
<DropdownMenu.Root open={isOpen} onOpenChange={setOpen}>
<DropdownMenu.Trigger asChild>
<button
type="button"
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '44px',
height: '44px',
padding: '8px',
background: isFullscreen ? 'rgba(0, 0, 0, 0.85)' : 'white',
border: isFullscreen ? '1px solid rgba(255, 255, 255, 0.1)' : '1px solid #e5e7eb',
borderRadius: '8px',
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
backdropFilter: isFullscreen ? 'blur(15px)' : 'none',
cursor: 'pointer',
transition: 'all 0.2s ease',
}}
>
<span
style={{
fontSize: '20px',
color: isFullscreen ? 'white' : '#374151',
}}
>
</span>
</button>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content
side="bottom"
align="start"
sideOffset={8}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{
background: 'linear-gradient(135deg, rgba(17, 24, 39, 0.97), rgba(31, 41, 55, 0.97))',
backdropFilter: 'blur(12px)',
borderRadius: '12px',
padding: '8px',
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(139, 92, 246, 0.3)',
minWidth: '220px',
zIndex: 9999,
animation: 'dropdownFadeIn 0.2s ease-out',
}}
>
{/* Site Navigation Section */}
<div
style={{
fontSize: '10px',
fontWeight: '600',
color: 'rgba(196, 181, 253, 0.7)',
marginBottom: '6px',
marginLeft: '12px',
textTransform: 'uppercase',
letterSpacing: '0.5px',
}}
>
Navigation
</div>
<DropdownMenu.Item asChild>
<Link
href="/"
style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '10px 14px',
borderRadius: '8px',
color: 'rgba(209, 213, 219, 1)',
fontSize: '14px',
fontWeight: '500',
textDecoration: 'none',
transition: 'all 0.2s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(139, 92, 246, 0.2)'
e.currentTarget.style.color = 'rgba(196, 181, 253, 1)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent'
e.currentTarget.style.color = 'rgba(209, 213, 219, 1)'
}}
>
<span style={{ fontSize: '16px' }}>🧮</span>
<span>Home</span>
</Link>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
<Link
href="/create"
style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '10px 14px',
borderRadius: '8px',
color: 'rgba(209, 213, 219, 1)',
fontSize: '14px',
fontWeight: '500',
textDecoration: 'none',
transition: 'all 0.2s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(139, 92, 246, 0.2)'
e.currentTarget.style.color = 'rgba(196, 181, 253, 1)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent'
e.currentTarget.style.color = 'rgba(209, 213, 219, 1)'
}}
>
<span style={{ fontSize: '16px' }}></span>
<span>Create</span>
</Link>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
<Link
href="/guide"
style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '10px 14px',
borderRadius: '8px',
color: 'rgba(209, 213, 219, 1)',
fontSize: '14px',
fontWeight: '500',
textDecoration: 'none',
transition: 'all 0.2s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(139, 92, 246, 0.2)'
e.currentTarget.style.color = 'rgba(196, 181, 253, 1)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent'
e.currentTarget.style.color = 'rgba(209, 213, 219, 1)'
}}
>
<span style={{ fontSize: '16px' }}>📖</span>
<span>Guide</span>
</Link>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
<Link
href="/games"
style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '10px 14px',
borderRadius: '8px',
color: 'rgba(209, 213, 219, 1)',
fontSize: '14px',
fontWeight: '500',
textDecoration: 'none',
transition: 'all 0.2s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(139, 92, 246, 0.2)'
e.currentTarget.style.color = 'rgba(196, 181, 253, 1)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent'
e.currentTarget.style.color = 'rgba(209, 213, 219, 1)'
}}
>
<span style={{ fontSize: '16px' }}>🎮</span>
<span>Games</span>
</Link>
</DropdownMenu.Item>
<DropdownMenu.Separator
style={{
height: '1px',
background: 'rgba(75, 85, 99, 0.5)',
margin: '6px 0',
}}
/>
{/* Controls Section */}
<div
style={{
fontSize: '10px',
fontWeight: '600',
color: 'rgba(196, 181, 253, 0.7)',
marginBottom: '6px',
marginLeft: '12px',
marginTop: '6px',
textTransform: 'uppercase',
letterSpacing: '0.5px',
}}
>
Controls
</div>
<DropdownMenu.Item
onSelect={toggleFullscreen}
style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '10px 14px',
borderRadius: '8px',
color: 'rgba(209, 213, 219, 1)',
fontSize: '14px',
fontWeight: '500',
cursor: 'pointer',
transition: 'all 0.2s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(59, 130, 246, 0.2)'
e.currentTarget.style.color = 'rgba(147, 197, 253, 1)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent'
e.currentTarget.style.color = 'rgba(209, 213, 219, 1)'
}}
>
<span style={{ fontSize: '16px' }}>{isFullscreen ? '🪟' : '⛶'}</span>
<span>{isFullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen'}</span>
</DropdownMenu.Item>
{isArcadePage && (
<DropdownMenu.Item
onSelect={() => router.push('/games')}
style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '10px 14px',
borderRadius: '8px',
color: 'rgba(209, 213, 219, 1)',
fontSize: '14px',
fontWeight: '500',
cursor: 'pointer',
transition: 'all 0.2s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(239, 68, 68, 0.2)'
e.currentTarget.style.color = 'rgba(252, 165, 165, 1)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent'
e.currentTarget.style.color = 'rgba(209, 213, 219, 1)'
}}
>
<span style={{ fontSize: '16px' }}>🚪</span>
<span>Exit Arcade</span>
</DropdownMenu.Item>
)}
<DropdownMenu.Separator
style={{
height: '1px',
background: 'rgba(75, 85, 99, 0.5)',
margin: '6px 0',
}}
/>
{/* Style Section */}
<div
style={{
fontSize: '10px',
fontWeight: '600',
color: 'rgba(196, 181, 253, 0.7)',
marginBottom: '6px',
marginLeft: '12px',
marginTop: '6px',
textTransform: 'uppercase',
letterSpacing: '0.5px',
}}
>
Abacus Style
</div>
<div style={{ padding: '0 6px' }}>
<AbacusDisplayDropdown isFullscreen={isFullscreen} />
</div>
</DropdownMenu.Content>
</DropdownMenu.Portal>
<style
dangerouslySetInnerHTML={{
__html: `
@keyframes dropdownFadeIn {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
`,
}}
/>
</DropdownMenu.Root>
)
}
/**
* Minimal navigation for game pages - centered game context with hamburger menu
*/
function MinimalNav({
isFullscreen,
isArcadePage,
pathname,
navSlot,
toggleFullscreen,
exitFullscreen,
router,
}: {
isFullscreen: boolean
isArcadePage: boolean
pathname: string | null
navSlot: React.ReactNode
toggleFullscreen: () => void
exitFullscreen: () => void
router: any
}) {
return (
<header
style={{
position: 'fixed',
top: '16px',
left: '16px',
right: '16px',
zIndex: 100,
display: 'flex',
justifyContent: 'center',
alignItems: 'flex-start',
pointerEvents: 'none',
}}
>
{/* Hamburger Menu - positioned absolutely on left */}
<div
style={{
position: 'absolute',
left: 0,
top: 0,
pointerEvents: 'auto',
}}
>
<HamburgerMenu
isFullscreen={isFullscreen}
isArcadePage={isArcadePage}
pathname={pathname}
toggleFullscreen={toggleFullscreen}
router={router}
/>
</div>
{/* Centered Game Context */}
{navSlot && (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '12px',
padding: '8px 16px',
background: isFullscreen ? 'rgba(0, 0, 0, 0.85)' : 'white',
border: isFullscreen ? '1px solid rgba(255, 255, 255, 0.1)' : '1px solid #e5e7eb',
borderRadius: '8px',
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
backdropFilter: isFullscreen ? 'blur(15px)' : 'none',
opacity: '0.95',
transition: 'opacity 0.3s ease',
pointerEvents: 'auto',
maxWidth: 'calc(100% - 128px)', // Leave space for hamburger + margin
}}
onMouseEnter={(e) => {
e.currentTarget.style.opacity = '1'
}}
onMouseLeave={(e) => {
e.currentTarget.style.opacity = '0.95'
}}
>
{navSlot}
{isFullscreen && (
<div
style={{
padding: '4px 8px',
background: 'rgba(34, 197, 94, 0.2)',
border: '1px solid rgba(34, 197, 94, 0.3)',
borderRadius: '9999px',
fontSize: '12px',
color: 'rgb(134, 239, 172)',
fontWeight: '600',
}}
>
FULLSCREEN
</div>
)}
</div>
)}
</header>
)
}
export function AppNavBar({ variant = 'full', navSlot }: AppNavBarProps) {
const pathname = usePathname()
const router = useRouter()
@@ -26,193 +462,15 @@ export function AppNavBar({ variant = 'full', navSlot }: AppNavBarProps) {
// Mini nav for games/arcade (both fullscreen and non-fullscreen)
if (actualVariant === 'minimal') {
return (
<header
className={css({
position: 'fixed',
top: isFullscreen ? '4' : '4',
right: '4',
zIndex: 100,
opacity: '0.95',
_hover: { opacity: '1' },
transition: 'all 0.3s ease',
})}
>
<div className={hstack({ gap: '2' })}>
{/* Game branding from slot */}
{navSlot && (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '12px',
padding: '8px 16px',
background: isFullscreen ? 'rgba(0, 0, 0, 0.85)' : 'white',
border: isFullscreen ? '1px solid rgba(255, 255, 255, 0.1)' : '1px solid #e5e7eb',
borderRadius: '8px',
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
backdropFilter: isFullscreen ? 'blur(15px)' : 'none',
}}
>
{navSlot}
{isFullscreen && (
<div
className={css({
px: '2',
py: '1',
background: 'rgba(34, 197, 94, 0.2)',
border: '1px solid rgba(34, 197, 94, 0.3)',
rounded: 'full',
fontSize: 'xs',
color: 'green.300',
fontWeight: 'semibold',
})}
>
FULLSCREEN
</div>
)}
</div>
)}
{/* Navigation Links */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '8px 12px',
background: isFullscreen ? 'rgba(0, 0, 0, 0.85)' : 'white',
border: isFullscreen ? '1px solid rgba(255, 255, 255, 0.1)' : '1px solid #e5e7eb',
borderRadius: '8px',
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
backdropFilter: isFullscreen ? 'blur(15px)' : 'none',
}}
>
<Link
href="/"
className={css({
display: 'flex',
alignItems: 'center',
fontSize: 'lg',
textDecoration: 'none',
color: isFullscreen ? 'white' : 'gray.700',
opacity: isFullscreen ? '0.8' : '1',
_hover: {
transform: 'scale(1.1)',
opacity: '1',
},
transition: 'all',
})}
title="Home"
>
🧮
</Link>
<div
className={css({
w: '1px',
h: '4',
bg: isFullscreen ? 'rgba(255, 255, 255, 0.2)' : 'gray.300',
})}
/>
<CompactNavLink
href="/create"
currentPath={pathname}
title="Create"
isFullscreen={isFullscreen}
>
</CompactNavLink>
<CompactNavLink
href="/guide"
currentPath={pathname}
title="Guide"
isFullscreen={isFullscreen}
>
📖
</CompactNavLink>
<CompactNavLink
href="/games"
currentPath={pathname}
title="Games"
isFullscreen={isFullscreen}
>
🎮
</CompactNavLink>
</div>
{/* Fullscreen Controls */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '8px 12px',
background: isFullscreen ? 'rgba(0, 0, 0, 0.85)' : 'white',
border: isFullscreen ? '1px solid rgba(255, 255, 255, 0.1)' : '1px solid #e5e7eb',
borderRadius: '8px',
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
backdropFilter: isFullscreen ? 'blur(15px)' : 'none',
}}
>
<button
onClick={toggleFullscreen}
title={isFullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen'}
className={css({
display: 'flex',
alignItems: 'center',
p: '1',
fontSize: 'md',
color: isFullscreen ? 'blue.300' : 'blue.600',
bg: isFullscreen ? 'rgba(59, 130, 246, 0.2)' : 'blue.50',
border: '1px solid',
borderColor: isFullscreen ? 'rgba(59, 130, 246, 0.3)' : 'blue.200',
rounded: 'md',
cursor: 'pointer',
transition: 'all 0.3s ease',
_hover: {
bg: isFullscreen ? 'rgba(59, 130, 246, 0.3)' : 'blue.100',
transform: 'scale(1.1)',
},
})}
>
{isFullscreen ? '🪟' : '⛶'}
</button>
{isArcadePage && (
<button
onClick={() => {
console.log(
'🔄 AppNavBar: Navigating to games with Next.js router (no page reload)'
)
router.push('/games')
}}
title="Exit Arcade"
className={css({
display: 'flex',
alignItems: 'center',
p: '1',
fontSize: 'md',
color: isFullscreen ? 'red.300' : 'red.600',
bg: isFullscreen ? 'rgba(239, 68, 68, 0.2)' : 'red.50',
border: '1px solid',
borderColor: isFullscreen ? 'rgba(239, 68, 68, 0.3)' : 'red.200',
rounded: 'md',
cursor: 'pointer',
transition: 'all 0.3s ease',
_hover: {
bg: isFullscreen ? 'rgba(239, 68, 68, 0.3)' : 'red.100',
transform: 'scale(1.1)',
},
})}
>
🚪
</button>
)}
</div>
{/* Abacus Display Dropdown */}
<AbacusDisplayDropdown isFullscreen={isFullscreen} />
</div>
</header>
<MinimalNav
isFullscreen={isFullscreen}
isArcadePage={isArcadePage}
pathname={pathname}
navSlot={navSlot}
toggleFullscreen={toggleFullscreen}
exitFullscreen={exitFullscreen}
router={router}
/>
)
}
@@ -307,47 +565,3 @@ function NavLink({
)
}
function CompactNavLink({
href,
currentPath,
title,
children,
isFullscreen = false,
}: {
href: string
currentPath: string | null
title: string
children: React.ReactNode
isFullscreen?: boolean
}) {
const isActive = currentPath === href || (href !== '/' && currentPath?.startsWith(href))
return (
<Link
href={href}
title={title}
className={css({
display: 'flex',
alignItems: 'center',
p: '1',
fontSize: 'md',
color: isFullscreen
? isActive
? 'white'
: 'rgba(255, 255, 255, 0.8)'
: isActive
? 'brand.600'
: 'gray.500',
rounded: 'md',
transition: 'all',
textDecoration: 'none',
_hover: {
color: isFullscreen ? 'white' : isActive ? 'brand.700' : 'gray.700',
transform: 'scale(1.1)',
},
})}
>
{children}
</Link>
)
}