feat(know-your-world): move region size filters inside map preview

Move the region type checkboxes from a standalone section into an overlay
inside the map preview. The filters now appear in the bottom-right corner
with a semi-transparent backdrop.

- Add region size filter overlay to DrillDownMapSelector
- Remove standalone region sizes section from SetupPhase
- Clean up unused imports (Checkbox, REGION_SIZE_CONFIG)

🤖 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-26 21:03:02 -06:00
parent 9499e4e8b5
commit 81301ab148
2 changed files with 145 additions and 175 deletions

View File

@ -1,6 +1,7 @@
'use client'
import { useState, useCallback, useMemo, useRef, useEffect } from 'react'
import * as Checkbox from '@radix-ui/react-checkbox'
import { css } from '@styled/css'
import { useTheme } from '@/contexts/ThemeContext'
import { MapSelectorMap } from './MapSelectorMap'
@ -15,6 +16,8 @@ import {
parseViewBox,
calculateFitCropViewBox,
getFilteredMapDataBySizesSync,
REGION_SIZE_CONFIG,
ALL_REGION_SIZES,
} from '../maps'
import type { RegionSize } from '../maps'
import {
@ -67,6 +70,10 @@ interface DrillDownMapSelectorProps {
selectedContinent: ContinentId | 'all'
/** Region sizes to include (for showing excluded regions dimmed) */
includeSizes: RegionSize[]
/** Callback when region sizes change */
onRegionSizesChange: (sizes: RegionSize[]) => void
/** Region counts per size category */
regionCountsBySize: Record<string, number>
}
interface BreadcrumbItem {
@ -82,6 +89,8 @@ export function DrillDownMapSelector({
selectedMap,
selectedContinent,
includeSizes,
onRegionSizesChange,
regionCountsBySize,
}: DrillDownMapSelectorProps) {
const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark'
@ -715,6 +724,138 @@ export function DrillDownMapSelector({
</button>
)
})()}
{/* Region Size Filters - positioned inside map, bottom right */}
<div
data-element="region-size-filters"
className={css({
position: 'absolute',
bottom: '3',
right: '3',
display: 'flex',
flexDirection: 'column',
gap: '1',
padding: '2',
bg: isDark ? 'gray.800/90' : 'white/90',
backdropFilter: 'blur(4px)',
rounded: 'lg',
border: '1px solid',
borderColor: isDark ? 'gray.700' : 'gray.300',
boxShadow: 'md',
zIndex: 10,
})}
>
<div
className={css({
display: 'flex',
flexWrap: 'wrap',
gap: '1',
maxWidth: '280px',
})}
>
{ALL_REGION_SIZES.map((size) => {
const config = REGION_SIZE_CONFIG[size]
const isChecked = includeSizes.includes(size)
const isOnlyOne = includeSizes.length === 1 && isChecked
const count = regionCountsBySize[size] || 0
const handleToggle = () => {
if (isOnlyOne) return
if (isChecked) {
onRegionSizesChange(includeSizes.filter((s) => s !== size))
} else {
onRegionSizesChange([...includeSizes, size])
}
}
return (
<Checkbox.Root
key={size}
checked={isChecked}
onCheckedChange={handleToggle}
disabled={isOnlyOne}
className={css({
display: 'inline-flex',
alignItems: 'center',
gap: '1',
paddingX: '2',
paddingY: '1',
bg: isChecked
? isDark
? 'blue.800'
: 'blue.500'
: isDark
? 'gray.700'
: 'gray.100',
border: '1px solid',
borderColor: isChecked
? isDark
? 'blue.600'
: 'blue.600'
: isDark
? 'gray.600'
: 'gray.300',
rounded: 'full',
cursor: isOnlyOne ? 'not-allowed' : 'pointer',
opacity: isOnlyOne ? 0.5 : 1,
transition: 'all 0.15s',
fontSize: 'xs',
_hover: isOnlyOne
? {}
: {
bg: isChecked
? isDark
? 'blue.700'
: 'blue.600'
: isDark
? 'gray.600'
: 'gray.200',
},
})}
>
<span>{config.emoji}</span>
<span
className={css({
fontWeight: '500',
color: isChecked
? 'white'
: isDark
? 'gray.200'
: 'gray.700',
})}
>
{config.label}
</span>
<span
className={css({
fontWeight: '600',
color: isChecked
? isDark
? 'blue.200'
: 'blue.100'
: isDark
? 'gray.400'
: 'gray.500',
bg: isChecked
? isDark
? 'blue.700'
: 'blue.600'
: isDark
? 'gray.600'
: 'gray.200',
paddingX: '1',
rounded: 'full',
minWidth: '4',
textAlign: 'center',
})}
>
{count}
</span>
</Checkbox.Root>
)
})}
</div>
</div>
</div>
{/* Peer Navigation - Mini-map thumbnails below main map (or planets at world level) */}

View File

@ -2,25 +2,14 @@
import { useCallback, useMemo } from 'react'
import * as Select from '@radix-ui/react-select'
import * as Checkbox from '@radix-ui/react-checkbox'
import { css } from '@styled/css'
import { useTheme } from '@/contexts/ThemeContext'
import { useKnowYourWorld } from '../Provider'
import { DrillDownMapSelector } from './DrillDownMapSelector'
import {
ALL_REGION_SIZES,
ASSISTANCE_LEVELS,
getFilteredMapDataBySizesSync,
REGION_SIZE_CONFIG,
} from '../maps'
import type { RegionSize, AssistanceLevelConfig } from '../maps'
import { ALL_REGION_SIZES, ASSISTANCE_LEVELS, getFilteredMapDataBySizesSync } from '../maps'
import type { AssistanceLevelConfig } from '../maps'
import type { ContinentId } from '../continents'
// Get term for regions based on map type
function getRegionTerm(selectedMap: 'world' | 'usa'): string {
return selectedMap === 'world' ? 'countries' : 'states'
}
// Generate feature badges for an assistance level
function getFeatureBadges(level: AssistanceLevelConfig): Array<{ label: string; icon: string }> {
const badges: Array<{ label: string; icon: string }> = []
@ -124,14 +113,6 @@ export function SetupPhase() {
return counts
}, [state.selectedMap, state.selectedContinent])
// Calculate the total region count for current selection
const totalRegionCount = useMemo(() => {
return state.includeSizes.reduce((sum, size) => sum + (regionCountsBySize[size] || 0), 0)
}, [state.includeSizes, regionCountsBySize])
// Get the term for regions (countries/states)
const regionTerm = getRegionTerm(state.selectedMap)
// Handle selection change from drill-down selector
const handleSelectionChange = useCallback(
(mapId: 'world' | 'usa', continentId: ContinentId | 'all') => {
@ -146,20 +127,6 @@ export function SetupPhase() {
const selectedStudyTime = STUDY_TIME_OPTIONS.find((opt) => opt.value === state.studyDuration)
const selectedAssistance = ASSISTANCE_LEVELS.find((level) => level.id === state.assistanceLevel)
// Handle toggling a region size
const toggleRegionSize = useCallback(
(size: RegionSize) => {
if (state.includeSizes.includes(size)) {
// Don't allow removing the last size
if (state.includeSizes.length === 1) return
setRegionSizes(state.includeSizes.filter((s) => s !== size))
} else {
setRegionSizes([...state.includeSizes, size])
}
},
[state.includeSizes, setRegionSizes]
)
// Styles for Radix Select components
const triggerStyles = css({
display: 'flex',
@ -270,6 +237,8 @@ export function SetupPhase() {
onSelectionChange={handleSelectionChange}
onStartGame={startGame}
includeSizes={state.includeSizes}
onRegionSizesChange={setRegionSizes}
regionCountsBySize={regionCountsBySize}
/>
</div>
@ -563,146 +532,6 @@ export function SetupPhase() {
</div>
</div>
{/* Region Types Selection */}
<div
data-section="region-sizes"
className={css({
padding: '5',
bg: isDark ? 'gray.800/50' : 'gray.50',
rounded: '2xl',
border: '1px solid',
borderColor: isDark ? 'gray.700' : 'gray.200',
})}
>
<div
className={css({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '4',
})}
>
<label className={labelStyles} style={{ marginBottom: 0 }}>
Region Types
</label>
<span
className={css({
fontSize: 'sm',
fontWeight: '600',
color: isDark ? 'blue.300' : 'blue.600',
})}
>
{totalRegionCount} {regionTerm} selected
</span>
</div>
<div
className={css({
display: 'flex',
flexWrap: 'wrap',
gap: '2',
})}
>
{ALL_REGION_SIZES.map((size) => {
const config = REGION_SIZE_CONFIG[size]
const isChecked = state.includeSizes.includes(size)
const isOnlyOne = state.includeSizes.length === 1 && isChecked
const count = regionCountsBySize[size] || 0
return (
<Checkbox.Root
key={size}
checked={isChecked}
onCheckedChange={() => toggleRegionSize(size)}
disabled={isOnlyOne}
className={css({
display: 'inline-flex',
alignItems: 'center',
gap: '2',
paddingX: '3',
paddingY: '2',
bg: isChecked
? isDark
? 'blue.800'
: 'blue.500'
: isDark
? 'gray.700'
: 'white',
border: '1px solid',
borderColor: isChecked
? isDark
? 'blue.600'
: 'blue.600'
: isDark
? 'gray.600'
: 'gray.300',
rounded: 'full',
cursor: isOnlyOne ? 'not-allowed' : 'pointer',
opacity: isOnlyOne ? 0.5 : 1,
transition: 'all 0.15s',
_hover: isOnlyOne
? {}
: {
bg: isChecked
? isDark
? 'blue.700'
: 'blue.600'
: isDark
? 'gray.600'
: 'gray.100',
},
_focus: {
outline: 'none',
boxShadow: '0 0 0 2px rgba(59, 130, 246, 0.3)',
},
})}
>
<span className={css({ fontSize: 'base' })}>{config.emoji}</span>
<span
className={css({
fontWeight: '500',
color: isChecked
? 'white'
: isDark
? 'gray.200'
: 'gray.700',
fontSize: 'sm',
})}
>
{config.label}
</span>
<span
className={css({
fontWeight: '600',
fontSize: 'xs',
color: isChecked
? isDark
? 'blue.200'
: 'blue.100'
: isDark
? 'gray.400'
: 'gray.500',
bg: isChecked
? isDark
? 'blue.700'
: 'blue.600'
: isDark
? 'gray.600'
: 'gray.200',
paddingX: '1.5',
paddingY: '0.5',
rounded: 'full',
minWidth: '6',
textAlign: 'center',
})}
>
{count}
</span>
</Checkbox.Root>
)
})}
</div>
</div>
{/* Tips Section */}
<div
data-element="tips"