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
/** Region counts per size category */
regionCountsBySize: Record<string, number>
/** When true, fills parent container and uses overlay positioning for UI elements */
fillContainer?: boolean
}
interface BreadcrumbItem {
@ -126,6 +128,7 @@ export function DrillDownMapSelector({
includeSizes,
onRegionSizesChange,
regionCountsBySize,
fillContainer = false,
}: DrillDownMapSelectorProps) {
const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark'
@ -749,68 +752,282 @@ export function DrillDownMapSelector({
}, [currentLevel, path, onSelectionChange])
return (
<div data-component="drill-down-map-selector" className={css({ width: '100%' })}>
{/* Breadcrumb Navigation */}
<div
data-element="breadcrumbs"
className={css({
display: 'flex',
alignItems: 'center',
gap: '2',
marginBottom: '3',
fontSize: 'sm',
flexWrap: 'wrap',
})}
>
{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)}
<div
data-component="drill-down-map-selector"
className={css({
width: '100%',
...(fillContainer && { height: '100%', position: 'relative' }),
})}
>
{/* Breadcrumb Navigation - different styles for fillContainer vs normal */}
{fillContainer ? (
/* Navigation breadcrumb - positioned well below nav bar */
<div
data-element="navigation-overlay"
className={css({
position: 'absolute',
top: 'calc(var(--app-nav-height, 92px) + 72px)',
left: { base: '16px', sm: '24px' },
zIndex: 50,
display: 'flex',
alignItems: 'center',
gap: '3',
padding: '10px 16px',
bg: isDark
? 'linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(15, 23, 42, 0.95) 100%)'
: 'linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(248, 250, 252, 0.95) 100%)',
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({
display: 'flex',
alignItems: 'center',
gap: '1',
color: isDark ? 'blue.400' : 'blue.600',
cursor: 'pointer',
_hover: { textDecoration: 'underline' },
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>{crumb.emoji}</span>
<span>{crumb.label}</span>
</button>
) : (
<span className={css({ fontSize: 'md' })}>
{breadcrumbs[breadcrumbs.length - 1]?.emoji}
</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
className={css({
display: 'flex',
alignItems: 'center',
gap: '1',
fontWeight: 'bold',
color: isDark ? 'gray.100' : 'gray.800',
fontSize: '14px',
fontWeight: '600',
color: isDark ? 'white' : 'gray.800',
})}
>
<span>{crumb.emoji}</span>
<span>{crumb.label}</span>
World
</span>
)}
</span>
))}
</div>
</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 */}
<div
ref={containerRef}
data-element="map-container"
className={css({ position: 'relative' })}
className={css({
position: 'relative',
...(fillContainer && { width: '100%', height: '100%' }),
})}
>
<MapSelectorMap
fillContainer={fillContainer}
mapData={mapData}
viewBox={viewBox}
onRegionClick={handleRegionClick}
@ -830,8 +1047,9 @@ export function DrillDownMapSelector({
focusedRegion={focusedRegionId}
/>
{/* Zoom Out Button - positioned inside map, upper right */}
{currentLevel > 0 &&
{/* Zoom Out Button - only shown when NOT in fillContainer mode (fillContainer has navigation overlay) */}
{!fillContainer &&
currentLevel > 0 &&
(() => {
// Calculate what we're going back to
const backToWorld = currentLevel === 1
@ -935,15 +1153,19 @@ export function DrillDownMapSelector({
data-element="region-size-filters"
className={css({
position: 'absolute',
top: '3',
right: '3',
padding: '2',
bg: isDark ? 'gray.800/90' : 'white/90',
backdropFilter: 'blur(4px)',
rounded: 'lg',
top: fillContainer ? 'calc(var(--app-nav-height, 92px) + 72px)' : '3',
right: { base: '16px', sm: '24px' },
padding: '12px',
bg: isDark
? 'linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(15, 23, 42, 0.95) 100%)'
: 'linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(248, 250, 252, 0.95) 100%)',
backdropFilter: 'blur(12px)',
border: '1px solid',
borderColor: isDark ? 'gray.700' : 'gray.300',
boxShadow: 'md',
borderColor: isDark ? 'rgba(71, 85, 105, 0.5)' : 'rgba(203, 213, 225, 0.8)',
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,
})}
>
@ -965,7 +1187,8 @@ export function DrillDownMapSelector({
</div>
{/* 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
data-element="peer-navigation"
className={css({

View File

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

View File

@ -126,6 +126,11 @@ interface MapSelectorMapProps {
* Region ID to visually highlight (e.g., when hovering over region name in popover)
*/
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({
@ -144,6 +149,7 @@ export function MapSelectorMap({
previewRemoveRegions = [],
animatedViewBox,
focusedRegion,
fillContainer = false,
}: MapSelectorMapProps) {
const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark'
@ -339,10 +345,11 @@ export function MapSelectorMap({
data-component="map-selector-map"
className={css({
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',
rounded: 'xl',
border: '2px solid',
rounded: fillContainer ? 'none' : 'xl',
border: fillContainer ? 'none' : '2px solid',
borderColor: isDark ? 'gray.700' : 'gray.200',
overflow: 'hidden',
position: 'relative',

View File

@ -10,6 +10,76 @@ import { ALL_REGION_SIZES, ASSISTANCE_LEVELS, getFilteredMapDataBySizesSync } fr
import type { AssistanceLevelConfig } from '../maps'
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
function getFeatureBadges(level: AssistanceLevelConfig): 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
}
// Game mode options with rich descriptions
// Game mode options
const GAME_MODE_OPTIONS = [
{
value: 'cooperative' as const,
@ -89,6 +159,7 @@ export function SetupPhase() {
// Get selected options for display
const selectedMode = GAME_MODE_OPTIONS.find((opt) => opt.value === state.gameMode)
const selectedAssistance = ASSISTANCE_LEVELS.find((level) => level.id === state.assistanceLevel)
const selectedAssistanceBadges = selectedAssistance ? getFeatureBadges(selectedAssistance) : []
// Calculate total region count for start button
const totalRegionCount = useMemo(() => {
@ -104,39 +175,43 @@ export function SetupPhase() {
return state.selectedMap === 'usa' ? 'USA' : 'World'
}, [state.selectedContinent, state.selectedMap])
// Styles for Radix Select components
const triggerStyles = css({
// Get travel theme for current region
const regionTheme = useMemo(() => {
return REGION_THEMES[contextLabel] ?? DEFAULT_THEME
}, [contextLabel])
// Card trigger styles - responsive dimensions
const cardTriggerStyles = css({
display: 'flex',
alignItems: 'center',
gap: '3',
padding: '3',
bg: isDark ? 'gray.800' : 'white',
border: '2px solid',
borderColor: isDark ? 'gray.600' : 'gray.300',
gap: { base: '2', sm: '3' },
padding: { base: '2', sm: '3' },
bg: isDark ? 'gray.700/80' : 'white/80',
rounded: 'xl',
cursor: 'pointer',
width: '100%', // Fill grid cell
transition: 'all 0.15s',
width: { base: 'calc(50% - 4px)', sm: '260px' },
height: { base: '64px', sm: '88px' },
textAlign: 'left',
_hover: {
borderColor: isDark ? 'blue.500' : 'blue.400',
bg: isDark ? 'gray.750' : 'gray.50',
bg: isDark ? 'gray.600/90' : 'white',
},
_focus: {
outline: 'none',
borderColor: 'blue.500',
boxShadow: '0 0 0 3px rgba(59, 130, 246, 0.2)',
ring: '2px solid',
ringColor: 'blue.500',
},
})
const contentStyles = css({
bg: isDark ? 'gray.800' : 'white',
border: '2px solid',
border: '1px solid',
borderColor: isDark ? 'gray.600' : 'gray.200',
rounded: 'xl',
shadow: 'xl',
overflow: 'hidden',
zIndex: 1000,
minWidth: '220px',
minWidth: '280px',
})
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 (
<div
data-component="setup-phase"
className={css({
display: 'flex',
flexDirection: 'column',
gap: '4',
maxWidth: '800px',
margin: '0 auto',
paddingTop: '16',
paddingX: '4',
paddingBottom: '6',
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
overflow: 'hidden',
zIndex: 0,
})}
>
{/* Header */}
<div
data-element="header"
className={css({
textAlign: 'center',
marginBottom: '2',
})}
>
<h1
className={css({
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>
{/* Full-viewport Map - fills entire container */}
<DrillDownMapSelector
selectedMap={state.selectedMap}
selectedContinent={state.selectedContinent}
onSelectionChange={handleSelectionChange}
onStartGame={startGame}
includeSizes={state.includeSizes}
onRegionSizesChange={setRegionSizes}
regionCountsBySize={regionCountsBySize}
fillContainer
/>
{/* Drill-Down Map Selector */}
<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 */}
{/* Game Setup Sequence - Mode → Guidance → Start */}
<div
data-section="settings"
data-element="setup-sequence"
className={css({
display: 'grid',
gridTemplateColumns: '1fr', // Stack on mobile
gap: '4',
padding: '5',
bg: isDark ? 'gray.800/50' : 'gray.50',
position: 'absolute',
bottom: { base: '2', sm: '4' },
left: { base: '2', sm: '50%' },
right: { base: '2', sm: 'auto' },
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',
border: '1px solid',
borderColor: isDark ? 'gray.700' : 'gray.200',
md: {
gridTemplateColumns: 'repeat(2, 1fr)', // 2 columns on desktop
},
shadow: 'xl',
zIndex: 50,
maxWidth: { base: '100%', sm: 'fit-content' },
})}
>
{/* Game Mode */}
<div data-setting="game-mode" className={css({ display: 'flex', flexDirection: 'column' })}>
<label className={labelStyles}>Mode</label>
<Select.Root
value={state.gameMode}
onValueChange={(value) => setMode(value as 'cooperative' | 'race' | 'turn-based')}
>
<Select.Trigger className={triggerStyles}>
<span className={css({ fontSize: '2xl' })}>{selectedMode?.emoji}</span>
<div className={css({ flex: 1, textAlign: 'left' })}>
<div
{/* Game Mode Selector */}
<Select.Root
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 })}>
{selectedMode?.emoji}
</span>
<div className={css({ flex: 1, minWidth: 0 })}>
<div
className={css({
display: 'flex',
alignItems: 'center',
gap: { base: '1', sm: '2' },
})}
>
<span
className={css({
fontWeight: '600',
color: isDark ? 'gray.100' : 'gray.900',
fontSize: 'sm',
fontSize: { base: 'sm', sm: 'md' },
color: isDark ? 'gray.100' : 'gray.800',
})}
>
{selectedMode?.label}
</div>
<div
</span>
<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({
fontSize: 'xs',
color: isDark ? 'gray.400' : 'gray.500',
lineHeight: 'tight',
fontWeight: '600',
fontSize: { base: 'sm', sm: 'md' },
color: isDark ? 'gray.100' : 'gray.800',
})}
>
{selectedMode?.description}
</div>
{selectedAssistance?.label}
</span>
<Select.Icon
className={css({ color: isDark ? 'gray.400' : 'gray.500', fontSize: 'xs' })}
>
</Select.Icon>
</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>
{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({
fontSize: 'xs',
color: isDark ? 'gray.400' : 'gray.500',
marginTop: '0.5',
lineHeight: 'tight',
display: { base: 'none', sm: 'block' },
})}
>
{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 })}>
<Select.ItemText>
<span
className={css({
fontWeight: '600',
fontSize: 'md',
color: isDark ? 'gray.100' : 'gray.900',
fontSize: 'sm',
})}
>
{option.label}
{level.label}
</span>
</Select.ItemText>
<div
className={css({
fontSize: 'xs',
fontSize: 'sm',
color: isDark ? 'gray.400' : 'gray.500',
lineHeight: 'tight',
marginTop: '1',
})}
>
{option.description}
{level.description}
</div>
</div>
</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 */}
{badges.length > 0 && (
<div
className={css({
display: 'flex',
gap: '1',
marginTop: '1',
mt: '2',
flexWrap: 'wrap',
})}
>
@ -411,55 +471,148 @@ export function SetupPhase() {
<span
key={badge.label}
className={css({
fontSize: '2xs',
padding: '0.5 1',
bg: isDark ? 'gray.700' : 'gray.200',
fontSize: 'xs',
padding: '1 2',
bg: isDark ? 'gray.600' : 'gray.200',
color: isDark ? 'gray.300' : 'gray.600',
rounded: 'sm',
rounded: 'md',
})}
>
{badge.icon} {badge.label}
</span>
))}
</div>
</div>
</Select.Item>
)
})}
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
</div>
</div>
)}
</div>
</Select.Item>
)
})}
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
{/* Start Game Button */}
<button
data-action="start-game"
onClick={startGame}
className={css({
width: '100%',
padding: '4',
fontSize: 'xl',
fontWeight: 'bold',
bg: 'blue.600',
color: 'white',
rounded: '2xl',
cursor: 'pointer',
boxShadow: 'lg',
transition: 'all 0.2s',
_hover: {
bg: 'blue.700',
transform: 'scale(1.02)',
},
_active: {
transform: 'scale(0.98)',
},
})}
>
Start Game ({contextLabel} - {totalRegionCount}{' '}
{totalRegionCount === 1 ? 'region' : 'regions'})
</button>
{/* Start Button - Travel-themed, region-specific */}
<button
data-action="start-game"
onClick={startGame}
className={css({
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: { base: '2', sm: '3' },
padding: { base: '2 4', sm: '3 5' },
width: { base: '100%', sm: 'auto' },
minWidth: { base: 'auto', sm: '220px' },
flex: { base: 'none', sm: 1 },
height: { base: '56px', sm: '88px' },
fontSize: { base: 'md', sm: 'lg' },
fontWeight: 'bold',
color: 'white',
rounded: 'xl',
cursor: 'pointer',
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)',
_hover: {
transform: 'scale(1.02)',
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>
)
}