# OpenSpec Change: enterprise-react-forms-v2
- **Change ID**: `enterprise-react-forms-v2`
- **Package**: `@suaveplan/react-forms`
- **Category**: react
- **Target path**: `packages/forms/react-forms`
- **Status**: PROPOSED
- **Date**: 2026-05-05
- **Supersedes**: `openspec/changes/enterprise-react-forms/`
- **Master design**: `openspec/cross-cutting-designs/2026-05-05-react-ui-react-i18n-react-forms.md` (§9, §11.2)
- **Prereqs (one SHIPPED, one in-flight)**:
- `enterprise-react-i18n` shipped 2026-05-05 — archived at `openspec/archive/2026-05-05-enterprise-react-i18n/`; capability spec at `openspec/specs/react-i18n/spec.md`. This change consumes `TranslationKey` for every `FieldConfig.label` / `placeholder` / `helperText` / `errorMessage`, `useTranslation` for label resolution at render time, `useErrorMessage` for `SuaveplanError`-coded validation failures, and the `ContributedTranslationKeys` augmentation pattern for default recipe button labels (`react-forms.wizard.next` etc.).
- `enterprise-react-ui-v2` (sibling, in-flight) — provides `Field` / `FieldGroup` / `FieldSet` / `InputGroup` / `Textarea` / `Checkbox` / `Switch` / `RadioGroup` / `Toggle` / `ToggleGroup` primitives the factory and recipes render onto. Wave 5b waits for Wave 3b before adding `@suaveplan/react-ui` as a peer dep (gate enforced via `scripts/lint/check-no-backwards-deps.ts` per-tier hierarchy).
---
## Why
The original `enterprise-react-forms` proposal bundled product-scope domain forms (`BankAccountForm`, `UserProfileForm`, `FriendRequestForm`, `GroupCreateForm`, `MessageComposeForm`, `ChangePasswordForm`, `TwoFactorForm`, `NotificationSettingsForm`, `PrivacySettingsForm`, `SiteConfigForm`, `SystemSettingsForm`, `NotificationPreferencesForm`) into the library. That conflates two layers: the **library** (primitives + composition machinery) and the **consumer's product** (the actual forms a particular app needs). The result is a package that nobody outside Suaveplan can adopt without inheriting our entity model, and that we ourselves cannot evolve without coordinating with every domain at once.
The user has approved a primitives + factory + recipes architecture instead. `@suaveplan/react-forms` v2 ships:
1. **Primitives** that wrap react-hook-form + react-ui controls into a tested, accessible, i18n'd surface.
2. **A `createDomainForm` factory** that takes a Zod schema + a typed field config and returns a working form component — without hiding react-hook-form. Three escape hatches (`children` render-prop, `
` static accessor, `useDomainForm()` hook) keep the abstraction transparent.
3. **Four recipes in v1** — `wizard`, `repeating-fieldset`, `conditional`, `async-search` — covering the multi-step, dynamic-array, dependent-field, and prefilled-edit patterns that every real app needs.
4. **A validation library** — synchronous validators (`required`, `minLength`, `maxLength`, `email`, `url`, `pattern`, `phone`, `postcode`, `creditCard`, `password-strength`, `iban`, `bic`, `custom`), async validators (`asyncEmailUnique`, `asyncUsernameAvailable`), and composers (`AND` / `OR` / `sequence`).
Domain-specific forms move out to consumer apps and to the example `@suaveplan/react-ui-pack-auth` (a separate change), which proves the factory contract end-to-end on register / login / password-reset / 2FA / magic-link.
The branded-`TranslationKey` discipline from §11.2 of the master design is the second non-negotiable: every user-facing string in a `FieldConfig` is typed as `TranslationKey`, never `string`. Validators return `messageKey: TranslationKey`. The CI gate (`scripts/lint/check-no-hardcoded-strings.ts`) catches literal regressions before they ship.
---
## Current-state audit (genesis)
The package does not exist on `genesis`. `STATUS.md` lists `react-forms` under `Pending`. There are no consumers waiting on a specific public surface yet — the example pack (`react-ui-pack-auth`) is the first consumer and will track this proposal.
---
## Available workspace deps on genesis
| Dep | Version source | Role |
|---|---|---|
| `@suaveplan/react-i18n` | `workspace:^` | `TranslationKey`, `useTranslation`, ``, locale glue |
| `@suaveplan/react-ui` | `workspace:^` | `Field`, `FieldGroup`, `Input`, `Select`, `Combobox`, `Calendar`, `Button`, etc. |
| `@suaveplan/api-schemas` | `workspace:^` | Shared request / response Zod schemas the factory consumes via `schema:` |
| `@suaveplan/error` | `workspace:^` | `defineCodes("REACT_FORMS", [...])` registry; `ReactFormsError` |
| `@suaveplan/timing` | `workspace:^` | Debounce / dedup for async field validators |
| `@suaveplan/types` | `workspace:^` | `Clock`, `LoggerLike`, `StorageLike` (used by form persistence recipes) |
| `react-hook-form` | `catalog:forms` | Form state core; surfaced (never hidden) by the factory |
| `@hookform/resolvers` | `catalog:forms` | Zod resolver bridge into RHF |
| `zod` | `catalog:forms` | Schema spine |
| `date-fns` | `catalog:datetime` | Date / datetime field formatters |
| `@opentelemetry/api` | `catalog:otel` | `form.submit.duration_ms`, `form.validation.duration_ms`, `form.async-validator.duration_ms`, `form.error_total` |
External peer: `react@^18`. No new deps need to be added to the root catalog — every entry above already exists.
---
## Public API surface
### `primitives/`
`Field` (re-export from `@suaveplan/react-ui`, single source), `FieldGroup`, `AccessibleDatePicker`, `DateTimePicker`, `MoneyInput`, `SearchableSelect` (async), `TagInput`, `FileUploadField` (defers to `@suaveplan/react-file-upload` when present, falls back to native ``), `RichTextField` (defers to `@suaveplan/react-rich-text`), `CodeInput` (OTP / 2FA, wraps `InputOTP`).
Each primitive is a separate subpath export so consumers can tree-shake — e.g. `import { MoneyInput } from '@suaveplan/react-forms/primitives/money-input/money-input'`.
### `core/`
- `ApiClientContext` — `` + `useApiClient()`. The factory's `onSubmit` receives `api` from this context. Implementation-agnostic (consumer brings their own client).
- `AuthContext` — `` + `useAuth()`. Surfaces `signIn` / `signOut` / `currentUser` so registration forms can auto-sign-in on success.
- `ValidationEngine` — combines a Zod schema, per-field async validators, cross-field rules, and a server-error mapper into a single resolver compatible with react-hook-form.
- `useFormValidation(schema, options)` — hook returning a memoised resolver from a `ValidationEngine`.
- `useDomainForm(config)` — the hook the factory wraps. Standalone-callable for consumers building bespoke forms that don't fit the factory's layout shape.
### `factory/`
- `createDomainForm(config)` — returns `ComponentType` plus a `.Field` static accessor and `.useFormContext` re-export.
- Field-kind registry typed via `FieldConfigByKind`; misconfigurations fail at module-evaluation time via Zod-validated `FieldConfig` schemas.
- Layout dispatcher (`vertical` / `horizontal` / `grid` / custom render fn).
### `recipes/`
- `createWizardForm({ steps, onComplete })` — multi-step form with per-step Zod sub-schema, navigation guards, and a `useWizardState()` hook.
- `createRepeatingFieldset({ minRows, maxRows, fieldset })` — dynamic-array rows backed by RHF's `useFieldArray`.
- `createConditionalField({ when, render })` — declarative show/hide based on other field values; participates in validation as a function of visibility.
- `createAsyncSearchForm({ load, schema, fields })` — async-prefilled forms (load entity by id, edit, save).
### `validation/`
- `validators/` — `required`, `minLength`, `maxLength`, `email`, `url`, `pattern`, `phone`, `postcode`, `creditCard`, `password-strength`, `iban`, `bic`, `custom`.
- `async/` — `asyncEmailUnique`, `asyncUsernameAvailable`. Each accepts `{ debounceMs, signal, dedupKey }`.
- `compose/` — `and(...validators)`, `or(...validators)`, `sequence(...validators)` (short-circuits on first failure).
All validators return `{ valid: true } | { valid: false; messageKey: TranslationKey; messageArgs?: Record }`. The `messageKey` field is typed as `TranslationKey`, not `string` — passing a literal misses the translation pipeline and fails the CI no-hardcoded-strings gate.
---
## Design decisions
### 1. Five-layer surface
`primitives/ → core/ → factory/ → recipes/ → validation/`. Each layer compiles, tests, and ships independently; later layers consume earlier ones, never the reverse. Ports the master-design §9.1 layout verbatim.
### 2. The factory surfaces RHF — three escape hatches
`createDomainForm` is the 80%-case ergonomics layer. The other 20% of forms need direct access to the RHF context, partial layouts, or a completely custom shell. The three hatches:
- **`children` render-prop** — passing `children` with the render-prop signature gives the consumer raw `useFormContext()` access plus pre-rendered field elements keyed by name.
- **`` static accessor** — for partial layouts where the consumer wants the factory's layout for some fields but inlines others into a custom JSX shape.
- **`useDomainForm()` hook** — the same hook the factory wraps, exported standalone. Consumers build their own component shell with full control over markup.
The factory never hides RHF; it only declares it. Consumers can always drop one layer down without losing their config.
### 3. `FieldConfig` strings are `TranslationKey`, never `string`
Every user-facing text property in `FieldConfig` is typed as `TranslationKey` (per master design §9.5.1 and §11.2). This is enforced at the type level:
```ts
export interface FieldConfigBase {
label: TranslationKey;
placeholder?: TranslationKey;
helperText?: TranslationKey;
errorMessage?: TranslationKey | { [code: string]: TranslationKey };
}
const RegisterForm = createDomainForm({
schema: RegisterRequestSchema,
fields: {
email: { kind: "email", label: "auth.email.label" }, // typechecks
name: { kind: "text", label: "Full name" satisfies string }, // TS error
},
// ...
});
```
The same constraint applies to `submit.label`, `submit.cancelLabel`, recipe step titles, and conditional-field gate labels.
### 4. `kind: 'custom'` is a first-class field kind
Captcha, signature pad, file dropzone with consumer-specific upload protocol — these are not escape hatches. They are first-class custom fields with full factory integration:
```ts
captcha: {
kind: "custom",
label: "auth.captcha.label",
render: ({ field, fieldState }) => ,
}
```
`kind: 'custom'` participates in layout, validation, error messaging, and submit wiring identically to built-in kinds. The factory exposes `field` (RHF `ControllerRenderProps`) and `fieldState` (RHF `ControllerFieldState`) to the render function.
### 5. All four recipes ship in v1
Splitting recipes across releases creates the "Phase 2 is unwritten" failure mode (`openspec/AGENTS.md` §2b). `wizard`, `repeating-fieldset`, `conditional`, `async-search` all ship in the v1 archive of this change. The factory's internal hook architecture is designed for all four from day one.
### 6. `ValidationEngine` semantics
The engine layers, in order, on every form mutation:
1. **Zod schema** — the spine. Synchronous; runs first.
2. **Cross-field rules** — e.g. `passwordConfirm matches password`. Synchronous; runs after the schema. Has access to the full form values object.
3. **Per-field async validators** — debounced via `@suaveplan/timing` (`debounce({ leading: false, trailing: true, maxWait })`), deduped by `(field, value)` key, cancelled via `AbortSignal` when the form unmounts or when a newer value supersedes the in-flight check.
4. **Server-error mapping** — when `onSubmit` throws a `ReactFormsError(REACT_FORMS.SERVER_VALIDATION)` carrying `{ field, code, messageArgs }`, the engine sets the corresponding RHF field error with the translated message resolved via `t(code, messageArgs)`.
`AbortSignal` threads through every async path. Switching to a different form mid-validation cancels in-flight checks instead of leaking promises.
### 7. Validator return shape is a discriminated union
`{ valid: true } | { valid: false; messageKey: TranslationKey; messageArgs?: Record }`. Plain-string returns are a type error. The engine resolves `messageKey` against the active i18n instance at error-display time, so the same validator works in every locale with no additional wiring.
### 8. OTel-first observability
Every form lifecycle event emits structured telemetry (see § Telemetry below). Counter / histogram names are stable contracts — consumers can build dashboards against them.
### 9. Server Actions are out of scope
`onSubmit` stays a user-defined async function. RSC / Server Actions integration is a Wave 8+ follow-up — the surface is too unstable in React 19 to bake in now.
---
## What changes
### ADDED
```text
packages/forms/react-forms/
├── README.md
├── package.json
├── src/
│ ├── index.ts # explicit named exports only
│ ├── error-codes.ts # defineCodes("REACT_FORMS", [...])
│ ├── primitives/
│ │ ├── field/ # re-export from react-ui
│ │ ├── accessible-date-picker/
│ │ ├── date-time-picker/
│ │ ├── money-input/
│ │ ├── searchable-select/
│ │ ├── tag-input/
│ │ ├── file-upload-field/
│ │ ├── rich-text-field/
│ │ └── code-input/
│ ├── core/
│ │ ├── api-client-context/
│ │ ├── auth-context/
│ │ ├── validation-engine/
│ │ ├── use-form-validation/
│ │ └── use-domain-form/
│ ├── factory/
│ │ ├── create-domain-form/
│ │ ├── field-config/ # FieldConfig types + Zod schemas per kind
│ │ └── layout/ # vertical / horizontal / grid dispatchers
│ ├── recipes/
│ │ ├── wizard/
│ │ ├── repeating-fieldset/
│ │ ├── conditional/
│ │ └── async-search/
│ └── validation/
│ ├── validators/ # required, email, url, ...
│ ├── async/ # asyncEmailUnique, asyncUsernameAvailable
│ └── compose/ # and / or / sequence
├── e2e/
│ ├── factory.spec.ts
│ ├── recipes.spec.ts
│ └── a11y.spec.ts
├── tsconfig.json
├── tsconfig.lib.json
├── vite.config.ts
└── vitest.config.ts
```
### REMOVED (vs the legacy `enterprise-react-forms` proposal)
Every domain form below is dropped from the package. Justification (applies uniformly): **product scope; belongs in consumer apps; replaced by the `createDomainForm` factory and the example `@suaveplan/react-ui-pack-auth` pack.**
- `auth/forms/` — `SignInForm`, `SignUpForm`, `ForgotPasswordForm`, `ResetPasswordForm`
- `bank/forms/` — `BankAccountForm`, `BankTransactionForm`
- `user/forms/` — `UserCreateForm`, `UserEditForm`, `OptimizedUserCreateForm`
- `friend/` — `FriendRequestForm`
- `group/forms/` — `GroupCreateForm`, `GroupEditForm`
- `message/forms/` — `MessageCreateForm`, `MessageEditForm`
- `notification/forms/` and `notifications/forms/` — `NotificationCreateForm`, `NotificationEditForm` (duplicate domains in upstream collapsed)
- `security/` — `ChangePasswordForm`, `TwoFactorForm` (re-emerge in `react-ui-pack-auth`)
- `settings/forms/` — `SettingCreateForm`, `SettingEditForm`, `NotificationSettingsForm`, `PrivacySettingsForm`
- `site/forms/` — `SiteCreateForm`, `SiteEditForm`, `SiteConfigForm`
- `system/forms/` — `SystemCreateForm`, `SystemEditForm`, `SystemSettingsForm`
- `demo/DemoApp.tsx` — example app, not library code
Also dropped from upstream: `core/components/FormProgressBar`, `FormSection`, `FormSkeleton`, `FormWizard`, `FormProvider` — superseded by the recipes (`wizard` recipe covers `FormWizard`; `react-ui` Skeleton covers `FormSkeleton`; layout is the factory's job). `shared/components/VirtualizedList`, `LazyFormSection`, `OptimizedFormField` — performance scaffolding belongs in `react-ui` or in consumer apps. `core/storage/` form persistence — moves to a `recipes/persistent-form` follow-up if demand surfaces. `security/csrf`, `security/rate-limiting`, `security/sanitization` — cross-cutting concerns that belong in `@suaveplan/api-client` or the consumer's transport layer, not in a forms package.
---
## Parity Checklist
| Capability | Upstream | Migrated | Notes |
|---|---|---|---|
| `Field` primitive | ✅ | ✅ | Re-exported from `@suaveplan/react-ui` to keep one source of truth |
| `AccessibleDatePicker` | ✅ | ✅ | Calendar + ARIA wired via react-ui `Calendar` |
| `DateTimePicker` | ✅ | ✅ | Time-of-day component composes date + time inputs |
| `MoneyInput` | ⬆️ | ⬆️ | Enhanced: locale-aware via `@suaveplan/i18n` formatters; upstream was hardcoded GBP |
| `SearchableSelect` (async) | ✅ | ⬆️ | Enhanced: debounce + dedup + AbortSignal via `@suaveplan/timing` |
| `TagInput` | ✅ | ✅ | |
| `FileUploadField` | ✅ | ✅ | Defers to `@suaveplan/react-file-upload` when present, native `` fallback |
| `RichTextField` | ✅ | ✅ | Defers to `@suaveplan/react-rich-text` |
| `CodeInput` (OTP) | ✅ | ✅ | Wraps `react-ui` `InputOTP` |
| `ApiClientContext` | ✅ | ✅ | |
| `AuthContext` | ✅ | ✅ | |
| `TranslationContext` | ✅ | 🚫 | Replaced by `@suaveplan/react-i18n`'s `TranslationProvider`; forms package consumes that, doesn't redefine it |
| `useFormValidation` | ✅ | ✅ | Now returns a `ValidationEngine`-backed resolver |
| `ValidationEngine` | ✅ | ⬆️ | Enhanced: cross-field rules + AbortSignal-cancellable async + server-error mapping |
| `useFormSubmission` | ✅ | 🚫 | Replaced by the factory's `submit.onSubmit` callback + `useDomainForm()`'s `submit` return |
| `useFormPersistence` | ✅ | 🚫 | Deferred to a follow-up `recipes/persistent-form` (can be built on top of `useDomainForm` + `StorageLike` from `@suaveplan/types`) |
| `FormProvider` / `FormStateContext` | ✅ | 🚫 | RHF's `` is sufficient; the factory wraps it |
| `FormProgressBar` | ✅ | 🚫 | Wizard recipe owns progress UI; consumers compose `react-ui` `Progress` if needed |
| `FormSection` | ✅ | 🚫 | Layout concern; the factory's layout dispatcher handles sections via `FieldGroup` |
| `FormSkeleton` | ✅ | 🚫 | `react-ui` Skeleton is the canonical source |
| `FormWizard` | ✅ | ⬆️ | Replaced by `createWizardForm` recipe (typed steps, sub-schemas, navigation guards) |
| `useDebounce` / `useDebouncedValidation` | ✅ | 🚫 | Replaced by `@suaveplan/timing`'s `debounce` directly inside the validation engine |
| `useLocalStorage` | ✅ | 🚫 | Out of scope; `StorageLike` from `@suaveplan/types` covers it generically |
| `useMemoryMonitor` / `usePerformanceMonitor` / `useRenderTracker` | ✅ | 🚫 | Dev-only telemetry; replaced by OTel histograms (see § Telemetry) |
| `useSchemaValidation` | ✅ | 🚫 | Folded into `ValidationEngine` |
| `usePagination` | ✅ | 🚫 | Out of scope for a forms package; `@suaveplan/pagination` covers this |
| `VirtualizedList` | ✅ | 🚫 | Belongs in `react-ui` if needed; not a forms concern |
| `LazyFormSection` / `OptimizedFormField` | ✅ | 🚫 | Premature optimisation; React 18 concurrent rendering covers the same need |
| `accessibility/` components + hooks | ✅ | ⬆️ | Subsumed by `react-ui`'s WCAG 2.2 AA baseline; forms inherit by composition |
| `core/i18n/` (custom translation context) | ✅ | 🚫 | Replaced by `@suaveplan/react-i18n` |
| `validation/` library | ✅ | ⬆️ | Enhanced: discriminated-union returns, `messageKey: TranslationKey` typing, async validators with debounce + dedup, compose helpers |
| `security/csrf`, `security/rate-limiting`, `security/sanitization` | ✅ | 🚫 | Cross-cutting transport concerns; belong in `@suaveplan/api-client`, not a forms package |
| `shared/state/` (form persistence) | ✅ | 🚫 | Deferred to `recipes/persistent-form` follow-up |
| `integration/` (mocks) | ✅ | 🚫 | Genesis uses `@suaveplan/testing` + real DI adapters per CLAUDE.md "no mocks" rule |
| **Domain forms** (auth, bank, user, friend, group, message, notification, settings, site, system) | ✅ | 🚫 | Product scope; replaced by `createDomainForm` factory. Auth set re-emerges in `react-ui-pack-auth`. |
| `createDomainForm` factory | ❌ | ✅ | New: declarative form assembly with three escape hatches |
| Wizard recipe | ❌ | ✅ | New: typed multi-step factory |
| Repeating-fieldset recipe | ❌ | ✅ | New: dynamic-array fieldsets via `useFieldArray` |
| Conditional-field recipe | ❌ | ✅ | New: declarative show/hide with validation gating |
| Async-search recipe | ❌ | ✅ | New: load-by-id then edit |
| Branded `TranslationKey` typing for FieldConfig | ❌ | ✅ | New: type-level enforcement of i18n discipline |
| OTel telemetry | ❌ | ✅ | New: form lifecycle counters + histograms |
---
## Field kinds shipped with v1
`text`, `email`, `password` (with optional `strengthMeter: boolean`), `number`, `currency`, `date`, `dateTime`, `select`, `multi-select`, `combobox` (async), `checkbox`, `radio`, `toggle`, `textarea`, `rich-text`, `file`, `tag-input`, `otp`, `custom`.
Each kind has a sibling Zod schema that validates its `FieldConfig` shape at factory-define time. A misconfigured field (e.g. a `select` without `options`) throws `ReactFormsError(REACT_FORMS.FIELD_CONFIG_INVALID)` at module evaluation, not on first render.
---
## Recipes
- **`createWizardForm({ steps, onComplete })`** — multi-step factory. Each step has its own Zod sub-schema; navigating forward validates the current step before advancing; navigating backward preserves entered data. Exposes `useWizardState()` for custom progress UIs.
- **`createRepeatingFieldset({ name, minRows, maxRows, fieldset })`** — dynamic-array fieldsets backed by RHF's `useFieldArray`. `fieldset` is itself a `FieldConfig` map, so rows nest factory-typed fields with full validation.
- **`createConditionalField({ when, render })`** — declarative show/hide. `when` is a predicate over the current form values; hidden fields are excluded from validation (and from the submitted payload) so optional sections don't pollute the schema.
- **`createAsyncSearchForm({ load, schema, fields })`** — async-prefilled forms. `load(id, signal)` fetches the entity; the form renders in a loading state until the promise resolves, then mounts with `defaultValues` populated.
---
## Validation library
### Synchronous validators
`required`, `minLength(min)`, `maxLength(max)`, `email`, `url`, `pattern(regex)`, `phone({ region })`, `postcode({ region })`, `creditCard` (Luhn), `password-strength({ minScore })`, `iban`, `bic`, `custom(predicate, messageKey)`.
### Async validators
`asyncEmailUnique({ check, debounceMs, dedupKey })`, `asyncUsernameAvailable({ check, debounceMs, dedupKey })`. Each accepts an injected `check(value, signal): Promise` so the validator is transport-agnostic and testable without network mocks.
### Composers
`and(...validators)` — all must pass. `or(...validators)` — at least one must pass. `sequence(...validators)` — short-circuits on first failure (cheap-first ordering).
All return the discriminated union shape; all accept `AbortSignal` where async; all surface `messageKey: TranslationKey`.
---
## Telemetry
Every form lifecycle event emits OTel data via `@suaveplan/types`'s `LoggerLike` and `@opentelemetry/api`:
| Instrument | Kind | Attributes | Description |
|---|---|---|---|
| `form.submit.duration_ms` | Histogram | `form_id` | End-to-end submit latency |
| `form.validation.duration_ms` | Histogram | `form_id`, `phase` (schema / cross-field / async) | Per-phase validation latency |
| `form.async-validator.duration_ms` | Histogram | `form_id`, `validator` (`asyncEmailUnique` / `asyncUsernameAvailable` / consumer-named) | Per-validator async latency |
| `form.error_total` | Counter | `form_id`, `kind` (`schema` / `cross-field` / `async` / `submit` / `server`) | Error counter by failure class |
| `form.field.touched_total` | Counter | `form_id`, `field` | First-touch counter for field-level analytics |
Every histogram/counter name is a stable contract — see § Migration in the spec for the rule when these need to change.
---
## Out of scope
- **Server Actions / RSC integration** — `onSubmit` stays user-defined. Targets a follow-up once React 19 server-rendering APIs stabilise.
- **Server-rendered form recipes** — same reason.
- **Domain-specific forms** — moved to consumer apps and the example `@suaveplan/react-ui-pack-auth` pack.
- **Form persistence (`useFormPersistence`)** — deferred to `recipes/persistent-form` follow-up.
- **CSRF / rate-limiting / sanitisation primitives** — belong in `@suaveplan/api-client` and transport layers.
- **Performance hooks** (`useMemoryMonitor`, `usePerformanceMonitor`, `useRenderTracker`) — replaced by OTel-shipped telemetry.
---
## Source mapping
| Legacy requirement (`enterprise-react-forms`) | Migrated artefact in v2 | Justification if dropped |
|---|---|---|
| `BankAccountForm`, `BankTransactionForm` | `createDomainForm` factory | Product scope |
| `UserCreateForm`, `UserEditForm`, `OptimizedUserCreateForm` | `createDomainForm` + `createAsyncSearchForm` recipe | Product scope |
| `FriendRequestForm` | `createDomainForm` factory | Product scope |
| `GroupCreateForm`, `GroupEditForm` | `createDomainForm` + `createAsyncSearchForm` recipe | Product scope |
| `MessageCreateForm`, `MessageEditForm` | `createDomainForm` + `createAsyncSearchForm` recipe | Product scope |
| `NotificationCreateForm`, `NotificationEditForm`, `NotificationPreferencesForm` | `createDomainForm` factory | Product scope |
| `ChangePasswordForm` | `react-ui-pack-auth` (separate change) | Belongs in the auth pack |
| `TwoFactorForm` | `react-ui-pack-auth` (separate change) | Belongs in the auth pack |
| `SignInForm`, `SignUpForm`, `ForgotPasswordForm`, `ResetPasswordForm` | `react-ui-pack-auth` (separate change) | Belongs in the auth pack |
| `SettingCreateForm`, `SettingEditForm`, `NotificationSettingsForm`, `PrivacySettingsForm` | `createDomainForm` factory | Product scope |
| `SiteCreateForm`, `SiteEditForm`, `SiteConfigForm` | `createDomainForm` factory | Product scope |
| `SystemCreateForm`, `SystemEditForm`, `SystemSettingsForm` | `createDomainForm` factory | Product scope |
| `Field`, `AccessibleDatePicker`, `DateTimePicker`, `MoneyInput`, `SearchableSelect`, `TagInput`, `CodeInput`, `RichTextField`, `FileUploadField` | `primitives/` | Direct port |
| `ApiClientContext`, `AuthContext` | `core/api-client-context`, `core/auth-context` | Direct port |
| `TranslationContext`, `core/i18n/` | `@suaveplan/react-i18n` | Cross-package consolidation |
| `useFormValidation`, `useFormSubmission`, `useFormPersistence` | `useFormValidation` (kept), others 🚫 | Submission folds into the factory; persistence is a follow-up recipe |
| `ValidationEngine` | `core/validation-engine` (enhanced) | ⬆️ AbortSignal + cross-field + server mapping |
| `validation/validators/`, `validation/async/`, `validation/compose/` | Direct port with `TranslationKey` typing | ⬆️ Discriminated-union returns |
| `FormWizard` component | `recipes/wizard/createWizardForm` | Refactored from component to factory |
| `FormProgressBar`, `FormSection`, `FormSkeleton`, `FormProvider` | 🚫 | Layout / chrome lives in `react-ui` |
| `useDebounce`, `useDebouncedValidation`, `useLocalStorage`, `useMemoryMonitor`, `usePerformanceMonitor`, `useRenderTracker`, `useSchemaValidation`, `usePagination` | 🚫 | Replaced by `@suaveplan/timing`, `@suaveplan/types` `StorageLike`, OTel telemetry, `@suaveplan/pagination` |
| `accessibility/` standalone components | 🚫 | Inherited via composition with `@suaveplan/react-ui` (WCAG 2.2 AA baseline) |
| `security/csrf`, `security/rate-limiting`, `security/sanitization` | 🚫 | Belongs in transport layer |
| `integration/mocks/` | 🚫 | Genesis uses real DI adapters; `@suaveplan/testing` provides the harness |
---
## Verification approach
Each requirement in `specs/react-forms/spec.md` carries a `**Verified by:**` clause naming the test files. The pre-publish gate is the standard CLAUDE.md sequence:
```bash
bunx turbo lint typecheck test --filter=@suaveplan/react-forms
bunx turbo test:coverage --filter=@suaveplan/react-forms # 100/100/100/100 hard gate
bunx turbo build --filter=@suaveplan/react-forms
bun scripts/lint/check-no-hardcoded-strings.ts packages/forms/react-forms
bun scripts/lint/check-translation-keys.ts packages/forms/react-forms
bun scripts/lint/check-test-runner-imports.ts packages/forms/react-forms
```
Plus the standard banned-pattern, barrel-shape, missing-`.md`, README, bare-version, and subpath-export scans from `openspec/AGENTS.md` §13.
---
## Migration impact
This is a pure additive on `genesis` — no existing package depends on `@suaveplan/react-forms` yet. The example pack `@suaveplan/react-ui-pack-auth` (a separate change) is the first consumer and will be developed against this proposal's surface in Wave 6.
For consumers porting from upstream `@suaveplan/react-forms@1.x`:
- **Old**: `import { SignInForm } from '@suaveplan/react-forms';`
- **New**: `import { LoginForm } from '@suaveplan/react-ui-pack-auth';` OR build with `createDomainForm({ schema: LoginRequestSchema, fields: { ... } })`.
A `MIGRATION.md` section in the package README enumerates each upstream form → new factory call. Consumer apps rebuild their domain forms at the call site once.
---
## Impact
### Affected dependents
Run `bunx turbo ls --filter=...@suaveplan/react-forms` to enumerate. Genesis state at proposal time:
- **No dependents on genesis yet** (verified by `bunx turbo ls --filter=...@suaveplan/react-forms`). The package does not exist; its first consumer (`@suaveplan/react-ui-pack-auth`) is itself a pending change tracking this proposal.
- `@suaveplan/react-ui-pack-auth` — [ ] tests run / [x] docs reviewed / [ ] no action needed (reason: separate change tracks this surface; both ship together in Wave 6 of the master design)
For foundation-package dependent sweep purposes, this package is **not foundational** — it sits high in the dep graph and has zero downstream consumers at proposal time.