Skip to main content

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 via useToast(). 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.
When you split a form component, walk the schema field by field and confirm each one is (a) rendered, (b) serialised into the payload, and (c) seeded from the entry. A field that survives in the schema but disappears from the JSX is silent data loss — the most-caught class of bug in this codebase.

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:
The helper runs the WCAG 2.1 A + AA rule packs. 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:
The wrong version makes every caller check two optional fields and permits the impossible state where both are set.

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 TODO without 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

If foo-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.