diff --git a/webui2/README.md b/webui2/README.md index e31e8aee416588090de11364bd0b9cfa86e0e232..12ebe1b4d48430b5f88020e284a3bb575b9f337e 100644 --- a/webui2/README.md +++ b/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 + +``` + +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` 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