Audit short-term: currency preview, date bounds, success destination, safe-area

- H3: Live currency preview below currency inputs shows the value formatted
  with thousands separators (en-US, USD) using Intl.NumberFormat. Skipped
  inside matrix cells to keep the Y1 monthly table compact.
- M1: Date inputs now apply min/max bounds. Default window is 1900-01-01 to
  2100-12-31; per-field override via FieldConfig.min/max as ISO strings.
- H6: On successful submit, replace the form with a SuccessDestination card
  (large checkmark, org name, "Submit another" + "safe to close" affordance).
  Prevents accidental duplicate submits from back-button / autofill replay.
- M6: Sticky submit bar respects iOS safe-area-inset-bottom.

FieldRenderer now takes a control prop so the currency preview can subscribe
to its single field via useWatch without re-rendering the whole form.
This commit is contained in:
Joel Brock
2026-05-11 09:48:03 -07:00
parent 0d84b9654b
commit a804650f65
4 changed files with 144 additions and 14 deletions

View File

@@ -182,6 +182,21 @@ export function EngagementForm({ config, cid, cs }: EngagementFormProps) {
if (load.kind === "loading") return <LoadingState />;
if (load.kind === "error") return <ErrorState message={load.message} />;
// After a successful submit, replace the form entirely with a destination
// screen. This prevents accidental duplicate submits (back-button, double-
// click, browser autofill replay) and gives the user a clear endpoint.
if (submitState.kind === "success") {
return (
<SuccessDestination
orgName={load.data.orgName}
onAnother={() => {
setSubmitState({ kind: "idle" });
window.scrollTo({ top: 0, behavior: "smooth" });
}}
/>
);
}
const onSubmit = async (values: Record<string, unknown>) => {
setSubmitState({ kind: "submitting" });
try {
@@ -283,6 +298,7 @@ export function EngagementForm({ config, cid, cs }: EngagementFormProps) {
<StageSection
section={section}
register={register}
control={control}
errors={errors}
formValues={evalState}
isCurrent={section.rank === currentRank}
@@ -392,7 +408,10 @@ function SubmitBar({
draftSavedAt: string | null;
}) {
return (
<div className="sticky bottom-3 z-20 flex flex-col gap-3 rounded-lg border border-rule bg-paper/95 px-5 py-4 shadow-[0_-4px_24px_-12px_rgba(60,80,40,0.2)] backdrop-blur sm:flex-row sm:items-center sm:justify-between sm:px-6">
<div
className="sticky bottom-3 z-20 flex flex-col gap-3 rounded-lg border border-rule bg-paper/95 px-5 py-4 shadow-[0_-4px_24px_-12px_rgba(60,80,40,0.2)] backdrop-blur sm:flex-row sm:items-center sm:justify-between sm:px-6"
style={{ marginBottom: "env(safe-area-inset-bottom)" }}
>
<div className="flex flex-col gap-0.5">
<SubmitFeedback state={state} />
{state.kind === "idle" && (
@@ -429,12 +448,6 @@ function SubmitFeedback({ state }: { state: SubmitStatus }) {
Saving your check-in
</p>
);
if (state.kind === "success")
return (
<p className="text-sm font-medium text-leaf-800" role="status">
Check-in saved. Thank you.
</p>
);
if (state.kind === "error")
return (
<p className="text-sm font-medium text-clay-700" role="alert">
@@ -471,6 +484,49 @@ function LoadingState() {
);
}
function SuccessDestination({
orgName,
onAnother,
}: {
orgName: string;
onAnother: () => void;
}) {
return (
<section
role="status"
aria-live="polite"
className="rounded-lg border-2 border-leaf-600 bg-leaf-50/40 px-6 py-10 text-center shadow-[0_1px_0_0_rgba(0,0,0,0.04),0_12px_32px_-16px_rgba(60,80,40,0.22)] sm:px-10 sm:py-12"
>
<div
aria-hidden
className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-leaf-700 text-paper shadow-sm"
>
<svg viewBox="0 0 24 24" className="h-7 w-7" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M5 12l5 5 9-11" />
</svg>
</div>
<h2 className="mt-5 font-display text-2xl font-medium leading-tight tracking-tight text-ink sm:text-3xl">
Check-in saved
</h2>
<p className="mx-auto mt-3 max-w-prose text-[15px] leading-relaxed text-ink-soft">
Thank you. We&apos;ve recorded this check-in for{" "}
<span className="font-medium text-ink">{orgName}</span>. Your engagement coordinator will see
it on their next review.
</p>
<div className="mt-7 flex flex-col items-center justify-center gap-3 sm:flex-row">
<button
type="button"
onClick={onAnother}
className="inline-flex items-center justify-center rounded-md border border-rule bg-paper px-5 py-2 text-sm font-medium text-ink-soft transition hover:bg-paper-2/60 focus:outline-none focus-visible:ring-2 focus-visible:ring-leaf-500/40"
>
Submit another check-in
</button>
<p className="text-xs text-ink-mute">It&apos;s safe to close this window.</p>
</div>
</section>
);
}
function ErrorState({ message }: { message: string }) {
return (
<div