@baldurpan/create-ai-workflow 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +165 -0
- package/dist/agents-block.js +70 -0
- package/dist/bin.js +14 -0
- package/dist/check/markdown.js +47 -0
- package/dist/check/parse.js +113 -0
- package/dist/check/rules.js +245 -0
- package/dist/cli.js +109 -0
- package/dist/commands/check.js +41 -0
- package/dist/commands/install.js +76 -0
- package/dist/commands/standards-add.js +145 -0
- package/dist/commands/update.js +214 -0
- package/dist/layout.js +115 -0
- package/dist/log.js +20 -0
- package/dist/manifest.js +53 -0
- package/dist/paths.js +41 -0
- package/package.json +37 -0
- package/templates/blocks/agents-block.md +30 -0
- package/templates/claude/agents/planner.agent.md +60 -0
- package/templates/claude/agents/reviewer.agent.md +77 -0
- package/templates/context/README.md +81 -0
- package/templates/context/plan-template.md +69 -0
- package/templates/context/plan-template.notes.md +55 -0
- package/templates/context/roles/coder.md +76 -0
- package/templates/context/workflow.md +143 -0
- package/templates/skills/feature-close/SKILL.md +102 -0
- package/templates/skills/feature-implement/SKILL.md +142 -0
- package/templates/skills/feature-plan/SKILL.md +138 -0
- package/templates/skills/feature-status/SKILL.md +86 -0
- package/templates/skills/onboard/SKILL.md +122 -0
- package/templates/skills/orchestrate/SKILL.md +92 -0
- package/templates/skills/roadmap/SKILL.md +97 -0
- package/templates/standards/.source +8 -0
- package/templates/standards/README.md +104 -0
- package/templates/standards/architecture/api-design.md +212 -0
- package/templates/standards/architecture/dependency-boundaries.md +104 -0
- package/templates/standards/architecture/feature-driven.md +102 -0
- package/templates/standards/architecture/folder-structure.md +108 -0
- package/templates/standards/architecture/monorepos.md +107 -0
- package/templates/standards/architecture/refactoring.md +97 -0
- package/templates/standards/architecture/shared-code.md +81 -0
- package/templates/standards/docs/PHP-SPEC.md +1195 -0
- package/templates/standards/docs/SPEC.md +540 -0
- package/templates/standards/examples/bad/data-fetching.tsx +52 -0
- package/templates/standards/examples/bad/derived-state.tsx +60 -0
- package/templates/standards/examples/bad/feature-component.tsx +80 -0
- package/templates/standards/examples/bad/prop-drilling.tsx +70 -0
- package/templates/standards/examples/good/data-fetching.tsx +53 -0
- package/templates/standards/examples/good/derived-state.tsx +52 -0
- package/templates/standards/examples/good/feature-component.tsx +141 -0
- package/templates/standards/examples/good/invoice.schema.ts +20 -0
- package/templates/standards/examples/good/prop-drilling.tsx +83 -0
- package/templates/standards/philosophy/ai-agent-behavior.md +93 -0
- package/templates/standards/philosophy/core-principles.md +49 -0
- package/templates/standards/philosophy/incremental-abstraction.md +56 -0
- package/templates/standards/philosophy/maintainability.md +66 -0
- package/templates/standards/philosophy/readability.md +87 -0
- package/templates/standards/php/README.md +62 -0
- package/templates/standards/php/anti-patterns.md +259 -0
- package/templates/standards/php/architecture.md +241 -0
- package/templates/standards/php/frameworks.md +197 -0
- package/templates/standards/php/rules.md +205 -0
- package/templates/standards/php/security.md +190 -0
- package/templates/standards/php/testing.md +139 -0
- package/templates/standards/php/tooling.md +245 -0
- package/templates/standards/react/accessibility.md +130 -0
- package/templates/standards/react/anti-patterns.md +187 -0
- package/templates/standards/react/component-design.md +139 -0
- package/templates/standards/react/error-boundaries.md +195 -0
- package/templates/standards/react/forms.md +147 -0
- package/templates/standards/react/hooks.md +88 -0
- package/templates/standards/react/memoization.md +87 -0
- package/templates/standards/react/state-management.md +123 -0
- package/templates/standards/react/testing.md +118 -0
- package/templates/standards/react/use-effect.md +110 -0
- package/templates/standards/security/api-security.md +136 -0
- package/templates/standards/security/auth.md +112 -0
- package/templates/standards/security/secrets.md +105 -0
- package/templates/standards/security/validation.md +89 -0
- package/templates/standards/templates/.editorconfig +16 -0
- package/templates/standards/templates/.nvmrc +1 -0
- package/templates/standards/templates/AGENTS.md +46 -0
- package/templates/standards/templates/CLAUDE.md +3 -0
- package/templates/standards/templates/_dot_gitignore +50 -0
- package/templates/standards/templates/biome.json +53 -0
- package/templates/standards/templates/eslint.config.js +61 -0
- package/templates/standards/templates/project-readme.md +76 -0
- package/templates/standards/templates/pull-request-template.md +46 -0
- package/templates/standards/templates/tsconfig.json +35 -0
- package/templates/standards/tooling/biome.md +92 -0
- package/templates/standards/tooling/ci.md +128 -0
- package/templates/standards/tooling/dates.md +194 -0
- package/templates/standards/tooling/dependencies.md +90 -0
- package/templates/standards/tooling/nx.md +113 -0
- package/templates/standards/tooling/observability.md +183 -0
- package/templates/standards/tooling/prisma.md +152 -0
- package/templates/standards/tooling/shadcn.md +115 -0
- package/templates/standards/tooling/tailwind.md +135 -0
- package/templates/standards/tooling/tanstack-query.md +214 -0
- package/templates/standards/tooling/tanstack-router.md +365 -0
- package/templates/standards/tooling/tanstack.md +31 -0
- package/templates/standards/tooling/vite.md +135 -0
- package/templates/standards/typescript/anti-patterns.md +107 -0
- package/templates/standards/typescript/error-handling.md +216 -0
- package/templates/standards/typescript/naming.md +78 -0
- package/templates/standards/typescript/rules.md +159 -0
- package/templates/standards/typescript/tsconfig/base.json +32 -0
- package/templates/standards/typescript/tsconfig/nextjs.json +12 -0
- package/templates/standards/typescript/tsconfig/react.json +9 -0
- package/templates/standards/typescript/validation.md +104 -0
- package/templates/stubs/executors.md +49 -0
- package/templates/stubs/findings.md +40 -0
- package/templates/stubs/history.md +13 -0
- package/templates/stubs/roadmap.md +29 -0
- package/templates/stubs/stack.md +35 -0
- package/templates/stubs/verify.md +48 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Prisma
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Prisma is the ORM of choice for TypeScript backend work. It provides type-safe database access with generated types derived from the schema.
|
|
6
|
+
|
|
7
|
+
## Key Principles
|
|
8
|
+
|
|
9
|
+
- Schema is the source of truth — all types are generated from it
|
|
10
|
+
- Use generated Prisma types directly — do not duplicate them as DTOs
|
|
11
|
+
- Use `prisma.$transaction` for multi-step operations that must succeed or fail together
|
|
12
|
+
- Validate external input before passing to Prisma
|
|
13
|
+
|
|
14
|
+
## Schema Conventions
|
|
15
|
+
|
|
16
|
+
```prisma
|
|
17
|
+
// prisma/schema.prisma
|
|
18
|
+
|
|
19
|
+
generator client {
|
|
20
|
+
provider = "prisma-client-js"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
datasource db {
|
|
24
|
+
provider = "postgresql"
|
|
25
|
+
url = env("DATABASE_URL")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
model User {
|
|
29
|
+
id String @id @default(cuid())
|
|
30
|
+
email String @unique
|
|
31
|
+
name String
|
|
32
|
+
role Role @default(MEMBER)
|
|
33
|
+
createdAt DateTime @default(now())
|
|
34
|
+
updatedAt DateTime @updatedAt
|
|
35
|
+
|
|
36
|
+
invoices Invoice[]
|
|
37
|
+
|
|
38
|
+
@@map("users") // snake_case table name
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
model Invoice {
|
|
42
|
+
id String @id @default(cuid())
|
|
43
|
+
title String
|
|
44
|
+
amount Decimal @db.Decimal(10, 2)
|
|
45
|
+
status InvoiceStatus @default(DRAFT)
|
|
46
|
+
dueDate DateTime
|
|
47
|
+
userId String
|
|
48
|
+
user User @relation(fields: [userId], references: [id])
|
|
49
|
+
createdAt DateTime @default(now())
|
|
50
|
+
updatedAt DateTime @updatedAt
|
|
51
|
+
|
|
52
|
+
@@map("invoices")
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
enum Role {
|
|
56
|
+
ADMIN
|
|
57
|
+
MEMBER
|
|
58
|
+
VIEWER
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
enum InvoiceStatus {
|
|
62
|
+
DRAFT
|
|
63
|
+
OPEN
|
|
64
|
+
PAID
|
|
65
|
+
VOID
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Conventions
|
|
70
|
+
|
|
71
|
+
- Use `cuid()` or `uuid()` for IDs — not auto-increment integers (avoids enumeration)
|
|
72
|
+
- Always include `createdAt` and `updatedAt` with `@default(now())` and `@updatedAt`
|
|
73
|
+
- Use `@@map` to keep table names snake_case while model names are PascalCase
|
|
74
|
+
- Use `Decimal` for monetary values — never `Float`
|
|
75
|
+
|
|
76
|
+
## Client Singleton
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
// lib/db.ts
|
|
80
|
+
import { PrismaClient } from "@prisma/client";
|
|
81
|
+
|
|
82
|
+
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
|
|
83
|
+
|
|
84
|
+
export const db =
|
|
85
|
+
globalForPrisma.prisma ??
|
|
86
|
+
new PrismaClient({
|
|
87
|
+
log: process.env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db;
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Using Generated Types
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import type { Invoice, User, InvoiceStatus } from "@prisma/client";
|
|
97
|
+
|
|
98
|
+
// Use Prisma's utility types for partial shapes
|
|
99
|
+
import type { Prisma } from "@prisma/client";
|
|
100
|
+
|
|
101
|
+
type InvoiceWithUser = Prisma.InvoiceGetPayload<{
|
|
102
|
+
include: { user: true };
|
|
103
|
+
}>;
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Migrations
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# Create a migration from schema changes
|
|
110
|
+
pnpm prisma migrate dev --name add_invoice_notes
|
|
111
|
+
|
|
112
|
+
# Apply migrations in production
|
|
113
|
+
pnpm prisma migrate deploy
|
|
114
|
+
|
|
115
|
+
# Reset development database
|
|
116
|
+
pnpm prisma migrate reset
|
|
117
|
+
|
|
118
|
+
# Regenerate the Prisma client after schema change
|
|
119
|
+
pnpm prisma generate
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Transactions
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
const [user, invoice] = await db.$transaction([
|
|
126
|
+
db.user.create({ data: userData }),
|
|
127
|
+
db.invoice.create({ data: { ...invoiceData, userId: user.id } }),
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
// For dependent operations
|
|
131
|
+
await db.$transaction(async (tx) => {
|
|
132
|
+
const user = await tx.user.create({ data: userData });
|
|
133
|
+
const invoice = await tx.invoice.create({
|
|
134
|
+
data: { ...invoiceData, userId: user.id },
|
|
135
|
+
});
|
|
136
|
+
return { user, invoice };
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## DO NOT
|
|
141
|
+
|
|
142
|
+
- Expose the Prisma client directly to API route handlers — wrap in a service layer
|
|
143
|
+
- Use raw `$queryRaw` without parameterized inputs (SQL injection risk)
|
|
144
|
+
- Store monetary values as `Float`
|
|
145
|
+
- Duplicate Prisma types as hand-written DTOs
|
|
146
|
+
|
|
147
|
+
## See Also
|
|
148
|
+
|
|
149
|
+
- [`../typescript/validation.md`](../typescript/validation.md) — Zod alongside generated Prisma types
|
|
150
|
+
- [`dates.md`](dates.md) — DateTime columns and timezone handling
|
|
151
|
+
- [`../typescript/error-handling.md`](../typescript/error-handling.md) — wrapping database errors
|
|
152
|
+
- [`../security/api-security.md`](../security/api-security.md) — SQL injection prevention
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# shadcn/ui
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
shadcn/ui provides accessible, unstyled UI primitives built on Radix UI. Components are copied into your codebase — you own and modify them directly.
|
|
6
|
+
|
|
7
|
+
## Key Properties
|
|
8
|
+
|
|
9
|
+
- Built on Radix UI — keyboard navigation and ARIA patterns are correct by default
|
|
10
|
+
- Styled with Tailwind CSS + CSS variables
|
|
11
|
+
- Not a dependency — components live in your `src/components/ui/` directory
|
|
12
|
+
- Fully customizable — modify any component without fighting against the library
|
|
13
|
+
|
|
14
|
+
## Setup
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm dlx shadcn@latest init
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
This sets up:
|
|
21
|
+
- `components.json` — config
|
|
22
|
+
- `src/components/ui/` — where components are added
|
|
23
|
+
- CSS variable theme in `globals.css`
|
|
24
|
+
|
|
25
|
+
## Adding Components
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pnpm dlx shadcn@latest add button
|
|
29
|
+
pnpm dlx shadcn@latest add dialog
|
|
30
|
+
pnpm dlx shadcn@latest add form
|
|
31
|
+
pnpm dlx shadcn@latest add table
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Components are added to `src/components/ui/`. They are yours to modify.
|
|
35
|
+
|
|
36
|
+
## Customization
|
|
37
|
+
|
|
38
|
+
### Via CSS Variables (Preferred)
|
|
39
|
+
|
|
40
|
+
Edit the CSS variables in `globals.css`:
|
|
41
|
+
|
|
42
|
+
```css
|
|
43
|
+
@layer base {
|
|
44
|
+
:root {
|
|
45
|
+
--primary: 221.2 83.2% 53.3%;
|
|
46
|
+
--primary-foreground: 210 40% 98%;
|
|
47
|
+
--radius: 0.5rem;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Via Component Modification
|
|
53
|
+
|
|
54
|
+
Fork the component directly in `src/components/ui/`. Changes survive upgrades because you own the file.
|
|
55
|
+
|
|
56
|
+
## Usage with Forms
|
|
57
|
+
|
|
58
|
+
shadcn/ui's `Form` component wraps React Hook Form:
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "~/components/ui/form";
|
|
62
|
+
import { useForm } from "react-hook-form";
|
|
63
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
64
|
+
|
|
65
|
+
const form = useForm({
|
|
66
|
+
resolver: zodResolver(schema),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
<Form {...form}>
|
|
70
|
+
<form onSubmit={form.handleSubmit(onSubmit)}>
|
|
71
|
+
<FormField
|
|
72
|
+
control={form.control}
|
|
73
|
+
name="title"
|
|
74
|
+
render={({ field }) => (
|
|
75
|
+
<FormItem>
|
|
76
|
+
<FormLabel>Title</FormLabel>
|
|
77
|
+
<FormControl>
|
|
78
|
+
<Input {...field} />
|
|
79
|
+
</FormControl>
|
|
80
|
+
<FormMessage />
|
|
81
|
+
</FormItem>
|
|
82
|
+
)}
|
|
83
|
+
/>
|
|
84
|
+
</form>
|
|
85
|
+
</Form>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## DO
|
|
89
|
+
|
|
90
|
+
- Prefer shadcn/ui components over custom implementations for common UI patterns
|
|
91
|
+
- Use CSS variable customization before forking a component
|
|
92
|
+
- Keep `src/components/ui/` for shadcn components; use `src/shared/components/` for your own
|
|
93
|
+
- Update components via `shadcn@latest add <component>` when you want upstream fixes (manual review required)
|
|
94
|
+
|
|
95
|
+
## DO NOT
|
|
96
|
+
|
|
97
|
+
- Install shadcn/ui as a package dependency — it is a code generator
|
|
98
|
+
- Modify shadcn components inside `node_modules`
|
|
99
|
+
- Build custom dialog, dropdown, or popover implementations when Radix primitives are available
|
|
100
|
+
- Assume accessibility — review each component in context (color contrast, label associations)
|
|
101
|
+
|
|
102
|
+
## When to Fork a Component
|
|
103
|
+
|
|
104
|
+
Fork (copy and modify) when:
|
|
105
|
+
- The component needs significant structural changes
|
|
106
|
+
- You need different default props or variants permanently
|
|
107
|
+
- The upstream component API doesn't fit your use case
|
|
108
|
+
|
|
109
|
+
Do not fork to change only styles — use CSS variables instead.
|
|
110
|
+
|
|
111
|
+
## See Also
|
|
112
|
+
|
|
113
|
+
- [`tailwind.md`](tailwind.md) — utility classes and theming
|
|
114
|
+
- [`../react/accessibility.md`](../react/accessibility.md) — why Radix primitives are preferred
|
|
115
|
+
- [`../react/forms.md`](../react/forms.md) — shadcn/ui form components
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Tailwind CSS
|
|
2
|
+
|
|
3
|
+
## Core Rules
|
|
4
|
+
|
|
5
|
+
- Utility-first: use Tailwind classes, not custom CSS, by default
|
|
6
|
+
- Use CSS variables for theming — not hardcoded color values
|
|
7
|
+
- Minimize arbitrary values (`[value]` syntax)
|
|
8
|
+
- Support dark mode via CSS variables and Tailwind's `dark:` variant
|
|
9
|
+
|
|
10
|
+
## Setup (v4)
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pnpm add tailwindcss @tailwindcss/vite
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
// vite.config.ts
|
|
18
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
19
|
+
|
|
20
|
+
export default defineConfig({
|
|
21
|
+
plugins: [react(), tailwindcss()],
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```css
|
|
26
|
+
/* src/styles/globals.css */
|
|
27
|
+
@import "tailwindcss";
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Theming with CSS Variables
|
|
31
|
+
|
|
32
|
+
Define design tokens as CSS variables. Wire them into Tailwind:
|
|
33
|
+
|
|
34
|
+
```css
|
|
35
|
+
/* src/styles/globals.css */
|
|
36
|
+
@import "tailwindcss";
|
|
37
|
+
|
|
38
|
+
@theme {
|
|
39
|
+
--color-primary: oklch(55% 0.2 250);
|
|
40
|
+
--color-primary-foreground: oklch(98% 0.01 250);
|
|
41
|
+
--color-background: oklch(100% 0 0);
|
|
42
|
+
--color-foreground: oklch(10% 0 0);
|
|
43
|
+
--color-muted: oklch(95% 0 0);
|
|
44
|
+
--color-border: oklch(88% 0 0);
|
|
45
|
+
--radius: 0.5rem;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.dark {
|
|
49
|
+
--color-background: oklch(10% 0 0);
|
|
50
|
+
--color-foreground: oklch(98% 0 0);
|
|
51
|
+
--color-muted: oklch(18% 0 0);
|
|
52
|
+
--color-border: oklch(25% 0 0);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Usage:
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
<div className="bg-background text-foreground border-border" />
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## DO
|
|
63
|
+
|
|
64
|
+
- Use `cn()` (clsx + tailwind-merge) for conditional class merging
|
|
65
|
+
- Define component variants with `cva` (class-variance-authority)
|
|
66
|
+
- Use `text-foreground`, `bg-background` etc. for themeable values
|
|
67
|
+
- Group Tailwind classes logically: layout → spacing → typography → color → state
|
|
68
|
+
|
|
69
|
+
## DO NOT
|
|
70
|
+
|
|
71
|
+
- Use arbitrary values for things that should be design tokens: `text-[#ff6b35]`
|
|
72
|
+
- Use `@apply` extensively — defeats the purpose of utility-first
|
|
73
|
+
- Write custom CSS for things Tailwind handles natively
|
|
74
|
+
|
|
75
|
+
## `cn()` Helper
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
// src/shared/utils/cn.ts
|
|
79
|
+
import { clsx, type ClassValue } from "clsx";
|
|
80
|
+
import { twMerge } from "tailwind-merge";
|
|
81
|
+
|
|
82
|
+
export function cn(...inputs: ClassValue[]) {
|
|
83
|
+
return twMerge(clsx(inputs));
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
<button className={cn("px-4 py-2 rounded", isActive && "bg-primary text-white")} />
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Component Variants with `cva`
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
95
|
+
import { cn } from "~/shared/utils/cn";
|
|
96
|
+
|
|
97
|
+
const buttonVariants = cva(
|
|
98
|
+
"inline-flex items-center justify-center rounded font-medium transition-colors",
|
|
99
|
+
{
|
|
100
|
+
variants: {
|
|
101
|
+
variant: {
|
|
102
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
103
|
+
outline: "border border-border bg-transparent hover:bg-muted",
|
|
104
|
+
ghost: "hover:bg-muted",
|
|
105
|
+
},
|
|
106
|
+
size: {
|
|
107
|
+
sm: "h-8 px-3 text-sm",
|
|
108
|
+
md: "h-10 px-4",
|
|
109
|
+
lg: "h-12 px-6 text-lg",
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
defaultVariants: { variant: "default", size: "md" },
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
interface ButtonProps
|
|
117
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
118
|
+
VariantProps<typeof buttonVariants> {}
|
|
119
|
+
|
|
120
|
+
export function Button({ className, variant, size, ...props }: ButtonProps) {
|
|
121
|
+
return <button className={cn(buttonVariants({ variant, size }), className)} {...props} />;
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## PRIORITY
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
Design tokens > Utility classes > Arbitrary values > Custom CSS
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## See Also
|
|
132
|
+
|
|
133
|
+
- [`shadcn.md`](shadcn.md) — Tailwind + Radix components
|
|
134
|
+
- [`../react/accessibility.md`](../react/accessibility.md) — focus styles, contrast
|
|
135
|
+
- [`../react/component-design.md`](../react/component-design.md) — `cn` and `cva` patterns
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# TanStack Query
|
|
2
|
+
|
|
3
|
+
The default solution for server state management. Replaces manual `useEffect` + `useState` data fetching entirely.
|
|
4
|
+
|
|
5
|
+
## Core Rules
|
|
6
|
+
|
|
7
|
+
- Use TanStack Query for any data that lives on the server — never duplicate it into local state
|
|
8
|
+
- Define `queryOptions` once and share them between loaders, components, and prefetches
|
|
9
|
+
- Use `useSuspenseQuery` when paired with route loaders or `<Suspense>` boundaries
|
|
10
|
+
- Use plain `useQuery` only when you need to render before the data resolves
|
|
11
|
+
- Invalidate queries on mutation success — don't manually update component state
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
17
|
+
|
|
18
|
+
const queryClient = new QueryClient({
|
|
19
|
+
defaultOptions: {
|
|
20
|
+
queries: {
|
|
21
|
+
staleTime: 60_000, // data stays fresh for 60s by default
|
|
22
|
+
retry: 1,
|
|
23
|
+
refetchOnWindowFocus: false,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
29
|
+
return (
|
|
30
|
+
<QueryClientProvider client={queryClient}>
|
|
31
|
+
{children}
|
|
32
|
+
</QueryClientProvider>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Query Options Pattern
|
|
38
|
+
|
|
39
|
+
Define query options as a function so the same definition works in loaders, components, and prefetches:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { queryOptions } from "@tanstack/react-query";
|
|
43
|
+
|
|
44
|
+
export function invoiceQueryOptions(id: string) {
|
|
45
|
+
return queryOptions({
|
|
46
|
+
queryKey: ["invoices", id],
|
|
47
|
+
queryFn: () => fetchInvoice(id),
|
|
48
|
+
staleTime: 30_000,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Component (with route loader prefetching — see tanstack-router.md)
|
|
53
|
+
const { data } = useSuspenseQuery(invoiceQueryOptions(invoiceId));
|
|
54
|
+
|
|
55
|
+
// Loader prefetch
|
|
56
|
+
loader: ({ context }) => context.queryClient.ensureQueryData(invoiceQueryOptions(id)),
|
|
57
|
+
|
|
58
|
+
// Manual prefetch (e.g., on hover, outside a route)
|
|
59
|
+
queryClient.prefetchQuery(invoiceQueryOptions(id));
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
This is the canonical pattern. Avoid defining query keys and functions inline at the call site.
|
|
63
|
+
|
|
64
|
+
## Queries
|
|
65
|
+
|
|
66
|
+
### Standard Query
|
|
67
|
+
|
|
68
|
+
Use when the component needs to render before data is available (initial mount without prefetch):
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
const { data, isLoading, error } = useQuery({
|
|
72
|
+
queryKey: ["invoices", filters],
|
|
73
|
+
queryFn: () => fetchInvoices(filters),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (error) return <ErrorState error={error} />;
|
|
77
|
+
if (isLoading) return <LoadingState />;
|
|
78
|
+
return <InvoiceTable invoices={data} />;
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Suspense Query
|
|
82
|
+
|
|
83
|
+
Use when paired with a route loader or `<Suspense>` boundary. No `isLoading` / `error` checks — boundaries handle them:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
const { data: invoices } = useSuspenseQuery(invoiceQueryOptions(filters));
|
|
87
|
+
return <InvoiceTable invoices={invoices} />;
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`<Suspense>` shows the fallback while loading. `<ErrorBoundary>` catches errors. See [`../react/error-boundaries.md`](../react/error-boundaries.md).
|
|
91
|
+
|
|
92
|
+
### Infinite Query
|
|
93
|
+
|
|
94
|
+
For cursor-paginated lists:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
|
|
98
|
+
queryKey: ["invoices", filters],
|
|
99
|
+
queryFn: ({ pageParam }) => fetchInvoices({ ...filters, cursor: pageParam }),
|
|
100
|
+
initialPageParam: undefined as string | undefined,
|
|
101
|
+
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// data.pages is an array of pages; flatten with .flatMap(page => page.items)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
See [`../architecture/api-design.md`](../architecture/api-design.md) for cursor pagination conventions.
|
|
108
|
+
|
|
109
|
+
## Mutations
|
|
110
|
+
|
|
111
|
+
### Basic Mutation
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
const { mutate, isPending } = useMutation({
|
|
115
|
+
mutationFn: createInvoice,
|
|
116
|
+
onSuccess: (newInvoice) => {
|
|
117
|
+
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Optimistic Updates
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
const { mutate } = useMutation({
|
|
126
|
+
mutationFn: createInvoice,
|
|
127
|
+
onMutate: async (newInvoice) => {
|
|
128
|
+
await queryClient.cancelQueries({ queryKey: ["invoices"] });
|
|
129
|
+
const previous = queryClient.getQueryData<Invoice[]>(["invoices"]);
|
|
130
|
+
queryClient.setQueryData<Invoice[]>(["invoices"], (old = []) => [
|
|
131
|
+
...old,
|
|
132
|
+
{ ...newInvoice, id: "temp", status: "draft" },
|
|
133
|
+
]);
|
|
134
|
+
return { previous };
|
|
135
|
+
},
|
|
136
|
+
onError: (_, __, context) => {
|
|
137
|
+
queryClient.setQueryData(["invoices"], context?.previous);
|
|
138
|
+
},
|
|
139
|
+
onSettled: () => {
|
|
140
|
+
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Query Key Conventions
|
|
146
|
+
|
|
147
|
+
Use arrays. Nest from general to specific. Filters go in a final object so they participate in cache keying:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
["invoices"] // all invoices (broad)
|
|
151
|
+
["invoices", "123"] // single invoice
|
|
152
|
+
["invoices", "123", "comments"] // nested resource
|
|
153
|
+
["invoices", { status: "open" }] // filtered list
|
|
154
|
+
["invoices", { status: "open", page: 2 }]
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Centralize query keys in a `queryKeys` factory for invalidation precision:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
export const invoiceKeys = {
|
|
161
|
+
all: ["invoices"] as const,
|
|
162
|
+
lists: () => [...invoiceKeys.all, "list"] as const,
|
|
163
|
+
list: (filters: InvoiceFilters) => [...invoiceKeys.lists(), filters] as const,
|
|
164
|
+
details: () => [...invoiceKeys.all, "detail"] as const,
|
|
165
|
+
detail: (id: string) => [...invoiceKeys.details(), id] as const,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Invalidate all lists but keep individual details fresh
|
|
169
|
+
queryClient.invalidateQueries({ queryKey: invoiceKeys.lists() });
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Invalidation Strategies
|
|
173
|
+
|
|
174
|
+
| After… | Invalidate… |
|
|
175
|
+
|---|---|
|
|
176
|
+
| `create` | List queries (`invoiceKeys.lists()`) |
|
|
177
|
+
| `update` (single resource) | The list **and** the specific detail (`invoiceKeys.detail(id)`) |
|
|
178
|
+
| `delete` | The list; remove the detail from cache (`queryClient.removeQueries`) |
|
|
179
|
+
| `bulk update` | The entire feature (`invoiceKeys.all`) |
|
|
180
|
+
|
|
181
|
+
`invalidateQueries` marks queries as stale and triggers refetch if they're active. `removeQueries` evicts them entirely.
|
|
182
|
+
|
|
183
|
+
## `staleTime` vs `gcTime`
|
|
184
|
+
|
|
185
|
+
- **`staleTime`** — how long data is considered fresh; while fresh, no background refetch
|
|
186
|
+
- **`gcTime`** (formerly `cacheTime`) — how long unused data is kept in memory after the last subscriber unmounts
|
|
187
|
+
|
|
188
|
+
Sensible defaults:
|
|
189
|
+
- List queries: `staleTime: 30_000`
|
|
190
|
+
- Detail queries: `staleTime: 60_000`
|
|
191
|
+
- Static reference data: `staleTime: Infinity` with explicit invalidation
|
|
192
|
+
|
|
193
|
+
## DO NOT
|
|
194
|
+
|
|
195
|
+
- Mix TanStack Query with manual `useEffect` fetch patterns
|
|
196
|
+
- Store server state in Zustand or Context — the query cache is the store
|
|
197
|
+
- Use TanStack Query for pure local UI state (modal open, selected tab without URL state)
|
|
198
|
+
- Define query keys inline at each call site — use `queryOptions` or a key factory
|
|
199
|
+
- Manually `setQueryData` for non-optimistic updates — invalidate instead
|
|
200
|
+
|
|
201
|
+
## PRIORITY
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
queryOptions (shared definition) > Inline query at call site
|
|
205
|
+
useSuspenseQuery + boundaries > useQuery + manual loading/error checks
|
|
206
|
+
Invalidate on mutation > Manual cache updates
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## See Also
|
|
210
|
+
|
|
211
|
+
- [`tanstack-router.md`](tanstack-router.md) — loaders + `useSuspenseQuery` pattern
|
|
212
|
+
- [`../react/state-management.md`](../react/state-management.md) — when to use TanStack Query vs other state
|
|
213
|
+
- [`../react/error-boundaries.md`](../react/error-boundaries.md) — Suspense and ErrorBoundary pairing
|
|
214
|
+
- [`../architecture/api-design.md`](../architecture/api-design.md) — pagination, error shape
|