# GSC Positioning Actionable + Table UX — 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:** Make GSC positioning actionable on recherche (sortable "Pos. GSC" column, 5-band filter, "Trier par opportunité" toggle), and upgrade BOTH keyword tables (recherche + reservoir) with a country flag in the term cell (replacing the visible Langue column) plus a hover row-actions column (copy + delete).

**Architecture:** Pure, unit-tested helpers (`country-flag.ts`, `gsc-positioning.ts`) feed presentational columns. GSC column/filter/sort stay recherche-only via optional `FiltersBar` props and research-only column defs. Flag + actions live in shared `columns.tsx` so both views inherit them. `DataTable` gains optional controlled `sorting` and a `group` row class for hover reveals. No DB/schema change — everything derives from data already loaded.

**Tech Stack:** Next 16 (App Router, server actions), TanStack Table v8, shadcn (Select, AlertDialog, Button), lucide-react, bun + biome, Drizzle/Postgres.

Spec: `docs/superpowers/specs/2026-06-06-gsc-positioning-actionable-design.md`

---

## File structure

- Create `src/lib/keywords/country-flag.ts` — pure `countryFlag(code)` helper.
- Create `src/lib/keywords/country-flag.test.ts` — unit tests.
- Create `src/lib/keywords/gsc-positioning.ts` — pure `gscBand` + `opportunityScore`.
- Create `src/lib/keywords/gsc-positioning.test.ts` — unit tests.
- Create `src/components/keywords/row-actions.tsx` — copy + delete row icons.
- Modify `src/components/ui/data-table.tsx` — optional controlled sorting + `group` row class.
- Modify `src/components/keywords/columns.tsx` — flag in term cell, remove `lang` from toggleable, `actionsColumn`, `onDeleteKeyword` meta.
- Modify `src/components/keywords/research/research-columns.tsx` — `gscPosition` + hidden `gscOpportunity` columns, term override (flag + GSC badge), actions last.
- Modify `src/components/keywords/filters-bar.tsx` — optional `gscPositionFilter` + `extraColumns` props.
- Modify `src/components/keywords/reservoir.tsx` — wire `onDeleteKeyword`.
- Modify `src/components/keywords/research/research-container.tsx` — controlled sorting, opportunity toggle, GSC filter props, `onDeleteKeyword`, default visibility.

Run tests with bun. Pure tests: `bun test <file>`. Lint: `bunx biome check --write <files>`. Types: `bunx tsc --noEmit`.

---

## Task 1: `countryFlag` pure helper

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

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

`src/lib/keywords/country-flag.test.ts`:
```ts
import { expect, test } from "bun:test";
import { countryFlag } from "./country-flag";

test("countryFlag maps ISO codes to regional-indicator emoji", () => {
  expect(countryFlag("FR")).toBe("🇫🇷");
  expect(countryFlag("us")).toBe("🇺🇸"); // case-insensitive
  expect(countryFlag("GB")).toBe("🇬🇧");
});

test("countryFlag falls back to a neutral flag for invalid input", () => {
  expect(countryFlag("")).toBe("🏳️");
  expect(countryFlag(null)).toBe("🏳️");
  expect(countryFlag(undefined)).toBe("🏳️");
  expect(countryFlag("XYZ")).toBe("🏳️");
  expect(countryFlag("1F")).toBe("🏳️");
});
```

- [ ] **Step 2: Run — verify it fails**

Run: `bun test src/lib/keywords/country-flag.test.ts`
Expected: FAIL (`countryFlag` not found).

- [ ] **Step 3: Implement**

`src/lib/keywords/country-flag.ts`:
```ts
// src/lib/keywords/country-flag.ts — pure ISO-3166 alpha-2 → flag emoji.

/** Regional-indicator flag emoji for a 2-letter ISO country code.
 *  Returns a neutral white flag for missing/invalid codes. */
export function countryFlag(code: string | null | undefined): string {
  if (!code || !/^[a-zA-Z]{2}$/.test(code)) return "🏳️";
  const BASE = 0x1f1e6; // regional indicator "A"
  const cc = code.toUpperCase();
  return String.fromCodePoint(
    BASE + cc.charCodeAt(0) - 65,
    BASE + cc.charCodeAt(1) - 65,
  );
}
```

