feta:优化流畅度

This commit is contained in:
2025-12-29 15:52:50 +08:00
parent abcbe3cddc
commit 67025a4865
20 changed files with 337 additions and 175 deletions

View File

@@ -1,6 +1,7 @@
'use client'
import { useEffect, useMemo, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { ChevronDown, Search, X } from 'lucide-react'
import { cn } from '@/lib/utils'
@@ -51,6 +52,8 @@ export default function LanguageSelect({
return allOptions.find((o) => o.code === value) || null
}, [allOptions, value])
const languageCount = options.length
const filtered = useMemo(() => {
const q = query.trim().toLowerCase()
if (!q) return allOptions
@@ -85,7 +88,9 @@ export default function LanguageSelect({
disabled={disabled}
onClick={() => setOpen(true)}
className={cn(
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background transition-colors',
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background select-none will-change-transform',
'transition-[color,background-color,border-color,box-shadow,transform] duration-150 ease-out hover:shadow-sm active:translate-y-px active:scale-[0.98]',
'motion-reduce:transform-none motion-reduce:transition-none',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
'disabled:cursor-not-allowed disabled:opacity-50',
triggerClassName
@@ -97,103 +102,111 @@ export default function LanguageSelect({
<ChevronDown className="h-4 w-4 opacity-50" />
</button>
{open && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm p-4"
onMouseDown={(e) => {
if (e.target === e.currentTarget) setOpen(false)
}}
>
<Card className="w-full max-w-3xl border-muted shadow-lg">
<CardHeader className="space-y-3">
<div className="flex items-start justify-between gap-4">
<div>
<CardTitle></CardTitle>
<CardDescription> 200+ </CardDescription>
{open &&
typeof document !== 'undefined' &&
createPortal(
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm p-4 animate-in fade-in-0 duration-150 motion-reduce:animate-none"
onMouseDown={(e) => {
if (e.target === e.currentTarget) setOpen(false)
}}
>
<Card className="w-full max-w-3xl border-muted shadow-lg will-change-transform animate-in fade-in-0 zoom-in-95 duration-200 motion-reduce:animate-none">
<CardHeader className="space-y-3">
<div className="flex items-start justify-between gap-4">
<div>
<CardTitle></CardTitle>
<CardDescription>
{languageCount} {allowAuto ? ' + 自动检测' : ''}
</CardDescription>
</div>
<Button variant="ghost" size="icon" onClick={() => setOpen(false)}>
<X className="h-4 w-4" />
</Button>
</div>
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
ref={inputRef}
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={searchPlaceholder}
className="pl-9"
/>
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
ref={inputRef}
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={searchPlaceholder}
className="pl-9"
/>
</div>
{query && (
<Button variant="outline" onClick={() => setQuery('')}>
</Button>
)}
</div>
{query && (
<Button variant="outline" onClick={() => setQuery('')}>
</Button>
)}
</div>
{!query.trim() && commonOptions && commonOptions.length > 0 && (
<div className="flex flex-wrap gap-2">
{commonOptions.map((o) => (
<button
key={o.code}
type="button"
onClick={() => handleSelect(o.code)}
className={cn(
'rounded-full border px-3 py-1 text-sm transition-colors',
o.code === value
? 'bg-primary text-primary-foreground border-transparent'
: 'hover:bg-accent hover:text-accent-foreground'
)}
>
{o.name}
</button>
))}
</div>
)}
</CardHeader>
<CardContent>
<div className="rounded-md border bg-background max-h-[60vh] overflow-y-auto">
{filtered.length === 0 ? (
<div className="p-6 text-center text-sm text-muted-foreground"></div>
) : (
<div className="divide-y">
{filtered.map((o) => (
{!query.trim() && commonOptions && commonOptions.length > 0 && (
<div className="flex flex-wrap gap-2">
{commonOptions.map((o) => (
<button
key={o.code}
type="button"
onClick={() => handleSelect(o.code)}
className={cn(
'w-full text-left px-4 py-3 hover:bg-accent transition-colors',
'flex items-center justify-between gap-4'
'rounded-full border px-3 py-1 text-sm select-none will-change-transform',
'transition-[color,background-color,border-color,box-shadow,transform] duration-150 ease-out active:scale-[0.98]',
'motion-reduce:transform-none motion-reduce:transition-none',
o.code === value
? 'bg-primary text-primary-foreground border-transparent'
: 'hover:bg-accent hover:text-accent-foreground'
)}
>
<div className="min-w-0">
<div className="font-medium truncate">{o.name}</div>
<div className="text-xs text-muted-foreground font-mono">{o.code}</div>
</div>
<div className="shrink-0">
{o.code === value ? (
<Badge variant="success"></Badge>
) : (
<Badge variant="outline" className="text-muted-foreground">
</Badge>
)}
</div>
{o.name}
</button>
))}
</div>
)}
</div>
</CardContent>
</Card>
</div>
)}
</CardHeader>
<CardContent>
<div className="rounded-md border bg-background max-h-[60vh] overflow-y-auto">
{filtered.length === 0 ? (
<div className="p-6 text-center text-sm text-muted-foreground">
</div>
) : (
<div className="divide-y">
{filtered.map((o) => (
<button
key={o.code}
type="button"
onClick={() => handleSelect(o.code)}
className={cn(
'w-full text-left px-4 py-3 hover:bg-accent active:bg-accent/80 transition-colors',
'flex items-center justify-between gap-4'
)}
>
<div className="min-w-0">
<div className="font-medium truncate">{o.name}</div>
<div className="text-xs text-muted-foreground font-mono">{o.code}</div>
</div>
<div className="shrink-0">
{o.code === value ? (
<Badge variant="success"></Badge>
) : (
<Badge variant="outline" className="text-muted-foreground">
</Badge>
)}
</div>
</button>
))}
</div>
)}
</div>
</CardContent>
</Card>
</div>,
document.body
)}
</>
)
}