Compose page chrome from the shared patterns
Never hand-roll the furniture.@/components/ui-patterns/ provides it:
Primitives come from
@/components/ui/ (shadcn/ui). A CI guard checks the conventions.
Every mutation gives feedback
Success and error both surface a toast viauseToast(). Browser alert() is
forbidden, and so is a silent failure — a mutation that appears to do nothing is worse
than one that reports an error.
Forms
- React Hook Form + Zod (
@hookform/resolvers/zod). - No
<form>elements. Submission goes through React Hook Form. - No inline handlers for mutations. Use a React Query mutation.
Data fetching
TanStack React Query throughout. Loading and error states are mandatory — an async component that renders only the happy path is incomplete, not minimal.File size
Component files are limited to 200 lines. ESLint warns past 200;npm run lint
fails past 800 unless the file is grandfathered in the size baseline. Grandfathered
files may shrink but never grow.
Split by responsibility, not by line count — a 250-line component usually contains two
components and a hook.
Accessibility
Every new interactive component — form, dialog, table, complex widget — ships with an axe assertion:color-contrast is disabled by default
because jsdom does not implement layout — contrast belongs in Playwright and staging
audits.
TypeScript rules
Where a function returns different shapes, use a discriminated union:
Code cleanliness
- No dead code — no commented-out blocks, unused imports, or unused parameters.
- No duplicate JSDoc blocks; no stale comments describing behaviour that has changed.
- No
TODOwithout a ticket ID:// TODO(NLE-123): handle edge case. - No magic numbers — name the constant.
- Functions under 40 lines by preference; extract named sub-functions past 80.
- If a function name contains “and”, it is two functions.
Never re-export through a leaf component
Iffoo-dialog-schema.ts exists, callers import from there. Adding
export { fooFormSchema } to foo-dialog.tsx “for compatibility” adds indirection and
drags schema imports into the client bundle. Grep for the call sites before assuming
there is a need.