- [ ] **Step 4: Run — verify pass**

Run: `bun test src/lib/keywords/country-flag.test.ts`
Expected: PASS (2 tests).

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

```bash
bunx biome check --write src/lib/keywords/country-flag.ts src/lib/keywords/country-flag.test.ts
git add src/lib/keywords/country-flag.ts src/lib/keywords/country-flag.test.ts
git commit -m "feat(keywords): countryFlag pure helper (ISO code → flag emoji)"
```

---

## Task 2: `gscBand` + `opportunityScore` pure helpers

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

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

`src/lib/keywords/gsc-positioning.test.ts`:
```ts
import { expect, test } from "bun:test";
import { gscBand, opportunityScore } from "./gsc-positioning";
import type { GscTracking } from "./types";

const g = (position: number, impressions = 100): GscTracking => ({
  clicks: 0,
  impressions,
  ctr: 0,
  position,
  updatedAt: "2026-06-06",
});

test("gscBand classifies by position; null/undefined = none", () => {
  expect(gscBand(null)).toBe("none");
  expect(gscBand(undefined)).toBe("none");
  expect(gscBand(g(10))).toBe("top10");
  expect(gscBand(g(10.4))).toBe("quickwin");
  expect(gscBand(g(20))).toBe("quickwin");
  expect(gscBand(g(20.1))).toBe("beyond");
});

test("opportunityScore: 0 outside quick-win, impressions × proximity inside", () => {
  expect(opportunityScore(null)).toBe(0);
  expect(opportunityScore(g(5))).toBe(0); // top10
  expect(opportunityScore(g(28))).toBe(0); // beyond
  expect(opportunityScore(g(11, 900))).toBe(9000); // 900 × (21 − 11)
  expect(opportunityScore(g(20, 900))).toBe(900); // 900 × (21 − 20)
  expect(opportunityScore(g(18, 120))).toBe(360); // 120 × (21 − 18)
  expect(opportunityScore(g(11, 900))).toBeGreaterThan(
    opportunityScore(g(20, 900)),
  );
});
```

- [ ] **Step 2: Run — verify it fails**

Run: `bun test src/lib/keywords/gsc-positioning.test.ts`
Expected: FAIL (functions not found).

- [ ] **Step 3: Implement**

`src/lib/keywords/gsc-positioning.ts`:
```ts
// src/lib/keywords/gsc-positioning.ts — pure GSC positioning helpers.
import type { GscTracking } from "./types";

export type GscBand = "top10" | "quickwin" | "beyond" | "none";

/** Classify a keyword's GSC positioning into a band. */
export function gscBand(gsc: GscTracking | null | undefined): GscBand {
  if (!gsc) return "none";
  if (gsc.position <= 10) return "top10";
  if (gsc.position <= 20) return "quickwin";
  return "beyond";
}

/** Striking-distance opportunity: rewards impressions AND proximity to page 1.
 *  Non-quick-win rows score 0 so they sink in the opportunity sort. */
export function opportunityScore(gsc: GscTracking | null | undefined): number {
  if (!gsc || gscBand(gsc) !== "quickwin") return 0;
  return gsc.impressions * (21 - gsc.position);
}
```

- [ ] **Step 4: Run — verify pass**

Run: `bun test src/lib/keywords/gsc-positioning.test.ts`
Expected: PASS (2 tests).

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

```bash
bunx biome check --write src/lib/keywords/gsc-positioning.ts src/lib/keywords/gsc-positioning.test.ts
git add src/lib/keywords/gsc-positioning.ts src/lib/keywords/gsc-positioning.test.ts
git commit -m "feat(keywords): gscBand + opportunityScore pure helpers"
```

---

## Task 3: `DataTable` — optional controlled sorting + group row class

**Files:**
- Modify: `src/components/ui/data-table.tsx`

> No new test (UI plumbing; covered by the live verification in Task 9). `SortingState` is already imported in this file.

- [ ] **Step 1: Add the two optional props to the interface**

