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,226 @@
|
|
|
1
|
+
# Code Commenting Standards
|
|
2
|
+
|
|
3
|
+
Documentation and commenting conventions for TypeScript and React codebases, favoring self-documenting code with strategic comments.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Philosophy
|
|
8
|
+
|
|
9
|
+
- **Code is the source of truth**: Comments explain why, not what
|
|
10
|
+
- **Self-documenting first**: Clear names and structure beat comments every time
|
|
11
|
+
- **Evergreen only**: No temporal comments about recent changes or fixes
|
|
12
|
+
- **Public APIs deserve docs**: JSDoc for exported functions, types, and components
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## When to Comment vs Self-Document
|
|
17
|
+
|
|
18
|
+
### Do Not Comment
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
// BAD: Comment restates the code
|
|
22
|
+
// Check if user is active
|
|
23
|
+
if (user.isActive) { ... }
|
|
24
|
+
|
|
25
|
+
// BAD: Comment about a recent change
|
|
26
|
+
// Fixed bug where users could submit empty names (PR #142)
|
|
27
|
+
const name = formData.get("name");
|
|
28
|
+
|
|
29
|
+
// BAD: Commented-out code
|
|
30
|
+
// const oldLogic = computeLegacyScore(user);
|
|
31
|
+
const score = computeScore(user);
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Do Comment
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
// GOOD: Explains a non-obvious business rule
|
|
38
|
+
// Users created before 2023 have unlimited storage due to legacy pricing
|
|
39
|
+
if (user.createdAt < LEGACY_CUTOFF_DATE) {
|
|
40
|
+
return Infinity;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// GOOD: Explains a workaround
|
|
44
|
+
// Prisma does not support upsert with composite keys in SQLite,
|
|
45
|
+
// so we use a transaction with findFirst + create/update instead
|
|
46
|
+
await prisma.$transaction(async (tx) => { ... });
|
|
47
|
+
|
|
48
|
+
// GOOD: Documents a performance decision
|
|
49
|
+
// Batched in chunks of 100 to avoid exceeding Postgres parameter limit (65535)
|
|
50
|
+
for (const chunk of chunks(items, 100)) { ... }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## JSDoc for Public APIs
|
|
56
|
+
|
|
57
|
+
### Exported Functions
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
/**
|
|
61
|
+
* Formats a date relative to now (e.g., "2 hours ago", "yesterday").
|
|
62
|
+
*
|
|
63
|
+
* Falls back to absolute date format for dates older than 7 days.
|
|
64
|
+
*
|
|
65
|
+
* @param date - The date to format
|
|
66
|
+
* @param locale - BCP 47 locale string (defaults to "en-US")
|
|
67
|
+
* @returns Formatted relative or absolute date string
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* formatRelativeDate(new Date()) // "just now"
|
|
72
|
+
* formatRelativeDate(subDays(new Date(), 2)) // "2 days ago"
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export function formatRelativeDate(date: Date, locale = "en-US"): string {
|
|
76
|
+
// ...
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Exported Types
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
/**
|
|
84
|
+
* Result of a server action that may fail with field-level errors.
|
|
85
|
+
*
|
|
86
|
+
* @typeParam T - The shape of field errors (keys are field names, values are error messages)
|
|
87
|
+
*/
|
|
88
|
+
export type ActionResult<T extends Record<string, string[]> = Record<string, string[]>> =
|
|
89
|
+
| { success: true }
|
|
90
|
+
| { success: false; error: string; fieldErrors?: T };
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### React Components
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
/**
|
|
97
|
+
* Displays a user avatar with fallback initials.
|
|
98
|
+
*
|
|
99
|
+
* Renders the user's profile image when available, otherwise shows
|
|
100
|
+
* initials derived from the user's name on a colored background.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```tsx
|
|
104
|
+
* <UserAvatar user={currentUser} size="lg" />
|
|
105
|
+
* <UserAvatar user={currentUser} size="sm" className="ring-2 ring-white" />
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
export function UserAvatar({ user, size = "md", className }: UserAvatarProps) {
|
|
109
|
+
// ...
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## TSDoc Tags Reference
|
|
116
|
+
|
|
117
|
+
| Tag | Usage |
|
|
118
|
+
|---|---|
|
|
119
|
+
| `@param name` | Describe a function parameter |
|
|
120
|
+
| `@returns` | Describe the return value |
|
|
121
|
+
| `@throws` | Document exceptions that may be thrown |
|
|
122
|
+
| `@example` | Provide usage examples (fenced code blocks) |
|
|
123
|
+
| `@typeParam T` | Describe a generic type parameter |
|
|
124
|
+
| `@see` | Reference related functions or documentation |
|
|
125
|
+
| `@deprecated` | Mark as deprecated with migration guidance |
|
|
126
|
+
| `@internal` | Mark as not part of the public API |
|
|
127
|
+
|
|
128
|
+
### Deprecation Pattern
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
/**
|
|
132
|
+
* @deprecated Use `formatRelativeDate` instead. Will be removed in v3.0.
|
|
133
|
+
* @see formatRelativeDate
|
|
134
|
+
*/
|
|
135
|
+
export function timeAgo(date: Date): string {
|
|
136
|
+
return formatRelativeDate(date);
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## TODO Conventions
|
|
143
|
+
|
|
144
|
+
### Format
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// TODO(username): Brief description of what needs to be done
|
|
148
|
+
// TODO(yacin): Add pagination support when user count exceeds 1000
|
|
149
|
+
|
|
150
|
+
// TODO: Acceptable when author is obvious from git blame
|
|
151
|
+
// TODO: Replace with server action once Next.js supports streaming responses in actions
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Rules
|
|
155
|
+
|
|
156
|
+
- Include a name or context when the TODO is non-trivial
|
|
157
|
+
- Never use TODO as an excuse to leave broken code -- the code must work without the TODO being resolved
|
|
158
|
+
- Do not use FIXME, HACK, or XXX -- use TODO with a clear description instead
|
|
159
|
+
- Periodically audit TODOs and convert to issues or delete stale ones
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## File-Level Comments
|
|
164
|
+
|
|
165
|
+
### When Needed
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
/**
|
|
169
|
+
* Prisma client singleton for Next.js.
|
|
170
|
+
*
|
|
171
|
+
* In development, Next.js clears the Node.js module cache on every request,
|
|
172
|
+
* which would create a new PrismaClient instance each time. This module
|
|
173
|
+
* stores the client on `globalThis` to prevent connection pool exhaustion.
|
|
174
|
+
*
|
|
175
|
+
* @see https://www.prisma.io/docs/guides/nextjs
|
|
176
|
+
*/
|
|
177
|
+
import { PrismaClient } from "@prisma/client";
|
|
178
|
+
|
|
179
|
+
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
|
|
180
|
+
|
|
181
|
+
export const prisma = globalForPrisma.prisma || new PrismaClient();
|
|
182
|
+
|
|
183
|
+
if (process.env.NODE_ENV !== "production") {
|
|
184
|
+
globalForPrisma.prisma = prisma;
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### When Not Needed
|
|
189
|
+
|
|
190
|
+
Do not add file-level comments to simple component files, route handlers, or utility files where the filename and exports make the purpose obvious.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Inline Comment Style
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
// Single-line comments use double slash with a space after
|
|
198
|
+
// Capitalize the first word. No trailing period for short comments
|
|
199
|
+
|
|
200
|
+
// Multi-line comments that explain a complex block
|
|
201
|
+
// should use consecutive single-line comments rather than
|
|
202
|
+
// block comments (/* */). This makes it easier to toggle
|
|
203
|
+
// individual lines during development.
|
|
204
|
+
|
|
205
|
+
/*
|
|
206
|
+
* Block comments are reserved for JSDoc on exported symbols
|
|
207
|
+
* and file-level documentation only.
|
|
208
|
+
*/
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Anti-Patterns
|
|
214
|
+
|
|
215
|
+
| Anti-Pattern | Problem | Correct Approach |
|
|
216
|
+
|---|---|---|
|
|
217
|
+
| Commenting every line | Noise, goes stale instantly | Write self-documenting code |
|
|
218
|
+
| `// Fixed in PR #142` | Temporal, use git history instead | Delete; `git blame` has this info |
|
|
219
|
+
| Commented-out code | Confusion about what is active | Delete; git has the history |
|
|
220
|
+
| `// This is a hack` without explanation | Unhelpful | Explain why the hack exists and when it can be removed |
|
|
221
|
+
| Missing JSDoc on public API | Users guess at behavior | Document all exported functions and types |
|
|
222
|
+
| `@author` tags | Redundant with git blame | Let version control track authorship |
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
_The best code reads like well-written prose. Comments are footnotes -- essential for context, distracting when overused._
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
# Component Patterns
|
|
2
|
+
|
|
3
|
+
Server and client component architecture for Next.js 15 App Router with composition, error boundaries, and loading states.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Philosophy
|
|
8
|
+
|
|
9
|
+
- **Server by default**: Every component is a Server Component unless it needs interactivity
|
|
10
|
+
- **Push client boundaries down**: Keep `"use client"` as close to the leaves as possible
|
|
11
|
+
- **Composition over props**: Use children and slots instead of deeply nested prop objects
|
|
12
|
+
- **Explicit boundaries**: Every async operation gets a Suspense boundary, every failure gets an Error Boundary
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Server vs Client Components
|
|
17
|
+
|
|
18
|
+
### Decision Tree
|
|
19
|
+
|
|
20
|
+
| Need | Component Type |
|
|
21
|
+
|---|---|
|
|
22
|
+
| Fetch data, access database | Server Component |
|
|
23
|
+
| Read from filesystem, access env vars | Server Component |
|
|
24
|
+
| Render static or async content | Server Component |
|
|
25
|
+
| useState, useEffect, useRef | Client Component (`"use client"`) |
|
|
26
|
+
| Event handlers (onClick, onChange) | Client Component |
|
|
27
|
+
| Browser APIs (localStorage, window) | Client Component |
|
|
28
|
+
| Third-party hooks (useForm, useSWR) | Client Component |
|
|
29
|
+
|
|
30
|
+
### Server Component (Default)
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// app/users/page.tsx -- Server Component (no directive needed)
|
|
34
|
+
import { prisma } from "@/lib/prisma";
|
|
35
|
+
import { UserCard } from "@/components/user-card";
|
|
36
|
+
|
|
37
|
+
export default async function UsersPage() {
|
|
38
|
+
const users = await prisma.user.findMany({
|
|
39
|
+
orderBy: { createdAt: "desc" },
|
|
40
|
+
take: 20,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<main className="container py-8">
|
|
45
|
+
<h1 className="text-2xl font-bold">Users</h1>
|
|
46
|
+
<div className="mt-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
47
|
+
{users.map((user) => (
|
|
48
|
+
<UserCard key={user.id} user={user} />
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
</main>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Client Component (Opt-In)
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// components/like-button.tsx
|
|
60
|
+
"use client";
|
|
61
|
+
|
|
62
|
+
import { useState, useTransition } from "react";
|
|
63
|
+
import { toggleLike } from "@/actions/posts";
|
|
64
|
+
|
|
65
|
+
interface LikeButtonProps {
|
|
66
|
+
postId: string;
|
|
67
|
+
initialLiked: boolean;
|
|
68
|
+
initialCount: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function LikeButton({ postId, initialLiked, initialCount }: LikeButtonProps) {
|
|
72
|
+
const [liked, setLiked] = useState(initialLiked);
|
|
73
|
+
const [count, setCount] = useState(initialCount);
|
|
74
|
+
const [isPending, startTransition] = useTransition();
|
|
75
|
+
|
|
76
|
+
function handleClick() {
|
|
77
|
+
setLiked(!liked);
|
|
78
|
+
setCount(liked ? count - 1 : count + 1);
|
|
79
|
+
startTransition(() => toggleLike(postId));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<button
|
|
84
|
+
onClick={handleClick}
|
|
85
|
+
disabled={isPending}
|
|
86
|
+
className="flex items-center gap-1.5"
|
|
87
|
+
aria-label={liked ? "Unlike post" : "Like post"}
|
|
88
|
+
>
|
|
89
|
+
<HeartIcon filled={liked} />
|
|
90
|
+
<span>{count}</span>
|
|
91
|
+
</button>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Composition Patterns
|
|
99
|
+
|
|
100
|
+
### Children Pattern
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
interface PageLayoutProps {
|
|
104
|
+
title: string;
|
|
105
|
+
description?: string;
|
|
106
|
+
actions?: React.ReactNode;
|
|
107
|
+
children: React.ReactNode;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function PageLayout({ title, description, actions, children }: PageLayoutProps) {
|
|
111
|
+
return (
|
|
112
|
+
<div className="container py-8">
|
|
113
|
+
<div className="flex items-center justify-between">
|
|
114
|
+
<div>
|
|
115
|
+
<h1 className="text-2xl font-bold">{title}</h1>
|
|
116
|
+
{description && <p className="text-muted-foreground mt-1">{description}</p>}
|
|
117
|
+
</div>
|
|
118
|
+
{actions && <div className="flex gap-2">{actions}</div>}
|
|
119
|
+
</div>
|
|
120
|
+
<div className="mt-8">{children}</div>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Compound Components
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
// components/card.tsx
|
|
130
|
+
interface CardProps {
|
|
131
|
+
children: React.ReactNode;
|
|
132
|
+
className?: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function Card({ children, className }: CardProps) {
|
|
136
|
+
return <div className={cn("rounded-lg border bg-card p-6", className)}>{children}</div>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function CardHeader({ children, className }: CardProps) {
|
|
140
|
+
return <div className={cn("mb-4", className)}>{children}</div>;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function CardTitle({ children, className }: { children: React.ReactNode; className?: string }) {
|
|
144
|
+
return <h3 className={cn("text-lg font-semibold", className)}>{children}</h3>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function CardContent({ children, className }: CardProps) {
|
|
148
|
+
return <div className={cn("text-sm", className)}>{children}</div>;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
Card.Header = CardHeader;
|
|
152
|
+
Card.Title = CardTitle;
|
|
153
|
+
Card.Content = CardContent;
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
// Usage
|
|
158
|
+
<Card>
|
|
159
|
+
<Card.Header>
|
|
160
|
+
<Card.Title>Monthly Revenue</Card.Title>
|
|
161
|
+
</Card.Header>
|
|
162
|
+
<Card.Content>
|
|
163
|
+
<RevenueChart data={data} />
|
|
164
|
+
</Card.Content>
|
|
165
|
+
</Card>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Render Props (Rare)
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// Use only when children pattern is insufficient
|
|
172
|
+
interface DataListProps<T> {
|
|
173
|
+
items: T[];
|
|
174
|
+
renderItem: (item: T, index: number) => React.ReactNode;
|
|
175
|
+
emptyState?: React.ReactNode;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function DataList<T>({ items, renderItem, emptyState }: DataListProps<T>) {
|
|
179
|
+
if (items.length === 0) {
|
|
180
|
+
return emptyState ?? <p className="text-muted-foreground">No items found.</p>;
|
|
181
|
+
}
|
|
182
|
+
return <ul className="divide-y">{items.map((item, i) => <li key={i}>{renderItem(item, i)}</li>)}</ul>;
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Props Design
|
|
189
|
+
|
|
190
|
+
### Required vs Optional
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
interface UserCardProps {
|
|
194
|
+
// Required: component cannot render without these
|
|
195
|
+
user: { id: string; name: string; email: string; avatarUrl?: string };
|
|
196
|
+
|
|
197
|
+
// Optional with defaults: common customizations
|
|
198
|
+
size?: "sm" | "md" | "lg";
|
|
199
|
+
showEmail?: boolean;
|
|
200
|
+
|
|
201
|
+
// Optional: extension points
|
|
202
|
+
className?: string;
|
|
203
|
+
actions?: React.ReactNode;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function UserCard({
|
|
207
|
+
user,
|
|
208
|
+
size = "md",
|
|
209
|
+
showEmail = true,
|
|
210
|
+
className,
|
|
211
|
+
actions,
|
|
212
|
+
}: UserCardProps) {
|
|
213
|
+
// ...
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Extending HTML Elements
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
import { type ComponentProps } from "react";
|
|
221
|
+
|
|
222
|
+
interface ButtonProps extends ComponentProps<"button"> {
|
|
223
|
+
variant?: "primary" | "secondary" | "destructive";
|
|
224
|
+
size?: "sm" | "md" | "lg";
|
|
225
|
+
isLoading?: boolean;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function Button({
|
|
229
|
+
variant = "primary",
|
|
230
|
+
size = "md",
|
|
231
|
+
isLoading,
|
|
232
|
+
disabled,
|
|
233
|
+
children,
|
|
234
|
+
className,
|
|
235
|
+
...props
|
|
236
|
+
}: ButtonProps) {
|
|
237
|
+
return (
|
|
238
|
+
<button
|
|
239
|
+
className={cn(buttonVariants({ variant, size }), className)}
|
|
240
|
+
disabled={disabled || isLoading}
|
|
241
|
+
{...props}
|
|
242
|
+
>
|
|
243
|
+
{isLoading && <Spinner className="mr-2" />}
|
|
244
|
+
{children}
|
|
245
|
+
</button>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Error Boundaries
|
|
253
|
+
|
|
254
|
+
### Route-Level Error Boundary
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
// app/dashboard/error.tsx
|
|
258
|
+
"use client";
|
|
259
|
+
|
|
260
|
+
interface ErrorProps {
|
|
261
|
+
error: Error & { digest?: string };
|
|
262
|
+
reset: () => void;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export default function DashboardError({ error, reset }: ErrorProps) {
|
|
266
|
+
return (
|
|
267
|
+
<div className="flex flex-col items-center justify-center gap-4 py-16" role="alert">
|
|
268
|
+
<h2 className="text-xl font-semibold">Something went wrong</h2>
|
|
269
|
+
<p className="text-muted-foreground max-w-md text-center">
|
|
270
|
+
An error occurred while loading the dashboard. Please try again.
|
|
271
|
+
</p>
|
|
272
|
+
<button onClick={reset} className="rounded-md bg-primary px-4 py-2 text-white">
|
|
273
|
+
Try again
|
|
274
|
+
</button>
|
|
275
|
+
</div>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Global Error Boundary
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// app/global-error.tsx
|
|
284
|
+
"use client";
|
|
285
|
+
|
|
286
|
+
export default function GlobalError({
|
|
287
|
+
error,
|
|
288
|
+
reset,
|
|
289
|
+
}: {
|
|
290
|
+
error: Error & { digest?: string };
|
|
291
|
+
reset: () => void;
|
|
292
|
+
}) {
|
|
293
|
+
return (
|
|
294
|
+
<html>
|
|
295
|
+
<body>
|
|
296
|
+
<div className="flex min-h-screen items-center justify-center">
|
|
297
|
+
<div className="text-center">
|
|
298
|
+
<h1 className="text-2xl font-bold">Application Error</h1>
|
|
299
|
+
<p className="mt-2">Something went wrong. Please refresh the page.</p>
|
|
300
|
+
<button onClick={reset} className="mt-4 rounded bg-blue-600 px-4 py-2 text-white">
|
|
301
|
+
Try again
|
|
302
|
+
</button>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
</body>
|
|
306
|
+
</html>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Suspense and Loading States
|
|
314
|
+
|
|
315
|
+
### Streaming with Suspense
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
// app/dashboard/page.tsx
|
|
319
|
+
import { Suspense } from "react";
|
|
320
|
+
import { RevenueChart } from "@/components/revenue-chart";
|
|
321
|
+
import { RecentOrders } from "@/components/recent-orders";
|
|
322
|
+
import { StatsSkeleton, TableSkeleton } from "@/components/skeletons";
|
|
323
|
+
|
|
324
|
+
export default function DashboardPage() {
|
|
325
|
+
return (
|
|
326
|
+
<div className="grid gap-6 lg:grid-cols-2">
|
|
327
|
+
<Suspense fallback={<StatsSkeleton />}>
|
|
328
|
+
<RevenueChart />
|
|
329
|
+
</Suspense>
|
|
330
|
+
<Suspense fallback={<TableSkeleton rows={5} />}>
|
|
331
|
+
<RecentOrders />
|
|
332
|
+
</Suspense>
|
|
333
|
+
</div>
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Route-Level Loading
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
// app/dashboard/loading.tsx
|
|
342
|
+
import { DashboardSkeleton } from "@/components/skeletons";
|
|
343
|
+
|
|
344
|
+
export default function DashboardLoading() {
|
|
345
|
+
return <DashboardSkeleton />;
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Skeleton Components
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
// components/skeletons.tsx
|
|
353
|
+
export function StatsSkeleton() {
|
|
354
|
+
return (
|
|
355
|
+
<div className="animate-pulse rounded-lg border p-6">
|
|
356
|
+
<div className="h-4 w-24 rounded bg-muted" />
|
|
357
|
+
<div className="mt-4 h-8 w-32 rounded bg-muted" />
|
|
358
|
+
</div>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function TableSkeleton({ rows = 5 }: { rows?: number }) {
|
|
363
|
+
return (
|
|
364
|
+
<div className="animate-pulse space-y-3">
|
|
365
|
+
{Array.from({ length: rows }).map((_, i) => (
|
|
366
|
+
<div key={i} className="h-12 rounded bg-muted" />
|
|
367
|
+
))}
|
|
368
|
+
</div>
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## Component File Structure
|
|
376
|
+
|
|
377
|
+
### Single Component File
|
|
378
|
+
|
|
379
|
+
```
|
|
380
|
+
components/
|
|
381
|
+
user-card.tsx # Component + types in one file
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Complex Component with Parts
|
|
385
|
+
|
|
386
|
+
```
|
|
387
|
+
components/
|
|
388
|
+
data-table/
|
|
389
|
+
data-table.tsx # Main component
|
|
390
|
+
columns.tsx # Column definitions
|
|
391
|
+
toolbar.tsx # Filter/search toolbar
|
|
392
|
+
pagination.tsx # Pagination controls
|
|
393
|
+
index.ts # Re-export: export { DataTable } from "./data-table"
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## Anti-Patterns
|
|
399
|
+
|
|
400
|
+
| Anti-Pattern | Problem | Correct Approach |
|
|
401
|
+
|---|---|---|
|
|
402
|
+
| `"use client"` at the top of every file | Sends everything to the client | Default to server, opt in to client |
|
|
403
|
+
| Fetching data in client components | Extra round trips, no streaming | Fetch in server components, pass as props |
|
|
404
|
+
| Prop drilling through many layers | Fragile, hard to refactor | Composition with children/slots or context |
|
|
405
|
+
| God components (500+ lines) | Untestable, hard to read | Split into composed sub-components |
|
|
406
|
+
| Inline functions in JSX | Re-created every render | Extract to named functions or useCallback |
|
|
407
|
+
| Missing Suspense boundaries | All-or-nothing loading | Wrap async components individually |
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
_Components are the atoms of your UI. Keep them small, composable, and server-rendered by default._
|