feat(thermometer): add "only" buttons to quickly select single category

- Appears on hover for each category option
- Shows green highlight when that category is the only one selected
- Click to set both min and max to that value

🤖 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 19:01:10 -06:00
parent 6c3c0ac70e
commit 623f882075
1 changed files with 98 additions and 31 deletions

View File

@ -236,24 +236,14 @@ export function RangeThermometer<T extends string>({
> >
{options.map((option, index) => { {options.map((option, index) => {
const inRange = isInRange(index) const inRange = isInRange(index)
const isOnly = minIndex === index && maxIndex === index
const count = counts?.[option.value] const count = counts?.[option.value]
const tooltipContent = formatTooltipContent(option) const tooltipContent = formatTooltipContent(option)
const buttonElement = ( const buttonElement = (
<button <div
type="button"
data-option={option.value} data-option={option.value}
data-in-range={inRange} data-in-range={inRange}
onClick={() => {
// Click moves nearest handle to this position
const distToMin = Math.abs(index - minIndex)
const distToMax = Math.abs(index - maxIndex)
if (distToMin <= distToMax) {
onChange(option.value, options[maxIndex].value)
} else {
onChange(options[minIndex].value, option.value)
}
}}
onMouseEnter={() => handleOptionHover(index)} onMouseEnter={() => handleOptionHover(index)}
onMouseLeave={handleOptionLeave} onMouseLeave={handleOptionLeave}
className={css({ className={css({
@ -263,37 +253,114 @@ export function RangeThermometer<T extends string>({
py: '1', py: '1',
px: '1.5', px: '1.5',
rounded: 'md', rounded: 'md',
cursor: 'pointer',
transition: 'all 0.15s', transition: 'all 0.15s',
bg: inRange ? (isDark ? 'blue.900/40' : 'blue.50') : 'transparent', bg: inRange ? (isDark ? 'blue.900/40' : 'blue.50') : 'transparent',
opacity: inRange ? 1 : 0.5, opacity: inRange ? 1 : 0.5,
_hover: { _hover: {
bg: isDark ? 'gray.700' : 'gray.100', bg: isDark ? 'gray.700' : 'gray.100',
opacity: 1, opacity: 1,
'& [data-only-button]': {
opacity: 1,
},
}, },
})} })}
> >
{/* Emoji */} {/* Main clickable area */}
{option.emoji && <span className={css({ fontSize: 'sm' })}>{option.emoji}</span>} <button
type="button"
{/* Label */} onClick={() => {
<span // Click moves nearest handle to this position
const distToMin = Math.abs(index - minIndex)
const distToMax = Math.abs(index - maxIndex)
if (distToMin <= distToMax) {
onChange(option.value, options[maxIndex].value)
} else {
onChange(options[minIndex].value, option.value)
}
}}
className={css({ className={css({
fontSize: 'xs', display: 'flex',
fontWeight: inRange ? '600' : '500', alignItems: 'center',
color: inRange gap: '1',
? isDark
? 'blue.300'
: 'blue.700'
: isDark
? 'gray.400'
: 'gray.600',
flex: 1, flex: 1,
textAlign: 'left', cursor: 'pointer',
bg: 'transparent',
border: 'none',
padding: 0,
})} })}
> >
{option.shortLabel || option.label} {/* Emoji */}
</span> {option.emoji && (
<span className={css({ fontSize: 'sm' })}>{option.emoji}</span>
)}
{/* Label */}
<span
className={css({
fontSize: 'xs',
fontWeight: inRange ? '600' : '500',
color: inRange
? isDark
? 'blue.300'
: 'blue.700'
: isDark
? 'gray.400'
: 'gray.600',
flex: 1,
textAlign: 'left',
})}
>
{option.shortLabel || option.label}
</span>
</button>
{/* Only button */}
<button
type="button"
data-only-button
title={`Show only ${option.label}`}
onClick={(e) => {
e.stopPropagation()
onChange(option.value, option.value)
}}
className={css({
fontSize: '2xs',
fontWeight: '600',
color: isOnly
? isDark
? 'green.300'
: 'green.700'
: isDark
? 'gray.400'
: 'gray.500',
bg: isOnly
? isDark
? 'green.900/50'
: 'green.100'
: 'transparent',
border: '1px solid',
borderColor: isOnly
? isDark
? 'green.700'
: 'green.300'
: isDark
? 'gray.600'
: 'gray.300',
px: '1',
py: '0',
rounded: 'sm',
cursor: 'pointer',
opacity: isOnly ? 1 : 0,
transition: 'all 0.15s',
_hover: {
bg: isDark ? 'green.900/50' : 'green.100',
borderColor: isDark ? 'green.600' : 'green.400',
color: isDark ? 'green.300' : 'green.700',
},
})}
>
only
</button>
{/* Count badge */} {/* Count badge */}
{count !== undefined && ( {count !== undefined && (
@ -325,7 +392,7 @@ export function RangeThermometer<T extends string>({
{count} {count}
</span> </span>
)} )}
</button> </div>
) )
// Wrap with tooltip if we have region names to show // Wrap with tooltip if we have region names to show