feat(know-your-world): responsive setup + travel-themed start button

- Add responsive layout for setup sequence (mobile: two rows, desktop: one row)
- Create travel-themed start button with region-specific gradients, flags, and icons
- Position breadcrumbs and region filter below nav using CSS variable
- Improve breadcrumb styling with pill design, mini-map preview, and proper effects
- Add themed content for all regions (World, USA, continents)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-11-27 14:19:23 -06:00
parent 57dd61b994
commit 02762fad81
4 changed files with 716 additions and 332 deletions

View File

@ -109,6 +109,8 @@ interface DrillDownMapSelectorProps {
onRegionSizesChange: (sizes: RegionSize[]) => void onRegionSizesChange: (sizes: RegionSize[]) => void
/** Region counts per size category */ /** Region counts per size category */
regionCountsBySize: Record<string, number> regionCountsBySize: Record<string, number>
/** When true, fills parent container and uses overlay positioning for UI elements */
fillContainer?: boolean
} }
interface BreadcrumbItem { interface BreadcrumbItem {
@ -126,6 +128,7 @@ export function DrillDownMapSelector({
includeSizes, includeSizes,
onRegionSizesChange, onRegionSizesChange,
regionCountsBySize, regionCountsBySize,
fillContainer = false,
}: DrillDownMapSelectorProps) { }: DrillDownMapSelectorProps) {
const { resolvedTheme } = useTheme() const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark' const isDark = resolvedTheme === 'dark'
@ -749,68 +752,282 @@ export function DrillDownMapSelector({
}, [currentLevel, path, onSelectionChange]) }, [currentLevel, path, onSelectionChange])
return ( return (
<div data-component="drill-down-map-selector" className={css({ width: '100%' })}> <div
{/* Breadcrumb Navigation */} data-component="drill-down-map-selector"
<div className={css({
data-element="breadcrumbs" width: '100%',
className={css({ ...(fillContainer && { height: '100%', position: 'relative' }),
display: 'flex', })}
alignItems: 'center', >
gap: '2', {/* Breadcrumb Navigation - different styles for fillContainer vs normal */}
marginBottom: '3', {fillContainer ? (
fontSize: 'sm', /* Navigation breadcrumb - positioned well below nav bar */
flexWrap: 'wrap', <div
})} data-element="navigation-overlay"
> className={css({
{breadcrumbs.map((crumb, index) => ( position: 'absolute',
<span top: 'calc(var(--app-nav-height, 92px) + 72px)',
key={crumb.label} left: { base: '16px', sm: '24px' },
className={css({ display: 'flex', alignItems: 'center', gap: '1' })} zIndex: 50,
> display: 'flex',
{index > 0 && ( alignItems: 'center',
<span className={css({ color: isDark ? 'gray.500' : 'gray.400' })}></span> gap: '3',
)} padding: '10px 16px',
{crumb.isClickable ? ( bg: isDark
<button ? 'linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(15, 23, 42, 0.95) 100%)'
data-action={`nav-${crumb.label.toLowerCase().replace(/\s/g, '-')}`} : 'linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(248, 250, 252, 0.95) 100%)',
onClick={() => handleBreadcrumbClick(crumb.path)} backdropFilter: 'blur(12px)',
border: '1px solid',
borderColor: isDark ? 'rgba(71, 85, 105, 0.5)' : 'rgba(203, 213, 225, 0.8)',
rounded: 'full',
shadow: isDark
? '0 4px 20px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.05)'
: '0 4px 20px rgba(0, 0, 0, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.8)',
})}
>
{currentLevel > 0 ? (
<>
{/* Back button with mini-map */}
{(() => {
const backToWorld = currentLevel === 1
const backToContinentId = currentLevel === 2 ? path[0] : null
const backLabel = backToWorld
? 'World'
: (CONTINENTS.find((c) => c.id === backToContinentId)?.name ?? 'Continent')
const backEmoji = backToWorld
? '🌍'
: (CONTINENTS.find((c) => c.id === backToContinentId)?.emoji ?? '🗺️')
// Get viewBox and regions for the mini-map preview
const backViewBox = backToWorld
? WORLD_MAP.viewBox
: backToContinentId
? calculateContinentViewBox(
WORLD_MAP.regions,
backToContinentId,
WORLD_MAP.viewBox,
'world'
)
: WORLD_MAP.viewBox
const backRegions = backToWorld
? WORLD_MAP.regions
: backToContinentId
? filterRegionsByContinent(WORLD_MAP.regions, backToContinentId)
: []
return (
<button
data-action="navigate-back"
onClick={handleZoomOut}
className={css({
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '6px 12px 6px 8px',
bg: isDark ? 'rgba(51, 65, 85, 0.6)' : 'rgba(241, 245, 249, 0.8)',
border: '1px solid',
borderColor: isDark ? 'rgba(71, 85, 105, 0.4)' : 'rgba(203, 213, 225, 0.6)',
rounded: 'full',
cursor: 'pointer',
transition: 'all 0.2s ease',
_hover: {
bg: isDark ? 'rgba(71, 85, 105, 0.7)' : 'rgba(226, 232, 240, 0.9)',
transform: 'translateX(-2px)',
},
})}
>
{/* Mini-map preview */}
<div
className={css({
width: '32px',
height: '32px',
rounded: 'full',
overflow: 'hidden',
border: '2px solid',
borderColor: isDark ? 'rgba(100, 116, 139, 0.5)' : 'rgba(203, 213, 225, 0.8)',
boxShadow: 'inset 0 1px 3px rgba(0,0,0,0.1)',
})}
>
<svg
viewBox={backViewBox}
className={css({
width: '100%',
height: '100%',
display: 'block',
})}
preserveAspectRatio="xMidYMid slice"
>
<rect
x="-10000"
y="-10000"
width="30000"
height="30000"
fill={isDark ? '#1e3a5f' : '#bae6fd'}
/>
{backRegions.map((region) => (
<path
key={region.id}
d={region.path}
fill={isDark ? '#64748b' : '#a8d4a8'}
stroke={isDark ? '#475569' : '#86b386'}
strokeWidth={0.3}
/>
))}
</svg>
</div>
<div className={css({ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' })}>
<span
className={css({
fontSize: '10px',
fontWeight: '500',
color: isDark ? 'gray.400' : 'gray.500',
textTransform: 'uppercase',
letterSpacing: '0.05em',
lineHeight: 1,
})}
>
Back to
</span>
<span
className={css({
fontSize: '13px',
fontWeight: '600',
color: isDark ? 'gray.200' : 'gray.700',
lineHeight: 1.2,
})}
>
{backEmoji} {backLabel}
</span>
</div>
</button>
)
})()}
{/* Current location badge */}
<div
className={css({ className={css({
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '1', gap: '8px',
color: isDark ? 'blue.400' : 'blue.600', padding: '6px 14px',
cursor: 'pointer', bg: isDark
_hover: { textDecoration: 'underline' }, ? 'linear-gradient(135deg, rgba(59, 130, 246, 0.2) 0%, rgba(139, 92, 246, 0.2) 100%)'
: 'linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%)',
border: '1px solid',
borderColor: isDark ? 'rgba(59, 130, 246, 0.3)' : 'rgba(59, 130, 246, 0.2)',
rounded: 'full',
})} })}
> >
<span>{crumb.emoji}</span> <span className={css({ fontSize: 'md' })}>
<span>{crumb.label}</span> {breadcrumbs[breadcrumbs.length - 1]?.emoji}
</button> </span>
) : ( <span
className={css({
fontSize: '14px',
fontWeight: '600',
color: isDark ? 'white' : 'gray.800',
})}
>
{breadcrumbs[breadcrumbs.length - 1]?.label}
</span>
</div>
</>
) : (
/* World level - current location badge */
<div
className={css({
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '6px 14px',
bg: isDark
? 'linear-gradient(135deg, rgba(59, 130, 246, 0.2) 0%, rgba(139, 92, 246, 0.2) 100%)'
: 'linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%)',
border: '1px solid',
borderColor: isDark ? 'rgba(59, 130, 246, 0.3)' : 'rgba(59, 130, 246, 0.2)',
rounded: 'full',
})}
>
<span className={css({ fontSize: 'md' })}>🌍</span>
<span <span
className={css({ className={css({
display: 'flex', fontSize: '14px',
alignItems: 'center', fontWeight: '600',
gap: '1', color: isDark ? 'white' : 'gray.800',
fontWeight: 'bold',
color: isDark ? 'gray.100' : 'gray.800',
})} })}
> >
<span>{crumb.emoji}</span> World
<span>{crumb.label}</span>
</span> </span>
)} </div>
</span> )}
))} </div>
</div> ) : (
/* Normal breadcrumb for non-fillContainer mode */
<div
data-element="breadcrumbs"
className={css({
display: 'flex',
alignItems: 'center',
gap: '2',
fontSize: 'sm',
flexWrap: 'wrap',
marginBottom: '3',
})}
>
{breadcrumbs.map((crumb, index) => (
<span
key={crumb.label}
className={css({ display: 'flex', alignItems: 'center', gap: '1' })}
>
{index > 0 && (
<span className={css({ color: isDark ? 'gray.500' : 'gray.400' })}></span>
)}
{crumb.isClickable ? (
<button
data-action={`nav-${crumb.label.toLowerCase().replace(/\s/g, '-')}`}
onClick={() => handleBreadcrumbClick(crumb.path)}
className={css({
display: 'flex',
alignItems: 'center',
gap: '1',
color: isDark ? 'blue.400' : 'blue.600',
cursor: 'pointer',
_hover: { textDecoration: 'underline' },
})}
>
<span>{crumb.emoji}</span>
<span>{crumb.label}</span>
</button>
) : (
<span
className={css({
display: 'flex',
alignItems: 'center',
gap: '1',
fontWeight: 'bold',
color: isDark ? 'gray.100' : 'gray.800',
})}
>
<span>{crumb.emoji}</span>
<span>{crumb.label}</span>
</span>
)}
</span>
))}
</div>
)}
{/* Interactive Map - wrapped in ref'd container for dimension measurement */} {/* Interactive Map - wrapped in ref'd container for dimension measurement */}
<div <div
ref={containerRef} ref={containerRef}
data-element="map-container" data-element="map-container"
className={css({ position: 'relative' })} className={css({
position: 'relative',
...(fillContainer && { width: '100%', height: '100%' }),
})}
> >
<MapSelectorMap <MapSelectorMap
fillContainer={fillContainer}
mapData={mapData} mapData={mapData}
viewBox={viewBox} viewBox={viewBox}
onRegionClick={handleRegionClick} onRegionClick={handleRegionClick}
@ -830,8 +1047,9 @@ export function DrillDownMapSelector({
focusedRegion={focusedRegionId} focusedRegion={focusedRegionId}
/> />
{/* Zoom Out Button - positioned inside map, upper right */} {/* Zoom Out Button - only shown when NOT in fillContainer mode (fillContainer has navigation overlay) */}
{currentLevel > 0 && {!fillContainer &&
currentLevel > 0 &&
(() => { (() => {
// Calculate what we're going back to // Calculate what we're going back to
const backToWorld = currentLevel === 1 const backToWorld = currentLevel === 1
@ -935,15 +1153,19 @@ export function DrillDownMapSelector({
data-element="region-size-filters" data-element="region-size-filters"
className={css({ className={css({
position: 'absolute', position: 'absolute',
top: '3', top: fillContainer ? 'calc(var(--app-nav-height, 92px) + 72px)' : '3',
right: '3', right: { base: '16px', sm: '24px' },
padding: '2', padding: '12px',
bg: isDark ? 'gray.800/90' : 'white/90', bg: isDark
backdropFilter: 'blur(4px)', ? 'linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(15, 23, 42, 0.95) 100%)'
rounded: 'lg', : 'linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(248, 250, 252, 0.95) 100%)',
backdropFilter: 'blur(12px)',
border: '1px solid', border: '1px solid',
borderColor: isDark ? 'gray.700' : 'gray.300', borderColor: isDark ? 'rgba(71, 85, 105, 0.5)' : 'rgba(203, 213, 225, 0.8)',
boxShadow: 'md', rounded: 'xl',
shadow: isDark
? '0 4px 20px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.05)'
: '0 4px 20px rgba(0, 0, 0, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.8)',
zIndex: 10, zIndex: 10,
})} })}
> >
@ -965,7 +1187,8 @@ export function DrillDownMapSelector({
</div> </div>
{/* Peer Navigation - Mini-map thumbnails below main map (or planets at world level) */} {/* Peer Navigation - Mini-map thumbnails below main map (or planets at world level) */}
{peers.length > 0 && ( {/* Hidden in fillContainer mode since there's no space below the map */}
{!fillContainer && peers.length > 0 && (
<div <div
data-element="peer-navigation" data-element="peer-navigation"
className={css({ className={css({

View File

@ -18,14 +18,9 @@ export function GameComponent() {
? state.currentPlayer ? state.currentPlayer
: undefined : undefined
// Use StandardGameLayout only for playing phase // Setup phase renders its own full-screen layout (map behind nav)
const content = ( // Playing phase uses StandardGameLayout (respects nav height)
<> // Results phase uses normal flow
{state.gamePhase === 'setup' && <SetupPhase />}
{state.gamePhase === 'playing' && <PlayingPhase />}
{state.gamePhase === 'results' && <ResultsPhase />}
</>
)
return ( return (
<PageWithNav <PageWithNav
@ -41,7 +36,13 @@ export function GameComponent() {
onSetup={state.gamePhase !== 'setup' ? returnToSetup : undefined} onSetup={state.gamePhase !== 'setup' ? returnToSetup : undefined}
onNewGame={state.gamePhase !== 'setup' && state.gamePhase !== 'results' ? endGame : undefined} onNewGame={state.gamePhase !== 'setup' && state.gamePhase !== 'results' ? endGame : undefined}
> >
{state.gamePhase === 'playing' ? <StandardGameLayout>{content}</StandardGameLayout> : content} {state.gamePhase === 'setup' && <SetupPhase />}
{state.gamePhase === 'playing' && (
<StandardGameLayout>
<PlayingPhase />
</StandardGameLayout>
)}
{state.gamePhase === 'results' && <ResultsPhase />}
</PageWithNav> </PageWithNav>
) )
} }

View File

@ -126,6 +126,11 @@ interface MapSelectorMapProps {
* Region ID to visually highlight (e.g., when hovering over region name in popover) * Region ID to visually highlight (e.g., when hovering over region name in popover)
*/ */
focusedRegion?: string | null focusedRegion?: string | null
/**
* When true, fills the parent container (100% width and height) instead of using fixed aspect ratio.
* Used for full-viewport setup screen layout.
*/
fillContainer?: boolean
} }
export function MapSelectorMap({ export function MapSelectorMap({
@ -144,6 +149,7 @@ export function MapSelectorMap({
previewRemoveRegions = [], previewRemoveRegions = [],
animatedViewBox, animatedViewBox,
focusedRegion, focusedRegion,
fillContainer = false,
}: MapSelectorMapProps) { }: MapSelectorMapProps) {
const { resolvedTheme } = useTheme() const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark' const isDark = resolvedTheme === 'dark'
@ -339,10 +345,11 @@ export function MapSelectorMap({
data-component="map-selector-map" data-component="map-selector-map"
className={css({ className={css({
width: '100%', width: '100%',
aspectRatio: '16 / 9', // Fixed aspect ratio - doesn't change when drilling down // When fillContainer is true, fill the parent; otherwise use fixed 16:9 aspect ratio
...(fillContainer ? { height: '100%' } : { aspectRatio: '16 / 9' }),
bg: isDark ? 'gray.900' : 'gray.50', bg: isDark ? 'gray.900' : 'gray.50',
rounded: 'xl', rounded: fillContainer ? 'none' : 'xl',
border: '2px solid', border: fillContainer ? 'none' : '2px solid',
borderColor: isDark ? 'gray.700' : 'gray.200', borderColor: isDark ? 'gray.700' : 'gray.200',
overflow: 'hidden', overflow: 'hidden',
position: 'relative', position: 'relative',

View File

@ -10,6 +10,76 @@ import { ALL_REGION_SIZES, ASSISTANCE_LEVELS, getFilteredMapDataBySizesSync } fr
import type { AssistanceLevelConfig } from '../maps' import type { AssistanceLevelConfig } from '../maps'
import { CONTINENTS, type ContinentId } from '../continents' import { CONTINENTS, type ContinentId } from '../continents'
// Travel-themed content for each region
interface RegionTheme {
gradient: string
gradientHover: string
icons: string[] // Decorative icons to show
actionText: string // "Let's explore!" / "Bon voyage!" etc.
flagEmojis: string[] // Representative flag emojis
}
const REGION_THEMES: Record<string, RegionTheme> = {
World: {
gradient: 'linear-gradient(135deg, #3b82f6 0%, #8b5cf6 50%, #06b6d4 100%)',
gradientHover: 'linear-gradient(135deg, #60a5fa 0%, #a78bfa 50%, #22d3ee 100%)',
icons: ['✈️', '🌍', '🌎', '🌏'],
actionText: 'Start World Tour!',
flagEmojis: ['🇫🇷', '🇯🇵', '🇧🇷', '🇦🇺', '🇿🇦'],
},
USA: {
gradient: 'linear-gradient(135deg, #dc2626 0%, #1d4ed8 100%)',
gradientHover: 'linear-gradient(135deg, #ef4444 0%, #3b82f6 100%)',
icons: ['🗽', '🦅', '🇺🇸'],
actionText: 'Start USA Tour!',
flagEmojis: ['🇺🇸'],
},
Africa: {
gradient: 'linear-gradient(135deg, #d97706 0%, #059669 100%)',
gradientHover: 'linear-gradient(135deg, #f59e0b 0%, #10b981 100%)',
icons: ['🦁', '🌍', '🐘'],
actionText: 'Start Safari!',
flagEmojis: ['🇿🇦', '🇰🇪', '🇪🇬', '🇳🇬', '🇲🇦'],
},
Asia: {
gradient: 'linear-gradient(135deg, #dc2626 0%, #f59e0b 100%)',
gradientHover: 'linear-gradient(135deg, #ef4444 0%, #fbbf24 100%)',
icons: ['🏯', '🌏', '🐉'],
actionText: 'Start Journey!',
flagEmojis: ['🇯🇵', '🇨🇳', '🇮🇳', '🇰🇷', '🇹🇭'],
},
Europe: {
gradient: 'linear-gradient(135deg, #1d4ed8 0%, #7c3aed 100%)',
gradientHover: 'linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%)',
icons: ['🏰', '🌍', '🗼'],
actionText: 'Start Voyage!',
flagEmojis: ['🇫🇷', '🇩🇪', '🇮🇹', '🇪🇸', '🇬🇧'],
},
'North America': {
gradient: 'linear-gradient(135deg, #059669 0%, #0891b2 100%)',
gradientHover: 'linear-gradient(135deg, #10b981 0%, #06b6d4 100%)',
icons: ['🗽', '🌎', '🍁'],
actionText: 'Start Exploring!',
flagEmojis: ['🇺🇸', '🇨🇦', '🇲🇽'],
},
Oceania: {
gradient: 'linear-gradient(135deg, #0891b2 0%, #059669 100%)',
gradientHover: 'linear-gradient(135deg, #06b6d4 0%, #10b981 100%)',
icons: ['🦘', '🌏', '🏝️'],
actionText: 'Start Adventure!',
flagEmojis: ['🇦🇺', '🇳🇿', '🇫🇯'],
},
'South America': {
gradient: 'linear-gradient(135deg, #059669 0%, #d97706 100%)',
gradientHover: 'linear-gradient(135deg, #10b981 0%, #f59e0b 100%)',
icons: ['🗿', '🌎', '🌴'],
actionText: 'Start Expedition!',
flagEmojis: ['🇧🇷', '🇦🇷', '🇨🇴', '🇵🇪', '🇨🇱'],
},
}
const DEFAULT_THEME: RegionTheme = REGION_THEMES.World
// Generate feature badges for an assistance level // Generate feature badges for an assistance level
function getFeatureBadges(level: AssistanceLevelConfig): Array<{ label: string; icon: string }> { function getFeatureBadges(level: AssistanceLevelConfig): Array<{ label: string; icon: string }> {
const badges: Array<{ label: string; icon: string }> = [] const badges: Array<{ label: string; icon: string }> = []
@ -31,7 +101,7 @@ function getFeatureBadges(level: AssistanceLevelConfig): Array<{ label: string;
return badges return badges
} }
// Game mode options with rich descriptions // Game mode options
const GAME_MODE_OPTIONS = [ const GAME_MODE_OPTIONS = [
{ {
value: 'cooperative' as const, value: 'cooperative' as const,
@ -89,6 +159,7 @@ export function SetupPhase() {
// Get selected options for display // Get selected options for display
const selectedMode = GAME_MODE_OPTIONS.find((opt) => opt.value === state.gameMode) const selectedMode = GAME_MODE_OPTIONS.find((opt) => opt.value === state.gameMode)
const selectedAssistance = ASSISTANCE_LEVELS.find((level) => level.id === state.assistanceLevel) const selectedAssistance = ASSISTANCE_LEVELS.find((level) => level.id === state.assistanceLevel)
const selectedAssistanceBadges = selectedAssistance ? getFeatureBadges(selectedAssistance) : []
// Calculate total region count for start button // Calculate total region count for start button
const totalRegionCount = useMemo(() => { const totalRegionCount = useMemo(() => {
@ -104,39 +175,43 @@ export function SetupPhase() {
return state.selectedMap === 'usa' ? 'USA' : 'World' return state.selectedMap === 'usa' ? 'USA' : 'World'
}, [state.selectedContinent, state.selectedMap]) }, [state.selectedContinent, state.selectedMap])
// Styles for Radix Select components // Get travel theme for current region
const triggerStyles = css({ const regionTheme = useMemo(() => {
return REGION_THEMES[contextLabel] ?? DEFAULT_THEME
}, [contextLabel])
// Card trigger styles - responsive dimensions
const cardTriggerStyles = css({
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '3', gap: { base: '2', sm: '3' },
padding: '3', padding: { base: '2', sm: '3' },
bg: isDark ? 'gray.800' : 'white', bg: isDark ? 'gray.700/80' : 'white/80',
border: '2px solid',
borderColor: isDark ? 'gray.600' : 'gray.300',
rounded: 'xl', rounded: 'xl',
cursor: 'pointer', cursor: 'pointer',
width: '100%', // Fill grid cell
transition: 'all 0.15s', transition: 'all 0.15s',
width: { base: 'calc(50% - 4px)', sm: '260px' },
height: { base: '64px', sm: '88px' },
textAlign: 'left',
_hover: { _hover: {
borderColor: isDark ? 'blue.500' : 'blue.400', bg: isDark ? 'gray.600/90' : 'white',
bg: isDark ? 'gray.750' : 'gray.50',
}, },
_focus: { _focus: {
outline: 'none', outline: 'none',
borderColor: 'blue.500', ring: '2px solid',
boxShadow: '0 0 0 3px rgba(59, 130, 246, 0.2)', ringColor: 'blue.500',
}, },
}) })
const contentStyles = css({ const contentStyles = css({
bg: isDark ? 'gray.800' : 'white', bg: isDark ? 'gray.800' : 'white',
border: '2px solid', border: '1px solid',
borderColor: isDark ? 'gray.600' : 'gray.200', borderColor: isDark ? 'gray.600' : 'gray.200',
rounded: 'xl', rounded: 'xl',
shadow: 'xl', shadow: 'xl',
overflow: 'hidden', overflow: 'hidden',
zIndex: 1000, zIndex: 1000,
minWidth: '220px', minWidth: '280px',
}) })
const itemStyles = css({ const itemStyles = css({
@ -155,255 +230,240 @@ export function SetupPhase() {
}, },
}) })
const labelStyles = css({
fontSize: 'xs',
fontWeight: '600',
color: isDark ? 'gray.400' : 'gray.500',
marginBottom: '2',
textTransform: 'uppercase',
letterSpacing: 'wide',
})
return ( return (
<div <div
data-component="setup-phase" data-component="setup-phase"
className={css({ className={css({
display: 'flex', position: 'fixed',
flexDirection: 'column', top: 0,
gap: '4', left: 0,
maxWidth: '800px', right: 0,
margin: '0 auto', bottom: 0,
paddingTop: '16', overflow: 'hidden',
paddingX: '4', zIndex: 0,
paddingBottom: '6',
})} })}
> >
{/* Header */} {/* Full-viewport Map - fills entire container */}
<div <DrillDownMapSelector
data-element="header" selectedMap={state.selectedMap}
className={css({ selectedContinent={state.selectedContinent}
textAlign: 'center', onSelectionChange={handleSelectionChange}
marginBottom: '2', onStartGame={startGame}
})} includeSizes={state.includeSizes}
> onRegionSizesChange={setRegionSizes}
<h1 regionCountsBySize={regionCountsBySize}
className={css({ fillContainer
fontSize: '2xl', />
fontWeight: 'bold',
color: isDark ? 'gray.100' : 'gray.900',
})}
>
Know Your World 🌍
</h1>
<p
className={css({
fontSize: 'sm',
color: isDark ? 'gray.400' : 'gray.600',
marginTop: '1',
})}
>
Click continents to zoom in, or start playing from any level
</p>
</div>
{/* Drill-Down Map Selector */} {/* Game Setup Sequence - Mode → Guidance → Start */}
<div data-section="map-selection">
<DrillDownMapSelector
selectedMap={state.selectedMap}
selectedContinent={state.selectedContinent}
onSelectionChange={handleSelectionChange}
onStartGame={startGame}
includeSizes={state.includeSizes}
onRegionSizesChange={setRegionSizes}
regionCountsBySize={regionCountsBySize}
/>
</div>
{/* Settings Row with Radix Selects */}
<div <div
data-section="settings" data-element="setup-sequence"
className={css({ className={css({
display: 'grid', position: 'absolute',
gridTemplateColumns: '1fr', // Stack on mobile bottom: { base: '2', sm: '4' },
gap: '4', left: { base: '2', sm: '50%' },
padding: '5', right: { base: '2', sm: 'auto' },
bg: isDark ? 'gray.800/50' : 'gray.50', transform: { base: 'none', sm: 'translateX(-50%)' },
display: 'flex',
flexWrap: 'wrap',
alignItems: 'stretch',
gap: '2',
padding: '2',
bg: isDark ? 'gray.800/95' : 'gray.100/95',
backdropFilter: 'blur(12px)',
rounded: '2xl', rounded: '2xl',
border: '1px solid', shadow: 'xl',
borderColor: isDark ? 'gray.700' : 'gray.200', zIndex: 50,
md: { maxWidth: { base: '100%', sm: 'fit-content' },
gridTemplateColumns: 'repeat(2, 1fr)', // 2 columns on desktop
},
})} })}
> >
{/* Game Mode */} {/* Game Mode Selector */}
<div data-setting="game-mode" className={css({ display: 'flex', flexDirection: 'column' })}> <Select.Root
<label className={labelStyles}>Mode</label> value={state.gameMode}
<Select.Root onValueChange={(value) => setMode(value as 'cooperative' | 'race' | 'turn-based')}
value={state.gameMode} >
onValueChange={(value) => setMode(value as 'cooperative' | 'race' | 'turn-based')} <Select.Trigger className={cardTriggerStyles}>
> <span className={css({ fontSize: { base: 'xl', sm: '2xl' }, flexShrink: 0 })}>
<Select.Trigger className={triggerStyles}> {selectedMode?.emoji}
<span className={css({ fontSize: '2xl' })}>{selectedMode?.emoji}</span> </span>
<div className={css({ flex: 1, textAlign: 'left' })}> <div className={css({ flex: 1, minWidth: 0 })}>
<div <div
className={css({
display: 'flex',
alignItems: 'center',
gap: { base: '1', sm: '2' },
})}
>
<span
className={css({ className={css({
fontWeight: '600', fontWeight: '600',
color: isDark ? 'gray.100' : 'gray.900', fontSize: { base: 'sm', sm: 'md' },
fontSize: 'sm', color: isDark ? 'gray.100' : 'gray.800',
})} })}
> >
{selectedMode?.label} {selectedMode?.label}
</div> </span>
<div <Select.Icon
className={css({ color: isDark ? 'gray.400' : 'gray.500', fontSize: 'xs' })}
>
</Select.Icon>
</div>
<div
className={css({
fontSize: 'xs',
color: isDark ? 'gray.400' : 'gray.500',
marginTop: '0.5',
lineHeight: 'tight',
display: { base: 'none', sm: 'block' },
})}
>
{selectedMode?.description}
</div>
</div>
</Select.Trigger>
<Select.Portal>
<Select.Content className={contentStyles} position="popper" sideOffset={5}>
<Select.Viewport>
{GAME_MODE_OPTIONS.map((option) => (
<Select.Item key={option.value} value={option.value} className={itemStyles}>
<span className={css({ fontSize: '2xl' })}>{option.emoji}</span>
<div className={css({ flex: 1 })}>
<Select.ItemText>
<span
className={css({
fontWeight: '600',
fontSize: 'md',
color: isDark ? 'gray.100' : 'gray.900',
})}
>
{option.label}
</span>
</Select.ItemText>
<div
className={css({
fontSize: 'sm',
color: isDark ? 'gray.400' : 'gray.500',
marginTop: '1',
})}
>
{option.description}
</div>
</div>
</Select.Item>
))}
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
{/* Assistance Level Selector */}
<Select.Root
value={state.assistanceLevel}
onValueChange={(value) =>
setAssistanceLevel(value as 'guided' | 'helpful' | 'standard' | 'none')
}
>
<Select.Trigger className={cardTriggerStyles}>
<span className={css({ fontSize: { base: 'xl', sm: '2xl' }, flexShrink: 0 })}>
{selectedAssistance?.emoji || '💡'}
</span>
<div className={css({ flex: 1, minWidth: 0 })}>
<div
className={css({
display: 'flex',
alignItems: 'center',
gap: { base: '1', sm: '2' },
})}
>
<span
className={css({ className={css({
fontSize: 'xs', fontWeight: '600',
color: isDark ? 'gray.400' : 'gray.500', fontSize: { base: 'sm', sm: 'md' },
lineHeight: 'tight', color: isDark ? 'gray.100' : 'gray.800',
})} })}
> >
{selectedMode?.description} {selectedAssistance?.label}
</div> </span>
<Select.Icon
className={css({ color: isDark ? 'gray.400' : 'gray.500', fontSize: 'xs' })}
>
</Select.Icon>
</div> </div>
<Select.Icon className={css({ color: isDark ? 'gray.400' : 'gray.500' })}> <div
className={css({
</Select.Icon> fontSize: 'xs',
</Select.Trigger> color: isDark ? 'gray.400' : 'gray.500',
<Select.Portal> marginTop: '0.5',
<Select.Content className={contentStyles} position="popper" sideOffset={5}> lineHeight: 'tight',
<Select.Viewport> display: { base: 'none', sm: 'block' },
{GAME_MODE_OPTIONS.map((option) => ( })}
<Select.Item key={option.value} value={option.value} className={itemStyles}> >
<span className={css({ fontSize: '2xl' })}>{option.emoji}</span> {selectedAssistance?.description}
</div>
{selectedAssistanceBadges.length > 0 && (
<div
className={css({
display: { base: 'none', sm: 'flex' },
gap: '1',
mt: '1.5',
flexWrap: 'wrap',
})}
>
{selectedAssistanceBadges.map((badge) => (
<span
key={badge.label}
className={css({
fontSize: '2xs',
padding: '0.5 1.5',
bg: isDark ? 'gray.600' : 'gray.300',
color: isDark ? 'gray.300' : 'gray.700',
rounded: 'full',
})}
>
{badge.icon} {badge.label}
</span>
))}
</div>
)}
</div>
</Select.Trigger>
<Select.Portal>
<Select.Content className={contentStyles} position="popper" sideOffset={5}>
<Select.Viewport>
{ASSISTANCE_LEVELS.map((level) => {
const badges = getFeatureBadges(level)
return (
<Select.Item key={level.id} value={level.id} className={itemStyles}>
<span className={css({ fontSize: '2xl' })}>{level.emoji}</span>
<div className={css({ flex: 1 })}> <div className={css({ flex: 1 })}>
<Select.ItemText> <Select.ItemText>
<span <span
className={css({ className={css({
fontWeight: '600', fontWeight: '600',
fontSize: 'md',
color: isDark ? 'gray.100' : 'gray.900', color: isDark ? 'gray.100' : 'gray.900',
fontSize: 'sm',
})} })}
> >
{option.label} {level.label}
</span> </span>
</Select.ItemText> </Select.ItemText>
<div <div
className={css({ className={css({
fontSize: 'xs', fontSize: 'sm',
color: isDark ? 'gray.400' : 'gray.500', color: isDark ? 'gray.400' : 'gray.500',
lineHeight: 'tight', marginTop: '1',
})} })}
> >
{option.description} {level.description}
</div> </div>
</div> {badges.length > 0 && (
</Select.Item>
))}
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
</div>
{/* Assistance Level */}
<div
data-setting="assistance-level"
className={css({ display: 'flex', flexDirection: 'column' })}
>
<label className={labelStyles}>Assistance</label>
<Select.Root
value={state.assistanceLevel}
onValueChange={(value) =>
setAssistanceLevel(value as 'guided' | 'helpful' | 'standard' | 'none')
}
>
<Select.Trigger className={triggerStyles}>
<span className={css({ fontSize: '2xl' })}>{selectedAssistance?.emoji || '💡'}</span>
<div className={css({ flex: 1, textAlign: 'left' })}>
<div
className={css({
fontWeight: '600',
color: isDark ? 'gray.100' : 'gray.900',
fontSize: 'sm',
})}
>
{selectedAssistance?.label}
</div>
<div
className={css({
fontSize: 'xs',
color: isDark ? 'gray.400' : 'gray.500',
lineHeight: 'tight',
})}
>
{selectedAssistance?.description}
</div>
{/* Feature badges */}
{selectedAssistance && (
<div
className={css({
display: 'flex',
gap: '1',
marginTop: '1',
flexWrap: 'wrap',
})}
>
{getFeatureBadges(selectedAssistance).map((badge) => (
<span
key={badge.label}
className={css({
fontSize: '2xs',
padding: '0.5 1',
bg: isDark ? 'gray.700' : 'gray.200',
color: isDark ? 'gray.300' : 'gray.600',
rounded: 'sm',
})}
>
{badge.icon} {badge.label}
</span>
))}
</div>
)}
</div>
<Select.Icon className={css({ color: isDark ? 'gray.400' : 'gray.500' })}>
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Content className={contentStyles} position="popper" sideOffset={5}>
<Select.Viewport>
{ASSISTANCE_LEVELS.map((level) => {
const badges = getFeatureBadges(level)
return (
<Select.Item key={level.id} value={level.id} className={itemStyles}>
<span className={css({ fontSize: '2xl' })}>{level.emoji}</span>
<div className={css({ flex: 1 })}>
<Select.ItemText>
<span
className={css({
fontWeight: '600',
color: isDark ? 'gray.100' : 'gray.900',
fontSize: 'sm',
})}
>
{level.label}
</span>
</Select.ItemText>
<div
className={css({
fontSize: 'xs',
color: isDark ? 'gray.400' : 'gray.500',
lineHeight: 'tight',
})}
>
{level.description}
</div>
{/* Feature badges */}
<div <div
className={css({ className={css({
display: 'flex', display: 'flex',
gap: '1', gap: '1',
marginTop: '1', mt: '2',
flexWrap: 'wrap', flexWrap: 'wrap',
})} })}
> >
@ -411,55 +471,148 @@ export function SetupPhase() {
<span <span
key={badge.label} key={badge.label}
className={css({ className={css({
fontSize: '2xs', fontSize: 'xs',
padding: '0.5 1', padding: '1 2',
bg: isDark ? 'gray.700' : 'gray.200', bg: isDark ? 'gray.600' : 'gray.200',
color: isDark ? 'gray.300' : 'gray.600', color: isDark ? 'gray.300' : 'gray.600',
rounded: 'sm', rounded: 'md',
})} })}
> >
{badge.icon} {badge.label} {badge.icon} {badge.label}
</span> </span>
))} ))}
</div> </div>
</div> )}
</Select.Item> </div>
) </Select.Item>
})} )
</Select.Viewport> })}
</Select.Content> </Select.Viewport>
</Select.Portal> </Select.Content>
</Select.Root> </Select.Portal>
</div> </Select.Root>
</div>
{/* Start Game Button */} {/* Start Button - Travel-themed, region-specific */}
<button <button
data-action="start-game" data-action="start-game"
onClick={startGame} onClick={startGame}
className={css({ className={css({
width: '100%', position: 'relative',
padding: '4', display: 'flex',
fontSize: 'xl', alignItems: 'center',
fontWeight: 'bold', justifyContent: 'center',
bg: 'blue.600', gap: { base: '2', sm: '3' },
color: 'white', padding: { base: '2 4', sm: '3 5' },
rounded: '2xl', width: { base: '100%', sm: 'auto' },
cursor: 'pointer', minWidth: { base: 'auto', sm: '220px' },
boxShadow: 'lg', flex: { base: 'none', sm: 1 },
transition: 'all 0.2s', height: { base: '56px', sm: '88px' },
_hover: { fontSize: { base: 'md', sm: 'lg' },
bg: 'blue.700', fontWeight: 'bold',
transform: 'scale(1.02)', color: 'white',
}, rounded: 'xl',
_active: { cursor: 'pointer',
transform: 'scale(0.98)', transition: 'all 0.2s ease-out',
}, overflow: 'hidden',
})} border: '2px solid rgba(255,255,255,0.2)',
> boxShadow: '0 4px 15px rgba(0,0,0,0.2)',
Start Game ({contextLabel} - {totalRegionCount}{' '} _hover: {
{totalRegionCount === 1 ? 'region' : 'regions'}) transform: 'scale(1.02)',
</button> boxShadow: '0 6px 20px rgba(0,0,0,0.3)',
},
_active: {
transform: 'scale(0.98)',
},
})}
style={{
background: regionTheme.gradient,
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = regionTheme.gradientHover
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = regionTheme.gradient
}}
>
{/* Decorative flag strip at top */}
<div
className={css({
position: 'absolute',
top: '0',
left: '0',
right: '0',
height: { base: '16px', sm: '20px' },
display: 'flex',
justifyContent: 'center',
gap: '1',
fontSize: { base: '2xs', sm: 'xs' },
bg: 'rgba(0,0,0,0.15)',
paddingTop: '1px',
overflow: 'hidden',
})}
>
{regionTheme.flagEmojis.map((flag, i) => (
<span key={i} className={css({ opacity: 0.9 })}>
{flag}
</span>
))}
</div>
{/* Main content */}
<div
className={css({
display: 'flex',
alignItems: 'center',
gap: { base: '2', sm: '3' },
marginTop: { base: '6px', sm: '8px' },
})}
>
{/* Travel icons */}
<div
className={css({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
fontSize: { base: 'lg', sm: '2xl' },
lineHeight: 1,
})}
>
<span>{regionTheme.icons[0]}</span>
<span className={css({ fontSize: { base: 'xs', sm: 'sm' }, marginTop: '-2px' })}>
{regionTheme.icons[1]}
</span>
</div>
{/* Text content */}
<div
className={css({
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
})}
>
<span
className={css({
fontSize: { base: 'md', sm: 'lg' },
fontWeight: 'bold',
textShadow: '0 1px 2px rgba(0,0,0,0.2)',
})}
>
Start {contextLabel}
</span>
<span
className={css({
fontSize: { base: 'xs', sm: 'sm' },
fontWeight: 'normal',
opacity: 0.9,
})}
>
{totalRegionCount} regions
</span>
</div>
</div>
</button>
</div>
</div> </div>
) )
} }