Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
# Add @suaveplan/react-auth — forms (Wave 2)
**Package**: `@suaveplan/react-auth`
**Status**: PROPOSED
**Type**: feature
- **Spec delta**: ./specs/react-auth/spec.md
## Why
Wave 1 shipped the `@suaveplan/react-auth` core (client, provider, context, hooks, guard, translations data) but explicitly deferred the form components to Wave 2. Downstream apps need shipping login/register/forgot-password/reset-password/email-verification surfaces to wire up the Better Auth flow. Wave 2 layers the five pure-UI form components on top of Wave 1's core, consuming the existing `enGb`/`cyGb`/`zhCn` translation packs (which Wave 1 sized for this exact purpose) and `@suaveplan/react-ui` primitives. No Wave 1 surface is modified — these forms are purely additive. The upstream enterprise audit (`../suaveplan/enterprise/react-auth.md` §2.32, §3.4, §5.5) called out the absence of `.test.tsx` coverage for these five components as the dominant test gap in the package; Wave 2 closes that gap by shipping 100% covered, accessible, schema-validated forms.
## What changes
- ADDED: `LoginForm` — pure UI form with email/password/remember-me, password show/hide, react-hook-form + Zod, "forgot password?" + "sign up" links.
- ADDED: `RegisterForm` — pure UI form with email/username/password+confirm/optional name/dob/sex/gender/company, password show/hide on both fields, react-hook-form + Zod with cross-field `confirmPassword` refinement.
- ADDED: `ForgotPasswordForm` — pure UI form with email field, success view (with "send another" + "back to login"), react-hook-form + Zod.
- ADDED: `ResetPasswordForm` — pure UI form with new password + confirm, success view, invalid-token view, react-hook-form + Zod with cross-field refinement.
- ADDED: `EmailVerificationView` — pure UI status view rendering `verifying` / `success` / `expired` / `error` states with appropriate icon, copy, CTA, and `back-to-login` link.
- ADDED: Form-scoped subpath exports under `./components/<form>` for every form module, mirroring Wave 1's per-component subpath convention.
- ADDED: New translation keys consumed by the forms — every form accepts an optional `translations?: Partial<AuthLoginStrings | …>` prop defaulting to the Wave 1 `enGb.auth.<group>` table so existing `cyGb` / `zhCn` packs work end-to-end with zero new strings (the Wave 1 packs were sized to cover Wave 2 form copy).
## Out of scope
- Page components (`Login`, `Register`, `ForgotPassword`, `ResetPassword`, `EmailVerify`, `Profile`) — deferred to Wave 3.
- `PermissionGate` component — deferred to Wave 3.
- MFA / passkey / magic-link flows — enterprise audit §4 enhancements, not in any current wave.
### Excluded from this migration
- **Page components** (`pages/login`, `pages/register`, `pages/forgot-password`, `pages/reset-password`, `pages/email-verification`, `pages/profile`) — Deferred to Wave 3 — scope-bounded migration to keep ship velocity at 100/100/100/100 quality. Pages wrap the Wave 2 forms with `useAuth` action wiring + `useNavigate` redirects.
- **`PermissionGate` component** — Deferred to Wave 3 — scope-bounded migration; ships next to the Wave 3 pages because the page surfaces are its primary consumer.
- **MFA / passkey / magic-link surfaces** (`MfaChallengePage`, `MfaEnrolmentPage`, `PasskeyRegistrationPage`, `PasskeyLoginPage`) — Enterprise audit §4 enhancements, never part of upstream — out of scope for every current wave.
## Source mapping
This change derives from `../suaveplan/enterprise/react-auth.md` (§1, §2.25, §2.30–32, §3.4, §5.5) and `../suaveplan/packages/react/react-auth/src/components/`.
| Requirement | Source lines | Notes |
|---|---|---|
| `LoginForm` shape | `components/login-form/login-form.tsx:1-210` | Verbatim semantics; `resolver: zodResolver(...) as any` cast removed via typed resolver narrowing — no `any` survives §2. |
| `RegisterForm` shape | `components/register-form/register-form.tsx:1-366` | Verbatim semantics; same `any` removal; split into `register-form.tsx` + `register-form-fields.tsx` to stay under the 300-line hard cap (§3). |
| `ForgotPasswordForm` shape | `components/forgot-password-form/forgot-password-form.tsx:1-190` | Verbatim semantics; same `any` removal. |
| `ResetPasswordForm` shape | `components/reset-password-form/reset-password-form.tsx:1-277` | Verbatim semantics; same `any` removal. |
| `EmailVerificationView` shape | `components/email-verification-view/email-verification-view.tsx:1-220` | Reworded: upstream consumed `useTranslation` from `@suaveplan/react-translate`; Wave 1 settled this package as no-react-translate (`enterprise/react-auth.md §5.5` migration impact note in Wave 1 proposal). Wave 2 follows that precedent — the view accepts a `translations?: Partial<AuthEmailVerificationStrings>` prop defaulting to `enGb.auth.emailVerification`. Callers wanting `cyGb` / `zhCn` pass that pack's slice. |
| 5 sibling `.test.tsx` files | `enterprise/react-auth.md §3.4 / §5.5` | Upstream gap — these tests do not exist in upstream and are added in this wave per the audit. |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| Zod-validated `<FormName>Props` boundary on every form | Genesis rule §0a — Zod boundary on every public option bundle. Upstream had `Props` interfaces but no runtime validation. |
| `react-hook-form` typed resolver, no `as any` cast | Genesis rule §2 — `any` is banned. Upstream's `resolver: zodResolver(schema) as any` is replaced with a typed wrapper. |
| 100% statement / branch / function / line coverage | Genesis-wide policy, not package-specific. |
| Sibling `<form>.md` per implementation file | Genesis rule §4. |
| Sibling `<form>.stories.tsx` per component | CLAUDE.md: "Every React component needs `.stories.tsx`." Upstream ships stories for pages / guards but not for these forms. |
| `translations?: Partial<...>` prop on every form, defaulting to `enGb.auth.<group>` | Genesis-specific — Wave 1 dropped `@suaveplan/react-translate` and parked translations as static packs. Wave 2 wires those packs in via prop injection to preserve the three-locale surface without re-introducing the dropped dep. |
## Verification approach
Every form gets a sibling `.test.tsx` that uses `@suaveplan/testing/runner` and `@testing-library/react` with real `react-hook-form` + real Zod. No mocks. Each test file covers:
- `src/components/login-form/login-form.test.tsx` — submit happy path, Zod validation errors (empty email, short password, bad email), password show/hide toggle, `error` prop renders alert, `isLoading` disables submit, `translations` prop overrides English defaults.
- `src/components/register-form/register-form.test.tsx` — same matrix + username regex, `confirmPassword` mismatch, optional field flow, sex select changes.
- `src/components/forgot-password-form/forgot-password-form.test.tsx` — submit happy path, Zod email error, `isSuccess` view renders email + `onReset` button, `error` alert, `translations` override.
- `src/components/reset-password-form/reset-password-form.test.tsx` — submit happy path, short password Zod error, `confirmPassword` mismatch, `isSuccess` view, `hasToken=false` invalid-token view with `onNavigateToForgotPassword` CTA, password toggle, `translations` override.
- `src/components/email-verification-view/email-verification-view.test.tsx` — `verifying` / `success` / `error` / `expired` branches, custom `message` overrides default copy, `onTryAgain` fires on error CTA, `isLoading` disables retry, `translations` override.
- `src/index.test.ts` — extended to assert the 5 new symbols (`LoginForm`, `RegisterForm`, `ForgotPasswordForm`, `ResetPasswordForm`, `EmailVerificationView`) appear in the root barrel.
## Migration impact
- `package.json` gains three new dependencies — `@suaveplan/react-ui` (`workspace:^`), `react-hook-form` and `@hookform/resolvers` (both `catalog:forms`). The five form subpath exports are appended to the `exports` block. Vite `lib.entry` gains five new entries; rollup `external` gains `@suaveplan/react-ui`, `react-hook-form`, `@hookform/resolvers`.
- No Wave 1 file is modified except `src/index.ts` (named re-exports for the 5 new components added — strictly additive) and `src/index.test.ts` (asserts the new symbols are present).
- No downstream consumers yet — `@suaveplan/react-auth-whitelabel` is the only sibling and it ships independently in parallel. Verified by `bunx turbo ls --filter=...@suaveplan/react-auth` (empty list).

