# Design — keywords/reservoir (page gabarit)

> Statut : validé (brainstorming) le 2026-06-03. Source : `docs/project/pages/keywords/reservoir.md`.
> Cette page **établit le gabarit** (layout dashboard, DataTable, filtres, Sheet, sparkline,
> fixtures typées) que les sub-agents répliqueront pour les autres pages de la suite.

## But

Réservoir de recherche de mots-clés **par site** (≠ rank-tracking). Écran de staging :
on collecte, qualifie (statut/thème), filtre et inspecte des mots-clés avant de les
pousser vers le planning/contenu. Données **mockées** (fixtures typées), remplaçables
par l'API plus tard.

## Données

### Source réelle
Dataset Mangools/KWFinder fourni par l'utilisateur (`kwfinder_import_export (1).csv`,
254 mots-clés, secteur bijouterie/diamants, locale `en`/`US`). **On n'invente pas de
données** : les valeurs proviennent du CSV. On en cure ~30 lignes variées.

### Modèle de données (révélé par le design)

```ts
type KeywordStatus = "idea" | "watch" | "active" | "archived";
type SearchIntent  = "informational" | "commercial" | "transactional" | "navigational";
type KeywordSource = "mangools" | "ahrefs" | "gsc";

interface SourceMetrics {
  volume: number;      // search volume (last known)
  kd: number;          // keyword difficulty 0-100
  cpc: number;         // CPC USD
  growthPct: number;   // keyword interest growth %
  updatedAt: string;   // ISO date
}

interface MonthlyVolume { month: string; volume: number } // série → sparkline

interface Keyword {
  id: string;
  siteId: string;                                        // isolation par site
  term: string;
  locale: { lang: string; country: string };            // "en" / "US"
  theme: string;                                         // taxo ci-dessous
  status: KeywordStatus;
  intent: SearchIntent[];                                // le CSV est multi-intent
  primarySource: KeywordSource;                          // "mangools" → pilote les colonnes table
  sources: KeywordSource[];                              // → colonne "sources"
  metrics: Partial<Record<KeywordSource, SourceMetrics>>;// SEUL mangools rempli
  trend: MonthlyVolume[];                                // 12 derniers mois du CSV
  serpFeatures?: string[];                               // bonus Sheet (AI Overview, PAA…)
}
```

**Multi-source = shape, Mangools rempli.** Le type porte les métriques indexées par
source mais seules les valeurs Mangools (vraies, du CSV) sont remplies. La table affiche
`primarySource`. Le Sheet montre la structure multi-source : ahrefs/gsc affichent un état
explicite **« source non connectée »** — jamais de chiffres inventés.

### Taxonomie de thèmes (5, dérivés des 254 mots-clés)

| `theme` | Libellé | Exemples |
|---|---|---|
| `lab-diamonds` | Diamants de labo & pierres | lab grown diamond vs natural, cz vs moissanite vs diamond, conflict free diamonds, kimberley process |
| `supplies` | Fournitures & outillage | suppliers for jewelry making, findings supplier, polishing, soldering, electroplating, stone setting |
| `metals` | Or, métaux & titrages | gold filled vs gold plated, what is vermeil, 925 sterling silver, 14k/18k gold, rhodium plating |
| `manufacturing` | Fabrication & CAO | custom jewelry design, lost wax casting, 3d printing, cad design, ring manufacturer |
| `ethics-industry` | Éthique & industrie | sustainable/ethical brands, responsible jewellery council, trade fairs, vicenzaoro |

### Sites & isolation (« un dataset par site » → re-mock visible)

3 sites fictifs, ~10 mots-clés chacun (≈30 total) :

| `siteId` | Nom | Thèmes |
|---|---|---|
| `lustre-lab` | Lustre Lab (e-shop diamants de labo) | `lab-diamonds` + `ethics-industry` |
| `atelier-forge` | Atelier Forge (fournitures bijouterie) | `supplies` + `manufacturing` |
| `maison-aurum` | Maison Aurum (marque or) | `metals` + `ethics-industry` |

Le switch de site (team-switcher du shell) change visiblement la table → satisfait le
critère d'acceptation « le switch de site re-mock les données ».

## Écran & composants

### Route & arborescence
- `src/app/(app)/keywords/reservoir/page.tsx` (server) → `DashboardHeader` +
  breadcrumb `Keywords / Reservoir` + `<Reservoir />` (client).
- Feature : `src/components/keywords/`. Données : `src/lib/keywords/`.

