refactor(web): migrate StatusTabs into issues list and user profile

Quentin Gliech and Claude Opus 4.6 (1M context) created

Replace ~45 lines of inline status toggle code in each consumer with
the StatusTabs composition component. Indicators accept an `active`
prop for explicit state control (needed because active state comes
from query parsing, not router URL matching).

Also remove unused cn/CircleDot/CircleCheck imports from issues list.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Change summary

webui2/src/components/shared/__snapshots__/status-tabs.test.tsx.snap |  8 
webui2/src/components/shared/status-tabs.tsx                         | 11 
webui2/src/routes/$repo/_issues/issues/index.tsx                     | 57 
webui2/src/routes/$repo/_issues/user/$id.tsx                         | 61 
4 files changed, 42 insertions(+), 95 deletions(-)

Detailed changes

webui2/src/components/shared/__snapshots__/status-tabs.test.tsx.snap 🔗

@@ -10,7 +10,7 @@ exports[`StatusTabs/Default matches snapshot 1`] = `
     >
       <svg
         aria-hidden="true"
-        class="lucide lucide-circle-dot size-4 group-[.active]:text-green-600 dark:group-[.active]:text-green-400"
+        class="lucide lucide-circle-dot size-4"
         fill="none"
         height="24"
         stroke="currentColor"
@@ -34,7 +34,7 @@ exports[`StatusTabs/Default matches snapshot 1`] = `
       </svg>
       Open
       <span
-        class="bg-muted ml-0.5 rounded-full px-1.5 py-0.5 text-xs leading-none"
+        class="bg-muted ml-0.5 rounded-full px-1.5 py-0.5 text-xs leading-none tabular-nums"
       >
         12
       </span>
@@ -44,7 +44,7 @@ exports[`StatusTabs/Default matches snapshot 1`] = `
     >
       <svg
         aria-hidden="true"
-        class="lucide lucide-circle-check size-4 group-[.active]:text-purple-600 dark:group-[.active]:text-purple-400"
+        class="lucide lucide-circle-check size-4"
         fill="none"
         height="24"
         stroke="currentColor"
@@ -66,7 +66,7 @@ exports[`StatusTabs/Default matches snapshot 1`] = `
       </svg>
       Closed
       <span
-        class="bg-muted ml-0.5 rounded-full px-1.5 py-0.5 text-xs leading-none"
+        class="bg-muted ml-0.5 rounded-full px-1.5 py-0.5 text-xs leading-none tabular-nums"
       >
         5
       </span>

webui2/src/components/shared/status-tabs.tsx 🔗

@@ -46,21 +46,22 @@ export const Tab: LinkComponent<typeof TabComponent> = (props) => {
 };
 
 interface IndicatorProps {
+  active?: boolean;
   className?: string;
 }
 
