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:
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user