feat: complete themed navigation system with game-specific chrome

Implement comprehensive game theming system where games declare their visual identity (name + background) that flows through to navigation chrome:

- Update AppNavBar with GameThemeContext integration and dynamic color calculation
- Enhance StandardGameLayout to accept and apply theme props
- Configure Memory Lightning with green-blue gradient theme
- Configure Memory Pairs with purple gradient theme
- Enable themed navigation backgrounds in fullscreen and non-fullscreen modes
- Display game names in mini navigation instead of generic labels

Games now have cohesive visual branding that extends from background through navigation chrome.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-09-28 12:07:33 -05:00
parent 618f5d2cb0
commit 0a4bf1765c
4 changed files with 71 additions and 13 deletions

View File

@@ -23,12 +23,16 @@ export function MemoryPairsGame() {
}, [setFullscreenElement])
return (
<StandardGameLayout>
<StandardGameLayout
theme={{
gameName: "Memory Pairs",
backgroundColor: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
}}
>
<div
ref={gameRef}
className={css({
flex: 1,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
padding: { base: '12px', sm: '16px', md: '20px' },
display: 'flex',
flexDirection: 'column',

View File

@@ -1731,12 +1731,16 @@ export default function MemoryQuizPage() {
}, [state.prefixAcceptanceTimeout])
return (
<StandardGameLayout>
<StandardGameLayout
theme={{
gameName: "Memory Lightning",
backgroundColor: "linear-gradient(to bottom right, #f0fdf4, #eff6ff)"
}}
>
<style dangerouslySetInnerHTML={{ __html: globalAnimations }} />
<div
style={{
background: 'linear-gradient(to bottom right, #f0fdf4, #eff6ff)',
flex: 1,
display: 'flex',
flexDirection: 'column',

View File

@@ -6,6 +6,7 @@ import { css } from '../../styled-system/css'
import { container, hstack } from '../../styled-system/patterns'
import { AbacusDisplayDropdown } from './AbacusDisplayDropdown'
import { useFullscreen } from '../contexts/FullscreenContext'
import { useGameTheme } from '../contexts/GameThemeContext'
interface AppNavBarProps {
variant?: 'full' | 'minimal'
@@ -17,6 +18,35 @@ export function AppNavBar({ variant = 'full' }: AppNavBarProps) {
const isGamePage = pathname?.startsWith('/games')
const isArcadePage = pathname?.startsWith('/arcade')
const { isFullscreen, toggleFullscreen, exitFullscreen } = useFullscreen()
const { theme: gameTheme } = useGameTheme()
// Helper function to get themed background colors
const getThemedBackground = (opacity: number = 0.85) => {
if (gameTheme?.backgroundColor) {
const color = gameTheme.backgroundColor
if (color.startsWith('#')) {
// Convert hex to rgba
const hex = color.slice(1)
const r = parseInt(hex.slice(0, 2), 16)
const g = parseInt(hex.slice(2, 4), 16)
const b = parseInt(hex.slice(4, 6), 16)
return `rgba(${r}, ${g}, ${b}, ${opacity})`
} else if (color.startsWith('rgb')) {
// Handle rgb/rgba formats
const match = color.match(/rgba?\(([^)]+)\)/)
if (match) {
const values = match[1].split(',').map(v => v.trim())
if (values.length >= 3) {
return `rgba(${values[0]}, ${values[1]}, ${values[2]}, ${opacity})`
}
}
} else if (color.startsWith('linear-gradient')) {
// For gradients, use a semi-transparent overlay
return `rgba(0, 0, 0, ${opacity})`
}
}
return isFullscreen ? 'rgba(0, 0, 0, 0.85)' : 'white'
}
// Auto-detect variant based on context
const actualVariant = variant === 'full' && (isGamePage || isArcadePage) ? 'minimal' : variant
@@ -34,7 +64,7 @@ export function AppNavBar({ variant = 'full' }: AppNavBarProps) {
transition: 'all 0.3s ease'
})}>
<div className={hstack({ gap: '2' })}>
{/* Arcade branding (fullscreen only) */}
{/* Game branding (fullscreen only) */}
{isFullscreen && (isArcadePage || isGamePage) && (
<div className={css({
display: 'flex',
@@ -42,7 +72,7 @@ export function AppNavBar({ variant = 'full' }: AppNavBarProps) {
gap: '3',
px: '4',
py: '2',
bg: 'rgba(0, 0, 0, 0.85)',
bg: getThemedBackground(0.85),
border: '1px solid rgba(255, 255, 255, 0.1)',
rounded: 'lg',
shadow: 'lg',
@@ -51,11 +81,11 @@ export function AppNavBar({ variant = 'full' }: AppNavBarProps) {
<h1 className={css({
fontSize: 'lg',
fontWeight: 'bold',
background: 'linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)',
background: gameTheme ? 'linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)' : 'linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)',
backgroundClip: 'text',
color: 'transparent'
})}>
🕹 {isArcadePage ? 'Arcade' : 'Game'}
🕹 {gameTheme?.gameName || (isArcadePage ? 'Arcade' : 'Game')}
</h1>
<div className={css({
px: '2',
@@ -79,7 +109,7 @@ export function AppNavBar({ variant = 'full' }: AppNavBarProps) {
gap: '2',
px: '3',
py: '2',
bg: isFullscreen ? 'rgba(0, 0, 0, 0.85)' : 'white',
bg: getThemedBackground(isFullscreen ? 0.85 : 1),
border: '1px solid',
borderColor: isFullscreen ? 'rgba(255, 255, 255, 0.1)' : 'gray.200',
rounded: 'lg',
@@ -128,7 +158,7 @@ export function AppNavBar({ variant = 'full' }: AppNavBarProps) {
gap: '2',
px: '3',
py: '2',
bg: isFullscreen ? 'rgba(0, 0, 0, 0.85)' : 'white',
bg: getThemedBackground(isFullscreen ? 0.85 : 1),
border: '1px solid',
borderColor: isFullscreen ? 'rgba(255, 255, 255, 0.1)' : 'gray.200',
rounded: 'lg',

View File

@@ -1,11 +1,16 @@
'use client'
import { ReactNode } from 'react'
import { ReactNode, useEffect } from 'react'
import { css } from '../../styled-system/css'
import { useGameTheme } from '../contexts/GameThemeContext'
interface StandardGameLayoutProps {
children: ReactNode
className?: string
theme?: {
gameName: string
backgroundColor: string
}
}
/**
@@ -15,7 +20,19 @@ interface StandardGameLayoutProps {
* 3. Perfect viewport fit on all devices
* 4. Consistent experience across all games
*/
export function StandardGameLayout({ children, className }: StandardGameLayoutProps) {
export function StandardGameLayout({ children, className, theme }: StandardGameLayoutProps) {
const { setTheme } = useGameTheme()
// Set the theme when component mounts and clean up on unmount
useEffect(() => {
if (theme) {
setTheme(theme)
}
return () => {
setTheme(null)
}
}, [theme, setTheme])
return (
<div className={css({
// Exact viewport sizing - no scrolling ever
@@ -35,7 +52,10 @@ export function StandardGameLayout({ children, className }: StandardGameLayoutPr
// Flex container for game content
display: 'flex',
flexDirection: 'column'
flexDirection: 'column',
// Apply the theme background if provided
background: theme?.backgroundColor || 'transparent'
}, className)}>
{children}
</div>