feat(front): run the auth forms on TanStack Form v2 #543

Merged
root merged 6 commits from claude/spec-410-implementation-000473 into main 2026-10-15 23:25:52 +00:00
root commented 2026-10-15 22:17:29 +00:00 (Migrated from gitlab.core-project.space)

Summary

Every auth form runs on TanStack Form v2 (@tanstack/react-form@2.0.0-alpha.2, pinned exactly). The design-system input, the form's refusal line and its submit button are bound once in lib/form.tsx; each page passes validators and hints only.

 apps/front/src/
 ├── lib/
+│   └── form.tsx              # useAppForm, refuse(); TextField, CheckboxField; Form, FormRefusal, SubmitButton
 └── auth/
     ├── components/
-    │   ├── auth-form.tsx     # AuthForm, AuthPage
-    │   ├── auth-field.tsx    # AuthField
+    │   └── auth-page.tsx     # AuthPage
     ├── credential-checks.ts  # + EMAIL_VALIDATORS, NEW_PASSWORD_VALIDATORS and their hints
     ├── rate-limit.ts         # + limitedFormRefusalOf / limitedFormRefusalText
     ├── sign-up.page.tsx      # on useAppForm; Username check is a 300 ms debounced async validator over Query
     ├── sign-in.page.tsx
     ├── forgot-password.page.tsx
     ├── reset-password.page.tsx
     └── account.page.tsx      # delete dialog's password form
form.Form onSubmit
  handleSubmit
    field validators (our refusal unions, translated at render — ADR 0025)
    onSubmit
      await mutation
      refusal code / rate limit → return createValidationError({ form, fields })   # never thrown (TanStack/form#2306)
  focus first [aria-invalid] field
form.Form unmount → form.reset()                                                  # pending debounce outlives unmount (TanStack/form#2296)