### Unités (1 fichier = 1 rôle, testable isolément)

| Fichier | Rôle | Dépend de |
|---|---|---|
| `lib/keywords/types.ts` | `Keyword` & types associés | — |
| `lib/keywords/sites.ts` | les 3 sites + `getKeywordsForSite(siteId)` | types, fixtures |
| `lib/keywords/fixtures.ts` | ~30 keywords curés, groupés par site (valeurs réelles du CSV) | types |
| `components/ui/data-table.tsx` | **DataTable générique réutilisable** (le gabarit), généralisé depuis flight-check ; meta = sélection de lignes | @tanstack/react-table, ui/table |
| `components/keywords/reservoir.tsx` | conteneur client : état (site actif, search, filtres, sélection bulk, ligne ouverte) + composition | tous ci-dessous |
| `components/keywords/columns.tsx` | `ColumnDef<Keyword>[]` : terme·thème·statut·intent·volume·KD·CPC·croissance·sparkline·sources·maj + checkbox | types, badges, sparkline |
| `components/keywords/filters-bar.tsx` | search `Input` + facettes (langue·thème·statut·intent·source) + plages volume/KD/CPC/croissance (min/max en popover) | ui/input, ui/select, ui/popover |
| `components/keywords/bulk-actions-bar.tsx` | barre contextuelle (apparaît si sélection) : changer statut, assigner thème, archiver, exporter (mock) | ui/button, ui/dropdown-menu |
| `components/keywords/keyword-sheet.tsx` | Sheet détail : entête + métriques multi-sources (Mangools / autres « non connectée ») + grand graphe tendance + SERP features + intents | ui/sheet, sparkline |
| `components/keywords/sparkline.tsx` | mini-SVG custom (token `chart-1`) | — |
| `components/keywords/status-badge.tsx` · `intent-badge.tsx` | variants `Badge` | ui/badge |

### Décisions d'architecture
1. **DataTable générique partagée** (`components/ui/data-table.tsx`) plutôt que dupliquer
   celle de flight-check. C'est le gabarit ; meta typée = sélection de lignes pour le bulk.
   Flight-check reste inchangé (pas de refactor non sollicité) ; la généralisation est
   additive.
2. **Site actif via context** : petit `SiteProvider` dans `(app)/layout.tsx`, écrit par le
   `team-switcher` (dont les 3 « teams » deviennent nos 3 sites) et lu par `reservoir`.
3. **États** : skeleton au montage + empty (« no results » via filtres TanStack) + erreur,
   **scaffoldés** pour la future API. Mock synchrone aujourd'hui — état explicite, pas de
   faux chargement permanent.

### Navigation (sidebar)
Le groupe placeholder **« Models »** de `app-sidebar.tsx` devient **Keywords**
(icône lucide, ex. `SearchIcon`), sous-menu vers les 5 pages de la section :

| Libellé | URL | État |
|---|---|---|
| Overview | `/keywords/overview` | placeholder (404 jusqu'à build) |
| Reservoir | `/keywords/reservoir` | **construite ici** |
| Recherche | `/keywords/recherche` | placeholder |
| Planning | `/keywords/planning` | placeholder |
| Import / Export | `/keywords/export-import` | placeholder |

## A11y
- DataTable : `scope` sur en-têtes, focus visible, navigation clavier (réf flight-check).
- Checkbox sélection avec `aria-label`. Sheet : focus trap natif Radix. Filtres clavier-accessibles.

## Critères d'acceptation (proto)
- [ ] Filtrer met à jour la table (facettes + recherche + plages).
- [ ] La sparkline reflète la série mensuelle (`trend`).
- [ ] Le Sheet affiche les métriques multi-sources (Mangools rempli, autres « non connectée »).
- [ ] Le switch de site (team-switcher) re-mock les données (isolation `siteId`).
- [ ] Bulk-actions bar apparaît sur sélection et agit sur l'état local.
- [ ] Clavier + focus visibles.
- [ ] Sidebar : groupe Keywords avec sous-menu navigable.

## Hors proto
API réelle · dédup/merge · persistence · re-check Google · virtualisation
(≈30 lignes n'en ont pas besoin ; la DataTable reste prête à l'accueillir).

## Conventions
Next 16 (lire `node_modules/next/dist/docs/` avant de coder) · shadcn (MCP) · Tailwind tokens ·
biome · bun · isolation `site_id` partout sur cette page.
