# AGENTS.md — PWA Stack Reference This file is the authoritative guide for AI agents (Claude Code, Cursor, Copilot, etc.) working on any app in this repository. **Always follow this stack. Never introduce dependencies outside of it without explicit approval.** --- ## Stack Overview | Layer | Tool | Version | | ----------------- | -------------------------------------- | ------- | | Runtime | Bun | latest | | Build | Vite | latest | | UI Framework | React | 19+ | | Component Library | shadcn/ui + Tailwind CSS v4 | latest | | Routing | TanStack Router | latest | | State Management | Zustand | latest | | Local Database | PGlite (PostgreSQL in WASM) | latest | | Forms | TanStack Form + Zod | latest | | Animations | CSS View Transitions API | native | | PWA | vite-plugin-pwa + Workbox | latest | | Testing (unit) | Vitest + Testing Library | latest | | Testing (e2e) | Playwright | latest | | Code Quality | Biome + lint-staged + simple-git-hooks | latest | --- ## Project Structure ``` src/ ├── features/ # One folder per domain feature │ ├── auth/ │ │ ├── components/ # UI components for this feature │ │ ├── hooks/ # Custom hooks │ │ ├── store.ts # Zustand store (if needed) │ │ ├── schema.ts # Zod schemas for this feature │ │ └── index.ts # Public API of the feature │ └── [feature-name]/ │ └── ... ├── shared/ │ ├── components/ # Generic reusable UI (Button, Modal, etc.) │ ├── hooks/ # Generic hooks (useMediaQuery, etc.) │ ├── db/ │ │ ├── client.ts # PGlite instance (singleton) │ │ ├── migrations/ # SQL migration files (001_init.sql, etc.) │ │ └── schema.ts # Zod schemas mirroring DB tables │ └── lib/ # Pure utility functions ├── routes/ # TanStack Router route files │ ├── __root.tsx # Root layout │ ├── index.tsx # / route │ └── [feature]/ │ └── index.tsx ├── app.tsx # App entry, router provider └── main.tsx # Bun/Vite entry point public/ ├── manifest.webmanifest # PWA manifest └── icons/ # PWA icons (all sizes) ``` --- ## Rules by Layer ### React & TypeScript - Use **React 19** features: use(), Suspense, transitions. - All files are `.tsx` or `.ts`. No `.js` or `.jsx`. - Prefer named exports over default exports (except route files — TanStack Router requires default exports). - No `any`. Use `unknown` and narrow with Zod when type is uncertain. - Use `satisfies` operator for type-safe object literals. ### Styling (Tailwind + shadcn/ui) - **Never write custom CSS files.** Use Tailwind utility classes exclusively. - Use `cn()` (from `src/shared/lib/utils.ts`) for conditional class merging. - shadcn/ui components live in `src/shared/components/ui/` — **never modify them directly**. Wrap them instead. - Dark mode via Tailwind's `dark:` variant. The `class` strategy is used (not `media`). - Responsive design: mobile-first. `sm:`, `md:`, `lg:` for larger screens. - For "native app" feel on mobile: use `touch-action: manipulation` on interactive elements, remove tap highlight with `[-webkit-tap-highlight-color:transparent]`. ### Routing (TanStack Router) - Use **file-based routing** via `@tanstack/router-plugin/vite`. - Route files live in `src/routes/`. Each file exports a `Route` created with `createFileRoute`. - **Always use `Link` and `useNavigate`** from TanStack Router. Never `` for internal navigation. - Search params are typed via Zod — define `validateSearch` on every route that uses search params. - Loaders (`loader` option on route) are the correct place for data fetching that should block navigation. ```ts // Example route with typed search params export const Route = createFileRoute("/runs/")({ validateSearch: z.object({ page: z.number().default(1), filter: z.enum(["all", "recent"]).default("all"), }), component: RunsPage, }); ``` ### Database (PGlite) - The PGlite instance is a **singleton** exported from `src/shared/db/client.ts`. - **Never import PGlite directly in components.** Always go through a hook or a feature's data layer. - Use **numbered SQL migration files**: `src/shared/db/migrations/001_init.sql`, `002_add_segments.sql`, etc. Run them in order on app start. - Zod schemas in `src/shared/db/schema.ts` mirror every DB table. Use them to validate data coming out of PGlite. - For reactive UI updates from DB changes, use a thin wrapper with Zustand's `subscribeWithSelector` or re-query on relevant user actions. ```ts // src/shared/db/client.ts import { PGlite } from "@electric-sql/pglite"; let _db: PGlite | null = null; export async function getDb(): Promise { if (!_db) { _db = new PGlite("idb://myapp"); await runMigrations(_db); } return _db; } ``` ### State Management (Zustand) - Zustand is for **ephemeral UI state only**: modals, active tabs, current user session, UI preferences. - **Persistent data lives in PGlite, not Zustand.** Do not use `zustand/middleware` persist for app data. - One store file per feature: `src/features/auth/store.ts`. - Use `subscribeWithSelector` middleware when components need to subscribe to slices. - Keep stores flat. Avoid deeply nested state. ```ts // Example store import { create } from "zustand"; interface UIStore { activeTab: string; setActiveTab: (tab: string) => void; } export const useUIStore = create((set) => ({ activeTab: "home", setActiveTab: (tab) => set({ activeTab: tab }), })); ``` ### Forms (TanStack Form + Zod) - All forms use `useForm` from `@tanstack/react-form`. - Validation is always done with a Zod schema via the `validators` option. - **Reuse Zod schemas** from `src/shared/db/schema.ts` or `src/features/[x]/schema.ts` — do not duplicate validation logic. - Error messages are shown inline below the field, never in a toast. ```ts const form = useForm({ defaultValues: { name: "" }, validators: { onChange: myZodSchema }, onSubmit: async ({ value }) => { /* ... */ }, }); ``` ### Animations (CSS View Transitions API) - Use the native **View Transitions API** for page transitions. No animation libraries. - Wrap navigation calls with `document.startViewTransition()`. - TanStack Router's `RouterDevtools` and upcoming native support handle this — check the TanStack Router docs for the current recommended integration. - Use `view-transition-name` CSS property on elements that should animate between routes. - Provide a `@media (prefers-reduced-motion: reduce)` fallback that disables transitions. ```css /* Disable transitions for users who prefer it */ @media (prefers-reduced-motion: reduce) { ::view-transition-old(*), ::view-transition-new(*) { animation: none; } } ``` ### PWA (vite-plugin-pwa) - Config lives in `vite.config.ts` under the `VitePWA()` plugin. - Strategy: `generateSW` (not `injectManifest`) unless custom SW logic is needed. - **Always precache** the PGlite WASM files — without this the app won't work offline. - `manifest.webmanifest` must include: `name`, `short_name`, `start_url: "/"`, `display: "standalone"`, `theme_color`, icons at 192×192 and 512×512. - Register the SW with `registerType: 'autoUpdate'` and show a "New version available" toast. ### Testing **Unit/Component Tests (Vitest)** - Test files co-located with source: `src/features/auth/auth.test.ts`. - Use Testing Library for component tests. Query by role, label, or text — never by class or id. - Mock PGlite with an in-memory instance (no `idb://` prefix) in tests. - Run with: `bun test` **E2E Tests (Playwright)** - Test files in `e2e/`. - Focus on critical user flows: onboarding, data entry, offline behavior, install prompt. - Use `page.evaluate()` to inspect IndexedDB/PGlite state when needed. - Run with: `bun e2e` ### Code Quality (Biome) - Biome handles both **formatting and linting** — no Prettier, no ESLint. - Config in `biome.json` at project root. - `lint-staged` + `simple-git-hooks` run Biome on staged files before every commit. - CI also runs `biome check --apply` — commits that fail Biome are rejected. - **Never disable Biome rules with inline comments** without a code comment explaining why. --- ## Commands ```bash bun install # Install dependencies bun dev # Start dev server bun build # Production build bun preview # Preview production build locally bun test # Run Vitest unit tests bun e2e # Run Playwright E2E tests bun lint # Run Biome linter bun format # Run Biome formatter ``` --- ## Do Not - ❌ Do not use `npm` or `yarn` — always use `bun` - ❌ Do not add ESLint, Prettier, or Husky — Biome + simple-git-hooks covers this - ❌ Do not use `react-query` or `swr` — data comes from PGlite, not a remote API - ❌ Do not store persistent app data in Zustand — use PGlite - ❌ Do not use Framer Motion / React Spring — use CSS View Transitions - ❌ Do not use `` for internal links — use TanStack Router's `` - ❌ Do not write raw CSS files — use Tailwind utilities - ❌ Do not modify files in `src/shared/components/ui/` — wrap shadcn components instead - ❌ Do not introduce a new dependency without checking if the existing stack already covers the need --- ## Adding a New Feature Checklist 1. Create `src/features/[name]/` folder 2. Add Zod schema in `src/features/[name]/schema.ts` 3. Add DB migration in `src/shared/db/migrations/` if needed 4. Add route file in `src/routes/[name]/index.tsx` 5. Add Zustand store in `src/features/[name]/store.ts` if UI state is needed 6. Write unit tests co-located with the feature 7. Write at least one Playwright E2E test for the happy path 8. Run `bun lint` and `bun test` before committing