feat:重构UI

This commit is contained in:
2025-12-26 16:03:12 +08:00
parent 1429e0e66a
commit abcbe3cddc
67 changed files with 12170 additions and 515 deletions

View File

@@ -0,0 +1,199 @@
'use client'
import { useEffect, useMemo, useRef, useState } from 'react'
import { ChevronDown, Search, X } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import type { LanguageOption } from '@/lib/languages'
type Props = {
value: string
onValueChange: (value: string) => void
options: LanguageOption[]
commonOptions?: LanguageOption[]
placeholder?: string
searchPlaceholder?: string
disabled?: boolean
allowAuto?: boolean
autoOption?: LanguageOption
triggerClassName?: string
}
export default function LanguageSelect({
value,
onValueChange,
options,
commonOptions,
placeholder = '选择语言',
searchPlaceholder = '搜索语言(名称 / 代码)',
disabled,
allowAuto,
autoOption,
triggerClassName,
}: Props) {
const [open, setOpen] = useState(false)
const [query, setQuery] = useState('')
const inputRef = useRef<HTMLInputElement | null>(null)
const allOptions = useMemo(() => {
const list = [...options]
if (allowAuto && autoOption) {
return [autoOption, ...list]
}
return list
}, [allowAuto, autoOption, options])
const selected = useMemo(() => {
return allOptions.find((o) => o.code === value) || null
}, [allOptions, value])
const filtered = useMemo(() => {
const q = query.trim().toLowerCase()
if (!q) return allOptions
return allOptions.filter((o) => (o.search || `${o.name} ${o.code}`).toLowerCase().includes(q))
}, [allOptions, query])
useEffect(() => {
if (!open) return
setQuery('')
const id = window.setTimeout(() => inputRef.current?.focus(), 0)
return () => window.clearTimeout(id)
}, [open])
useEffect(() => {
if (!open) return
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') setOpen(false)
}
window.addEventListener('keydown', onKeyDown)
return () => window.removeEventListener('keydown', onKeyDown)
}, [open])
const handleSelect = (code: string) => {
onValueChange(code)
setOpen(false)
}
return (
<>
<button
type="button"
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',
'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
)}
>
<span className={cn('line-clamp-1', selected ? 'text-foreground' : 'text-muted-foreground')}>
{selected?.name || placeholder}
</span>
<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>
</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>
{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) => (
<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'
)}
>
<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>
)}
</>
)
}

View File

