---
inclusion: fileMatch
fileMatchPattern: "frontend/**"
---
# Frontend Conventions

## Stack
- React 19, TypeScript strict mode, Vite 8
- Tailwind CSS with custom dark theme (surface-*, brand-* colors)
- TanStack Router (file-based routes in `routes.tsx`)
- TanStack Query for data fetching (hooks in `api/hooks.ts`)
- Recharts for charts, Monaco Editor for SQL, Lucide for icons

## API Client
- `api/client.ts` — shared fetch wrapper with `apiGet`, `apiPost`, `apiPut`, `apiDelete`
- Three API bases: `query` (→ `/api/`), `registry` (→ `/registry/`), `risk` (→ `/risk/`)
- Base URLs use `||` fallback (not `??`) because Vite inlines empty string for undefined env vars
- All hooks in `api/hooks.ts` — typed with TanStack Query

## Testing
- Vitest + MSW (Mock Service Worker) for deterministic tests
- Requires Node.js 24 via nvm — load before running: `export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" && nvm use 24`
- Test setup: `src/test/setup.ts` starts MSW server
- Mock handlers: `src/test/mocks/handlers.ts`
- Test helper: `src/test/render.tsx` provides `renderRoute(path)` with QueryClient + Router
- Run: `npx vitest --run`

## Components
- Shared UI in `components/ui.tsx`: StatusBadge, ConfidenceBar, TrendArrow, DateRangeSelector, TickerFilter, LoadingSpinner, ErrorBoundary, Card
- DataTable in `components/DataTable.tsx`: generic sortable/filterable/paginated table
- AppLayout in `components/AppLayout.tsx`: sidebar nav + main content area

## Docker
- `frontend/Dockerfile`: multi-stage node:24-alpine → nginxinc/nginx-unprivileged:alpine
- Listens on port 8080 (not 80) for K8s security context compatibility
- `frontend/nginx.conf`: SPA fallback + `/api/`, `/registry/`, `/risk/` reverse proxies

## Adding a New Page
1. Create `src/pages/MyPage.tsx`
2. Add route in `src/routes.tsx`
3. Add nav item in `components/AppLayout.tsx` navItems array
4. Add API hooks in `api/hooks.ts` if needed
5. Add MSW handler in `test/mocks/handlers.ts`
6. Add test in `test/pages.test.tsx`
