diff --git a/apps/gittensory-ui/src/components/site/app-panels/contributor-quality-table.tsx b/apps/gittensory-ui/src/components/site/app-panels/contributor-quality-table.tsx
index cdf66a8f42..0204d3ddd5 100644
--- a/apps/gittensory-ui/src/components/site/app-panels/contributor-quality-table.tsx
+++ b/apps/gittensory-ui/src/components/site/app-panels/contributor-quality-table.tsx
@@ -1,4 +1,5 @@
import { BoundaryBadge, StatusPill } from "@/components/site/control-primitives";
+import { TableScroll } from "@/components/site/data-table";
import { EmptyState } from "@/components/site/state-views";
import {
QUALITY_BAND_TONE,
@@ -30,13 +31,22 @@ export function ContributorQualityTable({
description="Quality bands appear once this maintainer's repos have cached open pull requests to shape."
/>
) : (
-
-
+
+
+
+ Contributors with their quality band and open pull request count.
+
- | Contributor |
- Band |
- Open PRs |
+
+ Contributor
+ |
+
+ Band
+ |
+
+ Open PRs
+ |
@@ -58,7 +68,7 @@ export function ContributorQualityTable({
))}
-
+
)}
);
diff --git a/apps/gittensory-ui/src/components/site/app-panels/maintainer-panel.tsx b/apps/gittensory-ui/src/components/site/app-panels/maintainer-panel.tsx
index b8be6d289d..f79ed8c1ff 100644
--- a/apps/gittensory-ui/src/components/site/app-panels/maintainer-panel.tsx
+++ b/apps/gittensory-ui/src/components/site/app-panels/maintainer-panel.tsx
@@ -34,6 +34,7 @@ import { MaintainerSettings } from "@/components/site/app-panels/maintainer-sett
import { OnboardingPreviewCard } from "@/components/site/app-panels/onboarding-preview-card";
import { CheckRunReadinessTable } from "@/components/site/check-run-readiness-table";
import type { CheckRunReadinessTableData } from "@/components/site/check-run-readiness-model";
+import { TableScroll } from "@/components/site/data-table";
import { StatCard } from "@/components/site/primitives";
import { RefreshMeta } from "@/components/site/refresh-meta";
import { EmptyState, LoadingState, StateBoundary } from "@/components/site/state-views";
@@ -340,52 +341,71 @@ function MaintainerDashboardView({
private
-
-
-
- | PR |
- Title |
- Author |
- Bucket |
- Slop |
- Reason |
-
-
-
- {data.reviewability.map((row) => (
-
- |
- {row.pr}
- |
- {row.title} |
- {row.author} |
-
-
- {row.bucket}
-
- |
-
- {row.slop ? (
-
- {row.slop.band} {row.slop.risk}
-
- ) : (
-
- —
-
- )}
- |
- {row.reason} |
+
+
+
+ Reviewable pull requests with bucket, slop band, and reason.
+
+
+
+ |
+ PR
+ |
+
+ Title
+ |
+
+ Author
+ |
+
+ Bucket
+ |
+
+ Slop
+ |
+
+ Reason
+ |
- ))}
-
-
+
+
+ {data.reviewability.map((row) => (
+
+ |
+ {row.pr}
+ |
+ {row.title} |
+
+ {row.author}
+ |
+
+
+ {row.bucket}
+
+ |
+
+ {row.slop ? (
+
+ {row.slop.band} {row.slop.risk}
+
+ ) : (
+
+ —
+
+ )}
+ |
+ {row.reason} |
+
+ ))}
+
+
+
@@ -717,24 +737,35 @@ export function PreviewResult({
{preview.installation?.permissionRemediation.length ? (
-
-
-
- | Permission |
- Current |
- Required |
-
-
-
- {preview.installation.permissionRemediation.map((row) => (
-
- | {row.permission} |
- {row.currentAccess} |
- {row.requiredAccess} |
+
+
+
+ GitHub App permission remediation: current versus required access per permission.
+
+
+
+ |
+ Permission
+ |
+
+ Current
+ |
+
+ Required
+ |
- ))}
-
-
+
+
+ {preview.installation.permissionRemediation.map((row) => (
+
+ | {row.permission} |
+ {row.currentAccess} |
+ {row.requiredAccess} |
+
+ ))}
+
+
+
) : null}
diff --git a/apps/gittensory-ui/src/components/site/control-primitives.tsx b/apps/gittensory-ui/src/components/site/control-primitives.tsx
index 2f61b308e7..68cc377f07 100644
--- a/apps/gittensory-ui/src/components/site/control-primitives.tsx
+++ b/apps/gittensory-ui/src/components/site/control-primitives.tsx
@@ -26,12 +26,12 @@ export function StatusPill({
return (
-
+
{children}
);
@@ -53,12 +53,12 @@ export function BoundaryBadge({ boundary, className }: { boundary: Boundary; cla
return (
-
+
{label}
);
diff --git a/apps/gittensory-ui/src/components/site/data-table.test.tsx b/apps/gittensory-ui/src/components/site/data-table.test.tsx
new file mode 100644
index 0000000000..255cf1e66f
--- /dev/null
+++ b/apps/gittensory-ui/src/components/site/data-table.test.tsx
@@ -0,0 +1,44 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import { TableScroll } from "@/components/site/data-table";
+
+describe("TableScroll", () => {
+ it("wraps its table in a keyboard-focusable, labelled scroll region (WCAG 2.1.1)", () => {
+ render(
+
+
+ Example data rows
+
+
+ | cell |
+
+
+
+ ,
+ );
+ const region = screen.getByRole("region", { name: "Example data" });
+ // A bare overflow-x-auto div is not a tab stop; this one is, so keyboard users can scroll it.
+ expect(region.tabIndex).toBe(0);
+ expect(region.className).toContain("overflow-x-auto");
+ // The inner table takes its accessible name from the caption, not the region label.
+ expect(screen.getByRole("table", { name: "Example data rows" })).toBeTruthy();
+ });
+
+ it("merges a caller className onto the scroll region without dropping the base classes", () => {
+ render(
+
+
+ ,
+ );
+ const region = screen.getByRole("region", { name: "Wide table" });
+ expect(region.className).toContain("mt-4");
+ expect(region.className).toContain("overflow-x-auto");
+ });
+});
diff --git a/apps/gittensory-ui/src/components/site/data-table.tsx b/apps/gittensory-ui/src/components/site/data-table.tsx
new file mode 100644
index 0000000000..a9f94eb6c7
--- /dev/null
+++ b/apps/gittensory-ui/src/components/site/data-table.tsx
@@ -0,0 +1,33 @@
+import type { ReactNode } from "react";
+
+import { cn } from "@/lib/utils";
+
+/**
+ * Accessible horizontal-scroll container for a wide data table (#794). A table that overflows its
+ * column area on a narrow viewport can be scrolled by pointer/trackpad but NOT by keyboard alone:
+ * marking the scroll region focusable (`tabIndex={0}`) with `role="region"` + an `aria-label` gives
+ * keyboard users a real tab stop to scroll from (WCAG 2.1.1 Keyboard) and names the region for
+ * assistive tech. Pair with a `` and `scope="col"` headers on the table inside — the three
+ * together are the standard responsive-table a11y pattern this app's bare `overflow-x-auto` divs and
+ * caption-less tables were missing.
+ */
+export function TableScroll({
+ label,
+ className,
+ children,
+}: {
+ label: string;
+ className?: string;
+ children: ReactNode;
+}) {
+ return (
+
+ {children}
+
+ );
+}