> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hr-easy.nlead.ch/llms.txt
> Use this file to discover all available pages before exploring further.

# Frontend conventions

> UI patterns, forms, data fetching, accessibility, and the TypeScript rules.

## Compose page chrome from the shared patterns

Never hand-roll the furniture. `@/components/ui-patterns/` provides it:

| Component                     | Use                                                  |
| ----------------------------- | ---------------------------------------------------- |
| `PageHeader`                  | Title left, actions top-right.                       |
| `ListToolbar` + `SearchInput` | Search left, filters middle, actions right.          |
| `EmptyState`                  | Empty lists, with a permission-gated call to action. |
| `ConfirmDialog`               | **Every** destructive action.                        |
| `AlertBanner`                 | Inline errors.                                       |

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.

<Warning>
  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.
</Warning>

## 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:

```tsx theme={null}
import { expectNoA11yViolations } from "@/test-utils/a11y";

it("has no axe-core WCAG 2.1 AA violations", async () => {
  const { container } = render(<MyComponent {...props} />);
  await expectNoA11yViolations(container);
});
```

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

| Rule                                          | Instead                                         |
| --------------------------------------------- | ----------------------------------------------- |
| No `any`                                      | `unknown`, narrowed with a type guard.          |
| No `@ts-ignore` / `@ts-expect-error`          | Fix the type.                                   |
| No `as SomeType` to silence an error          | Fix the source of the wrong type.               |
| No non-null `!` without a null check in scope | Check it.                                       |
| Prefer `const`                                | `let` only where reassignment actually happens. |
| Explicit return types on service functions    | Makes refactoring safe.                         |

Where a function returns different shapes, use a discriminated union:

```typescript theme={null}
// WRONG
type Result = { data?: Foo; error?: string };

// CORRECT
type Result = { success: true; data: Foo } | { success: false; error: string };
```

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.
