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:
parent
9499e4e8b5
commit
81301ab148
|
|
@ -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) */}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue