refactor(web): reorganize component hierarchy and fix shadcn config

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

Restructure components into three clear layers:

  ui/       — generic shadcn primitives (button, input, avatar, etc.)
              Managed by shadcn CLI. No domain knowledge.

  shared/   — app-level reusable components with domain awareness but
              no data fetching. Typed against GraphQL fragments.
              (IssueRow, LabelBadge, StatusBadge, CommentCard,
              Pagination, QueryInput, StatusTabs, WritePreview,
              EmptyState, SectionHeading)

  bugs/     — feature components with GraphQL mutations and auth
              (CommentBox, Timeline, TitleEditor, LabelEditor,
              IssueFilters)

Also update components.json for Tailwind v4 (drop tailwind.config
reference, set config to empty string per shadcn docs).

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

Change summary

webui2/components.json                                                   |  2 
webui2/src/components/bugs/CommentBox.tsx                                |  4 
webui2/src/components/bugs/IssueFilters.tsx                              |  2 
webui2/src/components/bugs/LabelEditor.tsx                               |  4 
webui2/src/components/bugs/Timeline.tsx                                  |  4 
webui2/src/components/shared/IdentitySummary.graphql                     |  0 
webui2/src/components/shared/IssueRow.graphql                            |  0 
webui2/src/components/shared/IssueRow.stories.tsx                        |  0 
webui2/src/components/shared/IssueRow.test.tsx                           |  0 
webui2/src/components/shared/IssueRow.tsx                                |  0 
webui2/src/components/shared/LabelBadge.graphql                          |  0 
webui2/src/components/shared/LabelBadge.stories.tsx                      |  0 
webui2/src/components/shared/LabelBadge.test.tsx                         |  0 
webui2/src/components/shared/LabelBadge.tsx                              |  0 
webui2/src/components/shared/StatusBadge.stories.tsx                     |  0 
webui2/src/components/shared/StatusBadge.test.tsx                        |  0 
webui2/src/components/shared/StatusBadge.tsx                             |  0 
webui2/src/components/shared/__snapshots__/IssueRow.test.tsx.snap        |  0 
webui2/src/components/shared/__snapshots__/LabelBadge.test.tsx.snap      |  0 
webui2/src/components/shared/__snapshots__/StatusBadge.test.tsx.snap     |  0 
webui2/src/components/shared/__snapshots__/comment-card.test.tsx.snap    |  0 
webui2/src/components/shared/__snapshots__/empty-state.test.tsx.snap     |  0 
webui2/src/components/shared/__snapshots__/pagination.test.tsx.snap      |  0 
webui2/src/components/shared/__snapshots__/query-input.test.tsx.snap     |  0 
webui2/src/components/shared/__snapshots__/section-heading.test.tsx.snap |  0 
webui2/src/components/shared/__snapshots__/status-tabs.test.tsx.snap     |  0 
webui2/src/components/shared/__snapshots__/write-preview.test.tsx.snap   |  0 
webui2/src/components/shared/comment-card.stories.tsx                    |  0 
webui2/src/components/shared/comment-card.test.tsx                       |  0 
webui2/src/components/shared/comment-card.tsx                            |  2 
webui2/src/components/shared/empty-state.stories.tsx                     |  0 
webui2/src/components/shared/empty-state.test.tsx                        |  0 
webui2/src/components/shared/empty-state.tsx                             |  0 
webui2/src/components/shared/pagination.stories.tsx                      |  0 
webui2/src/components/shared/pagination.test.tsx                         |  0 
webui2/src/components/shared/pagination.tsx                              |  2 
webui2/src/components/shared/query-input.stories.tsx                     |  0 
webui2/src/components/shared/query-input.test.tsx                        |  0 
webui2/src/components/shared/query-input.tsx                             |  0 
webui2/src/components/shared/section-heading.stories.tsx                 |  0 
webui2/src/components/shared/section-heading.test.tsx                    |  0 
webui2/src/components/shared/section-heading.tsx                         |  0 
webui2/src/components/shared/status-tabs.stories.tsx                     |  0 
webui2/src/components/shared/status-tabs.test.tsx                        |  0 
webui2/src/components/shared/status-tabs.tsx                             |  0 
webui2/src/components/shared/write-preview.stories.tsx                   |  0 
webui2/src/components/shared/write-preview.test.tsx                      |  0 
webui2/src/components/shared/write-preview.tsx                           |  0 
webui2/src/routes/$repo/_issues/issues/$id.tsx                           |  6 
webui2/src/routes/$repo/_issues/issues/index.tsx                         | 12 
webui2/src/routes/$repo/_issues/issues/new.tsx                           |  2 
webui2/src/routes/$repo/_issues/user/$id.tsx                             |  8 
52 files changed, 24 insertions(+), 24 deletions(-)

Detailed changes

webui2/components.json 🔗

@@ -4,7 +4,7 @@
   "rsc": false,
   "tsx": true,
   "tailwind": {
-    "config": "tailwind.config.ts",
+    "config": "",
     "css": "src/index.css",
     "baseColor": "zinc",
     "cssVariables": true,

webui2/src/components/bugs/CommentBox.tsx 🔗

@@ -11,9 +11,9 @@ import {
 } from "@/__generated__/graphql";
 import { Markdown } from "@/components/content/Markdown";
 import { Button } from "@/components/ui/button";
-import * as CommentCard from "@/components/ui/comment-card";
+import * as CommentCard from "@/components/shared/comment-card";
 import { Textarea } from "@/components/ui/textarea";
-import * as WritePreview from "@/components/ui/write-preview";
+import * as WritePreview from "@/components/shared/write-preview";
 import { useAuth } from "@/lib/auth";
 
 interface CommentBoxProps {

webui2/src/components/bugs/IssueFilters.tsx 🔗

@@ -6,7 +6,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
 import { useAuth } from "@/lib/auth";
 import { cn } from "@/lib/utils";
 
-import { LabelBadge } from "./LabelBadge";
+import { LabelBadge } from "@/components/shared/LabelBadge";
 
 // Max authors shown in the non-searching state. We intentionally cap this to
 // avoid a giant list — the current-user + recently-seen pattern covers the

webui2/src/components/bugs/LabelEditor.tsx 🔗

@@ -2,10 +2,10 @@ import { Settings2 } from "lucide-react";
 
 import { useBugChangeLabelsMutation, BugDetailDocument } from "@/__generated__/graphql";
 import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
-import { SectionHeading } from "@/components/ui/section-heading";
+import { SectionHeading } from "@/components/shared/section-heading";
 import { useAuth } from "@/lib/auth";
 
-import { LabelBadge } from "./LabelBadge";
+import { LabelBadge } from "@/components/shared/LabelBadge";
 
 interface LabelEditorProps {
   bugPrefix: string;

webui2/src/components/bugs/Timeline.tsx 🔗

@@ -11,11 +11,11 @@ import {
 } from "@/__generated__/graphql";
 import { Markdown } from "@/components/content/Markdown";
 import { Button } from "@/components/ui/button";
-import * as CommentCard from "@/components/ui/comment-card";
+import * as CommentCard from "@/components/shared/comment-card";
 import { Textarea } from "@/components/ui/textarea";
 import { useAuth } from "@/lib/auth";
 
-import { LabelBadge } from "./LabelBadge";
+import { LabelBadge } from "@/components/shared/LabelBadge";
 
 type TimelineNode = NonNullable<
   NonNullable<NonNullable<BugDetailQuery["repository"]>["bug"]>["timeline"]["nodes"][number]

webui2/src/components/ui/comment-card.tsx → webui2/src/components/shared/comment-card.tsx 🔗

@@ -1,7 +1,7 @@
 import type { IdentitySummaryFragment } from "@/__generated__/graphql";
 import { cn } from "@/lib/utils";
 
-import { Avatar, AvatarFallback, AvatarImage } from "./avatar";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
 
 interface RootProps {
   children: React.ReactNode;

webui2/src/components/ui/pagination.tsx → webui2/src/components/shared/pagination.tsx 🔗

@@ -4,7 +4,7 @@ import * as React from "react";
 
 import { cn } from "@/lib/utils";
 
-import { buttonVariants } from "./button";
+import { buttonVariants } from "@/components/ui/button";
 
 interface RootProps {
   children: React.ReactNode;

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

@@ -5,13 +5,13 @@ import { formatDistanceToNow } from "date-fns";
 import { type BugDetailQuery, BugDetailDocument } from "@/__generated__/graphql";
 import { CommentBox } from "@/components/bugs/CommentBox";
 import { LabelEditor } from "@/components/bugs/LabelEditor";
-import { StatusBadge } from "@/components/bugs/StatusBadge";
+import { StatusBadge } from "@/components/shared/StatusBadge";
 import { Timeline } from "@/components/bugs/Timeline";
 import { TitleEditor } from "@/components/bugs/TitleEditor";
 import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
 import { BackLink } from "@/components/ui/back-link";
-import { EmptyState } from "@/components/ui/empty-state";
-import { SectionHeading } from "@/components/ui/section-heading";
+import { EmptyState } from "@/components/shared/empty-state";
+import { SectionHeading } from "@/components/shared/section-heading";
 import { Separator } from "@/components/ui/separator";
 import { Skeleton } from "@/components/ui/skeleton";
 

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

@@ -8,13 +8,13 @@ import * as v from "valibot";
 import { type BugListQuery, BugListDocument } from "@/__generated__/graphql";
 import { IssueFilters } from "@/components/bugs/IssueFilters";
 import type { SortValue } from "@/components/bugs/IssueFilters";
-import * as IssueRow from "@/components/bugs/IssueRow";
-import { LabelBadgeLink } from "@/components/bugs/LabelBadge";
+import * as IssueRow from "@/components/shared/IssueRow";
+import { LabelBadgeLink } from "@/components/shared/LabelBadge";
 import { Button } from "@/components/ui/button";
-import { EmptyState } from "@/components/ui/empty-state";
-import * as Pagination from "@/components/ui/pagination";
-import * as QueryInput from "@/components/ui/query-input";
-import type { CompletionProvider } from "@/components/ui/query-input";
+import { EmptyState } from "@/components/shared/empty-state";
+import * as Pagination from "@/components/shared/pagination";
+import * as QueryInput from "@/components/shared/query-input";
+import type { CompletionProvider } from "@/components/shared/query-input";
 import { Skeleton } from "@/components/ui/skeleton";
 import { cn } from "@/lib/utils";
 

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

@@ -8,7 +8,7 @@ import { Button } from "@/components/ui/button";
 import { ButtonLink } from "@/components/ui/button-link";
 import { Input } from "@/components/ui/input";
 import { Textarea } from "@/components/ui/textarea";
-import * as WritePreview from "@/components/ui/write-preview";
+import * as WritePreview from "@/components/shared/write-preview";
 
 export const Route = createFileRoute("/$repo/_issues/issues/new")({
   component: RouteComponent,

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

@@ -14,12 +14,12 @@ import {
 import * as v from "valibot";
 
 import { type UserProfileQuery, UserProfileDocument } from "@/__generated__/graphql";
-import * as IssueRow from "@/components/bugs/IssueRow";
-import { LabelBadge } from "@/components/bugs/LabelBadge";
+import * as IssueRow from "@/components/shared/IssueRow";
+import { LabelBadge } from "@/components/shared/LabelBadge";
 import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
 import { BackLink } from "@/components/ui/back-link";
-import { EmptyState } from "@/components/ui/empty-state";
-import * as Pagination from "@/components/ui/pagination";
+import { EmptyState } from "@/components/shared/empty-state";
+import * as Pagination from "@/components/shared/pagination";
 import { Skeleton } from "@/components/ui/skeleton";
 import { cn } from "@/lib/utils";