In `interface DataTableProps`, after the `rowSelection`/`onRowSelectionChange` lines, add:
```ts
  sorting?: SortingState;
  onSortingChange?: (next: SortingState) => void;
```

- [ ] **Step 2: Accept the props and make sorting controllable**

In the destructured params, add `sorting: sortingProp,` and `onSortingChange,` (next to `onRowSelectionChange`).

Replace:
```ts
  const [sorting, setSorting] = useState<SortingState>([]);
```
with:
```ts
  const [internalSorting, setInternalSorting] = useState<SortingState>([]);
  const sorting = sortingProp ?? internalSorting;
```

Replace:
```ts
    onSortingChange: setSorting,
```
with:
```ts
    onSortingChange: (updater) => {
      const next = typeof updater === "function" ? updater(sorting) : updater;
      (onSortingChange ?? setInternalSorting)(next);
    },
```

- [ ] **Step 3: Add `group` to body rows (enables hover-revealed actions)**

Find the body `<TableRow ...>` (the data row inside `table.getRowModel().rows.map(...)`, NOT the header row). Add `group` to its `className`. Example — if it reads:
```tsx
<TableRow
  key={row.id}
  data-state={row.getIsSelected() && "selected"}
  className="cursor-pointer"
  ...
```
make the className include `group` (keep existing classes), e.g. `className="group cursor-pointer"`. If the row has no className, add `className="group"`.

- [ ] **Step 4: Typecheck + lint**

Run: `bunx tsc --noEmit` (exit 0); `bunx biome check --write src/components/ui/data-table.tsx`.

- [ ] **Step 5: Commit**

```bash
git add src/components/ui/data-table.tsx
git commit -m "feat(ui): DataTable optional controlled sorting + group row class"
```

---

## Task 4: `row-actions.tsx` — copy + delete icons

**Files:**
- Create: `src/components/keywords/row-actions.tsx`

> No unit test (presentational; verified live in Task 9).

- [ ] **Step 1: Implement**

`src/components/keywords/row-actions.tsx`:
```tsx
// src/components/keywords/row-actions.tsx — per-row copy + delete actions.
"use client";

import { CopyIcon, Trash2Icon } from "lucide-react";
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";

/** Hover-revealed row actions: copy the term, delete (with confirmation). */
export function KeywordRowActions({
  term,
  onDelete,
}: {
  term: string;
  onDelete?: () => void;
}) {
  return (
    <div className="flex items-center justify-end gap-0.5 opacity-0 transition-opacity focus-within:opacity-100 group-hover:opacity-100">
      <Button
        variant="ghost"
        size="icon"
        className="size-7"
        aria-label={`Copier ${term}`}
        onClick={(e) => {
          e.stopPropagation();
          void navigator.clipboard.writeText(term);
        }}
      >
        <CopyIcon className="size-3.5" />
      </Button>
      {onDelete ? (
        <AlertDialog>
          <AlertDialogTrigger asChild>
            <Button
              variant="ghost"
              size="icon"
              className="size-7 text-muted-foreground hover:text-destructive"
              aria-label={`Supprimer ${term}`}
              onClick={(e) => e.stopPropagation()}
            >
              <Trash2Icon className="size-3.5" />
            </Button>
          </AlertDialogTrigger>
          <AlertDialogContent onClick={(e) => e.stopPropagation()}>
            <AlertDialogHeader>
              <AlertDialogTitle>Supprimer « {term} » ?</AlertDialogTitle>
              <AlertDialogDescription>
                Cette action est irréversible.
              </AlertDialogDescription>
            </AlertDialogHeader>
            <AlertDialogFooter>
              <AlertDialogCancel>Annuler</AlertDialogCancel>
              <AlertDialogAction onClick={onDelete}>Supprimer</AlertDialogAction>
            </AlertDialogFooter>
          </AlertDialogContent>
        </AlertDialog>
      ) : null}
    </div>
  );
}
```

- [ ] **Step 2: Typecheck + lint + commit**

```bash
bunx tsc --noEmit
bunx biome check --write src/components/keywords/row-actions.tsx
git add src/components/keywords/row-actions.tsx
git commit -m "feat(keywords): KeywordRowActions (copy + delete icons)"
```