No blur handler on the bound inputs: a blur clears server refusals (TanStack/form#2341).

Player-visible differences, accepted by the owner

The spec put changes a Player sees out of scope; the owner chose to keep the library's behaviour instead of working around it.

  • All forms: after a refused submit, focus goes to the first refused field. While sending, the submit button is aria-disabled and stays focusable (was disabled); it follows the form's isSubmitting, not the mutation's isPending. A second press still sends nothing (tested on every form).
  • Typing clears a refusal in that field, and a form-level line (wrong credentials, rate limit, "something went wrong") on any change. Before, refusals stayed until the next submit. Applies to sign-in, sign-up's email and password, forgot-password, reset-password and, by the same library rule (not tested), the delete dialog. A server-refused Username still clears only when the Player types in Username (story 3).
  • Sign-up: every field refused before sending shows at once (was the first only); an empty Username on submit is marked and focused (was a silent no-op); a failed availability read marks the Username invalid under "Checking…" (was unmarked).
  • Delete dialog: the button no longer dims while sending; the field-to-alert gap is 0.75rem (was 0.5rem).

Evidence

  • Before: sign-up, sign-in, forgot-password, reset-password and account-page tests green on hand-rolled forms.
    After: the same tests green, unchanged in what they assert, plus:
sign-up   refused Username survives typing in email/password        ✓
sign-up   focus moves to the first refused field (it.each)          ✓
sign-up   Username typed and erased before 300 ms → no request      ✓
sign-up   availability for an older value is ignored                ✓
sign-in   typing in Password clears the line, Email stays marked    ✓
each form focus after a refused submit                              ✓
each form second press while sending → one call                     ✓

Gate on the rebased branch, Node 24: lint, typecheck, test (front 1400/1400), build, format:check — all green. accounts.spec (e2e) 3/3.

One sign-up assertion moved from same-tick to awaited ("refuses … by its format at once"): every v2 validator resolves through a promise. Its text is unchanged.

Merge Danger

Door: two-way

Front-only; revert restores the hand-rolled forms. No contract, API or data change.

Blast Radius: auth-forms

Sign-up, sign-in, password reset and Account deletion all go through one alpha library. A new alpha touches lib/form.tsx plus each form's validators; read TanStack/form#2306, #2341, #2296 before bumping.

Closes #410
Closes #418
Closes #419
Closes #420

## Summary Every auth form runs on TanStack Form v2 (`@tanstack/react-form@2.0.0-alpha.2`, pinned exactly). The design-system input, the form's refusal line and its submit button are bound once in `lib/form.tsx`; each page passes validators and hints only. ```diff apps/front/src/ ├── lib/ +│ └── form.tsx # useAppForm, refuse(); TextField, CheckboxField; Form, FormRefusal, SubmitButton └── auth/ ├── components/ - │ ├── auth-form.tsx # AuthForm, AuthPage - │ ├── auth-field.tsx # AuthField + │ └── auth-page.tsx # AuthPage ├── credential-checks.ts # + EMAIL_VALIDATORS, NEW_PASSWORD_VALIDATORS and their hints ├── rate-limit.ts # + limitedFormRefusalOf / limitedFormRefusalText ├── sign-up.page.tsx # on useAppForm; Username check is a 300 ms debounced async validator over Query ├── sign-in.page.tsx ├── forgot-password.page.tsx ├── reset-password.page.tsx └── account.page.tsx # delete dialog's password form ``` ```text form.Form onSubmit handleSubmit field validators (our refusal unions, translated at render — ADR 0025) onSubmit await mutation refusal code / rate limit → return createValidationError({ form, fields }) # never thrown (TanStack/form#2306) focus first [aria-invalid] field form.Form unmount → form.reset() # pending debounce outlives unmount (TanStack/form#2296) ``` No blur handler on the bound inputs: a blur clears server refusals (TanStack/form#2341). ### Player-visible differences, accepted by the owner The spec put changes a Player sees out of scope; the owner chose to keep the library's behaviour instead of working around it. - **All forms:** after a refused submit, focus goes to the first refused field. While sending, the submit button is `aria-disabled` and stays focusable (was `disabled`); it follows the form's `isSubmitting`, not the mutation's `isPending`. A second press still sends nothing (tested on every form). - **Typing clears a refusal** in that field, and a form-level line (wrong credentials, rate limit, "something went wrong") on any change. Before, refusals stayed until the next submit. Applies to sign-in, sign-up's email and password, forgot-password, reset-password and, by the same library rule (not tested), the delete dialog. A server-refused Username still clears only when the Player types in Username (story 3). - **Sign-up:** every field refused before sending shows at once (was the first only); an empty Username on submit is marked and focused (was a silent no-op); a failed availability read marks the Username invalid under "Checking…" (was unmarked). - **Delete dialog:** the button no longer dims while sending; the field-to-alert gap is 0.75rem (was 0.5rem). ## Evidence - **Before:** sign-up, sign-in, forgot-password, reset-password and account-page tests green on hand-rolled forms. **After:** the same tests green, unchanged in what they assert, plus: ```text sign-up refused Username survives typing in email/password ✓ sign-up focus moves to the first refused field (it.each) ✓ sign-up Username typed and erased before 300 ms → no request ✓ sign-up availability for an older value is ignored ✓ sign-in typing in Password clears the line, Email stays marked ✓ each form focus after a refused submit ✓ each form second press while sending → one call ✓ ``` Gate on the rebased branch, Node 24: lint, typecheck, test (front 1400/1400), build, format:check — all green. `accounts.spec` (e2e) 3/3. One sign-up assertion moved from same-tick to awaited ("refuses … by its format at once"): every v2 validator resolves through a promise. Its text is unchanged. ## Merge Danger **Door:** two-way Front-only; revert restores the hand-rolled forms. No contract, API or data change. **Blast Radius:** auth-forms Sign-up, sign-in, password reset and Account deletion all go through one alpha library. A new alpha touches `lib/form.tsx` plus each form's validators; read TanStack/form#2306, #2341, #2296 before bumping. Closes #410 Closes #418 Closes #419 Closes #420
root commented 2026-10-15 22:18:22 +00:00 (Migrated from gitlab.core-project.space)

added 14 commits

  • d866b191...eb0d0b8e - 8 commits from branch main
  • 6b5cc008 - build(deps): pin @tanstack/react-form to 2.0.0-alpha.2
  • 9b6c9095 - feat(front): bind the design-system input to TanStack Form v2 and move sign-up onto it
  • 356cb02e - refactor(front): move the Account delete dialog onto TanStack Form v2
  • 5d056e17 - feat(front): move sign-in, forgot-password and reset-password onto TanStack Form v2
  • 9db40bf3 - test(front): pin the auth forms' debounce, stale answers and second press
  • 817565cb - refactor(front): share the auth forms' email check and limited-form refusal

Compare with previous version

added 14 commits <ul><li>d866b191...eb0d0b8e - 8 commits from branch <code>main</code></li><li>6b5cc008 - build(deps): pin @tanstack/react-form to 2.0.0-alpha.2</li><li>9b6c9095 - feat(front): bind the design-system input to TanStack Form v2 and move sign-up onto it</li><li>356cb02e - refactor(front): move the Account delete dialog onto TanStack Form v2</li><li>5d056e17 - feat(front): move sign-in, forgot-password and reset-password onto TanStack Form v2</li><li>9db40bf3 - test(front): pin the auth forms&#39; debounce, stale answers and second press</li><li>817565cb - refactor(front): share the auth forms&#39; email check and limited-form refusal</li></ul> [Compare with previous version](/amaurycoudr/chess-project/-/merge_requests/43/diffs?diff_id=272&start_sha=d866b19156c95d68bd73f683be2ae98090424d16)
project_1_bot_a8dd9262857d850eef8dca5206e61de5 commented 2026-10-15 22:36:17 +00:00 (Migrated from gitlab.core-project.space)

Screenshots

Before is production, after is this merge request's review app at 40b59e3.

Desktop

Page Before After
Home
Local game
Room

Mobile

Page Before After
Home
Local game
Room
<!-- chess-screenshots --> ### Screenshots Before is production, after is this merge request's review app at 40b59e3. #### Desktop | Page | Before | After | | --- | --- | --- | | Home | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1712/artifacts/raw/review-screenshots/before/home-desktop.png" width="400"> | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1712/artifacts/raw/review-screenshots/after/home-desktop.png" width="400"> | | Local game | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1712/artifacts/raw/review-screenshots/before/local-desktop.png" width="400"> | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1712/artifacts/raw/review-screenshots/after/local-desktop.png" width="400"> | | Room | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1712/artifacts/raw/review-screenshots/before/room-desktop.png" width="400"> | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1712/artifacts/raw/review-screenshots/after/room-desktop.png" width="400"> | #### Mobile | Page | Before | After | | --- | --- | --- | | Home | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1712/artifacts/raw/review-screenshots/before/home-mobile.png" width="200"> | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1712/artifacts/raw/review-screenshots/after/home-mobile.png" width="200"> | | Local game | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1712/artifacts/raw/review-screenshots/before/local-mobile.png" width="200"> | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1712/artifacts/raw/review-screenshots/after/local-mobile.png" width="200"> | | Room | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1712/artifacts/raw/review-screenshots/before/room-mobile.png" width="200"> | <img src="https://gitlab.core-project.space/amaurycoudr/chess-project/-/jobs/1712/artifacts/raw/review-screenshots/after/room-mobile.png" width="200"> |
root commented 2026-10-15 22:52:50 +00:00 (Migrated from gitlab.core-project.space)

added 16 commits

  • 817565cb...ee4cc4e0 - 10 commits from branch main
  • 4430a3c7 - build(deps): pin @tanstack/react-form to 2.0.0-alpha.2
  • 1343f8d2 - feat(front): bind the design-system input to TanStack Form v2 and move sign-up onto it
  • ed624c3d - refactor(front): move the Account delete dialog onto TanStack Form v2
  • 305f8002 - feat(front): move sign-in, forgot-password and reset-password onto TanStack Form v2
  • 489aa43a - test(front): pin the auth forms' debounce, stale answers and second press
  • 5abbcd38 - refactor(front): share the auth forms' email check and limited-form refusal

Compare with previous version

added 16 commits <ul><li>817565cb...ee4cc4e0 - 10 commits from branch <code>main</code></li><li>4430a3c7 - build(deps): pin @tanstack/react-form to 2.0.0-alpha.2</li><li>1343f8d2 - feat(front): bind the design-system input to TanStack Form v2 and move sign-up onto it</li><li>ed624c3d - refactor(front): move the Account delete dialog onto TanStack Form v2</li><li>305f8002 - feat(front): move sign-in, forgot-password and reset-password onto TanStack Form v2</li><li>489aa43a - test(front): pin the auth forms&#39; debounce, stale answers and second press</li><li>5abbcd38 - refactor(front): share the auth forms&#39; email check and limited-form refusal</li></ul> [Compare with previous version](/amaurycoudr/chess-project/-/merge_requests/43/diffs?diff_id=287&start_sha=817565cb229d56e92e3d48ff6d7b4554083436a3)
root commented 2026-10-15 22:54:59 +00:00 (Migrated from gitlab.core-project.space)

added 8 commits

  • 5abbcd38...6c8f1635 - 2 commits from branch main
  • fc2fd8b1 - build(deps): pin @tanstack/react-form to 2.0.0-alpha.2
  • 4fab4200 - feat(front): bind the design-system input to TanStack Form v2 and move sign-up onto it
  • 8a1be3fd - refactor(front): move the Account delete dialog onto TanStack Form v2
  • 35f0c4be - feat(front): move sign-in, forgot-password and reset-password onto TanStack Form v2
  • a44c246a - test(front): pin the auth forms' debounce, stale answers and second press
  • 40b59e31 - refactor(front): share the auth forms' email check and limited-form refusal

Compare with previous version

added 8 commits <ul><li>5abbcd38...6c8f1635 - 2 commits from branch <code>main</code></li><li>fc2fd8b1 - build(deps): pin @tanstack/react-form to 2.0.0-alpha.2</li><li>4fab4200 - feat(front): bind the design-system input to TanStack Form v2 and move sign-up onto it</li><li>8a1be3fd - refactor(front): move the Account delete dialog onto TanStack Form v2</li><li>35f0c4be - feat(front): move sign-in, forgot-password and reset-password onto TanStack Form v2</li><li>a44c246a - test(front): pin the auth forms&#39; debounce, stale answers and second press</li><li>40b59e31 - refactor(front): share the auth forms&#39; email check and limited-form refusal</li></ul> [Compare with previous version](/amaurycoudr/chess-project/-/merge_requests/43/diffs?diff_id=291&start_sha=5abbcd3879580e621a152d16c551a0359cace7be)
root (Migrated from gitlab.core-project.space) merged commit into main 2026-10-15 23:25:52 +00:00
Sign in to join this conversation.
No description provided.