-export function OpenIndicator({ className }: IndicatorProps) {
+export function OpenIndicator({ active, className }: IndicatorProps) {
   return (
     <CircleDot
-      className={cn("size-4 group-[.active]:text-green-600 dark:group-[.active]:text-green-400", className)}
+      className={cn("size-4", active && "text-green-600 dark:text-green-400", className)}
     />
   );
 }
 
-export function ClosedIndicator({ className }: IndicatorProps) {
+export function ClosedIndicator({ active, className }: IndicatorProps) {
   return (
     <CircleCheck
-      className={cn("size-4 group-[.active]:text-purple-600 dark:group-[.active]:text-purple-400", className)}
+      className={cn("size-4", active && "text-purple-600 dark:text-purple-400", className)}
     />
   );
 }
@@ -71,7 +72,7 @@ interface CountProps {
 
 export function Count({ children }: CountProps) {
   return (
-    <span className="bg-muted ml-0.5 rounded-full px-1.5 py-0.5 text-xs leading-none">
+    <span className="bg-muted ml-0.5 rounded-full px-1.5 py-0.5 text-xs leading-none tabular-nums">
       {children}
     </span>
   );

webui2/src/routes/$repo/_issues/issues/index.tsx 🔗

@@ -1,13 +1,14 @@
 import { useReadQuery } from "@apollo/client/react";
 import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
 import { formatDistanceToNow } from "date-fns";
-import { CircleDot, CircleCheck, Search } from "lucide-react";
+import { Search } from "lucide-react";
 import { useMemo, useState } from "react";
 import * as v from "valibot";
 
 import { type BugListQuery, BugListDocument } from "@/__generated__/graphql";
 import { IssueFilters } from "@/components/shared/issue-filters";
 import * as IssueRow from "@/components/shared/issue-row";
+import * as StatusTabs from "@/components/shared/status-tabs";
 import { LabelBadgeLink } from "@/components/shared/label-badge";
 import { EmptyState } from "@/components/shared/empty-state";
 import * as Pagination from "@/components/shared/pagination";
@@ -17,7 +18,6 @@ import { Button } from "@/components/ui/button";
 import { Skeleton } from "@/components/ui/skeleton";
 import type { SortValue, StatusFilter } from "@/lib/query-utils";
 import { buildBaseQuery, buildQueryString, parseQueryString } from "@/lib/query-utils";
-import { cn } from "@/lib/utils";
 
 const issuesSearchSchema = v.object({
   q: v.fallback(v.string(), "status:open"),
@@ -182,53 +182,28 @@ function RouteComponent() {
       <div className="border-border rounded-md border">
         {/* Open / Closed toggle + filter dropdowns */}
         <div className="border-border flex items-center gap-2 overflow-x-auto border-b px-4 py-2">
-          <div className="flex shrink-0 items-center gap-1">
-            <Link
+          <StatusTabs.Root className="shrink-0">
+            <StatusTabs.Tab
               to="/$repo/issues"
-              params={{ repo: repo }}
+              params={{ repo }}
               search={{ q: queryWithStatus("open"), after: "" }}
-              className={cn(
-                "flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
-                statusFilter === "open"
-                  ? "bg-accent text-accent-foreground"
-                  : "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
-              )}
+              className={statusFilter === "open" ? "bg-accent text-accent-foreground" : ""}
             >
-              <CircleDot
-                className={cn(
-                  "size-4",
-                  statusFilter === "open" && "text-green-600 dark:text-green-400",
-                )}
-              />
+              <StatusTabs.OpenIndicator active={statusFilter === "open"} />
               Open
-              <span className="bg-muted ml-0.5 rounded-full px-1.5 py-0.5 text-xs leading-none tabular-nums">
-                {openCount}
-              </span>
-            </Link>
-
-            <Link
+              <StatusTabs.Count>{openCount}</StatusTabs.Count>
+            </StatusTabs.Tab>
+            <StatusTabs.Tab
               to="/$repo/issues"
-              params={{ repo: repo }}
+              params={{ repo }}
               search={{ q: queryWithStatus("closed"), after: "" }}
-              className={cn(
-                "flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
-                statusFilter === "closed"
-                  ? "bg-accent text-accent-foreground"
-                  : "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
-              )}
+              className={statusFilter === "closed" ? "bg-accent text-accent-foreground" : ""}
             >
-              <CircleCheck
-                className={cn(
-                  "size-4",
-                  statusFilter === "closed" && "text-purple-600 dark:text-purple-400",
-                )}
-              />
+              <StatusTabs.ClosedIndicator active={statusFilter === "closed"} />
               Closed
-              <span className="bg-muted ml-0.5 rounded-full px-1.5 py-0.5 text-xs leading-none tabular-nums">
-                {closedCount}
-              </span>
-            </Link>
-          </div>
+              <StatusTabs.Count>{closedCount}</StatusTabs.Count>
+            </StatusTabs.Tab>
+          </StatusTabs.Root>
 
           <div className="ml-auto">
             <IssueFilters

webui2/src/routes/$repo/_issues/user/$id.tsx 🔗

@@ -6,22 +6,18 @@
 import { useReadQuery } from "@apollo/client/react";
 import { createFileRoute, Link } from "@tanstack/react-router";
 import { formatDistanceToNow } from "date-fns";
-import {
-  CircleDot,
-  CircleCheck,
-  ShieldCheck,
-} from "lucide-react";
+import { CircleDot, CircleCheck, ShieldCheck } from "lucide-react";
 import * as v from "valibot";
 
 import { type UserProfileQuery, UserProfileDocument } from "@/__generated__/graphql";
 import * as IssueRow from "@/components/shared/issue-row";
 import { LabelBadge } from "@/components/shared/label-badge";
-import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
-import { BackLink } from "@/components/ui/back-link";
 import { EmptyState } from "@/components/shared/empty-state";
 import * as Pagination from "@/components/shared/pagination";
+import * as StatusTabs from "@/components/shared/status-tabs";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { BackLink } from "@/components/ui/back-link";
 import { Skeleton } from "@/components/ui/skeleton";
-import { cn } from "@/lib/utils";
 
 const profileSearchSchema = v.object({
   status: v.fallback(v.picklist(["open", "closed"]), "open"),
@@ -115,53 +111,28 @@ function RouteComponent() {
       {/* ── Issue list ─────────────────────────────────────────────────── */}
       <div className="border-border rounded-md border">
         {/* Open / Closed toggle */}
-        <div className="border-border flex items-center gap-1 border-b px-4 py-2">
-          <Link
+        <StatusTabs.Root className="border-border border-b px-4 py-2">
+          <StatusTabs.Tab
             to="/$repo/user/$id"
             params={{ repo, id }}
             search={{ status: "open", after: "" }}
-            className={cn(
-              "flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
-              statusFilter === "open"
-                ? "bg-accent text-accent-foreground"
-                : "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
-            )}
+            className={statusFilter === "open" ? "bg-accent text-accent-foreground" : ""}
           >
-            <CircleDot
-              className={cn(
-                "size-4",
-                statusFilter === "open" && "text-green-600 dark:text-green-400",
-              )}
-            />
+            <StatusTabs.OpenIndicator active={statusFilter === "open"} />
             Open
-            <span className="bg-muted ml-0.5 rounded-full px-1.5 py-0.5 text-xs leading-none">
-              {openCount}
-            </span>
-          </Link>
-
-          <Link
+            <StatusTabs.Count>{openCount}</StatusTabs.Count>
+          </StatusTabs.Tab>
+          <StatusTabs.Tab
             to="/$repo/user/$id"
             params={{ repo, id }}
             search={{ status: "closed", after: "" }}
-            className={cn(
-              "flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
-              statusFilter === "closed"
-                ? "bg-accent text-accent-foreground"
-                : "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
-            )}
+            className={statusFilter === "closed" ? "bg-accent text-accent-foreground" : ""}
           >
-            <CircleCheck
-              className={cn(
-                "size-4",
-                statusFilter === "closed" && "text-purple-600 dark:text-purple-400",
-              )}
-            />
+            <StatusTabs.ClosedIndicator active={statusFilter === "closed"} />
             Closed
-            <span className="bg-muted ml-0.5 rounded-full px-1.5 py-0.5 text-xs leading-none">
-              {closedCount}
-            </span>
-          </Link>
-        </div>
+            <StatusTabs.Count>{closedCount}</StatusTabs.Count>
+          </StatusTabs.Tab>
+        </StatusTabs.Root>
 
         {bugs?.nodes.length === 0 && (
           <EmptyState>No {statusFilter} issues.</EmptyState>