feat(know-your-world): unified region selector with inline list on desktop

- Combine region size selector and region list into one unified panel on desktop
- Hide "N regions" count on md+ screens, show inline scrollable list instead
- Keep "N regions" as clickable popover menu on mobile screens
- Add hideCountOnMd prop to RangeThermometer for responsive count display
- Constrain region list width to match thermometer (no panel expansion)
- Region names wrap within available width using CSS width:0 + minWidth:100% trick

🤖 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 15:21:15 -06:00
parent dc4d62195b
commit d329d80399
3 changed files with 205 additions and 192 deletions

View File

@@ -1052,106 +1052,102 @@ export function DrillDownMapSelector({
)
})()}
{/* Region Size Range Selector - positioned inside map, right side */}
{/* Right-side controls container - region size selector with inline list on desktop */}
<div
data-element="region-size-filters"
data-element="right-controls"
className={css({
position: 'absolute',
top: fillContainer ? '164px' : '3',
right: { base: '8px', sm: '24px' },
padding: { base: '2', sm: '3' },
bg: isDark ? 'gray.800' : 'gray.100',
rounded: 'xl',
shadow: 'lg',
zIndex: 10,
transform: { base: 'scale(0.75)', sm: 'scale(1)' },
transformOrigin: 'top right',
})}
>
<RangeThermometer
options={SIZE_OPTIONS}
minValue={sizesToRange(includeSizes)[0]}
maxValue={sizesToRange(includeSizes)[1]}
onChange={(min, max) => onRegionSizesChange(rangeToSizes(min, max))}
orientation="vertical"
isDark={isDark}
counts={regionCountsBySize as Partial<Record<RegionSize, number>>}
showTotalCount
onHoverPreview={setSizeRangePreview}
regionNamesByCategory={regionNamesBySize}
selectedRegionNames={selectedRegionNames}
onRegionNameHover={setPreviewRegionName}
/>
</div>
{/* Region List Panel - visible on larger screens only */}
{fillContainer && selectedRegionNames.length > 0 && (
{/* Region Size Range Selector with inline list expansion on desktop */}
<div
data-element="region-list-panel"
data-element="region-size-filters"
className={css({
position: 'absolute',
top: '164px',
right: { base: '8px', sm: '180px' },
display: { base: 'none', md: 'flex' },
display: 'flex',
flexDirection: 'column',
width: '180px',
maxHeight: '280px',
padding: '3',
bg: isDark ? 'gray.800' : 'gray.100',
rounded: 'xl',
shadow: 'lg',
zIndex: 10,
maxHeight: { base: 'none', md: fillContainer ? '400px' : 'none' },
overflow: 'hidden',
})}
>
{/* Header */}
<div
className={css({
px: '3',
py: '2',
borderBottom: '1px solid',
borderColor: isDark ? 'gray.700' : 'gray.200',
fontWeight: '600',
fontSize: 'sm',
color: isDark ? 'gray.100' : 'gray.800',
flexShrink: 0,
})}
>
{selectedRegionNames.length} regions
</div>
{/* Scrollable list */}
<div
className={css({
overflowY: 'auto',
flex: 1,
py: '1',
})}
>
{selectedRegionNames
.slice()
.sort((a, b) => a.localeCompare(b))
.map((name) => (
<div
key={name}
onMouseEnter={() => setPreviewRegionName(name)}
onMouseLeave={() => setPreviewRegionName(null)}
className={css({
px: '3',
py: '1',
fontSize: 'xs',
color: isDark ? 'gray.300' : 'gray.600',
cursor: 'pointer',
_hover: {
bg: isDark ? 'gray.700' : 'gray.200',
color: isDark ? 'gray.100' : 'gray.900',
},
})}
>
{name}
</div>
))}
</div>
<RangeThermometer
options={SIZE_OPTIONS}
minValue={sizesToRange(includeSizes)[0]}
maxValue={sizesToRange(includeSizes)[1]}
onChange={(min, max) => onRegionSizesChange(rangeToSizes(min, max))}
orientation="vertical"
isDark={isDark}
counts={regionCountsBySize as Partial<Record<RegionSize, number>>}
showTotalCount
onHoverPreview={setSizeRangePreview}
regionNamesByCategory={regionNamesBySize}
selectedRegionNames={selectedRegionNames}
onRegionNameHover={setPreviewRegionName}
hideCountOnMd={fillContainer && selectedRegionNames.length > 0}
/>
{/* Inline region list - visible on larger screens only, expands below thermometer */}
{fillContainer && selectedRegionNames.length > 0 && (
<div
data-element="region-list-inline"
className={css({
display: { base: 'none', md: 'flex' },
flexDirection: 'column',
borderTop: '1px solid',
borderColor: isDark ? 'gray.700' : 'gray.300',
marginTop: '2',
paddingTop: '2',
maxHeight: '200px',
/* Prevent this element from expanding the parent - use 0 width + min 100% trick */
width: 0,
minWidth: '100%',
})}
>
{/* Scrollable list */}
<div
className={css({
overflowY: 'auto',
flex: 1,
})}
>
{selectedRegionNames
.slice()
.sort((a, b) => a.localeCompare(b))
.map((name) => (
<div
key={name}
onMouseEnter={() => setPreviewRegionName(name)}
onMouseLeave={() => setPreviewRegionName(null)}
className={css({
px: '2',
py: '1',
fontSize: 'xs',
color: isDark ? 'gray.300' : 'gray.600',
cursor: 'pointer',
rounded: 'md',
overflowWrap: 'break-word',
_hover: {
bg: isDark ? 'gray.700' : 'gray.200',
color: isDark ? 'gray.100' : 'gray.900',
},
})}
>
{name}
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
</div>
{/* Peer Navigation - Mini-map thumbnails below main map (or planets at world level) */}

View File

@@ -29,6 +29,7 @@ export function RangeThermometer<T extends string>({
regionNamesByCategory,
selectedRegionNames,
onRegionNameHover,
hideCountOnMd = false,
}: RangeThermometerProps<T>) {
const isVertical = orientation === 'vertical'
const [isDragging, setIsDragging] = useState(false)
@@ -354,14 +355,21 @@ export function RangeThermometer<T extends string>({
<div className={css({ color: 'gray.300' })}>
{tooltipContent.names.join(', ')}
{tooltipContent.remaining > 0 && (
<span className={css({ color: 'gray.400', fontStyle: 'italic' })}>
<span
className={css({
color: 'gray.400',
fontStyle: 'italic',
})}
>
{' '}
...and {tooltipContent.remaining} more
</span>
)}
</div>
<Tooltip.Arrow
className={css({ fill: isDark ? 'gray.800' : 'gray.900' })}
className={css({
fill: isDark ? 'gray.800' : 'gray.900',
})}
/>
</Tooltip.Content>
</Tooltip.Portal>
@@ -483,129 +491,136 @@ export function RangeThermometer<T extends string>({
{/* Total count display - clickable to show all selected regions */}
{totalCount !== null && (
<Popover.Root>
<Popover.Trigger asChild>
<button
type="button"
data-element="total-count"
className={css({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '1',
py: '1.5',
px: '2',
bg: isDark ? 'blue.900/30' : 'blue.50',
rounded: 'md',
border: '1px solid',
borderColor: isDark ? 'blue.800' : 'blue.200',
cursor: selectedRegionNames?.length ? 'pointer' : 'default',
transition: 'all 0.15s',
_hover: selectedRegionNames?.length
? {
bg: isDark ? 'blue.900/50' : 'blue.100',
borderColor: isDark ? 'blue.700' : 'blue.300',
}
: {},
})}
>
<span
<div
className={css({
display: hideCountOnMd ? { base: 'block', md: 'none' } : 'block',
})}
>
<Popover.Root>
<Popover.Trigger asChild>
<button
type="button"
data-element="total-count"
className={css({
fontSize: 'sm',
fontWeight: 'bold',
color: isDark ? 'blue.300' : 'blue.700',
textDecoration: selectedRegionNames?.length ? 'underline' : 'none',
textDecorationStyle: 'dotted',
textUnderlineOffset: '2px',
})}
>
{totalCount}
</span>
<span
className={css({
fontSize: 'xs',
color: isDark ? 'blue.400' : 'blue.600',
})}
>
regions
</span>
</button>
</Popover.Trigger>
{selectedRegionNames && selectedRegionNames.length > 0 && (
<Popover.Portal>
<Popover.Content
side="top"
sideOffset={8}
align="center"
className={css({
bg: isDark ? 'gray.800' : 'white',
border: '1px solid',
borderColor: isDark ? 'gray.700' : 'gray.200',
rounded: 'lg',
boxShadow: 'xl',
zIndex: 50,
width: '200px',
maxHeight: '300px',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '1',
py: '1.5',
px: '2',
bg: isDark ? 'blue.900/30' : 'blue.50',
rounded: 'md',
border: '1px solid',
borderColor: isDark ? 'blue.800' : 'blue.200',
cursor: selectedRegionNames?.length ? 'pointer' : 'default',
transition: 'all 0.15s',
width: '100%',
_hover: selectedRegionNames?.length
? {
bg: isDark ? 'blue.900/50' : 'blue.100',
borderColor: isDark ? 'blue.700' : 'blue.300',
}
: {},
})}
>
{/* Header */}
<div
<span
className={css({
px: '3',
py: '2',
borderBottom: '1px solid',
borderColor: isDark ? 'gray.700' : 'gray.200',
fontWeight: '600',
fontSize: 'sm',
color: isDark ? 'gray.100' : 'gray.800',
fontWeight: 'bold',
color: isDark ? 'blue.300' : 'blue.700',
textDecoration: selectedRegionNames?.length ? 'underline' : 'none',
textDecorationStyle: 'dotted',
textUnderlineOffset: '2px',
})}
>
{selectedRegionNames.length} regions selected
</div>
{/* Scrollable list */}
<div
{totalCount}
</span>
<span
className={css({
overflowY: 'auto',
flex: 1,
py: '1',
fontSize: 'xs',
color: isDark ? 'blue.400' : 'blue.600',
})}
>
{selectedRegionNames
.slice()
.sort((a, b) => a.localeCompare(b))
.map((name) => (
<div
key={name}
onMouseEnter={() => onRegionNameHover?.(name)}
onMouseLeave={() => onRegionNameHover?.(null)}
className={css({
px: '3',
py: '1',
fontSize: 'xs',
color: isDark ? 'gray.300' : 'gray.600',
cursor: onRegionNameHover ? 'pointer' : 'default',
_hover: {
bg: isDark ? 'gray.700' : 'gray.100',
color: isDark ? 'gray.100' : 'gray.900',
},
})}
>
{name}
</div>
))}
</div>
<Popover.Arrow
regions
</span>
</button>
</Popover.Trigger>
{selectedRegionNames && selectedRegionNames.length > 0 && (
<Popover.Portal>
<Popover.Content
side="top"
sideOffset={8}
align="center"
className={css({
fill: isDark ? 'gray.800' : 'white',
bg: isDark ? 'gray.800' : 'white',
border: '1px solid',
borderColor: isDark ? 'gray.700' : 'gray.200',
rounded: 'lg',
boxShadow: 'xl',
zIndex: 50,
width: '200px',
maxHeight: '300px',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
})}
/>
</Popover.Content>
</Popover.Portal>
)}
</Popover.Root>
>
{/* Header */}
<div
className={css({
px: '3',
py: '2',
borderBottom: '1px solid',
borderColor: isDark ? 'gray.700' : 'gray.200',
fontWeight: '600',
fontSize: 'sm',
color: isDark ? 'gray.100' : 'gray.800',
})}
>
{selectedRegionNames.length} regions selected
</div>
{/* Scrollable list */}
<div
className={css({
overflowY: 'auto',
flex: 1,
py: '1',
})}
>
{selectedRegionNames
.slice()
.sort((a, b) => a.localeCompare(b))
.map((name) => (
<div
key={name}
onMouseEnter={() => onRegionNameHover?.(name)}
onMouseLeave={() => onRegionNameHover?.(null)}
className={css({
px: '3',
py: '1',
fontSize: 'xs',
color: isDark ? 'gray.300' : 'gray.600',
cursor: onRegionNameHover ? 'pointer' : 'default',
_hover: {
bg: isDark ? 'gray.700' : 'gray.100',
color: isDark ? 'gray.100' : 'gray.900',
},
})}
>
{name}
</div>
))}
</div>
<Popover.Arrow
className={css({
fill: isDark ? 'gray.800' : 'white',
})}
/>
</Popover.Content>
</Popover.Portal>
)}
</Popover.Root>
</div>
)}
</div>
</Tooltip.Provider>

View File

@@ -61,4 +61,6 @@ export interface RangeThermometerProps<T extends string> extends ThermometerBase
selectedRegionNames?: string[]
/** Callback when hovering over a region name in the popover (for map preview) */
onRegionNameHover?: (regionName: string | null) => void
/** Hide the total count on md+ breakpoints (use when inline list is shown on desktop) */
hideCountOnMd?: boolean
}