View File

@@ -0,0 +1,148 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: react-auth (add-react-auth-forms)
**Package**: `@suaveplan/react-auth`
**Status**: PROPOSED
> Every requirement carries a `**Verified by:**` line authored at draft time.
## ADDED Requirements
### Requirement: LoginForm renders a Zod-validated sign-in form with email, password, and remember-me
The package SHALL expose a `LoginForm` component that renders a card-layout sign-in form with email + password + remember-me fields. The form SHALL validate via `react-hook-form` + `zodResolver` against an internal `loginSchema` requiring email to be a non-empty valid email and password to be at least 8 characters. Submit SHALL invoke the caller-supplied `onSubmit({ email, password, rememberMe })`. The component SHALL render a destructive `Alert` when `error` is non-null, disable the submit button when `isLoading` or `formState.isSubmitting` is true, and accept a `translations` prop overriding the default `enGb.auth.login` strings.
#### Scenario: invalid email shows Zod error on blur
- **WHEN** the user types `not-an-email` into the email field and blurs out
- **THEN** the text "Invalid email address" appears
#### Scenario: short password rejected
- **WHEN** the user submits with password `"abc"`
- **THEN** the text "Password must be at least 8 characters" appears and `onSubmit` is NOT called
#### Scenario: happy path submit
- **WHEN** the user fills `alice@example.com` + `correcthorsebatterystaple` and clicks Sign in
- **THEN** `onSubmit({ email: "alice@example.com", password: "correcthorsebatterystaple", rememberMe: true })` is called
#### Scenario: error prop renders alert
- **WHEN** `error={new Error("Invalid credentials")}` is passed
- **THEN** an alert containing "Invalid credentials" is in the document
#### Scenario: translations prop overrides defaults
- **WHEN** `translations={{ submit: "Connexion" }}` is passed
- **THEN** the submit button displays "Connexion"
**Verified by:** `src/components/login-form/login-form.test.tsx::"shows Zod error when email is invalid"` AND `src/components/login-form/login-form.test.tsx::"shows Zod error when password is shorter than 8 characters"` AND `src/components/login-form/login-form.test.tsx::"calls onSubmit with the typed payload on happy path"` AND `src/components/login-form/login-form.test.tsx::"renders the error prop in a destructive Alert"` AND `src/components/login-form/login-form.test.tsx::"overrides the submit copy when translations prop is supplied"`
---
### Requirement: RegisterForm renders a Zod-validated sign-up form with cross-field password match
The package SHALL expose a `RegisterForm` component rendering email, username, password, confirm-password, optional first-name, surname, date-of-birth, sex (4-option select), gender, company fields. The Zod schema SHALL enforce email validity, username `^[a-zA-Z0-9_]+$` with length 3–30, password ≥ 8 chars, and a `confirmPassword` cross-field refinement matching `password`. Submit SHALL fire only when validation passes. The component SHALL accept `error`, `isLoading`, and `translations` props mirroring `LoginForm` semantics.
#### Scenario: confirmPassword mismatch blocks submit
- **WHEN** password `"longenough1"` and confirmPassword `"longenough2"` are submitted
- **THEN** "Passwords don't match" appears under the confirm field and `onSubmit` is NOT called
#### Scenario: username regex enforced
- **WHEN** username `"has space"` is submitted
- **THEN** the Zod regex error appears and `onSubmit` is NOT called
#### Scenario: happy path submit
- **WHEN** every required field is valid and the form is submitted
- **THEN** `onSubmit` receives the full typed payload including the default `sex: "prefer-not-to-say"`
**Verified by:** `src/components/register-form/register-form.test.tsx::"blocks submit when passwords do not match"` AND `src/components/register-form/register-form.test.tsx::"rejects usernames that do not match the regex"` AND `src/components/register-form/register-form.test.tsx::"calls onSubmit with the typed payload on happy path"` AND `src/components/register-form/register-form.test.tsx::"renders the error prop in a destructive Alert"`
---
### Requirement: ForgotPasswordForm renders an email-only Zod-validated form with success view
The package SHALL expose a `ForgotPasswordForm` component rendering an email-only form. When `isSuccess` is true the component SHALL switch to a confirmation card displaying `emailSent`, a "send another" button wired to `onReset`, and a "back to login" link. Otherwise it SHALL render the email form with Zod validation, a destructive alert when `error` is non-null, and a submit button disabled while `isLoading` or `isSubmitting`. A `translations` prop SHALL override `enGb.auth.forgotPassword` defaults.
#### Scenario: invalid email blocked
- **WHEN** the user submits "not-email"
- **THEN** "Invalid email address" appears and `onSubmit` is NOT called
#### Scenario: success view rendered
- **WHEN** `isSuccess` is true and `emailSent="alice@example.com"`
- **THEN** the document contains "alice@example.com" and a button whose click calls `onReset`
**Verified by:** `src/components/forgot-password-form/forgot-password-form.test.tsx::"blocks submit when email is invalid"` AND `src/components/forgot-password-form/forgot-password-form.test.tsx::"calls onSubmit with the typed payload on happy path"` AND `src/components/forgot-password-form/forgot-password-form.test.tsx::"renders the success view with emailSent and a working onReset button"` AND `src/components/forgot-password-form/forgot-password-form.test.tsx::"renders the error prop in a destructive Alert"`
---
### Requirement: ResetPasswordForm renders new+confirm password with success and invalid-token views
The package SHALL expose a `ResetPasswordForm` component supporting three view modes: form (default), success (`isSuccess=true`), and invalid token (`hasToken=false`). The schema SHALL require both `password` and `confirmPassword` ≥ 8 chars with a cross-field match refinement. The success view SHALL render a "sign in" CTA wired to `onNavigateToLogin`; the invalid-token view SHALL render a "request new link" CTA wired to `onNavigateToForgotPassword`. The form view SHALL accept `error`, `isLoading`, and `translations` props.
#### Scenario: password mismatch blocks submit
- **WHEN** password `"longenough1"` and confirm `"longenough2"` are submitted
- **THEN** "Passwords don't match" appears and `onSubmit` is NOT called
#### Scenario: success view renders CTA
- **WHEN** `isSuccess=true`
- **THEN** clicking the Sign in CTA calls `onNavigateToLogin`
#### Scenario: invalid token view renders CTA
- **WHEN** `hasToken=false`
- **THEN** clicking the Request new reset link CTA calls `onNavigateToForgotPassword`
**Verified by:** `src/components/reset-password-form/reset-password-form.test.tsx::"blocks submit when passwords do not match"` AND `src/components/reset-password-form/reset-password-form.test.tsx::"calls onSubmit with the typed payload on happy path"` AND `src/components/reset-password-form/reset-password-form.test.tsx::"renders the success view with a working onNavigateToLogin CTA"` AND `src/components/reset-password-form/reset-password-form.test.tsx::"renders the invalid-token view with a working onNavigateToForgotPassword CTA"`
---
### Requirement: EmailVerificationView renders four status modes with caller-supplied translations
The package SHALL expose an `EmailVerificationView` component accepting `status: "verifying" | "success" | "expired" | "error"`. Each status SHALL render a Card with an icon, title, description, and (where applicable) a CTA wired to one of `onContinueToLogin` / `onRegisterAgain` / `onTryAgain`. The `error` status SHALL render a destructive Alert; the `error` retry CTA SHALL be disabled while `isLoading`. A `translations` prop SHALL override `enGb.auth.emailVerification` defaults; an optional `message` prop SHALL override the default status description.
#### Scenario: verifying state renders spinner and copy
- **WHEN** `status="verifying"`
- **THEN** a `role="status"` spinner is present and the default English "Verifying email..." copy is visible
#### Scenario: success CTA fires onContinueToLogin
- **WHEN** `status="success"` and the CTA is clicked
- **THEN** `onContinueToLogin` is called once
#### Scenario: error CTA fires onTryAgain and respects isLoading
- **WHEN** `status="error"` and `isLoading=false`
- **THEN** the retry CTA is enabled and clicking it calls `onTryAgain`
#### Scenario: expired CTA fires onRegisterAgain
- **WHEN** `status="expired"` and the CTA is clicked
- **THEN** `onRegisterAgain` is called once
**Verified by:** `src/components/email-verification-view/email-verification-view.test.tsx::"renders the verifying status with a spinner"` AND `src/components/email-verification-view/email-verification-view.test.tsx::"fires onContinueToLogin from the success CTA"` AND `src/components/email-verification-view/email-verification-view.test.tsx::"fires onTryAgain from the error CTA"` AND `src/components/email-verification-view/email-verification-view.test.tsx::"fires onRegisterAgain from the expired CTA"` AND `src/components/email-verification-view/email-verification-view.test.tsx::"overrides the default copy when translations prop is supplied"`
---
### Requirement: Form components are reachable from the root barrel
The `@suaveplan/react-auth` root entry SHALL re-export the 5 Wave 2 form components as named exports (`LoginForm`, `RegisterForm`, `ForgotPasswordForm`, `ResetPasswordForm`, `EmailVerificationView`) with no `export *`. Each form SHALL also be reachable via a dedicated subpath export (`./components/login-form`, etc.) for tree-shaking.
#### Scenario: root barrel surface
- **WHEN** the root-barrel test enumerates `Object.keys(import("@suaveplan/react-auth"))`
- **THEN** every Wave 2 form symbol is present alongside the Wave 1 surface
**Verified by:** `src/index.test.ts::"the root barrel exports exactly the documented public surface"`
<!-- markdownlint-enable MD024 -->

