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:
@@ -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) */}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user