---
name: frontend-structure
description: React + Vite + Tailwind + SmartStack frontend structure (npm consumer of @atlashub/smartstack)
group: D
allowed-tools: [Read, Glob, Grep]
---

# Skill: Frontend SmartStack (React)

## Technical Stack
- React 19 + TypeScript 5.9
- Vite 7
- Tailwind CSS v4
- SmartStack npm: `@atlashub/smartstack`
- State/data: `@tanstack/react-query`
- Routing: `react-router-dom` (used by `DynamicRouter` from SmartStack)
- i18n: `react-i18next`

## Project Structure
```
web/{app-lower}-web/
├── src/
│   ├── main.tsx             # SmartStackProvider + imports every *Registry
│   ├── App.tsx              # DynamicRouter
│   ├── index.css            # Tailwind v4
│   ├── extensions/          # generated *Registry.ts files (PageRegistry.register)
│   │   ├── hrmRegistry.ts
│   │   └── catalogueRegistry.ts
│   ├── features/
│   │   └── {module}/{entity}/{pages,services,hooks,types}/
│   ├── components/
│   ├── services/
│   ├── hooks/
│   └── i18n/{fr,en,it,de}/{module}/{entity}.json
├── package.json
├── vite.config.ts
└── tsconfig.json
```

## Key Files

### `main.tsx` — entry point

All registries MUST be imported BEFORE `<App/>` renders so that
`PageRegistry.register()` calls execute before `DynamicRouter` tries to resolve
the first route. Missing imports = blank pages with no error.

```tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { SmartStackProvider } from '@atlashub/smartstack';
import App from './App';
import './index.css';
import './i18n';

// ⚠ MANDATORY: import every generated Registry before rendering.
// scaffold-routes emits one file per module; list them all here.
import './extensions/hrmRegistry';
import './extensions/catalogueRegistry';
// ... one line per module

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <BrowserRouter>
      <SmartStackProvider
        config={{
          apiUrl: import.meta.env.VITE_API_URL || 'http://localhost:5142',
          extensions: {},
          // Mobile shell: viewports under the breakpoint render the package's
          // "descente par paliers" shell (Applications → Modules → Sections)
          // with its transverse bottom bar. The shell is ON BY DEFAULT — this
          // block makes the choice explicit and is the only way to set
          // `breakpoint` / `shellComponent` / `bottomNav`. Written by /pwa.
          mobile: { enabled: true },
        }}
      >
        <App />
      </SmartStackProvider>
    </BrowserRouter>
  </StrictMode>,
);
```

This is the shape `ss init` emits: the config is an **inline JSX literal**
(`config={{ … }}`). A hoisted `const config = { … }` passed as `config={config}`
is equally valid — the scaffolders patch both forms — but the inline literal is
the default every generated app starts from.

### `App.tsx` — routing entry

Delegates everything to `DynamicRouter` from `@atlashub/smartstack`. No manual
`<Route>` declarations here — routes come from the menu API + PageRegistry.

```tsx
import { DynamicRouter } from '@atlashub/smartstack';

export default function App() {
  return <DynamicRouter />;
}
```

### `index.css` — Tailwind v4 + SmartStack styles

```css
@import "tailwindcss";
@source "node_modules/@atlashub/smartstack";
```

## Conventions
- One component per file
- Business hooks in `features/{app}/{module}/{entity}/hooks/`
- API clients in `features/{app}/{module}/{entity}/services/`
- `PermissionGuard` from `@atlashub/smartstack` to gate UI actions by permission
- One `*Registry.ts` per module — generated by `scaffold-routes` CLI
- i18n files per entity to avoid concurrent overwrites: `i18n/{locale}/{module}/{entity}.json`
- `useParams<{ id: string }>()` — always `:id`, never custom param names (DynamicRouter convention)

## Checklist before declaring the frontend working

1. `npm run dev` → Vite outputs `Local: http://localhost:3000/`
2. `curl http://localhost:3000/` → HTTP 200 with `<div id="root"></div>`
3. In browser DevTools console after login: `PageRegistry.list()` returns a non-empty array (if exposed)
4. Every module listed in the backend's `/api/navigation/menu` has a matching `*Registry.ts` import in `main.tsx`
5. No white-screen on any route — see `debug/frontend/SKILL.md` étape 7 if any

## Tests
- Vitest + Testing Library
- MSW for API mocks
- `renderWithProviders(ui)` helper: wraps with `QueryClientProvider` + `MemoryRouter` + `SmartStackProvider`
- One test file per page/hook: `tests/{module}/{entity}/{Entity}.test.tsx`
