red64-cli 0.1.0 → 0.3.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/README.md +1 -2
- package/dist/cli/parseArgs.d.ts.map +1 -1
- package/dist/cli/parseArgs.js +5 -0
- package/dist/cli/parseArgs.js.map +1 -1
- package/dist/components/init/CompleteStep.d.ts.map +1 -1
- package/dist/components/init/CompleteStep.js +2 -2
- package/dist/components/init/CompleteStep.js.map +1 -1
- package/dist/components/init/TestCheckStep.d.ts +16 -0
- package/dist/components/init/TestCheckStep.d.ts.map +1 -0
- package/dist/components/init/TestCheckStep.js +120 -0
- package/dist/components/init/TestCheckStep.js.map +1 -0
- package/dist/components/init/index.d.ts +1 -0
- package/dist/components/init/index.d.ts.map +1 -1
- package/dist/components/init/index.js +1 -0
- package/dist/components/init/index.js.map +1 -1
- package/dist/components/init/types.d.ts +9 -0
- package/dist/components/init/types.d.ts.map +1 -1
- package/dist/components/screens/InitScreen.d.ts.map +1 -1
- package/dist/components/screens/InitScreen.js +69 -6
- package/dist/components/screens/InitScreen.js.map +1 -1
- package/dist/components/screens/ListScreen.d.ts.map +1 -1
- package/dist/components/screens/ListScreen.js +28 -3
- package/dist/components/screens/ListScreen.js.map +1 -1
- package/dist/components/screens/StartScreen.d.ts.map +1 -1
- package/dist/components/screens/StartScreen.js +212 -13
- package/dist/components/screens/StartScreen.js.map +1 -1
- package/dist/components/ui/ArtifactsSidebar.d.ts +19 -0
- package/dist/components/ui/ArtifactsSidebar.d.ts.map +1 -0
- package/dist/components/ui/ArtifactsSidebar.js +51 -0
- package/dist/components/ui/ArtifactsSidebar.js.map +1 -0
- package/dist/components/ui/FeatureSidebar.d.ts.map +1 -1
- package/dist/components/ui/FeatureSidebar.js +1 -1
- package/dist/components/ui/FeatureSidebar.js.map +1 -1
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +1 -0
- package/dist/components/ui/index.js.map +1 -1
- package/dist/services/ClaudeErrorDetector.js +3 -3
- package/dist/services/ClaudeErrorDetector.js.map +1 -1
- package/dist/services/ConfigService.d.ts +1 -0
- package/dist/services/ConfigService.d.ts.map +1 -1
- package/dist/services/ConfigService.js.map +1 -1
- package/dist/services/ProjectDetector.d.ts +28 -0
- package/dist/services/ProjectDetector.d.ts.map +1 -0
- package/dist/services/ProjectDetector.js +236 -0
- package/dist/services/ProjectDetector.js.map +1 -0
- package/dist/services/TestRunner.d.ts +46 -0
- package/dist/services/TestRunner.d.ts.map +1 -0
- package/dist/services/TestRunner.js +85 -0
- package/dist/services/TestRunner.js.map +1 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +2 -0
- package/dist/services/index.js.map +1 -1
- package/dist/types/index.d.ts +13 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/framework/.red64/settings/templates/specs/gap-analysis.md +163 -0
- package/framework/agents/claude/.claude/agents/red64/spec-impl.md +131 -2
- package/framework/agents/claude/.claude/agents/red64/validate-gap.md +13 -7
- package/framework/agents/claude/.claude/commands/red64/spec-impl.md +24 -0
- package/framework/agents/claude/.claude/commands/red64/validate-gap.md +4 -0
- package/framework/agents/codex/.codex/agents/red64/spec-impl.md +131 -2
- package/framework/agents/codex/.codex/agents/red64/validate-gap.md +13 -7
- package/framework/agents/codex/.codex/commands/red64/spec-impl.md +24 -0
- package/framework/agents/codex/.codex/commands/red64/validate-gap.md +4 -0
- package/framework/stacks/generic/feedback.md +80 -0
- package/framework/stacks/nextjs/accessibility.md +437 -0
- package/framework/stacks/nextjs/api.md +431 -0
- package/framework/stacks/nextjs/coding-style.md +282 -0
- package/framework/stacks/nextjs/commenting.md +226 -0
- package/framework/stacks/nextjs/components.md +411 -0
- package/framework/stacks/nextjs/conventions.md +333 -0
- package/framework/stacks/nextjs/css.md +310 -0
- package/framework/stacks/nextjs/error-handling.md +442 -0
- package/framework/stacks/nextjs/feedback.md +124 -0
- package/framework/stacks/nextjs/migrations.md +332 -0
- package/framework/stacks/nextjs/models.md +362 -0
- package/framework/stacks/nextjs/queries.md +410 -0
- package/framework/stacks/nextjs/responsive.md +338 -0
- package/framework/stacks/nextjs/tech-stack.md +177 -0
- package/framework/stacks/nextjs/test-writing.md +475 -0
- package/framework/stacks/nextjs/validation.md +467 -0
- package/framework/stacks/python/api.md +468 -0
- package/framework/stacks/python/authentication.md +342 -0
- package/framework/stacks/python/code-quality.md +283 -0
- package/framework/stacks/python/code-refactoring.md +315 -0
- package/framework/stacks/python/coding-style.md +462 -0
- package/framework/stacks/python/conventions.md +399 -0
- package/framework/stacks/python/error-handling.md +512 -0
- package/framework/stacks/python/feedback.md +92 -0
- package/framework/stacks/python/implement-ai-llm.md +468 -0
- package/framework/stacks/python/migrations.md +388 -0
- package/framework/stacks/python/models.md +399 -0
- package/framework/stacks/python/python.md +232 -0
- package/framework/stacks/python/queries.md +451 -0
- package/framework/stacks/python/structure.md +245 -58
- package/framework/stacks/python/tech.md +92 -35
- package/framework/stacks/python/testing.md +380 -0
- package/framework/stacks/python/validation.md +471 -0
- package/framework/stacks/rails/authentication.md +176 -0
- package/framework/stacks/rails/code-quality.md +287 -0
- package/framework/stacks/rails/code-refactoring.md +299 -0
- package/framework/stacks/rails/feedback.md +130 -0
- package/framework/stacks/rails/implement-ai-llm-with-rubyllm.md +342 -0
- package/framework/stacks/rails/rails.md +301 -0
- package/framework/stacks/rails/rails8-best-practices.md +498 -0
- package/framework/stacks/rails/rails8-css.md +573 -0
- package/framework/stacks/rails/structure.md +140 -0
- package/framework/stacks/rails/tech.md +108 -0
- package/framework/stacks/react/code-quality.md +521 -0
- package/framework/stacks/react/components.md +625 -0
- package/framework/stacks/react/data-fetching.md +586 -0
- package/framework/stacks/react/feedback.md +110 -0
- package/framework/stacks/react/forms.md +694 -0
- package/framework/stacks/react/performance.md +640 -0
- package/framework/stacks/react/product.md +22 -9
- package/framework/stacks/react/state-management.md +472 -0
- package/framework/stacks/react/structure.md +351 -44
- package/framework/stacks/react/tech.md +219 -30
- package/framework/stacks/react/testing.md +690 -0
- package/package.json +1 -1
- package/framework/stacks/node/product.md +0 -27
- package/framework/stacks/node/structure.md +0 -82
- package/framework/stacks/node/tech.md +0 -63
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# Project Conventions
|
|
2
|
+
|
|
3
|
+
Next.js 15 App Router file conventions, folder structure, environment management, and development workflow.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Philosophy
|
|
8
|
+
|
|
9
|
+
- **Convention over configuration**: Follow Next.js file-based conventions; avoid custom routing
|
|
10
|
+
- **Colocation**: Keep related files together; tests next to source, types next to usage
|
|
11
|
+
- **Explicit boundaries**: Every route segment has its own loading, error, and not-found states
|
|
12
|
+
- **Environment safety**: Type-safe env vars, never leak secrets to the client
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## App Router File Conventions
|
|
17
|
+
|
|
18
|
+
### Special Files
|
|
19
|
+
|
|
20
|
+
| File | Purpose | Required |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| `page.tsx` | Route UI, makes segment publicly accessible | Yes (for routes) |
|
|
23
|
+
| `layout.tsx` | Shared UI wrapper, persists across navigations | Root required |
|
|
24
|
+
| `loading.tsx` | Loading UI shown while page/segment loads | Recommended |
|
|
25
|
+
| `error.tsx` | Error boundary for the segment | Recommended |
|
|
26
|
+
| `not-found.tsx` | UI for `notFound()` calls | Recommended |
|
|
27
|
+
| `template.tsx` | Like layout but re-mounts on navigation | Rare |
|
|
28
|
+
| `default.tsx` | Parallel route fallback | When using parallel routes |
|
|
29
|
+
| `route.ts` | API endpoint (GET, POST, etc.) | For API routes |
|
|
30
|
+
| `global-error.tsx` | Root-level error boundary (wraps `<html>`) | Recommended |
|
|
31
|
+
| `middleware.ts` | Runs before every request (at project root) | As needed |
|
|
32
|
+
|
|
33
|
+
### File Naming
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
app/
|
|
37
|
+
layout.tsx # Root layout (required)
|
|
38
|
+
page.tsx # Home page (/)
|
|
39
|
+
loading.tsx # Global loading state
|
|
40
|
+
error.tsx # Global error boundary
|
|
41
|
+
not-found.tsx # Global 404 page
|
|
42
|
+
global-error.tsx # Root error boundary
|
|
43
|
+
|
|
44
|
+
dashboard/
|
|
45
|
+
layout.tsx # Dashboard layout (sidebar, nav)
|
|
46
|
+
page.tsx # /dashboard
|
|
47
|
+
loading.tsx # Dashboard loading skeleton
|
|
48
|
+
error.tsx # Dashboard error boundary
|
|
49
|
+
|
|
50
|
+
settings/
|
|
51
|
+
page.tsx # /dashboard/settings
|
|
52
|
+
|
|
53
|
+
users/
|
|
54
|
+
page.tsx # /dashboard/users
|
|
55
|
+
[id]/
|
|
56
|
+
page.tsx # /dashboard/users/:id
|
|
57
|
+
edit/
|
|
58
|
+
page.tsx # /dashboard/users/:id/edit
|
|
59
|
+
|
|
60
|
+
(auth)/ # Route group (no URL segment)
|
|
61
|
+
login/
|
|
62
|
+
page.tsx # /login
|
|
63
|
+
register/
|
|
64
|
+
page.tsx # /register
|
|
65
|
+
|
|
66
|
+
api/
|
|
67
|
+
health/
|
|
68
|
+
route.ts # GET /api/health
|
|
69
|
+
users/
|
|
70
|
+
route.ts # GET, POST /api/users
|
|
71
|
+
[id]/
|
|
72
|
+
route.ts # GET, PATCH, DELETE /api/users/:id
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Folder Structure
|
|
78
|
+
|
|
79
|
+
### Full Project Layout
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
src/
|
|
83
|
+
app/ # Next.js routes and layouts
|
|
84
|
+
components/
|
|
85
|
+
ui/ # Reusable primitives (Button, Input, Card)
|
|
86
|
+
forms/ # Form-specific components
|
|
87
|
+
layouts/ # Layout building blocks (Sidebar, Header)
|
|
88
|
+
lib/
|
|
89
|
+
prisma.ts # Prisma client singleton
|
|
90
|
+
auth.ts # NextAuth configuration
|
|
91
|
+
utils.ts # General utilities
|
|
92
|
+
hooks/ # Custom React hooks
|
|
93
|
+
actions/ # Server actions
|
|
94
|
+
types/ # Shared TypeScript types
|
|
95
|
+
prisma/
|
|
96
|
+
schema.prisma # Database schema
|
|
97
|
+
migrations/ # Migration history
|
|
98
|
+
seed.ts # Seed data script
|
|
99
|
+
public/ # Static assets (favicons, og images)
|
|
100
|
+
tests/
|
|
101
|
+
e2e/ # Playwright end-to-end tests
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Naming Patterns
|
|
105
|
+
|
|
106
|
+
| Entity | Convention | Example |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| Route directories | kebab-case | `user-settings/`, `api-keys/` |
|
|
109
|
+
| Component files | kebab-case | `user-card.tsx`, `data-table.tsx` |
|
|
110
|
+
| Utility files | kebab-case | `format-date.ts`, `cn.ts` |
|
|
111
|
+
| Hook files | kebab-case with use- prefix | `use-debounce.ts` |
|
|
112
|
+
| Action files | kebab-case | `user-actions.ts` |
|
|
113
|
+
| Type files | kebab-case | `api-types.ts` |
|
|
114
|
+
| Constants | kebab-case file, UPPER_SNAKE exports | `config.ts` with `MAX_RETRIES` |
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Route Groups and Parallel Routes
|
|
119
|
+
|
|
120
|
+
### Route Groups (Parentheses)
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
app/
|
|
124
|
+
(marketing)/ # No URL impact
|
|
125
|
+
layout.tsx # Marketing layout (different nav)
|
|
126
|
+
page.tsx # / (home)
|
|
127
|
+
about/
|
|
128
|
+
page.tsx # /about
|
|
129
|
+
pricing/
|
|
130
|
+
page.tsx # /pricing
|
|
131
|
+
|
|
132
|
+
(dashboard)/ # No URL impact
|
|
133
|
+
layout.tsx # Dashboard layout (sidebar)
|
|
134
|
+
dashboard/
|
|
135
|
+
page.tsx # /dashboard
|
|
136
|
+
settings/
|
|
137
|
+
page.tsx # /dashboard/settings
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Dynamic Routes
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
// app/users/[id]/page.tsx
|
|
144
|
+
interface PageProps {
|
|
145
|
+
params: Promise<{ id: string }>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export default async function UserPage({ params }: PageProps) {
|
|
149
|
+
const { id } = await params;
|
|
150
|
+
const user = await prisma.user.findUnique({ where: { id } });
|
|
151
|
+
|
|
152
|
+
if (!user) {
|
|
153
|
+
notFound();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return <UserProfile user={user} />;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// app/blog/[...slug]/page.tsx -- Catch-all route
|
|
160
|
+
interface BlogPageProps {
|
|
161
|
+
params: Promise<{ slug: string[] }>;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export default async function BlogPage({ params }: BlogPageProps) {
|
|
165
|
+
const { slug } = await params;
|
|
166
|
+
// slug = ["2024", "01", "my-post"] for /blog/2024/01/my-post
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Environment Variables
|
|
173
|
+
|
|
174
|
+
### Naming Convention
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
# Server-only (never sent to browser)
|
|
178
|
+
DATABASE_URL="postgresql://..."
|
|
179
|
+
NEXTAUTH_SECRET="..."
|
|
180
|
+
STRIPE_SECRET_KEY="sk_..."
|
|
181
|
+
OPENAI_API_KEY="sk-..."
|
|
182
|
+
|
|
183
|
+
# Client-safe (bundled into browser code)
|
|
184
|
+
NEXT_PUBLIC_APP_NAME="MyApp"
|
|
185
|
+
NEXT_PUBLIC_APP_URL="https://myapp.com"
|
|
186
|
+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_..."
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Type-Safe Environment
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
// lib/env.ts
|
|
193
|
+
import { z } from "zod";
|
|
194
|
+
|
|
195
|
+
const envSchema = z.object({
|
|
196
|
+
DATABASE_URL: z.string().url(),
|
|
197
|
+
NEXTAUTH_SECRET: z.string().min(32),
|
|
198
|
+
NEXTAUTH_URL: z.string().url(),
|
|
199
|
+
NEXT_PUBLIC_APP_NAME: z.string(),
|
|
200
|
+
NEXT_PUBLIC_APP_URL: z.string().url(),
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
export const env = envSchema.parse(process.env);
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Files Hierarchy
|
|
207
|
+
|
|
208
|
+
| File | Purpose | Committed |
|
|
209
|
+
|---|---|---|
|
|
210
|
+
| `.env` | Defaults for all environments | Yes |
|
|
211
|
+
| `.env.local` | Local overrides with secrets | No (gitignored) |
|
|
212
|
+
| `.env.development` | Dev-specific defaults | Yes |
|
|
213
|
+
| `.env.production` | Prod-specific defaults | Yes |
|
|
214
|
+
| `.env.test` | Test environment | Yes |
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Git Workflow
|
|
219
|
+
|
|
220
|
+
### Branch Naming
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
feature/add-user-dashboard
|
|
224
|
+
fix/login-redirect-loop
|
|
225
|
+
chore/upgrade-prisma-6
|
|
226
|
+
refactor/extract-auth-middleware
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Commit Messages
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
feat: add user profile page with avatar upload
|
|
233
|
+
fix: prevent duplicate form submissions on slow networks
|
|
234
|
+
refactor: extract validation schemas to shared module
|
|
235
|
+
chore: upgrade Next.js to 15.1
|
|
236
|
+
docs: add API authentication guide
|
|
237
|
+
test: add E2E tests for checkout flow
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Pull Request Checklist
|
|
241
|
+
|
|
242
|
+
- TypeScript compiles without errors (`pnpm tsc --noEmit`)
|
|
243
|
+
- ESLint passes (`pnpm lint`)
|
|
244
|
+
- Tests pass (`pnpm test`)
|
|
245
|
+
- New routes have `loading.tsx` and `error.tsx`
|
|
246
|
+
- Environment variables documented if added
|
|
247
|
+
- Database migrations included if schema changed
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Feature Flags
|
|
252
|
+
|
|
253
|
+
### Simple Environment-Based
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
// lib/flags.ts
|
|
257
|
+
export const flags = {
|
|
258
|
+
newDashboard: process.env.NEXT_PUBLIC_FF_NEW_DASHBOARD === "true",
|
|
259
|
+
aiFeatures: process.env.FF_AI_FEATURES === "true",
|
|
260
|
+
} as const;
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Usage
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
import { flags } from "@/lib/flags";
|
|
267
|
+
|
|
268
|
+
export default function DashboardPage() {
|
|
269
|
+
if (flags.newDashboard) {
|
|
270
|
+
return <NewDashboard />;
|
|
271
|
+
}
|
|
272
|
+
return <LegacyDashboard />;
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Metadata and SEO
|
|
279
|
+
|
|
280
|
+
### Static Metadata
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// app/layout.tsx
|
|
284
|
+
import type { Metadata } from "next";
|
|
285
|
+
|
|
286
|
+
export const metadata: Metadata = {
|
|
287
|
+
title: {
|
|
288
|
+
default: "MyApp",
|
|
289
|
+
template: "%s | MyApp",
|
|
290
|
+
},
|
|
291
|
+
description: "A modern web application",
|
|
292
|
+
metadataBase: new URL("https://myapp.com"),
|
|
293
|
+
openGraph: {
|
|
294
|
+
type: "website",
|
|
295
|
+
locale: "en_US",
|
|
296
|
+
siteName: "MyApp",
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Dynamic Metadata
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
// app/users/[id]/page.tsx
|
|
305
|
+
import type { Metadata } from "next";
|
|
306
|
+
|
|
307
|
+
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
|
308
|
+
const { id } = await params;
|
|
309
|
+
const user = await prisma.user.findUnique({ where: { id } });
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
title: user?.name ?? "User Not Found",
|
|
313
|
+
description: user?.bio ?? undefined,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Anti-Patterns
|
|
321
|
+
|
|
322
|
+
| Anti-Pattern | Problem | Correct Approach |
|
|
323
|
+
|---|---|---|
|
|
324
|
+
| Custom routing library | Fights the framework | Use App Router file conventions |
|
|
325
|
+
| `NEXT_PUBLIC_` on secrets | Exposes secrets to the browser | Only prefix client-safe values |
|
|
326
|
+
| Missing `loading.tsx` | Blank screen during navigation | Add loading states for every route segment |
|
|
327
|
+
| Deeply nested routes (5+ levels) | Hard to navigate, slow resolution | Flatten with route groups |
|
|
328
|
+
| Environment variables without validation | Runtime crashes on missing values | Validate with zod at startup |
|
|
329
|
+
| Feature code in `app/` directory | Mixes routing with business logic | Keep route files thin, logic in `lib/` or `actions/` |
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
_Follow the framework's conventions. When Next.js provides a file-based solution, use it instead of inventing your own._
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# CSS and Styling
|
|
2
|
+
|
|
3
|
+
Tailwind CSS v4 patterns for Next.js projects with design tokens, component variants, and dark mode.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Philosophy
|
|
8
|
+
|
|
9
|
+
- **Utility-first**: Compose styles in markup; extract components, not CSS classes
|
|
10
|
+
- **Design tokens**: Define colors, spacing, and typography as CSS custom properties
|
|
11
|
+
- **No custom CSS unless necessary**: Tailwind utilities handle 95% of cases
|
|
12
|
+
- **Consistent variants**: Use class-variance-authority for component style APIs
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Tailwind CSS v4 Configuration
|
|
17
|
+
|
|
18
|
+
### CSS-First Config (No `tailwind.config.js`)
|
|
19
|
+
|
|
20
|
+
```css
|
|
21
|
+
/* app/globals.css */
|
|
22
|
+
@import "tailwindcss";
|
|
23
|
+
|
|
24
|
+
@theme {
|
|
25
|
+
/* Colors */
|
|
26
|
+
--color-brand-50: oklch(0.97 0.02 250);
|
|
27
|
+
--color-brand-500: oklch(0.55 0.2 250);
|
|
28
|
+
--color-brand-900: oklch(0.25 0.1 250);
|
|
29
|
+
|
|
30
|
+
/* Semantic colors */
|
|
31
|
+
--color-background: var(--color-white);
|
|
32
|
+
--color-foreground: var(--color-gray-950);
|
|
33
|
+
--color-muted: var(--color-gray-100);
|
|
34
|
+
--color-muted-foreground: var(--color-gray-500);
|
|
35
|
+
--color-border: var(--color-gray-200);
|
|
36
|
+
--color-primary: var(--color-brand-500);
|
|
37
|
+
--color-destructive: oklch(0.55 0.2 25);
|
|
38
|
+
|
|
39
|
+
/* Typography */
|
|
40
|
+
--font-sans: "Inter Variable", ui-sans-serif, system-ui, sans-serif;
|
|
41
|
+
--font-mono: "JetBrains Mono Variable", ui-monospace, monospace;
|
|
42
|
+
|
|
43
|
+
/* Radius */
|
|
44
|
+
--radius-sm: 0.25rem;
|
|
45
|
+
--radius-md: 0.375rem;
|
|
46
|
+
--radius-lg: 0.5rem;
|
|
47
|
+
|
|
48
|
+
/* Shadows */
|
|
49
|
+
--shadow-sm: 0 1px 2px 0 oklch(0 0 0 / 0.05);
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Dark Mode
|
|
54
|
+
|
|
55
|
+
```css
|
|
56
|
+
/* app/globals.css (continued) */
|
|
57
|
+
@variant dark (&:where(.dark, .dark *));
|
|
58
|
+
|
|
59
|
+
@theme {
|
|
60
|
+
/* Light mode defaults above, dark mode overrides: */
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.dark {
|
|
64
|
+
--color-background: var(--color-gray-950);
|
|
65
|
+
--color-foreground: var(--color-gray-50);
|
|
66
|
+
--color-muted: var(--color-gray-900);
|
|
67
|
+
--color-muted-foreground: var(--color-gray-400);
|
|
68
|
+
--color-border: var(--color-gray-800);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Dark Mode Toggle
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
// components/theme-toggle.tsx
|
|
76
|
+
"use client";
|
|
77
|
+
|
|
78
|
+
import { useEffect, useState } from "react";
|
|
79
|
+
|
|
80
|
+
export function ThemeToggle() {
|
|
81
|
+
const [dark, setDark] = useState(false);
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
const stored = localStorage.getItem("theme");
|
|
85
|
+
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
86
|
+
setDark(stored === "dark" || (!stored && prefersDark));
|
|
87
|
+
}, []);
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
document.documentElement.classList.toggle("dark", dark);
|
|
91
|
+
localStorage.setItem("theme", dark ? "dark" : "light");
|
|
92
|
+
}, [dark]);
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<button onClick={() => setDark(!dark)} aria-label="Toggle dark mode">
|
|
96
|
+
{dark ? <SunIcon /> : <MoonIcon />}
|
|
97
|
+
</button>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## The `cn()` Utility
|
|
105
|
+
|
|
106
|
+
### Setup
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
// lib/utils.ts
|
|
110
|
+
import { clsx, type ClassValue } from "clsx";
|
|
111
|
+
import { twMerge } from "tailwind-merge";
|
|
112
|
+
|
|
113
|
+
export function cn(...inputs: ClassValue[]) {
|
|
114
|
+
return twMerge(clsx(inputs));
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Usage
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { cn } from "@/lib/utils";
|
|
122
|
+
|
|
123
|
+
interface BadgeProps {
|
|
124
|
+
variant: "success" | "warning" | "error";
|
|
125
|
+
className?: string;
|
|
126
|
+
children: React.ReactNode;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function Badge({ variant, className, children }: BadgeProps) {
|
|
130
|
+
return (
|
|
131
|
+
<span
|
|
132
|
+
className={cn(
|
|
133
|
+
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium",
|
|
134
|
+
{
|
|
135
|
+
"bg-green-100 text-green-800": variant === "success",
|
|
136
|
+
"bg-yellow-100 text-yellow-800": variant === "warning",
|
|
137
|
+
"bg-red-100 text-red-800": variant === "error",
|
|
138
|
+
},
|
|
139
|
+
className
|
|
140
|
+
)}
|
|
141
|
+
>
|
|
142
|
+
{children}
|
|
143
|
+
</span>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Why `cn()`**: `clsx` handles conditionals, `twMerge` resolves conflicting Tailwind classes (e.g., a passed `className` of `bg-blue-500` correctly overrides an internal `bg-green-100`).
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Component Variants with CVA
|
|
153
|
+
|
|
154
|
+
### Setup
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// components/ui/button.tsx
|
|
158
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
159
|
+
import { cn } from "@/lib/utils";
|
|
160
|
+
|
|
161
|
+
const buttonVariants = cva(
|
|
162
|
+
// Base styles
|
|
163
|
+
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
164
|
+
{
|
|
165
|
+
variants: {
|
|
166
|
+
variant: {
|
|
167
|
+
primary: "bg-primary text-white hover:bg-primary/90",
|
|
168
|
+
secondary: "bg-muted text-foreground hover:bg-muted/80",
|
|
169
|
+
destructive: "bg-destructive text-white hover:bg-destructive/90",
|
|
170
|
+
outline: "border border-border bg-transparent hover:bg-muted",
|
|
171
|
+
ghost: "hover:bg-muted",
|
|
172
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
173
|
+
},
|
|
174
|
+
size: {
|
|
175
|
+
sm: "h-8 px-3 text-xs",
|
|
176
|
+
md: "h-10 px-4",
|
|
177
|
+
lg: "h-12 px-6 text-base",
|
|
178
|
+
icon: "h-10 w-10",
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
defaultVariants: {
|
|
182
|
+
variant: "primary",
|
|
183
|
+
size: "md",
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
interface ButtonProps
|
|
189
|
+
extends React.ComponentProps<"button">,
|
|
190
|
+
VariantProps<typeof buttonVariants> {
|
|
191
|
+
isLoading?: boolean;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function Button({
|
|
195
|
+
variant,
|
|
196
|
+
size,
|
|
197
|
+
isLoading,
|
|
198
|
+
disabled,
|
|
199
|
+
className,
|
|
200
|
+
children,
|
|
201
|
+
...props
|
|
202
|
+
}: ButtonProps) {
|
|
203
|
+
return (
|
|
204
|
+
<button
|
|
205
|
+
className={cn(buttonVariants({ variant, size }), className)}
|
|
206
|
+
disabled={disabled || isLoading}
|
|
207
|
+
{...props}
|
|
208
|
+
>
|
|
209
|
+
{isLoading && <Spinner className="mr-2 h-4 w-4 animate-spin" />}
|
|
210
|
+
{children}
|
|
211
|
+
</button>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export { buttonVariants };
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Common Patterns
|
|
221
|
+
|
|
222
|
+
### Container
|
|
223
|
+
|
|
224
|
+
```css
|
|
225
|
+
/* Use Tailwind's container or define a custom one */
|
|
226
|
+
@utility container {
|
|
227
|
+
margin-inline: auto;
|
|
228
|
+
padding-inline: 1rem;
|
|
229
|
+
max-width: 80rem;
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Focus Styles
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
// Consistent focus ring across all interactive elements
|
|
237
|
+
const focusRing = "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2";
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Animations
|
|
241
|
+
|
|
242
|
+
```css
|
|
243
|
+
/* app/globals.css */
|
|
244
|
+
@theme {
|
|
245
|
+
--animate-fade-in: fade-in 0.2s ease-out;
|
|
246
|
+
--animate-slide-up: slide-up 0.3s ease-out;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
@keyframes fade-in {
|
|
250
|
+
from { opacity: 0; }
|
|
251
|
+
to { opacity: 1; }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
@keyframes slide-up {
|
|
255
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
256
|
+
to { opacity: 1; transform: translateY(0); }
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## Layout Patterns
|
|
263
|
+
|
|
264
|
+
### Sidebar Layout
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
export function DashboardLayout({ children }: { children: React.ReactNode }) {
|
|
268
|
+
return (
|
|
269
|
+
<div className="flex min-h-screen">
|
|
270
|
+
<aside className="w-64 shrink-0 border-r bg-muted/50">
|
|
271
|
+
<nav className="p-4">
|
|
272
|
+
{/* nav items */}
|
|
273
|
+
</nav>
|
|
274
|
+
</aside>
|
|
275
|
+
<main className="flex-1 p-8">{children}</main>
|
|
276
|
+
</div>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Centered Content
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
export function AuthLayout({ children }: { children: React.ReactNode }) {
|
|
285
|
+
return (
|
|
286
|
+
<div className="flex min-h-screen items-center justify-center bg-muted/30">
|
|
287
|
+
<div className="w-full max-w-md rounded-xl border bg-background p-8 shadow-sm">
|
|
288
|
+
{children}
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Anti-Patterns
|
|
298
|
+
|
|
299
|
+
| Anti-Pattern | Problem | Correct Approach |
|
|
300
|
+
|---|---|---|
|
|
301
|
+
| `@apply` everywhere | Defeats utility-first purpose | Extract React components instead |
|
|
302
|
+
| Hardcoded colors | Inconsistent, no dark mode | Use design tokens from `@theme` |
|
|
303
|
+
| `!important` in utilities | Fragile, hard to override | Use `cn()` to merge conflicts |
|
|
304
|
+
| CSS modules + Tailwind | Two styling systems to maintain | Pick one; prefer Tailwind |
|
|
305
|
+
| Inline `style={{}}` | No design system, no responsive | Use Tailwind utilities |
|
|
306
|
+
| Magic numbers for spacing | Inconsistent spacing | Use Tailwind's spacing scale |
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
_Style components with utilities. Extract React components, not CSS classes. Let the design system enforce consistency._
|