---

## Task 5: `columns.tsx` — flag in term cell, drop Langue toggle, actions column, delete meta

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

- [ ] **Step 1: Add imports**

At the top of `columns.tsx`, add:
```ts
import { countryFlag } from "@/lib/keywords/country-flag";
import { KeywordRowActions } from "./row-actions";
```

- [ ] **Step 2: Extend the table meta augmentation**

In the `declare module "@tanstack/react-table"` block, add `onDeleteKeyword` to `TableMeta`:
```ts
  interface TableMeta<TData extends RowData> {
    cpcRange?: [number, number];
    onDeleteKeyword?: (id: string) => void;
  }
```

- [ ] **Step 3: Put the country flag in the term cell**

Replace the existing `term` column cell:
```ts
  {
    accessorKey: "term",
    header: "Terme",
    enableSorting: false,
    cell: ({ row }) => <span className="font-medium">{row.original.term}</span>,
  },
```
with:
```ts
  {
    accessorKey: "term",
    header: "Terme",
    enableSorting: false,
    cell: ({ row }) => (
      <span className="inline-flex items-center gap-1.5 font-medium">
        <span
          aria-label={row.original.locale.country}
          title={row.original.locale.country}
        >
          {countryFlag(row.original.locale.country)}
        </span>
        {row.original.term}
      </span>
    ),
  },
```

- [ ] **Step 4: Remove "Langue" from the toggleable columns (keep the column def for filtering)**

In `TOGGLEABLE_COLUMNS`, delete the line:
```ts
  { id: "lang", label: "Langue" },
```
Leave `DEFAULT_COLUMN_VISIBILITY.lang = false` and the `lang` column definition untouched (the language facet filter still needs the column to exist, hidden).

- [ ] **Step 5: Add the exported actions column**

After the `columns` array declaration (end of file), add:
```ts
/** Rightmost hover-revealed actions (copy + delete). Delete needs
 *  `meta.onDeleteKeyword`; omitted → only copy is shown. */
export const actionsColumn: ColumnDef<Keyword> = {
  id: "actions",
  header: () => <span className="sr-only">Actions</span>,
  enableSorting: false,
  enableHiding: false,
  enableGlobalFilter: false,
  cell: ({ row, table }) => {
    const onDelete = table.options.meta?.onDeleteKeyword;
    return (
      <KeywordRowActions
        term={row.original.term}
        onDelete={onDelete ? () => onDelete(row.original.id) : undefined}
      />
    );
  },
};
```

- [ ] **Step 6: Append the actions column to the shared `columns` array**

As the LAST entry of the `columns` array (after the `source` column), add:
```ts
  actionsColumn,
```

- [ ] **Step 7: Typecheck + lint**

Run: `bunx tsc --noEmit` (exit 0); `bunx biome check --write src/components/keywords/columns.tsx`.
Expected: tsc 0. (The reservoir already renders these `columns`; the flag + actions now appear there. `onDeleteKeyword` is wired in Task 7.)

- [ ] **Step 8: Commit**

```bash
git add src/components/keywords/columns.tsx
git commit -m "feat(keywords): country flag in term cell + actions column; drop Langue toggle"
```

---

## Task 6: `research-columns.tsx` — GSC columns + flag/badge term override + actions last

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

- [ ] **Step 1: Add imports**

Add:
```ts
import { countryFlag } from "@/lib/keywords/country-flag";
import { gscBand, opportunityScore } from "@/lib/keywords/gsc-positioning";
import { actionsColumn, columns as reservoirColumns } from "../columns";
```
(Replace the existing `import { columns as reservoirColumns } from "../columns";` with the combined import above.)

- [ ] **Step 2: Define the two GSC columns**

Above the `researchColumns` export, add:
```ts
const gscPositionColumn: ColumnDef<Keyword> = {
  id: "gscPosition",
  header: "Pos. GSC",
  accessorFn: (k) => k.gsc?.position ?? undefined,
  enableGlobalFilter: false,
  sortUndefined: "last",
  filterFn: (row, _id, value: string[]) =>
    !value?.length || value.includes(gscBand(row.original.gsc)),
  cell: ({ row }) => {
    const gsc = row.original.gsc;
    if (!gsc) return <span className="text-muted-foreground/50">—</span>;
    return (
      <span className="inline-flex items-center gap-1 tabular-nums">
        {gsc.position.toFixed(1)}
        {gscBand(gsc) === "quickwin" ? (
          <span title="Quick win (11–20)" aria-label="Quick win">
            ⚡
          </span>
        ) : null}
      </span>
    );
  },
};

// Hidden, sort-only column powering the "Trier par opportunité" toggle.
const gscOpportunityColumn: ColumnDef<Keyword> = {
  id: "gscOpportunity",
  header: () => null,
  accessorFn: (k) => opportunityScore(k.gsc),
  enableGlobalFilter: false,
  cell: () => null,
};
```

- [ ] **Step 3: Override the term cell (flag + GSC badge) on a base without the actions column**

Replace the existing `researchReservoirColumns` mapping:
```ts
const researchReservoirColumns = reservoirColumns.map((col) =>
  "accessorKey" in col && col.accessorKey === "term"
    ? {
        ...col,
        cell: ({ row }: { row: { original: Keyword } }) => {
          const kw = row.original;
          return (
            <span className="inline-flex items-center font-medium">
              {kw.term}
              {kw.gsc ? (
                ...
              ) : null}
            </span>
          );
        },
      }
    : col,
);
```
with:
```ts
const researchReservoirColumns = reservoirColumns
  .filter((col) => col.id !== "actions")
  .map((col) =>
    "accessorKey" in col && col.accessorKey === "term"
      ? {
          ...col,
          cell: ({ row }: { row: { original: Keyword } }) => {
            const kw = row.original;
            return (
              <span className="inline-flex items-center gap-1.5 font-medium">
                <span aria-label={kw.locale.country} title={kw.locale.country}>
                  {countryFlag(kw.locale.country)}
                </span>
                {kw.term}
                {kw.gsc ? (
                  <TooltipProvider>
                    <Tooltip>
                      <TooltipTrigger asChild>
                        <Badge
                          variant="secondary"
                          className="ml-1 align-middle text-[10px]"
                        >
                          GSC
                        </Badge>
                      </TooltipTrigger>
                      <TooltipContent>
                        Position {kw.gsc.position.toFixed(1)} · {kw.gsc.clicks}{" "}
                        clics · {kw.gsc.impressions} impr.
                      </TooltipContent>
                    </Tooltip>
                  </TooltipProvider>
                ) : null}
              </span>
            );
          },
        }
      : col,
  );
```

- [ ] **Step 4: Assemble `researchColumns` with GSC columns, targets, then actions last**

Replace:
```ts
export const researchColumns: ColumnDef<Keyword>[] = [
  ...researchReservoirColumns,
  {
    id: "targets",
    ...
  },
];
```
with the same array but inserting the GSC columns before `targets` and `actionsColumn` after it:
```ts
export const researchColumns: ColumnDef<Keyword>[] = [
  ...researchReservoirColumns,
  gscPositionColumn,
  gscOpportunityColumn,
  {
    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>
      );
    },
  },
  actionsColumn,
];
```

- [ ] **Step 5: Typecheck + lint**

Run: `bunx tsc --noEmit` (exit 0); `bunx biome check --write src/components/keywords/research/research-columns.tsx`.

- [ ] **Step 6: Commit**

```bash
git add src/components/keywords/research/research-columns.tsx
git commit -m "feat(gsc): Pos. GSC column + opportunity sort column; flag/actions in recherche"
```

---

## Task 7: `filters-bar.tsx` — optional GSC filter + extra columns props

**Files:**
- Modify: `src/components/keywords/filters-bar.tsx`

- [ ] **Step 1: Add the two optional props**

In `interface Props`, add:
```ts
  gscPositionFilter?: boolean;
  extraColumns?: { id: string; label: string }[];
```
And destructure them in the `FiltersBar({ ... })` signature: add `gscPositionFilter,` and `extraColumns,`.

- [ ] **Step 2: Render the GSC positioning Select (after the intent Select)**

