docs(web): update README for current architecture

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

Reflect the component layer system (ui/shared/bugs/code), colocated
GraphQL fragments, Storybook + Vitest testing setup, a11y testing,
and all current tooling.

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

Change summary

webui2/README.md | 121 +++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 111 insertions(+), 10 deletions(-)

Detailed changes

webui2/README.md 🔗

@@ -53,19 +53,59 @@ src/
 │   │   └── commit/     # commit/$hash
 │   └── auth/           # select-identity
 ├── components/
-│   ├── bugs/           # Issue components (BugRow, Timeline, ...)
-│   ├── code/           # Code browser (FileTree, FileViewer, ...)
-│   ├── content/        # Markdown renderer with repo-aware links
-│   ├── layout/         # Header + Shell
-│   └── ui/             # shadcn/ui + ButtonLink, BackLink
-├── graphql/            # .graphql source files — edit these, then run codegen
+│   ├── ui/             # shadcn/ui primitives (button, input, avatar, ...)
+│   ├── shared/         # Reusable app components with composition APIs
+│   ├── bugs/           # Bug-feature components with data fetching
+│   ├── code/           # Code browser components
+│   ├── content/        # Markdown renderer
+│   └── layout/         # Header + Shell
+├── graphql/            # .graphql query files — edit these, then run codegen
 ├── __generated__/      # Generated typed hooks — do not edit
 ├── assets/             # Logo SVG
-├── lib/                # apollo.ts, auth.tsx, theme.tsx, utils.ts
+├── lib/                # apollo.ts, auth.tsx, theme.tsx, utils.ts, query-utils.ts
 ├── routeTree.gen.ts    # Auto-generated route tree — do not edit
 └── App.tsx             # Router instance + context
 ```
 
+### Component layers
+
+Components are organized in three layers:
+
+- **`ui/`** — Generic primitives managed by shadcn CLI (`npx shadcn add`). No domain knowledge. Examples: button, input, avatar, badge, popover, separator, skeleton, textarea.
+
+- **`shared/`** — App-level reusable components. These know about the domain (bug status, labels, identities) but contain no data fetching. They use **composition APIs** (compound components) and are typed against **colocated GraphQL fragments**. Examples: issue-row, label-badge, status-badge, status-tabs, comment-card, pagination, query-input, write-preview, empty-state, section-heading, issue-filters.
+
+- **`bugs/`**, **`code/`** — Feature components with GraphQL mutations, `useAuth`, and other side effects. These compose `shared/` and `ui/` components.
+
+### GraphQL fragments
+
+Fragments are colocated with the components that consume them in `.graphql` files:
+
+```
+src/components/shared/
+├── identity-summary.graphql   → IdentitySummaryFragment
+├── label-badge.graphql        → LabelFieldsFragment
+├── issue-row.graphql          → BugSummaryFragment (composes above)
+src/components/bugs/
+└── timeline.graphql           → timeline event fragments
+```
+
+Components are typed against their fragments:
+
+```tsx
+import type { LabelFieldsFragment } from "@/__generated__/graphql";
+import { LabelBadge } from "@/components/shared/label-badge";
+
+// Spread fragment data directly onto the component
+<LabelBadge {...label} />
+```
+
+After changing any `.graphql` file, regenerate typed hooks:
+
+```bash
+pnpm codegen
+```
+
 ## Routing
 
 Routes use [TanStack Router](https://tanstack.com/router) with file-based routing and automatic code splitting. The `@tanstack/router-plugin` Vite plugin generates `routeTree.gen.ts` from the `src/routes/` directory.
@@ -80,6 +120,9 @@ The router context provides:
 Custom link components:
 - `ButtonLink` — `createLink()`-wrapped anchor with button styling and preload-on-intent
 - `BackLink` — uses `router.history.back()` when possible, falls back to a typed Link
+- `LabelBadgeLink` — `createLink()`-wrapped label badge for filter navigation
+- `StatusTabs.Tab` — `createLink()`-wrapped status toggle tab
+- `Pagination.Previous/Next` — `createLink()`-wrapped pagination buttons
 
 ## Data loading
 
@@ -100,18 +143,73 @@ The router waits for `toPromise()` before transitioning, then the component read
 
 Search params that affect data loading use `loaderDeps` so the loader re-runs when they change (e.g. issue filters, pagination cursors).
 
-After changing any `.graphql` file, regenerate typed hooks:
+## Storybook
+
+[Storybook 10](https://storybook.js.org/) is set up for component development and testing:
 
 ```bash
-pnpm codegen
+pnpm storybook        # Dev server on http://localhost:6006
+pnpm build-storybook  # Production build
+```
+
+Every presentational component has stories. Stories use the CSF3 format with `satisfies Meta<typeof Component>` for full type inference. Mock data is typed against GraphQL fragment types.
+
+## Testing
+
+Tests run via [Vitest 4](https://vitest.dev/) with two projects:
+
+| Project | Environment | What it does |
+| --- | --- | --- |
+| **storybook** | Chromium (Playwright) | Smoke tests every story + a11y checks (axe-core) + play function interaction tests |
+| **snapshot** | happy-dom | DOM snapshot tests via portable stories API |
+
+```bash
+pnpm test                          # Run all tests
+pnpm test -- --project=storybook   # Storybook tests only
+pnpm test -- --project=snapshot    # Snapshot tests only
+pnpm test -- -u                    # Update snapshots
+```
+
+### Adding tests
+
+Every story automatically becomes a smoke test and an a11y test. For snapshot tests, add a `*.test.tsx` file next to the story:
+
+```tsx
+import { composeStories } from "@storybook/react-vite";
+import { expect, test } from "vitest";
+import * as stories from "./my-component.stories";
+
+const composed = composeStories(stories);
+
+for (const [name, Story] of Object.entries(composed)) {
+  test(`MyComponent/${name} matches snapshot`, async () => {
+    await Story.run();
+    expect(document.body.firstChild).toMatchSnapshot();
+  });
+}
+```
+
+For interaction tests, add `play` functions to stories:
+
+```tsx
+export const MyInteraction: Story = {
+  play: async ({ canvasElement }) => {
+    const canvas = within(canvasElement);
+    await userEvent.click(canvas.getByRole("button"));
+    await expect(canvas.getByText("Result")).toBeVisible();
+  },
+};
 ```
 
 ## Tooling
 
 | Tool | Purpose |
 | --- | --- |
-| [oxlint](https://oxc.rs) | Linter with type-aware rules (replaces ESLint) |
+| [oxlint](https://oxc.rs) | Linter with type-aware rules + storybook/router plugins |
 | [oxfmt](https://oxc.rs) | Formatter with import + Tailwind class sorting |
+| [Storybook 10](https://storybook.js.org) | Component development + visual testing |
+| [Vitest 4](https://vitest.dev) | Test runner (browser + happy-dom) |
+| [@storybook/addon-a11y](https://storybook.js.org/addons/@storybook/addon-a11y) | Accessibility testing via axe-core |
 | [valibot](https://valibot.dev) | Runtime validation for search params and fetch responses |
 | [@tsconfig/bases](https://github.com/tsconfig/bases) | Shared tsconfig presets (vite-react + strictest) |
 
@@ -121,6 +219,9 @@ pnpm lint:fix    # oxlint with auto-fix
 pnpm fmt         # oxfmt format
 pnpm fmt:check   # oxfmt check only
 pnpm check       # lint + format check
+pnpm test        # vitest (all projects)
+pnpm storybook   # storybook dev server
+pnpm codegen     # regenerate GraphQL types
 ```
 
 ## Auth