feat: multi-feature update

This commit is contained in:
chaos
2026-06-15 06:16:16 +08:00
parent 6f415428d3
commit 04d30f9dd1
58 changed files with 4610 additions and 419 deletions
+93 -51
View File
@@ -17,6 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import * as React from 'react'
import { createPortal } from 'react-dom'
import { Check, ChevronsUpDown } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { cn } from '@/lib/utils'
@@ -150,10 +151,101 @@ export function ComboboxInput({
item?.scrollIntoView({ block: 'nearest' })
}, [highlightedIndex])
const [dropdownPos, setDropdownPos] = React.useState<{
top: number
left: number
width: number
} | null>(null)
const updateDropdownPos = React.useCallback(() => {
if (!containerRef.current) return
const rect = containerRef.current.getBoundingClientRect()
setDropdownPos({
top: rect.bottom + 4,
left: rect.left,
width: rect.width,
})
}, [])
// Update dropdown position when open
React.useEffect(() => {
if (!open) {
setDropdownPos(null)
return
}
updateDropdownPos()
const handleScroll = () => updateDropdownPos()
window.addEventListener('scroll', handleScroll, true)
window.addEventListener('resize', handleScroll)
return () => {
window.removeEventListener('scroll', handleScroll, true)
window.removeEventListener('resize', handleScroll)
}
}, [open, updateDropdownPos])
const showDropdown =
open &&
(filteredOptions.length > 0 || (allowCustomValue && searchValue.trim()))
const dropdownContent = showDropdown && dropdownPos ? (
<div
className='bg-popover text-popover-foreground fixed z-[100] rounded-md border shadow-md'
style={{
top: dropdownPos.top,
left: dropdownPos.left,
width: dropdownPos.width,
}}
>
{filteredOptions.length > 0 ? (
<ul
ref={listRef}
role='listbox'
className='max-h-[200px] overflow-y-auto p-1'
>
{filteredOptions.map((option, index) => (
<li
key={option.value}
role='option'
aria-selected={value === option.value}
data-highlighted={index === highlightedIndex}
className={cn(
'relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm select-none',
index === highlightedIndex &&
'bg-accent text-accent-foreground',
value === option.value && 'font-medium'
)}
onMouseEnter={() => setHighlightedIndex(index)}
onMouseDown={(e) => {
e.preventDefault() // Prevent blur
handleSelect(option.value)
}}
>
<Check
className={cn(
'size-4 shrink-0',
value === option.value ? 'opacity-100' : 'opacity-0'
)}
/>
{option.icon && <span>{option.icon}</span>}
<span className='truncate'>{option.label}</span>
</li>
))}
</ul>
) : (
<div className='px-2 py-6 text-center text-sm'>
{t(emptyText)}
{allowCustomValue && searchValue.trim() && (
<div className='text-muted-foreground mt-1 text-xs'>
{t('Press Enter to use "{{value}}"', {
value: searchValue.trim(),
})}
</div>
)}
</div>
)}
</div>
) : null
return (
<div ref={containerRef} className='relative'>
<Input
@@ -184,57 +276,7 @@ export function ComboboxInput({
/>
<ChevronsUpDown className='pointer-events-none absolute top-1/2 right-3 size-4 shrink-0 -translate-y-1/2 opacity-50' />
{showDropdown && (
<div className='bg-popover text-popover-foreground absolute top-full z-100 mt-1 w-full rounded-md border shadow-md'>
{filteredOptions.length > 0 ? (
<ul
ref={listRef}
role='listbox'
className='max-h-[200px] overflow-y-auto p-1'
>
{filteredOptions.map((option, index) => (
<li
key={option.value}
role='option'
aria-selected={value === option.value}
data-highlighted={index === highlightedIndex}
className={cn(
'relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm select-none',
index === highlightedIndex &&
'bg-accent text-accent-foreground',
value === option.value && 'font-medium'
)}
onMouseEnter={() => setHighlightedIndex(index)}
onMouseDown={(e) => {
e.preventDefault() // Prevent blur
handleSelect(option.value)
}}
>
<Check
className={cn(
'size-4 shrink-0',
value === option.value ? 'opacity-100' : 'opacity-0'
)}
/>
{option.icon && <span>{option.icon}</span>}
<span className='truncate'>{option.label}</span>
</li>
))}
</ul>
) : (
<div className='px-2 py-6 text-center text-sm'>
{t(emptyText)}
{allowCustomValue && searchValue.trim() && (
<div className='text-muted-foreground mt-1 text-xs'>
{t('Press Enter to use "{{value}}"', {
value: searchValue.trim(),
})}
</div>
)}
</div>
)}
</div>
)}
{dropdownContent && createPortal(dropdownContent, document.body)}
</div>
)
}