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:
parent
57dd61b994
commit
02762fad81
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue