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,224 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec: react-ui-packs
**Package**: `@suaveplan/react-ui-pack-auth` (first concrete pack against the capability)
**Status**: PROPOSED
> Every requirement carries a `**Verified by:**` line at authoring time. (See `openspec/AGENTS.md` Section 13.)
This delta introduces the `react-ui-packs` capability — the contract every `@suaveplan/react-ui-pack-<name>` package satisfies — and lands the first concrete implementation, the `auth` pack. The capability spec defines what every pack MUST provide; the requirements below are scoped to the `auth` pack specifically since it is the only pack landing in this change.
## ADDED Requirements
### Requirement: authPack exposes name "auth"
`@suaveplan/react-ui-pack-auth` SHALL export an `authPack` object that satisfies the `ComponentPack<'auth'>` type from `@suaveplan/react-ui/pack`. The object SHALL expose `name: 'auth'` literal, `version: '0.0.0'` literal, `peerRange: string` (semver range against `@suaveplan/react-ui`), and the optional `translations`, `onRegister`, `onUnregister`, `themeTokens`, `variants` slots defined by the contract.
#### Scenario: authPack literally satisfies ComponentPack<"auth">
- **GIVEN** a TypeScript test importing `authPack` from `@suaveplan/react-ui-pack-auth`
- **WHEN** the test asserts `expectTypeOf(authPack).toMatchTypeOf<ComponentPack<'auth'>>()`
- **THEN** the assertion passes at compile time without `// @ts-expect-error`
#### Scenario: authPack name field is the literal "auth"
- **WHEN** consumer code reads `authPack.name`
- **THEN** the value equals `'auth'` AND its TypeScript type is the literal `'auth'` (not `string`)
**Verified by:** `packages/react/react-ui-pack-auth/src/pack/pack.contract.test.tsx::"authPack literally satisfies ComponentPack<'auth'>"` AND `packages/react/react-ui-pack-auth/src/pack/pack.contract.test.tsx::"name is the literal 'auth'"`
---
### Requirement: authPack exposes the five canonical auth forms
`authPack.components` SHALL expose `LoginForm`, `RegisterForm`, `PasswordResetForm`, `TwoFactorForm`, and `MagicLinkForm` — exactly those five components, no more and no fewer in v1. Each SHALL be a React function component whose props type is the matching `<Form>Props` exported from the same package. The pack SHALL also export each component as a top-level named export so consumers can choose between the registry-lookup form (`usePackComponent('auth', 'LoginForm')`) and the direct-import form (`import { LoginForm }`) per master design §5.4.
#### Scenario: registry lookup returns the same reference as the direct export
- **GIVEN** a tree wrapped in `<PackRegistryProvider packs={[authPack]}>`
- **WHEN** a child calls `const FromRegistry = usePackComponent('auth', 'LoginForm')` and the test imports `LoginForm` directly
- **THEN** `Object.is(FromRegistry, LoginForm) === true`
#### Scenario: every form is a function component
- **WHEN** `typeof authPack.components.<formName>` is checked for each of the five forms
- **THEN** every value is `'function'` AND every component renders without throwing when wrapped in the standard provider stack
**Verified by:** `packages/react/react-ui-pack-auth/src/pack/pack.contract.test.tsx::"registry lookup returns same reference as direct export"` AND `packages/react/react-ui-pack-auth/src/pack/pack.contract.test.tsx::"every form is a function component"`
---
### Requirement: every form is built via createDomainForm
Every component in `authPack.components` SHALL be the return value of a single `createDomainForm` call from `@suaveplan/react-forms`. No component SHALL drop into the render-prop escape hatch, the `<Form.Field>` static accessor, or the `useDomainForm()` standalone hook. This is a structural requirement: if any of the five forms cannot be expressed via a single `createDomainForm` invocation, that is a defect in `@suaveplan/react-forms` and MUST be addressed there before this pack ships.
#### Scenario: createDomainForm is the only form constructor used
- **GIVEN** the `auth-pack` source tree
- **WHEN** the test runs `grep -rn "createDomainForm\|useDomainForm\|<.*\.Field" packages/react/react-ui-pack-auth/src/components/` and counts occurrences
- **THEN** every `createDomainForm` count is exactly 1 per form-component file (5 in total) AND `useDomainForm` count is 0 AND `<.*\.Field` count is 0
#### Scenario: each factory invocation passes a Zod schema as the spine
- **WHEN** the test inspects each `createDomainForm` call site
- **THEN** the `schema` property is set AND its value is a Zod object schema imported from `@suaveplan/api-schemas/auth`
**Verified by:** `packages/react/react-ui-pack-auth/src/pack/pack.factory-conformance.test.ts::"createDomainForm is the sole form constructor"` AND `packages/react/react-ui-pack-auth/src/pack/pack.factory-conformance.test.ts::"each factory passes a Zod schema as the spine"`
---
### Requirement: schemas are sourced from @suaveplan/api-schemas/auth
The pack SHALL NOT define a Zod schema inline for any form. Every form's `schema` property SHALL reference an export from `@suaveplan/api-schemas/auth`: `LoginInput` for `LoginForm`, `RegisterInput` (extended with `confirmPassword` and `terms` cross-field refinements via `.extend(...)`) for `RegisterForm`, `PasswordResetRequestInput` / `PasswordResetConfirmInput` for `PasswordResetForm`, `TwoFactorVerifyInput` for `TwoFactorForm`, and `MagicLinkRequestInput` / `MagicLinkVerifyInput` for `MagicLinkForm`. This guarantees the API server and the React client share a single source of truth.
#### Scenario: pack imports auth schemas from api-schemas only
- **GIVEN** the pack's source tree
- **WHEN** the test runs `grep -rn "z\.object\|new ZodObject" packages/react/react-ui-pack-auth/src/components/`
- **THEN** the only matches are the inline `.extend({ confirmPassword, terms })` call inside `register-form.ts` (cross-field augmentation, not a redefinition)
#### Scenario: server-schema change breaks the client at compile time
- **GIVEN** a hypothetical change in `@suaveplan/api-schemas/auth` that renames `LoginInput.email` to `LoginInput.emailAddress`
- **WHEN** `bunx turbo typecheck --filter=@suaveplan/react-ui-pack-auth` runs
- **THEN** the build fails because the field config references the missing `email` property
**Verified by:** `packages/react/react-ui-pack-auth/src/pack/pack.factory-conformance.test.ts::"schemas come from api-schemas only"` AND a `*.expect-error.ts` fixture proving the rename example above
---
### Requirement: every user-visible string is a TranslationKey
Every `label`, `placeholder`, `helperText`, `errorMessage`, `aria-label`, `title`, success-toast body, error-alert body, and submit-button text rendered by any of the five forms SHALL be typed as `TranslationKey` (the brand from `@suaveplan/react-i18n`), never as a string literal. Field configs MUST use the `react-ui-pack-auth.<form>.<field>.<role>` namespace. The pack's translation-key-existence gate (`scripts/lint/check-translation-keys.ts`) and no-hardcoded-strings gate (`scripts/lint/check-no-hardcoded-strings.ts`) SHALL both pass with zero diagnostics.
#### Scenario: a string-literal label fails typecheck
- **GIVEN** a hypothetical edit replacing `label: 'react-ui-pack-auth.login.email.label'` with `label: 'Email'`
- **WHEN** `bunx turbo typecheck --filter=@suaveplan/react-ui-pack-auth` runs
- **THEN** the typecheck fails with a `Type 'string' is not assignable to type 'TranslationKey'` error
#### Scenario: every t-call references an existing key
- **WHEN** `bun scripts/lint/check-translation-keys.ts` runs against `packages/react/react-ui-pack-auth/`
- **THEN** every `t('...')` call resolves to a key present in `src/translations/en-GB.json`
#### Scenario: no hardcoded user-visible strings exist in the pack source
- **WHEN** `bun scripts/lint/check-no-hardcoded-strings.ts` runs against `packages/react/react-ui-pack-auth/src/`
- **THEN** the script exits 0 with zero diagnostics
**Verified by:** `packages/react/react-ui-pack-auth/src/pack/pack.translation-key-conformance.test.ts::"string-literal label fails typecheck"` (TypeScript expect-error fixture) AND the two lint gates above wired into `bunx turbo lint --filter=@suaveplan/react-ui-pack-auth`
---
### Requirement: onRegister and onUnregister fire on PackRegistryProvider mount and unmount
`authPack.onRegister(ctx)` SHALL be called by `<PackRegistryProvider>` exactly once when the pack enters the `packs` prop, with a `PackContext` exposing `i18n`, `theme`, `logger`, and a pack-scoped `AbortSignal`. The hook SHALL call `ctx.i18n.addResourceBundle('react-ui-pack-auth', authPack.translations[ctx.i18n.locale])` so the message catalog is warm before the first form-render. `authPack.onUnregister(ctx)` SHALL be called exactly once when the pack leaves the `packs` prop and SHALL call `ctx.i18n.removeResourceBundle('react-ui-pack-auth', ctx.i18n.locale)`.
#### Scenario: onRegister adds the locale bundle
- **GIVEN** an `<PackRegistryProvider packs={[authPack]}>` with a fresh `I18nInstance` whose locale is `en-GB`
- **WHEN** the provider mounts
- **THEN** `i18n.addResourceBundle` was called once with `('react-ui-pack-auth', authPack.translations['en-GB'])` AND `pack.auth.lifecycle_total{phase=register, outcome=success}` counter incremented by 1
#### Scenario: onUnregister removes the locale bundle
- **GIVEN** a mounted `<PackRegistryProvider packs={[authPack]}>`
- **WHEN** the provider unmounts
- **THEN** `i18n.removeResourceBundle` was called once with `('react-ui-pack-auth', 'en-GB')` AND `pack.auth.lifecycle_total{phase=unregister, outcome=success}` counter incremented by 1
#### Scenario: pack-scoped AbortSignal aborts on unmount
- **GIVEN** an `onRegister` that captures `ctx.signal`
- **WHEN** the provider unmounts before any in-flight async work resolves
- **THEN** `ctx.signal.aborted === true` AND any AbortController-aware operation observes the abort
**Verified by:** `packages/react/react-ui-pack-auth/src/pack/pack.lifecycle.test.tsx::"onRegister adds the bundle"` AND `packages/react/react-ui-pack-auth/src/pack/pack.lifecycle.test.tsx::"onUnregister removes the bundle"` AND `packages/react/react-ui-pack-auth/src/pack/pack.lifecycle.test.tsx::"pack-scoped AbortSignal aborts on unmount"`
---
### Requirement: translation contributions register via TypeScript module augmentation
The pack SHALL contribute its translation keys into `@suaveplan/react-i18n`'s `ContributedTranslationKeys` interface via `declare module "@suaveplan/react-i18n" { interface ContributedTranslationKeys { 'react-ui-pack-auth.login.title': true; ... } }`. The augmentation file SHALL be auto-generated from `src/translations/en-GB.json` (the canonical source) by `bun scripts/generate-translation-types.ts` and SHALL be byte-identical across runs. The augmentation file SHALL be imported as a side-effect-only entry from the package's root `index.ts` so `import "@suaveplan/react-ui-pack-auth"` registers the keys with the type system.
#### Scenario: every key in en-GB.json appears in the augmentation file
- **WHEN** the test compares the keys in `src/translations/en-GB.json` against the keys augmented into `ContributedTranslationKeys`
- **THEN** the two sets are equal (no missing keys, no extra keys)
#### Scenario: typing a literal key from en-GB.json typechecks
- **GIVEN** any key in `src/translations/en-GB.json` (e.g. `'react-ui-pack-auth.login.title'`)
- **WHEN** consumer code declares `const k: TranslationKey = '<that-literal>'`
- **THEN** the assignment typechecks without error
#### Scenario: typing a key absent from en-GB.json fails typecheck
- **WHEN** consumer code declares `const k: TranslationKey = 'react-ui-pack-auth.does-not-exist'`
- **THEN** the assignment fails typecheck with `Type '...' is not assignable to type 'TranslationKey'`
**Verified by:** `packages/react/react-ui-pack-auth/src/translations/translations.test.ts::"augmentation mirrors en-GB.json"` AND a `*.expect-error.ts` fixture proving both the positive and negative typecheck scenarios above
---
### Requirement: pack ships translations in five locales with full key coverage
`authPack.translations` SHALL provide complete key coverage for `en-GB`, `zh-CN`, `ar-AE`, `cy-GB`, and `ru-RU`. The locale-completeness gate (`scripts/lint/check-locale-completeness.ts`) SHALL pass with zero diagnostics — every key in `en-GB.json` SHALL exist in each of the four other locale files. Welsh six-form plurals (`zero | one | two | few | many | other`) SHALL be exercised by the OTP "X attempts remaining" string in `src/translations/cy-GB.json`.
#### Scenario: every locale has every key
- **WHEN** `bun scripts/lint/check-locale-completeness.ts` runs against the pack
- **THEN** the script exits 0 with zero diagnostics
#### Scenario: Welsh six-form plurals all resolve
- **GIVEN** the OTP "X attempts remaining" key parsed by `MessageParser`
- **WHEN** the test resolves the plural for `count = 0, 1, 2, 3, 6, 4`
- **THEN** the categories returned are `zero, one, two, few, many, other` respectively AND each maps to a distinct translated string in `cy-GB.json`
**Verified by:** `packages/react/react-ui-pack-auth/src/translations/translations.test.ts::"every locale has every key"` AND `packages/react/react-ui-pack-auth/src/translations/translations.test.ts::"Welsh six-form plurals resolve"`
---
### Requirement: pack does not depend on Radix or any DOM library
The pack's `package.json` SHALL list `@suaveplan/react-ui` and `@suaveplan/react-i18n` as `peerDependencies` only — never as runtime `dependencies`. The pack SHALL NOT import any `@radix-ui/*` package directly, SHALL NOT import any DOM-manipulation library (`@floating-ui/*`, `framer-motion`, `react-aria`, `vaul`, `cmdk`), and SHALL NOT import `react-hook-form` directly. Every Radix primitive and every DOM concern reaches the pack transitively through `@suaveplan/react-ui`'s peer surface; every form-engine concern reaches it through `@suaveplan/react-forms`.
#### Scenario: peer-dep policy is honoured
- **WHEN** the test reads `packages/react/react-ui-pack-auth/package.json`
- **THEN** `dependencies` does NOT contain any `@suaveplan/react-ui*` or `@suaveplan/react-i18n` entry AND `peerDependencies` contains both with `workspace:^`
#### Scenario: no Radix or form-library import
- **WHEN** the test runs `grep -rn "from ['\"]\(@radix-ui\|@floating-ui\|framer-motion\|react-aria\|vaul\|cmdk\|react-hook-form\)" packages/react/react-ui-pack-auth/src/`
- **THEN** the grep returns zero matches
**Verified by:** `packages/react/react-ui-pack-auth/src/pack/pack.dependency-conformance.test.ts::"peer-dep policy"` AND `packages/react/react-ui-pack-auth/src/pack/pack.dependency-conformance.test.ts::"no Radix or form-library import"`
---
### Requirement: each form takes a typed AuthSubmitClient and onSuccess / onError callbacks
Each `<Form>Props` interface SHALL include `submitClient: AuthSubmitClient` (or pull from `<AuthSubmitClientProvider>` via `useAuthSubmitClient()` when the prop is omitted), `onSuccess: (result: <FormOutput>) => void`, and `onError?: (error: SuaveplanError) => void`. The pack SHALL NOT call `window.location.assign`, SHALL NOT push to a router, SHALL NOT touch `localStorage` or `sessionStorage`, and SHALL NOT depend on any global session singleton. When neither the prop nor the provider supplies an `AuthSubmitClient`, the form SHALL throw `ReactUiPackAuthError(REACT_UI_PACK_AUTH.SUBMIT_CLIENT_MISSING)` at render time.
#### Scenario: typed onSuccess receives the schema-typed payload
- **GIVEN** a `<LoginForm submitClient={stub} onSuccess={spy} />` whose stub resolves with a `LoginOutput`-shaped value
- **WHEN** the form is submitted with valid credentials
- **THEN** the spy is called once with a value that satisfies `LoginOutput.parse(value)`
#### Scenario: missing AuthSubmitClient throws SUBMIT_CLIENT_MISSING
- **GIVEN** a `<LoginForm onSuccess={spy} />` rendered with no `submitClient` prop and no `<AuthSubmitClientProvider>` ancestor
- **WHEN** the test renders the component
- **THEN** the render throws `ReactUiPackAuthError(REACT_UI_PACK_AUTH.SUBMIT_CLIENT_MISSING)`
#### Scenario: onError receives a SuaveplanError when the submit client throws
- **GIVEN** a stub `AuthSubmitClient.login` that rejects with `new SuaveplanError({ code: 'AUTH.INVALID_CREDENTIALS' })`
- **WHEN** the form submits with valid credentials
- **THEN** `onError` is called once with the original error AND the form renders the `react-ui-pack-auth.login.error.AUTH.INVALID_CREDENTIALS.body` translation key as a form-level alert
**Verified by:** `packages/react/react-ui-pack-auth/src/components/login-form/login-form.test.tsx::"onSuccess receives typed payload"` AND `packages/react/react-ui-pack-auth/src/components/login-form/login-form.test.tsx::"missing AuthSubmitClient throws SUBMIT_CLIENT_MISSING"` AND `packages/react/react-ui-pack-auth/src/components/login-form/login-form.test.tsx::"onError receives SuaveplanError"`