113 lines
4.3 KiB
TypeScript
113 lines
4.3 KiB
TypeScript
/**
|
|
* Textarea Component
|
|
*
|
|
* Multi-line text input with label and error states.
|
|
* Automatically adapts styling based on active theme (luxe or cyberpunk).
|
|
*/
|
|
|
|
import styled, { css } from 'styled-components';
|
|
import type { ThemeInterface } from '@lilith/ui-theme';
|
|
|
|
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
/** Textarea label */
|
|
label?: string;
|
|
/** Error message */
|
|
error?: string;
|
|
/** Full width textarea */
|
|
fullWidth?: boolean;
|
|
/** Number of visible rows */
|
|
rows?: number;
|
|
/** Custom className */
|
|
className?: string;
|
|
}
|
|
|
|
const TextareaWrapper = styled.div<{ $fullWidth: boolean }>`
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: ${({ $fullWidth }) => ($fullWidth ? '100%' : 'auto')};
|
|
margin-bottom: ${(props: { theme: ThemeInterface }) => props.theme.spacing.md};
|
|
`;
|
|
|
|
const Label = styled.label`
|
|
font-family: ${(props: { theme: ThemeInterface }) => props.theme.typography.fontFamily.body};
|
|
font-size: ${(props: { theme: ThemeInterface }) => props.theme.typography.fontSize.sm};
|
|
font-weight: ${(props: { theme: ThemeInterface }) => props.theme.typography.fontWeight.medium};
|
|
color: ${(props: { theme: ThemeInterface }) => props.theme.colors.text.primary};
|
|
margin-bottom: ${(props: { theme: ThemeInterface }) => props.theme.spacing.sm};
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
`;
|
|
|
|
const StyledTextarea = styled.textarea<{ $hasError: boolean }>`
|
|
font-family: ${(props: { theme: ThemeInterface }) => props.theme.typography.fontFamily.body};
|
|
font-size: ${(props: { theme: ThemeInterface }) => props.theme.typography.fontSize.base};
|
|
padding: ${(props: { theme: ThemeInterface }) => props.theme.spacing.md} ${(props: { theme: ThemeInterface }) => props.theme.spacing.md};
|
|
border: 2px solid
|
|
${(props: { theme: ThemeInterface; $hasError: boolean }) => (props.$hasError ? props.theme.colors.error : props.theme.colors.border)};
|
|
border-radius: ${(props: { theme: ThemeInterface }) => props.theme.borderRadius.md};
|
|
background-color: ${(props: { theme: ThemeInterface }) => props.theme.colors.surface};
|
|
color: ${(props: { theme: ThemeInterface }) => props.theme.colors.text.primary};
|
|
transition: all ${(props: { theme: ThemeInterface }) => props.theme.transitions.normal};
|
|
outline: none;
|
|
resize: vertical;
|
|
min-height: 120px;
|
|
|
|
&::placeholder {
|
|
color: ${(props: { theme: ThemeInterface }) => props.theme.colors.text.muted};
|
|
}
|
|
|
|
&:hover:not(:disabled) {
|
|
border-color: ${(props: { theme: ThemeInterface; $hasError: boolean }) =>
|
|
props.$hasError ? props.theme.colors.error : props.theme.colors.primary};
|
|
}
|
|
|
|
&:focus {
|
|
border-color: ${(props: { theme: ThemeInterface; $hasError: boolean }) =>
|
|
props.$hasError ? props.theme.colors.error : props.theme.colors.primary};
|
|
box-shadow: 0 0 0 3px
|
|
${(props: { theme: ThemeInterface; $hasError: boolean }) =>
|
|
props.$hasError ? `${props.theme.colors.error}20` : `${props.theme.colors.primary}20`};
|
|
|
|
${(props: { theme: ThemeInterface; $hasError: boolean }) =>
|
|
props.theme.extensions?.cyberpunk &&
|
|
!props.$hasError &&
|
|
css`
|
|
box-shadow: ${props.theme.extensions.cyberpunk.neonGlow.magenta};
|
|
`}
|
|
}
|
|
|
|
&:disabled {
|
|
background-color: ${(props: { theme: ThemeInterface }) => props.theme.colors.disabled.background};
|
|
color: ${(props: { theme: ThemeInterface }) => props.theme.colors.disabled.text};
|
|
cursor: not-allowed;
|
|
opacity: 0.6;
|
|
resize: none;
|
|
}
|
|
`;
|
|
|
|
const ErrorMessage = styled.span`
|
|
font-family: ${(props: { theme: ThemeInterface }) => props.theme.typography.fontFamily.body};
|
|
font-size: ${(props: { theme: ThemeInterface }) => props.theme.typography.fontSize.sm};
|
|
color: ${(props: { theme: ThemeInterface }) => props.theme.colors.error};
|
|
margin-top: ${(props: { theme: ThemeInterface }) => props.theme.spacing.sm};
|
|
`;
|
|
|
|
export const Textarea = ({
|
|
label,
|
|
error,
|
|
fullWidth = false,
|
|
rows = 5,
|
|
className,
|
|
id,
|
|
...props
|
|
}: TextareaProps) => {
|
|
const textareaId = id || label?.toLowerCase().replace(/\s+/g, '-');
|
|
|
|
return (
|
|
<TextareaWrapper $fullWidth={fullWidth} className={className}>
|
|
{label && <Label htmlFor={textareaId}>{label}</Label>}
|
|
<StyledTextarea $hasError={!!error} id={textareaId} rows={rows} {...props} />
|
|
{error && <ErrorMessage role="alert">{error}</ErrorMessage>}
|
|
</TextareaWrapper>
|
|
);
|
|
};
|