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,137 @@
# enterprise-react-forms-v2 — Implementation Tasks
Implementation order follows Wave 5 of the master design (`openspec/cross-cutting-designs/2026-05-05-react-ui-react-i18n-react-forms.md`): each layer is gated independently and must hit `bunx turbo lint typecheck test --filter=@suaveplan/react-forms` green plus `test:coverage` at 100/100/100/100 plus 100% docs coverage before the next layer starts.
Prerequisites (must be green on `genesis` before this change starts):
- [ ] `@suaveplan/react-i18n` shipped (Wave 1) — provides `TranslationKey`, `TranslationProvider`, `useTranslation`, `<Trans>`.
- [ ] `@suaveplan/react-ui` core scaffolding shipped (Wave 2) — provides `Field`, `FieldGroup`, `Input`, `Select`, `Combobox`, `Calendar`, `Button`, `Skeleton`, `InputOTP`.
- [ ] `@suaveplan/react-ui` form-primitive batch shipped (Wave 3b) — full set of form-primitive components.
---
## 0. Pre-flight (non-negotiable)
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
## 0. Scaffolding
- [ ] Create `packages/forms/react-forms/` with `package.json` (`workspace:^` for internal deps, `catalog:` for externals).
- [ ] Wire `tsconfig.json` and `tsconfig.lib.json` extending `@suaveplan/typescript-config/react-library.json`.
- [ ] Wire `vite.config.ts` (Vite 7) for the build.
- [ ] Wire `vitest.config.ts` via `createTestConfig()` from `@suaveplan/testing/config` — `provider: 'istanbul'`, thresholds `100/100/100/100`.
- [ ] Wire `playwright.config.ts` for `e2e/` via `@suaveplan/testing-e2e`.
- [ ] Add `README.md` (package name, one-paragraph description, install, quick-start, Modules section linking each co-located `.md`).
- [ ] Add `src/index.ts` as an explicit named-export barrel (no `export *`, per CLAUDE.md §10).
- [ ] Add `src/error-codes.ts` registering `defineCodes("REACT_FORMS", [...])` with codes: `FIELD_CONFIG_INVALID`, `SCHEMA_RESOLVER_FAILED`, `ASYNC_VALIDATOR_ABORTED`, `SERVER_VALIDATION`, `WIZARD_STEP_INVALID`, `WIZARD_STEP_NOT_FOUND`, `REPEATING_ROW_LIMIT`, `CUSTOM_FIELD_NO_RENDER`, `CONTEXT_NOT_PROVIDED`.
- [ ] Add `src/error-codes.md` documenting each code (purpose, when thrown, how to recover).
**Gate:** package builds, `bunx turbo build --filter=@suaveplan/react-forms` green; banned-pattern scan from AGENTS.md §13 step 4 returns zero output; barrel-shape scan from §13 step 4a returns zero output; test-runner import scan from §13 step 4b returns zero output.
---
## Layer 1 — Primitives
Each primitive ships as a five-files-per-feature module: `<name>.tsx` + `<name>.types.ts` + `<name>.test.tsx` + `<name>.stories.tsx` + `<name>.md` + `index.ts` (explicit named exports).
- [ ] **`primitives/field/`** — re-export `Field` and `FieldGroup` from `@suaveplan/react-ui` so the forms package has a single import path. Tests assert the re-exported references are identity-equal to the upstream symbols.
- [ ] **`primitives/accessible-date-picker/`** — Calendar + ARIA wrapper with `<Field>` integration; props typed with `TranslationKey` for `label`, `placeholder`, `helperText`.
- [ ] **`primitives/date-time-picker/`** — composes `AccessibleDatePicker` + a time input; supports `granularity: 'minute' | 'second' | 'hour'`.
- [ ] **`primitives/money-input/`** — locale-aware via `@suaveplan/i18n` `formatCurrency` / `parseCurrency`; `currency: string` (ISO 4217), `locale?: string` (defaults to active i18n locale).
- [ ] **`primitives/searchable-select/`** — async option loading; debounce + dedup via `@suaveplan/timing`'s `debounce({ leading: false, trailing: true, maxWait })`; `AbortSignal` cancellation on subsequent queries.
- [ ] **`primitives/tag-input/`** — chip-based multi-value input with keyboard (Enter / Backspace / arrow) navigation.
- [ ] **`primitives/file-upload-field/`** — peer-dep on `@suaveplan/react-file-upload`; falls back to native `<input type="file">` when peer absent.
- [ ] **`primitives/rich-text-field/`** — peer-dep on `@suaveplan/react-rich-text`; falls back to a styled `<textarea>` when peer absent.
- [ ] **`primitives/code-input/`** — wraps `react-ui`'s `InputOTP`; props for length, autoSubmit, validation pattern.
- [ ] Add a per-primitive subpath export to `package.json` (e.g. `"./primitives/money-input/money-input"`) so consumers tree-shake.
**Gate (Layer 1):** every primitive has 100% statement / branch / function / line coverage in vitest; every primitive has a `.md` meeting `docs:check`'s 200-word floor + all six required sections; every primitive renders a Storybook `Default`, `Playground`, `States`, `WithI18n`, and `WithTheme` story; `bunx turbo lint typecheck test --filter=@suaveplan/react-forms` green; `bun scripts/lint/check-no-hardcoded-strings.ts` returns zero output for the `primitives/` tree.
---
## Layer 2 — Core
- [ ] **`core/api-client-context/`** — `<ApiClientProvider value={api}>` + `useApiClient()`. Generic over `TApi`. Throws `REACT_FORMS.CONTEXT_NOT_PROVIDED` on use outside provider. Tests cover provider mount / unmount, value identity, and the throw-when-absent path.
- [ ] **`core/auth-context/`** — `<AuthProvider value={auth}>` + `useAuth()`. Generic over `TUser`. Surfaces `signIn` / `signOut` / `currentUser` / `isAuthenticated`. Same context-not-provided contract as ApiClientContext.
- [ ] **`core/validation-engine/`** — class implementing the four-phase pipeline (Zod schema → cross-field rules → async validators → server-error mapping). Constructor accepts `{ schema, crossFieldRules?, asyncValidators?, debounceMs?, clock?, signal? }`. Exposes `resolve(values, signal)` returning a RHF-compatible resolver result. Internal debounce + dedup via `@suaveplan/timing`. Tests cover each phase, AbortSignal cancellation, dedup keys, and server-error mapping with translated messages.
- [ ] **`core/use-form-validation/`** — hook returning a memoised resolver from a `ValidationEngine`. Stable identity across renders unless config dependencies change. Tests cover memoisation, re-creation on dep change, and correct resolver shape.
- [ ] **`core/use-domain-form/`** — the hook the factory wraps. Accepts the same config object. Returns `{ form: UseFormReturn, submit, isSubmitting, serverError }`. Tests cover RHF integration, submit lifecycle, and server-error mapping.
- [ ] Subpath exports for each core module.
**Gate (Layer 2):** Layer 1 still green; new modules at 100% coverage + 100% docs coverage; `bunx turbo lint typecheck test --filter=@suaveplan/react-forms` green.
---
## Layer 3 — Factory
- [ ] **`factory/field-config/`** — `FieldConfigBase` (with `label: TranslationKey`, `placeholder?: TranslationKey`, etc.); discriminated `FieldConfig` union over each of the 19 v1 field kinds (`text`, `email`, `password`, `number`, `currency`, `date`, `dateTime`, `select`, `multi-select`, `combobox`, `checkbox`, `radio`, `toggle`, `textarea`, `rich-text`, `file`, `tag-input`, `otp`, `custom`). Per-kind Zod schema validates the config at factory-define time. Tests cover valid configs, invalid configs (each kind), and the `REACT_FORMS.FIELD_CONFIG_INVALID` throw.
- [ ] **`factory/layout/`** — three built-in layout dispatchers (`vertical`, `horizontal`, `grid`) plus a `LayoutRenderFn` escape hatch typed as `(ctx: LayoutContext) => ReactNode`. Each layout accepts `FieldGroup` partitioning. Tests cover each layout's accessible-name structure.
- [ ] **`factory/create-domain-form/`** — `createDomainForm(config): ComponentType<FormProps> & { Field, useFormContext }`. Wraps `useDomainForm` + `<RHFProvider>` + the layout dispatcher. Mounts a Zod resolver via `@hookform/resolvers/zod` plus the `ValidationEngine`'s extra phases. Three escape hatches:
- [ ] `children` render-prop signature receives `{ form, fields, submit }` for raw composition.
- [ ] `Form.Field name={...}` static accessor renders a single field by name with the factory's chrome.
- [ ] `useDomainForm()` hook is exported standalone for consumers building a custom shell.
- [ ] Tests cover: factory throws on misconfigured field at module-eval time; renders all 19 kinds with correct ARIA; submit fires `onSubmit({ values, ctx })`; server error maps to RHF field error with translated message; each escape hatch works end-to-end.
- [ ] Subpath exports for `factory/create-domain-form/create-domain-form` and `factory/field-config/field-config`.
**Gate (Layer 3):** Layers 1-2 still green; factory at 100% coverage + 100% docs coverage; `e2e/factory.spec.ts` exercises the three escape hatches in a real browser via Playwright.
---
## Layer 4 — Recipes
- [ ] **`recipes/wizard/createWizardForm/`** — multi-step factory. `steps: WizardStep[]` where each `WizardStep = { id, title: TranslationKey, schema: ZodSchema, fields: FieldConfigMap }`. Internal state machine: `goNext()` validates current step before advancing; `goPrev()` preserves entered data; `complete()` runs after the final step's validation. Exposes `useWizardState()` for custom progress UIs. Throws `REACT_FORMS.WIZARD_STEP_NOT_FOUND` when navigating to an unknown id.
- [ ] **`recipes/repeating-fieldset/createRepeatingFieldset/`** — `name: string`, `minRows`, `maxRows`, `fieldset: FieldConfigMap`. Backed by RHF's `useFieldArray`. Add / remove / move row handlers; throws `REACT_FORMS.REPEATING_ROW_LIMIT` when `add()` is called at `maxRows` or `remove()` is called at `minRows`. ARIA `role="list"` on the fieldset, `role="listitem"` per row.
- [ ] **`recipes/conditional/createConditionalField/`** — `when: (values: TValues) => boolean`, `render: (ctx) => ReactNode`. Fields hidden by `when` are excluded from validation AND from the submitted payload (via RHF's `unregister` on hide). Tests cover: hidden field's value not in submit payload; hidden field's validation errors cleared; show / hide does not destroy entered data when the field re-mounts within the same submit cycle.
- [ ] **`recipes/async-search/createAsyncSearchForm/`** — `load: (id: string, signal: AbortSignal) => Promise<TValues>`, `schema`, `fields`. Renders `<Skeleton>` (from `@suaveplan/react-ui`) while `load` resolves; mounts the underlying `createDomainForm` with `defaultValues` populated on success; surfaces a translated error message on failure (key resolved via `useTranslation`).
- [ ] Tests for each recipe at 100% coverage; sibling `.md` files; Storybook stories; subpath exports.
- [ ] `e2e/recipes.spec.ts` exercises each recipe in a real browser.
**Gate (Layer 4):** Layers 1-3 still green; recipes at 100% coverage + 100% docs coverage; `e2e/recipes.spec.ts` green; `e2e/a11y.spec.ts` runs axe-core against every recipe story with zero violations.
---
## Layer 5 — Validation
- [ ] **`validation/validators/`** — one folder per validator (`required/`, `min-length/`, `max-length/`, `email/`, `url/`, `pattern/`, `phone/`, `postcode/`, `credit-card/`, `password-strength/`, `iban/`, `bic/`, `custom/`). Each validator returns the discriminated-union `{ valid: true } | { valid: false; messageKey: TranslationKey; messageArgs?: Record<string, unknown> }`. Per-validator tests with KAT cases (Luhn for `creditCard`, RFC 5321 for `email`, ISO 13616 for `iban`, ISO 9362 for `bic`). Sibling `.md` per validator.
- [ ] **`validation/async/`** — `async-email-unique/`, `async-username-available/`. Each accepts `{ check: (value, signal) => Promise<boolean>, debounceMs, dedupKey, signal }`. No fetch / network primitive baked in — `check` is injected. Tests cover debounce, dedup, AbortSignal cancellation; 100% branch coverage.
- [ ] **`validation/compose/`** — `and/`, `or/`, `sequence/`. Each accepts `(...validators)`. `sequence` short-circuits on first failure. Tests cover composition with sync + async mixes; 100% branch coverage.
- [ ] Subpath exports for every validator and every composer.
**Gate (Layer 5):** Layers 1-4 still green; validation library at 100% coverage + 100% docs coverage; CLAUDE.md "no banned patterns" scan green across `src/`.
---
## Telemetry
- [ ] Wire OTel histograms `form.submit.duration_ms`, `form.validation.duration_ms`, `form.async-validator.duration_ms` (with `validator` attribute), `form.field.touched_total` counter, `form.error_total` counter (with `kind` attribute).
- [ ] Document every instrument in `src/telemetry.md` with attributes, units, and a stable-contract pledge.
- [ ] Tests assert each instrument fires with the expected attribute set on the matching lifecycle event (use `@opentelemetry/sdk-metrics`'s in-memory exporter).
---
## Final pre-archive sweep
Before folding the spec delta and archiving the change, run the AGENTS.md §13 sequence in order — each step must exit green:
- [ ] `bunx turbo lint typecheck test --filter=@suaveplan/react-forms`
- [ ] `bun run check` (biome auto-fix)
- [ ] `bunx turbo test:coverage --filter=@suaveplan/react-forms` (must hit 100/100/100/100)
- [ ] `bunx turbo build --filter=@suaveplan/react-forms`
- [ ] Banned-pattern scan (`@deprecated`, `TODO`, `FIXME`, `ts-ignore`, `ts-expect-error`, `.skip`, `.only`) — zero output.
- [ ] Barrel-shape scan (§13 step 4a) — zero output.
- [ ] Test-runner import scan (§13 step 4b) — zero output.
- [ ] Missing `.md` scan — zero output.
- [ ] README check passes.
- [ ] Bare-version check — zero output.
- [ ] Subpath export count matches feature module count.
- [ ] Security scan (hardcoded secrets) — zero output.
- [ ] `bun scripts/lint/check-no-hardcoded-strings.ts packages/forms/react-forms` — zero output.
- [ ] `bun scripts/lint/check-translation-keys.ts packages/forms/react-forms` — zero output.
- [ ] `bun scripts/lint/check-test-runner-imports.ts packages/forms/react-forms` — zero output.
- [ ] Dependent sweep (`bunx turbo test --filter=...@suaveplan/react-forms`) — green; no dependents at first ship, but verify `react-ui-pack-auth` (when its change is in flight) compiles against the new surface.
- [ ] Update root manifest docs per AGENTS.md §13b: refresh `MANIFEST.md` (`bun scripts/folder-reorg/generate-manifest.ts`), refresh `MISSING.yml` (`bun scripts/generate-missing.ts`), edit `STATUS.md` (move row to Completed), edit `SUMMARY.md` (add per-package paragraph), run `bunx markdownlint-cli2 --fix`.
- [ ] Fold `specs/react-forms/spec.md` ADDED Requirements into `openspec/specs/react-forms/spec.md` (create the file if absent).
- [ ] `git mv openspec/changes/enterprise-react-forms-v2 openspec/archive/2026-MM-DD-enterprise-react-forms-v2`.
- [ ] Archive the legacy `openspec/changes/enterprise-react-forms/` folder via the standard supersession pattern (do NOT delete its history; archive with the supersession date).