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'
|
'use client'
|
||||||
|
|
||||||
import { useState, useCallback, useMemo, useRef, useEffect } from 'react'
|
import { useState, useCallback, useMemo, useRef, useEffect } from 'react'
|
||||||
|
import * as Checkbox from '@radix-ui/react-checkbox'
|
||||||
import { css } from '@styled/css'
|
import { css } from '@styled/css'
|
||||||
import { useTheme } from '@/contexts/ThemeContext'
|
import { useTheme } from '@/contexts/ThemeContext'
|
||||||
import { MapSelectorMap } from './MapSelectorMap'
|
import { MapSelectorMap } from './MapSelectorMap'
|
||||||
|
|
@ -15,6 +16,8 @@ import {
|
||||||
parseViewBox,
|
parseViewBox,
|
||||||
calculateFitCropViewBox,
|
calculateFitCropViewBox,
|
||||||
getFilteredMapDataBySizesSync,
|
getFilteredMapDataBySizesSync,
|
||||||
|
REGION_SIZE_CONFIG,
|
||||||
|
ALL_REGION_SIZES,
|
||||||
} from '../maps'
|
} from '../maps'
|
||||||
import type { RegionSize } from '../maps'
|
import type { RegionSize } from '../maps'
|
||||||
import {
|
import {
|
||||||
|
|
@ -67,6 +70,10 @@ interface DrillDownMapSelectorProps {
|
||||||
selectedContinent: ContinentId | 'all'
|
selectedContinent: ContinentId | 'all'
|
||||||
/** Region sizes to include (for showing excluded regions dimmed) */
|
/** Region sizes to include (for showing excluded regions dimmed) */
|
||||||
includeSizes: RegionSize[]
|
includeSizes: RegionSize[]
|
||||||
|
/** Callback when region sizes change */
|
||||||
|
onRegionSizesChange: (sizes: RegionSize[]) => void
|
||||||
|
/** Region counts per size category */
|
||||||
|
regionCountsBySize: Record<string, number>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BreadcrumbItem {
|
interface BreadcrumbItem {
|
||||||
|
|
@ -82,6 +89,8 @@ export function DrillDownMapSelector({
|
||||||
selectedMap,
|
selectedMap,
|
||||||
selectedContinent,
|
selectedContinent,
|
||||||
includeSizes,
|
includeSizes,
|
||||||
|
onRegionSizesChange,
|
||||||
|
regionCountsBySize,
|
||||||
}: DrillDownMapSelectorProps) {
|
}: DrillDownMapSelectorProps) {
|
||||||
const { resolvedTheme } = useTheme()
|
const { resolvedTheme } = useTheme()
|
||||||
const isDark = resolvedTheme === 'dark'
|
const isDark = resolvedTheme === 'dark'
|
||||||
|
|
@ -715,6 +724,138 @@ export function DrillDownMapSelector({
|
||||||
</button>
|
</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>
|
</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) */}
|
||||||
|
|
|
||||||
|
|
@ -2,25 +2,14 @@
|
||||||
|
|
||||||
import { useCallback, useMemo } from 'react'
|
import { useCallback, useMemo } from 'react'
|
||||||
import * as Select from '@radix-ui/react-select'
|
import * as Select from '@radix-ui/react-select'
|
||||||
import * as Checkbox from '@radix-ui/react-checkbox'
|
|
||||||
import { css } from '@styled/css'
|
import { css } from '@styled/css'
|
||||||
import { useTheme } from '@/contexts/ThemeContext'
|
import { useTheme } from '@/contexts/ThemeContext'
|
||||||
import { useKnowYourWorld } from '../Provider'
|
import { useKnowYourWorld } from '../Provider'
|
||||||
import { DrillDownMapSelector } from './DrillDownMapSelector'
|
import { DrillDownMapSelector } from './DrillDownMapSelector'
|
||||||
import {
|
import { ALL_REGION_SIZES, ASSISTANCE_LEVELS, getFilteredMapDataBySizesSync } from '../maps'
|
||||||
ALL_REGION_SIZES,
|
import type { AssistanceLevelConfig } from '../maps'
|
||||||
ASSISTANCE_LEVELS,
|
|
||||||
getFilteredMapDataBySizesSync,
|
|
||||||
REGION_SIZE_CONFIG,
|
|
||||||
} from '../maps'
|
|
||||||
import type { RegionSize, AssistanceLevelConfig } from '../maps'
|
|
||||||
import type { ContinentId } from '../continents'
|
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
|
// 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 }> = []
|
||||||
|
|
@ -124,14 +113,6 @@ export function SetupPhase() {
|
||||||
return counts
|
return counts
|
||||||
}, [state.selectedMap, state.selectedContinent])
|
}, [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
|
// Handle selection change from drill-down selector
|
||||||
const handleSelectionChange = useCallback(
|
const handleSelectionChange = useCallback(
|
||||||
(mapId: 'world' | 'usa', continentId: ContinentId | 'all') => {
|
(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 selectedStudyTime = STUDY_TIME_OPTIONS.find((opt) => opt.value === state.studyDuration)
|
||||||
const selectedAssistance = ASSISTANCE_LEVELS.find((level) => level.id === state.assistanceLevel)
|
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
|
// Styles for Radix Select components
|
||||||
const triggerStyles = css({
|
const triggerStyles = css({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|
@ -270,6 +237,8 @@ export function SetupPhase() {
|
||||||
onSelectionChange={handleSelectionChange}
|
onSelectionChange={handleSelectionChange}
|
||||||
onStartGame={startGame}
|
onStartGame={startGame}
|
||||||
includeSizes={state.includeSizes}
|
includeSizes={state.includeSizes}
|
||||||
|
onRegionSizesChange={setRegionSizes}
|
||||||
|
regionCountsBySize={regionCountsBySize}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -563,146 +532,6 @@ export function SetupPhase() {
|
||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* Tips Section */}
|
||||||
<div
|
<div
|
||||||
data-element="tips"
|
data-element="tips"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue