# Dialogs keywords : synchro avec le site actif + responsive — Design

- **Date** : 2026-07-24
- **Objectif** : les dialogs de SAISIE (Ajouter un mot-clé, Import, targeting Recherche) proposent les langues/pays du **site actif** au lieu de l'univers complet (51 langues / 250 pays), et les dialogs deviennent responsive.
- **Origine** : Bastien, en test pré-launch — « c'est pas synchro avec nos settings de sites » + « pas du tout responsive ». Impacte reservoir / recherche / planning (toolbar + dialogs partagés).
- **Décisions de cadrage (Bastien)** : (1) **strict site** — les dialogs ne proposent que langues/pays du site, avec repli sur l'univers si le site n'a rien de configuré ; (2) la **targeting-bar** de Recherche est restreinte au site aussi (on abandonne le « diaspora, tous pays ») ; (3) les **filtres facettes** (Langue/Pays des toolbars, dérivés des données présentes) restent **inchangés** — un filtre porte sur ce qui existe.

## Contexte technique (audit)

- `useActiveSite()` (`src/lib/keywords/site-context.tsx:91`) expose déjà `activeSite: Site` avec `primaryLanguage: string`, `languages?: string[]`, `targetCountries?: string[]` — **côté client, aucune requête à ajouter**. Pattern déjà utilisé (`research-container.tsx:198`, `reservoir.tsx:163`).
- Univers : `LANGUAGE_OPTIONS` (51), `COUNTRY_OPTIONS` (250), `languageLabel()`, `countryLabel()` dans `src/lib/sites/options.ts`.
- `DialogContent` base (`src/components/ui/dialog.tsx:71-84`) : largeur responsive OK, mais **pas de contrainte de hauteur ni scroll** → un contenu haut déborde.

## Volet A — Synchro dialogs avec le site actif

### A1 — Helpers centralisés (source unique, `src/lib/sites/options.ts`)
Deux fonctions pures, testables, réutilisées par tous les dialogs (évite de dupliquer la logique dans 4 composants) :

```ts
/** Options de langue du site : principale + additionnelles, dédupées, ordre
 *  principale d'abord. value = code, label = languageLabel(code). */
export function siteLanguageOptions(site: {
  primaryLanguage: string;
  languages?: string[] | null;
}): ComboOption[]

/** Options de pays du site : targetCountries mappés (value = alpha-2,
 *  label = countryLabel(code) = nom seul). Repli sur COUNTRY_OPTIONS (univers)
 *  si targetCountries est vide/absent — pour ne pas bloquer la saisie sur un
 *  site pas encore configuré. */
export function siteCountryOptions(site: {
  targetCountries?: string[] | null;
}): ComboOption[]
```

- Langue : `primaryLanguage` existe toujours → au moins une option, pas de repli langue nécessaire. Dédupe si `languages` contient déjà la principale.
- Pays : repli univers `COUNTRY_OPTIONS` documenté quand `targetCountries` vide.

### A2 — Brancher les 4 dialogs sur `useActiveSite()`
Aucun n'appelle `useActiveSite` aujourd'hui (ils reçoivent seulement `siteId`). Chacun appelle le hook et utilise les helpers A1 :

- **`add-keyword-dialog.tsx`** (`:21,51,52,65,66,87,88,131,140`) :
  - `options={siteLanguageOptions(activeSite)}` (au lieu de `LANGUAGE_OPTIONS`), `options={siteCountryOptions(activeSite)}` (au lieu de `COUNTRY_OPTIONS`).
  - Défauts : `useState(activeSite.primaryLanguage)`, `useState(activeSite.targetCountries?.[0] ?? "")` (au lieu de `"fr"`/`"FR"`). `reset()` et le fallback submit alignés sur ces valeurs. Si `targetCountries` vide, défaut pays = premier de l'univers ou vide (cohérent avec le repli A1).
- **`import-keywords-dialog.tsx`** (`:31,268`) : la LISTE langue passe de `LANGUAGE_OPTIONS` à `siteLanguageOptions(activeSite)`. Le `defaultLang` reçu en prop reste (déjà branché sur `primaryLanguage` par les pages), garanti présent dans la liste restreinte.
- **`import-brief-dialog.tsx`** (`:23-26,154`) : pays = `[AUTO, ...siteCountryOptions(activeSite)]` (garder le sentinel `"auto"` de détection automatique en tête, restreindre le reste au site).
- **`targeting-bar.tsx`** (`:96-97`) : `options={siteCountryOptions(activeSite)}` (au lieu des 250). Retirer le commentaire « diaspora, tous pays » (décision : restreint au site).

### A3 — Hors périmètre (inchangé)
- Filtres facettes Langue/Pays (`research-toolbar`, `reservoir-toolbar`, `planning-table`) : restent dérivés des données présentes (`facets.*`).
- `coverage-matrix` : déjà branché sur `targetCountries` — aucun changement.
- Formulaire de setup site (`site-form.tsx`) : c'est LUI qui définit les settings — inchangé.

## Volet B — Responsive

### B1 — `DialogContent` base (`src/components/ui/dialog.tsx`)
Ajouter au `className` de base une contrainte de hauteur + scroll : `max-h-[calc(100dvh-2rem)] overflow-y-auto`. Corrige TOUS les dialogs d'un coup (un contenu haut devient scrollable au lieu de déborder). Vérifier que ça ne casse pas les dialogs à contenu court (le `max-h` est une borne, pas une hauteur fixe).

### B2 — `add-keyword-dialog` rangée serrée (`:128-148`)
La rangée `flex gap-2` (langue `w-32` + pays `w-32` + thème `flex-1`) déborde sous ~360px. Passer en `flex-col sm:flex-row` (empilé sur mobile, en ligne sur `sm+`), largeurs `w-full sm:w-32`.

### B3 — `keyword-sheet.tsx` (`:277`)
Largeur/hauteur déjà en `min(...)` (s'adapte). Le layout interne dense (grilles KPI) est conçu desktop — **hors périmètre de ce chantier** (refonte responsive du panneau = travail distinct). Noté comme follow-up si besoin.

## Tests

- Helpers A1 (`siteLanguageOptions`/`siteCountryOptions`) : tests purs — site avec langues additionnelles, site sans additionnelles, site sans targetCountries (repli univers), dédup principale. bun:test.
- Vérif que les 4 dialogs consomment bien `activeSite` (pas de régression tsc ; les dialogs restent montés dans un arbre où `SiteProvider` existe — vérifier).
- Responsive : vérification visuelle d3k (dialog Ajouter un mot-clé en viewport étroit + large : dropdown langue = langues du site, pays = targetCountries, rangée empilée sur mobile, scroll si contenu haut).

## Hors périmètre explicite
- Refonte responsive dense du `keyword-sheet` (panneau overview).
- Traduction FR des noms de langues (données Google Ads en anglais) — sujet distinct.
- Alignement des filtres facettes sur le site (décision : inchangés).
