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,625 @@
|
|
|
1
|
+
# Component Patterns
|
|
2
|
+
|
|
3
|
+
Modern React component patterns for building maintainable, accessible, and reusable UI.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Philosophy
|
|
8
|
+
|
|
9
|
+
- **Composition over inheritance**: Build complex UIs from simple, composable pieces
|
|
10
|
+
- **Single responsibility**: Each component does one thing well
|
|
11
|
+
- **Props down, events up**: Data flows down, actions flow up
|
|
12
|
+
- **Accessibility first**: Build accessible components from the start
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Component Structure
|
|
17
|
+
|
|
18
|
+
### Basic Component
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
// components/ui/Button/Button.tsx
|
|
22
|
+
import { forwardRef, type ComponentPropsWithoutRef } from 'react';
|
|
23
|
+
import { cn } from '@/utils/cn';
|
|
24
|
+
|
|
25
|
+
interface ButtonProps extends ComponentPropsWithoutRef<'button'> {
|
|
26
|
+
variant?: 'primary' | 'secondary' | 'ghost' | 'destructive';
|
|
27
|
+
size?: 'sm' | 'md' | 'lg';
|
|
28
|
+
isLoading?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
32
|
+
({ className, variant = 'primary', size = 'md', isLoading, children, disabled, ...props }, ref) => {
|
|
33
|
+
return (
|
|
34
|
+
<button
|
|
35
|
+
ref={ref}
|
|
36
|
+
className={cn(
|
|
37
|
+
'inline-flex items-center justify-center rounded-md font-medium transition-colors',
|
|
38
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
|
|
39
|
+
'disabled:pointer-events-none disabled:opacity-50',
|
|
40
|
+
{
|
|
41
|
+
'bg-primary text-primary-foreground hover:bg-primary/90': variant === 'primary',
|
|
42
|
+
'bg-secondary text-secondary-foreground hover:bg-secondary/80': variant === 'secondary',
|
|
43
|
+
'hover:bg-accent hover:text-accent-foreground': variant === 'ghost',
|
|
44
|
+
'bg-destructive text-destructive-foreground hover:bg-destructive/90': variant === 'destructive',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
'h-8 px-3 text-sm': size === 'sm',
|
|
48
|
+
'h-10 px-4': size === 'md',
|
|
49
|
+
'h-12 px-6 text-lg': size === 'lg',
|
|
50
|
+
},
|
|
51
|
+
className
|
|
52
|
+
)}
|
|
53
|
+
disabled={disabled || isLoading}
|
|
54
|
+
{...props}
|
|
55
|
+
>
|
|
56
|
+
{isLoading && <Spinner className="mr-2 h-4 w-4" />}
|
|
57
|
+
{children}
|
|
58
|
+
</button>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
Button.displayName = 'Button';
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Component with Children
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// components/ui/Card/Card.tsx
|
|
70
|
+
import type { ReactNode } from 'react';
|
|
71
|
+
import { cn } from '@/utils/cn';
|
|
72
|
+
|
|
73
|
+
interface CardProps {
|
|
74
|
+
children: ReactNode;
|
|
75
|
+
className?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function Card({ children, className }: CardProps) {
|
|
79
|
+
return (
|
|
80
|
+
<div className={cn('rounded-lg border bg-card p-6 shadow-sm', className)}>
|
|
81
|
+
{children}
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Compound components
|
|
87
|
+
function CardHeader({ children, className }: CardProps) {
|
|
88
|
+
return <div className={cn('mb-4', className)}>{children}</div>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function CardTitle({ children, className }: CardProps) {
|
|
92
|
+
return <h3 className={cn('text-lg font-semibold', className)}>{children}</h3>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function CardContent({ children, className }: CardProps) {
|
|
96
|
+
return <div className={cn('', className)}>{children}</div>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function CardFooter({ children, className }: CardProps) {
|
|
100
|
+
return <div className={cn('mt-4 flex items-center gap-2', className)}>{children}</div>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Attach compound components
|
|
104
|
+
Card.Header = CardHeader;
|
|
105
|
+
Card.Title = CardTitle;
|
|
106
|
+
Card.Content = CardContent;
|
|
107
|
+
Card.Footer = CardFooter;
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Composition Patterns
|
|
113
|
+
|
|
114
|
+
### Compound Components
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// Usage of compound components
|
|
118
|
+
<Card>
|
|
119
|
+
<Card.Header>
|
|
120
|
+
<Card.Title>User Profile</Card.Title>
|
|
121
|
+
</Card.Header>
|
|
122
|
+
<Card.Content>
|
|
123
|
+
<p>{user.bio}</p>
|
|
124
|
+
</Card.Content>
|
|
125
|
+
<Card.Footer>
|
|
126
|
+
<Button variant="secondary">Edit</Button>
|
|
127
|
+
<Button>Save</Button>
|
|
128
|
+
</Card.Footer>
|
|
129
|
+
</Card>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Slot Pattern (Render Props)
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
// components/DataTable/DataTable.tsx
|
|
136
|
+
interface DataTableProps<T> {
|
|
137
|
+
data: T[];
|
|
138
|
+
columns: Column<T>[];
|
|
139
|
+
renderEmpty?: () => ReactNode;
|
|
140
|
+
renderLoading?: () => ReactNode;
|
|
141
|
+
isLoading?: boolean;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function DataTable<T>({
|
|
145
|
+
data,
|
|
146
|
+
columns,
|
|
147
|
+
renderEmpty = () => <p>No data</p>,
|
|
148
|
+
renderLoading = () => <Spinner />,
|
|
149
|
+
isLoading,
|
|
150
|
+
}: DataTableProps<T>) {
|
|
151
|
+
if (isLoading) {
|
|
152
|
+
return renderLoading();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!data.length) {
|
|
156
|
+
return renderEmpty();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<table>
|
|
161
|
+
<thead>
|
|
162
|
+
<tr>
|
|
163
|
+
{columns.map((col) => (
|
|
164
|
+
<th key={col.key}>{col.header}</th>
|
|
165
|
+
))}
|
|
166
|
+
</tr>
|
|
167
|
+
</thead>
|
|
168
|
+
<tbody>
|
|
169
|
+
{data.map((row, i) => (
|
|
170
|
+
<tr key={i}>
|
|
171
|
+
{columns.map((col) => (
|
|
172
|
+
<td key={col.key}>{col.render(row)}</td>
|
|
173
|
+
))}
|
|
174
|
+
</tr>
|
|
175
|
+
))}
|
|
176
|
+
</tbody>
|
|
177
|
+
</table>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Usage
|
|
182
|
+
<DataTable
|
|
183
|
+
data={users}
|
|
184
|
+
columns={[
|
|
185
|
+
{ key: 'name', header: 'Name', render: (user) => user.name },
|
|
186
|
+
{ key: 'email', header: 'Email', render: (user) => user.email },
|
|
187
|
+
{ key: 'actions', header: '', render: (user) => <UserActions user={user} /> },
|
|
188
|
+
]}
|
|
189
|
+
renderEmpty={() => <EmptyState icon={UsersIcon} message="No users found" />}
|
|
190
|
+
/>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Children as Function
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
// components/Disclosure/Disclosure.tsx
|
|
197
|
+
interface DisclosureProps {
|
|
198
|
+
children: (props: { isOpen: boolean; toggle: () => void }) => ReactNode;
|
|
199
|
+
defaultOpen?: boolean;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function Disclosure({ children, defaultOpen = false }: DisclosureProps) {
|
|
203
|
+
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
204
|
+
const toggle = () => setIsOpen((prev) => !prev);
|
|
205
|
+
|
|
206
|
+
return <>{children({ isOpen, toggle })}</>;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Usage
|
|
210
|
+
<Disclosure>
|
|
211
|
+
{({ isOpen, toggle }) => (
|
|
212
|
+
<div>
|
|
213
|
+
<button onClick={toggle}>
|
|
214
|
+
{isOpen ? 'Hide' : 'Show'} Details
|
|
215
|
+
</button>
|
|
216
|
+
{isOpen && <div>Hidden content here</div>}
|
|
217
|
+
</div>
|
|
218
|
+
)}
|
|
219
|
+
</Disclosure>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Props Patterns
|
|
225
|
+
|
|
226
|
+
### Polymorphic Components (as prop)
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
// components/ui/Box/Box.tsx
|
|
230
|
+
import { type ElementType, type ComponentPropsWithoutRef } from 'react';
|
|
231
|
+
|
|
232
|
+
type BoxProps<T extends ElementType> = {
|
|
233
|
+
as?: T;
|
|
234
|
+
children?: ReactNode;
|
|
235
|
+
} & Omit<ComponentPropsWithoutRef<T>, 'as'>;
|
|
236
|
+
|
|
237
|
+
export function Box<T extends ElementType = 'div'>({
|
|
238
|
+
as,
|
|
239
|
+
children,
|
|
240
|
+
...props
|
|
241
|
+
}: BoxProps<T>) {
|
|
242
|
+
const Component = as || 'div';
|
|
243
|
+
return <Component {...props}>{children}</Component>;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Usage
|
|
247
|
+
<Box as="section" className="p-4">Content</Box>
|
|
248
|
+
<Box as="article">Article content</Box>
|
|
249
|
+
<Box as="a" href="/about">Link styled as box</Box>
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Spreading Native Props
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
// Always extend native element props for flexibility
|
|
256
|
+
interface InputProps extends ComponentPropsWithoutRef<'input'> {
|
|
257
|
+
label: string;
|
|
258
|
+
error?: string;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function Input({ label, error, className, ...props }: InputProps) {
|
|
262
|
+
return (
|
|
263
|
+
<div>
|
|
264
|
+
<label>{label}</label>
|
|
265
|
+
<input
|
|
266
|
+
className={cn('input', error && 'input-error', className)}
|
|
267
|
+
{...props} // Spread all native input props
|
|
268
|
+
/>
|
|
269
|
+
{error && <span className="text-red-500">{error}</span>}
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// All native props work
|
|
275
|
+
<Input
|
|
276
|
+
label="Email"
|
|
277
|
+
type="email"
|
|
278
|
+
placeholder="Enter email"
|
|
279
|
+
required
|
|
280
|
+
autoComplete="email"
|
|
281
|
+
onChange={handleChange}
|
|
282
|
+
/>
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Default Props Pattern
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
// Use default values in destructuring
|
|
289
|
+
interface AvatarProps {
|
|
290
|
+
src?: string;
|
|
291
|
+
alt: string;
|
|
292
|
+
size?: 'sm' | 'md' | 'lg';
|
|
293
|
+
fallback?: string;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function Avatar({
|
|
297
|
+
src,
|
|
298
|
+
alt,
|
|
299
|
+
size = 'md', // Default value
|
|
300
|
+
fallback,
|
|
301
|
+
}: AvatarProps) {
|
|
302
|
+
const [error, setError] = useState(false);
|
|
303
|
+
|
|
304
|
+
const sizeClasses = {
|
|
305
|
+
sm: 'h-8 w-8',
|
|
306
|
+
md: 'h-10 w-10',
|
|
307
|
+
lg: 'h-14 w-14',
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
if (error || !src) {
|
|
311
|
+
return (
|
|
312
|
+
<div className={cn('rounded-full bg-gray-200', sizeClasses[size])}>
|
|
313
|
+
{fallback || alt.charAt(0).toUpperCase()}
|
|
314
|
+
</div>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return (
|
|
319
|
+
<img
|
|
320
|
+
src={src}
|
|
321
|
+
alt={alt}
|
|
322
|
+
className={cn('rounded-full object-cover', sizeClasses[size])}
|
|
323
|
+
onError={() => setError(true)}
|
|
324
|
+
/>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Controlled vs Uncontrolled
|
|
332
|
+
|
|
333
|
+
### Controlled Component
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
// Parent controls the state
|
|
337
|
+
interface ControlledInputProps {
|
|
338
|
+
value: string;
|
|
339
|
+
onChange: (value: string) => void;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function ControlledInput({ value, onChange }: ControlledInputProps) {
|
|
343
|
+
return (
|
|
344
|
+
<input
|
|
345
|
+
value={value}
|
|
346
|
+
onChange={(e) => onChange(e.target.value)}
|
|
347
|
+
/>
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Usage
|
|
352
|
+
const [email, setEmail] = useState('');
|
|
353
|
+
<ControlledInput value={email} onChange={setEmail} />
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### Uncontrolled Component
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
// Component manages its own state
|
|
360
|
+
export function UncontrolledInput({ defaultValue }: { defaultValue?: string }) {
|
|
361
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
362
|
+
|
|
363
|
+
const getValue = () => inputRef.current?.value;
|
|
364
|
+
|
|
365
|
+
return <input ref={inputRef} defaultValue={defaultValue} />;
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Hybrid (Controlled with Default)
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
// Support both controlled and uncontrolled usage
|
|
373
|
+
interface HybridInputProps {
|
|
374
|
+
value?: string;
|
|
375
|
+
defaultValue?: string;
|
|
376
|
+
onChange?: (value: string) => void;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export function HybridInput({ value, defaultValue, onChange }: HybridInputProps) {
|
|
380
|
+
const [internalValue, setInternalValue] = useState(defaultValue ?? '');
|
|
381
|
+
|
|
382
|
+
// Use controlled value if provided, otherwise use internal
|
|
383
|
+
const currentValue = value !== undefined ? value : internalValue;
|
|
384
|
+
|
|
385
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
386
|
+
const newValue = e.target.value;
|
|
387
|
+
if (value === undefined) {
|
|
388
|
+
setInternalValue(newValue);
|
|
389
|
+
}
|
|
390
|
+
onChange?.(newValue);
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
return <input value={currentValue} onChange={handleChange} />;
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
## Event Handling
|
|
400
|
+
|
|
401
|
+
### Event Handler Types
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
interface FormProps {
|
|
405
|
+
onSubmit: (data: FormData) => void;
|
|
406
|
+
// Use React event types
|
|
407
|
+
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
|
408
|
+
onChange?: React.ChangeEventHandler<HTMLInputElement>;
|
|
409
|
+
onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Preventing Default
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
function Form({ onSubmit }: FormProps) {
|
|
417
|
+
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
|
418
|
+
e.preventDefault();
|
|
419
|
+
// Process form
|
|
420
|
+
onSubmit(new FormData(e.currentTarget));
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
return <form onSubmit={handleSubmit}>{/* ... */}</form>;
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Event Delegation
|
|
428
|
+
|
|
429
|
+
```typescript
|
|
430
|
+
function List({ items, onItemClick }: { items: Item[]; onItemClick: (id: string) => void }) {
|
|
431
|
+
// Single handler on parent, not individual handlers
|
|
432
|
+
const handleClick = (e: React.MouseEvent<HTMLUListElement>) => {
|
|
433
|
+
const target = e.target as HTMLElement;
|
|
434
|
+
const item = target.closest('[data-item-id]');
|
|
435
|
+
if (item) {
|
|
436
|
+
onItemClick(item.getAttribute('data-item-id')!);
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
return (
|
|
441
|
+
<ul onClick={handleClick}>
|
|
442
|
+
{items.map((item) => (
|
|
443
|
+
<li key={item.id} data-item-id={item.id}>
|
|
444
|
+
{item.name}
|
|
445
|
+
</li>
|
|
446
|
+
))}
|
|
447
|
+
</ul>
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## Accessibility Patterns
|
|
455
|
+
|
|
456
|
+
### Keyboard Navigation
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
function Menu({ items }: { items: MenuItem[] }) {
|
|
460
|
+
const [focusIndex, setFocusIndex] = useState(0);
|
|
461
|
+
|
|
462
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
463
|
+
switch (e.key) {
|
|
464
|
+
case 'ArrowDown':
|
|
465
|
+
e.preventDefault();
|
|
466
|
+
setFocusIndex((i) => (i + 1) % items.length);
|
|
467
|
+
break;
|
|
468
|
+
case 'ArrowUp':
|
|
469
|
+
e.preventDefault();
|
|
470
|
+
setFocusIndex((i) => (i - 1 + items.length) % items.length);
|
|
471
|
+
break;
|
|
472
|
+
case 'Enter':
|
|
473
|
+
case ' ':
|
|
474
|
+
e.preventDefault();
|
|
475
|
+
items[focusIndex].action();
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
return (
|
|
481
|
+
<ul role="menu" onKeyDown={handleKeyDown}>
|
|
482
|
+
{items.map((item, index) => (
|
|
483
|
+
<li
|
|
484
|
+
key={item.id}
|
|
485
|
+
role="menuitem"
|
|
486
|
+
tabIndex={index === focusIndex ? 0 : -1}
|
|
487
|
+
>
|
|
488
|
+
{item.label}
|
|
489
|
+
</li>
|
|
490
|
+
))}
|
|
491
|
+
</ul>
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### ARIA Attributes
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
function ExpandableSection({ title, children }: { title: string; children: ReactNode }) {
|
|
500
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
501
|
+
const contentId = useId();
|
|
502
|
+
|
|
503
|
+
return (
|
|
504
|
+
<div>
|
|
505
|
+
<button
|
|
506
|
+
aria-expanded={isExpanded}
|
|
507
|
+
aria-controls={contentId}
|
|
508
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
509
|
+
>
|
|
510
|
+
{title}
|
|
511
|
+
<ChevronIcon className={isExpanded ? 'rotate-180' : ''} />
|
|
512
|
+
</button>
|
|
513
|
+
<div
|
|
514
|
+
id={contentId}
|
|
515
|
+
role="region"
|
|
516
|
+
aria-labelledby={contentId}
|
|
517
|
+
hidden={!isExpanded}
|
|
518
|
+
>
|
|
519
|
+
{children}
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### Focus Management
|
|
527
|
+
|
|
528
|
+
```typescript
|
|
529
|
+
function Modal({ isOpen, onClose, children }: ModalProps) {
|
|
530
|
+
const closeButtonRef = useRef<HTMLButtonElement>(null);
|
|
531
|
+
|
|
532
|
+
// Focus close button when modal opens
|
|
533
|
+
useEffect(() => {
|
|
534
|
+
if (isOpen) {
|
|
535
|
+
closeButtonRef.current?.focus();
|
|
536
|
+
}
|
|
537
|
+
}, [isOpen]);
|
|
538
|
+
|
|
539
|
+
// Trap focus inside modal
|
|
540
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
541
|
+
if (e.key === 'Escape') {
|
|
542
|
+
onClose();
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
if (!isOpen) return null;
|
|
547
|
+
|
|
548
|
+
return (
|
|
549
|
+
<div
|
|
550
|
+
role="dialog"
|
|
551
|
+
aria-modal="true"
|
|
552
|
+
onKeyDown={handleKeyDown}
|
|
553
|
+
>
|
|
554
|
+
<button ref={closeButtonRef} onClick={onClose}>
|
|
555
|
+
Close
|
|
556
|
+
</button>
|
|
557
|
+
{children}
|
|
558
|
+
</div>
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
---
|
|
564
|
+
|
|
565
|
+
## Using Radix UI Primitives
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
// Build accessible components on top of Radix
|
|
569
|
+
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
|
570
|
+
import { cn } from '@/utils/cn';
|
|
571
|
+
|
|
572
|
+
export function Dialog({ children, ...props }: DialogPrimitive.DialogProps) {
|
|
573
|
+
return <DialogPrimitive.Root {...props}>{children}</DialogPrimitive.Root>;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
export function DialogTrigger({ children, ...props }: DialogPrimitive.DialogTriggerProps) {
|
|
577
|
+
return <DialogPrimitive.Trigger asChild {...props}>{children}</DialogPrimitive.Trigger>;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export function DialogContent({ children, className, ...props }: DialogPrimitive.DialogContentProps) {
|
|
581
|
+
return (
|
|
582
|
+
<DialogPrimitive.Portal>
|
|
583
|
+
<DialogPrimitive.Overlay className="fixed inset-0 bg-black/50" />
|
|
584
|
+
<DialogPrimitive.Content
|
|
585
|
+
className={cn(
|
|
586
|
+
'fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2',
|
|
587
|
+
'rounded-lg bg-white p-6 shadow-lg',
|
|
588
|
+
className
|
|
589
|
+
)}
|
|
590
|
+
{...props}
|
|
591
|
+
>
|
|
592
|
+
{children}
|
|
593
|
+
</DialogPrimitive.Content>
|
|
594
|
+
</DialogPrimitive.Portal>
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Usage
|
|
599
|
+
<Dialog>
|
|
600
|
+
<DialogTrigger>
|
|
601
|
+
<Button>Open Dialog</Button>
|
|
602
|
+
</DialogTrigger>
|
|
603
|
+
<DialogContent>
|
|
604
|
+
<h2>Dialog Title</h2>
|
|
605
|
+
<p>Dialog content here</p>
|
|
606
|
+
</DialogContent>
|
|
607
|
+
</Dialog>
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
## Anti-Patterns
|
|
613
|
+
|
|
614
|
+
| Anti-Pattern | Problem | Correct Approach |
|
|
615
|
+
|--------------|---------|------------------|
|
|
616
|
+
| Props drilling | Hard to maintain | Use context or composition |
|
|
617
|
+
| Huge components | Hard to test, understand | Split into smaller components |
|
|
618
|
+
| Business logic in components | Not reusable, hard to test | Extract to hooks or services |
|
|
619
|
+
| Inline styles everywhere | No consistency | Use Tailwind or CSS modules |
|
|
620
|
+
| Missing key prop | Performance issues, bugs | Always provide stable keys |
|
|
621
|
+
| forwardRef forgotten | Can't access ref | Use forwardRef for reusable components |
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
625
|
+
_Components are the atoms of your UI. Keep them small, focused, and composable._
|