docs: 重新梳理 AGENTS.md — 精简去重 + 修正过时模式 + 新增 DnD/Virtual 文档

- 根 AGENTS.md 242→135 行,去掉冗余 compile 命令和重复目录树
- server AGENTS.md 408→220 行,移除与根重复的 Code Style 章节
- 修正 ORPC 契约/路由路径(定义在模块内,非 server/api/)
- 新增 @dnd-kit/helpers move()、@tanstack/react-virtual、MutationCache 模式
This commit is contained in:
2026-03-31 17:42:56 +08:00
parent 001d171111
commit d67aaa723e
2 changed files with 213 additions and 420 deletions
+182 -294
View File
@@ -4,405 +4,293 @@ TanStack Start fullstack web app with ORPC (contract-first RPC) and shadcn/ui.
## Tech Stack
> **⚠️ This project uses Bun — NOT Node.js / npm. All commands use `bun`. Always use `bun run <script>` (not `bun <script>`) to avoid conflicts with Bun built-in subcommands. Never use `npm`, `npx`, or `node`.**
> **⚠️ This project uses Bun — NOT Node.js / npm. Always use `bun run <script>` (not `bun <script>`). Never use `npm`, `npx`, or `node`.**
- **Framework**: TanStack Start (React 19 SSR, file-based routing)
- **Runtime**: Bun — **NOT Node.js**
- **Package Manager**: Bun — **NOT npm / yarn / pnpm**
- **Language**: TypeScript (strict mode)
- **Styling**: Tailwind CSS v4 + shadcn/ui (base-nova style, OKLCH colors)
- **UI Components**: shadcn/ui (copy-paste components in `src/components/ui/`)
- **Styling**: Tailwind CSS v4 + shadcn/ui (base-nova style, `@base-ui/react`)
- **Database**: PostgreSQL + Drizzle ORM v1 beta (`drizzle-orm/postgres-js`, RQBv2)
- **State**: TanStack Query v5
- **State**: TanStack Query v5 (with MutationCache auto-invalidation)
- **RPC**: ORPC (contract-first, type-safe)
- **Auth**: Better Auth (email+password, self-hosted)
- **DnD**: @dnd-kit/react
- **DnD**: @dnd-kit/react + @dnd-kit/helpers (`move()` for sortable)
- **Virtualization**: @tanstack/react-virtual (`useVirtualizer`)
- **Build**: Vite + Nitro
## Commands
```bash
# Development
bun run dev # Vite dev server (localhost:3000)
bun run db:studio # Drizzle Studio GUI
# Build
bun run build # Production build → .output/
bun run compile # Compile to standalone binary (current platform, depends on build)
bun run compile:darwin # Compile for macOS (arm64 + x64)
bun run compile:linux # Compile for Linux (x64 + arm64)
bun run compile:windows # Compile for Windows (default: x64)
# Code Quality
bun run fix # Biome auto-fix
bun run typecheck # TypeScript check
# Database
bun run db:generate # Generate migrations from schema
bun run db:migrate # Run migrations
bun run db:push # Push schema directly (dev only)
# Testing (not yet configured)
bun test path/to/test.ts # Run single test
bun test -t "pattern" # Run tests matching pattern
```
## Architecture Overview
### Route Architecture (Display + Admin separation)
### Route Architecture
```
/ → Dashboard homepage (bookmark display, daily use)
/admin → Admin panel overview (module cards)
/ → Dashboard homepage (bookmark display, daily use)
/admin → Admin panel overview (module cards)
/admin/bookmarks → Bookmark management (CRUD, DnD, Dialog forms)
/login → Login page
/signup → Signup page
/login → Login page
/signup → Signup page
```
- **Display pages** (`/`): Clean, no management UI. What users see daily.
- **Admin pages** (`/admin/*`): Full CRUD, management, configuration. Sidebar navigation.
- All authenticated routes are under `_protected` layout (auth guard → redirect to `/login`).
- All authenticated routes under `_protected` layout (auth guard → redirect to `/login`).
### Module System
Modules are directory-based under `src/modules/`. Each module is independent and pluggable.
Modules are directory-based under `src/modules/`. Each module provides:
- `index.ts``ModuleMetadata` (id, name, icon, adminRoute)
- `schema.ts` — Drizzle tables
- `contract.ts` — ORPC contracts (input/output Zod schemas)
- `router.ts` — ORPC handlers (business logic)
- `components/` — React UI components
```typescript
// src/modules/registry.ts
export interface ModuleMetadata {
id: string
name: string
description: string
icon: string // lucide-react icon name
adminRoute: string // Admin management route (e.g., '/admin/bookmarks')
enabled: boolean
}
```
Each module provides: `schema.ts` (Drizzle tables), `contract.ts` (ORPC contracts), `router.ts` (ORPC handlers), `components/` (UI), and `index.ts` (metadata).
Contracts and routers are registered centrally:
- `src/server/api/contracts/index.ts` — imports all module contracts
- `src/server/api/routers/index.ts` — imports all module routers
## Directory Structure
```
src/
├── client/ # Client-side code
│ └── orpc.ts # ORPC client + TanStack Query utils
├── components/ # Shared React components
│ ├── AdminSidebar.tsx # Admin panel sidebar navigation
│ ├── Error.tsx # Error boundary fallback
│ ├── NotFound.tsx # 404 fallback
│ └── ui/ # shadcn/ui components (DO NOT manually edit)
│ ├── button.tsx
├── card.tsx
│ ├── dialog.tsx
├── dropdown-menu.tsx
│ ├── input.tsx
├── separator.tsx
├── sheet.tsx
│ ├── sidebar.tsx
├── skeleton.tsx
├── sonner.tsx
│ ├── tooltip.tsx
│ ├── avatar.tsx
│ └── badge.tsx
├── hooks/ # Custom React hooks
│ └── use-mobile.ts # Mobile detection (shadcn sidebar dep)
├── lib/ # Utility functions
│ └── utils.ts # cn() utility (clsx + tailwind-merge)
├── modules/ # Feature modules
│ ├── registry.ts # Module metadata registry
│ └── bookmarks/ # Bookmarks module
│ ├── index.ts # Module metadata
│ ├── schema.ts # Drizzle tables (category, bookmark)
│ ├── contract.ts # ORPC contracts
│ ├── router.ts # ORPC handlers
│ └── components/ # Module UI
│ ├── BookmarkCard.tsx # Display: clean card (icon + name)
│ ├── CategoryGrid.tsx # Display: category grid for homepage
│ ├── BookmarkFormDialog.tsx # Admin: create/edit bookmark dialog
│ ├── BookmarkManager.tsx # Admin: bookmark list with DnD
│ ├── CategoryFormDialog.tsx # Admin: create/edit category dialog
│ ├── CategoryManager.tsx # Admin: category list with DnD
│ ├── IconPickerDialog.tsx # Admin: icon picker in dialog
│ ├── GreetingHeader.tsx # Display: time-based greeting
│ └── SearchBar.tsx # Display: multi-engine search
├── routes/ # TanStack Router file routes
│ ├── __root.tsx # Root layout (HTML shell, Toaster, TooltipProvider)
│ ├── _protected.tsx # Auth guard layout (redirect to /login)
├── client/
│ └── orpc.ts # ORPC client (isomorphic: SSR direct call / CSR fetch)
├── components/
│ ├── AdminSidebar.tsx # Admin sidebar (reads module registry)
│ ├── Error.tsx # Error boundary fallback
│ ├── NotFound.tsx # 404 fallback
│ └── ui/ # shadcn/ui components (DO NOT manually edit)
├── hooks/
└── use-mobile.ts
├── lib/
└── utils.ts # cn() utility
├── modules/
├── registry.ts # ModuleMetadata interface + modules[]
└── bookmarks/ # Bookmarks module (schema, contract, router, components/)
├── routes/ # TanStack Router file routes
│ ├── __root.tsx # Root layout (HTML shell, Toaster)
├── _protected.tsx # Auth guard layout
│ ├── _protected/
│ │ ├── index.tsx # Dashboard homepage (bookmark display)
│ │ ├── admin.tsx # Admin layout (SidebarProvider + Outlet)
│ │ └── admin/
│ ├── index.tsx # Admin overview (module cards)
│ └── bookmarks.tsx # Bookmark management page
│ ├── login.tsx # Login page
│ ├── signup.tsx # Signup page
└── api/
├── $.ts # OpenAPI handler + Scalar docs
├── auth.$.ts # Better Auth handler
├── health.ts # Health check endpoint
── rpc.$.ts # ORPC RPC handler
├── server/ # Server-side code
├── api/ # ORPC layer
│ ├── contracts/ # Input/output schemas (Zod)
│ ├── middlewares/ # Middleware (dbMiddleware, authMiddleware)
├── routers/ # Handler implementations
│ │ ├── interceptors.ts # Shared error interceptors
│ │ ├── context.ts # Request context types
│ │ ├── server.ts # ORPC server instance
│ │ └── types.ts # Type exports
│ ├── auth/ # Better Auth
│ │ ├── schema.ts # Auth tables (user, session, account, verification)
│ │ ├── index.ts # Auth instance (betterAuth + drizzleAdapter)
│ │ ├── client.ts # Auth client (createAuthClient)
│ │ └── functions.ts # Server functions (getSession)
│ └── db/
│ ├── schema/ # Drizzle table re-exports
│ ├── fields.ts # Shared field builders (id, createdAt, updatedAt)
│ ├── relations.ts # Drizzle relations (defineRelations, RQBv2)
│ └── index.ts # Database singleton (module-level export)
├── env.ts # Environment variable validation
├── router.tsx # Router configuration
├── routeTree.gen.ts # Auto-generated (DO NOT EDIT)
└── styles.css # Tailwind + shadcn CSS variables
│ │ ├── index.tsx # Dashboard homepage
│ │ ├── admin.tsx # Admin layout (SidebarProvider)
│ │ └── admin/bookmarks.tsx
├── login.tsx, signup.tsx
└── api/ # $.ts (OpenAPI), auth.$.ts, health.ts, rpc.$.ts
├── server/
│ ├── api/
│ ├── contracts/index.ts # Central contract registry
├── routers/index.ts # Central router registry
├── middlewares/ # dbMiddleware, authMiddleware
├── interceptors.ts # Validation error transform
── context.ts # BaseContext type
│ │ ├── server.ts # `os = implement(contract).$context<BaseContext>()`
│ └── types.ts # RouterClient type export
│ ├── auth/ # Better Auth (schema, instance, client, getSession)
└── db/ # Drizzle (fields, relations, singleton instance)
├── env.ts # @t3-oss/env-core validation
├── router.tsx # TanStack Router + QueryClient + MutationCache
├── routeTree.gen.ts # Auto-generated (DO NOT EDIT)
└── styles.css # Tailwind + shadcn CSS variables
```
## ORPC Pattern
### 1. Define Contract (`src/server/api/contracts/feature.contract.ts`)
### 1. Define Contract (in module: `src/modules/feature/contract.ts`)
```typescript
import { oc } from '@orpc/contract'
import { createSelectSchema } from 'drizzle-orm/zod'
import { createInsertSchema, createSelectSchema, createUpdateSchema } from 'drizzle-orm/zod'
import { z } from 'zod'
import * as schema from '@/modules/feature/schema'
import { generatedFieldKeys } from '@/server/db/fields'
const selectSchema = createSelectSchema(schema.myTable)
const insertSchema = createInsertSchema(schema.myTable).omit(generatedFieldKeys).omit({ userId: true })
const updateSchema = createUpdateSchema(schema.myTable).omit(generatedFieldKeys).omit({ userId: true })
export const list = oc.input(z.void()).output(z.array(selectSchema))
export const create = oc.input(insertSchema).output(selectSchema)
export const myResource = {
list: oc.input(z.void()).output(z.array(selectSchema)),
create: oc.input(insertSchema).output(selectSchema),
update: oc.input(z.object({ id: z.uuid(), data: updateSchema })).output(selectSchema),
remove: oc.input(z.object({ id: z.uuid() })).output(z.void()),
}
```
### 2. Implement Router (`src/server/api/routers/feature.router.ts`)
### 2. Implement Router (in module: `src/modules/feature/router.ts`)
```typescript
import { ORPCError } from '@orpc/server'
import * as schema from '@/modules/feature/schema'
import { authMiddleware, dbMiddleware } from '@/server/api/middlewares'
import { os } from '@/server/api/server'
export const list = os.feature.list
.use(dbMiddleware)
.use(authMiddleware)
.handler(async ({ context }) => {
return await context.db.query.myTable.findMany({
orderBy: { createdAt: 'desc' },
})
})
export const myResource = {
list: os.feature.myResource.list
.use(dbMiddleware).use(authMiddleware)
.handler(async ({ context }) => {
return await context.db.query.myTable.findMany({
where: { userId: context.user.id },
orderBy: { createdAt: 'desc' },
})
}),
create: os.feature.myResource.create
.use(dbMiddleware).use(authMiddleware)
.handler(async ({ context, input }) => {
const [created] = await context.db.insert(schema.myTable)
.values({ ...input, userId: context.user.id }).returning()
if (!created) throw new ORPCError('INTERNAL_SERVER_ERROR', { message: 'Failed to create' })
return created
}),
}
```
### 3. Register in Index Files
### 3. Register (`src/server/api/contracts/index.ts` + `routers/index.ts`)
```typescript
// src/server/api/contracts/index.ts
// contracts/index.ts
import * as feature from '@/modules/feature/contract'
export const contract = { feature }
// src/server/api/routers/index.ts
// routers/index.ts
import * as feature from '@/modules/feature/router'
export const router = os.router({ feature })
```
### 4. Use in Components
```typescript
import { useSuspenseQuery, useMutation } from '@tanstack/react-query'
import { orpc } from '@/client/orpc'
const { data } = useSuspenseQuery(orpc.feature.list.queryOptions())
const mutation = useMutation(orpc.feature.create.mutationOptions())
const { data } = useSuspenseQuery(orpc.feature.myResource.list.queryOptions())
const mutation = useMutation(orpc.feature.myResource.create.mutationOptions())
```
## UI Component Patterns (shadcn/ui)
### MutationCache Auto-Invalidation
`router.tsx` configures a global `MutationCache` that auto-invalidates queries in the same module when any mutation succeeds. No need for manual `queryClient.invalidateQueries()` in most cases.
### Dialog Forms (for CRUD operations)
## UI Component Patterns
### base-ui `render` Prop (CRITICAL)
shadcn/ui uses `@base-ui/react`. The `render` prop replaces Radix's `asChild`:
```typescript
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
// ✅ CORRECT
<DialogTrigger render={<Button />} />
<SidebarMenuButton render={<Link to="/admin" />}>
// Use render prop on DialogTrigger to avoid nested <button>
// ❌ WRONG — asChild does NOT exist
<DialogTrigger asChild><Button /></DialogTrigger>
```
### Dialog Forms
```typescript
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger render={<Button></Button>} />
<DialogTrigger render={trigger} />
<DialogContent>
{/* Form content */}
<form onSubmit={handleSubmit}>
<DialogHeader><DialogTitle></DialogTitle></DialogHeader>
{/* fields */}
<DialogFooter><Button type="submit"></Button></DialogFooter>
</form>
</DialogContent>
</Dialog>
```
### DropdownMenu (for action menus)
### DnD Sortable (with @dnd-kit/helpers)
```typescript
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { move } from '@dnd-kit/helpers'
import { DragDropProvider } from '@dnd-kit/react'
import { useSortable } from '@dnd-kit/react/sortable'
// Use render prop on trigger to avoid nested <button>
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="ghost" size="icon-sm" />}>
<MoreHorizontal className="size-4" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem></DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
// In sortable item:
const { ref, handleRef, isDragging } = useSortable({ id, index, group })
// In container — use move() for reordering:
const handleDragEnd = (event) => {
if (event.canceled) return
const reordered = move(items, event) // @dnd-kit/helpers handles index mapping
setItems(reordered)
reorderMutation.mutate(reordered.map((item, i) => ({ id: item.id, orderId: i })))
}
<DragDropProvider onDragEnd={handleDragEnd}>
{items.map((item, index) => <SortableItem key={item.id} index={index} />)}
</DragDropProvider>
```
### Virtual Scrolling in Dialogs
Use `useState` callback ref (NOT `useRef`) for scroll elements inside Dialogs — `useRef` doesn't trigger re-render when Dialog mounts:
```typescript
import { useVirtualizer } from '@tanstack/react-virtual'
const [scrollElement, setScrollElement] = useState<HTMLDivElement | null>(null)
const virtualizer = useVirtualizer({
count: rowCount,
getScrollElement: () => scrollElement, // useState, NOT useRef
estimateSize: () => ROW_HEIGHT,
overscan: 5,
})
<div ref={setScrollElement} className="max-h-80 overflow-y-auto">
<div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
{virtualizer.getVirtualItems().map((row) => (
<div key={row.key} style={{ position: 'absolute', transform: `translateY(${row.start}px)` }}>
{/* row content */}
</div>
))}
</div>
</div>
```
### Toast Notifications
```typescript
import { toast } from 'sonner'
toast.success('操作成功')
toast.error('操作失败')
```
### Admin Sidebar
- Uses shadcn `SidebarProvider` + `Sidebar` + `SidebarInset` pattern
- Navigation items from module registry
- `SidebarMenuButton` uses `render` prop for `Link` integration (NOT `asChild`)
### base-ui `render` Prop (IMPORTANT)
shadcn/ui v4 uses `@base-ui/react` instead of Radix primitives. The `render` prop replaces `asChild`:
```typescript
// CORRECT: render prop delegates element rendering
<DialogTrigger render={<Button />} />
<SidebarMenuButton render={<Link to="/admin" />}>
// WRONG: asChild does NOT exist in base-ui components
<DialogTrigger asChild><Button /></DialogTrigger> // ❌ asChild is NOT supported
```
## Database (Drizzle ORM v1 beta)
- **Driver**: `drizzle-orm/postgres-js` (NOT `bun-sql`)
- **Validation**: `drizzle-orm/zod` (built-in, NOT separate `drizzle-zod` package)
- **Relations**: Defined via `defineRelations()` in `src/server/db/relations.ts`
- **Query**: RQBv2 — use `db.query.tableName.findMany()` with object-style `orderBy` and `where`
- **Table naming**: No `Table` suffix — use `user`, `category`, `bookmark` (NOT `userTable`)
- **DB instance**: Module-level singleton `export const db = drizzle(...)` (NOT factory/closure pattern)
- **Relations**: `defineRelations()` in `src/server/db/relations.ts` — RQBv2 object syntax
- **Table naming**: No `Table` suffix — `user`, `category`, `bookmark`
- **DB instance**: Module-level singleton `export const db = drizzle(...)` (NOT factory pattern)
- **Shared fields**: Use `...generatedFields` spread for id/createdAt/updatedAt
### Schema Definition
```typescript
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
import { sql } from 'drizzle-orm'
// Schema — use generatedFields spread
export const myTable = pgTable('my_table', {
id: uuid().primaryKey().default(sql`uuidv7()`),
name: text().notNull(),
createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow().$onUpdateFn(() => new Date()),
...generatedFields, // id (uuid v7), createdAt, updatedAt
name: text('name').notNull(),
userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
})
```
### Relations (RQBv2)
```typescript
import { defineRelations } from 'drizzle-orm'
import * as schema from './schema'
// Relations — RQBv2 defineRelations
export const relations = defineRelations(schema, (r) => ({
user: {
categories: r.many.category(),
},
category: {
user: r.one.user({ from: r.category.userId, to: r.user.id }),
bookmarks: r.many.bookmark(),
myTable: {
user: r.one.user({ from: r.myTable.userId, to: r.user.id }),
},
}))
```
### DB Instance
```typescript
import { drizzle } from 'drizzle-orm/postgres-js'
import { relations } from '@/server/db/relations'
export const db = drizzle({ connection: env.DATABASE_URL, relations })
export type DB = typeof db
```
## Auth (Better Auth)
- **Instance**: `src/server/auth/index.ts``betterAuth()` with `drizzleAdapter(db, { provider: 'pg', schema: authSchema })`
- **Instance**: `src/server/auth/index.ts``betterAuth()` with `drizzleAdapter(db, { provider: 'pg', schema })`
- **Client**: `src/server/auth/client.ts``createAuthClient()` for React
- **Server function**: `src/server/auth/functions.ts``getSession()` via `createServerFn`
- **Auth tables**: Use `text` IDs (Better Auth manages its own IDs), NOT project's UUID v7
- **Schema key naming**: Export names must match Better Auth model names exactly (`user`, `session`, `account`, `verification`)
### Auth Middleware (ORPC)
```typescript
const sessionData = await auth.api.getSession({ headers: context.headers })
// Injects context.user and context.session
```
## Code Style
### Formatting (Biome)
- **Indent**: 2 spaces
- **Quotes**: Single `'`
- **Semicolons**: Omit (ASI)
- **Arrow parens**: Always `(x) => x`
### Imports
Biome auto-organizes:
1. External packages
2. Internal `@/*` aliases
3. Type imports (`import type { ... }`)
### TypeScript
- `strict: true`
- `noUncheckedIndexedAccess: true` — array access returns `T | undefined`
- Use `@/*` path aliases (maps to `src/*`)
### Naming
| Type | Convention | Example |
|------|------------|---------|
| Files (utils) | kebab-case | `auth-utils.ts` |
| Files (components) | PascalCase | `UserProfile.tsx` |
| Components | PascalCase arrow | `const Button = () => {}` |
| Functions | camelCase | `getUserById` |
| Types | PascalCase | `UserProfile` |
| Drizzle tables | camelCase, no suffix | `user`, `category` (NOT `userTable`) |
### React
- Use arrow functions for components (Biome enforced)
- Use `useSuspenseQuery` for guaranteed data
- Let React Compiler handle memoization (no manual `useMemo`/`useCallback`)
## Development Principles
> **These principles apply to ALL code changes. Agents MUST follow them on every task.**
1. **No backward compatibility** — Always use the latest API and patterns.
2. **Always sync documentation** — When code changes, immediately update all related documentation.
3. **Forward-only migration** — When upgrading dependencies, fully adopt the new API.
- **Route guard**: `beforeLoad` in `_protected.tsx` calls `getSession()` → redirect to `/login`
- **ORPC middleware**: `authMiddleware` calls `auth.api.getSession({ headers })` → injects `context.user`
## Critical Rules
**DO:**
- Run `bun run fix` before committing
- Use `@/*` path aliases
- Include `createdAt`/`updatedAt` on all tables
- Use `ORPCError` with proper codes
- Use `drizzle-orm/zod` (NOT `drizzle-zod`) for schema validation
- Use RQBv2 object syntax for `orderBy` and `where`
- Use `@/*` path aliases (not relative imports)
- Use `render` prop (NOT `asChild`) for base-ui component delegation
- Use Dialog modals for forms (NOT inline forms)
- Use `toast` from `sonner` for CRUD notifications
- Update `AGENTS.md` and other docs whenever code patterns change
- Use `ORPCError` with proper codes
- Use `drizzle-orm/zod` (NOT `drizzle-zod`)
- Use RQBv2 object syntax for `orderBy` and `where`
- Use `move()` from `@dnd-kit/helpers` for DnD reordering
- Use `useState` callback ref for virtualizer scroll elements inside Dialogs
**DON'T:**
- Use `npm`, `npx`, `node`, `yarn`, `pnpm` — always use `bun` / `bunx`
- Edit `src/routeTree.gen.ts` (auto-generated)
- Manually edit `src/components/ui/*.tsx` (use `bunx shadcn@latest add`)
- Use `as any`, `@ts-ignore`, `@ts-expect-error`
- Use `asChild` prop (use `render` prop instead — base-ui, NOT Radix)
- Commit `.env` files
- Use empty catch blocks
- Import from `drizzle-zod` (use `drizzle-orm/zod` instead)
- Use RQBv1 callback-style `orderBy` / old `relations()` API
- Use `drizzle-orm/bun-sql` driver (use `drizzle-orm/postgres-js`)
- Pass `schema` to `drizzle()` constructor (only `relations` is needed in RQBv2)
- Use `getDB()` factory pattern (use module-level `db` export instead)
- Edit `src/routeTree.gen.ts` (auto-generated)
- Use `asChild` prop (base-ui uses `render`, NOT Radix)
- Import from `drizzle-zod` (use `drizzle-orm/zod`)
- Use `drizzle-orm/bun-sql` driver
- Pass `schema` to `drizzle()` constructor (only `relations` needed in RQBv2)
- Add `Table` suffix to Drizzle table exports
- Leave docs out of sync with code changes
- Use `useRef` for scroll elements inside Dialog/conditional rendering