- 拆分品牌色「文字/底色」双通道:新增 --color-brand-ink(-rgb)(深色 #F87171) 与 tailwind brand.ink;text-brand → text-brand-ink 迁移 247 处; bg-brand/border-brand 保持 --color-brand-rgb 不翻转(红底白字 5.58:1 不能动) - dark 块补齐 --color-brand-bg: #2A1418(修复深色白字压浅粉底 1.04:1) - hero 徽章新增 --badge-accent-text 桥接:accentColor 直用作深底文字不可读 (#1e3a5f 深底 1.65:1),浅色取原色 / 深色 color-mix 提亮 - 硬编码颜色 token 化:bg-white/text-gray-*/border-gray-* → bg-bg-*/text-text-*/border-border-* - 可访问性:触控目标 <24px 归零(footer 链接列 [&_a]:min-h-6 收口 377 处等)、 news 搜索框补 aria-label、标题跳级 h2→h4 修正为 h3、装饰 blur 光斑容器补 overflow-hidden 消除伪文本裁剪 - 验证:tsc 0 / eslint 0 error / jest 0 失败用例 / dogfood 33 路由 × 双主题 P1-P3 全零
73 lines
2.1 KiB
TypeScript
73 lines
2.1 KiB
TypeScript
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-ink 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-ink"
|
|
role="alert"
|
|
data-testid="error-message"
|
|
>
|
|
{error}
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
LabeledTextarea.displayName = 'LabeledTextarea';
|
|
|
|
export { LabeledTextarea };
|