View File

@@ -0,0 +1,66 @@
# Tasks — Add @suaveplan/react-auth — forms (Wave 2)
> No retroactive ticks. Each box reflects work done in this change.
## 1. Spec authoring
- [ ] 1.1 Read `../suaveplan/enterprise/react-auth.md` end-to-end
- [ ] 1.2 Read `openspec/specs/react-auth/spec.md` (Wave 1 canonical)
- [ ] 1.3 Read `openspec/AGENTS.md` §§ 0c, 2a, 6a, 7, 12, 13, 14
- [ ] 1.4 Draft `proposal.md` from template with `## Source mapping` + `### Excluded`
- [ ] 1.5 Draft `specs/react-auth/spec.md` delta with `## ADDED Requirements`, one per form
- [ ] 1.6 Every requirement carries a `**Verified by:**` line at draft time
## 2. Implementation
- [ ] 2.1 Add `@suaveplan/react-ui` + `react-hook-form` + `@hookform/resolvers` deps to `package.json`
- [ ] 2.2 Add 5 subpath exports to `package.json` `exports` block
- [ ] 2.3 Extend `vite.config.ts` `lib.entry` with 5 new entries; extend `rollupOptions.external`
- [ ] 2.4 Ship `src/components/login-form/` (`login-form.tsx`, `.types.ts`, `.md`, `.stories.tsx`, `index.ts`)
- [ ] 2.5 Ship `src/components/register-form/` (split into `register-form.tsx` + `register-form-fields.tsx` to honour the 300-line cap)
- [ ] 2.6 Ship `src/components/forgot-password-form/`
- [ ] 2.7 Ship `src/components/reset-password-form/`
- [ ] 2.8 Ship `src/components/email-verification-view/`
- [ ] 2.9 Add `src/components/index.ts` barrel with explicit named re-exports for every form symbol
- [ ] 2.10 Extend `src/index.ts` with the 5 new components (named re-exports only)
- [ ] 2.11 Extend `src/index.test.ts` expected-keys list with the 5 new symbols
## 3. Tests
- [ ] 3.1 `src/components/login-form/login-form.test.tsx` — Zod errors, happy path, password toggle, `error`, `isLoading`, `translations` override
- [ ] 3.2 `src/components/register-form/register-form.test.tsx` — same matrix + username regex + `confirmPassword` refinement + sex select
- [ ] 3.3 `src/components/forgot-password-form/forgot-password-form.test.tsx`
- [ ] 3.4 `src/components/reset-password-form/reset-password-form.test.tsx`
- [ ] 3.5 `src/components/email-verification-view/email-verification-view.test.tsx`
- [ ] 3.6 Every `it(...)` carries `{ timeout: 5000 }`
- [ ] 3.7 All tests import from `@suaveplan/testing/runner`
## 4. Documentation
- [ ] 4.1 Sibling `.md` for every form (≥200 words, Basic + Advanced examples, full API reference)
- [ ] 4.2 No README changes (Wave 1's README already lists components/ subpaths generically)
## 5. Pre-archive 5-gate verification
- [ ] 5.1 `bun run check` — Biome clean across the monorepo
- [ ] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/react-auth --force` — green
- [ ] 5.3 `bunx turbo test:coverage --filter=@suaveplan/react-auth` — 100/100/100/100 istanbul
- [ ] 5.4 `bun scripts/openspec-conformance.ts @suaveplan/react-auth` — exit 0
- [ ] 5.5 `bunx turbo build --filter=@suaveplan/react-auth --force` — green
- [ ] 5.6 `bun scripts/lint/check-test-runner-imports.ts` — exit 0
## 6. Pre-archive 7-row checklist
- [ ] 6.1 Generics threaded — `useForm<TForm>` typed end-to-end, no `as any`
- [ ] 6.2 Zod schemas wired at every form's submit boundary
- [ ] 6.3 No new error codes introduced (Wave 1's registry unchanged); Zod parse failures surface through react-hook-form's `formState.errors`
- [ ] 6.4 N/A — no strict-mode behaviour toggle in this wave
- [ ] 6.5 N/A — no new OTel instruments in this wave (forms are pure UI; metrics live on the Wave 1 actions)
- [ ] 6.6 N/A — no lifecycle dedup claims on forms
- [ ] 6.7 No retroactive ticks
## 7. Archive
- [ ] 7.1 Fold spec delta — APPEND new requirements to `openspec/specs/react-auth/spec.md` under `## ADDED Requirements`; do NOT modify Wave 1 requirements
- [ ] 7.2 Move `openspec/changes/add-react-auth-forms/` → `openspec/archive/2026-05-19-add-react-auth-forms/`
- [ ] 7.3 No MANIFEST.md / STATUS.md updates (not a new package)