Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,106 @@
# @suaveplan/react-auth-whitelabel - White-label authentication React components with tenant branding wired through @suaveplan/react-ui runtime theme mutator
White-label authentication React components with tenant branding wired
through the `@suaveplan/react-ui` runtime theme-token mutator.
<!-- begin:auto-docs -->
## Documentation tree
- [login-form](./src/components/login-form/login-form.md) — `<LoginForm>` is the white-label login surface consumed by every tenant in the stack. It renders a controlled e…
- [login-form](./src/components/login-form/login-form.md) — `<LoginForm>` is the white-label login surface consumed by every tenant in the stack. It renders a controlled e…
- [password-reset-form](./src/components/password-reset-form/password-reset-form.md) — `<PasswordResetForm>` is the white-label password-reset surface. It owns a single state machine across two modes — `req…
- [password-reset-confirm](./src/components/password-reset-form/password-reset-confirm.md) — `PasswordResetConfirm` is the confirm-mode body of `<PasswordResetForm>` — the screen the user lands on from the email…
- [password-reset-form](./src/components/password-reset-form/password-reset-form.md) — `<PasswordResetForm>` is the white-label password-reset surface. It owns a single state machine across two modes — `req…
- [password-reset-request](./src/components/password-reset-form/password-reset-request.md) — `PasswordResetRequest` is the request-mode body of `<PasswordResetForm>` — the screen the user lands on when they click…
- [password-reset-success](./src/components/password-reset-form/password-reset-success.md) — `PasswordResetSuccess` is the success state of `<PasswordResetForm>`, shared between the request and confirm modes. It…
- [register-form](./src/components/register-form/register-form.md) — `<RegisterForm>` is the white-label self-registration surface. It renders the header (logo + title + subtitle), the inl…
- [register-form-fields](./src/components/register-form/register-form-fields.md) — `RegisterFormFields` is the inner field block of `<RegisterForm>` — name (optional, optionally required), email, passwo…
- [register-form-terms](./src/components/register-form/register-form-terms.md) — `RegisterFormTerms` is the terms-acceptance checkbox block of `<RegisterForm>`. It renders the acceptance checkbox, the…
- [register-form](./src/components/register-form/register-form.md) — `<RegisterForm>` is the white-label self-registration surface. It renders the header (logo + title + subtitle), the inl…
- [auth-context](./src/context/auth-context.md) — `auth-context.tsx` ships the `<AuthProvider>` React context provider together with the three hooks `useAuth`, `useUser`…
- [auth-http](./src/context/auth-http.md) — `auth-http.ts` houses the three helpers every `AuthProvider` verb shares: `resolveFetch`, `resolveNavigate`, and `postJ…
- [branding-context](./src/context/branding-context.md) — `branding-context.tsx` ships the `<BrandingProvider>` React context provider together with the hooks `useBranding`, `us…
- [error-codes](./src/errors/error-codes.md) — `error-codes.ts` registers the canonical, stable error-code registry used by every error thrown from `@suaveplan/react-…
- [react-auth-whitelabel-error](./src/errors/react-auth-whitelabel-error.md) — `ReactAuthWhitelabelError` is the canonical error class for every error thrown by `@suaveplan/react-auth-whitelabel`. I…
- [types](./src/types/branding.schema.md) — `TenantBrandingSchema` is the Zod schema that validates every `TenantBranding` payload entering the package from an unt…
- [branding.schema](./src/types/branding.schema.md) — `TenantBrandingSchema` is the Zod schema that validates every `TenantBranding` payload entering the package from an unt…
- [utils](./src/utils/navigate-or-call.md) — `navigateOrCall(target)` is the canonical helper used by every form component in this package to act on a "URL or click…
- [navigate-or-call](./src/utils/navigate-or-call.md) — `navigateOrCall(target)` is the canonical helper used by every form component in this package to act on a "URL or click…
<!-- end:auto-docs -->
## What ships
- **Forms** — `LoginForm`, `RegisterForm`, `PasswordResetForm`
- **Contexts** — `AuthProvider`, `BrandingProvider`
- **Hooks** — `useAuth`, `useBranding`, `useIsAuthenticated`, `useLogo`,
`useTenantBranding`, `useUser`
- **Types** — `AuthConfig`, `AuthContextValue`, `AuthUser`,
`BrandingContextValue`, `BrandingProviderProps`, `RegisterData`,
`TenantBranding`, `TenantBrandingSchema`
- **Errors** — `REACT_AUTH_WL_CODES` + `ReactAuthWhitelabelError`
## Quick start
`BrandingProvider` calls `useThemeMutator()` from
`@suaveplan/react-ui/theme`. That hook MUST be invoked inside a
`<ThemeProvider>`. Wrap your subtree like this:
```tsx
import { ThemeProvider, lightTheme } from "@suaveplan/react-ui/theme";
import {
AuthProvider,
BrandingProvider,
LoginForm,
} from "@suaveplan/react-auth-whitelabel";
<ThemeProvider themes={[lightTheme]} defaultTheme="light">
<BrandingProvider
brandingEndpoint="/api/branding"
tenantId={tenantSlug}
>
<AuthProvider config={{ authUrl: "/api/auth" }}>
<LoginForm
onForgotPassword={() => router.push("/forgot")}
onRegister={() => router.push("/register")}
/>
</AuthProvider>
</BrandingProvider>
</ThemeProvider>;
```
On every branding change, `BrandingProvider` writes `--brand-primary`,
`--brand-secondary`, `--brand-accent`, and `--brand-font` to
`document.documentElement.style` via
`useThemeMutator().setExtensionTokens(...)`. Cleanup is automatic on
unmount.
## Modules
- [`src/components/login-form/login-form.md`](./src/components/login-form/login-form.md)
- [`src/components/register-form/register-form.md`](./src/components/register-form/register-form.md)
- [`src/components/register-form/register-form-fields.md`](./src/components/register-form/register-form-fields.md)
- [`src/components/register-form/register-form-terms.md`](./src/components/register-form/register-form-terms.md)
- [`src/components/password-reset-form/password-reset-form.md`](./src/components/password-reset-form/password-reset-form.md)
- [`src/components/password-reset-form/password-reset-request.md`](./src/components/password-reset-form/password-reset-request.md)
- [`src/components/password-reset-form/password-reset-confirm.md`](./src/components/password-reset-form/password-reset-confirm.md)
- [`src/components/password-reset-form/password-reset-success.md`](./src/components/password-reset-form/password-reset-success.md)
- [`src/context/auth-context.md`](./src/context/auth-context.md)
- [`src/context/auth-http.md`](./src/context/auth-http.md)
- [`src/context/branding-context.md`](./src/context/branding-context.md)
- [`src/types/branding.schema.md`](./src/types/branding.schema.md)
- [`src/errors/error-codes.md`](./src/errors/error-codes.md)
- [`src/errors/react-auth-whitelabel-error.md`](./src/errors/react-auth-whitelabel-error.md)
## Out of scope (Excluded)
- MFA / passkey / WebAuthn flows — deferred until `@suaveplan/webauthn`
- `@suaveplan/otp` ship .
- Magic-link / SMS OTP — depends on `@suaveplan/security-codes` /
`@suaveplan/emailer` / `@suaveplan/sms`.
- Better-Auth first-class adapter — overlaps with the concurrent
`@suaveplan/react-auth` Wave 1.
- Audit-log integration, password-strength meter, rate limiter, axe-core
story gate — depend on packages not yet .
- Bundle-size snapshot — repo-wide tooling not yet wired.
- Playwright E2E specs — see `e2e/README.md`.

View File

@@ -0,0 +1,22 @@
# E2E specs — deferred
### Excluded
The three upstream Playwright specs at
`/mnt/s/web/suaveplan/packages/react/react-auth-whitelabel/e2e/{login-form,register-form,password-reset-form}.spec.ts`
were not ported in the initial migration. They rely on a running Storybook
instance at `/iframe.html?id=components-loginform--default&viewMode=story`
and on Playwright wiring that has not yet landed in Suaveplan.
Once `@suaveplan/storybook-test-utils` (referenced in the enterprise plan
§4 as a soft dep) ships and `playwright.config.ts` is wired at the repo
root for the React stack, port the three specs verbatim — every selector
already matches the JSX output (labels, `role="alert"`, button
names are identical to upstream because the `useTranslation()` default
values are byte-identical).
Manual smoke coverage in the interim:
```
bun run --filter=@suaveplan/react-auth-whitelabel build-storybook
# then open the served bundle and walk every story
```

View File

@@ -0,0 +1,74 @@
# `<LoginForm>`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`<LoginForm>` is the white-label login surface consumed by every tenant in
the stack. It renders a controlled email + password form with
optional remember-me checkbox, optional OAuth provider list, and optional
forgot-password / register-link affordances. The component pulls its visible
strings from `useTranslation()` so every label is localised, sources its
logo + tenant name from `useBranding()` + `useLogo()`, and delegates the
actual auth call to `useAuth().login(...)` unless the caller supplies an
`onSubmit` override. Submission errors are rendered inline in a `role="alert"`
region; provider-button clicks invoke `useAuth().loginWithProvider(...)`.
## Features
- Fully controlled inputs — no `react-hook-form` dependency.
- `useTranslation()` with `{ defaultValue }` for every visible string.
- Optional OAuth provider button list.
- Optional remember-me, forgot-password, register-link affordances.
- `onSubmit` override for plug-in flows (Better Auth, custom REST).
- Custom logo, title, subtitle overrides.
- Disabled state propagates to every input + button.
## Usage: Basic
```tsx
<LoginForm
onForgotPassword={() => router.push("/forgot")}
onRegister={() => router.push("/register")}
/>
```
## Usage: Advanced
```tsx
<LoginForm
providers={[
{ id: "google", name: "Google", icon: GoogleIcon },
{ id: "github", name: "GitHub", icon: "https://cdn/github.svg" },
]}
showRememberMe
onSubmit={async ({ email, password, rememberMe }) => {
await betterAuthClient.signIn.email({ email, password, rememberMe });
}}
onSuccess={() => router.push("/dashboard")}
onError={(err) => analytics.track("login_error", { code: err.code })}
logo={<TenantLogo />}
title={t("custom.title")}
subtitle={t("custom.subtitle")}
/>
```
## API Reference
- `LoginForm(props: LoginFormProps): ReactNode` — props on
`login-form.types.ts`.
- Returns a `role="alert"` region on error; a `role="form"` is implied via
the native `<form>` element.
## Implementation Notes
The OAuth list is hidden when `providers.length === 0`, so a tenant with no
configured providers gets a clean form. The remember-me checkbox is a UI
hint only — the actual semantics live with the backend the consumer wires
through `AuthConfig.fetch`. Provider-icon rendering supports both a React
component (`provider.icon` as `ComponentType<{ className?: string }>`) and
a URL string (`<img src={...} />`). The `navigateOrCall` helper inside the
file handles the string-or-handler shape on every link prop so consumers can
pass a URL or an in-app router callback.

View File

@@ -0,0 +1,70 @@
# `<PasswordResetConfirm>`
## Purpose
`PasswordResetConfirm` is the confirm-mode body of `<PasswordResetForm>` —
the screen the user lands on from the email link with a `token` query
parameter. It renders the new-password input, the confirm-password input,
the submit button (with translated in-flight copy), and an optional
back-to-login affordance. It is a separate named sub-component so the
parent stays under the 300-LOC hard budget and so the request/confirm
markup can be tested independently. The parent owns the `token` and the
network call; this component is purely the controlled-input shell.
## Features
- Two controlled inputs driven by the parent.
- Browser-native `minLength={minPasswordLength}` on the password field
supplements the parent's JS validation.
- Submit copy toggles via the parent's `isSubmitting` flag.
- Optional back-to-login affordance.
## Usage: Basic
```tsx
<PasswordResetConfirm
password={password}
setPassword={setPassword}
confirmPassword={confirmPassword}
setConfirmPassword={setConfirmPassword}
onSubmit={handleSubmit}
minPasswordLength={8}
isDisabled={false}
isSubmitting={false}
showLoginLink
/>
```
## Usage: Advanced
```tsx
<PasswordResetConfirm
password={password}
setPassword={setPassword}
confirmPassword={confirmPassword}
setConfirmPassword={setConfirmPassword}
onSubmit={async (e) => {
e.preventDefault();
await resetPassword(token, password);
setIsSuccess(true);
}}
minPasswordLength={12}
isDisabled={isLoading}
isSubmitting={isSubmitting}
showLoginLink
onLogin={() => router.push("/login")}
/>
```
## API Reference
- `PasswordResetConfirm(props: PasswordResetConfirmProps): ReactNode` —
see `password-reset-form.types.ts` for the prop shape.
## Implementation Notes
The component intentionally does NOT receive the reset token — the parent
holds it and either threads it into the `resetPassword(token, password)`
call or surfaces a `MISSING_RESET_TOKEN` error in the parent's `role="alert"`
region BEFORE handing off to this sub-component. Splitting that policy
out keeps the confirm-body purely concerned with the controlled inputs.

View File

@@ -0,0 +1,79 @@
# `<PasswordResetForm>`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`<PasswordResetForm>` is the white-label password-reset surface. It owns a
single state machine across two modes — `request` (email entry) and
`confirm` (new password entry with a reset token) — and a shared success
state. The component delegates the request-mode body to
`<PasswordResetRequest>`, the confirm-mode body to
`<PasswordResetConfirm>`, and the success view to
`<PasswordResetSuccess>`, keeping each rendered slice independently
testable and the parent file under the 300-LOC hard budget. Confirm mode
short-circuits with an inline `MISSING_RESET_TOKEN` error when the
`token` prop is missing — the network is never touched.
## Features
- Dual-mode form sharing a single state machine.
- Local validation (min length, confirm-password match, missing token)
blocks submission before any network call.
- Optional `onSubmit` override.
- Mode-aware default titles, subtitles, and success messages — every
string sourced from `useTranslation()`.
- Optional back-to-login affordance shared by every mode.
## Usage: Basic
```tsx
<PasswordResetForm mode="request" onLogin="/login" />
```
```tsx
<PasswordResetForm
mode="confirm"
token={new URLSearchParams(location.search).get("token") ?? undefined}
onLogin="/login"
/>
```
## Usage: Advanced
```tsx
<PasswordResetForm
mode={mode}
token={token}
minPasswordLength={12}
onSubmit={async (values) => {
if ("email" in values) {
await betterAuthClient.forgetPassword({ email: values.email });
} else {
await betterAuthClient.resetPassword({ token, password: values.password });
}
}}
successMessage={customSuccess}
onSuccess={() => analytics.track("password_reset_success")}
onError={(err) => analytics.track("password_reset_error", { code: err.code })}
/>
```
## API Reference
- `PasswordResetForm(props: PasswordResetFormProps): ReactNode` — see
`password-reset-form.types.ts`. `minPasswordLength` defaults to 8;
`mode` defaults to `"request"`.
## Implementation Notes
The success state is a single boolean — both modes share the same
`isSuccess` so the success view renders identically regardless of which
mode produced it. The mode-specific copy lives in lookup tables
(`defaultTitles`, `defaultSubtitles`, `defaultSuccessMessages`) keyed by
the `mode` discriminator. Submission errors are surfaced inline AND
forwarded to the `onError` callback; consumers that want analytics
without UI side-effects can ignore the inline error region by overriding
its styling.

View File

@@ -0,0 +1,64 @@
# `<PasswordResetRequest>`
## Purpose
`PasswordResetRequest` is the request-mode body of `<PasswordResetForm>` —
the screen the user lands on when they click "Forgot password?". It
renders the email input, the submit button (with a translated
"Sending..." copy while the network call is in flight), and an optional
back-to-login affordance. It exists as a named sub-component so the parent
`password-reset-form.tsx` stays under the 300-LOC hard budget while
keeping the request-vs-confirm modes structurally separate.
## Features
- Single controlled email input driven by the parent.
- Submit button copy toggles between "Send Reset Link" / "Sending..."
via the parent's `isSubmitting` flag.
- Optional back-to-login affordance (URL or handler).
- Every visible string sourced from `useTranslation()`.
## Usage: Basic
```tsx
<PasswordResetRequest
email={email}
setEmail={setEmail}
onSubmit={handleSubmit}
isDisabled={false}
isSubmitting={false}
showLoginLink
onLogin={() => router.push("/login")}
/>
```
## Usage: Advanced
```tsx
<PasswordResetRequest
email={email}
setEmail={setEmail}
onSubmit={async (e) => {
e.preventDefault();
await requestPasswordReset(email);
setIsSuccess(true);
}}
isDisabled={isLoading}
isSubmitting={isSubmitting}
showLoginLink
onLogin="https://app.example.com/login"
/>
```
## API Reference
- `PasswordResetRequest(props: PasswordResetRequestProps): ReactNode` —
see `password-reset-form.types.ts`.
## Implementation Notes
The component takes the submit handler as a prop instead of owning it,
because the parent form needs to coordinate request-vs-confirm submissions
through a single `isSubmitting` / `isSuccess` state machine. The
`navigateOrCall` helper handles both string-URL and callback shapes for
`onLogin` so consumers can pass either.

View File

@@ -0,0 +1,52 @@
# `<PasswordResetSuccess>`
## Purpose
`PasswordResetSuccess` is the success state of `<PasswordResetForm>`,
shared between the request and confirm modes. It renders a mode-specific
heading ("Check Your Email" for request mode, "Password Reset Complete"
for confirm mode), the success copy in a `role="status"` live region,
and an optional back-to-login button. The component is intentionally
stateless — it is purely a presentational view of the parent's success
state machine, which lets the parent file stay under the 300-LOC budget.
## Features
- Mode-specific success heading.
- `role="status"` live region for assistive technology.
- Optional back-to-login affordance (URL or handler).
## Usage: Basic
```tsx
<PasswordResetSuccess
mode="request"
message={t("auth.forgotPassword.successMessage", { defaultValue: "..." })}
showLoginLink
onLogin="/login"
/>
```
## Usage: Advanced
```tsx
<PasswordResetSuccess
mode={mode}
message={successMessage ?? defaultSuccessMessages[mode]}
showLoginLink={showLoginLink}
onLogin={onLogin}
/>
```
## API Reference
- `PasswordResetSuccess(props: PasswordResetSuccessProps): ReactNode` —
see `password-reset-form.types.ts`.
## Implementation Notes
`role="status"` is the WAI-ARIA pattern for non-interruptive live
announcements — assistive technologies will announce the success copy
without stealing focus. The parent owns the message string so a tenant
override (passed via `successMessage`) flows through this component
without any translation indirection.

View File

@@ -0,0 +1,64 @@
# `<RegisterFormFields>`
## Purpose
`RegisterFormFields` is the inner field block of `<RegisterForm>` — name
(optional, optionally required), email, password, and confirm-password
inputs together with their labels and required indicators. It exists as a
named sub-component so the parent `register-form.tsx` stays under the
300-LOC hard budget while keeping every field's translation key, autocomplete
attribute, and placeholder co-located with the input element. All four
inputs are fully controlled by the parent — every value + setter pair is
passed in through props.
## Features
- Fully controlled inputs driven by parent setters.
- `autocomplete` set per field (`name`, `email`, `new-password`).
- Required indicators driven by `requireName` and the always-required
email + password.
- `minLength={minPasswordLength}` on the password field — supplements the
parent's JS validation with browser-native min-length check.
- Every label/placeholder routed through `useTranslation()` with
`{ defaultValue }`.
## Usage: Basic
```tsx
<form>
<RegisterFormFields {...fieldProps} />
</form>
```
## Usage: Advanced
```tsx
<RegisterFormFields
name={name}
setName={setName}
email={email}
setEmail={setEmail}
password={password}
setPassword={setPassword}
confirmPassword={confirmPassword}
setConfirmPassword={setConfirmPassword}
showNameField
requireName
minPasswordLength={12}
isDisabled={isLoading || isSubmitting}
/>
```
## API Reference
- `RegisterFormFields(props: RegisterFormFieldsProps): ReactNode` — see
`register-form.types.ts` for the prop shape.
## Implementation Notes
The component is pure — it renders state but does not own it. The parent
form handles submission, error rendering, and submit-button copy. Splitting
the field block from the form shell isolates the markup churn (which is
mostly localised string keys) from the submission logic, making both files
easier to audit. Coverage is exercised end-to-end via the parent form's
test file.

View File

@@ -0,0 +1,58 @@
# `<RegisterFormTerms>`
## Purpose
`RegisterFormTerms` is the terms-acceptance checkbox block of
`<RegisterForm>`. It renders the acceptance checkbox, the configurable
terms-of-service affordance (rendered as a link when `termsUrl` is supplied,
plain text otherwise), and an optional privacy-policy link. Splitting it
into its own sub-component keeps the parent form under the 300-LOC budget
while making the (slightly intricate) "link OR text" markup easy to test in
isolation. When `showTermsCheckbox` is `false`, the component renders
`null` so the parent does not need to wrap it in a conditional.
## Features
- Checkbox is fully controlled by the parent.
- Terms text renders as `<a target="_blank" rel="noopener noreferrer">`
when `termsUrl` is supplied, plain inline text otherwise.
- Optional privacy-policy `<a>` appended after a translated "and" connector.
- Renders `null` when `showTermsCheckbox` is `false`.
## Usage: Basic
```tsx
<RegisterFormTerms
acceptTerms={acceptTerms}
setAcceptTerms={setAcceptTerms}
showTermsCheckbox
isDisabled={false}
/>
```
## Usage: Advanced
```tsx
<RegisterFormTerms
acceptTerms={acceptTerms}
setAcceptTerms={setAcceptTerms}
showTermsCheckbox
termsUrl="https://example.com/terms"
privacyUrl="https://example.com/privacy"
isDisabled={isSubmitting}
/>
```
## API Reference
- `RegisterFormTerms(props: RegisterFormTermsProps): ReactNode` — props on
`register-form.types.ts`. Returns `null` when the checkbox is hidden.
## Implementation Notes
The "and" connector is a separate translation key so the surrounding
fragment composes cleanly in locales where the connector is more than a
word (e.g. Welsh "ac" vs Spanish "y"). The component intentionally keeps no
state of its own — the parent form is the single source of truth for the
`acceptTerms` flag, and the parent's submit validator gates submission on
that flag.

View File

@@ -0,0 +1,66 @@
# `<RegisterForm>`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`<RegisterForm>` is the white-label self-registration surface. It renders
the header (logo + title + subtitle), the inline error region, the field
block (delegated to `RegisterFormFields`), the terms checkbox (delegated
to `RegisterFormTerms`), the submit button, an optional OAuth provider
list, and an optional back-to-login affordance. The component validates
password length, password equality, name presence (when `requireName`),
and terms acceptance BEFORE calling `useAuth().register(...)` (or the
`onSubmit` override). Every validation error renders inline in the
`role="alert"` region and short-circuits the auth call so the network is
never touched until the form is locally clean.
## Features
- Local validation: min password length, confirm-password match,
required name, terms acceptance.
- Field block + terms block extracted into sub-components so this file
stays under the 300-LOC hard budget.
- `onSubmit` override for plug-in flows.
- Optional OAuth providers, optional back-to-login link.
- Custom logo / title / subtitle.
## Usage: Basic
```tsx
<RegisterForm onLogin={() => router.push("/login")} />
```
## Usage: Advanced
```tsx
<RegisterForm
showNameField
requireName
minPasswordLength={12}
termsUrl="https://example.com/tos"
privacyUrl="https://example.com/privacy"
providers={[{ id: "google", name: "Google" }]}
onSubmit={async (values) => {
await betterAuthClient.signUp.email(values);
}}
onSuccess={() => router.push("/welcome")}
onError={(err) => analytics.track("signup_error", { code: err.code })}
/>
```
## API Reference
- `RegisterForm(props: RegisterFormProps): ReactNode` — see
`register-form.types.ts`. `minPasswordLength` defaults to 8.
## Implementation Notes
The validator is intentionally pre-`fetch` so a network round-trip never
runs against an invalid local form. The translator routes every error
message through `t(..., { defaultValue })` so a tenant overriding the
default English copy will see their override even on local validation.
The sub-component split is a budget-only concern; the parent owns all
state and submission logic.

View File

@@ -0,0 +1,75 @@
# `<AuthProvider>` and `useAuth` / `useUser` / `useIsAuthenticated`
## Purpose
`auth-context.tsx` ships the `<AuthProvider>` React context provider together
with the three hooks `useAuth`, `useUser`, and `useIsAuthenticated`. The
provider wires a tenant-aware auth backend through an injectable `fetch`
(`FetchLike`) and an injectable `navigate(url)` seam so consumers can plug
real backends in production and stub them in tests without `vi.mock`. Every
verb — `login`, `loginWithProvider`, `register`, `logout`,
`requestPasswordReset`, `resetPassword` — is surfaced on the context value
returned by `useAuth()` and translates HTTP failures into stable, code-tagged
errors via `ReactAuthWhitelabelError`.
## Features
- Injectable `fetch: FetchLike` for testability — defaults to
`globalThis.fetch`.
- Injectable `navigate(url)` seam — defaults to `window.location.href = url`
in browsers, no-op in non-browser environments.
- Session probe on mount unless `initialUser` is supplied (SSR/hydration).
- Network failures → `REACT_AUTH_WL.AUTH_NETWORK_FAILED`.
- Non-ok responses → `REACT_AUTH_WL.AUTH_REJECTED` with the backend's
`message` field threaded through when present.
- Hooks called outside the provider throw
`REACT_AUTH_WL.PROVIDER_OUTSIDE_TREE`.
## Usage: Basic
```tsx
<AuthProvider config={{ authUrl: "/api/auth" }}>
<App />
</AuthProvider>
```
```tsx
const { login, user, isAuthenticated } = useAuth();
await login("a@b.c", "pw");
```
## Usage: Advanced
```tsx
const config: AuthConfig = {
authUrl: "https://auth.acme.com",
redirectUrl: "/dashboard",
fetch: customFetch, // testable seam
navigate: (url) => router.navigate(url),
providers: [{ id: "google", name: "Google" }],
};
<AuthProvider config={config} initialUser={ssrUser}>
<Routes>...</Routes>
</AuthProvider>
```
## API Reference
- `AuthProvider({ config, initialUser?, children })` — context provider.
- `useAuth()` — returns `AuthContextValue` (`user`, `isLoading`,
`isAuthenticated`, `error`, `login`, `loginWithProvider`, `register`,
`logout`, `requestPasswordReset`, `resetPassword`).
- `useUser()` — returns `AuthUser | null`.
- `useIsAuthenticated()` — returns `boolean`.
## Implementation Notes
The session probe in `useEffect` swallows transport errors on purpose — a
session check that fails should not surface as a visible error to the user
(they simply remain anonymous). Verbs called from the UI translate failures
to typed errors AND surface them on the context's `error` state so a single
error region can render the current failure without each call site
re-implementing the same try/catch. Every verb sets `isLoading` true/false
around the network call so consumers can disable submit buttons via
`isLoading || isSubmitting`.

View File

@@ -0,0 +1,63 @@
# `auth-http.ts` — fetch + navigate seam helpers
## Purpose
`auth-http.ts` houses the three helpers every `AuthProvider` verb shares:
`resolveFetch`, `resolveNavigate`, and `postJson`. Each helper exists so the
`AuthProvider` body stays under the 300-LOC budget while keeping the
network-translation policy in a single, testable file. Failures from the
configured `fetch` are translated to `REACT_AUTH_WL.AUTH_NETWORK_FAILED`;
non-ok responses are translated to `REACT_AUTH_WL.AUTH_REJECTED` with the
backend-supplied `message` field threaded through the i18n message key when
present.
## Features
- `resolveFetch(config)` — pick the injected `fetch` over `globalThis.fetch`.
- `resolveNavigate(config)` — pick the injected `navigate(url)` over the
default `window.location.href` writer.
- `postJson(fetchImpl, url, body, rejectMessage, options?)` — single helper
for every JSON POST, with error translation, optional credentials, and
best-effort `.json()` recovery on failure responses.
## Usage: Basic
```ts
const fetchImpl = resolveFetch(config);
const navigate = resolveNavigate(config);
const data = await postJson(fetchImpl, url, body, "ui.label.fallback_msg");
```
## Usage: Advanced
```ts
try {
await postJson(fetchImpl, `${config.authUrl}/login`, { email, password },
"ui.login.rejected", { credentials: "include" });
} catch (err) {
if (err.code === REACT_AUTH_WL_CODES.AUTH_NETWORK_FAILED) {
showOfflineBanner();
} else if (err.code === REACT_AUTH_WL_CODES.AUTH_REJECTED) {
focusErrorRegion();
}
throw err;
}
```
## API Reference
- `resolveFetch(config: AuthConfig): FetchLike` — never throws; falls back to
`globalThis.fetch` when the config omits one.
- `defaultNavigate(url: string): void` — no-op when `window` is undefined.
- `resolveNavigate(config: AuthConfig): (url: string) => void` — picks the
injected seam or the default.
- `postJson(fetchImpl, url, body, rejectMessage, options?)` — returns the
parsed JSON body. Errors are subclasses of `ReactAuthWhitelabelError`.
## Implementation Notes
`postJson` swallows `.json()` parse errors on failure responses on purpose:
some backends return an empty 401/403 body, in which case the `rejectMessage`
fallback is the user-visible label. The `.json()` parse on success is also
swallowed and returns `{}` so a 204-style "no content" response keeps a
clean caller signature.

View File

@@ -0,0 +1,79 @@
# `<BrandingProvider>` and tenant branding hooks
## Purpose
`branding-context.tsx` ships the `<BrandingProvider>` React context provider
together with the hooks `useBranding`, `useTenantBranding`, and `useLogo`.
The provider's job is to resolve a `TenantBranding` payload (either static
via prop or fetched from a configurable endpoint) and to project that
payload onto two surfaces: (1) the `@suaveplan/react-ui` runtime
theme-token layer for every `--brand-*` CSS custom property, and (2) the
non-CSS DOM chrome — `document.title`, `<link rel="icon">`, and the tenant
custom-CSS `<style>` injection. The CSS path goes through
`useThemeMutator().setExtensionTokens(...)` so per-tenant tokens stack on
top of the active chassis theme exactly like any other mutator layer, with
automatic cleanup on unmount.
## Features
- Static branding via prop OR endpoint fetch via `brandingEndpoint` (+
optional `/tenantId` segment).
- Injectable `fetch: FetchLike` for testability.
- Zod validation (`TenantBrandingSchema.safeParse`) on every fetched
payload — Zod failures surface as `BRANDING_INVALID_PAYLOAD`.
- Network failures (transport throw or non-ok response) surface as
`BRANDING_FETCH_FAILED`.
- `--brand-primary`, `--brand-secondary`, `--brand-accent`, `--brand-font`
written via `setExtensionTokens` — cleanup is automatic on unmount.
- `document.title`, favicon `<link>`, and tenant `<style>` injection are
direct DOM writes (not CSS variables; no conflict with the mutator).
## Usage: Basic
```tsx
import { ThemeProvider, lightTheme } from "@suaveplan/react-ui/theme";
import { BrandingProvider } from "@suaveplan/react-auth-whitelabel";
<ThemeProvider themes={[lightTheme]} defaultTheme="light">
<BrandingProvider staticBranding={tenantBranding}>
<App />
</BrandingProvider>
</ThemeProvider>
```
## Usage: Advanced
```tsx
<BrandingProvider
brandingEndpoint="/api/branding"
tenantId={tenantSlug}
fetch={customFetch}
>
<App />
</BrandingProvider>
```
```tsx
const branding = useTenantBranding();
const lightLogo = useLogo(false);
const darkLogo = useLogo(true);
```
## API Reference
- `BrandingProvider(props)` — props include `brandingEndpoint?`,
`tenantId?`, `staticBranding?`, `fetch?`, `children`.
- `useBranding()` — returns `{ branding, isLoading, error, refresh }`.
- `useTenantBranding()` — returns the current `TenantBranding`.
- `useLogo(preferDark?)` — returns the most-appropriate logo URL or
`undefined`.
## Implementation Notes
The mutator effect runs on every `branding` change AND on every
`mutator` identity change. The mutator handle is stable across re-renders
(it's `useMemo`-wrapped inside `@suaveplan/react-ui`), so the effect only
re-fires when the branding payload actually changes. The cleanup
contract for `--brand-*` is delegated to `useThemeMutator` — when the
provider unmounts, the runtime invokes the mutator's `clear()` automatically,
removing every `--brand-*` declaration from `document.documentElement.style`.

View File

@@ -0,0 +1,65 @@
# Error codes — `REACT_AUTH_WL`
## Purpose
`error-codes.ts` registers the canonical, stable error-code registry used by
every error thrown from `@suaveplan/react-auth-whitelabel`. The codes are
namespaced under `REACT_AUTH_WL` and registered at module load via
`defineCodes`, which means the `@suaveplan/error` registry is wired before any
consumer imports a hook, context, or component. The codes survive minification,
remain stable across releases, and let consumers branch on `.code` instead of
inspecting the constructor name or message text — both of which are unstable
across bundles.
## Features
- Single source of truth for error-code identity in this package.
- `as const` literal-string codes for type-narrowing.
- Module-load-time registration so the wire-format adapter, redaction layer,
OTel span-event bridge, and i18n resolver all see the codes without an
explicit init call.
- Each code is thrown by ≥1 production code path — the
`error-codes.test.ts` grep-test enforces the contract.
## Usage: Basic
```ts
import { REACT_AUTH_WL_CODES } from "@suaveplan/react-auth-whitelabel/errors";
if (err.code === REACT_AUTH_WL_CODES.BRANDING_FETCH_FAILED) {
showFallbackBrand();
}
```
## Usage: Advanced
```ts
import {
REACT_AUTH_WL_CODES,
type ReactAuthWhitelabelCode,
} from "@suaveplan/react-auth-whitelabel/errors";
function isRecoverable(code: ReactAuthWhitelabelCode): boolean {
switch (code) {
case REACT_AUTH_WL_CODES.BRANDING_FETCH_FAILED:
case REACT_AUTH_WL_CODES.AUTH_NETWORK_FAILED:
return true;
default:
return false;
}
}
```
## API Reference
- `REACT_AUTH_WL_CODES` — frozen record of literal string codes; each value is
prefixed with `"REACT_AUTH_WL."`.
- `ReactAuthWhitelabelCode` — union of every `REACT_AUTH_WL_CODES[K]` value.
## Implementation Notes
`defineCodes("REACT_AUTH_WL", [...])` enforces uniqueness inside the namespace
and stamps the registry with the call site for diagnostics. We intentionally
do NOT eagerly import the registry from package code other than the error
class — the registry side-effect runs once at module evaluation and that is
the only required wiring.

View File

@@ -0,0 +1,68 @@
# `ReactAuthWhitelabelError`
## Purpose
`ReactAuthWhitelabelError` is the canonical error class for every error thrown
by `@suaveplan/react-auth-whitelabel`. It extends `SuaveplanError` so the
error participates in the project-wide error substrate (cause-chain traversal,
redaction-aware wire format, OTel span-event adapter, i18n message resolver).
Every instance is stamped with `context.tier === "react-auth-whitelabel"` so
log sinks and dashboards can group white-label auth failures without
inspecting `error.constructor.name`, which is unstable across minified
bundles. Construction always carries a `messageKey` so the i18n resolver can
localise the user-visible error string at the boundary.
## Features
- Type-narrowed `.code` of `ReactAuthWhitelabelCode`.
- Auto-injected `context.tier` for log/dashboard grouping.
- Carries a translation key, not a baked English string.
- Compatible with `SuaveplanError`'s cause-chain walker, wire serialiser,
and OTel span-event adapter.
## Usage: Basic
```ts
import { ReactAuthWhitelabelError } from "@suaveplan/react-auth-whitelabel/errors";
import { REACT_AUTH_WL_CODES } from "@suaveplan/react-auth-whitelabel/errors";
throw new ReactAuthWhitelabelError(
REACT_AUTH_WL_CODES.PROVIDER_OUTSIDE_TREE,
"react_auth_wl.error.provider_outside_tree",
{ context: { hook: "useAuth" } }
);
```
## Usage: Advanced
```ts
try {
await login(email, password);
} catch (cause) {
throw new ReactAuthWhitelabelError(
REACT_AUTH_WL_CODES.AUTH_NETWORK_FAILED,
"react_auth_wl.error.network",
{ cause, context: { authUrl: config.authUrl } }
);
}
```
The thrown error retains the original `cause` chain so monitoring sinks can
correlate transport errors with the user-facing failure.
## API Reference
- `new ReactAuthWhitelabelError(code, messageKey, options?)` — constructs an
error. `options.context.tier` is always set to `"react-auth-whitelabel"`,
even if the caller passes a different value (the helper merges, then
overrides `tier`).
- `.code` — `ReactAuthWhitelabelCode` literal.
- `.message` — short English description sourced from `messageKey` (the i18n
resolver substitutes a localised translation when available).
## Implementation Notes
The constructor merges caller-supplied `context` with the canonical `tier`
stamp. Callers must NOT pass a different `tier` — it will silently win at
merge time, but the contract is that this package's errors are always
tagged `"react-auth-whitelabel"`.

View File

@@ -0,0 +1,59 @@
# `TenantBrandingSchema`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`TenantBrandingSchema` is the Zod schema that validates every `TenantBranding`
payload entering the package from an untrusted boundary — specifically the
JSON returned by the branding endpoint configured on `<BrandingProvider>`.
It mirrors `TenantBranding` exactly: every field's optionality and type
match the TypeScript definition. The schema is the boundary contract the
`BrandingProvider` enforces before storing a fetched payload, so an invalid
or partial JSON response cannot poison the running app's theme tokens or
document chrome.
## Features
- Field-by-field parity with `TenantBranding`.
- `satisfies z.ZodType<TenantBranding>` keeps schema and type in lockstep —
a missing field on either side fails the compile.
- Imported with `import z from "zod"` per the Zod import rule.
- Required string fields use `.min(1)` so a JSON `""` value is rejected.
## Usage: Basic
```ts
import { TenantBrandingSchema } from "@suaveplan/react-auth-whitelabel";
const parsed = TenantBrandingSchema.parse(await response.json());
```
## Usage: Advanced
```ts
const result = TenantBrandingSchema.safeParse(payload);
if (!result.success) {
logger.warn("invalid branding payload", { issues: result.error.issues });
return;
}
applyBranding(result.data);
```
The `BrandingProvider` uses `safeParse` so a malformed payload is converted
into a `ReactAuthWhitelabelError(REACT_AUTH_WL.BRANDING_INVALID_PAYLOAD)` and
surfaced on the context's `error` field instead of crashing the React tree.
## API Reference
- `TenantBrandingSchema` — a `z.ZodObject` whose inferred type is exactly
`TenantBranding`.
## Implementation Notes
The schema lives in its own file so the type-only consumer of `TenantBranding`
can keep the runtime Zod dependency tree-shaken when validation is not used.
Static branding passed via `staticBranding` is trusted and never parsed — the
consumer authored it. Network payloads are always parsed.

View File

@@ -0,0 +1,53 @@
# `navigateOrCall`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`navigateOrCall(target)` is the canonical helper used by every form
component in this package to act on a "URL or click handler" prop. The
forms accept `string | (() => void) | undefined` for affordances like
`onForgotPassword`, `onRegister`, `onLogin`, etc. This helper centralises
the three-branch routing so each form does not carry an inline copy of
the same logic. Treating string values as URLs and function values as
imperative handlers lets consumers wire either a static href or an
in-app router callback without changing the component prop signature.
## Features
- String target → `window.location.href = target` (no-op outside a
browser).
- Function target → invoke directly.
- `undefined` → no-op.
## Usage: Basic
```ts
navigateOrCall("/login");
navigateOrCall(() => router.push("/login"));
navigateOrCall(undefined);
```
## Usage: Advanced
```tsx
<button onClick={() => navigateOrCall(props.onLogin)}>
Sign in
</button>
```
## API Reference
- `navigateOrCall(target: string | (() => void) | undefined): void` —
never throws; safely handles non-browser environments by detecting
`typeof window`.
## Implementation Notes
The string branch checks `typeof window !== "undefined"` so a server-side
or test environment that lacks `window` does not crash. The helper does
not return the target — callers cannot observe whether the navigation
ran; this matches the imperative redirect semantics expected at every
call site.