71432e9ae7
Глобальные правки через @theme в styles.css вместо 200+ точечных правок: - text-sm: 14px -> 13px (handoff workhorse — body, buttons, tabs) - text-2xs: undefined -> 11px (handoff IDs, mono meta — было невидимо) - @utility text-cap: 10.5px Tektur uppercase 0.10em (handoff caps) Codemod (30 sites): text-2xs + uppercase + tracking-[0.10em] -> text-cap. Sidebar section labels consolidated с explicit text-[10.5px] на text-cap. Покрывает 64 text-sm + 148 text-2xs автоматически. Table cells (12.5px) и page/modal titles (17px/22px) остаются explicit text-[Npx] arbitrary values по месту — нет slots в Tailwind scale.
74 lines
2.5 KiB
TypeScript
74 lines
2.5 KiB
TypeScript
import * as React from 'react'
|
||
import { cn } from '@/lib/utils'
|
||
|
||
/**
|
||
* TextArea — multi-line text input с handoff tokens. Заменяет {@code @nstart/ui}
|
||
* TextArea (Phase 3 migration step).
|
||
*
|
||
* Поддерживает 2 режима (как Input):
|
||
* - "bare" — просто <textarea>, для inline использования.
|
||
* - "field" — когда задан label/hint/error, рендерится FormField обёртка.
|
||
*/
|
||
export type TextAreaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement> & {
|
||
/** Если задан — рендерится field-обёртка с label выше. */
|
||
label?: React.ReactNode
|
||
/** Optional helper text under textarea. */
|
||
hint?: React.ReactNode
|
||
/** Error message — overrides hint визуально, accent в pink. */
|
||
error?: React.ReactNode
|
||
/** Wrapper className когда rendering field-mode. */
|
||
containerClassName?: string
|
||
}
|
||
|
||
export const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
||
function TextArea(
|
||
{ className, label, hint, error, required, containerClassName, id, rows = 4, ...props },
|
||
ref,
|
||
) {
|
||
const autoId = React.useId()
|
||
const inputId = id ?? autoId
|
||
const textareaEl = (
|
||
<textarea
|
||
id={inputId}
|
||
ref={ref}
|
||
rows={rows}
|
||
required={required}
|
||
aria-required={required || undefined}
|
||
aria-invalid={Boolean(error) || undefined}
|
||
className={cn(
|
||
'flex w-full rounded-md border border-line bg-surface px-3 py-2 text-sm text-ink',
|
||
'placeholder:text-mute',
|
||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-bg',
|
||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||
'resize-y min-h-[80px]',
|
||
error && 'border-pink focus-visible:ring-pink',
|
||
className,
|
||
)}
|
||
{...props}
|
||
/>
|
||
)
|
||
|
||
if (!label && !hint && !error) return textareaEl
|
||
|
||
return (
|
||
<div className={cn('flex flex-col gap-1', containerClassName)}>
|
||
{label && (
|
||
<label
|
||
htmlFor={inputId}
|
||
className="text-cap font-medium font-display text-mute leading-none"
|
||
>
|
||
{label}
|
||
{required && <span className="text-pink ml-0.5">*</span>}
|
||
</label>
|
||
)}
|
||
{textareaEl}
|
||
{error ? (
|
||
<span className="text-xs text-pink">{error}</span>
|
||
) : hint ? (
|
||
<span className="text-xs text-mute">{hint}</span>
|
||
) : null}
|
||
</div>
|
||
)
|
||
},
|
||
)
|