feat(ui): 重构核心 UI 组件库,新增 shadcn/ui 组件
- 重构 Button、Card、Badge、Input、Textarea 等基础组件 - 新增 Accordion、Alert、Dialog、Dropdown、Form 等 shadcn/ui 组件 - 新增 AnimatedCounter、StatsShowcase、MetricCard 等数据展示组件 - 新增 ScrollReveal 滚动动画组件 - 重构 Toast 通知系统与 Tooltip 提示组件 - 更新设计令牌系统,对齐新品牌视觉
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Textarea } from './textarea';
|
||||
import { Label } from './label';
|
||||
|
||||
/**
|
||||
* LabeledTextarea - 带 label 和 error 的 Textarea 包装组件
|
||||
* 用于兼容现有表单(如 contact 表单)的 label/error API
|
||||
* 底层使用标准 shadcn/ui Textarea + Label
|
||||
*/
|
||||
export interface LabeledTextareaProps extends React.ComponentProps<'textarea'> {
|
||||
label?: React.ReactNode;
|
||||
error?: string;
|
||||
hint?: React.ReactNode;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
const LabeledTextarea = React.forwardRef<HTMLTextAreaElement, LabeledTextareaProps>(
|
||||
({ label, error, hint, id, required, className, ...props }, ref) => {
|
||||
const generatedId = React.useId();
|
||||
const textareaId = id || generatedId;
|
||||
const errorId = `${textareaId}-error`;
|
||||
const hintId = `${textareaId}-hint`;
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{label && (
|
||||
<Label
|
||||
htmlFor={textareaId}
|
||||
className="block text-sm font-medium text-text-secondary mb-2"
|
||||
>
|
||||
{label}
|
||||
{required && (
|
||||
<span className="text-brand ml-1.5" aria-hidden="true">*</span>
|
||||
)}
|
||||
</Label>
|
||||
)}
|
||||
<Textarea
|
||||
id={textareaId}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
className,
|
||||
error && 'border-brand focus-visible:border-brand focus-visible:ring-brand/30'
|
||||
)}
|
||||
aria-required={required ? 'true' : undefined}
|
||||
aria-invalid={error ? 'true' : undefined}
|
||||
aria-describedby={cn(error && errorId, hint && hintId) || undefined}
|
||||
required={required}
|
||||
{...props}
|
||||
/>
|
||||
{hint && !error && (
|
||||
<p id={hintId} className="mt-1.5 text-xs text-text-muted">
|
||||
{hint}
|
||||
</p>
|
||||
)}
|
||||
{error && (
|
||||
<p
|
||||
id={errorId}
|
||||
className="mt-1.5 text-sm text-brand"
|
||||
role="alert"
|
||||
data-testid="error-message"
|
||||
>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
LabeledTextarea.displayName = 'LabeledTextarea';
|
||||
|
||||
export { LabeledTextarea };
|
||||
Reference in New Issue
Block a user