refactor: move game controls into title dropdown menu

Replaces separate control buttons with elegant dropdown on game title,
dramatically cleaning up the navigation layout.

**New GameTitleMenu component:**
- Radix dropdown with subtle affordances (small ▼ indicator)
- Obvious but not dominating: hover background + rotating arrow
- Dark translucent menu with color-coded hover states:
  - Purple for Setup ⚙️
  - Blue for New Game 🎮
  - Orange for Quit 🏟️ (separated by divider)
- Shows only when controls are available (!canModifyPlayers)

**Layout improvements:**
- Collapsed from 2-row left column to single row
- Now: [Title▼] [spacer] [Mode] [Room] | [Players]
- Players remain at full height, even more prominent
- Much cleaner, more space-efficient design

Result: Game controls are discoverable but hidden until needed, letting
the player avatars truly dominate the navigation.

🤖 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 08:05:14 -05:00
parent 4c0dc12204
commit e39a0313cb
2 changed files with 268 additions and 48 deletions

View File

@@ -2,8 +2,8 @@ import React from 'react'
import { ActivePlayersList } from './ActivePlayersList'
import { AddPlayerButton } from './AddPlayerButton'
import { FullscreenPlayerSelection } from './FullscreenPlayerSelection'
import { GameControlButtons } from './GameControlButtons'
import { GameModeIndicator } from './GameModeIndicator'
import { GameTitleMenu } from './GameTitleMenu'
import { NetworkPlayerIndicator } from './NetworkPlayerIndicator'
import { RoomInfo } from './RoomInfo'
@@ -146,66 +146,44 @@ export function GameContextNav({
width: 'auto',
}}
>
{/* Left side: Title and Controls in a column */}
{/* Left side: Title + Mode + Room (single row) */}
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '8px',
alignItems: 'center',
gap: '16px',
flex: 1,
}}
>
{/* Row 1: Title | Mode + Room */}
{/* Title with dropdown menu */}
<GameTitleMenu
navTitle={navTitle}
navEmoji={navEmoji}
onSetup={onSetup}
onNewGame={onNewGame}
onQuit={onExitSession}
showMenu={!canModifyPlayers}
/>
{/* Mode + Room */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '16px',
justifyContent: 'space-between',
gap: '8px',
marginLeft: 'auto',
}}
>
{/* Title */}
<h1
style={{
fontSize: '18px',
fontWeight: 'bold',
background: 'linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)',
backgroundClip: 'text',
color: 'transparent',
margin: 0,
whiteSpace: 'nowrap',
}}
>
{navEmoji && `${navEmoji} `}
{navTitle}
</h1>
<GameModeIndicator gameMode={gameMode} shouldEmphasize={shouldEmphasize} showFullscreenSelection={false} />
{/* Mode + Room */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
}}
>
<GameModeIndicator gameMode={gameMode} shouldEmphasize={shouldEmphasize} showFullscreenSelection={false} />
{roomInfo && (
<RoomInfo
roomName={roomInfo.roomName}
gameName={roomInfo.gameName}
playerCount={roomInfo.playerCount}
joinCode={roomInfo.joinCode}
shouldEmphasize={shouldEmphasize}
/>
)}
</div>
</div>
{/* Row 2: Control buttons */}
<div style={{ display: 'flex', alignItems: 'center' }}>
{!canModifyPlayers && (
<GameControlButtons onSetup={onSetup} onNewGame={onNewGame} onQuit={onExitSession} />
{roomInfo && (
<RoomInfo
roomName={roomInfo.roomName}
gameName={roomInfo.gameName}
playerCount={roomInfo.playerCount}
joinCode={roomInfo.joinCode}
shouldEmphasize={shouldEmphasize}
/>
)}
</div>
</div>

View File

@@ -0,0 +1,242 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import React from 'react'
interface GameTitleMenuProps {
navTitle: string
navEmoji?: string
onSetup?: () => void
onNewGame?: () => void
onQuit?: () => void
showMenu: boolean
}
export function GameTitleMenu({
navTitle,
navEmoji,
onSetup,
onNewGame,
onQuit,
showMenu,
}: GameTitleMenuProps) {
const [open, setOpen] = React.useState(false)
// Don't show dropdown if no menu items
if (!showMenu) {
return (
<h1
style={{
fontSize: '18px',
fontWeight: 'bold',
background: 'linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)',
backgroundClip: 'text',
color: 'transparent',
margin: 0,
whiteSpace: 'nowrap',
}}
>
{navEmoji && `${navEmoji} `}
{navTitle}
</h1>
)
}
return (
<DropdownMenu.Root open={open} onOpenChange={setOpen}>
<DropdownMenu.Trigger asChild>
<button
type="button"
style={{
fontSize: '18px',
fontWeight: 'bold',
border: 'none',
padding: '6px 10px',
margin: '-6px -10px',
whiteSpace: 'nowrap',
cursor: 'pointer',
display: 'inline-flex',
alignItems: 'center',
gap: '6px',
borderRadius: '8px',
background: open ? 'rgba(139, 92, 246, 0.08)' : 'transparent',
transition: 'all 0.2s ease',
}}
onMouseEnter={(e) => {
if (!open) {
e.currentTarget.style.background = 'rgba(139, 92, 246, 0.06)'
}
}}
onMouseLeave={(e) => {
if (!open) {
e.currentTarget.style.background = 'transparent'
}
}}
>
<span
style={{
background: 'linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)',
backgroundClip: 'text',
WebkitBackgroundClip: 'text',
color: 'transparent',
}}
>
{navEmoji && `${navEmoji} `}
{navTitle}
</span>
<span
style={{
fontSize: '9px',
color: 'rgba(139, 92, 246, 0.5)',
transition: 'transform 0.2s ease',
transform: open ? 'rotate(180deg)' : 'rotate(0deg)',
lineHeight: 0,
}}
>
</span>
</button>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content
side="bottom"
align="start"
sideOffset={8}
style={{
background: 'linear-gradient(135deg, rgba(17, 24, 39, 0.97), rgba(31, 41, 55, 0.97))',
backdropFilter: 'blur(12px)',
borderRadius: '12px',
padding: '6px',
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(139, 92, 246, 0.3)',
minWidth: '180px',
zIndex: 9999,
animation: 'dropdownFadeIn 0.2s ease-out',
}}
>
{onSetup && (
<DropdownMenu.Item
onSelect={onSetup}
style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '10px 14px',
borderRadius: '8px',
border: 'none',
background: 'transparent',
color: 'rgba(209, 213, 219, 1)',
fontSize: '14px',
fontWeight: '500',
cursor: 'pointer',
outline: '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>Setup</span>
</DropdownMenu.Item>
)}
{onNewGame && (
<DropdownMenu.Item
onSelect={onNewGame}
style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '10px 14px',
borderRadius: '8px',
border: 'none',
background: 'transparent',
color: 'rgba(209, 213, 219, 1)',
fontSize: '14px',
fontWeight: '500',
cursor: 'pointer',
outline: 'none',
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' }}>🎮</span>
<span>New Game</span>
</DropdownMenu.Item>
)}
{onQuit && (
<>
{(onSetup || onNewGame) && (
<DropdownMenu.Separator
style={{
height: '1px',
background: 'rgba(75, 85, 99, 0.5)',
margin: '4px 0',
}}
/>
)}
<DropdownMenu.Item
onSelect={onQuit}
style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '10px 14px',
borderRadius: '8px',
border: 'none',
background: 'transparent',
color: 'rgba(209, 213, 219, 1)',
fontSize: '14px',
fontWeight: '500',
cursor: 'pointer',
outline: 'none',
transition: 'all 0.2s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(251, 146, 60, 0.2)'
e.currentTarget.style.color = 'rgba(253, 186, 116, 1)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent'
e.currentTarget.style.color = 'rgba(209, 213, 219, 1)'
}}
>
<span style={{ fontSize: '16px' }}>🏟</span>
<span>Quit to Arcade</span>
</DropdownMenu.Item>
</>
)}
</DropdownMenu.Content>
</DropdownMenu.Portal>
<style
dangerouslySetInnerHTML={{
__html: `
@keyframes dropdownFadeIn {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
`,
}}
/>
</DropdownMenu.Root>
)
}