Show a refused profile field under the input it belongs to

The onboarding baseline step flashed a toast naming one refused field
and walked on to the done screen, stamping the account as set up over
a value the server never stored. The value the person typed was gone
with the step.

A refused field now writes its reason into that field's own error
slot, marks the control invalid, and holds the step until the person
corrects it. Every refused field is named, not only the first, and the
reason comes from the validator code rather than the validator's own
prose, in all seven shipped languages.
This commit is contained in:
Marc-André Bombeck committed 2026-09-09 23:21:21 +02:00
1 parent ae20ee3d9d
commit 36cbf48286
16 files changed
+438 -26

No files matched your search

+7
View File
@@ -1,5 +1,6 @@
import type { ReactNode } from "react";
import { FieldError } from "@/components/forms/field-error";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
@@ -10,6 +11,9 @@ import { cn } from "@/lib/utils";
* is a no-op on rendered output.
*
* - `hint` renders below the control as muted fine print.
* - `error` renders below the hint as the field's own error slot, with
* the id `<htmlFor>-error` so the control can point
* `aria-describedby` at it.
* - `labelAccessory` sits right-aligned in the label row (e.g. a char counter);
* when present the label row becomes a `justify-between` flex row.
*/
@@ -17,6 +21,7 @@ export function FieldGroup({
htmlFor,
label,
hint,
error,
labelAccessory,
className,
labelClassName,
@@ -25,6 +30,7 @@ export function FieldGroup({
htmlFor: string;
label: ReactNode;
hint?: ReactNode;
error?: ReactNode;
labelAccessory?: ReactNode;
className?: string;
labelClassName?: string;
@@ -46,6 +52,7 @@ export function FieldGroup({
)}
{children}
{hint ? <p className="text-muted-foreground text-xs">{hint}</p> : null}
<FieldError id={`${htmlFor}-error`} message={error} />
</div>
);
}