Immediately after the closing `</Select>` of the intent filter (before the `languages && ...` block), add:
```tsx
      {gscPositionFilter ? (
        <Select
          value={facetValue(columnFilters, "gscPosition")}
          onValueChange={(v) =>
            onColumnFilters(setFacet(columnFilters, "gscPosition", v))
          }
        >
          <SelectTrigger
            className="w-52"
            aria-label="Filtrer par positionnement GSC"
          >
            <SelectValue placeholder="Positionnement GSC" />
          </SelectTrigger>
          <SelectContent position="popper">
            <SelectItem value="all">Positionnement : tous</SelectItem>
            <SelectItem value="top10">Top 10 (≤ 10)</SelectItem>
            <SelectItem value="quickwin">Quick wins (11–20)</SelectItem>
            <SelectItem value="beyond">Au-delà (&gt; 20)</SelectItem>
            <SelectItem value="none">Non positionné</SelectItem>
          </SelectContent>
        </Select>
      ) : null}
```
> `position="popper"` avoids the radix item-aligned gotcha where the top "all" option can render above the viewport.

- [ ] **Step 3: Append `extraColumns` to the Colonnes dropdown**

Replace:
```tsx
          {TOGGLEABLE_COLUMNS.map((c) => (
```
with:
```tsx
          {[...TOGGLEABLE_COLUMNS, ...(extraColumns ?? [])].map((c) => (
```

- [ ] **Step 4: Typecheck + lint**

Run: `bunx tsc --noEmit` (exit 0); `bunx biome check --write src/components/keywords/filters-bar.tsx`.
Expected: tsc 0 (reservoir passes neither new prop → unchanged).

- [ ] **Step 5: Commit**

```bash
git add src/components/keywords/filters-bar.tsx
git commit -m "feat(keywords): FiltersBar optional GSC positioning filter + extraColumns"
```

---

## Task 8: `reservoir.tsx` — wire per-row delete

**Files:**
- Modify: `src/components/keywords/reservoir.tsx`

- [ ] **Step 1: Add a single-row delete handler**

Inside the `Reservoir` component, after the existing `deleteSelected` function, add:
```ts
  async function deleteOne(id: string) {
    try {
      await deleteKeywordsAction(activeSiteId, [id]);
      refresh();
    } catch {
      // swallow — matches the codebase's fire-and-forget convention
    }
  }
```
(`deleteKeywordsAction` is already imported in this file.)

- [ ] **Step 2: Pass it through the table meta**

Change the `DataTable` meta prop from:
```tsx
        meta={{ cpcRange }}
```
to:
```tsx
        meta={{ cpcRange, onDeleteKeyword: deleteOne }}
```

- [ ] **Step 3: Typecheck + lint + commit**

```bash
bunx tsc --noEmit
bunx biome check --write src/components/keywords/reservoir.tsx
git add src/components/keywords/reservoir.tsx
git commit -m "feat(keywords): per-row delete in reservoir"
```

---

## Task 9: `research-container.tsx` — controlled sorting, opportunity toggle, GSC filter, delete

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

- [ ] **Step 1: Add imports**

Add a `SortingState` type import and the delete action:
```ts
import type {
  ColumnFiltersState,
  RowSelectionState,
  SortingState,
  VisibilityState,
} from "@tanstack/react-table";
```
(merge `SortingState` into the existing `@tanstack/react-table` type import). And add:
```ts
import {
  deleteKeywordsAction,
  fetchKeywordsForSiteAction,
} from "@/lib/keywords/actions";
```
(merge `deleteKeywordsAction` into the existing `fetchKeywordsForSiteAction` import).

- [ ] **Step 2: Add sorting state + default-hide the opportunity column**

After the existing `const [syncMsg, setSyncMsg] = useState<string | null>(null);` line, add:
```ts
  const [sorting, setSorting] = useState<SortingState>([]);
```
Change the column visibility initial state from:
```ts
  const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
    DEFAULT_COLUMN_VISIBILITY,
  );
```
to:
```ts
  const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({
    ...DEFAULT_COLUMN_VISIBILITY,
    gscOpportunity: false,
  });
```

- [ ] **Step 3: Add the opportunity toggle + single-row delete handlers**

