Files
novalon-website/src/components/ui/labeled-textarea.tsx
T
zhangxiang 5ecab7b15d fix(theme): 品牌色令牌语义拆分与暗黑模式全站治理
- 拆分品牌色「文字/底色」双通道:新增 --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 全零
2026-09-20 11:50:58 +08:00

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 };