# keywords/recherche (lot 1) Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Build the `keywords/recherche` page lot 1 — a list view (reskin of the Reservoir table) and a `keyword × country` coverage matrix, with a view toggle and multi-country targeting, all on mock client state.

**Architecture:** Approach 1 — reuse the Reservoir table core (`DataTable`, colored badges, `FiltersBar`, `KeywordSheet`) for the list; a dedicated `CoverageMatrix` component (semantic `<table>`) for the matrix; a `ResearchContainer` orchestrates state (targets map, view, selected countries, row selection). Target state is pure-function-driven mock client state, re-seeded per site.

**Tech Stack:** Next.js 16 (App Router), React 19, TanStack Table v8, shadcn/ui, Tailwind, bun:test, biome.

---

## File structure

- `src/lib/keywords/targets.ts` — types, `TARGET_COUNTRIES`, pure helpers (`seedTargets`, `setCell`, `setForSelected`, `coverageOf`). **Pure, TDD'd.**
- `src/lib/keywords/targets.test.ts` — bun:test for the helpers.
- `src/components/keywords/research/target-status-badge.tsx` — small status badge.
- `src/components/keywords/research/research-columns.tsx` — list columns = Reservoir columns + "Pays ciblés"; augments `TableMeta` with `targets`.
- `src/components/keywords/research/targeting-bar.tsx` — bulk targeting bar (country multi-select + "Cibler").
- `src/components/keywords/research/coverage-matrix.tsx` — semantic matrix table with per-cell status menu.
- `src/components/keywords/research/research-container.tsx` — orchestration (list + matrix + toggle + targeting + sheet).
- `src/app/(app)/keywords/recherche/page.tsx` — replace stub body with `<ResearchContainer />`.

Verification note: this repo has **no React test runner** (tests are bun:test on pure functions, e.g. `sparkline.test.ts`). So Task 1 is TDD; component tasks (2–7) are verified with `bunx tsc --noEmit`, `bunx biome check <files>`, and a browser screenshot via the running dev server (same flow used for the Reservoir).

---

### Task 1: Target data model + pure helpers

**Files:**
- Create: `src/lib/keywords/targets.ts`
- Test: `src/lib/keywords/targets.test.ts`

- [ ] **Step 1: Write the failing test**

Create `src/lib/keywords/targets.test.ts`:

```ts
// src/lib/keywords/targets.test.ts
import { expect, test } from "bun:test";
import type { Keyword } from "./types";
import {
  TARGET_COUNTRIES,
  coverageOf,
  seedTargets,
  setCell,
  setForSelected,
} from "./targets";

function kw(id: string): Keyword {
  return {
    id,
    siteId: "s1",
    term: id,
    locale: { lang: "en", country: "US" },
    theme: "lab-diamonds",
    status: "idea",
    intent: ["informational"],
    primarySource: "mangools",
    sources: ["mangools"],
    metrics: {},
    trend: [],
  };
}

test("TARGET_COUNTRIES are US, GB, CA, AU", () => {
  expect(TARGET_COUNTRIES.map((c) => c.code)).toEqual(["US", "GB", "CA", "AU"]);
});

test("seedTargets is deterministic for the same input", () => {
  const ks = ["a", "b", "c", "d", "e"].map(kw);
  expect(seedTargets(ks)).toEqual(seedTargets(ks));
});

test("seedTargets covers at least one keyword", () => {
  const ks = ["a", "b", "c", "d", "e"].map(kw);
  const state = seedTargets(ks);
  expect(Object.keys(state).length).toBeGreaterThan(0);
});

test("setCell adds, updates, and removes a target", () => {
  let s = {};
  s = setCell(s, "a", "US", "candidate");
  expect(s.a.US).toBe("candidate");
  s = setCell(s, "a", "US", "planned");
  expect(s.a.US).toBe("planned");
  s = setCell(s, "a", "US", null);
  expect(s.a).toBeUndefined(); // last country removed -> keyword entry dropped
});

test("setForSelected creates candidates for each keyword x country", () => {
  const s = setForSelected({}, ["a", "b"], ["US", "CA"]);
  expect(s.a).toEqual({ US: "candidate", CA: "candidate" });
  expect(s.b).toEqual({ US: "candidate", CA: "candidate" });
});

test("setForSelected does not downgrade a more-advanced status", () => {
  let s = setCell({}, "a", "US", "live");
  s = setForSelected(s, ["a"], ["US", "CA"]); // candidate
  expect(s.a.US).toBe("live"); // preserved
  expect(s.a.CA).toBe("candidate"); // new
});

test("coverageOf returns covered countries in TARGET_COUNTRIES order", () => {
  const s = setForSelected({}, ["a"], ["CA", "US"]);
  expect(coverageOf(s, "a")).toEqual([
    { country: "US", status: "candidate" },
    { country: "CA", status: "candidate" },
  ]);
  expect(coverageOf(s, "missing")).toEqual([]);
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `bun test src/lib/keywords/targets.test.ts`
Expected: FAIL — cannot resolve module `./targets`.

- [ ] **Step 3: Write minimal implementation**

Create `src/lib/keywords/targets.ts`:

```ts
// src/lib/keywords/targets.ts
import type { Keyword } from "./types";

export type TargetStatus = "candidate" | "planned" | "live";

export interface TargetCountry {
  code: string;
  label: string;
  flag: string;
}

/** Fixed proto target markets (English markets, matching the en/US fixtures). */
export const TARGET_COUNTRIES: TargetCountry[] = [
  { code: "US", label: "États-Unis", flag: "🇺🇸" },
  { code: "GB", label: "Royaume-Uni", flag: "🇬🇧" },
  { code: "CA", label: "Canada", flag: "🇨🇦" },
  { code: "AU", label: "Australie", flag: "🇦🇺" },
];

/** keywordId -> (country code -> status). No entry = "non ciblé". */
export type TargetsState = Record<string, Partial<Record<string, TargetStatus>>>;

const STATUS_RANK: Record<TargetStatus, number> = {
  candidate: 1,
  planned: 2,
  live: 3,
};

const SEED_STATUSES: TargetStatus[] = ["candidate", "planned", "live"];

/**
 * Deterministic seed (no randomness — derived from index) so the matrix isn't
 * empty on load and tests are stable. Every 3rd keyword is left untargeted.
 */
export function seedTargets(keywords: Keyword[]): TargetsState {
  const state: TargetsState = {};
  keywords.forEach((k, i) => {
    if (i % 3 === 2) return; // leave untargeted
    const entry: Partial<Record<string, TargetStatus>> = {};
    const primary = TARGET_COUNTRIES[i % TARGET_COUNTRIES.length];
    entry[primary.code] = SEED_STATUSES[i % SEED_STATUSES.length];
    if (i % 2 === 0) {
      const secondary = TARGET_COUNTRIES[(i + 1) % TARGET_COUNTRIES.length];
      entry[secondary.code] = "candidate";
    }
    state[k.id] = entry;
  });
  return state;
}

/** Set (or clear, when status is null) one cell. Drops empty keyword entries. */
export function setCell(
  state: TargetsState,
  keywordId: string,
  country: string,
  status: TargetStatus | null,
): TargetsState {
  const entry = { ...(state[keywordId] ?? {}) };
  if (status === null) delete entry[country];
  else entry[country] = status;

  const next = { ...state };
  if (Object.keys(entry).length === 0) delete next[keywordId];
  else next[keywordId] = entry;
  return next;
}

/** Bulk-target keywords × countries; never downgrades a more-advanced status. */
export function setForSelected(
  state: TargetsState,
  keywordIds: string[],
  countries: string[],
  status: TargetStatus = "candidate",
): TargetsState {
  let next = state;
  for (const kwId of keywordIds) {
    for (const country of countries) {
      const current = next[kwId]?.[country];
      if (current && STATUS_RANK[current] >= STATUS_RANK[status]) continue;
      next = setCell(next, kwId, country, status);
    }
  }
  return next;
}

/** Covered countries for a keyword, in TARGET_COUNTRIES order. */
export function coverageOf(
  state: TargetsState,
  keywordId: string,
): { country: string; status: TargetStatus }[] {
  const entry = state[keywordId] ?? {};
  return TARGET_COUNTRIES.filter((c) => entry[c.code]).map((c) => ({
    country: c.code,
    status: entry[c.code] as TargetStatus,
  }));
}
```

- [ ] **Step 4: Run test to verify it passes**

Run: `bun test src/lib/keywords/targets.test.ts`
Expected: PASS (8 tests).

- [ ] **Step 5: Lint + commit**

```bash
bunx biome check --write src/lib/keywords/targets.ts src/lib/keywords/targets.test.ts
git add src/lib/keywords/targets.ts src/lib/keywords/targets.test.ts
git commit -m "feat: keyword target model + pure helpers (recherche)"
```

---

### Task 2: TargetStatusBadge

**Files:**
- Create: `src/components/keywords/research/target-status-badge.tsx`

- [ ] **Step 1: Write the component**

```tsx
// src/components/keywords/research/target-status-badge.tsx
import { cn } from "@/lib/utils";
import type { TargetStatus } from "@/lib/keywords/targets";

const META: Record<TargetStatus, { label: string; className: string }> = {
  candidate: { label: "candidat", className: "border border-border bg-transparent text-foreground" },
  planned: { label: "planifié", className: "bg-primary text-primary-foreground" },
  live: { label: "en ligne", className: "bg-emerald-600 text-white" },
};

export function TargetStatusBadge({
  status,
  prefix,
}: {
  status: TargetStatus;
  /** optional leading text, e.g. a flag in the list "Pays ciblés" cell */
  prefix?: string;
}) {
  const m = META[status];
  return (
    <span
      className={cn(
        "inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-xs font-medium",
        m.className,
      )}
    >
      {prefix ? <span aria-hidden>{prefix}</span> : null}
      {m.label}
    </span>
  );
}
```

- [ ] **Step 2: Verify it typechecks**

Run: `bunx tsc --noEmit`
Expected: no errors.

- [ ] **Step 3: Lint + commit**

```bash
bunx biome check --write src/components/keywords/research/target-status-badge.tsx
git add src/components/keywords/research/target-status-badge.tsx
git commit -m "feat: target status badge (recherche)"
```

---

### Task 3: research-columns (list = Reservoir columns + "Pays ciblés")

**Files:**
- Create: `src/components/keywords/research/research-columns.tsx`

- [ ] **Step 1: Write the component**

Reuses the exported Reservoir `columns` and appends a targets column. Augments `TableMeta` with `targets` (declaration merging — merges with the `cpcRange` augmentation already in `columns.tsx`). The targeted-countries summary is rendered inline in the column's `cell`.

```tsx
// src/components/keywords/research/research-columns.tsx
"use client";

import type { ColumnDef, RowData } from "@tanstack/react-table";
import {
  type TargetsState,
  TARGET_COUNTRIES,
  coverageOf,
} from "@/lib/keywords/targets";
import type { Keyword } from "@/lib/keywords/types";
import { columns as reservoirColumns } from "../columns";
import { TargetStatusBadge } from "./target-status-badge";

declare module "@tanstack/react-table" {
  // biome-ignore lint/correctness/noUnusedVariables: TData name required for module augmentation (TS2428)
  interface TableMeta<TData extends RowData> {
    targets?: TargetsState;
  }
}

const FLAG = new Map(TARGET_COUNTRIES.map((c) => [c.code, c.flag]));

export const researchColumns: ColumnDef<Keyword>[] = [
  ...reservoirColumns,
  {
    id: "targets",
    header: "Pays ciblés",
    enableSorting: false,
    enableGlobalFilter: false,
    cell: ({ row, table }) => {
      const cov = coverageOf(table.options.meta?.targets ?? {}, row.original.id);
      if (cov.length === 0)
        return <span className="text-muted-foreground">—</span>;
      return (
        <div className="flex flex-wrap gap-1">
          {cov.map((c) => (
            <TargetStatusBadge
              key={c.country}
              status={c.status}
              prefix={FLAG.get(c.country)}
            />
          ))}
        </div>
      );
    },
  },
];
```

- [ ] **Step 2: Verify it typechecks**

Run: `bunx tsc --noEmit`
Expected: no errors.

- [ ] **Step 3: Lint + commit**

```bash
bunx biome check --write src/components/keywords/research/research-columns.tsx
git add src/components/keywords/research/research-columns.tsx
git commit -m "feat: research list columns with targeted-countries column"
```

---

### Task 4: TargetingBar

**Files:**
- Create: `src/components/keywords/research/targeting-bar.tsx`

- [ ] **Step 1: Write the component**

```tsx
// src/components/keywords/research/targeting-bar.tsx
"use client";

import { Button } from "@/components/ui/button";
import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { TARGET_COUNTRIES } from "@/lib/keywords/targets";

export function TargetingBar({
  count,
  selectedCountries,
  onToggleCountry,
  onTarget,
  onClear,
}: {
  count: number;
  selectedCountries: string[];
  onToggleCountry: (code: string) => void;
  onTarget: () => void;
  onClear: () => void;
}) {
  if (count === 0) return null;
  const label =
    selectedCountries.length > 0
      ? `Pays (${selectedCountries.length})`
      : "Pays cibles";
  return (
    <div className="flex flex-wrap items-center gap-2 rounded-md border bg-muted/40 px-3 py-2 text-sm">
      <span className="font-medium">{count} sélectionné(s)</span>

      <DropdownMenu>
        <DropdownMenuTrigger asChild>
          <Button size="sm" variant="outline">
            {label}
          </Button>
        </DropdownMenuTrigger>
        <DropdownMenuContent align="start" className="w-44">
          <DropdownMenuLabel>Pays cibles</DropdownMenuLabel>
          <DropdownMenuSeparator />
          {TARGET_COUNTRIES.map((c) => (
            <DropdownMenuCheckboxItem
              key={c.code}
              checked={selectedCountries.includes(c.code)}
              onCheckedChange={() => onToggleCountry(c.code)}
              onSelect={(e) => e.preventDefault()}
            >
              <span aria-hidden>{c.flag}</span> {c.label}
            </DropdownMenuCheckboxItem>
          ))}
        </DropdownMenuContent>
      </DropdownMenu>

      <Button
        size="sm"
        onClick={onTarget}
        disabled={selectedCountries.length === 0}
      >
        Cibler
      </Button>

      <Button size="sm" variant="ghost" onClick={onClear} className="ml-auto">
        Annuler
      </Button>
    </div>
  );
}
```

- [ ] **Step 2: Verify it typechecks**

Run: `bunx tsc --noEmit`
Expected: no errors.

- [ ] **Step 3: Lint + commit**

```bash
bunx biome check --write src/components/keywords/research/targeting-bar.tsx
git add src/components/keywords/research/targeting-bar.tsx
git commit -m "feat: targeting bar (country multi-select + bulk target)"
```

---

### Task 5: CoverageMatrix

**Files:**
- Create: `src/components/keywords/research/coverage-matrix.tsx`

- [ ] **Step 1: Write the component**

Semantic `<table>` with `scope="col"` (countries) and `scope="row"` (terms). Each cell is a `DropdownMenu` whose items set the status (or clear it).

```tsx
// src/components/keywords/research/coverage-matrix.tsx
"use client";

import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
  type TargetStatus,
  type TargetsState,
  TARGET_COUNTRIES,
} from "@/lib/keywords/targets";
import type { Keyword } from "@/lib/keywords/types";
import { TargetStatusBadge } from "./target-status-badge";

const MENU: { value: TargetStatus | null; label: string }[] = [
  { value: null, label: "— non ciblé" },
  { value: "candidate", label: "candidat" },
  { value: "planned", label: "planifié" },
  { value: "live", label: "en ligne" },
];

function Cell({
  status,
  term,
  countryLabel,
  onSet,
}: {
  status: TargetStatus | undefined;
  term: string;
  countryLabel: string;
  onSet: (s: TargetStatus | null) => void;
}) {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <button
          type="button"
          className="inline-flex min-w-16 justify-center rounded px-1 py-0.5 hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
          aria-label={`${term} — ${countryLabel} : ${status ?? "non ciblé"}`}
        >
          {status ? (
            <TargetStatusBadge status={status} />
          ) : (
            <span className="text-muted-foreground">—</span>
          )}
        </button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="start">
        {MENU.map((m) => (
          <DropdownMenuItem
            key={m.label}
            onSelect={() => onSet(m.value)}
          >
            {m.label}
          </DropdownMenuItem>
        ))}
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

export function CoverageMatrix({
  keywords,
  targets,
  onSetCell,
}: {
  keywords: Keyword[];
  targets: TargetsState;
  onSetCell: (keywordId: string, country: string, status: TargetStatus | null) => void;
}) {
  if (keywords.length === 0)
    return (
      <div className="rounded-md border p-8 text-center text-muted-foreground">
        Aucun keyword pour ce site.
      </div>
    );

  return (
    <div className="overflow-x-auto rounded-md border">
      <table className="w-full caption-bottom text-sm">
        <caption className="sr-only">
          Couverture des mots-clés par pays cible
        </caption>
        <thead>
          <tr className="border-b">
            <th scope="col" className="px-3 py-2 text-left font-medium">
              Terme
            </th>
            {TARGET_COUNTRIES.map((c) => (
              <th key={c.code} scope="col" className="px-3 py-2 text-left font-medium">
                <span aria-hidden>{c.flag}</span> {c.code}
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {keywords.map((k) => (
            <tr key={k.id} className="border-b last:border-0">
              <th scope="row" className="px-3 py-2 text-left font-medium">
                {k.term}
              </th>
              {TARGET_COUNTRIES.map((c) => (
                <td key={c.code} className="px-3 py-2">
                  <Cell
                    status={targets[k.id]?.[c.code]}
                    term={k.term}
                    countryLabel={c.label}
                    onSet={(s) => onSetCell(k.id, c.code, s)}
                  />
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}
```

- [ ] **Step 2: Verify it typechecks**

Run: `bunx tsc --noEmit`
Expected: no errors.

- [ ] **Step 3: Lint + commit**

```bash
bunx biome check --write src/components/keywords/research/coverage-matrix.tsx
git add src/components/keywords/research/coverage-matrix.tsx
git commit -m "feat: coverage matrix (keyword x country, per-cell status menu)"
```

---

### Task 6: ResearchContainer

**Files:**
- Create: `src/components/keywords/research/research-container.tsx`

- [ ] **Step 1: Write the component**

Orchestrates list + matrix + toggle + targeting + sheet. Mirrors `reservoir.tsx` patterns (loading, cpcRange meta, site re-mock) and adds `targets`, `view`, `selectedCountries`.

```tsx
// src/components/keywords/research/research-container.tsx
"use client";

import type {
  ColumnFiltersState,
  RowSelectionState,
  VisibilityState,
} from "@tanstack/react-table";
import { useEffect, useMemo, useState } from "react";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { DataTable } from "@/components/ui/data-table";
import { Skeleton } from "@/components/ui/skeleton";
import { useActiveSite } from "@/lib/keywords/site-context";
import { getKeywordsForSite } from "@/lib/keywords/sites";
import {
  type TargetStatus,
  type TargetsState,
  seedTargets,
  setCell,
  setForSelected,
} from "@/lib/keywords/targets";
import type { Keyword } from "@/lib/keywords/types";
import { DEFAULT_COLUMN_VISIBILITY } from "../columns";
import { FiltersBar } from "../filters-bar";
import { KeywordSheet } from "../keyword-sheet";
import { CoverageMatrix } from "./coverage-matrix";
import { researchColumns } from "./research-columns";
import { TargetingBar } from "./targeting-bar";

export function ResearchContainer() {
  const { activeSiteId } = useActiveSite();

  const [loading, setLoading] = useState(true);
  const [rows, setRows] = useState<Keyword[]>([]);
  const [targets, setTargets] = useState<TargetsState>({});

  const [view, setView] = useState<"list" | "matrix">("list");
  const [search, setSearch] = useState("");
  const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
  const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
    DEFAULT_COLUMN_VISIBILITY,
  );
  const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
  const [selectedCountries, setSelectedCountries] = useState<string[]>([]);
  const [openKeyword, setOpenKeyword] = useState<Keyword | null>(null);

  // mock load + re-seed targets per site
  useEffect(() => {
    setLoading(true);
    setRowSelection({});
    const id = setTimeout(() => {
      const next = getKeywordsForSite(activeSiteId);
      setRows(next);
      setTargets(seedTargets(next));
      setLoading(false);
    }, 200);
    return () => clearTimeout(id);
  }, [activeSiteId]);

  const data = useMemo(() => rows, [rows]);

  const cpcRange = useMemo<[number, number]>(() => {
    const vals = rows.map((k) => k.metrics[k.primarySource]?.cpc ?? 0);
    if (!vals.length) return [0, 1];
    return [Math.min(...vals), Math.max(...vals)];
  }, [rows]);

  const matrixKeywords = useMemo(() => {
    const q = search.trim().toLowerCase();
    return q ? rows.filter((k) => k.term.toLowerCase().includes(q)) : rows;
  }, [rows, search]);

  const selectedCount = Object.values(rowSelection).filter(Boolean).length;

  function toggleCountry(code: string) {
    setSelectedCountries((prev) =>
      prev.includes(code) ? prev.filter((c) => c !== code) : [...prev, code],
    );
  }

  function targetSelected() {
    const ids = Object.entries(rowSelection)
      .filter(([, v]) => v)
      .map(([id]) => id);
    setTargets((prev) => setForSelected(prev, ids, selectedCountries));
    setRowSelection({});
    setSelectedCountries([]);
  }

  function setCellStatus(
    keywordId: string,
    country: string,
    status: TargetStatus | null,
  ) {
    setTargets((prev) => setCell(prev, keywordId, country, status));
  }

  if (loading) {
    return (
      <div className="space-y-3 p-4">
        <Skeleton className="h-9 w-full max-w-xl" />
        <Skeleton className="h-64 w-full" />
      </div>
    );
  }

  return (
    <div className="space-y-3 p-4">
      <div className="flex items-center justify-between gap-2">
        <FiltersBar
          search={search}
          onSearch={setSearch}
          columnFilters={columnFilters}
          onColumnFilters={setColumnFilters}
          columnVisibility={columnVisibility}
          onColumnVisibility={setColumnVisibility}
        />
        <Tabs value={view} onValueChange={(v) => setView(v as "list" | "matrix")}>
          <TabsList>
            <TabsTrigger value="list">Liste</TabsTrigger>
            <TabsTrigger value="matrix">Matrice</TabsTrigger>
          </TabsList>
        </Tabs>
      </div>

      {view === "list" ? (
        <>
          <TargetingBar
            count={selectedCount}
            selectedCountries={selectedCountries}
            onToggleCountry={toggleCountry}
            onTarget={targetSelected}
            onClear={() => {
              setRowSelection({});
              setSelectedCountries([]);
            }}
          />
          <DataTable
            columns={researchColumns}
            data={data}
            getRowId={(k) => k.id}
            globalFilter={search}
            columnFilters={columnFilters}
            columnVisibility={columnVisibility}
            rowSelection={rowSelection}
            onRowSelectionChange={setRowSelection}
            onRowClick={(k) => setOpenKeyword(k)}
            meta={{ cpcRange, targets }}
            caption="Recherche — source Reservoir du site actif"
          />
        </>
      ) : (
        <CoverageMatrix
          keywords={matrixKeywords}
          targets={targets}
          onSetCell={setCellStatus}
        />
      )}

      <KeywordSheet
        keyword={openKeyword}
        open={openKeyword !== null}
        onOpenChange={(o) => !o && setOpenKeyword(null)}
      />
    </div>
  );
}
```

- [ ] **Step 2: Verify it typechecks**

Run: `bunx tsc --noEmit`
Expected: no errors.

- [ ] **Step 3: Lint + commit**

```bash
bunx biome check --write src/components/keywords/research/research-container.tsx
git add src/components/keywords/research/research-container.tsx
git commit -m "feat: research container (list/matrix toggle, targeting, sheet)"
```

---

### Task 7: Wire the page + browser verification

**Files:**
- Modify: `src/app/(app)/keywords/recherche/page.tsx`

- [ ] **Step 1: Replace the stub body**

Replace the `<div className="flex h-64 …">…en construction.</div>` block with `<ResearchContainer />` and add the import. Final file:

```tsx
// src/app/(app)/keywords/recherche/page.tsx
import { DashboardHeader } from "@/components/dashboard-header";
import { ResearchContainer } from "@/components/keywords/research/research-container";
import {
  Breadcrumb,
  BreadcrumbItem,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";

export default function Page() {
  return (
    <>
      <DashboardHeader>
        <Breadcrumb>
          <BreadcrumbList>
            <BreadcrumbItem>Keywords</BreadcrumbItem>
            <BreadcrumbSeparator />
            <BreadcrumbItem>
              <BreadcrumbPage>Recherche</BreadcrumbPage>
            </BreadcrumbItem>
          </BreadcrumbList>
        </Breadcrumb>
      </DashboardHeader>
      <ResearchContainer />
    </>
  );
}
```

- [ ] **Step 2: Typecheck + lint + full test suite**

Run: `bunx tsc --noEmit && bunx biome check src/components/keywords/research src/app/"(app)"/keywords/recherche/page.tsx src/lib/keywords/targets.ts && bun test src/lib/keywords`
Expected: no tsc errors; biome clean on these files; all bun tests pass.

- [ ] **Step 3: Browser verification (dev server already running)**

Using the next-devtools browser tool (as for the Reservoir):
1. `start` (headless), `navigate` to `http://localhost:3000/keywords/recherche`.
2. `console_messages` errorsOnly — expect only the pre-existing `avatars/shadcn.jpg` 404, no React errors.
3. `screenshot` — confirm the **list** view: Reservoir-style table with colored KD/CPC badges + a "Pays ciblés" column showing flag + status badges; the **Liste | Matrice** toggle; the search/filters bar.
4. Select a row (`evaluate`: click first `tbody button[role="checkbox"]`), screenshot — confirm `TargetingBar` appears with "Pays cibles" dropdown + "Cibler".
5. Click the **Matrice** tab (`evaluate`: click the tab trigger with text "Matrice"), screenshot — confirm the `keyword × {US,GB,CA,AU}` grid with status badges / "—".
6. `close`.

Expected: all three screenshots render as described; no runtime errors.

- [ ] **Step 4: Commit**

```bash
git add src/app/"(app)"/keywords/recherche/page.tsx
git commit -m "feat: wire keywords/recherche page to ResearchContainer"
```

---

## Self-review

**Spec coverage:**
- List view (reskin Reservoir) → Tasks 3, 6. ✓
- Coverage matrix + toggle → Tasks 5, 6. ✓
- Multi-country targeting (bulk + cell) → Tasks 4, 5, 6 (`setForSelected`, `setCell`). ✓
- Status set `candidate/planned/live` + non-ciblé → Task 1. ✓
- Fixed countries US/GB/CA/AU → Task 1. ✓
- State mock, re-seed per site → Task 6 (`seedTargets` in the site effect). ✓
- Search filters both views; rich filters list-only → Task 6 (`matrixKeywords` filters by `search`; `columnFilters` only passed to `DataTable`). ✓
- Loading skeleton / empty matrix → Tasks 6, 5. ✓
- a11y matrix row+col scope, keyboard menus → Task 5. ✓
- Tests on pure helpers → Task 1. ✓
- Deferred items (per-country metrics, gaps, seasonal, shortlist, push-planning) → intentionally absent. ✓

**Placeholder scan:** Task 3 contains an intentional teaching note about removing a dummy `TargetsCell`; the *final* file form is given in full immediately after and contains no placeholder. No other TODO/TBD.

**Type consistency:** `TargetStatus`, `TargetsState`, `TARGET_COUNTRIES`, `seedTargets`, `setCell`, `setForSelected`, `coverageOf` are defined in Task 1 and used with identical signatures in Tasks 2–6. `TableMeta.targets` (Task 3) matches `meta={{ cpcRange, targets }}` (Task 6) and `table.options.meta?.targets` (Task 3). `view` union `"list" | "matrix"` consistent in Task 6.
