codecruise 0.1.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 +111 -0
- package/bin/codecruise.js +68 -0
- package/config/CLAUDE.md +107 -0
- package/config/agents/analyst.md +48 -0
- package/config/agents/architect-reviewer.md +161 -0
- package/config/agents/architect.md +119 -0
- package/config/agents/critic.md +63 -0
- package/config/agents/developer.md +96 -0
- package/config/agents/devops.md +81 -0
- package/config/agents/orchestrator.md +91 -0
- package/config/agents/planner.md +139 -0
- package/config/agents/retro.md +52 -0
- package/config/agents/reviewer.md +101 -0
- package/config/agents/security-reviewer.md +57 -0
- package/config/agents/stack/expo/AGENT.md +473 -0
- package/config/agents/stack/expo/rules/critical.md +427 -0
- package/config/agents/stack/expo/rules/native.md +455 -0
- package/config/agents/stack/expo/rules/navigation.md +445 -0
- package/config/agents/stack/expo/rules/performance.md +415 -0
- package/config/agents/stack/fastify/AGENT.md +397 -0
- package/config/agents/stack/fastify/rules/api-design.md +283 -0
- package/config/agents/stack/fastify/rules/critical.md +232 -0
- package/config/agents/stack/fastify/rules/queues.md +303 -0
- package/config/agents/stack/fastify/rules/security.md +384 -0
- package/config/agents/stack/index.yaml +48 -0
- package/config/agents/stack/nextjs/AGENT.md +421 -0
- package/config/agents/stack/nextjs/rules/components.md +413 -0
- package/config/agents/stack/nextjs/rules/critical.md +391 -0
- package/config/agents/stack/nextjs/rules/performance.md +403 -0
- package/config/agents/stack/nextjs/rules/styling.md +334 -0
- package/config/agents/stack/shared-ts/AGENT.md +384 -0
- package/config/agents/stack/shared-ts/rules/critical.md +315 -0
- package/config/agents/stack/shared-ts/rules/patterns.md +384 -0
- package/config/agents/stack/shared-ts/rules/zod.md +427 -0
- package/config/agents/tester.md +79 -0
- package/config/commands/architect-discuss.md +366 -0
- package/config/commands/architect-list.md +160 -0
- package/config/commands/architect-review.md +111 -0
- package/config/commands/architect.md +118 -0
- package/config/commands/compact.md +118 -0
- package/config/commands/companion.md +279 -0
- package/config/commands/dashboard.md +152 -0
- package/config/commands/doctor.md +227 -0
- package/config/commands/dogfood-report.md +101 -0
- package/config/commands/flags/run-autonomous.md +110 -0
- package/config/commands/flags/run-pause.md +80 -0
- package/config/commands/ingest.md +173 -0
- package/config/commands/init.md +128 -0
- package/config/commands/metrics.md +87 -0
- package/config/commands/parallel.md +320 -0
- package/config/commands/pause.md +55 -0
- package/config/commands/plan-review.md +130 -0
- package/config/commands/plan.md +216 -0
- package/config/commands/production-check.md +308 -0
- package/config/commands/refine.md +323 -0
- package/config/commands/resume.md +72 -0
- package/config/commands/retro.md +121 -0
- package/config/commands/retry.md +75 -0
- package/config/commands/role.md +310 -0
- package/config/commands/run.md +417 -0
- package/config/commands/scope.md +85 -0
- package/config/commands/setup-permissions.md +104 -0
- package/config/commands/skip.md +75 -0
- package/config/commands/spec-forge.md +213 -0
- package/config/commands/spec-help.md +194 -0
- package/config/commands/spec-patch.md +342 -0
- package/config/commands/spec-resolve.md +110 -0
- package/config/commands/spec-review.md +153 -0
- package/config/commands/status.md +114 -0
- package/config/commands/sync.md +131 -0
- package/config/commands/task.md +138 -0
- package/config/commands/verify.md +124 -0
- package/config/hooks/README.md +632 -0
- package/config/hooks/activity-log.sh +187 -0
- package/config/hooks/anti-rationalize.sh +52 -0
- package/config/hooks/capture-verification.sh +112 -0
- package/config/hooks/collect-metrics.sh +135 -0
- package/config/hooks/enforce-file-scope.sh +75 -0
- package/config/hooks/enforce-state-machine.sh +161 -0
- package/config/hooks/enforce-tdd.sh +180 -0
- package/config/hooks/format.sh +40 -0
- package/config/hooks/lib/activity-helpers.sh +162 -0
- package/config/hooks/lib/read-settings.sh +71 -0
- package/config/hooks/load-context-skills.sh +95 -0
- package/config/hooks/notify.sh +81 -0
- package/config/hooks/pre-commit.sample +35 -0
- package/config/hooks/protect-files.sh +63 -0
- package/config/hooks/track-agents.sh +41 -0
- package/config/hooks/track-commands.sh +37 -0
- package/config/hooks/track-enforcement.sh +44 -0
- package/config/hooks/track-ooda.sh +77 -0
- package/config/hooks/validate-commit-msg.sh +35 -0
- package/config/hooks/validate-plan.sh +213 -0
- package/config/hooks/verify-criteria.sh +46 -0
- package/config/hooks/verify-todo-completion.sh +140 -0
- package/config/rules/comments.md +25 -0
- package/config/rules/decision-rules.md +308 -0
- package/config/rules/hygiene.md +247 -0
- package/config/rules/pattern-detection.md +372 -0
- package/config/rules/profiles.md +193 -0
- package/config/rules/recovery.md +83 -0
- package/config/rules/scope-detection.md +213 -0
- package/config/rules/standards.md +127 -0
- package/config/rules/workflow.md +121 -0
- package/config/schemas.md +767 -0
- package/config/settings.json +195 -0
- package/config/skills/backend/SKILL.md +734 -0
- package/config/skills/database/SKILL.md +426 -0
- package/config/skills/frontend/SKILL.md +434 -0
- package/config/skills/git/SKILL.md +396 -0
- package/config/skills/index.yaml +36 -0
- package/config/skills/observability/SKILL.md +430 -0
- package/config/skills/package-dev/SKILL.md +498 -0
- package/config/skills/performance/SKILL.md +378 -0
- package/config/skills/resilience/SKILL.md +573 -0
- package/config/skills/testing/SKILL.md +398 -0
- package/config/skills/testing-patterns/SKILL.md +276 -0
- package/config/skills/typescript/SKILL.md +152 -0
- package/config/templates/CLAUDE.md +70 -0
- package/config/templates/README.md +117 -0
- package/config/templates/steering/adr-template.md +102 -0
- package/config/templates/steering/product.md +60 -0
- package/config/templates/steering/rfc-template.md +159 -0
- package/config/templates/steering/structure.md +146 -0
- package/config/templates/steering/tech.md +85 -0
- package/package.json +40 -0
- package/src/install.js +163 -0
- package/src/report.js +310 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: frontend
|
|
3
|
+
description: React, accessibility, and performance patterns
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
triggers:
|
|
6
|
+
- react
|
|
7
|
+
- component
|
|
8
|
+
- "*.tsx"
|
|
9
|
+
- accessibility
|
|
10
|
+
- a11y
|
|
11
|
+
- UI
|
|
12
|
+
- tailwind
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Frontend Skill
|
|
16
|
+
|
|
17
|
+
Comprehensive React, accessibility, and performance patterns.
|
|
18
|
+
|
|
19
|
+
## Quick Reference
|
|
20
|
+
|
|
21
|
+
### React Patterns
|
|
22
|
+
|
|
23
|
+
| ID | Rule | Priority |
|
|
24
|
+
|----|------|----------|
|
|
25
|
+
| react-1 | Use composition over prop drilling | CRITICAL |
|
|
26
|
+
| react-2 | Server Components by default, Client for interactivity | CRITICAL |
|
|
27
|
+
| react-3 | Colocate state with components that use it | HIGH |
|
|
28
|
+
| react-4 | Use discriminated unions for component states | HIGH |
|
|
29
|
+
| react-5 | Avoid useEffect for data fetching | HIGH |
|
|
30
|
+
| react-6 | Memoize only after measuring performance | MEDIUM |
|
|
31
|
+
| react-7 | Use React.lazy for code splitting | MEDIUM |
|
|
32
|
+
| react-8 | Prefer controlled inputs for forms | MEDIUM |
|
|
33
|
+
| react-9 | Use error boundaries for graceful failures | HIGH |
|
|
34
|
+
| react-10 | Keep components under 200 lines | MEDIUM |
|
|
35
|
+
|
|
36
|
+
### Accessibility (WCAG 2.1 AA)
|
|
37
|
+
|
|
38
|
+
| ID | Rule | Priority |
|
|
39
|
+
|----|------|----------|
|
|
40
|
+
| a11y-1 | Use semantic HTML elements | CRITICAL |
|
|
41
|
+
| a11y-2 | All images must have alt text | CRITICAL |
|
|
42
|
+
| a11y-3 | Interactive elements must be keyboard accessible | CRITICAL |
|
|
43
|
+
| a11y-4 | Maintain visible focus indicators | CRITICAL |
|
|
44
|
+
| a11y-5 | Color contrast ratio minimum 4.5:1 for text | CRITICAL |
|
|
45
|
+
| a11y-6 | Form inputs must have associated labels | CRITICAL |
|
|
46
|
+
| a11y-7 | Announce dynamic content with aria-live | HIGH |
|
|
47
|
+
| a11y-8 | Use skip links for main content | HIGH |
|
|
48
|
+
| a11y-9 | Support reduced motion preferences | HIGH |
|
|
49
|
+
| a11y-10 | Touch targets minimum 44x44 pixels | HIGH |
|
|
50
|
+
| a11y-11 | Provide text alternatives for icons | MEDIUM |
|
|
51
|
+
| a11y-12 | Use aria-describedby for error messages | MEDIUM |
|
|
52
|
+
| a11y-13 | Modal dialogs must trap focus | HIGH |
|
|
53
|
+
| a11y-14 | Use proper heading hierarchy (h1-h6) | HIGH |
|
|
54
|
+
| a11y-15 | Tables must have headers and captions | MEDIUM |
|
|
55
|
+
|
|
56
|
+
### Performance
|
|
57
|
+
|
|
58
|
+
| ID | Rule | Priority |
|
|
59
|
+
|----|------|----------|
|
|
60
|
+
| perf-1 | Lazy load below-the-fold content | HIGH |
|
|
61
|
+
| perf-2 | Use next/image for optimized images | HIGH |
|
|
62
|
+
| perf-3 | Avoid layout shift (CLS < 0.1) | HIGH |
|
|
63
|
+
| perf-4 | Minimize JavaScript bundle size | HIGH |
|
|
64
|
+
| perf-5 | Use CSS for animations, not JS | MEDIUM |
|
|
65
|
+
| perf-6 | Debounce expensive event handlers | MEDIUM |
|
|
66
|
+
| perf-7 | Virtualize long lists (>100 items) | MEDIUM |
|
|
67
|
+
| perf-8 | Preload critical assets | MEDIUM |
|
|
68
|
+
| perf-9 | Avoid blocking the main thread | HIGH |
|
|
69
|
+
| perf-10 | Use web workers for heavy computation | LOW |
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Critical Rules
|
|
74
|
+
|
|
75
|
+
### react-1: Composition Over Props
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
// BAD: Prop drilling
|
|
79
|
+
<Card showHeader showFooter showBorder variant="primary" size="lg" />
|
|
80
|
+
|
|
81
|
+
// GOOD: Composition
|
|
82
|
+
<Card>
|
|
83
|
+
<Card.Header>Title</Card.Header>
|
|
84
|
+
<Card.Body>Content</Card.Body>
|
|
85
|
+
<Card.Footer>Actions</Card.Footer>
|
|
86
|
+
</Card>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### react-2: Server vs Client Components
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
// Server Component (default) - data fetching
|
|
93
|
+
async function UserList() {
|
|
94
|
+
const users = await db.users.findMany();
|
|
95
|
+
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Client Component - interactivity
|
|
99
|
+
'use client'
|
|
100
|
+
function Counter() {
|
|
101
|
+
const [count, setCount] = useState(0);
|
|
102
|
+
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### react-4: Discriminated Unions for State
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
type State =
|
|
110
|
+
| { status: 'idle' }
|
|
111
|
+
| { status: 'loading' }
|
|
112
|
+
| { status: 'success'; data: User[] }
|
|
113
|
+
| { status: 'error'; error: Error };
|
|
114
|
+
|
|
115
|
+
function UserList() {
|
|
116
|
+
const [state, setState] = useState<State>({ status: 'idle' });
|
|
117
|
+
|
|
118
|
+
switch (state.status) {
|
|
119
|
+
case 'loading': return <Spinner />;
|
|
120
|
+
case 'error': return <Error message={state.error.message} />;
|
|
121
|
+
case 'success': return <List users={state.data} />;
|
|
122
|
+
default: return <Button onClick={load}>Load</Button>;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### a11y-1: Semantic HTML
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
// BAD
|
|
131
|
+
<div onClick={handleClick}>Click me</div>
|
|
132
|
+
<div class="nav">...</div>
|
|
133
|
+
|
|
134
|
+
// GOOD
|
|
135
|
+
<button onClick={handleClick}>Click me</button>
|
|
136
|
+
<nav aria-label="Main navigation">...</nav>
|
|
137
|
+
<main id="main-content">...</main>
|
|
138
|
+
<article>...</article>
|
|
139
|
+
<aside>...</aside>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### a11y-2: Image Alt Text
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
// Decorative image
|
|
146
|
+
<img src="decoration.svg" alt="" role="presentation" />
|
|
147
|
+
|
|
148
|
+
// Informative image
|
|
149
|
+
<img src="chart.png" alt="Sales increased 25% in Q4 2024" />
|
|
150
|
+
|
|
151
|
+
// Linked image
|
|
152
|
+
<a href="/profile">
|
|
153
|
+
<img src="avatar.jpg" alt="View John's profile" />
|
|
154
|
+
</a>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### a11y-3: Keyboard Navigation
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
// Ensure all interactive elements are focusable
|
|
161
|
+
<button>Focusable by default</button>
|
|
162
|
+
|
|
163
|
+
// Custom interactive element needs tabIndex
|
|
164
|
+
<div
|
|
165
|
+
role="button"
|
|
166
|
+
tabIndex={0}
|
|
167
|
+
onClick={handleClick}
|
|
168
|
+
onKeyDown={(e) => {
|
|
169
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
170
|
+
handleClick();
|
|
171
|
+
}
|
|
172
|
+
}}
|
|
173
|
+
>
|
|
174
|
+
Custom button
|
|
175
|
+
</div>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### a11y-4: Focus Indicators
|
|
179
|
+
|
|
180
|
+
```css
|
|
181
|
+
/* Never remove focus outline without replacement */
|
|
182
|
+
/* BAD */
|
|
183
|
+
button:focus { outline: none; }
|
|
184
|
+
|
|
185
|
+
/* GOOD */
|
|
186
|
+
button:focus-visible {
|
|
187
|
+
outline: 2px solid var(--focus-color);
|
|
188
|
+
outline-offset: 2px;
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### a11y-6: Form Labels
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
// Explicit label
|
|
196
|
+
<label htmlFor="email">Email</label>
|
|
197
|
+
<input id="email" type="email" />
|
|
198
|
+
|
|
199
|
+
// Implicit label
|
|
200
|
+
<label>
|
|
201
|
+
Email
|
|
202
|
+
<input type="email" />
|
|
203
|
+
</label>
|
|
204
|
+
|
|
205
|
+
// With error
|
|
206
|
+
<label htmlFor="email">Email</label>
|
|
207
|
+
<input
|
|
208
|
+
id="email"
|
|
209
|
+
type="email"
|
|
210
|
+
aria-invalid={!!error}
|
|
211
|
+
aria-describedby={error ? "email-error" : undefined}
|
|
212
|
+
/>
|
|
213
|
+
{error && <span id="email-error" role="alert">{error}</span>}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### a11y-9: Reduced Motion
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
// CSS
|
|
220
|
+
@media (prefers-reduced-motion: reduce) {
|
|
221
|
+
*, *::before, *::after {
|
|
222
|
+
animation-duration: 0.01ms !important;
|
|
223
|
+
transition-duration: 0.01ms !important;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// React
|
|
228
|
+
const prefersReducedMotion = window.matchMedia(
|
|
229
|
+
'(prefers-reduced-motion: reduce)'
|
|
230
|
+
).matches;
|
|
231
|
+
|
|
232
|
+
<motion.div
|
|
233
|
+
animate={{ opacity: 1 }}
|
|
234
|
+
transition={{ duration: prefersReducedMotion ? 0 : 0.3 }}
|
|
235
|
+
/>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### a11y-13: Modal Focus Trap
|
|
239
|
+
|
|
240
|
+
```tsx
|
|
241
|
+
function Modal({ isOpen, onClose, children }) {
|
|
242
|
+
const modalRef = useRef<HTMLDivElement>(null);
|
|
243
|
+
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
if (!isOpen) return;
|
|
246
|
+
|
|
247
|
+
const focusableElements = modalRef.current?.querySelectorAll(
|
|
248
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
249
|
+
);
|
|
250
|
+
const firstElement = focusableElements?.[0] as HTMLElement;
|
|
251
|
+
const lastElement = focusableElements?.[focusableElements.length - 1] as HTMLElement;
|
|
252
|
+
|
|
253
|
+
firstElement?.focus();
|
|
254
|
+
|
|
255
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
256
|
+
if (e.key === 'Escape') onClose();
|
|
257
|
+
if (e.key === 'Tab') {
|
|
258
|
+
if (e.shiftKey && document.activeElement === firstElement) {
|
|
259
|
+
e.preventDefault();
|
|
260
|
+
lastElement?.focus();
|
|
261
|
+
} else if (!e.shiftKey && document.activeElement === lastElement) {
|
|
262
|
+
e.preventDefault();
|
|
263
|
+
firstElement?.focus();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
269
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
270
|
+
}, [isOpen, onClose]);
|
|
271
|
+
|
|
272
|
+
if (!isOpen) return null;
|
|
273
|
+
|
|
274
|
+
return (
|
|
275
|
+
<div role="dialog" aria-modal="true" ref={modalRef}>
|
|
276
|
+
{children}
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### perf-1: Lazy Loading
|
|
283
|
+
|
|
284
|
+
```tsx
|
|
285
|
+
// Dynamic imports
|
|
286
|
+
const HeavyChart = dynamic(() => import('./HeavyChart'), {
|
|
287
|
+
loading: () => <Skeleton />,
|
|
288
|
+
ssr: false,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Intersection Observer for images
|
|
292
|
+
function LazyImage({ src, alt }) {
|
|
293
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
294
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
295
|
+
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
const observer = new IntersectionObserver(
|
|
298
|
+
([entry]) => {
|
|
299
|
+
if (entry.isIntersecting) {
|
|
300
|
+
setIsVisible(true);
|
|
301
|
+
observer.disconnect();
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
{ rootMargin: '100px' }
|
|
305
|
+
);
|
|
306
|
+
if (ref.current) observer.observe(ref.current);
|
|
307
|
+
return () => observer.disconnect();
|
|
308
|
+
}, []);
|
|
309
|
+
|
|
310
|
+
return (
|
|
311
|
+
<div ref={ref}>
|
|
312
|
+
{isVisible && <img src={src} alt={alt} />}
|
|
313
|
+
</div>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### perf-3: Avoid Layout Shift
|
|
319
|
+
|
|
320
|
+
```tsx
|
|
321
|
+
// Reserve space for images
|
|
322
|
+
<div style={{ aspectRatio: '16/9' }}>
|
|
323
|
+
<Image src={src} alt={alt} fill />
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
// Reserve space for dynamic content
|
|
327
|
+
<div style={{ minHeight: '200px' }}>
|
|
328
|
+
{isLoading ? <Skeleton /> : <Content />}
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
// Use CSS containment
|
|
332
|
+
.card {
|
|
333
|
+
contain: layout style;
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### perf-7: Virtualize Long Lists
|
|
338
|
+
|
|
339
|
+
```tsx
|
|
340
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
341
|
+
|
|
342
|
+
function VirtualList({ items }) {
|
|
343
|
+
const parentRef = useRef<HTMLDivElement>(null);
|
|
344
|
+
|
|
345
|
+
const virtualizer = useVirtualizer({
|
|
346
|
+
count: items.length,
|
|
347
|
+
getScrollElement: () => parentRef.current,
|
|
348
|
+
estimateSize: () => 50,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
return (
|
|
352
|
+
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
|
|
353
|
+
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
|
|
354
|
+
{virtualizer.getVirtualItems().map((virtualItem) => (
|
|
355
|
+
<div
|
|
356
|
+
key={virtualItem.key}
|
|
357
|
+
style={{
|
|
358
|
+
position: 'absolute',
|
|
359
|
+
top: 0,
|
|
360
|
+
transform: `translateY(${virtualItem.start}px)`,
|
|
361
|
+
height: `${virtualItem.size}px`,
|
|
362
|
+
}}
|
|
363
|
+
>
|
|
364
|
+
{items[virtualItem.index]}
|
|
365
|
+
</div>
|
|
366
|
+
))}
|
|
367
|
+
</div>
|
|
368
|
+
</div>
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## Form Patterns
|
|
376
|
+
|
|
377
|
+
```tsx
|
|
378
|
+
// react-hook-form + zod
|
|
379
|
+
const schema = z.object({
|
|
380
|
+
email: z.string().email('Invalid email'),
|
|
381
|
+
password: z.string().min(8, 'Min 8 characters'),
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
function LoginForm() {
|
|
385
|
+
const { register, handleSubmit, formState: { errors } } = useForm({
|
|
386
|
+
resolver: zodResolver(schema),
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
return (
|
|
390
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
391
|
+
<label htmlFor="email">Email</label>
|
|
392
|
+
<input
|
|
393
|
+
id="email"
|
|
394
|
+
type="email"
|
|
395
|
+
aria-invalid={!!errors.email}
|
|
396
|
+
aria-describedby={errors.email ? 'email-error' : undefined}
|
|
397
|
+
{...register('email')}
|
|
398
|
+
/>
|
|
399
|
+
{errors.email && (
|
|
400
|
+
<span id="email-error" role="alert">{errors.email.message}</span>
|
|
401
|
+
)}
|
|
402
|
+
|
|
403
|
+
<button type="submit">Login</button>
|
|
404
|
+
</form>
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## Error Boundaries
|
|
412
|
+
|
|
413
|
+
```tsx
|
|
414
|
+
'use client'
|
|
415
|
+
|
|
416
|
+
export default function Error({
|
|
417
|
+
error,
|
|
418
|
+
reset,
|
|
419
|
+
}: {
|
|
420
|
+
error: Error & { digest?: string };
|
|
421
|
+
reset: () => void;
|
|
422
|
+
}) {
|
|
423
|
+
useEffect(() => {
|
|
424
|
+
console.error(error);
|
|
425
|
+
}, [error]);
|
|
426
|
+
|
|
427
|
+
return (
|
|
428
|
+
<div role="alert">
|
|
429
|
+
<h2>Something went wrong</h2>
|
|
430
|
+
<button onClick={reset}>Try again</button>
|
|
431
|
+
</div>
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
```
|