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:
Thomas Hallock
2025-09-27 16:52:21 -05:00
parent ce6c2a1116
commit afec22ac3f
8 changed files with 841 additions and 241 deletions

View File

@@ -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",

View File

@@ -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

View File

@@ -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')

View File

@@ -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>

View File

@@ -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
})}

View File

@@ -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)'
}
})}

View 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>
)
}

View File

@@ -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({