feat: add cosmic fullscreen mode to abacus style dropdown
- Updated AbacusDisplayDropdown to accept isFullscreen prop - Added dark/cosmic styling that adapts to fullscreen mode - Updated trigger button, content panel, and all form controls - Enhanced FormField, SwitchField, and RadioGroupField components - Integrated with AppNavBar to pass fullscreen state - Ensures consistent cosmic theme across entire navigation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,9 @@
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@myriaddreamin/typst-all-in-one.ts": "0.6.1-rc3",
|
||||
"@myriaddreamin/typst-ts-renderer": "0.6.1-rc3",
|
||||
"@myriaddreamin/typst-ts-web-compiler": "0.6.1-rc3",
|
||||
|
||||
@@ -2,15 +2,19 @@
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { css } from '../../../styled-system/css'
|
||||
import { ChampionArena } from '../../components/ChampionArena'
|
||||
import { EnhancedChampionArena } from '../../components/EnhancedChampionArena'
|
||||
import { FullscreenProvider, useFullscreen } from '../../contexts/FullscreenContext'
|
||||
|
||||
function ArcadeContent() {
|
||||
const { isFullscreen, enterFullscreen, exitFullscreen } = useFullscreen()
|
||||
|
||||
useEffect(() => {
|
||||
// Automatically enter fullscreen when page loads
|
||||
enterFullscreen()
|
||||
// Check if we should enter fullscreen (from games page navigation)
|
||||
const shouldEnterFullscreen = sessionStorage.getItem('enterArcadeFullscreen')
|
||||
if (shouldEnterFullscreen === 'true') {
|
||||
sessionStorage.removeItem('enterArcadeFullscreen')
|
||||
enterFullscreen()
|
||||
}
|
||||
}, [enterFullscreen])
|
||||
|
||||
const handleExitArcade = async () => {
|
||||
@@ -41,80 +45,7 @@ function ArcadeContent() {
|
||||
animation: 'arcadeFloat 20s ease-in-out infinite'
|
||||
})} />
|
||||
|
||||
{/* Mini nav bar */}
|
||||
<div className={css({
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 100,
|
||||
background: 'rgba(0, 0, 0, 0.8)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
py: '2',
|
||||
px: '4'
|
||||
})}>
|
||||
<div className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
maxW: '6xl',
|
||||
mx: 'auto'
|
||||
})}>
|
||||
<div className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '3'
|
||||
})}>
|
||||
<h1 className={css({
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'bold',
|
||||
background: 'linear-gradient(135deg, #60a5fa, #a78bfa)',
|
||||
backgroundClip: 'text',
|
||||
color: 'transparent'
|
||||
})}>
|
||||
🕹️ Soroban Arcade
|
||||
</h1>
|
||||
|
||||
{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 MODE
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleExitArcade}
|
||||
className={css({
|
||||
px: '3',
|
||||
py: '1',
|
||||
background: 'rgba(239, 68, 68, 0.2)',
|
||||
border: '1px solid rgba(239, 68, 68, 0.3)',
|
||||
rounded: 'lg',
|
||||
color: 'red.300',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'semibold',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease',
|
||||
_hover: {
|
||||
background: 'rgba(239, 68, 68, 0.3)',
|
||||
transform: 'scale(1.05)'
|
||||
}
|
||||
})}
|
||||
>
|
||||
✕ Exit Arcade
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Note: Navigation is now handled by the enhanced AppNavBar */}
|
||||
|
||||
{/* Main content */}
|
||||
<div className={css({
|
||||
@@ -155,8 +86,8 @@ function ArcadeContent() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Full-screen Champion Arena */}
|
||||
<ChampionArena
|
||||
{/* Enhanced Full-screen Champion Arena */}
|
||||
<EnhancedChampionArena
|
||||
onConfigurePlayer={() => {}}
|
||||
className={css({
|
||||
background: 'rgba(255, 255, 255, 0.05)',
|
||||
@@ -172,11 +103,7 @@ function ArcadeContent() {
|
||||
}
|
||||
|
||||
export default function ArcadePage() {
|
||||
return (
|
||||
<FullscreenProvider>
|
||||
<ArcadeContent />
|
||||
</FullscreenProvider>
|
||||
)
|
||||
return <ArcadeContent />
|
||||
}
|
||||
|
||||
// Arcade-specific animations
|
||||
|
||||
@@ -2,14 +2,18 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { css } from '../../../styled-system/css'
|
||||
import { grid } from '../../../styled-system/patterns'
|
||||
import { useUserProfile } from '../../contexts/UserProfileContext'
|
||||
import { useGameMode } from '../../contexts/GameModeContext'
|
||||
import { FullscreenProvider, useFullscreen } from '../../contexts/FullscreenContext'
|
||||
|
||||
export default function GamesPage() {
|
||||
function GamesPageContent() {
|
||||
const { profile } = useUserProfile()
|
||||
const { gameMode, getActivePlayer } = useGameMode()
|
||||
const { enterFullscreen } = useFullscreen()
|
||||
const router = useRouter()
|
||||
|
||||
const handleGameClick = (gameType: string) => {
|
||||
// Navigate directly to games using the centralized game mode
|
||||
@@ -222,7 +226,19 @@ export default function GamesPage() {
|
||||
</p>
|
||||
|
||||
<button
|
||||
onClick={() => window.location.href = '/arcade'}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await enterFullscreen()
|
||||
// Set a flag so arcade knows to enter fullscreen
|
||||
sessionStorage.setItem('enterArcadeFullscreen', 'true')
|
||||
router.push('/arcade')
|
||||
} catch (error) {
|
||||
console.error('Failed to enter fullscreen:', error)
|
||||
// Navigate anyway if fullscreen fails
|
||||
sessionStorage.setItem('enterArcadeFullscreen', 'true')
|
||||
router.push('/arcade')
|
||||
}
|
||||
}}
|
||||
className={css({
|
||||
px: '12',
|
||||
py: '6',
|
||||
@@ -1129,6 +1145,10 @@ const globalAnimations = `
|
||||
}
|
||||
`
|
||||
|
||||
export default function GamesPage() {
|
||||
return <GamesPageContent />
|
||||
}
|
||||
|
||||
// Inject refined animations into the page
|
||||
if (typeof document !== 'undefined' && !document.getElementById('games-page-animations')) {
|
||||
const style = document.createElement('style')
|
||||
|
||||
@@ -3,6 +3,7 @@ import './globals.css'
|
||||
import { AbacusDisplayProvider } from '@/contexts/AbacusDisplayContext'
|
||||
import { UserProfileProvider } from '@/contexts/UserProfileContext'
|
||||
import { GameModeProvider } from '@/contexts/GameModeContext'
|
||||
import { FullscreenProvider } from '@/contexts/FullscreenContext'
|
||||
import { AppNavBar } from '@/components/AppNavBar'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -21,8 +22,10 @@ export default function RootLayout({
|
||||
<AbacusDisplayProvider>
|
||||
<UserProfileProvider>
|
||||
<GameModeProvider>
|
||||
<AppNavBar />
|
||||
{children}
|
||||
<FullscreenProvider>
|
||||
<AppNavBar />
|
||||
{children}
|
||||
</FullscreenProvider>
|
||||
</GameModeProvider>
|
||||
</UserProfileProvider>
|
||||
</AbacusDisplayProvider>
|
||||
|
||||
@@ -9,7 +9,11 @@ import { css } from '../../styled-system/css'
|
||||
import { stack, hstack } from '../../styled-system/patterns'
|
||||
import { useAbacusDisplay, ColorScheme, BeadShape } from '@/contexts/AbacusDisplayContext'
|
||||
|
||||
export function AbacusDisplayDropdown() {
|
||||
interface AbacusDisplayDropdownProps {
|
||||
isFullscreen?: boolean
|
||||
}
|
||||
|
||||
export function AbacusDisplayDropdown({ isFullscreen = false }: AbacusDisplayDropdownProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const { config, updateConfig, resetToDefaults } = useAbacusDisplay()
|
||||
|
||||
@@ -30,22 +34,23 @@ export function AbacusDisplayDropdown() {
|
||||
py: '2',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
color: 'gray.600',
|
||||
bg: 'white',
|
||||
color: isFullscreen ? 'white' : 'gray.600',
|
||||
bg: isFullscreen ? 'rgba(0, 0, 0, 0.85)' : 'white',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.200',
|
||||
borderColor: isFullscreen ? 'rgba(255, 255, 255, 0.1)' : 'gray.200',
|
||||
rounded: 'lg',
|
||||
shadow: 'sm',
|
||||
shadow: 'lg',
|
||||
backdropFilter: isFullscreen ? 'blur(15px)' : 'none',
|
||||
transition: 'all',
|
||||
cursor: 'pointer',
|
||||
_hover: {
|
||||
bg: 'gray.50',
|
||||
borderColor: 'gray.300'
|
||||
bg: isFullscreen ? 'rgba(0, 0, 0, 0.9)' : 'gray.50',
|
||||
borderColor: isFullscreen ? 'rgba(255, 255, 255, 0.2)' : 'gray.300'
|
||||
},
|
||||
_focus: {
|
||||
outline: 'none',
|
||||
ring: '2px',
|
||||
ringColor: 'brand.500',
|
||||
ringColor: isFullscreen ? 'blue.400' : 'brand.500',
|
||||
ringOffset: '1px'
|
||||
}
|
||||
})}
|
||||
@@ -71,11 +76,12 @@ export function AbacusDisplayDropdown() {
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
className={css({
|
||||
bg: 'white',
|
||||
bg: isFullscreen ? 'rgba(0, 0, 0, 0.85)' : 'white',
|
||||
rounded: 'xl',
|
||||
shadow: 'lg',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.200',
|
||||
borderColor: isFullscreen ? 'rgba(255, 255, 255, 0.1)' : 'gray.200',
|
||||
backdropFilter: isFullscreen ? 'blur(15px)' : 'none',
|
||||
p: '6',
|
||||
minW: '320px',
|
||||
maxW: '400px',
|
||||
@@ -94,7 +100,7 @@ export function AbacusDisplayDropdown() {
|
||||
<h3 className={css({
|
||||
fontSize: 'lg',
|
||||
fontWeight: 'semibold',
|
||||
color: 'gray.900'
|
||||
color: isFullscreen ? 'white' : 'gray.900'
|
||||
})}>
|
||||
🎨 Abacus Style
|
||||
</h3>
|
||||
@@ -102,8 +108,8 @@ export function AbacusDisplayDropdown() {
|
||||
onClick={resetToDefaults}
|
||||
className={css({
|
||||
fontSize: 'xs',
|
||||
color: 'gray.500',
|
||||
_hover: { color: 'gray.700' }
|
||||
color: isFullscreen ? 'gray.300' : 'gray.500',
|
||||
_hover: { color: isFullscreen ? 'white' : 'gray.700' }
|
||||
})}
|
||||
>
|
||||
Reset
|
||||
@@ -111,14 +117,14 @@ export function AbacusDisplayDropdown() {
|
||||
</div>
|
||||
<p className={css({
|
||||
fontSize: 'sm',
|
||||
color: 'gray.600'
|
||||
color: isFullscreen ? 'gray.300' : 'gray.600'
|
||||
})}>
|
||||
Configure display across the entire app
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Color Scheme */}
|
||||
<FormField label="Color Scheme">
|
||||
<FormField label="Color Scheme" isFullscreen={isFullscreen}>
|
||||
<RadioGroupField
|
||||
value={config.colorScheme}
|
||||
onValueChange={(value) => updateConfig({ colorScheme: value as ColorScheme })}
|
||||
@@ -128,11 +134,12 @@ export function AbacusDisplayDropdown() {
|
||||
{ value: 'heaven-earth', label: 'Heaven-Earth' },
|
||||
{ value: 'alternating', label: 'Alternating' }
|
||||
]}
|
||||
isFullscreen={isFullscreen}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
{/* Bead Shape */}
|
||||
<FormField label="Bead Shape">
|
||||
<FormField label="Bead Shape" isFullscreen={isFullscreen}>
|
||||
<RadioGroupField
|
||||
value={config.beadShape}
|
||||
onValueChange={(value) => updateConfig({ beadShape: value as BeadShape })}
|
||||
@@ -141,22 +148,25 @@ export function AbacusDisplayDropdown() {
|
||||
{ value: 'circle', label: '⭕ Circle' },
|
||||
{ value: 'square', label: '⬜ Square' }
|
||||
]}
|
||||
isFullscreen={isFullscreen}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
{/* Toggle Options */}
|
||||
<div className={stack({ gap: '4' })}>
|
||||
<FormField label="Hide Inactive Beads">
|
||||
<FormField label="Hide Inactive Beads" isFullscreen={isFullscreen}>
|
||||
<SwitchField
|
||||
checked={config.hideInactiveBeads}
|
||||
onCheckedChange={(checked) => updateConfig({ hideInactiveBeads: checked })}
|
||||
isFullscreen={isFullscreen}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Colored Numerals">
|
||||
<FormField label="Colored Numerals" isFullscreen={isFullscreen}>
|
||||
<SwitchField
|
||||
checked={config.coloredNumerals}
|
||||
onCheckedChange={(checked) => updateConfig({ coloredNumerals: checked })}
|
||||
isFullscreen={isFullscreen}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
@@ -170,17 +180,19 @@ export function AbacusDisplayDropdown() {
|
||||
// Helper Components (simplified versions of StyleControls components)
|
||||
function FormField({
|
||||
label,
|
||||
children
|
||||
children,
|
||||
isFullscreen = false
|
||||
}: {
|
||||
label: string
|
||||
children: React.ReactNode
|
||||
isFullscreen?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div className={stack({ gap: '2' })}>
|
||||
<Label.Root className={css({
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
color: 'gray.900'
|
||||
color: isFullscreen ? 'white' : 'gray.900'
|
||||
})}>
|
||||
{label}
|
||||
</Label.Root>
|
||||
@@ -191,10 +203,12 @@ function FormField({
|
||||
|
||||
function SwitchField({
|
||||
checked,
|
||||
onCheckedChange
|
||||
onCheckedChange,
|
||||
isFullscreen = false
|
||||
}: {
|
||||
checked: boolean
|
||||
onCheckedChange: (checked: boolean) => void
|
||||
isFullscreen?: boolean
|
||||
}) {
|
||||
return (
|
||||
<Switch.Root
|
||||
@@ -203,12 +217,18 @@ function SwitchField({
|
||||
className={css({
|
||||
w: '11',
|
||||
h: '6',
|
||||
bg: checked ? 'brand.600' : 'gray.300',
|
||||
bg: checked
|
||||
? (isFullscreen ? 'blue.500' : 'brand.600')
|
||||
: (isFullscreen ? 'rgba(255, 255, 255, 0.2)' : 'gray.300'),
|
||||
rounded: 'full',
|
||||
position: 'relative',
|
||||
transition: 'all',
|
||||
cursor: 'pointer',
|
||||
_hover: { bg: checked ? 'brand.700' : 'gray.400' }
|
||||
_hover: {
|
||||
bg: checked
|
||||
? (isFullscreen ? 'blue.600' : 'brand.700')
|
||||
: (isFullscreen ? 'rgba(255, 255, 255, 0.3)' : 'gray.400')
|
||||
}
|
||||
})}
|
||||
onClick={(e) => e.stopPropagation()} // Prevent dropdown close only on the switch itself
|
||||
>
|
||||
@@ -232,11 +252,13 @@ function SwitchField({
|
||||
function RadioGroupField({
|
||||
value,
|
||||
onValueChange,
|
||||
options
|
||||
options,
|
||||
isFullscreen = false
|
||||
}: {
|
||||
value: string
|
||||
onValueChange: (value: string) => void
|
||||
options: Array<{ value: string; label: string }>
|
||||
isFullscreen?: boolean
|
||||
}) {
|
||||
return (
|
||||
<RadioGroup.Root
|
||||
@@ -253,12 +275,12 @@ function RadioGroupField({
|
||||
h: '4',
|
||||
rounded: 'full',
|
||||
border: '2px solid',
|
||||
borderColor: 'gray.300',
|
||||
bg: 'white',
|
||||
borderColor: isFullscreen ? 'rgba(255, 255, 255, 0.3)' : 'gray.300',
|
||||
bg: isFullscreen ? 'rgba(255, 255, 255, 0.1)' : 'white',
|
||||
cursor: 'pointer',
|
||||
transition: 'all',
|
||||
_hover: { borderColor: 'brand.400' },
|
||||
'&[data-state=checked]': { borderColor: 'brand.600' }
|
||||
_hover: { borderColor: isFullscreen ? 'blue.400' : 'brand.400' },
|
||||
'&[data-state=checked]': { borderColor: isFullscreen ? 'blue.500' : 'brand.600' }
|
||||
})}
|
||||
onClick={(e) => e.stopPropagation()} // Prevent dropdown close only on radio button
|
||||
>
|
||||
@@ -276,7 +298,7 @@ function RadioGroupField({
|
||||
w: '1.5',
|
||||
h: '1.5',
|
||||
rounded: 'full',
|
||||
bg: 'brand.600'
|
||||
bg: isFullscreen ? 'blue.400' : 'brand.600'
|
||||
}
|
||||
})}
|
||||
/>
|
||||
@@ -284,7 +306,7 @@ function RadioGroupField({
|
||||
<label
|
||||
className={css({
|
||||
fontSize: 'sm',
|
||||
color: 'gray.900',
|
||||
color: isFullscreen ? 'white' : 'gray.900',
|
||||
cursor: 'pointer',
|
||||
flex: 1
|
||||
})}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { usePathname } from 'next/navigation'
|
||||
import { css } from '../../styled-system/css'
|
||||
import { container, hstack } from '../../styled-system/patterns'
|
||||
import { AbacusDisplayDropdown } from './AbacusDisplayDropdown'
|
||||
import { useFullscreen } from '../contexts/FullscreenContext'
|
||||
|
||||
interface AppNavBarProps {
|
||||
variant?: 'full' | 'minimal'
|
||||
@@ -13,35 +14,76 @@ interface AppNavBarProps {
|
||||
export function AppNavBar({ variant = 'full' }: AppNavBarProps) {
|
||||
const pathname = usePathname()
|
||||
const isGamePage = pathname?.startsWith('/games')
|
||||
const isArcadePage = pathname?.startsWith('/arcade')
|
||||
const { isFullscreen, toggleFullscreen, exitFullscreen } = useFullscreen()
|
||||
|
||||
// Auto-detect variant based on context
|
||||
const actualVariant = variant === 'full' && isGamePage ? 'minimal' : variant
|
||||
const actualVariant = variant === 'full' && (isGamePage || isArcadePage) ? 'minimal' : variant
|
||||
|
||||
// Mini nav for games/arcade (both fullscreen and non-fullscreen)
|
||||
if (actualVariant === 'minimal') {
|
||||
return (
|
||||
<header className={css({
|
||||
position: 'fixed',
|
||||
top: '4',
|
||||
top: isFullscreen ? '4' : '4',
|
||||
right: '4',
|
||||
zIndex: 40,
|
||||
// Make it less prominent during games
|
||||
opacity: '0.9',
|
||||
zIndex: 100,
|
||||
opacity: '0.95',
|
||||
_hover: { opacity: '1' },
|
||||
transition: 'all'
|
||||
transition: 'all 0.3s ease'
|
||||
})}>
|
||||
<div className={hstack({ gap: '3' })}>
|
||||
{/* Compact Navigation Menu */}
|
||||
<div className={hstack({ gap: '2' })}>
|
||||
{/* Arcade branding (fullscreen only) */}
|
||||
{isFullscreen && (isArcadePage || isGamePage) && (
|
||||
<div className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '3',
|
||||
px: '4',
|
||||
py: '2',
|
||||
bg: 'rgba(0, 0, 0, 0.85)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
rounded: 'lg',
|
||||
shadow: 'lg',
|
||||
backdropFilter: 'blur(15px)'
|
||||
})}>
|
||||
<h1 className={css({
|
||||
fontSize: 'lg',
|
||||
fontWeight: 'bold',
|
||||
background: 'linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)',
|
||||
backgroundClip: 'text',
|
||||
color: 'transparent'
|
||||
})}>
|
||||
🕹️ {isArcadePage ? 'Arcade' : 'Game'}
|
||||
</h1>
|
||||
<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 className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '2',
|
||||
px: '3',
|
||||
py: '2',
|
||||
bg: 'white',
|
||||
bg: isFullscreen ? 'rgba(0, 0, 0, 0.85)' : 'white',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.200',
|
||||
borderColor: isFullscreen ? 'rgba(255, 255, 255, 0.1)' : 'gray.200',
|
||||
rounded: 'lg',
|
||||
shadow: 'sm'
|
||||
shadow: 'lg',
|
||||
backdropFilter: isFullscreen ? 'blur(15px)' : 'none'
|
||||
})}>
|
||||
<Link
|
||||
href="/"
|
||||
@@ -50,25 +92,104 @@ export function AppNavBar({ variant = 'full' }: AppNavBarProps) {
|
||||
alignItems: 'center',
|
||||
fontSize: 'lg',
|
||||
textDecoration: 'none',
|
||||
_hover: { transform: 'scale(1.1)' },
|
||||
transition: 'transform'
|
||||
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: 'gray.300' })} />
|
||||
<CompactNavLink href="/create" currentPath={pathname} title="Create">
|
||||
<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">
|
||||
<CompactNavLink href="/guide" currentPath={pathname} title="Guide" isFullscreen={isFullscreen}>
|
||||
📖
|
||||
</CompactNavLink>
|
||||
<CompactNavLink href="/games" currentPath={pathname} title="Games">
|
||||
<CompactNavLink href="/games" currentPath={pathname} title="Games" isFullscreen={isFullscreen}>
|
||||
🎮
|
||||
</CompactNavLink>
|
||||
</div>
|
||||
<AbacusDisplayDropdown />
|
||||
|
||||
{/* Fullscreen Controls */}
|
||||
<div className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '2',
|
||||
px: '3',
|
||||
py: '2',
|
||||
bg: isFullscreen ? 'rgba(0, 0, 0, 0.85)' : 'white',
|
||||
border: '1px solid',
|
||||
borderColor: isFullscreen ? 'rgba(255, 255, 255, 0.1)' : 'gray.200',
|
||||
rounded: 'lg',
|
||||
shadow: 'lg',
|
||||
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={async () => {
|
||||
await exitFullscreen()
|
||||
window.location.href = '/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>
|
||||
)
|
||||
@@ -115,7 +236,7 @@ export function AppNavBar({ variant = 'full' }: AppNavBarProps) {
|
||||
</nav>
|
||||
|
||||
{/* Abacus Style Dropdown */}
|
||||
<AbacusDisplayDropdown />
|
||||
<AbacusDisplayDropdown isFullscreen={false} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -162,12 +283,14 @@ function CompactNavLink({
|
||||
href,
|
||||
currentPath,
|
||||
title,
|
||||
children
|
||||
children,
|
||||
isFullscreen = false
|
||||
}: {
|
||||
href: string
|
||||
currentPath: string | null
|
||||
title: string
|
||||
children: React.ReactNode
|
||||
isFullscreen?: boolean
|
||||
}) {
|
||||
const isActive = currentPath === href || (href !== '/' && currentPath?.startsWith(href))
|
||||
|
||||
@@ -180,12 +303,16 @@ function CompactNavLink({
|
||||
alignItems: 'center',
|
||||
p: '1',
|
||||
fontSize: 'md',
|
||||
color: isActive ? 'brand.600' : 'gray.500',
|
||||
color: isFullscreen
|
||||
? (isActive ? 'white' : 'rgba(255, 255, 255, 0.8)')
|
||||
: (isActive ? 'brand.600' : 'gray.500'),
|
||||
rounded: 'md',
|
||||
transition: 'all',
|
||||
textDecoration: 'none',
|
||||
_hover: {
|
||||
color: isActive ? 'brand.700' : 'gray.700',
|
||||
color: isFullscreen
|
||||
? 'white'
|
||||
: (isActive ? 'brand.700' : 'gray.700'),
|
||||
transform: 'scale(1.1)'
|
||||
}
|
||||
})}
|
||||
|
||||
598
apps/web/src/components/EnhancedChampionArena.tsx
Normal file
598
apps/web/src/components/EnhancedChampionArena.tsx
Normal file
@@ -0,0 +1,598 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useMemo } from 'react'
|
||||
import {
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
DragStartEvent,
|
||||
DragOverEvent,
|
||||
DragEndEvent,
|
||||
CollisionDetection,
|
||||
rectIntersection,
|
||||
getFirstCollision,
|
||||
pointerWithin,
|
||||
} from '@dnd-kit/core'
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
verticalListSortingStrategy,
|
||||
rectSortingStrategy,
|
||||
} from '@dnd-kit/sortable'
|
||||
import {
|
||||
useSortable,
|
||||
SortableContext as SortableContextType,
|
||||
} from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { useSpring, animated, useTransition, config } from '@react-spring/web'
|
||||
import { css } from '../../styled-system/css'
|
||||
import { useUserProfile } from '../contexts/UserProfileContext'
|
||||
import { useGameMode } from '../contexts/GameModeContext'
|
||||
import { GameSelector } from './GameSelector'
|
||||
|
||||
interface EnhancedChampionArenaProps {
|
||||
onGameModeChange?: (mode: 'single' | 'battle' | 'tournament') => void
|
||||
onConfigurePlayer?: (playerId: number) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
interface DraggablePlayer {
|
||||
id: number
|
||||
name: string
|
||||
emoji: string
|
||||
color: string
|
||||
isActive: boolean
|
||||
level: number
|
||||
}
|
||||
|
||||
// Animated Champion Card Component
|
||||
function ChampionCard({
|
||||
player,
|
||||
isOverlay = false,
|
||||
onConfigure,
|
||||
zone
|
||||
}: {
|
||||
player: DraggablePlayer
|
||||
isOverlay?: boolean
|
||||
onConfigure?: (id: number) => void
|
||||
zone: 'roster' | 'arena'
|
||||
}) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: player.id })
|
||||
|
||||
// React Spring animations
|
||||
const cardStyle = useSpring({
|
||||
transform: transform ? `translate3d(${transform.x}px, ${transform.y}px, 0)` : 'translate3d(0px, 0px, 0)',
|
||||
scale: isDragging ? 1.05 : 1,
|
||||
opacity: isDragging && !isOverlay ? 0.5 : 1,
|
||||
rotateZ: isDragging ? (Math.random() - 0.5) * 10 : 0,
|
||||
config: config.wobbly,
|
||||
})
|
||||
|
||||
const glowStyle = useSpring({
|
||||
boxShadow: zone === 'arena'
|
||||
? `0 0 ${isDragging ? '30px' : '20px'} ${player.color}${isDragging ? '80' : '40'}`
|
||||
: `0 ${isDragging ? '12px 30px' : '8px 20px'} rgba(0, 0, 0, ${isDragging ? '0.25' : '0.15'})`,
|
||||
config: config.gentle,
|
||||
})
|
||||
|
||||
const emojiStyle = useSpring({
|
||||
transform: isDragging ? 'scale(1.2) rotate(15deg)' : 'scale(1) rotate(0deg)',
|
||||
config: config.wobbly,
|
||||
})
|
||||
|
||||
return (
|
||||
<animated.div
|
||||
ref={setNodeRef}
|
||||
style={cardStyle}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className={css({
|
||||
position: 'relative',
|
||||
background: 'white',
|
||||
rounded: '2xl',
|
||||
p: '4',
|
||||
textAlign: 'center',
|
||||
cursor: isDragging ? 'grabbing' : 'grab',
|
||||
border: '3px solid',
|
||||
borderColor: player.color,
|
||||
width: '120px',
|
||||
minWidth: '120px',
|
||||
flexShrink: 0,
|
||||
userSelect: 'none',
|
||||
touchAction: 'none',
|
||||
transition: 'border-color 0.3s ease',
|
||||
zIndex: isDragging ? 1000 : 1,
|
||||
transformOrigin: 'center',
|
||||
})}
|
||||
>
|
||||
<animated.div style={glowStyle} className={css({
|
||||
position: 'absolute',
|
||||
top: '-3px',
|
||||
left: '-3px',
|
||||
right: '-3px',
|
||||
bottom: '-3px',
|
||||
rounded: '2xl',
|
||||
pointerEvents: 'none',
|
||||
})} />
|
||||
|
||||
{/* Configure Button */}
|
||||
{onConfigure && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onConfigure(player.id)
|
||||
}}
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
top: '2',
|
||||
right: '2',
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.300',
|
||||
rounded: 'full',
|
||||
w: '6',
|
||||
h: '6',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 'xs',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
zIndex: 10,
|
||||
_hover: {
|
||||
background: 'white',
|
||||
borderColor: player.color,
|
||||
transform: 'scale(1.1)'
|
||||
}
|
||||
})}
|
||||
>
|
||||
⚙️
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Remove Button for Arena */}
|
||||
{zone === 'arena' && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
// This will be handled by the parent
|
||||
}}
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
top: '-2',
|
||||
right: '-2',
|
||||
w: '6',
|
||||
h: '6',
|
||||
background: 'red.500',
|
||||
rounded: 'full',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 'xs',
|
||||
color: 'white',
|
||||
cursor: 'pointer',
|
||||
border: 'none',
|
||||
transition: 'all 0.3s ease',
|
||||
zIndex: 10,
|
||||
_hover: {
|
||||
background: 'red.600',
|
||||
transform: 'scale(1.1)'
|
||||
}
|
||||
})}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
|
||||
<animated.div
|
||||
style={emojiStyle}
|
||||
className={css({
|
||||
fontSize: '3xl',
|
||||
mb: '2',
|
||||
})}
|
||||
>
|
||||
{player.emoji}
|
||||
</animated.div>
|
||||
|
||||
<div className={css({
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'bold',
|
||||
color: 'gray.800'
|
||||
})}>
|
||||
{player.name}
|
||||
</div>
|
||||
|
||||
<div className={css({
|
||||
fontSize: 'xs',
|
||||
color: zone === 'arena' ? 'green.700' : 'gray.600',
|
||||
fontWeight: zone === 'arena' ? 'semibold' : 'normal',
|
||||
mt: '1'
|
||||
})}>
|
||||
{zone === 'arena' ? 'READY! 🔥' : `Level ${player.level}`}
|
||||
</div>
|
||||
</animated.div>
|
||||
)
|
||||
}
|
||||
|
||||
// Droppable Zone Component with animations
|
||||
function DroppableZone({
|
||||
id,
|
||||
children,
|
||||
title,
|
||||
subtitle,
|
||||
isDragOver,
|
||||
isEmpty
|
||||
}: {
|
||||
id: string
|
||||
children: React.ReactNode
|
||||
title: string
|
||||
subtitle: string
|
||||
isDragOver: boolean
|
||||
isEmpty: boolean
|
||||
}) {
|
||||
const zoneStyle = useSpring({
|
||||
background: isDragOver
|
||||
? (id === 'arena'
|
||||
? 'linear-gradient(135deg, #dcfce7, #bbf7d0)'
|
||||
: 'linear-gradient(135deg, #fef3c7, #fde68a)')
|
||||
: (id === 'arena'
|
||||
? 'linear-gradient(135deg, #fef3c7, #fde68a)'
|
||||
: 'linear-gradient(135deg, #f8fafc, #f1f5f9)'),
|
||||
borderColor: isDragOver ? (id === 'arena' ? '#4ade80' : '#fbbf24') : '#d1d5db',
|
||||
scale: isDragOver ? 1.02 : 1,
|
||||
config: config.gentle,
|
||||
})
|
||||
|
||||
const emptyStateStyle = useSpring({
|
||||
opacity: isEmpty ? (isDragOver ? 1 : 0.6) : 0,
|
||||
transform: isEmpty ? (isDragOver ? 'scale(1.1)' : 'scale(1)') : 'scale(0.8)',
|
||||
config: config.wobbly,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={css({ position: 'relative' })}>
|
||||
<h3 className={css({
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'bold',
|
||||
color: 'gray.800',
|
||||
mb: '4',
|
||||
textAlign: 'center'
|
||||
})}>
|
||||
{title}
|
||||
</h3>
|
||||
|
||||
<animated.div
|
||||
style={zoneStyle}
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '4',
|
||||
justifyContent: 'center',
|
||||
p: '6',
|
||||
rounded: id === 'arena' ? '3xl' : '2xl',
|
||||
border: '3px dashed',
|
||||
minH: id === 'arena' ? '64' : '32',
|
||||
position: 'relative',
|
||||
transition: 'min-height 0.3s ease',
|
||||
})}
|
||||
>
|
||||
{isEmpty && (
|
||||
<animated.div
|
||||
style={emptyStateStyle}
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
textAlign: 'center',
|
||||
pointerEvents: 'none',
|
||||
})}
|
||||
>
|
||||
<div className={css({
|
||||
fontSize: '4xl',
|
||||
mb: '4',
|
||||
})}>
|
||||
{isDragOver ? '✨' : (id === 'arena' ? '🏟️' : '🎯')}
|
||||
</div>
|
||||
<p className={css({
|
||||
color: 'gray.700',
|
||||
fontWeight: 'semibold',
|
||||
fontSize: 'lg'
|
||||
})}>
|
||||
{isDragOver ? `Drop to ${id === 'arena' ? 'enter the arena' : 'return to roster'}!` : subtitle}
|
||||
</p>
|
||||
</animated.div>
|
||||
)}
|
||||
{children}
|
||||
</animated.div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function EnhancedChampionArena({ onGameModeChange, onConfigurePlayer, className }: EnhancedChampionArenaProps) {
|
||||
const { profile } = useUserProfile()
|
||||
const { gameMode, players, setGameMode, updatePlayer } = useGameMode()
|
||||
const [activeId, setActiveId] = useState<number | null>(null)
|
||||
const [overId, setOverId] = useState<string | null>(null)
|
||||
|
||||
// Transform players into draggable format
|
||||
const availablePlayers = useMemo(() =>
|
||||
players
|
||||
.filter(player => !player.isActive)
|
||||
.map(player => ({
|
||||
id: player.id,
|
||||
name: player.id === 1 ? profile.player1Name : player.id === 2 ? profile.player2Name : player.name,
|
||||
emoji: player.id === 1 ? profile.player1Emoji : player.id === 2 ? profile.player2Emoji : player.emoji,
|
||||
color: player.color,
|
||||
isActive: false,
|
||||
level: Math.floor((profile.gamesPlayed || 0) / 5) + 1,
|
||||
})),
|
||||
[players, profile]
|
||||
)
|
||||
|
||||
const arenaPlayers = useMemo(() =>
|
||||
players
|
||||
.filter(player => player.isActive)
|
||||
.map(player => ({
|
||||
id: player.id,
|
||||
name: player.id === 1 ? profile.player1Name : player.id === 2 ? profile.player2Name : player.name,
|
||||
emoji: player.id === 1 ? profile.player1Emoji : player.id === 2 ? profile.player2Emoji : player.emoji,
|
||||
color: player.color,
|
||||
isActive: true,
|
||||
level: Math.floor((profile.gamesPlayed || 0) / 5) + 1,
|
||||
})),
|
||||
[players, profile]
|
||||
)
|
||||
|
||||
// Enhanced sensors for better touch and mouse support
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 8,
|
||||
},
|
||||
}),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
})
|
||||
)
|
||||
|
||||
// Custom collision detection for better drop zone detection
|
||||
const customCollisionDetection: CollisionDetection = (args) => {
|
||||
const pointerIntersections = pointerWithin(args)
|
||||
const intersections = pointerIntersections.length > 0
|
||||
? pointerIntersections
|
||||
: rectIntersection(args)
|
||||
|
||||
return getFirstCollision(intersections, 'id')
|
||||
}
|
||||
|
||||
const handleDragStart = (event: DragStartEvent) => {
|
||||
setActiveId(event.active.id as number)
|
||||
}
|
||||
|
||||
const handleDragOver = (event: DragOverEvent) => {
|
||||
setOverId(event.over?.id as string)
|
||||
}
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
setActiveId(null)
|
||||
setOverId(null)
|
||||
|
||||
if (!over) return
|
||||
|
||||
const playerId = active.id as number
|
||||
const targetZone = over.id as string
|
||||
|
||||
// Handle moving between zones
|
||||
if (targetZone === 'arena' || targetZone === 'roster') {
|
||||
const shouldActivate = targetZone === 'arena'
|
||||
updatePlayer(playerId, { isActive: shouldActivate })
|
||||
|
||||
// Update game mode based on arena players count
|
||||
const newArenaCount = shouldActivate
|
||||
? arenaPlayers.length + 1
|
||||
: arenaPlayers.length - (arenaPlayers.find(p => p.id === playerId) ? 1 : 0)
|
||||
|
||||
let newMode: 'single' | 'battle' | 'tournament' = 'single'
|
||||
if (newArenaCount === 1) newMode = 'single'
|
||||
else if (newArenaCount === 2) newMode = 'battle'
|
||||
else if (newArenaCount >= 3) newMode = 'tournament'
|
||||
|
||||
setGameMode(newMode)
|
||||
onGameModeChange?.(newMode)
|
||||
}
|
||||
}
|
||||
|
||||
// Find the active player for the drag overlay
|
||||
const activePlayer = activeId
|
||||
? [...availablePlayers, ...arenaPlayers].find(p => p.id === activeId)
|
||||
: null
|
||||
|
||||
// Animated transitions for players
|
||||
const rosterTransitions = useTransition(availablePlayers, {
|
||||
from: { opacity: 0, transform: 'scale(0.8) rotate(180deg)' },
|
||||
enter: { opacity: 1, transform: 'scale(1) rotate(0deg)' },
|
||||
leave: { opacity: 0, transform: 'scale(0.8) rotate(-180deg)' },
|
||||
config: config.wobbly,
|
||||
trail: 100,
|
||||
})
|
||||
|
||||
const arenaTransitions = useTransition(arenaPlayers, {
|
||||
from: { opacity: 0, transform: 'scale(0.8) translateY(50px)' },
|
||||
enter: { opacity: 1, transform: 'scale(1) translateY(0px)' },
|
||||
leave: { opacity: 0, transform: 'scale(0.8) translateY(-50px)' },
|
||||
config: config.wobbly,
|
||||
trail: 150,
|
||||
})
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={customCollisionDetection}
|
||||
onDragStart={handleDragStart}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<div className={css({
|
||||
background: 'white',
|
||||
rounded: '3xl',
|
||||
p: '8',
|
||||
border: '2px solid',
|
||||
borderColor: 'gray.200',
|
||||
boxShadow: '0 20px 40px rgba(0, 0, 0, 0.1)',
|
||||
transition: 'all 0.3s ease'
|
||||
}) + (className ? ` ${className}` : '')}>
|
||||
|
||||
{/* Header */}
|
||||
<div className={css({
|
||||
textAlign: 'center',
|
||||
mb: '8'
|
||||
})}>
|
||||
<h2 className={css({
|
||||
fontSize: { base: '2xl', md: '3xl' },
|
||||
fontWeight: 'bold',
|
||||
color: 'gray.900',
|
||||
mb: '2'
|
||||
})}>
|
||||
🏟️ Champion Arena
|
||||
</h2>
|
||||
<p className={css({
|
||||
color: 'gray.600',
|
||||
fontSize: 'lg',
|
||||
mb: '4'
|
||||
})}>
|
||||
Drag champions to experience the most tactile arena ever built!
|
||||
</p>
|
||||
|
||||
{/* Mode Indicator with Spring Animation */}
|
||||
<animated.div
|
||||
className={css({
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '2',
|
||||
background: arenaPlayers.length === 0
|
||||
? 'linear-gradient(135deg, #f3f4f6, #e5e7eb)'
|
||||
: gameMode === 'single'
|
||||
? 'linear-gradient(135deg, #dbeafe, #bfdbfe)'
|
||||
: gameMode === 'battle'
|
||||
? 'linear-gradient(135deg, #e9d5ff, #ddd6fe)'
|
||||
: 'linear-gradient(135deg, #fef3c7, #fde68a)',
|
||||
px: '4',
|
||||
py: '2',
|
||||
rounded: 'full',
|
||||
border: '2px solid',
|
||||
borderColor: arenaPlayers.length === 0
|
||||
? 'gray.300'
|
||||
: gameMode === 'single'
|
||||
? 'blue.300'
|
||||
: gameMode === 'battle'
|
||||
? 'purple.300'
|
||||
: 'yellow.300'
|
||||
})}
|
||||
>
|
||||
<span className={css({ fontSize: 'lg' })}>
|
||||
{arenaPlayers.length === 0 ? '🎯' : gameMode === 'single' ? '👤' : gameMode === 'battle' ? '⚔️' : '🏆'}
|
||||
</span>
|
||||
<span className={css({
|
||||
fontWeight: 'bold',
|
||||
color: arenaPlayers.length === 0 ? 'gray.700' : gameMode === 'single' ? 'blue.800' : gameMode === 'battle' ? 'purple.800' : 'yellow.800',
|
||||
textTransform: 'uppercase',
|
||||
fontSize: 'sm'
|
||||
})}>
|
||||
{arenaPlayers.length === 0 ? 'Select Champions' : gameMode === 'single' ? 'Solo Mode' : gameMode === 'battle' ? 'Battle Mode' : 'Tournament Mode'}
|
||||
</span>
|
||||
</animated.div>
|
||||
</div>
|
||||
|
||||
<div className={css({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: { base: '1fr', lg: '1fr 1fr' },
|
||||
gap: '8',
|
||||
alignItems: 'start'
|
||||
})}>
|
||||
|
||||
{/* Available Champions Roster */}
|
||||
<div className={css({ order: { base: 2, lg: 1 } })}>
|
||||
<SortableContext items={availablePlayers.map(p => p.id)} strategy={rectSortingStrategy}>
|
||||
<DroppableZone
|
||||
id="roster"
|
||||
title="🎯 Available Champions"
|
||||
subtitle="Drag champions here to remove from arena"
|
||||
isDragOver={overId === 'roster'}
|
||||
isEmpty={availablePlayers.length === 0}
|
||||
>
|
||||
{rosterTransitions((style, player) => (
|
||||
<animated.div key={player.id} style={style}>
|
||||
<ChampionCard
|
||||
player={player}
|
||||
zone="roster"
|
||||
onConfigure={onConfigurePlayer}
|
||||
/>
|
||||
</animated.div>
|
||||
))}
|
||||
</DroppableZone>
|
||||
</SortableContext>
|
||||
</div>
|
||||
|
||||
{/* Arena Drop Zone */}
|
||||
<div className={css({ order: { base: 1, lg: 2 } })}>
|
||||
<SortableContext items={arenaPlayers.map(p => p.id)} strategy={rectSortingStrategy}>
|
||||
<DroppableZone
|
||||
id="arena"
|
||||
title="🏟️ Battle Arena"
|
||||
subtitle="1 champion = Solo • 2 = Battle • 3+ = Tournament"
|
||||
isDragOver={overId === 'arena'}
|
||||
isEmpty={arenaPlayers.length === 0}
|
||||
>
|
||||
{arenaTransitions((style, player) => (
|
||||
<animated.div key={player.id} style={style}>
|
||||
<ChampionCard
|
||||
player={player}
|
||||
zone="arena"
|
||||
/>
|
||||
</animated.div>
|
||||
))}
|
||||
</DroppableZone>
|
||||
</SortableContext>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Game Selector */}
|
||||
<GameSelector
|
||||
variant="detailed"
|
||||
className={css({
|
||||
mt: '8',
|
||||
pt: '8',
|
||||
borderTop: '2px solid',
|
||||
borderColor: 'gray.200'
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Drag Overlay */}
|
||||
<DragOverlay>
|
||||
{activePlayer ? (
|
||||
<div className={css({
|
||||
transform: 'rotate(5deg) scale(1.1)',
|
||||
filter: 'drop-shadow(0 10px 20px rgba(0, 0, 0, 0.3))',
|
||||
})}>
|
||||
<ChampionCard player={activePlayer} isOverlay zone="roster" />
|
||||
</div>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
)
|
||||
}
|
||||
@@ -35,107 +35,7 @@ function FullscreenGameContent({ children, title }: FullscreenGameLayoutProps) {
|
||||
? 'linear-gradient(135deg, #0f0f23 0%, #1a1a3a 50%, #2d1b69 100%)'
|
||||
: 'white'
|
||||
})}>
|
||||
{/* Fullscreen mini nav */}
|
||||
{isFullscreen && (
|
||||
<div className={css({
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 100,
|
||||
background: 'rgba(0, 0, 0, 0.8)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
py: '2',
|
||||
px: '4'
|
||||
})}>
|
||||
<div className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
maxW: '6xl',
|
||||
mx: 'auto'
|
||||
})}>
|
||||
<div className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '3'
|
||||
})}>
|
||||
<h1 className={css({
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'bold',
|
||||
background: 'linear-gradient(135deg, #60a5fa, #a78bfa)',
|
||||
backgroundClip: 'text',
|
||||
color: 'transparent'
|
||||
})}>
|
||||
🕹️ {title}
|
||||
</h1>
|
||||
|
||||
<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 MODE
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={css({
|
||||
display: 'flex',
|
||||
gap: '2'
|
||||
})}>
|
||||
<button
|
||||
onClick={() => window.location.href = '/arcade'}
|
||||
className={css({
|
||||
px: '3',
|
||||
py: '1',
|
||||
background: 'rgba(59, 130, 246, 0.2)',
|
||||
border: '1px solid rgba(59, 130, 246, 0.3)',
|
||||
rounded: 'lg',
|
||||
color: 'blue.300',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'semibold',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease',
|
||||
_hover: {
|
||||
background: 'rgba(59, 130, 246, 0.3)',
|
||||
transform: 'scale(1.05)'
|
||||
}
|
||||
})}
|
||||
>
|
||||
🏟️ Back to Arena
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleExitGame}
|
||||
className={css({
|
||||
px: '3',
|
||||
py: '1',
|
||||
background: 'rgba(239, 68, 68, 0.2)',
|
||||
border: '1px solid rgba(239, 68, 68, 0.3)',
|
||||
rounded: 'lg',
|
||||
color: 'red.300',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'semibold',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease',
|
||||
_hover: {
|
||||
background: 'rgba(239, 68, 68, 0.3)',
|
||||
transform: 'scale(1.05)'
|
||||
}
|
||||
})}
|
||||
>
|
||||
✕ Exit Fullscreen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Note: Fullscreen navigation is now handled by the enhanced AppNavBar */}
|
||||
|
||||
{/* Game content */}
|
||||
<div className={css({
|
||||
|
||||
Reference in New Issue
Block a user