Base app architecture:
Route groups for customer, staff and owner.
* Reusable design system with around 20 UI components.
* Complete Prisma schema.
* Authentication with email/password and Google.
* Role and permission model.
* Protected routes.
* Clean separation between features, services, components and shared utilities.
58 lines
1.8 KiB
TypeScript
58 lines
1.8 KiB
TypeScript
import { forwardRef, useId } from "react";
|
|
import type { TextareaHTMLAttributes } from "react";
|
|
import { cn } from "@/lib/utils/cn";
|
|
|
|
export interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
label?: string;
|
|
error?: string;
|
|
hint?: string;
|
|
}
|
|
|
|
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
({ className, label, error, hint, id, required, rows = 4, ...props }, ref) => {
|
|
const generatedId = useId();
|
|
const textareaId = id ?? generatedId;
|
|
const hintId = hint ? `${textareaId}-hint` : undefined;
|
|
const errorId = error ? `${textareaId}-error` : undefined;
|
|
|
|
return (
|
|
<div className="w-full">
|
|
{label && (
|
|
<label
|
|
htmlFor={textareaId}
|
|
className="mb-2 ml-1 block text-[10px] font-bold uppercase tracking-widest text-muted"
|
|
>
|
|
{label}
|
|
{required && <span aria-hidden="true"> *</span>}
|
|
</label>
|
|
)}
|
|
<textarea
|
|
ref={ref}
|
|
id={textareaId}
|
|
rows={rows}
|
|
required={required}
|
|
aria-invalid={!!error || undefined}
|
|
aria-describedby={cn(hintId, errorId) || undefined}
|
|
className={cn(
|
|
"w-full resize-none rounded-xl border border-line bg-white p-4 text-sm outline-none transition-all placeholder:text-muted focus:ring-2 focus:ring-ink",
|
|
error && "border-red-300 focus:ring-red-400",
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
{hint && !error && (
|
|
<p id={hintId} className="mt-1.5 ml-1 text-xs text-muted">
|
|
{hint}
|
|
</p>
|
|
)}
|
|
{error && (
|
|
<p id={errorId} role="alert" className="mt-1.5 ml-1 text-xs font-medium text-red-500">
|
|
{error}
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
},
|
|
);
|
|
Textarea.displayName = "Textarea";
|