@@ -1,24 +1,25 @@
'use client'
import { useState, useRef } from 'react'
import { ArrowRightLeft, Copy, Sparkles, StopCircle, Check } from './icons'
import { Button } from './ui/button'
import { Textarea } from './ui/textarea'
import { Card } from './ui/card'
import { cn } from '../lib/utils'
import LanguageSelect from './LanguageSelect'
import {
AUTO_LANGUAGE,
COMMON_LANGUAGE_OPTIONS,
getLanguageName,
LANGUAGE_OPTIONS,
} from '@/lib/languages'
const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000'
const LANGUAGES = [
{ code: 'auto', name: '自动检测' },
{ code: 'zh', name: '中文' },
{ code: 'en', name: '英语' },
{ code: 'ja', name: '日语' },
{ code: 'ko', name: '韩语' },
{ code: 'fr', name: '法语' },
{ code: 'de', name: '德语' },
{ code: 'es', name: '西班牙语' },
]
const STYLES = [
{ value: 'literal', name: '直译' },
{ value: 'fluent', name: '意译' },
{ value: 'casual', name: '口语' },
{ value: 'literal', name: '直译', description: '保留原文结构' },
{ value: 'fluent', name: '意译', description: '自然流畅' },
{ value: 'casual', name: '口语', description: '轻松对话' },
]
export default function TranslatorForm() {
@@ -29,6 +30,7 @@ export default function TranslatorForm() {
const [style, setStyle] = useState('literal')
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState('')
const [copied, setCopied] = useState(false)
const abortRef = useRef<AbortController | null>(null)
const handleTranslate = async () => {
@@ -50,7 +52,9 @@ export default function TranslatorForm() {
body: JSON.stringify({
source_text: sourceText,
source_lang: sourceLang,
source_lang_name: getLanguageName(sourceLang),
target_lang: targetLang,
target_lang_name: getLanguageName(targetLang),
style,
}),
signal: abortRef.current.signal,
@@ -83,8 +87,8 @@ export default function TranslatorForm() {
}
}
}
} catch (err: unknown) {
if (err instanceof Error && err.name !== 'AbortError') {
} catch (err: any) {
if (err.name !== 'AbortError') {
setError(err.message || '翻译失败')
}
} finally {
@@ -101,121 +105,135 @@ export default function TranslatorForm() {
const handleCopy = () => {
navigator.clipboard.writeText(translation)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
const handleSwapLanguages = () => {
if (sourceLang === 'auto') return
setSourceLang(targetLang)
setTargetLang(sourceLang)
setSourceText(translation)
setTranslation(sourceText)
}
return (
<div className="max-w-4xl mx-auto">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
{/* 源语言选择 */}
<div>
<label className="block text-sm font-medium mb-1"></label>
<select
value={sourceLang}
onChange={(e) => setSourceLang(e.target.value)}
className="w-full p-2 border rounded"
>
{LANGUAGES.map((lang) => (
<option key={lang.code} value={lang.code}>
{lang.name}
</option>
))}
</select>
</div>
{/* 目标语言选择 */}
<div>
<label className="block text-sm font-medium mb-1"></label>
<select
value={targetLang}
onChange={(e) => setTargetLang(e.target.value)}
className="w-full p-2 border rounded"
>
{LANGUAGES.filter((l) => l.code !== 'auto').map((lang) => (
<option key={lang.code} value={lang.code}>
{lang.name}
</option>
))}
</select>
</div>
</div>
{/* 风格选择 */}
<div className="mb-4">
<label className="block text-sm font-medium mb-1"></label>
<div className="flex gap-4">
{STYLES.map((s) => (
<label key={s.value} className="flex items-center">
<input
type="radio"
name="style"
value={s.value}
checked={style === s.value}
onChange={(e) => setStyle(e.target.value)}
className="mr-1"
<div className="w-full max-w-5xl mx-auto space-y-6">
{/* Controls Bar */}
<Card className="p-4 bg-background/50 backdrop-blur-sm border-muted">
<div className="flex flex-col md:flex-row gap-4 justify-between items-center">
<div className="flex items-center gap-2 w-full md:w-auto">
<div className="w-[140px]">
<LanguageSelect
value={sourceLang}
onValueChange={setSourceLang}
options={LANGUAGE_OPTIONS}
commonOptions={[AUTO_LANGUAGE, ...COMMON_LANGUAGE_OPTIONS]}
placeholder="源语言"
allowAuto
autoOption={AUTO_LANGUAGE}
/>
{s.name}
</label>
))}
</div>
</div>
</div>
{/* 输入输出区域 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-sm font-medium mb-1"></label>
<textarea
value={sourceText}
onChange={(e) => setSourceText(e.target.value)}
placeholder="请输入要翻译的文本..."
className="w-full h-48 p-3 border rounded resize-none"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1"></label>
<div className="relative">
<textarea
value={translation}
readOnly
placeholder="翻译结果将显示在这里..."
className="w-full h-48 p-3 border rounded resize-none bg-gray-50"
/>
{translation && (
<button
onClick={handleCopy}
className="absolute top-2 right-2 px-2 py-1 text-xs bg-white border rounded hover:bg-gray-100"
<Button
variant="ghost"
size="icon"
onClick={handleSwapLanguages}
className="shrink-0"
disabled={sourceLang === 'auto'}
>
<ArrowRightLeft className="h-4 w-4" />
</Button>
<div className="w-[140px]">
<LanguageSelect
value={targetLang}
onValueChange={setTargetLang}
options={LANGUAGE_OPTIONS}
commonOptions={COMMON_LANGUAGE_OPTIONS}
placeholder="目标语言"
/>
</div>
</div>
<div className="flex items-center gap-2 w-full md:w-auto overflow-x-auto pb-2 md:pb-0">
{STYLES.map((s) => (
<Button
key={s.value}
variant={style === s.value ? "secondary" : "ghost"}
size="sm"
onClick={() => setStyle(s.value)}
className="whitespace-nowrap"
>
</button>
{s.name}
</Button>
))}
</div>
<div className="flex items-center gap-2 ml-auto">
{isLoading ? (
<Button onClick={handleStop} variant="destructive" size="sm">
<StopCircle className="mr-2 h-4 w-4" />
</Button>
) : (
<Button
onClick={handleTranslate}
disabled={!sourceText.trim()}
className="bg-primary hover:bg-primary/90 text-white shadow-lg shadow-primary/20"
>
<Sparkles className="mr-2 h-4 w-4" />
</Button>
)}
</div>
</div>
</div>
</Card>
{/* 错误提示 */}
{error && (
<div className="mb-4 p-3 bg-red-50 text-red-600 rounded">
{error}
</div>
)}
{/* Main Input/Output Area */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 h-[500px]">
{/* Source Text */}
<Card className="relative p-0 overflow-hidden border-muted focus-within:ring-2 focus-within:ring-primary/20 transition-all">
<Textarea
value={sourceText}
onChange={(e) => setSourceText(e.target.value)}
placeholder="在此输入要翻译的文本..."
className="w-full h-full min-h-[400px] p-6 text-lg border-0 focus-visible:ring-0 resize-none bg-transparent"
/>
<div className="absolute bottom-4 right-4 text-xs text-muted-foreground">
{sourceText.length}
</div>
</Card>
{/* 按钮 */}
<div className="flex gap-4">
<button
onClick={handleTranslate}
disabled={isLoading || !sourceText.trim()}
className="px-6 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
>
{isLoading ? '翻译中...' : '翻译'}
</button>
{isLoading && (
<button
onClick={handleStop}
className="px-6 py-2 bg-gray-600 text-white rounded hover:bg-gray-700"
>
</button>
)}
{/* Translation Result */}
<Card className="relative p-0 overflow-hidden border-muted bg-muted/30">
<Textarea
value={translation}
readOnly
placeholder="翻译结果..."
className="w-full h-full min-h-[400px] p-6 text-lg border-0 focus-visible:ring-0 resize-none bg-transparent cursor-default"
/>
{translation && (
<div className="absolute bottom-4 right-4 flex gap-2">
<Button
variant="secondary"
size="icon"
className="h-8 w-8 shadow-sm"
onClick={handleCopy}
>
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
)}
{error && (
<div className="absolute bottom-4 left-4 right-14 p-2 bg-red-100 dark:bg-red-900/30 text-red-600 text-sm rounded border border-red-200 dark:border-red-900">
{error}
</div>
)}
</Card>
</div>
</div>
)
}
}

View File

@@ -0,0 +1,143 @@
import * as React from "react"
export function ArrowRightLeft({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="m16 3 4 4-4 4" />
<path d="M20 7H4" />
<path d="m8 21-4-4 4-4" />
<path d="M4 17h16" />
</svg>
)
}
export function Check({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="M20 6 9 17l-5-5" />
</svg>
)
}
export function ChevronDown({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="m6 9 6 6 6-6" />
</svg>
)
}
export function ChevronUp({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="m18 15-6-6-6 6" />
</svg>
)
}
export function Copy({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
</svg>
)
}
export function Sparkles({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z" />
<path d="M5 3v4" />
<path d="M9 3v4" />
<path d="M3 5h4" />
<path d="M3 9h4" />
</svg>
)
}
export function StopCircle({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<circle cx="12" cy="12" r="10" />
<rect width="6" height="6" x="9" y="9" />
</svg>
)
}

View File

@@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "../../lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground",
secondary: "border-transparent bg-secondary text-secondary-foreground",
outline: "text-foreground",
destructive: "border-transparent bg-destructive text-destructive-foreground",
success: "border-transparent bg-emerald-500 text-white",
warning: "border-transparent bg-amber-500 text-white",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> { }
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@@ -0,0 +1,56 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "../../lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@@ -0,0 +1,79 @@
import * as React from "react"
import { cn } from "../../lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -0,0 +1,26 @@
import * as React from "react"
import { cn } from "../../lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> { }
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@@ -0,0 +1,20 @@
import * as React from "react"
import { cn } from "../../lib/utils"
const Label = React.forwardRef<HTMLLabelElement, React.LabelHTMLAttributes<HTMLLabelElement>>(
({ className, ...props }, ref) => (
<label
ref={ref}
className={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className
)}
{...props}
/>
)
)
Label.displayName = "Label"
export { Label }

View File

@@ -0,0 +1,160 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "../icons"
import { cn } from "../../lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
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 placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@@ -0,0 +1,118 @@
import * as React from "react"
import { cn } from "../../lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "../../lib/utils"
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> { }
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
export { Textarea }