After the `handleSync` function, add:
```ts
  const oppActive = sorting[0]?.id === "gscOpportunity";
  function toggleOpportunity() {
    setSorting(oppActive ? [] : [{ id: "gscOpportunity", desc: true }]);
  }

  async function deleteOne(id: string) {
    try {
      await deleteKeywordsAction(activeSiteId, [id]);
      await load();
    } catch {
      // swallow — matches the codebase's fire-and-forget convention
    }
  }
```

- [ ] **Step 4: Pass GSC filter + extraColumns to FiltersBar**

On the `<FiltersBar ... />` element, add these props:
```tsx
          gscPositionFilter
          extraColumns={[{ id: "gscPosition", label: "Pos. GSC" }]}
```

- [ ] **Step 5: Add the opportunity toggle button to the toolbar**

In the toolbar `<div className="flex items-center gap-2">` that holds the sync block and the Tabs, add — right before the `<Tabs ...>` element:
```tsx
          <Button
            variant={oppActive ? "default" : "outline"}
            size="sm"
            onClick={toggleOpportunity}
            aria-pressed={oppActive}
          >
            Trier par opportunité
          </Button>
```
(`Button` is already imported.)

- [ ] **Step 6: Wire sorting + delete into the DataTable**

On the `<DataTable ... />` in the list view, add `sorting`/`onSortingChange` and extend `meta`:
```tsx
            sorting={sorting}
            onSortingChange={setSorting}
```
and change:
```tsx
            meta={{ cpcRange, targets }}
```
to:
```tsx
            meta={{ cpcRange, targets, onDeleteKeyword: deleteOne }}
```

- [ ] **Step 7: Typecheck + lint**

Run: `bunx tsc --noEmit` (exit 0); `bunx biome check --write src/components/keywords/research/research-container.tsx`.

- [ ] **Step 8: Commit**

```bash
git add src/components/keywords/research/research-container.tsx
git commit -m "feat(gsc): recherche opportunity sort toggle + GSC filter + per-row delete"
```

---

## Task 10: Full checks + live verification

- [ ] **Step 1: Static checks**

Run:
```bash
bunx tsc --noEmit
bun test src/lib/keywords/country-flag.test.ts src/lib/keywords/gsc-positioning.test.ts
bunx biome check src/components/keywords src/components/ui/data-table.tsx src/lib/keywords
```
Expected: tsc 0; pure tests pass; biome clean.

- [ ] **Step 2: Live — reservoir (d3k)**

`d3k agent-browser open http://localhost:3000/keywords/reservoir`:
- Each term cell shows a country flag prefix; no "Langue" entry in the Colonnes menu.
- Hovering a row reveals copy + delete icons at the right; copy puts the term on the clipboard; delete opens the confirmation dialog and removes the row on confirm.
- `d3k errors` clean.

- [ ] **Step 3: Live — recherche (d3k)**

`d3k agent-browser open http://localhost:3000/keywords/recherche`:
- Flag + (when applicable) GSC badge in the term cell; "Pos. GSC" column sorts (⚡ on 11–20; "—" rows always last).
- "Positionnement GSC" filter: each of the 5 bands filters correctly.
- "Trier par opportunité" toggle floats high-impression quick-wins to the top; toggling off restores.
- Copy + delete row actions work; vol/kd/cpc columns unchanged.
- `d3k errors` clean.

- [ ] **Step 4: Final fixups commit (if any)**

```bash
git add -A && git commit -m "chore(gsc): table UX verification fixups"
```

---

## Notes / risks

- `sortUndefined: "last"` keeps non-positioned rows at the bottom in both sort directions; if the installed TanStack version lacks it, fall back to a custom comparator plus a stable secondary sort (verify in Task 6 / Task 10).
- The `lang` column stays defined-but-hidden so the language facet filter and reservoir's default-language filter keep working; only the toggleable/visible column is removed.
- Per-row delete is permanent (no undo/toast infra) — hence the AlertDialog confirmation.
- Keep the GSC column/filter/sort recherche-only; the flag + actions are intentionally shared across both tables.
- Branch: `feature/gsc-positioning-actionable` (already created off `dev`).
