# Catalogue URL State Contract

Mechanica catalogue state is now represented by a canonical, shareable query-string contract. The parser and serializer live in `src/utils/catalogueUrlState.ts`, with a React Router adapter in `src/hooks/useCatalogueUrlState.ts`.

## Canonical Parameters

| State | Canonical key | Accepted aliases | Default | Notes |
| --- | --- | --- | --- | --- |
| Search query | `q` | `query`, `search` | empty | Whitespace is collapsed and length-limited to avoid oversized shared URLs. |
| Category | `category` | `cat` | `all` | Pass registry category slugs as `validCategories` to reject stale or malformed links. |
| Difficulty | `difficulty` | `diff` | `all` | Defaults include beginner, introductory, intermediate, advanced, and expert. |
| Sort | `sort` | `order` | `featured` | Common aliases like `name`, `a-z`, `hardest`, and `latest` canonicalise to explicit sort keys. |
| Favourites only | `fav` | `favorite`, `favourite`, `bookmarked`, etc. | `false` | Serialises to `fav=1` only when enabled. |
| Layout mode | `view` | `layout` | `grid` | Supports `grid`, `list`, and `compact`. |
| Page | `page` | `p` | `1` | Positive integers only; pages above `maxPage` are capped with a warning. |

Default values are omitted, so `/catalogue` remains the canonical URL for the default catalogue view.

## Behaviour

The URL helpers are intentionally defensive:

- Unknown aliases are normalised into the canonical key order.
- Malformed categories, difficulties, sort keys, booleans, view modes, and pages fall back to defaults instead of throwing.
- Parse warnings are returned for telemetry, debug banners, or non-blocking QA assertions.
- Known catalogue parameters can be stripped while preserving unrelated query parameters such as `utm_*` values.
- Pagination can be reset automatically when search, filter, sort, or favourites-only state changes.

## React Router Usage

```tsx
import { useMemo } from 'react';

import { machineRegistry } from '../data/machineRegistry';
import { useCatalogueUrlState } from '../hooks/useCatalogueUrlState';

export function CataloguePage() {
  const categories = useMemo(
    () => Array.from(new Set(machineRegistry.map((machine) => machine.category))),
    [],
  );

  const catalogueUrl = useCatalogueUrlState({
    validCategories: categories,
    preserveUnknownParams: true,
    resetPageOnFilterChange: true,
  });

  return (
    <form>
      <input
        value={catalogueUrl.state.query}
        onChange={(event) =>
          catalogueUrl.setState({ query: event.currentTarget.value })
        }
      />

      <button type="button" onClick={() => catalogueUrl.resetState()}>
        Reset catalogue
      </button>

      <a href={catalogueUrl.canonicalHref}>Canonical link</a>
    </form>
  );
}
```

## Integration Checklist

When wiring the hook into catalogue controls:

1. Derive `validCategories` and any custom `validDifficulties` from the registry.
2. Use `catalogueUrl.state` as the single source of truth for filter controls.
3. Call `catalogueUrl.setState` for each control update; page reset is automatic for filter-like changes.
4. Use `catalogueUrl.hrefForState({ ... })` for precomputed links such as category chips.
5. If `catalogueUrl.isCanonical` is false, the page can optionally replace history with `catalogueUrl.canonicalHref` after first render.
6. Surface `catalogueUrl.warnings` only in development or diagnostics; malformed shared URLs should not interrupt the user.

## Test Coverage

`tests/catalogueUrlState.test.ts` covers default URLs, alias canonicalisation, malformed input, query preservation, pagination reset, and canonical href construction. These tests protect the share-link contract before it is connected to the visible catalogue controls.
