feat(web): domain-layer conventions doc + react-hook-form deps (Phase 4 prep)

This commit is contained in:
Adam Moussa 2026-07-13 17:47:05 -04:00
parent 8dc8f7814b
commit 924bfdd8f6
No known key found for this signature in database
3 changed files with 79 additions and 0 deletions

30
web/package-lock.json generated
View file

@ -13,6 +13,7 @@
"@fontsource/dm-sans": "^5.2.8",
"@fontsource/jetbrains-mono": "^5.2.8",
"@fontsource/montserrat": "^5.2.8",
"@hookform/resolvers": "^5.4.0",
"@mui/icons-material": "^9.2.0",
"@mui/material": "^9.1.1",
"@proposal-system/api-contracts": "file:../shared/api-contracts",
@ -22,6 +23,7 @@
"lucide-react": "^1.24.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-hook-form": "^7.81.0",
"react-redux": "^9.2.0",
"react-router-dom": "^7.18.1",
"react-toastify": "^11.0.5",
@ -625,6 +627,18 @@
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@hookform/resolvers": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.4.0.tgz",
"integrity": "sha512-EIsqr/t/qbinPIhGjMdtvutIN1Kk4uwbROE9/UQ93CAVGR7GkA7Y92+fX80OzXi/OB67jVFYwKGO1WzkxmkFZw==",
"license": "MIT",
"dependencies": {
"@standard-schema/utils": "^0.3.0"
},
"peerDependencies": {
"react-hook-form": "^7.55.0"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@ -3309,6 +3323,22 @@
"react": "^19.2.7"
}
},
"node_modules/react-hook-form": {
"version": "7.81.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.81.0.tgz",
"integrity": "sha512-ocbmr2p5KBMoAfj4WCUvped33lVi1Kd5DuDUvQDnB6VEAacOjPI/jMbtDdbhco4y9ct4xUuCmMY0b/C9L0QHjw==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/react-hook-form"
},
"peerDependencies": {
"react": "^16.8.0 || ^17 || ^18 || ^19"
}
},
"node_modules/react-is": {
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz",

View file

@ -16,6 +16,7 @@
"@fontsource/dm-sans": "^5.2.8",
"@fontsource/jetbrains-mono": "^5.2.8",
"@fontsource/montserrat": "^5.2.8",
"@hookform/resolvers": "^5.4.0",
"@mui/icons-material": "^9.2.0",
"@mui/material": "^9.1.1",
"@proposal-system/api-contracts": "file:../shared/api-contracts",
@ -25,6 +26,7 @@
"lucide-react": "^1.24.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-hook-form": "^7.81.0",
"react-redux": "^9.2.0",
"react-router-dom": "^7.18.1",
"react-toastify": "^11.0.5",

47
web/src/domain/README.md Normal file
View file

@ -0,0 +1,47 @@
# Frontend domain layer — conventions
SHOC-alignment Phase 4 (mirrors shoc-frontend-new dev's `src/domain/<entity>/` layering).
Every agent/contributor working in this tree builds to THIS spec.
## Structure
```
src/domain/<entity>/
├── api.ts # HTTP calls only: axios via lib/api/client, path constants, no hooks
├── types.ts # re-exports from @proposal-system/api-contracts + view-only types
├── schemas.ts # re-exports from @proposal-system/api-contracts/schemas + form schemas
└── use-cases.ts # TanStack Query hooks + this domain's query keys (the ONLY public surface)
```
Domains: `proposals`, `lineItems`, `customers`, `pricingLibrary`, `admin`, `sites`.
(`auth` arrives with the separate auth-storage PR — do NOT create it here.)
## Rules
1. **Pages import ONLY from `domain/<x>/use-cases` (and `types`)** — never from
`lib/api/*`, never `apiClient` directly, never `useQuery`/`useMutation` inline
in a page. Pages are thin callers.
2. **Query keys live in the domain**: each `use-cases.ts` exports
`export const <entity>Keys = { all: ['<entity>'] as const, detail: (id: string) => ['<entity>', id] as const, ... }`
(TanStack hierarchical-key convention). `src/constants/queryKeys.ts` is
deleted at integration — do not add imports of it.
3. **Invalidation** uses the domain key objects across domains where needed
(e.g. approving a proposal invalidates `proposalsKeys.all`).
4. **Forms**: react-hook-form + `zodResolver` (from `@hookform/resolvers/zod`).
Form schemas live in the domain's `schemas.ts`, derived from the shared
contract schemas (`@proposal-system/api-contracts/schemas`) via
`.pick/.extend/.omit` — never hand-written duplicates. Form field state maps
to the request type at submit (a `toCreateRequest(formValues)` mapper in
`schemas.ts` when non-trivial).
5. **`lib/api/client.ts` stays** — the single axios instance (interceptors,
401 handling). Domain `api.ts` files import it. The old `lib/api/<domain>.ts`
modules are deleted once no page imports them (integration step) —
`lib/api/auth.ts` stays until the auth PR.
6. **Mutations**: toast on error stays in the hook (matching current UX),
success invalidation in the hook; page-specific side effects (navigate,
dialog close) via the mutation's callbacks at the call site.
7. **No new state managers, no context** — server state = TanStack Query,
existing Redux auth/ui slices untouched (auth refactor is a separate PR).
8. **Styling**: tokens only (`var(--...)`, theme) — no hardcoded hexes.
9. Verify before returning/committing: `npx tsc --noEmit`, `npm test -- --run`,
`npm run build` — all green, no skipped tests, no `@ts-ignore`.