-
- {description}
-
-
-
+ testId={`integration-card-${pluginId}-${id}`}
+ href={href}
+ label={label}
+ Icon={Icon}
+ description={description}
+ control={
+ Action ? (
+
+
+
+ ) : null
+ }
+ />
);
})}
diff --git a/apps/web/e2e/tests/integrations/integrations-index-card-navigation.spec.ts b/apps/web/e2e/tests/integrations/integrations-index-card-navigation.spec.ts
new file mode 100644
index 0000000000..f735fa9b92
--- /dev/null
+++ b/apps/web/e2e/tests/integrations/integrations-index-card-navigation.spec.ts
@@ -0,0 +1,33 @@
+import { expect, test } from "../../fixtures/test-base";
+
+test.describe("integrations index card navigation", () => {
+ test("clicking a card body opens the workspace-scoped integration settings page", async ({
+ testPage,
+ seedData,
+ }) => {
+ const integrationsPath = `/settings/workspaces/${encodeURIComponent(seedData.workspaceId)}/integrations`;
+ await testPage.goto(integrationsPath);
+
+ const card = testPage.getByTestId("integration-card-github");
+ await expect(card).toBeVisible();
+
+ const cardLink = card.getByRole("link", { name: "GitHub" });
+ await cardLink.focus();
+ await expect(cardLink).toBeFocused();
+ await expect(cardLink).toHaveClass(/focus-visible:ring-inset/);
+
+ const cardBox = await card.boundingBox();
+ const descriptionBox = await card.locator("p").boundingBox();
+ expect(cardBox).not.toBeNull();
+ expect(descriptionBox).not.toBeNull();
+ await card.click({
+ position: {
+ x: descriptionBox!.x - cardBox!.x + descriptionBox!.width / 2,
+ y: descriptionBox!.y - cardBox!.y + descriptionBox!.height / 2,
+ },
+ });
+
+ await expect(testPage).toHaveURL(`${integrationsPath}/github`);
+ await expect(testPage.getByTestId("github-integration-heading")).toBeVisible();
+ });
+});
diff --git a/apps/web/e2e/tests/integrations/mobile-integrations-index-card-navigation.spec.ts b/apps/web/e2e/tests/integrations/mobile-integrations-index-card-navigation.spec.ts
new file mode 100644
index 0000000000..15d54bce4f
--- /dev/null
+++ b/apps/web/e2e/tests/integrations/mobile-integrations-index-card-navigation.spec.ts
@@ -0,0 +1,28 @@
+import { expect, test } from "../../fixtures/test-base";
+
+test.describe("integrations index card navigation on mobile", () => {
+ test("tapping a card body opens the workspace-scoped integration settings page", async ({
+ testPage,
+ seedData,
+ }) => {
+ const integrationsPath = `/settings/workspaces/${encodeURIComponent(seedData.workspaceId)}/integrations`;
+ await testPage.goto(integrationsPath);
+
+ const card = testPage.getByTestId("integration-card-github");
+ await expect(card).toBeVisible();
+
+ const cardBox = await card.boundingBox();
+ const descriptionBox = await card.locator("p").boundingBox();
+ expect(cardBox).not.toBeNull();
+ expect(descriptionBox).not.toBeNull();
+ await card.tap({
+ position: {
+ x: descriptionBox!.x - cardBox!.x + descriptionBox!.width / 2,
+ y: descriptionBox!.y - cardBox!.y + descriptionBox!.height / 2,
+ },
+ });
+
+ await expect(testPage).toHaveURL(`${integrationsPath}/github`);
+ await expect(testPage.getByTestId("github-integration-heading")).toBeVisible();
+ });
+});
diff --git a/docs/plans/clickable-integration-cards/plan.md b/docs/plans/clickable-integration-cards/plan.md
new file mode 100644
index 0000000000..aaba6746c4
--- /dev/null
+++ b/docs/plans/clickable-integration-cards/plan.md
@@ -0,0 +1,108 @@
+---
+spec: docs/specs/integrations/clickable-integration-cards.md
+created: 2026-08-21
+status: complete
+---
+
+# Implementation Plan: Clickable integration cards
+
+## Overview
+
+Update the shared integrations index card composition so each card has one
+accessible, full-card `AppLink` to the route it already exposes. Keep the
+native switch and plugin action above that navigation layer as independent
+interactive controls. Validate the shared component with its unit test and
+prove native navigation on both desktop and mobile Playwright projects.
+
+This is a frontend-only change. It does not add an API, state shape, route, or
+persistence boundary.
+
+## Frontend
+
+### Native and plugin integration cards
+
+Update `apps/web/components/integrations/integrations-index-page.tsx`:
+
+- Keep the existing `rootHref` and per-card `href` derivation for global and
+ workspace-scoped routes.
+- Replace the separate title and description links with one full-card `AppLink`
+ overlay that has the integration label as its accessible name.
+- Keep the visible icon, label, and description as card content while allowing
+ pointer events to reach the overlay for all non-control card areas.
+- Place the native enable control and plugin `action` in an interactive layer
+ above the overlay so toggling or activating them cannot navigate.
+- Preserve existing `data-testid` card identifiers, grid geometry, hover state,
+ plugin error boundary, and translated description content.
+
+### Unit coverage
+
+Update `apps/web/app/settings/integrations/page.test.tsx` to assert that native
+and plugin cards expose the expected accessible link and route, and that the
+native switch still drafts its state without calling navigation.
+
+## Backend
+
+None.
+
+## Tests
+
+- **What:** Native and plugin cards expose the existing global and
+ workspace-scoped routes through a full-card navigation target, while the
+ switch remains non-navigating.
+ **File:** `apps/web/app/settings/integrations/page.test.tsx`
+ **How:** Render the index with the existing navigation spy, inspect the card
+ link `href`, activate it, and separately activate the switch.
+- **What:** A pointer click on the native card body navigates from the desktop
+ integrations index.
+ **File:** `apps/web/e2e/tests/integrations/integrations-index-card-navigation.spec.ts`
+ **How:** Open the index, click a card body away from the switch, and assert the
+ workspace-scoped integration URL and heading.
+- **What:** A touch tap on the native card body navigates from the mobile
+ integrations index.
+ **File:** `apps/web/e2e/tests/integrations/mobile-integrations-index-card-navigation.spec.ts`
+ **How:** Use the configured `mobile-chrome` project, tap a card body, and
+ assert the same workspace-scoped route and integration page content.
+
+## E2E Tests
+
+- **Scenario:** Global or workspace index, body click on a native card, route
+ changes to that integration settings page.
+ **File:** `apps/web/e2e/tests/integrations/integrations-index-card-navigation.spec.ts`
+ **What to verify:** The URL includes the active workspace and integration
+ slug, and the destination heading is visible.
+- **Scenario:** Phone-sized workspace index, body tap on a native card, route
+ changes without a title-only interaction.
+ **File:** `apps/web/e2e/tests/integrations/mobile-integrations-index-card-navigation.spec.ts`
+ **What to verify:** The mobile project reaches the same integration settings
+ route and visible page heading.
+
+## Verification Results
+
+Implementation and verification completed.
+
+- Focused integration index unit suite: 12 passed.
+- Targeted ESLint for changed frontend and E2E files: passed.
+- Web TypeScript check with `NODE_OPTIONS=--max-old-space-size=8192`: passed.
+- Desktop blank-body navigation E2E: 1 passed.
+- Mobile blank-body navigation E2E: 1 passed.
+- Existing desktop enable/disable toggle E2E: 1 passed.
+
+The managed E2E runner rebuilt the backend and production Vite assets for each
+project run and completed its fixture setup and teardown successfully.
+
+## Implementation Waves And Parallel Candidates
+
+Wave 1:
+
+- [x] [Task 01: Card navigation](task-01-card-navigation.md)
+
+Wave 2:
+
+- [x] [Task 02: Navigation E2E coverage](task-02-navigation-e2e.md)
+
+Tasks are sequential because the E2E selectors and route assertions depend on
+the final card markup.
+
+## Open Questions
+
+None.
diff --git a/docs/plans/clickable-integration-cards/task-01-card-navigation.md b/docs/plans/clickable-integration-cards/task-01-card-navigation.md
new file mode 100644
index 0000000000..08488478ed
--- /dev/null
+++ b/docs/plans/clickable-integration-cards/task-01-card-navigation.md
@@ -0,0 +1,71 @@
+---
+id: "01-card-navigation"
+title: "Integration card navigation"
+status: complete
+wave: 1
+depends_on: []
+plan: "plan.md"
+spec: "../../specs/integrations/clickable-integration-cards.md"
+---
+
+# Task 01: Integration card navigation
+
+## Acceptance
+
+- Native and plugin integration cards expose one accessible full-card link that
+ uses their existing global or workspace-scoped `href`.
+- The native switch and plugin action remain clickable without triggering card
+ navigation.
+- The focused component test covers the link route and switch isolation.
+
+## Verification
+
+From the repository root, after the fresh-worktree install if dependencies are
+missing:
+
+```bash
+cd apps && pnpm install --frozen-lockfile
+cd apps && pnpm --filter @kandev/web test -- app/settings/integrations/page.test.tsx
+```
+
+## Files likely touched
+
+- `apps/web/components/integrations/integrations-index-page.tsx`
+- `apps/web/app/settings/integrations/page.test.tsx`
+
+## Dependencies
+
+None.
+
+## Parallelism
+
+`sequential`.
+
+## Inputs
+
+- `docs/specs/integrations/clickable-integration-cards.md`, What and Scenarios.
+- `docs/plans/clickable-integration-cards/plan.md`, Frontend and Tests.
+- Existing `AppLink` behavior in `apps/web/components/routing/app-link.tsx`.
+
+## Output contract
+
+Report the component and unit-test files changed, the focused test command and
+result, any accessibility or pointer-event risk, and the updated task and plan
+status before handing off to Task 02.
+
+## Results
+
+Updated the shared integration card composition for native and plugin cards.
+Each card now exposes one accessible overlay `AppLink`, while the native
+switch and plugin action remain above that link as independent controls.
+
+Focused verification passed:
+
+```text
+pnpm --filter @kandev/web test -- app/settings/integrations/page.test.tsx
+12 tests passed
+```
+
+Targeted ESLint and the web TypeScript check also passed. The default Node heap
+size was insufficient for the repository-wide typecheck, so it was rerun with
+`NODE_OPTIONS=--max-old-space-size=8192`.
diff --git a/docs/plans/clickable-integration-cards/task-02-navigation-e2e.md b/docs/plans/clickable-integration-cards/task-02-navigation-e2e.md
new file mode 100644
index 0000000000..27f7e195ab
--- /dev/null
+++ b/docs/plans/clickable-integration-cards/task-02-navigation-e2e.md
@@ -0,0 +1,75 @@
+---
+id: "02-navigation-e2e"
+title: "Integration card navigation E2E coverage"
+status: complete
+wave: 2
+depends_on: ["01-card-navigation"]
+plan: "plan.md"
+spec: "../../specs/integrations/clickable-integration-cards.md"
+---
+
+# Task 02: Integration card navigation E2E coverage
+
+## Acceptance
+
+- Desktop Playwright coverage clicks a native integration card body and reaches
+ the workspace-scoped integration settings page.
+- Mobile Playwright coverage taps the same card body with the configured
+ `mobile-chrome` project and reaches the same destination.
+- The tests use stable card or route selectors and do not regress the existing
+ switch-isolation coverage.
+
+## Verification
+
+From the repository root, the managed runner must build the production
+frontend before each project run:
+
+```bash
+cd apps && pnpm install --frozen-lockfile
+cd apps/web && pnpm e2e:run --project chromium tests/integrations/integrations-index-card-navigation.spec.ts
+cd apps/web && pnpm e2e:run --project mobile-chrome tests/integrations/mobile-integrations-index-card-navigation.spec.ts
+```
+
+## Files likely touched
+
+- `apps/web/e2e/tests/integrations/integrations-index-card-navigation.spec.ts`
+- `apps/web/e2e/tests/integrations/mobile-integrations-index-card-navigation.spec.ts`
+
+## Dependencies
+
+Task 01.
+
+## Parallelism
+
+`sequential`.
+
+## Inputs
+
+- `docs/specs/integrations/clickable-integration-cards.md`, Scenarios.
+- `docs/plans/clickable-integration-cards/plan.md`, E2E Tests.
+- Existing fixtures from `apps/web/e2e/fixtures/test-base.ts`.
+- Existing layout and toggle tests in `apps/web/e2e/tests/integrations/`.
+
+## Output contract
+
+Report both exact managed-runner commands, discovered test counts, pass/fail
+results, build and teardown evidence, and synchronized task and plan status.
+
+## Results
+
+Added desktop and mobile card-body navigation coverage. Both tests click or tap
+the lower card body away from the title, description, and native switch, then
+assert the workspace-scoped integration URL and destination heading.
+
+Managed verification passed:
+
+```text
+pnpm e2e:run --project chromium tests/integrations/integrations-index-card-navigation.spec.ts
+1 passed
+
+pnpm e2e:run --project mobile-chrome tests/integrations/mobile-integrations-index-card-navigation.spec.ts
+1 passed
+
+pnpm e2e:run --project chromium tests/integrations/integrations-index-enabled-toggle.spec.ts
+1 passed
+```
diff --git a/docs/specs/INDEX.md b/docs/specs/INDEX.md
index a411f051bb..5affabf59f 100644
--- a/docs/specs/INDEX.md
+++ b/docs/specs/INDEX.md
@@ -159,6 +159,7 @@ Per-workspace credentials and triage triggers for external services.
| [jira-status-filter](jira-status-filter/spec.md) | shipped |
| [pr-outcome-attribution](pr-outcome-attribution/spec.md) | shipped |
| [enable-disable-toggle](integrations/enable-disable-toggle.md) | shipped |
+| [clickable-integration-cards](integrations/clickable-integration-cards.md) | shipped |
## workspaces/ — workspace lifecycle
diff --git a/docs/specs/integrations/clickable-integration-cards.md b/docs/specs/integrations/clickable-integration-cards.md
new file mode 100644
index 0000000000..575452791f
--- /dev/null
+++ b/docs/specs/integrations/clickable-integration-cards.md
@@ -0,0 +1,59 @@
+---
+status: shipped
+created: 2026-08-21
+owner: Codex
+---
+
+# Clickable integration cards
+
+## Why
+
+The integrations index currently makes only the title and description links
+navigation targets. Users expect the card itself to be the obvious entry point,
+especially on a touch viewport, so requiring a precise tap on text makes the
+settings page harder to use.
+
+## What
+
+- Every native integration card on the global and workspace-scoped integrations
+ index exposes the existing integration route as a full-card navigation target.
+- Every plugin integration card uses the same full-card navigation behavior and
+ preserves its existing workspace-scoped route when one is present.
+- Clicking or tapping any card area that is not an interactive secondary control
+ opens that integration's settings page through the existing in-app navigation.
+- The native enable/disable switch and any plugin card action remain independent
+ controls. Activating either control does not navigate to the integration page.
+- The card navigation target has an accessible integration name and remains
+ usable with keyboard focus and activation.
+- Desktop and mobile retain the same route and control semantics. Mobile uses
+ the existing single-column integrations index and does not add an intermediate
+ drawer or another navigation step.
+
+## Scenarios
+
+- **GIVEN** the global integrations index, **WHEN** the user clicks the body of
+ a native Azure DevOps card outside its switch, **THEN** the app navigates to
+ `/settings/integrations/azure-devops` through the normal SPA route transition.
+- **GIVEN** a workspace-scoped integrations index, **WHEN** the user clicks the
+ body of a native card outside its switch, **THEN** the app navigates to the
+ matching `/settings/workspaces//integrations/`
+ route.
+- **GIVEN** a plugin integration card, **WHEN** the user clicks its body outside
+ the plugin action, **THEN** the app navigates to that plugin's existing
+ integration settings route.
+- **GIVEN** an integration card with an enabled or disabled switch, **WHEN** the
+ user activates the switch, **THEN** the switch changes or drafts its state and
+ the browser remains on the integrations index.
+- **GIVEN** the integrations index on a phone-sized viewport, **WHEN** the user
+ taps the body of a card outside its switch, **THEN** the app navigates to the
+ same integration settings route without requiring a title or description tap.
+- **GIVEN** a focused card navigation target, **WHEN** the user presses Enter,
+ **THEN** the app opens the same integration settings route as a pointer or
+ touch activation.
+
+## Out of scope
+
+- Changing integration routes, workspace selection, enable/disable persistence,
+ plugin registration, or integration page content.
+- Making the switch or plugin action navigate when its own control is activated.
+- Adding a new mobile navigation surface or changing the integrations grid.