A newer, more generic sibling of Form — not tied to react-hook-form. Used in 3 files on the source platform (newsletter/consult), vs. Form's 40+.
npx chandra-asri-ui add field<FieldSet>
<FieldGroup>
<Field>
<FieldLabel htmlFor="name">Name</FieldLabel>
<FieldContent>
<Input id="name" placeholder="Ada Lovelace" />
<FieldDescription>Shown next to your comments.</FieldDescription>
</FieldContent>
</Field>
<FieldSeparator>or</FieldSeparator>
<Field orientation="horizontal">
<Checkbox id="newsletter" />
<FieldTitle>Subscribe to the newsletter</FieldTitle>
</Field>
</FieldGroup>
</FieldSet>| Aspect | Form | Field |
|---|---|---|
| Dependency | react-hook-form | None — generic |
| Orientation | Always vertical | vertical / horizontal / responsive |
| Usage on source platform | Dominant (40+ files) | Limited (3 files) |
import { Label } from "@/app/components/ui/label";
import { cn } from "@/lib/utils";
import type { ComponentProps, HTMLAttributes, ReactNode } from "react";
/**
* The newer, more generic sibling of Form — not tied to react-hook-form,
* used for standalone layouts (newsletter/consult forms on the source
* platform) rather than the dominant 40+-file react-hook-form pattern.
* Container-query responsiveness on FieldGroup is simplified to a plain
* flex column here to avoid pulling in a container-query setup for one
* component.
*/
export function FieldSet({
className,
...props
}: HTMLAttributes<HTMLFieldSetElement>) {
return (
<fieldset className={cn("flex flex-col gap-6", className)} {...props} />
);
}
export function FieldLegend({
className,
variant = "legend",
...props
}: HTMLAttributes<HTMLLegendElement> & { variant?: "legend" | "label" }) {
return (
<legend
className={cn(
"mb-3 font-medium",
variant === "legend" ? "text-base" : "text-sm",
className,
)}
{...props}
/>
);
}
export function FieldGroup({
className,
...props
}: HTMLAttributes<HTMLDivElement>) {
return (
<div className={cn("flex w-full flex-col gap-7", className)} {...props} />
);
}
type FieldOrientation = "vertical" | "horizontal" | "responsive";
export function Field({
className,
orientation = "vertical",
"data-invalid": dataInvalid,
...props
}: HTMLAttributes<HTMLDivElement> & {
orientation?: FieldOrientation;
"data-invalid"?: boolean;
}) {
return (
<div
data-invalid={dataInvalid}
className={cn(
"flex w-full gap-3",
orientation === "vertical" && "flex-col",
orientation === "horizontal" && "flex-row items-center",
orientation === "responsive" && "flex-col sm:flex-row sm:items-center",
dataInvalid && "text-danger-70",
className,
)}
{...props}
/>
);
}
export function FieldContent({
className,
...props
}: HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("flex flex-1 flex-col gap-1.5 leading-snug", className)}
{...props}
/>
);
}
export function FieldLabel({
className,
...props
}: ComponentProps<typeof Label>) {
return (
<Label
className={cn(
"has-[[data-state=checked]]:border-primary-70 has-[[data-state=checked]]:bg-primary-70/5",
className,
)}
{...props}
/>
);
}
export function FieldTitle({
className,
...props
}: HTMLAttributes<HTMLSpanElement>) {
return (
<span
className={cn(
"flex w-fit items-center gap-2 text-sm leading-snug font-medium",
className,
)}
{...props}
/>
);
}
export function FieldDescription({
className,
...props
}: HTMLAttributes<HTMLParagraphElement>) {
return (
<p
className={cn(
"text-muted [&_a]:text-accent text-sm leading-normal font-normal [&_a]:underline",
className,
)}
{...props}
/>
);
}
export function FieldSeparator({
className,
children,
}: {
className?: string;
children?: ReactNode;
}) {
return (
<div className={cn("relative -my-2 h-5 text-sm", className)}>
<div className="absolute inset-0 flex items-center">
<div className="border-border w-full border-t" />
</div>
{children && (
<div className="relative flex justify-center">
<span className="bg-background text-muted px-2">{children}</span>
</div>
)}
</div>
);
}
export function FieldError({
className,
errors,
children,
...props
}: HTMLAttributes<HTMLDivElement> & { errors?: { message?: string }[] }) {
const messages = errors?.map((error) => error.message).filter(Boolean);
if (!messages?.length && !children) return null;
return (
<div
role="alert"
className={cn("text-danger-70 text-sm font-normal", className)}
{...props}
>
{messages && messages.length > 1 ? (
<ul className="ml-4 list-disc">
{messages.map((message) => (
<li key={message}>{message}</li>
))}
</ul>
) : (
(messages?.[0] ?? children)
)}
</div>
);
}